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

@@ -14,8 +14,6 @@
static void cstmt(Checker*, Node*);
static Type *cexpr(Checker*, Node*);
static Type *resolve_type(Checker*, Node*);
static void check_module_shadow(Checker*, const char *name, Pos,
const char *kindstr);
static Type *
err(Checker *c, Pos p, const char *fmt, ...)
@@ -69,6 +67,7 @@ static int src_imports(Node *file, const char *modtag, int source,
static Sym *lookup_visible(Checker *c, const char *name);
static Sym *lookup_visible_type(Checker *c, const char *name);
static Sym *lookup_bare_import_binding(Checker *c, const char *name);
static int local_shadows_import(Checker *c, const char *name);
static int reject_bare_import_values(Checker *c, Node *n);
static int reject_bare_import_types(Checker *c, Node *n);
static void resolve_typedecl(Checker *c, Node *d);
@@ -660,6 +659,9 @@ eval_def_const(Checker *c, Node *n, u64 *out, int depth)
}
case N_DOT: {
if (n->lhs == NULL || n->lhs->kind != N_IDENT) return 0;
/* Preserve source-0 `.wwi` self-qualification while refusing to
* fold through a closer lexical value binding. */
if (local_shadows_import(c, n->lhs->str)) return 0;
/* M1 #22: map the qualifier alias to its dotted import path. */
const char *mk = use_path(c->file, c->cur_mod, c->cur_source,
n->lhs->str);
@@ -1618,6 +1620,10 @@ cexpr(Checker *c, Node *n)
return n->type = base;
}
}
if (n->lhs && n->lhs->kind == N_IDENT
&& local_shadows_import(c, n->lhs->str))
return n->type = err(c, n->pos,
"selector '%s' undefined", n->str);
return n->type = err(c, n->pos,
"no enum member '%s' in %s",
n->str ? n->str : "?",
@@ -1650,6 +1656,10 @@ cexpr(Checker *c, Node *n)
for (Tfield *f = u->fields; f; f = f->next)
if (strcmp(f->name, n->str) == 0)
return n->type = f->type;
if (n->lhs && n->lhs->kind == N_IDENT
&& local_shadows_import(c, n->lhs->str))
return n->type = err(c, n->pos,
"selector '%s' undefined", n->str);
return n->type = err(c, n->pos, "no field '%s' in %s",
n->str, type_name(c->a, base));
}
@@ -1660,17 +1670,32 @@ cexpr(Checker *c, Node *n)
if (*q < '0' || *q > '9') { idx = -1; break; }
idx = idx * 10 + (*q - '0');
}
if (idx < 0 && n->lhs && n->lhs->kind == N_IDENT
&& local_shadows_import(c, n->lhs->str))
return n->type = err(c, n->pos,
"selector '%s' undefined", n->str);
if (idx < 0)
return n->type = err(c, n->pos,
"tuple field must be numeric");
Tparam *tp = u->params;
while (idx > 0 && tp) { tp = tp->next; idx--; }
if (tp == NULL && n->lhs && n->lhs->kind == N_IDENT
&& local_shadows_import(c, n->lhs->str))
return n->type = err(c, n->pos,
"selector '%s' undefined", n->str);
if (tp == NULL)
return n->type = err(c, n->pos,
"tuple index out of range");
return n->type = tp->type;
}
/* module-qualified: lhs is IDENT bound as SK_USE */
/* The lexical-shadow slice makes a same-spelled local receiver
* reachable here. Diagnose that invalid local selector, but retain
* the existing lenient fallback for non-identifier extensions such
* as the codegen-supported untyped string-literal `.ptr` form. */
if (n->lhs && n->lhs->kind == N_IDENT
&& local_shadows_import(c, n->lhs->str))
return n->type = err(c, n->pos,
"selector '%s' undefined", n->str);
return n->type = ty_err;
}
case N_INDEX: {
@@ -2308,8 +2333,6 @@ cexpr(Checker *c, Node *n)
type_name(c->a, st));
}
if (cs->str && cs->str[0]) {
check_module_shadow(c, cs->str,
cs->pos, "binding");
scope_define(c->cur, cs->str, SK_VAR, vt, cs);
}
}
@@ -2672,7 +2695,6 @@ clet(Checker *c, Node *n)
desugar_arrayslice(c, declared, n->rhs);
n->type = t;
if (n->str && n->str[0]) {
check_module_shadow(c, n->str, n->pos, "let");
Sym *s = scope_define(c->cur, n->str, SK_VAR, t, n);
if (s == NULL)
err(c, n->pos, "let '%s' redeclared in same scope",
@@ -2753,8 +2775,6 @@ cstmt(Checker *c, Node *n)
for (Node *nm = n->list; nm; nm = nm->next) {
Type *ft = tp ? tp->type : ty_err;
if (nm->str && nm->str[0]) {
check_module_shadow(c, nm->str,
nm->pos, "binding");
if (scope_define(c->cur, nm->str,
SK_VAR, ft, nm) == NULL)
err(c, nm->pos,
@@ -2764,7 +2784,6 @@ cstmt(Checker *c, Node *n)
if (tp) tp = tp->next;
}
} else if (n->str && n->str[0]) {
check_module_shadow(c, n->str, n->pos, "binding");
scope_define(c->cur, n->str, SK_VAR,
elem ? elem : ty_err, n);
}
@@ -2809,6 +2828,9 @@ cstmt(Checker *c, Node *n)
type_name(c->a, rt));
}
Tparam *tp = u ? u->params : NULL;
/* Like a Go VarSpec/ShortVarDecl, every declared type belongs to
* the declaration header: resolve and check all of them before any
* name enters the enclosing lexical scope. */
for (Node *l = n->list; l; l = l->next) {
Type *declared = l->lhs ? resolve_type(c, l->lhs) : NULL;
Type *elem = tp ? tp->type : NULL;
@@ -2819,16 +2841,18 @@ cstmt(Checker *c, Node *n)
l->str, type_name(c->a, declared),
type_name(c->a, elem));
l->type = t;
if (tp) tp = tp->next;
}
for (Node *l = n->list; l; l = l->next) {
if (l->str && l->str[0]) {
check_module_shadow(c, l->str, l->pos, "let");
Sym *s = scope_define(c->cur, l->str, SK_VAR, t, l);
Sym *s = scope_define(c->cur, l->str, SK_VAR,
l->type, l);
if (s == NULL)
err(c, l->pos,
"let '%s' redeclared in same scope",
l->str);
else if (n->op == TK_CONST) s->is_const = 1;
}
if (tp) tp = tp->next;
}
if (u && tp != NULL)
err(c, n->pos, "tuple has extra elements");
@@ -3161,6 +3185,23 @@ lookup_bare_import_binding(Checker *c, const char *name)
return NULL;
}
/* A package-name object remains in the file scope when a closer lexical
* value binding wins lookup. This predicate is intentionally narrower than
* ordinary invalid-selector checking: before lexical import shadowing became
* legal, only this newly reachable path was hidden by the shadow prohibition. */
static int
local_shadows_import(Checker *c, const char *name)
{
if (c == NULL || name == NULL || name[0] == '\0') return 0;
if (!src_imports(c->file, c->cur_mod, c->cur_source, name)) return 0;
for (Scope *s = c->cur; s && s != c->top; s = s->parent) {
Sym *r = scope_lookup_local(s, name);
if (r != NULL)
return r->kind == SK_VAR || r->kind == SK_PARAM;
}
return 0;
}
/* Walk a type subtree only far enough to classify package-name objects. This
* is deliberately not general type checking: the enum/array constant folders
* need the package diagnostic before they reduce an unsupported outer shape
@@ -3301,45 +3342,6 @@ lookup_visible_type(Checker *c, const char *name)
return NULL;
}
/*
* check_module_shadow — refuse value bindings that shadow an
* in-scope imported module bareword. "Value names and module names
* are disjoint": a fn param / let / mcase binding named `fmt` while
* the declaring source carries `use fmt;` would silently miscompile
* any `fmt.X` body lookup through the shadow's value bits (the
* cstage cexpr N_DOT path resolves the inner ident as the shadow
* and emits CALL through its bytes — task #19).
*
* Scope:
* - Fires only for nested-scope binds (c->cur != c->top). Same-leaf
* top-level decls (`use foo; fn foo(...)`) are intentional and
* handled by the SK_USE→SK_X promotion path with use_alias=1.
* - Filters by the declaring source's own use directives. lib/fmt's
* `fn fprintf(fmt: str, ...)` is fine because lib/fmt doesn't
* import itself.
* - Walks every scope (not just innermost) so a deeper shadow that
* happens to mask the SK_USE entry can't suppress the check.
*/
static void
check_module_shadow(Checker *c, const char *name, Pos pos,
const char *kindstr)
{
if (name == NULL || name[0] == '\0') return;
if (c == NULL || c->cur == c->top) return;
int seen_use = 0;
for (Scope *s = c->cur; s; s = s->parent) {
Sym *r = scope_lookup_local(s, name);
if (r && (r->kind == SK_USE || r->use_alias)) {
seen_use = 1;
break;
}
}
if (!seen_use) return;
if (!src_imports(c->file, c->cur_mod, c->cur_source, name)) return;
err(c, pos, "%s '%s' shadows imported module '%s'",
kindstr, name, name);
}
/* Self-contained `.wwi` files can carry the same origin-owned type/const
* fact through two direct dependencies (a diamond), or alongside a direct
* import of that origin. In strict package mode those compiler-generated
@@ -4148,23 +4150,116 @@ reject_nonfunction_main_decls(Checker *c, Node *file)
}
}
/* Import usage is a property of the file-local qualifier occurrence. Record
* qualified syntax before resolving declaration bodies so import diagnostics
* retain production Go's source order without making a failed bare lookup a
* use. WW already rejects lexical bindings that shadow an import qualifier. */
/* Import diagnostics precede body diagnostics, so usage must be known before
* pass 1. The temporary scopes mirror the real declaration points without
* contaminating the compilation scope that pass 1 owns. */
static void
mark_import_uses_node(Checker *c, Node *n, const char *owner, int source)
{
if (n == NULL) return;
if (n->kind == N_TNAME && n->str != NULL) {
switch (n->kind) {
case N_TNAME: {
if (n->str == NULL) return;
const char *dot = strrchr(n->str, '.');
if (dot != NULL) {
char *head = astrndup(c->a, n->str, (size_t)(dot - n->str));
(void)find_use_path(c->file, owner, source, head, 1);
if (scope_lookup(c->cur, head) == NULL)
(void)find_use_path(c->file, owner, source, head, 1);
}
} else if (n->kind == N_DOT && n->lhs != NULL
&& n->lhs->kind == N_IDENT && n->lhs->str != NULL) {
(void)find_use_path(c->file, owner, source, n->lhs->str, 1);
return;
}
case N_FNDECL: {
for (Node *p = n->attr; p; p = p->next)
mark_import_uses_node(c, p, owner, source);
mark_import_uses_node(c, n->lhs, owner, source);
for (Node *p = n->list; p; p = p->next)
mark_import_uses_node(c, p->lhs, owner, source);
Scope *saved = c->cur;
c->cur = newscope(c->a, saved);
for (Node *p = n->list; p; p = p->next)
if (p->str && p->str[0])
(void)scope_define(c->cur, p->str, SK_PARAM, NULL, p);
mark_import_uses_node(c, n->body, owner, source);
c->cur = saved;
return;
}
case N_BLOCK: {
Scope *saved = c->cur;
c->cur = newscope(c->a, saved);
for (Node *p = n->list; p; p = p->next)
mark_import_uses_node(c, p, owner, source);
c->cur = saved;
return;
}
case N_LET:
for (Node *p = n->attr; p; p = p->next)
mark_import_uses_node(c, p, owner, source);
mark_import_uses_node(c, n->lhs, owner, source);
mark_import_uses_node(c, n->rhs, owner, source);
if (n->str && n->str[0])
(void)scope_define(c->cur, n->str, SK_VAR, NULL, n);
return;
case N_MLET:
for (Node *p = n->attr; p; p = p->next)
mark_import_uses_node(c, p, owner, source);
mark_import_uses_node(c, n->rhs, owner, source);
for (Node *p = n->list; p; p = p->next)
mark_import_uses_node(c, p->lhs, owner, source);
for (Node *p = n->list; p; p = p->next)
if (p->str && p->str[0])
(void)scope_define(c->cur, p->str, SK_VAR, NULL, p);
return;
case N_FOR: {
Scope *saved = c->cur;
c->cur = newscope(c->a, saved);
mark_import_uses_node(c, n->lhs, owner, source);
mark_import_uses_node(c, n->cond, owner, source);
mark_import_uses_node(c, n->rhs, owner, source);
mark_import_uses_node(c, n->body, owner, source);
mark_import_uses_node(c, n->els, owner, source);
c->cur = saved;
return;
}
case N_FORRANGE: {
Scope *saved = c->cur;
c->cur = newscope(c->a, saved);
mark_import_uses_node(c, n->lhs, owner, source);
if (n->list != NULL) {
for (Node *p = n->list; p; p = p->next)
if (p->str && p->str[0])
(void)scope_define(c->cur, p->str,
SK_VAR, NULL, p);
} else if (n->str && n->str[0]) {
(void)scope_define(c->cur, n->str, SK_VAR, NULL, n);
}
mark_import_uses_node(c, n->body, owner, source);
mark_import_uses_node(c, n->els, owner, source);
c->cur = saved;
return;
}
case N_MATCH:
mark_import_uses_node(c, n->lhs, owner, source);
for (Node *cs = n->list; cs; cs = cs->next) {
Scope *saved = c->cur;
c->cur = newscope(c->a, saved);
mark_import_uses_node(c, cs->lhs, owner, source);
for (Node *p = cs->list; p; p = p->next)
mark_import_uses_node(c, p, owner, source);
if (cs->str && cs->str[0])
(void)scope_define(c->cur, cs->str, SK_VAR, NULL, cs);
mark_import_uses_node(c, cs->body, owner, source);
c->cur = saved;
}
return;
case N_DOT:
if (n->lhs != NULL && n->lhs->kind == N_IDENT
&& n->lhs->str != NULL
&& scope_lookup(c->cur, n->lhs->str) == NULL)
(void)find_use_path(c->file, owner, source,
n->lhs->str, 1);
break;
default:
break;
}
for (Node *p = n->attr; p; p = p->next)
mark_import_uses_node(c, p, owner, source);
@@ -4180,10 +4275,13 @@ mark_import_uses_node(Checker *c, Node *n, const char *owner, int source)
static void
mark_import_uses(Checker *c, Node *file)
{
Scope *saved = c->cur;
for (Node *d = file->list; d; d = d->next) {
if (d->kind == N_USE) continue;
c->cur = newscope(c->a, NULL);
mark_import_uses_node(c, d, decl_mod(file, d), d->sourceid);
}
c->cur = saved;
}
static void
@@ -4882,8 +4980,6 @@ check_file(Checker *c, Node *file)
Type *fnt = d->type;
for (Tparam *p = fnt->params; p; p = p->next) {
if (p->name && p->name[0]) {
check_module_shadow(c, p->name,
d->pos, "param");
if (scope_define(c->cur, p->name,
SK_PARAM, p->type, d) == NULL)
err(c, d->pos,

View File

@@ -606,10 +606,8 @@ struct Checker {
* rather than colliding io.read. */
int cur_source; /* lexical source-file scope of the declaration
* currently being checked. */
Node *file; /* current N_FILE root; used by check_module_shadow
* to consult the declaring source file's own `use`
* directives when refusing param/let names that
* would shadow an imported module bareword. */
Node *file; /* current N_FILE root; owns source-local import
* qualifier lookup and usage accounting. */
int loops; /* nesting count for break/continue */
int matcharms; /* nesting count for yield */
int errs;