diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index 7ea92f06..e58ac82a 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -30,6 +30,8 @@ type checker = struct { nresolved: i32, nunresolved: i32, errs: i32, + loops: i32, // break/continue loop-nesting guard; cstage + // twin cmd/wcc/check.c:598 (c->loops) istest: i32, // #15: `w6c_ww -T` — collect @test fns + // synth the entry; loud-reject a user main. verbose: i32, // when non-zero, log each unresolved name @@ -608,11 +610,51 @@ 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. + c.loops += 1; if (n.body != nil) { resolvewalk(c, n.body); }; + c.loops -= 1; if (n.els != nil) { resolvewalk(c, n.els); }; return; }; + // `for (init; cond; post) body` / `for (cond) body` — the body is the + // break/continue target. Walk init/cond/post outside the loop count + // (they hold no statements), bump only around the body, and keep the + // `else` outside (it targets an enclosing loop, like N_FORRANGE above). + // Mirrors cstage cmd/wcc/check.c:2529 (N_FOR, c->loops++). + if (k == syntax.nkind.N_FOR) { + if (n.lhs != nil) { resolvewalk(c, n.lhs); }; + if (n.cond != nil) { resolvewalk(c, n.cond); }; + if (n.rhs != nil) { resolvewalk(c, n.rhs); }; + c.loops += 1; + if (n.body != nil) { resolvewalk(c, n.body); }; + c.loops -= 1; + if (n.els != nil) { resolvewalk(c, n.els); }; + return; + }; + + // `break`/`continue` outside any enclosing for/for-range is an error + // in Hare (the reference), C, and Go. Align wwstage DOWN to cstage's + // rejection (rule 10). Mirrors cmd/wcc/check.c:2611-2615. + if (k == syntax.nkind.N_BREAK || k == syntax.nkind.N_CONTINUE) { + if (c.loops == 0) { + cerr(n.file); + cerr(":"); + cerr(strconv.i32tos(n.line, strconv.base.DEC)); + cerr(":"); + cerr(strconv.i32tos(n.col, strconv.base.DEC)); + cerr(": error: "); + if (k == syntax.nkind.N_BREAK) { cerr("break"); } + else { cerr("continue"); }; + cerr(" outside loop\n"); + c.errs += 1; + }; + return; + }; + // `match (e) { case let v: T => stmt; ... }` — the binding `v` // is declared by the case arm and visible inside its body. Push a // fresh scope so `case let e: str` doesn't collide with an outer diff --git a/test/wcc/989_catA_f2_reject.c b/test/wcc/989_catA_f2_reject.c index c88e7d0b..a63d8747 100644 --- a/test/wcc/989_catA_f2_reject.c +++ b/test/wcc/989_catA_f2_reject.c @@ -249,6 +249,50 @@ static const struct row rows[] = { " return r;\n" "};\n", 1, 9 }, + + /* break/continue outside any enclosing for/for-range → REJECT. Pre-fix + * wwstage had no loop-nesting guard (check.ww resolvewalk) and silently + * accepted, emitting a JMP to an undefined loop label; cstage rejects at + * cmd/wcc/check.c:2611-2615 (c->loops == 0). Both diagnostics match. */ + { "break_outside_loop", + "package main;\n" + "export fn main() void = {\n" + " break;\n" + "};\n", + 0, 0, "break outside loop" }, + + { "continue_outside_loop", + "package main;\n" + "export fn main() void = {\n" + " continue;\n" + "};\n", + 0, 0, "continue outside loop" }, + + /* break in a for's `else` block with NO enclosing loop → REJECT. The + * else runs at normal cond-false exit (skipped by break) and targets an + * ENCLOSING loop, so it is OUTSIDE this loop's break count — cstage + * cmd/wcc/check.c:2546-2549 keeps els out of the count; wwstage mirrors. */ + { "break_in_else_outside_loop", + "package main;\n" + "export fn main() void = {\n" + " let i: i32 = 0;\n" + " for (i < 1) { i = i + 1; } else { break; };\n" + "};\n", + 0, 0, "break outside loop" }, + + /* control — break/continue INSIDE a loop are valid → run 5. */ + { "break_continue_inloop_ok", + "package main;\n" + "export fn main() i32 = {\n" + " let i: i32 = 0;\n" + " for (i < 10) {\n" + " i = i + 1;\n" + " if (i == 3) { continue; };\n" + " if (i == 5) { break; };\n" + " };\n" + " return i;\n" + "};\n", + 1, 5 }, }; static int