diff --git a/docs/content/limitations/builtin-apis/performance-entries.mdx b/docs/content/limitations/builtin-apis/performance-entries.mdx index 92af98cb7..a42940b68 100644 --- a/docs/content/limitations/builtin-apis/performance-entries.mdx +++ b/docs/content/limitations/builtin-apis/performance-entries.mdx @@ -1,2 +1,4 @@ - **Performance entries:** `performance.mark()`, `measure()`, and the `getEntries` methods return entries with Node's fields, `toJSON()`, inspection, and identity. `Object.keys`, `for...in`, object spread, and `hasOwnProperty` on an entry are unsupported, and an entry stored in an `unknown` slot prints as a plain object. +- **Event-loop metrics:** `performance.eventLoopUtilization()` counts the main thread's CPU time since the event loop started as active time, so blocking system calls count as idle where Node counts them as active. + `monitorEventLoopDelay()` supports `enable()`, `disable()`, `reset()`, `percentile()`, and the `min`, `max`, `mean`, `stddev`, `count`, and `exceeds` readings; the BigInt readings, `percentiles`, and non-literal option objects are unsupported. diff --git a/docs/content/limitations/builtin-apis/streams-and-readline.mdx b/docs/content/limitations/builtin-apis/streams-and-readline.mdx index beda29c7d..73b48c88d 100644 --- a/docs/content/limitations/builtin-apis/streams-and-readline.mdx +++ b/docs/content/limitations/builtin-apis/streams-and-readline.mdx @@ -1,4 +1,7 @@ - **Streams and readline:** Stream chunks are Buffers or strings. + `NodeJS.ReadableStream` and `NodeJS.WritableStream`, and `stream.Readable` and `stream.Writable` under `@types/node`, accept every stream: when a program passes file, zlib, or `node:stream` streams through them, child-process pipes and `process.stdout` and `process.stderr` join those streams as `Readable` and `Writable` instances. + `readable.pipe(process.stdout)` and `readable.pipe(process.stderr)` compile and, as in Node, do not end the process stream. + A `pipeline` stage that arrives as an untyped value must be a native stream at run time; other values throw a not-supported error. `Readable.from` accepts arrays, Sets, Map and Set iterators, generators, async generators, and class iterables and async iterables whose values are strings or Buffers, and in JavaScript any iterable or async iterable of them; a stream that mixes strings and Buffers is unsupported. In JavaScript, a sync iterator value with a `then` method that is not a promise, and an object whose `[Symbol.asyncIterator]` or `[Symbol.iterator]` method is a generator method, are unsupported. Object-mode streams of other values (including `Readable.from` over a `Map`, whose values are entry arrays) are unsupported. @@ -11,6 +14,7 @@ `stream.compose` accepts two or more streams, or one `Duplex`; functions, iterables, and web streams as stages are unsupported, and the `ERR_INVALID_ARG_VALUE` message for a stream that is no longer readable or writable names the stream's class instead of printing the stream. `pipeline` from `node:stream/promises` accepts a trailing `{ signal, end }` object, and `finished` accepts `readable`, `writable`, `error`, `cleanup`, and `signal` options, with boolean literals for the flags. Stream constructors accept a `signal` option in an options object literal, and `stream.addAbortSignal` accepts any stream. + A `transform` or `flush` option callback whose completion callback types `data` as `any`, as @types/node's `TransformCallback` does, may pass a Buffer, a string, or nothing; any other value throws a TypeError where the callback is called, instead of Node's `ERR_INVALID_ARG_TYPE` stream error. `node:readline` and `node:readline/promises` (also reached as `readline.promises`) interfaces, from `createInterface(options)` or `createInterface(input, output, completer, terminal)`, read `process.stdin` or a readable stream: `question`, `prompt`, `write` (with a key object literal of `name`, `ctrl`, `meta`, and `shift`), `pause`, `resume`, `'line'`/`'close'`/`'pause'`/`'resume'`/`'error'`/`'history'`/`'SIGINT'`/`'SIGTSTP'`/`'SIGCONT'` listeners, and `for await` compile. The `history`, `historySize`, `removeHistoryDuplicates`, `completer`, `tabSize`, and `escapeCodeTimeout` options are validated as in Node. With `terminal: true`, or with no `terminal` option and `process.stdout` as the output when it is a TTY, the interface runs Node's terminal mode: the editing keys, the kill ring, undo and redo, history recording and recall, Tab completion through the completer, Ctrl+C, Ctrl+D, and Ctrl+Z, `TERM=dumb`, and the same escape sequences written to the output. diff --git a/docs/content/limitations/builtin-function-values.mdx b/docs/content/limitations/builtin-function-values.mdx index 80d0814b1..541c9fb2b 100644 --- a/docs/content/limitations/builtin-function-values.mdx +++ b/docs/content/limitations/builtin-function-values.mdx @@ -16,7 +16,11 @@ Other immutable aliases of table-backed Node builtins support direct calls, but Native calls accept standard process streams; custom native streams and mutations to `util.inspect.colors` remain unsupported. `util.inherits` links the prototypes of constructor functions, and `util.deprecate`, `util.callbackify`, and `util.debuglog` return native function values that forward receivers and arguments. -Class constructors passed to `util.inherits` or `util.deprecate` are refused. +In JavaScript, a constructor function can inherit from `EventEmitter`, the legacy `Stream` constructor, or a stream class through `util.inherits` or `Object.setPrototypeOf`. +`EventEmitter.call(this)` and `Stream.call(this)` initialize the object as Node does, and its listener methods keep their state in the object's own `_events`, `_eventsCount`, and `_maxListeners` properties. +The `captureRejections` option and `events.errorMonitor` listeners are not supported on such objects. +Calling `Readable`, `Writable`, `Duplex`, `Transform`, or `PassThrough` with a constructor function's receiver throws `SC2020` where the call runs. +Other class constructors passed to `util.inherits` or `util.deprecate` are refused. Deprecation warnings are emitted on the next tick and follow assignments to `process.noDeprecation` and `process.throwDeprecation`; compiled binaries take no Node command-line flags such as `--no-deprecation` or `--trace-deprecation`, and `process.traceDeprecation` is unsupported. A stored `util.deprecate` reports a `length` of 4, where Node reports 3. In TypeScript, a `util.callbackify` result must reach a callback whose error parameter accepts `null`; the @types/node overloads that type it as non-null are refused. diff --git a/docs/content/limitations/child-process-workers.mdx b/docs/content/limitations/child-process-workers.mdx index 5b41c5e53..9d6602509 100644 --- a/docs/content/limitations/child-process-workers.mdx +++ b/docs/content/limitations/child-process-workers.mdx @@ -12,3 +12,7 @@ Message listeners may return `void` or `Promise`. Messages use newline-delimited JSON, so their static types must be JSON-stringifiable or `unknown` backed by a checked JSON tree. `send()` returns the channel's backpressure state and invokes its callback later with `null` or an `Error`. `disconnect()` changes `connected` synchronously, before the later disconnect events. + +A `'data'` listener on a child's `stdout` or `stderr` whose chunk is `unknown` or `any` receives strings when a `setEncoding()` statement on the same stream precedes it in its block or an enclosing block, and Buffers when the function never calls `setEncoding()` on that stream. +Any other placement of `setEncoding()` is refused at compile time, and a `setEncoding()` call that runs elsewhere afterwards makes the listener throw a TypeError instead of receiving strings. +An inline `'exit'` or `'close'` listener may declare `(code: number, signal: string)`, which TypeScript accepts through the catch-all `on` overload; it receives the `null` values Node passes, and a typed use of a `null` value throws a TypeError. diff --git a/docs/content/limitations/console-and-process-streams.mdx b/docs/content/limitations/console-and-process-streams.mdx index 0d7cd8f30..c62b82f0c 100644 --- a/docs/content/limitations/console-and-process-streams.mdx +++ b/docs/content/limitations/console-and-process-streams.mdx @@ -9,4 +9,5 @@ JavaScript can store `process.stdin`, `stdout`, and `stderr`, replace output `wr Input supports data/end/error listeners, pause/resume, buffered reads, and TTY raw mode. Output writes are synchronous, with successful callbacks on the next-tick queue; queued backpressure is not modeled. Stored streams do not support piping, encoding changes, arbitrary properties, or other events. -TypeScript output parameters support `write(string)`. +A TypeScript `NodeJS.WritableStream` parameter that only receives `process.stdout` or `process.stderr` supports `write(string)`. +When other streams flow into the same parameter, the process streams join them as `Writable` instances with the full writable surface. diff --git a/docs/content/limitations/language-features/generics.mdx b/docs/content/limitations/language-features/generics.mdx index 6e203506f..a234c8f57 100644 --- a/docs/content/limitations/language-features/generics.mdx +++ b/docs/content/limitations/language-features/generics.mdx @@ -3,3 +3,9 @@ Independent concrete uses have no specialization-count limit; recursively expanding demands and type layouts produce diagnostics. Other generic calls compile when the target resolves statically. Generic function values require a concrete signature at use and a binding that is never reassigned. +- **Variadic wrappers:** A generic higher-order wrapper is specialized to each wrapped function's exact signature. + `Parameters`, `ReturnType`, and rest parameters typed as fixed tuples become that signature's parameter list. + A function expression with no parameters that reads `arguments` compiles only when its value is asserted to, or contextually typed as, one fixed-arity signature; `arguments` is then that signature's tuple. + Forwarding it with `fn.apply(this, arguments)` passes `this` through. + Reading `fn.name` on a wrapped function requires its name to be known where the wrapper is called, such as a function declaration or a `const`-bound function. + `Parameters` over a signature with optional or array-typed rest parameters is unsupported, because that tuple has no fixed length. diff --git a/docs/content/limitations/webassembly-modules.mdx b/docs/content/limitations/webassembly-modules.mdx index a0f49ca49..43fa55e48 100644 --- a/docs/content/limitations/webassembly-modules.mdx +++ b/docs/content/limitations/webassembly-modules.mdx @@ -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. diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json index c6e5bbb16..e5fd082f6 100644 --- a/docs/src/generated/node-v24-compatibility.json +++ b/docs/src/generated/node-v24-compatibility.json @@ -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" } }, { diff --git a/internal/compatibility/generated/node-v24-backlog.json b/internal/compatibility/generated/node-v24-backlog.json index 08f199659..51459d3b3 100644 --- a/internal/compatibility/generated/node-v24-backlog.json +++ b/internal/compatibility/generated/node-v24-backlog.json @@ -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" + ] } } }, diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index 43927ddc4..0e1e73a26 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -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" }, diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index 19f6e32d1..734e038ef 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -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"] }, diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index cb059fc1d..411291d94 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -80,21 +80,54 @@ declare namespace NodeJS { path?: string | undefined; syscall?: string | undefined; } - /* The piped child-output stream (child.stdout / child.stderr under - * stdio ["ignore", "pipe", "pipe"]): 'data' fires one Buffer chunk per - * read (≤64KB, consumer-driven — no listener, no read), 'end' fires - * once at EOF and always BEFORE the child's 'exit'. A flowing stream - * keeps the loop alive. */ + /* The readable-stream interface every Node stream implements. The + * piped child-output stream (child.stdout / child.stderr under stdio + * ["ignore", "pipe", "pipe"]) is declared with it: 'data' fires one + * Buffer chunk per read (≤64KB, consumer-driven — no listener, no + * read), 'end' fires once at EOF and always BEFORE the child's 'exit'. A + * flowing stream keeps the loop alive. A program that also passes + * other streams (fs, zlib, node:stream) where this interface is + * expected compiles it as the node:stream Readable, with child output + * bridged into one. */ interface ReadableStream { on(event: "data", listener: (chunk: Buffer) => void): void; on(event: "data", listener: (chunk: string) => void): void; on(event: "end", listener: () => void): void; + on(event: string, listener: (...args: any[]) => void): void; once(event: "data", listener: (chunk: Buffer) => void): void; once(event: "data", listener: (chunk: string) => void): void; once(event: "end", listener: () => void): void; + once(event: string, listener: (...args: any[]) => void): void; /* setEncoding('utf8') makes subsequent data chunks strings, including * multibyte sequences split across native pipe reads. */ setEncoding(encoding: string): this; + readonly readable: boolean; + pause(): this; + resume(): this; + isPaused(): boolean; + pipe(destination: T, options?: { end?: boolean }): T; + unpipe(destination?: WritableStream): this; + [Symbol.asyncIterator](): AsyncIterableIterator; + } + /* The writable-stream interface every Node stream implements: + * process.stdout/process.stderr, fs.WriteStream and the node:stream + * Writable family. Written only to the process streams, it compiles as + * their descriptor; with other streams flowing into it, as the + * node:stream Writable (the process streams convert into one). */ + interface WritableStream { + readonly writable: boolean; + write(data: string | Uint8Array, callback?: (error?: Error | null) => void): boolean; + write( + data: string | Uint8Array, + encoding: BufferEncoding, + callback?: (error?: Error | null) => void, + ): boolean; + end(callback?: () => void): void; + end(data: string | Uint8Array, callback?: () => void): void; + end(data: string | Uint8Array, encoding: BufferEncoding, callback?: () => void): void; + /* Node's EventEmitter signature: listeners take untyped arguments. */ + on(event: string, listener: (...args: any[]) => void): void; + once(event: string, listener: (...args: any[]) => void): void; } } @@ -1271,8 +1304,37 @@ declare module "node:perf_hooks" { getEntriesByType(type: EntryType): PerformanceEntry[]; clearMarks(name?: string): void; clearMeasures(name?: string): void; + eventLoopUtilization( + utilization1?: EventLoopUtilization, + utilization2?: EventLoopUtilization, + ): EventLoopUtilization; } export const performance: Performance; + /* Event-loop metrics: utilization (idle/active ms since the loop started) + * and monitorEventLoopDelay's sampling histogram (nanoseconds). */ + export interface EventLoopUtilization { + idle: number; + active: number; + utilization: number; + } + export interface EventLoopMonitorOptions { + resolution?: number | undefined; + } + export interface Histogram { + readonly count: number; + readonly exceeds: number; + readonly max: number; + readonly mean: number; + readonly min: number; + readonly stddev: number; + percentile(percentile: number): number; + reset(): void; + } + export interface IntervalHistogram extends Histogram { + enable(): boolean; + disable(): boolean; + } + export function monitorEventLoopDelay(options?: EventLoopMonitorOptions): IntervalHistogram; } declare module "perf_hooks" { export * from "node:perf_hooks"; @@ -3066,8 +3128,10 @@ declare module "child_process" { destroy(): void; on(event: "drain" | "finish", listener: () => void): void; on(event: "error", listener: (err: Error) => void): void; + on(event: string, listener: (...args: any[]) => void): void; once(event: "drain" | "finish", listener: () => void): void; once(event: "error", listener: (err: Error) => void): void; + once(event: string, listener: (...args: any[]) => void): void; } export interface ChildProcess extends Disposable { @@ -6414,7 +6478,7 @@ declare module "node:cluster" { /* node:tty — the fd probe (the same isatty(3) behind process.*.isTTY) * and the supported structural view of a process output stream. */ declare module "tty" { - export interface WriteStream { + export interface WriteStream extends NodeJS.WritableStream { write(data: string | Uint8Array, callback?: (error?: Error | null) => void): boolean; write( data: string | Uint8Array, @@ -6664,8 +6728,8 @@ declare module "stream" { resume(): this; isPaused(): boolean; setEncoding(encoding: string): this; - pipe(destination: T, options?: { end?: boolean }): T; - unpipe(destination?: Writable): this; + pipe(destination: T, options?: { end?: boolean }): T; + unpipe(destination?: NodeJS.WritableStream): this; destroy(error?: Error): this; readonly readable: boolean; readonly readableEnded: boolean; diff --git a/packages/compiler/src/backend/executable-features.ts b/packages/compiler/src/backend/executable-features.ts index 8c7718227..38f44e58e 100644 --- a/packages/compiler/src/backend/executable-features.ts +++ b/packages/compiler/src/backend/executable-features.ts @@ -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, }; } diff --git a/packages/compiler/src/backend/library-pack.ts b/packages/compiler/src/backend/library-pack.ts index 4ca5856ea..42275c5af 100644 --- a/packages/compiler/src/backend/library-pack.ts +++ b/packages/compiler/src/backend/library-pack.ts @@ -42,6 +42,7 @@ export async function compilePackedLibrary( bigint: options.bigint ?? false, searchParams: options.searchParams ?? false, qs: false, + dynBuiltinClass: false, parseArgs: false, stream: false, net: false, @@ -55,6 +56,7 @@ export async function compilePackedLibrary( tls: false, cipher: false, webcrypto: false, + perfLoop: false, tlsCa: false, }; const selection = await loadRuntimePack({ diff --git a/packages/compiler/src/backend/llvm/emit-module.ts b/packages/compiler/src/backend/llvm/emit-module.ts index c751d82ad..4e4f11828 100644 --- a/packages/compiler/src/backend/llvm/emit-module.ts +++ b/packages/compiler/src/backend/llvm/emit-module.ts @@ -1,4 +1,5 @@ /* LlEmitter's module-level output: module parts, helper definitions, library declarations and FFI callback definitions. */ +import { emitWasmModules } from "./wasm-modules.js"; import { type LlEmitter, ffiCallbackDummyLl, llStrBytes } from "./emitter.js"; import { mangleAsyncSpawn, @@ -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`, diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index d7efec115..b72a6e226 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -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); } diff --git a/packages/compiler/src/backend/llvm/expr-callbacks.ts b/packages/compiler/src/backend/llvm/expr-callbacks.ts index b7954c174..28bd15759 100644 --- a/packages/compiler/src/backend/llvm/expr-callbacks.ts +++ b/packages/compiler/src/backend/llvm/expr-callbacks.ts @@ -771,6 +771,31 @@ export function childDataThunkFor(host: LlvmEmitterContext, param: IrType): stri return sym; } +/** The child-stream 'data' thunk for an unknown/any chunk parameter: the + * runtime's Buffer (or, after setEncoding, string) chunk boxes into a + * checked-dynamic value the listener owns. */ +export function childDataDynThunkFor(host: LlvmEmitterContext, chunk: "bytes" | "str"): string { + const key = `cdd:${chunk}`; + let sym = host.resolveThunks.get(key); + if (sym) return sym; + sym = `sc_cdd_${host.resolveThunks.size}`; + host.resolveThunks.set(key, sym); + const box = chunk === "bytes" ? "scr_dyn_new_buffer" : "scr_dyn_new_str"; + host.declare(`declare ptr @${box}(ptr)`); + host.resolveThunkDefs.push( + `define internal void @${sym}(ptr %cb, ptr %chunk) ${FN_ATTRS} { ; child 'data' → dyn (${chunk})`, + `entry:`, + ` %d = call ptr @${box}(ptr %chunk)`, + ` %fnp = getelementptr inbounds %ScrClosure, ptr %cb, i64 0, i32 1`, + ` %fn = load ptr, ptr %fnp`, + ` call void %fn(ptr %cb, ptr %d)`, + ` ret void`, + `}`, + ``, + ); + return sym; +} + export function emitterFixedAdapter( host: LlvmEmitterContext, cbT: IrType & { kind: "func" }, diff --git a/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts b/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts index 3f7961163..9be757a20 100644 --- a/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts +++ b/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts @@ -17,7 +17,7 @@ export function streamDataAdapter( const p = cbT.params[0]; if ( cbT.params.length > 1 || - (p && p.kind !== "bytes" && p.kind !== "string" && p.kind !== "dyn") + (p && p.kind !== "bytes" && p.kind !== "string" && p.kind !== "dyn" && p.kind !== "union") ) { throw new InternalCompilerError( "llvm emitter bug: stream data listener param shape (frontend must fence)", @@ -73,6 +73,46 @@ export function streamDataAdapter( ); finish("%r0"); d.push(`}`, ``); + } else if (p.kind === "union") { + // A `Buffer | string` chunk: wrap whichever slot the stream filled at + // its arm of the (boxed) union. + const def = host.unionsById.get(p.unionId); + const bytesTag = def ? def.arms.findIndex((a) => a.kind === "bytes" && a.elem === "u8") : -1; + const strTag = def ? def.arms.findIndex((a) => a.kind === "string") : -1; + if ( + !def || + host.nullableUnions.has(p.unionId) || + bytesTag < 0 || + strTag < 0 || + def.arms.length !== 2 + ) + throw new InternalCompilerError( + "llvm emitter bug: stream data listener union is not Buffer | string (frontend must fence)", + ); + host.declare(`declare ptr @scr_union_new_ref(i32, ptr, ptr, ptr, ptr)`); + host.declare(`declare ptr @scr_bytes_retain_v(ptr)`); + host.declare(`declare void @scr_bytes_release_v(ptr)`); + host.declare(`declare ptr @scr_str_retain_v(ptr)`); + host.declare(`declare void @scr_str_release_v(ptr)`); + d.push( + ` %uslot = alloca ptr`, + ` %isb = icmp ne ptr %a0, null`, + ` br i1 %isb, label %buf, label %str`, + `buf:`, + ` %rb = call ptr @scr_bytes_retain_v(ptr %a0)`, + ` %ub = call ptr @scr_union_new_ref(i32 ${bytesTag}, ptr %rb, ptr @scr_bytes_retain_v, ptr @scr_bytes_release_v, ptr null)`, + ` store ptr %ub, ptr %uslot`, + ` br label %go`, + `str:`, + ` %rs = call ptr @scr_str_retain_v(ptr %a1)`, + ` %us = call ptr @scr_union_new_ref(i32 ${strTag}, ptr %rs, ptr @scr_str_retain_v, ptr @scr_str_release_v, ptr null)`, + ` store ptr %us, ptr %uslot`, + ` br label %go`, + `go:`, + ` %uv = load ptr, ptr %uslot`, + ); + finish("%uv"); + d.push(`}`, ``); } else { // dyn: box by runtime tag — the JS lane's adapter parameter. host.declare(`declare ptr @scr_dyn_new_buffer(ptr)`); diff --git a/packages/compiler/src/backend/llvm/lib-process.ts b/packages/compiler/src/backend/llvm/lib-process.ts index eaf7f31cd..4a7be1a35 100644 --- a/packages/compiler/src/backend/llvm/lib-process.ts +++ b/packages/compiler/src/backend/llvm/lib-process.ts @@ -305,6 +305,28 @@ export function emitChildProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr B.line(`${raw} = call ptr @${get}(ptr ${recv.name}) ; +1 or NULL`); return host.wrapNullable(raw, raw, def!.arms[streamTag]!, streamTag, e.type, nullTag); } + if (e.fn === "child.stdioStream") { + // The general stream representation's stdio read: the child's bridged + // runtime stream (+1) or NULL for an unpiped slot. + if (e.type.kind !== "union") + throw new InternalCompilerError(`llvm emitter bug: ${e.fn} result is not a union`); + const def = host.unionsById.get(e.type.unionId); + const streamTag = def ? def.arms.findIndex((a) => a.kind === "object") : -1; + const nullTag = def ? def.arms.findIndex((a) => a.kind === "nullT") : -1; + const fd = e.args[1]; + if (streamTag < 0 || nullTag < 0 || fd?.kind !== "numLit") + throw new InternalCompilerError(`llvm emitter bug: ${e.fn} lacks its arms or slot`); + const recv = host.emitExpr(e.args[0]!); + const key = fd.value === 0 ? "stdin" : fd.value === 1 ? "stdout" : "stderr"; + host.declare(`declare ptr @scr_stream_child_stdio(ptr, ptr, ${host.sizeType})`); + const raw = B.tmp(); + B.line( + `${raw} = call ptr @scr_stream_child_stdio(ptr ${recv.name}, ptr ${host.cstr(key)}, ${host.sizeType} ${key.length}) ; +1 or NULL`, + ); + const out = host.wrapNullable(raw, raw, def!.arms[streamTag]!, streamTag, e.type, nullTag); + host.emitPendingCheck(); + return out; + } if (e.fn === "writer.writeString" || e.fn === "writer.writeBytes") { const writer = host.emitExpr(e.args[0]!); const data = host.emitExpr(e.args[1]!); diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 074205b3b..95992a441 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -278,6 +278,14 @@ export const LIB_FN_SYMS: Record = { "perf.entryDetail": "scr_perf_entry_detail", "perf.entryKeep": "scr_perf_entry_keep", "perf.entryRef": "scr_perf_entry_ref", + "perf.loopStart": "scr_perf_loop_start", + "perf.idleTime": "scr_perf_idle_time", + "perf.eldNew": "scr_eld_new", + "perf.eldEnable": "scr_eld_enable", + "perf.eldDisable": "scr_eld_disable", + "perf.eldReset": "scr_eld_reset", + "perf.eldPercentile": "scr_eld_percentile", + "perf.eldStat": "scr_eld_stat", "process.memoryUsageTake": "scr_process_memory_usage_take", "process.memoryUsageField": "scr_process_memory_usage_field", "process.rss": "scr_process_rss", @@ -727,6 +735,8 @@ export const LIB_FN_SYMS: Record = { "child.unref": "scr_child_unref", "child.ref": "scr_child_ref", "procStream.write": "scr_proc_stream_write", + "stream.stdioWritable": "scr_stream_stdio_writable", + "stream.unboxDyn": "scr_stream_unbox_dyn", "process.kill": "scr_process_kill_named", "process.killNum": "scr_process_kill", // fs.promises (scr_bytes_io.c / scr_lib.c): the promise forms settle @@ -1128,6 +1138,7 @@ export const LIB_FN_SYMS: Record = { "readable.opFind": "scr_stream_op_find", "readable.opReduce": "scr_stream_op_reduce", "readable.pipe": "scr_stream_pipe", + "readable.pipeGeneral": "scr_stream_pipe_general", "writable.cork": "scr_stream_cork", "writable.uncork": "scr_stream_uncork", "stream.destroyErr": "scr_stream_destroy", @@ -1442,6 +1453,9 @@ export const LIB_FN_SYMS: Record = { "dyn.classPrototype": "scr_dyn_class_prototype", "dyn.classBasePrototype": "scr_dyn_class_base_prototype", "dyn.classInherit": "scr_dyn_class_inherit", + "dyn.builtinClass": "scr_dyn_builtin_class", + "dyn.builtinInstanceOf": "scr_dyn_builtin_instanceof", + "dyn.functionInstanceOf": "scr_dyn_function_instanceof", "dyn.classSuper": "scr_dyn_class_super", "dyn.assignPrototype": "scr_dyn_assign_prototype", "dyn.definePrototypeProps": "scr_dyn_define_prototype_props", @@ -1614,6 +1628,7 @@ export const USES_TIMERS_LIB_FNS = new Set([ "readable.opReduce", "readable.resume", "readable.pipe", + "readable.pipeGeneral", "readable.unpipe", "writable.write", "writable.writeStr", diff --git a/packages/compiler/src/backend/llvm/lib-stream.ts b/packages/compiler/src/backend/llvm/lib-stream.ts index b62b237df..0ded59a1a 100644 --- a/packages/compiler/src/backend/llvm/lib-stream.ts +++ b/packages/compiler/src/backend/llvm/lib-stream.ts @@ -56,6 +56,8 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV host.declare(`declare void @scr_child_stream_thunk0(ptr, ptr)`); } else if (param.kind === "union") { adapter = host.childDataThunkFor(param); + } else if (param.kind === "dyn") { + adapter = host.childDataDynThunkFor("bytes"); } else { adapter = "scr_child_stream_thunk_bytes"; host.declare(`declare void @scr_child_stream_thunk_bytes(ptr, ptr)`); @@ -78,7 +80,8 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV if (e.fn === "stream.onDataStr") { host.usesTimers = true; const cbT = e.args[1]!.type; - if (cbT.kind !== "func" || cbT.params[0]?.kind !== "string") { + const strParam = cbT.kind === "func" ? cbT.params[0]?.kind : undefined; + if (strParam !== "string" && strParam !== "dyn") { throw new InternalCompilerError( "llvm emitter bug: stream.onDataStr callback not a string func", ); @@ -87,10 +90,12 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV const cb = host.emitExpr(e.args[1]!); const once = host.emitExpr(e.args[2]!); host.toHeap(cb); - host.declare(`declare void @scr_child_stream_thunk_str(ptr, ptr)`); + let adapter = "scr_child_stream_thunk_str"; + if (strParam === "dyn") adapter = host.childDataDynThunkFor("str"); + else host.declare(`declare void @scr_child_stream_thunk_str(ptr, ptr)`); host.declare(`declare void @scr_child_stream_on_data_str(ptr, ptr, ptr, i1 zeroext)`); B.line( - `call void @scr_child_stream_on_data_str(ptr ${s0.name}, ptr ${cb.name}, ptr @scr_child_stream_thunk_str, i1 ${once.name})`, + `call void @scr_child_stream_on_data_str(ptr ${s0.name}, ptr ${cb.name}, ptr @${adapter}, i1 ${once.name})`, ); return { name: "", type: e.type }; } diff --git a/packages/compiler/src/backend/llvm/wasm-modules.ts b/packages/compiler/src/backend/llvm/wasm-modules.ts index 8b8479bc8..8a7045687 100644 --- a/packages/compiler/src/backend/llvm/wasm-modules.ts +++ b/packages/compiler/src/backend/llvm/wasm-modules.ts @@ -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(""); diff --git a/packages/compiler/src/backend/native-link-info-core.ts b/packages/compiler/src/backend/native-link-info-core.ts index 04fb15336..031f99ddd 100644 --- a/packages/compiler/src/backend/native-link-info-core.ts +++ b/packages/compiler/src/backend/native-link-info-core.ts @@ -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. */ diff --git a/packages/compiler/src/backend/native/contracts.ts b/packages/compiler/src/backend/native/contracts.ts index 052329401..8abdc6c38 100644 --- a/packages/compiler/src/backend/native/contracts.ts +++ b/packages/compiler/src/backend/native/contracts.ts @@ -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 diff --git a/packages/compiler/src/backend/native/executable.ts b/packages/compiler/src/backend/native/executable.ts index 813339389..33c126de8 100644 --- a/packages/compiler/src/backend/native/executable.ts +++ b/packages/compiler/src/backend/native/executable.ts @@ -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. diff --git a/packages/compiler/src/backend/runtime-pack-targets.test.ts b/packages/compiler/src/backend/runtime-pack-targets.test.ts index 49ca691cb..a39ce78e2 100644 --- a/packages/compiler/src/backend/runtime-pack-targets.test.ts +++ b/packages/compiler/src/backend/runtime-pack-targets.test.ts @@ -107,6 +107,7 @@ const features = { bigint: false, searchParams: false, qs: false, + dynBuiltinClass: false, parseArgs: false, stream: false, net: false, @@ -121,6 +122,7 @@ const features = { tlsCa: false, cipher: false, webcrypto: false, + perfLoop: false, }; test.each([ diff --git a/packages/compiler/src/backend/runtime-pack.test.ts b/packages/compiler/src/backend/runtime-pack.test.ts index 91a74eb7e..1681626b7 100644 --- a/packages/compiler/src/backend/runtime-pack.test.ts +++ b/packages/compiler/src/backend/runtime-pack.test.ts @@ -44,6 +44,7 @@ const BASE: NativeLinkFeatures = { bigint: false, searchParams: false, qs: false, + dynBuiltinClass: false, parseArgs: false, stream: false, net: false, @@ -58,6 +59,7 @@ const BASE: NativeLinkFeatures = { tlsCa: false, cipher: false, webcrypto: false, + perfLoop: false, }; async function fixture(target: NativeTargetSpec = MACOS_ARM64_TARGET) { diff --git a/packages/compiler/src/backend/warm-cache.ts b/packages/compiler/src/backend/warm-cache.ts index eb7822d10..9a505d517 100644 --- a/packages/compiler/src/backend/warm-cache.ts +++ b/packages/compiler/src/backend/warm-cache.ts @@ -50,6 +50,7 @@ export async function warmNativeCaches( bigint: false, searchParams: false, qs: false, + dynBuiltinClass: false, parseArgs: false, stream: false, net: false, @@ -63,6 +64,7 @@ export async function warmNativeCaches( tls: false, cipher: false, webcrypto: false, + perfLoop: false, tlsCa: false, }, }); diff --git a/packages/compiler/src/executable/executable-cache.test.ts b/packages/compiler/src/executable/executable-cache.test.ts index d12a5d1f0..5b0982dc1 100644 --- a/packages/compiler/src/executable/executable-cache.test.ts +++ b/packages/compiler/src/executable/executable-cache.test.ts @@ -45,6 +45,7 @@ const native: EarlyExecutableNativeFeatures = { bigint: false, searchParams: false, qs: false, + dynBuiltinClass: false, parseArgs: false, stream: false, net: false, @@ -59,6 +60,7 @@ const native: EarlyExecutableNativeFeatures = { tlsCa: false, cipher: false, webcrypto: false, + perfLoop: false, }; async function fixture(): Promise<{ diff --git a/packages/compiler/src/executable/executable-cache.ts b/packages/compiler/src/executable/executable-cache.ts index 55dde168d..fd4a34ec2 100644 --- a/packages/compiler/src/executable/executable-cache.ts +++ b/packages/compiler/src/executable/executable-cache.ts @@ -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 { diff --git a/packages/compiler/src/frontend/lowering/array-values.ts b/packages/compiler/src/frontend/lowering/array-values.ts index 8f2b28a3f..0ca8503df 100644 --- a/packages/compiler/src/frontend/lowering/array-values.ts +++ b/packages/compiler/src/frontend/lowering/array-values.ts @@ -8,6 +8,7 @@ import { type IrType, isIntegerBytesElem, type SrcLoc, + STRING, UNDEFINED_T, typeEquals, typeKey, @@ -84,29 +85,54 @@ export function arrayValueRead( }; } -/** Read a stabilized union of native arrays without copying either the - * receiver or its elements. Every arm contributes its missing-value case. */ +/** Read one position of a stabilized union of native arrays and tuples + * (`const [a, b] = re.exec(s) || ["", ""]`) without copying the receiver + * or its elements. Every array arm contributes its missing-value case; a + * tuple arm answers its field, or undefined past its end. Tuple arms need + * a constant position. With `stringChars` (the string iterator's code + * points as a fresh string[]), a string arm (`re.exec(s) || ""`) answers + * its code point at the position, or undefined past its end. */ export function unionArrayValueRead( lowerer: Lowerer, value: IrExpr, index: IrExpr, loc: SrcLoc, + stringChars?: (text: IrExpr) => IrExpr, ): IrExpr | null { if (value.type.kind !== "union") return null; const unionId = value.type.unionId; const arms = lowerer.unions.get(unionId)?.arms; if (!arms?.length) return null; + const position = index.kind === "numLit" ? String(index.value) : null; + // A tuple arm's answer at the position: its field, or undefined past + // its end. Null when the arm is neither an array nor a tuple. + const tupleField = (arm: IrType): { name: string; type: IrType } | "past" | null => { + if (arm.kind !== "record" || position === null) return null; + const shape = lowerer.shapes.get(arm.shapeId); + if (!shape?.tuple) return null; + return shape.fields.find((f) => f.name === position) ?? "past"; + }; // Keep the collection's IrType union layout and narrow each element at // its use; an inferred array predicate changes the collection's ABI. - for (const arm of arms) if (arm.kind !== "array") return null; + const stringArm = (arm: IrType): boolean => arm.kind === "string" && stringChars !== undefined; + for (const arm of arms) + if (arm.kind !== "array" && !stringArm(arm) && tupleField(arm) === null) return null; const answers = new Map(); for (const arm of arms) { - if (arm.kind !== "array") return null; - const answer = arrayValueType(lowerer, arm.elem); + const field = arm.kind === "array" || stringArm(arm) ? null : tupleField(arm); + const answer = + arm.kind === "array" + ? arrayValueType(lowerer, arm.elem) + : stringArm(arm) + ? arrayValueType(lowerer, STRING) + : field === "past" + ? UNDEFINED_T + : field!.type; const parts = answer.kind === "union" ? lowerer.unions.get(answer.unionId)!.arms : [answer]; for (const part of parts) answers.set(typeKey(part), part); } const joined = [...answers.values()].sort((a, b) => (typeKey(a) < typeKey(b) ? -1 : 1)); + if (joined.length === 1 && joined[0]!.kind === "undefinedT") return null; if ( joined.length > 1 && (!unionContainerArmsOk(joined) || @@ -118,12 +144,39 @@ export function unionArrayValueRead( let result: IrExpr | null = null; for (let tag = arms.length - 1; tag >= 0; tag--) { const arm = arms[tag]!; - if (arm.kind !== "array") return null; const receiver: IrExpr = { kind: "unionNarrow", unionId, tag, value, type: arm, loc }; - const read = lowerer.coerceToExpected( - arrayValueRead(lowerer, receiver, index, arm.elem, loc), - type, - ); + let read: IrExpr; + if (arm.kind === "array") { + read = lowerer.coerceToExpected( + arrayValueRead(lowerer, receiver, index, arm.elem, loc), + type, + ); + } else if (stringArm(arm)) { + read = lowerer.coerceToExpected( + arrayValueRead(lowerer, stringChars!(receiver), index, STRING, loc), + type, + ); + } else { + const field = tupleField(arm); + if (field === null) return null; + if (field === "past") { + const missing = lowerer.wrappedUndefined(type, loc); + if (!missing) return null; + read = missing; + } else { + read = lowerer.coerceToExpected( + { + kind: "recordGet", + obj: receiver, + shapeId: (arm as IrType & { kind: "record" }).shapeId, + field: field.name, + type: field.type, + loc, + }, + type, + ); + } + } result = result === null ? read diff --git a/packages/compiler/src/frontend/lowering/builtin-calls.ts b/packages/compiler/src/frontend/lowering/builtin-calls.ts index de287be4f..d2e4b53d5 100644 --- a/packages/compiler/src/frontend/lowering/builtin-calls.ts +++ b/packages/compiler/src/frontend/lowering/builtin-calls.ts @@ -1,4 +1,5 @@ import { lowerReadlineCursorCall } from "./builtins/readline.js"; +import { lowerMonitorEventLoopDelayCall } from "./builtins/performance.js"; import { lowerEnvironmentData, lowerReceiveMessageOnPort, @@ -104,6 +105,9 @@ export function lowerBuiltinCall( return lowerAddAbortListener(lowerer, expr, loc); } if (bi.module === "events" && bi.member === "once") return lowerEventsOnce(lowerer, expr, loc); + if (bi.module === "perf_hooks" && bi.member === "monitorEventLoopDelay") { + return lowerMonitorEventLoopDelayCall(lowerer, expr); + } if (bi.module === "events") { const helper = lowerEventsEmitterHelper(lowerer, expr, bi.member, loc); if (helper) return helper; diff --git a/packages/compiler/src/frontend/lowering/builtins/builtin-class-dyn.ts b/packages/compiler/src/frontend/lowering/builtins/builtin-class-dyn.ts new file mode 100644 index 000000000..ac73924ad --- /dev/null +++ b/packages/compiler/src/frontend/lowering/builtins/builtin-class-dyn.ts @@ -0,0 +1,149 @@ +import { numLit } from "../../../ir/build.js"; +import { BOOL, DYN, RUNTIME_EMITTER_CLASS, type IrExpr, type SrcLoc } from "../../../ir/ir.js"; +import * as ts from "../../ts7/adapter.js"; +import { locOf } from "../../program.js"; +import type { Lowerer } from "../lowerer.js"; + +/* EventEmitter and the stream classes as untyped constructor values, the + * bases of JavaScript constructor functions: + * + * function Bus() { EventEmitter.call(this); } + * util.inherits(Bus, EventEmitter); + * + * A compiled `class Bus extends EventEmitter` gets a native emitter prefix, + * but `new Bus()` on a constructor function builds an untyped object, so + * the base is the runtime's dyn constructor (scr_dyn_builtin_class.c): a + * function whose prototype carries Node's EventEmitter methods over the + * object's own `_events` table. The stream classes have prototype chains + * for util.inherits and instanceof; constructing over them refuses where + * it runs. These values appear only where a constructor function uses its + * base: util.inherits, `Base.call(this, …)` and `Base.apply(this, …)`, + * `Base.prototype` and `instanceof Base` on an untyped value. */ + +/** scr_dyn_builtin_class's kind for a runtime class, or null. */ +export function builtinClassKind(className: string): number | null { + switch (className) { + case RUNTIME_EMITTER_CLASS: + return 0; + case "%Readable": + return 2; + case "%Writable": + return 3; + case "%Duplex": + return 4; + case "%Transform": + return 5; + case "%PassThrough": + return 6; + default: + return null; + } +} + +/** The classes by builtin module member. JavaScript sources bind + * `require("events").EventEmitter` as a module member rather than a class + * value, and `require("stream")` is the legacy Stream constructor. */ +const MODULE_CLASS_KINDS: Readonly>>> = { + events: { EventEmitter: 0, default: 0 }, + stream: { Stream: 1, Readable: 2, Writable: 3, Duplex: 4, Transform: 5, PassThrough: 6 }, +}; + +/** The kind of a builtin module member that is one of those classes. */ +export function builtinMemberClassKind(module: string, member: string): number | null { + return MODULE_CLASS_KINDS[module]?.[member] ?? null; +} + +function unparenthesized(node: ts.Expression): ts.Expression { + let current = node; + while (ts.isParenthesizedExpression(current)) current = current.expression; + return current; +} + +/** The kind of a node that names one of those classes: a class value, a + * builtin module member, or the events and stream modules themselves. */ +export function builtinClassKindOf(lowerer: Lowerer, node: ts.Expression): number | null { + const expr = unparenthesized(node); + const mapped = lowerer.mapTypeOf(lowerer.typeOf(expr)); + if (mapped?.kind === "classval") return builtinClassKind(mapped.className); + const member = ts.isIdentifier(expr) + ? lowerer.builtinImportOf(expr) + : ts.isPropertyAccessExpression(expr) + ? lowerer.builtinMemberOf(expr) + : null; + if (member) return MODULE_CLASS_KINDS[member.module]?.[member.member] ?? null; + const module = lowerer.builtinNamespaceModuleOf(expr); + return module === "events" ? 0 : module === "stream" ? 1 : null; +} + +/** Whether evaluating the node can be skipped: a binding or a member of + * one, or of a builtin require. */ +function referenceOnly(node: ts.Expression): boolean { + const expr = unparenthesized(node); + if (ts.isIdentifier(expr)) return true; + if (ts.isPropertyAccessExpression(expr) && !expr.questionDotToken) { + const base = unparenthesized(expr.expression); + return ( + ts.isIdentifier(base) || + (ts.isCallExpression(base) && + ts.isIdentifier(base.expression) && + base.expression.text === "require" && + base.arguments.nodes.length === 1 && + ts.isStringLiteral(base.arguments.nodes[0]!)) + ); + } + return false; +} + +/** The untyped constructor named by a node, or null when it names none of + * those classes. */ +export function builtinClassDynValueOf(lowerer: Lowerer, node: ts.Expression): IrExpr | null { + const kind = builtinClassKindOf(lowerer, node); + if (kind === null) return null; + const loc = locOf(node); + const value = builtinClassValue(kind, loc); + if (referenceOnly(node)) return value; + return { + kind: "seqExpr", + stmts: [{ kind: "exprStmt", expr: lowerer.lowerExpr(node), loc }], + result: value, + type: DYN, + loc, + }; +} + +export function builtinClassValue(kind: number, loc: SrcLoc): IrExpr { + return { kind: "libCall", fn: "dyn.builtinClass", args: [numLit(kind, loc)], type: DYN, loc }; +} + +/** `Base.call(this, …)` / `Base.apply(this, args)` on one of those class + * values: Function.prototype.call/apply over the untyped constructor. */ +export function lowerBuiltinClassCall(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null { + const callee = call.expression; + if (!ts.isPropertyAccessExpression(callee) || callee.questionDotToken) return null; + const method = callee.name.text; + if (method !== "call" && method !== "apply") return null; + if (call.arguments.nodes.some(ts.isSpreadElement)) return null; + const receiver = builtinClassDynValueOf(lowerer, callee.expression); + if (receiver === null) return null; + const loc = locOf(call); + return { + kind: "dynInvoke", + recv: receiver, + method, + calleeName: callee.getText(), + args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), + type: DYN, + loc, + }; +} + +/** `value instanceof Base` for an untyped value. */ +export function builtinClassInstanceOf(value: IrExpr, kind: number, loc: SrcLoc): IrExpr { + return { + kind: "libCall", + fn: "dyn.builtinInstanceOf", + args: [value, numLit(kind, loc)], + type: BOOL, + loc, + }; +} diff --git a/packages/compiler/src/frontend/lowering/builtins/child-process-methods.ts b/packages/compiler/src/frontend/lowering/builtins/child-process-methods.ts index a435bf740..e1a25d169 100644 --- a/packages/compiler/src/frontend/lowering/builtins/child-process-methods.ts +++ b/packages/compiler/src/frontend/lowering/builtins/child-process-methods.ts @@ -1,5 +1,6 @@ import { boolLit, strLit } from "../../../ir/build.js"; import * as ts from "../../ts7/adapter.js"; +import { requestGeneralStreamSurface } from "../general-streams.js"; import { type Lowerer } from "../lowerer.js"; import { isJsSourceFile, locOf } from "../../program.js"; import { isChildSurfaceMember } from "../surfaces.js"; @@ -131,8 +132,16 @@ export function lowerChildMethodCall( ) { for (const param of listenerNode.parameters.nodes) lowerer.checkedCallbackParams.add(param); } - const cb = lowerer.lowerExpr(call.arguments.nodes[1]!); + const narrowed = + event === "exit" || event === "close" + ? narrowedExitListenerParams(lowerer, listenerNode) + : []; + const cb = + narrowed.length > 0 + ? lowerWithCheckedParams(lowerer, listenerNode, narrowed) + : lowerer.lowerExpr(call.arguments.nodes[1]!); if ( + narrowed.length > 0 || (isJsSourceFile(listenerNode.getSourceFile()) && cb.type.kind === "dyn") || (cb.type.kind === "func" && (cb.type.rest || @@ -301,10 +310,12 @@ export function lowerChildMethodCall( /** Method calls on piped child-output stream receivers (child.stdout / * child.stderr — the childStream kind): on/once("data" | "end"). - * 'data' listeners take zero parameters, `(chunk: Buffer)`, or a - * `Buffer | string`-union chunk (the ngrok appendOutput shape — the - * runtime only ever fires Buffers; the compiler-emitted adapter wraps - * the chunk at the union's Buffer arm); 'end' listeners take none. + * 'data' listeners take zero parameters, `(chunk: Buffer)`, `(chunk: + * string)`, an unknown/any chunk (boxed as a checked-dynamic value; see + * childStreamEncodedAt), or a `Buffer | string`-union chunk (the ngrok + * appendOutput shape — the runtime only ever fires Buffers; the + * compiler-emitted adapter wraps the chunk at the union's Buffer arm); + * 'end' listeners take none. * Statement position only (Node returns the stream for chaining; here * the result is void). Chained receivers (`child.stdout?.on(...)`) * ride the optional-chain re-dispatch (chainBlocked). Everything else @@ -335,6 +346,7 @@ export function lowerChildStreamMethodCall( const evT = lowerer.typeOf(call.arguments.nodes[0]!); const event = evT.isStringLiteralType() ? evT.value : null; if (event !== "data" && event !== "end") { + requestGeneralStreamSurface(lowerer, access.expression); lowerer.noLowering( `stream.${name}(${event === null ? "non-literal event" : `"${event}"`}, ...)`, call.arguments.nodes[0]!, @@ -342,6 +354,7 @@ export function lowerChildStreamMethodCall( ); } if (!ts.isExpressionStatement(call.parent)) { + requestGeneralStreamSurface(lowerer, access.expression); lowerer.unsupported( "SC1090", call, @@ -376,23 +389,32 @@ export function lowerChildStreamMethodCall( if ( param !== undefined && param.kind !== "string" && + param.kind !== "dyn" && !(param.kind === "bytes" && param.elem === "u8") && !unionOk(param) ) { lowerer.unsupported( "SC1090", call.arguments.nodes[1]!, - `data listeners whose parameter is not 'Buffer', 'string', or a Buffer-armed union (got '${lowerer.fmt(param)}')`, + `data listeners whose parameter is not 'Buffer', 'string', 'unknown', or a Buffer-armed union (got '${lowerer.fmt(param)}')`, ); } + // An unknown/any chunk is a Buffer, or a string once setEncoding ran. + // The registration takes whichever the enclosing code establishes; a + // later setEncoding that contradicts it throws the runtime's + // listener-type TypeError instead of delivering the other kind. + const strings = + param?.kind === "string" || + (param?.kind === "dyn" && childStreamEncodedAt(lowerer, access.expression, call)); return { kind: "libCall", - fn: param?.kind === "string" ? "stream.onDataStr" : "stream.onData", + fn: strings ? "stream.onDataStr" : "stream.onData", args: [receiver, cb, once], type: VOID, loc, }; } + requestGeneralStreamSurface(lowerer, access.expression); lowerer.noLowering( `ReadableStream.${name}`, call, @@ -418,6 +440,7 @@ export function lowerChildWriterMethodCall( if (name === "write") { if (call.arguments.nodes.length !== 1 || call.arguments.nodes.some(ts.isSpreadElement)) { + requestGeneralStreamSurface(lowerer, access.expression); lowerer.noLowering( `child stdin write with ${call.arguments.nodes.length} arguments`, call, @@ -461,6 +484,7 @@ export function lowerChildWriterMethodCall( const eventType = lowerer.typeOf(call.arguments.nodes[0]!); const event = eventType.isStringLiteralType() ? eventType.value : null; if (event !== "drain" && event !== "finish" && event !== "error") { + requestGeneralStreamSurface(lowerer, access.expression); lowerer.noLowering( `child stdin ${name}(${event === null ? "non-literal event" : `"${event}"`}, ...)`, call.arguments.nodes[0]!, @@ -507,6 +531,7 @@ export function lowerChildWriterMethodCall( return { kind: "libCall", fn, args: [receiver, cb, once], type: VOID, loc }; } + requestGeneralStreamSurface(lowerer, access.expression); lowerer.noLowering( `Writable.${name}`, call, @@ -514,3 +539,255 @@ export function lowerChildWriterMethodCall( lowerer.checker.getSymbolAtLocation(access.name), ); } + +/** A receiver spelled as a plain access path (`child.stdout`, `out`): its + * root symbol and member names, or null for other forms. A `const` alias + * of an access path (`const out = child.stdout`) resolves to that path, + * and so does a method call declared to return `this` + * (`child.stdout.setEncoding("utf8")`), so every spelling of one stream + * gets the same key. */ +function accessPathOf( + lowerer: Lowerer, + expr: ts.Expression, + depth = 0, +): { root: ts.Symbol; path: string } | null { + if (depth > 16) return null; + if (ts.isParenthesizedExpression(expr) || ts.isNonNullExpression(expr)) { + return accessPathOf(lowerer, expr.expression, depth + 1); + } + if (ts.isIdentifier(expr)) { + const root = lowerer.checker.getSymbolAtLocation(expr); + if (!root) return null; + const init = constInitializerOf(root); + const aliased = init === null ? null : accessPathOf(lowerer, init, depth + 1); + return aliased ?? { root, path: "" }; + } + if (ts.isPropertyAccessExpression(expr) && !expr.questionDotToken) { + const base = accessPathOf(lowerer, expr.expression, depth + 1); + return base === null ? null : { root: base.root, path: `${base.path}.${expr.name.text}` }; + } + if (returnsReceiver(lowerer, expr)) { + return accessPathOf( + lowerer, + (expr.expression as ts.PropertyAccessExpression).expression, + depth + 1, + ); + } + return null; +} + +/** A method call (`recv.m(...)`, not optional) whose declaration returns + * `this`, so its value is the receiver itself. */ +function returnsReceiver(lowerer: Lowerer, expr: ts.Expression): expr is ts.CallExpression { + if ( + !ts.isCallExpression(expr) || + expr.questionDotToken || + !ts.isPropertyAccessExpression(expr.expression) || + expr.expression.questionDotToken + ) + return false; + const declaration = lowerer.checker.getResolvedSignature(expr)?.declaration; + return declaration?.type?.kind === ts.SyntaxKind.ThisType; +} + +/** A call of a child_process factory (spawn, fork, exec, execFile), each + * of which returns a new ChildProcess. */ +function createsChildProcess(lowerer: Lowerer, call: ts.CallExpression): boolean { + const declaration = lowerer.checker.getResolvedSignature(call)?.declaration; + if ( + declaration === undefined || + !ts.isFunctionDeclaration(declaration) || + declaration.name === undefined || + !["spawn", "fork", "exec", "execFile"].includes(declaration.name.text) + ) + return false; + for (let node: ts.Node | undefined = declaration.parent; node; node = node.parent) { + if (ts.isModuleDeclaration(node) && ts.isStringLiteral(node.name)) { + return node.name.text === "child_process" || node.name.text === "node:child_process"; + } + } + return false; +} + +/** The statement that always evaluates `call` when it runs: the call is + * the statement's expression, a variable's initializer, or the receiver of + * a method chain that is one of those. Null for calls under a condition, + * an operator, an argument list or any other construct. */ +function unconditionalStatementOf(call: ts.CallExpression): ts.Statement | null { + let node: ts.Node = call; + for (;;) { + const parent: ts.Node | undefined = node.parent; + if (parent === undefined) return null; + if ( + ts.isParenthesizedExpression(parent) || + ts.isNonNullExpression(parent) || + (ts.isPropertyAccessExpression(parent) && + parent.expression === node && + !parent.questionDotToken) || + (ts.isCallExpression(parent) && parent.expression === node && !parent.questionDotToken) + ) { + node = parent; + continue; + } + if (ts.isExpressionStatement(parent)) return parent; + if (ts.isVariableDeclaration(parent) && parent.initializer === node) { + const list = parent.parent; + const statement = list?.parent; + return list !== undefined && + ts.isVariableDeclarationList(list) && + statement !== undefined && + ts.isVariableStatement(statement) + ? statement + : null; + } + return null; + } +} + +/** The initializer of a symbol declared once as a `const` variable, else null. */ +function constInitializerOf(symbol: ts.Symbol): ts.Expression | null { + const decls = symbol.declarations; + if (decls.length !== 1) return null; + const decl = decls[0]!; + if ( + !ts.isVariableDeclaration(decl) || + !ts.isIdentifier(decl.name) || + decl.initializer === undefined || + (ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) === 0 + ) + return null; + return decl.initializer; +} + +/** Whether two resolved access paths name different objects: the same root + * with different members, or two `const` roots each initialized by `new` + * or a child_process factory (such as two `spawn()` results). Any other + * call may return an object another binding already holds. */ +function distinctPaths( + lowerer: Lowerer, + a: { root: ts.Symbol; path: string }, + b: { root: ts.Symbol; path: string }, +): boolean { + if (a.root === b.root) return a.path !== b.path; + const fresh = (root: ts.Symbol): boolean => { + let init = constInitializerOf(root); + while (init !== null && ts.isParenthesizedExpression(init)) init = init.expression; + if (init === null) return false; + return ( + ts.isNewExpression(init) || (ts.isCallExpression(init) && createsChildProcess(lowerer, init)) + ); + }; + return fresh(a.root) && fresh(b.root); +} + +/** Whether an unknown-typed 'data' listener registered by `call` receives + * strings: a `setEncoding` statement on the same receiver path precedes + * the registration in its own block or an enclosing block of the same + * function. With no setEncoding on that path in the function, chunks are + * Buffers. Any other placement leaves the encoding state unclear, which + * is reported. */ +function childStreamEncodedAt( + lowerer: Lowerer, + receiver: ts.Expression, + call: ts.CallExpression, +): boolean { + const key = accessPathOf(lowerer, receiver); + const statement: ts.Node = call.parent!; + let scope: ts.Node | undefined = statement; + while (scope && !ts.isFunctionLike(scope) && !ts.isSourceFile(scope)) scope = scope.parent; + const unclear = (): never => + lowerer.unsupported( + "SC1090", + call.arguments.nodes[1]!, + "untyped child-stream 'data' listeners whose setEncoding state is unclear at their registration sites (declare the chunk as 'Buffer' or 'string')", + ); + if (key === null || scope === undefined) return unclear(); + const preceding = new Set(); + for (let node: ts.Node = statement; node !== scope;) { + const parent: ts.Node | undefined = node.parent; + if (parent === undefined) break; + if (ts.isBlock(parent) || ts.isSourceFile(parent)) { + for (const sibling of parent.statements.nodes) { + if (sibling === node) break; + preceding.add(sibling); + } + } + node = parent; + } + const receiverType = lowerer.checker.getNonNullableType(lowerer.typeOf(receiver)); + let before = false; + let elsewhere = false; + const visit = (node: ts.Node): void => { + if (node !== scope && ts.isFunctionLike(node)) return; + if ( + ts.isCallExpression(node) && + ts.isPropertyAccessExpression(node.expression) && + node.expression.name.text === "setEncoding" + ) { + const target = node.expression.expression; + const path = accessPathOf(lowerer, target); + if (path !== null && path.root === key.root && path.path === key.path) { + const statement = unconditionalStatementOf(node); + if (statement !== null && preceding.has(statement)) before = true; + else elsewhere = true; + } else if ( + (path === null || !distinctPaths(lowerer, path, key)) && + lowerer.checker.isTypeAssignableTo(receiverType, lowerer.typeOf(target)) + ) { + // Another spelling that can hold this stream (a parameter, a `let` + // copy, a call result): its setEncoding may be this stream's. + elsewhere = true; + } + } + ts.forEachChild(node, visit); + }; + visit(scope); + if (elsewhere) return unclear(); + return before; +} + +/** An inline 'exit'/'close' listener may declare `(code: number, signal: + * string)`: TypeScript accepts it through ChildProcess's catch-all `on` + * overload, while Node passes `number | null` and `Signals | null`. Those + * parameters bind as checked-dynamic values (the socket 'data' listener + * precedent), so the body observes the null Node passes; a typed use of a + * null value throws the checked conversion's TypeError. Returns the + * narrowed parameter symbols, empty when the declared types already + * admit null. */ +function narrowedExitListenerParams(lowerer: Lowerer, listener: ts.Expression): ts.Symbol[] { + if (!ts.isArrowFunction(listener) && !ts.isFunctionExpression(listener)) return []; + if (isJsSourceFile(listener.getSourceFile())) return []; + const symbols: ts.Symbol[] = []; + const wanted: readonly string[] = ["f64", "string"]; + listener.parameters.nodes.slice(0, 2).forEach((param, i) => { + if (!ts.isIdentifier(param.name) || param.dotDotDotToken || param.initializer) return; + if (param.questionToken) return; + const symbol = lowerer.checker.getSymbolAtLocation(param.name); + if (!symbol || lowerer.runtimeOptionalBindingTypes.has(symbol)) return; + if (lowerer.paramShape(param).type.kind !== wanted[i]) return; + symbols.push(symbol); + }); + return symbols; +} + +/** Lowers an inline listener with the given parameters bound as + * checked-dynamic values typed `unknown` for the body. */ +function lowerWithCheckedParams( + lowerer: Lowerer, + listener: ts.Expression, + symbols: readonly ts.Symbol[], +): IrExpr { + const previousImplicit = lowerer.implicitParamTypes; + const implicit = new Map(previousImplicit ?? []); + for (const symbol of symbols) { + implicit.set(symbol, lowerer.checker.getUnknownType()); + lowerer.runtimeOptionalBindingTypes.set(symbol, DYN); + } + lowerer.implicitParamTypes = implicit; + try { + return lowerer.lowerExpr(listener); + } finally { + for (const symbol of symbols) lowerer.runtimeOptionalBindingTypes.delete(symbol); + lowerer.implicitParamTypes = previousImplicit; + } +} diff --git a/packages/compiler/src/frontend/lowering/builtins/performance.ts b/packages/compiler/src/frontend/lowering/builtins/performance.ts index 2a9962b0f..7aac3b80c 100644 --- a/packages/compiler/src/frontend/lowering/builtins/performance.ts +++ b/packages/compiler/src/frontend/lowering/builtins/performance.ts @@ -18,6 +18,7 @@ import { } from "../../../ir/ir.js"; import { PERF_ENTRY_FIELDS, PERF_ENTRY_MARKER, perfEntryJsonShape } from "../../type-mapper.js"; import { builtinImportOf } from "./module-bindings.js"; +import { lowerOptionalArgument } from "../optional-arguments.js"; import { checkedIterableSpread } from "../checked-iterable-spread.js"; /** True for any spelling of node:perf_hooks' `performance` object — the @@ -134,6 +135,12 @@ export function lowerPerfHooksCall( loc, }; } + if ( + access.name.text === "eventLoopUtilization" && + isPerfHooksPerformanceExpr(lowerer, access.expression) + ) { + return lowerEventLoopUtilization(lowerer, expr); + } const timeline = PERF_TIMELINE_CALLS.get(access.name.text); if (timeline && isPerfHooksPerformanceExpr(lowerer, access.expression)) { return lowerPerfTimelineCall(lowerer, expr, access.name.text, timeline, loc); @@ -142,7 +149,7 @@ export function lowerPerfHooksCall( lowerer.noLowering( `perf_hooks performance.${access.name.text}`, expr, - "performance.now() (and its .bind(performance) function value), mark, measure, the getEntries family, clearMarks and clearMeasures are the lowered surface", + "performance.now() (and its .bind(performance) function value), mark, measure, the getEntries family, clearMarks, clearMeasures and eventLoopUtilization are the lowered surface", ); } return null; @@ -513,3 +520,305 @@ export function fencePerfEntryKeyWalk( `${what} over performance entries (Node keeps their fields as prototype getters, so own-key walks see none; read the fields or call toJSON())`, ); } + +/* ── event-loop metrics: eventLoopUtilization and monitorEventLoopDelay ── */ + +/** The stdlib IntervalHistogram (monitorEventLoopDelay's result), by the + * receiver's checker type. Its value is the runtime histogram handle. */ +function isIntervalHistogram(lowerer: Lowerer, node: ts.Expression): boolean { + const symbol = lowerer.typeOf(node).getSymbol(); + return symbol?.name === "IntervalHistogram" && lowerer.isStdlibSymbol(symbol); +} + +/** IntervalHistogram's numeric members, as perf.eldStat selectors. */ +const HISTOGRAM_STATS: ReadonlyMap = new Map([ + ["min", 0], + ["max", 1], + ["mean", 2], + ["stddev", 3], + ["count", 4], + ["exceeds", 5], +]); + +/** `h.min`, `h.max`, … on an IntervalHistogram: the runtime's current + * statistic (Node's hdr_histogram values; nanoseconds). Null for every + * other read. */ +export function lowerHistogramProperty( + lowerer: Lowerer, + expr: ts.PropertyAccessExpression, +): IrExpr | null { + const which = HISTOGRAM_STATS.get(expr.name.text); + if ( + which === undefined || + expr.questionDotToken || + !isIntervalHistogram(lowerer, expr.expression) + ) + return null; + const loc = locOf(expr); + return { + kind: "libCall", + fn: "perf.eldStat", + args: [lowerer.lowerExprExpecting(expr.expression, F64), numLit(which, loc)], + type: F64, + loc, + }; +} + +/** enable(), disable(), reset() and percentile(p) on an IntervalHistogram. + * Other members (the BigInt twins, the percentiles Map) fence by name. */ +export function lowerHistogramMethodCall( + lowerer: Lowerer, + expr: ts.CallExpression, + access: ts.PropertyAccessExpression, +): IrExpr | null { + if (access.questionDotToken || !isIntervalHistogram(lowerer, access.expression)) return null; + const loc = locOf(expr); + const name = access.name.text; + const handle = (): IrExpr => lowerer.lowerExprExpecting(access.expression, F64); + const args = expr.arguments.nodes; + if ((name === "enable" || name === "disable" || name === "reset") && args.length === 0) { + const fn = + name === "enable" + ? "perf.eldEnable" + : name === "disable" + ? "perf.eldDisable" + : "perf.eldReset"; + return { kind: "libCall", fn, args: [handle()], type: name === "reset" ? VOID : BOOL, loc }; + } + if (name === "percentile" && args.length === 1 && !ts.isSpreadElement(args[0]!)) { + // Node validates first: NaN, p <= 0 and p > 100 throw ERR_OUT_OF_RANGE. + const h = lowerer.declareHiddenLocal("%histogram", F64); + const p = lowerer.declareHiddenLocal("%percentile", F64); + const pRef = (): IrExpr => varRef(p.id, F64, loc); + const cmp = (op: "!==" | "<=" | ">", left: IrExpr, right: IrExpr): IrExpr => ({ + kind: "bin", + op, + left, + right, + type: BOOL, + loc, + }); + const or = (left: IrExpr, right: IrExpr): IrExpr => ({ + kind: "logical", + op: "||", + left, + right, + type: BOOL, + loc, + }); + const outOfRange = or( + or(cmp("!==", pRef(), pRef()), cmp("<=", pRef(), numLit(0, loc))), + cmp(">", pRef(), numLit(100, loc)), + ); + const message: IrExpr = { + kind: "strConcat", + left: { + kind: "strLit", + value: 'The value of "percentile" is out of range. It must be > 0 && <= 100. Received ', + type: STRING, + loc, + }, + right: { kind: "toString", operand: pRef(), type: STRING, loc }, + type: STRING, + loc, + }; + const thrown: IrExpr = { + kind: "libCall", + fn: "error.nodeThrow", + args: [ + numLit(2, loc), + { kind: "strLit", value: "ERR_OUT_OF_RANGE", type: STRING, loc }, + message, + ], + type: F64, + loc, + }; + return { + kind: "seqExpr", + stmts: [ + { kind: "varDecl", localId: h.id, init: handle(), loc }, + { kind: "varDecl", localId: p.id, init: lowerer.lowerExprExpecting(args[0]!, F64), loc }, + ], + result: { + kind: "ternary", + cond: outOfRange, + then: thrown, + else_: { + kind: "libCall", + fn: "perf.eldPercentile", + args: [varRef(h.id, F64, loc), pRef()], + type: F64, + loc, + }, + type: F64, + loc, + }, + type: F64, + loc, + }; + } + lowerer.noLowering( + `IntervalHistogram.${name}`, + expr, + "enable(), disable(), reset(), percentile(p) and the min, max, mean, stddev, count and exceeds readings are the lowered surface", + ); +} + +/** `monitorEventLoopDelay(options?)` from node:perf_hooks, in any import + * spelling (`lowerBuiltinCall`): a new sampling histogram handle. The + * options are an inline object literal; the runtime validates the + * resolution as Node does (an integer of at least 1). */ +export function lowerMonitorEventLoopDelayCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { + const loc = locOf(expr); + const args = expr.arguments.nodes; + let resolution: IrExpr = numLit(10, loc); + if (args.length > 1 || args.some(ts.isSpreadElement)) { + lowerer.noLowering( + "this monitorEventLoopDelay call form", + expr, + "pass at most one options object", + ); + } + if (args.length === 1) { + const options = args[0]!; + if (!ts.isObjectLiteralExpression(options)) { + lowerer.noLowering( + "monitorEventLoopDelay with a non-literal options object", + options, + "spell the options inline: monitorEventLoopDelay({ resolution: 20 })", + ); + } + for (const property of options.properties.nodes) { + const shorthand = ts.isShorthandPropertyAssignment(property); + if ( + (!shorthand && !ts.isPropertyAssignment(property)) || + !ts.isIdentifier(property.name) || + property.name.text !== "resolution" + ) { + lowerer.noLowering( + "monitorEventLoopDelay options other than resolution", + property, + "resolution is the one option", + ); + } + // An undefined resolution takes the default, as Node's destructuring. + resolution = lowerOptionalArgument( + lowerer, + shorthand ? property.name : property.initializer, + F64, + numLit(10, loc), + ); + } + } + return { kind: "libCall", fn: "perf.eldNew", args: [resolution], type: F64, loc }; +} + +/** performance.eventLoopUtilization(u1?, u2?) — Node's + * lib/internal/perf/event_loop_utilization.js over the runtime's loop + * start and idle time, one lifted helper per arity. */ +function lowerEventLoopUtilization(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { + const loc = locOf(expr); + const args = expr.arguments.nodes; + const resultT = lowerer.mapTypeOf(lowerer.typeOf(expr)); + if (resultT?.kind !== "record" || args.length > 2 || args.some(ts.isSpreadElement)) { + lowerer.noLowering("this performance.eventLoopUtilization call form", expr); + } + const helper = eluHelperOf(lowerer, args.length, resultT); + return { + kind: "call", + callee: helper, + args: args.map((arg) => lowerer.lowerExprExpecting(arg, resultT)), + type: resultT, + loc, + }; +} + +function eluHelperOf(lowerer: Lowerer, arity: number, recT: IrType & { kind: "record" }): string { + const name = `%perf.elu.${arity}`; + if (lowerer.liftedFns.some((f) => f.name === name)) return name; + const loc: SrcLoc = { file: "", start: 0, end: 0 }; + const f64Local = (id: string) => ({ id, name: id, type: F64, mutable: false }); + const params = Array.from({ length: arity }, (_, i) => ({ + localId: `u${i + 1}.0`, + name: `u${i + 1}`, + type: recT as IrType, + })); + const locals = [ + ...params.map((p) => ({ id: p.localId, name: p.name, type: recT as IrType, mutable: false })), + f64Local("ls.0"), + f64Local("idle.0"), + f64Local("active.0"), + ]; + const ref = (id: string): IrExpr => varRef(id, F64, loc); + const field = (param: string, f: string): IrExpr => ({ + kind: "recordGet", + obj: varRef(param, recT, loc), + shapeId: recT.shapeId, + field: f, + type: F64, + loc, + }); + const bin = (op: "+" | "-" | "/" | "<=", left: IrExpr, right: IrExpr): IrExpr => ({ + kind: "bin", + op, + left, + right, + type: op === "<=" ? BOOL : F64, + loc, + }); + const record = (idle: IrExpr, active: IrExpr, utilization: IrExpr): IrExpr => ({ + kind: "recordLit", + fields: [ + { name: "idle", value: idle }, + { name: "active", value: active }, + { name: "utilization", value: utilization }, + ], + type: recT, + loc, + }); + const result = (idle: IrExpr, active: IrExpr): IrStmt => ({ + kind: "return", + value: record(idle, active, bin("/", active, bin("+", idle, active))), + loc, + }); + const decl = (id: string, init: IrExpr): IrStmt => ({ kind: "varDecl", localId: id, init, loc }); + const body: IrStmt[] = [ + decl("ls.0", { kind: "libCall", fn: "perf.loopStart", args: [], type: F64, loc }), + { + kind: "if", + cond: bin("<=", ref("ls.0"), numLit(0, loc)), + then: [ + { kind: "return", value: record(numLit(0, loc), numLit(0, loc), numLit(0, loc)), loc }, + ], + else_: null, + loc, + }, + ]; + if (arity === 2) { + body.push( + decl("idle.0", bin("-", field("u1.0", "idle"), field("u2.0", "idle"))), + decl("active.0", bin("-", field("u1.0", "active"), field("u2.0", "active"))), + result(ref("idle.0"), ref("active.0")), + ); + } else { + body.push( + decl("idle.0", { kind: "libCall", fn: "perf.idleTime", args: [], type: F64, loc }), + decl( + "active.0", + bin( + "-", + bin("-", { kind: "libCall", fn: "perf.now", args: [], type: F64, loc }, ref("ls.0")), + ref("idle.0"), + ), + ), + arity === 0 + ? result(ref("idle.0"), ref("active.0")) + : result( + bin("-", ref("idle.0"), field("u1.0", "idle")), + bin("-", ref("active.0"), field("u1.0", "active")), + ), + ); + } + lowerer.liftedFns.push({ name, params, returnType: recT, locals, body, loc }); + return name; +} diff --git a/packages/compiler/src/frontend/lowering/builtins/process.ts b/packages/compiler/src/frontend/lowering/builtins/process.ts index eb3703a53..9ac1fc059 100644 --- a/packages/compiler/src/frontend/lowering/builtins/process.ts +++ b/packages/compiler/src/frontend/lowering/builtins/process.ts @@ -3,6 +3,7 @@ import { envSnapshotHelper, lowerProcessLoadEnvFile } from "./environment.js"; import { dynUndefinedExpr, boolLit, numLit, strLit, varRef } from "../../../ir/build.js"; import { timerStyleCallback } from "../lower-timers.js"; import * as ts from "../../ts7/adapter.js"; +import { requestGeneralStreamSurface } from "../general-streams.js"; import { type Lowerer, own } from "../lowerer.js"; import { isJsSourceFile, locOf } from "../../program.js"; import { isSafeToDiscard } from "../expressions/evaluation-safety.js"; @@ -225,6 +226,7 @@ export function lowerProcStreamMethodCall( const receiver = lowerer.lowerExpr(access.expression); const data = lowerer.lowerExpr(call.arguments.nodes[0]!); if (data.type.kind !== "string") { + requestGeneralStreamSurface(lowerer, access.expression); lowerer.noLowering( `write of '${lowerer.fmt(data.type)}' data on a stream value`, call.arguments.nodes[0]!, @@ -233,6 +235,7 @@ export function lowerProcStreamMethodCall( } return { kind: "libCall", fn: "procStream.write", args: [receiver, data], type: BOOL, loc }; } + requestGeneralStreamSurface(lowerer, access.expression); lowerer.noLowering( `WritableStream.${name}`, call, diff --git a/packages/compiler/src/frontend/lowering/builtins/properties.ts b/packages/compiler/src/frontend/lowering/builtins/properties.ts index b0caa0456..e60a14c0c 100644 --- a/packages/compiler/src/frontend/lowering/builtins/properties.ts +++ b/packages/compiler/src/frontend/lowering/builtins/properties.ts @@ -1,5 +1,5 @@ import { isBuiltinClassInstance } from "./receiver-types.js"; -import { strLit } from "../../../ir/build.js"; +import { numLit, strLit } from "../../../ir/build.js"; import * as ts from "../../ts7/adapter.js"; import { type Lowerer } from "../lowerer.js"; import { locOf } from "../../program.js"; @@ -192,6 +192,25 @@ export function lowerBuiltinExtraProperty( // +1-or-NULL runtime pair. if (kind === "child" && (name === "stdin" || name === "stdout" || name === "stderr")) { const receiver = lowerer.lowerExpr(expr.expression); + // Under the general stream representation (general-streams.ts) the + // declared stream type maps to a runtime stream class: the read + // answers the child's bridged stream (one per stdio slot, so repeated + // reads keep identity), fed by the same pipe. + const general = lowerer.generalNodeStreams ? generalChildStdioType(lowerer, expr, name) : null; + if (general) { + const type: IrType = { + kind: "union", + unionId: lowerer.unions.intern([general, { kind: "nullT" }]), + }; + const read: IrExpr = { + kind: "libCall", + fn: "child.stdioStream", + args: [receiver, numLit(name === "stdin" ? 0 : name === "stdout" ? 1 : 2, loc)], + type, + loc, + }; + return lowerer.maybeNarrow(read, expr); + } const streamType = name === "stdin" ? CHILDWRITER_T : CHILDSTREAM_T; const type: IrType = { kind: "union", @@ -259,3 +278,21 @@ export function lowerBuiltinExtraProperty( } return null; } + +/** The runtime stream class a child stdio read produces under the general + * stream representation, or null when the declared type keeps the stdio + * handle (the fallback declarations' ChildStdin). */ +function generalChildStdioType( + lowerer: Lowerer, + expr: ts.PropertyAccessExpression, + name: "stdin" | "stdout" | "stderr", +): IrType | null { + const className = name === "stdin" ? "%Writable" : "%Readable"; + const mapped = lowerer.mapTypeOf(lowerer.typeOf(expr)); + if (!mapped) return null; + const arms = + mapped.kind === "union" ? (lowerer.unions.get(mapped.unionId)?.arms ?? []) : [mapped]; + return arms.some((arm) => arm.kind === "object" && arm.className === className) + ? { kind: "object", className } + : null; +} diff --git a/packages/compiler/src/frontend/lowering/call-arguments.ts b/packages/compiler/src/frontend/lowering/call-arguments.ts index f5600b033..db7691a1c 100644 --- a/packages/compiler/src/frontend/lowering/call-arguments.ts +++ b/packages/compiler/src/frontend/lowering/call-arguments.ts @@ -1,3 +1,4 @@ +import { forwardedTupleType, pureForwardedValues, stripTypeWrappers } from "./typed-arguments.js"; import { dynUndefinedExpr } from "../../ir/build.js"; import { InternalCompilerError } from "../../errors.js"; import * as ts from "../ts7/adapter.js"; @@ -26,13 +27,13 @@ import { checkedIterableSpread } from "./checked-iterable-spread.js"; import { type ParamShape } from "./call-signatures.js"; import { arrayValueRead, arrayValueType } from "./array-values.js"; import { buildArrayConversion } from "./coercions/builders.js"; -import { typeKey } from "../type-mapper.js"; +import { restTupleElementTypes, typeKey } from "../type-mapper.js"; function fixedTupleSpreadInfo( lowerer: Lowerer, node: ts.Expression, ): { type: IrType & { kind: "record" }; fields: { name: string; type: IrType }[] } | null { - const type = lowerer.mapTypeOf(lowerer.typeOf(node)); + const type = forwardedTupleType(lowerer, node) ?? lowerer.mapTypeOf(lowerer.typeOf(node)); if (type?.kind !== "record") return null; const shape = lowerer.shapes.get(type.shapeId); if (!shape?.tuple || shape.fields.length === 0) return null; @@ -42,6 +43,18 @@ function fixedTupleSpreadInfo( }; } +/** `...t` over a provably EMPTY tuple bound to a plain name (`[]`, a rest + * tuple whose type parameter or `Parameters` resolved to no positions): + * it contributes no arguments, and reading the name has no effect. */ +function emptyTupleSpread(lowerer: Lowerer, node: ts.Expression): boolean { + let operand = node; + while (ts.isParenthesizedExpression(operand)) operand = operand.expression; + if (!ts.isIdentifier(operand)) return false; + const forwarded = forwardedTupleType(lowerer, operand); + if (forwarded) return forwarded.kind === "array"; + return restTupleElementTypes(lowerer.typeOf(operand), lowerer.typeCtx)?.length === 0; +} + export function completeArgs( lowerer: Lowerer, argNodes: readonly ts.Expression[], @@ -59,7 +72,9 @@ export function completeArgs( // graph. Ordinary calls and fixed tuples never need that analysis. const runtimeSpreads = argNodes.filter( (arg): arg is ts.SpreadElement => - ts.isSpreadElement(arg) && !fixedTupleSpreadInfo(lowerer, arg.expression), + ts.isSpreadElement(arg) && + !fixedTupleSpreadInfo(lowerer, arg.expression) && + !emptyTupleSpread(lowerer, arg.expression), ); // Spread operands lowered while choosing the completion, each exactly once. const loweredSpreads = new Map(); @@ -190,6 +205,12 @@ export function completeArgs( sources.push(arg); continue; } + if (emptyTupleSpread(lowerer, arg.expression)) continue; + const pure = pureForwardedValues(lowerer, arg.expression); + if (pure) { + for (const value of pure) sources.push({ ir: value }); + continue; + } const tuple = fixedTupleSpreadInfo(lowerer, arg.expression); if (!tuple) { sources.push(arg); @@ -204,7 +225,13 @@ export function completeArgs( // values as separately written arguments while the saved tuple stays // alive through the call. const spreadLoc = locOf(arg); - const value = lowerer.coerceInto(arg.expression, lowerer.lowerExpr(arg.expression), tuple.type); + // A forwarded tuple seen through type assertions (`...(arguments as + // unknown as Parameters)`) spreads the tuple itself. + const operand = + forwardedTupleType(lowerer, arg.expression) !== null + ? stripTypeWrappers(arg.expression) + : arg.expression; + const value = lowerer.coerceInto(arg.expression, lowerer.lowerExpr(operand), tuple.type); const saved = lowerer.declareHiddenLocal("%callSpread", tuple.type); const savedRef = (): IrExpr => ({ kind: "varRef", @@ -506,9 +533,38 @@ export function completeFuncValueArgs( shapes.push({ type: rest, mode: "rest" }); return completeArgs(lowerer, call.arguments.nodes, shapes, loc, call); } + // A fixed-tuple spread (`fn(...args)` over a tuple-typed rest binding) + // has a compile-time arity: completion flattens it into the positional + // slots exactly like a direct call's. + if ( + funcType.rest !== true && + call.arguments.nodes.some(ts.isSpreadElement) && + call.arguments.nodes.every( + (arg) => + !ts.isSpreadElement(arg) || + fixedTupleSpreadInfo(lowerer, arg.expression) !== null || + emptyTupleSpread(lowerer, arg.expression), + ) + ) { + const shapes: ParamShape[] = funcType.params.map((type) => ({ + type, + mode: omittedArgFor(lowerer, type, loc) ? "omittable" : "required", + })); + return completeArgs(lowerer, call.arguments.nodes, shapes, loc, call); + } const args = call.arguments.nodes.map((arg, index) => lowerer.lowerExprExpecting(arg, funcType.params[index]), ); + // Effect-free surplus arguments of a fixed-arity value (`(r: void) => + // void` drops its void slot) evaluate to nothing observable: drop them, + // as the callee ignores them. + while ( + funcType.rest !== true && + args.length > funcType.params.length && + isSafeToDiscard(args[args.length - 1]!) + ) { + args.pop(); + } for (let i = args.length; i < funcType.params.length; i++) { const absent = omittedArgFor(lowerer, funcType.params[i]!, loc); if (!absent) { diff --git a/packages/compiler/src/frontend/lowering/call-signatures.ts b/packages/compiler/src/frontend/lowering/call-signatures.ts index 7570fff88..954122a16 100644 --- a/packages/compiler/src/frontend/lowering/call-signatures.ts +++ b/packages/compiler/src/frontend/lowering/call-signatures.ts @@ -1,3 +1,8 @@ +import { + typedArgumentsCandidate, + typedArgumentsShapes, + typedArgumentsTarget, +} from "./typed-arguments.js"; import { InternalCompilerError } from "../../errors.js"; import * as ts from "../ts7/adapter.js"; import { bodyReadsArguments } from "../arguments-usage.js"; @@ -15,6 +20,7 @@ import { import { isJsSourceFile, isNodeEsmFile } from "../program.js"; import { genResultRecord, + restTupleParamTypes, isAnyArrayType, isUnitOnlyTsType, jsOpenObjectType, @@ -56,6 +62,10 @@ export interface ParamShape { bodyType?: IrType; callDefault?: IrExpr; entryNarrow?: IrType; + /** One flattened position of a fixed-tuple rest parameter + * (restTupleShapes): the declaration's last parameter owns every shape + * from its index on, and the body binds the tuple (declareParams). */ + restTuple?: true; } export interface FnSig { @@ -373,6 +383,28 @@ export function isThisParameter(param: ts.ParameterDeclaration): boolean { return ts.isIdentifier(param.name) && param.name.text === "this"; } +/** A TypeScript rest parameter bound to a name whose type is a fixed tuple + * (`...args: [A, B]`, `...args: A` with A bound to a tuple, `...args: + * Parameters` with F bound): one required shape per position. Call + * sites and function values see the equivalent fixed signature; the body + * rebuilds the tuple (declareParams) unless it only forwards it. Null for + * every other parameter. */ +export function restTupleShapes( + lowerer: Lowerer, + param: ts.ParameterDeclaration, + tsType?: ts.Type, +): ParamShape[] | null { + if ( + param.dotDotDotToken === undefined || + !ts.isIdentifier(param.name) || + isJsSourceFile(param.getSourceFile()) + ) { + return null; + } + const elems = restTupleParamTypes(tsType ?? lowerer.typeOf(param.name), lowerer.typeCtx); + return elems ? elems.map((type) => ({ type, mode: "required", restTuple: true })) : null; +} + /** ParamShapes for a whole parameter list. */ export function paramShapes( lowerer: Lowerer, @@ -380,21 +412,47 @@ export function paramShapes( signature?: ts.Signature, blameOf?: (param: ts.ParameterDeclaration, index: number) => ts.Node, ): ParamShape[] { - if (!signature) - return params.filter((p) => !isThisParameter(p)).map((param) => lowerer.paramShape(param)); + const out: ParamShape[] = []; + if (!signature) { + for (const param of params) { + if (isThisParameter(param)) continue; + const flattened = restTupleShapes(lowerer, param); + if (flattened) out.push(...flattened); + else out.push(lowerer.paramShape(param)); + } + return out; + } const symbols = signature.getParameters(); - return params.map((declParam, i) => { + const shapeAt = (declParam: ts.ParameterDeclaration, i: number): ParamShape => { const stage = lowerer.stageSourceType(declParam.name); if (stage) return { type: stage, mode: "required" as const }; const symbol = symbols[i]; const tsType = symbol ? lowerer.checker.getTypeOfSymbol(symbol) : lowerer.typeOf(declParam.name); + // A call-site instantiation whose concrete argument type does not map + // is that argument's blocker: name it there, with its concrete type, + // rather than as the declaration's unbound type parameter. + if ( + blameOf !== undefined && + !isJsSourceFile(declParam.getSourceFile()) && + (tsType.flags & ts.TypeFlags.Any) === 0 && + declParam.type !== undefined && + mentionsTypeParameter(lowerer, lowerer.checker.getTypeFromTypeNode(declParam.type)) && + lowerer.mapTypeOf(tsType) === null + ) { + lowerer.badType(blameOf(declParam, i), tsType); + } let mapped = jsOpenObjectType( declParam, lowerer.runtimeOptionalBindingType( declParam.name, - lowerer.mapTypeOf(tsType) ?? lowerer.irTypeOf(declParam.name), + lowerer.mapTypeOf(tsType) ?? + // A TypeScript instantiation at `any` (a type argument inferred + // from an untyped value) takes the checked-dynamic slot. + ((tsType.flags & ts.TypeFlags.Any) !== 0 && !isJsSourceFile(declParam.getSourceFile()) + ? DYN + : lowerer.irTypeOf(declParam.name)), ), lowerer.shapes, lowerer.unions, @@ -428,7 +486,32 @@ export function paramShapes( ); } return { type: mapped, mode: declParam.questionToken ? "omittable" : "required" }; + }; + params.forEach((declParam, i) => { + const symbol = symbols[i]; + const flattened = restTupleShapes( + lowerer, + declParam, + symbol ? lowerer.checker.getTypeOfSymbol(symbol) : undefined, + ); + if (flattened) out.push(...flattened); + else out.push(shapeAt(declParam, i)); }); + return out; +} + +/** A declared type that is (or is a union over) a bare type parameter + * or an instantiation of a generic alias/interface over type arguments. */ +function mentionsTypeParameter(lowerer: Lowerer, t: ts.Type): boolean { + if (t.flags & ts.TypeFlags.TypeParameter) return true; + const mentions = (part: ts.Type): boolean => mentionsTypeParameter(lowerer, part); + if (t.isUnionType()) return ts.constituentTypes(t).some(mentions); + if (t.getAliasTypeArguments().some(mentions)) return true; + return ( + (t.flags & ts.TypeFlags.Object) !== 0 && + ((t as ts.ObjectType).objectFlags & ts.ObjectFlags.Reference) !== 0 && + lowerer.checker.getTypeArguments(t as ts.TypeReference).some(mentions) + ); } /** A default expression may itself yield undefined. Keep that arm in the @@ -828,6 +911,16 @@ export function lambdaSignature( | ts.SetAccessorDeclaration, ): { shapes: ParamShape[]; funcType: IrType & { kind: "func" } } { if (!node.body) lowerer.unsupported("SC1090", node, "function overload signatures"); + // A parameterless function expression forwarding its `arguments`, + // asserted to a fixed signature: that signature is its ABI. + if (typedArgumentsCandidate(node) && !node.typeParameters?.nodes.length) { + const target = typedArgumentsTarget(lowerer, node); + if (target) { + const shapes = typedArgumentsShapes(target); + lowerer.typedArgumentsLambdas.add(node); + return { shapes, funcType: funcTypeFromParamShapes(shapes, target.ret) }; + } + } // A JS replacement of a native stream's write method receives the // actual call tuple, including overloads and omitted arguments. Node's // contextual overload signature cannot define this closure's ABI. diff --git a/packages/compiler/src/frontend/lowering/class-protocol-dispatch.ts b/packages/compiler/src/frontend/lowering/class-protocol-dispatch.ts index 0c9a24f24..298ff45dc 100644 --- a/packages/compiler/src/frontend/lowering/class-protocol-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-protocol-dispatch.ts @@ -188,7 +188,8 @@ function protocolSignature( call: ts.CallExpression, access: ts.PropertyAccessExpression, ): ProtocolSignature | null { - const recvTs = lowerer.typeOf(access.expression); + // A guarded optional step's receiver still carries its nullish arms. + const recvTs = lowerer.checker.getNonNullableType(lowerer.typeOf(access.expression)); const property = lowerer.checker.getPropertyOfType(recvTs, access.name.text); if (!property) return null; const declarations = property.declarations; @@ -240,10 +241,11 @@ export function lowerProtocolMethodCall( access: ts.PropertyAccessExpression, recv: IrExpr, ): IrExpr | null { + // An optional chain that already guarded this step (`s?.m(x)`, the chain + // re-dispatching with the narrowed receiver) calls like a plain one. if ( recv.type.kind !== "dyn" || - call.questionDotToken || - access.questionDotToken || + lowerer.chainBlocked(call, access) || call.arguments.nodes.some(ts.isSpreadElement) ) return null; diff --git a/packages/compiler/src/frontend/lowering/class-prototypes.ts b/packages/compiler/src/frontend/lowering/class-prototypes.ts index d337dc802..6c6c7811d 100644 --- a/packages/compiler/src/frontend/lowering/class-prototypes.ts +++ b/packages/compiler/src/frontend/lowering/class-prototypes.ts @@ -1,4 +1,5 @@ import { dynUndefinedExpr, varRef } from "../../ir/build.js"; +import { builtinClassKind, builtinClassValue } from "./builtins/builtin-class-dyn.js"; import * as ts from "../ts7/adapter.js"; import { BOOL, @@ -450,6 +451,19 @@ export function lowerClassPrototypeData( exactClassOfReceiver(lowerer, expr.expression) ?? (stored?.kind === "classval" ? lowerer.classes.get(stored.className) : undefined); if (!info) return null; + // EventEmitter.prototype and the stream prototypes: the untyped + // constructor's prototype object (builtin-class-dyn.ts). + const builtinKind = builtinClassKind(info.def.name); + if (builtinKind !== null) { + const loc = locOf(expr); + return { + kind: "dynKeyGet", + value: builtinClassValue(builtinKind, loc), + key: { kind: "strLit", value: "prototype", type: STRING, loc }, + type: DYN, + loc, + }; + } const parent = expr.parent; const descriptorKey = ts.isCallExpression(parent) && diff --git a/packages/compiler/src/frontend/lowering/expressions/binary.ts b/packages/compiler/src/frontend/lowering/expressions/binary.ts index cecbde717..8ef631f60 100644 --- a/packages/compiler/src/frontend/lowering/expressions/binary.ts +++ b/packages/compiler/src/frontend/lowering/expressions/binary.ts @@ -1,5 +1,6 @@ /* Binary operators: arithmetic, comparisons and equality, logical and nullish pairs, and string conversion for concatenation. */ import * as ts from "../../ts7/adapter.js"; +import { cleanFuncAdaptable } from "../coercions/function-adapters.js"; import { type Lowerer, jsFuncNameOf } from "../lowerer.js"; import { BIGINT_T, @@ -481,6 +482,22 @@ export function lowerNullishPair( ); } } + // An optional callback defaulted to a function of another signature + // (`o.tags ?? (() => ({}))`, fewer parameters or a narrower result): the + // result is the callback's own function type, and the default adapts into + // it (JavaScript ignores surplus arguments). The checker's merged union + // signature would lose the callback's result shape. + if (present?.kind === "func" && present.rest !== true) { + const raw = lowerer.lowerExpr(expr.right); + if ( + raw.type.kind === "func" && + (typeEquals(raw.type, present) || cleanFuncAdaptable(lowerer, raw.type, present)) + ) { + const right = lowerer.coerceInto(expr.right, raw, present); + lowerer.nullishFuncResults.add(expr); + return { kind: "nullish", left, right, type: present, loc }; + } + } lowerer.unsupported( "SC1090", expr, diff --git a/packages/compiler/src/frontend/lowering/expressions/element-access.ts b/packages/compiler/src/frontend/lowering/expressions/element-access.ts index 144742257..f39b7c428 100644 --- a/packages/compiler/src/frontend/lowering/expressions/element-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/element-access.ts @@ -63,6 +63,8 @@ import { npmStaticPackageOfPath } from "../../npm-static.js"; import { isSafeToRepeat } from "./evaluation-safety.js"; import { lowerClassPrototypeComputedAssignment } from "../class-prototypes.js"; import { isClassCallback } from "../class-callbacks.js"; +import { isKeyedFieldAccess } from "./keyed-field-targets.js"; +import { lowerFieldCompoundValue } from "./updates.js"; /** `a[i]` reads. Only f64 indices into array receivers are modeled; JS * string-key element access (`a["length"]`) and string indexing (`s[0]` @@ -1417,6 +1419,11 @@ export function lowerElementCompoundParts( const target = expr.left as ts.ElementAccessExpression; fenceNodeModuleMutation(lowerer, target, "assignment"); const loc = locOf(expr); + // A record through a computed key (`totals[side] += n` with a literal + // union key, `counts[name] += 1` on an index-signature record): the + // member compound's receiver, key, old value, rhs, write sequence. + if (isKeyedFieldAccess(lowerer, target)) + return { expr: lowerFieldCompoundValue(lowerer, target, op, expr.right, loc), stmts: null }; const checked = lowerer.lowerExpr(target.expression); if (checked?.type.kind === "dyn" && isJsSourceFile(expr.getSourceFile())) { const stmts: IrStmt[] = []; diff --git a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts index 70ed1b2ba..c2e5ddcbb 100644 --- a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts +++ b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts @@ -1,6 +1,8 @@ /* Identifier references in expression position: lowerExprInner's identifier branch and its helpers. */ import { cjsMemberAliasValueOf } from "../cjs-exports-object.js"; +import { builtinClassValue, builtinMemberClassKind } from "../builtins/builtin-class-dyn.js"; import * as ts from "../../ts7/adapter.js"; +import { InternalCompilerError } from "../../../errors.js"; import * as posix from "node:path/posix"; import { type Lowerer, PoisonError } from "../lowerer.js"; import { @@ -21,6 +23,7 @@ import { import { lowerNodeModuleIdentifier } from "../lower-node-module.js"; import { lowerCjsExportsObjectValue } from "../cjs-exports-object.js"; import { captureContextArguments } from "../function-context.js"; +import { argumentsOwner, typedArgumentsCandidate } from "../typed-arguments.js"; import { isCjsExportTableLiteral, isCjsJsFile, @@ -96,7 +99,18 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: // type and lowerLambda declared the local. if (expr.text === "arguments") { const binding = captureContextArguments(lowerer.fnStack); - if (binding) return { kind: "varRef", localId: binding.id, type: DYN, loc }; + if (binding && lowerer.ctx.pureTuples.has(binding.id)) { + throw new InternalCompilerError("lowerer bug: forwarded-only 'arguments' read as a value"); + } + if (binding) return { kind: "varRef", localId: binding.id, type: binding.type, loc }; + const owner = argumentsOwner(expr); + if (owner && typedArgumentsCandidate(owner)) { + lowerer.unsupported( + "SC1090", + expr, + "'arguments' in a function expression whose value is not asserted to one fixed-arity signature (assert it, as in `function () { … } as F`, and the function lowers against that signature)", + ); + } } // A compiler-projected adapter as a VALUE: the adapter has no // runtime function object (its calls rewrite directly) — call it @@ -539,6 +553,10 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: // the same identity-token story as stdlib globals above (the // harness adds worker_threads.Worker to its identity Set). if (isJsSourceFile(expr.getSourceFile())) { + // EventEmitter and the stream classes are the runtime's untyped + // constructors, so constructor functions can inherit from them. + const classKind = builtinMemberClassKind(bi.module, bi.member); + if (classKind !== null) return builtinClassValue(classKind, loc); // A function member is a function value that fences when called. if (builtinModuleFnOf(lowerer, bi.module, bi.member)) return builtinFunctionFenceValue(lowerer, bi, expr, loc); diff --git a/packages/compiler/src/frontend/lowering/expressions/keyed-field-targets.ts b/packages/compiler/src/frontend/lowering/expressions/keyed-field-targets.ts new file mode 100644 index 000000000..7a4f9c557 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/expressions/keyed-field-targets.ts @@ -0,0 +1,271 @@ +/* Read-modify-write targets for computed record keys: `r[k] op= v`, `r[k]++` and `r[k] ??= v`. */ +import * as ts from "../../ts7/adapter.js"; +import { + BOOL, + F64, + STRING, + typeEquals, + type IrExpr, + type IrStmt, + type SrcLoc, +} from "../../../ir/ir.js"; +import { isJsSourceFile } from "../../program.js"; +import { constituentTypes } from "../../ts7/checker.js"; +import type { FieldTarget } from "../lower-exprs.js"; +import type { Lowerer } from "../lowerer.js"; + +export type KeyedFieldTarget = FieldTarget & { container: "recordKeyed" }; + +/** The property names a computed key's type allows: one or more string + * literals (a literal union such as `"sent" | "received"`, enum string + * members, or a type parameter bound to one inside a generic instance). + * Null for any other key type. */ +export function literalFieldKeys(lowerer: Lowerer, keyNode: ts.Expression): string[] | null { + let t: ts.Type = lowerer.typeOf(keyNode); + if (t.flags & ts.TypeFlags.TypeParameter) { + const bound = lowerer.typeParamTsResolver(t); + if (!bound) return null; + t = bound; + } + const parts = t.flags & ts.TypeFlags.Union ? constituentTypes(t) : [t]; + const keys: string[] = []; + for (const part of parts) { + if (!part.isStringLiteralType()) return null; + if (!keys.includes(part.value)) keys.push(part.value); + } + return keys.length > 0 ? keys : null; +} + +/** Does `r[k]` read and write through declared record storage by key? A + * checker-only test (nothing lowers), so a caller can route before it + * evaluates the receiver: TypeScript sources only, a record whose declared + * fields include every name the key's literal type allows, or a pure + * index-signature record with a string key. */ +export function isKeyedFieldAccess(lowerer: Lowerer, access: ts.ElementAccessExpression): boolean { + if ( + access.questionDotToken || + isJsSourceFile(access.getSourceFile()) || + lowerer.isProcessEnv(access.expression) + ) + return false; + const keys = literalFieldKeys(lowerer, access.argumentExpression); + if (!keys && !(lowerer.typeOf(access.argumentExpression).flags & ts.TypeFlags.StringLike)) + return false; + const receiver = lowerer.mapTypeOf(lowerer.typeOf(access.expression)); + if (receiver?.kind !== "record") return false; + const shape = lowerer.shapes.get(receiver.shapeId); + if (!shape || shape.tuple) return false; + if (shape.fields.length === 0) return shape.indexValue !== undefined; + return keys !== null && keys.every((k) => shape.fields.some((f) => f.name === k)); +} + +/** The target of a read-modify-write through `r[k]`, or null when + * isKeyedFieldAccess declines. A literal-union key dispatches over the + * declared fields it names, which must share one type; a string key on a + * pure index-signature record reads and writes its entries. The receiver + * and key may arrive already lowered (and saved) by the caller; otherwise + * both lower here, unsaved. A caller that evaluates the target more than + * once saves `obj` and then `key`, the JavaScript evaluation order. With + * `numeric`, a missing index-signature entry reads as NaN (every numeric + * update applies ToNumber to it) instead of a value armed with undefined. */ +export function keyedFieldTarget( + lowerer: Lowerer, + access: ts.ElementAccessExpression, + numeric: boolean, + lowered?: { obj: IrExpr; key: IrExpr }, +): KeyedFieldTarget | null { + if (!isKeyedFieldAccess(lowerer, access)) return null; + const keyNode = access.argumentExpression; + const keys = literalFieldKeys(lowerer, keyNode); + const obj = lowered?.obj ?? lowerer.lowerExpr(access.expression); + if (obj.type.kind !== "record") { + lowerer.unsupported( + "SC1090", + access.expression, + `updating a computed key of a '${lowerer.fmt(obj.type)}' value (narrow the receiver to its record type first)`, + ); + } + const shapeId = obj.type.shapeId; + const shape = lowerer.shapes.get(shapeId)!; + // A single literal spelled at the site (or an identifier whose type is + // that one literal) names the field statically: identifier evaluation + // is pure, so skipping it matches JavaScript. + const indexed = shape.fields.length === 0; + const staticKey = + !indexed && keys?.length === 1 && (ts.isStringLiteralLike(keyNode) || ts.isIdentifier(keyNode)); + const key = staticKey ? null : (lowered?.key ?? lowerer.lowerExpr(keyNode)); + if (key && key.type.kind !== "string") { + lowerer.unsupported( + "SC1090", + keyNode, + `updating a record through a '${lowerer.fmt(key.type)}' key (convert the key to a string first)`, + ); + } + if (keys && !indexed) { + const fields = keys.map((k) => shape.fields.find((f) => f.name === k)); + const first = fields[0]; + if (!first || fields.some((f) => !f || !typeEquals(f.type, first.type))) { + lowerer.unsupported( + "SC1090", + access, + `updating '${lowerer.fmt(obj.type)}' through a key whose fields are not all stored with one type (${keys.map((k) => `'${k}'`).join(", ")}; switch over the key and update each field)`, + ); + } + return { + container: "recordKeyed", + obj, + shapeId, + field: keys[0]!, + keys, + key, + fieldType: first.type, + }; + } + if (!indexed || !shape.indexValue) { + lowerer.unsupported( + "SC1090", + access, + `updating '${lowerer.fmt(obj.type)}' through a runtime key (the stored record declares fields)`, + ); + } + return { + container: "recordKeyed", + obj, + shapeId, + field: "", + keys: null, + key, + fieldType: shape.indexValue, + ...(numeric && shape.indexValue.kind === "f64" ? { missNaN: true as const } : {}), + }; +} + +/** Does `key` name `field`? */ +function keyIs(key: IrExpr, field: string, loc: SrcLoc): IrExpr { + return { + kind: "strEq", + negated: false, + left: key, + right: { kind: "strLit", value: field, type: STRING, loc }, + type: BOOL, + loc, + }; +} + +/** A key outside its literal type (smuggled through a cast) names no + * declared field: a fixed-shape record cannot add the property, so the + * update throws the TypeError of a keyed write miss. */ +function keyMiss(key: IrExpr, target: KeyedFieldTarget, loc: SrcLoc): IrExpr { + const message: IrExpr = { + kind: "strConcat", + left: { + kind: "strConcat", + left: { kind: "strLit", value: "Cannot add property '", type: STRING, loc }, + right: key, + type: STRING, + loc, + }, + right: { kind: "strLit", value: "' to a fixed-shape object", type: STRING, loc }, + type: STRING, + loc, + }; + return { + kind: "libCall", + fn: "error.nodeThrow", + args: [ + { kind: "numLit", value: 1, type: F64, loc }, + { kind: "strLit", value: "", type: STRING, loc }, + message, + ], + type: target.fieldType, + loc, + }; +} + +/** The current value of a keyed target. */ +export function keyedFieldRead(lowerer: Lowerer, target: KeyedFieldTarget, loc: SrcLoc): IrExpr { + const { obj, shapeId, key, fieldType } = target; + if (!target.keys) { + const type = target.missNaN + ? fieldType + : fieldType.kind === "dyn" + ? fieldType + : lowerer.withUndefinedArmOf(fieldType); + if (!type || !key) throw new Error("keyed index-signature target without a key or read type"); + return { + kind: "recordKeyGet", + obj, + shapeId, + key, + ...(target.missNaN ? { missNaN: true as const } : {}), + type, + loc, + }; + } + const get = (field: string): IrExpr => ({ + kind: "recordGet", + obj, + shapeId, + field, + type: fieldType, + loc, + }); + if (!key) return get(target.keys[0]!); + return target.keys.reduceRight( + (otherwise, field) => ({ + kind: "ternary", + cond: keyIs(key, field, loc), + then: get(field), + else_: otherwise, + type: fieldType, + loc, + }), + keyMiss(key, target, loc), + ); +} + +/** Store `value` (already of the target's field type) through a keyed + * target. A multi-key dispatch references the value once per field, so a + * value that is not a plain local is saved first. */ +export function keyedFieldWrite( + lowerer: Lowerer, + target: KeyedFieldTarget, + value: IrExpr, + loc: SrcLoc, +): IrStmt { + const { obj, shapeId, key } = target; + if (!target.keys) { + if (!key) throw new Error("keyed index-signature target without a key"); + return { kind: "recordKeySet", obj, shapeId, key, value, loc }; + } + if (!key) return { kind: "recordSet", obj, shapeId, field: target.keys[0]!, value, loc }; + const prefix: IrStmt[] = []; + let stored = value; + if (value.kind !== "varRef") { + const local = lowerer.declareHiddenLocal("%keyedValue", value.type); + prefix.push({ kind: "varDecl", localId: local.id, init: value, loc }); + stored = { kind: "varRef", localId: local.id, type: value.type, loc }; + } + const chain = target.keys.reduceRight( + (otherwise, field) => [ + { + kind: "if", + cond: keyIs(key, field, loc), + then: [{ kind: "recordSet", obj, shapeId, field, value: stored, loc }], + else_: otherwise, + loc, + }, + ], + [{ kind: "exprStmt", expr: keyMiss(key, target, loc), loc }], + ); + return { kind: "block", body: [...prefix, ...chain], loc }; +} + +/** Save a keyed target's key after its receiver, so a multi-key dispatch + * reads and writes the field the key named when it was evaluated. */ +export function saveKeyedFieldKey( + target: FieldTarget, + save: (value: IrExpr, name: string) => IrExpr, +): void { + if (target.container === "recordKeyed" && target.key) target.key = save(target.key, "%keyedKey"); +} diff --git a/packages/compiler/src/frontend/lowering/expressions/narrowing.ts b/packages/compiler/src/frontend/lowering/expressions/narrowing.ts index b54da8180..f49d46fb0 100644 --- a/packages/compiler/src/frontend/lowering/expressions/narrowing.ts +++ b/packages/compiler/src/frontend/lowering/expressions/narrowing.ts @@ -26,6 +26,7 @@ import { jsBindingHasOpenWrites } from "../lower-stmts.js"; import { narrowClassUnion, narrowStoredClassValue, structuralNarrowing } from "../class-unions.js"; import { checkedClassAssertion, checkedClassUnionAssertion } from "../class-assertions.js"; import { isOptionalProcessStreamProperty } from "../builtins/process.js"; +import { requestGeneralStreamSurface } from "../general-streams.js"; import { literalValues } from "../../literal-values.js"; import { literalUnionArm } from "../../union-discriminants.js"; import { checkedArmExtraction } from "../coercions/unions.js"; @@ -1186,6 +1187,56 @@ function classInstanceOrAbsent(lowerer: Lowerer, type: IrType): boolean { ); } +/** `Object.fromEntries(...) as T` with T a declared record (the generic + * `transform>(t: T): T` idiom): the + * entries build a pure index-signature record, which has no layout in + * common with T's struct. Node keeps the object itself, with exactly the + * keys and the order the entries gave it. The result of fromEntries is a + * fresh object nothing else references, so the checked-dynamic bridge, + * which keeps keys and their order the same way `JSON.parse(text) as T` + * does, cannot be told apart from the original object; a value outside + * T throws the catchable TypeError of every checked cast. The entries' + * values must be scalars or checked-dynamic values, which the bridge + * passes by value or by identity; native arrays and records would be + * copied. Other sources may be aliased, where a rebuilt object would sever + * mutation, so they keep the exact-shape fence. */ +function freshEntriesRecordAssertion( + lowerer: Lowerer, + expr: ts.AsExpression | ts.TypeAssertion, + inner: IrExpr, + target: IrType | null, +): IrExpr | null { + if (inner.type.kind !== "record" || target?.kind !== "record") return null; + const source = lowerer.shapes.get(inner.type.shapeId); + const dest = lowerer.shapes.get(target.shapeId); + if (!source?.indexValue || source.tuple || source.fields.length > 0) return null; + if (!dest || dest.tuple || dest.indexValue || shapeHasAccessorSlots(dest)) return null; + // Native arrays and records in the entries would be copied on the way + // through the checked-dynamic bridge, severing their identity: only + // slots of scalars or checked-dynamic values pass. + if (extractionCopiesReference(lowerer, source.indexValue)) return null; + let operand: ts.Expression = expr.expression; + while (ts.isParenthesizedExpression(operand)) operand = operand.expression; + if ( + !ts.isCallExpression(operand) || + !ts.isPropertyAccessExpression(operand.expression) || + operand.expression.name.text !== "fromEntries" || + !lowerer.isStdlibGlobal(operand.expression.expression, "Object") + ) + return null; + if (!lowerer.dynConvertible(inner.type)) return null; + if ( + !canDynCheckTo( + target, + (id) => lowerer.shapes.get(id), + (id) => lowerer.unions.get(id), + ) + ) + return null; + const loc = locOf(expr); + return { kind: "dynCheck", value: lowerer.coerceToExpected(inner, DYN), type: target, loc }; +} + /** `e as T` and the old-style assertion `e` — one node shape (both * carry `.type` and `.expression`), one lowering. */ export function lowerAsExpression( @@ -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, diff --git a/packages/compiler/src/frontend/lowering/expressions/nullish-assignment.ts b/packages/compiler/src/frontend/lowering/expressions/nullish-assignment.ts index 4366b8d6b..81b3b9d69 100644 --- a/packages/compiler/src/frontend/lowering/expressions/nullish-assignment.ts +++ b/packages/compiler/src/frontend/lowering/expressions/nullish-assignment.ts @@ -18,6 +18,11 @@ import { arrayNumberRead, arrayValueRead, arrayValueStore } from "../array-value import { fenceNodeModuleMutation } from "../lower-node-module.js"; import { globalSymbolKey } from "./global-symbols.js"; import { tryLowerExpression } from "./try-lower-expression.js"; +import { + type KeyedFieldTarget, + keyedFieldTarget, + saveKeyedFieldKey, +} from "./keyed-field-targets.js"; import { eventAccessorRead, eventAccessorWrite, @@ -147,6 +152,7 @@ export function lowerShortCircuitAssignment(lowerer: Lowerer, expr: ts.BinaryExp } else { const receiver = save(lowerer.lowerExpr(target.expression), "%shortCircuitReceiver"); let key = lowerer.lowerExpr(target.argumentExpression); + let keyed: KeyedFieldTarget | null = null; if (receiver.type.kind === "dyn") { key = save( { @@ -245,6 +251,17 @@ export function lowerShortCircuitAssignment(lowerer: Lowerer, expr: ts.BinaryExp value: lowerer.coerceToExpected(value, indexValue), loc, }); + } else if ( + receiver.type.kind === "record" && + (keyed = keyedFieldTarget(lowerer, target, false, { obj: receiver, key })) + ) { + // `totals[side] ??= v` through a literal-union key: the read and + // the write dispatch over the declared fields the key can name. + const field = keyed; + saveKeyedFieldKey(field, save); + storage = field.fieldType; + read = lowerer.fieldGetExpr(field, locOf(target), target); + write = (value) => lowerer.fieldSetStmt(field, value, loc, target); } else { lowerer.unsupported( "SC1090", diff --git a/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts b/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts index 930e1a7cb..995514a4d 100644 --- a/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts +++ b/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts @@ -1,3 +1,5 @@ +import { isSafeToDiscard } from "./evaluation-safety.js"; +import { completeFuncValueArgs } from "../call-arguments.js"; import { dynUndefinedExpr } from "../../../ir/build.js"; import * as ts from "../../ts7/adapter.js"; import { DYN, STRING, UNDEFINED_T, VOID, isUnitType, typeEquals } from "../../../ir/ir.js"; @@ -429,7 +431,22 @@ export function lowerOptionalChain( if (dotNode === expr && ts.isCallExpression(expr)) { if (narrowed.kind !== "func") lowerer.badType(recvNode, lowerer.typeOf(recvNode)); const params = narrowed.params; + // Forwarded fixed tuples (`f?.(...args)`) complete into the slots. + if (expr.arguments.nodes.some(ts.isSpreadElement)) { + const completed = completeFuncValueArgs(lowerer, expr, narrowed, loc); + const call: IrExpr = { + kind: "callValue", + callee: recvRef, + args: completed, + type: narrowed.ret, + loc, + }; + return lowerer.finishOptionalChain(expr, id, receiver, call, loc); + } const args = expr.arguments.nodes.map((a, i) => lowerer.lowerExprExpecting(a, params[i])); + // Effect-free surplus arguments (a dropped `void` slot) are ignored. + while (narrowed.rest !== true && args.length > params.length && isSafeToDiscard(args.at(-1)!)) + args.pop(); const body: IrExpr = { kind: "callValue", callee: recvRef, args, type: narrowed.ret, loc }; return lowerer.finishOptionalChain(expr, id, receiver, body, loc); } diff --git a/packages/compiler/src/frontend/lowering/expressions/property-access.ts b/packages/compiler/src/frontend/lowering/expressions/property-access.ts index 3aac527f0..72a44b4c9 100644 --- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts @@ -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, diff --git a/packages/compiler/src/frontend/lowering/expressions/relational.ts b/packages/compiler/src/frontend/lowering/expressions/relational.ts index 5088f7be9..aaf881788 100644 --- a/packages/compiler/src/frontend/lowering/expressions/relational.ts +++ b/packages/compiler/src/frontend/lowering/expressions/relational.ts @@ -1,5 +1,6 @@ /* instanceof and in, including private-name and runtime-key in. */ import * as ts from "../../ts7/adapter.js"; +import { builtinClassInstanceOf, builtinClassKind } from "../builtins/builtin-class-dyn.js"; import { checkedPrivateIn, localPrivateBrandClass, @@ -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 = new Set([ + "errno", + "syscall", + "path", + "dest", + "info", + "errors", + "error", + "suppressed", + "actual", + "expected", + "operator", + "generatedMessage", +]); + const OBJECT_PROTOTYPE_KEYS: ReadonlySet = 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( + (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, diff --git a/packages/compiler/src/frontend/lowering/expressions/updates.ts b/packages/compiler/src/frontend/lowering/expressions/updates.ts index 2270f6043..41d8f6332 100644 --- a/packages/compiler/src/frontend/lowering/expressions/updates.ts +++ b/packages/compiler/src/frontend/lowering/expressions/updates.ts @@ -42,6 +42,7 @@ import type { CompoundOp } from "../surfaces.js"; import { tryLowerExpression } from "./try-lower-expression.js"; import { hasRuntimeStatics } from "../class-runtime-statics.js"; import { declareContextLocal } from "../function-context.js"; +import { keyedFieldTarget, keyedFieldWrite, saveKeyedFieldKey } from "./keyed-field-targets.js"; /** Read-modify-write updates read the member first: an unassigned * deferred-init receiver throws Node's member-read TypeError. */ @@ -337,10 +338,11 @@ export function lowerIncDec( } const target = ts.isPropertyAccessExpression(access) ? lowerer.fieldTarget(access) - : symbolFieldTarget(lowerer, access); + : (symbolFieldTarget(lowerer, access) ?? keyedFieldTarget(lowerer, access, true)); if (target && lowerer.dynConvertible(target.fieldType)) { checkDeferredMemberReceiver(lowerer, target, access); target.obj = save(target.obj, "%incrementReceiver"); + saveKeyedFieldKey(target, save); return finish(lowerer.fieldGetExpr(target, loc, access), (value) => lowerer.fieldSetStmt( target, @@ -617,6 +619,7 @@ export function fieldSetStmt( }; } } + if (target.container === "recordKeyed") return keyedFieldWrite(lowerer, target, value, loc); if (target.container === "dynamic") return { kind: "exprStmt", @@ -872,12 +875,13 @@ export function lowerFieldCompoundValue( const targetOf = (): FieldTarget | null => ts.isPropertyAccessExpression(access) ? lowerer.fieldTarget(access) - : symbolFieldTarget(lowerer, access); + : (symbolFieldTarget(lowerer, access) ?? keyedFieldTarget(lowerer, access, true)); const target = targetOf(); if (!target) lowerer.unsupported("SC1090", access, "compound assignment to unsupported field targets"); checkDeferredMemberReceiver(lowerer, target, access); target.obj = save(target.obj, "%compoundReceiver"); + saveKeyedFieldKey(target, save); // Accessors observe getter, RHS side effects, then setter, all through // the saved receiver. const fieldRead = lowerer.fieldGetExpr(target, locOf(access), access); diff --git a/packages/compiler/src/frontend/lowering/function-context.ts b/packages/compiler/src/frontend/lowering/function-context.ts index e31e194c5..55d8ed0b0 100644 --- a/packages/compiler/src/frontend/lowering/function-context.ts +++ b/packages/compiler/src/frontend/lowering/function-context.ts @@ -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; /** 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, diff --git a/packages/compiler/src/frontend/lowering/general-streams.ts b/packages/compiler/src/frontend/lowering/general-streams.ts new file mode 100644 index 000000000..0113a9958 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/general-streams.ts @@ -0,0 +1,114 @@ +/* The general stream representation (TypeMapperCtx.generalNodeStreams). + * + * NodeJS.ReadableStream and NodeJS.WritableStream (and @types/node's + * stream.Readable / stream.Writable, which also type child stdio) name the + * interface every Node stream implements. By default they map to the + * child/process stdio handles: the specialized representation of the + * values that commonly fill them (child.stdout, process.stdout). A program + * that moves another stream — fs.createReadStream(), a zlib Transform, a + * Readable subclass — into one of those slots needs the general + * representation instead: the runtime stream classes, with the stdio + * handles converted into them where they are read or flow. + * + * The decision is per program. Reachable emit lowers without it; a flow + * that needs it marks the lowerer (needsGeneralNodeStreams) and lowerToIr + * lowers the whole program again with it. Programs without such a flow + * never take the second pass, so their IR is unchanged. */ +import type { IrExpr, IrType } from "../../ir/ir.js"; +import { everyTypeChild } from "../../ir/traverse.js"; +import * as ts from "../ts7/adapter.js"; +import { isGeneralNodeStreamInterface } from "../type-mapper.js"; +import type { Lowerer } from "./lowerer.js"; +import { streamSidesOf } from "./lower-stream.js"; + +const STDIO_STREAM_KINDS: ReadonlySet = new Set([ + "childStream", + "childWriter", + "procStream", +]); + +/** Whether `type` (through union arms, record fields and the structural + * type children) contains a part satisfying `test`. */ +function containsPart(lowerer: Lowerer, type: IrType, test: (part: IrType) => boolean): boolean { + const seen = new Set(); + const walk = (part: IrType): boolean => { + if (test(part)) return true; + if (part.kind === "union") { + if (seen.has(`u:${part.unionId}`)) return false; + seen.add(`u:${part.unionId}`); + return (lowerer.unions.get(part.unionId)?.arms ?? []).some(walk); + } + if (part.kind === "record") { + if (seen.has(`r:${part.shapeId}`)) return false; + seen.add(`r:${part.shapeId}`); + return (lowerer.shapes.get(part.shapeId)?.fields ?? []).some((f) => walk(f.type)); + } + let found = false; + everyTypeChild(part, (child) => { + found = walk(child); + return !found; + }); + return found; + }; + return walk(type); +} + +function isGeneralStream(lowerer: Lowerer, part: IrType): boolean { + return ( + part.kind === "object" && streamSidesOf(lowerer, lowerer.classes.get(part.className)) !== null + ); +} + +/** A mismatched flow (requireExactShape's input) that the general stream + * representation resolves: a general stream into a stdio-represented + * slot, a child pipe handle into an 'unknown' slot, or an 'unknown' value + * into a child-pipe slot (the general classes take a checked cast). */ +export function needsGeneralNodeStreams( + lowerer: Lowerer, + actual: IrType, + expected: IrType, +): boolean { + const stdio = (part: IrType): boolean => STDIO_STREAM_KINDS.has(part.kind); + const pipeHandle = (part: IrType): boolean => + part.kind === "childStream" || part.kind === "childWriter"; + if (actual.kind === "dyn") return containsPart(lowerer, expected, pipeHandle); + if (containsPart(lowerer, expected, stdio)) + return containsPart(lowerer, actual, (p) => isGeneralStream(lowerer, p)); + return expected.kind === "dyn" && containsPart(lowerer, actual, pipeHandle); +} + +/** Under the general representation, process.stdout / process.stderr (the + * procStream fd scalar) flowing into a runtime Writable slot: an + * fd-backed Writable whose writes take the same synchronous path as + * process.stdout.write. Null when the flow is not that one. */ +export function stdioIntoGeneralStream( + lowerer: Lowerer, + expr: IrExpr, + expected: IrType, +): IrExpr | null { + if (!lowerer.generalNodeStreams || expr.type.kind !== "procStream") return null; + const writable: IrType = { kind: "object", className: "%Writable" }; + const accepts = + (expected.kind === "object" && expected.className === "%Writable") || + (expected.kind === "union" && lowerer.armTag(expected.unionId, writable) >= 0); + if (!accepts) return null; + return { + kind: "libCall", + fn: "stream.stdioWritable", + args: [expr], + type: writable, + loc: expr.loc, + }; +} + +/** A stdio-handle receiver whose member has no lowering on the handle + * (`out.on("error", ...)`, `child.stdout.pipe(...)`, `output.end()`): when + * its declared type is one the general representation maps to a runtime + * stream class, the program lowers again with it, where the full stream + * surface applies. The caller still reports its fence for this pass. */ +export function requestGeneralStreamSurface(lowerer: Lowerer, receiver: ts.Expression): void { + if (lowerer.generalNodeStreams) return; + const type = lowerer.checker.getNonNullableType(lowerer.typeOf(receiver)); + if (isGeneralNodeStreamInterface(type, lowerer.typeCtx)) + lowerer.requiresGeneralNodeStreams = true; +} diff --git a/packages/compiler/src/frontend/lowering/generic-functions.ts b/packages/compiler/src/frontend/lowering/generic-functions.ts index f588faf6d..6572c1c88 100644 --- a/packages/compiler/src/frontend/lowering/generic-functions.ts +++ b/packages/compiler/src/frontend/lowering/generic-functions.ts @@ -1,3 +1,4 @@ +import { staticNameBindings } from "./static-function-names.js"; import * as ts from "../ts7/adapter.js"; import { bodyReadsArguments } from "../arguments-usage.js"; import type { Lowerer } from "./lowerer.js"; @@ -100,6 +101,9 @@ export interface GenericFnInfo { * names where the slot binds the call site's object type. Computed on * first use. */ implicitPatterns?: (ImplicitPattern | null)[]; + /** Parameter index → symbol for the function parameters whose `.name` + * the body reads (static-function-names.ts); computed on first use. */ + nameReadParams?: Map; } export interface GenericInstance { @@ -140,6 +144,9 @@ export interface GenericInstance { implicitInferReturn?: true; /** An erased instance (every type parameter bound to `unknown`). */ erased?: true; + /** Name-keyed instances: function parameter → its call site's static + * name (null when only known at run time), folded at `.name` reads. */ + staticFnNames?: Map; } /** Defaults bind during inference; collection only resolves symbols in @@ -295,7 +302,13 @@ export function genericCallInstance( const retTs = lowerer.checker.getReturnTypeOfSignature(rsig); const local = localClassResultInstance(lowerer, expr, info, rsig, params, retTs); if (local) return local; - const mappedReturn = lowerer.mapTypeOf(retTs); + // A result instantiated at `any` (a type argument inferred from an + // untyped value, such as an untyped forwarding call) is checked-dynamic. + const mappedReturn = + lowerer.mapTypeOf(retTs) ?? + ((retTs.flags & ts.TypeFlags.Any) !== 0 && !isJsSourceFile(info.decl.getSourceFile()) + ? DYN + : null); if (!mappedReturn) { fenceGenericSignatureResult(lowerer, expr, retTs); lowerer.badType(expr, retTs); @@ -317,6 +330,7 @@ export function genericCallInstance( // different fields). Non-literal bindings (a key union, plain string) // share one runtime-keyed instance per IR signature, exactly the // widened discipline. + const named = staticNameBindings(lowerer, expr, info); const keyofTps = keyofConstrainedTypeParams(info); if (keyofTps.size > 0) { const tsBindings = new Map(); @@ -333,12 +347,19 @@ export function genericCallInstance( }) .join(","); return internGenericInstance(lowerer, expr, info, params, returnType, () => bindings, { - extraKey: `@${litKey}`, + extraKey: `@${litKey}${named?.key ?? ""}`, tsBindings, + ...(named ? { staticFnNames: named.names } : {}), }); } - return internGenericInstance(lowerer, expr, info, params, returnType, (tsBindings) => - lowerer.inferTypeParamBindings(expr, info, rsig, tsBindings), + return internGenericInstance( + lowerer, + expr, + info, + params, + returnType, + (tsBindings) => lowerer.inferTypeParamBindings(expr, info, rsig, tsBindings), + named ? { extraKey: named.key, staticFnNames: named.names } : undefined, ); } @@ -498,7 +519,11 @@ function internGenericInstance( params: ParamShape[], returnType: IrType, makeBindings: (tsBindings: Map) => Map, - opts?: { extraKey?: string; tsBindings?: Map }, + opts?: { + extraKey?: string; + tsBindings?: Map; + staticFnNames?: Map; + }, ): GenericInstance { // keyof-constrained instantiations append their literal keys // (extraKey): the IR signature alone under-discriminates there — two @@ -541,6 +566,7 @@ function internGenericInstance( bindings, tsBindings, typeArgsText, + ...(opts?.staticFnNames ? { staticFnNames: opts.staticFnNames } : {}), }; info.instances.set(key, inst); lowerer.instantiationQueue.push({ info, inst }); @@ -671,7 +697,11 @@ function unifySignatureBindings( tsBindings.set(sym, lowerer.typeParamTsResolver(inst) ?? inst); } if (!bindings.has(sym)) { - const mapped = lowerer.mapTypeOf(inst); + const mapped = + lowerer.mapTypeOf(inst) ?? + ((inst.flags & ts.TypeFlags.Any) !== 0 && !isJsSourceFile(info.decl.getSourceFile()) + ? DYN + : null); // A visitor's inferred T can be void. It still binds the // return convention of `(node) => T`; discarding it leaves // the callback's return type spuriously uninstantiated. @@ -824,6 +854,8 @@ export function lowerGenericInstance( const prevSuppress = lowerer.suppressStats; const prevClass = lowerer.currentClass; const prevImplicit = lowerer.implicitParamTypes; + const prevStaticFnNames = lowerer.staticFnNames; + lowerer.staticFnNames = inst.staticFnNames ?? null; // A generic METHOD of a generic-class INSTANTIATION lowers under BOTH // binding sets: the receiver instantiation's class type parameters // underneath, the method instantiation's own on top (disjoint symbol @@ -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; diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 0ca7d665a..c9f49bc68 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -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 = 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 diff --git a/packages/compiler/src/frontend/lowering/lower-comptime.ts b/packages/compiler/src/frontend/lowering/lower-comptime.ts index 41c6704c5..f239eea82 100644 --- a/packages/compiler/src/frontend/lowering/lower-comptime.ts +++ b/packages/compiler/src/frontend/lowering/lower-comptime.ts @@ -4,7 +4,7 @@ import { InternalCompilerError } from "../../errors.js"; * literals — records/arrays/unions included, subject to comptimeBakeable. */ import * as ts from "../ts7/adapter.js"; import type { Lowerer } from "./lowerer.js"; -import { type IrExpr, type IrType } from "../../ir/ir.js"; +import { isUnitType, type IrExpr, type IrType } from "../../ir/ir.js"; import { comptimeFailedDiag } from "../../diagnostics/diagnostic.js"; import { locOf } from "../program.js"; import { PoisonError } from "./lowerer.js"; @@ -76,7 +76,7 @@ export function lowerComptime(lowerer: Lowerer, expr: ts.CallExpression): IrExpr "SC1090", expr, `baking comptime results of type '${lowerer.fmt(target)}' into the binary ` + - `(only number, string, boolean, arrays, and records lower to literals)`, + `(number, string, boolean, and arrays, records and unions of those lower to literals; a record or union type reachable from itself does not)`, ); } // 5. The host executes the extracted callback under a finite budget. @@ -110,20 +110,104 @@ function comptimeFailureDetail(error: unknown): string { /** True when a comptime result of this type can be written as a literal * expression: number/string/boolean, arrays and record shapes of those, - * nested. Closures, class instances, unions, promises, dyn, and void - * cannot be baked. */ + * and unions of those with null or undefined (the mixed rows of a JSON + * table: `(string | number)[][]`), nested. Closures, class instances, + * promises, dyn, void, and recursive types (a record or union reachable + * from itself, directly or through arrays) cannot be baked. */ export function comptimeBakeable(lowerer: Lowerer, t: IrType): boolean { + return bakeableWithin(lowerer, t, new Set()); +} + +/** comptimeBakeable below the records and unions in `open` (keyed + * `record:` and `union:`), whose members are being + * checked: reaching one of them again means the type is recursive. */ +function bakeableWithin(lowerer: Lowerer, t: IrType, open: Set): boolean { switch (t.kind) { case "f64": case "string": case "bool": return true; case "array": - return lowerer.comptimeBakeable(t.elem); + return bakeableWithin(lowerer, t.elem, open); case "record": { const shape = lowerer.shapes.get(t.shapeId); - return !!shape && shape.fields.every((f) => lowerer.comptimeBakeable(f.type)); + const key = `record:${t.shapeId}`; + if (!shape || open.has(key)) return false; + open.add(key); + const bakeable = shape.fields.every((f) => bakeableWithin(lowerer, f.type, open)); + open.delete(key); + return bakeable; } + case "union": { + const arms = lowerer.unions.get(t.unionId)?.arms; + const key = `union:${t.unionId}`; + if (arms === undefined || open.has(key)) return false; + open.add(key); + const bakeable = arms.every( + (arm) => isUnitType(arm) || (arm.kind !== "union" && bakeableWithin(lowerer, arm, open)), + ); + open.delete(key); + return bakeable; + } + default: + return false; + } +} + +/** The tag of the arm of union `unionId` that `value` bakes into, or -1. + * A record value whose discriminant field holds one of the union's + * discriminant values takes that case's arm, since arms with overlapping + * shapes can both fit it. Any other value takes the first arm it fits, in + * the union's arm order: a JSON value fits exactly one scalar arm, and + * array or record arms are told apart by their contents. */ +function comptimeUnionArm(lowerer: Lowerer, unionId: string, value: unknown): number { + const def = lowerer.unions.get(unionId); + if (!def) return -1; + const discriminant = def.discriminant; + if ( + discriminant !== undefined && + typeof value === "object" && + value !== null && + !Array.isArray(value) + ) { + const key = (value as Record)[discriminant.field]; + const named = discriminant.cases.find((c) => c.values.some((v) => v === key)); + if (named !== undefined) + return comptimeFits(lowerer, value, def.arms[named.tag]!) ? named.tag : -1; + } + return def.arms.findIndex((arm) => comptimeFits(lowerer, value, arm)); +} + +/** Whether `value` bakes into the bakeable type `t` without a mismatch: + * the arm choice of a union literal. Records are exact, as in + * comptimeValueToIr; an absent field reads as undefined. */ +function comptimeFits(lowerer: Lowerer, value: unknown, t: IrType): boolean { + switch (t.kind) { + case "f64": + return typeof value === "number" && Number.isFinite(value); + case "string": + return typeof value === "string"; + case "bool": + return typeof value === "boolean"; + case "nullT": + return value === null; + case "undefinedT": + return value === undefined; + case "array": + return Array.isArray(value) && value.every((el) => comptimeFits(lowerer, el, t.elem)); + case "record": { + if (typeof value !== "object" || value === null || Array.isArray(value)) return false; + const shape = lowerer.shapes.get(t.shapeId); + if (!shape) return false; + const obj = value as Record; + const declared = new Set(shape.fields.map((f) => f.name)); + return ( + Object.keys(obj).every((key) => declared.has(key)) && + shape.fields.every((f) => comptimeFits(lowerer, obj[f.name], f.type)) + ); + } + case "union": + return comptimeUnionArm(lowerer, t.unionId, value) >= 0; default: return false; } @@ -265,6 +349,23 @@ export function comptimeValueToIr( })); return { kind: "recordLit", fields, type: expected, loc }; } + case "union": { + const arms = lowerer.unions.get(expected.unionId)!.arms; // bakeable-checked + const tag = comptimeUnionArm(lowerer, expected.unionId, value); + if (tag < 0) fail(describeComptimeValue(value)); + const arm = arms[tag]!; + const payload: IrExpr = isUnitType(arm) + ? { kind: "unitLit", unit: arm.kind === "nullT" ? "null" : "undefined", type: arm, loc } + : lowerer.comptimeValueToIr(value, arm, path, blame); + return { + kind: "unionWrap", + unionId: expected.unionId, + tag, + value: payload, + type: expected, + loc, + }; + } default: // comptimeBakeable already fenced these off. throw new InternalCompilerError(`lowerer bug: unbakeable comptime target '${expected.kind}'`); diff --git a/packages/compiler/src/frontend/lowering/lower-event-emitter.ts b/packages/compiler/src/frontend/lowering/lower-event-emitter.ts index 06e9c4ab1..4b8b9ea14 100644 --- a/packages/compiler/src/frontend/lowering/lower-event-emitter.ts +++ b/packages/compiler/src/frontend/lowering/lower-event-emitter.ts @@ -1245,7 +1245,19 @@ export function lowerEmitterMethodCall( ); } const p = cbT.params[0]; - if (p !== undefined && !(p.kind === "bytes" && p.elem === "u8") && p.kind !== "string") { + // A `Buffer | string` chunk (NodeJS.ReadableStream's listener + // shape) takes whichever side the stream delivers. + const chunkUnion = + p?.kind === "union" && + (lowerer.unions.get(p.unionId)?.arms ?? []).every( + (arm) => (arm.kind === "bytes" && arm.elem === "u8") || arm.kind === "string", + ); + if ( + p !== undefined && + !(p.kind === "bytes" && p.elem === "u8") && + p.kind !== "string" && + !chunkUnion + ) { lowerer.noLowering( `a 'data' listener whose chunk parameter is '${lowerer.fmt(p)}'`, args[1]!, diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index 672d38c03..4f81db1f1 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -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); } diff --git a/packages/compiler/src/frontend/lowering/lower-functions.ts b/packages/compiler/src/frontend/lowering/lower-functions.ts index 0ef601672..f0f2c45ea 100644 --- a/packages/compiler/src/frontend/lowering/lower-functions.ts +++ b/packages/compiler/src/frontend/lowering/lower-functions.ts @@ -1,3 +1,4 @@ +import { declareTypedArguments } from "./typed-arguments.js"; import { varRef } from "../../ir/build.js"; import { InternalCompilerError } from "../../errors.js"; import * as ts from "../ts7/adapter.js"; @@ -309,6 +310,8 @@ export function lowerLambda( : null; const { params, prologue } = lowerer.declareParams(node.parameters.nodes, shapes); if (receiver) prologue.unshift(receiver); + if (ts.isFunctionExpression(node) && lowerer.typedArgumentsLambdas.has(node)) + declareTypedArguments(lowerer, node, shapes, params, prologue); declareFunctionArguments( lowerer, node, diff --git a/packages/compiler/src/frontend/lowering/lower-inspect.ts b/packages/compiler/src/frontend/lowering/lower-inspect.ts index 3be155402..1310469fb 100644 --- a/packages/compiler/src/frontend/lowering/lower-inspect.ts +++ b/packages/compiler/src/frontend/lowering/lower-inspect.ts @@ -1,4 +1,5 @@ import { dynUndefinedExpr, boolLit, helperLoc, numLit, strLit, varRef } from "../../ir/build.js"; +import { builtinClassDynValueOf, builtinClassKindOf } from "./builtins/builtin-class-dyn.js"; import { InternalCompilerError } from "../../errors.js"; import * as ts from "../ts7/adapter.js"; @@ -2609,8 +2610,15 @@ function lowerUtilCallableCall( const arity = member === "deprecate" ? 4 : member === "callbackify" ? 1 : 2; // Program classes keep static prototypes: there is no function object // whose prototype chain the runtime could relink or wrap. + // EventEmitter and the stream classes are the exception as superCtor: + // the runtime has untyped constructors for them (builtin-class-dyn.ts). const classArgs = member === "inherits" ? 2 : member === "deprecate" ? 1 : 0; - for (const arg of expr.arguments.nodes.slice(0, classArgs)) { + const builtinBase = + member === "inherits" && expr.arguments.nodes[1] !== undefined + ? builtinClassKindOf(lowerer, expr.arguments.nodes[1]) + : null; + for (const [index, arg] of expr.arguments.nodes.slice(0, classArgs).entries()) { + if (index === 1 && builtinBase !== null) continue; if (lowerer.mapTypeOf(lowerer.typeOf(arg))?.kind === "classval") lowerer.noLowering( `util.${member} over a class constructor`, @@ -2629,9 +2637,12 @@ function lowerUtilCallableCall( const stmts: IrStmt[] = []; const args: IrExpr[] = []; for (let i = 0; i < arity; i++) { - const input: IrExpr = expr.arguments.nodes[i] - ? lowerer.lowerExprExpecting(expr.arguments.nodes[i]!, DYN) - : dynUndefinedExpr(loc); + const node = expr.arguments.nodes[i]; + const input: IrExpr = !node + ? dynUndefinedExpr(loc) + : i === 1 && builtinBase !== null + ? (builtinClassDynValueOf(lowerer, node) ?? lowerer.lowerExprExpecting(node, DYN)) + : lowerer.lowerExprExpecting(node, DYN); const saved = lowerer.declareHiddenLocal("%utilInput", DYN); stmts.push({ kind: "varDecl", localId: saved.id, init: input, loc }); args.push(varRef(saved.id, DYN, loc)); diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts index ba33811f9..11db0ec8a 100644 --- a/packages/compiler/src/frontend/lowering/lower-modules.ts +++ b/packages/compiler/src/frontend/lowering/lower-modules.ts @@ -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. diff --git a/packages/compiler/src/frontend/lowering/lower-stdlib.ts b/packages/compiler/src/frontend/lowering/lower-stdlib.ts index 3ad934ccc..78d690c6b 100644 --- a/packages/compiler/src/frontend/lowering/lower-stdlib.ts +++ b/packages/compiler/src/frontend/lowering/lower-stdlib.ts @@ -4,6 +4,7 @@ import { InternalCompilerError } from "../../errors.js"; import * as ts from "../ts7/adapter.js"; import { lowerToNumberArgument } from "./lower-exprs.js"; import type { Lowerer } from "./lowerer.js"; +import { isDeclaredInAmbientNamespace } from "../type-mapper.js"; import { arrayOf, BOOL, @@ -1023,7 +1024,15 @@ export function fenceStaticAbortControllerMemberRead( function isStdlibFetchInterface(lowerer: Lowerer, node: ts.Node, owner: string): ts.Symbol | null { const type = lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(node)); for (const symbol of [type.getSymbol(), type.getAliasSymbol()]) { - if (symbol?.name === owner && lowerer.isStdlibSymbol(symbol)) return symbol; + // NodeJS.ReadableStream shares the WHATWG interface's name. + if ( + symbol?.name === owner && + lowerer.isStdlibSymbol(symbol) && + !symbol.declarations.some( + (d) => ts.isInterfaceDeclaration(d) && isDeclaredInAmbientNamespace(d, "NodeJS"), + ) + ) + return symbol; } return null; } diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index f0ddc1570..c712541ee 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -1,5 +1,7 @@ +import { lowerTupleAliasDecl, stripTypeWrappers } from "./typed-arguments.js"; import { declaredMemberOf } from "./expressions/property-access.js"; import { hostsAll } from "./expressions/hosted-narrowing.js"; +import { requestGeneralStreamSurface } from "./general-streams.js"; import { matchAllRowsReadByElements, storedMatchAllRowsReadByElements, @@ -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 = 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, diff --git a/packages/compiler/src/frontend/lowering/lower-stream.ts b/packages/compiler/src/frontend/lowering/lower-stream.ts index e9424aec0..eb919909b 100644 --- a/packages/compiler/src/frontend/lowering/lower-stream.ts +++ b/packages/compiler/src/frontend/lowering/lower-stream.ts @@ -49,6 +49,7 @@ import { sideEffectFreeOptionValue } from "./surfaces.js"; import { lowerDynObjectLiteral } from "./expressions/object-literals.js"; import { dynUndefinedExpr, varRef } from "../../ir/build.js"; import { genResultRecord } from "../type-mapper.js"; +import { requestGeneralStreamSurface } from "./general-streams.js"; import { extractIteratorValue, genStep, genValue, stepDrivable } from "./lower-generators.js"; import { BOOL, @@ -586,6 +587,33 @@ function dynOptionsValue(lowerer: Lowerer, arg: ts.Expression): IrExpr | null { return v.type.kind === "dyn" ? v : null; } +/** @types/node declares the transform/flush completion callback as + * `(error?: Error | null, data?: any) => void`. Outside objectMode Node + * accepts only a Buffer, a string or nothing as `data`, so the callback + * parameter is typed with that union while the option callback lowers: a + * checked-dynamic value passed as `data` converts at the call. Returns the + * pinned parameter symbol (the caller unpins it), or null when the + * declared type needs no pin. */ +function pinAnyDataCompletion( + lowerer: Lowerer, + which: string, + fnNode: ts.MethodDeclaration | ts.FunctionExpression | ts.ArrowFunction, +): ts.Symbol | null { + const index = which === "transform" ? 2 : which === "flush" ? 0 : -1; + const param = index < 0 ? undefined : fnNode.parameters.nodes[index]; + if (param === undefined || !ts.isIdentifier(param.name) || param.dotDotDotToken) return null; + const symbol = lowerer.checker.getSymbolAtLocation(param.name); + if (!symbol || lowerer.stageSourceTypes.has(symbol)) return null; + const declared = lowerer.paramShape(param).type; + if (declared.kind !== "func" || declared.rest || declared.params[1]?.kind !== "dyn") return null; + const data = unionOf(lowerer, [BYTES, STRING, { kind: "undefinedT" }]); + lowerer.stageSourceTypes.set( + symbol, + funcOf([declared.params[0]!, data, ...declared.params.slice(2)], declared.ret), + ); + return symbol; +} + /** Parses the options object literal of a stream constructor. `which` * names the callbacks this class accepts, in canonical flag order. */ function parseStreamOptions( @@ -671,22 +699,27 @@ function parseStreamOptions( continue; } const full = tuples[name]!.map((t) => (t === "fn" ? null : t)); - // The completion-callback tail: typed by the checker contextually; - // accept the user's declared func type as the tuple's entry so the - // prefix check compares like with like. - const { funcType } = lowerer.lambdaSignature(fnNode); - const tuple: IrType[] = full.map((t, i) => { - if (t !== null) return t; - const declared = funcType.params[i]; - if (declared !== undefined && declared.kind !== "func" && declared.kind !== "dyn") { - lowerer.noLowering( - `a '${name}' callback whose parameter ${i} is '${lowerer.fmt(declared)}' where Node passes the completion callback`, - fnNode!, - ); - } - return declared ?? funcOf([], VOID); - }); - present.set(name, lowerStreamCallback(lowerer, name, fnNode, thisType, tuple)); + const pinned = pinAnyDataCompletion(lowerer, name, fnNode); + try { + // The completion-callback tail: typed by the checker contextually; + // accept the user's declared func type as the tuple's entry so the + // prefix check compares like with like. + const { funcType } = lowerer.lambdaSignature(fnNode); + const tuple: IrType[] = full.map((t, i) => { + if (t !== null) return t; + const declared = funcType.params[i]; + if (declared !== undefined && declared.kind !== "func" && declared.kind !== "dyn") { + lowerer.noLowering( + `a '${name}' callback whose parameter ${i} is '${lowerer.fmt(declared)}' where Node passes the completion callback`, + fnNode!, + ); + } + return declared ?? funcOf([], VOID); + }); + present.set(name, lowerStreamCallback(lowerer, name, fnNode, thisType, tuple)); + } finally { + if (pinned) lowerer.stageSourceTypes.delete(pinned); + } continue; } // `{ signal }` shorthand: the binding is the value. @@ -2044,6 +2077,24 @@ function lowerStreamArg( const v = isHashValueType(lowered.type) ? hashStreamOf(lowered, what === "pipeline" || what === "compose") : lowered; + // A checked-dynamic pipeline stage (an untyped source or destination): + // a boxed native stream unwraps at run time; other values (iterables, + // functions) throw the not-supported Error there. + if (v.type.kind === "dyn" && what === "pipeline") { + return { + kind: "libCall", + fn: "stream.unboxDyn", + args: [ + v, + strLit( + "pipeline stages other than native streams (iterables, generators, plain functions, and { readable, writable } pairs) are not supported yet", + locOf(node), + ), + ], + type: { kind: "object", className: "%Duplex" }, + loc: locOf(node), + }; + } const info = v.type.kind === "object" ? lowerer.classes.get(v.type.className) : undefined; if (!streamSidesOf(lowerer, info)) { // A provably-non-stream value in a JS source (the invalid-input @@ -3296,6 +3347,124 @@ function pipeEndOption(lowerer: Lowerer, options: ts.Expression | undefined, loc return end; } +/** `readable.pipe(process.stdout)`: the process stream joins the pipe as + * an fd-backed Writable (stream.stdioWritable), and — like Node, which + * never ends process.stdout or process.stderr from a pipe — the source's + * end does not end it. pipe() answers the process stream itself. */ +function pipeIntoStdio( + lowerer: Lowerer, + call: ts.CallExpression, + access: ts.PropertyAccessExpression, + target: IrExpr, +): IrExpr { + const loc = locOf(call); + // JS order: the receiver, then the destination, then the options. + const receiverValue = lowerer.lowerExpr(access.expression); + const source = lowerer.declareHiddenLocal("%pipeSource", receiverValue.type); + const stdio = lowerer.declareHiddenLocal("%pipeStdio", target.type); + const options = call.arguments.nodes[1]; + const stmts: IrStmt[] = [ + { kind: "varDecl", localId: source.id, init: receiverValue, loc }, + { kind: "varDecl", localId: stdio.id, init: target, loc }, + ]; + // The options literal still evaluates; its end flag has no effect here. + if (options !== undefined) { + stmts.push({ kind: "exprStmt", expr: pipeEndOption(lowerer, options, loc), loc }); + } + const receiver: IrExpr = { kind: "varRef", localId: source.id, type: receiverValue.type, loc }; + const writable: IrType = { kind: "object", className: "%Writable" }; + stmts.push({ + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "readable.pipe", + args: [ + receiver, + { + kind: "libCall", + fn: "stream.stdioWritable", + args: [{ kind: "varRef", localId: stdio.id, type: target.type, loc }], + type: writable, + loc, + }, + boolLit(false, loc), + ], + type: writable, + loc, + }, + loc, + }); + return { + kind: "seqExpr", + stmts, + result: { kind: "varRef", localId: stdio.id, type: target.type, loc }, + type: target.type, + loc, + }; +} + +/** The pipe() libCall for a destination of type `dst`. Under the general + * stream representation a Writable slot can hold the converted + * process.stdout / process.stderr, which a pipe never ends (as in Node), so + * the pipe takes readable.pipeGeneral, which checks for them. */ +function generalPipe(lowerer: Lowerer, dst: IrType): "readable.pipe" | "readable.pipeGeneral" { + return lowerer.generalNodeStreams && dst.kind === "object" && dst.className === "%Writable" + ? "readable.pipeGeneral" + : "readable.pipe"; +} + +/** `readable.pipe(untyped)`: a checked-dynamic destination unwraps a boxed + * native stream at run time (stream.unboxDyn; other values throw the + * not-supported Error), and pipe() answers the destination value itself. */ +function pipeIntoDyn( + lowerer: Lowerer, + call: ts.CallExpression, + access: ts.PropertyAccessExpression, + target: IrExpr, +): IrExpr { + const loc = locOf(call); + const receiverValue = lowerer.lowerExpr(access.expression); + const source = lowerer.declareHiddenLocal("%pipeSource", receiverValue.type); + const destination = lowerer.declareHiddenLocal("%pipeDestination", target.type); + const stmts: IrStmt[] = [ + { kind: "varDecl", localId: source.id, init: receiverValue, loc }, + { kind: "varDecl", localId: destination.id, init: target, loc }, + ]; + const end = pipeEndOption(lowerer, call.arguments.nodes[1], loc); + const duplex: IrType = { kind: "object", className: "%Duplex" }; + stmts.push({ + kind: "exprStmt", + expr: { + kind: "libCall", + fn: lowerer.generalNodeStreams ? "readable.pipeGeneral" : "readable.pipe", + args: [ + { kind: "varRef", localId: source.id, type: receiverValue.type, loc }, + { + kind: "libCall", + fn: "stream.unboxDyn", + args: [ + { kind: "varRef", localId: destination.id, type: target.type, loc }, + strLit("pipe destinations other than native streams are not supported yet", loc), + ], + type: duplex, + loc, + }, + end, + ], + type: duplex, + loc, + }, + loc, + }); + return { + kind: "seqExpr", + stmts, + result: { kind: "varRef", localId: destination.id, type: target.type, loc }, + type: target.type, + loc, + }; +} + export function lowerStreamMethodCall( lowerer: Lowerer, call: ts.CallExpression, @@ -3545,22 +3714,26 @@ export function lowerStreamMethodCall( loc, }; } - const dst = lowerer.lowerExpr(args[0]!); + const target = lowerer.lowerExpr(args[0]!); + if (target.type.kind === "procStream") return pipeIntoStdio(lowerer, call, access, target); + if (target.type.kind === "dyn") return pipeIntoDyn(lowerer, call, access, target); + const dst = target; const dstInfo = dst.type.kind === "object" ? lowerer.classes.get(dst.type.className) : undefined; const dstSides = streamSidesOf(lowerer, dstInfo); if (dstSides !== "w" && dstSides !== "rw") { + requestGeneralStreamSurface(lowerer, args[0]!); lowerer.noLowering( `pipe into a '${lowerer.fmt(dst.type)}'`, args[0]!, - "the destination must be a stream Writable/Duplex (process streams and sockets are not pipe destinations yet)", + "the destination must be a stream Writable/Duplex, process.stdout or process.stderr (sockets are not pipe destinations yet)", ); } const end = pipeEndOption(lowerer, args[1], loc); const receiver = lowerer.lowerExpr(access.expression); return { kind: "libCall", - fn: "readable.pipe", + fn: generalPipe(lowerer, dst.type), args: [receiver, dst, end], type: dst.type, loc, diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index fea001826..28a864afb 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -1,5 +1,8 @@ +import { onlyForwarded } from "./typed-arguments.js"; +import { needsGeneralNodeStreams, stdioIntoGeneralStream } from "./general-streams.js"; import { extendAssertedRecordMembers } from "./assertion-members.js"; import { jsDynamicDeclarations } from "./js-dynamic-objects.js"; +import { builtinClassValue, builtinMemberClassKind } from "./builtins/builtin-class-dyn.js"; import { lifetimeRegionType, lowerLifetimeRegionCall, @@ -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 | 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(); + /** Function expressions lowered against a fixed target signature with a + * tuple-typed `arguments` (typed-arguments.ts). */ + readonly typedArgumentsLambdas = new Set(); + /** Const bindings holding a forwarded fixed tuple (`const args = + * arguments as any` in a typed-arguments function) → the tuple type. */ + readonly tupleAliasSymbols = new Map(); + /** `??` expressions whose result is the optional callback's own function + * type (an adapted function default); their bindings take that type. */ + readonly nullishFuncResults = new Set(); + /** The static names of the current generic instance's name-keyed + * function parameters (static-function-names.ts). */ + staticFnNames: Map | null = null; /** Nesting depth of erased generic bodies being lowered (erased lambdas * and erased instances). */ erasingDepth = 0; @@ -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`. */ + * it before the checker's `AsyncIterable`. A stream option callback's + * completion parameter declared with an any-typed `data` is pinned here + * too while the callback lowers (pinAnyDataCompletion). */ readonly stageSourceTypes = new Map(); /** Set when a match result lowers to execGroups/matchGroups, the inputs * of regex span folding. */ @@ -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 `; 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", diff --git a/packages/compiler/src/frontend/lowering/static-function-names.ts b/packages/compiler/src/frontend/lowering/static-function-names.ts new file mode 100644 index 000000000..d86e55a80 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/static-function-names.ts @@ -0,0 +1,219 @@ +import * as ts from "../ts7/adapter.js"; +import { isJsSourceFile } from "../program.js"; +import type { Lowerer } from "./lowerer.js"; +import type { GenericFnInfo } from "./generic-functions.js"; +import { stripTypeWrappers } from "./typed-arguments.js"; + +/** `fn.name` inside generic higher-order functions. + * + * Native closures carry no name, but a generic wrapper is instantiated per + * call site anyway: when its body reads `.name` of a function parameter, + * the instantiation key also carries the static name of that call's + * argument (the function declaration's name, a const binding's inferred + * name, the name of the function a wrapper call returns), and the read + * folds to that string. A call whose argument's name is only known at run + * time keeps a precise refusal at the read. */ + +/** Parameter index → symbol for the parameters whose `.name` the generic + * body reads (and never reassigns). Computed once per generic. */ +export function nameReadParams(lowerer: Lowerer, info: GenericFnInfo): Map { + if (info.nameReadParams) return info.nameReadParams; + const out = new Map(); + // JavaScript generics read function names through the checked-value + // box, which carries the runtime name already. + if (isJsSourceFile(info.decl.getSourceFile())) { + info.nameReadParams = out; + return out; + } + const bySymbol = new Map(); + info.decl.parameters.nodes.forEach((param, i) => { + if (!ts.isIdentifier(param.name) || param.dotDotDotToken) return; + if (!functionTypedParameter(lowerer, param)) return; + const symbol = lowerer.checker.getSymbolAtLocation(param.name); + if (symbol) bySymbol.set(symbol, i); + }); + if (bySymbol.size > 0) { + const written = new Set(); + const read = new Set(); + ts.walkPreorder(info.decl, (node) => { + if (ts.isTypeNode(node)) return "skip"; + if ( + ts.isPropertyAccessExpression(node) && + node.name.text === "name" && + ts.isIdentifier(node.expression) + ) { + const symbol = lowerer.checker.getSymbolAtLocation(node.expression); + if (symbol && bySymbol.has(symbol)) read.add(symbol); + } + if ( + ts.isBinaryExpression(node) && + node.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && + node.operatorToken.kind <= ts.SyntaxKind.LastAssignment && + ts.isIdentifier(node.left) + ) { + const symbol = lowerer.checker.getSymbolAtLocation(node.left); + if (symbol) written.add(symbol); + } + return undefined; + }); + for (const symbol of read) { + if (!written.has(symbol)) out.set(bySymbol.get(symbol)!, symbol); + } + } + info.nameReadParams = out; + return out; +} + +/** A parameter declared with a function type, or with a type parameter + * constrained to one (`fn: F` where `F extends (...args: any) => any`). */ +function functionTypedParameter(lowerer: Lowerer, param: ts.ParameterDeclaration): boolean { + const t = lowerer.typeOf(param.name); + if (lowerer.checker.getCallSignatures(t).length > 0) return true; + if (!(t.flags & ts.TypeFlags.TypeParameter)) return false; + const symbol = t.getSymbol(); + const decl = symbol ? symbol.declarations.find(ts.isTypeParameterDeclaration) : undefined; + if (!decl?.constraint) return false; + const constraint = lowerer.checker.getTypeFromTypeNode(decl.constraint); + return lowerer.checker.getCallSignatures(constraint).length > 0; +} + +/** The instantiation key suffix and name bindings of one call of `info`, + * or null when the body reads no parameter's name. */ +export function staticNameBindings( + lowerer: Lowerer, + call: ts.CallExpression, + info: GenericFnInfo, +): { key: string; names: Map } | null { + const params = nameReadParams(lowerer, info); + if (params.size === 0) return null; + const names = new Map(); + const parts: string[] = []; + for (const [index, symbol] of params) { + const arg = call.arguments.nodes[index]; + const name = arg && !ts.isSpreadElement(arg) ? staticFunctionName(lowerer, arg) : null; + names.set(symbol, name); + parts.push(name === null ? "?" : JSON.stringify(name)); + } + return { key: `@name(${parts.join(",")})`, names }; +} + +/** The `name` JavaScript gives the function `expr` evaluates to, when it is + * statically known; null otherwise. */ +export function staticFunctionName(lowerer: Lowerer, expr: ts.Expression): string | null { + return staticNameOf(lowerer, expr, new Set()); +} + +function staticNameOf(lowerer: Lowerer, expr: ts.Expression, seen: Set): string | null { + const e = stripTypeWrappers(expr); + if (seen.has(e)) return null; + seen.add(e); + // An anonymous function as a call argument gets no inferred name. + if (ts.isFunctionExpression(e) || ts.isArrowFunction(e)) { + return !ts.isArrowFunction(e) && e.name ? e.name.text : ""; + } + if (ts.isIdentifier(e)) { + const symbol = lowerer.resolveValueSymbol(e); + if (!symbol) return null; + const bound = lowerer.staticFnNames?.get(symbol); + if (bound !== undefined) return bound; + const decl = symbol.valueDeclaration; + if (decl === undefined) return null; + if (ts.isFunctionDeclaration(decl)) return decl.name ? decl.name.text : "default"; + if ( + ts.isVariableDeclaration(decl) && + ts.isIdentifier(decl.name) && + decl.initializer !== undefined && + (ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) !== 0 + ) { + const init = stripTypeWrappers(decl.initializer); + // `const f = () => …` / `const f = function () {}`: the binding names + // an anonymous function definition. + if (ts.isArrowFunction(init)) return decl.name.text; + if (ts.isFunctionExpression(init)) return init.name ? init.name.text : decl.name.text; + return staticNameOf(lowerer, init, seen); + } + return null; + } + if (ts.isCallExpression(e) && ts.isIdentifier(e.expression)) { + // A call of a generic higher-order function returns the function its + // body returns: every `return` names the same anonymous definition or + // the same local const binding. + const symbol = lowerer.resolveValueSymbol(e.expression); + const info = symbol ? lowerer.genericFnsBySymbol.get(symbol) : undefined; + if (!info) return null; + return returnedFunctionName(lowerer, info.decl, seen); + } + if (ts.isPropertyAccessExpression(e)) { + const symbol = lowerer.checker.getSymbolAtLocation(e.name); + const decl = symbol ? symbol.valueDeclaration : undefined; + if (decl && ts.isMethodDeclaration(decl) && ts.isIdentifier(decl.name)) return decl.name.text; + return null; + } + return null; +} + +function returnedFunctionName( + lowerer: Lowerer, + decl: GenericFnInfo["decl"], + seen: Set, +): string | null { + const body = decl.body; + if (body === undefined) return null; + if (!ts.isBlock(body)) return functionValueName(lowerer, body, seen); + let name: string | null | undefined; + let ok = true; + ts.walkPreorder(body, (node) => { + if (!ok) return "stop"; + if (ts.isFunctionLike(node)) return "skip"; + if (!ts.isReturnStatement(node)) return undefined; + const here = node.expression ? functionValueName(lowerer, node.expression, seen) : null; + if (here === null || (name !== undefined && name !== here)) ok = false; + else name = here; + return undefined; + }); + return ok && name !== undefined ? name : null; +} + +/** The name of a returned function value: an anonymous definition returned + * directly is nameless; a local const binding names its definition. */ +function functionValueName( + lowerer: Lowerer, + expr: ts.Expression, + seen: Set, +): string | null { + const e = stripTypeWrappers(expr); + if (ts.isFunctionExpression(e) || ts.isArrowFunction(e)) { + return !ts.isArrowFunction(e) && e.name ? e.name.text : ""; + } + if (ts.isIdentifier(e)) return staticNameOf(lowerer, e, seen); + return null; +} + +/** `fn.name` where `fn` is a name-keyed parameter of the generic instance + * being lowered: the folded name, or a precise refusal when this call + * site's argument has no static name. Null for every other read. */ +export function staticFunctionNameRead( + lowerer: Lowerer, + expr: ts.PropertyAccessExpression, +): string | null { + const names = lowerer.staticFnNames; + if ( + names === null || + expr.name.text !== "name" || + expr.questionDotToken !== undefined || + !ts.isIdentifier(expr.expression) + ) { + return null; + } + const symbol = lowerer.checker.getSymbolAtLocation(expr.expression); + if (!symbol || !names.has(symbol)) return null; + const name = names.get(symbol); + if (name === null || name === undefined) { + lowerer.unsupported( + "SC1090", + expr, + `reading '${expr.expression.text}.name' when the call passes a function whose name is only known at run time (generic wrappers fold the name of a function declaration, a const-bound function, or a wrapper call's result)`, + ); + } + return name; +} diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index bf545024b..be2207bcd 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -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). diff --git a/packages/compiler/src/frontend/lowering/typed-arguments.ts b/packages/compiler/src/frontend/lowering/typed-arguments.ts new file mode 100644 index 000000000..ebddcafb3 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/typed-arguments.ts @@ -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`) 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; +} diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 99de76b4c..0bb31ad03 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -980,7 +980,7 @@ function isDeclaredInAmbientModule(d: ts.Declaration, name: string): boolean { /** True when the declaration's nearest enclosing namespace is `name` * (`declare global { namespace NodeJS { ... } }` — the NodeJS-global * interfaces @types/node declares outside any ambient module). */ -function isDeclaredInAmbientNamespace(d: ts.Declaration, name: string): boolean { +export function isDeclaredInAmbientNamespace(d: ts.Declaration, name: string): boolean { let node: ts.Node | undefined = d.parent; while (node) { if (ts.isModuleDeclaration(node) && ts.isIdentifier(node.name)) { @@ -991,6 +991,31 @@ function isDeclaredInAmbientNamespace(d: ts.Declaration, name: string): boolean return false; } +/** Whether `type` is one of the stream spellings the general stream + * representation (TypeMapperCtx.generalNodeStreams) maps differently: + * NodeJS.ReadableStream / NodeJS.WritableStream, @types/node's + * stream.Readable / stream.Writable, or the fallback declarations' + * ChildStdin. */ +export function isGeneralNodeStreamInterface( + type: ts.Type, + ctx: Pick, +): boolean { + const psym = type.getSymbol() ?? type.getAliasSymbol(); + if (psym === undefined) return false; + return psym.declarations.some((d) => { + if (!(ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d))) return false; + if (!ctx.isStdlibFile(d.getSourceFile())) return false; + if (psym.name === "ReadableStream" || psym.name === "WritableStream") + return ts.isInterfaceDeclaration(d) && isDeclaredInAmbientNamespace(d, "NodeJS"); + if (psym.name === "ChildStdin") return isDeclaredInAmbientModule(d, "child_process"); + return ( + (psym.name === "Readable" || psym.name === "Writable") && + isNodeTypesPath(d.getSourceFile().fileName) && + isDeclaredInAmbientModule(d, "stream") + ); + }); +} + /** A type declared in the shipped WebAssembly namespace (scriptc.d.ts): * its modules, instances, memories, tables, globals, error classes and the * Exports/Imports records. */ @@ -1146,6 +1171,13 @@ export interface TypeMapperCtx { * type descriptions for project-owned values, but their imported runtime * bindings are fenced separately by the Lowerer. */ isExternalTypeFile: (sf: ts.SourceFile) => boolean; + /** The program's general stream representation (lowerToIr decides it + * per program): NodeJS.ReadableStream / NodeJS.WritableStream and + * @types/node's stream.Readable / stream.Writable map to the runtime + * stream classes instead of the child/process stdio handles, so values + * from every stream producer share their slots. Off (the default), the + * stdio handles keep those spellings. */ + generalNodeStreams?: boolean; /** Successful context-free mappings, keyed by the * TypeScript server's stable type id. A generated facade can reference the * same large record/function type thousands of times; remapping its full @@ -1817,6 +1849,16 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { contextResolutions++; return viaIndexed; } + // `Parameters` / `ReturnType` over a bound function-typed + // parameter (variadic higher-order wrappers): the checker keeps both + // conditionals symbolic inside the body, and their `infer` positions + // match a call signature, which matchInferReference cannot decide. + const viaSignature = + mapBoundSignatureAlias(type, ctx) ?? mapBoundKeyofNeverConditional(type, ctx); + if (viaSignature !== null) { + contextResolutions++; + return viaSignature; + } // A conditional type over a bound parameter (`Infer`, a // mapped type's member): evaluate it against the bindings. if (type.isConditionalType()) { @@ -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` 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(); @@ -5039,6 +5170,167 @@ function mapBoundIndexedAccess(type: ts.Type, ctx: TypeMapperCtx): IrType | null return mapType(checker.getTypeOfSymbol(prop), ctx); } +/** The standard library's `Parameters` / `ReturnType` over a type + * parameter the current instantiation binds to a callable checker type: + * that type's one call signature. Concrete arguments never reach here (the + * checker evaluates those conditionals itself); generic bodies keep them + * symbolic because their `infer` sits in a call signature. */ +function boundSignatureAlias( + type: ts.Type, + ctx: TypeMapperCtx, +): { name: "Parameters" | "ReturnType"; sig: ts.Signature } | null { + const { checker, resolveTypeParamTs } = ctx; + if (!resolveTypeParamTs) return null; + const alias = type.getAliasSymbol(); + if (!alias || (alias.name !== "Parameters" && alias.name !== "ReturnType")) return null; + const aliasArgs = type.getAliasTypeArguments(); + const arg = aliasArgs[0]; + if (aliasArgs.length !== 1 || !arg || !(arg.flags & ts.TypeFlags.TypeParameter)) return null; + const isLibAlias = alias.declarations.some( + (d) => ts.isTypeAliasDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()), + ); + if (!isLibAlias) return null; + const bound = resolveTypeParamTs(arg); + if (!bound || bound.flags & ts.TypeFlags.TypeParameter) return null; + const sigs = checker.getCallSignatures(bound); + const sig = sigs[0]; + if (sigs.length !== 1 || !sig || sig.getTypeParameters().length > 0) return null; + return { name: alias.name, sig }; +} + +/** One position of a fixed-arity parameter tuple. An optional position + * still has a native slot (the function type's `T | undefined` slot). */ +interface TupleElement { + type: ts.Type; + optional: boolean; +} + +/** The positional parameters of a signature as a fixed tuple: a trailing + * rest flattened when its own type is a fixed tuple. Null for a variadic + * position, which has no fixed native arity. */ +function signatureParameters(sig: ts.Signature, ctx: TypeMapperCtx): TupleElement[] | null { + const { checker } = ctx; + const out: TupleElement[] = []; + for (const p of sig.getParameters()) { + const decl = p.valueDeclaration; + const type = checker.getTypeOfSymbol(p); + if (decl !== undefined && ts.isParameter(decl)) { + if (decl.dotDotDotToken !== undefined) { + const inner = restTupleElements(type, ctx); + if (!inner) return null; + out.push(...inner); + continue; + } + out.push({ + type, + optional: decl.questionToken !== undefined || decl.initializer !== undefined, + }); + continue; + } + out.push({ type, optional: (p.flags & ts.SymbolFlags.Optional) !== 0 }); + } + return out; +} + +/** The required-only positional parameter types of a signature (the + * value-level `Parameters` tuple), null when a position is optional + * or variadic: such a tuple's length varies per call. */ +function signatureParameterTypes(sig: ts.Signature, ctx: TypeMapperCtx): ts.Type[] | null { + const elems = signatureParameters(sig, ctx); + if (!elems || elems.some((e) => e.optional)) return null; + return elems.map((e) => e.type); +} + +/** The element types of a REST parameter's type when it is a fixed tuple: + * a tuple of required elements (`[number, string]`, the empty `[]`), a type + * parameter bound to one, or `Parameters` over a bound function type. + * Such a rest parameter has a compile-time arity, so its native ABI is the + * flattened positional list (exactly the equivalent fixed signature — + * TypeScript treats `(...a: [A, B]) => R` and `(a: A, b: B) => R` alike). */ +export function restTupleElementTypes(type: ts.Type, ctx: TypeMapperCtx): ts.Type[] | null { + return restTupleElements(type, ctx)?.map((e) => e.type) ?? null; +} + +/** restTupleElementTypes with each position's optionality: a rest tuple + * may end in optional positions (`[A, B?]`, `Parameters<(a: A, b?: B) => + * R>`), which flatten to `T | undefined` slots exactly as the function + * type's optional parameters do. */ +function restTupleElements(type: ts.Type, ctx: TypeMapperCtx): TupleElement[] | null { + const { checker } = ctx; + if (type.flags & ts.TypeFlags.TypeParameter) { + const bound = ctx.resolveTypeParamTs?.(type); + return bound && bound !== type ? restTupleElements(bound, ctx) : null; + } + const viaAlias = boundSignatureAlias(type, ctx); + if (viaAlias) { + return viaAlias.name === "Parameters" ? signatureParameters(viaAlias.sig, ctx) : null; + } + if (!checker.isTupleType(type)) return null; + const ref = type as ts.TupleTypeReference; + const tupleShape = + (ref.elementFlags as ts.ElementFlags[] | undefined) !== undefined + ? ref + : (ref.getTarget() as ts.TupleType | undefined); + const args = checker.getTypeArguments(ref); + if (args.length === 0) return []; + const flags = tupleShape?.elementFlags; + if (flags === undefined) return null; + if (flags.some((f) => !(f & (ts.ElementFlags.Required | ts.ElementFlags.Optional)))) return null; + return args.map((t, i) => ({ + type: t, + optional: (flags[i]! & ts.ElementFlags.Optional) !== 0, + })); +} + +/** One tuple element's IR type under the tuple mapping's rules. */ +function mapTupleElement(t: ts.Type, ctx: TypeMapperCtx): IrType | null { + let et = (t.flags & ts.TypeFlags.Any) !== 0 ? DYN : mapType(t, ctx); + if (et?.kind === "void" && isUnitOnlyTsType(t, ctx.resolveTypeParam)) + et = unitOnlyUnion(ctx.unions); + return !et || et.kind === "void" ? null : et; +} + +/** The mapped element types of a fixed-tuple rest parameter type + * (restTupleElementTypes), null when it is not one or an element does not + * map. */ +export function restTupleParamTypes(type: ts.Type, ctx: TypeMapperCtx): IrType[] | null { + const elems = restTupleElements(type, ctx); + if (!elems) return null; + const out: IrType[] = []; + for (const e of elems) { + let et = mapTupleElement(e.type, ctx); + // An optional position's slot holds undefined when the caller omits + // it, as an optional parameter's does (mapCallSignature). + if (et && e.optional && et.kind !== "dyn") et = withUndefinedArm(et, ctx.unions); + if (!et) return null; + out.push(et); + } + return out; +} + +/** `Parameters` (the fixed tuple of F's parameters, interned exactly as + * the spelled tuple maps) and `ReturnType` (F's mapped result) over a + * bound function-typed type parameter. */ +function mapBoundSignatureAlias(type: ts.Type, ctx: TypeMapperCtx): IrType | null { + const target = boundSignatureAlias(type, ctx); + if (!target) return null; + if (target.name === "ReturnType") { + const ret = ctx.checker.getReturnTypeOfSignature(target.sig); + return ret.flags & ts.TypeFlags.Any ? DYN : mapType(ret, ctx); + } + const elems = signatureParameterTypes(target.sig, ctx); + if (!elems) return null; + if (elems.length === 0) return arrayOf(unitOnlyUnion(ctx.unions)); + const fields: { name: string; type: IrType }[] = []; + for (let i = 0; i < elems.length; i++) { + const et = mapTupleElement(elems[i]!, ctx); + if (!et) return null; + fields.push({ name: String(i), type: et }); + } + fields.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0)); + return { kind: "record", shapeId: ctx.shapes.intern(fields, true) }; +} + /** `Partial` / `Readonly` where T is a STILL-GENERIC type parameter — * the one mapped-type spelling a monomorphized body can contain that the * checker cannot resolve for us (`keyof T` is unknown inside the body, so @@ -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>` (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` 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`, 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 | false | undefined { + const { checker } = ctx; + const wantSigs = checker.getCallSignatures(extendsType); + const want = wantSigs[0]; + if (wantSigs.length !== 1 || !want || want.getTypeParameters().length > 0) return undefined; + const wantShape = signatureShape(want, ctx); + if (wantShape === undefined) return undefined; + const isAny = (t: ts.Type): boolean => (t.flags & ts.TypeFlags.Any) !== 0; + const isAnyArray = (t: ts.Type): boolean => + checker.isArrayType(t) && isAny(checker.getTypeArguments(t as ts.TypeReference)[0]!); + if (!wantShape.params.every((p) => isAny(p.type) || isAnyArray(p.type))) return undefined; + if (wantShape.rest !== null && !isAny(wantShape.rest)) return undefined; + if (!(checked.flags & ts.TypeFlags.Object)) return false; + const gotSigs = checker.getCallSignatures(checked); + const got = gotSigs[0]; + if (gotSigs.length !== 1 || !got || got.getTypeParameters().length > 0) return undefined; + const gotShape = signatureShape(got, ctx); + if (gotShape !== undefined) { + // `(a: any) => infer R` rejects `(a: string, b: string) => R` and + // `(...args: [string, number]) => R`: they need more arguments. + if (wantShape.rest === null && requiredArguments(gotShape) > wantShape.params.length) + return false; + for (let i = 0; i < wantShape.params.length; i++) { + if (!isAnyArray(wantShape.params[i]!.type)) continue; + const param = gotShape.params[i]?.type ?? gotShape.rest; + if (param === undefined || param === null) continue; + const accepts = acceptsArray(param, ctx); + if (accepts !== true) return accepts; + } + } else if (wantShape.params.some((p) => isAnyArray(p.type))) { + return undefined; + } + const wantRet = checker.getReturnTypeOfSignature(want); + const infer = inferSymbolOf(wantRet, ctx); + if (infer) return new Map([[infer, checker.getReturnTypeOfSignature(got)]]); + return wantRet.flags & ts.TypeFlags.Any ? new Map() : undefined; +} + +/** A signature's parameters as a call sees them: the positional ones, with + * a fixed-tuple rest (`...args: [A, B?]`) flattened into positions, and the + * element type of an array rest (`any` for `...args: any`). Undefined when + * a parameter has no declaration, or the rest is a variadic tuple or + * another type. */ +function signatureShape( + sig: ts.Signature, + ctx: TypeMapperCtx, +): { params: { type: ts.Type; optional: boolean }[]; rest: ts.Type | null } | undefined { + const { checker } = ctx; + const params: { type: ts.Type; optional: boolean }[] = []; + for (const p of sig.getParameters()) { + const decl = p.valueDeclaration; + if (decl === undefined || !ts.isParameter(decl)) return undefined; + const type = checker.getTypeOfSymbol(p); + if (decl.dotDotDotToken === undefined) { + params.push({ + type, + optional: decl.questionToken !== undefined || decl.initializer !== undefined, + }); + continue; + } + if (type.flags & ts.TypeFlags.Any) return { params, rest: type }; + if (checker.isArrayType(type)) + return { params, rest: checker.getTypeArguments(type as ts.TypeReference)[0]! }; + const elems = restTupleElements(type, ctx); + if (!elems) return undefined; + for (const e of elems) params.push({ type: e.type, optional: e.optional }); + return { params, rest: null }; + } + return { params, rest: null }; +} + +/** The fewest arguments a call must pass: up to the last required position. */ +function requiredArguments(shape: { params: { optional: boolean }[] }): number { + let required = 0; + shape.params.forEach((p, i) => { + if (!p.optional) required = i + 1; + }); + return required; +} + +/** Whether a parameter of this type accepts an `any[]` argument: true for + * `any`, `unknown` and array types, false for primitives, undefined + * otherwise. */ +function acceptsArray(t: ts.Type, ctx: TypeMapperCtx): boolean | undefined { + if (t.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) return true; + if (ctx.checker.isArrayType(t)) return true; + const primitive = + ts.TypeFlags.StringLike | + ts.TypeFlags.NumberLike | + ts.TypeFlags.BooleanLike | + ts.TypeFlags.BigIntLike | + ts.TypeFlags.ESSymbolLike | + ts.TypeFlags.Undefined | + ts.TypeFlags.Null | + ts.TypeFlags.Void; + return t.flags & primitive ? false : undefined; +} + +/** `checked extends PromiseLike` (or `Promise`) over the + * standard library's Promise: U binds to the settled type. An object with + * no `then` member is no thenable. Everything else stays undecided. */ +function matchInferThenable( + checked: ts.Type, + extendsType: ts.Type, + ctx: TypeMapperCtx, +): Map | false | undefined { + const { checker } = ctx; + const stdlibNamed = (t: ts.Type, names: readonly string[]): boolean => { + const symbol = t.getSymbol(); + return ( + symbol !== undefined && + names.includes(symbol.name) && + symbol.declarations.some((d) => ctx.isStdlibFile(d.getSourceFile())) + ); + }; + if (!stdlibNamed(extendsType, ["PromiseLike", "Promise"])) return undefined; + const wantArgs = checker.getTypeArguments(extendsType as ts.TypeReference); + const infer = wantArgs.length === 1 ? inferSymbolOf(wantArgs[0]!, ctx) : undefined; + if (!infer) return undefined; + if (stdlibNamed(checked, ["Promise", "PromiseLike"])) { + const gotArgs = checker.getTypeArguments(checked as ts.TypeReference); + return gotArgs.length === 1 ? new Map([[infer, gotArgs[0]!]]) : undefined; + } + return checker.getPropertyOfType(checked, "then") === undefined ? false : undefined; +} + /** The type mentions a mapped key or bound alias parameter (a template * shape evalMappedTemplate does not reduce). */ function mentionsMappedParams( @@ -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` 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 { TZ?: string }` and every spread of it, or a declared + * environment `{ CI?: string } & Dictionary`. At runtime such a + * member is just another key, so the shape interns as the pure + * index-signature record. Its slot admits undefined, as the optional + * members do (`env.CI = undefined` keeps the key). A hybrid layout would + * give each member a struct slot, which enumerates before the overflow + * keys (Node keeps one insertion order) and makes `delete` and spread + * answer differently for declared and undeclared keys. Required members, + * and members typed otherwise than the slot (whose reads the slot type + * could not give back), keep the hybrid shape. */ +function coveredOptionalFieldsShape( + widened: ts.Type, + parts: RecordShapeParts, + ctx: TypeMapperCtx, +): string | null { + const slot = parts.indexValue; + if (!slot || parts.fields.length === 0) return null; + const { checker, shapes, unions } = ctx; + // The members' common type must be the slot plus undefined. Read it off + // the members (already interned) rather than building it: a hybrid that + // does not qualify must not intern anything new. + const optionalSlot = parts.fields[0]!.type; + if (!parts.fields.every((field) => typeEquals(field.type, optionalSlot))) return null; + if (optionalSlot.kind !== "union" || unions.armTag(optionalSlot.unionId, UNDEFINED_T) < 0) + return null; + const memberArms = unions.get(optionalSlot.unionId)?.arms ?? []; + const slotArms = + slot.kind === "union" ? (unions.get(slot.unionId)?.arms ?? []) : isUnitType(slot) ? [] : [slot]; + const definedArms = memberArms.filter((arm): boolean => arm.kind !== "undefinedT"); + const slotDefined = slotArms.filter((arm): boolean => arm.kind !== "undefinedT"); + if ( + definedArms.length === 0 || + definedArms.length !== slotDefined.length || + !slotDefined.every((arm) => definedArms.some((other) => typeEquals(arm, other))) + ) + return null; + if (!isSupportedIndexValue(optionalSlot)) return null; + const props = checker.getPropertiesOfType(widened); + if (props.length !== parts.fields.length) return null; + if (!props.every((p) => (p.flags & ts.SymbolFlags.Optional) !== 0)) return null; + return shapes.intern([], false, optionalSlot, []); +} + /** The type of an object literal that spreads an index-signature record * (`{ ...tags, bucket: b, status }`, the metric-tags idiom). The checker * drops the signature and types the literal by its explicit members only, diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index e4bad6ed9..7b1ef7109 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -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 ? {} diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index 30cd32a47..4e6577e47 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -75,6 +75,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "process.stdoutWriteBytes", "process.stderrWriteBytes", "procStream.write", + "child.stdioStream", + "stream.unboxDyn", "weakMap.new", "weakSet.new", "dyn.fromEntries", @@ -346,6 +348,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "stream.onDyn", "readable.resume", "readable.pipe", + "readable.pipeGeneral", "readable.unpipe", "writable.write", "writable.writeStr", @@ -389,6 +392,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "perf.entries", "perf.clearMarks", "perf.clearMeasures", + // monitorEventLoopDelay's resolution: Node's ERR_OUT_OF_RANGE. + "perf.eldNew", // Non-TTY stdin: Node's stdin has no setRawMode member, so the call is // the exact catchable TypeError. "process.stdinSetRawMode", @@ -581,6 +586,9 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "dyn.objCreateWithProperties", "dyn.getPrototype", "dyn.setPrototype", + // a proxy in the prototype chain can throw + "dyn.builtinInstanceOf", + "dyn.functionInstanceOf", "dyn.getOwnPropertyNames", "dyn.ownKeys", "dyn.getOwnPropertySymbols", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index f44e90867..9732fb62d 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -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 }, diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 5aafc2405..b52afafcb 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -149,6 +149,7 @@ const ABORT_SIGNAL_LIB_FNS: ReadonlySet = new Set([ /** Entry points that link the stream unit (with STREAM_LIB_PREFIXES). */ const STREAM_LIB_FNS: ReadonlySet = new Set([ + "child.stdioStream", "console.bind", "crypto.hashStream", "crypto.hashStreamPiped", @@ -167,6 +168,8 @@ const STREAM_LIB_FNS: ReadonlySet = new Set([ "stream.destroyErr", "stream.errored", "stream.prop", + "stream.stdioWritable", + "stream.unboxDyn", "stream.workerStdio", ]); @@ -238,8 +241,21 @@ const CHILD_PROCESS_LIB_FNS: ReadonlySet = new Set([ /** libCall name prefixes that link the child-process unit. */ const CHILD_PROCESS_LIB_PREFIXES: readonly string[] = ["child.", "cp.", "spawnRes.", "writer."]; +/** The entry points scr_dyn_builtin_class.c defines. The unit calls + * listeners through the dyn-invoke unit, renders unhandled 'error' + * payloads through the inspect unit, creates EventEmitter's private + * symbols through the symbol unit and shares defaultMaxListeners with the + * emitter unit, so each of them links all five. */ +const BUILTIN_CLASS_LIB_FNS: readonly IrLibFn[] = [ + "dyn.builtinClass", + "dyn.builtinInstanceOf", + "dyn.functionInstanceOf", +]; + /** Entry points that link the symbol unit, besides the sym. and disposable. families. */ const SYMBOL_LIB_FNS: ReadonlySet = new Set([ + // EventEmitter's kShapeMode and kCapture properties + ...BUILTIN_CLASS_LIB_FNS, "dyn.usingDispose", "dyn.usingMethod", "util.isDeepStrictEqual", @@ -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, diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index 84827e787..d6a4a09b4 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -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; } diff --git a/packages/compiler/src/library/ir-policy.ts b/packages/compiler/src/library/ir-policy.ts index 2d1bc85d4..0c343fa98 100644 --- a/packages/compiler/src/library/ir-policy.ts +++ b/packages/compiler/src/library/ir-policy.ts @@ -54,6 +54,11 @@ const LIB_MODE_REFUSED_PREFIXES: readonly [string, string][] = [ ["webcrypto.", "WebCrypto (crypto.subtle)"], ["fs.watch", "fs.watch"], ["watcher.", "fs.watch"], + // The event-loop metrics sample the loop and its timers (scr_perf_loop.c), + // which library links exclude. + ["perf.loopStart", "performance.eventLoopUtilization"], + ["perf.idleTime", "performance.eventLoopUtilization"], + ["perf.eld", "perf_hooks.monitorEventLoopDelay"], ["test.", "the node:test surface"], ["readable.", "the node:stream surface"], ["writable.", "the node:stream surface"], @@ -158,6 +163,7 @@ export function moduleLibAsyncSurface(mod: IrModule): { surface: string; loc: Sr [features.nodeTest, "the node:test surface"], [features.dynAsync, "the checked-dynamic async surface"], [features.dc, "the diagnostics_channel surface"], + [features.perfLoop, "the perf_hooks event-loop metrics"], ]; for (const [on, surface] of coarse) { if (on) return { surface, loc: entryLoc }; diff --git a/packages/compiler/src/native/cli.ts b/packages/compiler/src/native/cli.ts index 1585b6c9e..3fa934da1 100644 --- a/packages/compiler/src/native/cli.ts +++ b/packages/compiler/src/native/cli.ts @@ -89,6 +89,7 @@ async function main(): Promise { bigint: false, searchParams: false, qs: false, + dynBuiltinClass: false, parseArgs: false, stream: false, net: false, @@ -102,6 +103,7 @@ async function main(): Promise { tls: false, cipher: false, webcrypto: false, + perfLoop: false, tlsCa: false, }, options.optimization ?? "release", diff --git a/packages/compiler/src/native/prepare.ts b/packages/compiler/src/native/prepare.ts index e4216f511..a2a15179f 100644 --- a/packages/compiler/src/native/prepare.ts +++ b/packages/compiler/src/native/prepare.ts @@ -143,6 +143,7 @@ function validFeatures(value: unknown): value is NativeLinkFeatures { "bigint", "searchParams", "qs", + "dynBuiltinClass", "parseArgs", "stream", "net", @@ -157,6 +158,7 @@ function validFeatures(value: unknown): value is NativeLinkFeatures { "tlsCa", "cipher", "webcrypto", + "perfLoop", ].every((name) => typeof features[name] === "boolean"); } diff --git a/packages/compiler/src/native/sanitizer.test.ts b/packages/compiler/src/native/sanitizer.test.ts index 2e7574728..ea9690ce7 100644 --- a/packages/compiler/src/native/sanitizer.test.ts +++ b/packages/compiler/src/native/sanitizer.test.ts @@ -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, diff --git a/packages/compiler/src/wasm/binary.ts b/packages/compiler/src/wasm/binary.ts index 5ddc28cc9..eaedf25af 100644 --- a/packages/compiler/src/wasm/binary.ts +++ b/packages/compiler/src/wasm/binary.ts @@ -20,6 +20,7 @@ * The decoder never evaluates anything and keeps no module contents beyond * the counts and the feature set it reports. It is plain TypeScript that * the native compiler also compiles. */ +import { readMemargFlags } from "./module.js"; /** Proposals the summary can name, in report order. The first group is the * translatable set; the rest are reported as unsupported. */ @@ -1244,19 +1245,9 @@ class FunctionValidator { /** A memarg. A memory other than 0 stops typing (multi-memory). */ memarg(naturalAlign: number): void { const r = this.r; - let align = r.u32(); - let multi = false; - if (align >= 64 && align < 128) { - align -= 64; - multi = true; - } else if (align >= 64) { - r.malformed("malformed memop flags"); - } - let memory = 0; - if (multi) { - memory = r.u32(); - if (memory !== 0) this.otherMemory(memory); - } + const { align, memory } = readMemargFlags(() => r.u32()); + if (align < 0) r.malformed("malformed memop flags"); + if (memory !== 0) this.otherMemory(memory); if (this.m.memory64) r.u64(); else r.u32(); if (!this.typed) return; @@ -1840,9 +1831,9 @@ class FunctionValidator { scanMemarg(): void { const r = this.r; - const align = r.u32(); - if (align >= 128) r.malformed("malformed memop flags"); - if (align >= 64 && r.u32() !== 0) this.m.feature("multi-memory"); + const { align, memory } = readMemargFlags(() => r.u32()); + if (align < 0) r.malformed("malformed memop flags"); + if (memory !== 0) this.m.feature("multi-memory"); if (this.m.memory64) r.u64(); else r.u32(); } diff --git a/packages/compiler/src/wasm/module.ts b/packages/compiler/src/wasm/module.ts index bf5a3ce05..587a20d81 100644 --- a/packages/compiler/src/wasm/module.ts +++ b/packages/compiler/src/wasm/module.ts @@ -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; } } diff --git a/packages/compiler/src/wasm/translate.test.ts b/packages/compiler/src/wasm/translate.test.ts index 1d08386c9..86ba042ef 100644 --- a/packages/compiler/src/wasm/translate.test.ts +++ b/packages/compiler/src/wasm/translate.test.ts @@ -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 )) 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); diff --git a/packages/compiler/src/wasm/translate.ts b/packages/compiler/src/wasm/translate.ts index 095dba930..4c02499ca 100644 --- a/packages/compiler/src/wasm/translate.ts +++ b/packages/compiler/src/wasm/translate.ts @@ -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(); diff --git a/packages/compiler/surface-manifest.json b/packages/compiler/surface-manifest.json index 803fe6857..b7cb3ab94 100644 --- a/packages/compiler/surface-manifest.json +++ b/packages/compiler/surface-manifest.json @@ -1867,6 +1867,13 @@ "status": "static", "note": "recognized module (bare and node:-prefixed specifiers)" }, + { + "id": "node-builtin.perf_hooks.monitorEventLoopDelay", + "kind": "node-builtin", + "name": "perf_hooks.monitorEventLoopDelay", + "status": "static", + "note": "an unref'd sampling interval records the delay histogram; enable, disable, reset, percentile and the min, max, mean, stddev, count and exceeds readings are supported" + }, { "id": "node-builtin.perf_hooks.performance.clearMarks", "kind": "node-builtin", @@ -1879,6 +1886,13 @@ "name": "perf_hooks.performance.clearMeasures", "status": "static" }, + { + "id": "node-builtin.perf_hooks.performance.eventLoopUtilization", + "kind": "node-builtin", + "name": "perf_hooks.performance.eventLoopUtilization", + "status": "static", + "note": "active time is the main thread's CPU time since the loop started; the one- and two-argument difference forms are supported" + }, { "id": "node-builtin.perf_hooks.performance.getEntries", "kind": "node-builtin", diff --git a/packages/compiler/test/native-callable-values.test.ts b/packages/compiler/test/native-callable-values.test.ts index e64a9319e..6e65b0c99 100644 --- a/packages/compiler/test/native-callable-values.test.ts +++ b/packages/compiler/test/native-callable-values.test.ts @@ -102,19 +102,19 @@ test.each(["llvm"] as const)( writeFileSync( join(pkg, "index.js"), ` -const inspect = (value) => eval(value); +const inspect = (value) => process.on("__proto__", value); function stopped(value) { - eval(value); + process.on("__proto__", value); return { done() { return "wrong"; } }; } -try { console.log(stopped("0").done()); } -catch (error) { console.log(String(error).includes("SC2009")); } +try { console.log(stopped(() => {}).done()); } +catch (error) { console.log(String(error).includes("SC2020")); } const first = { inspect }; const second = { inspect }; console.log(typeof first.inspect, first.inspect === second.inspect); for (const callback of [first.inspect, second.inspect]) { - try { callback({}); } - catch (error) { console.log(String(error).includes("SC2009")); } + try { callback(() => {}); } + catch (error) { console.log(String(error).includes("SC2020")); } } `, ); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5817-constructor-function-instanceof.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5817-constructor-function-instanceof.cjs.json new file mode 100644 index 000000000..d9ad97beb --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5817-constructor-function-instanceof.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5817-constructor-function-instanceof.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5818-constructor-function-emitter-defaults.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5818-constructor-function-emitter-defaults.cjs.json new file mode 100644 index 000000000..f22e1aaeb --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5818-constructor-function-emitter-defaults.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5818-constructor-function-emitter-defaults.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6126-punycode-dependency/main.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6126-punycode-dependency/main.cjs.json index 5de83361a..36f02246c 100644 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6126-punycode-dependency/main.cjs.json +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6126-punycode-dependency/main.cjs.json @@ -12,7 +12,7 @@ "end": 376 }, "milestone": "M4", - "hint": "relative imports (./file, ../dir/file), package.json-mediated project imports (#alias via the imports field, self-name references via exports), statically compiled npm packages, and the built-in fs, fs/promises, path, os, url, crypto, zlib, child_process, net, http, tls, https, http2, dgram, dns, util, util/types, string_decoder, querystring, readline, events, stream, stream/promises, stream/consumers, buffer, assert, assert/strict, worker_threads, cluster, tty, async_hooks, timers, timers/promises, diagnostics_channel, perf_hooks, module, and console modules (bare or node:-prefixed) and node:test (node:-prefixed only, like in Node) are supported" + "hint": "relative imports (./file, ../dir/file), package.json-mediated project imports (#alias via the imports field, self-name references via exports), statically compiled npm packages, and the built-in fs, fs/promises, path, os, url, crypto, zlib, child_process, net, http, tls, https, http2, dgram, dns, dns/promises, util, util/types, string_decoder, querystring, readline, events, stream, stream/promises, stream/consumers, stream/web, buffer, assert, assert/strict, worker_threads, cluster, tty, async_hooks, timers, timers/promises, diagnostics_channel, perf_hooks, module, v8, and console modules (bare or node:-prefixed) and node:test (node:-prefixed only, like in Node) are supported" } ] } diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6380-variadic-wrapper-arguments-apply.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6380-variadic-wrapper-arguments-apply.ts.json new file mode 100644 index 000000000..df0124219 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6380-variadic-wrapper-arguments-apply.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6380-variadic-wrapper-arguments-apply.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6381-variadic-wrapper-async.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6381-variadic-wrapper-async.ts.json new file mode 100644 index 000000000..2138df43d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6381-variadic-wrapper-async.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6381-variadic-wrapper-async.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6382-variadic-wrapper-methods-this.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6382-variadic-wrapper-methods-this.ts.json new file mode 100644 index 000000000..2135f8b6f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6382-variadic-wrapper-methods-this.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6382-variadic-wrapper-methods-this.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6383-variadic-wrapper-rest-tuple.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6383-variadic-wrapper-rest-tuple.ts.json new file mode 100644 index 000000000..a2f7f32c4 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6383-variadic-wrapper-rest-tuple.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6383-variadic-wrapper-rest-tuple.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6384-variadic-wrapper-nested.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6384-variadic-wrapper-nested.ts.json new file mode 100644 index 000000000..d69618e3a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6384-variadic-wrapper-nested.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6384-variadic-wrapper-nested.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6385-variadic-wrapper-shared-local-names.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6385-variadic-wrapper-shared-local-names.ts.json new file mode 100644 index 000000000..5791565f2 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6385-variadic-wrapper-shared-local-names.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6385-variadic-wrapper-shared-local-names.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6390-catch-binding-predicate.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6390-catch-binding-predicate.ts.json new file mode 100644 index 000000000..fcfb2852f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6390-catch-binding-predicate.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6390-catch-binding-predicate.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6391-nullish-function-default.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6391-nullish-function-default.ts.json new file mode 100644 index 000000000..8200d301e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6391-nullish-function-default.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6391-nullish-function-default.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6392-optional-chain-protocol-calls.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6392-optional-chain-protocol-calls.ts.json new file mode 100644 index 000000000..f2343714c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6392-optional-chain-protocol-calls.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6392-optional-chain-protocol-calls.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6393-function-types-any-params.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6393-function-types-any-params.ts.json new file mode 100644 index 000000000..af0a9abe7 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6393-function-types-any-params.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6393-function-types-any-params.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6394-in-key-error-subclasses.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6394-in-key-error-subclasses.ts.json new file mode 100644 index 000000000..9df1e1014 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6394-in-key-error-subclasses.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6394-in-key-error-subclasses.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6395-generic-object-entries.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6395-generic-object-entries.ts.json new file mode 100644 index 000000000..879a09388 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6395-generic-object-entries.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6395-generic-object-entries.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6396-generic-any-instantiation.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6396-generic-any-instantiation.ts.json new file mode 100644 index 000000000..da41452ad --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6396-generic-any-instantiation.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6396-generic-any-instantiation.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6397-event-loop-metrics.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6397-event-loop-metrics.ts.json new file mode 100644 index 000000000..0a74534b0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6397-event-loop-metrics.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6397-event-loop-metrics.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6398-conditional-fixed-arity.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6398-conditional-fixed-arity.ts.json new file mode 100644 index 000000000..878f45b09 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6398-conditional-fixed-arity.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6398-conditional-fixed-arity.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6410-json-mixed-scalar-tables/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6410-json-mixed-scalar-tables/main.ts.json new file mode 100644 index 000000000..c7ec1ab56 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6410-json-mixed-scalar-tables/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6410-json-mixed-scalar-tables/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6411-union-array-tuple-destructuring.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6411-union-array-tuple-destructuring.ts.json new file mode 100644 index 000000000..94d8b78e7 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6411-union-array-tuple-destructuring.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6411-union-array-tuple-destructuring.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6453-constructor-function-emitter.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6453-constructor-function-emitter.cjs.json new file mode 100644 index 000000000..ae5c0050e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6453-constructor-function-emitter.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6453-constructor-function-emitter.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6454-constructor-function-stream-bases.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6454-constructor-function-stream-bases.cjs.json new file mode 100644 index 000000000..bc48a7f35 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6454-constructor-function-stream-bases.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6454-constructor-function-stream-bases.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6463-node-readable-stream-producers.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6463-node-readable-stream-producers.ts.json new file mode 100644 index 000000000..e413951ae --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6463-node-readable-stream-producers.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6463-node-readable-stream-producers.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6464-node-writable-stream-producers.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6464-node-writable-stream-producers.ts.json new file mode 100644 index 000000000..fd805a2af --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6464-node-writable-stream-producers.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6464-node-writable-stream-producers.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6465-node-stream-js-boundary/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6465-node-stream-js-boundary/main.ts.json new file mode 100644 index 000000000..95278eab8 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6465-node-stream-js-boundary/main.ts.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/6465-node-stream-js-boundary/consume.js", + "/tests/corpus/6465-node-stream-js-boundary/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6465-optional-members-over-index-signature.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6465-optional-members-over-index-signature.ts.json new file mode 100644 index 000000000..91a157207 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6465-optional-members-over-index-signature.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6465-optional-members-over-index-signature.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6466-never-assertion-argument.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6466-never-assertion-argument.ts.json new file mode 100644 index 000000000..17f25aa22 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6466-never-assertion-argument.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6466-never-assertion-argument.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6466-node-stdio-stream-surface.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6466-node-stdio-stream-surface.ts.json new file mode 100644 index 000000000..8ebcddd22 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6466-node-stdio-stream-surface.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6466-node-stdio-stream-surface.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6467-fromentries-record-assertion.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6467-fromentries-record-assertion.ts.json new file mode 100644 index 000000000..21ca66953 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6467-fromentries-record-assertion.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6467-fromentries-record-assertion.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6467-node-stream-untyped-sources/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6467-node-stream-untyped-sources/main.ts.json new file mode 100644 index 000000000..dc843c047 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6467-node-stream-untyped-sources/main.ts.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/6467-node-stream-untyped-sources/sources.js", + "/tests/corpus/6467-node-stream-untyped-sources/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6468-generic-unknown-record-entries.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6468-generic-unknown-record-entries.ts.json new file mode 100644 index 000000000..0b4118c70 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6468-generic-unknown-record-entries.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6468-generic-unknown-record-entries.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6468-node-stdio-writable-slot.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6468-node-stdio-writable-slot.ts.json new file mode 100644 index 000000000..baf428dee --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6468-node-stdio-writable-slot.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6468-node-stdio-writable-slot.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6469-node-stdio-writable-js-pipe/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6469-node-stdio-writable-js-pipe/main.ts.json new file mode 100644 index 000000000..60b1360b0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6469-node-stdio-writable-js-pipe/main.ts.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/6469-node-stdio-writable-js-pipe/pipe.js", + "/tests/corpus/6469-node-stdio-writable-js-pipe/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6470-stream-transform-any-data-callback.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6470-stream-transform-any-data-callback.ts.json new file mode 100644 index 000000000..3d4a39486 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6470-stream-transform-any-data-callback.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6470-stream-transform-any-data-callback.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6471-stream-data-buffer-or-string.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6471-stream-data-buffer-or-string.ts.json new file mode 100644 index 000000000..e8ff1e084 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6471-stream-data-buffer-or-string.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6471-stream-data-buffer-or-string.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6472-child-data-unknown-chunk.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6472-child-data-unknown-chunk.ts.json new file mode 100644 index 000000000..3c7183bb6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6472-child-data-unknown-chunk.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6472-child-data-unknown-chunk.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6490-literal-key-field-updates.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6490-literal-key-field-updates.ts.json new file mode 100644 index 000000000..1e4e1e939 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6490-literal-key-field-updates.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6490-literal-key-field-updates.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6491-index-record-updates.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6491-index-record-updates.ts.json new file mode 100644 index 000000000..babda8c3c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6491-index-record-updates.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6491-index-record-updates.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/webassembly-externref-lifetimes.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/webassembly-externref-lifetimes.mjs.json new file mode 100644 index 000000000..7d264b2eb --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/webassembly-externref-lifetimes.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/webassembly-externref-lifetimes.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/fromentries-aliased-entries.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/fromentries-aliased-entries.ts.json new file mode 100644 index 000000000..7388f2b71 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/fromentries-aliased-entries.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/fromentries-aliased-entries.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/variadic-wrapper-fences.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/variadic-wrapper-fences.ts.json new file mode 100644 index 000000000..c3e44b2e5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/variadic-wrapper-fences.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/variadic-wrapper-fences.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/child-stdin-fences.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/child-stdin-fences.ts.json deleted file mode 100644 index cfa602a2e..000000000 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/child-stdin-fences.ts.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "order": [ - "/tests/fixtures/node-types/child-stdin-fences.ts" - ], - "diags": [] -} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/child-stdin-streams.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/child-stdin-streams.ts.json new file mode 100644 index 000000000..2bb443d2a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/child-stdin-streams.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/node-types/child-stdin-streams.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/event-loop-metrics.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/event-loop-metrics.ts.json new file mode 100644 index 000000000..66246403d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/event-loop-metrics.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/node-types/event-loop-metrics.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/node-stream-interfaces.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/node-stream-interfaces.ts.json new file mode 100644 index 000000000..cfb5c1a10 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/node-stream-interfaces.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/node-types/node-stream-interfaces.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/stream-construction.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/stream-construction.ts.json new file mode 100644 index 000000000..35dc0be0d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/stream-construction.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/node-types/stream-construction.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/timeout-type.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/timeout-type.ts.json new file mode 100644 index 000000000..46f0b6e38 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/timeout-type.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/node-types/timeout-type.ts" + ], + "diags": [] +} diff --git a/packages/runtime-pack-common/runtime-pack-matrix.mjs b/packages/runtime-pack-common/runtime-pack-matrix.mjs index ade0ec48c..28ccf0f59 100644 --- a/packages/runtime-pack-common/runtime-pack-matrix.mjs +++ b/packages/runtime-pack-common/runtime-pack-matrix.mjs @@ -69,6 +69,8 @@ export const OPTIONAL_RUNTIME_SOURCES = [ ["scr_async_dyn.c", any("dynAsync", "dynInvoke", "dc", "fileHandle", "nativeFetch")], // The performance timeline emits its buffer warning through scr_async_dyn.c. ["scr_perf.c", "dynAsync"], + // perf_hooks event-loop metrics, only for programs that read them. + ["scr_perf_loop.c", "perfLoop"], // Blob and File box their reads as dyn promises through scr_async_dyn.c. ["scr_blob.c", "dynAsync"], ["scr_zlib.c", "zlib"], @@ -88,6 +90,10 @@ export const OPTIONAL_RUNTIME_SOURCES = [ ["scr_util_compare.c", all("parseArgs", "symbol")], ["scr_util_style.c", all("parseArgs", "inspect", "dynAsync")], ["scr_util_callable.c", all("parseArgs", "inspect", "dynAsync", "dynInvoke", "regex")], + // Untyped EventEmitter and stream constructor values call listeners + // through the dyn-invoke unit, inspect unhandled 'error' payloads and + // create EventEmitter's private symbols. + ["scr_dyn_builtin_class.c", all("dynBuiltinClass", "inspect", "dynInvoke", "symbol", "emitter")], ["scr_stream.c", "stream"], // The zlib stream classes reference both the zlib engine and the stream unit. ["scr_zlib_stream.c", all("zlib", "stream")], diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 527243055..28d642501 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -1153,6 +1153,12 @@ ScrDyn *scr_dyn_define_prototype_props(ScrDyn *target, ScrDyn *descs, ScrDyn *pr * must be static literals (the box never frees them; name may be NULL). */ ScrDyn *scr_dyn_new_func(ScrClosure *clo, ScrDynThunk thunk, uint32_t arity, const char *sig, const char *name); ScrDyn *scr_dyn_new_class(ScrClassObj *cls, const char *type_key); +/* EventEmitter and the stream constructors as untyped constructor values, + * and instanceof over untyped constructor functions + * (scr_dyn_builtin_class.c, linked with the dynBuiltinClass feature). */ +ScrDyn *scr_dyn_builtin_class(double kind); +bool scr_dyn_builtin_instanceof(ScrDyn *value, double kind); +bool scr_dyn_function_instanceof(ScrDyn *value, ScrDyn *callee); bool scr_dyn_class_is_key(const ScrDyn *value, const ScrStr *type_key); bool scr_dyn_class_is(const ScrDyn *value, const char *type_key); /* Calls a dyn value: a non-function kind throws the catchable TypeError @@ -1286,7 +1292,8 @@ typedef struct ScrDynHandleOps { * inspector returns `this`) instead of the bare class name. */ bool inspect_bracketed_past_depth; /* The @@toStringTag when it differs from cls: String() answers - * "[object ]" and util.inspect prints ` [] `. */ + * "[object ]" and util.inspect prints ` [] `, or + * ` ` when the tag ends with "." and cls, as Node does. */ const char *to_string_tag; /* The view has no properties, so util.inspect prints it in full at any * depth, as Node prints `{}`. */ diff --git a/packages/runtime/src/scr_dyn_builtin_class.c b/packages/runtime/src/scr_dyn_builtin_class.c new file mode 100644 index 000000000..6d894615b --- /dev/null +++ b/packages/runtime/src/scr_dyn_builtin_class.c @@ -0,0 +1,1510 @@ +/* EventEmitter and the node:stream constructors as checked-dynamic + * constructor values. JavaScript constructor functions inherit from them + * the pre-class way: + * + * function Bus() { EventEmitter.call(this); } + * util.inherits(Bus, EventEmitter); + * + * A compiled EventEmitter instance keeps its listeners in native state, but + * the receiver here is an ordinary dyn object built by `new Bus()`. So the + * EventEmitter value is a native function whose prototype carries Node's + * lib/events.js methods over that object's own `_events`, `_eventsCount` + * and `_maxListeners` properties, the state Node's own implementation + * keeps there. Calling it with a receiver runs EventEmitter.init. + * + * Stream is Node's legacy constructor: EventEmitter.call(this, opts) and a + * prototype that inherits EventEmitter's. Readable, Writable, Duplex, + * Transform and PassThrough have prototypes chained as in Node, so + * util.inherits onto them and instanceof work, but their native state has + * no dyn object form: calling one with a receiver throws the "not + * supported yet" Error where the construction runs. + * + * One constructor per kind and thread, so every boxing of the same class + * value is the same function. All arguments are borrowed. */ +#include "scr_runtime.h" + +#include +#include +#include + +#ifdef _WIN32 +#include +#define BC_PID() ((long)_getpid()) +#else +#include +#define BC_PID() ((long)getpid()) +#endif + +enum { + BC_EMITTER, + BC_STREAM, + BC_READABLE, + BC_WRITABLE, + BC_DUPLEX, + BC_TRANSFORM, + BC_PASSTHROUGH, + BC_COUNT +}; + +static const char *const bc_names[BC_COUNT] = { + "EventEmitter", "Stream", "Readable", "Writable", "Duplex", "Transform", "PassThrough", +}; + +static SCR_TL ScrDyn *bc_ctors[BC_COUNT]; +static SCR_TL bool bc_teardown_registered; +/* Node's private kShapeMode and kCapture symbols, which util.inspect shows + * on every emitter as Symbol(shapeMode) and Symbol(kCapture). */ +enum { BC_SHAPE_MODE, BC_CAPTURE, BC_SYMBOLS }; +static SCR_TL ScrDyn *bc_symbols[BC_SYMBOLS]; + +static ScrDyn *bc_class(int kind); + +/* ── property helpers ───────────────────────────────────────────────── */ + +static ScrDyn *bc_key(const char *name) { + ScrStr *text = scr_str_new(name, strlen(name)); + ScrDyn *key = scr_dyn_new_str(text); + scr_str_release(text); + return key; +} + +/* object[key] with object as the receiver: +1, or NULL with an exception + * pending. */ +static ScrDyn *bc_get_key(ScrDyn *object, ScrDyn *key) { + ScrDyn *value = scr_dyn_reflect_get(object, key, object); + if (!value && !scr_exc_pending()) value = scr_dyn_retain(scr_dyn_undefined()); + return value; +} + +static ScrDyn *bc_get(ScrDyn *object, const char *name) { + ScrDyn *key = bc_key(name); + ScrDyn *value = bc_get_key(object, key); + scr_dyn_release(key); + return value; +} + +/* object[key] = value; the value moves in. False with an exception + * pending. */ +static bool bc_set_key(ScrDyn *object, ScrDyn *key, ScrDyn *value) { + scr_dyn_key_set_computed(object, key, value); + scr_dyn_release(value); + return !scr_exc_pending(); +} + +static bool bc_set(ScrDyn *object, const char *name, ScrDyn *value) { + ScrDyn *key = bc_key(name); + bool ok = bc_set_key(object, key, value); + scr_dyn_release(key); + return ok; +} + +static bool bc_undefined(const ScrDyn *value) { + return value->kind == SCR_DYN_UNDEF; +} + +static double bc_number(const ScrDyn *value) { + return value->kind == SCR_DYN_NUM ? value->v.num : 0; +} + +/* receiver.method(...args), dispatched through the receiver's own lookup + * like Node's `this.emit(...)`. +1 or NULL. */ +static ScrDyn *bc_invoke(ScrDyn *receiver, const char *method, ScrDyn *const *args, size_t argc) { + ScrDyn *fn = bc_get(receiver, method); + if (!fn) return NULL; + scr_dyn_this_push_dyn(receiver); + ScrDyn *result = scr_dyn_call(fn, args, argc, method); + scr_dyn_this_pop(); + scr_dyn_release(fn); + return result; +} + +static ScrDyn *bc_apply(ScrDyn *fn, ScrDyn *receiver, ScrDyn *const *args, size_t argc) { + scr_dyn_this_push_dyn(receiver); + ScrDyn *result = scr_dyn_call(fn, args, argc, "listener"); + scr_dyn_this_pop(); + return result; +} + +/* Listener lists are replaced rather than mutated: a fresh array with item + * first, or without the item at index. Node mutates its array in place, + * which nothing outside the emitter observes (emit iterates a copy and + * listeners() answers one). A leak warning already given stays given. */ +static ScrDyn *bc_list_copy(ScrDyn *list, ScrDyn *first, double skip) { + ScrDyn *copy = scr_dyn_new_arr(); + if (first) scr_dyn_arr_push(copy, scr_dyn_retain(first)); + double length = scr_dyn_arr_len(list); + for (double i = 0; i < length; i++) { + if (i == skip) continue; + ScrDyn *item = scr_dyn_index_get(list, i); + if (!item) { + scr_dyn_release(copy); + return NULL; + } + scr_dyn_arr_push(copy, item); + } + ScrDyn *warned = bc_get(list, "warned"); + if (!warned || (scr_dyn_truthy(warned) && !bc_set(copy, "warned", scr_dyn_new_bool(true)))) { + if (warned) scr_dyn_release(warned); + scr_dyn_release(copy); + return NULL; + } + scr_dyn_release(warned); + return copy; +} + +static ScrDyn *bc_unused_args[1]; + +static ScrDyn *bc_arg(ScrDyn *const *args, size_t argc, size_t index) { + return index < argc ? args[index] : scr_dyn_undefined(); +} + +/* The receiver of a prototype method call, +1. Methods called without one + * (an extracted `emitter.on`) read `this` as undefined, as in strict code. */ +static ScrDyn *bc_this(void) { + return scr_dyn_this_get(); +} + +static bool bc_require_object(ScrDyn *self, const char *reading) { + if (self->kind != SCR_DYN_UNDEF && self->kind != SCR_DYN_NULL) return true; + char message[96]; + snprintf(message, sizeof message, "Cannot read properties of %s (reading '%s')", + self->kind == SCR_DYN_NULL ? "null" : "undefined", reading); + scr_throw_error_msg(SCR_ERR_TYPE, message, strlen(message)); + return false; +} + +static bool bc_check_listener(const ScrDyn *listener) { + if (scr_dyn_is_callable(listener)) return true; + scr_dyn_arg_type_fail("listener", "of type function", listener); + return false; +} + +static void bc_teardown(void); + +static ScrDyn *bc_symbol(int which) { + if (!bc_symbols[which]) { + if (!bc_teardown_registered) { + bc_teardown_registered = true; + scr_atexit(bc_teardown); + } + const char *name = which == BC_SHAPE_MODE ? "shapeMode" : "kCapture"; + ScrStr *description = scr_str_new(name, strlen(name)); + ScrSym *symbol = scr_sym_new(description); + scr_str_release(description); + bc_symbols[which] = scr_dyn_new_symbol(symbol); + scr_sym_release(symbol); + } + return bc_symbols[which]; +} + +/* ── EventEmitter.init ──────────────────────────────────────────────── */ + +/* `_events` is fresh unless the receiver already has its own (Node's + * shapeMode, a subclass that pre-populates it); `_maxListeners` keeps a + * value set before the call. */ +static bool bc_emitter_init(ScrDyn *self, ScrDyn *opts) { + if (!bc_require_object(self, "_events")) return false; + ScrDyn *events = bc_get(self, "_events"); + if (!events) return false; + bool fresh = bc_undefined(events); + if (!fresh) { + ScrDyn *proto = scr_dyn_get_prototype(self); + if (!proto) { + scr_dyn_release(events); + return false; + } + if (proto->kind != SCR_DYN_NULL) { + ScrDyn *inherited = bc_get(proto, "_events"); + if (!inherited) { + scr_dyn_release(proto); + scr_dyn_release(events); + return false; + } + fresh = scr_dyn_strict_eq(events, inherited); + scr_dyn_release(inherited); + } + scr_dyn_release(proto); + } + scr_dyn_release(events); + if (fresh) { + if (!bc_set(self, "_events", scr_dyn_new_obj_null_proto())) return false; + if (!bc_set(self, "_eventsCount", scr_dyn_new_num(0))) return false; + } + if (!bc_set_key(self, bc_symbol(BC_SHAPE_MODE), scr_dyn_new_bool(!fresh))) return false; + ScrDyn *max = bc_get(self, "_maxListeners"); + if (!max) return false; + bool keep = scr_dyn_truthy(max); + if (!bc_set(self, "_maxListeners", keep ? max : (scr_dyn_release(max), scr_dyn_retain(scr_dyn_undefined())))) + return false; + if (opts->kind != SCR_DYN_UNDEF && opts->kind != SCR_DYN_NULL) { + ScrDyn *capture = bc_get(opts, "captureRejections"); + if (!capture) return false; + bool requested = !bc_undefined(capture); + if (requested && capture->kind != SCR_DYN_BOOL) { + scr_dyn_arg_type_fail("options.captureRejections", "of type boolean", capture); + scr_dyn_release(capture); + return false; + } + bool enabled = requested && capture->v.b; + scr_dyn_release(capture); + if (enabled) { + static const char message[] = "EventEmitter captureRejections on a constructor function is not supported yet"; + scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); + return false; + } + } + return bc_set_key(self, bc_symbol(BC_CAPTURE), scr_dyn_new_bool(false)); +} + +/* ── listener bookkeeping (lib/events.js) ───────────────────────────── */ + +static double bc_max_listeners(ScrDyn *self) { + ScrDyn *max = bc_get(self, "_maxListeners"); + if (!max) return -1; + double value; + if (bc_undefined(max)) { + value = scr_emitter_get_default_max(); + } else { + value = bc_number(max); + } + scr_dyn_release(max); + return value; +} + +static void bc_warn_tick(ScrClosure *self) { + ScrStr *message = (ScrStr *)scr_box_get_ref(self->caps[0]); + if (scr_warning_emit_hook) { + scr_warning_emit_hook("MaxListenersExceededWarning", NULL, message); + } else { + fflush(stdout); + fprintf(stderr, "(node:%ld) MaxListenersExceededWarning: %.*s\n", BC_PID(), + (int)message->len, message->data); + if (!scr_warning_trace_hint_shown) { + scr_warning_trace_hint_shown = true; + fputs("(Use `node --trace-warnings ...` to show where the warning was created)\n", stderr); + } + } + scr_str_release(message); +} + +/* The constructor name util.inspect prints at depth -1: `[Bus]`. */ +static ScrStr *bc_constructor_name(ScrDyn *self) { + ScrDyn *ctor = bc_get(self, "constructor"); + ScrStr *name = NULL; + if (ctor && scr_dyn_is_callable(ctor)) { + ScrDyn *text = bc_get(ctor, "name"); + if (text && text->kind == SCR_DYN_STR && text->v.str->len) name = scr_str_retain(text->v.str); + if (text) scr_dyn_release(text); + } + if (ctor) scr_dyn_release(ctor); + if (scr_exc_pending()) scr_exc_clear(); + return name ? name : scr_str_new("Object", 6); +} + +static void bc_warn_leak(ScrDyn *self, ScrDyn *type, double count, double max) { + char counted[32], limit[32]; + snprintf(counted, sizeof counted, "%g", count); + snprintf(limit, sizeof limit, "%g", max); + ScrStr *event = scr_dyn_string_coerce_js(type); + if (!event) { + scr_exc_clear(); + event = scr_str_new("", 0); + } + ScrStr *cls = bc_constructor_name(self); + ScrJsonBuf text; + scr_jb_init(&text); + scr_jb_puts(&text, "Possible EventEmitter memory leak detected. "); + scr_jb_puts(&text, counted); + scr_jb_putc(&text, ' '); + scr_jb_put_str(&text, event); + scr_jb_puts(&text, " listeners added to ["); + scr_jb_put_str(&text, cls); + scr_jb_puts(&text, "]. MaxListeners is "); + scr_jb_puts(&text, limit); + scr_jb_puts(&text, ". Use emitter.setMaxListeners() to increase limit"); + scr_str_release(event); + scr_str_release(cls); + ScrClosure *tick = scr_closure_new((void *)&bc_warn_tick, 1); + ScrBox *box = scr_box_new(SCR_BOX_STR); + scr_box_set_ref(box, scr_jb_finish(&text)); + tick->caps[0] = box; + scr_next_tick(tick); +} + +/* _addListener(target, type, listener, prepend): +1 target or NULL. */ +static ScrDyn *bc_add_listener(ScrDyn *self, ScrDyn *type, ScrDyn *listener, bool prepend) { + if (!bc_check_listener(listener)) return NULL; + ScrDyn *events = bc_get(self, "_events"); + if (!events) return NULL; + ScrDyn *existing = NULL; + if (bc_undefined(events)) { + scr_dyn_release(events); + events = scr_dyn_new_obj_null_proto(); + if (!bc_set(self, "_events", scr_dyn_retain(events)) || !bc_set(self, "_eventsCount", scr_dyn_new_num(0))) + goto fail; + } else { + ScrDyn *watcher = bc_get(events, "newListener"); + if (!watcher) goto fail; + bool watched = !bc_undefined(watcher); + scr_dyn_release(watcher); + if (watched) { + ScrDyn *inner = bc_get(listener, "listener"); + if (!inner) goto fail; + ScrDyn *name = bc_key("newListener"); + ScrDyn *args[3] = { name, type, bc_undefined(inner) || inner->kind == SCR_DYN_NULL ? listener : inner }; + ScrDyn *emitted = bc_invoke(self, "emit", args, 3); + scr_dyn_release(name); + scr_dyn_release(inner); + if (!emitted) goto fail; + scr_dyn_release(emitted); + /* the listener may have replaced _events */ + scr_dyn_release(events); + events = bc_get(self, "_events"); + if (!events) return NULL; + } + existing = bc_get_key(events, type); + if (!existing) goto fail; + } + if (!existing || bc_undefined(existing)) { + if (!bc_set_key(events, type, scr_dyn_retain(listener))) goto fail; + ScrDyn *count = bc_get(self, "_eventsCount"); + if (!count) goto fail; + double next = bc_number(count) + 1; + scr_dyn_release(count); + if (!bc_set(self, "_eventsCount", scr_dyn_new_num(next))) goto fail; + } else { + if (scr_dyn_is_callable(existing)) { + ScrDyn *list = scr_dyn_new_arr(); + scr_dyn_arr_push(list, scr_dyn_retain(prepend ? listener : existing)); + scr_dyn_arr_push(list, scr_dyn_retain(prepend ? existing : listener)); + if (!bc_set_key(events, type, scr_dyn_retain(list))) { + scr_dyn_release(list); + goto fail; + } + scr_dyn_release(existing); + existing = list; + } else if (prepend) { + ScrDyn *list = bc_list_copy(existing, listener, -1); + if (!list) goto fail; + if (!bc_set_key(events, type, scr_dyn_retain(list))) { + scr_dyn_release(list); + goto fail; + } + scr_dyn_release(existing); + existing = list; + } else if (existing->kind == SCR_DYN_ARR) { + scr_dyn_arr_push(existing, scr_dyn_retain(listener)); + } + double max = bc_max_listeners(self); + if (scr_exc_pending()) goto fail; + double length = existing->kind == SCR_DYN_ARR ? scr_dyn_arr_len(existing) : 0; + if (max > 0 && length > max) { + ScrDyn *warned = bc_get(existing, "warned"); + if (!warned) goto fail; + bool already = scr_dyn_truthy(warned); + scr_dyn_release(warned); + if (!already) { + if (!bc_set(existing, "warned", scr_dyn_new_bool(true))) goto fail; + bc_warn_leak(self, type, length, max); + } + } + } + if (existing) scr_dyn_release(existing); + scr_dyn_release(events); + return scr_dyn_retain(self); +fail: + if (existing) scr_dyn_release(existing); + scr_dyn_release(events); + return NULL; +} + +/* removeListener(type, listener): +1 self or NULL. */ +static ScrDyn *bc_remove_listener(ScrDyn *self, ScrDyn *type, ScrDyn *listener) { + if (!bc_check_listener(listener)) return NULL; + ScrDyn *events = bc_get(self, "_events"); + if (!events) return NULL; + if (bc_undefined(events)) { + scr_dyn_release(events); + return scr_dyn_retain(self); + } + ScrDyn *list = bc_get_key(events, type); + if (!list) goto fail_events; + if (bc_undefined(list)) { + scr_dyn_release(list); + scr_dyn_release(events); + return scr_dyn_retain(self); + } + ScrDyn *wrapped = scr_dyn_is_callable(list) ? bc_get(list, "listener") : NULL; + if (scr_exc_pending()) goto fail_list; + bool match = scr_dyn_is_callable(list) && + (scr_dyn_strict_eq(list, listener) || (wrapped && scr_dyn_strict_eq(wrapped, listener))); + if (match) { + ScrDyn *count = bc_get(self, "_eventsCount"); + if (!count) goto fail_wrapped; + double left = bc_number(count) - 1; + scr_dyn_release(count); + if (!bc_set(self, "_eventsCount", scr_dyn_new_num(left))) goto fail_wrapped; + ScrDyn *shaped = bc_get_key(self, bc_symbol(BC_SHAPE_MODE)); + if (!shaped) goto fail_wrapped; + bool shape_mode = scr_dyn_truthy(shaped); + scr_dyn_release(shaped); + if (shape_mode) { + if (!bc_set_key(events, type, scr_dyn_retain(scr_dyn_undefined()))) goto fail_wrapped; + } else if (left == 0) { + if (!bc_set(self, "_events", scr_dyn_new_obj_null_proto())) goto fail_wrapped; + } else { + scr_dyn_key_delete_computed(events, type, false); + if (scr_exc_pending()) goto fail_wrapped; + ScrDyn *watcher = bc_get(events, "removeListener"); + if (!watcher) goto fail_wrapped; + bool watched = scr_dyn_truthy(watcher); + scr_dyn_release(watcher); + if (watched) { + ScrDyn *name = bc_key("removeListener"); + ScrDyn *args[3] = { name, type, wrapped && scr_dyn_truthy(wrapped) ? wrapped : listener }; + ScrDyn *emitted = bc_invoke(self, "emit", args, 3); + scr_dyn_release(name); + if (!emitted) goto fail_wrapped; + scr_dyn_release(emitted); + } + } + } else if (list->kind == SCR_DYN_ARR) { + double length = scr_dyn_arr_len(list); + double position = -1; + for (double i = length - 1; i >= 0; i--) { + ScrDyn *item = scr_dyn_index_get(list, i); + if (!item) goto fail_wrapped; + bool same = scr_dyn_strict_eq(item, listener); + if (!same && scr_dyn_is_callable(item)) { + ScrDyn *inner = bc_get(item, "listener"); + if (!inner) { + scr_dyn_release(item); + goto fail_wrapped; + } + same = scr_dyn_strict_eq(inner, listener); + scr_dyn_release(inner); + } + scr_dyn_release(item); + if (same) { + position = i; + break; + } + } + if (position >= 0) { + ScrDyn *rest = bc_list_copy(list, NULL, position); + if (!rest) goto fail_wrapped; + ScrDyn *stored = scr_dyn_arr_len(rest) == 1 ? scr_dyn_index_get(rest, 0) : scr_dyn_retain(rest); + scr_dyn_release(rest); + if (!stored || !bc_set_key(events, type, stored)) goto fail_wrapped; + ScrDyn *watcher = bc_get(events, "removeListener"); + if (!watcher) goto fail_wrapped; + bool watched = !bc_undefined(watcher); + scr_dyn_release(watcher); + if (watched) { + ScrDyn *name = bc_key("removeListener"); + ScrDyn *args[3] = { name, type, listener }; + ScrDyn *emitted = bc_invoke(self, "emit", args, 3); + scr_dyn_release(name); + if (!emitted) goto fail_wrapped; + scr_dyn_release(emitted); + } + } + } + if (wrapped) scr_dyn_release(wrapped); + scr_dyn_release(list); + scr_dyn_release(events); + return scr_dyn_retain(self); +fail_wrapped: + if (wrapped) scr_dyn_release(wrapped); +fail_list: + scr_dyn_release(list); +fail_events: + scr_dyn_release(events); + return NULL; +} + +/* ── once wrappers ──────────────────────────────────────────────────── */ + +enum { BC_ONCE_FIRED, BC_ONCE_TARGET, BC_ONCE_TYPE, BC_ONCE_LISTENER, BC_ONCE_SELF, BC_ONCE_CAPS }; + +static ScrBox *bc_box(ScrDyn *value /* moves in */) { + ScrBox *box = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v); + scr_box_set_ref(box, value); + return box; +} + +static ScrDyn *bc_cap(ScrClosure *closure, size_t index) { + return scr_box_get_ref(closure->caps[index]); /* +1 */ +} + +/* onceWrapper: the first call removes the wrapper, then calls the listener + * with the target as its receiver. The wrapper's reference to itself is + * dropped when it fires, so a fired wrapper holds no cycle. */ +static ScrDyn *bc_once_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + if (scr_box_get_f64(closure->caps[BC_ONCE_FIRED]) != 0) return scr_dyn_retain(scr_dyn_undefined()); + ScrDyn *target = bc_cap(closure, BC_ONCE_TARGET); + ScrDyn *type = bc_cap(closure, BC_ONCE_TYPE); + ScrDyn *listener = bc_cap(closure, BC_ONCE_LISTENER); + ScrDyn *self = bc_cap(closure, BC_ONCE_SELF); + ScrDyn *result = NULL; + ScrDyn *removal[2] = { type, self }; + ScrDyn *removed = bc_invoke(target, "removeListener", removal, 2); + if (removed) { + scr_dyn_release(removed); + scr_box_set_f64(closure->caps[BC_ONCE_FIRED], 1); + scr_box_set_ref(closure->caps[BC_ONCE_SELF], scr_dyn_retain(scr_dyn_undefined())); + result = bc_apply(listener, target, args, argc); + } + scr_dyn_release(self); + scr_dyn_release(listener); + scr_dyn_release(type); + scr_dyn_release(target); + return result; +} + +static ScrDyn *bc_once_wrap(ScrDyn *target, ScrDyn *type, ScrDyn *listener) { + ScrClosure *closure = scr_closure_new(NULL, BC_ONCE_CAPS); + closure->caps[BC_ONCE_FIRED] = scr_box_new(SCR_BOX_F64); + closure->caps[BC_ONCE_TARGET] = bc_box(scr_dyn_retain(target)); + closure->caps[BC_ONCE_TYPE] = bc_box(scr_dyn_retain(type)); + closure->caps[BC_ONCE_LISTENER] = bc_box(scr_dyn_retain(listener)); + closure->caps[BC_ONCE_SELF] = bc_box(scr_dyn_retain(scr_dyn_undefined())); + scr_closure_retain(closure); + ScrDyn *wrapper = scr_dyn_new_func(closure, bc_once_call, 0, "native:events", "bound onceWrapper"); + scr_box_set_ref(closure->caps[BC_ONCE_SELF], scr_dyn_retain(wrapper)); + scr_closure_release(closure); + if (!bc_set(wrapper, "listener", scr_dyn_retain(listener))) { + scr_dyn_release(wrapper); + return NULL; + } + return wrapper; +} + +/* ── EventEmitter.prototype ─────────────────────────────────────────── */ + +static ScrDyn *bc_on(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; + ScrDyn *self = bc_this(); + ScrDyn *result = bc_require_object(self, "_events") + ? bc_add_listener(self, bc_arg(args, argc, 0), bc_arg(args, argc, 1), false) : NULL; + scr_dyn_release(self); + return result; +} + +static ScrDyn *bc_prepend(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; + ScrDyn *self = bc_this(); + ScrDyn *result = bc_require_object(self, "_events") + ? bc_add_listener(self, bc_arg(args, argc, 0), bc_arg(args, argc, 1), true) : NULL; + scr_dyn_release(self); + return result; +} + +static ScrDyn *bc_once_common(ScrDyn *const *args, size_t argc, bool prepend) { + ScrDyn *self = bc_this(); + ScrDyn *type = bc_arg(args, argc, 0); + ScrDyn *listener = bc_arg(args, argc, 1); + ScrDyn *result = NULL; + if (bc_check_listener(listener) && bc_require_object(self, "_events")) { + ScrDyn *wrapper = bc_once_wrap(self, type, listener); + if (wrapper) { + result = bc_add_listener(self, type, wrapper, prepend); + scr_dyn_release(wrapper); + } + } + scr_dyn_release(self); + return result; +} + +static ScrDyn *bc_once(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; + return bc_once_common(args, argc, false); +} + +static ScrDyn *bc_prepend_once(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; + return bc_once_common(args, argc, true); +} + +static ScrDyn *bc_off(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; + ScrDyn *self = bc_this(); + ScrDyn *result = bc_require_object(self, "_events") + ? bc_remove_listener(self, bc_arg(args, argc, 0), bc_arg(args, argc, 1)) : NULL; + scr_dyn_release(self); + return result; +} + +/* removeAllListeners(type?): with no 'removeListener' watcher the table is + * reset; otherwise every listener is removed through removeListener, last + * first, so the watcher sees each one (and itself last). */ +static ScrDyn *bc_remove_all(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; + ScrDyn *self = bc_this(); + if (!bc_require_object(self, "_events")) { + scr_dyn_release(self); + return NULL; + } + ScrDyn *type = bc_arg(args, argc, 0); + bool all = argc == 0; + ScrDyn *events = bc_get(self, "_events"); + ScrDyn *watcher = NULL; + if (!events) goto fail; + if (bc_undefined(events)) goto done; + watcher = bc_get(events, "removeListener"); + if (!watcher) goto fail; + if (bc_undefined(watcher)) { + if (all) { + if (!bc_set(self, "_events", scr_dyn_new_obj_null_proto()) || !bc_set(self, "_eventsCount", scr_dyn_new_num(0))) + goto fail; + } else { + ScrDyn *present = bc_get_key(events, type); + if (!present) goto fail; + bool has = !bc_undefined(present); + scr_dyn_release(present); + if (has) { + ScrDyn *count = bc_get(self, "_eventsCount"); + if (!count) goto fail; + double left = bc_number(count) - 1; + scr_dyn_release(count); + if (left == 0) { + if (!bc_set(self, "_events", scr_dyn_new_obj_null_proto()) || !bc_set(self, "_eventsCount", scr_dyn_new_num(0))) + goto fail; + } else { + if (!bc_set(self, "_eventsCount", scr_dyn_new_num(left))) goto fail; + scr_dyn_key_delete_computed(events, type, false); + if (scr_exc_pending()) goto fail; + } + } + } + goto done; + } + if (all) { + ScrDyn *keys = scr_dyn_own_keys(events); + if (!keys) goto fail; + double length = scr_dyn_arr_len(keys); + for (double i = 0; i < length; i++) { + ScrDyn *key = scr_dyn_index_get(keys, i); + if (!key) { + scr_dyn_release(keys); + goto fail; + } + bool skip = key->kind == SCR_DYN_STR && key->v.str->len == 14 && memcmp(key->v.str->data, "removeListener", 14) == 0; + ScrDyn *done_key = NULL; + if (!skip) { + ScrDyn *one[1] = { key }; + done_key = bc_invoke(self, "removeAllListeners", one, 1); + } + scr_dyn_release(key); + if (!skip && !done_key) { + scr_dyn_release(keys); + goto fail; + } + if (done_key) scr_dyn_release(done_key); + } + scr_dyn_release(keys); + ScrDyn *name = bc_key("removeListener"); + ScrDyn *one[1] = { name }; + ScrDyn *cleared = bc_invoke(self, "removeAllListeners", one, 1); + scr_dyn_release(name); + if (!cleared) goto fail; + scr_dyn_release(cleared); + if (!bc_set(self, "_events", scr_dyn_new_obj_null_proto()) || !bc_set(self, "_eventsCount", scr_dyn_new_num(0))) + goto fail; + goto done; + } + { + ScrDyn *listeners = bc_get_key(events, type); + if (!listeners) goto fail; + if (scr_dyn_is_callable(listeners)) { + ScrDyn *pair[2] = { type, listeners }; + ScrDyn *removed = bc_invoke(self, "removeListener", pair, 2); + scr_dyn_release(listeners); + if (!removed) goto fail; + scr_dyn_release(removed); + } else if (listeners->kind == SCR_DYN_ARR) { + for (double i = scr_dyn_arr_len(listeners) - 1; i >= 0; i--) { + ScrDyn *item = scr_dyn_index_get(listeners, i); + if (!item) { + scr_dyn_release(listeners); + goto fail; + } + ScrDyn *pair[2] = { type, item }; + ScrDyn *removed = bc_invoke(self, "removeListener", pair, 2); + scr_dyn_release(item); + if (!removed) { + scr_dyn_release(listeners); + goto fail; + } + scr_dyn_release(removed); + } + scr_dyn_release(listeners); + } else { + scr_dyn_release(listeners); + } + } +done: + if (watcher) scr_dyn_release(watcher); + if (events) scr_dyn_release(events); + return self; +fail: + if (watcher) scr_dyn_release(watcher); + if (events) scr_dyn_release(events); + scr_dyn_release(self); + return NULL; +} + +/* emit(type, ...args): Node's unhandled 'error' rule, then each listener + * with the emitter as its receiver over a copy of the list. */ +static ScrDyn *bc_emit(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; + ScrDyn *self = bc_this(); + if (!bc_require_object(self, "_events")) { + scr_dyn_release(self); + return NULL; + } + ScrDyn *type = bc_arg(args, argc, 0); + ScrDyn *const *rest = argc > 1 ? args + 1 : bc_unused_args; + size_t restc = argc > 1 ? argc - 1 : 0; + bool error = type->kind == SCR_DYN_STR && type->v.str->len == 5 && memcmp(type->v.str->data, "error", 5) == 0; + ScrDyn *events = bc_get(self, "_events"); + if (!events) goto fail_self; + if (!bc_undefined(events)) { + if (error) { + ScrDyn *handler = bc_get(events, "error"); + if (!handler) goto fail_events; + error = bc_undefined(handler); + scr_dyn_release(handler); + } + } else if (!error) { + scr_dyn_release(events); + scr_dyn_release(self); + return scr_dyn_new_bool(false); + } + if (error) { + ScrDyn *payload = restc ? rest[0] : scr_dyn_undefined(); + if (scr_dyn_err_instanceof(payload, SCR_ERR_ERROR)) { + scr_dyn_throw(scr_dyn_retain(payload)); + } else { + ScrStr *shown = scr_insp_dyn(payload, 0, 2); + ScrJsonBuf text; + scr_jb_init(&text); + scr_jb_puts(&text, "Unhandled error. ("); + scr_jb_put_str(&text, shown); + scr_jb_putc(&text, ')'); + scr_str_release(shown); + ScrStr *message = scr_jb_finish(&text); + scr_throw_error_msg_code(SCR_ERR_ERROR, message->data, message->len, "ERR_UNHANDLED_ERROR"); + scr_str_release(message); + } + goto fail_events; + } + ScrDyn *handler = bc_get_key(events, type); + if (!handler) goto fail_events; + bool fired = !bc_undefined(handler); + if (scr_dyn_is_callable(handler)) { + ScrDyn *result = bc_apply(handler, self, rest, restc); + scr_dyn_release(handler); + if (!result) goto fail_events; + scr_dyn_release(result); + } else if (handler->kind == SCR_DYN_ARR) { + /* arrayClone: listeners added or removed during the emit run next time */ + size_t length = (size_t)scr_dyn_arr_len(handler); + ScrDyn **listeners = malloc((length ? length : 1) * sizeof *listeners); + if (!listeners) scr_trap("out of memory"); + for (size_t i = 0; i < length; i++) listeners[i] = scr_dyn_index_get(handler, (double)i); + scr_dyn_release(handler); + bool ok = true; + for (size_t i = 0; i < length && ok; i++) { + if (!listeners[i]) { + ok = false; + break; + } + ScrDyn *result = bc_apply(listeners[i], self, rest, restc); + if (!result) ok = false; + else scr_dyn_release(result); + } + for (size_t i = 0; i < length; i++) + if (listeners[i]) scr_dyn_release(listeners[i]); + free(listeners); + if (!ok) goto fail_events; + } else { + scr_dyn_release(handler); + } + scr_dyn_release(events); + scr_dyn_release(self); + return scr_dyn_new_bool(fired); +fail_events: + scr_dyn_release(events); +fail_self: + scr_dyn_release(self); + return NULL; +} + +/* _listeners(target, type, unwrap): a fresh array. */ +static ScrDyn *bc_listeners_of(bool unwrap, ScrDyn *const *args, size_t argc) { + ScrDyn *self = bc_this(); + if (!bc_require_object(self, "_events")) { + scr_dyn_release(self); + return NULL; + } + ScrDyn *result = scr_dyn_new_arr(); + ScrDyn *events = bc_get(self, "_events"); + ScrDyn *list = NULL; + if (!events) goto fail; + if (bc_undefined(events)) goto done; + list = bc_get_key(events, bc_arg(args, argc, 0)); + if (!list) goto fail; + size_t length = scr_dyn_is_callable(list) ? 1 : list->kind == SCR_DYN_ARR ? (size_t)scr_dyn_arr_len(list) : 0; + for (size_t i = 0; i < length; i++) { + ScrDyn *item = scr_dyn_is_callable(list) ? scr_dyn_retain(list) : scr_dyn_index_get(list, (double)i); + if (!item) goto fail; + if (unwrap && scr_dyn_is_callable(item)) { + ScrDyn *inner = bc_get(item, "listener"); + if (!inner) { + scr_dyn_release(item); + goto fail; + } + if (!bc_undefined(inner)) { + scr_dyn_release(item); + item = inner; + } else { + scr_dyn_release(inner); + } + } + scr_dyn_arr_push(result, item); + } +done: + if (list) scr_dyn_release(list); + if (events) scr_dyn_release(events); + scr_dyn_release(self); + return result; +fail: + if (list) scr_dyn_release(list); + if (events) scr_dyn_release(events); + scr_dyn_release(result); + scr_dyn_release(self); + return NULL; +} + +static ScrDyn *bc_listeners(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; + return bc_listeners_of(true, args, argc); +} + +static ScrDyn *bc_raw_listeners(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; + return bc_listeners_of(false, args, argc); +} + +/* listenerCount(type, listener?) over an emitter's _events. */ +static ScrDyn *bc_count_listeners(ScrDyn *self, ScrDyn *type, ScrDyn *only) { + ScrDyn *events = bc_get(self, "_events"); + if (!events) return NULL; + double count = 0; + if (!bc_undefined(events)) { + ScrDyn *list = bc_get_key(events, type); + if (!list) { + scr_dyn_release(events); + return NULL; + } + size_t length = scr_dyn_is_callable(list) ? 1 : list->kind == SCR_DYN_ARR ? (size_t)scr_dyn_arr_len(list) : 0; + for (size_t i = 0; i < length; i++) { + if (!only) { + count++; + continue; + } + ScrDyn *item = scr_dyn_is_callable(list) ? scr_dyn_retain(list) : scr_dyn_index_get(list, (double)i); + if (!item) break; + bool same = scr_dyn_strict_eq(item, only); + if (!same && scr_dyn_is_callable(item)) { + ScrDyn *inner = bc_get(item, "listener"); + if (inner) { + same = scr_dyn_strict_eq(inner, only); + scr_dyn_release(inner); + } + } + scr_dyn_release(item); + if (same) count++; + } + scr_dyn_release(list); + } + scr_dyn_release(events); + if (scr_exc_pending()) return NULL; + return scr_dyn_new_num(count); +} + +static ScrDyn *bc_listener_count(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; + ScrDyn *self = bc_this(); + ScrDyn *result = bc_require_object(self, "_events") + ? bc_count_listeners(self, bc_arg(args, argc, 0), + argc > 1 && !bc_undefined(args[1]) && args[1]->kind != SCR_DYN_NULL ? args[1] : NULL) + : NULL; + scr_dyn_release(self); + return result; +} + +/* EventEmitter.listenerCount(emitter, type): the deprecated static. */ +static ScrDyn *bc_static_listener_count(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; + ScrDyn *emitter = bc_arg(args, argc, 0); + ScrDyn *type = bc_arg(args, argc, 1); + ScrDyn *method = bc_require_object(emitter, "listenerCount") ? bc_get(emitter, "listenerCount") : NULL; + if (!method) return NULL; + ScrDyn *result; + if (scr_dyn_is_callable(method)) result = bc_apply(method, emitter, &type, 1); + else result = bc_count_listeners(emitter, type, NULL); + scr_dyn_release(method); + return result; +} + +static ScrDyn *bc_event_names(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; + (void)args; + (void)argc; + ScrDyn *self = bc_this(); + if (!bc_require_object(self, "_eventsCount")) { + scr_dyn_release(self); + return NULL; + } + ScrDyn *count = bc_get(self, "_eventsCount"); + ScrDyn *result = NULL; + if (count) { + if (bc_number(count) > 0) { + ScrDyn *events = bc_get(self, "_events"); + if (events) { + result = scr_dyn_own_keys(events); + scr_dyn_release(events); + } + } else { + result = scr_dyn_new_arr(); + } + scr_dyn_release(count); + } + scr_dyn_release(self); + return result; +} + +static ScrDyn *bc_set_max(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; + ScrDyn *self = bc_this(); + ScrDyn *n = bc_arg(args, argc, 0); + if (n->kind != SCR_DYN_NUM) { + scr_dyn_arg_type_fail("n", "of type number", n); + scr_dyn_release(self); + return NULL; + } + if (n->v.num < 0 || n->v.num != n->v.num) { + ScrStr *shown = scr_dyn_string_coerce_js(n); + ScrJsonBuf text; + scr_jb_init(&text); + scr_jb_puts(&text, "The value of \"n\" is out of range. It must be a non-negative number. Received "); + if (shown) scr_jb_put_str(&text, shown); + if (shown) scr_str_release(shown); + ScrStr *message = scr_jb_finish(&text); + scr_throw_error_msg_code(SCR_ERR_RANGE, message->data, message->len, "ERR_OUT_OF_RANGE"); + scr_str_release(message); + scr_dyn_release(self); + return NULL; + } + if (!bc_require_object(self, "_maxListeners") || !bc_set(self, "_maxListeners", scr_dyn_retain(n))) { + scr_dyn_release(self); + return NULL; + } + return self; +} + +/* EventEmitter.defaultMaxListeners, Node's accessor over the one default + * the typed EventEmitter reads too (scr_events_emitter.c). The setter + * validates as Node's validateNumber(arg, 'defaultMaxListeners', 0). */ +static ScrDyn *bc_default_max_get(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; + (void)args; + (void)argc; + return scr_dyn_new_num(scr_emitter_get_default_max()); +} + +static ScrDyn *bc_default_max_set(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; + static const char name[] = "defaultMaxListeners"; + ScrStr *key = scr_str_new(name, sizeof name - 1); + scr_emitter_set_default_max_chk(argc ? args[0] : scr_dyn_undefined(), key); + scr_str_release(key); + if (scr_exc_pending()) return NULL; + return scr_dyn_retain(scr_dyn_undefined()); +} + +static void bc_define_default_max(ScrDyn *ctor) { + ScrDyn *descriptor = scr_dyn_new_obj(); + scr_dyn_obj_set(descriptor, "get", 3, + scr_dyn_new_func(scr_closure_new(NULL, 0), bc_default_max_get, 0, "native:events", "get")); + scr_dyn_obj_set(descriptor, "set", 3, + scr_dyn_new_func(scr_closure_new(NULL, 0), bc_default_max_set, 1, "native:events", "set")); + scr_dyn_obj_set(descriptor, "enumerable", 10, scr_dyn_new_bool(true)); + ScrStr *name = scr_str_new("defaultMaxListeners", 19); + ScrDyn *key = scr_dyn_new_str(name); + scr_str_release(name); + ScrDyn *defined = scr_dyn_define_property(ctor, key, descriptor); + if (defined) scr_dyn_release(defined); + scr_dyn_release(key); + scr_dyn_release(descriptor); +} + +static ScrDyn *bc_get_max(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; + (void)args; + (void)argc; + ScrDyn *self = bc_this(); + double max = bc_require_object(self, "_maxListeners") ? bc_max_listeners(self) : -1; + scr_dyn_release(self); + if (scr_exc_pending()) return NULL; + return scr_dyn_new_num(max); +} + +/* ── Stream.prototype.pipe (lib/internal/streams/legacy.js) ─────────── */ + +enum { BC_PIPE_SOURCE, BC_PIPE_DEST, BC_PIPE_ENDED, BC_PIPE_HANDLERS, BC_PIPE_CAPS }; +enum { BC_ONDATA, BC_ONDRAIN, BC_ONEND, BC_ONCLOSE, BC_ONERROR, BC_CLEANUP, BC_HANDLER_COUNT }; + +static ScrDyn *bc_pipe_handler(ScrClosure *state, int which); + +static ScrDyn *bc_pipe_get_handler(ScrClosure *state, int which) { + ScrDyn *handlers = bc_cap(state, BC_PIPE_HANDLERS); + ScrDyn *handler = scr_dyn_index_get(handlers, which); + scr_dyn_release(handlers); + return handler; +} + +/* Calls source/dest.removeListener for one pipe handler. */ +static bool bc_pipe_remove(ScrDyn *emitter, const char *event, ScrClosure *state, int which) { + ScrDyn *handler = bc_pipe_get_handler(state, which); + if (!handler) return false; + ScrDyn *name = bc_key(event); + ScrDyn *pair[2] = { name, handler }; + ScrDyn *removed = bc_invoke(emitter, "removeListener", pair, 2); + scr_dyn_release(name); + scr_dyn_release(handler); + if (!removed) return false; + scr_dyn_release(removed); + return true; +} + +static bool bc_pipe_on(ScrDyn *emitter, const char *event, ScrClosure *state, int which) { + ScrDyn *handler = bc_pipe_get_handler(state, which); + if (!handler) return false; + ScrDyn *name = bc_key(event); + ScrDyn *pair[2] = { name, handler }; + ScrDyn *added = bc_invoke(emitter, "on", pair, 2); + scr_dyn_release(name); + scr_dyn_release(handler); + if (!added) return false; + scr_dyn_release(added); + return true; +} + +static bool bc_pipe_cleanup(ScrClosure *state) { + /* A second run (dest emitting 'close' from end(), with cleanup still + * queued on the source's 'end') finds the listeners removed, as Node's + * repeated removeListener calls do. */ + ScrDyn *handlers = bc_cap(state, BC_PIPE_HANDLERS); + bool cleaned = handlers->kind == SCR_DYN_ARR && scr_dyn_arr_len(handlers) == 0; + scr_dyn_release(handlers); + if (cleaned) return true; + ScrDyn *source = bc_cap(state, BC_PIPE_SOURCE); + ScrDyn *dest = bc_cap(state, BC_PIPE_DEST); + bool ok = bc_pipe_remove(source, "data", state, BC_ONDATA) && + bc_pipe_remove(dest, "drain", state, BC_ONDRAIN) && + bc_pipe_remove(source, "end", state, BC_ONEND) && + bc_pipe_remove(source, "close", state, BC_ONCLOSE) && + bc_pipe_remove(source, "error", state, BC_ONERROR) && + bc_pipe_remove(dest, "error", state, BC_ONERROR) && + bc_pipe_remove(source, "end", state, BC_CLEANUP) && + bc_pipe_remove(source, "close", state, BC_CLEANUP) && + bc_pipe_remove(dest, "close", state, BC_CLEANUP); + scr_dyn_release(dest); + scr_dyn_release(source); + /* the handlers array held the closures that hold this state */ + if (ok) scr_box_set_ref(state->caps[BC_PIPE_HANDLERS], scr_dyn_new_arr()); + return ok; +} + +static ScrDyn *bc_pipe_event(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + ScrClosure *state = (ScrClosure *)scr_box_get_ref(closure->caps[0]); + int which = (int)scr_box_get_f64(closure->caps[1]); + ScrDyn *source = bc_cap(state, BC_PIPE_SOURCE); + ScrDyn *dest = bc_cap(state, BC_PIPE_DEST); + bool ok = true; + switch (which) { + case BC_ONDATA: { + ScrDyn *writable = bc_get(dest, "writable"); + if (!writable) { + ok = false; + break; + } + bool open = scr_dyn_truthy(writable); + scr_dyn_release(writable); + if (!open) break; + ScrDyn *chunk = bc_arg(args, argc, 0); + ScrDyn *written = bc_invoke(dest, "write", &chunk, 1); + if (!written) { + ok = false; + break; + } + bool full = written->kind == SCR_DYN_BOOL && !written->v.b; + scr_dyn_release(written); + if (full) { + ScrDyn *pause = bc_get(source, "pause"); + if (!pause) { + ok = false; + break; + } + if (scr_dyn_is_callable(pause)) { + ScrDyn *paused = bc_apply(pause, source, bc_unused_args, 0); + if (paused) scr_dyn_release(paused); + else ok = false; + } + scr_dyn_release(pause); + } + break; + } + case BC_ONDRAIN: { + ScrDyn *readable = bc_get(source, "readable"); + ScrDyn *resume = readable ? bc_get(source, "resume") : NULL; + if (!readable || !resume) { + ok = false; + } else if (scr_dyn_truthy(readable) && scr_dyn_is_callable(resume)) { + ScrDyn *resumed = bc_apply(resume, source, bc_unused_args, 0); + if (resumed) scr_dyn_release(resumed); + else ok = false; + } + if (readable) scr_dyn_release(readable); + if (resume) scr_dyn_release(resume); + break; + } + case BC_ONEND: + case BC_ONCLOSE: { + if (scr_box_get_f64(state->caps[BC_PIPE_ENDED]) != 0) break; + scr_box_set_f64(state->caps[BC_PIPE_ENDED], 1); + ScrDyn *done = which == BC_ONEND ? bc_invoke(dest, "end", bc_unused_args, 0) : NULL; + if (which == BC_ONCLOSE) { + ScrDyn *destroy = bc_get(dest, "destroy"); + if (!destroy) { + ok = false; + break; + } + if (scr_dyn_is_callable(destroy)) done = bc_apply(destroy, dest, bc_unused_args, 0); + else done = scr_dyn_retain(scr_dyn_undefined()); + scr_dyn_release(destroy); + } + if (done) scr_dyn_release(done); + else ok = false; + break; + } + case BC_ONERROR: { + ok = bc_pipe_cleanup(state); + if (!ok) break; + ScrDyn *rethrow = scr_dyn_this_get(); + ScrDyn *name = bc_key("error"); + ScrDyn *count = rethrow->kind == SCR_DYN_UNDEF ? NULL : bc_invoke(rethrow, "listenerCount", &name, 1); + scr_dyn_release(name); + if (count && bc_number(count) == 0) { + scr_dyn_throw(scr_dyn_retain(bc_arg(args, argc, 0))); + ok = false; + } else if (!count && scr_exc_pending()) { + ok = false; + } + if (count) scr_dyn_release(count); + scr_dyn_release(rethrow); + break; + } + case BC_CLEANUP: + ok = bc_pipe_cleanup(state); + break; + default: + break; + } + scr_dyn_release(dest); + scr_dyn_release(source); + scr_closure_release(state); + return ok ? scr_dyn_retain(scr_dyn_undefined()) : NULL; +} + +static ScrDyn *bc_pipe_handler(ScrClosure *state, int which) { + ScrClosure *closure = scr_closure_new(NULL, 2); + ScrBox *owner = scr_box_new_obj(scr_closure_retain_v, scr_closure_release_v, scr_closure_trace_v); + scr_box_set_ref(owner, scr_closure_retain(state)); + closure->caps[0] = owner; + closure->caps[1] = scr_box_new(SCR_BOX_F64); + scr_box_set_f64(closure->caps[1], which); + return scr_dyn_new_func(closure, bc_pipe_event, 0, "native:stream", which == BC_ONDATA ? "ondata" + : which == BC_ONDRAIN ? "ondrain" : which == BC_ONEND ? "onend" : which == BC_ONCLOSE ? "onclose" + : which == BC_ONERROR ? "onerror" : "cleanup"); +} + +/* Stream.prototype.pipe(dest, options): Node's legacy pipe for streams + * that are only EventEmitters. 'end' and 'close' end or destroy the + * destination unless options.end is false or the destination is stdio. */ +static ScrDyn *bc_pipe(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; + ScrDyn *source = bc_this(); + ScrDyn *dest = bc_arg(args, argc, 0); + ScrDyn *options = bc_arg(args, argc, 1); + if (!bc_require_object(source, "on") || !bc_require_object(dest, "on")) { + scr_dyn_release(source); + return NULL; + } + ScrClosure *state = scr_closure_new(NULL, BC_PIPE_CAPS); + state->caps[BC_PIPE_SOURCE] = bc_box(scr_dyn_retain(source)); + state->caps[BC_PIPE_DEST] = bc_box(scr_dyn_retain(dest)); + state->caps[BC_PIPE_ENDED] = scr_box_new(SCR_BOX_F64); + ScrDyn *handlers = scr_dyn_new_arr(); + for (int i = 0; i < BC_HANDLER_COUNT; i++) scr_dyn_arr_push(handlers, bc_pipe_handler(state, i)); + state->caps[BC_PIPE_HANDLERS] = bc_box(handlers); + bool ok = bc_pipe_on(source, "data", state, BC_ONDATA) && bc_pipe_on(dest, "drain", state, BC_ONDRAIN); + bool end_dest = true; + if (ok && options->kind != SCR_DYN_UNDEF && options->kind != SCR_DYN_NULL) { + ScrDyn *end = bc_get(options, "end"); + if (!end) ok = false; + else { + end_dest = !(end->kind == SCR_DYN_BOOL && !end->v.b); + scr_dyn_release(end); + } + } + if (ok && end_dest) { + ScrDyn *stdio = bc_get(dest, "_isStdio"); + if (!stdio) ok = false; + else { + end_dest = !scr_dyn_truthy(stdio); + scr_dyn_release(stdio); + } + } + if (ok && end_dest) + ok = bc_pipe_on(source, "end", state, BC_ONEND) && bc_pipe_on(source, "close", state, BC_ONCLOSE); + if (ok) { + /* prependListener(source, 'error', onerror) and the same on dest */ + ScrDyn *onerror = bc_pipe_get_handler(state, BC_ONERROR); + ScrDyn *name = bc_key("error"); + ScrDyn *pair[2] = { name, onerror }; + ScrDyn *a = onerror ? bc_invoke(source, "prependListener", pair, 2) : NULL; + ScrDyn *b = a ? bc_invoke(dest, "prependListener", pair, 2) : NULL; + ok = a && b; + if (a) scr_dyn_release(a); + if (b) scr_dyn_release(b); + scr_dyn_release(name); + if (onerror) scr_dyn_release(onerror); + } + if (ok) + ok = bc_pipe_on(source, "end", state, BC_CLEANUP) && bc_pipe_on(source, "close", state, BC_CLEANUP) && + bc_pipe_on(dest, "close", state, BC_CLEANUP); + ScrDyn *result = NULL; + if (ok) { + ScrDyn *name = bc_key("pipe"); + ScrDyn *pair[2] = { name, source }; + ScrDyn *emitted = bc_invoke(dest, "emit", pair, 2); + scr_dyn_release(name); + if (emitted) { + scr_dyn_release(emitted); + result = scr_dyn_retain(dest); + } + } + scr_closure_release(state); + scr_dyn_release(source); + return result; +} + +/* ── the constructors ───────────────────────────────────────────────── */ + +static ScrDyn *bc_construct(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + int kind = (int)scr_box_get_f64(closure->caps[0]); + ScrDyn *self = bc_this(); + ScrDyn *result = NULL; + if (kind == BC_EMITTER || kind == BC_STREAM) { + if (bc_emitter_init(self, bc_arg(args, argc, 0))) result = scr_dyn_retain(scr_dyn_undefined()); + } else { + char message[160]; + snprintf(message, sizeof message, + "'%s' as the base of a constructor function (util.inherits or %s.call(this)) is not supported yet", + bc_names[kind], bc_names[kind]); + scr_throw_error_msg_code(SCR_ERR_ERROR, message, strlen(message), "SC2020"); + } + scr_dyn_release(self); + return result; +} + +static void bc_method(ScrDyn *prototype, const char *name, ScrDynThunk thunk, uint32_t arity) { + ScrDyn *fn = scr_dyn_new_func(scr_closure_new(NULL, 0), thunk, arity, "native:events", name); + bc_set(prototype, name, fn); +} + +static void bc_alias(ScrDyn *prototype, const char *alias, const char *name) { + ScrDyn *fn = bc_get(prototype, name); + if (fn) bc_set(prototype, alias, fn); +} + +static void bc_teardown(void) { + for (int i = 0; i < BC_COUNT; i++) { + if (!bc_ctors[i]) continue; + ScrDyn *ctor = bc_ctors[i]; + /* prototype.constructor, EventEmitter.EventEmitter and Stream.Stream + * point back at the constructor, and Stream.Readable … at constructors + * whose own prototype is Stream: break those cycles first. */ + ScrDyn *prototype = bc_get(ctor, "prototype"); + if (prototype && prototype->kind == SCR_DYN_OBJ) bc_set(prototype, "constructor", scr_dyn_retain(scr_dyn_undefined())); + if (prototype) scr_dyn_release(prototype); + bc_set(ctor, bc_names[i], scr_dyn_retain(scr_dyn_undefined())); + if (i == BC_STREAM) + for (int other = BC_READABLE; other < BC_COUNT; other++) + bc_set(ctor, bc_names[other], scr_dyn_retain(scr_dyn_undefined())); + if (scr_exc_pending()) scr_exc_clear(); + } + for (int i = 0; i < BC_COUNT; i++) { + if (!bc_ctors[i]) continue; + ScrDyn *ctor = bc_ctors[i]; + bc_ctors[i] = NULL; + scr_dyn_release(ctor); + } + for (int i = 0; i < BC_SYMBOLS; i++) { + if (bc_symbols[i]) scr_dyn_release(bc_symbols[i]); + bc_symbols[i] = NULL; + } + if (scr_exc_pending()) scr_exc_clear(); +} + +static ScrDyn *bc_new_class(int kind, ScrDyn *parent) { + ScrClosure *closure = scr_closure_new(NULL, 1); + closure->function_kind = 4; /* an ordinary function with its own prototype */ + closure->caps[0] = scr_box_new(SCR_BOX_F64); + scr_box_set_f64(closure->caps[0], kind); + ScrDyn *ctor = scr_dyn_new_func(closure, bc_construct, 1, "native:events", bc_names[kind]); + ScrDyn *prototype = parent ? NULL : scr_dyn_new_obj(); + if (parent) { + ScrDyn *base = bc_get(parent, "prototype"); + prototype = scr_dyn_obj_create(base); + scr_dyn_release(base); + ScrDyn *linked = scr_dyn_set_prototype(ctor, parent); + if (linked) scr_dyn_release(linked); + } + bc_set(prototype, "constructor", scr_dyn_retain(ctor)); + bc_set(ctor, "prototype", scr_dyn_retain(prototype)); + if (kind == BC_EMITTER) { + bc_set(prototype, "_events", scr_dyn_retain(scr_dyn_undefined())); + bc_set(prototype, "_eventsCount", scr_dyn_new_num(0)); + bc_set(prototype, "_maxListeners", scr_dyn_retain(scr_dyn_undefined())); + bc_set_key(prototype, bc_symbol(BC_CAPTURE), scr_dyn_new_bool(false)); + bc_method(prototype, "setMaxListeners", bc_set_max, 1); + bc_method(prototype, "getMaxListeners", bc_get_max, 0); + bc_method(prototype, "emit", bc_emit, 1); + bc_method(prototype, "addListener", bc_on, 2); + bc_alias(prototype, "on", "addListener"); + bc_method(prototype, "prependListener", bc_prepend, 2); + bc_method(prototype, "once", bc_once, 2); + bc_method(prototype, "prependOnceListener", bc_prepend_once, 2); + bc_method(prototype, "removeListener", bc_off, 2); + bc_alias(prototype, "off", "removeListener"); + bc_method(prototype, "removeAllListeners", bc_remove_all, 1); + bc_method(prototype, "listeners", bc_listeners, 1); + bc_method(prototype, "rawListeners", bc_raw_listeners, 1); + bc_method(prototype, "listenerCount", bc_listener_count, 2); + bc_method(prototype, "eventNames", bc_event_names, 0); + bc_set(ctor, "EventEmitter", scr_dyn_retain(ctor)); + bc_define_default_max(ctor); + ScrDyn *count = scr_dyn_new_func(scr_closure_new(NULL, 0), bc_static_listener_count, 2, "native:events", "listenerCount"); + bc_set(ctor, "listenerCount", count); + } else if (kind == BC_STREAM) { + ScrDyn *pipe = scr_dyn_new_func(scr_closure_new(NULL, 0), bc_pipe, 2, "native:stream", "pipe"); + bc_set(prototype, "pipe", pipe); + } + scr_dyn_release(prototype); + if (scr_exc_pending()) scr_exc_clear(); + return ctor; +} + +/* All seven constructors are created together, each after its parent, so + * Stream's static members name the same constructors every other read + * answers. */ +static ScrDyn *bc_class(int kind) { + if (!bc_ctors[kind]) { + static const int parents[BC_COUNT] = { + -1, BC_EMITTER, BC_STREAM, BC_STREAM, BC_READABLE, BC_DUPLEX, BC_TRANSFORM, + }; + for (int each = 0; each < BC_COUNT; each++) { + if (bc_ctors[each]) continue; + bc_ctors[each] = bc_new_class(each, parents[each] >= 0 ? bc_ctors[parents[each]] : NULL); + } + /* Registered after the constructors exist, so the teardown runs before + * the runtime state their properties were stored in (scr_atexit is + * last in, first out). */ + if (!bc_teardown_registered) { + bc_teardown_registered = true; + scr_atexit(bc_teardown); + } + /* require("stream") is Stream itself, carrying its classes */ + ScrDyn *stream = bc_ctors[BC_STREAM]; + bc_set(stream, "Stream", scr_dyn_retain(stream)); + for (int other = BC_READABLE; other < BC_COUNT; other++) + bc_set(stream, bc_names[other], scr_dyn_retain(bc_ctors[other])); + if (scr_exc_pending()) scr_exc_clear(); + } + return bc_ctors[kind]; +} + +/* The constructor value for kind (the BC_* order), +1. */ +ScrDyn *scr_dyn_builtin_class(double kind) { + int index = (int)kind; + if (index < 0 || index >= BC_COUNT) scr_trap("scriptc: unknown builtin class value\n"); + return scr_dyn_retain(bc_class(index)); +} + +/* OrdinaryHasInstance over the value's prototype chain. */ +static bool bc_chain_has(ScrDyn *value, ScrDyn *prototype) { + if (value->kind != SCR_DYN_OBJ && value->kind != SCR_DYN_ARR && value->kind != SCR_DYN_FUNC && + value->kind != SCR_DYN_PROXY) + return false; + ScrDyn *current = scr_dyn_get_prototype(value); + while (current && current->kind != SCR_DYN_NULL) { + if (scr_dyn_strict_eq(current, prototype)) { + scr_dyn_release(current); + return true; + } + ScrDyn *next = scr_dyn_get_prototype(current); + scr_dyn_release(current); + current = next; + } + if (current) scr_dyn_release(current); + return false; +} + +/* `value instanceof `. Node's Writable also answers true for an + * object whose _writableState is a WritableState, which no untyped object + * has, so the prototype chain answers for every kind. */ +bool scr_dyn_builtin_instanceof(ScrDyn *value, double kind) { + int index = (int)kind; + if (index < 0 || index >= BC_COUNT) scr_trap("scriptc: unknown builtin class value\n"); + ScrDyn *prototype = bc_get(bc_class(index), "prototype"); + if (!prototype) return false; + bool found = bc_chain_has(value, prototype); + scr_dyn_release(prototype); + return found; +} + +/* `value instanceof callee` for an untyped constructor function. */ +bool scr_dyn_function_instanceof(ScrDyn *value, ScrDyn *callee) { + if (!scr_dyn_is_callable(callee)) { + static const char message[] = "Right-hand side of 'instanceof' is not callable"; + scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + return false; + } + ScrDyn *prototype = bc_get(callee, "prototype"); + if (!prototype) return false; + bool found = false; + if (prototype->kind == SCR_DYN_OBJ || prototype->kind == SCR_DYN_ARR || prototype->kind == SCR_DYN_FUNC || + prototype->kind == SCR_DYN_PROXY) { + found = bc_chain_has(value, prototype); + } else if (value->kind == SCR_DYN_OBJ || value->kind == SCR_DYN_ARR || value->kind == SCR_DYN_FUNC) { + static const char message[] = "Function has non-object prototype 'undefined' in instanceof check"; + scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + } + scr_dyn_release(prototype); + return found; +} diff --git a/packages/runtime/src/scr_inspect.c b/packages/runtime/src/scr_inspect.c index be2d52513..c1673891b 100644 --- a/packages/runtime/src/scr_inspect.c +++ b/packages/runtime/src/scr_inspect.c @@ -1097,6 +1097,16 @@ static ScrStr *scr_insp_class_tag(const char *cls, const char *tail) { return ib_take(&out); } +/* Node prints a @@toStringTag that ends with "." and the constructor name + * (WebAssembly.Module for Module) in place of the name, without the + * bracketed tag. */ +static bool insp_tag_names_class(const char *tag, const char *cls) { + if (!tag) return false; + size_t tag_len = strlen(tag), cls_len = strlen(cls); + return tag_len > cls_len + 1 && tag[tag_len - cls_len - 1] == '.' && + memcmp(tag + tag_len - cls_len, cls, cls_len) == 0; +} + /* A native Set or Map held as a checked-dynamic value: `Set(n) { a, b }`, * `Map(n) { k => v }`, at most 100 entries like Node's maxArrayLength. */ /* Node's formatWeakCollection: the entries of a WeakMap or WeakSet are not @@ -1495,18 +1505,13 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { ScrStr *body = scr_insp_dyn(view, recurse, depth); scr_dyn_release(view); InspBuf out = {0}; - /* util.inspect's getPrefix: a tag whose last dotted segment (the - * whole tag without a dot) is the constructor name replaces it - * (WebAssembly.Module {}); any other tag follows it in brackets. */ - const char *tag = ops->to_string_tag; - const char *dot = tag ? strrchr(tag, '.') : NULL; - if (tag && strcmp(ops->cls, dot && dot > tag ? dot + 1 : tag) == 0) { - ib_cstr(&out, tag); + if (insp_tag_names_class(ops->to_string_tag, ops->cls)) { + ib_cstr(&out, ops->to_string_tag); } else { ib_cstr(&out, ops->cls); - if (tag) { + if (ops->to_string_tag) { ib_cstr(&out, " ["); - ib_cstr(&out, tag); + ib_cstr(&out, ops->to_string_tag); ib_char(&out, ']'); } } diff --git a/packages/runtime/src/scr_lib.h b/packages/runtime/src/scr_lib.h index 690f84875..bae678292 100644 --- a/packages/runtime/src/scr_lib.h +++ b/packages/runtime/src/scr_lib.h @@ -174,6 +174,21 @@ double scr_process_uptime(void); /* perf_hooks performance.now(): fractional ms since process start (the * uptime anchor — Node's timeOrigin for a compiled program). */ double scr_perf_now(void); +/* perf_hooks event-loop metrics (scr_perf_loop.c). loop_mark records the + * loop's start (the emitted main calls it before the loop runs, only in + * programs that read the metric); loop_start answers it (0 before) and + * idle_time the idle ms since. The eld_* family is monitorEventLoopDelay's + * IntervalHistogram over a handle; eld_percentile expects 0 < p <= 100; eld_stat which: 0 min, 1 max, 2 mean, 3 stddev, + * 4 count, 5 exceeds. */ +void scr_perf_loop_mark(void); +double scr_perf_loop_start(void); +double scr_perf_idle_time(void); +double scr_eld_new(double resolution); +bool scr_eld_enable(double handle); +bool scr_eld_disable(double handle); +void scr_eld_reset(double handle); +double scr_eld_percentile(double handle, double percentile); +double scr_eld_stat(double handle, double which); /* perf_hooks user timing (scr_perf.c): mark, measure, the getEntries * family and the clears take the call's dyn argument vector (borrowed) * and apply Node's validation and errors. mark and measure answer 1 and diff --git a/packages/runtime/src/scr_perf_loop.c b/packages/runtime/src/scr_perf_loop.c new file mode 100644 index 000000000..68d3bf10f --- /dev/null +++ b/packages/runtime/src/scr_perf_loop.c @@ -0,0 +1,327 @@ +/* node:perf_hooks event-loop metrics: performance.eventLoopUtilization() + * and monitorEventLoopDelay()'s IntervalHistogram. + * + * Event-loop utilization. Node measures idle time as the time its loop + * spends blocked in the poll phase. A compiled program's main thread is + * busy exactly while it runs on the CPU (or blocks in a synchronous call), + * so the thread's CPU time stands in for Node's active time: idle is the + * wall time since the loop started minus that CPU time. The loop start is + * marked by the emitted main just before the loop runs, and only in + * programs that ask for the metric; before it, utilization is all zeros, + * like Node during the main module's synchronous evaluation. + * + * Event-loop delay. Each histogram samples on an unref'd interval at its + * resolution and records the time between samples in nanoseconds, like + * Node's ELDHistogram (Histogram::RecordDelta). Values land in an + * HdrHistogram-shaped counts array (lowest 1, three significant figures: + * 2048 sub-buckets per power of two), so min, max, mean, stddev and + * percentiles answer the bucket values Node's hdr_histogram answers. */ + +#include "scr_runtime.h" + +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#endif + +/* ── event-loop utilization ─────────────────────────────────────────── */ + +static SCR_TL double scr_perf_loop_start_ms = 0; /* performance.now() at loop start */ +static SCR_TL double scr_perf_loop_cpu0_ms = 0; /* thread CPU time at loop start */ + +/* The calling thread's CPU time in milliseconds; 0 where unavailable. */ +static double scr_perf_thread_cpu_ms(void) { +#if defined(_WIN32) + FILETIME created, exited, kernel, user; + if (!GetThreadTimes(GetCurrentThread(), &created, &exited, &kernel, &user)) return 0; + ULARGE_INTEGER k, u; + k.LowPart = kernel.dwLowDateTime; + k.HighPart = kernel.dwHighDateTime; + u.LowPart = user.dwLowDateTime; + u.HighPart = user.dwHighDateTime; + return (double)(k.QuadPart + u.QuadPart) / 1e4; /* 100ns units */ +#elif defined(CLOCK_THREAD_CPUTIME_ID) && !defined(__wasi__) + struct timespec ts; + if (clock_gettime(CLOCK_THREAD_CPUTIME_ID, &ts) != 0) return 0; + return (double)ts.tv_sec * 1000.0 + (double)ts.tv_nsec / 1e6; +#else + return 0; +#endif +} + +void scr_perf_loop_mark(void) { + if (scr_perf_loop_start_ms > 0) return; + scr_perf_loop_cpu0_ms = scr_perf_thread_cpu_ms(); + double now = scr_perf_now(); + /* Node's loopStart is positive once set; a mark at the very first + * microsecond still counts as started. */ + scr_perf_loop_start_ms = now > 0 ? now : 1e-6; +} + +double scr_perf_loop_start(void) { return scr_perf_loop_start_ms; } + +double scr_perf_idle_time(void) { + if (scr_perf_loop_start_ms <= 0) return 0; + double wall = scr_perf_now() - scr_perf_loop_start_ms; + double busy = scr_perf_thread_cpu_ms() - scr_perf_loop_cpu0_ms; + double idle = wall - busy; + if (idle < 0) idle = 0; + if (idle > wall) idle = wall; + return idle; +} + +/* ── HdrHistogram-shaped counts (lowest 1, 3 significant figures) ───── */ + +#define SCR_HDR_SUB_BUCKET_HALF_MAG 10 +#define SCR_HDR_SUB_BUCKET_HALF (1 << SCR_HDR_SUB_BUCKET_HALF_MAG) /* 1024 */ +#define SCR_HDR_SUB_BUCKET_COUNT (SCR_HDR_SUB_BUCKET_HALF * 2) /* 2048 */ +#define SCR_HDR_SUB_BUCKET_MASK ((int64_t)SCR_HDR_SUB_BUCKET_COUNT - 1) + +typedef struct { + int64_t *counts; /* owned, grown on demand */ + size_t counts_len; + int64_t total; + int64_t min_value; /* INT64_MAX while empty */ + int64_t max_value; /* 0 while empty */ + double count; /* Node's count_ (successful records) */ + uint64_t prev_ns; /* RecordDelta's previous sample; 0 = none */ + double resolution; /* ms */ + double timer; /* the sampling interval's handle while enabled, else 0 */ + ScrClosure *tick; /* owned: the sampling callback, identifies this histogram */ + bool enabled; +} ScrEld; + +static int scr_hdr_bucket_index(int64_t value) { + int pow2ceiling = 64 - __builtin_clzll((uint64_t)(value | SCR_HDR_SUB_BUCKET_MASK)); + return pow2ceiling - (SCR_HDR_SUB_BUCKET_HALF_MAG + 1); +} + +static size_t scr_hdr_counts_index(int64_t value) { + int bucket = scr_hdr_bucket_index(value); + int64_t sub = value >> bucket; + return (size_t)(((int64_t)(bucket + 1) << SCR_HDR_SUB_BUCKET_HALF_MAG) + (sub - SCR_HDR_SUB_BUCKET_HALF)); +} + +static int64_t scr_hdr_value_at_index(size_t index) { + int bucket = (int)(index >> SCR_HDR_SUB_BUCKET_HALF_MAG) - 1; + int64_t sub = (int64_t)(index & (SCR_HDR_SUB_BUCKET_HALF - 1)) + SCR_HDR_SUB_BUCKET_HALF; + if (bucket < 0) { + sub -= SCR_HDR_SUB_BUCKET_HALF; + bucket = 0; + } + return sub << bucket; +} + +static int64_t scr_hdr_range_size(int64_t value) { + int bucket = scr_hdr_bucket_index(value); + int64_t sub = value >> bucket; + int adjusted = sub >= SCR_HDR_SUB_BUCKET_COUNT ? bucket + 1 : bucket; + return (int64_t)1 << adjusted; +} + +static int64_t scr_hdr_lowest_equivalent(int64_t value) { + int bucket = scr_hdr_bucket_index(value); + int64_t sub = value >> bucket; + return sub << bucket; +} + +static int64_t scr_hdr_highest_equivalent(int64_t value) { + return scr_hdr_lowest_equivalent(value) + scr_hdr_range_size(value) - 1; +} + +static int64_t scr_hdr_median_equivalent(int64_t value) { + return scr_hdr_lowest_equivalent(value) + (scr_hdr_range_size(value) >> 1); +} + +/* Newton's square root: keeps this unit free of a libm reference. */ +static double scr_eld_sqrt(double x) { + if (!(x > 0)) return x == 0 ? 0 : NAN; + double r = x >= 1 ? x : 1; + for (int i = 0; i < 64; i++) { + double next = 0.5 * (r + x / r); + if (next == r) break; + r = next; + } + return r; +} + +static void scr_eld_record(ScrEld *h, int64_t value) { + if (value < 0) return; + size_t index = scr_hdr_counts_index(value); + if (index >= h->counts_len) { + size_t len = h->counts_len ? h->counts_len : SCR_HDR_SUB_BUCKET_COUNT; + while (len <= index) len *= 2; + int64_t *grown = realloc(h->counts, len * sizeof(int64_t)); + if (grown == NULL) return; + for (size_t i = h->counts_len; i < len; i++) grown[i] = 0; + h->counts = grown; + h->counts_len = len; + } + h->counts[index]++; + h->total++; + if (value != 0 && value < h->min_value) h->min_value = value; + if (value > h->max_value) h->max_value = value; + h->count += 1; +} + +static void scr_eld_clear(ScrEld *h) { + for (size_t i = 0; i < h->counts_len; i++) h->counts[i] = 0; + h->total = 0; + h->min_value = INT64_MAX; + h->max_value = 0; + h->count = 0; + h->prev_ns = 0; +} + +/* ── monitorEventLoopDelay ──────────────────────────────────────────── */ + +static SCR_TL ScrEld **scr_elds; +static SCR_TL size_t scr_elds_len; + +static ScrEld *scr_eld_of(double handle) { + if (!(handle >= 1) || handle > (double)scr_elds_len) return NULL; + return scr_elds[(size_t)handle - 1]; +} + +static void scr_eld_tick(ScrClosure *self) { + for (size_t i = 0; i < scr_elds_len; i++) { + ScrEld *h = scr_elds[i]; + if (h->tick != self) continue; + uint64_t now = scr_hrtime_ns(); + if (h->prev_ns > 0 && now > h->prev_ns) scr_eld_record(h, (int64_t)(now - h->prev_ns)); + h->prev_ns = now; + return; + } +} + +/* Histograms live for the thread's lifetime (Node frees them with their + * JS object); release them when the script ends so their callbacks and + * counts do not outlive it. */ +static void scr_eld_cleanup(void) { + for (size_t i = 0; i < scr_elds_len; i++) { + ScrEld *h = scr_elds[i]; + scr_closure_release(h->tick); + free(h->counts); + free(h); + } + free(scr_elds); + scr_elds = NULL; + scr_elds_len = 0; +} + +/* Node's validateInteger(resolution, 'options.resolution', 1): a catchable + * RangeError (ERR_OUT_OF_RANGE) for a non-integer or a value out of range, + * with the value as util.inspect renders it ("-0" included). */ +static bool scr_eld_resolution_ok(double resolution) { + bool integer = isfinite(resolution) && trunc(resolution) == resolution; + if (integer && resolution >= 1 && resolution <= 9007199254740991.0) return true; + char received[48]; + if (resolution == 0 && signbit(resolution)) + memcpy(received, "-0", 3); + else + scr_num_received(resolution, received); + char msg[192]; + int len = snprintf(msg, sizeof msg, + "The value of \"options.resolution\" is out of range. It must be %s. Received %s", + integer ? ">= 1 && <= 9007199254740991" : "an integer", received); + scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE"); + return false; +} + +double scr_eld_new(double resolution) { + if (!scr_eld_resolution_ok(resolution)) return 0; + if (scr_elds_len == 0 && scr_elds == NULL) scr_atexit(scr_eld_cleanup); + ScrEld *h = calloc(1, sizeof(ScrEld)); + ScrEld **grown = realloc(scr_elds, (scr_elds_len + 1) * sizeof(ScrEld *)); + if (h == NULL || grown == NULL) { + free(h); + if (grown != NULL) scr_elds = grown; + return 0; + } + scr_elds = grown; + h->min_value = INT64_MAX; + h->resolution = resolution; + h->tick = scr_closure_new((void *)scr_eld_tick, 0); + scr_elds[scr_elds_len++] = h; + return (double)scr_elds_len; +} + +bool scr_eld_enable(double handle) { + ScrEld *h = scr_eld_of(handle); + if (h == NULL || h->enabled) return false; + h->enabled = true; + h->timer = scr_set_interval(scr_closure_retain(h->tick), h->resolution); + scr_timer_unref(h->timer); + return true; +} + +bool scr_eld_disable(double handle) { + ScrEld *h = scr_eld_of(handle); + if (h == NULL || !h->enabled) return false; + h->enabled = false; + scr_clear_interval(h->timer); + h->timer = 0; + return true; +} + +void scr_eld_reset(double handle) { + ScrEld *h = scr_eld_of(handle); + if (h != NULL) scr_eld_clear(h); +} + +/* hdr_value_at_percentile; the compiler checked Node's 0 < p <= 100 range. */ +double scr_eld_percentile(double handle, double percentile) { + ScrEld *h = scr_eld_of(handle); + if (h == NULL || h->total == 0) return 0; + double requested = percentile < 100.0 ? percentile : 100.0; + int64_t wanted = (int64_t)((requested / 100.0) * (double)h->total + 0.5); + if (wanted < 1) wanted = 1; + int64_t seen = 0; + for (size_t i = 0; i < h->counts_len; i++) { + seen += h->counts[i]; + if (seen >= wanted) return (double)scr_hdr_highest_equivalent(scr_hdr_value_at_index(i)); + } + return 0; +} + +/* which: 0 min, 1 max, 2 mean, 3 stddev, 4 count, 5 exceeds. */ +double scr_eld_stat(double handle, double which) { + ScrEld *h = scr_eld_of(handle); + int w = (int)which; + if (h == NULL) return w == 0 ? (double)INT64_MAX : w >= 2 && w <= 3 ? NAN : 0; + switch (w) { + case 0: + if (h->counts_len > 0 && h->counts[0] > 0) return 0; + return h->min_value == INT64_MAX ? (double)INT64_MAX + : (double)scr_hdr_lowest_equivalent(h->min_value); + case 1: + return h->max_value == 0 ? 0 : (double)scr_hdr_highest_equivalent(h->max_value); + case 2: + case 3: { + if (h->total == 0) return NAN; + double sum = 0; + for (size_t i = 0; i < h->counts_len; i++) { + if (h->counts[i] == 0) continue; + sum += (double)h->counts[i] * (double)scr_hdr_median_equivalent(scr_hdr_value_at_index(i)); + } + double mean = sum / (double)h->total; + if (w == 2) return mean; + double geo = 0; + for (size_t i = 0; i < h->counts_len; i++) { + if (h->counts[i] == 0) continue; + double dev = (double)scr_hdr_median_equivalent(scr_hdr_value_at_index(i)) - mean; + geo += dev * dev * (double)h->counts[i]; + } + return scr_eld_sqrt(geo / (double)h->total); + } + case 4: + return h->count; + default: + return 0; + } +} diff --git a/packages/runtime/src/scr_runtime.h b/packages/runtime/src/scr_runtime.h index 59cafe208..bf01a4a9b 100644 --- a/packages/runtime/src/scr_runtime.h +++ b/packages/runtime/src/scr_runtime.h @@ -75,5 +75,7 @@ #include "scr_assert.h" #include "scr_console.h" #include "scr_fs_cp.h" +#include "scr_wasm_abi.h" +#include "scr_wasm.h" #endif /* SCR_RUNTIME_H */ diff --git a/packages/runtime/src/scr_stream.c b/packages/runtime/src/scr_stream.c index 5f0bb493d..56f9fd54b 100644 --- a/packages/runtime/src/scr_stream.c +++ b/packages/runtime/src/scr_stream.c @@ -6451,8 +6451,10 @@ static ScrDyn *scr_stream_view_call(ScrClosure *cb, ScrDyn *const *args, size_t end = option->kind == SCR_DYN_UNDEF || scr_dyn_truthy(option); scr_dyn_release(option); } + /* scr_stream_pipe_general: a JavaScript caller can pipe into the + * converted process.stdout / process.stderr, which a pipe never ends. */ ScrStream *returned = !strcmp(method, "pipe") - ? scr_stream_pipe(stream, destination, end) : scr_stream_unpipe(stream, destination); + ? scr_stream_pipe_general(stream, destination, end) : scr_stream_unpipe(stream, destination); scr_stream_release(returned); if (!strcmp(method, "pipe") && !scr_exc_pending()) result = scr_dyn_retain(first); } @@ -6619,3 +6621,104 @@ ScrStream *scr_crypto_hash_stream_piped(ScrCryptoHash *hash) { if (s) scr_crypto_hash_stream_hold(hash); return s; } + +/* ── the compiler's general stream representation ───────────────────── + * NodeJS.ReadableStream / NodeJS.WritableStream slots that also hold other + * streams compile as this unit's classes; the stdio handles convert into + * them where they are read or flow. Both entry points sit last in the unit + * and reference only global symbols and no new constant data, so programs + * that never convert link the same bytes as before. */ + +/* destroy() on a bridged child stdin closes the pipe, as Node's does. */ +static void scr_stream_child_stdin_destroy(ScrClosure *cb, ScrStream *s, ScrError *err) { + ScrChildWriter *writer = scr_box_get_ref(cb->caps[0]); + scr_child_writer_destroy(writer); + scr_child_writer_release(writer); + scr_stream_destroy_done(s, err ? scr_error_retain(err) : NULL); +} + +/* A child stdio read (the child.stdioStream libCall): the bridged stream + * the checked-dynamic ChildProcess view answers for the same key ("stdin", + * "stdout" or "stderr", passed by the compiled program), so both views + * share one stream per slot. The stdin stream (the only five-byte key) + * gains the destroy hook that closes the pipe. +1, or NULL for an unpiped + * slot or a throw. */ +ScrStream *scr_stream_child_stdio(void *child, const char *key, size_t key_len) { + ScrDyn *handle = scr_dyn_new_handle(child, SCR_DYNH_CHILD); + ScrDyn *box = scr_dyn_handle_ops_of(handle)->get(child, key, key_len); + scr_dyn_release(handle); + if (box == NULL) return NULL; + ScrStream *stream = box->kind == SCR_DYN_TYPED_REF ? scr_dyn_typed_ref_unbox(box) : NULL; + scr_dyn_release(box); + if (stream != NULL && key_len == 5 && stream->st->destroy_cb == NULL) { + ScrChildWriter *writer = scr_child_stdin(child); + if (writer != NULL) { + ScrClosure *destroy = scr_closure_new((void *)scr_stream_child_stdin_destroy, 1); + destroy->caps[0] = scr_box_new_obj(scr_child_writer_retain_v, scr_child_writer_release_v, NULL); + scr_box_set_ref(destroy->caps[0], writer); + stream->st->destroy_cb = destroy; + stream->st->destroy_inv = scr_stream_child_stdin_destroy; + } + } + return stream; +} + +/* process.stdout / process.stderr flowing into a Writable slot (the + * stream.stdioWritable libCall): a Writable whose _write hands each chunk + * to the descriptor through process.stdout.write's synchronous path and + * completes at once, so ordering against console output and direct writes + * is unchanged. The closure keeps the fd as given (1 or 2). Each thread + * keeps one per descriptor, so every conversion of process.stdout answers + * the same stream, as Node's process.stdout is one object: listeners, + * writable state and identity are shared. The cache is released at exit. */ +static SCR_TL ScrStream *scr_stream_stdio_writables[2]; +static void scr_stream_stdio_write(ScrClosure *cb, ScrStream *s, ScrBytes *chunk) { + int fd = (int)scr_box_get_f64(cb->caps[0]); + (void)scr_stdio_write(fd == 2 ? 2 : 1, chunk->data, chunk->len * scr_bytes_elem_size(chunk->elem)); + scr_stream_write_done(s, NULL); +} +static void scr_stream_stdio_writables_free(void) { + for (int i = 0; i < 2; i++) { + ScrStream *s = scr_stream_stdio_writables[i]; + scr_stream_stdio_writables[i] = NULL; + if (s) scr_stream_release(s); + } +} +ScrStream *scr_stream_stdio_writable(double fd) { + int i = fd == 2 ? 1 : 0; + if (scr_stream_stdio_writables[i] == NULL) { + if (scr_stream_stdio_writables[1 - i] == NULL) scr_atexit(scr_stream_stdio_writables_free); + ScrClosure *write = scr_closure_new((void *)scr_stream_stdio_write, 1); + write->caps[0] = scr_box_new(SCR_BOX_F64); + scr_box_set_f64(write->caps[0], i ? 2 : 1); + scr_stream_stdio_writables[i] = scr_stream_new_writable(-1, true, true, write, + scr_stream_stdio_write, NULL, NULL, NULL, NULL); + } + return scr_stream_retain(scr_stream_stdio_writables[i]); +} + +/* readable.pipe() into a Writable slot under the general representation + * (the readable.pipeGeneral libCall), and pipe() called from JavaScript on + * a native stream: Node never ends process.stdout or process.stderr from a + * pipe, so their converted Writable keeps `end` off. */ +ScrStream *scr_stream_pipe_general(ScrStream *src, ScrStream *dst, bool end) { + if (dst == scr_stream_stdio_writables[0] || dst == scr_stream_stdio_writables[1]) end = false; + return scr_stream_pipe(src, dst, end); +} + +/* A pipeline() stage that arrives as a checked-dynamic value (the + * stream.unboxDyn libCall): a boxed native stream unwraps by identity + * (+1); any other value throws the compiled program's not-supported + * message (Node also takes iterables and functions there). */ +ScrStream *scr_stream_unbox_dyn(const ScrDyn *value, const ScrStr *message) { + if (value->kind == SCR_DYN_TYPED_REF && value->v.typed_ref.type_key_len > 7 && + !memcmp(value->v.typed_ref.type_key, "object:", 7)) { + ScrStream *stream = scr_dyn_typed_ref_unbox(value); + const ScrVt *vt = stream->vt; + if ((vt->pre >= scr_readable_vt.pre && vt->pre <= scr_readable_vt.post) || + (vt->pre >= scr_writable_vt.pre && vt->pre <= scr_writable_vt.post)) return stream; + value->v.typed_ref.release(stream); + } + scr_throw_error_msg_code(SCR_ERR_ERROR, message->data, message->len, "SC2020"); + return NULL; +} diff --git a/packages/runtime/src/scr_stream.h b/packages/runtime/src/scr_stream.h index 82727c1dc..69da3dc0e 100644 --- a/packages/runtime/src/scr_stream.h +++ b/packages/runtime/src/scr_stream.h @@ -125,6 +125,17 @@ void scr_stream_init_passthrough(ScrStream *s, double rhwm, double whwm, bool au ScrStream *scr_stream_new_readable_dyn(struct ScrDyn *opts); void scr_stream_worker_stdio(void); ScrStream *scr_stream_new_writable_dyn(struct ScrDyn *opts); +/* The general stream representation's conversions (+1): a child's bridged + * stdio stream by key (NULL when unpiped), and process.stdout/stderr as a + * Writable (fd 1 or 2). */ +ScrStream *scr_stream_child_stdio(void *child, const char *key, size_t key_len); +ScrStream *scr_stream_stdio_writable(double fd); +/* readable.pipe() under the general representation: a converted + * process.stdout / process.stderr destination is never ended (dst +1). */ +ScrStream *scr_stream_pipe_general(ScrStream *src, ScrStream *dst, bool end); +/* A boxed native stream out of a checked-dynamic pipeline stage (+1), or + * NULL with `message` thrown as the not-supported Error. */ +ScrStream *scr_stream_unbox_dyn(const struct ScrDyn *value, const ScrStr *message); ScrStream *scr_stream_new_duplex_dyn(struct ScrDyn *opts); ScrStream *scr_stream_new_transform_dyn(struct ScrDyn *opts); ScrStream *scr_stream_new_passthrough_dyn(struct ScrDyn *opts); diff --git a/packages/runtime/src/scr_wasm.c b/packages/runtime/src/scr_wasm.c index 0884b9cf2..8f9244c28 100644 --- a/packages/runtime/src/scr_wasm.c +++ b/packages/runtime/src/scr_wasm.c @@ -58,6 +58,16 @@ #include #include +#if defined(_WIN32) +#ifndef NOMINMAX +#define NOMINMAX +#endif +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#endif + #if !defined(_WIN32) #define SCR_WASM_SIGNALS 1 #include @@ -122,7 +132,6 @@ void scr_wasm_table_copy(void *inst, int32_t dt, int32_t st, int32_t d, int32_t void scr_wasm_table_init(void *inst, int32_t t, int32_t seg, int32_t d, int32_t s, int32_t n); void scr_wasm_elem_drop(void *inst, int32_t seg); void scr_wasm_global_set_ref(void *cell, void *value); -void *scr_wasm_global_get_ref(void *cell); #define SCR_WASM_PAGE 65536u #define SCR_WASM_MAX_PAGES 65536u @@ -345,17 +354,13 @@ static _Thread_local ScrWasmEntry *scr_wasm_top; static _Thread_local bool scr_wasm_running; /* translated code (not an import) is on top */ static _Thread_local int scr_wasm_trap_code; -/* Externref values translated code holds without a reference (import - * results and values read from tables and globals): they stay alive until - * the outermost export call has converted its results - * (scr_wasm_exit_outermost), whatever later replaces the slot they came - * from. */ +/* Externref values an import returned: translated code holds them without + * a reference, so they stay alive until the outermost export call + * returns. */ static _Thread_local ScrDyn **scr_wasm_kept; static _Thread_local size_t scr_wasm_kept_count, scr_wasm_kept_cap; static void scr_wasm_keep(ScrDyn *d) { - /* A loop that reads the same slot keeps it once. */ - if (scr_wasm_kept_count && scr_wasm_kept[scr_wasm_kept_count - 1] == d) return; if (scr_wasm_kept_count == scr_wasm_kept_cap) { size_t cap = scr_wasm_kept_cap ? scr_wasm_kept_cap * 2 : 16; ScrDyn **grown = realloc(scr_wasm_kept, cap * sizeof *grown); @@ -370,13 +375,6 @@ static void scr_wasm_release_kept(void) { while (scr_wasm_kept_count) scr_dyn_release(scr_wasm_kept[--scr_wasm_kept_count]); } -/* After an outermost export, initializer or start call: its results hold - * their own references by now, so the kept values can go. Nested calls - * (an import that calls an export) leave them to the outermost one. */ -static void scr_wasm_exit_outermost(void) { - if (!scr_wasm_top && scr_wasm_kept_count) scr_wasm_release_kept(); -} - #ifdef SCR_WASM_ASAN /* Uninstrumented, so the sanitizer inserts no __asan_handle_no_return * before the jump; the abandoned frames are unpoisoned below. */ @@ -453,6 +451,15 @@ static int scr_wasm_enter(ScrWasmThunk fn, void *inst, const uint64_t *args, uin return scr_wasm_enter_ref(fn, NULL, NULL, inst, args, results); } +/* Called by each caller of scr_wasm_enter once it has converted the + * call's results: after the outermost call, the references kept for it + * (import results, and references a table or global replaced while the + * guest could still hold them) are released. The results' externrefs are + * borrowed from that list until then. */ +static void scr_wasm_leave(void) { + if (!scr_wasm_top && scr_wasm_kept_count) scr_wasm_release_kept(); +} + static void scr_wasm_init_thunk(void *inst, const uint64_t *args, uint64_t *results) { (void)args; (void)results; @@ -671,6 +678,44 @@ static void scr_wasm_thread_init(void) { } #endif +/* Checked memories (explicit bounds checks, no guard pages) keep one base + * address for their whole life: the ArrayBuffer of `memory.buffer` and + * every view of it alias the storage, so growing must not move it. The + * address space for the memory's largest size is reserved once and pages + * are committed as the memory grows. Hosts without address-space + * reservation (WASI) move the storage only while no buffer has been + * handed out; after that, growth that would move it fails (-1, a + * RangeError for Memory.prototype.grow). */ +#if defined(_WIN32) +#define SCR_WASM_STABLE_RESERVE 1 +static uint8_t *scr_wasm_reserve(uint64_t bytes) { + return VirtualAlloc(NULL, (SIZE_T)bytes, MEM_RESERVE, PAGE_NOACCESS); +} +static bool scr_wasm_commit(uint8_t *base, uint64_t from, uint64_t to) { + return to == from || VirtualAlloc(base + from, (SIZE_T)(to - from), MEM_COMMIT, PAGE_READWRITE) != NULL; +} +static void scr_wasm_unreserve(ScrWasmMemory *m) { + if (m->base) VirtualFree(m->base, 0, MEM_RELEASE); +} +#elif !defined(__wasi__) +#define SCR_WASM_STABLE_RESERVE 1 +static uint8_t *scr_wasm_reserve(uint64_t bytes) { +#if !defined(MAP_NORESERVE) +#define MAP_NORESERVE 0 +#endif + void *base = mmap(NULL, bytes, PROT_NONE, MAP_PRIVATE | MAP_ANON | MAP_NORESERVE, -1, 0); + return base == MAP_FAILED ? NULL : base; +} +static bool scr_wasm_commit(uint8_t *base, uint64_t from, uint64_t to) { + return to == from || mprotect(base + from, to - from, PROT_READ | PROT_WRITE) == 0; +} +static void scr_wasm_unreserve(ScrWasmMemory *m) { + if (m->base) munmap(m->base, m->reserved); +} +#else +static void scr_wasm_unreserve(ScrWasmMemory *m) { free(m->base); } +#endif + static ScrWasmMemory *scr_wasm_memory_retain(ScrWasmMemory *m) { m->rc++; return m; @@ -687,15 +732,13 @@ static bool scr_wasm_memory_unexposed(const ScrWasmMemory *m) { static void scr_wasm_memory_free(ScrWasmMemory *m) { for (size_t i = 0; i < m->exposed_count; i++) scr_bytes_release(m->exposed[i]); free(m->exposed); - for (size_t i = 0; i < m->retired_count; i++) free(m->retired[i]); - free(m->retired); #if SCR_WASM_SIGNALS if (m->guarded) { scr_wasm_range_remove(m->base); munmap(m->base, m->reserved); } else #endif - free(m->base); + scr_wasm_unreserve(m); free(m); } @@ -764,15 +807,29 @@ static ScrWasmMemory *scr_wasm_memory_new(uint64_t min, uint64_t max, bool guard #else (void)guarded; #endif +#if SCR_WASM_STABLE_RESERVE + uint64_t reserve = m->max_pages * SCR_WASM_PAGE; + m->base = reserve ? scr_wasm_reserve(reserve) : NULL; + if (reserve && (!m->base || !scr_wasm_commit(m->base, 0, m->size))) { + if (m->base) { + m->reserved = reserve; + scr_wasm_unreserve(m); + } +#else m->base = m->size ? calloc(1, m->size) : NULL; if (m->size && !m->base) { +#endif free(m); char msg[200]; snprintf(msg, sizeof msg, "%s: Out of memory: Cannot allocate Wasm memory for new instance", what); scr_wasm_range_error(msg); return NULL; } +#if SCR_WASM_STABLE_RESERVE + m->reserved = reserve; +#else m->reserved = m->size; +#endif return m; } @@ -789,23 +846,21 @@ static int64_t scr_wasm_memory_grow_by(ScrWasmMemory *m, uint64_t delta) { return (int64_t)old; } #endif - /* A new block, not realloc: ArrayBuffers created before the growth keep - * pointing into the old one, which stays alive with the memory. */ - uint8_t *grown = calloc(1, size); +#if SCR_WASM_STABLE_RESERVE + /* Committed pages read as zero; the base never moves. */ + if (size > m->reserved || !scr_wasm_commit(m->base, m->size, size)) return -1; + m->size = size; +#else + /* A buffer handed out aliases the storage: moving it would leave the + * buffer's views reading freed memory. */ + if (m->buffer_exposed) return -1; + uint8_t *grown = realloc(m->base, size); if (!grown) return -1; - if (m->base) { - uint8_t **retired = realloc(m->retired, (m->retired_count + 1) * sizeof *retired); - if (!retired) { - free(grown); - return -1; - } - memcpy(grown, m->base, m->size); - m->retired = retired; - m->retired[m->retired_count++] = m->base; - } + memset(grown + m->size, 0, size - m->size); m->base = grown; m->reserved = size; m->size = size; +#endif return (int64_t)old; } @@ -896,10 +951,19 @@ static void *scr_wasm_ref_retain(char elem, void *v) { return v; } +/* The reference a table or global slot drops: while a call runs, the guest + * may still hold it in a local or on its operand stack (it read the slot + * before the store), so it stays alive until the outermost call returns. */ +static void scr_wasm_drop_ref(ScrDyn *old) { + if (!old) return; + if (scr_wasm_top) scr_wasm_keep(old); + scr_dyn_release(old); +} + static void scr_wasm_table_store(ScrWasmTable *t, uint32_t i, void *v) { void *old = t->elems[i]; t->elems[i] = scr_wasm_ref_retain(t->elem, v); - if (t->elem == 'x') scr_dyn_release(old); + if (t->elem == 'x') scr_wasm_drop_ref(old); } static ScrWasmTable *scr_wasm_inst_table(void *inst, int32_t t) { return ((ScrWasmTable **)inst)[4 + t]; } @@ -907,9 +971,7 @@ static ScrWasmTable *scr_wasm_inst_table(void *inst, int32_t t) { return ((ScrWa void *scr_wasm_table_get(void *inst, int32_t ti, int32_t i) { ScrWasmTable *t = scr_wasm_inst_table(inst, ti); if ((uint32_t)i >= t->size) scr_wasm_trap(SCR_WASM_TRAP_TABLE_OOB); - void *v = t->elems[(uint32_t)i]; - if (t->elem == 'x' && v) scr_wasm_keep(v); - return v; + return t->elems[(uint32_t)i]; } void scr_wasm_table_set(void *inst, int32_t ti, int32_t i, void *value) { @@ -992,19 +1054,12 @@ static void scr_wasm_global_release(ScrWasmGlobal *g) { free(g); } -void *scr_wasm_global_get_ref(void *cell) { - ScrWasmGlobal *g = cell; - ScrDyn *v = (ScrDyn *)(uintptr_t)g->value; - if (v) scr_wasm_keep(v); - return v; -} - void scr_wasm_global_set_ref(void *cell, void *value) { ScrWasmGlobal *g = cell; ScrDyn *old = (ScrDyn *)(uintptr_t)g->value; if (value) scr_dyn_retain(value); g->value = (uint64_t)(uintptr_t)value; - scr_dyn_release(old); + scr_wasm_drop_ref(old); } /* ── handles ─────────────────────────────────────────────────────────── */ @@ -1243,8 +1298,8 @@ static ScrDyn *scr_wasm_export_call(ScrClosure *clo, ScrDyn *const *args, size_t result = scr_dyn_new_arr(); for (size_t i = 0; i < nr; i++) scr_dyn_arr_push(result, scr_wasm_to_js(res[i], r[i])); } + scr_wasm_leave(); } - scr_wasm_exit_outermost(); if (a != stack_args) free(a); if (r != stack_res) free(r); scr_wasm_inst_release(host); @@ -1351,6 +1406,7 @@ static ScrDyn *scr_wasm_memory_buffer(ScrWasmMemory *m) { m->buffer = NULL; } if (!m->buffer) { + m->buffer_exposed = true; m->buffer_size = m->size; ScrBytes *bytes = scr_bytes_from_external(m->base, (size_t)m->size); /* Earlier buffers JavaScript no longer holds need no tracking. */ @@ -1911,7 +1967,7 @@ static ScrDyn *scr_wasm_instance_from(const ScrWasmModuleDesc *desc, ScrDyn *imp bool was = scr_wasm_instantiating; scr_wasm_instantiating = true; int trap = scr_wasm_enter(scr_wasm_init_thunk, inst, NULL, NULL); - scr_wasm_exit_outermost(); + scr_wasm_leave(); scr_wasm_instantiating = was; if (trap) { if (trap == SCR_WASM_TRAP_DATA_SEGMENT) { @@ -1929,7 +1985,7 @@ static ScrDyn *scr_wasm_instance_from(const ScrWasmModuleDesc *desc, ScrDyn *imp } if (desc->start) { trap = scr_wasm_enter(scr_wasm_start_thunk, inst, NULL, NULL); - scr_wasm_exit_outermost(); + scr_wasm_leave(); if (trap) { scr_wasm_throw_trap(trap); return NULL; diff --git a/packages/runtime/src/scr_wasm.h b/packages/runtime/src/scr_wasm.h new file mode 100644 index 000000000..7b0a29689 --- /dev/null +++ b/packages/runtime/src/scr_wasm.h @@ -0,0 +1,23 @@ +/* scriptc runtime — the WebAssembly JavaScript API (scr_wasm.c): the entry + * points the LLVM backend calls for the wasm.* lib functions. Included by + * scr_runtime.h, which says where new declarations go. The contract with + * translated modules, including scr_wasm_registry, is in scr_wasm_abi.h. */ +#ifndef SCR_WASM_H +#define SCR_WASM_H + +#include "scr_async.h" +#include "scr_dyn.h" +#include "scr_wasm_abi.h" + +ScrPromise *scr_wasm_compile(ScrDyn *bytes); +bool scr_wasm_validate(ScrDyn *bytes); +ScrPromise *scr_wasm_instantiate(ScrDyn *source, ScrDyn *imports); +ScrDyn *scr_wasm_module_new(ScrDyn *bytes); +ScrDyn *scr_wasm_instance_new(ScrDyn *module, ScrDyn *imports); +bool scr_wasm_is(const ScrDyn *value, double kind); +ScrDyn *scr_wasm_error_new(double kind, ScrDyn *message, ScrDyn *options); +ScrDyn *scr_wasm_function(double kind); +ScrDyn *scr_wasm_namespace(void); +ScrDyn *scr_wasm_global_view(void); + +#endif /* SCR_WASM_H */ diff --git a/packages/runtime/src/scr_wasm_abi.h b/packages/runtime/src/scr_wasm_abi.h index 9f4fae47c..083a2f77a 100644 --- a/packages/runtime/src/scr_wasm_abi.h +++ b/packages/runtime/src/scr_wasm_abi.h @@ -25,8 +25,10 @@ * With guard pages (no SCR_WASM_DESC_BOUNDS_CHECKS) memory_base never * changes: the memory is reserved once at its largest size plus a guard * region, and grow only commits pages. With explicit bounds checks the - * translated code reloads memory_base and the byte length after every - * call. */ + * memory is reserved at its largest size without a guard region, so + * memory_base does not change either where the host can reserve address + * space; the translated code still reloads memory_base and the byte + * length after every call. */ #ifndef SCR_WASM_ABI_H #define SCR_WASM_ABI_H #include @@ -183,11 +185,7 @@ typedef struct ScrWasmMemory { struct ScrDyn *buffer; /* the cached ArrayBuffer */ uint64_t buffer_size; /* the size it was created at */ uint64_t declared_max; /* pages; UINT64_MAX when the memory has no maximum */ - /* Explicit-bounds memories (not guarded): the blocks grow replaced. - * ArrayBuffers created before a growth still point into them, so they - * live as long as the memory. */ - uint8_t **retired; - size_t retired_count; + bool buffer_exposed; /* a buffer over the storage was handed out */ /* The byte stores of every ArrayBuffer handed out over this memory's * storage (retained). The storage outlives the memory while JavaScript * still holds one of them or a view over it. */ diff --git a/tests/corpus/1052-comptime-records.ts b/tests/corpus/1052-comptime-records.ts index 47cccf7b3..658aaed32 100644 --- a/tests/corpus/1052-comptime-records.ts +++ b/tests/corpus/1052-comptime-records.ts @@ -43,3 +43,20 @@ const meta = comptime(() => { return { kind: "meta", tags, count: tags.length }; }); console.log(meta.kind, meta.tags.join("|"), meta.count); + +// A discriminated union whose arms both fit a "labeled" value without a +// label: the discriminant picks the arm. +type Item = { kind: "plain"; v: number } | { kind: "labeled"; v: number; label?: string }; +const items: Item[] = comptime( + () => + [ + { kind: "plain", v: 1 }, + { kind: "labeled", v: 2 }, + { kind: "labeled", v: 3, label: "x" }, + ] as Item[], +); +for (const item of items) { + if (item.kind === "labeled") console.log("labeled", item.v, item.label); + else console.log("plain", item.v); +} +console.log(JSON.stringify(items)); diff --git a/tests/corpus/5817-constructor-function-instanceof.cjs b/tests/corpus/5817-constructor-function-instanceof.cjs new file mode 100644 index 000000000..355f53a64 --- /dev/null +++ b/tests/corpus/5817-constructor-function-instanceof.cjs @@ -0,0 +1,15 @@ +// `this instanceof F` in a constructor function, in a program that uses no +// EventEmitter or stream constructor value: the call-without-new idiom +// returns a new instance, and instanceof follows the function's prototype. +"use strict"; +function Glob(pattern) { + if (!(this instanceof Glob)) return new Glob(pattern); + this.pattern = pattern; +} +Glob.prototype.describe = function () { + return `glob ${this.pattern}`; +}; +const called = Glob("*.ts"); +const constructed = new Glob("*.js"); +console.log(called.describe(), constructed.describe()); +console.log(called instanceof Glob, {} instanceof Glob, Object.create(Glob.prototype) instanceof Glob); diff --git a/tests/corpus/5818-constructor-function-emitter-defaults.cjs b/tests/corpus/5818-constructor-function-emitter-defaults.cjs new file mode 100644 index 000000000..d89383b3a --- /dev/null +++ b/tests/corpus/5818-constructor-function-emitter-defaults.cjs @@ -0,0 +1,53 @@ +// EventEmitter.defaultMaxListeners read and written through the untyped +// EventEmitter constructor (Node's accessor and its validation), and a +// legacy pipe whose destination emits 'close' synchronously from end(). +"use strict"; +const util = require("util"); +const EventEmitter = require("events"); +const Stream = require("stream"); + +function Bus() { + EventEmitter.call(this); +} +util.inherits(Bus, EventEmitter); + +console.log(EventEmitter.defaultMaxListeners); +console.log(new Bus().getMaxListeners()); +EventEmitter.defaultMaxListeners = 3; +console.log(EventEmitter.defaultMaxListeners, new Bus().getMaxListeners()); +for (const value of [-1, "4", Number.NaN]) { + try { + EventEmitter.defaultMaxListeners = value; + } catch (error) { + console.log(error.name, error.code, error.message); + } +} +console.log(EventEmitter.defaultMaxListeners); +EventEmitter.defaultMaxListeners = 10; + +function Source() { + Stream.call(this); + this.readable = true; +} +util.inherits(Source, Stream); + +function Sink() { + Stream.call(this); + this.writable = true; +} +util.inherits(Sink, Stream); +Sink.prototype.write = function (chunk) { + console.log("write", chunk); + return true; +}; +Sink.prototype.end = function () { + console.log("sink end"); + this.emit("close"); +}; + +const source = new Source(); +const sink = new Sink(); +source.pipe(sink); +source.emit("data", "a"); +console.log("emit end", source.emit("end")); +console.log("listeners", source.listenerCount("end"), source.listenerCount("data"), sink.listenerCount("close")); diff --git a/tests/corpus/6380-variadic-wrapper-arguments-apply.ts b/tests/corpus/6380-variadic-wrapper-arguments-apply.ts new file mode 100644 index 000000000..1a14b6e2f --- /dev/null +++ b/tests/corpus/6380-variadic-wrapper-arguments-apply.ts @@ -0,0 +1,77 @@ +// A generic higher-order wrapper over `(...args: any) => any` that forwards +// `arguments` with `fn.apply(this, arguments)`, and types its options with +// Parameters / ReturnType. Each call site specializes the wrapper to +// the wrapped function's exact signature. +type AnyFn = (...args: any) => any; + +interface TraceOptions { + name?: string; + tags?: (...args: Parameters) => Record; + onResult?: (result: ReturnType) => void; +} + +function trace(fn: F, opts: TraceOptions = {}): F { + return function (this: unknown) { + const args = arguments as unknown as Parameters; + const tags = opts.tags ? opts.tags(...args) : {}; + console.log("enter", opts.name ?? fn.name, JSON.stringify(tags)); + const result: ReturnType = fn.apply(this, arguments as any); + opts.onResult?.(result); + console.log("exit", opts.name ?? fn.name); + return result; + } as F; +} + +function add(a: number, b: number): number { + return a + b; +} +const tracedAdd = trace(add, { + tags: (a, b) => ({ a: String(a), b: String(b) }), + onResult: (r) => console.log("result", r), +}); +console.log(tracedAdd(2, 3)); +console.log(tracedAdd(10, -4)); + +// A wrapped function with an optional parameter: the omitted position +// arrives as undefined, in the wrapper and its tags callback alike. +function titled(name: string, title?: string): string { + return title ? `${title} ${name}` : name; +} +const tracedTitled = trace(titled, { tags: (name, title) => ({ name, title: title ?? "-" }) }); +console.log(tracedTitled("ada")); +console.log(tracedTitled("ada", "dr")); + +const greet = (who: string, punct: string): string => `hi ${who}${punct}`; +const tracedGreet = trace(greet); +console.log(tracedGreet("ada", "!")); + +const tick = trace(function tick(): number { + return 42; +}); +console.log(tick()); + +interface Point { + x: number; + y: number; +} +const norm = trace((p: Point, scale: number): Point => ({ x: p.x * scale, y: p.y * scale }), { + name: "norm", + tags: (p) => ({ x: String(p.x) }), +}); +console.log(JSON.stringify(norm({ x: 1, y: 2 }, 3))); + +// The arguments tuple spread straight into an optional callback. +function audit(fn: F, check?: (...args: Parameters) => boolean): F { + return function (this: unknown) { + console.log("audit", check?.(...(arguments as unknown as Parameters))); + return fn.apply(this, arguments as any); + } as F; +} +const audited = audit(add, (a, b) => a < b); +console.log(audited(1, 2), audited(5, 3)); +console.log(audit(add)(7, 8)); + +const logOnly = trace((msg: string): void => { + console.log("log:", msg); +}); +logOnly("done"); diff --git a/tests/corpus/6381-variadic-wrapper-async.ts b/tests/corpus/6381-variadic-wrapper-async.ts new file mode 100644 index 000000000..200454a5e --- /dev/null +++ b/tests/corpus/6381-variadic-wrapper-async.ts @@ -0,0 +1,81 @@ +// Variadic generic wrappers around async functions: the wrapper forwards +// `arguments`, awaits the wrapped promise and reports its settled value +// through an Awaited> callback; rejections propagate. +type AnyFn = (...args: any) => any; + +interface Opts { + name?: string; + tags?: (...args: Parameters) => Record; + onSettled?: (value: Awaited>) => void; +} + +function traceAsync Promise>(fn: F, opts: Opts = {}): F { + return async function (this: unknown) { + const args = arguments as unknown as Parameters; + const tags = opts.tags ? opts.tags(...args) : {}; + console.log("start", opts.name ?? fn.name, JSON.stringify(tags)); + try { + const value: Awaited> = await fn.apply(this, arguments as any); + opts.onSettled?.(value); + console.log("ok", opts.name ?? fn.name); + return value; + } catch (e) { + console.log("failed", opts.name ?? fn.name, (e as Error).message); + throw e; + } + } as F; +} + +// The settled result spelled as a conditional over ReturnType. +type Settled = ReturnType extends PromiseLike ? U : ReturnType; + +function summarized Promise>( + fn: F, + summarize: (value: Settled) => string, +): F { + return async function (this: unknown) { + const value: Settled = await fn.apply(this, arguments as any); + console.log("summary", summarize(value)); + return value; + } as F; +} + +async function fetchUser(id: number, prefix: string): Promise { + await Promise.resolve(); + if (id < 0) throw new Error(`bad id ${id}`); + return `${prefix}${id}`; +} + +const getUser = traceAsync(fetchUser, { + tags: (id) => ({ id: String(id) }), + onSettled: (v) => console.log("settled", v.toUpperCase()), +}); + +const sleepless = traceAsync(async (): Promise => 7, { name: "seven" }); + +interface Report { + ok: boolean; + items: string[]; +} +const report = summarized( + async (count: number): Promise => ({ ok: count > 0, items: ["a", "b"].slice(0, count) }), + (r) => `${r.ok}:${r.items.join("+")}`, +); +const counted = summarized(async (n: number): Promise => n * 2, (n) => `n=${n + 1}`); + +async function main(): Promise { + console.log(await getUser(1, "u-")); + console.log(await sleepless()); + console.log(JSON.stringify(await report(1))); + console.log(await counted(4)); + try { + await getUser(-2, "u-"); + } catch (e) { + console.log("caught", (e as Error).message); + } + const pending = getUser(3, "a-"); + const second = await getUser(4, "b-"); + console.log(second, await pending); +} + +main().then(() => console.log("main done")); diff --git a/tests/corpus/6382-variadic-wrapper-methods-this.ts b/tests/corpus/6382-variadic-wrapper-methods-this.ts new file mode 100644 index 000000000..54e043207 --- /dev/null +++ b/tests/corpus/6382-variadic-wrapper-methods-this.ts @@ -0,0 +1,46 @@ +// Variadic generic wrappers on methods: the wrapper passes its own `this` +// through `fn.apply(this, arguments)`, so a wrapped `function (this: C)` +// stored as a class property sees the receiver it was called on. +type AnyFn = (...args: any) => any; + +function logged(fn: F, label: string): F { + return function (this: unknown) { + console.log(`[${label}] call with ${arguments.length} args`); + return fn.apply(this, arguments as any); + } as F; +} + +class Counter { + count = 0; + step: number; + constructor(step: number) { + this.step = step; + } + bump = logged(function (this: Counter, times: number): number { + this.count += this.step * times; + return this.count; + }, "bump"); + reset = logged((): void => { + this.count = 0; + }, "reset"); +} + +const c = new Counter(5); +console.log(c.bump(1)); +console.log(c.bump(2)); +c.reset(); +console.log(c.count); + +class Repo { + base: string; + constructor(base: string) { + this.base = base; + } + load = logged(async function (this: Repo, id: string): Promise { + await Promise.resolve(); + return `${this.base}/${id}`; + }, "load"); +} + +const repo = new Repo("https://x.test"); +repo.load("7").then((url) => console.log("loaded", url)); diff --git a/tests/corpus/6383-variadic-wrapper-rest-tuple.ts b/tests/corpus/6383-variadic-wrapper-rest-tuple.ts new file mode 100644 index 000000000..64f1cbacc --- /dev/null +++ b/tests/corpus/6383-variadic-wrapper-rest-tuple.ts @@ -0,0 +1,54 @@ +// The type-safe variadic wrapper: rest tuples instead of `arguments`. +// `...args: A` with A bound to the wrapped function's parameter tuple +// flattens to that fixed signature; `fn.apply(this, args)`, `fn(...args)` +// and `opts.tags(...args)` forward the tuple's positions. +interface Opts { + name?: string; + tags?: (...args: A) => Record; + onResult?: (r: R) => void; +} + +function wrap(fn: (...args: A) => R, opts: Opts = {}): (...args: A) => R { + return function (this: unknown, ...args: A): R { + const tags = opts.tags ? opts.tags(...args) : {}; + console.log("enter", opts.name ?? fn.name, JSON.stringify(tags)); + const r = fn.apply(this, args); + opts.onResult?.(r); + return r; + }; +} + +function viaSpread(fn: (...args: A) => R): (...args: A) => R { + return (...args: A): R => { + console.log("spread", args.length); + return fn(...args); + }; +} + +function mul(a: number, b: number): number { + return a * b; +} +const m = wrap(mul, { tags: (a, b) => ({ a: String(a), b: String(b) }), onResult: (r) => console.log("r", r) }); +console.log(m(6, 7)); + +const concat = wrap((s: string, n: number, flag: boolean) => `${s}:${n}:${flag}`, { name: "concat" }); +console.log(concat("x", 1, true)); + +const opt = wrap((s: string, n?: number) => `${s}:${n ?? "none"}`, { tags: (s, n) => ({ s, n: String(n) }) }); +console.log(opt("a", undefined), opt("b", 2)); + +const zero = wrap(() => "nothing"); +console.log(zero()); + +const spreadMul = viaSpread(mul); +console.log(spreadMul(3, 3)); + +// A concrete tuple rest parameter has the same fixed signature. +function pair(...args: [string, number]): string { + return `${args[0]}=${args[1]}`; +} +console.log(pair("k", 9)); +const asValue: (k: string, v: number) => string = pair; +console.log(asValue("v", 1)); +const t: [string, number] = ["t", 2]; +console.log(pair(...t)); diff --git a/tests/corpus/6384-variadic-wrapper-nested.ts b/tests/corpus/6384-variadic-wrapper-nested.ts new file mode 100644 index 000000000..76f714402 --- /dev/null +++ b/tests/corpus/6384-variadic-wrapper-nested.ts @@ -0,0 +1,37 @@ +// Nested variadic wrappers: wrapping an already wrapped function, mixing +// the `arguments` form and the rest-tuple form. The inner wrapper's +// function binding gives the outer wrapper its `fn.name`. +type AnyFn = (...args: any) => any; + +function trace(fn: F, label?: string): F { + const wrapped = function (this: unknown) { + console.log("trace>", label ?? fn.name); + const out = fn.apply(this, arguments as any); + console.log("trace<", label ?? fn.name); + return out; + }; + return wrapped as unknown as F; +} + +function timed(fn: (...args: A) => R): (...args: A) => R { + return function (this: unknown, ...args: A): R { + console.log("timed>", fn.name, args.length); + return fn.apply(this, args); + }; +} + +function area(w: number, h: number): number { + return w * h; +} + +const twice = trace(trace(area)); +console.log(twice(2, 3)); + +const labelled = trace(trace(area, "inner"), "outer"); +console.log(labelled(4, 5)); + +const mixed = timed(trace(area)); +console.log(mixed(6, 7)); + +const mixed2 = trace(timed(area)); +console.log(mixed2(1, 9)); diff --git a/tests/corpus/6385-variadic-wrapper-shared-local-names.ts b/tests/corpus/6385-variadic-wrapper-shared-local-names.ts new file mode 100644 index 000000000..20712bc69 --- /dev/null +++ b/tests/corpus/6385-variadic-wrapper-shared-local-names.ts @@ -0,0 +1,75 @@ +// Variadic wrappers whose bodies declare the same local names: one only +// forwards its `arguments` (or its alias of them), the other also reads an +// element, so its tuple exists. Each wrapper forwards its own arguments, also +// when a block-local alias shadows an outer one in the same function. +type AnyFn = (...args: any) => any; + +function forward(fn: F): F { + return function (this: unknown) { + return fn.apply(this, arguments as any); + } as F; +} + +function logFirst(fn: F): F { + return function (this: unknown) { + console.log("first:", arguments[0], "of", arguments.length); + return fn.apply(this, arguments as any); + } as F; +} + +function forwardAlias(fn: F): F { + return function (this: unknown) { + const args = arguments as any; + return fn.apply(this, args); + } as F; +} + +function logFirstAlias(fn: F): F { + return function (this: unknown) { + const args = arguments as any; + console.log("alias first:", args[0]); + return fn.apply(this, args); + } as F; +} + +function shadowed(fn: F): F { + return function (this: unknown) { + const args = arguments as any; + console.log("outer first:", args[0]); + if (args.length > 1) { + const args = arguments as any; + return fn.apply(this, args); + } + return fn.apply(this, args); + } as F; +} + +function add3(a: number, b: number, c: number): number { + return a + b + c; +} + +function greet(name: string): string { + return "hi " + name; +} + +const sum = forward(add3); +console.log(sum(1, 2, 3)); +const hello = logFirst(greet); +console.log(hello("ada")); + +const sumAlias = forwardAlias(add3); +console.log(sumAlias(4, 5, 6)); +const helloAlias = logFirstAlias(greet); +console.log(helloAlias("grace")); + +// The other order: the wrapper that reads an element first. +const hello2 = logFirst(greet); +console.log(hello2("linus")); +const sum2 = forward(add3); +console.log(sum2(7, 8, 9)); + +// A block-local alias with the same name as the outer one. +const sumShadowed = shadowed(add3); +console.log(sumShadowed(10, 20, 30)); +const helloShadowed = shadowed(greet); +console.log(helloShadowed("ken")); diff --git a/tests/corpus/6390-catch-binding-predicate.ts b/tests/corpus/6390-catch-binding-predicate.ts new file mode 100644 index 000000000..82c8797b5 --- /dev/null +++ b/tests/corpus/6390-catch-binding-predicate.ts @@ -0,0 +1,42 @@ +// A catch binding narrowed by a user type predicate reads its fields. +interface ErrorLike { + message: string; + name: string; + code?: string; +} +function isErrorLike(value: unknown): value is ErrorLike { + return typeof value === "object" && value !== null && "message" in value && "name" in value; +} +interface Coded { + code: string; +} +function isCoded(value: unknown): value is Coded { + return typeof value === "object" && value !== null && typeof (value as Coded).code === "string"; +} +function report(fn: () => void): void { + try { + fn(); + } catch (err) { + if (isErrorLike(err)) { + console.log("error", { message: err ? err.message : "none", name: err.name, code: err.code ?? "-" }); + if (err.message.includes("disk")) console.log("disk problem"); + } else { + console.log("not error-like", String(err)); + } + if (isCoded(err) && err.code === "ENOSPC") console.log("coded", err.code); + } +} +report(() => { + throw new Error("disk full"); +}); +report(() => { + const e = new Error("no space") as Error & { code?: string }; + e.code = "ENOSPC"; + throw e; +}); +report(() => { + throw "plain"; +}); +report(() => { + throw { message: "obj", name: "Custom", code: "ENOSPC" }; +}); diff --git a/tests/corpus/6391-nullish-function-default.ts b/tests/corpus/6391-nullish-function-default.ts new file mode 100644 index 000000000..6721d97bf --- /dev/null +++ b/tests/corpus/6391-nullish-function-default.ts @@ -0,0 +1,13 @@ +// `??` with a function fallback whose signature differs from the optional +// callback's (fewer parameters, narrower result). +interface Opts { + tags?: (a: number, b: number) => Record; + label?: (n: number) => string; +} +function run(o: Opts, x: number, y: number): string { + const tags = o.tags ?? (() => ({})); + const label = o.label ?? ((n: number) => `#${n}`); + return `${label(x)} ${JSON.stringify(tags(x, y))}`; +} +console.log(run({}, 1, 2)); +console.log(run({ tags: (a, b) => ({ a, b }), label: (n) => `n${n}` }, 3, 4)); diff --git a/tests/corpus/6392-optional-chain-protocol-calls.ts b/tests/corpus/6392-optional-chain-protocol-calls.ts new file mode 100644 index 000000000..a08a4ee85 --- /dev/null +++ b/tests/corpus/6392-optional-chain-protocol-calls.ts @@ -0,0 +1,47 @@ +// Optional calls on an interface-typed method chain (a do-nothing tracer). +interface Span { + addTags(tags: Record): Span; +} +interface Scope { + active(): Span | null; +} +interface Tracer { + scope(): Scope; +} +class NoopSpan implements Span { + addTags(tags: Record): Span { + console.log("tags", JSON.stringify(tags)); + return this; + } +} +class NoopScope implements Scope { + span: Span | null; + constructor(span: Span | null) { + this.span = span; + } + active(): Span | null { + return this.span; + } +} +class NoopTracer implements Tracer { + s: Scope; + constructor(s: Scope) { + this.s = s; + } + scope(): Scope { + return this.s; + } +} +const quiet: Tracer = new NoopTracer(new NoopScope(null)); +const loud: Tracer = new NoopTracer(new NoopScope(new NoopSpan())); +function tagActiveSpan(tracer: Tracer, tags: Record): void { + tracer.scope().active()?.addTags(tags); +} +tagActiveSpan(quiet, { a: 1 }); +tagActiveSpan(loud, { b: 2 }); + +// Narrowed locals and non-null assertions reach the same protocol calls. +const scope2: Scope = new NoopScope(new NoopSpan()); +const current = scope2.active(); +current?.addTags({ c: 3 }); +scope2.active()!.addTags({ d: 4 }); diff --git a/tests/corpus/6393-function-types-any-params.ts b/tests/corpus/6393-function-types-any-params.ts new file mode 100644 index 000000000..ec7799c9d --- /dev/null +++ b/tests/corpus/6393-function-types-any-params.ts @@ -0,0 +1,8 @@ +// Function values whose parameters are typed any (optional or not). +function build(limit?: any): string { + return `limit=${limit ?? "none"}`; +} +const fns: Array<(limit?: any) => string> = [build]; +console.log(fns[0]!(), fns[0]!(5)); +const take: (v: any) => number = (v) => (typeof v === "number" ? v : -1); +console.log(take(3), take("x")); diff --git a/tests/corpus/6394-in-key-error-subclasses.ts b/tests/corpus/6394-in-key-error-subclasses.ts new file mode 100644 index 000000000..5ade6bcd1 --- /dev/null +++ b/tests/corpus/6394-in-key-error-subclasses.ts @@ -0,0 +1,31 @@ +// `'key' in err` on Error receivers, including subclasses that declare it. +class HttpError extends Error { + status: number; + constructor(message: string, status: number) { + super(message); + this.status = status; + } +} +function describe(err: Error): string { + if ("status" in err) return `status ${(err as HttpError).status}`; + if ("code" in err) return "has code"; + return `plain ${err.message}`; +} +console.log(describe(new Error("x"))); +console.log(describe(new HttpError("nf", 404))); +class Timeout extends HttpError { + get retryable(): boolean { + return true; + } +} +function retryable(err: Error): boolean { + return "retryable" in err; +} +console.log(retryable(new Error("x")), retryable(new HttpError("a", 500)), retryable(new Timeout("t", 504))); +console.log(describe(new Timeout("t", 504))); +// Keys every Error has: its own stack, and Error.prototype's and +// Object.prototype's members. +function inherent(err: Error): boolean[] { + return ["stack" in err, "toString" in err, "constructor" in err, "hasOwnProperty" in err]; +} +console.log(inherent(new Error("s")), inherent(new HttpError("h", 400)), inherent(new TypeError("t"))); diff --git a/tests/corpus/6395-generic-object-entries.ts b/tests/corpus/6395-generic-object-entries.ts new file mode 100644 index 000000000..772367a3a --- /dev/null +++ b/tests/corpus/6395-generic-object-entries.ts @@ -0,0 +1,7 @@ +// Object.entries over a generic record parameter. +function maskSecrets>(tags: T): Record { + return Object.fromEntries( + Object.entries(tags).map(([k, v]) => [k, k.includes("secret") ? "[redacted]" : v]), + ); +} +console.log(JSON.stringify(maskSecrets({ user: "ada", secretToken: "x", n: 1 }))); diff --git a/tests/corpus/6396-generic-any-instantiation.ts b/tests/corpus/6396-generic-any-instantiation.ts new file mode 100644 index 000000000..c3c9da060 --- /dev/null +++ b/tests/corpus/6396-generic-any-instantiation.ts @@ -0,0 +1,18 @@ +// A generic method instantiated at `any` from an untyped forwarding call. +type AnyFn = (...args: any) => any; +class Tracer { + trace(name: string, fn: () => T): T { + console.log("span", name); + return fn(); + } +} +const tracer = new Tracer(); +function traced(name: string, fn: F): F { + return function (this: unknown) { + const args = arguments; + const result = tracer.trace(name, () => fn.apply(this, args as any)); + return result; + } as F; +} +const add = traced("add", (a: number, b: number) => a + b); +console.log(add(1, 2)); diff --git a/tests/corpus/6397-event-loop-metrics.ts b/tests/corpus/6397-event-loop-metrics.ts new file mode 100644 index 000000000..8477dcc48 --- /dev/null +++ b/tests/corpus/6397-event-loop-metrics.ts @@ -0,0 +1,70 @@ +// Event-loop monitoring under @types/node: monitorEventLoopDelay's histogram, +// eventLoopUtilization deltas, and an unref'd interval typed NodeJS.Timeout. +import { monitorEventLoopDelay, performance } from "node:perf_hooks"; +import * as perfHooks from "node:perf_hooks"; +import { monitorEventLoopDelay as eld } from "node:perf_hooks"; + +const histogram = monitorEventLoopDelay({ resolution: 10 }); +console.log("enable", histogram.enable(), histogram.enable()); +const first = performance.eventLoopUtilization(); +console.log("before loop", first.idle, first.active, first.utilization); + +let last = performance.eventLoopUtilization(); +let ticks = 0; +// The unref'd interval keeps nothing alive; this timer holds the loop open +// until the third sample. +const keepAlive = setTimeout(() => console.log("never"), 10_000); +const interval = setInterval(() => { + const now = performance.eventLoopUtilization(); + const delta = performance.eventLoopUtilization(now, last); + last = now; + ticks++; + const p50 = histogram.percentile(50); + const p99 = histogram.percentile(99); + console.log( + "tick", + ticks, + delta.utilization >= 0 && delta.utilization <= 1, + delta.idle >= 0, + p50 > 0, + p99 >= p50, + histogram.max >= p99, + histogram.min <= p50, + histogram.count > 0, + ); + histogram.reset(); + console.log("after reset", histogram.count, histogram.max, histogram.min, Number.isNaN(histogram.mean)); + if (ticks === 3) { + clearInterval(interval); + clearTimeout(keepAlive); + console.log("disable", histogram.disable(), histogram.disable()); + const since = performance.eventLoopUtilization(first); + console.log("since", since.utilization > 0 && since.utilization <= 1, since.idle > 0); + try { + histogram.percentile(0); + } catch (e) { + console.log((e as Error).message, (e as { code?: string }).code); + } + } +}, 50); +interval.unref(); +console.log("hasRef", interval.hasRef()); + +// Other import spellings, and Node's resolution validation. +const aliased = eld(); +const namespaced = perfHooks.monitorEventLoopDelay({ resolution: 20 }); +console.log("spellings", aliased.enable(), namespaced.enable(), aliased.disable(), namespaced.disable()); +for (const resolution of [0, -0, -1, 1.5, NaN, Infinity, 1e16, 1]) { + try { + eld({ resolution }); + console.log("created", resolution); + } catch (e) { + console.log((e as Error).name, (e as { code?: string }).code, (e as Error).message); + } +} +// An undefined resolution takes Node's default. +function withResolution(resolution?: number): boolean { + const h = monitorEventLoopDelay({ resolution }); + return h.enable() && h.disable(); +} +console.log("defaults", withResolution(), withResolution(20), eld({ resolution: undefined }).enable()); diff --git a/tests/corpus/6398-conditional-fixed-arity.ts b/tests/corpus/6398-conditional-fixed-arity.ts new file mode 100644 index 000000000..b98bc146a --- /dev/null +++ b/tests/corpus/6398-conditional-fixed-arity.ts @@ -0,0 +1,66 @@ +// Conditional types over function types with fixed `any` and `any[]` +// parameters, as a generic wrapper's specialization sees them: a function +// that needs more arguments than the pattern passes takes the false +// branch, including through a tuple rest parameter, and a positional +// `any[]` parameter matches only functions whose parameter there accepts +// an array. +type FirstResult = F extends (a: any) => infer R ? R : "takes more"; +type NoArgResult = F extends () => infer R ? R : "takes some"; +type ArrayResult = F extends (items: any[]) => infer R ? R : "not array"; + +function label any>(fn: F, value: FirstResult): string { + const local: FirstResult = value; + return `${fn.name}: ${typeof local} ${String(local)}`; +} + +function labelNoArgs any>(fn: F, value: NoArgResult): string { + const local: NoArgResult = value; + return `${fn.name}: ${typeof local} ${String(local)}`; +} + +function labelArray any>(fn: F, value: ArrayResult): string { + const local: ArrayResult = value; + return `${fn.name}: ${typeof local} ${String(local)}`; +} + +function none(): boolean { + return true; +} +function one(a: string): number { + return a.length; +} +function two(a: string, b: string): number { + return a.length + b.length; +} +function optional(a: string, b?: string): number { + return a.length + (b?.length ?? 0); +} +function rest(a: string, ...more: string[]): number { + return a.length + more.length; +} +function pair(...args: [string, number]): number { + return args[0].length + args[1]; +} +function count(items: string[]): number { + return items.length; +} +function anything(value: unknown): string { + return typeof value; +} + +console.log(label(none, true)); +console.log(label(one, 3)); +console.log(label(two, "takes more")); +console.log(label(optional, 4)); +console.log(label(rest, 5)); +console.log(label(pair, "takes more")); +console.log(labelNoArgs(none, false)); +console.log(labelNoArgs(one, "takes some")); +console.log(labelNoArgs(optional, "takes some")); +console.log(labelNoArgs(pair, "takes some")); +console.log(labelArray(none, true)); +console.log(labelArray(one, "not array")); +console.log(labelArray(rest, "not array")); +console.log(labelArray(count, 7)); +console.log(labelArray(anything, "object")); +console.log(labelArray(two, "not array")); diff --git a/tests/corpus/6410-json-mixed-scalar-tables/extra.json b/tests/corpus/6410-json-mixed-scalar-tables/extra.json new file mode 100644 index 000000000..7af99c22f --- /dev/null +++ b/tests/corpus/6410-json-mixed-scalar-tables/extra.json @@ -0,0 +1 @@ +[["x", 1, "two", 3], [4, "five", null], ["six"], [null]] diff --git a/tests/corpus/6410-json-mixed-scalar-tables/main.ts b/tests/corpus/6410-json-mixed-scalar-tables/main.ts new file mode 100644 index 000000000..c60f962dc --- /dev/null +++ b/tests/corpus/6410-json-mixed-scalar-tables/main.ts @@ -0,0 +1,26 @@ +// JSON tables whose rows mix strings and numbers, the shape of a +// character-set table (`[["8140", 12288, 65292], ...]`) and of a +// code-point range map (`[[[0, 44], "disallowed"], [[65, 65], "mapped", +// [97]], ...]`). A package requires them at load or inside a function, and +// an ES module imports one; each row keeps its strings and numbers. +import { lookup, rowCount } from "code-table"; +import { mapChars, status } from "range-map"; +import extra from "./extra.json" with { type: "json" }; + +console.log(lookup("8150"), lookup("a1"), lookup("zzzz"), rowCount()); +const mapped = mapChars("AB-c9"); +console.log(mapped.text, mapped.error); +console.log(JSON.stringify(mapChars("a,b")), JSON.stringify(mapChars("{"))); +console.log(status(10), status(46), status(66), status(1000)); + +for (const row of extra) { + const head = row[0]; + const rest = row.slice(1); + console.log( + typeof head, + head, + rest.length, + rest.map((v) => (v === null ? "null" : typeof v === "number" ? v + 1 : v.toUpperCase())).join(","), + ); +} +console.log(JSON.stringify(extra)); diff --git a/tests/corpus/6410-json-mixed-scalar-tables/node_modules/code-table/index.d.ts b/tests/corpus/6410-json-mixed-scalar-tables/node_modules/code-table/index.d.ts new file mode 100644 index 000000000..ae7db1d98 --- /dev/null +++ b/tests/corpus/6410-json-mixed-scalar-tables/node_modules/code-table/index.d.ts @@ -0,0 +1,2 @@ +export declare function lookup(code: string): number | string; +export declare function rowCount(): number; diff --git a/tests/corpus/6410-json-mixed-scalar-tables/node_modules/code-table/index.js b/tests/corpus/6410-json-mixed-scalar-tables/node_modules/code-table/index.js new file mode 100644 index 000000000..dc10bb817 --- /dev/null +++ b/tests/corpus/6410-json-mixed-scalar-tables/node_modules/code-table/index.js @@ -0,0 +1,10 @@ +'use strict'; +const table = require('./table.json'); +exports.lookup = function (code) { + for (const row of table) if (row[0] === code) return row[1]; + return -1; +}; +exports.rowCount = function () { + // A second require of the same document answers the same rows. + return require('./table.json').length; +}; diff --git a/tests/corpus/6410-json-mixed-scalar-tables/node_modules/code-table/package.json b/tests/corpus/6410-json-mixed-scalar-tables/node_modules/code-table/package.json new file mode 100644 index 000000000..e15622d87 --- /dev/null +++ b/tests/corpus/6410-json-mixed-scalar-tables/node_modules/code-table/package.json @@ -0,0 +1 @@ +{ "name": "code-table", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" } diff --git a/tests/corpus/6410-json-mixed-scalar-tables/node_modules/code-table/table.json b/tests/corpus/6410-json-mixed-scalar-tables/node_modules/code-table/table.json new file mode 100644 index 000000000..5abaa6772 --- /dev/null +++ b/tests/corpus/6410-json-mixed-scalar-tables/node_modules/code-table/table.json @@ -0,0 +1 @@ +[["8140", 12288, 65292], ["8150", 65507, 65340], ["a1", "。", 62]] diff --git a/tests/corpus/6410-json-mixed-scalar-tables/node_modules/range-map/index.d.ts b/tests/corpus/6410-json-mixed-scalar-tables/node_modules/range-map/index.d.ts new file mode 100644 index 000000000..322bd20e1 --- /dev/null +++ b/tests/corpus/6410-json-mixed-scalar-tables/node_modules/range-map/index.d.ts @@ -0,0 +1,2 @@ +export declare function mapChars(s: string): { text: string; error: boolean }; +export declare function status(cp: number): string; diff --git a/tests/corpus/6410-json-mixed-scalar-tables/node_modules/range-map/index.js b/tests/corpus/6410-json-mixed-scalar-tables/node_modules/range-map/index.js new file mode 100644 index 000000000..481d93ad4 --- /dev/null +++ b/tests/corpus/6410-json-mixed-scalar-tables/node_modules/range-map/index.js @@ -0,0 +1,37 @@ +'use strict'; +var table = require('./table.json'); +function findStatus(val) { + var start = 0; + var end = table.length - 1; + while (start <= end) { + var mid = Math.floor((start + end) / 2); + var target = table[mid]; + if (target[0][0] <= val && target[0][1] >= val) { + return target; + } else if (target[0][0] > val) { + end = mid - 1; + } else { + start = mid + 1; + } + } + return null; +} +exports.status = function (cp) { + var found = findStatus(cp); + return found === null ? 'unknown' : found[1]; +}; +exports.mapChars = function (s) { + var error = false; + var out = ''; + for (var i = 0; i < s.length; ++i) { + var cp = s.codePointAt(i); + var found = findStatus(cp); + if (found === null) { error = true; continue; } + switch (found[1]) { + case 'disallowed': error = true; out += String.fromCodePoint(cp); break; + case 'mapped': out += String.fromCodePoint.apply(String, found[2]); break; + case 'valid': out += String.fromCodePoint(cp); break; + } + } + return { text: out, error: error }; +}; diff --git a/tests/corpus/6410-json-mixed-scalar-tables/node_modules/range-map/package.json b/tests/corpus/6410-json-mixed-scalar-tables/node_modules/range-map/package.json new file mode 100644 index 000000000..bc877e1e8 --- /dev/null +++ b/tests/corpus/6410-json-mixed-scalar-tables/node_modules/range-map/package.json @@ -0,0 +1 @@ +{ "name": "range-map", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" } diff --git a/tests/corpus/6410-json-mixed-scalar-tables/node_modules/range-map/table.json b/tests/corpus/6410-json-mixed-scalar-tables/node_modules/range-map/table.json new file mode 100644 index 000000000..900f9e316 --- /dev/null +++ b/tests/corpus/6410-json-mixed-scalar-tables/node_modules/range-map/table.json @@ -0,0 +1 @@ +[[[0,44],"disallowed"],[[45,46],"valid"],[[47,47],"disallowed"],[[48,57],"valid"],[[65,65],"mapped",[97]],[[66,67],"mapped",[98,99]],[[97,122],"valid"]] diff --git a/tests/corpus/6411-union-array-tuple-destructuring.ts b/tests/corpus/6411-union-array-tuple-destructuring.ts new file mode 100644 index 000000000..f62eba996 --- /dev/null +++ b/tests/corpus/6411-union-array-tuple-destructuring.ts @@ -0,0 +1,42 @@ +// Array destructuring of a value whose type is a union of array and tuple +// types: a regex match or a default row (`re.exec(s) || ["", "", ""]`), and +// functions answering either a fixed tuple or a list. Each position reads +// the arm the value holds; positions past a tuple's end and past an array's +// end read undefined, and defaults fire exactly there. +function scan(text: string): string { + const [match, head, word] = /(\w*)-(\w+)/.exec(text) || ["", "", ""]; + return `${match}|${head}|${word.toUpperCase()}`; +} +console.log(scan("ab-cd"), scan("none"), scan("-x")); + +function pick(flag: boolean): [string, number] | string[] { + return flag ? ["a", 1] : ["b", "c"]; +} +const [p, q] = pick(true); +const [r, s = "dflt", t = "z"] = pick(false); +const [u, , w = "w"] = pick(true); +console.log(p, q, r, s, t, u, w); +console.log(typeof q, typeof s); + +function pair(n: number): number[] | [number, number] { + return n > 1 ? [n, n * 2] : [n]; +} +const [x, y] = pair(7); +const [x1, y1 = -1] = pair(1); +console.log(x, y, x1, y1); + +function row(kind: string): [string, boolean] | [string, boolean, number] { + return kind === "long" ? [kind, true, 3] : [kind, false]; +} +for (const k of ["long", "short"]) { + const [name, flag, count = 0] = row(k); + console.log(name, flag, count); +} + +// A match or an empty string (`re.exec(s) || ""`): a string destructures +// by code point, so the empty string binds undefined. +function firstHit(text: string): string { + const [hit] = /\d+/.exec(text) || ""; + return hit ? `hit ${hit}` : "none"; +} +console.log(firstHit("ab12"), firstHit("ab")); diff --git a/tests/corpus/6453-constructor-function-emitter.cjs b/tests/corpus/6453-constructor-function-emitter.cjs new file mode 100644 index 000000000..d07453e14 --- /dev/null +++ b/tests/corpus/6453-constructor-function-emitter.cjs @@ -0,0 +1,114 @@ +// Constructor functions that inherit from EventEmitter the pre-class way: +// util.inherits and EventEmitter.call(this), the call-without-new guard, +// prototype methods that emit, and the listener API over the instance. +"use strict"; +const util = require("util"); +const { EventEmitter } = require("events"); + +util.inherits(Bus, EventEmitter); +function Bus(name) { + if (!(this instanceof Bus)) return new Bus(name); + EventEmitter.call(this); + this.name = name; + this.entries = []; +} +Bus.prototype.add = function (entry) { + this.entries.push(entry); + this.emit("added", entry, this.entries.length); + return this; +}; + +// A second level: Sub inherits Bus, and Bus's constructor runs through call. +function Sub(name) { + Bus.call(this, name); + this.sub = true; +} +util.inherits(Sub, Bus); +Sub.prototype.add = function (entry) { + return Bus.prototype.add.call(this, entry.toUpperCase()); +}; + +// Object.setPrototypeOf instead of util.inherits. +function Legacy() { + EventEmitter.call(this); +} +Object.setPrototypeOf(Legacy.prototype, EventEmitter.prototype); + +function main() { + const bus = Bus("main"); + console.log(bus instanceof Bus, bus instanceof EventEmitter, Bus.super_ === EventEmitter); + console.log(Object.keys(bus).join(","), bus._eventsCount, bus._maxListeners); + bus.on("added", function (entry, count) { + console.log("added", entry, count, this === bus); + }); + bus.once("added", (entry) => console.log("first only", entry)); + bus.prependListener("added", () => console.log("prepended")); + bus.prependOnceListener("added", () => console.log("prepended once")); + bus.add("a").add("b"); + console.log(bus.listenerCount("added"), bus.eventNames(), bus.getMaxListeners()); + + const handler = () => console.log("never"); + bus.on("x", handler); + bus.on("x", () => console.log("kept")); + bus.off("x", handler); + console.log(bus.emit("x"), bus.emit("nothing"), bus.listeners("x").length, bus.rawListeners("added").length); + + try { + bus.emit("error", new Error("boom")); + } catch (err) { + console.log("caught", err.message); + } + try { + bus.emit("error", "plain"); + } catch (err) { + console.log("caught", err.code, err.message); + } + bus.on("error", (err) => console.log("handled", err.message)); + bus.emit("error", new Error("quiet")); + + bus.on("newListener", (event, listener) => console.log("newListener", event, typeof listener)); + bus.on("removeListener", (event) => console.log("removeListener", event)); + bus.on("later", () => {}); + bus.removeAllListeners("added"); + console.log(bus.listenerCount("added"), bus.listenerCount("later")); + bus.removeAllListeners(); + console.log(bus.eventNames().length); + + const sub = new Sub("child"); + sub.on("added", (entry) => console.log("sub added", entry)); + sub.add("c"); + console.log(sub instanceof Sub, sub instanceof Bus, sub instanceof EventEmitter, sub.entries); + + const legacy = new Legacy(); + console.log("legacy", legacy instanceof EventEmitter, legacy.getMaxListeners()); + try { + legacy.setMaxListeners(-1); + } catch (err) { + console.log(err.name, err.code); + } + try { + legacy.on("bad", "not a function"); + } catch (err) { + console.log(err.name, err.code); + } + console.log(new Legacy()); + + // The leak warning goes to stderr with the process id: run it in a child + // and print its output with the id normalized. + const { spawnSync } = require("child_process"); + const child = spawnSync(process.execPath, [process.argv[1], "leak"], { encoding: "utf8" }); + console.log(child.stdout + child.stderr.replace(/\(node:\d+\)/g, "(node:)")); +} + +function leak() { + const legacy = new Legacy(); + legacy.setMaxListeners(1); + legacy.on("many", () => {}); + legacy.on("many", () => {}); + legacy.on("many", () => {}); + console.log("leak", legacy.listenerCount("many")); + setTimeout(() => console.log("leak done"), 1); +} + +if (process.argv[2] === "leak") leak(); +else main(); diff --git a/tests/corpus/6454-constructor-function-stream-bases.cjs b/tests/corpus/6454-constructor-function-stream-bases.cjs new file mode 100644 index 000000000..4b2bb6789 --- /dev/null +++ b/tests/corpus/6454-constructor-function-stream-bases.cjs @@ -0,0 +1,65 @@ +// The legacy Stream constructor as a constructor function's base: its +// EventEmitter state and the legacy pipe between two such streams. The +// stream classes take util.inherits, and instanceof follows the chain. +"use strict"; +const util = require("util"); +const Stream = require("stream"); +const { Readable, Writable, Duplex, Transform, PassThrough } = require("stream"); + +function Source(items) { + Stream.call(this); + this.items = items; + this.readable = true; +} +util.inherits(Source, Stream); +Source.prototype.start = function () { + for (const item of this.items) this.emit("data", item); + this.emit("end"); +}; + +function Sink() { + Stream.call(this); + this.writable = true; + this.chunks = []; +} +util.inherits(Sink, Stream); +Sink.prototype.write = function (chunk) { + this.chunks.push(chunk); + return true; +}; +Sink.prototype.end = function () { + console.log("sink end", this.chunks.join("+")); +}; + +const source = new Source(["a", "b", "c"]); +const sink = new Sink(); +sink.on("pipe", (from) => console.log("piped", from === source)); +console.log(source.pipe(sink) === sink, sink.listenerCount("pipe")); +source.start(); +console.log("after end", source.listenerCount("data"), sink.listenerCount("drain")); + +const kept = new Sink(); +const second = new Source(["x"]); +second.pipe(kept, { end: false }); +second.start(); +console.log("kept open", kept.chunks); + +function Reader() {} +util.inherits(Reader, Readable); +function Both() {} +util.inherits(Both, Duplex); +function Shift() {} +util.inherits(Shift, Transform); +function Through() {} +util.inherits(Through, PassThrough); +function Out() {} +util.inherits(Out, Writable); +console.log(Reader.super_ === Readable, Through.super_ === PassThrough, Out.super_ === Writable); +const reader = Object.create(Reader.prototype); +const through = Object.create(Through.prototype); +const out = Object.create(Out.prototype); +const both = Object.create(Both.prototype); +console.log(reader instanceof Readable, reader instanceof Writable); +console.log(through instanceof Transform, through instanceof Duplex, through instanceof Readable); +console.log(out instanceof Writable, out instanceof Readable, both instanceof Readable); +console.log(typeof Shift.prototype.constructor, Shift.prototype instanceof Duplex); diff --git a/tests/corpus/6463-node-readable-stream-producers.ts b/tests/corpus/6463-node-readable-stream-producers.ts new file mode 100644 index 000000000..4d06fac94 --- /dev/null +++ b/tests/corpus/6463-node-readable-stream-producers.ts @@ -0,0 +1,75 @@ +// NodeJS.ReadableStream is the interface every Node readable implements. +// One helper typed with it consumes a file stream, a Readable subclass, +// Readable.from, a gunzip Transform fed through pipe(), and a child's +// stdout. A program that mixes those producers compiles the interface as +// the node:stream Readable, with the child's pipe bridged into one. +import { spawn } from "node:child_process"; +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "node:path"; +import { Readable } from "node:stream"; +import * as zlib from "node:zlib"; + +function drain(label: string, src: NodeJS.ReadableStream): Promise { + return new Promise((resolve, reject) => { + let text = ""; + let chunks = 0; + src.on("data", (chunk: Buffer | string) => { + chunks++; + text += typeof chunk === "string" ? `[s]${chunk}` : chunk.toString("utf8"); + }); + src.on("error", (err) => reject(err)); + src.on("end", () => { + console.log(label, "end", chunks > 0, src.readable); + resolve(text); + }); + }); +} + +class Countdown extends Readable { + private left = 3; + _read(): void { + this.push(this.left > 0 ? `t-${this.left--};` : null); + } +} + +function openText(file: string): NodeJS.ReadableStream { + return fs.createReadStream(file); +} + +function openDecoded(file: string): NodeJS.ReadableStream { + const stream = fs.createReadStream(file); + stream.setEncoding("utf8"); + return stream; +} + +async function main(): Promise { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "scriptc-node-readable-")); + const plain = path.join(dir, "plain.txt"); + const packed = path.join(dir, "packed.gz"); + fs.writeFileSync(plain, "hello from a file\n"); + fs.writeFileSync(packed, zlib.gzipSync(Buffer.from("inflated text\n"))); + + console.log(JSON.stringify(await drain("file", openText(plain)))); + console.log(JSON.stringify(await drain("decoded", openDecoded(plain)))); + console.log(JSON.stringify(await drain("subclass", new Countdown()))); + console.log(JSON.stringify(await drain("from", Readable.from(["a", "b", "c"])))); + const gunzip = fs.createReadStream(packed).pipe(zlib.createGunzip()); + console.log(JSON.stringify(await drain("gunzip", gunzip))); + + const child = spawn("sh", ["-c", "printf 'child line\\n'"], { + stdio: ["ignore", "pipe", "inherit"], + }); + const out = child.stdout; + if (out !== null) console.log(JSON.stringify(await drain("child", out))); + console.log("same stdout", child.stdout === out); + + try { + await drain("missing", openText(path.join(dir, "missing.txt"))); + } catch (err) { + console.log("missing", (err as NodeJS.ErrnoException).code); + } + fs.rmSync(dir, { recursive: true }); +} + +void main(); diff --git a/tests/corpus/6464-node-writable-stream-producers.ts b/tests/corpus/6464-node-writable-stream-producers.ts new file mode 100644 index 000000000..bc9986b9b --- /dev/null +++ b/tests/corpus/6464-node-writable-stream-producers.ts @@ -0,0 +1,68 @@ +// NodeJS.WritableStream is the interface every Node writable implements: +// process.stdout/stderr, fs.WriteStream and the node:stream Writable +// family. Helpers typed with it accept all of them, a copy helper pipes a +// file stream into one, and readable.pipe(process.stdout) leaves the +// process stream open afterwards, as in Node. +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "node:path"; +import { Readable, Writable } from "node:stream"; + +function emit(dst: NodeJS.WritableStream, line: string): boolean { + return dst.write(`${line}\n`); +} + +function copy(src: NodeJS.ReadableStream, dst: NodeJS.WritableStream): Promise { + return new Promise((resolve, reject) => { + src.on("error", reject); + dst.on("error", reject); + dst.on("finish", () => resolve()); + src.pipe(dst); + }); +} + +class Collector extends Writable { + readonly parts: string[] = []; + _write(chunk: Buffer, _encoding: string, callback: (error?: Error | null) => void): void { + this.parts.push(chunk.toString("utf8")); + callback(); + } +} + +function finished(stream: NodeJS.WritableStream): Promise { + return new Promise((resolve) => { + stream.end(() => resolve()); + }); +} + +async function main(): Promise { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "scriptc-node-writable-")); + const target = path.join(dir, "out.txt"); + const file = fs.createWriteStream(target); + console.log("file write", emit(file, "to a file")); + console.log("stdout write", emit(process.stdout, "to stdout")); + emit(process.stderr, "to stderr"); + const collector = new Collector(); + emit(collector, "to a collector"); + await finished(file); + await finished(collector); + console.log(JSON.stringify(fs.readFileSync(target, "utf8"))); + console.log(JSON.stringify(collector.parts)); + + const source = path.join(dir, "source.txt"); + const copied = path.join(dir, "copied.txt"); + fs.writeFileSync(source, "copied through pipe\n"); + await copy(fs.createReadStream(source), fs.createWriteStream(copied)); + console.log(JSON.stringify(fs.readFileSync(copied, "utf8"))); + + await new Promise((resolve) => { + const piped = Readable.from(["piped ", "to ", "stdout\n"]); + piped.on("end", () => resolve()); + console.log("pipe answers stdout", piped.pipe(process.stdout) === process.stdout); + }); + // The pipe did not end process.stdout. + emit(process.stdout, "still writable"); + fs.rmSync(dir, { recursive: true }); +} + +void main(); diff --git a/tests/corpus/6465-node-stream-js-boundary/consume.js b/tests/corpus/6465-node-stream-js-boundary/consume.js new file mode 100644 index 000000000..ceb8f3abf --- /dev/null +++ b/tests/corpus/6465-node-stream-js-boundary/consume.js @@ -0,0 +1,12 @@ +export function streamToText(stream) { + return new Promise((resolve, reject) => { + const chunks = []; + stream.on("data", (chunk) => chunks.push(Buffer.from(chunk))); + stream.on("error", reject); + stream.on("end", () => resolve(Buffer.concat(chunks).toString("utf8"))); + }); +} + +export function describeStream(stream) { + console.log(typeof stream.on, typeof stream.pipe, stream.readable); +} diff --git a/tests/corpus/6465-node-stream-js-boundary/main.ts b/tests/corpus/6465-node-stream-js-boundary/main.ts new file mode 100644 index 000000000..0079c0733 --- /dev/null +++ b/tests/corpus/6465-node-stream-js-boundary/main.ts @@ -0,0 +1,33 @@ +// NodeJS.ReadableStream values crossing into untyped JavaScript and into +// 'unknown': a file stream and a child's stdout share one helper with a +// JavaScript stream consumer, which sees ordinary Node streams. +import { spawn } from "node:child_process"; +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "node:path"; +import { describeStream, streamToText } from "./consume.js"; + +function open(file: string): NodeJS.ReadableStream { + return fs.createReadStream(file); +} + +function textOf(stream: NodeJS.ReadableStream): Promise { + return streamToText(stream); +} + +async function main(): Promise { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "scriptc-node-stream-js-")); + const file = path.join(dir, "input.txt"); + fs.writeFileSync(file, "through javascript\n"); + const held: unknown = open(file); + console.log(typeof held); + describeStream(held); + console.log(JSON.stringify(await textOf(open(file)))); + const child = spawn("sh", ["-c", "printf 'child output\\n'"], { + stdio: ["ignore", "pipe", "inherit"], + }); + if (child.stdout !== null) console.log(JSON.stringify(await textOf(child.stdout))); + fs.rmSync(dir, { recursive: true }); +} + +void main(); diff --git a/tests/corpus/6465-optional-members-over-index-signature.ts b/tests/corpus/6465-optional-members-over-index-signature.ts new file mode 100644 index 000000000..df957c8d9 --- /dev/null +++ b/tests/corpus/6465-optional-members-over-index-signature.ts @@ -0,0 +1,73 @@ +// An index signature whose value admits undefined already describes an +// optional member of the same type (`interface Env extends Dict +// { TZ?: string }`, @types/node's ProcessEnv): such a record is one key +// set in insertion order, so spread, delete, `in` and every enumeration +// treat the declared member like any other key. +interface Env { + TZ?: string; + [key: string]: string | undefined; +} + +const base: Env = { A: "1", TZ: "UTC", B: "2" }; +const env = { ...base }; +env.C = "3"; +for (const key of ["A", "TZ", "MISSING"]) { + delete env[key]; +} +console.log(Object.entries(env), Object.keys(base)); +const probe = (key: string): boolean => key in env; +console.log(probe("TZ"), probe("B"), env.TZ, env.B); +env.TZ = "Europe/Berlin"; +console.log(JSON.stringify(env), env); + +const extended: Env = { ...base, D: "4" }; +delete extended.TZ; +extended.TZ = "Asia/Tokyo"; +console.log(extended, Object.keys(extended).join(",")); + +// A declared member written after another key comes after it. +interface Scores { + best?: number; + [name: string]: number | undefined; +} +const scores: Scores = {}; +scores["x"] = 1; +scores.best = 2; +console.log(Object.keys(scores), JSON.stringify(scores)); + +// The process environment has the same shape; a child environment built +// from it keeps the parent's keys in order. +process.env.SCX_FIRST = "first"; +process.env.SCX_DROP = "drop"; +process.env.SCX_LAST = "last"; +const DROP = ["SCX_DROP", "SCX_NOT_SET"]; +function childEnv(): string[] { + const runEnv = { ...process.env }; + runEnv.SCX_ADDED = "added"; + for (const key of DROP) { + delete runEnv[key]; + } + runEnv.SCX_FIRST = runEnv.SCX_FIRST ? `${runEnv.SCX_FIRST}!` : "none"; + return Object.entries(runEnv) + .filter(([key]) => key.startsWith("SCX_")) + .map(([key, value]) => `${key}=${value || ""}`); +} +console.log(childEnv()); + +// A record of declared optional members over a slot without undefined is +// the same key set: merging it over the process environment keeps both. +type Dictionary = { [key: string]: T }; +type BuildEnv = { CI?: string; DEBUG?: string } & Dictionary; +function merged(buildEnv: BuildEnv): string[] { + const env = { ...process.env, ...buildEnv }; + return [ + String(env.CI === "1" || env.SCX_FIRST === "first"), + `${env.DEBUG ?? "-"}`, + Object.keys(env) + .filter((key) => key.startsWith("SCX_") || key === "CI" || key === "DEBUG") + .join(","), + ]; +} +const buildEnv: BuildEnv = { SCX_BUILD: "b", CI: "1" }; +buildEnv.DEBUG = "yes"; +console.log(merged(buildEnv), Object.keys(buildEnv), buildEnv.CI, buildEnv["SCX_BUILD"]); diff --git a/tests/corpus/6466-never-assertion-argument.ts b/tests/corpus/6466-never-assertion-argument.ts new file mode 100644 index 000000000..8d2c1f2bf --- /dev/null +++ b/tests/corpus/6466-never-assertion-argument.ts @@ -0,0 +1,31 @@ +// `value as never` makes an unknown value fit any parameter: the argument +// is checked against the parameter's type like any unknown value the +// checker admits there, and a typed value passes unchanged. +type Out = { routes: { src: string; dest?: string }[]; count: number }; + +function show(tags: Record): void { + console.log(JSON.stringify(tags)); +} + +function report(tag: (result: Out) => Record, value: unknown): void { + show(tag(value as never)); +} + +const describe = (r: Out): Record => ({ count: r.count, first: r.routes[0]?.src }); +report(describe, JSON.parse('{"routes":[{"src":"/a"},{"src":"/b","dest":"/c"}],"count":2}')); + +// Through a promise of unknown, the way a wrapper tags an async result. +function later(value: Out): Promise { + return Promise.resolve(value); +} +later({ routes: [], count: 7 }).then((value) => { + show(describe(value as never)); +}); + +// A typed value keeps its identity through the assertion. +function bump(target: Out): void { + target.count++; +} +const typed: Out = { routes: [{ src: "/x" }], count: 1 }; +bump(typed as never); +console.log(typed.count); diff --git a/tests/corpus/6466-node-stdio-stream-surface.ts b/tests/corpus/6466-node-stdio-stream-surface.ts new file mode 100644 index 000000000..d3726360f --- /dev/null +++ b/tests/corpus/6466-node-stdio-stream-surface.ts @@ -0,0 +1,45 @@ +// The stdio handles reach the full stream surface when a program uses it: +// child.stdout piped to process.stdout, a file stream piped into a +// child's stdin, and an 'error' listener on a NodeJS.WritableStream +// parameter that only receives the process streams. Each of these +// compiles the NodeJS stream interfaces as the node:stream classes. +import { spawn } from "node:child_process"; +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "node:path"; + +function report(out: NodeJS.WritableStream, text: string): void { + out.on("error", (err) => console.log("error", err.message)); + out.write(`${text}\n`); +} + +function echo(text: string): Promise { + return new Promise((resolve) => { + const child = spawn("sh", ["-c", `echo ${text}`], { stdio: ["ignore", "pipe", "inherit"] }); + child.stdout?.pipe(process.stdout); + child.on("close", (code) => resolve(code)); + }); +} + +function cat(input: string): Promise { + return new Promise((resolve) => { + const child = spawn("cat", [], { stdio: ["pipe", "pipe", "inherit"] }); + child.stdout?.pipe(process.stdout); + child.on("close", (code) => resolve(code)); + if (child.stdin) fs.createReadStream(input).pipe(child.stdin); + }); +} + +async function main(): Promise { + report(process.stdout, "reported"); + report(process.stderr, "reported to stderr"); + console.log("echo exit", await echo("piped from a child")); + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "scriptc-node-stdio-")); + const input = path.join(dir, "input.txt"); + fs.writeFileSync(input, "file contents through cat\n"); + console.log("cat exit", await cat(input)); + report(process.stdout, "stdout stays open"); + fs.rmSync(dir, { recursive: true }); +} + +void main(); diff --git a/tests/corpus/6467-fromentries-record-assertion.ts b/tests/corpus/6467-fromentries-record-assertion.ts new file mode 100644 index 000000000..cf8f1c1ff --- /dev/null +++ b/tests/corpus/6467-fromentries-record-assertion.ts @@ -0,0 +1,43 @@ +// `Object.fromEntries(...) as T` for a declared record T keeps exactly +// the keys the entries created, in their order, as Node keeps the object +// itself, as long as the entries hold scalars or checked-dynamic values. +type Tags = { "error.type": string; "error.msg": string; "error.cause"?: string }; + +function scrub(input: string): string { + return input.replace(/secret/g, "***"); +} + +function redact(tags: Tags): Tags { + return Object.fromEntries( + Object.entries(tags).map(([key, value]) => [key, typeof value === "string" ? scrub(value) : value]), + ) as Tags; +} + +const e = new Error("a secret here"); +const plain = redact({ "error.type": e.name, "error.msg": e.message }); +console.log(plain, Object.keys(plain), "error.cause" in plain, plain["error.msg"].length); +const caused = redact({ "error.type": "TypeError", "error.msg": "m", "error.cause": "secret cause" }); +console.log(caused, JSON.stringify(caused)); + +// Keys the record does not declare stay with the object. +type Point = { x: number; y: number }; +const p = Object.fromEntries([ + ["y", 2], + ["x", 1], + ["z", 3], +]) as Point; +console.log(p, p.x + p.y, Object.keys(p), JSON.stringify(p)); + +// Entries held as unknown keep their identity: the object's members are +// the same array and record, and writes through either reach both. +type Holder = { list: number[]; point: { x: number } }; +const list = [1, 2]; +const point = { x: 1 }; +const entries: [string, unknown][] = [ + ["list", list], + ["point", point], +]; +const held = Object.fromEntries(entries) as Holder; +held.list.push(3); +held.point.x = 5; +console.log(list.length, point.x, held.list === list, held.point === point); diff --git a/tests/corpus/6467-node-stream-untyped-sources/main.ts b/tests/corpus/6467-node-stream-untyped-sources/main.ts new file mode 100644 index 000000000..5cb3a628d --- /dev/null +++ b/tests/corpus/6467-node-stream-untyped-sources/main.ts @@ -0,0 +1,58 @@ +// Streams that arrive untyped meet NodeJS stream interface slots: an +// untyped JavaScript function's stream returned as NodeJS.ReadableStream, +// a checked cast from 'unknown', pipe() and pipeline() into untyped +// destinations, and 'for await' over a NodeJS.ReadableStream +// parameter fed by a file stream and a child's stdout. +import { spawn } from "node:child_process"; +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "node:path"; +import { pipeline } from "node:stream/promises"; +import * as zlib from "node:zlib"; +import { collector, joined, openUntyped } from "./sources.js"; + +function open(file: string): NodeJS.ReadableStream { + return openUntyped(file); +} + +function held(file: string): unknown { + return fs.createReadStream(file); +} + +async function collect(label: string, src: NodeJS.ReadableStream): Promise { + let text = ""; + for await (const chunk of src) text += chunk.toString(); + console.log(label, JSON.stringify(text)); +} + +async function main(): Promise { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "scriptc-node-stream-untyped-")); + const plain = path.join(dir, "plain.txt"); + const packed = path.join(dir, "packed.gz"); + fs.writeFileSync(plain, "plain text\n"); + fs.writeFileSync(packed, zlib.gzipSync(Buffer.from("packed text\n"))); + + await collect("returned", open(plain)); + await collect("cast", held(plain) as NodeJS.ReadableStream); + const child = spawn("sh", ["-c", "printf 'child text\\n'"], { stdio: ["ignore", "pipe", "inherit"] }); + if (child.stdout) await collect("child", child.stdout); + + await new Promise((resolve) => { + const piped = fs.createReadStream(plain); + piped.on("end", () => resolve()); + piped.pipe(collector()); + }); + console.log("pipe", JSON.stringify(joined())); + + const sink = collector(); + await pipeline(openUntyped(packed), zlib.createGunzip(), sink); + console.log("pipeline", JSON.stringify(joined())); + try { + await pipeline(openUntyped(path.join(dir, "missing.gz")), zlib.createGunzip(), collector()); + } catch (err) { + console.log("pipeline error", (err as NodeJS.ErrnoException).code); + } + fs.rmSync(dir, { recursive: true }); +} + +void main(); diff --git a/tests/corpus/6467-node-stream-untyped-sources/sources.js b/tests/corpus/6467-node-stream-untyped-sources/sources.js new file mode 100644 index 000000000..ce4b5dcb7 --- /dev/null +++ b/tests/corpus/6467-node-stream-untyped-sources/sources.js @@ -0,0 +1,20 @@ +import * as fs from "node:fs"; +import { PassThrough } from "node:stream"; + +const parts = []; + +/** @returns {any} */ +export function openUntyped(file) { + return fs.createReadStream(file); +} + +/** @returns {any} */ +export function collector() { + const sink = new PassThrough(); + sink.on("data", (chunk) => parts.push(chunk.toString("utf8"))); + return sink; +} + +export function joined() { + return parts.splice(0).join(""); +} diff --git a/tests/corpus/6468-generic-unknown-record-entries.ts b/tests/corpus/6468-generic-unknown-record-entries.ts new file mode 100644 index 000000000..f0e57756a --- /dev/null +++ b/tests/corpus/6468-generic-unknown-record-entries.ts @@ -0,0 +1,33 @@ +// Inside a generic body, `Object.entries(tags)` over `T extends +// Record` reads `[string, T[keyof T]]` elements. An +// instance whose T is the unknown-valued record view reads every member +// as unknown; a record instance reads the union of its member types. +function upper(text: string): string { + return text.toUpperCase(); +} + +function scrub(text: string): string { + return text.replace(/secret/g, "***"); +} + +function describe>(tags: T): string[] { + return Object.entries(tags).map( + ([key, value]) => `${key}=${typeof value === "string" ? upper(value) : String(value)}`, + ); +} + +function redact>(tags: T): T { + return Object.fromEntries( + Object.entries(tags).map(([key, value]) => [key, typeof value === "string" ? scrub(value) : value]), + ) as T; +} + +function flatten(value: unknown): Record { + return typeof value === "object" && value !== null ? (value as Record) : { value }; +} + +console.log(describe(flatten({ b: 1, a: "x", c: true }))); +console.log(describe(flatten(5))); +console.log(describe({ name: "n", size: 3 })); +const cleaned = redact(flatten({ token: "secret", count: 2 })); +console.log(cleaned, Object.keys(cleaned)); diff --git a/tests/corpus/6468-node-stdio-writable-slot.ts b/tests/corpus/6468-node-stdio-writable-slot.ts new file mode 100644 index 000000000..a326344d1 --- /dev/null +++ b/tests/corpus/6468-node-stdio-writable-slot.ts @@ -0,0 +1,49 @@ +// process.stdout and process.stderr in NodeJS.WritableStream slots of a +// program that also passes other streams there: every slot holds the same +// stream object, a pipe through such a slot leaves the process stream +// open, and Writable's listener methods return the stream for chaining. +import { Readable, Writable } from "node:stream"; + +class Sink extends Writable { + _write(_chunk: Buffer, _encoding: string, callback: (error?: Error | null) => void): void { + callback(); + } +} + +function same(a: NodeJS.WritableStream, b: NodeJS.WritableStream): boolean { + return a === b; +} + +function pipeInto(dst: NodeJS.WritableStream, parts: string[]): Promise { + return new Promise((resolve) => { + const src = Readable.from(parts); + src.on("end", () => resolve()); + src.pipe(dst); + }); +} + +function write(dst: NodeJS.WritableStream, text: string): boolean { + return dst.write(text); +} + +async function main(): Promise { + console.log("stdout same", same(process.stdout, process.stdout)); + console.log("stderr same", same(process.stderr, process.stderr)); + console.log("stdout and stderr", same(process.stdout, process.stderr)); + console.log("sink and stdout", same(new Sink(), process.stdout)); + + await pipeInto(process.stdout, ["piped ", "through ", "a slot\n"]); + // The pipe did not end process.stdout: writes through a slot still work. + console.log("write after pipe", write(process.stdout, "still open\n")); + await pipeInto(process.stderr, ["piped to stderr\n"]); + write(process.stderr, "stderr still open\n"); + + // Writable's listener methods return the stream. + const sink = new Sink(); + await new Promise((resolve) => { + sink.on("finish", () => resolve()).end("done"); + }); + console.log("sink finished", sink.writableFinished); +} + +void main(); diff --git a/tests/corpus/6469-node-stdio-writable-js-pipe/main.ts b/tests/corpus/6469-node-stdio-writable-js-pipe/main.ts new file mode 100644 index 000000000..258755f83 --- /dev/null +++ b/tests/corpus/6469-node-stdio-writable-js-pipe/main.ts @@ -0,0 +1,36 @@ +// process.stdout and process.stderr reach an untyped JavaScript helper +// through NodeJS.WritableStream slots, and the helper pipes into them. As +// in Node, the pipe leaves the process stream open, so later writes +// through the same slots still work. +import { Readable, Writable } from "node:stream"; +import { pipeAll } from "./pipe.js"; + +class Sink extends Writable { + readonly parts: string[] = []; + _write(chunk: Buffer, _encoding: string, callback: (error?: Error | null) => void): void { + this.parts.push(chunk.toString("utf8")); + callback(); + } +} + +function into(destination: NodeJS.WritableStream, parts: string[]): Promise { + return new Promise((resolve) => { + pipeAll(Readable.from(parts), destination, () => resolve()); + }); +} + +function write(destination: NodeJS.WritableStream, text: string): boolean { + return destination.write(text); +} + +async function main(): Promise { + const sink = new Sink(); + await into(sink, ["to ", "a sink"]); + console.log("sink", JSON.stringify(sink.parts), sink.writableEnded); + await into(process.stdout, ["piped ", "by javascript\n"]); + console.log("write after pipe", write(process.stdout, "still open\n")); + await into(process.stderr, ["piped to stderr\n"]); + write(process.stderr, "stderr still open\n"); +} + +void main(); diff --git a/tests/corpus/6469-node-stdio-writable-js-pipe/pipe.js b/tests/corpus/6469-node-stdio-writable-js-pipe/pipe.js new file mode 100644 index 000000000..d48d592ad --- /dev/null +++ b/tests/corpus/6469-node-stdio-writable-js-pipe/pipe.js @@ -0,0 +1,4 @@ +export function pipeAll(source, destination, done) { + source.on("end", () => done()); + source.pipe(destination); +} diff --git a/tests/corpus/6470-stream-transform-any-data-callback.ts b/tests/corpus/6470-stream-transform-any-data-callback.ts new file mode 100644 index 000000000..cc4935a7e --- /dev/null +++ b/tests/corpus/6470-stream-transform-any-data-callback.ts @@ -0,0 +1,60 @@ +// Transform option callbacks whose completion callback declares an +// any-typed `data` (the @types/node TransformCallback spelling): the data +// passed back may be a Buffer, a string, or omitted. +import { PassThrough, Transform } from "node:stream"; +import { createGunzip, gzipSync } from "node:zlib"; + +type Done = (error?: Error | null, data?: any) => void; + +interface Progress { + bytes: number; + chunks: number; +} + +function counting(progress: Progress): Transform { + return new Transform({ + transform(chunk: Buffer, _encoding: string, callback: Done) { + progress.bytes += chunk.length; + progress.chunks++; + callback(null, chunk); + }, + }); +} + +const upper = new Transform({ + transform(chunk: Buffer, _encoding: string, callback: Done) { + const text = chunk.toString(); + if (text === "skip") { + callback(); + return; + } + callback(null, text.toUpperCase()); + }, + flush(callback: Done) { + callback(null, Buffer.from("!")); + }, +}); +upper.on("data", (d: Buffer) => console.log("upper", JSON.stringify(d.toString()))); +upper.on("end", () => console.log("upper end")); +upper.write("hello"); +upper.write("skip"); +upper.end("world"); + +const failing = new Transform({ + transform(chunk: Buffer, _encoding: string, callback: Done) { + callback(new Error(`refused ${chunk.length} bytes`)); + }, +}); +failing.on("error", (e: Error) => console.log("error", e.message)); +failing.write("abc"); + +const progress: Progress = { bytes: 0, chunks: 0 }; +const counter = counting(progress); +const out = new PassThrough(); +counter.pipe(createGunzip()).pipe(out); +const parts: string[] = []; +out.on("data", (d: Buffer) => parts.push(d.toString())); +out.on("end", () => { + console.log("gunzip", JSON.stringify(parts.join("")), progress.bytes > 0, progress.chunks); +}); +counter.end(gzipSync(Buffer.from("compressed payload"))); diff --git a/tests/corpus/6471-stream-data-buffer-or-string.ts b/tests/corpus/6471-stream-data-buffer-or-string.ts new file mode 100644 index 000000000..69289ced5 --- /dev/null +++ b/tests/corpus/6471-stream-data-buffer-or-string.ts @@ -0,0 +1,44 @@ +// 'data' listeners on runtime streams may declare the chunk as +// `Buffer | string` (the @types/node ReadStream listener type): the +// listener receives a Buffer, or a string once an encoding applies. +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "node:path"; +import { PassThrough, Readable } from "node:stream"; + +function describe(label: string, chunk: Buffer | string): void { + if (typeof chunk === "string") console.log(label, "string", JSON.stringify(chunk)); + else console.log(label, "buffer", chunk.length, JSON.stringify(chunk.toString())); +} + +const raw = new PassThrough(); +raw.on("data", (chunk: Buffer | string) => describe("raw", chunk)); +raw.write("abc"); +raw.end(Buffer.from("de")); + +const text = new PassThrough(); +text.setEncoding("utf8"); +text.on("data", (chunk: Buffer | string) => describe("text", chunk)); +text.end("héllo"); + +const r = new Readable({ read() {} }); +r.on("data", (chunk: Buffer | string) => describe("readable", chunk)); +r.on("end", () => console.log("readable end")); +r.push("one"); +r.push(null); + +const file = path.join(os.tmpdir(), `scriptc-6471-${process.pid}.txt`); +fs.writeFileSync(file, "file contents\n"); +const stream = fs.createReadStream(file); +const release = (): void => console.log("released"); +stream.on("data", (chunk: Buffer | string) => describe("file", chunk)); +stream.on("close", release); +stream.on("error", release); +stream.on("end", () => { + const encoded = fs.createReadStream(file, { encoding: "utf8" }); + encoded.on("data", (chunk: Buffer | string) => describe("file utf8", chunk)); + encoded.on("close", () => { + fs.unlinkSync(file); + console.log("done"); + }); +}); diff --git a/tests/corpus/6472-child-data-unknown-chunk.ts b/tests/corpus/6472-child-data-unknown-chunk.ts new file mode 100644 index 000000000..8308aa8cf --- /dev/null +++ b/tests/corpus/6472-child-data-unknown-chunk.ts @@ -0,0 +1,87 @@ +// A child stream 'data' listener whose chunk is unknown or any receives a +// Buffer, or a string when setEncoding ran before the listener registered. +import { spawn } from "node:child_process"; + +function report(code: number | null, kinds: string[], stdout: string, stderr: string): void { + console.log("code", code, "kinds", [...new Set(kinds)].join(",")); + console.log("stdout", JSON.stringify(stdout)); + console.log("stderr", JSON.stringify(stderr)); +} + +function runEncoded(script: string): Promise { + return new Promise((resolve) => { + const child = spawn("/bin/sh", ["-c", script], { stdio: "pipe" }); + let stdout = ""; + let stderr = ""; + const kinds: string[] = []; + if (child.stdout) { + child.stdout.setEncoding("utf8"); + child.stdout.on("data", (c: any) => { + kinds.push(typeof c); + stdout += c; + }); + } + const err = child.stderr; + if (err !== null) { + err.setEncoding("utf8"); + err.on("data", (c: unknown) => { + kinds.push(typeof c); + stderr += String(c); + }); + } + child.on("close", (code) => { + report(code, kinds, stdout, stderr); + resolve(); + }); + }); +} + +function runRaw(script: string): Promise { + return new Promise((resolve) => { + const child = spawn("/bin/sh", ["-c", script], { stdio: "pipe" }); + let stdout = ""; + let stderr = ""; + const kinds: string[] = []; + child.stdout?.on("data", (c: unknown) => { + kinds.push(Buffer.isBuffer(c) ? "buffer" : typeof c); + stdout += String(c); + }); + if (child.stderr) { + child.stderr.on("data", (c: any) => { + const chunk = String(c); + kinds.push(Buffer.isBuffer(c) ? "buffer" : typeof c); + stderr += chunk; + }); + } + child.on("close", (code) => { + report(code, kinds, stdout, stderr); + resolve(); + }); + }); +} + +// setEncoding through one const alias of the stream and the listener +// through another: both name child.stdout, so chunks are strings. +function runAliased(script: string): Promise { + return new Promise((resolve) => { + const child = spawn("/bin/sh", ["-c", script], { stdio: "pipe" }); + const encoded = child.stdout!; + const listened = child.stdout!; + let stdout = ""; + const kinds: string[] = []; + encoded.setEncoding("utf8"); + listened.on("data", (c: unknown) => { + kinds.push(typeof c); + stdout += String(c); + }); + child.on("close", (code) => { + report(code, kinds, stdout, ""); + resolve(); + }); + }); +} + +runEncoded("printf 'h\\303\\251llo'; echo warn >&2") + .then(() => runRaw("printf raw; echo oops >&2; exit 2")) + .then(() => runAliased("printf alias")) + .then(() => console.log("done")); diff --git a/tests/corpus/6490-literal-key-field-updates.ts b/tests/corpus/6490-literal-key-field-updates.ts new file mode 100644 index 000000000..f90739c31 --- /dev/null +++ b/tests/corpus/6490-literal-key-field-updates.ts @@ -0,0 +1,132 @@ +// Read-modify-write through a key whose type is a union of literal field +// names (`totals[side] += n`): every compound operator, ++/-- and logical +// assignment dispatch over the declared fields the key can name. The +// receiver and key evaluate once, before the right side. +type Side = "sent" | "received"; +interface Traffic { + sent: number; + received: number; + label: string; + updatedAt: number; +} + +const traffic: Traffic = { sent: 0, received: 0, label: "link", updatedAt: 0 }; + +function record(side: Side, bytes: number): void { + traffic[side] += bytes; + traffic.updatedAt++; +} +record("sent", 120); +record("received", 64); +record("sent", 8); +console.log(JSON.stringify(traffic)); + +function adjust(side: Side): string { + const parts: number[] = []; + parts.push((traffic[side] -= 3)); + parts.push((traffic[side] *= 2)); + parts.push((traffic[side] /= 4)); + parts.push((traffic[side] %= 7)); + parts.push((traffic[side] **= 2)); + parts.push((traffic[side] |= 1)); + parts.push((traffic[side] &= 0xff)); + parts.push((traffic[side] ^= 0x0f)); + parts.push((traffic[side] <<= 2)); + parts.push((traffic[side] >>= 1)); + parts.push((traffic[side] >>>= 1)); + return parts.join(","); +} +console.log(adjust("sent"), adjust("received")); + +// ++ and -- in statement and value positions, prefix and postfix. +const sides: Side[] = ["sent", "received", "sent"]; +for (const s of sides) traffic[s]++; +for (const s of sides) --traffic[s]; +const post = traffic[sides[1]!]--; +const pre = ++traffic[sides[0]!]; +console.log(post, pre, traffic.sent, traffic.received); + +// One literal key, spelled at the site or carried by a const. +traffic["sent"] += 1000; +const fixed = "received"; +traffic[fixed] -= 1; +traffic["received"]++; +console.log(traffic.sent, traffic.received); + +// Evaluation order: receiver, key, old value, right side, write. +let trace = ""; +function pick(s: Side): Side { + trace += `key:${s};`; + return s; +} +function target(): Traffic { + trace += "receiver;"; + return traffic; +} +function amount(n: number): number { + trace += `rhs:${n};`; + return n; +} +console.log((target()[pick("received")] += amount(5)), trace); +trace = ""; +console.log(target()[pick("sent")]++, trace); +trace = ""; +let current = traffic; +function swap(): number { + current = { sent: -1, received: -1, label: "other", updatedAt: 0 }; + return 1; +} +current[pick("sent")] += swap(); +console.log(traffic.sent, current.sent, trace); + +// The right side throws after the old value is read: nothing is written. +function fail(): number { + throw new Error("rhs failed"); +} +try { + traffic[pick("received")] *= fail(); +} catch (e) { + if (e instanceof Error) console.log(e.message, traffic.received); +} + +// String fields append. +type Part = "head" | "tail"; +const text: { head: string; tail: string; size: number } = { head: "a", tail: "z", size: 0 }; +function append(p: Part, s: string): string { + return (text[p] += s); +} +console.log(append("head", "bc"), append("tail", "yx"), append("head", "!")); +console.log(JSON.stringify(text)); + +// Optional fields with ??=: only a missing value takes the right side. +type Phase = "dns" | "connect" | "tls"; +const timings: { dns?: number; connect?: number; tls?: number; total: number } = { total: 0 }; +function mark(phase: Phase, at: number): number { + return (timings[phase] ??= at); +} +console.log(mark("dns", 4), mark("connect", 9), mark("dns", 12), mark("tls", 0), mark("tls", 7)); +console.log(timings.dns, timings.connect, timings.tls, timings.total); + +// ||= and &&= test truthiness and write only in the selected branch. +const flags = { quiet: false, verbose: true, color: false }; +type Flag = keyof typeof flags; +function settle(f: Flag, value: boolean): string { + const a = (flags[f] ||= value); + const b = (flags[f] &&= f !== "color"); + return `${f}:${a}:${b}`; +} +console.log(settle("quiet", true), settle("verbose", false), settle("color", true)); +console.log(JSON.stringify(flags)); + +// Nested receivers and keys from a typed lookup. +const stats = { day: { hits: 1, misses: 2 }, week: { hits: 10, misses: 20 } }; +type Window = keyof typeof stats; +type Outcome = "hits" | "misses"; +const events: [Window, Outcome][] = [ + ["day", "hits"], + ["week", "misses"], + ["day", "misses"], + ["day", "hits"], +]; +for (const [w, o] of events) stats[w][o] += 1; +console.log(JSON.stringify(stats)); diff --git a/tests/corpus/6491-index-record-updates.ts b/tests/corpus/6491-index-record-updates.ts new file mode 100644 index 000000000..108d34012 --- /dev/null +++ b/tests/corpus/6491-index-record-updates.ts @@ -0,0 +1,44 @@ +// Compound assignment and ++/-- through a string key of an +// index-signature record: a missing entry reads as undefined, which every +// numeric operator turns into NaN, and the write inserts or replaces it. +const counts: Record = {}; +const words = "the cat and the hat and the bat".split(" "); +for (const w of words) { + counts[w] ??= 0; + counts[w]++; +} +console.log(JSON.stringify(counts)); + +const sums: { [key: string]: number } = { a: 1, b: 2 }; +sums["a"] += 10; +sums.b *= 3; +const key = "a"; +sums[key] -= 1; +console.log(JSON.stringify(sums)); + +// A missing entry: undefined op number is NaN, and the value form yields it. +console.log((sums["missing"] += 1), sums["missing"], Number.isNaN(sums["missing"])); +const later = sums["gone"]++; +console.log(later, sums["gone"], Object.keys(sums).join(",")); + +// Totals keyed by a runtime string, with the key and right side evaluated once. +let trace = ""; +function name(n: string): string { + trace += `key:${n};`; + return n; +} +function size(n: number): number { + trace += `rhs:${n};`; + return n; +} +const totals: Record = { disk: 0, net: 0 }; +const samples: [string, number][] = [ + ["disk", 4], + ["net", 7], + ["disk", 2], +]; +for (const [k, v] of samples) totals[name(k)] += size(v); +console.log(JSON.stringify(totals), trace); +const before = totals["net"]--; +const after = --totals["net"]; +console.log(before, after, totals["net"]); diff --git a/tests/corpus/webassembly-externref-lifetimes.mjs b/tests/corpus/webassembly-externref-lifetimes.mjs new file mode 100644 index 000000000..6d61d9c93 --- /dev/null +++ b/tests/corpus/webassembly-externref-lifetimes.mjs @@ -0,0 +1,44 @@ +// Externref values the guest holds stay alive until it is done with them: +// an import's result returned through an export, and a table's or global's +// reference read into a local before the guest or JavaScript replaces it. +// Each object is created fresh, so the runtime holds the only reference. +// (The module's text is in tests/wasm-spec/corpus/externref-lifetimes.wat.) +const bytes = Buffer.from("AGFzbQEAAAABCAJgAAFvYAAAAhgCA2VudgRtYWtlAAADZW52BWNsZWFyAAEDBQQAAAAABAQBbwABBgYBbwHQbwsHQQYEc2xvdAMABWNlbGxzAQAEcGFzcwACCnRha2VHbG9iYWwAAwl0YWtlVGFibGUABA50YWtlQWNyb3NzQ2FsbAAFCjUEBAAQAAsOAQFvIwAhANBvJAAgAAsSAQFvQQAlACEAQQDQbyYAIAALDAEBbyMAIQAQASAACw==", "base64"); + +let made = 0; +let exports; +const env = { + make: () => ({ id: ++made, label: `made ${made}`, items: [made, made * 2] }), + clear: () => { + exports.slot.value = null; + }, +}; +exports = new WebAssembly.Instance(new WebAssembly.Module(bytes), { env }).exports; + +const describe = (v) => (v === null ? "null" : `${v.id} ${v.label} ${v.items.join("+")}`); +const fresh = (n) => ({ id: n, label: `fresh ${n}`, items: [n, n + 1] }); + +const passed = []; +for (let i = 0; i < 200; i++) passed.push(describe(exports.pass())); +console.log("pass", passed.length, passed[0], passed[199]); + +const fromGlobal = []; +for (let i = 0; i < 200; i++) { + exports.slot.value = fresh(i); + fromGlobal.push(describe(exports.takeGlobal())); +} +console.log("global", fromGlobal[0], fromGlobal[199], describe(exports.slot.value)); + +const fromTable = []; +for (let i = 0; i < 200; i++) { + exports.cells.set(0, fresh(1000 + i)); + fromTable.push(describe(exports.takeTable())); +} +console.log("table", fromTable[0], fromTable[199], describe(exports.cells.get(0))); + +const acrossCall = []; +for (let i = 0; i < 200; i++) { + exports.slot.value = fresh(2000 + i); + acrossCall.push(describe(exports.takeAcrossCall())); +} +console.log("across call", acrossCall[0], acrossCall[199], describe(exports.slot.value)); diff --git a/tests/diagnostics/arrow-bind.ts b/tests/diagnostics/arrow-bind.ts index c78db514b..0b677c5ef 100644 --- a/tests/diagnostics/arrow-bind.ts +++ b/tests/diagnostics/arrow-bind.ts @@ -1,4 +1,5 @@ -// Typed preset and rest signatures still exceed the callable adapter. +// Typed rest signatures still exceed the callable adapter. (A typed preset +// binds to a fixed signature and compiles.) const preset = ((value: number) => value).bind(null, 1); const rest = ((...values: number[]) => values.length).bind(null); export {}; diff --git a/tests/diagnostics/child-process.ts b/tests/diagnostics/child-process.ts index f96bce418..02b4349e8 100644 --- a/tests/diagnostics/child-process.ts +++ b/tests/diagnostics/child-process.ts @@ -42,3 +42,13 @@ const staticWorker = new URL("./child-process.ts", import.meta.url); fork(staticWorker, [], { serialization: "advanced" }); fork(staticWorker, [], { stdio: ["ignore", "ignore", "ignore", "ignore"] }); // Keep each fence on its own statement so diagnostics remain site-specific. +// An unknown 'data' chunk takes the setEncoding state established before +// the listener registers; a setEncoding placed elsewhere leaves it unclear. +const pipedOut = piped.stdout!; +pipedOut.on("data", (chunk: unknown) => console.log(String(chunk))); +pipedOut.setEncoding("utf8"); +// A setEncoding through a copy that can hold the same stream (a `let` +// binding here) leaves the state unclear as well. +let pipedErr = piped.stderr!; +pipedErr.setEncoding("utf8"); +piped.stderr!.on("data", (chunk: unknown) => console.log(String(chunk))); diff --git a/tests/diagnostics/comptime.ts b/tests/diagnostics/comptime.ts index 9ba882aaf..645779ea7 100644 --- a/tests/diagnostics/comptime.ts +++ b/tests/diagnostics/comptime.ts @@ -20,3 +20,14 @@ const nested = comptime(() => comptime(() => 1)); const eventually = comptime(async () => 1); const lied = comptime(() => 1 as unknown as string); + +// A record type reachable from its own fields has no literal form. +interface Chain { + value: number; + next: Chain | null; +} +const chain: Chain = comptime(() => ({ value: 1, next: null }) as Chain); + +// A union reachable from itself through an array has no literal form either. +type Tree = number | Tree[]; +const tree: Tree = comptime((): Tree => [1, [2, [3]]]); diff --git a/tests/diagnostics/fromentries-aliased-entries.ts b/tests/diagnostics/fromentries-aliased-entries.ts new file mode 100644 index 000000000..ee5cc5338 --- /dev/null +++ b/tests/diagnostics/fromentries-aliased-entries.ts @@ -0,0 +1,13 @@ +// `Object.fromEntries(...) as T` keeps the object only when the entries' +// values are scalars or checked-dynamic values. Native arrays and records +// in the entries would be copied, severing their identity, so the +// assertion keeps the exact-shape fence (corpus 6467 covers the cases that +// lower). +type Scores = { a: number[]; b: number[] }; +const shared = [10]; +const scores = Object.fromEntries([ + ["a", shared], + ["b", shared], +]) as Scores; +scores.a.push(11); +console.log(shared.length, scores.b.length); diff --git a/tests/diagnostics/json-shape/main.ts b/tests/diagnostics/json-shape/main.ts index 1d9552506..a46338d52 100644 --- a/tests/diagnostics/json-shape/main.ts +++ b/tests/diagnostics/json-shape/main.ts @@ -1,7 +1,7 @@ -/* A JSON document whose shape doesn't bake: a null-valued field has no - * standalone STATIC type, so the import reports the dynamic-family type - * diagnostic (SC2011 at the import site — the shape maps in the embedded - * engine) and uses of the binding poison. */ +/* A JSON document whose value doesn't bake: `1e400` parses to Infinity, + * which no number literal spells, so the import reports the bake failure + * at the import site and uses of the binding poison. (Its null-valued + * field bakes as null.) */ import bad from "./nulled.json" with { type: "json" }; console.log(bad.name); diff --git a/tests/diagnostics/json-shape/nulled.json b/tests/diagnostics/json-shape/nulled.json index fa4738449..5e506a6af 100644 --- a/tests/diagnostics/json-shape/nulled.json +++ b/tests/diagnostics/json-shape/nulled.json @@ -1 +1 @@ -{ "name": "x", "deprecated": null } +{ "name": "x", "deprecated": null, "limit": 1e400 } diff --git a/tests/diagnostics/variadic-wrapper-fences.ts b/tests/diagnostics/variadic-wrapper-fences.ts new file mode 100644 index 000000000..d2ad65646 --- /dev/null +++ b/tests/diagnostics/variadic-wrapper-fences.ts @@ -0,0 +1,28 @@ +// Variadic generic wrappers specialize per call site; the forms that have +// no fixed signature or no static name keep precise refusals. +type AnyFn = (...args: any) => any; + +// `arguments` with no fixed signature to lower against. +const loose = function () { + return arguments[0]; +}; + +// `fn.name` where the argument's name is only known at run time. +function named(fn: F): string { + return fn.name; +} +function one(): number { + return 1; +} +function two(): number { + return 2; +} +const pick = (flag: boolean) => (flag ? one : two); +console.log(named(pick(true))); + +// Forwarding a fixed tuple to a variadic function value. +function forward(sink: (...xs: number[]) => number, ...args: A): number { + return sink.apply(null, args); +} +console.log(forward((...xs: number[]) => xs.length, 1, 2)); +export { loose }; diff --git a/tests/fixtures/node-types/README.md b/tests/fixtures/node-types/README.md index 0784c57fe..243ce0b81 100644 --- a/tests/fixtures/node-types/README.md +++ b/tests/fixtures/node-types/README.md @@ -21,6 +21,7 @@ What the fixture pins (see tests/harness/project-config.test.ts): (process.uptime, Buffer.from, setInterval) reports the SC2020-family fence naming @types/node, instead of typechecking its way into a broken binary — and never a raw "Cannot find name" error. +- `event-loop-metrics.ts` and `timeout-type.ts` — performance.eventLoopUtilization, monitorEventLoopDelay's IntervalHistogram and NodeJS.Timeout as declared types lower under @types/node exactly as under the fallback declarations. - `source-import/` — an imported TypeScript source may use the `RequestInfo` global supplied by TypeScript's default DOM lib while scriptc adopts the project's @types/node declarations. Projects WITHOUT @types/node (every other fixture and the whole corpus) keep diff --git a/tests/fixtures/node-types/child-stdin-fences.ts b/tests/fixtures/node-types/child-stdin-fences.ts deleted file mode 100644 index 0859d8513..000000000 --- a/tests/fixtures/node-types/child-stdin-fences.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { spawn } from "node:child_process"; - -const child = spawn("node", []); -child.stdin.write("x", "utf8"); -child.stdin.end("x"); -child.stdin.destroy(new Error("stop")); diff --git a/tests/fixtures/node-types/child-stdin-streams.ts b/tests/fixtures/node-types/child-stdin-streams.ts new file mode 100644 index 000000000..c433d7bfb --- /dev/null +++ b/tests/fixtures/node-types/child-stdin-streams.ts @@ -0,0 +1,40 @@ +// child.stdin forms beyond write(string): a write with an encoding, end +// with a final chunk, and destroy with an error observed by an 'error' +// listener. They compile through the general stream representation, where +// child.stdin is the child's bridged Writable. +import { spawn } from "node:child_process"; + +function feed(): Promise { + return new Promise((resolve) => { + const child = spawn("cat", [], { stdio: ["pipe", "pipe", "inherit"] }); + let out = ""; + child.stdout.on("data", (chunk: Buffer) => { + out += chunk.toString(); + }); + child.on("close", (code) => { + console.log("fed", JSON.stringify(out), code); + resolve(); + }); + child.stdin.write("first ", "utf8"); + child.stdin.end("second\n"); + }); +} + +function abort(): Promise { + return new Promise((resolve) => { + const child = spawn("cat", [], { stdio: ["pipe", "ignore", "inherit"] }); + child.stdin.on("error", (err) => console.log("stdin error", err.message)); + child.on("close", (code) => { + console.log("aborted", code); + resolve(); + }); + child.stdin.destroy(new Error("stop")); + }); +} + +async function main(): Promise { + await feed(); + await abort(); +} + +void main(); diff --git a/tests/fixtures/node-types/event-loop-metrics.ts b/tests/fixtures/node-types/event-loop-metrics.ts new file mode 100644 index 000000000..f5c97c41b --- /dev/null +++ b/tests/fixtures/node-types/event-loop-metrics.ts @@ -0,0 +1,49 @@ +// Event-loop monitoring under @types/node: monitorEventLoopDelay's histogram, +// eventLoopUtilization deltas, and an unref'd interval typed NodeJS.Timeout. +import { monitorEventLoopDelay, performance } from "node:perf_hooks"; + +const histogram = monitorEventLoopDelay({ resolution: 10 }); +console.log("enable", histogram.enable(), histogram.enable()); +const first = performance.eventLoopUtilization(); +console.log("before loop", first.idle, first.active, first.utilization); + +let last = performance.eventLoopUtilization(); +let ticks = 0; +// The unref'd interval keeps nothing alive; this timer holds the loop open +// until the third sample. +const keepAlive = setTimeout(() => console.log("never"), 10_000); +const interval: NodeJS.Timeout = setInterval(() => { + const now = performance.eventLoopUtilization(); + const delta = performance.eventLoopUtilization(now, last); + last = now; + ticks++; + const p50 = histogram.percentile(50); + const p99 = histogram.percentile(99); + console.log( + "tick", + ticks, + delta.utilization >= 0 && delta.utilization <= 1, + delta.idle >= 0, + p50 > 0, + p99 >= p50, + histogram.max >= p99, + histogram.min <= p50, + histogram.count > 0, + ); + histogram.reset(); + console.log("after reset", histogram.count, histogram.max, histogram.min, Number.isNaN(histogram.mean)); + if (ticks === 3) { + clearInterval(interval); + clearTimeout(keepAlive); + console.log("disable", histogram.disable(), histogram.disable()); + const since = performance.eventLoopUtilization(first); + console.log("since", since.utilization > 0 && since.utilization <= 1, since.idle > 0); + try { + histogram.percentile(0); + } catch (e) { + console.log((e as Error).message, (e as { code?: string }).code); + } + } +}, 50); +interval.unref(); +console.log("hasRef", interval.hasRef()); diff --git a/tests/fixtures/node-types/node-stream-interfaces.ts b/tests/fixtures/node-types/node-stream-interfaces.ts new file mode 100644 index 000000000..2809dc315 --- /dev/null +++ b/tests/fixtures/node-types/node-stream-interfaces.ts @@ -0,0 +1,61 @@ +// @types/node's stream interfaces fed by every producer: NodeJS.ReadableStream +// and stream.Readable slots take file streams, a gunzip Transform and a +// child's stdout; NodeJS.WritableStream and stream.Writable slots take file +// streams and process.stdout. +import { spawn } from "node:child_process"; +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "node:path"; +import { Readable, Writable } from "node:stream"; +import * as zlib from "node:zlib"; + +function drain(label: string, src: NodeJS.ReadableStream): Promise { + return new Promise((resolve, reject) => { + let text = ""; + src.on("data", (chunk: Buffer | string) => { + text += chunk.toString(); + }); + src.on("error", reject); + src.on("end", () => { + console.log(label, "end"); + resolve(text); + }); + }); +} + +function emit(dst: NodeJS.WritableStream, line: string): void { + dst.write(`${line}\n`); +} + +function finish(dst: Writable): Promise { + return new Promise((resolve) => dst.end(() => resolve())); +} + +async function main(): Promise { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "scriptc-node-types-streams-")); + const plain = path.join(dir, "plain.txt"); + const packed = path.join(dir, "packed.gz"); + fs.writeFileSync(plain, "from a file\n"); + fs.writeFileSync(packed, zlib.gzipSync(Buffer.from("from gunzip\n"))); + const file: Readable = fs.createReadStream(plain); + console.log(JSON.stringify(await drain("file", file))); + console.log(JSON.stringify(await drain("gunzip", fs.createReadStream(packed).pipe(zlib.createGunzip())))); + const child = spawn("sh", ["-c", "printf 'from a child\\n'"], { stdio: ["ignore", "pipe", "inherit"] }); + if (child.stdout) console.log(JSON.stringify(await drain("child", child.stdout))); + + const target = path.join(dir, "out.txt"); + const out: Writable = fs.createWriteStream(target); + emit(out, "to a file"); + emit(process.stdout, "to stdout"); + await finish(out); + console.log(JSON.stringify(fs.readFileSync(target, "utf8"))); + await new Promise((resolve) => { + const piped = fs.createReadStream(plain); + piped.on("end", () => resolve()); + piped.pipe(process.stdout); + }); + emit(process.stdout, "stdout still open"); + fs.rmSync(dir, { recursive: true }); +} + +void main(); diff --git a/tests/fixtures/node-types/stream-construction.ts b/tests/fixtures/node-types/stream-construction.ts new file mode 100644 index 000000000..a28643f7c --- /dev/null +++ b/tests/fixtures/node-types/stream-construction.ts @@ -0,0 +1,121 @@ +// Streams constructed under @types/node: option-bag Transform and +// PassThrough streams with @types/node's TransformCallback, a gunzip stage, +// fs.ReadStream listeners, and child-process listeners declared with +// unknown chunks and non-null exit types. +import { spawn } from "node:child_process"; +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "node:path"; +import { PassThrough, Transform } from "node:stream"; +import { createGunzip, gzipSync } from "node:zlib"; + +interface Progress { + bytes: number; + chunks: number; +} + +function createCounter(progress: Progress) { + return new Transform({ + transform(chunk, _encoding, callback) { + const length = typeof chunk === "string" ? Buffer.byteLength(chunk) : chunk.length; + progress.bytes += length; + progress.chunks++; + callback(null, chunk); + }, + flush(callback) { + callback(); + }, + }); +} + +function gunzipThrough(payload: Buffer): Promise { + return new Promise((resolve) => { + const progress: Progress = { bytes: 0, chunks: 0 }; + const counter = createCounter(progress); + const gunzip = createGunzip(); + const out = new PassThrough(); + const parts: string[] = []; + counter.pipe(gunzip).pipe(out); + out.on("data", (d: Buffer) => parts.push(d.toString())); + out.on("end", () => resolve(`${parts.join("")} ${progress.bytes > 0} ${progress.chunks}`)); + counter.end(payload); + }); +} + +function readFileStream(file: string): Promise { + return new Promise((resolve) => { + const stream = fs.createReadStream(file); + let text = ""; + const release = () => console.log("released"); + stream.on("close", release); + stream.on("error", release); + stream.on("data", (chunk) => { + text += chunk.toString(); + }); + stream.on("end", () => resolve(text)); + }); +} + +function run(script: string): Promise { + return new Promise((resolve, reject) => { + const child = spawn("/bin/sh", ["-c", script], { stdio: "pipe" }); + let stdout = ""; + let stderr = ""; + if (child.stdout) { + child.stdout.setEncoding("utf8"); + child.stdout.on("data", (c) => { + stdout += c; + }); + } + if (child.stderr) { + child.stderr.on("data", (c) => { + const chunk = String(c); + stderr += chunk; + }); + } + child.on("error", reject); + child.on("close", (code: number, signal: string) => { + if (code === 0) return resolve(stdout); + reject(new Error(`Exited with ${code || signal}: ${stderr.trim()}`)); + }); + }); +} + +// setEncoding returns its stream: a listener on the returned value, or on +// the stream after `const out = stream.setEncoding(...)`, receives strings. +function readEncoded(script: string): Promise { + return new Promise((resolve) => { + const child = spawn("/bin/sh", ["-c", script], { stdio: "pipe" }); + let outText = ""; + let errText = ""; + const out = child.stdout.setEncoding("utf8"); + out.on("data", (c: unknown) => { + outText += typeof c + ":" + String(c) + ";"; + }); + const err = child.stderr.setEncoding("utf8"); + child.stderr.on("data", (c: unknown) => { + errText += typeof c + ":" + String(c) + ";"; + }); + void err; + child.on("close", () => resolve(outText + " err " + errText)); + }); +} + +async function main(): Promise { + console.log("gunzip", await gunzipThrough(gzipSync(Buffer.from("payload")))); + const file = path.join(os.tmpdir(), `scriptc-stream-construction-${process.pid}.txt`); + fs.writeFileSync(file, "file text\n"); + console.log("file", JSON.stringify(await readFileStream(file))); + fs.unlinkSync(file); + console.log("run", JSON.stringify(await run("echo hi; echo warn >&2"))); + console.log("encoded", JSON.stringify(await readEncoded("printf hi; printf warn >&2"))); + for (const script of ["echo bad >&2; exit 3", "kill -TERM $$"]) { + try { + await run(script); + } catch (e) { + console.log("error", (e as Error).message); + } + } +} + +void main(); diff --git a/tests/fixtures/node-types/timeout-type.ts b/tests/fixtures/node-types/timeout-type.ts new file mode 100644 index 000000000..42daee86d --- /dev/null +++ b/tests/fixtures/node-types/timeout-type.ts @@ -0,0 +1,30 @@ +// NodeJS.Timeout as a declared type under @types/node: fields, clearTimeout, +// ref/unref/hasRef, refresh. +class Poller { + private timer: NodeJS.Timeout | undefined; + private count = 0; + start(): void { + this.timer = setInterval(() => { + this.count++; + console.log("poll", this.count); + if (this.count === 3) this.stop(); + }, 5); + } + stop(): void { + if (this.timer) { + clearInterval(this.timer); + this.timer = undefined; + console.log("stopped"); + } + } +} +new Poller().start(); +const t: NodeJS.Timeout = setTimeout(() => console.log("never"), 1000); +console.log(t.hasRef()); +t.unref(); +console.log(t.hasRef()); +t.ref(); +console.log(t.hasRef()); +clearTimeout(t); +const timers: NodeJS.Timeout[] = [setTimeout(() => console.log("a"), 30), setTimeout(() => console.log("b"), 31)]; +console.log(timers.length); diff --git a/tests/harness/__snapshots__/arrow-bind.ts.txt b/tests/harness/__snapshots__/arrow-bind.ts.txt index 4618bdfe2..d63201d59 100644 --- a/tests/harness/__snapshots__/arrow-bind.ts.txt +++ b/tests/harness/__snapshots__/arrow-bind.ts.txt @@ -1,17 +1,8 @@ -arrow-bind.ts:2:16 - error SC1100: passing 'unknown' values where '((null | undefined)[]) => number' is expected is not supported yet +arrow-bind.ts:4:14 - error SC1100: passing 'unknown' values where '(number[]) => number' is expected is not supported yet - 1 | // Typed preset and rest signatures still exceed the callable adapter. - 2 | const preset = ((value: number) => value).bind(null, 1); - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 3 | const rest = ((...values: number[]) => values.length).bind(null); - - hint: validate with 'as ' first — the cast checks the dynamic value at runtime and throws on mismatch - -arrow-bind.ts:3:14 - error SC1100: passing 'unknown' values where '(number[]) => number' is expected is not supported yet - - 2 | const preset = ((value: number) => value).bind(null, 1); - 3 | const rest = ((...values: number[]) => values.length).bind(null); + 3 | const preset = ((value: number) => value).bind(null, 1); + 4 | const rest = ((...values: number[]) => values.length).bind(null); | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 4 | export {}; + 5 | export {}; hint: validate with 'as ' first — the cast checks the dynamic value at runtime and throws on mismatch \ No newline at end of file diff --git a/tests/harness/__snapshots__/child-process.ts.txt b/tests/harness/__snapshots__/child-process.ts.txt index 2d83132f5..f5f9f8f86 100644 --- a/tests/harness/__snapshots__/child-process.ts.txt +++ b/tests/harness/__snapshots__/child-process.ts.txt @@ -91,4 +91,18 @@ child-process.ts:43:64 - error SC2020: 'fork without a fourth-slot IPC channel' | ^~~~~~~~ 44 | // Keep each fence on its own statement so diagnostics remain site-specific. - hint: the fourth stdio entry must be "ipc" \ No newline at end of file + hint: the fourth stdio entry must be "ipc" + +child-process.ts:48:21 - error SC1090: untyped child-stream 'data' listeners whose setEncoding state is unclear at their registration sites (declare the chunk as 'Buffer' or 'string') are not supported yet + + 47 | const pipedOut = piped.stdout!; + 48 | pipedOut.on("data", (chunk: unknown) => console.log(String(chunk))); + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 49 | pipedOut.setEncoding("utf8"); + +child-process.ts:54:26 - error SC1090: untyped child-stream 'data' listeners whose setEncoding state is unclear at their registration sites (declare the chunk as 'Buffer' or 'string') are not supported yet + + 53 | pipedErr.setEncoding("utf8"); + 54 | piped.stderr!.on("data", (chunk: unknown) => console.log(String(chunk))); + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 55 | \ No newline at end of file diff --git a/tests/harness/__snapshots__/comptime.ts.txt b/tests/harness/__snapshots__/comptime.ts.txt index 2b1411ae5..757c30e6c 100644 --- a/tests/harness/__snapshots__/comptime.ts.txt +++ b/tests/harness/__snapshots__/comptime.ts.txt @@ -45,4 +45,18 @@ comptime.ts:22:23 - error SC1110: comptime evaluation failed: expected 'string' 21 | 22 | const lied = comptime(() => 1 as unknown as string); | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 23 | \ No newline at end of file + 23 | + +comptime.ts:29:22 - error SC1090: baking comptime results of type '{ next: null | ...; value: number }' into the binary (number, string, boolean, and arrays, records and unions of those lower to literals; a record or union type reachable from itself does not) is not supported yet + + 28 | } + 29 | const chain: Chain = comptime(() => ({ value: 1, next: null }) as Chain); + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 30 | + +comptime.ts:33:20 - error SC1090: baking comptime results of type '(...)[] | number' into the binary (number, string, boolean, and arrays, records and unions of those lower to literals; a record or union type reachable from itself does not) is not supported yet + + 32 | type Tree = number | Tree[]; + 33 | const tree: Tree = comptime((): Tree => [1, [2, [3]]]); + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 34 | \ No newline at end of file diff --git a/tests/harness/__snapshots__/fromentries-aliased-entries.ts.txt b/tests/harness/__snapshots__/fromentries-aliased-entries.ts.txt new file mode 100644 index 000000000..7b0f02266 --- /dev/null +++ b/tests/harness/__snapshots__/fromentries-aliased-entries.ts.txt @@ -0,0 +1,8 @@ +fromentries-aliased-entries.ts:8:16 - error SC2002: record shapes must match exactly or width-coerce: expected '{ a: number[]; b: number[] }', got '{ [key: string]: number[] }' — 'a' is not a declared field of the source, and the source's index signature could hold it at runtime (a completed undefined would drop that value) + + 7 | const shared = [10]; + 8 | const scores = Object.fromEntries([ + | ^~~~~~~~~~~~~~~~~~~~ + 9 | ["a", shared], + + hint: width subtyping compiles as a copy (each expected field copied off the source; extra fields drop) — this pair is outside the copy's rules; build a literal with exactly the expected fields instead \ No newline at end of file diff --git a/tests/harness/__snapshots__/json-shape/main.ts.txt b/tests/harness/__snapshots__/json-shape/main.ts.txt index b03f098a6..85f5d2917 100644 --- a/tests/harness/__snapshots__/json-shape/main.ts.txt +++ b/tests/harness/__snapshots__/json-shape/main.ts.txt @@ -1,6 +1,6 @@ -json-shape/main.ts:5:8 - error SC2001: values of type '{ name: string; deprecated: null; }' cannot be compiled yet (supported: number, string, boolean, arrays, Maps, Sets, RegExp, read-only Date values, functions, classes, records, unions of those, and 'unknown') +json-shape/main.ts:5:8 - error SC1110: comptime evaluation failed: expected 'number' at $.limit, got Infinity (only finite numbers can be written as literals) - 4 | * engine) and uses of the binding poison. */ + 4 | * field bakes as null.) */ 5 | import bad from "./nulled.json" with { type: "json" }; | ^~~ 6 | diff --git a/tests/harness/__snapshots__/variadic-wrapper-fences.ts.txt b/tests/harness/__snapshots__/variadic-wrapper-fences.ts.txt new file mode 100644 index 000000000..0cb2774d2 --- /dev/null +++ b/tests/harness/__snapshots__/variadic-wrapper-fences.ts.txt @@ -0,0 +1,29 @@ +variadic-wrapper-fences.ts:6:7 - error SC2009: values of type '() => any' cannot be compiled: the function shape is supported, but its signature is variadic ('arguments'-reading), and a compiled signature is fixed-arity + + 5 | // `arguments` with no fixed signature to lower against. + 6 | const loose = function () { + | ^~~~~ + 7 | return arguments[0]; + + hint: the outer shape is supported — the named component is the blocker; restate it inside the compilable set (number, string, boolean, arrays, Maps, Sets, RegExp, functions, classes, records, unions of those, and 'unknown') + +variadic-wrapper-fences.ts:7:10 - error SC1090: '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) is not supported yet + + 6 | const loose = function () { + 7 | return arguments[0]; + | ^~~~~~~~~ + 8 | }; + +variadic-wrapper-fences.ts:12:10 - error SC1090: reading 'fn.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) is not supported yet (instantiating 'named' with <() => number>) + + 11 | function named(fn: F): string { + 12 | return fn.name; + | ^~~~~~~ + 13 | } + +variadic-wrapper-fences.ts:25:10 - error SC1090: forwarding a fixed argument tuple through 'apply' to a variadic function value is not supported yet (instantiating 'forward' with <[number, number]>) + + 24 | function forward(sink: (...xs: number[]) => number, ...args: A): number { + 25 | return sink.apply(null, args); + | ^~~~~~~~~~~~~~~~~~~~~~ + 26 | } \ No newline at end of file diff --git a/tests/harness/corpus-costs/5817-constructor-function-instanceof.cjs.json b/tests/harness/corpus-costs/5817-constructor-function-instanceof.cjs.json new file mode 100644 index 000000000..15eedbeaa --- /dev/null +++ b/tests/harness/corpus-costs/5817-constructor-function-instanceof.cjs.json @@ -0,0 +1 @@ +{ "plain": 2655, "san": 7944 } diff --git a/tests/harness/corpus-costs/5818-constructor-function-emitter-defaults.cjs.json b/tests/harness/corpus-costs/5818-constructor-function-emitter-defaults.cjs.json new file mode 100644 index 000000000..0f4eb2e02 --- /dev/null +++ b/tests/harness/corpus-costs/5818-constructor-function-emitter-defaults.cjs.json @@ -0,0 +1 @@ +{ "plain": 2746, "san": 51663 } diff --git a/tests/harness/corpus-costs/6380-variadic-wrapper-arguments-apply.ts.json b/tests/harness/corpus-costs/6380-variadic-wrapper-arguments-apply.ts.json new file mode 100644 index 000000000..553943f5a --- /dev/null +++ b/tests/harness/corpus-costs/6380-variadic-wrapper-arguments-apply.ts.json @@ -0,0 +1 @@ +{ "plain": 3870, "san": 3660 } diff --git a/tests/harness/corpus-costs/6381-variadic-wrapper-async.ts.json b/tests/harness/corpus-costs/6381-variadic-wrapper-async.ts.json new file mode 100644 index 000000000..5cad95fef --- /dev/null +++ b/tests/harness/corpus-costs/6381-variadic-wrapper-async.ts.json @@ -0,0 +1 @@ +{ "plain": 3550, "san": 4820 } diff --git a/tests/harness/corpus-costs/6382-variadic-wrapper-methods-this.ts.json b/tests/harness/corpus-costs/6382-variadic-wrapper-methods-this.ts.json new file mode 100644 index 000000000..644fc5fea --- /dev/null +++ b/tests/harness/corpus-costs/6382-variadic-wrapper-methods-this.ts.json @@ -0,0 +1 @@ +{ "plain": 3000, "san": 6260 } diff --git a/tests/harness/corpus-costs/6383-variadic-wrapper-rest-tuple.ts.json b/tests/harness/corpus-costs/6383-variadic-wrapper-rest-tuple.ts.json new file mode 100644 index 000000000..f83a73df7 --- /dev/null +++ b/tests/harness/corpus-costs/6383-variadic-wrapper-rest-tuple.ts.json @@ -0,0 +1 @@ +{ "plain": 3140, "san": 4540 } diff --git a/tests/harness/corpus-costs/6384-variadic-wrapper-nested.ts.json b/tests/harness/corpus-costs/6384-variadic-wrapper-nested.ts.json new file mode 100644 index 000000000..21dee9274 --- /dev/null +++ b/tests/harness/corpus-costs/6384-variadic-wrapper-nested.ts.json @@ -0,0 +1 @@ +{ "plain": 3220, "san": 2960 } diff --git a/tests/harness/corpus-costs/6385-variadic-wrapper-shared-local-names.ts.json b/tests/harness/corpus-costs/6385-variadic-wrapper-shared-local-names.ts.json new file mode 100644 index 000000000..187f1764f --- /dev/null +++ b/tests/harness/corpus-costs/6385-variadic-wrapper-shared-local-names.ts.json @@ -0,0 +1 @@ +{ "plain": 2200, "san": 3300 } diff --git a/tests/harness/corpus-costs/6390-catch-binding-predicate.ts.json b/tests/harness/corpus-costs/6390-catch-binding-predicate.ts.json new file mode 100644 index 000000000..5b661172c --- /dev/null +++ b/tests/harness/corpus-costs/6390-catch-binding-predicate.ts.json @@ -0,0 +1 @@ +{ "plain": 3020, "san": 3560 } diff --git a/tests/harness/corpus-costs/6391-nullish-function-default.ts.json b/tests/harness/corpus-costs/6391-nullish-function-default.ts.json new file mode 100644 index 000000000..a3ae15ea4 --- /dev/null +++ b/tests/harness/corpus-costs/6391-nullish-function-default.ts.json @@ -0,0 +1 @@ +{ "plain": 4100, "san": 3150 } diff --git a/tests/harness/corpus-costs/6392-optional-chain-protocol-calls.ts.json b/tests/harness/corpus-costs/6392-optional-chain-protocol-calls.ts.json new file mode 100644 index 000000000..3fa72b729 --- /dev/null +++ b/tests/harness/corpus-costs/6392-optional-chain-protocol-calls.ts.json @@ -0,0 +1 @@ +{ "plain": 4400, "san": 4200 } diff --git a/tests/harness/corpus-costs/6393-function-types-any-params.ts.json b/tests/harness/corpus-costs/6393-function-types-any-params.ts.json new file mode 100644 index 000000000..73f790694 --- /dev/null +++ b/tests/harness/corpus-costs/6393-function-types-any-params.ts.json @@ -0,0 +1 @@ +{ "plain": 2790, "san": 3150 } diff --git a/tests/harness/corpus-costs/6394-in-key-error-subclasses.ts.json b/tests/harness/corpus-costs/6394-in-key-error-subclasses.ts.json new file mode 100644 index 000000000..6fe8b6765 --- /dev/null +++ b/tests/harness/corpus-costs/6394-in-key-error-subclasses.ts.json @@ -0,0 +1 @@ +{ "plain": 2900, "san": 3120 } diff --git a/tests/harness/corpus-costs/6395-generic-object-entries.ts.json b/tests/harness/corpus-costs/6395-generic-object-entries.ts.json new file mode 100644 index 000000000..d38b11826 --- /dev/null +++ b/tests/harness/corpus-costs/6395-generic-object-entries.ts.json @@ -0,0 +1 @@ +{ "plain": 2990, "san": 3030 } diff --git a/tests/harness/corpus-costs/6396-generic-any-instantiation.ts.json b/tests/harness/corpus-costs/6396-generic-any-instantiation.ts.json new file mode 100644 index 000000000..d38f92d43 --- /dev/null +++ b/tests/harness/corpus-costs/6396-generic-any-instantiation.ts.json @@ -0,0 +1 @@ +{ "plain": 2590, "san": 2850 } diff --git a/tests/harness/corpus-costs/6397-event-loop-metrics.ts.json b/tests/harness/corpus-costs/6397-event-loop-metrics.ts.json new file mode 100644 index 000000000..26c5a51a4 --- /dev/null +++ b/tests/harness/corpus-costs/6397-event-loop-metrics.ts.json @@ -0,0 +1 @@ +{ "plain": 3300, "san": 4140 } diff --git a/tests/harness/corpus-costs/6398-conditional-fixed-arity.ts.json b/tests/harness/corpus-costs/6398-conditional-fixed-arity.ts.json new file mode 100644 index 000000000..c8912b309 --- /dev/null +++ b/tests/harness/corpus-costs/6398-conditional-fixed-arity.ts.json @@ -0,0 +1 @@ +{ "plain": 3630, "san": 3870 } diff --git a/tests/harness/corpus-costs/6453-constructor-function-emitter.cjs.json b/tests/harness/corpus-costs/6453-constructor-function-emitter.cjs.json new file mode 100644 index 000000000..a09a333d5 --- /dev/null +++ b/tests/harness/corpus-costs/6453-constructor-function-emitter.cjs.json @@ -0,0 +1 @@ +{ "plain": 4300, "san": 51474 } diff --git a/tests/harness/corpus-costs/6454-constructor-function-stream-bases.cjs.json b/tests/harness/corpus-costs/6454-constructor-function-stream-bases.cjs.json new file mode 100644 index 000000000..25987b67f --- /dev/null +++ b/tests/harness/corpus-costs/6454-constructor-function-stream-bases.cjs.json @@ -0,0 +1 @@ +{ "plain": 3371, "san": 3844 } diff --git a/tests/harness/corpus-costs/6465-optional-members-over-index-signature.ts.json b/tests/harness/corpus-costs/6465-optional-members-over-index-signature.ts.json new file mode 100644 index 000000000..93c788dd2 --- /dev/null +++ b/tests/harness/corpus-costs/6465-optional-members-over-index-signature.ts.json @@ -0,0 +1 @@ +{ "plain": 3225, "san": 26787 } diff --git a/tests/harness/corpus-costs/6466-never-assertion-argument.ts.json b/tests/harness/corpus-costs/6466-never-assertion-argument.ts.json new file mode 100644 index 000000000..4b7e88af5 --- /dev/null +++ b/tests/harness/corpus-costs/6466-never-assertion-argument.ts.json @@ -0,0 +1 @@ +{ "plain": 2895, "san": 23565 } diff --git a/tests/harness/corpus-costs/6467-fromentries-record-assertion.ts.json b/tests/harness/corpus-costs/6467-fromentries-record-assertion.ts.json new file mode 100644 index 000000000..390533b9e --- /dev/null +++ b/tests/harness/corpus-costs/6467-fromentries-record-assertion.ts.json @@ -0,0 +1 @@ +{ "plain": 3093, "san": 22400 } diff --git a/tests/harness/corpus-costs/6468-generic-unknown-record-entries.ts.json b/tests/harness/corpus-costs/6468-generic-unknown-record-entries.ts.json new file mode 100644 index 000000000..8489daced --- /dev/null +++ b/tests/harness/corpus-costs/6468-generic-unknown-record-entries.ts.json @@ -0,0 +1 @@ +{ "plain": 4560, "san": 4770 } diff --git a/tests/harness/corpus-costs/6468-node-stdio-writable-slot.ts.json b/tests/harness/corpus-costs/6468-node-stdio-writable-slot.ts.json new file mode 100644 index 000000000..3fafdfc4b --- /dev/null +++ b/tests/harness/corpus-costs/6468-node-stdio-writable-slot.ts.json @@ -0,0 +1 @@ +{ "plain": 3000, "san": 3500 } diff --git a/tests/harness/corpus-costs/6469-node-stdio-writable-js-pipe/main.ts.json b/tests/harness/corpus-costs/6469-node-stdio-writable-js-pipe/main.ts.json new file mode 100644 index 000000000..3fafdfc4b --- /dev/null +++ b/tests/harness/corpus-costs/6469-node-stdio-writable-js-pipe/main.ts.json @@ -0,0 +1 @@ +{ "plain": 3000, "san": 3500 } diff --git a/tests/harness/corpus-costs/webassembly-api.mjs.json b/tests/harness/corpus-costs/webassembly-api.mjs.json index 9ae6a0914..dd2a34243 100644 --- a/tests/harness/corpus-costs/webassembly-api.mjs.json +++ b/tests/harness/corpus-costs/webassembly-api.mjs.json @@ -1 +1 @@ -{ "plain": 5791, "san": 8967 } +{ "plain": 6776, "san": 5737 } diff --git a/tests/harness/corpus-costs/webassembly-externref-lifetimes.mjs.json b/tests/harness/corpus-costs/webassembly-externref-lifetimes.mjs.json new file mode 100644 index 000000000..d88c1b461 --- /dev/null +++ b/tests/harness/corpus-costs/webassembly-externref-lifetimes.mjs.json @@ -0,0 +1 @@ +{ "plain": 3200, "san": 5700 } diff --git a/tests/harness/corpus-costs/webassembly-spec-control/main.mjs.json b/tests/harness/corpus-costs/webassembly-spec-control/main.mjs.json index 0fc407f44..223969e02 100644 --- a/tests/harness/corpus-costs/webassembly-spec-control/main.mjs.json +++ b/tests/harness/corpus-costs/webassembly-spec-control/main.mjs.json @@ -1 +1 @@ -{ "plain": 6897, "san": 6811 } +{ "plain": 6916, "san": 8022 } diff --git a/tests/harness/corpus-costs/webassembly-spec-float/main.mjs.json b/tests/harness/corpus-costs/webassembly-spec-float/main.mjs.json index 32346d30e..3a4a79b75 100644 --- a/tests/harness/corpus-costs/webassembly-spec-float/main.mjs.json +++ b/tests/harness/corpus-costs/webassembly-spec-float/main.mjs.json @@ -1 +1 @@ -{ "plain": 5824, "san": 5785 } +{ "plain": 5393, "san": 6409 } diff --git a/tests/harness/corpus-costs/webassembly-spec-int/main.mjs.json b/tests/harness/corpus-costs/webassembly-spec-int/main.mjs.json index bd7c3fe27..c3541f5ca 100644 --- a/tests/harness/corpus-costs/webassembly-spec-int/main.mjs.json +++ b/tests/harness/corpus-costs/webassembly-spec-int/main.mjs.json @@ -1 +1 @@ -{ "plain": 5263, "san": 5365 } +{ "plain": 4576, "san": 5275 } diff --git a/tests/harness/corpus-costs/webassembly-spec-memory/main.mjs.json b/tests/harness/corpus-costs/webassembly-spec-memory/main.mjs.json index 278affa93..074a854fe 100644 --- a/tests/harness/corpus-costs/webassembly-spec-memory/main.mjs.json +++ b/tests/harness/corpus-costs/webassembly-spec-memory/main.mjs.json @@ -1 +1 @@ -{ "plain": 5258, "san": 6666 } +{ "plain": 6100, "san": 7066 } diff --git a/tests/harness/corpus-costs/webassembly-spec-table/main.mjs.json b/tests/harness/corpus-costs/webassembly-spec-table/main.mjs.json index baed25510..da0d7c4e2 100644 --- a/tests/harness/corpus-costs/webassembly-spec-table/main.mjs.json +++ b/tests/harness/corpus-costs/webassembly-spec-table/main.mjs.json @@ -1 +1 @@ -{ "plain": 5592, "san": 6866 } +{ "plain": 6712, "san": 7095 } diff --git a/tests/harness/corpus-costs/webassembly-typed.ts.json b/tests/harness/corpus-costs/webassembly-typed.ts.json index 216ca1c3c..e698fa802 100644 --- a/tests/harness/corpus-costs/webassembly-typed.ts.json +++ b/tests/harness/corpus-costs/webassembly-typed.ts.json @@ -1 +1 @@ -{ "plain": 3255, "san": 3877 } +{ "plain": 3602, "san": 3848 } diff --git a/tests/harness/corpus-costs/webassembly-worker.ts.json b/tests/harness/corpus-costs/webassembly-worker.ts.json index e1bab7992..b91ffeb21 100644 --- a/tests/harness/corpus-costs/webassembly-worker.ts.json +++ b/tests/harness/corpus-costs/webassembly-worker.ts.json @@ -1 +1 @@ -{ "plain": 3597, "san": 42856 } +{ "plain": 4051, "san": 4892 } diff --git a/tests/harness/errors.test.ts b/tests/harness/errors.test.ts index 6d93675ef..c35022f51 100644 --- a/tests/harness/errors.test.ts +++ b/tests/harness/errors.test.ts @@ -1040,7 +1040,7 @@ describe("refused statements in speculatively lowered JavaScript functions", () `(function () { var root = this; console.log("before"); - console.log(root === eval("this")); + process.on("__proto__", () => root); console.log("after"); }).call(this); `, @@ -1051,7 +1051,7 @@ describe("refused statements in speculatively lowered JavaScript functions", () "speculative-apply", `(function () { console.log("before"); - console.log(typeof eval("1")); + process.on("__proto__", () => {}); }).apply(null, []); `, "before\n", @@ -1062,7 +1062,7 @@ describe("refused statements in speculatively lowered JavaScript functions", () `class Base {} class Derived extends (function () { console.log("before"); - console.log(typeof eval("1")); + process.on("__proto__", () => {}); return Base; })() {} console.log(typeof Derived); @@ -1076,7 +1076,7 @@ console.log(typeof Derived); expect(r.stdout).toBe(stdout); expect(r.stderr).toMatch( new RegExp( - `^Uncaught Error: values of type '\\(x: string\\) => any' cannot be compiled: .* \\[SC2009 at .*${name}\\.cjs:${line}\\]\\n$`, + `^Uncaught Error: 'process\\.on\\("__proto__", \\.\\.\\.\\)' is part of the standard library types but has no scriptc lowering yet \\[SC2020 at .*${name}\\.cjs:${line}\\]\\n$`, ), ); }); diff --git a/tests/harness/library-asyncfree.test.ts b/tests/harness/library-asyncfree.test.ts index 0adbef7bc..7c4d029cc 100644 --- a/tests/harness/library-asyncfree.test.ts +++ b/tests/harness/library-asyncfree.test.ts @@ -73,6 +73,21 @@ describe("async_free detection over the IR", () => { ); }); + test("the event-loop metrics refuse", async () => { + expect( + await surfaceOf( + "eld", + `import { monitorEventLoopDelay } from "node:perf_hooks";\nconst h = monitorEventLoopDelay();\nconsole.log(h.enable());\n`, + ), + ).toContain("monitorEventLoopDelay"); + expect( + await surfaceOf( + "elu", + `import { performance } from "node:perf_hooks";\nconsole.log(performance.eventLoopUtilization().idle >= 0);\n`, + ), + ).toContain("eventLoopUtilization"); + }); + test("child_process refuses even in its synchronous spelling", async () => { expect( await surfaceOf( diff --git a/tests/harness/library-profile.test.ts b/tests/harness/library-profile.test.ts index eb1bcc4cd..2b28a6cd0 100644 --- a/tests/harness/library-profile.test.ts +++ b/tests/harness/library-profile.test.ts @@ -569,10 +569,13 @@ describe("library profile fences", () => { expect(processIds).toContain("node-builtin.process.isTTY"); expect(r.profile.fences[1]!.surfaces.every((s) => s.detector !== undefined)).toBe(true); // The user-timing timeline reads the same clock (mark() and measure() - // default to performance.now()), so it joins the perf_hooks family. + // default to performance.now()), and so do the event-loop metrics, so + // they join the perf_hooks family. expect(r.profile.fences[2]!.surfaces.map((s) => s.id)).toEqual([ + "node-builtin.perf_hooks.monitorEventLoopDelay", "node-builtin.perf_hooks.performance.clearMarks", "node-builtin.perf_hooks.performance.clearMeasures", + "node-builtin.perf_hooks.performance.eventLoopUtilization", "node-builtin.perf_hooks.performance.getEntries", "node-builtin.perf_hooks.performance.mark", "node-builtin.perf_hooks.performance.measure", diff --git a/tests/harness/npm.test.ts b/tests/harness/npm.test.ts index 25baaded6..168fdb6bd 100644 --- a/tests/harness/npm.test.ts +++ b/tests/harness/npm.test.ts @@ -60,7 +60,7 @@ const KNOWN: Record = { }, "island-web-plumbing": { run: "SC2020" }, "lazy-traps": { build: "SC2013" }, - "misc-shims": { build: "SC2013" }, + "misc-shims": { run: "SC2020" }, "namespace": { build: "SC1013" }, "node24-async-events": { run: "SC2020" }, "node24-commonjs-metadata": { run: "SC2020" }, diff --git a/tests/harness/project-config.test.ts b/tests/harness/project-config.test.ts index 742eb5b4f..1e1905368 100644 --- a/tests/harness/project-config.test.ts +++ b/tests/harness/project-config.test.ts @@ -52,6 +52,32 @@ test("node-types: filesystem capacity supports typed number and BigInt results u expect(comparableStderr(native.stderr)).toBe(node.stderr); }); +for (const [name, title] of [ + [ + "event-loop-metrics", + "eventLoopUtilization and monitorEventLoopDelay follow @types/node's types", + ], + ["timeout-type", "NodeJS.Timeout fields, ref/unref/hasRef and clearInterval under @types/node"], +] as const) { + test(`node-types: ${title}`, async () => { + const outDir = outDirFor(name); + const entry = join(nodeTypesDir, `${name}.ts`); + const result = await compile(entry, { + outPath: join(outDir, process.platform === "win32" ? `${name}.exe` : name), + outDir, + sanitize, + }); + expect(result.ok, !result.ok ? JSON.stringify(result.diagnostics, null, 2) : "").toBe(true); + if (!result.ok) return; + const [native, node] = await Promise.all([ + execFileAsync(result.binaryPath), + execFileAsync(process.execPath, ["--disable-warning=MODULE_TYPELESS_PACKAGE_JSON", entry]), + ]); + expect(native.stdout).toBe(node.stdout); + expect(comparableStderr(native.stderr)).toBe(node.stderr); + }); +} + test("node-types: http header arrays and server.address() follow @types/node's types", async () => { const outDir = outDirFor("http-headers-address"); const entry = join(nodeTypesDir, "http-headers-address.ts"); @@ -518,6 +544,42 @@ test("node-types: stream subclasses extend the @types/node stream classes", asyn expect(comparableStderr(native.stderr)).toBe(node.stderr); }); +test("node-types: NodeJS stream interfaces and stream.Readable/Writable take every stream producer", async () => { + const outDir = outDirFor("node-stream-interfaces"); + const entry = join(nodeTypesDir, "node-stream-interfaces.ts"); + const result = await compile(entry, { + outPath: join(outDir, "node-stream-interfaces"), + outDir, + sanitize, + }); + expect(result.ok, !result.ok ? JSON.stringify(result.diagnostics, null, 2) : "").toBe(true); + if (!result.ok) return; + const [native, node] = await Promise.all([ + execFileAsync(result.binaryPath), + execFileAsync(process.execPath, ["--disable-warning=MODULE_TYPELESS_PACKAGE_JSON", entry]), + ]); + expect(native.stdout).toBe(node.stdout); + expect(comparableStderr(native.stderr)).toBe(node.stderr); +}); + +test("node-types: streams construct and child listeners take @types/node's parameter types", async () => { + const outDir = outDirFor("stream-construction"); + const entry = join(nodeTypesDir, "stream-construction.ts"); + const result = await compile(entry, { + outPath: join(outDir, "stream-construction"), + outDir, + sanitize, + }); + expect(result.ok, !result.ok ? JSON.stringify(result.diagnostics, null, 2) : "").toBe(true); + if (!result.ok) return; + const [native, node] = await Promise.all([ + execFileAsync(result.binaryPath), + execFileAsync(process.execPath, ["--disable-warning=MODULE_TYPELESS_PACKAGE_JSON", entry]), + ]); + expect(native.stdout).toBe(node.stdout); + expect(comparableStderr(native.stderr)).toBe(node.stderr); +}); + test("node-types: promisified setTimeout and setImmediate lower as timers/promises", async () => { const outDir = outDirFor("promisify-timers"); const entry = join(nodeTypesDir, "promisify-timers.ts"); @@ -590,26 +652,22 @@ test("node-types: promisified dns lookups settle with the lookup objects", async expect(comparableStderr(native.stderr)).toBe(node.stderr); }); -test("node-types: unsupported child stdin forms remain named fences", async () => { - const dir = outDirFor("node-child-stdin-fences"); - const entry = join(nodeTypesDir, "child-stdin-fences.ts"); +test("node-types: child stdin forms beyond write(string) compile through the general stream representation", async () => { + const dir = outDirFor("node-child-stdin-streams"); + const entry = join(nodeTypesDir, "child-stdin-streams.ts"); const result = await compile(entry, { - outPath: join(dir, "child-stdin-fences"), + outPath: join(dir, "child-stdin-streams"), outDir: dir, sanitize, }); - expect(result.ok).toBe(false); - if (result.ok) return; - expect(result.diagnostics.map((diagnostic) => diagnostic.code)).toEqual([ - "SC2020", - "SC2020", - "SC2020", - ]); - expect(result.diagnostics.map((diagnostic) => diagnostic.message)).toEqual([ - expect.stringContaining("child stdin write with 2 arguments"), - expect.stringContaining("Writable.end"), - expect.stringContaining("Writable.destroy"), + expect(result.ok, !result.ok ? JSON.stringify(result.diagnostics, null, 2) : "").toBe(true); + if (!result.ok) return; + const [native, node] = await Promise.all([ + execFileAsync(result.binaryPath), + execFileAsync(process.execPath, ["--disable-warning=MODULE_TYPELESS_PACKAGE_JSON", entry]), ]); + expect(native.stdout).toBe(node.stdout); + expect(comparableStderr(native.stderr)).toBe(node.stderr); }); test("node-types: callback exec uses the typed ExecException overloads", async () => { diff --git a/tests/harness/self-hosting-native-toolchain.test.ts b/tests/harness/self-hosting-native-toolchain.test.ts index edb4b8b0f..f924845f1 100644 --- a/tests/harness/self-hosting-native-toolchain.test.ts +++ b/tests/harness/self-hosting-native-toolchain.test.ts @@ -30,6 +30,7 @@ const features: NativeLinkFeatures = { bigint: false, searchParams: false, qs: false, + dynBuiltinClass: false, parseArgs: false, stream: false, net: false, @@ -44,6 +45,7 @@ const features: NativeLinkFeatures = { tlsCa: false, cipher: false, webcrypto: false, + perfLoop: false, }; test("native toolchain configuration, staging and helper validation are static", async () => { diff --git a/tests/harness/self-hosting-runtime-features.test.ts b/tests/harness/self-hosting-runtime-features.test.ts index d2c2b589b..8d65d72cd 100644 --- a/tests/harness/self-hosting-runtime-features.test.ts +++ b/tests/harness/self-hosting-runtime-features.test.ts @@ -50,6 +50,7 @@ const featureCalls: readonly [string, IrLibFn][] = [ ["bigint", "bigint.parse"], ["searchParams", "sp.new"], ["qs", "qs.parse"], + ["dynBuiltinClass", "dyn.builtinClass"], ["parseArgs", "util.parseArgs"], ["parseArgs", "util.isDeepStrictEqual"], ["parseArgs", "util.styleText"], diff --git a/tests/harness/self-hosting-runtime-pack.test.ts b/tests/harness/self-hosting-runtime-pack.test.ts index 37db7637f..f8a280ef4 100644 --- a/tests/harness/self-hosting-runtime-pack.test.ts +++ b/tests/harness/self-hosting-runtime-pack.test.ts @@ -31,6 +31,7 @@ const base: NativeLinkFeatures = { bigint: false, searchParams: false, qs: false, + dynBuiltinClass: false, parseArgs: false, stream: false, net: false, @@ -45,6 +46,7 @@ const base: NativeLinkFeatures = { tlsCa: false, cipher: false, webcrypto: false, + perfLoop: false, }; test("runtime manifest validation and selection are entirely static", async () => { diff --git a/tests/harness/webassembly-memory-growth.test.ts b/tests/harness/webassembly-memory-growth.test.ts new file mode 100644 index 000000000..02fafe5ca --- /dev/null +++ b/tests/harness/webassembly-memory-growth.test.ts @@ -0,0 +1,96 @@ +/* WebAssembly memory growth while JavaScript holds views of the buffer — + * scriptc-only. + * + * Node detaches the previous ArrayBuffer when a memory grows (its views + * read as empty). scriptc does not detach it yet: the previous buffer and + * its views keep their length and keep aliasing the memory's storage, + * which therefore never moves, also in the explicitly bounds-checked + * configuration (sanitized builds, Windows) that has no guard pages. These + * pins check that writes through an old view and through the guest reach + * the same storage after growth, from JavaScript and from the guest. + * Under SCRIPTC_SAN=1 an access through a view of moved storage is an + * AddressSanitizer report. + */ +import { execFile } from "node:child_process"; +import { createHash } from "node:crypto"; +import { mkdirSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { promisify } from "node:util"; +import { describe, expect, test } from "vitest"; +import { compile } from "@scriptc/compiler"; + +const execFileAsync = promisify(execFile); +const repoRoot = join(import.meta.dirname, "../.."); +const cacheDir = join(repoRoot, "node_modules/.cache/scriptc-tests"); +const sanitize = process.env["SCRIPTC_SAN"] === "1"; + +// (module +// (memory (export "memory") 1 64) +// (func (export "grow") (param i32) (result i32) (memory.grow (local.get 0))) +// (func (export "poke") (param i32 i32) (i32.store8 (local.get 0) (local.get 1))) +// (func (export "peek") (param i32) (result i32) (i32.load8_u (local.get 0)))) +const growthModule = + "AGFzbQEAAAABCwJgAX8Bf2ACf38AAwQDAAEABQQBAQFABx8EBm1lbW9yeQIABGdyb3cAAARwb2tlAAEEcGVlawACChoDBgAgAEAACwkAIAAgAToAAAsHACAALQAACw=="; + +async function run(name: string, source: string): Promise { + const key = createHash("sha256") + .update(source) + .update(sanitize ? "san" : "plain") + .digest("hex") + .slice(0, 16); + const outDir = join(cacheDir, `wasm-growth-${key}`); + mkdirSync(outDir, { recursive: true }); + const file = join(outDir, `${name}.mjs`); + writeFileSync(file, source); + const result = await compile(file, { + outPath: join(outDir, name), + outDir, + sanitize, + backend: "llvm", + }); + if (!result.ok) { + throw new Error( + "program failed to compile:\n" + + result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"), + ); + } + const { stdout } = await execFileAsync(result.binaryPath, [], { encoding: "utf8" }); + return stdout; +} + +describe(`WebAssembly memory growth with live views${sanitize ? " (sanitized)" : ""}`, () => { + test("Memory.prototype.grow keeps earlier views on the memory's storage", async () => { + const out = await run( + "js-growth", + `const bytes = Buffer.from("${growthModule}", "base64"); +const { memory } = new WebAssembly.Instance(new WebAssembly.Module(bytes)).exports; +const view = new Uint8Array(memory.buffer); +view[0] = 7; +for (let i = 0; i < 40; i++) memory.grow(1); +view[1] = 9; +const now = new Uint8Array(memory.buffer); +now[2] = 11; +console.log(view.length, now.length, now[0], now[1], view[2]); +`, + ); + expect(out).toBe("65536 2686976 7 9 11\n"); + }, 180_000); + + test("memory.grow in the guest keeps earlier views on the memory's storage", async () => { + const out = await run( + "guest-growth", + `const bytes = Buffer.from("${growthModule}", "base64"); +const { exports } = new WebAssembly.Instance(new WebAssembly.Module(bytes)); +const view = new Uint8Array(exports.memory.buffer); +view[0] = 7; +let pages = 0; +for (let i = 0; i < 40; i++) pages = exports.grow(1); +view[1] = 9; +exports.poke(2, 11); +const now = new Uint8Array(exports.memory.buffer); +console.log(pages, view.length, now.length, exports.peek(0), exports.peek(1), view[2], now[2]); +`, + ); + expect(out).toBe("40 65536 2686976 7 9 11 11\n"); + }, 180_000); +}); diff --git a/tests/wasm-spec/corpus/externref-lifetimes.wat b/tests/wasm-spec/corpus/externref-lifetimes.wat new file mode 100644 index 000000000..ce7d9769e --- /dev/null +++ b/tests/wasm-spec/corpus/externref-lifetimes.wat @@ -0,0 +1,24 @@ +(module + (import "env" "make" (func $make (result externref))) + (import "env" "clear" (func $clear)) + (global $g (export "slot") (mut externref) (ref.null extern)) + (table $t (export "cells") 1 externref) + ;; An import's result, returned through an export. + (func (export "pass") (result externref) + (call $make)) + ;; A global's reference, read into a local, then replaced by the guest. + (func (export "takeGlobal") (result externref) (local $x externref) + (local.set $x (global.get $g)) + (global.set $g (ref.null extern)) + (local.get $x)) + ;; A table's reference, read into a local, then replaced by the guest. + (func (export "takeTable") (result externref) (local $x externref) + (local.set $x (table.get $t (i32.const 0))) + (table.set $t (i32.const 0) (ref.null extern)) + (local.get $x)) + ;; A global's reference, read into a local, then replaced by JavaScript + ;; during an import call. + (func (export "takeAcrossCall") (result externref) (local $x externref) + (local.set $x (global.get $g)) + (call $clear) + (local.get $x)))