# pom writes PEG parsers in Rust with operators instead of macros

> pom is a parser combinator library that builds parsing expression grammars by overloading Rust operators, so a grammar reads like the rules it implements. It is small, macro-free and MIT licensed, and its releases are infrequent.

**J-F-Liu/pom** — PEG parser combinators using operator overloading without macros.

- Repository: https://github.com/J-F-Liu/pom
- Stars: 526 · Forks: 35
- Language: Rust
- License: MIT
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/j-f-liu-pom

## Why operator overloading instead of a macro

pom describes itself as PEG parser combinators created using operator overloading without macros, and the README spends a paragraph arguing the case for that choice.

The argument runs like this. A parser combinator is a higher-order function that accepts several parsers as input and returns a new parser as its output, and combinators enable a recursive descent parsing strategy that facilitates modular piecewise construction and testing. With operator overloading, a combinator can take the form of an infix operator, used to glue different parsers to form a complete rule. That lets parsers be defined in an embedded style, in code which is similar in structure to the rules of the formal grammar. And the code is easier to debug than macros.

The stated main advantage is that you do not go through any kind of code generation step, you are always using the vanilla language underneath. The README is candid that there are build issues, and the usual issues around error messages and debuggability, though it notes in fairness those are about as bad with macros as with code generation.

The practical payoff is that grammar expressions and plain code can be freely intermixed. In a macro-based parser library, the boundary between a grammar and ordinary Rust is enforced by the macro syntax itself. With operators, the boundary blurs, and a function call can sit in the middle of a rule.

The whole library is one dependency, bstr, and the manifest comments that only one function is used from it so no features are needed. That is about as small as a Rust parser library gets.

## The combinator table is the entire public surface

The API is small enough to read in one screen, and the README's two tables are the whole thing.

The basic parsers cover the terminals. empty() always succeeds and consumes no input. end() matches end of input. any() matches any symbol and returns the symbol. sym(t) matches a single terminal symbol. seq(s) matches a sequence of symbols, list(p,s) matches a list of p separated by s, one_of(set) succeeds when the current input symbol is one of the set, and none_of(set) succeeds when it is none of them. is_a(predicate) succeeds when the predicate returns true on the current input symbol and not_a(predicate) when it returns false. take(n) reads n symbols and skip(n) skips n. call(pf) calls a parser factory, which the README notes can be used to create recursive parsers.

The combinators are the operators and methods. The choice operator matches p or q and returns the result of the first success. The sequence operator matches p and q and, if both succeed, returns a pair of results. Then there are two sequence variants that keep only one side: p - q returns the result of p, and p * q returns the result of q. The bind operator parses p to get a result P, then parses q and returns the result of q applied to P, which is how a value is threaded into the next parser.

The lookahead pair works as expected: -p succeeds when p succeeds without consuming input, and !p succeeds when p fails without consuming input. On the method side, p.opt() makes a parser optional and returns an Option. p.repeat(m..n) covers zero or more, one or more, a bounded range, and an exact count. p.map(f) converts a parser result, and p.convert(f) does the same but fails in case of conversion error. p.pos() gives the input position after matching p, and p.collect() collects all matched input symbols.

That last one, collect(), is worth noting because it is how you get the raw text of a match back, which is what makes a parser useful for source-to-source tools and editors rather than only for structured values.

## The crate is a single-symbol parser unless you turn on the utf8 feature

The manifest makes a design decision that is easy to miss: the default feature set.

```toml
[features]
default = ["utf8"]
utf8 = []
trace = []
```

The utf8 feature is on by default, and the crate is called pom after all, but the symbol type is still a single symbol. The feature decides whether that symbol is a byte or a character. Turning utf8 off gives you byte-level parsing, which is what you want for a binary format, and turning it on gives you character-level parsing for text.

The examples directory makes the range explicit. There is a duration.rs, a json.rs, a json_char.rs and a json_file.rs, a simple.rs, a whitespace.rs, and two UTF-8 examples, utf8.rs and utf8_mixed.rs, alongside a test.json data file. Having both a json.rs and a json_char.rs side by side is the clearest evidence that the symbol granularity is a choice you make per parser rather than a property of the library.

There is a second optional feature, trace, which is off by default. The name suggests it instruments the parse, which is the kind of thing you turn on when a grammar is misbehaving and you need to see how far the parser got. The README does not describe what the trace output looks like, so the API reference on docs.rs is the place to check.

The manifest also records that the package is called pom, version 3.4.1, edition 2021, MIT licensed, with categories of parsing and keywords for parser, parser-combinators, parsing and PEG. The one dependency, bstr, is commented in the manifest as being used for a single function with no features needed, which is a hint that the crate could be vendored in a couple of hours if a dependency-free build ever became a requirement.

## PEG's no-ambiguity rule is a feature and a constraint

pom is built on parsing expression grammar, and the README's explanation of what that means is worth reading because PEG properties are design constraints rather than implementation details.

PEG stands for parsing expression grammar, a type of analytic formal grammar, which describes a formal language in terms of a set of rules for recognizing strings in the language. Unlike context-free grammars, PEGs cannot be ambiguous: if a string parses, it has exactly one valid parse tree. Each parsing function conceptually takes an input string as its argument and yields either success, in which case the function may optionally move forward or consume one or more characters of the input, or failure, in which case no input is consumed.

