/* * 989_sepbuild_run — M3-tail commit-3 `ww build --sep` 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`. * * Chosen chain (smallest real multi-package graph with a genuine * multi-level dep edge): root → os → { rt, time }. rt and time are * true leaves; os imports both AND its public interface exposes a * transitive type (os returns/takes time.instant in sibling protos), so * the root's sep-unit needs the TRANSITIVE `.wwi` closure for name * resolution — direct-deps-only does not type-check (the lead-ratified * superseding of rob-c3-spec §1.3). The graph thus exercises: * - discovery of the transitive package set, * - 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. * * Asserts (all COLD — `.sepwork` scratch is wiped each run): * 1. Build + run, BOTH stages → exit EXPECT_EXIT (cross-boundary * os.getpid resolves + links across sep `.o`s; 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`. * 3. KEYSTONE (load-bearing, §3.1) THROUGH the driver: for each non-leaf * package P, transform the driver's own `

.unit.ww` by substituting * each dep's `.wwi` section with that dep's full directory BODIES, * `w6c -c` it, and cmp vs the driver's `

.s`. Byte-identical proves * 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 * 989_m3sep_run.c conventions; 989 prefix per the m3sep/m2wwi precedent. */ #include #include #include #include #include #include #include #define EXPECT_EXIT 7 static int runwait(const char *cmd) { int rc = system(cmd); if (rc == -1) return -1; if (WIFEXITED(rc)) return WEXITSTATUS(rc); return 1; } static const char * absbin(void) { const char *b = getenv("BIN"); if (!b) b = "out/bin"; if (b[0] == '/') return b; static char buf[2048]; char cwd[1024]; if (getcwd(cwd, sizeof cwd) == NULL) return NULL; snprintf(buf, sizeof buf, "%s/%s", cwd, b); return buf; } static int slurp(const char *path, char **outbuf, size_t *outlen) { FILE *f = fopen(path, "rb"); if (!f) return -1; fseek(f, 0, SEEK_END); long n = ftell(f); fseek(f, 0, SEEK_SET); if (n < 0) { fclose(f); return -1; } char *b = malloc((size_t)n + 1); if (!b) { fclose(f); return -1; } if (fread(b, 1, (size_t)n, f) != (size_t)n) { free(b); fclose(f); return -1; } b[n] = '\0'; fclose(f); *outbuf = b; *outlen = (size_t)n; return 0; } static int files_eq(const char *a, const char *b) { char *ba = NULL, *bb = NULL; size_t na = 0, nb = 0; if (slurp(a, &ba, &na) < 0 || slurp(b, &bb, &nb) < 0) { free(ba); free(bb); return -1; } int eq = (na == nb && memcmp(ba, bb, na) == 0); free(ba); free(bb); return eq ? 0 : 1; } static int write_file(const char *path, const char *body) { FILE *f = fopen(path, "wb"); if (!f) return -1; fputs(body, f); fclose(f); return 0; } static int strs_cmp(const void *a, const void *b) { return strcmp(*(const char *const *)a, *(const char *const *)b); } /* Concatenate a package directory's *.ww bodies (less *test.ww and * *.combined.ww, byte-sorted) into `out`. Mirrors the driver's body * enumeration so the substituted bodies-unit matches the sep-unit * structurally. */ static int append_dir_bodies(FILE *out, const char *dir) { DIR *d = opendir(dir); if (!d) return -1; char *names[256]; int n = 0; struct dirent *ent; while ((ent = readdir(d)) != NULL && n < 256) { const char *nm = ent->d_name; size_t nl = strlen(nm); if (nl <= 3 || strcmp(nm + nl - 3, ".ww") != 0) continue; if (nl >= 7 && strcmp(nm + nl - 7, "test.ww") == 0) continue; if (nl >= 12 && strcmp(nm + nl - 12, ".combined.ww") == 0) continue; names[n++] = strdup(nm); } closedir(d); if (n > 1) qsort(names, (size_t)n, sizeof names[0], strs_cmp); int rc = 0; for (int i = 0; i < n; i++) { char fp[1024]; snprintf(fp, sizeof fp, "%s/%s", dir, names[i]); char *b = NULL; size_t bn = 0; if (slurp(fp, &b, &bn) == 0) { fwrite(b, 1, bn, out); fputc('\n', out); free(b); } else rc = -1; free(names[i]); } return rc; } /* Transform the driver's sep-unit at `unitf` into a bodies-unit at * `bodiesf`: every `//ww:module ` dep section (its `.wwi` content) * is replaced by 's full directory bodies; the trailing * `//ww:module-reset` primary body is copied verbatim. The result is the * SAME unit P would compile with deps-as-bodies — the keystone's other * arm. roots the dotted-path → dir mapping (dots → slashes). */ static int compose_bodies(const char *unitf, const char *libdir, const char *bodiesf) { char *buf = NULL; size_t n = 0; if (slurp(unitf, &buf, &n) < 0) return -1; FILE *out = fopen(bodiesf, "wb"); if (!out) { free(buf); return -1; } int rc = 0; size_t i = 0; while (i < n) { size_t j = i; while (j < n && buf[j] != '\n') j++; size_t linelen = j - i; if (strncmp(buf + i, "//ww:module-reset", 17) == 0) { /* primary body: copy this line + everything after. */ fwrite(buf + i, 1, n - i, out); break; } if (strncmp(buf + i, "//ww:module ", 12) == 0) { /* dep section: emit the directive, then dir bodies; * skip the original `.wwi` content to the next * directive. */ fwrite(buf + i, 1, linelen, out); fputc('\n', out); char path[256]; size_t pl = linelen - 12; if (pl >= sizeof path) pl = sizeof path - 1; memcpy(path, buf + i + 12, pl); path[pl] = '\0'; char dir[1024]; char form[256]; size_t k; for (k = 0; path[k]; k++) form[k] = (path[k] == '.') ? '/' : path[k]; form[k] = '\0'; snprintf(dir, sizeof dir, "%s/%s", libdir, form); if (append_dir_bodies(out, dir) < 0) rc = -1; /* advance past the wwi content to the next directive. */ i = j + 1; while (i < n) { if (strncmp(buf + i, "//ww:module", 11) == 0) break; size_t e = i; while (e < n && buf[e] != '\n') e++; i = (e < n) ? e + 1 : n; } continue; } i = j + 1; } fclose(out); free(buf); return rc; } static const char *root_src = "package main;\n" "import os;\n" "fn main() i32 = { return os.getpid() - os.getpid() + 7; };\n"; int main(void) { const char *bin = absbin(); if (!bin) return 1; char td[64], cmd[8192], libdir[2048]; int fail = 0; 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); char rootww[1024]; snprintf(rootww, sizeof rootww, "%s/root.ww", td); if (write_file(rootww, root_src)) { fail++; goto out; } /* The two driver stages and their scratch dirs. All per-package paths * are rebuilt from `td` (a small fixed buffer) + the stage tag rather * than chained through a large path buffer, so the snprintfs are * provably non-truncating (warning-clean, like 989_m3sep_run). */ struct { const char *drv, *tag; char prog[1024]; } stg[] = { { "ww", "cs", {0} }, { "ww_ww", "ww", {0} } }; 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", bin, stg[s].drv, stg[s].prog, rootww); if (runwait(cmd) != 0) { fprintf(stderr, "sepbuild FAIL: %s build --sep\n", stg[s].drv); fail++; continue; } int rc = runwait(stg[s].prog); if (rc != EXPECT_EXIT) { fprintf(stderr, "sepbuild FAIL: %s prog exit=%d expected %d\n", stg[s].drv, rc, EXPECT_EXIT); fail++; } } /* reverse-topo correctness is implicit: every package compiled (a * dep's `.wwi` existed before its importer) → build succeeded above. * Assert the discovered package set materialized in the scratch. */ const char *pkgs[] = { "time", "rt", "os", "__root" }; for (int i = 0; i < 4; i++) { char p[1024]; /* #69: the root is compiled WITHOUT `-I`, so it produces no `.wwi` * (its interface is never consumed); assert its `.s` materialized * instead. Dep packages still emit a `.wwi`. */ int is_root = (strcmp(pkgs[i], "__root") == 0); snprintf(p, sizeof p, "%s/prog.cs.sepwork/%s%s", td, pkgs[i], is_root ? ".s" : ".wwi"); if (access(p, 0) != 0) { fprintf(stderr, "sepbuild FAIL: missing %s%s (discovery/topo)\n", pkgs[i], is_root ? ".s" : ".wwi"); fail++; } } /* cs==ww (rule 10): per-package .s/.wwi/.unit.ww + final binary. The * root has no `.wwi` post-#69, so it is excluded from the .wwi compare. */ for (int i = 0; i < 4; i++) { const char *suf[] = { ".s", ".wwi", ".unit.ww" }; for (int k = 0; k < 3; k++) { if (strcmp(pkgs[i], "__root") == 0 && strcmp(suf[k], ".wwi") == 0) continue; char a[1024], b[1024]; snprintf(a, sizeof a, "%s/prog.%s.sepwork/%s%s", td, stg[0].tag, pkgs[i], suf[k]); snprintf(b, sizeof b, "%s/prog.%s.sepwork/%s%s", td, stg[1].tag, pkgs[i], suf[k]); if (files_eq(a, b) != 0) { fprintf(stderr, "sepbuild FAIL: cs!=ww for %s%s (rule 10)\n", pkgs[i], suf[k]); fail++; } } } if (files_eq(stg[0].prog, stg[1].prog) != 0) { fprintf(stderr, "sepbuild FAIL: cs exe != ww exe (rule 10)\n"); fail++; } /* KEYSTONE through the driver: bodies-unit == sep-unit codegen, for * each non-leaf package (os, root). Transform the driver's own *

