The driver flip moves build artifacts from next-to-source <stem>.s to a .sepwork/ scratch dir. 29 Pattern-B gates now build `ww build --sep -o <stem>` and read <stem>.sepwork/__root.s (multi-package gates concat all <stem>.sepwork/*.s, since cross-package labels live in per-package .s). All intermediates redirect to /tmp (WW_PKGCACHE + -o), so the corpus runs parallel-safe with no source-tree pollution. Tests pass now (--sep is live) and survive the flip. 915 additionally retargeted off strconv's PRIVATE left_shift_table (a let, not export) — which separate compilation correctly hides — onto a test-local package that exports its own probe table (#96). The combined path only linked it via a single-unit private leak; encapsulation is now honored under sep. Test-only; all 5 *_ww binaries HOLD. 989_m1mangle_run deferred (blocked by #99, imported-package fn main mangling under sep).
232 lines
7.6 KiB
C
232 lines
7.6 KiB
C
/*
|
|
* 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 `<mod>.<leaf>` instead of bare `<leaf>`. 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.<leaf>` (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 <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;
|
|
}
|
|
|
|
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 <sym>,` 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 <prog>` splits the asm across
|
|
* <prog>.sepwork/<pkg>.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 <prog>.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;
|
|
}
|