fix: allow lexical import shadowing
This commit is contained in:
220
cmd/wcc/check.c
220
cmd/wcc/check.c
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user