/* * 989_sepdotpath_run — sep-build of a DOTTED-path (multi-component) package * (#57). Commit-6 broad-soak prereq: the real toolchain sep-builds packages * like `encoding.utf8`, whose body must mangle on the full dotted import path * (`encoding.utf8.X`), not the leaf `package` clause (`utf8.X`). * * Root cause it guards: the sep producer splices a package's own body under * `//ww:module-reset` (so `-c` keeps imported==0). Pre-#57 that reset carried * NO path, so the body's `package ;` clause set curmod to the LEAF — * the DEFINER mangled `b.val` while every importer (spliced under * `//ww:module a.b`) referenced `a.b.val` → unresolved at link. #57 threads * the dotted path through the reset (`//ww:module-reset a.b`), demoting the * leaf clause to an assertion. Single-COMPONENT packages were always clean * (leaf == dotted path) — that invariant is the regression guard below. * * Graph (smallest that mixes both kinds): root -> { a.b (dotted), c (single) }. * * Asserts (all COLD — `.sepwork` scratch is wiped each run): * 1. Build + run, BOTH stages → exit EXPECT_EXIT (the dotted-package symbol * resolves + links; the program runs). Pre-#57 the link failed. * 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. NON-VACUITY (a) — definer==importer on the DOTTED form: the producer's * `a.b.s` DEFINES `a.b.val` and the root's `__root.s` REFERENCES * `a.b.val`. Pre-#57 the definer emitted the leaf `b.val` → the strings * mismatched; this row would fail (and assert 1's link would fail). * The reset directive in `a.b.unit.ww` carries the dotted path. * 4. NON-VACUITY (b) — single-component regression guard: package `c` * sep-builds in the SAME graph, defines `c.cval` (leaf == dotted), and * cs==ww (covered by assert 2). Cross-commit byte-id of single-component * output is additionally pinned by 989_sepbuild_run + the 990-997 gates. * * Light wwstage-driver test (CLAUDE.md rule 14): all intermediates are * `-o`-redirected to /tmp, so it is phase-1 parallel-safe. Models * 989_sepbuild_run.c conventions; 989 prefix per the sep-gate precedent. */ #include #include #include #include #include #include #define EXPECT_EXIT 37 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; } /* 0 if `needle` occurs in the file at `path`, 1 if absent, -1 on read err. */ static int file_contains(const char *path, const char *needle) { char *b = NULL; size_t n = 0; if (slurp(path, &b, &n) < 0) return -1; int found = (strstr(b, needle) != NULL); free(b); return found ? 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; } int main(void) { const char *bin = absbin(); if (!bin) return 1; char td[64], cmd[8192], p[1024]; int fail = 0; snprintf(td, sizeof td, "/tmp/wwsepdot_%d", getpid()); snprintf(cmd, sizeof cmd, "rm -rf %s", td); runwait(cmd); mkdir(td, 0755); /* lib/a/b — a 2-level dotted package (clause `package b;`, dir a/b). */ snprintf(p, sizeof p, "%s/lib", td); mkdir(p, 0755); snprintf(p, sizeof p, "%s/lib/a", td); mkdir(p, 0755); snprintf(p, sizeof p, "%s/lib/a/b", td); mkdir(p, 0755); snprintf(p, sizeof p, "%s/lib/c", td); mkdir(p, 0755); snprintf(p, sizeof p, "%s/lib/a/b/mod.ww", td); if (write_file(p, "package b;\nexport fn val() i32 = { return 42; };\n")) { fail++; goto out; } snprintf(p, sizeof p, "%s/lib/c/mod.ww", td); if (write_file(p, "package c;\nexport fn cval() i32 = { return 5; };\n")) { fail++; goto out; } char rootww[1024]; snprintf(rootww, sizeof rootww, "%s/root.ww", td); if (write_file(rootww, "package main;\n" "import a.b;\n" "import c;\n" "fn main() i32 = { return b.val() - c.cval(); };\n")) { fail++; goto out; } /* Two driver stages and their scratch dirs (paths rebuilt from the * small fixed `td` + stage tag → provably non-truncating snprintfs). */ 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); /* Per-stage fresh WW_PKGCACHE → every package compiles COLD, so the * `.s`/`.unit.ww` this gate inspects are always produced (a warm * shared out/.pkgcache hit would skip them). Mirrors 989_pkgcache. */ snprintf(cmd, sizeof cmd, "WW_PKGCACHE='%s/cache.%s' timeout 240 %s/%s build --sep " "-I %s/lib -o %s %s >/dev/null 2>&1", td, stg[s].tag, bin, stg[s].drv, td, stg[s].prog, rootww); if (runwait(cmd) != 0) { fprintf(stderr, "sepdotpath FAIL: %s build --sep\n", stg[s].drv); fail++; continue; } int rc = runwait(stg[s].prog); if (rc != EXPECT_EXIT) { fprintf(stderr, "sepdotpath FAIL: %s prog exit=%d expected %d\n", stg[s].drv, rc, EXPECT_EXIT); fail++; } } /* The discovered package set materialized (dotted + single + root). */ const char *pkgs[] = { "a.b", "c", "__root" }; for (int i = 0; i < 3; i++) { /* #69: the root is compiled WITHOUT `-I`, so it produces no `.wwi`; * assert its `.s` materialized instead. Deps 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, "sepdotpath FAIL: missing %s%s (discovery)\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 < 3; 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, "sepdotpath 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, "sepdotpath FAIL: cs exe != ww exe (rule 10)\n"); fail++; } /* NON-VACUITY (a): definer == importer on the DOTTED qualification. * Pre-#57 the definer emitted the leaf `b.val` while the importer * referenced `a.b.val` → mismatch + unresolved link. */ { char def_s[1024], ref_s[1024], unit[1024]; snprintf(def_s, sizeof def_s, "%s/prog.cs.sepwork/a.b.s", td); snprintf(ref_s, sizeof ref_s, "%s/prog.cs.sepwork/__root.s", td); snprintf(unit, sizeof unit, "%s/prog.cs.sepwork/a.b.unit.ww", td); if (file_contains(def_s, "TEXT a.b.val") != 0) { fprintf(stderr, "sepdotpath FAIL: definer a.b.s lacks dotted " "`TEXT a.b.val` (leaf-clause regression)\n"); fail++; } if (file_contains(ref_s, "a.b.val") != 0) { fprintf(stderr, "sepdotpath FAIL: importer __root.s lacks " "reference to a.b.val\n"); fail++; } if (file_contains(unit, "//ww:module-reset a.b") != 0) { fprintf(stderr, "sepdotpath FAIL: a.b.unit.ww reset directive " "does not carry the dotted path\n"); fail++; } } /* NON-VACUITY (b): single-component `c` sep-builds in the SAME graph and * defines its leaf==dotted symbol `c.cval` (curmod resolves to the same * string with or without #57 → byte-id preserved). cs==ww already * asserted above; cross-commit byte-id pinned by 989_sepbuild + 990-997. */ { char c_s[1024]; snprintf(c_s, sizeof c_s, "%s/prog.cs.sepwork/c.s", td); if (file_contains(c_s, "TEXT c.cval") != 0) { fprintf(stderr, "sepdotpath FAIL: single-component c.s lacks " "`TEXT c.cval` (regression)\n"); fail++; } } out: snprintf(cmd, sizeof cmd, "rm -rf %s", td); runwait(cmd); if (fail) { fprintf(stderr, "sepdotpath: %d check(s) failed\n", fail); return 1; } printf("sepdotpath: dotted `a.b` + single `c` via build_one_sep — " "build+run (exit %d) + cs==ww per-pkg .s/.wwi/.unit + final binary " "+ definer==importer on a.b.val + single-component c.cval intact\n", EXPECT_EXIT); return 0; }