diff --git a/Makefile b/Makefile index 630cba7d..d91299fa 100644 --- a/Makefile +++ b/Makefile @@ -235,6 +235,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_fnlabel_mangle \ $(BIN)/test_cgreturn_variant_zero \ $(BIN)/test_arrlit_str_full \ + $(BIN)/test_redecl \ $(BIN)/test_param_shadow_mod \ $(BIN)/test_localoff_scope \ $(BIN)/test_cast_enum_movl \ @@ -457,6 +458,11 @@ $(BIN)/test_arrlit_str_full: test/wcc/711_arrlit_str_full.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_redecl: test/wcc/712_redecl.c \ + $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ + $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_use_promote_alias: test/wcc/699_use_promote_alias.c \ $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(LIB)/libwwrt.a | $(BIN) diff --git a/cmd/wcc/check.c b/cmd/wcc/check.c index e96b5b5f..030c1601 100644 --- a/cmd/wcc/check.c +++ b/cmd/wcc/check.c @@ -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; diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 9592734c..9d03a411 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -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 ") 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 diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index acc5100c..43ef6dee 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -3064,7 +3064,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 diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index 13b03d51..d47a5118 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -179,6 +179,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 ") 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; @@ -273,6 +281,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) { @@ -353,6 +369,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) { @@ -996,6 +1021,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) { diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index f32c9311..a850686b 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -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 ") 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 diff --git a/test/wcc/300_check.c b/test/wcc/300_check.c index 85f84570..2f4fe981 100644 --- a/test/wcc/300_check.c +++ b/test/wcc/300_check.c @@ -96,7 +96,9 @@ static const struct row rows[] = { { "fn f() void = { -true; };", "non-numeric" }, { "fn f() void = { *5; };", "deref non-pointer" }, { "fn f() void = { let x: i32 = 1; let x: i32 = 2; };", - "ok" }, /* shadowing in inner scope; same scope flagged */ + "redeclared" }, /* same-scope let-redecl rejected (#32). */ + { "fn f() void = { let x: i32 = 1; { let x: i32 = 2; }; };", + "ok" }, /* nested-block shadow stays legal. */ { "fn f() void = { break; };", "break outside loop" }, { "fn f() void = { continue; };", "continue outside loop" }, { "fn f(x: i32) void = { x[0]; };", "indexing non-indexable" }, diff --git a/test/wcc/709_localoff_scope.c b/test/wcc/709_localoff_scope.c index fe3c4445..1e504371 100644 --- a/test/wcc/709_localoff_scope.c +++ b/test/wcc/709_localoff_scope.c @@ -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 diff --git a/test/wcc/712_redecl.c b/test/wcc/712_redecl.c new file mode 100644 index 00000000..907f0f00 --- /dev/null +++ b/test/wcc/712_redecl.c @@ -0,0 +1,281 @@ +/* + * 712_redecl — check: refuse same-scope let/mlet/param/top-let + * redeclarations (task #32). + * + * Pre-fix: cmd/wcc/check.c silently dropped the duplicate insert when + * scope_define returned NULL. `let a: i32 = 1; let a: i32 = 2;` in + * one block compiled cleanly and the second store last-write-wins; + * `fn f(a: i32, a: i32)` accepted both params, body resolved to the + * second. Surfaced post-#27 (which dropped the localoff name-dedup + * shim that had been masking the issue at codegen) — see commit + * 1292f98's follow-up note and worker-27's filed task. + * + * Post-fix: cstage check.c errors at every site where scope_define / + * scope_define_in_module's NULL return was previously ignored: + * + * site | message + * ---------------------------+-------------------------------------- + * block-body N_LET | let '%s' redeclared in same scope + * N_MLET (`let (a,b)=…`) | let '%s' redeclared in same scope + * N_FORRANGE tuple-binding | binding '%s' redeclared in same scope + * N_FNDECL param | param '%s' redeclared + * top-level N_LET | duplicate let %s + * + * N_MCASE (case-binding) and N_FORRANGE single-binding already lived + * in fresh per-arm / per-loop scopes with at most one bind, so they + * weren't part of the bug class. Top-level N_TYPEDECL / N_DEF / + * N_FNDECL already errored on NULL ("duplicate %s"). + * + * Cstage-only. Wwstage's check.ww is a single-pass resolve walk with + * no per-block scoping (line 1015 comment), exercised only by + * wwdump_ww as a diagnostic; adding the guard there today would + * false-positive on legal cross-block shadow. Tracked under #11 + * (wwstage checkfile pass with per-block scoping). Matches the + * test/wcc/708 + test/wcc/696 cstage-only neg-case precedent. + * + * row | gate | what it pins + * --------------------------+----------------+-------------------- + * neg_let_same_block | build fails | site 1443 + * neg_mlet_same_block | build fails | site 1562 + * neg_mlet_tuple_dup | build fails | site 1562 (dup-in) + * neg_forrange_tuple_dup | build fails | site 1505 + * neg_param_dup | build fails | site 1914 + * neg_toplet_dup | build fails | site 1880 + * pos_nested_block_shadow | exit=1 | scope boundary + * pos_nested_shadow_typed | exit=2 | (name,type) — bucket + * | | keys by name only + * pos_forrange_body_shadow | exit=99 | for body N_BLOCK + * | | opens a fresh scope + * pos_mcase_per_arm | exit=7 | match arm scope + */ +#include +#include +#include +#include +#include +#include + +static int +runwait(const char *cmd) +{ + int rc = system(cmd); + if (rc == -1) return -1; + if (WIFEXITED(rc)) return WEXITSTATUS(rc); + return -1; +} + +/* + * kind == 0: negative — build must fail (any nonzero exit). + * kind == 1: positive — build must succeed AND binary exits with `want`. + */ +struct row { const char *label; int kind; const char *src; int want; }; + +static const struct row rows[] = { + /* neg: site 1443 — block-body let dup. */ + { "neg_let_same_block", 0, + "fn main() i32 = {\n" + " let a: i32 = 1;\n" + " let a: i32 = 2;\n" + " return a;\n" + "};\n", + 0 }, + + /* neg: site 1562 — mlet shadows earlier same-block let. */ + { "neg_mlet_same_block", 0, + "fn pair() (i32, i32) = { return (10, 20); };\n" + "fn main() i32 = {\n" + " let a: i32 = 1;\n" + " let (a, b) = pair();\n" + " return a + b;\n" + "};\n", + 0 }, + + /* neg: site 1562 — mlet pattern lists the same name twice. */ + { "neg_mlet_tuple_dup", 0, + "fn pair() (i32, i32) = { return (10, 20); };\n" + "fn main() i32 = {\n" + " let (a, a) = pair();\n" + " return a;\n" + "};\n", + 0 }, + + /* neg: site 1505 — forrange tuple-pattern lists same name twice. */ + { "neg_forrange_tuple_dup", 0, + "fn main() i32 = {\n" + " let xs: [1](i32, i32) = [(10i32, 20i32)];\n" + " for (let (a, a) .. xs) {\n" + " return a;\n" + " };\n" + " return -1;\n" + "};\n", + 0 }, + + /* neg: site 1914 — two params with the same name. */ + { "neg_param_dup", 0, + "fn f(a: i32, a: i32) i32 = { return a; };\n" + "fn main() i32 = { return f(1, 2); };\n", + 0 }, + + /* neg: site 1880 — top-level let dup. */ + { "neg_toplet_dup", 0, + "let x: i32 = 1;\n" + "let x: i32 = 2;\n" + "fn main() i32 = { return x; };\n", + 0 }, + + /* pos: nested-block shadow stays legal. Inner block opens a + * fresh scope, scope_define's per-scope hash bucket isolates the + * inner `a` from the outer one. Outer `a` untouched after the + * inner block exits. */ + { "pos_nested_block_shadow", 1, + "fn main() i32 = {\n" + " let a: i32 = 1;\n" + " {\n" + " let a: i32 = 99;\n" + " };\n" + " return a;\n" + "};\n", + 1 }, + + /* pos: nested-block shadow with a different type. Confirms the + * scope-bucket key is the name alone, not (name, type) — the + * inner `s: str` does NOT collide with the outer `s: i32`. */ + { "pos_nested_shadow_typed", 1, + "fn main() i32 = {\n" + " let s: i32 = 2;\n" + " {\n" + " let s: str = \"hi\";\n" + " let _ = s;\n" + " };\n" + " return s;\n" + "};\n", + 2 }, + + /* pos: for-loop body N_BLOCK opens a fresh scope, so a `let x` + * inside the body doesn't collide with the for-range's binding + * `x` (which lives in the per-loop scope, one level above). */ + { "pos_forrange_body_shadow", 1, + "fn main() i32 = {\n" + " let arr: [1]i32 = [42i32];\n" + " let r: i32 = 0;\n" + " for (let x .. arr) {\n" + " let x: i32 = 99;\n" + " r = x;\n" + " };\n" + " return r;\n" + "};\n", + 99 }, + + /* pos: each match arm is its own scope, so two arms each + * binding `v` don't collide. Pre-#32 this was already legal + * (newscope wrapper at check.c:1186); pinned here so a future + * scope-tightening can't quietly remove that wrapper. */ + { "pos_mcase_per_arm", 1, + "type T = (i32 | f64);\n" + "fn main() i32 = {\n" + " let t: T = 7i32;\n" + " match (t) {\n" + " case let v: i32 => { return v; };\n" + " case let v: f64 => { return -1; };\n" + " };\n" + "};\n", + 7 }, +}; + +static int +run_row(const char *driver, const struct row *r, int i) +{ + char src[128], tmpdir[128], cmd[2048]; + snprintf(src, sizeof src, "/tmp/wcredecl_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/wcredecl_%d_d_%d", getpid(), i); + + FILE *f = fopen(src, "wb"); + if (!f) return -1; + fputs(r->src, f); + fclose(f); + + mkdir(tmpdir, 0755); + snprintf(cmd, sizeof cmd, "cd %s && %s build %s >/dev/null 2>&1", + tmpdir, driver, src); + int rc = runwait(cmd); + + const char *base = strrchr(src, '/'); + base = base ? base + 1 : src; + char outbin[256]; + snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base); + char *dot = strrchr(outbin, '.'); + if (dot && strcmp(dot, ".ww") == 0) *dot = '\0'; + + if (r->kind == 0) { + /* Negative — build must fail. */ + if (rc == 0) { + fprintf(stderr, + "redecl[%s]: build unexpectedly succeeded\n", + r->label); + unlink(outbin); + } + unlink(src); + /* combined.ww left next to src by the driver */ + char combined[256]; + snprintf(combined, sizeof combined, "%s.combined.ww", src); + dot = strrchr(combined, '.'); + (void)dot; + snprintf(combined, sizeof combined, "/tmp/wcredecl_%d_%d.combined.ww", + getpid(), i); + unlink(combined); + rmdir(tmpdir); + return rc == 0 ? -1 : 0; + } + + /* Positive — build then run. */ + if (rc != 0) { + fprintf(stderr, "redecl[%s]: build failed\n", r->label); + unlink(src); + rmdir(tmpdir); + return -1; + } + int got = runwait(outbin); + unlink(src); + unlink(outbin); + char combined[256]; + snprintf(combined, sizeof combined, "/tmp/wcredecl_%d_%d.combined.ww", + getpid(), i); + unlink(combined); + rmdir(tmpdir); + if (got != r->want) { + fprintf(stderr, "redecl[%s]: exit=%d want=%d\n", + r->label, got, r->want); + return -1; + } + return 0; +} + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char absbin[512]; + if (bin[0] != '/') { + char cwd[256]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); + bin = absbin; + } + + char cdrv[640]; + snprintf(cdrv, sizeof cdrv, "%s/ww", bin); + + int n = (int)(sizeof rows / sizeof rows[0]); + int fail = 0; + for (int i = 0; i < n; i++) { + if (run_row(cdrv, &rows[i], i) != 0) fail++; + } + + if (fail) { + fprintf(stderr, "redecl: %d/%d row(s) failed\n", fail, n); + return 1; + } + printf("redecl: %d/%d ok\n", n, n); + return 0; +}