mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-02 08:35:07 +08:00
feat: support process write callback and encoding args (#114)
* feat: support process write callback and encoding args - Lower static stdout and stderr writes with encoding and callback overloads. - Schedule completion callbacks through the next-tick queue in C and LLVM builds. - Cover encoding aliases, ordering, and async-free refusal behavior. * fix: handle omitted process write arguments
This commit is contained in:
+12
-6
@@ -205,12 +205,18 @@ declare var process: {
|
||||
/* The raw byte writes — no newline, no formatting. stdout shares
|
||||
* console.log's stream, each call is submitted promptly, and ordering is
|
||||
* preserved. The boolean is Node's backpressure signal — these synchronous
|
||||
* writes always return true. The Uint8Array overload
|
||||
* and isTTY exist so real CLIs TYPECHECK (they are @types/node surface);
|
||||
* only the one-string form has a lowering — everything else fences at
|
||||
* its use site. */
|
||||
stdout: { write(data: string): boolean; write(data: Uint8Array): boolean; readonly isTTY: boolean };
|
||||
stderr: { write(data: string): boolean; write(data: Uint8Array): boolean; readonly isTTY: boolean };
|
||||
* writes always return true. Static BufferEncoding arguments and the
|
||||
* completion callback overloads match Node's WritableStream surface. */
|
||||
stdout: {
|
||||
write(data: string | Uint8Array, callback?: (error?: Error | null) => void): boolean;
|
||||
write(data: string | Uint8Array, encoding: BufferEncoding, callback?: (error?: Error | null) => void): boolean;
|
||||
readonly isTTY: boolean;
|
||||
};
|
||||
stderr: {
|
||||
write(data: string | Uint8Array, callback?: (error?: Error | null) => void): boolean;
|
||||
write(data: string | Uint8Array, encoding: BufferEncoding, callback?: (error?: Error | null) => void): boolean;
|
||||
readonly isTTY: boolean;
|
||||
};
|
||||
/* The stdin stream, the piped-input slice: the TTY probe, the
|
||||
* data/end/error events (on and once — a 'data' listener keeps the
|
||||
* event loop alive until EOF, like Node's flowing stdin), destroy()
|
||||
|
||||
@@ -5417,9 +5417,27 @@ export function emitExpr(E: CEmitter, e: IrExpr): Temp {
|
||||
case "zlib.inflateSync":
|
||||
return finish(`scr_zlib_inflate(${arg(0)})`);
|
||||
case "process.stdoutWriteBytes":
|
||||
return finish(`scr_process_stdout_write_bytes(${arg(0)})`);
|
||||
return finish(`scr_process_stdout_write_bytes(${arg(0)}, ${arg(1)})`);
|
||||
case "process.stderrWriteBytes":
|
||||
return finish(`scr_process_stderr_write_bytes(${arg(0)})`);
|
||||
return finish(`scr_process_stderr_write_bytes(${arg(0)}, ${arg(1)})`);
|
||||
case "process.stdoutWriteBytesCb":
|
||||
case "process.stderrWriteBytesCb": {
|
||||
// Submit the bytes only after every call argument evaluated,
|
||||
// then move the completion callback onto the next-tick queue.
|
||||
// The shared error-first adapter materializes success `null`.
|
||||
E.usesTimers = true;
|
||||
const cbT = e.args[2]!.type;
|
||||
if (cbT.kind !== "func") throw new Error("emitter bug: process write callback not a func");
|
||||
const cb = args[2]!;
|
||||
E.moveTemp(cb);
|
||||
const adapter = E.fsRenameThunkFor(cbT);
|
||||
const write = e.fn === "process.stdoutWriteBytesCb"
|
||||
? "scr_process_stdout_write_bytes"
|
||||
: "scr_process_stderr_write_bytes";
|
||||
const out = E.newTemp(e.type, `${write}(${arg(0)}, ${arg(1)})`);
|
||||
E.line(`scr_process_write_callback(${cb.name}, &${adapter});${E.srcComment(e.loc)}`);
|
||||
return out;
|
||||
}
|
||||
case "tp.setTimeout":
|
||||
return finish(`scr_tp_set_timeout(${arg(0)})`);
|
||||
case "tp.setImmediate":
|
||||
|
||||
@@ -11049,6 +11049,26 @@ class LlEmitter {
|
||||
const B = this.B;
|
||||
// Loop liveness first (one table for generic and special shapes).
|
||||
if (USES_TIMERS_LIB_FNS.has(e.fn)) this.usesTimers = true;
|
||||
if (e.fn === "process.stdoutWriteBytesCb" || e.fn === "process.stderrWriteBytesCb") {
|
||||
// All arguments evaluate before bytes are submitted. The callback
|
||||
// then moves into the next-tick entry and its error-first adapter
|
||||
// constructs Node's success `null` argument when the tick fires.
|
||||
this.usesTimers = true;
|
||||
const args = e.args.map((a) => this.emitExpr(a));
|
||||
const cbT = e.args[2]!.type;
|
||||
if (cbT.kind !== "func") throw new Error("llvm emitter bug: process write callback not a func");
|
||||
this.moveTemp(args[2]!);
|
||||
const adapter = this.fsRenameThunkFor(cbT);
|
||||
const write = e.fn === "process.stdoutWriteBytesCb"
|
||||
? "scr_process_stdout_write_bytes"
|
||||
: "scr_process_stderr_write_bytes";
|
||||
this.declare(`declare zeroext i1 @${write}(ptr, ptr)`);
|
||||
this.declare(`declare void @scr_process_write_callback(ptr, ptr)`);
|
||||
const out = B.tmp();
|
||||
B.line(`${out} = call i1 @${write}(ptr ${args[0]!.name}, ptr ${args[1]!.name})`);
|
||||
B.line(`call void @scr_process_write_callback(ptr ${args[2]!.name}, ptr @${adapter})`);
|
||||
return { name: out, type: e.type };
|
||||
}
|
||||
if (e.fn === "fs.renameCb") {
|
||||
// The callback MOVES into the next-turn operation. Its emitted
|
||||
// adapter materializes the callback's Error | null union (or the
|
||||
|
||||
@@ -5910,12 +5910,13 @@ function optionMember(p: ts.ObjectLiteralElementLike): { name: string; value: ts
|
||||
export function lowerProcessMethodCall(L: Lowerer, call: ts.CallExpression,
|
||||
access: ts.PropertyAccessExpression,): IrExpr | null {
|
||||
if (call.questionDotToken) return null;
|
||||
// process.stdout.write(s) / process.stderr.write(s): the raw byte
|
||||
// write — no newline, no formatting. stdout shares console.log's
|
||||
// process.stdout.write(s[, encoding][, callback]) and stderr's twin:
|
||||
// raw bytes, no newline or formatting. stdout shares console.log's
|
||||
// promptly-submitted stream, preserving source order. Node's boolean
|
||||
// is a backpressure signal; this synchronous write is constantly true.
|
||||
// @types/node's wider forms (Buffer data, encoding, callback) typecheck
|
||||
// and fence here.
|
||||
// String encodings are compile-time-known BufferEncoding spellings;
|
||||
// byte chunks evaluate but ignore the encoding like Node. Completion
|
||||
// callbacks ride the next-tick queue and receive the success `null`.
|
||||
// process.stdin.destroy(): a deliberate no-op — no stream machinery
|
||||
// exists to tear down, and no other stdin surface observes the
|
||||
// destroyed state (SEMANTICS.md documents it).
|
||||
@@ -6037,14 +6038,74 @@ function optionMember(p: ts.ObjectLiteralElementLike): { name: string; value: ts
|
||||
const stream = L.stdlibGlobalMember(access.expression, "process");
|
||||
if (stream === "stdout" || stream === "stderr") {
|
||||
const loc = locOf(call);
|
||||
if (call.arguments.length !== 1) {
|
||||
const args = call.arguments;
|
||||
if (args.length < 1 || args.length > 3 || args.some(ts.isSpreadElement)) {
|
||||
L.noLowering(
|
||||
`process.${stream}.write with ${call.arguments.length} arguments`,
|
||||
`process.${stream}.write with ${args.length} arguments`,
|
||||
call,
|
||||
"the supported form is write(data) with one string — encodings and callbacks have no lowering",
|
||||
"the supported forms are write(data[, encoding][, callback]) with a static BufferEncoding and completion callback",
|
||||
);
|
||||
}
|
||||
let data = L.lowerExpr(call.arguments[0]!);
|
||||
const secondNode = args[1];
|
||||
const thirdNode = args[2];
|
||||
const secondUndefined = secondNode
|
||||
? lowerStaticallyUndefinedBuiltinArg(L, secondNode)
|
||||
: null;
|
||||
const thirdUndefined = thirdNode
|
||||
? lowerStaticallyUndefinedBuiltinArg(L, thirdNode)
|
||||
: null;
|
||||
const secondT = secondNode && !secondUndefined ? L.mapTypeOf(L.typeOf(secondNode)) : undefined;
|
||||
const callbackNode = args.length === 3 && !thirdUndefined
|
||||
? thirdNode!
|
||||
: args.length === 2 && secondT?.kind === "func"
|
||||
? secondNode!
|
||||
: undefined;
|
||||
const encodingNode =
|
||||
(args.length === 3 || (args.length === 2 && callbackNode === undefined)) && !secondUndefined
|
||||
? secondNode!
|
||||
: undefined;
|
||||
|
||||
// Node's BufferEncoding aliases normalize before bytes are made.
|
||||
// Preserve an effectful literal-typed expression even when its
|
||||
// spelling folds (for example a function returning `"binary"`).
|
||||
const encoding: IrExpr = ((): IrExpr => {
|
||||
const defaultEncoding = { kind: "strLit", value: "utf8", type: STRING, loc } satisfies IrExpr;
|
||||
if (secondUndefined) {
|
||||
return defaultAfterUndefined(secondUndefined, defaultEncoding);
|
||||
}
|
||||
if (!encodingNode) {
|
||||
return defaultEncoding;
|
||||
}
|
||||
const aliases: Record<string, string | undefined> = {
|
||||
utf8: "utf8", "utf-8": "utf8", hex: "hex", base64: "base64",
|
||||
base64url: "base64url", latin1: "latin1", binary: "latin1", ascii: "ascii",
|
||||
utf16le: "utf16le", "utf-16le": "utf16le", ucs2: "utf16le", "ucs-2": "utf16le",
|
||||
};
|
||||
const t = L.typeOf(encodingNode);
|
||||
const raw = t.isStringLiteralType() ? t.value : undefined;
|
||||
const canonical = raw !== undefined ? own(aliases, raw) : undefined;
|
||||
if (canonical === undefined) {
|
||||
L.noLowering(
|
||||
`process.${stream}.write with this encoding`,
|
||||
encodingNode,
|
||||
'use a literal "utf8", "hex", "base64", "base64url", "latin1", "binary", "ascii", "utf16le", or "ucs2" encoding',
|
||||
);
|
||||
}
|
||||
const evaluated = L.lowerExprExpecting(encodingNode, STRING);
|
||||
if (canonical === raw) return evaluated;
|
||||
const normalized: IrExpr = { kind: "strLit", value: canonical, type: STRING, loc: locOf(encodingNode) };
|
||||
return droppableStatic(evaluated)
|
||||
? normalized
|
||||
: {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "exprStmt", expr: evaluated, loc: evaluated.loc }],
|
||||
result: normalized,
|
||||
type: STRING,
|
||||
loc: evaluated.loc,
|
||||
};
|
||||
})();
|
||||
|
||||
let data = L.lowerExpr(args[0]!);
|
||||
// A checked-dynamic argument in a JS file takes the validated
|
||||
// string exit (the trust-but-verify boundary: commander's
|
||||
// `writeOut: (str) => process.stdout.write(str)` — str untyped):
|
||||
@@ -6053,24 +6114,92 @@ function optionMember(p: ts.ObjectLiteralElementLike): { name: string; value: ts
|
||||
if (data.type.kind === "dyn" && isJsSourceFile(call.getSourceFile())) {
|
||||
data = { kind: "dynCheck", value: data, type: STRING, loc };
|
||||
}
|
||||
// The Buffer overload writes raw bytes through the same promptly
|
||||
// submitted streams.
|
||||
if (data.type.kind === "bytes" && data.type.elem === "u8") {
|
||||
const isBytes = data.type.kind === "bytes" && data.type.elem === "u8";
|
||||
if (!isBytes && data.type.kind !== "string") {
|
||||
L.noLowering(
|
||||
`process.${stream}.write of non-string data`,
|
||||
args[0]!,
|
||||
"strings and Buffer/Uint8Array values write; narrow unions first",
|
||||
);
|
||||
}
|
||||
|
||||
let callback: IrExpr | undefined;
|
||||
if (callbackNode) {
|
||||
callback = L.lowerExpr(callbackNode);
|
||||
if (callback.type.kind === "dyn" && isJsSourceFile(call.getSourceFile())) {
|
||||
callback = {
|
||||
kind: "dynCheck",
|
||||
value: callback,
|
||||
type: funcOf([DYN], VOID),
|
||||
loc: locOf(callbackNode),
|
||||
};
|
||||
}
|
||||
let callbackOk = callback.type.kind === "func" && callback.type.params.length <= 1;
|
||||
if (callbackOk && callback.type.kind === "func" && callback.type.params.length === 1) {
|
||||
const param = callback.type.params[0]!;
|
||||
if (param.kind !== "dyn") {
|
||||
const def = param.kind === "union" ? L.unions.get(param.unionId) : undefined;
|
||||
callbackOk = !!def &&
|
||||
def.arms.some((a) => a.kind === "nullT") &&
|
||||
def.arms.some((a) => a.kind === "object" && a.className === "%Error") &&
|
||||
def.arms.every((a) =>
|
||||
a.kind === "nullT" || a.kind === "undefinedT" ||
|
||||
(a.kind === "object" && a.className === "%Error"));
|
||||
}
|
||||
}
|
||||
if (!callbackOk) {
|
||||
L.unsupported(
|
||||
"SC1090",
|
||||
callbackNode,
|
||||
"process output completion callbacks must accept at most one Error | null parameter",
|
||||
);
|
||||
}
|
||||
callback = voidizedCallback(L, callback, locOf(callbackNode));
|
||||
}
|
||||
|
||||
// Encoding- or callback-bearing writes use one fixed byte ABI. For
|
||||
// strings, Buffer.from's encoder runs after data/encoding evaluation
|
||||
// and before the callback expression; only its pure allocation moves
|
||||
// earlier than Node's internal write conversion.
|
||||
if (callback || args.length > 1 || isBytes) {
|
||||
const bytes = isBytes
|
||||
? data
|
||||
: { kind: "libCall", fn: "buffer.fromStr", args: [data, encoding], type: BYTES_U8, loc } satisfies IrExpr;
|
||||
const defaultRuntimeEncoding = { kind: "strLit", value: "utf8", type: STRING, loc } satisfies IrExpr;
|
||||
let runtimeEncoding = isBytes ? encoding : defaultRuntimeEncoding;
|
||||
// An explicitly-undefined third argument is still evaluated after
|
||||
// data and encoding, even though it schedules no callback. Strings
|
||||
// already evaluate encoding while producing `bytes`; byte chunks
|
||||
// fold both ignored argument effects into this final ABI slot.
|
||||
if (thirdUndefined && !droppableStatic(thirdUndefined)) {
|
||||
const effects = isBytes
|
||||
? [encoding, thirdUndefined].filter((effect) => !droppableStatic(effect))
|
||||
: [thirdUndefined];
|
||||
runtimeEncoding = {
|
||||
kind: "seqExpr",
|
||||
stmts: effects.map((effect) => ({ kind: "exprStmt", expr: effect, loc: effect.loc })),
|
||||
result: defaultRuntimeEncoding,
|
||||
type: STRING,
|
||||
loc: thirdUndefined.loc,
|
||||
};
|
||||
}
|
||||
if (callback) {
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: stream === "stdout" ? "process.stdoutWriteBytesCb" : "process.stderrWriteBytesCb",
|
||||
args: [bytes, runtimeEncoding, callback],
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: stream === "stdout" ? "process.stdoutWriteBytes" : "process.stderrWriteBytes",
|
||||
args: [data],
|
||||
args: [bytes, runtimeEncoding],
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (data.type.kind !== "string") {
|
||||
L.noLowering(
|
||||
`process.${stream}.write of non-string data`,
|
||||
call.arguments[0]!,
|
||||
"strings and Buffer/Uint8Array values write; narrow unions first",
|
||||
);
|
||||
}
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: stream === "stdout" ? "process.stdoutWrite" : "process.stderrWrite",
|
||||
|
||||
@@ -2882,9 +2882,13 @@ export type IrLibFn =
|
||||
| "zlib.deflateSync"
|
||||
| "zlib.inflateSync"
|
||||
/** The Buffer overloads of the raw stream writes — same promptly
|
||||
* submitted streams as process.stdoutWrite/stderrWrite, constantly true. */
|
||||
* submitted streams as process.stdoutWrite/stderrWrite, constantly true.
|
||||
* The encoding arg is evaluated but ignored for bytes, like Node. The Cb
|
||||
* forms move their program-shaped completion callback to the tick queue. */
|
||||
| "process.stdoutWriteBytes"
|
||||
| "process.stderrWriteBytes"
|
||||
| "process.stdoutWriteBytesCb"
|
||||
| "process.stderrWriteBytesCb"
|
||||
| "fsp.readFile"
|
||||
| "fsp.writeFile"
|
||||
| "fsp.mkdir"
|
||||
@@ -6490,6 +6494,8 @@ const LIB_MODE_REFUSED_PREFIXES: readonly [string, string][] = [
|
||||
// exclude — refuse the surface like the rest of the event-loop family.
|
||||
["fs.existsChk", "the async fs callback surface (fs.exists)"],
|
||||
["fs.renameCb", "the async fs callback surface (fs.rename)"],
|
||||
["process.stdoutWriteBytesCb", "process.stdout.write completion callbacks"],
|
||||
["process.stderrWriteBytesCb", "process.stderr.write completion callbacks"],
|
||||
["timers.", "the timers surface (setTimeout family)"],
|
||||
["tp.", "the timers/promises surface"],
|
||||
["cp.", "the child_process surface"],
|
||||
|
||||
@@ -751,8 +751,12 @@ export const LIB_FN_SIGS: Record<IrLibFn, { argTypes: (IrType | null)[]; result:
|
||||
"fsp.readFileBytes": { argTypes: [STRING], result: { kind: "promise", inner: BYTES_U8 } },
|
||||
"zlib.deflateSync": { argTypes: [BYTES_U8], result: BYTES_U8 },
|
||||
"zlib.inflateSync": { argTypes: [BYTES_U8], result: BYTES_U8 },
|
||||
"process.stdoutWriteBytes": { argTypes: [BYTES_U8], result: BOOL },
|
||||
"process.stderrWriteBytes": { argTypes: [BYTES_U8], result: BOOL },
|
||||
"process.stdoutWriteBytes": { argTypes: [BYTES_U8, STRING], result: BOOL },
|
||||
"process.stderrWriteBytes": { argTypes: [BYTES_U8, STRING], result: BOOL },
|
||||
// Completion callback is program-dependent: zero params, checked-dynamic,
|
||||
// or an optional Error | null slot (same success shape as fs.rename).
|
||||
"process.stdoutWriteBytesCb": { argTypes: [BYTES_U8, STRING, null], result: BOOL },
|
||||
"process.stderrWriteBytesCb": { argTypes: [BYTES_U8, STRING, null], result: BOOL },
|
||||
"fsp.readFile": { argTypes: [STRING, STRING], result: { kind: "promise", inner: STRING } },
|
||||
"fsp.writeFile": { argTypes: [STRING, STRING], result: { kind: "promise", inner: VOID } },
|
||||
"fsp.mkdir": { argTypes: [STRING], result: { kind: "promise", inner: VOID } },
|
||||
@@ -3972,7 +3976,11 @@ function validateFunction(
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (e.fn === "fs.renameCb") {
|
||||
if (
|
||||
e.fn === "fs.renameCb" ||
|
||||
e.fn === "process.stdoutWriteBytesCb" ||
|
||||
e.fn === "process.stderrWriteBytesCb"
|
||||
) {
|
||||
// The callback is void and accepts either no parameters, one
|
||||
// checked-dynamic error slot (JS), or Error | null (optionally
|
||||
// including undefined for an explicitly optional parameter).
|
||||
@@ -3994,7 +4002,7 @@ function validateFunction(
|
||||
}
|
||||
}
|
||||
if (!ok) {
|
||||
err(`libCall fs.renameCb callback shape (frontend must fence)`, e.loc);
|
||||
err(`libCall ${e.fn} callback shape (frontend must fence)`, e.loc);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -628,8 +628,9 @@ ScrArr *scr_active_resources(void) {
|
||||
* left queued on the uncaught paths) never run — the teardown releases
|
||||
* them, like Node dropping the queue at exit. */
|
||||
typedef struct ScrNtick {
|
||||
ScrClosure *cb; /* owned; NULL for a raw C-hook entry */
|
||||
void (*raw)(void); /* the hook when cb == NULL (stream tick markers) */
|
||||
ScrClosure *cb; /* owned; NULL for a raw C-hook entry */
|
||||
ScrFsRenameFn error_cb; /* process-write success adapter, or NULL */
|
||||
void (*raw)(void); /* hook when cb == NULL (stream markers) */
|
||||
struct ScrNtick *next;
|
||||
} ScrNtick;
|
||||
|
||||
@@ -650,6 +651,20 @@ void scr_next_tick(ScrClosure *cb /*moves*/) {
|
||||
scr_nt_tail = t;
|
||||
}
|
||||
|
||||
/* stdout/stderr write completions are Node nextTicks too, but their
|
||||
* callback receives the success `null` argument. Reuse the error-first
|
||||
* adapter ABI also used by fs.rename: emitted adapters construct the
|
||||
* program's Error | null union, and NULL selects its null arm. */
|
||||
void scr_process_write_callback(ScrClosure *cb /*moves*/, ScrFsRenameFn fn) {
|
||||
ScrNtick *t = calloc(1, sizeof *t);
|
||||
if (!t) scr_oom();
|
||||
t->cb = cb;
|
||||
t->error_cb = fn;
|
||||
if (scr_nt_tail) scr_nt_tail->next = t;
|
||||
else scr_nt_head = t;
|
||||
scr_nt_tail = t;
|
||||
}
|
||||
|
||||
/* A RAW C-hook tick: the stream unit enqueues one marker per deferred
|
||||
* stream emission so those emissions interleave with user nextTicks in
|
||||
* true FIFO order — in Node they ARE nextTicks (resume_, emitReadable_,
|
||||
@@ -2006,10 +2021,12 @@ bool scr_loop_run(ScrPromise *top_level) {
|
||||
scr_nt_head = t->next;
|
||||
if (scr_nt_head == NULL) scr_nt_tail = NULL;
|
||||
ScrClosure *cb = t->cb;
|
||||
ScrFsRenameFn error_cb = t->error_cb;
|
||||
void (*raw)(void) = t->raw;
|
||||
free(t);
|
||||
if (cb) {
|
||||
((void (*)(ScrClosure *))cb->fn)(cb);
|
||||
if (error_cb) error_cb(cb, NULL);
|
||||
else ((void (*)(ScrClosure *))cb->fn)(cb);
|
||||
scr_closure_release(cb);
|
||||
} else {
|
||||
raw(); /* one stream tick, FIFO with the user ticks around it */
|
||||
|
||||
@@ -153,12 +153,14 @@ ScrBytes *scr_crypto_random_bytes(double n) {
|
||||
|
||||
/* ── process.stdout/stderr.write(buf) ──────────────────────────────────── */
|
||||
|
||||
bool scr_process_stdout_write_bytes(const ScrBytes *b) {
|
||||
bool scr_process_stdout_write_bytes(const ScrBytes *b, const ScrStr *encoding) {
|
||||
(void)encoding;
|
||||
scr_stdio_write(1, b->data, b->len * scr_bytes_elem_size(b->elem));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool scr_process_stderr_write_bytes(const ScrBytes *b) {
|
||||
bool scr_process_stderr_write_bytes(const ScrBytes *b, const ScrStr *encoding) {
|
||||
(void)encoding;
|
||||
scr_stdio_write(2, b->data, b->len * scr_bytes_elem_size(b->elem));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -3783,6 +3783,10 @@ bool scr_immediate_has_ref(double handle);
|
||||
* teardown (they must never run yet must not leak). cb ownership moves
|
||||
* in. */
|
||||
void scr_next_tick(ScrClosure *cb);
|
||||
/* A successful process.stdout/stderr.write completion on that same queue.
|
||||
* The callback and its program-shaped Error | null adapter both MOVE into
|
||||
* the entry; the adapter is invoked with NULL (Node's success argument). */
|
||||
void scr_process_write_callback(ScrClosure *cb, ScrFsRenameFn fn);
|
||||
/* A raw C-hook entry on the SAME queue: the stream unit enqueues one
|
||||
* marker per deferred stream emission, so stream ticks and user
|
||||
* nextTicks run in true FIFO order (in Node they are the same queue).
|
||||
@@ -4978,10 +4982,12 @@ ScrPromise *scr_fsp_read_file_bytes(ScrStr *path); /* +1 */
|
||||
* RangeError catchably (same check as scr_crypto_random_string). */
|
||||
ScrBytes *scr_crypto_random_bytes(double n); /* +1 */
|
||||
|
||||
/* process.stdout/stderr.write(buf): the raw byte writes' Buffer overloads
|
||||
* (same streams and buffering as the string forms). Constantly true. */
|
||||
bool scr_process_stdout_write_bytes(const ScrBytes *b);
|
||||
bool scr_process_stderr_write_bytes(const ScrBytes *b);
|
||||
/* process.stdout/stderr.write(buf[, encoding]): the raw byte writes' Buffer
|
||||
* overloads (same streams and buffering as the string forms). The encoding
|
||||
* is evaluated by the caller and ignored here, as Node does for bytes.
|
||||
* Constantly true. */
|
||||
bool scr_process_stdout_write_bytes(const ScrBytes *b, const ScrStr *encoding);
|
||||
bool scr_process_stderr_write_bytes(const ScrBytes *b, const ScrStr *encoding);
|
||||
|
||||
/* The Node-shaped fs error thrower (scr_lib.c): formats "ENOENT: no such
|
||||
* file or directory, open 'x'" and throws it catchably. Shared with
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
// Static process stdout/stderr writes: BufferEncoding arguments affect
|
||||
// strings, are ignored for byte chunks, and completion callbacks run later
|
||||
// with Node's success `null`. Argument effects still precede submission.
|
||||
|
||||
function chunk(): string {
|
||||
console.log("data-expression");
|
||||
return "7374617469632d656e636f6465647c";
|
||||
}
|
||||
|
||||
function encoding(): "hex" {
|
||||
console.log("encoding-expression");
|
||||
return "hex";
|
||||
}
|
||||
|
||||
function completion(label: string): (error?: Error | null) => number {
|
||||
console.log(`callback-expression:${label}`);
|
||||
return (error) => {
|
||||
console.log(`callback:${label}:${error === null}`);
|
||||
return 1; // Writable.write ignores completion callback results.
|
||||
};
|
||||
}
|
||||
|
||||
function omitted(label: string): undefined {
|
||||
console.log(`omitted-expression:${label}`);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
process.stdout.write("literal-two-undefined|", undefined);
|
||||
process.stdout.write("literal-three-undefined|", "utf8", undefined);
|
||||
process.stdout.write("effect-two-undefined|", omitted("encoding"));
|
||||
process.stdout.write("effect-three-undefined|", "utf8", omitted("callback"));
|
||||
process.stdout.write(Buffer.from("byte-two-undefined|"), omitted("byte-encoding"));
|
||||
process.stdout.write(Buffer.from("byte-three-undefined|"), "utf8", omitted("byte-callback"));
|
||||
|
||||
const encoded = process.stdout.write(chunk(), encoding(), completion("encoded"));
|
||||
console.log("encoded-return", encoded);
|
||||
|
||||
process.stdout.write("ZGVmYXVsdHxlbmNvZGluZ3w=", "base64");
|
||||
process.stdout.write("é|", "binary");
|
||||
process.stdout.write(Buffer.from("buffer|"), "utf16le", completion("buffer"));
|
||||
process.stdout.write("default|", completion("default"));
|
||||
process.stdout.write("zero|", () => console.log("callback:zero"));
|
||||
|
||||
Promise.resolve().then(() => console.log("promise"));
|
||||
process.nextTick(() => console.log("tick"));
|
||||
console.log("body-done");
|
||||
|
||||
process.stderr.write("stderr-body|", "utf8", (error) => {
|
||||
process.stderr.write(`stderr-callback:${error === null}\n`);
|
||||
});
|
||||
@@ -85,4 +85,22 @@ describe("async_free detection over the IR", () => {
|
||||
await surfaceOf("sig", `process.on("SIGINT", () => console.log("int"));\nconsole.log("armed");\n`),
|
||||
).toContain("signal");
|
||||
});
|
||||
|
||||
test("process output completion callbacks refuse as event-loop work", async () => {
|
||||
expect(
|
||||
await surfaceOf(
|
||||
"stdout-write-callback",
|
||||
`process.stdout.write("x", () => console.log("written"));\n`,
|
||||
),
|
||||
).toContain("process.stdout.write completion callbacks");
|
||||
});
|
||||
|
||||
test("explicitly omitted process output callbacks remain async_free", async () => {
|
||||
expect(
|
||||
await surfaceOf(
|
||||
"stdout-write-undefined",
|
||||
`process.stdout.write("x", undefined);\nprocess.stdout.write("y", "utf8", undefined);\n`,
|
||||
),
|
||||
).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user