parser(step 30): expression grammar over the precedence table #8
Loading…
Reference in a new issue
No description provided.
Delete branch "parser-step-30-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?
Step 30 of
docs/parser/: the expression grammar — a Pratt / precedence-climbingreader over the binding-power table in
language-spec.md▸ Syntax ▸ Precedenceand Associativity.
What's here
infix_op()— the whole operator table in one place, so a transposed row is aone-line diff rather than a scattered edit.
parse_bp/parse_unary/parse_postfix/parse_primary, plusparse_tuple_or_group,parse_array_value,parse_new_value,parse_new_array,parse_call_args,parse_brace_elems.(after a completed primary is a call; a(starting aprimary is a tuple or a group — position alone settles it. The one two-token
peek (
[]{, telling[] { 1, 2 }from[]int) uses a new boundedCursor::nth.array_sizeis now shared by[N]Tandnew [N]T, so the two cannotdisagree about the 64-bit limit.
Ranges and
isare non-repeating per the grammar's[ ... ], soa..b..candx is A is Bare refused at the second operator. Parenthesized,(a..b)..cstill parses.
Four conflicts I resolved rather than papered over
The step doc says to stop and flag rather than silently reorder, so these are
the calls worth reviewing:
a??b??c.docs/parser/30-expressions.mdasserted(?? a (?? b c))andlabelled it "left-assoc" — self-contradictory. The spec's table (level 16,
Left-to-right) and
language-grammar.mdboth give(?? (?? a b) c). Iimplemented the spec's reading and corrected the doc.
BinOp::Iswas unbuildable.isreads a type on the right, so itcannot be a
BinOpover twoExprs. Replaced withExpr::TypeTest(Expr, TypeRef), symmetric with the existingCast.Expr::Parenremoved. With it,(1+2)*3dumps(* ((+ 1 2)) 3),contradicting the step doc's own acceptance line. Parens now group without a
node, exactly as
(int)yieldsintin the type grammar.AssignOpgained&&=/||=, which the spec's level-18 row lists andthe lexer already emits.
IntLitwidened tou128to match the scanner'spayload — narrowing would have refused legal
s128/u128literals.Plus one grammar fix:
postfixmadef()underivable, butnull-coalescing.catwritesobj?.DoMethod(). Now"(" [ call-args ] ")".Deferred to step 50 (as the step doc says)
func-valuelambdas, andthisas a primary.thisis a reserved keyword withno AST node and no fixture — its only occurrences are inside
${...}interpolation, which the lexer keeps verbatim. Inventing a node without a
fixture would break the fixture-first contract.
Verification
14 new unit tests, RED before GREEN: one per boundary between adjacent
precedence levels, both associativity directions, the
-x as u16case thattells the two readings of
asapart, postfix chains, call-args vs tuples,malformed-input offsets, and span coverage.
cargo fmt --check— cleancargo clippy --all --all-targets -- -D warnings— cleancargo test --all— 56 passed, 0 failedThe
parse-phase fixture driver stays#[ignore]d; it goes green at step 70,per its own note.
Incidental finding
a.1never reaches the member rule — the scanner reads.1as a float literal.Harmless today, since Catlang has no
.0tuple-field syntax (the fixtures indextuples with
point[0]). Noted in the test rather than worked around.Add `parse_expr` — a Pratt / precedence-climbing reader over the binding-power table in `language-spec.md` ▸ Syntax ▸ Precedence and Associativity. Levels 3b (`as`) through 18 (assignment) climb; level 2's suffixes and level 1's `::` are read by `parse_postfix` and `parse_qualified_name`, which is where the spec puts them. `infix_op()` holds the whole table in one place, so a transposed row is a one-line diff rather than a scattered edit. Paren disambiguation is by position and needs no backtracking: a `(` after a completed primary is a call, a `(` starting a primary is a tuple or a group. The one two-token peek (`[` `]` `{`, telling `[] { 1, 2 }` from `[]int`) uses a new bounded `Cursor::nth`. Four conflicts between the step doc, the AST and the spec, resolved rather than papered over: - `docs/parser/30-expressions.md` asserted `a??b??c` == `(?? a (?? b c))` and labelled it "left-assoc". The spec's table (level 16, Left-to-right) and `language-grammar.md` both say `(?? (?? a b) c)`. The spec wins; the doc is corrected. - `BinOp::Is` was unbuildable: `is` reads a *type* on the right, so it cannot be a `BinOp` over two `Expr`s. Replaced with `Expr::TypeTest(Expr, TypeRef)`, symmetric with the existing `Cast`. - `Expr::Paren` had to go. With it, `(1+2)*3` dumps `(* ((+ 1 2)) 3)`, which contradicts the step doc's own acceptance line. Parens now group without a node, exactly as `(int)` yields `int` in the type grammar. - `AssignOp` had no `&&=` / `||=`, which the spec's level-18 row lists and the lexer already emits. Added. `IntLit` widened to `u128` to match the scanner's payload — narrowing would have refused legal `s128`/`u128` literals. Also in `docs/prd/language-grammar.md`: `postfix` made `f()` underivable, but `null-coalescing.cat` writes `obj?.DoMethod()`. Now `"(" [ call-args ] ")"`. Ranges and `is` are non-repeating per the grammar's `[ ... ]`, so `a..b..c` and `x is A is B` are refused at the second operator; parenthesized, `(a..b)..c` still parses. `array_size` is now shared by `[N]T` and `new [N]T` so the two cannot disagree about the 64-bit limit. Deferred to step 50, as the step doc says: `func-value` lambdas, and `this` as a primary (a reserved keyword with no AST node and no fixture — its only occurrences are inside `${...}` interpolation, which the lexer keeps verbatim). 14 new unit tests: one per boundary between adjacent precedence levels, both associativity directions, the `-x as u16` case that tells the two readings of `as` apart, postfix chains, call-args vs tuples, and span coverage. Verified with cargo fmt --check, cargo clippy --all --all-targets -- -D warnings, and cargo test --all (56 tests).