parser: decompose into atomic testable steps (docs/parser/) #3

Merged
crowmaki merged 4 commits from docs/parser-decomposition into main 2026-08-21 19:47:35 +00:00
Collaborator

What

Decomposes the parser — the second pipeline phase, after lexer/ — into clear, atomic, testable steps, and locks the first round of design decisions the decomposition surfaced.

Files

File Capability
docs/parser/README.md index, reading order, locked design decisions
docs/parser/00-scaffold.md parser/ crate under the workspace; parse() -> Result<Program, ParseError>; golden-dump harness
docs/parser/10-ast.md AST node families + a dump() inspector so tests assert a printed form
docs/parser/20-type-refs.md the type grammar (annotations feed everything)
docs/parser/30-expressions.md Pratt / precedence-climbing over the spec's own operator table
docs/parser/40-statements.md statements & control flow (incl. let / const)
docs/parser/50-functions.md function / lambda declarations, :: / this / overloads
docs/parser/60-user-types.md struct / enum / interface / impl / type-alias / #[SOA]
docs/parser/70-top-level.md program assembly, main, attribute attachment, hello-world
docs/prd/language-grammar.md the formal EBNF, promoted into the PRD (canonical grammar)
lexer/src/token.rs reserve impl / operator as KeywordKind
lexer/src/lexer.rs scan the two new keywords + a lexer test

Each parser step follows the same skeleton: Deliverable → Grammar → AST → parser-function names → Acceptance (inline Rust unit tests and the fixture(s) it completes) → RED→GREEN note. Steps are bottom-up so each is independently testable, and each names exactly which of the 32 fixtures in tests/fixtures/parse/ it completes.

Design decisions now locked

These came out of review; the PRD EBNF and the lexer token changes implement them:

  1. The grammar is promoted into docs/prd/language-grammar.md as an EBNF appendix — the language's first formal grammar. Parser steps defer to it.
  2. impl and operator are reserved keywords. Both were plain identifiers; the lexer now emits KeywordImpl / KeywordOperator (enum member + scanner map + a unit test). No parser-side Identifier("impl") contextual hack.
  3. Precedence strategy: Pratt / precedence-climbing over the existing table.

Two further points (the golden-dump() test contract and every AST node carrying a source Span) were confirmed as accepted defaults rather than open questions and are recorded as such in the README.

Notes

  • The markdown changes are docs-only; the only Rust change is the 2-keyword reservation in the lexer, which is cargo fmt-clean and passes cargo test (9 passed, 0 failed).
  • Base is main; the docs and lexer change depend on nothing not already on main.

Adversarial grammar review (latest)

The EBNF (docs/prd/language-grammar.md) went through an adversarial review
against the spec, the lexer token vocabulary, and all 32 parse/ fixtures.
The reviewer confirmed 5 CRITICAL + 3 MAJOR + 2 MINOR findings; two additional
gaps (functional static/instance Type::m = (...) ->, and parameter'd function
types (f32) -> float) were caught on independent re-derivation. All fixes
are applied so that every fixture in tests/fixtures/parse/ is now
derivable from the grammar
. Notable:

  • bare arrow with no return type (-> { ... }) — the return type is inferred
  • array-type literals as expressions: [N]T, []T { ... }, [..]T { ... }
  • destructuring declarations let/const (x, y) = ... with _
  • interface method closing ; optional
  • &&= / ||= restored to compound-assignment (lexer emits them)
  • is moved to its own stricter level; unbound span a.. spelled out
  • paren-first and ? disambiguation rules pinned (no silent guesses)
