Open-source project
pointlander/peg avatar
pointlander/peg

peg generates a packrat parser in Go from a .peg grammar file

Peg, Parsing Expression Grammar, is an implementation of a Packrat parser generator.

1,118 stars122 forksGoBSD-3-Clause

At a glance

What is it?
peg is Andrew Snodgrass's Go implementation of the Packrat parsing expression grammar generator originally written in C by Ian Piumarta, and it plugs into the Go toolchain through go:generate. It fits a Go project that wants a hand-written style recursive descent parser from a declarative grammar, and it does not fit a project that needs left recursion or a grammar written at runtime.
Who is it for?
peg fits a Go project that wants a generated recursive descent parser with backtracking and negative lookahead, and it does not fit a team that needs left recursion, a runtime-loadable grammar, or error messages as rich as a hand-written parser. Before adopting it, check the peg-file-syntax.md documentation for the constructs it supports, and note that the generator is self-hosted, so the first go generate has to bootstrap the tool before it can generate anything else.
Can I use it commercially?
Yes. BSD-3-Clause 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 last received commits 11 days ago.
What is it written in?
Mainly Go, 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

Packrat gives backtracking and lookahead that a regex engine cannot

peg is an implementation of a Parsing Expression Grammar in Go, and the README explains the concept with the usual comparison to regular expressions. A PEG is a way to create grammars similar in principle to regular expressions, but which allow better code integration.

The specific implementation is a Packrat parser generator, originally implemented as peg/leg by Ian Piumarta in C. The README describes a Packrat parser as a descent recursive parser capable of backtracking and negative look-ahead assertions, which are problematic for regular expression engines.

That is the technical reason to reach for a PEG rather than a regular expression. A regular expression cannot express nested structure, so every non-trivial language needs a hand-written parser. A PEG keeps the declarative shape, you write rules that look like patterns, and the generated parser handles the recursion.

The two capabilities named in the README are the ones that matter in practice. Backtracking means a rule can try an alternative when a later part of the input fails, which is the natural way to express ambiguity in a grammar. Negative lookahead means you can assert that something does not appear, which is the mechanism for saying, for example, that an identifier is not a keyword without duplicating the keyword list.

The licence is BSD-3-Clause and the last push was on 2026-10-12. The author line credits Andrew Snodgrass, and the project has no GitHub releases, so the version comes from the module proxy.

peg is a Go tool dependency, and the grammar is a generated Go file

The usage section is four steps, and it uses the modern Go tool dependency mechanism rather than the older go get and go install pattern.

First, add peg as a tool dependency:

code
go get -tool github.com/pointlander/peg@main

Second, create your mygrammar.peg file. Third, add a go:generate line in a Go source file of your package:

go
//go:generate go tool peg mygrammar.peg

The README notes this is a widespread community design pattern, adding lines like this in doc.go. And fourth, generate mygrammar.peg.go from mygrammar.peg with go generate.

The example in the README shows the same flow applied to the project itself, creating the file peg.peg.go with:

code
go tool peg -inline -switch peg.peg

The -inline and -switch flags are worth noticing, since they change the shape of the generated code rather than just its formatting. The -h flag prints the tool's own help.

The go.mod is minimal in a way that says something about the project. It declares the module as github.com/pointlander/peg, requires go 1.25, and has a single tool directive for github.com/pointlander/peg itself, and no other dependencies at all. A parser generator that depends on nothing but the standard library is a reasonable thing to want in a build.

The peg file syntax is documented separately, in docs/peg-file-syntax.md, for creating your own *.peg file.

The generator is self-hosted, so the first build is a bootstrap

The development section describes a self-hosting arrangement, and it is the part of the repository layout that explains the peg.peg file.

The top level holds peg.peg, the grammar for the generator itself, alongside peg.peg.go, the generated Go source for it. There is also a bootstrap/ directory with bootstrap.bash and generate-grammars.bash, a grammars/ directory, a set/ directory, a tree/ directory, cmd/, main.go, and peg_test.go.

The build instructions make the ordering explicit. Under Generate, the note says to bootstrap and generate grammar *.peg.go, and that these commands should initially be executed once before other commands, with go generate. Under Build, it is simply go build, with a note that go generate is required once beforehand. The same note appears under Test, Lint and Benchmark, so the sequence is: generate once, then everything else works.

