Promote commit-3's tri-color topo bail into a loud dep_cycle error that names the full import cycle, byte-identical both stages (cmd/ww/main.c + selfhost/cmd/ww/main.ww, deps.ha:243 parity). Add a non-vacuous negative gate (989_sepcycle_dup) proving the pre-existing w6l duplicate-symbol reject fires loud + non-zero on a cross-package collision; no new linker code. #58(b)(c) link-arg parity deferred (system()-string vs procrun()-argv is structurally un-unifiable in this scope); #61 filed for the byte-id-blind w6l_ww dup-message divergence.
This commit is contained in:
307
test/wcc/989_sepcycle_dup.c
Normal file
307
test/wcc/989_sepcycle_dup.c
Normal file
@@ -0,0 +1,307 @@
|
||||
/*
|
||||
* 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 <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/wait.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user