.unit.ww (deps as .wwi) into a bodies-unit (deps as full dir * bodies), `w6c -c` it, cmp vs the driver's

.s. */ const char *keypkgs[] = { "os", "__root" }; for (int i = 0; i < 2; i++) { char unitf[1024], bodiesf[1024], bodies_s[1024], sep_s[1024]; snprintf(unitf, sizeof unitf, "%s/prog.cs.sepwork/%s.unit.ww", td, keypkgs[i]); snprintf(bodiesf, sizeof bodiesf, "%s/%s.bodies.ww", td, keypkgs[i]); snprintf(bodies_s, sizeof bodies_s, "%s/%s.bodies.s", td, keypkgs[i]); snprintf(sep_s, sizeof sep_s, "%s/prog.cs.sepwork/%s.s", td, keypkgs[i]); if (compose_bodies(unitf, libdir, bodiesf) < 0) { fprintf(stderr, "sepbuild FAIL: compose bodies for %s\n", keypkgs[i]); fail++; continue; } snprintf(cmd, sizeof cmd, "timeout 240 %s/w6c -c -o %s %s >/dev/null 2>&1", bin, bodies_s, bodiesf); if (runwait(cmd) != 0) { fprintf(stderr, "sepbuild FAIL: %s bodies -c\n", keypkgs[i]); fail++; continue; } if (files_eq(bodies_s, sep_s) != 0) { fprintf(stderr, "sepbuild FAIL: %s — bodies.s != driver sep.s " "(the .wwi does not convey the dep facts P needs)\n", keypkgs[i]); fail++; } } /* 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. */ { int rc_cs, rc_ww; snprintf(cmd, sizeof cmd, "%s/ww run --sep %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); rc_ww = runwait(cmd); if (rc_cs != rc_ww || rc_cs != EXPECT_EXIT) { fprintf(stderr, "sepbuild FAIL: run --sep cs=%d ww=%d " "(both must run via sep, exit %d)\n", rc_cs, rc_ww, EXPECT_EXIT); fail++; } } out: snprintf(cmd, sizeof cmd, "rm -rf %s", td); runwait(cmd); if (fail) { fprintf(stderr, "sepbuild: %d check(s) failed\n", fail); return 1; } printf("sepbuild: real chain root->os->{rt,time} via build_one_sep — " "build+run (exit %d) + cs==ww per-pkg .s/.wwi/.unit + final binary " "+ transitive-topo discovery + keystone bodies==.wwi (os,root)\n", EXPECT_EXIT); return 0; }