From 999b110001d0353d095c6127a983bcb5b6be00cb Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Sun, 9 Aug 2026 01:59:33 +0900 Subject: [PATCH] wcc: forrange bindings get a per-loop scope; dup binders reject The TODO(#11) silent-accept of `for (let (a, a) .. xs)` rested on a stale premise -- resolvewalk has per-block scopes since #53 and IS the live w6c_ww checker pass. Bindings now install in a per-LOOP scope (the N_MCASE pattern), so a duplicate name within one pattern errs "redeclared in same scope" (cstage parity via stamptuplebinds' scopedefine-nil check) while sequential same-name loops stay legal. kwtab restores its stated alphabetical order (`import` before `is`, both stages, kinds swapped in lockstep with names). --- cmd/wcc/tok.c | 2 +- internal/wwfixture/types.ww | 8 ++--- lib/ww/syntax/tok.ww | 8 ++--- selfhost/cmd/wcc/check.ww | 36 +++++++++++++------ .../data/forrange_dup_binder_reject/case.ww | 11 ++++++ 5 files changed, 45 insertions(+), 20 deletions(-) create mode 100644 test/wcc/data/forrange_dup_binder_reject/case.ww diff --git a/cmd/wcc/tok.c b/cmd/wcc/tok.c index d4bbfd6b..8ebc6ade 100644 --- a/cmd/wcc/tok.c +++ b/cmd/wcc/tok.c @@ -24,8 +24,8 @@ static const struct kwent kwtab[] = { { "fn", TK_FN }, { "for", TK_FOR }, { "if", TK_IF }, - { "is", TK_IS }, { "import", TK_USE }, + { "is", TK_IS }, { "let", TK_LET }, { "match", TK_MATCH }, { "nil", TK_NIL }, diff --git a/internal/wwfixture/types.ww b/internal/wwfixture/types.ww index 5b9e7e27..710789d4 100644 --- a/internal/wwfixture/types.ww +++ b/internal/wwfixture/types.ww @@ -1,13 +1,13 @@ package wwfixture; def protocolversion: i32 = 1; -def corpuscount: i32 = 1758; -def errorcount: i32 = 351; +def corpuscount: i32 = 1759; +def errorcount: i32 = 352; def compilecount: i32 = 21; def runcount: i32 = 209; def runexitcount: i32 = 1177; -def nativecount: i32 = 3516; -def corpushash: str = "ab5ae796c10e476af0c1f40e0d0b216b3eb708f62b6ee17aafcf79d5e1dd8a69"; +def nativecount: i32 = 3518; +def corpushash: str = "38613db49b4c87a14d87fb9392e5500e17cc0c95767fb31d8eb9bc4fe2bf74c5"; type directive = enum i32 { ERROR = 0, diff --git a/lib/ww/syntax/tok.ww b/lib/ww/syntax/tok.ww index 116b0953..6ee01eba 100644 --- a/lib/ww/syntax/tok.ww +++ b/lib/ww/syntax/tok.ww @@ -156,16 +156,16 @@ export type tok = struct { // (probe at cc69daf — len() returns 0, no diagnostic); filed bug. let kwnames: [30]str = [ "as", "break", "case", "chan", "const", "continue", "def", "defer", - "else", "enum", "export", "false", "fn", "for", "if", "is", - "import", "let", "match", "nil", "package", "proc", "return", + "else", "enum", "export", "false", "fn", "for", "if", "import", + "is", "let", "match", "nil", "package", "proc", "return", "static", "struct", "switch", "true", "type", "void", "yield", ]; let kwkinds: [30]tkind = [ tkind.TK_AS, tkind.TK_BREAK, tkind.TK_CASE, tkind.TK_CHAN, tkind.TK_CONST, tkind.TK_CONTINUE, tkind.TK_DEF, tkind.TK_DEFER, tkind.TK_ELSE, tkind.TK_ENUM, tkind.TK_EXPORT, tkind.TK_FALSE, - tkind.TK_FN, tkind.TK_FOR, tkind.TK_IF, tkind.TK_IS, - tkind.TK_USE, tkind.TK_LET, tkind.TK_MATCH, tkind.TK_NIL, + tkind.TK_FN, tkind.TK_FOR, tkind.TK_IF, tkind.TK_USE, + tkind.TK_IS, tkind.TK_LET, tkind.TK_MATCH, tkind.TK_NIL, tkind.TK_MODULE, tkind.TK_PROC, tkind.TK_RETURN, tkind.TK_STATIC, tkind.TK_STRUCT, tkind.TK_SWITCH, tkind.TK_TRUE, tkind.TK_TYPE, tkind.TK_VOID, tkind.TK_YIELD, diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index b75c8f8b..263ce52a 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -443,7 +443,19 @@ fn stamptuplebinds(c: *checker, binds: *syntax.node, elems: *syntax.node, let bnm: str = b.str; if (bnm.len > 0) { checkmoduleshadow(c, bnm, what); - syntax.scopedefine(c.cur, bnm, syntax.skind.SK_VAR, nil, b); + // first registration wins; a nil return is a + // same-scope duplicate — `let (a, a) = ..` / + // `for (let (a, a) .. xs)`. Mirrors cstage + // scope_define == NULL → "redeclared". + let ds: *syntax.sym = syntax.scopedefine(c.cur, bnm, syntax.skind.SK_VAR, nil, b); + if (ds == nil) { + cerr("error: "); + cerr(what); + cerr(" '"); + cerr(bnm); + cerr("' redeclared in same scope\n"); + c.errs += 1; + }; }; }; if (b.type_ == nil) { @@ -550,17 +562,17 @@ fn resolvewalk(c: *checker, n: *syntax.node) void = { // `for (let x .. slice) body` / `for (let (a, b) .. slice) body` — // 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. + // install bindings and walk the body/else. The bindings live in a + // per-LOOP scope (the N_MCASE pattern): sequential same-name loops + // each rebind fresh, and a duplicate name WITHIN one pattern + // (`for (let (a, a) .. xs)`) now errs via stamptuplebinds' dup + // check — the old TODO(#11) silent-accept rested on a stale + // premise (resolvewalk HAS block scopes since #53 and IS the live + // w6c_ww checker pass). if (k == syntax.nkind.N_FORRANGE) { if (n.lhs != nil) { resolvewalk(c, n.lhs); }; + let frouter: *syntax.scope = c.cur; + c.cur = syntax.newscope(frouter); if (n.list != nil) { // Tuple destructure `for (let (a,b) .. xs)`: peel the // iterable's element type and distribute its tuple @@ -628,10 +640,12 @@ fn resolvewalk(c: *checker, n: *syntax.node) void = { }; // `break`/`continue` in the body target this loop; the `else` // block runs at normal cond-false exit (skipped by break) and - // targets an ENCLOSING loop. Mirrors cstage cmd/wcc/check.c:2494. + // targets an ENCLOSING loop — outside the loop scope, like + // cstage's saved-scope restore. Mirrors cmd/wcc/check.c:2494. c.loops += 1; if (n.body != nil) { resolvewalk(c, n.body); }; c.loops -= 1; + c.cur = frouter; if (n.els != nil) { resolvewalk(c, n.els); }; return; }; diff --git a/test/wcc/data/forrange_dup_binder_reject/case.ww b/test/wcc/data/forrange_dup_binder_reject/case.ww new file mode 100644 index 00000000..2653c88c --- /dev/null +++ b/test/wcc/data/forrange_dup_binder_reject/case.ww @@ -0,0 +1,11 @@ +//ww:error "redeclared in same scope" +// A duplicate name within one destructure pattern; the old TODO(#11) +// silent-accept rested on a stale premise (resolvewalk has block +// scopes since #53). Bindings live in a per-LOOP scope, so sequential +// same-name loops stay legal. +package main; +export fn main() i32 = { + let s: [](i64, i64) = alloc([], 1u64)!; + for (let (a, a) .. s) { }; + return 0; +};