From b319186a684959a5b9d63906d9c5a11fe48fd1fe Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 29 Oct 2026 03:01:39 -0500 Subject: [PATCH] Count the arguments a replaced stdio write receives A JavaScript function assigned to process.stdout.write or process.stderr.write takes its parameters from the call, not from Node's overloads, but the lowering returned before giving the function its arguments slot. `arguments.length` then folded to the declared parameter count (3 for `function (chunk, encoding, callback)` called with one argument, 0 for a function without parameters), and reading `arguments[i]` was refused at run time. The replacement now receives the call's arguments, so `arguments` holds what the caller passed, as in Node. --- .../src/frontend/lowering/call-signatures.ts | 3 ++ ...stdio-write-replacement-arguments.cjs.json | 6 +++ .../stdio-write-replacement-arguments.cjs | 43 +++++++++++++++++++ ...stdio-write-replacement-arguments.cjs.json | 1 + 4 files changed, 53 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/stdio-write-replacement-arguments.cjs.json create mode 100644 tests/corpus/stdio-write-replacement-arguments.cjs create mode 100644 tests/harness/corpus-costs/stdio-write-replacement-arguments.cjs.json diff --git a/packages/compiler/src/frontend/lowering/call-signatures.ts b/packages/compiler/src/frontend/lowering/call-signatures.ts index 45329d077..783082288 100644 --- a/packages/compiler/src/frontend/lowering/call-signatures.ts +++ b/packages/compiler/src/frontend/lowering/call-signatures.ts @@ -1062,6 +1062,9 @@ export function lambdaSignature( type: DYN, mode: "required", })); + // `arguments` holds the call's own tuple: a console line passes one + // or two arguments whatever the replacement declares. + appendArgumentsParameter(lowerer, node, shapes, "arguments"); return { shapes, funcType: funcTypeFromParamShapes(shapes, DYN) }; } } diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/stdio-write-replacement-arguments.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/stdio-write-replacement-arguments.cjs.json new file mode 100644 index 000000000..23315d78e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/stdio-write-replacement-arguments.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/stdio-write-replacement-arguments.cjs" + ], + "diags": [] +} diff --git a/tests/corpus/stdio-write-replacement-arguments.cjs b/tests/corpus/stdio-write-replacement-arguments.cjs new file mode 100644 index 000000000..c33a78210 --- /dev/null +++ b/tests/corpus/stdio-write-replacement-arguments.cjs @@ -0,0 +1,43 @@ +// A replaced process.stdout.write or process.stderr.write sees the call's own +// arguments: arguments.length is the number the caller passed, whatever the +// replacement declares. +const seen = []; +const originalOut = process.stdout.write; +process.stdout.write = function (chunk, encodingOrCallback, callback) { + seen.push(`stdout declared3 ${arguments.length} ${typeof callback}`); + return true; +}; +process.stdout.write("one"); +process.stdout.write("two", "utf8"); +process.stdout.write("three", "utf8", () => {}); +process.stdout.write = function () { + seen.push(`stdout declared0 ${arguments.length} ${arguments[0]}`); + return true; +}; +process.stdout.write("zero"); +process.stdout.write = function (chunk) { + seen.push(`stdout declared1 ${arguments.length} ${arguments[1]}`); + return true; +}; +process.stdout.write("a", "latin1"); +process.stdout.write = originalOut; +const originalErr = process.stderr.write; +process.stderr.write = function (chunk, encodingOrCallback, callback) { + seen.push(`stderr declared3 ${arguments.length} ${typeof callback}`); + return true; +}; +process.stderr.write("one"); +process.stderr.write("two", "utf8"); +process.stderr.write("three", "utf8", () => {}); +process.stderr.write = function () { + seen.push(`stderr declared0 ${arguments.length} ${arguments[0]}`); + return true; +}; +process.stderr.write("zero"); +process.stderr.write = function (chunk) { + seen.push(`stderr declared1 ${arguments.length} ${arguments[1]}`); + return true; +}; +process.stderr.write("a", "latin1"); +process.stderr.write = originalErr; +console.log(seen.join("\n")); diff --git a/tests/harness/corpus-costs/stdio-write-replacement-arguments.cjs.json b/tests/harness/corpus-costs/stdio-write-replacement-arguments.cjs.json new file mode 100644 index 000000000..2b44f308c --- /dev/null +++ b/tests/harness/corpus-costs/stdio-write-replacement-arguments.cjs.json @@ -0,0 +1 @@ +{ "plain": 3757, "san": 5000 }