🐯 tygr: TYpes into Grammar Routines
Define your grammar once as Rust types and get parser, printer, and presentation in EBNF for free.
Example
Given this EBNF:
Expr = Expr1 { Op1 Expr1 } ;
Op1 = "+" | "-" ;
Expr1 = Expr2 { Op2 Expr2 } ;
Op2 = "*" | "/" ;
Expr2 = "(" Expr ")" | Int ;
Int = 'digit' { 'digit' } ;
Write it once in Rust:
use *;
>);
>);
char_class!;
; // StringOf1: one or more digits
char_class!;
// parsed & printed, but omitted from the BNF
; // StringOf: zero or more spaces
Get parser, printer, and EBNF generator:
let e = parse.unwrap; // parse
assert_eq!; // round-trip print
assert_eq!;
More examples
arith— the grammar above, as a runnable example.json— a full JSON grammar.json_optimized— the same JSON grammar, hand-tuned for parsing throughput.
Each is a small CLI:
| |
The optimized JSON grammar is also benchmarked (cargo bench) against the inputs
in data/.
How it works
| Rust | Meaning |
|---|---|
struct |
sequence (A B C) |
enum |
alternation (A | B) |
Vec<T> |
repetition ({ T }) |
Option<T> |
optional ([ T ]) |
StringEq!("…") |
literal string ("…") |
StringEqCI!("…") |
case-insensitive literal string ("…"i) |
char_class!(Name, "…", |c| …) |
define a character class (CC) |
CharOf<CC> |
a character of the given character class |
StringOf<CC> (StringOf1<CC>) |
(non-empty) sequence of characters |
VecSep<T, S> |
separated list (T { S T }) |
Either<A, B> |
inline alternation (( A | B )) |
(A, B) |
inline sequence (A B) |
Box<T> |
indirection (recursive rules) |
Raw<T> |
parse via T, store matched String |
NotFollowedBy<T> |
negative lookahead (!T) |
Hidden<T> / #[grammar(hidden)] |
parse & print, but hide from EBNF |
Wrap<L, T, R> |
L T R, dereferencing to T |
Prefix<P, T> |
P T, dereferencing to T |
Suffix<T, S> |
T S, dereferencing to T |
See also
Deriving many routines from one spec. Writing a grammar once and deriving several routines from it is an old idea: BNFC, for example, turns a single labelled-BNF file into a parser, a pretty-printer, and a LaTeX document of the grammar. tygr follows this spirit — one Rust type yields a parser, a printer, and an EBNF presentation.
Deriving parsers from grammars. There are far too many parser libraries to list; a few available to the Rust ecosystem, grouped by how the grammar is written:
| Project | Grammar is written as… |
|---|---|
| tygr | types |
| gramma | annotated types in a macro DSL |
| nom, chumsky, combine | expressions |
| pest, tree-sitter (Rust bindings) | DSL |
Writing the grammar as types is what tygr trades on. A DSL lives outside the language,
so it misses the tooling — go-to-definition, refactoring, type checking — and composes
poorly with the rest of Rust (traits, generics, #[derive], functions). Expression-based
combinators keep all of that, but you write the parsing expressions and the types you
want the result in — two descriptions of the same structure to keep in sync. With tygr
the type is the grammar, so there is nothing to keep in sync.
PEG and parser combinators. tygr reads a grammar as a recursive-descent parser with backtracking, the reading refined by Parsing Expression Grammars (PEG) and parser combinators (Parsec).
Contributing
Please open an issue before starting a pull request. See CONTRIBUTING.md for the development workflow.
License
This software is provided AS-IS, with no implied warranties, support, or conditions of any kind.
Licensed under either of Apache License, Version 2.0 or MIT license at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.