selfhost+test: graduate structlookup same-module-first (#4b)

Class A silent miscompile, latent until two modules export the same
struct leaf name. Wwstage's structlookup (selfhost/cmd/wcc/cgenutil.ww)
walked c.structs head-first by sname, returning the FIRST match.
cgdot's *struct field-load branch handed it inner.str (the bare
leaf from a parsed N_TPTR whose inner is N_TNAME) and the head-pick
silently emitted the wrong-module field offset — a displacement
against BX that loaded whatever the colliding-module struct happened
to align there. Cstage carries no sister bug: resolve_typename
(cmd/wcc/check.c:65) already routes bare-leaf TY_STRUCT names
through scope_lookup_prefer per c->cur_mod, and cgen.c reads
fi.foff off the typed Sym — cs vs ws asm diverged on every bare-
leaf collision but no in-tree corpus declares two same-leaf
structs, so 995_self_rebuild stayed green (same surfacing pattern
as #4a enumlookup post-strings).

Fifth leaf of the trio leaf-name lookup graduation (after #27
aliaslookup, #28/#31 fnparams/fnretlookupmod, #4a enumlookup):
structlookup grows a same-module-first walk before the head-walk
fallback, mirroring aliaslookup's two-pass shape. No structlookupmod
variant — pkg.S collapses at parse time (lib/ww/parse/parse.ww
joindotted) into a single N_TNAME str routed through the existing
embedded-dot smod==pkg branch, so there's no cgdot-style N_DOT
consumer surface to add a *mod variant for (deferred per rob until
one surfaces). No cstage symmetric fix needed for the same reason
the bug doesn't surface there.

734_struct_modshadow pins the fix with 2 rows: row 1 bare-leaf in
module M must fold against M's own S even with another module's
same-leaf S at the head of c.structs (asserts the matching field-
load disp inside the right TEXT sym + bad disp NOT-presence anti-
check + byte-id between stages); row 2 pkg-qualified alpha.S
from inside alpha is defensive coverage of the pre-existing
embedded-dot smod==pkg branch — same path pre/post-fix (no
sentinel-flip on this commit), pinned here so a future regression
to the embedded-dot lookup is caught.
This commit is contained in:
2026-05-18 14:04:26 +09:00
parent 45339d2f5b
commit f8d2f92316
5 changed files with 300 additions and 21 deletions

View File

@@ -1430,19 +1430,28 @@ export fn callsretsize(c: *cgen, n: *node) i32 = {
};
fn structlookup(c: *cgen, name: str) *structinfo = {
// Exact match first: bare-from-source struct names and already-
// leafed lookups hit here directly.
// Same-module first, then any. Trio-leaf graduation mirroring
// aliaslookup (#27), fnret/fnparamslookupmod (#28/#31), and
// enumlookup (#4a): without the prefer pass a bare-leaf struct
// name in module M can collapse onto another module's same-leaf
// struct prepended earlier in c.structs, silently picking the
// wrong totsize / field offsets.
let s: *structinfo = c.structs;
for (s != nil) {
if (streq(s.sname, name)) {
if (streq(s.smod, c.curmod)) { return s; };
};
s = s.sinext;
};
s = c.structs;
for (s != nil) {
let sn: str = s.sname;
if (streq(sn, name)) { return s; };
s = s.sinext;
};
// Module-qualified form: `pkg.S` → match the leaf scoped to its
// originating module. Mirrors aliaslookup's mod-filter; the
// `smod == pkg` guard is what prevents two modules with same-
// leaf-name structs from collapsing into whichever entry appears
// first in the chain.
// Module-qualified form embedded in name (`pkg.S`): scope the
// leaf to its originating module. The `smod == pkg` guard
// prevents same-leaf structs in two modules from collapsing.
let i: i32 = name.len - 1;
for (i >= 0) {
if (name[i] == 46u8) { // '.'