syntax: align for-header grammar to cstage (Go's three forms)

The bbdd8bed residue, ruled by the Go derivation (ww's for IS the Go
for; the cstage grammar is the language definition and already the
Go-shaped one):
- bare `for { }` accepted (align UP to parse.c:1023, incl. for-else);
- 3-clause cond and post each omissible — `for (init;; post)`,
  `for (init; cond)`, `for (init;)` (align UP, parse.c:1092-1097);
- 2-clause `for (cond; post)` REJECTED (align DOWN): the form exists
  in neither Go nor cstage (parse.c:1101 eats a stray ';' then
  requires ')'), so the old wwstage accept was a parser bug. The
  reject wording mirrors cstage's failure mode per stage.

forhdr_{brace,emptymid,partial} run fixtures pin the accepted forms
byte-identically; forhdr_twoclause_reject pins both reject fragments.
Corpus pin 1745/346/21/209/1169/3490.
This commit is contained in:
2026-08-08 19:19:27 +09:00
parent b52f30a894
commit 241f28c96b
6 changed files with 87 additions and 18 deletions

View File

@@ -139,12 +139,24 @@ fn parsefor(p: *parser) *node = {
let pl = p.curline;
let pc = p.curcol;
advance(p); // past `for`
// Go's bare `for { }` — no header at all (cmd/wcc/parse.c:1023;
// Go's for has exactly three forms and this is the empty one).
// Keeps the for-else arm exactly as the cstage brace form does.
if (p.curkind == tkind.TK_LBRACE) {
let n = newnode(nkind.N_FOR, pf, pl, pc);
n.body = parseblock(p);
if (accepttok(p, tkind.TK_ELSE)) { n.els = parseblock(p); };
return n;
};
expecttok(p, tkind.TK_LPAREN, "expected '(' after for");
// Five forms (matching C parser):
// Six forms (matching C parser):
// for { } — bare Go form, no header
// for () — empty cond, unconditional loop
// for (cond) — only cond
// for (init; cond; post) — C-style 3-clause
// for (init; cond; post) — C-style 3-clause (cond and
// post each omissible, parse.c:1092)
// for (let x .. expr) — Hare-style range, single binding
// for (let (a, b) .. expr) — range with tuple destructure
@@ -225,9 +237,15 @@ fn parsefor(p: *parser) *node = {
expecttok(p, tkind.TK_SEMI, "expected ';' after for-init let");
let n = newnode(nkind.N_FOR, pf, pl, pc);
n.lhs = first;
n.cond = parseexpr(p);
expecttok(p, tkind.TK_SEMI, "expected ';' after for cond");
n.rhs = parseexpr(p);
// cond and post are each omissible (`for (init;; post)`,
// `for (init; cond)`, `for (init;)`) — mirror cstage
// parse.c:1092-1097 exactly.
if (p.curkind != tkind.TK_SEMI && p.curkind != tkind.TK_RPAREN) {
n.cond = parseexpr(p);
};
if (accepttok(p, tkind.TK_SEMI)) {
n.rhs = parseexpr(p);
};
expecttok(p, tkind.TK_RPAREN, "expected ')' after for");
n.body = parseblock(p);
if (accepttok(p, tkind.TK_ELSE)) { n.els = parseblock(p); };
@@ -237,15 +255,14 @@ fn parsefor(p: *parser) *node = {
errmsg(p, "expected name after 'let' in for");
};
// for (cond) or for (cond; post)
// for (cond) — a 2-clause `for (cond; post)` is NOT a form: it
// exists in neither Go's grammar nor the cstage one (parse.c:1101
// consumes a stray ';' then requires ')'), so the old wwstage
// accept was a parser bug, not a feature. Mirror cstage: eat the
// stray ';' and demand ')' — a post expression then fails loud.
let n = newnode(nkind.N_FOR, pf, pl, pc);
let first = parseexpr(p);
if (accepttok(p, tkind.TK_SEMI)) {
n.cond = first;
n.rhs = parseexpr(p);
} else {
n.cond = first;
};
n.cond = parseexpr(p);
if (accepttok(p, tkind.TK_SEMI)) { };
expecttok(p, tkind.TK_RPAREN, "expected ')' after for");
n.body = parseblock(p);
// Optional `else { ... }` — runs at normal cond-false exit; skipped