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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user