fix: allow lexical import shadowing

This commit is contained in:
2026-08-22 17:53:31 +09:00
parent 89f519d5cb
commit 6279d46652
19 changed files with 1420 additions and 270 deletions

View File

@@ -3642,13 +3642,16 @@ fn cgdot(c: *cgen, n: *syntax.node) void = {
// → 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.
// enumlookup's same-module-first walk. A live local with the same
// spelling wins before this name-only registry fallback; valid local
// enum selectors were already folded by the checker.
if (lhs != nil) {
let etname: str;
let etmod: str;
etname.ptr = nil; etname.len = 0;
etmod.ptr = nil; etmod.len = 0;
if (lhs.kind == syntax.nkind.N_IDENT) {
if (lhs.kind == syntax.nkind.N_IDENT
&& localfindnode(c, lhs.str) == nil) {
etname = lhs.str;
};
if (lhs.kind == syntax.nkind.N_DOT) {

View File

@@ -37,9 +37,8 @@ type checker = struct {
// preference in bare-leaf lookups.
cursource: i32, // lexical source-file scope of the current decl;
// selects only that file's import bindings.
file: *syntax.node, // N_FILE root; used by checkmoduleshadow
// to consult the declaring source's own
// `use` directives.
file: *syntax.node, // N_FILE root; owns source-local `use`
// bindings and their usage state.
allococtx: *syntax.node, // #3/B': the one empty alloc([], n) call node
// with let-declared slice context this walk;
// any other empty alloc has no element hint
@@ -224,6 +223,24 @@ fn usepathfor(file: *syntax.node, modtag: str, source: i32, alias: str) str = {
// modkeyfor — the module key for a directly imported alias. Empty means
// the referencing package did not itself declare that import.
fn modkeyfor(c: *checker, alias: str) str = {
let found: *syntax.sym = syntax.scopelookupprefer(c.cur, c.curmod, alias);
if (found != nil && found.scope != c.top
&& (found.skind == syntax.skind.SK_VAR
|| found.skind == syntax.skind.SK_PARAM)) {
let empty: str;
return empty;
};
// Imported interface declarations may qualify their own package from the
// source-0 section without a source import node. Preserve that bridge, but
// only after a closer lexical binding has been excluded above.
if (c.cursource == 0 && c.curmod.len != 0) {
let (prefix, suffix) = strings.rcut(c.curmod, ".");
let leaf: str = suffix;
if (leaf.len == 0) { leaf = c.curmod; };
if (syntax.streq(alias, leaf)) {
return usepathfor(c.file, c.curmod, c.cursource, alias);
};
};
return usepathfor(c.file, c.curmod, c.cursource, alias);
};
@@ -264,6 +281,23 @@ fn srcimports(file: *syntax.node, modtag: str, source: i32, name: str) bool = {
return false;
};
// A package-name object remains in the file scope when a closer lexical value
// wins lookup. Keep this predicate restricted to that newly reachable path so
// unrelated WW selector extensions retain their established diagnostics.
fn localshadowsimport(c: *checker, name: str) bool = {
if (!srcimports(c.file, c.curmod, c.cursource, name)) { return false; };
let s: *syntax.scope = c.cur;
for (s != nil && s != c.top) {
let found: *syntax.sym = syntax.scopelookuplocal(s, name);
if (found != nil) {
return found.skind == syntax.skind.SK_VAR
|| found.skind == syntax.skind.SK_PARAM;
};
s = s.parent;
};
return false;
};
fn lookupvisible(c: *checker, name: str) *syntax.sym = {
let found: *syntax.sym = syntax.scopelookupprefer(c.cur, c.curmod, name);
let builtin: *syntax.sym = nil;
@@ -398,39 +432,6 @@ fn synthesizedtestrun(c: *checker, e: *syntax.node) bool = {
return c.synthtestrun != nil && c.synthtestrun == e;
};
// checkmoduleshadow — enforce "value names and module names are
// disjoint" at nested-scope binds. Mirrors cstage check_module_shadow
// (cmd/wcc/check.c). Fires for fn params / lets / forrange iters /
// mcase bindings whose name matches an in-scope `use foo;` import
// declared in the same source file. Top-level decls are exempt
// (their same-leaf-as-module pattern is the intentional coexistence
// shape — `use fnmatch; fn fnmatch(...)` etc.).
fn checkmoduleshadow(c: *checker, name: str, kindstr: str) void = {
if (name.len == 0) { return; };
if (c.cur == c.top) { return; };
let seen: bool = false;
let s: *syntax.scope = c.cur;
for (s != nil) {
let r: *syntax.sym = syntax.scopelookuplocal(s, name);
if (r != nil) {
if (r.skind == syntax.skind.SK_USE) {
seen = true;
s = nil;
};
};
if (s != nil) { s = s.parent; };
};
if (!seen) { return; };
if (!srcimports(c.file, c.curmod, c.cursource, name)) { return; };
cerr(kindstr);
cerr(" '");
cerr(name);
cerr("' shadows imported module '");
cerr(name);
cerr("'\n");
c.errs += 1;
};
// installdecl — install the top-level decl's name into the top scope.
// We don't compute its type yet (that's the resolve pass) — just bind
// the name so forward references resolve.
@@ -593,9 +594,11 @@ fn installtop(c: *checker, d: *syntax.node, nm: str, mod: str, k: syntax.skind,
// `let (a,b) = f()`, `for (let (a,b) .. s)`, and the ww-extension
// multi-assign `a, _ = f()`.
//
// `define` (the binding contexts: let-unpack + for-range) installs each
// named binder as a fresh SK_VAR and back-fills its declared type onto
// .lhs so use sites resolve through the N_IDENT exprtype path. Multi-
// `define` (the binding contexts: let-unpack + for-range) back-fills and
// resolves every declared type before installing any named binder as a fresh
// SK_VAR. This gives the whole declaration header the outer lexical scope,
// matching the Cstage twin and Go's VarSpec/ShortVarDecl declaration point.
// Multi-
// assign targets are pre-declared lvalues, so it passes false: .lhs is
// left untouched (an N_INDEX/N_DOT target carries a live operand there)
// and only the type_ stamp fires on the still-untyped slots.
@@ -614,23 +617,6 @@ fn stamptuplebinds(c: *checker, binds: *syntax.node, elems: *syntax.node,
if (pt != nil) { et = pt.lhs; };
if (define) {
if (b.lhs == nil) { b.lhs = et; };
let bnm: str = b.str;
if (bnm.len > 0) {
checkmoduleshadow(c, bnm, what);
// first registration wins; a nil return is a
// same-scope duplicate — `let (a, a) = ..` /
// `for (let (a, a) .. xs)`. Mirrors cstage
// scope_define == NULL → "redeclared".
let ds: *syntax.sym = syntax.scopedefine(c.cur, bnm, syntax.skind.SK_VAR, nil, b);
if (ds == nil) {
cerr("error: ");
cerr(what);
cerr(" '");
cerr(bnm);
cerr("' redeclared in same scope\n");
c.errs += 1;
};
};
};
if (b.type_ == nil) {
let src: *syntax.node = b.lhs;
@@ -643,6 +629,27 @@ fn stamptuplebinds(c: *checker, binds: *syntax.node, elems: *syntax.node,
b = b.next;
if (pt != nil) { pt = pt.next; };
};
if (define) {
b = binds;
for (b != nil) {
let bnm: str = b.str;
if (bnm.len > 0) {
// First registration wins; a nil return is a same-scope
// duplicate within this declaration.
let ds: *syntax.sym = syntax.scopedefine(c.cur, bnm,
syntax.skind.SK_VAR, nil, b);
if (ds == nil) {
cerr("error: ");
cerr(what);
cerr(" '");
cerr(bnm);
cerr("' redeclared in same scope\n");
c.errs += 1;
};
};
b = b.next;
};
};
};
// resolvewalk — recursive AST walk that, for every nkind.N_IDENT and
@@ -818,7 +825,6 @@ fn resolvewalk(c: *checker, n: *syntax.node) void = {
} else {
let bnm: str = n.str;
if (bnm.len > 0) {
checkmoduleshadow(c, bnm, "binding");
// C4 (task #7): bind the ELEMENT type so field
// reads off a by-value aggregate binding
// (`for (let t .. threads) { t.pc }`) resolve —
@@ -876,11 +882,13 @@ fn resolvewalk(c: *checker, n: *syntax.node) void = {
};
// `for (init; cond; post) body` / `for (cond) body` — the body is the
// break/continue target. Walk init/cond/post outside the loop count
// (they hold no statements), bump only around the body, and keep the
// `else` outside (it targets an enclosing loop, like N_FORRANGE above).
// break/continue target. Its initializer scope covers cond/post/body/else;
// only the body is inside the loop count, so `else` still targets an
// enclosing loop.
// Mirrors cstage cmd/wcc/check.c:2529 (N_FOR, c->loops++).
if (k == syntax.nkind.N_FOR) {
let forouter: *syntax.scope = c.cur;
c.cur = syntax.newscope(forouter);
if (n.lhs != nil) { resolvewalk(c, n.lhs); };
if (n.cond != nil) { resolvewalk(c, n.cond); };
if (n.rhs != nil) { resolvewalk(c, n.rhs); };
@@ -888,6 +896,7 @@ fn resolvewalk(c: *checker, n: *syntax.node) void = {
if (n.body != nil) { resolvewalk(c, n.body); };
c.loops -= 1;
if (n.els != nil) { resolvewalk(c, n.els); };
c.cur = forouter;
return;
};
@@ -930,7 +939,6 @@ fn resolvewalk(c: *checker, n: *syntax.node) void = {
c.cur = syntax.newscope(outer);
let nm: str = n.str;
if (nm.len > 0) {
checkmoduleshadow(c, nm, "binding");
syntax.scopedefine(c.cur, nm, syntax.skind.SK_VAR, nil, n);
};
if (n.body != nil) {
@@ -997,8 +1005,6 @@ fn resolvewalk(c: *checker, n: *syntax.node) void = {
// the un-annotated binder stays untyped and asserttyped aborts.
// Mirrors cstage check.c:2017 (cexpr(rhs), unconditional).
if (n.rhs != nil) {
// `rt` would shadow the imported lib/rt module
// (checkmoduleshadow errors); `rty` avoids it.
// #99 alias transparency: a NAMED tuple alias rhs
// (`type pair = (i64,i64)`) destructures like its
// base — resolvealias mirrors cstage's
@@ -1158,7 +1164,6 @@ fn resolvewalk(c: *checker, n: *syntax.node) void = {
if (k == syntax.nkind.N_LET) {
let nm: str = n.str;
if (nm.len > 0) {
checkmoduleshadow(c, nm, "let");
let s: *syntax.sym = syntax.scopedefine(c.cur, nm, syntax.skind.SK_VAR, nil, n);
// catB-22: flag a `const` binding so checkassign can
// reject a later reassignment. The parser stamps
@@ -4868,8 +4873,7 @@ fn exprtype(c: *checker, e: *syntax.node, hint: *syntax.node) *syntax.node = {
};
}; };
// A.6.1.5b stamp cases — mirror cstage check.c:833-866. Pure
// type-AST stamps; never rewrite e.kind. Lenient on misses
// (cstage errors); falls through to nil under scruttype L656.
// type-AST stamps; never rewrite e.kind.
//
// Pseudo-fields .len/.cap/.ptr on slice/str/array. Cstage
// L833-842. `str` lives as N_TNAME("str") in wwstage — no
@@ -4938,6 +4942,23 @@ fn exprtype(c: *checker, e: *syntax.node, hint: *syntax.node) *syntax.node = {
};
};
}; };
// A same-spelled lexical receiver that fails every valid local
// selector shape is the newly reachable shadowing error path. Stamp
// recovery so asserttyped and cgen cannot replace the causal,
// positioned diagnostic. Missing struct/tuple/enum members use this
// same shadow-specific spelling in both compiler stages.
if (lhsn != nil && lhsn.kind == syntax.nkind.N_IDENT
&& localshadowsimport(c, lhsn.str)) {
// Cstage's N_DOT position is the receiver start; use the
// identifier position rather than WW parser's dot token.
cerr(lhsn.file); cerr(":");
cerr(strconv.i32tos(lhsn.line, strconv.base.DEC)); cerr(":");
cerr(strconv.i32tos(lhsn.col, strconv.base.DEC));
cerr(": error: selector '"); cerr(e.str);
cerr("' undefined\n");
c.errs += 1;
e.type_ = c.tc.tyerr: *void;
};
};
return nil;
};
@@ -7631,7 +7652,6 @@ fn installparams(c: *checker, params: *syntax.node) void = {
normalizevariadicparam(p);
let nm: str = p.str;
if (nm.len > 0) {
checkmoduleshadow(c, nm, "param");
syntax.scopedefine(c.cur, nm, syntax.skind.SK_PARAM, nil, p);
};
};
@@ -7846,39 +7866,195 @@ fn topdeclkind(d: *syntax.node) bool = {
|| d.kind == syntax.nkind.N_LET);
};
// Record only qualified syntax in each owning source before name resolution.
// This preserves source-position import diagnostics and never lets a failed
// bare lookup consume an ordinary import.
fn markimportusesnode(c: *checker, n: *syntax.node, owner: str,
source: i32) void = {
// Mark only a qualifier whose spelling still denotes the source-local package
// binding. The scratch scope contains lexical binders only; package and import
// symbols remain owned by the real checker scope.
fn markimportqualifier(c: *checker, scope: *syntax.scope, owner: str,
source: i32, alias: str) void = {
if (alias.len == 0) { return; };
if (syntax.scopelookup(scope, alias) != nil) { return; };
if (!srcimports(c.file, owner, source, alias)) { return; };
let marked: str = usepathfor(c.file, owner, source, alias);
};
fn markimportusesnode(c: *checker, n: *syntax.node, scope: *syntax.scope,
owner: str, source: i32, top: bool) void = {
if (n == nil) { return; };
if (n.kind == syntax.nkind.N_TNAME) {
// strings.rcut returns the whole input as its head on a miss. Require
// a real qualifier separator so a bare package TNAME never counts as use.
let k: syntax.nkind = n.kind;
if (k == syntax.nkind.N_USE) { return; };
if (k == syntax.nkind.N_TNAME) {
if (strings.contains(n.str, ".")) {
let (head, leaf) = strings.rcut(n.str, ".");
findusepath(c.file, owner, source, head, true);
markimportqualifier(c, scope, owner, source, head);
};
} else { if (n.kind == syntax.nkind.N_DOT && n.lhs != nil
&& n.lhs.kind == syntax.nkind.N_IDENT) {
findusepath(c.file, owner, source, n.lhs.str, true);
}; };
return;
};
if (k == syntax.nkind.N_DOT) {
if (n.lhs != nil && n.lhs.kind == syntax.nkind.N_IDENT) {
markimportqualifier(c, scope, owner, source, n.lhs.str);
} else {
markimportusesnode(c, n.lhs, scope, owner, source, false);
};
return;
};
if (k == syntax.nkind.N_FNDECL) {
let p: *syntax.node = n.attr;
for (p != nil) {
markimportusesnode(c, p, scope, owner, source, false);
p = p.next;
};
markimportusesnode(c, n.lhs, scope, owner, source, false);
p = n.list;
for (p != nil) {
let a: *syntax.node = p.attr;
for (a != nil) {
markimportusesnode(c, a, scope, owner, source, false);
a = a.next;
};
markimportusesnode(c, p.lhs, scope, owner, source, false);
markimportusesnode(c, p.rhs, scope, owner, source, false);
p = p.next;
};
let fnscope: *syntax.scope = syntax.newscope(scope);
p = n.list;
for (p != nil) {
if (p.kind == syntax.nkind.N_PARAM && p.str.len > 0) {
syntax.scopedefine(fnscope, p.str,
syntax.skind.SK_PARAM, nil, p);
};
p = p.next;
};
markimportusesnode(c, n.body, fnscope, owner, source, false);
return;
};
if (k == syntax.nkind.N_BLOCK) {
let inner: *syntax.scope = syntax.newscope(scope);
let p: *syntax.node = n.list;
for (p != nil) {
markimportusesnode(c, p, inner, owner, source, false);
p = p.next;
};
return;
};
if (k == syntax.nkind.N_LET) {
let p: *syntax.node = n.attr;
for (p != nil) {
markimportusesnode(c, p, scope, owner, source, false);
p = p.next;
};
markimportusesnode(c, n.lhs, scope, owner, source, false);
markimportusesnode(c, n.rhs, scope, owner, source, false);
if (!top && n.str.len > 0) {
syntax.scopedefine(scope, n.str, syntax.skind.SK_VAR, nil, n);
};
return;
};
if (k == syntax.nkind.N_MLET) {
let a: *syntax.node = n.attr;
for (a != nil) {
markimportusesnode(c, a, scope, owner, source, false);
a = a.next;
};
markimportusesnode(c, n.rhs, scope, owner, source, false);
let p: *syntax.node = n.list;
for (p != nil) {
a = p.attr;
for (a != nil) {
markimportusesnode(c, a, scope, owner, source, false);
a = a.next;
};
markimportusesnode(c, p.lhs, scope, owner, source, false);
p = p.next;
};
p = n.list;
for (p != nil) {
if (p.str.len > 0) {
syntax.scopedefine(scope, p.str, syntax.skind.SK_VAR, nil, p);
};
p = p.next;
};
return;
};
if (k == syntax.nkind.N_FOR) {
let inner: *syntax.scope = syntax.newscope(scope);
markimportusesnode(c, n.lhs, inner, owner, source, false);
markimportusesnode(c, n.cond, inner, owner, source, false);
markimportusesnode(c, n.rhs, inner, owner, source, false);
markimportusesnode(c, n.body, inner, owner, source, false);
markimportusesnode(c, n.els, inner, owner, source, false);
return;
};
if (k == syntax.nkind.N_FORRANGE) {
markimportusesnode(c, n.lhs, scope, owner, source, false);
let inner: *syntax.scope = syntax.newscope(scope);
let p: *syntax.node = n.list;
for (p != nil) {
let a: *syntax.node = p.attr;
for (a != nil) {
markimportusesnode(c, a, scope, owner, source, false);
a = a.next;
};
markimportusesnode(c, p.lhs, scope, owner, source, false);
if (p.str.len > 0) {
syntax.scopedefine(inner, p.str, syntax.skind.SK_VAR, nil, p);
};
p = p.next;
};
if (n.list == nil && n.str.len > 0) {
syntax.scopedefine(inner, n.str, syntax.skind.SK_VAR, nil, n);
};
markimportusesnode(c, n.body, inner, owner, source, false);
markimportusesnode(c, n.els, scope, owner, source, false);
return;
};
if (k == syntax.nkind.N_MCASE) {
markimportusesnode(c, n.lhs, scope, owner, source, false);
let p: *syntax.node = n.list;
for (p != nil) {
markimportusesnode(c, p, scope, owner, source, false);
p = p.next;
};
let inner: *syntax.scope = syntax.newscope(scope);
if (n.str.len > 0) {
syntax.scopedefine(inner, n.str, syntax.skind.SK_VAR, nil, n);
};
markimportusesnode(c, n.body, inner, owner, source, false);
return;
};
let p: *syntax.node = n.attr;
for (p != nil) { markimportusesnode(c, p, owner, source); p = p.next; };
markimportusesnode(c, n.lhs, owner, source);
markimportusesnode(c, n.rhs, owner, source);
markimportusesnode(c, n.cond, owner, source);
markimportusesnode(c, n.body, owner, source);
markimportusesnode(c, n.els, owner, source);
for (p != nil) {
markimportusesnode(c, p, scope, owner, source, false);
p = p.next;
};
markimportusesnode(c, n.lhs, scope, owner, source, false);
markimportusesnode(c, n.rhs, scope, owner, source, false);
markimportusesnode(c, n.cond, scope, owner, source, false);
markimportusesnode(c, n.body, scope, owner, source, false);
markimportusesnode(c, n.els, scope, owner, source, false);
p = n.list;
for (p != nil) { markimportusesnode(c, p, owner, source); p = p.next; };
for (p != nil) {
markimportusesnode(c, p, scope, owner, source, false);
p = p.next;
};
};
fn markimportuses(c: *checker, file: *syntax.node) void = {
let d: *syntax.node = file.list;
for (d != nil) {
if (d.kind != syntax.nkind.N_USE) {
markimportusesnode(c, d, declmod(file, d), d.sourceid);
let scope: *syntax.scope = syntax.newscope(nil);
markimportusesnode(c, d, scope, declmod(file, d),
d.sourceid, true);
};
d = d.next;
};