Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions docs/src/generated/node-v24-compatibility.json
Original file line number Diff line number Diff line change
Expand Up @@ -164,9 +164,9 @@
"apiEntries": 39,
"static": {
"supported": 0,
"partial": 18,
"partial": 19,
"refused": 0,
"not-implemented": 21,
"not-implemented": 20,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
Expand Down Expand Up @@ -9519,9 +9519,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
Expand Down
19 changes: 11 additions & 8 deletions internal/compatibility/generated/node-v24-backlog.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
"tierItemCount": 3454,
"tiers": {
"static": {
"audit-partial": 1197,
"verify-gap": 2130,
"audit-partial": 1198,
"verify-gap": 2129,
"replace-refusal": 40,
"classify": 2,
"implement": 85
Expand Down Expand Up @@ -4221,12 +4221,15 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:child_process",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:child_process.maxBuffer",
"tests": [
"tests/corpus/5637-child-max-buffer-sync.ts",
"tests/corpus/5638-child-max-buffer-async.ts"
]
}
}
},
Expand Down
14 changes: 10 additions & 4 deletions internal/compatibility/generated/node-v24-internal.json
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,9 @@
"apiEntries": 39,
"static": {
"supported": 0,
"partial": 18,
"partial": 19,
"refused": 0,
"not-implemented": 21,
"not-implemented": 20,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
Expand Down Expand Up @@ -11674,6 +11674,8 @@
"tests/corpus/3131-child-rest-listeners.js",
"tests/corpus/3132-child-generic-stream-completion.ts",
"tests/corpus/5627-exec-callback.ts",
"tests/corpus/5637-child-max-buffer-sync.ts",
"tests/corpus/5638-child-max-buffer-async.ts",
"tests/fixtures/node-types/child-exec.ts",
"tests/harness/project-config.test.ts"
]
Expand Down Expand Up @@ -12029,8 +12031,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:child_process"
"status": "partial",
"evidence": "compiler-feature:child_process.maxBuffer",
"tests": [
"tests/corpus/5637-child-max-buffer-sync.ts",
"tests/corpus/5638-child-max-buffer-async.ts"
]
},
"anchorSource": "exact"
},
Expand Down
1 change: 1 addition & 0 deletions internal/compatibility/static-support.json
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,7 @@
{ "chapter": "child_process", "signatures": ["`subprocess[Symbol.dispose]()`"], "status": "partial", "evidence": ["tests/corpus/2844-node-disposable-resources.ts"] },
{ "chapter": "child_process", "signatures": ["`child_process.execFile(file[, args][, options][, callback])`"], "status": "partial", "evidence": ["tests/corpus/2911-execfile-callback.ts"] },
{ "chapter": "child_process", "signatures": ["`child_process.exec(command[, options][, callback])`"], "status": "partial", "evidence": ["tests/corpus/5627-exec-callback.ts", "tests/fixtures/node-types/child-exec.ts", "tests/harness/project-config.test.ts"] },
{ "chapter": "child_process", "signatures": ["`maxBuffer` and Unicode"], "status": "partial", "evidence": ["tests/corpus/5637-child-max-buffer-sync.ts", "tests/corpus/5638-child-max-buffer-async.ts"] },
{ "chapter": "child_process", "signatures": ["`child_process.fork(modulePath[, args][, options])`", "`subprocess.send(message[, sendHandle[, options]][, callback])`", "`subprocess.disconnect()`", "`connected` Type: {boolean} Set to `false` after `subprocess.disconnect()` is called.", "Event: `'message'`", "Event: `'disconnect'`"], "status": "partial", "evidence": ["tests/corpus/2946-child-fork-ipc/main.ts"] },
{ "chapter": "child_process", "signatures": ["Event: `'close'`"], "status": "partial", "evidence": ["tests/corpus/2939-child-shell-text.ts", "tests/corpus/3131-child-rest-listeners.js"] },
{ "chapter": "child_process", "signatures": ["`stdout` Type: {stream.Readable|null|undefined}", "`stderr` Type: {stream.Readable|null|undefined}"], "status": "partial", "evidence": ["tests/corpus/1565-spawn-pipe-streams.ts", "tests/corpus/2939-child-shell-text.ts"] },
Expand Down
9 changes: 6 additions & 3 deletions packages/compiler/src/backend/llvm/lib-process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,17 @@ export function emitChildProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr
const cb = host.emitExpr(callbackArg);
host.toHeap(cb);
const shell = host.emitExpr(e.args[3]!);
const maxBuffer = host.emitExpr(e.args[4]!);
const adapter = host.execFileThunkFor(cbT);
host.usesTimers = true;
host.declare(`declare ptr @scr_exec_file(ptr, ptr, ptr, ptr, i1)`);
host.declare(`declare ptr @scr_exec_file(ptr, ptr, ptr, ptr, i1, double)`);
const out = B.tmp();
B.line(
`${out} = call ptr @scr_exec_file(ptr ${cmd.name}, ptr ${argv.name}, ptr ${cb.name}, ptr @${adapter}, i1 ${shell.name})`,
`${out} = call ptr @scr_exec_file(ptr ${cmd.name}, ptr ${argv.name}, ptr ${cb.name}, ptr @${adapter}, i1 ${shell.name}, double ${maxBuffer.name})`,
);
return host.own({ name: out, type: e.type });
const owned = host.own({ name: out, type: e.type });
host.emitPendingCheck();
return owned;
}
if (
e.fn === "cp.spawn" ||
Expand Down
4 changes: 3 additions & 1 deletion packages/compiler/src/backend/llvm/lib-shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -724,14 +724,16 @@ export const LIB_FN_SYMS: Record<string, string> = {
"process.offSignal": "scr_signal_off",
"process.offExit": "scr_process_off_exit",
// child_process (scr_child.c): spawnSync/exec forms are synchronous
// (execSync/execCapture throw — the may-throw check); handle getters
// (spawnSync, execSync, execCapture and execValidate throw — the
// may-throw check); handle getters
// are pure reads. Registration forms are special-cased (moves + per-
// shape adapters); spawn itself is special-cased for usesTimers.
"cp.spawnSync": "scr_spawn_sync",
"cp.spawnSyncOpts": "scr_spawn_sync_opts",
"cp.spawnSyncStdioStr": "scr_spawn_sync_stdio_str",
"cp.execSync": "scr_exec_sync",
"cp.execCapture": "scr_exec_capture",
"cp.execValidate": "scr_exec_validate",
"spawnRes.stdout": "scr_spawn_res_stdout",
"spawnRes.stderr": "scr_spawn_res_stderr",
"child.killed": "scr_child_killed",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
SPAWNRES_T,
STRING,
type SrcLoc,
VOID,
arrayOf,
} from "../../../ir/ir.js";
import { optionMember } from "./arguments.js";
Expand All @@ -28,7 +29,8 @@ import { execResultShape } from "../../type-mapper.js";
* promisified execFile/exec behind an already-settled promise (the
* fs/promises stance, divergence 23). Options are the exec-sync slice minus stdio (the async form always
* captures both streams, no echo): encoding must spell utf8, cwd/env/
* timeout lower, maxBuffer/windowsHide are accepted no-ops, killSignal
* timeout lower, maxBuffer applies per stream (Node's RangeError
* ERR_CHILD_PROCESS_STDIO_MAXBUFFER), windowsHide is a no-op, killSignal
* only as its SIGTERM default. Rejections carry Node's async messages
* ("Command failed: <cmd>\n<stderr>" — the trailing newline is Node's —
* and "spawn <file> ENOENT" with .code); Node's numeric `.code` on a
Expand Down Expand Up @@ -85,6 +87,8 @@ export function lowerExecFileAsyncCall(
let cwd: IrExpr = emptyStr;
let env: IrExpr = envRec(false, emptyPairs());
let timeout: IrExpr = numLit(0, loc);
// Node's default maxBuffer for exec and execFile.
let maxBuffer: IrExpr = numLit(1024 * 1024, loc);
if (optsNode) {
if (!ts.isObjectLiteralExpression(optsNode)) {
lowerer.noLowering(
Expand Down Expand Up @@ -161,7 +165,7 @@ export function lowerExecFileAsyncCall(
timeout = lowerer.lowerExprExpecting(m.value, F64);
break;
case "maxBuffer":
lowerer.lowerExpr(m.value); // accepted, not enforced (divergence 50)
maxBuffer = lowerer.lowerExprExpecting(m.value, F64);
break;
case "windowsHide":
lowerer.lowerExpr(m.value); // Node no-op on POSIX
Expand All @@ -186,18 +190,51 @@ export function lowerExecFileAsyncCall(
}
}
}
const resultT: IrType = { kind: "promise", inner: { kind: "record", shapeId: helper.shapeId } };
const args = [cmd, argv, cwd, env, timeout, maxBuffer];
if (literalOptionsValid(timeout, maxBuffer)) {
return { kind: "call", callee: helper.name, args, type: resultT, loc };
}
// Node's promisified forms call execFile synchronously, so an invalid
// timeout or maxBuffer throws before the promise exists: the arguments
// are evaluated in order, checked, then passed to the helper.
const types = [STRING, arrayOf(STRING), STRING, envRecT, F64, F64];
const stmts: IrStmt[] = [];
const saved = args.map((init, i) => {
const local = lowerer.declareHiddenLocal("%execArg", types[i]!);
stmts.push({ kind: "varDecl", localId: local.id, init, loc });
return varRef(local.id, types[i]!, loc);
});
stmts.push({
kind: "exprStmt",
expr: { kind: "libCall", fn: "cp.execValidate", args: [saved[4]!, saved[5]!], type: VOID, loc },
loc,
});
return {
kind: "call",
callee: helper.name,
args: [cmd, argv, cwd, env, timeout],
type: { kind: "promise", inner: { kind: "record", shapeId: helper.shapeId } },
kind: "seqExpr",
stmts,
result: { kind: "call", callee: helper.name, args: saved, type: resultT, loc },
type: resultT,
loc,
};
}

/** Literal options Node's validateTimeout and validateMaxBuffer accept,
* which need no check before the call. */
function literalOptionsValid(timeout: IrExpr, maxBuffer: IrExpr): boolean {
return (
timeout.kind === "numLit" &&
Number.isSafeInteger(timeout.value) &&
timeout.value >= 0 &&
maxBuffer.kind === "numLit" &&
maxBuffer.value >= 0
);
}

/** The interned `%execFileAsync` helper behind promisified-execFile
* calls (`%execAsync`, with `shell`, behind promisified-exec ones): an
* ASYNC IR function (params: cmd, argv, cwd, env {has, pairs}, timeoutMs)
* ASYNC IR function (params: cmd, argv, cwd, env {has, pairs}, timeoutMs,
* maxBuffer)
* whose body runs the may-throw cp.execCapture libCall — the async
* machinery turns a throw into the rejection, Node's promisified
* behavior — and returns the `{ stdout, stderr }` record built from the
Expand Down Expand Up @@ -253,6 +290,7 @@ function execFileAsyncHelper(
loc,
},
varRef("timeout.0", F64, loc),
varRef("maxBuffer.0", F64, loc),
],
type: SPAWNRES_T,
loc,
Expand Down Expand Up @@ -299,6 +337,7 @@ function execFileAsyncHelper(
{ localId: "cwd.0", name: "cwd", type: STRING },
{ localId: "env.0", name: "env", type: envRecT },
{ localId: "timeout.0", name: "timeout", type: F64 },
{ localId: "maxBuffer.0", name: "maxBuffer", type: F64 },
],
returnType: recT,
async: true,
Expand All @@ -308,6 +347,7 @@ function execFileAsyncHelper(
{ id: "cwd.0", name: "cwd", type: STRING, mutable: false },
{ id: "env.0", name: "env", type: envRecT, mutable: false },
{ id: "timeout.0", name: "timeout", type: F64, mutable: false },
{ id: "maxBuffer.0", name: "maxBuffer", type: F64, mutable: false },
{ id: "r.0", name: "r", type: SPAWNRES_T, mutable: false },
],
body,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ import { optionMember } from "./arguments.js";
* are drawn from the honestly-implemented set — `encoding` (must be the
* "utf8"/"utf-8" literal, like spawnSync — outputs are captured utf8),
* `cwd`, `env`, `input`, `timeout`, `stdio` (the "pipe"/"ignore"
* string form or a 3-tuple of those), `maxBuffer` (accepted, not
* enforced — the capture grows), `killSignal` (accepted only as the
* string form or a 3-tuple of those), `maxBuffer` (Node's 1 MiB default
* otherwise; 0 means no limit), `killSignal` (accepted only as the
* SIGTERM default), `windowsHide`/`shell:false` on execFileSync (Node
* no-ops here). Every other member (a non-default killSignal, shell:true
* on execFileSync, ...) fences by name. */
Expand Down Expand Up @@ -99,6 +99,8 @@ export function lowerExecSyncCall(
let hasEnv: IrExpr = boolLit(false, loc);
let envPairs: IrExpr = emptyPairs;
let timeout: IrExpr = numLit(0, loc);
// Node's default maxBuffer for the sync forms.
let maxBuffer: IrExpr = numLit(1024 * 1024, loc);
let stdoutMode = 1;
let stderrMode = 0;
let stdinInherit = false;
Expand Down Expand Up @@ -138,6 +140,7 @@ export function lowerExecSyncCall(
runtime.stderrMode,
// The runtime options carry a utf8 encoding (others throw).
boolLit(false, loc),
runtime.maxBuffer,
],
type: STRING,
loc,
Expand Down Expand Up @@ -338,9 +341,7 @@ export function lowerExecSyncCall(
applyStdio(m.value);
break;
case "maxBuffer":
// Accepted, not enforced (the capture grows unbounded — no
// real corpus hits the cap); evaluate for side effects.
lowerer.lowerExpr(m.value);
maxBuffer = lowerer.lowerExprExpecting(m.value, F64);
break;
case "killSignal": {
const t = lowerer.typeOf(m.value);
Expand Down Expand Up @@ -401,6 +402,7 @@ export function lowerExecSyncCall(
numLit(stdoutMode + (stdinInherit ? 4 : 0), loc),
numLit(stderrMode, loc),
boolLit(!encodingGiven, loc),
maxBuffer,
],
type: STRING,
loc,
Expand Down Expand Up @@ -445,6 +447,7 @@ function lowerExecSyncRuntimeOptions(
hasInput: IrExpr;
cwd: IrExpr;
timeout: IrExpr;
maxBuffer: IrExpr;
stdoutMode: IrExpr;
stderrMode: IrExpr;
} | null {
Expand Down Expand Up @@ -518,6 +521,7 @@ function lowerExecSyncRuntimeOptions(
hasInput,
cwd: readOpt("cwd", emptyStr, STRING),
timeout: readOpt("timeout", { kind: "numLit", value: 0, type: F64, loc }, F64),
maxBuffer: readOpt("maxBuffer", { kind: "numLit", value: 1024 * 1024, type: F64, loc }, F64),
stdoutMode: modeCall(1),
stderrMode: modeCall(2),
};
Expand Down
33 changes: 8 additions & 25 deletions packages/compiler/src/frontend/lowering/builtins/child-process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -731,10 +731,9 @@ export function lowerForkCall(lowerer: Lowerer, expr: ts.CallExpression, loc: Sr
* stdin/stdout/stderr piped; the runtime captures both outputs and invokes
* the error-first callback after settlement. The callback may ignore a suffix of `(error, stdout,
* stderr)`, but every declared parameter must have the Node shape. Inline
* `encoding: "utf8"` and a pure numeric `maxBuffer` are accepted; the latter
* is not enforced by the growing native capture, as
* with the synchronous and promisified capture forms. Other options remain
* fenced until their lifecycle can share this asynchronous core. */
* `encoding: "utf8"` and a numeric `maxBuffer` (Node's 1 MiB default
* otherwise, validated by the runtime) are accepted. Other options remain fenced until their
* lifecycle can share this asynchronous core. */
export function lowerExecFileCall(
lowerer: Lowerer,
expr: ts.CallExpression,
Expand Down Expand Up @@ -773,6 +772,8 @@ export function lowerExecFileCall(
expr.arguments.nodes.length >= 3 ? expr.arguments.nodes[1] : undefined,
loc,
);
// Node's default maxBuffer for exec and execFile.
let maxBuffer: IrExpr = numLit(1024 * 1024, loc);
const optionsNode =
expr.arguments.nodes.length === (shell ? 3 : 4)
? expr.arguments.nodes[shell ? 1 : 2]
Expand All @@ -786,18 +787,6 @@ export function lowerExecFileCall(
"pass encoding and maxBuffer in an inline object literal",
);
}
const pureNumber = (node: ts.Expression): boolean =>
ts.isNumericLiteral(node) ||
(ts.isParenthesizedExpression(node) && pureNumber(node.expression)) ||
(ts.isBinaryExpression(node) &&
[
ts.SyntaxKind.PlusToken,
ts.SyntaxKind.MinusToken,
ts.SyntaxKind.AsteriskToken,
ts.SyntaxKind.SlashToken,
].includes(node.operatorToken.kind) &&
pureNumber(node.left) &&
pureNumber(node.right));
for (const property of options.properties.nodes) {
const member = optionMember(property);
if (!member)
Expand All @@ -818,18 +807,12 @@ export function lowerExecFileCall(
);
}
} else if (member.name === "maxBuffer") {
if (!pureNumber(member.value)) {
lowerer.noLowering(
`${what} with a non-literal maxBuffer`,
member.value,
"pass a numeric literal or literal arithmetic (the native capture grows without enforcing this limit)",
);
}
maxBuffer = lowerer.lowerExprExpecting(member.value, F64);
} else {
lowerer.noLowering(
`${what} option '${member.name}'`,
property,
"only encoding: 'utf8' and a pure numeric maxBuffer are supported in the callback form",
"only encoding: 'utf8' and maxBuffer are supported in the callback form",
);
}
}
Expand Down Expand Up @@ -881,7 +864,7 @@ export function lowerExecFileCall(
return {
kind: "libCall",
fn: "cp.execFile",
args: [cmd, argv, callback, boolLit(shell, loc)],
args: [cmd, argv, callback, boolLit(shell, loc), maxBuffer],
type: CHILD_T,
loc,
};
Expand Down
Loading
Loading