parser(step 40): statements & control flow #9

Merged
crowmaki merged 4 commits from parser/stmt into main 2026-08-23 19:39:05 +00:00
Collaborator

Implements parser step 40 — Statements & control flow: parse_stmt() and the recursive-descent layer above the step-30 expression core. Turns a function body into Stmts.

What's here

  • parse_stmt (single-token dispatch), parse_block, parse_var_decl (named + tuple-destructure), parse_expr_stmt, parse_return, parse_if/parse_while, parse_for (ForIn vs C-style ForC), parse_switch/parse_case/parse_default, plus parse_stmt_text harness.
  • stmt_span + span-clamped statement construction; 14 shapes assert 0..len span coverage.
  • 12 new test fns / 51 total parser tests; 26 malformed-statement refusals asserted at byte offsets.
  • Uses the existing AST from step 10 unchanged; ast.rs, lexer, and the ignored step-70 fixture driver untouched.

Verification

  • cargo fmt --check — clean
  • cargo clippy --all --all-targets -- -D warnings — zero warnings
  • cargo test --all — 51 parser tests pass, no regressions

Open items flagged (not silently decided)

  1. Comma lists in for slots are refused. language-grammar.md writes init/update as expr ("," expr)*, but Stmt::ForC holds one init + one update. for (i = 0, j = 1;;) errors at the comma rather than dropping the second expression. Either ForC needs list slots or the grammar should drop the comma list — no fixture uses it.
  2. default arm is wrapped in a synthesized Stmt::Block (the Switch default slot holds one stmt).
  3. ast.rs dump cosmetics: ForC/Switch/Case use format! (not sexp), so absent parts leave literal spaces ((for ; ; (block))). Cosmetic, not a behavior bug — worth a follow-up on ast.rs if those should read like the rest.
  4. Step-doc vs dump mismatch: 40-statements.md:89 shows (for (assign i 0) (< i n) (++ i)); the implemented (AST) dump is (for (expr (assign i = 0)) ;(< i n) ;(++ i) (block)). The dump is the contract; tests follow it.
Implements parser **step 40 — Statements & control flow**: `parse_stmt()` and the recursive-descent layer above the step-30 expression core. Turns a function body into `Stmt`s. ## What's here - `parse_stmt` (single-token dispatch), `parse_block`, `parse_var_decl` (named + tuple-destructure), `parse_expr_stmt`, `parse_return`, `parse_if`/`parse_while`, `parse_for` (ForIn vs C-style ForC), `parse_switch`/`parse_case`/`parse_default`, plus `parse_stmt_text` harness. - `stmt_span` + span-clamped statement construction; 14 shapes assert `0..len` span coverage. - 12 new test fns / 51 total parser tests; 26 malformed-statement refusals asserted at byte offsets. - Uses the existing AST from step 10 unchanged; `ast.rs`, `lexer`, and the ignored step-70 fixture driver untouched. ## Verification - `cargo fmt --check` — clean - `cargo clippy --all --all-targets -- -D warnings` — zero warnings - `cargo test --all` — 51 parser tests pass, no regressions ## Open items flagged (not silently decided) 1. **Comma lists in `for` slots are refused.** `language-grammar.md` writes `init`/`update` as `expr ("," expr)*`, but `Stmt::ForC` holds one init + one update. `for (i = 0, j = 1;;)` errors at the comma rather than dropping the second expression. Either `ForC` needs list slots or the grammar should drop the comma list — no fixture uses it. 2. **`default` arm is wrapped in a synthesized `Stmt::Block`** (the `Switch` default slot holds one stmt). 3. **`ast.rs` dump cosmetics:** `ForC`/`Switch`/`Case` use `format!` (not `sexp`), so absent parts leave literal spaces (`(for ; ; (block))`). Cosmetic, not a behavior bug — worth a follow-up on `ast.rs` if those should read like the rest. 4. **Step-doc vs dump mismatch:** `40-statements.md:89` shows `(for (assign i 0) (< i n) (++ i))`; the implemented (AST) dump is `(for (expr (assign i = 0)) ;(< i n) ;(++ i) (block))`. The dump is the contract; tests follow it.
Add the statement layer above step 30's expression core: parse_stmt
dispatches on the leading token into blocks, let/const (named and
destructuring), return, if/else, while, for (for-in and the C-style
three-slot header), switch, break, continue, and expression statements.
Every node joins the spans of its parts, and a local stmt_span recovers a
body's extent since ast.rs gives Stmt no span() accessor.

