From 4ae29d69495eca935e2138dd43571b4a131962c2 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Wed, 17 Jun 2026 00:17:25 +0900 Subject: [PATCH] test/wcc: c6 dual-path sep==combined soak gate (#46) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make the sep-compile == combined-compile output equivalence a standing regression test — the last criterion before M4 can delete the combined.ww embedding model. 989_c6soak_run (phase-1): Tier-A/B + same-leaf collision soak. Two real roots (utf8: encoding.utf8 dotted-path + strings; collide: strings.contains + bytes.contains) built both ways with isolated cold WW_PKGCACHE; asserts combined-run-exit == sep-run-exit on both drivers, cs==ww per-pkg .s/.wwi, and two distinct collision labels. 994_w6c_ww (phase-2): Tier-C folded into the existing w6c byte-id oracle. A sep-built w6c emits byte-identical .s to a combined-built w6c on all five bootstrap targets (w6c/wwdump/w6a/w6l/ww .combined.ww); .wwi is in the compare set. The tool binaries themselves differ by a benign constant link-layout delta — the oracle is tool OUTPUT, not tool-binary identity. Test-only; the five wwstage binary pins are unchanged. --- Makefile | 21 ++- test/wcc/989_c6soak_run.c | 324 ++++++++++++++++++++++++++++++++++++++ test/wcc/994_w6c_ww.c | 171 +++++++++++++++++++- 3 files changed, 514 insertions(+), 2 deletions(-) create mode 100644 test/wcc/989_c6soak_run.c diff --git a/Makefile b/Makefile index a216c380..c87ac6c3 100644 --- a/Makefile +++ b/Makefile @@ -568,6 +568,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_sepcycle_dup \ $(BIN)/test_separchive_run \ $(BIN)/test_pkgcache_run \ + $(BIN)/test_c6soak_run \ $(BIN)/test_floatlit_run \ $(BIN)/test_checked_run \ $(BIN)/test_floatarr_run \ @@ -2918,7 +2919,14 @@ $(BIN)/test_w6l_ww: test/wcc/992_w6l_ww.c $(BIN)/w6l $(BIN)/w6l_ww \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< -$(BIN)/test_w6c_ww: test/wcc/994_w6c_ww.c $(BIN)/w6c_ww $(BIN)/wwdump_ww | $(BIN) +# Tier-C fold (#46 c6): the sep-build legs drive BOTH tool trios — the cstage +# `ww --sep` shells out to w6c/w6a/w6l (toolpath self_dir), the wwstage +# `ww_ww --sep` to the _ww trio — plus both drivers + libwwrt. +$(BIN)/test_w6c_ww: test/wcc/994_w6c_ww.c $(BIN)/w6c_ww $(BIN)/wwdump_ww \ + $(BIN)/ww $(BIN)/ww_ww \ + $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ + $(BIN)/w6a_ww $(BIN)/w6l_ww \ + $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< $(BIN)/test_ww_ww: test/wcc/993_ww_ww.c $(BIN)/ww $(BIN)/ww_ww \ @@ -3200,6 +3208,17 @@ $(BIN)/test_pkgcache_run: test/wcc/989_pkgcache_run.c $(BIN)/ww $(BIN)/ww_ww \ $(BIN)/w6l $(BIN)/w6l_ww $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +# 989_c6soak_run — M3-tail commit-6 dual-path soak gate, phase-1 leg (#46 +# c6). Builds real lib chains (encoding.utf8 dotted + strings/bytes same-leaf +# `contains`) BOTH combined and --sep, asserts combined==sep run-equivalence +# (the M4 behavioral oracle, NOT binary identity), cs==ww per-pkg .s/.wwi, and +# the §3 distinct-label non-vacuity. Needs both driver + both compiler + both +# linker stages + libwwrt.a. (Tier-C capstone folds into 994_w6c_ww.) +$(BIN)/test_c6soak_run: test/wcc/989_c6soak_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 $@ $< + $(BIN)/test_floatlit_run: test/wcc/989_floatlit_run.c $(BIN)/ww $(BIN)/w6c \ $(BIN)/w6c_ww $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< diff --git a/test/wcc/989_c6soak_run.c b/test/wcc/989_c6soak_run.c new file mode 100644 index 00000000..ca2a2f2b --- /dev/null +++ b/test/wcc/989_c6soak_run.c @@ -0,0 +1,324 @@ +/* + * 989_c6soak_run — M3-tail commit-6 dual-path soak gate, phase-1 leg + * (#46 c6, rob-c6-spec.md Tier-A/B + §3 collision). + * + * The STANDING regression-gate for M4: separate-compilation must be + * behaviorally equivalent to the combined (embed-everything) build on + * real, multi-package code. The oracle is OBSERVABLE OUTPUT, not binary + * identity — a sep-linked binary and a combined-linked binary differ in + * symbol order/layout (rob-c6-spec §2 ★), so we run both and compare + * exit codes, never `cmp` the executables. + * + * Targets (real lib chains, cheap → phase-1): + * - utf8 : encoding.utf8 (DOTTED-path #57) + strings → decode + count + * runes ("héllo" = 5). Exercises the multi-component import + * the combined build resolves by leaf and sep must resolve + * by the full dotted path. + * - collide: strings.contains AND bytes.contains — the SAME leaf `contains` + * exported by two packages (rob-c6-spec §3, the #48/#49 class + * on real code). sep path-qualifies them to DISTINCT symbols; + * the program prints both correct results; the §3 non-vacuity + * check below greps the per-pkg .s to prove the two labels are + * genuinely distinct (disambiguation is real, not luck). + * + * Asserts per target (all COLD — fresh per-stage WW_PKGCACHE, scratch + * wiped each run): + * 1. DUAL-PATH OUTPUT EQUIVALENCE (the teeth): combined-build run-exit == + * sep-build run-exit == expected, for BOTH driver stages. This is the + * M4 behavioral proof: sep emits a program that behaves identically to + * the combined one. + * 2. cs==ww (rule 10): the cstage `ww` and wwstage `ww_ww` sep-drivers emit + * byte-identical per-package .s and .wwi (the .wwi enters the compare + * set — rob-c6-spec). Both also link to a binary that runs to `expect`. + * 3. §3 NON-VACUITY (collide only): the per-package strings.s defines + * `TEXT strings.contains` and bytes.s defines `TEXT bytes.contains` — + * two DISTINCT symbols. The w6l dup-gate stays silent (no collision); + * both resolve; the program returns 7. + * + * The heavy Tier-C capstone (real tool w6c/wwdump self-compile output + * equivalence) is FOLDED into the 990-997 bootstrap oracle (994_w6c_ww), + * not bolted on here (rob-c6-spec §2 / amendment-B). + * + * Light wwstage-driver test (CLAUDE.md rule 14): all intermediates are + * `-o`-redirected to /tmp, so it is phase-1 parallel-safe. Models + * 989_sepbuild_run.c / 989_sepdotpath_run.c conventions; 989 prefix per + * the sep-gate precedent. + */ +#include +#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; +} + +/* 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; +} + +/* cs==ww over a sep-build's per-package output: every .s/.wwi the cstage + * driver produced in `csdir` must be byte-identical to the wwstage + * driver's same-named file in `wwdir`. Returns the count of mismatches. */ +static int +cmp_sepwork(const char *csdir, const char *wwdir, const char *label) +{ + DIR *d = opendir(csdir); + if (!d) { + fprintf(stderr, "c6soak FAIL: %s — no cs sepwork %s\n", label, csdir); + return 1; + } + int bad = 0, seen = 0; + struct dirent *ent; + while ((ent = readdir(d)) != NULL) { + const char *nm = ent->d_name; + size_t nl = strlen(nm); + int is_s = (nl > 2 && strcmp(nm + nl - 2, ".s") == 0); + int is_wwi = (nl > 4 && strcmp(nm + nl - 4, ".wwi") == 0); + if (!is_s && !is_wwi) continue; + seen++; + char a[2048], b[2048]; + snprintf(a, sizeof a, "%s/%s", csdir, nm); + snprintf(b, sizeof b, "%s/%s", wwdir, nm); + if (files_eq(a, b) != 0) { + fprintf(stderr, "c6soak FAIL: %s — cs!=ww for %s (rule 10)\n", + label, nm); + bad++; + } + } + closedir(d); + /* An existing-but-empty sepwork would pass the cs==ww loop vacuously + * (no per-pkg .s/.wwi to compare); the utf8 root has no other cs==ww + * anchor, so demand at least one compared file. */ + if (seen == 0) { + fprintf(stderr, "c6soak FAIL: %s — no .s/.wwi in %s\n", label, csdir); + bad++; + } + return bad; +} + +struct root { + const char *label; + const char *src; + int expect; + int collide; /* run the §3 distinct-label non-vacuity check */ +}; + +static struct root roots[] = { + { "utf8", + "package main;\n" + "import encoding.utf8;\n" + "import strings;\n" + "fn main() i32 = {\n" + " let d = utf8.decode(strings.toutf8(\"h\303\251llo\"));\n" + " let n: i32 = 0;\n" + " for (true) {\n" + " match (utf8.next(&d)) {\n" + " case rune => n += 1;\n" + " case => break;\n" + " };\n" + " };\n" + " return n;\n" + "};\n", 5, 0 }, + { "collide", + "package main;\n" + "import strings;\n" + "import bytes;\n" + "fn main() i32 = {\n" + " let s = strings.contains(\"hello\", \"ell\");\n" + " let b = bytes.contains(strings.toutf8(\"hello\"), strings.toutf8(\"ell\"));\n" + " if (s && b) { return 7; };\n" + " return 1;\n" + "};\n", 7, 1 }, + { NULL, NULL, 0, 0 }, +}; + +int +main(void) +{ + const char *bin = absbin(); + if (!bin) return 1; + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + + /* The real include set the sep producer needs to resolve the lib + + * frontend package graph (verified live; the bare default path grabs + * the in-tree *test.combined.ww fixtures and collides on `main`). */ + char inc[4096]; + snprintf(inc, sizeof inc, "-I %s/lib -I %s/lib/ww -I %s/selfhost/cmd/wcc", + cwd, cwd, cwd); + + char td[64], cmd[8192]; + int fail = 0; + snprintf(td, sizeof td, "/tmp/wwc6soak_%d", getpid()); + snprintf(cmd, sizeof cmd, "rm -rf %s", td); + runwait(cmd); + mkdir(td, 0755); + + for (int i = 0; roots[i].label; i++) { + struct root *r = &roots[i]; + char rootww[1024]; + snprintf(rootww, sizeof rootww, "%s/%s.ww", td, r->label); + if (write_file(rootww, r->src)) { fail++; continue; } + + /* 1. combined build (cstage driver) → run. */ + char comb[1024]; + snprintf(comb, sizeof comb, "%s/%s.comb", td, r->label); + snprintf(cmd, sizeof cmd, + "timeout 240 %s/ww build %s -o %s %s >/dev/null 2>&1", + bin, inc, comb, rootww); + if (runwait(cmd) != 0) { + fprintf(stderr, "c6soak FAIL: %s combined build\n", r->label); + fail++; + continue; + } + int comb_rc = runwait(comb); + + /* sep build both stages, fresh per-stage WW_PKGCACHE so every + * package compiles COLD (a warm shared cache would skip the + * per-pkg .s/.wwi this gate inspects — mirrors 989_pkgcache). */ + struct { const char *drv, *tag; char prog[1024]; int rc; } + stg[] = { { "ww", "cs", {0}, -1 }, { "ww_ww", "ww", {0}, -1 } }; + int built = 1; + for (int s = 0; s < 2; s++) { + /* `.bin` infix keeps the stage-`ww` output distinct from the + * `