wcc/ww: reject duplicate top-level decls, aligned to cstage

installdecl routes all four kinds (fn/type/def/let) through installtop,
which turns scopedefineinmodule's nil return into cstage's exact
"duplicate <kind> <name>" reject, keyed (name,mod) so cross-package
same-leaf decls coexist. Builtin redecls are dropped, not dup-errored:
cstage never scopes builtins (lookup_builtin first, check.c:69), so a
user redecl is dead there — wwstage mirrors via scopesamekeysym +
no-source-decl test. -T synth __wwtests installs direct, mirroring
check.c:3079. Closes the silent dup-fn hole (user fn run vs lib/test
run built a broken test binary with no diagnostic). Per-kind reject
rows + cross-package/builtin accept byte-id rows + -T collision parity
row in 910/997. (#23-team, category-A addendum closed)
This commit is contained in:
2026-06-11 00:57:43 +09:00
parent 16c83e70d3
commit 19e9535ef4
13 changed files with 448 additions and 50 deletions

View File

@@ -10330,6 +10330,30 @@ export fn scopedefineinmodule(s: *scope, name: str, mod: str, k: skind, t: *tinf
return sy;
};
// scopesamekeysym — the entry scopedefineinmodule(name, mod) treats as a
// duplicate (same name, same mod-key), or nil if the key is free. Lets a
// caller that got a nil from scopedefineinmodule learn WHAT it collided
// with (e.g. a pre-seeded builtin vs a genuine user redeclaration). The
// match logic mirrors scopedefineinmodule's reject branch exactly.
export fn scopesamekeysym(s: *scope, name: str, mod: str) *sym = {
let h: u64 = hashstr(name);
let bi: i32 = (h % (s.nbuckets: u64)): i32;
let b: *sym = s.buckets[bi];
for (b != nil) {
if (streq(b.name, name)) {
if (b.mod.len == 0) {
if (mod.len == 0) { return b; };
} else {
if (mod.len > 0) {
if (streq(b.mod, mod)) { return b; };
};
};
};
b = b.hashnext;
};
return nil;
};
// selfhost/cmd/wcc/check.ww — minimal port of cmd/wcc/check.c.
//
// Status: name-resolution + primitive-type seeding only. Full type
@@ -10592,16 +10616,12 @@ fn checkmoduleshadow(c: *checker, name: str, kindstr: str) void = {
// scopelookupuselocal (lib/ww/sym.ww) to the SK_USE that coexists in the
// landed scope — the coexistence-equivalent of cstage's use_alias bit.
// Cite: project memory module_type_name_collision (cstage fix
// 2026-05-13). #11 (wwstage checkfile pass) revisits the dup-decl errors
// below when wwstage grows a real check pass on the cgen path.
// TODO(#11): cstage check.c errors on duplicate top-level type/def/fn
// (see cmd/wcc/check.c L1800/L1839/L1860 "duplicate <kind>") and on
// duplicate top-level let (cmd/wcc/check.c L1880, "duplicate let %s")
// once #32 lands. Wwstage's installdecl just drops the second insert
// silently. Add `if (s == nil) err(...)` here once #11 wires checkfile
// into w6c_ww. Silent-accept matches the deferred-check design — see
// test/wcc/708 and test/wcc/696 for the same cstage-only neg-case
// precedent.
// 2026-05-13).
// #23: top-level duplicate type/def/fn/let now reject loud here, keyed
// on (name, mod) exactly as cstage's install pass does
// (cmd/wcc/check.c:2852/2911/2932/2955) — see dupdecl below. (Same-scope
// LOCAL dup `let a=1; let a=2;` is a different path and stays deferred to
// #11; test/wcc/708 + test/wcc/696 are that cstage-only neg-case.)
fn installdecl(c: *checker, file: *node, d: *node) void = {
if (d == nil) { return; };
let k: nkind = d.kind;
@@ -10619,10 +10639,43 @@ fn installdecl(c: *checker, file: *node, d: *node) void = {
};
scopedefine(c.top, nm, skind.SK_USE, nil, d); return;
};
if (k == nkind.N_DEF) { scopedefineinmodule(c.top, nm, mod, skind.SK_DEF, nil, d); return; };
if (k == nkind.N_TYPEDECL) { scopedefineinmodule(c.top, nm, mod, skind.SK_TYPE, nil, d); return; };
if (k == nkind.N_FNDECL) { scopedefineinmodule(c.top, nm, mod, skind.SK_FN, nil, d); return; };
if (k == nkind.N_LET) { scopedefineinmodule(c.top, nm, mod, skind.SK_VAR, nil, d); return; };
if (k == nkind.N_DEF) { installtop(c, d, nm, mod, skind.SK_DEF, "def"); return; };
if (k == nkind.N_TYPEDECL) { installtop(c, d, nm, mod, skind.SK_TYPE, "type"); return; };
if (k == nkind.N_FNDECL) { installtop(c, d, nm, mod, skind.SK_FN, "fn"); return; };
if (k == nkind.N_LET) { installtop(c, d, nm, mod, skind.SK_VAR, "let"); return; };
};
// installtop — install a top-level decl name into c.top, rejecting a
// genuine within-module duplicate loud (#23) with cstage's exact
// "duplicate <kind> %s" wording (cmd/wcc/check.c:2852/2911/2932/2955).
//
// scopedefineinmodule returns nil only on a same-(name, mod) re-install.
// Same-leaf cross-package decls carry distinct mods (the flat-bundle
// model) and an imported-module bareword's SK_USE keys on mod="", so a
// coexisting same-leaf type/fn in its own package never collides.
//
// The one nil that is NOT a user duplicate: a redeclaration of a
// pre-seeded builtin (the predeclared `nomem`, or a primtype name). cstage
// keeps no builtins in the scope at all — resolve_typename consults
// lookup_builtin FIRST (cmd/wcc/check.c:69) and the builtin always wins,
// so a user `type nomem = !void` / `type int = ...` installs dead and is
// silently ignored, never a duplicate error. wwstage seeds builtins INTO
// c.top, so the same redeclaration surfaces here as a collision; we mirror
// cstage by dropping it (the seeded builtin stays, and wins resolution)
// rather than erroring. A pre-seeded builtin is identified by its sym
// carrying no real source decl (primtypes: decl=nil; `nomem`: a
// checkinit-synthesized N_TYPEDECL with an empty .file) — user decls
// always carry their parsed source file.
fn installtop(c: *checker, d: *node, nm: str, mod: str, k: skind, kind: str) void = {
if (scopedefineinmodule(c.top, nm, mod, k, nil, d) != nil) { return; };
let prev: *sym = scopesamekeysym(c.top, nm, mod);
if (prev != nil) {
if (prev.decl == nil) { return; };
if (prev.decl.file.len == 0) { return; };
};
cerr(d.file); cerr(": error: duplicate "); cerr(kind);
cerr(" "); cerr(nm); cerr("\n");
c.errs += 1;
};
// stamptuplebinds — distribute a tuple's per-element types onto a
@@ -15918,8 +15971,13 @@ export fn checkfile(c: *checker, file: *node) void = {
tab.lhs = tsl;
tab.rhs = arr;
// pass 1 already ran; install the table name now so main
// resolves it (declmod returns "" for tab.nmod="").
installdecl(c, file, tab);
// resolves it (declmod returns "" for tab.nmod=""). Goes
// direct to scopedefineinmodule, NOT installdecl: cstage's
// synth install (cmd/wcc/check.c:3079) bypasses the
// duplicate-decl reject (#23) the same way, so a pathological
// user `const __wwtests` stays a downstream type error in
// both stages rather than a dup-diagnostic in only one.
scopedefineinmodule(c.top, tab.str, "", skind.SK_VAR, nil, tab);
let arg: *node = newnode(nkind.N_IDENT, pf, pl, pc);
arg.str = "__wwtests";

View File

@@ -260,16 +260,12 @@ fn checkmoduleshadow(c: *checker, name: str, kindstr: str) void = {
// scopelookupuselocal (lib/ww/sym.ww) to the SK_USE that coexists in the
// landed scope — the coexistence-equivalent of cstage's use_alias bit.
// Cite: project memory module_type_name_collision (cstage fix
// 2026-05-13). #11 (wwstage checkfile pass) revisits the dup-decl errors
// below when wwstage grows a real check pass on the cgen path.
// TODO(#11): cstage check.c errors on duplicate top-level type/def/fn
// (see cmd/wcc/check.c L1800/L1839/L1860 "duplicate <kind>") and on
// duplicate top-level let (cmd/wcc/check.c L1880, "duplicate let %s")
// once #32 lands. Wwstage's installdecl just drops the second insert
// silently. Add `if (s == nil) err(...)` here once #11 wires checkfile
// into w6c_ww. Silent-accept matches the deferred-check design — see
// test/wcc/708 and test/wcc/696 for the same cstage-only neg-case
// precedent.
// 2026-05-13).
// #23: top-level duplicate type/def/fn/let now reject loud here, keyed
// on (name, mod) exactly as cstage's install pass does
// (cmd/wcc/check.c:2852/2911/2932/2955) — see dupdecl below. (Same-scope
// LOCAL dup `let a=1; let a=2;` is a different path and stays deferred to
// #11; test/wcc/708 + test/wcc/696 are that cstage-only neg-case.)
fn installdecl(c: *checker, file: *node, d: *node) void = {
if (d == nil) { return; };
let k: nkind = d.kind;
@@ -287,10 +283,43 @@ fn installdecl(c: *checker, file: *node, d: *node) void = {
};
scopedefine(c.top, nm, skind.SK_USE, nil, d); return;
};
if (k == nkind.N_DEF) { scopedefineinmodule(c.top, nm, mod, skind.SK_DEF, nil, d); return; };
if (k == nkind.N_TYPEDECL) { scopedefineinmodule(c.top, nm, mod, skind.SK_TYPE, nil, d); return; };
if (k == nkind.N_FNDECL) { scopedefineinmodule(c.top, nm, mod, skind.SK_FN, nil, d); return; };
if (k == nkind.N_LET) { scopedefineinmodule(c.top, nm, mod, skind.SK_VAR, nil, d); return; };
if (k == nkind.N_DEF) { installtop(c, d, nm, mod, skind.SK_DEF, "def"); return; };
if (k == nkind.N_TYPEDECL) { installtop(c, d, nm, mod, skind.SK_TYPE, "type"); return; };
if (k == nkind.N_FNDECL) { installtop(c, d, nm, mod, skind.SK_FN, "fn"); return; };
if (k == nkind.N_LET) { installtop(c, d, nm, mod, skind.SK_VAR, "let"); return; };
};
// installtop — install a top-level decl name into c.top, rejecting a
// genuine within-module duplicate loud (#23) with cstage's exact
// "duplicate <kind> %s" wording (cmd/wcc/check.c:2852/2911/2932/2955).
//
// scopedefineinmodule returns nil only on a same-(name, mod) re-install.
// Same-leaf cross-package decls carry distinct mods (the flat-bundle
// model) and an imported-module bareword's SK_USE keys on mod="", so a
// coexisting same-leaf type/fn in its own package never collides.
//
// The one nil that is NOT a user duplicate: a redeclaration of a
// pre-seeded builtin (the predeclared `nomem`, or a primtype name). cstage
// keeps no builtins in the scope at all — resolve_typename consults
// lookup_builtin FIRST (cmd/wcc/check.c:69) and the builtin always wins,
// so a user `type nomem = !void` / `type int = ...` installs dead and is
// silently ignored, never a duplicate error. wwstage seeds builtins INTO
// c.top, so the same redeclaration surfaces here as a collision; we mirror
// cstage by dropping it (the seeded builtin stays, and wins resolution)
// rather than erroring. A pre-seeded builtin is identified by its sym
// carrying no real source decl (primtypes: decl=nil; `nomem`: a
// checkinit-synthesized N_TYPEDECL with an empty .file) — user decls
// always carry their parsed source file.
fn installtop(c: *checker, d: *node, nm: str, mod: str, k: skind, kind: str) void = {
if (scopedefineinmodule(c.top, nm, mod, k, nil, d) != nil) { return; };
let prev: *sym = scopesamekeysym(c.top, nm, mod);
if (prev != nil) {
if (prev.decl == nil) { return; };
if (prev.decl.file.len == 0) { return; };
};
cerr(d.file); cerr(": error: duplicate "); cerr(kind);
cerr(" "); cerr(nm); cerr("\n");
c.errs += 1;
};
// stamptuplebinds — distribute a tuple's per-element types onto a
@@ -5586,8 +5615,13 @@ export fn checkfile(c: *checker, file: *node) void = {
tab.lhs = tsl;
tab.rhs = arr;
// pass 1 already ran; install the table name now so main
// resolves it (declmod returns "" for tab.nmod="").
installdecl(c, file, tab);
// resolves it (declmod returns "" for tab.nmod=""). Goes
// direct to scopedefineinmodule, NOT installdecl: cstage's
// synth install (cmd/wcc/check.c:3079) bypasses the
// duplicate-decl reject (#23) the same way, so a pathological
// user `const __wwtests` stays a downstream type error in
// both stages rather than a dup-diagnostic in only one.
scopedefineinmodule(c.top, tab.str, "", skind.SK_VAR, nil, tab);
let arg: *node = newnode(nkind.N_IDENT, pf, pl, pc);
arg.str = "__wwtests";

View File

@@ -10330,6 +10330,30 @@ export fn scopedefineinmodule(s: *scope, name: str, mod: str, k: skind, t: *tinf
return sy;
};
// scopesamekeysym — the entry scopedefineinmodule(name, mod) treats as a
// duplicate (same name, same mod-key), or nil if the key is free. Lets a
// caller that got a nil from scopedefineinmodule learn WHAT it collided
// with (e.g. a pre-seeded builtin vs a genuine user redeclaration). The
// match logic mirrors scopedefineinmodule's reject branch exactly.
export fn scopesamekeysym(s: *scope, name: str, mod: str) *sym = {
let h: u64 = hashstr(name);
let bi: i32 = (h % (s.nbuckets: u64)): i32;
let b: *sym = s.buckets[bi];
for (b != nil) {
if (streq(b.name, name)) {
if (b.mod.len == 0) {
if (mod.len == 0) { return b; };
} else {
if (mod.len > 0) {
if (streq(b.mod, mod)) { return b; };
};
};
};
b = b.hashnext;
};
return nil;
};
// selfhost/cmd/wcc/check.ww — minimal port of cmd/wcc/check.c.
//
// Status: name-resolution + primitive-type seeding only. Full type
@@ -10592,16 +10616,12 @@ fn checkmoduleshadow(c: *checker, name: str, kindstr: str) void = {
// scopelookupuselocal (lib/ww/sym.ww) to the SK_USE that coexists in the
// landed scope — the coexistence-equivalent of cstage's use_alias bit.
// Cite: project memory module_type_name_collision (cstage fix
// 2026-05-13). #11 (wwstage checkfile pass) revisits the dup-decl errors
// below when wwstage grows a real check pass on the cgen path.
// TODO(#11): cstage check.c errors on duplicate top-level type/def/fn
// (see cmd/wcc/check.c L1800/L1839/L1860 "duplicate <kind>") and on
// duplicate top-level let (cmd/wcc/check.c L1880, "duplicate let %s")
// once #32 lands. Wwstage's installdecl just drops the second insert
// silently. Add `if (s == nil) err(...)` here once #11 wires checkfile
// into w6c_ww. Silent-accept matches the deferred-check design — see
// test/wcc/708 and test/wcc/696 for the same cstage-only neg-case
// precedent.
// 2026-05-13).
// #23: top-level duplicate type/def/fn/let now reject loud here, keyed
// on (name, mod) exactly as cstage's install pass does
// (cmd/wcc/check.c:2852/2911/2932/2955) — see dupdecl below. (Same-scope
// LOCAL dup `let a=1; let a=2;` is a different path and stays deferred to
// #11; test/wcc/708 + test/wcc/696 are that cstage-only neg-case.)
fn installdecl(c: *checker, file: *node, d: *node) void = {
if (d == nil) { return; };
let k: nkind = d.kind;
@@ -10619,10 +10639,43 @@ fn installdecl(c: *checker, file: *node, d: *node) void = {
};
scopedefine(c.top, nm, skind.SK_USE, nil, d); return;
};
if (k == nkind.N_DEF) { scopedefineinmodule(c.top, nm, mod, skind.SK_DEF, nil, d); return; };
if (k == nkind.N_TYPEDECL) { scopedefineinmodule(c.top, nm, mod, skind.SK_TYPE, nil, d); return; };
if (k == nkind.N_FNDECL) { scopedefineinmodule(c.top, nm, mod, skind.SK_FN, nil, d); return; };
if (k == nkind.N_LET) { scopedefineinmodule(c.top, nm, mod, skind.SK_VAR, nil, d); return; };
if (k == nkind.N_DEF) { installtop(c, d, nm, mod, skind.SK_DEF, "def"); return; };
if (k == nkind.N_TYPEDECL) { installtop(c, d, nm, mod, skind.SK_TYPE, "type"); return; };
if (k == nkind.N_FNDECL) { installtop(c, d, nm, mod, skind.SK_FN, "fn"); return; };
if (k == nkind.N_LET) { installtop(c, d, nm, mod, skind.SK_VAR, "let"); return; };
};
// installtop — install a top-level decl name into c.top, rejecting a
// genuine within-module duplicate loud (#23) with cstage's exact
// "duplicate <kind> %s" wording (cmd/wcc/check.c:2852/2911/2932/2955).
//
// scopedefineinmodule returns nil only on a same-(name, mod) re-install.
// Same-leaf cross-package decls carry distinct mods (the flat-bundle
// model) and an imported-module bareword's SK_USE keys on mod="", so a
// coexisting same-leaf type/fn in its own package never collides.
//
// The one nil that is NOT a user duplicate: a redeclaration of a
// pre-seeded builtin (the predeclared `nomem`, or a primtype name). cstage
// keeps no builtins in the scope at all — resolve_typename consults
// lookup_builtin FIRST (cmd/wcc/check.c:69) and the builtin always wins,
// so a user `type nomem = !void` / `type int = ...` installs dead and is
// silently ignored, never a duplicate error. wwstage seeds builtins INTO
// c.top, so the same redeclaration surfaces here as a collision; we mirror
// cstage by dropping it (the seeded builtin stays, and wins resolution)
// rather than erroring. A pre-seeded builtin is identified by its sym
// carrying no real source decl (primtypes: decl=nil; `nomem`: a
// checkinit-synthesized N_TYPEDECL with an empty .file) — user decls
// always carry their parsed source file.
fn installtop(c: *checker, d: *node, nm: str, mod: str, k: skind, kind: str) void = {
if (scopedefineinmodule(c.top, nm, mod, k, nil, d) != nil) { return; };
let prev: *sym = scopesamekeysym(c.top, nm, mod);
if (prev != nil) {
if (prev.decl == nil) { return; };
if (prev.decl.file.len == 0) { return; };
};
cerr(d.file); cerr(": error: duplicate "); cerr(kind);
cerr(" "); cerr(nm); cerr("\n");
c.errs += 1;
};
// stamptuplebinds — distribute a tuple's per-element types onto a
@@ -15918,8 +15971,13 @@ export fn checkfile(c: *checker, file: *node) void = {
tab.lhs = tsl;
tab.rhs = arr;
// pass 1 already ran; install the table name now so main
// resolves it (declmod returns "" for tab.nmod="").
installdecl(c, file, tab);
// resolves it (declmod returns "" for tab.nmod=""). Goes
// direct to scopedefineinmodule, NOT installdecl: cstage's
// synth install (cmd/wcc/check.c:3079) bypasses the
// duplicate-decl reject (#23) the same way, so a pathological
// user `const __wwtests` stays a downstream type error in
// both stages rather than a dup-diagnostic in only one.
scopedefineinmodule(c.top, tab.str, "", skind.SK_VAR, nil, tab);
let arg: *node = newnode(nkind.N_IDENT, pf, pl, pc);
arg.str = "__wwtests";