From e386da4aa412a74dbb19b870417a5753fe524c0f Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Wed, 28 Oct 2026 21:50:25 -0700 Subject: [PATCH] Gate the typed stdio write slot out of the default events object The typed write slot, the drain probes and the run-time-typed chunk write all live in scr_events.c, a unit every program links. Unconditionally they changed the default object, so executables of programs that never touch the write slot changed byte for byte (three corpus programs differed runtime-only). Typed programs must stay byte-identical. Keep the default object exactly as it was and put the new code behind SCR_STDIO_WRITE_SLOT, the way SCR_STREAM_WRITE_ERRORS gates the stream unit: the shared write body, the run-time-typed chunk entry point and Node's anonymous write function exist only in that build, and scr_stdio_method keeps its own body everywhere else. The lowering sites that reach the slot (the write method as a value, its replacement, a patched program's typed writes) now emit a distinct marker lib call, process.stdioSlot, which is the same scr_process_stdio at run time. The feature scan turns it, together with process.stdioWriteDyn, into the stdioWriteSlot runtime feature, which selects the matching runtime pack variant and defines SCR_STDIO_WRITE_SLOT for the executable. The pack matrix enumerates the combinations with stdioExtended and nodeIsland explicitly, so each existing variant keeps its defines and its object. Both new lib fns join USES_TIMERS_LIB_FNS: the write callbacks they queue run from a nextTick, and a program whose only stdio use is the write slot would otherwise drop the callback. Co-Authored-By: Claude Opus 5.5 --- .../src/backend/executable-features.ts | 1 + .../compiler/src/backend/llvm/lib-shared.ts | 6 +++ .../src/backend/native-link-info-core.ts | 3 ++ .../compiler/src/backend/native/contracts.ts | 3 ++ .../compiler/src/backend/native/executable.ts | 2 + .../compiler/src/backend/runtime-pack-core.ts | 4 ++ .../src/executable/executable-cache.ts | 2 + .../src/frontend/lowering/builtins/process.ts | 4 +- .../src/frontend/lowering/lower-stmts.ts | 2 +- packages/compiler/src/index.ts | 1 + .../compiler/src/ir/builtin-signatures.ts | 5 ++ packages/compiler/src/ir/runtime-features.ts | 10 ++++ .../compiler/src/native/sanitizer.test.ts | 1 + .../runtime-darwin-arm64/scripts/build.mjs | 1 + .../runtime-pack-matrix.mjs | 23 +++++++++ .../runtime-pack-common/scripts/build.mjs | 1 + .../test/runtime-pack-matrix.test.ts | 32 ++++++++++++ packages/runtime/src/scr_events.c | 50 ++++++++++++++++++- 18 files changed, 147 insertions(+), 4 deletions(-) diff --git a/packages/compiler/src/backend/executable-features.ts b/packages/compiler/src/backend/executable-features.ts index f333aff7e..bbf3cdfff 100644 --- a/packages/compiler/src/backend/executable-features.ts +++ b/packages/compiler/src/backend/executable-features.ts @@ -21,6 +21,7 @@ export function executableLinkFeatures(mod: IrModule, largePages = false): Nativ ...(features.warningReplace ? { warningReplace: true } : {}), ...(features.stdioExtended ? { stdioExtended: true } : {}), ...(features.streamWriteErrors ? { streamWriteErrors: true } : {}), + ...(features.stdioWriteSlot ? { stdioWriteSlot: true } : {}), ...(features.nodeIsland ? { nodeIsland: true } : {}), ...(features.moduleNamespace ? { moduleNamespace: true } : {}), ...(features.moduleNamespaceDyn ? { moduleNamespaceDyn: true } : {}), diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index e7f918fdf..694f8e8f0 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -379,6 +379,7 @@ export const LIB_FN_SYMS: Record = { "process.envPairs": "scr_env_pairs", "process.stdoutWrite": "scr_process_stdout_write", "process.stdio": "scr_process_stdio", + "process.stdioSlot": "scr_process_stdio", "process.stdioWriteDyn": "scr_process_stdio_write_dyn", "process.stderrWrite": "scr_process_stderr_write", "process.isTTY": "scr_process_is_tty", @@ -1869,6 +1870,11 @@ export const USES_TIMERS_LIB_FNS = new Set([ "worker.receiveMessageOnPort", "worker.broadcastChannel", "process.stdio", + // The typed write slot and the run-time-typed chunk write keep the loop + // live for the write callbacks they queue: both reach the same stream + // object as process.stdio and call back from a nextTick. + "process.stdioSlot", + "process.stdioWriteDyn", "fetch.start", "fetch.input", "fetch.function", diff --git a/packages/compiler/src/backend/native-link-info-core.ts b/packages/compiler/src/backend/native-link-info-core.ts index 54aa87b4d..43d4efa22 100644 --- a/packages/compiler/src/backend/native-link-info-core.ts +++ b/packages/compiler/src/backend/native-link-info-core.ts @@ -37,6 +37,9 @@ export interface NativeLinkFeatures { /** A write completion callback takes the error argument: links the * SCR_STREAM_WRITE_ERRORS build of the stream unit. */ streamWriteErrors?: boolean; + /** The program reaches the process output streams' write slot: links the + * SCR_STDIO_WRITE_SLOT build of the events unit. */ + stdioWriteSlot?: boolean; /** Calls into Node.js (@scriptc/node): scr_node_island.c and the * SCR_NODE_ISLANDS variant of the event loop unit. */ nodeIsland?: boolean; diff --git a/packages/compiler/src/backend/native/contracts.ts b/packages/compiler/src/backend/native/contracts.ts index a58d2310f..7a332e11f 100644 --- a/packages/compiler/src/backend/native/contracts.ts +++ b/packages/compiler/src/backend/native/contracts.ts @@ -164,6 +164,9 @@ export interface CcOptions { /** SCR_STREAM_WRITE_ERRORS: the stream unit publishes the error each * write completion callback receives (error-taking write callbacks). */ streamWriteErrors?: boolean; + /** SCR_STDIO_WRITE_SLOT: the events unit carries the typed write slot + * (the write method as a value, its replacement, run-time-typed chunks). */ + stdioWriteSlot?: boolean; /** Calls into Node.js (@scriptc/node): compiles scr_node_island.c and * the event loop with SCR_NODE_ISLANDS (its helper slot). */ nodeIsland?: boolean; diff --git a/packages/compiler/src/backend/native/executable.ts b/packages/compiler/src/backend/native/executable.ts index 4d099a6d9..3ec34ca7c 100644 --- a/packages/compiler/src/backend/native/executable.ts +++ b/packages/compiler/src/backend/native/executable.ts @@ -412,6 +412,8 @@ const RUNTIME_DEFINES: readonly { // scr_stream.c's write-callback error publication, which the compiler's // error-first write adapter reads. { define: "SCR_STREAM_WRITE_ERRORS", when: (opts) => opts.streamWriteErrors }, + // scr_events.c's typed process output write slot. + { define: "SCR_STDIO_WRITE_SLOT", when: (opts) => opts.stdioWriteSlot }, // scr_async.c's native promise reactions, which the web streams unit uses, // scr_json.c's and scr_dyn_builtin_class.c's instanceof over its classes, // and scr_fetch.c's bodies of its streams. diff --git a/packages/compiler/src/backend/runtime-pack-core.ts b/packages/compiler/src/backend/runtime-pack-core.ts index cd22be057..a26e5e912 100644 --- a/packages/compiler/src/backend/runtime-pack-core.ts +++ b/packages/compiler/src/backend/runtime-pack-core.ts @@ -104,6 +104,9 @@ export interface RuntimeFeatureSet extends NativeLinkFeatures { /** The program's write completion callbacks take the error argument: * the SCR_STREAM_WRITE_ERRORS build of the stream unit. */ streamWriteErrors: boolean; + /** The program reaches the process output streams' write slot: the + * SCR_STDIO_WRITE_SLOT build of the events unit. */ + stdioWriteSlot: boolean; nodeIsland: boolean; brotli: boolean; moduleNamespace: boolean; @@ -316,6 +319,7 @@ export function effectiveRuntimeFeatures( warningReplace: features.warningReplace === true, stdioExtended: features.stdioExtended === true, streamWriteErrors: features.streamWriteErrors === true, + stdioWriteSlot: features.stdioWriteSlot === true, nodeIsland: features.nodeIsland === true, brotli: features.brotli === true, moduleNamespace: features.moduleNamespace === true, diff --git a/packages/compiler/src/executable/executable-cache.ts b/packages/compiler/src/executable/executable-cache.ts index 36965eb89..702adc403 100644 --- a/packages/compiler/src/executable/executable-cache.ts +++ b/packages/compiler/src/executable/executable-cache.ts @@ -67,6 +67,8 @@ export interface EarlyExecutableNativeFeatures { stdioExtended?: boolean; /** Error-taking write completion callbacks: SCR_STREAM_WRITE_ERRORS. */ streamWriteErrors?: boolean; + /** The process output write slot: SCR_STDIO_WRITE_SLOT. */ + stdioWriteSlot?: boolean; nodeIsland?: boolean; moduleNamespace?: boolean; moduleNamespaceDyn?: boolean; diff --git a/packages/compiler/src/frontend/lowering/builtins/process.ts b/packages/compiler/src/frontend/lowering/builtins/process.ts index e5c9bd5af..3211994b9 100644 --- a/packages/compiler/src/frontend/lowering/builtins/process.ts +++ b/packages/compiler/src/frontend/lowering/builtins/process.ts @@ -416,7 +416,7 @@ export function lowerProcessStreamProperty( kind: "dynKeyGet", value: { kind: "libCall", - fn: "process.stdio", + fn: "process.stdioSlot", args: [numLit(direct === "stdout" ? 1 : 2, loc)], type: DYN, loc, @@ -1242,7 +1242,7 @@ export function lowerProcessMethodCall( if (programPatchesStdio(lowerer) && !args.some(ts.isSpreadElement)) { const handle: IrExpr = { kind: "libCall", - fn: "process.stdio", + fn: "process.stdioSlot", args: [numLit(stream === "stdout" ? 1 : 2, loc)], type: DYN, loc, diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index cfea92522..c452beb94 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -9214,7 +9214,7 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm args: [ { kind: "libCall", - fn: "process.stdio", + fn: "process.stdioSlot", args: [numLit(stream === "stdout" ? 1 : 2, loc)], type: DYN, loc, diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index ecd6734a8..759871465 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -701,6 +701,7 @@ async function compileExecutableNative( ...(features.warningReplace ? { warningReplace: true } : {}), ...(features.stdioExtended ? { stdioExtended: true } : {}), ...(features.streamWriteErrors ? { streamWriteErrors: true } : {}), + ...(features.stdioWriteSlot ? { stdioWriteSlot: true } : {}), ...(features.nodeIsland ? { nodeIsland: true } : {}), ...(features.moduleNamespace ? { moduleNamespace: true } : {}), ...(features.moduleNamespaceDyn ? { moduleNamespaceDyn: true } : {}), diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index d8d3ce999..60fa80788 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -3668,6 +3668,11 @@ function buildLibFnSignatures() { * never throws. */ "process.stdoutWrite": { argTypes: [STRING], result: BOOL }, "process.stdio": { argTypes: [F64], result: DYN }, + /** The same stream object, reached by the typed write-slot lowering + * (the write method as a value, its replacement, a patched program's + * typed writes): identical at run time, and the link switch for the + * SCR_STDIO_WRITE_SLOT build of the events unit. */ + "process.stdioSlot": { argTypes: [F64], result: DYN }, /** process.stdout/stderr.write(chunk) with a run-time chunk: Node's * Writable validation, then a synchronous write. */ "process.stdioWriteDyn": { argTypes: [F64, DYN], result: BOOL }, diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index f922204d0..43780b711 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -647,6 +647,7 @@ const PROCESS_EVENT_LIB_FNS: ReadonlySet = new Set([ "process.onExit", "process.onSignal", "process.stdio", + "process.stdioSlot", "process.stdioWriteDyn", "stdin.nextChunk", "stdin.onData", @@ -1190,6 +1191,14 @@ const RUNTIME_FEATURE_RULES = { * through writable.writeEcb: the SCR_STREAM_WRITE_ERRORS stream unit * publishes the error the adapter delivers. */ streamWriteErrors: rule({ fns: ["writable.writeEcb"] }), + /** The program reaches the process output streams' write slot — the + * write method as a value (process.stdioSlot), its replacement, a patched + * program's typed writes, or a run-time-typed chunk + * (process.stdioWriteDyn): the SCR_STDIO_WRITE_SLOT build of the events + * unit carries the shared write body, the dynamic-chunk entry point and + * Node's anonymous write function. Every other program links the default + * object, byte for byte. */ + stdioWriteSlot: rule({ fns: ["process.stdioSlot", "process.stdioWriteDyn"] }), stream: rule({ initial: (mod) => (mod.classes ?? []).some((c) => RUNTIME_STREAM_CLASSES.has(c.name)), fns: [...STREAM_LIB_FNS, "dyn.builtinStreamClass"], @@ -1314,6 +1323,7 @@ function noRuntimeFeatures(): RuntimeFeatures { searchParams: false, urlIdna: false, stdioExtended: false, + stdioWriteSlot: false, streamWriteErrors: false, moduleNamespace: false, moduleNamespaceDyn: false, diff --git a/packages/compiler/src/native/sanitizer.test.ts b/packages/compiler/src/native/sanitizer.test.ts index 0e7bccafd..faea14004 100644 --- a/packages/compiler/src/native/sanitizer.test.ts +++ b/packages/compiler/src/native/sanitizer.test.ts @@ -33,6 +33,7 @@ const features = { warningReplace: false, stdioExtended: false, streamWriteErrors: false, + stdioWriteSlot: false, nodeIsland: false, brotli: false, moduleNamespace: false, diff --git a/packages/runtime-darwin-arm64/scripts/build.mjs b/packages/runtime-darwin-arm64/scripts/build.mjs index 24bde2db5..3e7c8930a 100644 --- a/packages/runtime-darwin-arm64/scripts/build.mjs +++ b/packages/runtime-darwin-arm64/scripts/build.mjs @@ -377,6 +377,7 @@ async function build() { "SCR_WARNING_REPLACE", "SCR_STDIO_EXTENDED", "SCR_STREAM_WRITE_ERRORS", + "SCR_STDIO_WRITE_SLOT", "SCR_NATIVE_REACTIONS", "SCR_HANDLE_CLASSES", "SCR_YIN", diff --git a/packages/runtime-pack-common/runtime-pack-matrix.mjs b/packages/runtime-pack-common/runtime-pack-matrix.mjs index 5e3b722d2..630a261b8 100644 --- a/packages/runtime-pack-common/runtime-pack-matrix.mjs +++ b/packages/runtime-pack-common/runtime-pack-matrix.mjs @@ -324,6 +324,29 @@ export function variantsFor(source) { when: { stdioExtended: true, nodeIsland: true }, defines: ["SCR_STDIO_EXTENDED", "SCR_NODE_ISLANDS"], }); + // The typed process output write slot (the write method as a value, its + // replacement, run-time-typed chunks): the shared write body, the + // dynamic-chunk entry point and Node's anonymous write function. Every + // other program keeps the default object, byte for byte. + for (const [id, when, defines] of [ + ["stdio-write-slot", { stdioWriteSlot: true }, ["SCR_STDIO_WRITE_SLOT"]], + [ + "stdio-extended-write-slot", + { stdioExtended: true, stdioWriteSlot: true }, + ["SCR_STDIO_EXTENDED", "SCR_STDIO_WRITE_SLOT"], + ], + [ + "node-island-write-slot", + { nodeIsland: true, stdioWriteSlot: true }, + ["SCR_NODE_ISLANDS", "SCR_STDIO_WRITE_SLOT"], + ], + [ + "stdio-extended-node-island-write-slot", + { stdioExtended: true, nodeIsland: true, stdioWriteSlot: true }, + ["SCR_STDIO_EXTENDED", "SCR_NODE_ISLANDS", "SCR_STDIO_WRITE_SLOT"], + ], + ]) + variants.push({ id, when, defines }); } // Programs with Node islands link the event loop with the Node island slot; // every other program keeps the default object, byte for byte. diff --git a/packages/runtime-pack-common/scripts/build.mjs b/packages/runtime-pack-common/scripts/build.mjs index e8af727ea..9d58f77fd 100644 --- a/packages/runtime-pack-common/scripts/build.mjs +++ b/packages/runtime-pack-common/scripts/build.mjs @@ -546,6 +546,7 @@ async function build() { "SCR_WARNING_REPLACE", "SCR_STDIO_EXTENDED", "SCR_STREAM_WRITE_ERRORS", + "SCR_STDIO_WRITE_SLOT", "SCR_NATIVE_REACTIONS", "SCR_HANDLE_CLASSES", "SCR_YIN", diff --git a/packages/runtime-pack-common/test/runtime-pack-matrix.test.ts b/packages/runtime-pack-common/test/runtime-pack-matrix.test.ts index 9157d7bd0..a0107b78b 100644 --- a/packages/runtime-pack-common/test/runtime-pack-matrix.test.ts +++ b/packages/runtime-pack-common/test/runtime-pack-matrix.test.ts @@ -24,6 +24,38 @@ function selected( return matches[0]; } +test("the typed stdio write slot selects the events build that carries it", () => { + const plain = { stdioExtended: false, nodeIsland: false, workers: false }; + // Alone, the write slot selects its own build. + expect(selected("scr_events.c", { ...plain, stdioWriteSlot: true }).defines).toEqual([ + "SCR_STDIO_WRITE_SLOT", + ]); + // Beside the other events features it keeps each one's defines. + expect( + selected("scr_events.c", { ...plain, stdioExtended: true, stdioWriteSlot: true }).defines, + ).toEqual(["SCR_STDIO_EXTENDED", "SCR_STDIO_WRITE_SLOT"]); + expect( + selected("scr_events.c", { ...plain, nodeIsland: true, stdioWriteSlot: true }).defines, + ).toEqual(["SCR_NODE_ISLANDS", "SCR_STDIO_WRITE_SLOT"]); + expect( + selected("scr_events.c", { + ...plain, + stdioExtended: true, + nodeIsland: true, + stdioWriteSlot: true, + }).defines, + ).toEqual(["SCR_STDIO_EXTENDED", "SCR_NODE_ISLANDS", "SCR_STDIO_WRITE_SLOT"]); + // A program that never reaches the write slot keeps the old object. + expect( + selected("scr_events.c", { + ...plain, + stdioExtended: true, + nodeIsland: true, + stdioWriteSlot: false, + }).defines, + ).toEqual(["SCR_STDIO_EXTENDED", "SCR_NODE_ISLANDS"]); +}); + test("web streams and Node islands select an async build that defines both", () => { const variant = selected("scr_async.c", { webStreams: true, nodeIsland: true, workers: false }); expect(variant.defines).toEqual( diff --git a/packages/runtime/src/scr_events.c b/packages/runtime/src/scr_events.c index 8794c270c..e73515f66 100644 --- a/packages/runtime/src/scr_events.c +++ b/packages/runtime/src/scr_events.c @@ -1312,9 +1312,13 @@ static void scr_stdio_resize_listener(ScrStdio *stream, ScrDyn *listener, bool r #endif +#ifdef SCR_STDIO_WRITE_SLOT /* stream.write(chunk[, encoding][, callback]) on a process output stream: * Node's Writable.write validation, then a synchronous descriptor write. - * Returns +1 (true/false) or NULL with an exception pending. */ + * Returns +1 (true/false) or NULL with an exception pending. The typed + * write-slot build factors it out of scr_stdio_method so the dynamic-chunk + * entry point below shares it; every other build keeps the method's own + * body, byte for byte. */ static ScrDyn *scr_stdio_write_call(ScrStdio *stream, ScrDyn *const *args, size_t argc) { ScrDyn *arg = argc ? args[0] : scr_dyn_undefined(); ScrDyn *result = NULL; @@ -1356,6 +1360,7 @@ static ScrDyn *scr_stdio_write_call(ScrStdio *stream, ScrDyn *const *args, size_ if (callback->kind == SCR_DYN_FUNC) scr_next_tick(scr_stdio_callback(callback, (void *)scr_stdio_write_done)); return scr_dyn_new_bool(true); } +#endif static ScrDyn *scr_stdio_method(ScrClosure *cb, ScrDyn *const *args, size_t argc) { int method = (int)scr_box_get_f64(cb->caps[0]); @@ -1374,7 +1379,42 @@ static ScrDyn *scr_stdio_method(ScrClosure *cb, ScrDyn *const *args, size_t argc scr_dyn_arr_push(result, columns); scr_dyn_arr_push(result, rows); } else if (method == STDIO_WRITE && stream->fd != 0) { +#ifdef SCR_STDIO_WRITE_SLOT result = scr_stdio_write_call(stream, args, argc); +#else + ScrDyn *encoding = argc > 1 ? args[1] : scr_dyn_undefined(); + ScrDyn *callback = argc > 2 ? args[2] : scr_dyn_undefined(); + if (encoding->kind == SCR_DYN_FUNC) { callback = encoding; encoding = scr_dyn_undefined(); } + if (callback->kind != SCR_DYN_UNDEF && callback->kind != SCR_DYN_NULL && callback->kind != SCR_DYN_FUNC) { + scr_dyn_arg_type_fail("cb", "of type function", callback); + goto done; + } + ScrBytes *bytes = NULL; + if (arg->kind == SCR_DYN_STR) { + if (encoding->kind != SCR_DYN_UNDEF && encoding->kind != SCR_DYN_NULL && encoding->kind != SCR_DYN_STR) { + result = scr_stdio_refusal("write encoding"); + goto done; + } + ScrStr *enc = encoding->kind == SCR_DYN_STR ? scr_str_retain(encoding->v.str) : scr_str_new("utf8", 4); + bytes = scr_bytes_from_str(arg->v.str, enc); + scr_str_release(enc); + } else if (arg->kind == SCR_DYN_BYTES && arg->v.bytes->elem == SCR_BYTES_U8) { + bytes = arg->v.bytes->shared ? scr_bytes_copy(arg->v.bytes) : scr_bytes_retain(arg->v.bytes); + } else { + scr_dyn_arg_type_fail("chunk", "of type string or an instance of Buffer, TypedArray, or DataView", arg); + goto done; + } + if (!bytes) goto done; + int error = scr_stdio_write_raw(stream->fd, bytes->data, bytes->len); + scr_bytes_release(bytes); + if (error) { + scr_stdio_queue_write_error(stream, callback, error); + result = scr_dyn_new_bool(false); + goto done; + } + if (callback->kind == SCR_DYN_FUNC) scr_next_tick(scr_stdio_callback(callback, (void *)scr_stdio_write_done)); + result = scr_dyn_new_bool(true); +#endif } else if (method == STDIO_ON || method == STDIO_ONCE || method == STDIO_OFF) { ScrDyn *listener = argc > 1 ? args[1] : scr_dyn_undefined(); if (listener->kind != SCR_DYN_FUNC) { @@ -1595,8 +1635,14 @@ ScrDyn *scr_process_stdio(double fd) { ScrClosure *cb = scr_closure_new(NULL, 1); cb->caps[0] = scr_box_new(SCR_BOX_F64); scr_box_set_f64(cb->caps[0], i); +#ifdef SCR_STDIO_WRITE_SLOT + // Node's process.stdout.write is an anonymous (bound) function: + // .name is "" there, while the other stream methods carry their names. scr_stdio_methods[i] = scr_dyn_new_func( cb, scr_stdio_method, i == STDIO_WRITE ? 3 : 1, "", i == STDIO_WRITE ? "" : scr_stdio_names[i]); +#else + scr_stdio_methods[i] = scr_dyn_new_func(cb, scr_stdio_method, i == STDIO_WRITE ? 3 : 1, "", scr_stdio_names[i]); +#endif } scr_stdio_write_hook = scr_stdio_intercept; } @@ -1612,6 +1658,7 @@ double scr_dyn_process_stdio_fd(const ScrDyn *value, const ScrDynPath *path) { return stream ? stream->fd : 0; } +#ifdef SCR_STDIO_WRITE_SLOT /* process.stdout/stderr.write(chunk) with a run-time chunk: the stream * method's own validation and write, without creating the stream value * or installing the write hook. False with an exception pending when the @@ -1624,6 +1671,7 @@ bool scr_process_stdio_write_dyn(double fd, ScrDyn *chunk) { scr_dyn_release(result); return accepted; } +#endif /* ── the process 'exit' event ───────────────────────────────────────── * Listeners run SYNCHRONOUSLY when the process exits: at the end of a