From 23ec584deb26ef1d50834d548abd0c1a7b463604 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 21 Oct 2026 13:24:10 -0500 Subject: [PATCH] Read nullable union chunks of push and write by their representation Readable.push and Writable.write with a union chunk (Buffer, string and null arms) read the chunk's tag and payload from a tagged union box. A union with one reference arm and null, such as `string | null` or `Buffer | null`, is the arm pointer itself, so `push(line)` with `line: string | null` and `push(done ? null : text)` read past the string and crashed. Both now read the tag and payload with unionTag and unionPeek, as the other union consumers do. unshift and end refuse union chunks and are unchanged. --- .../compiler/src/backend/llvm/lib-stream.ts | 12 +-- .../6820-stream-push-nullable-chunks.ts.json | 6 ++ ...6821-stream-write-nullable-chunks.cjs.json | 6 ++ .../6820-stream-push-nullable-chunks.ts | 77 +++++++++++++++++++ .../6821-stream-write-nullable-chunks.cjs | 23 ++++++ .../6820-stream-push-nullable-chunks.ts.json | 1 + ...6821-stream-write-nullable-chunks.cjs.json | 1 + 7 files changed, 118 insertions(+), 8 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6820-stream-push-nullable-chunks.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6821-stream-write-nullable-chunks.cjs.json create mode 100644 tests/corpus/6820-stream-push-nullable-chunks.ts create mode 100644 tests/corpus/6821-stream-write-nullable-chunks.cjs create mode 100644 tests/harness/corpus-costs/6820-stream-push-nullable-chunks.ts.json create mode 100644 tests/harness/corpus-costs/6821-stream-write-nullable-chunks.cjs.json diff --git a/packages/compiler/src/backend/llvm/lib-stream.ts b/packages/compiler/src/backend/llvm/lib-stream.ts index a319d0257..d3b4b1f5b 100644 --- a/packages/compiler/src/backend/llvm/lib-stream.ts +++ b/packages/compiler/src/backend/llvm/lib-stream.ts @@ -274,10 +274,9 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV .filter((a) => a.tag >= 0); if (present.length === 0) throw new InternalCompilerError(`llvm emitter bug: ${e.fn} union lacks its arms`); - const tagP = B.tmp(); - const tag = B.tmp(); - B.line(`${tagP} = getelementptr inbounds %ScrUnion, ptr ${args[1]!.name}, i64 0, i32 1`); - B.line(`${tag} = load i32, ptr ${tagP}`); + // A tagged box, or the arm pointer itself for a nullable union + // (`string | null`, `Buffer | null`). + const tag = host.unionTag(args[1]!.name, t.unionId); const slot = B.slot(); B.entryAllocas.push(`${slot} = alloca i1`); const lj = B.newLabel("scu.j"); @@ -290,10 +289,7 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV B.line(`store i1 ${r}, ptr ${slot}`); return; } - const pk = B.tmp(); - const pv = B.tmp(); - B.line(`${pk} = getelementptr inbounds %ScrUnion, ptr ${args[1]!.name}, i64 0, i32 5`); - B.line(`${pv} = load ptr, ptr ${pk} ; borrowed payload`); + const pv = host.unionPeek(args[1]!.name, t.unionId); // borrowed payload const r = B.tmp(); if (pushing) { host.declare(`declare zeroext i1 @${entry}(ptr, ptr)`); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6820-stream-push-nullable-chunks.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6820-stream-push-nullable-chunks.ts.json new file mode 100644 index 000000000..26bb4ae9e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6820-stream-push-nullable-chunks.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6820-stream-push-nullable-chunks.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6821-stream-write-nullable-chunks.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6821-stream-write-nullable-chunks.cjs.json new file mode 100644 index 000000000..e296beb66 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6821-stream-write-nullable-chunks.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6821-stream-write-nullable-chunks.cjs" + ], + "diags": [] +} diff --git a/tests/corpus/6820-stream-push-nullable-chunks.ts b/tests/corpus/6820-stream-push-nullable-chunks.ts new file mode 100644 index 000000000..5b79525b1 --- /dev/null +++ b/tests/corpus/6820-stream-push-nullable-chunks.ts @@ -0,0 +1,77 @@ +// Readable subclasses pushing `string | null` and `Buffer | null` chunks, a +// conditional `push(done ? null : chunk)`, and writes of `string | Buffer` +// chunks. +import { Readable, Writable } from "node:stream"; + +class Lines extends Readable { + private index = 0; + private readonly lines: string[]; + constructor(lines: string[]) { + super(); + this.lines = lines; + } + next(): string | null { + return this.index < this.lines.length ? this.lines[this.index++]! : null; + } + _read(): void { + const line: string | null = this.next(); + this.push(line); + } +} + +class Blocks extends Readable { + private sent = 0; + _read(): void { + const block: Buffer | null = this.sent < 3 ? Buffer.from([65 + this.sent, 10]) : null; + this.sent++; + this.push(block); + } +} + +class Countdown extends Readable { + private left: number; + constructor(left: number) { + super({ encoding: "utf8" }); + this.left = left; + } + _read(): void { + this.left--; + this.push(this.left < 0 ? null : `t-${this.left} `); + } +} + +function collect(label: string, stream: Readable): Promise { + return new Promise((resolve) => { + const parts: string[] = []; + stream.on("data", (chunk: Buffer | string) => + parts.push(typeof chunk === "string" ? chunk : chunk.toString()), + ); + stream.on("end", () => { + console.log(label, JSON.stringify(parts.join("")), parts.length); + resolve(); + }); + }); +} + +async function main(): Promise { + await collect("lines", new Lines(["a\n", "bb\n", "ccc\n"])); + await collect("empty lines", new Lines([])); + await collect("blocks", new Blocks()); + await collect("countdown", new Countdown(3)); + + const written: string[] = []; + const sink = new Writable({ + write(chunk: Buffer, _encoding, callback) { + written.push(chunk.toString()); + callback(); + }, + }); + const pieces: (string | Buffer)[] = ["x", Buffer.from("y"), "z"]; + for (const piece of pieces) { + const chunk: string | Buffer = piece; + console.log("write", Buffer.isBuffer(chunk) ? "buffer" : "string", sink.write(chunk)); + } + sink.end(() => console.log("written", JSON.stringify(written.join("")))); +} + +void main(); diff --git a/tests/corpus/6821-stream-write-nullable-chunks.cjs b/tests/corpus/6821-stream-write-nullable-chunks.cjs new file mode 100644 index 000000000..c7bf32bc0 --- /dev/null +++ b/tests/corpus/6821-stream-write-nullable-chunks.cjs @@ -0,0 +1,23 @@ +// Writes and pushes of a chunk that is a string or null: a null write throws +// ERR_STREAM_NULL_VALUES, and a null push ends the readable. +const { Writable, Readable } = require("node:stream"); +const written = []; +const sink = new Writable({ + write(chunk, _encoding, callback) { + written.push(chunk.toString()); + callback(); + }, +}); +for (let i = 0; i < 3; i++) { + const chunk = i === 1 ? null : `c${i}`; + try { + console.log("write", JSON.stringify(chunk), sink.write(chunk)); + } catch (error) { + console.log("write threw", error.name, error.code, error.message); + } +} +const r = new Readable({ read() {} }); +for (let i = 0; i < 3; i++) r.push(i === 2 ? null : `r${i}`); +r.on("data", (d) => console.log("data", String(d))); +r.on("end", () => console.log("end")); +sink.end(() => console.log("written", JSON.stringify(written.join("")))); diff --git a/tests/harness/corpus-costs/6820-stream-push-nullable-chunks.ts.json b/tests/harness/corpus-costs/6820-stream-push-nullable-chunks.ts.json new file mode 100644 index 000000000..dd2eae649 --- /dev/null +++ b/tests/harness/corpus-costs/6820-stream-push-nullable-chunks.ts.json @@ -0,0 +1 @@ +{ "plain": 3100, "san": 41100 } diff --git a/tests/harness/corpus-costs/6821-stream-write-nullable-chunks.cjs.json b/tests/harness/corpus-costs/6821-stream-write-nullable-chunks.cjs.json new file mode 100644 index 000000000..24b9052ff --- /dev/null +++ b/tests/harness/corpus-costs/6821-stream-write-nullable-chunks.cjs.json @@ -0,0 +1 @@ +{ "plain": 2400, "san": 40400 }