feat: parser slice 18 — list literal expressions #4
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/004-feat-parser-slice-18-list-literal-expressions"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
feat: parser slice 18 — list literal expressions
Along the way:
expr[i])if/elseas value-producing primarywhileas value-producing primarya..b,a..=b)::<T>)Touches 36 files:
docs/impl/02-lexer.md,docs/impl/03-parser.md,docs/impl/slices.md,docs/records.md,src/rust/crates/ck_driver/tests/architecture.rs,src/rust/crates/ck_lex/src/lib.rsand 30 more.Reads well. One question on
src/rust/crates/ck_parser/src/tests/turbofish.rs, nothing blocking.@ -0,0 +22,4 @@return n;}}panic!("no expression node under SourceFile");This panics. Is
SourceFileunreachable, or unreachable today? If it is the second I would rather see an error the caller can handle.@ -0,0 +54,4 @@assert_eq!(path.kind, SyntaxKind::PathExpr);let tf = find_first(path, SyntaxKind::Turbofish).expect("Turbofish");// The inner GenericArgs carries one TypeExpr argument.let args = find_first(tf, SyntaxKind::GenericArgs).expect("GenericArgs");The expect message on
argsis the only thing a user sees if this fires. Does it say what they should do, or only what we assumed?It said what I assumed. Reworded in
src/rust/crates/ck_parser/src/tests/turbofish.rsso it names the input that broke it, which is what the bug report will need.Unreachable today, which is not the same thing.
SourceFilereturns an error variant now, andsrc/rust/crates/ck_parser/src/tests/list_expr.rshas a test that drives the path.Adds RecordExpr / RecordExprFieldList / RecordExprField as a conditional postfix on `{` in the Pratt loop. Disambiguation uses a non-consuming, balanced-brace scan: a record literal requires the first field to be `Ident :` and the field-value to close on `,` or `}` at depth 0; an `->` first means the brace is a match body, not a record literal. Pun-shorthand (`Point { x }`) is intentionally not in the grammar — the `:` is the discriminator that lets `{` keep its single "opens a block" job alongside match / for / while / if. New diagnostic codes E0076..E0078.expr[i]) 2efd63aeadif/elseas value-producing primary 278683016aLands `IfExpr` (`if Expr Block ElseBranch?`) and `ElseBranch` (`else (IfExpr | Block)`) as a primary in `parse_primary`, alongside `match`, lambdas, list / record literals, and blocks. The condition is parenthesis-free (`if parts.empty { ... }`) and brace-free for the same reason match's scrutinee and for's iterable are: `{` is not in the slice-10 postfix/infix tables, so the condition parse stops cleanly at the then-brace without lookahead — one mechanism, three call sites. `else if` chains fall out for free as nested `IfExpr` children inside an outer `ElseBranch`; no special `ElseIfBranch` kind. Adds `If` / `Else` keyword tokens and two diagnostics (E0080 for the missing then-brace, E0081 for a stray-after-`else`).whileas value-producing primary ad7c046a72Lands `WhileExpr` (`while Expr Block`) as a primary in `parse_primary`, alongside `if`, `match`, lambdas, list / record literals, and blocks. The expression's *value* is `Unit` (the loop runs for its effect, never for a result), but the *syntactic slot* stays uniform with slice 21's `if`: a misuse like `let x: Int = while ...` is type-checker territory, not parser territory. The condition is parenthesis-free and brace-free for the same reason match's scrutinee, for's iterable, and if's condition are — `{` is not in the slice-10 postfix/infix tables, so the condition parse stops cleanly at the body brace without lookahead. One mechanism, four call sites (`if` / `while` / `match` / `for`). Adds the `While` keyword token and one diagnostic (E0082 for the missing body brace); no trailing-branch slot, so no second code. Ratchets the R3 file LoC cap 840 → 831 after replacing the duplicate per-slice bullet list in `ck_parser/src/lib.rs` with a pointer to `docs/impl/slices.md` (the canonical place). Same precedent as slice 17's compression.a..b,a..=b) 1be4df3dc3Lands `RangeExpr` as a third infix tier in `expr.rs`'s Pratt loop at binding power `(35, 36)` — between add/sub at `(40, 41)` and comparison at `(30, 31)`. The placement is contractual: `a + 1 .. b * 2` reads as `(a + 1) .. (b * 2)` (range looser than arithmetic, the common case for `start .. start + width`) and `a..b < c..d` reads as `(a..b) < (c..d)` (range tighter than comparison, so range bounds fold before comparison operates on whole-range values). Both `..` (exclusive) and `..=` (inclusive) use the same numbers because inclusivity is a token-level discriminator on the node, not a precedence one. Left-associative for uniformity with the rest of the table; chained-range detection (`a..b..c`) is a semantic concern for the type/flow stage, same convention chained-comparison follows. Wiring is two arms in `expr.rs`: an `infix_binding_power` arm for `DotDot | DotDotEq => (35, 36)` and a new `infix_node_kind(op)` helper that returns `RangeExpr` for the range tokens and `BinaryExpr` for every other infix. The Pratt loop calls `start_node_at(cp, infix_node_kind(op))` so the wrapping shape is decided once at the fold point — no operator-specific branching elsewhere. Same mechanism, two destinations; AST projections get range-specific `lo()` / `hi()` / `inclusive()` accessors without re-discriminating on the operator token, the same convention `IfExpr` / `MatchExpr` / `WhileExpr` / `ForStmt` / `LambdaExpr` follow despite each being structurally close to a generic call. Lexer adds the `DotDotEq` 3-byte token via a third byte of lookahead (`peek2`) and a maximal-munch arm placed before the existing `DotDot` arm — same precedence the `::` / `==` / `=>` / `<=` / `>=` / `->` arms already rely on. The pattern-side `..` rest sigil (slice 13: `[x, ..xs]`, record `{ name, .. }`) is unaffected: it lives in `parse_pattern`, never reaches the expression Pratt loop, and wraps under `RestPattern`. One token, two grammatical roles, two non-overlapping productions. Scope is intentionally infix-only. Prefix `..b`, postfix `a..`, and bare `..` are not in slice 23 — the slot's role here is "range *between* two values"; open-ended forms are a different design conversation (what does `xs[..3]` mean, what type does `0..` have without an upper bound). Adding them later is additive: a new `parse_primary` arm and a new `parse_postfix_loop` arm, same `(35, 36)` numbers. No new diagnostic code. The operator has no close token, so the only failure mode (missing RHS via `a..`, `a..=`) falls through to the slice-10 E0048 from `parse_primary`. The "Pratt fills in an Error LHS when the operator appears with no real LHS" recovery is reused unchanged — that path is what cleaned up the cascading E0075 / E0062 diagnostics in `tests/lex/program_match.cktest`, where the broken `[x, ..rest] =>` match arm was previously misparsed as a list expression with five cascading false errors. The golden is re-blessed on this commit: five fewer diagnostics on the same broken-input source, recovery quality improvement, not a regression. The CST shift folds `..rest` into a `RangeExpr(Error, .., rest)` inside the list expression instead of fragmenting the surrounding match-body parse. 19 new parser tests in `tests/range_expr.rs` cover both shapes, both precedence contracts (add/sub looser, comparison tighter, unary minus tighter), composition with every primary that came before (call / member / list / index / paren / for-iterable / let-RHS / inclusive range in `for` iterable), chained ranges, the pattern-side rest sigil non-collision, missing-RHS recovery on both operators, production-path parity (expression seam vs `parse_str`), and byte-completeness. Four new lexer tests cover the bare `..=` token, `0..=10` post-int, and the `.. =` (space-separated) two-token case. R3 cap stays at 831; the worst-case file (`ck_parser/src/lib.rs`) is unchanged by this slice, so no opportunity to tighten.::<T>) ef2e769428Two coupled declaration slices land together (mirroring the `slices 6 + 7` and `slices 8 + 9` commits): they share the lib.rs diagnostic-table extraction that freed the R3 line budget for both. Slice 25 — `ConstDecl` (`const Name : TypeExpr = Expr ;`), in const_decl.rs: - mandatory type annotation (no module-boundary inference, as on Param) - value slot is the slice-10 Pratt parser; `;` terminator - new diagnostics E0083..E0086 (E0086 distinct from the statement E0055) Slice 26 — `EffectDecl` / `EffectOp` (`effect Name <Generics>? { fn-sig* }`), in effect_decl.rs: - operation signatures reuse the slice-7 param-list / return-type / effect-clause helpers verbatim (E0030..E0034 keep their meaning) - EffectOp is its own node kind (not TraitMethod) so operations() stays separate from a trait's methods() and the context stays greppable - marker effects (`effect Alloc<Ctx> {}`) and op-name overloads parse - the bundled `test { }` mock block stays deferred to slice 27; an effect body recovers over it the way a trait body recovers over a stray block (E0090, brace-balanced) - new diagnostics E0087..E0091 (E0087 distinct from the effect-clause E0034) Quality: - R3 ratchet tightened 831 -> 828: removed the diagnostic-code table duplicated in ck_parser/lib.rs (canonical copy already lives in docs/impl/03-parser.md), dropping lib.rs 831 -> 760. New worst case is ck_lex/lib.rs at 828. - promoted trait_decl's skip_brace_balanced to pub(super) for reuse - placeholder_decl fixture + its unit test now use `actor` (the longest-lived remaining placeholder, slice 29) - re-blessed tests/lex/program_effects.cktest: its effect decl now parses into a real EffectDecl tree (one fewer E0012) - docs updated in lockstep: slices.md (25/26 -> Done), 03-parser.md (code table, narrative, mechanics sections, status table) fmt clean; clippy -D warnings clean; 481 tests pass (incl. R1-R4).