diff --git a/docs/content/limitations/builtin-apis/streams-and-readline.mdx b/docs/content/limitations/builtin-apis/streams-and-readline.mdx index c48f04bcc..14735a9a1 100644 --- a/docs/content/limitations/builtin-apis/streams-and-readline.mdx +++ b/docs/content/limitations/builtin-apis/streams-and-readline.mdx @@ -1 +1 @@ -- **Streams and readline:** Stream chunks are Buffers or strings. `Readable.from` accepts arrays, Sets, Map and Set iterators, generators, async generators, and class iterables and async iterables whose values are strings or Buffers, and in JavaScript any iterable or async iterable of them; a stream that mixes strings and Buffers is unsupported. In JavaScript, a sync iterator value with a `then` method that is not a promise, and an object whose `[Symbol.asyncIterator]` or `[Symbol.iterator]` method is a generator method, are unsupported. Object-mode streams of other values (including `Readable.from` over a `Map`, whose values are entry arrays) are unsupported. `pipeline` accepts one run of function stages: async generator functions between streams (`async function* (source)`, iterating `source` with `for await`), an async generator source function (`async function* ({ signal })`), and an async function destination whose value resolves the promise form and reaches the callback's second argument. Stages may take the `{ signal }` argument. An async generator function as the destination, using the PassThrough that `pipeline()` returns for a function destination, several runs of function stages, and function stages together with pipeline options are unsupported. `Readable.from` accepts `objectMode` and `highWaterMark` options. The `Readable` operators (`map`, `filter`, `forEach`, `toArray`, `some`, `find`, `every`, `flatMap`, `drop`, `take`, and `reduce`) accept `concurrency`, `highWaterMark`, and `signal` options; their callbacks take only the chunk, `map` returns strings or Buffers, and `flatMap` returns strings or arrays of them. `readable.compose` is unsupported. `stream.compose` accepts two or more streams, or one `Duplex`; functions, iterables, and web streams as stages are unsupported, and the `ERR_INVALID_ARG_VALUE` message for a stream that is no longer readable or writable names the stream's class instead of printing the stream. `pipeline` from `node:stream/promises` accepts a trailing `{ signal, end }` object, and `finished` accepts `readable`, `writable`, `error`, `cleanup`, and `signal` options, with boolean literals for the flags. Stream constructors accept a `signal` option in an options object literal, and `stream.addAbortSignal` accepts any stream. `node:readline` and `node:readline/promises` (also reached as `readline.promises`) interfaces, from `createInterface(options)` or `createInterface(input, output, completer, terminal)`, read `process.stdin` or a readable stream: `question`, `prompt`, `write` (with a key object literal of `name`, `ctrl`, `meta`, and `shift`), `pause`, `resume`, `'line'`/`'close'`/`'pause'`/`'resume'`/`'error'`/`'history'`/`'SIGINT'`/`'SIGTSTP'`/`'SIGCONT'` listeners, and `for await` compile. The `history`, `historySize`, `removeHistoryDuplicates`, `completer`, `tabSize`, and `escapeCodeTimeout` options are validated as in Node. With `terminal: true`, or with no `terminal` option and `process.stdout` as the output when it is a TTY, the interface runs Node's terminal mode: the editing keys, the kill ring, undo and redo, history recording and recall, Tab completion through the completer, Ctrl+C, Ctrl+D, and Ctrl+Z, `TERM=dumb`, and the same escape sequences written to the output. The output's `'resize'` event does not redraw the line. The cursor never stops between the two UTF-16 units of a supplementary character: where Node's Up or Down movement through a multiline line would, the cursor moves to the character's start or end. In JavaScript, a `'history'` listener receives a copy of the history; its changes are written back when the listener returns, but the copy is not `rl.history` itself. `rl.history`, `historySize`, `historyIndex`, `crlfDelay`, `tabSize`, `escapeCodeTimeout`, `line`, `cursor`, and `terminal` read as in Node, except that `rl.cursor` of an interface that is not a terminal throws instead of answering `undefined`. `readline.cursorTo`, `moveCursor`, `clearLine`, and `clearScreenDown` write to `process.stdout`, `process.stderr`, a writable stream, or nothing for `null` and `undefined`; their callbacks take no parameters. The `readline/promises` `Readline` class writes to the same outputs. `readline.emitKeypressEvents` emits `'keypress'` events on `process.stdin` or a readable stream, shared with terminal interfaces on that stream as in Node; a listener's first parameter must admit `undefined`, which it receives for escape sequences. Chained listener registration is unsupported. +- **Streams and readline:** Stream chunks are Buffers or strings. `Readable.from` accepts arrays, Sets, Map and Set iterators, generators, async generators, and class iterables and async iterables whose values are strings or Buffers, and in JavaScript any iterable or async iterable of them; a stream that mixes strings and Buffers is unsupported. In JavaScript, a sync iterator value with a `then` method that is not a promise, and an object whose `[Symbol.asyncIterator]` or `[Symbol.iterator]` method is a generator method, are unsupported. Object-mode streams of other values (including `Readable.from` over a `Map`, whose values are entry arrays) are unsupported. `pipeline` accepts one run of function stages: async generator functions between streams (`async function* (source)`, iterating `source` with `for await`), an async generator source function (`async function* ({ signal })`), and an async function destination whose value resolves the promise form and reaches the callback's second argument. Stages may take the `{ signal }` argument. An async generator function as the destination, using the PassThrough that `pipeline()` returns for a function destination, several runs of function stages, and function stages together with pipeline options are unsupported. `Readable.from` accepts `objectMode` and `highWaterMark` options. The `Readable` operators (`map`, `filter`, `forEach`, `toArray`, `some`, `find`, `every`, `flatMap`, `drop`, `take`, and `reduce`) accept `concurrency`, `highWaterMark`, and `signal` options; their callbacks take only the chunk, `map` returns strings or Buffers, and `flatMap` returns strings or arrays of them. `readable.compose` is unsupported. `stream.compose` accepts two or more streams, or one `Duplex`; functions, iterables, and web streams as stages are unsupported, and the `ERR_INVALID_ARG_VALUE` message for a stream that is no longer readable or writable names the stream's class instead of printing the stream. `pipeline` from `node:stream/promises` accepts a trailing `{ signal, end }` object, and `finished` accepts `readable`, `writable`, `error`, `cleanup`, and `signal` options, with boolean literals for the flags. Stream constructors accept a `signal` option in an options object literal, and `stream.addAbortSignal` accepts any stream. `node:readline` and `node:readline/promises` (also reached as `readline.promises`) interfaces, from `createInterface(options)` or `createInterface(input, output, completer, terminal)`, read `process.stdin` or a readable stream: `question`, `prompt`, `write` (with a key object literal of `name`, `ctrl`, `meta`, and `shift`), `pause`, `resume`, `'line'`/`'close'`/`'pause'`/`'resume'`/`'error'`/`'history'`/`'SIGINT'`/`'SIGTSTP'`/`'SIGCONT'` listeners, and `for await` compile. The `history`, `historySize`, `removeHistoryDuplicates`, `completer`, `tabSize`, and `escapeCodeTimeout` options are validated as in Node. With `terminal: true`, or with no `terminal` option and `process.stdout` as the output when it is a TTY, the interface runs Node's terminal mode: the editing keys, the kill ring, undo and redo, history recording and recall, Tab completion through the completer, Ctrl+C, Ctrl+D, and Ctrl+Z, `TERM=dumb`, and the same escape sequences written to the output. The output's `'resize'` event does not redraw the line. The cursor never stops between the two UTF-16 units of a supplementary character: where Node's Up or Down movement through a multiline line would, the cursor moves to the character's start or end. In JavaScript, a `'history'` listener receives a copy of the history; its changes are written back when the listener returns, but the copy is not `rl.history` itself. `rl.history`, `historySize`, `historyIndex`, `crlfDelay`, `tabSize`, `escapeCodeTimeout`, `line`, `cursor`, and `terminal` read as in Node, except that `rl.cursor` of an interface that is not a terminal throws instead of answering `undefined`. `rl[Symbol.dispose]()` closes the interface, and `instanceof` with `readline.Interface` or `readlinePromises.Interface` answers whether the interface came from that module, as in Node. In JavaScript, the input and output can be any expression that evaluates to a stream, such as a function's result or an object's member; a value that is not a stream is unsupported. In JavaScript, `question()` and `setPrompt()` accept a value that is not a string: when such a prompt is written, an interface that is not a terminal throws `ERR_INVALID_ARG_TYPE` as Node does (the `readline/promises` question rejects with it) and the question stays pending, and a terminal interface writes `String(value)`. A terminal interface's prompt that is an object or a symbol, `getPrompt()` while the prompt is not a string, and `getCursorPos()` with an object or symbol prompt are unsupported. `readline.cursorTo`, `moveCursor`, `clearLine`, and `clearScreenDown` write to `process.stdout`, `process.stderr`, a writable stream, or nothing for `null` and `undefined`; their callbacks take no parameters. The `readline/promises` `Readline` class writes to the same outputs. `readline.emitKeypressEvents` emits `'keypress'` events on `process.stdin` or a readable stream, shared with terminal interfaces on that stream as in Node; a listener's first parameter must admit `undefined`, which it receives for escape sequences. Chained listener registration is unsupported. diff --git a/internal/compatibility/generated/node-v24-backlog.json b/internal/compatibility/generated/node-v24-backlog.json index 0ad30b783..b62a17cd9 100644 --- a/internal/compatibility/generated/node-v24-backlog.json +++ b/internal/compatibility/generated/node-v24-backlog.json @@ -58997,7 +58997,9 @@ "tests/corpus/5298-readline-abort.cjs", "tests/corpus/5299-readline-stdin.ts", "tests/corpus/5872-readline-positional-create-interface.mjs", - "tests/corpus/5873-readline-promises-namespace.cjs" + "tests/corpus/5873-readline-promises-namespace.cjs", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] } } @@ -59029,7 +59031,9 @@ "tests/corpus/5298-readline-abort.cjs", "tests/corpus/5299-readline-stdin.ts", "tests/corpus/5872-readline-positional-create-interface.mjs", - "tests/corpus/5873-readline-promises-namespace.cjs" + "tests/corpus/5873-readline-promises-namespace.cjs", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] } } @@ -59257,7 +59261,9 @@ "tests/corpus/5298-readline-abort.cjs", "tests/corpus/5299-readline-stdin.ts", "tests/corpus/5872-readline-positional-create-interface.mjs", - "tests/corpus/5873-readline-promises-namespace.cjs" + "tests/corpus/5873-readline-promises-namespace.cjs", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] } } @@ -59318,7 +59324,9 @@ "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" + "tests/corpus/5299-readline-stdin.ts", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] } } @@ -59350,7 +59358,9 @@ "tests/corpus/5298-readline-abort.cjs", "tests/corpus/5299-readline-stdin.ts", "tests/corpus/5872-readline-positional-create-interface.mjs", - "tests/corpus/5873-readline-promises-namespace.cjs" + "tests/corpus/5873-readline-promises-namespace.cjs", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] } } @@ -59433,7 +59443,8 @@ "confidence": "high", "source": "compiler-dedicated:readline.createInterface", "tests": [ - "tests/corpus/1475-readline-closed-stdin.ts" + "tests/corpus/1475-readline-closed-stdin.ts", + "tests/corpus/5878-readline-dynamic-streams.mjs" ] } } @@ -59572,7 +59583,8 @@ "confidence": "high", "source": "compiler-dedicated:rl.close", "tests": [ - "tests/corpus/1475-readline-closed-stdin.ts" + "tests/corpus/1475-readline-closed-stdin.ts", + "tests/corpus/5878-readline-dynamic-streams.mjs" ] } } @@ -59602,7 +59614,9 @@ "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" + "tests/corpus/5299-readline-stdin.ts", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] } } @@ -59632,7 +59646,9 @@ "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" + "tests/corpus/5299-readline-stdin.ts", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] } } @@ -59662,7 +59678,9 @@ "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" + "tests/corpus/5299-readline-stdin.ts", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] } } @@ -59692,7 +59710,9 @@ "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" + "tests/corpus/5299-readline-stdin.ts", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] } } @@ -59722,7 +59742,9 @@ "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" + "tests/corpus/5299-readline-stdin.ts", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] } } @@ -59752,7 +59774,9 @@ "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" + "tests/corpus/5299-readline-stdin.ts", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] } } @@ -59782,7 +59806,9 @@ "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" + "tests/corpus/5299-readline-stdin.ts", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] } } @@ -59812,7 +59838,9 @@ "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" + "tests/corpus/5299-readline-stdin.ts", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] } } @@ -59932,7 +59960,9 @@ "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" + "tests/corpus/5299-readline-stdin.ts", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] } } @@ -59962,7 +59992,9 @@ "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" + "tests/corpus/5299-readline-stdin.ts", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] } } @@ -59992,7 +60024,9 @@ "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" + "tests/corpus/5299-readline-stdin.ts", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] } } @@ -60050,7 +60084,9 @@ "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" + "tests/corpus/5299-readline-stdin.ts", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] } } @@ -60080,7 +60116,9 @@ "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" + "tests/corpus/5299-readline-stdin.ts", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] } } diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index 2f0565f84..ee139efa0 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -82445,7 +82445,10 @@ "tests/corpus/5791-readline-keypress-interfaces.cjs", "tests/corpus/5792-readline-keypress-listeners.ts", "tests/corpus/5872-readline-positional-create-interface.mjs", - "tests/corpus/5873-readline-promises-namespace.cjs" + "tests/corpus/5873-readline-promises-namespace.cjs", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs", + "tests/corpus/5878-readline-dynamic-streams.mjs" ] }, "anchorSource": "chapter" @@ -82499,7 +82502,9 @@ "tests/corpus/5298-readline-abort.cjs", "tests/corpus/5299-readline-stdin.ts", "tests/corpus/5872-readline-positional-create-interface.mjs", - "tests/corpus/5873-readline-promises-namespace.cjs" + "tests/corpus/5873-readline-promises-namespace.cjs", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] }, "anchorSource": "exact" @@ -82530,7 +82535,9 @@ "tests/corpus/5298-readline-abort.cjs", "tests/corpus/5299-readline-stdin.ts", "tests/corpus/5872-readline-positional-create-interface.mjs", - "tests/corpus/5873-readline-promises-namespace.cjs" + "tests/corpus/5873-readline-promises-namespace.cjs", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] }, "anchorSource": "exact" @@ -82750,7 +82757,9 @@ "tests/corpus/5298-readline-abort.cjs", "tests/corpus/5299-readline-stdin.ts", "tests/corpus/5872-readline-positional-create-interface.mjs", - "tests/corpus/5873-readline-promises-namespace.cjs" + "tests/corpus/5873-readline-promises-namespace.cjs", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] }, "anchorSource": "exact" @@ -82832,7 +82841,9 @@ "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" + "tests/corpus/5299-readline-stdin.ts", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] }, "anchorSource": "exact" @@ -82863,7 +82874,9 @@ "tests/corpus/5298-readline-abort.cjs", "tests/corpus/5299-readline-stdin.ts", "tests/corpus/5872-readline-positional-create-interface.mjs", - "tests/corpus/5873-readline-promises-namespace.cjs" + "tests/corpus/5873-readline-promises-namespace.cjs", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] }, "anchorSource": "exact" @@ -82943,7 +82956,8 @@ "status": "partial", "evidence": "compiler-dedicated:readline.createInterface", "tests": [ - "tests/corpus/1475-readline-closed-stdin.ts" + "tests/corpus/1475-readline-closed-stdin.ts", + "tests/corpus/5878-readline-dynamic-streams.mjs" ] }, "anchorSource": "exact" @@ -83099,7 +83113,8 @@ "status": "partial", "evidence": "compiler-dedicated:rl.close", "tests": [ - "tests/corpus/1475-readline-closed-stdin.ts" + "tests/corpus/1475-readline-closed-stdin.ts", + "tests/corpus/5878-readline-dynamic-streams.mjs" ] }, "anchorSource": "exact" @@ -83128,7 +83143,9 @@ "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" + "tests/corpus/5299-readline-stdin.ts", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] }, "anchorSource": "exact" @@ -83157,7 +83174,9 @@ "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" + "tests/corpus/5299-readline-stdin.ts", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] }, "anchorSource": "exact" @@ -83186,7 +83205,9 @@ "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" + "tests/corpus/5299-readline-stdin.ts", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] }, "anchorSource": "exact" @@ -83215,7 +83236,9 @@ "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" + "tests/corpus/5299-readline-stdin.ts", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] }, "anchorSource": "exact" @@ -83244,7 +83267,9 @@ "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" + "tests/corpus/5299-readline-stdin.ts", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] }, "anchorSource": "exact" @@ -83273,7 +83298,9 @@ "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" + "tests/corpus/5299-readline-stdin.ts", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] }, "anchorSource": "exact" @@ -83302,7 +83329,9 @@ "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" + "tests/corpus/5299-readline-stdin.ts", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] }, "anchorSource": "exact" @@ -83331,7 +83360,9 @@ "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" + "tests/corpus/5299-readline-stdin.ts", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] }, "anchorSource": "exact" @@ -83447,7 +83478,9 @@ "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" + "tests/corpus/5299-readline-stdin.ts", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] }, "anchorSource": "exact" @@ -83476,7 +83509,9 @@ "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" + "tests/corpus/5299-readline-stdin.ts", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] }, "anchorSource": "exact" @@ -83505,7 +83540,9 @@ "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" + "tests/corpus/5299-readline-stdin.ts", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] }, "anchorSource": "exact" @@ -83561,7 +83598,9 @@ "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" + "tests/corpus/5299-readline-stdin.ts", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] }, "anchorSource": "exact" @@ -83590,7 +83629,9 @@ "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" + "tests/corpus/5299-readline-stdin.ts", + "tests/corpus/5876-readline-dispose-instanceof.ts", + "tests/corpus/5877-readline-question-query.cjs" ] }, "anchorSource": "exact" diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index 5807c1ae9..87a51470e 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -189,13 +189,13 @@ {"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", "tests/corpus/5872-readline-positional-create-interface.mjs", "tests/corpus/5873-readline-promises-namespace.cjs"]}, + {"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", "tests/corpus/5872-readline-positional-create-interface.mjs", "tests/corpus/5873-readline-promises-namespace.cjs", "tests/corpus/5876-readline-dispose-instanceof.ts", "tests/corpus/5877-readline-question-query.cjs"]}, {"chapter": "readline", "symbols": ["'history'", "rl.line"], "status": "partial", "evidence": ["tests/corpus/5780-readline-history-options.ts", "tests/corpus/5781-readline-history-options.cjs"]}, {"chapter": "readline", "symbols": ["'SIGINT'", "'SIGTSTP'", "'SIGCONT'", "line", "cursor", "completer"], "status": "partial", "evidence": ["tests/corpus/5782-readline-terminal-editing.ts", "tests/corpus/5783-readline-terminal-completion.cjs", "tests/corpus/5784-readline-terminal-promises.ts", "tests/corpus/5785-readline-terminal-keys.ts", "tests/corpus/5786-readline-terminal-history.cjs"]}, {"chapter": "readline", "symbols": ["readline.cursorTo", "readline.moveCursor", "readline.clearLine", "readline.clearScreenDown", "rl.getCursorPos"], "status": "partial", "evidence": ["tests/corpus/5787-readline-cursor-helpers.cjs", "tests/corpus/5789-readline-cursor-pos.cjs"]}, {"chapter": "readline", "symbols": ["readlinePromises.Readline", "rl.clearLine", "rl.clearScreenDown", "rl.commit", "rl.cursorTo", "rl.moveCursor", "rl.rollback"], "status": "partial", "evidence": ["tests/corpus/5788-readline-promises-readline.ts", "tests/corpus/5789-readline-cursor-pos.cjs"]}, {"chapter": "readline", "symbols": ["readline.emitKeypressEvents"], "status": "partial", "evidence": ["tests/corpus/5790-readline-keypress-events.ts", "tests/corpus/5791-readline-keypress-interfaces.cjs", "tests/corpus/5792-readline-keypress-listeners.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": "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", "tests/corpus/5876-readline-dispose-instanceof.ts", "tests/corpus/5877-readline-question-query.cjs"]}, {"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": "stream", "symbols": ["readable.map", "readable.filter", "readable.forEach", "readable.toArray", "readable.some", "readable.find", "readable.every", "readable.flatMap", "readable.drop", "readable.take", "readable.reduce"], "status": "partial", "evidence": ["tests/corpus/5335-stream-operators-values.ts", "tests/corpus/5336-stream-operators-promise-timing.ts", "tests/corpus/5337-stream-operators-concurrency.ts", "tests/corpus/5338-stream-operators-checks.cjs"]}, {"chapter": "stream", "symbols": ["stream.Readable.from"], "status": "partial", "evidence": ["tests/corpus/5340-readable-from-options.ts", "tests/corpus/5341-readable-from-dynamic.cjs", "tests/corpus/5420-readable-from-generators.ts", "tests/corpus/5421-readable-from-generators.cjs", "tests/corpus/5610-readable-from-sets-and-views.ts", "tests/corpus/5611-readable-from-class-iterables.ts", "tests/corpus/5612-readable-from-iterables.cjs"]}, @@ -338,7 +338,7 @@ { "chapter": "fs", "signatures": ["`filehandle[Symbol.asyncDispose]()`"], "status": "partial", "evidence": ["tests/corpus/2844-node-disposable-resources.ts"] }, { "chapter": "fs", "signatures": ["`fsPromises.realpath(path[, options])`"], "status": "partial", "evidence": ["tests/corpus/2940-fsp-realpath.ts"] }, { "chapter": "fs", "signatures": ["`fsPromises.readdir(path[, options])`"], "status": "partial", "evidence": ["tests/corpus/2942-fsp-readdir-dirent.ts"] }, - { "chapter": "readline", "signatures": ["`rl[Symbol.dispose]()`"], "status": "partial", "evidence": ["tests/corpus/2844-node-disposable-resources.ts"] }, + { "chapter": "readline", "signatures": ["`rl[Symbol.dispose]()`"], "status": "partial", "evidence": ["tests/corpus/2844-node-disposable-resources.ts", "tests/corpus/5876-readline-dispose-instanceof.ts"] }, { "chapter": "timers", "signatures": ["`immediate[Symbol.dispose]()`", "`timeout[Symbol.dispose]()`"], "status": "partial", "evidence": ["tests/corpus/2844-node-disposable-resources.ts"] }, { "chapter": "stream", "signatures": ["`readable[Symbol.asyncIterator]()`", "`readable.iterator([options])`"], "status": "partial", "evidence": ["tests/corpus/1746-stream-for-await.ts", "tests/corpus/2861-readable-iterator-close.ts"] }, { "chapter": "stream", "signatures": ["`writable.destroy([error])`"], "status": "partial", "evidence": ["tests/corpus/2897-child-stdin-destroy.ts"] }, @@ -405,7 +405,7 @@ { "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": ["net.SocketAddress", "net.BlockList"], "status": "partial", "evidence": ["tests/corpus/5659-net-blocklist-socketaddress.ts", "tests/fixtures/node-types/net-blocklist.ts"] }, { "symbols": ["SocketAddress.parse", "BlockList.isBlockList", "blockList.addAddress", "blockList.addAddresses", "blockList.addCIDR", "blockList.addCIDRs", "blockList.addRange", "blockList.addSubnet", "blockList.check", "blockList.clear", "blockList.fromJSON", "blockList.removeAddress", "blockList.removeCIDR", "blockList.removeRange", "blockList.removeSubnet", "blockList.toJSON"], "status": "supported", "evidence": ["tests/corpus/5659-net-blocklist-socketaddress.ts", "tests/fixtures/node-types/net-blocklist.ts"] }, - { "symbols": ["readline.createInterface", "rl.close"], "status": "partial", "evidence": ["tests/corpus/1475-readline-closed-stdin.ts"] }, + { "symbols": ["readline.createInterface", "rl.close"], "status": "partial", "evidence": ["tests/corpus/1475-readline-closed-stdin.ts", "tests/corpus/5878-readline-dynamic-streams.mjs"] }, { "symbols": ["test", "it", "test.skip", "test.todo", "test.only", "describe", "suite", "before", "after", "beforeEach", "afterEach", "context.test", "context.skip", "context.todo", "context.assert"], "status": "partial", "evidence": ["tests/harness/node-test.test.ts"] }, { "symbols": ["http.request", "http.get", "http.createServer", "https.request", "https.get", "https.createServer", "request.end", "request.destroy", "request.setTimeout", "request.write", "response.end", "response.getHeader", "response.hasHeader", "response.removeHeader", "response.setHeader", "response.write", "response.writeHead", "headersTimeout", "requestTimeout", "timeout", "keepAliveTimeout", "keepAliveTimeoutBuffer"], "status": "partial", "evidence": ["tests/corpus/1780-http-res-surface.ts", "tests/corpus/2672-http-request-response-callback.ts", "tests/corpus/2689-http-server-timeout-properties.ts"] } ] diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index 0a0837389..f8fafe010 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -3839,7 +3839,10 @@ declare module "readline" { meta?: boolean; shift?: boolean; } - export interface Interface extends Disposable { + export class Interface implements Disposable { + /* createInterface creates interfaces; instanceof tells the two + * modules' classes apart. */ + protected constructor(); /* terminal says whether the interface edits lines as a terminal; * line and cursor are the terminal's (line is '' otherwise). history * is the history option itself (or the interface's own empty array); @@ -3954,7 +3957,10 @@ declare module "readline/promises" { /* 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 { + export class Interface implements Disposable { + /* createInterface creates interfaces; instanceof tells the two + * modules' classes apart. */ + protected constructor(); /* terminal says whether the interface edits lines as a terminal; * line and cursor are the terminal's (line is '' otherwise). history * is the history option itself (or the interface's own empty array); diff --git a/packages/compiler/src/backend/llvm/lib-dispatch.ts b/packages/compiler/src/backend/llvm/lib-dispatch.ts index daa8e08f3..2328839c9 100644 --- a/packages/compiler/src/backend/llvm/lib-dispatch.ts +++ b/packages/compiler/src/backend/llvm/lib-dispatch.ts @@ -82,12 +82,19 @@ export function emitIoLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue } // A signal that is not an AbortSignal throws ERR_INVALID_ARG_TYPE. if (mode & 8) host.emitPendingCheck(); + // readline/promises interfaces are instances of that module's class. + if (mode & 32) { + host.declare(`declare void @scr_rl_mark_promises(double)`); + B.line(`call void @scr_rl_mark_promises(double ${t})`); + } return { name: t, type: e.type }; } - if (e.fn === "rl.question") { + if (e.fn === "rl.question" || e.fn === "rl.questionDyn") { // The answer callback MOVES into the interface's registry; throws - // Node's use-after-close error (the may-throw seed). + // Node's use-after-close error (the may-throw seed), and with a + // checked-dynamic query that is not a string, ERR_INVALID_ARG_TYPE. host.usesTimers = true; + const fn = e.fn === "rl.question" ? "scr_rl_question" : "scr_rl_question_dyn"; const cbT = e.args[2]!.type; if (cbT.kind !== "func") throw new InternalCompilerError("llvm emitter bug: rl.question callback not a func"); @@ -100,29 +107,37 @@ export function emitIoLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue ? "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)`); + host.declare(`declare void @${fn}(double, ptr, ptr, ptr)`); B.line( - `call void @scr_rl_question(double ${args[0]!.name}, ptr ${args[1]!.name}, ptr ${args[2]!.name}, ptr @${adapter})`, + `call void @${fn}(double ${args[0]!.name}, ptr ${args[1]!.name}, ptr ${args[2]!.name}, ptr @${adapter})`, ); host.emitPendingCheck(); return { name: "", type: e.type }; } - if (e.fn === "rl.questionAsync" || e.fn === "rl.questionAsyncSignal") { - // The promise question never throws: closed interfaces and aborted - // signals reject the answered promise (+1). + if ( + e.fn === "rl.questionAsync" || + e.fn === "rl.questionAsyncSignal" || + e.fn === "rl.questionAsyncDyn" || + e.fn === "rl.questionAsyncSignalDyn" + ) { + // The promise question never throws: closed interfaces, aborted + // signals and queries that are not strings reject the answered + // promise (+1). host.usesTimers = true; const args = e.args.map((a) => host.emitExpr(a)); + const dyn = e.fn === "rl.questionAsyncDyn" || e.fn === "rl.questionAsyncSignalDyn"; + const fn = dyn ? "scr_rl_question_promise_dyn" : "scr_rl_question_promise"; let signal = "null"; let add = "null"; - if (e.fn === "rl.questionAsyncSignal") { + if (e.fn === "rl.questionAsyncSignal" || e.fn === "rl.questionAsyncSignalDyn") { 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)`); + host.declare(`declare ptr @${fn}(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})`, + `${t} = call ptr @${fn}(double ${args[0]!.name}, ptr ${args[1]!.name}, ptr ${signal}, ptr ${add})`, ); return host.own({ name: t, type: e.type }); } diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index e4c12058f..a713546a3 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -1023,6 +1023,7 @@ export const LIB_FN_SYMS: Record = { "rl.prompt": "scr_rl_prompt", "rl.promptWith": "scr_rl_prompt_with", "rl.setPrompt": "scr_rl_set_prompt", + "rl.setPromptDyn": "scr_rl_set_prompt_dyn", "rl.getPrompt": "scr_rl_get_prompt", "rl.write": "scr_rl_write", "rl.writeKey": "scr_rl_write_key", @@ -1038,6 +1039,9 @@ export const LIB_FN_SYMS: Record = { "rl.terminal": "scr_rl_terminal", "rl.line": "scr_rl_line", "rl.isTerminal": "scr_rl_is_terminal", + "rl.isPromises": "scr_rl_is_promises", + "rl.inputFromDyn": "scr_stream_rl_from_dyn", + "rl.outputFromDyn": "scr_stream_rl_from_dyn", "rl.cursor": "scr_rl_cursor", "rl.seq": "scr_rl_seq", "rl.moves": "scr_rl_moves", diff --git a/packages/compiler/src/frontend/lowering/builtins/readline.ts b/packages/compiler/src/frontend/lowering/builtins/readline.ts index 3a0e7ccf0..0c0da9a40 100644 --- a/packages/compiler/src/frontend/lowering/builtins/readline.ts +++ b/packages/compiler/src/frontend/lowering/builtins/readline.ts @@ -18,6 +18,7 @@ import { READLINE_DOCUMENTED_OPTIONS, fenceOrDropOptionKey } from "../surfaces.j import { streamSidesOf } from "../lower-stream.js"; import type { IrLibFn } from "../../../ir/builtin-signatures.js"; import { voidizedCallback } from "../lower-server.js"; +import { builtinImportOf } from "./module-bindings.js"; /** The ambient module declaring `d`, or null. */ function ambientModuleOf(d: ts.Node): string | null { @@ -270,6 +271,80 @@ function lowerReadlineUiCall( return { kind: "seqExpr", stmts, result: recv, type: F64, loc }; } +/** The module ("readline" or "readline/promises") whose Interface class + * `node` names, or null. */ +function interfaceClassModule(lowerer: Lowerer, node: ts.Expression): string | null { + const builtin = ts.isIdentifier(node) + ? builtinImportOf(lowerer, node) + : ts.isPropertyAccessExpression(node) && !node.questionDotToken + ? lowerer.builtinMemberOf(node) + : null; + if (builtin?.member !== "Interface") return null; + return builtin.module === "readline" || builtin.module === "readline/promises" + ? builtin.module + : null; +} + +/** `rl instanceof readline.Interface` and `rl instanceof + * readlinePromises.Interface` over a readline Interface: an interface is + * an instance of the class of the module that created it (Node's + * readline/promises Interface does not extend node:readline's). Null for + * other instanceof tests. */ +export function lowerReadlineInstanceof( + lowerer: Lowerer, + expr: ts.BinaryExpression, +): IrExpr | null { + const module = interfaceClassModule(lowerer, expr.right); + if (module === null || !isReadlineTyped(lowerer, expr.left)) return null; + const loc = locOf(expr); + const recv = lowerer.lowerExprExpecting(expr.left, F64); + const isPromises: IrExpr = { + kind: "libCall", + fn: "rl.isPromises", + args: [recv], + type: BOOL, + loc, + }; + if (module === "readline/promises") return isPromises; + return { kind: "unary", op: "!", operand: isPromises, type: BOOL, loc }; +} + +/** `rl[Symbol.dispose]()` on a readline Interface: Node's dispose method + * calls close(), which does nothing on a closed interface. Null for other + * element-access calls. */ +export function lowerReadlineDisposeCall(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null { + const access = call.expression; + if (!ts.isElementAccessExpression(access) || access.questionDotToken || call.questionDotToken) { + return null; + } + let key = access.argumentExpression; + while (ts.isParenthesizedExpression(key)) key = key.expression; + if ( + !ts.isPropertyAccessExpression(key) || + lowerer.stdlibGlobalMember(key, "Symbol") !== "dispose" + ) { + return null; + } + if (!isReadlineTyped(lowerer, access.expression)) return null; + if (call.arguments.length > 0) { + lowerer.noLowering("readline.Interface[Symbol.dispose] with arguments", call); + } + const recv = lowerer.lowerExprExpecting(access.expression, F64); + return { kind: "libCall", fn: "rl.close", args: [recv], type: VOID, loc: locOf(call) }; +} + +/** A question query or setPrompt argument: the string, or for a value + * that need not be one (JavaScript), the checked-dynamic value, which the + * runtime writes the way Node's kWriteToOutput does (dyn: true). */ +function lowerPromptArg(lowerer: Lowerer, node: ts.Expression): { value: IrExpr; dyn: boolean } { + if (lowerer.typeOf(node).flags & ts.TypeFlags.StringLike) { + return { value: lowerer.lowerExprExpecting(node, STRING), dyn: false }; + } + const value = lowerer.lowerExpr(node); + if (value.type.kind === "string") return { value, dyn: false }; + return { value: lowerer.coerceInto(node, value, DYN), dyn: true }; +} + /** Method calls on readline Interface receivers (both modules): * - question(query, cb) — the callback form; question(query, { signal }?) * — the promise form (readline/promises); @@ -303,18 +378,20 @@ export function lowerReadlineMethodCall( args.length <= 2 ) { const recv = receiver(); - const query = lowerer.lowerExprExpecting(args[0]!, STRING); + const query = lowerPromptArg(lowerer, args[0]!); 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 fn = query.dyn ? "rl.questionAsyncDyn" : "rl.questionAsync"; + return { kind: "libCall", fn, args: [recv, query.value], type, loc }; } const sig = lowerer.lowerExprExpecting(signal, DYN); - return { kind: "libCall", fn: "rl.questionAsyncSignal", args: [recv, query, sig], type, loc }; + const fn = query.dyn ? "rl.questionAsyncSignalDyn" : "rl.questionAsyncSignal"; + return { kind: "libCall", fn, args: [recv, query.value, sig], type, loc }; } if (name === "question" && args.length === 2) { const recv = receiver(); - const query = lowerer.lowerExprExpecting(args[0]!, STRING); + const query = lowerPromptArg(lowerer, args[0]!); const cb = lowerer.lowerExpr(args[1]!); if (cb.type.kind !== "func" || cb.type.ret.kind !== "void" || cb.type.params.length > 1) { lowerer.unsupported( @@ -331,7 +408,8 @@ export function lowerReadlineMethodCall( `question callbacks whose parameter is not 'string' (got '${lowerer.fmt(param)}')`, ); } - return { kind: "libCall", fn: "rl.question", args: [recv, query, cb], type: VOID, loc }; + const fn = query.dyn ? "rl.questionDyn" : "rl.question"; + return { kind: "libCall", fn, args: [recv, query.value, cb], type: VOID, loc }; } const simple: Record = { close: "rl.close", @@ -366,8 +444,9 @@ export function lowerReadlineMethodCall( } 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 }; + const prompt = lowerPromptArg(lowerer, args[0]!); + const fn = prompt.dyn ? "rl.setPromptDyn" : "rl.setPrompt"; + return { kind: "libCall", fn, args: [recv, prompt.value], type: VOID, loc }; } if (name === "getPrompt" && args.length === 0) { return { kind: "libCall", fn: "rl.getPrompt", args: [receiver()], type: STRING, loc }; @@ -871,6 +950,25 @@ export function lowerReadlineCursorCall( return { kind: "seqExpr", stmts, result, type: BOOL, loc }; } +/** A checked-dynamic input or output (a JavaScript function's result, an + * object's member, an element that may be undefined) as the stream it + * holds: the runtime refuses a value that is not a stream with that + * side, and answers null for an output of null or undefined. Other + * values pass through. */ +function streamFromDyn(lowerer: Lowerer, node: ts.Expression, output: boolean): IrExpr { + let v = lowerer.lowerExpr(node); + if (v.type.kind === "union") v = lowerer.coerceInto(node, v, DYN); + if (v.type.kind !== "dyn") return v; + const loc = locOf(node); + return { + kind: "libCall", + fn: output ? "rl.outputFromDyn" : "rl.inputFromDyn", + args: [v, boolLit(output, loc)], + type: { kind: "object", className: output ? "%Writable" : "%Readable" }, + loc, + }; +} + /** The interface's input: process.stdin (null) or a readable stream. */ function lowerInput(lowerer: Lowerer, node: ts.Expression): IrExpr | null { if ( @@ -879,7 +977,7 @@ function lowerInput(lowerer: Lowerer, node: ts.Expression): IrExpr | null { ) { return null; } - const v = lowerer.lowerExpr(node); + const v = streamFromDyn(lowerer, node, false); const info = v.type.kind === "object" ? lowerer.classes.get(v.type.className) : undefined; const sides = streamSidesOf(lowerer, info); if (sides !== "r" && sides !== "rw") { @@ -904,7 +1002,7 @@ function lowerOutput(lowerer: Lowerer, node: ts.Expression): IrExpr | "stdout" | return "none"; } if (ts.isIdentifier(node) && node.text === "undefined") return "none"; - const v = lowerer.lowerExpr(node); + const v = streamFromDyn(lowerer, node, true); const info = v.type.kind === "object" ? lowerer.classes.get(v.type.className) : undefined; const sides = streamSidesOf(lowerer, info); if (sides !== "w" && sides !== "rw") { @@ -923,8 +1021,8 @@ function lowerOutput(lowerer: Lowerer, node: ts.Expression): IrExpr | "stdout" | * 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. An interface that can be a + * prompt, 32 node:readline/promises), then the input and output streams + * when present, crlfDelay, the prompt and the signal when present. An interface that can be a * terminal one (a `terminal` option that is not false, or none with * process.stdout as the output, which is a terminal when it is a TTY) * goes through rl.terminal with the completer. */ @@ -1107,7 +1205,8 @@ export function lowerReadlineCreateCall( (typeof output === "object" ? 2 : 0) | (output === "stdout" ? 4 : 0) | (signal !== null ? 8 : 0) | - (prompt !== null ? 16 : 0); + (prompt !== null ? 16 : 0) | + (promises ? 32 : 0); const args: IrExpr[] = [numLit(mode, loc)]; if (input !== null) args.push(input); if (typeof output === "object") args.push(output); diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 67ca01b3f..7d538dac0 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -34,6 +34,7 @@ import { import { cjsExportCallIgnoresThis, cjsInFileExportGlobalOf } from "./cjs-exports-object.js"; import { lowerGenMethodCall } from "./lower-generators.js"; import { lowerClassSymbolMethodCall } from "./symbol-methods.js"; +import { lowerReadlineDisposeCall } from "./builtins/readline.js"; import { errorToStringCall } from "./error-methods.js"; import { lowerClassPrototypeAssign, lowerClassPrototypeDescriptors } from "./class-prototypes.js"; import { @@ -1648,6 +1649,8 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { if (timersDispose) return timersDispose; const symbolMethod = lowerClassSymbolMethodCall(lowerer, expr); if (symbolMethod) return symbolMethod; + const readlineDispose = lowerReadlineDisposeCall(lowerer, expr); + if (readlineDispose) return readlineDispose; lowerer.fenceUnsupportedFetchConstructorMember(expr.expression); lowerer.fenceStaticResponseMember(expr.expression, "call"); lowerer.fenceStaticHeadersMember(expr.expression, "call"); diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index 09407f71f..e7f6ed5d4 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -9,7 +9,7 @@ import { lowerUnionEquality, tagEqualityMayMissAlias } from "./strict-equality.j import { isOptionalProcessStreamProperty } from "./builtins/process.js"; import { lowerWorkerMetadata } from "./builtins/workers.js"; import { esmImportBindingKind } from "./builtins/module-bindings.js"; -import { lowerReadlineProperty } from "./builtins/readline.js"; +import { lowerReadlineInstanceof, lowerReadlineProperty } from "./builtins/readline.js"; import { lowerBlobInstanceof } from "./builtins/blob.js"; import { lowerWebAssemblyTypeof, refuseWebAssemblyUse } from "./builtins/webassembly.js"; import { lowerKeyObjectInstanceof } from "./builtins/key-object.js"; @@ -12816,6 +12816,8 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc if (keyObject) return keyObject; const netAddress = lowerNetAddressInstanceof(lowerer, expr); if (netAddress) return netAddress; + const readlineInterface = lowerReadlineInstanceof(lowerer, expr); + if (readlineInterface) return readlineInterface; } if (lowerer.isStdlibGlobal(expr.right, "SharedArrayBuffer")) { const value = lowerer.coerceInto(expr.left, lowerer.lowerExpr(expr.left), DYN); diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index fc4c383ff..82a463ae8 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -435,6 +435,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "https.requestFn", "https.requestFnCb", "rl.question", + "rl.questionDyn", "rl.checkOptions", "rl.checkCompleter", "readable.fromArr", @@ -457,6 +458,11 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "rl.resume", "rl.prompt", "rl.promptWith", + "rl.setPromptDyn", + "rl.getPrompt", + "rl.cursorPos", + "rl.inputFromDyn", + "rl.outputFromDyn", "rl.write", "rl.writeKey", "rl.cursor", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index df5bf0091..e85c45641 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -3959,6 +3959,14 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ argTypes: [F64, STRING, DYN], result: { kind: "promise", inner: STRING }, }, + /** The question forms with a checked-dynamic query (a JavaScript value + * that need not be a string). */ + "rl.questionDyn": { argTypes: [F64, DYN, null], result: VOID }, + "rl.questionAsyncDyn": { argTypes: [F64, DYN], result: { kind: "promise", inner: STRING } }, + "rl.questionAsyncSignalDyn": { + argTypes: [F64, DYN, DYN], + result: { kind: "promise", inner: STRING }, + }, "rl.close": { argTypes: [F64], result: VOID }, "rl.pause": { argTypes: [F64], result: VOID }, "rl.resume": { argTypes: [F64], result: VOID }, @@ -3966,6 +3974,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ /** prompt(preserveCursor): [handle, preserveCursor] */ "rl.promptWith": { argTypes: [F64, BOOL], result: VOID }, "rl.setPrompt": { argTypes: [F64, STRING], result: VOID }, + "rl.setPromptDyn": { argTypes: [F64, DYN], result: VOID }, "rl.getPrompt": { argTypes: [F64], result: STRING }, "rl.write": { argTypes: [F64, STRING], result: VOID }, /** write(data, key): [handle, hasData, data, hasKey, hasName, name, @@ -3998,6 +4007,14 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "rl.terminal": { argTypes: [F64, F64, BOOL, DYN, BOOL], result: VOID }, "rl.line": { argTypes: [F64], result: STRING }, "rl.isTerminal": { argTypes: [F64], result: BOOL }, + "rl.isPromises": { argTypes: [F64], result: BOOL }, + /** createInterface's input or output as a checked-dynamic value: + * [value, output] — the stream; refused when it is not one. */ + "rl.inputFromDyn": { argTypes: [DYN, BOOL], result: { kind: "object", className: "%Readable" } }, + "rl.outputFromDyn": { + argTypes: [DYN, BOOL], + result: { kind: "object", className: "%Writable" }, + }, "rl.cursor": { argTypes: [F64], result: F64 }, /** readline.cursorTo / moveCursor / clearLine / clearScreenDown: rl.seq * [kind (0-3), the first and second numbers, whether the second is diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 6e71fe91c..5fcb66055 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -192,6 +192,8 @@ const STREAM_LIB_FNS: ReadonlySet = new Set([ "fileHandle.createWriteStream", "fs.createReadStream", "fs.createWriteStream", + "rl.inputFromDyn", + "rl.outputFromDyn", "sp.finished", "sp.pipeline", "sp.pipelineStages", @@ -663,12 +665,16 @@ const PROCESS_EVENT_LIB_FNS: ReadonlySet = new Set([ "rl.question", "rl.questionAsync", "rl.questionAsyncSignal", + "rl.questionDyn", + "rl.questionAsyncDyn", + "rl.questionAsyncSignalDyn", "rl.close", "rl.pause", "rl.resume", "rl.prompt", "rl.promptWith", "rl.setPrompt", + "rl.setPromptDyn", "rl.getPrompt", "rl.write", "rl.writeKey", @@ -685,6 +691,7 @@ const PROCESS_EVENT_LIB_FNS: ReadonlySet = new Set([ "rl.terminal", "rl.line", "rl.isTerminal", + "rl.isPromises", "rl.cursor", "rl.seq", "rl.moves", diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5876-readline-dispose-instanceof.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5876-readline-dispose-instanceof.ts.json new file mode 100644 index 000000000..959c704b0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5876-readline-dispose-instanceof.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5876-readline-dispose-instanceof.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5877-readline-question-query.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5877-readline-question-query.cjs.json new file mode 100644 index 000000000..fe2bcfeee --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5877-readline-question-query.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5877-readline-question-query.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5878-readline-dynamic-streams.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5878-readline-dynamic-streams.mjs.json new file mode 100644 index 000000000..ddd665241 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5878-readline-dynamic-streams.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5878-readline-dynamic-streams.mjs" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_readline.c b/packages/runtime/src/scr_readline.c index e031a4e34..1bc48bbe7 100644 --- a/packages/runtime/src/scr_readline.c +++ b/packages/runtime/src/scr_readline.c @@ -185,11 +185,28 @@ void scr_rl_do_resume(ScrRl *rl) { } } +bool scr_rl_prompt_writable(ScrRl *rl) { + if (!rl->prompt_value) return true; + scr_dyn_arg_type_fail("stringToWrite", "of type string", rl->prompt_value); + return false; +} + static void scr_rl_prompt_now(ScrRl *rl) { if (rl->paused) scr_rl_do_resume(rl); if (scr_exc_pending()) return; if (rl->tty) scr_rl_tty_ops->prompt(rl, false); - else scr_rl_write_out(rl, rl->prompt); + else if (scr_rl_prompt_writable(rl)) scr_rl_write_out(rl, rl->prompt); +} + +/* setPrompt(this[kOldPrompt]) when a question is answered or cancelled */ +static void scr_rl_restore_prompt(ScrRl *rl) { + if (!rl->old_prompt) return; + scr_str_release(rl->prompt); + rl->prompt = rl->old_prompt; + rl->old_prompt = NULL; + if (rl->prompt_value) scr_dyn_release(rl->prompt_value); + rl->prompt_value = rl->old_prompt_value; + rl->old_prompt_value = NULL; } /* ── close ─────────────────────────────────────────────────────────── */ @@ -251,11 +268,7 @@ void scr_rl_on_line(ScrRl *rl, ScrStr *line) { ScrClosure *cb = rl->q_cb; 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; - } + scr_rl_restore_prompt(rl); fn(cb, line); scr_closure_release(cb); return; @@ -619,6 +632,8 @@ static void scr_rl_cleanup_atexit(void) { if (rl->out) rl->io->release(rl->out); scr_str_release(rl->prompt); if (rl->old_prompt) scr_str_release(rl->old_prompt); + if (rl->prompt_value) scr_dyn_release(rl->prompt_value); + if (rl->old_prompt_value) scr_dyn_release(rl->old_prompt_value); free(rl->line_buf); free(rl->parked); free(rl); @@ -748,7 +763,58 @@ void scr_rl_question(double id, ScrStr *query, ScrClosure *cb /*moves*/, ScrRlLi return; } rl->old_prompt = rl->prompt; + rl->old_prompt_value = rl->prompt_value; rl->prompt = scr_str_retain(query); + rl->prompt_value = NULL; + rl->q_cb = cb; + rl->q_fn = fn; + scr_rl_prompt_now(rl); +} + +/* A prompt that is not a string: String(value) for a primitive, which is + * what the terminal's line refresh and getCursorPos concatenate. An + * object's valueOf/toString would run on every refresh, and a symbol + * throws there, so a terminal interface refuses them (false, the refusal + * pending); another interface only writes the prompt, which throws for + * any value that is not a string, so it keeps ''. +1. */ +static bool scr_rl_prompt_text(ScrRl *rl, ScrDyn *value, ScrStr **out) { + if (scr_rl_primitive_prompt(value)) { + *out = scr_dyn_string_coerce(value); + return true; + } + if (!rl->tty) { + *out = scr_str_new("", 0); + return true; + } + static const char m[] = + "a terminal readline prompt that is an object or a symbol has no lowering"; + scr_throw_error_msg_code(SCR_ERR_ERROR, m, sizeof m - 1, "SC2020"); + return false; +} + +/* question(query, cb) with a checked-dynamic query: a string goes through + * scr_rl_question. Another value becomes the prompt, the callback is + * kept, and the prompt write throws ERR_INVALID_ARG_TYPE on an interface + * that is not a terminal (the question stays pending, as in Node). */ +void scr_rl_question_dyn(double id, ScrDyn *query, ScrClosure *cb /*moves*/, ScrRlLineFn fn) { + if (query->kind == SCR_DYN_STR) { + scr_rl_question(id, query->v.str, cb, fn); + return; + } + ScrRl *rl = scr_rl_find(id); + if (!rl || rl->closed || rl->q_cb) { + scr_rl_question(id, NULL, cb, fn); /* the closed throw, or the re-prompt */ + return; + } + ScrStr *text; + if (!scr_rl_prompt_text(rl, query, &text)) { + scr_closure_release(cb); + return; + } + rl->old_prompt = rl->prompt; + rl->old_prompt_value = rl->prompt_value; + rl->prompt = text; + rl->prompt_value = scr_dyn_retain(query); rl->q_cb = cb; rl->q_fn = fn; scr_rl_prompt_now(rl); @@ -857,11 +923,7 @@ static ScrDyn *scr_rl_ask_abort_thunk(ScrClosure *clo, ScrDyn *const *args, size 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; - } + scr_rl_restore_prompt(rl); if (rl->tty) { scr_rl_tty_ops->clear_line(rl); } else { @@ -877,8 +939,11 @@ static ScrDyn *scr_rl_ask_abort_thunk(ScrClosure *clo, ScrDyn *const *args, size /* 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) { + * the signal is (or becomes) aborted. A checked-dynamic query (query_dyn, + * query NULL) that is not a string rejects with what question() throws, + * as Node's promise executor does. */ +static ScrPromise *scr_rl_ask(double id, ScrStr *query, ScrDyn *query_dyn, ScrDyn *signal, + ScrAbortAddFn add) { RlAsk *a = calloc(1, sizeof *a); if (!a) scr_rl_oom(); a->rc = 1; @@ -928,10 +993,29 @@ ScrPromise *scr_rl_question_promise(double id, ScrStr *query, ScrDyn *signal, Sc 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); + if (!query_dyn) { + scr_rl_question(id, query, cb, &scr_rl_ask_answer); + return result; + } + scr_rl_ask_retain_v(a); + scr_rl_question_dyn(id, query_dyn, cb, &scr_rl_ask_answer); + if (scr_exc_pending() && !a->settled) { + a->settled = true; + scr_promise_reject_pending(a->p); + } + scr_rl_ask_release_v(a); return result; } +ScrPromise *scr_rl_question_promise(double id, ScrStr *query, ScrDyn *signal, ScrAbortAddFn add) { + return scr_rl_ask(id, query, NULL, signal, add); +} + +ScrPromise *scr_rl_question_promise_dyn(double id, ScrDyn *query, ScrDyn *signal, + ScrAbortAddFn add) { + return scr_rl_ask(id, NULL, query, signal, add); +} + void scr_rl_close(double id) { ScrRl *rl = scr_rl_find(id); if (rl) scr_rl_settle_close(rl); /* twice is a no-op, like Node */ @@ -985,13 +1069,48 @@ void scr_rl_set_prompt(double id, ScrStr *prompt) { if (!rl) return; scr_str_release(rl->prompt); rl->prompt = scr_str_retain(prompt); + if (rl->prompt_value) scr_dyn_release(rl->prompt_value); + rl->prompt_value = NULL; +} + +/* setPrompt(value) with a checked-dynamic value; borrows. */ +void scr_rl_set_prompt_dyn(double id, ScrDyn *prompt) { + if (prompt->kind == SCR_DYN_STR) { + scr_rl_set_prompt(id, prompt->v.str); + return; + } + ScrRl *rl = scr_rl_find(id); + if (!rl) return; + ScrStr *text; + if (!scr_rl_prompt_text(rl, prompt, &text)) return; + scr_str_release(rl->prompt); + rl->prompt = text; + if (rl->prompt_value) scr_dyn_release(rl->prompt_value); + rl->prompt_value = scr_dyn_retain(prompt); } ScrStr *scr_rl_get_prompt(double id) { ScrRl *rl = scr_rl_find(id); + if (rl && rl->prompt_value) { + static const char m[] = "rl.getPrompt() of a prompt that is not a string has no lowering"; + scr_throw_error_msg_code(SCR_ERR_ERROR, m, sizeof m - 1, "SC2020"); + return scr_str_new("", 0); + } return rl ? scr_str_retain(rl->prompt) : scr_str_new("", 0); } +/* Called after a node:readline/promises interface opens: its Interface + * class is not node:readline's, which instanceof tells apart. */ +void scr_rl_mark_promises(double id) { + ScrRl *rl = scr_rl_find(id); + if (rl) rl->promises = true; +} + +bool scr_rl_is_promises(double id) { + ScrRl *rl = scr_rl_find(id); + return rl && rl->promises; +} + bool scr_rl_is_terminal(double id) { ScrRl *rl = scr_rl_find(id); return rl && rl->tty; diff --git a/packages/runtime/src/scr_readline.h b/packages/runtime/src/scr_readline.h index 9385f6dd7..4557dcb4d 100644 --- a/packages/runtime/src/scr_readline.h +++ b/packages/runtime/src/scr_readline.h @@ -47,13 +47,22 @@ void scr_rl_input_error(double id, ScrError *err); 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); +/* The same with a checked-dynamic query (borrowed). A query that is not a + * string makes the prompt write throw ERR_INVALID_ARG_TYPE (the promise + * form rejects with it) unless the interface is a terminal; the question + * stays pending, as in Node. */ +void scr_rl_question_dyn(double id, ScrDyn *query, ScrClosure *cb /*moves*/, + void (*fn)(ScrClosure *, ScrStr *)); +ScrPromise *scr_rl_question_promise_dyn(double id, ScrDyn *query, ScrDyn *signal, + ScrAbortAddFn add); void scr_rl_close(double id); 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_prompt_with(double id, bool preserve); /* prompt(preserveCursor); may throw */ void scr_rl_set_prompt(double id, ScrStr *prompt); -ScrStr *scr_rl_get_prompt(double id); /* +1 */ +void scr_rl_set_prompt_dyn(double id, ScrDyn *prompt); /* borrows; may throw */ +ScrStr *scr_rl_get_prompt(double id); /* +1; may throw (a prompt that is not a string) */ 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); /* createInterface's history and editing options, in Node's order: the @@ -83,6 +92,8 @@ double scr_rl_num_prop(double id, double which); * when there is a completer (the inspect unit links only then). */ void scr_rl_terminal(double id, double mode, bool on, ScrDyn *completer, bool promises, ScrStr *(*inspect)(ScrDyn *, double, double)); +void scr_rl_mark_promises(double id); +bool scr_rl_is_promises(double id); bool scr_rl_is_terminal(double id); /* readline.cursorTo / moveCursor / clearLine / clearScreenDown: the data * written (+1; kind 0-3), after the argument checks (may throw), and @@ -135,6 +146,9 @@ void scr_rl_error_thunk_dyn(ScrClosure *cb, ScrError *err); * 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 "> "). */ +/* A checked-dynamic input or output as its stream (+1; NULL for an output + * of null or undefined); refuses other values (may throw). */ +ScrStream *scr_stream_rl_from_dyn(const ScrDyn *value, bool output); 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); diff --git a/packages/runtime/src/scr_readline_internal.h b/packages/runtime/src/scr_readline_internal.h index 0fe2e2cb2..49ef42f39 100644 --- a/packages/runtime/src/scr_readline_internal.h +++ b/packages/runtime/src/scr_readline_internal.h @@ -56,11 +56,17 @@ typedef struct ScrRl { bool stdin_in; /* reads process.stdin through the shared consumer */ bool dead; /* stdin only: created after stdin ended */ bool paused; + bool promises; /* created by node:readline/promises */ 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; + /* A prompt that is not a string (setPrompt or question with another + * value), +1, or NULL; prompt then holds String(value) for a terminal + * interface. Writing it the way Node's kWriteToOutput does throws + * ERR_INVALID_ARG_TYPE. old_prompt_value is old_prompt's. */ + ScrDyn *prompt_value, *old_prompt_value; ScrClosure *q_cb; /* the pending question's callback (owned) */ ScrRlLineFn q_fn; /* the last promise question (kQuestionReject), which the terminal's @@ -114,6 +120,14 @@ ScrArr *scr_rl_history_of(ScrRl *rl); /* borrowed; created on first use */ void scr_rl_emit(ScrRl *rl, int ev, ScrStr *line, ScrError *err); void scr_rl_emit_history(ScrRl *rl); void scr_rl_write_out(ScrRl *rl, ScrStr *text); +/* kWriteToOutput(prompt)'s validateString: false with ERR_INVALID_ARG_TYPE + * pending when the prompt is not a string. */ +bool scr_rl_prompt_writable(ScrRl *rl); +/* A prompt value whose String() the interface keeps in prompt. */ +static inline bool scr_rl_primitive_prompt(const ScrDyn *v) { + return v->kind == SCR_DYN_NULL || v->kind == SCR_DYN_UNDEF || v->kind == SCR_DYN_BOOL || + v->kind == SCR_DYN_NUM || v->kind == SCR_DYN_BIGINT; +} /* kOnLine: a pending question takes the line, else 'line'. Moves line. */ void scr_rl_on_line(ScrRl *rl, ScrStr *line); void scr_rl_do_pause(ScrRl *rl); diff --git a/packages/runtime/src/scr_readline_tty.c b/packages/runtime/src/scr_readline_tty.c index 38271411f..1c5fcd5f6 100644 --- a/packages/runtime/src/scr_readline_tty.c +++ b/packages/runtime/src/scr_readline_tty.c @@ -2255,7 +2255,7 @@ static void tty_prompt(ScrRl *rl, bool preserve) { bool dumb = term && term->len == 4 && memcmp(term->data, "dumb", 4) == 0; if (term) scr_str_release(term); if (dumb) { - scr_rl_write_out(rl, rl->prompt); + if (scr_rl_prompt_writable(rl)) scr_rl_write_out(rl, rl->prompt); return; } if (!preserve) t->cursor = 0; @@ -2333,6 +2333,12 @@ double scr_rl_cursor_pos(double id, double which) { if (rl->tty) { pos = tty_cursor_pos(rl); } else { + if (rl->prompt_value && !scr_rl_primitive_prompt(rl->prompt_value)) { + static const char m[] = + "rl.getCursorPos() with a prompt that is an object or a symbol has no lowering"; + scr_throw_error_msg_code(SCR_ERR_ERROR, m, sizeof m - 1, "SC2020"); + return 0; + } ScrRlTty probe = {0}; probe.columns = INFINITY; rl->tty = &probe; diff --git a/packages/runtime/src/scr_stream.c b/packages/runtime/src/scr_stream.c index 244e2ec5d..6087228a1 100644 --- a/packages/runtime/src/scr_stream.c +++ b/packages/runtime/src/scr_stream.c @@ -4728,6 +4728,31 @@ static void scr_stream_rl_hooks_free(void) { } } +/* createInterface's input or output given as a checked-dynamic value (a + * JavaScript function's result, an object's member): the stream (+1). + * An output of null or undefined is NULL (no output). A value that is + * not a stream with a readable (input) or writable (output) side is + * refused. */ +ScrStream *scr_stream_rl_from_dyn(const ScrDyn *value, bool output) { + if (output && (value->kind == SCR_DYN_NULL || value->kind == SCR_DYN_UNDEF)) return NULL; + if (value->kind == SCR_DYN_TYPED_REF && value->v.typed_ref.type_key_len > 7 && + !memcmp(value->v.typed_ref.type_key, "object:", 7)) { + ScrStream *stream = scr_dyn_typed_ref_unbox(value); + const ScrVt *vt = stream->vt; + bool duplex = vt == &scr_duplex_vt || vt == &scr_transform_vt || vt == &scr_passthrough_vt || + scr_vt_within(vt, &scr_duplex_vt); + bool ok = duplex || (output ? vt == &scr_writable_vt || scr_vt_within(vt, &scr_writable_vt) + : vt == &scr_readable_vt || scr_vt_within(vt, &scr_readable_vt)); + if (ok) return stream; + value->v.typed_ref.release(stream); + } + static const char in_m[] = "createInterface with an input that is not a readable stream has no lowering"; + static const char out_m[] = "createInterface with an output that is not a writable stream has no lowering"; + if (output) scr_throw_error_msg_code(SCR_ERR_ERROR, out_m, sizeof out_m - 1, "SC2020"); + else scr_throw_error_msg_code(SCR_ERR_ERROR, in_m, sizeof in_m - 1, "SC2020"); + return NULL; +} + 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, diff --git a/tests/corpus/5876-readline-dispose-instanceof.ts b/tests/corpus/5876-readline-dispose-instanceof.ts new file mode 100644 index 000000000..63f058e9b --- /dev/null +++ b/tests/corpus/5876-readline-dispose-instanceof.ts @@ -0,0 +1,28 @@ +// rl[Symbol.dispose]() closes the interface as close() does, and a second +// call does nothing. instanceof tells node:readline's Interface from +// node:readline/promises' one. +import * as readline from "node:readline"; +import * as readlinePromises from "node:readline/promises"; +import { PassThrough } from "node:stream"; + +const rl = readline.createInterface({ input: new PassThrough() }); +rl.on("close", () => console.log("close 1")); +rl[Symbol.dispose](); +console.log("disposed"); +rl[Symbol.dispose](); +rl.close(); +console.log("after second dispose"); + +const prl = readlinePromises.createInterface({ input: new PassThrough() }); +prl.on("close", () => console.log("close 2")); +console.log(rl instanceof readline.Interface, rl instanceof readlinePromises.Interface); +console.log(prl instanceof readline.Interface, prl instanceof readlinePromises.Interface); +prl[Symbol.dispose](); + +function make(): readline.Interface { + return readline.createInterface({ input: new PassThrough() }); +} +const made = make(); +made.on("close", () => console.log("close 3")); +made[Symbol.dispose](); +console.log("done"); diff --git a/tests/corpus/5877-readline-question-query.cjs b/tests/corpus/5877-readline-question-query.cjs new file mode 100644 index 000000000..734311b16 --- /dev/null +++ b/tests/corpus/5877-readline-question-query.cjs @@ -0,0 +1,51 @@ +// question() with a query that is not a string: a non-terminal interface +// throws (callback form) or rejects (readline/promises) with Node's +// ERR_INVALID_ARG_TYPE from the output write, and the question stays +// pending; a terminal interface writes the query as a string. +const readline = require("readline"); +const readlinePromises = require("readline/promises"); +const { PassThrough } = require("stream"); + +function capture(name) { + const output = new PassThrough(); + output.on("data", (d) => console.log(name, JSON.stringify(String(d)))); + return output; +} + +async function main() { + const input = new PassThrough(); + const rl = readline.createInterface({ input, output: capture("out") }); + rl.on("line", (line) => console.log("line", line)); + try { + rl.question(42, (answer) => console.log("answer", answer)); + } catch (e) { + console.log(e.name, e.code, e.message); + } + try { + rl.prompt(); + } catch (e) { + console.log("prompt", e.code, e.message); + } + input.write("first\nsecond\n"); + await new Promise((resolve) => setImmediate(resolve)); + console.log("prompt after", JSON.stringify(rl.getPrompt())); + rl.close(); + + for (const query of [undefined, null, { a: 1 }, [1, 2], true]) { + const prl = readlinePromises.createInterface({ input: new PassThrough() }); + try { + await prl.question(query); + } catch (e) { + console.log("rejected", e.code, e.message); + } + prl.close(); + } + + const tin = new PassThrough(); + const trl = readline.createInterface({ input: tin, output: capture("tty"), terminal: true }); + trl.question(7, (answer) => console.log("tty answer", answer)); + tin.write("x\r"); + await new Promise((resolve) => setImmediate(resolve)); + trl.close(); +} +main(); diff --git a/tests/corpus/5878-readline-dynamic-streams.mjs b/tests/corpus/5878-readline-dynamic-streams.mjs new file mode 100644 index 000000000..b1b9f4ee5 --- /dev/null +++ b/tests/corpus/5878-readline-dynamic-streams.mjs @@ -0,0 +1,41 @@ +// createInterface over an input or output that is a function's result, +// an object's member or an array element. +import * as readline from "node:readline"; +import * as readlinePromises from "node:readline/promises"; +import { PassThrough, Readable, Writable } from "node:stream"; + +function source(lines) { + return Readable.from([lines.join("\n") + "\n"]); +} +const streams = { input: source(["a", "b"]), output: new PassThrough() }; +streams.output.on("data", (d) => console.log("out", JSON.stringify(String(d)))); + +const rl1 = readline.createInterface({ input: source(["one", "two"]) }); +for await (const line of rl1) console.log("rl1", line); + +const rl2 = readline.createInterface({ input: streams.input, output: streams.output, prompt: "? " }); +rl2.prompt(); +for await (const line of rl2) console.log("rl2", line); + +const inputs = [source(["x"]), source(["y"])]; +for (const [i, input] of inputs.entries()) { + const rl = readline.createInterface(input); + rl.on("line", (line) => console.log("rl3", i, line)); + await new Promise((resolve) => { + rl.on("close", () => resolve(undefined)); + }); +} + +const sink = new Writable({ + write(chunk, _encoding, callback) { + console.log("sink", JSON.stringify(String(chunk))); + callback(); + }, +}); +const make = () => ({ input: new PassThrough(), output: sink }); +const opts = make(); +const prl = readlinePromises.createInterface(opts.input, opts.output); +const answer = prl.question("name? "); +opts.input.write("ada\n"); +console.log("answer", await answer); +prl.close(); diff --git a/tests/harness/corpus-costs.json b/tests/harness/corpus-costs.json index 62703f6a4..b4d0ff3aa 100644 --- a/tests/harness/corpus-costs.json +++ b/tests/harness/corpus-costs.json @@ -2129,6 +2129,9 @@ "5873-readline-promises-namespace.cjs": 2023, "5874-worker-stdio-streams.ts": 2839, "5875-worker-stdout-pipe.cjs": 2740, + "5876-readline-dispose-instanceof.ts": 2465, + "5877-readline-question-query.cjs": 3233, + "5878-readline-dynamic-streams.mjs": 2925, "5879-worker-stdin.ts": 3158, "5880-emitter-leak-warning-delivery.ts": 3418, "5881-inspect-weak-collections.ts": 2615, @@ -5218,6 +5221,9 @@ "5873-readline-promises-namespace.cjs": 3811, "5874-worker-stdio-streams.ts": 27801, "5875-worker-stdout-pipe.cjs": 31391, + "5876-readline-dispose-instanceof.ts": 29361, + "5877-readline-question-query.cjs": 170940, + "5878-readline-dynamic-streams.mjs": 25503, "5879-worker-stdin.ts": 33232, "5880-emitter-leak-warning-delivery.ts": 4319, "5881-inspect-weak-collections.ts": 2915,