Skip to content
Merged
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
2 changes: 1 addition & 1 deletion docs/src/generated/node-v24-compatibility-meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
"schemaVersion": 3,
"nodeVersion": "24.22.0",
"nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93",
"artifactVersion": "c51180a4672492a73f1d",
"artifactVersion": "fba8a4c99de7c5424f50",
"rowCount": 3880
}
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": 17,
"refused": 0,
"not-implemented": 21,
"not-implemented": 22,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
Expand Down Expand Up @@ -9312,9 +9312,9 @@
"inherited": true
},
"static": {
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
}
},
{
Expand Down
20 changes: 8 additions & 12 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": 3472,
"tiers": {
"static": {
"audit-partial": 1059,
"verify-gap": 2286,
"audit-partial": 1058,
"verify-gap": 2287,
"replace-refusal": 40,
"classify": 2,
"implement": 85
Expand Down Expand Up @@ -3941,16 +3941,12 @@
"priority": "high",
"tiers": {
"static": {
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:child_process.child_process.exec",
"tests": [
"tests/corpus/5627-exec-callback.ts",
"tests/fixtures/node-types/child-exec.ts",
"tests/harness/project-config.test.ts"
]
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:child_process",
"tests": []
}
}
},
Expand Down
18 changes: 5 additions & 13 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": 17,
"refused": 0,
"not-implemented": 21,
"not-implemented": 22,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
Expand Down Expand Up @@ -11663,10 +11663,7 @@
"tests/corpus/3127-spawn-runtime-options.ts",
"tests/corpus/3130-child-generic-streams.ts",
"tests/corpus/3131-child-rest-listeners.js",
"tests/corpus/3132-child-generic-stream-completion.ts",
"tests/corpus/5627-exec-callback.ts",
"tests/fixtures/node-types/child-exec.ts",
"tests/harness/project-config.test.ts"
"tests/corpus/3132-child-generic-stream-completion.ts"
]
},
"anchorSource": "chapter"
Expand Down Expand Up @@ -11735,13 +11732,8 @@
"inherited": true
},
"static": {
"status": "partial",
"evidence": "compiler-feature:child_process.child_process.exec",
"tests": [
"tests/corpus/5627-exec-callback.ts",
"tests/fixtures/node-types/child-exec.ts",
"tests/harness/project-config.test.ts"
]
"status": "not-implemented",
"evidence": "compiler-unmatched:child_process"
},
"anchorSource": "exact"
},
Expand Down
1 change: 0 additions & 1 deletion internal/compatibility/static-support.json
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,6 @@
{ "chapter": "packages", "symbols": ["--input-type"], "status": "not-applicable" },
{ "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": ["`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
8 changes: 4 additions & 4 deletions packages/compiler/ambient/scriptc-node-fallback.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2971,10 +2971,10 @@ declare module "child_process" {
args?: string[] | null,
options?: ExecSyncOptions,
): unknown;
/* The async shell form. The callback forms lower like execFile's, with
* the command line run by /bin/sh -c, and util.promisify recognizes it
* statically, as it does execFile; reached calls without a callback
* fence at their site. */
/* The async shell form — declared surface without full lowering (the
* type-level tolerance story): harness code on paths tests don't reach
* must typecheck; a reached call fences at its site. util.promisify
* recognizes it statically, as it does execFile. */
export function exec(
command: string,
options?: ExecSyncOptions | ((error: Error | null, stdout: string, stderr: string) => void),
Expand Down
5 changes: 2 additions & 3 deletions packages/compiler/src/backend/llvm/lib-process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,12 @@ export function emitChildProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr
const argv = host.emitExpr(e.args[1]!);
const cb = host.emitExpr(callbackArg);
host.moveTemp(cb);
const shell = host.emitExpr(e.args[3]!);
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)`);
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})`,
);
return host.own({ name: out, type: e.type });
}
Expand Down
4 changes: 2 additions & 2 deletions packages/compiler/src/frontend/lowering/builtin-calls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ export function lowerBuiltinCall(
loc: SrcLoc,
namespaceMember?: ts.MemberName,
): IrExpr {
if (bi.module === "child_process" && (bi.member === "execFile" || bi.member === "exec")) {
return lowerer.lowerExecFileCall(expr, loc, bi.member === "exec");
if (bi.module === "child_process" && bi.member === "execFile") {
return lowerer.lowerExecFileCall(expr, loc);
}
if (bi.module === "events" && bi.member === "addAbortListener") {
return lowerAddAbortListener(lowerer, expr, loc);
Expand Down
72 changes: 22 additions & 50 deletions packages/compiler/src/frontend/lowering/builtins/child-process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -725,57 +725,35 @@ export function lowerForkCall(lowerer: Lowerer, expr: ts.CallExpression, loc: Sr
}

/** `execFile(file[, args][, options], callback)` — the asynchronous callback
* slice. With `shell`, `exec(command[, options], callback)`: the command
* line runs with /bin/sh -c, as execSync runs it, and failure messages show
* the command line as Node's do. The child starts immediately with
* 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,
* slice. The child starts immediately with 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. */
export function lowerExecFileCall(
lowerer: Lowerer,
expr: ts.CallExpression,
loc: SrcLoc,
shell = false,
): IrExpr {
const what = shell ? "exec" : "execFile";
export function lowerExecFileCall(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr {
if (
expr.arguments.some(ts.isSpreadElement) ||
expr.arguments.length < 2 ||
expr.arguments.length > (shell ? 3 : 4)
expr.arguments.length > 4
) {
lowerer.noLowering(
`${what} with ${expr.arguments.length} arguments`,
`execFile with ${expr.arguments.length} arguments`,
expr,
shell
? "the supported callback forms are exec(command, callback) and exec(command, { encoding: 'utf8', maxBuffer: N }, callback)"
: "the supported callback forms are execFile(file, callback), execFile(file, args, callback), and execFile(file, args, { encoding: 'utf8', maxBuffer: N }, callback)",
"the supported callback forms are execFile(file, callback), execFile(file, args, callback), and execFile(file, args, { encoding: 'utf8', maxBuffer: N }, callback)",
);
}
const first = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING);
const cmd = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING);
const argsNode = expr.arguments.length >= 3 ? expr.arguments[1] : undefined;
const callbackNode = expr.arguments[expr.arguments.length - 1]!;
// exec: /bin/sh -c <command>, execSync's lowering (the runtime reads the
// command line back from argv[1] for failure messages); execFile: the
// file and its args list.
const cmd: IrExpr = shell ? { kind: "strLit", value: "/bin/sh", type: STRING, loc } : first;
const argv: IrExpr = shell
? {
kind: "arrayLit",
elems: [{ kind: "strLit", value: "-c", type: STRING, loc }, first],
type: arrayOf(STRING),
loc,
}
: lowerChildArgsArg(lowerer, expr.arguments.length >= 3 ? expr.arguments[1] : undefined, loc);
const optionsNode =
expr.arguments.length === (shell ? 3 : 4) ? expr.arguments[shell ? 1 : 2] : undefined;
if (optionsNode) {
const options = optionsNode;
const argv = lowerChildArgsArg(lowerer, argsNode, loc);
if (expr.arguments.length === 4) {
const options = expr.arguments[2]!;
if (!ts.isObjectLiteralExpression(options)) {
lowerer.noLowering(
`${what} with a non-literal options argument`,
"execFile with a non-literal options argument",
options,
"pass encoding and maxBuffer in an inline object literal",
);
Expand All @@ -796,7 +774,7 @@ export function lowerExecFileCall(
const member = optionMember(property);
if (!member)
lowerer.noLowering(
`${what} with this options shape`,
"execFile with this options shape",
property,
"use plain encoding and maxBuffer properties without spreads or computed keys",
);
Expand All @@ -806,22 +784,22 @@ export function lowerExecFileCall(
(member.value.text !== "utf8" && member.value.text !== "utf-8")
) {
lowerer.noLowering(
`${what} with a non-literal utf8 encoding`,
"execFile with a non-literal utf8 encoding",
member.value,
"pass the literal 'utf8' or 'utf-8' (other expressions might have side effects)",
);
}
} else if (member.name === "maxBuffer") {
if (!pureNumber(member.value)) {
lowerer.noLowering(
`${what} with a non-literal maxBuffer`,
"execFile with a non-literal maxBuffer",
member.value,
"pass a numeric literal or literal arithmetic (the native capture grows without enforcing this limit)",
);
}
} else {
lowerer.noLowering(
`${what} option '${member.name}'`,
`execFile option '${member.name}'`,
property,
"only encoding: 'utf8' and a pure numeric maxBuffer are supported in the callback form",
);
Expand All @@ -837,14 +815,14 @@ export function lowerExecFileCall(
lowerer.unsupported(
"SC1090",
callbackNode,
`${what} callbacks take (error), (error, stdout), (error, stdout, stderr), or no parameters`,
"execFile callbacks take (error), (error, stdout), (error, stdout, stderr), or no parameters",
);
}
if (callback.type.ret.kind !== "void") {
lowerer.unsupported(
"SC1090",
callbackNode,
`${what} callbacks returning a value (make the callback body a block, or return nothing)`,
"execFile callbacks returning a value (make the callback body a block, or return nothing)",
);
}
const errorParam = callback.type.params[0];
Expand All @@ -859,7 +837,7 @@ export function lowerExecFileCall(
lowerer.unsupported(
"SC1090",
callbackNode,
`${what} callbacks whose first parameter is not 'Error | null' (got '${lowerer.fmt(errorParam)}')`,
`execFile callbacks whose first parameter is not 'Error | null' (got '${lowerer.fmt(errorParam)}')`,
);
}
}
Expand All @@ -868,15 +846,9 @@ export function lowerExecFileCall(
lowerer.unsupported(
"SC1090",
callbackNode,
`${what} callbacks whose ${i === 1 ? "stdout" : "stderr"} parameter is not 'string'`,
`execFile callbacks whose ${i === 1 ? "stdout" : "stderr"} parameter is not 'string'`,
);
}
}
return {
kind: "libCall",
fn: "cp.execFile",
args: [cmd, argv, callback, boolLit(shell, loc)],
type: CHILD_T,
loc,
};
return { kind: "libCall", fn: "cp.execFile", args: [cmd, argv, callback], type: CHILD_T, loc };
}
25 changes: 0 additions & 25 deletions packages/compiler/src/frontend/lowering/builtins/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,28 +21,6 @@ const ASSERTION_ERROR_PROPERTIES: ReadonlySet<string> = new Set([
* declaration is not stdlib). Reads only: writes keep their fence (no
* compiled program constructs an errno error). Null for non-error
* receivers and non-stdlib members, so the chain keeps trying. */
/** True for child_process.ExecException, exec's callback error. Its
* `code` is the child's numeric exit status (null after a signal), which
* the runtime error's string code slot does not hold, so reads, `in` tests
* and typeof tests of `code` refuse instead of answering from the slot. */
export function isExecExceptionType(lowerer: Lowerer, t: ts.Type): boolean {
const sym = t.getSymbol();
return (
sym?.name === "ExecException" &&
lowerer.checker
.declarationsOf(sym)
.some((d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile()))
);
}

export function refuseExecExceptionCode(lowerer: Lowerer, node: ts.Node): never {
return lowerer.noLowering(
"ExecException.code",
node,
"spawn reports the exit status through its 'close' event",
);
}

export function lowerErrorCodeProperty(
lowerer: Lowerer,
expr: ts.PropertyAccessExpression,
Expand Down Expand Up @@ -142,9 +120,6 @@ export function lowerErrorCodeProperty(
!(recvT.className === "%Error" && lowerer.typeOf(expr.expression).isIntersectionType())
)
return null;
if (expr.name.text === "code" && isExecExceptionType(lowerer, lowerer.typeOf(expr.expression))) {
refuseExecExceptionCode(lowerer, expr);
}
const rawReceiver = lowerer.lowerExpr(expr.expression);
const receiver =
rawReceiver.type.kind === "dyn"
Expand Down
5 changes: 0 additions & 5 deletions packages/compiler/src/frontend/lowering/lower-exprs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { lowerWorkerMetadata } from "./builtins/workers.js";
import { lowerReadlineProperty } from "./builtins/readline.js";
import { lowerBlobInstanceof } from "./builtins/blob.js";
import { lowerKeyObjectInstanceof } from "./builtins/key-object.js";
import { isExecExceptionType, refuseExecExceptionCode } from "./builtins/errors.js";
import { STATS_TYPE_TESTS } from "./builtins/filesystem.js";
import { threadsImportOf } from "../threads-import.js";
import { experimentalImportOf, lifetimeRegionMemberOf } from "../experimental-import.js";
Expand Down Expand Up @@ -11119,7 +11118,6 @@ function lowerErrorCodeTypeofTest(
recv = recv.expression;
const errT = errorRootedObjectOf(lowerer, lowerer.typeOf(recv));
if (!errT) continue;
if (isExecExceptionType(lowerer, lowerer.typeOf(recv))) refuseExecExceptionCode(lowerer, prop);
const caught = lowerer.caughtLocalOf(recv);
let receiver: IrExpr;
if (caught) {
Expand Down Expand Up @@ -12716,9 +12714,6 @@ function lowerInExpression(lowerer: Lowerer, expr: ts.BinaryExpression, loc: Src
loc,
};
}
if (key === "code" && isExecExceptionType(lowerer, lowerer.typeOf(expr.right))) {
refuseExecExceptionCode(lowerer, expr);
}
let recv = lowerer.lowerExpr(expr.right);
const classInfo =
recv.type.kind === "classval" ? lowerer.classes.get(recv.type.className) : undefined;
Expand Down
4 changes: 2 additions & 2 deletions packages/compiler/src/frontend/lowering/lowerer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11485,8 +11485,8 @@ export class Lowerer {
return lowerSpawnCall(this, expr, loc);
}

lowerExecFileCall(expr: ts.CallExpression, loc: SrcLoc, shell = false): IrExpr {
return lowerExecFileCall(this, expr, loc, shell);
lowerExecFileCall(expr: ts.CallExpression, loc: SrcLoc): IrExpr {
return lowerExecFileCall(this, expr, loc);
}

lowerExecSyncCall(expr: ts.CallExpression, shell: boolean, loc: SrcLoc): IrExpr {
Expand Down
Loading
Loading