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:
6
Makefile
6
Makefile
@@ -234,6 +234,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \
|
||||
$(BIN)/test_nested_call_rhs \
|
||||
$(BIN)/test_fnlabel_mangle \
|
||||
$(BIN)/test_cgreturn_variant_zero \
|
||||
$(BIN)/test_param_shadow_mod \
|
||||
$(BIN)/test_use_promote_alias \
|
||||
$(BIN)/test_field_signed $(BIN)/test_frame_argcount \
|
||||
$(BIN)/test_selfhost $(BIN)/test_w6a_ww $(BIN)/test_w6l_ww \
|
||||
@@ -430,6 +431,11 @@ $(BIN)/test_cgreturn_variant_zero: test/wcc/707_cgreturn_variant_zero.c \
|
||||
$(LIB)/libwwrt.a | $(BIN)
|
||||
$(CC) $(CFLAGS) -o $@ $<
|
||||
|
||||
$(BIN)/test_param_shadow_mod: test/wcc/708_param_shadow_mod.c \
|
||||
$(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \
|
||||
$(LIB)/libwwrt.a | $(BIN)
|
||||
$(CC) $(CFLAGS) -o $@ $<
|
||||
|
||||
$(BIN)/test_use_promote_alias: test/wcc/699_use_promote_alias.c \
|
||||
$(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \
|
||||
$(LIB)/libwwrt.a | $(BIN)
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -2965,13 +2965,13 @@ export fn main(argc: i32, argv: **u8) i32 = {
|
||||
};
|
||||
|
||||
let ar: *arena = newarena();
|
||||
let asm: asm_;
|
||||
let s: asm_;
|
||||
let nlen: u64 = cstrlen(src);
|
||||
let fname: str = astrndup(ar, src, nlen);
|
||||
init(&asm, ar, fname, buf, blen);
|
||||
init(&s, ar, fname, buf, blen);
|
||||
|
||||
if (parse(&asm) != 0) { return 1; };
|
||||
if (encode(&asm) != 0) { return 1; };
|
||||
if (parse(&s) != 0) { return 1; };
|
||||
if (encode(&s) != 0) { return 1; };
|
||||
|
||||
// Open output for write.
|
||||
let fd: i32 = os.open(out, os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 420i32); // 0o644
|
||||
@@ -2979,7 +2979,7 @@ export fn main(argc: i32, argv: **u8) i32 = {
|
||||
os.write(2, "w6a: cannot open output\n".ptr, 23u64);
|
||||
return 1;
|
||||
};
|
||||
let rc: i32 = emitelf(&asm, fd);
|
||||
let rc: i32 = emitelf(&s, fd);
|
||||
os.close(fd);
|
||||
return rc;
|
||||
};
|
||||
|
||||
@@ -99,13 +99,13 @@ export fn main(argc: i32, argv: **u8) i32 = {
|
||||
};
|
||||
|
||||
let ar: *arena = newarena();
|
||||
let asm: asm_;
|
||||
let s: asm_;
|
||||
let nlen: u64 = cstrlen(src);
|
||||
let fname: str = astrndup(ar, src, nlen);
|
||||
init(&asm, ar, fname, buf, blen);
|
||||
init(&s, ar, fname, buf, blen);
|
||||
|
||||
if (parse(&asm) != 0) { return 1; };
|
||||
if (encode(&asm) != 0) { return 1; };
|
||||
if (parse(&s) != 0) { return 1; };
|
||||
if (encode(&s) != 0) { return 1; };
|
||||
|
||||
// Open output for write.
|
||||
let fd: i32 = os.open(out, os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 420i32); // 0o644
|
||||
@@ -113,7 +113,7 @@ export fn main(argc: i32, argv: **u8) i32 = {
|
||||
os.write(2, "w6a: cannot open output\n".ptr, 23u64);
|
||||
return 1;
|
||||
};
|
||||
let rc: i32 = emitelf(&asm, fd);
|
||||
let rc: i32 = emitelf(&s, fd);
|
||||
os.close(fd);
|
||||
return rc;
|
||||
};
|
||||
|
||||
@@ -5160,6 +5160,9 @@ type checker = struct {
|
||||
// currently being walked; "" for primary
|
||||
// compilation unit. Drives same-module
|
||||
// preference in bare-leaf lookups.
|
||||
file: *node, // N_FILE root; used by checkmoduleshadow
|
||||
// to consult the declaring source's own
|
||||
// `use` directives.
|
||||
};
|
||||
|
||||
// seedprimitives — install the built-in type names so `i32`, `str`,
|
||||
@@ -5216,6 +5219,74 @@ fn declmod(file: *node, d: *node) str = {
|
||||
return empty;
|
||||
};
|
||||
|
||||
// srcimports — does the source file that contributed decl-module
|
||||
// `modtag` carry `use <name>;`? Mirrors cstage's src_imports —
|
||||
// `modtag.len == 0` means primary, matching declmod's empty-str
|
||||
// return for primary-source decls.
|
||||
fn srcimports(file: *node, modtag: str, name: str) bool = {
|
||||
if (file == nil) { return false; };
|
||||
if (name.len == 0) { return false; };
|
||||
let u: *node = file.list;
|
||||
for (u != nil) {
|
||||
if (u.kind == nkind.N_USE) {
|
||||
// Skip self-imports: lib/fmt/fmttest.ww carries
|
||||
// `use fmt;` while 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.
|
||||
if (u.module.len > 0) {
|
||||
if (streq(u.module, u.str)) {
|
||||
u = u.next;
|
||||
continue;
|
||||
};
|
||||
};
|
||||
let um: str = declmod(file, u);
|
||||
let m: bool = false;
|
||||
if (modtag.len == 0) {
|
||||
if (um.len == 0) { m = true; };
|
||||
} else { if (streq(um, modtag)) { m = true; }; };
|
||||
if (m) {
|
||||
if (streq(u.str, name)) { return true; };
|
||||
};
|
||||
};
|
||||
u = u.next;
|
||||
};
|
||||
return false;
|
||||
};
|
||||
|
||||
// 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: *scope = c.cur;
|
||||
for (s != nil) {
|
||||
let r: *sym = scopelookuplocal(s, name);
|
||||
if (r != nil) {
|
||||
if (r.skind == skind.SK_USE) {
|
||||
seen = true;
|
||||
s = nil;
|
||||
};
|
||||
};
|
||||
if (s != nil) { s = s.parent; };
|
||||
};
|
||||
if (!seen) { return; };
|
||||
if (!srcimports(c.file, c.curmod, name)) { return; };
|
||||
os.write(2, kindstr.ptr, kindstr.len: u64);
|
||||
os.write(2, " '".ptr, 2u64);
|
||||
os.write(2, name.ptr, name.len: u64);
|
||||
os.write(2, "' shadows imported module '".ptr, 27u64);
|
||||
os.write(2, name.ptr, name.len: u64);
|
||||
os.write(2, "'\n".ptr, 2u64);
|
||||
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.
|
||||
@@ -5334,6 +5405,7 @@ fn resolvewalk(c: *checker, n: *node) void = {
|
||||
for (m != nil) {
|
||||
let bnm: str = m.str;
|
||||
if (bnm.len > 0) {
|
||||
checkmoduleshadow(c, bnm, "binding");
|
||||
scopedefine(c.cur, bnm, skind.SK_VAR, nil, m);
|
||||
};
|
||||
m = m.next;
|
||||
@@ -5341,6 +5413,7 @@ fn resolvewalk(c: *checker, n: *node) void = {
|
||||
} else {
|
||||
let bnm: str = n.str;
|
||||
if (bnm.len > 0) {
|
||||
checkmoduleshadow(c, bnm, "binding");
|
||||
scopedefine(c.cur, bnm, skind.SK_VAR, nil, n);
|
||||
};
|
||||
};
|
||||
@@ -5361,6 +5434,7 @@ fn resolvewalk(c: *checker, n: *node) void = {
|
||||
c.cur = newscope(c.a, outer);
|
||||
let nm: str = n.str;
|
||||
if (nm.len > 0) {
|
||||
checkmoduleshadow(c, nm, "binding");
|
||||
scopedefine(c.cur, nm, skind.SK_VAR, nil, n);
|
||||
};
|
||||
if (n.body != nil) { resolvewalk(c, n.body); };
|
||||
@@ -5407,6 +5481,7 @@ fn resolvewalk(c: *checker, n: *node) void = {
|
||||
if (k == nkind.N_LET) {
|
||||
let nm: str = n.str;
|
||||
if (nm.len > 0) {
|
||||
checkmoduleshadow(c, nm, "let");
|
||||
scopedefine(c.cur, nm, skind.SK_VAR, nil, n);
|
||||
};
|
||||
};
|
||||
@@ -6052,6 +6127,7 @@ fn installparams(c: *checker, params: *node) void = {
|
||||
if (p.kind == nkind.N_PARAM) {
|
||||
let nm: str = p.str;
|
||||
if (nm.len > 0) {
|
||||
checkmoduleshadow(c, nm, "param");
|
||||
scopedefine(c.cur, nm, skind.SK_PARAM, nil, p);
|
||||
};
|
||||
};
|
||||
@@ -6088,12 +6164,14 @@ export fn checkinit(c: *checker, a: *arena, tc: *tctx) void = {
|
||||
c.fnret = nil;
|
||||
let empty: str;
|
||||
c.curmod = empty;
|
||||
c.file = nil;
|
||||
seedprimitives(c);
|
||||
};
|
||||
|
||||
export fn checkfile(c: *checker, file: *node) void = {
|
||||
if (file == nil) { return; };
|
||||
if (file.kind != nkind.N_FILE) { return; };
|
||||
c.file = file;
|
||||
|
||||
// Pass 1: install all top-level names.
|
||||
let d: *node = file.list;
|
||||
|
||||
@@ -35,6 +35,9 @@ type checker = struct {
|
||||
// currently being walked; "" for primary
|
||||
// compilation unit. Drives same-module
|
||||
// preference in bare-leaf lookups.
|
||||
file: *node, // N_FILE root; used by checkmoduleshadow
|
||||
// to consult the declaring source's own
|
||||
// `use` directives.
|
||||
};
|
||||
|
||||
// seedprimitives — install the built-in type names so `i32`, `str`,
|
||||
@@ -91,6 +94,74 @@ fn declmod(file: *node, d: *node) str = {
|
||||
return empty;
|
||||
};
|
||||
|
||||
// srcimports — does the source file that contributed decl-module
|
||||
// `modtag` carry `use <name>;`? Mirrors cstage's src_imports —
|
||||
// `modtag.len == 0` means primary, matching declmod's empty-str
|
||||
// return for primary-source decls.
|
||||
fn srcimports(file: *node, modtag: str, name: str) bool = {
|
||||
if (file == nil) { return false; };
|
||||
if (name.len == 0) { return false; };
|
||||
let u: *node = file.list;
|
||||
for (u != nil) {
|
||||
if (u.kind == nkind.N_USE) {
|
||||
// Skip self-imports: lib/fmt/fmttest.ww carries
|
||||
// `use fmt;` while 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.
|
||||
if (u.module.len > 0) {
|
||||
if (streq(u.module, u.str)) {
|
||||
u = u.next;
|
||||
continue;
|
||||
};
|
||||
};
|
||||
let um: str = declmod(file, u);
|
||||
let m: bool = false;
|
||||
if (modtag.len == 0) {
|
||||
if (um.len == 0) { m = true; };
|
||||
} else { if (streq(um, modtag)) { m = true; }; };
|
||||
if (m) {
|
||||
if (streq(u.str, name)) { return true; };
|
||||
};
|
||||
};
|
||||
u = u.next;
|
||||
};
|
||||
return false;
|
||||
};
|
||||
|
||||
// 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: *scope = c.cur;
|
||||
for (s != nil) {
|
||||
let r: *sym = scopelookuplocal(s, name);
|
||||
if (r != nil) {
|
||||
if (r.skind == skind.SK_USE) {
|
||||
seen = true;
|
||||
s = nil;
|
||||
};
|
||||
};
|
||||
if (s != nil) { s = s.parent; };
|
||||
};
|
||||
if (!seen) { return; };
|
||||
if (!srcimports(c.file, c.curmod, name)) { return; };
|
||||
os.write(2, kindstr.ptr, kindstr.len: u64);
|
||||
os.write(2, " '".ptr, 2u64);
|
||||
os.write(2, name.ptr, name.len: u64);
|
||||
os.write(2, "' shadows imported module '".ptr, 27u64);
|
||||
os.write(2, name.ptr, name.len: u64);
|
||||
os.write(2, "'\n".ptr, 2u64);
|
||||
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.
|
||||
@@ -209,6 +280,7 @@ fn resolvewalk(c: *checker, n: *node) void = {
|
||||
for (m != nil) {
|
||||
let bnm: str = m.str;
|
||||
if (bnm.len > 0) {
|
||||
checkmoduleshadow(c, bnm, "binding");
|
||||
scopedefine(c.cur, bnm, skind.SK_VAR, nil, m);
|
||||
};
|
||||
m = m.next;
|
||||
@@ -216,6 +288,7 @@ fn resolvewalk(c: *checker, n: *node) void = {
|
||||
} else {
|
||||
let bnm: str = n.str;
|
||||
if (bnm.len > 0) {
|
||||
checkmoduleshadow(c, bnm, "binding");
|
||||
scopedefine(c.cur, bnm, skind.SK_VAR, nil, n);
|
||||
};
|
||||
};
|
||||
@@ -236,6 +309,7 @@ fn resolvewalk(c: *checker, n: *node) void = {
|
||||
c.cur = newscope(c.a, outer);
|
||||
let nm: str = n.str;
|
||||
if (nm.len > 0) {
|
||||
checkmoduleshadow(c, nm, "binding");
|
||||
scopedefine(c.cur, nm, skind.SK_VAR, nil, n);
|
||||
};
|
||||
if (n.body != nil) { resolvewalk(c, n.body); };
|
||||
@@ -282,6 +356,7 @@ fn resolvewalk(c: *checker, n: *node) void = {
|
||||
if (k == nkind.N_LET) {
|
||||
let nm: str = n.str;
|
||||
if (nm.len > 0) {
|
||||
checkmoduleshadow(c, nm, "let");
|
||||
scopedefine(c.cur, nm, skind.SK_VAR, nil, n);
|
||||
};
|
||||
};
|
||||
@@ -927,6 +1002,7 @@ fn installparams(c: *checker, params: *node) void = {
|
||||
if (p.kind == nkind.N_PARAM) {
|
||||
let nm: str = p.str;
|
||||
if (nm.len > 0) {
|
||||
checkmoduleshadow(c, nm, "param");
|
||||
scopedefine(c.cur, nm, skind.SK_PARAM, nil, p);
|
||||
};
|
||||
};
|
||||
@@ -963,12 +1039,14 @@ export fn checkinit(c: *checker, a: *arena, tc: *tctx) void = {
|
||||
c.fnret = nil;
|
||||
let empty: str;
|
||||
c.curmod = empty;
|
||||
c.file = nil;
|
||||
seedprimitives(c);
|
||||
};
|
||||
|
||||
export fn checkfile(c: *checker, file: *node) void = {
|
||||
if (file == nil) { return; };
|
||||
if (file.kind != nkind.N_FILE) { return; };
|
||||
c.file = file;
|
||||
|
||||
// Pass 1: install all top-level names.
|
||||
let d: *node = file.list;
|
||||
|
||||
@@ -5160,6 +5160,9 @@ type checker = struct {
|
||||
// currently being walked; "" for primary
|
||||
// compilation unit. Drives same-module
|
||||
// preference in bare-leaf lookups.
|
||||
file: *node, // N_FILE root; used by checkmoduleshadow
|
||||
// to consult the declaring source's own
|
||||
// `use` directives.
|
||||
};
|
||||
|
||||
// seedprimitives — install the built-in type names so `i32`, `str`,
|
||||
@@ -5216,6 +5219,74 @@ fn declmod(file: *node, d: *node) str = {
|
||||
return empty;
|
||||
};
|
||||
|
||||
// srcimports — does the source file that contributed decl-module
|
||||
// `modtag` carry `use <name>;`? Mirrors cstage's src_imports —
|
||||
// `modtag.len == 0` means primary, matching declmod's empty-str
|
||||
// return for primary-source decls.
|
||||
fn srcimports(file: *node, modtag: str, name: str) bool = {
|
||||
if (file == nil) { return false; };
|
||||
if (name.len == 0) { return false; };
|
||||
let u: *node = file.list;
|
||||
for (u != nil) {
|
||||
if (u.kind == nkind.N_USE) {
|
||||
// Skip self-imports: lib/fmt/fmttest.ww carries
|
||||
// `use fmt;` while 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.
|
||||
if (u.module.len > 0) {
|
||||
if (streq(u.module, u.str)) {
|
||||
u = u.next;
|
||||
continue;
|
||||
};
|
||||
};
|
||||
let um: str = declmod(file, u);
|
||||
let m: bool = false;
|
||||
if (modtag.len == 0) {
|
||||
if (um.len == 0) { m = true; };
|
||||
} else { if (streq(um, modtag)) { m = true; }; };
|
||||
if (m) {
|
||||
if (streq(u.str, name)) { return true; };
|
||||
};
|
||||
};
|
||||
u = u.next;
|
||||
};
|
||||
return false;
|
||||
};
|
||||
|
||||
// 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: *scope = c.cur;
|
||||
for (s != nil) {
|
||||
let r: *sym = scopelookuplocal(s, name);
|
||||
if (r != nil) {
|
||||
if (r.skind == skind.SK_USE) {
|
||||
seen = true;
|
||||
s = nil;
|
||||
};
|
||||
};
|
||||
if (s != nil) { s = s.parent; };
|
||||
};
|
||||
if (!seen) { return; };
|
||||
if (!srcimports(c.file, c.curmod, name)) { return; };
|
||||
os.write(2, kindstr.ptr, kindstr.len: u64);
|
||||
os.write(2, " '".ptr, 2u64);
|
||||
os.write(2, name.ptr, name.len: u64);
|
||||
os.write(2, "' shadows imported module '".ptr, 27u64);
|
||||
os.write(2, name.ptr, name.len: u64);
|
||||
os.write(2, "'\n".ptr, 2u64);
|
||||
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.
|
||||
@@ -5334,6 +5405,7 @@ fn resolvewalk(c: *checker, n: *node) void = {
|
||||
for (m != nil) {
|
||||
let bnm: str = m.str;
|
||||
if (bnm.len > 0) {
|
||||
checkmoduleshadow(c, bnm, "binding");
|
||||
scopedefine(c.cur, bnm, skind.SK_VAR, nil, m);
|
||||
};
|
||||
m = m.next;
|
||||
@@ -5341,6 +5413,7 @@ fn resolvewalk(c: *checker, n: *node) void = {
|
||||
} else {
|
||||
let bnm: str = n.str;
|
||||
if (bnm.len > 0) {
|
||||
checkmoduleshadow(c, bnm, "binding");
|
||||
scopedefine(c.cur, bnm, skind.SK_VAR, nil, n);
|
||||
};
|
||||
};
|
||||
@@ -5361,6 +5434,7 @@ fn resolvewalk(c: *checker, n: *node) void = {
|
||||
c.cur = newscope(c.a, outer);
|
||||
let nm: str = n.str;
|
||||
if (nm.len > 0) {
|
||||
checkmoduleshadow(c, nm, "binding");
|
||||
scopedefine(c.cur, nm, skind.SK_VAR, nil, n);
|
||||
};
|
||||
if (n.body != nil) { resolvewalk(c, n.body); };
|
||||
@@ -5407,6 +5481,7 @@ fn resolvewalk(c: *checker, n: *node) void = {
|
||||
if (k == nkind.N_LET) {
|
||||
let nm: str = n.str;
|
||||
if (nm.len > 0) {
|
||||
checkmoduleshadow(c, nm, "let");
|
||||
scopedefine(c.cur, nm, skind.SK_VAR, nil, n);
|
||||
};
|
||||
};
|
||||
@@ -6052,6 +6127,7 @@ fn installparams(c: *checker, params: *node) void = {
|
||||
if (p.kind == nkind.N_PARAM) {
|
||||
let nm: str = p.str;
|
||||
if (nm.len > 0) {
|
||||
checkmoduleshadow(c, nm, "param");
|
||||
scopedefine(c.cur, nm, skind.SK_PARAM, nil, p);
|
||||
};
|
||||
};
|
||||
@@ -6088,12 +6164,14 @@ export fn checkinit(c: *checker, a: *arena, tc: *tctx) void = {
|
||||
c.fnret = nil;
|
||||
let empty: str;
|
||||
c.curmod = empty;
|
||||
c.file = nil;
|
||||
seedprimitives(c);
|
||||
};
|
||||
|
||||
export fn checkfile(c: *checker, file: *node) void = {
|
||||
if (file == nil) { return; };
|
||||
if (file.kind != nkind.N_FILE) { return; };
|
||||
c.file = file;
|
||||
|
||||
// Pass 1: install all top-level names.
|
||||
let d: *node = file.list;
|
||||
|
||||
163
test/wcc/708_param_shadow_mod.c
Normal file
163
test/wcc/708_param_shadow_mod.c
Normal file
@@ -0,0 +1,163 @@
|
||||
/*
|
||||
* 706_param_shadow_mod — "value names and module names are disjoint."
|
||||
*
|
||||
* Pre-fix (task #19): a fn param / local-let named `shadowmod` while
|
||||
* the same source carried `use shadowmod;` would compile cleanly and
|
||||
* silently miscompile any `shadowmod.X` body lookup — cstage's cexpr
|
||||
* N_DOT path resolved the inner ident through the shadow's value
|
||||
* bits, then emitted CALL through the str's .ptr field. Symptom in
|
||||
* the field was a SIGSEGV inside lib/log's lprintfln (worked around
|
||||
* by renaming `fmt: str` → `format: str` at commit 6b6d7dd).
|
||||
*
|
||||
* Post-fix: cstage check.c (and wwstage check.ww, run by wwdump_ww)
|
||||
* refuse the bind at the decl site with a `<kind> '<name>' shadows
|
||||
* imported module '<name>'` diagnostic. Same-leaf top-level decls
|
||||
* (e.g. `use fnmatch; fn fnmatch(...)`) are exempt — the rule fires
|
||||
* only for nested-scope binds whose declaring source file imports
|
||||
* the module.
|
||||
*
|
||||
* row | shape | gate
|
||||
* -----------+--------------------------------------+--------------
|
||||
* neg_param | `use shadowmod; fn p(shadowmod: str)`| must fail
|
||||
* neg_let | `use shadowmod; ... let shadowmod` | must fail
|
||||
* pos_rename | rename param away from `shadowmod` | must succeed,
|
||||
* exit = 42
|
||||
*
|
||||
* Fixtures live in test/wcc/data/paramshadowmod/. Cstage-only:
|
||||
* wwstage's check.ww runs only inside wwdump_ww (diagnostic), and
|
||||
* the actual selfhost compile pipeline (994_w6c_ww) doesn't trip
|
||||
* because wwstage's cgen takes the module-qualified emit path for
|
||||
* any N_DOT-callee bare ident — see STATUS.md's `Wwstage no-
|
||||
* checkfile-pass smell` note + #11 (deferred wwstage checkfile pass).
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/wait.h>
|
||||
|
||||
static int
|
||||
runwait(const char *cmd)
|
||||
{
|
||||
int rc = system(cmd);
|
||||
if (rc == -1) return -1;
|
||||
if (WIFEXITED(rc)) return WEXITSTATUS(rc);
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int
|
||||
run_neg(const char *driver, const char *fixdir, const char *tag)
|
||||
{
|
||||
char src[64];
|
||||
snprintf(src, sizeof src, "neg_%s.ww", tag);
|
||||
char cmd[2048];
|
||||
/* cd into the fixture dir so the driver's source-dir-first
|
||||
* import search resolves `use shadowmod;`. */
|
||||
snprintf(cmd, sizeof cmd,
|
||||
"cd %s && %s build %s >/dev/null 2>&1", fixdir, driver, src);
|
||||
int rc = runwait(cmd);
|
||||
if (rc == 0) {
|
||||
fprintf(stderr,
|
||||
"param_shadow_mod[neg_%s]: build unexpectedly succeeded "
|
||||
"— shadow rule did not fire\n", tag);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
run_pos(const char *driver, const char *fixdir)
|
||||
{
|
||||
char cmd[2048];
|
||||
snprintf(cmd, sizeof cmd,
|
||||
"cd %s && %s build pos_rename.ww >/dev/null 2>&1",
|
||||
fixdir, driver);
|
||||
if (runwait(cmd) != 0) {
|
||||
fprintf(stderr,
|
||||
"param_shadow_mod[pos_rename]: build failed — rule "
|
||||
"over-triggered on the rename\n");
|
||||
return 1;
|
||||
}
|
||||
char bin[2048];
|
||||
snprintf(bin, sizeof bin, "%s/pos_rename", fixdir);
|
||||
int got = runwait(bin);
|
||||
unlink(bin);
|
||||
if (got != 42) {
|
||||
fprintf(stderr,
|
||||
"param_shadow_mod[pos_rename]: exit=%d want=42\n", got);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* pos_selfimp — same-module self-import. selfimp/selfimptest.ww
|
||||
* carries `use selfimp;` from inside the module whose tag is also
|
||||
* "selfimp" (matches lib/fmt/fmttest.ww's shape that surfaced the
|
||||
* over-trigger originally). check_module_shadow's u->module ==
|
||||
* u->str skip must drop the directive from the import scan, so the
|
||||
* `selfimp: str` param does NOT trip the rule.
|
||||
*/
|
||||
static int
|
||||
run_pos_selfimp(const char *driver, const char *fixdir)
|
||||
{
|
||||
char cmd[2048];
|
||||
snprintf(cmd, sizeof cmd,
|
||||
"cd %s && %s build selfimp/selfimptest.ww >/dev/null 2>&1",
|
||||
fixdir, driver);
|
||||
if (runwait(cmd) != 0) {
|
||||
fprintf(stderr,
|
||||
"param_shadow_mod[pos_selfimp]: build failed — "
|
||||
"self-import skip regressed\n");
|
||||
return 1;
|
||||
}
|
||||
char bin[2048];
|
||||
snprintf(bin, sizeof bin, "%s/selfimptest", fixdir);
|
||||
int got = runwait(bin);
|
||||
unlink(bin);
|
||||
if (got != 7) {
|
||||
fprintf(stderr,
|
||||
"param_shadow_mod[pos_selfimp]: exit=%d want=7\n", got);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
main(void)
|
||||
{
|
||||
const char *bin = getenv("BIN");
|
||||
if (!bin) bin = "out/bin";
|
||||
char absbin[1024];
|
||||
if (bin[0] != '/') {
|
||||
char cwd[1024];
|
||||
if (getcwd(cwd, sizeof cwd) == NULL) return 1;
|
||||
snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin);
|
||||
bin = absbin;
|
||||
}
|
||||
|
||||
char cdrv[1024];
|
||||
snprintf(cdrv, sizeof cdrv, "%s/ww", bin);
|
||||
|
||||
char fixdir[1024];
|
||||
if (getcwd(fixdir, sizeof fixdir) == NULL) return 1;
|
||||
size_t cwd_n = strlen(fixdir);
|
||||
const char *rel = "/test/wcc/data/paramshadowmod";
|
||||
if (cwd_n + strlen(rel) + 1 >= sizeof fixdir) return 1;
|
||||
memcpy(fixdir + cwd_n, rel, strlen(rel) + 1);
|
||||
|
||||
int fail = 0;
|
||||
fail += run_neg(cdrv, fixdir, "param");
|
||||
fail += run_neg(cdrv, fixdir, "let");
|
||||
fail += run_pos(cdrv, fixdir);
|
||||
fail += run_pos_selfimp(cdrv, fixdir);
|
||||
|
||||
if (fail) {
|
||||
fprintf(stderr,
|
||||
"param_shadow_mod: %d row(s) failed\n", fail);
|
||||
return 1;
|
||||
}
|
||||
printf("param_shadow_mod: 4/4 ok\n");
|
||||
return 0;
|
||||
}
|
||||
10
test/wcc/data/paramshadowmod/neg_let.ww
Normal file
10
test/wcc/data/paramshadowmod/neg_let.ww
Normal file
@@ -0,0 +1,10 @@
|
||||
// neg_let — local `let shadowmod: i32 = ...` shadows the imported
|
||||
// module from inside a fn body. Same rule fires for nested-scope
|
||||
// let binds, not just params.
|
||||
|
||||
use shadowmod;
|
||||
|
||||
export fn main() i32 = {
|
||||
let shadowmod: i32 = 0i32;
|
||||
return shadowmod;
|
||||
};
|
||||
13
test/wcc/data/paramshadowmod/neg_param.ww
Normal file
13
test/wcc/data/paramshadowmod/neg_param.ww
Normal file
@@ -0,0 +1,13 @@
|
||||
// neg_param — fn param `shadowmod: str` shadows the imported module.
|
||||
// Under the "value names and module names are disjoint" rule the
|
||||
// build must fail with a clear diagnostic at the param decl site.
|
||||
|
||||
use shadowmod;
|
||||
|
||||
fn probe(shadowmod: str) i32 = {
|
||||
return shadowmod.len;
|
||||
};
|
||||
|
||||
export fn main() i32 = {
|
||||
return probe("hi");
|
||||
};
|
||||
14
test/wcc/data/paramshadowmod/pos_rename.ww
Normal file
14
test/wcc/data/paramshadowmod/pos_rename.ww
Normal file
@@ -0,0 +1,14 @@
|
||||
// pos_rename — positive case. The fn param is renamed away from the
|
||||
// imported module's bareword, so the rule doesn't fire and the body
|
||||
// can call `shadowmod.say()` cleanly. Built + run; exit code = 42.
|
||||
|
||||
use shadowmod;
|
||||
|
||||
fn probe(s: str) i32 = {
|
||||
let _ = s;
|
||||
return shadowmod.say();
|
||||
};
|
||||
|
||||
export fn main() i32 = {
|
||||
return probe("hi");
|
||||
};
|
||||
7
test/wcc/data/paramshadowmod/selfimp/selfimp.ww
Normal file
7
test/wcc/data/paramshadowmod/selfimp/selfimp.ww
Normal file
@@ -0,0 +1,7 @@
|
||||
// pos_selfimp/selfimp.ww — minimal "module" body. The interesting
|
||||
// scenario lives in the sibling selfimptest.ww file, which carries
|
||||
// `use selfimp;` from inside the same module.
|
||||
|
||||
export fn touch() i32 = {
|
||||
return 0i32;
|
||||
};
|
||||
19
test/wcc/data/paramshadowmod/selfimp/selfimptest.ww
Normal file
19
test/wcc/data/paramshadowmod/selfimp/selfimptest.ww
Normal file
@@ -0,0 +1,19 @@
|
||||
// pos_selfimp/selfimptest.ww — same-module self-import case. This
|
||||
// file's MODULE tag is "selfimp" (parent dir basename), and it
|
||||
// carries `use selfimp;` — exactly the lib/fmt/fmttest.ww shape that
|
||||
// originally surfaced check_module_shadow's over-trigger on
|
||||
// `fn bsprintf(... fmt: str, ...)`.
|
||||
//
|
||||
// src_imports' self-import skip (u->module == u->str) drops these
|
||||
// entries from the import scan, so the param `selfimp: str` here
|
||||
// must NOT be flagged as shadowing — build + run, exit = 7.
|
||||
|
||||
use selfimp;
|
||||
|
||||
fn probe(selfimp: str) i32 = {
|
||||
return selfimp.len;
|
||||
};
|
||||
|
||||
export fn main() i32 = {
|
||||
return probe("regress");
|
||||
};
|
||||
7
test/wcc/data/paramshadowmod/shadowmod/shadowmod.ww
Normal file
7
test/wcc/data/paramshadowmod/shadowmod/shadowmod.ww
Normal file
@@ -0,0 +1,7 @@
|
||||
// paramshadowmod/shadowmod — a tiny module the negative/positive
|
||||
// fixtures import as `use shadowmod;`. Carries one fn so the leaf
|
||||
// resolves through the module dot path when name resolution succeeds.
|
||||
|
||||
export fn say() i32 = {
|
||||
return 42i32;
|
||||
};
|
||||
Reference in New Issue
Block a user