Skip to content

Replace tree-sitter parser with nom - #1

Merged
toddATavail merged 18 commits into
mainfrom
nom-parser
Apr 18, 2026
Merged

Replace tree-sitter parser with nom#1
toddATavail merged 18 commits into
mainfrom
nom-parser

Conversation

@toddATavail

@toddATavail toddATavail commented Apr 18, 2026

Copy link
Copy Markdown
Owner

Summary

Replace the tree-sitter parser with a hand-written nom combinator parser, delivering a complete rewrite of the parsing layer while preserving the existing compiler backend (IR, optimizer, evaluator) byte-for-byte.

What changed

New nom parser (parser/combinators.rs, parser/errors.rs)

  • Full nom combinator parser covering the entire xDy grammar: constants, standard dice (xDy), custom dice (xD[...]), arithmetic (+, -, *, /, %, ^), grouping, drop lowest/highest, formal parameters, environmental variables, and dynamic expressions.
  • Correct operator precedence: exponentiation binds tighter than unary negation (-a^2 is -(a^2)), matching tree-sitter behavior.
  • Negative constant handling: -2147483648 (i32::MIN) parses correctly via a dedicated negative_overflowed_constant combinator that handles the sign+digits boundary as a unit.
  • Constant overflow saturates to i32::MAX/i32::MIN instead of failing.
  • Identifiers accept . and inline whitespace, matching the tree-sitter grammar ([\p{L}_][\p{L}\p{N}\p{Z}._-]*).
  • Structured error propagation via cut at commit points (after binary operators, delimiters, d/D operator, drop keyword), so errors propagate instead of being silently swallowed.

Typed AST (ast.rs)

  • Strongly typed AST with Function, Expression, DiceExpression, ArithmeticExpression, DropDirection, and Span types.
  • ASTVisitor trait with accept() dispatch on all expression types, enabling clean tree-walking passes.

AST-walking compiler (compiler.rs)

  • Compiler walks the nom AST and emits byte-identical IR to the old tree-sitter compiler.
  • Exposed as public API for power users who want to drive the parse → AST → codegen pipeline manually.
  • All lifetime parameters renamed from 'a to 'src (and 's/'e/'r to 'src/'env/'rng) for self-documenting code.

Diagnostics module (diagnostics.rs)

  • Two-layer error reporting: enhanced parser error propagation + fix-and-retry doctor.
  • 11 diagnostic kinds: UnclosedDelimiter, MissingRightOperand, MissingLeftOperand, MissingDiceFaces, IncompleteDropClause, BareIdentifier, IncompleteParameterDefinition, TrailingInput, EmptyExpression, UnexpectedToken, UnexpectedEof.
  • Structured Diagnostic type with SourceSpan, Suggestion (including complete corrected source), and Placeholder regions with valid_kinds for UI integration.
  • OffsetMap tracks position adjustments across fix-and-retry iterations.
  • Bare identifier detection handles d/D splitting (xD6{x}D6 or {xD6}) and expression-context variables (4 + hello4 + {hello}).
  • Missing operand diagnostics offer both insert-placeholder and drop-operator suggestions.

S-expression support (s_expr.rs)

  • S-expression writer for AST pretty-printing (debugging, golden-file testing).
  • S-expression reader (read_s_expr()) that roundtrips perfectly with the writer.
  • Variables and parameters containing whitespace are quoted with double quotes in both directions.

Comprehensive test infrastructure

  • 3,241 lines of parser tests (tests/parser.rs) covering all grammar constructs, edge cases, Unicode, whitespace tolerance, and operator precedence.
  • 93 data-driven parse cases (tests/test_parse.txt) covering all expressions from test_compile_unoptimized.txt plus overflow boundaries, Unicode, embedded spaces, and whitespace tolerance.
  • 33 data-driven error diagnostic cases (tests/test_parser_errors.txt) with exact match on diagnostic kind, span, message, suggestions, and placeholders.
  • 345 lines of diagnostics tests (tests/diagnostics.rs) including corrected source validity and performance (2000 diagnose() calls < 1s).
  • Tests organized into submodules: compiler, diagnostics, evaluator, histogram, optimizer, parser.

tree-sitter decoupling

  • Removed tree-sitter, tree-sitter-xdy, and cc dependencies from the xdy crate.
  • Deleted the old tree-sitter Compiler struct, SyntaxTreeVisitor trait, and CanVisitSyntaxTree trait (~800 lines).
  • tree-sitter-xdy crate preserved untouched as a standalone package.

