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:
2026-05-16 11:17:20 +09:00
parent ed94467a0c
commit b8b32ab80c
9 changed files with 432 additions and 34 deletions

View File

@@ -1441,7 +1441,10 @@ clet(Checker *c, Node *n)
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;
if (s == NULL)
err(c, n->pos, "let '%s' redeclared in same scope",
n->str);
else if (n->op == TK_CONST) s->is_const = 1;
}
}
@@ -1502,8 +1505,11 @@ cstmt(Checker *c, Node *n)
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 (scope_define(c->cur, nm->str,
SK_VAR, ft, nm) == NULL)
err(c, nm->pos,
"binding '%s' redeclared in same scope",
nm->str);
}
if (tp) tp = tp->next;
}
@@ -1560,7 +1566,11 @@ cstmt(Checker *c, Node *n)
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;
if (s == NULL)
err(c, l->pos,
"let '%s' redeclared in same scope",
l->str);
else if (n->op == TK_CONST) s->is_const = 1;
}
if (tp) tp = tp->next;
}
@@ -1876,9 +1886,10 @@ check_file(Checker *c, Node *file)
prev->decl = d;
prev->use_alias = 1;
if (mod && prev->mod == NULL) prev->mod = mod;
} else
scope_define_in_module(c->cur, d->str,
mod, SK_VAR, t, d);
} else if (!scope_define_in_module(c->cur,
d->str, mod, SK_VAR, t, d))
err(c, d->pos, "duplicate let %s",
d->str);
}
break;
}
@@ -1911,8 +1922,11 @@ check_file(Checker *c, Node *file)
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);
if (scope_define(c->cur, p->name,
SK_PARAM, p->type, d) == NULL)
err(c, d->pos,
"param '%s' redeclared",
p->name);
}
}
Type *prev = c->ret;