driver: implement post-target run argv semantics

This commit is contained in:
2026-08-23 17:33:31 +09:00
parent 0b24b11d65
commit 29122f2a48
6 changed files with 579 additions and 17 deletions

View File

@@ -25131,3 +25131,307 @@ fn documentationclean(root: str) void = {
&& !directoryhasfragment(root, ".request"));
clean(root);
};
// An explicit run target ends driver-option parsing. The child receives every
// later string unchanged, including spellings that would have been options at
// the driver front; argv[0] remains WW's private PID-bearing executable path.
fn runargvexpect(out: *commandout, code: i32, stdout: str, stderr: str,
message: str) void = {
if (out.termination != exec.termination.EXIT || out.code != code
|| !same(out.stdout, stdout) || !same(out.stderr, stderr)) {
abort(message);
};
};
fn runargvrequire(ok: bool, message: str) void = {
if (!ok) { abort(message); };
};
fn runargvprivateclean(record: str, message: str) void = {
let path: str = readfile(record);
runargvrequire(path.len != 0 && !os.exists(path)
&& !os.exists(strings.concat(path, ".sepwork")), message);
};
fn runargvnormalizedstderr(stderr: str) str = {
let prefix: str = "/tmp/ww_run_";
let begin: i32 = pos(stderr, prefix);
runargvrequire(begin >= 0, "run argv: missing private diagnostic path");
let digits: i32 = begin + prefix.len;
let end: i32 = digits;
for (end < stderr.len && stderr[end] >= '0' && stderr[end] <= '9') {
end += 1;
};
runargvrequire(end > digits,
"run argv: malformed private diagnostic path");
return strings.concat(strings.sub(stderr, 0, begin), "$RUN",
strings.sub(stderr, end, stderr.len));
};
@test fn run_post_target_arguments_are_program_argv() void = {
let root: str = fresh();
let source: str = strings.concat(root, "/source");
let command: str = strings.concat(source, "/cmd/argv");
let library: str = strings.concat(source, "/lib/argv");
let tests: str = strings.concat(source, "/proof/tests");
mkdirall(command); mkdirall(library); mkdirall(tests);
let named: str = strings.concat(source, "/named.ww");
let program: str = strings.concat(
"package main;\nimport os;\nimport strings;\n",
"fn main() i32 = {\n",
" let av: []str = os.args(); let i: i32 = 1;\n",
" if (av.len > 1) {\n",
" let fd: i32 = os.open(av[1], os.flag.WRONLY | os.flag.CREATE |\n",
" os.flag.TRUNC, 384i32); if (fd < 0) { return 96; };\n",
" os.write(fd, av[0].ptr, av[0].len: u64); os.close(fd);\n",
" };\n",
" for (i < av.len) {\n",
" os.write(os.STDOUT_FILENO, \"<\".ptr, 1u64);\n",
" os.write(os.STDOUT_FILENO, av[i].ptr, av[i].len: u64);\n",
" os.write(os.STDOUT_FILENO, \">\\n\".ptr, 2u64); i += 1;\n",
" };\n",
" if (av.len == 3 && strings.compare(av[2], \"exit-zero\") == 0) {\n",
" return 0;\n",
" }; return 47;\n};\n");
writefile(named, program);
writefile(strings.concat(command, "/main.ww"), program);
writefile(strings.concat(library, "/library.ww"),
"package argvlib;\nexport fn value() i32 = { return 1; };\n");
writefile(strings.concat(tests, "/tests.ww"),
"package argvtests;\nfn value() i32 = { return 1; };\n");
writefile(strings.concat(tests, "/tests_test.ww"), strings.concat(
"package argvtests;\n",
"@test fn ordinary() void = { assert(value() == 1); };\n"));
let expected: str = strings.concat(
"<-I>\n<runtime-include>\n<--I=joined-include>\n",
"<-o>\n<runtime-output>\n<--o=joined-output>\n",
"<-L>\n<runtime-library-dir>\n<-l>\n<runtime-library>\n",
"<-p>\n<ignored>\n<-->\n<later>\n<later.ww>\n<>\n<-I>\n");
let suffix: []str = ["-I", "runtime-include", "--I=joined-include",
"-o", "runtime-output", "--o=joined-output", "-L",
"runtime-library-dir", "-l", "runtime-library", "-p", "ignored",
"--", "later", "later.ww", "", "-I"];
let stages: []str = ["ww", "ww_ww"];
let tags: []str = ["c", "ww"];
let targets: []str = [named, "cmd.argv", command];
let targettags: []str = ["named", "dotted", "directory"];
let out: commandout;
let si: i32 = 0;
for (si < stages.len) {
let ti: i32 = 0;
for (ti < targets.len) {
let record: str = strings.concat(root, "/private-", tags[si],
"-", targettags[ti]);
let ignoredoutput: str = strings.concat(root, "/ignored-run-",
tags[si], "-", targettags[ti]);
let av: []str = [driver(stages[si]), "run", "-I", source,
"-o", ignoredoutput, targets[ti]];
append(av, record);
let ai: i32 = 0;
for (ai < suffix.len) { append(av, suffix[ai]); ai += 1; };
runcommand(root, strings.concat("run-argv-", tags[si], "-",
targettags[ti]), av,
(120i64 * (time.second: i64)): time.duration, &out);
runargvexpect(&out, 47,
strings.concat("<", record, ">\n", expected), "",
"run argv: target suffix result");
runargvrequire(!os.exists(ignoredoutput)
&& !os.exists(strings.concat(ignoredoutput, ".sepwork")),
"run argv: run published an output");
runargvprivateclean(record, "run argv: runtime-nonzero cleanup");
ti += 1;
};
let successrecord: str = strings.concat(root, "/private-success-",
tags[si]);
let successav: []str = [driver(stages[si]), "run", named,
successrecord, "exit-zero"];
runcommand(root, strings.concat("run-argv-success-", tags[si]),
successav, (120i64 * (time.second: i64)): time.duration, &out);
runargvexpect(&out, 0,
strings.concat("<", successrecord, ">\n<exit-zero>\n"), "",
"run argv: runtime-success result");
runargvprivateclean(successrecord, "run argv: runtime-success cleanup");
// Before an explicit target, the existing option grammar remains in
// force. No-operand run retains its implicit current-directory target.
let unknownav: []str = [driver(stages[si]), "run", "-p", named];
runcommand(root, strings.concat("run-argv-unknown-", tags[si]),
unknownav, time.second, &out);
runargvexpect(&out, 2, "", "ww run: unknown flag\n",
"run argv: pre-target unknown flag");
let missingflagav: []str = [driver(stages[si]), "run", "-I"];
runcommand(root, strings.concat("run-argv-missing-flag-", tags[si]),
missingflagav, time.second, &out);
runargvexpect(&out, 2, "", "ww run: -I needs an argument\n",
"run argv: pre-target missing option value");
let leadingstopav: []str = [driver(stages[si]), "run", "--", named];
runcommand(root, strings.concat("run-argv-leading-stop-", tags[si]),
leadingstopav, time.second, &out);
runargvexpect(&out, 2, "", "ww run: unknown flag\n",
"run argv: leading terminator control");
let defaultav: []str = [driver(stages[si]), "run"];
runcommanddir(root, strings.concat("run-argv-default-", tags[si]),
command, defaultav,
(120i64 * (time.second: i64)): time.duration, &out);
runargvexpect(&out, 47, "", "", "run argv: default-dot control");
// Target selection and loading diagnose before inert runtime bytes.
let missingtarget: []str = [driver(stages[si]), "run", "-I", source,
"missing.argv", "-p", "ignored"];
runcommand(root, strings.concat("run-argv-missing-target-", tags[si]),
missingtarget, time.second, &out);
runargvexpect(&out, 1, "",
"ww run: cannot find module missing.argv\n",
"run argv: missing target precedence");
let nonmain: []str = [driver(stages[si]), "run", "-I", source,
"lib.argv", "--", "later.ww"];
runcommand(root, strings.concat("run-argv-non-main-", tags[si]),
nonmain, time.second, &out);
runargvexpect(&out, 1, "",
"ww: package lib.argv is not a main package\n",
"run argv: non-main target precedence");
si += 1;
};
// A selected main that reaches the compiler still diagnoses before any
// flag-like suffix can be interpreted or any runtime side effect can occur.
let broken: str = strings.concat(source, "/broken.ww");
let brokenmarker: str = strings.concat(root, "/broken-ran");
writefile(broken, strings.concat(
"package main;\nimport os;\nfn main() i32 = {\n",
" let fd: i32 = os.open(\"", brokenmarker, "\", os.flag.WRONLY |\n",
" os.flag.CREATE | os.flag.TRUNC, 384i32);\n",
" if (fd >= 0) { os.close(fd); }; return missing;\n};\n"));
let producerstderr: str = "";
let producerwant: str = strings.concat(
"$RUN/main.sepwork/__root.unit.ww:7:41: error: undefined: missing\n",
"ww: w6c failed for (root)\n");
si = 0;
for (si < stages.len) {
let brokenav: []str = [driver(stages[si]), "run", broken,
"-p", "ignored", "--", brokenmarker];
runcommand(root, strings.concat("run-argv-producer-", tags[si]),
brokenav, (120i64 * (time.second: i64)): time.duration, &out);
runargvrequire(out.termination == exec.termination.EXIT
&& out.code == 1 && out.stdout.len == 0
&& occurrences(out.stderr, "undefined: missing") == 1
&& occurrences(out.stderr, "ww: w6c failed for (root)") == 1
&& pos(out.stderr, "undefined: missing")
< pos(out.stderr, "ww: w6c failed for (root)")
&& !has(out.stderr, "unknown flag") && !os.exists(brokenmarker),
"run argv: producer failure precedence");
let normalized: str = runargvnormalizedstderr(out.stderr);
runargvrequire(same(normalized, producerwant),
"run argv: exact producer diagnostic");
if (si == 0) { producerstderr = strings.dup(normalized); }
else {
runargvrequire(same(producerstderr, normalized),
"run argv: producer diagnostic stage parity");
};
si += 1;
};
// Runtime suffixes are request-local even while the two fronts execute
// concurrently and share their source closure.
let crecord: str = strings.concat(root, "/private-parallel-c");
let wrecord: str = strings.concat(root, "/private-parallel-ww");
let cav: []str = [driver("ww"), "run", named, crecord,
"c-only", "", "-I"];
let wav: []str = [driver("ww_ww"), "run", "-I", source, command,
wrecord, "ww-only", "--", "tail.ww"];
let cc: exec.command;
cc.path = cav[0]; cc.argv = cav; cc.env = os.getenvs(); cc.dir = repo();
cc.stdoutpath = strings.concat(root, "/run-argv-parallel-c.stdout");
cc.stderrpath = strings.concat(root, "/run-argv-parallel-c.stderr");
cc.deadline = time.add(time.now(time.clock.monotonic),
(120i64 * (time.second: i64)): time.duration);
cc.grace = (100i64 * (time.millisecond: i64)): time.duration;
let wc: exec.command;
wc.path = wav[0]; wc.argv = wav; wc.env = os.getenvs(); wc.dir = repo();
wc.stdoutpath = strings.concat(root, "/run-argv-parallel-ww.stdout");
wc.stderrpath = strings.concat(root, "/run-argv-parallel-ww.stderr");
wc.deadline = time.add(time.now(time.clock.monotonic),
(120i64 * (time.second: i64)): time.duration);
wc.grace = (100i64 * (time.millisecond: i64)): time.duration;
let cp: exec.process;
let wp: exec.process;
exec.start(&cp, &cc); exec.start(&wp, &wc);
let cdone: bool = false;
let wdone: bool = false;
for (!cdone || !wdone) {
if (!cdone) { cdone = exec.poll(&cp); };
if (!wdone) { wdone = exec.poll(&wp); };
if (!cdone || !wdone) {
time.sleep(time.millisecond, time.clock.monotonic);
};
};
runargvrequire(cp.result.errno == 0 && cp.result.cleanuperrno == 0
&& cp.result.termination == exec.termination.EXIT && cp.result.code == 47
&& wp.result.errno == 0 && wp.result.cleanuperrno == 0
&& wp.result.termination == exec.termination.EXIT && wp.result.code == 47
&& same(readfile(cc.stdoutpath), strings.concat("<", crecord,
">\n<c-only>\n<>\n<-I>\n"))
&& readfile(cc.stderrpath).len == 0
&& same(readfile(wc.stdoutpath), strings.concat("<", wrecord,
">\n<ww-only>\n<-->\n<tail.ww>\n"))
&& readfile(wc.stderrpath).len == 0,
"run argv: concurrent isolation");
runargvprivateclean(crecord, "run argv: parallel C cleanup");
runargvprivateclean(wrecord, "run argv: parallel WW cleanup");
// Run argv never changes build/test products or persistent action bytes.
let cbuildwork: str = strings.concat(root, "/build-work-c");
let wbuildwork: str = strings.concat(root, "/build-work-ww");
let cbuildout: str = strings.concat(root, "/build-output-c");
let wbuildout: str = strings.concat(root, "/build-output-ww");
mkdirall(cbuildwork); mkdirall(wbuildwork);
let cbuildav: []str = [driver("ww"), "build", "-w", cbuildwork,
"-o", cbuildout, named];
let wbuildav: []str = [driver("ww_ww"), "build", "-w", wbuildwork,
"-o", wbuildout, named];
runcommand(root, "run-argv-build-c", cbuildav,
(120i64 * (time.second: i64)): time.duration, &out);
runargvexpect(&out, 0, "", "", "run argv: C build control");
runcommand(root, "run-argv-build-ww", wbuildav,
(120i64 * (time.second: i64)): time.duration, &out);
runargvexpect(&out, 0, "", "", "run argv: WW build control");
runargvrequire(same(readfile(cbuildout), readfile(wbuildout))
&& same(wrongsuffixsemantictreesnapshot(cbuildwork),
wrongsuffixsemantictreesnapshot(wbuildwork)),
"run argv: build artifact parity");
let ctestwork: str = strings.concat(root, "/test-work-c");
let wtestwork: str = strings.concat(root, "/test-work-ww");
let ctestout: str = strings.concat(root, "/test-output-c");
let wtestout: str = strings.concat(root, "/test-output-ww");
mkdirall(ctestwork); mkdirall(wtestwork);
let ctestav: []str = [driver("ww"), "test", "-c", "-w", ctestwork,
"-I", source, "-o", ctestout, "proof.tests"];
let wtestav: []str = [driver("ww_ww"), "test", "-c", "-w", wtestwork,
"-I", source, "-o", wtestout, "proof.tests"];
runcommand(root, "run-argv-test-c", ctestav,
(120i64 * (time.second: i64)): time.duration, &out);
runargvexpect(&out, 0, "", "", "run argv: C test control");
runcommand(root, "run-argv-test-ww", wtestav,
(120i64 * (time.second: i64)): time.duration, &out);
runargvexpect(&out, 0, "", "", "run argv: WW test control");
runargvrequire(same(readfile(ctestout), readfile(wtestout))
&& same(wrongsuffixsemantictreesnapshot(ctestwork),
wrongsuffixsemantictreesnapshot(wtestwork)),
"run argv: test artifact parity");
runargvrequire(!wrongsuffixpathfragment(root, ".new")
&& !wrongsuffixpathfragment(root, ".old")
&& !wrongsuffixpathfragment(root, ".wwtxn.")
&& !wrongsuffixpathfragment(root, ".install")
&& !wrongsuffixpathfragment(root, ".sepwork")
&& !wrongsuffixpathfragment(root, ".stage")
&& !wrongsuffixpathfragment(root, ".capture")
&& !wrongsuffixpathfragment(root, ".result")
&& !wrongsuffixpathfragment(root, ".request")
&& !wrongsuffixpathfragment(root, ".descriptor")
&& !wrongsuffixpathfragment(root, ".child"),
"run argv: normal residue");
clean(root);
};