Documentation overhaul

  • Root README: project brochure (language features, examples, performance, planned work).
  • Crate README: technical deep-dive covering compilation pipeline, ISA reference, VM model, optimizer internals, histogram engine, and diagnostics.
  • Railroad diagram generation via ebnsf: EBNF grammar in xdy/grammar/xdy.ebnf compiled to SVG at build time and inlined into parser Rustdoc.
  • Mermaid diagrams via aquamarine in compiler, optimizer, and evaluator Rustdoc.
  • Rustdoc # Lifetimes sections on all major types.

Dependency upgrades

  • rand 0.8 → 0.10 (thread_rng()rand::rng(), gen_range()random_range()).
  • criterion 0.5 → 0.8.

Build & tooling

  • Top-level justfile with verify, fmt, clippy, test, bench, doc recipes.
  • CompilationError now carries ParseError with lifetime parameter for full diagnostic fidelity.
  • EvaluationError updated correspondingly. Both lose Copy (ParseError contains Vec).

Performance

Benchmarked across 316 expressions: nom delivers 48% aggregate speedup (median 62% per expression), with 99% of cases faster than tree-sitter.

Compatibility

  • All existing end-to-end test cases pass unchanged.
  • Public API (compile, compile_unoptimized, evaluate) preserved with the same signatures (modulo lifetime on CompilationError).
  • IR, optimizer, and evaluator are completely untouched.

