Merge main

This commit is contained in:
Chris Tate
2026-10-20 12:08:37 -05:00
258 changed files with 9519 additions and 433 deletions
@@ -1,2 +1,4 @@
- **Performance entries:** `performance.mark()`, `measure()`, and the `getEntries` methods return entries with Node's fields, `toJSON()`, inspection, and identity.
`Object.keys`, `for...in`, object spread, and `hasOwnProperty` on an entry are unsupported, and an entry stored in an `unknown` slot prints as a plain object.
- **Event-loop metrics:** `performance.eventLoopUtilization()` counts the main thread's CPU time since the event loop started as active time, so blocking system calls count as idle where Node counts them as active.
`monitorEventLoopDelay()` supports `enable()`, `disable()`, `reset()`, `percentile()`, and the `min`, `max`, `mean`, `stddev`, `count`, and `exceeds` readings; the BigInt readings, `percentiles`, and non-literal option objects are unsupported.
@@ -1,4 +1,7 @@
- **Streams and readline:** Stream chunks are Buffers or strings.
`NodeJS.ReadableStream` and `NodeJS.WritableStream`, and `stream.Readable` and `stream.Writable` under `@types/node`, accept every stream: when a program passes file, zlib, or `node:stream` streams through them, child-process pipes and `process.stdout` and `process.stderr` join those streams as `Readable` and `Writable` instances.
`readable.pipe(process.stdout)` and `readable.pipe(process.stderr)` compile and, as in Node, do not end the process stream.
A `pipeline` stage that arrives as an untyped value must be a native stream at run time; other values throw a not-supported error.
`Readable.from` accepts arrays, Sets, Map and Set iterators, generators, async generators, and class iterables and async iterables whose values are strings or Buffers, and in JavaScript any iterable or async iterable of them; a stream that mixes strings and Buffers is unsupported.
In JavaScript, a sync iterator value with a `then` method that is not a promise, and an object whose `[Symbol.asyncIterator]` or `[Symbol.iterator]` method is a generator method, are unsupported.
Object-mode streams of other values (including `Readable.from` over a `Map`, whose values are entry arrays) are unsupported.
@@ -11,6 +14,7 @@
`stream.compose` accepts two or more streams, or one `Duplex`; functions, iterables, and web streams as stages are unsupported, and the `ERR_INVALID_ARG_VALUE` message for a stream that is no longer readable or writable names the stream's class instead of printing the stream.
`pipeline` from `node:stream/promises` accepts a trailing `{ signal, end }` object, and `finished` accepts `readable`, `writable`, `error`, `cleanup`, and `signal` options, with boolean literals for the flags.
Stream constructors accept a `signal` option in an options object literal, and `stream.addAbortSignal` accepts any stream.
A `transform` or `flush` option callback whose completion callback types `data` as `any`, as @types/node's `TransformCallback` does, may pass a Buffer, a string, or nothing; any other value throws a TypeError where the callback is called, instead of Node's `ERR_INVALID_ARG_TYPE` stream error.
`node:readline` and `node:readline/promises` (also reached as `readline.promises`) interfaces, from `createInterface(options)` or `createInterface(input, output, completer, terminal)`, read `process.stdin` or a readable stream: `question`, `prompt`, `write` (with a key object literal of `name`, `ctrl`, `meta`, and `shift`), `pause`, `resume`, `'line'`/`'close'`/`'pause'`/`'resume'`/`'error'`/`'history'`/`'SIGINT'`/`'SIGTSTP'`/`'SIGCONT'` listeners, and `for await` compile.
The `history`, `historySize`, `removeHistoryDuplicates`, `completer`, `tabSize`, and `escapeCodeTimeout` options are validated as in Node.
With `terminal: true`, or with no `terminal` option and `process.stdout` as the output when it is a TTY, the interface runs Node's terminal mode: the editing keys, the kill ring, undo and redo, history recording and recall, Tab completion through the completer, Ctrl+C, Ctrl+D, and Ctrl+Z, `TERM=dumb`, and the same escape sequences written to the output.
@@ -16,7 +16,11 @@ Other immutable aliases of table-backed Node builtins support direct calls, but
Native calls accept standard process streams; custom native streams and mutations to `util.inspect.colors` remain unsupported.
`util.inherits` links the prototypes of constructor functions, and `util.deprecate`, `util.callbackify`, and `util.debuglog` return native function values that forward receivers and arguments.
Class constructors passed to `util.inherits` or `util.deprecate` are refused.
In JavaScript, a constructor function can inherit from `EventEmitter`, the legacy `Stream` constructor, or a stream class through `util.inherits` or `Object.setPrototypeOf`.
`EventEmitter.call(this)` and `Stream.call(this)` initialize the object as Node does, and its listener methods keep their state in the object's own `_events`, `_eventsCount`, and `_maxListeners` properties.
The `captureRejections` option and `events.errorMonitor` listeners are not supported on such objects.
Calling `Readable`, `Writable`, `Duplex`, `Transform`, or `PassThrough` with a constructor function's receiver throws `SC2020` where the call runs.
Other class constructors passed to `util.inherits` or `util.deprecate` are refused.
Deprecation warnings are emitted on the next tick and follow assignments to `process.noDeprecation` and `process.throwDeprecation`; compiled binaries take no Node command-line flags such as `--no-deprecation` or `--trace-deprecation`, and `process.traceDeprecation` is unsupported.
A stored `util.deprecate` reports a `length` of 4, where Node reports 3.
In TypeScript, a `util.callbackify` result must reach a callback whose error parameter accepts `null`; the @types/node overloads that type it as non-null are refused.
@@ -12,3 +12,7 @@ Message listeners may return `void` or `Promise<void>`.
Messages use newline-delimited JSON, so their static types must be JSON-stringifiable or `unknown` backed by a checked JSON tree.
`send()` returns the channel's backpressure state and invokes its callback later with `null` or an `Error`.
`disconnect()` changes `connected` synchronously, before the later disconnect events.
A `'data'` listener on a child's `stdout` or `stderr` whose chunk is `unknown` or `any` receives strings when a `setEncoding()` statement on the same stream precedes it in its block or an enclosing block, and Buffers when the function never calls `setEncoding()` on that stream.
Any other placement of `setEncoding()` is refused at compile time, and a `setEncoding()` call that runs elsewhere afterwards makes the listener throw a TypeError instead of receiving strings.
An inline `'exit'` or `'close'` listener may declare `(code: number, signal: string)`, which TypeScript accepts through the catch-all `on` overload; it receives the `null` values Node passes, and a typed use of a `null` value throws a TypeError.
@@ -9,4 +9,5 @@ JavaScript can store `process.stdin`, `stdout`, and `stderr`, replace output `wr
Input supports data/end/error listeners, pause/resume, buffered reads, and TTY raw mode.
Output writes are synchronous, with successful callbacks on the next-tick queue; queued backpressure is not modeled.
Stored streams do not support piping, encoding changes, arbitrary properties, or other events.
TypeScript output parameters support `write(string)`.
A TypeScript `NodeJS.WritableStream` parameter that only receives `process.stdout` or `process.stderr` supports `write(string)`.
When other streams flow into the same parameter, the process streams join them as `Writable` instances with the full writable surface.
@@ -3,3 +3,9 @@
Independent concrete uses have no specialization-count limit; recursively expanding demands and type layouts produce diagnostics.
Other generic calls compile when the target resolves statically.
Generic function values require a concrete signature at use and a binding that is never reassigned.
- **Variadic wrappers:** A generic higher-order wrapper is specialized to each wrapped function's exact signature.
`Parameters<F>`, `ReturnType<F>`, and rest parameters typed as fixed tuples become that signature's parameter list.
A function expression with no parameters that reads `arguments` compiles only when its value is asserted to, or contextually typed as, one fixed-arity signature; `arguments` is then that signature's tuple.
Forwarding it with `fn.apply(this, arguments)` passes `this` through.
Reading `fn.name` on a wrapped function requires its name to be known where the wrapper is called, such as a function declaration or a `const`-bound function.
`Parameters<F>` over a signature with optional or array-typed rest parameters is unsupported, because that tuple has no fixed length.
@@ -21,7 +21,7 @@ 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`.
Its contents stay readable, 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.
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.
+20 -20
View File
@@ -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
@@ -54688,9 +54688,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -54803,9 +54803,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -55286,9 +55286,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -55309,9 +55309,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -55677,9 +55677,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
"verification": "test-backed"
}
},
{
@@ -55815,9 +55815,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -7,8 +7,8 @@
"tierItemCount": 3454,
"tiers": {
"static": {
"audit-partial": 1191,
"verify-gap": 2136,
"audit-partial": 1197,
"verify-gap": 2130,
"replace-refusal": 40,
"classify": 2,
"implement": 85
@@ -54277,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"
]
}
}
},
@@ -54402,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"
]
}
}
},
@@ -54927,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"
]
}
}
},
@@ -54952,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"
]
}
}
},
@@ -55352,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"
]
}
}
},
@@ -55512,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"
]
}
}
},
@@ -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
@@ -75646,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"
@@ -77687,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"
},
@@ -77802,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"
},
@@ -78285,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"
},
@@ -78308,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"
},
@@ -78676,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"
},
@@ -78826,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"
},
@@ -139,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"],
@@ -247,6 +249,8 @@
{ "chapter": "util", "symbols": ["util.parseEnv"], "status": "partial", "evidence": ["tests/corpus/node24-env-parse.ts", "tests/corpus/node24-env-parse-errors.ts", "tests/corpus/node24-env-imports.cjs", "tests/fixtures/node-types/env-files.ts", "tests/harness/project-config.test.ts"] },
{ "chapter": "process", "symbols": ["process.memoryUsage.rss"], "status": "partial", "evidence": ["tests/corpus/5210-process-memory-usage.ts"] },
{ "chapter": "perf_hooks", "symbols": ["performance.now"], "status": "partial", "evidence": ["tests/corpus/2427-perf-hooks-now.ts", "tests/corpus/2446-performance-global.ts"] },
{ "chapter": "perf_hooks", "symbols": ["performance.eventLoopUtilization"], "status": "partial", "evidence": ["tests/corpus/6397-event-loop-metrics.ts", "tests/fixtures/node-types/event-loop-metrics.ts"] },
{ "chapter": "perf_hooks", "symbols": ["histogram.enable", "histogram.disable", "histogram.percentile", "histogram.reset"], "status": "partial", "evidence": ["tests/corpus/6397-event-loop-metrics.ts", "tests/fixtures/node-types/event-loop-metrics.ts"] },
{ "chapter": "perf_hooks", "symbols": ["performance.mark", "performance.measure", "performance.getEntries", "performance.getEntriesByName", "performance.getEntriesByType", "performance.clearMarks", "performance.clearMeasures"], "status": "partial", "evidence": ["tests/corpus/5212-perf-marks-measures.ts", "tests/corpus/5213-perf-timing-errors.ts", "tests/corpus/5214-perf-entry-inspect.ts", "tests/corpus/5215-perf-timing-validation.js"] },
{ "chapter": "perf_hooks", "symbols": ["PerformanceEntry", "PerformanceMark", "PerformanceMeasure"], "status": "partial", "evidence": ["tests/corpus/5212-perf-marks-measures.ts", "tests/corpus/5214-perf-entry-inspect.ts"] },
{ "chapter": "perf_hooks", "parent": "Class: `PerformanceEntry`", "symbols": ["duration", "entryType", "name", "startTime"], "status": "partial", "evidence": ["tests/corpus/5212-perf-marks-measures.ts", "tests/corpus/5214-perf-entry-inspect.ts"] },
+72 -8
View File
@@ -80,21 +80,54 @@ declare namespace NodeJS {
path?: string | undefined;
syscall?: string | undefined;
}
/* The piped child-output stream (child.stdout / child.stderr under
* stdio ["ignore", "pipe", "pipe"]): 'data' fires one Buffer chunk per
* read (≤64KB, consumer-driven — no listener, no read), 'end' fires
* once at EOF and always BEFORE the child's 'exit'. A flowing stream
* keeps the loop alive. */
/* The readable-stream interface every Node stream implements. The
* piped child-output stream (child.stdout / child.stderr under stdio
* ["ignore", "pipe", "pipe"]) is declared with it: 'data' fires one
* Buffer chunk per read (≤64KB, consumer-driven — no listener, no
* read), 'end' fires once at EOF and always BEFORE the child's 'exit'. A
* flowing stream keeps the loop alive. A program that also passes
* other streams (fs, zlib, node:stream) where this interface is
* expected compiles it as the node:stream Readable, with child output
* bridged into one. */
interface ReadableStream {
on(event: "data", listener: (chunk: Buffer) => void): void;
on(event: "data", listener: (chunk: string) => void): void;
on(event: "end", listener: () => void): void;
on(event: string, listener: (...args: any[]) => void): void;
once(event: "data", listener: (chunk: Buffer) => void): void;
once(event: "data", listener: (chunk: string) => void): void;
once(event: "end", listener: () => void): void;
once(event: string, listener: (...args: any[]) => void): void;
/* setEncoding('utf8') makes subsequent data chunks strings, including
* multibyte sequences split across native pipe reads. */
setEncoding(encoding: string): this;
readonly readable: boolean;
pause(): this;
resume(): this;
isPaused(): boolean;
pipe<T extends WritableStream>(destination: T, options?: { end?: boolean }): T;
unpipe(destination?: WritableStream): this;
[Symbol.asyncIterator](): AsyncIterableIterator<string | Buffer>;
}
/* The writable-stream interface every Node stream implements:
* process.stdout/process.stderr, fs.WriteStream and the node:stream
* Writable family. Written only to the process streams, it compiles as
* their descriptor; with other streams flowing into it, as the
* node:stream Writable (the process streams convert into one). */
interface WritableStream {
readonly writable: boolean;
write(data: string | Uint8Array, callback?: (error?: Error | null) => void): boolean;
write(
data: string | Uint8Array,
encoding: BufferEncoding,
callback?: (error?: Error | null) => void,
): boolean;
end(callback?: () => void): void;
end(data: string | Uint8Array, callback?: () => void): void;
end(data: string | Uint8Array, encoding: BufferEncoding, callback?: () => void): void;
/* Node's EventEmitter signature: listeners take untyped arguments. */
on(event: string, listener: (...args: any[]) => void): void;
once(event: string, listener: (...args: any[]) => void): void;
}
}
@@ -1271,8 +1304,37 @@ declare module "node:perf_hooks" {
getEntriesByType(type: EntryType): PerformanceEntry[];
clearMarks(name?: string): void;
clearMeasures(name?: string): void;
eventLoopUtilization(
utilization1?: EventLoopUtilization,
utilization2?: EventLoopUtilization,
): EventLoopUtilization;
}
export const performance: Performance;
/* Event-loop metrics: utilization (idle/active ms since the loop started)
* and monitorEventLoopDelay's sampling histogram (nanoseconds). */
export interface EventLoopUtilization {
idle: number;
active: number;
utilization: number;
}
export interface EventLoopMonitorOptions {
resolution?: number | undefined;
}
export interface Histogram {
readonly count: number;
readonly exceeds: number;
readonly max: number;
readonly mean: number;
readonly min: number;
readonly stddev: number;
percentile(percentile: number): number;
reset(): void;
}
export interface IntervalHistogram extends Histogram {
enable(): boolean;
disable(): boolean;
}
export function monitorEventLoopDelay(options?: EventLoopMonitorOptions): IntervalHistogram;
}
declare module "perf_hooks" {
export * from "node:perf_hooks";
@@ -3066,8 +3128,10 @@ declare module "child_process" {
destroy(): void;
on(event: "drain" | "finish", listener: () => void): void;
on(event: "error", listener: (err: Error) => void): void;
on(event: string, listener: (...args: any[]) => void): void;
once(event: "drain" | "finish", listener: () => void): void;
once(event: "error", listener: (err: Error) => void): void;
once(event: string, listener: (...args: any[]) => void): void;
}
export interface ChildProcess extends Disposable {
@@ -6414,7 +6478,7 @@ declare module "node:cluster" {
/* node:tty — the fd probe (the same isatty(3) behind process.*.isTTY)
* and the supported structural view of a process output stream. */
declare module "tty" {
export interface WriteStream {
export interface WriteStream extends NodeJS.WritableStream {
write(data: string | Uint8Array, callback?: (error?: Error | null) => void): boolean;
write(
data: string | Uint8Array,
@@ -6664,8 +6728,8 @@ declare module "stream" {
resume(): this;
isPaused(): boolean;
setEncoding(encoding: string): this;
pipe<T extends Writable>(destination: T, options?: { end?: boolean }): T;
unpipe(destination?: Writable): this;
pipe<T extends NodeJS.WritableStream>(destination: T, options?: { end?: boolean }): T;
unpipe(destination?: NodeJS.WritableStream): this;
destroy(error?: Error): this;
readonly readable: boolean;
readonly readableEnded: boolean;
@@ -32,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,
@@ -46,5 +47,6 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures {
tlsCa: features.tlsCa,
cipher: features.cipher,
webcrypto: features.webcrypto,
perfLoop: features.perfLoop,
};
}
@@ -42,6 +42,7 @@ export async function compilePackedLibrary(
bigint: options.bigint ?? false,
searchParams: options.searchParams ?? false,
qs: false,
dynBuiltinClass: false,
parseArgs: false,
stream: false,
net: false,
@@ -55,6 +56,7 @@ export async function compilePackedLibrary(
tls: false,
cipher: false,
webcrypto: false,
perfLoop: false,
tlsCa: false,
};
const selection = await loadRuntimePack({
@@ -1,4 +1,5 @@
/* LlEmitter's module-level output: module parts, helper definitions, library declarations and FFI callback definitions. */
import { emitWasmModules } from "./wasm-modules.js";
import { type LlEmitter, ffiCallbackDummyLl, llStrBytes } from "./emitter.js";
import {
mangleAsyncSpawn,
@@ -29,7 +30,6 @@ import {
import { emitLlvmLayouts } from "./layouts.js";
import { moduleRuntimeFeatures } from "../../ir/runtime-features.js";
import { emitArraySearchHelpers } from "./array-search.js";
import { emitWasmModules } from "./wasm-modules.js";
import { emitClassMembershipHelper } from "./classes.js";
import { type RegexMatcherCode, compileRegexMatcher } from "./regex-matchers.js";
import {
@@ -313,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()`);
}
@@ -877,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`,
@@ -182,6 +182,7 @@ import {
genResultThunkFor,
childExitThunkFor,
childExitSignalThunkFor,
childDataDynThunkFor,
childDataThunkFor,
execFileThunkFor,
ipcMessageThunkFor,
@@ -4213,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 &&
@@ -5576,6 +5591,10 @@ export class LlEmitter {
return childDataThunkFor(this, param);
}
childDataDynThunkFor(chunk: "bytes" | "str"): string {
return childDataDynThunkFor(this, chunk);
}
emitterFixedAdapter(cbT: IrType & { kind: "func" }): { fn: string; shim: string } {
return emitterFixedAdapter(this, cbT);
}
@@ -771,6 +771,31 @@ export function childDataThunkFor(host: LlvmEmitterContext, param: IrType): stri
return sym;
}
/** The child-stream 'data' thunk for an unknown/any chunk parameter: the
* runtime's Buffer (or, after setEncoding, string) chunk boxes into a
* checked-dynamic value the listener owns. */
export function childDataDynThunkFor(host: LlvmEmitterContext, chunk: "bytes" | "str"): string {
const key = `cdd:${chunk}`;
let sym = host.resolveThunks.get(key);
if (sym) return sym;
sym = `sc_cdd_${host.resolveThunks.size}`;
host.resolveThunks.set(key, sym);
const box = chunk === "bytes" ? "scr_dyn_new_buffer" : "scr_dyn_new_str";
host.declare(`declare ptr @${box}(ptr)`);
host.resolveThunkDefs.push(
`define internal void @${sym}(ptr %cb, ptr %chunk) ${FN_ATTRS} { ; child 'data' → dyn (${chunk})`,
`entry:`,
` %d = call ptr @${box}(ptr %chunk)`,
` %fnp = getelementptr inbounds %ScrClosure, ptr %cb, i64 0, i32 1`,
` %fn = load ptr, ptr %fnp`,
` call void %fn(ptr %cb, ptr %d)`,
` ret void`,
`}`,
``,
);
return sym;
}
export function emitterFixedAdapter(
host: LlvmEmitterContext,
cbT: IrType & { kind: "func" },
@@ -17,7 +17,7 @@ export function streamDataAdapter(
const p = cbT.params[0];
if (
cbT.params.length > 1 ||
(p && p.kind !== "bytes" && p.kind !== "string" && p.kind !== "dyn")
(p && p.kind !== "bytes" && p.kind !== "string" && p.kind !== "dyn" && p.kind !== "union")
) {
throw new InternalCompilerError(
"llvm emitter bug: stream data listener param shape (frontend must fence)",
@@ -73,6 +73,46 @@ export function streamDataAdapter(
);
finish("%r0");
d.push(`}`, ``);
} else if (p.kind === "union") {
// A `Buffer | string` chunk: wrap whichever slot the stream filled at
// its arm of the (boxed) union.
const def = host.unionsById.get(p.unionId);
const bytesTag = def ? def.arms.findIndex((a) => a.kind === "bytes" && a.elem === "u8") : -1;
const strTag = def ? def.arms.findIndex((a) => a.kind === "string") : -1;
if (
!def ||
host.nullableUnions.has(p.unionId) ||
bytesTag < 0 ||
strTag < 0 ||
def.arms.length !== 2
)
throw new InternalCompilerError(
"llvm emitter bug: stream data listener union is not Buffer | string (frontend must fence)",
);
host.declare(`declare ptr @scr_union_new_ref(i32, ptr, ptr, ptr, ptr)`);
host.declare(`declare ptr @scr_bytes_retain_v(ptr)`);
host.declare(`declare void @scr_bytes_release_v(ptr)`);
host.declare(`declare ptr @scr_str_retain_v(ptr)`);
host.declare(`declare void @scr_str_release_v(ptr)`);
d.push(
` %uslot = alloca ptr`,
` %isb = icmp ne ptr %a0, null`,
` br i1 %isb, label %buf, label %str`,
`buf:`,
` %rb = call ptr @scr_bytes_retain_v(ptr %a0)`,
` %ub = call ptr @scr_union_new_ref(i32 ${bytesTag}, ptr %rb, ptr @scr_bytes_retain_v, ptr @scr_bytes_release_v, ptr null)`,
` store ptr %ub, ptr %uslot`,
` br label %go`,
`str:`,
` %rs = call ptr @scr_str_retain_v(ptr %a1)`,
` %us = call ptr @scr_union_new_ref(i32 ${strTag}, ptr %rs, ptr @scr_str_retain_v, ptr @scr_str_release_v, ptr null)`,
` store ptr %us, ptr %uslot`,
` br label %go`,
`go:`,
` %uv = load ptr, ptr %uslot`,
);
finish("%uv");
d.push(`}`, ``);
} else {
// dyn: box by runtime tag — the JS lane's adapter parameter.
host.declare(`declare ptr @scr_dyn_new_buffer(ptr)`);
@@ -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]!);
@@ -278,6 +278,14 @@ export const LIB_FN_SYMS: Record<string, string> = {
"perf.entryDetail": "scr_perf_entry_detail",
"perf.entryKeep": "scr_perf_entry_keep",
"perf.entryRef": "scr_perf_entry_ref",
"perf.loopStart": "scr_perf_loop_start",
"perf.idleTime": "scr_perf_idle_time",
"perf.eldNew": "scr_eld_new",
"perf.eldEnable": "scr_eld_enable",
"perf.eldDisable": "scr_eld_disable",
"perf.eldReset": "scr_eld_reset",
"perf.eldPercentile": "scr_eld_percentile",
"perf.eldStat": "scr_eld_stat",
"process.memoryUsageTake": "scr_process_memory_usage_take",
"process.memoryUsageField": "scr_process_memory_usage_field",
"process.rss": "scr_process_rss",
@@ -727,6 +735,8 @@ export const LIB_FN_SYMS: Record<string, string> = {
"child.unref": "scr_child_unref",
"child.ref": "scr_child_ref",
"procStream.write": "scr_proc_stream_write",
"stream.stdioWritable": "scr_stream_stdio_writable",
"stream.unboxDyn": "scr_stream_unbox_dyn",
"process.kill": "scr_process_kill_named",
"process.killNum": "scr_process_kill",
// fs.promises (scr_bytes_io.c / scr_lib.c): the promise forms settle
@@ -1128,6 +1138,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
"readable.opFind": "scr_stream_op_find",
"readable.opReduce": "scr_stream_op_reduce",
"readable.pipe": "scr_stream_pipe",
"readable.pipeGeneral": "scr_stream_pipe_general",
"writable.cork": "scr_stream_cork",
"writable.uncork": "scr_stream_uncork",
"stream.destroyErr": "scr_stream_destroy",
@@ -1442,6 +1453,9 @@ export const LIB_FN_SYMS: Record<string, string> = {
"dyn.classPrototype": "scr_dyn_class_prototype",
"dyn.classBasePrototype": "scr_dyn_class_base_prototype",
"dyn.classInherit": "scr_dyn_class_inherit",
"dyn.builtinClass": "scr_dyn_builtin_class",
"dyn.builtinInstanceOf": "scr_dyn_builtin_instanceof",
"dyn.functionInstanceOf": "scr_dyn_function_instanceof",
"dyn.classSuper": "scr_dyn_class_super",
"dyn.assignPrototype": "scr_dyn_assign_prototype",
"dyn.definePrototypeProps": "scr_dyn_define_prototype_props",
@@ -1614,6 +1628,7 @@ export const USES_TIMERS_LIB_FNS = new Set<string>([
"readable.opReduce",
"readable.resume",
"readable.pipe",
"readable.pipeGeneral",
"readable.unpipe",
"writable.write",
"writable.writeStr",
@@ -56,6 +56,8 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV
host.declare(`declare void @scr_child_stream_thunk0(ptr, ptr)`);
} else if (param.kind === "union") {
adapter = host.childDataThunkFor(param);
} else if (param.kind === "dyn") {
adapter = host.childDataDynThunkFor("bytes");
} else {
adapter = "scr_child_stream_thunk_bytes";
host.declare(`declare void @scr_child_stream_thunk_bytes(ptr, ptr)`);
@@ -78,7 +80,8 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV
if (e.fn === "stream.onDataStr") {
host.usesTimers = true;
const cbT = e.args[1]!.type;
if (cbT.kind !== "func" || cbT.params[0]?.kind !== "string") {
const strParam = cbT.kind === "func" ? cbT.params[0]?.kind : undefined;
if (strParam !== "string" && strParam !== "dyn") {
throw new InternalCompilerError(
"llvm emitter bug: stream.onDataStr callback not a string func",
);
@@ -87,10 +90,12 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV
const cb = host.emitExpr(e.args[1]!);
const once = host.emitExpr(e.args[2]!);
host.toHeap(cb);
host.declare(`declare void @scr_child_stream_thunk_str(ptr, ptr)`);
let adapter = "scr_child_stream_thunk_str";
if (strParam === "dyn") adapter = host.childDataDynThunkFor("str");
else host.declare(`declare void @scr_child_stream_thunk_str(ptr, ptr)`);
host.declare(`declare void @scr_child_stream_on_data_str(ptr, ptr, ptr, i1 zeroext)`);
B.line(
`call void @scr_child_stream_on_data_str(ptr ${s0.name}, ptr ${cb.name}, ptr @scr_child_stream_thunk_str, i1 ${once.name})`,
`call void @scr_child_stream_on_data_str(ptr ${s0.name}, ptr ${cb.name}, ptr @${adapter}, i1 ${once.name})`,
);
return { name: "", type: e.type };
}
@@ -68,8 +68,8 @@ export function emitWasmModules(
out.push(`@scr_wasm_registry = constant { i64, ptr } { i64 0, ptr null }`);
} else {
out.push(
`@scr_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 @scr_wasm_registry_modules }`,
`@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("");
@@ -49,6 +49,7 @@ export interface NativeLinkFeatures {
bigint: boolean;
searchParams: boolean;
qs: boolean;
dynBuiltinClass: boolean;
parseArgs: boolean;
stream: boolean;
net: boolean;
@@ -64,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. */
@@ -152,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;
@@ -216,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
@@ -196,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"),
@@ -236,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.
@@ -107,6 +107,7 @@ const features = {
bigint: false,
searchParams: false,
qs: false,
dynBuiltinClass: false,
parseArgs: false,
stream: false,
net: false,
@@ -121,6 +122,7 @@ const features = {
tlsCa: false,
cipher: false,
webcrypto: false,
perfLoop: false,
};
test.each([
@@ -44,6 +44,7 @@ const BASE: NativeLinkFeatures = {
bigint: false,
searchParams: false,
qs: false,
dynBuiltinClass: false,
parseArgs: false,
stream: false,
net: false,
@@ -58,6 +59,7 @@ const BASE: NativeLinkFeatures = {
tlsCa: false,
cipher: false,
webcrypto: false,
perfLoop: false,
};
async function fixture(target: NativeTargetSpec = MACOS_ARM64_TARGET) {
@@ -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,
},
});
@@ -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<{
@@ -72,6 +72,7 @@ export interface EarlyExecutableNativeFeatures {
bigint: boolean;
searchParams: boolean;
qs: boolean;
dynBuiltinClass: boolean;
parseArgs: boolean;
stream: boolean;
net: boolean;
@@ -84,6 +85,7 @@ export interface EarlyExecutableNativeFeatures {
nodeTest: boolean;
tls: boolean;
cipher: boolean;
perfLoop: boolean;
tlsCa: boolean;
webcrypto: boolean;
}
@@ -190,6 +192,7 @@ const BOOLEAN_NATIVE_KEYS = [
"bigint",
"searchParams",
"qs",
"dynBuiltinClass",
"parseArgs",
"stream",
"net",
@@ -204,6 +207,7 @@ const BOOLEAN_NATIVE_KEYS = [
"tlsCa",
"cipher",
"webcrypto",
"perfLoop",
] as const satisfies readonly (keyof EarlyExecutableNativeFeatures)[];
function validNativeFeatures(value: unknown): value is EarlyExecutableNativeFeatures {
@@ -8,6 +8,7 @@ import {
type IrType,
isIntegerBytesElem,
type SrcLoc,
STRING,
UNDEFINED_T,
typeEquals,
typeKey,
@@ -84,29 +85,54 @@ export function arrayValueRead(
};
}
/** Read a stabilized union of native arrays without copying either the
* receiver or its elements. Every arm contributes its missing-value case. */
/** Read one position of a stabilized union of native arrays and tuples
* (`const [a, b] = re.exec(s) || ["", ""]`) without copying the receiver
* or its elements. Every array arm contributes its missing-value case; a
* tuple arm answers its field, or undefined past its end. Tuple arms need
* a constant position. With `stringChars` (the string iterator's code
* points as a fresh string[]), a string arm (`re.exec(s) || ""`) answers
* its code point at the position, or undefined past its end. */
export function unionArrayValueRead(
lowerer: Lowerer,
value: IrExpr,
index: IrExpr,
loc: SrcLoc,
stringChars?: (text: IrExpr) => IrExpr,
): IrExpr | null {
if (value.type.kind !== "union") return null;
const unionId = value.type.unionId;
const arms = lowerer.unions.get(unionId)?.arms;
if (!arms?.length) return null;
const position = index.kind === "numLit" ? String(index.value) : null;
// A tuple arm's answer at the position: its field, or undefined past
// its end. Null when the arm is neither an array nor a tuple.
const tupleField = (arm: IrType): { name: string; type: IrType } | "past" | null => {
if (arm.kind !== "record" || position === null) return null;
const shape = lowerer.shapes.get(arm.shapeId);
if (!shape?.tuple) return null;
return shape.fields.find((f) => f.name === position) ?? "past";
};
// Keep the collection's IrType union layout and narrow each element at
// its use; an inferred array predicate changes the collection's ABI.
for (const arm of arms) if (arm.kind !== "array") return null;
const stringArm = (arm: IrType): boolean => arm.kind === "string" && stringChars !== undefined;
for (const arm of arms)
if (arm.kind !== "array" && !stringArm(arm) && tupleField(arm) === null) return null;
const answers = new Map<string, IrType>();
for (const arm of arms) {
if (arm.kind !== "array") return null;
const answer = arrayValueType(lowerer, arm.elem);
const field = arm.kind === "array" || stringArm(arm) ? null : tupleField(arm);
const answer =
arm.kind === "array"
? arrayValueType(lowerer, arm.elem)
: stringArm(arm)
? arrayValueType(lowerer, STRING)
: field === "past"
? UNDEFINED_T
: field!.type;
const parts = answer.kind === "union" ? lowerer.unions.get(answer.unionId)!.arms : [answer];
for (const part of parts) answers.set(typeKey(part), part);
}
const joined = [...answers.values()].sort((a, b) => (typeKey(a) < typeKey(b) ? -1 : 1));
if (joined.length === 1 && joined[0]!.kind === "undefinedT") return null;
if (
joined.length > 1 &&
(!unionContainerArmsOk(joined) ||
@@ -118,12 +144,39 @@ export function unionArrayValueRead(
let result: IrExpr | null = null;
for (let tag = arms.length - 1; tag >= 0; tag--) {
const arm = arms[tag]!;
if (arm.kind !== "array") return null;
const receiver: IrExpr = { kind: "unionNarrow", unionId, tag, value, type: arm, loc };
const read = lowerer.coerceToExpected(
arrayValueRead(lowerer, receiver, index, arm.elem, loc),
type,
);
let read: IrExpr;
if (arm.kind === "array") {
read = lowerer.coerceToExpected(
arrayValueRead(lowerer, receiver, index, arm.elem, loc),
type,
);
} else if (stringArm(arm)) {
read = lowerer.coerceToExpected(
arrayValueRead(lowerer, stringChars!(receiver), index, STRING, loc),
type,
);
} else {
const field = tupleField(arm);
if (field === null) return null;
if (field === "past") {
const missing = lowerer.wrappedUndefined(type, loc);
if (!missing) return null;
read = missing;
} else {
read = lowerer.coerceToExpected(
{
kind: "recordGet",
obj: receiver,
shapeId: (arm as IrType & { kind: "record" }).shapeId,
field: field.name,
type: field.type,
loc,
},
type,
);
}
}
result =
result === null
? read
@@ -1,4 +1,5 @@
import { lowerReadlineCursorCall } from "./builtins/readline.js";
import { lowerMonitorEventLoopDelayCall } from "./builtins/performance.js";
import {
lowerEnvironmentData,
lowerReceiveMessageOnPort,
@@ -104,6 +105,9 @@ export function lowerBuiltinCall(
return lowerAddAbortListener(lowerer, expr, loc);
}
if (bi.module === "events" && bi.member === "once") return lowerEventsOnce(lowerer, expr, loc);
if (bi.module === "perf_hooks" && bi.member === "monitorEventLoopDelay") {
return lowerMonitorEventLoopDelayCall(lowerer, expr);
}
if (bi.module === "events") {
const helper = lowerEventsEmitterHelper(lowerer, expr, bi.member, loc);
if (helper) return helper;
@@ -0,0 +1,149 @@
import { numLit } from "../../../ir/build.js";
import { BOOL, DYN, RUNTIME_EMITTER_CLASS, type IrExpr, type SrcLoc } from "../../../ir/ir.js";
import * as ts from "../../ts7/adapter.js";
import { locOf } from "../../program.js";
import type { Lowerer } from "../lowerer.js";
/* EventEmitter and the stream classes as untyped constructor values, the
* bases of JavaScript constructor functions:
*
* function Bus() { EventEmitter.call(this); }
* util.inherits(Bus, EventEmitter);
*
* A compiled `class Bus extends EventEmitter` gets a native emitter prefix,
* but `new Bus()` on a constructor function builds an untyped object, so
* the base is the runtime's dyn constructor (scr_dyn_builtin_class.c): a
* function whose prototype carries Node's EventEmitter methods over the
* object's own `_events` table. The stream classes have prototype chains
* for util.inherits and instanceof; constructing over them refuses where
* it runs. These values appear only where a constructor function uses its
* base: util.inherits, `Base.call(this, …)` and `Base.apply(this, …)`,
* `Base.prototype` and `instanceof Base` on an untyped value. */
/** scr_dyn_builtin_class's kind for a runtime class, or null. */
export function builtinClassKind(className: string): number | null {
switch (className) {
case RUNTIME_EMITTER_CLASS:
return 0;
case "%Readable":
return 2;
case "%Writable":
return 3;
case "%Duplex":
return 4;
case "%Transform":
return 5;
case "%PassThrough":
return 6;
default:
return null;
}
}
/** The classes by builtin module member. JavaScript sources bind
* `require("events").EventEmitter` as a module member rather than a class
* value, and `require("stream")` is the legacy Stream constructor. */
const MODULE_CLASS_KINDS: Readonly<Record<string, Readonly<Record<string, number>>>> = {
events: { EventEmitter: 0, default: 0 },
stream: { Stream: 1, Readable: 2, Writable: 3, Duplex: 4, Transform: 5, PassThrough: 6 },
};
/** The kind of a builtin module member that is one of those classes. */
export function builtinMemberClassKind(module: string, member: string): number | null {
return MODULE_CLASS_KINDS[module]?.[member] ?? null;
}
function unparenthesized(node: ts.Expression): ts.Expression {
let current = node;
while (ts.isParenthesizedExpression(current)) current = current.expression;
return current;
}
/** The kind of a node that names one of those classes: a class value, a
* builtin module member, or the events and stream modules themselves. */
export function builtinClassKindOf(lowerer: Lowerer, node: ts.Expression): number | null {
const expr = unparenthesized(node);
const mapped = lowerer.mapTypeOf(lowerer.typeOf(expr));
if (mapped?.kind === "classval") return builtinClassKind(mapped.className);
const member = ts.isIdentifier(expr)
? lowerer.builtinImportOf(expr)
: ts.isPropertyAccessExpression(expr)
? lowerer.builtinMemberOf(expr)
: null;
if (member) return MODULE_CLASS_KINDS[member.module]?.[member.member] ?? null;
const module = lowerer.builtinNamespaceModuleOf(expr);
return module === "events" ? 0 : module === "stream" ? 1 : null;
}
/** Whether evaluating the node can be skipped: a binding or a member of
* one, or of a builtin require. */
function referenceOnly(node: ts.Expression): boolean {
const expr = unparenthesized(node);
if (ts.isIdentifier(expr)) return true;
if (ts.isPropertyAccessExpression(expr) && !expr.questionDotToken) {
const base = unparenthesized(expr.expression);
return (
ts.isIdentifier(base) ||
(ts.isCallExpression(base) &&
ts.isIdentifier(base.expression) &&
base.expression.text === "require" &&
base.arguments.nodes.length === 1 &&
ts.isStringLiteral(base.arguments.nodes[0]!))
);
}
return false;
}
/** The untyped constructor named by a node, or null when it names none of
* those classes. */
export function builtinClassDynValueOf(lowerer: Lowerer, node: ts.Expression): IrExpr | null {
const kind = builtinClassKindOf(lowerer, node);
if (kind === null) return null;
const loc = locOf(node);
const value = builtinClassValue(kind, loc);
if (referenceOnly(node)) return value;
return {
kind: "seqExpr",
stmts: [{ kind: "exprStmt", expr: lowerer.lowerExpr(node), loc }],
result: value,
type: DYN,
loc,
};
}
export function builtinClassValue(kind: number, loc: SrcLoc): IrExpr {
return { kind: "libCall", fn: "dyn.builtinClass", args: [numLit(kind, loc)], type: DYN, loc };
}
/** `Base.call(this, …)` / `Base.apply(this, args)` on one of those class
* values: Function.prototype.call/apply over the untyped constructor. */
export function lowerBuiltinClassCall(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null {
const callee = call.expression;
if (!ts.isPropertyAccessExpression(callee) || callee.questionDotToken) return null;
const method = callee.name.text;
if (method !== "call" && method !== "apply") return null;
if (call.arguments.nodes.some(ts.isSpreadElement)) return null;
const receiver = builtinClassDynValueOf(lowerer, callee.expression);
if (receiver === null) return null;
const loc = locOf(call);
return {
kind: "dynInvoke",
recv: receiver,
method,
calleeName: callee.getText(),
args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)),
type: DYN,
loc,
};
}
/** `value instanceof Base` for an untyped value. */
export function builtinClassInstanceOf(value: IrExpr, kind: number, loc: SrcLoc): IrExpr {
return {
kind: "libCall",
fn: "dyn.builtinInstanceOf",
args: [value, numLit(kind, loc)],
type: BOOL,
loc,
};
}
@@ -1,5 +1,6 @@
import { boolLit, strLit } from "../../../ir/build.js";
import * as ts from "../../ts7/adapter.js";
import { requestGeneralStreamSurface } from "../general-streams.js";
import { type Lowerer } from "../lowerer.js";
import { isJsSourceFile, locOf } from "../../program.js";
import { isChildSurfaceMember } from "../surfaces.js";
@@ -131,8 +132,16 @@ export function lowerChildMethodCall(
) {
for (const param of listenerNode.parameters.nodes) lowerer.checkedCallbackParams.add(param);
}
const cb = lowerer.lowerExpr(call.arguments.nodes[1]!);
const narrowed =
event === "exit" || event === "close"
? narrowedExitListenerParams(lowerer, listenerNode)
: [];
const cb =
narrowed.length > 0
? lowerWithCheckedParams(lowerer, listenerNode, narrowed)
: lowerer.lowerExpr(call.arguments.nodes[1]!);
if (
narrowed.length > 0 ||
(isJsSourceFile(listenerNode.getSourceFile()) && cb.type.kind === "dyn") ||
(cb.type.kind === "func" &&
(cb.type.rest ||
@@ -301,10 +310,12 @@ export function lowerChildMethodCall(
/** Method calls on piped child-output stream receivers (child.stdout /
* child.stderr — the childStream kind): on/once("data" | "end").
* 'data' listeners take zero parameters, `(chunk: Buffer)`, or a
* `Buffer | string`-union chunk (the ngrok appendOutput shape — the
* runtime only ever fires Buffers; the compiler-emitted adapter wraps
* the chunk at the union's Buffer arm); 'end' listeners take none.
* 'data' listeners take zero parameters, `(chunk: Buffer)`, `(chunk:
* string)`, an unknown/any chunk (boxed as a checked-dynamic value; see
* childStreamEncodedAt), or a `Buffer | string`-union chunk (the ngrok
* appendOutput shape — the runtime only ever fires Buffers; the
* compiler-emitted adapter wraps the chunk at the union's Buffer arm);
* 'end' listeners take none.
* Statement position only (Node returns the stream for chaining; here
* the result is void). Chained receivers (`child.stdout?.on(...)`)
* ride the optional-chain re-dispatch (chainBlocked). Everything else
@@ -335,6 +346,7 @@ export function lowerChildStreamMethodCall(
const evT = lowerer.typeOf(call.arguments.nodes[0]!);
const event = evT.isStringLiteralType() ? evT.value : null;
if (event !== "data" && event !== "end") {
requestGeneralStreamSurface(lowerer, access.expression);
lowerer.noLowering(
`stream.${name}(${event === null ? "non-literal event" : `"${event}"`}, ...)`,
call.arguments.nodes[0]!,
@@ -342,6 +354,7 @@ export function lowerChildStreamMethodCall(
);
}
if (!ts.isExpressionStatement(call.parent)) {
requestGeneralStreamSurface(lowerer, access.expression);
lowerer.unsupported(
"SC1090",
call,
@@ -376,23 +389,32 @@ export function lowerChildStreamMethodCall(
if (
param !== undefined &&
param.kind !== "string" &&
param.kind !== "dyn" &&
!(param.kind === "bytes" && param.elem === "u8") &&
!unionOk(param)
) {
lowerer.unsupported(
"SC1090",
call.arguments.nodes[1]!,
`data listeners whose parameter is not 'Buffer', 'string', or a Buffer-armed union (got '${lowerer.fmt(param)}')`,
`data listeners whose parameter is not 'Buffer', 'string', 'unknown', or a Buffer-armed union (got '${lowerer.fmt(param)}')`,
);
}
// An unknown/any chunk is a Buffer, or a string once setEncoding ran.
// The registration takes whichever the enclosing code establishes; a
// later setEncoding that contradicts it throws the runtime's
// listener-type TypeError instead of delivering the other kind.
const strings =
param?.kind === "string" ||
(param?.kind === "dyn" && childStreamEncodedAt(lowerer, access.expression, call));
return {
kind: "libCall",
fn: param?.kind === "string" ? "stream.onDataStr" : "stream.onData",
fn: strings ? "stream.onDataStr" : "stream.onData",
args: [receiver, cb, once],
type: VOID,
loc,
};
}
requestGeneralStreamSurface(lowerer, access.expression);
lowerer.noLowering(
`ReadableStream.${name}`,
call,
@@ -418,6 +440,7 @@ export function lowerChildWriterMethodCall(
if (name === "write") {
if (call.arguments.nodes.length !== 1 || call.arguments.nodes.some(ts.isSpreadElement)) {
requestGeneralStreamSurface(lowerer, access.expression);
lowerer.noLowering(
`child stdin write with ${call.arguments.nodes.length} arguments`,
call,
@@ -461,6 +484,7 @@ export function lowerChildWriterMethodCall(
const eventType = lowerer.typeOf(call.arguments.nodes[0]!);
const event = eventType.isStringLiteralType() ? eventType.value : null;
if (event !== "drain" && event !== "finish" && event !== "error") {
requestGeneralStreamSurface(lowerer, access.expression);
lowerer.noLowering(
`child stdin ${name}(${event === null ? "non-literal event" : `"${event}"`}, ...)`,
call.arguments.nodes[0]!,
@@ -507,6 +531,7 @@ export function lowerChildWriterMethodCall(
return { kind: "libCall", fn, args: [receiver, cb, once], type: VOID, loc };
}
requestGeneralStreamSurface(lowerer, access.expression);
lowerer.noLowering(
`Writable.${name}`,
call,
@@ -514,3 +539,255 @@ export function lowerChildWriterMethodCall(
lowerer.checker.getSymbolAtLocation(access.name),
);
}
/** A receiver spelled as a plain access path (`child.stdout`, `out`): its
* root symbol and member names, or null for other forms. A `const` alias
* of an access path (`const out = child.stdout`) resolves to that path,
* and so does a method call declared to return `this`
* (`child.stdout.setEncoding("utf8")`), so every spelling of one stream
* gets the same key. */
function accessPathOf(
lowerer: Lowerer,
expr: ts.Expression,
depth = 0,
): { root: ts.Symbol; path: string } | null {
if (depth > 16) return null;
if (ts.isParenthesizedExpression(expr) || ts.isNonNullExpression(expr)) {
return accessPathOf(lowerer, expr.expression, depth + 1);
}
if (ts.isIdentifier(expr)) {
const root = lowerer.checker.getSymbolAtLocation(expr);
if (!root) return null;
const init = constInitializerOf(root);
const aliased = init === null ? null : accessPathOf(lowerer, init, depth + 1);
return aliased ?? { root, path: "" };
}
if (ts.isPropertyAccessExpression(expr) && !expr.questionDotToken) {
const base = accessPathOf(lowerer, expr.expression, depth + 1);
return base === null ? null : { root: base.root, path: `${base.path}.${expr.name.text}` };
}
if (returnsReceiver(lowerer, expr)) {
return accessPathOf(
lowerer,
(expr.expression as ts.PropertyAccessExpression).expression,
depth + 1,
);
}
return null;
}
/** A method call (`recv.m(...)`, not optional) whose declaration returns
* `this`, so its value is the receiver itself. */
function returnsReceiver(lowerer: Lowerer, expr: ts.Expression): expr is ts.CallExpression {
if (
!ts.isCallExpression(expr) ||
expr.questionDotToken ||
!ts.isPropertyAccessExpression(expr.expression) ||
expr.expression.questionDotToken
)
return false;
const declaration = lowerer.checker.getResolvedSignature(expr)?.declaration;
return declaration?.type?.kind === ts.SyntaxKind.ThisType;
}
/** A call of a child_process factory (spawn, fork, exec, execFile), each
* of which returns a new ChildProcess. */
function createsChildProcess(lowerer: Lowerer, call: ts.CallExpression): boolean {
const declaration = lowerer.checker.getResolvedSignature(call)?.declaration;
if (
declaration === undefined ||
!ts.isFunctionDeclaration(declaration) ||
declaration.name === undefined ||
!["spawn", "fork", "exec", "execFile"].includes(declaration.name.text)
)
return false;
for (let node: ts.Node | undefined = declaration.parent; node; node = node.parent) {
if (ts.isModuleDeclaration(node) && ts.isStringLiteral(node.name)) {
return node.name.text === "child_process" || node.name.text === "node:child_process";
}
}
return false;
}
/** The statement that always evaluates `call` when it runs: the call is
* the statement's expression, a variable's initializer, or the receiver of
* a method chain that is one of those. Null for calls under a condition,
* an operator, an argument list or any other construct. */
function unconditionalStatementOf(call: ts.CallExpression): ts.Statement | null {
let node: ts.Node = call;
for (;;) {
const parent: ts.Node | undefined = node.parent;
if (parent === undefined) return null;
if (
ts.isParenthesizedExpression(parent) ||
ts.isNonNullExpression(parent) ||
(ts.isPropertyAccessExpression(parent) &&
parent.expression === node &&
!parent.questionDotToken) ||
(ts.isCallExpression(parent) && parent.expression === node && !parent.questionDotToken)
) {
node = parent;
continue;
}
if (ts.isExpressionStatement(parent)) return parent;
if (ts.isVariableDeclaration(parent) && parent.initializer === node) {
const list = parent.parent;
const statement = list?.parent;
return list !== undefined &&
ts.isVariableDeclarationList(list) &&
statement !== undefined &&
ts.isVariableStatement(statement)
? statement
: null;
}
return null;
}
}
/** The initializer of a symbol declared once as a `const` variable, else null. */
function constInitializerOf(symbol: ts.Symbol): ts.Expression | null {
const decls = symbol.declarations;
if (decls.length !== 1) return null;
const decl = decls[0]!;
if (
!ts.isVariableDeclaration(decl) ||
!ts.isIdentifier(decl.name) ||
decl.initializer === undefined ||
(ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) === 0
)
return null;
return decl.initializer;
}
/** Whether two resolved access paths name different objects: the same root
* with different members, or two `const` roots each initialized by `new`
* or a child_process factory (such as two `spawn()` results). Any other
* call may return an object another binding already holds. */
function distinctPaths(
lowerer: Lowerer,
a: { root: ts.Symbol; path: string },
b: { root: ts.Symbol; path: string },
): boolean {
if (a.root === b.root) return a.path !== b.path;
const fresh = (root: ts.Symbol): boolean => {
let init = constInitializerOf(root);
while (init !== null && ts.isParenthesizedExpression(init)) init = init.expression;
if (init === null) return false;
return (
ts.isNewExpression(init) || (ts.isCallExpression(init) && createsChildProcess(lowerer, init))
);
};
return fresh(a.root) && fresh(b.root);
}
/** Whether an unknown-typed 'data' listener registered by `call` receives
* strings: a `setEncoding` statement on the same receiver path precedes
* the registration in its own block or an enclosing block of the same
* function. With no setEncoding on that path in the function, chunks are
* Buffers. Any other placement leaves the encoding state unclear, which
* is reported. */
function childStreamEncodedAt(
lowerer: Lowerer,
receiver: ts.Expression,
call: ts.CallExpression,
): boolean {
const key = accessPathOf(lowerer, receiver);
const statement: ts.Node = call.parent!;
let scope: ts.Node | undefined = statement;
while (scope && !ts.isFunctionLike(scope) && !ts.isSourceFile(scope)) scope = scope.parent;
const unclear = (): never =>
lowerer.unsupported(
"SC1090",
call.arguments.nodes[1]!,
"untyped child-stream 'data' listeners whose setEncoding state is unclear at their registration sites (declare the chunk as 'Buffer' or 'string')",
);
if (key === null || scope === undefined) return unclear();
const preceding = new Set<ts.Node>();
for (let node: ts.Node = statement; node !== scope;) {
const parent: ts.Node | undefined = node.parent;
if (parent === undefined) break;
if (ts.isBlock(parent) || ts.isSourceFile(parent)) {
for (const sibling of parent.statements.nodes) {
if (sibling === node) break;
preceding.add(sibling);
}
}
node = parent;
}
const receiverType = lowerer.checker.getNonNullableType(lowerer.typeOf(receiver));
let before = false;
let elsewhere = false;
const visit = (node: ts.Node): void => {
if (node !== scope && ts.isFunctionLike(node)) return;
if (
ts.isCallExpression(node) &&
ts.isPropertyAccessExpression(node.expression) &&
node.expression.name.text === "setEncoding"
) {
const target = node.expression.expression;
const path = accessPathOf(lowerer, target);
if (path !== null && path.root === key.root && path.path === key.path) {
const statement = unconditionalStatementOf(node);
if (statement !== null && preceding.has(statement)) before = true;
else elsewhere = true;
} else if (
(path === null || !distinctPaths(lowerer, path, key)) &&
lowerer.checker.isTypeAssignableTo(receiverType, lowerer.typeOf(target))
) {
// Another spelling that can hold this stream (a parameter, a `let`
// copy, a call result): its setEncoding may be this stream's.
elsewhere = true;
}
}
ts.forEachChild(node, visit);
};
visit(scope);
if (elsewhere) return unclear();
return before;
}
/** An inline 'exit'/'close' listener may declare `(code: number, signal:
* string)`: TypeScript accepts it through ChildProcess's catch-all `on`
* overload, while Node passes `number | null` and `Signals | null`. Those
* parameters bind as checked-dynamic values (the socket 'data' listener
* precedent), so the body observes the null Node passes; a typed use of a
* null value throws the checked conversion's TypeError. Returns the
* narrowed parameter symbols, empty when the declared types already
* admit null. */
function narrowedExitListenerParams(lowerer: Lowerer, listener: ts.Expression): ts.Symbol[] {
if (!ts.isArrowFunction(listener) && !ts.isFunctionExpression(listener)) return [];
if (isJsSourceFile(listener.getSourceFile())) return [];
const symbols: ts.Symbol[] = [];
const wanted: readonly string[] = ["f64", "string"];
listener.parameters.nodes.slice(0, 2).forEach((param, i) => {
if (!ts.isIdentifier(param.name) || param.dotDotDotToken || param.initializer) return;
if (param.questionToken) return;
const symbol = lowerer.checker.getSymbolAtLocation(param.name);
if (!symbol || lowerer.runtimeOptionalBindingTypes.has(symbol)) return;
if (lowerer.paramShape(param).type.kind !== wanted[i]) return;
symbols.push(symbol);
});
return symbols;
}
/** Lowers an inline listener with the given parameters bound as
* checked-dynamic values typed `unknown` for the body. */
function lowerWithCheckedParams(
lowerer: Lowerer,
listener: ts.Expression,
symbols: readonly ts.Symbol[],
): IrExpr {
const previousImplicit = lowerer.implicitParamTypes;
const implicit = new Map(previousImplicit ?? []);
for (const symbol of symbols) {
implicit.set(symbol, lowerer.checker.getUnknownType());
lowerer.runtimeOptionalBindingTypes.set(symbol, DYN);
}
lowerer.implicitParamTypes = implicit;
try {
return lowerer.lowerExpr(listener);
} finally {
for (const symbol of symbols) lowerer.runtimeOptionalBindingTypes.delete(symbol);
lowerer.implicitParamTypes = previousImplicit;
}
}
@@ -18,6 +18,7 @@ import {
} from "../../../ir/ir.js";
import { PERF_ENTRY_FIELDS, PERF_ENTRY_MARKER, perfEntryJsonShape } from "../../type-mapper.js";
import { builtinImportOf } from "./module-bindings.js";
import { lowerOptionalArgument } from "../optional-arguments.js";
import { checkedIterableSpread } from "../checked-iterable-spread.js";
/** True for any spelling of node:perf_hooks' `performance` object — the
@@ -134,6 +135,12 @@ export function lowerPerfHooksCall(
loc,
};
}
if (
access.name.text === "eventLoopUtilization" &&
isPerfHooksPerformanceExpr(lowerer, access.expression)
) {
return lowerEventLoopUtilization(lowerer, expr);
}
const timeline = PERF_TIMELINE_CALLS.get(access.name.text);
if (timeline && isPerfHooksPerformanceExpr(lowerer, access.expression)) {
return lowerPerfTimelineCall(lowerer, expr, access.name.text, timeline, loc);
@@ -142,7 +149,7 @@ export function lowerPerfHooksCall(
lowerer.noLowering(
`perf_hooks performance.${access.name.text}`,
expr,
"performance.now() (and its .bind(performance) function value), mark, measure, the getEntries family, clearMarks and clearMeasures are the lowered surface",
"performance.now() (and its .bind(performance) function value), mark, measure, the getEntries family, clearMarks, clearMeasures and eventLoopUtilization are the lowered surface",
);
}
return null;
@@ -513,3 +520,305 @@ export function fencePerfEntryKeyWalk(
`${what} over performance entries (Node keeps their fields as prototype getters, so own-key walks see none; read the fields or call toJSON())`,
);
}
/* ── event-loop metrics: eventLoopUtilization and monitorEventLoopDelay ── */
/** The stdlib IntervalHistogram (monitorEventLoopDelay's result), by the
* receiver's checker type. Its value is the runtime histogram handle. */
function isIntervalHistogram(lowerer: Lowerer, node: ts.Expression): boolean {
const symbol = lowerer.typeOf(node).getSymbol();
return symbol?.name === "IntervalHistogram" && lowerer.isStdlibSymbol(symbol);
}
/** IntervalHistogram's numeric members, as perf.eldStat selectors. */
const HISTOGRAM_STATS: ReadonlyMap<string, number> = new Map([
["min", 0],
["max", 1],
["mean", 2],
["stddev", 3],
["count", 4],
["exceeds", 5],
]);
/** `h.min`, `h.max`, … on an IntervalHistogram: the runtime's current
* statistic (Node's hdr_histogram values; nanoseconds). Null for every
* other read. */
export function lowerHistogramProperty(
lowerer: Lowerer,
expr: ts.PropertyAccessExpression,
): IrExpr | null {
const which = HISTOGRAM_STATS.get(expr.name.text);
if (
which === undefined ||
expr.questionDotToken ||
!isIntervalHistogram(lowerer, expr.expression)
)
return null;
const loc = locOf(expr);
return {
kind: "libCall",
fn: "perf.eldStat",
args: [lowerer.lowerExprExpecting(expr.expression, F64), numLit(which, loc)],
type: F64,
loc,
};
}
/** enable(), disable(), reset() and percentile(p) on an IntervalHistogram.
* Other members (the BigInt twins, the percentiles Map) fence by name. */
export function lowerHistogramMethodCall(
lowerer: Lowerer,
expr: ts.CallExpression,
access: ts.PropertyAccessExpression,
): IrExpr | null {
if (access.questionDotToken || !isIntervalHistogram(lowerer, access.expression)) return null;
const loc = locOf(expr);
const name = access.name.text;
const handle = (): IrExpr => lowerer.lowerExprExpecting(access.expression, F64);
const args = expr.arguments.nodes;
if ((name === "enable" || name === "disable" || name === "reset") && args.length === 0) {
const fn =
name === "enable"
? "perf.eldEnable"
: name === "disable"
? "perf.eldDisable"
: "perf.eldReset";
return { kind: "libCall", fn, args: [handle()], type: name === "reset" ? VOID : BOOL, loc };
}
if (name === "percentile" && args.length === 1 && !ts.isSpreadElement(args[0]!)) {
// Node validates first: NaN, p <= 0 and p > 100 throw ERR_OUT_OF_RANGE.
const h = lowerer.declareHiddenLocal("%histogram", F64);
const p = lowerer.declareHiddenLocal("%percentile", F64);
const pRef = (): IrExpr => varRef(p.id, F64, loc);
const cmp = (op: "!==" | "<=" | ">", left: IrExpr, right: IrExpr): IrExpr => ({
kind: "bin",
op,
left,
right,
type: BOOL,
loc,
});
const or = (left: IrExpr, right: IrExpr): IrExpr => ({
kind: "logical",
op: "||",
left,
right,
type: BOOL,
loc,
});
const outOfRange = or(
or(cmp("!==", pRef(), pRef()), cmp("<=", pRef(), numLit(0, loc))),
cmp(">", pRef(), numLit(100, loc)),
);
const message: IrExpr = {
kind: "strConcat",
left: {
kind: "strLit",
value: 'The value of "percentile" is out of range. It must be > 0 && <= 100. Received ',
type: STRING,
loc,
},
right: { kind: "toString", operand: pRef(), type: STRING, loc },
type: STRING,
loc,
};
const thrown: IrExpr = {
kind: "libCall",
fn: "error.nodeThrow",
args: [
numLit(2, loc),
{ kind: "strLit", value: "ERR_OUT_OF_RANGE", type: STRING, loc },
message,
],
type: F64,
loc,
};
return {
kind: "seqExpr",
stmts: [
{ kind: "varDecl", localId: h.id, init: handle(), loc },
{ kind: "varDecl", localId: p.id, init: lowerer.lowerExprExpecting(args[0]!, F64), loc },
],
result: {
kind: "ternary",
cond: outOfRange,
then: thrown,
else_: {
kind: "libCall",
fn: "perf.eldPercentile",
args: [varRef(h.id, F64, loc), pRef()],
type: F64,
loc,
},
type: F64,
loc,
},
type: F64,
loc,
};
}
lowerer.noLowering(
`IntervalHistogram.${name}`,
expr,
"enable(), disable(), reset(), percentile(p) and the min, max, mean, stddev, count and exceeds readings are the lowered surface",
);
}
/** `monitorEventLoopDelay(options?)` from node:perf_hooks, in any import
* spelling (`lowerBuiltinCall`): a new sampling histogram handle. The
* options are an inline object literal; the runtime validates the
* resolution as Node does (an integer of at least 1). */
export function lowerMonitorEventLoopDelayCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
const loc = locOf(expr);
const args = expr.arguments.nodes;
let resolution: IrExpr = numLit(10, loc);
if (args.length > 1 || args.some(ts.isSpreadElement)) {
lowerer.noLowering(
"this monitorEventLoopDelay call form",
expr,
"pass at most one options object",
);
}
if (args.length === 1) {
const options = args[0]!;
if (!ts.isObjectLiteralExpression(options)) {
lowerer.noLowering(
"monitorEventLoopDelay with a non-literal options object",
options,
"spell the options inline: monitorEventLoopDelay({ resolution: 20 })",
);
}
for (const property of options.properties.nodes) {
const shorthand = ts.isShorthandPropertyAssignment(property);
if (
(!shorthand && !ts.isPropertyAssignment(property)) ||
!ts.isIdentifier(property.name) ||
property.name.text !== "resolution"
) {
lowerer.noLowering(
"monitorEventLoopDelay options other than resolution",
property,
"resolution is the one option",
);
}
// An undefined resolution takes the default, as Node's destructuring.
resolution = lowerOptionalArgument(
lowerer,
shorthand ? property.name : property.initializer,
F64,
numLit(10, loc),
);
}
}
return { kind: "libCall", fn: "perf.eldNew", args: [resolution], type: F64, loc };
}
/** performance.eventLoopUtilization(u1?, u2?) — Node's
* lib/internal/perf/event_loop_utilization.js over the runtime's loop
* start and idle time, one lifted helper per arity. */
function lowerEventLoopUtilization(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
const loc = locOf(expr);
const args = expr.arguments.nodes;
const resultT = lowerer.mapTypeOf(lowerer.typeOf(expr));
if (resultT?.kind !== "record" || args.length > 2 || args.some(ts.isSpreadElement)) {
lowerer.noLowering("this performance.eventLoopUtilization call form", expr);
}
const helper = eluHelperOf(lowerer, args.length, resultT);
return {
kind: "call",
callee: helper,
args: args.map((arg) => lowerer.lowerExprExpecting(arg, resultT)),
type: resultT,
loc,
};
}
function eluHelperOf(lowerer: Lowerer, arity: number, recT: IrType & { kind: "record" }): string {
const name = `%perf.elu.${arity}`;
if (lowerer.liftedFns.some((f) => f.name === name)) return name;
const loc: SrcLoc = { file: "<builtin>", start: 0, end: 0 };
const f64Local = (id: string) => ({ id, name: id, type: F64, mutable: false });
const params = Array.from({ length: arity }, (_, i) => ({
localId: `u${i + 1}.0`,
name: `u${i + 1}`,
type: recT as IrType,
}));
const locals = [
...params.map((p) => ({ id: p.localId, name: p.name, type: recT as IrType, mutable: false })),
f64Local("ls.0"),
f64Local("idle.0"),
f64Local("active.0"),
];
const ref = (id: string): IrExpr => varRef(id, F64, loc);
const field = (param: string, f: string): IrExpr => ({
kind: "recordGet",
obj: varRef(param, recT, loc),
shapeId: recT.shapeId,
field: f,
type: F64,
loc,
});
const bin = (op: "+" | "-" | "/" | "<=", left: IrExpr, right: IrExpr): IrExpr => ({
kind: "bin",
op,
left,
right,
type: op === "<=" ? BOOL : F64,
loc,
});
const record = (idle: IrExpr, active: IrExpr, utilization: IrExpr): IrExpr => ({
kind: "recordLit",
fields: [
{ name: "idle", value: idle },
{ name: "active", value: active },
{ name: "utilization", value: utilization },
],
type: recT,
loc,
});
const result = (idle: IrExpr, active: IrExpr): IrStmt => ({
kind: "return",
value: record(idle, active, bin("/", active, bin("+", idle, active))),
loc,
});
const decl = (id: string, init: IrExpr): IrStmt => ({ kind: "varDecl", localId: id, init, loc });
const body: IrStmt[] = [
decl("ls.0", { kind: "libCall", fn: "perf.loopStart", args: [], type: F64, loc }),
{
kind: "if",
cond: bin("<=", ref("ls.0"), numLit(0, loc)),
then: [
{ kind: "return", value: record(numLit(0, loc), numLit(0, loc), numLit(0, loc)), loc },
],
else_: null,
loc,
},
];
if (arity === 2) {
body.push(
decl("idle.0", bin("-", field("u1.0", "idle"), field("u2.0", "idle"))),
decl("active.0", bin("-", field("u1.0", "active"), field("u2.0", "active"))),
result(ref("idle.0"), ref("active.0")),
);
} else {
body.push(
decl("idle.0", { kind: "libCall", fn: "perf.idleTime", args: [], type: F64, loc }),
decl(
"active.0",
bin(
"-",
bin("-", { kind: "libCall", fn: "perf.now", args: [], type: F64, loc }, ref("ls.0")),
ref("idle.0"),
),
),
arity === 0
? result(ref("idle.0"), ref("active.0"))
: result(
bin("-", ref("idle.0"), field("u1.0", "idle")),
bin("-", ref("active.0"), field("u1.0", "active")),
),
);
}
lowerer.liftedFns.push({ name, params, returnType: recT, locals, body, loc });
return name;
}
@@ -3,6 +3,7 @@ import { envSnapshotHelper, lowerProcessLoadEnvFile } from "./environment.js";
import { dynUndefinedExpr, boolLit, numLit, strLit, varRef } from "../../../ir/build.js";
import { timerStyleCallback } from "../lower-timers.js";
import * as ts from "../../ts7/adapter.js";
import { requestGeneralStreamSurface } from "../general-streams.js";
import { type Lowerer, own } from "../lowerer.js";
import { isJsSourceFile, locOf } from "../../program.js";
import { isSafeToDiscard } from "../expressions/evaluation-safety.js";
@@ -225,6 +226,7 @@ export function lowerProcStreamMethodCall(
const receiver = lowerer.lowerExpr(access.expression);
const data = lowerer.lowerExpr(call.arguments.nodes[0]!);
if (data.type.kind !== "string") {
requestGeneralStreamSurface(lowerer, access.expression);
lowerer.noLowering(
`write of '${lowerer.fmt(data.type)}' data on a stream value`,
call.arguments.nodes[0]!,
@@ -233,6 +235,7 @@ export function lowerProcStreamMethodCall(
}
return { kind: "libCall", fn: "procStream.write", args: [receiver, data], type: BOOL, loc };
}
requestGeneralStreamSurface(lowerer, access.expression);
lowerer.noLowering(
`WritableStream.${name}`,
call,
@@ -1,5 +1,5 @@
import { isBuiltinClassInstance } from "./receiver-types.js";
import { strLit } from "../../../ir/build.js";
import { numLit, strLit } from "../../../ir/build.js";
import * as ts from "../../ts7/adapter.js";
import { type Lowerer } from "../lowerer.js";
import { locOf } from "../../program.js";
@@ -192,6 +192,25 @@ export function lowerBuiltinExtraProperty(
// +1-or-NULL runtime pair.
if (kind === "child" && (name === "stdin" || name === "stdout" || name === "stderr")) {
const receiver = lowerer.lowerExpr(expr.expression);
// Under the general stream representation (general-streams.ts) the
// declared stream type maps to a runtime stream class: the read
// answers the child's bridged stream (one per stdio slot, so repeated
// reads keep identity), fed by the same pipe.
const general = lowerer.generalNodeStreams ? generalChildStdioType(lowerer, expr, name) : null;
if (general) {
const type: IrType = {
kind: "union",
unionId: lowerer.unions.intern([general, { kind: "nullT" }]),
};
const read: IrExpr = {
kind: "libCall",
fn: "child.stdioStream",
args: [receiver, numLit(name === "stdin" ? 0 : name === "stdout" ? 1 : 2, loc)],
type,
loc,
};
return lowerer.maybeNarrow(read, expr);
}
const streamType = name === "stdin" ? CHILDWRITER_T : CHILDSTREAM_T;
const type: IrType = {
kind: "union",
@@ -259,3 +278,21 @@ export function lowerBuiltinExtraProperty(
}
return null;
}
/** The runtime stream class a child stdio read produces under the general
* stream representation, or null when the declared type keeps the stdio
* handle (the fallback declarations' ChildStdin). */
function generalChildStdioType(
lowerer: Lowerer,
expr: ts.PropertyAccessExpression,
name: "stdin" | "stdout" | "stderr",
): IrType | null {
const className = name === "stdin" ? "%Writable" : "%Readable";
const mapped = lowerer.mapTypeOf(lowerer.typeOf(expr));
if (!mapped) return null;
const arms =
mapped.kind === "union" ? (lowerer.unions.get(mapped.unionId)?.arms ?? []) : [mapped];
return arms.some((arm) => arm.kind === "object" && arm.className === className)
? { kind: "object", className }
: null;
}
@@ -1,3 +1,4 @@
import { forwardedTupleType, pureForwardedValues, stripTypeWrappers } from "./typed-arguments.js";
import { dynUndefinedExpr } from "../../ir/build.js";
import { InternalCompilerError } from "../../errors.js";
import * as ts from "../ts7/adapter.js";
@@ -26,13 +27,13 @@ import { checkedIterableSpread } from "./checked-iterable-spread.js";
import { type ParamShape } from "./call-signatures.js";
import { arrayValueRead, arrayValueType } from "./array-values.js";
import { buildArrayConversion } from "./coercions/builders.js";
import { typeKey } from "../type-mapper.js";
import { restTupleElementTypes, typeKey } from "../type-mapper.js";
function fixedTupleSpreadInfo(
lowerer: Lowerer,
node: ts.Expression,
): { type: IrType & { kind: "record" }; fields: { name: string; type: IrType }[] } | null {
const type = lowerer.mapTypeOf(lowerer.typeOf(node));
const type = forwardedTupleType(lowerer, node) ?? lowerer.mapTypeOf(lowerer.typeOf(node));
if (type?.kind !== "record") return null;
const shape = lowerer.shapes.get(type.shapeId);
if (!shape?.tuple || shape.fields.length === 0) return null;
@@ -42,6 +43,18 @@ function fixedTupleSpreadInfo(
};
}
/** `...t` over a provably EMPTY tuple bound to a plain name (`[]`, a rest
* tuple whose type parameter or `Parameters<F>` resolved to no positions):
* it contributes no arguments, and reading the name has no effect. */
function emptyTupleSpread(lowerer: Lowerer, node: ts.Expression): boolean {
let operand = node;
while (ts.isParenthesizedExpression(operand)) operand = operand.expression;
if (!ts.isIdentifier(operand)) return false;
const forwarded = forwardedTupleType(lowerer, operand);
if (forwarded) return forwarded.kind === "array";
return restTupleElementTypes(lowerer.typeOf(operand), lowerer.typeCtx)?.length === 0;
}
export function completeArgs(
lowerer: Lowerer,
argNodes: readonly ts.Expression[],
@@ -59,7 +72,9 @@ export function completeArgs(
// graph. Ordinary calls and fixed tuples never need that analysis.
const runtimeSpreads = argNodes.filter(
(arg): arg is ts.SpreadElement =>
ts.isSpreadElement(arg) && !fixedTupleSpreadInfo(lowerer, arg.expression),
ts.isSpreadElement(arg) &&
!fixedTupleSpreadInfo(lowerer, arg.expression) &&
!emptyTupleSpread(lowerer, arg.expression),
);
// Spread operands lowered while choosing the completion, each exactly once.
const loweredSpreads = new Map<ts.Expression, IrExpr>();
@@ -190,6 +205,12 @@ export function completeArgs(
sources.push(arg);
continue;
}
if (emptyTupleSpread(lowerer, arg.expression)) continue;
const pure = pureForwardedValues(lowerer, arg.expression);
if (pure) {
for (const value of pure) sources.push({ ir: value });
continue;
}
const tuple = fixedTupleSpreadInfo(lowerer, arg.expression);
if (!tuple) {
sources.push(arg);
@@ -204,7 +225,13 @@ export function completeArgs(
// values as separately written arguments while the saved tuple stays
// alive through the call.
const spreadLoc = locOf(arg);
const value = lowerer.coerceInto(arg.expression, lowerer.lowerExpr(arg.expression), tuple.type);
// A forwarded tuple seen through type assertions (`...(arguments as
// unknown as Parameters<F>)`) spreads the tuple itself.
const operand =
forwardedTupleType(lowerer, arg.expression) !== null
? stripTypeWrappers(arg.expression)
: arg.expression;
const value = lowerer.coerceInto(arg.expression, lowerer.lowerExpr(operand), tuple.type);
const saved = lowerer.declareHiddenLocal("%callSpread", tuple.type);
const savedRef = (): IrExpr => ({
kind: "varRef",
@@ -506,9 +533,38 @@ export function completeFuncValueArgs(
shapes.push({ type: rest, mode: "rest" });
return completeArgs(lowerer, call.arguments.nodes, shapes, loc, call);
}
// A fixed-tuple spread (`fn(...args)` over a tuple-typed rest binding)
// has a compile-time arity: completion flattens it into the positional
// slots exactly like a direct call's.
if (
funcType.rest !== true &&
call.arguments.nodes.some(ts.isSpreadElement) &&
call.arguments.nodes.every(
(arg) =>
!ts.isSpreadElement(arg) ||
fixedTupleSpreadInfo(lowerer, arg.expression) !== null ||
emptyTupleSpread(lowerer, arg.expression),
)
) {
const shapes: ParamShape[] = funcType.params.map((type) => ({
type,
mode: omittedArgFor(lowerer, type, loc) ? "omittable" : "required",
}));
return completeArgs(lowerer, call.arguments.nodes, shapes, loc, call);
}
const args = call.arguments.nodes.map((arg, index) =>
lowerer.lowerExprExpecting(arg, funcType.params[index]),
);
// Effect-free surplus arguments of a fixed-arity value (`(r: void) =>
// void` drops its void slot) evaluate to nothing observable: drop them,
// as the callee ignores them.
while (
funcType.rest !== true &&
args.length > funcType.params.length &&
isSafeToDiscard(args[args.length - 1]!)
) {
args.pop();
}
for (let i = args.length; i < funcType.params.length; i++) {
const absent = omittedArgFor(lowerer, funcType.params[i]!, loc);
if (!absent) {
@@ -1,3 +1,8 @@
import {
typedArgumentsCandidate,
typedArgumentsShapes,
typedArgumentsTarget,
} from "./typed-arguments.js";
import { InternalCompilerError } from "../../errors.js";
import * as ts from "../ts7/adapter.js";
import { bodyReadsArguments } from "../arguments-usage.js";
@@ -15,6 +20,7 @@ import {
import { isJsSourceFile, isNodeEsmFile } from "../program.js";
import {
genResultRecord,
restTupleParamTypes,
isAnyArrayType,
isUnitOnlyTsType,
jsOpenObjectType,
@@ -56,6 +62,10 @@ export interface ParamShape {
bodyType?: IrType;
callDefault?: IrExpr;
entryNarrow?: IrType;
/** One flattened position of a fixed-tuple rest parameter
* (restTupleShapes): the declaration's last parameter owns every shape
* from its index on, and the body binds the tuple (declareParams). */
restTuple?: true;
}
export interface FnSig {
@@ -373,6 +383,28 @@ export function isThisParameter(param: ts.ParameterDeclaration): boolean {
return ts.isIdentifier(param.name) && param.name.text === "this";
}
/** A TypeScript rest parameter bound to a name whose type is a fixed tuple
* (`...args: [A, B]`, `...args: A` with A bound to a tuple, `...args:
* Parameters<F>` with F bound): one required shape per position. Call
* sites and function values see the equivalent fixed signature; the body
* rebuilds the tuple (declareParams) unless it only forwards it. Null for
* every other parameter. */
export function restTupleShapes(
lowerer: Lowerer,
param: ts.ParameterDeclaration,
tsType?: ts.Type,
): ParamShape[] | null {
if (
param.dotDotDotToken === undefined ||
!ts.isIdentifier(param.name) ||
isJsSourceFile(param.getSourceFile())
) {
return null;
}
const elems = restTupleParamTypes(tsType ?? lowerer.typeOf(param.name), lowerer.typeCtx);
return elems ? elems.map((type) => ({ type, mode: "required", restTuple: true })) : null;
}
/** ParamShapes for a whole parameter list. */
export function paramShapes(
lowerer: Lowerer,
@@ -380,21 +412,47 @@ export function paramShapes(
signature?: ts.Signature,
blameOf?: (param: ts.ParameterDeclaration, index: number) => ts.Node,
): ParamShape[] {
if (!signature)
return params.filter((p) => !isThisParameter(p)).map((param) => lowerer.paramShape(param));
const out: ParamShape[] = [];
if (!signature) {
for (const param of params) {
if (isThisParameter(param)) continue;
const flattened = restTupleShapes(lowerer, param);
if (flattened) out.push(...flattened);
else out.push(lowerer.paramShape(param));
}
return out;
}
const symbols = signature.getParameters();
return params.map((declParam, i) => {
const shapeAt = (declParam: ts.ParameterDeclaration, i: number): ParamShape => {
const stage = lowerer.stageSourceType(declParam.name);
if (stage) return { type: stage, mode: "required" as const };
const symbol = symbols[i];
const tsType = symbol
? lowerer.checker.getTypeOfSymbol(symbol)
: lowerer.typeOf(declParam.name);
// A call-site instantiation whose concrete argument type does not map
// is that argument's blocker: name it there, with its concrete type,
// rather than as the declaration's unbound type parameter.
if (
blameOf !== undefined &&
!isJsSourceFile(declParam.getSourceFile()) &&
(tsType.flags & ts.TypeFlags.Any) === 0 &&
declParam.type !== undefined &&
mentionsTypeParameter(lowerer, lowerer.checker.getTypeFromTypeNode(declParam.type)) &&
lowerer.mapTypeOf(tsType) === null
) {
lowerer.badType(blameOf(declParam, i), tsType);
}
let mapped = jsOpenObjectType(
declParam,
lowerer.runtimeOptionalBindingType(
declParam.name,
lowerer.mapTypeOf(tsType) ?? lowerer.irTypeOf(declParam.name),
lowerer.mapTypeOf(tsType) ??
// A TypeScript instantiation at `any` (a type argument inferred
// from an untyped value) takes the checked-dynamic slot.
((tsType.flags & ts.TypeFlags.Any) !== 0 && !isJsSourceFile(declParam.getSourceFile())
? DYN
: lowerer.irTypeOf(declParam.name)),
),
lowerer.shapes,
lowerer.unions,
@@ -428,7 +486,32 @@ export function paramShapes(
);
}
return { type: mapped, mode: declParam.questionToken ? "omittable" : "required" };
};
params.forEach((declParam, i) => {
const symbol = symbols[i];
const flattened = restTupleShapes(
lowerer,
declParam,
symbol ? lowerer.checker.getTypeOfSymbol(symbol) : undefined,
);
if (flattened) out.push(...flattened);
else out.push(shapeAt(declParam, i));
});
return out;
}
/** A declared type that is (or is a union over) a bare type parameter
* or an instantiation of a generic alias/interface over type arguments. */
function mentionsTypeParameter(lowerer: Lowerer, t: ts.Type): boolean {
if (t.flags & ts.TypeFlags.TypeParameter) return true;
const mentions = (part: ts.Type): boolean => mentionsTypeParameter(lowerer, part);
if (t.isUnionType()) return ts.constituentTypes(t).some(mentions);
if (t.getAliasTypeArguments().some(mentions)) return true;
return (
(t.flags & ts.TypeFlags.Object) !== 0 &&
((t as ts.ObjectType).objectFlags & ts.ObjectFlags.Reference) !== 0 &&
lowerer.checker.getTypeArguments(t as ts.TypeReference).some(mentions)
);
}
/** A default expression may itself yield undefined. Keep that arm in the
@@ -828,6 +911,16 @@ export function lambdaSignature(
| ts.SetAccessorDeclaration,
): { shapes: ParamShape[]; funcType: IrType & { kind: "func" } } {
if (!node.body) lowerer.unsupported("SC1090", node, "function overload signatures");
// A parameterless function expression forwarding its `arguments`,
// asserted to a fixed signature: that signature is its ABI.
if (typedArgumentsCandidate(node) && !node.typeParameters?.nodes.length) {
const target = typedArgumentsTarget(lowerer, node);
if (target) {
const shapes = typedArgumentsShapes(target);
lowerer.typedArgumentsLambdas.add(node);
return { shapes, funcType: funcTypeFromParamShapes(shapes, target.ret) };
}
}
// A JS replacement of a native stream's write method receives the
// actual call tuple, including overloads and omitted arguments. Node's
// contextual overload signature cannot define this closure's ABI.
@@ -188,7 +188,8 @@ function protocolSignature(
call: ts.CallExpression,
access: ts.PropertyAccessExpression,
): ProtocolSignature | null {
const recvTs = lowerer.typeOf(access.expression);
// A guarded optional step's receiver still carries its nullish arms.
const recvTs = lowerer.checker.getNonNullableType(lowerer.typeOf(access.expression));
const property = lowerer.checker.getPropertyOfType(recvTs, access.name.text);
if (!property) return null;
const declarations = property.declarations;
@@ -240,10 +241,11 @@ export function lowerProtocolMethodCall(
access: ts.PropertyAccessExpression,
recv: IrExpr,
): IrExpr | null {
// An optional chain that already guarded this step (`s?.m(x)`, the chain
// re-dispatching with the narrowed receiver) calls like a plain one.
if (
recv.type.kind !== "dyn" ||
call.questionDotToken ||
access.questionDotToken ||
lowerer.chainBlocked(call, access) ||
call.arguments.nodes.some(ts.isSpreadElement)
)
return null;
@@ -1,4 +1,5 @@
import { dynUndefinedExpr, varRef } from "../../ir/build.js";
import { builtinClassKind, builtinClassValue } from "./builtins/builtin-class-dyn.js";
import * as ts from "../ts7/adapter.js";
import {
BOOL,
@@ -450,6 +451,19 @@ export function lowerClassPrototypeData(
exactClassOfReceiver(lowerer, expr.expression) ??
(stored?.kind === "classval" ? lowerer.classes.get(stored.className) : undefined);
if (!info) return null;
// EventEmitter.prototype and the stream prototypes: the untyped
// constructor's prototype object (builtin-class-dyn.ts).
const builtinKind = builtinClassKind(info.def.name);
if (builtinKind !== null) {
const loc = locOf(expr);
return {
kind: "dynKeyGet",
value: builtinClassValue(builtinKind, loc),
key: { kind: "strLit", value: "prototype", type: STRING, loc },
type: DYN,
loc,
};
}
const parent = expr.parent;
const descriptorKey =
ts.isCallExpression(parent) &&
@@ -1,5 +1,6 @@
/* Binary operators: arithmetic, comparisons and equality, logical and nullish pairs, and string conversion for concatenation. */
import * as ts from "../../ts7/adapter.js";
import { cleanFuncAdaptable } from "../coercions/function-adapters.js";
import { type Lowerer, jsFuncNameOf } from "../lowerer.js";
import {
BIGINT_T,
@@ -481,6 +482,22 @@ export function lowerNullishPair(
);
}
}
// An optional callback defaulted to a function of another signature
// (`o.tags ?? (() => ({}))`, fewer parameters or a narrower result): the
// result is the callback's own function type, and the default adapts into
// it (JavaScript ignores surplus arguments). The checker's merged union
// signature would lose the callback's result shape.
if (present?.kind === "func" && present.rest !== true) {
const raw = lowerer.lowerExpr(expr.right);
if (
raw.type.kind === "func" &&
(typeEquals(raw.type, present) || cleanFuncAdaptable(lowerer, raw.type, present))
) {
const right = lowerer.coerceInto(expr.right, raw, present);
lowerer.nullishFuncResults.add(expr);
return { kind: "nullish", left, right, type: present, loc };
}
}
lowerer.unsupported(
"SC1090",
expr,
@@ -63,6 +63,8 @@ import { npmStaticPackageOfPath } from "../../npm-static.js";
import { isSafeToRepeat } from "./evaluation-safety.js";
import { lowerClassPrototypeComputedAssignment } from "../class-prototypes.js";
import { isClassCallback } from "../class-callbacks.js";
import { isKeyedFieldAccess } from "./keyed-field-targets.js";
import { lowerFieldCompoundValue } from "./updates.js";
/** `a[i]` reads. Only f64 indices into array receivers are modeled; JS
* string-key element access (`a["length"]`) and string indexing (`s[0]`
@@ -1417,6 +1419,11 @@ export function lowerElementCompoundParts(
const target = expr.left as ts.ElementAccessExpression;
fenceNodeModuleMutation(lowerer, target, "assignment");
const loc = locOf(expr);
// A record through a computed key (`totals[side] += n` with a literal
// union key, `counts[name] += 1` on an index-signature record): the
// member compound's receiver, key, old value, rhs, write sequence.
if (isKeyedFieldAccess(lowerer, target))
return { expr: lowerFieldCompoundValue(lowerer, target, op, expr.right, loc), stmts: null };
const checked = lowerer.lowerExpr(target.expression);
if (checked?.type.kind === "dyn" && isJsSourceFile(expr.getSourceFile())) {
const stmts: IrStmt[] = [];
@@ -1,6 +1,8 @@
/* Identifier references in expression position: lowerExprInner's identifier branch and its helpers. */
import { cjsMemberAliasValueOf } from "../cjs-exports-object.js";
import { builtinClassValue, builtinMemberClassKind } from "../builtins/builtin-class-dyn.js";
import * as ts from "../../ts7/adapter.js";
import { InternalCompilerError } from "../../../errors.js";
import * as posix from "node:path/posix";
import { type Lowerer, PoisonError } from "../lowerer.js";
import {
@@ -21,6 +23,7 @@ import {
import { lowerNodeModuleIdentifier } from "../lower-node-module.js";
import { lowerCjsExportsObjectValue } from "../cjs-exports-object.js";
import { captureContextArguments } from "../function-context.js";
import { argumentsOwner, typedArgumentsCandidate } from "../typed-arguments.js";
import {
isCjsExportTableLiteral,
isCjsJsFile,
@@ -96,7 +99,18 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc:
// type and lowerLambda declared the local.
if (expr.text === "arguments") {
const binding = captureContextArguments(lowerer.fnStack);
if (binding) return { kind: "varRef", localId: binding.id, type: DYN, loc };
if (binding && lowerer.ctx.pureTuples.has(binding.id)) {
throw new InternalCompilerError("lowerer bug: forwarded-only 'arguments' read as a value");
}
if (binding) return { kind: "varRef", localId: binding.id, type: binding.type, loc };
const owner = argumentsOwner(expr);
if (owner && typedArgumentsCandidate(owner)) {
lowerer.unsupported(
"SC1090",
expr,
"'arguments' in a function expression whose value is not asserted to one fixed-arity signature (assert it, as in `function () { … } as F`, and the function lowers against that signature)",
);
}
}
// A compiler-projected adapter as a VALUE: the adapter has no
// runtime function object (its calls rewrite directly) — call it
@@ -539,6 +553,10 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc:
// the same identity-token story as stdlib globals above (the
// harness adds worker_threads.Worker to its identity Set).
if (isJsSourceFile(expr.getSourceFile())) {
// EventEmitter and the stream classes are the runtime's untyped
// constructors, so constructor functions can inherit from them.
const classKind = builtinMemberClassKind(bi.module, bi.member);
if (classKind !== null) return builtinClassValue(classKind, loc);
// A function member is a function value that fences when called.
if (builtinModuleFnOf(lowerer, bi.module, bi.member))
return builtinFunctionFenceValue(lowerer, bi, expr, loc);
@@ -0,0 +1,271 @@
/* Read-modify-write targets for computed record keys: `r[k] op= v`, `r[k]++` and `r[k] ??= v`. */
import * as ts from "../../ts7/adapter.js";
import {
BOOL,
F64,
STRING,
typeEquals,
type IrExpr,
type IrStmt,
type SrcLoc,
} from "../../../ir/ir.js";
import { isJsSourceFile } from "../../program.js";
import { constituentTypes } from "../../ts7/checker.js";
import type { FieldTarget } from "../lower-exprs.js";
import type { Lowerer } from "../lowerer.js";
export type KeyedFieldTarget = FieldTarget & { container: "recordKeyed" };
/** The property names a computed key's type allows: one or more string
* literals (a literal union such as `"sent" | "received"`, enum string
* members, or a type parameter bound to one inside a generic instance).
* Null for any other key type. */
export function literalFieldKeys(lowerer: Lowerer, keyNode: ts.Expression): string[] | null {
let t: ts.Type = lowerer.typeOf(keyNode);
if (t.flags & ts.TypeFlags.TypeParameter) {
const bound = lowerer.typeParamTsResolver(t);
if (!bound) return null;
t = bound;
}
const parts = t.flags & ts.TypeFlags.Union ? constituentTypes(t) : [t];
const keys: string[] = [];
for (const part of parts) {
if (!part.isStringLiteralType()) return null;
if (!keys.includes(part.value)) keys.push(part.value);
}
return keys.length > 0 ? keys : null;
}
/** Does `r[k]` read and write through declared record storage by key? A
* checker-only test (nothing lowers), so a caller can route before it
* evaluates the receiver: TypeScript sources only, a record whose declared
* fields include every name the key's literal type allows, or a pure
* index-signature record with a string key. */
export function isKeyedFieldAccess(lowerer: Lowerer, access: ts.ElementAccessExpression): boolean {
if (
access.questionDotToken ||
isJsSourceFile(access.getSourceFile()) ||
lowerer.isProcessEnv(access.expression)
)
return false;
const keys = literalFieldKeys(lowerer, access.argumentExpression);
if (!keys && !(lowerer.typeOf(access.argumentExpression).flags & ts.TypeFlags.StringLike))
return false;
const receiver = lowerer.mapTypeOf(lowerer.typeOf(access.expression));
if (receiver?.kind !== "record") return false;
const shape = lowerer.shapes.get(receiver.shapeId);
if (!shape || shape.tuple) return false;
if (shape.fields.length === 0) return shape.indexValue !== undefined;
return keys !== null && keys.every((k) => shape.fields.some((f) => f.name === k));
}
/** The target of a read-modify-write through `r[k]`, or null when
* isKeyedFieldAccess declines. A literal-union key dispatches over the
* declared fields it names, which must share one type; a string key on a
* pure index-signature record reads and writes its entries. The receiver
* and key may arrive already lowered (and saved) by the caller; otherwise
* both lower here, unsaved. A caller that evaluates the target more than
* once saves `obj` and then `key`, the JavaScript evaluation order. With
* `numeric`, a missing index-signature entry reads as NaN (every numeric
* update applies ToNumber to it) instead of a value armed with undefined. */
export function keyedFieldTarget(
lowerer: Lowerer,
access: ts.ElementAccessExpression,
numeric: boolean,
lowered?: { obj: IrExpr; key: IrExpr },
): KeyedFieldTarget | null {
if (!isKeyedFieldAccess(lowerer, access)) return null;
const keyNode = access.argumentExpression;
const keys = literalFieldKeys(lowerer, keyNode);
const obj = lowered?.obj ?? lowerer.lowerExpr(access.expression);
if (obj.type.kind !== "record") {
lowerer.unsupported(
"SC1090",
access.expression,
`updating a computed key of a '${lowerer.fmt(obj.type)}' value (narrow the receiver to its record type first)`,
);
}
const shapeId = obj.type.shapeId;
const shape = lowerer.shapes.get(shapeId)!;
// A single literal spelled at the site (or an identifier whose type is
// that one literal) names the field statically: identifier evaluation
// is pure, so skipping it matches JavaScript.
const indexed = shape.fields.length === 0;
const staticKey =
!indexed && keys?.length === 1 && (ts.isStringLiteralLike(keyNode) || ts.isIdentifier(keyNode));
const key = staticKey ? null : (lowered?.key ?? lowerer.lowerExpr(keyNode));
if (key && key.type.kind !== "string") {
lowerer.unsupported(
"SC1090",
keyNode,
`updating a record through a '${lowerer.fmt(key.type)}' key (convert the key to a string first)`,
);
}
if (keys && !indexed) {
const fields = keys.map((k) => shape.fields.find((f) => f.name === k));
const first = fields[0];
if (!first || fields.some((f) => !f || !typeEquals(f.type, first.type))) {
lowerer.unsupported(
"SC1090",
access,
`updating '${lowerer.fmt(obj.type)}' through a key whose fields are not all stored with one type (${keys.map((k) => `'${k}'`).join(", ")}; switch over the key and update each field)`,
);
}
return {
container: "recordKeyed",
obj,
shapeId,
field: keys[0]!,
keys,
key,
fieldType: first.type,
};
}
if (!indexed || !shape.indexValue) {
lowerer.unsupported(
"SC1090",
access,
`updating '${lowerer.fmt(obj.type)}' through a runtime key (the stored record declares fields)`,
);
}
return {
container: "recordKeyed",
obj,
shapeId,
field: "",
keys: null,
key,
fieldType: shape.indexValue,
...(numeric && shape.indexValue.kind === "f64" ? { missNaN: true as const } : {}),
};
}
/** Does `key` name `field`? */
function keyIs(key: IrExpr, field: string, loc: SrcLoc): IrExpr {
return {
kind: "strEq",
negated: false,
left: key,
right: { kind: "strLit", value: field, type: STRING, loc },
type: BOOL,
loc,
};
}
/** A key outside its literal type (smuggled through a cast) names no
* declared field: a fixed-shape record cannot add the property, so the
* update throws the TypeError of a keyed write miss. */
function keyMiss(key: IrExpr, target: KeyedFieldTarget, loc: SrcLoc): IrExpr {
const message: IrExpr = {
kind: "strConcat",
left: {
kind: "strConcat",
left: { kind: "strLit", value: "Cannot add property '", type: STRING, loc },
right: key,
type: STRING,
loc,
},
right: { kind: "strLit", value: "' to a fixed-shape object", type: STRING, loc },
type: STRING,
loc,
};
return {
kind: "libCall",
fn: "error.nodeThrow",
args: [
{ kind: "numLit", value: 1, type: F64, loc },
{ kind: "strLit", value: "", type: STRING, loc },
message,
],
type: target.fieldType,
loc,
};
}
/** The current value of a keyed target. */
export function keyedFieldRead(lowerer: Lowerer, target: KeyedFieldTarget, loc: SrcLoc): IrExpr {
const { obj, shapeId, key, fieldType } = target;
if (!target.keys) {
const type = target.missNaN
? fieldType
: fieldType.kind === "dyn"
? fieldType
: lowerer.withUndefinedArmOf(fieldType);
if (!type || !key) throw new Error("keyed index-signature target without a key or read type");
return {
kind: "recordKeyGet",
obj,
shapeId,
key,
...(target.missNaN ? { missNaN: true as const } : {}),
type,
loc,
};
}
const get = (field: string): IrExpr => ({
kind: "recordGet",
obj,
shapeId,
field,
type: fieldType,
loc,
});
if (!key) return get(target.keys[0]!);
return target.keys.reduceRight<IrExpr>(
(otherwise, field) => ({
kind: "ternary",
cond: keyIs(key, field, loc),
then: get(field),
else_: otherwise,
type: fieldType,
loc,
}),
keyMiss(key, target, loc),
);
}
/** Store `value` (already of the target's field type) through a keyed
* target. A multi-key dispatch references the value once per field, so a
* value that is not a plain local is saved first. */
export function keyedFieldWrite(
lowerer: Lowerer,
target: KeyedFieldTarget,
value: IrExpr,
loc: SrcLoc,
): IrStmt {
const { obj, shapeId, key } = target;
if (!target.keys) {
if (!key) throw new Error("keyed index-signature target without a key");
return { kind: "recordKeySet", obj, shapeId, key, value, loc };
}
if (!key) return { kind: "recordSet", obj, shapeId, field: target.keys[0]!, value, loc };
const prefix: IrStmt[] = [];
let stored = value;
if (value.kind !== "varRef") {
const local = lowerer.declareHiddenLocal("%keyedValue", value.type);
prefix.push({ kind: "varDecl", localId: local.id, init: value, loc });
stored = { kind: "varRef", localId: local.id, type: value.type, loc };
}
const chain = target.keys.reduceRight<IrStmt[]>(
(otherwise, field) => [
{
kind: "if",
cond: keyIs(key, field, loc),
then: [{ kind: "recordSet", obj, shapeId, field, value: stored, loc }],
else_: otherwise,
loc,
},
],
[{ kind: "exprStmt", expr: keyMiss(key, target, loc), loc }],
);
return { kind: "block", body: [...prefix, ...chain], loc };
}
/** Save a keyed target's key after its receiver, so a multi-key dispatch
* reads and writes the field the key named when it was evaluated. */
export function saveKeyedFieldKey(
target: FieldTarget,
save: (value: IrExpr, name: string) => IrExpr,
): void {
if (target.container === "recordKeyed" && target.key) target.key = save(target.key, "%keyedKey");
}
@@ -26,6 +26,7 @@ import { jsBindingHasOpenWrites } from "../lower-stmts.js";
import { narrowClassUnion, narrowStoredClassValue, structuralNarrowing } from "../class-unions.js";
import { checkedClassAssertion, checkedClassUnionAssertion } from "../class-assertions.js";
import { isOptionalProcessStreamProperty } from "../builtins/process.js";
import { requestGeneralStreamSurface } from "../general-streams.js";
import { literalValues } from "../../literal-values.js";
import { literalUnionArm } from "../../union-discriminants.js";
import { checkedArmExtraction } from "../coercions/unions.js";
@@ -1186,6 +1187,56 @@ function classInstanceOrAbsent(lowerer: Lowerer, type: IrType): boolean {
);
}
/** `Object.fromEntries(...) as T` with T a declared record (the generic
* `transform<T extends Record<string, unknown>>(t: T): T` idiom): the
* entries build a pure index-signature record, which has no layout in
* common with T's struct. Node keeps the object itself, with exactly the
* keys and the order the entries gave it. The result of fromEntries is a
* fresh object nothing else references, so the checked-dynamic bridge,
* which keeps keys and their order the same way `JSON.parse(text) as T`
* does, cannot be told apart from the original object; a value outside
* T throws the catchable TypeError of every checked cast. The entries'
* values must be scalars or checked-dynamic values, which the bridge
* passes by value or by identity; native arrays and records would be
* copied. Other sources may be aliased, where a rebuilt object would sever
* mutation, so they keep the exact-shape fence. */
function freshEntriesRecordAssertion(
lowerer: Lowerer,
expr: ts.AsExpression | ts.TypeAssertion,
inner: IrExpr,
target: IrType | null,
): IrExpr | null {
if (inner.type.kind !== "record" || target?.kind !== "record") return null;
const source = lowerer.shapes.get(inner.type.shapeId);
const dest = lowerer.shapes.get(target.shapeId);
if (!source?.indexValue || source.tuple || source.fields.length > 0) return null;
if (!dest || dest.tuple || dest.indexValue || shapeHasAccessorSlots(dest)) return null;
// Native arrays and records in the entries would be copied on the way
// through the checked-dynamic bridge, severing their identity: only
// slots of scalars or checked-dynamic values pass.
if (extractionCopiesReference(lowerer, source.indexValue)) return null;
let operand: ts.Expression = expr.expression;
while (ts.isParenthesizedExpression(operand)) operand = operand.expression;
if (
!ts.isCallExpression(operand) ||
!ts.isPropertyAccessExpression(operand.expression) ||
operand.expression.name.text !== "fromEntries" ||
!lowerer.isStdlibGlobal(operand.expression.expression, "Object")
)
return null;
if (!lowerer.dynConvertible(inner.type)) return null;
if (
!canDynCheckTo(
target,
(id) => lowerer.shapes.get(id),
(id) => lowerer.unions.get(id),
)
)
return null;
const loc = locOf(expr);
return { kind: "dynCheck", value: lowerer.coerceToExpected(inner, DYN), type: target, loc };
}
/** `e as T` and the old-style assertion `<T>e` — one node shape (both
* carry `.type` and `.expression`), one lowering. */
export function lowerAsExpression(
@@ -1323,6 +1374,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 +1429,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 +1509,9 @@ export function lowerAsExpression(
if (DYN_HANDLE_KINDS.has(target.kind)) {
return { kind: "dynCheck", value: inner, type: target, loc: locOf(expr) };
}
// `value as NodeJS.ReadableStream`: the general stream classes take
// the checked cast (general-streams.ts).
requestGeneralStreamSurface(lowerer, expr);
lowerer.unsupported(
"SC1090",
expr,
@@ -18,6 +18,11 @@ import { arrayNumberRead, arrayValueRead, arrayValueStore } from "../array-value
import { fenceNodeModuleMutation } from "../lower-node-module.js";
import { globalSymbolKey } from "./global-symbols.js";
import { tryLowerExpression } from "./try-lower-expression.js";
import {
type KeyedFieldTarget,
keyedFieldTarget,
saveKeyedFieldKey,
} from "./keyed-field-targets.js";
import {
eventAccessorRead,
eventAccessorWrite,
@@ -147,6 +152,7 @@ export function lowerShortCircuitAssignment(lowerer: Lowerer, expr: ts.BinaryExp
} else {
const receiver = save(lowerer.lowerExpr(target.expression), "%shortCircuitReceiver");
let key = lowerer.lowerExpr(target.argumentExpression);
let keyed: KeyedFieldTarget | null = null;
if (receiver.type.kind === "dyn") {
key = save(
{
@@ -245,6 +251,17 @@ export function lowerShortCircuitAssignment(lowerer: Lowerer, expr: ts.BinaryExp
value: lowerer.coerceToExpected(value, indexValue),
loc,
});
} else if (
receiver.type.kind === "record" &&
(keyed = keyedFieldTarget(lowerer, target, false, { obj: receiver, key }))
) {
// `totals[side] ??= v` through a literal-union key: the read and
// the write dispatch over the declared fields the key can name.
const field = keyed;
saveKeyedFieldKey(field, save);
storage = field.fieldType;
read = lowerer.fieldGetExpr(field, locOf(target), target);
write = (value) => lowerer.fieldSetStmt(field, value, loc, target);
} else {
lowerer.unsupported(
"SC1090",
@@ -1,3 +1,5 @@
import { isSafeToDiscard } from "./evaluation-safety.js";
import { completeFuncValueArgs } from "../call-arguments.js";
import { dynUndefinedExpr } from "../../../ir/build.js";
import * as ts from "../../ts7/adapter.js";
import { DYN, STRING, UNDEFINED_T, VOID, isUnitType, typeEquals } from "../../../ir/ir.js";
@@ -429,7 +431,22 @@ export function lowerOptionalChain(
if (dotNode === expr && ts.isCallExpression(expr)) {
if (narrowed.kind !== "func") lowerer.badType(recvNode, lowerer.typeOf(recvNode));
const params = narrowed.params;
// Forwarded fixed tuples (`f?.(...args)`) complete into the slots.
if (expr.arguments.nodes.some(ts.isSpreadElement)) {
const completed = completeFuncValueArgs(lowerer, expr, narrowed, loc);
const call: IrExpr = {
kind: "callValue",
callee: recvRef,
args: completed,
type: narrowed.ret,
loc,
};
return lowerer.finishOptionalChain(expr, id, receiver, call, loc);
}
const args = expr.arguments.nodes.map((a, i) => lowerer.lowerExprExpecting(a, params[i]));
// Effect-free surplus arguments (a dropped `void` slot) are ignored.
while (narrowed.rest !== true && args.length > params.length && isSafeToDiscard(args.at(-1)!))
args.pop();
const body: IrExpr = { kind: "callValue", callee: recvRef, args, type: narrowed.ret, loc };
return lowerer.finishOptionalChain(expr, id, receiver, body, loc);
}
@@ -1,5 +1,9 @@
/* Property reads (`a.b`): lowerExprInner's property-access branch, intrinsic and union properties, field reads and targets, and match-group reads. */
import * as ts from "../../ts7/adapter.js";
import { lowerHistogramProperty } from "../builtins/performance.js";
import { staticFunctionNameRead } from "../static-function-names.js";
import { typedArgumentsArity } from "../typed-arguments.js";
import { isThisParameter } from "../call-signatures.js";
import { localPrivateBrandClass } from "../private-brands.js";
import { type Lowerer, PoisonError, jsFuncNameOf, neverTaintedJsType, own } from "../lowerer.js";
import {
@@ -155,6 +159,7 @@ import { errorPropertyRead } from "../error-methods.js";
import { isSafeToRepeat } from "./evaluation-safety.js";
import { isZlibStreamMember, lowerZlibStreamBytesWritten } from "../builtins/compression.js";
import { isFsStreamMember, lowerFsStreamProperty } from "../builtins/fs-streams.js";
import { keyedFieldRead } from "./keyed-field-targets.js";
/** True when a property access names an ABSTRACT property declaration
* (`abstract p: number` — a PropertyDeclaration, not an accessor).
@@ -255,6 +260,23 @@ export function lowerPropertyAccessExpr(
const navigator = lowerNavigatorMember(lowerer, expr, NODE_COMPAT_VERSION);
if (navigator) return navigator;
}
// `fn.name` on a function parameter of a generic instance keyed by
// its argument's static name (call-site specialization).
{
const staticName = staticFunctionNameRead(lowerer, expr);
if (staticName !== null) return { kind: "strLit", value: staticName, type: STRING, loc };
}
// An IntervalHistogram's readings (min, max, mean, stddev, count, exceeds).
{
const reading = lowerHistogramProperty(lowerer, expr);
if (reading) return reading;
}
// `.length` of a forwarded fixed tuple (a typed `arguments`, a tuple
// rest parameter, an alias of either): the signature's arity.
if (expr.name.text === "length" && !expr.questionDotToken) {
const arity = typedArgumentsArity(lowerer, expr.expression);
if (arity !== null) return { kind: "numLit", value: arity, type: F64, loc };
}
// `require("./x").member` over a program module's export table: the
// module's %init at this position, then the member read exactly like
// a `const m = require("./x")` binding's (cjs-inline-require.ts).
@@ -570,9 +592,12 @@ export function lowerPropertyAccessExpr(
p.dotDotDotToken === undefined,
);
if (fixedArity) {
// A `this` parameter is type-only: callers never pass it.
return {
kind: "numLit",
value: (fn as ts.FunctionLikeDeclaration).parameters.nodes.length,
value: (fn as ts.FunctionLikeDeclaration).parameters.nodes.filter(
(p) => !isThisParameter(p),
).length,
type: F64,
loc,
};
@@ -3508,6 +3533,7 @@ export function fieldGetExpr(
);
if (checked) target = { ...target, obj: checked };
}
if (target.container === "recordKeyed") return keyedFieldRead(lowerer, target, loc);
if (target.container === "dynamic")
return lowerer.coerceInto(
blame,
@@ -1,5 +1,6 @@
/* instanceof and in, including private-name and runtime-key in. */
import * as ts from "../../ts7/adapter.js";
import { builtinClassInstanceOf, builtinClassKind } from "../builtins/builtin-class-dyn.js";
import {
checkedPrivateIn,
localPrivateBrandClass,
@@ -473,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,
@@ -581,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",
@@ -1146,6 +1171,24 @@ function lowerPrivateIn(
}
/** The string keys every record inherits from Object.prototype. */
/** Keys built-in errors carry outside their class fields: Node's system
* error properties, and AggregateError's, SuppressedError's and
* AssertionError's own members. */
const BUILTIN_ERROR_SLOT_KEYS: ReadonlySet<string> = new Set([
"errno",
"syscall",
"path",
"dest",
"info",
"errors",
"error",
"suppressed",
"actual",
"expected",
"operator",
"generatedMessage",
]);
const OBJECT_PROTOTYPE_KEYS: ReadonlySet<string> = new Set([
"constructor",
"__defineGetter__",
@@ -1414,6 +1457,50 @@ export function lowerInExpression(
) {
return { kind: "boolLit", value: true, type: BOOL, loc };
}
// 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)) &&
(recv.kind === "varRef" || recv.kind === "caughtNarrow")
) {
return { kind: "boolLit", value: true, type: BOOL, loc };
}
// Any other key: a member a program class declares. The receiver's
// runtime class is its static class or a descendant; the key is
// present exactly when that class (or an ancestor) declares it. Keys
// built-in errors keep outside their class fields depend on the
// runtime error and stay fenced, as do keys no program class declares.
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;
if (
(recv.kind === "varRef" || recv.kind === "caughtNarrow") &&
!BUILTIN_ERROR_SLOT_KEYS.has(key) &&
[...lowerer.classes.values()].some((c) => c.builtinError !== true && declares(c))
) {
const staticName = recv.type.className;
const staticInfo = lowerer.classes.get(staticName);
if (staticInfo && declares(staticInfo))
return { kind: "boolLit", value: true, type: BOOL, loc };
const tests: IrExpr[] = [];
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: recv, className: name, type: BOOL, loc });
}
return tests.reduce<IrExpr>(
(acc, test) =>
acc.kind === "boolLit"
? test
: { kind: "logical", op: "||", left: acc, right: test, type: BOOL, loc },
{ kind: "boolLit", value: false, type: BOOL, loc },
);
}
lowerer.unsupported(
"SC1090",
expr,
@@ -42,6 +42,7 @@ import type { CompoundOp } from "../surfaces.js";
import { tryLowerExpression } from "./try-lower-expression.js";
import { hasRuntimeStatics } from "../class-runtime-statics.js";
import { declareContextLocal } from "../function-context.js";
import { keyedFieldTarget, keyedFieldWrite, saveKeyedFieldKey } from "./keyed-field-targets.js";
/** Read-modify-write updates read the member first: an unassigned
* deferred-init receiver throws Node's member-read TypeError. */
@@ -337,10 +338,11 @@ export function lowerIncDec(
}
const target = ts.isPropertyAccessExpression(access)
? lowerer.fieldTarget(access)
: symbolFieldTarget(lowerer, access);
: (symbolFieldTarget(lowerer, access) ?? keyedFieldTarget(lowerer, access, true));
if (target && lowerer.dynConvertible(target.fieldType)) {
checkDeferredMemberReceiver(lowerer, target, access);
target.obj = save(target.obj, "%incrementReceiver");
saveKeyedFieldKey(target, save);
return finish(lowerer.fieldGetExpr(target, loc, access), (value) =>
lowerer.fieldSetStmt(
target,
@@ -617,6 +619,7 @@ export function fieldSetStmt(
};
}
}
if (target.container === "recordKeyed") return keyedFieldWrite(lowerer, target, value, loc);
if (target.container === "dynamic")
return {
kind: "exprStmt",
@@ -872,12 +875,13 @@ export function lowerFieldCompoundValue(
const targetOf = (): FieldTarget | null =>
ts.isPropertyAccessExpression(access)
? lowerer.fieldTarget(access)
: symbolFieldTarget(lowerer, access);
: (symbolFieldTarget(lowerer, access) ?? keyedFieldTarget(lowerer, access, true));
const target = targetOf();
if (!target)
lowerer.unsupported("SC1090", access, "compound assignment to unsupported field targets");
checkDeferredMemberReceiver(lowerer, target, access);
target.obj = save(target.obj, "%compoundReceiver");
saveKeyedFieldKey(target, save);
// Accessors observe getter, RHS side effects, then setter, all through
// the saved receiver.
const fieldRead = lowerer.fieldGetExpr(target, locOf(access), access);
@@ -48,6 +48,12 @@ export interface FnCtx {
argumentsLocal?: IrLocal | null;
/** Arrows inherit the nearest ordinary function's arguments binding. */
inheritsArguments?: boolean;
/** Tuple bindings of this function that are only forwarded
* (typed-arguments.ts onlyForwarded): binding local id → the native
* slots forwarding reads. Such a binding is never materialized. Local ids
* are unique within one function only, and nested functions never read
* these bindings (a read there needs the tuple). */
pureTuples: Map<string, IrLocal[]>;
/** Declared return type — lets `return` detect record-shape mismatches
* (SC2002) before the validator would ICE on them. */
returnType: IrType;
@@ -81,6 +87,7 @@ export function newFnCtx(
captures: lifted ? [] : null,
captureSources: [],
captureBySymbol: new Map(),
pureTuples: new Map(),
selfSymbol,
selfType,
returnType,
@@ -0,0 +1,114 @@
/* The general stream representation (TypeMapperCtx.generalNodeStreams).
*
* NodeJS.ReadableStream and NodeJS.WritableStream (and @types/node's
* stream.Readable / stream.Writable, which also type child stdio) name the
* interface every Node stream implements. By default they map to the
* child/process stdio handles: the specialized representation of the
* values that commonly fill them (child.stdout, process.stdout). A program
* that moves another stream — fs.createReadStream(), a zlib Transform, a
* Readable subclass — into one of those slots needs the general
* representation instead: the runtime stream classes, with the stdio
* handles converted into them where they are read or flow.
*
* The decision is per program. Reachable emit lowers without it; a flow
* that needs it marks the lowerer (needsGeneralNodeStreams) and lowerToIr
* lowers the whole program again with it. Programs without such a flow
* never take the second pass, so their IR is unchanged. */
import type { IrExpr, IrType } from "../../ir/ir.js";
import { everyTypeChild } from "../../ir/traverse.js";
import * as ts from "../ts7/adapter.js";
import { isGeneralNodeStreamInterface } from "../type-mapper.js";
import type { Lowerer } from "./lowerer.js";
import { streamSidesOf } from "./lower-stream.js";
const STDIO_STREAM_KINDS: ReadonlySet<IrType["kind"]> = new Set([
"childStream",
"childWriter",
"procStream",
]);
/** Whether `type` (through union arms, record fields and the structural
* type children) contains a part satisfying `test`. */
function containsPart(lowerer: Lowerer, type: IrType, test: (part: IrType) => boolean): boolean {
const seen = new Set<string>();
const walk = (part: IrType): boolean => {
if (test(part)) return true;
if (part.kind === "union") {
if (seen.has(`u:${part.unionId}`)) return false;
seen.add(`u:${part.unionId}`);
return (lowerer.unions.get(part.unionId)?.arms ?? []).some(walk);
}
if (part.kind === "record") {
if (seen.has(`r:${part.shapeId}`)) return false;
seen.add(`r:${part.shapeId}`);
return (lowerer.shapes.get(part.shapeId)?.fields ?? []).some((f) => walk(f.type));
}
let found = false;
everyTypeChild(part, (child) => {
found = walk(child);
return !found;
});
return found;
};
return walk(type);
}
function isGeneralStream(lowerer: Lowerer, part: IrType): boolean {
return (
part.kind === "object" && streamSidesOf(lowerer, lowerer.classes.get(part.className)) !== null
);
}
/** A mismatched flow (requireExactShape's input) that the general stream
* representation resolves: a general stream into a stdio-represented
* slot, a child pipe handle into an 'unknown' slot, or an 'unknown' value
* into a child-pipe slot (the general classes take a checked cast). */
export function needsGeneralNodeStreams(
lowerer: Lowerer,
actual: IrType,
expected: IrType,
): boolean {
const stdio = (part: IrType): boolean => STDIO_STREAM_KINDS.has(part.kind);
const pipeHandle = (part: IrType): boolean =>
part.kind === "childStream" || part.kind === "childWriter";
if (actual.kind === "dyn") return containsPart(lowerer, expected, pipeHandle);
if (containsPart(lowerer, expected, stdio))
return containsPart(lowerer, actual, (p) => isGeneralStream(lowerer, p));
return expected.kind === "dyn" && containsPart(lowerer, actual, pipeHandle);
}
/** Under the general representation, process.stdout / process.stderr (the
* procStream fd scalar) flowing into a runtime Writable slot: an
* fd-backed Writable whose writes take the same synchronous path as
* process.stdout.write. Null when the flow is not that one. */
export function stdioIntoGeneralStream(
lowerer: Lowerer,
expr: IrExpr,
expected: IrType,
): IrExpr | null {
if (!lowerer.generalNodeStreams || expr.type.kind !== "procStream") return null;
const writable: IrType = { kind: "object", className: "%Writable" };
const accepts =
(expected.kind === "object" && expected.className === "%Writable") ||
(expected.kind === "union" && lowerer.armTag(expected.unionId, writable) >= 0);
if (!accepts) return null;
return {
kind: "libCall",
fn: "stream.stdioWritable",
args: [expr],
type: writable,
loc: expr.loc,
};
}
/** A stdio-handle receiver whose member has no lowering on the handle
* (`out.on("error", ...)`, `child.stdout.pipe(...)`, `output.end()`): when
* its declared type is one the general representation maps to a runtime
* stream class, the program lowers again with it, where the full stream
* surface applies. The caller still reports its fence for this pass. */
export function requestGeneralStreamSurface(lowerer: Lowerer, receiver: ts.Expression): void {
if (lowerer.generalNodeStreams) return;
const type = lowerer.checker.getNonNullableType(lowerer.typeOf(receiver));
if (isGeneralNodeStreamInterface(type, lowerer.typeCtx))
lowerer.requiresGeneralNodeStreams = true;
}
@@ -1,3 +1,4 @@
import { staticNameBindings } from "./static-function-names.js";
import * as ts from "../ts7/adapter.js";
import { bodyReadsArguments } from "../arguments-usage.js";
import type { Lowerer } from "./lowerer.js";
@@ -100,6 +101,9 @@ export interface GenericFnInfo {
* names where the slot binds the call site's object type. Computed on
* first use. */
implicitPatterns?: (ImplicitPattern | null)[];
/** Parameter index → symbol for the function parameters whose `.name`
* the body reads (static-function-names.ts); computed on first use. */
nameReadParams?: Map<number, ts.Symbol>;
}
export interface GenericInstance {
@@ -140,6 +144,9 @@ export interface GenericInstance {
implicitInferReturn?: true;
/** An erased instance (every type parameter bound to `unknown`). */
erased?: true;
/** Name-keyed instances: function parameter → its call site's static
* name (null when only known at run time), folded at `.name` reads. */
staticFnNames?: Map<ts.Symbol, string | null>;
}
/** Defaults bind during inference; collection only resolves symbols in
@@ -295,7 +302,13 @@ export function genericCallInstance(
const retTs = lowerer.checker.getReturnTypeOfSignature(rsig);
const local = localClassResultInstance(lowerer, expr, info, rsig, params, retTs);
if (local) return local;
const mappedReturn = lowerer.mapTypeOf(retTs);
// A result instantiated at `any` (a type argument inferred from an
// untyped value, such as an untyped forwarding call) is checked-dynamic.
const mappedReturn =
lowerer.mapTypeOf(retTs) ??
((retTs.flags & ts.TypeFlags.Any) !== 0 && !isJsSourceFile(info.decl.getSourceFile())
? DYN
: null);
if (!mappedReturn) {
fenceGenericSignatureResult(lowerer, expr, retTs);
lowerer.badType(expr, retTs);
@@ -317,6 +330,7 @@ export function genericCallInstance(
// different fields). Non-literal bindings (a key union, plain string)
// share one runtime-keyed instance per IR signature, exactly the
// widened discipline.
const named = staticNameBindings(lowerer, expr, info);
const keyofTps = keyofConstrainedTypeParams(info);
if (keyofTps.size > 0) {
const tsBindings = new Map<ts.Symbol, ts.Type>();
@@ -333,12 +347,19 @@ export function genericCallInstance(
})
.join(",");
return internGenericInstance(lowerer, expr, info, params, returnType, () => bindings, {
extraKey: `@${litKey}`,
extraKey: `@${litKey}${named?.key ?? ""}`,
tsBindings,
...(named ? { staticFnNames: named.names } : {}),
});
}
return internGenericInstance(lowerer, expr, info, params, returnType, (tsBindings) =>
lowerer.inferTypeParamBindings(expr, info, rsig, tsBindings),
return internGenericInstance(
lowerer,
expr,
info,
params,
returnType,
(tsBindings) => lowerer.inferTypeParamBindings(expr, info, rsig, tsBindings),
named ? { extraKey: named.key, staticFnNames: named.names } : undefined,
);
}
@@ -498,7 +519,11 @@ function internGenericInstance(
params: ParamShape[],
returnType: IrType,
makeBindings: (tsBindings: Map<ts.Symbol, ts.Type>) => Map<ts.Symbol, IrType>,
opts?: { extraKey?: string; tsBindings?: Map<ts.Symbol, ts.Type> },
opts?: {
extraKey?: string;
tsBindings?: Map<ts.Symbol, ts.Type>;
staticFnNames?: Map<ts.Symbol, string | null>;
},
): GenericInstance {
// keyof-constrained instantiations append their literal keys
// (extraKey): the IR signature alone under-discriminates there — two
@@ -541,6 +566,7 @@ function internGenericInstance(
bindings,
tsBindings,
typeArgsText,
...(opts?.staticFnNames ? { staticFnNames: opts.staticFnNames } : {}),
};
info.instances.set(key, inst);
lowerer.instantiationQueue.push({ info, inst });
@@ -671,7 +697,11 @@ function unifySignatureBindings(
tsBindings.set(sym, lowerer.typeParamTsResolver(inst) ?? inst);
}
if (!bindings.has(sym)) {
const mapped = lowerer.mapTypeOf(inst);
const mapped =
lowerer.mapTypeOf(inst) ??
((inst.flags & ts.TypeFlags.Any) !== 0 && !isJsSourceFile(info.decl.getSourceFile())
? DYN
: null);
// A visitor's inferred T can be void. It still binds the
// return convention of `(node) => T`; discarding it leaves
// the callback's return type spuriously uninstantiated.
@@ -824,6 +854,8 @@ export function lowerGenericInstance(
const prevSuppress = lowerer.suppressStats;
const prevClass = lowerer.currentClass;
const prevImplicit = lowerer.implicitParamTypes;
const prevStaticFnNames = lowerer.staticFnNames;
lowerer.staticFnNames = inst.staticFnNames ?? null;
// A generic METHOD of a generic-class INSTANTIATION lowers under BOTH
// binding sets: the receiver instantiation's class type parameters
// underneath, the method instantiation's own on top (disjoint symbol
@@ -984,6 +1016,7 @@ export function lowerGenericInstance(
lowerer.typeParamBindings = prevBindings;
lowerer.typeParamTsBindings = prevTsBindings;
lowerer.implicitParamTypes = prevImplicit;
lowerer.staticFnNames = prevStaticFnNames;
lowerer.instantiationContext = prevContext;
lowerer.localClassInstantiations.pop();
lowerer.suppressStats = prevSuppress;
@@ -1,4 +1,6 @@
import { isForwardedTupleSpread, lowerForwardingApply } from "./typed-arguments.js";
import { guardNullProto, nullProtoRecordShape } from "./null-prototype.js";
import { lowerBuiltinClassCall } from "./builtins/builtin-class-dyn.js";
import { refuseModuleMemberThis } from "./module-member-this.js";
import { lowerUnionEquality, tagEqualityMayMissAlias } from "./strict-equality.js";
import { lowerHashStreamCall } from "./builtins/hash-stream.js";
@@ -150,6 +152,7 @@ import {
import {
fencePerfEntryKeyWalk,
isPerfEntryType,
lowerHistogramMethodCall,
lowerPerfHooksCall,
perfEntryTag,
} from "./builtins/performance.js";
@@ -255,7 +258,6 @@ const ZLIB_STREAM_METHODS: ReadonlySet<string> = new Set(["flush", "params", "cl
export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
const loc = locOf(expr);
// Object's checker return type is `any`. Claim known object inputs
// before generic checked-dynamic dispatch tries to load the constructor.
if (
@@ -267,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;
@@ -1407,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
@@ -1418,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) ??
@@ -1902,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;
@@ -3320,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 (
@@ -8802,6 +8832,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
@@ -4,7 +4,7 @@ import { InternalCompilerError } from "../../errors.js";
* literals — records/arrays/unions included, subject to comptimeBakeable. */
import * as ts from "../ts7/adapter.js";
import type { Lowerer } from "./lowerer.js";
import { type IrExpr, type IrType } from "../../ir/ir.js";
import { isUnitType, type IrExpr, type IrType } from "../../ir/ir.js";
import { comptimeFailedDiag } from "../../diagnostics/diagnostic.js";
import { locOf } from "../program.js";
import { PoisonError } from "./lowerer.js";
@@ -76,7 +76,7 @@ export function lowerComptime(lowerer: Lowerer, expr: ts.CallExpression): IrExpr
"SC1090",
expr,
`baking comptime results of type '${lowerer.fmt(target)}' into the binary ` +
`(only number, string, boolean, arrays, and records lower to literals)`,
`(number, string, boolean, and arrays, records and unions of those lower to literals; a record or union type reachable from itself does not)`,
);
}
// 5. The host executes the extracted callback under a finite budget.
@@ -110,20 +110,104 @@ function comptimeFailureDetail(error: unknown): string {
/** True when a comptime result of this type can be written as a literal
* expression: number/string/boolean, arrays and record shapes of those,
* nested. Closures, class instances, unions, promises, dyn, and void
* cannot be baked. */
* and unions of those with null or undefined (the mixed rows of a JSON
* table: `(string | number)[][]`), nested. Closures, class instances,
* promises, dyn, void, and recursive types (a record or union reachable
* from itself, directly or through arrays) cannot be baked. */
export function comptimeBakeable(lowerer: Lowerer, t: IrType): boolean {
return bakeableWithin(lowerer, t, new Set());
}
/** comptimeBakeable below the records and unions in `open` (keyed
* `record:<shapeId>` and `union:<unionId>`), whose members are being
* checked: reaching one of them again means the type is recursive. */
function bakeableWithin(lowerer: Lowerer, t: IrType, open: Set<string>): boolean {
switch (t.kind) {
case "f64":
case "string":
case "bool":
return true;
case "array":
return lowerer.comptimeBakeable(t.elem);
return bakeableWithin(lowerer, t.elem, open);
case "record": {
const shape = lowerer.shapes.get(t.shapeId);
return !!shape && shape.fields.every((f) => lowerer.comptimeBakeable(f.type));
const key = `record:${t.shapeId}`;
if (!shape || open.has(key)) return false;
open.add(key);
const bakeable = shape.fields.every((f) => bakeableWithin(lowerer, f.type, open));
open.delete(key);
return bakeable;
}
case "union": {
const arms = lowerer.unions.get(t.unionId)?.arms;
const key = `union:${t.unionId}`;
if (arms === undefined || open.has(key)) return false;
open.add(key);
const bakeable = arms.every(
(arm) => isUnitType(arm) || (arm.kind !== "union" && bakeableWithin(lowerer, arm, open)),
);
open.delete(key);
return bakeable;
}
default:
return false;
}
}
/** The tag of the arm of union `unionId` that `value` bakes into, or -1.
* A record value whose discriminant field holds one of the union's
* discriminant values takes that case's arm, since arms with overlapping
* shapes can both fit it. Any other value takes the first arm it fits, in
* the union's arm order: a JSON value fits exactly one scalar arm, and
* array or record arms are told apart by their contents. */
function comptimeUnionArm(lowerer: Lowerer, unionId: string, value: unknown): number {
const def = lowerer.unions.get(unionId);
if (!def) return -1;
const discriminant = def.discriminant;
if (
discriminant !== undefined &&
typeof value === "object" &&
value !== null &&
!Array.isArray(value)
) {
const key = (value as Record<string, unknown>)[discriminant.field];
const named = discriminant.cases.find((c) => c.values.some((v) => v === key));
if (named !== undefined)
return comptimeFits(lowerer, value, def.arms[named.tag]!) ? named.tag : -1;
}
return def.arms.findIndex((arm) => comptimeFits(lowerer, value, arm));
}
/** Whether `value` bakes into the bakeable type `t` without a mismatch:
* the arm choice of a union literal. Records are exact, as in
* comptimeValueToIr; an absent field reads as undefined. */
function comptimeFits(lowerer: Lowerer, value: unknown, t: IrType): boolean {
switch (t.kind) {
case "f64":
return typeof value === "number" && Number.isFinite(value);
case "string":
return typeof value === "string";
case "bool":
return typeof value === "boolean";
case "nullT":
return value === null;
case "undefinedT":
return value === undefined;
case "array":
return Array.isArray(value) && value.every((el) => comptimeFits(lowerer, el, t.elem));
case "record": {
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
const shape = lowerer.shapes.get(t.shapeId);
if (!shape) return false;
const obj = value as Record<string, unknown>;
const declared = new Set(shape.fields.map((f) => f.name));
return (
Object.keys(obj).every((key) => declared.has(key)) &&
shape.fields.every((f) => comptimeFits(lowerer, obj[f.name], f.type))
);
}
case "union":
return comptimeUnionArm(lowerer, t.unionId, value) >= 0;
default:
return false;
}
@@ -265,6 +349,23 @@ export function comptimeValueToIr(
}));
return { kind: "recordLit", fields, type: expected, loc };
}
case "union": {
const arms = lowerer.unions.get(expected.unionId)!.arms; // bakeable-checked
const tag = comptimeUnionArm(lowerer, expected.unionId, value);
if (tag < 0) fail(describeComptimeValue(value));
const arm = arms[tag]!;
const payload: IrExpr = isUnitType(arm)
? { kind: "unitLit", unit: arm.kind === "nullT" ? "null" : "undefined", type: arm, loc }
: lowerer.comptimeValueToIr(value, arm, path, blame);
return {
kind: "unionWrap",
unionId: expected.unionId,
tag,
value: payload,
type: expected,
loc,
};
}
default:
// comptimeBakeable already fenced these off.
throw new InternalCompilerError(`lowerer bug: unbakeable comptime target '${expected.kind}'`);
@@ -1245,7 +1245,19 @@ export function lowerEmitterMethodCall(
);
}
const p = cbT.params[0];
if (p !== undefined && !(p.kind === "bytes" && p.elem === "u8") && p.kind !== "string") {
// A `Buffer | string` chunk (NodeJS.ReadableStream's listener
// shape) takes whichever side the stream delivers.
const chunkUnion =
p?.kind === "union" &&
(lowerer.unions.get(p.unionId)?.arms ?? []).every(
(arm) => (arm.kind === "bytes" && arm.elem === "u8") || arm.kind === "string",
);
if (
p !== undefined &&
!(p.kind === "bytes" && p.elem === "u8") &&
p.kind !== "string" &&
!chunkUnion
) {
lowerer.noLowering(
`a 'data' listener whose chunk parameter is '${lowerer.fmt(p)}'`,
args[1]!,
@@ -151,6 +151,21 @@ export type FieldTarget =
// (the write-slot type; reads arm it with undefined under
// noUncheckedIndexedAccess in fieldGetExpr).
| { container: "recordOvf"; obj: IrExpr; shapeId: string; field: string; fieldType: IrType }
// A computed key of a record in a read-modify-write (`r[k] += v`,
// `r[k]++`, `r[k] ??= v`; keyed-field-targets.ts): `keys` lists the
// declared fields a literal-union key can name, which share fieldType;
// null keys mean a string key into a pure index-signature record, whose
// entries fieldType types. `key` is null when the one literal is static.
| {
container: "recordKeyed";
obj: IrExpr;
shapeId: string;
field: string;
keys: readonly string[] | null;
key: IrExpr | null;
fieldType: IrType;
missNaN?: true;
}
| { container: "accessor"; obj: IrExpr; className: string; field: string; fieldType: IrType }
// A RECORD accessor property (an object-literal `get x()`/`set x(v)` —
// the shape carries %get:/%set: closure slots): reads call the getter
@@ -2245,6 +2260,19 @@ export function caughtRead(
if (narrowed?.kind === "dyn") {
return { kind: "caughtToDyn", value: ref, type: DYN, loc };
}
// A binding a user type predicate (or an assertion) narrowed to a
// structural type (`isErrorLike(e)` → `{ message: string; … }`, an
// intersection like `TypeError & { code: unknown }`): the thrown value is
// still whatever was thrown, so it crosses as a checked-dynamic value and
// each member read validates its own result, as JavaScript reads it.
const checked = lowerer.typeOf(node);
if (
narrowed?.kind === "record" ||
narrowed?.kind === "union" ||
(narrowed === null && (checked.flags & (ts.TypeFlags.Object | ts.TypeFlags.Intersection)) !== 0)
) {
return { kind: "caughtToDyn", value: ref, type: DYN, loc };
}
lowerer.unsupported("SC1063", node);
}
@@ -1,3 +1,4 @@
import { declareTypedArguments } from "./typed-arguments.js";
import { varRef } from "../../ir/build.js";
import { InternalCompilerError } from "../../errors.js";
import * as ts from "../ts7/adapter.js";
@@ -309,6 +310,8 @@ export function lowerLambda(
: null;
const { params, prologue } = lowerer.declareParams(node.parameters.nodes, shapes);
if (receiver) prologue.unshift(receiver);
if (ts.isFunctionExpression(node) && lowerer.typedArgumentsLambdas.has(node))
declareTypedArguments(lowerer, node, shapes, params, prologue);
declareFunctionArguments(
lowerer,
node,
@@ -1,4 +1,5 @@
import { dynUndefinedExpr, boolLit, helperLoc, numLit, strLit, varRef } from "../../ir/build.js";
import { builtinClassDynValueOf, builtinClassKindOf } from "./builtins/builtin-class-dyn.js";
import { InternalCompilerError } from "../../errors.js";
import * as ts from "../ts7/adapter.js";
@@ -2609,8 +2610,15 @@ function lowerUtilCallableCall(
const arity = member === "deprecate" ? 4 : member === "callbackify" ? 1 : 2;
// Program classes keep static prototypes: there is no function object
// whose prototype chain the runtime could relink or wrap.
// EventEmitter and the stream classes are the exception as superCtor:
// the runtime has untyped constructors for them (builtin-class-dyn.ts).
const classArgs = member === "inherits" ? 2 : member === "deprecate" ? 1 : 0;
for (const arg of expr.arguments.nodes.slice(0, classArgs)) {
const builtinBase =
member === "inherits" && expr.arguments.nodes[1] !== undefined
? builtinClassKindOf(lowerer, expr.arguments.nodes[1])
: null;
for (const [index, arg] of expr.arguments.nodes.slice(0, classArgs).entries()) {
if (index === 1 && builtinBase !== null) continue;
if (lowerer.mapTypeOf(lowerer.typeOf(arg))?.kind === "classval")
lowerer.noLowering(
`util.${member} over a class constructor`,
@@ -2629,9 +2637,12 @@ function lowerUtilCallableCall(
const stmts: IrStmt[] = [];
const args: IrExpr[] = [];
for (let i = 0; i < arity; i++) {
const input: IrExpr = expr.arguments.nodes[i]
? lowerer.lowerExprExpecting(expr.arguments.nodes[i]!, DYN)
: dynUndefinedExpr(loc);
const node = expr.arguments.nodes[i];
const input: IrExpr = !node
? dynUndefinedExpr(loc)
: i === 1 && builtinBase !== null
? (builtinClassDynValueOf(lowerer, node) ?? lowerer.lowerExprExpecting(node, DYN))
: lowerer.lowerExprExpecting(node, DYN);
const saved = lowerer.declareHiddenLocal("%utilInput", DYN);
stmts.push({ kind: "varDecl", localId: saved.id, init: input, loc });
args.push(varRef(saved.id, DYN, loc));
@@ -872,6 +872,14 @@ export function moduleArtifacts(
valueInstanceOfRoots.add(root.def.name);
}
break;
case "libCall":
// The general stream representation's child stdio read bridges
// through the runtime install that boxes both stream classes.
if (node.fn === "child.stdioStream") {
visitClass("%Readable");
visitClass("%Writable");
}
break;
case "dynFrom":
if (boxedChild(node.value.type)) {
// A generic child can expose native stdio without importing stream.
@@ -4,6 +4,7 @@ import { InternalCompilerError } from "../../errors.js";
import * as ts from "../ts7/adapter.js";
import { lowerToNumberArgument } from "./lower-exprs.js";
import type { Lowerer } from "./lowerer.js";
import { isDeclaredInAmbientNamespace } from "../type-mapper.js";
import {
arrayOf,
BOOL,
@@ -1023,7 +1024,15 @@ export function fenceStaticAbortControllerMemberRead(
function isStdlibFetchInterface(lowerer: Lowerer, node: ts.Node, owner: string): ts.Symbol | null {
const type = lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(node));
for (const symbol of [type.getSymbol(), type.getAliasSymbol()]) {
if (symbol?.name === owner && lowerer.isStdlibSymbol(symbol)) return symbol;
// NodeJS.ReadableStream shares the WHATWG interface's name.
if (
symbol?.name === owner &&
lowerer.isStdlibSymbol(symbol) &&
!symbol.declarations.some(
(d) => ts.isInterfaceDeclaration(d) && isDeclaredInAmbientNamespace(d, "NodeJS"),
)
)
return symbol;
}
return null;
}
@@ -1,5 +1,7 @@
import { lowerTupleAliasDecl, stripTypeWrappers } from "./typed-arguments.js";
import { declaredMemberOf } from "./expressions/property-access.js";
import { hostsAll } from "./expressions/hosted-narrowing.js";
import { requestGeneralStreamSurface } from "./general-streams.js";
import {
matchAllRowsReadByElements,
storedMatchAllRowsReadByElements,
@@ -2402,20 +2404,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);
@@ -4736,6 +4748,10 @@ export function lowerVarDecl(
}
}
if (!ts.isIdentifier(decl.name)) lowerer.unsupported("SC1031", decl.name);
{
const alias = lowerTupleAliasDecl(lowerer, decl);
if (alias) return alias;
}
// A NULLISH generic binding (`const i: I<A & B> = null as any` — the
// declared type has no mapping, the value is provably null/undefined
@@ -5156,6 +5172,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
@@ -8971,6 +8997,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,
@@ -49,6 +49,7 @@ import { sideEffectFreeOptionValue } from "./surfaces.js";
import { lowerDynObjectLiteral } from "./expressions/object-literals.js";
import { dynUndefinedExpr, varRef } from "../../ir/build.js";
import { genResultRecord } from "../type-mapper.js";
import { requestGeneralStreamSurface } from "./general-streams.js";
import { extractIteratorValue, genStep, genValue, stepDrivable } from "./lower-generators.js";
import {
BOOL,
@@ -586,6 +587,33 @@ function dynOptionsValue(lowerer: Lowerer, arg: ts.Expression): IrExpr | null {
return v.type.kind === "dyn" ? v : null;
}
/** @types/node declares the transform/flush completion callback as
* `(error?: Error | null, data?: any) => void`. Outside objectMode Node
* accepts only a Buffer, a string or nothing as `data`, so the callback
* parameter is typed with that union while the option callback lowers: a
* checked-dynamic value passed as `data` converts at the call. Returns the
* pinned parameter symbol (the caller unpins it), or null when the
* declared type needs no pin. */
function pinAnyDataCompletion(
lowerer: Lowerer,
which: string,
fnNode: ts.MethodDeclaration | ts.FunctionExpression | ts.ArrowFunction,
): ts.Symbol | null {
const index = which === "transform" ? 2 : which === "flush" ? 0 : -1;
const param = index < 0 ? undefined : fnNode.parameters.nodes[index];
if (param === undefined || !ts.isIdentifier(param.name) || param.dotDotDotToken) return null;
const symbol = lowerer.checker.getSymbolAtLocation(param.name);
if (!symbol || lowerer.stageSourceTypes.has(symbol)) return null;
const declared = lowerer.paramShape(param).type;
if (declared.kind !== "func" || declared.rest || declared.params[1]?.kind !== "dyn") return null;
const data = unionOf(lowerer, [BYTES, STRING, { kind: "undefinedT" }]);
lowerer.stageSourceTypes.set(
symbol,
funcOf([declared.params[0]!, data, ...declared.params.slice(2)], declared.ret),
);
return symbol;
}
/** Parses the options object literal of a stream constructor. `which`
* names the callbacks this class accepts, in canonical flag order. */
function parseStreamOptions(
@@ -671,22 +699,27 @@ function parseStreamOptions(
continue;
}
const full = tuples[name]!.map((t) => (t === "fn" ? null : t));
// The completion-callback tail: typed by the checker contextually;
// accept the user's declared func type as the tuple's entry so the
// prefix check compares like with like.
const { funcType } = lowerer.lambdaSignature(fnNode);
const tuple: IrType[] = full.map((t, i) => {
if (t !== null) return t;
const declared = funcType.params[i];
if (declared !== undefined && declared.kind !== "func" && declared.kind !== "dyn") {
lowerer.noLowering(
`a '${name}' callback whose parameter ${i} is '${lowerer.fmt(declared)}' where Node passes the completion callback`,
fnNode!,
);
}
return declared ?? funcOf([], VOID);
});
present.set(name, lowerStreamCallback(lowerer, name, fnNode, thisType, tuple));
const pinned = pinAnyDataCompletion(lowerer, name, fnNode);
try {
// The completion-callback tail: typed by the checker contextually;
// accept the user's declared func type as the tuple's entry so the
// prefix check compares like with like.
const { funcType } = lowerer.lambdaSignature(fnNode);
const tuple: IrType[] = full.map((t, i) => {
if (t !== null) return t;
const declared = funcType.params[i];
if (declared !== undefined && declared.kind !== "func" && declared.kind !== "dyn") {
lowerer.noLowering(
`a '${name}' callback whose parameter ${i} is '${lowerer.fmt(declared)}' where Node passes the completion callback`,
fnNode!,
);
}
return declared ?? funcOf([], VOID);
});
present.set(name, lowerStreamCallback(lowerer, name, fnNode, thisType, tuple));
} finally {
if (pinned) lowerer.stageSourceTypes.delete(pinned);
}
continue;
}
// `{ signal }` shorthand: the binding is the value.
@@ -2044,6 +2077,24 @@ function lowerStreamArg(
const v = isHashValueType(lowered.type)
? hashStreamOf(lowered, what === "pipeline" || what === "compose")
: lowered;
// A checked-dynamic pipeline stage (an untyped source or destination):
// a boxed native stream unwraps at run time; other values (iterables,
// functions) throw the not-supported Error there.
if (v.type.kind === "dyn" && what === "pipeline") {
return {
kind: "libCall",
fn: "stream.unboxDyn",
args: [
v,
strLit(
"pipeline stages other than native streams (iterables, generators, plain functions, and { readable, writable } pairs) are not supported yet",
locOf(node),
),
],
type: { kind: "object", className: "%Duplex" },
loc: locOf(node),
};
}
const info = v.type.kind === "object" ? lowerer.classes.get(v.type.className) : undefined;
if (!streamSidesOf(lowerer, info)) {
// A provably-non-stream value in a JS source (the invalid-input
@@ -3296,6 +3347,124 @@ function pipeEndOption(lowerer: Lowerer, options: ts.Expression | undefined, loc
return end;
}
/** `readable.pipe(process.stdout)`: the process stream joins the pipe as
* an fd-backed Writable (stream.stdioWritable), and — like Node, which
* never ends process.stdout or process.stderr from a pipe — the source's
* end does not end it. pipe() answers the process stream itself. */
function pipeIntoStdio(
lowerer: Lowerer,
call: ts.CallExpression,
access: ts.PropertyAccessExpression,
target: IrExpr,
): IrExpr {
const loc = locOf(call);
// JS order: the receiver, then the destination, then the options.
const receiverValue = lowerer.lowerExpr(access.expression);
const source = lowerer.declareHiddenLocal("%pipeSource", receiverValue.type);
const stdio = lowerer.declareHiddenLocal("%pipeStdio", target.type);
const options = call.arguments.nodes[1];
const stmts: IrStmt[] = [
{ kind: "varDecl", localId: source.id, init: receiverValue, loc },
{ kind: "varDecl", localId: stdio.id, init: target, loc },
];
// The options literal still evaluates; its end flag has no effect here.
if (options !== undefined) {
stmts.push({ kind: "exprStmt", expr: pipeEndOption(lowerer, options, loc), loc });
}
const receiver: IrExpr = { kind: "varRef", localId: source.id, type: receiverValue.type, loc };
const writable: IrType = { kind: "object", className: "%Writable" };
stmts.push({
kind: "exprStmt",
expr: {
kind: "libCall",
fn: "readable.pipe",
args: [
receiver,
{
kind: "libCall",
fn: "stream.stdioWritable",
args: [{ kind: "varRef", localId: stdio.id, type: target.type, loc }],
type: writable,
loc,
},
boolLit(false, loc),
],
type: writable,
loc,
},
loc,
});
return {
kind: "seqExpr",
stmts,
result: { kind: "varRef", localId: stdio.id, type: target.type, loc },
type: target.type,
loc,
};
}
/** The pipe() libCall for a destination of type `dst`. Under the general
* stream representation a Writable slot can hold the converted
* process.stdout / process.stderr, which a pipe never ends (as in Node), so
* the pipe takes readable.pipeGeneral, which checks for them. */
function generalPipe(lowerer: Lowerer, dst: IrType): "readable.pipe" | "readable.pipeGeneral" {
return lowerer.generalNodeStreams && dst.kind === "object" && dst.className === "%Writable"
? "readable.pipeGeneral"
: "readable.pipe";
}
/** `readable.pipe(untyped)`: a checked-dynamic destination unwraps a boxed
* native stream at run time (stream.unboxDyn; other values throw the
* not-supported Error), and pipe() answers the destination value itself. */
function pipeIntoDyn(
lowerer: Lowerer,
call: ts.CallExpression,
access: ts.PropertyAccessExpression,
target: IrExpr,
): IrExpr {
const loc = locOf(call);
const receiverValue = lowerer.lowerExpr(access.expression);
const source = lowerer.declareHiddenLocal("%pipeSource", receiverValue.type);
const destination = lowerer.declareHiddenLocal("%pipeDestination", target.type);
const stmts: IrStmt[] = [
{ kind: "varDecl", localId: source.id, init: receiverValue, loc },
{ kind: "varDecl", localId: destination.id, init: target, loc },
];
const end = pipeEndOption(lowerer, call.arguments.nodes[1], loc);
const duplex: IrType = { kind: "object", className: "%Duplex" };
stmts.push({
kind: "exprStmt",
expr: {
kind: "libCall",
fn: lowerer.generalNodeStreams ? "readable.pipeGeneral" : "readable.pipe",
args: [
{ kind: "varRef", localId: source.id, type: receiverValue.type, loc },
{
kind: "libCall",
fn: "stream.unboxDyn",
args: [
{ kind: "varRef", localId: destination.id, type: target.type, loc },
strLit("pipe destinations other than native streams are not supported yet", loc),
],
type: duplex,
loc,
},
end,
],
type: duplex,
loc,
},
loc,
});
return {
kind: "seqExpr",
stmts,
result: { kind: "varRef", localId: destination.id, type: target.type, loc },
type: target.type,
loc,
};
}
export function lowerStreamMethodCall(
lowerer: Lowerer,
call: ts.CallExpression,
@@ -3545,22 +3714,26 @@ export function lowerStreamMethodCall(
loc,
};
}
const dst = lowerer.lowerExpr(args[0]!);
const target = lowerer.lowerExpr(args[0]!);
if (target.type.kind === "procStream") return pipeIntoStdio(lowerer, call, access, target);
if (target.type.kind === "dyn") return pipeIntoDyn(lowerer, call, access, target);
const dst = target;
const dstInfo =
dst.type.kind === "object" ? lowerer.classes.get(dst.type.className) : undefined;
const dstSides = streamSidesOf(lowerer, dstInfo);
if (dstSides !== "w" && dstSides !== "rw") {
requestGeneralStreamSurface(lowerer, args[0]!);
lowerer.noLowering(
`pipe into a '${lowerer.fmt(dst.type)}'`,
args[0]!,
"the destination must be a stream Writable/Duplex (process streams and sockets are not pipe destinations yet)",
"the destination must be a stream Writable/Duplex, process.stdout or process.stderr (sockets are not pipe destinations yet)",
);
}
const end = pipeEndOption(lowerer, args[1], loc);
const receiver = lowerer.lowerExpr(access.expression);
return {
kind: "libCall",
fn: "readable.pipe",
fn: generalPipe(lowerer, dst.type),
args: [receiver, dst, end],
type: dst.type,
loc,
@@ -1,5 +1,8 @@
import { onlyForwarded } from "./typed-arguments.js";
import { needsGeneralNodeStreams, stdioIntoGeneralStream } from "./general-streams.js";
import { extendAssertedRecordMembers } from "./assertion-members.js";
import { jsDynamicDeclarations } from "./js-dynamic-objects.js";
import { builtinClassValue, builtinMemberClassKind } from "./builtins/builtin-class-dyn.js";
import {
lifetimeRegionType,
lowerLifetimeRegionCall,
@@ -883,6 +886,10 @@ export interface LowererMode {
/** Statically resolved child_process.fork program roots, in stable target-id order. */
forkTargets?: readonly ts.SourceFile[];
frontendServices?: FrontendServices | undefined;
/** The program's general stream representation (TypeMapperCtx.
* generalNodeStreams). lowerToIr turns it on only after a pass without
* it saw a stream flow that needs it (requiresGeneralNodeStreams). */
generalNodeStreams?: boolean;
/** LoadResult.inlineRequireCycleCalls. */
inlineRequireCycleCalls?: ReadonlySet<ts.Node> | undefined;
/** LowerOptions.libraryCallbacks (see there). */
@@ -1039,10 +1046,40 @@ export function lowerToIr(
for (const d of extraModuleCycleDiags) reachableEmit.pushDiag(d);
for (const d of ffiValidation.diagnostics) reachableEmit.pushDiag(d);
const emitted = reachableEmit.emitReachable(options.libRoots);
const { reachable } = emitted;
let { reachable } = emitted;
let result = emitted.result;
let resultLowerer = reachableEmit;
timing("reachable-emit", { reachable: reachable.size });
// The general stream representation (TypeMapperCtx.generalNodeStreams)
// is a per-program decision: only a program whose reachable code moved
// a general stream into a stdio-represented stream slot lowers again
// with it. Every other program keeps the single pass above unchanged.
const generalNodeStreams = reachableEmit.requiresGeneralNodeStreams;
if (generalNodeStreams) {
const general = new Lowerer(program, entry, moduleOrder, {
frontendServices: options.frontendServices,
projectCheckJs: options.projectCheckJs ?? false,
targetPlatform,
startupCrash,
inlineRequireCycleCalls: options.inlineRequireCycleCalls,
ffiImports,
libraryCallbacks,
forkTargets,
workerTargets,
externalTypes,
externalTypeSpecifiersByFile,
tierMode,
ffiBindingSymbols: ffiValidation.symbolsByName,
generalNodeStreams: true,
});
for (const d of extraModuleCycleDiags) general.pushDiag(d);
for (const d of ffiValidation.diagnostics) general.pushDiag(d);
const generalEmitted = general.emitReachable(options.libRoots);
reachable = generalEmitted.reachable;
result = generalEmitted.result;
resultLowerer = general;
timing("general-streams-relower", { reachable: reachable.size });
}
// A generic class-rest support decision can depend on record metadata
// whose historical owner is discovered only while retained bodies lower.
// If the settled answer is a fence, rerun the ordinary reachable emit so
@@ -1050,10 +1087,11 @@ export function lowerToIr(
// declarators stay unvisited, bindings block, cascades and stats match the
// historical compiler. This is a rare compatibility fallback; programs
// without such a settled fence retain checker-backed IR exactly once.
if (reachableEmit.requiresHistoricalOrderRelower) {
if (resultLowerer.requiresHistoricalOrderRelower) {
const emit = new Lowerer(program, entry, moduleOrder, {
frontendServices: options.frontendServices,
projectCheckJs: options.projectCheckJs ?? false,
...(generalNodeStreams ? { generalNodeStreams: true } : {}),
reachable,
targetPlatform,
startupCrash,
@@ -1145,24 +1183,34 @@ export function lowerToIr(
timing("wasm-discovery", { modules: wasmModules.length });
}
if (options.coverage !== true) return result;
const remainder = new Lowerer(program, entry, moduleOrder, {
frontendServices: options.frontendServices,
projectCheckJs: options.projectCheckJs ?? false,
reachable,
remainder: true,
alreadyFlushed: resultLowerer.flushedSymbols,
inlineRequireCycleCalls: options.inlineRequireCycleCalls,
targetPlatform,
ffiImports,
libraryCallbacks,
forkTargets,
workerTargets,
ffiBindingSymbols: ffiValidation.symbolsByName,
externalTypes,
externalTypeSpecifiersByFile,
tierMode,
});
const rem = remainder.run();
const remainderPass = (general: boolean): Lowerer =>
new Lowerer(program, entry, moduleOrder, {
frontendServices: options.frontendServices,
projectCheckJs: options.projectCheckJs ?? false,
...(general ? { generalNodeStreams: true } : {}),
reachable,
remainder: true,
alreadyFlushed: resultLowerer.flushedSymbols,
inlineRequireCycleCalls: options.inlineRequireCycleCalls,
targetPlatform,
ffiImports,
libraryCallbacks,
forkTargets,
workerTargets,
ffiBindingSymbols: ffiValidation.symbolsByName,
externalTypes,
externalTypeSpecifiersByFile,
tierMode,
});
let remainder = remainderPass(generalNodeStreams);
let rem = remainder.run();
// Unreached code reports what it would compile to once reached: a
// stream flow there that needs the general representation lowers the
// remainder again with it, as reachable emit would.
if (!generalNodeStreams && remainder.requiresGeneralNodeStreams) {
remainder = remainderPass(true);
rem = remainder.run();
}
timing("coverage-remainder");
// Tiers (tier-analysis.ts): reached statements in dynamic-tier functions,
// and with tierReport the report-only demotion analysis.
@@ -1948,6 +1996,18 @@ export class Lowerer {
* type parameters are bound to `unknown`, so lambdaSignature admits
* them. */
readonly erasingLambdas = new Set<ts.Node>();
/** Function expressions lowered against a fixed target signature with a
* tuple-typed `arguments` (typed-arguments.ts). */
readonly typedArgumentsLambdas = new Set<ts.Node>();
/** Const bindings holding a forwarded fixed tuple (`const args =
* arguments as any` in a typed-arguments function) → the tuple type. */
readonly tupleAliasSymbols = new Map<ts.Symbol, IrType>();
/** `??` expressions whose result is the optional callback's own function
* type (an adapted function default); their bindings take that type. */
readonly nullishFuncResults = new Set<ts.Node>();
/** The static names of the current generic instance's name-keyed
* function parameters (static-function-names.ts). */
staticFnNames: Map<ts.Symbol, string | null> | null = null;
/** Nesting depth of erased generic bodies being lowered (erased lambdas
* and erased instances). */
erasingDepth = 0;
@@ -2049,7 +2109,9 @@ export class Lowerer {
/** A pipeline() function stage's `source` parameter: the IR type of what
* the pipeline passes it (the previous stage as an async iterable), set
* while the stage lowers. Parameter typing and `for await` heads read
* it before the checker's `AsyncIterable<T>`. */
* it before the checker's `AsyncIterable<T>`. A stream option callback's
* completion parameter declared with an any-typed `data` is pinned here
* too while the callback lowers (pinAnyDataCompletion). */
readonly stageSourceTypes = new Map<ts.Symbol, IrType>();
/** Set when a match result lowers to execGroups/matchGroups, the inputs
* of regex span folding. */
@@ -2527,6 +2589,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. */
@@ -2988,6 +3056,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. */
@@ -3116,6 +3185,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);
@@ -3212,6 +3282,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 &&
@@ -8496,6 +8567,8 @@ export class Lowerer {
* validator ICEs on them as the usual backstop. */
requireExactShape(node: ts.Node, actual: IrType, expected: IrType): void {
if (typeEquals(actual, expected)) return;
if (!this.generalNodeStreams && needsGeneralNodeStreams(this, actual, expected))
this.requiresGeneralNodeStreams = true;
// dyn mismatches first. A dyn ('unknown') value flowing into a typed
// slot needs a CHECKED cast — the hint points at `as <type>`; tsc
// usually rejects this before we do, so the fence is mostly defensive.
@@ -8713,6 +8786,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)) {
@@ -10947,6 +11023,70 @@ 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 tupleType = 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);
const fn = decl.parent;
const symbol = this.checker.getSymbolAtLocation(nameNode);
if (
fn !== undefined &&
symbol !== undefined &&
onlyForwarded(
this,
fn,
(id) => id !== nameNode && this.checker.getSymbolAtLocation(id) === symbol,
)
) {
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 });
}
/** 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`
@@ -10983,6 +11123,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;
@@ -11314,6 +11465,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
@@ -12628,6 +12786,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",
@@ -0,0 +1,219 @@
import * as ts from "../ts7/adapter.js";
import { isJsSourceFile } from "../program.js";
import type { Lowerer } from "./lowerer.js";
import type { GenericFnInfo } from "./generic-functions.js";
import { stripTypeWrappers } from "./typed-arguments.js";
/** `fn.name` inside generic higher-order functions.
*
* Native closures carry no name, but a generic wrapper is instantiated per
* call site anyway: when its body reads `.name` of a function parameter,
* the instantiation key also carries the static name of that call's
* argument (the function declaration's name, a const binding's inferred
* name, the name of the function a wrapper call returns), and the read
* folds to that string. A call whose argument's name is only known at run
* time keeps a precise refusal at the read. */
/** Parameter index → symbol for the parameters whose `.name` the generic
* body reads (and never reassigns). Computed once per generic. */
export function nameReadParams(lowerer: Lowerer, info: GenericFnInfo): Map<number, ts.Symbol> {
if (info.nameReadParams) return info.nameReadParams;
const out = new Map<number, ts.Symbol>();
// JavaScript generics read function names through the checked-value
// box, which carries the runtime name already.
if (isJsSourceFile(info.decl.getSourceFile())) {
info.nameReadParams = out;
return out;
}
const bySymbol = new Map<ts.Symbol, number>();
info.decl.parameters.nodes.forEach((param, i) => {
if (!ts.isIdentifier(param.name) || param.dotDotDotToken) return;
if (!functionTypedParameter(lowerer, param)) return;
const symbol = lowerer.checker.getSymbolAtLocation(param.name);
if (symbol) bySymbol.set(symbol, i);
});
if (bySymbol.size > 0) {
const written = new Set<ts.Symbol>();
const read = new Set<ts.Symbol>();
ts.walkPreorder(info.decl, (node) => {
if (ts.isTypeNode(node)) return "skip";
if (
ts.isPropertyAccessExpression(node) &&
node.name.text === "name" &&
ts.isIdentifier(node.expression)
) {
const symbol = lowerer.checker.getSymbolAtLocation(node.expression);
if (symbol && bySymbol.has(symbol)) read.add(symbol);
}
if (
ts.isBinaryExpression(node) &&
node.operatorToken.kind >= ts.SyntaxKind.FirstAssignment &&
node.operatorToken.kind <= ts.SyntaxKind.LastAssignment &&
ts.isIdentifier(node.left)
) {
const symbol = lowerer.checker.getSymbolAtLocation(node.left);
if (symbol) written.add(symbol);
}
return undefined;
});
for (const symbol of read) {
if (!written.has(symbol)) out.set(bySymbol.get(symbol)!, symbol);
}
}
info.nameReadParams = out;
return out;
}
/** A parameter declared with a function type, or with a type parameter
* constrained to one (`fn: F` where `F extends (...args: any) => any`). */
function functionTypedParameter(lowerer: Lowerer, param: ts.ParameterDeclaration): boolean {
const t = lowerer.typeOf(param.name);
if (lowerer.checker.getCallSignatures(t).length > 0) return true;
if (!(t.flags & ts.TypeFlags.TypeParameter)) return false;
const symbol = t.getSymbol();
const decl = symbol ? symbol.declarations.find(ts.isTypeParameterDeclaration) : undefined;
if (!decl?.constraint) return false;
const constraint = lowerer.checker.getTypeFromTypeNode(decl.constraint);
return lowerer.checker.getCallSignatures(constraint).length > 0;
}
/** The instantiation key suffix and name bindings of one call of `info`,
* or null when the body reads no parameter's name. */
export function staticNameBindings(
lowerer: Lowerer,
call: ts.CallExpression,
info: GenericFnInfo,
): { key: string; names: Map<ts.Symbol, string | null> } | null {
const params = nameReadParams(lowerer, info);
if (params.size === 0) return null;
const names = new Map<ts.Symbol, string | null>();
const parts: string[] = [];
for (const [index, symbol] of params) {
const arg = call.arguments.nodes[index];
const name = arg && !ts.isSpreadElement(arg) ? staticFunctionName(lowerer, arg) : null;
names.set(symbol, name);
parts.push(name === null ? "?" : JSON.stringify(name));
}
return { key: `@name(${parts.join(",")})`, names };
}
/** The `name` JavaScript gives the function `expr` evaluates to, when it is
* statically known; null otherwise. */
export function staticFunctionName(lowerer: Lowerer, expr: ts.Expression): string | null {
return staticNameOf(lowerer, expr, new Set());
}
function staticNameOf(lowerer: Lowerer, expr: ts.Expression, seen: Set<ts.Node>): string | null {
const e = stripTypeWrappers(expr);
if (seen.has(e)) return null;
seen.add(e);
// An anonymous function as a call argument gets no inferred name.
if (ts.isFunctionExpression(e) || ts.isArrowFunction(e)) {
return !ts.isArrowFunction(e) && e.name ? e.name.text : "";
}
if (ts.isIdentifier(e)) {
const symbol = lowerer.resolveValueSymbol(e);
if (!symbol) return null;
const bound = lowerer.staticFnNames?.get(symbol);
if (bound !== undefined) return bound;
const decl = symbol.valueDeclaration;
if (decl === undefined) return null;
if (ts.isFunctionDeclaration(decl)) return decl.name ? decl.name.text : "default";
if (
ts.isVariableDeclaration(decl) &&
ts.isIdentifier(decl.name) &&
decl.initializer !== undefined &&
(ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) !== 0
) {
const init = stripTypeWrappers(decl.initializer);
// `const f = () => …` / `const f = function () {}`: the binding names
// an anonymous function definition.
if (ts.isArrowFunction(init)) return decl.name.text;
if (ts.isFunctionExpression(init)) return init.name ? init.name.text : decl.name.text;
return staticNameOf(lowerer, init, seen);
}
return null;
}
if (ts.isCallExpression(e) && ts.isIdentifier(e.expression)) {
// A call of a generic higher-order function returns the function its
// body returns: every `return` names the same anonymous definition or
// the same local const binding.
const symbol = lowerer.resolveValueSymbol(e.expression);
const info = symbol ? lowerer.genericFnsBySymbol.get(symbol) : undefined;
if (!info) return null;
return returnedFunctionName(lowerer, info.decl, seen);
}
if (ts.isPropertyAccessExpression(e)) {
const symbol = lowerer.checker.getSymbolAtLocation(e.name);
const decl = symbol ? symbol.valueDeclaration : undefined;
if (decl && ts.isMethodDeclaration(decl) && ts.isIdentifier(decl.name)) return decl.name.text;
return null;
}
return null;
}
function returnedFunctionName(
lowerer: Lowerer,
decl: GenericFnInfo["decl"],
seen: Set<ts.Node>,
): string | null {
const body = decl.body;
if (body === undefined) return null;
if (!ts.isBlock(body)) return functionValueName(lowerer, body, seen);
let name: string | null | undefined;
let ok = true;
ts.walkPreorder(body, (node) => {
if (!ok) return "stop";
if (ts.isFunctionLike(node)) return "skip";
if (!ts.isReturnStatement(node)) return undefined;
const here = node.expression ? functionValueName(lowerer, node.expression, seen) : null;
if (here === null || (name !== undefined && name !== here)) ok = false;
else name = here;
return undefined;
});
return ok && name !== undefined ? name : null;
}
/** The name of a returned function value: an anonymous definition returned
* directly is nameless; a local const binding names its definition. */
function functionValueName(
lowerer: Lowerer,
expr: ts.Expression,
seen: Set<ts.Node>,
): string | null {
const e = stripTypeWrappers(expr);
if (ts.isFunctionExpression(e) || ts.isArrowFunction(e)) {
return !ts.isArrowFunction(e) && e.name ? e.name.text : "";
}
if (ts.isIdentifier(e)) return staticNameOf(lowerer, e, seen);
return null;
}
/** `fn.name` where `fn` is a name-keyed parameter of the generic instance
* being lowered: the folded name, or a precise refusal when this call
* site's argument has no static name. Null for every other read. */
export function staticFunctionNameRead(
lowerer: Lowerer,
expr: ts.PropertyAccessExpression,
): string | null {
const names = lowerer.staticFnNames;
if (
names === null ||
expr.name.text !== "name" ||
expr.questionDotToken !== undefined ||
!ts.isIdentifier(expr.expression)
) {
return null;
}
const symbol = lowerer.checker.getSymbolAtLocation(expr.expression);
if (!symbol || !names.has(symbol)) return null;
const name = names.get(symbol);
if (name === null || name === undefined) {
lowerer.unsupported(
"SC1090",
expr,
`reading '${expr.expression.text}.name' when the call passes a function whose name is only known at run time (generic wrappers fold the name of a function declaration, a const-bound function, or a wrapper call's result)`,
);
}
return name;
}
@@ -2452,6 +2452,27 @@ export const AMBIENT_SURFACE_FNS: readonly AmbientSurfaceRow[] = [
name: "perf_hooks.performance.clearMeasures",
fns: ["perf.clearMeasures"],
},
{
id: "node-builtin.perf_hooks.performance.eventLoopUtilization",
kind: "node-builtin",
name: "perf_hooks.performance.eventLoopUtilization",
fns: ["perf.loopStart", "perf.idleTime"],
note: "active time is the main thread's CPU time since the loop started; the one- and two-argument difference forms are supported",
},
{
id: "node-builtin.perf_hooks.monitorEventLoopDelay",
kind: "node-builtin",
name: "perf_hooks.monitorEventLoopDelay",
fns: [
"perf.eldNew",
"perf.eldEnable",
"perf.eldDisable",
"perf.eldReset",
"perf.eldPercentile",
"perf.eldStat",
],
note: "an unref'd sampling interval records the delay histogram; enable, disable, reset, percentile and the min, max, mean, stddev, count and exceeds readings are supported",
},
// ── the process global's ambient reads and authority calls
// (lowerProcessProperty/lowerProcessMethodCall — process is a
// provenance-checked global here, not an importable module).
@@ -0,0 +1,514 @@
import * as ts from "../ts7/adapter.js";
import { bodyReadsArguments } from "../arguments-usage.js";
import { isJsSourceFile, locOf } from "../program.js";
import { restTupleElementTypes, unitOnlyUnion } from "../type-mapper.js";
import { DYN, type IrExpr, type IrLocal, type IrStmt, type IrType, arrayOf } from "../../ir/ir.js";
import type { Lowerer } from "./lowerer.js";
import { captureContextArguments } from "./function-context.js";
import { isThisParameter, type ParamShape } from "./call-signatures.js";
import { tryLowerExpression } from "./expressions/try-lower-expression.js";
import { omittedArgFor } from "./call-arguments.js";
/** Variadic forwarding through a FIXED signature.
*
* A TypeScript function expression that declares no parameters and reads
* `arguments` (`function (this: unknown) { return fn.apply(this, arguments) }
* as F`) is lowered against the fixed function type its value is asserted
* to: one native slot per position of that type, and `arguments` is the
* tuple of those slots. Inside a generic instantiation F is the call
* site's concrete signature, so a higher-order wrapper specializes to the
* wrapped function's exact ABI. Forwarding (`f.apply(thisArg, args)`,
* `f(...args)`) then calls with the tuple's positions directly. */
/** `e`, with the type-only wrappers that never change its value removed. */
export function stripTypeWrappers(e: ts.Expression): ts.Expression {
let cur = e;
for (;;) {
if (ts.isParenthesizedExpression(cur)) cur = cur.expression;
else if (ts.isAsExpression(cur) || ts.isTypeAssertion(cur) || ts.isSatisfiesExpression(cur))
cur = cur.expression;
else if (ts.isNonNullExpression(cur)) cur = cur.expression;
else return cur;
}
}
/** The outermost type assertion around `node` (`node as unknown as F` → F),
* looking through parentheses; null when the value is not asserted. */
function assertedTypeNode(node: ts.Node): ts.TypeNode | null {
let cur: ts.Node = node;
let found: ts.TypeNode | null = null;
for (let parent = cur.parent; parent !== undefined; parent = cur.parent) {
if (ts.isParenthesizedExpression(parent)) {
cur = parent;
continue;
}
if ((ts.isAsExpression(parent) || ts.isTypeAssertion(parent)) && parent.expression === cur) {
found = parent.type;
cur = parent;
continue;
}
break;
}
return found;
}
/** The fixed function type a value is asserted to, or null. */
function assertedFuncType(lowerer: Lowerer, node: ts.Node): (IrType & { kind: "func" }) | null {
const typeNode = assertedTypeNode(node);
if (!typeNode) return null;
const mapped = lowerer.mapTypeOf(lowerer.checker.getTypeFromTypeNode(typeNode));
return mapped?.kind === "func" ? mapped : null;
}
function sameFuncType(a: IrType & { kind: "func" }, b: IrType & { kind: "func" }): boolean {
return JSON.stringify(a) === JSON.stringify(b);
}
/** A function expression whose `arguments` can be typed: TypeScript source,
* no runtime parameters (a `this` parameter is type-only), an ordinary
* (non-generator) function that reads its own `arguments`. */
export function typedArgumentsCandidate(node: ts.Node): node is ts.FunctionExpression {
return (
ts.isFunctionExpression(node) &&
node.asteriskToken === undefined &&
!isJsSourceFile(node.getSourceFile()) &&
node.parameters.nodes.every(isThisParameter) &&
bodyReadsArguments(node)
);
}
/** The fixed signature a typed-arguments function expression is lowered
* against: the type its value is asserted to (`function () {…} as F`), the
* one type every use of its const binding asserts it to (`const w =
* function () {…}; return w as unknown as F`), or its contextual function
* type. Null when none names one fixed-arity signature. */
export function typedArgumentsTarget(
lowerer: Lowerer,
node: ts.FunctionExpression,
): (IrType & { kind: "func" }) | null {
const fixed = (t: (IrType & { kind: "func" }) | null) => (t && t.rest !== true ? t : null);
const direct = assertedFuncType(lowerer, node);
if (direct) return fixed(direct);
let top: ts.Node = node;
while (top.parent && ts.isParenthesizedExpression(top.parent)) top = top.parent;
const decl = top.parent;
if (
decl &&
ts.isVariableDeclaration(decl) &&
decl.initializer === top &&
decl.type === undefined &&
ts.isIdentifier(decl.name) &&
(ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) !== 0
) {
const symbol = lowerer.checker.getSymbolAtLocation(decl.name);
const scope = enclosingScope(decl);
if (symbol && scope) {
let target: (IrType & { kind: "func" }) | null = null;
let ok = true;
let uses = 0;
ts.walkPreorder(scope, (n) => {
if (!ok) return "stop";
if (n === decl.name || !ts.isIdentifier(n) || n.text !== decl.name.text) return undefined;
if (lowerer.checker.getSymbolAtLocation(n) !== symbol) return undefined;
uses++;
const asserted = assertedFuncType(lowerer, n);
if (!asserted || (target && !sameFuncType(target, asserted))) ok = false;
else target = asserted;
return undefined;
});
if (ok && uses > 0) return fixed(target);
}
}
const ctxType = lowerer.checker.getContextualType(node);
if (!ctxType) return null;
const sigs = lowerer.checker.getCallSignatures(lowerer.checker.getNonNullableType(ctxType));
if (sigs.length !== 1) return null;
const mapped = lowerer.mapTypeOf(lowerer.checker.getNonNullableType(ctxType));
return mapped?.kind === "func" ? fixed(mapped) : null;
}
function enclosingScope(node: ts.Node): ts.Node | null {
for (let cur = node.parent; cur !== undefined; cur = cur.parent) {
if (ts.isSourceFile(cur) || (ts.isFunctionLike(cur) && cur.body !== undefined)) return cur;
}
return null;
}
/** The tuple type of a fixed parameter list, interned exactly as the
* spelled tuple (and `Parameters<F>`) map: positional fields, or the
* empty-array form for no parameters. */
export function tupleTypeOfParams(lowerer: Lowerer, params: readonly IrType[]): IrType {
if (params.length === 0) return arrayOf(unitOnlyUnion(lowerer.unions));
const fields = params.map((type, i) => ({ name: String(i), type }));
fields.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
return { kind: "record", shapeId: lowerer.shapes.intern(fields, true) };
}
/** The ABI shapes of a typed-arguments function: one required slot per
* position of its target signature. */
export function typedArgumentsShapes(target: IrType & { kind: "func" }): ParamShape[] {
return target.params.map((type) => ({ type, mode: "required" }));
}
/** Declares the hidden positional slots of a typed-arguments function and
* binds its `arguments` to their tuple. */
export function declareTypedArguments(
lowerer: Lowerer,
node: ts.FunctionExpression,
shapes: readonly ParamShape[],
params: { localId: string; name: string; type: IrType }[],
prologue: IrStmt[],
): void {
const loc = locOf(node);
const slots = shapes.map((shape, i) => {
const slot = lowerer.declareHiddenLocal(`%arguments.${i}`, shape.type);
params.push({ localId: slot.id, name: `%arguments.${i}`, type: shape.type });
return slot;
});
const tupleType = tupleTypeOfParams(
lowerer,
shapes.map((s) => s.type),
);
const local = lowerer.declareHiddenLocal("%arguments", tupleType);
lowerer.ctx.argumentsLocal = local;
// Pure forwarding (every read is forwarded, counted, or aliased by a
// binding that is itself only forwarded): no tuple exists at all.
if (
onlyForwarded(lowerer, node, (id) => id.text === "arguments" && argumentsOwner(id) === node)
) {
lowerer.ctx.pureTuples.set(local.id, slots);
return;
}
prologue.push({
kind: "varDecl",
localId: local.id,
init: lowerer.tupleOfSlots(slots, tupleType, node, loc),
loc,
});
}
/** Whether every read of a tuple binding in `fn` (identified by `isRoot`)
* only forwards it: `f.apply(t, x)`, `g(...x)` into a program function,
* `x.length`, or a const alias whose own reads only forward. Reads inside
* nested functions (arrows included) capture the binding and need the
* tuple. Type assertions around a read never matter. */
export function onlyForwarded(
lowerer: Lowerer,
fn: ts.Node,
isRoot: (id: ts.Identifier) => boolean,
): boolean {
const body = (fn as ts.Node & { body?: ts.Node }).body;
if (body === undefined) return false;
const roots: ((id: ts.Identifier) => boolean)[] = [isRoot];
let ok = true;
const visit = (scope: ts.Node, test: (id: ts.Identifier) => boolean): void => {
ts.walkPreorder(scope, (n) => {
if (!ok) return "stop";
if (ts.isTypeNode(n)) return "skip";
if (!ts.isIdentifier(n) || !test(n)) return undefined;
for (let cur = n.parent; cur !== undefined && cur !== fn; cur = cur.parent) {
if (ts.isFunctionLike(cur)) {
ok = false;
return "stop";
}
}
let top: ts.Node = n;
while (
top.parent &&
(ts.isParenthesizedExpression(top.parent) ||
ts.isAsExpression(top.parent) ||
ts.isTypeAssertion(top.parent) ||
ts.isSatisfiesExpression(top.parent) ||
ts.isNonNullExpression(top.parent))
)
top = top.parent;
const p = top.parent;
if (!p) {
ok = false;
return "stop";
}
if (ts.isPropertyAccessExpression(p) && p.expression === top && p.name.text === "length")
return undefined;
if (
ts.isCallExpression(p) &&
p.arguments.nodes.length === 2 &&
p.arguments.nodes[1] === top &&
ts.isPropertyAccessExpression(p.expression) &&
p.expression.name.text === "apply" &&
p.expression.questionDotToken === undefined
)
return undefined;
if (ts.isSpreadElement(p) && p.parent && ts.isCallExpression(p.parent)) {
const sigs = lowerer.checker.getCallSignatures(
lowerer.checker.getNonNullableType(lowerer.typeOf(p.parent.expression)),
);
const decl = sigs.length === 1 ? sigs[0]!.declaration : undefined;
if (decl && !decl.getSourceFile().isDeclarationFile) return undefined;
}
if (
ts.isVariableDeclaration(p) &&
p.initializer === top &&
ts.isIdentifier(p.name) &&
(ts.getCombinedNodeFlags(p) & ts.NodeFlags.Const) !== 0
) {
const alias = lowerer.checker.getSymbolAtLocation(p.name);
const aliasName = p.name;
if (alias) {
roots.push((id) => id !== aliasName && lowerer.checker.getSymbolAtLocation(id) === alias);
return undefined;
}
}
ok = false;
return "stop";
});
};
for (let i = 0; i < roots.length && ok; i++) visit(body, roots[i]!);
return ok;
}
/** The native slots of a pure-forwarded tuple binding read at `node` in
* the current function, or null when the tuple is materialized. */
function pureSlotsOf(lowerer: Lowerer, node: ts.Expression): IrLocal[] | null {
const operand = stripTypeWrappers(node);
if (!ts.isIdentifier(operand)) return null;
const local =
operand.text === "arguments" ? lowerer.ctx.argumentsLocal : lowerer.peekLocal(operand);
return local ? (lowerer.ctx.pureTuples.get(local.id) ?? null) : null;
}
/** The positional values `node` forwards when it is a pure-forwarded tuple
* (slot reads, in order), or null. */
export function pureForwardedValues(lowerer: Lowerer, node: ts.Expression): IrExpr[] | null {
const slots = pureSlotsOf(lowerer, node);
if (!slots) return null;
const loc = locOf(node);
return slots.map((slot) => ({ kind: "varRef", localId: slot.id, type: slot.type, loc }));
}
/** Whether `node` reads a tuple-typed `arguments` binding: the nearest
* ordinary function (arrows inherit it) is a typed-arguments function.
* Looks the binding up without allocating captures. */
function isTypedArgumentsRead(lowerer: Lowerer, node: ts.Expression): boolean {
if (!ts.isIdentifier(node) || node.text !== "arguments") return false;
const stack = lowerer.fnStack;
let owner = stack.length - 1;
while (owner > 0 && stack[owner]!.inheritsArguments) owner--;
const own = owner >= 0 ? stack[owner]!.argumentsLocal : undefined;
return own !== undefined && own !== null && own.type.kind !== "dyn";
}
/** The fixed tuple `node` forwards, or null: the typed `arguments`, a
* fixed-tuple rest parameter, or a const alias of either — seen through
* type assertions, which never change the value. */
export function forwardedTupleType(lowerer: Lowerer, node: ts.Expression): IrType | null {
const operand = stripTypeWrappers(node);
if (!ts.isIdentifier(operand)) return null;
if (operand.text === "arguments") {
if (!isTypedArgumentsRead(lowerer, operand)) return null;
return captureContextArguments(lowerer.fnStack)!.type;
}
const symbol = lowerer.resolveValueSymbol(operand);
if (!symbol) return null;
const alias = lowerer.tupleAliasSymbols.get(symbol);
if (alias) return alias;
const decl = symbol.valueDeclaration;
if (
decl !== undefined &&
ts.isParameter(decl) &&
decl.dotDotDotToken !== undefined &&
ts.isIdentifier(decl.name) &&
!isJsSourceFile(decl.getSourceFile()) &&
// Checker-only probe first: an ordinary array rest never maps here.
restTupleElementTypes(lowerer.typeOf(operand), lowerer.typeCtx) !== null
) {
const t = lowerer.mapTypeOf(lowerer.typeOf(operand));
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;
}
+593 -28
View File
@@ -980,7 +980,7 @@ function isDeclaredInAmbientModule(d: ts.Declaration, name: string): boolean {
/** True when the declaration's nearest enclosing namespace is `name`
* (`declare global { namespace NodeJS { ... } }` — the NodeJS-global
* interfaces @types/node declares outside any ambient module). */
function isDeclaredInAmbientNamespace(d: ts.Declaration, name: string): boolean {
export function isDeclaredInAmbientNamespace(d: ts.Declaration, name: string): boolean {
let node: ts.Node | undefined = d.parent;
while (node) {
if (ts.isModuleDeclaration(node) && ts.isIdentifier(node.name)) {
@@ -991,6 +991,31 @@ function isDeclaredInAmbientNamespace(d: ts.Declaration, name: string): boolean
return false;
}
/** Whether `type` is one of the stream spellings the general stream
* representation (TypeMapperCtx.generalNodeStreams) maps differently:
* NodeJS.ReadableStream / NodeJS.WritableStream, @types/node's
* stream.Readable / stream.Writable, or the fallback declarations'
* ChildStdin. */
export function isGeneralNodeStreamInterface(
type: ts.Type,
ctx: Pick<TypeMapperCtx, "isStdlibFile">,
): boolean {
const psym = type.getSymbol() ?? type.getAliasSymbol();
if (psym === undefined) return false;
return psym.declarations.some((d) => {
if (!(ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d))) return false;
if (!ctx.isStdlibFile(d.getSourceFile())) return false;
if (psym.name === "ReadableStream" || psym.name === "WritableStream")
return ts.isInterfaceDeclaration(d) && isDeclaredInAmbientNamespace(d, "NodeJS");
if (psym.name === "ChildStdin") return isDeclaredInAmbientModule(d, "child_process");
return (
(psym.name === "Readable" || psym.name === "Writable") &&
isNodeTypesPath(d.getSourceFile().fileName) &&
isDeclaredInAmbientModule(d, "stream")
);
});
}
/** A type declared in the shipped WebAssembly namespace (scriptc.d.ts):
* its modules, instances, memories, tables, globals, error classes and the
* Exports/Imports records. */
@@ -1146,6 +1171,13 @@ export interface TypeMapperCtx {
* type descriptions for project-owned values, but their imported runtime
* bindings are fenced separately by the Lowerer. */
isExternalTypeFile: (sf: ts.SourceFile) => boolean;
/** The program's general stream representation (lowerToIr decides it
* per program): NodeJS.ReadableStream / NodeJS.WritableStream and
* @types/node's stream.Readable / stream.Writable map to the runtime
* stream classes instead of the child/process stdio handles, so values
* from every stream producer share their slots. Off (the default), the
* stdio handles keep those spellings. */
generalNodeStreams?: boolean;
/** Successful context-free mappings, keyed by the
* TypeScript server's stable type id. A generated facade can reference the
* same large record/function type thousands of times; remapping its full
@@ -1817,6 +1849,16 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
contextResolutions++;
return viaIndexed;
}
// `Parameters<F>` / `ReturnType<F>` over a bound function-typed
// parameter (variadic higher-order wrappers): the checker keeps both
// conditionals symbolic inside the body, and their `infer` positions
// match a call signature, which matchInferReference cannot decide.
const viaSignature =
mapBoundSignatureAlias(type, ctx) ?? mapBoundKeyofNeverConditional(type, ctx);
if (viaSignature !== null) {
contextResolutions++;
return viaSignature;
}
// A conditional type over a bound parameter (`Infer<S["left"]>`, a
// mapped type's member): evaluate it against the bindings.
if (type.isConditionalType()) {
@@ -3179,6 +3221,20 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
// global-namespace interface real code writes in its own signatures —
// prefixStream's `output` param). The representation is the raw fd
// scalar; the lowered surface is write(string).
// Under the program's general stream representation (generalNodeStreams)
// NodeJS.WritableStream is the runtime Writable instead: the process
// streams convert into it where they flow (process.stdioWritable).
const nodeWritableInterface =
psym?.name === "WritableStream" &&
psym.declarations.some(
(d) =>
ts.isInterfaceDeclaration(d) &&
ctx.isStdlibFile(d.getSourceFile()) &&
isDeclaredInAmbientNamespace(d, "NodeJS"),
);
if (nodeWritableInterface && ctx.generalNodeStreams === true) {
return { kind: "object", className: "%Writable" };
}
if (
(psym?.name === "WriteStream" &&
psym.declarations.some(
@@ -3188,13 +3244,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
(isDeclaredInAmbientModule(d, "tty") ||
(isDeclaredInAmbientModule(d, "process") && isDeclaredInAmbientNamespace(d, "NodeJS"))),
)) ||
(psym?.name === "WritableStream" &&
psym.declarations.some(
(d) =>
ts.isInterfaceDeclaration(d) &&
ctx.isStdlibFile(d.getSourceFile()) &&
isDeclaredInAmbientNamespace(d, "NodeJS"),
))
nodeWritableInterface
) {
return PROCSTREAM_T;
}
@@ -3204,6 +3254,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
// of the broad @types class fence where they are constructed.
if (
(psym?.name === "ChildStdin" &&
ctx.generalNodeStreams !== true &&
psym.declarations.some(
(d) =>
ts.isInterfaceDeclaration(d) &&
@@ -3211,6 +3262,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
isDeclaredInAmbientModule(d, "child_process"),
)) ||
(psym?.name === "Writable" &&
ctx.generalNodeStreams !== true &&
psym.declarations.some(
(d) =>
(ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) &&
@@ -3221,6 +3273,20 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
) {
return { kind: "childWriter" };
}
// Under the general stream representation the fallback declarations'
// ChildStdin is the runtime Writable too (child.stdioStream bridges it).
if (
ctx.generalNodeStreams === true &&
psym?.name === "ChildStdin" &&
psym.declarations.some(
(d) =>
ts.isInterfaceDeclaration(d) &&
ctx.isStdlibFile(d.getSourceFile()) &&
isDeclaredInAmbientModule(d, "child_process"),
)
) {
return { kind: "object", className: "%Writable" };
}
// The static node:stream classes (the shipped fallback declarations —
// NOT @types/node, whose stream.Readable also types child stdio; under
// @types/node the childStream mapping below keeps priority and the
@@ -3228,6 +3294,9 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
// branch: the fallback's Readable must map to the runtime class, and
// under the fallback child.stdout is NodeJS.ReadableStream (its own
// branch below), so the two never collide.
// @types/node's Duplex/Transform/PassThrough classes have no child-stdio
// reading, so they map to the runtime classes too; its Readable and
// Writable join them under the general representation.
for (const [irName, rec] of RUNTIME_STREAM_CLASSES) {
if (
psym?.name === rec.lib &&
@@ -3235,38 +3304,39 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
(d) =>
ts.isClassDeclaration(d) &&
ctx.isStdlibFile(d.getSourceFile()) &&
!isNodeTypesPath(d.getSourceFile().fileName) &&
(!isNodeTypesPath(d.getSourceFile().fileName) ||
ctx.generalNodeStreams === true ||
(rec.lib !== "Readable" && rec.lib !== "Writable")) &&
isDeclaredInAmbientModule(d, "stream"),
)
) {
return { kind: "object", className: irName };
}
}
// node:zlib's stream classes (the shipped fallback declarations): their
// instances are runtime Transform streams.
// node:zlib's stream classes (the shipped fallback declarations and
// @types/node's): their instances are runtime Transform streams.
if (
psym !== undefined &&
ZLIB_STREAM_MODES.has(psym.name) &&
psym.declarations.some(
(d) =>
ts.isClassDeclaration(d) &&
(ts.isClassDeclaration(d) ||
(ts.isInterfaceDeclaration(d) && isNodeTypesPath(d.getSourceFile().fileName))) &&
ctx.isStdlibFile(d.getSourceFile()) &&
!isNodeTypesPath(d.getSourceFile().fileName) &&
isDeclaredInAmbientModule(d, "zlib"),
)
) {
return { kind: "object", className: "%Transform" };
}
// fs.ReadStream / fs.WriteStream (the shipped fallback declarations):
// their instances are runtime Readable / Writable streams
// (scr_fs_stream.c).
// fs.ReadStream / fs.WriteStream (the shipped fallback declarations and
// @types/node's): their instances are runtime Readable / Writable
// streams (scr_fs_stream.c).
if (
(psym?.name === "ReadStream" || psym?.name === "WriteStream") &&
psym.declarations.some(
(d) =>
ts.isClassDeclaration(d) &&
ctx.isStdlibFile(d.getSourceFile()) &&
!isNodeTypesPath(d.getSourceFile().fileName) &&
isDeclaredInAmbientModule(d, "fs"),
)
) {
@@ -3288,6 +3358,9 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
isDeclaredInAmbientNamespace(d, "NodeJS"),
))
) {
// The general representation: the runtime Readable, which child
// stdio reads produce through the child stream bridge.
if (ctx.generalNodeStreams === true) return { kind: "object", className: "%Readable" };
return { kind: "childStream" };
}
// dgram.Socket: the same ambiguous "Socket" name disambiguated by its
@@ -3998,6 +4071,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",
@@ -4334,10 +4428,18 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
const mapped = mapType(checker.getTypeOfSymbol(p), ctx);
if (!mapped) return false;
const arms = mapped.kind === "union" ? ctx.unions.get(mapped.unionId)?.arms : [mapped];
// Under the general stream representation the stdio members are the
// runtime stream classes.
const isExpected = (arm: IrType): boolean =>
arm.kind === expected ||
(ctx.generalNodeStreams === true &&
arm.kind === "object" &&
((expected === "childStream" && arm.className === "%Readable") ||
(expected === "childWriter" && arm.className === "%Writable")));
return (
arms !== undefined &&
arms.some((arm) => arm.kind === expected) &&
arms.every((arm) => arm.kind === expected || arm.kind === unit)
arms.some(isExpected) &&
arms.every((arm) => isExpected(arm) || arm.kind === unit)
);
};
const childShaped =
@@ -4550,11 +4652,37 @@ function mapCallSignature(sig: ts.Signature, ctx: TypeMapperCtx): IrType | null
// declaration as a typed packed-array closure would bypass its boxed
// spread/error semantics and change coverage classification.
if (rest && decl !== undefined && isJsSourceFile(decl.getSourceFile())) return null;
// A fixed-tuple rest (`...args: [A, B]`, `...args: Parameters<F>` over
// a bound F) flattens to its positional parameters. Pattern rests keep
// their packed-record ABI and stay unmapped as values.
if (rest && decl !== undefined && ts.isParameter(decl) && ts.isIdentifier(decl.name)) {
const elems = restTupleParamTypes(checker.getTypeOfSymbol(p), ctx);
if (elems) {
params.push(...elems);
continue;
}
}
const optional =
decl !== undefined &&
ts.isParameter(decl) &&
(decl.questionToken !== undefined || decl.initializer !== undefined);
let pt = mapType(checker.getTypeOfSymbol(p), ctx);
// A parameter a TypeScript program types `any` takes whatever the
// caller passes: the checked-dynamic slot, as an `any`-typed binding
// already is (dynFallbackType). Declaration files keep their paths.
// Only function declarations and spelled function types: 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)) &&
!decl.getSourceFile().isDeclarationFile &&
(checker.getTypeOfSymbol(p).flags & ts.TypeFlags.Any) !== 0
)
pt = DYN;
if (
decl &&
ts.isParameter(decl) &&
@@ -4702,6 +4830,9 @@ function unionDiscriminant(
): IrUnionDiscriminant | undefined {
if (arms.filter((arm) => arm.kind === "record").length < 2 || parts.length < 2) return undefined;
const { checker } = ctx;
// Tuple shapes keep positions only: their checker `length` literal has
// no slot to compare (discriminantField refuses tuple shapes too).
if (parts.some((part) => ctx.shapes.get(part.mapped.shapeId)?.tuple)) return undefined;
for (const candidate of checker.getPropertiesOfType(parts[0]!.source)) {
if (candidate.name.startsWith("__@")) continue;
const byTag = new Map<number, (string | number | boolean)[]>();
@@ -5039,6 +5170,167 @@ function mapBoundIndexedAccess(type: ts.Type, ctx: TypeMapperCtx): IrType | null
return mapType(checker.getTypeOfSymbol(prop), ctx);
}
/** The standard library's `Parameters<F>` / `ReturnType<F>` over a type
* parameter the current instantiation binds to a callable checker type:
* that type's one call signature. Concrete arguments never reach here (the
* checker evaluates those conditionals itself); generic bodies keep them
* symbolic because their `infer` sits in a call signature. */
function boundSignatureAlias(
type: ts.Type,
ctx: TypeMapperCtx,
): { name: "Parameters" | "ReturnType"; sig: ts.Signature } | null {
const { checker, resolveTypeParamTs } = ctx;
if (!resolveTypeParamTs) return null;
const alias = type.getAliasSymbol();
if (!alias || (alias.name !== "Parameters" && alias.name !== "ReturnType")) return null;
const aliasArgs = type.getAliasTypeArguments();
const arg = aliasArgs[0];
if (aliasArgs.length !== 1 || !arg || !(arg.flags & ts.TypeFlags.TypeParameter)) return null;
const isLibAlias = alias.declarations.some(
(d) => ts.isTypeAliasDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()),
);
if (!isLibAlias) return null;
const bound = resolveTypeParamTs(arg);
if (!bound || bound.flags & ts.TypeFlags.TypeParameter) return null;
const sigs = checker.getCallSignatures(bound);
const sig = sigs[0];
if (sigs.length !== 1 || !sig || sig.getTypeParameters().length > 0) return null;
return { name: alias.name, sig };
}
/** One position of a fixed-arity parameter tuple. An optional position
* still has a native slot (the function type's `T | undefined` slot). */
interface TupleElement {
type: ts.Type;
optional: boolean;
}
/** The positional parameters of a signature as a fixed tuple: a trailing
* rest flattened when its own type is a fixed tuple. Null for a variadic
* position, which has no fixed native arity. */
function signatureParameters(sig: ts.Signature, ctx: TypeMapperCtx): TupleElement[] | null {
const { checker } = ctx;
const out: TupleElement[] = [];
for (const p of sig.getParameters()) {
const decl = p.valueDeclaration;
const type = checker.getTypeOfSymbol(p);
if (decl !== undefined && ts.isParameter(decl)) {
if (decl.dotDotDotToken !== undefined) {
const inner = restTupleElements(type, ctx);
if (!inner) return null;
out.push(...inner);
continue;
}
out.push({
type,
optional: decl.questionToken !== undefined || decl.initializer !== undefined,
});
continue;
}
out.push({ type, optional: (p.flags & ts.SymbolFlags.Optional) !== 0 });
}
return out;
}
/** The required-only positional parameter types of a signature (the
* value-level `Parameters<F>` tuple), null when a position is optional
* or variadic: such a tuple's length varies per call. */
function signatureParameterTypes(sig: ts.Signature, ctx: TypeMapperCtx): ts.Type[] | null {
const elems = signatureParameters(sig, ctx);
if (!elems || elems.some((e) => e.optional)) return null;
return elems.map((e) => e.type);
}
/** The element types of a REST parameter's type when it is a fixed tuple:
* a tuple of required elements (`[number, string]`, the empty `[]`), a type
* parameter bound to one, or `Parameters<F>` over a bound function type.
* Such a rest parameter has a compile-time arity, so its native ABI is the
* flattened positional list (exactly the equivalent fixed signature —
* TypeScript treats `(...a: [A, B]) => R` and `(a: A, b: B) => R` alike). */
export function restTupleElementTypes(type: ts.Type, ctx: TypeMapperCtx): ts.Type[] | null {
return restTupleElements(type, ctx)?.map((e) => e.type) ?? null;
}
/** restTupleElementTypes with each position's optionality: a rest tuple
* may end in optional positions (`[A, B?]`, `Parameters<(a: A, b?: B) =>
* R>`), which flatten to `T | undefined` slots exactly as the function
* type's optional parameters do. */
function restTupleElements(type: ts.Type, ctx: TypeMapperCtx): TupleElement[] | null {
const { checker } = ctx;
if (type.flags & ts.TypeFlags.TypeParameter) {
const bound = ctx.resolveTypeParamTs?.(type);
return bound && bound !== type ? restTupleElements(bound, ctx) : null;
}
const viaAlias = boundSignatureAlias(type, ctx);
if (viaAlias) {
return viaAlias.name === "Parameters" ? signatureParameters(viaAlias.sig, ctx) : null;
}
if (!checker.isTupleType(type)) return null;
const ref = type as ts.TupleTypeReference;
const tupleShape =
(ref.elementFlags as ts.ElementFlags[] | undefined) !== undefined
? ref
: (ref.getTarget() as ts.TupleType | undefined);
const args = checker.getTypeArguments(ref);
if (args.length === 0) return [];
const flags = tupleShape?.elementFlags;
if (flags === undefined) return null;
if (flags.some((f) => !(f & (ts.ElementFlags.Required | ts.ElementFlags.Optional)))) return null;
return args.map((t, i) => ({
type: t,
optional: (flags[i]! & ts.ElementFlags.Optional) !== 0,
}));
}
/** One tuple element's IR type under the tuple mapping's rules. */
function mapTupleElement(t: ts.Type, ctx: TypeMapperCtx): IrType | null {
let et = (t.flags & ts.TypeFlags.Any) !== 0 ? DYN : mapType(t, ctx);
if (et?.kind === "void" && isUnitOnlyTsType(t, ctx.resolveTypeParam))
et = unitOnlyUnion(ctx.unions);
return !et || et.kind === "void" ? null : et;
}
/** The mapped element types of a fixed-tuple rest parameter type
* (restTupleElementTypes), null when it is not one or an element does not
* map. */
export function restTupleParamTypes(type: ts.Type, ctx: TypeMapperCtx): IrType[] | null {
const elems = restTupleElements(type, ctx);
if (!elems) return null;
const out: IrType[] = [];
for (const e of elems) {
let et = mapTupleElement(e.type, ctx);
// An optional position's slot holds undefined when the caller omits
// it, as an optional parameter's does (mapCallSignature).
if (et && e.optional && et.kind !== "dyn") et = withUndefinedArm(et, ctx.unions);
if (!et) return null;
out.push(et);
}
return out;
}
/** `Parameters<F>` (the fixed tuple of F's parameters, interned exactly as
* the spelled tuple maps) and `ReturnType<F>` (F's mapped result) over a
* bound function-typed type parameter. */
function mapBoundSignatureAlias(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
const target = boundSignatureAlias(type, ctx);
if (!target) return null;
if (target.name === "ReturnType") {
const ret = ctx.checker.getReturnTypeOfSignature(target.sig);
return ret.flags & ts.TypeFlags.Any ? DYN : mapType(ret, ctx);
}
const elems = signatureParameterTypes(target.sig, ctx);
if (!elems) return null;
if (elems.length === 0) return arrayOf(unitOnlyUnion(ctx.unions));
const fields: { name: string; type: IrType }[] = [];
for (let i = 0; i < elems.length; i++) {
const et = mapTupleElement(elems[i]!, ctx);
if (!et) return null;
fields.push({ name: String(i), type: et });
}
fields.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
return { kind: "record", shapeId: ctx.shapes.intern(fields, true) };
}
/** `Partial<T>` / `Readonly<T>` where T is a STILL-GENERIC type parameter —
* the one mapped-type spelling a monomorphized body can contain that the
* checker cannot resolve for us (`keyof T` is unknown inside the body, so
@@ -5110,13 +5402,17 @@ function mapGenericAwaitedAlias(type: ts.Type, ctx: TypeMapperCtx): IrType | nul
const aliasArgs = type.getAliasTypeArguments();
const arg = aliasArgs[0];
if (aliasArgs.length !== 1 || !arg) return null;
if (!(arg.flags & ts.TypeFlags.TypeParameter)) return null;
// `Awaited<ReturnType<F>>` (an async wrapper's settled result) resolves
// through F's bound signature like a bare type parameter.
const viaSignature =
arg.flags & ts.TypeFlags.TypeParameter ? null : boundSignatureAlias(arg, ctx);
if (!(arg.flags & ts.TypeFlags.TypeParameter) && viaSignature?.name !== "ReturnType") return null;
// Provenance: the STANDARD LIBRARY's alias, not a user's shadowing one.
const isLibAlias = alias.declarations.some(
(d) => ts.isTypeAliasDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()),
);
if (!isLibAlias) return null;
const bound = resolveTypeParam(arg);
const bound = viaSignature ? mapBoundSignatureAlias(arg, ctx) : resolveTypeParam(arg);
if (!bound) return null;
let r = bound;
while (r.kind === "promise") r = r.inner;
@@ -5142,11 +5438,15 @@ function mapGenericIndexedAccess(type: ts.Type, ctx: TypeMapperCtx): IrType | nu
const obj = type.getObjectType();
if (obj === undefined || !(obj.flags & ts.TypeFlags.TypeParameter)) return null;
const bound = resolveTypeParam(obj);
const idx = type.getIndexType();
if (idx === undefined) return null;
// `T[keyof T]` over a T bound to an unknown-valued record view
// (`Record<string, unknown>` maps to the checked-dynamic value, see
// mapRecordType): every member reads as unknown.
if (bound?.kind === "dyn" && idx.isIndexType() && idx.getTarget() === obj) return DYN;
if (!bound || bound.kind !== "record") return null;
const shape = shapes.get(bound.shapeId);
if (!shape || shape.tuple) return null;
const idx = type.getIndexType();
if (idx === undefined) return null;
let covered: IrType[] | null = null;
if (idx.isStringLiteralType()) {
const f = shape.fields.find((x) => x.name === idx.value);
@@ -5163,7 +5463,62 @@ function mapGenericIndexedAccess(type: ts.Type, ctx: TypeMapperCtx): IrType | nu
}
if (!covered || covered.length === 0) return null;
const first = covered[0]!;
return covered.every((t) => typeEquals(t, first)) ? first : null;
if (covered.every((t) => typeEquals(t, first))) return first;
// Only the `keyof T` index itself covers every field at once; a key
// parameter bound to string keeps its literal-key resolution below.
if (!(idx.isIndexType() && idx.getTarget() === obj)) return null;
// Fields of different scalar types (`Object.entries` over a bound record
// reads `T[keyof T]`): any one of them, the union of their types.
const arms: IrType[] = [];
for (const t of covered) {
const parts = t.kind === "union" ? (ctx.unions.get(t.unionId)?.arms ?? null) : [t];
if (!parts) return null;
for (const part of parts) {
if (
part.kind !== "f64" &&
part.kind !== "string" &&
part.kind !== "bool" &&
part.kind !== "undefinedT" &&
part.kind !== "nullT"
)
return null;
if (!arms.some((a) => typeEquals(a, part))) arms.push(part);
}
}
if (arms.length === 1) return arms[0]!;
arms.sort((a, b) => (typeKey(a) < typeKey(b) ? -1 : 1));
return { kind: "union", unionId: ctx.unions.intern(arms) };
}
/** `keyof T extends never ? A : B` over a bound record T (the lib's
* Object.entries element spelling): decided by whether the binding has any
* key. */
function mapBoundKeyofNeverConditional(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
const { resolveTypeParam, shapes } = ctx;
if (!resolveTypeParam || !type.isConditionalType()) return null;
const check = type.getCheckType();
const ext = type.getExtendsType();
if (!check || !ext || !(ext.flags & ts.TypeFlags.Never) || !check.isIndexType()) return null;
const target = check.getTarget();
if (!target || !(target.flags & ts.TypeFlags.TypeParameter)) return null;
const bound = resolveTypeParam(target);
// A T bound to the checked-dynamic value (`Record<string, unknown>`, an
// unknown-valued record view) has no static key set to decide the
// branch, but both branches of the lib's `keyof T extends never ?
// unknown : T[keyof T]` read as unknown there: one answer either way.
if (bound?.kind === "dyn") {
const whenEmpty = type.getTrueType();
const otherwise = type.getFalseType();
const a = whenEmpty ? mapType(whenEmpty, ctx) : null;
const b = otherwise ? mapType(otherwise, ctx) : null;
return a && b && typeEquals(a, b) ? a : null;
}
if (bound?.kind !== "record") return null;
const shape = shapes.get(bound.shapeId);
if (!shape || shape.tuple) return null;
const hasKeys = shape.fields.length > 0 || shape.indexValue !== undefined;
const branch = hasKeys ? type.getFalseType() : type.getTrueType();
return branch ? mapType(branch, ctx) : null;
}
/** A mapped type over a type parameter of the instance being lowered (`{
@@ -5306,10 +5661,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();
@@ -5330,6 +5688,154 @@ function matchInferReference(
return inferred;
}
/** The type parameter an `infer X` declares, when `t` is one. */
function inferSymbolOf(t: ts.Type, _ctx: TypeMapperCtx): ts.Symbol | undefined {
if (!(t.flags & ts.TypeFlags.TypeParameter)) return undefined;
const symbol = t.getSymbol();
return symbol &&
symbol.declarations.some((d) => d.parent !== undefined && ts.isInferTypeNode(d.parent))
? symbol
: undefined;
}
/** `checked extends (...args: any) => infer R` — the shape of the standard
* library's ReturnType, over a concrete function type: R binds to its
* result. Pattern parameters must be `any` or `any[]`, and a pattern rest
* `any` or `any[]`; anything else stays undecided. The match follows
* TypeScript's assignability: a primitive never matches a function type,
* a function that needs more arguments than a pattern without a rest
* passes never matches, and a positional `any[]` parameter needs the
* function's parameter there to accept an array. */
function matchInferSignature(
checked: ts.Type,
extendsType: ts.Type,
ctx: TypeMapperCtx,
): Map<ts.Symbol, ts.Type> | false | undefined {
const { checker } = ctx;
const wantSigs = checker.getCallSignatures(extendsType);
const want = wantSigs[0];
if (wantSigs.length !== 1 || !want || want.getTypeParameters().length > 0) return undefined;
const wantShape = signatureShape(want, ctx);
if (wantShape === undefined) return undefined;
const isAny = (t: ts.Type): boolean => (t.flags & ts.TypeFlags.Any) !== 0;
const isAnyArray = (t: ts.Type): boolean =>
checker.isArrayType(t) && isAny(checker.getTypeArguments(t as ts.TypeReference)[0]!);
if (!wantShape.params.every((p) => isAny(p.type) || isAnyArray(p.type))) return undefined;
if (wantShape.rest !== null && !isAny(wantShape.rest)) return undefined;
if (!(checked.flags & ts.TypeFlags.Object)) return false;
const gotSigs = checker.getCallSignatures(checked);
const got = gotSigs[0];
if (gotSigs.length !== 1 || !got || got.getTypeParameters().length > 0) return undefined;
const gotShape = signatureShape(got, ctx);
if (gotShape !== undefined) {
// `(a: any) => infer R` rejects `(a: string, b: string) => R` and
// `(...args: [string, number]) => R`: they need more arguments.
if (wantShape.rest === null && requiredArguments(gotShape) > wantShape.params.length)
return false;
for (let i = 0; i < wantShape.params.length; i++) {
if (!isAnyArray(wantShape.params[i]!.type)) continue;
const param = gotShape.params[i]?.type ?? gotShape.rest;
if (param === undefined || param === null) continue;
const accepts = acceptsArray(param, ctx);
if (accepts !== true) return accepts;
}
} else if (wantShape.params.some((p) => isAnyArray(p.type))) {
return undefined;
}
const wantRet = checker.getReturnTypeOfSignature(want);
const infer = inferSymbolOf(wantRet, ctx);
if (infer) return new Map([[infer, checker.getReturnTypeOfSignature(got)]]);
return wantRet.flags & ts.TypeFlags.Any ? new Map() : undefined;
}
/** A signature's parameters as a call sees them: the positional ones, with
* a fixed-tuple rest (`...args: [A, B?]`) flattened into positions, and the
* element type of an array rest (`any` for `...args: any`). Undefined when
* a parameter has no declaration, or the rest is a variadic tuple or
* another type. */
function signatureShape(
sig: ts.Signature,
ctx: TypeMapperCtx,
): { params: { type: ts.Type; optional: boolean }[]; rest: ts.Type | null } | undefined {
const { checker } = ctx;
const params: { type: ts.Type; optional: boolean }[] = [];
for (const p of sig.getParameters()) {
const decl = p.valueDeclaration;
if (decl === undefined || !ts.isParameter(decl)) return undefined;
const type = checker.getTypeOfSymbol(p);
if (decl.dotDotDotToken === undefined) {
params.push({
type,
optional: decl.questionToken !== undefined || decl.initializer !== undefined,
});
continue;
}
if (type.flags & ts.TypeFlags.Any) return { params, rest: type };
if (checker.isArrayType(type))
return { params, rest: checker.getTypeArguments(type as ts.TypeReference)[0]! };
const elems = restTupleElements(type, ctx);
if (!elems) return undefined;
for (const e of elems) params.push({ type: e.type, optional: e.optional });
return { params, rest: null };
}
return { params, rest: null };
}
/** The fewest arguments a call must pass: up to the last required position. */
function requiredArguments(shape: { params: { optional: boolean }[] }): number {
let required = 0;
shape.params.forEach((p, i) => {
if (!p.optional) required = i + 1;
});
return required;
}
/** Whether a parameter of this type accepts an `any[]` argument: true for
* `any`, `unknown` and array types, false for primitives, undefined
* otherwise. */
function acceptsArray(t: ts.Type, ctx: TypeMapperCtx): boolean | undefined {
if (t.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) return true;
if (ctx.checker.isArrayType(t)) return true;
const primitive =
ts.TypeFlags.StringLike |
ts.TypeFlags.NumberLike |
ts.TypeFlags.BooleanLike |
ts.TypeFlags.BigIntLike |
ts.TypeFlags.ESSymbolLike |
ts.TypeFlags.Undefined |
ts.TypeFlags.Null |
ts.TypeFlags.Void;
return t.flags & primitive ? false : undefined;
}
/** `checked extends PromiseLike<infer U>` (or `Promise<infer U>`) over the
* standard library's Promise: U binds to the settled type. An object with
* no `then` member is no thenable. Everything else stays undecided. */
function matchInferThenable(
checked: ts.Type,
extendsType: ts.Type,
ctx: TypeMapperCtx,
): Map<ts.Symbol, ts.Type> | false | undefined {
const { checker } = ctx;
const stdlibNamed = (t: ts.Type, names: readonly string[]): boolean => {
const symbol = t.getSymbol();
return (
symbol !== undefined &&
names.includes(symbol.name) &&
symbol.declarations.some((d) => ctx.isStdlibFile(d.getSourceFile()))
);
};
if (!stdlibNamed(extendsType, ["PromiseLike", "Promise"])) return undefined;
const wantArgs = checker.getTypeArguments(extendsType as ts.TypeReference);
const infer = wantArgs.length === 1 ? inferSymbolOf(wantArgs[0]!, ctx) : undefined;
if (!infer) return undefined;
if (stdlibNamed(checked, ["Promise", "PromiseLike"])) {
const gotArgs = checker.getTypeArguments(checked as ts.TypeReference);
return gotArgs.length === 1 ? new Map([[infer, gotArgs[0]!]]) : undefined;
}
return checker.getPropertyOfType(checked, "then") === undefined ? false : undefined;
}
/** The type mentions a mapped key or bound alias parameter (a template
* shape evalMappedTemplate does not reduce). */
function mentionsMappedParams(
@@ -5493,10 +5999,22 @@ export function isUnitOnlyTsType(t: ts.Type, resolveTypeParam?: TypeParamResolve
return parts.every(
(p) =>
(p.flags & UNIT) !== 0 ||
((p.flags & ts.TypeFlags.TypeParameter) !== 0 && resolveTypeParam?.(p)?.kind === "void"),
((p.flags & ts.TypeFlags.TypeParameter) !== 0 && resolveTypeParam?.(p)?.kind === "void") ||
boundVoidReturnType(p, resolveTypeParam),
);
}
/** `ReturnType<F>` over a type parameter bound to a void-returning
* function (a variadic wrapper's result slot): a unit-only value. */
function boundVoidReturnType(p: ts.Type, resolveTypeParam?: TypeParamResolver): boolean {
if (!resolveTypeParam || p.getAliasSymbol()?.name !== "ReturnType") return false;
const args = p.getAliasTypeArguments();
const arg = args[0];
if (args.length !== 1 || !arg || !(arg.flags & ts.TypeFlags.TypeParameter)) return false;
const bound = resolveTypeParam(arg);
return bound?.kind === "func" && bound.ret.kind === "void";
}
/** IR-level `t | undefined`, canonicalized and fenced exactly like the
* ts-union branch of mapType (typeKey-sorted arms, deduplicated;
* Date/dyn/void arms unrepresentable; a func arm IS representable — the
@@ -6164,6 +6682,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 };
@@ -6177,6 +6697,51 @@ function mapRecordType(widened: ts.Type, ctx: TypeMapperCtx): IrType | null {
}
}
/** An index signature already describes every OPTIONAL declared member
* whose type is its slot type: @types/node's `ProcessEnv extends
* Dict<string> { TZ?: string }` and every spread of it, or a declared
* environment `{ CI?: string } & Dictionary<string>`. At runtime such a
* member is just another key, so the shape interns as the pure
* index-signature record. Its slot admits undefined, as the optional
* members do (`env.CI = undefined` keeps the key). A hybrid layout would
* give each member a struct slot, which enumerates before the overflow
* keys (Node keeps one insertion order) and makes `delete` and spread
* answer differently for declared and undeclared keys. Required members,
* and members typed otherwise than the slot (whose reads the slot type
* could not give back), keep the hybrid shape. */
function coveredOptionalFieldsShape(
widened: ts.Type,
parts: RecordShapeParts,
ctx: TypeMapperCtx,
): string | null {
const slot = parts.indexValue;
if (!slot || parts.fields.length === 0) return null;
const { checker, shapes, unions } = ctx;
// The members' common type must be the slot plus undefined. Read it off
// the members (already interned) rather than building it: a hybrid that
// does not qualify must not intern anything new.
const optionalSlot = parts.fields[0]!.type;
if (!parts.fields.every((field) => typeEquals(field.type, optionalSlot))) return null;
if (optionalSlot.kind !== "union" || unions.armTag(optionalSlot.unionId, UNDEFINED_T) < 0)
return null;
const memberArms = unions.get(optionalSlot.unionId)?.arms ?? [];
const slotArms =
slot.kind === "union" ? (unions.get(slot.unionId)?.arms ?? []) : isUnitType(slot) ? [] : [slot];
const definedArms = memberArms.filter((arm): boolean => arm.kind !== "undefinedT");
const slotDefined = slotArms.filter((arm): boolean => arm.kind !== "undefinedT");
if (
definedArms.length === 0 ||
definedArms.length !== slotDefined.length ||
!slotDefined.every((arm) => definedArms.some((other) => typeEquals(arm, other)))
)
return null;
if (!isSupportedIndexValue(optionalSlot)) return null;
const props = checker.getPropertiesOfType(widened);
if (props.length !== parts.fields.length) return null;
if (!props.every((p) => (p.flags & ts.SymbolFlags.Optional) !== 0)) return null;
return shapes.intern([], false, optionalSlot, []);
}
/** The type of an object literal that spreads an index-signature record
* (`{ ...tags, bucket: b, status }`, the metric-tags idiom). The checker
* drops the signature and types the literal by its explicit members only,
+2
View File
@@ -551,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,
@@ -567,6 +568,7 @@ async function compileExecutableNative(
tlsCa: features.tlsCa,
cipher: features.cipher,
webcrypto: features.webcrypto,
perfLoop: features.perfLoop,
...(onArtifactReady === undefined ? {} : { onArtifactReady }),
...(ffi === null
? {}
@@ -75,6 +75,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"process.stdoutWriteBytes",
"process.stderrWriteBytes",
"procStream.write",
"child.stdioStream",
"stream.unboxDyn",
"weakMap.new",
"weakSet.new",
"dyn.fromEntries",
@@ -346,6 +348,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"stream.onDyn",
"readable.resume",
"readable.pipe",
"readable.pipeGeneral",
"readable.unpipe",
"writable.write",
"writable.writeStr",
@@ -389,6 +392,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"perf.entries",
"perf.clearMarks",
"perf.clearMeasures",
// monitorEventLoopDelay's resolution: Node's ERR_OUT_OF_RANGE.
"perf.eldNew",
// Non-TTY stdin: Node's stdin has no setRawMode member, so the call is
// the exact catchable TypeError.
"process.stdinSetRawMode",
@@ -581,6 +586,9 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"dyn.objCreateWithProperties",
"dyn.getPrototype",
"dyn.setPrototype",
// a proxy in the prototype chain can throw
"dyn.builtinInstanceOf",
"dyn.functionInstanceOf",
"dyn.getOwnPropertyNames",
"dyn.ownKeys",
"dyn.getOwnPropertySymbols",
+49 -2
View File
@@ -440,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
@@ -2633,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 },
@@ -2654,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 },
@@ -3534,6 +3568,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 },
@@ -3691,7 +3735,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
@@ -3889,6 +3935,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 },
+39 -5
View File
@@ -149,6 +149,7 @@ const ABORT_SIGNAL_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
/** Entry points that link the stream unit (with STREAM_LIB_PREFIXES). */
const STREAM_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"child.stdioStream",
"console.bind",
"crypto.hashStream",
"crypto.hashStreamPiped",
@@ -167,6 +168,8 @@ const STREAM_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"stream.destroyErr",
"stream.errored",
"stream.prop",
"stream.stdioWritable",
"stream.unboxDyn",
"stream.workerStdio",
]);
@@ -238,8 +241,21 @@ const CHILD_PROCESS_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
/** libCall name prefixes that link the child-process unit. */
const CHILD_PROCESS_LIB_PREFIXES: readonly string[] = ["child.", "cp.", "spawnRes.", "writer."];
/** The entry points scr_dyn_builtin_class.c defines. The unit calls
* listeners through the dyn-invoke unit, renders unhandled 'error'
* payloads through the inspect unit, creates EventEmitter's private
* symbols through the symbol unit and shares defaultMaxListeners with the
* emitter unit, so each of them links all five. */
const BUILTIN_CLASS_LIB_FNS: readonly IrLibFn[] = [
"dyn.builtinClass",
"dyn.builtinInstanceOf",
"dyn.functionInstanceOf",
];
/** Entry points that link the symbol unit, besides the sym. and disposable. families. */
const SYMBOL_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
// EventEmitter's kShapeMode and kCapture properties
...BUILTIN_CLASS_LIB_FNS,
"dyn.usingDispose",
"dyn.usingMethod",
"util.isDeepStrictEqual",
@@ -499,14 +515,14 @@ 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",
],
// scr_wasm.c boxes its handles and promises through the dyn-async unit.
prefixes: [...READABLE_DYN_ASYNC_PREFIXES, "wasm."],
expr: (node, tables) =>
(node.kind === "awaitExpr" && node.type.kind === "dyn") ||
@@ -516,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) =>
@@ -535,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
@@ -569,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]!),
}),
@@ -594,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] }),
@@ -675,6 +707,7 @@ function noRuntimeFeatures(): RuntimeFeatures {
dgram: false,
dnsPromises: false,
dynAsync: false,
dynBuiltinClass: false,
dynInvoke: false,
dynTier: false,
emitter: false,
@@ -691,6 +724,7 @@ function noRuntimeFeatures(): RuntimeFeatures {
net: false,
nodeTest: false,
parseArgs: false,
perfLoop: false,
processEvents: false,
qs: false,
regex: false,
+16 -2
View File
@@ -5228,6 +5228,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 +5733,9 @@ function validateFunction(
}
return true;
}
if (e.fn === "readable.pipe") {
if (e.fn === "readable.pipe" || e.fn === "readable.pipeGeneral") {
if (!isStreamObject(e.args[1]?.type) || !typeEquals(e.type, e.args[1]!.type)) {
err(`libCall readable.pipe must return its destination's type`, e.loc);
err(`libCall ${e.fn} must return its destination's type`, e.loc);
}
return true;
}
@@ -54,6 +54,11 @@ const LIB_MODE_REFUSED_PREFIXES: readonly [string, string][] = [
["webcrypto.", "WebCrypto (crypto.subtle)"],
["fs.watch", "fs.watch"],
["watcher.", "fs.watch"],
// The event-loop metrics sample the loop and its timers (scr_perf_loop.c),
// which library links exclude.
["perf.loopStart", "performance.eventLoopUtilization"],
["perf.idleTime", "performance.eventLoopUtilization"],
["perf.eld", "perf_hooks.monitorEventLoopDelay"],
["test.", "the node:test surface"],
["readable.", "the node:stream surface"],
["writable.", "the node:stream surface"],
@@ -158,6 +163,7 @@ export function moduleLibAsyncSurface(mod: IrModule): { surface: string; loc: Sr
[features.nodeTest, "the node:test surface"],
[features.dynAsync, "the checked-dynamic async surface"],
[features.dc, "the diagnostics_channel surface"],
[features.perfLoop, "the perf_hooks event-loop metrics"],
];
for (const [on, surface] of coarse) {
if (on) return { surface, loc: entryLoc };
+2
View File
@@ -89,6 +89,7 @@ async function main(): Promise<number> {
bigint: false,
searchParams: false,
qs: false,
dynBuiltinClass: false,
parseArgs: false,
stream: false,
net: false,
@@ -102,6 +103,7 @@ async function main(): Promise<number> {
tls: false,
cipher: false,
webcrypto: false,
perfLoop: false,
tlsCa: false,
},
options.optimization ?? "release",
+2
View File
@@ -143,6 +143,7 @@ function validFeatures(value: unknown): value is NativeLinkFeatures {
"bigint",
"searchParams",
"qs",
"dynBuiltinClass",
"parseArgs",
"stream",
"net",
@@ -157,6 +158,7 @@ function validFeatures(value: unknown): value is NativeLinkFeatures {
"tlsCa",
"cipher",
"webcrypto",
"perfLoop",
].every((name) => typeof features[name] === "boolean");
}
@@ -96,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,
@@ -110,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,
+7 -16
View File
@@ -20,6 +20,7 @@
* The decoder never evaluates anything and keeps no module contents beyond
* the counts and the feature set it reports. It is plain TypeScript that
* the native compiler also compiles. */
import { readMemargFlags } from "./module.js";
/** Proposals the summary can name, in report order. The first group is the
* translatable set; the rest are reported as unsupported. */
@@ -1244,19 +1245,9 @@ class FunctionValidator {
/** A memarg. A memory other than 0 stops typing (multi-memory). */
memarg(naturalAlign: number): void {
const r = this.r;
let align = r.u32();
let multi = false;
if (align >= 64 && align < 128) {
align -= 64;
multi = true;
} else if (align >= 64) {
r.malformed("malformed memop flags");
}
let memory = 0;
if (multi) {
memory = r.u32();
if (memory !== 0) this.otherMemory(memory);
}
const { align, memory } = readMemargFlags(() => r.u32());
if (align < 0) r.malformed("malformed memop flags");
if (memory !== 0) this.otherMemory(memory);
if (this.m.memory64) r.u64();
else r.u32();
if (!this.typed) return;
@@ -1840,9 +1831,9 @@ class FunctionValidator {
scanMemarg(): void {
const r = this.r;
const align = r.u32();
if (align >= 128) r.malformed("malformed memop flags");
if (align >= 64 && r.u32() !== 0) this.m.feature("multi-memory");
const { align, memory } = readMemargFlags(() => r.u32());
if (align < 0) r.malformed("malformed memop flags");
if (memory !== 0) this.m.feature("multi-memory");
if (this.m.memory64) r.u64();
else r.u32();
}
+21 -10
View File
@@ -155,15 +155,6 @@ export class WasmCursor {
}
}
/** A memarg: the alignment flags, the memory index when flag bit 6 is
* set (multi-memory), then the offset. binary.ts validates it the same
* way. */
memarg(): { memory: number; offset: number } {
const flags = this.u32();
const memory = flags & 0x40 ? this.u32() : 0;
return { memory, offset: this.u32() };
}
/** A signed LEB128 of at most 33 bits. */
s33(): number {
let result = 0;
@@ -292,6 +283,21 @@ function decodeUtf8(bytes: Uint8Array, start: number, end: number): string {
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. */
@@ -380,7 +386,12 @@ export function skipImmediates(c: WasmCursor, op: number): void {
}
}
default:
if (op >= 0x28 && op <= 0x3e) c.memarg();
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;
}
}
@@ -138,6 +138,57 @@ describe("translateWasmModule", () => {
});
});
/** A module from its sections: `[id, ...payload]`, each payload under 128
* bytes. */
function wasmModule(...sections: number[][]): Uint8Array {
const out = [0, 97, 115, 109, 1, 0, 0, 0];
for (const [id, ...payload] of sections) out.push(id!, payload.length, ...payload);
return new Uint8Array(out);
}
/** (module (memory 1) (func (export "f") (param i32) (result i32)
* local.get 0 i32.load <memarg>)) with the memarg's bytes as given. */
function loadModule(memarg: number[]): Uint8Array {
const body = [0, 0x20, 0, 0x28, ...memarg, 0x0b];
return wasmModule(
[1, 1, 0x60, 1, 0x7f, 1, 0x7f],
[3, 1, 0],
[5, 1, 0, 1],
[7, 1, 1, 0x66, 0, 0],
[10, 1, body.length, ...body],
);
}
describe("memargs and operand types", () => {
test("a memarg naming memory 0 explicitly translates as the plain form", () => {
// Flags 0x42: alignment 2 with bit 6 set, then memory index 0 and
// offset 4. Validation and translation read the same three immediates.
// Only the descriptor (which records the module's byte length) differs.
const code = (memarg: number[]): string[] =>
translateWasmModule(loadModule(memarg), SHA, { boundsChecks: true })
.text.join("\n")
.split("\n")
.filter((line) => !line.startsWith(`@${wasmSymbolPrefix(SHA)}_desc =`));
expect(code([0x42, 0, 4])).toEqual(code([0x02, 4]));
});
test("a memarg naming another memory is refused", () => {
expect(() =>
translateWasmModule(loadModule([0x42, 1, 4]), SHA, { boundsChecks: true }),
).toThrow(/memory 1/);
});
test("local.tee of an i32 into an externref local is refused", () => {
// (func (local externref) i32.const 1 local.tee 0 drop): the operand
// never reaches a reference slot.
const body = [1, 1, 0x6f, 0x41, 1, 0x22, 0, 0x1a, 0x0b];
const bytes = wasmModule([1, 1, 0x60, 0, 0], [3, 1, 0], [10, 1, body.length, ...body]);
expect(() => translateWasmModule(bytes, SHA, { boundsChecks: false })).toThrow(
/local\.tee operand type does not match/,
);
});
});
describe("module parsing", () => {
test("reads the parts translation needs", () => {
const m = parseWasmModule(ADD);
+60 -46
View File
@@ -48,6 +48,7 @@ import {
VAL_I64,
WasmCursor,
parseWasmModule,
readMemargFlags,
skipImmediates,
type WasmFuncType,
type WasmModule,
@@ -119,7 +120,6 @@ const RUNTIME_DECLS = [
"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)",
"declare ptr @scr_wasm_global_get_ref(ptr)",
];
function llType(t: number): string {
@@ -915,6 +915,39 @@ class FunctionTranslator {
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");
@@ -1625,39 +1658,37 @@ class FunctionTranslator {
}
case 0x20: {
const i = c.u32();
const t = this.localTypes[i]!;
const t = this.localType(i);
this.push(this.emit(`load ${llType(t)}, ptr %l${i}`), t);
return;
}
case 0x21: {
const i = c.u32();
const v = this.pop();
this.expectType(v.t, this.localTypes[i]!);
this.add(` store ${llType(v.t)} ${v.v}, ptr %l${i}`);
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 v = this.stack[this.stack.length - 1]!;
this.expectType(v.t, this.localTypes[i]!);
this.add(` store ${llType(v.t)} ${v.v}, ptr %l${i}`);
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));
// An externref read stays alive until the outermost export call
// returns, whatever later replaces the global's value.
if (t === VAL_EXTERNREF)
this.push(this.emit(`call ptr @scr_wasm_global_get_ref(ptr ${cell})`), t);
else this.push(this.emit(`load ${llType(t)}, ptr ${cell}, align 8`), t);
this.push(this.emit(`load ${llType(t)}, ptr ${cell}, align 8`), t);
return;
}
case 0x24: {
const g = c.u32();
const v = this.pop();
this.expectType(v.t, this.m.globals[g]!.type);
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})`);
@@ -1675,14 +1706,15 @@ class FunctionTranslator {
}
case 0x26: {
const t = c.u32();
const v = this.pop();
const i = this.pop();
this.expectType(v.t, this.m.tables[t]!.elemType);
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: {
c.u32();
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`);
@@ -1691,7 +1723,7 @@ class FunctionTranslator {
return;
}
case 0x40: {
c.u32();
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();
@@ -1736,7 +1768,7 @@ class FunctionTranslator {
if (op >= 0x28 && op <= 0x35) {
const offset = this.memargOffset(c);
const [vt, mt, bytes, ext] = LOAD_INFO[op]!;
const addr = this.pop();
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`);
@@ -1749,9 +1781,9 @@ class FunctionTranslator {
}
if (op >= 0x36 && op <= 0x3e) {
const offset = this.memargOffset(c);
const [, mt, bytes] = STORE_INFO[op]!;
const v = this.pop();
const addr = this.pop();
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)) {
@@ -1763,24 +1795,6 @@ class FunctionTranslator {
this.numeric(op);
}
/** The offset of a load or store's memarg; a memory other than 0 is
* refused (only one memory translates). */
memargOffset(c: WasmCursor): number {
const { memory, offset } = c.memarg();
if (memory !== 0) throw new WasmTranslateError("multiple memories are not supported");
return offset;
}
/** A value written to a local, global or table has the destination's
* type; validation guarantees it, so a mismatch means the translator
* lost track of the operand stack. */
expectType(actual: number, expected: number): void {
if (actual !== expected)
throw new Error(
`wasm translator: value of type 0x${actual.toString(16)} where 0x${expected.toString(16)} is expected`,
);
}
numeric(op: number): void {
const I32 = VAL_I32;
const I64 = VAL_I64;
@@ -2069,7 +2083,7 @@ class FunctionTranslator {
case 8: {
// memory.init
const seg = c.u32();
c.u32();
this.memoryIndex(c);
const n = this.pop();
const s = this.pop();
const d = this.pop();
@@ -2082,8 +2096,8 @@ class FunctionTranslator {
this.add(` call void @scr_wasm_data_drop(ptr %inst, i32 ${c.u32()})`);
return;
case 10: {
c.u32();
c.u32();
this.memoryIndex(c);
this.memoryIndex(c);
const n = this.pop();
const s = this.pop();
const d = this.pop();
@@ -2093,7 +2107,7 @@ class FunctionTranslator {
return;
}
case 11: {
c.u32();
this.memoryIndex(c);
const n = this.pop();
const v = this.pop();
const d = this.pop();
+14
View File
@@ -1867,6 +1867,13 @@
"status": "static",
"note": "recognized module (bare and node:-prefixed specifiers)"
},
{
"id": "node-builtin.perf_hooks.monitorEventLoopDelay",
"kind": "node-builtin",
"name": "perf_hooks.monitorEventLoopDelay",
"status": "static",
"note": "an unref'd sampling interval records the delay histogram; enable, disable, reset, percentile and the min, max, mean, stddev, count and exceeds readings are supported"
},
{
"id": "node-builtin.perf_hooks.performance.clearMarks",
"kind": "node-builtin",
@@ -1879,6 +1886,13 @@
"name": "perf_hooks.performance.clearMeasures",
"status": "static"
},
{
"id": "node-builtin.perf_hooks.performance.eventLoopUtilization",
"kind": "node-builtin",
"name": "perf_hooks.performance.eventLoopUtilization",
"status": "static",
"note": "active time is the main thread's CPU time since the loop started; the one- and two-argument difference forms are supported"
},
{
"id": "node-builtin.perf_hooks.performance.getEntries",
"kind": "node-builtin",
@@ -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")); }
}
`,
);
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5817-constructor-function-instanceof.cjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5818-constructor-function-emitter-defaults.cjs"
],
"diags": []
}
@@ -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"
}
]
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6380-variadic-wrapper-arguments-apply.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6381-variadic-wrapper-async.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6382-variadic-wrapper-methods-this.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6383-variadic-wrapper-rest-tuple.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6384-variadic-wrapper-nested.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6385-variadic-wrapper-shared-local-names.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6390-catch-binding-predicate.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6391-nullish-function-default.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6392-optional-chain-protocol-calls.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6393-function-types-any-params.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6394-in-key-error-subclasses.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6395-generic-object-entries.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6396-generic-any-instantiation.ts"
],
"diags": []
}

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