From 3fa5ccd5aefd0aa2d889e1ad03d59ad99ef17728 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Wed, 20 May 2026 04:42:50 +0900 Subject: [PATCH] selfhost/cmd/wcc/check: same-module preference on N_IDENT callee lookup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- selfhost/cmd/w6c/main.combined.ww | 15 ++++++++++++++- selfhost/cmd/wcc/check.ww | 15 ++++++++++++++- selfhost/cmd/wwdump/main.combined.ww | 15 ++++++++++++++- 3 files changed, 42 insertions(+), 3 deletions(-) diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index c1039f5e..d448cd45 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -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_DOT) { nm = callee.str; }; 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.skind != skind.SK_FN) { return nil; }; if (s.decl == nil) { return nil; }; diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index 3ded9f30..76a7712f 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -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_DOT) { nm = callee.str; }; 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.skind != skind.SK_FN) { return nil; }; if (s.decl == nil) { return nil; }; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 5b0ae1b4..c773f9c7 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -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_DOT) { nm = callee.str; }; 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.skind != skind.SK_FN) { return nil; }; if (s.decl == nil) { return nil; };