/* * 989_seproot_export_run — sep-build of a ROOT unit whose `export fn` * references an unexported LOCAL type (#69, BUG-1). * * Root cause it guards: the `--sep` producer loop compiled EVERY package, * INCLUDING the root build-target, with the `.wwi`-producer `-I` flag. * `-I` triggers wwi_emit -> check_exported_type, which rejects an exported * declaration that references an unexported type. A terminal binary's root * legitimately has such a decl (`export fn use(a: *t)` over an unexported * local `type t`) — fine, because the root is never imported, so its `.wwi` * is never consumed. The combined build never passes `-I` -> builds+runs; * the sep build rejected the root -> blocked every real tool's sep-build. * Fix: for pi==root the producer invokes `w6c -c -o` WITHOUT `-I`. * * Graph (smallest reproducing shape): root -> { c (one real dep package) }. * The root exports `use(a: *t)` over the unexported local `type t`. * * Asserts (all COLD — per-stage WW_PKGCACHE wipes the package cache): * 1. Build + run, BOTH stages -> exit EXPECT_EXIT. Pre-fix the root w6c * pass exited 1 ("exported declaration references unexported type"), * so the build never produced a binary. * 2. cs==ww (rule 10): per-package `.s`/`.wwi`/`.unit.ww` for {c,__root} * AND the final binary are byte-identical between the two drivers. * 3. NON-VACUITY (forced -I reject): re-run the EXACT pre-fix root * invocation on the produced `__root.unit.ww` — `w6c -c -I -o` * MUST exit non-zero (the export-check fires on this pattern), while * `w6c -c -o` (the post-fix root invocation) MUST exit 0. Demonstrated * for BOTH compilers (w6c, w6c_ww). This is the bug, isolated. * 4. The root `.s` is produced and defines the exported fn symbol. * * Light wwstage-driver test (CLAUDE.md rule 14): all intermediates are * `-o`-redirected to /tmp, so it is phase-1 parallel-safe. Models * 989_sepdotpath_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/wwseproot_%d", getpid()); snprintf(cmd, sizeof cmd, "rm -rf %s", td); runwait(cmd); mkdir(td, 0755); /* lib/c — one real dep package the root imports. */ snprintf(p, sizeof p, "%s/lib", td); mkdir(p, 0755); snprintf(p, sizeof p, "%s/lib/c", td); mkdir(p, 0755); 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; } /* Root: `export fn use(a: *t)` over the UNEXPORTED local `type t` — * exactly the pattern `-I`'s check_exported_type rejected. */ char rootww[1024]; snprintf(rootww, sizeof rootww, "%s/root.ww", td); if (write_file(rootww, "package main;\n" "import c;\n" "type t = struct { v: i32 };\n" "export fn use(a: *t) i32 = { return a.v; };\n" "fn main() i32 = {\n" "\tlet x: t = t { v = 42 };\n" "\treturn use(&x) - c.cval();\n" "};\n")) { fail++; goto out; } 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. */ 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, "seproot FAIL: %s build --sep (pre-fix: -I on root " "rejects export-fn-over-unexported-type)\n", stg[s].drv); fail++; continue; } int rc = runwait(stg[s].prog); if (rc != EXPECT_EXIT) { fprintf(stderr, "seproot FAIL: %s prog exit=%d expected %d\n", stg[s].drv, rc, EXPECT_EXIT); fail++; } } /* cs==ww (rule 10): per-package .s/.unit.ww + final binary. The root's * `.wwi` is intentionally NOT produced (the fix), so it is excluded * here and asserted ABSENT below; the dep `c` still emits a `.wwi`. */ const char *pkgs[] = { "c", "__root" }; for (int i = 0; i < 2; 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, "seproot 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, "seproot FAIL: cs exe != ww exe (rule 10)\n"); fail++; } /* NON-VACUITY: the fix omits `-I` for the root, so the root's `.wwi` * (the producer flag's only effect) must NOT exist — BOTH stages. */ for (int s = 0; s < 2; s++) { char rw[1024]; snprintf(rw, sizeof rw, "%s/prog.%s.sepwork/__root.wwi", td, stg[s].tag); if (access(rw, 0) == 0) { fprintf(stderr, "seproot FAIL: %s produced __root.wwi (-I still " "passed for the root)\n", stg[s].drv); fail++; } } /* NON-VACUITY: replay the EXACT pre-fix root invocation on the produced * `__root.unit.ww`. With `-I` (the .wwi producer) the export-check fires * and w6c exits non-zero; without `-I` (the post-fix invocation) it * exits 0. Proven for BOTH compilers. */ { char unit[1024], wwi[1024], asmf[1024]; snprintf(unit, sizeof unit, "%s/prog.cs.sepwork/__root.unit.ww", td); struct { const char *comp; } cc[] = { { "w6c" }, { "w6c_ww" } }; for (int j = 0; j < 2; j++) { snprintf(wwi, sizeof wwi, "%s/nv.%s.wwi", td, cc[j].comp); snprintf(asmf, sizeof asmf, "%s/nv.%s.s", td, cc[j].comp); snprintf(cmd, sizeof cmd, "%s/%s -c -I %s -o %s %s >/dev/null 2>&1", bin, cc[j].comp, wwi, asmf, unit); if (runwait(cmd) == 0) { fprintf(stderr, "seproot FAIL: %s -c -I accepted the root " "export-over-unexported-type (bug not reproduced -> " "vacuous gate)\n", cc[j].comp); fail++; } snprintf(cmd, sizeof cmd, "%s/%s -c -o %s %s >/dev/null 2>&1", bin, cc[j].comp, asmf, unit); if (runwait(cmd) != 0) { fprintf(stderr, "seproot FAIL: %s -c -o (no -I) rejected the " "root unit (post-fix invocation must succeed)\n", cc[j].comp); fail++; } } } /* The root `.s` is produced and defines the exported fn. */ { char rs[1024]; snprintf(rs, sizeof rs, "%s/prog.cs.sepwork/__root.s", td); if (file_contains(rs, "use") != 0) { fprintf(stderr, "seproot FAIL: __root.s lacks the exported `use` " "symbol (root compile produced no code)\n"); fail++; } } out: snprintf(cmd, sizeof cmd, "rm -rf %s", td); runwait(cmd); if (fail) { fprintf(stderr, "seproot: %d check(s) failed\n", fail); return 1; } printf("seproot: root `export fn use(a:*t)` over unexported `type t` " "via build_one_sep — build+run (exit %d) + cs==ww per-pkg + final " "binary + forced -I rejects (bug) while -o accepts (both compilers)\n", EXPECT_EXIT); return 0; }