/* * 989_sepcycle_dup — M3-tail commit-4 negative gates (#46, #31). * * Two error PATHS the commit-4 link-hardening relies on, each asserted to * ACTUALLY FIRE (ken #263: a happy-path-green run does not prove a reject * works — a no-op reject ships green). Every negative leg asserts * (exit != 0) AND (the expected stderr message) AND a non-vacuity flip * (remove the defect → green), so the gate cannot pass for the wrong * reason. * * A. LOUD dep-cycle reject (spec §2, Hare deps.ha:243). A 3-package * import cycle root->A->B->C->A fed to `ww build --sep` must exit * non-zero, print the `dependency cycle: A -> B -> C -> A` chain, and * produce NO output binary. cs==ww (rule 10): the cycle message is * pure driver code, so cstage `ww` and wwstage `ww_ww` emit * BYTE-IDENTICAL stderr — asserted here. Non-vacuity: break the cycle * (C stops importing A) → both stages build green and the program * runs (exit 7). * * B. #31 duplicate-symbol reject (spec §3; w6l ALREADY detects it, this * gate only proves it fires). Two trivial root units each define a * bare `main` (the #31 bare-collision class — #53 path-qualifies * normal exports, so `main` is the cleanest bare clash); both `.o` * fed directly to `w6l` AND `w6l_ww` must exit non-zero with a * `duplicate symbol` message. Non-vacuity: link a single `.o` * (+ libwwrt.a) → clean link, runs. * NOTE on parity: w6l and w6l_ww BOTH reject loud + non-zero and both * name "duplicate symbol", but the EXACT text differs — cstage adds * the path+symbol (`w6l: u2.o: duplicate symbol main`), wwstage prints * the bare `w6l: duplicate symbol`. That is a PRE-EXISTING w6l_ww vs * w6l message divergence on the reject path (byte-id-blind); commit 4 * adds NO linker code and must hold w6l_ww byte-identical, so it is * NOT reconciled here. The gate asserts the achievable reject parity: * both exit != 0 AND both stderr contain "duplicate symbol". The * text-divergence is filed (see report) for a sibling linker task. * * Light wwstage-driver test (CLAUDE.md rule 14): all fixtures + scratch * live under /tmp, COLD each run. Models 989_sepbuild_run.c conventions. */ #include #include #include #include #include #include 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; } /* True iff file `path` contains the literal substring `needle`. */ static int file_has(const char *path, const char *needle) { char *b = NULL; size_t n = 0; if (slurp(path, &b, &n) < 0) return 0; int found = (strstr(b, needle) != NULL); free(b); return found; } 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]; int fail = 0; snprintf(td, sizeof td, "/tmp/wwcd_%d", getpid()); snprintf(cmd, sizeof cmd, "rm -rf %s", td); runwait(cmd); mkdir(td, 0755); /* ---- A. dep-cycle reject ----------------------------------------- */ char rootww[1024], dira[1024], dirb[1024], dirc[1024], fa[1100], fb[1100], fc[1100]; snprintf(rootww, sizeof rootww, "%s/root.ww", td); snprintf(dira, sizeof dira, "%s/pkga", td); snprintf(dirb, sizeof dirb, "%s/pkgb", td); snprintf(dirc, sizeof dirc, "%s/pkgc", td); mkdir(dira, 0755); mkdir(dirb, 0755); mkdir(dirc, 0755); snprintf(fa, sizeof fa, "%s/a.ww", dira); snprintf(fb, sizeof fb, "%s/b.ww", dirb); snprintf(fc, sizeof fc, "%s/c.ww", dirc); if (write_file(rootww, "package main;\n" "import pkga;\n" "fn main() i32 = { return pkga.av(); };\n") || write_file(fa, "package pkga;\n" "import pkgb;\n" "export fn av() i32 = { return pkgb.bv(); };\n") || write_file(fb, "package pkgb;\n" "import pkgc;\n" "export fn bv() i32 = { return pkgc.cv(); };\n") || write_file(fc, "package pkgc;\n" "import pkga;\n" "export fn cv() i32 = { return pkga.av(); };\n")) { fail++; goto out; } struct { const char *drv, *tag; } stg[] = { { "ww", "cs" }, { "ww_ww", "ww" } }; char cerr_path[2][1100]; for (int s = 0; s < 2; s++) { char prog[1100], errf[1100]; snprintf(prog, sizeof prog, "%s/prog.%s", td, stg[s].tag); snprintf(errf, sizeof errf, "%s/cyc.%s.err", td, stg[s].tag); snprintf(cerr_path[s], sizeof cerr_path[s], "%s", errf); snprintf(cmd, sizeof cmd, "%s/%s build --sep -o %s %s >/dev/null 2>%s", bin, stg[s].drv, prog, rootww, errf); int rc = runwait(cmd); if (rc == 0) { fprintf(stderr, "cycle FAIL: %s build --sep returned 0 " "(no-op reject)\n", stg[s].drv); fail++; } if (!file_has(errf, "dependency cycle: pkga -> pkgb -> pkgc -> pkga")) { fprintf(stderr, "cycle FAIL: %s missing cycle-chain " "message\n", stg[s].drv); fail++; } if (access(prog, 0) == 0) { fprintf(stderr, "cycle FAIL: %s produced a partial " "binary %s\n", stg[s].drv, prog); fail++; } } /* cs==ww (rule 10): the cycle message is pure driver code → stderr * byte-identical between the two stages. */ if (files_eq(cerr_path[0], cerr_path[1]) != 0) { fprintf(stderr, "cycle FAIL: cs stderr != ww stderr (rule 10)\n"); fail++; } /* Non-vacuity: break the cycle → both stages build + run (exit 7). */ if (write_file(fc, "package pkgc;\n" "export fn cv() i32 = { return 7; };\n")) { fail++; goto out; } for (int s = 0; s < 2; s++) { char prog[1100]; snprintf(prog, sizeof prog, "%s/ok.%s", td, stg[s].tag); snprintf(cmd, sizeof cmd, "%s/%s build --sep -o %s %s >/dev/null 2>&1", bin, stg[s].drv, prog, rootww); if (runwait(cmd) != 0) { fprintf(stderr, "cycle FAIL(non-vacuity): %s could not " "build the acyclic graph\n", stg[s].drv); fail++; continue; } int rc = runwait(prog); if (rc != 7) { fprintf(stderr, "cycle FAIL(non-vacuity): %s prog exit=%d " "expected 7\n", stg[s].drv, rc); fail++; } } /* ---- B. #31 duplicate-symbol reject ------------------------------ */ char u1[1100], u2[1100], s1[1100], s2[1100], o1[1100], o2[1100]; snprintf(u1, sizeof u1, "%s/u1.ww", td); snprintf(u2, sizeof u2, "%s/u2.ww", td); snprintf(s1, sizeof s1, "%s/u1.s", td); snprintf(s2, sizeof s2, "%s/u2.s", td); snprintf(o1, sizeof o1, "%s/u1.o", td); snprintf(o2, sizeof o2, "%s/u2.o", td); if (write_file(u1, "package main;\nfn main() i32 = { return 0; };\n") || write_file(u2, "package main;\nfn main() i32 = { return 1; };\n")) { fail++; goto out; } snprintf(cmd, sizeof cmd, "%s/w6c -c -o %s %s && %s/w6a -o %s %s", bin, s1, u1, bin, o1, s1); if (runwait(cmd) != 0) { fprintf(stderr, "dup FAIL: build u1.o\n"); fail++; goto out; } snprintf(cmd, sizeof cmd, "%s/w6c -c -o %s %s && %s/w6a -o %s %s", bin, s2, u2, bin, o2, s2); if (runwait(cmd) != 0) { fprintf(stderr, "dup FAIL: build u2.o\n"); fail++; goto out; } /* Both linkers must reject the dup loud + non-zero. */ const char *lnk[] = { "w6l", "w6l_ww" }; for (int i = 0; i < 2; i++) { char errf[1100], prog[1100]; snprintf(errf, sizeof errf, "%s/dup.%s.err", td, lnk[i]); snprintf(prog, sizeof prog, "%s/dupprog.%s", td, lnk[i]); snprintf(cmd, sizeof cmd, "%s/%s -o %s %s %s >/dev/null 2>%s", bin, lnk[i], prog, o1, o2, errf); int rc = runwait(cmd); if (rc == 0) { fprintf(stderr, "dup FAIL: %s accepted a duplicate " "symbol (exit 0)\n", lnk[i]); fail++; } if (!file_has(errf, "duplicate symbol")) { fprintf(stderr, "dup FAIL: %s missing 'duplicate symbol' " "message\n", lnk[i]); fail++; } if (access(prog, 0) == 0) { fprintf(stderr, "dup FAIL: %s produced a partial binary " "%s on the reject path\n", lnk[i], prog); fail++; } } /* Non-vacuity: a single .o + libwwrt.a links clean and runs. */ { char rt[1100], prog[1100]; snprintf(rt, sizeof rt, "%s/../lib/libwwrt.a", bin); for (int i = 0; i < 2; i++) { snprintf(prog, sizeof prog, "%s/single.%s", td, lnk[i]); snprintf(cmd, sizeof cmd, "%s/%s -o %s %s %s >/dev/null 2>&1", bin, lnk[i], prog, o1, rt); if (runwait(cmd) != 0) { fprintf(stderr, "dup FAIL(non-vacuity): %s could " "not link a single .o\n", lnk[i]); fail++; continue; } if (runwait(prog) != 0) { fprintf(stderr, "dup FAIL(non-vacuity): %s single " "prog did not run clean\n", lnk[i]); fail++; } } } out: snprintf(cmd, sizeof cmd, "rm -rf %s", td); runwait(cmd); if (fail) { fprintf(stderr, "sepcycle_dup: %d check(s) failed\n", fail); return 1; } printf("sepcycle_dup: dep-cycle reject (chain named, cs==ww stderr, no " "partial binary, non-vacuity flip) + #31 dup-symbol reject (w6l & " "w6l_ww loud+non-zero, non-vacuity single-.o link)\n"); return 0; }