mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-07 08:33:50 +08:00
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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
7be1a990c7
commit
e386da4aa4
@@ -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 } : {}),
|
||||
|
||||
@@ -379,6 +379,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"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<string>([
|
||||
"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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 } : {}),
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -647,6 +647,7 @@ const PROCESS_EVENT_LIB_FNS: ReadonlySet<IrLibFn> = 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,
|
||||
|
||||
@@ -33,6 +33,7 @@ const features = {
|
||||
warningReplace: false,
|
||||
stdioExtended: false,
|
||||
streamWriteErrors: false,
|
||||
stdioWriteSlot: false,
|
||||
nodeIsland: false,
|
||||
brotli: false,
|
||||
moduleNamespace: false,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user