From 2f3e25b431fc13f0f8a08fdb0505b0c277dc734a Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 08:35:56 -0500 Subject: [PATCH] Accept Buffer-compatible chunk annotations and keep super(undefined) in byte mode --- .../src/frontend/lowering/lower-stream.ts | 18 ++++++---- tests/corpus/7240-objectmode-streams.ts | 33 ++++++++++++++++- .../stream-byte-mode-chunk-fences.ts | 36 +++++++++++++++++++ .../stream-byte-mode-chunk-fences.ts.txt | 8 +++++ 4 files changed, 87 insertions(+), 8 deletions(-) create mode 100644 tests/diagnostics/stream-byte-mode-chunk-fences.ts create mode 100644 tests/harness/__snapshots__/stream-byte-mode-chunk-fences.ts.txt diff --git a/packages/compiler/src/frontend/lowering/lower-stream.ts b/packages/compiler/src/frontend/lowering/lower-stream.ts index cb81348eb..6540b3a99 100644 --- a/packages/compiler/src/frontend/lowering/lower-stream.ts +++ b/packages/compiler/src/frontend/lowering/lower-stream.ts @@ -312,11 +312,13 @@ function lowerStreamCallback( // A write or transform chunk of another type converts from the // chunk's value when the callback runs (an object-mode side's value), // or throws a TypeError there — on a side known to be byte mode the - // chunk is a Buffer, so the declared type is the fence's; an encoding - // of another type (`string | null | undefined`) converts from the - // encoding argument's value. + // chunk is a Buffer, so the declared type must be Node's or a + // supertype of it (`Buffer | string`); an encoding of another type + // (`string | null | undefined`) converts from the encoding argument's + // value. if ( - ((i === 0 && chunkValues) || i === 1) && + ((i === 0 && (chunkValues || widensInto(lowerer, fullTuple[0]!, funcType.params[0]!, loc))) || + i === 1) && (which === "write" || which === "transform") && funcType.params[i]!.kind !== "func" && canConvertToDyn( @@ -1369,11 +1371,13 @@ export function lowerStreamSuperCall( // set objectMode, and a literal says so with objectMode (or a Duplex's // writableObjectMode) true; otherwise _write and _transform receive // Buffers. + // super(undefined) and super(null) pass no options (followOptionsArg + // answers null), so their sides are byte mode too. const optsNode = argNodes[0] !== undefined ? followOptionsArg(lowerer, argNodes[0]) : null; const chunkValues = - argNodes[0] !== undefined && - (optsNode === null || - !ts.isObjectLiteralExpression(optsNode) || + optsNode !== null && + optsNode !== undefined && + (!ts.isObjectLiteralExpression(optsNode) || literalWritesValues(optsNode, cls !== "%Readable" && cls !== "%Writable")); const fallback = (name: string): IrExpr | null => { const methodName = UNDERSCORE_METHODS.get(name); diff --git a/tests/corpus/7240-objectmode-streams.ts b/tests/corpus/7240-objectmode-streams.ts index 9316ff480..47f799eee 100644 --- a/tests/corpus/7240-objectmode-streams.ts +++ b/tests/corpus/7240-objectmode-streams.ts @@ -2,7 +2,8 @@ // and Writable in pipeline(), typed 'data' listeners, a Transform with // readableObjectMode parsing bytes into records, an object-mode Duplex, // a PassThrough's object-counted high-water mark, and Readable.from over -// a Map, a Set of records and an array mixing strings and Buffers. +// a Map, a Set of records and an array mixing strings and Buffers, and +// byte-mode chunk parameters typed Buffer | string. import { Duplex, PassThrough, Readable, Transform, Writable } from "node:stream"; import { pipeline } from "node:stream/promises"; @@ -119,6 +120,34 @@ async function collections(): Promise { for await (const t of Readable.from(tags)) console.log("set", JSON.stringify(t)); } +// Byte-mode sides pass Buffers: a chunk parameter typed as a supertype +// of Buffer takes them, inline and as an underscore method, and +// super(undefined) leaves the stream in byte mode. +class Collector extends Writable { + constructor() { + super(undefined); + } + _write(chunk: Buffer | string, _encoding: BufferEncoding, cb: (err?: Error | null) => void): void { + console.log("collector", Buffer.isBuffer(chunk), typeof chunk === "string" ? chunk : chunk.toString()); + cb(); + } +} + +async function byteChunks(): Promise { + const inline = new Writable({ + write(chunk: Buffer | string, _encoding: BufferEncoding, cb: (err?: Error | null) => void): void { + console.log("inline", Buffer.isBuffer(chunk), typeof chunk === "string" ? chunk : chunk.toString()); + cb(); + }, + }); + inline.write("one"); + inline.end(Buffer.from("two")); + await new Promise((resolve) => inline.on("finish", () => resolve())); + const collector = new Collector(); + collector.end("three"); + await new Promise((resolve) => collector.on("finish", () => resolve())); +} + async function main(): Promise { await records(); await collections(); @@ -126,5 +155,7 @@ async function main(): Promise { sided(); duplex(); passthrough(); + await new Promise((resolve) => setTimeout(resolve, 5)); + await byteChunks(); } main(); diff --git a/tests/diagnostics/stream-byte-mode-chunk-fences.ts b/tests/diagnostics/stream-byte-mode-chunk-fences.ts new file mode 100644 index 000000000..6afd1d794 --- /dev/null +++ b/tests/diagnostics/stream-byte-mode-chunk-fences.ts @@ -0,0 +1,36 @@ +// A byte-mode stream's write chunks are Buffers: an underscore method +// whose chunk parameter has another type is fenced, and super(undefined) +// passes no options, so the stream is in byte mode. A parameter that +// admits the Buffer (Buffer | string) is not fenced, inline or as a +// method. +import { Writable } from "node:stream"; + +class Numbers extends Writable { + constructor() { + super(undefined); + } + _write(chunk: number, _encoding: BufferEncoding, cb: (err?: Error | null) => void): void { + console.log(chunk); + cb(); + } +} +new Numbers().end("x"); + +const accepted = new Writable({ + write(chunk: Buffer | string, _encoding: BufferEncoding, cb: (err?: Error | null) => void): void { + console.log(typeof chunk === "string" ? chunk : chunk.toString()); + cb(); + }, +}); +accepted.end("z"); + +class Wide extends Writable { + constructor() { + super(undefined); + } + _write(chunk: Buffer | string, _encoding: BufferEncoding, cb: (err?: Error | null) => void): void { + console.log(typeof chunk === "string" ? chunk : chunk.toString()); + cb(); + } +} +new Wide().end("w"); diff --git a/tests/harness/__snapshots__/stream-byte-mode-chunk-fences.ts.txt b/tests/harness/__snapshots__/stream-byte-mode-chunk-fences.ts.txt new file mode 100644 index 000000000..39a06dfd7 --- /dev/null +++ b/tests/harness/__snapshots__/stream-byte-mode-chunk-fences.ts.txt @@ -0,0 +1,8 @@ +stream-byte-mode-chunk-fences.ts:10:5 - error SC2020: 'a '_write' method whose parameter 0 is 'number' where Node passes 'Uint8Array'' is part of the standard library types but has no scriptc lowering yet + + 9 | constructor() { + 10 | super(undefined); + | ^~~~~~~~~~~~~~~~ + 11 | } + + hint: annotate the parameter with the Node signature's type or a supertype of it (chunks are Buffers here) \ No newline at end of file