## What Decomposes the **parser** — the second pipeline phase, after `lexer/` — into clear, atomic, testable steps, and locks the first round of design decisions the decomposition surfaced. ## Files | File | Capability | |---|---| | `docs/parser/README.md` | index, reading order, locked design decisions | | `docs/parser/00-scaffold.md` | `parser/` crate under the workspace; `parse() -> Result<Program, ParseError>`; golden-dump harness | | `docs/parser/10-ast.md` | AST node families + a `dump()` inspector so tests assert a printed form | | `docs/parser/20-type-refs.md` | the type grammar (annotations feed everything) | | `docs/parser/30-expressions.md` | Pratt / precedence-climbing over the spec's own operator table | | `docs/parser/40-statements.md` | statements & control flow (incl. `let` / `const`) | | `docs/parser/50-functions.md` | `function` / lambda declarations, `::` / `this` / overloads | | `docs/parser/60-user-types.md` | struct / enum / interface / impl / type-alias / `#[SOA]` | | `docs/parser/70-top-level.md` | program assembly, `main`, attribute attachment, hello-world | | `docs/prd/language-grammar.md` | **the formal EBNF**, promoted into the PRD (canonical grammar) | | `lexer/src/token.rs` | reserve `impl` / `operator` as `KeywordKind` | | `lexer/src/lexer.rs` | scan the two new keywords + a lexer test | Each parser step follows the same skeleton: **Deliverable → Grammar → AST → parser-function names → Acceptance (inline Rust unit tests *and* the fixture(s) it completes) → RED→GREEN note**. Steps are bottom-up so each is independently testable, and each names exactly which of the 32 fixtures in `tests/fixtures/parse/` it completes. ## Design decisions now locked These came out of review; the PRD EBNF and the lexer token changes implement them: 1. **The grammar is promoted** into `docs/prd/language-grammar.md` as an EBNF appendix — the language's first formal grammar. Parser steps defer to it. 2. **`impl` and `operator` are reserved keywords.** Both were plain identifiers; the lexer now emits `KeywordImpl` / `KeywordOperator` (enum member + scanner map + a unit test). No parser-side `Identifier("impl")` contextual hack. 3. **Precedence strategy: Pratt / precedence-climbing** over the existing table. Two further points (the golden-`dump()` test contract and every AST node carrying a source `Span`) were confirmed as accepted defaults rather than open questions and are recorded as such in the README. ## Notes - The markdown changes are docs-only; the only Rust change is the 2-keyword reservation in the lexer, which is `cargo fmt`-clean and passes `cargo test` (9 passed, 0 failed). - Base is `main`; the docs and lexer change depend on nothing not already on `main`. ## Adversarial grammar review (latest) The EBNF (`docs/prd/language-grammar.md`) went through an adversarial review against the spec, the lexer token vocabulary, and all 32 `parse/` fixtures. The reviewer confirmed 5 CRITICAL + 3 MAJOR + 2 MINOR findings; two additional gaps (functional static/instance `Type::m = (...) ->`, and parameter'd function types `(f32) -> float`) were caught on independent re-derivation. All fixes are applied so that **every fixture in `tests/fixtures/parse/` is now derivable from the grammar**. Notable: - bare arrow with no return type (`-> { ... }`) — the return type is inferred - array-type literals as expressions: `[N]T`, `[]T { ... }`, `[..]T { ... }` - destructuring declarations `let/const (x, y) = ...` with `_` - interface method closing `;` optional - `&&=` / `||=` restored to compound-assignment (lexer emits them) - `is` moved to its own stricter level; unbound span `a..` spelled out - paren-first and `?` disambiguation rules pinned (no silent guesses)
- docs/prd/language-grammar.md: promote the formal grammar as an EBNF
  appendix (parser step files now defer to it).
- lexer: reserve 'impl' and 'operator' as keywords (KeywordKind + scanner
  map + a lexer test). Both had only ever appeared as syntax.
- docs/parser: open questions resolved into locked decisions; step docs now
  reference the EBNF and the keyword reservation.
Response to the adversarial grammar review of docs/prd/language-grammar.md.
Confirms the reviewer's findings and fixes both its and inherited gaps:

- array-type literals in expression position: [N]T / []T {...} / [..]T
  (static-arrays, array-initialization, array-iteration, c-style-loop)
- bare arrow with no return,type: 'function f(this) -> { ... }' (inferred)
- destructuring declarations: const (x, y) = point; with '_' target
- interface-method trailing ';' now optional (fixture omits it)
- &&= and ||= restored to compound-assignment (lexer emits them)
- functional static/instance spelling: Type::m = (...) -> (no 'const')
- is at its own level 12, looser than relational, in both table and chain
- unbounded span a..  as additive (..|...)? [additive], requires a start
- paren-first disambiguation (function-type/lambda vs tuple vs
  parenthesized) stated for and type position
- LL(2) lookahead note for '?' postfix-vs-ternary conflict
- multiplier vs additive precedence preserved (postfix > unary > cast)
crowmaki approved these changes 2026-08-21 19:47:32 +00:00
crowmaki deleted branch docs/parser-decomposition 2026-08-21 19:47:35 +00: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
crowmaki/catlang!3
No description provided.