Do not merge: CI check of the exec lowering changes

This commit is contained in:
Chris Tate
2026-10-17 01:46:05 -05:00
parent 4700ce5c21
commit b407154eee
9 changed files with 73 additions and 40 deletions
+4 -4
View File
@@ -2971,10 +2971,10 @@ declare module "child_process" {
args?: string[] | null,
options?: ExecSyncOptions,
): unknown;
/* 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. */
/* 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. */
export function exec(
command: string,
options?: ExecSyncOptions | ((error: Error | null, stdout: string, stderr: string) => void),
@@ -18,12 +18,13 @@ 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)`);
host.declare(`declare ptr @scr_exec_file(ptr, ptr, ptr, ptr, i1)`);
const out = B.tmp();
B.line(
`${out} = call ptr @scr_exec_file(ptr ${cmd.name}, ptr ${argv.name}, ptr ${cb.name}, ptr @${adapter})`,
`${out} = call ptr @scr_exec_file(ptr ${cmd.name}, ptr ${argv.name}, ptr ${cb.name}, ptr @${adapter}, i1 ${shell.name})`,
);
return host.own({ name: out, type: e.type });
}
@@ -55,8 +55,8 @@ export function lowerBuiltinCall(
loc: SrcLoc,
namespaceMember?: ts.MemberName,
): IrExpr {
if (bi.module === "child_process" && bi.member === "execFile") {
return lowerer.lowerExecFileCall(expr, loc);
if (bi.module === "child_process" && (bi.member === "execFile" || bi.member === "exec")) {
return lowerer.lowerExecFileCall(expr, loc, bi.member === "exec");
}
if (bi.module === "events" && bi.member === "addAbortListener") {
return lowerAddAbortListener(lowerer, expr, loc);
@@ -725,35 +725,57 @@ export function lowerForkCall(lowerer: Lowerer, expr: ts.CallExpression, loc: Sr
}
/** `execFile(file[, args][, options], callback)` — the asynchronous callback
* 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,
* 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,
* 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): IrExpr {
export function lowerExecFileCall(
lowerer: Lowerer,
expr: ts.CallExpression,
loc: SrcLoc,
shell = false,
): IrExpr {
const what = shell ? "exec" : "execFile";
if (
expr.arguments.some(ts.isSpreadElement) ||
expr.arguments.length < 2 ||
expr.arguments.length > 4
expr.arguments.length > (shell ? 3 : 4)
) {
lowerer.noLowering(
`execFile with ${expr.arguments.length} arguments`,
`${what} with ${expr.arguments.length} arguments`,
expr,
"the supported callback forms are execFile(file, callback), execFile(file, args, callback), and execFile(file, args, { encoding: 'utf8', maxBuffer: N }, callback)",
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)",
);
}
const cmd = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING);
const argsNode = expr.arguments.length >= 3 ? expr.arguments[1] : undefined;
const first = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING);
const callbackNode = expr.arguments[expr.arguments.length - 1]!;
const argv = lowerChildArgsArg(lowerer, argsNode, loc);
if (expr.arguments.length === 4) {
const options = expr.arguments[2]!;
// 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;
if (!ts.isObjectLiteralExpression(options)) {
lowerer.noLowering(
"execFile with a non-literal options argument",
`${what} with a non-literal options argument`,
options,
"pass encoding and maxBuffer in an inline object literal",
);
@@ -774,7 +796,7 @@ export function lowerExecFileCall(lowerer: Lowerer, expr: ts.CallExpression, loc
const member = optionMember(property);
if (!member)
lowerer.noLowering(
"execFile with this options shape",
`${what} with this options shape`,
property,
"use plain encoding and maxBuffer properties without spreads or computed keys",
);
@@ -784,7 +806,7 @@ export function lowerExecFileCall(lowerer: Lowerer, expr: ts.CallExpression, loc
(member.value.text !== "utf8" && member.value.text !== "utf-8")
) {
lowerer.noLowering(
"execFile with a non-literal utf8 encoding",
`${what} with a non-literal utf8 encoding`,
member.value,
"pass the literal 'utf8' or 'utf-8' (other expressions might have side effects)",
);
@@ -792,14 +814,14 @@ export function lowerExecFileCall(lowerer: Lowerer, expr: ts.CallExpression, loc
} else if (member.name === "maxBuffer") {
if (!pureNumber(member.value)) {
lowerer.noLowering(
"execFile with a non-literal maxBuffer",
`${what} 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(
`execFile option '${member.name}'`,
`${what} option '${member.name}'`,
property,
"only encoding: 'utf8' and a pure numeric maxBuffer are supported in the callback form",
);
@@ -815,14 +837,14 @@ export function lowerExecFileCall(lowerer: Lowerer, expr: ts.CallExpression, loc
lowerer.unsupported(
"SC1090",
callbackNode,
"execFile callbacks take (error), (error, stdout), (error, stdout, stderr), or no parameters",
`${what} callbacks take (error), (error, stdout), (error, stdout, stderr), or no parameters`,
);
}
if (callback.type.ret.kind !== "void") {
lowerer.unsupported(
"SC1090",
callbackNode,
"execFile callbacks returning a value (make the callback body a block, or return nothing)",
`${what} callbacks returning a value (make the callback body a block, or return nothing)`,
);
}
const errorParam = callback.type.params[0];
@@ -837,7 +859,7 @@ export function lowerExecFileCall(lowerer: Lowerer, expr: ts.CallExpression, loc
lowerer.unsupported(
"SC1090",
callbackNode,
`execFile callbacks whose first parameter is not 'Error | null' (got '${lowerer.fmt(errorParam)}')`,
`${what} callbacks whose first parameter is not 'Error | null' (got '${lowerer.fmt(errorParam)}')`,
);
}
}
@@ -846,9 +868,15 @@ export function lowerExecFileCall(lowerer: Lowerer, expr: ts.CallExpression, loc
lowerer.unsupported(
"SC1090",
callbackNode,
`execFile callbacks whose ${i === 1 ? "stdout" : "stderr"} parameter is not 'string'`,
`${what} callbacks whose ${i === 1 ? "stdout" : "stderr"} parameter is not 'string'`,
);
}
}
return { kind: "libCall", fn: "cp.execFile", args: [cmd, argv, callback], type: CHILD_T, loc };
return {
kind: "libCall",
fn: "cp.execFile",
args: [cmd, argv, callback, boolLit(shell, loc)],
type: CHILD_T,
loc,
};
}
@@ -11485,8 +11485,8 @@ export class Lowerer {
return lowerSpawnCall(this, expr, loc);
}
lowerExecFileCall(expr: ts.CallExpression, loc: SrcLoc): IrExpr {
return lowerExecFileCall(this, expr, loc);
lowerExecFileCall(expr: ts.CallExpression, loc: SrcLoc, shell = false): IrExpr {
return lowerExecFileCall(this, expr, loc, shell);
}
lowerExecSyncCall(expr: ts.CallExpression, shell: boolean, loc: SrcLoc): IrExpr {
@@ -2222,8 +2222,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
/** child_process.execFile's callback slice: starts an all-piped child,
* captures stdout/stderr, and moves the error-first callback into the
* child registry. The callback shape is program-dependent and checked
* by the validator/backends. */
"cp.execFile": { argTypes: [STRING, arrayOf(STRING), null], result: CHILD_T },
* by the validator/backends. The last argument is exec's shell flag:
* the argv is ["-c", command] and failure messages show the command. */
"cp.execFile": { argTypes: [STRING, arrayOf(STRING), null, BOOL], result: CHILD_T },
// spawn's options form: per-slot stdio modes (0 ignore / 1 inherit /
// 2 fd) with the out/err fds for mode 2, detached, env replacement
// pairs, cwd ("" = inherit).
@@ -951,6 +951,7 @@ test("library callbacks retain child, specialized, and generic result diagnostic
{ kind: "strLit", value: "tool", type: STRING, loc },
{ kind: "arrayLit", elems: [], type: arrayOf(STRING), loc },
{ kind: "boolLit", value: true, type: F64, loc },
{ kind: "boolLit", value: false, type: BOOL, loc },
],
};
expect(validateModule(expressionModule(expr, [])).map((error) => error.message)).toEqual([
+4 -4
View File
@@ -2265,14 +2265,14 @@ static void scr_exec_file_fire(ScrChild *c) {
}
ScrChild *scr_exec_file(ScrStr *cmd, ScrArr *args, ScrClosure *cb,
ScrExecFileFn fn) {
ScrExecFileFn fn, bool shell) {
ScrStr *empty = scr_str_new("", 0);
ScrChild *c = scr_spawn_opts(cmd, args, 3, 3, 3, 0, 0, false, false, false,
NULL, empty);
scr_str_release(empty);
c->exec_cb = cb;
c->exec_fn = fn;
char *display = scr_exec_display(cmd, args, false);
char *display = scr_exec_display(cmd, args, shell);
c->exec_display = scr_str_new(display, strlen(display));
free(display);
scr_child_stream_capture_begin(c->out_stream);
@@ -4905,14 +4905,14 @@ static void scr_exec_file_fire(ScrChild *c) {
}
ScrChild *scr_exec_file(ScrStr *cmd, ScrArr *args, ScrClosure *cb,
ScrExecFileFn fn) {
ScrExecFileFn fn, bool shell) {
ScrStr *empty = scr_str_new("", 0);
ScrChild *c = scr_spawn_opts(cmd, args, 3, 3, 3, 0, 0, false, false, false,
NULL, empty);
scr_str_release(empty);
c->exec_cb = cb;
c->exec_fn = fn;
char *display = scr_exec_display(cmd, args, false);
char *display = scr_exec_display(cmd, args, shell);
c->exec_display = scr_str_new(display, strlen(display));
free(display);
scr_child_stream_capture_begin(c->out_stream);
+4 -2
View File
@@ -155,9 +155,11 @@ ScrChild *scr_spawn_dynamic(ScrStr *cmd, ScrArr *args, const ScrDyn *options);
void scr_child_on_spawn(ScrChild *child, ScrClosure *cb); /* moves zero-argument callback */
/* The callback slice: file + args, default options. The returned child has
* all three stdio slots piped; stdout/stderr are captured internally and
* the callback moves into the child registry until settlement. */
* the callback moves into the child registry until settlement. With shell
* (exec), args is ["-c", command] for /bin/sh and failure messages show
* the command, as for scr_exec_sync. */
ScrChild *scr_exec_file(ScrStr *cmd, ScrArr *args, ScrClosure *cb /*moves*/,
ScrExecFileFn fn);
ScrExecFileFn fn, bool shell);
ScrChild *scr_child_retain(ScrChild *c);
void scr_child_release(ScrChild *c);
void *scr_child_retain_v(void *p);