Convert grammars (currently Pest and a supported PEG subset) into Marser parser combinators in Rust.
The generated output is a single grammar() function that returns a Marser parser. For the chosen entry rule, the converter targets strict language equivalence with the selected syntax where implemented. The parser output type is a generated Parsed<'src> enum with one variant per rule.
Try it in the browser: https://grammar-to-marser.arnedebo.com — paste a grammar, preview the Rust output, download a Cargo project, or share a link.
Note: This project was almost entirely vibe coded. If you run into bugs or rough edges, feel free to open an issue.
cargo install grammar-to-marserOr build from this repository:
cargo install --path .grammar-to-marser <grammar-file> [entry_rule] [--syntax pest|peg] [--output <path>] [--trace]| Argument | Description |
|---|---|
grammar-file |
Path to the grammar file |
entry_rule |
Rule to use as the parser entry point (defaults to the last rule in the file) |
--syntax pest|peg |
Input grammar syntax (defaults to pest) |
--output <path> |
Write generated Rust to a file instead of stdout |
--trace |
Emit trace instrumentation in the generated parser |
Given calc.pest:
expr = { term ~ (("+" | "-") ~ term)* }
term = { factor ~ (("*" | "/") ~ factor)* }
factor = { number | "(" ~ expr ~ ")" }
number = @{ ASCII_DIGIT+ }
WHITESPACE = _{ " " | "\t" }
Generate a parser for the expr rule:
grammar-to-marser calc.pest expr --syntax pestThe output is Rust source defining pub fn grammar<'src>() -> impl Parser<'src, &'src str, Output = Parsed<'src>> + Clone, plus a Parsed<'src> enum. Each rule becomes a variant; fields come from binds inside that rule. Nested rule values are Box<Parsed<'src>>; leaf rules capture their matched slice as &'src str. Use it with Marser's Parser::parse_str or parse_whole_input:
use marser::parser::Parser;
let parser = grammar();
let parsed = parser.parse_whole_input("1 + 2 * 3").unwrap();The conversion logic is also available as a library:
use grammar_to_marser::{ConvertOptions, convert_grammar_source};
let rust = convert_grammar_source(
pest_source,
&ConvertOptions {
entry_rule: "expr".to_string(),
..Default::default()
},
)?;Other exports include convert_source, list_rules, convert_grammar, list_grammar_rules, parse_pest_grammar, and parse_peg_grammar.
The converter handles a practical subset of Pest, including:
- Rule modifiers: silent (
_), atomic (@), compound atomic ($), non-atomic (!) - Sequences, ordered choice, optional (
?), repetition (*,+,{m,},{m}), and negative lookahead (!) - Positive lookahead (
&) and case-insensitive strings (^"...") - Builtins:
ASCII_ALPHA,ASCII_ALPHANUMERIC,ASCII_DIGIT,ANY,SOI,EOI - Implicit whitespace via
WHITESPACEandCOMMENTrules - Right recursion and mutual recursion (via Marser's
recursivehelpers) - Typed
Parsed<'src>output with one enum variant per rule - Pest node tags as field names; tagged non-rule matchers become
&'src strslices
The following are not supported and produce conversion errors:
- Left recursion
- Pest stack features (
PUSH,POP,DROP,PEEK,PEEK_ALL) - Pest-style
Pairtrees, spans, or memoization (.memoized()) - Mutual recursion groups larger than Marser's
recursive12limit
Repetition and whitespace rules are validated with Pest-style progress checks. Unsupported constructs that could affect matching semantics are hard errors, not warnings.
# Run tests (includes equivalence checks against Pest for fixture grammars)
cargo test
# Regenerate committed output snapshots after converter changes
cargo run --features dev-tools --bin update-test-fixturesThe web/ crate is a WASM build of the converter used by the browser demo.
Pest source
→ parse with Marser meta-grammar
→ normalize & validate (whitespace, recursion, repetitions, builtins)
→ specialize rule contexts (atomic vs non-atomic)
→ SCC analysis for recursive rules
→ emit Marser parser combinators
See PLAN.md for the full design document.