wcc/ww: don't interface-check the sep-build root unit (BUG-1, #69)

The --sep producer compiled the ROOT build-target with the .wwi-producer
-I flag, so check_exported_type ran on the root and rejected a real-tool
root's legitimate `export fn f(a: *t)` over an unexported local type t
(the root is terminal — its interface is never imported, and its .wwi is
never consumed). The combined build never passes -I, so it built fine.
For pi==root, invoke w6c with -c -o only, no -I. Both stages (the wwstage
twin builds the shorter root argv). Gates that asserted __root.wwi exists
encoded the buggy behavior; updated to assert __root.s (the consumed
product) while deps' .wwi byte-id is retained.

M3-tail commit-6 prerequisite. Gate 989_seproot_export_run reproduces the
export-fn-over-unexported-type root + proves it sep-builds, with a
non-vacuity leg that the forced -I path still rejects.
This commit is contained in:
2026-06-16 17:16:37 +09:00
parent 747475174a
commit 2ff54c8bd4
7 changed files with 362 additions and 26 deletions

View File

@@ -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,

View File

@@ -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 <wwi> 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)");

View File

@@ -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 <wwi> 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;

View File

@@ -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 <wwi> 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;

View File

@@ -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]);

View File

@@ -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]);

View File

@@ -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 <wwi> -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/stat.h>
#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;
}