selfhost/cmd/wcc/check: same-module preference on N_IDENT callee lookup

exprtype's N_CALL callee resolution used flat scopelookup, returning
the first match in the bucket regardless of caller module. Two
modules exporting fns with the same leaf name (e.g. alpha.foo i64
+ beta.foo str) caused bare-leaf callees inside one of them to pick
the other's fn, then false-positive at return type.

Cstage cexpr N_IDENT routes through scope_lookup_prefer(c->cur,
c->cur_mod, name) which short-circuits to the same-module hit
before falling through to flat scope. Mirror at check.ww:675 —
splits N_IDENT vs N_DOT so the latter keeps flat scopelookup and
the explicit module qualifier path stays distinct (tracked as #58).

c.curmod is already tracked by checkfile pass 2 (check.ww:1240-1241),
so this is a one-call swap on the N_IDENT branch. No plumbing.

Reviewer cascade probe across all 131 .ww/.combined.ww files in
lib/ + selfhost/ shows lib/memio/memiotest.combined.ww drops 4
spurious "let: not assignable" lines as a side effect, with no new
errors. Net improvement.
This commit is contained in:
2026-05-20 04:42:50 +09:00
parent 9fed4ca492
commit 3fa5ccd5ae
3 changed files with 42 additions and 3 deletions

View File

@@ -7666,7 +7666,20 @@ fn exprtype(c: *checker, e: *node) *node = {
if (callee.kind == nkind.N_IDENT) { nm = callee.str; }; if (callee.kind == nkind.N_IDENT) { nm = callee.str; };
if (callee.kind == nkind.N_DOT) { nm = callee.str; }; if (callee.kind == nkind.N_DOT) { nm = callee.str; };
if (nm.len == 0) { return nil; }; if (nm.len == 0) { return nil; };
let s: *sym = scopelookup(c.cur, nm); // #56: bare-leaf N_IDENT calls go through scopelookupprefer so
// `foo()` inside module M binds to M.foo rather than another
// module's same-leaf foo at the head of the flat scope bucket.
// Mirrors cstage cexpr N_IDENT routing through
// scope_lookup_prefer with c->cur_mod. N_DOT keeps the bare
// scopelookup — its module-qualified resolution is a separate
// gap (parser stores the leaf in callee.str; mod is in
// callee.lhs.str, not consumed here yet).
let s: *sym = nil;
if (callee.kind == nkind.N_IDENT) {
s = scopelookupprefer(c.cur, c.curmod, nm);
} else {
s = scopelookup(c.cur, nm);
};
if (s == nil) { return nil; }; if (s == nil) { return nil; };
if (s.skind != skind.SK_FN) { return nil; }; if (s.skind != skind.SK_FN) { return nil; };
if (s.decl == nil) { return nil; }; if (s.decl == nil) { return nil; };

View File

@@ -672,7 +672,20 @@ fn exprtype(c: *checker, e: *node) *node = {
if (callee.kind == nkind.N_IDENT) { nm = callee.str; }; if (callee.kind == nkind.N_IDENT) { nm = callee.str; };
if (callee.kind == nkind.N_DOT) { nm = callee.str; }; if (callee.kind == nkind.N_DOT) { nm = callee.str; };
if (nm.len == 0) { return nil; }; if (nm.len == 0) { return nil; };
let s: *sym = scopelookup(c.cur, nm); // #56: bare-leaf N_IDENT calls go through scopelookupprefer so
// `foo()` inside module M binds to M.foo rather than another
// module's same-leaf foo at the head of the flat scope bucket.
// Mirrors cstage cexpr N_IDENT routing through
// scope_lookup_prefer with c->cur_mod. N_DOT keeps the bare
// scopelookup — its module-qualified resolution is a separate
// gap (parser stores the leaf in callee.str; mod is in
// callee.lhs.str, not consumed here yet).
let s: *sym = nil;
if (callee.kind == nkind.N_IDENT) {
s = scopelookupprefer(c.cur, c.curmod, nm);
} else {
s = scopelookup(c.cur, nm);
};
if (s == nil) { return nil; }; if (s == nil) { return nil; };
if (s.skind != skind.SK_FN) { return nil; }; if (s.skind != skind.SK_FN) { return nil; };
if (s.decl == nil) { return nil; }; if (s.decl == nil) { return nil; };

View File

@@ -7666,7 +7666,20 @@ fn exprtype(c: *checker, e: *node) *node = {
if (callee.kind == nkind.N_IDENT) { nm = callee.str; }; if (callee.kind == nkind.N_IDENT) { nm = callee.str; };
if (callee.kind == nkind.N_DOT) { nm = callee.str; }; if (callee.kind == nkind.N_DOT) { nm = callee.str; };
if (nm.len == 0) { return nil; }; if (nm.len == 0) { return nil; };
let s: *sym = scopelookup(c.cur, nm); // #56: bare-leaf N_IDENT calls go through scopelookupprefer so
// `foo()` inside module M binds to M.foo rather than another
// module's same-leaf foo at the head of the flat scope bucket.
// Mirrors cstage cexpr N_IDENT routing through
// scope_lookup_prefer with c->cur_mod. N_DOT keeps the bare
// scopelookup — its module-qualified resolution is a separate
// gap (parser stores the leaf in callee.str; mod is in
// callee.lhs.str, not consumed here yet).
let s: *sym = nil;
if (callee.kind == nkind.N_IDENT) {
s = scopelookupprefer(c.cur, c.curmod, nm);
} else {
s = scopelookup(c.cur, nm);
};
if (s == nil) { return nil; }; if (s == nil) { return nil; };
if (s.skind != skind.SK_FN) { return nil; }; if (s.skind != skind.SK_FN) { return nil; };
if (s.decl == nil) { return nil; }; if (s.decl == nil) { return nil; };