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
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 — 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),
Expand Down
5 changes: 3 additions & 2 deletions packages/compiler/src/backend/llvm/lib-process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
}
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") {
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);
Expand Down
72 changes: 50 additions & 22 deletions packages/compiler/src/frontend/lowering/builtins/child-process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
);
Expand All @@ -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",
);
Expand All @@ -784,22 +806,22 @@ 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)",
);
}
} 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",
);
Expand All @@ -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];
Expand All @@ -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)}')`,
);
}
}
Expand All @@ -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,
};
}
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): 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 {
Expand Down
5 changes: 3 additions & 2 deletions packages/compiler/src/ir/builtin-signatures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
1 change: 1 addition & 0 deletions packages/compiler/src/ir/validate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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([
Expand Down
8 changes: 4 additions & 4 deletions packages/runtime/src/scr_child.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
6 changes: 4 additions & 2 deletions packages/runtime/src/scr_child.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions tests/harness/self-hosting-native-driver.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// CI-only: run the native bootstrap lane for this check.
import { verifyBootstrapSeed } from "../../scripts/bootstrap-seed.mjs";
import { execFile, spawnSync } from "node:child_process";
import {
Expand Down
Loading