From 19e9535ef4521713fbcbdc28dee04441c5743957 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Thu, 11 Jun 2026 00:57:43 +0900 Subject: [PATCH] wcc/ww: reject duplicate top-level decls, aligned to cstage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit installdecl routes all four kinds (fn/type/def/let) through installtop, which turns scopedefineinmodule's nil return into cstage's exact "duplicate " 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) --- lib/ww/sym.ww | 24 +++++++ selfhost/cmd/w6c/main.combined.ww | 90 ++++++++++++++++++++----- selfhost/cmd/wcc/check.ww | 66 ++++++++++++++----- selfhost/cmd/wwdump/main.combined.ww | 90 ++++++++++++++++++++----- test/wcc/910_at_test.c | 73 +++++++++++++++++++- test/wcc/997_at_test_ww.c | 99 +++++++++++++++++++++++++++- test/wcc/data/attest_userrun.ww | 9 +++ test/wcc/data/builtin_redecl_ok.ww | 14 ++++ test/wcc/data/dup_def.ww | 5 ++ test/wcc/data/dup_fn.ww | 5 ++ test/wcc/data/dup_let.ww | 5 ++ test/wcc/data/dup_type.ww | 5 ++ test/wcc/data/dup_xpkg_ok.ww | 13 ++++ 13 files changed, 448 insertions(+), 50 deletions(-) create mode 100644 test/wcc/data/attest_userrun.ww create mode 100644 test/wcc/data/builtin_redecl_ok.ww create mode 100644 test/wcc/data/dup_def.ww create mode 100644 test/wcc/data/dup_fn.ww create mode 100644 test/wcc/data/dup_let.ww create mode 100644 test/wcc/data/dup_type.ww create mode 100644 test/wcc/data/dup_xpkg_ok.ww diff --git a/lib/ww/sym.ww b/lib/ww/sym.ww index 0c78e7f1..c7c3937b 100644 --- a/lib/ww/sym.ww +++ b/lib/ww/sym.ww @@ -276,3 +276,27 @@ export fn scopedefineinmodule(s: *scope, name: str, mod: str, k: skind, t: *tinf s.last = sy; 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; +}; diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index e57808da..dba71a7a 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -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 ") 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 %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"; diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index c594c81a..b5ae9006 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -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 ") 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 %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"; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 2f87138a..40f62a99 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -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 ") 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 %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"; diff --git a/test/wcc/910_at_test.c b/test/wcc/910_at_test.c index f1231f29..0d44554b 100644 --- a/test/wcc/910_at_test.c +++ b/test/wcc/910_at_test.c @@ -265,6 +265,58 @@ linkfail(const char *bin, const char *comp) return rc; } +/* accept — ` ` (NON-T) must exit zero. The #23 cross-package + * legal control: distinct-mod same-leaf decls coexist, so the dup reject + * keyed on (name, mod) must NOT fire. */ +static int +accept(const char *bin, const char *comp, const char *fixture, const char *what) +{ + char cmd[4096]; + snprintf(cmd, sizeof cmd, "%s/%s %s -o /dev/null 2>/dev/null", + bin, comp, fixture); + if (runwait(cmd) != 0) { + fprintf(stderr, "910 FAIL: %s rejected %s (expected accept)\n", + comp, what); + return 1; + } + return 0; +} + +/* collide_run — a @test unit defining a user `fn run` collides with + * lib/test's bound runner once ` test -c` bundles it; ` -T` of + * the combined must loud-reject the duplicate (#23). Pre-fix wwstage built + * a binary that called the user run and silently skipped every @test. */ +static int +collide_run(const char *bin, const char *comp, const char *drv) +{ + int pid = getpid(); + char stem[256], comb[300], cmd[4096]; + snprintf(stem, sizeof stem, "/tmp/at910cr_%s_%d", comp, pid); + snprintf(comb, sizeof comb, "%s.combined.ww", stem); + snprintf(cmd, sizeof cmd, + "%s/%s test -c -o %s test/wcc/data/attest_userrun.ww > /dev/null 2>&1", + bin, drv, stem); + runwait(cmd); + if (access(comb, 0) != 0) { + fprintf(stderr, "910 FAIL: %s build produced no %s\n", drv, comb); + return 1; + } + snprintf(cmd, sizeof cmd, "%s/%s -T %s -o /dev/null 2>/dev/null", + bin, comp, comb); + int rc = 0; + if (runwait(cmd) == 0) { + fprintf(stderr, "910 FAIL: %s -T accepted user `fn run` collision " + "(expected duplicate-fn reject)\n", comp); + rc = 1; + } + unlink(comb); + char tmp[320]; + snprintf(tmp, sizeof tmp, "%s.s", stem); unlink(tmp); + snprintf(tmp, sizeof tmp, "%s.o", stem); unlink(tmp); + unlink(stem); + return rc; +} + int main(void) { @@ -280,7 +332,26 @@ main(void) "undefined symbol in @test body") != 0) return 1; if (linkfail(bin, "w6c") != 0) return 1; + /* #23 — top-level duplicate-decl reject (one row per kind: all four + * route through installtop, so each deserves its own pin) + legal + * cross-package control + the builtin-redecl carve-out accept. */ + if (reject_plain(bin, "w6c", "test/wcc/data/dup_fn.ww", + "same-module duplicate fn") != 0) return 1; + if (reject_plain(bin, "w6c", "test/wcc/data/dup_type.ww", + "same-module duplicate type") != 0) return 1; + if (reject_plain(bin, "w6c", "test/wcc/data/dup_def.ww", + "same-module duplicate def") != 0) return 1; + if (reject_plain(bin, "w6c", "test/wcc/data/dup_let.ww", + "same-module duplicate let") != 0) return 1; + if (accept(bin, "w6c", "test/wcc/data/dup_xpkg_ok.ww", + "cross-package same-name") != 0) return 1; + if (accept(bin, "w6c", "test/wcc/data/builtin_redecl_ok.ww", + "builtin nomem redecl (carve-out)") != 0) return 1; + if (collide_run(bin, "w6c", "ww") != 0) return 1; + printf("@test -T: run ok + user-main and bad-signature rejected + " - "non-T @test drop + checked-body + dangling-call link-fail (#6)\n"); + "non-T @test drop + checked-body + dangling-call link-fail (#6) + " + "dup fn/type/def/let reject + xpkg + builtin-redecl accept + " + "fn-run collision (#23)\n"); return 0; } diff --git a/test/wcc/997_at_test_ww.c b/test/wcc/997_at_test_ww.c index 9ebcd6a6..d18d8510 100644 --- a/test/wcc/997_at_test_ww.c +++ b/test/wcc/997_at_test_ww.c @@ -288,6 +288,83 @@ reject_plain(const char *bin, const char *fixture, const char *what) return 0; } +/* accept_byteid — a #23 legal-accept control: `` must COMPILE on + * BOTH stages (no false dup reject) AND be cs/ww byte-identical (rule 10). + * Used for the cross-package same-leaf control (keying is (name, mod), so + * distinct-package leaves coexist) and the builtin-redecl carve-out (the + * one structural point where wwstage seeds builtins and cstage does not — + * byte-id proves the drop-the-redecl mirror is exact). */ +static int +accept_byteid(const char *bin, const char *fixture, const char *what) +{ + int pid = getpid(); + char cs[256], ws[256], cmd[4096]; + snprintf(cs, sizeof cs, "/tmp/at997ab_c_%d.s", pid); + snprintf(ws, sizeof ws, "/tmp/at997ab_w_%d.s", pid); + + snprintf(cmd, sizeof cmd, + "%s/w6c %s -o %s 2>/dev/null", bin, fixture, cs); + if (runwait(cmd) != 0) { + fprintf(stderr, "997 FAIL: w6c rejected %s\n", what); + return 1; + } + snprintf(cmd, sizeof cmd, + "%s/w6c_ww %s -o %s 2>/dev/null", bin, fixture, ws); + if (runwait(cmd) != 0) { + fprintf(stderr, "997 FAIL: w6c_ww rejected %s\n", what); + return 1; + } + char *bc = NULL, *bw = NULL; + size_t nc = 0, nw = 0; + int rc = 0; + if (slurp(cs, &bc, &nc) < 0 || slurp(ws, &bw, &nw) < 0) { + fprintf(stderr, "997 FAIL: slurp %s asm\n", what); + rc = 1; + } else if (nc != nw || memcmp(bc, bw, nc) != 0) { + fprintf(stderr, "997 FAIL: %s asm differs (cs %zu, ww %zu)\n", + what, nc, nw); + rc = 1; + } + free(bc); free(bw); + unlink(cs); unlink(ws); + return rc; +} + +/* collide_run — a @test unit defining a user `fn run` collides with + * lib/test's bound runner once `ww test -c` bundles it; `w6c_ww -T` of the + * combined must loud-reject the duplicate (#23). Pre-fix wwstage built a + * binary that called the user run and silently skipped every @test. */ +static int +collide_run(const char *bin) +{ + int pid = getpid(); + char stem[256], comb[300], cmd[4096]; + snprintf(stem, sizeof stem, "/tmp/at997cr_%d", pid); + snprintf(comb, sizeof comb, "%s.combined.ww", stem); + snprintf(cmd, sizeof cmd, + "%s/ww test -c -o %s test/wcc/data/attest_userrun.ww > /dev/null 2>&1", + bin, stem); + runwait(cmd); + if (access(comb, 0) != 0) { + fprintf(stderr, "997 FAIL: ww build produced no %s\n", comb); + return 1; + } + snprintf(cmd, sizeof cmd, "%s/w6c_ww -T %s -o /dev/null 2>/dev/null", + bin, comb); + int rc = 0; + if (runwait(cmd) == 0) { + fprintf(stderr, "997 FAIL: w6c_ww -T accepted user `fn run` collision " + "(expected duplicate-fn reject)\n"); + rc = 1; + } + unlink(comb); + char tmp[320]; + snprintf(tmp, sizeof tmp, "%s.s", stem); unlink(tmp); + snprintf(tmp, sizeof tmp, "%s.o", stem); unlink(tmp); + unlink(stem); + return rc; +} + int main(void) { @@ -303,7 +380,27 @@ main(void) if (reject_plain(bin, "test/wcc/data/attest_undefbody.ww", "undefined symbol in @test body") != 0) return 1; + /* #23 — wwstage top-level duplicate-decl reject (one row per kind: + * all four route through installtop) + legal cross-package control + * (byte-id) + the builtin-redecl carve-out (byte-id) + the -T + * `fn run` collision parity row. */ + if (reject_plain(bin, "test/wcc/data/dup_fn.ww", + "same-module duplicate fn") != 0) return 1; + if (reject_plain(bin, "test/wcc/data/dup_type.ww", + "same-module duplicate type") != 0) return 1; + if (reject_plain(bin, "test/wcc/data/dup_def.ww", + "same-module duplicate def") != 0) return 1; + if (reject_plain(bin, "test/wcc/data/dup_let.ww", + "same-module duplicate let") != 0) return 1; + if (accept_byteid(bin, "test/wcc/data/dup_xpkg_ok.ww", + "cross-package same-name") != 0) return 1; + if (accept_byteid(bin, "test/wcc/data/builtin_redecl_ok.ww", + "builtin nomem redecl (carve-out)") != 0) return 1; + if (collide_run(bin) != 0) return 1; + printf("@test -T (ww_ww): run ok + cs/ww byte-id + rejects + " - "non-T @test drop cs/ww byte-id + checked-body (#6)\n"); + "non-T @test drop cs/ww byte-id + checked-body (#6) + " + "dup fn/type/def/let reject + xpkg/builtin-redecl byte-id + " + "fn-run collision (#23)\n"); return 0; } diff --git a/test/wcc/data/attest_userrun.ww b/test/wcc/data/attest_userrun.ww new file mode 100644 index 00000000..813a99a7 --- /dev/null +++ b/test/wcc/data/attest_userrun.ww @@ -0,0 +1,9 @@ +// #23 -T collision — a user `fn run` collides with lib/test's bound +// runner `run` (the synth's callee) once `ww test -c` bundles lib/test. +// Both stages must loud-reject "duplicate fn run" (rc!=0), never silently +// build a binary that calls the wrong run and skips every @test. +fn run() void = { return; }; + +@test fn t_one() void = { + assert(1 == 1); +}; diff --git a/test/wcc/data/builtin_redecl_ok.ww b/test/wcc/data/builtin_redecl_ok.ww new file mode 100644 index 00000000..38826204 --- /dev/null +++ b/test/wcc/data/builtin_redecl_ok.ww @@ -0,0 +1,14 @@ +// #23 builtin carve-out — a user redecl of a pre-seeded builtin name is +// NOT a duplicate (cstage keeps no builtins in scope: lookup_builtin wins +// first at check.c:69, so the user version installs dead). wwstage seeds +// builtins into c.top, so installtop must DROP this redecl rather than +// erroring. Both stages accept; the builtin `nomem` (= !void) wins, so +// g()'s i32 arm returns 7. Pins the 771/774/926 shape. +type nomem = !void; +fn g() (i32 | nomem) = { return 7; }; +export fn main() i32 = { + match (g()) { + case let v: i32 => return v; + case nomem => return 99; + }; +}; diff --git a/test/wcc/data/dup_def.ww b/test/wcc/data/dup_def.ww new file mode 100644 index 00000000..c7ab771b --- /dev/null +++ b/test/wcc/data/dup_def.ww @@ -0,0 +1,5 @@ +// #23 — two top-level defs of the same name in one (flat) module. +// cstage's install pass rejects "duplicate def D"; wwstage now mirrors. +def D: i32 = 1; +def D: i32 = 2; +export fn main() i32 = { return 0; }; diff --git a/test/wcc/data/dup_fn.ww b/test/wcc/data/dup_fn.ww new file mode 100644 index 00000000..2977f191 --- /dev/null +++ b/test/wcc/data/dup_fn.ww @@ -0,0 +1,5 @@ +// #23 — two top-level fns of the same name in the same (flat) module. +// cstage's install pass rejects "duplicate fn foo"; wwstage now mirrors. +fn foo() i32 = { return 1; }; +fn foo() i32 = { return 2; }; +export fn main() i32 = { return foo(); }; diff --git a/test/wcc/data/dup_let.ww b/test/wcc/data/dup_let.ww new file mode 100644 index 00000000..a42a0530 --- /dev/null +++ b/test/wcc/data/dup_let.ww @@ -0,0 +1,5 @@ +// #23 — two top-level lets of the same name in one (flat) module. +// cstage's install pass rejects "duplicate let g"; wwstage now mirrors. +let g: i32 = 1; +let g: i32 = 2; +export fn main() i32 = { return 0; }; diff --git a/test/wcc/data/dup_type.ww b/test/wcc/data/dup_type.ww new file mode 100644 index 00000000..6bae68c0 --- /dev/null +++ b/test/wcc/data/dup_type.ww @@ -0,0 +1,5 @@ +// #23 — two top-level types of the same name in one (flat) module. +// cstage's install pass rejects "duplicate type t"; wwstage now mirrors. +type t = i32; +type t = u32; +export fn main() i32 = { return 0; }; diff --git a/test/wcc/data/dup_xpkg_ok.ww b/test/wcc/data/dup_xpkg_ok.ww new file mode 100644 index 00000000..e267031f --- /dev/null +++ b/test/wcc/data/dup_xpkg_ok.ww @@ -0,0 +1,13 @@ +// #23 legal control — same leaf `foo` in DISTINCT packages of one flat +// bundle is legal (the dup key is (name, mod), not bare name). Mirrors a +// driver-emitted *.combined.ww. Must compile clean + byte-identical. +package aa; +export fn foo() i32 = { return 1; }; + +package bb; +export fn foo() i32 = { return 2; }; + +package main; +import aa; +import bb; +export fn main() i32 = { return aa.foo() + bb.foo(); };