Gazelle
An LR parser generator for Rust with runtime operator precedence and natural lexer feedback.
What Makes Gazelle Different
1. Runtime Operator Precedence
Traditional grammars encode precedence through structure - one rule per level:
expr: add_expr;
add_expr: add_expr '+' mul_expr | mul_expr;
mul_expr: mul_expr '*' unary_expr | unary_expr;
// ... 10 more levels for a full language
Gazelle's prec terminals carry precedence at runtime:
grammar!
One rule. The lexer provides precedence per token:
'+' => Op,
'*' => Op,
This enables user-defined operators at runtime - see examples/calculator.rs.
2. Natural Lexer Feedback
The infamous C typedef problem: is T * x a multiplication or pointer declaration? The lexer needs parser state to decide.
Traditional parsers hide the parse loop, requiring globals or hacks. Gazelle uses a push-based API - you drive the loop:
loop
No magic. The lexer and parser share state through actions, and you control when each runs. See examples/c11/ for a complete C11 parser using this for typedef disambiguation.
3. Clean Separation of Grammar and Actions
The grammar is pure grammar:
grammar!
Actions are split into a types trait and an actions trait:
Action methods return Result — the error type defaults to ParseError but can be customized for domain-specific errors.
This gives you:
- Full IDE support in action code (autocomplete, type hints, go-to-definition)
- Compile errors point to your code, not generated code
- Multiple implementations (interpreter, AST builder, pretty-printer)
- Grammars reusable across different backends
4. Parser Generator as a Library
Most parser generators are build tools. Gazelle exposes table construction as a library:
use ;
use GrammarBuilder;
use CompiledTable;
use ;
// Option 1: Parse grammar from string
let grammar = parse_grammar.unwrap;
// Option 2: Build programmatically
let mut gb = new;
let num = gb.t;
let plus = gb.t;
let expr = gb.nt;
gb.rule;
gb.rule;
let grammar = gb.build;
// Build tables and parse
let compiled = build;
let mut parser = new;
let num_id = compiled.symbol_id.unwrap;
// ... push tokens with parser.maybe_reduce() and parser.shift()
Enables grammar analyzers, conflict debuggers, or parsers for dynamic grammars. See examples/runtime_grammar.rs for complete examples.
Examples
Calculator with User-Defined Operators
$ cargo run --example calculator
> operator ^ pow right 3;
defined: ^ = pow right 3
> 2 ^ 3 ^ 2;
512 // right-assoc: 2^(3^2) = 2^9
> x = 2 * 3 ^ 2;
x = 18 // ^ binds tighter than *
C11 Parser
A complete C11 parser demonstrating:
- Lexer feedback for typedef disambiguation (Jourdan's approach)
- Dynamic precedence collapsing 10+ expression levels into one rule
- Full C11 test suite (41 tests)
$ cargo test --example c11
The expression grammar:
// Traditional C grammar: 10+ cascading rules
// Gazelle: one rule with prec terminals
assignment_expression = cast_expression
| assignment_expression BINOP assignment_expression
| assignment_expression STAR assignment_expression
| assignment_expression QUESTION expression COLON assignment_expression;
Usage
use ;
use grammar;
grammar!
;
When to Use Gazelle
Good fit:
- Languages with user-definable operators or precedence
- C-family languages needing lexer feedback (typedef disambiguation)
- Complex expression grammars you want to simplify
- When you want full IDE support in semantic actions
Consider alternatives for:
- Simple formats (JSON, TOML) - nom or pest may be simpler
- Error recovery focus - chumsky or tree-sitter
- Maximum ecosystem maturity - lalrpop
License
MIT