/* * 989_barefn_collide_run — #84 cgen bare-module fn-leaf collision gate. * * A bare-module fn (module==NULL — a package-LESS `//ww:module-reset` * primary, e.g. a user test file) whose leaf collides with an IMPORTED * module's same-leaf fn was MIS-MANGLED to the imported qualified name: * mod_collect skipped bare decls, so the bare fn never entered mod_map, * and at emission mod_lookup_for_fn(leaf, hint=NULL) first-matched the * imported entry → emitted `.` instead of bare ``. That * produced a DUPLICATE symbol with the imported fn (w6l-tolerated, #31- * class) and the bare fn became silently dead — a #40 residual / #263- * class silent miscompile, gate-blind and symmetric cs==ww. Surfaced by * #80's `ww test` coexist (lib/test exports `run`). * * Fixture (the minimal real repro): a dir-package `aa` exporting `run`, * imported by a PACKAGE-LESS root that defines its OWN bare `fn run` and * calls it from `main`. Package-less is REQUIRED: a `package main` root's * decls mangle `main.` (the package clause sets the module), so its * `run` would be `main.run` — no collision with `aa.run`, vacuous. Only a * package-less primary yields a truly BARE `run`, the #84 trigger. * * Asserts (combined `ww build`, BOTH driver stages; the fix lives in * mod_collect/mod_lookup_for_fn which run in every mode, so combined is * the minimal repro — `--sep` reproduces identically): * 1. Build + run, BOTH stages → exit 9 (the USER's bare `run`, return 9), * never aa.run (return 5). * 2. cs==ww (rule 10): the combined `.s` is byte-identical between the * two driver stages. * 3. NON-VACUITY + SOUNDNESS (the #84-specific signal): the `.s` has * EXACTLY ONE `TEXT run` (the user's, bare) AND EXACTLY ONE `TEXT * aa.run` (the import) — i.e. DISTINCT symbols, no dup. Pre-fix this * row fails: two `TEXT aa.run` (the user's `run` mis-mangled onto the * import) and zero bare `TEXT run`. (Exit alone is not a reliable * signal: pre-fix the dup happened to bind the user's copy under this * link order and still exited 9 — the label count is the proof.) * * Light wwstage-driver test (CLAUDE.md rule 14): all intermediates are * `-o`-redirected to /tmp, phase-parallel-safe. Models 989_sepbuild_run.c. */ #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; } 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; } /* Count occurrences of a line beginning with `^TEXT ,` in the .s. */ static int count_text_label(const char *sfile, const char *sym) { char *buf = NULL; size_t n = 0; if (slurp(sfile, &buf, &n) < 0) return -1; char needle[128]; snprintf(needle, sizeof needle, "TEXT %s,", sym); size_t nl = strlen(needle); int count = 0; size_t i = 0; while (i < n) { size_t j = i; while (j < n && buf[j] != '\n') j++; if (j - i >= nl && strncmp(buf + i, needle, nl) == 0) count++; i = j + 1; } free(buf); return count; } static const char *aa_src = "package aa;\n" "export fn run() i32 = { return 5; };\n"; /* PACKAGE-LESS root (no `package` clause) → its `fn run` is bare (the #84 * trigger); a `package main` root would mangle it `main.run` (vacuous). */ static const char *root_src = "import aa;\n" "fn run() i32 = { return 9; };\n" "export fn main() i32 = { return run(); };\n"; int main(void) { const char *bin = absbin(); if (!bin) { fprintf(stderr, "84 FAIL: getcwd\n"); return 1; } char td[64], cmd[8192]; int fail = 0; snprintf(td, sizeof td, "/tmp/wwbare84_%d", getpid()); snprintf(cmd, sizeof cmd, "rm -rf %s", td); runwait(cmd); mkdir(td, 0755); /* All paths derive from `td` (a small fixed 64-byte buffer) so the * snprintfs are provably non-truncating (warning-clean). */ char aadir[1024], aaww[1024], rootww[1024]; snprintf(aadir, sizeof aadir, "%s/aa", td); mkdir(aadir, 0755); snprintf(aaww, sizeof aaww, "%s/aa/aa.ww", td); snprintf(rootww, sizeof rootww, "%s/root.ww", td); if (write_file(aaww, aa_src) || write_file(rootww, root_src)) { fprintf(stderr, "84 FAIL: write fixture\n"); fail++; goto out; } struct { const char *drv, *tag; char prog[1024], sfile[1024]; } stg[] = { { "ww", "cs", {0}, {0} }, { "ww_ww", "ww", {0}, {0} } }; for (int s = 0; s < 2; s++) { snprintf(stg[s].prog, sizeof stg[s].prog, "%s/prog.%s", td, stg[s].tag); snprintf(stg[s].sfile, sizeof stg[s].sfile, "%s/prog.%s.s", td, stg[s].tag); /* #93 sep layout: `--sep -o ` splits the asm across * .sepwork/.s — the bare user `TEXT run,` lands in * __root.s and the imported `TEXT aa.run,` in aa.s. Concat all * the per-unit .s (sorted glob order is deterministic) into the * flat .s the label-count + byte-id checks consume. The * `-I %s` source path is preserved; WW_PKGCACHE is pinned under * the tmpdir so the shared out/.pkgcache stays untouched. */ snprintf(cmd, sizeof cmd, "WW_PKGCACHE=%s/pkgc timeout 240 %s/%s build --sep -o %s " "-I %s %s >/dev/null 2>&1 && cat %s.sepwork/*.s > %s", td, bin, stg[s].drv, stg[s].prog, td, rootww, stg[s].prog, stg[s].sfile); if (runwait(cmd) != 0) { fprintf(stderr, "84 FAIL: %s build\n", stg[s].drv); fail++; continue; } /* (1) the user's bare `run` (9) must win over imported aa.run (5). */ int rc = runwait(stg[s].prog); if (rc != 9) { fprintf(stderr, "84 FAIL: %s prog exit=%d expected 9 " "(bare user run, not aa.run=5)\n", stg[s].drv, rc); fail++; } /* (3) distinct symbols, no dup: exactly one bare `run` + one * `aa.run`. Pre-fix: two `aa.run` (user run mis-mangled) + zero * bare `run`. */ int nrun = count_text_label(stg[s].sfile, "run"); int naa = count_text_label(stg[s].sfile, "aa.run"); if (nrun != 1 || naa != 1) { fprintf(stderr, "84 FAIL: %s labels TEXT run=%d aa.run=%d " "(want 1/1 — bare user run distinct from import, no dup)\n", stg[s].drv, nrun, naa); fail++; } } /* (2) cs==ww (rule 10): the combined .s is byte-identical. */ if (files_eq(stg[0].sfile, stg[1].sfile) != 0) { fprintf(stderr, "84 FAIL: cs .s != ww .s (rule 10)\n"); fail++; } out: snprintf(cmd, sizeof cmd, "rm -rf %s", td); runwait(cmd); if (fail) { fprintf(stderr, "84: %d check(s) failed\n", fail); return 1; } printf("barefn_collide: package-less root `fn run` (bare) coexists with " "imported aa.run — distinct labels, no dup, user run wins (exit 9), " "cs==ww, both driver stages (#84)\n"); return 0; }