diff --git a/docs/content/docs/cli.mdx b/docs/content/docs/cli.mdx
index 8be1a2642..786cf2c63 100644
--- a/docs/content/docs/cli.mdx
+++ b/docs/content/docs/cli.mdx
@@ -134,7 +134,7 @@ Verifies and loads the installed precompiled runtime objects and vendored librar
Coverage only. After the report, list the functions that already operate on values typed only at runtime and the bindings, fields, functions, classes and modules that unsupported constructs would move to the dynamic tier, each with the diagnostic behind it. With a name such as x, Point, or Point.x, show only the matching units. See Dynamic tier.
--wasm <file.wasm>
- Coverage only. Report a WebAssembly module the program loads from a path its sources do not spell out. The report already lists modules whose paths the compiler resolves, modules inlined as base64 literals, and .wasm files shipped by packages that use WebAssembly; a scriptc.wasm array in package.json adds files the same way as this flag. Each module is listed with its size, whether it validates, and the WebAssembly proposals it uses. scriptc does not compile WebAssembly modules to native code yet. Repeatable.
+ Coverage only. Report a WebAssembly module the program loads from a path its sources do not spell out. The report already lists modules whose paths the compiler resolves, modules inlined as base64 literals, and .wasm files shipped by packages that use WebAssembly; a scriptc.wasm array in package.json adds files the same way as this flag. Each module is listed with its size, whether it validates, and the WebAssembly proposals it uses. Builds translate the modules they find to native code; a build reads the scriptc.wasm array but not this flag. Repeatable.
--sanitize
Build an executable with AddressSanitizer and a reference-count audit at exit, the same checks scriptc's own test suite uses. Requires a C compiler and is available only for host builds. Not supported with --emit=asm or --emit=obj.
diff --git a/docs/content/limitations/builtin-apis/performance-entries.mdx b/docs/content/limitations/builtin-apis/performance-entries.mdx
index 92af98cb7..a42940b68 100644
--- a/docs/content/limitations/builtin-apis/performance-entries.mdx
+++ b/docs/content/limitations/builtin-apis/performance-entries.mdx
@@ -1,2 +1,4 @@
- **Performance entries:** `performance.mark()`, `measure()`, and the `getEntries` methods return entries with Node's fields, `toJSON()`, inspection, and identity.
`Object.keys`, `for...in`, object spread, and `hasOwnProperty` on an entry are unsupported, and an entry stored in an `unknown` slot prints as a plain object.
+- **Event-loop metrics:** `performance.eventLoopUtilization()` counts the main thread's CPU time since the event loop started as active time, so blocking system calls count as idle where Node counts them as active.
+ `monitorEventLoopDelay()` supports `enable()`, `disable()`, `reset()`, `percentile()`, and the `min`, `max`, `mean`, `stddev`, `count`, and `exceeds` readings; the BigInt readings, `percentiles`, and non-literal option objects are unsupported.
diff --git a/docs/content/limitations/builtin-apis/streams-and-readline.mdx b/docs/content/limitations/builtin-apis/streams-and-readline.mdx
index beda29c7d..73b48c88d 100644
--- a/docs/content/limitations/builtin-apis/streams-and-readline.mdx
+++ b/docs/content/limitations/builtin-apis/streams-and-readline.mdx
@@ -1,4 +1,7 @@
- **Streams and readline:** Stream chunks are Buffers or strings.
+ `NodeJS.ReadableStream` and `NodeJS.WritableStream`, and `stream.Readable` and `stream.Writable` under `@types/node`, accept every stream: when a program passes file, zlib, or `node:stream` streams through them, child-process pipes and `process.stdout` and `process.stderr` join those streams as `Readable` and `Writable` instances.
+ `readable.pipe(process.stdout)` and `readable.pipe(process.stderr)` compile and, as in Node, do not end the process stream.
+ A `pipeline` stage that arrives as an untyped value must be a native stream at run time; other values throw a not-supported error.
`Readable.from` accepts arrays, Sets, Map and Set iterators, generators, async generators, and class iterables and async iterables whose values are strings or Buffers, and in JavaScript any iterable or async iterable of them; a stream that mixes strings and Buffers is unsupported.
In JavaScript, a sync iterator value with a `then` method that is not a promise, and an object whose `[Symbol.asyncIterator]` or `[Symbol.iterator]` method is a generator method, are unsupported.
Object-mode streams of other values (including `Readable.from` over a `Map`, whose values are entry arrays) are unsupported.
@@ -11,6 +14,7 @@
`stream.compose` accepts two or more streams, or one `Duplex`; functions, iterables, and web streams as stages are unsupported, and the `ERR_INVALID_ARG_VALUE` message for a stream that is no longer readable or writable names the stream's class instead of printing the stream.
`pipeline` from `node:stream/promises` accepts a trailing `{ signal, end }` object, and `finished` accepts `readable`, `writable`, `error`, `cleanup`, and `signal` options, with boolean literals for the flags.
Stream constructors accept a `signal` option in an options object literal, and `stream.addAbortSignal` accepts any stream.
+ A `transform` or `flush` option callback whose completion callback types `data` as `any`, as @types/node's `TransformCallback` does, may pass a Buffer, a string, or nothing; any other value throws a TypeError where the callback is called, instead of Node's `ERR_INVALID_ARG_TYPE` stream error.
`node:readline` and `node:readline/promises` (also reached as `readline.promises`) interfaces, from `createInterface(options)` or `createInterface(input, output, completer, terminal)`, read `process.stdin` or a readable stream: `question`, `prompt`, `write` (with a key object literal of `name`, `ctrl`, `meta`, and `shift`), `pause`, `resume`, `'line'`/`'close'`/`'pause'`/`'resume'`/`'error'`/`'history'`/`'SIGINT'`/`'SIGTSTP'`/`'SIGCONT'` listeners, and `for await` compile.
The `history`, `historySize`, `removeHistoryDuplicates`, `completer`, `tabSize`, and `escapeCodeTimeout` options are validated as in Node.
With `terminal: true`, or with no `terminal` option and `process.stdout` as the output when it is a TTY, the interface runs Node's terminal mode: the editing keys, the kill ring, undo and redo, history recording and recall, Tab completion through the completer, Ctrl+C, Ctrl+D, and Ctrl+Z, `TERM=dumb`, and the same escape sequences written to the output.
diff --git a/docs/content/limitations/builtin-function-values.mdx b/docs/content/limitations/builtin-function-values.mdx
index 80d0814b1..541c9fb2b 100644
--- a/docs/content/limitations/builtin-function-values.mdx
+++ b/docs/content/limitations/builtin-function-values.mdx
@@ -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.
diff --git a/docs/content/limitations/child-process-workers.mdx b/docs/content/limitations/child-process-workers.mdx
index 5b41c5e53..9d6602509 100644
--- a/docs/content/limitations/child-process-workers.mdx
+++ b/docs/content/limitations/child-process-workers.mdx
@@ -12,3 +12,7 @@ Message listeners may return `void` or `Promise`.
Messages use newline-delimited JSON, so their static types must be JSON-stringifiable or `unknown` backed by a checked JSON tree.
`send()` returns the channel's backpressure state and invokes its callback later with `null` or an `Error`.
`disconnect()` changes `connected` synchronously, before the later disconnect events.
+
+A `'data'` listener on a child's `stdout` or `stderr` whose chunk is `unknown` or `any` receives strings when a `setEncoding()` statement on the same stream precedes it in its block or an enclosing block, and Buffers when the function never calls `setEncoding()` on that stream.
+Any other placement of `setEncoding()` is refused at compile time, and a `setEncoding()` call that runs elsewhere afterwards makes the listener throw a TypeError instead of receiving strings.
+An inline `'exit'` or `'close'` listener may declare `(code: number, signal: string)`, which TypeScript accepts through the catch-all `on` overload; it receives the `null` values Node passes, and a typed use of a `null` value throws a TypeError.
diff --git a/docs/content/limitations/console-and-process-streams.mdx b/docs/content/limitations/console-and-process-streams.mdx
index 0d7cd8f30..c62b82f0c 100644
--- a/docs/content/limitations/console-and-process-streams.mdx
+++ b/docs/content/limitations/console-and-process-streams.mdx
@@ -9,4 +9,5 @@ JavaScript can store `process.stdin`, `stdout`, and `stderr`, replace output `wr
Input supports data/end/error listeners, pause/resume, buffered reads, and TTY raw mode.
Output writes are synchronous, with successful callbacks on the next-tick queue; queued backpressure is not modeled.
Stored streams do not support piping, encoding changes, arbitrary properties, or other events.
-TypeScript output parameters support `write(string)`.
+A TypeScript `NodeJS.WritableStream` parameter that only receives `process.stdout` or `process.stderr` supports `write(string)`.
+When other streams flow into the same parameter, the process streams join them as `Writable` instances with the full writable surface.
diff --git a/docs/content/limitations/language-features/generics.mdx b/docs/content/limitations/language-features/generics.mdx
index 6e203506f..a234c8f57 100644
--- a/docs/content/limitations/language-features/generics.mdx
+++ b/docs/content/limitations/language-features/generics.mdx
@@ -3,3 +3,9 @@
Independent concrete uses have no specialization-count limit; recursively expanding demands and type layouts produce diagnostics.
Other generic calls compile when the target resolves statically.
Generic function values require a concrete signature at use and a binding that is never reassigned.
+- **Variadic wrappers:** A generic higher-order wrapper is specialized to each wrapped function's exact signature.
+ `Parameters`, `ReturnType`, and rest parameters typed as fixed tuples become that signature's parameter list.
+ A function expression with no parameters that reads `arguments` compiles only when its value is asserted to, or contextually typed as, one fixed-arity signature; `arguments` is then that signature's tuple.
+ Forwarding it with `fn.apply(this, arguments)` passes `this` through.
+ Reading `fn.name` on a wrapped function requires its name to be known where the wrapper is called, such as a function declaration or a `const`-bound function.
+ `Parameters` over a signature with optional or array-typed rest parameters is unsupported, because that tuple has no fixed length.
diff --git a/docs/content/limitations/webassembly-modules.mdx b/docs/content/limitations/webassembly-modules.mdx
index e3bf6e500..43fa55e48 100644
--- a/docs/content/limitations/webassembly-modules.mdx
+++ b/docs/content/limitations/webassembly-modules.mdx
@@ -1,7 +1,31 @@
-Compiled programs can't load WebAssembly modules yet.
-Code that only mentions the `WebAssembly` types type-checks, including packages whose declarations name them, and `typeof WebAssembly` answers as it does in Node.js.
-Every use of the API itself is rejected: `WebAssembly.compile`, `instantiate` and `validate`, `new WebAssembly.Module` and the other constructors, `instanceof` checks, and reads of `globalThis.WebAssembly`.
-TypeScript sources fail to build with SC2020 at each use, and JavaScript sources throw that diagnostic when the statement runs.
+scriptc translates WebAssembly modules to native code when it builds the program; there is no interpreter or JIT in the executable.
+A build translates every module whose bytes it can see: files at paths the compiler resolves (`require.resolve`, `createRequire(...).resolve`, `import.meta.resolve`, `new URL("./x.wasm", import.meta.url)`, `path.join(__dirname, ...)`), including `.so` WebAssembly shared libraries such as quickjs-wasi's extensions, modules inlined as base64 or byte-array literals, `.wasm` files shipped by packages whose code uses WebAssembly, and files listed in a `scriptc.wasm` array in `package.json`.
+At run time `WebAssembly.compile`, `instantiate`, `validate` and `new WebAssembly.Module` find the translated module by the SHA-256 of the bytes they are given.
+Bytes the build did not see, including bytes produced at run time, reject with a `CompileError` that says the module was not translated, and `WebAssembly.validate` answers `false` for them.
+`scriptc coverage` lists the modules a build finds, with their size, whether they validate, and the WebAssembly proposals they use.
-`scriptc coverage` lists the modules a program would load, with their size, whether they validate, and the WebAssembly proposals they use.
-It finds modules at paths the compiler resolves, modules inlined as base64 literals, `.wasm` files shipped by packages that use WebAssembly, and files named with [`--wasm`](/docs/cli) or a `scriptc.wasm` array in `package.json`.
+Translated modules can use the WebAssembly 2.0 feature set: the MVP plus sign-extension operators, non-trapping float-to-int conversions, bulk memory, multi-value, mutable globals, reference types, and extended constant expressions.
+Modules that use SIMD, threads, exception handling, tail calls, typed function references or GC, memory64, or multiple memories are not translated, so loading them rejects with the `CompileError` above; packages that fall back to a scalar build when that happens, such as undici and es-module-lexer, take their fallback.
+
+Supported: `WebAssembly.compile`, `instantiate` (with bytes or a `Module`), `validate`, `new WebAssembly.Module`, `new WebAssembly.Instance`, `new WebAssembly.Memory`, `new WebAssembly.Table`, `new WebAssembly.Global`, `WebAssembly.Module.imports`, `exports` and `customSections`, and `instanceof` for every class; the error classes `CompileError`, `LinkError` and `RuntimeError`, constructed with or without `new`; `globalThis.WebAssembly` and its `compile`, `instantiate` and `validate` functions as values.
+Instances expose exported functions, memories (`buffer`, `grow`), tables (`get`, `set`, `grow`, `length`) and globals (`value`, `valueOf`).
+Imports can be functions, numbers or BigInts for immutable globals, and memories, tables and globals that the program constructs or other instances export; their limits, types and mutability are checked with Node.js's `LinkError` messages.
+Function references from a table or global reach JavaScript as callable functions, and exported functions can be stored in tables and globals, which is what dynamic linkers such as quickjs-wasi's extension loader do.
+Traps throw `WebAssembly.RuntimeError` with Node.js's messages, unbounded recursion throws `RangeError`, and an exception thrown by an imported function propagates through the WebAssembly frames unchanged.
+A `WebAssembly.Module` can be sent to a worker through `workerData` or `postMessage`.
+
+Not supported yet: the `Tag` and `Exception` constructors, shared memories (`new WebAssembly.Memory({ shared: true, ... })` throws a `TypeError`), `compileStreaming` and `instantiateStreaming`, and the classes used as values other than with `new`, `instanceof` and `WebAssembly.Module`'s static functions.
+TypeScript sources fail to build with SC2020 at each such use, and JavaScript sources throw that diagnostic when the statement runs.
+Programs built for `wasm32-wasi` can't use the WebAssembly API.
+
+Differences from Node.js:
+
+- Growing a memory, from JavaScript or inside the module, does not detach the previous `ArrayBuffer`.
+ The memory never moves, so views created before the growth stay valid but keep their old length; code that reads `memory.buffer` again after growing, as WebAssembly glue code does, behaves as in Node.js.
+- `valueOf()` answers a `WebAssembly.Global`'s value only when the receiver's static type is `WebAssembly.Global`; through an untyped value it answers the object, and converting a `Global` to a primitive uses the object rather than its value.
+- Instances are never freed, together with their memories, imports and exports.
+- A load whose result the module never uses may be removed instead of trapping when its address is out of bounds.
+ Loads whose result is dropped explicitly still trap.
+- NaN payloads may differ, for example when a signaling NaN passes through arithmetic or a JavaScript number.
+- `WebAssembly.compile` and `instantiate` with bytes settle after the current microtasks, as in Node.js, but can settle after `setImmediate` callbacks that Node.js would run later.
+- The executable reads `.wasm` files at run time from the paths they had when it was built.
diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json
index 2f7fd6696..e5fd082f6 100644
--- a/docs/src/generated/node-v24-compatibility.json
+++ b/docs/src/generated/node-v24-compatibility.json
@@ -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"
}
},
{
diff --git a/internal/compatibility/generated/node-v24-backlog.json b/internal/compatibility/generated/node-v24-backlog.json
index 3b419ea20..51459d3b3 100644
--- a/internal/compatibility/generated/node-v24-backlog.json
+++ b/internal/compatibility/generated/node-v24-backlog.json
@@ -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"
+ ]
}
}
},
diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json
index 2582c75b3..0e1e73a26 100644
--- a/internal/compatibility/generated/node-v24-internal.json
+++ b/internal/compatibility/generated/node-v24-internal.json
@@ -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"
},
diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json
index 32eea0977..734e038ef 100644
--- a/internal/compatibility/static-support.json
+++ b/internal/compatibility/static-support.json
@@ -28,6 +28,7 @@
"webstreams": "unreviewed"
},
"evidenceByManifestPrefix": {
+ "stdlib.webassembly": ["tests/corpus/webassembly-api.mjs", "tests/corpus/webassembly-typed.ts", "tests/corpus/webassembly-worker.ts", "tests/corpus/webassembly-spec-int/main.mjs", "tests/corpus/webassembly-spec-float/main.mjs", "tests/corpus/webassembly-spec-control/main.mjs", "tests/corpus/webassembly-spec-memory/main.mjs", "tests/corpus/webassembly-spec-table/main.mjs", "tests/corpus/webassembly-spec-imports/main.mjs", "tests/corpus/webassembly-jsapi.mjs", "tests/corpus/webassembly-wpt-jsapi.mjs", "tests/corpus/webassembly-dylink/main.mjs"],
"node-builtin.fs.readFileSync": ["tests/corpus/1403-buffer-fs.ts", "tests/corpus/2595-fs-arg-ladders.cjs", "tests/corpus/2919-fs-function-values.ts", "tests/corpus/2920-fs-promises-function-values.ts", "tests/corpus/fs-read-encodings.ts"],
"syntax.using-declarations": ["tests/corpus/2841-using-sync.ts"],
"syntax.await-using-declarations": ["tests/corpus/2842-await-using.ts"],
@@ -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"] },
diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts
index cb059fc1d..411291d94 100644
--- a/packages/compiler/ambient/scriptc-node-fallback.d.ts
+++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts
@@ -80,21 +80,54 @@ declare namespace NodeJS {
path?: string | undefined;
syscall?: string | undefined;
}
- /* The piped child-output stream (child.stdout / child.stderr under
- * stdio ["ignore", "pipe", "pipe"]): 'data' fires one Buffer chunk per
- * read (≤64KB, consumer-driven — no listener, no read), 'end' fires
- * once at EOF and always BEFORE the child's 'exit'. A flowing stream
- * keeps the loop alive. */
+ /* The readable-stream interface every Node stream implements. The
+ * piped child-output stream (child.stdout / child.stderr under stdio
+ * ["ignore", "pipe", "pipe"]) is declared with it: 'data' fires one
+ * Buffer chunk per read (≤64KB, consumer-driven — no listener, no
+ * read), 'end' fires once at EOF and always BEFORE the child's 'exit'. A
+ * flowing stream keeps the loop alive. A program that also passes
+ * other streams (fs, zlib, node:stream) where this interface is
+ * expected compiles it as the node:stream Readable, with child output
+ * bridged into one. */
interface ReadableStream {
on(event: "data", listener: (chunk: Buffer) => void): void;
on(event: "data", listener: (chunk: string) => void): void;
on(event: "end", listener: () => void): void;
+ on(event: string, listener: (...args: any[]) => void): void;
once(event: "data", listener: (chunk: Buffer) => void): void;
once(event: "data", listener: (chunk: string) => void): void;
once(event: "end", listener: () => void): void;
+ once(event: string, listener: (...args: any[]) => void): void;
/* setEncoding('utf8') makes subsequent data chunks strings, including
* multibyte sequences split across native pipe reads. */
setEncoding(encoding: string): this;
+ readonly readable: boolean;
+ pause(): this;
+ resume(): this;
+ isPaused(): boolean;
+ pipe(destination: T, options?: { end?: boolean }): T;
+ unpipe(destination?: WritableStream): this;
+ [Symbol.asyncIterator](): AsyncIterableIterator;
+ }
+ /* The writable-stream interface every Node stream implements:
+ * process.stdout/process.stderr, fs.WriteStream and the node:stream
+ * Writable family. Written only to the process streams, it compiles as
+ * their descriptor; with other streams flowing into it, as the
+ * node:stream Writable (the process streams convert into one). */
+ interface WritableStream {
+ readonly writable: boolean;
+ write(data: string | Uint8Array, callback?: (error?: Error | null) => void): boolean;
+ write(
+ data: string | Uint8Array,
+ encoding: BufferEncoding,
+ callback?: (error?: Error | null) => void,
+ ): boolean;
+ end(callback?: () => void): void;
+ end(data: string | Uint8Array, callback?: () => void): void;
+ end(data: string | Uint8Array, encoding: BufferEncoding, callback?: () => void): void;
+ /* Node's EventEmitter signature: listeners take untyped arguments. */
+ on(event: string, listener: (...args: any[]) => void): void;
+ once(event: string, listener: (...args: any[]) => void): void;
}
}
@@ -1271,8 +1304,37 @@ declare module "node:perf_hooks" {
getEntriesByType(type: EntryType): PerformanceEntry[];
clearMarks(name?: string): void;
clearMeasures(name?: string): void;
+ eventLoopUtilization(
+ utilization1?: EventLoopUtilization,
+ utilization2?: EventLoopUtilization,
+ ): EventLoopUtilization;
}
export const performance: Performance;
+ /* Event-loop metrics: utilization (idle/active ms since the loop started)
+ * and monitorEventLoopDelay's sampling histogram (nanoseconds). */
+ export interface EventLoopUtilization {
+ idle: number;
+ active: number;
+ utilization: number;
+ }
+ export interface EventLoopMonitorOptions {
+ resolution?: number | undefined;
+ }
+ export interface Histogram {
+ readonly count: number;
+ readonly exceeds: number;
+ readonly max: number;
+ readonly mean: number;
+ readonly min: number;
+ readonly stddev: number;
+ percentile(percentile: number): number;
+ reset(): void;
+ }
+ export interface IntervalHistogram extends Histogram {
+ enable(): boolean;
+ disable(): boolean;
+ }
+ export function monitorEventLoopDelay(options?: EventLoopMonitorOptions): IntervalHistogram;
}
declare module "perf_hooks" {
export * from "node:perf_hooks";
@@ -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(destination: T, options?: { end?: boolean }): T;
- unpipe(destination?: Writable): this;
+ pipe(destination: T, options?: { end?: boolean }): T;
+ unpipe(destination?: NodeJS.WritableStream): this;
destroy(error?: Error): this;
readonly readable: boolean;
readonly readableEnded: boolean;
diff --git a/packages/compiler/shims/fs-extra/core/api.ts b/packages/compiler/shims/fs-extra/core/api.ts
index a17a5f9a7..7a59f17e2 100644
--- a/packages/compiler/shims/fs-extra/core/api.ts
+++ b/packages/compiler/shims/fs-extra/core/api.ts
@@ -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;
diff --git a/packages/compiler/src/backend/executable-features.ts b/packages/compiler/src/backend/executable-features.ts
index b36f47d88..38f44e58e 100644
--- a/packages/compiler/src/backend/executable-features.ts
+++ b/packages/compiler/src/backend/executable-features.ts
@@ -16,6 +16,7 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures {
fileHandle: features.fileHandle,
fetch: features.fetch,
...(features.eventTarget ? { eventTarget: true } : {}),
+ ...(features.wasm ? { wasm: true } : {}),
...(features.warningReplace ? { warningReplace: true } : {}),
...(features.stdioExtended ? { stdioExtended: true } : {}),
zlib: features.zlib,
@@ -31,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,
};
}
diff --git a/packages/compiler/src/backend/library-pack.ts b/packages/compiler/src/backend/library-pack.ts
index 4ca5856ea..42275c5af 100644
--- a/packages/compiler/src/backend/library-pack.ts
+++ b/packages/compiler/src/backend/library-pack.ts
@@ -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({
diff --git a/packages/compiler/src/backend/llvm/emit-module.ts b/packages/compiler/src/backend/llvm/emit-module.ts
index 86b275763..4e4f11828 100644
--- a/packages/compiler/src/backend/llvm/emit-module.ts
+++ b/packages/compiler/src/backend/llvm/emit-module.ts
@@ -1,4 +1,5 @@
/* LlEmitter's module-level output: module parts, helper definitions, library declarations and FFI callback definitions. */
+import { emitWasmModules } from "./wasm-modules.js";
import { type LlEmitter, ffiCallbackDummyLl, llStrBytes } from "./emitter.js";
import {
mangleAsyncSpawn,
@@ -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`,
diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts
index f7b9f2517..b72a6e226 100644
--- a/packages/compiler/src/backend/llvm/emitter.ts
+++ b/packages/compiler/src/backend/llvm/emitter.ts
@@ -1,3 +1,4 @@
+import { wasmBoundsChecks } from "./wasm-modules.js";
import { jsTruthy } from "./jsval.js";
import { type ArraySearchKind } from "./array-search.js";
import { emitStringInputs } from "./string-lifetimes.js";
@@ -181,6 +182,7 @@ import {
genResultThunkFor,
childExitThunkFor,
childExitSignalThunkFor,
+ childDataDynThunkFor,
childDataThunkFor,
execFileThunkFor,
ipcMessageThunkFor,
@@ -413,6 +415,9 @@ export interface LlvmTargetOptions {
* teardown after `process.exit` to a helper process (`--detached-exit`).
* Executables only; the runtime's weak default keeps it off. */
detachedExit?: boolean;
+ /** Translated WebAssembly memories check bounds explicitly instead of
+ * relying on guard pages (wasm-modules.ts wasmBoundsChecks). */
+ wasmBoundsChecks?: boolean;
}
export function emitLlvmModule(input: IrModule, options: LlvmTargetOptions = {}): string {
@@ -682,6 +687,7 @@ export class LlEmitter {
readonly emitLibraryIdentity: boolean;
readonly runtimeAbiMarker: boolean;
readonly detachedExit: boolean;
+ readonly wasmBoundsChecks: boolean;
/** Interned string literals: UTF-8 text → { symbol, byte length } —
* first-use order, the runtime ABI’s determinism discipline. */
readonly literals = new Map();
@@ -1175,6 +1181,13 @@ export class LlEmitter {
this.emitLibraryIdentity = options.emitLibraryIdentity !== false;
this.runtimeAbiMarker = options.runtimeAbiMarker === true;
this.detachedExit = options.detachedExit === true;
+ this.wasmBoundsChecks =
+ options.wasmBoundsChecks === true ||
+ wasmBoundsChecks({
+ sanitize: false,
+ pointerBits: options.pointerBits ?? 64,
+ targetTriple: options.targetTriple ?? "",
+ });
this.rcHelpers = options.inlineRc !== undefined ? new Set() : null;
this.rcMode = options.inlineRc;
this.rcHelpersNoinline = options.rcHelpersNoinline === true;
@@ -4201,6 +4214,20 @@ export class LlEmitter {
/** Moves need plain slots that nothing else reads behind the binding's
* back: no debugger view, no coroutine frame, no borrowed (+0) return. */
+ /** Whether the program reads event-loop utilization (perf.loopStart /
+ * perf.idleTime): main then marks the loop's start before running it. */
+ private loopMetricsRead: boolean | undefined;
+ readsLoopMetrics(): boolean {
+ if (this.loopMetricsRead !== undefined) return this.loopMetricsRead;
+ const visitor = {
+ expr: (e: IrExpr): boolean =>
+ !(e.kind === "libCall" && (e.fn === "perf.loopStart" || e.fn === "perf.idleTime")),
+ stmt: (): boolean => true,
+ };
+ this.loopMetricsRead = !this.mod.functions.every((fn) => everyStmtList(fn.body, visitor));
+ return this.loopMetricsRead;
+ }
+
private lastUseMovesAllowed(): boolean {
return (
this.debug === null &&
@@ -5564,6 +5591,10 @@ export class LlEmitter {
return childDataThunkFor(this, param);
}
+ childDataDynThunkFor(chunk: "bytes" | "str"): string {
+ return childDataDynThunkFor(this, chunk);
+ }
+
emitterFixedAdapter(cbT: IrType & { kind: "func" }): { fn: string; shim: string } {
return emitterFixedAdapter(this, cbT);
}
diff --git a/packages/compiler/src/backend/llvm/expr-callbacks.ts b/packages/compiler/src/backend/llvm/expr-callbacks.ts
index b7954c174..28bd15759 100644
--- a/packages/compiler/src/backend/llvm/expr-callbacks.ts
+++ b/packages/compiler/src/backend/llvm/expr-callbacks.ts
@@ -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" },
diff --git a/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts b/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts
index 3f7961163..9be757a20 100644
--- a/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts
+++ b/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts
@@ -17,7 +17,7 @@ export function streamDataAdapter(
const p = cbT.params[0];
if (
cbT.params.length > 1 ||
- (p && p.kind !== "bytes" && p.kind !== "string" && p.kind !== "dyn")
+ (p && p.kind !== "bytes" && p.kind !== "string" && p.kind !== "dyn" && p.kind !== "union")
) {
throw new InternalCompilerError(
"llvm emitter bug: stream data listener param shape (frontend must fence)",
@@ -73,6 +73,46 @@ export function streamDataAdapter(
);
finish("%r0");
d.push(`}`, ``);
+ } else if (p.kind === "union") {
+ // A `Buffer | string` chunk: wrap whichever slot the stream filled at
+ // its arm of the (boxed) union.
+ const def = host.unionsById.get(p.unionId);
+ const bytesTag = def ? def.arms.findIndex((a) => a.kind === "bytes" && a.elem === "u8") : -1;
+ const strTag = def ? def.arms.findIndex((a) => a.kind === "string") : -1;
+ if (
+ !def ||
+ host.nullableUnions.has(p.unionId) ||
+ bytesTag < 0 ||
+ strTag < 0 ||
+ def.arms.length !== 2
+ )
+ throw new InternalCompilerError(
+ "llvm emitter bug: stream data listener union is not Buffer | string (frontend must fence)",
+ );
+ host.declare(`declare ptr @scr_union_new_ref(i32, ptr, ptr, ptr, ptr)`);
+ host.declare(`declare ptr @scr_bytes_retain_v(ptr)`);
+ host.declare(`declare void @scr_bytes_release_v(ptr)`);
+ host.declare(`declare ptr @scr_str_retain_v(ptr)`);
+ host.declare(`declare void @scr_str_release_v(ptr)`);
+ d.push(
+ ` %uslot = alloca ptr`,
+ ` %isb = icmp ne ptr %a0, null`,
+ ` br i1 %isb, label %buf, label %str`,
+ `buf:`,
+ ` %rb = call ptr @scr_bytes_retain_v(ptr %a0)`,
+ ` %ub = call ptr @scr_union_new_ref(i32 ${bytesTag}, ptr %rb, ptr @scr_bytes_retain_v, ptr @scr_bytes_release_v, ptr null)`,
+ ` store ptr %ub, ptr %uslot`,
+ ` br label %go`,
+ `str:`,
+ ` %rs = call ptr @scr_str_retain_v(ptr %a1)`,
+ ` %us = call ptr @scr_union_new_ref(i32 ${strTag}, ptr %rs, ptr @scr_str_retain_v, ptr @scr_str_release_v, ptr null)`,
+ ` store ptr %us, ptr %uslot`,
+ ` br label %go`,
+ `go:`,
+ ` %uv = load ptr, ptr %uslot`,
+ );
+ finish("%uv");
+ d.push(`}`, ``);
} else {
// dyn: box by runtime tag — the JS lane's adapter parameter.
host.declare(`declare ptr @scr_dyn_new_buffer(ptr)`);
diff --git a/packages/compiler/src/backend/llvm/lib-dispatch.ts b/packages/compiler/src/backend/llvm/lib-dispatch.ts
index 16759a592..6cec9d403 100644
--- a/packages/compiler/src/backend/llvm/lib-dispatch.ts
+++ b/packages/compiler/src/backend/llvm/lib-dispatch.ts
@@ -857,6 +857,7 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
case "event":
case "events":
case "blob":
+ case "wasm":
case "record":
case "disposable":
case "punycode":
diff --git a/packages/compiler/src/backend/llvm/lib-process.ts b/packages/compiler/src/backend/llvm/lib-process.ts
index eaf7f31cd..4a7be1a35 100644
--- a/packages/compiler/src/backend/llvm/lib-process.ts
+++ b/packages/compiler/src/backend/llvm/lib-process.ts
@@ -305,6 +305,28 @@ export function emitChildProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr
B.line(`${raw} = call ptr @${get}(ptr ${recv.name}) ; +1 or NULL`);
return host.wrapNullable(raw, raw, def!.arms[streamTag]!, streamTag, e.type, nullTag);
}
+ if (e.fn === "child.stdioStream") {
+ // The general stream representation's stdio read: the child's bridged
+ // runtime stream (+1) or NULL for an unpiped slot.
+ if (e.type.kind !== "union")
+ throw new InternalCompilerError(`llvm emitter bug: ${e.fn} result is not a union`);
+ const def = host.unionsById.get(e.type.unionId);
+ const streamTag = def ? def.arms.findIndex((a) => a.kind === "object") : -1;
+ const nullTag = def ? def.arms.findIndex((a) => a.kind === "nullT") : -1;
+ const fd = e.args[1];
+ if (streamTag < 0 || nullTag < 0 || fd?.kind !== "numLit")
+ throw new InternalCompilerError(`llvm emitter bug: ${e.fn} lacks its arms or slot`);
+ const recv = host.emitExpr(e.args[0]!);
+ const key = fd.value === 0 ? "stdin" : fd.value === 1 ? "stdout" : "stderr";
+ host.declare(`declare ptr @scr_stream_child_stdio(ptr, ptr, ${host.sizeType})`);
+ const raw = B.tmp();
+ B.line(
+ `${raw} = call ptr @scr_stream_child_stdio(ptr ${recv.name}, ptr ${host.cstr(key)}, ${host.sizeType} ${key.length}) ; +1 or NULL`,
+ );
+ const out = host.wrapNullable(raw, raw, def!.arms[streamTag]!, streamTag, e.type, nullTag);
+ host.emitPendingCheck();
+ return out;
+ }
if (e.fn === "writer.writeString" || e.fn === "writer.writeBytes") {
const writer = host.emitExpr(e.args[0]!);
const data = host.emitExpr(e.args[1]!);
diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts
index 503ed0e6d..91242b45c 100644
--- a/packages/compiler/src/backend/llvm/lib-shared.ts
+++ b/packages/compiler/src/backend/llvm/lib-shared.ts
@@ -127,6 +127,11 @@ export const LIB_FN_SYMS: Record = {
"str.encodeUriComponent": "scr_str_encode_uri_component",
"error.cause": "scr_error_cause",
"error.setCause": "scr_error_set_cause",
+ "error.setCode": "scr_error_set_code_value",
+ "error.setCodeDyn": "scr_error_set_code_dyn",
+ "error.assign": "scr_error_assign_dyn",
+ "error.hasKey": "scr_error_has_key",
+ "error.hasCode": "scr_error_has_code",
"error.defineCause": "scr_error_define_cause",
"error.deleteCause": "scr_error_delete_cause",
"error.hasCause": "scr_error_has_cause",
@@ -278,6 +283,14 @@ export const LIB_FN_SYMS: Record = {
"perf.entryDetail": "scr_perf_entry_detail",
"perf.entryKeep": "scr_perf_entry_keep",
"perf.entryRef": "scr_perf_entry_ref",
+ "perf.loopStart": "scr_perf_loop_start",
+ "perf.idleTime": "scr_perf_idle_time",
+ "perf.eldNew": "scr_eld_new",
+ "perf.eldEnable": "scr_eld_enable",
+ "perf.eldDisable": "scr_eld_disable",
+ "perf.eldReset": "scr_eld_reset",
+ "perf.eldPercentile": "scr_eld_percentile",
+ "perf.eldStat": "scr_eld_stat",
"process.memoryUsageTake": "scr_process_memory_usage_take",
"process.memoryUsageField": "scr_process_memory_usage_field",
"process.rss": "scr_process_rss",
@@ -727,6 +740,8 @@ export const LIB_FN_SYMS: Record = {
"child.unref": "scr_child_unref",
"child.ref": "scr_child_ref",
"procStream.write": "scr_proc_stream_write",
+ "stream.stdioWritable": "scr_stream_stdio_writable",
+ "stream.unboxDyn": "scr_stream_unbox_dyn",
"process.kill": "scr_process_kill_named",
"process.killNum": "scr_process_kill",
// fs.promises (scr_bytes_io.c / scr_lib.c): the promise forms settle
@@ -859,6 +874,24 @@ export const LIB_FN_SYMS: Record = {
"event.get": "scr_event_obj_get",
"event.set": "scr_event_obj_set",
"blob.new": "scr_blob_new",
+ "wasm.compile": "scr_wasm_compile",
+ "wasm.validate": "scr_wasm_validate",
+ "wasm.instantiate": "scr_wasm_instantiate",
+ "wasm.moduleNew": "scr_wasm_module_new",
+ "wasm.instanceNew": "scr_wasm_instance_new",
+ "wasm.is": "scr_wasm_is",
+ "wasm.errorNew": "scr_wasm_error_new",
+ "wasm.memoryNew": "scr_wasm_memory_construct",
+ "wasm.tableNew": "scr_wasm_table_construct",
+ "wasm.globalNew": "scr_wasm_global_construct",
+ "wasm.withoutNew": "scr_wasm_construct_without_new",
+ "wasm.moduleImports": "scr_wasm_module_imports",
+ "wasm.moduleExports": "scr_wasm_module_exports",
+ "wasm.moduleCustomSections": "scr_wasm_module_custom_sections",
+ "wasm.valueOf": "scr_wasm_value_of",
+ "wasm.function": "scr_wasm_function",
+ "wasm.namespace": "scr_wasm_namespace",
+ "wasm.globalView": "scr_wasm_global_view",
"blob.fileNew": "scr_file_new",
"blob.slice": "scr_blob_slice",
"blob.text": "scr_blob_text",
@@ -1110,6 +1143,7 @@ export const LIB_FN_SYMS: Record = {
"readable.opFind": "scr_stream_op_find",
"readable.opReduce": "scr_stream_op_reduce",
"readable.pipe": "scr_stream_pipe",
+ "readable.pipeGeneral": "scr_stream_pipe_general",
"writable.cork": "scr_stream_cork",
"writable.uncork": "scr_stream_uncork",
"stream.destroyErr": "scr_stream_destroy",
@@ -1424,6 +1458,9 @@ export const LIB_FN_SYMS: Record = {
"dyn.classPrototype": "scr_dyn_class_prototype",
"dyn.classBasePrototype": "scr_dyn_class_base_prototype",
"dyn.classInherit": "scr_dyn_class_inherit",
+ "dyn.builtinClass": "scr_dyn_builtin_class",
+ "dyn.builtinInstanceOf": "scr_dyn_builtin_instanceof",
+ "dyn.functionInstanceOf": "scr_dyn_function_instanceof",
"dyn.classSuper": "scr_dyn_class_super",
"dyn.assignPrototype": "scr_dyn_assign_prototype",
"dyn.definePrototypeProps": "scr_dyn_define_prototype_props",
@@ -1596,6 +1633,7 @@ export const USES_TIMERS_LIB_FNS = new Set([
"readable.opReduce",
"readable.resume",
"readable.pipe",
+ "readable.pipeGeneral",
"readable.unpipe",
"writable.write",
"writable.writeStr",
diff --git a/packages/compiler/src/backend/llvm/lib-stream.ts b/packages/compiler/src/backend/llvm/lib-stream.ts
index b62b237df..0ded59a1a 100644
--- a/packages/compiler/src/backend/llvm/lib-stream.ts
+++ b/packages/compiler/src/backend/llvm/lib-stream.ts
@@ -56,6 +56,8 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV
host.declare(`declare void @scr_child_stream_thunk0(ptr, ptr)`);
} else if (param.kind === "union") {
adapter = host.childDataThunkFor(param);
+ } else if (param.kind === "dyn") {
+ adapter = host.childDataDynThunkFor("bytes");
} else {
adapter = "scr_child_stream_thunk_bytes";
host.declare(`declare void @scr_child_stream_thunk_bytes(ptr, ptr)`);
@@ -78,7 +80,8 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV
if (e.fn === "stream.onDataStr") {
host.usesTimers = true;
const cbT = e.args[1]!.type;
- if (cbT.kind !== "func" || cbT.params[0]?.kind !== "string") {
+ const strParam = cbT.kind === "func" ? cbT.params[0]?.kind : undefined;
+ if (strParam !== "string" && strParam !== "dyn") {
throw new InternalCompilerError(
"llvm emitter bug: stream.onDataStr callback not a string func",
);
@@ -87,10 +90,12 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV
const cb = host.emitExpr(e.args[1]!);
const once = host.emitExpr(e.args[2]!);
host.toHeap(cb);
- host.declare(`declare void @scr_child_stream_thunk_str(ptr, ptr)`);
+ let adapter = "scr_child_stream_thunk_str";
+ if (strParam === "dyn") adapter = host.childDataDynThunkFor("str");
+ else host.declare(`declare void @scr_child_stream_thunk_str(ptr, ptr)`);
host.declare(`declare void @scr_child_stream_on_data_str(ptr, ptr, ptr, i1 zeroext)`);
B.line(
- `call void @scr_child_stream_on_data_str(ptr ${s0.name}, ptr ${cb.name}, ptr @scr_child_stream_thunk_str, i1 ${once.name})`,
+ `call void @scr_child_stream_on_data_str(ptr ${s0.name}, ptr ${cb.name}, ptr @${adapter}, i1 ${once.name})`,
);
return { name: "", type: e.type };
}
diff --git a/packages/compiler/src/backend/llvm/wasm-modules.ts b/packages/compiler/src/backend/llvm/wasm-modules.ts
new file mode 100644
index 000000000..8a7045687
--- /dev/null
+++ b/packages/compiler/src/backend/llvm/wasm-modules.ts
@@ -0,0 +1,77 @@
+/* The WebAssembly modules translated into a program (IrModule.wasmModules):
+ * each module's translation (wasm/translate.ts) and the registry the
+ * runtime looks modules up in (scr_wasm_registry, scr_wasm_abi.h). The
+ * translations join the program module, so the native helper optimizes,
+ * partitions and caches them with it. Programs that do not use the
+ * WebAssembly API carry no wasmModules and emit nothing here. */
+import type { IrModule } from "../../ir/ir.js";
+import { decodeBase64 } from "../../wasm/base64.js";
+import { translateWasmModule, WasmTranslateError } from "../../wasm/translate.js";
+
+function hexBytes(hex: string): Uint8Array {
+ const out = new Uint8Array(hex.length / 2);
+ for (let i = 0; i < out.length; i++) out[i] = parseInt(hex.slice(2 * i, 2 * i + 2), 16);
+ return out;
+}
+
+/** Whether translated memories check bounds explicitly instead of relying
+ * on guard pages and the runtime's fault handler: sanitizer builds (whose
+ * runtime owns SIGSEGV), 32-bit targets, and Windows, which has no
+ * fault handler yet. */
+export function wasmBoundsChecks(options: {
+ sanitize: boolean;
+ pointerBits: 32 | 64;
+ targetTriple: string;
+}): boolean {
+ if (options.sanitize || options.pointerBits === 32) return true;
+ const target = options.targetTriple || process.platform;
+ return /windows|mingw|cygwin|win32|msvc/.test(target);
+}
+
+/** The top-level lines of every translated module plus the registry.
+ * `declared` holds the declarations the module already carries; runtime
+ * entry points it lacks are declared here. A module the translator
+ * cannot translate is left out of the registry, so loading it at run
+ * time rejects with the CompileError for untranslated bytes. */
+export function emitWasmModules(
+ mod: IrModule,
+ declared: ReadonlySet,
+ boundsChecks: boolean,
+): string[] {
+ const modules = mod.wasmModules;
+ if (modules === undefined) return [];
+ const out: string[] = [];
+ const decls = new Set(declared);
+ const descriptors: string[] = [];
+ for (const module of modules) {
+ const bytes = decodeBase64(module.bytes);
+ if (bytes === null) continue;
+ let translation;
+ try {
+ translation = translateWasmModule(bytes, hexBytes(module.sha256), {
+ boundsChecks,
+ customSections: mod.wasmCustomSections === true,
+ });
+ } catch (error) {
+ if (error instanceof WasmTranslateError) continue;
+ throw error;
+ }
+ for (const decl of translation.decls) {
+ if (decls.has(decl)) continue;
+ decls.add(decl);
+ out.push(decl);
+ }
+ for (const part of translation.text) out.push(part);
+ descriptors.push(translation.descriptor);
+ }
+ if (descriptors.length === 0) {
+ out.push(`@scr_wasm_registry = constant { i64, ptr } { i64 0, ptr null }`);
+ } else {
+ out.push(
+ `@sc_wasm_registry_modules = private constant [${descriptors.length} x ptr] [${descriptors.map((d) => `ptr ${d}`).join(", ")}]`,
+ `@scr_wasm_registry = constant { i64, ptr } { i64 ${descriptors.length}, ptr @sc_wasm_registry_modules }`,
+ );
+ }
+ out.push("");
+ return out;
+}
diff --git a/packages/compiler/src/backend/native-link-info-core.ts b/packages/compiler/src/backend/native-link-info-core.ts
index 8f2df0452..031f99ddd 100644
--- a/packages/compiler/src/backend/native-link-info-core.ts
+++ b/packages/compiler/src/backend/native-link-info-core.ts
@@ -25,6 +25,9 @@ export interface NativeLinkFeatures {
fileHandle: boolean;
fetch: boolean;
eventTarget?: boolean;
+ /** The WebAssembly API (scr_wasm.c). Absent when unused, so programs
+ * without it keep their link identity. */
+ wasm?: boolean;
/** The program assigns process.emitWarning: links the
* SCR_WARNING_REPLACE variant of the dyn-async unit. */
warningReplace?: boolean;
@@ -46,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. */
diff --git a/packages/compiler/src/backend/native/contracts.ts b/packages/compiler/src/backend/native/contracts.ts
index e64614595..8abdc6c38 100644
--- a/packages/compiler/src/backend/native/contracts.ts
+++ b/packages/compiler/src/backend/native/contracts.ts
@@ -118,6 +118,8 @@ export interface CcOptions {
* native fetch AbortSignal built on the same engine: compiles
* scr_event_target.c. */
eventTarget?: boolean;
+ /** The WebAssembly API: compiles scr_wasm.c. */
+ wasm?: boolean;
/** SCR_WARNING_REPLACE: process.emitWarning replacement support. */
warningReplace?: boolean;
/** SCR_STDIO_EXTENDED: stdin encoding, string data listeners, resize. */
@@ -150,6 +152,11 @@ export interface CcOptions {
* line (escape-only programs ride the always-linked component encoder
* and never flip this). */
qs?: boolean;
+ /** The program uses EventEmitter or a stream class as an untyped
+ * constructor value (a constructor function's base): compiles
+ * scr_dyn_builtin_class.c, which also references the inspection,
+ * dyn-invoke and symbol units. */
+ dynBuiltinClass?: boolean;
/** The program uses native util parsing, comparison, or styling. Optional
* utility units also require their symbol, inspection, and warning features. */
parseArgs?: boolean;
@@ -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
diff --git a/packages/compiler/src/backend/native/executable.ts b/packages/compiler/src/backend/native/executable.ts
index 9c083fe9b..33c126de8 100644
--- a/packages/compiler/src/backend/native/executable.ts
+++ b/packages/compiler/src/backend/native/executable.ts
@@ -182,6 +182,7 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
unit((u) => u.opts.dc, "scr_dc.c"),
unit((u) => u.opts.dynTier, "scr_js_value.c"),
unit((u) => u.opts.eventTarget || u.nativeFetch, "scr_event_target.c"),
+ unit((u) => u.opts.wasm, "scr_wasm.c"),
unit(
(u) =>
u.opts.dynAsync ||
@@ -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.
diff --git a/packages/compiler/src/backend/runtime-pack-core.ts b/packages/compiler/src/backend/runtime-pack-core.ts
index 6a660e00c..be94b33b9 100644
--- a/packages/compiler/src/backend/runtime-pack-core.ts
+++ b/packages/compiler/src/backend/runtime-pack-core.ts
@@ -82,6 +82,7 @@ export interface RuntimeFeatureSet extends NativeLinkFeatures {
workers: boolean;
youngNursery: boolean;
eventTarget: boolean;
+ wasm: boolean;
dynTier: boolean;
warningReplace: boolean;
stdioExtended: boolean;
@@ -272,6 +273,7 @@ export function effectiveRuntimeFeatures(
workers: features.workers === true,
youngNursery: features.youngNursery === true,
eventTarget: features.eventTarget === true,
+ wasm: features.wasm === true,
dynTier: features.dynTier === true,
warningReplace: features.warningReplace === true,
stdioExtended: features.stdioExtended === true,
diff --git a/packages/compiler/src/backend/runtime-pack-targets.test.ts b/packages/compiler/src/backend/runtime-pack-targets.test.ts
index 49ca691cb..a39ce78e2 100644
--- a/packages/compiler/src/backend/runtime-pack-targets.test.ts
+++ b/packages/compiler/src/backend/runtime-pack-targets.test.ts
@@ -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([
diff --git a/packages/compiler/src/backend/runtime-pack.test.ts b/packages/compiler/src/backend/runtime-pack.test.ts
index 91a74eb7e..1681626b7 100644
--- a/packages/compiler/src/backend/runtime-pack.test.ts
+++ b/packages/compiler/src/backend/runtime-pack.test.ts
@@ -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) {
diff --git a/packages/compiler/src/backend/target-diagnostics.ts b/packages/compiler/src/backend/target-diagnostics.ts
index 365d5e913..d130ce95e 100644
--- a/packages/compiler/src/backend/target-diagnostics.ts
+++ b/packages/compiler/src/backend/target-diagnostics.ts
@@ -192,6 +192,14 @@ export function moduleWasiUnavailableSurface(
found = { surface: "OS signals (WASI Preview 1 has no signal API)", loc };
return false;
}
+ if (node.fn.startsWith("wasm.")) {
+ found = {
+ surface:
+ "the WebAssembly API (a wasm32-wasi program cannot load translated modules into its own linear memory yet)",
+ loc,
+ };
+ return false;
+ }
if (node.fn === "crypto.cipherNew") {
found = {
surface:
diff --git a/packages/compiler/src/backend/warm-cache.ts b/packages/compiler/src/backend/warm-cache.ts
index eb7822d10..9a505d517 100644
--- a/packages/compiler/src/backend/warm-cache.ts
+++ b/packages/compiler/src/backend/warm-cache.ts
@@ -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,
},
});
diff --git a/packages/compiler/src/coverage/report.ts b/packages/compiler/src/coverage/report.ts
index 6570f23d1..89733e255 100644
--- a/packages/compiler/src/coverage/report.ts
+++ b/packages/compiler/src/coverage/report.ts
@@ -221,13 +221,13 @@ export function renderCoverage(input: CoverageInput, opts: CoverageRenderOptions
out.push("");
}
- // The WebAssembly modules the build can see: what translating them would
- // take (wasm/discover.ts). No module compiles to native code yet; the
- // sites that load one are blockers or deferred fences above.
+ // The WebAssembly modules the build can see (wasm/discover.ts). A build
+ // translates the valid ones to native code (wasm/translate.ts) when the
+ // program uses the WebAssembly API; the others fail to load at run time.
const wasm = input.wasmModules ?? [];
if (wasm.length > 0) {
out.push(
- ` ${c(DIM, "WebAssembly modules:")} ${c(DIM, "(found at build time; none compile to native code yet)")}`,
+ ` ${c(DIM, "WebAssembly modules:")} ${c(DIM, "(found at build time; valid ones compile to native code)")}`,
);
for (const m of wasm) {
const name = m.path !== undefined ? displayPath(m.path, opts.root) : "inline module";
diff --git a/packages/compiler/src/coverage/surface-manifest.ts b/packages/compiler/src/coverage/surface-manifest.ts
index d82367611..9867a5617 100644
--- a/packages/compiler/src/coverage/surface-manifest.ts
+++ b/packages/compiler/src/coverage/surface-manifest.ts
@@ -444,16 +444,17 @@ export function generateSurfaceManifest(compilerVersion: string): SurfaceManifes
}
}
- // ── the WebAssembly global: every reached use refuses (webAssemblyDiag) ─
+ // ── the WebAssembly global (builtins/webassembly.ts, wasm/translate.ts):
+ // the core API lowers over modules translated at build time; the rest
+ // refuses at each use (webAssemblyDiag) ─
{
const refusal = webAssemblyDiag("WebAssembly", { file: "", start: 0, end: 0 });
add({
id: "stdlib.webassembly",
kind: "stdlib",
name: "globalThis.WebAssembly",
- status: "unsupported",
- code: refusal.code,
- ...(refusal.hint !== undefined ? { note: refusal.hint } : {}),
+ status: "static",
+ note: `compile, instantiate, validate, the Module, Instance, Memory, Table and Global constructors, Module.imports, exports and customSections, instanceof, the error classes, exports, imports and Module transfer to workers, over the modules translated at build time; the rest refuses with ${refusal.code} (${refusal.hint ?? ""})`,
});
}
diff --git a/packages/compiler/src/diagnostics/diagnostic.ts b/packages/compiler/src/diagnostics/diagnostic.ts
index 049202624..e4d4006ca 100644
--- a/packages/compiler/src/diagnostics/diagnostic.ts
+++ b/packages/compiler/src/diagnostics/diagnostic.ts
@@ -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)",
};
}
diff --git a/packages/compiler/src/executable/executable-cache.test.ts b/packages/compiler/src/executable/executable-cache.test.ts
index d12a5d1f0..5b0982dc1 100644
--- a/packages/compiler/src/executable/executable-cache.test.ts
+++ b/packages/compiler/src/executable/executable-cache.test.ts
@@ -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<{
diff --git a/packages/compiler/src/executable/executable-cache.ts b/packages/compiler/src/executable/executable-cache.ts
index 4f3fed552..fd4a34ec2 100644
--- a/packages/compiler/src/executable/executable-cache.ts
+++ b/packages/compiler/src/executable/executable-cache.ts
@@ -56,6 +56,7 @@ export interface EarlyExecutableNativeFeatures {
fileHandle: boolean;
fetch: boolean;
eventTarget?: boolean;
+ wasm?: boolean;
warningReplace?: boolean;
stdioExtended?: boolean;
zlib: boolean;
@@ -71,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 {
diff --git a/packages/compiler/src/frontend/lowering/array-values.ts b/packages/compiler/src/frontend/lowering/array-values.ts
index 8f2b28a3f..0ca8503df 100644
--- a/packages/compiler/src/frontend/lowering/array-values.ts
+++ b/packages/compiler/src/frontend/lowering/array-values.ts
@@ -8,6 +8,7 @@ import {
type IrType,
isIntegerBytesElem,
type SrcLoc,
+ STRING,
UNDEFINED_T,
typeEquals,
typeKey,
@@ -84,29 +85,54 @@ export function arrayValueRead(
};
}
-/** Read a stabilized union of native arrays without copying either the
- * receiver or its elements. Every arm contributes its missing-value case. */
+/** Read one position of a stabilized union of native arrays and tuples
+ * (`const [a, b] = re.exec(s) || ["", ""]`) without copying the receiver
+ * or its elements. Every array arm contributes its missing-value case; a
+ * tuple arm answers its field, or undefined past its end. Tuple arms need
+ * a constant position. With `stringChars` (the string iterator's code
+ * points as a fresh string[]), a string arm (`re.exec(s) || ""`) answers
+ * its code point at the position, or undefined past its end. */
export function unionArrayValueRead(
lowerer: Lowerer,
value: IrExpr,
index: IrExpr,
loc: SrcLoc,
+ stringChars?: (text: IrExpr) => IrExpr,
): IrExpr | null {
if (value.type.kind !== "union") return null;
const unionId = value.type.unionId;
const arms = lowerer.unions.get(unionId)?.arms;
if (!arms?.length) return null;
+ const position = index.kind === "numLit" ? String(index.value) : null;
+ // A tuple arm's answer at the position: its field, or undefined past
+ // its end. Null when the arm is neither an array nor a tuple.
+ const tupleField = (arm: IrType): { name: string; type: IrType } | "past" | null => {
+ if (arm.kind !== "record" || position === null) return null;
+ const shape = lowerer.shapes.get(arm.shapeId);
+ if (!shape?.tuple) return null;
+ return shape.fields.find((f) => f.name === position) ?? "past";
+ };
// Keep the collection's IrType union layout and narrow each element at
// its use; an inferred array predicate changes the collection's ABI.
- for (const arm of arms) if (arm.kind !== "array") return null;
+ const stringArm = (arm: IrType): boolean => arm.kind === "string" && stringChars !== undefined;
+ for (const arm of arms)
+ if (arm.kind !== "array" && !stringArm(arm) && tupleField(arm) === null) return null;
const answers = new Map();
for (const arm of arms) {
- if (arm.kind !== "array") return null;
- const answer = arrayValueType(lowerer, arm.elem);
+ const field = arm.kind === "array" || stringArm(arm) ? null : tupleField(arm);
+ const answer =
+ arm.kind === "array"
+ ? arrayValueType(lowerer, arm.elem)
+ : stringArm(arm)
+ ? arrayValueType(lowerer, STRING)
+ : field === "past"
+ ? UNDEFINED_T
+ : field!.type;
const parts = answer.kind === "union" ? lowerer.unions.get(answer.unionId)!.arms : [answer];
for (const part of parts) answers.set(typeKey(part), part);
}
const joined = [...answers.values()].sort((a, b) => (typeKey(a) < typeKey(b) ? -1 : 1));
+ if (joined.length === 1 && joined[0]!.kind === "undefinedT") return null;
if (
joined.length > 1 &&
(!unionContainerArmsOk(joined) ||
@@ -118,12 +144,39 @@ export function unionArrayValueRead(
let result: IrExpr | null = null;
for (let tag = arms.length - 1; tag >= 0; tag--) {
const arm = arms[tag]!;
- if (arm.kind !== "array") return null;
const receiver: IrExpr = { kind: "unionNarrow", unionId, tag, value, type: arm, loc };
- const read = lowerer.coerceToExpected(
- arrayValueRead(lowerer, receiver, index, arm.elem, loc),
- type,
- );
+ let read: IrExpr;
+ if (arm.kind === "array") {
+ read = lowerer.coerceToExpected(
+ arrayValueRead(lowerer, receiver, index, arm.elem, loc),
+ type,
+ );
+ } else if (stringArm(arm)) {
+ read = lowerer.coerceToExpected(
+ arrayValueRead(lowerer, stringChars!(receiver), index, STRING, loc),
+ type,
+ );
+ } else {
+ const field = tupleField(arm);
+ if (field === null) return null;
+ if (field === "past") {
+ const missing = lowerer.wrappedUndefined(type, loc);
+ if (!missing) return null;
+ read = missing;
+ } else {
+ read = lowerer.coerceToExpected(
+ {
+ kind: "recordGet",
+ obj: receiver,
+ shapeId: (arm as IrType & { kind: "record" }).shapeId,
+ field: field.name,
+ type: field.type,
+ loc,
+ },
+ type,
+ );
+ }
+ }
result =
result === null
? read
diff --git a/packages/compiler/src/frontend/lowering/builtin-calls.ts b/packages/compiler/src/frontend/lowering/builtin-calls.ts
index de287be4f..d2e4b53d5 100644
--- a/packages/compiler/src/frontend/lowering/builtin-calls.ts
+++ b/packages/compiler/src/frontend/lowering/builtin-calls.ts
@@ -1,4 +1,5 @@
import { lowerReadlineCursorCall } from "./builtins/readline.js";
+import { lowerMonitorEventLoopDelayCall } from "./builtins/performance.js";
import {
lowerEnvironmentData,
lowerReceiveMessageOnPort,
@@ -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;
diff --git a/packages/compiler/src/frontend/lowering/builtins/builtin-class-dyn.ts b/packages/compiler/src/frontend/lowering/builtins/builtin-class-dyn.ts
new file mode 100644
index 000000000..ac73924ad
--- /dev/null
+++ b/packages/compiler/src/frontend/lowering/builtins/builtin-class-dyn.ts
@@ -0,0 +1,149 @@
+import { numLit } from "../../../ir/build.js";
+import { BOOL, DYN, RUNTIME_EMITTER_CLASS, type IrExpr, type SrcLoc } from "../../../ir/ir.js";
+import * as ts from "../../ts7/adapter.js";
+import { locOf } from "../../program.js";
+import type { Lowerer } from "../lowerer.js";
+
+/* EventEmitter and the stream classes as untyped constructor values, the
+ * bases of JavaScript constructor functions:
+ *
+ * function Bus() { EventEmitter.call(this); }
+ * util.inherits(Bus, EventEmitter);
+ *
+ * A compiled `class Bus extends EventEmitter` gets a native emitter prefix,
+ * but `new Bus()` on a constructor function builds an untyped object, so
+ * the base is the runtime's dyn constructor (scr_dyn_builtin_class.c): a
+ * function whose prototype carries Node's EventEmitter methods over the
+ * object's own `_events` table. The stream classes have prototype chains
+ * for util.inherits and instanceof; constructing over them refuses where
+ * it runs. These values appear only where a constructor function uses its
+ * base: util.inherits, `Base.call(this, …)` and `Base.apply(this, …)`,
+ * `Base.prototype` and `instanceof Base` on an untyped value. */
+
+/** scr_dyn_builtin_class's kind for a runtime class, or null. */
+export function builtinClassKind(className: string): number | null {
+ switch (className) {
+ case RUNTIME_EMITTER_CLASS:
+ return 0;
+ case "%Readable":
+ return 2;
+ case "%Writable":
+ return 3;
+ case "%Duplex":
+ return 4;
+ case "%Transform":
+ return 5;
+ case "%PassThrough":
+ return 6;
+ default:
+ return null;
+ }
+}
+
+/** The classes by builtin module member. JavaScript sources bind
+ * `require("events").EventEmitter` as a module member rather than a class
+ * value, and `require("stream")` is the legacy Stream constructor. */
+const MODULE_CLASS_KINDS: Readonly>>> = {
+ events: { EventEmitter: 0, default: 0 },
+ stream: { Stream: 1, Readable: 2, Writable: 3, Duplex: 4, Transform: 5, PassThrough: 6 },
+};
+
+/** The kind of a builtin module member that is one of those classes. */
+export function builtinMemberClassKind(module: string, member: string): number | null {
+ return MODULE_CLASS_KINDS[module]?.[member] ?? null;
+}
+
+function unparenthesized(node: ts.Expression): ts.Expression {
+ let current = node;
+ while (ts.isParenthesizedExpression(current)) current = current.expression;
+ return current;
+}
+
+/** The kind of a node that names one of those classes: a class value, a
+ * builtin module member, or the events and stream modules themselves. */
+export function builtinClassKindOf(lowerer: Lowerer, node: ts.Expression): number | null {
+ const expr = unparenthesized(node);
+ const mapped = lowerer.mapTypeOf(lowerer.typeOf(expr));
+ if (mapped?.kind === "classval") return builtinClassKind(mapped.className);
+ const member = ts.isIdentifier(expr)
+ ? lowerer.builtinImportOf(expr)
+ : ts.isPropertyAccessExpression(expr)
+ ? lowerer.builtinMemberOf(expr)
+ : null;
+ if (member) return MODULE_CLASS_KINDS[member.module]?.[member.member] ?? null;
+ const module = lowerer.builtinNamespaceModuleOf(expr);
+ return module === "events" ? 0 : module === "stream" ? 1 : null;
+}
+
+/** Whether evaluating the node can be skipped: a binding or a member of
+ * one, or of a builtin require. */
+function referenceOnly(node: ts.Expression): boolean {
+ const expr = unparenthesized(node);
+ if (ts.isIdentifier(expr)) return true;
+ if (ts.isPropertyAccessExpression(expr) && !expr.questionDotToken) {
+ const base = unparenthesized(expr.expression);
+ return (
+ ts.isIdentifier(base) ||
+ (ts.isCallExpression(base) &&
+ ts.isIdentifier(base.expression) &&
+ base.expression.text === "require" &&
+ base.arguments.nodes.length === 1 &&
+ ts.isStringLiteral(base.arguments.nodes[0]!))
+ );
+ }
+ return false;
+}
+
+/** The untyped constructor named by a node, or null when it names none of
+ * those classes. */
+export function builtinClassDynValueOf(lowerer: Lowerer, node: ts.Expression): IrExpr | null {
+ const kind = builtinClassKindOf(lowerer, node);
+ if (kind === null) return null;
+ const loc = locOf(node);
+ const value = builtinClassValue(kind, loc);
+ if (referenceOnly(node)) return value;
+ return {
+ kind: "seqExpr",
+ stmts: [{ kind: "exprStmt", expr: lowerer.lowerExpr(node), loc }],
+ result: value,
+ type: DYN,
+ loc,
+ };
+}
+
+export function builtinClassValue(kind: number, loc: SrcLoc): IrExpr {
+ return { kind: "libCall", fn: "dyn.builtinClass", args: [numLit(kind, loc)], type: DYN, loc };
+}
+
+/** `Base.call(this, …)` / `Base.apply(this, args)` on one of those class
+ * values: Function.prototype.call/apply over the untyped constructor. */
+export function lowerBuiltinClassCall(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null {
+ const callee = call.expression;
+ if (!ts.isPropertyAccessExpression(callee) || callee.questionDotToken) return null;
+ const method = callee.name.text;
+ if (method !== "call" && method !== "apply") return null;
+ if (call.arguments.nodes.some(ts.isSpreadElement)) return null;
+ const receiver = builtinClassDynValueOf(lowerer, callee.expression);
+ if (receiver === null) return null;
+ const loc = locOf(call);
+ return {
+ kind: "dynInvoke",
+ recv: receiver,
+ method,
+ calleeName: callee.getText(),
+ args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)),
+ type: DYN,
+ loc,
+ };
+}
+
+/** `value instanceof Base` for an untyped value. */
+export function builtinClassInstanceOf(value: IrExpr, kind: number, loc: SrcLoc): IrExpr {
+ return {
+ kind: "libCall",
+ fn: "dyn.builtinInstanceOf",
+ args: [value, numLit(kind, loc)],
+ type: BOOL,
+ loc,
+ };
+}
diff --git a/packages/compiler/src/frontend/lowering/builtins/child-process-methods.ts b/packages/compiler/src/frontend/lowering/builtins/child-process-methods.ts
index a435bf740..e1a25d169 100644
--- a/packages/compiler/src/frontend/lowering/builtins/child-process-methods.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/child-process-methods.ts
@@ -1,5 +1,6 @@
import { boolLit, strLit } from "../../../ir/build.js";
import * as ts from "../../ts7/adapter.js";
+import { requestGeneralStreamSurface } from "../general-streams.js";
import { type Lowerer } from "../lowerer.js";
import { isJsSourceFile, locOf } from "../../program.js";
import { isChildSurfaceMember } from "../surfaces.js";
@@ -131,8 +132,16 @@ export function lowerChildMethodCall(
) {
for (const param of listenerNode.parameters.nodes) lowerer.checkedCallbackParams.add(param);
}
- const cb = lowerer.lowerExpr(call.arguments.nodes[1]!);
+ const narrowed =
+ event === "exit" || event === "close"
+ ? narrowedExitListenerParams(lowerer, listenerNode)
+ : [];
+ const cb =
+ narrowed.length > 0
+ ? lowerWithCheckedParams(lowerer, listenerNode, narrowed)
+ : lowerer.lowerExpr(call.arguments.nodes[1]!);
if (
+ narrowed.length > 0 ||
(isJsSourceFile(listenerNode.getSourceFile()) && cb.type.kind === "dyn") ||
(cb.type.kind === "func" &&
(cb.type.rest ||
@@ -301,10 +310,12 @@ export function lowerChildMethodCall(
/** Method calls on piped child-output stream receivers (child.stdout /
* child.stderr — the childStream kind): on/once("data" | "end").
- * 'data' listeners take zero parameters, `(chunk: Buffer)`, or a
- * `Buffer | string`-union chunk (the ngrok appendOutput shape — the
- * runtime only ever fires Buffers; the compiler-emitted adapter wraps
- * the chunk at the union's Buffer arm); 'end' listeners take none.
+ * 'data' listeners take zero parameters, `(chunk: Buffer)`, `(chunk:
+ * string)`, an unknown/any chunk (boxed as a checked-dynamic value; see
+ * childStreamEncodedAt), or a `Buffer | string`-union chunk (the ngrok
+ * appendOutput shape — the runtime only ever fires Buffers; the
+ * compiler-emitted adapter wraps the chunk at the union's Buffer arm);
+ * 'end' listeners take none.
* Statement position only (Node returns the stream for chaining; here
* the result is void). Chained receivers (`child.stdout?.on(...)`)
* ride the optional-chain re-dispatch (chainBlocked). Everything else
@@ -335,6 +346,7 @@ export function lowerChildStreamMethodCall(
const evT = lowerer.typeOf(call.arguments.nodes[0]!);
const event = evT.isStringLiteralType() ? evT.value : null;
if (event !== "data" && event !== "end") {
+ requestGeneralStreamSurface(lowerer, access.expression);
lowerer.noLowering(
`stream.${name}(${event === null ? "non-literal event" : `"${event}"`}, ...)`,
call.arguments.nodes[0]!,
@@ -342,6 +354,7 @@ export function lowerChildStreamMethodCall(
);
}
if (!ts.isExpressionStatement(call.parent)) {
+ requestGeneralStreamSurface(lowerer, access.expression);
lowerer.unsupported(
"SC1090",
call,
@@ -376,23 +389,32 @@ export function lowerChildStreamMethodCall(
if (
param !== undefined &&
param.kind !== "string" &&
+ param.kind !== "dyn" &&
!(param.kind === "bytes" && param.elem === "u8") &&
!unionOk(param)
) {
lowerer.unsupported(
"SC1090",
call.arguments.nodes[1]!,
- `data listeners whose parameter is not 'Buffer', 'string', or a Buffer-armed union (got '${lowerer.fmt(param)}')`,
+ `data listeners whose parameter is not 'Buffer', 'string', 'unknown', or a Buffer-armed union (got '${lowerer.fmt(param)}')`,
);
}
+ // An unknown/any chunk is a Buffer, or a string once setEncoding ran.
+ // The registration takes whichever the enclosing code establishes; a
+ // later setEncoding that contradicts it throws the runtime's
+ // listener-type TypeError instead of delivering the other kind.
+ const strings =
+ param?.kind === "string" ||
+ (param?.kind === "dyn" && childStreamEncodedAt(lowerer, access.expression, call));
return {
kind: "libCall",
- fn: param?.kind === "string" ? "stream.onDataStr" : "stream.onData",
+ fn: strings ? "stream.onDataStr" : "stream.onData",
args: [receiver, cb, once],
type: VOID,
loc,
};
}
+ requestGeneralStreamSurface(lowerer, access.expression);
lowerer.noLowering(
`ReadableStream.${name}`,
call,
@@ -418,6 +440,7 @@ export function lowerChildWriterMethodCall(
if (name === "write") {
if (call.arguments.nodes.length !== 1 || call.arguments.nodes.some(ts.isSpreadElement)) {
+ requestGeneralStreamSurface(lowerer, access.expression);
lowerer.noLowering(
`child stdin write with ${call.arguments.nodes.length} arguments`,
call,
@@ -461,6 +484,7 @@ export function lowerChildWriterMethodCall(
const eventType = lowerer.typeOf(call.arguments.nodes[0]!);
const event = eventType.isStringLiteralType() ? eventType.value : null;
if (event !== "drain" && event !== "finish" && event !== "error") {
+ requestGeneralStreamSurface(lowerer, access.expression);
lowerer.noLowering(
`child stdin ${name}(${event === null ? "non-literal event" : `"${event}"`}, ...)`,
call.arguments.nodes[0]!,
@@ -507,6 +531,7 @@ export function lowerChildWriterMethodCall(
return { kind: "libCall", fn, args: [receiver, cb, once], type: VOID, loc };
}
+ requestGeneralStreamSurface(lowerer, access.expression);
lowerer.noLowering(
`Writable.${name}`,
call,
@@ -514,3 +539,255 @@ export function lowerChildWriterMethodCall(
lowerer.checker.getSymbolAtLocation(access.name),
);
}
+
+/** A receiver spelled as a plain access path (`child.stdout`, `out`): its
+ * root symbol and member names, or null for other forms. A `const` alias
+ * of an access path (`const out = child.stdout`) resolves to that path,
+ * and so does a method call declared to return `this`
+ * (`child.stdout.setEncoding("utf8")`), so every spelling of one stream
+ * gets the same key. */
+function accessPathOf(
+ lowerer: Lowerer,
+ expr: ts.Expression,
+ depth = 0,
+): { root: ts.Symbol; path: string } | null {
+ if (depth > 16) return null;
+ if (ts.isParenthesizedExpression(expr) || ts.isNonNullExpression(expr)) {
+ return accessPathOf(lowerer, expr.expression, depth + 1);
+ }
+ if (ts.isIdentifier(expr)) {
+ const root = lowerer.checker.getSymbolAtLocation(expr);
+ if (!root) return null;
+ const init = constInitializerOf(root);
+ const aliased = init === null ? null : accessPathOf(lowerer, init, depth + 1);
+ return aliased ?? { root, path: "" };
+ }
+ if (ts.isPropertyAccessExpression(expr) && !expr.questionDotToken) {
+ const base = accessPathOf(lowerer, expr.expression, depth + 1);
+ return base === null ? null : { root: base.root, path: `${base.path}.${expr.name.text}` };
+ }
+ if (returnsReceiver(lowerer, expr)) {
+ return accessPathOf(
+ lowerer,
+ (expr.expression as ts.PropertyAccessExpression).expression,
+ depth + 1,
+ );
+ }
+ return null;
+}
+
+/** A method call (`recv.m(...)`, not optional) whose declaration returns
+ * `this`, so its value is the receiver itself. */
+function returnsReceiver(lowerer: Lowerer, expr: ts.Expression): expr is ts.CallExpression {
+ if (
+ !ts.isCallExpression(expr) ||
+ expr.questionDotToken ||
+ !ts.isPropertyAccessExpression(expr.expression) ||
+ expr.expression.questionDotToken
+ )
+ return false;
+ const declaration = lowerer.checker.getResolvedSignature(expr)?.declaration;
+ return declaration?.type?.kind === ts.SyntaxKind.ThisType;
+}
+
+/** A call of a child_process factory (spawn, fork, exec, execFile), each
+ * of which returns a new ChildProcess. */
+function createsChildProcess(lowerer: Lowerer, call: ts.CallExpression): boolean {
+ const declaration = lowerer.checker.getResolvedSignature(call)?.declaration;
+ if (
+ declaration === undefined ||
+ !ts.isFunctionDeclaration(declaration) ||
+ declaration.name === undefined ||
+ !["spawn", "fork", "exec", "execFile"].includes(declaration.name.text)
+ )
+ return false;
+ for (let node: ts.Node | undefined = declaration.parent; node; node = node.parent) {
+ if (ts.isModuleDeclaration(node) && ts.isStringLiteral(node.name)) {
+ return node.name.text === "child_process" || node.name.text === "node:child_process";
+ }
+ }
+ return false;
+}
+
+/** The statement that always evaluates `call` when it runs: the call is
+ * the statement's expression, a variable's initializer, or the receiver of
+ * a method chain that is one of those. Null for calls under a condition,
+ * an operator, an argument list or any other construct. */
+function unconditionalStatementOf(call: ts.CallExpression): ts.Statement | null {
+ let node: ts.Node = call;
+ for (;;) {
+ const parent: ts.Node | undefined = node.parent;
+ if (parent === undefined) return null;
+ if (
+ ts.isParenthesizedExpression(parent) ||
+ ts.isNonNullExpression(parent) ||
+ (ts.isPropertyAccessExpression(parent) &&
+ parent.expression === node &&
+ !parent.questionDotToken) ||
+ (ts.isCallExpression(parent) && parent.expression === node && !parent.questionDotToken)
+ ) {
+ node = parent;
+ continue;
+ }
+ if (ts.isExpressionStatement(parent)) return parent;
+ if (ts.isVariableDeclaration(parent) && parent.initializer === node) {
+ const list = parent.parent;
+ const statement = list?.parent;
+ return list !== undefined &&
+ ts.isVariableDeclarationList(list) &&
+ statement !== undefined &&
+ ts.isVariableStatement(statement)
+ ? statement
+ : null;
+ }
+ return null;
+ }
+}
+
+/** The initializer of a symbol declared once as a `const` variable, else null. */
+function constInitializerOf(symbol: ts.Symbol): ts.Expression | null {
+ const decls = symbol.declarations;
+ if (decls.length !== 1) return null;
+ const decl = decls[0]!;
+ if (
+ !ts.isVariableDeclaration(decl) ||
+ !ts.isIdentifier(decl.name) ||
+ decl.initializer === undefined ||
+ (ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) === 0
+ )
+ return null;
+ return decl.initializer;
+}
+
+/** Whether two resolved access paths name different objects: the same root
+ * with different members, or two `const` roots each initialized by `new`
+ * or a child_process factory (such as two `spawn()` results). Any other
+ * call may return an object another binding already holds. */
+function distinctPaths(
+ lowerer: Lowerer,
+ a: { root: ts.Symbol; path: string },
+ b: { root: ts.Symbol; path: string },
+): boolean {
+ if (a.root === b.root) return a.path !== b.path;
+ const fresh = (root: ts.Symbol): boolean => {
+ let init = constInitializerOf(root);
+ while (init !== null && ts.isParenthesizedExpression(init)) init = init.expression;
+ if (init === null) return false;
+ return (
+ ts.isNewExpression(init) || (ts.isCallExpression(init) && createsChildProcess(lowerer, init))
+ );
+ };
+ return fresh(a.root) && fresh(b.root);
+}
+
+/** Whether an unknown-typed 'data' listener registered by `call` receives
+ * strings: a `setEncoding` statement on the same receiver path precedes
+ * the registration in its own block or an enclosing block of the same
+ * function. With no setEncoding on that path in the function, chunks are
+ * Buffers. Any other placement leaves the encoding state unclear, which
+ * is reported. */
+function childStreamEncodedAt(
+ lowerer: Lowerer,
+ receiver: ts.Expression,
+ call: ts.CallExpression,
+): boolean {
+ const key = accessPathOf(lowerer, receiver);
+ const statement: ts.Node = call.parent!;
+ let scope: ts.Node | undefined = statement;
+ while (scope && !ts.isFunctionLike(scope) && !ts.isSourceFile(scope)) scope = scope.parent;
+ const unclear = (): never =>
+ lowerer.unsupported(
+ "SC1090",
+ call.arguments.nodes[1]!,
+ "untyped child-stream 'data' listeners whose setEncoding state is unclear at their registration sites (declare the chunk as 'Buffer' or 'string')",
+ );
+ if (key === null || scope === undefined) return unclear();
+ const preceding = new Set();
+ for (let node: ts.Node = statement; node !== scope;) {
+ const parent: ts.Node | undefined = node.parent;
+ if (parent === undefined) break;
+ if (ts.isBlock(parent) || ts.isSourceFile(parent)) {
+ for (const sibling of parent.statements.nodes) {
+ if (sibling === node) break;
+ preceding.add(sibling);
+ }
+ }
+ node = parent;
+ }
+ const receiverType = lowerer.checker.getNonNullableType(lowerer.typeOf(receiver));
+ let before = false;
+ let elsewhere = false;
+ const visit = (node: ts.Node): void => {
+ if (node !== scope && ts.isFunctionLike(node)) return;
+ if (
+ ts.isCallExpression(node) &&
+ ts.isPropertyAccessExpression(node.expression) &&
+ node.expression.name.text === "setEncoding"
+ ) {
+ const target = node.expression.expression;
+ const path = accessPathOf(lowerer, target);
+ if (path !== null && path.root === key.root && path.path === key.path) {
+ const statement = unconditionalStatementOf(node);
+ if (statement !== null && preceding.has(statement)) before = true;
+ else elsewhere = true;
+ } else if (
+ (path === null || !distinctPaths(lowerer, path, key)) &&
+ lowerer.checker.isTypeAssignableTo(receiverType, lowerer.typeOf(target))
+ ) {
+ // Another spelling that can hold this stream (a parameter, a `let`
+ // copy, a call result): its setEncoding may be this stream's.
+ elsewhere = true;
+ }
+ }
+ ts.forEachChild(node, visit);
+ };
+ visit(scope);
+ if (elsewhere) return unclear();
+ return before;
+}
+
+/** An inline 'exit'/'close' listener may declare `(code: number, signal:
+ * string)`: TypeScript accepts it through ChildProcess's catch-all `on`
+ * overload, while Node passes `number | null` and `Signals | null`. Those
+ * parameters bind as checked-dynamic values (the socket 'data' listener
+ * precedent), so the body observes the null Node passes; a typed use of a
+ * null value throws the checked conversion's TypeError. Returns the
+ * narrowed parameter symbols, empty when the declared types already
+ * admit null. */
+function narrowedExitListenerParams(lowerer: Lowerer, listener: ts.Expression): ts.Symbol[] {
+ if (!ts.isArrowFunction(listener) && !ts.isFunctionExpression(listener)) return [];
+ if (isJsSourceFile(listener.getSourceFile())) return [];
+ const symbols: ts.Symbol[] = [];
+ const wanted: readonly string[] = ["f64", "string"];
+ listener.parameters.nodes.slice(0, 2).forEach((param, i) => {
+ if (!ts.isIdentifier(param.name) || param.dotDotDotToken || param.initializer) return;
+ if (param.questionToken) return;
+ const symbol = lowerer.checker.getSymbolAtLocation(param.name);
+ if (!symbol || lowerer.runtimeOptionalBindingTypes.has(symbol)) return;
+ if (lowerer.paramShape(param).type.kind !== wanted[i]) return;
+ symbols.push(symbol);
+ });
+ return symbols;
+}
+
+/** Lowers an inline listener with the given parameters bound as
+ * checked-dynamic values typed `unknown` for the body. */
+function lowerWithCheckedParams(
+ lowerer: Lowerer,
+ listener: ts.Expression,
+ symbols: readonly ts.Symbol[],
+): IrExpr {
+ const previousImplicit = lowerer.implicitParamTypes;
+ const implicit = new Map(previousImplicit ?? []);
+ for (const symbol of symbols) {
+ implicit.set(symbol, lowerer.checker.getUnknownType());
+ lowerer.runtimeOptionalBindingTypes.set(symbol, DYN);
+ }
+ lowerer.implicitParamTypes = implicit;
+ try {
+ return lowerer.lowerExpr(listener);
+ } finally {
+ for (const symbol of symbols) lowerer.runtimeOptionalBindingTypes.delete(symbol);
+ lowerer.implicitParamTypes = previousImplicit;
+ }
+}
diff --git a/packages/compiler/src/frontend/lowering/builtins/errors.ts b/packages/compiler/src/frontend/lowering/builtins/errors.ts
index a8b4dbcd6..439c9fca3 100644
--- a/packages/compiler/src/frontend/lowering/builtins/errors.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/errors.ts
@@ -52,6 +52,20 @@ export function refuseExecExceptionCode(lowerer: Lowerer, node: ts.Node): never
* declaration is not stdlib). Reads only: writes keep their fence (no
* compiled program constructs an errno error). Null for non-error
* receivers and non-stdlib members, so the chain keeps trying. */
+/** Error members with slots of their own: never read through the dynamic
+ * node, even when an intersection restates them. */
+const ERROR_SLOT_MEMBERS = new Set([
+ "code",
+ "cause",
+ "stack",
+ "message",
+ "name",
+ "errno",
+ "syscall",
+ "path",
+ "dest",
+]);
+
export function lowerErrorCodeProperty(
lowerer: Lowerer,
expr: ts.PropertyAccessExpression,
@@ -135,6 +149,33 @@ export function lowerErrorCodeProperty(
declared,
);
}
+ // A member an `Error & { ... }` intersection adds beyond Error's own and
+ // the system-error metadata (`Object.assign(new Error(m), { status })`):
+ // an own property of the error's checked-dynamic node, read at the
+ // declared type (the intersection mapping admits only checkable types).
+ if (
+ recvT.className === "%Error" &&
+ !ERROR_SLOT_MEMBERS.has(expr.name.text) &&
+ !expr.questionDotToken &&
+ !lowerer.isStdlibMember(expr) &&
+ lowerer.typeOf(expr.expression).isIntersectionType()
+ ) {
+ const declared = lowerer.mapTypeOf(lowerer.typeOf(expr));
+ if (declared && declared.kind !== "void") {
+ const receiver = lowerer.lowerExpr(expr.expression);
+ const loc = locOf(expr);
+ return lowerer.coerceToExpected(
+ {
+ kind: "dynKeyGet",
+ value: lowerer.coerceToExpected(receiver, DYN),
+ key: { kind: "strLit", value: expr.name.text, type: STRING, loc },
+ type: DYN,
+ loc,
+ },
+ declared,
+ );
+ }
+ }
// A program class under the receiver's class can carry its own `code`
// (a declared field or a property added through an untyped reference),
// which Node reads instead of the errno code: read through the error's
diff --git a/packages/compiler/src/frontend/lowering/builtins/performance.ts b/packages/compiler/src/frontend/lowering/builtins/performance.ts
index 2a9962b0f..7aac3b80c 100644
--- a/packages/compiler/src/frontend/lowering/builtins/performance.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/performance.ts
@@ -18,6 +18,7 @@ import {
} from "../../../ir/ir.js";
import { PERF_ENTRY_FIELDS, PERF_ENTRY_MARKER, perfEntryJsonShape } from "../../type-mapper.js";
import { builtinImportOf } from "./module-bindings.js";
+import { lowerOptionalArgument } from "../optional-arguments.js";
import { checkedIterableSpread } from "../checked-iterable-spread.js";
/** True for any spelling of node:perf_hooks' `performance` object — the
@@ -134,6 +135,12 @@ export function lowerPerfHooksCall(
loc,
};
}
+ if (
+ access.name.text === "eventLoopUtilization" &&
+ isPerfHooksPerformanceExpr(lowerer, access.expression)
+ ) {
+ return lowerEventLoopUtilization(lowerer, expr);
+ }
const timeline = PERF_TIMELINE_CALLS.get(access.name.text);
if (timeline && isPerfHooksPerformanceExpr(lowerer, access.expression)) {
return lowerPerfTimelineCall(lowerer, expr, access.name.text, timeline, loc);
@@ -142,7 +149,7 @@ export function lowerPerfHooksCall(
lowerer.noLowering(
`perf_hooks performance.${access.name.text}`,
expr,
- "performance.now() (and its .bind(performance) function value), mark, measure, the getEntries family, clearMarks and clearMeasures are the lowered surface",
+ "performance.now() (and its .bind(performance) function value), mark, measure, the getEntries family, clearMarks, clearMeasures and eventLoopUtilization are the lowered surface",
);
}
return null;
@@ -513,3 +520,305 @@ export function fencePerfEntryKeyWalk(
`${what} over performance entries (Node keeps their fields as prototype getters, so own-key walks see none; read the fields or call toJSON())`,
);
}
+
+/* ── event-loop metrics: eventLoopUtilization and monitorEventLoopDelay ── */
+
+/** The stdlib IntervalHistogram (monitorEventLoopDelay's result), by the
+ * receiver's checker type. Its value is the runtime histogram handle. */
+function isIntervalHistogram(lowerer: Lowerer, node: ts.Expression): boolean {
+ const symbol = lowerer.typeOf(node).getSymbol();
+ return symbol?.name === "IntervalHistogram" && lowerer.isStdlibSymbol(symbol);
+}
+
+/** IntervalHistogram's numeric members, as perf.eldStat selectors. */
+const HISTOGRAM_STATS: ReadonlyMap = new Map([
+ ["min", 0],
+ ["max", 1],
+ ["mean", 2],
+ ["stddev", 3],
+ ["count", 4],
+ ["exceeds", 5],
+]);
+
+/** `h.min`, `h.max`, … on an IntervalHistogram: the runtime's current
+ * statistic (Node's hdr_histogram values; nanoseconds). Null for every
+ * other read. */
+export function lowerHistogramProperty(
+ lowerer: Lowerer,
+ expr: ts.PropertyAccessExpression,
+): IrExpr | null {
+ const which = HISTOGRAM_STATS.get(expr.name.text);
+ if (
+ which === undefined ||
+ expr.questionDotToken ||
+ !isIntervalHistogram(lowerer, expr.expression)
+ )
+ return null;
+ const loc = locOf(expr);
+ return {
+ kind: "libCall",
+ fn: "perf.eldStat",
+ args: [lowerer.lowerExprExpecting(expr.expression, F64), numLit(which, loc)],
+ type: F64,
+ loc,
+ };
+}
+
+/** enable(), disable(), reset() and percentile(p) on an IntervalHistogram.
+ * Other members (the BigInt twins, the percentiles Map) fence by name. */
+export function lowerHistogramMethodCall(
+ lowerer: Lowerer,
+ expr: ts.CallExpression,
+ access: ts.PropertyAccessExpression,
+): IrExpr | null {
+ if (access.questionDotToken || !isIntervalHistogram(lowerer, access.expression)) return null;
+ const loc = locOf(expr);
+ const name = access.name.text;
+ const handle = (): IrExpr => lowerer.lowerExprExpecting(access.expression, F64);
+ const args = expr.arguments.nodes;
+ if ((name === "enable" || name === "disable" || name === "reset") && args.length === 0) {
+ const fn =
+ name === "enable"
+ ? "perf.eldEnable"
+ : name === "disable"
+ ? "perf.eldDisable"
+ : "perf.eldReset";
+ return { kind: "libCall", fn, args: [handle()], type: name === "reset" ? VOID : BOOL, loc };
+ }
+ if (name === "percentile" && args.length === 1 && !ts.isSpreadElement(args[0]!)) {
+ // Node validates first: NaN, p <= 0 and p > 100 throw ERR_OUT_OF_RANGE.
+ const h = lowerer.declareHiddenLocal("%histogram", F64);
+ const p = lowerer.declareHiddenLocal("%percentile", F64);
+ const pRef = (): IrExpr => varRef(p.id, F64, loc);
+ const cmp = (op: "!==" | "<=" | ">", left: IrExpr, right: IrExpr): IrExpr => ({
+ kind: "bin",
+ op,
+ left,
+ right,
+ type: BOOL,
+ loc,
+ });
+ const or = (left: IrExpr, right: IrExpr): IrExpr => ({
+ kind: "logical",
+ op: "||",
+ left,
+ right,
+ type: BOOL,
+ loc,
+ });
+ const outOfRange = or(
+ or(cmp("!==", pRef(), pRef()), cmp("<=", pRef(), numLit(0, loc))),
+ cmp(">", pRef(), numLit(100, loc)),
+ );
+ const message: IrExpr = {
+ kind: "strConcat",
+ left: {
+ kind: "strLit",
+ value: 'The value of "percentile" is out of range. It must be > 0 && <= 100. Received ',
+ type: STRING,
+ loc,
+ },
+ right: { kind: "toString", operand: pRef(), type: STRING, loc },
+ type: STRING,
+ loc,
+ };
+ const thrown: IrExpr = {
+ kind: "libCall",
+ fn: "error.nodeThrow",
+ args: [
+ numLit(2, loc),
+ { kind: "strLit", value: "ERR_OUT_OF_RANGE", type: STRING, loc },
+ message,
+ ],
+ type: F64,
+ loc,
+ };
+ return {
+ kind: "seqExpr",
+ stmts: [
+ { kind: "varDecl", localId: h.id, init: handle(), loc },
+ { kind: "varDecl", localId: p.id, init: lowerer.lowerExprExpecting(args[0]!, F64), loc },
+ ],
+ result: {
+ kind: "ternary",
+ cond: outOfRange,
+ then: thrown,
+ else_: {
+ kind: "libCall",
+ fn: "perf.eldPercentile",
+ args: [varRef(h.id, F64, loc), pRef()],
+ type: F64,
+ loc,
+ },
+ type: F64,
+ loc,
+ },
+ type: F64,
+ loc,
+ };
+ }
+ lowerer.noLowering(
+ `IntervalHistogram.${name}`,
+ expr,
+ "enable(), disable(), reset(), percentile(p) and the min, max, mean, stddev, count and exceeds readings are the lowered surface",
+ );
+}
+
+/** `monitorEventLoopDelay(options?)` from node:perf_hooks, in any import
+ * spelling (`lowerBuiltinCall`): a new sampling histogram handle. The
+ * options are an inline object literal; the runtime validates the
+ * resolution as Node does (an integer of at least 1). */
+export function lowerMonitorEventLoopDelayCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
+ const loc = locOf(expr);
+ const args = expr.arguments.nodes;
+ let resolution: IrExpr = numLit(10, loc);
+ if (args.length > 1 || args.some(ts.isSpreadElement)) {
+ lowerer.noLowering(
+ "this monitorEventLoopDelay call form",
+ expr,
+ "pass at most one options object",
+ );
+ }
+ if (args.length === 1) {
+ const options = args[0]!;
+ if (!ts.isObjectLiteralExpression(options)) {
+ lowerer.noLowering(
+ "monitorEventLoopDelay with a non-literal options object",
+ options,
+ "spell the options inline: monitorEventLoopDelay({ resolution: 20 })",
+ );
+ }
+ for (const property of options.properties.nodes) {
+ const shorthand = ts.isShorthandPropertyAssignment(property);
+ if (
+ (!shorthand && !ts.isPropertyAssignment(property)) ||
+ !ts.isIdentifier(property.name) ||
+ property.name.text !== "resolution"
+ ) {
+ lowerer.noLowering(
+ "monitorEventLoopDelay options other than resolution",
+ property,
+ "resolution is the one option",
+ );
+ }
+ // An undefined resolution takes the default, as Node's destructuring.
+ resolution = lowerOptionalArgument(
+ lowerer,
+ shorthand ? property.name : property.initializer,
+ F64,
+ numLit(10, loc),
+ );
+ }
+ }
+ return { kind: "libCall", fn: "perf.eldNew", args: [resolution], type: F64, loc };
+}
+
+/** performance.eventLoopUtilization(u1?, u2?) — Node's
+ * lib/internal/perf/event_loop_utilization.js over the runtime's loop
+ * start and idle time, one lifted helper per arity. */
+function lowerEventLoopUtilization(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
+ const loc = locOf(expr);
+ const args = expr.arguments.nodes;
+ const resultT = lowerer.mapTypeOf(lowerer.typeOf(expr));
+ if (resultT?.kind !== "record" || args.length > 2 || args.some(ts.isSpreadElement)) {
+ lowerer.noLowering("this performance.eventLoopUtilization call form", expr);
+ }
+ const helper = eluHelperOf(lowerer, args.length, resultT);
+ return {
+ kind: "call",
+ callee: helper,
+ args: args.map((arg) => lowerer.lowerExprExpecting(arg, resultT)),
+ type: resultT,
+ loc,
+ };
+}
+
+function eluHelperOf(lowerer: Lowerer, arity: number, recT: IrType & { kind: "record" }): string {
+ const name = `%perf.elu.${arity}`;
+ if (lowerer.liftedFns.some((f) => f.name === name)) return name;
+ const loc: SrcLoc = { file: "", start: 0, end: 0 };
+ const f64Local = (id: string) => ({ id, name: id, type: F64, mutable: false });
+ const params = Array.from({ length: arity }, (_, i) => ({
+ localId: `u${i + 1}.0`,
+ name: `u${i + 1}`,
+ type: recT as IrType,
+ }));
+ const locals = [
+ ...params.map((p) => ({ id: p.localId, name: p.name, type: recT as IrType, mutable: false })),
+ f64Local("ls.0"),
+ f64Local("idle.0"),
+ f64Local("active.0"),
+ ];
+ const ref = (id: string): IrExpr => varRef(id, F64, loc);
+ const field = (param: string, f: string): IrExpr => ({
+ kind: "recordGet",
+ obj: varRef(param, recT, loc),
+ shapeId: recT.shapeId,
+ field: f,
+ type: F64,
+ loc,
+ });
+ const bin = (op: "+" | "-" | "/" | "<=", left: IrExpr, right: IrExpr): IrExpr => ({
+ kind: "bin",
+ op,
+ left,
+ right,
+ type: op === "<=" ? BOOL : F64,
+ loc,
+ });
+ const record = (idle: IrExpr, active: IrExpr, utilization: IrExpr): IrExpr => ({
+ kind: "recordLit",
+ fields: [
+ { name: "idle", value: idle },
+ { name: "active", value: active },
+ { name: "utilization", value: utilization },
+ ],
+ type: recT,
+ loc,
+ });
+ const result = (idle: IrExpr, active: IrExpr): IrStmt => ({
+ kind: "return",
+ value: record(idle, active, bin("/", active, bin("+", idle, active))),
+ loc,
+ });
+ const decl = (id: string, init: IrExpr): IrStmt => ({ kind: "varDecl", localId: id, init, loc });
+ const body: IrStmt[] = [
+ decl("ls.0", { kind: "libCall", fn: "perf.loopStart", args: [], type: F64, loc }),
+ {
+ kind: "if",
+ cond: bin("<=", ref("ls.0"), numLit(0, loc)),
+ then: [
+ { kind: "return", value: record(numLit(0, loc), numLit(0, loc), numLit(0, loc)), loc },
+ ],
+ else_: null,
+ loc,
+ },
+ ];
+ if (arity === 2) {
+ body.push(
+ decl("idle.0", bin("-", field("u1.0", "idle"), field("u2.0", "idle"))),
+ decl("active.0", bin("-", field("u1.0", "active"), field("u2.0", "active"))),
+ result(ref("idle.0"), ref("active.0")),
+ );
+ } else {
+ body.push(
+ decl("idle.0", { kind: "libCall", fn: "perf.idleTime", args: [], type: F64, loc }),
+ decl(
+ "active.0",
+ bin(
+ "-",
+ bin("-", { kind: "libCall", fn: "perf.now", args: [], type: F64, loc }, ref("ls.0")),
+ ref("idle.0"),
+ ),
+ ),
+ arity === 0
+ ? result(ref("idle.0"), ref("active.0"))
+ : result(
+ bin("-", ref("idle.0"), field("u1.0", "idle")),
+ bin("-", ref("active.0"), field("u1.0", "active")),
+ ),
+ );
+ }
+ lowerer.liftedFns.push({ name, params, returnType: recT, locals, body, loc });
+ return name;
+}
diff --git a/packages/compiler/src/frontend/lowering/builtins/process.ts b/packages/compiler/src/frontend/lowering/builtins/process.ts
index eb3703a53..9ac1fc059 100644
--- a/packages/compiler/src/frontend/lowering/builtins/process.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/process.ts
@@ -3,6 +3,7 @@ import { envSnapshotHelper, lowerProcessLoadEnvFile } from "./environment.js";
import { dynUndefinedExpr, boolLit, numLit, strLit, varRef } from "../../../ir/build.js";
import { timerStyleCallback } from "../lower-timers.js";
import * as ts from "../../ts7/adapter.js";
+import { requestGeneralStreamSurface } from "../general-streams.js";
import { type Lowerer, own } from "../lowerer.js";
import { isJsSourceFile, locOf } from "../../program.js";
import { isSafeToDiscard } from "../expressions/evaluation-safety.js";
@@ -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,
diff --git a/packages/compiler/src/frontend/lowering/builtins/properties.ts b/packages/compiler/src/frontend/lowering/builtins/properties.ts
index b0caa0456..e60a14c0c 100644
--- a/packages/compiler/src/frontend/lowering/builtins/properties.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/properties.ts
@@ -1,5 +1,5 @@
import { isBuiltinClassInstance } from "./receiver-types.js";
-import { strLit } from "../../../ir/build.js";
+import { numLit, strLit } from "../../../ir/build.js";
import * as ts from "../../ts7/adapter.js";
import { type Lowerer } from "../lowerer.js";
import { locOf } from "../../program.js";
@@ -192,6 +192,25 @@ export function lowerBuiltinExtraProperty(
// +1-or-NULL runtime pair.
if (kind === "child" && (name === "stdin" || name === "stdout" || name === "stderr")) {
const receiver = lowerer.lowerExpr(expr.expression);
+ // Under the general stream representation (general-streams.ts) the
+ // declared stream type maps to a runtime stream class: the read
+ // answers the child's bridged stream (one per stdio slot, so repeated
+ // reads keep identity), fed by the same pipe.
+ const general = lowerer.generalNodeStreams ? generalChildStdioType(lowerer, expr, name) : null;
+ if (general) {
+ const type: IrType = {
+ kind: "union",
+ unionId: lowerer.unions.intern([general, { kind: "nullT" }]),
+ };
+ const read: IrExpr = {
+ kind: "libCall",
+ fn: "child.stdioStream",
+ args: [receiver, numLit(name === "stdin" ? 0 : name === "stdout" ? 1 : 2, loc)],
+ type,
+ loc,
+ };
+ return lowerer.maybeNarrow(read, expr);
+ }
const streamType = name === "stdin" ? CHILDWRITER_T : CHILDSTREAM_T;
const type: IrType = {
kind: "union",
@@ -259,3 +278,21 @@ export function lowerBuiltinExtraProperty(
}
return null;
}
+
+/** The runtime stream class a child stdio read produces under the general
+ * stream representation, or null when the declared type keeps the stdio
+ * handle (the fallback declarations' ChildStdin). */
+function generalChildStdioType(
+ lowerer: Lowerer,
+ expr: ts.PropertyAccessExpression,
+ name: "stdin" | "stdout" | "stderr",
+): IrType | null {
+ const className = name === "stdin" ? "%Writable" : "%Readable";
+ const mapped = lowerer.mapTypeOf(lowerer.typeOf(expr));
+ if (!mapped) return null;
+ const arms =
+ mapped.kind === "union" ? (lowerer.unions.get(mapped.unionId)?.arms ?? []) : [mapped];
+ return arms.some((arm) => arm.kind === "object" && arm.className === className)
+ ? { kind: "object", className }
+ : null;
+}
diff --git a/packages/compiler/src/frontend/lowering/builtins/webassembly.ts b/packages/compiler/src/frontend/lowering/builtins/webassembly.ts
index dc61853f8..56347e437 100644
--- a/packages/compiler/src/frontend/lowering/builtins/webassembly.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/webassembly.ts
@@ -2,17 +2,44 @@ import * as ts from "../../ts7/adapter.js";
import { type Lowerer, PoisonError } from "../lowerer.js";
import { locOf } from "../../program.js";
import { webAssemblyDiag } from "../../../diagnostics/diagnostic.js";
-import { STRING, type IrExpr } from "../../../ir/ir.js";
+import { BOOL, DYN, F64, STRING, type IrExpr } from "../../../ir/ir.js";
+import { dynUndefinedExpr } from "../../../ir/build.js";
+import { lowerDynObjectLiteral } from "../expressions/object-literals.js";
import { stdlibGlobalNameOf } from "../surfaces.js";
+import { isWebAssemblyType } from "../../type-mapper.js";
/* The WebAssembly API. scriptc.d.ts declares its types (lib.dom's
- * namespace) so that packages whose declarations mention them typecheck,
- * and the type mapper gives those types the checked-dynamic representation.
- * No module compiles to native code yet, so every reached use of the API
- * value fences at its expression with webAssemblyDiag: a build error in
- * TypeScript sources, a located runtime fence in JavaScript ones. typeof
- * needs no value and folds to Node's answer. */
+ * namespace) and the type mapper gives them the checked-dynamic
+ * representation. Modules whose bytes the build sees are translated to
+ * native code (wasm/translate.ts) and found at run time by the SHA-256 of
+ * their bytes (scr_wasm.c):
+ * - WebAssembly.compile, instantiate and validate calls, new
+ * WebAssembly.Module, Instance, Memory, Table and Global, and
+ * WebAssembly.Module.imports, exports and customSections lower to
+ * wasm.* libCalls; members of the values they produce (exports, memory
+ * buffers, globals, tables) dispatch through the native handles;
+ * - the classes called without `new` throw Node's TypeError
+ * (wasm.withoutNew);
+ * - `v instanceof WebAssembly.` lowers to wasm.is;
+ * - the error classes construct with or without `new` (wasm.errorNew).
+ * Every other reached use of the API value (the classes as values, the
+ * streaming functions, Tag and Exception) still fences at its expression
+ * with webAssemblyDiag: a build error in TypeScript sources, a located
+ * runtime fence in JavaScript ones. typeof needs no value and folds to Node's
+ * answer. A lowering marks the lowerer, so the build translates the
+ * modules it discovers (lowerToIr). */
+/** The classes `wasm.is` answers, by kind. */
+const WASM_CLASSES = [
+ "Module",
+ "Instance",
+ "Memory",
+ "Table",
+ "Global",
+ "CompileError",
+ "LinkError",
+ "RuntimeError",
+] as const;
/** Strips the spellings that leave a value unchanged: parentheses, type
* assertions, `satisfies` and non-null assertions. */
function bare(expr: ts.Expression): ts.Expression {
@@ -130,3 +157,248 @@ export function lowerWebAssemblyTypeof(lowerer: Lowerer, operand: ts.Expression)
}
return null;
}
+
+/** The member `WebAssembly.` names (any spelling of the namespace),
+ * or null. */
+function webAssemblyMember(lowerer: Lowerer, expr: ts.Expression): string | null {
+ const surface = webAssemblySurface(lowerer, expr);
+ if (surface === null || !surface.startsWith("WebAssembly.")) return null;
+ const member = surface.slice("WebAssembly.".length);
+ return member.includes(".") ? null : member;
+}
+
+function lowerArg(lowerer: Lowerer, arg: ts.Expression): IrExpr {
+ return ts.isObjectLiteralExpression(arg)
+ ? lowerDynObjectLiteral(lowerer, arg)
+ : lowerer.lowerExprExpecting(arg, DYN);
+}
+
+/** An argument whose evaluation has no effect: a literal, a plain
+ * identifier, or an empty object or array literal. */
+function isInert(arg: ts.Expression): boolean {
+ const node = bare(arg);
+ if (ts.isObjectLiteralExpression(node)) return node.properties.nodes.length === 0;
+ if (ts.isArrayLiteralExpression(node)) return node.elements.nodes.length === 0;
+ return (
+ ts.isIdentifier(node) ||
+ ts.isNumericLiteral(node) ||
+ node.kind === ts.SyntaxKind.BigIntLiteral ||
+ ts.isStringLiteral(node) ||
+ ts.isNoSubstitutionTemplateLiteral(node) ||
+ node.kind === ts.SyntaxKind.NullKeyword ||
+ node.kind === ts.SyntaxKind.TrueKeyword ||
+ node.kind === ts.SyntaxKind.FalseKeyword
+ );
+}
+
+/** The arguments, padded with undefined to `count`. Extra arguments are
+ * ignored, as the API ignores them, when evaluating them has no effect;
+ * null for spreads or other extra arguments (they stay refused). */
+function lowerArgs(
+ lowerer: Lowerer,
+ args: readonly ts.Expression[],
+ count: number,
+ loc: IrExpr["loc"],
+): IrExpr[] | null {
+ if (args.some(ts.isSpreadElement)) return null;
+ if (args.length > count) {
+ if (!args.slice(count).every(isInert)) return null;
+ args = args.slice(0, count);
+ }
+ const out = args.map((arg) => lowerArg(lowerer, arg));
+ while (out.length < count) out.push(dynUndefinedExpr(loc));
+ return out;
+}
+
+function errorNew(member: string, args: IrExpr[], loc: IrExpr["loc"]): IrExpr {
+ const kind = (WASM_CLASSES as readonly string[]).indexOf(member);
+ return {
+ kind: "libCall",
+ fn: "wasm.errorNew",
+ args: [{ kind: "numLit", value: kind, type: F64, loc }, args[0]!, args[1]!],
+ type: DYN,
+ loc,
+ };
+}
+
+/** The classes that require `new`, in wasm.withoutNew's numbering. */
+const NEW_ONLY = ["Module", "Instance", "Memory", "Table", "Global"];
+
+/** `WebAssembly.Module.imports(module)`, `exports(module)` and
+ * `customSections(module, name)`. Their results are arrays of descriptors
+ * or ArrayBuffers built as checked-dynamic values, then taken into the
+ * call's own type. Null for anything else. */
+function lowerModuleStatic(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null {
+ const surface = webAssemblySurface(lowerer, call.expression);
+ const name = surface?.startsWith("WebAssembly.Module.")
+ ? surface.slice("WebAssembly.Module.".length)
+ : null;
+ if (name !== "imports" && name !== "exports" && name !== "customSections") return null;
+ const loc = locOf(call);
+ const args = lowerArgs(lowerer, call.arguments.nodes, name === "customSections" ? 2 : 1, loc);
+ if (args === null) return null;
+ lowerer.usesWebAssembly = true;
+ if (name === "customSections") lowerer.usesWasmCustomSections = true;
+ const fn =
+ name === "imports"
+ ? "wasm.moduleImports"
+ : name === "exports"
+ ? "wasm.moduleExports"
+ : "wasm.moduleCustomSections";
+ const result: IrExpr = { kind: "libCall", fn, args, type: DYN, loc };
+ return lowerer.coerceInto(call, result, lowerer.irTypeOf(call));
+}
+
+/** `WebAssembly.compile(bytes)`, `WebAssembly.instantiate(source,
+ * imports?)`, `WebAssembly.validate(bytes)`, the error classes called
+ * without `new`, the other classes called without `new` (a TypeError), and
+ * Module's static functions. Null for anything else. */
+export function lowerWebAssemblyCall(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null {
+ if (call.questionDotToken) return null;
+ const member = webAssemblyMember(lowerer, call.expression);
+ if (member === null) return lowerModuleStatic(lowerer, call);
+ const loc = locOf(call);
+ const newOnly = NEW_ONLY.indexOf(member);
+ if (newOnly >= 0) {
+ // The arguments evaluate, then the call throws.
+ const args = lowerArgs(lowerer, call.arguments.nodes, 2, loc);
+ if (args === null) return null;
+ lowerer.usesWebAssembly = true;
+ return {
+ kind: "libCall",
+ fn: "wasm.withoutNew",
+ args: [{ kind: "numLit", value: newOnly, type: F64, loc }, args[0]!, args[1]!],
+ type: DYN,
+ loc,
+ };
+ }
+ const isError = member === "CompileError" || member === "LinkError" || member === "RuntimeError";
+ if (member !== "compile" && member !== "instantiate" && member !== "validate" && !isError)
+ return null;
+ const used = member === "instantiate" || isError ? 2 : 1;
+ const args = lowerArgs(lowerer, call.arguments.nodes, used, loc);
+ if (args === null) return null;
+ lowerer.usesWebAssembly = true;
+ let result: IrExpr;
+ if (isError) result = errorNew(member, args, loc);
+ else if (member === "validate")
+ result = { kind: "libCall", fn: "wasm.validate", args: [args[0]!], type: BOOL, loc };
+ else if (member === "compile")
+ result = {
+ kind: "libCall",
+ fn: "wasm.compile",
+ args: [args[0]!],
+ type: { kind: "promise", inner: DYN },
+ loc,
+ };
+ else
+ result = {
+ kind: "libCall",
+ fn: "wasm.instantiate",
+ args: [args[0]!, args[1]!],
+ type: { kind: "promise", inner: DYN },
+ loc,
+ };
+ return result;
+}
+
+/** `new WebAssembly.Module(bytes)`, `new WebAssembly.Instance(module,
+ * imports?)`, `new WebAssembly.Memory(descriptor)`, `new
+ * WebAssembly.Table(descriptor, value?)`, `new
+ * WebAssembly.Global(descriptor, value?)` and the error classes. Null for
+ * anything else. */
+export function lowerWebAssemblyNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr | null {
+ const member = webAssemblyMember(lowerer, expr.expression);
+ if (member === null) return null;
+ const isError = member === "CompileError" || member === "LinkError" || member === "RuntimeError";
+ if (!NEW_ONLY.includes(member) && !isError) return null;
+ const loc = locOf(expr);
+ const used = member === "Module" || member === "Memory" ? 1 : 2;
+ const args = lowerArgs(lowerer, expr.arguments?.nodes ?? [], used, loc);
+ if (args === null) return null;
+ lowerer.usesWebAssembly = true;
+ let result: IrExpr;
+ if (isError) result = errorNew(member, args, loc);
+ else if (member === "Module")
+ result = { kind: "libCall", fn: "wasm.moduleNew", args: [args[0]!], type: DYN, loc };
+ else if (member === "Memory")
+ result = { kind: "libCall", fn: "wasm.memoryNew", args: [args[0]!], type: DYN, loc };
+ else if (member === "Table")
+ result = { kind: "libCall", fn: "wasm.tableNew", args: [args[0]!, args[1]!], type: DYN, loc };
+ else if (member === "Global")
+ result = { kind: "libCall", fn: "wasm.globalNew", args: [args[0]!, args[1]!], type: DYN, loc };
+ else
+ result = {
+ kind: "libCall",
+ fn: "wasm.instanceNew",
+ args: [args[0]!, args[1]!],
+ type: DYN,
+ loc,
+ };
+ return result;
+}
+
+/** `v instanceof WebAssembly.`. Null for anything else. */
+export function lowerWebAssemblyInstanceof(
+ lowerer: Lowerer,
+ expr: ts.BinaryExpression,
+): IrExpr | null {
+ const member = webAssemblyMember(lowerer, expr.right);
+ if (member === null) return null;
+ const kind = (WASM_CLASSES as readonly string[]).indexOf(member);
+ if (kind < 0) return null;
+ const loc = locOf(expr);
+ lowerer.usesWebAssembly = true;
+ const value = lowerer.coerceInto(expr.left, lowerer.lowerExpr(expr.left), DYN);
+ return {
+ kind: "libCall",
+ fn: "wasm.is",
+ args: [value, { kind: "numLit", value: kind, type: F64, loc }],
+ type: BOOL,
+ loc,
+ };
+}
+
+/** WebAssembly API values: `WebAssembly.compile`, `instantiate` and
+ * `validate` read as functions, the namespace itself (`WebAssembly`,
+ * `globalThis.WebAssembly`), and the global object cast to a type with a
+ * `WebAssembly` member (pi codemode's spelling). Null for anything else,
+ * which refuseWebAssemblyUse then fences. */
+export function lowerWebAssemblyValue(lowerer: Lowerer, expr: ts.Expression): IrExpr | null {
+ const surface = webAssemblySurface(lowerer, expr);
+ if (surface === null) return null;
+ const loc = locOf(expr);
+ const node = bare(expr);
+ const fn = (name: string, args: IrExpr[]): IrExpr => {
+ lowerer.usesWebAssembly = true;
+ return { kind: "libCall", fn: name as "wasm.namespace", args, type: DYN, loc };
+ };
+ if (surface === "globalThis.WebAssembly") {
+ // The cast spelling names the global object, not its member.
+ const cast =
+ (ts.isAsExpression(expr) || ts.isTypeAssertion(expr)) &&
+ isGlobalThis(lowerer, expr.expression);
+ return cast ? fn("wasm.globalView", []) : fn("wasm.namespace", []);
+ }
+ if (surface === "WebAssembly") return fn("wasm.namespace", []);
+ const index = ["WebAssembly.compile", "WebAssembly.instantiate", "WebAssembly.validate"].indexOf(
+ surface,
+ );
+ if (index < 0 || !ts.isPropertyAccessExpression(node)) return null;
+ return fn("wasm.function", [{ kind: "numLit", value: index, type: F64, loc }]);
+}
+
+/** `global.valueOf()` on a receiver typed as a WebAssembly value: a
+ * Global answers its value (the generic dyn.valueOf answers the object
+ * itself for native handles other than Date). Null for other receivers. */
+export function lowerWebAssemblyValueOf(
+ lowerer: Lowerer,
+ receiver: ts.Expression,
+ recv: IrExpr,
+ loc: IrExpr["loc"],
+): IrExpr | null {
+ const ctx = { checker: lowerer.checker, isStdlibFile: lowerer.isStdlibFile };
+ if (!isWebAssemblyType(lowerer.typeOf(receiver), ctx)) return null;
+ lowerer.usesWebAssembly = true;
+ return { kind: "libCall", fn: "wasm.valueOf", args: [recv], type: DYN, loc };
+}
diff --git a/packages/compiler/src/frontend/lowering/call-arguments.ts b/packages/compiler/src/frontend/lowering/call-arguments.ts
index f5600b033..db7691a1c 100644
--- a/packages/compiler/src/frontend/lowering/call-arguments.ts
+++ b/packages/compiler/src/frontend/lowering/call-arguments.ts
@@ -1,3 +1,4 @@
+import { forwardedTupleType, pureForwardedValues, stripTypeWrappers } from "./typed-arguments.js";
import { dynUndefinedExpr } from "../../ir/build.js";
import { InternalCompilerError } from "../../errors.js";
import * as ts from "../ts7/adapter.js";
@@ -26,13 +27,13 @@ import { checkedIterableSpread } from "./checked-iterable-spread.js";
import { type ParamShape } from "./call-signatures.js";
import { arrayValueRead, arrayValueType } from "./array-values.js";
import { buildArrayConversion } from "./coercions/builders.js";
-import { typeKey } from "../type-mapper.js";
+import { restTupleElementTypes, typeKey } from "../type-mapper.js";
function fixedTupleSpreadInfo(
lowerer: Lowerer,
node: ts.Expression,
): { type: IrType & { kind: "record" }; fields: { name: string; type: IrType }[] } | null {
- const type = lowerer.mapTypeOf(lowerer.typeOf(node));
+ const type = forwardedTupleType(lowerer, node) ?? lowerer.mapTypeOf(lowerer.typeOf(node));
if (type?.kind !== "record") return null;
const shape = lowerer.shapes.get(type.shapeId);
if (!shape?.tuple || shape.fields.length === 0) return null;
@@ -42,6 +43,18 @@ function fixedTupleSpreadInfo(
};
}
+/** `...t` over a provably EMPTY tuple bound to a plain name (`[]`, a rest
+ * tuple whose type parameter or `Parameters` resolved to no positions):
+ * it contributes no arguments, and reading the name has no effect. */
+function emptyTupleSpread(lowerer: Lowerer, node: ts.Expression): boolean {
+ let operand = node;
+ while (ts.isParenthesizedExpression(operand)) operand = operand.expression;
+ if (!ts.isIdentifier(operand)) return false;
+ const forwarded = forwardedTupleType(lowerer, operand);
+ if (forwarded) return forwarded.kind === "array";
+ return restTupleElementTypes(lowerer.typeOf(operand), lowerer.typeCtx)?.length === 0;
+}
+
export function completeArgs(
lowerer: Lowerer,
argNodes: readonly ts.Expression[],
@@ -59,7 +72,9 @@ export function completeArgs(
// graph. Ordinary calls and fixed tuples never need that analysis.
const runtimeSpreads = argNodes.filter(
(arg): arg is ts.SpreadElement =>
- ts.isSpreadElement(arg) && !fixedTupleSpreadInfo(lowerer, arg.expression),
+ ts.isSpreadElement(arg) &&
+ !fixedTupleSpreadInfo(lowerer, arg.expression) &&
+ !emptyTupleSpread(lowerer, arg.expression),
);
// Spread operands lowered while choosing the completion, each exactly once.
const loweredSpreads = new Map();
@@ -190,6 +205,12 @@ export function completeArgs(
sources.push(arg);
continue;
}
+ if (emptyTupleSpread(lowerer, arg.expression)) continue;
+ const pure = pureForwardedValues(lowerer, arg.expression);
+ if (pure) {
+ for (const value of pure) sources.push({ ir: value });
+ continue;
+ }
const tuple = fixedTupleSpreadInfo(lowerer, arg.expression);
if (!tuple) {
sources.push(arg);
@@ -204,7 +225,13 @@ export function completeArgs(
// values as separately written arguments while the saved tuple stays
// alive through the call.
const spreadLoc = locOf(arg);
- const value = lowerer.coerceInto(arg.expression, lowerer.lowerExpr(arg.expression), tuple.type);
+ // A forwarded tuple seen through type assertions (`...(arguments as
+ // unknown as Parameters)`) spreads the tuple itself.
+ const operand =
+ forwardedTupleType(lowerer, arg.expression) !== null
+ ? stripTypeWrappers(arg.expression)
+ : arg.expression;
+ const value = lowerer.coerceInto(arg.expression, lowerer.lowerExpr(operand), tuple.type);
const saved = lowerer.declareHiddenLocal("%callSpread", tuple.type);
const savedRef = (): IrExpr => ({
kind: "varRef",
@@ -506,9 +533,38 @@ export function completeFuncValueArgs(
shapes.push({ type: rest, mode: "rest" });
return completeArgs(lowerer, call.arguments.nodes, shapes, loc, call);
}
+ // A fixed-tuple spread (`fn(...args)` over a tuple-typed rest binding)
+ // has a compile-time arity: completion flattens it into the positional
+ // slots exactly like a direct call's.
+ if (
+ funcType.rest !== true &&
+ call.arguments.nodes.some(ts.isSpreadElement) &&
+ call.arguments.nodes.every(
+ (arg) =>
+ !ts.isSpreadElement(arg) ||
+ fixedTupleSpreadInfo(lowerer, arg.expression) !== null ||
+ emptyTupleSpread(lowerer, arg.expression),
+ )
+ ) {
+ const shapes: ParamShape[] = funcType.params.map((type) => ({
+ type,
+ mode: omittedArgFor(lowerer, type, loc) ? "omittable" : "required",
+ }));
+ return completeArgs(lowerer, call.arguments.nodes, shapes, loc, call);
+ }
const args = call.arguments.nodes.map((arg, index) =>
lowerer.lowerExprExpecting(arg, funcType.params[index]),
);
+ // Effect-free surplus arguments of a fixed-arity value (`(r: void) =>
+ // void` drops its void slot) evaluate to nothing observable: drop them,
+ // as the callee ignores them.
+ while (
+ funcType.rest !== true &&
+ args.length > funcType.params.length &&
+ isSafeToDiscard(args[args.length - 1]!)
+ ) {
+ args.pop();
+ }
for (let i = args.length; i < funcType.params.length; i++) {
const absent = omittedArgFor(lowerer, funcType.params[i]!, loc);
if (!absent) {
diff --git a/packages/compiler/src/frontend/lowering/call-signatures.ts b/packages/compiler/src/frontend/lowering/call-signatures.ts
index 7570fff88..954122a16 100644
--- a/packages/compiler/src/frontend/lowering/call-signatures.ts
+++ b/packages/compiler/src/frontend/lowering/call-signatures.ts
@@ -1,3 +1,8 @@
+import {
+ typedArgumentsCandidate,
+ typedArgumentsShapes,
+ typedArgumentsTarget,
+} from "./typed-arguments.js";
import { InternalCompilerError } from "../../errors.js";
import * as ts from "../ts7/adapter.js";
import { bodyReadsArguments } from "../arguments-usage.js";
@@ -15,6 +20,7 @@ import {
import { isJsSourceFile, isNodeEsmFile } from "../program.js";
import {
genResultRecord,
+ restTupleParamTypes,
isAnyArrayType,
isUnitOnlyTsType,
jsOpenObjectType,
@@ -56,6 +62,10 @@ export interface ParamShape {
bodyType?: IrType;
callDefault?: IrExpr;
entryNarrow?: IrType;
+ /** One flattened position of a fixed-tuple rest parameter
+ * (restTupleShapes): the declaration's last parameter owns every shape
+ * from its index on, and the body binds the tuple (declareParams). */
+ restTuple?: true;
}
export interface FnSig {
@@ -373,6 +383,28 @@ export function isThisParameter(param: ts.ParameterDeclaration): boolean {
return ts.isIdentifier(param.name) && param.name.text === "this";
}
+/** A TypeScript rest parameter bound to a name whose type is a fixed tuple
+ * (`...args: [A, B]`, `...args: A` with A bound to a tuple, `...args:
+ * Parameters` with F bound): one required shape per position. Call
+ * sites and function values see the equivalent fixed signature; the body
+ * rebuilds the tuple (declareParams) unless it only forwards it. Null for
+ * every other parameter. */
+export function restTupleShapes(
+ lowerer: Lowerer,
+ param: ts.ParameterDeclaration,
+ tsType?: ts.Type,
+): ParamShape[] | null {
+ if (
+ param.dotDotDotToken === undefined ||
+ !ts.isIdentifier(param.name) ||
+ isJsSourceFile(param.getSourceFile())
+ ) {
+ return null;
+ }
+ const elems = restTupleParamTypes(tsType ?? lowerer.typeOf(param.name), lowerer.typeCtx);
+ return elems ? elems.map((type) => ({ type, mode: "required", restTuple: true })) : null;
+}
+
/** ParamShapes for a whole parameter list. */
export function paramShapes(
lowerer: Lowerer,
@@ -380,21 +412,47 @@ export function paramShapes(
signature?: ts.Signature,
blameOf?: (param: ts.ParameterDeclaration, index: number) => ts.Node,
): ParamShape[] {
- if (!signature)
- return params.filter((p) => !isThisParameter(p)).map((param) => lowerer.paramShape(param));
+ const out: ParamShape[] = [];
+ if (!signature) {
+ for (const param of params) {
+ if (isThisParameter(param)) continue;
+ const flattened = restTupleShapes(lowerer, param);
+ if (flattened) out.push(...flattened);
+ else out.push(lowerer.paramShape(param));
+ }
+ return out;
+ }
const symbols = signature.getParameters();
- return params.map((declParam, i) => {
+ const shapeAt = (declParam: ts.ParameterDeclaration, i: number): ParamShape => {
const stage = lowerer.stageSourceType(declParam.name);
if (stage) return { type: stage, mode: "required" as const };
const symbol = symbols[i];
const tsType = symbol
? lowerer.checker.getTypeOfSymbol(symbol)
: lowerer.typeOf(declParam.name);
+ // A call-site instantiation whose concrete argument type does not map
+ // is that argument's blocker: name it there, with its concrete type,
+ // rather than as the declaration's unbound type parameter.
+ if (
+ blameOf !== undefined &&
+ !isJsSourceFile(declParam.getSourceFile()) &&
+ (tsType.flags & ts.TypeFlags.Any) === 0 &&
+ declParam.type !== undefined &&
+ mentionsTypeParameter(lowerer, lowerer.checker.getTypeFromTypeNode(declParam.type)) &&
+ lowerer.mapTypeOf(tsType) === null
+ ) {
+ lowerer.badType(blameOf(declParam, i), tsType);
+ }
let mapped = jsOpenObjectType(
declParam,
lowerer.runtimeOptionalBindingType(
declParam.name,
- lowerer.mapTypeOf(tsType) ?? lowerer.irTypeOf(declParam.name),
+ lowerer.mapTypeOf(tsType) ??
+ // A TypeScript instantiation at `any` (a type argument inferred
+ // from an untyped value) takes the checked-dynamic slot.
+ ((tsType.flags & ts.TypeFlags.Any) !== 0 && !isJsSourceFile(declParam.getSourceFile())
+ ? DYN
+ : lowerer.irTypeOf(declParam.name)),
),
lowerer.shapes,
lowerer.unions,
@@ -428,7 +486,32 @@ export function paramShapes(
);
}
return { type: mapped, mode: declParam.questionToken ? "omittable" : "required" };
+ };
+ params.forEach((declParam, i) => {
+ const symbol = symbols[i];
+ const flattened = restTupleShapes(
+ lowerer,
+ declParam,
+ symbol ? lowerer.checker.getTypeOfSymbol(symbol) : undefined,
+ );
+ if (flattened) out.push(...flattened);
+ else out.push(shapeAt(declParam, i));
});
+ return out;
+}
+
+/** A declared type that is (or is a union over) a bare type parameter
+ * or an instantiation of a generic alias/interface over type arguments. */
+function mentionsTypeParameter(lowerer: Lowerer, t: ts.Type): boolean {
+ if (t.flags & ts.TypeFlags.TypeParameter) return true;
+ const mentions = (part: ts.Type): boolean => mentionsTypeParameter(lowerer, part);
+ if (t.isUnionType()) return ts.constituentTypes(t).some(mentions);
+ if (t.getAliasTypeArguments().some(mentions)) return true;
+ return (
+ (t.flags & ts.TypeFlags.Object) !== 0 &&
+ ((t as ts.ObjectType).objectFlags & ts.ObjectFlags.Reference) !== 0 &&
+ lowerer.checker.getTypeArguments(t as ts.TypeReference).some(mentions)
+ );
}
/** A default expression may itself yield undefined. Keep that arm in the
@@ -828,6 +911,16 @@ export function lambdaSignature(
| ts.SetAccessorDeclaration,
): { shapes: ParamShape[]; funcType: IrType & { kind: "func" } } {
if (!node.body) lowerer.unsupported("SC1090", node, "function overload signatures");
+ // A parameterless function expression forwarding its `arguments`,
+ // asserted to a fixed signature: that signature is its ABI.
+ if (typedArgumentsCandidate(node) && !node.typeParameters?.nodes.length) {
+ const target = typedArgumentsTarget(lowerer, node);
+ if (target) {
+ const shapes = typedArgumentsShapes(target);
+ lowerer.typedArgumentsLambdas.add(node);
+ return { shapes, funcType: funcTypeFromParamShapes(shapes, target.ret) };
+ }
+ }
// A JS replacement of a native stream's write method receives the
// actual call tuple, including overloads and omitted arguments. Node's
// contextual overload signature cannot define this closure's ABI.
diff --git a/packages/compiler/src/frontend/lowering/class-protocol-dispatch.ts b/packages/compiler/src/frontend/lowering/class-protocol-dispatch.ts
index 0c9a24f24..298ff45dc 100644
--- a/packages/compiler/src/frontend/lowering/class-protocol-dispatch.ts
+++ b/packages/compiler/src/frontend/lowering/class-protocol-dispatch.ts
@@ -188,7 +188,8 @@ function protocolSignature(
call: ts.CallExpression,
access: ts.PropertyAccessExpression,
): ProtocolSignature | null {
- const recvTs = lowerer.typeOf(access.expression);
+ // A guarded optional step's receiver still carries its nullish arms.
+ const recvTs = lowerer.checker.getNonNullableType(lowerer.typeOf(access.expression));
const property = lowerer.checker.getPropertyOfType(recvTs, access.name.text);
if (!property) return null;
const declarations = property.declarations;
@@ -240,10 +241,11 @@ export function lowerProtocolMethodCall(
access: ts.PropertyAccessExpression,
recv: IrExpr,
): IrExpr | null {
+ // An optional chain that already guarded this step (`s?.m(x)`, the chain
+ // re-dispatching with the narrowed receiver) calls like a plain one.
if (
recv.type.kind !== "dyn" ||
- call.questionDotToken ||
- access.questionDotToken ||
+ lowerer.chainBlocked(call, access) ||
call.arguments.nodes.some(ts.isSpreadElement)
)
return null;
diff --git a/packages/compiler/src/frontend/lowering/class-prototypes.ts b/packages/compiler/src/frontend/lowering/class-prototypes.ts
index d337dc802..6c6c7811d 100644
--- a/packages/compiler/src/frontend/lowering/class-prototypes.ts
+++ b/packages/compiler/src/frontend/lowering/class-prototypes.ts
@@ -1,4 +1,5 @@
import { dynUndefinedExpr, varRef } from "../../ir/build.js";
+import { builtinClassKind, builtinClassValue } from "./builtins/builtin-class-dyn.js";
import * as ts from "../ts7/adapter.js";
import {
BOOL,
@@ -450,6 +451,19 @@ export function lowerClassPrototypeData(
exactClassOfReceiver(lowerer, expr.expression) ??
(stored?.kind === "classval" ? lowerer.classes.get(stored.className) : undefined);
if (!info) return null;
+ // EventEmitter.prototype and the stream prototypes: the untyped
+ // constructor's prototype object (builtin-class-dyn.ts).
+ const builtinKind = builtinClassKind(info.def.name);
+ if (builtinKind !== null) {
+ const loc = locOf(expr);
+ return {
+ kind: "dynKeyGet",
+ value: builtinClassValue(builtinKind, loc),
+ key: { kind: "strLit", value: "prototype", type: STRING, loc },
+ type: DYN,
+ loc,
+ };
+ }
const parent = expr.parent;
const descriptorKey =
ts.isCallExpression(parent) &&
diff --git a/packages/compiler/src/frontend/lowering/coercions/function-adapters.ts b/packages/compiler/src/frontend/lowering/coercions/function-adapters.ts
index 445014b88..3d8817ab1 100644
--- a/packages/compiler/src/frontend/lowering/coercions/function-adapters.ts
+++ b/packages/compiler/src/frontend/lowering/coercions/function-adapters.ts
@@ -4,6 +4,7 @@ import { InternalCompilerError } from "../../../errors.js";
import type { IrExpr, IrParam, IrStmt, IrType, SrcLoc } from "../../../ir/ir.js";
import {
canAdaptDynFuncTo,
+ canDynCheckTo,
DYN,
F64,
isUnitType,
@@ -341,6 +342,19 @@ export function funcCoerceAdapter(
voidRet = toT.ret.kind === "dyn" ? "dyn" : "strand";
}
}
+ // An untyped result (a generic wrapper's `any` handed back `as F`) takes
+ // the checked-dynamic extraction to the slot's result: a record checks
+ // its shape, a Promise keeps its identity and checks its payload when it
+ // settles.
+ const checkedRet =
+ strandRet &&
+ fromT.ret.kind === "dyn" &&
+ canDynCheckTo(
+ toT.ret,
+ (id) => lowerer.shapes.get(id),
+ (id) => lowerer.unions.get(id),
+ );
+ if (checkedRet) strandRet = false;
const key = `fnadapt:${typeKey(fromT)}:${typeKey(toT)}`;
const existing = lowerer.coercions.retags.get(key);
if (existing) return existing;
@@ -440,6 +454,10 @@ export function funcCoerceAdapter(
`a '${lowerer.fmt(fromT)}' function invoked through a '${lowerer.fmt(toT)}' slot (the result cannot convert to '${lowerer.fmt(toT.ret)}' — the checker's loose function compatibility admitted the assignment, but the call has no exact lowering)`,
),
];
+ } else if (checkedRet) {
+ body = [
+ { kind: "return", value: { kind: "dynCheck", value: call, type: toT.ret, loc }, loc },
+ ];
} else {
const result = lowerer.coerceToExpected(call, toT.ret);
if (!typeEquals(result.type, toT.ret))
diff --git a/packages/compiler/src/frontend/lowering/expressions/binary.ts b/packages/compiler/src/frontend/lowering/expressions/binary.ts
index cecbde717..8ef631f60 100644
--- a/packages/compiler/src/frontend/lowering/expressions/binary.ts
+++ b/packages/compiler/src/frontend/lowering/expressions/binary.ts
@@ -1,5 +1,6 @@
/* Binary operators: arithmetic, comparisons and equality, logical and nullish pairs, and string conversion for concatenation. */
import * as ts from "../../ts7/adapter.js";
+import { cleanFuncAdaptable } from "../coercions/function-adapters.js";
import { type Lowerer, jsFuncNameOf } from "../lowerer.js";
import {
BIGINT_T,
@@ -481,6 +482,22 @@ export function lowerNullishPair(
);
}
}
+ // An optional callback defaulted to a function of another signature
+ // (`o.tags ?? (() => ({}))`, fewer parameters or a narrower result): the
+ // result is the callback's own function type, and the default adapts into
+ // it (JavaScript ignores surplus arguments). The checker's merged union
+ // signature would lose the callback's result shape.
+ if (present?.kind === "func" && present.rest !== true) {
+ const raw = lowerer.lowerExpr(expr.right);
+ if (
+ raw.type.kind === "func" &&
+ (typeEquals(raw.type, present) || cleanFuncAdaptable(lowerer, raw.type, present))
+ ) {
+ const right = lowerer.coerceInto(expr.right, raw, present);
+ lowerer.nullishFuncResults.add(expr);
+ return { kind: "nullish", left, right, type: present, loc };
+ }
+ }
lowerer.unsupported(
"SC1090",
expr,
diff --git a/packages/compiler/src/frontend/lowering/expressions/element-access.ts b/packages/compiler/src/frontend/lowering/expressions/element-access.ts
index 144742257..f39b7c428 100644
--- a/packages/compiler/src/frontend/lowering/expressions/element-access.ts
+++ b/packages/compiler/src/frontend/lowering/expressions/element-access.ts
@@ -63,6 +63,8 @@ import { npmStaticPackageOfPath } from "../../npm-static.js";
import { isSafeToRepeat } from "./evaluation-safety.js";
import { lowerClassPrototypeComputedAssignment } from "../class-prototypes.js";
import { isClassCallback } from "../class-callbacks.js";
+import { isKeyedFieldAccess } from "./keyed-field-targets.js";
+import { lowerFieldCompoundValue } from "./updates.js";
/** `a[i]` reads. Only f64 indices into array receivers are modeled; JS
* string-key element access (`a["length"]`) and string indexing (`s[0]`
@@ -1417,6 +1419,11 @@ export function lowerElementCompoundParts(
const target = expr.left as ts.ElementAccessExpression;
fenceNodeModuleMutation(lowerer, target, "assignment");
const loc = locOf(expr);
+ // A record through a computed key (`totals[side] += n` with a literal
+ // union key, `counts[name] += 1` on an index-signature record): the
+ // member compound's receiver, key, old value, rhs, write sequence.
+ if (isKeyedFieldAccess(lowerer, target))
+ return { expr: lowerFieldCompoundValue(lowerer, target, op, expr.right, loc), stmts: null };
const checked = lowerer.lowerExpr(target.expression);
if (checked?.type.kind === "dyn" && isJsSourceFile(expr.getSourceFile())) {
const stmts: IrStmt[] = [];
diff --git a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts
index 70ed1b2ba..c2e5ddcbb 100644
--- a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts
+++ b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts
@@ -1,6 +1,8 @@
/* Identifier references in expression position: lowerExprInner's identifier branch and its helpers. */
import { cjsMemberAliasValueOf } from "../cjs-exports-object.js";
+import { builtinClassValue, builtinMemberClassKind } from "../builtins/builtin-class-dyn.js";
import * as ts from "../../ts7/adapter.js";
+import { InternalCompilerError } from "../../../errors.js";
import * as posix from "node:path/posix";
import { type Lowerer, PoisonError } from "../lowerer.js";
import {
@@ -21,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);
diff --git a/packages/compiler/src/frontend/lowering/expressions/keyed-field-targets.ts b/packages/compiler/src/frontend/lowering/expressions/keyed-field-targets.ts
new file mode 100644
index 000000000..7a4f9c557
--- /dev/null
+++ b/packages/compiler/src/frontend/lowering/expressions/keyed-field-targets.ts
@@ -0,0 +1,271 @@
+/* Read-modify-write targets for computed record keys: `r[k] op= v`, `r[k]++` and `r[k] ??= v`. */
+import * as ts from "../../ts7/adapter.js";
+import {
+ BOOL,
+ F64,
+ STRING,
+ typeEquals,
+ type IrExpr,
+ type IrStmt,
+ type SrcLoc,
+} from "../../../ir/ir.js";
+import { isJsSourceFile } from "../../program.js";
+import { constituentTypes } from "../../ts7/checker.js";
+import type { FieldTarget } from "../lower-exprs.js";
+import type { Lowerer } from "../lowerer.js";
+
+export type KeyedFieldTarget = FieldTarget & { container: "recordKeyed" };
+
+/** The property names a computed key's type allows: one or more string
+ * literals (a literal union such as `"sent" | "received"`, enum string
+ * members, or a type parameter bound to one inside a generic instance).
+ * Null for any other key type. */
+export function literalFieldKeys(lowerer: Lowerer, keyNode: ts.Expression): string[] | null {
+ let t: ts.Type = lowerer.typeOf(keyNode);
+ if (t.flags & ts.TypeFlags.TypeParameter) {
+ const bound = lowerer.typeParamTsResolver(t);
+ if (!bound) return null;
+ t = bound;
+ }
+ const parts = t.flags & ts.TypeFlags.Union ? constituentTypes(t) : [t];
+ const keys: string[] = [];
+ for (const part of parts) {
+ if (!part.isStringLiteralType()) return null;
+ if (!keys.includes(part.value)) keys.push(part.value);
+ }
+ return keys.length > 0 ? keys : null;
+}
+
+/** Does `r[k]` read and write through declared record storage by key? A
+ * checker-only test (nothing lowers), so a caller can route before it
+ * evaluates the receiver: TypeScript sources only, a record whose declared
+ * fields include every name the key's literal type allows, or a pure
+ * index-signature record with a string key. */
+export function isKeyedFieldAccess(lowerer: Lowerer, access: ts.ElementAccessExpression): boolean {
+ if (
+ access.questionDotToken ||
+ isJsSourceFile(access.getSourceFile()) ||
+ lowerer.isProcessEnv(access.expression)
+ )
+ return false;
+ const keys = literalFieldKeys(lowerer, access.argumentExpression);
+ if (!keys && !(lowerer.typeOf(access.argumentExpression).flags & ts.TypeFlags.StringLike))
+ return false;
+ const receiver = lowerer.mapTypeOf(lowerer.typeOf(access.expression));
+ if (receiver?.kind !== "record") return false;
+ const shape = lowerer.shapes.get(receiver.shapeId);
+ if (!shape || shape.tuple) return false;
+ if (shape.fields.length === 0) return shape.indexValue !== undefined;
+ return keys !== null && keys.every((k) => shape.fields.some((f) => f.name === k));
+}
+
+/** The target of a read-modify-write through `r[k]`, or null when
+ * isKeyedFieldAccess declines. A literal-union key dispatches over the
+ * declared fields it names, which must share one type; a string key on a
+ * pure index-signature record reads and writes its entries. The receiver
+ * and key may arrive already lowered (and saved) by the caller; otherwise
+ * both lower here, unsaved. A caller that evaluates the target more than
+ * once saves `obj` and then `key`, the JavaScript evaluation order. With
+ * `numeric`, a missing index-signature entry reads as NaN (every numeric
+ * update applies ToNumber to it) instead of a value armed with undefined. */
+export function keyedFieldTarget(
+ lowerer: Lowerer,
+ access: ts.ElementAccessExpression,
+ numeric: boolean,
+ lowered?: { obj: IrExpr; key: IrExpr },
+): KeyedFieldTarget | null {
+ if (!isKeyedFieldAccess(lowerer, access)) return null;
+ const keyNode = access.argumentExpression;
+ const keys = literalFieldKeys(lowerer, keyNode);
+ const obj = lowered?.obj ?? lowerer.lowerExpr(access.expression);
+ if (obj.type.kind !== "record") {
+ lowerer.unsupported(
+ "SC1090",
+ access.expression,
+ `updating a computed key of a '${lowerer.fmt(obj.type)}' value (narrow the receiver to its record type first)`,
+ );
+ }
+ const shapeId = obj.type.shapeId;
+ const shape = lowerer.shapes.get(shapeId)!;
+ // A single literal spelled at the site (or an identifier whose type is
+ // that one literal) names the field statically: identifier evaluation
+ // is pure, so skipping it matches JavaScript.
+ const indexed = shape.fields.length === 0;
+ const staticKey =
+ !indexed && keys?.length === 1 && (ts.isStringLiteralLike(keyNode) || ts.isIdentifier(keyNode));
+ const key = staticKey ? null : (lowered?.key ?? lowerer.lowerExpr(keyNode));
+ if (key && key.type.kind !== "string") {
+ lowerer.unsupported(
+ "SC1090",
+ keyNode,
+ `updating a record through a '${lowerer.fmt(key.type)}' key (convert the key to a string first)`,
+ );
+ }
+ if (keys && !indexed) {
+ const fields = keys.map((k) => shape.fields.find((f) => f.name === k));
+ const first = fields[0];
+ if (!first || fields.some((f) => !f || !typeEquals(f.type, first.type))) {
+ lowerer.unsupported(
+ "SC1090",
+ access,
+ `updating '${lowerer.fmt(obj.type)}' through a key whose fields are not all stored with one type (${keys.map((k) => `'${k}'`).join(", ")}; switch over the key and update each field)`,
+ );
+ }
+ return {
+ container: "recordKeyed",
+ obj,
+ shapeId,
+ field: keys[0]!,
+ keys,
+ key,
+ fieldType: first.type,
+ };
+ }
+ if (!indexed || !shape.indexValue) {
+ lowerer.unsupported(
+ "SC1090",
+ access,
+ `updating '${lowerer.fmt(obj.type)}' through a runtime key (the stored record declares fields)`,
+ );
+ }
+ return {
+ container: "recordKeyed",
+ obj,
+ shapeId,
+ field: "",
+ keys: null,
+ key,
+ fieldType: shape.indexValue,
+ ...(numeric && shape.indexValue.kind === "f64" ? { missNaN: true as const } : {}),
+ };
+}
+
+/** Does `key` name `field`? */
+function keyIs(key: IrExpr, field: string, loc: SrcLoc): IrExpr {
+ return {
+ kind: "strEq",
+ negated: false,
+ left: key,
+ right: { kind: "strLit", value: field, type: STRING, loc },
+ type: BOOL,
+ loc,
+ };
+}
+
+/** A key outside its literal type (smuggled through a cast) names no
+ * declared field: a fixed-shape record cannot add the property, so the
+ * update throws the TypeError of a keyed write miss. */
+function keyMiss(key: IrExpr, target: KeyedFieldTarget, loc: SrcLoc): IrExpr {
+ const message: IrExpr = {
+ kind: "strConcat",
+ left: {
+ kind: "strConcat",
+ left: { kind: "strLit", value: "Cannot add property '", type: STRING, loc },
+ right: key,
+ type: STRING,
+ loc,
+ },
+ right: { kind: "strLit", value: "' to a fixed-shape object", type: STRING, loc },
+ type: STRING,
+ loc,
+ };
+ return {
+ kind: "libCall",
+ fn: "error.nodeThrow",
+ args: [
+ { kind: "numLit", value: 1, type: F64, loc },
+ { kind: "strLit", value: "", type: STRING, loc },
+ message,
+ ],
+ type: target.fieldType,
+ loc,
+ };
+}
+
+/** The current value of a keyed target. */
+export function keyedFieldRead(lowerer: Lowerer, target: KeyedFieldTarget, loc: SrcLoc): IrExpr {
+ const { obj, shapeId, key, fieldType } = target;
+ if (!target.keys) {
+ const type = target.missNaN
+ ? fieldType
+ : fieldType.kind === "dyn"
+ ? fieldType
+ : lowerer.withUndefinedArmOf(fieldType);
+ if (!type || !key) throw new Error("keyed index-signature target without a key or read type");
+ return {
+ kind: "recordKeyGet",
+ obj,
+ shapeId,
+ key,
+ ...(target.missNaN ? { missNaN: true as const } : {}),
+ type,
+ loc,
+ };
+ }
+ const get = (field: string): IrExpr => ({
+ kind: "recordGet",
+ obj,
+ shapeId,
+ field,
+ type: fieldType,
+ loc,
+ });
+ if (!key) return get(target.keys[0]!);
+ return target.keys.reduceRight(
+ (otherwise, field) => ({
+ kind: "ternary",
+ cond: keyIs(key, field, loc),
+ then: get(field),
+ else_: otherwise,
+ type: fieldType,
+ loc,
+ }),
+ keyMiss(key, target, loc),
+ );
+}
+
+/** Store `value` (already of the target's field type) through a keyed
+ * target. A multi-key dispatch references the value once per field, so a
+ * value that is not a plain local is saved first. */
+export function keyedFieldWrite(
+ lowerer: Lowerer,
+ target: KeyedFieldTarget,
+ value: IrExpr,
+ loc: SrcLoc,
+): IrStmt {
+ const { obj, shapeId, key } = target;
+ if (!target.keys) {
+ if (!key) throw new Error("keyed index-signature target without a key");
+ return { kind: "recordKeySet", obj, shapeId, key, value, loc };
+ }
+ if (!key) return { kind: "recordSet", obj, shapeId, field: target.keys[0]!, value, loc };
+ const prefix: IrStmt[] = [];
+ let stored = value;
+ if (value.kind !== "varRef") {
+ const local = lowerer.declareHiddenLocal("%keyedValue", value.type);
+ prefix.push({ kind: "varDecl", localId: local.id, init: value, loc });
+ stored = { kind: "varRef", localId: local.id, type: value.type, loc };
+ }
+ const chain = target.keys.reduceRight(
+ (otherwise, field) => [
+ {
+ kind: "if",
+ cond: keyIs(key, field, loc),
+ then: [{ kind: "recordSet", obj, shapeId, field, value: stored, loc }],
+ else_: otherwise,
+ loc,
+ },
+ ],
+ [{ kind: "exprStmt", expr: keyMiss(key, target, loc), loc }],
+ );
+ return { kind: "block", body: [...prefix, ...chain], loc };
+}
+
+/** Save a keyed target's key after its receiver, so a multi-key dispatch
+ * reads and writes the field the key named when it was evaluated. */
+export function saveKeyedFieldKey(
+ target: FieldTarget,
+ save: (value: IrExpr, name: string) => IrExpr,
+): void {
+ if (target.container === "recordKeyed" && target.key) target.key = save(target.key, "%keyedKey");
+}
diff --git a/packages/compiler/src/frontend/lowering/expressions/narrowing.ts b/packages/compiler/src/frontend/lowering/expressions/narrowing.ts
index b54da8180..9f98ac2db 100644
--- a/packages/compiler/src/frontend/lowering/expressions/narrowing.ts
+++ b/packages/compiler/src/frontend/lowering/expressions/narrowing.ts
@@ -26,6 +26,7 @@ import { jsBindingHasOpenWrites } from "../lower-stmts.js";
import { narrowClassUnion, narrowStoredClassValue, structuralNarrowing } from "../class-unions.js";
import { checkedClassAssertion, checkedClassUnionAssertion } from "../class-assertions.js";
import { isOptionalProcessStreamProperty } from "../builtins/process.js";
+import { requestGeneralStreamSurface } from "../general-streams.js";
import { literalValues } from "../../literal-values.js";
import { literalUnionArm } from "../../union-discriminants.js";
import { checkedArmExtraction } from "../coercions/unions.js";
@@ -1186,6 +1187,56 @@ function classInstanceOrAbsent(lowerer: Lowerer, type: IrType): boolean {
);
}
+/** `Object.fromEntries(...) as T` with T a declared record (the generic
+ * `transform>(t: T): T` idiom): the
+ * entries build a pure index-signature record, which has no layout in
+ * common with T's struct. Node keeps the object itself, with exactly the
+ * keys and the order the entries gave it. The result of fromEntries is a
+ * fresh object nothing else references, so the checked-dynamic bridge,
+ * which keeps keys and their order the same way `JSON.parse(text) as T`
+ * does, cannot be told apart from the original object; a value outside
+ * T throws the catchable TypeError of every checked cast. The entries'
+ * values must be scalars or checked-dynamic values, which the bridge
+ * passes by value or by identity; native arrays and records would be
+ * copied. Other sources may be aliased, where a rebuilt object would sever
+ * mutation, so they keep the exact-shape fence. */
+function freshEntriesRecordAssertion(
+ lowerer: Lowerer,
+ expr: ts.AsExpression | ts.TypeAssertion,
+ inner: IrExpr,
+ target: IrType | null,
+): IrExpr | null {
+ if (inner.type.kind !== "record" || target?.kind !== "record") return null;
+ const source = lowerer.shapes.get(inner.type.shapeId);
+ const dest = lowerer.shapes.get(target.shapeId);
+ if (!source?.indexValue || source.tuple || source.fields.length > 0) return null;
+ if (!dest || dest.tuple || dest.indexValue || shapeHasAccessorSlots(dest)) return null;
+ // Native arrays and records in the entries would be copied on the way
+ // through the checked-dynamic bridge, severing their identity: only
+ // slots of scalars or checked-dynamic values pass.
+ if (extractionCopiesReference(lowerer, source.indexValue)) return null;
+ let operand: ts.Expression = expr.expression;
+ while (ts.isParenthesizedExpression(operand)) operand = operand.expression;
+ if (
+ !ts.isCallExpression(operand) ||
+ !ts.isPropertyAccessExpression(operand.expression) ||
+ operand.expression.name.text !== "fromEntries" ||
+ !lowerer.isStdlibGlobal(operand.expression.expression, "Object")
+ )
+ return null;
+ if (!lowerer.dynConvertible(inner.type)) return null;
+ if (
+ !canDynCheckTo(
+ target,
+ (id) => lowerer.shapes.get(id),
+ (id) => lowerer.unions.get(id),
+ )
+ )
+ return null;
+ const loc = locOf(expr);
+ return { kind: "dynCheck", value: lowerer.coerceToExpected(inner, DYN), type: target, loc };
+}
+
/** `e as T` and the old-style assertion `e` — one node shape (both
* carry `.type` and `.expression`), one lowering. */
export function lowerAsExpression(
@@ -1237,12 +1288,14 @@ export function lowerAsExpression(
// the requested shape; an assertion does not bypass runtime checks.
const narrowed = lowerer.mapTypeOf(lowerer.typeOf(expr.expression));
const bridged =
- narrowed &&
- (narrowed.kind === "f64" ||
- narrowed.kind === "bool" ||
- narrowed.kind === "string" ||
- narrowed.kind === "object" ||
- narrowed.kind === "dyn");
+ (narrowed &&
+ (narrowed.kind === "f64" ||
+ narrowed.kind === "bool" ||
+ narrowed.kind === "string" ||
+ narrowed.kind === "object" ||
+ narrowed.kind === "dyn")) ||
+ // `catch (e: any)`: the binding reads as its checked-dynamic value.
+ (narrowed === null && (lowerer.typeOf(expr.expression).flags & ts.TypeFlags.Any) !== 0);
if (!bridged) lowerer.unsupported("SC1063", expr);
}
const inner = lowerer.lowerExpr(expr.expression);
@@ -1323,6 +1376,8 @@ export function lowerAsExpression(
) {
return lowerer.coerceInto(expr, inner, target);
}
+ const rebuilt = freshEntriesRecordAssertion(lowerer, expr, inner, target);
+ if (rebuilt) return rebuilt;
// `job as Job & { startedAt: number }` on a record: the asserted
// record refines the stored one, so the assertion keeps the object
// (a width copy would sever writes through the asserted view).
@@ -1376,6 +1431,12 @@ export function lowerAsExpression(
return inner; // erasure, unchanged
}
+ // `value as never` is the escape hatch that makes an unknown value fit
+ // any slot (`tagSuccess(value as never)`). No value has type never, so
+ // checking against its placeholder layout would reject every value.
+ // Erase it instead: the consuming slot applies the same validated
+ // extraction an unknown value gets wherever the checker admits it.
+ if (expr.type.kind === ts.SyntaxKind.NeverKeyword) return inner;
const targetTs = lowerer.checker.getTypeFromTypeNode(expr.type);
const target = lowerer.mapTypeOf(targetTs) ?? dynFallbackType(lowerer, expr.type, targetTs);
if (!target) lowerer.badType(expr.type, targetTs);
@@ -1450,6 +1511,9 @@ export function lowerAsExpression(
if (DYN_HANDLE_KINDS.has(target.kind)) {
return { kind: "dynCheck", value: inner, type: target, loc: locOf(expr) };
}
+ // `value as NodeJS.ReadableStream`: the general stream classes take
+ // the checked cast (general-streams.ts).
+ requestGeneralStreamSurface(lowerer, expr);
lowerer.unsupported(
"SC1090",
expr,
diff --git a/packages/compiler/src/frontend/lowering/expressions/nullish-assignment.ts b/packages/compiler/src/frontend/lowering/expressions/nullish-assignment.ts
index 4366b8d6b..81b3b9d69 100644
--- a/packages/compiler/src/frontend/lowering/expressions/nullish-assignment.ts
+++ b/packages/compiler/src/frontend/lowering/expressions/nullish-assignment.ts
@@ -18,6 +18,11 @@ import { arrayNumberRead, arrayValueRead, arrayValueStore } from "../array-value
import { fenceNodeModuleMutation } from "../lower-node-module.js";
import { globalSymbolKey } from "./global-symbols.js";
import { tryLowerExpression } from "./try-lower-expression.js";
+import {
+ type KeyedFieldTarget,
+ keyedFieldTarget,
+ saveKeyedFieldKey,
+} from "./keyed-field-targets.js";
import {
eventAccessorRead,
eventAccessorWrite,
@@ -147,6 +152,7 @@ export function lowerShortCircuitAssignment(lowerer: Lowerer, expr: ts.BinaryExp
} else {
const receiver = save(lowerer.lowerExpr(target.expression), "%shortCircuitReceiver");
let key = lowerer.lowerExpr(target.argumentExpression);
+ let keyed: KeyedFieldTarget | null = null;
if (receiver.type.kind === "dyn") {
key = save(
{
@@ -245,6 +251,17 @@ export function lowerShortCircuitAssignment(lowerer: Lowerer, expr: ts.BinaryExp
value: lowerer.coerceToExpected(value, indexValue),
loc,
});
+ } else if (
+ receiver.type.kind === "record" &&
+ (keyed = keyedFieldTarget(lowerer, target, false, { obj: receiver, key }))
+ ) {
+ // `totals[side] ??= v` through a literal-union key: the read and
+ // the write dispatch over the declared fields the key can name.
+ const field = keyed;
+ saveKeyedFieldKey(field, save);
+ storage = field.fieldType;
+ read = lowerer.fieldGetExpr(field, locOf(target), target);
+ write = (value) => lowerer.fieldSetStmt(field, value, loc, target);
} else {
lowerer.unsupported(
"SC1090",
diff --git a/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts b/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts
index 930e1a7cb..995514a4d 100644
--- a/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts
+++ b/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts
@@ -1,3 +1,5 @@
+import { isSafeToDiscard } from "./evaluation-safety.js";
+import { completeFuncValueArgs } from "../call-arguments.js";
import { dynUndefinedExpr } from "../../../ir/build.js";
import * as ts from "../../ts7/adapter.js";
import { DYN, STRING, UNDEFINED_T, VOID, isUnitType, typeEquals } from "../../../ir/ir.js";
@@ -429,7 +431,22 @@ export function lowerOptionalChain(
if (dotNode === expr && ts.isCallExpression(expr)) {
if (narrowed.kind !== "func") lowerer.badType(recvNode, lowerer.typeOf(recvNode));
const params = narrowed.params;
+ // Forwarded fixed tuples (`f?.(...args)`) complete into the slots.
+ if (expr.arguments.nodes.some(ts.isSpreadElement)) {
+ const completed = completeFuncValueArgs(lowerer, expr, narrowed, loc);
+ const call: IrExpr = {
+ kind: "callValue",
+ callee: recvRef,
+ args: completed,
+ type: narrowed.ret,
+ loc,
+ };
+ return lowerer.finishOptionalChain(expr, id, receiver, call, loc);
+ }
const args = expr.arguments.nodes.map((a, i) => lowerer.lowerExprExpecting(a, params[i]));
+ // Effect-free surplus arguments (a dropped `void` slot) are ignored.
+ while (narrowed.rest !== true && args.length > params.length && isSafeToDiscard(args.at(-1)!))
+ args.pop();
const body: IrExpr = { kind: "callValue", callee: recvRef, args, type: narrowed.ret, loc };
return lowerer.finishOptionalChain(expr, id, receiver, body, loc);
}
diff --git a/packages/compiler/src/frontend/lowering/expressions/property-access.ts b/packages/compiler/src/frontend/lowering/expressions/property-access.ts
index e0bfee25d..65c8fbcbc 100644
--- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts
+++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts
@@ -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,
diff --git a/packages/compiler/src/frontend/lowering/expressions/relational.ts b/packages/compiler/src/frontend/lowering/expressions/relational.ts
index 4aa62534e..91d1468c2 100644
--- a/packages/compiler/src/frontend/lowering/expressions/relational.ts
+++ b/packages/compiler/src/frontend/lowering/expressions/relational.ts
@@ -1,5 +1,6 @@
/* instanceof and in, including private-name and runtime-key in. */
import * as ts from "../../ts7/adapter.js";
+import { builtinClassInstanceOf, builtinClassKind } from "../builtins/builtin-class-dyn.js";
import {
checkedPrivateIn,
localPrivateBrandClass,
@@ -25,7 +26,7 @@ import {
typeEquals,
typeKey,
} from "../../../ir/ir.js";
-import { refuseWebAssemblyUse } from "../builtins/webassembly.js";
+import { lowerWebAssemblyInstanceof, refuseWebAssemblyUse } from "../builtins/webassembly.js";
import { lowerSocketInstanceOf } from "../lower-server.js";
import { builtinClassRefOf } from "../builtins/class-values.js";
import { lowerZlibInstanceOf } from "../builtins/compression.js";
@@ -62,6 +63,8 @@ import { nullProtoRecordShape, recordNullProtoTest } from "../null-prototype.js"
* Folding is limited to side-effect-free operands — dropping a computed
* operand would skip its effects, so those are rejected instead. */
export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc: SrcLoc): IrExpr {
+ const webAssembly = lowerWebAssemblyInstanceof(lowerer, expr);
+ if (webAssembly !== null) return webAssembly;
refuseWebAssemblyUse(lowerer, expr.right);
// `x instanceof net.Socket` over a union with a netSocket arm — the
// h2 compat 'connect' narrowing (lower-server.ts): a union tag test.
@@ -471,6 +474,22 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc
const value = lowerer.coerceInto(expr.left, lowerer.lowerExpr(expr.left), DYN);
return checkedClassInstanceOf(lowerer, value, constructor, loc);
}
+ // `this instanceof Glob` in a JavaScript constructor function: the
+ // function's prototype against the value's prototype chain.
+ if (
+ constructor?.type.kind === "func" &&
+ isJsSourceFile(expr.getSourceFile()) &&
+ lowerer.dynConvertible(constructor.type)
+ ) {
+ const value = lowerer.coerceInto(expr.left, lowerer.lowerExpr(expr.left), DYN);
+ return {
+ kind: "libCall",
+ fn: "dyn.functionInstanceOf",
+ args: [value, { kind: "dynFrom", value: constructor, type: DYN, loc }],
+ type: BOOL,
+ loc,
+ };
+ }
lowerer.unsupported(
"SC1090",
expr.right,
@@ -579,6 +598,14 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc
loc,
};
}
+ // `u instanceof EventEmitter` (or a stream class): an untyped object
+ // built by a constructor function inherits the untyped constructor's
+ // prototype (builtin-class-dyn.ts). Compiled emitters and streams have
+ // no untyped form, so the prototype chain answers for every value.
+ const builtinKind = builtinClassKind(target.def.name);
+ if (left.type.kind === "dyn" && builtinKind !== null) {
+ return builtinClassInstanceOf(left, builtinKind, loc);
+ }
if (left.type.kind === "dyn") {
lowerer.unsupported(
"SC1090",
@@ -1144,6 +1171,24 @@ function lowerPrivateIn(
}
/** The string keys every record inherits from Object.prototype. */
+/** Keys built-in errors carry outside their class fields: Node's system
+ * error properties, and AggregateError's, SuppressedError's and
+ * AssertionError's own members. */
+const BUILTIN_ERROR_SLOT_KEYS: ReadonlySet = new Set([
+ "errno",
+ "syscall",
+ "path",
+ "dest",
+ "info",
+ "errors",
+ "error",
+ "suppressed",
+ "actual",
+ "expected",
+ "operator",
+ "generatedMessage",
+]);
+
const OBJECT_PROTOTYPE_KEYS: ReadonlySet = new Set([
"constructor",
"__defineGetter__",
@@ -1360,9 +1405,10 @@ export function lowerInExpression(
// predicate's `err instanceof Error && "code" in err` shape): `code`
// answers from the runtime error's code slot (stamped by fs/system/
// event/spawn errors, absent on plain constructions — exactly Node's
- // own-property answer), and the always-declared members fold true.
- // Everything else fences: the runtime object carries no other dynamic
- // properties to ask (`stack` would be a lie — Node has one, we don't).
+ // own-property answer), and the always-present members (message, name,
+ // stack, Object.prototype's keys, members the static class declares)
+ // fold true. Other keys test the declaring subclasses and the error's
+ // own runtime properties; keys of built-in errors' extra slots fence.
if (recv.type.kind === "object") {
// %DOMException first: `'cause' in e` answers the options form's
// own-property record (the runtime slot), and code/name/message are
@@ -1384,34 +1430,77 @@ export function lowerInExpression(
if (key === "cause") {
return { kind: "libCall", fn: "error.hasCause", args: [recv], type: BOOL, loc };
}
+ // The code slot, or an own `code` with another value
+ // (`err.code = undefined` is present).
if (key === "code") {
- const codeType = lowerer.envValueType();
- if (codeType.kind !== "union")
- throw new InternalCompilerError("lowerer bug: error code type is not a union");
- const undefTag = lowerer.armTag(codeType.unionId, UNDEFINED_T);
- const read: IrExpr = {
+ return { kind: "libCall", fn: "error.hasCode", args: [recv], type: BOOL, loc };
+ }
+ // The receiver evaluates for its effects when the answer is static.
+ const always = (): IrExpr =>
+ recv.kind === "varRef" || recv.kind === "caughtNarrow"
+ ? { kind: "boolLit", value: true, type: BOOL, loc }
+ : {
+ kind: "seqExpr",
+ stmts: [{ kind: "exprStmt", expr: recv, loc }],
+ result: { kind: "boolLit", value: true, type: BOOL, loc },
+ type: BOOL,
+ loc,
+ };
+ if (key === "message" || key === "name") return always();
+ // Every Error has its own `stack`, and Error.prototype's `toString`
+ // and `constructor` are among Object.prototype's keys.
+ if (key === "stack" || OBJECT_PROTOTYPE_KEYS.has(key)) return always();
+ // A member the static class declares is always present.
+ const declares = (c: ClassInfo): boolean =>
+ c.fields.has(key) ||
+ lowerer.findMethodOn(c, key) !== null ||
+ lowerer.findMethodOn(c, `get:${key}`) !== null ||
+ lowerer.findMethodOn(c, `set:${key}`) !== null;
+ const staticInfo = lowerer.classes.get(recv.type.className);
+ if (staticInfo && declares(staticInfo)) return always();
+ // Any other key: a descendant class that declares it (instanceof), or
+ // an own property set at runtime (a throw site's extra fields,
+ // Object.assign, writes through a dynamic view). Keys built-in errors
+ // keep outside their class fields depend on the runtime error and
+ // stay fenced.
+ if (!BUILTIN_ERROR_SLOT_KEYS.has(key)) {
+ const stable = recv.kind === "varRef" || recv.kind === "caughtNarrow";
+ const slot = stable ? null : lowerer.declareHiddenLocal("%inRecv", recv.type);
+ const ref = (): IrExpr => (slot ? varRef(slot.id, recv.type, loc) : recv);
+ const tests: IrExpr[] = [];
+ const staticName = recv.type.className;
+ for (const c of lowerer.classes.values()) {
+ const name = c.def.name;
+ if (name === staticName || !lowerer.isSubclassOf(name, staticName) || !declares(c))
+ continue;
+ // A declaring ancestor below the static class already covers it.
+ if (c.base && c.base.def.name !== staticName && declares(c.base)) continue;
+ tests.push({ kind: "instanceOf", value: ref(), className: name, type: BOOL, loc });
+ }
+ tests.push({
kind: "libCall",
- fn: "error.code",
- args: [recv],
- type: codeType,
+ fn: "error.hasKey",
+ args: [ref(), { kind: "strLit", value: key, type: STRING, loc }],
+ type: BOOL,
loc,
- };
+ });
+ const answer = tests.reduce((acc, test) => ({
+ kind: "logical",
+ op: "||",
+ left: acc,
+ right: test,
+ type: BOOL,
+ loc,
+ }));
+ if (!slot) return answer;
return {
- kind: "unionIsTag",
- unionId: codeType.unionId,
- tag: undefTag,
- negated: true,
- value: read,
+ kind: "seqExpr",
+ stmts: [{ kind: "varDecl", localId: slot.id, init: recv, loc }],
+ result: answer,
type: BOOL,
loc,
};
}
- if (
- (key === "message" || key === "name") &&
- (recv.kind === "varRef" || recv.kind === "caughtNarrow")
- ) {
- return { kind: "boolLit", value: true, type: BOOL, loc };
- }
lowerer.unsupported(
"SC1090",
expr,
diff --git a/packages/compiler/src/frontend/lowering/expressions/updates.ts b/packages/compiler/src/frontend/lowering/expressions/updates.ts
index 2270f6043..41d8f6332 100644
--- a/packages/compiler/src/frontend/lowering/expressions/updates.ts
+++ b/packages/compiler/src/frontend/lowering/expressions/updates.ts
@@ -42,6 +42,7 @@ import type { CompoundOp } from "../surfaces.js";
import { tryLowerExpression } from "./try-lower-expression.js";
import { hasRuntimeStatics } from "../class-runtime-statics.js";
import { declareContextLocal } from "../function-context.js";
+import { keyedFieldTarget, keyedFieldWrite, saveKeyedFieldKey } from "./keyed-field-targets.js";
/** Read-modify-write updates read the member first: an unassigned
* deferred-init receiver throws Node's member-read TypeError. */
@@ -337,10 +338,11 @@ export function lowerIncDec(
}
const target = ts.isPropertyAccessExpression(access)
? lowerer.fieldTarget(access)
- : symbolFieldTarget(lowerer, access);
+ : (symbolFieldTarget(lowerer, access) ?? keyedFieldTarget(lowerer, access, true));
if (target && lowerer.dynConvertible(target.fieldType)) {
checkDeferredMemberReceiver(lowerer, target, access);
target.obj = save(target.obj, "%incrementReceiver");
+ saveKeyedFieldKey(target, save);
return finish(lowerer.fieldGetExpr(target, loc, access), (value) =>
lowerer.fieldSetStmt(
target,
@@ -617,6 +619,7 @@ export function fieldSetStmt(
};
}
}
+ if (target.container === "recordKeyed") return keyedFieldWrite(lowerer, target, value, loc);
if (target.container === "dynamic")
return {
kind: "exprStmt",
@@ -872,12 +875,13 @@ export function lowerFieldCompoundValue(
const targetOf = (): FieldTarget | null =>
ts.isPropertyAccessExpression(access)
? lowerer.fieldTarget(access)
- : symbolFieldTarget(lowerer, access);
+ : (symbolFieldTarget(lowerer, access) ?? keyedFieldTarget(lowerer, access, true));
const target = targetOf();
if (!target)
lowerer.unsupported("SC1090", access, "compound assignment to unsupported field targets");
checkDeferredMemberReceiver(lowerer, target, access);
target.obj = save(target.obj, "%compoundReceiver");
+ saveKeyedFieldKey(target, save);
// Accessors observe getter, RHS side effects, then setter, all through
// the saved receiver.
const fieldRead = lowerer.fieldGetExpr(target, locOf(access), access);
diff --git a/packages/compiler/src/frontend/lowering/function-context.ts b/packages/compiler/src/frontend/lowering/function-context.ts
index e31e194c5..92042b7d7 100644
--- a/packages/compiler/src/frontend/lowering/function-context.ts
+++ b/packages/compiler/src/frontend/lowering/function-context.ts
@@ -48,6 +48,16 @@ export interface FnCtx {
argumentsLocal?: IrLocal | null;
/** Arrows inherit the nearest ordinary function's arguments binding. */
inheritsArguments?: boolean;
+ /** A function lowered on demand on top of an unrelated caller's frames
+ * (a generic instance): lexical `this` resolution stops here, so the
+ * body sees what it sees when lowered alone. */
+ thisBoundary?: true;
+ /** Tuple bindings of this function that are only forwarded
+ * (typed-arguments.ts onlyForwarded): binding local id → the native
+ * slots forwarding reads. Such a binding is never materialized. Local ids
+ * are unique within one function only, and nested functions never read
+ * these bindings (a read there needs the tuple). */
+ pureTuples: Map;
/** Declared return type — lets `return` detect record-shape mismatches
* (SC2002) before the validator would ICE on them. */
returnType: IrType;
@@ -81,6 +91,7 @@ export function newFnCtx(
captures: lifted ? [] : null,
captureSources: [],
captureBySymbol: new Map(),
+ pureTuples: new Map(),
selfSymbol,
selfType,
returnType,
@@ -154,7 +165,16 @@ export function captureContextBinding(
throw new InternalCompilerError("lowerer bug: no active function context");
const direct = bindingInContext(current, symbol);
if (direct !== null) return { local: direct, origin: direct, error: null };
- for (let depth = stack.length - 2; depth >= 0; depth--) {
+ let floor = 0;
+ if (symbol === undefined) {
+ for (let depth = stack.length - 1; depth >= 0; depth--) {
+ if (stack[depth]!.thisBoundary) {
+ floor = depth;
+ break;
+ }
+ }
+ }
+ for (let depth = stack.length - 2; depth >= floor; depth--) {
const origin = bindingInContext(stack[depth]!, symbol);
if (origin === null) continue;
diff --git a/packages/compiler/src/frontend/lowering/general-streams.ts b/packages/compiler/src/frontend/lowering/general-streams.ts
new file mode 100644
index 000000000..0113a9958
--- /dev/null
+++ b/packages/compiler/src/frontend/lowering/general-streams.ts
@@ -0,0 +1,114 @@
+/* The general stream representation (TypeMapperCtx.generalNodeStreams).
+ *
+ * NodeJS.ReadableStream and NodeJS.WritableStream (and @types/node's
+ * stream.Readable / stream.Writable, which also type child stdio) name the
+ * interface every Node stream implements. By default they map to the
+ * child/process stdio handles: the specialized representation of the
+ * values that commonly fill them (child.stdout, process.stdout). A program
+ * that moves another stream — fs.createReadStream(), a zlib Transform, a
+ * Readable subclass — into one of those slots needs the general
+ * representation instead: the runtime stream classes, with the stdio
+ * handles converted into them where they are read or flow.
+ *
+ * The decision is per program. Reachable emit lowers without it; a flow
+ * that needs it marks the lowerer (needsGeneralNodeStreams) and lowerToIr
+ * lowers the whole program again with it. Programs without such a flow
+ * never take the second pass, so their IR is unchanged. */
+import type { IrExpr, IrType } from "../../ir/ir.js";
+import { everyTypeChild } from "../../ir/traverse.js";
+import * as ts from "../ts7/adapter.js";
+import { isGeneralNodeStreamInterface } from "../type-mapper.js";
+import type { Lowerer } from "./lowerer.js";
+import { streamSidesOf } from "./lower-stream.js";
+
+const STDIO_STREAM_KINDS: ReadonlySet = new Set([
+ "childStream",
+ "childWriter",
+ "procStream",
+]);
+
+/** Whether `type` (through union arms, record fields and the structural
+ * type children) contains a part satisfying `test`. */
+function containsPart(lowerer: Lowerer, type: IrType, test: (part: IrType) => boolean): boolean {
+ const seen = new Set();
+ const walk = (part: IrType): boolean => {
+ if (test(part)) return true;
+ if (part.kind === "union") {
+ if (seen.has(`u:${part.unionId}`)) return false;
+ seen.add(`u:${part.unionId}`);
+ return (lowerer.unions.get(part.unionId)?.arms ?? []).some(walk);
+ }
+ if (part.kind === "record") {
+ if (seen.has(`r:${part.shapeId}`)) return false;
+ seen.add(`r:${part.shapeId}`);
+ return (lowerer.shapes.get(part.shapeId)?.fields ?? []).some((f) => walk(f.type));
+ }
+ let found = false;
+ everyTypeChild(part, (child) => {
+ found = walk(child);
+ return !found;
+ });
+ return found;
+ };
+ return walk(type);
+}
+
+function isGeneralStream(lowerer: Lowerer, part: IrType): boolean {
+ return (
+ part.kind === "object" && streamSidesOf(lowerer, lowerer.classes.get(part.className)) !== null
+ );
+}
+
+/** A mismatched flow (requireExactShape's input) that the general stream
+ * representation resolves: a general stream into a stdio-represented
+ * slot, a child pipe handle into an 'unknown' slot, or an 'unknown' value
+ * into a child-pipe slot (the general classes take a checked cast). */
+export function needsGeneralNodeStreams(
+ lowerer: Lowerer,
+ actual: IrType,
+ expected: IrType,
+): boolean {
+ const stdio = (part: IrType): boolean => STDIO_STREAM_KINDS.has(part.kind);
+ const pipeHandle = (part: IrType): boolean =>
+ part.kind === "childStream" || part.kind === "childWriter";
+ if (actual.kind === "dyn") return containsPart(lowerer, expected, pipeHandle);
+ if (containsPart(lowerer, expected, stdio))
+ return containsPart(lowerer, actual, (p) => isGeneralStream(lowerer, p));
+ return expected.kind === "dyn" && containsPart(lowerer, actual, pipeHandle);
+}
+
+/** Under the general representation, process.stdout / process.stderr (the
+ * procStream fd scalar) flowing into a runtime Writable slot: an
+ * fd-backed Writable whose writes take the same synchronous path as
+ * process.stdout.write. Null when the flow is not that one. */
+export function stdioIntoGeneralStream(
+ lowerer: Lowerer,
+ expr: IrExpr,
+ expected: IrType,
+): IrExpr | null {
+ if (!lowerer.generalNodeStreams || expr.type.kind !== "procStream") return null;
+ const writable: IrType = { kind: "object", className: "%Writable" };
+ const accepts =
+ (expected.kind === "object" && expected.className === "%Writable") ||
+ (expected.kind === "union" && lowerer.armTag(expected.unionId, writable) >= 0);
+ if (!accepts) return null;
+ return {
+ kind: "libCall",
+ fn: "stream.stdioWritable",
+ args: [expr],
+ type: writable,
+ loc: expr.loc,
+ };
+}
+
+/** A stdio-handle receiver whose member has no lowering on the handle
+ * (`out.on("error", ...)`, `child.stdout.pipe(...)`, `output.end()`): when
+ * its declared type is one the general representation maps to a runtime
+ * stream class, the program lowers again with it, where the full stream
+ * surface applies. The caller still reports its fence for this pass. */
+export function requestGeneralStreamSurface(lowerer: Lowerer, receiver: ts.Expression): void {
+ if (lowerer.generalNodeStreams) return;
+ const type = lowerer.checker.getNonNullableType(lowerer.typeOf(receiver));
+ if (isGeneralNodeStreamInterface(type, lowerer.typeCtx))
+ lowerer.requiresGeneralNodeStreams = true;
+}
diff --git a/packages/compiler/src/frontend/lowering/generic-functions.ts b/packages/compiler/src/frontend/lowering/generic-functions.ts
index f588faf6d..b586884c5 100644
--- a/packages/compiler/src/frontend/lowering/generic-functions.ts
+++ b/packages/compiler/src/frontend/lowering/generic-functions.ts
@@ -1,3 +1,4 @@
+import { staticNameBindings } from "./static-function-names.js";
import * as ts from "../ts7/adapter.js";
import { bodyReadsArguments } from "../arguments-usage.js";
import type { Lowerer } from "./lowerer.js";
@@ -100,6 +101,9 @@ export interface GenericFnInfo {
* names where the slot binds the call site's object type. Computed on
* first use. */
implicitPatterns?: (ImplicitPattern | null)[];
+ /** Parameter index → symbol for the function parameters whose `.name`
+ * the body reads (static-function-names.ts); computed on first use. */
+ nameReadParams?: Map;
}
export interface GenericInstance {
@@ -140,6 +144,9 @@ export interface GenericInstance {
implicitInferReturn?: true;
/** An erased instance (every type parameter bound to `unknown`). */
erased?: true;
+ /** Name-keyed instances: function parameter → its call site's static
+ * name (null when only known at run time), folded at `.name` reads. */
+ staticFnNames?: Map;
}
/** Defaults bind during inference; collection only resolves symbols in
@@ -295,7 +302,13 @@ export function genericCallInstance(
const retTs = lowerer.checker.getReturnTypeOfSignature(rsig);
const local = localClassResultInstance(lowerer, expr, info, rsig, params, retTs);
if (local) return local;
- const mappedReturn = lowerer.mapTypeOf(retTs);
+ // A result instantiated at `any` (a type argument inferred from an
+ // untyped value, such as an untyped forwarding call) is checked-dynamic.
+ const mappedReturn =
+ lowerer.mapTypeOf(retTs) ??
+ ((retTs.flags & ts.TypeFlags.Any) !== 0 && !isJsSourceFile(info.decl.getSourceFile())
+ ? DYN
+ : null);
if (!mappedReturn) {
fenceGenericSignatureResult(lowerer, expr, retTs);
lowerer.badType(expr, retTs);
@@ -317,6 +330,7 @@ export function genericCallInstance(
// different fields). Non-literal bindings (a key union, plain string)
// share one runtime-keyed instance per IR signature, exactly the
// widened discipline.
+ const named = staticNameBindings(lowerer, expr, info);
const keyofTps = keyofConstrainedTypeParams(info);
if (keyofTps.size > 0) {
const tsBindings = new Map();
@@ -333,12 +347,19 @@ export function genericCallInstance(
})
.join(",");
return internGenericInstance(lowerer, expr, info, params, returnType, () => bindings, {
- extraKey: `@${litKey}`,
+ extraKey: `@${litKey}${named?.key ?? ""}`,
tsBindings,
+ ...(named ? { staticFnNames: named.names } : {}),
});
}
- return internGenericInstance(lowerer, expr, info, params, returnType, (tsBindings) =>
- lowerer.inferTypeParamBindings(expr, info, rsig, tsBindings),
+ return internGenericInstance(
+ lowerer,
+ expr,
+ info,
+ params,
+ returnType,
+ (tsBindings) => lowerer.inferTypeParamBindings(expr, info, rsig, tsBindings),
+ named ? { extraKey: named.key, staticFnNames: named.names } : undefined,
);
}
@@ -498,7 +519,11 @@ function internGenericInstance(
params: ParamShape[],
returnType: IrType,
makeBindings: (tsBindings: Map) => Map,
- opts?: { extraKey?: string; tsBindings?: Map },
+ opts?: {
+ extraKey?: string;
+ tsBindings?: Map;
+ staticFnNames?: Map;
+ },
): GenericInstance {
// keyof-constrained instantiations append their literal keys
// (extraKey): the IR signature alone under-discriminates there — two
@@ -541,6 +566,7 @@ function internGenericInstance(
bindings,
tsBindings,
typeArgsText,
+ ...(opts?.staticFnNames ? { staticFnNames: opts.staticFnNames } : {}),
};
info.instances.set(key, inst);
lowerer.instantiationQueue.push({ info, inst });
@@ -671,7 +697,11 @@ function unifySignatureBindings(
tsBindings.set(sym, lowerer.typeParamTsResolver(inst) ?? inst);
}
if (!bindings.has(sym)) {
- const mapped = lowerer.mapTypeOf(inst);
+ const mapped =
+ lowerer.mapTypeOf(inst) ??
+ ((inst.flags & ts.TypeFlags.Any) !== 0 && !isJsSourceFile(info.decl.getSourceFile())
+ ? DYN
+ : null);
// A visitor's inferred T can be void. It still binds the
// return convention of `(node) => T`; discarding it leaves
// the callback's return type spuriously uninstantiated.
@@ -824,6 +854,8 @@ export function lowerGenericInstance(
const prevSuppress = lowerer.suppressStats;
const prevClass = lowerer.currentClass;
const prevImplicit = lowerer.implicitParamTypes;
+ const prevStaticFnNames = lowerer.staticFnNames;
+ lowerer.staticFnNames = inst.staticFnNames ?? null;
// A generic METHOD of a generic-class INSTANTIATION lowers under BOTH
// binding sets: the receiver instantiation's class type parameters
// underneath, the method instantiation's own on top (disjoint symbol
@@ -873,6 +905,8 @@ export function lowerGenericInstance(
: lowerer.bodyReturnType(isAsync, inst.returnType);
const fnCtx = newFnCtx(false, null, null, bodyReturn);
fnCtx.isAsync = isAsync;
+ // Instances lower on demand, on top of whichever caller reached them.
+ fnCtx.thisBoundary = true;
// Implicit-any instances whose declared return did not map lower in
// return-INFERENCE mode: `return` statements record here bare, and the
// post-pass (resolveInferredReturn) settles the type and wraps them.
@@ -984,6 +1018,7 @@ export function lowerGenericInstance(
lowerer.typeParamBindings = prevBindings;
lowerer.typeParamTsBindings = prevTsBindings;
lowerer.implicitParamTypes = prevImplicit;
+ lowerer.staticFnNames = prevStaticFnNames;
lowerer.instantiationContext = prevContext;
lowerer.localClassInstantiations.pop();
lowerer.suppressStats = prevSuppress;
diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts
index 5d326709c..b0b7884f2 100644
--- a/packages/compiler/src/frontend/lowering/lower-calls.ts
+++ b/packages/compiler/src/frontend/lowering/lower-calls.ts
@@ -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 = 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,
+ };
+}
diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts
index bce4ac6f0..680812905 100644
--- a/packages/compiler/src/frontend/lowering/lower-classes.ts
+++ b/packages/compiler/src/frontend/lowering/lower-classes.ts
@@ -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;
+ /** Own TypeScript overrides whose declared result differs from the
+ * inherited ABI's but converts into it (`Promise` over `any`, a
+ * union narrowed to its inherited arm): the slot function `%C.m` keeps the
+ * inherited ABI and converts the result of the body, which lowers as
+ * `%C.m$own` with the declared result (async when declared so). */
+ returnAdapters?: Map;
/** OWN GENERIC instance methods (own type parameters — `m(x: T)`),
* monomorphized per call site like top-level generic functions: instance
* `n` is the module function `%C.m%n` taking `this` as param 0. They
@@ -2256,6 +2263,7 @@ export function collectClassShapeInner(
}
>();
const methodEntryFences = new Map();
+ const returnAdapters = new Map();
// Own accessor declarations ("get:x"/"set:x" → node), for the
// partial-override analysis below (diagnostics need the node).
const accessorNodes = new Map();
@@ -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` over
+ // an `any` base, `string | undefined` over `string`): the slot
+ // keeps the inherited ABI and converts the body's result at the
+ // boundary (lowerClassMethodMemberInner).
+ const ownAsync =
+ member.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) === true;
+ if (
+ !fencedJsOverride &&
+ !functionLocalClass(decl) &&
+ overridden.sig.gen === undefined &&
+ // An async base's slot answers its promise: a Promise type, or
+ // the checked-dynamic value of an untyped async method.
+ (overridden.sig.async !== true ||
+ overridden.sig.ret.kind === "promise" ||
+ overridden.sig.ret.kind === "dyn") &&
+ !overridden.sig.abstract &&
+ overridden.sig.params.length === shapes.length &&
+ overridden.sig.params.every(
+ (p, i) =>
+ p.mode === shapes[i]!.mode &&
+ (p.mode === "required" || p.mode === "omittable") &&
+ // Each inherited argument converts into the override's
+ // parameter: equal, a checked extraction from an `any`
+ // slot, or a widening (a contravariant parameter).
+ (typeEquals(p.type, shapes[i]!.type) ||
+ (p.type.kind === "dyn" &&
+ canDynCheckTo(
+ shapes[i]!.type,
+ (id) => lowerer.shapes.get(id),
+ (id) => lowerer.unions.get(id),
+ )) ||
+ lowerer.coercibleValue(p.type, shapes[i]!.type)),
+ ) &&
+ (ownAsync ? ft.ret.kind === "promise" : true) &&
+ (typeEquals(ft.ret, overridden.sig.ret) ||
+ (overridden.sig.ret.kind === "dyn"
+ ? ft.ret.kind !== "void" && lowerer.dynConvertible(ft.ret)
+ : lowerer.coercibleValue(ft.ret, overridden.sig.ret) ||
+ // A promise whose payload widens into the inherited one
+ // (`Promise` over an untyped async `Promise`).
+ (ft.ret.kind === "promise" &&
+ overridden.sig.ret.kind === "promise" &&
+ lowerer.promisePayloadWidens(ft.ret, overridden.sig.ret)) ||
+ // A JavaScript override's inferred `any` result checks
+ // into the inherited type (`toString() { return
+ // this.message }` over Error's).
+ (ft.ret.kind === "dyn" &&
+ isJsSourceFile(member.getSourceFile()) &&
+ overridden.sig.ret.kind !== "void" &&
+ canDynCheckTo(
+ overridden.sig.ret,
+ (id) => lowerer.shapes.get(id),
+ (id) => lowerer.unions.get(id),
+ ))))
+ ) {
+ returnAdapters.set(mName, { params: [...shapes], ret: ft.ret, async: ownAsync });
+ shapes.splice(0, shapes.length, ...overridden.sig.params);
+ ft.ret = overridden.sig.ret;
+ if (overridden.sig.async === true) thunkAsyncSlot(overridden, mName);
+ } else if (fencedJsOverride) {
// A dormant JS method must not poison the whole class. Keep
// the inherited call/dispatch ABI and refuse at method entry;
// lowering this body with the base signature would change JS.
@@ -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,
diff --git a/packages/compiler/src/frontend/lowering/lower-comptime.ts b/packages/compiler/src/frontend/lowering/lower-comptime.ts
index 41c6704c5..f239eea82 100644
--- a/packages/compiler/src/frontend/lowering/lower-comptime.ts
+++ b/packages/compiler/src/frontend/lowering/lower-comptime.ts
@@ -4,7 +4,7 @@ import { InternalCompilerError } from "../../errors.js";
* literals — records/arrays/unions included, subject to comptimeBakeable. */
import * as ts from "../ts7/adapter.js";
import type { Lowerer } from "./lowerer.js";
-import { type IrExpr, type IrType } from "../../ir/ir.js";
+import { isUnitType, type IrExpr, type IrType } from "../../ir/ir.js";
import { comptimeFailedDiag } from "../../diagnostics/diagnostic.js";
import { locOf } from "../program.js";
import { PoisonError } from "./lowerer.js";
@@ -76,7 +76,7 @@ export function lowerComptime(lowerer: Lowerer, expr: ts.CallExpression): IrExpr
"SC1090",
expr,
`baking comptime results of type '${lowerer.fmt(target)}' into the binary ` +
- `(only number, string, boolean, arrays, and records lower to literals)`,
+ `(number, string, boolean, and arrays, records and unions of those lower to literals; a record or union type reachable from itself does not)`,
);
}
// 5. The host executes the extracted callback under a finite budget.
@@ -110,20 +110,104 @@ function comptimeFailureDetail(error: unknown): string {
/** True when a comptime result of this type can be written as a literal
* expression: number/string/boolean, arrays and record shapes of those,
- * nested. Closures, class instances, unions, promises, dyn, and void
- * cannot be baked. */
+ * and unions of those with null or undefined (the mixed rows of a JSON
+ * table: `(string | number)[][]`), nested. Closures, class instances,
+ * promises, dyn, void, and recursive types (a record or union reachable
+ * from itself, directly or through arrays) cannot be baked. */
export function comptimeBakeable(lowerer: Lowerer, t: IrType): boolean {
+ return bakeableWithin(lowerer, t, new Set());
+}
+
+/** comptimeBakeable below the records and unions in `open` (keyed
+ * `record:` and `union:`), whose members are being
+ * checked: reaching one of them again means the type is recursive. */
+function bakeableWithin(lowerer: Lowerer, t: IrType, open: Set): boolean {
switch (t.kind) {
case "f64":
case "string":
case "bool":
return true;
case "array":
- return lowerer.comptimeBakeable(t.elem);
+ return bakeableWithin(lowerer, t.elem, open);
case "record": {
const shape = lowerer.shapes.get(t.shapeId);
- return !!shape && shape.fields.every((f) => lowerer.comptimeBakeable(f.type));
+ const key = `record:${t.shapeId}`;
+ if (!shape || open.has(key)) return false;
+ open.add(key);
+ const bakeable = shape.fields.every((f) => bakeableWithin(lowerer, f.type, open));
+ open.delete(key);
+ return bakeable;
}
+ case "union": {
+ const arms = lowerer.unions.get(t.unionId)?.arms;
+ const key = `union:${t.unionId}`;
+ if (arms === undefined || open.has(key)) return false;
+ open.add(key);
+ const bakeable = arms.every(
+ (arm) => isUnitType(arm) || (arm.kind !== "union" && bakeableWithin(lowerer, arm, open)),
+ );
+ open.delete(key);
+ return bakeable;
+ }
+ default:
+ return false;
+ }
+}
+
+/** The tag of the arm of union `unionId` that `value` bakes into, or -1.
+ * A record value whose discriminant field holds one of the union's
+ * discriminant values takes that case's arm, since arms with overlapping
+ * shapes can both fit it. Any other value takes the first arm it fits, in
+ * the union's arm order: a JSON value fits exactly one scalar arm, and
+ * array or record arms are told apart by their contents. */
+function comptimeUnionArm(lowerer: Lowerer, unionId: string, value: unknown): number {
+ const def = lowerer.unions.get(unionId);
+ if (!def) return -1;
+ const discriminant = def.discriminant;
+ if (
+ discriminant !== undefined &&
+ typeof value === "object" &&
+ value !== null &&
+ !Array.isArray(value)
+ ) {
+ const key = (value as Record)[discriminant.field];
+ const named = discriminant.cases.find((c) => c.values.some((v) => v === key));
+ if (named !== undefined)
+ return comptimeFits(lowerer, value, def.arms[named.tag]!) ? named.tag : -1;
+ }
+ return def.arms.findIndex((arm) => comptimeFits(lowerer, value, arm));
+}
+
+/** Whether `value` bakes into the bakeable type `t` without a mismatch:
+ * the arm choice of a union literal. Records are exact, as in
+ * comptimeValueToIr; an absent field reads as undefined. */
+function comptimeFits(lowerer: Lowerer, value: unknown, t: IrType): boolean {
+ switch (t.kind) {
+ case "f64":
+ return typeof value === "number" && Number.isFinite(value);
+ case "string":
+ return typeof value === "string";
+ case "bool":
+ return typeof value === "boolean";
+ case "nullT":
+ return value === null;
+ case "undefinedT":
+ return value === undefined;
+ case "array":
+ return Array.isArray(value) && value.every((el) => comptimeFits(lowerer, el, t.elem));
+ case "record": {
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
+ const shape = lowerer.shapes.get(t.shapeId);
+ if (!shape) return false;
+ const obj = value as Record;
+ const declared = new Set(shape.fields.map((f) => f.name));
+ return (
+ Object.keys(obj).every((key) => declared.has(key)) &&
+ shape.fields.every((f) => comptimeFits(lowerer, obj[f.name], f.type))
+ );
+ }
+ case "union":
+ return comptimeUnionArm(lowerer, t.unionId, value) >= 0;
default:
return false;
}
@@ -265,6 +349,23 @@ export function comptimeValueToIr(
}));
return { kind: "recordLit", fields, type: expected, loc };
}
+ case "union": {
+ const arms = lowerer.unions.get(expected.unionId)!.arms; // bakeable-checked
+ const tag = comptimeUnionArm(lowerer, expected.unionId, value);
+ if (tag < 0) fail(describeComptimeValue(value));
+ const arm = arms[tag]!;
+ const payload: IrExpr = isUnitType(arm)
+ ? { kind: "unitLit", unit: arm.kind === "nullT" ? "null" : "undefined", type: arm, loc }
+ : lowerer.comptimeValueToIr(value, arm, path, blame);
+ return {
+ kind: "unionWrap",
+ unionId: expected.unionId,
+ tag,
+ value: payload,
+ type: expected,
+ loc,
+ };
+ }
default:
// comptimeBakeable already fenced these off.
throw new InternalCompilerError(`lowerer bug: unbakeable comptime target '${expected.kind}'`);
diff --git a/packages/compiler/src/frontend/lowering/lower-event-emitter.ts b/packages/compiler/src/frontend/lowering/lower-event-emitter.ts
index 06e9c4ab1..4b8b9ea14 100644
--- a/packages/compiler/src/frontend/lowering/lower-event-emitter.ts
+++ b/packages/compiler/src/frontend/lowering/lower-event-emitter.ts
@@ -1245,7 +1245,19 @@ export function lowerEmitterMethodCall(
);
}
const p = cbT.params[0];
- if (p !== undefined && !(p.kind === "bytes" && p.elem === "u8") && p.kind !== "string") {
+ // A `Buffer | string` chunk (NodeJS.ReadableStream's listener
+ // shape) takes whichever side the stream delivers.
+ const chunkUnion =
+ p?.kind === "union" &&
+ (lowerer.unions.get(p.unionId)?.arms ?? []).every(
+ (arm) => (arm.kind === "bytes" && arm.elem === "u8") || arm.kind === "string",
+ );
+ if (
+ p !== undefined &&
+ !(p.kind === "bytes" && p.elem === "u8") &&
+ p.kind !== "string" &&
+ !chunkUnion
+ ) {
lowerer.noLowering(
`a 'data' listener whose chunk parameter is '${lowerer.fmt(p)}'`,
args[1]!,
diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts
index 749b3f535..9047cf2c5 100644
--- a/packages/compiler/src/frontend/lowering/lower-exprs.ts
+++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts
@@ -1,4 +1,4 @@
-import { refuseWebAssemblyUse } from "./builtins/webassembly.js";
+import { lowerWebAssemblyValue, refuseWebAssemblyUse } from "./builtins/webassembly.js";
import { dynUndefinedExpr, nodeThrowExpr, numLit, strLit, varRef } from "../../ir/build.js";
import { InternalCompilerError } from "../../errors.js";
/* Expression lowering: the expression dispatch (lowerExpr), literals
@@ -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);
diff --git a/packages/compiler/src/frontend/lowering/lower-functions.ts b/packages/compiler/src/frontend/lowering/lower-functions.ts
index 0ef601672..f0f2c45ea 100644
--- a/packages/compiler/src/frontend/lowering/lower-functions.ts
+++ b/packages/compiler/src/frontend/lowering/lower-functions.ts
@@ -1,3 +1,4 @@
+import { declareTypedArguments } from "./typed-arguments.js";
import { varRef } from "../../ir/build.js";
import { InternalCompilerError } from "../../errors.js";
import * as ts from "../ts7/adapter.js";
@@ -309,6 +310,8 @@ export function lowerLambda(
: null;
const { params, prologue } = lowerer.declareParams(node.parameters.nodes, shapes);
if (receiver) prologue.unshift(receiver);
+ if (ts.isFunctionExpression(node) && lowerer.typedArgumentsLambdas.has(node))
+ declareTypedArguments(lowerer, node, shapes, params, prologue);
declareFunctionArguments(
lowerer,
node,
diff --git a/packages/compiler/src/frontend/lowering/lower-inspect.ts b/packages/compiler/src/frontend/lowering/lower-inspect.ts
index 89b56a1ee..1310469fb 100644
--- a/packages/compiler/src/frontend/lowering/lower-inspect.ts
+++ b/packages/compiler/src/frontend/lowering/lower-inspect.ts
@@ -1,4 +1,5 @@
import { dynUndefinedExpr, boolLit, helperLoc, numLit, strLit, varRef } from "../../ir/build.js";
+import { builtinClassDynValueOf, builtinClassKindOf } from "./builtins/builtin-class-dyn.js";
import { InternalCompilerError } from "../../errors.js";
import * as ts from "../ts7/adapter.js";
@@ -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));
diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts
index 16dc0a2ca..bea6c7f7d 100644
--- a/packages/compiler/src/frontend/lowering/lower-modules.ts
+++ b/packages/compiler/src/frontend/lowering/lower-modules.ts
@@ -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.
diff --git a/packages/compiler/src/frontend/lowering/lower-stdlib.ts b/packages/compiler/src/frontend/lowering/lower-stdlib.ts
index 3ad934ccc..78d690c6b 100644
--- a/packages/compiler/src/frontend/lowering/lower-stdlib.ts
+++ b/packages/compiler/src/frontend/lowering/lower-stdlib.ts
@@ -4,6 +4,7 @@ import { InternalCompilerError } from "../../errors.js";
import * as ts from "../ts7/adapter.js";
import { lowerToNumberArgument } from "./lower-exprs.js";
import type { Lowerer } from "./lowerer.js";
+import { isDeclaredInAmbientNamespace } from "../type-mapper.js";
import {
arrayOf,
BOOL,
@@ -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;
}
diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts
index f004ac15f..ed5d97480 100644
--- a/packages/compiler/src/frontend/lowering/lower-stmts.ts
+++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts
@@ -1,5 +1,7 @@
+import { lowerTupleAliasDecl, stripTypeWrappers } from "./typed-arguments.js";
import { declaredMemberOf } from "./expressions/property-access.js";
import { hostsAll } from "./expressions/hosted-narrowing.js";
+import { requestGeneralStreamSurface } from "./general-streams.js";
import {
matchAllRowsReadByElements,
storedMatchAllRowsReadByElements,
@@ -1495,6 +1497,8 @@ const THROWN_AS_DYN: ReadonlySet = new Set([
]);
export function lowerStmt(lowerer: Lowerer, stmt: ts.Statement): IrStmt | IrStmt[] | null {
+ if (ownPrototypeResetStatement(lowerer, stmt))
+ return { kind: "block", body: [], loc: locOf(stmt) };
if (ts.isClassDeclaration(stmt)) {
if (!stmt.name) lowerer.unsupported("SC1090", stmt, "anonymous local class declarations");
const info = lowerer.lowerClassExpressionInfo(stmt);
@@ -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 = 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;
diff --git a/packages/compiler/src/frontend/lowering/lower-stream.ts b/packages/compiler/src/frontend/lowering/lower-stream.ts
index e9424aec0..eb919909b 100644
--- a/packages/compiler/src/frontend/lowering/lower-stream.ts
+++ b/packages/compiler/src/frontend/lowering/lower-stream.ts
@@ -49,6 +49,7 @@ import { sideEffectFreeOptionValue } from "./surfaces.js";
import { lowerDynObjectLiteral } from "./expressions/object-literals.js";
import { dynUndefinedExpr, varRef } from "../../ir/build.js";
import { genResultRecord } from "../type-mapper.js";
+import { requestGeneralStreamSurface } from "./general-streams.js";
import { extractIteratorValue, genStep, genValue, stepDrivable } from "./lower-generators.js";
import {
BOOL,
@@ -586,6 +587,33 @@ function dynOptionsValue(lowerer: Lowerer, arg: ts.Expression): IrExpr | null {
return v.type.kind === "dyn" ? v : null;
}
+/** @types/node declares the transform/flush completion callback as
+ * `(error?: Error | null, data?: any) => void`. Outside objectMode Node
+ * accepts only a Buffer, a string or nothing as `data`, so the callback
+ * parameter is typed with that union while the option callback lowers: a
+ * checked-dynamic value passed as `data` converts at the call. Returns the
+ * pinned parameter symbol (the caller unpins it), or null when the
+ * declared type needs no pin. */
+function pinAnyDataCompletion(
+ lowerer: Lowerer,
+ which: string,
+ fnNode: ts.MethodDeclaration | ts.FunctionExpression | ts.ArrowFunction,
+): ts.Symbol | null {
+ const index = which === "transform" ? 2 : which === "flush" ? 0 : -1;
+ const param = index < 0 ? undefined : fnNode.parameters.nodes[index];
+ if (param === undefined || !ts.isIdentifier(param.name) || param.dotDotDotToken) return null;
+ const symbol = lowerer.checker.getSymbolAtLocation(param.name);
+ if (!symbol || lowerer.stageSourceTypes.has(symbol)) return null;
+ const declared = lowerer.paramShape(param).type;
+ if (declared.kind !== "func" || declared.rest || declared.params[1]?.kind !== "dyn") return null;
+ const data = unionOf(lowerer, [BYTES, STRING, { kind: "undefinedT" }]);
+ lowerer.stageSourceTypes.set(
+ symbol,
+ funcOf([declared.params[0]!, data, ...declared.params.slice(2)], declared.ret),
+ );
+ return symbol;
+}
+
/** Parses the options object literal of a stream constructor. `which`
* names the callbacks this class accepts, in canonical flag order. */
function parseStreamOptions(
@@ -671,22 +699,27 @@ function parseStreamOptions(
continue;
}
const full = tuples[name]!.map((t) => (t === "fn" ? null : t));
- // The completion-callback tail: typed by the checker contextually;
- // accept the user's declared func type as the tuple's entry so the
- // prefix check compares like with like.
- const { funcType } = lowerer.lambdaSignature(fnNode);
- const tuple: IrType[] = full.map((t, i) => {
- if (t !== null) return t;
- const declared = funcType.params[i];
- if (declared !== undefined && declared.kind !== "func" && declared.kind !== "dyn") {
- lowerer.noLowering(
- `a '${name}' callback whose parameter ${i} is '${lowerer.fmt(declared)}' where Node passes the completion callback`,
- fnNode!,
- );
- }
- return declared ?? funcOf([], VOID);
- });
- present.set(name, lowerStreamCallback(lowerer, name, fnNode, thisType, tuple));
+ const pinned = pinAnyDataCompletion(lowerer, name, fnNode);
+ try {
+ // The completion-callback tail: typed by the checker contextually;
+ // accept the user's declared func type as the tuple's entry so the
+ // prefix check compares like with like.
+ const { funcType } = lowerer.lambdaSignature(fnNode);
+ const tuple: IrType[] = full.map((t, i) => {
+ if (t !== null) return t;
+ const declared = funcType.params[i];
+ if (declared !== undefined && declared.kind !== "func" && declared.kind !== "dyn") {
+ lowerer.noLowering(
+ `a '${name}' callback whose parameter ${i} is '${lowerer.fmt(declared)}' where Node passes the completion callback`,
+ fnNode!,
+ );
+ }
+ return declared ?? funcOf([], VOID);
+ });
+ present.set(name, lowerStreamCallback(lowerer, name, fnNode, thisType, tuple));
+ } finally {
+ if (pinned) lowerer.stageSourceTypes.delete(pinned);
+ }
continue;
}
// `{ signal }` shorthand: the binding is the value.
@@ -2044,6 +2077,24 @@ function lowerStreamArg(
const v = isHashValueType(lowered.type)
? hashStreamOf(lowered, what === "pipeline" || what === "compose")
: lowered;
+ // A checked-dynamic pipeline stage (an untyped source or destination):
+ // a boxed native stream unwraps at run time; other values (iterables,
+ // functions) throw the not-supported Error there.
+ if (v.type.kind === "dyn" && what === "pipeline") {
+ return {
+ kind: "libCall",
+ fn: "stream.unboxDyn",
+ args: [
+ v,
+ strLit(
+ "pipeline stages other than native streams (iterables, generators, plain functions, and { readable, writable } pairs) are not supported yet",
+ locOf(node),
+ ),
+ ],
+ type: { kind: "object", className: "%Duplex" },
+ loc: locOf(node),
+ };
+ }
const info = v.type.kind === "object" ? lowerer.classes.get(v.type.className) : undefined;
if (!streamSidesOf(lowerer, info)) {
// A provably-non-stream value in a JS source (the invalid-input
@@ -3296,6 +3347,124 @@ function pipeEndOption(lowerer: Lowerer, options: ts.Expression | undefined, loc
return end;
}
+/** `readable.pipe(process.stdout)`: the process stream joins the pipe as
+ * an fd-backed Writable (stream.stdioWritable), and — like Node, which
+ * never ends process.stdout or process.stderr from a pipe — the source's
+ * end does not end it. pipe() answers the process stream itself. */
+function pipeIntoStdio(
+ lowerer: Lowerer,
+ call: ts.CallExpression,
+ access: ts.PropertyAccessExpression,
+ target: IrExpr,
+): IrExpr {
+ const loc = locOf(call);
+ // JS order: the receiver, then the destination, then the options.
+ const receiverValue = lowerer.lowerExpr(access.expression);
+ const source = lowerer.declareHiddenLocal("%pipeSource", receiverValue.type);
+ const stdio = lowerer.declareHiddenLocal("%pipeStdio", target.type);
+ const options = call.arguments.nodes[1];
+ const stmts: IrStmt[] = [
+ { kind: "varDecl", localId: source.id, init: receiverValue, loc },
+ { kind: "varDecl", localId: stdio.id, init: target, loc },
+ ];
+ // The options literal still evaluates; its end flag has no effect here.
+ if (options !== undefined) {
+ stmts.push({ kind: "exprStmt", expr: pipeEndOption(lowerer, options, loc), loc });
+ }
+ const receiver: IrExpr = { kind: "varRef", localId: source.id, type: receiverValue.type, loc };
+ const writable: IrType = { kind: "object", className: "%Writable" };
+ stmts.push({
+ kind: "exprStmt",
+ expr: {
+ kind: "libCall",
+ fn: "readable.pipe",
+ args: [
+ receiver,
+ {
+ kind: "libCall",
+ fn: "stream.stdioWritable",
+ args: [{ kind: "varRef", localId: stdio.id, type: target.type, loc }],
+ type: writable,
+ loc,
+ },
+ boolLit(false, loc),
+ ],
+ type: writable,
+ loc,
+ },
+ loc,
+ });
+ return {
+ kind: "seqExpr",
+ stmts,
+ result: { kind: "varRef", localId: stdio.id, type: target.type, loc },
+ type: target.type,
+ loc,
+ };
+}
+
+/** The pipe() libCall for a destination of type `dst`. Under the general
+ * stream representation a Writable slot can hold the converted
+ * process.stdout / process.stderr, which a pipe never ends (as in Node), so
+ * the pipe takes readable.pipeGeneral, which checks for them. */
+function generalPipe(lowerer: Lowerer, dst: IrType): "readable.pipe" | "readable.pipeGeneral" {
+ return lowerer.generalNodeStreams && dst.kind === "object" && dst.className === "%Writable"
+ ? "readable.pipeGeneral"
+ : "readable.pipe";
+}
+
+/** `readable.pipe(untyped)`: a checked-dynamic destination unwraps a boxed
+ * native stream at run time (stream.unboxDyn; other values throw the
+ * not-supported Error), and pipe() answers the destination value itself. */
+function pipeIntoDyn(
+ lowerer: Lowerer,
+ call: ts.CallExpression,
+ access: ts.PropertyAccessExpression,
+ target: IrExpr,
+): IrExpr {
+ const loc = locOf(call);
+ const receiverValue = lowerer.lowerExpr(access.expression);
+ const source = lowerer.declareHiddenLocal("%pipeSource", receiverValue.type);
+ const destination = lowerer.declareHiddenLocal("%pipeDestination", target.type);
+ const stmts: IrStmt[] = [
+ { kind: "varDecl", localId: source.id, init: receiverValue, loc },
+ { kind: "varDecl", localId: destination.id, init: target, loc },
+ ];
+ const end = pipeEndOption(lowerer, call.arguments.nodes[1], loc);
+ const duplex: IrType = { kind: "object", className: "%Duplex" };
+ stmts.push({
+ kind: "exprStmt",
+ expr: {
+ kind: "libCall",
+ fn: lowerer.generalNodeStreams ? "readable.pipeGeneral" : "readable.pipe",
+ args: [
+ { kind: "varRef", localId: source.id, type: receiverValue.type, loc },
+ {
+ kind: "libCall",
+ fn: "stream.unboxDyn",
+ args: [
+ { kind: "varRef", localId: destination.id, type: target.type, loc },
+ strLit("pipe destinations other than native streams are not supported yet", loc),
+ ],
+ type: duplex,
+ loc,
+ },
+ end,
+ ],
+ type: duplex,
+ loc,
+ },
+ loc,
+ });
+ return {
+ kind: "seqExpr",
+ stmts,
+ result: { kind: "varRef", localId: destination.id, type: target.type, loc },
+ type: target.type,
+ loc,
+ };
+}
+
export function lowerStreamMethodCall(
lowerer: Lowerer,
call: ts.CallExpression,
@@ -3545,22 +3714,26 @@ export function lowerStreamMethodCall(
loc,
};
}
- const dst = lowerer.lowerExpr(args[0]!);
+ const target = lowerer.lowerExpr(args[0]!);
+ if (target.type.kind === "procStream") return pipeIntoStdio(lowerer, call, access, target);
+ if (target.type.kind === "dyn") return pipeIntoDyn(lowerer, call, access, target);
+ const dst = target;
const dstInfo =
dst.type.kind === "object" ? lowerer.classes.get(dst.type.className) : undefined;
const dstSides = streamSidesOf(lowerer, dstInfo);
if (dstSides !== "w" && dstSides !== "rw") {
+ requestGeneralStreamSurface(lowerer, args[0]!);
lowerer.noLowering(
`pipe into a '${lowerer.fmt(dst.type)}'`,
args[0]!,
- "the destination must be a stream Writable/Duplex (process streams and sockets are not pipe destinations yet)",
+ "the destination must be a stream Writable/Duplex, process.stdout or process.stderr (sockets are not pipe destinations yet)",
);
}
const end = pipeEndOption(lowerer, args[1], loc);
const receiver = lowerer.lowerExpr(access.expression);
return {
kind: "libCall",
- fn: "readable.pipe",
+ fn: generalPipe(lowerer, dst.type),
args: [receiver, dst, end],
type: dst.type,
loc,
diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts
index 1c2dea445..ecd511495 100644
--- a/packages/compiler/src/frontend/lowering/lowerer.ts
+++ b/packages/compiler/src/frontend/lowering/lowerer.ts
@@ -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 | 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();
+ 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();
+ /** Function expressions lowered against a fixed target signature with a
+ * tuple-typed `arguments` (typed-arguments.ts). */
+ readonly typedArgumentsLambdas = new Set();
+ /** Const bindings holding a forwarded fixed tuple (`const args =
+ * arguments as any` in a typed-arguments function) → the tuple type. */
+ readonly tupleAliasSymbols = new Map();
+ /** `??` expressions whose result is the optional callback's own function
+ * type (an adapted function default); their bindings take that type. */
+ readonly nullishFuncResults = new Set();
+ /** The static names of the current generic instance's name-keyed
+ * function parameters (static-function-names.ts). */
+ staticFnNames: Map | null = null;
/** Nesting depth of erased generic bodies being lowered (erased lambdas
* and erased instances). */
erasingDepth = 0;
@@ -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`. */
+ * it before the checker's `AsyncIterable`. A stream option callback's
+ * completion parameter declared with an any-typed `data` is pinned here
+ * too while the callback lowers (pinAnyDataCompletion). */
readonly stageSourceTypes = new Map();
/** Set when a match result lowers to execGroups/matchGroups, the inputs
* of regex span folding. */
@@ -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();
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 `; 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
diff --git a/packages/compiler/src/frontend/lowering/static-function-names.ts b/packages/compiler/src/frontend/lowering/static-function-names.ts
new file mode 100644
index 000000000..d86e55a80
--- /dev/null
+++ b/packages/compiler/src/frontend/lowering/static-function-names.ts
@@ -0,0 +1,219 @@
+import * as ts from "../ts7/adapter.js";
+import { isJsSourceFile } from "../program.js";
+import type { Lowerer } from "./lowerer.js";
+import type { GenericFnInfo } from "./generic-functions.js";
+import { stripTypeWrappers } from "./typed-arguments.js";
+
+/** `fn.name` inside generic higher-order functions.
+ *
+ * Native closures carry no name, but a generic wrapper is instantiated per
+ * call site anyway: when its body reads `.name` of a function parameter,
+ * the instantiation key also carries the static name of that call's
+ * argument (the function declaration's name, a const binding's inferred
+ * name, the name of the function a wrapper call returns), and the read
+ * folds to that string. A call whose argument's name is only known at run
+ * time keeps a precise refusal at the read. */
+
+/** Parameter index → symbol for the parameters whose `.name` the generic
+ * body reads (and never reassigns). Computed once per generic. */
+export function nameReadParams(lowerer: Lowerer, info: GenericFnInfo): Map {
+ if (info.nameReadParams) return info.nameReadParams;
+ const out = new Map();
+ // JavaScript generics read function names through the checked-value
+ // box, which carries the runtime name already.
+ if (isJsSourceFile(info.decl.getSourceFile())) {
+ info.nameReadParams = out;
+ return out;
+ }
+ const bySymbol = new Map();
+ info.decl.parameters.nodes.forEach((param, i) => {
+ if (!ts.isIdentifier(param.name) || param.dotDotDotToken) return;
+ if (!functionTypedParameter(lowerer, param)) return;
+ const symbol = lowerer.checker.getSymbolAtLocation(param.name);
+ if (symbol) bySymbol.set(symbol, i);
+ });
+ if (bySymbol.size > 0) {
+ const written = new Set();
+ const read = new Set();
+ ts.walkPreorder(info.decl, (node) => {
+ if (ts.isTypeNode(node)) return "skip";
+ if (
+ ts.isPropertyAccessExpression(node) &&
+ node.name.text === "name" &&
+ ts.isIdentifier(node.expression)
+ ) {
+ const symbol = lowerer.checker.getSymbolAtLocation(node.expression);
+ if (symbol && bySymbol.has(symbol)) read.add(symbol);
+ }
+ if (
+ ts.isBinaryExpression(node) &&
+ node.operatorToken.kind >= ts.SyntaxKind.FirstAssignment &&
+ node.operatorToken.kind <= ts.SyntaxKind.LastAssignment &&
+ ts.isIdentifier(node.left)
+ ) {
+ const symbol = lowerer.checker.getSymbolAtLocation(node.left);
+ if (symbol) written.add(symbol);
+ }
+ return undefined;
+ });
+ for (const symbol of read) {
+ if (!written.has(symbol)) out.set(bySymbol.get(symbol)!, symbol);
+ }
+ }
+ info.nameReadParams = out;
+ return out;
+}
+
+/** A parameter declared with a function type, or with a type parameter
+ * constrained to one (`fn: F` where `F extends (...args: any) => any`). */
+function functionTypedParameter(lowerer: Lowerer, param: ts.ParameterDeclaration): boolean {
+ const t = lowerer.typeOf(param.name);
+ if (lowerer.checker.getCallSignatures(t).length > 0) return true;
+ if (!(t.flags & ts.TypeFlags.TypeParameter)) return false;
+ const symbol = t.getSymbol();
+ const decl = symbol ? symbol.declarations.find(ts.isTypeParameterDeclaration) : undefined;
+ if (!decl?.constraint) return false;
+ const constraint = lowerer.checker.getTypeFromTypeNode(decl.constraint);
+ return lowerer.checker.getCallSignatures(constraint).length > 0;
+}
+
+/** The instantiation key suffix and name bindings of one call of `info`,
+ * or null when the body reads no parameter's name. */
+export function staticNameBindings(
+ lowerer: Lowerer,
+ call: ts.CallExpression,
+ info: GenericFnInfo,
+): { key: string; names: Map } | null {
+ const params = nameReadParams(lowerer, info);
+ if (params.size === 0) return null;
+ const names = new Map();
+ const parts: string[] = [];
+ for (const [index, symbol] of params) {
+ const arg = call.arguments.nodes[index];
+ const name = arg && !ts.isSpreadElement(arg) ? staticFunctionName(lowerer, arg) : null;
+ names.set(symbol, name);
+ parts.push(name === null ? "?" : JSON.stringify(name));
+ }
+ return { key: `@name(${parts.join(",")})`, names };
+}
+
+/** The `name` JavaScript gives the function `expr` evaluates to, when it is
+ * statically known; null otherwise. */
+export function staticFunctionName(lowerer: Lowerer, expr: ts.Expression): string | null {
+ return staticNameOf(lowerer, expr, new Set());
+}
+
+function staticNameOf(lowerer: Lowerer, expr: ts.Expression, seen: Set): string | null {
+ const e = stripTypeWrappers(expr);
+ if (seen.has(e)) return null;
+ seen.add(e);
+ // An anonymous function as a call argument gets no inferred name.
+ if (ts.isFunctionExpression(e) || ts.isArrowFunction(e)) {
+ return !ts.isArrowFunction(e) && e.name ? e.name.text : "";
+ }
+ if (ts.isIdentifier(e)) {
+ const symbol = lowerer.resolveValueSymbol(e);
+ if (!symbol) return null;
+ const bound = lowerer.staticFnNames?.get(symbol);
+ if (bound !== undefined) return bound;
+ const decl = symbol.valueDeclaration;
+ if (decl === undefined) return null;
+ if (ts.isFunctionDeclaration(decl)) return decl.name ? decl.name.text : "default";
+ if (
+ ts.isVariableDeclaration(decl) &&
+ ts.isIdentifier(decl.name) &&
+ decl.initializer !== undefined &&
+ (ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) !== 0
+ ) {
+ const init = stripTypeWrappers(decl.initializer);
+ // `const f = () => …` / `const f = function () {}`: the binding names
+ // an anonymous function definition.
+ if (ts.isArrowFunction(init)) return decl.name.text;
+ if (ts.isFunctionExpression(init)) return init.name ? init.name.text : decl.name.text;
+ return staticNameOf(lowerer, init, seen);
+ }
+ return null;
+ }
+ if (ts.isCallExpression(e) && ts.isIdentifier(e.expression)) {
+ // A call of a generic higher-order function returns the function its
+ // body returns: every `return` names the same anonymous definition or
+ // the same local const binding.
+ const symbol = lowerer.resolveValueSymbol(e.expression);
+ const info = symbol ? lowerer.genericFnsBySymbol.get(symbol) : undefined;
+ if (!info) return null;
+ return returnedFunctionName(lowerer, info.decl, seen);
+ }
+ if (ts.isPropertyAccessExpression(e)) {
+ const symbol = lowerer.checker.getSymbolAtLocation(e.name);
+ const decl = symbol ? symbol.valueDeclaration : undefined;
+ if (decl && ts.isMethodDeclaration(decl) && ts.isIdentifier(decl.name)) return decl.name.text;
+ return null;
+ }
+ return null;
+}
+
+function returnedFunctionName(
+ lowerer: Lowerer,
+ decl: GenericFnInfo["decl"],
+ seen: Set,
+): string | null {
+ const body = decl.body;
+ if (body === undefined) return null;
+ if (!ts.isBlock(body)) return functionValueName(lowerer, body, seen);
+ let name: string | null | undefined;
+ let ok = true;
+ ts.walkPreorder(body, (node) => {
+ if (!ok) return "stop";
+ if (ts.isFunctionLike(node)) return "skip";
+ if (!ts.isReturnStatement(node)) return undefined;
+ const here = node.expression ? functionValueName(lowerer, node.expression, seen) : null;
+ if (here === null || (name !== undefined && name !== here)) ok = false;
+ else name = here;
+ return undefined;
+ });
+ return ok && name !== undefined ? name : null;
+}
+
+/** The name of a returned function value: an anonymous definition returned
+ * directly is nameless; a local const binding names its definition. */
+function functionValueName(
+ lowerer: Lowerer,
+ expr: ts.Expression,
+ seen: Set,
+): string | null {
+ const e = stripTypeWrappers(expr);
+ if (ts.isFunctionExpression(e) || ts.isArrowFunction(e)) {
+ return !ts.isArrowFunction(e) && e.name ? e.name.text : "";
+ }
+ if (ts.isIdentifier(e)) return staticNameOf(lowerer, e, seen);
+ return null;
+}
+
+/** `fn.name` where `fn` is a name-keyed parameter of the generic instance
+ * being lowered: the folded name, or a precise refusal when this call
+ * site's argument has no static name. Null for every other read. */
+export function staticFunctionNameRead(
+ lowerer: Lowerer,
+ expr: ts.PropertyAccessExpression,
+): string | null {
+ const names = lowerer.staticFnNames;
+ if (
+ names === null ||
+ expr.name.text !== "name" ||
+ expr.questionDotToken !== undefined ||
+ !ts.isIdentifier(expr.expression)
+ ) {
+ return null;
+ }
+ const symbol = lowerer.checker.getSymbolAtLocation(expr.expression);
+ if (!symbol || !names.has(symbol)) return null;
+ const name = names.get(symbol);
+ if (name === null || name === undefined) {
+ lowerer.unsupported(
+ "SC1090",
+ expr,
+ `reading '${expr.expression.text}.name' when the call passes a function whose name is only known at run time (generic wrappers fold the name of a function declaration, a const-bound function, or a wrapper call's result)`,
+ );
+ }
+ return name;
+}
diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts
index c98634695..37a1c625d 100644
--- a/packages/compiler/src/frontend/lowering/surfaces.ts
+++ b/packages/compiler/src/frontend/lowering/surfaces.ts
@@ -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).
diff --git a/packages/compiler/src/frontend/lowering/typed-arguments.ts b/packages/compiler/src/frontend/lowering/typed-arguments.ts
new file mode 100644
index 000000000..b52a3e059
--- /dev/null
+++ b/packages/compiler/src/frontend/lowering/typed-arguments.ts
@@ -0,0 +1,516 @@
+import * as ts from "../ts7/adapter.js";
+import { bodyReadsArguments } from "../arguments-usage.js";
+import { isJsSourceFile, locOf } from "../program.js";
+import { restTupleElementTypes, unitOnlyUnion } from "../type-mapper.js";
+import { DYN, type IrExpr, type IrLocal, type IrStmt, type IrType, arrayOf } from "../../ir/ir.js";
+import type { Lowerer } from "./lowerer.js";
+import { captureContextArguments } from "./function-context.js";
+import { isThisParameter, type ParamShape } from "./call-signatures.js";
+import { tryLowerExpression } from "./expressions/try-lower-expression.js";
+import { omittedArgFor } from "./call-arguments.js";
+
+/** Variadic forwarding through a FIXED signature.
+ *
+ * A TypeScript function expression that declares no parameters and reads
+ * `arguments` (`function (this: unknown) { return fn.apply(this, arguments) }
+ * as F`) is lowered against the fixed function type its value is asserted
+ * to: one native slot per position of that type, and `arguments` is the
+ * tuple of those slots. Inside a generic instantiation F is the call
+ * site's concrete signature, so a higher-order wrapper specializes to the
+ * wrapped function's exact ABI. Forwarding (`f.apply(thisArg, args)`,
+ * `f(...args)`) then calls with the tuple's positions directly. */
+
+/** `e`, with the type-only wrappers that never change its value removed. */
+export function stripTypeWrappers(e: ts.Expression): ts.Expression {
+ let cur = e;
+ for (;;) {
+ if (ts.isParenthesizedExpression(cur)) cur = cur.expression;
+ else if (ts.isAsExpression(cur) || ts.isTypeAssertion(cur) || ts.isSatisfiesExpression(cur))
+ cur = cur.expression;
+ else if (ts.isNonNullExpression(cur)) cur = cur.expression;
+ else return cur;
+ }
+}
+
+/** The outermost type assertion around `node` (`node as unknown as F` → F),
+ * looking through parentheses; null when the value is not asserted. */
+function assertedTypeNode(node: ts.Node): ts.TypeNode | null {
+ let cur: ts.Node = node;
+ let found: ts.TypeNode | null = null;
+ for (let parent = cur.parent; parent !== undefined; parent = cur.parent) {
+ if (ts.isParenthesizedExpression(parent)) {
+ cur = parent;
+ continue;
+ }
+ if ((ts.isAsExpression(parent) || ts.isTypeAssertion(parent)) && parent.expression === cur) {
+ found = parent.type;
+ cur = parent;
+ continue;
+ }
+ break;
+ }
+ return found;
+}
+
+/** The fixed function type a value is asserted to, or null. */
+function assertedFuncType(lowerer: Lowerer, node: ts.Node): (IrType & { kind: "func" }) | null {
+ const typeNode = assertedTypeNode(node);
+ if (!typeNode) return null;
+ const mapped = lowerer.mapTypeOf(lowerer.checker.getTypeFromTypeNode(typeNode));
+ return mapped?.kind === "func" ? mapped : null;
+}
+
+function sameFuncType(a: IrType & { kind: "func" }, b: IrType & { kind: "func" }): boolean {
+ return JSON.stringify(a) === JSON.stringify(b);
+}
+
+/** A function expression whose `arguments` can be typed: TypeScript source,
+ * no runtime parameters (a `this` parameter is type-only), an ordinary
+ * (non-generator) function that reads its own `arguments`. */
+export function typedArgumentsCandidate(node: ts.Node): node is ts.FunctionExpression {
+ return (
+ ts.isFunctionExpression(node) &&
+ node.asteriskToken === undefined &&
+ !isJsSourceFile(node.getSourceFile()) &&
+ node.parameters.nodes.every(isThisParameter) &&
+ bodyReadsArguments(node)
+ );
+}
+
+/** The fixed signature a typed-arguments function expression is lowered
+ * against: the type its value is asserted to (`function () {…} as F`), the
+ * one type every use of its const binding asserts it to (`const w =
+ * function () {…}; return w as unknown as F`), or its contextual function
+ * type. Null when none names one fixed-arity signature. */
+export function typedArgumentsTarget(
+ lowerer: Lowerer,
+ node: ts.FunctionExpression,
+): (IrType & { kind: "func" }) | null {
+ const fixed = (t: (IrType & { kind: "func" }) | null) => (t && t.rest !== true ? t : null);
+ const direct = assertedFuncType(lowerer, node);
+ if (direct) return fixed(direct);
+ let top: ts.Node = node;
+ while (top.parent && ts.isParenthesizedExpression(top.parent)) top = top.parent;
+ const decl = top.parent;
+ if (
+ decl &&
+ ts.isVariableDeclaration(decl) &&
+ decl.initializer === top &&
+ decl.type === undefined &&
+ ts.isIdentifier(decl.name) &&
+ (ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) !== 0
+ ) {
+ const symbol = lowerer.checker.getSymbolAtLocation(decl.name);
+ const scope = enclosingScope(decl);
+ if (symbol && scope) {
+ let target: (IrType & { kind: "func" }) | null = null;
+ let ok = true;
+ let uses = 0;
+ ts.walkPreorder(scope, (n) => {
+ if (!ok) return "stop";
+ if (n === decl.name || !ts.isIdentifier(n) || n.text !== decl.name.text) return undefined;
+ if (lowerer.checker.getSymbolAtLocation(n) !== symbol) return undefined;
+ uses++;
+ const asserted = assertedFuncType(lowerer, n);
+ if (!asserted || (target && !sameFuncType(target, asserted))) ok = false;
+ else target = asserted;
+ return undefined;
+ });
+ if (ok && uses > 0) return fixed(target);
+ }
+ }
+ const ctxType = lowerer.checker.getContextualType(node);
+ if (!ctxType) return null;
+ const sigs = lowerer.checker.getCallSignatures(lowerer.checker.getNonNullableType(ctxType));
+ if (sigs.length !== 1) return null;
+ const mapped = lowerer.mapTypeOf(lowerer.checker.getNonNullableType(ctxType));
+ return mapped?.kind === "func" ? fixed(mapped) : null;
+}
+
+function enclosingScope(node: ts.Node): ts.Node | null {
+ for (let cur = node.parent; cur !== undefined; cur = cur.parent) {
+ if (ts.isSourceFile(cur) || (ts.isFunctionLike(cur) && cur.body !== undefined)) return cur;
+ }
+ return null;
+}
+
+/** The tuple type of a fixed parameter list, interned exactly as the
+ * spelled tuple (and `Parameters`) map: positional fields, or the
+ * empty-array form for no parameters. */
+export function tupleTypeOfParams(lowerer: Lowerer, params: readonly IrType[]): IrType {
+ if (params.length === 0) return arrayOf(unitOnlyUnion(lowerer.unions));
+ const fields = params.map((type, i) => ({ name: String(i), type }));
+ fields.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
+ return { kind: "record", shapeId: lowerer.shapes.intern(fields, true) };
+}
+
+/** The ABI shapes of a typed-arguments function: one required slot per
+ * position of its target signature. */
+export function typedArgumentsShapes(target: IrType & { kind: "func" }): ParamShape[] {
+ return target.params.map((type) => ({ type, mode: "required" }));
+}
+
+/** Declares the hidden positional slots of a typed-arguments function and
+ * binds its `arguments` to their tuple. */
+export function declareTypedArguments(
+ lowerer: Lowerer,
+ node: ts.FunctionExpression,
+ shapes: readonly ParamShape[],
+ params: { localId: string; name: string; type: IrType }[],
+ prologue: IrStmt[],
+): void {
+ const loc = locOf(node);
+ const slots = shapes.map((shape, i) => {
+ const slot = lowerer.declareHiddenLocal(`%arguments.${i}`, shape.type);
+ params.push({ localId: slot.id, name: `%arguments.${i}`, type: shape.type });
+ return slot;
+ });
+ const tupleType = tupleTypeOfParams(
+ lowerer,
+ shapes.map((s) => s.type),
+ );
+ const local = lowerer.declareHiddenLocal("%arguments", tupleType);
+ lowerer.ctx.argumentsLocal = local;
+ // Pure forwarding (every read is forwarded, counted, or aliased by a
+ // binding that is itself only forwarded): no tuple exists at all.
+ if (
+ onlyForwarded(lowerer, node, (id) => id.text === "arguments" && argumentsOwner(id) === node)
+ ) {
+ lowerer.ctx.pureTuples.set(local.id, slots);
+ return;
+ }
+ prologue.push({
+ kind: "varDecl",
+ localId: local.id,
+ init: lowerer.tupleOfSlots(slots, tupleType, node, loc),
+ loc,
+ });
+}
+
+/** Whether every read of a tuple binding in `fn` (identified by `isRoot`)
+ * only forwards it: `f.apply(t, x)`, `g(...x)` into a program function,
+ * `x.length`, or a const alias whose own reads only forward. Reads inside
+ * nested functions (arrows included) capture the binding and need the
+ * tuple. Type assertions around a read never matter. */
+export function onlyForwarded(
+ lowerer: Lowerer,
+ fn: ts.Node,
+ isRoot: (id: ts.Identifier) => boolean,
+): boolean {
+ const body = (fn as ts.Node & { body?: ts.Node }).body;
+ if (body === undefined) return false;
+ const roots: ((id: ts.Identifier) => boolean)[] = [isRoot];
+ let ok = true;
+ const visit = (scope: ts.Node, test: (id: ts.Identifier) => boolean): void => {
+ ts.walkPreorder(scope, (n) => {
+ if (!ok) return "stop";
+ if (ts.isTypeNode(n)) return "skip";
+ if (!ts.isIdentifier(n) || !test(n)) return undefined;
+ for (let cur = n.parent; cur !== undefined && cur !== fn; cur = cur.parent) {
+ if (ts.isFunctionLike(cur)) {
+ ok = false;
+ return "stop";
+ }
+ }
+ let top: ts.Node = n;
+ while (
+ top.parent &&
+ (ts.isParenthesizedExpression(top.parent) ||
+ ts.isAsExpression(top.parent) ||
+ ts.isTypeAssertion(top.parent) ||
+ ts.isSatisfiesExpression(top.parent) ||
+ ts.isNonNullExpression(top.parent))
+ )
+ top = top.parent;
+ const p = top.parent;
+ if (!p) {
+ ok = false;
+ return "stop";
+ }
+ if (ts.isPropertyAccessExpression(p) && p.expression === top && p.name.text === "length")
+ return undefined;
+ if (
+ ts.isCallExpression(p) &&
+ p.arguments.nodes.length === 2 &&
+ p.arguments.nodes[1] === top &&
+ ts.isPropertyAccessExpression(p.expression) &&
+ p.expression.name.text === "apply" &&
+ p.expression.questionDotToken === undefined
+ )
+ return undefined;
+ if (ts.isSpreadElement(p) && p.parent && ts.isCallExpression(p.parent)) {
+ const sigs = lowerer.checker.getCallSignatures(
+ lowerer.checker.getNonNullableType(lowerer.typeOf(p.parent.expression)),
+ );
+ const decl = sigs.length === 1 ? sigs[0]!.declaration : undefined;
+ if (decl && !decl.getSourceFile().isDeclarationFile) return undefined;
+ }
+ if (
+ ts.isVariableDeclaration(p) &&
+ p.initializer === top &&
+ ts.isIdentifier(p.name) &&
+ (ts.getCombinedNodeFlags(p) & ts.NodeFlags.Const) !== 0
+ ) {
+ const alias = lowerer.checker.getSymbolAtLocation(p.name);
+ const aliasName = p.name;
+ if (alias) {
+ roots.push((id) => id !== aliasName && lowerer.checker.getSymbolAtLocation(id) === alias);
+ return undefined;
+ }
+ }
+ ok = false;
+ return "stop";
+ });
+ };
+ for (let i = 0; i < roots.length && ok; i++) visit(body, roots[i]!);
+ return ok;
+}
+
+/** The native slots of a pure-forwarded tuple binding read at `node` in
+ * the current function, or null when the tuple is materialized. */
+function pureSlotsOf(lowerer: Lowerer, node: ts.Expression): IrLocal[] | null {
+ const operand = stripTypeWrappers(node);
+ if (!ts.isIdentifier(operand)) return null;
+ const local =
+ operand.text === "arguments" ? lowerer.ctx.argumentsLocal : lowerer.peekLocal(operand);
+ return local ? (lowerer.ctx.pureTuples.get(local.id) ?? null) : null;
+}
+
+/** The positional values `node` forwards when it is a pure-forwarded tuple
+ * (slot reads, in order), or null. */
+export function pureForwardedValues(lowerer: Lowerer, node: ts.Expression): IrExpr[] | null {
+ const slots = pureSlotsOf(lowerer, node);
+ if (!slots) return null;
+ const loc = locOf(node);
+ return slots.map((slot) => ({ kind: "varRef", localId: slot.id, type: slot.type, loc }));
+}
+
+/** Whether `node` reads a tuple-typed `arguments` binding: the nearest
+ * ordinary function (arrows inherit it) is a typed-arguments function.
+ * Looks the binding up without allocating captures. */
+function isTypedArgumentsRead(lowerer: Lowerer, node: ts.Expression): boolean {
+ if (!ts.isIdentifier(node) || node.text !== "arguments") return false;
+ const stack = lowerer.fnStack;
+ let owner = stack.length - 1;
+ while (owner > 0 && stack[owner]!.inheritsArguments) owner--;
+ const own = owner >= 0 ? stack[owner]!.argumentsLocal : undefined;
+ return own !== undefined && own !== null && own.type.kind !== "dyn";
+}
+
+/** The fixed tuple `node` forwards, or null: the typed `arguments`, a
+ * fixed-tuple rest parameter, or a const alias of either — seen through
+ * type assertions, which never change the value. */
+export function forwardedTupleType(lowerer: Lowerer, node: ts.Expression): IrType | null {
+ const operand = stripTypeWrappers(node);
+ if (!ts.isIdentifier(operand)) return null;
+ if (operand.text === "arguments") {
+ if (!isTypedArgumentsRead(lowerer, operand)) return null;
+ return captureContextArguments(lowerer.fnStack)!.type;
+ }
+ const symbol = lowerer.resolveValueSymbol(operand);
+ if (!symbol) return null;
+ const alias = lowerer.tupleAliasSymbols.get(symbol);
+ if (alias) return alias;
+ const decl = symbol.valueDeclaration;
+ if (
+ decl !== undefined &&
+ ts.isParameter(decl) &&
+ decl.dotDotDotToken !== undefined &&
+ ts.isIdentifier(decl.name) &&
+ !isJsSourceFile(decl.getSourceFile()) &&
+ // Checker-only probe first: an ordinary array rest never maps here.
+ restTupleElementTypes(lowerer.typeOf(operand), lowerer.typeCtx) !== null
+ ) {
+ // A tuple with optional positions has no value type of its own; its
+ // forwarded-only binding carries the slots' tuple (declareRestTupleParam).
+ const t = lowerer.mapTypeOf(lowerer.typeOf(operand)) ?? lowerer.peekLocal(operand)?.type;
+ if (t && isTupleValueType(lowerer, t)) return t;
+ }
+ return null;
+}
+
+function isTupleValueType(lowerer: Lowerer, t: IrType): boolean {
+ if (t.kind === "record") return lowerer.shapes.get(t.shapeId)?.tuple === true;
+ // The empty tuple's representation (mapType's `[]` rule): an array over
+ // the unit-only union. Looked up, never interned, so probing a value
+ // leaves the registries untouched.
+ if (t.kind !== "array" || t.elem.kind !== "union") return false;
+ const arms = lowerer.unions.get(t.elem.unionId)?.arms;
+ return (
+ arms !== undefined &&
+ arms.length === 2 &&
+ arms.some((a) => a.kind === "nullT") &&
+ arms.some((a) => a.kind === "undefinedT")
+ );
+}
+
+/** The positional reads of a forwarded tuple value (already saved in a
+ * local), in order. */
+function tupleReads(
+ lowerer: Lowerer,
+ ref: () => IrExpr,
+ tupleType: IrType,
+ loc: IrExpr["loc"],
+): IrExpr[] {
+ if (tupleType.kind !== "record") return [];
+ const shape = lowerer.shapes.get(tupleType.shapeId)!;
+ return [...shape.fields]
+ .sort((a, b) => Number(a.name) - Number(b.name))
+ .map((field) => ({
+ kind: "recordGet" as const,
+ obj: ref(),
+ shapeId: tupleType.shapeId,
+ field: field.name,
+ type: field.type,
+ loc,
+ }));
+}
+
+/** `f.apply(thisArg, args)` where `args` forwards a fixed tuple and `f` is a
+ * fixed-arity function value: a direct call of `f` with the tuple's
+ * positions and `thisArg` as the call-time receiver. Null for every other
+ * form (the existing Function.prototype paths keep them). */
+export function lowerForwardingApply(
+ lowerer: Lowerer,
+ call: ts.CallExpression,
+ access: ts.PropertyAccessExpression,
+): IrExpr | null {
+ if (access.name.text !== "apply" || access.questionDotToken !== undefined) return null;
+ const argNodes = call.arguments.nodes;
+ if (argNodes.length !== 2 || argNodes.some(ts.isSpreadElement)) return null;
+ const tupleType = forwardedTupleType(lowerer, argNodes[1]!);
+ if (!tupleType) return null;
+ const callee = tryLowerExpression(lowerer, access.expression);
+ if (callee?.type.kind !== "func") return null;
+ const fnType = callee.type;
+ const loc = locOf(call);
+ if (fnType.rest === true) {
+ lowerer.unsupported(
+ "SC1090",
+ call,
+ "forwarding a fixed argument tuple through 'apply' to a variadic function value",
+ );
+ }
+ const savedFn = lowerer.declareHiddenLocal("%applyFn", fnType);
+ const savedThis = lowerer.declareHiddenLocal("%applyThis", DYN);
+ const stmts: IrStmt[] = [
+ { kind: "varDecl", localId: savedFn.id, init: callee, loc },
+ {
+ kind: "varDecl",
+ localId: savedThis.id,
+ init: lowerer.lowerExprExpecting(argNodes[0]!, DYN),
+ loc,
+ },
+ ];
+ let reads = pureForwardedValues(lowerer, argNodes[1]!);
+ if (!reads) {
+ const savedArgs = lowerer.declareHiddenLocal("%applyArgs", tupleType);
+ stmts.push({
+ kind: "varDecl",
+ localId: savedArgs.id,
+ init: lowerer.lowerExpr(stripTypeWrappers(argNodes[1]!)),
+ loc,
+ });
+ reads = tupleReads(
+ lowerer,
+ () => ({ kind: "varRef", localId: savedArgs.id, type: tupleType, loc }),
+ tupleType,
+ loc,
+ );
+ }
+ // JS drops surplus positions and fills missing ones with undefined.
+ const args = fnType.params.map((param, i): IrExpr => {
+ const read = reads[i];
+ if (read) return lowerer.coerceInto(call, read, param);
+ const absent = omittedArgFor(lowerer, param, loc);
+ if (!absent) {
+ lowerer.unsupported(
+ "SC1090",
+ call,
+ "forwarding fewer arguments through 'apply' than the callee's required parameters",
+ );
+ }
+ return absent;
+ });
+ const invoked: IrExpr = {
+ kind: "callValue",
+ callee: { kind: "varRef", localId: savedFn.id, type: fnType, loc },
+ receiver: { kind: "varRef", localId: savedThis.id, type: DYN, loc },
+ args,
+ type: fnType.ret,
+ loc,
+ };
+ return { kind: "seqExpr", stmts, result: invoked, type: fnType.ret, loc };
+}
+
+/** `const args = arguments as …` (or an alias of a rest tuple) inside a
+ * typed-arguments function: the binding holds the tuple itself — the
+ * assertion only re-describes the value — and stays a forwarding source. */
+export function lowerTupleAliasDecl(lowerer: Lowerer, decl: ts.VariableDeclaration): IrStmt | null {
+ if (
+ !decl.initializer ||
+ !ts.isIdentifier(decl.name) ||
+ (ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) === 0 ||
+ lowerer.fnStack.length === 0
+ ) {
+ return null;
+ }
+ const tupleType = forwardedTupleType(lowerer, decl.initializer);
+ if (!tupleType) return null;
+ const symbol = lowerer.checker.getSymbolAtLocation(decl.name);
+ if (!symbol) return null;
+ const declared = lowerer.mapTypeOf(lowerer.typeOf(decl.name));
+ if (declared && declared.kind !== "dyn" && JSON.stringify(declared) !== JSON.stringify(tupleType))
+ return null;
+ lowerer.tupleAliasSymbols.set(symbol, tupleType);
+ const slots = pureSlotsOf(lowerer, decl.initializer);
+ if (slots) {
+ // An alias of a pure-forwarded tuple is itself only forwarded
+ // (onlyForwarded followed it): it names the same slots.
+ const local = lowerer.declareLocal(decl.name, decl.name.text, tupleType, false);
+ lowerer.ctx.pureTuples.set(local.id, slots);
+ return { kind: "block", body: [], loc: locOf(decl) };
+ }
+ const init = lowerer.lowerExpr(stripTypeWrappers(decl.initializer));
+ const local = lowerer.declareLocal(decl.name, decl.name.text, tupleType, false);
+ return { kind: "varDecl", localId: local.id, init, loc: locOf(decl) };
+}
+
+/** A spread operand forwarding a fixed tuple (forwardedTupleType),
+ * including one that resolved to no positions at all. */
+export function isForwardedTupleSpread(lowerer: Lowerer, node: ts.Expression): boolean {
+ return forwardedTupleType(lowerer, node) !== null;
+}
+
+/** `arguments` read inside a typed-arguments function: its fixed arity. */
+export function typedArgumentsArity(lowerer: Lowerer, node: ts.Expression): number | null {
+ if (!ts.isIdentifier(node)) return null;
+ const t = forwardedTupleType(lowerer, node);
+ if (!t) return null;
+ if (t.kind !== "record") return 0;
+ const fields = lowerer.shapes.get(t.shapeId)?.fields ?? [];
+ // A position that may be undefined may also be absent at the call: the
+ // native signature always passes it, so the count is not static.
+ if (
+ fields.some(
+ (f) => f.type.kind === "union" && lowerer.armTag(f.type.unionId, { kind: "undefinedT" }) >= 0,
+ )
+ ) {
+ lowerer.unsupported(
+ "SC1090",
+ node.parent ?? node,
+ "'length' of an argument tuple whose positions may be omitted (the native signature always passes every position, so the count is not static)",
+ );
+ }
+ return fields.length;
+}
+
+/** The function whose `arguments` an identifier reads: the nearest
+ * enclosing non-arrow function. */
+export function argumentsOwner(node: ts.Node): ts.Node | null {
+ for (let cur = node.parent; cur !== undefined; cur = cur.parent) {
+ if (ts.isFunctionLike(cur) && !ts.isArrowFunction(cur)) return cur;
+ if (ts.isSourceFile(cur)) return null;
+ }
+ return null;
+}
diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts
index 3c79a1281..87711a659 100644
--- a/packages/compiler/src/frontend/type-mapper.ts
+++ b/packages/compiler/src/frontend/type-mapper.ts
@@ -980,7 +980,7 @@ function isDeclaredInAmbientModule(d: ts.Declaration, name: string): boolean {
/** True when the declaration's nearest enclosing namespace is `name`
* (`declare global { namespace NodeJS { ... } }` — the NodeJS-global
* interfaces @types/node declares outside any ambient module). */
-function isDeclaredInAmbientNamespace(d: ts.Declaration, name: string): boolean {
+export function isDeclaredInAmbientNamespace(d: ts.Declaration, name: string): boolean {
let node: ts.Node | undefined = d.parent;
while (node) {
if (ts.isModuleDeclaration(node) && ts.isIdentifier(node.name)) {
@@ -991,6 +991,31 @@ function isDeclaredInAmbientNamespace(d: ts.Declaration, name: string): boolean
return false;
}
+/** Whether `type` is one of the stream spellings the general stream
+ * representation (TypeMapperCtx.generalNodeStreams) maps differently:
+ * NodeJS.ReadableStream / NodeJS.WritableStream, @types/node's
+ * stream.Readable / stream.Writable, or the fallback declarations'
+ * ChildStdin. */
+export function isGeneralNodeStreamInterface(
+ type: ts.Type,
+ ctx: Pick,
+): boolean {
+ const psym = type.getSymbol() ?? type.getAliasSymbol();
+ if (psym === undefined) return false;
+ return psym.declarations.some((d) => {
+ if (!(ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d))) return false;
+ if (!ctx.isStdlibFile(d.getSourceFile())) return false;
+ if (psym.name === "ReadableStream" || psym.name === "WritableStream")
+ return ts.isInterfaceDeclaration(d) && isDeclaredInAmbientNamespace(d, "NodeJS");
+ if (psym.name === "ChildStdin") return isDeclaredInAmbientModule(d, "child_process");
+ return (
+ (psym.name === "Readable" || psym.name === "Writable") &&
+ isNodeTypesPath(d.getSourceFile().fileName) &&
+ isDeclaredInAmbientModule(d, "stream")
+ );
+ });
+}
+
/** A type declared in the shipped WebAssembly namespace (scriptc.d.ts):
* its modules, instances, memories, tables, globals, error classes and the
* Exports/Imports records. */
@@ -1146,6 +1171,13 @@ export interface TypeMapperCtx {
* type descriptions for project-owned values, but their imported runtime
* bindings are fenced separately by the Lowerer. */
isExternalTypeFile: (sf: ts.SourceFile) => boolean;
+ /** The program's general stream representation (lowerToIr decides it
+ * per program): NodeJS.ReadableStream / NodeJS.WritableStream and
+ * @types/node's stream.Readable / stream.Writable map to the runtime
+ * stream classes instead of the child/process stdio handles, so values
+ * from every stream producer share their slots. Off (the default), the
+ * stdio handles keep those spellings. */
+ generalNodeStreams?: boolean;
/** Successful context-free mappings, keyed by the
* TypeScript server's stable type id. A generated facade can reference the
* same large record/function type thousands of times; remapping its full
@@ -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 & Promise`: a promise
+ // of a WebAssembly value either way.
+ if (
+ type.isIntersectionType() &&
+ ts.constituentTypes(type).every((t) => {
+ if (t.getSymbol()?.name !== "Promise") return false;
+ const arg = ctx.checker.getTypeArguments(t as ts.TypeReference)[0];
+ return arg !== undefined && isWebAssemblyType(arg, ctx);
+ })
+ )
+ return { kind: "promise", inner: DYN };
// Immutable (@scriptc/types) is a deep-readonly view: the value is
// still the T it maps.
const immutableSource = immutableSourceType(type, ctx);
@@ -1805,6 +1849,16 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
contextResolutions++;
return viaIndexed;
}
+ // `Parameters` / `ReturnType` over a bound function-typed
+ // parameter (variadic higher-order wrappers): the checker keeps both
+ // conditionals symbolic inside the body, and their `infer` positions
+ // match a call signature, which matchInferReference cannot decide.
+ const viaSignature =
+ mapBoundSignatureAlias(type, ctx) ?? mapBoundKeyofNeverConditional(type, ctx);
+ if (viaSignature !== null) {
+ contextResolutions++;
+ return viaSignature;
+ }
// A conditional type over a bound parameter (`Infer`, a
// mapped type's member): evaluate it against the bindings.
if (type.isConditionalType()) {
@@ -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` 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();
@@ -5036,6 +5186,167 @@ function mapBoundIndexedAccess(type: ts.Type, ctx: TypeMapperCtx): IrType | null
return mapType(checker.getTypeOfSymbol(prop), ctx);
}
+/** The standard library's `Parameters` / `ReturnType` over a type
+ * parameter the current instantiation binds to a callable checker type:
+ * that type's one call signature. Concrete arguments never reach here (the
+ * checker evaluates those conditionals itself); generic bodies keep them
+ * symbolic because their `infer` sits in a call signature. */
+function boundSignatureAlias(
+ type: ts.Type,
+ ctx: TypeMapperCtx,
+): { name: "Parameters" | "ReturnType"; sig: ts.Signature } | null {
+ const { checker, resolveTypeParamTs } = ctx;
+ if (!resolveTypeParamTs) return null;
+ const alias = type.getAliasSymbol();
+ if (!alias || (alias.name !== "Parameters" && alias.name !== "ReturnType")) return null;
+ const aliasArgs = type.getAliasTypeArguments();
+ const arg = aliasArgs[0];
+ if (aliasArgs.length !== 1 || !arg || !(arg.flags & ts.TypeFlags.TypeParameter)) return null;
+ const isLibAlias = alias.declarations.some(
+ (d) => ts.isTypeAliasDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()),
+ );
+ if (!isLibAlias) return null;
+ const bound = resolveTypeParamTs(arg);
+ if (!bound || bound.flags & ts.TypeFlags.TypeParameter) return null;
+ const sigs = checker.getCallSignatures(bound);
+ const sig = sigs[0];
+ if (sigs.length !== 1 || !sig || sig.getTypeParameters().length > 0) return null;
+ return { name: alias.name, sig };
+}
+
+/** One position of a fixed-arity parameter tuple. An optional position
+ * still has a native slot (the function type's `T | undefined` slot). */
+interface TupleElement {
+ type: ts.Type;
+ optional: boolean;
+}
+
+/** The positional parameters of a signature as a fixed tuple: a trailing
+ * rest flattened when its own type is a fixed tuple. Null for a variadic
+ * position, which has no fixed native arity. */
+function signatureParameters(sig: ts.Signature, ctx: TypeMapperCtx): TupleElement[] | null {
+ const { checker } = ctx;
+ const out: TupleElement[] = [];
+ for (const p of sig.getParameters()) {
+ const decl = p.valueDeclaration;
+ const type = checker.getTypeOfSymbol(p);
+ if (decl !== undefined && ts.isParameter(decl)) {
+ if (decl.dotDotDotToken !== undefined) {
+ const inner = restTupleElements(type, ctx);
+ if (!inner) return null;
+ out.push(...inner);
+ continue;
+ }
+ out.push({
+ type,
+ optional: decl.questionToken !== undefined || decl.initializer !== undefined,
+ });
+ continue;
+ }
+ out.push({ type, optional: (p.flags & ts.SymbolFlags.Optional) !== 0 });
+ }
+ return out;
+}
+
+/** The required-only positional parameter types of a signature (the
+ * value-level `Parameters` tuple), null when a position is optional
+ * or variadic: such a tuple's length varies per call. */
+function signatureParameterTypes(sig: ts.Signature, ctx: TypeMapperCtx): ts.Type[] | null {
+ const elems = signatureParameters(sig, ctx);
+ if (!elems || elems.some((e) => e.optional)) return null;
+ return elems.map((e) => e.type);
+}
+
+/** The element types of a REST parameter's type when it is a fixed tuple:
+ * a tuple of required elements (`[number, string]`, the empty `[]`), a type
+ * parameter bound to one, or `Parameters` over a bound function type.
+ * Such a rest parameter has a compile-time arity, so its native ABI is the
+ * flattened positional list (exactly the equivalent fixed signature —
+ * TypeScript treats `(...a: [A, B]) => R` and `(a: A, b: B) => R` alike). */
+export function restTupleElementTypes(type: ts.Type, ctx: TypeMapperCtx): ts.Type[] | null {
+ return restTupleElements(type, ctx)?.map((e) => e.type) ?? null;
+}
+
+/** restTupleElementTypes with each position's optionality: a rest tuple
+ * may end in optional positions (`[A, B?]`, `Parameters<(a: A, b?: B) =>
+ * R>`), which flatten to `T | undefined` slots exactly as the function
+ * type's optional parameters do. */
+function restTupleElements(type: ts.Type, ctx: TypeMapperCtx): TupleElement[] | null {
+ const { checker } = ctx;
+ if (type.flags & ts.TypeFlags.TypeParameter) {
+ const bound = ctx.resolveTypeParamTs?.(type);
+ return bound && bound !== type ? restTupleElements(bound, ctx) : null;
+ }
+ const viaAlias = boundSignatureAlias(type, ctx);
+ if (viaAlias) {
+ return viaAlias.name === "Parameters" ? signatureParameters(viaAlias.sig, ctx) : null;
+ }
+ if (!checker.isTupleType(type)) return null;
+ const ref = type as ts.TupleTypeReference;
+ const tupleShape =
+ (ref.elementFlags as ts.ElementFlags[] | undefined) !== undefined
+ ? ref
+ : (ref.getTarget() as ts.TupleType | undefined);
+ const args = checker.getTypeArguments(ref);
+ if (args.length === 0) return [];
+ const flags = tupleShape?.elementFlags;
+ if (flags === undefined) return null;
+ if (flags.some((f) => !(f & (ts.ElementFlags.Required | ts.ElementFlags.Optional)))) return null;
+ return args.map((t, i) => ({
+ type: t,
+ optional: (flags[i]! & ts.ElementFlags.Optional) !== 0,
+ }));
+}
+
+/** One tuple element's IR type under the tuple mapping's rules. */
+function mapTupleElement(t: ts.Type, ctx: TypeMapperCtx): IrType | null {
+ let et = (t.flags & ts.TypeFlags.Any) !== 0 ? DYN : mapType(t, ctx);
+ if (et?.kind === "void" && isUnitOnlyTsType(t, ctx.resolveTypeParam))
+ et = unitOnlyUnion(ctx.unions);
+ return !et || et.kind === "void" ? null : et;
+}
+
+/** The mapped element types of a fixed-tuple rest parameter type
+ * (restTupleElementTypes), null when it is not one or an element does not
+ * map. */
+export function restTupleParamTypes(type: ts.Type, ctx: TypeMapperCtx): IrType[] | null {
+ const elems = restTupleElements(type, ctx);
+ if (!elems) return null;
+ const out: IrType[] = [];
+ for (const e of elems) {
+ let et = mapTupleElement(e.type, ctx);
+ // An optional position's slot holds undefined when the caller omits
+ // it, as an optional parameter's does (mapCallSignature).
+ if (et && e.optional && et.kind !== "dyn") et = withUndefinedArm(et, ctx.unions);
+ if (!et) return null;
+ out.push(et);
+ }
+ return out;
+}
+
+/** `Parameters` (the fixed tuple of F's parameters, interned exactly as
+ * the spelled tuple maps) and `ReturnType` (F's mapped result) over a
+ * bound function-typed type parameter. */
+function mapBoundSignatureAlias(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
+ const target = boundSignatureAlias(type, ctx);
+ if (!target) return null;
+ if (target.name === "ReturnType") {
+ const ret = ctx.checker.getReturnTypeOfSignature(target.sig);
+ return ret.flags & ts.TypeFlags.Any ? DYN : mapType(ret, ctx);
+ }
+ const elems = signatureParameterTypes(target.sig, ctx);
+ if (!elems) return null;
+ if (elems.length === 0) return arrayOf(unitOnlyUnion(ctx.unions));
+ const fields: { name: string; type: IrType }[] = [];
+ for (let i = 0; i < elems.length; i++) {
+ const et = mapTupleElement(elems[i]!, ctx);
+ if (!et) return null;
+ fields.push({ name: String(i), type: et });
+ }
+ fields.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
+ return { kind: "record", shapeId: ctx.shapes.intern(fields, true) };
+}
+
/** `Partial` / `Readonly` where T is a STILL-GENERIC type parameter —
* the one mapped-type spelling a monomorphized body can contain that the
* checker cannot resolve for us (`keyof T` is unknown inside the body, so
@@ -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>` (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` 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`, 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 | false | undefined {
+ const { checker } = ctx;
+ const wantSigs = checker.getCallSignatures(extendsType);
+ const want = wantSigs[0];
+ if (wantSigs.length !== 1 || !want || want.getTypeParameters().length > 0) return undefined;
+ const wantShape = signatureShape(want, ctx);
+ if (wantShape === undefined) return undefined;
+ const isAny = (t: ts.Type): boolean => (t.flags & ts.TypeFlags.Any) !== 0;
+ const isAnyArray = (t: ts.Type): boolean =>
+ checker.isArrayType(t) && isAny(checker.getTypeArguments(t as ts.TypeReference)[0]!);
+ if (!wantShape.params.every((p) => isAny(p.type) || isAnyArray(p.type))) return undefined;
+ if (wantShape.rest !== null && !isAny(wantShape.rest)) return undefined;
+ if (!(checked.flags & ts.TypeFlags.Object)) return false;
+ const gotSigs = checker.getCallSignatures(checked);
+ const got = gotSigs[0];
+ if (gotSigs.length !== 1 || !got || got.getTypeParameters().length > 0) return undefined;
+ const gotShape = signatureShape(got, ctx);
+ if (gotShape !== undefined) {
+ // `(a: any) => infer R` rejects `(a: string, b: string) => R` and
+ // `(...args: [string, number]) => R`: they need more arguments.
+ if (wantShape.rest === null && requiredArguments(gotShape) > wantShape.params.length)
+ return false;
+ for (let i = 0; i < wantShape.params.length; i++) {
+ if (!isAnyArray(wantShape.params[i]!.type)) continue;
+ const param = gotShape.params[i]?.type ?? gotShape.rest;
+ if (param === undefined || param === null) continue;
+ const accepts = acceptsArray(param, ctx);
+ if (accepts !== true) return accepts;
+ }
+ } else if (wantShape.params.some((p) => isAnyArray(p.type))) {
+ return undefined;
+ }
+ const wantRet = checker.getReturnTypeOfSignature(want);
+ const infer = inferSymbolOf(wantRet, ctx);
+ if (infer) return new Map([[infer, checker.getReturnTypeOfSignature(got)]]);
+ return wantRet.flags & ts.TypeFlags.Any ? new Map() : undefined;
+}
+
+/** A signature's parameters as a call sees them: the positional ones, with
+ * a fixed-tuple rest (`...args: [A, B?]`) flattened into positions, and the
+ * element type of an array rest (`any` for `...args: any`). Undefined when
+ * a parameter has no declaration, or the rest is a variadic tuple or
+ * another type. */
+function signatureShape(
+ sig: ts.Signature,
+ ctx: TypeMapperCtx,
+): { params: { type: ts.Type; optional: boolean }[]; rest: ts.Type | null } | undefined {
+ const { checker } = ctx;
+ const params: { type: ts.Type; optional: boolean }[] = [];
+ for (const p of sig.getParameters()) {
+ const decl = p.valueDeclaration;
+ if (decl === undefined || !ts.isParameter(decl)) return undefined;
+ const type = checker.getTypeOfSymbol(p);
+ if (decl.dotDotDotToken === undefined) {
+ params.push({
+ type,
+ optional: decl.questionToken !== undefined || decl.initializer !== undefined,
+ });
+ continue;
+ }
+ if (type.flags & ts.TypeFlags.Any) return { params, rest: type };
+ if (checker.isArrayType(type))
+ return { params, rest: checker.getTypeArguments(type as ts.TypeReference)[0]! };
+ const elems = restTupleElements(type, ctx);
+ if (!elems) return undefined;
+ for (const e of elems) params.push({ type: e.type, optional: e.optional });
+ return { params, rest: null };
+ }
+ return { params, rest: null };
+}
+
+/** The fewest arguments a call must pass: up to the last required position. */
+function requiredArguments(shape: { params: { optional: boolean }[] }): number {
+ let required = 0;
+ shape.params.forEach((p, i) => {
+ if (!p.optional) required = i + 1;
+ });
+ return required;
+}
+
+/** Whether a parameter of this type accepts an `any[]` argument: true for
+ * `any`, `unknown` and array types, false for primitives, undefined
+ * otherwise. */
+function acceptsArray(t: ts.Type, ctx: TypeMapperCtx): boolean | undefined {
+ if (t.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) return true;
+ if (ctx.checker.isArrayType(t)) return true;
+ const primitive =
+ ts.TypeFlags.StringLike |
+ ts.TypeFlags.NumberLike |
+ ts.TypeFlags.BooleanLike |
+ ts.TypeFlags.BigIntLike |
+ ts.TypeFlags.ESSymbolLike |
+ ts.TypeFlags.Undefined |
+ ts.TypeFlags.Null |
+ ts.TypeFlags.Void;
+ return t.flags & primitive ? false : undefined;
+}
+
+/** `checked extends PromiseLike` (or `Promise`) over the
+ * standard library's Promise: U binds to the settled type. An object with
+ * no `then` member is no thenable. Everything else stays undecided. */
+function matchInferThenable(
+ checked: ts.Type,
+ extendsType: ts.Type,
+ ctx: TypeMapperCtx,
+): Map | false | undefined {
+ const { checker } = ctx;
+ const stdlibNamed = (t: ts.Type, names: readonly string[]): boolean => {
+ const symbol = t.getSymbol();
+ return (
+ symbol !== undefined &&
+ names.includes(symbol.name) &&
+ symbol.declarations.some((d) => ctx.isStdlibFile(d.getSourceFile()))
+ );
+ };
+ if (!stdlibNamed(extendsType, ["PromiseLike", "Promise"])) return undefined;
+ const wantArgs = checker.getTypeArguments(extendsType as ts.TypeReference);
+ const infer = wantArgs.length === 1 ? inferSymbolOf(wantArgs[0]!, ctx) : undefined;
+ if (!infer) return undefined;
+ if (stdlibNamed(checked, ["Promise", "PromiseLike"])) {
+ const gotArgs = checker.getTypeArguments(checked as ts.TypeReference);
+ return gotArgs.length === 1 ? new Map([[infer, gotArgs[0]!]]) : undefined;
+ }
+ return checker.getPropertyOfType(checked, "then") === undefined ? false : undefined;
+}
+
/** The type mentions a mapped key or bound alias parameter (a template
* shape evalMappedTemplate does not reduce). */
function mentionsMappedParams(
@@ -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` 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 { TZ?: string }` and every spread of it, or a declared
+ * environment `{ CI?: string } & Dictionary`. At runtime such a
+ * member is just another key, so the shape interns as the pure
+ * index-signature record. Its slot admits undefined, as the optional
+ * members do (`env.CI = undefined` keeps the key). A hybrid layout would
+ * give each member a struct slot, which enumerates before the overflow
+ * keys (Node keeps one insertion order) and makes `delete` and spread
+ * answer differently for declared and undeclared keys. Required members,
+ * and members typed otherwise than the slot (whose reads the slot type
+ * could not give back), keep the hybrid shape. */
+function coveredOptionalFieldsShape(
+ widened: ts.Type,
+ parts: RecordShapeParts,
+ ctx: TypeMapperCtx,
+): string | null {
+ const slot = parts.indexValue;
+ if (!slot || parts.fields.length === 0) return null;
+ const { checker, shapes, unions } = ctx;
+ // The members' common type must be the slot plus undefined. Read it off
+ // the members (already interned) rather than building it: a hybrid that
+ // does not qualify must not intern anything new.
+ const optionalSlot = parts.fields[0]!.type;
+ if (!parts.fields.every((field) => typeEquals(field.type, optionalSlot))) return null;
+ if (optionalSlot.kind !== "union" || unions.armTag(optionalSlot.unionId, UNDEFINED_T) < 0)
+ return null;
+ const memberArms = unions.get(optionalSlot.unionId)?.arms ?? [];
+ const slotArms =
+ slot.kind === "union" ? (unions.get(slot.unionId)?.arms ?? []) : isUnitType(slot) ? [] : [slot];
+ const definedArms = memberArms.filter((arm): boolean => arm.kind !== "undefinedT");
+ const slotDefined = slotArms.filter((arm): boolean => arm.kind !== "undefinedT");
+ if (
+ definedArms.length === 0 ||
+ definedArms.length !== slotDefined.length ||
+ !slotDefined.every((arm) => definedArms.some((other) => typeEquals(arm, other)))
+ )
+ return null;
+ if (!isSupportedIndexValue(optionalSlot)) return null;
+ const props = checker.getPropertiesOfType(widened);
+ if (props.length !== parts.fields.length) return null;
+ if (!props.every((p) => (p.flags & ts.SymbolFlags.Optional) !== 0)) return null;
+ return shapes.intern([], false, optionalSlot, []);
+}
+
/** The type of an object literal that spreads an index-signature record
* (`{ ...tags, bucket: b, status }`, the metric-tags idiom). The checker
* drops the signature and types the literal by its explicit members only,
diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts
index b6ed2d443..7b1ef7109 100644
--- a/packages/compiler/src/index.ts
+++ b/packages/compiler/src/index.ts
@@ -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 } : {}),
});
diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts
index 0cbafcb3f..142b1e25c 100644
--- a/packages/compiler/src/ir/builtin-effects.ts
+++ b/packages/compiler/src/ir/builtin-effects.ts
@@ -75,6 +75,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([
"process.stdoutWriteBytes",
"process.stderrWriteBytes",
"procStream.write",
+ "child.stdioStream",
+ "stream.unboxDyn",
"weakMap.new",
"weakSet.new",
"dyn.fromEntries",
@@ -133,6 +135,20 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([
"blob.new",
"blob.fileNew",
"blob.slice",
+ "wasm.compile",
+ "wasm.validate",
+ "wasm.instantiate",
+ "wasm.moduleNew",
+ "wasm.instanceNew",
+ "wasm.errorNew",
+ "wasm.memoryNew",
+ "wasm.tableNew",
+ "wasm.globalNew",
+ "wasm.withoutNew",
+ "wasm.moduleImports",
+ "wasm.moduleExports",
+ "wasm.moduleCustomSections",
+ "wasm.valueOf",
"events.addAbortListener",
"fetch.streamNew",
"fetch.streamFrom",
@@ -332,6 +348,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = 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 = 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 = 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 = 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",
diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts
index 8468785b9..99ca04ae4 100644
--- a/packages/compiler/src/ir/builtin-signatures.ts
+++ b/packages/compiler/src/ir/builtin-signatures.ts
@@ -108,6 +108,45 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
"blob.bytes": { argTypes: [DYN], result: { kind: "promise", inner: BYTES_U8 } },
"blob.arrayBuffer": { argTypes: [DYN], result: { kind: "promise", inner: DYN } },
"blob.is": { argTypes: [DYN, F64], result: BOOL },
+ /** WebAssembly (scr_wasm.c): modules translated at build time
+ * (wasm/translate.ts), found at run time by the SHA-256 of their bytes.
+ * compile and instantiate over bytes settle on a later loop turn;
+ * instantiate over a Module settles now, as in Node. `wasm.is` answers
+ * instanceof for Module, Instance, Memory, Table and Global (kinds 0-4)
+ * and CompileError, LinkError and RuntimeError (5-7); `wasm.errorNew`
+ * constructs one of the error classes (kind 5-7, message, options). */
+ "wasm.compile": { argTypes: [DYN], result: { kind: "promise", inner: DYN } },
+ "wasm.validate": { argTypes: [DYN], result: BOOL },
+ "wasm.instantiate": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } },
+ "wasm.moduleNew": { argTypes: [DYN], result: DYN },
+ "wasm.instanceNew": { argTypes: [DYN, DYN], result: DYN },
+ "wasm.is": { argTypes: [DYN, F64], result: BOOL },
+ "wasm.errorNew": { argTypes: [F64, DYN, DYN], result: DYN },
+ /** new WebAssembly.Memory(descriptor), Table(descriptor, value) and
+ * Global(descriptor, value) (an undefined value is the type's default);
+ * `wasm.withoutNew` throws the TypeError for Module, Instance, Memory,
+ * Table or Global (kinds 0-4) called without new, after its arguments
+ * evaluate. */
+ "wasm.memoryNew": { argTypes: [DYN], result: DYN },
+ "wasm.tableNew": { argTypes: [DYN, DYN], result: DYN },
+ "wasm.globalNew": { argTypes: [DYN, DYN], result: DYN },
+ "wasm.withoutNew": { argTypes: [F64, DYN, DYN], result: DYN },
+ /** WebAssembly.Module.imports(module), exports(module) and
+ * customSections(module, name): checked-dynamic arrays of descriptor
+ * objects and ArrayBuffers. */
+ "wasm.moduleImports": { argTypes: [DYN], result: DYN },
+ "wasm.moduleExports": { argTypes: [DYN], result: DYN },
+ "wasm.moduleCustomSections": { argTypes: [DYN, DYN], result: DYN },
+ /** `v.valueOf()` on a WebAssembly-typed receiver: a Global's value,
+ * dyn.valueOf's answer for anything else. */
+ "wasm.valueOf": { argTypes: [DYN], result: DYN },
+ /** WebAssembly.compile (0), instantiate (1) and validate (2) as function
+ * values, and the namespace object holding them. */
+ "wasm.function": { argTypes: [F64], result: DYN },
+ "wasm.namespace": { argTypes: [], result: DYN },
+ /** The global object viewed through a cast to a type with a WebAssembly
+ * member: an object holding the namespace. */
+ "wasm.globalView": { argTypes: [], result: DYN },
"abort.timeout": { argTypes: [DYN], result: DYN },
"abort.now": { argTypes: [DYN], result: DYN },
"abort.any": { argTypes: [DYN], result: DYN },
@@ -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 },
diff --git a/packages/compiler/src/ir/function-references.test.ts b/packages/compiler/src/ir/function-references.test.ts
index 23bcb86ef..4ecc60a6f 100644
--- a/packages/compiler/src/ir/function-references.test.ts
+++ b/packages/compiler/src/ir/function-references.test.ts
@@ -212,6 +212,8 @@ const MODULE_FIELDS: Record>, Role> = {
"contracts.acyclic[]": "other",
"contracts.immortalGlobals[]": "other",
"contracts.programLifetime[]": "other",
+ "wasmModules[].sha256": "other",
+ "wasmModules[].bytes": "other",
"threadSlotKeys[]": "other",
};
diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts
index 6d63007fb..4c5bbd183 100644
--- a/packages/compiler/src/ir/ir.ts
+++ b/packages/compiler/src/ir/ir.ts
@@ -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. */
diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts
index caa39e66c..b52afafcb 100644
--- a/packages/compiler/src/ir/runtime-features.ts
+++ b/packages/compiler/src/ir/runtime-features.ts
@@ -149,6 +149,7 @@ const ABORT_SIGNAL_LIB_FNS: ReadonlySet = new Set([
/** Entry points that link the stream unit (with STREAM_LIB_PREFIXES). */
const STREAM_LIB_FNS: ReadonlySet = new Set([
+ "child.stdioStream",
"console.bind",
"crypto.hashStream",
"crypto.hashStreamPiped",
@@ -167,6 +168,8 @@ const STREAM_LIB_FNS: ReadonlySet = new Set([
"stream.destroyErr",
"stream.errored",
"stream.prop",
+ "stream.stdioWritable",
+ "stream.unboxDyn",
"stream.workerStdio",
]);
@@ -238,8 +241,21 @@ const CHILD_PROCESS_LIB_FNS: ReadonlySet = new Set([
/** libCall name prefixes that link the child-process unit. */
const CHILD_PROCESS_LIB_PREFIXES: readonly string[] = ["child.", "cp.", "spawnRes.", "writer."];
+/** The entry points scr_dyn_builtin_class.c defines. The unit calls
+ * listeners through the dyn-invoke unit, renders unhandled 'error'
+ * payloads through the inspect unit, creates EventEmitter's private
+ * symbols through the symbol unit and shares defaultMaxListeners with the
+ * emitter unit, so each of them links all five. */
+const BUILTIN_CLASS_LIB_FNS: readonly IrLibFn[] = [
+ "dyn.builtinClass",
+ "dyn.builtinInstanceOf",
+ "dyn.functionInstanceOf",
+];
+
/** Entry points that link the symbol unit, besides the sym. and disposable. families. */
const SYMBOL_LIB_FNS: ReadonlySet = new Set([
+ // EventEmitter's kShapeMode and kCapture properties
+ ...BUILTIN_CLASS_LIB_FNS,
"dyn.usingDispose",
"dyn.usingMethod",
"util.isDeepStrictEqual",
@@ -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,
};
diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts
index 84827e787..835dfa8ba 100644
--- a/packages/compiler/src/ir/validate.ts
+++ b/packages/compiler/src/ir/validate.ts
@@ -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;
}
diff --git a/packages/compiler/src/library/ir-policy.ts b/packages/compiler/src/library/ir-policy.ts
index 2d1bc85d4..0c343fa98 100644
--- a/packages/compiler/src/library/ir-policy.ts
+++ b/packages/compiler/src/library/ir-policy.ts
@@ -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 };
diff --git a/packages/compiler/src/native/cli.ts b/packages/compiler/src/native/cli.ts
index 1585b6c9e..3fa934da1 100644
--- a/packages/compiler/src/native/cli.ts
+++ b/packages/compiler/src/native/cli.ts
@@ -89,6 +89,7 @@ async function main(): Promise {
bigint: false,
searchParams: false,
qs: false,
+ dynBuiltinClass: false,
parseArgs: false,
stream: false,
net: false,
@@ -102,6 +103,7 @@ async function main(): Promise {
tls: false,
cipher: false,
webcrypto: false,
+ perfLoop: false,
tlsCa: false,
},
options.optimization ?? "release",
diff --git a/packages/compiler/src/native/prepare.ts b/packages/compiler/src/native/prepare.ts
index e4216f511..a2a15179f 100644
--- a/packages/compiler/src/native/prepare.ts
+++ b/packages/compiler/src/native/prepare.ts
@@ -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");
}
diff --git a/packages/compiler/src/native/sanitizer.test.ts b/packages/compiler/src/native/sanitizer.test.ts
index 33be5b30e..ea9690ce7 100644
--- a/packages/compiler/src/native/sanitizer.test.ts
+++ b/packages/compiler/src/native/sanitizer.test.ts
@@ -75,6 +75,7 @@ test.skipIf(process.platform !== "linux" && process.platform !== "darwin")(
workers: false,
youngNursery: false,
eventTarget: false,
+ wasm: false,
dynTier: false,
warningReplace: false,
stdioExtended: false,
@@ -95,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,
diff --git a/packages/compiler/src/wasm/base64.ts b/packages/compiler/src/wasm/base64.ts
new file mode 100644
index 000000000..122bcd4ef
--- /dev/null
+++ b/packages/compiler/src/wasm/base64.ts
@@ -0,0 +1,61 @@
+/* Base64 for WebAssembly module bytes: decoding inline modules found in
+ * source text (discover.ts) and carrying translated modules on the IR
+ * (IrModule.wasmModules). Plain TypeScript, so the native compiler
+ * compiles it too. */
+
+const BASE64_VALUES = (() => {
+ const table = new Int16Array(128).fill(-1);
+ const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+ for (let i = 0; i < alphabet.length; i++) table[alphabet.charCodeAt(i)] = i;
+ // base64url spellings
+ table["-".charCodeAt(0)] = 62;
+ table["_".charCodeAt(0)] = 63;
+ return table;
+})();
+
+/** Decodes standard or URL-safe base64 (padding optional); null when the
+ * text is not base64. */
+export function decodeBase64(text: string): Uint8Array | null {
+ let end = text.length;
+ while (end > 0 && text.charCodeAt(end - 1) === 61) end--;
+ if (text.length - end > 2 || end % 4 === 1) return null;
+ const out = new Uint8Array(Math.floor((end * 3) / 4));
+ let acc = 0;
+ let bits = 0;
+ let j = 0;
+ for (let i = 0; i < end; i++) {
+ const c = text.charCodeAt(i);
+ const v = c < 128 ? BASE64_VALUES[c]! : -1;
+ if (v < 0) return null;
+ acc = ((acc << 6) | v) & 0xffffff;
+ bits += 6;
+ if (bits >= 8) {
+ bits -= 8;
+ out[j++] = (acc >> bits) & 0xff;
+ }
+ }
+ return j === out.length ? out : out.subarray(0, j);
+}
+
+const BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+
+/** Standard base64 with padding. */
+export function encodeBase64(bytes: Uint8Array): string {
+ const parts: string[] = [];
+ let chunk = "";
+ for (let i = 0; i < bytes.length; i += 3) {
+ const a = bytes[i]!;
+ const b = i + 1 < bytes.length ? bytes[i + 1]! : 0;
+ const c = i + 2 < bytes.length ? bytes[i + 2]! : 0;
+ const n = (a << 16) | (b << 8) | c;
+ chunk += BASE64_ALPHABET[(n >> 18) & 63]! + BASE64_ALPHABET[(n >> 12) & 63]!;
+ chunk += i + 1 < bytes.length ? BASE64_ALPHABET[(n >> 6) & 63]! : "=";
+ chunk += i + 2 < bytes.length ? BASE64_ALPHABET[n & 63]! : "=";
+ if (chunk.length >= 65536) {
+ parts.push(chunk);
+ chunk = "";
+ }
+ }
+ parts.push(chunk);
+ return parts.join("");
+}
diff --git a/packages/compiler/src/wasm/binary.ts b/packages/compiler/src/wasm/binary.ts
index 5ddc28cc9..eaedf25af 100644
--- a/packages/compiler/src/wasm/binary.ts
+++ b/packages/compiler/src/wasm/binary.ts
@@ -20,6 +20,7 @@
* The decoder never evaluates anything and keeps no module contents beyond
* the counts and the feature set it reports. It is plain TypeScript that
* the native compiler also compiles. */
+import { readMemargFlags } from "./module.js";
/** Proposals the summary can name, in report order. The first group is the
* translatable set; the rest are reported as unsupported. */
@@ -1244,19 +1245,9 @@ class FunctionValidator {
/** A memarg. A memory other than 0 stops typing (multi-memory). */
memarg(naturalAlign: number): void {
const r = this.r;
- let align = r.u32();
- let multi = false;
- if (align >= 64 && align < 128) {
- align -= 64;
- multi = true;
- } else if (align >= 64) {
- r.malformed("malformed memop flags");
- }
- let memory = 0;
- if (multi) {
- memory = r.u32();
- if (memory !== 0) this.otherMemory(memory);
- }
+ const { align, memory } = readMemargFlags(() => r.u32());
+ if (align < 0) r.malformed("malformed memop flags");
+ if (memory !== 0) this.otherMemory(memory);
if (this.m.memory64) r.u64();
else r.u32();
if (!this.typed) return;
@@ -1840,9 +1831,9 @@ class FunctionValidator {
scanMemarg(): void {
const r = this.r;
- const align = r.u32();
- if (align >= 128) r.malformed("malformed memop flags");
- if (align >= 64 && r.u32() !== 0) this.m.feature("multi-memory");
+ const { align, memory } = readMemargFlags(() => r.u32());
+ if (align < 0) r.malformed("malformed memop flags");
+ if (memory !== 0) this.m.feature("multi-memory");
if (this.m.memory64) r.u64();
else r.u32();
}
diff --git a/packages/compiler/src/wasm/discover.ts b/packages/compiler/src/wasm/discover.ts
index 95fc28647..931bc1dac 100644
--- a/packages/compiler/src/wasm/discover.ts
+++ b/packages/compiler/src/wasm/discover.ts
@@ -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 = 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 {
diff --git a/packages/compiler/src/wasm/module.ts b/packages/compiler/src/wasm/module.ts
new file mode 100644
index 000000000..587a20d81
--- /dev/null
+++ b/packages/compiler/src/wasm/module.ts
@@ -0,0 +1,676 @@
+/* The structure of a WebAssembly module, for the translator.
+ *
+ * binary.ts validates a module and keeps nothing but counts. This parser
+ * reads the parts translation needs (types, imports, functions, tables,
+ * memories, globals, exports, the start function, element and data
+ * segments, function bodies and custom sections) from bytes that binary.ts
+ * already accepted, so it checks nothing beyond what it must to stay in
+ * bounds. Constant expressions and function bodies are kept as byte ranges
+ * that the translator reads itself.
+ *
+ * i64 values never become JavaScript numbers: the translator reads their
+ * LEB128 encodings into two 32-bit halves. Plain TypeScript, so the native
+ * compiler compiles it too. */
+
+export const VAL_I32 = 0x7f;
+export const VAL_I64 = 0x7e;
+export const VAL_F32 = 0x7d;
+export const VAL_F64 = 0x7c;
+export const VAL_FUNCREF = 0x70;
+export const VAL_EXTERNREF = 0x6f;
+
+export const KIND_FUNC = 0;
+export const KIND_TABLE = 1;
+export const KIND_MEMORY = 2;
+export const KIND_GLOBAL = 3;
+
+export interface WasmFuncType {
+ params: number[];
+ results: number[];
+}
+
+/** A byte range of the module: a constant expression (through its `end`
+ * opcode) or a payload. */
+export interface WasmRange {
+ start: number;
+ end: number;
+}
+
+export interface WasmImport {
+ module: string;
+ name: string;
+ kind: number;
+ /** Functions: the type index. Tables and globals: the value type. */
+ type: number;
+ min: number;
+ max: number | null;
+ mutable: boolean;
+}
+
+export interface WasmTable {
+ elemType: number;
+ min: number;
+ max: number | null;
+ imported: boolean;
+ /** A defined table's initializer (reference-types table with an init
+ * expression), or null for the null reference. */
+ init: WasmRange | null;
+}
+
+export interface WasmMemory {
+ min: number;
+ max: number | null;
+ imported: boolean;
+}
+
+export interface WasmGlobal {
+ type: number;
+ mutable: boolean;
+ imported: boolean;
+ init: WasmRange | null;
+}
+
+export interface WasmExport {
+ name: string;
+ kind: number;
+ index: number;
+}
+
+export interface WasmElem {
+ mode: "active" | "passive" | "declarative";
+ table: number;
+ offset: WasmRange | null;
+ elemType: number;
+ /** One initializer per element: a function index (>= 0), or -1 when the
+ * element is a constant expression in `exprs`. */
+ funcs: number[];
+ exprs: (WasmRange | null)[];
+}
+
+export interface WasmData {
+ mode: "active" | "passive";
+ memory: number;
+ offset: WasmRange | null;
+ payload: WasmRange;
+}
+
+export interface WasmCode {
+ /** Declared locals, expanded (not including parameters). */
+ locals: number[];
+ body: WasmRange;
+}
+
+export interface WasmCustom {
+ name: string;
+ payload: WasmRange;
+}
+
+export interface WasmModule {
+ bytes: Uint8Array;
+ types: WasmFuncType[];
+ imports: WasmImport[];
+ /** The type index of every function, imported functions first. */
+ funcTypes: number[];
+ importedFuncs: number;
+ tables: WasmTable[];
+ memories: WasmMemory[];
+ globals: WasmGlobal[];
+ exports: WasmExport[];
+ start: number | null;
+ elems: WasmElem[];
+ datas: WasmData[];
+ codes: WasmCode[];
+ customs: WasmCustom[];
+ /** Functions referenced outside a body (exports, element segments,
+ * ref.func in constant expressions): only these need a function
+ * reference record. Bodies add their own ref.func targets, which a valid
+ * module also declares in one of these places. */
+ declaredRefs: Set;
+}
+
+/** A byte cursor over a module that binary.ts validated. */
+export class WasmCursor {
+ bytes: Uint8Array;
+ pos: number;
+ constructor(bytes: Uint8Array, pos: number) {
+ this.bytes = bytes;
+ this.pos = pos;
+ }
+
+ u8(): number {
+ const b = this.bytes[this.pos];
+ if (b === undefined) throw new Error("wasm: unexpected end of module");
+ this.pos++;
+ return b;
+ }
+
+ u32(): number {
+ let result = 0;
+ let scale = 1;
+ for (;;) {
+ const b = this.u8();
+ result += (b & 0x7f) * scale;
+ if ((b & 0x80) === 0) return result;
+ scale *= 128;
+ }
+ }
+
+ /** A signed LEB128 of at most 33 bits. */
+ s33(): number {
+ let result = 0;
+ let scale = 1;
+ for (;;) {
+ const b = this.u8();
+ result += (b & 0x7f) * scale;
+ scale *= 128;
+ if ((b & 0x80) === 0) {
+ if (b & 0x40) result -= scale;
+ return result;
+ }
+ }
+ }
+
+ /** A signed 32-bit LEB128 as a signed 32-bit number. */
+ s32(): number {
+ return this.s33() | 0;
+ }
+
+ /** A signed 64-bit LEB128 as its two's-complement halves [lo, hi], each
+ * an unsigned 32-bit number. */
+ s64(): [number, number] {
+ let lo = 0;
+ let hi = 0;
+ let shift = 0;
+ let b = 0;
+ do {
+ b = this.u8();
+ const bits = b & 0x7f;
+ if (shift < 32) {
+ lo = (lo | (bits << shift)) >>> 0;
+ if (shift > 25) hi = (hi | (bits >>> (32 - shift))) >>> 0;
+ } else if (shift < 64) {
+ hi = (hi | (bits << (shift - 32))) >>> 0;
+ }
+ shift += 7;
+ } while (b & 0x80);
+ if (shift < 64 && b & 0x40) {
+ // Sign-extend from bit `shift`.
+ if (shift < 32) {
+ lo = (lo | (~0 << shift)) >>> 0;
+ hi = 0xffffffff;
+ } else {
+ hi = (hi | (~0 << (shift - 32))) >>> 0;
+ }
+ }
+ return [lo, hi];
+ }
+
+ u32le(): number {
+ const b = this.bytes;
+ const p = this.pos;
+ this.pos += 4;
+ return (b[p]! | (b[p + 1]! << 8) | (b[p + 2]! << 16) | (b[p + 3]! << 24)) >>> 0;
+ }
+
+ name(): string {
+ const length = this.u32();
+ const start = this.pos;
+ this.pos += length;
+ return decodeUtf8(this.bytes, start, this.pos);
+ }
+
+ /** Skips a constant expression through its `end` and answers its range. */
+ constExpr(): WasmRange {
+ const start = this.pos;
+ for (;;) {
+ const op = this.u8();
+ if (op === 0x0b) break;
+ skipImmediates(this, op);
+ }
+ return { start, end: this.pos };
+ }
+
+ valType(): number {
+ return this.u8();
+ }
+
+ /** A block type: -1 for the empty type, a value type, or a type index. */
+ blockType():
+ | { kind: "empty" }
+ | { kind: "value"; type: number }
+ | { kind: "index"; index: number } {
+ const b = this.bytes[this.pos]!;
+ if (b === 0x40) {
+ this.pos++;
+ return { kind: "empty" };
+ }
+ if (
+ b === VAL_I32 ||
+ b === VAL_I64 ||
+ b === VAL_F32 ||
+ b === VAL_F64 ||
+ b === VAL_FUNCREF ||
+ b === VAL_EXTERNREF
+ ) {
+ this.pos++;
+ return { kind: "value", type: b };
+ }
+ return { kind: "index", index: this.s33() };
+ }
+}
+
+function decodeUtf8(bytes: Uint8Array, start: number, end: number): string {
+ let out = "";
+ let i = start;
+ while (i < end) {
+ const b = bytes[i]!;
+ let cp = b;
+ let need = 0;
+ if (b >= 0xf0) {
+ cp = b & 0x07;
+ need = 3;
+ } else if (b >= 0xe0) {
+ cp = b & 0x0f;
+ need = 2;
+ } else if (b >= 0xc0) {
+ cp = b & 0x1f;
+ need = 1;
+ }
+ for (let k = 1; k <= need; k++) cp = cp * 64 + (bytes[i + k]! & 0x3f);
+ out += String.fromCodePoint(cp);
+ i += need + 1;
+ }
+ return out;
+}
+
+/** The flags of a memarg (the immediates of loads, stores and atomics):
+ * the alignment exponent and the memory index. Flags with bit 6 set
+ * (multi-memory) are followed by an explicit memory index, which may be
+ * 0; flags of 128 or more are malformed and answer align -1 without
+ * reading further. The offset follows. Validation (binary.ts), translation
+ * and skipImmediates all read memargs through this function, so they agree
+ * on where each instruction ends; `u32` reads the next u32 immediate of
+ * their own reader. */
+export function readMemargFlags(u32: () => number): { align: number; memory: number } {
+ const flags = u32();
+ if (flags >= 128) return { align: -1, memory: 0 };
+ if (flags >= 64) return { align: flags - 64, memory: u32() };
+ return { align: flags, memory: 0 };
+}
+
+/** Reads the immediates of opcode `op` (already consumed). Covers the
+ * translatable feature level; anything else was refused before
+ * translation. */
+export function skipImmediates(c: WasmCursor, op: number): void {
+ switch (op) {
+ case 0x02:
+ case 0x03:
+ case 0x04:
+ c.blockType();
+ return;
+ case 0x0c:
+ case 0x0d:
+ case 0x10:
+ case 0x20:
+ case 0x21:
+ case 0x22:
+ case 0x23:
+ case 0x24:
+ case 0x25:
+ case 0x26:
+ case 0xd2:
+ c.u32();
+ return;
+ case 0x0e: {
+ const n = c.u32();
+ for (let i = 0; i <= n; i++) c.u32();
+ return;
+ }
+ case 0x11:
+ c.u32();
+ c.u32();
+ return;
+ case 0x1c: {
+ const n = c.u32();
+ for (let i = 0; i < n; i++) c.valType();
+ return;
+ }
+ case 0x3f:
+ case 0x40:
+ // The memory index (a LEB128 zero; V8 accepts long encodings).
+ c.u32();
+ return;
+ case 0x41:
+ c.s32();
+ return;
+ case 0x42:
+ c.s64();
+ return;
+ case 0x43:
+ c.pos += 4;
+ return;
+ case 0x44:
+ c.pos += 8;
+ return;
+ case 0xd0:
+ c.valType();
+ return;
+ case 0xfc: {
+ const sub = c.u32();
+ switch (sub) {
+ case 8:
+ c.u32();
+ c.u32();
+ return;
+ case 9:
+ case 13:
+ case 15:
+ case 16:
+ case 17:
+ c.u32();
+ return;
+ case 10:
+ c.u32();
+ c.u32();
+ return;
+ case 11:
+ c.u32();
+ return;
+ case 12:
+ case 14:
+ c.u32();
+ c.u32();
+ return;
+ default:
+ return;
+ }
+ }
+ default:
+ if (op >= 0x28 && op <= 0x3e) {
+ const flags = readMemargFlags(() => c.u32());
+ if (flags.align < 0 || flags.memory !== 0)
+ throw new Error(`wasm: memarg of memory ${flags.memory} is not supported`);
+ c.u32();
+ }
+ return;
+ }
+}
+
+function limits(c: WasmCursor): { min: number; max: number | null } {
+ const flags = c.u8();
+ const min = c.u32();
+ const max = flags & 0x01 ? c.u32() : null;
+ return { min, max };
+}
+
+/** Parses a module binary.ts reported as valid within the translatable
+ * feature level. */
+export function parseWasmModule(bytes: Uint8Array): WasmModule {
+ const m: WasmModule = {
+ bytes,
+ types: [],
+ imports: [],
+ funcTypes: [],
+ importedFuncs: 0,
+ tables: [],
+ memories: [],
+ globals: [],
+ exports: [],
+ start: null,
+ elems: [],
+ datas: [],
+ codes: [],
+ customs: [],
+ declaredRefs: new Set(),
+ };
+ const c = new WasmCursor(bytes, 8);
+ while (c.pos < bytes.length) {
+ const id = c.u8();
+ const size = c.u32();
+ const end = c.pos + size;
+ switch (id) {
+ case 0: {
+ const name = c.name();
+ m.customs.push({ name, payload: { start: c.pos, end } });
+ break;
+ }
+ case 1: {
+ const n = c.u32();
+ for (let i = 0; i < n; i++) {
+ c.u8(); // 0x60
+ const params: number[] = [];
+ const np = c.u32();
+ for (let k = 0; k < np; k++) params.push(c.valType());
+ const results: number[] = [];
+ const nr = c.u32();
+ for (let k = 0; k < nr; k++) results.push(c.valType());
+ m.types.push({ params, results });
+ }
+ break;
+ }
+ case 2: {
+ const n = c.u32();
+ for (let i = 0; i < n; i++) {
+ const module = c.name();
+ const name = c.name();
+ const kind = c.u8();
+ const imp: WasmImport = {
+ module,
+ name,
+ kind,
+ type: 0,
+ min: 0,
+ max: null,
+ mutable: false,
+ };
+ if (kind === KIND_FUNC) {
+ imp.type = c.u32();
+ m.funcTypes.push(imp.type);
+ m.importedFuncs++;
+ } else if (kind === KIND_TABLE) {
+ imp.type = c.valType();
+ const l = limits(c);
+ imp.min = l.min;
+ imp.max = l.max;
+ m.tables.push({
+ elemType: imp.type,
+ min: l.min,
+ max: l.max,
+ imported: true,
+ init: null,
+ });
+ } else if (kind === KIND_MEMORY) {
+ const l = limits(c);
+ imp.min = l.min;
+ imp.max = l.max;
+ m.memories.push({ min: l.min, max: l.max, imported: true });
+ } else {
+ imp.type = c.valType();
+ imp.mutable = c.u8() === 1;
+ m.globals.push({ type: imp.type, mutable: imp.mutable, imported: true, init: null });
+ }
+ m.imports.push(imp);
+ }
+ break;
+ }
+ case 3: {
+ const n = c.u32();
+ for (let i = 0; i < n; i++) m.funcTypes.push(c.u32());
+ break;
+ }
+ case 4: {
+ const n = c.u32();
+ for (let i = 0; i < n; i++) {
+ let init: WasmRange | null = null;
+ if (c.bytes[c.pos] === 0x40) {
+ // table with an initializer: 0x40 0x00 tabletype expr
+ c.pos += 2;
+ const elemType = c.valType();
+ const l = limits(c);
+ init = c.constExpr();
+ m.tables.push({ elemType, min: l.min, max: l.max, imported: false, init });
+ continue;
+ }
+ const elemType = c.valType();
+ const l = limits(c);
+ m.tables.push({ elemType, min: l.min, max: l.max, imported: false, init });
+ }
+ break;
+ }
+ case 5: {
+ const n = c.u32();
+ for (let i = 0; i < n; i++) {
+ const l = limits(c);
+ m.memories.push({ min: l.min, max: l.max, imported: false });
+ }
+ break;
+ }
+ case 6: {
+ const n = c.u32();
+ for (let i = 0; i < n; i++) {
+ const type = c.valType();
+ const mutable = c.u8() === 1;
+ const init = c.constExpr();
+ noteConstRefs(m, init);
+ m.globals.push({ type, mutable, imported: false, init });
+ }
+ break;
+ }
+ case 7: {
+ const n = c.u32();
+ for (let i = 0; i < n; i++) {
+ const name = c.name();
+ const kind = c.u8();
+ const index = c.u32();
+ m.exports.push({ name, kind, index });
+ if (kind === KIND_FUNC) m.declaredRefs.add(index);
+ }
+ break;
+ }
+ case 8:
+ m.start = c.u32();
+ break;
+ case 9: {
+ const n = c.u32();
+ for (let i = 0; i < n; i++) m.elems.push(elemSegment(m, c));
+ break;
+ }
+ case 10: {
+ const n = c.u32();
+ for (let i = 0; i < n; i++) {
+ const size = c.u32();
+ const bodyEnd = c.pos + size;
+ const locals: number[] = [];
+ const groups = c.u32();
+ for (let g = 0; g < groups; g++) {
+ const count = c.u32();
+ const type = c.valType();
+ for (let k = 0; k < count; k++) locals.push(type);
+ }
+ m.codes.push({ locals, body: { start: c.pos, end: bodyEnd } });
+ c.pos = bodyEnd;
+ }
+ break;
+ }
+ case 11: {
+ const n = c.u32();
+ for (let i = 0; i < n; i++) {
+ const flags = c.u32();
+ if (flags === 1) {
+ const size = c.u32();
+ m.datas.push({
+ mode: "passive",
+ memory: 0,
+ offset: null,
+ payload: { start: c.pos, end: c.pos + size },
+ });
+ c.pos += size;
+ continue;
+ }
+ const memory = flags === 2 ? c.u32() : 0;
+ const offset = c.constExpr();
+ const size = c.u32();
+ m.datas.push({
+ mode: "active",
+ memory,
+ offset,
+ payload: { start: c.pos, end: c.pos + size },
+ });
+ c.pos += size;
+ }
+ break;
+ }
+ default:
+ // 12 (data count) and anything binary.ts accepted without content
+ // translation needs.
+ break;
+ }
+ c.pos = end;
+ }
+ return m;
+}
+
+function elemSegment(m: WasmModule, c: WasmCursor): WasmElem {
+ const flags = c.u32();
+ const passiveOrDeclarative = (flags & 1) !== 0;
+ const explicitTable = (flags & 2) !== 0;
+ const usesExprs = (flags & 4) !== 0;
+ let mode: WasmElem["mode"] = "active";
+ if (passiveOrDeclarative) mode = explicitTable ? "declarative" : "passive";
+ let table = 0;
+ let offset: WasmRange | null = null;
+ if (!passiveOrDeclarative) {
+ if (explicitTable) table = c.u32();
+ offset = c.constExpr();
+ }
+ let elemType = VAL_FUNCREF;
+ if (passiveOrDeclarative || explicitTable) {
+ // elemkind (0x00 = funcref) or a reference type
+ const b = c.u8();
+ elemType = usesExprs ? b : VAL_FUNCREF;
+ }
+ const n = c.u32();
+ const funcs: number[] = [];
+ const exprs: (WasmRange | null)[] = [];
+ for (let i = 0; i < n; i++) {
+ if (usesExprs) {
+ const expr = c.constExpr();
+ const direct = refFuncOf(m.bytes, expr);
+ if (direct !== null) {
+ funcs.push(direct);
+ exprs.push(null);
+ m.declaredRefs.add(direct);
+ } else {
+ noteConstRefs(m, expr);
+ funcs.push(-1);
+ exprs.push(expr);
+ }
+ } else {
+ const f = c.u32();
+ funcs.push(f);
+ exprs.push(null);
+ m.declaredRefs.add(f);
+ }
+ }
+ return { mode, table, offset, elemType, funcs, exprs };
+}
+
+/** The function of a `ref.func f; end` expression, or null. */
+function refFuncOf(bytes: Uint8Array, expr: WasmRange): number | null {
+ if (bytes[expr.start] !== 0xd2) return null;
+ const c = new WasmCursor(bytes, expr.start + 1);
+ const f = c.u32();
+ return c.pos === expr.end - 1 && bytes[c.pos] === 0x0b ? f : null;
+}
+
+function noteConstRefs(m: WasmModule, expr: WasmRange): void {
+ const c = new WasmCursor(m.bytes, expr.start);
+ while (c.pos < expr.end) {
+ const op = c.u8();
+ if (op === 0xd2) {
+ m.declaredRefs.add(c.u32());
+ continue;
+ }
+ skipImmediates(c, op);
+ }
+}
diff --git a/packages/compiler/src/wasm/translate.test.ts b/packages/compiler/src/wasm/translate.test.ts
new file mode 100644
index 000000000..86ba042ef
--- /dev/null
+++ b/packages/compiler/src/wasm/translate.test.ts
@@ -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 )) with the memarg's bytes as given. */
+function loadModule(memarg: number[]): Uint8Array {
+ const body = [0, 0x20, 0, 0x28, ...memarg, 0x0b];
+ return wasmModule(
+ [1, 1, 0x60, 1, 0x7f, 1, 0x7f],
+ [3, 1, 0],
+ [5, 1, 0, 1],
+ [7, 1, 1, 0x66, 0, 0],
+ [10, 1, body.length, ...body],
+ );
+}
+
+describe("memargs and operand types", () => {
+ test("a memarg naming memory 0 explicitly translates as the plain form", () => {
+ // Flags 0x42: alignment 2 with bit 6 set, then memory index 0 and
+ // offset 4. Validation and translation read the same three immediates.
+ // Only the descriptor (which records the module's byte length) differs.
+ const code = (memarg: number[]): string[] =>
+ translateWasmModule(loadModule(memarg), SHA, { boundsChecks: true })
+ .text.join("\n")
+ .split("\n")
+ .filter((line) => !line.startsWith(`@${wasmSymbolPrefix(SHA)}_desc =`));
+ expect(code([0x42, 0, 4])).toEqual(code([0x02, 4]));
+ });
+
+ test("a memarg naming another memory is refused", () => {
+ expect(() =>
+ translateWasmModule(loadModule([0x42, 1, 4]), SHA, { boundsChecks: true }),
+ ).toThrow(/memory 1/);
+ });
+
+ test("local.tee of an i32 into an externref local is refused", () => {
+ // (func (local externref) i32.const 1 local.tee 0 drop): the operand
+ // never reaches a reference slot.
+ const body = [1, 1, 0x6f, 0x41, 1, 0x22, 0, 0x1a, 0x0b];
+ const bytes = wasmModule([1, 1, 0x60, 0, 0], [3, 1, 0], [10, 1, body.length, ...body]);
+ expect(() => translateWasmModule(bytes, SHA, { boundsChecks: false })).toThrow(
+ /local\.tee operand type does not match/,
+ );
+ });
+});
+
+describe("module parsing", () => {
+ test("reads the parts translation needs", () => {
+ const m = parseWasmModule(ADD);
+ expect(m.types).toEqual([{ params: [0x7f, 0x7f], results: [0x7f] }]);
+ expect(m.exports.map((e) => e.name)).toEqual(["add", "mem"]);
+ expect(m.memories).toEqual([{ min: 1, max: null, imported: false }]);
+ expect(m.datas[0]!.mode).toBe("active");
+ expect([...m.declaredRefs]).toEqual([0]);
+ });
+
+ test("decodes signed 64-bit LEB128 into two's-complement halves", () => {
+ const halves = (bytes: number[]): [number, number] =>
+ new WasmCursor(new Uint8Array(bytes), 0).s64();
+ expect(halves([0x7f])).toEqual([0xffffffff, 0xffffffff]);
+ expect(halves([0x80, 0x80, 0x80, 0x80, 0x10])).toEqual([0, 1]);
+ expect(halves([0xff, 0xff, 0xff, 0xff, 0x0f])).toEqual([0xffffffff, 0]);
+ expect(halves([0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x7f])).toEqual([
+ 0, 0x80000000,
+ ]);
+ expect(halves([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00])).toEqual([
+ 0xffffffff, 0x7fffffff,
+ ]);
+ });
+});
+
+describe("stack checks", () => {
+ test("only functions through which the stack can grow without bound check it", () => {
+ // f0 <-> f1 recurse mutually, f2 recurses on itself, f3 calls f0 once.
+ const calls = new Uint8Array([
+ 0, 97, 115, 109, 1, 0, 0, 0, 1, 4, 1, 96, 0, 0, 3, 5, 4, 0, 0, 0, 0, 10, 21, 4, 4, 0, 16, 1,
+ 11, 4, 0, 16, 0, 11, 4, 0, 16, 2, 11, 4, 0, 16, 0, 11,
+ ]);
+ expect([...stackCheckedFunctions(parseWasmModule(calls))].sort()).toEqual([0, 1, 2]);
+ expect(stackCheckedFunctions(parseWasmModule(ADD)).size).toBe(0);
+ // Only code without the fault handler checks explicitly.
+ const checked = translateWasmModule(calls, SHA, { boundsChecks: true }).text.join("\n");
+ expect(checked.split("@scr_wasm_stack_limit").length - 1).toBe(3);
+ const guarded = translateWasmModule(calls, SHA, { boundsChecks: false }).text.join("\n");
+ expect(guarded).not.toContain("@scr_wasm_stack_limit");
+ });
+});
+
+describe("helpers", () => {
+ test("equal signatures share their identity across modules", () => {
+ expect(signatureHash("ii:i")).toBe(signatureHash("ii:i"));
+ expect(signatureHash("ii:i")).not.toBe(signatureHash("i:i"));
+ expect(signatureHash(":")).toMatch(/^u0x[0-9a-f]{16}$/);
+ });
+
+ test("base64 round-trips module bytes", () => {
+ expect(encodeBase64(ADD)).toBe(Buffer.from(ADD).toString("base64"));
+ expect(decodeBase64(encodeBase64(ADD))).toEqual(ADD);
+ expect(encodeBase64(new Uint8Array([1]))).toBe("AQ==");
+ expect(encodeBase64(new Uint8Array([1, 2]))).toBe("AQI=");
+ });
+});
diff --git a/packages/compiler/src/wasm/translate.ts b/packages/compiler/src/wasm/translate.ts
new file mode 100644
index 000000000..4c02499ca
--- /dev/null
+++ b/packages/compiler/src/wasm/translate.ts
@@ -0,0 +1,2296 @@
+/* WebAssembly to LLVM IR.
+ *
+ * Translates one validated module (binary.ts accepted it within the
+ * translatable feature level) to LLVM assembly that the program's emitter
+ * appends to its own module, so the native helper optimizes, partitions and
+ * caches it with the program. The runtime (scr_wasm.c) implements the
+ * JavaScript API over the descriptor this emits; scr_wasm_abi.h is the
+ * contract both sides follow.
+ *
+ * Shape of the output:
+ * - one internal function per wasm function, taking the instance as its
+ * first parameter (`ptr %inst`) and the wasm values directly (i32, i64,
+ * float, double; references are pointers). Several results return as a
+ * literal struct. An imported function is a stub that packs its
+ * arguments and calls scr_wasm_import_call.
+ * - one thunk per exported function, `void (ptr inst, ptr args, ptr
+ * results)` over 64-bit slots, which the runtime calls under its trap
+ * guard;
+ * - an init function (globals, table initializers, active element and
+ * data segments); the descriptor names the start function;
+ * - constant data (segments, names, signatures) and the descriptor.
+ *
+ * The instance layout (scr_wasm_abi.h): host at 0, memory base at 8,
+ * memory object at 16, function reference records at 24, then one pointer
+ * per table and one cell pointer per global.
+ *
+ * Memory accesses compute `base + zext(address) + offset` in 64 bits. With
+ * guard pages (64-bit hosts) the runtime reserves enough address space that
+ * every such address past the memory faults, so accesses are unchecked and
+ * the base is loaded once per function (the memory never moves). With
+ * explicit bounds checks (sanitizer builds and hosts without the fault
+ * handler) the base and byte length live in function-local slots that are
+ * reloaded after every call and memory.grow.
+ *
+ * Locals, block results and loop parameters are allocas; LLVM promotes them
+ * to SSA values. Traps call scr_wasm_trap, which never returns. Plain
+ * TypeScript without bigint, so the native compiler compiles it too. */
+import {
+ KIND_FUNC,
+ KIND_GLOBAL,
+ KIND_MEMORY,
+ KIND_TABLE,
+ VAL_EXTERNREF,
+ VAL_F32,
+ VAL_F64,
+ VAL_FUNCREF,
+ VAL_I32,
+ VAL_I64,
+ WasmCursor,
+ parseWasmModule,
+ readMemargFlags,
+ skipImmediates,
+ type WasmFuncType,
+ type WasmModule,
+ type WasmRange,
+} from "./module.js";
+
+/** Trap codes (scr_wasm_abi.h SCR_WASM_TRAP_*). */
+export const TRAP_UNREACHABLE = 1;
+export const TRAP_MEMORY_OOB = 2;
+export const TRAP_DIV_BY_ZERO = 3;
+export const TRAP_DIV_UNREPRESENTABLE = 4;
+export const TRAP_REM_BY_ZERO = 5;
+export const TRAP_FLOAT_UNREPRESENTABLE = 6;
+export const TRAP_TABLE_OOB = 7;
+export const TRAP_SIGNATURE = 8;
+export const TRAP_NULL = 9;
+/** scr_wasm_abi.h SCR_WASM_TRAP_EXHAUSTION. */
+export const TRAP_EXHAUSTION = 100;
+
+/** The descriptor ABI version both sides agree on. */
+export const WASM_ABI_VERSION = 2;
+
+/** Instance slot offsets (bytes). */
+const OFF_MEMBASE = 8;
+const OFF_MEMORY = 16;
+const OFF_FUNCREFS = 24;
+const OFF_TABLES = 32;
+/** sizeof(ScrWasmFuncRef). */
+const FUNCREF_SIZE = 24;
+
+export interface WasmTranslateOptions {
+ /** Explicit bounds checks instead of guard pages. */
+ boundsChecks: boolean;
+ /** Keep the custom sections, for WebAssembly.Module.customSections. */
+ customSections?: boolean;
+}
+
+export interface WasmTranslation {
+ /** External declarations, one line each. */
+ decls: string[];
+ /** Definitions in the emitter's top-level grammar. */
+ text: string[];
+ /** The descriptor's symbol (internal). */
+ descriptor: string;
+}
+
+/** A construct the translator does not support in an otherwise valid module. */
+export class WasmTranslateError extends Error {}
+
+/** Attributes of translated functions. Recursion must exhaust the stack
+ * as it does in V8, so LLVM may not turn self-recursion into loops (tail
+ * recursion elimination, including its accumulator form, which would make
+ * `f(x) + 1` recursion loop forever) or calls into jumps. */
+const FUNC_ATTRS = `nounwind "disable-tail-calls"="true"`;
+
+const RUNTIME_DECLS = [
+ "declare void @scr_wasm_trap(i32) noreturn nounwind cold",
+ "declare void @scr_wasm_import_call(ptr, i32, ptr, ptr)",
+ "declare i32 @scr_wasm_memory_grow(ptr, i32)",
+ "declare void @scr_wasm_memory_fill(ptr, i32, i32, i32)",
+ "declare void @scr_wasm_memory_copy(ptr, i32, i32, i32)",
+ "declare void @scr_wasm_memory_init(ptr, i32, i32, i32, i32)",
+ "declare void @scr_wasm_data_drop(ptr, i32)",
+ "declare ptr @scr_wasm_table_get(ptr, i32, i32)",
+ "declare void @scr_wasm_table_set(ptr, i32, i32, ptr)",
+ "declare i32 @scr_wasm_table_grow(ptr, i32, ptr, i32)",
+ "declare void @scr_wasm_table_fill(ptr, i32, i32, ptr, i32)",
+ "declare void @scr_wasm_table_copy(ptr, i32, i32, i32, i32, i32)",
+ "declare void @scr_wasm_table_init(ptr, i32, i32, i32, i32, i32)",
+ "declare void @scr_wasm_elem_drop(ptr, i32)",
+ "declare void @scr_wasm_global_set_ref(ptr, ptr)",
+];
+
+function llType(t: number): string {
+ switch (t) {
+ case VAL_I32:
+ return "i32";
+ case VAL_I64:
+ return "i64";
+ case VAL_F32:
+ return "float";
+ case VAL_F64:
+ return "double";
+ case VAL_FUNCREF:
+ case VAL_EXTERNREF:
+ return "ptr";
+ default:
+ throw new WasmTranslateError(`value type 0x${t.toString(16)} is not supported`);
+ }
+}
+
+function zeroOf(t: number): string {
+ switch (t) {
+ case VAL_I32:
+ case VAL_I64:
+ return "0";
+ case VAL_F32:
+ case VAL_F64:
+ return "0.0";
+ default:
+ return "null";
+ }
+}
+
+/** The runtime's signature letter of a value type. */
+function sigChar(t: number): string {
+ switch (t) {
+ case VAL_I32:
+ return "i";
+ case VAL_I64:
+ return "I";
+ case VAL_F32:
+ return "f";
+ case VAL_F64:
+ return "F";
+ case VAL_FUNCREF:
+ return "r";
+ case VAL_EXTERNREF:
+ return "x";
+ default:
+ throw new WasmTranslateError(`value type 0x${t.toString(16)} is not supported`);
+ }
+}
+
+function sigString(ft: WasmFuncType): string {
+ let s = "";
+ for (const p of ft.params) s += sigChar(p);
+ s += ":";
+ for (const r of ft.results) s += sigChar(r);
+ return s;
+}
+
+function retType(results: readonly number[]): string {
+ if (results.length === 0) return "void";
+ if (results.length === 1) return llType(results[0]!);
+ return `{ ${results.map(llType).join(", ")} }`;
+}
+
+function hex8(n: number): string {
+ return (n >>> 0).toString(16).padStart(8, "0");
+}
+
+/** An i64 constant from its halves, in LLVM's unsigned hexadecimal form. */
+function i64Lit(lo: number, hi: number): string {
+ return `u0x${hex8(hi)}${hex8(lo)}`;
+}
+
+/** The canonical signature identity call_indirect compares: two
+ * independent 32-bit FNV-1a hashes of the signature text. Equal signatures
+ * in different modules get the same identity. */
+export function signatureHash(sig: string): string {
+ let a = 0x811c9dc5;
+ let b = 0x01000193 ^ 0x5bd1e995;
+ for (let i = 0; i < sig.length; i++) {
+ const c = sig.charCodeAt(i);
+ a = Math.imul(a ^ c, 0x01000193);
+ b = Math.imul(b ^ c, 0x01000193) ^ (b >>> 13);
+ }
+ return i64Lit(a >>> 0, b >>> 0);
+}
+
+/** UTF-8 bytes of a string. */
+function utf8Bytes(text: string): number[] {
+ const out: number[] = [];
+ for (let i = 0; i < text.length; i++) {
+ const cp = text.codePointAt(i)!;
+ if (cp > 0xffff) i++;
+ if (cp < 0x80) out.push(cp);
+ else if (cp < 0x800) out.push(0xc0 | (cp >> 6), 0x80 | (cp & 0x3f));
+ else if (cp < 0x10000)
+ out.push(0xe0 | (cp >> 12), 0x80 | ((cp >> 6) & 0x3f), 0x80 | (cp & 0x3f));
+ else {
+ out.push(
+ 0xf0 | (cp >> 18),
+ 0x80 | ((cp >> 12) & 0x3f),
+ 0x80 | ((cp >> 6) & 0x3f),
+ 0x80 | (cp & 0x3f),
+ );
+ }
+ }
+ return out;
+}
+
+/** An LLVM c"..." string body for bytes. */
+function cBytes(bytes: Uint8Array, start: number, end: number): string {
+ let s = "";
+ for (let i = start; i < end; i++) {
+ const b = bytes[i]!;
+ if (b >= 0x20 && b < 0x7f && b !== 0x22 && b !== 0x5c) s += String.fromCharCode(b);
+ else s += "\\" + (b < 16 ? "0" : "") + b.toString(16).toUpperCase();
+ }
+ return s;
+}
+
+interface StackValue {
+ v: string;
+ t: number;
+ /** The line of the load that produced the value (this.lines). */
+ load?: number;
+}
+
+interface Frame {
+ kind: "block" | "loop" | "if" | "func";
+ params: number[];
+ results: number[];
+ /** Branch target: the loop header, or the end label. */
+ target: string;
+ end: string;
+ /** Allocas the values of a branch to this frame are stored in: the loop
+ * parameters for a loop, the results otherwise. */
+ branchSlots: string[];
+ /** Allocas of the results at the frame's end (loops: their own). */
+ resultSlots: string[];
+ height: number;
+ /** An if whose else branch has not started. */
+ elseLabel: string | null;
+ /** The parameter values of an if, for its implicit else. */
+ ifParams: StackValue[];
+ /** The start of the frame was reachable. */
+ live: boolean;
+}
+
+class Lines {
+ lines: string[] = [];
+ add(line: string): void {
+ this.lines.push(line);
+ }
+}
+
+/** One module's translation. */
+class ModuleTranslator {
+ m: WasmModule;
+ p: string;
+ opts: WasmTranslateOptions;
+ text: string[] = [];
+ /** Functions that need a reference record. */
+ refs: Set;
+ /** Functions whose entry checks the stack (stackCheckedFunctions). */
+ stackChecked: Set;
+ typeHashes: string[];
+ memoryCount: number;
+
+ constructor(m: WasmModule, prefix: string, opts: WasmTranslateOptions) {
+ this.m = m;
+ this.p = prefix;
+ this.opts = opts;
+ this.refs = new Set(m.declaredRefs);
+ this.typeHashes = m.types.map((t) => signatureHash(sigString(t)));
+ this.memoryCount = m.memories.length;
+ if (m.memories.length > 1) throw new WasmTranslateError("multiple memories are not supported");
+ this.stackChecked = stackCheckedFunctions(m);
+ }
+
+ fn(index: number): string {
+ return `@${this.p}_f${index}`;
+ }
+
+ funcType(index: number): WasmFuncType {
+ return this.m.types[this.m.funcTypes[index]!]!;
+ }
+
+ globalSlot(index: number): number {
+ return OFF_TABLES + 8 * (this.m.tables.length + index);
+ }
+
+ tableSlot(index: number): number {
+ return OFF_TABLES + 8 * index;
+ }
+
+ signature(ft: WasmFuncType, name: string): string {
+ const params = ["ptr %inst"];
+ for (let i = 0; i < ft.params.length; i++) params.push(`${llType(ft.params[i]!)} %p${i}`);
+ return `${retType(ft.results)} ${name}(${params.join(", ")})`;
+ }
+
+ translate(): void {
+ const m = this.m;
+ for (let i = 0; i < m.importedFuncs; i++) this.importStub(i);
+ for (let i = 0; i < m.codes.length; i++) {
+ const index = m.importedFuncs + i;
+ const f = new FunctionTranslator(this, index);
+ this.text.push(...f.translate());
+ }
+ }
+
+ importStub(index: number): void {
+ const ft = this.funcType(index);
+ const out: string[] = [];
+ out.push(`define internal ${this.signature(ft, this.fn(index))} nounwind {`);
+ out.push(`entry:`);
+ const np = Math.max(1, ft.params.length);
+ const nr = Math.max(1, ft.results.length);
+ out.push(` %args = alloca [${np} x i64], align 8`);
+ out.push(` %res = alloca [${nr} x i64], align 8`);
+ for (let i = 0; i < ft.params.length; i++) {
+ const slot = toSlot(out, ft.params[i]!, `%p${i}`, `a${i}`);
+ out.push(` %ap${i} = getelementptr i64, ptr %args, i64 ${i}`);
+ out.push(` store i64 ${slot}, ptr %ap${i}`);
+ }
+ out.push(` %host = load ptr, ptr %inst`);
+ out.push(` call void @scr_wasm_import_call(ptr %host, i32 ${index}, ptr %args, ptr %res)`);
+ emitSlotReturn(out, ft.results, "%res");
+ out.push(`}`);
+ this.text.push(out.join("\n"));
+ }
+
+ /** The thunk the runtime calls for exported function `index`. */
+ exportThunk(index: number): string {
+ const name = `@${this.p}_x${index}`;
+ const ft = this.funcType(index);
+ const out: string[] = [];
+ out.push(`define internal void ${name}(ptr %inst, ptr %a, ptr %r) nounwind {`);
+ out.push(`entry:`);
+ const args: string[] = ["ptr %inst"];
+ for (let i = 0; i < ft.params.length; i++) {
+ out.push(` %ap${i} = getelementptr i64, ptr %a, i64 ${i}`);
+ out.push(` %as${i} = load i64, ptr %ap${i}`);
+ args.push(`${llType(ft.params[i]!)} ${fromSlot(out, ft.params[i]!, `%as${i}`, `a${i}`)}`);
+ }
+ const ret = retType(ft.results);
+ if (ft.results.length === 0) {
+ out.push(` call void ${this.fn(index)}(${args.join(", ")})`);
+ } else {
+ out.push(` %res = call ${ret} ${this.fn(index)}(${args.join(", ")})`);
+ for (let i = 0; i < ft.results.length; i++) {
+ let v = "%res";
+ if (ft.results.length > 1) {
+ out.push(` %rv${i} = extractvalue ${ret} %res, ${i}`);
+ v = `%rv${i}`;
+ }
+ const slot = toSlot(out, ft.results[i]!, v, `r${i}`);
+ out.push(` %rp${i} = getelementptr i64, ptr %r, i64 ${i}`);
+ out.push(` store i64 ${slot}, ptr %rp${i}`);
+ }
+ }
+ out.push(` ret void`);
+ out.push(`}`);
+ this.text.push(out.join("\n"));
+ return name;
+ }
+
+ /** A call thunk for signature `type`: `(code, inst, args, results)`
+ * calls `code` (a function of that signature) with the arguments in
+ * 64-bit slots. The runtime calls a function reference that reaches
+ * JavaScript through it when the module does not export the function. */
+ callThunk(type: number): string {
+ const name = `@${this.p}_k${type}`;
+ const ft = this.m.types[type]!;
+ const out: string[] = [];
+ out.push(`define internal void ${name}(ptr %code, ptr %inst, ptr %a, ptr %r) nounwind {`);
+ out.push(`entry:`);
+ const args: string[] = ["ptr %inst"];
+ for (let i = 0; i < ft.params.length; i++) {
+ out.push(` %ap${i} = getelementptr i64, ptr %a, i64 ${i}`);
+ out.push(` %as${i} = load i64, ptr %ap${i}`);
+ args.push(`${llType(ft.params[i]!)} ${fromSlot(out, ft.params[i]!, `%as${i}`, `a${i}`)}`);
+ }
+ const ret = retType(ft.results);
+ if (ft.results.length === 0) {
+ out.push(` call void %code(${args.join(", ")})`);
+ } else {
+ out.push(` %res = call ${ret} %code(${args.join(", ")})`);
+ for (let i = 0; i < ft.results.length; i++) {
+ let v = "%res";
+ if (ft.results.length > 1) {
+ out.push(` %rv${i} = extractvalue ${ret} %res, ${i}`);
+ v = `%rv${i}`;
+ }
+ const slot = toSlot(out, ft.results[i]!, v, `r${i}`);
+ out.push(` %rp${i} = getelementptr i64, ptr %r, i64 ${i}`);
+ out.push(` store i64 ${slot}, ptr %rp${i}`);
+ }
+ }
+ out.push(` ret void`);
+ out.push(`}`);
+ this.text.push(out.join("\n"));
+ return name;
+ }
+
+ /** Emits the init function: globals, table initializers and segments,
+ * in the specification's order. The runtime calls the start function
+ * (which takes and returns nothing) separately. */
+ initFunction(): string {
+ const m = this.m;
+ const name = `@${this.p}_init`;
+ const f = new FunctionTranslator(this, -1);
+ const body = f.lines;
+ // Globals.
+ for (let g = 0; g < m.globals.length; g++) {
+ const global = m.globals[g]!;
+ if (global.imported || global.init === null) continue;
+ const v = f.constExpr(global.init);
+ const cell = f.loadPtrSlot(this.globalSlot(g));
+ if (global.type === VAL_EXTERNREF) {
+ // A defined externref global starts as null or another global's
+ // value; the runtime's setter retains it.
+ body.add(` call void @scr_wasm_global_set_ref(ptr ${cell}, ptr ${v})`);
+ } else {
+ body.add(` store ${llType(global.type)} ${v}, ptr ${cell}`);
+ }
+ }
+ // Defined tables with an initializer.
+ for (let t = 0; t < m.tables.length; t++) {
+ const table = m.tables[t]!;
+ if (table.imported || table.init === null) continue;
+ const v = f.constExpr(table.init);
+ body.add(
+ ` call void @scr_wasm_table_fill(ptr %inst, i32 ${t}, i32 0, ptr ${v}, i32 ${table.min})`,
+ );
+ }
+ // Element segments: active ones initialize their table and drop;
+ // declarative ones only drop.
+ for (let s = 0; s < m.elems.length; s++) {
+ const seg = m.elems[s]!;
+ if (seg.mode === "active") {
+ const offset = f.constExpr(seg.offset!);
+ body.add(
+ ` call void @scr_wasm_table_init(ptr %inst, i32 ${seg.table}, i32 ${s}, i32 ${offset}, i32 0, i32 ${seg.funcs.length})`,
+ );
+ }
+ if (seg.mode !== "passive") body.add(` call void @scr_wasm_elem_drop(ptr %inst, i32 ${s})`);
+ }
+ for (let s = 0; s < m.datas.length; s++) {
+ const seg = m.datas[s]!;
+ if (seg.mode !== "active") continue;
+ const offset = f.constExpr(seg.offset!);
+ const length = seg.payload.end - seg.payload.start;
+ body.add(
+ ` call void @scr_wasm_memory_init(ptr %inst, i32 ${s}, i32 ${offset}, i32 0, i32 ${length})`,
+ );
+ body.add(` call void @scr_wasm_data_drop(ptr %inst, i32 ${s})`);
+ }
+ body.add(` ret void`);
+ const out = [`define internal void ${name}(ptr %inst) nounwind {`, `entry:`];
+ out.push(...f.entry.lines);
+ out.push(...body.lines);
+ out.push(`}`);
+ this.text.push(out.join("\n"));
+ return name;
+ }
+
+ /** Constant data and the descriptor. */
+ descriptor(sha256: Uint8Array): string {
+ const m = this.m;
+ const p = this.p;
+ const lines: string[] = [];
+ // First, so that every function its constant expressions reference has
+ // a record in the tables below.
+ const init = this.initFunction();
+ let strings = 0;
+ const cstr = (text: readonly number[]): string => {
+ const sym = `@${p}_s${strings++}`;
+ const bytes = new Uint8Array(text);
+ lines.push(
+ `${sym} = private unnamed_addr constant [${bytes.length + 1} x i8] c"${cBytes(bytes, 0, bytes.length)}\\00"`,
+ );
+ return sym;
+ };
+ const textStr = (text: string): { sym: string; len: number } => {
+ const bytes = utf8Bytes(text);
+ return { sym: cstr(bytes), len: bytes.length };
+ };
+ // Imports.
+ const importRows: string[] = [];
+ let funcImport = 0;
+ let tableImport = 0;
+ let memoryImport = 0;
+ let globalImport = 0;
+ for (const imp of m.imports) {
+ const mod = textStr(imp.module);
+ const name = textStr(imp.name);
+ let index = 0;
+ let type = "";
+ if (imp.kind === KIND_FUNC) {
+ index = funcImport++;
+ type = sigString(m.types[imp.type]!);
+ } else if (imp.kind === KIND_TABLE) {
+ index = tableImport++;
+ type = sigChar(imp.type);
+ } else if (imp.kind === KIND_MEMORY) {
+ index = memoryImport++;
+ } else {
+ index = globalImport++;
+ type = sigChar(imp.type);
+ }
+ const typeSym = cstr(utf8Bytes(type));
+ importRows.push(
+ `{ ptr, i64, ptr, i64, i64, i64, ptr } { ptr ${mod.sym}, i64 ${mod.len}, ptr ${name.sym}, i64 ${name.len}, i64 ${imp.kind}, i64 ${index}, ptr ${typeSym} }`,
+ );
+ }
+ const importType = "{ ptr, i64, ptr, i64, i64, i64, ptr }";
+ const importsSym = `@${p}_imports`;
+ lines.push(
+ `${importsSym} = private constant [${importRows.length} x ${importType}] [${importRows.join(", ")}]`,
+ );
+ // Exports.
+ const thunks = new Map();
+ const exportRows: string[] = [];
+ for (const ex of m.exports) {
+ const name = textStr(ex.name);
+ let type = "";
+ let call = "null";
+ if (ex.kind === KIND_FUNC) {
+ type = sigString(this.funcType(ex.index));
+ let thunk = thunks.get(ex.index);
+ if (thunk === undefined) {
+ thunk = this.exportThunk(ex.index);
+ thunks.set(ex.index, thunk);
+ }
+ call = thunk;
+ } else if (ex.kind === KIND_TABLE) {
+ type = sigChar(m.tables[ex.index]!.elemType);
+ } else if (ex.kind === KIND_GLOBAL) {
+ type = sigChar(m.globals[ex.index]!.type);
+ }
+ const typeSym = cstr(utf8Bytes(type));
+ exportRows.push(
+ `{ ptr, i64, i64, i64, ptr, ptr } { ptr ${name.sym}, i64 ${name.len}, i64 ${ex.kind}, i64 ${ex.index}, ptr ${typeSym}, ptr ${call} }`,
+ );
+ }
+ const exportType = "{ ptr, i64, i64, i64, ptr, ptr }";
+ const exportsSym = `@${p}_exports`;
+ lines.push(
+ `${exportsSym} = private constant [${exportRows.length} x ${exportType}] [${exportRows.join(", ")}]`,
+ );
+ // Function references: code, signature identity and signature text for
+ // every function a reference can name.
+ const nf = m.funcTypes.length;
+ const codes: string[] = [];
+ const callThunks: string[] = [];
+ const callThunkSyms = new Map();
+ const types: string[] = [];
+ const sigs: string[] = [];
+ const sigSyms = new Map();
+ for (let f = 0; f < nf; f++) {
+ const referenced = this.refs.has(f);
+ codes.push(referenced ? `ptr ${this.fn(f)}` : "ptr null");
+ // Referenced functions the module does not export reach JavaScript
+ // through their signature's call thunk.
+ if (referenced && !thunks.has(f)) {
+ const type = m.funcTypes[f]!;
+ let thunk = callThunkSyms.get(type);
+ if (thunk === undefined) {
+ thunk = this.callThunk(type);
+ callThunkSyms.set(type, thunk);
+ }
+ callThunks.push(`ptr ${thunk}`);
+ } else {
+ callThunks.push("ptr null");
+ }
+ types.push(`i64 ${this.typeHashes[m.funcTypes[f]!]!}`);
+ const sig = sigString(this.funcType(f));
+ let sym = sigSyms.get(sig);
+ if (sym === undefined) {
+ sym = cstr(utf8Bytes(sig));
+ sigSyms.set(sig, sym);
+ }
+ sigs.push(`ptr ${sym}`);
+ }
+ const codesSym = `@${p}_codes`;
+ const typesSym = `@${p}_types`;
+ const sigsSym = `@${p}_sigs`;
+ const callThunksSym = `@${p}_calls`;
+ lines.push(`${codesSym} = private constant [${nf} x ptr] [${codes.join(", ")}]`);
+ lines.push(`${callThunksSym} = private constant [${nf} x ptr] [${callThunks.join(", ")}]`);
+ lines.push(`${typesSym} = private constant [${nf} x i64] [${types.join(", ")}]`);
+ lines.push(`${sigsSym} = private constant [${nf} x ptr] [${sigs.join(", ")}]`);
+ // Tables.
+ const tableRows = m.tables.map(
+ (t) =>
+ `{ i64, i64, i64, i64 } { i64 ${sigChar(t.elemType).charCodeAt(0)}, i64 ${t.min}, i64 ${t.max === null ? -1 : t.max}, i64 ${t.imported ? 1 : 0} }`,
+ );
+ const tablesSym = `@${p}_tables`;
+ lines.push(
+ `${tablesSym} = private constant [${tableRows.length} x { i64, i64, i64, i64 }] [${tableRows.join(", ")}]`,
+ );
+ // Globals.
+ const globalRows = m.globals.map(
+ (g) =>
+ `{ i64, i64, i64 } { i64 ${sigChar(g.type).charCodeAt(0)}, i64 ${g.mutable ? 1 : 0}, i64 ${g.imported ? 1 : 0} }`,
+ );
+ const globalsSym = `@${p}_globals`;
+ lines.push(
+ `${globalsSym} = private constant [${globalRows.length} x { i64, i64, i64 }] [${globalRows.join(", ")}]`,
+ );
+ // Data segments.
+ const dataRows: string[] = [];
+ for (let s = 0; s < m.datas.length; s++) {
+ const seg = m.datas[s]!;
+ const len = seg.payload.end - seg.payload.start;
+ if (len === 0) {
+ dataRows.push(`{ ptr, i64 } { ptr null, i64 0 }`);
+ continue;
+ }
+ const sym = `@${p}_d${s}`;
+ lines.push(
+ `${sym} = private unnamed_addr constant [${len} x i8] c"${cBytes(m.bytes, seg.payload.start, seg.payload.end)}", align 1`,
+ );
+ dataRows.push(`{ ptr, i64 } { ptr ${sym}, i64 ${len} }`);
+ }
+ const datasSym = `@${p}_datas`;
+ lines.push(
+ `${datasSym} = private constant [${dataRows.length} x { ptr, i64 }] [${dataRows.join(", ")}]`,
+ );
+ // Element segments: a function index, -1 for null, or -(2 + g) for the
+ // value of global g.
+ const elemRows: string[] = [];
+ for (let s = 0; s < m.elems.length; s++) {
+ const seg = m.elems[s]!;
+ const items: string[] = [];
+ for (let k = 0; k < seg.funcs.length; k++) {
+ const f = seg.funcs[k]!;
+ if (f >= 0) {
+ items.push(`i64 ${f}`);
+ continue;
+ }
+ items.push(`i64 ${this.elemExprCode(seg.exprs[k]!)}`);
+ }
+ let itemsRef = "ptr null";
+ if (items.length > 0) {
+ const sym = `@${p}_e${s}`;
+ lines.push(`${sym} = private constant [${items.length} x i64] [${items.join(", ")}]`);
+ itemsRef = `ptr ${sym}`;
+ }
+ elemRows.push(
+ `{ i64, i64, ptr } { i64 ${sigChar(seg.elemType).charCodeAt(0)}, i64 ${items.length}, ${itemsRef} }`,
+ );
+ }
+ const elemsSym = `@${p}_elems`;
+ lines.push(
+ `${elemsSym} = private constant [${elemRows.length} x { i64, i64, ptr }] [${elemRows.join(", ")}]`,
+ );
+ // Custom sections, when the program reads them.
+ const customRows: string[] = [];
+ if (this.opts.customSections === true) {
+ for (let s = 0; s < m.customs.length; s++) {
+ const custom = m.customs[s]!;
+ const name = textStr(custom.name);
+ const len = custom.payload.end - custom.payload.start;
+ let data = "ptr null";
+ if (len > 0) {
+ const sym = `@${p}_c${s}`;
+ lines.push(
+ `${sym} = private unnamed_addr constant [${len} x i8] c"${cBytes(m.bytes, custom.payload.start, custom.payload.end)}", align 1`,
+ );
+ data = `ptr ${sym}`;
+ }
+ customRows.push(
+ `{ ptr, i64, ptr, i64 } { ptr ${name.sym}, i64 ${name.len}, ${data}, i64 ${len} }`,
+ );
+ }
+ }
+ const customsSym = `@${p}_customs`;
+ lines.push(
+ `${customsSym} = private constant [${customRows.length} x { ptr, i64, ptr, i64 }] [${customRows.join(", ")}]`,
+ );
+ const memory = m.memories[0];
+ const sha: string[] = [];
+ for (let i = 0; i < 32; i++) sha.push(`i8 ${sha256[i]!}`);
+ const desc = `@${p}_desc`;
+ const fields = [
+ `[32 x i8] [${sha.join(", ")}]`,
+ `i64 ${m.bytes.length}`,
+ `i64 ${WASM_ABI_VERSION}`,
+ `i64 ${this.opts.boundsChecks ? 1 : 0}`,
+ `i64 ${m.imports.length}`,
+ `ptr ${importsSym}`,
+ `i64 ${m.exports.length}`,
+ `ptr ${exportsSym}`,
+ `i64 ${nf}`,
+ `i64 ${m.importedFuncs}`,
+ `ptr ${codesSym}`,
+ `ptr ${typesSym}`,
+ `ptr ${sigsSym}`,
+ `i64 ${m.tables.length}`,
+ `ptr ${tablesSym}`,
+ `i64 ${this.memoryCount}`,
+ `i64 ${memory === undefined ? 0 : memory.min}`,
+ `i64 ${memory === undefined || memory.max === null ? -1 : memory.max}`,
+ `i64 ${m.globals.length}`,
+ `ptr ${globalsSym}`,
+ `i64 ${m.datas.length}`,
+ `ptr ${datasSym}`,
+ `i64 ${m.elems.length}`,
+ `ptr ${elemsSym}`,
+ `ptr ${init}`,
+ `ptr ${m.start === null ? "null" : this.fn(m.start)}`,
+ `i64 ${customRows.length}`,
+ `ptr ${customsSym}`,
+ `ptr ${callThunksSym}`,
+ ];
+ const descType =
+ "{ [32 x i8], i64, i64, i64, i64, ptr, i64, ptr, i64, i64, ptr, ptr, ptr, i64, ptr, i64, i64, i64, i64, ptr, i64, ptr, i64, ptr, ptr, ptr, i64, ptr, ptr }";
+ lines.push(`${desc} = internal constant ${descType} { ${fields.join(", ")} }`);
+ this.text.push(lines.join("\n"));
+ return desc;
+ }
+
+ /** The element-segment code of a non-ref.func constant expression. */
+ elemExprCode(expr: WasmRange): number {
+ const c = new WasmCursor(this.m.bytes, expr.start);
+ const op = c.u8();
+ if (op === 0xd0) {
+ c.valType();
+ if (c.u8() === 0x0b) return -1;
+ } else if (op === 0x23) {
+ const g = c.u32();
+ if (c.u8() === 0x0b) return -(2 + g);
+ }
+ throw new WasmTranslateError(
+ "element segment initializers other than ref.null, ref.func and global.get are not supported",
+ );
+ }
+}
+
+/** Store a value as a 64-bit slot; answers the slot operand. */
+function toSlot(out: string[], t: number, v: string, tag: string): string {
+ switch (t) {
+ case VAL_I32:
+ out.push(` %${tag}z = zext i32 ${v} to i64`);
+ return `%${tag}z`;
+ case VAL_I64:
+ return v;
+ case VAL_F32:
+ out.push(` %${tag}b = bitcast float ${v} to i32`);
+ out.push(` %${tag}z = zext i32 %${tag}b to i64`);
+ return `%${tag}z`;
+ case VAL_F64:
+ out.push(` %${tag}z = bitcast double ${v} to i64`);
+ return `%${tag}z`;
+ default:
+ out.push(` %${tag}z = ptrtoint ptr ${v} to i64`);
+ return `%${tag}z`;
+ }
+}
+
+function fromSlot(out: string[], t: number, slot: string, tag: string): string {
+ switch (t) {
+ case VAL_I32:
+ out.push(` %${tag}v = trunc i64 ${slot} to i32`);
+ return `%${tag}v`;
+ case VAL_I64:
+ return slot;
+ case VAL_F32:
+ out.push(` %${tag}t = trunc i64 ${slot} to i32`);
+ out.push(` %${tag}v = bitcast i32 %${tag}t to float`);
+ return `%${tag}v`;
+ case VAL_F64:
+ out.push(` %${tag}v = bitcast i64 ${slot} to double`);
+ return `%${tag}v`;
+ default:
+ out.push(` %${tag}v = inttoptr i64 ${slot} to ptr`);
+ return `%${tag}v`;
+ }
+}
+
+function emitSlotReturn(out: string[], results: readonly number[], buffer: string): void {
+ if (results.length === 0) {
+ out.push(` ret void`);
+ return;
+ }
+ const vals: string[] = [];
+ for (let i = 0; i < results.length; i++) {
+ out.push(` %rp${i} = getelementptr i64, ptr ${buffer}, i64 ${i}`);
+ out.push(` %rs${i} = load i64, ptr %rp${i}`);
+ vals.push(fromSlot(out, results[i]!, `%rs${i}`, `r${i}`));
+ }
+ if (results.length === 1) {
+ out.push(` ret ${llType(results[0]!)} ${vals[0]}`);
+ return;
+ }
+ const ret = retType(results);
+ let agg = "undef";
+ for (let i = 0; i < results.length; i++) {
+ out.push(` %agg${i} = insertvalue ${ret} ${agg}, ${llType(results[i]!)} ${vals[i]}, ${i}`);
+ agg = `%agg${i}`;
+ }
+ out.push(` ret ${ret} ${agg}`);
+}
+
+const LOAD_INFO: Record = {
+ // opcode: [result type, memory type, bytes, extension]
+ 0x28: [VAL_I32, "i32", 4, ""],
+ 0x29: [VAL_I64, "i64", 8, ""],
+ 0x2a: [VAL_F32, "float", 4, ""],
+ 0x2b: [VAL_F64, "double", 8, ""],
+ 0x2c: [VAL_I32, "i8", 1, "sext"],
+ 0x2d: [VAL_I32, "i8", 1, "zext"],
+ 0x2e: [VAL_I32, "i16", 2, "sext"],
+ 0x2f: [VAL_I32, "i16", 2, "zext"],
+ 0x30: [VAL_I64, "i8", 1, "sext"],
+ 0x31: [VAL_I64, "i8", 1, "zext"],
+ 0x32: [VAL_I64, "i16", 2, "sext"],
+ 0x33: [VAL_I64, "i16", 2, "zext"],
+ 0x34: [VAL_I64, "i32", 4, "sext"],
+ 0x35: [VAL_I64, "i32", 4, "zext"],
+};
+
+const STORE_INFO: Record = {
+ 0x36: [VAL_I32, "i32", 4],
+ 0x37: [VAL_I64, "i64", 8],
+ 0x38: [VAL_F32, "float", 4],
+ 0x39: [VAL_F64, "double", 8],
+ 0x3a: [VAL_I32, "i8", 1],
+ 0x3b: [VAL_I32, "i16", 2],
+ 0x3c: [VAL_I64, "i8", 1],
+ 0x3d: [VAL_I64, "i16", 2],
+ 0x3e: [VAL_I64, "i32", 4],
+};
+
+/** Translates one function body (index >= 0) or hosts the init function's
+ * constant expressions (index -1). */
+class FunctionTranslator {
+ mt: ModuleTranslator;
+ m: WasmModule;
+ index: number;
+ ft: WasmFuncType;
+ entry = new Lines();
+ lines = new Lines();
+ stack: StackValue[] = [];
+ frames: Frame[] = [];
+ localTypes: number[] = [];
+ next = 0;
+ labels = 0;
+ live = true;
+ trapLabels = new Map();
+ /** Guard mode: the memory base, loaded once in the entry block. */
+ baseValue: string | null = null;
+ checked: boolean;
+
+ constructor(mt: ModuleTranslator, index: number) {
+ this.mt = mt;
+ this.m = mt.m;
+ this.index = index;
+ this.ft = index >= 0 ? mt.funcType(index) : { params: [], results: [] };
+ this.checked = mt.opts.boundsChecks;
+ }
+
+ tmp(): string {
+ return `%v${this.next++}`;
+ }
+
+ label(hint: string): string {
+ return `${hint}${this.labels++}`;
+ }
+
+ add(line: string): void {
+ this.lines.add(line);
+ }
+
+ emit(expr: string): string {
+ const v = this.tmp();
+ this.add(` ${v} = ${expr}`);
+ return v;
+ }
+
+ push(v: string, t: number): void {
+ this.stack.push({ v, t });
+ }
+
+ pop(): StackValue {
+ const v = this.stack.pop();
+ if (v === undefined) throw new Error("wasm translator: operand stack underflow");
+ return v;
+ }
+
+ /** Pops a value that must have type `t`: the type a local, global,
+ * table or memory store writes. Validation guarantees it; a mismatch
+ * means the translator and the validator disagree about the code, so
+ * the module is not translated. */
+ popType(t: number, what: string): StackValue {
+ const v = this.pop();
+ if (v.t !== t) throw new WasmTranslateError(`${what} operand type does not match`);
+ return v;
+ }
+
+ /** The declared type of local `i` (parameters first). */
+ localType(i: number): number {
+ const t = this.localTypes[i];
+ if (t === undefined) throw new WasmTranslateError(`unknown local ${i}`);
+ return t;
+ }
+
+ /** The offset of a memarg over memory 0 (the only memory a translatable
+ * module has). */
+ memargOffset(c: WasmCursor): number {
+ const flags = readMemargFlags(() => c.u32());
+ if (flags.align < 0 || flags.memory !== 0)
+ throw new WasmTranslateError(`memarg of memory ${flags.memory} is not supported`);
+ return c.u32();
+ }
+
+ /** A memory index immediate (memory.size, memory.grow and the bulk
+ * memory instructions), which must be 0. */
+ memoryIndex(c: WasmCursor): void {
+ const index = c.u32();
+ if (index !== 0) throw new WasmTranslateError(`memory ${index} is not supported`);
+ }
+
+ popN(n: number): StackValue[] {
+ const out = this.stack.splice(this.stack.length - n, n);
+ if (out.length !== n) throw new Error("wasm translator: operand stack underflow");
+ return out;
+ }
+
+ /** Starts a new basic block. */
+ startBlock(label: string): void {
+ this.add(`${label}:`);
+ }
+
+ alloca(type: string): string {
+ const v = `%s${this.next++}`;
+ this.entry.add(` ${v} = alloca ${type}`);
+ return v;
+ }
+
+ slotPtr(offset: number): string {
+ if (offset === 0) return "%inst";
+ return this.emit(`getelementptr inbounds i8, ptr %inst, i64 ${offset}`);
+ }
+
+ loadPtrSlot(offset: number): string {
+ return this.emit(`load ptr, ptr ${this.slotPtr(offset)}, align 8`);
+ }
+
+ trap(code: number): string {
+ let label = this.trapLabels.get(code);
+ if (label === undefined) {
+ label = `trap${code}`;
+ this.trapLabels.set(code, label);
+ }
+ return label;
+ }
+
+ // ── memory ──────────────────────────────────────────────────────────
+
+ /** Checked mode: the slots holding the base and byte length. */
+ memSlots: { base: string; size: string } | null = null;
+
+ refreshMemory(): void {
+ if (!this.checked || this.memSlots === null) return;
+ // From the memory object, not the instance: a memory another instance
+ // (or JavaScript) grew may have moved.
+ const mem = this.loadPtrSlot(OFF_MEMORY);
+ const base = this.emit(`load ptr, ptr ${mem}, align 8`);
+ this.add(` store ptr ${base}, ptr ${this.memSlots.base}`);
+ const sizePtr = this.emit(`getelementptr inbounds i8, ptr ${mem}, i64 8`);
+ const size = this.emit(`load i64, ptr ${sizePtr}, align 8`);
+ this.add(` store i64 ${size}, ptr ${this.memSlots.size}`);
+ }
+
+ base(): string {
+ if (this.checked) {
+ return this.emit(`load ptr, ptr ${this.memSlots!.base}`);
+ }
+ if (this.baseValue === null) {
+ this.baseValue = "%membase";
+ }
+ return this.baseValue;
+ }
+
+ /** The address of an access of `bytes` bytes at `addr + offset`. */
+ address(addr: string, offset: number, bytes: number): string {
+ const wide = this.emit(`zext i32 ${addr} to i64`);
+ const ea = offset === 0 ? wide : this.emit(`add i64 ${wide}, ${offset}`);
+ if (this.checked) {
+ const size = this.emit(`load i64, ptr ${this.memSlots!.size}`);
+ const endAddr = this.emit(`add i64 ${ea}, ${bytes}`);
+ const oob = this.emit(`icmp ugt i64 ${endAddr}, ${size}`);
+ const ok = this.label("ok");
+ this.add(` br i1 ${oob}, label %${this.trap(TRAP_MEMORY_OOB)}, label %${ok}`);
+ this.startBlock(ok);
+ }
+ return this.emit(`getelementptr i8, ptr ${this.base()}, i64 ${ea}`);
+ }
+
+ // ── constant expressions ────────────────────────────────────────────
+
+ constExpr(range: WasmRange): string {
+ const c = new WasmCursor(this.m.bytes, range.start);
+ const stack: string[] = [];
+ for (;;) {
+ const op = c.u8();
+ if (op === 0x0b) break;
+ switch (op) {
+ case 0x41:
+ stack.push(String(c.s32()));
+ break;
+ case 0x42: {
+ const [lo, hi] = c.s64();
+ stack.push(i64Lit(lo, hi));
+ break;
+ }
+ case 0x43:
+ stack.push(this.emit(`bitcast i32 ${c.u32le() | 0} to float`));
+ break;
+ case 0x44: {
+ const lo = c.u32le();
+ const hi = c.u32le();
+ stack.push(this.emit(`bitcast i64 ${i64Lit(lo, hi)} to double`));
+ break;
+ }
+ case 0x23: {
+ const g = c.u32();
+ const cell = this.loadPtrSlot(this.mt.globalSlot(g));
+ stack.push(this.emit(`load ${llType(this.m.globals[g]!.type)}, ptr ${cell}`));
+ break;
+ }
+ case 0xd0:
+ c.valType();
+ stack.push("null");
+ break;
+ case 0xd2:
+ stack.push(this.funcRef(c.u32()));
+ break;
+ case 0x6a:
+ case 0x6b:
+ case 0x6c:
+ case 0x7c:
+ case 0x7d:
+ case 0x7e: {
+ const b = stack.pop()!;
+ const a = stack.pop()!;
+ const t = op < 0x70 ? "i32" : "i64";
+ const name =
+ op === 0x6a || op === 0x7c ? "add" : op === 0x6b || op === 0x7d ? "sub" : "mul";
+ stack.push(this.emit(`${name} ${t} ${a}, ${b}`));
+ break;
+ }
+ default:
+ throw new WasmTranslateError(
+ `constant expression opcode 0x${op.toString(16)} is not supported`,
+ );
+ }
+ }
+ return stack[stack.length - 1]!;
+ }
+
+ funcRef(f: number): string {
+ this.mt.refs.add(f);
+ const records = this.loadPtrSlot(OFF_FUNCREFS);
+ return this.emit(`getelementptr inbounds i8, ptr ${records}, i64 ${f * FUNCREF_SIZE}`);
+ }
+
+ // ── control ─────────────────────────────────────────────────────────
+
+ blockSig(c: WasmCursor): { params: number[]; results: number[] } {
+ const bt = c.blockType();
+ if (bt.kind === "empty") return { params: [], results: [] };
+ if (bt.kind === "value") return { params: [], results: [bt.type] };
+ const t = this.m.types[bt.index]!;
+ return { params: t.params.slice(), results: t.results.slice() };
+ }
+
+ slotsFor(types: readonly number[]): string[] {
+ return types.map((t) => this.alloca(llType(t)));
+ }
+
+ storeTo(slots: readonly string[], values: readonly StackValue[]): void {
+ for (let i = 0; i < slots.length; i++) {
+ const v = values[i]!;
+ this.add(` store ${llType(v.t)} ${v.v}, ptr ${slots[i]}`);
+ }
+ }
+
+ loadFrom(slots: readonly string[], types: readonly number[]): void {
+ for (let i = 0; i < slots.length; i++) {
+ const t = types[i]!;
+ this.push(this.emit(`load ${llType(t)}, ptr ${slots[i]}`), t);
+ }
+ }
+
+ /** Branch to the frame `depth` levels out (the stack keeps its values). */
+ branch(depth: number): void {
+ const f = this.frames[this.frames.length - 1 - depth]!;
+ const n = f.branchSlots.length;
+ if (n > 0) this.storeTo(f.branchSlots, this.stack.slice(this.stack.length - n));
+ this.add(` br label %${f.target}`);
+ }
+
+ /** The label of a block that branches to `depth`, for conditional
+ * branches and tables: the target itself when no values travel. */
+ branchEdge(depth: number, pending: string[]): string {
+ const f = this.frames[this.frames.length - 1 - depth]!;
+ if (f.branchSlots.length === 0) return f.target;
+ const edge = this.label("edge");
+ const saved = this.lines;
+ const edgeLines = new Lines();
+ this.lines = edgeLines;
+ this.startBlock(edge);
+ this.branch(depth);
+ this.lines = saved;
+ pending.push(...edgeLines.lines);
+ return edge;
+ }
+
+ goDead(): void {
+ this.live = false;
+ }
+
+ pushFrame(kind: Frame["kind"], sig: { params: number[]; results: number[] }): Frame {
+ const end = this.label("end");
+ const frame: Frame = {
+ kind,
+ params: sig.params,
+ results: sig.results,
+ target: end,
+ end,
+ branchSlots: [],
+ resultSlots: this.slotsFor(sig.results),
+ height: this.stack.length - sig.params.length,
+ elseLabel: null,
+ ifParams: [],
+ live: true,
+ };
+ frame.branchSlots = frame.resultSlots;
+ this.frames.push(frame);
+ return frame;
+ }
+
+ /** Skips dead code from just after a terminator to the `else` or `end`
+ * of the current frame; answers the opcode it stopped at (consumed). */
+ skipDead(c: WasmCursor): number {
+ let depth = 0;
+ for (;;) {
+ const op = c.u8();
+ if (op === 0x02 || op === 0x03 || op === 0x04) {
+ c.blockType();
+ depth++;
+ continue;
+ }
+ if (op === 0x0b) {
+ if (depth === 0) return op;
+ depth--;
+ continue;
+ }
+ if (op === 0x05 && depth === 0) return op;
+ skipImmediates(c, op);
+ }
+ }
+
+ translate(): string[] {
+ const ft = this.ft;
+ const code = this.m.codes[this.index - this.m.importedFuncs]!;
+ // Locals.
+ const localSlots: string[] = [];
+ this.localTypes = ft.params.concat(code.locals);
+ for (let i = 0; i < this.localTypes.length; i++) {
+ const t = this.localTypes[i]!;
+ const slot = `%l${i}`;
+ localSlots.push(slot);
+ this.entry.add(` ${slot} = alloca ${llType(t)}`);
+ }
+ for (let i = 0; i < ft.params.length; i++) {
+ this.entry.add(` store ${llType(ft.params[i]!)} %p${i}, ptr %l${i}`);
+ }
+ for (let i = ft.params.length; i < this.localTypes.length; i++) {
+ const t = this.localTypes[i]!;
+ this.entry.add(` store ${llType(t)} ${zeroOf(t)}, ptr %l${i}`);
+ }
+ if (this.checked && this.mt.memoryCount > 0) {
+ this.memSlots = { base: "%mbase", size: "%msize" };
+ this.entry.add(` %mbase = alloca ptr`);
+ this.entry.add(` %msize = alloca i64`);
+ }
+ // The function frame: branches to it return.
+ const ret = this.label("ret");
+ const fnFrame: Frame = {
+ kind: "func",
+ params: [],
+ results: ft.results,
+ target: ret,
+ end: ret,
+ branchSlots: [],
+ resultSlots: this.slotsFor(ft.results),
+ height: 0,
+ elseLabel: null,
+ ifParams: [],
+ live: true,
+ };
+ fnFrame.branchSlots = fnFrame.resultSlots;
+ this.frames.push(fnFrame);
+ if (this.memSlots !== null) this.refreshMemory();
+ const c = new WasmCursor(this.m.bytes, code.body.start);
+ this.body(c, code.body.end);
+ // Epilogue.
+ const out: string[] = [];
+ out.push(`define internal ${this.mt.signature(ft, this.mt.fn(this.index))} ${FUNC_ATTRS} {`);
+ out.push(`entry:`);
+ out.push(...this.entry.lines);
+ if (!this.checked && this.baseValue !== null) {
+ out.push(` %membase.p = getelementptr inbounds i8, ptr %inst, i64 ${OFF_MEMBASE}`);
+ out.push(` %membase = load ptr, ptr %membase.p, align 8`);
+ }
+ if (this.checked && this.mt.stackChecked.has(this.index)) {
+ // Without the fault handler, stack exhaustion is an explicit check of
+ // the stack pointer against the runtime's guard for the running stack
+ // (thread or async fiber), in the functions through which the stack
+ // can grow without bound (stackCheckedFunctions).
+ // The address of a frame slot stands for the stack pointer; unlike
+ // llvm.stacksave it constrains no other code.
+ out.push(` %sp = alloca i8, align 1`);
+ out.push(` %sp.i = ptrtoint ptr %sp to i64`);
+ out.push(` %limit.p = call ptr @llvm.threadlocal.address.p0(ptr @scr_wasm_stack_limit)`);
+ out.push(` %limit = load i64, ptr %limit.p, align 8`);
+ out.push(` %exhausted = icmp ult i64 %sp.i, %limit`);
+ out.push(` br i1 %exhausted, label %exhausted.trap, label %body`);
+ out.push(`exhausted.trap:`);
+ out.push(` call void @scr_wasm_trap(i32 ${TRAP_EXHAUSTION})`);
+ out.push(` unreachable`);
+ } else {
+ out.push(` br label %body`);
+ }
+ out.push(`body:`);
+ out.push(...this.lines.lines);
+ for (const [code2, label] of this.trapLabels) {
+ out.push(`${label}:`);
+ out.push(` call void @scr_wasm_trap(i32 ${code2})`);
+ out.push(` unreachable`);
+ }
+ out.push(`}`);
+ return [out.join("\n")];
+ }
+
+ /** The function body; returns at the function's final `end`. */
+ body(c: WasmCursor, end: number): void {
+ while (c.pos < end) {
+ if (!this.live) {
+ const op = this.skipDead(c);
+ if (op === 0x05) this.opElse();
+ else if (this.opEnd()) return;
+ continue;
+ }
+ const op = c.u8();
+ if (op === 0x0b) {
+ if (this.opEnd()) return;
+ continue;
+ }
+ this.instruction(c, op);
+ }
+ }
+
+ /** `end`: closes the innermost frame; true when that was the function. */
+ opEnd(): boolean {
+ const f = this.frames[this.frames.length - 1]!;
+ if (this.live) {
+ const n = f.results.length;
+ this.storeTo(f.resultSlots, this.stack.slice(this.stack.length - n));
+ this.add(` br label %${f.end}`);
+ }
+ if (f.elseLabel !== null) {
+ // An if without else: the implicit else passes its parameters on.
+ this.startBlock(f.elseLabel);
+ if (f.live) {
+ this.storeTo(f.resultSlots, f.ifParams);
+ }
+ this.add(` br label %${f.end}`);
+ f.elseLabel = null;
+ }
+ this.frames.pop();
+ this.stack.length = f.height;
+ this.startBlock(f.end);
+ if (f.kind === "func") {
+ if (f.results.length === 0) this.add(` ret void`);
+ else {
+ this.loadFrom(f.resultSlots, f.results);
+ const vals = this.popN(f.results.length);
+ if (vals.length === 1) this.add(` ret ${llType(vals[0]!.t)} ${vals[0]!.v}`);
+ else {
+ const rt = retType(f.results);
+ let agg = "undef";
+ for (let i = 0; i < vals.length; i++) {
+ agg = this.emit(`insertvalue ${rt} ${agg}, ${llType(vals[i]!.t)} ${vals[i]!.v}, ${i}`);
+ }
+ this.add(` ret ${rt} ${agg}`);
+ }
+ }
+ return true;
+ }
+ this.loadFrom(f.resultSlots, f.results);
+ this.live = true;
+ return false;
+ }
+
+ opElse(): void {
+ const f = this.frames[this.frames.length - 1]!;
+ if (this.live) {
+ const n = f.results.length;
+ this.storeTo(f.resultSlots, this.stack.slice(this.stack.length - n));
+ this.add(` br label %${f.end}`);
+ }
+ this.stack.length = f.height;
+ this.startBlock(f.elseLabel!);
+ f.elseLabel = null;
+ for (const v of f.ifParams) this.stack.push(v);
+ this.live = f.live;
+ if (!f.live) {
+ // The if itself was dead: its else branch is too.
+ this.add(` unreachable`);
+ }
+ }
+
+ call(index: number): void {
+ const ft = this.mt.funcType(index);
+ const args = this.popN(ft.params.length);
+ const argText = ["ptr %inst", ...args.map((a) => `${llType(a.t)} ${a.v}`)].join(", ");
+ this.callWith(this.mt.fn(index), ft, argText);
+ }
+
+ callWith(callee: string, ft: WasmFuncType, argText: string): void {
+ const rt = retType(ft.results);
+ if (ft.results.length === 0) {
+ this.add(` call void ${callee}(${argText})`);
+ } else {
+ const r = this.emit(`call ${rt} ${callee}(${argText})`);
+ if (ft.results.length === 1) this.push(r, ft.results[0]!);
+ else
+ for (let i = 0; i < ft.results.length; i++)
+ this.push(this.emit(`extractvalue ${rt} ${r}, ${i}`), ft.results[i]!);
+ }
+ this.refreshMemory();
+ }
+
+ callIndirect(typeIndex: number, table: number): void {
+ const ft = this.m.types[typeIndex]!;
+ const idx = this.pop();
+ const args = this.popN(ft.params.length);
+ const t = this.loadPtrSlot(this.mt.tableSlot(table));
+ const sizePtr = this.emit(`getelementptr inbounds i8, ptr ${t}, i64 8`);
+ const size = this.emit(`load i32, ptr ${sizePtr}, align 8`);
+ const inRange = this.emit(`icmp ult i32 ${idx.v}, ${size}`);
+ const ok = this.label("ci");
+ this.add(` br i1 ${inRange}, label %${ok}, label %${this.trap(TRAP_TABLE_OOB)}`);
+ this.startBlock(ok);
+ const elems = this.emit(`load ptr, ptr ${t}, align 8`);
+ const wide = this.emit(`zext i32 ${idx.v} to i64`);
+ const slot = this.emit(`getelementptr inbounds ptr, ptr ${elems}, i64 ${wide}`);
+ const rec = this.emit(`load ptr, ptr ${slot}, align 8`);
+ const isNull = this.emit(`icmp eq ptr ${rec}, null`);
+ const nn = this.label("cn");
+ this.add(` br i1 ${isNull}, label %${this.trap(TRAP_SIGNATURE)}, label %${nn}`);
+ this.startBlock(nn);
+ const typePtr = this.emit(`getelementptr inbounds i8, ptr ${rec}, i64 8`);
+ const type = this.emit(`load i64, ptr ${typePtr}, align 8`);
+ const same = this.emit(`icmp eq i64 ${type}, ${this.mt.typeHashes[typeIndex]}`);
+ const go = this.label("cg");
+ this.add(` br i1 ${same}, label %${go}, label %${this.trap(TRAP_SIGNATURE)}`);
+ this.startBlock(go);
+ const codePtr = this.emit(`load ptr, ptr ${rec}, align 8`);
+ const instPtr = this.emit(`getelementptr inbounds i8, ptr ${rec}, i64 16`);
+ const inst = this.emit(`load ptr, ptr ${instPtr}, align 8`);
+ const argText = [`ptr ${inst}`, ...args.map((a) => `${llType(a.t)} ${a.v}`)].join(", ");
+ this.callWith(codePtr, ft, argText);
+ }
+
+ /** Integer division and remainder with wasm's traps. */
+ divRem(op: number, t: string): void {
+ const b = this.pop();
+ const a = this.pop();
+ const ty = b.t;
+ const zero = this.emit(`icmp eq ${t} ${b.v}, 0`);
+ const signed = op === 0x6d || op === 0x6f || op === 0x7f || op === 0x81;
+ const rem = op === 0x6f || op === 0x70 || op === 0x81 || op === 0x82;
+ const nz = this.label("nz");
+ this.add(
+ ` br i1 ${zero}, label %${this.trap(rem ? TRAP_REM_BY_ZERO : TRAP_DIV_BY_ZERO)}, label %${nz}`,
+ );
+ this.startBlock(nz);
+ const min = t === "i32" ? "-2147483648" : "-9223372036854775808";
+ if (signed && !rem) {
+ const isMin = this.emit(`icmp eq ${t} ${a.v}, ${min}`);
+ const isNeg1 = this.emit(`icmp eq ${t} ${b.v}, -1`);
+ const ovf = this.emit(`and i1 ${isMin}, ${isNeg1}`);
+ const okl = this.label("dv");
+ this.add(` br i1 ${ovf}, label %${this.trap(TRAP_DIV_UNREPRESENTABLE)}, label %${okl}`);
+ this.startBlock(okl);
+ this.push(this.emit(`sdiv ${t} ${a.v}, ${b.v}`), ty);
+ return;
+ }
+ if (signed && rem) {
+ // INT_MIN % -1 is 0 in wasm (LLVM's srem would be undefined).
+ const isNeg1 = this.emit(`icmp eq ${t} ${b.v}, -1`);
+ const divisor = this.emit(`select i1 ${isNeg1}, ${t} 1, ${t} ${b.v}`);
+ this.push(this.emit(`srem ${t} ${a.v}, ${divisor}`), ty);
+ return;
+ }
+ this.push(this.emit(`${rem ? "urem" : "udiv"} ${t} ${a.v}, ${b.v}`), ty);
+ }
+
+ /** Trapping float-to-int conversion. */
+ truncTrap(op: number): void {
+ const x = this.pop();
+ const from = x.t === VAL_F32 ? "float" : "double";
+ // [lower bound exclusive?, lower, upper (exclusive)] as exact doubles.
+ let lo = "";
+ let loInclusive = false;
+ let hi = "";
+ let to = "i32";
+ let signed = true;
+ switch (op) {
+ case 0xa8: // i32.trunc_f32_s
+ case 0xaa: // i32.trunc_f64_s
+ lo = op === 0xa8 ? "-2147483648.0" : "-2147483649.0";
+ loInclusive = op === 0xa8;
+ hi = "2147483648.0";
+ break;
+ case 0xa9:
+ case 0xab:
+ lo = "-1.0";
+ hi = "4294967296.0";
+ signed = false;
+ break;
+ case 0xae:
+ case 0xb0:
+ lo = "-9223372036854775808.0";
+ loInclusive = true;
+ hi = "9223372036854775808.0";
+ to = "i64";
+ break;
+ default:
+ lo = "-1.0";
+ hi = "18446744073709551616.0";
+ to = "i64";
+ signed = false;
+ break;
+ }
+ const d = from === "double" ? x.v : this.emit(`fpext float ${x.v} to double`);
+ const above = this.emit(`fcmp ${loInclusive ? "oge" : "ogt"} double ${d}, ${hexDouble(lo)}`);
+ const below = this.emit(`fcmp olt double ${d}, ${hexDouble(hi)}`);
+ const ok = this.emit(`and i1 ${above}, ${below}`);
+ const okl = this.label("cv");
+ this.add(` br i1 ${ok}, label %${okl}, label %${this.trap(TRAP_FLOAT_UNREPRESENTABLE)}`);
+ this.startBlock(okl);
+ this.push(
+ this.emit(`${signed ? "fptosi" : "fptoui"} ${from} ${x.v} to ${to}`),
+ to === "i32" ? VAL_I32 : VAL_I64,
+ );
+ }
+
+ unaryIntrinsic(name: string, t: string, vt: number, extra = ""): void {
+ const a = this.pop();
+ this.push(this.emit(`call ${t} @llvm.${name}(${t} ${a.v}${extra})`), vt);
+ }
+
+ binaryIntrinsic(name: string, t: string, vt: number): void {
+ const b = this.pop();
+ const a = this.pop();
+ this.push(this.emit(`call ${t} @llvm.${name}(${t} ${a.v}, ${t} ${b.v})`), vt);
+ }
+
+ binary(instr: string, t: string, vt: number): void {
+ const b = this.pop();
+ const a = this.pop();
+ this.push(this.emit(`${instr} ${t} ${a.v}, ${b.v}`), vt);
+ }
+
+ compare(pred: string, t: string, float: boolean): void {
+ const b = this.pop();
+ const a = this.pop();
+ const r = this.emit(`${float ? "fcmp" : "icmp"} ${pred} ${t} ${a.v}, ${b.v}`);
+ this.push(this.emit(`zext i1 ${r} to i32`), VAL_I32);
+ }
+
+ eqz(t: string): void {
+ const a = this.pop();
+ const r = this.emit(`icmp eq ${t} ${a.v}, 0`);
+ this.push(this.emit(`zext i1 ${r} to i32`), VAL_I32);
+ }
+
+ shift(instr: string, t: string, vt: number): void {
+ const b = this.pop();
+ const a = this.pop();
+ const masked = this.emit(`and ${t} ${b.v}, ${t === "i32" ? 31 : 63}`);
+ this.push(this.emit(`${instr} ${t} ${a.v}, ${masked}`), vt);
+ }
+
+ rotate(name: string, t: string, vt: number): void {
+ const b = this.pop();
+ const a = this.pop();
+ this.push(this.emit(`call ${t} @llvm.${name}.${t}(${t} ${a.v}, ${t} ${a.v}, ${t} ${b.v})`), vt);
+ }
+
+ convert(instr: string, from: string, to: string, vt: number): void {
+ const a = this.pop();
+ this.push(this.emit(`${instr} ${from} ${a.v} to ${to}`), vt);
+ }
+
+ instruction(c: WasmCursor, op: number): void {
+ switch (op) {
+ case 0x00: // unreachable
+ this.add(` br label %${this.trap(TRAP_UNREACHABLE)}`);
+ this.goDead();
+ return;
+ case 0x01:
+ return;
+ case 0x02: {
+ // block
+ const sig = this.blockSig(c);
+ this.pushFrame("block", sig);
+ return;
+ }
+ case 0x03: {
+ // loop
+ const sig = this.blockSig(c);
+ const f = this.pushFrame("loop", sig);
+ f.target = this.label("loop");
+ f.branchSlots = this.slotsFor(sig.params);
+ const params = this.popN(sig.params.length);
+ this.storeTo(f.branchSlots, params);
+ this.add(` br label %${f.target}`);
+ this.startBlock(f.target);
+ this.loadFrom(f.branchSlots, sig.params);
+ return;
+ }
+ case 0x04: {
+ // if
+ const sig = this.blockSig(c);
+ const cond = this.pop();
+ const f = this.pushFrame("if", sig);
+ f.ifParams = this.stack.slice(this.stack.length - sig.params.length);
+ const then = this.label("then");
+ f.elseLabel = this.label("else");
+ const test = this.emit(`icmp ne i32 ${cond.v}, 0`);
+ this.add(` br i1 ${test}, label %${then}, label %${f.elseLabel}`);
+ this.startBlock(then);
+ return;
+ }
+ case 0x05:
+ this.opElse();
+ return;
+ case 0x0c: {
+ const depth = c.u32();
+ this.branch(depth);
+ this.goDead();
+ return;
+ }
+ case 0x0d: {
+ const depth = c.u32();
+ const cond = this.pop();
+ const test = this.emit(`icmp ne i32 ${cond.v}, 0`);
+ const pending: string[] = [];
+ const target = this.branchEdge(depth, pending);
+ const cont = this.label("br");
+ this.add(` br i1 ${test}, label %${target}, label %${cont}`);
+ for (const line of pending) this.add(line);
+ this.startBlock(cont);
+ return;
+ }
+ case 0x0e: {
+ const n = c.u32();
+ const depths: number[] = [];
+ for (let i = 0; i < n; i++) depths.push(c.u32());
+ const def = c.u32();
+ const idx = this.pop();
+ const pending: string[] = [];
+ const edges = new Map();
+ const edgeOf = (d: number): string => {
+ let e = edges.get(d);
+ if (e === undefined) {
+ e = this.branchEdge(d, pending);
+ edges.set(d, e);
+ }
+ return e;
+ };
+ const cases = depths.map((d, i) => `i32 ${i}, label %${edgeOf(d)}`);
+ this.add(` switch i32 ${idx.v}, label %${edgeOf(def)} [${cases.join(" ")}]`);
+ for (const line of pending) this.add(line);
+ this.goDead();
+ return;
+ }
+ case 0x0f: // return
+ this.branch(this.frames.length - 1);
+ this.goDead();
+ return;
+ case 0x10:
+ this.call(c.u32());
+ return;
+ case 0x11: {
+ const type = c.u32();
+ const table = c.u32();
+ this.callIndirect(type, table);
+ return;
+ }
+ case 0x1a: {
+ // A dropped load must still fault when it is out of bounds: LLVM
+ // would delete an unused ordinary load.
+ const v = this.pop();
+ if (v.load !== undefined) {
+ const line = this.lines.lines[v.load]!;
+ this.lines.lines[v.load] = line.replace(" = load ", " = load volatile ");
+ }
+ return;
+ }
+ case 0x1b:
+ case 0x1c: {
+ if (op === 0x1c) {
+ const n = c.u32();
+ for (let i = 0; i < n; i++) c.valType();
+ }
+ const cond = this.pop();
+ const b = this.pop();
+ const a = this.pop();
+ const test = this.emit(`icmp ne i32 ${cond.v}, 0`);
+ const t = llType(a.t);
+ this.push(this.emit(`select i1 ${test}, ${t} ${a.v}, ${t} ${b.v}`), a.t);
+ return;
+ }
+ case 0x20: {
+ const i = c.u32();
+ const t = this.localType(i);
+ this.push(this.emit(`load ${llType(t)}, ptr %l${i}`), t);
+ return;
+ }
+ case 0x21: {
+ const i = c.u32();
+ const t = this.localType(i);
+ const v = this.popType(t, "local.set");
+ this.add(` store ${llType(t)} ${v.v}, ptr %l${i}`);
+ return;
+ }
+ case 0x22: {
+ const i = c.u32();
+ const t = this.localType(i);
+ const v = this.popType(t, "local.tee");
+ this.stack.push(v);
+ this.add(` store ${llType(t)} ${v.v}, ptr %l${i}`);
+ return;
+ }
+ case 0x23: {
+ const g = c.u32();
+ const t = this.m.globals[g]!.type;
+ const cell = this.loadPtrSlot(this.mt.globalSlot(g));
+ this.push(this.emit(`load ${llType(t)}, ptr ${cell}, align 8`), t);
+ return;
+ }
+ case 0x24: {
+ const g = c.u32();
+ const global = this.m.globals[g];
+ if (global === undefined) throw new WasmTranslateError(`unknown global ${g}`);
+ const v = this.popType(global.type, "global.set");
+ const cell = this.loadPtrSlot(this.mt.globalSlot(g));
+ if (v.t === VAL_EXTERNREF)
+ this.add(` call void @scr_wasm_global_set_ref(ptr ${cell}, ptr ${v.v})`);
+ else this.add(` store ${llType(v.t)} ${v.v}, ptr ${cell}, align 8`);
+ return;
+ }
+ case 0x25: {
+ const t = c.u32();
+ const i = this.pop();
+ this.push(
+ this.emit(`call ptr @scr_wasm_table_get(ptr %inst, i32 ${t}, i32 ${i.v})`),
+ this.m.tables[t]!.elemType,
+ );
+ return;
+ }
+ case 0x26: {
+ const t = c.u32();
+ const table = this.m.tables[t];
+ if (table === undefined) throw new WasmTranslateError(`unknown table ${t}`);
+ const v = this.popType(table.elemType, "table.set");
+ const i = this.popType(VAL_I32, "table.set");
+ this.add(` call void @scr_wasm_table_set(ptr %inst, i32 ${t}, i32 ${i.v}, ptr ${v.v})`);
+ return;
+ }
+ case 0x3f: {
+ this.memoryIndex(c);
+ const mem = this.loadPtrSlot(OFF_MEMORY);
+ const sizePtr = this.emit(`getelementptr inbounds i8, ptr ${mem}, i64 8`);
+ const size = this.emit(`load i64, ptr ${sizePtr}, align 8`);
+ const pages = this.emit(`lshr i64 ${size}, 16`);
+ this.push(this.emit(`trunc i64 ${pages} to i32`), VAL_I32);
+ return;
+ }
+ case 0x40: {
+ this.memoryIndex(c);
+ const delta = this.pop();
+ this.push(this.emit(`call i32 @scr_wasm_memory_grow(ptr %inst, i32 ${delta.v})`), VAL_I32);
+ this.refreshMemory();
+ return;
+ }
+ case 0x41:
+ this.push(String(c.s32()), VAL_I32);
+ return;
+ case 0x42: {
+ const [lo, hi] = c.s64();
+ this.push(i64Lit(lo, hi), VAL_I64);
+ return;
+ }
+ case 0x43:
+ this.push(this.emit(`bitcast i32 ${c.u32le() | 0} to float`), VAL_F32);
+ return;
+ case 0x44: {
+ const lo = c.u32le();
+ const hi = c.u32le();
+ this.push(this.emit(`bitcast i64 ${i64Lit(lo, hi)} to double`), VAL_F64);
+ return;
+ }
+ case 0xd0:
+ c.valType();
+ this.push("null", c.bytes[c.pos - 1]!);
+ return;
+ case 0xd1: {
+ const a = this.pop();
+ const r = this.emit(`icmp eq ptr ${a.v}, null`);
+ this.push(this.emit(`zext i1 ${r} to i32`), VAL_I32);
+ return;
+ }
+ case 0xd2:
+ this.push(this.funcRef(c.u32()), VAL_FUNCREF);
+ return;
+ case 0xfc:
+ this.prefixed(c);
+ return;
+ default:
+ break;
+ }
+ if (op >= 0x28 && op <= 0x35) {
+ const offset = this.memargOffset(c);
+ const [vt, mt, bytes, ext] = LOAD_INFO[op]!;
+ const addr = this.popType(VAL_I32, "load address");
+ const p = this.address(addr.v, offset, bytes);
+ const line = this.lines.lines.length;
+ const loaded = this.emit(`load ${mt}, ptr ${p}, align 1`);
+ if (ext === "") this.stack.push({ v: loaded, t: vt, load: line });
+ else {
+ const extended = this.emit(`${ext} ${mt} ${loaded} to ${llType(vt)}`);
+ this.stack.push({ v: extended, t: vt, load: line });
+ }
+ return;
+ }
+ if (op >= 0x36 && op <= 0x3e) {
+ const offset = this.memargOffset(c);
+ const [vt, mt, bytes] = STORE_INFO[op]!;
+ const v = this.popType(vt, "store");
+ const addr = this.popType(VAL_I32, "store address");
+ const p = this.address(addr.v, offset, bytes);
+ let value = v.v;
+ if (mt === "i8" || mt === "i16" || (mt === "i32" && v.t === VAL_I64)) {
+ value = this.emit(`trunc ${llType(v.t)} ${v.v} to ${mt}`);
+ }
+ this.add(` store ${mt} ${value}, ptr ${p}, align 1`);
+ return;
+ }
+ this.numeric(op);
+ }
+
+ numeric(op: number): void {
+ const I32 = VAL_I32;
+ const I64 = VAL_I64;
+ const F32 = VAL_F32;
+ const F64 = VAL_F64;
+ switch (op) {
+ case 0x45:
+ return this.eqz("i32");
+ case 0x46:
+ return this.compare("eq", "i32", false);
+ case 0x47:
+ return this.compare("ne", "i32", false);
+ case 0x48:
+ return this.compare("slt", "i32", false);
+ case 0x49:
+ return this.compare("ult", "i32", false);
+ case 0x4a:
+ return this.compare("sgt", "i32", false);
+ case 0x4b:
+ return this.compare("ugt", "i32", false);
+ case 0x4c:
+ return this.compare("sle", "i32", false);
+ case 0x4d:
+ return this.compare("ule", "i32", false);
+ case 0x4e:
+ return this.compare("sge", "i32", false);
+ case 0x4f:
+ return this.compare("uge", "i32", false);
+ case 0x50:
+ return this.eqz("i64");
+ case 0x51:
+ return this.compare("eq", "i64", false);
+ case 0x52:
+ return this.compare("ne", "i64", false);
+ case 0x53:
+ return this.compare("slt", "i64", false);
+ case 0x54:
+ return this.compare("ult", "i64", false);
+ case 0x55:
+ return this.compare("sgt", "i64", false);
+ case 0x56:
+ return this.compare("ugt", "i64", false);
+ case 0x57:
+ return this.compare("sle", "i64", false);
+ case 0x58:
+ return this.compare("ule", "i64", false);
+ case 0x59:
+ return this.compare("sge", "i64", false);
+ case 0x5a:
+ return this.compare("uge", "i64", false);
+ case 0x5b:
+ return this.compare("oeq", "float", true);
+ case 0x5c:
+ return this.compare("une", "float", true);
+ case 0x5d:
+ return this.compare("olt", "float", true);
+ case 0x5e:
+ return this.compare("ogt", "float", true);
+ case 0x5f:
+ return this.compare("ole", "float", true);
+ case 0x60:
+ return this.compare("oge", "float", true);
+ case 0x61:
+ return this.compare("oeq", "double", true);
+ case 0x62:
+ return this.compare("une", "double", true);
+ case 0x63:
+ return this.compare("olt", "double", true);
+ case 0x64:
+ return this.compare("ogt", "double", true);
+ case 0x65:
+ return this.compare("ole", "double", true);
+ case 0x66:
+ return this.compare("oge", "double", true);
+ case 0x67:
+ return this.unaryIntrinsic("ctlz.i32", "i32", I32, ", i1 false");
+ case 0x68:
+ return this.unaryIntrinsic("cttz.i32", "i32", I32, ", i1 false");
+ case 0x69:
+ return this.unaryIntrinsic("ctpop.i32", "i32", I32);
+ case 0x6a:
+ return this.binary("add", "i32", I32);
+ case 0x6b:
+ return this.binary("sub", "i32", I32);
+ case 0x6c:
+ return this.binary("mul", "i32", I32);
+ case 0x6d:
+ case 0x6e:
+ case 0x6f:
+ case 0x70:
+ return this.divRem(op, "i32");
+ case 0x71:
+ return this.binary("and", "i32", I32);
+ case 0x72:
+ return this.binary("or", "i32", I32);
+ case 0x73:
+ return this.binary("xor", "i32", I32);
+ case 0x74:
+ return this.shift("shl", "i32", I32);
+ case 0x75:
+ return this.shift("ashr", "i32", I32);
+ case 0x76:
+ return this.shift("lshr", "i32", I32);
+ case 0x77:
+ return this.rotate("fshl", "i32", I32);
+ case 0x78:
+ return this.rotate("fshr", "i32", I32);
+ case 0x79:
+ return this.unaryIntrinsic("ctlz.i64", "i64", I64, ", i1 false");
+ case 0x7a:
+ return this.unaryIntrinsic("cttz.i64", "i64", I64, ", i1 false");
+ case 0x7b:
+ return this.unaryIntrinsic("ctpop.i64", "i64", I64);
+ case 0x7c:
+ return this.binary("add", "i64", I64);
+ case 0x7d:
+ return this.binary("sub", "i64", I64);
+ case 0x7e:
+ return this.binary("mul", "i64", I64);
+ case 0x7f:
+ case 0x80:
+ case 0x81:
+ case 0x82:
+ return this.divRem(op, "i64");
+ case 0x83:
+ return this.binary("and", "i64", I64);
+ case 0x84:
+ return this.binary("or", "i64", I64);
+ case 0x85:
+ return this.binary("xor", "i64", I64);
+ case 0x86:
+ return this.shift("shl", "i64", I64);
+ case 0x87:
+ return this.shift("ashr", "i64", I64);
+ case 0x88:
+ return this.shift("lshr", "i64", I64);
+ case 0x89:
+ return this.rotate("fshl", "i64", I64);
+ case 0x8a:
+ return this.rotate("fshr", "i64", I64);
+ case 0x8b:
+ return this.unaryIntrinsic("fabs.f32", "float", F32);
+ case 0x8c: {
+ const a = this.pop();
+ this.push(this.emit(`fneg float ${a.v}`), F32);
+ return;
+ }
+ case 0x8d:
+ return this.unaryIntrinsic("ceil.f32", "float", F32);
+ case 0x8e:
+ return this.unaryIntrinsic("floor.f32", "float", F32);
+ case 0x8f:
+ return this.unaryIntrinsic("trunc.f32", "float", F32);
+ case 0x90:
+ return this.unaryIntrinsic("roundeven.f32", "float", F32);
+ case 0x91:
+ return this.unaryIntrinsic("sqrt.f32", "float", F32);
+ case 0x92:
+ return this.binary("fadd", "float", F32);
+ case 0x93:
+ return this.binary("fsub", "float", F32);
+ case 0x94:
+ return this.binary("fmul", "float", F32);
+ case 0x95:
+ return this.binary("fdiv", "float", F32);
+ case 0x96:
+ return this.binaryIntrinsic("minimum.f32", "float", F32);
+ case 0x97:
+ return this.binaryIntrinsic("maximum.f32", "float", F32);
+ case 0x98:
+ return this.binaryIntrinsic("copysign.f32", "float", F32);
+ case 0x99:
+ return this.unaryIntrinsic("fabs.f64", "double", F64);
+ case 0x9a: {
+ const a = this.pop();
+ this.push(this.emit(`fneg double ${a.v}`), F64);
+ return;
+ }
+ case 0x9b:
+ return this.unaryIntrinsic("ceil.f64", "double", F64);
+ case 0x9c:
+ return this.unaryIntrinsic("floor.f64", "double", F64);
+ case 0x9d:
+ return this.unaryIntrinsic("trunc.f64", "double", F64);
+ case 0x9e:
+ return this.unaryIntrinsic("roundeven.f64", "double", F64);
+ case 0x9f:
+ return this.unaryIntrinsic("sqrt.f64", "double", F64);
+ case 0xa0:
+ return this.binary("fadd", "double", F64);
+ case 0xa1:
+ return this.binary("fsub", "double", F64);
+ case 0xa2:
+ return this.binary("fmul", "double", F64);
+ case 0xa3:
+ return this.binary("fdiv", "double", F64);
+ case 0xa4:
+ return this.binaryIntrinsic("minimum.f64", "double", F64);
+ case 0xa5:
+ return this.binaryIntrinsic("maximum.f64", "double", F64);
+ case 0xa6:
+ return this.binaryIntrinsic("copysign.f64", "double", F64);
+ case 0xa7:
+ return this.convert("trunc", "i64", "i32", I32);
+ case 0xa8:
+ case 0xa9:
+ case 0xaa:
+ case 0xab:
+ case 0xae:
+ case 0xaf:
+ case 0xb0:
+ case 0xb1:
+ return this.truncTrap(op);
+ case 0xac:
+ return this.convert("sext", "i32", "i64", I64);
+ case 0xad:
+ return this.convert("zext", "i32", "i64", I64);
+ case 0xb2:
+ return this.convert("sitofp", "i32", "float", F32);
+ case 0xb3:
+ return this.convert("uitofp", "i32", "float", F32);
+ case 0xb4:
+ return this.convert("sitofp", "i64", "float", F32);
+ case 0xb5:
+ return this.convert("uitofp", "i64", "float", F32);
+ case 0xb6:
+ return this.convert("fptrunc", "double", "float", F32);
+ case 0xb7:
+ return this.convert("sitofp", "i32", "double", F64);
+ case 0xb8:
+ return this.convert("uitofp", "i32", "double", F64);
+ case 0xb9:
+ return this.convert("sitofp", "i64", "double", F64);
+ case 0xba:
+ return this.convert("uitofp", "i64", "double", F64);
+ case 0xbb:
+ return this.convert("fpext", "float", "double", F64);
+ case 0xbc:
+ return this.convert("bitcast", "float", "i32", I32);
+ case 0xbd:
+ return this.convert("bitcast", "double", "i64", I64);
+ case 0xbe:
+ return this.convert("bitcast", "i32", "float", F32);
+ case 0xbf:
+ return this.convert("bitcast", "i64", "double", F64);
+ case 0xc0:
+ case 0xc1: {
+ const a = this.pop();
+ const n = op === 0xc0 ? "i8" : "i16";
+ const narrow = this.emit(`trunc i32 ${a.v} to ${n}`);
+ this.push(this.emit(`sext ${n} ${narrow} to i32`), I32);
+ return;
+ }
+ case 0xc2:
+ case 0xc3:
+ case 0xc4: {
+ const a = this.pop();
+ const n = op === 0xc2 ? "i8" : op === 0xc3 ? "i16" : "i32";
+ const narrow = this.emit(`trunc i64 ${a.v} to ${n}`);
+ this.push(this.emit(`sext ${n} ${narrow} to i64`), I64);
+ return;
+ }
+ default:
+ throw new WasmTranslateError(`opcode 0x${op.toString(16)} is not supported`);
+ }
+ }
+
+ prefixed(c: WasmCursor): void {
+ const sub = c.u32();
+ if (sub <= 7) {
+ // Saturating float-to-int conversions.
+ const a = this.pop();
+ const from = a.t === VAL_F32 ? "f32" : "f64";
+ const fromTy = a.t === VAL_F32 ? "float" : "double";
+ const to = sub < 4 ? "i32" : "i64";
+ const signed = (sub & 1) === 0;
+ this.push(
+ this.emit(
+ `call ${to} @llvm.${signed ? "fptosi" : "fptoui"}.sat.${to}.${from}(${fromTy} ${a.v})`,
+ ),
+ to === "i32" ? VAL_I32 : VAL_I64,
+ );
+ return;
+ }
+ switch (sub) {
+ case 8: {
+ // memory.init
+ const seg = c.u32();
+ this.memoryIndex(c);
+ const n = this.pop();
+ const s = this.pop();
+ const d = this.pop();
+ this.add(
+ ` call void @scr_wasm_memory_init(ptr %inst, i32 ${seg}, i32 ${d.v}, i32 ${s.v}, i32 ${n.v})`,
+ );
+ return;
+ }
+ case 9:
+ this.add(` call void @scr_wasm_data_drop(ptr %inst, i32 ${c.u32()})`);
+ return;
+ case 10: {
+ this.memoryIndex(c);
+ this.memoryIndex(c);
+ const n = this.pop();
+ const s = this.pop();
+ const d = this.pop();
+ this.add(
+ ` call void @scr_wasm_memory_copy(ptr %inst, i32 ${d.v}, i32 ${s.v}, i32 ${n.v})`,
+ );
+ return;
+ }
+ case 11: {
+ this.memoryIndex(c);
+ const n = this.pop();
+ const v = this.pop();
+ const d = this.pop();
+ this.add(
+ ` call void @scr_wasm_memory_fill(ptr %inst, i32 ${d.v}, i32 ${v.v}, i32 ${n.v})`,
+ );
+ return;
+ }
+ case 12: {
+ const seg = c.u32();
+ const t = c.u32();
+ const n = this.pop();
+ const s = this.pop();
+ const d = this.pop();
+ this.add(
+ ` call void @scr_wasm_table_init(ptr %inst, i32 ${t}, i32 ${seg}, i32 ${d.v}, i32 ${s.v}, i32 ${n.v})`,
+ );
+ return;
+ }
+ case 13:
+ this.add(` call void @scr_wasm_elem_drop(ptr %inst, i32 ${c.u32()})`);
+ return;
+ case 14: {
+ const dt = c.u32();
+ const st = c.u32();
+ const n = this.pop();
+ const s = this.pop();
+ const d = this.pop();
+ this.add(
+ ` call void @scr_wasm_table_copy(ptr %inst, i32 ${dt}, i32 ${st}, i32 ${d.v}, i32 ${s.v}, i32 ${n.v})`,
+ );
+ return;
+ }
+ case 15: {
+ const t = c.u32();
+ const n = this.pop();
+ const v = this.pop();
+ this.push(
+ this.emit(`call i32 @scr_wasm_table_grow(ptr %inst, i32 ${t}, ptr ${v.v}, i32 ${n.v})`),
+ VAL_I32,
+ );
+ return;
+ }
+ case 16: {
+ const t = c.u32();
+ const tab = this.loadPtrSlot(this.mt.tableSlot(t));
+ const sizePtr = this.emit(`getelementptr inbounds i8, ptr ${tab}, i64 8`);
+ this.push(this.emit(`load i32, ptr ${sizePtr}, align 8`), VAL_I32);
+ return;
+ }
+ case 17: {
+ const t = c.u32();
+ const n = this.pop();
+ const v = this.pop();
+ const d = this.pop();
+ this.add(
+ ` call void @scr_wasm_table_fill(ptr %inst, i32 ${t}, i32 ${d.v}, ptr ${v.v}, i32 ${n.v})`,
+ );
+ return;
+ }
+ default:
+ throw new WasmTranslateError(`opcode 0xfc ${sub} is not supported`);
+ }
+ }
+}
+
+/** The functions through which the stack can grow without bound, which
+ * check the stack pointer on entry: members of a cycle of direct calls
+ * (including self-recursion), and functions that call through a table or
+ * call an import (an indirect call can reach any function, and an import
+ * can call back into an export). Every other call chain is finite, so its
+ * depth is bounded by the module and the guard's reserve absorbs it. */
+export function stackCheckedFunctions(m: WasmModule): Set {
+ const n = m.funcTypes.length;
+ const callees: number[][] = [];
+ const checked = new Set();
+ for (let f = 0; f < n; f++) callees.push([]);
+ for (let i = 0; i < m.codes.length; i++) {
+ const f = m.importedFuncs + i;
+ const body = m.codes[i]!.body;
+ const c = new WasmCursor(m.bytes, body.start);
+ while (c.pos < body.end) {
+ const op = c.u8();
+ if (op === 0x10) {
+ const callee = c.u32();
+ if (callee < m.importedFuncs) checked.add(f);
+ else callees[f]!.push(callee);
+ } else if (op === 0x11) {
+ c.u32();
+ c.u32();
+ checked.add(f);
+ } else {
+ skipImmediates(c, op);
+ }
+ }
+ }
+ // Tarjan's strongly connected components, iteratively.
+ const index = new Int32Array(n).fill(-1);
+ const low = new Int32Array(n);
+ const onStack = new Uint8Array(n);
+ const stack: number[] = [];
+ let next = 0;
+ for (let root = m.importedFuncs; root < n; root++) {
+ if (index[root] !== -1) continue;
+ const workNode: number[] = [root];
+ const workEdge: number[] = [0];
+ index[root] = low[root] = next++;
+ stack.push(root);
+ onStack[root] = 1;
+ while (workNode.length > 0) {
+ const top = workNode.length - 1;
+ const v = workNode[top]!;
+ const edges = callees[v]!;
+ const e = workEdge[top]!;
+ if (e < edges.length) {
+ workEdge[top] = e + 1;
+ const w = edges[e]!;
+ if (index[w] === -1) {
+ index[w] = low[w] = next++;
+ stack.push(w);
+ onStack[w] = 1;
+ workNode.push(w);
+ workEdge.push(0);
+ } else if (onStack[w]) {
+ low[v] = Math.min(low[v]!, index[w]!);
+ }
+ continue;
+ }
+ workNode.pop();
+ workEdge.pop();
+ if (workNode.length > 0) {
+ const parent = workNode[workNode.length - 1]!;
+ low[parent] = Math.min(low[parent]!, low[v]!);
+ }
+ if (low[v] === index[v]) {
+ const component: number[] = [];
+ for (;;) {
+ const w = stack.pop()!;
+ onStack[w] = 0;
+ component.push(w);
+ if (w === v) break;
+ }
+ if (component.length > 1 || edges.includes(v)) for (const w of component) checked.add(w);
+ }
+ }
+ }
+ return checked;
+}
+
+/** A decimal double literal as LLVM's exact hexadecimal form. */
+function hexDouble(text: string): string {
+ const view = new DataView(new ArrayBuffer(8));
+ view.setFloat64(0, Number(text));
+ return `0x${hex8(view.getUint32(0))}${hex8(view.getUint32(4))}`.toUpperCase().replace("0X", "0x");
+}
+
+function hexOf(bytes: Uint8Array): string {
+ let s = "";
+ for (let i = 0; i < bytes.length; i++) s += bytes[i]!.toString(16).padStart(2, "0");
+ return s;
+}
+
+/** The symbol prefix of a module's translation. */
+export function wasmSymbolPrefix(sha256: Uint8Array): string {
+ return `scr_wasm_${hexOf(sha256).slice(0, 16)}`;
+}
+
+/** Translates a module that binary.ts validated within the translatable
+ * feature level. Throws WasmTranslateError for constructs outside it. */
+export function translateWasmModule(
+ bytes: Uint8Array,
+ sha256: Uint8Array,
+ opts: WasmTranslateOptions,
+): WasmTranslation {
+ const m = parseWasmModule(bytes);
+ for (const t of m.tables) llType(t.elemType);
+ const mt = new ModuleTranslator(m, wasmSymbolPrefix(sha256), opts);
+ mt.translate();
+ const descriptor = mt.descriptor(sha256);
+ const decls = RUNTIME_DECLS.slice();
+ // Initial-exec: executables and their static runtime define it, so the
+ // check is one thread-pointer-relative load, not a TLS lookup call.
+ if (opts.boundsChecks)
+ decls.push("@scr_wasm_stack_limit = external thread_local(initialexec) global i64");
+ return { decls, text: mt.text, descriptor };
+}
diff --git a/packages/compiler/surface-manifest.json b/packages/compiler/surface-manifest.json
index b2eeaecc0..b7cb3ab94 100644
--- a/packages/compiler/surface-manifest.json
+++ b/packages/compiler/surface-manifest.json
@@ -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",
diff --git a/packages/compiler/test/native-callable-values.test.ts b/packages/compiler/test/native-callable-values.test.ts
index e64a9319e..6e65b0c99 100644
--- a/packages/compiler/test/native-callable-values.test.ts
+++ b/packages/compiler/test/native-callable-values.test.ts
@@ -102,19 +102,19 @@ test.each(["llvm"] as const)(
writeFileSync(
join(pkg, "index.js"),
`
-const inspect = (value) => eval(value);
+const inspect = (value) => process.on("__proto__", value);
function stopped(value) {
- eval(value);
+ process.on("__proto__", value);
return { done() { return "wrong"; } };
}
-try { console.log(stopped("0").done()); }
-catch (error) { console.log(String(error).includes("SC2009")); }
+try { console.log(stopped(() => {}).done()); }
+catch (error) { console.log(String(error).includes("SC2020")); }
const first = { inspect };
const second = { inspect };
console.log(typeof first.inspect, first.inspect === second.inspect);
for (const callback of [first.inspect, second.inspect]) {
- try { callback({}); }
- catch (error) { console.log(String(error).includes("SC2009")); }
+ try { callback(() => {}); }
+ catch (error) { console.log(String(error).includes("SC2020")); }
}
`,
);
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5817-constructor-function-instanceof.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5817-constructor-function-instanceof.cjs.json
new file mode 100644
index 000000000..d9ad97beb
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5817-constructor-function-instanceof.cjs.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/5817-constructor-function-instanceof.cjs"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5818-constructor-function-emitter-defaults.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5818-constructor-function-emitter-defaults.cjs.json
new file mode 100644
index 000000000..f22e1aaeb
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5818-constructor-function-emitter-defaults.cjs.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/5818-constructor-function-emitter-defaults.cjs"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6126-punycode-dependency/main.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6126-punycode-dependency/main.cjs.json
index 5de83361a..36f02246c 100644
--- a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6126-punycode-dependency/main.cjs.json
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6126-punycode-dependency/main.cjs.json
@@ -12,7 +12,7 @@
"end": 376
},
"milestone": "M4",
- "hint": "relative imports (./file, ../dir/file), package.json-mediated project imports (#alias via the imports field, self-name references via exports), statically compiled npm packages, and the built-in fs, fs/promises, path, os, url, crypto, zlib, child_process, net, http, tls, https, http2, dgram, dns, util, util/types, string_decoder, querystring, readline, events, stream, stream/promises, stream/consumers, buffer, assert, assert/strict, worker_threads, cluster, tty, async_hooks, timers, timers/promises, diagnostics_channel, perf_hooks, module, and console modules (bare or node:-prefixed) and node:test (node:-prefixed only, like in Node) are supported"
+ "hint": "relative imports (./file, ../dir/file), package.json-mediated project imports (#alias via the imports field, self-name references via exports), statically compiled npm packages, and the built-in fs, fs/promises, path, os, url, crypto, zlib, child_process, net, http, tls, https, http2, dgram, dns, dns/promises, util, util/types, string_decoder, querystring, readline, events, stream, stream/promises, stream/consumers, stream/web, buffer, assert, assert/strict, worker_threads, cluster, tty, async_hooks, timers, timers/promises, diagnostics_channel, perf_hooks, module, v8, and console modules (bare or node:-prefixed) and node:test (node:-prefixed only, like in Node) are supported"
}
]
}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6380-variadic-wrapper-arguments-apply.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6380-variadic-wrapper-arguments-apply.ts.json
new file mode 100644
index 000000000..df0124219
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6380-variadic-wrapper-arguments-apply.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6380-variadic-wrapper-arguments-apply.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6381-variadic-wrapper-async.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6381-variadic-wrapper-async.ts.json
new file mode 100644
index 000000000..2138df43d
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6381-variadic-wrapper-async.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6381-variadic-wrapper-async.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6382-variadic-wrapper-methods-this.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6382-variadic-wrapper-methods-this.ts.json
new file mode 100644
index 000000000..2135f8b6f
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6382-variadic-wrapper-methods-this.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6382-variadic-wrapper-methods-this.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6383-variadic-wrapper-rest-tuple.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6383-variadic-wrapper-rest-tuple.ts.json
new file mode 100644
index 000000000..a2f7f32c4
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6383-variadic-wrapper-rest-tuple.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6383-variadic-wrapper-rest-tuple.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6384-variadic-wrapper-nested.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6384-variadic-wrapper-nested.ts.json
new file mode 100644
index 000000000..d69618e3a
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6384-variadic-wrapper-nested.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6384-variadic-wrapper-nested.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6385-variadic-wrapper-shared-local-names.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6385-variadic-wrapper-shared-local-names.ts.json
new file mode 100644
index 000000000..5791565f2
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6385-variadic-wrapper-shared-local-names.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6385-variadic-wrapper-shared-local-names.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6390-catch-binding-predicate.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6390-catch-binding-predicate.ts.json
new file mode 100644
index 000000000..fcfb2852f
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6390-catch-binding-predicate.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6390-catch-binding-predicate.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6391-nullish-function-default.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6391-nullish-function-default.ts.json
new file mode 100644
index 000000000..8200d301e
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6391-nullish-function-default.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6391-nullish-function-default.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6392-optional-chain-protocol-calls.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6392-optional-chain-protocol-calls.ts.json
new file mode 100644
index 000000000..f2343714c
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6392-optional-chain-protocol-calls.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6392-optional-chain-protocol-calls.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6393-function-types-any-params.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6393-function-types-any-params.ts.json
new file mode 100644
index 000000000..af0a9abe7
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6393-function-types-any-params.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6393-function-types-any-params.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6394-in-key-error-subclasses.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6394-in-key-error-subclasses.ts.json
new file mode 100644
index 000000000..9df1e1014
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6394-in-key-error-subclasses.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6394-in-key-error-subclasses.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6395-generic-object-entries.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6395-generic-object-entries.ts.json
new file mode 100644
index 000000000..879a09388
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6395-generic-object-entries.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6395-generic-object-entries.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6396-generic-any-instantiation.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6396-generic-any-instantiation.ts.json
new file mode 100644
index 000000000..da41452ad
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6396-generic-any-instantiation.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6396-generic-any-instantiation.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6397-event-loop-metrics.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6397-event-loop-metrics.ts.json
new file mode 100644
index 000000000..0a74534b0
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6397-event-loop-metrics.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6397-event-loop-metrics.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6398-conditional-fixed-arity.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6398-conditional-fixed-arity.ts.json
new file mode 100644
index 000000000..878f45b09
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6398-conditional-fixed-arity.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6398-conditional-fixed-arity.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6400-override-any-base.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6400-override-any-base.ts.json
new file mode 100644
index 000000000..a885a5175
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6400-override-any-base.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6400-override-any-base.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6401-override-param-variance.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6401-override-param-variance.ts.json
new file mode 100644
index 000000000..ee14b5af3
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6401-override-param-variance.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6401-override-param-variance.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6402-override-js-base/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6402-override-js-base/main.ts.json
new file mode 100644
index 000000000..3ac1bf1fe
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6402-override-js-base/main.ts.json
@@ -0,0 +1,7 @@
+{
+ "order": [
+ "/tests/corpus/6402-override-js-base/pool.js",
+ "/tests/corpus/6402-override-js-base/main.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6403-error-code-write.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6403-error-code-write.ts.json
new file mode 100644
index 000000000..b673f7920
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6403-error-code-write.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6403-error-code-write.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6404-error-in-and-assign.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6404-error-in-and-assign.ts.json
new file mode 100644
index 000000000..c880bc57b
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6404-error-in-and-assign.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6404-error-in-and-assign.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6405-wrapped-default-parameters.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6405-wrapped-default-parameters.ts.json
new file mode 100644
index 000000000..8be83f858
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6405-wrapped-default-parameters.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6405-wrapped-default-parameters.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6406-npm-instance-receiver/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6406-npm-instance-receiver/main.ts.json
new file mode 100644
index 000000000..e651808f7
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6406-npm-instance-receiver/main.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6406-npm-instance-receiver/main.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6409-catch-binding-shapes.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6409-catch-binding-shapes.ts.json
new file mode 100644
index 000000000..57e3aeaf1
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6409-catch-binding-shapes.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6409-catch-binding-shapes.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6410-json-mixed-scalar-tables/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6410-json-mixed-scalar-tables/main.ts.json
new file mode 100644
index 000000000..c7ec1ab56
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6410-json-mixed-scalar-tables/main.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6410-json-mixed-scalar-tables/main.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6410-wrapper-untyped-results.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6410-wrapper-untyped-results.ts.json
new file mode 100644
index 000000000..20c1be33a
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6410-wrapper-untyped-results.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6410-wrapper-untyped-results.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6411-prototype-reset-constructors/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6411-prototype-reset-constructors/main.ts.json
new file mode 100644
index 000000000..adfb3989d
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6411-prototype-reset-constructors/main.ts.json
@@ -0,0 +1,7 @@
+{
+ "order": [
+ "/tests/corpus/6411-prototype-reset-constructors/issues.js",
+ "/tests/corpus/6411-prototype-reset-constructors/main.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6411-union-array-tuple-destructuring.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6411-union-array-tuple-destructuring.ts.json
new file mode 100644
index 000000000..94d8b78e7
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6411-union-array-tuple-destructuring.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6411-union-array-tuple-destructuring.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6412-js-error-tostring-override/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6412-js-error-tostring-override/main.ts.json
new file mode 100644
index 000000000..145c02428
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6412-js-error-tostring-override/main.ts.json
@@ -0,0 +1,7 @@
+{
+ "order": [
+ "/tests/corpus/6412-js-error-tostring-override/errors.js",
+ "/tests/corpus/6412-js-error-tostring-override/main.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6413-override-js-async-base/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6413-override-js-async-base/main.ts.json
new file mode 100644
index 000000000..de5fe5afb
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6413-override-js-async-base/main.ts.json
@@ -0,0 +1,7 @@
+{
+ "order": [
+ "/tests/corpus/6413-override-js-async-base/lock.js",
+ "/tests/corpus/6413-override-js-async-base/main.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6414-catch-values-roundtrip.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6414-catch-values-roundtrip.ts.json
new file mode 100644
index 000000000..797bd8ca2
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6414-catch-values-roundtrip.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6414-catch-values-roundtrip.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6415-error-code-dynamic-writes.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6415-error-code-dynamic-writes.ts.json
new file mode 100644
index 000000000..6500a5b3b
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6415-error-code-dynamic-writes.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6415-error-code-dynamic-writes.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6453-constructor-function-emitter.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6453-constructor-function-emitter.cjs.json
new file mode 100644
index 000000000..ae5c0050e
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6453-constructor-function-emitter.cjs.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6453-constructor-function-emitter.cjs"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6454-constructor-function-stream-bases.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6454-constructor-function-stream-bases.cjs.json
new file mode 100644
index 000000000..bc48a7f35
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6454-constructor-function-stream-bases.cjs.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6454-constructor-function-stream-bases.cjs"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6463-node-readable-stream-producers.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6463-node-readable-stream-producers.ts.json
new file mode 100644
index 000000000..e413951ae
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6463-node-readable-stream-producers.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6463-node-readable-stream-producers.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6464-node-writable-stream-producers.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6464-node-writable-stream-producers.ts.json
new file mode 100644
index 000000000..fd805a2af
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6464-node-writable-stream-producers.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6464-node-writable-stream-producers.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6465-node-stream-js-boundary/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6465-node-stream-js-boundary/main.ts.json
new file mode 100644
index 000000000..95278eab8
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6465-node-stream-js-boundary/main.ts.json
@@ -0,0 +1,7 @@
+{
+ "order": [
+ "/tests/corpus/6465-node-stream-js-boundary/consume.js",
+ "/tests/corpus/6465-node-stream-js-boundary/main.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6465-optional-members-over-index-signature.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6465-optional-members-over-index-signature.ts.json
new file mode 100644
index 000000000..91a157207
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6465-optional-members-over-index-signature.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6465-optional-members-over-index-signature.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6466-never-assertion-argument.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6466-never-assertion-argument.ts.json
new file mode 100644
index 000000000..17f25aa22
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6466-never-assertion-argument.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6466-never-assertion-argument.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6466-node-stdio-stream-surface.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6466-node-stdio-stream-surface.ts.json
new file mode 100644
index 000000000..8ebcddd22
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6466-node-stdio-stream-surface.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6466-node-stdio-stream-surface.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6467-fromentries-record-assertion.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6467-fromentries-record-assertion.ts.json
new file mode 100644
index 000000000..21ca66953
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6467-fromentries-record-assertion.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6467-fromentries-record-assertion.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6467-node-stream-untyped-sources/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6467-node-stream-untyped-sources/main.ts.json
new file mode 100644
index 000000000..dc843c047
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6467-node-stream-untyped-sources/main.ts.json
@@ -0,0 +1,7 @@
+{
+ "order": [
+ "/tests/corpus/6467-node-stream-untyped-sources/sources.js",
+ "/tests/corpus/6467-node-stream-untyped-sources/main.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6468-generic-unknown-record-entries.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6468-generic-unknown-record-entries.ts.json
new file mode 100644
index 000000000..0b4118c70
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6468-generic-unknown-record-entries.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6468-generic-unknown-record-entries.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6468-node-stdio-writable-slot.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6468-node-stdio-writable-slot.ts.json
new file mode 100644
index 000000000..baf428dee
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6468-node-stdio-writable-slot.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6468-node-stdio-writable-slot.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6469-node-stdio-writable-js-pipe/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6469-node-stdio-writable-js-pipe/main.ts.json
new file mode 100644
index 000000000..60b1360b0
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6469-node-stdio-writable-js-pipe/main.ts.json
@@ -0,0 +1,7 @@
+{
+ "order": [
+ "/tests/corpus/6469-node-stdio-writable-js-pipe/pipe.js",
+ "/tests/corpus/6469-node-stdio-writable-js-pipe/main.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6470-stream-transform-any-data-callback.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6470-stream-transform-any-data-callback.ts.json
new file mode 100644
index 000000000..3d4a39486
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6470-stream-transform-any-data-callback.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6470-stream-transform-any-data-callback.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6471-stream-data-buffer-or-string.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6471-stream-data-buffer-or-string.ts.json
new file mode 100644
index 000000000..e8ff1e084
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6471-stream-data-buffer-or-string.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6471-stream-data-buffer-or-string.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6472-child-data-unknown-chunk.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6472-child-data-unknown-chunk.ts.json
new file mode 100644
index 000000000..3c7183bb6
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6472-child-data-unknown-chunk.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6472-child-data-unknown-chunk.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6490-literal-key-field-updates.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6490-literal-key-field-updates.ts.json
new file mode 100644
index 000000000..1e4e1e939
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6490-literal-key-field-updates.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6490-literal-key-field-updates.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6491-index-record-updates.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6491-index-record-updates.ts.json
new file mode 100644
index 000000000..babda8c3c
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6491-index-record-updates.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6491-index-record-updates.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/webassembly-api.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/webassembly-api.mjs.json
new file mode 100644
index 000000000..f3e1a711b
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/webassembly-api.mjs.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/webassembly-api.mjs"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/webassembly-dylink/main.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/webassembly-dylink/main.mjs.json
new file mode 100644
index 000000000..14b064864
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/webassembly-dylink/main.mjs.json
@@ -0,0 +1,7 @@
+{
+ "order": [
+ "/tests/corpus/webassembly-dylink/extensions.js",
+ "/tests/corpus/webassembly-dylink/main.mjs"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/webassembly-externref-lifetimes.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/webassembly-externref-lifetimes.mjs.json
new file mode 100644
index 000000000..7d264b2eb
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/webassembly-externref-lifetimes.mjs.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/webassembly-externref-lifetimes.mjs"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/webassembly-jsapi.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/webassembly-jsapi.mjs.json
new file mode 100644
index 000000000..4f8ee17cf
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/webassembly-jsapi.mjs.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/webassembly-jsapi.mjs"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/webassembly-lifetimes.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/webassembly-lifetimes.mjs.json
new file mode 100644
index 000000000..c9ab8d500
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/webassembly-lifetimes.mjs.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/webassembly-lifetimes.mjs"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/webassembly-spec-control/main.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/webassembly-spec-control/main.mjs.json
new file mode 100644
index 000000000..58a263265
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/webassembly-spec-control/main.mjs.json
@@ -0,0 +1,8 @@
+{
+ "order": [
+ "/tests/wasm-spec/runner.mjs",
+ "/tests/corpus/webassembly-spec-control/data.mjs",
+ "/tests/corpus/webassembly-spec-control/main.mjs"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/webassembly-spec-float/main.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/webassembly-spec-float/main.mjs.json
new file mode 100644
index 000000000..e871d3b1d
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/webassembly-spec-float/main.mjs.json
@@ -0,0 +1,8 @@
+{
+ "order": [
+ "/tests/wasm-spec/runner.mjs",
+ "/tests/corpus/webassembly-spec-float/data.mjs",
+ "/tests/corpus/webassembly-spec-float/main.mjs"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/webassembly-spec-imports/main.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/webassembly-spec-imports/main.mjs.json
new file mode 100644
index 000000000..a23646c06
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/webassembly-spec-imports/main.mjs.json
@@ -0,0 +1,8 @@
+{
+ "order": [
+ "/tests/wasm-spec/runner.mjs",
+ "/tests/corpus/webassembly-spec-imports/data.mjs",
+ "/tests/corpus/webassembly-spec-imports/main.mjs"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/webassembly-spec-int/main.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/webassembly-spec-int/main.mjs.json
new file mode 100644
index 000000000..d19226a63
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/webassembly-spec-int/main.mjs.json
@@ -0,0 +1,8 @@
+{
+ "order": [
+ "/tests/wasm-spec/runner.mjs",
+ "/tests/corpus/webassembly-spec-int/data.mjs",
+ "/tests/corpus/webassembly-spec-int/main.mjs"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/webassembly-spec-memory/main.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/webassembly-spec-memory/main.mjs.json
new file mode 100644
index 000000000..4510ade3a
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/webassembly-spec-memory/main.mjs.json
@@ -0,0 +1,8 @@
+{
+ "order": [
+ "/tests/wasm-spec/runner.mjs",
+ "/tests/corpus/webassembly-spec-memory/data.mjs",
+ "/tests/corpus/webassembly-spec-memory/main.mjs"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/webassembly-spec-table/main.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/webassembly-spec-table/main.mjs.json
new file mode 100644
index 000000000..50409b1d0
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/webassembly-spec-table/main.mjs.json
@@ -0,0 +1,8 @@
+{
+ "order": [
+ "/tests/wasm-spec/runner.mjs",
+ "/tests/corpus/webassembly-spec-table/data.mjs",
+ "/tests/corpus/webassembly-spec-table/main.mjs"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/webassembly-typed.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/webassembly-typed.ts.json
new file mode 100644
index 000000000..7a3adb475
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/webassembly-typed.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/webassembly-typed.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/webassembly-worker.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/webassembly-worker.ts.json
new file mode 100644
index 000000000..2b4ffd4ba
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/webassembly-worker.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/webassembly-worker.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/webassembly-wpt-jsapi.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/webassembly-wpt-jsapi.mjs.json
new file mode 100644
index 000000000..dd80857d8
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/webassembly-wpt-jsapi.mjs.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/webassembly-wpt-jsapi.mjs"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/fromentries-aliased-entries.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/fromentries-aliased-entries.ts.json
new file mode 100644
index 000000000..7388f2b71
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/fromentries-aliased-entries.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/diagnostics/fromentries-aliased-entries.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/util-inherits-runtime-class.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/util-inherits-runtime-class.ts.json
deleted file mode 100644
index 5dfbdc324..000000000
--- a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/util-inherits-runtime-class.ts.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "order": [
- "/tests/diagnostics/util-inherits-runtime-class.ts"
- ],
- "diags": []
-}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/variadic-wrapper-fences.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/variadic-wrapper-fences.ts.json
new file mode 100644
index 000000000..c3e44b2e5
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/variadic-wrapper-fences.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/diagnostics/variadic-wrapper-fences.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/child-stdin-fences.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/child-stdin-fences.ts.json
deleted file mode 100644
index cfa602a2e..000000000
--- a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/child-stdin-fences.ts.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "order": [
- "/tests/fixtures/node-types/child-stdin-fences.ts"
- ],
- "diags": []
-}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/child-stdin-streams.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/child-stdin-streams.ts.json
new file mode 100644
index 000000000..2bb443d2a
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/child-stdin-streams.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/fixtures/node-types/child-stdin-streams.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/event-loop-metrics.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/event-loop-metrics.ts.json
new file mode 100644
index 000000000..66246403d
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/event-loop-metrics.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/fixtures/node-types/event-loop-metrics.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/node-stream-interfaces.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/node-stream-interfaces.ts.json
new file mode 100644
index 000000000..cfb5c1a10
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/node-stream-interfaces.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/fixtures/node-types/node-stream-interfaces.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/stream-construction.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/stream-construction.ts.json
new file mode 100644
index 000000000..35dc0be0d
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/stream-construction.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/fixtures/node-types/stream-construction.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/timeout-type.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/timeout-type.ts.json
new file mode 100644
index 000000000..46f0b6e38
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/timeout-type.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/fixtures/node-types/timeout-type.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/runtime-pack-common/runtime-pack-matrix.mjs b/packages/runtime-pack-common/runtime-pack-matrix.mjs
index 36ffaaf0f..28ccf0f59 100644
--- a/packages/runtime-pack-common/runtime-pack-matrix.mjs
+++ b/packages/runtime-pack-common/runtime-pack-matrix.mjs
@@ -69,6 +69,8 @@ export const OPTIONAL_RUNTIME_SOURCES = [
["scr_async_dyn.c", any("dynAsync", "dynInvoke", "dc", "fileHandle", "nativeFetch")],
// The performance timeline emits its buffer warning through scr_async_dyn.c.
["scr_perf.c", "dynAsync"],
+ // perf_hooks event-loop metrics, only for programs that read them.
+ ["scr_perf_loop.c", "perfLoop"],
// Blob and File box their reads as dyn promises through scr_async_dyn.c.
["scr_blob.c", "dynAsync"],
["scr_zlib.c", "zlib"],
@@ -88,6 +90,10 @@ export const OPTIONAL_RUNTIME_SOURCES = [
["scr_util_compare.c", all("parseArgs", "symbol")],
["scr_util_style.c", all("parseArgs", "inspect", "dynAsync")],
["scr_util_callable.c", all("parseArgs", "inspect", "dynAsync", "dynInvoke", "regex")],
+ // Untyped EventEmitter and stream constructor values call listeners
+ // through the dyn-invoke unit, inspect unhandled 'error' payloads and
+ // create EventEmitter's private symbols.
+ ["scr_dyn_builtin_class.c", all("dynBuiltinClass", "inspect", "dynInvoke", "symbol", "emitter")],
["scr_stream.c", "stream"],
// The zlib stream classes reference both the zlib engine and the stream unit.
["scr_zlib_stream.c", all("zlib", "stream")],
@@ -112,6 +118,7 @@ export const OPTIONAL_RUNTIME_SOURCES = [
["scr_tls_ca.c", "tlsCaEffective"],
["scr_tls.c", "tlsEffective"],
["scr_cipher.c", "cipher"],
+ ["scr_wasm.c", "wasm"],
["scr_webcrypto.c", "webcrypto"],
["scr_fetch.c", "nativeFetch"],
];
diff --git a/packages/runtime-wasm32-wasi/scripts/build.mjs b/packages/runtime-wasm32-wasi/scripts/build.mjs
index a5a30dae0..64d28f15f 100644
--- a/packages/runtime-wasm32-wasi/scripts/build.mjs
+++ b/packages/runtime-wasm32-wasi/scripts/build.mjs
@@ -21,6 +21,7 @@ process.env.SCRIPTC_RUNTIME_PACK_CONFIG = JSON.stringify({
"scr_tls_ca.c",
"scr_tls.c",
"scr_cipher.c",
+ "scr_wasm.c",
"scr_fetch.c",
],
omitArchives: ["mbedtls"],
diff --git a/packages/runtime/src/scr_async.c b/packages/runtime/src/scr_async.c
index 8185422af..ef91de6fb 100644
--- a/packages/runtime/src/scr_async.c
+++ b/packages/runtime/src/scr_async.c
@@ -70,14 +70,6 @@ void __lsan_unregister_root_region(const void *p, size_t size) __attribute__((we
#endif
#endif
-#ifdef SCR_ASAN_FIBERS
-#define SCR_FIBER_STACK (8 * 1024 * 1024)
-#else
-/* Deep enough for ordinary recursion inside async bodies and generators
- * once the overflow guard's reserve is taken off; idle and shallow fibers
- * only touch the pages they use. */
-#define SCR_FIBER_STACK (512 * 1024)
-#endif
#if !defined(_WIN32) && !defined(__wasi__)
/* Keep stacks out of malloc's size classes. Reuse a bounded number during
diff --git a/packages/runtime/src/scr_async.h b/packages/runtime/src/scr_async.h
index 6f56758ce..041d2ea03 100644
--- a/packages/runtime/src/scr_async.h
+++ b/packages/runtime/src/scr_async.h
@@ -29,6 +29,20 @@ void *scr_promise_retain_v(void *p);
void scr_promise_release_v(void *p);
typedef struct ScrFiber ScrFiber;
+/* Bytes of each async fiber's native stack (scr_async.c), mapped without a
+ * guard page: compiled code checks scr_stack_guard instead, which sits
+ * scr_stack_guard_for's reserve above the mapping's start. Deep enough for
+ * ordinary recursion inside async bodies and generators once that reserve
+ * is taken off; idle and shallow fibers only touch the pages they use.
+ * AddressSanitizer's larger frames get more. */
+#if defined(__has_feature)
+#if __has_feature(address_sanitizer)
+#define SCR_FIBER_STACK (8 * 1024 * 1024)
+#endif
+#endif
+#ifndef SCR_FIBER_STACK
+#define SCR_FIBER_STACK (512 * 1024)
+#endif
/* Spawn + run eagerly to the first suspension; returns the promise, +1. */
ScrPromise *scr_async_spawn(void (*entry)(ScrFiber *, void *), void *argpack);
/* `await f(...)` of a fiber-backed async f: spawn and await in one call,
diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h
index fb1bc1548..28d642501 100644
--- a/packages/runtime/src/scr_dyn.h
+++ b/packages/runtime/src/scr_dyn.h
@@ -191,6 +191,11 @@ typedef enum {
X(SHARED_ARRAY_BUFFER, 37) \
X(STDIO, 27) /* stable process stdin/stdout/stderr values */ \
X(URL, 28) /* native WHATWG URL */ \
+ X(WASM_GLOBAL, 61) /* WebAssembly.Global */ \
+ X(WASM_INSTANCE, 58) /* WebAssembly.Instance */ \
+ X(WASM_MEMORY, 59) /* WebAssembly.Memory */ \
+ X(WASM_MODULE, 57) /* WebAssembly.Module (scr_wasm.c) */ \
+ X(WASM_TABLE, 60) /* WebAssembly.Table */ \
X(WEAK_MAP, 23) /* native weak-key metadata */ \
X(WEAK_SET, 24) /* native weak membership */ \
X(WEB_CONTROLLER, 11) /* native ReadableStreamDefaultController */ \
@@ -1148,6 +1153,12 @@ ScrDyn *scr_dyn_define_prototype_props(ScrDyn *target, ScrDyn *descs, ScrDyn *pr
* must be static literals (the box never frees them; name may be NULL). */
ScrDyn *scr_dyn_new_func(ScrClosure *clo, ScrDynThunk thunk, uint32_t arity, const char *sig, const char *name);
ScrDyn *scr_dyn_new_class(ScrClassObj *cls, const char *type_key);
+/* EventEmitter and the stream constructors as untyped constructor values,
+ * and instanceof over untyped constructor functions
+ * (scr_dyn_builtin_class.c, linked with the dynBuiltinClass feature). */
+ScrDyn *scr_dyn_builtin_class(double kind);
+bool scr_dyn_builtin_instanceof(ScrDyn *value, double kind);
+bool scr_dyn_function_instanceof(ScrDyn *value, ScrDyn *callee);
bool scr_dyn_class_is_key(const ScrDyn *value, const ScrStr *type_key);
bool scr_dyn_class_is(const ScrDyn *value, const char *type_key);
/* Calls a dyn value: a non-function kind throws the catchable TypeError
@@ -1281,7 +1292,8 @@ typedef struct ScrDynHandleOps {
* inspector returns `this`) instead of the bare class name. */
bool inspect_bracketed_past_depth;
/* The @@toStringTag when it differs from cls: String() answers
- * "[object ]" and util.inspect prints ` [] `. */
+ * "[object ]" and util.inspect prints ` [] `, or
+ * ` ` when the tag ends with "." and cls, as Node does. */
const char *to_string_tag;
/* The view has no properties, so util.inspect prints it in full at any
* depth, as Node prints `{}`. */
diff --git a/packages/runtime/src/scr_dyn_builtin_class.c b/packages/runtime/src/scr_dyn_builtin_class.c
new file mode 100644
index 000000000..6d894615b
--- /dev/null
+++ b/packages/runtime/src/scr_dyn_builtin_class.c
@@ -0,0 +1,1510 @@
+/* EventEmitter and the node:stream constructors as checked-dynamic
+ * constructor values. JavaScript constructor functions inherit from them
+ * the pre-class way:
+ *
+ * function Bus() { EventEmitter.call(this); }
+ * util.inherits(Bus, EventEmitter);
+ *
+ * A compiled EventEmitter instance keeps its listeners in native state, but
+ * the receiver here is an ordinary dyn object built by `new Bus()`. So the
+ * EventEmitter value is a native function whose prototype carries Node's
+ * lib/events.js methods over that object's own `_events`, `_eventsCount`
+ * and `_maxListeners` properties, the state Node's own implementation
+ * keeps there. Calling it with a receiver runs EventEmitter.init.
+ *
+ * Stream is Node's legacy constructor: EventEmitter.call(this, opts) and a
+ * prototype that inherits EventEmitter's. Readable, Writable, Duplex,
+ * Transform and PassThrough have prototypes chained as in Node, so
+ * util.inherits onto them and instanceof work, but their native state has
+ * no dyn object form: calling one with a receiver throws the "not
+ * supported yet" Error where the construction runs.
+ *
+ * One constructor per kind and thread, so every boxing of the same class
+ * value is the same function. All arguments are borrowed. */
+#include "scr_runtime.h"
+
+#include
+#include
+#include
+
+#ifdef _WIN32
+#include
+#define BC_PID() ((long)_getpid())
+#else
+#include
+#define BC_PID() ((long)getpid())
+#endif
+
+enum {
+ BC_EMITTER,
+ BC_STREAM,
+ BC_READABLE,
+ BC_WRITABLE,
+ BC_DUPLEX,
+ BC_TRANSFORM,
+ BC_PASSTHROUGH,
+ BC_COUNT
+};
+
+static const char *const bc_names[BC_COUNT] = {
+ "EventEmitter", "Stream", "Readable", "Writable", "Duplex", "Transform", "PassThrough",
+};
+
+static SCR_TL ScrDyn *bc_ctors[BC_COUNT];
+static SCR_TL bool bc_teardown_registered;
+/* Node's private kShapeMode and kCapture symbols, which util.inspect shows
+ * on every emitter as Symbol(shapeMode) and Symbol(kCapture). */
+enum { BC_SHAPE_MODE, BC_CAPTURE, BC_SYMBOLS };
+static SCR_TL ScrDyn *bc_symbols[BC_SYMBOLS];
+
+static ScrDyn *bc_class(int kind);
+
+/* ── property helpers ───────────────────────────────────────────────── */
+
+static ScrDyn *bc_key(const char *name) {
+ ScrStr *text = scr_str_new(name, strlen(name));
+ ScrDyn *key = scr_dyn_new_str(text);
+ scr_str_release(text);
+ return key;
+}
+
+/* object[key] with object as the receiver: +1, or NULL with an exception
+ * pending. */
+static ScrDyn *bc_get_key(ScrDyn *object, ScrDyn *key) {
+ ScrDyn *value = scr_dyn_reflect_get(object, key, object);
+ if (!value && !scr_exc_pending()) value = scr_dyn_retain(scr_dyn_undefined());
+ return value;
+}
+
+static ScrDyn *bc_get(ScrDyn *object, const char *name) {
+ ScrDyn *key = bc_key(name);
+ ScrDyn *value = bc_get_key(object, key);
+ scr_dyn_release(key);
+ return value;
+}
+
+/* object[key] = value; the value moves in. False with an exception
+ * pending. */
+static bool bc_set_key(ScrDyn *object, ScrDyn *key, ScrDyn *value) {
+ scr_dyn_key_set_computed(object, key, value);
+ scr_dyn_release(value);
+ return !scr_exc_pending();
+}
+
+static bool bc_set(ScrDyn *object, const char *name, ScrDyn *value) {
+ ScrDyn *key = bc_key(name);
+ bool ok = bc_set_key(object, key, value);
+ scr_dyn_release(key);
+ return ok;
+}
+
+static bool bc_undefined(const ScrDyn *value) {
+ return value->kind == SCR_DYN_UNDEF;
+}
+
+static double bc_number(const ScrDyn *value) {
+ return value->kind == SCR_DYN_NUM ? value->v.num : 0;
+}
+
+/* receiver.method(...args), dispatched through the receiver's own lookup
+ * like Node's `this.emit(...)`. +1 or NULL. */
+static ScrDyn *bc_invoke(ScrDyn *receiver, const char *method, ScrDyn *const *args, size_t argc) {
+ ScrDyn *fn = bc_get(receiver, method);
+ if (!fn) return NULL;
+ scr_dyn_this_push_dyn(receiver);
+ ScrDyn *result = scr_dyn_call(fn, args, argc, method);
+ scr_dyn_this_pop();
+ scr_dyn_release(fn);
+ return result;
+}
+
+static ScrDyn *bc_apply(ScrDyn *fn, ScrDyn *receiver, ScrDyn *const *args, size_t argc) {
+ scr_dyn_this_push_dyn(receiver);
+ ScrDyn *result = scr_dyn_call(fn, args, argc, "listener");
+ scr_dyn_this_pop();
+ return result;
+}
+
+/* Listener lists are replaced rather than mutated: a fresh array with item
+ * first, or without the item at index. Node mutates its array in place,
+ * which nothing outside the emitter observes (emit iterates a copy and
+ * listeners() answers one). A leak warning already given stays given. */
+static ScrDyn *bc_list_copy(ScrDyn *list, ScrDyn *first, double skip) {
+ ScrDyn *copy = scr_dyn_new_arr();
+ if (first) scr_dyn_arr_push(copy, scr_dyn_retain(first));
+ double length = scr_dyn_arr_len(list);
+ for (double i = 0; i < length; i++) {
+ if (i == skip) continue;
+ ScrDyn *item = scr_dyn_index_get(list, i);
+ if (!item) {
+ scr_dyn_release(copy);
+ return NULL;
+ }
+ scr_dyn_arr_push(copy, item);
+ }
+ ScrDyn *warned = bc_get(list, "warned");
+ if (!warned || (scr_dyn_truthy(warned) && !bc_set(copy, "warned", scr_dyn_new_bool(true)))) {
+ if (warned) scr_dyn_release(warned);
+ scr_dyn_release(copy);
+ return NULL;
+ }
+ scr_dyn_release(warned);
+ return copy;
+}
+
+static ScrDyn *bc_unused_args[1];
+
+static ScrDyn *bc_arg(ScrDyn *const *args, size_t argc, size_t index) {
+ return index < argc ? args[index] : scr_dyn_undefined();
+}
+
+/* The receiver of a prototype method call, +1. Methods called without one
+ * (an extracted `emitter.on`) read `this` as undefined, as in strict code. */
+static ScrDyn *bc_this(void) {
+ return scr_dyn_this_get();
+}
+
+static bool bc_require_object(ScrDyn *self, const char *reading) {
+ if (self->kind != SCR_DYN_UNDEF && self->kind != SCR_DYN_NULL) return true;
+ char message[96];
+ snprintf(message, sizeof message, "Cannot read properties of %s (reading '%s')",
+ self->kind == SCR_DYN_NULL ? "null" : "undefined", reading);
+ scr_throw_error_msg(SCR_ERR_TYPE, message, strlen(message));
+ return false;
+}
+
+static bool bc_check_listener(const ScrDyn *listener) {
+ if (scr_dyn_is_callable(listener)) return true;
+ scr_dyn_arg_type_fail("listener", "of type function", listener);
+ return false;
+}
+
+static void bc_teardown(void);
+
+static ScrDyn *bc_symbol(int which) {
+ if (!bc_symbols[which]) {
+ if (!bc_teardown_registered) {
+ bc_teardown_registered = true;
+ scr_atexit(bc_teardown);
+ }
+ const char *name = which == BC_SHAPE_MODE ? "shapeMode" : "kCapture";
+ ScrStr *description = scr_str_new(name, strlen(name));
+ ScrSym *symbol = scr_sym_new(description);
+ scr_str_release(description);
+ bc_symbols[which] = scr_dyn_new_symbol(symbol);
+ scr_sym_release(symbol);
+ }
+ return bc_symbols[which];
+}
+
+/* ── EventEmitter.init ──────────────────────────────────────────────── */
+
+/* `_events` is fresh unless the receiver already has its own (Node's
+ * shapeMode, a subclass that pre-populates it); `_maxListeners` keeps a
+ * value set before the call. */
+static bool bc_emitter_init(ScrDyn *self, ScrDyn *opts) {
+ if (!bc_require_object(self, "_events")) return false;
+ ScrDyn *events = bc_get(self, "_events");
+ if (!events) return false;
+ bool fresh = bc_undefined(events);
+ if (!fresh) {
+ ScrDyn *proto = scr_dyn_get_prototype(self);
+ if (!proto) {
+ scr_dyn_release(events);
+ return false;
+ }
+ if (proto->kind != SCR_DYN_NULL) {
+ ScrDyn *inherited = bc_get(proto, "_events");
+ if (!inherited) {
+ scr_dyn_release(proto);
+ scr_dyn_release(events);
+ return false;
+ }
+ fresh = scr_dyn_strict_eq(events, inherited);
+ scr_dyn_release(inherited);
+ }
+ scr_dyn_release(proto);
+ }
+ scr_dyn_release(events);
+ if (fresh) {
+ if (!bc_set(self, "_events", scr_dyn_new_obj_null_proto())) return false;
+ if (!bc_set(self, "_eventsCount", scr_dyn_new_num(0))) return false;
+ }
+ if (!bc_set_key(self, bc_symbol(BC_SHAPE_MODE), scr_dyn_new_bool(!fresh))) return false;
+ ScrDyn *max = bc_get(self, "_maxListeners");
+ if (!max) return false;
+ bool keep = scr_dyn_truthy(max);
+ if (!bc_set(self, "_maxListeners", keep ? max : (scr_dyn_release(max), scr_dyn_retain(scr_dyn_undefined()))))
+ return false;
+ if (opts->kind != SCR_DYN_UNDEF && opts->kind != SCR_DYN_NULL) {
+ ScrDyn *capture = bc_get(opts, "captureRejections");
+ if (!capture) return false;
+ bool requested = !bc_undefined(capture);
+ if (requested && capture->kind != SCR_DYN_BOOL) {
+ scr_dyn_arg_type_fail("options.captureRejections", "of type boolean", capture);
+ scr_dyn_release(capture);
+ return false;
+ }
+ bool enabled = requested && capture->v.b;
+ scr_dyn_release(capture);
+ if (enabled) {
+ static const char message[] = "EventEmitter captureRejections on a constructor function is not supported yet";
+ scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020");
+ return false;
+ }
+ }
+ return bc_set_key(self, bc_symbol(BC_CAPTURE), scr_dyn_new_bool(false));
+}
+
+/* ── listener bookkeeping (lib/events.js) ───────────────────────────── */
+
+static double bc_max_listeners(ScrDyn *self) {
+ ScrDyn *max = bc_get(self, "_maxListeners");
+ if (!max) return -1;
+ double value;
+ if (bc_undefined(max)) {
+ value = scr_emitter_get_default_max();
+ } else {
+ value = bc_number(max);
+ }
+ scr_dyn_release(max);
+ return value;
+}
+
+static void bc_warn_tick(ScrClosure *self) {
+ ScrStr *message = (ScrStr *)scr_box_get_ref(self->caps[0]);
+ if (scr_warning_emit_hook) {
+ scr_warning_emit_hook("MaxListenersExceededWarning", NULL, message);
+ } else {
+ fflush(stdout);
+ fprintf(stderr, "(node:%ld) MaxListenersExceededWarning: %.*s\n", BC_PID(),
+ (int)message->len, message->data);
+ if (!scr_warning_trace_hint_shown) {
+ scr_warning_trace_hint_shown = true;
+ fputs("(Use `node --trace-warnings ...` to show where the warning was created)\n", stderr);
+ }
+ }
+ scr_str_release(message);
+}
+
+/* The constructor name util.inspect prints at depth -1: `[Bus]`. */
+static ScrStr *bc_constructor_name(ScrDyn *self) {
+ ScrDyn *ctor = bc_get(self, "constructor");
+ ScrStr *name = NULL;
+ if (ctor && scr_dyn_is_callable(ctor)) {
+ ScrDyn *text = bc_get(ctor, "name");
+ if (text && text->kind == SCR_DYN_STR && text->v.str->len) name = scr_str_retain(text->v.str);
+ if (text) scr_dyn_release(text);
+ }
+ if (ctor) scr_dyn_release(ctor);
+ if (scr_exc_pending()) scr_exc_clear();
+ return name ? name : scr_str_new("Object", 6);
+}
+
+static void bc_warn_leak(ScrDyn *self, ScrDyn *type, double count, double max) {
+ char counted[32], limit[32];
+ snprintf(counted, sizeof counted, "%g", count);
+ snprintf(limit, sizeof limit, "%g", max);
+ ScrStr *event = scr_dyn_string_coerce_js(type);
+ if (!event) {
+ scr_exc_clear();
+ event = scr_str_new("", 0);
+ }
+ ScrStr *cls = bc_constructor_name(self);
+ ScrJsonBuf text;
+ scr_jb_init(&text);
+ scr_jb_puts(&text, "Possible EventEmitter memory leak detected. ");
+ scr_jb_puts(&text, counted);
+ scr_jb_putc(&text, ' ');
+ scr_jb_put_str(&text, event);
+ scr_jb_puts(&text, " listeners added to [");
+ scr_jb_put_str(&text, cls);
+ scr_jb_puts(&text, "]. MaxListeners is ");
+ scr_jb_puts(&text, limit);
+ scr_jb_puts(&text, ". Use emitter.setMaxListeners() to increase limit");
+ scr_str_release(event);
+ scr_str_release(cls);
+ ScrClosure *tick = scr_closure_new((void *)&bc_warn_tick, 1);
+ ScrBox *box = scr_box_new(SCR_BOX_STR);
+ scr_box_set_ref(box, scr_jb_finish(&text));
+ tick->caps[0] = box;
+ scr_next_tick(tick);
+}
+
+/* _addListener(target, type, listener, prepend): +1 target or NULL. */
+static ScrDyn *bc_add_listener(ScrDyn *self, ScrDyn *type, ScrDyn *listener, bool prepend) {
+ if (!bc_check_listener(listener)) return NULL;
+ ScrDyn *events = bc_get(self, "_events");
+ if (!events) return NULL;
+ ScrDyn *existing = NULL;
+ if (bc_undefined(events)) {
+ scr_dyn_release(events);
+ events = scr_dyn_new_obj_null_proto();
+ if (!bc_set(self, "_events", scr_dyn_retain(events)) || !bc_set(self, "_eventsCount", scr_dyn_new_num(0)))
+ goto fail;
+ } else {
+ ScrDyn *watcher = bc_get(events, "newListener");
+ if (!watcher) goto fail;
+ bool watched = !bc_undefined(watcher);
+ scr_dyn_release(watcher);
+ if (watched) {
+ ScrDyn *inner = bc_get(listener, "listener");
+ if (!inner) goto fail;
+ ScrDyn *name = bc_key("newListener");
+ ScrDyn *args[3] = { name, type, bc_undefined(inner) || inner->kind == SCR_DYN_NULL ? listener : inner };
+ ScrDyn *emitted = bc_invoke(self, "emit", args, 3);
+ scr_dyn_release(name);
+ scr_dyn_release(inner);
+ if (!emitted) goto fail;
+ scr_dyn_release(emitted);
+ /* the listener may have replaced _events */
+ scr_dyn_release(events);
+ events = bc_get(self, "_events");
+ if (!events) return NULL;
+ }
+ existing = bc_get_key(events, type);
+ if (!existing) goto fail;
+ }
+ if (!existing || bc_undefined(existing)) {
+ if (!bc_set_key(events, type, scr_dyn_retain(listener))) goto fail;
+ ScrDyn *count = bc_get(self, "_eventsCount");
+ if (!count) goto fail;
+ double next = bc_number(count) + 1;
+ scr_dyn_release(count);
+ if (!bc_set(self, "_eventsCount", scr_dyn_new_num(next))) goto fail;
+ } else {
+ if (scr_dyn_is_callable(existing)) {
+ ScrDyn *list = scr_dyn_new_arr();
+ scr_dyn_arr_push(list, scr_dyn_retain(prepend ? listener : existing));
+ scr_dyn_arr_push(list, scr_dyn_retain(prepend ? existing : listener));
+ if (!bc_set_key(events, type, scr_dyn_retain(list))) {
+ scr_dyn_release(list);
+ goto fail;
+ }
+ scr_dyn_release(existing);
+ existing = list;
+ } else if (prepend) {
+ ScrDyn *list = bc_list_copy(existing, listener, -1);
+ if (!list) goto fail;
+ if (!bc_set_key(events, type, scr_dyn_retain(list))) {
+ scr_dyn_release(list);
+ goto fail;
+ }
+ scr_dyn_release(existing);
+ existing = list;
+ } else if (existing->kind == SCR_DYN_ARR) {
+ scr_dyn_arr_push(existing, scr_dyn_retain(listener));
+ }
+ double max = bc_max_listeners(self);
+ if (scr_exc_pending()) goto fail;
+ double length = existing->kind == SCR_DYN_ARR ? scr_dyn_arr_len(existing) : 0;
+ if (max > 0 && length > max) {
+ ScrDyn *warned = bc_get(existing, "warned");
+ if (!warned) goto fail;
+ bool already = scr_dyn_truthy(warned);
+ scr_dyn_release(warned);
+ if (!already) {
+ if (!bc_set(existing, "warned", scr_dyn_new_bool(true))) goto fail;
+ bc_warn_leak(self, type, length, max);
+ }
+ }
+ }
+ if (existing) scr_dyn_release(existing);
+ scr_dyn_release(events);
+ return scr_dyn_retain(self);
+fail:
+ if (existing) scr_dyn_release(existing);
+ scr_dyn_release(events);
+ return NULL;
+}
+
+/* removeListener(type, listener): +1 self or NULL. */
+static ScrDyn *bc_remove_listener(ScrDyn *self, ScrDyn *type, ScrDyn *listener) {
+ if (!bc_check_listener(listener)) return NULL;
+ ScrDyn *events = bc_get(self, "_events");
+ if (!events) return NULL;
+ if (bc_undefined(events)) {
+ scr_dyn_release(events);
+ return scr_dyn_retain(self);
+ }
+ ScrDyn *list = bc_get_key(events, type);
+ if (!list) goto fail_events;
+ if (bc_undefined(list)) {
+ scr_dyn_release(list);
+ scr_dyn_release(events);
+ return scr_dyn_retain(self);
+ }
+ ScrDyn *wrapped = scr_dyn_is_callable(list) ? bc_get(list, "listener") : NULL;
+ if (scr_exc_pending()) goto fail_list;
+ bool match = scr_dyn_is_callable(list) &&
+ (scr_dyn_strict_eq(list, listener) || (wrapped && scr_dyn_strict_eq(wrapped, listener)));
+ if (match) {
+ ScrDyn *count = bc_get(self, "_eventsCount");
+ if (!count) goto fail_wrapped;
+ double left = bc_number(count) - 1;
+ scr_dyn_release(count);
+ if (!bc_set(self, "_eventsCount", scr_dyn_new_num(left))) goto fail_wrapped;
+ ScrDyn *shaped = bc_get_key(self, bc_symbol(BC_SHAPE_MODE));
+ if (!shaped) goto fail_wrapped;
+ bool shape_mode = scr_dyn_truthy(shaped);
+ scr_dyn_release(shaped);
+ if (shape_mode) {
+ if (!bc_set_key(events, type, scr_dyn_retain(scr_dyn_undefined()))) goto fail_wrapped;
+ } else if (left == 0) {
+ if (!bc_set(self, "_events", scr_dyn_new_obj_null_proto())) goto fail_wrapped;
+ } else {
+ scr_dyn_key_delete_computed(events, type, false);
+ if (scr_exc_pending()) goto fail_wrapped;
+ ScrDyn *watcher = bc_get(events, "removeListener");
+ if (!watcher) goto fail_wrapped;
+ bool watched = scr_dyn_truthy(watcher);
+ scr_dyn_release(watcher);
+ if (watched) {
+ ScrDyn *name = bc_key("removeListener");
+ ScrDyn *args[3] = { name, type, wrapped && scr_dyn_truthy(wrapped) ? wrapped : listener };
+ ScrDyn *emitted = bc_invoke(self, "emit", args, 3);
+ scr_dyn_release(name);
+ if (!emitted) goto fail_wrapped;
+ scr_dyn_release(emitted);
+ }
+ }
+ } else if (list->kind == SCR_DYN_ARR) {
+ double length = scr_dyn_arr_len(list);
+ double position = -1;
+ for (double i = length - 1; i >= 0; i--) {
+ ScrDyn *item = scr_dyn_index_get(list, i);
+ if (!item) goto fail_wrapped;
+ bool same = scr_dyn_strict_eq(item, listener);
+ if (!same && scr_dyn_is_callable(item)) {
+ ScrDyn *inner = bc_get(item, "listener");
+ if (!inner) {
+ scr_dyn_release(item);
+ goto fail_wrapped;
+ }
+ same = scr_dyn_strict_eq(inner, listener);
+ scr_dyn_release(inner);
+ }
+ scr_dyn_release(item);
+ if (same) {
+ position = i;
+ break;
+ }
+ }
+ if (position >= 0) {
+ ScrDyn *rest = bc_list_copy(list, NULL, position);
+ if (!rest) goto fail_wrapped;
+ ScrDyn *stored = scr_dyn_arr_len(rest) == 1 ? scr_dyn_index_get(rest, 0) : scr_dyn_retain(rest);
+ scr_dyn_release(rest);
+ if (!stored || !bc_set_key(events, type, stored)) goto fail_wrapped;
+ ScrDyn *watcher = bc_get(events, "removeListener");
+ if (!watcher) goto fail_wrapped;
+ bool watched = !bc_undefined(watcher);
+ scr_dyn_release(watcher);
+ if (watched) {
+ ScrDyn *name = bc_key("removeListener");
+ ScrDyn *args[3] = { name, type, listener };
+ ScrDyn *emitted = bc_invoke(self, "emit", args, 3);
+ scr_dyn_release(name);
+ if (!emitted) goto fail_wrapped;
+ scr_dyn_release(emitted);
+ }
+ }
+ }
+ if (wrapped) scr_dyn_release(wrapped);
+ scr_dyn_release(list);
+ scr_dyn_release(events);
+ return scr_dyn_retain(self);
+fail_wrapped:
+ if (wrapped) scr_dyn_release(wrapped);
+fail_list:
+ scr_dyn_release(list);
+fail_events:
+ scr_dyn_release(events);
+ return NULL;
+}
+
+/* ── once wrappers ──────────────────────────────────────────────────── */
+
+enum { BC_ONCE_FIRED, BC_ONCE_TARGET, BC_ONCE_TYPE, BC_ONCE_LISTENER, BC_ONCE_SELF, BC_ONCE_CAPS };
+
+static ScrBox *bc_box(ScrDyn *value /* moves in */) {
+ ScrBox *box = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v);
+ scr_box_set_ref(box, value);
+ return box;
+}
+
+static ScrDyn *bc_cap(ScrClosure *closure, size_t index) {
+ return scr_box_get_ref(closure->caps[index]); /* +1 */
+}
+
+/* onceWrapper: the first call removes the wrapper, then calls the listener
+ * with the target as its receiver. The wrapper's reference to itself is
+ * dropped when it fires, so a fired wrapper holds no cycle. */
+static ScrDyn *bc_once_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) {
+ if (scr_box_get_f64(closure->caps[BC_ONCE_FIRED]) != 0) return scr_dyn_retain(scr_dyn_undefined());
+ ScrDyn *target = bc_cap(closure, BC_ONCE_TARGET);
+ ScrDyn *type = bc_cap(closure, BC_ONCE_TYPE);
+ ScrDyn *listener = bc_cap(closure, BC_ONCE_LISTENER);
+ ScrDyn *self = bc_cap(closure, BC_ONCE_SELF);
+ ScrDyn *result = NULL;
+ ScrDyn *removal[2] = { type, self };
+ ScrDyn *removed = bc_invoke(target, "removeListener", removal, 2);
+ if (removed) {
+ scr_dyn_release(removed);
+ scr_box_set_f64(closure->caps[BC_ONCE_FIRED], 1);
+ scr_box_set_ref(closure->caps[BC_ONCE_SELF], scr_dyn_retain(scr_dyn_undefined()));
+ result = bc_apply(listener, target, args, argc);
+ }
+ scr_dyn_release(self);
+ scr_dyn_release(listener);
+ scr_dyn_release(type);
+ scr_dyn_release(target);
+ return result;
+}
+
+static ScrDyn *bc_once_wrap(ScrDyn *target, ScrDyn *type, ScrDyn *listener) {
+ ScrClosure *closure = scr_closure_new(NULL, BC_ONCE_CAPS);
+ closure->caps[BC_ONCE_FIRED] = scr_box_new(SCR_BOX_F64);
+ closure->caps[BC_ONCE_TARGET] = bc_box(scr_dyn_retain(target));
+ closure->caps[BC_ONCE_TYPE] = bc_box(scr_dyn_retain(type));
+ closure->caps[BC_ONCE_LISTENER] = bc_box(scr_dyn_retain(listener));
+ closure->caps[BC_ONCE_SELF] = bc_box(scr_dyn_retain(scr_dyn_undefined()));
+ scr_closure_retain(closure);
+ ScrDyn *wrapper = scr_dyn_new_func(closure, bc_once_call, 0, "native:events", "bound onceWrapper");
+ scr_box_set_ref(closure->caps[BC_ONCE_SELF], scr_dyn_retain(wrapper));
+ scr_closure_release(closure);
+ if (!bc_set(wrapper, "listener", scr_dyn_retain(listener))) {
+ scr_dyn_release(wrapper);
+ return NULL;
+ }
+ return wrapper;
+}
+
+/* ── EventEmitter.prototype ─────────────────────────────────────────── */
+
+static ScrDyn *bc_on(ScrClosure *closure, ScrDyn *const *args, size_t argc) {
+ (void)closure;
+ ScrDyn *self = bc_this();
+ ScrDyn *result = bc_require_object(self, "_events")
+ ? bc_add_listener(self, bc_arg(args, argc, 0), bc_arg(args, argc, 1), false) : NULL;
+ scr_dyn_release(self);
+ return result;
+}
+
+static ScrDyn *bc_prepend(ScrClosure *closure, ScrDyn *const *args, size_t argc) {
+ (void)closure;
+ ScrDyn *self = bc_this();
+ ScrDyn *result = bc_require_object(self, "_events")
+ ? bc_add_listener(self, bc_arg(args, argc, 0), bc_arg(args, argc, 1), true) : NULL;
+ scr_dyn_release(self);
+ return result;
+}
+
+static ScrDyn *bc_once_common(ScrDyn *const *args, size_t argc, bool prepend) {
+ ScrDyn *self = bc_this();
+ ScrDyn *type = bc_arg(args, argc, 0);
+ ScrDyn *listener = bc_arg(args, argc, 1);
+ ScrDyn *result = NULL;
+ if (bc_check_listener(listener) && bc_require_object(self, "_events")) {
+ ScrDyn *wrapper = bc_once_wrap(self, type, listener);
+ if (wrapper) {
+ result = bc_add_listener(self, type, wrapper, prepend);
+ scr_dyn_release(wrapper);
+ }
+ }
+ scr_dyn_release(self);
+ return result;
+}
+
+static ScrDyn *bc_once(ScrClosure *closure, ScrDyn *const *args, size_t argc) {
+ (void)closure;
+ return bc_once_common(args, argc, false);
+}
+
+static ScrDyn *bc_prepend_once(ScrClosure *closure, ScrDyn *const *args, size_t argc) {
+ (void)closure;
+ return bc_once_common(args, argc, true);
+}
+
+static ScrDyn *bc_off(ScrClosure *closure, ScrDyn *const *args, size_t argc) {
+ (void)closure;
+ ScrDyn *self = bc_this();
+ ScrDyn *result = bc_require_object(self, "_events")
+ ? bc_remove_listener(self, bc_arg(args, argc, 0), bc_arg(args, argc, 1)) : NULL;
+ scr_dyn_release(self);
+ return result;
+}
+
+/* removeAllListeners(type?): with no 'removeListener' watcher the table is
+ * reset; otherwise every listener is removed through removeListener, last
+ * first, so the watcher sees each one (and itself last). */
+static ScrDyn *bc_remove_all(ScrClosure *closure, ScrDyn *const *args, size_t argc) {
+ (void)closure;
+ ScrDyn *self = bc_this();
+ if (!bc_require_object(self, "_events")) {
+ scr_dyn_release(self);
+ return NULL;
+ }
+ ScrDyn *type = bc_arg(args, argc, 0);
+ bool all = argc == 0;
+ ScrDyn *events = bc_get(self, "_events");
+ ScrDyn *watcher = NULL;
+ if (!events) goto fail;
+ if (bc_undefined(events)) goto done;
+ watcher = bc_get(events, "removeListener");
+ if (!watcher) goto fail;
+ if (bc_undefined(watcher)) {
+ if (all) {
+ if (!bc_set(self, "_events", scr_dyn_new_obj_null_proto()) || !bc_set(self, "_eventsCount", scr_dyn_new_num(0)))
+ goto fail;
+ } else {
+ ScrDyn *present = bc_get_key(events, type);
+ if (!present) goto fail;
+ bool has = !bc_undefined(present);
+ scr_dyn_release(present);
+ if (has) {
+ ScrDyn *count = bc_get(self, "_eventsCount");
+ if (!count) goto fail;
+ double left = bc_number(count) - 1;
+ scr_dyn_release(count);
+ if (left == 0) {
+ if (!bc_set(self, "_events", scr_dyn_new_obj_null_proto()) || !bc_set(self, "_eventsCount", scr_dyn_new_num(0)))
+ goto fail;
+ } else {
+ if (!bc_set(self, "_eventsCount", scr_dyn_new_num(left))) goto fail;
+ scr_dyn_key_delete_computed(events, type, false);
+ if (scr_exc_pending()) goto fail;
+ }
+ }
+ }
+ goto done;
+ }
+ if (all) {
+ ScrDyn *keys = scr_dyn_own_keys(events);
+ if (!keys) goto fail;
+ double length = scr_dyn_arr_len(keys);
+ for (double i = 0; i < length; i++) {
+ ScrDyn *key = scr_dyn_index_get(keys, i);
+ if (!key) {
+ scr_dyn_release(keys);
+ goto fail;
+ }
+ bool skip = key->kind == SCR_DYN_STR && key->v.str->len == 14 && memcmp(key->v.str->data, "removeListener", 14) == 0;
+ ScrDyn *done_key = NULL;
+ if (!skip) {
+ ScrDyn *one[1] = { key };
+ done_key = bc_invoke(self, "removeAllListeners", one, 1);
+ }
+ scr_dyn_release(key);
+ if (!skip && !done_key) {
+ scr_dyn_release(keys);
+ goto fail;
+ }
+ if (done_key) scr_dyn_release(done_key);
+ }
+ scr_dyn_release(keys);
+ ScrDyn *name = bc_key("removeListener");
+ ScrDyn *one[1] = { name };
+ ScrDyn *cleared = bc_invoke(self, "removeAllListeners", one, 1);
+ scr_dyn_release(name);
+ if (!cleared) goto fail;
+ scr_dyn_release(cleared);
+ if (!bc_set(self, "_events", scr_dyn_new_obj_null_proto()) || !bc_set(self, "_eventsCount", scr_dyn_new_num(0)))
+ goto fail;
+ goto done;
+ }
+ {
+ ScrDyn *listeners = bc_get_key(events, type);
+ if (!listeners) goto fail;
+ if (scr_dyn_is_callable(listeners)) {
+ ScrDyn *pair[2] = { type, listeners };
+ ScrDyn *removed = bc_invoke(self, "removeListener", pair, 2);
+ scr_dyn_release(listeners);
+ if (!removed) goto fail;
+ scr_dyn_release(removed);
+ } else if (listeners->kind == SCR_DYN_ARR) {
+ for (double i = scr_dyn_arr_len(listeners) - 1; i >= 0; i--) {
+ ScrDyn *item = scr_dyn_index_get(listeners, i);
+ if (!item) {
+ scr_dyn_release(listeners);
+ goto fail;
+ }
+ ScrDyn *pair[2] = { type, item };
+ ScrDyn *removed = bc_invoke(self, "removeListener", pair, 2);
+ scr_dyn_release(item);
+ if (!removed) {
+ scr_dyn_release(listeners);
+ goto fail;
+ }
+ scr_dyn_release(removed);
+ }
+ scr_dyn_release(listeners);
+ } else {
+ scr_dyn_release(listeners);
+ }
+ }
+done:
+ if (watcher) scr_dyn_release(watcher);
+ if (events) scr_dyn_release(events);
+ return self;
+fail:
+ if (watcher) scr_dyn_release(watcher);
+ if (events) scr_dyn_release(events);
+ scr_dyn_release(self);
+ return NULL;
+}
+
+/* emit(type, ...args): Node's unhandled 'error' rule, then each listener
+ * with the emitter as its receiver over a copy of the list. */
+static ScrDyn *bc_emit(ScrClosure *closure, ScrDyn *const *args, size_t argc) {
+ (void)closure;
+ ScrDyn *self = bc_this();
+ if (!bc_require_object(self, "_events")) {
+ scr_dyn_release(self);
+ return NULL;
+ }
+ ScrDyn *type = bc_arg(args, argc, 0);
+ ScrDyn *const *rest = argc > 1 ? args + 1 : bc_unused_args;
+ size_t restc = argc > 1 ? argc - 1 : 0;
+ bool error = type->kind == SCR_DYN_STR && type->v.str->len == 5 && memcmp(type->v.str->data, "error", 5) == 0;
+ ScrDyn *events = bc_get(self, "_events");
+ if (!events) goto fail_self;
+ if (!bc_undefined(events)) {
+ if (error) {
+ ScrDyn *handler = bc_get(events, "error");
+ if (!handler) goto fail_events;
+ error = bc_undefined(handler);
+ scr_dyn_release(handler);
+ }
+ } else if (!error) {
+ scr_dyn_release(events);
+ scr_dyn_release(self);
+ return scr_dyn_new_bool(false);
+ }
+ if (error) {
+ ScrDyn *payload = restc ? rest[0] : scr_dyn_undefined();
+ if (scr_dyn_err_instanceof(payload, SCR_ERR_ERROR)) {
+ scr_dyn_throw(scr_dyn_retain(payload));
+ } else {
+ ScrStr *shown = scr_insp_dyn(payload, 0, 2);
+ ScrJsonBuf text;
+ scr_jb_init(&text);
+ scr_jb_puts(&text, "Unhandled error. (");
+ scr_jb_put_str(&text, shown);
+ scr_jb_putc(&text, ')');
+ scr_str_release(shown);
+ ScrStr *message = scr_jb_finish(&text);
+ scr_throw_error_msg_code(SCR_ERR_ERROR, message->data, message->len, "ERR_UNHANDLED_ERROR");
+ scr_str_release(message);
+ }
+ goto fail_events;
+ }
+ ScrDyn *handler = bc_get_key(events, type);
+ if (!handler) goto fail_events;
+ bool fired = !bc_undefined(handler);
+ if (scr_dyn_is_callable(handler)) {
+ ScrDyn *result = bc_apply(handler, self, rest, restc);
+ scr_dyn_release(handler);
+ if (!result) goto fail_events;
+ scr_dyn_release(result);
+ } else if (handler->kind == SCR_DYN_ARR) {
+ /* arrayClone: listeners added or removed during the emit run next time */
+ size_t length = (size_t)scr_dyn_arr_len(handler);
+ ScrDyn **listeners = malloc((length ? length : 1) * sizeof *listeners);
+ if (!listeners) scr_trap("out of memory");
+ for (size_t i = 0; i < length; i++) listeners[i] = scr_dyn_index_get(handler, (double)i);
+ scr_dyn_release(handler);
+ bool ok = true;
+ for (size_t i = 0; i < length && ok; i++) {
+ if (!listeners[i]) {
+ ok = false;
+ break;
+ }
+ ScrDyn *result = bc_apply(listeners[i], self, rest, restc);
+ if (!result) ok = false;
+ else scr_dyn_release(result);
+ }
+ for (size_t i = 0; i < length; i++)
+ if (listeners[i]) scr_dyn_release(listeners[i]);
+ free(listeners);
+ if (!ok) goto fail_events;
+ } else {
+ scr_dyn_release(handler);
+ }
+ scr_dyn_release(events);
+ scr_dyn_release(self);
+ return scr_dyn_new_bool(fired);
+fail_events:
+ scr_dyn_release(events);
+fail_self:
+ scr_dyn_release(self);
+ return NULL;
+}
+
+/* _listeners(target, type, unwrap): a fresh array. */
+static ScrDyn *bc_listeners_of(bool unwrap, ScrDyn *const *args, size_t argc) {
+ ScrDyn *self = bc_this();
+ if (!bc_require_object(self, "_events")) {
+ scr_dyn_release(self);
+ return NULL;
+ }
+ ScrDyn *result = scr_dyn_new_arr();
+ ScrDyn *events = bc_get(self, "_events");
+ ScrDyn *list = NULL;
+ if (!events) goto fail;
+ if (bc_undefined(events)) goto done;
+ list = bc_get_key(events, bc_arg(args, argc, 0));
+ if (!list) goto fail;
+ size_t length = scr_dyn_is_callable(list) ? 1 : list->kind == SCR_DYN_ARR ? (size_t)scr_dyn_arr_len(list) : 0;
+ for (size_t i = 0; i < length; i++) {
+ ScrDyn *item = scr_dyn_is_callable(list) ? scr_dyn_retain(list) : scr_dyn_index_get(list, (double)i);
+ if (!item) goto fail;
+ if (unwrap && scr_dyn_is_callable(item)) {
+ ScrDyn *inner = bc_get(item, "listener");
+ if (!inner) {
+ scr_dyn_release(item);
+ goto fail;
+ }
+ if (!bc_undefined(inner)) {
+ scr_dyn_release(item);
+ item = inner;
+ } else {
+ scr_dyn_release(inner);
+ }
+ }
+ scr_dyn_arr_push(result, item);
+ }
+done:
+ if (list) scr_dyn_release(list);
+ if (events) scr_dyn_release(events);
+ scr_dyn_release(self);
+ return result;
+fail:
+ if (list) scr_dyn_release(list);
+ if (events) scr_dyn_release(events);
+ scr_dyn_release(result);
+ scr_dyn_release(self);
+ return NULL;
+}
+
+static ScrDyn *bc_listeners(ScrClosure *closure, ScrDyn *const *args, size_t argc) {
+ (void)closure;
+ return bc_listeners_of(true, args, argc);
+}
+
+static ScrDyn *bc_raw_listeners(ScrClosure *closure, ScrDyn *const *args, size_t argc) {
+ (void)closure;
+ return bc_listeners_of(false, args, argc);
+}
+
+/* listenerCount(type, listener?) over an emitter's _events. */
+static ScrDyn *bc_count_listeners(ScrDyn *self, ScrDyn *type, ScrDyn *only) {
+ ScrDyn *events = bc_get(self, "_events");
+ if (!events) return NULL;
+ double count = 0;
+ if (!bc_undefined(events)) {
+ ScrDyn *list = bc_get_key(events, type);
+ if (!list) {
+ scr_dyn_release(events);
+ return NULL;
+ }
+ size_t length = scr_dyn_is_callable(list) ? 1 : list->kind == SCR_DYN_ARR ? (size_t)scr_dyn_arr_len(list) : 0;
+ for (size_t i = 0; i < length; i++) {
+ if (!only) {
+ count++;
+ continue;
+ }
+ ScrDyn *item = scr_dyn_is_callable(list) ? scr_dyn_retain(list) : scr_dyn_index_get(list, (double)i);
+ if (!item) break;
+ bool same = scr_dyn_strict_eq(item, only);
+ if (!same && scr_dyn_is_callable(item)) {
+ ScrDyn *inner = bc_get(item, "listener");
+ if (inner) {
+ same = scr_dyn_strict_eq(inner, only);
+ scr_dyn_release(inner);
+ }
+ }
+ scr_dyn_release(item);
+ if (same) count++;
+ }
+ scr_dyn_release(list);
+ }
+ scr_dyn_release(events);
+ if (scr_exc_pending()) return NULL;
+ return scr_dyn_new_num(count);
+}
+
+static ScrDyn *bc_listener_count(ScrClosure *closure, ScrDyn *const *args, size_t argc) {
+ (void)closure;
+ ScrDyn *self = bc_this();
+ ScrDyn *result = bc_require_object(self, "_events")
+ ? bc_count_listeners(self, bc_arg(args, argc, 0),
+ argc > 1 && !bc_undefined(args[1]) && args[1]->kind != SCR_DYN_NULL ? args[1] : NULL)
+ : NULL;
+ scr_dyn_release(self);
+ return result;
+}
+
+/* EventEmitter.listenerCount(emitter, type): the deprecated static. */
+static ScrDyn *bc_static_listener_count(ScrClosure *closure, ScrDyn *const *args, size_t argc) {
+ (void)closure;
+ ScrDyn *emitter = bc_arg(args, argc, 0);
+ ScrDyn *type = bc_arg(args, argc, 1);
+ ScrDyn *method = bc_require_object(emitter, "listenerCount") ? bc_get(emitter, "listenerCount") : NULL;
+ if (!method) return NULL;
+ ScrDyn *result;
+ if (scr_dyn_is_callable(method)) result = bc_apply(method, emitter, &type, 1);
+ else result = bc_count_listeners(emitter, type, NULL);
+ scr_dyn_release(method);
+ return result;
+}
+
+static ScrDyn *bc_event_names(ScrClosure *closure, ScrDyn *const *args, size_t argc) {
+ (void)closure;
+ (void)args;
+ (void)argc;
+ ScrDyn *self = bc_this();
+ if (!bc_require_object(self, "_eventsCount")) {
+ scr_dyn_release(self);
+ return NULL;
+ }
+ ScrDyn *count = bc_get(self, "_eventsCount");
+ ScrDyn *result = NULL;
+ if (count) {
+ if (bc_number(count) > 0) {
+ ScrDyn *events = bc_get(self, "_events");
+ if (events) {
+ result = scr_dyn_own_keys(events);
+ scr_dyn_release(events);
+ }
+ } else {
+ result = scr_dyn_new_arr();
+ }
+ scr_dyn_release(count);
+ }
+ scr_dyn_release(self);
+ return result;
+}
+
+static ScrDyn *bc_set_max(ScrClosure *closure, ScrDyn *const *args, size_t argc) {
+ (void)closure;
+ ScrDyn *self = bc_this();
+ ScrDyn *n = bc_arg(args, argc, 0);
+ if (n->kind != SCR_DYN_NUM) {
+ scr_dyn_arg_type_fail("n", "of type number", n);
+ scr_dyn_release(self);
+ return NULL;
+ }
+ if (n->v.num < 0 || n->v.num != n->v.num) {
+ ScrStr *shown = scr_dyn_string_coerce_js(n);
+ ScrJsonBuf text;
+ scr_jb_init(&text);
+ scr_jb_puts(&text, "The value of \"n\" is out of range. It must be a non-negative number. Received ");
+ if (shown) scr_jb_put_str(&text, shown);
+ if (shown) scr_str_release(shown);
+ ScrStr *message = scr_jb_finish(&text);
+ scr_throw_error_msg_code(SCR_ERR_RANGE, message->data, message->len, "ERR_OUT_OF_RANGE");
+ scr_str_release(message);
+ scr_dyn_release(self);
+ return NULL;
+ }
+ if (!bc_require_object(self, "_maxListeners") || !bc_set(self, "_maxListeners", scr_dyn_retain(n))) {
+ scr_dyn_release(self);
+ return NULL;
+ }
+ return self;
+}
+
+/* EventEmitter.defaultMaxListeners, Node's accessor over the one default
+ * the typed EventEmitter reads too (scr_events_emitter.c). The setter
+ * validates as Node's validateNumber(arg, 'defaultMaxListeners', 0). */
+static ScrDyn *bc_default_max_get(ScrClosure *closure, ScrDyn *const *args, size_t argc) {
+ (void)closure;
+ (void)args;
+ (void)argc;
+ return scr_dyn_new_num(scr_emitter_get_default_max());
+}
+
+static ScrDyn *bc_default_max_set(ScrClosure *closure, ScrDyn *const *args, size_t argc) {
+ (void)closure;
+ static const char name[] = "defaultMaxListeners";
+ ScrStr *key = scr_str_new(name, sizeof name - 1);
+ scr_emitter_set_default_max_chk(argc ? args[0] : scr_dyn_undefined(), key);
+ scr_str_release(key);
+ if (scr_exc_pending()) return NULL;
+ return scr_dyn_retain(scr_dyn_undefined());
+}
+
+static void bc_define_default_max(ScrDyn *ctor) {
+ ScrDyn *descriptor = scr_dyn_new_obj();
+ scr_dyn_obj_set(descriptor, "get", 3,
+ scr_dyn_new_func(scr_closure_new(NULL, 0), bc_default_max_get, 0, "native:events", "get"));
+ scr_dyn_obj_set(descriptor, "set", 3,
+ scr_dyn_new_func(scr_closure_new(NULL, 0), bc_default_max_set, 1, "native:events", "set"));
+ scr_dyn_obj_set(descriptor, "enumerable", 10, scr_dyn_new_bool(true));
+ ScrStr *name = scr_str_new("defaultMaxListeners", 19);
+ ScrDyn *key = scr_dyn_new_str(name);
+ scr_str_release(name);
+ ScrDyn *defined = scr_dyn_define_property(ctor, key, descriptor);
+ if (defined) scr_dyn_release(defined);
+ scr_dyn_release(key);
+ scr_dyn_release(descriptor);
+}
+
+static ScrDyn *bc_get_max(ScrClosure *closure, ScrDyn *const *args, size_t argc) {
+ (void)closure;
+ (void)args;
+ (void)argc;
+ ScrDyn *self = bc_this();
+ double max = bc_require_object(self, "_maxListeners") ? bc_max_listeners(self) : -1;
+ scr_dyn_release(self);
+ if (scr_exc_pending()) return NULL;
+ return scr_dyn_new_num(max);
+}
+
+/* ── Stream.prototype.pipe (lib/internal/streams/legacy.js) ─────────── */
+
+enum { BC_PIPE_SOURCE, BC_PIPE_DEST, BC_PIPE_ENDED, BC_PIPE_HANDLERS, BC_PIPE_CAPS };
+enum { BC_ONDATA, BC_ONDRAIN, BC_ONEND, BC_ONCLOSE, BC_ONERROR, BC_CLEANUP, BC_HANDLER_COUNT };
+
+static ScrDyn *bc_pipe_handler(ScrClosure *state, int which);
+
+static ScrDyn *bc_pipe_get_handler(ScrClosure *state, int which) {
+ ScrDyn *handlers = bc_cap(state, BC_PIPE_HANDLERS);
+ ScrDyn *handler = scr_dyn_index_get(handlers, which);
+ scr_dyn_release(handlers);
+ return handler;
+}
+
+/* Calls source/dest.removeListener for one pipe handler. */
+static bool bc_pipe_remove(ScrDyn *emitter, const char *event, ScrClosure *state, int which) {
+ ScrDyn *handler = bc_pipe_get_handler(state, which);
+ if (!handler) return false;
+ ScrDyn *name = bc_key(event);
+ ScrDyn *pair[2] = { name, handler };
+ ScrDyn *removed = bc_invoke(emitter, "removeListener", pair, 2);
+ scr_dyn_release(name);
+ scr_dyn_release(handler);
+ if (!removed) return false;
+ scr_dyn_release(removed);
+ return true;
+}
+
+static bool bc_pipe_on(ScrDyn *emitter, const char *event, ScrClosure *state, int which) {
+ ScrDyn *handler = bc_pipe_get_handler(state, which);
+ if (!handler) return false;
+ ScrDyn *name = bc_key(event);
+ ScrDyn *pair[2] = { name, handler };
+ ScrDyn *added = bc_invoke(emitter, "on", pair, 2);
+ scr_dyn_release(name);
+ scr_dyn_release(handler);
+ if (!added) return false;
+ scr_dyn_release(added);
+ return true;
+}
+
+static bool bc_pipe_cleanup(ScrClosure *state) {
+ /* A second run (dest emitting 'close' from end(), with cleanup still
+ * queued on the source's 'end') finds the listeners removed, as Node's
+ * repeated removeListener calls do. */
+ ScrDyn *handlers = bc_cap(state, BC_PIPE_HANDLERS);
+ bool cleaned = handlers->kind == SCR_DYN_ARR && scr_dyn_arr_len(handlers) == 0;
+ scr_dyn_release(handlers);
+ if (cleaned) return true;
+ ScrDyn *source = bc_cap(state, BC_PIPE_SOURCE);
+ ScrDyn *dest = bc_cap(state, BC_PIPE_DEST);
+ bool ok = bc_pipe_remove(source, "data", state, BC_ONDATA) &&
+ bc_pipe_remove(dest, "drain", state, BC_ONDRAIN) &&
+ bc_pipe_remove(source, "end", state, BC_ONEND) &&
+ bc_pipe_remove(source, "close", state, BC_ONCLOSE) &&
+ bc_pipe_remove(source, "error", state, BC_ONERROR) &&
+ bc_pipe_remove(dest, "error", state, BC_ONERROR) &&
+ bc_pipe_remove(source, "end", state, BC_CLEANUP) &&
+ bc_pipe_remove(source, "close", state, BC_CLEANUP) &&
+ bc_pipe_remove(dest, "close", state, BC_CLEANUP);
+ scr_dyn_release(dest);
+ scr_dyn_release(source);
+ /* the handlers array held the closures that hold this state */
+ if (ok) scr_box_set_ref(state->caps[BC_PIPE_HANDLERS], scr_dyn_new_arr());
+ return ok;
+}
+
+static ScrDyn *bc_pipe_event(ScrClosure *closure, ScrDyn *const *args, size_t argc) {
+ ScrClosure *state = (ScrClosure *)scr_box_get_ref(closure->caps[0]);
+ int which = (int)scr_box_get_f64(closure->caps[1]);
+ ScrDyn *source = bc_cap(state, BC_PIPE_SOURCE);
+ ScrDyn *dest = bc_cap(state, BC_PIPE_DEST);
+ bool ok = true;
+ switch (which) {
+ case BC_ONDATA: {
+ ScrDyn *writable = bc_get(dest, "writable");
+ if (!writable) {
+ ok = false;
+ break;
+ }
+ bool open = scr_dyn_truthy(writable);
+ scr_dyn_release(writable);
+ if (!open) break;
+ ScrDyn *chunk = bc_arg(args, argc, 0);
+ ScrDyn *written = bc_invoke(dest, "write", &chunk, 1);
+ if (!written) {
+ ok = false;
+ break;
+ }
+ bool full = written->kind == SCR_DYN_BOOL && !written->v.b;
+ scr_dyn_release(written);
+ if (full) {
+ ScrDyn *pause = bc_get(source, "pause");
+ if (!pause) {
+ ok = false;
+ break;
+ }
+ if (scr_dyn_is_callable(pause)) {
+ ScrDyn *paused = bc_apply(pause, source, bc_unused_args, 0);
+ if (paused) scr_dyn_release(paused);
+ else ok = false;
+ }
+ scr_dyn_release(pause);
+ }
+ break;
+ }
+ case BC_ONDRAIN: {
+ ScrDyn *readable = bc_get(source, "readable");
+ ScrDyn *resume = readable ? bc_get(source, "resume") : NULL;
+ if (!readable || !resume) {
+ ok = false;
+ } else if (scr_dyn_truthy(readable) && scr_dyn_is_callable(resume)) {
+ ScrDyn *resumed = bc_apply(resume, source, bc_unused_args, 0);
+ if (resumed) scr_dyn_release(resumed);
+ else ok = false;
+ }
+ if (readable) scr_dyn_release(readable);
+ if (resume) scr_dyn_release(resume);
+ break;
+ }
+ case BC_ONEND:
+ case BC_ONCLOSE: {
+ if (scr_box_get_f64(state->caps[BC_PIPE_ENDED]) != 0) break;
+ scr_box_set_f64(state->caps[BC_PIPE_ENDED], 1);
+ ScrDyn *done = which == BC_ONEND ? bc_invoke(dest, "end", bc_unused_args, 0) : NULL;
+ if (which == BC_ONCLOSE) {
+ ScrDyn *destroy = bc_get(dest, "destroy");
+ if (!destroy) {
+ ok = false;
+ break;
+ }
+ if (scr_dyn_is_callable(destroy)) done = bc_apply(destroy, dest, bc_unused_args, 0);
+ else done = scr_dyn_retain(scr_dyn_undefined());
+ scr_dyn_release(destroy);
+ }
+ if (done) scr_dyn_release(done);
+ else ok = false;
+ break;
+ }
+ case BC_ONERROR: {
+ ok = bc_pipe_cleanup(state);
+ if (!ok) break;
+ ScrDyn *rethrow = scr_dyn_this_get();
+ ScrDyn *name = bc_key("error");
+ ScrDyn *count = rethrow->kind == SCR_DYN_UNDEF ? NULL : bc_invoke(rethrow, "listenerCount", &name, 1);
+ scr_dyn_release(name);
+ if (count && bc_number(count) == 0) {
+ scr_dyn_throw(scr_dyn_retain(bc_arg(args, argc, 0)));
+ ok = false;
+ } else if (!count && scr_exc_pending()) {
+ ok = false;
+ }
+ if (count) scr_dyn_release(count);
+ scr_dyn_release(rethrow);
+ break;
+ }
+ case BC_CLEANUP:
+ ok = bc_pipe_cleanup(state);
+ break;
+ default:
+ break;
+ }
+ scr_dyn_release(dest);
+ scr_dyn_release(source);
+ scr_closure_release(state);
+ return ok ? scr_dyn_retain(scr_dyn_undefined()) : NULL;
+}
+
+static ScrDyn *bc_pipe_handler(ScrClosure *state, int which) {
+ ScrClosure *closure = scr_closure_new(NULL, 2);
+ ScrBox *owner = scr_box_new_obj(scr_closure_retain_v, scr_closure_release_v, scr_closure_trace_v);
+ scr_box_set_ref(owner, scr_closure_retain(state));
+ closure->caps[0] = owner;
+ closure->caps[1] = scr_box_new(SCR_BOX_F64);
+ scr_box_set_f64(closure->caps[1], which);
+ return scr_dyn_new_func(closure, bc_pipe_event, 0, "native:stream", which == BC_ONDATA ? "ondata"
+ : which == BC_ONDRAIN ? "ondrain" : which == BC_ONEND ? "onend" : which == BC_ONCLOSE ? "onclose"
+ : which == BC_ONERROR ? "onerror" : "cleanup");
+}
+
+/* Stream.prototype.pipe(dest, options): Node's legacy pipe for streams
+ * that are only EventEmitters. 'end' and 'close' end or destroy the
+ * destination unless options.end is false or the destination is stdio. */
+static ScrDyn *bc_pipe(ScrClosure *closure, ScrDyn *const *args, size_t argc) {
+ (void)closure;
+ ScrDyn *source = bc_this();
+ ScrDyn *dest = bc_arg(args, argc, 0);
+ ScrDyn *options = bc_arg(args, argc, 1);
+ if (!bc_require_object(source, "on") || !bc_require_object(dest, "on")) {
+ scr_dyn_release(source);
+ return NULL;
+ }
+ ScrClosure *state = scr_closure_new(NULL, BC_PIPE_CAPS);
+ state->caps[BC_PIPE_SOURCE] = bc_box(scr_dyn_retain(source));
+ state->caps[BC_PIPE_DEST] = bc_box(scr_dyn_retain(dest));
+ state->caps[BC_PIPE_ENDED] = scr_box_new(SCR_BOX_F64);
+ ScrDyn *handlers = scr_dyn_new_arr();
+ for (int i = 0; i < BC_HANDLER_COUNT; i++) scr_dyn_arr_push(handlers, bc_pipe_handler(state, i));
+ state->caps[BC_PIPE_HANDLERS] = bc_box(handlers);
+ bool ok = bc_pipe_on(source, "data", state, BC_ONDATA) && bc_pipe_on(dest, "drain", state, BC_ONDRAIN);
+ bool end_dest = true;
+ if (ok && options->kind != SCR_DYN_UNDEF && options->kind != SCR_DYN_NULL) {
+ ScrDyn *end = bc_get(options, "end");
+ if (!end) ok = false;
+ else {
+ end_dest = !(end->kind == SCR_DYN_BOOL && !end->v.b);
+ scr_dyn_release(end);
+ }
+ }
+ if (ok && end_dest) {
+ ScrDyn *stdio = bc_get(dest, "_isStdio");
+ if (!stdio) ok = false;
+ else {
+ end_dest = !scr_dyn_truthy(stdio);
+ scr_dyn_release(stdio);
+ }
+ }
+ if (ok && end_dest)
+ ok = bc_pipe_on(source, "end", state, BC_ONEND) && bc_pipe_on(source, "close", state, BC_ONCLOSE);
+ if (ok) {
+ /* prependListener(source, 'error', onerror) and the same on dest */
+ ScrDyn *onerror = bc_pipe_get_handler(state, BC_ONERROR);
+ ScrDyn *name = bc_key("error");
+ ScrDyn *pair[2] = { name, onerror };
+ ScrDyn *a = onerror ? bc_invoke(source, "prependListener", pair, 2) : NULL;
+ ScrDyn *b = a ? bc_invoke(dest, "prependListener", pair, 2) : NULL;
+ ok = a && b;
+ if (a) scr_dyn_release(a);
+ if (b) scr_dyn_release(b);
+ scr_dyn_release(name);
+ if (onerror) scr_dyn_release(onerror);
+ }
+ if (ok)
+ ok = bc_pipe_on(source, "end", state, BC_CLEANUP) && bc_pipe_on(source, "close", state, BC_CLEANUP) &&
+ bc_pipe_on(dest, "close", state, BC_CLEANUP);
+ ScrDyn *result = NULL;
+ if (ok) {
+ ScrDyn *name = bc_key("pipe");
+ ScrDyn *pair[2] = { name, source };
+ ScrDyn *emitted = bc_invoke(dest, "emit", pair, 2);
+ scr_dyn_release(name);
+ if (emitted) {
+ scr_dyn_release(emitted);
+ result = scr_dyn_retain(dest);
+ }
+ }
+ scr_closure_release(state);
+ scr_dyn_release(source);
+ return result;
+}
+
+/* ── the constructors ───────────────────────────────────────────────── */
+
+static ScrDyn *bc_construct(ScrClosure *closure, ScrDyn *const *args, size_t argc) {
+ int kind = (int)scr_box_get_f64(closure->caps[0]);
+ ScrDyn *self = bc_this();
+ ScrDyn *result = NULL;
+ if (kind == BC_EMITTER || kind == BC_STREAM) {
+ if (bc_emitter_init(self, bc_arg(args, argc, 0))) result = scr_dyn_retain(scr_dyn_undefined());
+ } else {
+ char message[160];
+ snprintf(message, sizeof message,
+ "'%s' as the base of a constructor function (util.inherits or %s.call(this)) is not supported yet",
+ bc_names[kind], bc_names[kind]);
+ scr_throw_error_msg_code(SCR_ERR_ERROR, message, strlen(message), "SC2020");
+ }
+ scr_dyn_release(self);
+ return result;
+}
+
+static void bc_method(ScrDyn *prototype, const char *name, ScrDynThunk thunk, uint32_t arity) {
+ ScrDyn *fn = scr_dyn_new_func(scr_closure_new(NULL, 0), thunk, arity, "native:events", name);
+ bc_set(prototype, name, fn);
+}
+
+static void bc_alias(ScrDyn *prototype, const char *alias, const char *name) {
+ ScrDyn *fn = bc_get(prototype, name);
+ if (fn) bc_set(prototype, alias, fn);
+}
+
+static void bc_teardown(void) {
+ for (int i = 0; i < BC_COUNT; i++) {
+ if (!bc_ctors[i]) continue;
+ ScrDyn *ctor = bc_ctors[i];
+ /* prototype.constructor, EventEmitter.EventEmitter and Stream.Stream
+ * point back at the constructor, and Stream.Readable … at constructors
+ * whose own prototype is Stream: break those cycles first. */
+ ScrDyn *prototype = bc_get(ctor, "prototype");
+ if (prototype && prototype->kind == SCR_DYN_OBJ) bc_set(prototype, "constructor", scr_dyn_retain(scr_dyn_undefined()));
+ if (prototype) scr_dyn_release(prototype);
+ bc_set(ctor, bc_names[i], scr_dyn_retain(scr_dyn_undefined()));
+ if (i == BC_STREAM)
+ for (int other = BC_READABLE; other < BC_COUNT; other++)
+ bc_set(ctor, bc_names[other], scr_dyn_retain(scr_dyn_undefined()));
+ if (scr_exc_pending()) scr_exc_clear();
+ }
+ for (int i = 0; i < BC_COUNT; i++) {
+ if (!bc_ctors[i]) continue;
+ ScrDyn *ctor = bc_ctors[i];
+ bc_ctors[i] = NULL;
+ scr_dyn_release(ctor);
+ }
+ for (int i = 0; i < BC_SYMBOLS; i++) {
+ if (bc_symbols[i]) scr_dyn_release(bc_symbols[i]);
+ bc_symbols[i] = NULL;
+ }
+ if (scr_exc_pending()) scr_exc_clear();
+}
+
+static ScrDyn *bc_new_class(int kind, ScrDyn *parent) {
+ ScrClosure *closure = scr_closure_new(NULL, 1);
+ closure->function_kind = 4; /* an ordinary function with its own prototype */
+ closure->caps[0] = scr_box_new(SCR_BOX_F64);
+ scr_box_set_f64(closure->caps[0], kind);
+ ScrDyn *ctor = scr_dyn_new_func(closure, bc_construct, 1, "native:events", bc_names[kind]);
+ ScrDyn *prototype = parent ? NULL : scr_dyn_new_obj();
+ if (parent) {
+ ScrDyn *base = bc_get(parent, "prototype");
+ prototype = scr_dyn_obj_create(base);
+ scr_dyn_release(base);
+ ScrDyn *linked = scr_dyn_set_prototype(ctor, parent);
+ if (linked) scr_dyn_release(linked);
+ }
+ bc_set(prototype, "constructor", scr_dyn_retain(ctor));
+ bc_set(ctor, "prototype", scr_dyn_retain(prototype));
+ if (kind == BC_EMITTER) {
+ bc_set(prototype, "_events", scr_dyn_retain(scr_dyn_undefined()));
+ bc_set(prototype, "_eventsCount", scr_dyn_new_num(0));
+ bc_set(prototype, "_maxListeners", scr_dyn_retain(scr_dyn_undefined()));
+ bc_set_key(prototype, bc_symbol(BC_CAPTURE), scr_dyn_new_bool(false));
+ bc_method(prototype, "setMaxListeners", bc_set_max, 1);
+ bc_method(prototype, "getMaxListeners", bc_get_max, 0);
+ bc_method(prototype, "emit", bc_emit, 1);
+ bc_method(prototype, "addListener", bc_on, 2);
+ bc_alias(prototype, "on", "addListener");
+ bc_method(prototype, "prependListener", bc_prepend, 2);
+ bc_method(prototype, "once", bc_once, 2);
+ bc_method(prototype, "prependOnceListener", bc_prepend_once, 2);
+ bc_method(prototype, "removeListener", bc_off, 2);
+ bc_alias(prototype, "off", "removeListener");
+ bc_method(prototype, "removeAllListeners", bc_remove_all, 1);
+ bc_method(prototype, "listeners", bc_listeners, 1);
+ bc_method(prototype, "rawListeners", bc_raw_listeners, 1);
+ bc_method(prototype, "listenerCount", bc_listener_count, 2);
+ bc_method(prototype, "eventNames", bc_event_names, 0);
+ bc_set(ctor, "EventEmitter", scr_dyn_retain(ctor));
+ bc_define_default_max(ctor);
+ ScrDyn *count = scr_dyn_new_func(scr_closure_new(NULL, 0), bc_static_listener_count, 2, "native:events", "listenerCount");
+ bc_set(ctor, "listenerCount", count);
+ } else if (kind == BC_STREAM) {
+ ScrDyn *pipe = scr_dyn_new_func(scr_closure_new(NULL, 0), bc_pipe, 2, "native:stream", "pipe");
+ bc_set(prototype, "pipe", pipe);
+ }
+ scr_dyn_release(prototype);
+ if (scr_exc_pending()) scr_exc_clear();
+ return ctor;
+}
+
+/* All seven constructors are created together, each after its parent, so
+ * Stream's static members name the same constructors every other read
+ * answers. */
+static ScrDyn *bc_class(int kind) {
+ if (!bc_ctors[kind]) {
+ static const int parents[BC_COUNT] = {
+ -1, BC_EMITTER, BC_STREAM, BC_STREAM, BC_READABLE, BC_DUPLEX, BC_TRANSFORM,
+ };
+ for (int each = 0; each < BC_COUNT; each++) {
+ if (bc_ctors[each]) continue;
+ bc_ctors[each] = bc_new_class(each, parents[each] >= 0 ? bc_ctors[parents[each]] : NULL);
+ }
+ /* Registered after the constructors exist, so the teardown runs before
+ * the runtime state their properties were stored in (scr_atexit is
+ * last in, first out). */
+ if (!bc_teardown_registered) {
+ bc_teardown_registered = true;
+ scr_atexit(bc_teardown);
+ }
+ /* require("stream") is Stream itself, carrying its classes */
+ ScrDyn *stream = bc_ctors[BC_STREAM];
+ bc_set(stream, "Stream", scr_dyn_retain(stream));
+ for (int other = BC_READABLE; other < BC_COUNT; other++)
+ bc_set(stream, bc_names[other], scr_dyn_retain(bc_ctors[other]));
+ if (scr_exc_pending()) scr_exc_clear();
+ }
+ return bc_ctors[kind];
+}
+
+/* The constructor value for kind (the BC_* order), +1. */
+ScrDyn *scr_dyn_builtin_class(double kind) {
+ int index = (int)kind;
+ if (index < 0 || index >= BC_COUNT) scr_trap("scriptc: unknown builtin class value\n");
+ return scr_dyn_retain(bc_class(index));
+}
+
+/* OrdinaryHasInstance over the value's prototype chain. */
+static bool bc_chain_has(ScrDyn *value, ScrDyn *prototype) {
+ if (value->kind != SCR_DYN_OBJ && value->kind != SCR_DYN_ARR && value->kind != SCR_DYN_FUNC &&
+ value->kind != SCR_DYN_PROXY)
+ return false;
+ ScrDyn *current = scr_dyn_get_prototype(value);
+ while (current && current->kind != SCR_DYN_NULL) {
+ if (scr_dyn_strict_eq(current, prototype)) {
+ scr_dyn_release(current);
+ return true;
+ }
+ ScrDyn *next = scr_dyn_get_prototype(current);
+ scr_dyn_release(current);
+ current = next;
+ }
+ if (current) scr_dyn_release(current);
+ return false;
+}
+
+/* `value instanceof `. Node's Writable also answers true for an
+ * object whose _writableState is a WritableState, which no untyped object
+ * has, so the prototype chain answers for every kind. */
+bool scr_dyn_builtin_instanceof(ScrDyn *value, double kind) {
+ int index = (int)kind;
+ if (index < 0 || index >= BC_COUNT) scr_trap("scriptc: unknown builtin class value\n");
+ ScrDyn *prototype = bc_get(bc_class(index), "prototype");
+ if (!prototype) return false;
+ bool found = bc_chain_has(value, prototype);
+ scr_dyn_release(prototype);
+ return found;
+}
+
+/* `value instanceof callee` for an untyped constructor function. */
+bool scr_dyn_function_instanceof(ScrDyn *value, ScrDyn *callee) {
+ if (!scr_dyn_is_callable(callee)) {
+ static const char message[] = "Right-hand side of 'instanceof' is not callable";
+ scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
+ return false;
+ }
+ ScrDyn *prototype = bc_get(callee, "prototype");
+ if (!prototype) return false;
+ bool found = false;
+ if (prototype->kind == SCR_DYN_OBJ || prototype->kind == SCR_DYN_ARR || prototype->kind == SCR_DYN_FUNC ||
+ prototype->kind == SCR_DYN_PROXY) {
+ found = bc_chain_has(value, prototype);
+ } else if (value->kind == SCR_DYN_OBJ || value->kind == SCR_DYN_ARR || value->kind == SCR_DYN_FUNC) {
+ static const char message[] = "Function has non-object prototype 'undefined' in instanceof check";
+ scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
+ }
+ scr_dyn_release(prototype);
+ return found;
+}
diff --git a/packages/runtime/src/scr_error.h b/packages/runtime/src/scr_error.h
index 453ebf3e1..a7c921f23 100644
--- a/packages/runtime/src/scr_error.h
+++ b/packages/runtime/src/scr_error.h
@@ -282,6 +282,21 @@ SCR_COLD void scr_undef_global_read(ScrStr *name);
* +1 or NULL when absent (the compiler's undefined arm). The _msg_code
* thrower is scr_throw_error_msg with the code stamped on the payload. */
void scr_error_set_code(ScrError *e, const char *code);
+/* The program's `err.code = s` write into the same slot (and the error's
+ * checked-dynamic node, when it has one). Borrows both (scr_json.c). */
+void scr_error_set_code_value(ScrError *e, ScrStr *code);
+/* `err.code = v` with a value that is not statically a string: a string
+ * takes the slot as above; anything else clears the slot and lands on the
+ * error's checked-dynamic node, as a dynamic property write would.
+ * Borrows both (scr_json.c). */
+void scr_error_set_code_dyn(ScrError *e, struct ScrDyn *value);
+/* Object.assign(err, ...sources): the copy onto the error's checked-dynamic
+ * node, then the typed slots re-read from it. Borrows both (scr_json.c). */
+void scr_error_assign_dyn(ScrError *e, const struct ScrDyn *sources);
+/* `key in err` for a key no compiled class declares: a runtime-set own
+ * property or an Object.prototype member. Borrows both (scr_json.c). */
+bool scr_error_has_key(ScrError *e, const ScrStr *key);
+bool scr_error_has_code(ScrError *e);
const char *scr_error_message_code(const ScrStr *msg);
ScrStr *scr_error_code(ScrError *e);
SCR_COLD void scr_throw_error_msg_code(int kind, const char *message, size_t len, const char *code);
diff --git a/packages/runtime/src/scr_inspect.c b/packages/runtime/src/scr_inspect.c
index 3903cabeb..c1673891b 100644
--- a/packages/runtime/src/scr_inspect.c
+++ b/packages/runtime/src/scr_inspect.c
@@ -1097,6 +1097,16 @@ static ScrStr *scr_insp_class_tag(const char *cls, const char *tail) {
return ib_take(&out);
}
+/* Node prints a @@toStringTag that ends with "." and the constructor name
+ * (WebAssembly.Module for Module) in place of the name, without the
+ * bracketed tag. */
+static bool insp_tag_names_class(const char *tag, const char *cls) {
+ if (!tag) return false;
+ size_t tag_len = strlen(tag), cls_len = strlen(cls);
+ return tag_len > cls_len + 1 && tag[tag_len - cls_len - 1] == '.' &&
+ memcmp(tag + tag_len - cls_len, cls, cls_len) == 0;
+}
+
/* A native Set or Map held as a checked-dynamic value: `Set(n) { a, b }`,
* `Map(n) { k => v }`, at most 100 entries like Node's maxArrayLength. */
/* Node's formatWeakCollection: the entries of a WeakMap or WeakSet are not
@@ -1495,11 +1505,15 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) {
ScrStr *body = scr_insp_dyn(view, recurse, depth);
scr_dyn_release(view);
InspBuf out = {0};
- ib_cstr(&out, ops->cls);
- if (ops->to_string_tag) {
- ib_cstr(&out, " [");
+ if (insp_tag_names_class(ops->to_string_tag, ops->cls)) {
ib_cstr(&out, ops->to_string_tag);
- ib_char(&out, ']');
+ } else {
+ ib_cstr(&out, ops->cls);
+ if (ops->to_string_tag) {
+ ib_cstr(&out, " [");
+ ib_cstr(&out, ops->to_string_tag);
+ ib_char(&out, ']');
+ }
}
ib_char(&out, ' ');
ib_bytes(&out, body->data, body->len);
diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c
index f2b469667..9746e8eb1 100644
--- a/packages/runtime/src/scr_json.c
+++ b/packages/runtime/src/scr_json.c
@@ -4428,6 +4428,7 @@ static void scr_dyn_descriptor_fields_drop(ScrDyn **fields) {
}
static bool scr_dyn_key_is_index(const char *key, size_t len, double *out);
static void scr_error_sync_cause(ScrDyn *view, const ScrStr *key);
+static void scr_error_sync_slots(const ScrDyn *view, ScrDyn *obj, const ScrStr *key);
/* Property tables belong to closures, not temporary checked wrappers. Seed
* the configurable, non-writable builtin members once so redefinitions and
@@ -6719,6 +6720,10 @@ ScrStr *scr_dyn_format_j(const ScrDyn *d) {
typedef struct {
ScrError *err; /* retained (pins the address — no pointer reuse) */
ScrDyn *dyn; /* retained */
+ /* An own `code` that is not a string (`err.code = undefined`), retained,
+ * or NULL. The code slot holds only strings, and a subclass's typed view
+ * rebuilds its properties from the slots on every read. */
+ ScrDyn *code;
} ScrErrDynEnt;
static SCR_TL ScrErrDynEnt *scr_errdyn_cache = NULL;
@@ -6822,6 +6827,7 @@ static void scr_errdyn_sweep(void) {
for (size_t i = 0; i < n_dead; i++) {
scr_error_release(dead[i].err);
scr_errdyn_release_node(dead[i].dyn);
+ scr_dyn_release(dead[i].code);
}
free(dead);
}
@@ -6842,6 +6848,7 @@ static void scr_errdyn_append(ScrError *e, ScrDyn *d) {
}
scr_errdyn_cache[scr_errdyn_n].err = scr_error_retain(e);
scr_errdyn_cache[scr_errdyn_n].dyn = scr_dyn_retain(d);
+ scr_errdyn_cache[scr_errdyn_n].code = NULL;
scr_errdyn_n++;
if (scr_errdyn_n * 2 > scr_errdyn_slots) {
free(scr_errdyn_by_err);
@@ -6897,6 +6904,7 @@ static void scr_errdyn_teardown(void) {
for (size_t i = 0; i < n; i++) {
scr_error_release(cache[i].err);
scr_errdyn_release_node(cache[i].dyn);
+ scr_dyn_release(cache[i].code);
}
free(cache);
}
@@ -7048,7 +7056,13 @@ ScrDyn *scr_error_dyn_fields(const ScrError *e) {
/* errno rides fs errors (with syscall) and zlib errors (without). */
if (e->system_call || e->system_errno != 0)
scr_dyn_obj_set(d, "errno", 5, scr_dyn_new_num(e->system_errno));
- if (e->code) scr_dyn_obj_set(d, "code", 4, scr_dyn_new_str(e->code));
+ if (e->code) {
+ scr_dyn_obj_set(d, "code", 4, scr_dyn_new_str(e->code));
+ } else {
+ size_t hit = scr_errdyn_find(e, true);
+ if (hit != SIZE_MAX && scr_errdyn_cache[hit].code != NULL)
+ scr_dyn_obj_set(d, "code", 4, scr_dyn_retain(scr_errdyn_cache[hit].code));
+ }
if (e->system_call) scr_dyn_obj_set(d, "syscall", 7, scr_dyn_new_str(e->system_call));
}
if (!e->errno_order && e->system_path) scr_dyn_obj_set(d, "path", 4, scr_dyn_new_str(e->system_path));
@@ -7250,18 +7264,35 @@ ScrError *scr_errdyn_err_of(const ScrDyn *d) {
void scr_errdyn_put(ScrError *e, ScrDyn *d) { scr_errdyn_append(e, d); }
-/* Error.cause has one live value even when the Error already crossed a
- * checked-value boundary. Reflective data-property edits update its slot. */
-static void scr_error_sync_cause(ScrDyn *view, const ScrStr *key) {
+/* An Error's slots (name, message, stack, cause, code) have one live value
+ * even when the Error already crossed a checked-value boundary: writes and
+ * deletes of those own properties through its checked-dynamic node update
+ * the slots typed reads use. `view` is the error's node; `obj` holds the
+ * properties (the node itself, or a typed view's materialized object). */
+static void scr_error_sync_slots(const ScrDyn *view, ScrDyn *obj, const ScrStr *key) {
bool cause = key->len == 5 && memcmp(key->data, "cause", 5) == 0;
bool stack = key->len == 5 && memcmp(key->data, "stack", 5) == 0;
bool name = key->len == 4 && memcmp(key->data, "name", 4) == 0;
bool message = key->len == 7 && memcmp(key->data, "message", 7) == 0;
- if (!cause && !stack && !name && !message) return;
+ bool code = key->len == 4 && memcmp(key->data, "code", 4) == 0;
+ if (!cause && !stack && !name && !message && !code) return;
+ if (obj == NULL || obj->kind != SCR_DYN_OBJ) return;
size_t hit = scr_errdyn_find(view, false);
if (hit == SIZE_MAX) return;
- ScrDynEntry *entry = scr_dyn_entry(view, key);
+ ScrDynEntry *entry = scr_dyn_entry(obj, key);
ScrError *e = scr_errdyn_cache[hit].err;
+ if (code) {
+ /* A string takes the slot; any other value clears it and stays an own
+ * property, which the entry keeps for views rebuilt from the slots. */
+ bool string = entry && !entry->accessor && entry->value->kind == SCR_DYN_STR;
+ ScrStr *next = string ? scr_str_retain(entry->value->v.str) : NULL;
+ scr_str_release(e->code);
+ e->code = next;
+ ScrDyn *other = entry && !string && !entry->accessor ? scr_dyn_retain(entry->value) : NULL;
+ scr_dyn_release(scr_errdyn_cache[hit].code);
+ scr_errdyn_cache[hit].code = other;
+ return;
+ }
if (name || message) {
/* The native error's toString and inspect read these fields. */
if (!entry || entry->accessor || entry->value->kind != SCR_DYN_STR) return;
@@ -7292,6 +7323,10 @@ static void scr_error_sync_cause(ScrDyn *view, const ScrStr *key) {
e->cause_enumerable = entry && entry->enumerable;
}
+static void scr_error_sync_cause(ScrDyn *view, const ScrStr *key) {
+ scr_error_sync_slots(view, view, key);
+}
+
/* Error.prototype.toString over the checked-dynamic error encoding (an
* object carrying the reserved "%error" marker): `name` defaults to "Error"
* and `message` to "", both read through the prototype; either empty leaves
@@ -8323,7 +8358,12 @@ void scr_dyn_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value) {
if (scr_event_ref_hooks && scr_event_ref_hooks->set(recv, key, value)) return;
ScrDyn *materialized = scr_dyn_typed_ref_materialize(recv);
scr_dyn_key_set(materialized, key, value);
- if (!scr_exc_pending()) scr_dyn_typed_ref_commit(recv);
+ if (!scr_exc_pending()) {
+ scr_dyn_typed_ref_commit(recv);
+ /* An Error subclass's class commit writes its declared fields; the
+ * slots it inherits from Error take the write here. */
+ scr_error_sync_slots(recv, materialized, key);
+ }
scr_dyn_release(materialized);
return;
}
@@ -10097,7 +10137,10 @@ void scr_dyn_key_delete(ScrDyn *recv, const ScrStr *key, bool strict) {
scr_dyn_entry(materialized, key) != NULL;
if (!scr_exc_pending()) scr_dyn_key_delete(materialized, key, strict);
bool deleted = own && !scr_exc_pending();
- if (!scr_exc_pending()) scr_dyn_typed_ref_commit(recv);
+ if (!scr_exc_pending()) {
+ scr_dyn_typed_ref_commit(recv);
+ scr_error_sync_slots(recv, materialized, key);
+ }
scr_dyn_release(materialized);
if (deleted && scr_exc_pending()) {
/* The native layout requires this field, so the commit rejected the
@@ -12478,7 +12521,17 @@ ScrDyn *scr_dyn_assign(ScrDyn *target, const ScrDyn *src) {
if (target->kind == SCR_DYN_TYPED_REF) {
ScrDyn *materialized = scr_dyn_typed_ref_materialize(target);
ScrDyn *assigned = scr_dyn_assign(materialized, src);
- if (!scr_exc_pending()) scr_dyn_typed_ref_commit(target);
+ if (!scr_exc_pending()) {
+ scr_dyn_typed_ref_commit(target);
+ /* An Error subclass's inherited slots take the copied values. */
+ static const char *const slots[] = { "name", "message", "stack", "cause", "code" };
+ for (size_t i = 0; i < sizeof slots / sizeof slots[0]; i++) {
+ ScrStr *key = scr_str_new(slots[i], strlen(slots[i]));
+ if (materialized->kind == SCR_DYN_OBJ && scr_dyn_entry(materialized, key) != NULL)
+ scr_error_sync_slots(target, materialized, key);
+ scr_str_release(key);
+ }
+ }
scr_dyn_release(assigned);
scr_dyn_release(materialized);
return scr_exc_pending() ? NULL : scr_dyn_retain(target);
@@ -14603,3 +14656,80 @@ ScrDyn *scr_dyn_iter_take(const ScrDyn *src, size_t n, const char *spell) {
bool scr_dyn_iterates_by_protocol(const ScrDyn *value) {
return scr_dyn_sync_generator(value);
}
+
+/* The program's writes onto compiled errors (scr_error.h). Kept at the end
+ * of the file, after every function they call, so the rest of the unit
+ * emits in its existing order. */
+
+/* `err.code = s` from the program (an Error asserted to carry `code`):
+ * the same slot every coded throw stamps, so reads and `'code' in err`
+ * observe the write. Borrows both. */
+void scr_error_set_code_value(ScrError *e, ScrStr *code) {
+ scr_str_release(e->code);
+ e->code = scr_str_retain(code);
+ /* An error that already crossed into the checked-dynamic tree renders
+ * and reads through that node, so the write lands there too. */
+ ScrDyn *view = scr_errdyn_view_of(e);
+ if (view != NULL && view->kind == SCR_DYN_OBJ) scr_dyn_obj_set(view, "code", 4, scr_dyn_new_str(code));
+}
+
+void scr_error_set_code_dyn(ScrError *e, ScrDyn *value) {
+ if (value != NULL && value->kind == SCR_DYN_STR) {
+ scr_error_set_code_value(e, value->v.str);
+ return;
+ }
+ scr_str_release(e->code);
+ e->code = NULL;
+ ScrDyn *view = scr_dyn_from_error(e);
+ ScrStr *key = scr_str_new("code", 4);
+ scr_dyn_key_set(view, key, value);
+ scr_str_release(key);
+ scr_dyn_release(view);
+}
+
+/* Object.assign(err, ...sources) onto a compiled error: each source's own
+ * members land on the error's checked-dynamic node, then the slots that
+ * typed reads use (name, message, stack, cause, code) take the result.
+ * Borrows both; throws what the copy throws (a nullish source is skipped,
+ * as in JS). */
+void scr_error_assign_dyn(ScrError *e, const ScrDyn *sources) {
+ ScrDyn *view = scr_dyn_from_error(e);
+ ScrDyn *r = scr_dyn_assign_all(view, sources);
+ if (r != NULL) {
+ scr_dyn_release(r);
+ if (view->kind == SCR_DYN_OBJ) {
+ scr_error_commit_dyn(e, view);
+ if (!scr_exc_pending()) {
+ ScrDyn *code = scr_dyn_obj_get(view, "code", 4);
+ ScrStr *next = code != NULL && code->kind == SCR_DYN_STR ? scr_str_retain(code->v.str) : NULL;
+ scr_str_release(e->code);
+ e->code = next;
+ }
+ }
+ }
+ scr_dyn_release(view);
+}
+
+/* `'code' in err`: the code slot holds a string, or the error has an own
+ * `code` property with another value (`err.code = undefined`), which lives
+ * on its checked-dynamic node. Borrows; never throws. */
+bool scr_error_has_code(ScrError *e) {
+ if (e->code != NULL) return true;
+ const ScrDyn *view = scr_errdyn_view_of(e);
+ if (view == NULL) return false;
+ ScrStr *key = scr_str_new("code", 4);
+ bool own = scr_dyn_has_own(view, key);
+ scr_str_release(key);
+ return own;
+}
+
+/* `key in err` for a key no compiled class of the receiver declares: an own
+ * property set at runtime, which lives on the error's checked-dynamic node
+ * (that node exists once anything reflected or wrote through it), or a
+ * member of Object.prototype. Borrows both; never throws. */
+bool scr_error_has_key(ScrError *e, const ScrStr *key) {
+ const ScrDyn *view = scr_errdyn_view_of(e);
+ if (view != NULL && view->kind == SCR_DYN_OBJ && scr_dyn_find_entry(view, key->data, key->len) != NULL)
+ return true;
+ return scr_dyn_object_proto_name(key);
+}
diff --git a/packages/runtime/src/scr_lib.h b/packages/runtime/src/scr_lib.h
index 690f84875..bae678292 100644
--- a/packages/runtime/src/scr_lib.h
+++ b/packages/runtime/src/scr_lib.h
@@ -174,6 +174,21 @@ double scr_process_uptime(void);
/* perf_hooks performance.now(): fractional ms since process start (the
* uptime anchor — Node's timeOrigin for a compiled program). */
double scr_perf_now(void);
+/* perf_hooks event-loop metrics (scr_perf_loop.c). loop_mark records the
+ * loop's start (the emitted main calls it before the loop runs, only in
+ * programs that read the metric); loop_start answers it (0 before) and
+ * idle_time the idle ms since. The eld_* family is monitorEventLoopDelay's
+ * IntervalHistogram over a handle; eld_percentile expects 0 < p <= 100; eld_stat which: 0 min, 1 max, 2 mean, 3 stddev,
+ * 4 count, 5 exceeds. */
+void scr_perf_loop_mark(void);
+double scr_perf_loop_start(void);
+double scr_perf_idle_time(void);
+double scr_eld_new(double resolution);
+bool scr_eld_enable(double handle);
+bool scr_eld_disable(double handle);
+void scr_eld_reset(double handle);
+double scr_eld_percentile(double handle, double percentile);
+double scr_eld_stat(double handle, double which);
/* perf_hooks user timing (scr_perf.c): mark, measure, the getEntries
* family and the clears take the call's dyn argument vector (borrowed)
* and apply Node's validation and errors. mark and measure answer 1 and
diff --git a/packages/runtime/src/scr_perf_loop.c b/packages/runtime/src/scr_perf_loop.c
new file mode 100644
index 000000000..68d3bf10f
--- /dev/null
+++ b/packages/runtime/src/scr_perf_loop.c
@@ -0,0 +1,327 @@
+/* node:perf_hooks event-loop metrics: performance.eventLoopUtilization()
+ * and monitorEventLoopDelay()'s IntervalHistogram.
+ *
+ * Event-loop utilization. Node measures idle time as the time its loop
+ * spends blocked in the poll phase. A compiled program's main thread is
+ * busy exactly while it runs on the CPU (or blocks in a synchronous call),
+ * so the thread's CPU time stands in for Node's active time: idle is the
+ * wall time since the loop started minus that CPU time. The loop start is
+ * marked by the emitted main just before the loop runs, and only in
+ * programs that ask for the metric; before it, utilization is all zeros,
+ * like Node during the main module's synchronous evaluation.
+ *
+ * Event-loop delay. Each histogram samples on an unref'd interval at its
+ * resolution and records the time between samples in nanoseconds, like
+ * Node's ELDHistogram (Histogram::RecordDelta). Values land in an
+ * HdrHistogram-shaped counts array (lowest 1, three significant figures:
+ * 2048 sub-buckets per power of two), so min, max, mean, stddev and
+ * percentiles answer the bucket values Node's hdr_histogram answers. */
+
+#include "scr_runtime.h"
+
+#include