diff --git a/Makefile b/Makefile index 17e93010..5059f1a5 100644 --- a/Makefile +++ b/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) diff --git a/cmd/wcc/check.c b/cmd/wcc/check.c index 131a7fc2..e96b5b5f 100644 --- a/cmd/wcc/check.c +++ b/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 ;` 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; diff --git a/cmd/wcc/ww.h b/cmd/wcc/ww.h index a8dd9b45..446ba666 100644 --- a/cmd/wcc/ww.h +++ b/cmd/wcc/ww.h @@ -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; }; diff --git a/selfhost/cmd/w6a/main.combined.ww b/selfhost/cmd/w6a/main.combined.ww index 2c906dc6..31b76d95 100644 --- a/selfhost/cmd/w6a/main.combined.ww +++ b/selfhost/cmd/w6a/main.combined.ww @@ -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; }; diff --git a/selfhost/cmd/w6a/main.ww b/selfhost/cmd/w6a/main.ww index ee4b6df2..069c4ba8 100644 --- a/selfhost/cmd/w6a/main.ww +++ b/selfhost/cmd/w6a/main.ww @@ -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; }; diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 058043c3..b5f69496 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -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 ;`? 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; diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index 255c6976..13b03d51 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -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 ;`? 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; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 1549bbc7..e29082f8 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -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 ;`? 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; diff --git a/test/wcc/708_param_shadow_mod.c b/test/wcc/708_param_shadow_mod.c new file mode 100644 index 00000000..57b92433 --- /dev/null +++ b/test/wcc/708_param_shadow_mod.c @@ -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 ` '' shadows + * imported module ''` 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 +#include +#include +#include +#include +#include + +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; +} diff --git a/test/wcc/data/paramshadowmod/neg_let.ww b/test/wcc/data/paramshadowmod/neg_let.ww new file mode 100644 index 00000000..2c4ff33e --- /dev/null +++ b/test/wcc/data/paramshadowmod/neg_let.ww @@ -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; +}; diff --git a/test/wcc/data/paramshadowmod/neg_param.ww b/test/wcc/data/paramshadowmod/neg_param.ww new file mode 100644 index 00000000..34d8e807 --- /dev/null +++ b/test/wcc/data/paramshadowmod/neg_param.ww @@ -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"); +}; diff --git a/test/wcc/data/paramshadowmod/pos_rename.ww b/test/wcc/data/paramshadowmod/pos_rename.ww new file mode 100644 index 00000000..8e809718 --- /dev/null +++ b/test/wcc/data/paramshadowmod/pos_rename.ww @@ -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"); +}; diff --git a/test/wcc/data/paramshadowmod/selfimp/selfimp.ww b/test/wcc/data/paramshadowmod/selfimp/selfimp.ww new file mode 100644 index 00000000..db9c7069 --- /dev/null +++ b/test/wcc/data/paramshadowmod/selfimp/selfimp.ww @@ -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; +}; diff --git a/test/wcc/data/paramshadowmod/selfimp/selfimptest.ww b/test/wcc/data/paramshadowmod/selfimp/selfimptest.ww new file mode 100644 index 00000000..51be6dd4 --- /dev/null +++ b/test/wcc/data/paramshadowmod/selfimp/selfimptest.ww @@ -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"); +}; diff --git a/test/wcc/data/paramshadowmod/shadowmod/shadowmod.ww b/test/wcc/data/paramshadowmod/shadowmod/shadowmod.ww new file mode 100644 index 00000000..3befa30e --- /dev/null +++ b/test/wcc/data/paramshadowmod/shadowmod/shadowmod.ww @@ -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; +};