That is the standard pattern for a self-hosted compiler or generator, and it has a practical consequence. The first build on a fresh checkout is not just a build, it is a bootstrap, and if the generated file is out of date with respect to the grammar the results can drift. The project handles that by keeping the generated file in the repository, which is why peg.peg.go is present alongside peg.peg rather than being produced only at release time.

The test suite runs with go test -short ./..., the benchmark with go test -benchmem -bench ., and linting with golangci-lint run and golangci-lint fmt. The development requirements are Go as pinned in go.mod, golangci-lint v2 or later, and Bash 3.2.x or higher.

Version stamping uses git describe, and Go 1.18 embeds the same information

The Set version section is a small piece of engineering that is worth copying, because it answers a question every released tool has to answer.

The instruction is to use the version from the tag if the current commit has a tag, and otherwise the current commit hash, with the command:

code
go build -ldflags "-X main.Version=$(git describe --tags --exact-match 2>/dev/null || git rev-parse --short HEAD)"

The shell construct is the interesting part. git describe --tags --exact-match succeeds only when the current commit is exactly a tag, and the 2>/dev/null swallows the error otherwise, so the fallback git rev-parse --short HEAD kicks in and the commit hash is used. The -X flag then writes that string into the main.Version variable at link time.

The README also points out that since Go 1.18 the go command embeds version control information, and that you can read it with go version -m peg. That is a genuinely useful piece of advice for a distributed binary, because it means a user who has only the executable can find out which commit it came from without any build-time metadata file shipped alongside it.

For a parser generator, that matters a little more than usual. A generated parser is checked into your repository, and if two developers generate slightly different output from the same grammar because their generator builds differed, the diff is confusing. Embedding the version makes that traceable.

Where peg is the wrong tool

The first wrong-tool case is a grammar with left recursion. A PEG parser cannot handle a rule that is directly or indirectly left recursive, because the parser would recurse forever before it could try the alternatives. This is a well-known limitation of the packrat PEG approach rather than a bug in this implementation, and the practical consequence is that a grammar for a language with left-recursive expression syntax, the usual infix operator precedence rules, has to be restructured before it can be expressed as a PEG.

The second is error message quality. A generated parser reports where a rule failed, and the message is derived from the grammar rather than written by a human. A hand-written recursive descent parser can say expected a semicolon after the return type on line 42, while a generated one says something closer to no match found. For a compiler or a configuration language where users type things by hand, that difference matters.

The third is that the grammar is a build artefact. The .peg file has to exist in the repository, the generated .go file has to be checked in, and CI has to verify they are in sync. A team that prefers a hand-written recursive descent parser, where the parser is source code and the errors are as good as the author made them, gets nothing from this tool.

The fourth is the toolchain requirement. The go.mod requires Go 1.25, and the generator has to be fetched with go get -tool at a specific ref. A team on an older Go release, or one with a locked and mirrored dependency set, has to make room for it. The absence of any third-party dependency makes that easier, but the Go version floor is still a floor.

Editorial conclusion

peg fits a Go project that wants a generated recursive descent parser with backtracking and negative lookahead, and it does not fit a team that needs left recursion, a runtime-loadable grammar, or error messages as rich as a hand-written parser. Before adopting it, check the peg-file-syntax.md documentation for the constructs it supports, and note that the generator is self-hosted, so the first go generate has to bootstrap the tool before it can generate anything else.

Frequently asked questions

What is peg and what does it generate?

peg is a Go implementation of a Packrat parsing expression grammar generator, originally implemented as peg/leg by Ian Piumarta in C. It generates a recursive descent parser with backtracking and negative lookahead from a .peg grammar file, producing a Go source file such as mygrammar.peg.go.

How do I add peg to a Go project?

Add it as a tool dependency with go get -tool github.com/pointlander/peg@main, create your .peg grammar file, add a //go:generate go tool peg mygrammar.peg line in a Go source file of your package, and then run go generate.

Why does building peg require running go generate first?

The generator is self-hosted, so peg.peg is the grammar for the generator and peg.peg.go is its generated source. The README marks go generate as required once beforehand for the build, test, lint and benchmark targets.

What are the development requirements for peg?

Golang as pinned in go.mod, which requires Go 1.25, golangci-lint v2 or later, and Bash 3.2.x or higher. The go.mod declares no third-party dependencies, only a tool directive for the project itself.

Official sources

  1. Issues
  2. License: BSD-3-Clause
  3. pointlander/peg on GitHub
  4. README
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.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/pointlander-peg.svg)](https://hysenlabs.com/projects/pointlander-peg)

Was this useful?