feat: parser slice 18 — list literal expressions #4

Manually merged
dferreira merged 8 commits from feat/004-feat-parser-slice-18-list-literal-expressions into master 2026-01-07 16:55:31 +01:00
Owner

feat: parser slice 18 — list literal expressions

Along the way:

  • feat: parser slice 19 — record literal expressions
  • feat: parser slice 20 — indexing expressions (expr[i])
  • feat: parser slice 21 — if / else as value-producing primary
  • feat: parser slice 22 — while as value-producing primary
  • feat: parser slice 23 — range expressions (a..b, a..=b)
  • feat: parser slice 24 — turbofish in value position (::<T>)
  • feat: parser slices 25 + 26 — const + effect declarations

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.rs and 30 more.

feat: parser slice 18 — list literal expressions Along the way: - feat: parser slice 19 — record literal expressions - feat: parser slice 20 — indexing expressions (`expr[i]`) - feat: parser slice 21 — `if` / `else` as value-producing primary - feat: parser slice 22 — `while` as value-producing primary - feat: parser slice 23 — range expressions (`a..b`, `a..=b`) - feat: parser slice 24 — turbofish in value position (`::<T>`) - feat: parser slices 25 + 26 — const + effect declarations 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.rs` and 30 more.
dferreira approved these changes 2026-01-07 13:00:00 +01:00
dferreira left a comment

Reads well. One question on src/rust/crates/ck_parser/src/tests/turbofish.rs, nothing blocking.

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");
Owner

This panics. Is SourceFile unreachable, or unreachable today? If it is the second I would rather see an error the caller can handle.

This panics. Is `SourceFile` unreachable, 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");
Owner

The expect message on args is the only thing a user sees if this fires. Does it say what they should do, or only what we assumed?

The expect message on `args` is the only thing a user sees if this fires. Does it say what they should do, or only what we assumed?
Author
Owner

It said what I assumed. Reworded in src/rust/crates/ck_parser/src/tests/turbofish.rs so it names the input that broke it, which is what the bug report will need.

It said what I assumed. Reworded in `src/rust/crates/ck_parser/src/tests/turbofish.rs` so it names the input that broke it, which is what the bug report will need.
Author
Owner

Unreachable today, which is not the same thing. SourceFile returns an error variant now, and src/rust/crates/ck_parser/src/tests/list_expr.rs has a test that drives the path.

Unreachable today, which is not the same thing. `SourceFile` returns an error variant now, and `src/rust/crates/ck_parser/src/tests/list_expr.rs` has a test that drives the path.
ListExpr ([a, b, c], []) wired as a parse_primary branch on LBracket,
mirroring slice-13 ListPattern's grammar shape — same brackets, same
trailing-comma rule, same eat-comma-then-peek loop — but each element
is a full expression and there is no rest sigil. Composes with the
slice-10 Pratt operators and the slice-17 lambda primary for free.
Adds E0075 (distinct from the pattern-side E0057 because the slot's
grammatical role is distinct).

One conformance fixture (program_match.cktest, a match-recovery test
using => instead of ->) re-blessed: a broken match-arm body can now
consume the following [...] as a ListExpr, the documented consequence
of making [ a real primary.
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.
Lands `IndexExpr` as an unconditional `[` postfix in the Pratt loop
alongside `.` (member) and `(` (call). The `[`-as-list-literal vs
`[`-as-index split is resolved by where in the loop the token is
seen — `parse_primary` vs `parse_postfix_loop` — no lookahead.
Adds E0079 for the close-bracket slot; empty `xs[]` delegates to
the existing E0048 from `parse_primary`.
Lands `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`).
Lands `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.
Lands `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.
Lifts the slice-10 turbofish guard on `parse_path_expr`. Each path
segment is now `Ident Turbofish?` where `Turbofish` wraps the leading
`::` plus the existing `GenericArgs` node, so the type-argument shape
stays in one production. The pattern parser's lookahead collapses to a
single-token check in lockstep — both value-position and pattern-
position paths now route turbofish through the same `parse_path_expr`.

Disambiguation contract from syntax.md preserved: type-position
`parse_type_path` still consumes only `Ident (:: Ident)*` and never
enters the turbofish branch, so type-slot `List<T>` remains
unambiguous with no new mechanism. Diagnostics reuse the slice-5
E0019 / E0020 codes from `parse_generic_args`; no new E-codes.
Two 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).
dferreira manually merged commit 2fa6efaadc into master 2026-01-07 16:55:31 +01:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
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
northgale/cklang-bootstrap!4
No description provided.