test/wcc: carrier ownership repair and driver-contract adaptation

Every surviving carrier now owns its artifacts: checked mkdir/mkdtemp/
fopen acquisition, one all-exit cleanup funnel per carrier, ENOENT-
tolerant checked unlinks, exact-path deletion (rm -rf only for an
owned pid-keyed dir or a .sepwork beneath one), and cleanup failure
fails a passing carrier without overwriting its diagnostic. In the
same pass the carriers adapt to the driver contract this branch lands:
--sep and WW_PKGCACHE are gone, -S and the /tmp/ww_run_<pid> scratch
contract are asserted, and rows whose runtime or reject coverage moved
to test/wcc/data fixtures or test/lang @test owners are trimmed to the
byte/artifact/diagnostic observations only they can make.

Repair and adaptation ride together because most files interleave both
in the same hunks; splitting would manufacture intermediate carrier
states that never existed and cannot run against either driver.
This commit is contained in:
2026-08-07 23:02:47 +09:00
parent b2899dd8d3
commit a95a7a316b
132 changed files with 7573 additions and 6172 deletions

View File

@@ -1,12 +1,13 @@
/*
* 989_sepbuild_run — M3-tail commit-3 `ww build --sep` driver gate (#46).
* 989_sepbuild_run — M3-tail commit-3 `ww build` driver gate (#46).
*
* Certifies build_one_sep (cmd/ww/main.c + selfhost/cmd/ww/main.ww twin)
* END-TO-END through the REAL `ww` / `ww_ww` drivers on a REAL lib chain
* (NOT the 989_m3sep synth fixture, which exercises the M3-CORE codegen
* via a hand harness). The driver adds ONLY orchestration around that
* proven core: discover_deps, reverse-topo, the transitive `w6c -c -I`
* producer loop, and a flat `w6l`.
* producer loop, and `w6l` over the root `.o` plus reverse-topo dependency
* `.a` archives.
*
* Chosen chain (smallest real multi-package graph with a genuine
* multi-level dep edge): root → os → { rt, time }. rt and time are
@@ -19,13 +20,15 @@
* - reverse-topo dep-first ordering (time/rt before os before root),
* - the transitive-closure `.wwi` prepend tagged by dotted path,
* - one-pass `w6c -c -I` (consumer + producer) per package,
* - flat `w6l` of the `.o` set + libwwrt.a.
* - `w6l` of the root `.o`, dependency `.a` set, and libwwrt.a.
*
* Asserts (all COLD — `<stem>.sepwork` scratch is wiped each run):
* Asserts (fresh output stems under an invocation-owned root; retained
* `<stem>.sepwork` is inspected, then removed by final carrier cleanup):
* 1. Build + run, BOTH stages → exit EXPECT_EXIT (cross-boundary
* os.getpid resolves + links across sep `.o`s; the program runs).
* os.getpid resolves across the root `.o` and dependency `.a` boundary;
* the program runs).
* 2. cs==ww (rule 10): per-package `.s`/`.wwi`/`.unit.ww` AND the final
* binary are byte-identical between `ww --sep` and `ww_ww --sep`.
* binary are byte-identical between `ww` and `ww_ww`.
* 3. KEYSTONE (load-bearing, §3.1) THROUGH the driver: for each non-leaf
* package P, transform the driver's own `<P>.unit.ww` by substituting
* each dep's `.wwi` section with that dep's full directory BODIES,
@@ -33,8 +36,7 @@
* the `.wwi` conveys exactly the dep facts P's codegen needs. (Leaves
* rt/time have no deps → keystone is vacuous; checked on os + root.)
*
* Light wwstage-driver test (CLAUDE.md rule 14): all intermediates are
* `-o`-redirected to /tmp, so it is phase-1 parallel-safe. Models
* All intermediates are `-o`-redirected to /tmp for isolation. Models
* 989_m3sep_run.c conventions; 989 prefix per the m3sep/m2wwi precedent.
*/
#include <stdio.h>
@@ -44,6 +46,7 @@
#include <sys/wait.h>
#include <sys/stat.h>
#include <dirent.h>
#include <errno.h>
#define EXPECT_EXIT 7
@@ -232,22 +235,10 @@ main(void)
snprintf(libdir, sizeof libdir, "%s/../../lib", bin);
snprintf(td, sizeof td, "/tmp/wwsep_%d", getpid());
snprintf(cmd, sizeof cmd, "rm -rf %s", td);
runwait(cmd);
mkdir(td, 0755);
/* This test byte-id compares the per-package INTERMEDIATES (.s, .unit.ww)
* between the cs and ww legs, which a cache HIT legitimately skips
* producing (a HIT copies only .wwi/.o and bypasses compose+w6c). So the
* compare requires every package to COLD-compile: pin a private per-pid
* cache to force misses. This is orthogonal to #104 — the atomic temp+
* rename closed the torn-read race (by construction; #105 is the deferred
* white-box guard). Concurrent shared-cache build correctness is smoked by
* 989_pkgcache_concurrent_run. Here the pin is for intermediate-compare
* isolation, not a torn-read mask. */
char cachedir[80];
snprintf(cachedir, sizeof cachedir, "%s/pkgcache", td);
setenv("WW_PKGCACHE", cachedir, 1);
if (mkdir(td, 0755) != 0) {
fprintf(stderr, "sepbuild FAIL: cannot acquire %s\n", td);
return 1;
}
char rootww[1024];
snprintf(rootww, sizeof rootww, "%s/root.ww", td);
@@ -263,10 +254,10 @@ main(void)
for (int s = 0; s < 2; s++) {
snprintf(stg[s].prog, sizeof stg[s].prog, "%s/prog.%s", td, stg[s].tag);
snprintf(cmd, sizeof cmd,
"timeout 240 %s/%s build --sep -o %s %s >/dev/null 2>&1",
"timeout 240 %s/%s build -o %s %s >/dev/null 2>&1",
bin, stg[s].drv, stg[s].prog, rootww);
if (runwait(cmd) != 0) {
fprintf(stderr, "sepbuild FAIL: %s build --sep\n", stg[s].drv);
fprintf(stderr, "sepbuild FAIL: %s build\n", stg[s].drv);
fail++;
continue;
}
@@ -352,21 +343,19 @@ main(void)
}
}
/* run --sep symmetry: E3-C1 makes sep the sole compile path, so
* `run` now genuinely sep-compiles-then-runs (the old "no sep run
* path → loud-reject exit 2" is gone with the amalgamator). BOTH
* stages must build+run the cross-boundary fixture identically
* (rule 10) and exit EXPECT_EXIT — `--sep` is an accepted no-op. */
/* `run` must compile and execute this cross-package graph through the
* sole separate-compilation path. Both stages must agree and return
* EXPECT_EXIT. */
{
int rc_cs, rc_ww;
snprintf(cmd, sizeof cmd,
"%s/ww run --sep %s >/dev/null 2>&1", bin, rootww);
"%s/ww run %s >/dev/null 2>&1", bin, rootww);
rc_cs = runwait(cmd);
snprintf(cmd, sizeof cmd,
"%s/ww_ww run --sep %s >/dev/null 2>&1", bin, rootww);
"%s/ww_ww run %s >/dev/null 2>&1", bin, rootww);
rc_ww = runwait(cmd);
if (rc_cs != rc_ww || rc_cs != EXPECT_EXIT) {
fprintf(stderr, "sepbuild FAIL: run --sep cs=%d ww=%d "
fprintf(stderr, "sepbuild FAIL: run cs=%d ww=%d "
"(both must run via sep, exit %d)\n", rc_cs, rc_ww,
EXPECT_EXIT);
fail++;
@@ -374,8 +363,33 @@ main(void)
}
out:
snprintf(cmd, sizeof cmd, "rm -rf %s", td);
runwait(cmd);
snprintf(cmd, sizeof cmd,
"rm -rf -- '%s/prog.cs.sepwork' '%s/prog.ww.sepwork'", td, td);
if (runwait(cmd) != 0) {
fprintf(stderr, "sepbuild FAIL: cannot clean .sepwork trees\n");
fail++;
}
{
char path[1200];
snprintf(path, sizeof path, "%s/root.ww", td);
if (unlink(path) != 0 && errno != ENOENT) { fprintf(stderr, "sepbuild FAIL: cannot remove %s\n", path); fail++; }
snprintf(path, sizeof path, "%s/prog.cs", td);
if (unlink(path) != 0 && errno != ENOENT) { fprintf(stderr, "sepbuild FAIL: cannot remove %s\n", path); fail++; }
snprintf(path, sizeof path, "%s/prog.ww", td);
if (unlink(path) != 0 && errno != ENOENT) { fprintf(stderr, "sepbuild FAIL: cannot remove %s\n", path); fail++; }
snprintf(path, sizeof path, "%s/os.bodies.ww", td);
if (unlink(path) != 0 && errno != ENOENT) { fprintf(stderr, "sepbuild FAIL: cannot remove %s\n", path); fail++; }
snprintf(path, sizeof path, "%s/os.bodies.s", td);
if (unlink(path) != 0 && errno != ENOENT) { fprintf(stderr, "sepbuild FAIL: cannot remove %s\n", path); fail++; }
snprintf(path, sizeof path, "%s/__root.bodies.ww", td);
if (unlink(path) != 0 && errno != ENOENT) { fprintf(stderr, "sepbuild FAIL: cannot remove %s\n", path); fail++; }
snprintf(path, sizeof path, "%s/__root.bodies.s", td);
if (unlink(path) != 0 && errno != ENOENT) { fprintf(stderr, "sepbuild FAIL: cannot remove %s\n", path); fail++; }
}
if (rmdir(td) != 0 && errno != ENOENT) {
fprintf(stderr, "sepbuild FAIL: cannot remove %s\n", td);
fail++;
}
if (fail) {
fprintf(stderr, "sepbuild: %d check(s) failed\n", fail);
return 1;