No description
  • Rust 98.5%
  • Shell 1.5%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
crowmaki 8eaf115bb0 parser(step 30): expression grammar over the precedence table (#8)
Step 30 of `docs/parser/`: the expression grammar — a Pratt / precedence-climbing
reader over the binding-power table in `language-spec.md` ▸ Syntax ▸ Precedence
and Associativity.

## What's here

- `infix_op()` — the whole operator table in one place, so a transposed row is a
  one-line diff rather than a scattered edit.
- `parse_bp` / `parse_unary` / `parse_postfix` / `parse_primary`, plus
  `parse_tuple_or_group`, `parse_array_value`, `parse_new_value`,
  `parse_new_array`, `parse_call_args`, `parse_brace_elems`.
- No backtracking. A `(` after a completed primary is a call; a `(` starting a
  primary is a tuple or a group — position alone settles it. The one two-token
  peek (`[` `]` `{`, telling `[] { 1, 2 }` from `[]int`) uses a new bounded
  `Cursor::nth`.
- `array_size` is now shared by `[N]T` and `new [N]T`, so the two cannot
  disagree about the 64-bit limit.

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.

## 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:

1. **`a??b??c`.** `docs/parser/30-expressions.md` asserted `(?? a (?? b c))` and
   labelled it "left-assoc" — self-contradictory. The spec's table (level 16,
   Left-to-right) and `language-grammar.md` both give `(?? (?? a b) c)`. I
   implemented the spec's reading and corrected the doc.
2. **`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`.
3. **`Expr::Paren` removed.** With it, `(1+2)*3` dumps `(* ((+ 1 2)) 3)`,
   contradicting the step doc's own acceptance line. Parens now group without a
   node, exactly as `(int)` yields `int` in the type grammar.
4. **`AssignOp` gained `&&=` / `||=`**, which the spec's level-18 row lists and
   the lexer already emits. `IntLit` widened to `u128` to match the scanner's
   payload — narrowing would have refused legal `s128`/`u128` literals.

Plus one grammar fix: `postfix` made `f()` underivable, but
`null-coalescing.cat` writes `obj?.DoMethod()`. Now `"(" [ call-args ] ")"`.

## Deferred to step 50 (as the step doc says)

`func-value` lambdas, and `this` as a primary. `this` is a reserved keyword with
no 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 u16` case that
tells the two readings of `as` apart, postfix chains, call-args vs tuples,
malformed-input offsets, and span coverage.

- `cargo fmt --check` — clean
- `cargo clippy --all --all-targets -- -D warnings` — clean
- `cargo test --all` — 56 passed, 0 failed

The `parse`-phase fixture driver stays `#[ignore]`d; it goes green at step 70,
per its own note.

## Incidental finding

`a.1` never reaches the member rule — the scanner reads `.1` as a float literal.
Harmless today, since Catlang has no `.0` tuple-field syntax (the fixtures index
tuples with `point[0]`). Noted in the test rather than worked around.

Co-authored-by: Alic Szecsei <aszecsei@gmail.com>
Reviewed-on: #8
2026-08-22 03:02:39 +00:00
.githooks parser-fixes (#7) 2026-08-22 02:22:00 +00:00
docs parser(step 30): expression grammar over the precedence table (#8) 2026-08-22 03:02:39 +00:00
lexer parser-fixes (#7) 2026-08-22 02:22:00 +00:00
parser parser(step 30): expression grammar over the precedence table (#8) 2026-08-22 03:02:39 +00:00
tests/fixtures parser-fixes (#7) 2026-08-22 02:22:00 +00:00
.gitignore Lexer (lex phase): minimal viable scanner 2026-08-21 03:17:38 -07:00
AGENTS.md parser-fixes (#7) 2026-08-22 02:22:00 +00:00
Cargo.toml Implement parser steps 00-20: crate scaffold, AST + dump, type grammar (#6) 2026-08-21 21:45:10 +00:00
CLAUDE.md docs: improve AGENTS.md and add CLAUDE.md pointer 2026-08-21 02:26:49 -07:00
CRATE.md Restructure front end into a Cargo workspace (#1) 2026-08-21 16:47:02 +00:00
README.md Bootstrap Catlang repo: spec (docs/prd), AGENTS.md, and 55 feature fixtures 2026-08-21 02:10:12 -07:00

Catlang

A systems programming language for low-level work (game development et al.), with a C-like syntax, static typing, and no automatic memory management.

Designed for both speed and kindness.

Layout

AGENTS.md                     working conventions (read this first)
README.md                     you are here
docs/prd/                     product + language specification
  ├─ 0-overview.md            vision, tenets, memory model, object model
  ├─ 0-feature-manifest.md    every feature → its test fixture + compiler phase
  └─ language-spec.md         the language reference
tests/fixtures/               one tiny .cat program per feature (the TDD contract)
  ├─ lex/                     must tokenize
  ├─ parse/                   must build a syntax tree
  └─ semantics/               must pass semantic analysis (full front end)

The project so far

The spec is source-of-truth: docs/prd/language-spec.md alongside the overview and the feature manifest. The fixtures are the tests: each one is a small program lifted from the spec's own examples, and every language feature maps to exactly one fixture that the growing compiler pipeline must accept.

The compiler itself does not exist yet — that is the work. Development is test-driven and proceeds front-end first: make the lex fixtures tokenize, then parse, then compile, adding features as the fixtures demand (RED → implement → GREEN → commit).

See AGENTS.md for the full conventions.