Files
ww/test/wcc/989_barefn_collide_run.c
Hojun-Cho 7b9488706b parse: enforce strict-package — reject package-less files (#24a)
Flip the soft-default to a hard "missing package clause" error symmetrically in
both stages (cmd/wcc/parse.c + lib/ww/syntax/parse.ww): the first real decl of a
primary section with empty pathmod/resetmod and no seen clause is now rejected.
Closes the documented soft-default divergence (the 63-wrapper carve-out).

The gate flip can't be split from the migration it breaks, so this is one atomic
commit: ~80 test/wcc wrappers gain `package main;` via a shared wwtestpkg.h
helper, 6 data fixtures plus 17 asm-grep assertions update for the bare->main.<leaf>
root-helper mangle shift, and rt/ declares `package rt;` with @symbol pinning the
bare rt_ensure/rt_malloc linker names.

Root mangling narrows: the executable entry `main` stays bare (existing
carve-out), but root helper symbols become main.X. The #84 cluster is rewritten
to assert main.run distinct from aa.run/test.run; its cgen fix and bare machinery
are retained — still load-bearing for package-less module-reset deps. New
table-driven test 782_strict_package.c (6 rows, both stages).

Retiring //ww:module-reset is deferred to #24b: it is load-bearing (clears the
.wwi pathmod so the body's package clause asserts), not a vestige; fusing its
removal here would be a silent mismatch.

All byte-id gates green; full make test reports "all 335 tests passed".
2026-06-29 03:55:26 +09:00

231 lines
7.5 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`).
*
* Strict-package (#24a) form: a dir-package `aa` exporting `run`, imported
* by a `package main;` root that defines its OWN `fn run` and calls it from
* `main`. The root's `run` mangles `main.run` — DISTINCT from the imported
* `aa.run` and from the bare entry `main`. The original #84 trigger (a
* truly BARE `run` from a PACKAGE-LESS root) is unreachable now that
* strict-package forbids package-less primaries; the #84 cgen machinery
* (mod_collect / mod_lookup_for_fn) stays dormant-but-load-bearing for
* genuinely package-less `//ww:module-reset` deps until #24b/#26.
*
* Asserts (combined `ww build`, BOTH driver stages; `--sep` reproduces
* identically):
* 1. Build + run, BOTH stages → exit 9 (the user's `main.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. DISTINCTNESS: the `.s` has EXACTLY ONE `TEXT main.run` (the user's)
* AND EXACTLY ONE `TEXT aa.run` (the import) — distinct symbols, no dup.
*
* 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";
/* Strict-package (#24a) root: declares `package main;`, so its `fn run`
* mangles `main.run` — DISTINCT from the imported `aa.run`, and from the
* bare entry `main`. The bare call `run()` resolves to the local `main.run`
* via the standard exact-hint path (the #84 special-case is bypassed for a
* moduled root). */
static const char *root_src =
"package main;\n"
"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 `main.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 "
"(user main.run, not aa.run=5)\n", stg[s].drv, rc);
fail++;
}
/* (3) distinct symbols, no dup: exactly one `main.run` + one
* `aa.run`. */
int nrun = count_text_label(stg[s].sfile, "main.run");
int naa = count_text_label(stg[s].sfile, "aa.run");
if (nrun != 1 || naa != 1) {
fprintf(stderr, "84 FAIL: %s labels TEXT main.run=%d aa.run=%d "
"(want 1/1 — user main.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 main;` root `fn run` (main.run) coexists "
"with imported aa.run — distinct labels, no dup, user run wins "
"(exit 9), cs==ww, both driver stages (#84/#24a)\n");
return 0;
}