diff --git a/docs/content/docs/limitations.mdx b/docs/content/docs/limitations.mdx index 04ba7583c..eb8d9c570 100644 --- a/docs/content/docs/limitations.mdx +++ b/docs/content/docs/limitations.mdx @@ -52,6 +52,7 @@ A declaration in the standard library does not guarantee that an API compiles. U - **Text encoding:** `TextEncoder` and `TextDecoder` can be stored in fields, arguments, return values, arrays, and closures. Encoding supports UTF-8, including `encodeInto` into a Uint8Array or Buffer view. Codec properties expose the canonical encoding and decoder flags. Decoding accepts recognized WHATWG labels, including runtime labels and omitted or undefined encodings. Literal constructor options support `fatal` and `ignoreBOM`; literal decode options support streaming UTF-8, UTF-16, and single-byte encodings. Other legacy streaming encodings, runtime options objects, inspection, JSON conversion, and module-scope declarations using `var` or lacking an initializer are unsupported. Use initialized `let` or `const` bindings for stored codecs. - **EventEmitter:** Literal event names use fixed argument tuples. Computed string names require a constant prefix or suffix that excludes `error`, meta events, and stream-internal events; their arguments must match the exact arity and support checked conversion. Unrestricted names, symbols, names that may reach internal events, and typed `listeners()`/`rawListeners()` results for names with possible computed registrations are unsupported. +- **Streams and readline:** Stream chunks are Buffers or strings. Object-mode streams of other values, `Readable.from` over arrays of other values, the `Readable` iterator helpers (`map`, `filter`, `toArray`, and the others), `stream.compose`, and generator stages in `pipeline` are unsupported. `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. `node:readline` and `node:readline/promises` interfaces read `process.stdin` or a readable stream without terminal mode: `question`, `prompt`, `write`, `pause`, `resume`, `'line'`/`'close'`/`'pause'`/`'resume'`/`'error'` listeners, and `for await` compile. Terminal mode, `completer`, history options, and chained listener registration are unsupported. - **CommonJS metadata:** The module graph is fixed at build time. Cache deletion or reloading, metadata writes, `module.paths` mutation, and `require.extensions` are unsupported. Read-only access to `module.id`, `filename`, `path`, `paths`, `loaded`, `isPreloading`, `parent`, `children`, `require.main`, and `require.cache` lookup/enumeration compiles. - **ESM metadata:** Native `import.meta.url`, `filename`, `dirname`, and `main` support dot access, statically known keys, and destructuring selected fields. Storing or mutating the metadata object, rest bindings, runtime keys, and stored `resolve` callbacks are unsupported. Native `import.meta.resolve()` requires a literal specifier. Module instances distinguished by URL query strings or fragments are unsupported. - **Date:** Date unions, year/month constructors, setters, statically typed TypeScript identity comparisons, thrown Date values, and locale/string formatters are unsupported. Zero-argument and single number/string constructors, storage and passing, `getTime`, `valueOf`, `toISOString`, calendar getters, `getTimezoneOffset`, and single-string `Date.parse` compile. JavaScript Dates retain identity through checked storage and support Date-copy construction and JSON serialization. See [Dates and timezones](#dates-and-timezones) for parsing restrictions. diff --git a/docs/src/generated/node-v24-compatibility-meta.json b/docs/src/generated/node-v24-compatibility-meta.json index cbd1fe11f..b0c9b4109 100644 --- a/docs/src/generated/node-v24-compatibility-meta.json +++ b/docs/src/generated/node-v24-compatibility-meta.json @@ -2,6 +2,6 @@ "schemaVersion": 3, "nodeVersion": "24.15.0", "nodeCommit": "848430679556aed0bd073f2bc263331ad84fa119", - "artifactVersion": "442e4b7745b74b802a16", + "artifactVersion": "5bc8e72822dd7065df84", "rowCount": 3662 } diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json index 5771110b4..67efad31f 100644 --- a/docs/src/generated/node-v24-compatibility.json +++ b/docs/src/generated/node-v24-compatibility.json @@ -599,9 +599,9 @@ "apiEntries": 43, "static": { "supported": 0, - "partial": 4, + "partial": 21, "refused": 0, - "not-implemented": 39, + "not-implemented": 22, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -644,9 +644,9 @@ "apiEntries": 137, "static": { "supported": 0, - "partial": 36, + "partial": 38, "refused": 0, - "not-implemented": 101, + "not-implemented": 99, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -55258,9 +55258,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" } }, { @@ -55281,9 +55281,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" } }, { @@ -55465,9 +55465,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" } }, { @@ -55511,9 +55511,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" } }, { @@ -55534,9 +55534,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" } }, { @@ -55764,9 +55764,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" } }, { @@ -55787,9 +55787,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" } }, { @@ -55810,9 +55810,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" } }, { @@ -55833,9 +55833,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" } }, { @@ -55856,9 +55856,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" } }, { @@ -55879,9 +55879,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" } }, { @@ -55902,9 +55902,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" } }, { @@ -55994,9 +55994,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" } }, { @@ -56017,9 +56017,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" } }, { @@ -56040,9 +56040,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" } }, { @@ -56086,9 +56086,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" } }, { @@ -56109,9 +56109,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" } }, { @@ -58110,9 +58110,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented static async-iteration subset.", + "verification": "test-backed" } }, { @@ -58593,9 +58593,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented static async-iteration subset.", + "verification": "test-backed" } }, { diff --git a/internal/compatibility/generated/node-v24-backlog.json b/internal/compatibility/generated/node-v24-backlog.json index 65ca1b437..c745613fe 100644 --- a/internal/compatibility/generated/node-v24-backlog.json +++ b/internal/compatibility/generated/node-v24-backlog.json @@ -8,8 +8,8 @@ "tiers": { "static": { "replace-refusal": 40, - "verify-gap": 2296, - "audit-partial": 852, + "verify-gap": 2277, + "audit-partial": 871, "classify": 2, "implement": 80 } @@ -54988,12 +54988,17 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:readline", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:readline.readlinePromises.Interface", + "tests": [ + "tests/corpus/5295-readline-promises-question.ts", + "tests/corpus/5296-readline-lines.ts", + "tests/corpus/5298-readline-abort.cjs", + "tests/corpus/5299-readline-stdin.ts" + ] } } }, @@ -55013,12 +55018,17 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:readline", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:readline.rl.question", + "tests": [ + "tests/corpus/5295-readline-promises-question.ts", + "tests/corpus/5296-readline-lines.ts", + "tests/corpus/5298-readline-abort.cjs", + "tests/corpus/5299-readline-stdin.ts" + ] } } }, @@ -55213,12 +55223,17 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:readline", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:readline.readlinePromises.createInterface", + "tests": [ + "tests/corpus/5295-readline-promises-question.ts", + "tests/corpus/5296-readline-lines.ts", + "tests/corpus/5298-readline-abort.cjs", + "tests/corpus/5299-readline-stdin.ts" + ] } } }, @@ -55263,12 +55278,17 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:readline", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:readline.readline.Interface", + "tests": [ + "tests/corpus/5295-readline-promises-question.ts", + "tests/corpus/5296-readline-lines.ts", + "tests/corpus/5297-readline-callback-events.cjs", + "tests/corpus/5299-readline-stdin.ts" + ] } } }, @@ -55288,12 +55308,17 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:readline", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:readline.rl.question", + "tests": [ + "tests/corpus/5295-readline-promises-question.ts", + "tests/corpus/5296-readline-lines.ts", + "tests/corpus/5298-readline-abort.cjs", + "tests/corpus/5299-readline-stdin.ts" + ] } } }, @@ -55523,7 +55548,10 @@ "confidence": "high", "source": "compiler-feature:readline.rl", "tests": [ - "tests/corpus/2844-node-disposable-resources.ts" + "tests/corpus/5295-readline-promises-question.ts", + "tests/corpus/5296-readline-lines.ts", + "tests/corpus/5297-readline-callback-events.cjs", + "tests/corpus/5299-readline-stdin.ts" ] } } @@ -55544,12 +55572,17 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:readline", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:readline.rl.pause", + "tests": [ + "tests/corpus/5295-readline-promises-question.ts", + "tests/corpus/5296-readline-lines.ts", + "tests/corpus/5297-readline-callback-events.cjs", + "tests/corpus/5299-readline-stdin.ts" + ] } } }, @@ -55569,12 +55602,17 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:readline", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:readline.rl.prompt", + "tests": [ + "tests/corpus/5295-readline-promises-question.ts", + "tests/corpus/5296-readline-lines.ts", + "tests/corpus/5297-readline-callback-events.cjs", + "tests/corpus/5299-readline-stdin.ts" + ] } } }, @@ -55594,12 +55632,17 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:readline", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:readline.rl.resume", + "tests": [ + "tests/corpus/5295-readline-promises-question.ts", + "tests/corpus/5296-readline-lines.ts", + "tests/corpus/5297-readline-callback-events.cjs", + "tests/corpus/5299-readline-stdin.ts" + ] } } }, @@ -55619,12 +55662,17 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:readline", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:readline.rl.setPrompt", + "tests": [ + "tests/corpus/5295-readline-promises-question.ts", + "tests/corpus/5296-readline-lines.ts", + "tests/corpus/5297-readline-callback-events.cjs", + "tests/corpus/5299-readline-stdin.ts" + ] } } }, @@ -55644,12 +55692,17 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:readline", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:readline.rl.getPrompt", + "tests": [ + "tests/corpus/5295-readline-promises-question.ts", + "tests/corpus/5296-readline-lines.ts", + "tests/corpus/5297-readline-callback-events.cjs", + "tests/corpus/5299-readline-stdin.ts" + ] } } }, @@ -55669,12 +55722,17 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:readline", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:readline.rl.write", + "tests": [ + "tests/corpus/5295-readline-promises-question.ts", + "tests/corpus/5296-readline-lines.ts", + "tests/corpus/5297-readline-callback-events.cjs", + "tests/corpus/5299-readline-stdin.ts" + ] } } }, @@ -55694,12 +55752,17 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:readline", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:readline.rl", + "tests": [ + "tests/corpus/5295-readline-promises-question.ts", + "tests/corpus/5296-readline-lines.ts", + "tests/corpus/5297-readline-callback-events.cjs", + "tests/corpus/5299-readline-stdin.ts" + ] } } }, @@ -55794,12 +55857,17 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:readline", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:readline.'close'", + "tests": [ + "tests/corpus/5295-readline-promises-question.ts", + "tests/corpus/5296-readline-lines.ts", + "tests/corpus/5297-readline-callback-events.cjs", + "tests/corpus/5299-readline-stdin.ts" + ] } } }, @@ -55819,12 +55887,17 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:readline", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:readline.'error'", + "tests": [ + "tests/corpus/5295-readline-promises-question.ts", + "tests/corpus/5296-readline-lines.ts", + "tests/corpus/5297-readline-callback-events.cjs", + "tests/corpus/5299-readline-stdin.ts" + ] } } }, @@ -55844,12 +55917,17 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:readline", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:readline.'line'", + "tests": [ + "tests/corpus/5295-readline-promises-question.ts", + "tests/corpus/5296-readline-lines.ts", + "tests/corpus/5297-readline-callback-events.cjs", + "tests/corpus/5299-readline-stdin.ts" + ] } } }, @@ -55894,12 +55972,17 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:readline", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:readline.'pause'", + "tests": [ + "tests/corpus/5295-readline-promises-question.ts", + "tests/corpus/5296-readline-lines.ts", + "tests/corpus/5297-readline-callback-events.cjs", + "tests/corpus/5299-readline-stdin.ts" + ] } } }, @@ -55919,12 +56002,17 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:readline", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:readline.'resume'", + "tests": [ + "tests/corpus/5295-readline-promises-question.ts", + "tests/corpus/5296-readline-lines.ts", + "tests/corpus/5297-readline-callback-events.cjs", + "tests/corpus/5299-readline-stdin.ts" + ] } } }, @@ -57552,7 +57640,9 @@ "tests": [ "tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", - "tests/corpus/1814-stream-pipeline.cjs" + "tests/corpus/1814-stream-pipeline.cjs", + "tests/corpus/5302-stream-promises-options.ts", + "tests/corpus/5303-stream-finished-callback-opts.cjs" ] } } @@ -57581,7 +57671,9 @@ "tests": [ "tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", - "tests/corpus/1814-stream-pipeline.cjs" + "tests/corpus/1814-stream-pipeline.cjs", + "tests/corpus/5302-stream-promises-options.ts", + "tests/corpus/5303-stream-finished-callback-opts.cjs" ] } } @@ -57610,7 +57702,9 @@ "tests": [ "tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", - "tests/corpus/1814-stream-pipeline.cjs" + "tests/corpus/1814-stream-pipeline.cjs", + "tests/corpus/5302-stream-promises-options.ts", + "tests/corpus/5303-stream-finished-callback-opts.cjs" ] } } @@ -57639,7 +57733,9 @@ "tests": [ "tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", - "tests/corpus/1814-stream-pipeline.cjs" + "tests/corpus/1814-stream-pipeline.cjs", + "tests/corpus/5302-stream-promises-options.ts", + "tests/corpus/5303-stream-finished-callback-opts.cjs" ] } } @@ -57668,7 +57764,9 @@ "tests": [ "tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", - "tests/corpus/1814-stream-pipeline.cjs" + "tests/corpus/1814-stream-pipeline.cjs", + "tests/corpus/5302-stream-promises-options.ts", + "tests/corpus/5303-stream-finished-callback-opts.cjs" ] } } @@ -57697,7 +57795,9 @@ "tests": [ "tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", - "tests/corpus/1814-stream-pipeline.cjs" + "tests/corpus/1814-stream-pipeline.cjs", + "tests/corpus/5302-stream-promises-options.ts", + "tests/corpus/5303-stream-finished-callback-opts.cjs" ] } } @@ -58043,12 +58143,15 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:stream", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:stream.stream.addAbortSignal", + "tests": [ + "tests/corpus/5300-stream-add-abort-signal.ts", + "tests/corpus/5301-stream-add-abort-signal-errors.cjs" + ] } } }, @@ -58076,7 +58179,9 @@ "tests": [ "tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", - "tests/corpus/1814-stream-pipeline.cjs" + "tests/corpus/1814-stream-pipeline.cjs", + "tests/corpus/5302-stream-promises-options.ts", + "tests/corpus/5303-stream-finished-callback-opts.cjs" ] } } @@ -58130,7 +58235,9 @@ "tests": [ "tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", - "tests/corpus/1814-stream-pipeline.cjs" + "tests/corpus/1814-stream-pipeline.cjs", + "tests/corpus/5302-stream-promises-options.ts", + "tests/corpus/5303-stream-finished-callback-opts.cjs" ] } } @@ -58159,7 +58266,9 @@ "tests": [ "tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", - "tests/corpus/1814-stream-pipeline.cjs" + "tests/corpus/1814-stream-pipeline.cjs", + "tests/corpus/5302-stream-promises-options.ts", + "tests/corpus/5303-stream-finished-callback-opts.cjs" ] } } @@ -58188,7 +58297,9 @@ "tests": [ "tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", - "tests/corpus/1814-stream-pipeline.cjs" + "tests/corpus/1814-stream-pipeline.cjs", + "tests/corpus/5302-stream-promises-options.ts", + "tests/corpus/5303-stream-finished-callback-opts.cjs" ] } } @@ -58217,7 +58328,9 @@ "tests": [ "tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", - "tests/corpus/1814-stream-pipeline.cjs" + "tests/corpus/1814-stream-pipeline.cjs", + "tests/corpus/5302-stream-promises-options.ts", + "tests/corpus/5303-stream-finished-callback-opts.cjs" ] } } @@ -58246,7 +58359,9 @@ "tests": [ "tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", - "tests/corpus/1814-stream-pipeline.cjs" + "tests/corpus/1814-stream-pipeline.cjs", + "tests/corpus/5302-stream-promises-options.ts", + "tests/corpus/5303-stream-finished-callback-opts.cjs" ] } } @@ -58592,12 +58707,15 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:stream", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:stream.stream.addAbortSignal", + "tests": [ + "tests/corpus/5300-stream-add-abort-signal.ts", + "tests/corpus/5301-stream-add-abort-signal-errors.cjs" + ] } } }, @@ -58625,7 +58743,9 @@ "tests": [ "tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", - "tests/corpus/1814-stream-pipeline.cjs" + "tests/corpus/1814-stream-pipeline.cjs", + "tests/corpus/5302-stream-promises-options.ts", + "tests/corpus/5303-stream-finished-callback-opts.cjs" ] } } @@ -58704,7 +58824,9 @@ "tests": [ "tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", - "tests/corpus/1814-stream-pipeline.cjs" + "tests/corpus/1814-stream-pipeline.cjs", + "tests/corpus/5302-stream-promises-options.ts", + "tests/corpus/5303-stream-finished-callback-opts.cjs" ] } } @@ -58760,7 +58882,9 @@ "tests": [ "tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", - "tests/corpus/1814-stream-pipeline.cjs" + "tests/corpus/1814-stream-pipeline.cjs", + "tests/corpus/5302-stream-promises-options.ts", + "tests/corpus/5303-stream-finished-callback-opts.cjs" ] } } @@ -58789,7 +58913,9 @@ "tests": [ "tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", - "tests/corpus/1814-stream-pipeline.cjs" + "tests/corpus/1814-stream-pipeline.cjs", + "tests/corpus/5302-stream-promises-options.ts", + "tests/corpus/5303-stream-finished-callback-opts.cjs" ] } } @@ -58818,7 +58944,9 @@ "tests": [ "tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", - "tests/corpus/1814-stream-pipeline.cjs" + "tests/corpus/1814-stream-pipeline.cjs", + "tests/corpus/5302-stream-promises-options.ts", + "tests/corpus/5303-stream-finished-callback-opts.cjs" ] } } @@ -58872,7 +59000,9 @@ "tests": [ "tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", - "tests/corpus/1814-stream-pipeline.cjs" + "tests/corpus/1814-stream-pipeline.cjs", + "tests/corpus/5302-stream-promises-options.ts", + "tests/corpus/5303-stream-finished-callback-opts.cjs" ] } } @@ -59381,7 +59511,9 @@ "tests": [ "tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", - "tests/corpus/1814-stream-pipeline.cjs" + "tests/corpus/1814-stream-pipeline.cjs", + "tests/corpus/5302-stream-promises-options.ts", + "tests/corpus/5303-stream-finished-callback-opts.cjs" ] } } @@ -59410,7 +59542,9 @@ "tests": [ "tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", - "tests/corpus/1814-stream-pipeline.cjs" + "tests/corpus/1814-stream-pipeline.cjs", + "tests/corpus/5302-stream-promises-options.ts", + "tests/corpus/5303-stream-finished-callback-opts.cjs" ] } } @@ -59439,7 +59573,9 @@ "tests": [ "tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", - "tests/corpus/1814-stream-pipeline.cjs" + "tests/corpus/1814-stream-pipeline.cjs", + "tests/corpus/5302-stream-promises-options.ts", + "tests/corpus/5303-stream-finished-callback-opts.cjs" ] } } @@ -59468,7 +59604,9 @@ "tests": [ "tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", - "tests/corpus/1814-stream-pipeline.cjs" + "tests/corpus/1814-stream-pipeline.cjs", + "tests/corpus/5302-stream-promises-options.ts", + "tests/corpus/5303-stream-finished-callback-opts.cjs" ] } } @@ -59497,7 +59635,9 @@ "tests": [ "tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", - "tests/corpus/1814-stream-pipeline.cjs" + "tests/corpus/1814-stream-pipeline.cjs", + "tests/corpus/5302-stream-promises-options.ts", + "tests/corpus/5303-stream-finished-callback-opts.cjs" ] } } @@ -59526,7 +59666,9 @@ "tests": [ "tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", - "tests/corpus/1814-stream-pipeline.cjs" + "tests/corpus/1814-stream-pipeline.cjs", + "tests/corpus/5302-stream-promises-options.ts", + "tests/corpus/5303-stream-finished-callback-opts.cjs" ] } } @@ -59555,7 +59697,9 @@ "tests": [ "tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", - "tests/corpus/1814-stream-pipeline.cjs" + "tests/corpus/1814-stream-pipeline.cjs", + "tests/corpus/5302-stream-promises-options.ts", + "tests/corpus/5303-stream-finished-callback-opts.cjs" ] } } @@ -59584,7 +59728,9 @@ "tests": [ "tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", - "tests/corpus/1814-stream-pipeline.cjs" + "tests/corpus/1814-stream-pipeline.cjs", + "tests/corpus/5302-stream-promises-options.ts", + "tests/corpus/5303-stream-finished-callback-opts.cjs" ] } } @@ -59613,7 +59759,9 @@ "tests": [ "tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", - "tests/corpus/1814-stream-pipeline.cjs" + "tests/corpus/1814-stream-pipeline.cjs", + "tests/corpus/5302-stream-promises-options.ts", + "tests/corpus/5303-stream-finished-callback-opts.cjs" ] } } @@ -60848,7 +60996,9 @@ "tests": [ "tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", - "tests/corpus/1814-stream-pipeline.cjs" + "tests/corpus/1814-stream-pipeline.cjs", + "tests/corpus/5302-stream-promises-options.ts", + "tests/corpus/5303-stream-finished-callback-opts.cjs" ] } } @@ -61002,7 +61152,9 @@ "tests": [ "tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", - "tests/corpus/1814-stream-pipeline.cjs" + "tests/corpus/1814-stream-pipeline.cjs", + "tests/corpus/5302-stream-promises-options.ts", + "tests/corpus/5303-stream-finished-callback-opts.cjs" ] } } @@ -61031,7 +61183,9 @@ "tests": [ "tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", - "tests/corpus/1814-stream-pipeline.cjs" + "tests/corpus/1814-stream-pipeline.cjs", + "tests/corpus/5302-stream-promises-options.ts", + "tests/corpus/5303-stream-finished-callback-opts.cjs" ] } } diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index b8d601a9c..64ab183f7 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -626,9 +626,9 @@ "apiEntries": 43, "static": { "supported": 0, - "partial": 4, + "partial": 21, "refused": 0, - "not-implemented": 39, + "not-implemented": 22, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -701,9 +701,9 @@ "apiEntries": 137, "static": { "supported": 0, - "partial": 36, + "partial": 38, "refused": 0, - "not-implemented": 101, + "not-implemented": 99, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -75896,7 +75896,11 @@ "evidence": "derived:descendants", "tests": [ "tests/corpus/1475-readline-closed-stdin.ts", - "tests/corpus/2844-node-disposable-resources.ts" + "tests/corpus/5295-readline-promises-question.ts", + "tests/corpus/5296-readline-lines.ts", + "tests/corpus/5297-readline-callback-events.cjs", + "tests/corpus/5298-readline-abort.cjs", + "tests/corpus/5299-readline-stdin.ts" ] }, "anchorSource": "chapter" @@ -75942,8 +75946,14 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:readline" + "status": "partial", + "evidence": "compiler-feature:readline.readlinePromises.Interface", + "tests": [ + "tests/corpus/5295-readline-promises-question.ts", + "tests/corpus/5296-readline-lines.ts", + "tests/corpus/5298-readline-abort.cjs", + "tests/corpus/5299-readline-stdin.ts" + ] }, "anchorSource": "exact" }, @@ -75965,8 +75975,14 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:readline" + "status": "partial", + "evidence": "compiler-feature:readline.rl.question", + "tests": [ + "tests/corpus/5295-readline-promises-question.ts", + "tests/corpus/5296-readline-lines.ts", + "tests/corpus/5298-readline-abort.cjs", + "tests/corpus/5299-readline-stdin.ts" + ] }, "anchorSource": "exact" }, @@ -76149,8 +76165,14 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:readline" + "status": "partial", + "evidence": "compiler-feature:readline.readlinePromises.createInterface", + "tests": [ + "tests/corpus/5295-readline-promises-question.ts", + "tests/corpus/5296-readline-lines.ts", + "tests/corpus/5298-readline-abort.cjs", + "tests/corpus/5299-readline-stdin.ts" + ] }, "anchorSource": "exact" }, @@ -76218,8 +76240,14 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:readline" + "status": "partial", + "evidence": "compiler-feature:readline.readline.Interface", + "tests": [ + "tests/corpus/5295-readline-promises-question.ts", + "tests/corpus/5296-readline-lines.ts", + "tests/corpus/5297-readline-callback-events.cjs", + "tests/corpus/5299-readline-stdin.ts" + ] }, "anchorSource": "exact" }, @@ -76241,8 +76269,14 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:readline" + "status": "partial", + "evidence": "compiler-feature:readline.rl.question", + "tests": [ + "tests/corpus/5295-readline-promises-question.ts", + "tests/corpus/5296-readline-lines.ts", + "tests/corpus/5298-readline-abort.cjs", + "tests/corpus/5299-readline-stdin.ts" + ] }, "anchorSource": "exact" }, @@ -76480,7 +76514,10 @@ "status": "partial", "evidence": "compiler-feature:readline.rl", "tests": [ - "tests/corpus/2844-node-disposable-resources.ts" + "tests/corpus/5295-readline-promises-question.ts", + "tests/corpus/5296-readline-lines.ts", + "tests/corpus/5297-readline-callback-events.cjs", + "tests/corpus/5299-readline-stdin.ts" ] }, "anchorSource": "exact" @@ -76503,8 +76540,14 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:readline" + "status": "partial", + "evidence": "compiler-feature:readline.rl.pause", + "tests": [ + "tests/corpus/5295-readline-promises-question.ts", + "tests/corpus/5296-readline-lines.ts", + "tests/corpus/5297-readline-callback-events.cjs", + "tests/corpus/5299-readline-stdin.ts" + ] }, "anchorSource": "exact" }, @@ -76526,8 +76569,14 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:readline" + "status": "partial", + "evidence": "compiler-feature:readline.rl.prompt", + "tests": [ + "tests/corpus/5295-readline-promises-question.ts", + "tests/corpus/5296-readline-lines.ts", + "tests/corpus/5297-readline-callback-events.cjs", + "tests/corpus/5299-readline-stdin.ts" + ] }, "anchorSource": "exact" }, @@ -76549,8 +76598,14 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:readline" + "status": "partial", + "evidence": "compiler-feature:readline.rl.resume", + "tests": [ + "tests/corpus/5295-readline-promises-question.ts", + "tests/corpus/5296-readline-lines.ts", + "tests/corpus/5297-readline-callback-events.cjs", + "tests/corpus/5299-readline-stdin.ts" + ] }, "anchorSource": "exact" }, @@ -76572,8 +76627,14 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:readline" + "status": "partial", + "evidence": "compiler-feature:readline.rl.setPrompt", + "tests": [ + "tests/corpus/5295-readline-promises-question.ts", + "tests/corpus/5296-readline-lines.ts", + "tests/corpus/5297-readline-callback-events.cjs", + "tests/corpus/5299-readline-stdin.ts" + ] }, "anchorSource": "exact" }, @@ -76595,8 +76656,14 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:readline" + "status": "partial", + "evidence": "compiler-feature:readline.rl.getPrompt", + "tests": [ + "tests/corpus/5295-readline-promises-question.ts", + "tests/corpus/5296-readline-lines.ts", + "tests/corpus/5297-readline-callback-events.cjs", + "tests/corpus/5299-readline-stdin.ts" + ] }, "anchorSource": "exact" }, @@ -76618,8 +76685,14 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:readline" + "status": "partial", + "evidence": "compiler-feature:readline.rl.write", + "tests": [ + "tests/corpus/5295-readline-promises-question.ts", + "tests/corpus/5296-readline-lines.ts", + "tests/corpus/5297-readline-callback-events.cjs", + "tests/corpus/5299-readline-stdin.ts" + ] }, "anchorSource": "exact" }, @@ -76641,8 +76714,14 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:readline" + "status": "partial", + "evidence": "compiler-feature:readline.rl", + "tests": [ + "tests/corpus/5295-readline-promises-question.ts", + "tests/corpus/5296-readline-lines.ts", + "tests/corpus/5297-readline-callback-events.cjs", + "tests/corpus/5299-readline-stdin.ts" + ] }, "anchorSource": "exact" }, @@ -76733,8 +76812,14 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:readline" + "status": "partial", + "evidence": "compiler-feature:readline.'close'", + "tests": [ + "tests/corpus/5295-readline-promises-question.ts", + "tests/corpus/5296-readline-lines.ts", + "tests/corpus/5297-readline-callback-events.cjs", + "tests/corpus/5299-readline-stdin.ts" + ] }, "anchorSource": "exact" }, @@ -76756,8 +76841,14 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:readline" + "status": "partial", + "evidence": "compiler-feature:readline.'error'", + "tests": [ + "tests/corpus/5295-readline-promises-question.ts", + "tests/corpus/5296-readline-lines.ts", + "tests/corpus/5297-readline-callback-events.cjs", + "tests/corpus/5299-readline-stdin.ts" + ] }, "anchorSource": "exact" }, @@ -76779,8 +76870,14 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:readline" + "status": "partial", + "evidence": "compiler-feature:readline.'line'", + "tests": [ + "tests/corpus/5295-readline-promises-question.ts", + "tests/corpus/5296-readline-lines.ts", + "tests/corpus/5297-readline-callback-events.cjs", + "tests/corpus/5299-readline-stdin.ts" + ] }, "anchorSource": "exact" }, @@ -76825,8 +76922,14 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:readline" + "status": "partial", + "evidence": "compiler-feature:readline.'pause'", + "tests": [ + "tests/corpus/5295-readline-promises-question.ts", + "tests/corpus/5296-readline-lines.ts", + "tests/corpus/5297-readline-callback-events.cjs", + "tests/corpus/5299-readline-stdin.ts" + ] }, "anchorSource": "exact" }, @@ -76848,8 +76951,14 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:readline" + "status": "partial", + "evidence": "compiler-feature:readline.'resume'", + "tests": [ + "tests/corpus/5295-readline-promises-question.ts", + "tests/corpus/5296-readline-lines.ts", + "tests/corpus/5297-readline-callback-events.cjs", + "tests/corpus/5299-readline-stdin.ts" + ] }, "anchorSource": "exact" }, @@ -79642,7 +79751,11 @@ "tests/corpus/2893-child-stdin-duplex.ts", "tests/corpus/2894-child-stdin-backpressure.ts", "tests/corpus/2895-child-stdin-write-after-end.ts", - "tests/corpus/2897-child-stdin-destroy.ts" + "tests/corpus/2897-child-stdin-destroy.ts", + "tests/corpus/5300-stream-add-abort-signal.ts", + "tests/corpus/5301-stream-add-abort-signal-errors.cjs", + "tests/corpus/5302-stream-promises-options.ts", + "tests/corpus/5303-stream-finished-callback-opts.cjs" ] }, "anchorSource": "chapter" @@ -79762,7 +79875,9 @@ "tests": [ "tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", - "tests/corpus/1814-stream-pipeline.cjs" + "tests/corpus/1814-stream-pipeline.cjs", + "tests/corpus/5302-stream-promises-options.ts", + "tests/corpus/5303-stream-finished-callback-opts.cjs" ] }, "anchorSource": "exact" @@ -79790,7 +79905,9 @@ "tests": [ "tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", - "tests/corpus/1814-stream-pipeline.cjs" + "tests/corpus/1814-stream-pipeline.cjs", + "tests/corpus/5302-stream-promises-options.ts", + "tests/corpus/5303-stream-finished-callback-opts.cjs" ] }, "anchorSource": "exact" @@ -79818,7 +79935,9 @@ "tests": [ "tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", - "tests/corpus/1814-stream-pipeline.cjs" + "tests/corpus/1814-stream-pipeline.cjs", + "tests/corpus/5302-stream-promises-options.ts", + "tests/corpus/5303-stream-finished-callback-opts.cjs" ] }, "anchorSource": "exact" @@ -79869,7 +79988,9 @@ "tests": [ "tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", - "tests/corpus/1814-stream-pipeline.cjs" + "tests/corpus/1814-stream-pipeline.cjs", + "tests/corpus/5302-stream-promises-options.ts", + "tests/corpus/5303-stream-finished-callback-opts.cjs" ] }, "anchorSource": "exact" @@ -79897,7 +80018,9 @@ "tests": [ "tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", - "tests/corpus/1814-stream-pipeline.cjs" + "tests/corpus/1814-stream-pipeline.cjs", + "tests/corpus/5302-stream-promises-options.ts", + "tests/corpus/5303-stream-finished-callback-opts.cjs" ] }, "anchorSource": "exact" @@ -79925,7 +80048,9 @@ "tests": [ "tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", - "tests/corpus/1814-stream-pipeline.cjs" + "tests/corpus/1814-stream-pipeline.cjs", + "tests/corpus/5302-stream-promises-options.ts", + "tests/corpus/5303-stream-finished-callback-opts.cjs" ] }, "anchorSource": "exact" @@ -80247,8 +80372,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:stream" + "status": "partial", + "evidence": "compiler-feature:stream.stream.addAbortSignal", + "tests": [ + "tests/corpus/5300-stream-add-abort-signal.ts", + "tests/corpus/5301-stream-add-abort-signal-errors.cjs" + ] }, "anchorSource": "exact" }, @@ -80275,7 +80404,9 @@ "tests": [ "tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", - "tests/corpus/1814-stream-pipeline.cjs" + "tests/corpus/1814-stream-pipeline.cjs", + "tests/corpus/5302-stream-promises-options.ts", + "tests/corpus/5303-stream-finished-callback-opts.cjs" ] }, "anchorSource": "exact" @@ -80326,7 +80457,9 @@ "tests": [ "tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", - "tests/corpus/1814-stream-pipeline.cjs" + "tests/corpus/1814-stream-pipeline.cjs", + "tests/corpus/5302-stream-promises-options.ts", + "tests/corpus/5303-stream-finished-callback-opts.cjs" ] }, "anchorSource": "exact" @@ -80354,7 +80487,9 @@ "tests": [ "tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", - "tests/corpus/1814-stream-pipeline.cjs" + "tests/corpus/1814-stream-pipeline.cjs", + "tests/corpus/5302-stream-promises-options.ts", + "tests/corpus/5303-stream-finished-callback-opts.cjs" ] }, "anchorSource": "exact" @@ -80405,7 +80540,9 @@ "tests": [ "tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", - "tests/corpus/1814-stream-pipeline.cjs" + "tests/corpus/1814-stream-pipeline.cjs", + "tests/corpus/5302-stream-promises-options.ts", + "tests/corpus/5303-stream-finished-callback-opts.cjs" ] }, "anchorSource": "ancestor" @@ -80433,7 +80570,9 @@ "tests": [ "tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", - "tests/corpus/1814-stream-pipeline.cjs" + "tests/corpus/1814-stream-pipeline.cjs", + "tests/corpus/5302-stream-promises-options.ts", + "tests/corpus/5303-stream-finished-callback-opts.cjs" ] }, "anchorSource": "ancestor" @@ -80461,7 +80600,9 @@ "tests": [ "tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", - "tests/corpus/1814-stream-pipeline.cjs" + "tests/corpus/1814-stream-pipeline.cjs", + "tests/corpus/5302-stream-promises-options.ts", + "tests/corpus/5303-stream-finished-callback-opts.cjs" ] }, "anchorSource": "ancestor" @@ -80783,8 +80924,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:stream" + "status": "partial", + "evidence": "compiler-feature:stream.stream.addAbortSignal", + "tests": [ + "tests/corpus/5300-stream-add-abort-signal.ts", + "tests/corpus/5301-stream-add-abort-signal-errors.cjs" + ] }, "anchorSource": "ancestor" }, @@ -80811,7 +80956,9 @@ "tests": [ "tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", - "tests/corpus/1814-stream-pipeline.cjs" + "tests/corpus/1814-stream-pipeline.cjs", + "tests/corpus/5302-stream-promises-options.ts", + "tests/corpus/5303-stream-finished-callback-opts.cjs" ] }, "anchorSource": "ancestor" @@ -80908,7 +81055,9 @@ "tests": [ "tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", - "tests/corpus/1814-stream-pipeline.cjs" + "tests/corpus/1814-stream-pipeline.cjs", + "tests/corpus/5302-stream-promises-options.ts", + "tests/corpus/5303-stream-finished-callback-opts.cjs" ] }, "anchorSource": "exact" @@ -80962,7 +81111,9 @@ "tests": [ "tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", - "tests/corpus/1814-stream-pipeline.cjs" + "tests/corpus/1814-stream-pipeline.cjs", + "tests/corpus/5302-stream-promises-options.ts", + "tests/corpus/5303-stream-finished-callback-opts.cjs" ] }, "anchorSource": "exact" @@ -80990,7 +81141,9 @@ "tests": [ "tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", - "tests/corpus/1814-stream-pipeline.cjs" + "tests/corpus/1814-stream-pipeline.cjs", + "tests/corpus/5302-stream-promises-options.ts", + "tests/corpus/5303-stream-finished-callback-opts.cjs" ] }, "anchorSource": "exact" @@ -81018,7 +81171,9 @@ "tests": [ "tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", - "tests/corpus/1814-stream-pipeline.cjs" + "tests/corpus/1814-stream-pipeline.cjs", + "tests/corpus/5302-stream-promises-options.ts", + "tests/corpus/5303-stream-finished-callback-opts.cjs" ] }, "anchorSource": "exact" @@ -81069,7 +81224,9 @@ "tests": [ "tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", - "tests/corpus/1814-stream-pipeline.cjs" + "tests/corpus/1814-stream-pipeline.cjs", + "tests/corpus/5302-stream-promises-options.ts", + "tests/corpus/5303-stream-finished-callback-opts.cjs" ] }, "anchorSource": "exact" @@ -81633,7 +81790,9 @@ "tests": [ "tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", - "tests/corpus/1814-stream-pipeline.cjs" + "tests/corpus/1814-stream-pipeline.cjs", + "tests/corpus/5302-stream-promises-options.ts", + "tests/corpus/5303-stream-finished-callback-opts.cjs" ] }, "anchorSource": "exact" @@ -81661,7 +81820,9 @@ "tests": [ "tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", - "tests/corpus/1814-stream-pipeline.cjs" + "tests/corpus/1814-stream-pipeline.cjs", + "tests/corpus/5302-stream-promises-options.ts", + "tests/corpus/5303-stream-finished-callback-opts.cjs" ] }, "anchorSource": "exact" @@ -81689,7 +81850,9 @@ "tests": [ "tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", - "tests/corpus/1814-stream-pipeline.cjs" + "tests/corpus/1814-stream-pipeline.cjs", + "tests/corpus/5302-stream-promises-options.ts", + "tests/corpus/5303-stream-finished-callback-opts.cjs" ] }, "anchorSource": "exact" @@ -81717,7 +81880,9 @@ "tests": [ "tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", - "tests/corpus/1814-stream-pipeline.cjs" + "tests/corpus/1814-stream-pipeline.cjs", + "tests/corpus/5302-stream-promises-options.ts", + "tests/corpus/5303-stream-finished-callback-opts.cjs" ] }, "anchorSource": "exact" @@ -81745,7 +81910,9 @@ "tests": [ "tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", - "tests/corpus/1814-stream-pipeline.cjs" + "tests/corpus/1814-stream-pipeline.cjs", + "tests/corpus/5302-stream-promises-options.ts", + "tests/corpus/5303-stream-finished-callback-opts.cjs" ] }, "anchorSource": "exact" @@ -81773,7 +81940,9 @@ "tests": [ "tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", - "tests/corpus/1814-stream-pipeline.cjs" + "tests/corpus/1814-stream-pipeline.cjs", + "tests/corpus/5302-stream-promises-options.ts", + "tests/corpus/5303-stream-finished-callback-opts.cjs" ] }, "anchorSource": "exact" @@ -81801,7 +81970,9 @@ "tests": [ "tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", - "tests/corpus/1814-stream-pipeline.cjs" + "tests/corpus/1814-stream-pipeline.cjs", + "tests/corpus/5302-stream-promises-options.ts", + "tests/corpus/5303-stream-finished-callback-opts.cjs" ] }, "anchorSource": "exact" @@ -81829,7 +82000,9 @@ "tests": [ "tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", - "tests/corpus/1814-stream-pipeline.cjs" + "tests/corpus/1814-stream-pipeline.cjs", + "tests/corpus/5302-stream-promises-options.ts", + "tests/corpus/5303-stream-finished-callback-opts.cjs" ] }, "anchorSource": "exact" @@ -81857,7 +82030,9 @@ "tests": [ "tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", - "tests/corpus/1814-stream-pipeline.cjs" + "tests/corpus/1814-stream-pipeline.cjs", + "tests/corpus/5302-stream-promises-options.ts", + "tests/corpus/5303-stream-finished-callback-opts.cjs" ] }, "anchorSource": "exact" @@ -83204,7 +83379,9 @@ "tests": [ "tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", - "tests/corpus/1814-stream-pipeline.cjs" + "tests/corpus/1814-stream-pipeline.cjs", + "tests/corpus/5302-stream-promises-options.ts", + "tests/corpus/5303-stream-finished-callback-opts.cjs" ] }, "anchorSource": "exact" @@ -83462,7 +83639,9 @@ "tests": [ "tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", - "tests/corpus/1814-stream-pipeline.cjs" + "tests/corpus/1814-stream-pipeline.cjs", + "tests/corpus/5302-stream-promises-options.ts", + "tests/corpus/5303-stream-finished-callback-opts.cjs" ] }, "anchorSource": "ancestor" @@ -83490,7 +83669,9 @@ "tests": [ "tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", - "tests/corpus/1814-stream-pipeline.cjs" + "tests/corpus/1814-stream-pipeline.cjs", + "tests/corpus/5302-stream-promises-options.ts", + "tests/corpus/5303-stream-finished-callback-opts.cjs" ] }, "anchorSource": "ancestor" diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index d4a4ae2de..6776b362a 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -170,6 +170,9 @@ {"chapter": "events", "symbols": ["CustomEvent", "detail"], "status": "partial", "evidence": ["tests/corpus/5130-event-target-dispatch.js", "tests/corpus/5132-event-target-typed.ts"]}, {"chapter": "events", "symbols": ["events.errorMonitor"], "status": "partial", "evidence": ["tests/corpus/5134-events-error-monitor.ts", "tests/corpus/5136-events-error-monitor-string-name.ts", "tests/corpus/5137-events-error-monitor-computed-name.js", "tests/corpus/5138-events-error-monitor-shared-listener.ts"]}, {"chapter": "events", "symbols": ["events.addAbortListener"], "status": "partial", "evidence": ["tests/corpus/5133-abort-signal-event-target.ts"]}, + {"chapter": "readline", "symbols": ["readlinePromises.Interface", "readlinePromises.createInterface", "rl.question"], "status": "partial", "evidence": ["tests/corpus/5295-readline-promises-question.ts", "tests/corpus/5296-readline-lines.ts", "tests/corpus/5298-readline-abort.cjs", "tests/corpus/5299-readline-stdin.ts"]}, + {"chapter": "readline", "symbols": ["readline.Interface", "rl.pause", "rl.resume", "rl.prompt", "rl.setPrompt", "rl.getPrompt", "rl.write", "rl", "'close'", "'error'", "'line'", "'pause'", "'resume'"], "status": "partial", "evidence": ["tests/corpus/5295-readline-promises-question.ts", "tests/corpus/5296-readline-lines.ts", "tests/corpus/5297-readline-callback-events.cjs", "tests/corpus/5299-readline-stdin.ts"]}, + {"chapter": "stream", "symbols": ["stream.addAbortSignal"], "status": "partial", "evidence": ["tests/corpus/5300-stream-add-abort-signal.ts", "tests/corpus/5301-stream-add-abort-signal-errors.cjs"]}, {"chapter": "worker_threads", "symbols": ["Worker"], "status": "partial", "evidence": ["tests/corpus/native-worker-messages.ts", "tests/corpus/native-worker-commonjs.cjs", "tests/corpus/native-worker-environment.ts", "tests/corpus/native-worker-roots/main.ts"]}, {"chapter": "worker_threads", "symbols": ["MessagePort", "parentPort", "worker_threads.workerData", "isMainThread", "threadId"], "status": "partial", "evidence": ["tests/corpus/native-worker-commonjs.cjs", "tests/corpus/native-worker-port-lifetime.ts", "tests/corpus/native-worker-references.ts"]}, {"chapter": "worker_threads", "symbols": ["worker.postMessage", "port.postMessage"], "status": "partial", "evidence": ["tests/corpus/native-worker-message-graphs.ts", "tests/corpus/native-worker-shared-views.ts", "tests/corpus/native-worker-clone-errors.ts", "tests/corpus/native-worker-delayed-messages.ts"]}, @@ -314,7 +317,7 @@ { "symbols": ["AsyncLocalStorage", "asyncLocalStorage.run", "asyncLocalStorage.exit", "asyncLocalStorage.getStore", "asyncLocalStorage.enterWith", "asyncLocalStorage.disable"], "status": "partial", "evidence": ["tests/corpus/2213-async-local-storage.cjs"] }, { "symbols": ["StringDecoder", "stringDecoder.write", "stringDecoder.end"], "status": "partial", "evidence": ["tests/corpus/1662-string-decoder-encodings.ts", "tests/corpus/node24-string-decoder.ts"] }, { "symbols": ["setImmediate", "setInterval", "setTimeout", "clearImmediate", "clearInterval", "clearTimeout", "timersPromises.setTimeout", "timersPromises.setImmediate", "timersPromises.setInterval", "immediate.hasRef", "immediate.ref", "immediate.unref", "timeout.hasRef", "timeout.ref", "timeout.unref", "timeout.refresh"], "status": "partial", "evidence": ["tests/corpus/1804-timers-module.ts", "tests/corpus/1805-timer-callback-args.ts", "tests/corpus/2093-timers-promises.ts", "tests/corpus/2852-timers-promises-interval.ts"] }, - { "symbols": ["stream.finished", "stream.pipeline", "stream.getDefaultHighWaterMark", "readable.push", "readable.unshift", "readable.read", "readable.pause", "readable.resume", "readable.isPaused", "readable.setEncoding", "readable.pipe", "readable.unpipe", "readable.destroy", "writable.write", "writable.end", "writable.cork", "writable.uncork", "writable.setDefaultEncoding"], "status": "partial", "evidence": ["tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", "tests/corpus/1814-stream-pipeline.cjs"] }, + { "symbols": ["stream.finished", "stream.pipeline", "stream.getDefaultHighWaterMark", "readable.push", "readable.unshift", "readable.read", "readable.pause", "readable.resume", "readable.isPaused", "readable.setEncoding", "readable.pipe", "readable.unpipe", "readable.destroy", "writable.write", "writable.end", "writable.cork", "writable.uncork", "writable.setDefaultEncoding"], "status": "partial", "evidence": ["tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", "tests/corpus/1814-stream-pipeline.cjs", "tests/corpus/5302-stream-promises-options.ts", "tests/corpus/5303-stream-finished-callback-opts.cjs"] }, { "symbols": ["net.createServer", "net.connect", "net.createConnection", "net.getDefaultAutoSelectFamilyAttemptTimeout", "net.setDefaultAutoSelectFamilyAttemptTimeout", "server.address", "server.close", "server.listen", "socket.destroy", "socket.end", "socket.pause", "socket.resume", "socket.setEncoding", "socket.setTimeout", "socket.write"], "status": "partial", "evidence": ["tests/corpus/2500-net-autosel-timeout.ts", "tests/corpus/2596-net-arg-ladders.cjs"] }, { "symbols": ["dgram.createSocket", "socket.address", "socket.bind", "socket.close", "socket.connect", "socket.ref", "socket.remoteAddress", "socket.send", "socket.unref"], "status": "partial", "evidence": ["tests/corpus/2597-dgram-send-ladders.cjs"] }, { "symbols": ["readline.createInterface", "rl.close"], "status": "partial", "evidence": ["tests/corpus/1475-readline-closed-stdin.ts"] }, diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index 6316d37eb..06833c8ad 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -3033,29 +3033,82 @@ declare module "node:querystring" { * on("close", cb). An open interface keeps the process alive until * close()/EOF, Node's semantics; lines split on \n and \r\n. */ declare module "readline" { + /* The non-terminal Interface over process.stdin or a stream input + * (scr_readline.c). Listener registrations are statements: chaining + * on()/once() results has no lowering. */ export interface Interface extends Disposable { question(query: string, callback: (answer: string) => void): void; close(): void; - on(event: "close", listener: () => void): void; + pause(): this; + resume(): this; + prompt(preserveCursor?: boolean): void; + setPrompt(prompt: string): void; + getPrompt(): string; + write(data: string): void; + on(event: "line", listener: (input: string) => void): this; + on(event: "close" | "pause" | "resume", listener: () => void): this; + on(event: "error", listener: (err: Error) => void): this; + once(event: "line", listener: (input: string) => void): this; + once(event: "close" | "pause" | "resume", listener: () => void): this; + once(event: "error", listener: (err: Error) => void): this; + addListener(event: "line", listener: (input: string) => void): this; + addListener(event: "close" | "pause" | "resume", listener: () => void): this; + addListener(event: "error", listener: (err: Error) => void): this; + [Symbol.asyncIterator](): AsyncIterableIterator; [Symbol.dispose](): void; } - export function createInterface(options: { + export interface ReadLineOptions { input: unknown; output?: unknown; terminal?: boolean; - /* crlfDelay: Infinity is accepted — the lowered splitter already - * holds a trailing \r for the next chunk, Node's Infinity behavior; - * completer and the history knobs fence by name (no interactive - * terminal). The options-record stance (see http.RequestOptions) - * admits every key; the walk decides. */ + /* Node's crlfDelay: a \n arriving within this many ms (at least 100) + * after a line ended by \r is dropped. completer and the history + * knobs fence by name (no interactive terminal). The options-record + * stance (see http.RequestOptions) admits every key; the walk + * decides. */ crlfDelay?: number; completer?: unknown; + signal?: AbortSignal; [option: string]: unknown; - }): Interface; + } + export function createInterface(options: ReadLineOptions): Interface; + export function createInterface(input: unknown, output?: unknown): Interface; } declare module "node:readline" { export * from "readline"; } +declare module "readline/promises" { + import { ReadLineOptions } from "readline"; + /* The readline Interface with the promise question: it resolves with + * the next line, rejects with an AbortError when options.signal aborts, + * and with ERR_USE_AFTER_CLOSE after close(). */ + export interface Interface extends Disposable { + question(query: string, options?: { signal?: AbortSignal }): Promise; + close(): void; + pause(): this; + resume(): this; + prompt(preserveCursor?: boolean): void; + setPrompt(prompt: string): void; + getPrompt(): string; + write(data: string): void; + on(event: "line", listener: (input: string) => void): this; + on(event: "close" | "pause" | "resume", listener: () => void): this; + on(event: "error", listener: (err: Error) => void): this; + once(event: "line", listener: (input: string) => void): this; + once(event: "close" | "pause" | "resume", listener: () => void): this; + once(event: "error", listener: (err: Error) => void): this; + addListener(event: "line", listener: (input: string) => void): this; + addListener(event: "close" | "pause" | "resume", listener: () => void): this; + addListener(event: "error", listener: (err: Error) => void): this; + [Symbol.asyncIterator](): AsyncIterableIterator; + [Symbol.dispose](): void; + } + export function createInterface(options: ReadLineOptions): Interface; + export function createInterface(input: unknown, output?: unknown): Interface; +} +declare module "node:readline/promises" { + export * from "readline/promises"; +} /* node:zlib — the one-shot zlib/raw/gzip codecs lower for string/Buffer * inputs with Node's default options; libz links only into zlib-using @@ -4731,7 +4784,7 @@ declare module "stream" { emitClose?: boolean; captureRejections?: boolean; defaultEncoding?: string; - signal?: unknown; + signal?: AbortSignal; } interface WritableOptions { highWaterMark?: number; @@ -4751,7 +4804,7 @@ declare module "stream" { autoDestroy?: boolean; emitClose?: boolean; captureRejections?: boolean; - signal?: unknown; + signal?: AbortSignal; } interface DuplexOptions { highWaterMark?: number; @@ -4780,7 +4833,7 @@ declare module "stream" { writable?: boolean; defaultEncoding?: string; writev?(this: Duplex, chunks: unknown[], callback: (error?: Error | null) => void): void; - signal?: unknown; + signal?: AbortSignal; } interface TransformOptions { highWaterMark?: number; @@ -4816,7 +4869,7 @@ declare module "stream" { readable?: boolean; writable?: boolean; defaultEncoding?: string; - signal?: unknown; + signal?: AbortSignal; } class Readable extends EventEmitter { @@ -5060,15 +5113,34 @@ declare module "stream" { constructor(opts?: TransformOptions); } + /* addAbortSignal(signal, stream): the stream, destroyed with an + * AbortError (cause: signal.reason) when the signal aborts. */ + function addAbortSignal(signal: AbortSignal, stream: T): T; /* Declared so requires typecheck; each fences at its use site. */ function pipeline(...streams: unknown[]): unknown; + /* finished's options: readable/writable choose which ends count, + * error: false ignores 'error' events, signal aborts the wait. */ + interface FinishedOptions { + readable?: boolean; + writable?: boolean; + error?: boolean; + signal?: AbortSignal; + } function finished(stream: unknown, callback: (err?: Error | null) => void): () => void; + function finished( + stream: unknown, + options: FinishedOptions | null, + callback: (err?: Error | null) => void, + ): () => void; /* Folds to the TARGET platform's default at compile time (win32 16384, * else 65536 — Node's own state.js split); literal argument only. */ function getDefaultHighWaterMark(objectMode: boolean): number; namespace promises { function pipeline(...streams: unknown[]): Promise; - function finished(stream: unknown): Promise; + function finished( + stream: unknown, + options?: FinishedOptions & { cleanup?: boolean }, + ): Promise; } class Stream extends EventEmitter {} @@ -5079,12 +5151,19 @@ declare module "stream" { Transform, PassThrough, Stream, + addAbortSignal, pipeline, finished, getDefaultHighWaterMark, promises, }; - export type { ReadableOptions, WritableOptions, DuplexOptions, TransformOptions }; + export type { + ReadableOptions, + WritableOptions, + DuplexOptions, + TransformOptions, + FinishedOptions, + }; } declare module "node:stream" { import stream = require("stream"); @@ -5094,9 +5173,20 @@ declare module "stream/promises" { /* The promise forms of finished/pipeline: a void promise the stream's * terminal point settles (rejected with the error, or * ERR_STREAM_PREMATURE_CLOSE on an early close). Stream arguments are - * the lowered surface — iterables/generators/functions fence per site. */ + * the lowered surface — iterables/generators/functions fence per site. + * pipeline's trailing { signal, end } object and finished's options + * (readable, writable, error, cleanup, signal) are supported. */ function pipeline(...streams: unknown[]): Promise; - function finished(stream: unknown): Promise; + function finished( + stream: unknown, + options?: { + readable?: boolean; + writable?: boolean; + error?: boolean; + cleanup?: boolean; + signal?: AbortSignal; + } | null, + ): Promise; export { pipeline, finished }; } declare module "node:stream/promises" { diff --git a/packages/compiler/src/backend/llvm/lib-dispatch.ts b/packages/compiler/src/backend/llvm/lib-dispatch.ts index 635695118..b569dd5a6 100644 --- a/packages/compiler/src/backend/llvm/lib-dispatch.ts +++ b/packages/compiler/src/backend/llvm/lib-dispatch.ts @@ -36,13 +36,43 @@ export function emitAssertInspectLibCall(host: LlvmEmitterContext, e: LibCallExp export function emitIoLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue { const B = host.B; - if (e.fn === "rl.create") { + if (e.fn === "rl.create" || e.fn === "rl.createSignal") { // readline interface handles are runtime IDs (doubles); an open - // interface holds the loop. + // interface on stdin holds the loop. The mode literal says which + // optional arguments follow (lowerReadlineCreateCall): a stream input + // or output goes through scr_stream.c's bridge. host.usesTimers = true; - host.declare(`declare double @scr_rl_create()`); + const modeArg = e.args[0]!; + if (modeArg.kind !== "numLit") { + throw new InternalCompilerError("llvm emitter bug: rl.create mode is not a literal"); + } + const mode = modeArg.value; + let i = 1; + const input = mode & 1 ? host.emitExpr(e.args[i++]!).name : "null"; + const output = mode & 2 ? host.emitExpr(e.args[i++]!).name : "null"; + const crlf = host.emitExpr(e.args[i++]!).name; + const prompt = mode & 16 ? host.emitExpr(e.args[i++]!).name : "null"; + const signal = mode & 8 ? host.emitExpr(e.args[i++]!).name : "null"; + let add = "null"; + if (mode & 8) { + host.declare(`declare ptr @scr_events_add_abort_listener(ptr, ptr)`); + add = "@scr_events_add_abort_listener"; + } + const stdout = mode & 4 ? "true" : "false"; const t = B.tmp(); - B.line(`${t} = call double @scr_rl_create()`); + if (mode & 3) { + host.declare(`declare double @scr_stream_rl_open(ptr, ptr, i1, double, ptr, ptr, ptr)`); + B.line( + `${t} = call double @scr_stream_rl_open(ptr ${input}, ptr ${output}, i1 ${stdout}, double ${crlf}, ptr ${prompt}, ptr ${signal}, ptr ${add})`, + ); + } else { + host.declare(`declare double @scr_rl_open(ptr, ptr, ptr, i1, double, ptr, ptr, ptr)`); + B.line( + `${t} = call double @scr_rl_open(ptr null, ptr null, ptr null, i1 ${stdout}, double ${crlf}, ptr ${prompt}, ptr ${signal}, ptr ${add})`, + ); + } + // A signal that is not an AbortSignal throws ERR_INVALID_ARG_TYPE. + if (mode & 8) host.emitPendingCheck(); return { name: t, type: e.type }; } if (e.fn === "rl.question") { @@ -54,7 +84,12 @@ export function emitIoLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue throw new InternalCompilerError("llvm emitter bug: rl.question callback not a func"); const args = e.args.map((a) => host.emitExpr(a)); host.moveTemp(args[2]!); - const adapter = cbT.params.length === 0 ? "scr_rl_answer_thunk0" : "scr_rl_answer_thunk_str"; + const adapter = + cbT.params.length === 0 + ? "scr_rl_answer_thunk0" + : cbT.params[0]!.kind === "dyn" + ? "scr_rl_answer_thunk_dyn" + : "scr_rl_answer_thunk_str"; host.declare(`declare void @${adapter}(ptr, ptr)`); host.declare(`declare void @scr_rl_question(double, ptr, ptr, ptr)`); B.line( @@ -63,20 +98,53 @@ export function emitIoLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue host.emitPendingCheck(); return { name: "", type: e.type }; } - if (e.fn === "rl.close") { - const id = host.emitExpr(e.args[0]!); - host.declare(`declare void @scr_rl_close(double)`); - B.line(`call void @scr_rl_close(double ${id.name})`); - return { name: "", type: e.type }; - } - if (e.fn === "rl.onClose") { - // The close listener MOVES into the interface's registry. + if (e.fn === "rl.questionAsync" || e.fn === "rl.questionAsyncSignal") { + // The promise question never throws: closed interfaces and aborted + // signals reject the answered promise (+1). host.usesTimers = true; + const args = e.args.map((a) => host.emitExpr(a)); + let signal = "null"; + let add = "null"; + if (e.fn === "rl.questionAsyncSignal") { + signal = args[2]!.name; + host.declare(`declare ptr @scr_events_add_abort_listener(ptr, ptr)`); + add = "@scr_events_add_abort_listener"; + } + host.declare(`declare ptr @scr_rl_question_promise(double, ptr, ptr, ptr)`); + const t = B.tmp(); + B.line( + `${t} = call ptr @scr_rl_question_promise(double ${args[0]!.name}, ptr ${args[1]!.name}, ptr ${signal}, ptr ${add})`, + ); + return host.own({ name: t, type: e.type }); + } + if (e.fn === "rl.on") { + // The listener MOVES into the interface's registry; the adapter is + // picked by event and parameter count ('line': the text, 'error': the + // error, the rest none). + host.usesTimers = true; + const cbT = e.args[2]!.type; + const evArg = e.args[1]!; + if (cbT.kind !== "func" || evArg.kind !== "numLit") { + throw new InternalCompilerError("llvm emitter bug: rl.on operands"); + } const id = host.emitExpr(e.args[0]!); - const cb = host.emitExpr(e.args[1]!); + const ev = host.emitExpr(evArg); + const cb = host.emitExpr(e.args[2]!); + const once = host.emitExpr(e.args[3]!); host.moveTemp(cb); - host.declare(`declare void @scr_rl_on_close(double, ptr)`); - B.line(`call void @scr_rl_on_close(double ${id.name}, ptr ${cb.name})`); + let adapter = "null"; + if (evArg.value === 0 || evArg.value === 4) { + const param = cbT.params[0]; + const kind = + param === undefined ? "0" : param.kind === "dyn" ? "_dyn" : evArg.value === 0 ? "_str" : ""; + const name = evArg.value === 0 ? `scr_rl_answer_thunk${kind}` : `scr_rl_error_thunk${kind}`; + host.declare(`declare void @${name}(ptr, ptr)`); + adapter = `@${name}`; + } + host.declare(`declare void @scr_rl_on(double, double, ptr, ptr, i1)`); + B.line( + `call void @scr_rl_on(double ${id.name}, double ${ev.name}, ptr ${cb.name}, ptr ${adapter}, i1 ${once.name})`, + ); return { name: "", type: e.type }; } return host.emitGenericLibCall(e); @@ -315,7 +383,12 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue { if (USES_TIMERS_LIB_FNS.has(e.fn)) host.usesTimers = true; if (e.fn === "process.nextTick") return host.emitAsyncContextLibCall(e); - if (e.fn === "sp.pipeline") return host.emitStreamLibCall(e); + if ( + e.fn === "sp.pipeline" || + e.fn.startsWith("sp.pipelineOpts") || + e.fn.startsWith("sp.finishedOpts") + ) + return host.emitStreamLibCall(e); if (e.fn === "bigint.parse" && e.args[0]?.kind === "strLit") { // Literals (`123n`) and constant BigInt("...") texts are immortal // statics instead of a parse at every evaluation. diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 698b1eb1c..b36b8ebfd 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -853,6 +853,18 @@ export const LIB_FN_SYMS: Record = { "readable.resume": "scr_stream_resume", "readable.isPaused": "scr_stream_is_paused", "readable.setEncoding": "scr_stream_set_encoding", + // node:readline's plain method calls (scr_readline.c): borrowed + // operands; getPrompt answers +1, iterNext a +1 promise. + "rl.close": "scr_rl_close", + "rl.pause": "scr_rl_pause", + "rl.resume": "scr_rl_resume", + "rl.prompt": "scr_rl_prompt", + "rl.setPrompt": "scr_rl_set_prompt", + "rl.getPrompt": "scr_rl_get_prompt", + "rl.write": "scr_rl_write", + "rl.iterOpen": "scr_rl_iter_open", + "rl.iterNext": "scr_rl_iter_next", + "rl.iterReturn": "scr_rl_iter_return", "readable.nextChunk": "scr_stream_next_chunk", "readable.nextChunkDyn": "scr_stream_next_chunk_dyn", "readable.fromArr": "scr_stream_from_arr", @@ -1242,6 +1254,12 @@ export const USES_TIMERS_LIB_FNS = new Set([ "stream.pipelineDyn", "sp.finished", "sp.pipeline", + "sp.pipelineOpts", + "sp.pipelineOptsSignal", + "sp.finishedOpts", + "sp.finishedOptsSignal", + "stream.finishedOpts", + "stream.finishedOptsSignal", "sc.text", "sc.json", "sc.buffer", diff --git a/packages/compiler/src/backend/llvm/lib-stream.ts b/packages/compiler/src/backend/llvm/lib-stream.ts index 34b7e0301..4bf1df6fc 100644 --- a/packages/compiler/src/backend/llvm/lib-stream.ts +++ b/packages/compiler/src/backend/llvm/lib-stream.ts @@ -13,6 +13,29 @@ const STREAM_CANONICAL_CBS: Record host.emitExpr(a)); + const arr = B.slot(); + B.entryAllocas.push(`${arr} = alloca [${n} x ptr]`); + for (let i = 0; i < n; i++) { + const p = B.tmp(); + B.line(`${p} = getelementptr inbounds [${n} x ptr], ptr ${arr}, i64 0, i64 ${i}`); + B.line(`store ptr ${args[1 + i]!.name}, ptr ${p}`); + } + const end = args[1 + n]!.name; + let signal = "null"; + let add = "null"; + if (e.fn === "sp.pipelineOptsSignal") { + signal = args[2 + n]!.name; + host.declare(`declare ptr @scr_events_add_abort_listener(ptr, ptr)`); + add = "@scr_events_add_abort_listener"; + } + const t = B.tmp(); + host.declare(`declare ptr @scr_sp_pipeline_opts(double, ptr, i1, ptr, ptr)`); + B.line( + `${t} = call ptr @scr_sp_pipeline_opts(double ${f64Lit(n)}, ptr ${arr}, i1 ${end}, ptr ${signal}, ptr ${add})`, + ); + const out = host.own({ name: t, type: e.type }); + host.emitPendingCheck(); + return out; + } + if (e.fn === "sp.finishedOpts" || e.fn === "sp.finishedOptsSignal") { + // finished(s, options) → a pending void promise (+1). + const args = e.args.map((a) => host.emitExpr(a)); + let signal = "null"; + let add = "null"; + if (e.fn === "sp.finishedOptsSignal") { + signal = args[5]!.name; + host.declare(`declare ptr @scr_events_add_abort_listener(ptr, ptr)`); + add = "@scr_events_add_abort_listener"; + } + const t = B.tmp(); + host.declare(`declare ptr @scr_sp_finished_opts(ptr, double, double, i1, i1, ptr, ptr)`); + B.line( + `${t} = call ptr @scr_sp_finished_opts(ptr ${args[0]!.name}, double ${args[1]!.name}, double ${args[2]!.name}, i1 ${args[3]!.name}, i1 ${args[4]!.name}, ptr ${signal}, ptr ${add})`, + ); + return host.own({ name: t, type: e.type }); + } + if (e.fn === "stream.finishedOpts" || e.fn === "stream.finishedOptsSignal") { + // finished(s, options, cb) → the +1 cleanup closure; typed callbacks + // ride the "e" thunk, checked-dynamic ones the runtime's own. + const args = e.args.map((a) => host.emitExpr(a)); + const cbIdx = e.args.length - 1; + let signal = "null"; + let add = "null"; + if (e.fn === "stream.finishedOptsSignal") { + signal = args[4]!.name; + host.declare(`declare ptr @scr_events_add_abort_listener(ptr, ptr)`); + add = "@scr_events_add_abort_listener"; + } + const head = `ptr ${args[0]!.name}`; + const flags = `double ${args[1]!.name}, double ${args[2]!.name}, i1 ${args[3]!.name}, ptr ${signal}, ptr ${add}`; + const t = B.tmp(); + const cbT = e.args[cbIdx]!.type; + if (cbT.kind === "dyn") { + host.declare( + `declare ptr @scr_stream_finished_opts_dyn(ptr, ptr, double, double, i1, ptr, ptr)`, + ); + B.line( + `${t} = call ptr @scr_stream_finished_opts_dyn(${head}, ptr ${args[cbIdx]!.name}, ${flags})`, + ); + } else { + if (cbT.kind !== "func") + throw new InternalCompilerError( + "llvm emitter bug: stream.finishedOpts callback not a func", + ); + host.moveTemp(args[cbIdx]!); + const thunk = host.streamCbThunkFor("e", cbT); + host.declare( + `declare ptr @scr_stream_finished_opts(ptr, ptr, ptr, double, double, i1, ptr, ptr)`, + ); + B.line( + `${t} = call ptr @scr_stream_finished_opts(${head}, ptr ${args[cbIdx]!.name}, ptr @${thunk}, ${flags})`, + ); + } + const out = host.own({ name: t, type: e.type }); + host.emitPendingCheck(); + return out; + } if (e.fn === "stream.pipeline" || e.fn === "stream.pipelineDyn") { // pipeline(count, s1..sn, cb): the destination answers +1. The // stream list rides a stack array (the C compound literal). diff --git a/packages/compiler/src/frontend/builtin-modules.ts b/packages/compiler/src/frontend/builtin-modules.ts index bf5c5349a..b45dce6d7 100644 --- a/packages/compiler/src/frontend/builtin-modules.ts +++ b/packages/compiler/src/frontend/builtin-modules.ts @@ -21,6 +21,7 @@ export const SUPPORTED_BUILTIN_MODULES = [ "string_decoder", "querystring", "readline", + "readline/promises", "http2", "assert", "assert/strict", diff --git a/packages/compiler/src/frontend/lowering/builtins/module-calls.ts b/packages/compiler/src/frontend/lowering/builtins/module-calls.ts index 7dec93820..5c5fbc31f 100644 --- a/packages/compiler/src/frontend/lowering/builtins/module-calls.ts +++ b/packages/compiler/src/frontend/lowering/builtins/module-calls.ts @@ -181,7 +181,10 @@ export function lowerBuiltinModuleCall( const lowered = lowerDiagnosticsChannelCall(lowerer, expr, bi, loc); if (lowered) return lowered; } - if (bi.module === "readline" && bi.member === "createInterface") { + if ( + (bi.module === "readline" || bi.module === "readline/promises") && + bi.member === "createInterface" + ) { return lowerReadlineCreateCall(lowerer, expr, loc); } if (bi.module === "fs" && bi.member === "readFileSync" && expr.arguments.length === 2) { diff --git a/packages/compiler/src/frontend/lowering/builtins/readline.ts b/packages/compiler/src/frontend/lowering/builtins/readline.ts index d9f4db510..f887aef30 100644 --- a/packages/compiler/src/frontend/lowering/builtins/readline.ts +++ b/packages/compiler/src/frontend/lowering/builtins/readline.ts @@ -1,39 +1,91 @@ import * as ts from "../../ts7/adapter.js"; import { type Lowerer } from "../lowerer.js"; import { locOf } from "../../program.js"; -import { F64, type IrExpr, type SrcLoc, STRING, VOID } from "../../../ir/ir.js"; +import { DYN, F64, type IrExpr, type SrcLoc, STRING, VOID } from "../../../ir/ir.js"; +import { boolLit, numLit } from "../../../ir/build.js"; import { READLINE_DOCUMENTED_OPTIONS, fenceOrDropOptionKey } from "../surfaces.js"; +import { streamSidesOf } from "../lower-stream.js"; -/** True when `node`'s checker type is readline's Interface (stdlib - * provenance + the enclosing "readline" ambient module — the name alone - * is too generic). The interface maps to an f64 handle, so the IR type - * cannot discriminate it from a plain number. */ -function isReadlineTyped(lowerer: Lowerer, node: ts.Expression): boolean { +/** The ambient module declaring `d`, or null. */ +function ambientModuleOf(d: ts.Node): string | null { + let up: ts.Node | undefined = d.parent; + while (up) { + if (ts.isModuleDeclaration(up) && ts.isStringLiteral(up.name)) return up.name.text; + up = up.parent; + } + return null; +} + +function isReadlineModule(spec: string | null): boolean { + return ( + spec === "readline" || + spec === "node:readline" || + spec === "readline/promises" || + spec === "node:readline/promises" + ); +} + +/** True when `node`'s checker type is a readline Interface (stdlib + * provenance + the enclosing "readline" or "readline/promises" ambient + * module — the name alone is too generic). The interface maps to an f64 + * handle, so the IR type cannot discriminate it from a plain number. */ +export function isReadlineTyped(lowerer: Lowerer, node: ts.Expression): boolean { const t = lowerer.checker.getTypeAtLocation(node); const sym = t.getAliasSymbol() ?? t.getSymbol(); if (sym?.name !== "Interface") return false; return lowerer.checker.declarationsOf(sym).some((d) => { if (!ts.isClassDeclaration(d) && !ts.isInterfaceDeclaration(d)) return false; if (!lowerer.isStdlibFile(d.getSourceFile())) return false; - let up: ts.Node | undefined = d.parent; - while (up) { - if (ts.isModuleDeclaration(up) && ts.isStringLiteral(up.name)) { - return up.name.text === "readline" || up.name.text === "node:readline"; - } - up = up.parent; - } - return false; + return isReadlineModule(ambientModuleOf(d)); }); } -/** Method calls on readline Interface receivers: `rl.question(query, cb)` - * writes the query to stdout and delivers the next stdin line's text to - * the callback (one (answer: string) parameter, or none); `rl.close()` - * fires the 'close' listeners synchronously (Node's inline emit) and - * releases the loop; `rl.on("close", cb)` registers a zero-arg - * listener. Everything else the lib declares (on("line"), prompt, - * setPrompt, ...) fences member-qualified. Null for non-Interface - * receivers. */ +/** The runtime's listener event numbers (scr_readline.c). */ +const RL_EVENTS: Record = { line: 0, close: 1, pause: 2, resume: 3, error: 4 }; + +/** True when the call's resolved signature answers a Promise — the + * readline/promises question. */ +function returnsPromise(lowerer: Lowerer, call: ts.CallExpression): boolean { + const t = lowerer.checker.getTypeAtLocation(call); + return (t.getSymbol()?.name ?? "") === "Promise"; +} + +/** The `{ signal }` options argument of the promise question: the signal + * expression, or null when absent. Other members fence by name. */ +function questionSignal(lowerer: Lowerer, node: ts.Expression | undefined): ts.Expression | null { + if (node === undefined) return null; + if (!ts.isObjectLiteralExpression(node)) { + lowerer.noLowering( + "readline/promises question options that are not an object literal", + node, + "write the options inline: question(query, { signal })", + ); + } + let signal: ts.Expression | null = null; + for (const p of node.properties) { + if (ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && p.name.text === "signal") { + signal = p.initializer; + continue; + } + lowerer.noLowering( + "readline/promises question with this option", + p, + "signal is the supported question option", + ); + } + return signal; +} + +/** Method calls on readline Interface receivers (both modules): + * - question(query, cb) — the callback form; question(query, { signal }?) + * — the promise form (readline/promises); + * - close(), pause(), resume(), prompt(), setPrompt(p), getPrompt(), + * write(data); + * - on/once/addListener for 'line' ((input: string) or no parameters), + * 'close'/'pause'/'resume' (no parameters) and 'error' ((err: Error) or + * no parameters), as statements. + * Everything else the lib declares fences member-qualified. Null for + * non-Interface receivers. */ export function lowerReadlineMethodCall( lowerer: Lowerer, call: ts.CallExpression, @@ -44,159 +96,314 @@ export function lowerReadlineMethodCall( if (!lowerer.isStdlibMember(access)) return null; const name = access.name.text; const loc = locOf(call); - if (name === "question" && call.arguments.length === 2) { - const receiver = lowerer.lowerExprExpecting(access.expression, F64); - const query = lowerer.lowerExprExpecting(call.arguments[0]!, STRING); - const cb = lowerer.lowerExpr(call.arguments[1]!); + const args = call.arguments; + const receiver = (): IrExpr => lowerer.lowerExprExpecting(access.expression, F64); + if ( + name === "question" && + returnsPromise(lowerer, call) && + args.length >= 1 && + args.length <= 2 + ) { + const recv = receiver(); + const query = lowerer.lowerExprExpecting(args[0]!, STRING); + const signal = questionSignal(lowerer, args[1]); + const type = { kind: "promise", inner: STRING } as const; + if (signal === null) { + return { kind: "libCall", fn: "rl.questionAsync", args: [recv, query], type, loc }; + } + const sig = lowerer.lowerExprExpecting(signal, DYN); + return { kind: "libCall", fn: "rl.questionAsyncSignal", args: [recv, query, sig], type, loc }; + } + if (name === "question" && args.length === 2) { + const recv = receiver(); + const query = lowerer.lowerExprExpecting(args[0]!, STRING); + const cb = lowerer.lowerExpr(args[1]!); if (cb.type.kind !== "func" || cb.type.ret.kind !== "void" || cb.type.params.length > 1) { lowerer.unsupported( "SC1090", - call.arguments[1]!, + args[1]!, "question callbacks with more than one parameter or a return value", ); } const param = cb.type.params[0]; - if (param !== undefined && param.kind !== "string") { + if (param !== undefined && param.kind !== "string" && param.kind !== "dyn") { lowerer.unsupported( "SC1090", - call.arguments[1]!, + args[1]!, `question callbacks whose parameter is not 'string' (got '${lowerer.fmt(param)}')`, ); } - return { kind: "libCall", fn: "rl.question", args: [receiver, query, cb], type: VOID, loc }; + return { kind: "libCall", fn: "rl.question", args: [recv, query, cb], type: VOID, loc }; } - if (name === "close" && call.arguments.length === 0) { - const receiver = lowerer.lowerExprExpecting(access.expression, F64); - return { kind: "libCall", fn: "rl.close", args: [receiver], type: VOID, loc }; - } - if (name === "on" && call.arguments.length === 2) { - const evT = lowerer.typeOf(call.arguments[0]!); - const event = evT.isStringLiteralType() ? evT.value : null; - if (event !== "close") { + const simple: Record = { + close: "rl.close", + pause: "rl.pause", + resume: "rl.resume", + prompt: "rl.prompt", + }; + const simpleFn = simple[name]; + if (simpleFn !== undefined && (args.length === 0 || (name === "prompt" && args.length === 1))) { + if ((name === "pause" || name === "resume") && !ts.isExpressionStatement(call.parent)) { + lowerer.unsupported( + "SC1090", + call, + `using the result of readline ${name}() (call it as its own statement)`, + ); + } + // prompt(preserveCursor) only moves the cursor of a terminal + // interface: a boolean literal is accepted and has no effect here. + const flag = args[0]; + if ( + flag !== undefined && + flag.kind !== ts.SyntaxKind.TrueKeyword && + flag.kind !== ts.SyntaxKind.FalseKeyword + ) { lowerer.noLowering( - `readline.Interface.on(${event === null ? "non-literal event" : `"${event}"`}, ...)`, - call.arguments[0]!, - '"close" is the supported readline event (question(query, cb) is the line consumer)', + "readline prompt() with a non-literal preserveCursor", + flag, + "preserveCursor only affects terminal interfaces — pass a boolean literal or omit it", + ); + } + return { kind: "libCall", fn: simpleFn, args: [receiver()], type: VOID, loc }; + } + if (name === "setPrompt" && args.length === 1) { + const recv = receiver(); + const prompt = lowerer.lowerExprExpecting(args[0]!, STRING); + return { kind: "libCall", fn: "rl.setPrompt", args: [recv, prompt], type: VOID, loc }; + } + if (name === "getPrompt" && args.length === 0) { + return { kind: "libCall", fn: "rl.getPrompt", args: [receiver()], type: STRING, loc }; + } + if (name === "write" && args.length === 1) { + const recv = receiver(); + const data = lowerer.lowerExprExpecting(args[0]!, STRING); + return { kind: "libCall", fn: "rl.write", args: [recv, data], type: VOID, loc }; + } + if ((name === "on" || name === "once" || name === "addListener") && args.length === 2) { + const evT = lowerer.typeOf(args[0]!); + const event = evT.isStringLiteralType() ? evT.value : null; + const ev = event === null ? undefined : RL_EVENTS[event]; + if (ev === undefined) { + lowerer.noLowering( + `readline.Interface.${name}(${event === null ? "non-literal event" : `"${event}"`}, ...)`, + args[0]!, + '"line", "close", "pause", "resume" and "error" are the supported readline events', ); } if (!ts.isExpressionStatement(call.parent)) { lowerer.unsupported( "SC1090", call, - "chaining readline listener registration (the result is void here — register each listener as its own statement)", + "chaining readline listener registration (register each listener as its own statement)", ); } - const receiver = lowerer.lowerExprExpecting(access.expression, F64); - const cb = lowerer.lowerExpr(call.arguments[1]!); - if (cb.type.kind !== "func" || cb.type.ret.kind !== "void" || cb.type.params.length > 0) { + const recv = receiver(); + const cb = lowerer.lowerExpr(args[1]!); + const want = event === "line" ? "string" : event === "error" ? "error" : null; + const param = cb.type.kind === "func" ? cb.type.params[0] : undefined; + const paramOk = + param === undefined || + (want !== null && param.kind === "dyn") || + (want === "string" && param.kind === "string") || + (want === "error" && param.kind === "object" && param.className === "Error"); + if ( + cb.type.kind !== "func" || + cb.type.ret.kind !== "void" || + cb.type.params.length > (want === null ? 0 : 1) || + !paramOk + ) { lowerer.unsupported( "SC1090", - call.arguments[1]!, - "close listeners with parameters or a return value (use ())", + args[1]!, + want === "string" + ? "'line' listeners other than (input: string) => void" + : want === "error" + ? "'error' listeners other than (err: Error) => void" + : `'${event}' listeners with parameters or a return value (use ())`, ); } - return { kind: "libCall", fn: "rl.onClose", args: [receiver, cb], type: VOID, loc }; + return { + kind: "libCall", + fn: "rl.on", + args: [recv, numLit(ev, loc), cb, boolLit(name === "once", loc)], + type: VOID, + loc, + }; } lowerer.noLowering( `readline.Interface.${name}`, call, - 'question(query, cb), close(), and on("close", cb) are the supported Interface members', + "question, close, pause, resume, prompt, setPrompt, getPrompt, write, for await, and on/once for 'line', 'close', 'pause', 'resume' and 'error' are the supported Interface members", lowerer.checker.getSymbolAtLocation(access.name), ); } -// readline.createInterface({ input: process.stdin, output: -// process.stdout }): exactly that options shape — the runtime reads -// fd 0 and writes prompts to stdout, so any OTHER stream would be a -// lie. `terminal` is accepted only as the literal false (the pipe -// behavior this implements); other members fence by name. +/** The interface's input: process.stdin (null) or a readable stream. */ +function lowerInput(lowerer: Lowerer, node: ts.Expression): IrExpr | null { + if ( + ts.isPropertyAccessExpression(node) && + lowerer.stdlibGlobalMember(node, "process") === "stdin" + ) { + return null; + } + const v = lowerer.lowerExpr(node); + const info = v.type.kind === "object" ? lowerer.classes.get(v.type.className) : undefined; + const sides = streamSidesOf(lowerer, info); + if (sides !== "r" && sides !== "rw") { + lowerer.noLowering( + "createInterface with this input", + node, + "process.stdin and readable streams are the supported inputs", + ); + } + return v; +} + +/** The interface's output: "stdout", a writable stream, or "none". */ +function lowerOutput(lowerer: Lowerer, node: ts.Expression): IrExpr | "stdout" | "none" { + if ( + ts.isPropertyAccessExpression(node) && + lowerer.stdlibGlobalMember(node, "process") === "stdout" + ) { + return "stdout"; + } + if (node.kind === ts.SyntaxKind.NullKeyword) { + return "none"; + } + if (ts.isIdentifier(node) && node.text === "undefined") return "none"; + const v = lowerer.lowerExpr(node); + const info = v.type.kind === "object" ? lowerer.classes.get(v.type.className) : undefined; + const sides = streamSidesOf(lowerer, info); + if (sides !== "w" && sides !== "rw") { + lowerer.noLowering( + "createInterface with this output", + node, + "process.stdout and writable streams are the supported outputs", + ); + } + return v; +} + +/** createInterface({ input, output?, terminal?: false, crlfDelay?, + * prompt?, signal? }) or createInterface(input, output?), from + * node:readline or node:readline/promises. The input is process.stdin + * or a readable stream; the output process.stdout, a writable stream, or + * absent (prompts are then not written). rl.create's arguments: the mode + * literal (1 stream input, 2 stream output, 4 stdout output, 8 signal, + * 16 prompt), then the input and output streams when present, crlfDelay, + * the prompt and the signal when present. */ export function lowerReadlineCreateCall( lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc, ): IrExpr { - const optsNode = expr.arguments.length === 1 ? expr.arguments[0] : undefined; - if (!optsNode || !ts.isObjectLiteralExpression(optsNode)) { + let input: IrExpr | null = null; + let output: IrExpr | "stdout" | "none" = "none"; + let crlf: IrExpr = numLit(0, loc); + let prompt: IrExpr | null = null; + let signal: IrExpr | null = null; + const optsNode = expr.arguments[0]; + if (optsNode === undefined) { lowerer.noLowering( - "createInterface with this argument shape", + "createInterface without arguments", expr, - "the supported form is createInterface({ input: process.stdin, output: process.stdout })", + "pass { input: process.stdin, output: process.stdout } or createInterface(input, output)", ); } - let sawInput = false; - for (const p of optsNode.properties) { - if (!ts.isPropertyAssignment(p) || !ts.isIdentifier(p.name)) { + if (!ts.isObjectLiteralExpression(optsNode)) { + if (expr.arguments.length > 2) { lowerer.noLowering( - "createInterface with this options shape", - p, - "spreads, computed keys, and shorthand options have no lowering — write each member inline", + "createInterface with a completer or terminal argument", + expr.arguments[2]!, + "the positional form createInterface(input, output) is supported (terminal line editing has no lowering)", ); } - const member = p.name.text; - const streamOf = (node: ts.Expression): string | null => - ts.isPropertyAccessExpression(node) ? lowerer.stdlibGlobalMember(node, "process") : null; - if (member === "input") { - if (streamOf(p.initializer) !== "stdin") { - lowerer.noLowering( - "createInterface with a non-stdin input", - p.initializer, - "process.stdin is the one supported input stream", - ); - } - sawInput = true; - } else if (member === "output") { - if (streamOf(p.initializer) !== "stdout") { - lowerer.noLowering( - "createInterface with a non-stdout output", - p.initializer, - "process.stdout is the one supported output stream", - ); - } - } else if (member === "terminal") { - const t = lowerer.typeOf(p.initializer); - if (!(t.flags & ts.TypeFlags.BooleanLiteral) || lowerer.checker.typeToString(t) !== "false") { - lowerer.noLowering( - "createInterface with terminal enabled", - p.initializer, - "terminal line editing has no lowering — the pipe behavior is what compiles", - ); - } - } else if (member === "crlfDelay") { - // Infinity states the lowered behavior: the splitter holds a - // trailing \r until the next chunk decides \r\n vs \r, with no - // time limit (scr_readline.c) — Node's crlfDelay: Infinity. - // Finite delays would need a timer the splitter does not have. - if (!ts.isIdentifier(p.initializer) || p.initializer.text !== "Infinity") { - lowerer.noLowering( - "createInterface with a finite crlfDelay", - p.initializer, - "the lowered splitter always joins \\r\\n across chunks (Node's crlfDelay: Infinity) — Infinity is the accepted value", - ); - } - } else if (member === "completer") { + input = lowerInput(lowerer, optsNode); + if (expr.arguments[1] !== undefined) output = lowerOutput(lowerer, expr.arguments[1]); + } else { + if (expr.arguments.length > 1) { lowerer.noLowering( - "createInterface with a completer", - p, - "tab completion needs an interactive terminal — the lowered interface reads piped lines (terminal: false)", + "createInterface with more than one argument", + expr.arguments[1]!, + "pass every option in the one options object", ); - } else { - fenceOrDropOptionKey( - lowerer, - p, - member, - "createInterface", - READLINE_DOCUMENTED_OPTIONS, - "input, output, terminal: false, and crlfDelay: Infinity are the supported options", + } + let sawInput = false; + for (const p of optsNode.properties) { + if (!ts.isPropertyAssignment(p) || !ts.isIdentifier(p.name)) { + lowerer.noLowering( + "createInterface with this options shape", + p, + "spreads, computed keys, and shorthand options have no lowering — write each member inline", + ); + } + const member = p.name.text; + if (member === "input") { + input = lowerInput(lowerer, p.initializer); + sawInput = true; + } else if (member === "output") { + output = lowerOutput(lowerer, p.initializer); + } else if (member === "terminal") { + const t = lowerer.typeOf(p.initializer); + if ( + !(t.flags & ts.TypeFlags.BooleanLiteral) || + lowerer.checker.typeToString(t) !== "false" + ) { + lowerer.noLowering( + "createInterface with terminal enabled", + p.initializer, + "terminal line editing has no lowering — the pipe behavior is what compiles", + ); + } + } else if (member === "crlfDelay") { + crlf = lowerer.lowerExprExpecting(p.initializer, F64); + } else if (member === "prompt") { + prompt = lowerer.lowerExprExpecting(p.initializer, STRING); + } else if (member === "signal") { + signal = lowerer.lowerExprExpecting(p.initializer, DYN); + } else if (member === "completer") { + lowerer.noLowering( + "createInterface with a completer", + p, + "tab completion needs an interactive terminal — the lowered interface reads piped lines (terminal: false)", + ); + } else { + fenceOrDropOptionKey( + lowerer, + p, + member, + "createInterface", + READLINE_DOCUMENTED_OPTIONS, + "input, output, terminal: false, crlfDelay, prompt and signal are the supported options", + ); + // An undocumented key, dropped like Node drops it. + } + } + if (!sawInput) { + lowerer.noLowering( + "createInterface without an input stream", + optsNode, + "pass { input: process.stdin, output: process.stdout }", ); - // An undocumented key, dropped like Node drops it. } } - if (!sawInput) { - lowerer.noLowering( - "createInterface without an input stream", - optsNode, - "pass { input: process.stdin, output: process.stdout }", - ); - } - return { kind: "libCall", fn: "rl.create", args: [], type: F64, loc }; + const mode = + (input !== null ? 1 : 0) | + (typeof output === "object" ? 2 : 0) | + (output === "stdout" ? 4 : 0) | + (signal !== null ? 8 : 0) | + (prompt !== null ? 16 : 0); + const args: IrExpr[] = [numLit(mode, loc)]; + if (input !== null) args.push(input); + if (typeof output === "object") args.push(output); + args.push(crlf); + if (prompt !== null) args.push(prompt); + if (signal !== null) args.push(signal); + return { + kind: "libCall", + fn: signal !== null ? "rl.createSignal" : "rl.create", + args, + type: F64, + loc, + }; } diff --git a/packages/compiler/src/frontend/lowering/lower-async-iteration.ts b/packages/compiler/src/frontend/lowering/lower-async-iteration.ts index 79fb0db0c..62b7a89a5 100644 --- a/packages/compiler/src/frontend/lowering/lower-async-iteration.ts +++ b/packages/compiler/src/frontend/lowering/lower-async-iteration.ts @@ -7,13 +7,23 @@ import { dynUndefinedExpr } from "../../ir/build.js"; import * as ts from "../ts7/adapter.js"; import { InternalCompilerError } from "../../errors.js"; import type { Lowerer } from "./lowerer.js"; -import { BOOL, DYN, type IrExpr, type IrStmt, type IrType, VOID } from "../../ir/ir.js"; +import { + BOOL, + DYN, + F64, + type IrExpr, + type IrStmt, + type IrType, + STRING, + VOID, +} from "../../ir/ir.js"; import { locOf } from "../program.js"; import { extractIteratorValue } from "./lower-generators.js"; import { streamSidesOf } from "./lower-stream.js"; import { staticReadableStreamElementType } from "./lower-stdlib.js"; import { genResultRecord } from "../type-mapper.js"; import { sideEffectFreeOptionValue } from "./surfaces.js"; +import { isReadlineTyped } from "./builtins/readline.js"; interface NodeReadablePlan { source: ts.Expression; @@ -828,7 +838,143 @@ function lowerClassAsyncIterator( /** Claims the built-in static async-iterator families. Typed async * generators and process.stdin retain their specialized drivers; this is * the common stream protocol path. */ +/** The readline Interface a for-await iterates: the interface itself or + * an explicit rl[Symbol.asyncIterator]() call. */ +function readlinePlan(lowerer: Lowerer, expression: ts.Expression): ts.Expression | null { + if (isReadlineTyped(lowerer, expression)) return expression; + const call = symbolAsyncIteratorCall(lowerer, expression); + if (call === null) return null; + const source = (call.expression as ts.PropertyAccessExpression | ts.ElementAccessExpression) + .expression; + return isReadlineTyped(lowerer, source) ? source : null; +} + +/** for await (const line of rl): the interface's one async iterator + * (events.on over 'line' — scr_readline.c). Each line binds as a string; + * an abrupt completion while the body owns a line calls return(), which + * stops the iterator listening while queued lines stay readable. */ +function lowerReadlineLines( + lowerer: Lowerer, + stmt: ts.ForOfStatement, + source: ts.Expression, +): IrStmt { + const binding = checkForAwaitBinding(lowerer, stmt); + const loc = locOf(stmt); + lowerer.scopes.push(new Map()); + try { + const rl = lowerer.declareHiddenLocal("%faReadline", F64); + const active = lowerer.declareHiddenLocal("%faActive", BOOL); + active.mutable = true; + const promiseT: IrType = { kind: "promise", inner: DYN }; + const promise = lowerer.declareHiddenLocal("%rlNext", promiseT); + const value = lowerer.declareHiddenLocal("%rlValue", DYN); + const line = lowerer.declareLocal( + binding.declaration.name, + binding.declaration.name.text, + STRING, + binding.mutable, + ); + const rlRef = (): IrExpr => ({ kind: "varRef", localId: rl.id, type: F64, loc }); + const valueRef = (): IrExpr => ({ kind: "varRef", localId: value.id, type: DYN, loc }); + const body = lowerer.inCtl("loop", () => lowerer.lowerScopedBlock(stmt.statement)); + const loop: IrStmt = { + kind: "while", + cond: { kind: "boolLit", value: true, type: BOOL, loc }, + body: [ + { + kind: "assign", + localId: active.id, + value: { kind: "boolLit", value: false, type: BOOL, loc }, + loc, + }, + { + kind: "varDecl", + localId: promise.id, + init: { kind: "libCall", fn: "rl.iterNext", args: [rlRef()], type: promiseT, loc }, + loc, + }, + { + kind: "varDecl", + localId: value.id, + init: { + kind: "awaitExpr", + value: { kind: "varRef", localId: promise.id, type: promiseT, loc }, + type: DYN, + loc, + }, + loc, + }, + { + kind: "if", + cond: { kind: "dynTest", test: "undefined", value: valueRef(), type: BOOL, loc }, + then: [{ kind: "break", loc }], + else_: null, + loc, + }, + { + kind: "varDecl", + localId: line.id, + init: { kind: "dynCheck", value: valueRef(), type: STRING, loc }, + loc, + }, + { + kind: "assign", + localId: active.id, + value: { kind: "boolLit", value: true, type: BOOL, loc }, + loc, + }, + ...body, + ], + loc, + }; + return { + kind: "block", + body: [ + { kind: "varDecl", localId: rl.id, init: lowerer.lowerExprExpecting(source, F64), loc }, + { + kind: "exprStmt", + expr: { kind: "libCall", fn: "rl.iterOpen", args: [rlRef()], type: VOID, loc }, + loc, + }, + { + kind: "varDecl", + localId: active.id, + init: { kind: "boolLit", value: false, type: BOOL, loc }, + loc, + }, + { + kind: "tryCatch", + tryBody: [loop], + catchBody: null, + catchLocalId: null, + finallyBody: [ + { + kind: "if", + cond: boolRef(active.id, loc), + then: [ + { + kind: "exprStmt", + expr: { kind: "libCall", fn: "rl.iterReturn", args: [rlRef()], type: VOID, loc }, + loc, + }, + ], + else_: null, + loc, + }, + ], + loc, + }, + ], + loc, + }; + } finally { + lowerer.scopes.pop(); + } +} + export function lowerForAwaitBuiltin(lowerer: Lowerer, stmt: ts.ForOfStatement): IrStmt | null { + const readline = readlinePlan(lowerer, stmt.expression); + if (readline) return lowerReadlineLines(lowerer, stmt, readline); const node = nodeReadablePlan(lowerer, stmt.expression); if (node) return lowerNodeReadable(lowerer, stmt, node); const web = webReadablePlan(lowerer, stmt.expression); diff --git a/packages/compiler/src/frontend/lowering/lower-stream.ts b/packages/compiler/src/frontend/lowering/lower-stream.ts index 529b523f5..7c8eb075f 100644 --- a/packages/compiler/src/frontend/lowering/lower-stream.ts +++ b/packages/compiler/src/frontend/lowering/lower-stream.ts @@ -37,6 +37,7 @@ import { newFnCtx, own } from "./lowerer.js"; import { appendImplicitUndefinedReturn } from "./function-returns.js"; import { bufEncoding, knownBufEncoding } from "./containers/bytes.js"; import { tryLowerExpression } from "./expressions/try-lower-expression.js"; +import { sideEffectFreeOptionValue } from "./surfaces.js"; import { BOOL, DYN, @@ -60,6 +61,29 @@ import { const BYTES = BYTES_U8; +/** An expression that only reads bindings and properties (no calls, + * assignments or construction): reading it a step later observes the + * same values. */ +function pureRead(node: ts.Expression): boolean { + if ( + ts.isParenthesizedExpression(node) || + ts.isNonNullExpression(node) || + ts.isAsExpression(node) + ) { + return pureRead(node.expression); + } + if (ts.isIdentifier(node) || node.kind === ts.SyntaxKind.ThisKeyword) return true; + if (ts.isPropertyAccessExpression(node)) return pureRead(node.expression); + if (ts.isElementAccessExpression(node)) { + return ( + (ts.isStringLiteral(node.argumentExpression) || + ts.isNumericLiteral(node.argumentExpression)) && + pureRead(node.expression) + ); + } + return false; +} + /** The stream sides of a receiver class: the nearest stream-class * ancestor's, or null off the stream hierarchy. */ export function streamSidesOf( @@ -334,6 +358,10 @@ interface StreamOptions { * as a follow-up readable.pushEncoding — push(string) decodes through * it (Node's Buffer.from(chunk, state.defaultEncoding)). */ pushEncoding: string | null; + /** The `signal` option's value: applied with stream.addAbortSignal + * right after construction (Node's constructors call addAbortSignal + * once the state exists), or null when absent. */ + signal: IrExpr | null; /** Present callbacks in canonical order with their flag bits. */ flags: number; cbs: IrExpr[]; @@ -559,6 +587,7 @@ function parseStreamOptions( encoding: null, badEncoding: null, pushEncoding: null, + signal: null, flags: 0, cbs: [], }; @@ -635,6 +664,11 @@ function parseStreamOptions( present.set(name, lowerStreamCallback(lowerer, name, fnNode, thisType, tuple)); continue; } + // `{ signal }` shorthand: the binding is the value. + if (name === "signal" && ts.isShorthandPropertyAssignment(prop)) { + if (ts.isIdentifier(prop.name)) out.signal = lowerer.lowerExprExpecting(prop.name, DYN); + continue; + } // Scalar options. if (!ts.isPropertyAssignment(prop)) { lowerer.unsupported( @@ -729,13 +763,22 @@ function parseStreamOptions( "utf8 (the default) is the supported write-side string encoding", ); break; - case "signal": - lowerer.noLowering( - `the ${ctorName} option 'signal'`, - prop, - "AbortSignal-driven destruction is not lowered yet", - ); + case "signal": { + // `if (options.signal) addAbortSignal(options.signal, this)`: + // applied over the fresh stream, so the value is read after the + // constructor — only values whose evaluation has no effects. + if (value.kind === ts.SyntaxKind.NullKeyword) break; + if (ts.isIdentifier(value) && value.text === "undefined") break; + if (!sideEffectFreeOptionValue(value) && !pureRead(value)) { + lowerer.noLowering( + `the ${ctorName} option 'signal' with this value`, + prop, + "pass a variable or property read (const signal = controller.signal) — the signal is read after construction", + ); + } + out.signal = lowerer.lowerExprExpecting(value, DYN); break; + } case "captureRejections": { // `false` is the default — admit the literal; the rejection- // capturing mode has no lowering (async listeners' promises are @@ -824,6 +867,7 @@ function streamCtorArgs( encoding: string | null; badEncoding: string | null; pushEncoding: string | null; + signal: IrExpr | null; } { const shape = streamCtorShape(cls); const o = parseStreamOptions( @@ -852,6 +896,7 @@ function streamCtorArgs( encoding: o.encoding, badEncoding: o.badEncoding, pushEncoding: o.pushEncoding, + signal: o.signal, }; } @@ -907,13 +952,23 @@ export function lowerStreamNew(lowerer: Lowerer, expr: ts.NewExpression, info: C loc, }; } - if (parsed.encoding === null) return built; - // { encoding }: chain a setEncoding over the fresh stream (the operand - // temp releases with the frame; setEncoding answers the receiver +1). + if (parsed.encoding !== null) { + // { encoding }: chain a setEncoding over the fresh stream (the operand + // temp releases with the frame; setEncoding answers the receiver +1). + built = { + kind: "libCall", + fn: "readable.setEncoding", + args: [built, strLit(parsed.encoding, loc)], + type: thisType, + loc, + }; + } + if (parsed.signal === null) return built; + // { signal }: addAbortSignal over the fresh stream (answers it +1). return { kind: "libCall", - fn: "readable.setEncoding", - args: [built, strLit(parsed.encoding, loc)], + fn: "stream.addAbortSignal", + args: [built, parsed.signal], type: thisType, loc, }; @@ -1178,6 +1233,19 @@ export function lowerStreamSuperCall( loc, }); } + if (parsed.signal !== null) { + out.push({ + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "stream.addAbortSignal", + args: [thisRef(), parsed.signal], + type: thisType, + loc, + }, + loc, + }); + } return out; } @@ -1423,7 +1491,7 @@ const PIPELINE_STAGE_EXPECTED = function lowerStreamArg( lowerer: Lowerer, node: ts.Expression, - what: "finished" | "pipeline", + what: "finished" | "pipeline" | "addAbortSignal", ): IrExpr { const v = lowerer.lowerExpr(node); const info = v.type.kind === "object" ? lowerer.classes.get(v.type.className) : undefined; @@ -1563,6 +1631,113 @@ function lowerEosCallback( ); } +/** finished()'s options literal: readable/writable/error (and cleanup + * for the promise form) as boolean literals, signal as any expression. + * readable/writable answer -1 when absent (Node's default: whether the + * stream has that side). */ +function eosOptions( + lowerer: Lowerer, + node: ts.Expression, + promiseForm: boolean, +): { readable: number; writable: number; error: boolean; cleanup: boolean; signal: IrExpr | null } { + const out = { + readable: -1, + writable: -1, + error: true, + cleanup: false, + signal: null as IrExpr | null, + }; + if (!ts.isObjectLiteralExpression(node)) { + lowerer.noLowering( + "finished options that are not an object literal", + node, + "write the options inline: finished(stream, { readable, writable, error, signal })", + ); + } + const literal = (p: ts.PropertyAssignment, key: string): boolean => { + if (p.initializer.kind === ts.SyntaxKind.TrueKeyword) return true; + if (p.initializer.kind === ts.SyntaxKind.FalseKeyword) return false; + lowerer.noLowering( + `the finished option '${key}' with a non-literal value`, + p.initializer, + "pass true or false", + ); + }; + for (const p of node.properties) { + const key = + (ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) && ts.isIdentifier(p.name) + ? p.name.text + : null; + if (key === "signal") { + out.signal = ts.isShorthandPropertyAssignment(p) + ? lowerer.lowerExprExpecting(p.name as ts.Identifier, DYN) + : lowerer.lowerExprExpecting((p as ts.PropertyAssignment).initializer, DYN); + continue; + } + if (key !== null && ts.isPropertyAssignment(p)) { + if (key === "readable") out.readable = literal(p, key) ? 1 : 0; + else if (key === "writable") out.writable = literal(p, key) ? 1 : 0; + else if (key === "error") out.error = literal(p, key); + else if (key === "cleanup" && promiseForm) out.cleanup = literal(p, key); + else { + lowerer.noLowering( + `the finished option '${key}'`, + p, + promiseForm + ? "readable, writable, error, cleanup and signal are the supported options" + : "readable, writable, error and signal are the supported options", + ); + } + continue; + } + lowerer.noLowering( + "finished options with this member", + p, + "write each option as a plain property", + ); + } + return out; +} + +/** pipeline()'s trailing options literal (the promise form): signal as + * any expression, end as a boolean literal. */ +function pipelineOptions( + lowerer: Lowerer, + node: ts.ObjectLiteralExpression, +): { end: boolean; signal: IrExpr | null } { + const out = { end: true, signal: null as IrExpr | null }; + for (const p of node.properties) { + const key = + (ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) && ts.isIdentifier(p.name) + ? p.name.text + : null; + if (key === "signal") { + out.signal = ts.isShorthandPropertyAssignment(p) + ? lowerer.lowerExprExpecting(p.name as ts.Identifier, DYN) + : lowerer.lowerExprExpecting((p as ts.PropertyAssignment).initializer, DYN); + continue; + } + if (key === "end" && ts.isPropertyAssignment(p)) { + if (p.initializer.kind === ts.SyntaxKind.TrueKeyword) continue; + if (p.initializer.kind === ts.SyntaxKind.FalseKeyword) { + out.end = false; + continue; + } + lowerer.noLowering( + "the pipeline option 'end' with a non-literal value", + p.initializer, + "pass true or false", + ); + } + lowerer.noLowering( + key === null ? "pipeline options with this member" : `the pipeline option '${key}'`, + p, + "signal and end are the supported pipeline options", + ); + } + return out; +} + /** `finished(...)` / `pipeline(...)` / `getDefaultHighWaterMark(...)` * imported from node:stream (named imports, destructured requires, and * namespace-member calls all land here). Null for members this spoke @@ -1580,13 +1755,11 @@ export function lowerStreamModuleCall( // ERR_STREAM_PREMATURE_CLOSE — the callback form's status, boxed // through the promise instead). The options argument has no lowering. if (bi.member === "finished") { - if (args.length !== 1) { + if (args.length !== 1 && args.length !== 2) { lowerer.noLowering( `stream/promises.finished with ${args.length} arguments`, call, - args.length === 2 - ? "the options argument has no lowering yet — the one-argument form is supported" - : "the supported form is finished(stream)", + "the supported forms are finished(stream) and finished(stream, options)", ); } let recv: IrExpr; @@ -1596,6 +1769,24 @@ export function lowerStreamModuleCall( if (e instanceof StreamArgTypeThrow) return e.expr; throw e; } + const optsNode = args[1]; + if (optsNode !== undefined && optsNode.kind !== ts.SyntaxKind.NullKeyword) { + const o = eosOptions(lowerer, optsNode, true); + const head: IrExpr[] = [ + recv, + numLit(o.readable, loc), + numLit(o.writable, loc), + boolLit(o.error, loc), + boolLit(o.cleanup, loc), + ]; + return { + kind: "libCall", + fn: o.signal ? "sp.finishedOptsSignal" : "sp.finishedOpts", + args: o.signal ? [...head, o.signal] : head, + type: { kind: "promise", inner: VOID }, + loc, + }; + } return { kind: "libCall", fn: "sp.finished", @@ -1616,13 +1807,29 @@ export function lowerStreamModuleCall( "the supported form is pipeline(source, ...streams)", ); } + const lastArg = args[args.length - 1]!; + const opts = + ts.isObjectLiteralExpression(lastArg) && args.length >= 3 + ? pipelineOptions(lowerer, lastArg) + : null; + const stageArgs = opts ? args.slice(0, -1) : args; let streams: IrExpr[]; try { - streams = args.map((a) => lowerStreamArg(lowerer, a, "pipeline")); + streams = stageArgs.map((a) => lowerStreamArg(lowerer, a, "pipeline")); } catch (e) { if (e instanceof StreamArgTypeThrow) return e.expr; throw e; } + if (opts) { + const head = [numLit(streams.length, loc), ...streams, boolLit(opts.end, loc)]; + return { + kind: "libCall", + fn: opts.signal ? "sp.pipelineOptsSignal" : "sp.pipelineOpts", + args: opts.signal ? [...head, opts.signal] : head, + type: { kind: "promise", inner: VOID }, + loc, + }; + } return { kind: "libCall", fn: "sp.pipeline", @@ -1703,14 +1910,39 @@ export function lowerStreamModuleCall( ); } - if (bi.member === "finished") { + if (bi.member === "addAbortSignal") { + // addAbortSignal(signal, stream) → the stream: destroyed with an + // AbortError when the signal aborts (now, if it already has). if (args.length !== 2) { + lowerer.noLowering( + `addAbortSignal with ${args.length} arguments`, + call, + "the supported form is addAbortSignal(signal, stream)", + ); + } + const signal = lowerer.lowerExprExpecting(args[0]!, DYN); + let stream: IrExpr; + try { + stream = lowerStreamArg(lowerer, args[1]!, "addAbortSignal"); + } catch (e) { + if (e instanceof StreamArgTypeThrow) return e.expr; + throw e; + } + return { + kind: "libCall", + fn: "stream.addAbortSignalFn", + args: [signal, stream], + type: stream.type, + loc, + }; + } + + if (bi.member === "finished") { + if (args.length !== 2 && args.length !== 3) { lowerer.noLowering( `finished with ${args.length} arguments`, call, - args.length === 3 - ? "the options argument has no lowering yet — the two-argument form is supported" - : "the supported form is finished(stream, callback)", + "the supported forms are finished(stream, callback) and finished(stream, options, callback)", ); } let recv: IrExpr; @@ -1720,7 +1952,24 @@ export function lowerStreamModuleCall( if (e instanceof StreamArgTypeThrow) return e.expr; throw e; } - const { cb, dyn } = lowerEosCallback(lowerer, "finished", args[1]!, recv.type); + if (args.length === 3 && args[1]!.kind !== ts.SyntaxKind.NullKeyword) { + const o = eosOptions(lowerer, args[1]!, false); + const { cb } = lowerEosCallback(lowerer, "finished", args[2]!, recv.type); + const head: IrExpr[] = [ + recv, + numLit(o.readable, loc), + numLit(o.writable, loc), + boolLit(o.error, loc), + ]; + return { + kind: "libCall", + fn: o.signal ? "stream.finishedOptsSignal" : "stream.finishedOpts", + args: o.signal ? [...head, o.signal, cb] : [...head, cb], + type: funcOf([], VOID), + loc, + }; + } + const { cb, dyn } = lowerEosCallback(lowerer, "finished", args[args.length - 1]!, recv.type); return { kind: "libCall", fn: dyn ? "stream.finishedDyn" : "stream.finished", diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index 5069427dc..8c9157252 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -1617,6 +1617,11 @@ export const BUILTIN_MODULE_FNS: Record< readline: { createInterface: { fn: "rl.create", params: [], result: F64 }, }, + // node:readline/promises: the same Interface with the promise question + // (lowerReadlineMethodCall tells the forms apart by the call's result). + "readline/promises": { + createInterface: { fn: "rl.create", params: [], result: F64 }, + }, // node:net and node:http lower ENTIRELY through the server spoke // (lower-server.ts — every call shape is special-cased: closures, the // optional connect host, writeHead's literal headers); the member list diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 1300ea496..7c8b2cb8a 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -1928,11 +1928,10 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { ) { return { kind: "object", className: RUNTIME_EMITTER_CLASS }; } - // readline.Interface: the interface value is an f64 handle into the - // runtime's registry (the Timeout-id precedent). The NAME is generic, - // so the provenance check adds the enclosing ambient module, like - // net.Server (@types/node also declares Interface in - // readline/promises — that one stays unmapped and fences). + // readline.Interface and readline/promises.Interface: the interface + // value is an f64 handle into the runtime's registry (the Timeout-id + // precedent). The NAME is generic, so the provenance check adds the + // enclosing ambient module, like net.Server. if ( psym?.name === "Interface" && checker @@ -1941,7 +1940,8 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "readline"), + (isDeclaredInAmbientModule(d, "readline") || + isDeclaredInAmbientModule(d, "readline/promises")), ) ) { return F64; diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index 5cb04ac22..f086c1ceb 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -374,6 +374,15 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "https.requestFn", "https.requestFnCb", "rl.question", + "rl.createSignal", + "stream.finishedOpts", + "stream.finishedOptsSignal", + "stream.addAbortSignal", + "stream.addAbortSignalFn", + "rl.pause", + "rl.resume", + "rl.prompt", + "rl.write", "json.parse", "json.parseReviver", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index e45acd446..371dbbbc7 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -3018,6 +3018,14 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * propagates the first error by destroying the rest, calls cb after the * last 'close'; answers the destination +1. The Dyn twins take the * callback as a checked-dynamic VALUE (mustCall wrappers). */ + /** stream.addAbortSignal: [stream, signal] — the constructors' signal + * option, chained over the fresh stream (falsy signals are ignored); + * stream.addAbortSignalFn: [signal, stream] — addAbortSignal(signal, + * stream). Both answer the stream (+1) and throw ERR_INVALID_ARG_TYPE + * for a value that is not an AbortSignal; they link the event-target + * unit (addAbortListener). */ + "stream.addAbortSignal": { argTypes: [null, DYN], result: VOID }, + "stream.addAbortSignalFn": { argTypes: [DYN, null], result: VOID }, "stream.finished": { argTypes: [null, null], result: VOID }, "stream.finishedDyn": { argTypes: [null, DYN], result: VOID }, "stream.pipeline": { argTypes: [F64], result: VOID }, @@ -3028,6 +3036,25 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * chaining/destroyer semantics settling a void promise (fulfilled on a * clean finish, rejected with the finish status otherwise). */ "sp.finished": { argTypes: [null], result: { kind: "promise", inner: VOID } }, + /** The options forms: finished(s, { readable, writable, error, cleanup, + * signal }) as [s, readable, writable, error, cleanup, signal?] — + * readable/writable -1 (absent), 0 or 1 — and pipeline(...streams, + * { signal, end }) as [count, s1..sn, end, signal?]. The Signal forms + * link the event-target unit (addAbortListener). The callback form + * stream.finishedOpts: [s, readable, writable, error, signal?, cb] → + * the cleanup closure. */ + "sp.finishedOpts": { + argTypes: [null, F64, F64, BOOL, BOOL], + result: { kind: "promise", inner: VOID }, + }, + "sp.finishedOptsSignal": { + argTypes: [null, F64, F64, BOOL, BOOL, DYN], + result: { kind: "promise", inner: VOID }, + }, + "sp.pipelineOpts": { argTypes: [F64], result: { kind: "promise", inner: VOID } }, + "sp.pipelineOptsSignal": { argTypes: [F64], result: { kind: "promise", inner: VOID } }, + "stream.finishedOpts": { argTypes: [null, F64, F64, BOOL, null], result: VOID }, + "stream.finishedOptsSignal": { argTypes: [null, F64, F64, BOOL, DYN, null], result: VOID }, "sp.pipeline": { argTypes: [F64], result: { kind: "promise", inner: VOID } }, /** node:stream/consumers — the promise consumers over the readable * machinery: (s) → a pending promise settled at the terminal point @@ -3293,27 +3320,46 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "strdec.next": { argTypes: [STRING, F64, BYTES_U8], result: F64 }, "strdec.end": { argTypes: [STRING, F64], result: STRING }, // node:readline: the interface handle is f64; the callbacks' func types - // are program-dependent (zero-param or (answer: string) — the emitter - // picks the adapter), so those slots are null like child.onExit's. - /** node:readline's question/close slice (scr_readline.c, linked under - * the events gate — these fns imply moduleUsesProcessEvents). The - * interface value is an f64 handle (the Timeout-id precedent). - * rl.create: [] → f64 — createInterface({ input: process.stdin, - * output: process.stdout }), registering the unit's shared stdin - * consumer (an OPEN interface keeps the loop alive until close/EOF, - * Node's semantics). rl.question: [handle, query, cb] — writes the - * query to stdout (Node writes under pipes too) and delivers the next - * line's text through a backend-picked adapter (zero-param or - * (answer: string)); THROWS Node's "readline was closed" on a closed - * interface (may-throw). rl.close: [handle] — fires 'close' listeners - * SYNCHRONOUSLY (Node's inline emit) and detaches the consumer (the - * loop stops waiting on fd 0). rl.onClose: [handle, cb] — a zero-arg - * listener (moves); stdin EOF closes every open interface with the - * buffered partial line DISCARDED, like Node. */ - "rl.create": { argTypes: [], result: F64 }, + // are program-dependent (the emitter picks the adapter by parameter + // count), so those slots are null like child.onExit's. + /** node:readline and node:readline/promises (scr_readline.c, linked + * under the events gate — these fns imply moduleUsesProcessEvents; a + * stream input or output adds scr_stream.c's bridge). The interface + * value is an f64 handle (the Timeout-id precedent); an open interface + * on stdin keeps the loop alive until close/EOF. + * rl.create: [mode, input?, output?, crlfDelay, prompt?] → f64, mode + * bits 1 stream input, 2 stream output, 4 stdout output, 16 prompt + * (lowerReadlineCreateCall); rl.createSignal: the same plus the + * trailing AbortSignal (mode bit 8), which links the event-target unit + * for addAbortListener. rl.question: [handle, query, cb] — the callback + * form; THROWS ERR_USE_AFTER_CLOSE on a closed interface (may-throw). + * rl.questionAsync(Signal): [handle, query, signal?] → promise, + * rejecting instead of throwing. rl.close/pause/resume/prompt: + * [handle] (pause/resume/prompt throw ERR_USE_AFTER_CLOSE when + * closed); rl.setPrompt/getPrompt; rl.write: [handle, data] (throws + * when closed). rl.on: [handle, event, cb, once] — the event number + * (0 'line', 1 'close', 2 'pause', 3 'resume', 4 'error') and the + * listener (moves). rl.iterOpen/iterNext/iterReturn: the async + * iterator — iterNext answers promise of the line or undefined. */ + "rl.create": { argTypes: [F64], result: F64 }, + "rl.createSignal": { argTypes: [F64], result: F64 }, "rl.question": { argTypes: [F64, STRING, null], result: VOID }, + "rl.questionAsync": { argTypes: [F64, STRING], result: { kind: "promise", inner: STRING } }, + "rl.questionAsyncSignal": { + argTypes: [F64, STRING, DYN], + result: { kind: "promise", inner: STRING }, + }, "rl.close": { argTypes: [F64], result: VOID }, - "rl.onClose": { argTypes: [F64, null], result: VOID }, + "rl.pause": { argTypes: [F64], result: VOID }, + "rl.resume": { argTypes: [F64], result: VOID }, + "rl.prompt": { argTypes: [F64], result: VOID }, + "rl.setPrompt": { argTypes: [F64, STRING], result: VOID }, + "rl.getPrompt": { argTypes: [F64], result: STRING }, + "rl.write": { argTypes: [F64, STRING], result: VOID }, + "rl.on": { argTypes: [F64, F64, null, BOOL], result: VOID }, + "rl.iterOpen": { argTypes: [F64], result: VOID }, + "rl.iterNext": { argTypes: [F64], result: { kind: "promise", inner: DYN } }, + "rl.iterReturn": { argTypes: [F64], result: VOID }, /** node:timers/promises — the promisified pair (scr_async.c, beside * the timer heap they ride): tp.setTimeout: [ms] → a pending void * promise a one-shot heap timer fulfills (the loop's timer phase, FIFO diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 16a4ebb16..e610fc506 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -164,6 +164,18 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run features.legacyTextDecoder = true; if (fn.startsWith("fetch.")) features.fetch = true; if (fn.startsWith("event.") || fn === "events.addAbortListener") features.eventTarget = true; + // Signal-taking readline entry points call addAbortListener. + if ( + fn === "rl.createSignal" || + fn === "rl.questionAsyncSignal" || + fn === "stream.addAbortSignal" || + fn === "stream.addAbortSignalFn" || + fn === "sp.finishedOptsSignal" || + fn === "sp.pipelineOptsSignal" || + fn === "stream.finishedOptsSignal" + ) { + features.eventTarget = true; + } if (PROCESS_EVENT_LIB_FNS.has(fn)) features.processEvents = true; if (fn.startsWith("emitter.")) features.emitter = true; if ( @@ -180,7 +192,12 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run fn === "stream.errored" || fn === "sp.finished" || fn === "sp.pipeline" || - fn === "console.bind" + fn === "console.bind" || + fn === "stream.addAbortSignal" || + fn === "stream.addAbortSignalFn" || + fn.startsWith("sp.finishedOpts") || + fn.startsWith("sp.pipelineOpts") || + fn.startsWith("stream.finishedOpts") ) features.stream = true; if (fn.startsWith("zlib.")) features.zlib = true; @@ -426,9 +443,21 @@ const PROCESS_EVENT_LIB_FNS: ReadonlySet = new Set([ // node:readline rides the stdin unit (scr_readline.c links beside // scr_events.c under the same gate). "rl.create", + "rl.createSignal", "rl.question", + "rl.questionAsync", + "rl.questionAsyncSignal", "rl.close", - "rl.onClose", + "rl.pause", + "rl.resume", + "rl.prompt", + "rl.setPrompt", + "rl.getPrompt", + "rl.write", + "rl.on", + "rl.iterOpen", + "rl.iterNext", + "rl.iterReturn", ]); /** True when the module uses the process-events surface — the link switch diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index 56e39f3a1..33da2a30d 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -3960,11 +3960,15 @@ function validateFunction( e.fn === "stream.pipeline" || e.fn === "stream.pipelineDyn" || e.fn === "sp.pipeline" || + e.fn === "sp.pipelineOpts" || + e.fn === "sp.pipelineOptsSignal" || e.fn === "writable.write" || e.fn === "writable.writeStr" || e.fn === "writable.writeU" || e.fn === "writable.end" || - e.fn === "readable.unpipe"; + e.fn === "readable.unpipe" || + e.fn === "rl.create" || + e.fn === "rl.createSignal"; if (variadic ? e.args.length < sig.argTypes.length : e.args.length !== sig.argTypes.length) { err(`libCall ${e.fn}: ${e.args.length} args, expected ${sig.argTypes.length}`, e.loc); } @@ -5163,7 +5167,11 @@ function validateFunction( e.fn === "stream.finished" || e.fn === "stream.finishedDyn" || e.fn === "stream.pipeline" || - e.fn === "stream.pipelineDyn" + e.fn === "stream.pipelineDyn" || + e.fn === "stream.addAbortSignal" || + e.fn === "stream.addAbortSignalFn" || + e.fn === "stream.finishedOpts" || + e.fn === "stream.finishedOptsSignal" ) { // Receiver/result: stream-hierarchy objects (a class whose base // chain reaches a runtime stream class). Constructors have no @@ -5179,6 +5187,15 @@ function validateFunction( } return false; }; + if (e.fn === "stream.addAbortSignal" || e.fn === "stream.addAbortSignalFn") { + // [stream, signal] (the option) or [signal, stream] (the module + // function): the result is the stream argument's type. + const s = e.args[e.fn === "stream.addAbortSignal" ? 0 : 1]; + if (!isStreamObject(s?.type) || !typeEquals(e.type, s!.type)) { + err(`libCall ${e.fn} must take and return a stream-hierarchy object`, e.loc); + } + return true; + } if (e.fn.endsWith(".new")) { if (!isStreamObject(e.type)) { err(`libCall ${e.fn} must return a stream class, got ${e.type.kind}`, e.loc); @@ -5210,6 +5227,20 @@ function validateFunction( if (e.type.kind !== "void") err(`libCall ${e.fn} must be void`, e.loc); return true; } + if (e.fn === "stream.finishedOpts" || e.fn === "stream.finishedOptsSignal") { + // (recv, readable, writable, error[, signal], cb) → the cleanup + // closure; the callback is a func or a checked-dynamic value. + if (!isStreamObject(e.args[0]?.type)) { + err(`libCall ${e.fn} receiver must be a stream-hierarchy object`, e.loc); + } + const cbK = e.args[e.args.length - 1]?.type.kind; + if (cbK !== "func" && cbK !== "dyn") + err(`libCall ${e.fn} callback must be a func or dyn`, e.loc); + if (e.type.kind !== "func") { + err(`libCall ${e.fn} must return the cleanup closure, got ${e.type.kind}`, e.loc); + } + return true; + } if (e.fn === "stream.finished" || e.fn === "stream.finishedDyn") { // (recv, cb) → the cleanup closure. if (!isStreamObject(e.args[0]?.type)) { diff --git a/packages/compiler/surface-manifest.json b/packages/compiler/surface-manifest.json index 7d0cf4a67..64d6346ed 100644 --- a/packages/compiler/surface-manifest.json +++ b/packages/compiler/surface-manifest.json @@ -2049,6 +2049,19 @@ "name": "readline.createInterface", "status": "static" }, + { + "id": "node-builtin.readline.promises", + "kind": "node-builtin", + "name": "readline/promises", + "status": "static", + "note": "recognized module (bare and node:-prefixed specifiers)" + }, + { + "id": "node-builtin.readline.promises.createInterface", + "kind": "node-builtin", + "name": "readline/promises.createInterface", + "status": "static" + }, { "id": "node-builtin.stream", "kind": "node-builtin", diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5295-readline-promises-question.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5295-readline-promises-question.ts.json new file mode 100644 index 000000000..eda7c2f9a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5295-readline-promises-question.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5295-readline-promises-question.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5296-readline-lines.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5296-readline-lines.ts.json new file mode 100644 index 000000000..59278cfaa --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5296-readline-lines.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5296-readline-lines.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5297-readline-callback-events.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5297-readline-callback-events.cjs.json new file mode 100644 index 000000000..f970f659c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5297-readline-callback-events.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5297-readline-callback-events.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5298-readline-abort.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5298-readline-abort.cjs.json new file mode 100644 index 000000000..907e7c728 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5298-readline-abort.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5298-readline-abort.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5299-readline-stdin.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5299-readline-stdin.ts.json new file mode 100644 index 000000000..b3c02e08d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5299-readline-stdin.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5299-readline-stdin.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5300-stream-add-abort-signal.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5300-stream-add-abort-signal.ts.json new file mode 100644 index 000000000..2c02f31ea --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5300-stream-add-abort-signal.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5300-stream-add-abort-signal.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5301-stream-add-abort-signal-errors.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5301-stream-add-abort-signal-errors.cjs.json new file mode 100644 index 000000000..d910a68d5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5301-stream-add-abort-signal-errors.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5301-stream-add-abort-signal-errors.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5302-stream-promises-options.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5302-stream-promises-options.ts.json new file mode 100644 index 000000000..d6406565a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5302-stream-promises-options.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5302-stream-promises-options.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5303-stream-finished-callback-opts.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5303-stream-finished-callback-opts.cjs.json new file mode 100644 index 000000000..d52247df7 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5303-stream-finished-callback-opts.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5303-stream-finished-callback-opts.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/readline-promises.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/readline-promises.ts.json new file mode 100644 index 000000000..d70e40fb1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/readline-promises.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/node-types/readline-promises.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_readline.c b/packages/runtime/src/scr_readline.c index 286d0ea1d..6f1f23f31 100644 --- a/packages/runtime/src/scr_readline.c +++ b/packages/runtime/src/scr_readline.c @@ -1,31 +1,37 @@ -/* node:readline — the question/close slice over the stdin unit - * (scr_events.c), linked exactly when the program uses it (the events - * gating; rl.* libCalls imply events). +/* node:readline and node:readline/promises — the non-terminal Interface + * (lib/internal/readline/interface.js) over process.stdin or a stream + * input, linked only when the program uses it (the events gating; + * rl.* libCalls imply events). The stream side (listeners on the input, + * pause/resume, output writes) lives in scr_stream.c behind ScrRlIo, so + * stdin-only programs never link the stream unit. * - * Model, pinned against Node under pipes (SEMANTICS.md + corpus 1475): - * - createInterface({ input: process.stdin, output: process.stdout }) - * registers this unit's ONE shared stdin data listener (a consumer: the - * loop watches fd 0 and the process stays alive while any interface is - * open, exactly Node's un-closed readline) and a global 'end' listener. - * - Lines split on \n and \r\n (and a bare \r followed by more input); - * a trailing \r holds until the next chunk decides \r\n vs \r. - * - question(query, cb) writes the query to stdout (Node writes it under - * pipes too) and delivers the NEXT line's text; a line arriving with no - * pending question is DROPPED (Node emits 'line' that nobody hears). - * - close() removes the consumer (the loop stops waiting on fd 0), drops - * a pending question (its callback never fires), and fires the 'close' - * listeners SYNCHRONOUSLY — Node's close() emits inline, which is why - * the portless prompt's close-listener resolve wins over the question - * callback's (oracle-pinned). - * - stdin EOF closes every open interface the same way: the buffered - * partial line is DISCARDED (Node), then 'close' fires. - * - question() after close() throws Node's ERR_USE_AFTER_CLOSE message. - * - An interface created AFTER stdin ended is DEAD (pinned against - * Node): a question still writes its prompt, but nothing ever answers - * and 'close' never fires — Node's process exits with the question - * pending, and the loop exhausts the same way here. */ + * Behavior, matching Node v24.15.0: + * - Input bytes run through a UTF-8 decoder that holds an incomplete + * trailing sequence for the next chunk (StringDecoder); invalid bytes + * decode to U+FFFD. String chunks pass through unchanged. + * - Lines split on /\r?\n|\r(?!\n)/: a trailing \r ends its line at once, + * and a \n starting the next chunk is dropped when it arrives within + * crlfDelay ms (default and minimum 100). + * - Each line answers a pending question, or else is emitted as 'line'. + * A second question while one is pending writes the prompt again and is + * otherwise ignored. + * - The input's 'end' emits a non-empty partial line as 'line' (a pending + * question does not receive it), then close(). + * - close() pauses (emitting 'pause' when not already paused), removes + * the input listeners, and emits 'close' synchronously. A pending + * question is never answered. + * - Prompts are written only when an output was given. + * - The async iterator is events.on(rl, 'line', { close: ['close'], + * highWaterMark: 1024 }): lines queue until next() takes them, the + * input pauses above 1024 queued lines and resumes once the queue + * empties, and return() stops listening while queued lines stay + * readable. + * - On stdin, an interface created after stdin ended is DEAD: a question + * still writes its prompt, but nothing ever answers and 'close' never + * fires. */ #include "scr_runtime.h" +#include #include #include #include @@ -35,24 +41,76 @@ static void scr_rl_oom(void) { abort(); } +static void *scr_rl_grow(void *p, size_t *cap, size_t need, size_t elem) { + if (need <= *cap) return p; + size_t c = *cap ? *cap * 2 : 4; + while (c < need) c *= 2; + void *q = realloc(p, c * elem); + if (!q) scr_rl_oom(); + *cap = c; + return q; +} + +enum { RL_EV_LINE, RL_EV_CLOSE, RL_EV_PAUSE, RL_EV_RESUME, RL_EV_ERROR, RL_EV_COUNT }; + +typedef void (*ScrRlLineFn)(ScrClosure *, ScrStr *); /* owns the +1 line */ +typedef void (*ScrRlErrFn)(ScrClosure *, ScrError *); /* borrows the error */ + +/* One listener: a user closure (fn: the line or error adapter; NULL for + * the zero-argument events), or the async iterator's own handler. */ +typedef struct { + ScrClosure *cb; + void *fn; + bool once; + bool iter; +} RlEntry; + +typedef struct { + RlEntry *v; + size_t n, cap; +} RlList; + +typedef struct { + ScrStr **q; /* queued lines (owned) */ + size_t head, len, cap; + ScrPromise **waiters; /* pending next() promises (owned) */ + size_t wn, wcap; + ScrError *error; /* an 'error' no next() has seen yet (owned) */ + bool finished, paused; +} RlIter; + typedef struct ScrRl { double id; bool closed; - bool dead; /* created after stdin ended: close fires at question() */ - ScrClosure *q_cb; /* pending question's callback (owned), or NULL */ - void (*q_fn)(ScrClosure *, ScrStr *); - ScrClosure **close_cbs; /* owned zero-arg listeners */ - size_t n_close, cap_close; - char *buf; /* undelivered stdin bytes */ - size_t len, cap; + bool stdin_in; /* reads process.stdin through the shared consumer */ + bool dead; /* stdin only: created after stdin ended */ + bool paused; + const ScrRlIo *io; /* the stream side, for a stream input or output */ + void *in; /* the input stream (+1), or NULL */ + void *out; /* the output stream (+1), or NULL */ + bool out_stdout; + ScrStr *prompt, *old_prompt; + ScrClosure *q_cb; /* the pending question's callback (owned) */ + ScrRlLineFn q_fn; + RlList ev[RL_EV_COUNT]; + /* the line splitter */ + char *line_buf; /* the unfinished line (kLine_buffer), UTF-8 */ + size_t line_len, line_cap; + unsigned char held[4]; /* the decoder's incomplete trailing sequence */ + size_t n_held; + double saw_return_at, crlf_delay; + /* stdin bytes that arrived while paused */ + char *parked; + size_t parked_len, parked_cap; + RlIter *iter; + ScrDyn *abort_dispose; /* the signal option's listener disposable (+1) */ struct ScrRl *next; } ScrRl; static SCR_TL ScrRl *scr_rls = NULL; static SCR_TL double scr_rl_next_id = 1; -static SCR_TL size_t scr_rl_open = 0; /* un-closed, un-dead interfaces */ -static SCR_TL ScrClosure *scr_rl_data_cb = NULL; /* the shared consumer (borrowed - * mirror; the registry owns) */ +static SCR_TL size_t scr_rl_stdin_open = 0; /* open stdin interfaces */ +static SCR_TL ScrClosure *scr_rl_data_cb = NULL; /* the shared stdin consumer (borrowed) */ static SCR_TL bool scr_rl_end_registered = false; static ScrRl *scr_rl_find(double id) { @@ -62,141 +120,600 @@ static ScrRl *scr_rl_find(double id) { return NULL; } -/* Fires the close listeners (snapshot; synchronous, like Node's emit) and - * releases everything the interface holds. */ +static void scr_rl_throw_closed(void) { + static const char m[] = "readline was closed"; + scr_throw_error_msg_code(SCR_ERR_ERROR, m, sizeof m - 1, "ERR_USE_AFTER_CLOSE"); +} + +/* ── listeners ─────────────────────────────────────────────────────── */ + +static void scr_rl_add(ScrRl *rl, int ev, ScrClosure *cb /*moves*/, void *fn, bool once, bool iter) { + RlList *l = &rl->ev[ev]; + l->v = scr_rl_grow(l->v, &l->cap, l->n + 1, sizeof *l->v); + l->v[l->n++] = (RlEntry){cb, fn, once, iter}; +} + +static void scr_rl_remove_iter(ScrRl *rl) { + for (int ev = 0; ev < RL_EV_COUNT; ev++) { + RlList *l = &rl->ev[ev]; + size_t k = 0; + for (size_t i = 0; i < l->n; i++) { + if (!l->v[i].iter) l->v[k++] = l->v[i]; + } + l->n = k; + } +} + +static void scr_rl_iter_line(ScrRl *rl, ScrStr *line /*moves*/); +static void scr_rl_iter_error(ScrRl *rl, ScrError *err); +static void scr_rl_iter_close(ScrRl *rl); + +/* emit(ev, ...): a snapshot in listener order; once-listeners leave before + * they run (Node's onceWrapper). `line` is borrowed (each line listener + * receives its own +1); `err` is borrowed. */ +static void scr_rl_emit(ScrRl *rl, int ev, ScrStr *line, ScrError *err) { + RlList *l = &rl->ev[ev]; + size_t n = l->n; + if (n == 0) return; + RlEntry *snap = malloc(n * sizeof *snap); + if (!snap) scr_rl_oom(); + memcpy(snap, l->v, n * sizeof *snap); + for (size_t i = 0; i < n; i++) { + if (!snap[i].iter) scr_closure_retain(snap[i].cb); + } + size_t k = 0; + for (size_t i = 0; i < l->n; i++) { + if (l->v[i].once) { + if (!l->v[i].iter) scr_closure_release(l->v[i].cb); + } else { + l->v[k++] = l->v[i]; + } + } + l->n = k; + for (size_t i = 0; i < n; i++) { + RlEntry e = snap[i]; + if (!scr_exc_pending()) { + if (e.iter) { + if (ev == RL_EV_LINE) scr_rl_iter_line(rl, scr_str_retain(line)); + else if (ev == RL_EV_ERROR) scr_rl_iter_error(rl, err); + else if (ev == RL_EV_CLOSE) scr_rl_iter_close(rl); + } else if (ev == RL_EV_LINE) { + ((ScrRlLineFn)e.fn)(e.cb, scr_str_retain(line)); + } else if (ev == RL_EV_ERROR) { + ((ScrRlErrFn)e.fn)(e.cb, err); + } else { + ((void (*)(ScrClosure *))e.cb->fn)(e.cb); + } + } + if (!e.iter) scr_closure_release(e.cb); + } + free(snap); +} + +/* ── output, pause/resume ──────────────────────────────────────────── */ + +static void scr_rl_write_out(ScrRl *rl, ScrStr *text) { + if (rl->out) rl->io->write(rl->out, text); + else if (rl->out_stdout) scr_stdio_write_str(1, text); +} + +static void scr_rl_feed_bytes(ScrRl *rl, const char *p, size_t n); + +/* pause()/resume() without the closed check (Node's methods throw on a + * closed interface; close() itself pauses first). */ +static void scr_rl_do_pause(ScrRl *rl) { + if (rl->paused) return; + if (rl->in) rl->io->pause(rl->in); + rl->paused = true; + scr_rl_emit(rl, RL_EV_PAUSE, NULL, NULL); +} + +static void scr_rl_do_resume(ScrRl *rl) { + if (!rl->paused) return; + if (rl->in) rl->io->resume(rl->in); + rl->paused = false; + scr_rl_emit(rl, RL_EV_RESUME, NULL, NULL); + /* stdin bytes that arrived while paused (the pipe holds them in Node) */ + if (rl->stdin_in && rl->parked_len > 0 && !rl->closed) { + char *p = rl->parked; + size_t n = rl->parked_len; + rl->parked = NULL; + rl->parked_len = rl->parked_cap = 0; + scr_rl_feed_bytes(rl, p, n); + free(p); + } +} + +static void scr_rl_prompt_now(ScrRl *rl) { + if (rl->paused) scr_rl_do_resume(rl); + if (scr_exc_pending()) return; + scr_rl_write_out(rl, rl->prompt); +} + +/* ── close ─────────────────────────────────────────────────────────── */ + +static void scr_rl_detach_stdin(ScrRl *rl) { + if (!rl->stdin_in || rl->dead) return; + if (scr_rl_stdin_open > 0) scr_rl_stdin_open--; + /* The last stdin interface leaving detaches the shared consumer so the + * loop stops waiting on fd 0 (Node's pause-on-close). */ + if (scr_rl_stdin_open == 0 && scr_rl_data_cb != NULL) { + scr_stdin_remove_data(scr_rl_data_cb); + scr_rl_data_cb = NULL; + } +} + static void scr_rl_settle_close(ScrRl *rl) { if (rl->closed) return; + scr_rl_do_pause(rl); + if (scr_exc_pending()) return; rl->closed = true; - if (!rl->dead && scr_rl_open > 0) scr_rl_open--; + /* onSelfCloseWithoutTerminal: the input listeners go first (they were + * registered at construction). */ + if (rl->in) { + rl->io->detach(rl->in, rl->id); + rl->io->release(rl->in); + rl->in = NULL; + } + scr_rl_detach_stdin(rl); + if (rl->abort_dispose) { + ScrDyn *d = rl->abort_dispose; + rl->abort_dispose = NULL; + ScrDyn *r = scr_dyn_call(d, NULL, 0, "dispose"); + if (r) scr_dyn_release(r); + scr_dyn_release(d); + } if (rl->q_cb) { scr_closure_release(rl->q_cb); /* a pending question never answers */ rl->q_cb = NULL; } - /* The last open interface leaving detaches the shared consumer so the - * loop stops waiting on fd 0 (Node's pause-on-close). */ - if (scr_rl_open == 0 && scr_rl_data_cb != NULL) { - scr_stdin_remove_data(scr_rl_data_cb); - scr_rl_data_cb = NULL; + scr_rl_emit(rl, RL_EV_CLOSE, NULL, NULL); + free(rl->line_buf); + rl->line_buf = NULL; + rl->line_len = rl->line_cap = 0; + free(rl->parked); + rl->parked = NULL; + rl->parked_len = rl->parked_cap = 0; + if (rl->out) { + rl->io->release(rl->out); + rl->out = NULL; } - size_t n = rl->n_close; - ScrClosure **snap = rl->close_cbs; - rl->close_cbs = NULL; - rl->n_close = rl->cap_close = 0; - for (size_t i = 0; i < n; i++) { - if (!scr_exc_pending()) { - ((void (*)(ScrClosure *))snap[i]->fn)(snap[i]); - } - scr_closure_release(snap[i]); - } - free(snap); - free(rl->buf); - rl->buf = NULL; - rl->len = rl->cap = 0; } -/* One complete line off the front of the buffer: *adv is the byte count - * consumed (terminator included), *line_len the line's length. False when - * no complete line is buffered (a trailing \r holds for the \r\n - * decision). */ -static bool scr_rl_scan(const ScrRl *rl, size_t *line_len, size_t *adv) { - for (size_t i = 0; i < rl->len; i++) { - char c = rl->buf[i]; - if (c == '\n') { - *line_len = i; - *adv = i + 1; - return true; - } - if (c == '\r') { - if (i + 1 >= rl->len) return false; /* hold: \r\n may straddle chunks */ - *line_len = i; - *adv = rl->buf[i + 1] == '\n' ? i + 2 : i + 1; - return true; - } - } - return false; -} +/* ── lines ─────────────────────────────────────────────────────────── */ -static void scr_rl_drain(ScrRl *rl) { - size_t line_len, adv; - while (!rl->closed && scr_rl_scan(rl, &line_len, &adv)) { - ScrStr *line = NULL; +/* kOnLine: a pending question takes the line, else 'line'. Moves line. */ +static void scr_rl_on_line(ScrRl *rl, ScrStr *line) { + if (rl->q_cb) { ScrClosure *cb = rl->q_cb; - void (*fn)(ScrClosure *, ScrStr *) = rl->q_fn; - if (cb) line = scr_str_new(rl->buf, line_len); - memmove(rl->buf, rl->buf + adv, rl->len - adv); - rl->len -= adv; - if (cb) { - rl->q_cb = NULL; /* consumed BEFORE the callback runs (once) */ - fn(cb, line); /* the adapter owns the +1 line */ - scr_closure_release(cb); - if (scr_exc_pending()) return; + ScrRlLineFn fn = rl->q_fn; + rl->q_cb = NULL; + if (rl->old_prompt) { + scr_str_release(rl->prompt); + rl->prompt = rl->old_prompt; + rl->old_prompt = NULL; } - /* No pending question: the line drops, exactly Node's unheard 'line'. */ + fn(cb, line); + scr_closure_release(cb); + return; } + scr_rl_emit(rl, RL_EV_LINE, line, NULL); + scr_str_release(line); } -/* The shared stdin data consumer: appends and drains every open - * interface (chunks broadcast, like any stdin listener set). */ +static void scr_rl_buf_append(ScrRl *rl, const char *p, size_t n) { + rl->line_buf = scr_rl_grow(rl->line_buf, &rl->line_cap, rl->line_len + n + 1, 1); + memcpy(rl->line_buf + rl->line_len, p, n); + rl->line_len += n; +} + +/* kNormalWrite over decoded UTF-8 text. */ +static void scr_rl_feed_text(ScrRl *rl, const char *p, size_t n) { + if (rl->saw_return_at != 0 && scr_now_ms() - rl->saw_return_at <= rl->crlf_delay) { + if (n > 0 && p[0] == '\n') { + p++; + n--; + } + rl->saw_return_at = 0; + } + bool ending = false; + for (size_t i = 0; i < n && !ending; i++) ending = p[i] == '\n' || p[i] == '\r'; + if (!ending) { + if (n > 0) scr_rl_buf_append(rl, p, n); + return; + } + /* The unfinished line joins the new text; the scan runs over both. */ + char *text = p ? (char *)p : (char *)""; + size_t len = n; + char *joined = NULL; + if (rl->line_len > 0) { + joined = malloc(rl->line_len + n); + if (!joined) scr_rl_oom(); + memcpy(joined, rl->line_buf, rl->line_len); + memcpy(joined + rl->line_len, p, n); + text = joined; + len = rl->line_len + n; + } + rl->line_len = 0; + rl->saw_return_at = len > 0 && text[len - 1] == '\r' ? scr_now_ms() : 0; + /* Split first (the remainder becomes the new unfinished line before any + * line is delivered, as in Node), then deliver in order. */ + size_t n_lines = 0, cap_lines = 0; + ScrStr **lines = NULL; + size_t start = 0; + for (size_t i = 0; i < len; i++) { + size_t adv; + if (text[i] == '\n') adv = 1; + else if (text[i] == '\r') adv = i + 1 < len && text[i + 1] == '\n' ? 2 : 1; + else continue; + lines = scr_rl_grow(lines, &cap_lines, n_lines + 1, sizeof *lines); + lines[n_lines++] = scr_str_new(text + start, i - start); + i += adv - 1; + start = i + 1; + } + if (start < len) scr_rl_buf_append(rl, text + start, len - start); + free(joined); + for (size_t i = 0; i < n_lines; i++) { + if (rl->closed || scr_exc_pending()) { + scr_str_release(lines[i]); + continue; + } + scr_rl_on_line(rl, lines[i]); + } + free(lines); +} + +/* The length of an incomplete UTF-8 sequence ending the buffer (0 when the + * buffer ends on a complete sequence or invalid bytes). */ +static size_t scr_rl_incomplete_tail(const unsigned char *p, size_t n) { + for (size_t back = 1; back <= 3 && back <= n; back++) { + unsigned char c = p[n - back]; + if ((c & 0xC0) == 0x80) continue; /* a continuation byte */ + size_t need = c >= 0xF0 && c <= 0xF4 ? 4 + : c >= 0xE0 && c <= 0xEF ? 3 + : c >= 0xC2 && c <= 0xDF ? 2 + : 0; + return need > back ? back : 0; + } + return 0; +} + +/* decoder.write(bytes) then kNormalWrite. */ +static void scr_rl_feed_bytes(ScrRl *rl, const char *p, size_t n) { + if (rl->stdin_in && rl->paused && !rl->closed) { + rl->parked = scr_rl_grow(rl->parked, &rl->parked_cap, rl->parked_len + n, 1); + memcpy(rl->parked + rl->parked_len, p, n); + rl->parked_len += n; + return; + } + size_t total = rl->n_held + n; + unsigned char *all = malloc(total ? total : 1); + if (!all) scr_rl_oom(); + memcpy(all, rl->held, rl->n_held); + memcpy(all + rl->n_held, p, n); + size_t tail = scr_rl_incomplete_tail(all, total); + rl->n_held = tail; + memcpy(rl->held, all + total - tail, tail); + ScrStr *text = scr_str_from_utf8_lossy(all, total - tail); + free(all); + scr_rl_feed_text(rl, text->data, text->len); + scr_str_release(text); +} + +/* The input ended: a non-empty unfinished line is emitted as 'line' (not + * offered to a pending question), then close(). */ +static void scr_rl_feed_end(ScrRl *rl) { + if (rl->closed) return; + if (rl->line_len > 0) { + ScrStr *line = scr_str_new(rl->line_buf, rl->line_len); + rl->line_len = 0; + scr_rl_emit(rl, RL_EV_LINE, line, NULL); + scr_str_release(line); + if (scr_exc_pending()) return; + } + scr_rl_settle_close(rl); +} + +void scr_rl_input_bytes(double id, ScrBytes *chunk) { + ScrRl *rl = scr_rl_find(id); + if (!rl || rl->closed) return; + char *p = malloc(chunk->len ? chunk->len : 1); + if (!p) scr_rl_oom(); + scr_bytes_read(chunk, 0, p, chunk->len); + scr_rl_feed_bytes(rl, p, chunk->len); + free(p); +} + +void scr_rl_input_str(double id, ScrStr *chunk) { + ScrRl *rl = scr_rl_find(id); + if (!rl || rl->closed) return; + scr_rl_feed_text(rl, chunk->data, chunk->len); +} + +void scr_rl_input_end(double id) { + ScrRl *rl = scr_rl_find(id); + if (rl) scr_rl_feed_end(rl); +} + +/* The input's 'error' becomes the interface's: with no listener (the + * iterator counts as one), it is thrown, as EventEmitter's emit('error') + * throws. */ +void scr_rl_input_error(double id, ScrError *err) { + ScrRl *rl = scr_rl_find(id); + if (!rl) return; + if (rl->ev[RL_EV_ERROR].n == 0) { + scr_throw_obj(scr_error_retain(err), &scr_error_retain_v, &scr_error_release_v, + scr_error_trace_arg()); + return; + } + scr_rl_emit(rl, RL_EV_ERROR, NULL, err); +} + +/* ── stdin input ───────────────────────────────────────────────────── */ + +/* The shared stdin consumer: every open stdin interface gets each chunk. */ static void scr_rl_data_adapter(ScrClosure *cb, ScrBytes *chunk) { (void)cb; for (ScrRl *rl = scr_rls; rl; rl = rl->next) { - if (rl->closed || rl->dead) continue; - if (rl->len + chunk->len > rl->cap) { - rl->cap = (rl->len + chunk->len) * 2 + 64; - rl->buf = realloc(rl->buf, rl->cap); - if (!rl->buf) scr_rl_oom(); - } - scr_bytes_read(chunk, 0, rl->buf + rl->len, chunk->len); - rl->len += chunk->len; - scr_rl_drain(rl); + if (rl->closed || rl->dead || !rl->stdin_in) continue; + scr_rl_input_bytes(rl->id, chunk); if (scr_exc_pending()) return; } } -/* stdin EOF: every open interface closes — the buffered partial line is - * DISCARDED (Node; a trailing held \r still terminates its line first). */ static void scr_rl_end_thunk(ScrClosure *cb) { (void)cb; scr_rl_data_cb = NULL; /* the stdin unit dropped its listeners itself */ for (ScrRl *rl = scr_rls; rl; rl = rl->next) { - if (rl->closed || rl->dead) continue; - if (rl->len > 0 && rl->buf[rl->len - 1] == '\r' && rl->q_cb) { - /* "a\r" then EOF: the \r terminates the line (Node's crlfDelay - * expiry collapsed to EOF time). */ - ScrStr *line = scr_str_new(rl->buf, rl->len - 1); - ScrClosure *qcb = rl->q_cb; - void (*fn)(ScrClosure *, ScrStr *) = rl->q_fn; - rl->q_cb = NULL; - rl->len = 0; - fn(qcb, line); - scr_closure_release(qcb); + if (rl->closed || rl->dead || !rl->stdin_in) continue; + if (rl->parked_len > 0) { + /* parked bytes still end their lines before the input ends */ + bool paused = rl->paused; + rl->paused = false; + char *p = rl->parked; + size_t n = rl->parked_len; + rl->parked = NULL; + rl->parked_len = rl->parked_cap = 0; + scr_rl_feed_bytes(rl, p, n); + free(p); + rl->paused = paused; if (scr_exc_pending()) return; } - scr_rl_settle_close(rl); + scr_rl_feed_end(rl); if (scr_exc_pending()) return; } } -/* Exit-time cleanup (the events-unit precedent, before the RC audit): - * whatever an interface still holds — a parked question, close listeners - * on a dead interface that never asked — releases here. */ -static void scr_rl_cleanup_atexit(void) { - for (ScrRl *rl = scr_rls; rl; rl = rl->next) { - if (rl->q_cb) { - scr_closure_release(rl->q_cb); - rl->q_cb = NULL; - } - for (size_t i = 0; i < rl->n_close; i++) scr_closure_release(rl->close_cbs[i]); - free(rl->close_cbs); - rl->close_cbs = NULL; - rl->n_close = rl->cap_close = 0; - free(rl->buf); - rl->buf = NULL; - rl->len = rl->cap = 0; +/* ── async iteration (events.on) ───────────────────────────────────── */ + +static void scr_rl_iter_settle_done(ScrPromise *p) { + scr_promise_fulfill_ref(p, scr_dyn_retain(scr_dyn_undefined()), scr_dyn_retain_v, + scr_dyn_release_v, NULL); +} + +static void scr_rl_iter_settle_line(ScrPromise *p, ScrStr *line /*moves*/) { + ScrDyn *v = scr_dyn_new_str(line); + scr_str_release(line); + scr_promise_fulfill_ref(p, v, scr_dyn_retain_v, scr_dyn_release_v, NULL); +} + +static void scr_rl_iter_reject(ScrPromise *p, ScrError *err) { + scr_throw_obj(scr_error_retain(err), &scr_error_retain_v, &scr_error_release_v, + scr_error_trace_arg()); + scr_promise_reject_pending(p); +} + +static ScrPromise *scr_rl_iter_shift_waiter(RlIter *it) { + ScrPromise *p = it->waiters[0]; + memmove(it->waiters, it->waiters + 1, (it->wn - 1) * sizeof *it->waiters); + it->wn--; + return p; +} + +/* closeHandler */ +static void scr_rl_iter_close(ScrRl *rl) { + RlIter *it = rl->iter; + if (!it) return; + scr_rl_remove_iter(rl); + it->finished = true; + it->paused = false; + while (it->wn > 0) { + ScrPromise *p = scr_rl_iter_shift_waiter(it); + scr_rl_iter_settle_done(p); + scr_promise_release(p); } } -double scr_rl_create(void) { +/* eventHandler */ +static void scr_rl_iter_line(ScrRl *rl, ScrStr *line) { + RlIter *it = rl->iter; + if (it->finished) { + scr_str_release(line); + return; + } + if (it->wn > 0) { + ScrPromise *p = scr_rl_iter_shift_waiter(it); + scr_rl_iter_settle_line(p, line); + scr_promise_release(p); + return; + } + it->q = scr_rl_grow(it->q, &it->cap, it->head + it->len + 1, sizeof *it->q); + it->q[it->head + it->len++] = line; + if (!it->paused && it->len > 1024) { + it->paused = true; + if (!rl->closed) scr_rl_do_pause(rl); + } +} + +/* errorHandler */ +static void scr_rl_iter_error(ScrRl *rl, ScrError *err) { + RlIter *it = rl->iter; + if (it->wn > 0) { + ScrPromise *p = scr_rl_iter_shift_waiter(it); + scr_rl_iter_reject(p, err); + scr_promise_release(p); + } else if (!it->error) { + it->error = scr_error_retain(err); + } + scr_rl_iter_close(rl); +} + +/* rl[Symbol.asyncIterator](): one iterator per interface, created on the + * first call; it listens for 'line', 'error' and 'close' from then on. */ +void scr_rl_iter_open(double id) { + ScrRl *rl = scr_rl_find(id); + if (!rl || rl->iter) return; + rl->iter = calloc(1, sizeof *rl->iter); + if (!rl->iter) scr_rl_oom(); + scr_rl_add(rl, RL_EV_LINE, NULL, NULL, false, true); + scr_rl_add(rl, RL_EV_ERROR, NULL, NULL, false, true); + scr_rl_add(rl, RL_EV_CLOSE, NULL, NULL, false, true); +} + +/* next(): a promise of the next line, or of undefined once done. */ +ScrPromise *scr_rl_iter_next(double id) { + ScrPromise *p = scr_promise_new(); + ScrRl *rl = scr_rl_find(id); + RlIter *it = rl ? rl->iter : NULL; + if (!it) { + scr_rl_iter_settle_done(p); + return p; + } + if (it->len > 0) { + ScrStr *line = it->q[it->head++]; + it->len--; + if (it->len == 0) it->head = 0; + if (it->paused && it->len < 1) { + it->paused = false; + if (!rl->closed) scr_rl_do_resume(rl); + } + scr_rl_iter_settle_line(p, line); + return p; + } + if (it->error) { + ScrError *err = it->error; + it->error = NULL; + scr_rl_iter_reject(p, err); + scr_error_release(err); + return p; + } + if (it->finished) { + scr_rl_iter_settle_done(p); + return p; + } + it->waiters = scr_rl_grow(it->waiters, &it->wcap, it->wn + 1, sizeof *it->waiters); + it->waiters[it->wn++] = scr_promise_retain(p); + return p; +} + +/* return(): the loop left early. */ +void scr_rl_iter_return(double id) { + ScrRl *rl = scr_rl_find(id); + if (rl) scr_rl_iter_close(rl); +} + +/* ── creation ──────────────────────────────────────────────────────── */ + +static void scr_rl_iter_free(RlIter *it) { + for (size_t i = 0; i < it->len; i++) scr_str_release(it->q[it->head + i]); + free(it->q); + for (size_t i = 0; i < it->wn; i++) scr_promise_release(it->waiters[i]); + free(it->waiters); + if (it->error) scr_error_release(it->error); + free(it); +} + +/* Exit-time cleanup (the events-unit precedent, before the RC audit): + * whatever an interface still holds releases here. */ +static void scr_rl_cleanup_atexit(void) { + ScrRl *rl = scr_rls; + scr_rls = NULL; + while (rl) { + ScrRl *next = rl->next; + if (rl->q_cb) scr_closure_release(rl->q_cb); + for (int ev = 0; ev < RL_EV_COUNT; ev++) { + for (size_t i = 0; i < rl->ev[ev].n; i++) { + if (!rl->ev[ev].v[i].iter) scr_closure_release(rl->ev[ev].v[i].cb); + } + free(rl->ev[ev].v); + } + if (rl->iter) scr_rl_iter_free(rl->iter); + if (rl->abort_dispose) scr_dyn_release(rl->abort_dispose); + if (rl->in) rl->io->release(rl->in); + if (rl->out) rl->io->release(rl->out); + scr_str_release(rl->prompt); + if (rl->old_prompt) scr_str_release(rl->old_prompt); + free(rl->line_buf); + free(rl->parked); + free(rl); + rl = next; + } +} + +static void scr_rl_close_job(ScrClosure *job) { + ScrRl *rl = scr_rl_find(scr_box_get_f64(job->caps[0])); + if (rl) scr_rl_settle_close(rl); +} + +static ScrDyn *scr_rl_abort_thunk(ScrClosure *clo, ScrDyn *const *args, size_t argc) { + (void)args; + (void)argc; + ScrRl *rl = scr_rl_find(scr_box_get_f64(clo->caps[0])); + if (rl) scr_rl_settle_close(rl); + return scr_dyn_retain(scr_dyn_undefined()); +} + +/* validateAbortSignal(signal, 'options.signal'): the aborted flag (0 or + * 1), or -1 with ERR_INVALID_ARG_TYPE pending for a value that is not an + * AbortSignal. */ +static int scr_rl_signal_aborted(ScrDyn *signal) { + ScrDyn *aborted = NULL; + if (signal->kind == SCR_DYN_OBJ) { + ScrDyn *v = scr_dyn_obj_get(signal, "aborted", 7); + aborted = v ? scr_dyn_retain(v) : NULL; + } else if (signal->kind == SCR_DYN_HANDLE) { + ScrStr *k = scr_str_new("aborted", 7); + aborted = scr_dyn_handle_key_get(signal, k); + scr_str_release(k); + } + if (!aborted) { + scr_dyn_prop_type_fail("options.signal", "an instance of AbortSignal", signal); + return -1; + } + int r = scr_dyn_truthy(aborted) ? 1 : 0; + scr_dyn_release(aborted); + return r; +} + +/* The `signal` option: an aborted signal closes the interface on the next + * tick; otherwise a later abort closes it. */ +static void scr_rl_watch_signal(ScrRl *rl, ScrDyn *signal, ScrAbortAddFn add) { + int state = scr_rl_signal_aborted(signal); + if (state < 0) return; + bool is_aborted = state == 1; + if (is_aborted) { + ScrClosure *job = scr_closure_new((void *)&scr_rl_close_job, 1); + job->caps[0] = scr_box_new(SCR_BOX_F64); + scr_box_set_f64(job->caps[0], rl->id); + scr_next_tick(job); + return; + } + ScrClosure *clo = scr_closure_new((void *)&scr_rl_abort_thunk, 1); + clo->caps[0] = scr_box_new(SCR_BOX_F64); + scr_box_set_f64(clo->caps[0], rl->id); + ScrDyn *listener = scr_dyn_new_func(clo, &scr_rl_abort_thunk, 0, "()", "onAborted"); + rl->abort_dispose = add(signal, listener); + scr_dyn_release(listener); +} + +/* createInterface: input is a stream (+1, moves; io non-NULL) or NULL for + * process.stdin; output is a stream (+1, moves), or NULL with out_stdout + * selecting process.stdout. crlf_delay is Node's option value (0 when + * absent); prompt is borrowed (NULL: "> "); signal is NULL or a borrowed + * AbortSignal, `add` its addAbortListener. Answers the interface + * handle. */ +double scr_rl_open(const ScrRlIo *io, void *in, void *out, bool out_stdout, double crlf_delay, + ScrStr *prompt, ScrDyn *signal, ScrAbortAddFn add) { static SCR_TL bool cleanup_registered = false; if (!cleanup_registered) { cleanup_registered = true; @@ -205,77 +722,284 @@ double scr_rl_create(void) { ScrRl *rl = calloc(1, sizeof *rl); if (!rl) scr_rl_oom(); rl->id = scr_rl_next_id++; + rl->stdin_in = in == NULL; + rl->io = io; + rl->in = in; + rl->out = out; + rl->out_stdout = out_stdout; + rl->prompt = prompt ? scr_str_retain(prompt) : scr_str_new("> ", 2); + /* this.crlfDelay = crlfDelay ? MathMax(kMincrlfDelay, crlfDelay) : kMincrlfDelay */ + rl->crlf_delay = isnan(crlf_delay) || crlf_delay == 0 ? 100 : fmax(100, crlf_delay); rl->next = scr_rls; scr_rls = rl; - if (scr_stdin_ended()) { - rl->dead = true; /* close fires when a question is asked */ - return rl->id; - } - scr_rl_open++; - if (scr_rl_data_cb == NULL) { - /* One shared consumer + one global end hook. The closures carry no - * captures; their fn slots are never invoked directly for the data - * one (the registered adapter is), and the end one IS its thunk. */ - scr_rl_data_cb = scr_closure_new((void *)&scr_rl_data_adapter, 0); - scr_stdin_on_data(scr_rl_data_cb, &scr_rl_data_adapter, false); - } - if (!scr_rl_end_registered) { - scr_rl_end_registered = true; - scr_stdin_on_end(scr_closure_new((void *)&scr_rl_end_thunk, 0), false); + if (in == NULL) { + if (scr_stdin_ended()) { + rl->dead = true; /* a question writes its prompt; nothing answers */ + } else { + scr_rl_stdin_open++; + if (scr_rl_data_cb == NULL) { + scr_rl_data_cb = scr_closure_new((void *)&scr_rl_data_adapter, 0); + scr_stdin_on_data(scr_rl_data_cb, &scr_rl_data_adapter, false); + } + if (!scr_rl_end_registered) { + scr_rl_end_registered = true; + scr_stdin_on_end(scr_closure_new((void *)&scr_rl_end_thunk, 0), false); + } + } } + if (signal && scr_dyn_truthy(signal)) scr_rl_watch_signal(rl, signal, add); return rl->id; } -void scr_rl_question(double id, const ScrStr *query, ScrClosure *cb /*moves*/, - void (*fn)(ScrClosure *, ScrStr *)) { +/* ── methods ───────────────────────────────────────────────────────── */ + +/* question(query, cb): throws ERR_USE_AFTER_CLOSE on a closed interface; + * while a question is pending only the prompt is written again. */ +void scr_rl_question(double id, ScrStr *query, ScrClosure *cb /*moves*/, ScrRlLineFn fn) { ScrRl *rl = scr_rl_find(id); if (!rl || rl->closed) { scr_closure_release(cb); - /* Node's ERR_USE_AFTER_CLOSE. */ - scr_throw_error_msg(SCR_ERR_ERROR, "readline was closed", 19); + scr_rl_throw_closed(); return; } - /* The prompt writes to stdout under pipes too (Node's question does), and - * must be visible before input arrives even though it has no trailing - * newline. */ - scr_stdio_write_str(1, query); - if (rl->dead) { - /* Created after stdin ended: pinned against Node — the prompt still - * writes, but the DESTROYED stream delivers nothing and 'close' - * never fires (Node's process exits with the question pending; the - * loop exhausts the same way here). The callback releases now. */ + if (rl->q_cb) { scr_closure_release(cb); + scr_rl_prompt_now(rl); return; } - if (rl->q_cb) scr_closure_release(rl->q_cb); /* re-ask replaces (Node) */ + rl->old_prompt = rl->prompt; + rl->prompt = scr_str_retain(query); rl->q_cb = cb; rl->q_fn = fn; - scr_rl_drain(rl); /* an already-buffered line answers immediately */ + scr_rl_prompt_now(rl); +} + +/* The promise question's state, shared by the answer adapter and the abort + * listener. The signal listener's disposable rides the answer adapter + * (caps[1]), not this state: the listener holds the state, so the state + * must not hold the listener. */ +typedef struct { + size_t rc; + ScrPromise *p; + ScrDyn *signal; /* +1 or NULL */ + double id; + bool settled; +} RlAsk; + +static void *scr_rl_ask_retain_v(void *a) { + ((RlAsk *)a)->rc++; + return a; +} + +static void scr_rl_ask_release_v(void *v) { + RlAsk *a = v; + if (--a->rc > 0) return; + scr_promise_release(a->p); + if (a->signal) scr_dyn_release(a->signal); + free(a); +} + +static void *scr_rl_dyn_retain_v(void *p) { return p ? scr_dyn_retain(p) : NULL; } +static void scr_rl_dyn_release_v(void *p) { scr_dyn_release(p); } + +/* Calls and drops a disposable (+1). */ +static void scr_rl_dispose(ScrDyn *d) { + if (!d) return; + ScrDyn *r = scr_dyn_call(d, NULL, 0, "dispose"); + if (r) scr_dyn_release(r); + scr_dyn_release(d); +} + +/* caps[0] the question state; caps[1] the signal listener's disposable + * (a dyn value, or an empty box). */ +static void scr_rl_ask_answer(ScrClosure *cb, ScrStr *answer) { + RlAsk *a = scr_box_get_ref(cb->caps[0]); /* +1 */ + scr_rl_dispose(scr_box_get_ref(cb->caps[1])); + scr_box_set_ref(cb->caps[1], NULL); + if (!a->settled) { + a->settled = true; + scr_promise_fulfill_str(a->p, answer); + } else { + scr_str_release(answer); + } + scr_rl_ask_release_v(a); +} + +/* new AbortError(undefined, { cause: signal.reason }) */ +static ScrError *scr_rl_abort_error(ScrDyn *signal) { + ScrStr *message = scr_str_new("The operation was aborted", 25); + ScrError *e = scr_error_new(SCR_ERR_ERROR, message); + scr_str_release(message); + scr_str_release(e->name); + e->name = scr_str_new("AbortError", 10); + /* AbortError assigns code, then name: own enumerable properties */ + scr_error_set_code(e, "ABORT_ERR"); + e->name_present = true; + e->name_enumerable = true; + ScrStr *k = scr_str_new("reason", 6); + ScrDyn *reason = scr_dyn_handle_key_get(signal, k); + scr_str_release(k); + scr_error_define_cause(e, reason ? reason : scr_dyn_undefined()); + if (reason) scr_dyn_release(reason); + return e; +} + +static void scr_rl_ask_reject(RlAsk *a, ScrError *e /*moves*/) { + a->settled = true; + scr_throw_obj(e, &scr_error_retain_v, &scr_error_release_v, scr_error_trace_arg()); + scr_promise_reject_pending(a->p); +} + +/* onAbort: kQuestionCancel (drops whichever question is pending, writing + * clearLine's "\r\n"), then the rejection. */ +static ScrDyn *scr_rl_ask_abort_thunk(ScrClosure *clo, ScrDyn *const *args, size_t argc) { + (void)args; + (void)argc; + RlAsk *a = scr_box_get_ref(clo->caps[0]); /* +1 */ + ScrRl *rl = scr_rl_find(a->id); + if (rl && rl->q_cb) { + scr_closure_release(rl->q_cb); + rl->q_cb = NULL; + if (rl->old_prompt) { + scr_str_release(rl->prompt); + rl->prompt = rl->old_prompt; + rl->old_prompt = NULL; + } + ScrStr *crlf = scr_str_new("\r\n", 2); + scr_rl_write_out(rl, crlf); + scr_str_release(crlf); + } + if (!a->settled) scr_rl_ask_reject(a, scr_rl_abort_error(a->signal)); + scr_rl_ask_release_v(a); + return scr_dyn_retain(scr_dyn_undefined()); +} + +/* readline/promises question(query, { signal }): rejects with + * ERR_USE_AFTER_CLOSE on a closed interface and with an AbortError when + * the signal is (or becomes) aborted. */ +ScrPromise *scr_rl_question_promise(double id, ScrStr *query, ScrDyn *signal, ScrAbortAddFn add) { + RlAsk *a = calloc(1, sizeof *a); + if (!a) scr_rl_oom(); + a->rc = 1; + a->p = scr_promise_new(); + a->id = id; + ScrDyn *dispose = NULL; + ScrPromise *result = scr_promise_retain(a->p); + if (signal && scr_dyn_truthy(signal)) { + int state = scr_rl_signal_aborted(signal); + if (state < 0) { + a->settled = true; + scr_promise_reject_pending(a->p); + scr_rl_ask_release_v(a); + return result; + } + bool is_aborted = state == 1; + a->signal = scr_dyn_retain(signal); + if (is_aborted) { + scr_rl_ask_reject(a, scr_rl_abort_error(signal)); + scr_rl_ask_release_v(a); + return result; + } + ScrClosure *clo = scr_closure_new((void *)&scr_rl_ask_abort_thunk, 1); + clo->caps[0] = scr_box_new_obj(&scr_rl_ask_retain_v, &scr_rl_ask_release_v, NULL); + scr_box_set_ref(clo->caps[0], scr_rl_ask_retain_v(a)); + ScrDyn *listener = scr_dyn_new_func(clo, &scr_rl_ask_abort_thunk, 0, "()", "onAbort"); + dispose = add(signal, listener); + scr_dyn_release(listener); + } + ScrRl *rl = scr_rl_find(id); + if (!rl || rl->closed) { + scr_rl_dispose(dispose); + static const char m[] = "readline was closed"; + ScrStr *message = scr_str_new(m, sizeof m - 1); + ScrError *e = scr_error_new(SCR_ERR_ERROR, message); + scr_str_release(message); + scr_error_set_code(e, "ERR_USE_AFTER_CLOSE"); + scr_rl_ask_reject(a, e); + scr_rl_ask_release_v(a); + return result; + } + ScrClosure *cb = scr_closure_new((void *)&scr_rl_ask_answer, 2); + cb->caps[0] = scr_box_new_obj(&scr_rl_ask_retain_v, &scr_rl_ask_release_v, NULL); + scr_box_set_ref(cb->caps[0], a); /* the creation reference moves here */ + cb->caps[1] = scr_box_new_obj(&scr_rl_dyn_retain_v, &scr_rl_dyn_release_v, NULL); + scr_box_set_ref(cb->caps[1], dispose); /* moves (NULL without a signal) */ + scr_rl_question(id, query, cb, &scr_rl_ask_answer); + return result; } void scr_rl_close(double id) { ScrRl *rl = scr_rl_find(id); - if (!rl) return; - scr_rl_settle_close(rl); /* twice is a no-op, like Node */ + if (rl) scr_rl_settle_close(rl); /* twice is a no-op, like Node */ } -void scr_rl_on_close(double id, ScrClosure *cb /*moves*/) { +void scr_rl_pause(double id) { ScrRl *rl = scr_rl_find(id); if (!rl || rl->closed) { - scr_closure_release(cb); /* after 'close' emitted: never fires */ + scr_rl_throw_closed(); return; } - if (rl->n_close == rl->cap_close) { - rl->cap_close = rl->cap_close ? rl->cap_close * 2 : 2; - rl->close_cbs = realloc(rl->close_cbs, rl->cap_close * sizeof *rl->close_cbs); - if (!rl->close_cbs) scr_rl_oom(); - } - rl->close_cbs[rl->n_close++] = cb; + scr_rl_do_pause(rl); } -/* ── the runtime-provided answer adapters ────────────────────────────── */ +void scr_rl_resume(double id) { + ScrRl *rl = scr_rl_find(id); + if (!rl || rl->closed) { + scr_rl_throw_closed(); + return; + } + scr_rl_do_resume(rl); +} -/* A zero-param question callback: the answer is ignored. */ +void scr_rl_prompt(double id) { + ScrRl *rl = scr_rl_find(id); + if (!rl || rl->closed) { + scr_rl_throw_closed(); + return; + } + scr_rl_prompt_now(rl); +} + +void scr_rl_set_prompt(double id, ScrStr *prompt) { + ScrRl *rl = scr_rl_find(id); + if (!rl) return; + scr_str_release(rl->prompt); + rl->prompt = scr_str_retain(prompt); +} + +ScrStr *scr_rl_get_prompt(double id) { + ScrRl *rl = scr_rl_find(id); + return rl ? scr_str_retain(rl->prompt) : scr_str_new("", 0); +} + +/* write(data): the text runs through the line splitter as if it were + * input. */ +void scr_rl_write(double id, ScrStr *data) { + ScrRl *rl = scr_rl_find(id); + if (!rl || rl->closed) { + scr_rl_throw_closed(); + return; + } + if (rl->paused) scr_rl_do_resume(rl); + if (scr_exc_pending()) return; + scr_rl_feed_text(rl, data->data, data->len); +} + +/* on/once for 'line' (fn: the line adapter), 'close', 'pause', 'resume' + * (fn NULL), and 'error' (fn: the error adapter). Listeners added after + * 'close' was emitted never run. */ +void scr_rl_on(double id, double ev, ScrClosure *cb /*moves*/, void *fn, bool once) { + ScrRl *rl = scr_rl_find(id); + if (!rl || ev < 0 || ev >= RL_EV_COUNT) { + scr_closure_release(cb); + return; + } + scr_rl_add(rl, (int)ev, cb, fn, once, false); +} + +/* ── the runtime-provided adapters ─────────────────────────────────── */ + +/* A zero-param line/answer callback: the text is ignored. */ void scr_rl_answer_thunk0(ScrClosure *cb, ScrStr *answer) { scr_str_release(answer); ((void (*)(ScrClosure *))cb->fn)(cb); @@ -286,3 +1010,26 @@ void scr_rl_answer_thunk0(ScrClosure *cb, ScrStr *answer) { void scr_rl_answer_thunk_str(ScrClosure *cb, ScrStr *answer) { ((void (*)(ScrClosure *, ScrStr *))cb->fn)(cb, answer); } + +/* An (err: Error) listener. */ +void scr_rl_error_thunk(ScrClosure *cb, ScrError *err) { + ((void (*)(ScrClosure *, ScrError *))cb->fn)(cb, scr_error_retain(err)); +} + +/* JavaScript listeners take checked-dynamic values (the adapter owns + * the +1 dyn parameter it passes). */ +void scr_rl_answer_thunk_dyn(ScrClosure *cb, ScrStr *answer) { + ScrDyn *d = scr_dyn_new_str(answer); + scr_str_release(answer); + ((void (*)(ScrClosure *, ScrDyn *))cb->fn)(cb, d); +} + +void scr_rl_error_thunk_dyn(ScrClosure *cb, ScrError *err) { + ScrDyn *d = scr_dyn_from_error(err); + ((void (*)(ScrClosure *, ScrDyn *))cb->fn)(cb, d); +} + +void scr_rl_error_thunk0(ScrClosure *cb, ScrError *err) { + (void)err; + ((void (*)(ScrClosure *))cb->fn)(cb); +} diff --git a/packages/runtime/src/scr_runtime.h b/packages/runtime/src/scr_runtime.h index 3e5817177..1179f5f66 100644 --- a/packages/runtime/src/scr_runtime.h +++ b/packages/runtime/src/scr_runtime.h @@ -2992,6 +2992,10 @@ extern SCR_TL void (*scr_emitter_on_hook)(ScrEmitter *em, ScrStr *name); * are called by compiler-emitted completion-callback closures. */ typedef struct ScrStreamState ScrStreamState; typedef struct ScrBytes ScrBytes; /* full definition below (C11 repeat) */ +/* events.addAbortListener (scr_events_add_abort_listener): passed to the + * signal-taking stream and readline entry points by programs that link + * the event-target unit. */ +typedef ScrDyn *(*ScrAbortAddFn)(ScrDyn *signal, ScrDyn *listener); typedef struct ScrStream { size_t rc; @@ -3227,6 +3231,30 @@ ScrStream *scr_stream_destroy(ScrStream *s, ScrError *err); /* recv +1 */ * the iterator's internal error consumer suppresses an unhandled crash when * no user error listener exists. Receiver +1. */ ScrStream *scr_stream_iterator_close(ScrStream *s); +/* stream.addAbortSignal(signal, s) and the constructors' signal option + * (from_option: a falsy signal is ignored). Borrows; answers s +1, or + * NULL with ERR_INVALID_ARG_TYPE pending. */ +ScrStream *scr_stream_add_abort_signal(ScrStream *s, ScrDyn *signal, bool from_option, + ScrAbortAddFn add); +/* finished(s, options[, cb]) — Node's eos with its listeners and state + * predicates: readable/writable are -1 (the default), 0 or 1; on_error is + * options.error !== false; signal is NULL or borrowed (ERR_INVALID_ARG_TYPE + * pending and NULL answered for a non-signal). The callback forms answer + * the +1 cleanup closure (cb moves; the typed form takes its invoke + * adapter); the promise form rejects instead of throwing. */ +ScrClosure *scr_stream_finished_opts(ScrStream *s, ScrClosure *cb, ScrStreamErrInv inv, + double readable, double writable, bool on_error, + ScrDyn *signal, ScrAbortAddFn add); +ScrClosure *scr_stream_finished_opts_dyn(ScrStream *s, ScrDyn *cb, double readable, + double writable, bool on_error, ScrDyn *signal, + ScrAbortAddFn add); +ScrPromise *scr_sp_finished_opts(ScrStream *s, double readable, double writable, bool on_error, + bool cleanup, ScrDyn *signal, ScrAbortAddFn add); +/* stream/promises pipeline(...streams, { signal, end }): end false leaves + * the destination open; a non-signal value rejects with + * ERR_INVALID_ARG_TYPE. Streams borrowed. */ +ScrPromise *scr_sp_pipeline_opts(double n, ScrStream **streams, bool end, ScrDyn *signal, + ScrAbortAddFn add); double scr_stream_prop(ScrStream *s, const char *name); ScrError *scr_stream_errored(ScrStream *s); /* +1 or NULL */ @@ -6494,22 +6522,59 @@ bool scr_stdin_pending(void); void scr_stdin_remove_data(ScrClosure *cb); bool scr_stdin_ended(void); -/* ── node:readline (scr_readline.c, linked under the events gate) ────── - * The question/close slice, Node's semantics pinned under pipes (the file - * header has the full model): create answers the f64 interface handle; - * question writes the query to stdout and delivers the next line's text - * through the adapter (fn(cb, +1 answer)) — throwing Node's - * "readline was closed" on a closed interface (may-throw); close fires - * the 'close' listeners SYNCHRONOUSLY (Node's inline emit) and detaches - * the stdin consumer; onClose registers a zero-arg listener (moves). - * Answer adapters: thunk0 ignores the line, thunk_str passes it. */ -double scr_rl_create(void); -void scr_rl_question(double id, const ScrStr *query, ScrClosure *cb /*moves*/, +/* ── node:readline and node:readline/promises (scr_readline.c, linked + * under the events gate) ───────────────────────────────────────────── + * The non-terminal Interface (the file header has the model). Interfaces + * are f64 handles. A stream input or output goes through ScrRlIo, which + * scr_stream.c provides (scr_stream_rl_open), so stdin-only programs + * never link the stream unit; the stream side feeds input through + * scr_rl_input_*. Signal-taking entry points get addAbortListener as + * `add` (scr_events_add_abort_listener), passed only by programs that + * link the event-target unit. Listener event numbers: 0 'line', + * 1 'close', 2 'pause', 3 'resume', 4 'error'. */ +typedef struct ScrRlIo { + void (*pause)(void *s); + void (*resume)(void *s); + void (*detach)(void *s, double id); /* removes the interface's input listeners */ + void (*write)(void *s, ScrStr *text); /* borrows text */ + void (*release)(void *s); +} ScrRlIo; +double scr_rl_open(const ScrRlIo *io, void *in /*+1 or NULL: stdin*/, void *out /*+1 or NULL*/, + bool out_stdout, double crlf_delay, ScrStr *prompt, ScrDyn *signal, + ScrAbortAddFn add); +void scr_rl_input_bytes(double id, ScrBytes *chunk); +void scr_rl_input_str(double id, ScrStr *chunk); +void scr_rl_input_end(double id); +void scr_rl_input_error(double id, ScrError *err); +/* question: throws ERR_USE_AFTER_CLOSE on a closed interface (may-throw); + * fn(cb, +1 answer). The promise form never throws. */ +void scr_rl_question(double id, ScrStr *query, ScrClosure *cb /*moves*/, void (*fn)(ScrClosure *, ScrStr *)); +ScrPromise *scr_rl_question_promise(double id, ScrStr *query, ScrDyn *signal, ScrAbortAddFn add); void scr_rl_close(double id); -void scr_rl_on_close(double id, ScrClosure *cb /*moves*/); +void scr_rl_pause(double id); /* may throw ERR_USE_AFTER_CLOSE */ +void scr_rl_resume(double id); /* may throw ERR_USE_AFTER_CLOSE */ +void scr_rl_prompt(double id); /* may throw ERR_USE_AFTER_CLOSE */ +void scr_rl_set_prompt(double id, ScrStr *prompt); +ScrStr *scr_rl_get_prompt(double id); /* +1 */ +void scr_rl_write(double id, ScrStr *data); /* may throw ERR_USE_AFTER_CLOSE */ +void scr_rl_on(double id, double ev, ScrClosure *cb /*moves*/, void *fn, bool once); +void scr_rl_iter_open(double id); +ScrPromise *scr_rl_iter_next(double id); /* +1 promise: the line, or undefined */ +void scr_rl_iter_return(double id); +/* Listener adapters: line/answer (fn(cb, +1 text)) and error listeners. */ void scr_rl_answer_thunk0(ScrClosure *cb, ScrStr *answer); void scr_rl_answer_thunk_str(ScrClosure *cb, ScrStr *answer); +void scr_rl_error_thunk(ScrClosure *cb, ScrError *err); +void scr_rl_error_thunk0(ScrClosure *cb, ScrError *err); +void scr_rl_answer_thunk_dyn(ScrClosure *cb, ScrStr *answer); +void scr_rl_error_thunk_dyn(ScrClosure *cb, ScrError *err); +/* createInterface with a stream input and/or output (scr_stream.c): in is + * the input stream or NULL for process.stdin; out the output stream or + * NULL (out_stdout picks process.stdout). Both borrowed, like prompt + * (NULL: the default "> "). */ +double scr_stream_rl_open(ScrStream *in, ScrStream *out, bool out_stdout, double crlf_delay, + ScrStr *prompt, ScrDyn *signal, ScrAbortAddFn add); void scr_stdin_data_thunk0(ScrClosure *cb, ScrBytes *chunk); void scr_stdin_data_thunk_bytes(ScrClosure *cb, ScrBytes *chunk); /* The loop-side registration (scr_async.c, always linked) and the diff --git a/packages/runtime/src/scr_stream.c b/packages/runtime/src/scr_stream.c index 4ae08b2a8..b459038ab 100644 --- a/packages/runtime/src/scr_stream.c +++ b/packages/runtime/src/scr_stream.c @@ -2097,12 +2097,40 @@ void scr_stream_finished_dyn_inv(ScrClosure *cb, ScrStream *s, ScrError *err) { typedef struct { size_t rc; size_t n, closed; + size_t watched; /* stages with a watcher: all, or all but the last (end: false) */ ScrStream **streams; /* owned */ ScrClosure *cb; /* owned; NULL once fired */ ScrStreamErrInv inv; ScrError *err; /* first error, owned */ + ScrDyn *dispose; /* the signal listener's disposable (+1), or NULL */ + /* contexts holding a disposable: it holds the abort listener, which + * holds the context, so exit releases what a pipeline that never + * finished still holds */ + void *live_prev, *live_next; } ScrStreamPipeCtx; +static SCR_TL ScrStreamPipeCtx *scr_pipe_live = NULL; + +static void scr_pipe_unlink(ScrStreamPipeCtx *ctx) { + ScrStreamPipeCtx *prev = ctx->live_prev, *next = ctx->live_next; + if (prev) prev->live_next = next; + else if (scr_pipe_live == ctx) scr_pipe_live = next; + if (next) next->live_prev = prev; + ctx->live_prev = ctx->live_next = NULL; +} + +static void scr_pipe_atexit(void) { + while (scr_pipe_live) { + ScrStreamPipeCtx *ctx = scr_pipe_live; + scr_pipe_unlink(ctx); + if (ctx->dispose) { + ScrDyn *d = ctx->dispose; + ctx->dispose = NULL; + scr_dyn_release(d); + } + } +} + static void *scr_stream_pipectx_retain_v(void *p) { ((ScrStreamPipeCtx *)p)->rc++; return p; @@ -2115,24 +2143,38 @@ static void scr_stream_pipectx_release_v(void *p) { free(ctx->streams); if (ctx->cb) scr_closure_release(ctx->cb); if (ctx->err) scr_error_release(ctx->err); + if (ctx->dispose) scr_dyn_release(ctx->dispose); + scr_pipe_unlink(ctx); free(ctx); } +static void scr_stream_pipectx_dispose(ScrStreamPipeCtx *ctx) { + if (!ctx->dispose) return; + scr_pipe_unlink(ctx); + ScrDyn *d = ctx->dispose; + ctx->dispose = NULL; + ScrDyn *r = scr_dyn_call(d, NULL, 0, "dispose"); + if (r) scr_dyn_release(r); + scr_dyn_release(d); +} + static void scr_stream_pipeline_watch_inv(ScrClosure *cb, ScrStream *s, ScrError *err) { ScrStreamPipeCtx *ctx = scr_box_get_ref(cb->caps[0]); /* +1 */ ctx->closed++; if (err != NULL && ctx->err == NULL) { ctx->err = scr_error_retain(err); + scr_stream_pipectx_dispose(ctx); /* Node's pipeline destroyer: every other stream goes down with the * SAME error, pipeline order (each emits 'error' then 'close'). */ - for (size_t i = 0; i < ctx->n; i++) { + for (size_t i = 0; i < ctx->watched; i++) { if (ctx->streams[i] != s) { ScrStream *chain = scr_stream_destroy(ctx->streams[i], ctx->err); scr_stream_release(chain); } } } - if (ctx->closed == ctx->n && ctx->cb != NULL) { + if (ctx->closed == ctx->watched && ctx->cb != NULL) { + scr_stream_pipectx_dispose(ctx); ScrClosure *ucb = ctx->cb; ctx->cb = NULL; ctx->inv(ucb, ctx->streams[ctx->n - 1], ctx->err); @@ -2154,31 +2196,90 @@ ScrStream *scr_stream_pipeline_dyn(double n, ScrStream **streams, ScrDyn *cb) { return scr_stream_pipeline(n, streams, c, &scr_stream_finished_dyn_inv); } +static ScrError *scr_stream_abort_error(ScrDyn *signal); +static ScrDyn *scr_stream_signal_prop(ScrDyn *signal, const char *key); +static void *scr_stream_dyn_retain_v(void *p); +static void scr_stream_dyn_release_v(void *p); + +/* pipeline's signal: an abort destroys every watched stage with + * new AbortError(undefined, { cause: signal.reason }) (finishImpl's + * destroyers); the callback follows the last close. caps[0] the context, + * caps[1] the signal. */ +static ScrDyn *scr_stream_pipeline_abort_thunk(ScrClosure *clo, ScrDyn *const *args, size_t argc) { + (void)args; + (void)argc; + ScrStreamPipeCtx *ctx = scr_box_get_ref(clo->caps[0]); /* +1 */ + ScrDyn *signal = scr_box_get_ref(clo->caps[1]); /* +1 */ + if (ctx->err == NULL && ctx->cb != NULL) { + ctx->err = scr_stream_abort_error(signal); + scr_stream_pipectx_dispose(ctx); + for (size_t i = 0; i < ctx->watched; i++) { + ScrStream *chain = scr_stream_destroy(ctx->streams[i], ctx->err); + scr_stream_release(chain); + } + } + scr_dyn_release(signal); + scr_stream_pipectx_release_v(ctx); + return scr_dyn_retain(scr_dyn_undefined()); +} + +static ScrStream *scr_stream_pipeline_ex(size_t n, ScrStream **streams, ScrClosure *cb, + ScrStreamErrInv inv, bool end, ScrDyn *signal, + ScrAbortAddFn add); + ScrStream *scr_stream_pipeline(double n_d, ScrStream **streams /*borrowed*/, ScrClosure *cb /*moves*/, ScrStreamErrInv inv) { - size_t n = (size_t)n_d; + return scr_stream_pipeline_ex((size_t)n_d, streams, cb, inv, true, NULL, NULL); +} + +static ScrStream *scr_stream_pipeline_ex(size_t n, ScrStream **streams, ScrClosure *cb, + ScrStreamErrInv inv, bool end, ScrDyn *signal, + ScrAbortAddFn add) { ScrStreamPipeCtx *ctx = calloc(1, sizeof *ctx); if (!ctx) scr_stream_oom(); ctx->rc = 1; ctx->n = n; + ctx->watched = end ? n : n - 1; ctx->streams = calloc(n, sizeof *ctx->streams); if (!ctx->streams) scr_stream_oom(); for (size_t i = 0; i < n; i++) ctx->streams[i] = scr_stream_retain(streams[i]); ctx->cb = cb; ctx->inv = inv; - /* Chain the pipes first (end: true — Node's pipeline forwards EOF). */ + /* Chain the pipes first (Node's pipeline forwards EOF; end: false + * leaves the destination open). */ for (size_t i = 0; i + 1 < n; i++) { - ScrStream *d = scr_stream_pipe(streams[i], streams[i + 1], true); + ScrStream *d = scr_stream_pipe(streams[i], streams[i + 1], end || i + 2 < n); scr_stream_release(d); } - /* One watcher per stream: the terminal statuses drive error propagation - * and the final callback (after the LAST 'close'). */ - for (size_t i = 0; i < n; i++) { + /* One watcher per stream (the destination is not watched under end: + * false): the terminal statuses drive error propagation and the final + * callback (after the LAST 'close'). */ + for (size_t i = 0; i < ctx->watched; i++) { ScrClosure *w = scr_closure_new((void *)&scr_stream_pipeline_watch_inv, 1); w->caps[0] = scr_box_new_obj(&scr_stream_pipectx_retain_v, &scr_stream_pipectx_release_v, NULL); scr_box_set_ref(w->caps[0], scr_stream_pipectx_retain_v(ctx)); scr_stream_fin_add(streams[i]->st, w, &scr_stream_pipeline_watch_inv); } + if (signal) { + ScrClosure *clo = scr_closure_new((void *)&scr_stream_pipeline_abort_thunk, 2); + clo->caps[0] = scr_box_new_obj(&scr_stream_pipectx_retain_v, &scr_stream_pipectx_release_v, NULL); + scr_box_set_ref(clo->caps[0], scr_stream_pipectx_retain_v(ctx)); + clo->caps[1] = scr_box_new_obj(&scr_stream_dyn_retain_v, &scr_stream_dyn_release_v, NULL); + scr_box_set_ref(clo->caps[1], scr_dyn_retain(signal)); + ScrDyn *listener = scr_dyn_new_func(clo, &scr_stream_pipeline_abort_thunk, 0, "()", "abort"); + ctx->dispose = add(signal, listener); + scr_dyn_release(listener); + if (ctx->dispose) { + static SCR_TL bool registered = false; + if (!registered) { + registered = true; + scr_atexit(scr_pipe_atexit); + } + ctx->live_next = scr_pipe_live; + if (scr_pipe_live) scr_pipe_live->live_prev = ctx; + scr_pipe_live = ctx; + } + } scr_stream_pipectx_release_v(ctx); /* the boxes own it now */ return scr_stream_retain(streams[n - 1]); } @@ -2226,6 +2327,32 @@ ScrPromise *scr_sp_pipeline(double n, ScrStream **streams) { return p; } +/* pipeline(...streams, { signal, end }): end false leaves the destination + * open (the promise settles once the stages before it finish); signal is + * NULL or borrowed — a value that is not an AbortSignal rejects with + * ERR_INVALID_ARG_TYPE before anything is piped. */ +ScrPromise *scr_sp_pipeline_opts(double n, ScrStream **streams, bool end, ScrDyn *signal, + ScrAbortAddFn add) { + ScrPromise *p = scr_promise_new(); + if (signal && scr_dyn_is_nullish(signal)) signal = NULL; + if (signal) { + ScrDyn *aborted = NULL; + if (signal->kind == SCR_DYN_OBJ || signal->kind == SCR_DYN_HANDLE) { + aborted = scr_stream_signal_prop(signal, "aborted"); + } + if (!aborted) { + scr_dyn_prop_type_fail("options.signal", "an instance of AbortSignal", signal); + scr_promise_reject_pending(p); + return p; + } + scr_dyn_release(aborted); + } + ScrStream *dst = scr_stream_pipeline_ex((size_t)n, streams, scr_sp_watcher(p), + &scr_sp_settle_inv, end, signal, add); + scr_stream_release(dst); + return p; +} + /* ── node:stream/consumers ──────────────────────────────────────────── * text/json/buffer/arrayBuffer over the readable machinery: a native 'data' listener * accumulates every chunk (Buffer chunks as-is; string chunks — an @@ -2369,6 +2496,671 @@ ScrPromise *scr_sc_json(ScrStream *s) { return scr_sc_consume(s, SCR_SC_JSON); } ScrPromise *scr_sc_buffer(ScrStream *s) { return scr_sc_consume(s, SCR_SC_BUFFER); } ScrPromise *scr_sc_array_buffer(ScrStream *s) { return scr_sc_consume(s, SCR_SC_ARRAY_BUFFER); } +/* ── stream.addAbortSignal and the constructors' signal option ───────── + * lib/internal/streams/add-abort-signal.js: the signal must be an object + * with an `aborted` property (ERR_INVALID_ARG_TYPE otherwise). An aborted + * signal destroys the stream now with new AbortError(undefined, { cause: + * signal.reason }); otherwise an abort listener does it later, and the + * listener is removed when the stream finishes (eos — whose own 'error' + * listener also marks the stream's errors handled). The signal's + * properties are read through the generic checked-dynamic getters, and + * addAbortListener arrives as `add` from the event-target unit, so this + * unit links without it. */ + +static ScrDyn *scr_stream_signal_prop(ScrDyn *signal, const char *key) { + size_t n = strlen(key); + if (signal->kind == SCR_DYN_OBJ) { + ScrDyn *v = scr_dyn_obj_get(signal, key, n); + return v ? scr_dyn_retain(v) : NULL; + } + ScrStr *k = scr_str_new(key, n); + ScrDyn *v = scr_dyn_handle_key_get(signal, k); + scr_str_release(k); + return v; +} + +static ScrError *scr_stream_abort_error(ScrDyn *signal) { + ScrStr *message = scr_str_new("The operation was aborted", 25); + ScrError *e = scr_error_new(SCR_ERR_ERROR, message); + scr_str_release(message); + scr_str_release(e->name); + e->name = scr_str_new("AbortError", 10); + /* AbortError assigns code, then name: own enumerable properties */ + scr_error_set_code(e, "ABORT_ERR"); + e->name_present = true; + e->name_enumerable = true; + ScrDyn *reason = scr_stream_signal_prop(signal, "reason"); + scr_error_define_cause(e, reason ? reason : scr_dyn_undefined()); + if (reason) scr_dyn_release(reason); + return e; +} + +static void scr_stream_abort_now(ScrStream *s, ScrDyn *signal) { + ScrError *e = scr_stream_abort_error(signal); + scr_stream_release(scr_stream_destroy(s, e)); + scr_error_release(e); +} + +/* onAbort: caps[0] the stream, caps[1] the signal. */ +static ScrDyn *scr_stream_abort_thunk(ScrClosure *clo, ScrDyn *const *args, size_t argc) { + (void)args; + (void)argc; + ScrStream *s = scr_box_get_ref(clo->caps[0]); /* +1, NULL after exit teardown */ + ScrDyn *signal = scr_box_get_ref(clo->caps[1]); /* +1 */ + if (s) scr_stream_abort_now(s, signal); + scr_dyn_release(signal); + if (s) scr_stream_release(s); + return scr_dyn_retain(scr_dyn_undefined()); +} + +/* A pending abort hook: the listener closure (borrowed) holds the stream, + * and the stream's watcher holds the listener through the disposable — a + * cycle the stream's finish breaks. Hooks still pending at exit drop the + * listener's stream so the stream can be freed. */ +typedef struct ScrAbortHook { + ScrClosure *listener; /* borrowed */ + struct ScrAbortHook *prev, *next; +} ScrAbortHook; + +static SCR_TL ScrAbortHook *scr_abort_hooks = NULL; + +static void scr_abort_hook_unlink(ScrAbortHook *h) { + if (h->prev) h->prev->next = h->next; + else if (scr_abort_hooks == h) scr_abort_hooks = h->next; + if (h->next) h->next->prev = h->prev; + free(h); +} + +static void scr_abort_hooks_atexit(void) { + while (scr_abort_hooks) { + ScrAbortHook *h = scr_abort_hooks; + scr_box_set_ref(h->listener->caps[0], NULL); + scr_abort_hook_unlink(h); + } +} + +static void *scr_stream_ptr_keep_v(void *p) { return p; } +static void scr_stream_ptr_drop_v(void *p) { (void)p; } + +/* The eos callback: the disposable (caps[0]) removes the abort listener; + * caps[1] is the pending hook. */ +static void scr_stream_abort_dispose_inv(ScrClosure *cb, ScrStream *s, ScrError *err) { + (void)s; + (void)err; + ScrAbortHook *h = scr_box_get_ref(cb->caps[1]); + if (h) { + scr_box_set_ref(cb->caps[1], NULL); + scr_abort_hook_unlink(h); + } + ScrDyn *dispose = scr_box_get_ref(cb->caps[0]); /* +1 */ + ScrDyn *r = scr_dyn_call(dispose, NULL, 0, "dispose"); + if (r) scr_dyn_release(r); + scr_dyn_release(dispose); +} + +static void *scr_stream_dyn_retain_v(void *p) { return p ? scr_dyn_retain(p) : NULL; } +static void scr_stream_dyn_release_v(void *p) { scr_dyn_release(p); } + +/* addAbortSignal(signal, stream), or the `signal` option (from_option: + * a falsy value is ignored, Node's `if (options.signal)`). Borrows both; + * answers the stream +1, or NULL with ERR_INVALID_ARG_TYPE pending. */ +ScrStream *scr_stream_add_abort_signal(ScrStream *s, ScrDyn *signal, bool from_option, + ScrAbortAddFn add) { + if (from_option && (signal == NULL || !scr_dyn_truthy(signal))) return scr_stream_retain(s); + if (signal && signal->kind == SCR_DYN_NULL) { + /* typeof null is 'object': the `in` test itself throws */ + static const char m[] = "Cannot use 'in' operator to search for 'aborted' in null"; + scr_throw_error_msg(SCR_ERR_TYPE, m, sizeof m - 1); + return NULL; + } + ScrDyn *aborted = NULL; + if (signal && (signal->kind == SCR_DYN_OBJ || signal->kind == SCR_DYN_HANDLE)) { + aborted = scr_stream_signal_prop(signal, "aborted"); + } + if (aborted == NULL) { + scr_dyn_arg_type_fail("signal", "an instance of AbortSignal", + signal ? signal : scr_dyn_undefined()); + return NULL; + } + bool is_aborted = scr_dyn_truthy(aborted); + scr_dyn_release(aborted); + if (is_aborted) { + scr_stream_abort_now(s, signal); + return scr_stream_retain(s); + } + ScrClosure *clo = scr_closure_new((void *)&scr_stream_abort_thunk, 2); + clo->caps[0] = scr_box_new_obj(&scr_stream_retain_v, &scr_stream_release_v, &scr_stream_trace); + scr_box_set_ref(clo->caps[0], scr_stream_retain(s)); + clo->caps[1] = scr_box_new_obj(&scr_stream_dyn_retain_v, &scr_stream_dyn_release_v, NULL); + scr_box_set_ref(clo->caps[1], scr_dyn_retain(signal)); + ScrDyn *listener = scr_dyn_new_func(clo, &scr_stream_abort_thunk, 0, "()", "onAbort"); + ScrDyn *dispose = add(signal, listener); + scr_dyn_release(listener); + if (dispose == NULL) return NULL; + static SCR_TL bool registered = false; + if (!registered) { + registered = true; + scr_atexit(scr_abort_hooks_atexit); + } + ScrAbortHook *h = calloc(1, sizeof *h); + if (!h) scr_stream_oom(); + h->listener = clo; + h->next = scr_abort_hooks; + if (scr_abort_hooks) scr_abort_hooks->prev = h; + scr_abort_hooks = h; + ScrClosure *w = scr_closure_new((void *)&scr_stream_abort_dispose_inv, 2); + w->caps[0] = scr_box_new_obj(&scr_stream_dyn_retain_v, &scr_stream_dyn_release_v, NULL); + scr_box_set_ref(w->caps[0], dispose); /* moves */ + w->caps[1] = scr_box_new_obj(&scr_stream_ptr_keep_v, &scr_stream_ptr_drop_v, NULL); + scr_box_set_ref(w->caps[1], h); + scr_closure_release(scr_stream_finished(s, w, &scr_stream_abort_dispose_inv)); + return scr_stream_retain(s); +} + +/* ── node:readline over stream inputs and outputs ───────────────────── + * The Interface's stream side (scr_readline.c has the model): 'data', + * 'end' and 'error' listeners on the input, registered in that order like + * Node's constructor ('error' first there; it is never observable before + * the others), removed again when the interface closes, and prompt + * writes to the output. The input resumes at creation (input.resume()). */ + +typedef struct ScrRlHook { + ScrStream *s; /* borrowed: the interface holds the stream */ + double id; + ScrClosure *data, *end, *error; /* borrowed: the emitter owns them */ + struct ScrRlHook *next; +} ScrRlHook; + +static SCR_TL ScrRlHook *scr_rl_hooks = NULL; + +static double scr_stream_rl_id(ScrClosure *cb) { return scr_box_get_f64(cb->caps[0]); } + +static void scr_stream_rl_data(ScrClosure *cb, void *b, void *str) { + if (b != NULL) scr_rl_input_bytes(scr_stream_rl_id(cb), (ScrBytes *)b); + else if (str != NULL) scr_rl_input_str(scr_stream_rl_id(cb), (ScrStr *)str); +} + +static void scr_stream_rl_end(ScrClosure *cb) { scr_rl_input_end(scr_stream_rl_id(cb)); } + +static void scr_stream_rl_error(ScrClosure *cb, void *err) { + scr_rl_input_error(scr_stream_rl_id(cb), (ScrError *)err); +} + +static ScrClosure *scr_stream_rl_listener(void *fn, double id) { + ScrClosure *c = scr_closure_new(fn, 1); + c->caps[0] = scr_box_new(SCR_BOX_F64); + scr_box_set_f64(c->caps[0], id); + return c; +} + +static void scr_stream_rl_on(ScrStream *s, const char *name, ScrClosure *cb, ScrEeInvoke inv) { + ScrStr *n = scr_str_new(name, strlen(name)); + scr_emitter_release(scr_emitter_on((ScrEmitter *)s, n, cb, inv, false, false)); + scr_str_release(n); +} + +static void scr_stream_rl_off(ScrStream *s, const char *name, ScrClosure *cb) { + ScrStr *n = scr_str_new(name, strlen(name)); + scr_emitter_release(scr_emitter_off((ScrEmitter *)s, n, cb)); + scr_str_release(n); +} + +static void scr_stream_rl_pause(void *s) { scr_stream_release(scr_stream_pause(s)); } +static void scr_stream_rl_resume(void *s) { scr_stream_release(scr_stream_resume(s)); } +static void scr_stream_rl_write(void *s, ScrStr *text) { scr_stream_write_str(s, text, NULL); } +static void scr_stream_rl_release(void *s) { scr_stream_release(s); } + +static void scr_stream_rl_detach(void *s, double id) { + for (ScrRlHook **at = &scr_rl_hooks; *at; at = &(*at)->next) { + ScrRlHook *h = *at; + if (h->s != s || h->id != id) continue; + *at = h->next; + scr_stream_rl_off(h->s, "data", h->data); + scr_stream_rl_off(h->s, "error", h->error); + scr_stream_rl_off(h->s, "end", h->end); + free(h); + return; + } +} + +static const ScrRlIo scr_stream_rl_io = { + &scr_stream_rl_pause, &scr_stream_rl_resume, &scr_stream_rl_detach, + &scr_stream_rl_write, &scr_stream_rl_release, +}; + +static void scr_stream_rl_hooks_free(void) { + while (scr_rl_hooks) { + ScrRlHook *h = scr_rl_hooks; + scr_rl_hooks = h->next; + free(h); + } +} + +double scr_stream_rl_open(ScrStream *in, ScrStream *out, bool out_stdout, double crlf_delay, + ScrStr *prompt, ScrDyn *signal, ScrAbortAddFn add) { + double id = scr_rl_open(&scr_stream_rl_io, in ? scr_stream_retain(in) : NULL, + out ? scr_stream_retain(out) : NULL, out_stdout, crlf_delay, prompt, + signal, add); + if (in == NULL) return id; + static SCR_TL bool registered = false; + if (!registered) { + registered = true; + scr_atexit(scr_stream_rl_hooks_free); + } + ScrRlHook *h = calloc(1, sizeof *h); + if (!h) scr_stream_oom(); + h->s = in; + h->id = id; + h->error = scr_stream_rl_listener((void *)&scr_stream_rl_error, id); + h->data = scr_stream_rl_listener((void *)&scr_stream_rl_data, id); + h->end = scr_stream_rl_listener((void *)&scr_stream_rl_end, id); + h->next = scr_rl_hooks; + scr_rl_hooks = h; + scr_stream_rl_on(in, "error", h->error, scr_ee_inv_fixed1); + scr_stream_rl_on(in, "data", h->data, scr_ee_inv_fixed2); + scr_stream_rl_on(in, "end", h->end, scr_ee_inv_fixed0); + scr_stream_rl_resume(in); + return id; +} + +/* ── finished(stream, options) (lib/internal/streams/end-of-stream.js) ── + * The options form registers Node's own listeners on the stream — 'end', + * 'finish', 'error' (unless options.error is false) and 'close', in that + * order — and decides with Node's predicates over the stream state: + * options.readable/writable (default: whether the stream has that side) + * choose which ends count, and willEmitClose (autoDestroy, emitClose, + * not yet closed, and both sides counted) defers the callback to 'close'. + * options.signal: an abort removes the listeners and calls back with + * new AbortError(undefined, { cause: signal.reason }). The callback runs + * once; the answered cleanup closure removes the listeners. */ + +typedef struct ScrEos { + size_t rc; + /* Borrowed: the stream's listeners hold the context, so the context + * must not hold the stream; every closure that can run after the + * listeners are gone (ticks, the abort listener, cleanup) holds the + * stream itself. */ + ScrStream *s; + bool pending; /* the callback has not run */ + ScrClosure *cb; /* owned until it runs or cleanup drops it (callback form) */ + ScrStreamErrInv inv; + bool readable, writable; + bool will_emit_close; + bool readable_finished, writable_finished; + bool on_error; + ScrClosure *l_end, *l_finish, *l_error, *l_close; /* emitter-owned */ + ScrDyn *dispose; /* the signal listener's disposable (+1) */ + ScrDyn *signal; /* +1 */ + ScrPromise *promise; /* the promise form (+1) */ + bool auto_cleanup; + struct ScrEos *live_prev, *live_next; /* pending contexts, released at exit */ +} ScrEos; + +static SCR_TL ScrEos *scr_eos_live = NULL; + +static void scr_eos_unlink(ScrEos *e) { + if (e->live_prev) e->live_prev->live_next = e->live_next; + else if (scr_eos_live == e) scr_eos_live = e->live_next; + if (e->live_next) e->live_next->live_prev = e->live_prev; + e->live_prev = e->live_next = NULL; +} + +/* The callback and promise a context still holds: dropped once it + * settles, is cleaned up, or the program exits. */ +static void scr_eos_drop(ScrEos *e) { + e->pending = false; + scr_eos_unlink(e); + if (e->cb) { + scr_closure_release(e->cb); + e->cb = NULL; + } + if (e->promise) { + scr_promise_release(e->promise); + e->promise = NULL; + } +} + +static void *scr_eos_retain_v(void *p); +static void scr_eos_release_v(void *p); + +/* At exit the disposable goes too: it holds the abort listener, which + * holds this context. */ +static void scr_eos_atexit(void) { + while (scr_eos_live) { + ScrEos *e = scr_eos_retain_v(scr_eos_live); + ScrDyn *d = e->dispose; + e->dispose = NULL; + scr_eos_drop(e); + if (d) scr_dyn_release(d); + scr_eos_release_v(e); + } +} + +static void *scr_eos_retain_v(void *p) { + ((ScrEos *)p)->rc++; + return p; +} + +static void scr_eos_release_v(void *p) { + ScrEos *e = p; + if (--e->rc > 0) return; + if (e->cb) scr_closure_release(e->cb); + if (e->dispose) scr_dyn_release(e->dispose); + if (e->signal) scr_dyn_release(e->signal); + if (e->promise) scr_promise_release(e->promise); + scr_eos_unlink(e); + free(e); +} + +/* A closure over the context; `keep` also holds the stream. */ +static ScrClosure *scr_eos_closure_k(void *fn, ScrEos *e, bool keep) { + ScrClosure *c = scr_closure_new(fn, keep ? 2 : 1); + c->caps[0] = scr_box_new_obj(&scr_eos_retain_v, &scr_eos_release_v, NULL); + scr_box_set_ref(c->caps[0], scr_eos_retain_v(e)); + if (keep) { + c->caps[1] = scr_box_new_obj(&scr_stream_retain_v, &scr_stream_release_v, &scr_stream_trace); + scr_box_set_ref(c->caps[1], scr_stream_retain(e->s)); + } + return c; +} + +static ScrClosure *scr_eos_closure(void *fn, ScrEos *e) { return scr_eos_closure_k(fn, e, false); } + +static ScrEos *scr_eos_of(ScrClosure *c) { return scr_box_get_ref(c->caps[0]); /* +1 */ } + +/* Node's predicates (internal/streams/utils.js) over the state. */ +static bool scr_eos_closed(const ScrStreamState *st) { return st->close_scheduled || st->close_emitted; } +static bool scr_eos_readable_prop(const ScrStreamState *st) { + return st->has_r && !st->destroyed && !st->error_emitted && !st->r.end_emitted; +} +static bool scr_eos_writable_prop(const ScrStreamState *st) { + return st->has_w && !st->destroyed && !st->errored && !st->w.ending; +} +static bool scr_eos_readable_finished(const ScrStreamState *st, bool strict) { + if (!st->has_r || st->errored) return false; + return st->r.end_emitted || (!strict && st->r.ended && st->r.length == 0); +} +static bool scr_eos_writable_finished(const ScrStreamState *st, bool strict) { + if (!st->has_w) return false; + if (st->w.finished) return true; + if (st->errored) return false; + return !strict && st->w.ending && st->w.length == 0; +} +/* isReadable/isWritable: -1 for null (no such property), else 0/1. */ +static int scr_eos_is_readable(const ScrStreamState *st) { + if (!st->has_r) return -1; + if (st->destroyed) return 0; + return scr_eos_readable_prop(st) && !st->r.end_emitted; +} +static int scr_eos_is_writable(const ScrStreamState *st) { + if (!st->has_w) return -1; + if (st->destroyed) return 0; + return scr_eos_writable_prop(st) && !st->w.ending; +} + +static void scr_eos_unlisten(ScrEos *e) { + ScrStream *s = e->s; + if (e->l_end) scr_stream_rl_off(s, "end", e->l_end); + if (e->l_finish) scr_stream_rl_off(s, "finish", e->l_finish); + if (e->l_error) scr_stream_rl_off(s, "error", e->l_error); + if (e->l_close) scr_stream_rl_off(s, "close", e->l_close); + e->l_end = e->l_finish = e->l_error = e->l_close = NULL; +} + +static void scr_eos_dispose_signal(ScrEos *e) { + if (!e->dispose) return; + ScrDyn *d = e->dispose; + e->dispose = NULL; + ScrDyn *r = scr_dyn_call(d, NULL, 0, "dispose"); + if (r) scr_dyn_release(r); + scr_dyn_release(d); +} + +static void scr_eos_settle(ScrEos *e, ScrError *err); + +/* callback.call(stream, err): once; borrows err. */ +static void scr_eos_fire(ScrEos *e, ScrError *err) { + if (!e->pending) return; + scr_eos_dispose_signal(e); + scr_eos_settle(e, err); +} + +/* Runs the callback (or settles the promise) and drops them. */ +static void scr_eos_settle(ScrEos *e, ScrError *err) { + scr_eos_retain_v(e); + ScrClosure *cb = e->cb; + ScrPromise *p = e->promise; + e->cb = NULL; + e->promise = NULL; + e->pending = false; + scr_eos_unlink(e); + if (p) { + if (e->auto_cleanup) scr_eos_unlisten(e); + if (err != NULL) { + scr_throw_obj(scr_error_retain(err), &scr_error_retain_v, &scr_error_release_v, + scr_error_trace_arg()); + scr_promise_reject_pending(p); + } else { + scr_promise_fulfill_void(p); + } + scr_promise_release(p); + } else if (cb) { + e->inv(cb, e->s, err); + scr_closure_release(cb); + } + scr_eos_release_v(e); +} + +static void scr_eos_premature(ScrEos *e) { + ScrError *err = scr_stream_mkerr("ERR_STREAM_PREMATURE_CLOSE", "Premature close"); + scr_eos_fire(e, err); + scr_error_release(err); +} + +static void scr_eos_onfinish(ScrClosure *c) { + ScrEos *e = scr_eos_of(c); + ScrStreamState *st = e->s->st; + e->writable_finished = true; + if (st->destroyed) e->will_emit_close = false; + if (!(e->will_emit_close && (!scr_eos_readable_prop(st) || e->readable))) { + if (!e->readable || e->readable_finished) scr_eos_fire(e, NULL); + } + scr_eos_release_v(e); +} + +static void scr_eos_onend(ScrClosure *c) { + ScrEos *e = scr_eos_of(c); + ScrStreamState *st = e->s->st; + e->readable_finished = true; + if (st->destroyed) e->will_emit_close = false; + if (!(e->will_emit_close && (!scr_eos_writable_prop(st) || e->writable))) { + if (!e->writable || e->writable_finished) scr_eos_fire(e, NULL); + } + scr_eos_release_v(e); +} + +static void scr_eos_onerror(ScrClosure *c, void *err) { + ScrEos *e = scr_eos_of(c); + scr_eos_fire(e, (ScrError *)err); + scr_eos_release_v(e); +} + +static void scr_eos_onclose(ScrClosure *c) { + ScrEos *e = scr_eos_of(c); + ScrStreamState *st = e->s->st; + if (st->errored) { + scr_eos_fire(e, st->errored); + } else if (e->readable && !e->readable_finished && st->has_r && + !scr_eos_readable_finished(st, false)) { + scr_eos_premature(e); + } else if (e->writable && !e->writable_finished && !scr_eos_writable_finished(st, false)) { + scr_eos_premature(e); + } else { + scr_eos_fire(e, NULL); + } + scr_eos_release_v(e); +} + +static void scr_eos_onclosed(ScrClosure *c) { + ScrEos *e = scr_eos_of(c); + ScrStreamState *st = e->s->st; + scr_eos_fire(e, st->errored); + scr_eos_release_v(e); +} + +/* abort: the listeners go (cleanup keeps the callback for this call), + * then the callback runs with the AbortError. */ +static void scr_eos_abort(ScrEos *e) { + if (!e->pending) return; + scr_eos_unlisten(e); + scr_eos_dispose_signal(e); + ScrError *err = scr_stream_abort_error(e->signal); + scr_eos_settle(e, err); + scr_error_release(err); +} + +static void scr_eos_abort_tick(ScrClosure *c) { + ScrEos *e = scr_eos_of(c); + scr_eos_abort(e); + scr_eos_release_v(e); +} + +static ScrDyn *scr_eos_abort_thunk(ScrClosure *c, ScrDyn *const *args, size_t argc) { + (void)args; + (void)argc; + ScrEos *e = scr_eos_of(c); + scr_eos_abort(e); + scr_eos_release_v(e); + return scr_dyn_retain(scr_dyn_undefined()); +} + +static void scr_eos_cleanup_fn(ScrClosure *c) { + ScrEos *e = scr_eos_of(c); + scr_eos_unlisten(e); + scr_eos_dispose_signal(e); + if (e->pending) scr_eos_drop(e); + scr_eos_release_v(e); +} + +/* readable/writable: -1 for the default; on_error: options.error !== + * false; signal: NULL when absent (borrowed). cb moves. Answers the +1 + * cleanup closure, or NULL with ERR_INVALID_ARG_TYPE pending for a signal + * that is not an AbortSignal. */ +static ScrClosure *scr_stream_eos_impl(ScrStream *s, ScrClosure *cb, ScrStreamErrInv inv, + ScrPromise *p, bool auto_cleanup, int readable, + int writable, bool on_error, ScrDyn *signal, + ScrAbortAddFn add) { + ScrStreamState *st = s->st; + static SCR_TL bool registered = false; + if (!registered) { + registered = true; + scr_atexit(scr_eos_atexit); + } + if (signal && !scr_dyn_is_nullish(signal)) { + ScrDyn *aborted = NULL; + if (signal->kind == SCR_DYN_OBJ || signal->kind == SCR_DYN_HANDLE) { + aborted = scr_stream_signal_prop(signal, "aborted"); + } + if (!aborted) { + if (cb) scr_closure_release(cb); + scr_dyn_prop_type_fail("options.signal", "an instance of AbortSignal", signal); + return NULL; + } + scr_dyn_release(aborted); + } else { + signal = NULL; + } + ScrEos *e = calloc(1, sizeof *e); + if (!e) scr_stream_oom(); + e->rc = 1; + e->s = s; + e->pending = true; + e->cb = cb; + e->inv = inv; + e->promise = p ? scr_promise_retain(p) : NULL; + e->auto_cleanup = auto_cleanup; + e->live_next = scr_eos_live; + if (scr_eos_live) scr_eos_live->live_prev = e; + scr_eos_live = e; + e->readable = readable < 0 ? st->has_r : readable != 0; + e->writable = writable < 0 ? st->has_w : writable != 0; + e->on_error = on_error; + e->will_emit_close = st->auto_destroy && st->emit_close && !scr_eos_closed(st) && + st->has_r == e->readable && st->has_w == e->writable; + e->writable_finished = scr_eos_writable_finished(st, false); + e->readable_finished = scr_eos_readable_finished(st, false); + e->l_end = scr_eos_closure((void *)&scr_eos_onend, e); + e->l_finish = scr_eos_closure((void *)&scr_eos_onfinish, e); + e->l_close = scr_eos_closure((void *)&scr_eos_onclose, e); + scr_stream_rl_on(s, "end", e->l_end, scr_ee_inv_fixed0); + scr_stream_rl_on(s, "finish", e->l_finish, scr_ee_inv_fixed0); + if (on_error) { + e->l_error = scr_eos_closure((void *)&scr_eos_onerror, e); + scr_stream_rl_on(s, "error", e->l_error, scr_ee_inv_fixed1); + } + scr_stream_rl_on(s, "close", e->l_close, scr_ee_inv_fixed0); + bool closed = scr_eos_closed(st); + if (closed) { + scr_next_tick(scr_eos_closure_k((void *)&scr_eos_onclose, e, true)); + } else if (st->error_emitted) { + if (!e->will_emit_close) scr_next_tick(scr_eos_closure_k((void *)&scr_eos_onclosed, e, true)); + } else if (!e->readable && (!e->will_emit_close || scr_eos_is_readable(st) == 1) && + (e->writable_finished || scr_eos_is_writable(st) == 0) && + (!st->has_w || (!st->w.writing && st->w.head == st->w.n))) { + scr_next_tick(scr_eos_closure_k((void *)&scr_eos_onclosed, e, true)); + } else if (!e->writable && (!e->will_emit_close || scr_eos_is_writable(st) == 1) && + (e->readable_finished || scr_eos_is_readable(st) == 0)) { + scr_next_tick(scr_eos_closure_k((void *)&scr_eos_onclosed, e, true)); + } + if (signal && !closed) { + e->signal = scr_dyn_retain(signal); + ScrDyn *aborted = scr_stream_signal_prop(signal, "aborted"); + bool is_aborted = aborted && scr_dyn_truthy(aborted); + if (aborted) scr_dyn_release(aborted); + if (is_aborted) { + scr_next_tick(scr_eos_closure_k((void *)&scr_eos_abort_tick, e, true)); + } else { + ScrClosure *clo = scr_eos_closure_k((void *)&scr_eos_abort_thunk, e, true); + ScrDyn *listener = scr_dyn_new_func(clo, &scr_eos_abort_thunk, 0, "()", "abort"); + e->dispose = add(signal, listener); + scr_dyn_release(listener); + } + } + ScrClosure *cleanup = scr_eos_closure_k((void *)&scr_eos_cleanup_fn, e, true); + scr_eos_release_v(e); + return cleanup; +} + +ScrClosure *scr_stream_finished_opts(ScrStream *s, ScrClosure *cb /*moves*/, ScrStreamErrInv inv, + double readable, double writable, bool on_error, + ScrDyn *signal, ScrAbortAddFn add) { + return scr_stream_eos_impl(s, cb, inv, NULL, false, (int)readable, (int)writable, on_error, + signal, add); +} + +ScrClosure *scr_stream_finished_opts_dyn(ScrStream *s, ScrDyn *cb, double readable, + double writable, bool on_error, ScrDyn *signal, + ScrAbortAddFn add) { + ScrClosure *c = scr_stream_dynopt_clo(cb, (void *)&scr_stream_finished_dyn_inv); + return scr_stream_eos_impl(s, c, &scr_stream_finished_dyn_inv, NULL, false, (int)readable, + (int)writable, on_error, signal, add); +} + +/* stream/promises finished(stream, options): cleanup removes the + * listeners once the promise settles. */ +ScrPromise *scr_sp_finished_opts(ScrStream *s, double readable, double writable, bool on_error, + bool cleanup, ScrDyn *signal, ScrAbortAddFn add) { + ScrPromise *p = scr_promise_new(); + ScrClosure *cleanup_fn = scr_stream_eos_impl(s, NULL, NULL, p, cleanup, (int)readable, + (int)writable, on_error, signal, add); + if (cleanup_fn == NULL) { + scr_promise_reject_pending(p); + return p; + } + scr_closure_release(cleanup_fn); + return p; +} + /* ── node:console Console instances over writable streams ───────────── * Node's kWriteToConsole: with ignoreErrors (the default) one shared noop * 'error' listener brackets the write when the stream has none, and the diff --git a/tests/corpus/5295-readline-promises-question.ts b/tests/corpus/5295-readline-promises-question.ts new file mode 100644 index 000000000..a585a9ff8 --- /dev/null +++ b/tests/corpus/5295-readline-promises-question.ts @@ -0,0 +1,49 @@ +// node:readline/promises question() over a stream input and output: the +// prompt is written to the output, the next line answers, a second +// question while one is pending writes the first prompt again and never +// settles, close() leaves a pending question unsettled, and a question on +// a closed interface rejects with ERR_USE_AFTER_CLOSE. Without an output +// nothing is written. setPrompt/getPrompt/prompt manage the prompt, and +// write() feeds text as if it were input. +import * as readline from "node:readline/promises"; +import { PassThrough, Readable } from "node:stream"; + +const out = new PassThrough(); +out.on("data", (c: Buffer) => console.log("output:", JSON.stringify(c.toString()))); + +const rl = readline.createInterface({ input: Readable.from(["alpha\nbeta\n"]), output: out }); +const first = rl.question("first? "); +const second = rl.question("second? "); +first.then((answer: string) => console.log("first answer:", answer)); +second.then((answer: string) => console.log("second answer (never):", answer)); +rl.on("line", (line: string) => console.log("line:", line)); +rl.on("pause", () => console.log("pause")); +rl.on("close", () => console.log("close")); +await new Promise((resolve) => setTimeout(resolve, 10)); + +const quiet = readline.createInterface({ input: new Readable({ read() {} }) }); +quiet.question("not written ").then((answer: string) => console.log("never:", answer)); +quiet.close(); +try { + await quiet.question("again "); +} catch (e) { + console.log("after close:", (e as Error & { code?: string }).code, (e as Error).message); +} + +const prompter = readline.createInterface({ + input: new Readable({ read() {} }), + output: out, + prompt: "$ ", +}); +console.log("prompt:", JSON.stringify(prompter.getPrompt())); +prompter.prompt(); +prompter.setPrompt("> "); +prompter.prompt(true); +prompter.on("line", (line: string) => console.log("typed:", line)); +prompter.write("one\ntwo"); +prompter.write("\n"); +const typed = prompter.question("q? "); +prompter.write("answer\n"); +console.log("answered:", await typed); +console.log("prompt restored:", JSON.stringify(prompter.getPrompt())); +prompter.close(); diff --git a/tests/corpus/5296-readline-lines.ts b/tests/corpus/5296-readline-lines.ts new file mode 100644 index 000000000..c11c0e193 --- /dev/null +++ b/tests/corpus/5296-readline-lines.ts @@ -0,0 +1,43 @@ +// Line splitting and async iteration on node:readline/promises: \n, \r\n +// and a lone \r end lines, a \n that arrives in the next chunk after a +// \r is dropped (within crlfDelay), UTF-8 sequences split across chunks +// decode whole, and the unfinished last line is emitted at the end of +// input. for await reads the same lines; after a break the lines already +// queued are still read by the next loop. More than 1024 queued lines +// pause the input until the loop catches up. +import { createInterface } from "node:readline/promises"; +import { Readable } from "node:stream"; + +const src = new Readable({ read() {} }); +const rl = createInterface({ input: src, crlfDelay: Infinity }); +rl.on("line", (line: string) => console.log("line", JSON.stringify(line))); +rl.on("close", () => console.log("closed")); +for (const chunk of ["one\r", "\ntwo\r\nthree\rfour", "\n", "cafÃ", "©\nlast"]) { + src.push(Buffer.from(chunk, "latin1")); +} +src.push(null); +await new Promise((resolve) => setTimeout(resolve, 10)); + +const lines = createInterface({ input: Readable.from(["1\n2\n3\n4"]) }); +for await (const line of lines) { + console.log("first loop", line); + break; +} +for await (const line of lines) console.log("second loop", line); +console.log("loops done"); + +const many: string[] = []; +for (let i = 0; i < 1500; i++) many.push(`row ${i}`); +const big = createInterface({ input: Readable.from([many.join("\n") + "\n"]) }); +let pauses = 0; +let resumes = 0; +big.on("pause", () => pauses++); +big.on("resume", () => resumes++); +let count = 0; +let lastLine = ""; +for await (const line of big) { + count++; + lastLine = line; + if (count === 1) await new Promise((resolve) => setTimeout(resolve, 5)); +} +console.log("read", count, lastLine, "pauses", pauses, "resumes", resumes); diff --git a/tests/corpus/5297-readline-callback-events.cjs b/tests/corpus/5297-readline-callback-events.cjs new file mode 100644 index 000000000..a20ed72e2 --- /dev/null +++ b/tests/corpus/5297-readline-callback-events.cjs @@ -0,0 +1,32 @@ +// The callback node:readline Interface from CommonJS: 'line' listeners +// (on and once) in registration order, a question answered by the next +// line while other lines go to 'line', 'pause' before 'close' when the +// input ends, listeners added after 'close' never running, and a question +// after close throwing ERR_USE_AFTER_CLOSE. An error on the input reaches +// the interface's 'error' listener. +'use strict'; +const readline = require('node:readline'); +const { Readable } = require('node:stream'); + +const rl = readline.createInterface({ input: Readable.from(['a\nb\nc\nd']) }); +rl.on('line', (line) => console.log('on', line)); +rl.once('line', (line) => console.log('once', line)); +rl.question('', (answer) => console.log('answer', answer)); +rl.on('pause', () => console.log('pause')); +rl.on('close', () => { + console.log('close'); + rl.on('close', () => console.log('never')); + try { + rl.question('again', () => {}); + } catch (e) { + console.log('question after close:', e.code, e.message); + } + rl.close(); +}); + +const broken = new Readable({ read() {} }); +const rl2 = readline.createInterface({ input: broken }); +rl2.on('error', (err) => console.log('interface error:', err.message)); +rl2.on('line', (line) => console.log('rl2 line', line)); +broken.push('ok\n'); +setTimeout(() => broken.destroy(new Error('input failed')), 5); diff --git a/tests/corpus/5298-readline-abort.cjs b/tests/corpus/5298-readline-abort.cjs new file mode 100644 index 000000000..cacab7a7d --- /dev/null +++ b/tests/corpus/5298-readline-abort.cjs @@ -0,0 +1,46 @@ +// AbortSignal with node:readline/promises: an aborted question rejects +// with an AbortError whose cause is the signal's reason and writes +// clearLine's "\r\n"; an already aborted signal rejects before the prompt +// is written. The createInterface signal option closes the interface when +// it aborts, on the next tick when it is already aborted. +'use strict'; +const { createInterface } = require('node:readline/promises'); +const { PassThrough, Readable } = require('node:stream'); + +async function main() { + const out = new PassThrough(); + out.on('data', (c) => console.log('output:', JSON.stringify(c.toString()))); + const rl = createInterface({ input: new Readable({ read() {} }), output: out }); + const ac = new AbortController(); + const pending = rl.question('name? ', { signal: ac.signal }); + ac.abort(); + try { + await pending; + } catch (e) { + console.log(e.name, e.code, e.message, e.cause instanceof Error, e.cause.name); + } + const pre = new AbortController(); + pre.abort('why'); + try { + await rl.question('again? ', { signal: pre.signal }); + } catch (e) { + console.log('pre-aborted:', e.name, e.cause); + } + const answered = new AbortController(); + rl.write(''); + const later = rl.question('third? ', { signal: answered.signal }); + rl.write('yes\n'); + console.log('answered:', await later); + answered.abort(); + const closer = new AbortController(); + const rl2 = createInterface({ input: new Readable({ read() {} }), signal: closer.signal }); + rl2.on('close', () => console.log('rl2 closed by abort')); + closer.abort(); + const early = new AbortController(); + early.abort(); + const rl3 = createInterface({ input: new Readable({ read() {} }), signal: early.signal }); + rl3.on('close', () => console.log('rl3 closed on the next tick')); + console.log('rl3 created'); + rl.close(); +} +main(); diff --git a/tests/corpus/5299-readline-stdin.ts b/tests/corpus/5299-readline-stdin.ts new file mode 100644 index 000000000..292c94ba8 --- /dev/null +++ b/tests/corpus/5299-readline-stdin.ts @@ -0,0 +1,37 @@ +// node:readline/promises over process.stdin: the program re-runs itself +// with piped input and prints the child's output. The first line answers +// the question (its prompt goes to stdout), the 'line' listener receives +// the rest, the trailing line without a newline included, and 'close' +// follows the end of input. A second child reads every line with for +// await, and a callback interface reports \r\n-terminated lines. +import { execFileSync } from "node:child_process"; +import { createInterface } from "node:readline/promises"; +import * as readlineCb from "node:readline"; + +const mode = process.argv[2]; +if (mode === "question") { + const rl = createInterface({ input: process.stdin, output: process.stdout }); + const answer = rl.question("name? "); + const rest: string[] = []; + rl.on("line", (line: string) => rest.push(line)); + rl.on("close", () => console.log("closed after", rest.join(","))); + console.log(`\nhello ${await answer}`); +} else if (mode === "lines") { + const rl = createInterface({ input: process.stdin }); + let n = 0; + for await (const line of rl) console.log(++n, line); + console.log("done"); +} else if (mode === "callback") { + const rl = readlineCb.createInterface({ input: process.stdin }); + rl.on("line", (line: string) => console.log("line:", line)); + rl.on("close", () => console.log("closed")); +} else { + for (const [child, input] of [ + ["question", "Ada\nblue\nx\nz"], + ["lines", "a\n\nb\r\nc"], + ["callback", "first\r\nsecond\nthird"], + ]) { + const stdout = execFileSync(process.execPath, [process.argv[1]!, child!], { input, encoding: "utf8" }); + console.log(child, JSON.stringify(stdout)); + } +} diff --git a/tests/corpus/5300-stream-add-abort-signal.ts b/tests/corpus/5300-stream-add-abort-signal.ts new file mode 100644 index 000000000..7d479501a --- /dev/null +++ b/tests/corpus/5300-stream-add-abort-signal.ts @@ -0,0 +1,31 @@ +// stream.addAbortSignal and the constructors' signal option: an abort +// destroys the stream with an AbortError (no unhandled error: the +// listener eos registers handles it), an already aborted signal destroys +// it during construction, the subclass super({ signal }) form applies it +// too, and an abort after the stream finished changes nothing. +import { addAbortSignal, Readable, Writable } from "node:stream"; + +const ac = new AbortController(); +const r = addAbortSignal(ac.signal, new Readable({ read() {} })); +r.on("close", () => console.log("close", r.destroyed, r.errored?.name)); +ac.abort(); +console.log("after abort", r.destroyed); +const pre = new AbortController(); +pre.abort("x"); +const signal = pre.signal; +const w = new Writable({ write(_c, _e, cb) { cb(); }, signal }); +console.log("pre", w.destroyed, w.errored?.name); +w.on("error", (e: Error) => console.log("w error", e.name, e.message)); +w.on("close", () => console.log("w close")); +const ok = new Readable({ read() { this.push("a"); this.push(null); } }); +const ac2 = new AbortController(); +addAbortSignal(ac2.signal, ok).resume(); +ok.on("close", () => { ac2.abort(); console.log("ok close, abort after finish", ok.errored); }); +class Sub extends Readable { + constructor(s: AbortSignal) { super({ signal: s }); } + _read() {} +} +const ac3 = new AbortController(); +const sub = new Sub(ac3.signal); +sub.on("error", (e: Error) => console.log("sub error", e.name)); +ac3.abort(); diff --git a/tests/corpus/5301-stream-add-abort-signal-errors.cjs b/tests/corpus/5301-stream-add-abort-signal-errors.cjs new file mode 100644 index 000000000..0a1643372 --- /dev/null +++ b/tests/corpus/5301-stream-add-abort-signal-errors.cjs @@ -0,0 +1,16 @@ +// addAbortSignal's argument checks from CommonJS: Node's +// ERR_INVALID_ARG_TYPE messages for values that are not AbortSignals or +// streams (null fails the in-operator check), a falsy signal option is +// ignored, and the AbortError carries the abort reason as its cause. +const { addAbortSignal, Readable } = require("node:stream"); +const r = new Readable({ read() {} }); +for (const bad of [{}, null, 1, "x", undefined]) { + try { addAbortSignal(bad, r); console.log("accepted", bad); } catch (e) { console.log(e.code, e.message); } +} +try { addAbortSignal(new AbortController().signal, {}); } catch (e) { console.log(e.code, e.message); } +const ac = new AbortController(); +ac.abort(new Error("reason here")); +const s = new Readable({ read() {}, signal: ac.signal }); +s.on("error", (e) => console.log("error", e.name, e.cause.message, e.code)); +const t = new Readable({ read() {}, signal: false }); +console.log("falsy signal ignored", t.destroyed); diff --git a/tests/corpus/5302-stream-promises-options.ts b/tests/corpus/5302-stream-promises-options.ts new file mode 100644 index 000000000..5326dddd6 --- /dev/null +++ b/tests/corpus/5302-stream-promises-options.ts @@ -0,0 +1,87 @@ +// stream/promises options: pipeline(..., { signal }) destroys every stage +// with an AbortError in pipeline order and rejects after the last close +// (an already aborted signal too), and { end: false } settles when the +// source is done while the destination stays open. finished(stream, +// options): readable/writable choose which end counts, error: false +// waits for close, signal rejects without destroying the stream, and +// cleanup removes the listeners. The callback form takes the options. +import { Duplex, PassThrough, Readable, Writable, finished as finishedCb } from "node:stream"; +import { finished, pipeline } from "node:stream/promises"; + +{ + const ac = new AbortController(); + const src = new Readable({ read() {} }); + const mid = new PassThrough(); + const dst = new Writable({ write(c, _e, cb) { console.log("write", c.toString()); cb(); } }); + src.on("error", (e: Error) => console.log("src error", e.name)); + src.on("close", () => console.log("src close")); + mid.on("error", (e: Error) => console.log("mid error", e.name)); + mid.on("close", () => console.log("mid close")); + dst.on("error", (e: Error) => console.log("dst error", e.name)); + dst.on("close", () => console.log("dst close")); + const p = pipeline(src, mid, dst, { signal: ac.signal }); + src.push("a"); + setTimeout(() => ac.abort(), 5); + try { await p; } catch (e) { console.log("rejected", (e as Error).name); } +} +{ + const ac = new AbortController(); + ac.abort(); + const src = Readable.from(["x"]); + const dst = new Writable({ write(_c, _e, cb) { cb(); } }); + try { await pipeline(src, dst, { signal: ac.signal }); } catch (e) { console.log("pre", (e as Error).name, src.destroyed, dst.destroyed); } +} +{ + const chunks: string[] = []; + const dst = new Writable({ write(c, _e, cb) { chunks.push(c.toString()); cb(); } }); + dst.on("finish", () => console.log("dst finish")); + await pipeline(Readable.from(["1", "2"]), dst, { end: false }); + console.log("resolved end:false", chunks.join(""), dst.writableEnded, dst.destroyed); + dst.end("3"); + await new Promise((r) => setTimeout(r, 5)); + console.log("after manual end", chunks.join("")); +} +console.log("done"); + +{ + const d = new Duplex({ read() {}, write(_c, _e, cb) { cb(); } }); + d.on("finish", () => console.log("d finish")); + d.on("close", () => console.log("d close")); + const p = finished(d, { readable: false }).then(() => console.log("d settled (readable: false)")); + d.end("x"); + await p; +} +{ + const d = new PassThrough(); + d.on("end", () => console.log("pt end")); + const p = finished(d, { writable: false }).then(() => console.log("pt settled (writable: false)")); + d.resume(); + d.end("y"); + await p; +} +{ + const r = new Readable({ read() {} }); + r.on("error", (e: Error) => console.log("r error event", e.message)); + r.on("close", () => console.log("r close")); + const p = finished(r, { error: false }).catch((e) => console.log("r rejected", (e as Error).message)); + r.destroy(new Error("boom")); + await p; +} +{ + const ac = new AbortController(); + const r = new Readable({ read() {} }); + const p = finished(r, { signal: ac.signal }).catch((e) => console.log("aborted", (e as Error).name, r.destroyed)); + ac.abort(); + await p; +} +{ + const r = Readable.from(["a"]); + r.resume(); + await finished(r, { cleanup: true }); + console.log("cleanup listeners", r.listenerCount("end"), r.listenerCount("close")); +} +{ + const w = new Writable({ write(_c, _e, cb) { cb(); } }); + finishedCb(w, { writable: true }, (err?: Error | null) => console.log("callback form", err)); + w.end("z"); +} diff --git a/tests/corpus/5303-stream-finished-callback-opts.cjs b/tests/corpus/5303-stream-finished-callback-opts.cjs new file mode 100644 index 000000000..4ae73e54e --- /dev/null +++ b/tests/corpus/5303-stream-finished-callback-opts.cjs @@ -0,0 +1,35 @@ +// finished(stream, options, callback) from CommonJS: the callback runs +// once with the error or no argument, `this` unused; readable: false on a +// Duplex settles at 'finish'; error: false leaves the 'error' event to the +// stream's own listeners and reports at close; the returned cleanup +// function removes the listeners so the callback never runs; signal +// aborts the wait with an AbortError; a non-signal value throws +// ERR_INVALID_ARG_TYPE. +'use strict'; +const { Duplex, Readable, finished } = require('node:stream'); + +const d = new Duplex({ read() {}, write(c, e, cb) { cb(); } }); +d.on('finish', () => console.log('finish')); +finished(d, { readable: false }, (err) => console.log('duplex settled', err)); +d.end('x'); + +const r = new Readable({ read() {} }); +r.on('error', (e) => console.log('error event', e.message)); +finished(r, { error: false }, (err) => console.log('error:false settled', err.message)); +r.destroy(new Error('bad')); + +const quiet = new Readable({ read() {} }); +const cleanup = finished(quiet, null, (err) => console.log('never', err)); +cleanup(); +quiet.destroy(); + +const ac = new AbortController(); +const waiting = new Readable({ read() {} }); +finished(waiting, { signal: ac.signal }, (err) => console.log('aborted', err.name, err.cause.name, waiting.destroyed)); +ac.abort(); + +try { + finished(new Readable({ read() {} }), { signal: {} }, () => {}); +} catch (e) { + console.log(e.code, e.message); +} diff --git a/tests/diagnostics/options-record.ts b/tests/diagnostics/options-record.ts index 01a54c5f9..67ae46a47 100644 --- a/tests/diagnostics/options-record.ts +++ b/tests/diagnostics/options-record.ts @@ -46,9 +46,9 @@ fs.watch("/tmp", { persistent: false }, () => {}); dns.lookup("localhost", { family: 4, hints: 32 }, () => {}); dns.lookup("localhost", { family: 4, all: true }, () => {}); -// readline: a finite crlfDelay is not the lowered splitter; completer +// readline: history only exists for terminal interfaces; completer // needs an interactive terminal. -readline.createInterface({ input: process.stdin, output: process.stdout, crlfDelay: 100 }); +readline.createInterface({ input: process.stdin, output: process.stdout, historySize: 10 }); readline.createInterface({ input: process.stdin, output: process.stdout, completer: (l: string) => [[l], l] }); // A bare __proto__: value entry is not an ordinary undocumented key: it diff --git a/tests/fixtures/node-types/readline-promises.ts b/tests/fixtures/node-types/readline-promises.ts new file mode 100644 index 000000000..db5352ffc --- /dev/null +++ b/tests/fixtures/node-types/readline-promises.ts @@ -0,0 +1,18 @@ +// readline/promises under @types/node: the Interface over process.stdin, +// a question answered through write() with the prompt on stdout, 'line' +// for text with no pending question, close(), and the rejection of a +// question asked after close. +import { createInterface } from "node:readline/promises"; + +const rl = createInterface({ input: process.stdin, output: process.stdout }); +rl.on("line", (line: string) => console.log("line:", line)); +rl.on("close", () => console.log("closed")); +const answer = rl.question("name? "); +rl.write("Ada\nextra\n"); +console.log("\nname:", await answer); +rl.close(); +try { + await rl.question("again? "); +} catch (err) { + console.log("after close:", (err as Error & { code?: string }).code); +} diff --git a/tests/harness/__snapshots__/options-record.ts.txt b/tests/harness/__snapshots__/options-record.ts.txt index 60c552e95..a4c74104a 100644 --- a/tests/harness/__snapshots__/options-record.ts.txt +++ b/tests/harness/__snapshots__/options-record.ts.txt @@ -106,18 +106,18 @@ options-record.ts:47:38 - error SC2020: 'lookup option 'all'' is part of the sta hint: the all-addresses callback shape has no lowering — the lowered callback is (err, address, family) over one IPv4 answer -options-record.ts:51:85 - error SC2020: 'createInterface with a finite crlfDelay' is part of the standard library types but has no scriptc lowering yet +options-record.ts:51:74 - error SC2020: 'createInterface option 'historySize'' is part of the standard library types but has no scriptc lowering yet 50 | // needs an interactive terminal. - 51 | readline.createInterface({ input: process.stdin, output: process.stdout, crlfDelay: 100 }); - | ^~~ + 51 | readline.createInterface({ input: process.stdin, output: process.stdout, historySize: 10 }); + | ^~~~~~~~~~~~~~~ 52 | readline.createInterface({ input: process.stdin, output: process.stdout, completer: (l: string) => [[l], l] }); - hint: the lowered splitter always joins \r\n across chunks (Node's crlfDelay: Infinity) — Infinity is the accepted value + hint: input, output, terminal: false, crlfDelay, prompt and signal are the supported options options-record.ts:52:74 - error SC2020: 'createInterface with a completer' is part of the standard library types but has no scriptc lowering yet - 51 | readline.createInterface({ input: process.stdin, output: process.stdout, crlfDelay: 100 }); + 51 | readline.createInterface({ input: process.stdin, output: process.stdout, historySize: 10 }); 52 | readline.createInterface({ input: process.stdin, output: process.stdout, completer: (l: string) => [[l], l] }); | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 53 | diff --git a/tests/harness/project-config.test.ts b/tests/harness/project-config.test.ts index edc5d8524..cd187e2fc 100644 --- a/tests/harness/project-config.test.ts +++ b/tests/harness/project-config.test.ts @@ -126,6 +126,24 @@ test("node-types: util.styleText supports stored calls and standard streams unde expect(comparableStderr(native.stderr)).toBe(node.stderr); }); +test("node-types: readline/promises over stdin compiles under @types/node", async () => { + const outDir = outDirFor("readline-promises"); + const entry = join(nodeTypesDir, "readline-promises.ts"); + const result = await compile(entry, { + outPath: join(outDir, "readline-promises"), + 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); +}); + function outDirFor(name: string): string { return mkdtempSync(join(tmpdir(), `scriptc-config-${name}-`)); }