Test plan

  • All existing end-to-end tests pass (tests/*.txt)
  • New parser tests cover all grammar constructs
  • Diagnostics tests verify error recovery and suggestions
  • S-expression roundtrip tests
  • Performance benchmarks confirm speedup
  • cargo clippy clean
  • cargo test all pass

- All positive/negative test cases are passing.
- Error reporting has not been tested yet; this is mission-critical, and the
  underlying justification for changing the parser substrate.
- Restructured test modules.
- Fixed an ordering bug in `primary` that caused misparses in certain cases.
rand 0.10 renamed the `gen` family of methods to avoid conflicting with
the `gen` keyword reserved in Rust edition 2024:

- `thread_rng()` → `rand::rng()`
- `Rng::gen_range()` → `Rng::random_range()`

Updated all call sites in evaluator internals (`roll_range`,
`roll_standard_dice`, `roll_custom_dice`) and all 22 doctests across
`lib.rs`, `compiler.rs`, and `evaluator.rs`.

Also resolved 4 `mismatched_lifetime_syntaxes` warnings in the parser
by making elided lifetimes explicit (`Span<'_>`, `Function<'_>`,
`ParseError<'_>`) where the compiler flagged inconsistent usage.

cargo check, clippy, and test all pass cleanly (76/76 tests, 0 warnings).
Implement `CodeGenerator`, a crate-internal component that walks the
nom parser's AST (`ast::Function`) and emits byte-identical IR to the
existing tree-sitter compiler. This is the critical-path item for
replacing tree-sitter with nom in the compilation pipeline.

Also fix three parser bugs discovered during verification:
- Constant overflow now saturates to `i32::MAX`/`i32::MIN` instead of
  failing, matching tree-sitter behavior.
- Identifiers now accept `.` and inline whitespace, matching the
  tree-sitter grammar (`[\p{L}_][\p{L}\p{N}\p{Z}._-]*`).
- Exponentiation now binds tighter than unary negation (`-a^2` is
  `-(a^2)`), matching tree-sitter precedence.

Fix five pre-existing broken rustdoc links across the codebase.
Replace tree-sitter parsing in compile() and compile_unoptimized() with
parser::parse() followed by CodeGenerator::generate(). The old
tree-sitter Compiler struct remains for now (decoupling is xdy-ddf).

CompilationError gains a lifetime and carries ParseError, giving callers
full diagnostic fidelity. EvaluationError updated to match. Both lose
Copy (ParseError contains Vec).

Fix i32::MIN boundary in the parser: add negative_overflowed_constant
combinator to unary, which parses sign+digits as a unit when digits
overflow positive i32. Without this, 2147483648 clamps to i32::MAX
before negation, producing -2147483647 instead of i32::MIN.

Add top-level justfile with verify, fmt, clippy, test, bench, doc.
Introduce a two-layer error reporting system: enhanced parser error
propagation via `cut` at commit points, and a new diagnostics module
with a fix-and-retry doctor that collects all parse errors in a single
pass.

Parser changes (errors.rs, combinators.rs):
- Add `cut` after binary operators, delimiters, d/D operator, and
  `drop` keyword so errors propagate instead of being swallowed by
  `many0`/`fold_many0`.
- Restructure `dice` to factor out common `dice_count d_operator`
  prefix; add `drop_clause` combinator.
- Add context classifiers for closing delimiters, right operand,
  and drop direction.
- Fix EOF TODO: `ErrorKind::Eof` now produces "end of input" instead
  of the incorrect incomplete-function-body mapping.

Diagnostics module (diagnostics.rs):
- Structured types: Diagnostic, DiagnosticKind (11 variants),
  SourceSpan, Suggestion, Placeholder, DiagnoseResult.
- Fix-and-retry doctor: parse, analyze error, apply fix, re-parse
  until clean or unfixable. OffsetMap tracks position adjustments
  across iterations.
- Error patterns: UnclosedDelimiter, MissingRightOperand,
  MissingLeftOperand, MissingDiceFaces, IncompleteDropClause,
  BareIdentifier, IncompleteParameterDefinition, TrailingInput,
  EmptyExpression, UnexpectedToken, UnexpectedEof.
- Suggested fixes include complete corrected source strings and
  placeholder regions with valid_kinds for UI integration.
- Missing operand diagnostics offer both insert-placeholder and
  drop-operator suggestions.
- Bare identifier detection handles d/D splitting (xD6 → {x}D6
  or {xD6}) and expression-context variables (4 + hello → 4 +
  {hello}).

Test infrastructure:
- Data-driven test_errors.txt with 33 canonical test cases.
- read_error_test_cases() helper for parsing the test format.
- Tests: error diagnostics (exact match on kind, span, message,
  all suggestions and placeholders), corrected source validity,
  valid program contract, OffsetMap unit tests, performance
  (2000 diagnose() calls < 1s).
Introduce ASTVisitor trait in ast module with accept() dispatch on
Expression, DiceExpression, and ArithmeticExpression. Rebuild
CodeGenerator as an ASTVisitor<Output=AddressingMode> implementation.
Make codegen module and CodeGenerator public for power users who want
to drive the parse → AST → codegen pipeline manually.
Remove tree-sitter, tree-sitter-xdy, and cc dependencies from the xdy
crate. Delete the old tree-sitter Compiler struct, SyntaxTreeVisitor
trait, and CanVisitSyntaxTree trait (~800 lines). Move CodeGenerator
into compiler.rs and rename it to Compiler, with Compiler::compile as
the primary entry point. Delete codegen.rs and the redundant
test_codegen_matches_tree_sitter test.

Rename all source-text lifetime parameters from 'a to 'src (and 's/'e/'r
to 'src/'env/'rng in evaluate functions) for self-documenting code. Add
Function, Expression, ParseError, and Span.

Add "run just verify before review" guardrail to AGENTS.md.

tree-sitter-xdy crate is preserved untouched.
…e constants.

Parser: negative constants are now always Constant(-N), not Neg(Constant(N)).
The negative_constant combinator handles all negated integer literals directly,
bailing on d/D and ^ so negation remains looser than dice and exponentiation.
Removed dice_after_count (no longer needed). Replaced dead code paths with
unreachable!(). Simplified exponent combinator.

S-expressions: added read_s_expr() reader that roundtrips perfectly with the
existing writer. Variables and parameters containing whitespace are now quoted
with double quotes in both directions. Fixed Constant::size_s_expr panic on
i32::MIN.

Tests: added test_parse.txt (93 data-driven cases) covering all expressions
from test_compile_unoptimized.txt plus overflow boundaries, Unicode, embedded
spaces, operator precedence, and whitespace tolerance. Replaced 6 inline
boilerplate test functions with test_parse_to_s_expr and test_s_expr_roundtrip.
Renamed test_errors.txt to test_parser_errors.txt.
Benchmark 316 expressions across the tree-sitter and nom parser regimes.
Nom delivers 48% aggregate speedup (median 62% per expression), with 99%
of cases faster. Update README performance section with findings, remove
stale tree-sitter references from Safety and Planned Work sections. Fix
missing #[cfg(test)] gates on error diagnostic support types in support.rs.
…diagrams.

Differentiate the two READMEs: the root README is the project brochure
(language features, examples, performance, planned work), while the crate
README is now a technical deep-dive covering the compilation pipeline, ISA
reference, VM model, optimizer internals, histogram engine, and diagnostics.

Add railroad diagram generation via ebnsf: the EBNF grammar in
xdy/grammar/xdy.ebnf is compiled to an SVG at build time and inlined into
the parser module's Rustdoc. Add Mermaid diagrams via aquamarine to the
compiler, optimizer, and evaluator Rustdoc.

Fix stale tree-sitter references in the crate README (safety section,
error reporting planned work). Resolve all cargo doc warnings in
diagnostics.rs.
Update the Performance section of README.md with current nom parser
benchmarks measured on a 2026 MacBook Pro. Remove .beads/ from Git
tracking and add it to .gitignore.
@toddATavail
toddATavail merged commit 7d3db9e into main Apr 18, 2026
3 checks passed
@toddATavail
toddATavail deleted the nom-parser branch April 18, 2026 00:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant