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