wcc: reject break/continue outside loop in wwstage, align to cstage (#7)

The selfhost checker's resolvewalk had no loop-nesting guard and no
N_BREAK/N_CONTINUE arm, so `break`/`continue` outside any loop fell through
the generic child recursion and was silently accepted -- while cstage
(cmd/wcc/check.c) correctly rejects them. A cs!=ww checker divergence
(rule 10); cstage is correct (break/continue outside a loop is an error in
Hare/C/Go), so align wwstage DOWN, not cstage up.

Mirror cstage's mechanism exactly (check.c:598/2494/2529/2611): a `loops`
counter incremented around for and for-range bodies -- the for-`else` and
the init/cond/post walked OUTSIDE the count, since a break there targets an
enclosing loop -- rejecting break/continue when loops==0 with a
byte-identical `file:line:col: error: <kw> outside loop` diagnostic.
match/switch are not loop targets, matching cstage.

The divergence survived because 300_check.c only exercised the in-process C
checker, never w6c_ww; the fix adds 4 rows to the both-stage
989_catA_f2_reject carrier (break/continue outside loop, the for-else
els-outside-count edge, and an in-loop control). make clean && make test:
all 402 passed, byte-id self-compile gates 990-996 green.
This commit is contained in:
2026-06-23 00:23:15 +09:00
parent 2e07e3bfe7
commit 30a4920ccf
2 changed files with 86 additions and 0 deletions

View File

@@ -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

View File

@@ -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