selfhost+cstage+test: graduate enumlookup same-module-first + N_DOT enumlookupmod (#4a)

Class A silent miscompile, latent until two modules export the same
enum leaf name. Wwstage's enumlookup (selfhost/cmd/wcc/cgen.ww)
walked c.enums head-first by ename; cgdot handed it the bare leaf
from N_DOT.lhs.str for both `Color.MEMBER` (lhs N_IDENT) and
`pkg.Color.MEMBER` (lhs N_DOT) shapes, silently dropping the
explicit qualifier on the second. Cstage's enum-member fold
(cmd/wcc/check.c cexpr N_DOT) was carrying the same head-pick on
the lhs-ident lookup — pre-fix the mismatch surfaced as a
"not assignable to <same-leaf>" checker error rather than a silent
wrong-constant because resolve_typename for the fn return spec
already used scope_lookup_prefer correctly, so the rhs's wrong-
module-Color clashed with the return type's right-module-Color.
No in-tree corpus currently declares two same-leaf enums, so
995_self_rebuild stayed green and the latent miscompile only
surfaces once a stdlib port introduces the collision (same shape
as #27 surfacing when lib/strings dragged utf8's invalid alias
into the chain alongside strconv's invalid).

Fourth leaf of the trio leaf-name lookup graduation (after #27
aliaslookup, #28 fnparamslookupmod, #31 fnretlookupmod): wwstage
enumlookup grows a same-module-first walk before the head-walk
fallback, mirroring aliaslookup's two-pass shape (cgen.ww:75).
The N_DOT consumer surface — `pkg.Enum.MEMBER`, already used
in-corpus by os.flag.RDONLY, temp.mode.RDWR, os.whence.SET etc.
— routes through a new enumlookupmod variant with the explicit
N_DOT.lhs.lhs.str as the mod qualifier (mirror of fnret/
fnparamslookupmod). Cstage's check.c cexpr N_DOT lhs lookup
graduates from scope_lookup to scope_lookup_prefer to align
symmetrically (rule 10: both stages pick same-module-first on
the bare-leaf shape).

733_enum_modshadow pins both surfaces with 3 rows: row 1 bare-leaf
in module M must fold against M's own Color even with another
module's same-leaf Color at the head of c.enums; row 2 same-module
`mod.Color.MEMBER` from inside that mod pins the API surface; row 3
cross-module `othermod.Color.MEMBER` from a third module with no
local Color sentinel-flips the cgdot etmod tracking + enumlookupmod
path independently of row 1's same-module-first fallback. Asserts
the matching \$N, immediate inside the right TEXT sym + bad_imm
NOT-presence anti-check on both stages plus byte-id between stages
per row.
This commit is contained in:
2026-05-18 13:32:56 +09:00
parent d09197af8e
commit 45339d2f5b
7 changed files with 368 additions and 32 deletions

View File

@@ -11843,11 +11843,15 @@ fn cgdot(c: *cgen, n: *node) void = {
};
};
// Enum member access: `EnumName.MEMBER` or `pkg.EnumName.MEMBER`
// → inline the pre-computed constant. With driver-side
// concatenation, both forms key off the leaf type name.
// → inline the pre-computed constant. `pkg.Enum.MEMBER` keeps
// `pkg` so enumlookupmod can prefer the explicit module on a
// leaf collision; bare `Enum.MEMBER` falls back to c.curmod via
// enumlookup's same-module-first walk.
if (lhs != nil) {
let etname: str;
let etmod: str;
etname.ptr = nil; etname.len = 0;
etmod.ptr = nil; etmod.len = 0;
if (lhs.kind == nkind.N_IDENT) {
etname = lhs.str;
};
@@ -11855,11 +11859,12 @@ fn cgdot(c: *cgen, n: *node) void = {
if (lhs.lhs != nil) {
if (lhs.lhs.kind == nkind.N_IDENT) {
etname = lhs.str;
etmod = lhs.lhs.str;
};
};
};
if (etname.len > 0) {
let en: *enumtype = enumlookup(c, etname);
let en: *enumtype = enumlookupmod(c, etname, etmod);
if (en != nil) {
let v: u64;
if (enummemberval(en, fld, &v)) {
@@ -18972,18 +18977,26 @@ fn collectenums(c: *cgen, file: *node) void = {
};
fn enumlookup(c: *cgen, name: str) *enumtype = {
// Exact match first: bare-from-source idents and already-leafed
// names hit here directly.
// Same-module first, then any. Trio-leaf graduation mirroring
// aliaslookup (#27) and fnret/fnparamslookupmod (#28/#31): without
// the prefer pass a bare-leaf enum ident in module M can collapse
// onto another module's same-leaf enum prepended earlier in
// c.enums, silently folding `Foo.MEMBER` to the wrong constant.
let e: *enumtype = c.enums;
for (e != nil) {
if (streq(e.ename, name)) {
if (streq(e.emod, c.curmod)) { return e; };
};
e = e.etnext;
};
e = c.enums;
for (e != nil) {
if (streq(e.ename, name)) { return e; };
e = e.etnext;
};
// Module-qualified form: `pkg.enum` → match the leaf scoped to
// its originating module. Mirrors aliaslookup's mod-filter; the
// `emod == pkg` guard is what prevents two modules with same-
// leaf-name enums from collapsing into whichever entry appears
// first in the chain.
// Module-qualified form embedded in name (`pkg.enum`): scope the
// leaf to its originating module. The `emod == pkg` guard prevents
// same-leaf enums in two modules from collapsing.
let i: i32 = name.len - 1;
for (i >= 0) {
if (name[i] == 46u8) { // '.'
@@ -19009,6 +19022,23 @@ fn enumlookup(c: *cgen, name: str) *enumtype = {
return nil;
};
// enumlookupmod — same-module-first leaf walk for `pkg.Enum.MEMBER`
// where the qualifier is an explicit N_IDENT module name. Mirrors
// fnparamslookupmod / fnretlookupmod (#28 / #31). Falls back to the
// bare enumlookup so a missing or empty mod still finds the leaf.
fn enumlookupmod(c: *cgen, name: str, mod: str) *enumtype = {
if (mod.len > 0) {
let e: *enumtype = c.enums;
for (e != nil) {
if (streq(e.ename, name)) {
if (streq(e.emod, mod)) { return e; };
};
e = e.etnext;
};
};
return enumlookup(c, name);
};
fn enummemberval(en: *enumtype, mname: str, out: *u64) bool = {
let m: *enummember = en.members;
for (m != nil) {

View File

@@ -256,18 +256,26 @@ fn collectenums(c: *cgen, file: *node) void = {
};
fn enumlookup(c: *cgen, name: str) *enumtype = {
// Exact match first: bare-from-source idents and already-leafed
// names hit here directly.
// Same-module first, then any. Trio-leaf graduation mirroring
// aliaslookup (#27) and fnret/fnparamslookupmod (#28/#31): without
// the prefer pass a bare-leaf enum ident in module M can collapse
// onto another module's same-leaf enum prepended earlier in
// c.enums, silently folding `Foo.MEMBER` to the wrong constant.
let e: *enumtype = c.enums;
for (e != nil) {
if (streq(e.ename, name)) {
if (streq(e.emod, c.curmod)) { return e; };
};
e = e.etnext;
};
e = c.enums;
for (e != nil) {
if (streq(e.ename, name)) { return e; };
e = e.etnext;
};
// Module-qualified form: `pkg.enum` → match the leaf scoped to
// its originating module. Mirrors aliaslookup's mod-filter; the
// `emod == pkg` guard is what prevents two modules with same-
// leaf-name enums from collapsing into whichever entry appears
// first in the chain.
// Module-qualified form embedded in name (`pkg.enum`): scope the
// leaf to its originating module. The `emod == pkg` guard prevents
// same-leaf enums in two modules from collapsing.
let i: i32 = name.len - 1;
for (i >= 0) {
if (name[i] == 46u8) { // '.'
@@ -293,6 +301,23 @@ fn enumlookup(c: *cgen, name: str) *enumtype = {
return nil;
};
// enumlookupmod — same-module-first leaf walk for `pkg.Enum.MEMBER`
// where the qualifier is an explicit N_IDENT module name. Mirrors
// fnparamslookupmod / fnretlookupmod (#28 / #31). Falls back to the
// bare enumlookup so a missing or empty mod still finds the leaf.
fn enumlookupmod(c: *cgen, name: str, mod: str) *enumtype = {
if (mod.len > 0) {
let e: *enumtype = c.enums;
for (e != nil) {
if (streq(e.ename, name)) {
if (streq(e.emod, mod)) { return e; };
};
e = e.etnext;
};
};
return enumlookup(c, name);
};
fn enummemberval(en: *enumtype, mname: str, out: *u64) bool = {
let m: *enummember = en.members;
for (m != nil) {

View File

@@ -1177,11 +1177,15 @@ fn cgdot(c: *cgen, n: *node) void = {
};
};
// Enum member access: `EnumName.MEMBER` or `pkg.EnumName.MEMBER`
// → inline the pre-computed constant. With driver-side
// concatenation, both forms key off the leaf type name.
// → inline the pre-computed constant. `pkg.Enum.MEMBER` keeps
// `pkg` so enumlookupmod can prefer the explicit module on a
// leaf collision; bare `Enum.MEMBER` falls back to c.curmod via
// enumlookup's same-module-first walk.
if (lhs != nil) {
let etname: str;
let etmod: str;
etname.ptr = nil; etname.len = 0;
etmod.ptr = nil; etmod.len = 0;
if (lhs.kind == nkind.N_IDENT) {
etname = lhs.str;
};
@@ -1189,11 +1193,12 @@ fn cgdot(c: *cgen, n: *node) void = {
if (lhs.lhs != nil) {
if (lhs.lhs.kind == nkind.N_IDENT) {
etname = lhs.str;
etmod = lhs.lhs.str;
};
};
};
if (etname.len > 0) {
let en: *enumtype = enumlookup(c, etname);
let en: *enumtype = enumlookupmod(c, etname, etmod);
if (en != nil) {
let v: u64;
if (enummemberval(en, fld, &v)) {

View File

@@ -11843,11 +11843,15 @@ fn cgdot(c: *cgen, n: *node) void = {
};
};
// Enum member access: `EnumName.MEMBER` or `pkg.EnumName.MEMBER`
// → inline the pre-computed constant. With driver-side
// concatenation, both forms key off the leaf type name.
// → inline the pre-computed constant. `pkg.Enum.MEMBER` keeps
// `pkg` so enumlookupmod can prefer the explicit module on a
// leaf collision; bare `Enum.MEMBER` falls back to c.curmod via
// enumlookup's same-module-first walk.
if (lhs != nil) {
let etname: str;
let etmod: str;
etname.ptr = nil; etname.len = 0;
etmod.ptr = nil; etmod.len = 0;
if (lhs.kind == nkind.N_IDENT) {
etname = lhs.str;
};
@@ -11855,11 +11859,12 @@ fn cgdot(c: *cgen, n: *node) void = {
if (lhs.lhs != nil) {
if (lhs.lhs.kind == nkind.N_IDENT) {
etname = lhs.str;
etmod = lhs.lhs.str;
};
};
};
if (etname.len > 0) {
let en: *enumtype = enumlookup(c, etname);
let en: *enumtype = enumlookupmod(c, etname, etmod);
if (en != nil) {
let v: u64;
if (enummemberval(en, fld, &v)) {
@@ -18972,18 +18977,26 @@ fn collectenums(c: *cgen, file: *node) void = {
};
fn enumlookup(c: *cgen, name: str) *enumtype = {
// Exact match first: bare-from-source idents and already-leafed
// names hit here directly.
// Same-module first, then any. Trio-leaf graduation mirroring
// aliaslookup (#27) and fnret/fnparamslookupmod (#28/#31): without
// the prefer pass a bare-leaf enum ident in module M can collapse
// onto another module's same-leaf enum prepended earlier in
// c.enums, silently folding `Foo.MEMBER` to the wrong constant.
let e: *enumtype = c.enums;
for (e != nil) {
if (streq(e.ename, name)) {
if (streq(e.emod, c.curmod)) { return e; };
};
e = e.etnext;
};
e = c.enums;
for (e != nil) {
if (streq(e.ename, name)) { return e; };
e = e.etnext;
};
// Module-qualified form: `pkg.enum` → match the leaf scoped to
// its originating module. Mirrors aliaslookup's mod-filter; the
// `emod == pkg` guard is what prevents two modules with same-
// leaf-name enums from collapsing into whichever entry appears
// first in the chain.
// Module-qualified form embedded in name (`pkg.enum`): scope the
// leaf to its originating module. The `emod == pkg` guard prevents
// same-leaf enums in two modules from collapsing.
let i: i32 = name.len - 1;
for (i >= 0) {
if (name[i] == 46u8) { // '.'
@@ -19009,6 +19022,23 @@ fn enumlookup(c: *cgen, name: str) *enumtype = {
return nil;
};
// enumlookupmod — same-module-first leaf walk for `pkg.Enum.MEMBER`
// where the qualifier is an explicit N_IDENT module name. Mirrors
// fnparamslookupmod / fnretlookupmod (#28 / #31). Falls back to the
// bare enumlookup so a missing or empty mod still finds the leaf.
fn enumlookupmod(c: *cgen, name: str, mod: str) *enumtype = {
if (mod.len > 0) {
let e: *enumtype = c.enums;
for (e != nil) {
if (streq(e.ename, name)) {
if (streq(e.emod, mod)) { return e; };
};
e = e.etnext;
};
};
return enumlookup(c, name);
};
fn enummemberval(en: *enumtype, mname: str, out: *u64) bool = {
let m: *enummember = en.members;
for (m != nil) {