wcc/ww: promote module-name SK_USE to value kind (use_alias), aligned to cstage

A primary-package top-level decl whose leaf also names a bundled module
(`type sym` vs `import sym`; the lib/test->fnmatch->ascii -T floor's
`ascii` module vs a `@test fn ascii`) collided in the flat scope: #23's
installtop dup-check false-fired "duplicate fn/type" where cstage
coexists. cstage keys the module name out of the value namespace by
PROMOTING the same-leaf SK_USE in place to the value kind with
use_alias=1 (cmd/wcc/check.c:2831/2848/2871/2907/2928/2951), so one
correctly-kinded sym serves bare refs (call/structlit/var) while
`name.member` still resolves the module via the `kind==SK_USE ||
use_alias` N_DOT guards (check.c:87/1337).

This REVERSES wwstage's documented two-sym coexistence design
(lib/ww/sym.ww scopelookupuselocal): keeping the SK_USE as a separate
coexisting sym ripples into every bare-reference resolver (~25
scopelookup sites), and a missed site is a byte-id-consistent-but-wrong
cat-A risk the gate cannot prove away — the same failure mode retired
with the name-keyed variant-match cluster. The promote model is correct
by construction: one sym of the right kind, identical to cstage.

sym gains a use_alias field; the three N_DOT/N_CALL module-qualified
guards honor use_alias. cstage installs every SK_USE in a dedicated
first pass, so its value-arm promote is order-INDEPENDENT; wwstage
installs in source order, so BOTH directions of the collision are
promoted to reach cstage's identical single-sym end state:
  - use-before-value (`import aa` then `fn aa`): installtop promotes the
    pre-installed SK_USE to the value kind (use_alias=1).
  - value-before-use (`fn aa` then `import aa`): installdecl's N_USE arm
    promotes the pre-installed value sym in place (set use_alias=1,
    no coexisting SK_USE), mirroring cstage's self-import N_USE arm
    (check.c:2823-2834 `if (prev) prev->use_alias = 1`).
Both orders compile + are cs/ww byte-identical AND byte-identical to
each other. Cannot split: promote without the guards leaves
`name.member` red on the promoted sym; the guards without promote are
inert (no use_alias is ever set) — no bisect-clean intermediate. (#30)

Pins (910/997): modfn_coexist_ok (use-before-value) AND
modfn_coexist_vbu_ok (value-before-use) both accept on cstage + are
cs/ww byte-id + run to exit 6 (bare fn and qualified module both
resolve); the dup_fn row still rejects both stages (regression). fnmatch
byte-id holds. The order-dependence is exactly what regresses silently,
so both orders are pinned.
This commit is contained in:
2026-06-11 04:51:07 +09:00
parent 14b33993c2
commit 64f0ddf01b
8 changed files with 312 additions and 38 deletions

View File

@@ -317,6 +317,25 @@ collide_run(const char *bin, const char *comp, const char *drv)
return rc;
}
/* modfn_run — `<drv> run <fixture>` and assert exit 6 (#30). The fixture's
* main returns `aa() + aa.helper()` = 1 + 5: the bare `aa()` must resolve to
* the fn (1) and the qualified `aa.helper()` to the module member (5). A
* mis-resolution that still compiled would run to a different code; this
* pins the resolved TARGETS, not just acceptance. */
static int
modfn_run(const char *bin, const char *drv, const char *fixture, const char *what)
{
char cmd[4096];
snprintf(cmd, sizeof cmd, "%s/%s run %s > /dev/null 2>&1", bin, drv, fixture);
int rc = runwait(cmd);
if (rc != 6) {
fprintf(stderr, "910 FAIL: %s run %s exited %d (expected 6 — bare fn "
"+ qualified module resolution)\n", drv, what, rc);
return 1;
}
return 0;
}
int
main(void)
{
@@ -347,11 +366,29 @@ main(void)
"cross-package same-name") != 0) return 1;
if (accept(bin, "w6c", "test/wcc/data/builtin_redecl_ok.ww",
"builtin nomem redecl (carve-out)") != 0) return 1;
/* #30 — a top-level fn whose leaf matches an imported module name
* coexists with the module bareword (different namespaces). Accept
* proves both the bare `aa()` (fn) and qualified `aa.helper()`
* (module) resolve; a mis-resolution would fail to compile. Both
* decl orders are pinned (use-before-value AND value-before-use): the
* order-dependence is exactly what regresses silently — cstage is
* order-independent, wwstage closes both directions via the symmetric
* promotes in installtop + installdecl's N_USE arm. */
if (accept(bin, "w6c", "test/wcc/data/modfn_coexist_ok.ww",
"fn named like imported module (use-before-value)") != 0) return 1;
if (accept(bin, "w6c", "test/wcc/data/modfn_coexist_vbu_ok.ww",
"fn named like imported module (value-before-use)") != 0) return 1;
/* runtime-resolve: both orders run to exit 6 (bare fn=1 + module
* member=5), pinning the resolved targets on the cstage driver. */
if (modfn_run(bin, "ww", "test/wcc/data/modfn_coexist_ok.ww",
"use-before-value") != 0) return 1;
if (modfn_run(bin, "ww", "test/wcc/data/modfn_coexist_vbu_ok.ww",
"value-before-use") != 0) return 1;
if (collide_run(bin, "w6c", "ww") != 0) return 1;
printf("@test -T: run ok + user-main and bad-signature rejected + "
"non-T @test drop + checked-body + dangling-call link-fail (#6) + "
"dup fn/type/def/let reject + xpkg + builtin-redecl accept + "
"fn-run collision (#23)\n");
"modfn coexist (#30) + fn-run collision (#23)\n");
return 0;
}