The no-ambiguity property is what makes a PEG parser predictable, and it is also why the operator set is the way it is. Ordered choice is committed choice: the choice operator returns the result of the first success, so a later alternative is only tried if the earlier one failed outright, never if it succeeded but matched less than you wanted. For a grammar where two alternatives could both match, PEG forces you to order them by specificity yourself.

The same commitment shows up in the failure rule: a failed parser consumes no input. That keeps backtracking predictable, since the input position is a simple property of the parse rather than something the combinators have to unwind.

The practical downside is that PEGs do not support left recursion, and the consequence is that some grammars that are natural in a CFG have to be restructured. The README does not discuss the workaround, so if your grammar is left recursive you will need to work that out yourself or pick a different tool.

## Where to learn it, and what the repository contains

The project points at three places to learn it, and the first is unusually good.

The tutorial lives in the repository as doc/article.md, the API reference is published on docs.rs, and the README links to Learning Parser Combinators With Rust by Bodil Stokke. That third link matters, because it means you can learn the concept from a general treatment and then read the pom-specific tutorial for the API, rather than learning an unfamiliar library and a unfamiliar grammar formalism at the same time.

There is a Discord badge, so a community exists.

The repository layout is small and conventional. There is src/ for the library, tests/ for the test suite, benches/ for benchmarks, examples/ for the runnable examples listed earlier, doc/ for the tutorial, assets/ for images, and a rustfmt.toml plus an .editorconfig, so the project formats itself with a checked-in configuration. A .travis.yml carries the build badge, which is an old choice but also a signal about the project's vintage.

The benches directory is worth a second look if you are evaluating performance, since the crate is small enough that the benchmark suite is likely more about the combinator overhead than about the grammar you write. Note that there is no published benchmark numbers in the README, so you would be running those yourself rather than reading them.

For a crate of this size, the fastest way to judge it is to read the examples directory. duration.rs, json.rs, whitespace.rs and simple.rs cover the common shapes, and the two UTF-8 examples show the symbol-granularity decision in practice.

## Where pom is the wrong tool

The first wrong-tool case is a grammar with left recursion or a need for error recovery. PEG has no left recursion, and the ordered-choice semantics mean a failure part-way through a rule does not tell you what the parser expected. If you are writing an editor that must keep parsing after a syntax error and report several of them, that is a different kind of parser than the one pom builds.

The second case is a very large grammar with heavy backtracking. The choice operator and the sequence operators explore alternatives, and a grammar where every rule can match in several ways turns into exponential behaviour. The combinator set has no memoisation, since the manifest lists a single dependency and no caching layer. For a configuration language or a small protocol, that is irrelevant. For a large general-purpose language, it is the wrong shape.

The third case is a team that wants a maintained dependency with predictable releases. The release history here is thin and dated. The recent releases are v3.4 tagged on 2024-03-06, v3.1.0 on 2020-08-04 and v3.0.3 on 2019-12-17, while the manifest carries version 3.4.1, so the working tree is ahead of the last tag. The last push was on 2026-09-28 and the default branch is master.

The alternative approach, and the one to weigh against it, is a parser generator with a grammar file. The difference in approach is fundamental. A generator such as LALR or a hand-written recursive descent parser derives a table or a function set from a grammar at build time, which gives you left recursion, unambiguous precedence rules and often better error positions, at the cost of a build step and a grammar syntax that is not Rust. pom keeps everything in ordinary Rust source, which means the grammar is type checked, refactored by the usual tooling, and debugged with the usual debugger, but gives up the generator's strengths.

There is a third path worth naming, and it is a matter of taste rather than capability: writing the recursive descent parser by hand in plain Rust with no combinator layer at all. For a small grammar, that is often the shortest path, and it keeps full control over error messages. pom's argument against that is that its operator style keeps the grammar and the code interleaved, so you get combinator structure without a separate grammar description to keep in sync.

## Conclusion

pom suits a Rust developer who wants a readable, macro-free PEG parser for a small or medium grammar and would rather compose operators than write a parser generator. It does not suit someone who needs error recovery, left recursion, or a maintained release cadence, since the last tagged version is 3.4.0 from 2024-03-06 while the manifest carries 3.4.1. Before adopting, read the tutorial in doc/article.md and the API reference on docs.rs, and confirm that the operator precedence in your grammar matches what the table documents, since a mis-ordered sequence silently parses the wrong language.

## FAQ

### What is pom, the Rust parser library?

pom is a set of PEG parser combinators for Rust that use operator overloading rather than macros to build rules. It has one dependency, bstr, and an MIT licence.

### Which operators does pom use to combine parsers?

The choice operator matches p or q and returns the first success, the sequence operator matches both and returns a pair, p minus q and p times q return one side only, and the bind operator threads p's result into q. Lookahead is written as -p and !p.

### How do I make pom parse UTF-8 characters instead of bytes?

The utf8 feature is enabled by default in the manifest's default feature list, and disabling it gives byte-level parsing. The examples directory shows both approaches with json.rs and json_char.rs.

### Does pom support left recursion?

No. PEGs cannot be ambiguous and, as the README explains, each parsing function either succeeds and may consume input or fails without consuming any. Left recursive grammars are therefore not expressible and have to be restructured.

## Sources

- [Issues](https://github.com/J-F-Liu/pom/issues)
- [J-F-Liu/pom on GitHub](https://github.com/J-F-Liu/pom)
- [License: MIT](https://github.com/J-F-Liu/pom/blob/master/LICENSE)
- [README](https://github.com/J-F-Liu/pom/blob/master/README.md)
- [Releases](https://github.com/J-F-Liu/pom/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/j-f-liu-pom
