cstage+selfhost+test: refuse let/param shadow of imported module (#19)
When `use fmt;` is in scope and a local/param named `fmt` shadows it, `fmt.X` in the body silently resolved to the str-typed value sym and emitted `CALL AX` through str.ptr → runtime crash. Surfaced during #15 (lib/log's printfln family); worked around by renaming the param `fmt`→`format`. Per rob + user, option (C): "value names and module names are disjoint." Refuse the shadow at the decl site. Single rule, no non-local reasoning, no silent footgun if a future lib/X exports a new leaf. cstage: src_imports walks file->list for N_USE entries (skipping self-imports where u->module == u->str — same-module fixtures like lib/fmt/fmttest.ww carry these); check_module_shadow runs before each SK_PARAM / SK_VAR scope_define (param, clet, mlet, forrange single + tuple, mcase). Wwstage mirror in check.ww; wwdump-only diagnostic today, full enforcement waits on #11 checkfile pass. Bootstrap byte-id holds — no codegen change. One source patch in selfhost/cmd/w6a/main.ww renames an outer `let asm: asm_;` to `s` to sidestep task #27 (cstage localoff scope-blind dedup); unrelated to #19 but the new rule's first run flagged it as a self-shadow. Test 708 (param_shadow_mod): 4 rows — neg_param (param shadow errs at fn decl line), neg_let (let shadow errs at let decl), pos_rename (rename compiles + runs), pos_selfimp (in-module use is skipped). 4 wired sites without dedicated rows deferred to task #28. Follow-up: lib/log can revert format→fmt now that the silent crash is impossible.
This commit is contained in:
101
cmd/wcc/check.c
101
cmd/wcc/check.c
@@ -14,6 +14,8 @@
|
||||
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, ...)
|
||||
@@ -1217,8 +1219,11 @@ cexpr(Checker *c, Node *n)
|
||||
type_name(c->a, alt->type),
|
||||
type_name(c->a, st));
|
||||
}
|
||||
if (cs->str && cs->str[0])
|
||||
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);
|
||||
}
|
||||
}
|
||||
cstmt(c, cs->body);
|
||||
c->cur = saved;
|
||||
@@ -1434,6 +1439,7 @@ clet(Checker *c, Node *n)
|
||||
type_name(c->a, initt), type_name(c->a, declared));
|
||||
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 && n->op == TK_CONST) s->is_const = 1;
|
||||
}
|
||||
@@ -1493,12 +1499,16 @@ cstmt(Checker *c, Node *n)
|
||||
Tparam *tp = (etu && etu->kind == TY_TUPLE) ? etu->params : NULL;
|
||||
for (Node *nm = n->list; nm; nm = nm->next) {
|
||||
Type *ft = tp ? tp->type : ty_err;
|
||||
if (nm->str && nm->str[0])
|
||||
if (nm->str && nm->str[0]) {
|
||||
check_module_shadow(c, nm->str,
|
||||
nm->pos, "binding");
|
||||
scope_define(c->cur, nm->str,
|
||||
SK_VAR, ft, nm);
|
||||
}
|
||||
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);
|
||||
}
|
||||
@@ -1548,6 +1558,7 @@ cstmt(Checker *c, Node *n)
|
||||
type_name(c->a, declared));
|
||||
l->type = t;
|
||||
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);
|
||||
if (s && n->op == TK_CONST) s->is_const = 1;
|
||||
}
|
||||
@@ -1667,10 +1678,88 @@ decl_mod(Node *file, Node *d)
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/*
|
||||
* src_imports — does the source file that contributed decl-module
|
||||
* `modtag` carry `use <name>;` somewhere? With driver concatenation
|
||||
* the combined N_FILE collects N_USE nodes from every contributing
|
||||
* source; each carries its origin module tag on n->module. Filter
|
||||
* by `modtag` so lib/log's `use fmt;` only colours decls whose
|
||||
* d->module == "log", not lib/fmt's own decls.
|
||||
*
|
||||
* modtag == NULL → primary compilation unit's own use directives
|
||||
* (N_USE nodes with module == NULL).
|
||||
*/
|
||||
static int
|
||||
src_imports(Node *file, const char *modtag, const char *name)
|
||||
{
|
||||
if (file == NULL || name == NULL || name[0] == '\0') return 0;
|
||||
for (Node *u = file->list; u; u = u->next) {
|
||||
if (u->kind != N_USE) continue;
|
||||
/* Skip self-imports: lib/fmt/fmttest.ww carries `use fmt;`
|
||||
* even though its module tag is also "fmt"; that directive
|
||||
* doesn't introduce a foreign module bareword and lib/fmt's
|
||||
* own `fn bsprintf(fmt: str, ...)` is not a shadow of it. */
|
||||
if (u->module && u->str && strcmp(u->module, u->str) == 0)
|
||||
continue;
|
||||
/* decl_mod normalises the raw `// MODULE:` tag back to NULL
|
||||
* for primary-source N_USEs (the primary's own tag won't
|
||||
* appear as a `use` import elsewhere). modtag matches the
|
||||
* same convention from decl_mod called on the binding decl. */
|
||||
const char *um = decl_mod(file, u);
|
||||
if (modtag == NULL) {
|
||||
if (um != NULL) continue;
|
||||
} else {
|
||||
if (um == NULL || strcmp(um, modtag) != 0) continue;
|
||||
}
|
||||
if (u->str && strcmp(u->str, name) == 0) return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 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, name)) return;
|
||||
err(c, pos, "%s '%s' shadows imported module '%s'",
|
||||
kindstr, name, name);
|
||||
}
|
||||
|
||||
void
|
||||
check_file(Checker *c, Node *file)
|
||||
{
|
||||
if (file == NULL || file->kind != N_FILE) return;
|
||||
c->file = file;
|
||||
|
||||
/* pass 1: install names (types first, then defs/fns).
|
||||
* For self-referential types we install the named-type placeholder
|
||||
@@ -1819,8 +1908,12 @@ check_file(Checker *c, Node *file)
|
||||
c->cur = newscope(c->a, saved);
|
||||
Type *fnt = d->type;
|
||||
for (Tparam *p = fnt->params; p; p = p->next) {
|
||||
if (p->name && p->name[0])
|
||||
scope_define(c->cur, p->name, SK_PARAM, p->type, d);
|
||||
if (p->name && p->name[0]) {
|
||||
check_module_shadow(c, p->name,
|
||||
d->pos, "param");
|
||||
scope_define(c->cur, p->name,
|
||||
SK_PARAM, p->type, d);
|
||||
}
|
||||
}
|
||||
Type *prev = c->ret;
|
||||
c->ret = fnt->ret;
|
||||
|
||||
@@ -524,6 +524,10 @@ struct Checker {
|
||||
* preference in bare-leaf lookups so a bare
|
||||
* `read` inside lib/os resolves to os.read
|
||||
* rather than colliding io.read. */
|
||||
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. */
|
||||
int loops; /* nesting count for break/continue */
|
||||
int errs;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user