View File

@@ -365,6 +365,25 @@ collide_run(const char *bin)
return rc;
}
/* modfn_run — `ww_ww run <fixture>` and assert exit 6 (#30): the wwstage
* runtime twin of 910's modfn_run. The fixture's main returns `aa() +
* aa.helper()` = 1 + 5; the bare fn must resolve to 1 and the qualified
* module member to 5, so a mis-resolution that still compiled runs to a
* different code. Pins the resolved TARGETS on the wwstage driver. */
static int
modfn_run(const char *bin, const char *fixture, const char *what)
{
char cmd[4096];
snprintf(cmd, sizeof cmd, "%s/ww_ww run %s > /dev/null 2>&1", bin, fixture);
int rc = runwait(cmd);
if (rc != 6) {
fprintf(stderr, "997 FAIL: ww_ww run %s exited %d (expected 6 — bare "
"fn + qualified module resolution)\n", what, rc);
return 1;
}
return 0;
}
int
main(void)
{
@@ -396,11 +415,30 @@ main(void)
"cross-package same-name") != 0) return 1;
if (accept_byteid(bin, "test/wcc/data/builtin_redecl_ok.ww",
"builtin nomem redecl (carve-out)") != 0) return 1;
/* #30 — a top-level fn whose leaf matches an imported module name
* coexists with the module bareword. wwstage now PROMOTES the same-leaf
* collision in place (use_alias=1), mirroring cstage exactly, so byte-id
* proves the resolved call targets match. Both decl orders are pinned:
* use-before-value promotes via installtop's value-arm, value-before-use
* via installdecl's N_USE arm — both reach the identical single-sym end
* state, so cstage and wwstage agree on every source order. The bare
* `aa()` and qualified `aa.helper()` both resolve, else the compile
* fails. */
if (accept_byteid(bin, "test/wcc/data/modfn_coexist_ok.ww",
"fn named like imported module (use-before-value)") != 0) return 1;
if (accept_byteid(bin, "test/wcc/data/modfn_coexist_vbu_ok.ww",
"fn named like imported module (value-before-use)") != 0) return 1;
/* runtime-resolve: both orders run to exit 6 on the wwstage driver,
* pinning that the promoted sym resolves bare fn=1 + module member=5. */
if (modfn_run(bin, "test/wcc/data/modfn_coexist_ok.ww",
"use-before-value") != 0) return 1;
if (modfn_run(bin, "test/wcc/data/modfn_coexist_vbu_ok.ww",
"value-before-use") != 0) return 1;
if (collide_run(bin) != 0) return 1;
printf("@test -T (ww_ww): run ok + cs/ww byte-id + rejects + "
"non-T @test drop cs/ww byte-id + checked-body (#6) + "
"dup fn/type/def/let reject + xpkg/builtin-redecl byte-id + "
"fn-run collision (#23)\n");
"modfn coexist byte-id (#30) + fn-run collision (#23)\n");
return 0;
}

View File

@@ -0,0 +1,19 @@
// #30 legal control — a top-level fn whose leaf matches an imported
// MODULE name. The module bareword (SK_USE) and the value-namespace fn
// coexist: cstage promotes the SK_USE in place with use_alias
// (cmd/wcc/check.c:2928), wwstage keeps both as separate coexisting syms
// (lib/ww/sym.ww scopedefineinmodule #30 cross-namespace skip). A bundled
// `ascii` module + a primary-package `@test fn ascii` hit this in the wild
// (989_lib_byteid via the lib/test->fnmatch->ascii -T floor). Must compile
// clean + be cs/ww byte-identical. Mirrors a driver-emitted *.combined.ww.
//
// Both refs must resolve, so any mis-resolution fails to COMPILE: `aa()`
// must bind the fn (a module is not callable), and `aa.helper()` must bind
// through the module (the fn has no field `helper`). Accept proves both.
package aa;
export fn helper() i32 = { return 5; };
package main;
import aa;
fn aa() i32 = { return 1; };
export fn main() i32 = { return aa() + aa.helper(); };

View File

@@ -0,0 +1,21 @@
// #30 legal control — VALUE-BEFORE-USE order. The twin of
// modfn_coexist_ok.ww with the decl order flipped: the value-namespace
// `fn aa` is declared BEFORE `import aa`. cstage is order-independent
// (it installs every SK_USE in a dedicated first pass, cmd/wcc/check.c
// :2811+), so its value-arm promote always fires. wwstage installs in
// source order, so this direction is closed by the symmetric promote in
// installdecl's N_USE arm (set use_alias on the pre-installed value sym,
// mirroring cmd/wcc/check.c:2823-2834). Both orders must compile clean +
// be cs/ww byte-identical AND byte-identical to the use-before-value
// order — the order-dependence is exactly what regresses silently.
//
// Both refs must resolve, so any mis-resolution fails to COMPILE: `aa()`
// must bind the fn (a module is not callable), and `aa.helper()` must bind
// through the module (the fn has no field `helper`). Accept proves both.
package aa;
export fn helper() i32 = { return 5; };
package main;
fn aa() i32 = { return 1; };
import aa;
export fn main() i32 = { return aa() + aa.helper(); };