diff --git a/Makefile b/Makefile index 6d1ab2e8..1eab979a 100644 --- a/Makefile +++ b/Makefile @@ -565,6 +565,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_m3sep_run \ $(BIN)/test_sepbuild_run \ $(BIN)/test_sepdotpath_run \ + $(BIN)/test_seproot_export_run \ $(BIN)/test_sepcycle_dup \ $(BIN)/test_separchive_run \ $(BIN)/test_pkgcache_run \ @@ -3122,6 +3123,17 @@ $(BIN)/test_sepdotpath_run: test/wcc/989_sepdotpath_run.c $(BIN)/ww $(BIN)/ww_ww $(BIN)/w6l $(BIN)/w6l_ww $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +# 989_seproot_export_run — sep-build of a ROOT whose `export fn` references an +# unexported LOCAL type (#69, BUG-1): the producer must NOT pass `-I` for the +# root, since `-I` triggers check_exported_type, which rejects that legal +# terminal-binary pattern. Drives BOTH driver stages + replays the forced +# `-I` reject directly on the produced root unit (both compilers), so it needs +# both driver + both compiler + both linker stages + libwwrt for the link. +$(BIN)/test_seproot_export_run: test/wcc/989_seproot_export_run.c $(BIN)/ww $(BIN)/ww_ww \ + $(BIN)/w6c $(BIN)/w6c_ww $(BIN)/w6a $(BIN)/w6a_ww \ + $(BIN)/w6l $(BIN)/w6l_ww $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + # 989_sepcycle_dup — M3-tail commit-4 negative gates (#46, #31): a loud # dep-cycle reject (both driver stages, cs==ww stderr) + the #31 # duplicate-symbol reject (both linker stages). Needs both driver stages, diff --git a/cmd/ww/main.c b/cmd/ww/main.c index 48c40fe7..70a07c96 100644 --- a/cmd/ww/main.c +++ b/cmd/ww/main.c @@ -1225,8 +1225,19 @@ build_one_sep(const char *src, int entry_is_dir, const char *out, if (!fresh) { if (sep_compose_unit(g, pi, scratch, order, norder, srcdir, unitf) < 0) { free(order); free(g); return 1; } - snprintf(cmd, sizeof cmd, "%s -c -I %s -o %s %s", - c6, wwi, asmf, unitf); + /* BUG-1 (#69): -I is purely the root's UNUSED + * `.wwi` output path, but it triggers wwi_emit → + * check_exported_type on the root. A terminal binary's + * root legitimately has `export fn` over an unexported + * LOCAL type (the root is never imported), which the + * export-check rejects. Skip -I for the root; its `.wwi` + * is never consumed. */ + if (pi == root) + snprintf(cmd, sizeof cmd, "%s -c -o %s %s", + c6, asmf, unitf); + else + snprintf(cmd, sizeof cmd, "%s -c -I %s -o %s %s", + c6, wwi, asmf, unitf); if (run(cmd) != 0) { fprintf(stderr, "ww --sep: w6c failed for %s\n", g->pkg[pi].path[0] ? g->pkg[pi].path : "(root)"); diff --git a/selfhost/cmd/ww/main.combined.ww b/selfhost/cmd/ww/main.combined.ww index f371449c..db830671 100644 --- a/selfhost/cmd/ww/main.combined.ww +++ b/selfhost/cmd/ww/main.combined.ww @@ -4510,16 +4510,28 @@ fn buildonesep(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8, return 1; }; { - let argv: []*u8 = alloc([], 8u64)!; - argv.len = 8; + // BUG-1 (#69): -I is purely the root's UNUSED + // `.wwi` output path, but it triggers wwiemit -> + // checkexportedtype on the root. A terminal binary's + // root legitimately has `export fn` over an unexported + // LOCAL type (the root is never imported), which the + // export-check rejects. Build a shorter root argv + // without the -I/wwi pair; root's `.wwi` is unconsumed. + let alen: u64 = 8u64; + if (pi == root) { alen = 6u64; }; + let argv: []*u8 = alloc([], alen)!; + argv.len = (alen: i32); argv[0] = "w6c\0".ptr; argv[1] = "-c\0".ptr; - argv[2] = "-I\0".ptr; - argv[3] = wwi; - argv[4] = "-o\0".ptr; - argv[5] = asmf; - argv[6] = unitf; - argv[7] = nil; + let k: u64 = 2u64; + if (pi != root) { + argv[k] = "-I\0".ptr; k += 1u64; + argv[k] = wwi; k += 1u64; + }; + argv[k] = "-o\0".ptr; k += 1u64; + argv[k] = asmf; k += 1u64; + argv[k] = unitf; k += 1u64; + argv[k] = nil; if (procrun(c6, argv.ptr) != 0) { cerr("ww --sep: w6c failed\n"); return 1; diff --git a/selfhost/cmd/ww/main.ww b/selfhost/cmd/ww/main.ww index 541cccc0..0934a3e5 100644 --- a/selfhost/cmd/ww/main.ww +++ b/selfhost/cmd/ww/main.ww @@ -1636,16 +1636,28 @@ fn buildonesep(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8, return 1; }; { - let argv: []*u8 = alloc([], 8u64)!; - argv.len = 8; + // BUG-1 (#69): -I is purely the root's UNUSED + // `.wwi` output path, but it triggers wwiemit -> + // checkexportedtype on the root. A terminal binary's + // root legitimately has `export fn` over an unexported + // LOCAL type (the root is never imported), which the + // export-check rejects. Build a shorter root argv + // without the -I/wwi pair; root's `.wwi` is unconsumed. + let alen: u64 = 8u64; + if (pi == root) { alen = 6u64; }; + let argv: []*u8 = alloc([], alen)!; + argv.len = (alen: i32); argv[0] = "w6c\0".ptr; argv[1] = "-c\0".ptr; - argv[2] = "-I\0".ptr; - argv[3] = wwi; - argv[4] = "-o\0".ptr; - argv[5] = asmf; - argv[6] = unitf; - argv[7] = nil; + let k: u64 = 2u64; + if (pi != root) { + argv[k] = "-I\0".ptr; k += 1u64; + argv[k] = wwi; k += 1u64; + }; + argv[k] = "-o\0".ptr; k += 1u64; + argv[k] = asmf; k += 1u64; + argv[k] = unitf; k += 1u64; + argv[k] = nil; if (procrun(c6, argv.ptr) != 0) { cerr("ww --sep: w6c failed\n"); return 1; diff --git a/test/wcc/989_sepbuild_run.c b/test/wcc/989_sepbuild_run.c index 065056bb..eddc98ae 100644 --- a/test/wcc/989_sepbuild_run.c +++ b/test/wcc/989_sepbuild_run.c @@ -271,18 +271,26 @@ main(void) const char *pkgs[] = { "time", "rt", "os", "__root" }; for (int i = 0; i < 4; i++) { char p[1024]; - snprintf(p, sizeof p, "%s/prog.cs.sepwork/%s.wwi", td, pkgs[i]); + /* #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.wwi (discovery/topo)\n", - pkgs[i]); + 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. */ + /* 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]); diff --git a/test/wcc/989_sepdotpath_run.c b/test/wcc/989_sepdotpath_run.c index 114e7f58..dd8b7d24 100644 --- a/test/wcc/989_sepdotpath_run.c +++ b/test/wcc/989_sepdotpath_run.c @@ -185,18 +185,25 @@ main(void) /* The discovered package set materialized (dotted + single + root). */ const char *pkgs[] = { "a.b", "c", "__root" }; for (int i = 0; i < 3; i++) { - snprintf(p, sizeof p, "%s/prog.cs.sepwork/%s.wwi", td, pkgs[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.wwi (discovery)\n", - pkgs[i]); + 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. */ + /* 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]); diff --git a/test/wcc/989_seproot_export_run.c b/test/wcc/989_seproot_export_run.c new file mode 100644 index 00000000..665c6c27 --- /dev/null +++ b/test/wcc/989_seproot_export_run.c @@ -0,0 +1,274 @@ +/* + * 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; +}