Two shapes are decided here rather than invented silently: consecutive
`case` labels with no statements between them merge into one Case's value
list (a `default` arm's statements are wrapped in a synthesized Block,
because Switch's default slot holds one statement), and the grammar's
comma-separated for-init/update lists are refused, since ForC holds one
init statement and one update expression.

Inline golden tests cover every Stmt kind, statement spans, and the
malformed forms with the offset each is reported at. Where
docs/parser/40-statements.md spells a dump differently from ast.rs, the
dump wins.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Author
Collaborator

Prior-commit adversarial review of parser(step 40). I traced every Stmt branch, computed spans by hand, cross-checked the golden strings against ast.rs dump() and the source-of-truth grammar (docs/prd/language-grammar.md), and ran the suite: cargo test -p parser = 51 passed, cargo clippy --all --all-targets -- -D warnings clean, cargo fmt --check clean. The golden assertions are genuine dump() output, not fudged.

Verdict: COMMENT (concerns), no approval. Findings ranked 0-5, top first.

[3] Comma lists in for init/update are refused, contradicting the binding grammar.
no_comma_list (parser.rs:1412) hard-rejects for (i = 0, j = 0; ...) and for (;; i++, j++), but language-grammar.md:363-364 (the source of truth) writes these slots as expr ("," expr)*. Stmt::ForC holds one init stmt and one update expr, and the author chose refusal over truncation - defensible as a local call, but per AGENTS.md the grammar is authoritative and divergences are meant to be "resolved in docs/prd/ first." As landed, the parser rejects a documented, derivable production to-spec user code will hit. Fix: grow ForC to hold lists (init: Vec / update: Vec, as 40-statements.md's own case-body machinery suggests) - or amend language-grammar.md to a single-expression slot and update ForC's AST comment.

[1] for-in lookahead hijacks a grammar-valid C-style init built on the in binary.
parser.rs:1353 routes for (<ident> in ...) to ForIn unconditionally. in is a BP_COMPARE binary (infix_op:420), so for (a in b; a < n; ++a) is a derivable for-c whose init is a membership expr - but it is misread as for-in and dies on the ;. It fails hard (no silent wrong tree) and for-in is the idiomatic reading, hence low severity, but the ambiguity is real and undocumented in 40-statements.md; at minimum a note in the step doc is owed.

[1] Synthesized Block around every default arm.
parse_default (parser.rs:1498) wraps the arm in Stmt::Block because Switch.default is Option<Box>, asymmetric with Case which holds Vec directly. The block is real and spans the arm (no invented position), so tolerable - but a single-statement default gains a block it never wrote, and the dump renders default: as (block ...). If Case may hold a list, Switch.default should too.

[1] Dump cosmetics + stale step doc (open items #3/#4, both real).
ast.rs renders ForC/Switch/Case with format! rather than sexp, so absent parts leave literal doubled/trailing spaces the goldens now guarantee: (for ; ; (block)) (parser.rs:2129), (switch x (case 1 ) ) (2165), (switch x ) (2166). And docs/parser/40-statements.md:89 still writes (for (assign i 0) (< i n) (++ i)) - no (expr ...) wrapper, no =, no ; separators - which the implementation provably does not emit (2114-2121). Following the dump is right (dump is the contract, README locked decision #4); the step doc just needs reconciling.

[1] Qualified binding rejected with the wrong message.
let A::b = x; is correctly refused at byte 5 (parser.rs:2236), but it fails through parse_var_decl's expect(';') at 1220, so the user sees "expected ';' after a let declaration" pointed at the :: - when the real rule is "binding positions may not be qualified." Offsets are right; the message names the wrong production. parse_ident_name already errors on a non-identifier; surfacing the qualification case there would be more honest.

Note (not a finding, out of the 0-5 rank): step 40 claims to complete array-iteration.cat, array-c-style-loop.cat and the function-*.cat fixtures, but tests/fixtures.rs is #[ignore]d, so those binding fixtures are never actually parsed by this PR - acceptance rests on hand-written inline strings of the same shape. Confirm the fixtures genuinely parse when the step-70 driver is enabled.

Top finding: for-c init/update reject the comma list the source-of-truth grammar promises. Severity 3.

Prior-commit adversarial review of parser(step 40). I traced every Stmt branch, computed spans by hand, cross-checked the golden strings against ast.rs dump() and the source-of-truth grammar (docs/prd/language-grammar.md), and ran the suite: `cargo test -p parser` = 51 passed, `cargo clippy --all --all-targets -- -D warnings` clean, `cargo fmt --check` clean. The golden assertions are genuine dump() output, not fudged. Verdict: COMMENT (concerns), no approval. Findings ranked 0-5, top first. [3] Comma lists in `for` init/update are refused, contradicting the binding grammar. `no_comma_list` (parser.rs:1412) hard-rejects `for (i = 0, j = 0; ...)` and `for (;; i++, j++)`, but language-grammar.md:363-364 (the source of truth) writes these slots as `expr ("," expr)*`. Stmt::ForC holds one init stmt and one update expr, and the author chose refusal over truncation - defensible as a *local* call, but per AGENTS.md the grammar is authoritative and divergences are meant to be "resolved in docs/prd/ first." As landed, the parser rejects a documented, derivable production to-spec user code will hit. Fix: grow ForC to hold lists (init: Vec<Stmt> / update: Vec<Expr>, as 40-statements.md's own case-body machinery suggests) - or amend language-grammar.md to a single-expression slot and update ForC's AST comment. [1] for-in lookahead hijacks a grammar-valid C-style init built on the `in` binary. parser.rs:1353 routes `for (<ident> in ...)` to ForIn unconditionally. `in` is a BP_COMPARE binary (infix_op:420), so `for (a in b; a < n; ++a)` is a derivable for-c whose init is a membership expr - but it is misread as for-in and dies on the `;`. It fails hard (no silent wrong tree) and for-in is the idiomatic reading, hence low severity, but the ambiguity is real and undocumented in 40-statements.md; at minimum a note in the step doc is owed. [1] Synthesized Block around every `default` arm. parse_default (parser.rs:1498) wraps the arm in Stmt::Block because Switch.default is Option<Box<Stmt>>, asymmetric with Case which holds Vec<Stmt> directly. The block is real and spans the arm (no invented position), so tolerable - but a single-statement default gains a block it never wrote, and the dump renders `default:` as `(block ...)`. If Case may hold a list, Switch.default should too. [1] Dump cosmetics + stale step doc (open items #3/#4, both real). ast.rs renders ForC/Switch/Case with format! rather than sexp, so absent parts leave literal doubled/trailing spaces the goldens now guarantee: `(for ; ; (block))` (parser.rs:2129), `(switch x (case 1 ) )` (2165), `(switch x )` (2166). And docs/parser/40-statements.md:89 still writes `(for (assign i 0) (< i n) (++ i))` - no `(expr ...)` wrapper, no `=`, no `;` separators - which the implementation provably does not emit (2114-2121). Following the dump is right (dump is the contract, README locked decision #4); the step doc just needs reconciling. [1] Qualified binding rejected with the wrong message. `let A::b = x;` is correctly refused at byte 5 (parser.rs:2236), but it fails through parse_var_decl's `expect(';')` at 1220, so the user sees "expected ';' after a `let` declaration" pointed at the `::` - when the real rule is "binding positions may not be qualified." Offsets are right; the message names the wrong production. parse_ident_name already errors on a non-identifier; surfacing the qualification case there would be more honest. Note (not a finding, out of the 0-5 rank): step 40 claims to complete array-iteration.cat, array-c-style-loop.cat and the function-*.cat fixtures, but tests/fixtures.rs is `#[ignore]`d, so those binding fixtures are never actually parsed by this PR - acceptance rests on hand-written inline strings of the same shape. Confirm the fixtures genuinely parse when the step-70 driver is enabled. Top finding: for-c init/update reject the comma list the source-of-truth grammar promises. Severity 3.
Author
Collaborator

[Severity 3] Comma lists in for-c init/update refused — contradicts the binding grammar.
Anchor: parser/src/parser.rs:1412 (fn no_comma_list), raised via parser.rs:1374 & 1396.

The source-of-truth grammar (docs/prd/language-grammar.md:363-364) writes init = var-decl | expr ("," expr)* and update = expr ("," expr)*, but Stmt::ForC holds a single init stmt and a single update expr, so this hard-refuses for (i = 0, j = 0; ...) and for (;; i++, j++). Refusal beats silent truncation — agreed — but the grammar is authoritative and AGENTS.md says resolve such gaps in docs/prd/ first. As landed, a documented, derivable production is unparseable. Either grow ForC to carry lists (init: Vec, update: Vec) or amend the grammar to a single-expression slot and update the ForC AST comment.

[Severity 3] Comma lists in for-c init/update refused — contradicts the binding grammar. Anchor: parser/src/parser.rs:1412 (fn no_comma_list), raised via parser.rs:1374 & 1396. The source-of-truth grammar (docs/prd/language-grammar.md:363-364) writes `init = var-decl | expr ("," expr)*` and `update = expr ("," expr)*`, but Stmt::ForC holds a single init stmt and a single update expr, so this hard-refuses `for (i = 0, j = 0; ...)` and `for (;; i++, j++)`. Refusal beats silent truncation — agreed — but the grammar is authoritative and AGENTS.md says resolve such gaps in docs/prd/ first. As landed, a documented, derivable production is unparseable. Either grow ForC to carry lists (init: Vec<Stmt>, update: Vec<Expr>) or amend the grammar to a single-expression slot and update the ForC AST comment.
Author
Collaborator

[Severity 1] for-in lookahead hijacks a grammar-valid C-style init built on the in binary.
Anchor: parser/src/parser.rs:1353.

parser.rs:1353 routes for (<ident> in ...) to ForIn unconditionally. in is also a BP_COMPARE binary (parser.rs:420), so for (a in b; a < n; ++a) is a derivable for-c whose init is a membership expression — but it is misread as for-in and dies on the ;. It fails hard (no silent wrong tree) and for-in is the idiomatic reading, hence low severity, but the ambiguity is real and undocumented in 40-statements.md; at minimum a note in the step doc is owed.

[Severity 1] for-in lookahead hijacks a grammar-valid C-style init built on the `in` binary. Anchor: parser/src/parser.rs:1353. parser.rs:1353 routes `for (<ident> in ...)` to ForIn unconditionally. `in` is also a BP_COMPARE binary (parser.rs:420), so `for (a in b; a < n; ++a)` is a derivable for-c whose init is a membership expression — but it is misread as for-in and dies on the `;`. It fails hard (no silent wrong tree) and for-in is the idiomatic reading, hence low severity, but the ambiguity is real and undocumented in 40-statements.md; at minimum a note in the step doc is owed.
Author
Collaborator

[Severity 1] Synthesized Block wraps every default arm.
Anchor: parser/src/parser.rs:1498 (parse_default), see also 1487-1490.

Switch.default is Option<Box> while Case holds Vec directly, so parse_default wraps each default arm in a Stmt::Block. The block spans the arm correctly (no invented position) so it is safe, but a single-statement default: g(); gains a block it never wrote and renders as (block (expr (g))) instead of a symmetric (case ...)-style list. If Case may hold a list, the default slot should too — or the asymmetry is worth a written rationale.

[Severity 1] Synthesized Block wraps every `default` arm. Anchor: parser/src/parser.rs:1498 (parse_default), see also 1487-1490. Switch.default is Option<Box<Stmt>> while Case holds Vec<Stmt> directly, so parse_default wraps each default arm in a Stmt::Block. The block spans the arm correctly (no invented position) so it is safe, but a single-statement `default: g();` gains a block it never wrote and renders as `(block (expr (g)))` instead of a symmetric `(case ...)`-style list. If Case may hold a list, the default slot should too — or the asymmetry is worth a written rationale.
Author
Collaborator

[Severity 1] Dump cosmetics now part of the test contract + stale step doc (open items #3/#4, both real).
Anchor: parser/src/parser.rs:2121 (and 2129, 2165-2166).

ast.rs renders ForC/Switch/Case with format! rather than sexp, so absent parts leave literal doubled/trailing spaces the goldens now guarantee: (for ; ; (block)) (2129), (switch x (case 1 ) ) (2165), (switch x ) (2166). And docs/parser/40-statements.md:89 still promises (for (assign i 0) (< i n) (++ i)) — no (expr ...) wrapper, no =, no ; separators — which the implementation provably does not emit (2114-2121). ForC renders the init slot wrapped in an ExprStmt and separates slots with ;. Following the dump is correct (dump = contract, README locked decision #4); the step doc simply needs reconciling to the real dumps.

[Severity 1] Dump cosmetics now part of the test contract + stale step doc (open items #3/#4, both real). Anchor: parser/src/parser.rs:2121 (and 2129, 2165-2166). ast.rs renders ForC/Switch/Case with format! rather than sexp, so absent parts leave literal doubled/trailing spaces the goldens now guarantee: `(for ; ; (block))` (2129), `(switch x (case 1 ) )` (2165), `(switch x )` (2166). And docs/parser/40-statements.md:89 still promises `(for (assign i 0) (< i n) (++ i))` — no `(expr ...)` wrapper, no `=`, no `;` separators — which the implementation provably does not emit (2114-2121). ForC renders the init slot wrapped in an ExprStmt and separates slots with `;`. Following the dump is correct (dump = contract, README locked decision #4); the step doc simply needs reconciling to the real dumps.
Author
Collaborator

[Severity 1] Qualified binding refused with the wrong diagnostic.
Anchor: parser/src/parser.rs:2236 (test), root cause parse_var_decl:1220.

let A::b = x; is correctly refused at byte 5, but it fails through parse_var_decl's expect(';') at 1220, so the user sees "expected ';' after a let declaration" pointed at the :: — when the real rule is that binding positions are never ::-qualified. The rejection offset and the test are right; the message names the wrong production. parse_ident_name already errors on a non-identifier; surfacing the qualification case there would be more honest about what actually went wrong.

[Severity 1] Qualified binding refused with the wrong diagnostic. Anchor: parser/src/parser.rs:2236 (test), root cause parse_var_decl:1220. `let A::b = x;` is correctly refused at byte 5, but it fails through parse_var_decl's `expect(';')` at 1220, so the user sees "expected ';' after a `let` declaration" pointed at the `::` — when the real rule is that binding positions are never `::`-qualified. The rejection offset and the test are right; the message names the wrong production. parse_ident_name already errors on a non-identifier; surfacing the qualification case there would be more honest about what actually went wrong.
Author
Collaborator

[Note — not ranked 0-5] The step's fixture-acceptance claim is unverified.

Step 40's acceptance section says it completes array-iteration.cat, array-c-style-loop.cat and the function-*.cat fixtures, but tests/fixtures.rs is #[ignore]d (the step-70 driver), so none of those binding fixtures is actually parsed by this PR. Acceptance rests on hand-written inline strings of the same shape (which do pass — 51 tests green), not on the manifest's real fixtures. Out of scope for this PR per its own note; worth confirming the fixtures genuinely parse when the step-70 driver is enabled, since that is the binding acceptance surface per AGENTS.md.

[Note — not ranked 0-5] The step's fixture-acceptance claim is unverified. Step 40's acceptance section says it completes array-iteration.cat, array-c-style-loop.cat and the function-*.cat fixtures, but tests/fixtures.rs is `#[ignore]`d (the step-70 driver), so none of those binding fixtures is actually parsed by this PR. Acceptance rests on hand-written inline strings of the same shape (which do pass — 51 tests green), not on the manifest's real fixtures. Out of scope for this PR per its own note; worth confirming the fixtures genuinely parse when the step-70 driver is enabled, since that is the binding acceptance surface per AGENTS.md.
- ForC init/update hold comma lists (grammar init/update = expr ("," expr)*):
  init is Vec<Stmt> (one ExprStmt per expr), update is Vec<Expr>; drop the
  no_comma_list refusal and its tests, add positive comma-list goldens.
- Switch.default becomes Option<DefaultArm> (new node, symmetric with Case)
  instead of a synthesized Stmt::Block; dumps as (default ...).
- parse_ident_name rejects a trailing '::' with an honest diagnostic
  ("binding positions may not be ::'-qualified") at the correct byte, so
  let A::b = x; no longer surfaces as a missing ';'.
- docs/parser/40-statements.md: reconcile the for/switch dump examples to the
  real output and document the for-in lookahead ambiguity (disambiguating is
  deliberately deferred; it fails hard, never a wrong tree).
Author
Collaborator

Addressed the prior-commit review (addresses sev 1 & sev 3) in 27ef7c1:

  • sev 3 — Stmt::ForC init/update now hold comma lists (grammar init/update = expr (\",\" expr)*): init is Vec<Stmt> (one ExprStmt per expr, only the last reaching the shared ; in its span), update is Vec<Expr>; no_comma_list dropped, with positive comma-list goldens.
  • sev 1 (default block) — Switch.default is now Option<DefaultArm> (new node symmetric with Case), dumps (default ...); the synthesized Stmt::Block is gone.
  • sev 1 (qualified binding) — parse_ident_name rejects a trailing :: with an honest diagnostic at byte 5; let A::b = x; no longer reads as a missing ;.
  • sev 1 (doc) — 40-statements.md reconciled to the real for/switch dumps and documents the for (<ident> in ...) lookahead ambiguity (deferred: fails hard, never a wrong tree).

Verification: cargo fmt --check, cargo clippy --all --all-targets -- -D warnings, and cargo test --all all pass (53 parser tests green, 0 failures).

Addressed the prior-commit review (addresses sev 1 & sev 3) in 27ef7c1: - **sev 3** — `Stmt::ForC` init/update now hold comma lists (grammar `init`/`update = expr (\",\" expr)*`): init is `Vec<Stmt>` (one `ExprStmt` per expr, only the last reaching the shared `;` in its span), update is `Vec<Expr>`; `no_comma_list` dropped, with positive comma-list goldens. - **sev 1 (default block)** — `Switch.default` is now `Option<DefaultArm>` (new node symmetric with `Case`), dumps `(default ...)`; the synthesized `Stmt::Block` is gone. - **sev 1 (qualified binding)** — `parse_ident_name` rejects a trailing `::` with an honest diagnostic at byte 5; `let A::b = x;` no longer reads as a missing `;`. - **sev 1 (doc)** — 40-statements.md reconciled to the real `for`/`switch` dumps and documents the `for (<ident> in ...)` lookahead ambiguity (deferred: fails hard, never a wrong tree). Verification: `cargo fmt --check`, `cargo clippy --all --all-targets -- -D warnings`, and `cargo test --all` all pass (53 parser tests green, 0 failures).
A header like "for (a in b; a < n; ++a)" is a grammar-valid C-style loop whose
init is the membership expression "a in b", but parse_for's two-token lookahead
routed it to for-in and it died on the ';'. The cursor is a position into a
fixed token vector, so the for-in read is now speculative: parse the iterable,
then let the header terminator decide. ')' completes the for-in; a ';' or ','
means the C-style slots, so rewind to just after '(' and re-parse "a in b" as
the init. Anything else stays a for-in "expected ')'" diagnostic — the likely
intent.

Cursor gains Clone to make the save/restore cheap and side-effect-free.
Author
Collaborator

Follow-up: the for-in / C-style overlap is no longer merely documented — it is now resolved by backtracking (c753609).

for (a in b; a < n; ++a) is a grammar-valid C-style loop whose init is the membership expression a in b; the two-token for-in lookahead used to hijack it and fail at the ;. Since the Cursor is just a position into a fixed token vector (it now derives Clone, which is the whole point), parse_for parses the for-in speculatively and lets the header terminator decide: ) → for-in; ; or , → rewind to just after ( and re-parse as the C-style init (no hand-spliced nodes). Anything else stays the for-in "expected ')'" diagnostic, so a broken for-in is not re-reported as a bad C-style init.

New tests: an_in_init_takes_the_c_style_branch (incl. a , init-list variant) and a_broken_for_in_keeps_its_own_diagnostic. Gates still pass: fmt clean, clippy -D warnings clean, parser 55/55. The step doc now documents the disambiguation instead of the old workaround.

Follow-up: the for-in / C-style overlap is no longer merely documented — it is now resolved by backtracking (c753609). `for (a in b; a < n; ++a)` is a grammar-valid C-style loop whose init is the membership expression `a in b`; the two-token for-in lookahead used to hijack it and fail at the `;`. Since the Cursor is just a position into a fixed token vector (it now derives Clone, which is the whole point), parse_for parses the for-in speculatively and lets the header terminator decide: `)` → for-in; `;` or `,` → rewind to just after `(` and re-parse as the C-style init (no hand-spliced nodes). Anything else stays the for-in "expected ')'" diagnostic, so a broken for-in is not re-reported as a bad C-style init. New tests: `an_in_init_takes_the_c_style_branch` (incl. a `,` init-list variant) and `a_broken_for_in_keeps_its_own_diagnostic`. Gates still pass: fmt clean, clippy -D warnings clean, parser 55/55. The step doc now documents the disambiguation instead of the old workaround.
Switch.default is Option<DefaultArm>, so the two cases were already distinct —
None means no default (the non-exhaustive case, an error for the semantics
phase), Some with an empty body means an explicit default that is a no-op. Add
a test that destructures both and asserts they stay apart at the AST level
(None vs Some with an empty body), plus a dump-level assert_ne, and record the
semantic contract in 40-statements.md so the distinction can't silently
collapse. Parser suite: 56/56.
crowmaki deleted branch parser/stmt 2026-08-23 19:39:05 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
crowmaki/catlang!9
No description provided.