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".
This commit is contained in:
2026-06-29 03:55:26 +09:00
parent d34c90d932
commit 7b9488706b
95 changed files with 579 additions and 223 deletions

View File

@@ -12,27 +12,23 @@
* 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.
* 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; 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),
* 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. 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.)
* 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.
@@ -136,9 +132,13 @@ 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). */
/* 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";
@@ -191,21 +191,20 @@ main(void)
fail++;
continue;
}
/* (1) the user's bare `run` (9) must win over imported aa.run (5). */
/* (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 "
"(bare user run, not aa.run=5)\n", stg[s].drv, rc);
"(user main.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");
/* (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 run=%d aa.run=%d "
"(want 1/1 — bare user run distinct from import, no dup)\n",
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++;
}
@@ -224,8 +223,8 @@ out:
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");
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;
}