diff --git a/docs/content/limitations/builtin-apis/streams-and-readline.mdx b/docs/content/limitations/builtin-apis/streams-and-readline.mdx index cdd5df47e..0ac0ca3a6 100644 --- a/docs/content/limitations/builtin-apis/streams-and-readline.mdx +++ b/docs/content/limitations/builtin-apis/streams-and-readline.mdx @@ -18,6 +18,10 @@ 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. diff --git a/internal/compatibility/generated/node-v24-backlog.json b/internal/compatibility/generated/node-v24-backlog.json index a02446908..817b723e3 100644 --- a/internal/compatibility/generated/node-v24-backlog.json +++ b/internal/compatibility/generated/node-v24-backlog.json @@ -58969,7 +58969,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" ] } } @@ -59001,7 +59003,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" ] } } @@ -59229,7 +59233,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" ] } } @@ -59290,7 +59296,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" ] } } @@ -59322,7 +59330,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" ] } } @@ -59405,7 +59415,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" ] } } @@ -59544,7 +59555,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" ] } } @@ -59574,7 +59586,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" ] } } @@ -59604,7 +59618,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" ] } } @@ -59634,7 +59650,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" ] } } @@ -59664,7 +59682,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" ] } } @@ -59694,7 +59714,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" ] } } @@ -59724,7 +59746,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" ] } } @@ -59754,7 +59778,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" ] } } @@ -59784,7 +59810,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" ] } } @@ -59904,7 +59932,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" ] } } @@ -59934,7 +59964,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" ] } } @@ -59964,7 +59996,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" ] } } @@ -60022,7 +60056,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" ] } } @@ -60052,7 +60088,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 3690ce077..3fc7b1a54 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -82492,7 +82492,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" @@ -82546,7 +82549,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" @@ -82577,7 +82582,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" @@ -82797,7 +82804,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" @@ -82879,7 +82888,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" @@ -82910,7 +82921,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" @@ -82990,7 +83003,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" @@ -83146,7 +83160,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" @@ -83175,7 +83190,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" @@ -83204,7 +83221,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" @@ -83233,7 +83252,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" @@ -83262,7 +83283,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" @@ -83291,7 +83314,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" @@ -83320,7 +83345,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" @@ -83349,7 +83376,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" @@ -83378,7 +83407,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" @@ -83494,7 +83525,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" @@ -83523,7 +83556,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" @@ -83552,7 +83587,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" @@ -83608,7 +83645,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" @@ -83637,7 +83676,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 855d065f7..668c3740c 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -199,13 +199,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"]}, @@ -348,7 +348,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"] }, @@ -416,7 +416,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 628e14f25..3bf97fe6a 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -4006,7 +4006,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); @@ -4121,7 +4124,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 31d7ca5c2..16759a592 100644 --- a/packages/compiler/src/backend/llvm/lib-dispatch.ts +++ b/packages/compiler/src/backend/llvm/lib-dispatch.ts @@ -83,12 +83,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"); @@ -101,29 +108,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 d01374235..44741edc8 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -1040,6 +1040,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", @@ -1055,6 +1056,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 6baeb65d1..bcf77a39b 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.nodes.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/expressions/relational.ts b/packages/compiler/src/frontend/lowering/expressions/relational.ts index b92c5e7e5..619c0fe37 100644 --- a/packages/compiler/src/frontend/lowering/expressions/relational.ts +++ b/packages/compiler/src/frontend/lowering/expressions/relational.ts @@ -32,6 +32,7 @@ import { lowerZlibInstanceOf } from "../builtins/compression.js"; import { numLit, strLit, varRef } from "../../../ir/build.js"; import { lowerDisposableStackInstanceOf } from "../builtins/disposable-stack.js"; import { lowerBlobInstanceof } from "../builtins/blob.js"; +import { lowerReadlineInstanceof } from "../builtins/readline.js"; import { lowerKeyObjectInstanceof } from "../builtins/key-object.js"; import { lowerCryptoKeyInstanceof } from "../builtins/webcrypto.js"; import { lowerNetAddressInstanceof } from "../builtins/net-address.js"; @@ -179,6 +180,8 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc if (cryptoKey) return cryptoKey; 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/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 699f04ec8..ed549e4ff 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -35,6 +35,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 { @@ -1683,6 +1684,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/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index 4d4193f73..826151009 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -448,6 +448,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "https.requestFn", "https.requestFnCb", "rl.question", + "rl.questionDyn", "rl.checkOptions", "rl.checkCompleter", "readable.fromArr", @@ -470,6 +471,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 2a0e5124a..c55444e60 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -4019,6 +4019,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 }, @@ -4026,6 +4034,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, @@ -4058,6 +4067,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 34ec76f8d..9abe15345 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -151,6 +151,8 @@ const STREAM_LIB_FNS: ReadonlySet = new Set([ "fileHandle.createWriteStream", "fs.createReadStream", "fs.createWriteStream", + "rl.inputFromDyn", + "rl.outputFromDyn", "sp.finished", "sp.pipeline", "sp.pipelineStages", @@ -356,12 +358,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", @@ -378,6 +384,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 c2d27e7ce..5f0bb493d 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 30899d72b..b18e2ab28 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, @@ -5223,6 +5226,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,