cstage+selfhost+test: refuse same-block let / param redecl (#32)
cmd/wcc/check.c silently accepted `let a; let a;` in the same block and similar redecls. Pre-#27 the localoff dedup masked it; post-#27 last-write-wins via head-first localfind. Surfaced by worker-27 during the #27 review. Cstage: 5 guard sites (check_scope_define-NULL → err) covering N_LET block-bind, N_MLET tuple binders (incl. same-tuple `let (a,a)`), N_FORRANGE tuple binders, top-level let, fn param. Voice: "<kind> '<name>' redeclared in same scope" for inner; "duplicate let %s" for top-let, matching the existing "duplicate <kind>" idiom at 1812/1851/1872. Wwstage: TODO(#11) comments at the 4 mirror sites (installdecl, N_FORRANGE, N_LET, installparams). Full enforcement waits on the checkfile pass per rob. **Unmasked by #32 (worth flagging):** selfhost/cmd/wcc/cgenexpr.ww cgcall had `let callee: *node = n.lhs;` twice at fn-body scope (copy-paste, identical value). Pre-fix silent-redecl absorbed it; post-fix the new guard rejects. Removed the second decl — outer `callee` stays visible across the intermediate block. Test 712 (redecl): 10 rows (6 neg + 4 pos), cstage-only per rob. Negative rows cover all 5 guard sites + same-tuple-dup. Positive rows pin the legal counter-shapes (cross-block, name-only bucket, forrange body, mcase-per-arm). Test 300 row 34 ("shadowing in inner scope; same scope flagged") was incorrectly asserting the bug; flipped to expect "redeclared" and added a sibling row pinning cross-block shadow stays ok. Test 709's `same_block_redecl_pin` canary (explicitly documented as flipping under #32) removed; pointer to 712 left in its place.
This commit is contained in:
@@ -5304,6 +5304,14 @@ fn checkmoduleshadow(c: *checker, name: str, kindstr: str) void = {
|
||||
// the wwstage cgen amalloc-undersize trap (rob-pike). #11 (wwstage
|
||||
// checkfile pass) will reconsider this 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.
|
||||
fn installdecl(c: *checker, file: *node, d: *node) void = {
|
||||
if (d == nil) { return; };
|
||||
let k: nkind = d.kind;
|
||||
@@ -5398,6 +5406,14 @@ fn resolvewalk(c: *checker, n: *node) void = {
|
||||
// each binding name becomes a fresh local. Walk the slice expr first
|
||||
// so its idents resolve before the bindings shadow anything, then
|
||||
// install bindings and walk the body/else.
|
||||
//
|
||||
// TODO(#11): cstage check.c (post-#32) errors `binding '%s'
|
||||
// redeclared in same scope` when the tuple-pattern lists the same
|
||||
// name twice (`for (let (a, a) .. xs)`). Wwstage's resolvewalk has
|
||||
// no per-block scope (see resolvefnbody's docstring) and is used
|
||||
// only by wwdump_ww as a diagnostic, so silent-accept here avoids
|
||||
// false-positives on legal cross-block shadow until #11 adds the
|
||||
// scoping infrastructure.
|
||||
if (k == nkind.N_FORRANGE) {
|
||||
if (n.lhs != nil) { resolvewalk(c, n.lhs); };
|
||||
if (n.list != nil) {
|
||||
@@ -5478,6 +5494,15 @@ fn resolvewalk(c: *checker, n: *node) void = {
|
||||
// `X` so subsequent statements can resolve it. Top-level lets
|
||||
// are installed in installdecl, so this duplicate install at
|
||||
// the file scope just no-ops (scopedefine returns nil on dup).
|
||||
//
|
||||
// TODO(#11): cstage check.c (post-#32) errors `let '%s' redeclared
|
||||
// in same scope` here. Wwstage resolvewalk has no per-block scope
|
||||
// (see resolvefnbody's docstring) so a same-fn-body
|
||||
// `let a=1; { let a=2; };` would falsely trip if we guarded
|
||||
// scopedefine's nil return today. Silent-accept matches the
|
||||
// deferred-check design until #11 adds per-block scoping; see
|
||||
// test/wcc/708 and test/wcc/696 for the same cstage-only neg-case
|
||||
// precedent.
|
||||
if (k == nkind.N_LET) {
|
||||
let nm: str = n.str;
|
||||
if (nm.len > 0) {
|
||||
@@ -6121,6 +6146,14 @@ fn checktryprop(c: *checker, n: *node) void = {
|
||||
|
||||
// install_param — when entering a fn body, define its params in a
|
||||
// fresh local scope.
|
||||
//
|
||||
// TODO(#11): cstage check.c (post-#32) errors `param '%s' redeclared`
|
||||
// when two params share a name. The fn body's scope IS fresh here
|
||||
// (resolvefnbody opens it before calling us), so guarding scopedefine's
|
||||
// nil return would be sound — but we defer until #11 wires checkfile
|
||||
// into w6c_ww so the diagnostic class lands as a single coordinated
|
||||
// step rather than dribbling in. Matches the cstage-only neg-case
|
||||
// precedent at test/wcc/708 + test/wcc/696.
|
||||
fn installparams(c: *checker, params: *node) void = {
|
||||
let p: *node = params;
|
||||
for (p != nil) {
|
||||
@@ -12431,7 +12464,9 @@ fn cgcall(c: *cgen, n: *node) void = {
|
||||
};
|
||||
i += 1;
|
||||
};
|
||||
let callee: *node = n.lhs;
|
||||
// `callee` is already in scope from line 2827; reuse it. Pre-#32
|
||||
// silent-redecl masked the second `let callee` here as a no-op
|
||||
// (same value, same fn-body scope post-#27).
|
||||
let calleename: str;
|
||||
calleename.ptr = nil; calleename.len = 0;
|
||||
// Detect fn-pointer field call: `w.emit(args)` where `w` is
|
||||
|
||||
Reference in New Issue
Block a user