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:
2026-05-16 08:39:35 +09:00
parent 1aece29d53
commit c9bbfcb6a6
15 changed files with 584 additions and 14 deletions

View File

@@ -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)

View File

@@ -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;

View File

@@ -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;
};

View File

@@ -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;
};

View File

@@ -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;
};

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View 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;
}

View 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;
};

View 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");
};

View 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");
};

View 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;
};

View 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");
};

View 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;
};