Merge main

This commit is contained in:
Chris Tate
2026-10-20 12:31:50 -05:00
390 changed files with 21804 additions and 648 deletions
+1 -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>
@@ -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,4 +1,7 @@
- **Streams and readline:** Stream chunks are Buffers or strings.
`NodeJS.ReadableStream` and `NodeJS.WritableStream`, and `stream.Readable` and `stream.Writable` under `@types/node`, accept every stream: when a program passes file, zlib, or `node:stream` streams through them, child-process pipes and `process.stdout` and `process.stderr` join those streams as `Readable` and `Writable` instances.
`readable.pipe(process.stdout)` and `readable.pipe(process.stderr)` compile and, as in Node, do not end the process stream.
A `pipeline` stage that arrives as an untyped value must be a native stream at run time; other values throw a not-supported error.
`Readable.from` accepts arrays, Sets, Map and Set iterators, generators, async generators, and class iterables and async iterables whose values are strings or Buffers, and in JavaScript any iterable or async iterable of them; a stream that mixes strings and Buffers is unsupported.
In JavaScript, a sync iterator value with a `then` method that is not a promise, and an object whose `[Symbol.asyncIterator]` or `[Symbol.iterator]` method is a generator method, are unsupported.
Object-mode streams of other values (including `Readable.from` over a `Map`, whose values are entry arrays) are unsupported.
@@ -11,6 +14,7 @@
`stream.compose` accepts two or more streams, or one `Duplex`; functions, iterables, and web streams as stages are unsupported, and the `ERR_INVALID_ARG_VALUE` message for a stream that is no longer readable or writable names the stream's class instead of printing the stream.
`pipeline` from `node:stream/promises` accepts a trailing `{ signal, end }` object, and `finished` accepts `readable`, `writable`, `error`, `cleanup`, and `signal` options, with boolean literals for the flags.
Stream constructors accept a `signal` option in an options object literal, and `stream.addAbortSignal` accepts any stream.
A `transform` or `flush` option callback whose completion callback types `data` as `any`, as @types/node's `TransformCallback` does, may pass a Buffer, a string, or nothing; any other value throws a TypeError where the callback is called, instead of Node's `ERR_INVALID_ARG_TYPE` stream error.
`node:readline` and `node:readline/promises` (also reached as `readline.promises`) interfaces, from `createInterface(options)` or `createInterface(input, output, completer, terminal)`, read `process.stdin` or a readable stream: `question`, `prompt`, `write` (with a key object literal of `name`, `ctrl`, `meta`, and `shift`), `pause`, `resume`, `'line'`/`'close'`/`'pause'`/`'resume'`/`'error'`/`'history'`/`'SIGINT'`/`'SIGTSTP'`/`'SIGCONT'` listeners, and `for await` compile.
The `history`, `historySize`, `removeHistoryDuplicates`, `completer`, `tabSize`, and `escapeCodeTimeout` options are validated as in Node.
With `terminal: true`, or with no `terminal` option and `process.stdout` as the output when it is a TTY, the interface runs Node's terminal mode: the editing keys, the kill ring, undo and redo, history recording and recall, Tab completion through the completer, Ctrl+C, Ctrl+D, and Ctrl+Z, `TERM=dumb`, and the same escape sequences written to the output.
@@ -16,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.
@@ -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,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.
+25 -25
View File
@@ -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
@@ -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"
}
},
{
@@ -7,9 +7,9 @@
"tierItemCount": 3454,
"tiers": {
"static": {
"audit-partial": 1190,
"verify-gap": 2136,
"replace-refusal": 41,
"audit-partial": 1197,
"verify-gap": 2130,
"replace-refusal": 40,
"classify": 2,
"implement": 85
}
@@ -34235,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"
]
}
}
},
@@ -54264,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"
]
}
}
},
@@ -54389,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"
]
}
}
},
@@ -54914,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"
]
}
}
},
@@ -54939,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"
]
}
}
},
@@ -55339,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"
]
}
}
},
@@ -55499,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"
]
}
}
},
@@ -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
@@ -52298,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"
@@ -53649,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"
},
@@ -75620,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"
@@ -77661,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"
},
@@ -77776,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"
},
@@ -78259,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"
},
@@ -78282,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"
},
@@ -78650,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"
},
@@ -78800,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"
},
@@ -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"],
@@ -138,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"],
@@ -246,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"] },
+72 -8
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";
@@ -3066,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 {
@@ -6414,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,
@@ -6664,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;
+8 -1
View File
@@ -1081,8 +1081,15 @@ function copyEntrySync(
throw new Error(`Cannot overwrite non-directory '${dest}' with directory '${src}'.`);
if (!destExists) fs.mkdirSync(dest);
for (const item of fs.readdirSync(src)) {
const srcItem = path.join(src, item);
const destItem = path.join(dest, item);
copyEntrySync(state, path.join(src, item), destItem, fs.existsSync(destItem), false);
// fs-extra's copyDirItem checks every entry against its destination:
// a destination symlink back into the source tree must not let the
// overwrite below unlink the source's own file.
const destItemStat = statOrNullSync(destItem);
if (destItemStat !== null && sameNode(fs.statSync(srcItem), destItemStat))
throw new Error("Source and destination must not be the same.");
copyEntrySync(state, srcItem, destItem, destItemStat !== null, false);
}
if (!destExists) fs.chmodSync(dest, srcStat.mode);
return;
@@ -16,6 +16,7 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures {
fileHandle: features.fileHandle,
fetch: features.fetch,
...(features.eventTarget ? { eventTarget: true } : {}),
...(features.wasm ? { wasm: true } : {}),
...(features.warningReplace ? { warningReplace: true } : {}),
...(features.stdioExtended ? { stdioExtended: true } : {}),
zlib: features.zlib,
@@ -31,6 +32,7 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures {
bigint: features.bigint,
searchParams: features.searchParams,
qs: features.qs,
dynBuiltinClass: features.dynBuiltinClass,
parseArgs: features.parseArgs,
stream: features.stream,
net: features.net,
@@ -45,5 +47,6 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures {
tlsCa: features.tlsCa,
cipher: features.cipher,
webcrypto: features.webcrypto,
perfLoop: features.perfLoop,
};
}
@@ -42,6 +42,7 @@ export async function compilePackedLibrary(
bigint: options.bigint ?? false,
searchParams: options.searchParams ?? false,
qs: false,
dynBuiltinClass: false,
parseArgs: false,
stream: false,
net: false,
@@ -55,6 +56,7 @@ export async function compilePackedLibrary(
tls: false,
cipher: false,
webcrypto: false,
perfLoop: false,
tlsCa: false,
};
const selection = await loadRuntimePack({
@@ -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,
@@ -312,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()`);
}
@@ -730,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
@@ -873,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);
}
@@ -771,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" },
@@ -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)`);
@@ -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":
@@ -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",
@@ -727,6 +740,8 @@ 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",
"process.kill": "scr_process_kill_named",
"process.killNum": "scr_process_kill",
// fs.promises (scr_bytes_io.c / scr_lib.c): the promise forms settle
@@ -859,6 +874,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",
@@ -1110,6 +1143,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",
@@ -1424,6 +1458,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",
@@ -1596,6 +1633,7 @@ export const USES_TIMERS_LIB_FNS = new Set<string>([
"readable.opReduce",
"readable.resume",
"readable.pipe",
"readable.pipeGeneral",
"readable.unpipe",
"writable.write",
"writable.writeStr",
@@ -56,6 +56,8 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV
host.declare(`declare void @scr_child_stream_thunk0(ptr, ptr)`);
} else if (param.kind === "union") {
adapter = host.childDataThunkFor(param);
} else if (param.kind === "dyn") {
adapter = host.childDataDynThunkFor("bytes");
} else {
adapter = "scr_child_stream_thunk_bytes";
host.declare(`declare void @scr_child_stream_thunk_bytes(ptr, ptr)`);
@@ -78,7 +80,8 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV
if (e.fn === "stream.onDataStr") {
host.usesTimers = true;
const cbT = e.args[1]!.type;
if (cbT.kind !== "func" || cbT.params[0]?.kind !== "string") {
const strParam = cbT.kind === "func" ? cbT.params[0]?.kind : undefined;
if (strParam !== "string" && strParam !== "dyn") {
throw new InternalCompilerError(
"llvm emitter bug: stream.onDataStr callback not a string func",
);
@@ -87,10 +90,12 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV
const cb = host.emitExpr(e.args[1]!);
const once = host.emitExpr(e.args[2]!);
host.toHeap(cb);
host.declare(`declare void @scr_child_stream_thunk_str(ptr, ptr)`);
let adapter = "scr_child_stream_thunk_str";
if (strParam === "dyn") adapter = host.childDataDynThunkFor("str");
else host.declare(`declare void @scr_child_stream_thunk_str(ptr, ptr)`);
host.declare(`declare void @scr_child_stream_on_data_str(ptr, ptr, ptr, i1 zeroext)`);
B.line(
`call void @scr_child_stream_on_data_str(ptr ${s0.name}, ptr ${cb.name}, ptr @scr_child_stream_thunk_str, i1 ${once.name})`,
`call void @scr_child_stream_on_data_str(ptr ${s0.name}, ptr ${cb.name}, ptr @${adapter}, i1 ${once.name})`,
);
return { name: "", type: e.type };
}
@@ -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;
@@ -46,6 +49,7 @@ export interface NativeLinkFeatures {
bigint: boolean;
searchParams: boolean;
qs: boolean;
dynBuiltinClass: boolean;
parseArgs: boolean;
stream: boolean;
net: boolean;
@@ -61,6 +65,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. */
@@ -118,6 +118,8 @@ export interface CcOptions {
* native fetch AbortSignal built on the same engine: compiles
* scr_event_target.c. */
eventTarget?: boolean;
/** The WebAssembly API: compiles scr_wasm.c. */
wasm?: boolean;
/** SCR_WARNING_REPLACE: process.emitWarning replacement support. */
warningReplace?: boolean;
/** SCR_STDIO_EXTENDED: stdin encoding, string data listeners, resize. */
@@ -150,6 +152,11 @@ export interface CcOptions {
* line (escape-only programs ride the always-linked component encoder
* and never flip this). */
qs?: boolean;
/** The program uses EventEmitter or a stream class as an untyped
* constructor value (a constructor function's base): compiles
* scr_dyn_builtin_class.c, which also references the inspection,
* dyn-invoke and symbol units. */
dynBuiltinClass?: boolean;
/** The program uses native util parsing, comparison, or styling. Optional
* utility units also require their symbol, inspection, and warning features. */
parseArgs?: boolean;
@@ -214,6 +221,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,6 +238,15 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
(u) => u.opts.parseArgs && u.opts.inspect && u.opts.dynAsync && u.opts.dynInvoke && u.regex,
"scr_util_callable.c",
),
unit(
(u) =>
u.opts.dynBuiltinClass &&
u.opts.inspect &&
u.opts.dynInvoke &&
u.opts.symbol &&
(u.opts.emitter || u.opts.workers),
"scr_dyn_builtin_class.c",
),
unit((u) => u.opts.stream, "scr_stream.c", "scr_fs_stream.c"),
// The zlib stream classes reference both the zlib engine and the
// stream unit.
@@ -82,6 +82,7 @@ export interface RuntimeFeatureSet extends NativeLinkFeatures {
workers: boolean;
youngNursery: boolean;
eventTarget: boolean;
wasm: boolean;
dynTier: boolean;
warningReplace: boolean;
stdioExtended: boolean;
@@ -272,6 +273,7 @@ export function effectiveRuntimeFeatures(
workers: features.workers === true,
youngNursery: features.youngNursery === true,
eventTarget: features.eventTarget === true,
wasm: features.wasm === true,
dynTier: features.dynTier === true,
warningReplace: features.warningReplace === true,
stdioExtended: features.stdioExtended === true,
@@ -107,6 +107,7 @@ const features = {
bigint: false,
searchParams: false,
qs: false,
dynBuiltinClass: false,
parseArgs: false,
stream: false,
net: false,
@@ -121,6 +122,7 @@ const features = {
tlsCa: false,
cipher: false,
webcrypto: false,
perfLoop: false,
};
test.each([
@@ -44,6 +44,7 @@ const BASE: NativeLinkFeatures = {
bigint: false,
searchParams: false,
qs: false,
dynBuiltinClass: false,
parseArgs: false,
stream: false,
net: false,
@@ -58,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,6 +50,7 @@ export async function warmNativeCaches(
bigint: false,
searchParams: false,
qs: false,
dynBuiltinClass: false,
parseArgs: false,
stream: false,
net: false,
@@ -63,6 +64,7 @@ export async function warmNativeCaches(
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 ?? ""})`,
});
}
@@ -740,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,6 +45,7 @@ const native: EarlyExecutableNativeFeatures = {
bigint: false,
searchParams: false,
qs: false,
dynBuiltinClass: false,
parseArgs: false,
stream: false,
net: false,
@@ -59,6 +60,7 @@ const native: EarlyExecutableNativeFeatures = {
tlsCa: false,
cipher: false,
webcrypto: false,
perfLoop: false,
};
async function fixture(): Promise<{
@@ -56,6 +56,7 @@ export interface EarlyExecutableNativeFeatures {
fileHandle: boolean;
fetch: boolean;
eventTarget?: boolean;
wasm?: boolean;
warningReplace?: boolean;
stdioExtended?: boolean;
zlib: boolean;
@@ -71,6 +72,7 @@ export interface EarlyExecutableNativeFeatures {
bigint: boolean;
searchParams: boolean;
qs: boolean;
dynBuiltinClass: boolean;
parseArgs: boolean;
stream: boolean;
net: boolean;
@@ -83,6 +85,7 @@ export interface EarlyExecutableNativeFeatures {
nodeTest: boolean;
tls: boolean;
cipher: boolean;
perfLoop: boolean;
tlsCa: boolean;
webcrypto: boolean;
}
@@ -189,6 +192,7 @@ const BOOLEAN_NATIVE_KEYS = [
"bigint",
"searchParams",
"qs",
"dynBuiltinClass",
"parseArgs",
"stream",
"net",
@@ -203,6 +207,7 @@ const BOOLEAN_NATIVE_KEYS = [
"tlsCa",
"cipher",
"webcrypto",
"perfLoop",
] as const satisfies readonly (keyof EarlyExecutableNativeFeatures)[];
function validNativeFeatures(value: unknown): value is EarlyExecutableNativeFeatures {
@@ -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,
@@ -104,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;
@@ -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;
}
}
@@ -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
@@ -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";
@@ -225,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]!,
@@ -233,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,
@@ -1,5 +1,5 @@
import { isBuiltinClassInstance } from "./receiver-types.js";
import { strLit } from "../../../ir/build.js";
import { numLit, strLit } from "../../../ir/build.js";
import * as ts from "../../ts7/adapter.js";
import { type Lowerer } from "../lowerer.js";
import { locOf } from "../../program.js";
@@ -192,6 +192,25 @@ export function lowerBuiltinExtraProperty(
// +1-or-NULL runtime pair.
if (kind === "child" && (name === "stdin" || name === "stdout" || name === "stderr")) {
const receiver = lowerer.lowerExpr(expr.expression);
// Under the general stream representation (general-streams.ts) the
// declared stream type maps to a runtime stream class: the read
// answers the child's bridged stream (one per stdio slot, so repeated
// reads keep identity), fed by the same pipe.
const general = lowerer.generalNodeStreams ? generalChildStdioType(lowerer, expr, name) : null;
if (general) {
const type: IrType = {
kind: "union",
unionId: lowerer.unions.intern([general, { kind: "nullT" }]),
};
const read: IrExpr = {
kind: "libCall",
fn: "child.stdioStream",
args: [receiver, numLit(name === "stdin" ? 0 : name === "stdout" ? 1 : 2, loc)],
type,
loc,
};
return lowerer.maybeNarrow(read, expr);
}
const streamType = name === "stdin" ? CHILDWRITER_T : CHILDSTREAM_T;
const type: IrType = {
kind: "union",
@@ -259,3 +278,21 @@ export function lowerBuiltinExtraProperty(
}
return null;
}
/** The runtime stream class a child stdio read produces under the general
* stream representation, or null when the declared type keeps the stdio
* handle (the fallback declarations' ChildStdin). */
function generalChildStdioType(
lowerer: Lowerer,
expr: ts.PropertyAccessExpression,
name: "stdin" | "stdout" | "stderr",
): IrType | null {
const className = name === "stdin" ? "%Writable" : "%Readable";
const mapped = lowerer.mapTypeOf(lowerer.typeOf(expr));
if (!mapped) return null;
const arms =
mapped.kind === "union" ? (lowerer.unions.get(mapped.unionId)?.arms ?? []) : [mapped];
return arms.some((arm) => arm.kind === "object" && arm.className === className)
? { kind: "object", className }
: null;
}
@@ -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.
@@ -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) &&
@@ -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))
@@ -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[] = [];
@@ -1,6 +1,8 @@
/* Identifier references in expression position: lowerExprInner's identifier branch and its helpers. */
import { cjsMemberAliasValueOf } from "../cjs-exports-object.js";
import { builtinClassValue, builtinMemberClassKind } from "../builtins/builtin-class-dyn.js";
import * as ts from "../../ts7/adapter.js";
import { InternalCompilerError } from "../../../errors.js";
import * as posix from "node:path/posix";
import { type Lowerer, PoisonError } from "../lowerer.js";
import {
@@ -21,6 +23,7 @@ import {
import { lowerNodeModuleIdentifier } from "../lower-node-module.js";
import { lowerCjsExportsObjectValue } from "../cjs-exports-object.js";
import { captureContextArguments } from "../function-context.js";
import { argumentsOwner, typedArgumentsCandidate } from "../typed-arguments.js";
import {
isCjsExportTableLiteral,
isCjsJsFile,
@@ -96,7 +99,18 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc:
// type and lowerLambda declared the local.
if (expr.text === "arguments") {
const binding = captureContextArguments(lowerer.fnStack);
if (binding) return { kind: "varRef", localId: binding.id, type: DYN, loc };
if (binding && lowerer.ctx.pureTuples.has(binding.id)) {
throw new InternalCompilerError("lowerer bug: forwarded-only 'arguments' read as a value");
}
if (binding) return { kind: "varRef", localId: binding.id, type: binding.type, loc };
const owner = argumentsOwner(expr);
if (owner && typedArgumentsCandidate(owner)) {
lowerer.unsupported(
"SC1090",
expr,
"'arguments' in a function expression whose value is not asserted to one fixed-arity signature (assert it, as in `function () { … } as F`, and the function lowers against that signature)",
);
}
}
// A compiler-projected adapter as a VALUE: the adapter has no
// runtime function object (its calls rewrite directly) — call it
@@ -539,6 +553,10 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc:
// the same identity-token story as stdlib globals above (the
// harness adds worker_threads.Worker to its identity Set).
if (isJsSourceFile(expr.getSourceFile())) {
// EventEmitter and the stream classes are the runtime's untyped
// constructors, so constructor functions can inherit from them.
const classKind = builtinMemberClassKind(bi.module, bi.member);
if (classKind !== null) return builtinClassValue(classKind, loc);
// A function member is a function value that fences when called.
if (builtinModuleFnOf(lowerer, bi.module, bi.member))
return builtinFunctionFenceValue(lowerer, bi, expr, loc);
@@ -0,0 +1,271 @@
/* Read-modify-write targets for computed record keys: `r[k] op= v`, `r[k]++` and `r[k] ??= v`. */
import * as ts from "../../ts7/adapter.js";
import {
BOOL,
F64,
STRING,
typeEquals,
type IrExpr,
type IrStmt,
type SrcLoc,
} from "../../../ir/ir.js";
import { isJsSourceFile } from "../../program.js";
import { constituentTypes } from "../../ts7/checker.js";
import type { FieldTarget } from "../lower-exprs.js";
import type { Lowerer } from "../lowerer.js";
export type KeyedFieldTarget = FieldTarget & { container: "recordKeyed" };
/** The property names a computed key's type allows: one or more string
* literals (a literal union such as `"sent" | "received"`, enum string
* members, or a type parameter bound to one inside a generic instance).
* Null for any other key type. */
export function literalFieldKeys(lowerer: Lowerer, keyNode: ts.Expression): string[] | null {
let t: ts.Type = lowerer.typeOf(keyNode);
if (t.flags & ts.TypeFlags.TypeParameter) {
const bound = lowerer.typeParamTsResolver(t);
if (!bound) return null;
t = bound;
}
const parts = t.flags & ts.TypeFlags.Union ? constituentTypes(t) : [t];
const keys: string[] = [];
for (const part of parts) {
if (!part.isStringLiteralType()) return null;
if (!keys.includes(part.value)) keys.push(part.value);
}
return keys.length > 0 ? keys : null;
}
/** Does `r[k]` read and write through declared record storage by key? A
* checker-only test (nothing lowers), so a caller can route before it
* evaluates the receiver: TypeScript sources only, a record whose declared
* fields include every name the key's literal type allows, or a pure
* index-signature record with a string key. */
export function isKeyedFieldAccess(lowerer: Lowerer, access: ts.ElementAccessExpression): boolean {
if (
access.questionDotToken ||
isJsSourceFile(access.getSourceFile()) ||
lowerer.isProcessEnv(access.expression)
)
return false;
const keys = literalFieldKeys(lowerer, access.argumentExpression);
if (!keys && !(lowerer.typeOf(access.argumentExpression).flags & ts.TypeFlags.StringLike))
return false;
const receiver = lowerer.mapTypeOf(lowerer.typeOf(access.expression));
if (receiver?.kind !== "record") return false;
const shape = lowerer.shapes.get(receiver.shapeId);
if (!shape || shape.tuple) return false;
if (shape.fields.length === 0) return shape.indexValue !== undefined;
return keys !== null && keys.every((k) => shape.fields.some((f) => f.name === k));
}
/** The target of a read-modify-write through `r[k]`, or null when
* isKeyedFieldAccess declines. A literal-union key dispatches over the
* declared fields it names, which must share one type; a string key on a
* pure index-signature record reads and writes its entries. The receiver
* and key may arrive already lowered (and saved) by the caller; otherwise
* both lower here, unsaved. A caller that evaluates the target more than
* once saves `obj` and then `key`, the JavaScript evaluation order. With
* `numeric`, a missing index-signature entry reads as NaN (every numeric
* update applies ToNumber to it) instead of a value armed with undefined. */
export function keyedFieldTarget(
lowerer: Lowerer,
access: ts.ElementAccessExpression,
numeric: boolean,
lowered?: { obj: IrExpr; key: IrExpr },
): KeyedFieldTarget | null {
if (!isKeyedFieldAccess(lowerer, access)) return null;
const keyNode = access.argumentExpression;
const keys = literalFieldKeys(lowerer, keyNode);
const obj = lowered?.obj ?? lowerer.lowerExpr(access.expression);
if (obj.type.kind !== "record") {
lowerer.unsupported(
"SC1090",
access.expression,
`updating a computed key of a '${lowerer.fmt(obj.type)}' value (narrow the receiver to its record type first)`,
);
}
const shapeId = obj.type.shapeId;
const shape = lowerer.shapes.get(shapeId)!;
// A single literal spelled at the site (or an identifier whose type is
// that one literal) names the field statically: identifier evaluation
// is pure, so skipping it matches JavaScript.
const indexed = shape.fields.length === 0;
const staticKey =
!indexed && keys?.length === 1 && (ts.isStringLiteralLike(keyNode) || ts.isIdentifier(keyNode));
const key = staticKey ? null : (lowered?.key ?? lowerer.lowerExpr(keyNode));
if (key && key.type.kind !== "string") {
lowerer.unsupported(
"SC1090",
keyNode,
`updating a record through a '${lowerer.fmt(key.type)}' key (convert the key to a string first)`,
);
}
if (keys && !indexed) {
const fields = keys.map((k) => shape.fields.find((f) => f.name === k));
const first = fields[0];
if (!first || fields.some((f) => !f || !typeEquals(f.type, first.type))) {
lowerer.unsupported(
"SC1090",
access,
`updating '${lowerer.fmt(obj.type)}' through a key whose fields are not all stored with one type (${keys.map((k) => `'${k}'`).join(", ")}; switch over the key and update each field)`,
);
}
return {
container: "recordKeyed",
obj,
shapeId,
field: keys[0]!,
keys,
key,
fieldType: first.type,
};
}
if (!indexed || !shape.indexValue) {
lowerer.unsupported(
"SC1090",
access,
`updating '${lowerer.fmt(obj.type)}' through a runtime key (the stored record declares fields)`,
);
}
return {
container: "recordKeyed",
obj,
shapeId,
field: "",
keys: null,
key,
fieldType: shape.indexValue,
...(numeric && shape.indexValue.kind === "f64" ? { missNaN: true as const } : {}),
};
}
/** Does `key` name `field`? */
function keyIs(key: IrExpr, field: string, loc: SrcLoc): IrExpr {
return {
kind: "strEq",
negated: false,
left: key,
right: { kind: "strLit", value: field, type: STRING, loc },
type: BOOL,
loc,
};
}
/** A key outside its literal type (smuggled through a cast) names no
* declared field: a fixed-shape record cannot add the property, so the
* update throws the TypeError of a keyed write miss. */
function keyMiss(key: IrExpr, target: KeyedFieldTarget, loc: SrcLoc): IrExpr {
const message: IrExpr = {
kind: "strConcat",
left: {
kind: "strConcat",
left: { kind: "strLit", value: "Cannot add property '", type: STRING, loc },
right: key,
type: STRING,
loc,
},
right: { kind: "strLit", value: "' to a fixed-shape object", type: STRING, loc },
type: STRING,
loc,
};
return {
kind: "libCall",
fn: "error.nodeThrow",
args: [
{ kind: "numLit", value: 1, type: F64, loc },
{ kind: "strLit", value: "", type: STRING, loc },
message,
],
type: target.fieldType,
loc,
};
}
/** The current value of a keyed target. */
export function keyedFieldRead(lowerer: Lowerer, target: KeyedFieldTarget, loc: SrcLoc): IrExpr {
const { obj, shapeId, key, fieldType } = target;
if (!target.keys) {
const type = target.missNaN
? fieldType
: fieldType.kind === "dyn"
? fieldType
: lowerer.withUndefinedArmOf(fieldType);
if (!type || !key) throw new Error("keyed index-signature target without a key or read type");
return {
kind: "recordKeyGet",
obj,
shapeId,
key,
...(target.missNaN ? { missNaN: true as const } : {}),
type,
loc,
};
}
const get = (field: string): IrExpr => ({
kind: "recordGet",
obj,
shapeId,
field,
type: fieldType,
loc,
});
if (!key) return get(target.keys[0]!);
return target.keys.reduceRight<IrExpr>(
(otherwise, field) => ({
kind: "ternary",
cond: keyIs(key, field, loc),
then: get(field),
else_: otherwise,
type: fieldType,
loc,
}),
keyMiss(key, target, loc),
);
}
/** Store `value` (already of the target's field type) through a keyed
* target. A multi-key dispatch references the value once per field, so a
* value that is not a plain local is saved first. */
export function keyedFieldWrite(
lowerer: Lowerer,
target: KeyedFieldTarget,
value: IrExpr,
loc: SrcLoc,
): IrStmt {
const { obj, shapeId, key } = target;
if (!target.keys) {
if (!key) throw new Error("keyed index-signature target without a key");
return { kind: "recordKeySet", obj, shapeId, key, value, loc };
}
if (!key) return { kind: "recordSet", obj, shapeId, field: target.keys[0]!, value, loc };
const prefix: IrStmt[] = [];
let stored = value;
if (value.kind !== "varRef") {
const local = lowerer.declareHiddenLocal("%keyedValue", value.type);
prefix.push({ kind: "varDecl", localId: local.id, init: value, loc });
stored = { kind: "varRef", localId: local.id, type: value.type, loc };
}
const chain = target.keys.reduceRight<IrStmt[]>(
(otherwise, field) => [
{
kind: "if",
cond: keyIs(key, field, loc),
then: [{ kind: "recordSet", obj, shapeId, field, value: stored, loc }],
else_: otherwise,
loc,
},
],
[{ kind: "exprStmt", expr: keyMiss(key, target, loc), loc }],
);
return { kind: "block", body: [...prefix, ...chain], loc };
}
/** Save a keyed target's key after its receiver, so a multi-key dispatch
* reads and writes the field the key named when it was evaluated. */
export function saveKeyedFieldKey(
target: FieldTarget,
save: (value: IrExpr, name: string) => IrExpr,
): void {
if (target.container === "recordKeyed" && target.key) target.key = save(target.key, "%keyedKey");
}
@@ -26,6 +26,7 @@ import { jsBindingHasOpenWrites } from "../lower-stmts.js";
import { narrowClassUnion, narrowStoredClassValue, structuralNarrowing } from "../class-unions.js";
import { checkedClassAssertion, checkedClassUnionAssertion } from "../class-assertions.js";
import { isOptionalProcessStreamProperty } from "../builtins/process.js";
import { requestGeneralStreamSurface } from "../general-streams.js";
import { literalValues } from "../../literal-values.js";
import { literalUnionArm } from "../../union-discriminants.js";
import { checkedArmExtraction } from "../coercions/unions.js";
@@ -1186,6 +1187,56 @@ function classInstanceOrAbsent(lowerer: Lowerer, type: IrType): boolean {
);
}
/** `Object.fromEntries(...) as T` with T a declared record (the generic
* `transform<T extends Record<string, unknown>>(t: T): T` idiom): the
* entries build a pure index-signature record, which has no layout in
* common with T's struct. Node keeps the object itself, with exactly the
* keys and the order the entries gave it. The result of fromEntries is a
* fresh object nothing else references, so the checked-dynamic bridge,
* which keeps keys and their order the same way `JSON.parse(text) as T`
* does, cannot be told apart from the original object; a value outside
* T throws the catchable TypeError of every checked cast. The entries'
* values must be scalars or checked-dynamic values, which the bridge
* passes by value or by identity; native arrays and records would be
* copied. Other sources may be aliased, where a rebuilt object would sever
* mutation, so they keep the exact-shape fence. */
function freshEntriesRecordAssertion(
lowerer: Lowerer,
expr: ts.AsExpression | ts.TypeAssertion,
inner: IrExpr,
target: IrType | null,
): IrExpr | null {
if (inner.type.kind !== "record" || target?.kind !== "record") return null;
const source = lowerer.shapes.get(inner.type.shapeId);
const dest = lowerer.shapes.get(target.shapeId);
if (!source?.indexValue || source.tuple || source.fields.length > 0) return null;
if (!dest || dest.tuple || dest.indexValue || shapeHasAccessorSlots(dest)) return null;
// Native arrays and records in the entries would be copied on the way
// through the checked-dynamic bridge, severing their identity: only
// slots of scalars or checked-dynamic values pass.
if (extractionCopiesReference(lowerer, source.indexValue)) return null;
let operand: ts.Expression = expr.expression;
while (ts.isParenthesizedExpression(operand)) operand = operand.expression;
if (
!ts.isCallExpression(operand) ||
!ts.isPropertyAccessExpression(operand.expression) ||
operand.expression.name.text !== "fromEntries" ||
!lowerer.isStdlibGlobal(operand.expression.expression, "Object")
)
return null;
if (!lowerer.dynConvertible(inner.type)) return null;
if (
!canDynCheckTo(
target,
(id) => lowerer.shapes.get(id),
(id) => lowerer.unions.get(id),
)
)
return null;
const loc = locOf(expr);
return { kind: "dynCheck", value: lowerer.coerceToExpected(inner, DYN), type: target, loc };
}
/** `e as T` and the old-style assertion `<T>e` — one node shape (both
* carry `.type` and `.expression`), one lowering. */
export function lowerAsExpression(
@@ -1237,12 +1288,14 @@ export function lowerAsExpression(
// the requested shape; an assertion does not bypass runtime checks.
const narrowed = lowerer.mapTypeOf(lowerer.typeOf(expr.expression));
const bridged =
narrowed &&
(narrowed.kind === "f64" ||
narrowed.kind === "bool" ||
narrowed.kind === "string" ||
narrowed.kind === "object" ||
narrowed.kind === "dyn");
(narrowed &&
(narrowed.kind === "f64" ||
narrowed.kind === "bool" ||
narrowed.kind === "string" ||
narrowed.kind === "object" ||
narrowed.kind === "dyn")) ||
// `catch (e: any)`: the binding reads as its checked-dynamic value.
(narrowed === null && (lowerer.typeOf(expr.expression).flags & ts.TypeFlags.Any) !== 0);
if (!bridged) lowerer.unsupported("SC1063", expr);
}
const inner = lowerer.lowerExpr(expr.expression);
@@ -1323,6 +1376,8 @@ export function lowerAsExpression(
) {
return lowerer.coerceInto(expr, inner, target);
}
const rebuilt = freshEntriesRecordAssertion(lowerer, expr, inner, target);
if (rebuilt) return rebuilt;
// `job as Job & { startedAt: number }` on a record: the asserted
// record refines the stored one, so the assertion keeps the object
// (a width copy would sever writes through the asserted view).
@@ -1376,6 +1431,12 @@ export function lowerAsExpression(
return inner; // erasure, unchanged
}
// `value as never` is the escape hatch that makes an unknown value fit
// any slot (`tagSuccess(value as never)`). No value has type never, so
// checking against its placeholder layout would reject every value.
// Erase it instead: the consuming slot applies the same validated
// extraction an unknown value gets wherever the checker admits it.
if (expr.type.kind === ts.SyntaxKind.NeverKeyword) return inner;
const targetTs = lowerer.checker.getTypeFromTypeNode(expr.type);
const target = lowerer.mapTypeOf(targetTs) ?? dynFallbackType(lowerer, expr.type, targetTs);
if (!target) lowerer.badType(expr.type, targetTs);
@@ -1450,6 +1511,9 @@ export function lowerAsExpression(
if (DYN_HANDLE_KINDS.has(target.kind)) {
return { kind: "dynCheck", value: inner, type: target, loc: locOf(expr) };
}
// `value as NodeJS.ReadableStream`: the general stream classes take
// the checked cast (general-streams.ts).
requestGeneralStreamSurface(lowerer, expr);
lowerer.unsupported(
"SC1090",
expr,
@@ -18,6 +18,11 @@ import { arrayNumberRead, arrayValueRead, arrayValueStore } from "../array-value
import { fenceNodeModuleMutation } from "../lower-node-module.js";
import { globalSymbolKey } from "./global-symbols.js";
import { tryLowerExpression } from "./try-lower-expression.js";
import {
type KeyedFieldTarget,
keyedFieldTarget,
saveKeyedFieldKey,
} from "./keyed-field-targets.js";
import {
eventAccessorRead,
eventAccessorWrite,
@@ -147,6 +152,7 @@ export function lowerShortCircuitAssignment(lowerer: Lowerer, expr: ts.BinaryExp
} else {
const receiver = save(lowerer.lowerExpr(target.expression), "%shortCircuitReceiver");
let key = lowerer.lowerExpr(target.argumentExpression);
let keyed: KeyedFieldTarget | null = null;
if (receiver.type.kind === "dyn") {
key = save(
{
@@ -245,6 +251,17 @@ export function lowerShortCircuitAssignment(lowerer: Lowerer, expr: ts.BinaryExp
value: lowerer.coerceToExpected(value, indexValue),
loc,
});
} else if (
receiver.type.kind === "record" &&
(keyed = keyedFieldTarget(lowerer, target, false, { obj: receiver, key }))
) {
// `totals[side] ??= v` through a literal-union key: the read and
// the write dispatch over the declared fields the key can name.
const field = keyed;
saveKeyedFieldKey(field, save);
storage = field.fieldType;
read = lowerer.fieldGetExpr(field, locOf(target), target);
write = (value) => lowerer.fieldSetStmt(field, value, loc, target);
} else {
lowerer.unsupported(
"SC1090",
@@ -1,3 +1,5 @@
import { isSafeToDiscard } from "./evaluation-safety.js";
import { completeFuncValueArgs } from "../call-arguments.js";
import { dynUndefinedExpr } from "../../../ir/build.js";
import * as ts from "../../ts7/adapter.js";
import { DYN, STRING, UNDEFINED_T, VOID, isUnitType, typeEquals } from "../../../ir/ir.js";
@@ -429,7 +431,22 @@ export function lowerOptionalChain(
if (dotNode === expr && ts.isCallExpression(expr)) {
if (narrowed.kind !== "func") lowerer.badType(recvNode, lowerer.typeOf(recvNode));
const params = narrowed.params;
// Forwarded fixed tuples (`f?.(...args)`) complete into the slots.
if (expr.arguments.nodes.some(ts.isSpreadElement)) {
const completed = completeFuncValueArgs(lowerer, expr, narrowed, loc);
const call: IrExpr = {
kind: "callValue",
callee: recvRef,
args: completed,
type: narrowed.ret,
loc,
};
return lowerer.finishOptionalChain(expr, id, receiver, call, loc);
}
const args = expr.arguments.nodes.map((a, i) => lowerer.lowerExprExpecting(a, params[i]));
// Effect-free surplus arguments (a dropped `void` slot) are ignored.
while (narrowed.rest !== true && args.length > params.length && isSafeToDiscard(args.at(-1)!))
args.pop();
const body: IrExpr = { kind: "callValue", callee: recvRef, args, type: narrowed.ret, loc };
return lowerer.finishOptionalChain(expr, id, receiver, body, loc);
}
@@ -1,6 +1,10 @@
/* Property reads (`a.b`): lowerExprInner's property-access branch, intrinsic and union properties, field reads and targets, and match-group reads. */
import { staticallySelected } from "../static-typeof.js";
import * as ts from "../../ts7/adapter.js";
import { lowerHistogramProperty } from "../builtins/performance.js";
import { staticFunctionNameRead } from "../static-function-names.js";
import { typedArgumentsArity } from "../typed-arguments.js";
import { isThisParameter } from "../call-signatures.js";
import { localPrivateBrandClass } from "../private-brands.js";
import { type Lowerer, PoisonError, jsFuncNameOf, neverTaintedJsType, own } from "../lowerer.js";
import {
@@ -156,6 +160,7 @@ import { errorPropertyRead } from "../error-methods.js";
import { isSafeToRepeat } from "./evaluation-safety.js";
import { isZlibStreamMember, lowerZlibStreamBytesWritten } from "../builtins/compression.js";
import { isFsStreamMember, lowerFsStreamProperty } from "../builtins/fs-streams.js";
import { keyedFieldRead } from "./keyed-field-targets.js";
/** True when a property access names an ABSTRACT property declaration
* (`abstract p: number` — a PropertyDeclaration, not an accessor).
@@ -256,6 +261,23 @@ export function lowerPropertyAccessExpr(
const navigator = lowerNavigatorMember(lowerer, expr, NODE_COMPAT_VERSION);
if (navigator) return navigator;
}
// `fn.name` on a function parameter of a generic instance keyed by
// its argument's static name (call-site specialization).
{
const staticName = staticFunctionNameRead(lowerer, expr);
if (staticName !== null) return { kind: "strLit", value: staticName, type: STRING, loc };
}
// An IntervalHistogram's readings (min, max, mean, stddev, count, exceeds).
{
const reading = lowerHistogramProperty(lowerer, expr);
if (reading) return reading;
}
// `.length` of a forwarded fixed tuple (a typed `arguments`, a tuple
// rest parameter, an alias of either): the signature's arity.
if (expr.name.text === "length" && !expr.questionDotToken) {
const arity = typedArgumentsArity(lowerer, expr.expression);
if (arity !== null) return { kind: "numLit", value: arity, type: F64, loc };
}
// `require("./x").member` over a program module's export table: the
// module's %init at this position, then the member read exactly like
// a `const m = require("./x")` binding's (cjs-inline-require.ts).
@@ -574,9 +596,12 @@ export function lowerPropertyAccessExpr(
p.dotDotDotToken === undefined,
);
if (fixedArity) {
// A `this` parameter is type-only: callers never pass it.
return {
kind: "numLit",
value: (fn as ts.FunctionLikeDeclaration).parameters.nodes.length,
value: (fn as ts.FunctionLikeDeclaration).parameters.nodes.filter(
(p) => !isThisParameter(p),
).length,
type: F64,
loc,
};
@@ -1121,6 +1146,23 @@ export function lowerPropertyAccessExpr(
);
}
}
// A stdlib-declared member (`message` of an interface extending Error)
// on a catch binding narrowed to a structural type: the binding reads
// as its checked-dynamic value, so the member does too.
if (!expr.questionDotToken && lowerer.caughtLocalOf(expr.expression)) {
const recv = lowerer.lowerExpr(expr.expression);
if (recv.kind === "caughtToDyn") {
const read: IrExpr = {
kind: "dynKeyGet",
key: { kind: "strLit", value: expr.name.text, type: STRING, loc: locOf(expr.name) },
value: recv,
type: DYN,
loc,
};
const declared = lowerer.mapTypeOf(lowerer.typeOf(expr));
return declared && declared.kind !== "void" ? lowerer.coerceToExpected(read, declared) : read;
}
}
// The lib fence's PROPERTY chokepoint: a stdlib-declared member that
// no lowering above claimed ([1,2].entries, Math.SQRT2, Promise.all,
// re.exec as a value, ...) reports SC2020 here.
@@ -3512,6 +3554,7 @@ export function fieldGetExpr(
);
if (checked) target = { ...target, obj: checked };
}
if (target.container === "recordKeyed") return keyedFieldRead(lowerer, target, loc);
if (target.container === "dynamic")
return lowerer.coerceInto(
blame,
@@ -1,5 +1,6 @@
/* instanceof and in, including private-name and runtime-key in. */
import * as ts from "../../ts7/adapter.js";
import { builtinClassInstanceOf, builtinClassKind } from "../builtins/builtin-class-dyn.js";
import {
checkedPrivateIn,
localPrivateBrandClass,
@@ -25,7 +26,7 @@ import {
typeEquals,
typeKey,
} from "../../../ir/ir.js";
import { refuseWebAssemblyUse } from "../builtins/webassembly.js";
import { lowerWebAssemblyInstanceof, refuseWebAssemblyUse } from "../builtins/webassembly.js";
import { lowerSocketInstanceOf } from "../lower-server.js";
import { builtinClassRefOf } from "../builtins/class-values.js";
import { lowerZlibInstanceOf } from "../builtins/compression.js";
@@ -62,6 +63,8 @@ import { nullProtoRecordShape, recordNullProtoTest } from "../null-prototype.js"
* Folding is limited to side-effect-free operands — dropping a computed
* operand would skip its effects, so those are rejected instead. */
export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc: SrcLoc): IrExpr {
const webAssembly = lowerWebAssemblyInstanceof(lowerer, expr);
if (webAssembly !== null) return webAssembly;
refuseWebAssemblyUse(lowerer, expr.right);
// `x instanceof net.Socket` over a union with a netSocket arm — the
// h2 compat 'connect' narrowing (lower-server.ts): a union tag test.
@@ -471,6 +474,22 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc
const value = lowerer.coerceInto(expr.left, lowerer.lowerExpr(expr.left), DYN);
return checkedClassInstanceOf(lowerer, value, constructor, loc);
}
// `this instanceof Glob` in a JavaScript constructor function: the
// function's prototype against the value's prototype chain.
if (
constructor?.type.kind === "func" &&
isJsSourceFile(expr.getSourceFile()) &&
lowerer.dynConvertible(constructor.type)
) {
const value = lowerer.coerceInto(expr.left, lowerer.lowerExpr(expr.left), DYN);
return {
kind: "libCall",
fn: "dyn.functionInstanceOf",
args: [value, { kind: "dynFrom", value: constructor, type: DYN, loc }],
type: BOOL,
loc,
};
}
lowerer.unsupported(
"SC1090",
expr.right,
@@ -579,6 +598,14 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc
loc,
};
}
// `u instanceof EventEmitter` (or a stream class): an untyped object
// built by a constructor function inherits the untyped constructor's
// prototype (builtin-class-dyn.ts). Compiled emitters and streams have
// no untyped form, so the prototype chain answers for every value.
const builtinKind = builtinClassKind(target.def.name);
if (left.type.kind === "dyn" && builtinKind !== null) {
return builtinClassInstanceOf(left, builtinKind, loc);
}
if (left.type.kind === "dyn") {
lowerer.unsupported(
"SC1090",
@@ -1144,6 +1171,24 @@ function lowerPrivateIn(
}
/** The string keys every record inherits from Object.prototype. */
/** Keys built-in errors carry outside their class fields: Node's system
* error properties, and AggregateError's, SuppressedError's and
* AssertionError's own members. */
const BUILTIN_ERROR_SLOT_KEYS: ReadonlySet<string> = new Set([
"errno",
"syscall",
"path",
"dest",
"info",
"errors",
"error",
"suppressed",
"actual",
"expected",
"operator",
"generatedMessage",
]);
const OBJECT_PROTOTYPE_KEYS: ReadonlySet<string> = new Set([
"constructor",
"__defineGetter__",
@@ -1360,9 +1405,10 @@ export function lowerInExpression(
// predicate's `err instanceof Error && "code" in err` shape): `code`
// answers from the runtime error's code slot (stamped by fs/system/
// event/spawn errors, absent on plain constructions — exactly Node's
// own-property answer), and the always-declared members fold true.
// Everything else fences: the runtime object carries no other dynamic
// properties to ask (`stack` would be a lie — Node has one, we don't).
// own-property answer), and the always-present members (message, name,
// stack, Object.prototype's keys, members the static class declares)
// fold true. Other keys test the declaring subclasses and the error's
// own runtime properties; keys of built-in errors' extra slots fence.
if (recv.type.kind === "object") {
// %DOMException first: `'cause' in e` answers the options form's
// own-property record (the runtime slot), and code/name/message are
@@ -1384,34 +1430,77 @@ export function lowerInExpression(
if (key === "cause") {
return { kind: "libCall", fn: "error.hasCause", args: [recv], type: BOOL, loc };
}
// The code slot, or an own `code` with another value
// (`err.code = undefined` is present).
if (key === "code") {
const codeType = lowerer.envValueType();
if (codeType.kind !== "union")
throw new InternalCompilerError("lowerer bug: error code type is not a union");
const undefTag = lowerer.armTag(codeType.unionId, UNDEFINED_T);
const read: IrExpr = {
return { kind: "libCall", fn: "error.hasCode", args: [recv], type: BOOL, loc };
}
// The receiver evaluates for its effects when the answer is static.
const always = (): IrExpr =>
recv.kind === "varRef" || recv.kind === "caughtNarrow"
? { kind: "boolLit", value: true, type: BOOL, loc }
: {
kind: "seqExpr",
stmts: [{ kind: "exprStmt", expr: recv, loc }],
result: { kind: "boolLit", value: true, type: BOOL, loc },
type: BOOL,
loc,
};
if (key === "message" || key === "name") return always();
// Every Error has its own `stack`, and Error.prototype's `toString`
// and `constructor` are among Object.prototype's keys.
if (key === "stack" || OBJECT_PROTOTYPE_KEYS.has(key)) return always();
// A member the static class declares is always present.
const declares = (c: ClassInfo): boolean =>
c.fields.has(key) ||
lowerer.findMethodOn(c, key) !== null ||
lowerer.findMethodOn(c, `get:${key}`) !== null ||
lowerer.findMethodOn(c, `set:${key}`) !== null;
const staticInfo = lowerer.classes.get(recv.type.className);
if (staticInfo && declares(staticInfo)) return always();
// Any other key: a descendant class that declares it (instanceof), or
// an own property set at runtime (a throw site's extra fields,
// Object.assign, writes through a dynamic view). Keys built-in errors
// keep outside their class fields depend on the runtime error and
// stay fenced.
if (!BUILTIN_ERROR_SLOT_KEYS.has(key)) {
const stable = recv.kind === "varRef" || recv.kind === "caughtNarrow";
const slot = stable ? null : lowerer.declareHiddenLocal("%inRecv", recv.type);
const ref = (): IrExpr => (slot ? varRef(slot.id, recv.type, loc) : recv);
const tests: IrExpr[] = [];
const staticName = recv.type.className;
for (const c of lowerer.classes.values()) {
const name = c.def.name;
if (name === staticName || !lowerer.isSubclassOf(name, staticName) || !declares(c))
continue;
// A declaring ancestor below the static class already covers it.
if (c.base && c.base.def.name !== staticName && declares(c.base)) continue;
tests.push({ kind: "instanceOf", value: ref(), className: name, type: BOOL, loc });
}
tests.push({
kind: "libCall",
fn: "error.code",
args: [recv],
type: codeType,
fn: "error.hasKey",
args: [ref(), { kind: "strLit", value: key, type: STRING, loc }],
type: BOOL,
loc,
};
});
const answer = tests.reduce((acc, test) => ({
kind: "logical",
op: "||",
left: acc,
right: test,
type: BOOL,
loc,
}));
if (!slot) return answer;
return {
kind: "unionIsTag",
unionId: codeType.unionId,
tag: undefTag,
negated: true,
value: read,
kind: "seqExpr",
stmts: [{ kind: "varDecl", localId: slot.id, init: recv, loc }],
result: answer,
type: BOOL,
loc,
};
}
if (
(key === "message" || key === "name") &&
(recv.kind === "varRef" || recv.kind === "caughtNarrow")
) {
return { kind: "boolLit", value: true, type: BOOL, loc };
}
lowerer.unsupported(
"SC1090",
expr,
@@ -42,6 +42,7 @@ import type { CompoundOp } from "../surfaces.js";
import { tryLowerExpression } from "./try-lower-expression.js";
import { hasRuntimeStatics } from "../class-runtime-statics.js";
import { declareContextLocal } from "../function-context.js";
import { keyedFieldTarget, keyedFieldWrite, saveKeyedFieldKey } from "./keyed-field-targets.js";
/** Read-modify-write updates read the member first: an unassigned
* deferred-init receiver throws Node's member-read TypeError. */
@@ -337,10 +338,11 @@ export function lowerIncDec(
}
const target = ts.isPropertyAccessExpression(access)
? lowerer.fieldTarget(access)
: symbolFieldTarget(lowerer, access);
: (symbolFieldTarget(lowerer, access) ?? keyedFieldTarget(lowerer, access, true));
if (target && lowerer.dynConvertible(target.fieldType)) {
checkDeferredMemberReceiver(lowerer, target, access);
target.obj = save(target.obj, "%incrementReceiver");
saveKeyedFieldKey(target, save);
return finish(lowerer.fieldGetExpr(target, loc, access), (value) =>
lowerer.fieldSetStmt(
target,
@@ -617,6 +619,7 @@ export function fieldSetStmt(
};
}
}
if (target.container === "recordKeyed") return keyedFieldWrite(lowerer, target, value, loc);
if (target.container === "dynamic")
return {
kind: "exprStmt",
@@ -872,12 +875,13 @@ export function lowerFieldCompoundValue(
const targetOf = (): FieldTarget | null =>
ts.isPropertyAccessExpression(access)
? lowerer.fieldTarget(access)
: symbolFieldTarget(lowerer, access);
: (symbolFieldTarget(lowerer, access) ?? keyedFieldTarget(lowerer, access, true));
const target = targetOf();
if (!target)
lowerer.unsupported("SC1090", access, "compound assignment to unsupported field targets");
checkDeferredMemberReceiver(lowerer, target, access);
target.obj = save(target.obj, "%compoundReceiver");
saveKeyedFieldKey(target, save);
// Accessors observe getter, RHS side effects, then setter, all through
// the saved receiver.
const fieldRead = lowerer.fieldGetExpr(target, locOf(access), access);
@@ -48,6 +48,16 @@ export interface FnCtx {
argumentsLocal?: IrLocal | null;
/** Arrows inherit the nearest ordinary function's arguments binding. */
inheritsArguments?: boolean;
/** A function lowered on demand on top of an unrelated caller's frames
* (a generic instance): lexical `this` resolution stops here, so the
* body sees what it sees when lowered alone. */
thisBoundary?: true;
/** Tuple bindings of this function that are only forwarded
* (typed-arguments.ts onlyForwarded): binding local id → the native
* slots forwarding reads. Such a binding is never materialized. Local ids
* are unique within one function only, and nested functions never read
* these bindings (a read there needs the tuple). */
pureTuples: Map<string, IrLocal[]>;
/** Declared return type — lets `return` detect record-shape mismatches
* (SC2002) before the validator would ICE on them. */
returnType: IrType;
@@ -81,6 +91,7 @@ export function newFnCtx(
captures: lifted ? [] : null,
captureSources: [],
captureBySymbol: new Map(),
pureTuples: new Map(),
selfSymbol,
selfType,
returnType,
@@ -154,7 +165,16 @@ export function captureContextBinding(
throw new InternalCompilerError("lowerer bug: no active function context");
const direct = bindingInContext(current, symbol);
if (direct !== null) return { local: direct, origin: direct, error: null };
for (let depth = stack.length - 2; depth >= 0; depth--) {
let floor = 0;
if (symbol === undefined) {
for (let depth = stack.length - 1; depth >= 0; depth--) {
if (stack[depth]!.thisBoundary) {
floor = depth;
break;
}
}
}
for (let depth = stack.length - 2; depth >= floor; depth--) {
const origin = bindingInContext(stack[depth]!, symbol);
if (origin === null) continue;
@@ -0,0 +1,114 @@
/* The general stream representation (TypeMapperCtx.generalNodeStreams).
*
* NodeJS.ReadableStream and NodeJS.WritableStream (and @types/node's
* stream.Readable / stream.Writable, which also type child stdio) name the
* interface every Node stream implements. By default they map to the
* child/process stdio handles: the specialized representation of the
* values that commonly fill them (child.stdout, process.stdout). A program
* that moves another stream — fs.createReadStream(), a zlib Transform, a
* Readable subclass — into one of those slots needs the general
* representation instead: the runtime stream classes, with the stdio
* handles converted into them where they are read or flow.
*
* The decision is per program. Reachable emit lowers without it; a flow
* that needs it marks the lowerer (needsGeneralNodeStreams) and lowerToIr
* lowers the whole program again with it. Programs without such a flow
* never take the second pass, so their IR is unchanged. */
import type { IrExpr, IrType } from "../../ir/ir.js";
import { everyTypeChild } from "../../ir/traverse.js";
import * as ts from "../ts7/adapter.js";
import { isGeneralNodeStreamInterface } from "../type-mapper.js";
import type { Lowerer } from "./lowerer.js";
import { streamSidesOf } from "./lower-stream.js";
const STDIO_STREAM_KINDS: ReadonlySet<IrType["kind"]> = new Set([
"childStream",
"childWriter",
"procStream",
]);
/** Whether `type` (through union arms, record fields and the structural
* type children) contains a part satisfying `test`. */
function containsPart(lowerer: Lowerer, type: IrType, test: (part: IrType) => boolean): boolean {
const seen = new Set<string>();
const walk = (part: IrType): boolean => {
if (test(part)) return true;
if (part.kind === "union") {
if (seen.has(`u:${part.unionId}`)) return false;
seen.add(`u:${part.unionId}`);
return (lowerer.unions.get(part.unionId)?.arms ?? []).some(walk);
}
if (part.kind === "record") {
if (seen.has(`r:${part.shapeId}`)) return false;
seen.add(`r:${part.shapeId}`);
return (lowerer.shapes.get(part.shapeId)?.fields ?? []).some((f) => walk(f.type));
}
let found = false;
everyTypeChild(part, (child) => {
found = walk(child);
return !found;
});
return found;
};
return walk(type);
}
function isGeneralStream(lowerer: Lowerer, part: IrType): boolean {
return (
part.kind === "object" && streamSidesOf(lowerer, lowerer.classes.get(part.className)) !== null
);
}
/** A mismatched flow (requireExactShape's input) that the general stream
* representation resolves: a general stream into a stdio-represented
* slot, a child pipe handle into an 'unknown' slot, or an 'unknown' value
* into a child-pipe slot (the general classes take a checked cast). */
export function needsGeneralNodeStreams(
lowerer: Lowerer,
actual: IrType,
expected: IrType,
): boolean {
const stdio = (part: IrType): boolean => STDIO_STREAM_KINDS.has(part.kind);
const pipeHandle = (part: IrType): boolean =>
part.kind === "childStream" || part.kind === "childWriter";
if (actual.kind === "dyn") return containsPart(lowerer, expected, pipeHandle);
if (containsPart(lowerer, expected, stdio))
return containsPart(lowerer, actual, (p) => isGeneralStream(lowerer, p));
return expected.kind === "dyn" && containsPart(lowerer, actual, pipeHandle);
}
/** Under the general representation, process.stdout / process.stderr (the
* procStream fd scalar) flowing into a runtime Writable slot: an
* fd-backed Writable whose writes take the same synchronous path as
* process.stdout.write. Null when the flow is not that one. */
export function stdioIntoGeneralStream(
lowerer: Lowerer,
expr: IrExpr,
expected: IrType,
): IrExpr | null {
if (!lowerer.generalNodeStreams || expr.type.kind !== "procStream") return null;
const writable: IrType = { kind: "object", className: "%Writable" };
const accepts =
(expected.kind === "object" && expected.className === "%Writable") ||
(expected.kind === "union" && lowerer.armTag(expected.unionId, writable) >= 0);
if (!accepts) return null;
return {
kind: "libCall",
fn: "stream.stdioWritable",
args: [expr],
type: writable,
loc: expr.loc,
};
}
/** A stdio-handle receiver whose member has no lowering on the handle
* (`out.on("error", ...)`, `child.stdout.pipe(...)`, `output.end()`): when
* its declared type is one the general representation maps to a runtime
* stream class, the program lowers again with it, where the full stream
* surface applies. The caller still reports its fence for this pass. */
export function requestGeneralStreamSurface(lowerer: Lowerer, receiver: ts.Expression): void {
if (lowerer.generalNodeStreams) return;
const type = lowerer.checker.getNonNullableType(lowerer.typeOf(receiver));
if (isGeneralNodeStreamInterface(type, lowerer.typeCtx))
lowerer.requiresGeneralNodeStreams = true;
}
@@ -1,3 +1,4 @@
import { staticNameBindings } from "./static-function-names.js";
import * as ts from "../ts7/adapter.js";
import { bodyReadsArguments } from "../arguments-usage.js";
import type { Lowerer } from "./lowerer.js";
@@ -100,6 +101,9 @@ export interface GenericFnInfo {
* names where the slot binds the call site's object type. Computed on
* first use. */
implicitPatterns?: (ImplicitPattern | null)[];
/** Parameter index → symbol for the function parameters whose `.name`
* the body reads (static-function-names.ts); computed on first use. */
nameReadParams?: Map<number, ts.Symbol>;
}
export interface GenericInstance {
@@ -140,6 +144,9 @@ export interface GenericInstance {
implicitInferReturn?: true;
/** An erased instance (every type parameter bound to `unknown`). */
erased?: true;
/** Name-keyed instances: function parameter → its call site's static
* name (null when only known at run time), folded at `.name` reads. */
staticFnNames?: Map<ts.Symbol, string | null>;
}
/** Defaults bind during inference; collection only resolves symbols in
@@ -295,7 +302,13 @@ export function genericCallInstance(
const retTs = lowerer.checker.getReturnTypeOfSignature(rsig);
const local = localClassResultInstance(lowerer, expr, info, rsig, params, retTs);
if (local) return local;
const mappedReturn = lowerer.mapTypeOf(retTs);
// A result instantiated at `any` (a type argument inferred from an
// untyped value, such as an untyped forwarding call) is checked-dynamic.
const mappedReturn =
lowerer.mapTypeOf(retTs) ??
((retTs.flags & ts.TypeFlags.Any) !== 0 && !isJsSourceFile(info.decl.getSourceFile())
? DYN
: null);
if (!mappedReturn) {
fenceGenericSignatureResult(lowerer, expr, retTs);
lowerer.badType(expr, retTs);
@@ -317,6 +330,7 @@ export function genericCallInstance(
// different fields). Non-literal bindings (a key union, plain string)
// share one runtime-keyed instance per IR signature, exactly the
// widened discipline.
const named = staticNameBindings(lowerer, expr, info);
const keyofTps = keyofConstrainedTypeParams(info);
if (keyofTps.size > 0) {
const tsBindings = new Map<ts.Symbol, ts.Type>();
@@ -333,12 +347,19 @@ export function genericCallInstance(
})
.join(",");
return internGenericInstance(lowerer, expr, info, params, returnType, () => bindings, {
extraKey: `@${litKey}`,
extraKey: `@${litKey}${named?.key ?? ""}`,
tsBindings,
...(named ? { staticFnNames: named.names } : {}),
});
}
return internGenericInstance(lowerer, expr, info, params, returnType, (tsBindings) =>
lowerer.inferTypeParamBindings(expr, info, rsig, tsBindings),
return internGenericInstance(
lowerer,
expr,
info,
params,
returnType,
(tsBindings) => lowerer.inferTypeParamBindings(expr, info, rsig, tsBindings),
named ? { extraKey: named.key, staticFnNames: named.names } : undefined,
);
}
@@ -498,7 +519,11 @@ function internGenericInstance(
params: ParamShape[],
returnType: IrType,
makeBindings: (tsBindings: Map<ts.Symbol, ts.Type>) => Map<ts.Symbol, IrType>,
opts?: { extraKey?: string; tsBindings?: Map<ts.Symbol, ts.Type> },
opts?: {
extraKey?: string;
tsBindings?: Map<ts.Symbol, ts.Type>;
staticFnNames?: Map<ts.Symbol, string | null>;
},
): GenericInstance {
// keyof-constrained instantiations append their literal keys
// (extraKey): the IR signature alone under-discriminates there — two
@@ -541,6 +566,7 @@ function internGenericInstance(
bindings,
tsBindings,
typeArgsText,
...(opts?.staticFnNames ? { staticFnNames: opts.staticFnNames } : {}),
};
info.instances.set(key, inst);
lowerer.instantiationQueue.push({ info, inst });
@@ -671,7 +697,11 @@ function unifySignatureBindings(
tsBindings.set(sym, lowerer.typeParamTsResolver(inst) ?? inst);
}
if (!bindings.has(sym)) {
const mapped = lowerer.mapTypeOf(inst);
const mapped =
lowerer.mapTypeOf(inst) ??
((inst.flags & ts.TypeFlags.Any) !== 0 && !isJsSourceFile(info.decl.getSourceFile())
? DYN
: null);
// A visitor's inferred T can be void. It still binds the
// return convention of `(node) => T`; discarding it leaves
// the callback's return type spuriously uninstantiated.
@@ -824,6 +854,8 @@ export function lowerGenericInstance(
const prevSuppress = lowerer.suppressStats;
const prevClass = lowerer.currentClass;
const prevImplicit = lowerer.implicitParamTypes;
const prevStaticFnNames = lowerer.staticFnNames;
lowerer.staticFnNames = inst.staticFnNames ?? null;
// A generic METHOD of a generic-class INSTANTIATION lowers under BOTH
// binding sets: the receiver instantiation's class type parameters
// underneath, the method instantiation's own on top (disjoint symbol
@@ -873,6 +905,8 @@ export function lowerGenericInstance(
: lowerer.bodyReturnType(isAsync, inst.returnType);
const fnCtx = newFnCtx(false, null, null, bodyReturn);
fnCtx.isAsync = isAsync;
// Instances lower on demand, on top of whichever caller reached them.
fnCtx.thisBoundary = true;
// Implicit-any instances whose declared return did not map lower in
// return-INFERENCE mode: `return` statements record here bare, and the
// post-pass (resolveInferredReturn) settles the type and wraps them.
@@ -984,6 +1018,7 @@ export function lowerGenericInstance(
lowerer.typeParamBindings = prevBindings;
lowerer.typeParamTsBindings = prevTsBindings;
lowerer.implicitParamTypes = prevImplicit;
lowerer.staticFnNames = prevStaticFnNames;
lowerer.instantiationContext = prevContext;
lowerer.localClassInstantiations.pop();
lowerer.suppressStats = prevSuppress;
@@ -1,7 +1,10 @@
import { isForwardedTupleSpread, lowerForwardingApply } from "./typed-arguments.js";
import { guardNullProto, nullProtoRecordShape } from "./null-prototype.js";
import { lowerBuiltinClassCall } from "./builtins/builtin-class-dyn.js";
import { refuseModuleMemberThis } from "./module-member-this.js";
import { lowerUnionEquality, tagEqualityMayMissAlias } from "./strict-equality.js";
import { lowerHashStreamCall } from "./builtins/hash-stream.js";
import { lowerWebAssemblyValueOf } from "./builtins/webassembly.js";
import { lowerResponseStaticJson } from "./lower-stdlib.js";
import { lowerArrayCallbackCall } from "./containers/array-callback-call.js";
/** Dispatch source calls, builtin members and receiver-specific operations.
@@ -149,6 +152,7 @@ import {
import {
fencePerfEntryKeyWalk,
isPerfEntryType,
lowerHistogramMethodCall,
lowerPerfHooksCall,
perfEntryTag,
} from "./builtins/performance.js";
@@ -254,7 +258,6 @@ const ZLIB_STREAM_METHODS: ReadonlySet<string> = new Set(["flush", "params", "cl
export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
const loc = locOf(expr);
// Object's checker return type is `any`. Claim known object inputs
// before generic checked-dynamic dispatch tries to load the constructor.
if (
@@ -266,6 +269,11 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
if (object) return object;
}
// `EventEmitter.call(this)` in a constructor function: the untyped
// EventEmitter constructor initializes the receiver.
const builtinClassCall = lowerBuiltinClassCall(lowerer, expr);
if (builtinClassCall) return builtinClassCall;
// require() of a JSON document: the shared parsed object.
const json = lowerJsonRequireCall(lowerer, expr);
if (json) return json;
@@ -1406,6 +1414,9 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
if (ts.isPropertyAccessExpression(expr.expression)) {
const intrinsic =
fenceNodeModuleMutationCall(lowerer, expr, expr.expression) ??
// `f.apply(thisArg, args)` forwarding a fixed argument tuple (typed
// `arguments`, a tuple rest parameter): a direct call.
lowerForwardingApply(lowerer, expr, expr.expression) ??
// Builtin namespace imports first (`fs.readFileSync(...)` where fs
// is `import * as fs from "node:fs"`): the same tables and fences
// as named builtin imports — before anything below tries to lower
@@ -1417,6 +1428,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
// .bind(performance) function value over the runtime's
// process-start-anchored monotonic clock.
lowerPerfHooksCall(lowerer, expr, expr.expression) ??
lowerHistogramMethodCall(lowerer, expr, expr.expression) ??
// The composed crypto pattern (randomBytes(n).toString(enc))
// — its receiver is a Buffer-typed CALL no other lowering claims.
lowerer.lowerCryptoComposedCall(expr, expr.expression) ??
@@ -1901,6 +1913,19 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
// or marshals the callee and applies through a runtime-built argument
// list (lowerSpreadArgsCall). Shapes outside its lanes fall through
// to the historical fences.
// Forwarded fixed tuples (`fn(...args)` over a tuple rest parameter or a
// typed `arguments`) have a compile-time arity: complete them into the
// callee's positional slots directly, without the runtime-arity lane.
if (
callee.type.rest !== true &&
expr.arguments.nodes.some(ts.isSpreadElement) &&
expr.arguments.nodes.every(
(a) => !ts.isSpreadElement(a) || isForwardedTupleSpread(lowerer, a.expression),
)
) {
const args = completeFuncValueArgs(lowerer, expr, callee.type, loc);
return { kind: "callValue", callee, args, type: callee.type.ret, loc };
}
if (expr.arguments.nodes.some((a) => ts.isSpreadElement(a))) {
const spreadServed = lowerSpreadArgsCall(lowerer, expr, callee, loc);
if (spreadServed) return spreadServed;
@@ -3319,7 +3344,13 @@ function lowerDynReceiverMethodCall(
// array — scalar narrowings bridge through maybeNarrow's dynCheck and
// take the ordinary typed paths, but there is no static home for an
// any-elemented array). Typed receivers keep their own lowerings.
const recvTs = lowerer.typeOf(access.expression);
// An optional step the chain already guarded (`s?.m(x)`) has a receiver
// proven non-nullish.
const rawRecvTs = lowerer.typeOf(access.expression);
const recvTs =
access.questionDotToken !== undefined && lowerer.chainHandled.has(access)
? lowerer.checker.getNonNullableType(rawRecvTs)
: rawRecvTs;
if (lowerer.classImplementedProtocol(recvTs)) {
const property = lowerer.checker.getPropertyOfType(recvTs, access.name.text);
if (
@@ -3644,6 +3675,8 @@ function lowerDynReceiverMethodCall(
// answers each kind's own: primitives and plain objects themselves, a
// Date its time value, an own or inherited valueOf member its result.
if (access.name.text === "valueOf" && call.arguments.nodes.length === 0) {
const webAssembly = lowerWebAssemblyValueOf(lowerer, access.expression, recv, locOf(call));
if (webAssembly !== null) return webAssembly;
return {
kind: "libCall",
fn: "dyn.valueOf",
@@ -4351,6 +4384,14 @@ function lowerCaughtPredicateCall(
const paramSymbol = lowerer.checker.getSymbolAtLocation(param.name);
if (!paramSymbol) return null;
const ret = decl.body.statements.nodes.length === 1 ? decl.body.statements.nodes[0] : undefined;
// A longer guard whose parameter is `unknown` (or `any`) takes the caught
// value as an ordinary checked-dynamic argument: the call path converts
// the binding, and the narrowed binding reads through the same value.
if (
(!ret || !ts.isReturnStatement(ret) || !ret.expression) &&
lowerer.mapTypeOf(lowerer.checker.getTypeOfSymbol(paramSymbol))?.kind === "dyn"
)
return null;
if (!ret || !ts.isReturnStatement(ret) || !ret.expression) {
lowerer.unsupported(
"SC1090",
@@ -6892,6 +6933,7 @@ function recordAssignHelper(
const helper = `%obj.assign.${lowerer.arrHofHelpers.size}`;
lowerer.arrHofHelpers.set(key, helper);
const sShape = lowerer.shapes.get(srcShapeId)!;
const tShape = lowerer.shapes.get(targetShapeId)!;
const tT: IrType = { kind: "record", shapeId: targetShapeId };
const sT: IrType = { kind: "record", shapeId: srcShapeId };
const tRef: IrExpr = { kind: "varRef", localId: "t.0", type: tT, loc };
@@ -6906,12 +6948,13 @@ function recordAssignHelper(
type: f.type,
loc,
};
const targetType = tShape.fields.find((x) => x.name === f.name)!.type;
const set: IrStmt = {
kind: "recordSet",
obj: tRef,
shapeId: targetShapeId,
field: f.name,
value: get,
value: typeEquals(targetType, f.type) ? get : lowerer.coerceToExpected(get, targetType),
loc,
};
const present = lowerer.recordFieldPresent(sRef, srcShapeId, f.name, loc);
@@ -7459,6 +7502,8 @@ function lowerObjectStaticCall(
};
}
}
const onError = lowerObjectAssignOntoError(lowerer, call);
if (onError) return onError;
const hybrid = lowerObjectAssignHybrid(lowerer, call);
if (hybrid) return hybrid;
// `Object.assign({}, lit)` — an EMPTY fresh-literal target and one
@@ -7551,7 +7596,12 @@ function lowerObjectStaticCall(
!shapeHasAccessorSlots(sShape) &&
sShape.fields.every((sf) => {
const tf = tShape.fields.find((x) => x.name === sf.name);
return tf !== undefined && typeEquals(tf.type, sf.type);
// A source value converts into a wider target field (`{ bail:
// true }` into `bail?: boolean`).
return (
tf !== undefined &&
(typeEquals(tf.type, sf.type) || lowerer.coercibleValue(sf.type, tf.type))
);
});
if (ok) {
const loc = locOf(call);
@@ -7569,6 +7619,37 @@ function lowerObjectStaticCall(
}
}
}
// `throw Object.assign(record, { extra })`: an error-shaped record given
// a member its type lacks, thrown at once. A fixed record cannot grow
// the member, so the throw carries a checked-dynamic copy of the target
// with the source's members added. The copy is unobservable: control
// leaves with the throw, so nothing on that path reads the target again.
{
let parent: ts.Node | undefined = call.parent;
while (parent && ts.isParenthesizedExpression(parent)) parent = parent.parent;
const argNodes = call.arguments.nodes;
if (
parent &&
ts.isThrowStatement(parent) &&
argNodes.length === 2 &&
!argNodes.some((a) => ts.isSpreadElement(a))
) {
const tProbe = tryLowerExpression(lowerer, argNodes[0]!);
const sProbe = tryLowerExpression(lowerer, argNodes[1]!);
if (
tProbe?.type.kind === "record" &&
lowerer.dynConvertible(tProbe.type) &&
sProbe &&
(sProbe.type.kind === "dyn" || lowerer.dynConvertible(sProbe.type))
) {
const loc = locOf(call);
const target = lowerer.coerceToExpected(lowerer.lowerExpr(argNodes[0]!), DYN);
const source = lowerer.coerceToExpected(lowerer.lowerExpr(argNodes[1]!), DYN);
if (target.type.kind === "dyn" && source.type.kind === "dyn")
return { kind: "libCall", fn: "dyn.assign", args: [target, source], type: DYN, loc };
}
}
}
// `Object.assign(target, ...sources)` over a CHECKED-DYNAMIC target
// — the n-ary/spread form (`Object.assign({}, ...plugins.map(p =>
// p.options), coreOptions)`, support.js's option-table merge). The
@@ -8799,6 +8880,9 @@ export function lowerRecordFieldCall(
};
return init ? { kind: "seqExpr", stmts: [init], result, type: result.type, loc } : result;
}
// An optional callback field the checker narrowed at this call
// (`o.cb ? o.cb(x) : …`, `if (o.cb) o.cb(x)`): extract the func arm.
if (callee.type.kind === "union") callee = lowerer.maybeNarrow(callee, access);
if (callee.type.kind !== "func") lowerer.badType(access, lowerer.typeOf(access));
// JavaScript passes the record as `this`. A record with no dynamic form
// cannot be that receiver; the library's own function fields (the
@@ -9645,6 +9729,30 @@ export function lowerObjectMethodCall(
`calls of the abstract method '${method}' with no concrete implementation below the receiver's static class`,
);
}
// The static receiver's nearest declaration is an override adapted to
// the inherited ABI, and nothing below overrides it: call its body
// directly under its declared signature (exact parameters and result).
// The slot function stays reachable; it lifts the body.
const adapted = found.declarer.returnAdapters?.get(method);
// A TypeScript override's own entry answers its declared result. A
// JavaScript body's result is inferred, so its callers keep the slot.
const declarerDecl = found.declarer.decl;
if (
adapted &&
!(declarerDecl && isJsSourceFile(declarerDecl.getSourceFile())) &&
!lowerer.overrideBelow(info, method)
) {
lowerer.noteEdge(`%${found.declarer.def.name}.${method}`);
const receiver = lowerReceiver();
const args = lowerer.completeArgs(call.arguments.nodes, adapted.params, locOf(call), call);
return {
kind: "call",
callee: `%${found.declarer.def.name}.${method}$own`,
args: [lowerer.upcastTo(receiver, found.declarer.def.name), ...args],
type: adapted.ret,
loc: locOf(call),
};
}
if (lowerer.overrideBelow(info, method)) lowerer.noteVirtualEdge(info, method);
else lowerer.noteEdge(`%${found.declarer.def.name}.${method}`);
const receiver = lowerReceiver();
@@ -10255,3 +10363,60 @@ function maybePromiseHandlerResult(
}
return stmt;
}
/** `Object.assign(err, ...sources)` in TypeScript onto an error-hierarchy
* target (`Object.assign(new Error(message), details)`): the sources'
* own members land on the error — the runtime copy over its
* checked-dynamic node, whose name, message, stack, cause and code feed
* back into the typed slots — and the call answers the error itself.
* Null for every other target. */
function lowerObjectAssignOntoError(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null {
const argNodes = call.arguments.nodes;
if (
isJsSourceFile(call.getSourceFile()) ||
argNodes.length < 2 ||
argNodes.some((a) => ts.isSpreadElement(a))
)
return null;
const probe = tryLowerExpression(lowerer, argNodes[0]!);
if (probe?.type.kind !== "object" || !lowerer.isErrorRootedClass(probe.type.className))
return null;
const loc = locOf(call);
const target = lowerer.lowerExpr(argNodes[0]!);
const slot = lowerer.declareHiddenLocal("%assignErr", target.type);
const sources: IrExpr[] = [];
for (const node of argNodes.slice(1)) {
const raw = lowerer.lowerExpr(node);
fenceSymbolFieldCopy(lowerer, node, raw.type);
const source = lowerer.coerceToExpected(raw, DYN);
if (source.type.kind !== "dyn") {
lowerer.unsupported(
"SC1090",
node,
`Object.assign onto an Error from a '${lowerer.fmt(raw.type)}' source (sources enter the error's dynamic properties)`,
);
}
sources.push(source);
}
const ref = (): IrExpr => ({ kind: "varRef", localId: slot.id, type: target.type, loc });
return {
kind: "seqExpr",
stmts: [
{ kind: "varDecl", localId: slot.id, init: target, loc },
{
kind: "exprStmt",
expr: {
kind: "libCall",
fn: "error.assign",
args: [ref(), { kind: "dynArrLit", elems: sources, type: DYN, loc }],
type: VOID,
loc,
},
loc,
},
],
result: ref(),
type: target.type,
loc,
};
}
@@ -31,6 +31,7 @@ import type { FnCtx, Lowerer } from "./lowerer.js";
import { inlineNamespaceRequireOf, lowerWithInlineRequireInit } from "./cjs-inline-require.js";
import {
BOOL,
canDynCheckTo,
DATE_T,
DYN,
F64,
@@ -500,6 +501,12 @@ export interface ClassInfo {
/** Own JS overrides whose signature cannot use the inherited ABI. The slot
* retains that ABI, but its implementation throws before executing. */
methodEntryFences?: Map<string, ScrDiagnostic>;
/** Own TypeScript overrides whose declared result differs from the
* inherited ABI's but converts into it (`Promise<void>` over `any`, a
* union narrowed to its inherited arm): the slot function `%C.m` keeps the
* inherited ABI and converts the result of the body, which lowers as
* `%C.m$own` with the declared result (async when declared so). */
returnAdapters?: Map<string, { params: ParamShape[]; ret: IrType; async: boolean }>;
/** OWN GENERIC instance methods (own type parameters — `m<T>(x: T)`),
* monomorphized per call site like top-level generic functions: instance
* `n` is the module function `%C.m%n` taking `this` as param 0. They
@@ -2256,6 +2263,7 @@ export function collectClassShapeInner(
}
>();
const methodEntryFences = new Map<string, ScrDiagnostic>();
const returnAdapters = new Map<string, { params: ParamShape[]; ret: IrType; async: boolean }>();
// Own accessor declarations ("get:x"/"set:x" → node), for the
// partial-override analysis below (diagnostics need the node).
const accessorNodes = new Map<string, ts.AccessorDeclaration>();
@@ -3441,7 +3449,67 @@ export function collectClassShapeInner(
overridden.declarer.decl !== null &&
isJsSourceFile(overridden.declarer.decl.getSourceFile()) &&
overridden.sig.gen === undefined;
if (fencedJsOverride) {
// A TypeScript override that only changes the result, into a
// type that converts into the inherited one (`Promise<void>` over
// an `any` base, `string | undefined` over `string`): the slot
// keeps the inherited ABI and converts the body's result at the
// boundary (lowerClassMethodMemberInner).
const ownAsync =
member.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) === true;
if (
!fencedJsOverride &&
!functionLocalClass(decl) &&
overridden.sig.gen === undefined &&
// An async base's slot answers its promise: a Promise type, or
// the checked-dynamic value of an untyped async method.
(overridden.sig.async !== true ||
overridden.sig.ret.kind === "promise" ||
overridden.sig.ret.kind === "dyn") &&
!overridden.sig.abstract &&
overridden.sig.params.length === shapes.length &&
overridden.sig.params.every(
(p, i) =>
p.mode === shapes[i]!.mode &&
(p.mode === "required" || p.mode === "omittable") &&
// Each inherited argument converts into the override's
// parameter: equal, a checked extraction from an `any`
// slot, or a widening (a contravariant parameter).
(typeEquals(p.type, shapes[i]!.type) ||
(p.type.kind === "dyn" &&
canDynCheckTo(
shapes[i]!.type,
(id) => lowerer.shapes.get(id),
(id) => lowerer.unions.get(id),
)) ||
lowerer.coercibleValue(p.type, shapes[i]!.type)),
) &&
(ownAsync ? ft.ret.kind === "promise" : true) &&
(typeEquals(ft.ret, overridden.sig.ret) ||
(overridden.sig.ret.kind === "dyn"
? ft.ret.kind !== "void" && lowerer.dynConvertible(ft.ret)
: lowerer.coercibleValue(ft.ret, overridden.sig.ret) ||
// A promise whose payload widens into the inherited one
// (`Promise<void>` over an untyped async `Promise<any>`).
(ft.ret.kind === "promise" &&
overridden.sig.ret.kind === "promise" &&
lowerer.promisePayloadWidens(ft.ret, overridden.sig.ret)) ||
// A JavaScript override's inferred `any` result checks
// into the inherited type (`toString() { return
// this.message }` over Error's).
(ft.ret.kind === "dyn" &&
isJsSourceFile(member.getSourceFile()) &&
overridden.sig.ret.kind !== "void" &&
canDynCheckTo(
overridden.sig.ret,
(id) => lowerer.shapes.get(id),
(id) => lowerer.unions.get(id),
))))
) {
returnAdapters.set(mName, { params: [...shapes], ret: ft.ret, async: ownAsync });
shapes.splice(0, shapes.length, ...overridden.sig.params);
ft.ret = overridden.sig.ret;
if (overridden.sig.async === true) thunkAsyncSlot(overridden, mName);
} else if (fencedJsOverride) {
// A dormant JS method must not poison the whole class. Keep
// the inherited call/dispatch ABI and refuse at method entry;
// lowering this body with the base signature would change JS.
@@ -3496,7 +3564,34 @@ export function collectClassShapeInner(
// fiber spawn wrapper; vtable slots hold raw implementations) —
// an override chain touching an async method on either end would
// put a spawn wrapper behind a virtual slot, so it fences.
if (overridden && (asyncMember || overridden.sig.async === true)) {
// An override chain touching async methods dispatches through
// plain slot functions that return the body's promise: the async
// bodies lower as `%C.m$own` behind them (thunkAsyncSlot).
if (
overridden &&
!returnAdapters.has(mName) &&
(asyncMember || overridden.sig.async === true) &&
!isJsSourceFile(member.getSourceFile()) &&
!functionLocalClass(decl) &&
member.asteriskToken === undefined &&
overridden.sig.gen === undefined &&
!overridden.sig.abstract &&
ft.ret.kind === "promise" &&
typeEquals(ft.ret, overridden.sig.ret) &&
overridden.sig.params.length === shapes.length &&
overridden.sig.params.every(
(p, i) => typeEquals(p.type, shapes[i]!.type) && p.mode === shapes[i]!.mode,
)
) {
if (overridden.sig.async === true) thunkAsyncSlot(overridden, mName);
if (asyncMember)
returnAdapters.set(mName, { params: [...shapes], ret: ft.ret, async: true });
}
if (
overridden &&
!returnAdapters.has(mName) &&
(asyncMember || overridden.sig.async === true)
) {
const message = `overriding ${overridden.sig.async === true ? "the async method" : "a method with an async method"} '${mName}' (async methods dispatch statically — the vtable slot machinery has no fiber-spawn story)`;
if (isJsSourceFile(member.getSourceFile()))
methodEntryFences.set(mName, unsupportedDiag("SC1090", locOf(member.name), message));
@@ -3527,7 +3622,7 @@ export function collectClassShapeInner(
} else {
methods.set(
mName,
asyncMember
asyncMember && !returnAdapters.has(mName)
? { params: shapes, ret: ft.ret, async: true as const }
: { params: shapes, ret: ft.ret },
);
@@ -4245,6 +4340,7 @@ export function collectClassShapeInner(
fieldOrder,
methods,
...(methodEntryFences.size > 0 ? { methodEntryFences } : {}),
...(returnAdapters.size > 0 ? { returnAdapters } : {}),
decl,
...(emitOverride !== undefined ? { emitOverride } : {}),
ctor,
@@ -6623,6 +6719,28 @@ function abstractMemberSignature(
/** A nominal return subtype uses the same owned pointer as the inherited
* return slot. The current class is still being collected, so consult its
* already collected base chain before the complete class graph is available. */
/** An inherited ASYNC method that an override now dispatches against: its
* slot becomes a plain function returning the body's promise, and the
* async body lowers behind it as `%C.m$own` (the adapter path with an
* identity conversion), so a virtual call never enters an async body
* without its spawn wrapper. */
function thunkAsyncSlot(
overridden: { declarer: ClassInfo; sig: ClassMethodSignature },
mName: string,
): void {
if (overridden.sig.async !== true) return;
const declarer = overridden.declarer;
declarer.returnAdapters ??= new Map();
if (!declarer.returnAdapters.has(mName)) {
declarer.returnAdapters.set(mName, {
params: [...overridden.sig.params],
ret: overridden.sig.ret,
async: true,
});
}
delete overridden.sig.async;
}
function overrideReturnAbi(
lowerer: Lowerer,
declared: IrType,
@@ -7845,6 +7963,82 @@ function lowerClassMethodMemberInner(
allowDiagSink: true,
});
}
// An override converting its declared result into the inherited ABI:
// the body lowers as `%C.m$own` under its declared result, and the slot
// function forwards to it and converts what it returns.
const adapter = info.returnAdapters?.get(mName);
if (adapter) {
const ownSig: ClassMethodSignature = { ...sig, params: adapter.params, ret: adapter.ret };
if (adapter.async) ownSig.async = true;
const own = lowerClassMethodBody(lowerer, info, fnLike, `%${className}.${mName}$own`, ownSig);
if (!own) return null;
lowerer.liftedFns.push(own);
const loc = locOf(fnLike);
const params: IrParam[] = [
{ localId: "this.0", name: "this", type: thisType },
...sig.params.map((p, i) => ({ localId: `p.${i}`, name: `p${i}`, type: p.type })),
];
const fnCtx = newFnCtx(false, null, null, sig.ret);
lowerer.fnStack.push(fnCtx);
try {
const call: IrExpr = {
kind: "call",
callee: own.name,
args: params.map((p, i): IrExpr => {
const ref: IrExpr = { kind: "varRef", localId: p.localId, type: p.type, loc };
// `this` passes through; each inherited argument converts into
// the override's declared parameter (checked where it narrows).
return i === 0 ? ref : lowerer.coerceInto(fnLike.name, ref, adapter.params[i - 1]!.type);
}),
type: adapter.ret,
loc,
};
// A void slot discards the override's result after running it. A
// toString slot hands on what every ToString consumer of the result
// makes of it (`String(err)`, templates): undefined reads
// "undefined", as in Node. Other slots check the result into the
// inherited type.
const result = (): IrExpr =>
mName === "toString" && sig.ret.kind === "string"
? call.type.kind === "dyn"
? { kind: "libCall", fn: "dyn.stringConstructor", args: [call], type: STRING, loc }
: lowerer.ensureString(call, fnLike.name)
: lowerer.coerceInto(fnLike.name, call, sig.ret);
const body: IrStmt[] =
sig.ret.kind === "void"
? [
{ kind: "exprStmt", expr: call, loc },
{ kind: "return", value: null, loc },
]
: [{ kind: "return", value: result(), loc }];
return {
name: `%${className}.${mName}`,
params,
returnType: sig.ret,
locals: [
...params.map((p) => ({ id: p.localId, name: p.name, type: p.type, mutable: false })),
...lowerer.ctx.locals,
],
body,
loc,
};
} finally {
lowerer.fnStack.pop();
}
}
return lowerClassMethodBody(lowerer, info, fnLike, `%${className}.${mName}`, sig);
}
function lowerClassMethodBody(
lowerer: Lowerer,
info: ClassInfo,
fnLike: ts.MethodDeclaration | ts.AccessorDeclaration,
fnName: string,
sig: ClassMethodSignature,
): IrFunction | null {
const className = info.def.name;
const thisType: IrType = { kind: "object", className };
if (!fnLike.body) return null;
const prevClass = lowerer.currentClass;
lowerer.currentClass = info;
// ASYNC methods: the module function is an async IrFunction — its
@@ -7886,7 +8080,7 @@ function lowerClassMethodMemberInner(
const body = [...declared.prologue, ...lowerer.lowerStmts(fnLike.body.statements.nodes)];
appendImplicitUndefinedReturn(lowerer, body, bodyReturn, locOf(fnLike));
const fn: IrFunction = {
name: `%${className}.${mName}`,
name: fnName,
params,
returnType: bodyReturn,
locals: lowerer.ctx.locals,
@@ -4,7 +4,7 @@ import { InternalCompilerError } from "../../errors.js";
* literals — records/arrays/unions included, subject to comptimeBakeable. */
import * as ts from "../ts7/adapter.js";
import type { Lowerer } from "./lowerer.js";
import { type IrExpr, type IrType } from "../../ir/ir.js";
import { isUnitType, type IrExpr, type IrType } from "../../ir/ir.js";
import { comptimeFailedDiag } from "../../diagnostics/diagnostic.js";
import { locOf } from "../program.js";
import { PoisonError } from "./lowerer.js";
@@ -76,7 +76,7 @@ export function lowerComptime(lowerer: Lowerer, expr: ts.CallExpression): IrExpr
"SC1090",
expr,
`baking comptime results of type '${lowerer.fmt(target)}' into the binary ` +
`(only number, string, boolean, arrays, and records lower to literals)`,
`(number, string, boolean, and arrays, records and unions of those lower to literals; a record or union type reachable from itself does not)`,
);
}
// 5. The host executes the extracted callback under a finite budget.
@@ -110,20 +110,104 @@ function comptimeFailureDetail(error: unknown): string {
/** True when a comptime result of this type can be written as a literal
* expression: number/string/boolean, arrays and record shapes of those,
* nested. Closures, class instances, unions, promises, dyn, and void
* cannot be baked. */
* and unions of those with null or undefined (the mixed rows of a JSON
* table: `(string | number)[][]`), nested. Closures, class instances,
* promises, dyn, void, and recursive types (a record or union reachable
* from itself, directly or through arrays) cannot be baked. */
export function comptimeBakeable(lowerer: Lowerer, t: IrType): boolean {
return bakeableWithin(lowerer, t, new Set());
}
/** comptimeBakeable below the records and unions in `open` (keyed
* `record:<shapeId>` and `union:<unionId>`), whose members are being
* checked: reaching one of them again means the type is recursive. */
function bakeableWithin(lowerer: Lowerer, t: IrType, open: Set<string>): boolean {
switch (t.kind) {
case "f64":
case "string":
case "bool":
return true;
case "array":
return lowerer.comptimeBakeable(t.elem);
return bakeableWithin(lowerer, t.elem, open);
case "record": {
const shape = lowerer.shapes.get(t.shapeId);
return !!shape && shape.fields.every((f) => lowerer.comptimeBakeable(f.type));
const key = `record:${t.shapeId}`;
if (!shape || open.has(key)) return false;
open.add(key);
const bakeable = shape.fields.every((f) => bakeableWithin(lowerer, f.type, open));
open.delete(key);
return bakeable;
}
case "union": {
const arms = lowerer.unions.get(t.unionId)?.arms;
const key = `union:${t.unionId}`;
if (arms === undefined || open.has(key)) return false;
open.add(key);
const bakeable = arms.every(
(arm) => isUnitType(arm) || (arm.kind !== "union" && bakeableWithin(lowerer, arm, open)),
);
open.delete(key);
return bakeable;
}
default:
return false;
}
}
/** The tag of the arm of union `unionId` that `value` bakes into, or -1.
* A record value whose discriminant field holds one of the union's
* discriminant values takes that case's arm, since arms with overlapping
* shapes can both fit it. Any other value takes the first arm it fits, in
* the union's arm order: a JSON value fits exactly one scalar arm, and
* array or record arms are told apart by their contents. */
function comptimeUnionArm(lowerer: Lowerer, unionId: string, value: unknown): number {
const def = lowerer.unions.get(unionId);
if (!def) return -1;
const discriminant = def.discriminant;
if (
discriminant !== undefined &&
typeof value === "object" &&
value !== null &&
!Array.isArray(value)
) {
const key = (value as Record<string, unknown>)[discriminant.field];
const named = discriminant.cases.find((c) => c.values.some((v) => v === key));
if (named !== undefined)
return comptimeFits(lowerer, value, def.arms[named.tag]!) ? named.tag : -1;
}
return def.arms.findIndex((arm) => comptimeFits(lowerer, value, arm));
}
/** Whether `value` bakes into the bakeable type `t` without a mismatch:
* the arm choice of a union literal. Records are exact, as in
* comptimeValueToIr; an absent field reads as undefined. */
function comptimeFits(lowerer: Lowerer, value: unknown, t: IrType): boolean {
switch (t.kind) {
case "f64":
return typeof value === "number" && Number.isFinite(value);
case "string":
return typeof value === "string";
case "bool":
return typeof value === "boolean";
case "nullT":
return value === null;
case "undefinedT":
return value === undefined;
case "array":
return Array.isArray(value) && value.every((el) => comptimeFits(lowerer, el, t.elem));
case "record": {
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
const shape = lowerer.shapes.get(t.shapeId);
if (!shape) return false;
const obj = value as Record<string, unknown>;
const declared = new Set(shape.fields.map((f) => f.name));
return (
Object.keys(obj).every((key) => declared.has(key)) &&
shape.fields.every((f) => comptimeFits(lowerer, obj[f.name], f.type))
);
}
case "union":
return comptimeUnionArm(lowerer, t.unionId, value) >= 0;
default:
return false;
}
@@ -265,6 +349,23 @@ export function comptimeValueToIr(
}));
return { kind: "recordLit", fields, type: expected, loc };
}
case "union": {
const arms = lowerer.unions.get(expected.unionId)!.arms; // bakeable-checked
const tag = comptimeUnionArm(lowerer, expected.unionId, value);
if (tag < 0) fail(describeComptimeValue(value));
const arm = arms[tag]!;
const payload: IrExpr = isUnitType(arm)
? { kind: "unitLit", unit: arm.kind === "nullT" ? "null" : "undefined", type: arm, loc }
: lowerer.comptimeValueToIr(value, arm, path, blame);
return {
kind: "unionWrap",
unionId: expected.unionId,
tag,
value: payload,
type: expected,
loc,
};
}
default:
// comptimeBakeable already fenced these off.
throw new InternalCompilerError(`lowerer bug: unbakeable comptime target '${expected.kind}'`);
@@ -1245,7 +1245,19 @@ export function lowerEmitterMethodCall(
);
}
const p = cbT.params[0];
if (p !== undefined && !(p.kind === "bytes" && p.elem === "u8") && p.kind !== "string") {
// A `Buffer | string` chunk (NodeJS.ReadableStream's listener
// shape) takes whichever side the stream delivers.
const chunkUnion =
p?.kind === "union" &&
(lowerer.unions.get(p.unionId)?.arms ?? []).every(
(arm) => (arm.kind === "bytes" && arm.elem === "u8") || arm.kind === "string",
);
if (
p !== undefined &&
!(p.kind === "bytes" && p.elem === "u8") &&
p.kind !== "string" &&
!chunkUnion
) {
lowerer.noLowering(
`a 'data' listener whose chunk parameter is '${lowerer.fmt(p)}'`,
args[1]!,
@@ -1,4 +1,4 @@
import { refuseWebAssemblyUse } from "./builtins/webassembly.js";
import { lowerWebAssemblyValue, refuseWebAssemblyUse } from "./builtins/webassembly.js";
import { dynUndefinedExpr, nodeThrowExpr, numLit, strLit, varRef } from "../../ir/build.js";
import { InternalCompilerError } from "../../errors.js";
/* Expression lowering: the expression dispatch (lowerExpr), literals
@@ -152,6 +152,21 @@ export type FieldTarget =
// (the write-slot type; reads arm it with undefined under
// noUncheckedIndexedAccess in fieldGetExpr).
| { container: "recordOvf"; obj: IrExpr; shapeId: string; field: string; fieldType: IrType }
// A computed key of a record in a read-modify-write (`r[k] += v`,
// `r[k]++`, `r[k] ??= v`; keyed-field-targets.ts): `keys` lists the
// declared fields a literal-union key can name, which share fieldType;
// null keys mean a string key into a pure index-signature record, whose
// entries fieldType types. `key` is null when the one literal is static.
| {
container: "recordKeyed";
obj: IrExpr;
shapeId: string;
field: string;
keys: readonly string[] | null;
key: IrExpr | null;
fieldType: IrType;
missNaN?: true;
}
| { container: "accessor"; obj: IrExpr; className: string; field: string; fieldType: IrType }
// A RECORD accessor property (an object-literal `get x()`/`set x(v)` —
// the shape carries %get:/%set: closure slots): reads call the getter
@@ -264,7 +279,10 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
if (externalTypeSpecifier !== null) {
lowerer.externalHostFence(externalTypeSpecifier, expr);
}
// Every reached use of the WebAssembly API fences at its expression.
// WebAssembly API values lower; every other reached use of the API
// fences at its expression.
const webAssemblyValue = lowerWebAssemblyValue(lowerer, expr);
if (webAssemblyValue !== null) return webAssemblyValue;
refuseWebAssemblyUse(lowerer, expr);
if (ts.isNumericLiteral(expr)) {
@@ -467,7 +485,7 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
// Only arrows can see an enclosing method's `this` (JS lexical-this);
// `this` in plain nested functions is already a tsc error under
// noImplicitThis, so the generic scope walk here is preflight-safe.
const local = lowerer.resolveThis();
const local = lowerer.resolveThis(expr);
if (local) {
const value: IrExpr = { kind: "varRef", localId: local.id, type: local.type, loc };
// `this.#x` in a function-local class's prototype method, whose `this`
@@ -1040,6 +1058,15 @@ export function nullishBranches(
export function lowerCondition(lowerer: Lowerer, expr: ts.Expression): IrExpr {
let e: ts.Expression = expr;
while (ts.isParenthesizedExpression(e)) e = e.expression;
// `if (Object.setPrototypeOf)`: the feature test of transpiled Error
// subclasses' prototype reset. Node always has it.
if (
ts.isPropertyAccessExpression(e) &&
!e.questionDotToken &&
e.name.text === "setPrototypeOf" &&
lowerer.isStdlibGlobal(e.expression, "Object")
)
return { kind: "boolLit", value: true, type: BOOL, loc: locOf(e) };
if (ts.isPropertyAccessExpression(e)) {
const presence = lowerPromiseThenPresence(lowerer, e, {
kind: "boolLit",
@@ -2245,7 +2272,25 @@ export function caughtRead(
// Unnarrowed reads cross into unknown. The caught-value dispatch pass
// retains known native class payloads through live identity capsules;
// other payload kinds use the runtime's ordinary conversion.
if (narrowed?.kind === "dyn") {
// `catch (e: any)` (or useUnknownInCatchVariables off) reads the same way.
if (
narrowed?.kind === "dyn" ||
(narrowed === null && (lowerer.typeOf(node).flags & ts.TypeFlags.Any) !== 0)
) {
return { kind: "caughtToDyn", value: ref, type: DYN, loc };
}
// A binding a user type predicate (or an assertion) narrowed to a
// structural type (`isErrorLike(e)` → `{ message: string; … }`, an
// intersection like `TypeError & { code: unknown }`): the thrown value is
// still whatever was thrown, so it crosses as a checked-dynamic value and
// each member read validates its own result, as JavaScript reads it.
const checked = lowerer.typeOf(node);
if (
narrowed?.kind === "record" ||
narrowed?.kind === "union" ||
(narrowed === null && (checked.flags & (ts.TypeFlags.Object | ts.TypeFlags.Intersection)) !== 0)
) {
return { kind: "caughtToDyn", value: ref, type: DYN, loc };
}
lowerer.unsupported("SC1063", node);
@@ -1,3 +1,4 @@
import { declareTypedArguments } from "./typed-arguments.js";
import { varRef } from "../../ir/build.js";
import { InternalCompilerError } from "../../errors.js";
import * as ts from "../ts7/adapter.js";
@@ -309,6 +310,8 @@ export function lowerLambda(
: null;
const { params, prologue } = lowerer.declareParams(node.parameters.nodes, shapes);
if (receiver) prologue.unshift(receiver);
if (ts.isFunctionExpression(node) && lowerer.typedArgumentsLambdas.has(node))
declareTypedArguments(lowerer, node, shapes, params, prologue);
declareFunctionArguments(
lowerer,
node,
@@ -1,4 +1,5 @@
import { dynUndefinedExpr, boolLit, helperLoc, numLit, strLit, varRef } from "../../ir/build.js";
import { builtinClassDynValueOf, builtinClassKindOf } from "./builtins/builtin-class-dyn.js";
import { InternalCompilerError } from "../../errors.js";
import * as ts from "../ts7/adapter.js";
@@ -35,7 +36,6 @@ import type { ClassInfo } from "./lower-classes.js";
import { isSafeToDiscard, isSafeToRepeat } from "./expressions/evaluation-safety.js";
import { symbolFieldDisplayName } from "./symbol-fields.js";
import { eventTargetRootOf } from "./lower-event-target.js";
import { builtinClassRefOf } from "./builtins/class-values.js";
import { lowerSystemErrorMap } from "./lower-system-errors.js";
import { isPerfEntryType, perfEntryJson } from "./builtins/performance.js";
import { recordMayKeepSource, recordSourceTest } from "./record-sources.js";
@@ -2610,21 +2610,15 @@ function lowerUtilCallableCall(
const arity = member === "deprecate" ? 4 : member === "callbackify" ? 1 : 2;
// Program classes keep static prototypes: there is no function object
// whose prototype chain the runtime could relink or wrap.
// EventEmitter and the stream classes are the exception as superCtor:
// the runtime has untyped constructors for them (builtin-class-dyn.ts).
const classArgs = member === "inherits" ? 2 : member === "deprecate" ? 1 : 0;
// A runtime-provided parent (`util.inherits(Ctor, stream.Readable)`, the
// from2 and multistream shape) has no prototype object the runtime could
// link a constructor function's prototype to.
const parent = member === "inherits" ? expr.arguments.nodes[1] : undefined;
if (parent !== undefined && !ts.isSpreadElement(parent)) {
const ref = builtinClassRefOf(lowerer, parent);
if (ref !== null)
lowerer.noLowering(
`util.inherits onto the runtime-provided class '${ref.name}'`,
parent,
"declare the subclass with extends",
);
}
for (const arg of expr.arguments.nodes.slice(0, classArgs)) {
const builtinBase =
member === "inherits" && expr.arguments.nodes[1] !== undefined
? builtinClassKindOf(lowerer, expr.arguments.nodes[1])
: null;
for (const [index, arg] of expr.arguments.nodes.slice(0, classArgs).entries()) {
if (index === 1 && builtinBase !== null) continue;
if (lowerer.mapTypeOf(lowerer.typeOf(arg))?.kind === "classval")
lowerer.noLowering(
`util.${member} over a class constructor`,
@@ -2643,9 +2637,12 @@ function lowerUtilCallableCall(
const stmts: IrStmt[] = [];
const args: IrExpr[] = [];
for (let i = 0; i < arity; i++) {
const input: IrExpr = expr.arguments.nodes[i]
? lowerer.lowerExprExpecting(expr.arguments.nodes[i]!, DYN)
: dynUndefinedExpr(loc);
const node = expr.arguments.nodes[i];
const input: IrExpr = !node
? dynUndefinedExpr(loc)
: i === 1 && builtinBase !== null
? (builtinClassDynValueOf(lowerer, node) ?? lowerer.lowerExprExpecting(node, DYN))
: lowerer.lowerExprExpecting(node, DYN);
const saved = lowerer.declareHiddenLocal("%utilInput", DYN);
stmts.push({ kind: "varDecl", localId: saved.id, init: input, loc });
args.push(varRef(saved.id, DYN, loc));
@@ -925,6 +925,14 @@ export function moduleArtifacts(
valueInstanceOfRoots.add(root.def.name);
}
break;
case "libCall":
// The general stream representation's child stdio read bridges
// through the runtime install that boxes both stream classes.
if (node.fn === "child.stdioStream") {
visitClass("%Readable");
visitClass("%Writable");
}
break;
case "dynFrom":
if (boxedChild(node.value.type)) {
// A generic child can expose native stdio without importing stream.
@@ -4,6 +4,7 @@ import { InternalCompilerError } from "../../errors.js";
import * as ts from "../ts7/adapter.js";
import { lowerToNumberArgument } from "./lower-exprs.js";
import type { Lowerer } from "./lowerer.js";
import { isDeclaredInAmbientNamespace } from "../type-mapper.js";
import {
arrayOf,
BOOL,
@@ -1023,7 +1024,15 @@ export function fenceStaticAbortControllerMemberRead(
function isStdlibFetchInterface(lowerer: Lowerer, node: ts.Node, owner: string): ts.Symbol | null {
const type = lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(node));
for (const symbol of [type.getSymbol(), type.getAliasSymbol()]) {
if (symbol?.name === owner && lowerer.isStdlibSymbol(symbol)) return symbol;
// NodeJS.ReadableStream shares the WHATWG interface's name.
if (
symbol?.name === owner &&
lowerer.isStdlibSymbol(symbol) &&
!symbol.declarations.some(
(d) => ts.isInterfaceDeclaration(d) && isDeclaredInAmbientNamespace(d, "NodeJS"),
)
)
return symbol;
}
return null;
}
@@ -1,5 +1,7 @@
import { lowerTupleAliasDecl, stripTypeWrappers } from "./typed-arguments.js";
import { declaredMemberOf } from "./expressions/property-access.js";
import { hostsAll } from "./expressions/hosted-narrowing.js";
import { requestGeneralStreamSurface } from "./general-streams.js";
import {
matchAllRowsReadByElements,
storedMatchAllRowsReadByElements,
@@ -1495,6 +1497,8 @@ const THROWN_AS_DYN: ReadonlySet<IrType["kind"]> = new Set([
]);
export function lowerStmt(lowerer: Lowerer, stmt: ts.Statement): IrStmt | IrStmt[] | null {
if (ownPrototypeResetStatement(lowerer, stmt))
return { kind: "block", body: [], loc: locOf(stmt) };
if (ts.isClassDeclaration(stmt)) {
if (!stmt.name) lowerer.unsupported("SC1090", stmt, "anonymous local class declarations");
const info = lowerer.lowerClassExpressionInfo(stmt);
@@ -2417,20 +2421,30 @@ export function lowerBindingPattern(
lowerer.fenceStaticHeadersIteration(pattern);
if (
srcType.kind === "union" &&
lowerer.unions.get(srcType.unionId)?.arms.every((arm) => arm.kind === "array")
lowerer.unions
.get(srcType.unionId)
?.arms.every(
(arm) =>
arm.kind === "array" ||
arm.kind === "string" ||
(arm.kind === "record" && lowerer.shapes.get(arm.shapeId)?.tuple === true),
) &&
lowerer.unions.get(srcType.unionId)!.arms.some((arm) => arm.kind !== "string")
) {
for (let i = 0; i < pattern.elements.nodes.length; i++) {
const el = pattern.elements.nodes[i]!;
if (ts.isOmittedExpression(el) || el.name === undefined) continue;
if (el.dotDotDotToken)
lowerer.unsupported("SC1031", el, "rest elements over a union of array layouts");
lowerer.unsupported("SC1031", el, "rest elements over a union of array or tuple layouts");
const loc = locOf(el);
let value = unionArrayValueRead(lowerer, srcRef(), numLit(i, loc), loc);
let value = unionArrayValueRead(lowerer, srcRef(), numLit(i, loc), loc, (text) =>
strCharsCall(lowerer, text, loc),
);
if (!value)
lowerer.unsupported(
"SC1031",
el,
"array union elements without a common native representation",
"array or tuple union elements without a common native representation",
);
if (el.initializer) value = applyBindingDefault(lowerer, el, value);
lowerer.bindPatternTarget(el.name, value, isLet, out);
@@ -4751,6 +4765,10 @@ export function lowerVarDecl(
}
}
if (!ts.isIdentifier(decl.name)) lowerer.unsupported("SC1031", decl.name);
{
const alias = lowerTupleAliasDecl(lowerer, decl);
if (alias) return alias;
}
// A NULLISH generic binding (`const i: I<A & B> = null as any` — the
// declared type has no mapping, the value is provably null/undefined
@@ -5171,6 +5189,16 @@ export function lowerVarDecl(
init.type.kind === "func"
)
type = init.type;
// A typed-arguments function expression (typed-arguments.ts): the
// checker types the binding variadic, but its value has the fixed
// signature every use asserts.
if (
!decl.type &&
init.type.kind === "func" &&
(lowerer.typedArgumentsLambdas.has(stripTypeWrappers(decl.initializer)) ||
lowerer.nullishFuncResults.has(stripTypeWrappers(decl.initializer)))
)
type = init.type;
// A TypeScript lambda whose closure returns the runtime result (a missing
// read, a `??` default of another type) instead of the checker's bare
// return type: the binding keeps the checker's parameters and takes the
@@ -7463,6 +7491,34 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm
loc,
};
}
// `err.code = v` on an Error value (`err as Error & { code?:
// string }`): typed reads come from the runtime error's code
// slot, so the write lands there. A value that is not statically
// a string checks at runtime: strings take the slot, anything
// else clears it and stays a dynamic own property. The
// system-error members (errno, syscall, path, dest) read
// through the checked-dynamic node and keep the dynamic write, as
// do JavaScript files, whose reads use that node too.
if (
!isJsSourceFile(expr.getSourceFile()) &&
recv?.type.kind === "object" &&
lowerer.isErrorRootedClass(recv.type.className) &&
expr.left.name.text === "code"
) {
const loc = locOf(expr);
const stringValue = isStringTyped(lowerer.typeOf(expr.right));
return {
kind: "exprStmt",
expr: {
kind: "libCall",
fn: stringValue ? "error.setCode" : "error.setCodeDyn",
args: [recv, lowerer.lowerExprExpecting(expr.right, stringValue ? STRING : DYN)],
type: VOID,
loc,
},
loc,
};
}
if (recv?.type.kind === "object" && recv.type.className === "%Error") {
const loc = locOf(expr);
return {
@@ -8982,6 +9038,8 @@ export function lowerForOf(lowerer: Lowerer, stmt: ts.ForOfStatement): IrStmt {
}
const builtin = lowerForAwaitBuiltin(lowerer, stmt);
if (builtin) return builtin;
// A NodeJS.ReadableStream iterates as the general Readable.
requestGeneralStreamSurface(lowerer, stmt.expression);
lowerer.unsupported(
"SC1070",
stmt,
@@ -11286,6 +11344,141 @@ export function stdlibBufferVarAlias(lowerer: Lowerer, decl: ts.VariableDeclarat
);
}
/** A checker type that is a string or string literal (or a union of them). */
function isStringTyped(t: ts.Type): boolean {
if (t.isUnionType()) return ts.constituentTypes(t).every(isStringTyped);
return (t.flags & (ts.TypeFlags.String | ts.TypeFlags.StringLiteral)) !== 0;
}
/** The Error-subclass prototype reset of transpiled code, inside a class
* constructor:
*
* const proto = new.target.prototype;
* if (Object.setPrototypeOf) Object.setPrototypeOf(this, proto);
* else this.__proto__ = proto;
*
* (also `Object.setPrototypeOf(this, new.target.prototype)` and
* `Object.setPrototypeOf(this, C.prototype)` in C's own constructor when no
* class extends C). A compiled instance already has exactly that prototype,
* so each statement is a no-op. True for those statements. */
function ownPrototypeResetStatement(lowerer: Lowerer, stmt: ts.Statement): boolean {
if (ts.isVariableStatement(stmt)) {
const decls = stmt.declarationList.declarations.nodes;
if (decls.length !== 1 || (stmt.declarationList.flags & ts.NodeFlags.Const) === 0) return false;
const decl = decls[0]!;
if (!ts.isIdentifier(decl.name) || !decl.initializer) return false;
const ctor = enclosingConstructor(decl);
if (!ctor || !ownPrototypeRef(lowerer, decl.initializer, ctor)) return false;
const symbol = lowerer.checker.getSymbolAtLocation(decl.name);
if (!symbol) return false;
// Every other use of the binding must be one of the reset statements.
let onlyResets = true;
ts.walkPreorder(ctor, (n) => {
if (!onlyResets) return "stop";
if (!ts.isIdentifier(n) || n === decl.name) return undefined;
if (lowerer.checker.getSymbolAtLocation(n) !== symbol) return undefined;
const statement = n.parent?.parent;
if (
!statement ||
!ts.isExpressionStatement(statement) ||
resetTarget(lowerer, statement.expression) !== n
)
onlyResets = false;
return undefined;
});
return onlyResets;
}
if (!ts.isExpressionStatement(stmt)) return false;
const ctor = enclosingConstructor(stmt);
if (!ctor) return false;
const target = resetTarget(lowerer, stmt.expression);
return target !== null && ownPrototypeRef(lowerer, target, ctor);
}
/** The prototype operand of `Object.setPrototypeOf(this, P)` or
* `this.__proto__ = P`, or null. */
function resetTarget(lowerer: Lowerer, expr: ts.Expression): ts.Expression | null {
if (
ts.isCallExpression(expr) &&
expr.arguments.nodes.length === 2 &&
expr.arguments.nodes[0]!.kind === ts.SyntaxKind.ThisKeyword &&
ts.isPropertyAccessExpression(expr.expression) &&
expr.expression.name.text === "setPrototypeOf" &&
lowerer.isStdlibGlobal(expr.expression.expression, "Object")
)
return expr.arguments.nodes[1]!;
if (
ts.isBinaryExpression(expr) &&
expr.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
ts.isPropertyAccessExpression(expr.left) &&
expr.left.expression.kind === ts.SyntaxKind.ThisKeyword &&
expr.left.name.text === "__proto__"
)
return expr.right;
return null;
}
/** The class constructor whose body holds `node`, crossing only statements
* and blocks (any nested function rebinds `this`). */
function enclosingConstructor(node: ts.Node): ts.ConstructorDeclaration | null {
for (let cur = node.parent; cur !== undefined; cur = cur.parent) {
if (ts.isConstructorDeclaration(cur)) return cur;
if (
ts.isFunctionLike(cur) ||
ts.isClassDeclaration(cur) ||
ts.isClassExpression(cur) ||
ts.isSourceFile(cur)
)
return null;
}
return null;
}
/** Whether `node` denotes the prototype of the object `ctor` constructs:
* `new.target.prototype`, a const holding it, or the class's own
* `C.prototype` when no class extends C. */
function ownPrototypeRef(
lowerer: Lowerer,
node: ts.Expression,
ctor: ts.ConstructorDeclaration,
): boolean {
while (ts.isParenthesizedExpression(node)) node = node.expression;
if (ts.isPropertyAccessExpression(node) && node.name.text === "prototype") {
const base = node.expression;
if (
ts.isMetaProperty(base) &&
base.keywordToken === ts.SyntaxKind.NewKeyword &&
base.name.text === "target"
)
return true;
const cls = ctor.parent;
if (!ts.isIdentifier(base) || !(ts.isClassDeclaration(cls) || ts.isClassExpression(cls)))
return false;
const symbol = lowerer.resolveValueSymbol(base);
const info = symbol ? lowerer.classBySymbol.get(symbol) : undefined;
if (!info || symbol!.valueDeclaration !== cls) return false;
for (const other of lowerer.classes.values()) {
if (other !== info && lowerer.isSubclassOf(other.def.name, info.def.name)) return false;
}
return true;
}
if (ts.isIdentifier(node)) {
const symbol = lowerer.resolveValueSymbol(node);
const decl = symbol?.valueDeclaration;
return (
decl !== undefined &&
ts.isVariableDeclaration(decl) &&
decl.initializer !== undefined &&
ts.isVariableDeclarationList(decl.parent) &&
(decl.parent.flags & ts.NodeFlags.Const) !== 0 &&
enclosingConstructor(decl) === ctor &&
!ts.isIdentifier(decl.initializer) &&
ownPrototypeRef(lowerer, decl.initializer, ctor)
);
}
return false;
}
function isAssertionInitializer(node: ts.Expression): boolean {
let e = node;
while (ts.isParenthesizedExpression(e)) e = e.expression;
@@ -49,6 +49,7 @@ import { sideEffectFreeOptionValue } from "./surfaces.js";
import { lowerDynObjectLiteral } from "./expressions/object-literals.js";
import { dynUndefinedExpr, varRef } from "../../ir/build.js";
import { genResultRecord } from "../type-mapper.js";
import { requestGeneralStreamSurface } from "./general-streams.js";
import { extractIteratorValue, genStep, genValue, stepDrivable } from "./lower-generators.js";
import {
BOOL,
@@ -586,6 +587,33 @@ function dynOptionsValue(lowerer: Lowerer, arg: ts.Expression): IrExpr | null {
return v.type.kind === "dyn" ? v : null;
}
/** @types/node declares the transform/flush completion callback as
* `(error?: Error | null, data?: any) => void`. Outside objectMode Node
* accepts only a Buffer, a string or nothing as `data`, so the callback
* parameter is typed with that union while the option callback lowers: a
* checked-dynamic value passed as `data` converts at the call. Returns the
* pinned parameter symbol (the caller unpins it), or null when the
* declared type needs no pin. */
function pinAnyDataCompletion(
lowerer: Lowerer,
which: string,
fnNode: ts.MethodDeclaration | ts.FunctionExpression | ts.ArrowFunction,
): ts.Symbol | null {
const index = which === "transform" ? 2 : which === "flush" ? 0 : -1;
const param = index < 0 ? undefined : fnNode.parameters.nodes[index];
if (param === undefined || !ts.isIdentifier(param.name) || param.dotDotDotToken) return null;
const symbol = lowerer.checker.getSymbolAtLocation(param.name);
if (!symbol || lowerer.stageSourceTypes.has(symbol)) return null;
const declared = lowerer.paramShape(param).type;
if (declared.kind !== "func" || declared.rest || declared.params[1]?.kind !== "dyn") return null;
const data = unionOf(lowerer, [BYTES, STRING, { kind: "undefinedT" }]);
lowerer.stageSourceTypes.set(
symbol,
funcOf([declared.params[0]!, data, ...declared.params.slice(2)], declared.ret),
);
return symbol;
}
/** Parses the options object literal of a stream constructor. `which`
* names the callbacks this class accepts, in canonical flag order. */
function parseStreamOptions(
@@ -671,22 +699,27 @@ function parseStreamOptions(
continue;
}
const full = tuples[name]!.map((t) => (t === "fn" ? null : t));
// The completion-callback tail: typed by the checker contextually;
// accept the user's declared func type as the tuple's entry so the
// prefix check compares like with like.
const { funcType } = lowerer.lambdaSignature(fnNode);
const tuple: IrType[] = full.map((t, i) => {
if (t !== null) return t;
const declared = funcType.params[i];
if (declared !== undefined && declared.kind !== "func" && declared.kind !== "dyn") {
lowerer.noLowering(
`a '${name}' callback whose parameter ${i} is '${lowerer.fmt(declared)}' where Node passes the completion callback`,
fnNode!,
);
}
return declared ?? funcOf([], VOID);
});
present.set(name, lowerStreamCallback(lowerer, name, fnNode, thisType, tuple));
const pinned = pinAnyDataCompletion(lowerer, name, fnNode);
try {
// The completion-callback tail: typed by the checker contextually;
// accept the user's declared func type as the tuple's entry so the
// prefix check compares like with like.
const { funcType } = lowerer.lambdaSignature(fnNode);
const tuple: IrType[] = full.map((t, i) => {
if (t !== null) return t;
const declared = funcType.params[i];
if (declared !== undefined && declared.kind !== "func" && declared.kind !== "dyn") {
lowerer.noLowering(
`a '${name}' callback whose parameter ${i} is '${lowerer.fmt(declared)}' where Node passes the completion callback`,
fnNode!,
);
}
return declared ?? funcOf([], VOID);
});
present.set(name, lowerStreamCallback(lowerer, name, fnNode, thisType, tuple));
} finally {
if (pinned) lowerer.stageSourceTypes.delete(pinned);
}
continue;
}
// `{ signal }` shorthand: the binding is the value.
@@ -2044,6 +2077,24 @@ function lowerStreamArg(
const v = isHashValueType(lowered.type)
? hashStreamOf(lowered, what === "pipeline" || what === "compose")
: lowered;
// A checked-dynamic pipeline stage (an untyped source or destination):
// a boxed native stream unwraps at run time; other values (iterables,
// functions) throw the not-supported Error there.
if (v.type.kind === "dyn" && what === "pipeline") {
return {
kind: "libCall",
fn: "stream.unboxDyn",
args: [
v,
strLit(
"pipeline stages other than native streams (iterables, generators, plain functions, and { readable, writable } pairs) are not supported yet",
locOf(node),
),
],
type: { kind: "object", className: "%Duplex" },
loc: locOf(node),
};
}
const info = v.type.kind === "object" ? lowerer.classes.get(v.type.className) : undefined;
if (!streamSidesOf(lowerer, info)) {
// A provably-non-stream value in a JS source (the invalid-input
@@ -3296,6 +3347,124 @@ function pipeEndOption(lowerer: Lowerer, options: ts.Expression | undefined, loc
return end;
}
/** `readable.pipe(process.stdout)`: the process stream joins the pipe as
* an fd-backed Writable (stream.stdioWritable), and — like Node, which
* never ends process.stdout or process.stderr from a pipe — the source's
* end does not end it. pipe() answers the process stream itself. */
function pipeIntoStdio(
lowerer: Lowerer,
call: ts.CallExpression,
access: ts.PropertyAccessExpression,
target: IrExpr,
): IrExpr {
const loc = locOf(call);
// JS order: the receiver, then the destination, then the options.
const receiverValue = lowerer.lowerExpr(access.expression);
const source = lowerer.declareHiddenLocal("%pipeSource", receiverValue.type);
const stdio = lowerer.declareHiddenLocal("%pipeStdio", target.type);
const options = call.arguments.nodes[1];
const stmts: IrStmt[] = [
{ kind: "varDecl", localId: source.id, init: receiverValue, loc },
{ kind: "varDecl", localId: stdio.id, init: target, loc },
];
// The options literal still evaluates; its end flag has no effect here.
if (options !== undefined) {
stmts.push({ kind: "exprStmt", expr: pipeEndOption(lowerer, options, loc), loc });
}
const receiver: IrExpr = { kind: "varRef", localId: source.id, type: receiverValue.type, loc };
const writable: IrType = { kind: "object", className: "%Writable" };
stmts.push({
kind: "exprStmt",
expr: {
kind: "libCall",
fn: "readable.pipe",
args: [
receiver,
{
kind: "libCall",
fn: "stream.stdioWritable",
args: [{ kind: "varRef", localId: stdio.id, type: target.type, loc }],
type: writable,
loc,
},
boolLit(false, loc),
],
type: writable,
loc,
},
loc,
});
return {
kind: "seqExpr",
stmts,
result: { kind: "varRef", localId: stdio.id, type: target.type, loc },
type: target.type,
loc,
};
}
/** The pipe() libCall for a destination of type `dst`. Under the general
* stream representation a Writable slot can hold the converted
* process.stdout / process.stderr, which a pipe never ends (as in Node), so
* the pipe takes readable.pipeGeneral, which checks for them. */
function generalPipe(lowerer: Lowerer, dst: IrType): "readable.pipe" | "readable.pipeGeneral" {
return lowerer.generalNodeStreams && dst.kind === "object" && dst.className === "%Writable"
? "readable.pipeGeneral"
: "readable.pipe";
}
/** `readable.pipe(untyped)`: a checked-dynamic destination unwraps a boxed
* native stream at run time (stream.unboxDyn; other values throw the
* not-supported Error), and pipe() answers the destination value itself. */
function pipeIntoDyn(
lowerer: Lowerer,
call: ts.CallExpression,
access: ts.PropertyAccessExpression,
target: IrExpr,
): IrExpr {
const loc = locOf(call);
const receiverValue = lowerer.lowerExpr(access.expression);
const source = lowerer.declareHiddenLocal("%pipeSource", receiverValue.type);
const destination = lowerer.declareHiddenLocal("%pipeDestination", target.type);
const stmts: IrStmt[] = [
{ kind: "varDecl", localId: source.id, init: receiverValue, loc },
{ kind: "varDecl", localId: destination.id, init: target, loc },
];
const end = pipeEndOption(lowerer, call.arguments.nodes[1], loc);
const duplex: IrType = { kind: "object", className: "%Duplex" };
stmts.push({
kind: "exprStmt",
expr: {
kind: "libCall",
fn: lowerer.generalNodeStreams ? "readable.pipeGeneral" : "readable.pipe",
args: [
{ kind: "varRef", localId: source.id, type: receiverValue.type, loc },
{
kind: "libCall",
fn: "stream.unboxDyn",
args: [
{ kind: "varRef", localId: destination.id, type: target.type, loc },
strLit("pipe destinations other than native streams are not supported yet", loc),
],
type: duplex,
loc,
},
end,
],
type: duplex,
loc,
},
loc,
});
return {
kind: "seqExpr",
stmts,
result: { kind: "varRef", localId: destination.id, type: target.type, loc },
type: target.type,
loc,
};
}
export function lowerStreamMethodCall(
lowerer: Lowerer,
call: ts.CallExpression,
@@ -3545,22 +3714,26 @@ export function lowerStreamMethodCall(
loc,
};
}
const dst = lowerer.lowerExpr(args[0]!);
const target = lowerer.lowerExpr(args[0]!);
if (target.type.kind === "procStream") return pipeIntoStdio(lowerer, call, access, target);
if (target.type.kind === "dyn") return pipeIntoDyn(lowerer, call, access, target);
const dst = target;
const dstInfo =
dst.type.kind === "object" ? lowerer.classes.get(dst.type.className) : undefined;
const dstSides = streamSidesOf(lowerer, dstInfo);
if (dstSides !== "w" && dstSides !== "rw") {
requestGeneralStreamSurface(lowerer, args[0]!);
lowerer.noLowering(
`pipe into a '${lowerer.fmt(dst.type)}'`,
args[0]!,
"the destination must be a stream Writable/Duplex (process streams and sockets are not pipe destinations yet)",
"the destination must be a stream Writable/Duplex, process.stdout or process.stderr (sockets are not pipe destinations yet)",
);
}
const end = pipeEndOption(lowerer, args[1], loc);
const receiver = lowerer.lowerExpr(access.expression);
return {
kind: "libCall",
fn: "readable.pipe",
fn: generalPipe(lowerer, dst.type),
args: [receiver, dst, end],
type: dst.type,
loc,
@@ -1,5 +1,8 @@
import { onlyForwarded } from "./typed-arguments.js";
import { needsGeneralNodeStreams, stdioIntoGeneralStream } from "./general-streams.js";
import { extendAssertedRecordMembers } from "./assertion-members.js";
import { jsDynamicDeclarations } from "./js-dynamic-objects.js";
import { builtinClassValue, builtinMemberClassKind } from "./builtins/builtin-class-dyn.js";
import {
lifetimeRegionType,
lowerLifetimeRegionCall,
@@ -30,8 +33,13 @@ import { isSettledIteratorValueRead } from "./lower-generators.js";
import { concreteCollectionNarrow } from "./collection-narrowing.js";
import { lowerWorkerMetadata } from "./builtins/workers.js";
import { fenceStaticBlobMember, lowerBlobNew, lowerStaticBlobCall } from "./builtins/blob.js";
import { refuseWebAssemblyUse } from "./builtins/webassembly.js";
import {
lowerWebAssemblyCall,
lowerWebAssemblyNew,
refuseWebAssemblyUse,
} from "./builtins/webassembly.js";
import { discoverWasmModules, type WasmModuleReport } from "../../wasm/discover.js";
import { encodeBase64 } from "../../wasm/base64.js";
import { resolveRequireRuntime } from "../runtime-resolve.js";
import { lowerKeyObjectCall } from "./builtins/key-object.js";
import { fenceWebCryptoMember, lowerWebCryptoCall } from "./builtins/webcrypto.js";
@@ -186,6 +194,7 @@ import type {
IrGlobal,
IrLocal,
IrModule,
IrWasmModule,
IrParam,
IrRecordShape,
IrStmt,
@@ -881,6 +890,10 @@ export interface LowererMode {
/** Statically resolved child_process.fork program roots, in stable target-id order. */
forkTargets?: readonly ts.SourceFile[];
frontendServices?: FrontendServices | undefined;
/** The program's general stream representation (TypeMapperCtx.
* generalNodeStreams). lowerToIr turns it on only after a pass without
* it saw a stream flow that needs it (requiresGeneralNodeStreams). */
generalNodeStreams?: boolean;
/** LoadResult.inlineRequireCycleCalls. */
inlineRequireCycleCalls?: ReadonlySet<ts.Node> | undefined;
/** LowerOptions.libraryCallbacks (see there). */
@@ -1041,10 +1054,40 @@ export function lowerToIr(
for (const d of extraModuleCycleDiags) reachableEmit.pushDiag(d);
for (const d of ffiValidation.diagnostics) reachableEmit.pushDiag(d);
const emitted = reachableEmit.emitReachable(options.libRoots);
const { reachable } = emitted;
let { reachable } = emitted;
let result = emitted.result;
let resultLowerer = reachableEmit;
timing("reachable-emit", { reachable: reachable.size });
// The general stream representation (TypeMapperCtx.generalNodeStreams)
// is a per-program decision: only a program whose reachable code moved
// a general stream into a stdio-represented stream slot lowers again
// with it. Every other program keeps the single pass above unchanged.
const generalNodeStreams = reachableEmit.requiresGeneralNodeStreams;
if (generalNodeStreams) {
const general = new Lowerer(program, entry, moduleOrder, {
frontendServices: options.frontendServices,
projectCheckJs: options.projectCheckJs ?? false,
targetPlatform,
startupCrash,
inlineRequireCycleCalls,
ffiImports,
libraryCallbacks,
forkTargets,
workerTargets,
externalTypes,
externalTypeSpecifiersByFile,
tierMode,
ffiBindingSymbols: ffiValidation.symbolsByName,
generalNodeStreams: true,
});
for (const d of extraModuleCycleDiags) general.pushDiag(d);
for (const d of ffiValidation.diagnostics) general.pushDiag(d);
const generalEmitted = general.emitReachable(options.libRoots);
reachable = generalEmitted.reachable;
result = generalEmitted.result;
resultLowerer = general;
timing("general-streams-relower", { reachable: reachable.size });
}
// A generic class-rest support decision can depend on record metadata
// whose historical owner is discovered only while retained bodies lower.
// If the settled answer is a fence, rerun the ordinary reachable emit so
@@ -1052,10 +1095,11 @@ export function lowerToIr(
// declarators stay unvisited, bindings block, cascades and stats match the
// historical compiler. This is a rare compatibility fallback; programs
// without such a settled fence retain checker-backed IR exactly once.
if (reachableEmit.requiresHistoricalOrderRelower) {
if (resultLowerer.requiresHistoricalOrderRelower) {
const emit = new Lowerer(program, entry, moduleOrder, {
frontendServices: options.frontendServices,
projectCheckJs: options.projectCheckJs ?? false,
...(generalNodeStreams ? { generalNodeStreams: true } : {}),
reachable,
targetPlatform,
startupCrash,
@@ -1111,25 +1155,70 @@ export function lowerToIr(
diagnostics: [...result.diagnostics, ...contracts.diagnostics],
};
else if (contracts.count > 0) result = { ...result, contracts };
const discoverModules = (
onModule?: (sha256: string, bytes: Uint8Array) => void,
): WasmModuleReport[] =>
discoverWasmModules({
files: moduleOrder.includes(entry) ? moduleOrder : [...moduleOrder, entry],
entry: entry.fileName,
configured: options.wasmFiles ?? [],
resolveRequest: (fromFile, specifier) => {
const resolved = resolveRequireRuntime(
fromFile,
specifier,
targetPlatform,
undefined,
options.frontendServices,
);
return resolved.ok ? resolved.value : null;
},
...(onModule === undefined ? {} : { onModule }),
});
// A program that uses the WebAssembly API carries every module the build
// can see and translate (wasm/translate.ts); the rest fail at run time
// with the CompileError for untranslated bytes.
if (result.module !== null && resultLowerer.usesWebAssembly) {
const bytesByHash = new Map<string, Uint8Array>();
const reports = discoverModules((sha256, bytes) => bytesByHash.set(sha256, bytes));
const wasmModules: IrWasmModule[] = [];
for (const report of reports) {
const bytes = bytesByHash.get(report.sha256);
if (report.status !== "valid" || bytes === undefined) continue;
wasmModules.push({ sha256: report.sha256, bytes: encodeBase64(bytes) });
}
result.module.wasmModules = wasmModules;
if (resultLowerer.usesWasmCustomSections) result.module.wasmCustomSections = true;
timing("wasm-discovery", { modules: wasmModules.length });
}
if (options.coverage !== true) return result;
const remainder = new Lowerer(program, entry, moduleOrder, {
frontendServices: options.frontendServices,
projectCheckJs: options.projectCheckJs ?? false,
reachable,
remainder: true,
alreadyFlushed: resultLowerer.flushedSymbols,
inlineRequireCycleCalls,
targetPlatform,
ffiImports,
libraryCallbacks,
forkTargets,
workerTargets,
ffiBindingSymbols: ffiValidation.symbolsByName,
externalTypes,
externalTypeSpecifiersByFile,
tierMode,
});
const rem = remainder.run();
const remainderPass = (general: boolean): Lowerer =>
new Lowerer(program, entry, moduleOrder, {
frontendServices: options.frontendServices,
projectCheckJs: options.projectCheckJs ?? false,
...(general ? { generalNodeStreams: true } : {}),
reachable,
remainder: true,
alreadyFlushed: resultLowerer.flushedSymbols,
inlineRequireCycleCalls,
targetPlatform,
ffiImports,
libraryCallbacks,
forkTargets,
workerTargets,
ffiBindingSymbols: ffiValidation.symbolsByName,
externalTypes,
externalTypeSpecifiersByFile,
tierMode,
});
let remainder = remainderPass(generalNodeStreams);
let rem = remainder.run();
// Unreached code reports what it would compile to once reached: a
// stream flow there that needs the general representation lowers the
// remainder again with it, as reachable emit would.
if (!generalNodeStreams && remainder.requiresGeneralNodeStreams) {
remainder = remainderPass(true);
rem = remainder.run();
}
timing("coverage-remainder");
// Tiers (tier-analysis.ts): reached statements in dynamic-tier functions,
// and with tierReport the report-only demotion analysis.
@@ -1164,21 +1253,7 @@ export function lowerToIr(
},
};
if (tiers !== undefined) covered.tiers = tiers;
const wasmModules = discoverWasmModules({
files: moduleOrder.includes(entry) ? moduleOrder : [...moduleOrder, entry],
entry: entry.fileName,
configured: options.wasmFiles ?? [],
resolveRequest: (fromFile, specifier) => {
const resolved = resolveRequireRuntime(
fromFile,
specifier,
targetPlatform,
undefined,
options.frontendServices,
);
return resolved.ok ? resolved.value : null;
},
});
const wasmModules = discoverModules();
timing("wasm-discovery", { modules: wasmModules.length });
if (wasmModules.length > 0) covered.wasmModules = wasmModules;
return covered;
@@ -1929,6 +2004,18 @@ export class Lowerer {
* type parameters are bound to `unknown`, so lambdaSignature admits
* them. */
readonly erasingLambdas = new Set<ts.Node>();
/** Function expressions lowered against a fixed target signature with a
* tuple-typed `arguments` (typed-arguments.ts). */
readonly typedArgumentsLambdas = new Set<ts.Node>();
/** Const bindings holding a forwarded fixed tuple (`const args =
* arguments as any` in a typed-arguments function) → the tuple type. */
readonly tupleAliasSymbols = new Map<ts.Symbol, IrType>();
/** `??` expressions whose result is the optional callback's own function
* type (an adapted function default); their bindings take that type. */
readonly nullishFuncResults = new Set<ts.Node>();
/** The static names of the current generic instance's name-keyed
* function parameters (static-function-names.ts). */
staticFnNames: Map<ts.Symbol, string | null> | null = null;
/** Nesting depth of erased generic bodies being lowered (erased lambdas
* and erased instances). */
erasingDepth = 0;
@@ -2030,7 +2117,9 @@ export class Lowerer {
/** A pipeline() function stage's `source` parameter: the IR type of what
* the pipeline passes it (the previous stage as an async iterable), set
* while the stage lowers. Parameter typing and `for await` heads read
* it before the checker's `AsyncIterable<T>`. */
* it before the checker's `AsyncIterable<T>`. A stream option callback's
* completion parameter declared with an any-typed `data` is pinned here
* too while the callback lowers (pinAnyDataCompletion). */
readonly stageSourceTypes = new Map<ts.Symbol, IrType>();
/** Set when a match result lowers to execGroups/matchGroups, the inputs
* of regex span folding. */
@@ -2508,6 +2597,12 @@ export class Lowerer {
/** Settled generic class-rest metadata requires the historical emit
* fallback so its fence can poison the original statement atomically. */
requiresHistoricalOrderRelower = false;
/** A general stream value (a runtime stream class instance) reached a
* slot the child/process stdio handles represent — NodeJS.ReadableStream,
* NodeJS.WritableStream, or @types/node's stream.Readable/Writable — or
* a stdio handle reached an 'unknown' slot. lowerToIr then lowers the
* program again under the general stream representation. */
requiresGeneralNodeStreams = false;
/** Helpers that snapshot shape declaration order into their bodies.
* Reachability lowers inits before the declarations they discover, so
* these rebuild after the worklist restores historical shape metadata. */
@@ -2762,6 +2857,12 @@ export class Lowerer {
readonly workerTargets: readonly ts.SourceFile[];
readonly workerTargetIdByPath = new Map<string, number>();
usesWorkers = false;
/** The program uses the WebAssembly API (builtins/webassembly.ts): the
* build translates the modules it discovers. */
usesWebAssembly = false;
/** The program calls WebAssembly.Module.customSections: translated
* modules keep their custom sections. */
usesWasmCustomSections = false;
/** The program constructs a LifetimeRegion (lifetime-regions.ts). */
usesLifetimeRegions = false;
/** Per-directory answers of experimental-import.ts's declaration-file test. */
@@ -2963,6 +3064,7 @@ export class Lowerer {
* refuse and fence exactly as before. Stage S6 demotes the units of
* committed refusals in "auto" and keeps them refusals in "static-only". */
readonly tierMode: TierMode;
readonly generalNodeStreams: boolean;
/** Functions finishModule placed in the dynamic tier (ir/tiers.ts). */
dynamicFunctions: DynamicFunction[] = [];
/** Their IR, for coverage's dynamic statement count. */
@@ -3091,6 +3193,7 @@ export class Lowerer {
this.startupCrash = mode.startupCrash ?? null;
this.targetPlatform = mode.targetPlatform ?? process.platform;
this.tierMode = mode.tierMode ?? "auto";
this.generalNodeStreams = mode.generalNodeStreams ?? false;
this.workerTargets = mode.workerTargets ?? [];
this.workerTargets.forEach((sf, id) => {
this.workerTargetIdByPath.set(tsgoPath(resolve(sf.fileName)), id);
@@ -3187,6 +3290,7 @@ export class Lowerer {
isExternalTypeFile: (sf) =>
this.externalTypeSpecifiersByFile.has(tsgoPath(resolve(sf.fileName))),
typeMemo: this.typeMemo,
...(this.generalNodeStreams ? { generalNodeStreams: true } : {}),
canMemoizeType: () =>
this.typeParamBindings === null &&
this.typeParamTsBindings === null &&
@@ -8471,6 +8575,8 @@ export class Lowerer {
* validator ICEs on them as the usual backstop. */
requireExactShape(node: ts.Node, actual: IrType, expected: IrType): void {
if (typeEquals(actual, expected)) return;
if (!this.generalNodeStreams && needsGeneralNodeStreams(this, actual, expected))
this.requiresGeneralNodeStreams = true;
// dyn mismatches first. A dyn ('unknown') value flowing into a typed
// slot needs a CHECKED cast — the hint points at `as <type>`; tsc
// usually rejects this before we do, so the fence is mostly defensive.
@@ -8688,6 +8794,9 @@ export class Lowerer {
}
if (expr.type.kind === "void" && expected.kind !== "void" && this.neverValued.has(expr))
return this.divergentValue(expr, expected);
// process.stdout/stderr into a general Writable slot (general-streams.ts).
const stdioStream = stdioIntoGeneralStream(this, expr, expected);
if (stdioStream) return this.coerceToExpected(stdioStream, expected);
// Iterator-typed slots: native iterators and differently typed
// generators step through an adapter generator.
if (expected.kind === "generator" && !typeEquals(expr.type, expected)) {
@@ -9723,6 +9832,8 @@ export class Lowerer {
node = node.expression;
if (expected) {
if (ts.isConditionalExpression(node)) return lowerTernary(this, node, expected);
const escaped = this.neverAssertionInto(node, expected);
if (escaped) return escaped;
const fresh = this.emptyCollectionFor(node, expected);
if (fresh) return this.coerceInto(node, fresh, expected);
}
@@ -10636,6 +10747,14 @@ export class Lowerer {
return findMethodOn(this, info, name);
}
/** A class whose root is the runtime's %Error (builtin errors and their
* program subclasses). */
isErrorRootedClass(className: string): boolean {
let info = this.classes.get(className) ?? null;
while (info && info.base) info = info.base;
return info?.def.name === "%Error";
}
isSubclassOf(sub: string, sup: string): boolean {
return isSubclassOf(this, sub, sup);
}
@@ -10922,6 +11041,108 @@ export class Lowerer {
return declareContextLocal(this.ctx, name, type, false, undefined, undefined);
}
/** The body binding of a fixed-tuple rest parameter: one native slot per
* position (the flattened ABI), rebuilt into the tuple the source names.
* The empty tuple rides the empty-array representation. */
declareRestTupleParam(
decl: ts.ParameterDeclaration,
nameNode: ts.Identifier,
elemShapes: readonly ParamShape[],
params: IrParam[],
prologue: IrStmt[],
): void {
const loc = locOf(decl);
const name = nameNode.text;
const fn = decl.parent;
const symbol = this.checker.getSymbolAtLocation(nameNode);
const isRead = (id: ts.Identifier): boolean =>
id !== nameNode && this.checker.getSymbolAtLocation(id) === symbol;
const pure = fn !== undefined && symbol !== undefined && onlyForwarded(this, fn, isRead);
// A forwarded-only tuple is never built: a tuple with optional
// positions (`Parameters<(n?: T) => R>`, whose length varies per call)
// forwards its slots, an omitted position as undefined — which a
// callee's default treats exactly as an omitted argument. Its length
// is the one read that would observe the difference.
const tupleType =
pure &&
elemShapes.length > 0 &&
this.mapTypeOf(this.typeOf(nameNode)) === null &&
!readsLength(fn!, isRead)
? this.forwardedSlotsTuple(elemShapes)
: this.irTypeOf(nameNode);
const slots = elemShapes.map((shape, i) => {
const slot = this.declareHiddenLocal(`%${name}.${i}`, shape.type);
params.push({ localId: slot.id, name: `%${name}.${i}`, type: shape.type });
return slot;
});
const body = this.declareLocal(nameNode, name, tupleType, true);
if (pure) {
this.ctx.pureTuples.set(body.id, slots);
return;
}
const init = this.tupleOfSlots(slots, tupleType, nameNode, loc);
prologue.push({ kind: "varDecl", localId: body.id, init, loc });
}
/** `value as never` flowing into a typed slot: `never` is assignable to
* every type, so the assertion promises the slot's type. A dynamic value
* takes the checked extraction to it, as `value as T` would. Null for
* every other form, and for numeric, dynamic and union slots, which
* keep the existing lowering. */
private neverAssertionInto(node: ts.Expression, expected: IrType): IrExpr | null {
if (!(ts.isAsExpression(node) || ts.isTypeAssertion(node))) return null;
if (expected.kind === "f64" || expected.kind === "dyn" || expected.kind === "union")
return null;
if (!(this.checker.getTypeFromTypeNode(node.type).flags & ts.TypeFlags.Never)) return null;
const innerTs = this.typeOf(node.expression);
if (!(innerTs.flags & ts.TypeFlags.Any) && this.mapTypeOf(innerTs)?.kind !== "dyn") return null;
if (
!canDynCheckTo(
expected,
(id) => this.shapes.get(id),
(id) => this.unions.get(id),
)
)
return null;
const inner = this.lowerExpr(node.expression);
if (inner.type.kind !== "dyn") return null;
return { kind: "dynCheck", value: inner, type: expected, loc: locOf(node) };
}
/** The tuple record of a forwarded-only rest tuple's native slots. */
private forwardedSlotsTuple(elemShapes: readonly ParamShape[]): IrType {
const fields = elemShapes.map((shape, i) => ({ name: String(i), type: shape.type }));
fields.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
return { kind: "record", shapeId: this.shapes.intern(fields, true) };
}
/** A fresh tuple holding the given positional slots, typed `tupleType`
* (a tuple record, or the empty-array form of `[]`). */
tupleOfSlots(slots: readonly IrLocal[], tupleType: IrType, blame: ts.Node, loc: SrcLoc): IrExpr {
if (slots.length === 0) {
if (tupleType.kind !== "array") this.badType(blame, this.typeOf(blame));
return { kind: "arrayLit", elems: [], type: tupleType, loc };
}
const shape = tupleType.kind === "record" ? this.shapes.get(tupleType.shapeId) : undefined;
if (!shape?.tuple || shape.fields.length !== slots.length)
this.badType(blame, this.typeOf(blame));
return {
kind: "recordLit",
fields: slots.map((slot, i) => {
const field = shape.fields.find((f) => f.name === String(i))!;
return {
name: field.name,
value: this.coerceToExpected(
{ kind: "varRef", localId: slot.id, type: slot.type, loc },
field.type,
),
};
}),
type: tupleType,
loc,
};
}
/** Declares a callee's parameter locals from its ParamShapes and builds
* the DEFAULT-PARAM PROLOGUE. Required/optional/rest params bind their
* symbol directly (one local of the ABI type). A defaulted param `x: T = e`
@@ -10958,6 +11179,17 @@ export class Lowerer {
return defaultAfterUndefined(undefinedValue, this.coerceInto(node, absent, expected));
};
decls.forEach((decl, i) => {
// A fixed-tuple rest parameter owns every remaining (flattened)
// shape, possibly none for `[]` (restTupleShapes).
if (
decl.dotDotDotToken !== undefined &&
ts.isIdentifier(decl.name) &&
i === decls.length - 1 &&
(shapes[i] === undefined ? shapes.length === i : shapes[i]!.restTuple === true)
) {
this.declareRestTupleParam(decl, decl.name, shapes.slice(i), params, prologue);
return;
}
const shape = shapes[i]!;
if (ts.isArrayBindingPattern(decl.name) || ts.isObjectBindingPattern(decl.name)) {
// Pattern parameter: one hidden ABI slot carries the source value;
@@ -11289,6 +11521,13 @@ export class Lowerer {
}
if (!symbol) return null;
const direct = this.resolveKey(symbol, ident);
if (direct && this.ctx.pureTuples.has(direct.id)) {
// onlyForwarded proved every read forwards; a plain read here means
// the analysis and the lowering disagree.
throw new InternalCompilerError(
`lowerer bug: forwarded-only tuple '${ident.text}' read as a value`,
);
}
if (direct) return direct;
// A PARAMETER PROPERTY declares two symbols: references resolve to the
// PARAMETER symbol, while the declaration's name binds the PROPERTY
@@ -11318,8 +11557,8 @@ export class Lowerer {
/** Lexical `this` — the enclosing method's this-param, possibly captured
* through arrows (function expressions/declarations reset `this` in JS;
* their bodies never see an enclosing method's binding). */
resolveThis(): IrLocal | null {
return this.resolveKey(undefined);
resolveThis(blame?: ts.Node): IrLocal | null {
return this.resolveKey(undefined, blame);
}
peekLocal(ident: ts.Identifier): IrLocal | null {
@@ -11961,6 +12200,8 @@ export class Lowerer {
// dispatch. The general identifier-call paths have no lowering for a
// promise-returning ambient global, and `import` is a keyword callee no
// identifier path matches.
const webAssembly = lowerWebAssemblyCall(this, expr);
if (webAssembly !== null) return webAssembly;
refuseWebAssemblyUse(this, expr.expression);
return (
lowerLifetimeRegionCall(this, expr) ??
@@ -12189,6 +12430,8 @@ export class Lowerer {
}
lowerNew(expr: ts.NewExpression): IrExpr {
const webAssembly = lowerWebAssemblyNew(this, expr);
if (webAssembly !== null) return webAssembly;
refuseWebAssemblyUse(this, expr.expression, "new ");
const builtin =
lowerLifetimeRegionNew(this, expr) ??
@@ -12601,6 +12844,12 @@ export class Lowerer {
}
// A bare `fs.constants` read (not one of the baked bits above).
if (bi.member === "constants" && bi.module === "fs") return fsConstantsObject(loc);
// `stream.Readable` as a JavaScript value: the runtime's untyped
// constructor, the base a constructor function inherits.
if (isJsSourceFile(expr.getSourceFile())) {
const classKind = builtinMemberClassKind(bi.module, bi.member);
if (classKind !== null) return builtinClassValue(classKind, loc);
}
if (builtinModuleFnOf(this, bi.module, bi.member)) {
this.unsupported(
"SC1090",
@@ -13150,6 +13399,31 @@ export class Lowerer {
}
}
/** Whether some read of a binding (identified by `isRead`) in `fn` takes
* its `.length`. */
function readsLength(fn: ts.Node, isRead: (id: ts.Identifier) => boolean): boolean {
let found = false;
ts.walkPreorder(fn, (n) => {
if (found) return "stop";
if (!ts.isIdentifier(n) || !isRead(n)) return undefined;
let top: ts.Node = n;
while (
top.parent &&
(ts.isParenthesizedExpression(top.parent) ||
ts.isAsExpression(top.parent) ||
ts.isTypeAssertion(top.parent) ||
ts.isSatisfiesExpression(top.parent) ||
ts.isNonNullExpression(top.parent))
)
top = top.parent;
const p = top.parent;
if (p && ts.isPropertyAccessExpression(p) && p.expression === top && p.name.text === "length")
found = true;
return undefined;
});
return found;
}
/** A union of record layouts flowing into one record slot: a function
* returning differently-shaped object literals (`{ ok: false }` and
* `{ ok: true, size }`, whose union the checker normalizes to arms with
@@ -0,0 +1,219 @@
import * as ts from "../ts7/adapter.js";
import { isJsSourceFile } from "../program.js";
import type { Lowerer } from "./lowerer.js";
import type { GenericFnInfo } from "./generic-functions.js";
import { stripTypeWrappers } from "./typed-arguments.js";
/** `fn.name` inside generic higher-order functions.
*
* Native closures carry no name, but a generic wrapper is instantiated per
* call site anyway: when its body reads `.name` of a function parameter,
* the instantiation key also carries the static name of that call's
* argument (the function declaration's name, a const binding's inferred
* name, the name of the function a wrapper call returns), and the read
* folds to that string. A call whose argument's name is only known at run
* time keeps a precise refusal at the read. */
/** Parameter index → symbol for the parameters whose `.name` the generic
* body reads (and never reassigns). Computed once per generic. */
export function nameReadParams(lowerer: Lowerer, info: GenericFnInfo): Map<number, ts.Symbol> {
if (info.nameReadParams) return info.nameReadParams;
const out = new Map<number, ts.Symbol>();
// JavaScript generics read function names through the checked-value
// box, which carries the runtime name already.
if (isJsSourceFile(info.decl.getSourceFile())) {
info.nameReadParams = out;
return out;
}
const bySymbol = new Map<ts.Symbol, number>();
info.decl.parameters.nodes.forEach((param, i) => {
if (!ts.isIdentifier(param.name) || param.dotDotDotToken) return;
if (!functionTypedParameter(lowerer, param)) return;
const symbol = lowerer.checker.getSymbolAtLocation(param.name);
if (symbol) bySymbol.set(symbol, i);
});
if (bySymbol.size > 0) {
const written = new Set<ts.Symbol>();
const read = new Set<ts.Symbol>();
ts.walkPreorder(info.decl, (node) => {
if (ts.isTypeNode(node)) return "skip";
if (
ts.isPropertyAccessExpression(node) &&
node.name.text === "name" &&
ts.isIdentifier(node.expression)
) {
const symbol = lowerer.checker.getSymbolAtLocation(node.expression);
if (symbol && bySymbol.has(symbol)) read.add(symbol);
}
if (
ts.isBinaryExpression(node) &&
node.operatorToken.kind >= ts.SyntaxKind.FirstAssignment &&
node.operatorToken.kind <= ts.SyntaxKind.LastAssignment &&
ts.isIdentifier(node.left)
) {
const symbol = lowerer.checker.getSymbolAtLocation(node.left);
if (symbol) written.add(symbol);
}
return undefined;
});
for (const symbol of read) {
if (!written.has(symbol)) out.set(bySymbol.get(symbol)!, symbol);
}
}
info.nameReadParams = out;
return out;
}
/** A parameter declared with a function type, or with a type parameter
* constrained to one (`fn: F` where `F extends (...args: any) => any`). */
function functionTypedParameter(lowerer: Lowerer, param: ts.ParameterDeclaration): boolean {
const t = lowerer.typeOf(param.name);
if (lowerer.checker.getCallSignatures(t).length > 0) return true;
if (!(t.flags & ts.TypeFlags.TypeParameter)) return false;
const symbol = t.getSymbol();
const decl = symbol ? symbol.declarations.find(ts.isTypeParameterDeclaration) : undefined;
if (!decl?.constraint) return false;
const constraint = lowerer.checker.getTypeFromTypeNode(decl.constraint);
return lowerer.checker.getCallSignatures(constraint).length > 0;
}
/** The instantiation key suffix and name bindings of one call of `info`,
* or null when the body reads no parameter's name. */
export function staticNameBindings(
lowerer: Lowerer,
call: ts.CallExpression,
info: GenericFnInfo,
): { key: string; names: Map<ts.Symbol, string | null> } | null {
const params = nameReadParams(lowerer, info);
if (params.size === 0) return null;
const names = new Map<ts.Symbol, string | null>();
const parts: string[] = [];
for (const [index, symbol] of params) {
const arg = call.arguments.nodes[index];
const name = arg && !ts.isSpreadElement(arg) ? staticFunctionName(lowerer, arg) : null;
names.set(symbol, name);
parts.push(name === null ? "?" : JSON.stringify(name));
}
return { key: `@name(${parts.join(",")})`, names };
}
/** The `name` JavaScript gives the function `expr` evaluates to, when it is
* statically known; null otherwise. */
export function staticFunctionName(lowerer: Lowerer, expr: ts.Expression): string | null {
return staticNameOf(lowerer, expr, new Set());
}
function staticNameOf(lowerer: Lowerer, expr: ts.Expression, seen: Set<ts.Node>): string | null {
const e = stripTypeWrappers(expr);
if (seen.has(e)) return null;
seen.add(e);
// An anonymous function as a call argument gets no inferred name.
if (ts.isFunctionExpression(e) || ts.isArrowFunction(e)) {
return !ts.isArrowFunction(e) && e.name ? e.name.text : "";
}
if (ts.isIdentifier(e)) {
const symbol = lowerer.resolveValueSymbol(e);
if (!symbol) return null;
const bound = lowerer.staticFnNames?.get(symbol);
if (bound !== undefined) return bound;
const decl = symbol.valueDeclaration;
if (decl === undefined) return null;
if (ts.isFunctionDeclaration(decl)) return decl.name ? decl.name.text : "default";
if (
ts.isVariableDeclaration(decl) &&
ts.isIdentifier(decl.name) &&
decl.initializer !== undefined &&
(ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) !== 0
) {
const init = stripTypeWrappers(decl.initializer);
// `const f = () => …` / `const f = function () {}`: the binding names
// an anonymous function definition.
if (ts.isArrowFunction(init)) return decl.name.text;
if (ts.isFunctionExpression(init)) return init.name ? init.name.text : decl.name.text;
return staticNameOf(lowerer, init, seen);
}
return null;
}
if (ts.isCallExpression(e) && ts.isIdentifier(e.expression)) {
// A call of a generic higher-order function returns the function its
// body returns: every `return` names the same anonymous definition or
// the same local const binding.
const symbol = lowerer.resolveValueSymbol(e.expression);
const info = symbol ? lowerer.genericFnsBySymbol.get(symbol) : undefined;
if (!info) return null;
return returnedFunctionName(lowerer, info.decl, seen);
}
if (ts.isPropertyAccessExpression(e)) {
const symbol = lowerer.checker.getSymbolAtLocation(e.name);
const decl = symbol ? symbol.valueDeclaration : undefined;
if (decl && ts.isMethodDeclaration(decl) && ts.isIdentifier(decl.name)) return decl.name.text;
return null;
}
return null;
}
function returnedFunctionName(
lowerer: Lowerer,
decl: GenericFnInfo["decl"],
seen: Set<ts.Node>,
): string | null {
const body = decl.body;
if (body === undefined) return null;
if (!ts.isBlock(body)) return functionValueName(lowerer, body, seen);
let name: string | null | undefined;
let ok = true;
ts.walkPreorder(body, (node) => {
if (!ok) return "stop";
if (ts.isFunctionLike(node)) return "skip";
if (!ts.isReturnStatement(node)) return undefined;
const here = node.expression ? functionValueName(lowerer, node.expression, seen) : null;
if (here === null || (name !== undefined && name !== here)) ok = false;
else name = here;
return undefined;
});
return ok && name !== undefined ? name : null;
}
/** The name of a returned function value: an anonymous definition returned
* directly is nameless; a local const binding names its definition. */
function functionValueName(
lowerer: Lowerer,
expr: ts.Expression,
seen: Set<ts.Node>,
): string | null {
const e = stripTypeWrappers(expr);
if (ts.isFunctionExpression(e) || ts.isArrowFunction(e)) {
return !ts.isArrowFunction(e) && e.name ? e.name.text : "";
}
if (ts.isIdentifier(e)) return staticNameOf(lowerer, e, seen);
return null;
}
/** `fn.name` where `fn` is a name-keyed parameter of the generic instance
* being lowered: the folded name, or a precise refusal when this call
* site's argument has no static name. Null for every other read. */
export function staticFunctionNameRead(
lowerer: Lowerer,
expr: ts.PropertyAccessExpression,
): string | null {
const names = lowerer.staticFnNames;
if (
names === null ||
expr.name.text !== "name" ||
expr.questionDotToken !== undefined ||
!ts.isIdentifier(expr.expression)
) {
return null;
}
const symbol = lowerer.checker.getSymbolAtLocation(expr.expression);
if (!symbol || !names.has(symbol)) return null;
const name = names.get(symbol);
if (name === null || name === undefined) {
lowerer.unsupported(
"SC1090",
expr,
`reading '${expr.expression.text}.name' when the call passes a function whose name is only known at run time (generic wrappers fold the name of a function declaration, a const-bound function, or a wrapper call's result)`,
);
}
return name;
}
@@ -2458,6 +2458,27 @@ export const AMBIENT_SURFACE_FNS: readonly AmbientSurfaceRow[] = [
name: "perf_hooks.performance.clearMeasures",
fns: ["perf.clearMeasures"],
},
{
id: "node-builtin.perf_hooks.performance.eventLoopUtilization",
kind: "node-builtin",
name: "perf_hooks.performance.eventLoopUtilization",
fns: ["perf.loopStart", "perf.idleTime"],
note: "active time is the main thread's CPU time since the loop started; the one- and two-argument difference forms are supported",
},
{
id: "node-builtin.perf_hooks.monitorEventLoopDelay",
kind: "node-builtin",
name: "perf_hooks.monitorEventLoopDelay",
fns: [
"perf.eldNew",
"perf.eldEnable",
"perf.eldDisable",
"perf.eldReset",
"perf.eldPercentile",
"perf.eldStat",
],
note: "an unref'd sampling interval records the delay histogram; enable, disable, reset, percentile and the min, max, mean, stddev, count and exceeds readings are supported",
},
// ── the process global's ambient reads and authority calls
// (lowerProcessProperty/lowerProcessMethodCall — process is a
// provenance-checked global here, not an importable module).
@@ -0,0 +1,516 @@
import * as ts from "../ts7/adapter.js";
import { bodyReadsArguments } from "../arguments-usage.js";
import { isJsSourceFile, locOf } from "../program.js";
import { restTupleElementTypes, unitOnlyUnion } from "../type-mapper.js";
import { DYN, type IrExpr, type IrLocal, type IrStmt, type IrType, arrayOf } from "../../ir/ir.js";
import type { Lowerer } from "./lowerer.js";
import { captureContextArguments } from "./function-context.js";
import { isThisParameter, type ParamShape } from "./call-signatures.js";
import { tryLowerExpression } from "./expressions/try-lower-expression.js";
import { omittedArgFor } from "./call-arguments.js";
/** Variadic forwarding through a FIXED signature.
*
* A TypeScript function expression that declares no parameters and reads
* `arguments` (`function (this: unknown) { return fn.apply(this, arguments) }
* as F`) is lowered against the fixed function type its value is asserted
* to: one native slot per position of that type, and `arguments` is the
* tuple of those slots. Inside a generic instantiation F is the call
* site's concrete signature, so a higher-order wrapper specializes to the
* wrapped function's exact ABI. Forwarding (`f.apply(thisArg, args)`,
* `f(...args)`) then calls with the tuple's positions directly. */
/** `e`, with the type-only wrappers that never change its value removed. */
export function stripTypeWrappers(e: ts.Expression): ts.Expression {
let cur = e;
for (;;) {
if (ts.isParenthesizedExpression(cur)) cur = cur.expression;
else if (ts.isAsExpression(cur) || ts.isTypeAssertion(cur) || ts.isSatisfiesExpression(cur))
cur = cur.expression;
else if (ts.isNonNullExpression(cur)) cur = cur.expression;
else return cur;
}
}
/** The outermost type assertion around `node` (`node as unknown as F` → F),
* looking through parentheses; null when the value is not asserted. */
function assertedTypeNode(node: ts.Node): ts.TypeNode | null {
let cur: ts.Node = node;
let found: ts.TypeNode | null = null;
for (let parent = cur.parent; parent !== undefined; parent = cur.parent) {
if (ts.isParenthesizedExpression(parent)) {
cur = parent;
continue;
}
if ((ts.isAsExpression(parent) || ts.isTypeAssertion(parent)) && parent.expression === cur) {
found = parent.type;
cur = parent;
continue;
}
break;
}
return found;
}
/** The fixed function type a value is asserted to, or null. */
function assertedFuncType(lowerer: Lowerer, node: ts.Node): (IrType & { kind: "func" }) | null {
const typeNode = assertedTypeNode(node);
if (!typeNode) return null;
const mapped = lowerer.mapTypeOf(lowerer.checker.getTypeFromTypeNode(typeNode));
return mapped?.kind === "func" ? mapped : null;
}
function sameFuncType(a: IrType & { kind: "func" }, b: IrType & { kind: "func" }): boolean {
return JSON.stringify(a) === JSON.stringify(b);
}
/** A function expression whose `arguments` can be typed: TypeScript source,
* no runtime parameters (a `this` parameter is type-only), an ordinary
* (non-generator) function that reads its own `arguments`. */
export function typedArgumentsCandidate(node: ts.Node): node is ts.FunctionExpression {
return (
ts.isFunctionExpression(node) &&
node.asteriskToken === undefined &&
!isJsSourceFile(node.getSourceFile()) &&
node.parameters.nodes.every(isThisParameter) &&
bodyReadsArguments(node)
);
}
/** The fixed signature a typed-arguments function expression is lowered
* against: the type its value is asserted to (`function () {…} as F`), the
* one type every use of its const binding asserts it to (`const w =
* function () {…}; return w as unknown as F`), or its contextual function
* type. Null when none names one fixed-arity signature. */
export function typedArgumentsTarget(
lowerer: Lowerer,
node: ts.FunctionExpression,
): (IrType & { kind: "func" }) | null {
const fixed = (t: (IrType & { kind: "func" }) | null) => (t && t.rest !== true ? t : null);
const direct = assertedFuncType(lowerer, node);
if (direct) return fixed(direct);
let top: ts.Node = node;
while (top.parent && ts.isParenthesizedExpression(top.parent)) top = top.parent;
const decl = top.parent;
if (
decl &&
ts.isVariableDeclaration(decl) &&
decl.initializer === top &&
decl.type === undefined &&
ts.isIdentifier(decl.name) &&
(ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) !== 0
) {
const symbol = lowerer.checker.getSymbolAtLocation(decl.name);
const scope = enclosingScope(decl);
if (symbol && scope) {
let target: (IrType & { kind: "func" }) | null = null;
let ok = true;
let uses = 0;
ts.walkPreorder(scope, (n) => {
if (!ok) return "stop";
if (n === decl.name || !ts.isIdentifier(n) || n.text !== decl.name.text) return undefined;
if (lowerer.checker.getSymbolAtLocation(n) !== symbol) return undefined;
uses++;
const asserted = assertedFuncType(lowerer, n);
if (!asserted || (target && !sameFuncType(target, asserted))) ok = false;
else target = asserted;
return undefined;
});
if (ok && uses > 0) return fixed(target);
}
}
const ctxType = lowerer.checker.getContextualType(node);
if (!ctxType) return null;
const sigs = lowerer.checker.getCallSignatures(lowerer.checker.getNonNullableType(ctxType));
if (sigs.length !== 1) return null;
const mapped = lowerer.mapTypeOf(lowerer.checker.getNonNullableType(ctxType));
return mapped?.kind === "func" ? fixed(mapped) : null;
}
function enclosingScope(node: ts.Node): ts.Node | null {
for (let cur = node.parent; cur !== undefined; cur = cur.parent) {
if (ts.isSourceFile(cur) || (ts.isFunctionLike(cur) && cur.body !== undefined)) return cur;
}
return null;
}
/** The tuple type of a fixed parameter list, interned exactly as the
* spelled tuple (and `Parameters<F>`) map: positional fields, or the
* empty-array form for no parameters. */
export function tupleTypeOfParams(lowerer: Lowerer, params: readonly IrType[]): IrType {
if (params.length === 0) return arrayOf(unitOnlyUnion(lowerer.unions));
const fields = params.map((type, i) => ({ name: String(i), type }));
fields.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
return { kind: "record", shapeId: lowerer.shapes.intern(fields, true) };
}
/** The ABI shapes of a typed-arguments function: one required slot per
* position of its target signature. */
export function typedArgumentsShapes(target: IrType & { kind: "func" }): ParamShape[] {
return target.params.map((type) => ({ type, mode: "required" }));
}
/** Declares the hidden positional slots of a typed-arguments function and
* binds its `arguments` to their tuple. */
export function declareTypedArguments(
lowerer: Lowerer,
node: ts.FunctionExpression,
shapes: readonly ParamShape[],
params: { localId: string; name: string; type: IrType }[],
prologue: IrStmt[],
): void {
const loc = locOf(node);
const slots = shapes.map((shape, i) => {
const slot = lowerer.declareHiddenLocal(`%arguments.${i}`, shape.type);
params.push({ localId: slot.id, name: `%arguments.${i}`, type: shape.type });
return slot;
});
const tupleType = tupleTypeOfParams(
lowerer,
shapes.map((s) => s.type),
);
const local = lowerer.declareHiddenLocal("%arguments", tupleType);
lowerer.ctx.argumentsLocal = local;
// Pure forwarding (every read is forwarded, counted, or aliased by a
// binding that is itself only forwarded): no tuple exists at all.
if (
onlyForwarded(lowerer, node, (id) => id.text === "arguments" && argumentsOwner(id) === node)
) {
lowerer.ctx.pureTuples.set(local.id, slots);
return;
}
prologue.push({
kind: "varDecl",
localId: local.id,
init: lowerer.tupleOfSlots(slots, tupleType, node, loc),
loc,
});
}
/** Whether every read of a tuple binding in `fn` (identified by `isRoot`)
* only forwards it: `f.apply(t, x)`, `g(...x)` into a program function,
* `x.length`, or a const alias whose own reads only forward. Reads inside
* nested functions (arrows included) capture the binding and need the
* tuple. Type assertions around a read never matter. */
export function onlyForwarded(
lowerer: Lowerer,
fn: ts.Node,
isRoot: (id: ts.Identifier) => boolean,
): boolean {
const body = (fn as ts.Node & { body?: ts.Node }).body;
if (body === undefined) return false;
const roots: ((id: ts.Identifier) => boolean)[] = [isRoot];
let ok = true;
const visit = (scope: ts.Node, test: (id: ts.Identifier) => boolean): void => {
ts.walkPreorder(scope, (n) => {
if (!ok) return "stop";
if (ts.isTypeNode(n)) return "skip";
if (!ts.isIdentifier(n) || !test(n)) return undefined;
for (let cur = n.parent; cur !== undefined && cur !== fn; cur = cur.parent) {
if (ts.isFunctionLike(cur)) {
ok = false;
return "stop";
}
}
let top: ts.Node = n;
while (
top.parent &&
(ts.isParenthesizedExpression(top.parent) ||
ts.isAsExpression(top.parent) ||
ts.isTypeAssertion(top.parent) ||
ts.isSatisfiesExpression(top.parent) ||
ts.isNonNullExpression(top.parent))
)
top = top.parent;
const p = top.parent;
if (!p) {
ok = false;
return "stop";
}
if (ts.isPropertyAccessExpression(p) && p.expression === top && p.name.text === "length")
return undefined;
if (
ts.isCallExpression(p) &&
p.arguments.nodes.length === 2 &&
p.arguments.nodes[1] === top &&
ts.isPropertyAccessExpression(p.expression) &&
p.expression.name.text === "apply" &&
p.expression.questionDotToken === undefined
)
return undefined;
if (ts.isSpreadElement(p) && p.parent && ts.isCallExpression(p.parent)) {
const sigs = lowerer.checker.getCallSignatures(
lowerer.checker.getNonNullableType(lowerer.typeOf(p.parent.expression)),
);
const decl = sigs.length === 1 ? sigs[0]!.declaration : undefined;
if (decl && !decl.getSourceFile().isDeclarationFile) return undefined;
}
if (
ts.isVariableDeclaration(p) &&
p.initializer === top &&
ts.isIdentifier(p.name) &&
(ts.getCombinedNodeFlags(p) & ts.NodeFlags.Const) !== 0
) {
const alias = lowerer.checker.getSymbolAtLocation(p.name);
const aliasName = p.name;
if (alias) {
roots.push((id) => id !== aliasName && lowerer.checker.getSymbolAtLocation(id) === alias);
return undefined;
}
}
ok = false;
return "stop";
});
};
for (let i = 0; i < roots.length && ok; i++) visit(body, roots[i]!);
return ok;
}
/** The native slots of a pure-forwarded tuple binding read at `node` in
* the current function, or null when the tuple is materialized. */
function pureSlotsOf(lowerer: Lowerer, node: ts.Expression): IrLocal[] | null {
const operand = stripTypeWrappers(node);
if (!ts.isIdentifier(operand)) return null;
const local =
operand.text === "arguments" ? lowerer.ctx.argumentsLocal : lowerer.peekLocal(operand);
return local ? (lowerer.ctx.pureTuples.get(local.id) ?? null) : null;
}
/** The positional values `node` forwards when it is a pure-forwarded tuple
* (slot reads, in order), or null. */
export function pureForwardedValues(lowerer: Lowerer, node: ts.Expression): IrExpr[] | null {
const slots = pureSlotsOf(lowerer, node);
if (!slots) return null;
const loc = locOf(node);
return slots.map((slot) => ({ kind: "varRef", localId: slot.id, type: slot.type, loc }));
}
/** Whether `node` reads a tuple-typed `arguments` binding: the nearest
* ordinary function (arrows inherit it) is a typed-arguments function.
* Looks the binding up without allocating captures. */
function isTypedArgumentsRead(lowerer: Lowerer, node: ts.Expression): boolean {
if (!ts.isIdentifier(node) || node.text !== "arguments") return false;
const stack = lowerer.fnStack;
let owner = stack.length - 1;
while (owner > 0 && stack[owner]!.inheritsArguments) owner--;
const own = owner >= 0 ? stack[owner]!.argumentsLocal : undefined;
return own !== undefined && own !== null && own.type.kind !== "dyn";
}
/** The fixed tuple `node` forwards, or null: the typed `arguments`, a
* fixed-tuple rest parameter, or a const alias of either — seen through
* type assertions, which never change the value. */
export function forwardedTupleType(lowerer: Lowerer, node: ts.Expression): IrType | null {
const operand = stripTypeWrappers(node);
if (!ts.isIdentifier(operand)) return null;
if (operand.text === "arguments") {
if (!isTypedArgumentsRead(lowerer, operand)) return null;
return captureContextArguments(lowerer.fnStack)!.type;
}
const symbol = lowerer.resolveValueSymbol(operand);
if (!symbol) return null;
const alias = lowerer.tupleAliasSymbols.get(symbol);
if (alias) return alias;
const decl = symbol.valueDeclaration;
if (
decl !== undefined &&
ts.isParameter(decl) &&
decl.dotDotDotToken !== undefined &&
ts.isIdentifier(decl.name) &&
!isJsSourceFile(decl.getSourceFile()) &&
// Checker-only probe first: an ordinary array rest never maps here.
restTupleElementTypes(lowerer.typeOf(operand), lowerer.typeCtx) !== null
) {
// A tuple with optional positions has no value type of its own; its
// forwarded-only binding carries the slots' tuple (declareRestTupleParam).
const t = lowerer.mapTypeOf(lowerer.typeOf(operand)) ?? lowerer.peekLocal(operand)?.type;
if (t && isTupleValueType(lowerer, t)) return t;
}
return null;
}
function isTupleValueType(lowerer: Lowerer, t: IrType): boolean {
if (t.kind === "record") return lowerer.shapes.get(t.shapeId)?.tuple === true;
// The empty tuple's representation (mapType's `[]` rule): an array over
// the unit-only union. Looked up, never interned, so probing a value
// leaves the registries untouched.
if (t.kind !== "array" || t.elem.kind !== "union") return false;
const arms = lowerer.unions.get(t.elem.unionId)?.arms;
return (
arms !== undefined &&
arms.length === 2 &&
arms.some((a) => a.kind === "nullT") &&
arms.some((a) => a.kind === "undefinedT")
);
}
/** The positional reads of a forwarded tuple value (already saved in a
* local), in order. */
function tupleReads(
lowerer: Lowerer,
ref: () => IrExpr,
tupleType: IrType,
loc: IrExpr["loc"],
): IrExpr[] {
if (tupleType.kind !== "record") return [];
const shape = lowerer.shapes.get(tupleType.shapeId)!;
return [...shape.fields]
.sort((a, b) => Number(a.name) - Number(b.name))
.map((field) => ({
kind: "recordGet" as const,
obj: ref(),
shapeId: tupleType.shapeId,
field: field.name,
type: field.type,
loc,
}));
}
/** `f.apply(thisArg, args)` where `args` forwards a fixed tuple and `f` is a
* fixed-arity function value: a direct call of `f` with the tuple's
* positions and `thisArg` as the call-time receiver. Null for every other
* form (the existing Function.prototype paths keep them). */
export function lowerForwardingApply(
lowerer: Lowerer,
call: ts.CallExpression,
access: ts.PropertyAccessExpression,
): IrExpr | null {
if (access.name.text !== "apply" || access.questionDotToken !== undefined) return null;
const argNodes = call.arguments.nodes;
if (argNodes.length !== 2 || argNodes.some(ts.isSpreadElement)) return null;
const tupleType = forwardedTupleType(lowerer, argNodes[1]!);
if (!tupleType) return null;
const callee = tryLowerExpression(lowerer, access.expression);
if (callee?.type.kind !== "func") return null;
const fnType = callee.type;
const loc = locOf(call);
if (fnType.rest === true) {
lowerer.unsupported(
"SC1090",
call,
"forwarding a fixed argument tuple through 'apply' to a variadic function value",
);
}
const savedFn = lowerer.declareHiddenLocal("%applyFn", fnType);
const savedThis = lowerer.declareHiddenLocal("%applyThis", DYN);
const stmts: IrStmt[] = [
{ kind: "varDecl", localId: savedFn.id, init: callee, loc },
{
kind: "varDecl",
localId: savedThis.id,
init: lowerer.lowerExprExpecting(argNodes[0]!, DYN),
loc,
},
];
let reads = pureForwardedValues(lowerer, argNodes[1]!);
if (!reads) {
const savedArgs = lowerer.declareHiddenLocal("%applyArgs", tupleType);
stmts.push({
kind: "varDecl",
localId: savedArgs.id,
init: lowerer.lowerExpr(stripTypeWrappers(argNodes[1]!)),
loc,
});
reads = tupleReads(
lowerer,
() => ({ kind: "varRef", localId: savedArgs.id, type: tupleType, loc }),
tupleType,
loc,
);
}
// JS drops surplus positions and fills missing ones with undefined.
const args = fnType.params.map((param, i): IrExpr => {
const read = reads[i];
if (read) return lowerer.coerceInto(call, read, param);
const absent = omittedArgFor(lowerer, param, loc);
if (!absent) {
lowerer.unsupported(
"SC1090",
call,
"forwarding fewer arguments through 'apply' than the callee's required parameters",
);
}
return absent;
});
const invoked: IrExpr = {
kind: "callValue",
callee: { kind: "varRef", localId: savedFn.id, type: fnType, loc },
receiver: { kind: "varRef", localId: savedThis.id, type: DYN, loc },
args,
type: fnType.ret,
loc,
};
return { kind: "seqExpr", stmts, result: invoked, type: fnType.ret, loc };
}
/** `const args = arguments as …` (or an alias of a rest tuple) inside a
* typed-arguments function: the binding holds the tuple itself — the
* assertion only re-describes the value — and stays a forwarding source. */
export function lowerTupleAliasDecl(lowerer: Lowerer, decl: ts.VariableDeclaration): IrStmt | null {
if (
!decl.initializer ||
!ts.isIdentifier(decl.name) ||
(ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) === 0 ||
lowerer.fnStack.length === 0
) {
return null;
}
const tupleType = forwardedTupleType(lowerer, decl.initializer);
if (!tupleType) return null;
const symbol = lowerer.checker.getSymbolAtLocation(decl.name);
if (!symbol) return null;
const declared = lowerer.mapTypeOf(lowerer.typeOf(decl.name));
if (declared && declared.kind !== "dyn" && JSON.stringify(declared) !== JSON.stringify(tupleType))
return null;
lowerer.tupleAliasSymbols.set(symbol, tupleType);
const slots = pureSlotsOf(lowerer, decl.initializer);
if (slots) {
// An alias of a pure-forwarded tuple is itself only forwarded
// (onlyForwarded followed it): it names the same slots.
const local = lowerer.declareLocal(decl.name, decl.name.text, tupleType, false);
lowerer.ctx.pureTuples.set(local.id, slots);
return { kind: "block", body: [], loc: locOf(decl) };
}
const init = lowerer.lowerExpr(stripTypeWrappers(decl.initializer));
const local = lowerer.declareLocal(decl.name, decl.name.text, tupleType, false);
return { kind: "varDecl", localId: local.id, init, loc: locOf(decl) };
}
/** A spread operand forwarding a fixed tuple (forwardedTupleType),
* including one that resolved to no positions at all. */
export function isForwardedTupleSpread(lowerer: Lowerer, node: ts.Expression): boolean {
return forwardedTupleType(lowerer, node) !== null;
}
/** `arguments` read inside a typed-arguments function: its fixed arity. */
export function typedArgumentsArity(lowerer: Lowerer, node: ts.Expression): number | null {
if (!ts.isIdentifier(node)) return null;
const t = forwardedTupleType(lowerer, node);
if (!t) return null;
if (t.kind !== "record") return 0;
const fields = lowerer.shapes.get(t.shapeId)?.fields ?? [];
// A position that may be undefined may also be absent at the call: the
// native signature always passes it, so the count is not static.
if (
fields.some(
(f) => f.type.kind === "union" && lowerer.armTag(f.type.unionId, { kind: "undefinedT" }) >= 0,
)
) {
lowerer.unsupported(
"SC1090",
node.parent ?? node,
"'length' of an argument tuple whose positions may be omitted (the native signature always passes every position, so the count is not static)",
);
}
return fields.length;
}
/** The function whose `arguments` an identifier reads: the nearest
* enclosing non-arrow function. */
export function argumentsOwner(node: ts.Node): ts.Node | null {
for (let cur = node.parent; cur !== undefined; cur = cur.parent) {
if (ts.isFunctionLike(cur) && !ts.isArrowFunction(cur)) return cur;
if (ts.isSourceFile(cur)) return null;
}
return null;
}
+645 -29
View File
@@ -980,7 +980,7 @@ function isDeclaredInAmbientModule(d: ts.Declaration, name: string): boolean {
/** True when the declaration's nearest enclosing namespace is `name`
* (`declare global { namespace NodeJS { ... } }` — the NodeJS-global
* interfaces @types/node declares outside any ambient module). */
function isDeclaredInAmbientNamespace(d: ts.Declaration, name: string): boolean {
export function isDeclaredInAmbientNamespace(d: ts.Declaration, name: string): boolean {
let node: ts.Node | undefined = d.parent;
while (node) {
if (ts.isModuleDeclaration(node) && ts.isIdentifier(node.name)) {
@@ -991,6 +991,31 @@ function isDeclaredInAmbientNamespace(d: ts.Declaration, name: string): boolean
return false;
}
/** Whether `type` is one of the stream spellings the general stream
* representation (TypeMapperCtx.generalNodeStreams) maps differently:
* NodeJS.ReadableStream / NodeJS.WritableStream, @types/node's
* stream.Readable / stream.Writable, or the fallback declarations'
* ChildStdin. */
export function isGeneralNodeStreamInterface(
type: ts.Type,
ctx: Pick<TypeMapperCtx, "isStdlibFile">,
): boolean {
const psym = type.getSymbol() ?? type.getAliasSymbol();
if (psym === undefined) return false;
return psym.declarations.some((d) => {
if (!(ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d))) return false;
if (!ctx.isStdlibFile(d.getSourceFile())) return false;
if (psym.name === "ReadableStream" || psym.name === "WritableStream")
return ts.isInterfaceDeclaration(d) && isDeclaredInAmbientNamespace(d, "NodeJS");
if (psym.name === "ChildStdin") return isDeclaredInAmbientModule(d, "child_process");
return (
(psym.name === "Readable" || psym.name === "Writable") &&
isNodeTypesPath(d.getSourceFile().fileName) &&
isDeclaredInAmbientModule(d, "stream")
);
});
}
/** A type declared in the shipped WebAssembly namespace (scriptc.d.ts):
* its modules, instances, memories, tables, globals, error classes and the
* Exports/Imports records. */
@@ -1146,6 +1171,13 @@ export interface TypeMapperCtx {
* type descriptions for project-owned values, but their imported runtime
* bindings are fenced separately by the Lowerer. */
isExternalTypeFile: (sf: ts.SourceFile) => boolean;
/** The program's general stream representation (lowerToIr decides it
* per program): NodeJS.ReadableStream / NodeJS.WritableStream and
* @types/node's stream.Readable / stream.Writable map to the runtime
* stream classes instead of the child/process stdio handles, so values
* from every stream producer share their slots. Off (the default), the
* stdio handles keep those spellings. */
generalNodeStreams?: boolean;
/** Successful context-free mappings, keyed by the
* TypeScript server's stable type id. A generated facade can reference the
* same large record/function type thousands of times; remapping its full
@@ -1744,6 +1776,18 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
// option) holds that dynamic value too, so the whole union is dynamic.
if (type.isUnionType() && ts.constituentTypes(type).some((t) => isWebAssemblyType(t, ctx)))
return DYN;
// JavaScript inference merges WebAssembly.instantiate's overloads into
// `Promise<WebAssemblyInstantiatedSource> & Promise<Instance>`: a promise
// of a WebAssembly value either way.
if (
type.isIntersectionType() &&
ts.constituentTypes(type).every((t) => {
if (t.getSymbol()?.name !== "Promise") return false;
const arg = ctx.checker.getTypeArguments(t as ts.TypeReference)[0];
return arg !== undefined && isWebAssemblyType(arg, ctx);
})
)
return { kind: "promise", inner: DYN };
// Immutable<T> (@scriptc/types) is a deep-readonly view: the value is
// still the T it maps.
const immutableSource = immutableSourceType(type, ctx);
@@ -1805,6 +1849,16 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
contextResolutions++;
return viaIndexed;
}
// `Parameters<F>` / `ReturnType<F>` over a bound function-typed
// parameter (variadic higher-order wrappers): the checker keeps both
// conditionals symbolic inside the body, and their `infer` positions
// match a call signature, which matchInferReference cannot decide.
const viaSignature =
mapBoundSignatureAlias(type, ctx) ?? mapBoundKeyofNeverConditional(type, ctx);
if (viaSignature !== null) {
contextResolutions++;
return viaSignature;
}
// A conditional type over a bound parameter (`Infer<S["left"]>`, a
// mapped type's member): evaluate it against the bindings.
if (type.isConditionalType()) {
@@ -3176,6 +3230,20 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
// global-namespace interface real code writes in its own signatures —
// prefixStream's `output` param). The representation is the raw fd
// scalar; the lowered surface is write(string).
// Under the program's general stream representation (generalNodeStreams)
// NodeJS.WritableStream is the runtime Writable instead: the process
// streams convert into it where they flow (process.stdioWritable).
const nodeWritableInterface =
psym?.name === "WritableStream" &&
psym.declarations.some(
(d) =>
ts.isInterfaceDeclaration(d) &&
ctx.isStdlibFile(d.getSourceFile()) &&
isDeclaredInAmbientNamespace(d, "NodeJS"),
);
if (nodeWritableInterface && ctx.generalNodeStreams === true) {
return { kind: "object", className: "%Writable" };
}
if (
(psym?.name === "WriteStream" &&
psym.declarations.some(
@@ -3185,13 +3253,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
(isDeclaredInAmbientModule(d, "tty") ||
(isDeclaredInAmbientModule(d, "process") && isDeclaredInAmbientNamespace(d, "NodeJS"))),
)) ||
(psym?.name === "WritableStream" &&
psym.declarations.some(
(d) =>
ts.isInterfaceDeclaration(d) &&
ctx.isStdlibFile(d.getSourceFile()) &&
isDeclaredInAmbientNamespace(d, "NodeJS"),
))
nodeWritableInterface
) {
return PROCSTREAM_T;
}
@@ -3201,6 +3263,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
// of the broad @types class fence where they are constructed.
if (
(psym?.name === "ChildStdin" &&
ctx.generalNodeStreams !== true &&
psym.declarations.some(
(d) =>
ts.isInterfaceDeclaration(d) &&
@@ -3208,6 +3271,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
isDeclaredInAmbientModule(d, "child_process"),
)) ||
(psym?.name === "Writable" &&
ctx.generalNodeStreams !== true &&
psym.declarations.some(
(d) =>
(ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) &&
@@ -3218,6 +3282,20 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
) {
return { kind: "childWriter" };
}
// Under the general stream representation the fallback declarations'
// ChildStdin is the runtime Writable too (child.stdioStream bridges it).
if (
ctx.generalNodeStreams === true &&
psym?.name === "ChildStdin" &&
psym.declarations.some(
(d) =>
ts.isInterfaceDeclaration(d) &&
ctx.isStdlibFile(d.getSourceFile()) &&
isDeclaredInAmbientModule(d, "child_process"),
)
) {
return { kind: "object", className: "%Writable" };
}
// The static node:stream classes (the shipped fallback declarations —
// NOT @types/node, whose stream.Readable also types child stdio; under
// @types/node the childStream mapping below keeps priority and the
@@ -3225,6 +3303,9 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
// branch: the fallback's Readable must map to the runtime class, and
// under the fallback child.stdout is NodeJS.ReadableStream (its own
// branch below), so the two never collide.
// @types/node's Duplex/Transform/PassThrough classes have no child-stdio
// reading, so they map to the runtime classes too; its Readable and
// Writable join them under the general representation.
for (const [irName, rec] of RUNTIME_STREAM_CLASSES) {
if (
psym?.name === rec.lib &&
@@ -3232,38 +3313,39 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
(d) =>
ts.isClassDeclaration(d) &&
ctx.isStdlibFile(d.getSourceFile()) &&
!isNodeTypesPath(d.getSourceFile().fileName) &&
(!isNodeTypesPath(d.getSourceFile().fileName) ||
ctx.generalNodeStreams === true ||
(rec.lib !== "Readable" && rec.lib !== "Writable")) &&
isDeclaredInAmbientModule(d, "stream"),
)
) {
return { kind: "object", className: irName };
}
}
// node:zlib's stream classes (the shipped fallback declarations): their
// instances are runtime Transform streams.
// node:zlib's stream classes (the shipped fallback declarations and
// @types/node's): their instances are runtime Transform streams.
if (
psym !== undefined &&
ZLIB_STREAM_MODES.has(psym.name) &&
psym.declarations.some(
(d) =>
ts.isClassDeclaration(d) &&
(ts.isClassDeclaration(d) ||
(ts.isInterfaceDeclaration(d) && isNodeTypesPath(d.getSourceFile().fileName))) &&
ctx.isStdlibFile(d.getSourceFile()) &&
!isNodeTypesPath(d.getSourceFile().fileName) &&
isDeclaredInAmbientModule(d, "zlib"),
)
) {
return { kind: "object", className: "%Transform" };
}
// fs.ReadStream / fs.WriteStream (the shipped fallback declarations):
// their instances are runtime Readable / Writable streams
// (scr_fs_stream.c).
// fs.ReadStream / fs.WriteStream (the shipped fallback declarations and
// @types/node's): their instances are runtime Readable / Writable
// streams (scr_fs_stream.c).
if (
(psym?.name === "ReadStream" || psym?.name === "WriteStream") &&
psym.declarations.some(
(d) =>
ts.isClassDeclaration(d) &&
ctx.isStdlibFile(d.getSourceFile()) &&
!isNodeTypesPath(d.getSourceFile().fileName) &&
isDeclaredInAmbientModule(d, "fs"),
)
) {
@@ -3285,6 +3367,9 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
isDeclaredInAmbientNamespace(d, "NodeJS"),
))
) {
// The general representation: the runtime Readable, which child
// stdio reads produce through the child stream bridge.
if (ctx.generalNodeStreams === true) return { kind: "object", className: "%Readable" };
return { kind: "childStream" };
}
// dgram.Socket: the same ambiguous "Socket" name disambiguated by its
@@ -3995,6 +4080,27 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
return { kind: "record", shapeId: perfEntryShape(ctx.shapes, ctx.unions) };
}
// readline's getCursorPos() record, in Node's key order ({ cols, rows }).
// perf_hooks' EventLoopUtilization (performance.eventLoopUtilization's
// result): a plain data record in Node's key order.
if (isStdlibInterface("EventLoopUtilization")) {
return {
kind: "record",
shapeId: ctx.shapes.intern(
[
{ name: "active", type: F64 },
{ name: "idle", type: F64 },
{ name: "utilization", type: F64 },
],
false,
undefined,
["idle", "active", "utilization"],
),
};
}
// monitorEventLoopDelay's IntervalHistogram: a numeric handle into the
// runtime's histogram table, like a timer handle. Its members lower over
// the handle (builtins/performance.ts).
if (isStdlibInterface("IntervalHistogram")) return F64;
if (isStdlibInterface("CursorPos")) {
return {
kind: "record",
@@ -4331,10 +4437,18 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
const mapped = mapType(checker.getTypeOfSymbol(p), ctx);
if (!mapped) return false;
const arms = mapped.kind === "union" ? ctx.unions.get(mapped.unionId)?.arms : [mapped];
// Under the general stream representation the stdio members are the
// runtime stream classes.
const isExpected = (arm: IrType): boolean =>
arm.kind === expected ||
(ctx.generalNodeStreams === true &&
arm.kind === "object" &&
((expected === "childStream" && arm.className === "%Readable") ||
(expected === "childWriter" && arm.className === "%Writable")));
return (
arms !== undefined &&
arms.some((arm) => arm.kind === expected) &&
arms.every((arm) => arm.kind === expected || arm.kind === unit)
arms.some(isExpected) &&
arms.every((arm) => isExpected(arm) || arm.kind === unit)
);
};
const childShaped =
@@ -4547,11 +4661,44 @@ function mapCallSignature(sig: ts.Signature, ctx: TypeMapperCtx): IrType | null
// declaration as a typed packed-array closure would bypass its boxed
// spread/error semantics and change coverage classification.
if (rest && decl !== undefined && isJsSourceFile(decl.getSourceFile())) return null;
// A fixed-tuple rest (`...args: [A, B]`, `...args: Parameters<F>` over
// a bound F) flattens to its positional parameters. Pattern rests keep
// their packed-record ABI and stay unmapped as values.
if (rest && decl !== undefined && ts.isParameter(decl) && ts.isIdentifier(decl.name)) {
const elems = restTupleParamTypes(checker.getTypeOfSymbol(p), ctx);
if (elems) {
params.push(...elems);
continue;
}
}
const optional =
decl !== undefined &&
ts.isParameter(decl) &&
(decl.questionToken !== undefined || decl.initializer !== undefined);
let pt = mapType(checker.getTypeOfSymbol(p), ctx);
// A parameter a TypeScript program types `any` takes whatever the
// caller passes: the checked-dynamic slot, as an `any`-typed binding
// already is (dynFallbackType). Declaration files keep their paths.
// Only function declarations, spelled function types and lambda
// parameters whose default is the only source of their type (`(n =
// lookup() ?? 5) => ...` with an `any` lookup): other lambdas and
// methods in object literals keep their existing checked-dynamic story.
if (
!pt &&
!rest &&
decl !== undefined &&
ts.isParameter(decl) &&
decl.parent !== undefined &&
(ts.isFunctionDeclaration(decl.parent) ||
ts.isFunctionTypeNode(decl.parent) ||
((ts.isArrowFunction(decl.parent) || ts.isFunctionExpression(decl.parent)) &&
decl.initializer !== undefined &&
decl.type === undefined &&
!isJsSourceFile(decl.getSourceFile()))) &&
!decl.getSourceFile().isDeclarationFile &&
(checker.getTypeOfSymbol(p).flags & ts.TypeFlags.Any) !== 0
)
pt = DYN;
if (
decl &&
ts.isParameter(decl) &&
@@ -4699,6 +4846,9 @@ function unionDiscriminant(
): IrUnionDiscriminant | undefined {
if (arms.filter((arm) => arm.kind === "record").length < 2 || parts.length < 2) return undefined;
const { checker } = ctx;
// Tuple shapes keep positions only: their checker `length` literal has
// no slot to compare (discriminantField refuses tuple shapes too).
if (parts.some((part) => ctx.shapes.get(part.mapped.shapeId)?.tuple)) return undefined;
for (const candidate of checker.getPropertiesOfType(parts[0]!.source)) {
if (candidate.name.startsWith("__@")) continue;
const byTag = new Map<number, (string | number | boolean)[]>();
@@ -5036,6 +5186,167 @@ function mapBoundIndexedAccess(type: ts.Type, ctx: TypeMapperCtx): IrType | null
return mapType(checker.getTypeOfSymbol(prop), ctx);
}
/** The standard library's `Parameters<F>` / `ReturnType<F>` over a type
* parameter the current instantiation binds to a callable checker type:
* that type's one call signature. Concrete arguments never reach here (the
* checker evaluates those conditionals itself); generic bodies keep them
* symbolic because their `infer` sits in a call signature. */
function boundSignatureAlias(
type: ts.Type,
ctx: TypeMapperCtx,
): { name: "Parameters" | "ReturnType"; sig: ts.Signature } | null {
const { checker, resolveTypeParamTs } = ctx;
if (!resolveTypeParamTs) return null;
const alias = type.getAliasSymbol();
if (!alias || (alias.name !== "Parameters" && alias.name !== "ReturnType")) return null;
const aliasArgs = type.getAliasTypeArguments();
const arg = aliasArgs[0];
if (aliasArgs.length !== 1 || !arg || !(arg.flags & ts.TypeFlags.TypeParameter)) return null;
const isLibAlias = alias.declarations.some(
(d) => ts.isTypeAliasDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()),
);
if (!isLibAlias) return null;
const bound = resolveTypeParamTs(arg);
if (!bound || bound.flags & ts.TypeFlags.TypeParameter) return null;
const sigs = checker.getCallSignatures(bound);
const sig = sigs[0];
if (sigs.length !== 1 || !sig || sig.getTypeParameters().length > 0) return null;
return { name: alias.name, sig };
}
/** One position of a fixed-arity parameter tuple. An optional position
* still has a native slot (the function type's `T | undefined` slot). */
interface TupleElement {
type: ts.Type;
optional: boolean;
}
/** The positional parameters of a signature as a fixed tuple: a trailing
* rest flattened when its own type is a fixed tuple. Null for a variadic
* position, which has no fixed native arity. */
function signatureParameters(sig: ts.Signature, ctx: TypeMapperCtx): TupleElement[] | null {
const { checker } = ctx;
const out: TupleElement[] = [];
for (const p of sig.getParameters()) {
const decl = p.valueDeclaration;
const type = checker.getTypeOfSymbol(p);
if (decl !== undefined && ts.isParameter(decl)) {
if (decl.dotDotDotToken !== undefined) {
const inner = restTupleElements(type, ctx);
if (!inner) return null;
out.push(...inner);
continue;
}
out.push({
type,
optional: decl.questionToken !== undefined || decl.initializer !== undefined,
});
continue;
}
out.push({ type, optional: (p.flags & ts.SymbolFlags.Optional) !== 0 });
}
return out;
}
/** The required-only positional parameter types of a signature (the
* value-level `Parameters<F>` tuple), null when a position is optional
* or variadic: such a tuple's length varies per call. */
function signatureParameterTypes(sig: ts.Signature, ctx: TypeMapperCtx): ts.Type[] | null {
const elems = signatureParameters(sig, ctx);
if (!elems || elems.some((e) => e.optional)) return null;
return elems.map((e) => e.type);
}
/** The element types of a REST parameter's type when it is a fixed tuple:
* a tuple of required elements (`[number, string]`, the empty `[]`), a type
* parameter bound to one, or `Parameters<F>` over a bound function type.
* Such a rest parameter has a compile-time arity, so its native ABI is the
* flattened positional list (exactly the equivalent fixed signature —
* TypeScript treats `(...a: [A, B]) => R` and `(a: A, b: B) => R` alike). */
export function restTupleElementTypes(type: ts.Type, ctx: TypeMapperCtx): ts.Type[] | null {
return restTupleElements(type, ctx)?.map((e) => e.type) ?? null;
}
/** restTupleElementTypes with each position's optionality: a rest tuple
* may end in optional positions (`[A, B?]`, `Parameters<(a: A, b?: B) =>
* R>`), which flatten to `T | undefined` slots exactly as the function
* type's optional parameters do. */
function restTupleElements(type: ts.Type, ctx: TypeMapperCtx): TupleElement[] | null {
const { checker } = ctx;
if (type.flags & ts.TypeFlags.TypeParameter) {
const bound = ctx.resolveTypeParamTs?.(type);
return bound && bound !== type ? restTupleElements(bound, ctx) : null;
}
const viaAlias = boundSignatureAlias(type, ctx);
if (viaAlias) {
return viaAlias.name === "Parameters" ? signatureParameters(viaAlias.sig, ctx) : null;
}
if (!checker.isTupleType(type)) return null;
const ref = type as ts.TupleTypeReference;
const tupleShape =
(ref.elementFlags as ts.ElementFlags[] | undefined) !== undefined
? ref
: (ref.getTarget() as ts.TupleType | undefined);
const args = checker.getTypeArguments(ref);
if (args.length === 0) return [];
const flags = tupleShape?.elementFlags;
if (flags === undefined) return null;
if (flags.some((f) => !(f & (ts.ElementFlags.Required | ts.ElementFlags.Optional)))) return null;
return args.map((t, i) => ({
type: t,
optional: (flags[i]! & ts.ElementFlags.Optional) !== 0,
}));
}
/** One tuple element's IR type under the tuple mapping's rules. */
function mapTupleElement(t: ts.Type, ctx: TypeMapperCtx): IrType | null {
let et = (t.flags & ts.TypeFlags.Any) !== 0 ? DYN : mapType(t, ctx);
if (et?.kind === "void" && isUnitOnlyTsType(t, ctx.resolveTypeParam))
et = unitOnlyUnion(ctx.unions);
return !et || et.kind === "void" ? null : et;
}
/** The mapped element types of a fixed-tuple rest parameter type
* (restTupleElementTypes), null when it is not one or an element does not
* map. */
export function restTupleParamTypes(type: ts.Type, ctx: TypeMapperCtx): IrType[] | null {
const elems = restTupleElements(type, ctx);
if (!elems) return null;
const out: IrType[] = [];
for (const e of elems) {
let et = mapTupleElement(e.type, ctx);
// An optional position's slot holds undefined when the caller omits
// it, as an optional parameter's does (mapCallSignature).
if (et && e.optional && et.kind !== "dyn") et = withUndefinedArm(et, ctx.unions);
if (!et) return null;
out.push(et);
}
return out;
}
/** `Parameters<F>` (the fixed tuple of F's parameters, interned exactly as
* the spelled tuple maps) and `ReturnType<F>` (F's mapped result) over a
* bound function-typed type parameter. */
function mapBoundSignatureAlias(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
const target = boundSignatureAlias(type, ctx);
if (!target) return null;
if (target.name === "ReturnType") {
const ret = ctx.checker.getReturnTypeOfSignature(target.sig);
return ret.flags & ts.TypeFlags.Any ? DYN : mapType(ret, ctx);
}
const elems = signatureParameterTypes(target.sig, ctx);
if (!elems) return null;
if (elems.length === 0) return arrayOf(unitOnlyUnion(ctx.unions));
const fields: { name: string; type: IrType }[] = [];
for (let i = 0; i < elems.length; i++) {
const et = mapTupleElement(elems[i]!, ctx);
if (!et) return null;
fields.push({ name: String(i), type: et });
}
fields.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
return { kind: "record", shapeId: ctx.shapes.intern(fields, true) };
}
/** `Partial<T>` / `Readonly<T>` where T is a STILL-GENERIC type parameter —
* the one mapped-type spelling a monomorphized body can contain that the
* checker cannot resolve for us (`keyof T` is unknown inside the body, so
@@ -5107,13 +5418,17 @@ function mapGenericAwaitedAlias(type: ts.Type, ctx: TypeMapperCtx): IrType | nul
const aliasArgs = type.getAliasTypeArguments();
const arg = aliasArgs[0];
if (aliasArgs.length !== 1 || !arg) return null;
if (!(arg.flags & ts.TypeFlags.TypeParameter)) return null;
// `Awaited<ReturnType<F>>` (an async wrapper's settled result) resolves
// through F's bound signature like a bare type parameter.
const viaSignature =
arg.flags & ts.TypeFlags.TypeParameter ? null : boundSignatureAlias(arg, ctx);
if (!(arg.flags & ts.TypeFlags.TypeParameter) && viaSignature?.name !== "ReturnType") return null;
// Provenance: the STANDARD LIBRARY's alias, not a user's shadowing one.
const isLibAlias = alias.declarations.some(
(d) => ts.isTypeAliasDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()),
);
if (!isLibAlias) return null;
const bound = resolveTypeParam(arg);
const bound = viaSignature ? mapBoundSignatureAlias(arg, ctx) : resolveTypeParam(arg);
if (!bound) return null;
let r = bound;
while (r.kind === "promise") r = r.inner;
@@ -5139,11 +5454,15 @@ function mapGenericIndexedAccess(type: ts.Type, ctx: TypeMapperCtx): IrType | nu
const obj = type.getObjectType();
if (obj === undefined || !(obj.flags & ts.TypeFlags.TypeParameter)) return null;
const bound = resolveTypeParam(obj);
const idx = type.getIndexType();
if (idx === undefined) return null;
// `T[keyof T]` over a T bound to an unknown-valued record view
// (`Record<string, unknown>` maps to the checked-dynamic value, see
// mapRecordType): every member reads as unknown.
if (bound?.kind === "dyn" && idx.isIndexType() && idx.getTarget() === obj) return DYN;
if (!bound || bound.kind !== "record") return null;
const shape = shapes.get(bound.shapeId);
if (!shape || shape.tuple) return null;
const idx = type.getIndexType();
if (idx === undefined) return null;
let covered: IrType[] | null = null;
if (idx.isStringLiteralType()) {
const f = shape.fields.find((x) => x.name === idx.value);
@@ -5160,7 +5479,62 @@ function mapGenericIndexedAccess(type: ts.Type, ctx: TypeMapperCtx): IrType | nu
}
if (!covered || covered.length === 0) return null;
const first = covered[0]!;
return covered.every((t) => typeEquals(t, first)) ? first : null;
if (covered.every((t) => typeEquals(t, first))) return first;
// Only the `keyof T` index itself covers every field at once; a key
// parameter bound to string keeps its literal-key resolution below.
if (!(idx.isIndexType() && idx.getTarget() === obj)) return null;
// Fields of different scalar types (`Object.entries` over a bound record
// reads `T[keyof T]`): any one of them, the union of their types.
const arms: IrType[] = [];
for (const t of covered) {
const parts = t.kind === "union" ? (ctx.unions.get(t.unionId)?.arms ?? null) : [t];
if (!parts) return null;
for (const part of parts) {
if (
part.kind !== "f64" &&
part.kind !== "string" &&
part.kind !== "bool" &&
part.kind !== "undefinedT" &&
part.kind !== "nullT"
)
return null;
if (!arms.some((a) => typeEquals(a, part))) arms.push(part);
}
}
if (arms.length === 1) return arms[0]!;
arms.sort((a, b) => (typeKey(a) < typeKey(b) ? -1 : 1));
return { kind: "union", unionId: ctx.unions.intern(arms) };
}
/** `keyof T extends never ? A : B` over a bound record T (the lib's
* Object.entries element spelling): decided by whether the binding has any
* key. */
function mapBoundKeyofNeverConditional(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
const { resolveTypeParam, shapes } = ctx;
if (!resolveTypeParam || !type.isConditionalType()) return null;
const check = type.getCheckType();
const ext = type.getExtendsType();
if (!check || !ext || !(ext.flags & ts.TypeFlags.Never) || !check.isIndexType()) return null;
const target = check.getTarget();
if (!target || !(target.flags & ts.TypeFlags.TypeParameter)) return null;
const bound = resolveTypeParam(target);
// A T bound to the checked-dynamic value (`Record<string, unknown>`, an
// unknown-valued record view) has no static key set to decide the
// branch, but both branches of the lib's `keyof T extends never ?
// unknown : T[keyof T]` read as unknown there: one answer either way.
if (bound?.kind === "dyn") {
const whenEmpty = type.getTrueType();
const otherwise = type.getFalseType();
const a = whenEmpty ? mapType(whenEmpty, ctx) : null;
const b = otherwise ? mapType(otherwise, ctx) : null;
return a && b && typeEquals(a, b) ? a : null;
}
if (bound?.kind !== "record") return null;
const shape = shapes.get(bound.shapeId);
if (!shape || shape.tuple) return null;
const hasKeys = shape.fields.length > 0 || shape.indexValue !== undefined;
const branch = hasKeys ? type.getFalseType() : type.getTrueType();
return branch ? mapType(branch, ctx) : null;
}
/** A mapped type over a type parameter of the instance being lowered (`{
@@ -5303,10 +5677,13 @@ function matchInferReference(
const target = (t: ts.Type): ts.Type | undefined =>
t.isTypeReference?.() ? (t as ts.TypeReference).getTarget?.() : undefined;
const wantTarget = target(extendsType) ?? extendsType.getAliasSymbol();
if (!wantTarget) return undefined;
if (!wantTarget) return matchInferSignature(checked, extendsType, ctx);
const gotTarget = target(checked) ?? checked.getAliasSymbol();
if (!gotTarget) return checked.flags & ts.TypeFlags.Object ? undefined : false;
if (gotTarget !== wantTarget) return undefined;
if (!gotTarget)
return checked.flags & ts.TypeFlags.Object
? matchInferThenable(checked, extendsType, ctx)
: false;
if (gotTarget !== wantTarget) return matchInferThenable(checked, extendsType, ctx);
const wantArgs = target(extendsType)
? checker.getTypeArguments(extendsType as ts.TypeReference)
: extendsType.getAliasTypeArguments();
@@ -5327,6 +5704,154 @@ function matchInferReference(
return inferred;
}
/** The type parameter an `infer X` declares, when `t` is one. */
function inferSymbolOf(t: ts.Type, _ctx: TypeMapperCtx): ts.Symbol | undefined {
if (!(t.flags & ts.TypeFlags.TypeParameter)) return undefined;
const symbol = t.getSymbol();
return symbol &&
symbol.declarations.some((d) => d.parent !== undefined && ts.isInferTypeNode(d.parent))
? symbol
: undefined;
}
/** `checked extends (...args: any) => infer R` — the shape of the standard
* library's ReturnType, over a concrete function type: R binds to its
* result. Pattern parameters must be `any` or `any[]`, and a pattern rest
* `any` or `any[]`; anything else stays undecided. The match follows
* TypeScript's assignability: a primitive never matches a function type,
* a function that needs more arguments than a pattern without a rest
* passes never matches, and a positional `any[]` parameter needs the
* function's parameter there to accept an array. */
function matchInferSignature(
checked: ts.Type,
extendsType: ts.Type,
ctx: TypeMapperCtx,
): Map<ts.Symbol, ts.Type> | false | undefined {
const { checker } = ctx;
const wantSigs = checker.getCallSignatures(extendsType);
const want = wantSigs[0];
if (wantSigs.length !== 1 || !want || want.getTypeParameters().length > 0) return undefined;
const wantShape = signatureShape(want, ctx);
if (wantShape === undefined) return undefined;
const isAny = (t: ts.Type): boolean => (t.flags & ts.TypeFlags.Any) !== 0;
const isAnyArray = (t: ts.Type): boolean =>
checker.isArrayType(t) && isAny(checker.getTypeArguments(t as ts.TypeReference)[0]!);
if (!wantShape.params.every((p) => isAny(p.type) || isAnyArray(p.type))) return undefined;
if (wantShape.rest !== null && !isAny(wantShape.rest)) return undefined;
if (!(checked.flags & ts.TypeFlags.Object)) return false;
const gotSigs = checker.getCallSignatures(checked);
const got = gotSigs[0];
if (gotSigs.length !== 1 || !got || got.getTypeParameters().length > 0) return undefined;
const gotShape = signatureShape(got, ctx);
if (gotShape !== undefined) {
// `(a: any) => infer R` rejects `(a: string, b: string) => R` and
// `(...args: [string, number]) => R`: they need more arguments.
if (wantShape.rest === null && requiredArguments(gotShape) > wantShape.params.length)
return false;
for (let i = 0; i < wantShape.params.length; i++) {
if (!isAnyArray(wantShape.params[i]!.type)) continue;
const param = gotShape.params[i]?.type ?? gotShape.rest;
if (param === undefined || param === null) continue;
const accepts = acceptsArray(param, ctx);
if (accepts !== true) return accepts;
}
} else if (wantShape.params.some((p) => isAnyArray(p.type))) {
return undefined;
}
const wantRet = checker.getReturnTypeOfSignature(want);
const infer = inferSymbolOf(wantRet, ctx);
if (infer) return new Map([[infer, checker.getReturnTypeOfSignature(got)]]);
return wantRet.flags & ts.TypeFlags.Any ? new Map() : undefined;
}
/** A signature's parameters as a call sees them: the positional ones, with
* a fixed-tuple rest (`...args: [A, B?]`) flattened into positions, and the
* element type of an array rest (`any` for `...args: any`). Undefined when
* a parameter has no declaration, or the rest is a variadic tuple or
* another type. */
function signatureShape(
sig: ts.Signature,
ctx: TypeMapperCtx,
): { params: { type: ts.Type; optional: boolean }[]; rest: ts.Type | null } | undefined {
const { checker } = ctx;
const params: { type: ts.Type; optional: boolean }[] = [];
for (const p of sig.getParameters()) {
const decl = p.valueDeclaration;
if (decl === undefined || !ts.isParameter(decl)) return undefined;
const type = checker.getTypeOfSymbol(p);
if (decl.dotDotDotToken === undefined) {
params.push({
type,
optional: decl.questionToken !== undefined || decl.initializer !== undefined,
});
continue;
}
if (type.flags & ts.TypeFlags.Any) return { params, rest: type };
if (checker.isArrayType(type))
return { params, rest: checker.getTypeArguments(type as ts.TypeReference)[0]! };
const elems = restTupleElements(type, ctx);
if (!elems) return undefined;
for (const e of elems) params.push({ type: e.type, optional: e.optional });
return { params, rest: null };
}
return { params, rest: null };
}
/** The fewest arguments a call must pass: up to the last required position. */
function requiredArguments(shape: { params: { optional: boolean }[] }): number {
let required = 0;
shape.params.forEach((p, i) => {
if (!p.optional) required = i + 1;
});
return required;
}
/** Whether a parameter of this type accepts an `any[]` argument: true for
* `any`, `unknown` and array types, false for primitives, undefined
* otherwise. */
function acceptsArray(t: ts.Type, ctx: TypeMapperCtx): boolean | undefined {
if (t.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) return true;
if (ctx.checker.isArrayType(t)) return true;
const primitive =
ts.TypeFlags.StringLike |
ts.TypeFlags.NumberLike |
ts.TypeFlags.BooleanLike |
ts.TypeFlags.BigIntLike |
ts.TypeFlags.ESSymbolLike |
ts.TypeFlags.Undefined |
ts.TypeFlags.Null |
ts.TypeFlags.Void;
return t.flags & primitive ? false : undefined;
}
/** `checked extends PromiseLike<infer U>` (or `Promise<infer U>`) over the
* standard library's Promise: U binds to the settled type. An object with
* no `then` member is no thenable. Everything else stays undecided. */
function matchInferThenable(
checked: ts.Type,
extendsType: ts.Type,
ctx: TypeMapperCtx,
): Map<ts.Symbol, ts.Type> | false | undefined {
const { checker } = ctx;
const stdlibNamed = (t: ts.Type, names: readonly string[]): boolean => {
const symbol = t.getSymbol();
return (
symbol !== undefined &&
names.includes(symbol.name) &&
symbol.declarations.some((d) => ctx.isStdlibFile(d.getSourceFile()))
);
};
if (!stdlibNamed(extendsType, ["PromiseLike", "Promise"])) return undefined;
const wantArgs = checker.getTypeArguments(extendsType as ts.TypeReference);
const infer = wantArgs.length === 1 ? inferSymbolOf(wantArgs[0]!, ctx) : undefined;
if (!infer) return undefined;
if (stdlibNamed(checked, ["Promise", "PromiseLike"])) {
const gotArgs = checker.getTypeArguments(checked as ts.TypeReference);
return gotArgs.length === 1 ? new Map([[infer, gotArgs[0]!]]) : undefined;
}
return checker.getPropertyOfType(checked, "then") === undefined ? false : undefined;
}
/** The type mentions a mapped key or bound alias parameter (a template
* shape evalMappedTemplate does not reduce). */
function mentionsMappedParams(
@@ -5490,10 +6015,22 @@ export function isUnitOnlyTsType(t: ts.Type, resolveTypeParam?: TypeParamResolve
return parts.every(
(p) =>
(p.flags & UNIT) !== 0 ||
((p.flags & ts.TypeFlags.TypeParameter) !== 0 && resolveTypeParam?.(p)?.kind === "void"),
((p.flags & ts.TypeFlags.TypeParameter) !== 0 && resolveTypeParam?.(p)?.kind === "void") ||
boundVoidReturnType(p, resolveTypeParam),
);
}
/** `ReturnType<F>` over a type parameter bound to a void-returning
* function (a variadic wrapper's result slot): a unit-only value. */
function boundVoidReturnType(p: ts.Type, resolveTypeParam?: TypeParamResolver): boolean {
if (!resolveTypeParam || p.getAliasSymbol()?.name !== "ReturnType") return false;
const args = p.getAliasTypeArguments();
const arg = args[0];
if (args.length !== 1 || !arg || !(arg.flags & ts.TypeFlags.TypeParameter)) return false;
const bound = resolveTypeParam(arg);
return bound?.kind === "func" && bound.ret.kind === "void";
}
/** IR-level `t | undefined`, canonicalized and fenced exactly like the
* ts-union branch of mapType (typeKey-sorted arms, deduplicated;
* Date/dyn/void arms unrepresentable; a func arm IS representable — the
@@ -5626,6 +6163,7 @@ function mapErrorMetadataIntersection(type: ts.Type, ctx: TypeMapperCtx): IrType
});
if (!hasError) return null;
let error: IrType | null = null;
const extra: ts.Symbol[] = [];
for (const part of parts) {
const mapped = mapType(part, ctx);
if (mapped?.kind === "object" && RUNTIME_ERROR_CLASSES.has(mapped.className)) {
@@ -5646,14 +6184,45 @@ function mapErrorMetadataIntersection(type: ts.Type, ctx: TypeMapperCtx): IrType
: ["code", "syscall", "path", "dest"].includes(property.name)
? "string"
: null;
if (!expected || !(property.flags & ts.SymbolFlags.Optional)) return null;
if (!expected) {
extra.push(property);
continue;
}
const field = mapType(ctx.checker.getTypeOfSymbol(property), ctx);
const arms =
field?.kind === "union" ? ctx.unions.get(field.unionId)?.arms : field ? [field] : [];
if (!arms?.length || !arms.every((arm) => arm.kind === expected || arm.kind === "undefinedT"))
return null;
// A required member must still admit the absent value the slot reads.
if (
!(property.flags & ts.SymbolFlags.Optional) &&
!arms.some((arm) => arm.kind === "undefinedT")
)
return null;
}
}
if (!error) return null;
// Members beyond Error's own and the system-error metadata (`Error & {
// status: number }`, Object.assign's result) are own properties of the
// error's checked-dynamic node: each must map to a type a checked read
// can produce. Error's own string members keep their slots.
for (const property of extra) {
const field = mapType(ctx.checker.getTypeOfSymbol(property), ctx);
if (!field || field.kind === "void") return null;
if (property.name === "message" || property.name === "name") {
if (field.kind !== "string") return null;
continue;
}
if (property.name === "stack" || property.name === "cause") return null;
if (
!canDynCheckTo(
field,
(id) => ctx.shapes.get(id),
(id) => ctx.unions.get(id),
)
)
return null;
}
return error;
}
@@ -6161,6 +6730,8 @@ function mapRecordType(widened: ts.Type, ctx: TypeMapperCtx): IrType | null {
(inner.indexValue === undefined || inner.indexValue.kind === "dyn")
)
return DYN;
const covered = coveredOptionalFieldsShape(widened, inner, ctx);
if (covered !== null) return { kind: "record", shapeId: covered };
const merged = dictSpreadLiteralShape(widened, inner, ctx);
if (merged === "dyn") return DYN;
if (merged !== null) return { kind: "record", shapeId: merged };
@@ -6174,6 +6745,51 @@ function mapRecordType(widened: ts.Type, ctx: TypeMapperCtx): IrType | null {
}
}
/** An index signature already describes every OPTIONAL declared member
* whose type is its slot type: @types/node's `ProcessEnv extends
* Dict<string> { TZ?: string }` and every spread of it, or a declared
* environment `{ CI?: string } & Dictionary<string>`. At runtime such a
* member is just another key, so the shape interns as the pure
* index-signature record. Its slot admits undefined, as the optional
* members do (`env.CI = undefined` keeps the key). A hybrid layout would
* give each member a struct slot, which enumerates before the overflow
* keys (Node keeps one insertion order) and makes `delete` and spread
* answer differently for declared and undeclared keys. Required members,
* and members typed otherwise than the slot (whose reads the slot type
* could not give back), keep the hybrid shape. */
function coveredOptionalFieldsShape(
widened: ts.Type,
parts: RecordShapeParts,
ctx: TypeMapperCtx,
): string | null {
const slot = parts.indexValue;
if (!slot || parts.fields.length === 0) return null;
const { checker, shapes, unions } = ctx;
// The members' common type must be the slot plus undefined. Read it off
// the members (already interned) rather than building it: a hybrid that
// does not qualify must not intern anything new.
const optionalSlot = parts.fields[0]!.type;
if (!parts.fields.every((field) => typeEquals(field.type, optionalSlot))) return null;
if (optionalSlot.kind !== "union" || unions.armTag(optionalSlot.unionId, UNDEFINED_T) < 0)
return null;
const memberArms = unions.get(optionalSlot.unionId)?.arms ?? [];
const slotArms =
slot.kind === "union" ? (unions.get(slot.unionId)?.arms ?? []) : isUnitType(slot) ? [] : [slot];
const definedArms = memberArms.filter((arm): boolean => arm.kind !== "undefinedT");
const slotDefined = slotArms.filter((arm): boolean => arm.kind !== "undefinedT");
if (
definedArms.length === 0 ||
definedArms.length !== slotDefined.length ||
!slotDefined.every((arm) => definedArms.some((other) => typeEquals(arm, other)))
)
return null;
if (!isSupportedIndexValue(optionalSlot)) return null;
const props = checker.getPropertiesOfType(widened);
if (props.length !== parts.fields.length) return null;
if (!props.every((p) => (p.flags & ts.SymbolFlags.Optional) !== 0)) return null;
return shapes.intern([], false, optionalSlot, []);
}
/** The type of an object literal that spreads an index-signature record
* (`{ ...tags, bucket: b, status }`, the metric-tags idiom). The checker
* drops the signature and types the literal by its explicit members only,
+5
View File
@@ -535,6 +535,7 @@ async function compileExecutableNative(
fileHandle: features.fileHandle,
fetch: features.fetch,
...(features.eventTarget ? { eventTarget: true } : {}),
...(features.wasm ? { wasm: true } : {}),
...(features.warningReplace ? { warningReplace: true } : {}),
...(features.stdioExtended ? { stdioExtended: true } : {}),
zlib: features.zlib,
@@ -550,6 +551,7 @@ async function compileExecutableNative(
bigint: features.bigint,
searchParams: features.searchParams,
qs: features.qs,
dynBuiltinClass: features.dynBuiltinClass,
parseArgs: features.parseArgs,
stream: features.stream,
net: features.net,
@@ -566,6 +568,7 @@ async function compileExecutableNative(
tlsCa: features.tlsCa,
cipher: features.cipher,
webcrypto: features.webcrypto,
perfLoop: features.perfLoop,
...(onArtifactReady === undefined ? {} : { onArtifactReady }),
...(ffi === null
? {}
@@ -730,6 +733,7 @@ async function prepareExecutableInput(
wasi: buildPlatform === "wasi",
runtimeAbiMarker: outputKind === "obj",
objectAudit: opts.sanitize === true,
...(opts.sanitize === true ? { wasmBoundsChecks: true } : {}),
...inlineRcOption(opts.optimization, opts.sanitize === true),
});
} catch (err) {
@@ -795,6 +799,7 @@ async function prepareExecutableInput(
wasi: buildPlatform === "wasi",
runtimeAbiMarker: useRuntimePack,
objectAudit: opts.sanitize === true,
...(opts.sanitize === true ? { wasmBoundsChecks: true } : {}),
...inlineRcOption(opts.optimization, opts.sanitize === true),
...(opts.detachedExit === true ? { detachedExit: true } : {}),
});
@@ -75,6 +75,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"process.stdoutWriteBytes",
"process.stderrWriteBytes",
"procStream.write",
"child.stdioStream",
"stream.unboxDyn",
"weakMap.new",
"weakSet.new",
"dyn.fromEntries",
@@ -133,6 +135,20 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"blob.new",
"blob.fileNew",
"blob.slice",
"wasm.compile",
"wasm.validate",
"wasm.instantiate",
"wasm.moduleNew",
"wasm.instanceNew",
"wasm.errorNew",
"wasm.memoryNew",
"wasm.tableNew",
"wasm.globalNew",
"wasm.withoutNew",
"wasm.moduleImports",
"wasm.moduleExports",
"wasm.moduleCustomSections",
"wasm.valueOf",
"events.addAbortListener",
"fetch.streamNew",
"fetch.streamFrom",
@@ -332,6 +348,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"stream.onDyn",
"readable.resume",
"readable.pipe",
"readable.pipeGeneral",
"readable.unpipe",
"writable.write",
"writable.writeStr",
@@ -375,6 +392,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"perf.entries",
"perf.clearMarks",
"perf.clearMeasures",
// monitorEventLoopDelay's resolution: Node's ERR_OUT_OF_RANGE.
"perf.eldNew",
// Non-TTY stdin: Node's stdin has no setRawMode member, so the call is
// the exact catchable TypeError.
"process.stdinSetRawMode",
@@ -411,6 +430,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"error.stack",
"error.cause",
"error.setCause",
"error.assign",
"error.deleteCause",
"dyn.objectTag",
// Numeric coercion runs user valueOf/toString — throws propagate.
@@ -567,6 +587,9 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"dyn.objCreateWithProperties",
"dyn.getPrototype",
"dyn.setPrototype",
// a proxy in the prototype chain can throw
"dyn.builtinInstanceOf",
"dyn.functionInstanceOf",
"dyn.getOwnPropertyNames",
"dyn.ownKeys",
"dyn.getOwnPropertySymbols",
+105 -2
View File
@@ -108,6 +108,45 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
"blob.bytes": { argTypes: [DYN], result: { kind: "promise", inner: BYTES_U8 } },
"blob.arrayBuffer": { argTypes: [DYN], result: { kind: "promise", inner: DYN } },
"blob.is": { argTypes: [DYN, F64], result: BOOL },
/** WebAssembly (scr_wasm.c): modules translated at build time
* (wasm/translate.ts), found at run time by the SHA-256 of their bytes.
* compile and instantiate over bytes settle on a later loop turn;
* instantiate over a Module settles now, as in Node. `wasm.is` answers
* instanceof for Module, Instance, Memory, Table and Global (kinds 0-4)
* and CompileError, LinkError and RuntimeError (5-7); `wasm.errorNew`
* constructs one of the error classes (kind 5-7, message, options). */
"wasm.compile": { argTypes: [DYN], result: { kind: "promise", inner: DYN } },
"wasm.validate": { argTypes: [DYN], result: BOOL },
"wasm.instantiate": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } },
"wasm.moduleNew": { argTypes: [DYN], result: DYN },
"wasm.instanceNew": { argTypes: [DYN, DYN], result: DYN },
"wasm.is": { argTypes: [DYN, F64], result: BOOL },
"wasm.errorNew": { argTypes: [F64, DYN, DYN], result: DYN },
/** new WebAssembly.Memory(descriptor), Table(descriptor, value) and
* Global(descriptor, value) (an undefined value is the type's default);
* `wasm.withoutNew` throws the TypeError for Module, Instance, Memory,
* Table or Global (kinds 0-4) called without new, after its arguments
* evaluate. */
"wasm.memoryNew": { argTypes: [DYN], result: DYN },
"wasm.tableNew": { argTypes: [DYN, DYN], result: DYN },
"wasm.globalNew": { argTypes: [DYN, DYN], result: DYN },
"wasm.withoutNew": { argTypes: [F64, DYN, DYN], result: DYN },
/** WebAssembly.Module.imports(module), exports(module) and
* customSections(module, name): checked-dynamic arrays of descriptor
* objects and ArrayBuffers. */
"wasm.moduleImports": { argTypes: [DYN], result: DYN },
"wasm.moduleExports": { argTypes: [DYN], result: DYN },
"wasm.moduleCustomSections": { argTypes: [DYN, DYN], result: DYN },
/** `v.valueOf()` on a WebAssembly-typed receiver: a Global's value,
* dyn.valueOf's answer for anything else. */
"wasm.valueOf": { argTypes: [DYN], result: DYN },
/** WebAssembly.compile (0), instantiate (1) and validate (2) as function
* values, and the namespace object holding them. */
"wasm.function": { argTypes: [F64], result: DYN },
"wasm.namespace": { argTypes: [], result: DYN },
/** The global object viewed through a cast to a type with a WebAssembly
* member: an object holding the namespace. */
"wasm.globalView": { argTypes: [], result: DYN },
"abort.timeout": { argTypes: [DYN], result: DYN },
"abort.now": { argTypes: [DYN], result: DYN },
"abort.any": { argTypes: [DYN], result: DYN },
@@ -401,6 +440,20 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
"perf.entryDetail": { argTypes: [F64], result: DYN },
"perf.entryKeep": { argTypes: [DYN], result: VOID },
"perf.entryRef": { argTypes: [F64], result: DYN },
/** perf_hooks event-loop metrics (scr_perf_loop.c): loopStart is the
* loop's start in performance.now() ms (0 before it), idleTime the idle
* ms since. eldNew(resolution ms) creates a monitorEventLoopDelay
* histogram handle (MAY THROW Node's resolution RangeError); enable/disable answer whether the state changed;
* eldPercentile expects Node's validated range; eldStat(which) reads
* 0 min, 1 max, 2 mean, 3 stddev, 4 count, 5 exceeds. */
"perf.loopStart": { argTypes: [], result: F64 },
"perf.idleTime": { argTypes: [], result: F64 },
"perf.eldNew": { argTypes: [F64], result: F64 },
"perf.eldEnable": { argTypes: [F64], result: BOOL },
"perf.eldDisable": { argTypes: [F64], result: BOOL },
"perf.eldReset": { argTypes: [F64], result: VOID },
"perf.eldPercentile": { argTypes: [F64, F64], result: F64 },
"perf.eldStat": { argTypes: [F64, F64], result: F64 },
/** process.memoryUsage(): memoryUsageTake samples the resident set and
* the runtime heap once (MAY THROW Node's uv_resident_set_memory error)
* and memoryUsageField(k) reads that sample (0 rss, 1 heapTotal, 2
@@ -2594,8 +2647,13 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
* queue, end/destroy settle it, writable is a pure state read, and
* listener callbacks move into the writer registry. */
"child.stdin": { argTypes: [CHILD_T], result: VOID },
/** Under the general stream representation (general-streams.ts), a
* stdio read (slot 0, 1 or 2) answers the child's bridged runtime stream
* — `Writable | null` for stdin, `Readable | null` otherwise; the bridge
* throws when the program lacks the stream unit's boxing helpers. */
"child.stdioStream": { argTypes: [CHILD_T, F64], result: VOID },
// Listener registrations: callback func shapes are program-dependent
// (zero-param / Buffer / Buffer-armed union) — the slots pin arity,
// (zero-param / Buffer / string / unknown / Buffer-armed union) — the slots pin arity,
// the stream receiver, and the once-flag.
"stream.onData": { argTypes: [CHILDSTREAM_T, null, BOOL], result: VOID },
"stream.onDataStr": { argTypes: [CHILDSTREAM_T, null, BOOL], result: VOID },
@@ -2615,6 +2673,21 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
* stderrWrite paths so buffering and ordering stay identical. Data
* borrowed; the bool is Node's always-true backpressure signal. */
"procStream.write": { argTypes: [PROCSTREAM_T, STRING], result: BOOL },
/** process.stdout/stderr flowing into a runtime Writable slot under the
* general stream representation: an fd-backed Writable (+1) whose
* writes take process.stdout.write's synchronous path. */
/** A pipeline() stage that is a checked-dynamic value: a boxed native
* stream unwraps by identity (+1); anything else throws the given
* not-supported message. Typed as a Duplex so every stage position
* accepts it; the runtime pipeline checks the stream's real sides. */
"stream.unboxDyn": {
argTypes: [DYN, STRING],
result: { kind: "object", className: "%Duplex" },
},
"stream.stdioWritable": {
argTypes: [PROCSTREAM_T],
result: { kind: "object", className: "%Writable" },
},
"child.killed": { argTypes: [CHILD_T], result: BOOL },
"child.kill": { argTypes: [CHILD_T, STRING], result: BOOL },
"child.killNum": { argTypes: [CHILD_T, F64], result: BOOL },
@@ -3203,6 +3276,23 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
"error.hasCause": { argTypes: [null], result: BOOL },
/** Assignment borrows both operands and retains the new cause. */
"error.setCause": { argTypes: [null, DYN], result: VOID },
/** `err.code = s` on an error-hierarchy receiver: the code slot that
* error.code reads and coded throws stamp. Borrows both; never throws. */
"error.setCode": { argTypes: [null, STRING], result: VOID },
/** `err.code = v` with a value that is not statically a string: strings
* take the code slot, anything else clears it and sets the dynamic own
* property. Borrows both; never throws. */
"error.setCodeDyn": { argTypes: [null, DYN], result: VOID },
/** `Object.assign(err, ...sources)` onto an error-hierarchy receiver
* (args: the error, a dyn array of the sources): the own-member copy
* onto its checked-dynamic node, then the typed slots (name, message,
* stack, cause, code) re-read from it. Throws what the copy throws. */
"error.assign": { argTypes: [null, DYN], result: VOID },
/** `key in err` for a key no compiled class of the receiver declares
* (args: the error, the key): a runtime-set own property (Object.assign,
* throw-site fields, dynamic writes) or an Object.prototype member. */
"error.hasKey": { argTypes: [null, STRING], result: BOOL },
"error.hasCode": { argTypes: [null], result: BOOL },
"error.defineCause": { argTypes: [null, DYN], result: VOID },
"error.deleteCause": { argTypes: [null], result: VOID },
/** The compiler-resolved Node-parity throw for always-throwing lowered
@@ -3495,6 +3585,16 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
"dyn.classPrototype": { argTypes: [DYN, DYN], result: DYN },
"dyn.classBasePrototype": { argTypes: [DYN], result: DYN },
"dyn.classInherit": { argTypes: [DYN, DYN], result: DYN },
/** EventEmitter (0), Stream (1), Readable (2), Writable (3), Duplex (4),
* Transform (5) and PassThrough (6) as untyped constructor values, one
* per thread (scr_dyn_builtin_class.c). */
"dyn.builtinClass": { argTypes: [F64], result: DYN },
/** `value instanceof C` for those constructors, over the value's
* prototype chain. */
"dyn.builtinInstanceOf": { argTypes: [DYN, F64], result: BOOL },
/** `value instanceof f` for an untyped constructor function: the
* prototype chain against f.prototype. */
"dyn.functionInstanceOf": { argTypes: [DYN, DYN], result: BOOL },
"dyn.classSuper": { argTypes: [DYN, DYN, DYN], result: VOID },
"dyn.assignPrototype": { argTypes: [DYN, DYN, DYN], result: DYN },
"dyn.definePrototypeProps": { argTypes: [DYN, DYN, DYN], result: DYN },
@@ -3652,7 +3752,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
* returns the below-hwm answer. readable.read: (recv, size — -1 for
* absent) → Buffer|null union. pause/resume return recv +1 (`this`);
* isPaused answers the flag. readable.pipe: (recv, dst, end) → dst +1,
* fires 'pipe' on dst; readable.unpipe (recv[, dst]) → recv +1.
* fires 'pipe' on dst; readable.pipeGeneral: the same, with `end` off
* when dst is the converted process.stdout / process.stderr;
* readable.unpipe (recv[, dst]) → recv +1.
* writable.write/writeStr: (recv, chunk[, cb]) → below-hwm bool (cb
* MOVES when present, called after the user write completes).
* writable.end: (recv, flags[, chunk][, cb]) → recv +1. cork/uncork are
@@ -3850,6 +3952,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
"readable.resume": { argTypes: [null], result: VOID },
"readable.isPaused": { argTypes: [null], result: BOOL },
"readable.pipe": { argTypes: [null, null, BOOL], result: VOID },
"readable.pipeGeneral": { argTypes: [null, null, BOOL], result: VOID },
"readable.unpipe": { argTypes: [null], result: VOID },
"readable.flowing": { argTypes: [null], result: VOID },
"writable.write": { argTypes: [null, BYTES_U8], result: BOOL },
@@ -212,6 +212,8 @@ const MODULE_FIELDS: Record<StringFields<Omit<IrModule, "functions">>, Role> = {
"contracts.acyclic[]": "other",
"contracts.immortalGlobals[]": "other",
"contracts.programLifetime[]": "other",
"wasmModules[].sha256": "other",
"wasmModules[].bytes": "other",
"threadSlotKeys[]": "other",
};
+18
View File
@@ -1044,6 +1044,24 @@ export interface IrModule {
* write in the program (ir/contract-check.ts). Backends may rely on them
* unconditionally. Absent when the program writes no contract. */
contracts?: IrContracts;
/** The WebAssembly modules translated into this program (wasm/
* translate.ts), present exactly when the program uses the WebAssembly
* API (possibly empty). The backend appends each module's translation
* and emits the registry the runtime looks modules up in. */
wasmModules?: IrWasmModule[];
/** The program calls WebAssembly.Module.customSections, so translated
* modules keep their custom sections (otherwise they carry none, and
* debug sections cost nothing). */
wasmCustomSections?: boolean;
}
/** A WebAssembly module whose bytes the build saw, validated within the
* translatable feature level. */
export interface IrWasmModule {
/** Hex SHA-256 of the bytes. */
sha256: string;
/** The bytes, base64. */
bytes: string;
}
/** Verified contract facts, keyed by IR names. */
+49 -6
View File
@@ -149,6 +149,7 @@ const ABORT_SIGNAL_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
/** Entry points that link the stream unit (with STREAM_LIB_PREFIXES). */
const STREAM_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"child.stdioStream",
"console.bind",
"crypto.hashStream",
"crypto.hashStreamPiped",
@@ -167,6 +168,8 @@ const STREAM_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"stream.destroyErr",
"stream.errored",
"stream.prop",
"stream.stdioWritable",
"stream.unboxDyn",
"stream.workerStdio",
]);
@@ -238,8 +241,21 @@ const CHILD_PROCESS_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
/** libCall name prefixes that link the child-process unit. */
const CHILD_PROCESS_LIB_PREFIXES: readonly string[] = ["child.", "cp.", "spawnRes.", "writer."];
/** The entry points scr_dyn_builtin_class.c defines. The unit calls
* listeners through the dyn-invoke unit, renders unhandled 'error'
* payloads through the inspect unit, creates EventEmitter's private
* symbols through the symbol unit and shares defaultMaxListeners with the
* emitter unit, so each of them links all five. */
const BUILTIN_CLASS_LIB_FNS: readonly IrLibFn[] = [
"dyn.builtinClass",
"dyn.builtinInstanceOf",
"dyn.functionInstanceOf",
];
/** Entry points that link the symbol unit, besides the sym. and disposable. families. */
const SYMBOL_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
// EventEmitter's kShapeMode and kCapture properties
...BUILTIN_CLASS_LIB_FNS,
"dyn.usingDispose",
"dyn.usingMethod",
"util.isDeepStrictEqual",
@@ -467,7 +483,12 @@ const RUNTIME_FEATURE_RULES = {
* queueMicrotask scheduling goes through the runtime's *_als entry
* points, which carry the active stores into each callback. */
asyncContext: rule({ fns: [...ASYNC_CONTEXT_LIB_FNS] }),
bigint: rule({ fns: ["process.hrtimeValue"], prefixes: ["bigint."], types: ["bigint"] }),
/** scr_wasm.c converts i64 values through BigInt. */
bigint: rule({
fns: ["process.hrtimeValue"],
prefixes: ["bigint.", "wasm."],
types: ["bigint"],
}),
childProcess: rule({
fns: [...CHILD_PROCESS_LIB_FNS],
prefixes: CHILD_PROCESS_LIB_PREFIXES,
@@ -494,14 +515,15 @@ const RUNTIME_FEATURE_RULES = {
(fn) => fn.async === true && fn.generator === undefined && fn.returnType.kind === "dyn",
),
// The legacy url functions and crypto.createCipheriv emit their
// deprecation warnings through the dyn-async unit.
// deprecation warnings through the dyn-async unit; scr_wasm.c boxes its
// handles and promises through it.
fns: [
...DYN_ASYNC_LIB_FNS,
...READABLE_DYN_ASYNC_LIB_FNS,
...LEGACY_URL_LIB_FNS,
"crypto.cipherNew",
],
prefixes: READABLE_DYN_ASYNC_PREFIXES,
prefixes: [...READABLE_DYN_ASYNC_PREFIXES, "wasm."],
expr: (node, tables) =>
(node.kind === "awaitExpr" && node.type.kind === "dyn") ||
// Rejection reasons cross as dyn values (allSettled's records, any's
@@ -510,9 +532,16 @@ const RUNTIME_FEATURE_RULES = {
(node.name === "promise.allSettled" || node.name === "promise.any")) ||
(node.kind === "dynFrom" && boxesPromise(node.value.type, tables(), new Set())),
}),
/** EventEmitter and the stream classes as untyped constructor values
* (scr_dyn_builtin_class.c), which call listeners through the dyn-invoke
* unit, render unhandled 'error' payloads through the inspect unit and
* create EventEmitter's private symbols through the symbol unit. */
dynBuiltinClass: rule({
fns: [...BUILTIN_CLASS_LIB_FNS],
}),
dynInvoke: rule({
initial: (mod) => mod.workers === true,
fns: [...DYN_INVOKE_LIB_FNS, ...UTIL_CALLABLE_LIB_FNS],
fns: [...DYN_INVOKE_LIB_FNS, ...UTIL_CALLABLE_LIB_FNS, ...BUILTIN_CLASS_LIB_FNS],
// Constant-key reads of Object.prototype member names answer the
// inherited member through scr_dyn_invoke.c.
expr: (node, _tables) =>
@@ -529,7 +558,7 @@ const RUNTIME_FEATURE_RULES = {
emitter: rule({
initial: (mod) =>
mod.workers === true || (mod.classes ?? []).some((c) => c.name === RUNTIME_EMITTER_CLASS),
fns: [...STREAM_LIB_FNS],
fns: [...STREAM_LIB_FNS, ...BUILTIN_CLASS_LIB_FNS],
prefixes: ["emitter.", ...STREAM_LIB_PREFIXES],
}),
/** Subclasses of the runtime EventTarget/Event bases tear down and box
@@ -563,7 +592,13 @@ const RUNTIME_FEATURE_RULES = {
* terminal unit prints completion errors through it when there is a
* completer. */
inspect: rule({
fns: ["global.native", "util.styleText", "crypto.cipherNew", ...UTIL_CALLABLE_LIB_FNS],
fns: [
"global.native",
"util.styleText",
"crypto.cipherNew",
...BUILTIN_CLASS_LIB_FNS,
...UTIL_CALLABLE_LIB_FNS,
],
prefixes: ["insp.", "console.", "test."],
libCall: (node) => node.fn === "rl.terminal" && !isDynUndefined(node.args[3]!),
}),
@@ -588,6 +623,9 @@ const RUNTIME_FEATURE_RULES = {
/** scr_util_callable.c reads options through the parseArgs unit's
* descriptors. */
parseArgs: rule({ fns: [...PARSE_ARGS_LIB_FNS, ...UTIL_CALLABLE_LIB_FNS] }),
/** perf_hooks event-loop metrics (eventLoopUtilization,
* monitorEventLoopDelay): scr_perf_loop.c. */
perfLoop: rule({ fns: ["perf.idleTime"], prefixes: ["perf.loop", "perf.eld"] }),
processEvents: rule({ fns: [...PROCESS_EVENT_LIB_FNS, "util.styleText"], types: ["procStream"] }),
/** scr_url_legacy.c parses and stringifies queries. */
qs: rule({ fns: [...QS_LIB_FNS, ...LEGACY_URL_LIB_FNS] }),
@@ -640,6 +678,8 @@ const RUNTIME_FEATURE_RULES = {
tlsCa: rule({ prefixes: ["tlsca."] }),
/** process.emitWarning is assigned (process.setEmitWarning). */
warningReplace: rule({ fns: ["process.setEmitWarning"] }),
/** The WebAssembly API (scr_wasm.c). */
wasm: rule({ prefixes: ["wasm."] }),
/** crypto.subtle importKey/sign/verify/digest and CryptoKey:
* scr_webcrypto.c. */
webcrypto: rule({ prefixes: ["webcrypto."] }),
@@ -667,6 +707,7 @@ function noRuntimeFeatures(): RuntimeFeatures {
dgram: false,
dnsPromises: false,
dynAsync: false,
dynBuiltinClass: false,
dynInvoke: false,
dynTier: false,
emitter: false,
@@ -683,6 +724,7 @@ function noRuntimeFeatures(): RuntimeFeatures {
net: false,
nodeTest: false,
parseArgs: false,
perfLoop: false,
processEvents: false,
qs: false,
regex: false,
@@ -693,6 +735,7 @@ function noRuntimeFeatures(): RuntimeFeatures {
tls: false,
tlsCa: false,
warningReplace: false,
wasm: false,
webcrypto: false,
zlib: false,
};
+21 -2
View File
@@ -4113,6 +4113,11 @@ function validateFunction(
e.fn === "error.cause" ||
e.fn === "error.hasCause" ||
e.fn === "error.setCause" ||
e.fn === "error.setCode" ||
e.fn === "error.setCodeDyn" ||
e.fn === "error.assign" ||
e.fn === "error.hasKey" ||
e.fn === "error.hasCode" ||
e.fn === "error.defineCause" ||
e.fn === "error.deleteCause"
) {
@@ -5228,6 +5233,20 @@ function validateFunction(
}
return true;
}
if (e.fn === "child.stdioStream") {
const def = e.type.kind === "union" ? unions.get(e.type.unionId) : undefined;
const slot = e.args[1];
const className =
slot?.kind === "numLit" ? (slot.value === 0 ? "%Writable" : "%Readable") : null;
const ok =
def &&
className !== null &&
def.arms.length === 2 &&
def.arms.some((a) => a.kind === "object" && a.className === className) &&
def.arms.some((a) => a.kind === "nullT");
if (!ok) err(`libCall ${e.fn} must return the stream class of its stdio slot or null`, e.loc);
return true;
}
if (e.fn === "child.stdin" || e.fn === "child.stdout" || e.fn === "child.stderr") {
const def = e.type.kind === "union" ? unions.get(e.type.unionId) : undefined;
const handleKind = e.fn === "child.stdin" ? "childWriter" : "childStream";
@@ -5719,9 +5738,9 @@ function validateFunction(
}
return true;
}
if (e.fn === "readable.pipe") {
if (e.fn === "readable.pipe" || e.fn === "readable.pipeGeneral") {
if (!isStreamObject(e.args[1]?.type) || !typeEquals(e.type, e.args[1]!.type)) {
err(`libCall readable.pipe must return its destination's type`, e.loc);
err(`libCall ${e.fn} must return its destination's type`, e.loc);
}
return true;
}
@@ -54,6 +54,11 @@ const LIB_MODE_REFUSED_PREFIXES: readonly [string, string][] = [
["webcrypto.", "WebCrypto (crypto.subtle)"],
["fs.watch", "fs.watch"],
["watcher.", "fs.watch"],
// The event-loop metrics sample the loop and its timers (scr_perf_loop.c),
// which library links exclude.
["perf.loopStart", "performance.eventLoopUtilization"],
["perf.idleTime", "performance.eventLoopUtilization"],
["perf.eld", "perf_hooks.monitorEventLoopDelay"],
["test.", "the node:test surface"],
["readable.", "the node:stream surface"],
["writable.", "the node:stream surface"],
@@ -158,6 +163,7 @@ export function moduleLibAsyncSurface(mod: IrModule): { surface: string; loc: Sr
[features.nodeTest, "the node:test surface"],
[features.dynAsync, "the checked-dynamic async surface"],
[features.dc, "the diagnostics_channel surface"],
[features.perfLoop, "the perf_hooks event-loop metrics"],
];
for (const [on, surface] of coarse) {
if (on) return { surface, loc: entryLoc };
+2
View File
@@ -89,6 +89,7 @@ async function main(): Promise<number> {
bigint: false,
searchParams: false,
qs: false,
dynBuiltinClass: false,
parseArgs: false,
stream: false,
net: false,
@@ -102,6 +103,7 @@ async function main(): Promise<number> {
tls: false,
cipher: false,
webcrypto: false,
perfLoop: false,
tlsCa: false,
},
options.optimization ?? "release",
+2
View File
@@ -143,6 +143,7 @@ function validFeatures(value: unknown): value is NativeLinkFeatures {
"bigint",
"searchParams",
"qs",
"dynBuiltinClass",
"parseArgs",
"stream",
"net",
@@ -157,6 +158,7 @@ function validFeatures(value: unknown): value is NativeLinkFeatures {
"tlsCa",
"cipher",
"webcrypto",
"perfLoop",
].every((name) => typeof features[name] === "boolean");
}
@@ -75,6 +75,7 @@ test.skipIf(process.platform !== "linux" && process.platform !== "darwin")(
workers: false,
youngNursery: false,
eventTarget: false,
wasm: false,
dynTier: false,
warningReplace: false,
stdioExtended: false,
@@ -95,6 +96,7 @@ test.skipIf(process.platform !== "linux" && process.platform !== "darwin")(
bigint: false,
searchParams: false,
qs: false,
dynBuiltinClass: false,
parseArgs: false,
stream: false,
net: false,
@@ -109,6 +111,7 @@ test.skipIf(process.platform !== "linux" && process.platform !== "darwin")(
tlsCa: false,
cipher: false,
webcrypto: false,
perfLoop: false,
nativeFetch: false,
netEffective: false,
httpEffective: false,
+61
View File
@@ -0,0 +1,61 @@
/* Base64 for WebAssembly module bytes: decoding inline modules found in
* source text (discover.ts) and carrying translated modules on the IR
* (IrModule.wasmModules). Plain TypeScript, so the native compiler
* compiles it too. */
const BASE64_VALUES = (() => {
const table = new Int16Array(128).fill(-1);
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
for (let i = 0; i < alphabet.length; i++) table[alphabet.charCodeAt(i)] = i;
// base64url spellings
table["-".charCodeAt(0)] = 62;
table["_".charCodeAt(0)] = 63;
return table;
})();
/** Decodes standard or URL-safe base64 (padding optional); null when the
* text is not base64. */
export function decodeBase64(text: string): Uint8Array | null {
let end = text.length;
while (end > 0 && text.charCodeAt(end - 1) === 61) end--;
if (text.length - end > 2 || end % 4 === 1) return null;
const out = new Uint8Array(Math.floor((end * 3) / 4));
let acc = 0;
let bits = 0;
let j = 0;
for (let i = 0; i < end; i++) {
const c = text.charCodeAt(i);
const v = c < 128 ? BASE64_VALUES[c]! : -1;
if (v < 0) return null;
acc = ((acc << 6) | v) & 0xffffff;
bits += 6;
if (bits >= 8) {
bits -= 8;
out[j++] = (acc >> bits) & 0xff;
}
}
return j === out.length ? out : out.subarray(0, j);
}
const BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
/** Standard base64 with padding. */
export function encodeBase64(bytes: Uint8Array): string {
const parts: string[] = [];
let chunk = "";
for (let i = 0; i < bytes.length; i += 3) {
const a = bytes[i]!;
const b = i + 1 < bytes.length ? bytes[i + 1]! : 0;
const c = i + 2 < bytes.length ? bytes[i + 2]! : 0;
const n = (a << 16) | (b << 8) | c;
chunk += BASE64_ALPHABET[(n >> 18) & 63]! + BASE64_ALPHABET[(n >> 12) & 63]!;
chunk += i + 1 < bytes.length ? BASE64_ALPHABET[(n >> 6) & 63]! : "=";
chunk += i + 2 < bytes.length ? BASE64_ALPHABET[n & 63]! : "=";
if (chunk.length >= 65536) {
parts.push(chunk);
chunk = "";
}
}
parts.push(chunk);
return parts.join("");
}
+7 -16
View File
@@ -20,6 +20,7 @@
* The decoder never evaluates anything and keeps no module contents beyond
* the counts and the feature set it reports. It is plain TypeScript that
* the native compiler also compiles. */
import { readMemargFlags } from "./module.js";
/** Proposals the summary can name, in report order. The first group is the
* translatable set; the rest are reported as unsupported. */
@@ -1244,19 +1245,9 @@ class FunctionValidator {
/** A memarg. A memory other than 0 stops typing (multi-memory). */
memarg(naturalAlign: number): void {
const r = this.r;
let align = r.u32();
let multi = false;
if (align >= 64 && align < 128) {
align -= 64;
multi = true;
} else if (align >= 64) {
r.malformed("malformed memop flags");
}
let memory = 0;
if (multi) {
memory = r.u32();
if (memory !== 0) this.otherMemory(memory);
}
const { align, memory } = readMemargFlags(() => r.u32());
if (align < 0) r.malformed("malformed memop flags");
if (memory !== 0) this.otherMemory(memory);
if (this.m.memory64) r.u64();
else r.u32();
if (!this.typed) return;
@@ -1840,9 +1831,9 @@ class FunctionValidator {
scanMemarg(): void {
const r = this.r;
const align = r.u32();
if (align >= 128) r.malformed("malformed memop flags");
if (align >= 64 && r.u32() !== 0) this.m.feature("multi-memory");
const { align, memory } = readMemargFlags(() => r.u32());
if (align < 0) r.malformed("malformed memop flags");
if (memory !== 0) this.m.feature("multi-memory");
if (this.m.memory64) r.u64();
else r.u32();
}
+26 -39
View File
@@ -6,7 +6,9 @@
* 1. path: a `.wasm` path the compiler resolves from the source text
* (`require.resolve`, `createRequire(...).resolve`, `import.meta.resolve`,
* `new URL("./x.wasm", import.meta.url)`, and `path.join(__dirname, ...)`
* or `__dirname + "/x.wasm"`);
* or `__dirname + "/x.wasm"`), or a `.so` path spelled the same ways
* whose file is a module (WebAssembly shared libraries, such as
* quickjs-wasi's extensions);
* 2. inline: a string literal that decodes as base64 to a module, or an
* array literal of its bytes;
* 3. package: every `.wasm` file shipped by a reached npm package whose
@@ -28,6 +30,9 @@ import {
trackedReadFile,
} from "../frontend/input-tracker.js";
import { analyzeWasm, isWasmHeader, type WasmModuleSummary } from "./binary.js";
import { decodeBase64 } from "./base64.js";
export { decodeBase64 };
export type WasmSourceKind = "path" | "inline" | "package" | "config";
@@ -56,6 +61,9 @@ export interface WasmDiscoveryInput {
configured: readonly string[];
/** require.resolve from `fromFile`, or null when it does not resolve. */
resolveRequest: (fromFile: string, specifier: string) => string | null;
/** Receives each distinct module's bytes once, when it is first found
* (builds translate them). */
onModule?: (sha256: string, bytes: Uint8Array) => void;
}
const SCAN_KINDS: ReadonlySet<ts.SyntaxKind> = new Set([
@@ -72,6 +80,9 @@ const BASE64_MAGIC = "AGFzbQ";
const BYTES_MAGIC = /\[\s*(?:0|0x0+)\s*,\s*(?:97|0x61)\s*,\s*(?:115|0x73)\s*,\s*(?:109|0x6d)\s*,/i;
/** Package directories deeper than this are not searched for modules. */
const PACKAGE_SCAN_DEPTH = 6;
/** A `.so` path in a string literal: the cheap test before a file with no
* `.wasm` mention is walked for shared-library paths. */
const SHARED_LIBRARY_PATH = /\.so["'`]/;
/** Larger files are reported by size but not read. */
const MAX_MODULE_BYTES = 256 * 1024 * 1024;
@@ -110,6 +121,7 @@ class Discovery {
};
this.byHash.set(sha256, report);
this.order.push(sha256);
this.input.onModule?.(sha256, bytes);
}
if (report.path === undefined && path !== undefined) report.path = path;
addSource(report, source);
@@ -132,6 +144,8 @@ class Discovery {
return;
}
if (bytes.length > MAX_MODULE_BYTES) return;
// A shared-library path names a module only when the file is one.
if (source.kind === "path" && !path.endsWith(".wasm") && !isWasmHeader(bytes)) return;
this.byPath.set(path, this.addBytes(bytes, path, source));
}
@@ -139,7 +153,7 @@ class Discovery {
scanFile(sf: ts.SourceFile): void {
const text = sf.text;
const paths = text.includes(".wasm");
const paths = text.includes(".wasm") || SHARED_LIBRARY_PATH.test(text);
const base64 = text.includes(BASE64_MAGIC);
const bytes = BYTES_MAGIC.test(text);
if (!paths && !base64 && !bytes) return;
@@ -177,7 +191,8 @@ class Discovery {
);
}
/** The module path a resolution expression names, when it ends in .wasm. */
/** The module path a resolution expression names, when it ends in .wasm
* (or .so). */
resolvedPath(sf: ts.SourceFile, node: ts.Node): string | null {
const dir = dirname(sf.fileName);
if (ts.isCallExpression(node)) {
@@ -238,7 +253,7 @@ class Discovery {
if (!ts.isStringLiteral(arg) && !ts.isNoSubstitutionTemplateLiteral(arg)) return null;
parts.push(arg.text);
}
if (!parts[parts.length - 1]!.endsWith(".wasm")) return null;
if (!isModulePath(parts[parts.length - 1]!)) return null;
let path = parts[0]!;
for (let i = 1; i < parts.length; i++) path = resolve(path, parts[i]!);
return path;
@@ -307,9 +322,15 @@ function addSource(report: WasmModuleReport, source: WasmSource): void {
report.sources.push(source);
}
/** A path that may name a module: `.wasm`, or `.so` for WebAssembly shared
* libraries (addFile keeps those only when the file is a module). */
function isModulePath(text: string): boolean {
return text.endsWith(".wasm") || text.endsWith(".so");
}
function wasmLiteral(node: ts.Expression): string | null {
if (!ts.isStringLiteral(node) && !ts.isNoSubstitutionTemplateLiteral(node)) return null;
return node.text.endsWith(".wasm") ? node.text : null;
return isModulePath(node.text) ? node.text : null;
}
function isDirname(node: ts.Expression): boolean {
@@ -351,40 +372,6 @@ function byteArrayLiteral(node: ts.ArrayLiteralExpression): Uint8Array | null {
return out;
}
const BASE64_VALUES = (() => {
const table = new Int16Array(128).fill(-1);
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
for (let i = 0; i < alphabet.length; i++) table[alphabet.charCodeAt(i)] = i;
// base64url spellings
table["-".charCodeAt(0)] = 62;
table["_".charCodeAt(0)] = 63;
return table;
})();
/** Decodes standard or URL-safe base64 (padding optional); null when the
* text is not base64. */
export function decodeBase64(text: string): Uint8Array | null {
let end = text.length;
while (end > 0 && text.charCodeAt(end - 1) === 61) end--;
if (text.length - end > 2 || end % 4 === 1) return null;
const out = new Uint8Array(Math.floor((end * 3) / 4));
let acc = 0;
let bits = 0;
let j = 0;
for (let i = 0; i < end; i++) {
const c = text.charCodeAt(i);
const v = c < 128 ? BASE64_VALUES[c]! : -1;
if (v < 0) return null;
acc = ((acc << 6) | v) & 0xffffff;
bits += 6;
if (bits >= 8) {
bits -= 8;
out[j++] = (acc >> bits) & 0xff;
}
}
return j === out.length ? out : out.subarray(0, j);
}
/** The root directory of the npm package that contains `file` (the
* directory below its innermost node_modules), or null outside one. */
export function packageRootOf(file: string): string | null {
+676
View File
@@ -0,0 +1,676 @@
/* The structure of a WebAssembly module, for the translator.
*
* binary.ts validates a module and keeps nothing but counts. This parser
* reads the parts translation needs (types, imports, functions, tables,
* memories, globals, exports, the start function, element and data
* segments, function bodies and custom sections) from bytes that binary.ts
* already accepted, so it checks nothing beyond what it must to stay in
* bounds. Constant expressions and function bodies are kept as byte ranges
* that the translator reads itself.
*
* i64 values never become JavaScript numbers: the translator reads their
* LEB128 encodings into two 32-bit halves. Plain TypeScript, so the native
* compiler compiles it too. */
export const VAL_I32 = 0x7f;
export const VAL_I64 = 0x7e;
export const VAL_F32 = 0x7d;
export const VAL_F64 = 0x7c;
export const VAL_FUNCREF = 0x70;
export const VAL_EXTERNREF = 0x6f;
export const KIND_FUNC = 0;
export const KIND_TABLE = 1;
export const KIND_MEMORY = 2;
export const KIND_GLOBAL = 3;
export interface WasmFuncType {
params: number[];
results: number[];
}
/** A byte range of the module: a constant expression (through its `end`
* opcode) or a payload. */
export interface WasmRange {
start: number;
end: number;
}
export interface WasmImport {
module: string;
name: string;
kind: number;
/** Functions: the type index. Tables and globals: the value type. */
type: number;
min: number;
max: number | null;
mutable: boolean;
}
export interface WasmTable {
elemType: number;
min: number;
max: number | null;
imported: boolean;
/** A defined table's initializer (reference-types table with an init
* expression), or null for the null reference. */
init: WasmRange | null;
}
export interface WasmMemory {
min: number;
max: number | null;
imported: boolean;
}
export interface WasmGlobal {
type: number;
mutable: boolean;
imported: boolean;
init: WasmRange | null;
}
export interface WasmExport {
name: string;
kind: number;
index: number;
}
export interface WasmElem {
mode: "active" | "passive" | "declarative";
table: number;
offset: WasmRange | null;
elemType: number;
/** One initializer per element: a function index (>= 0), or -1 when the
* element is a constant expression in `exprs`. */
funcs: number[];
exprs: (WasmRange | null)[];
}
export interface WasmData {
mode: "active" | "passive";
memory: number;
offset: WasmRange | null;
payload: WasmRange;
}
export interface WasmCode {
/** Declared locals, expanded (not including parameters). */
locals: number[];
body: WasmRange;
}
export interface WasmCustom {
name: string;
payload: WasmRange;
}
export interface WasmModule {
bytes: Uint8Array;
types: WasmFuncType[];
imports: WasmImport[];
/** The type index of every function, imported functions first. */
funcTypes: number[];
importedFuncs: number;
tables: WasmTable[];
memories: WasmMemory[];
globals: WasmGlobal[];
exports: WasmExport[];
start: number | null;
elems: WasmElem[];
datas: WasmData[];
codes: WasmCode[];
customs: WasmCustom[];
/** Functions referenced outside a body (exports, element segments,
* ref.func in constant expressions): only these need a function
* reference record. Bodies add their own ref.func targets, which a valid
* module also declares in one of these places. */
declaredRefs: Set<number>;
}
/** A byte cursor over a module that binary.ts validated. */
export class WasmCursor {
bytes: Uint8Array;
pos: number;
constructor(bytes: Uint8Array, pos: number) {
this.bytes = bytes;
this.pos = pos;
}
u8(): number {
const b = this.bytes[this.pos];
if (b === undefined) throw new Error("wasm: unexpected end of module");
this.pos++;
return b;
}
u32(): number {
let result = 0;
let scale = 1;
for (;;) {
const b = this.u8();
result += (b & 0x7f) * scale;
if ((b & 0x80) === 0) return result;
scale *= 128;
}
}
/** A signed LEB128 of at most 33 bits. */
s33(): number {
let result = 0;
let scale = 1;
for (;;) {
const b = this.u8();
result += (b & 0x7f) * scale;
scale *= 128;
if ((b & 0x80) === 0) {
if (b & 0x40) result -= scale;
return result;
}
}
}
/** A signed 32-bit LEB128 as a signed 32-bit number. */
s32(): number {
return this.s33() | 0;
}
/** A signed 64-bit LEB128 as its two's-complement halves [lo, hi], each
* an unsigned 32-bit number. */
s64(): [number, number] {
let lo = 0;
let hi = 0;
let shift = 0;
let b = 0;
do {
b = this.u8();
const bits = b & 0x7f;
if (shift < 32) {
lo = (lo | (bits << shift)) >>> 0;
if (shift > 25) hi = (hi | (bits >>> (32 - shift))) >>> 0;
} else if (shift < 64) {
hi = (hi | (bits << (shift - 32))) >>> 0;
}
shift += 7;
} while (b & 0x80);
if (shift < 64 && b & 0x40) {
// Sign-extend from bit `shift`.
if (shift < 32) {
lo = (lo | (~0 << shift)) >>> 0;
hi = 0xffffffff;
} else {
hi = (hi | (~0 << (shift - 32))) >>> 0;
}
}
return [lo, hi];
}
u32le(): number {
const b = this.bytes;
const p = this.pos;
this.pos += 4;
return (b[p]! | (b[p + 1]! << 8) | (b[p + 2]! << 16) | (b[p + 3]! << 24)) >>> 0;
}
name(): string {
const length = this.u32();
const start = this.pos;
this.pos += length;
return decodeUtf8(this.bytes, start, this.pos);
}
/** Skips a constant expression through its `end` and answers its range. */
constExpr(): WasmRange {
const start = this.pos;
for (;;) {
const op = this.u8();
if (op === 0x0b) break;
skipImmediates(this, op);
}
return { start, end: this.pos };
}
valType(): number {
return this.u8();
}
/** A block type: -1 for the empty type, a value type, or a type index. */
blockType():
| { kind: "empty" }
| { kind: "value"; type: number }
| { kind: "index"; index: number } {
const b = this.bytes[this.pos]!;
if (b === 0x40) {
this.pos++;
return { kind: "empty" };
}
if (
b === VAL_I32 ||
b === VAL_I64 ||
b === VAL_F32 ||
b === VAL_F64 ||
b === VAL_FUNCREF ||
b === VAL_EXTERNREF
) {
this.pos++;
return { kind: "value", type: b };
}
return { kind: "index", index: this.s33() };
}
}
function decodeUtf8(bytes: Uint8Array, start: number, end: number): string {
let out = "";
let i = start;
while (i < end) {
const b = bytes[i]!;
let cp = b;
let need = 0;
if (b >= 0xf0) {
cp = b & 0x07;
need = 3;
} else if (b >= 0xe0) {
cp = b & 0x0f;
need = 2;
} else if (b >= 0xc0) {
cp = b & 0x1f;
need = 1;
}
for (let k = 1; k <= need; k++) cp = cp * 64 + (bytes[i + k]! & 0x3f);
out += String.fromCodePoint(cp);
i += need + 1;
}
return out;
}
/** The flags of a memarg (the immediates of loads, stores and atomics):
* the alignment exponent and the memory index. Flags with bit 6 set
* (multi-memory) are followed by an explicit memory index, which may be
* 0; flags of 128 or more are malformed and answer align -1 without
* reading further. The offset follows. Validation (binary.ts), translation
* and skipImmediates all read memargs through this function, so they agree
* on where each instruction ends; `u32` reads the next u32 immediate of
* their own reader. */
export function readMemargFlags(u32: () => number): { align: number; memory: number } {
const flags = u32();
if (flags >= 128) return { align: -1, memory: 0 };
if (flags >= 64) return { align: flags - 64, memory: u32() };
return { align: flags, memory: 0 };
}
/** Reads the immediates of opcode `op` (already consumed). Covers the
* translatable feature level; anything else was refused before
* translation. */
export function skipImmediates(c: WasmCursor, op: number): void {
switch (op) {
case 0x02:
case 0x03:
case 0x04:
c.blockType();
return;
case 0x0c:
case 0x0d:
case 0x10:
case 0x20:
case 0x21:
case 0x22:
case 0x23:
case 0x24:
case 0x25:
case 0x26:
case 0xd2:
c.u32();
return;
case 0x0e: {
const n = c.u32();
for (let i = 0; i <= n; i++) c.u32();
return;
}
case 0x11:
c.u32();
c.u32();
return;
case 0x1c: {
const n = c.u32();
for (let i = 0; i < n; i++) c.valType();
return;
}
case 0x3f:
case 0x40:
// The memory index (a LEB128 zero; V8 accepts long encodings).
c.u32();
return;
case 0x41:
c.s32();
return;
case 0x42:
c.s64();
return;
case 0x43:
c.pos += 4;
return;
case 0x44:
c.pos += 8;
return;
case 0xd0:
c.valType();
return;
case 0xfc: {
const sub = c.u32();
switch (sub) {
case 8:
c.u32();
c.u32();
return;
case 9:
case 13:
case 15:
case 16:
case 17:
c.u32();
return;
case 10:
c.u32();
c.u32();
return;
case 11:
c.u32();
return;
case 12:
case 14:
c.u32();
c.u32();
return;
default:
return;
}
}
default:
if (op >= 0x28 && op <= 0x3e) {
const flags = readMemargFlags(() => c.u32());
if (flags.align < 0 || flags.memory !== 0)
throw new Error(`wasm: memarg of memory ${flags.memory} is not supported`);
c.u32();
}
return;
}
}
function limits(c: WasmCursor): { min: number; max: number | null } {
const flags = c.u8();
const min = c.u32();
const max = flags & 0x01 ? c.u32() : null;
return { min, max };
}
/** Parses a module binary.ts reported as valid within the translatable
* feature level. */
export function parseWasmModule(bytes: Uint8Array): WasmModule {
const m: WasmModule = {
bytes,
types: [],
imports: [],
funcTypes: [],
importedFuncs: 0,
tables: [],
memories: [],
globals: [],
exports: [],
start: null,
elems: [],
datas: [],
codes: [],
customs: [],
declaredRefs: new Set<number>(),
};
const c = new WasmCursor(bytes, 8);
while (c.pos < bytes.length) {
const id = c.u8();
const size = c.u32();
const end = c.pos + size;
switch (id) {
case 0: {
const name = c.name();
m.customs.push({ name, payload: { start: c.pos, end } });
break;
}
case 1: {
const n = c.u32();
for (let i = 0; i < n; i++) {
c.u8(); // 0x60
const params: number[] = [];
const np = c.u32();
for (let k = 0; k < np; k++) params.push(c.valType());
const results: number[] = [];
const nr = c.u32();
for (let k = 0; k < nr; k++) results.push(c.valType());
m.types.push({ params, results });
}
break;
}
case 2: {
const n = c.u32();
for (let i = 0; i < n; i++) {
const module = c.name();
const name = c.name();
const kind = c.u8();
const imp: WasmImport = {
module,
name,
kind,
type: 0,
min: 0,
max: null,
mutable: false,
};
if (kind === KIND_FUNC) {
imp.type = c.u32();
m.funcTypes.push(imp.type);
m.importedFuncs++;
} else if (kind === KIND_TABLE) {
imp.type = c.valType();
const l = limits(c);
imp.min = l.min;
imp.max = l.max;
m.tables.push({
elemType: imp.type,
min: l.min,
max: l.max,
imported: true,
init: null,
});
} else if (kind === KIND_MEMORY) {
const l = limits(c);
imp.min = l.min;
imp.max = l.max;
m.memories.push({ min: l.min, max: l.max, imported: true });
} else {
imp.type = c.valType();
imp.mutable = c.u8() === 1;
m.globals.push({ type: imp.type, mutable: imp.mutable, imported: true, init: null });
}
m.imports.push(imp);
}
break;
}
case 3: {
const n = c.u32();
for (let i = 0; i < n; i++) m.funcTypes.push(c.u32());
break;
}
case 4: {
const n = c.u32();
for (let i = 0; i < n; i++) {
let init: WasmRange | null = null;
if (c.bytes[c.pos] === 0x40) {
// table with an initializer: 0x40 0x00 tabletype expr
c.pos += 2;
const elemType = c.valType();
const l = limits(c);
init = c.constExpr();
m.tables.push({ elemType, min: l.min, max: l.max, imported: false, init });
continue;
}
const elemType = c.valType();
const l = limits(c);
m.tables.push({ elemType, min: l.min, max: l.max, imported: false, init });
}
break;
}
case 5: {
const n = c.u32();
for (let i = 0; i < n; i++) {
const l = limits(c);
m.memories.push({ min: l.min, max: l.max, imported: false });
}
break;
}
case 6: {
const n = c.u32();
for (let i = 0; i < n; i++) {
const type = c.valType();
const mutable = c.u8() === 1;
const init = c.constExpr();
noteConstRefs(m, init);
m.globals.push({ type, mutable, imported: false, init });
}
break;
}
case 7: {
const n = c.u32();
for (let i = 0; i < n; i++) {
const name = c.name();
const kind = c.u8();
const index = c.u32();
m.exports.push({ name, kind, index });
if (kind === KIND_FUNC) m.declaredRefs.add(index);
}
break;
}
case 8:
m.start = c.u32();
break;
case 9: {
const n = c.u32();
for (let i = 0; i < n; i++) m.elems.push(elemSegment(m, c));
break;
}
case 10: {
const n = c.u32();
for (let i = 0; i < n; i++) {
const size = c.u32();
const bodyEnd = c.pos + size;
const locals: number[] = [];
const groups = c.u32();
for (let g = 0; g < groups; g++) {
const count = c.u32();
const type = c.valType();
for (let k = 0; k < count; k++) locals.push(type);
}
m.codes.push({ locals, body: { start: c.pos, end: bodyEnd } });
c.pos = bodyEnd;
}
break;
}
case 11: {
const n = c.u32();
for (let i = 0; i < n; i++) {
const flags = c.u32();
if (flags === 1) {
const size = c.u32();
m.datas.push({
mode: "passive",
memory: 0,
offset: null,
payload: { start: c.pos, end: c.pos + size },
});
c.pos += size;
continue;
}
const memory = flags === 2 ? c.u32() : 0;
const offset = c.constExpr();
const size = c.u32();
m.datas.push({
mode: "active",
memory,
offset,
payload: { start: c.pos, end: c.pos + size },
});
c.pos += size;
}
break;
}
default:
// 12 (data count) and anything binary.ts accepted without content
// translation needs.
break;
}
c.pos = end;
}
return m;
}
function elemSegment(m: WasmModule, c: WasmCursor): WasmElem {
const flags = c.u32();
const passiveOrDeclarative = (flags & 1) !== 0;
const explicitTable = (flags & 2) !== 0;
const usesExprs = (flags & 4) !== 0;
let mode: WasmElem["mode"] = "active";
if (passiveOrDeclarative) mode = explicitTable ? "declarative" : "passive";
let table = 0;
let offset: WasmRange | null = null;
if (!passiveOrDeclarative) {
if (explicitTable) table = c.u32();
offset = c.constExpr();
}
let elemType = VAL_FUNCREF;
if (passiveOrDeclarative || explicitTable) {
// elemkind (0x00 = funcref) or a reference type
const b = c.u8();
elemType = usesExprs ? b : VAL_FUNCREF;
}
const n = c.u32();
const funcs: number[] = [];
const exprs: (WasmRange | null)[] = [];
for (let i = 0; i < n; i++) {
if (usesExprs) {
const expr = c.constExpr();
const direct = refFuncOf(m.bytes, expr);
if (direct !== null) {
funcs.push(direct);
exprs.push(null);
m.declaredRefs.add(direct);
} else {
noteConstRefs(m, expr);
funcs.push(-1);
exprs.push(expr);
}
} else {
const f = c.u32();
funcs.push(f);
exprs.push(null);
m.declaredRefs.add(f);
}
}
return { mode, table, offset, elemType, funcs, exprs };
}
/** The function of a `ref.func f; end` expression, or null. */
function refFuncOf(bytes: Uint8Array, expr: WasmRange): number | null {
if (bytes[expr.start] !== 0xd2) return null;
const c = new WasmCursor(bytes, expr.start + 1);
const f = c.u32();
return c.pos === expr.end - 1 && bytes[c.pos] === 0x0b ? f : null;
}
function noteConstRefs(m: WasmModule, expr: WasmRange): void {
const c = new WasmCursor(m.bytes, expr.start);
while (c.pos < expr.end) {
const op = c.u8();
if (op === 0xd2) {
m.declaredRefs.add(c.u32());
continue;
}
skipImmediates(c, op);
}
}
@@ -0,0 +1,247 @@
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, test } from "vitest";
import { analyzeWasm } from "./binary.js";
import { decodeBase64, encodeBase64 } from "./base64.js";
import { WasmCursor, parseWasmModule } from "./module.js";
import {
signatureHash,
stackCheckedFunctions,
translateWasmModule,
wasmSymbolPrefix,
} from "./translate.js";
const SHA = new Uint8Array(32).fill(0xab);
/** (module (func (export "add") (param i32 i32) (result i32) local.get 0 local.get 1 i32.add)
* (memory (export "mem") 1) (data (i32.const 8) "hi")) */
const ADD = new Uint8Array([
0, 97, 115, 109, 1, 0, 0, 0, 1, 7, 1, 96, 2, 127, 127, 1, 127, 3, 2, 1, 0, 5, 3, 1, 0, 1, 7, 13,
2, 3, 97, 100, 100, 0, 0, 3, 109, 101, 109, 2, 0, 10, 9, 1, 7, 0, 32, 0, 32, 1, 106, 11, 11, 8, 1,
0, 65, 8, 11, 2, 104, 105,
]);
/** Every top-level line must be one the native helper's text partitioner
* accepts (native/llvm-codegen/src/partition.cpp), so translated modules
* keep the program on its concurrent parsing path. */
function topLevelLines(text: string[]): string[] {
const lines: string[] = [];
for (const part of text) {
let inFunction = false;
for (const line of part.split("\n")) {
if (inFunction) {
if (line === "}") inFunction = false;
continue;
}
if (line.startsWith("define ")) inFunction = true;
lines.push(line);
}
}
return lines;
}
describe("translateWasmModule", () => {
test("emits functions, thunks, data and the descriptor in the helper's grammar", () => {
const out = translateWasmModule(ADD, SHA, { boundsChecks: false });
const prefix = wasmSymbolPrefix(SHA);
expect(prefix).toBe("scr_wasm_abababababababab");
expect(out.descriptor).toBe(`@${prefix}_desc`);
const text = out.text.join("\n");
expect(text).toContain(`define internal i32 @${prefix}_f0(ptr %inst, i32 %p0, i32 %p1)`);
expect(text).toContain(`define internal void @${prefix}_x0(ptr %inst, ptr %a, ptr %r)`);
expect(text).toContain(`c"hi"`);
expect(text).toContain(`"disable-tail-calls"="true"`);
for (const line of topLevelLines(out.text)) {
expect(
line === "" || /^(define internal |@[\w.$-]+ = (private|internal) )/.test(line),
line,
).toBe(true);
}
for (const decl of out.decls) expect(decl).toMatch(/^declare /);
});
test("keeps custom sections only when asked, and gives unexported table functions call thunks", () => {
// (module (type $t (func (param i32) (result i32))) (table 2 funcref)
// (elem (i32.const 0) $hidden $shown) (func $hidden (type $t) local.get 0)
// (func $shown (export "shown") (type $t) local.get 0)), plus a custom
// section "meta" with "hi".
const refs = new Uint8Array([
0, 97, 115, 109, 1, 0, 0, 0, 1, 6, 1, 96, 1, 127, 1, 127, 3, 3, 2, 0, 0, 4, 4, 1, 112, 0, 2,
7, 9, 1, 5, 115, 104, 111, 119, 110, 0, 1, 9, 8, 1, 0, 65, 0, 11, 2, 0, 1, 10, 11, 2, 4, 0,
32, 0, 11, 4, 0, 32, 0, 11, 0, 7, 4, 109, 101, 116, 97, 104, 105,
]);
const prefix = wasmSymbolPrefix(SHA);
const plain = translateWasmModule(refs, SHA, { boundsChecks: false }).text.join("\n");
expect(plain).toContain(
`@${prefix}_customs = private constant [0 x { ptr, i64, ptr, i64 }] []`,
);
expect(plain).not.toContain(`c"hi"`);
const kept = translateWasmModule(refs, SHA, { boundsChecks: false, customSections: true });
const text = kept.text.join("\n");
expect(text).toContain(`@${prefix}_c0 = private unnamed_addr constant [2 x i8] c"hi", align 1`);
expect(text).toContain(`@${prefix}_customs = private constant [1 x { ptr, i64, ptr, i64 }]`);
// One call thunk for the signature, used by the unexported function
// only (the exported one has its export thunk).
expect(text).toContain(
`define internal void @${prefix}_k0(ptr %code, ptr %inst, ptr %a, ptr %r)`,
);
expect(text).toContain(`%res = call i32 %code(ptr %inst, i32 `);
expect(text).toContain(
`@${prefix}_calls = private constant [2 x ptr] [ptr @${prefix}_k0, ptr null]`,
);
for (const line of topLevelLines(kept.text)) {
expect(
line === "" || /^(define internal |@[\w.$-]+ = (private|internal) )/.test(line),
line,
).toBe(true);
}
});
test("checks memory accesses explicitly only when asked", () => {
const guarded = translateWasmModule(ADD, SHA, { boundsChecks: false }).text.join("\n");
const checked = translateWasmModule(ADD, SHA, { boundsChecks: true }).text.join("\n");
expect(guarded).not.toContain("%msize");
expect(checked).toContain("%msize");
});
test("reads a memarg with an explicit memory index as one immediate", () => {
// (func (result i32) (i32.load 0 offset=8 (i32.const 0))) with memory 1:
// memarg flags 0x42 (alignment 2, memory index present), memory 0,
// offset 8, then `end`.
const bytes = new Uint8Array([
0, 97, 115, 109, 1, 0, 0, 0, 1, 5, 1, 96, 0, 1, 127, 3, 2, 1, 0, 5, 3, 1, 0, 1, 10, 10, 1, 8,
0, 65, 0, 40, 66, 0, 8, 11,
]);
expect(analyzeWasm(bytes).status).toBe("valid");
const text = translateWasmModule(bytes, SHA, { boundsChecks: false }).text.join("\n");
expect(text).toContain("add i64 %v1, 8");
expect(text).toContain("load i32, ptr %v3");
});
test("translates every valid module of the WebAssembly spec test suite", () => {
const cases = readFileSync(join(import.meta.dirname, "../../test/wasm-spec-cases.txt"), "utf8")
.split("\n")
.filter((line) => line !== "" && !line.startsWith("#"));
let translated = 0;
for (const line of cases) {
const [, , where, b64] = line.split(" ");
const bytes = decodeBase64(b64!)!;
if (analyzeWasm(bytes).status !== "valid") continue;
try {
translateWasmModule(bytes, SHA, { boundsChecks: false });
} catch (error) {
throw new Error(`${where}: ${error instanceof Error ? error.message : String(error)}`);
}
translated++;
}
expect(translated).toBeGreaterThan(800);
});
});
/** A module from its sections: `[id, ...payload]`, each payload under 128
* bytes. */
function wasmModule(...sections: number[][]): Uint8Array {
const out = [0, 97, 115, 109, 1, 0, 0, 0];
for (const [id, ...payload] of sections) out.push(id!, payload.length, ...payload);
return new Uint8Array(out);
}
/** (module (memory 1) (func (export "f") (param i32) (result i32)
* local.get 0 i32.load <memarg>)) with the memarg's bytes as given. */
function loadModule(memarg: number[]): Uint8Array {
const body = [0, 0x20, 0, 0x28, ...memarg, 0x0b];
return wasmModule(
[1, 1, 0x60, 1, 0x7f, 1, 0x7f],
[3, 1, 0],
[5, 1, 0, 1],
[7, 1, 1, 0x66, 0, 0],
[10, 1, body.length, ...body],
);
}
describe("memargs and operand types", () => {
test("a memarg naming memory 0 explicitly translates as the plain form", () => {
// Flags 0x42: alignment 2 with bit 6 set, then memory index 0 and
// offset 4. Validation and translation read the same three immediates.
// Only the descriptor (which records the module's byte length) differs.
const code = (memarg: number[]): string[] =>
translateWasmModule(loadModule(memarg), SHA, { boundsChecks: true })
.text.join("\n")
.split("\n")
.filter((line) => !line.startsWith(`@${wasmSymbolPrefix(SHA)}_desc =`));
expect(code([0x42, 0, 4])).toEqual(code([0x02, 4]));
});
test("a memarg naming another memory is refused", () => {
expect(() =>
translateWasmModule(loadModule([0x42, 1, 4]), SHA, { boundsChecks: true }),
).toThrow(/memory 1/);
});
test("local.tee of an i32 into an externref local is refused", () => {
// (func (local externref) i32.const 1 local.tee 0 drop): the operand
// never reaches a reference slot.
const body = [1, 1, 0x6f, 0x41, 1, 0x22, 0, 0x1a, 0x0b];
const bytes = wasmModule([1, 1, 0x60, 0, 0], [3, 1, 0], [10, 1, body.length, ...body]);
expect(() => translateWasmModule(bytes, SHA, { boundsChecks: false })).toThrow(
/local\.tee operand type does not match/,
);
});
});
describe("module parsing", () => {
test("reads the parts translation needs", () => {
const m = parseWasmModule(ADD);
expect(m.types).toEqual([{ params: [0x7f, 0x7f], results: [0x7f] }]);
expect(m.exports.map((e) => e.name)).toEqual(["add", "mem"]);
expect(m.memories).toEqual([{ min: 1, max: null, imported: false }]);
expect(m.datas[0]!.mode).toBe("active");
expect([...m.declaredRefs]).toEqual([0]);
});
test("decodes signed 64-bit LEB128 into two's-complement halves", () => {
const halves = (bytes: number[]): [number, number] =>
new WasmCursor(new Uint8Array(bytes), 0).s64();
expect(halves([0x7f])).toEqual([0xffffffff, 0xffffffff]);
expect(halves([0x80, 0x80, 0x80, 0x80, 0x10])).toEqual([0, 1]);
expect(halves([0xff, 0xff, 0xff, 0xff, 0x0f])).toEqual([0xffffffff, 0]);
expect(halves([0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x7f])).toEqual([
0, 0x80000000,
]);
expect(halves([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00])).toEqual([
0xffffffff, 0x7fffffff,
]);
});
});
describe("stack checks", () => {
test("only functions through which the stack can grow without bound check it", () => {
// f0 <-> f1 recurse mutually, f2 recurses on itself, f3 calls f0 once.
const calls = new Uint8Array([
0, 97, 115, 109, 1, 0, 0, 0, 1, 4, 1, 96, 0, 0, 3, 5, 4, 0, 0, 0, 0, 10, 21, 4, 4, 0, 16, 1,
11, 4, 0, 16, 0, 11, 4, 0, 16, 2, 11, 4, 0, 16, 0, 11,
]);
expect([...stackCheckedFunctions(parseWasmModule(calls))].sort()).toEqual([0, 1, 2]);
expect(stackCheckedFunctions(parseWasmModule(ADD)).size).toBe(0);
// Only code without the fault handler checks explicitly.
const checked = translateWasmModule(calls, SHA, { boundsChecks: true }).text.join("\n");
expect(checked.split("@scr_wasm_stack_limit").length - 1).toBe(3);
const guarded = translateWasmModule(calls, SHA, { boundsChecks: false }).text.join("\n");
expect(guarded).not.toContain("@scr_wasm_stack_limit");
});
});
describe("helpers", () => {
test("equal signatures share their identity across modules", () => {
expect(signatureHash("ii:i")).toBe(signatureHash("ii:i"));
expect(signatureHash("ii:i")).not.toBe(signatureHash("i:i"));
expect(signatureHash(":")).toMatch(/^u0x[0-9a-f]{16}$/);
});
test("base64 round-trips module bytes", () => {
expect(encodeBase64(ADD)).toBe(Buffer.from(ADD).toString("base64"));
expect(decodeBase64(encodeBase64(ADD))).toEqual(ADD);
expect(encodeBase64(new Uint8Array([1]))).toBe("AQ==");
expect(encodeBase64(new Uint8Array([1, 2]))).toBe("AQI=");
});
});
File diff suppressed because it is too large Load Diff
+16 -3
View File
@@ -1867,6 +1867,13 @@
"status": "static",
"note": "recognized module (bare and node:-prefixed specifiers)"
},
{
"id": "node-builtin.perf_hooks.monitorEventLoopDelay",
"kind": "node-builtin",
"name": "perf_hooks.monitorEventLoopDelay",
"status": "static",
"note": "an unref'd sampling interval records the delay histogram; enable, disable, reset, percentile and the min, max, mean, stddev, count and exceeds readings are supported"
},
{
"id": "node-builtin.perf_hooks.performance.clearMarks",
"kind": "node-builtin",
@@ -1879,6 +1886,13 @@
"name": "perf_hooks.performance.clearMeasures",
"status": "static"
},
{
"id": "node-builtin.perf_hooks.performance.eventLoopUtilization",
"kind": "node-builtin",
"name": "perf_hooks.performance.eventLoopUtilization",
"status": "static",
"note": "active time is the main thread's CPU time since the loop started; the one- and two-argument difference forms are supported"
},
{
"id": "node-builtin.perf_hooks.performance.getEntries",
"kind": "node-builtin",
@@ -5699,9 +5713,8 @@
"id": "stdlib.webassembly",
"kind": "stdlib",
"name": "globalThis.WebAssembly",
"status": "unsupported",
"code": "SC2020",
"note": "WebAssembly support is planned as translation of each module whose bytes the build can see (an inline literal, or a file read from a path the compiler resolves) into native code; 'scriptc coverage' lists the modules this program would load (https://scriptc.dev/limitations)"
"status": "static",
"note": "compile, instantiate, validate, 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 SC2020 (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))"
},
{
"id": "stdlib.webcrypto.CryptoKey",
@@ -102,19 +102,19 @@ test.each(["llvm"] as const)(
writeFileSync(
join(pkg, "index.js"),
`
const inspect = (value) => eval(value);
const inspect = (value) => process.on("__proto__", value);
function stopped(value) {
eval(value);
process.on("__proto__", value);
return { done() { return "wrong"; } };
}
try { console.log(stopped("0").done()); }
catch (error) { console.log(String(error).includes("SC2009")); }
try { console.log(stopped(() => {}).done()); }
catch (error) { console.log(String(error).includes("SC2020")); }
const first = { inspect };
const second = { inspect };
console.log(typeof first.inspect, first.inspect === second.inspect);
for (const callback of [first.inspect, second.inspect]) {
try { callback({}); }
catch (error) { console.log(String(error).includes("SC2009")); }
try { callback(() => {}); }
catch (error) { console.log(String(error).includes("SC2020")); }
}
`,
);

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