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

@@ -44,10 +44,10 @@
* same_name_diff_type | `let a: i32 = 5;` then disjoint | exit=2
* | block `let a: str = "hi";`. Returns|
* | a.len from the str scope. |
* same_block_redecl_pin | `let a: i32 = 1; let a: i32 = 9;` | exit=9
* | in same block (checker silently |
* | accepts today). Pins last-write- |
* | wins via head-first localfind. |
* (removed) | `let a: i32 = 1; let a: i32 = 9;` | --
* | post-#32 the checker rejects this; |
* | covered by 712_redecl's |
* | neg_let_same_block row. |
* defer_shadow | outer `a`, deferred call captures | exit=42
* | &outer-a, inner-block shadow `a`, |
* | return outer a. Pins both: cgfn |
@@ -165,23 +165,13 @@ static const struct row rows[] = {
" return r;\n"
"};\n",
2 },
/* 5. Same-block re-declaration. Today's checker silently accepts
* `let a: i32 = 0; let a: i32 = 9;` (scope_define returns NULL
* on dup but the caller in cmd/wcc/check.c:1443 doesn't error;
* wwstage check.ww behaves the same). Pre-fix: both lets shared
* one slot, last write wins by storage. Post-fix: each let gets
* its own slot but localfind walks head-first → still last-
* write-wins observably. This row pins that observable contract
* — if the checker tightens later to reject same-scope redecl
* (task #32), this row is the canary that flips from "exit 9"
* to "build fails", explicitly opting in to the new shape. */
{ "same_block_redecl_pin",
"fn main() i32 = {\n"
" let a: i32 = 1;\n"
" let a: i32 = 9;\n"
" return a;\n"
"};\n",
9 },
/* 5. Same-block re-declaration moved to test/wcc/712_redecl
* (`neg_let_same_block`) when #32 made it a build-time error.
* The pin's purpose — exercising the localoff fresh-stub path on
* a same-name same-block dup — is now an upstream-rejected shape,
* so the codegen branch it covered is no longer reachable through
* legal source. Row slot kept empty for stability of the
* surrounding row numbering. */
/* 6. Defer + inner-block shadow + outer-scope post-defer read.
*
* `defer touch(&a)` queues the call; at fn-exit the defer's