cpp-peglib: A Header-Only PEG Parser Generator for C++
A single file C++ header-only PEG (Parsing Expression Grammars) library
At a glance
- What is it?
- cpp-peglib puts a parsing expression grammar engine in a single C++17 header, with semantic actions, a left recursion extension, packrat parsing, custom error messages and error recovery, all configured from a grammar string at runtime rather than from generated code. It also ships a benchmark that compares it against Yacc on TPC-H queries.
- Who is it for?
- cpp-peglib fits a C++ project that needs a parser for a small or medium grammar and would rather write the grammar than maintain generated code and a build step for it. It is a poor fit for a grammar with heavy backtracking, because ordered choice commits to the first alternative that matches, and a poor fit if you need the ecosystem around a generated parser, since there is no generated code here at all.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository received new commits within the last day.
- What is it written in?
- Mainly C++, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
One Header, a Grammar String and No Code Generation
cpp-peglib is a C++17 header-only PEG library, and the promise in the README is that you can start using it right away just by including peglib.h in your project.
That is the structural difference from the parser generators most C++ developers know. There is no grammar file that a tool turns into a parser, and therefore no generated source in your tree, no regeneration step in your build, and no stale generated file to argue about in a review. The grammar is a string handed to the parser at runtime.
The example in the README is a calculator, and the shape of it is the whole API:
parser parser(R"(
Additive <- Multiplicative '+' Additive / Multiplicative
Multiplicative <- Primary '*' Multiplicative / Primary
Primary <- '(' Additive ')' / Number
Number <- < [0-9]+ >
%whitespace <- [ \t]*
)");The operator is the left arrow for a rule, the vertical bar for ordered choice, the angle brackets for a token, and %whitespace is the rule that makes the parser skip spaces between tokens.
There is one compiler requirement, and the README is explicit about it. The library only supports C++17 compilers, so the compiler option -std=c++17 must be enabled, with /std:c++17 /Zc:__cplusplus for MSVC. On some Linux distributions such as Ubuntu and CentOS the -pthread option is needed when linking, and the README points at three issue threads where that is discussed.
The Extended Syntax Goes Well Past Plain PEG
The PEG syntax itself is the one described on page 2 of Bryan Ford's document on parsing expression grammars, and cpp-peglib adds a substantial set of extensions on top of it.
Some are conveniences that a regular expression user would expect. There is a case-insensitive literal operator '...'i and a case-insensitive character class [... ]i, both for ASCII letters, a negated character class operator, a regex-like repetition operator with a range, and the predefined classes for digits, word characters and whitespace, plus the POSIX character classes such as [[:alpha:]] and [[:^alpha:]], all with ASCII semantics. There are also hex and Unicode escapes such as \x20 and \u10FFFF.
Others are structural. The capture scope operator $name(... ), the named capture operator $name<... >, the backreference operator $name, and the dictionary operator | give the grammar a naming and reuse vocabulary that plain PEG lacks. A cut operator, an ignore operator, a parameterised rule or macro written as MACRO_NAME(... ), and the %word expression are in the same family.
Two of them are for error handling and three are for optimisation. The error recovery operator is %recovery(... ), and the syntax sugar exp⇑label or exp^label is a shorthand for (exp / %recover(label)). The optimisation instructions are { no_ast_opt } to suppress an AST node, { no_whitespace } to turn off %whitespace skipping inside a rule, and { ast_name: NodeTag } to override the name of an AST node.
The error message instruction is label { error_message "..." }, which is how a rule gets a human-facing message attached to it.
Left Recursion Through Seed Growing
Left recursive rules are supported, and the README says so in a form that covers all three cases: direct, indirect, and mutual left recursion, via a seed-growing algorithm, allowing natural expression of left-associative operators.
This is the extension that most changes what a PEG can express. Plain PEG, as Ford describes it, cannot handle a rule that refers to itself on the left, which is exactly the shape of most arithmetic and expression grammars, and the usual workaround is to rewrite the rule right recursively, which then turns a left-associative operator into a right-associative one and changes the parse.
The seed-growing algorithm solves that by growing a seed value for the rule and extending it as long as the rule keeps matching further input, which restores the natural left-associative form. The example in the README already uses the right recursive form for the calculator, which suggests the seeded behaviour is what a left recursive version of the same rule would give.
There is also a dedicated infix expression construct, written as { precedence L - + L / * }, which is a more declarative way to state operator precedence and associativity than encoding it in the rule structure.
Packrat Parsing and Where It Pays
The library supports the linear-time parsing known as Packrat parsing, and it is off by default, enabled with enable_packrat_parsing.
The README describes the trade-off in a paragraph that is unusual in its honesty. It remembers the result of a rule at a position and reuses it when the rule is tried there again, which takes memory that grows with the input, more so when the parse builds values or an AST. It pays where a grammar often tries a rule again at one position, and can cost more than it saves where it seldom does, so measure with your own grammar and input.
That is the correct framing. Packrat parsing turns a potentially exponential parse into a linear one, but only for grammars where the same rule is retried at the same position, which is common in expression grammars with a shared prefix and uncommon in grammars where each alternative diverges immediately. The memory cost scales with the input and the number of distinct positions memoised, and for a parse that also builds an AST the memo table holds values rather than just booleans.
The benchmark harness in the repository measures exactly this kind of thing, and it is discussed in the next section.
Semantic Actions and What SemanticValues Carries
Semantic values are attached to rules, and the API is small. There are four available semantic action signatures:
[](const SemanticValues& vs, any& dt)
[](const SemanticValues& vs)
[](SemanticValues& vs, any& dt)
[](SemanticValues& vs)The first and third take a read-write context, and the README describes any& dt as a read-write context data which can be used for whatever purposes, with the initial context data set in the peg::parser::parse method. A semantic action can return a value of an arbitrary data type, which is wrapped by peg::any, and if a user returns nothing in a semantic action, the first semantic value in the const SemanticValues& vs argument will be returned, which the README notes is the same behaviour as the Yacc parser.
The SemanticValues structure itself carries more than the values. It contains the semantic values, the matched string information, token information if the rule is literal or uses a token boundary operator, and the choice number when the rule is a prioritised choice. The struct also exposes the input path and the input string, a string_view of the matched text, line_info() giving the line and column in bytes at which the match started, and the tokens vector.
The choice number is what makes the semantic action pattern work. The calculator example switches on vs.choice() to tell which alternative matched, which is the standard way to attach different behaviour to different branches of an ordered choice.
Error Messages, Recovery and the End of Input Check
Two behaviours around failure are worth knowing before you write a grammar, and one of them is a default you may want to turn off.
The end of input check is done by default, and to disable it you call disable_eoi_check. The reason it exists is that a PEG matches happily at the end of the input, so a parser that stops early without complaining can silently accept a truncated document, and the check turns that into an error.
For reporting, the parser has a logger hook. The README's example installs one with set_logger, taking the line, the column, the message and the rule, and the callback is free to print them however it likes, which is how you get a diagnostic that points at a position in the input rather than a generic failure.
The recovery operator %recovery( ... ) is the more interesting one. It lets a rule recover from a failed match, so a grammar can skip over a malformed section and keep parsing the rest of the document, with a label naming the recovery point and the syntax sugar exp⇑label providing a compact form. Combined with the error_message instruction, that means a grammar can carry both a recovery route and a readable message for the part that failed.
The practical caveat is that recovery hides errors by design, so it is worth reserving it for sections of a format where partial success is genuinely better than no result, rather than applying it broadly.
The Repository, the Test Targets and the Benchmark Harness
The repository is a single header library with a proper amount of supporting material around it. The top level holds peglib.h, a justfile, a CMakeLists.txt, a .clang-format, a pre-commit configuration and a .github directory, plus example/, grammar/, benchmark/, abi/, lint/, pl0/, scripts/, test/, docs/ and a peg.vim file for syntax highlighting of the grammar language in Vim.
The example directory is worth a look for what the library can express, with calc.cc through calc5.cc showing variants of the calculator, and separate files for choice handling, a docx example, enter and leave actions, indentation handling and sequences.
The build is driven by just. The test target builds the peglib-test-main target and runs it, and then runs it a second time with the environment variable CPPPEGLIB_TEST_BLOB_ROUNDTRIP set to 1, which is a separate run of the same suite in a different mode. The release target calls a shell script in scripts/.
The bench target is more interesting than a normal benchmark script, because it compares against a different parser rather than against a previous version of itself. It builds the benchmark target, runs it with 10 iterations, extracts the median for each case, and prints a table with the TPC-H Q1 query, the full TPC-H set and a large SQL file, for both cpp-peglib and Yacc, and then computes the ratio of the two for the large SQL case, with an optional PEG-opt variant alongside.
So the project publishes a claim and the harness to check it, and the release history is busy, with v1.21.0 on 2026-10-07, v1.20.0 on 2026-10-05 and v1.19.1 on 2026-10-01, and the last push was on 2026-10-07.
Editorial conclusion
cpp-peglib fits a C++ project that needs a parser for a small or medium grammar and would rather write the grammar than maintain generated code and a build step for it. It is a poor fit for a grammar with heavy backtracking, because ordered choice commits to the first alternative that matches, and a poor fit if you need the ecosystem around a generated parser, since there is no generated code here at all. Before adopting it, check your compiler, because the library requires C++17 and some Linux distributions need -pthread at link time, and read the packrat note, since the memoisation costs memory in proportion to the input. The library is MIT licensed, the newest release is v1.21.0 from 2026-10-07, and the last push was on 2026-10-07.
Frequently asked questions
What is cpp-peglib?
It is a C++17 header-only PEG library. You include peglib.h in your project and hand a grammar string to a parser object at runtime, with no code generation step and no generated source in your tree. The grammar syntax follows Bryan Ford's PEG document with a set of additional operators layered on top.
What compiler settings does cpp-peglib need?
The library only supports C++17 compilers, so the -std=c++17 option must be enabled, with /std:c++17 /Zc:__cplusplus for MSVC. The README also notes that some Linux distributions such as Ubuntu and CentOS need the -pthread option when linking, with three issue threads linked for that.
Does cpp-peglib support left recursive grammars?
Yes, through a seed-growing algorithm, and it covers direct, indirect and mutual left recursion, which allows left-associative operators to be written naturally. There is also a dedicated infix expression construct written as { precedence L - + L / * } for declaring operator precedence and associativity.
When should I enable packrat parsing in cpp-peglib?
It is off by default and turned on with enable_packrat_parsing. The README is explicit that it pays where a grammar often tries a rule again at one position and can cost more than it saves where it seldom does, and that it takes memory growing with the input, so you should measure it with your own grammar and input.
How do I attach a semantic action to a rule in cpp-peglib?
You assign a lambda to the rule name on the parser, and there are four signatures available, taking SemanticValues by const or non-const reference and an optional any& dt read-write context. A returned value of any type is wrapped by peg::any, and returning nothing yields the first semantic value, the same behaviour as the Yacc parser.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/yhirose-cpp-peglib)Was this useful?