One Grammar. Every Parser.
Today: Go and TypeScript targets, three Go engines and a published bytecode, all from one PEG-based grammar. The aim is every target, engine, optimization profile and use, at the best measured speed.
A .pego grammar extends Parsing Expression Grammars with
types, operator precedence, left recursion and error recovery, so one file defines both the syntax and the values you
get back. PEGO is a grammar compiler and parsing platform designed for every target, engine and use: it checks and
compiles a grammar once, and every target and engine returns the same result for the features it supports.
Today PEGO generates Go and TypeScript and runs grammars on three engine backends in Go: a closure backend and two
VMs for a bytecode with a published specification. Only the Go VMs implement the bytecode, and it stays unstable until
it is declared stable and versioned, which the first tagged release does not do. One grammar gives trees, typed values,
recognition, declared recovery, an incremental Document and streaming, with per-target gaps. Generated Go needs only
the standard library, and generated TypeScript has no dependencies. Its best fit today is a Go program, alone or with a
TypeScript front end, that parses its own DSL, configuration, query or policy language.
Next come a tagged release with work limits on by default, a public cross-tool benchmark, faster parsers and
generation profiles for speed, memory and size, editor support for the languages you define, and Document and
streaming in TypeScript. More target languages and another implementation of the bytecode follow after the release.
Speed is a pillar: the aim is the best measured speed in every target and use, against published
targets. Today generated Go is slower than Go's hand-written standard-library
parsers on every comparable workload: it takes 1.1–5.4× their time for typed values and 1.6–6.8× for trees, from JSON
(3.89 ms against 3.50 ms for encoding/json Unmarshal into any, which records no positions) to left-recursive
arithmetic (see the benchmarks).
Not yet released or safe by default. There is no tagged release, and the language, the API and file formats may change. For untrusted input, only the nesting cap is on: work limits are opt-in, there is no memory limit, and no run-time bound holds for every grammar. Why PEGO pairs each aim with its status today, compares PEGO with other tools and lists the current limits.
calc.pego
Edit in the playground →
// calc.pego
type Num = node[string]
type Bin = { Left Expr; Op node[string]; Right Expr }
type Expr ~ Bin | Num
@start
rule main Expr = #operators {
skip " "*
operand num
operand "(" e:main ")" => $e
level additive { infix left "+" / "-" => Bin{Left: $lhs, Op: $op, Right: $rhs} }
level multiplicative { infix left "*" / "/" => Bin{Left: $lhs, Op: $op, Right: $rhs} }
level power { infix right "^" => Bin{Left: $lhs, Op: $op, Right: $rhs} }
}
rule num Num = ([0-9]+) #atomic
pego parse -g calc.pego -f sexpr -i '1 + 2 * 3 ^ 2'
Bin{Left: Num("1"), Op: "+", Right: Bin{Left: Num("2"), Op: "*", Right: Bin{Left: Num("3"), Op: "^", Right: Num("2")}}}
pego parse -g calc.pego -i '1 + * 2'
pego: 1:5: syntax error: expected "(", [0-9]
p, _ := pego.CompileSource(src, "main")
tree, err := p.Parse("1 + 2 * 3 ^ 2") // a *pego.Node, or a *pego.SyntaxError with line, column and expectations
Note
PEGO is pre-release: the language, the Go API and the file formats may change between versions. See the implementation status and the living backlog.
Why PEGO
Typed values from the grammar
Declare struct and join types and build them in actions. Rule types are
inferred and checked when the grammar compiles. pego gen -types emits matching Go values directly, and generated
TypeScript converts its tree with parseAST (see the code generation guide).
Errors that point the way
Failures report the farthest position and what was expected there. @error
rewrites the message, and #recover skips the broken part and keeps going, where the grammar declares it.
Reparse after an edit
A Document keeps the rule results whose examined input an edit did not reach; how
much that is depends on how the grammar splits into rules (see
incremental reuse). On the Minilang workload, an 89 KB program edited by one
character reparses in 116–133 µs against 15.6–24.5 ms for a full parse, on the engine backends (see the
benchmarks).
Long input, bounded memory
ParseStream hands over each element of a #stream repetition as it matches and
drops the input behind it, so the input and tree memory it keeps depend on the size of one element, not on the
length of the input. The start rule needs a top-level #stream repetition, and recovered errors are kept unless
you set a limit (see the streaming guide).
Go and TypeScript today, more targets as the aim
pego gen emits a standalone parser: Go that needs only the
standard library, or a TypeScript module with no dependencies. The repository tests run the TypeScript under Node.js;
Deno, Bun and browsers are documented but not tested. Generated TypeScript has no Document or streaming (see the
TypeScript guide).
A published bytecode
Grammars also compile to a bytecode with a published
specification, implemented today by PEGO's Go VMs and unstable until it is declared stable
and versioned. A precompiled .pegoc file of the JSON or Minilang grammar is ready to parse in 35.5–135 µs.
Every backend, one answer
For the features they share, the test suite checks that the closure backend, both bytecode VMs, generated Go and generated TypeScript return the same trees, values, positions and errors.
Speed as a goal, measured in the open
The aim is to match, then beat, hand-written parsers while returning typed
values. Today generated parsers take 1.1–5.4× the time of the Go standard library's hand-written parsers for typed
values and 1.6–6.8× for trees on PEGO's benchmarks, which do different work (encoding/json builds no positions,
encoding/xml returns tokens, encoding/csv returns records, go/parser builds a positioned Go syntax tree); a
recognizer validates JSON with zero allocations (see the benchmarks and the
targets).
Limits you can set
Nested rule calls are capped by default: 100,000 on the closure backend, the recursive VM
and generated Go, and 10,000,000 on the iterative VM, which runs on its own stack
(deep nesting). Work limits and cancellation are opt-in;
a generated parser carries them when it is generated with -work-controls
(Cancellation and Work Limits).
Precedence without the pain
Operators are declared by binding level in an Operators Rule, not a ladder of rules. Left recursion, direct or indirect, works.
See what your grammar does
pego trace shows every rule call with positions, results and memo hits,
pego explain which calls make up a syntax error, and pego profile where the time and the backtracking go.
pego lint points out alternatives that can never match and other likely mistakes before any input does.
Editor support for grammars
pego lsp gives any LSP editor errors as you type in .pego files, formatting,
go to definition, hover with inferred rule types, rename and completion; a VS Code extension is included.
Context when you need it
Predicates and scoped variables handle indentation, matching tags and other things plain PEG cannot.
#Install
go get github.com/ornew/pego # library
go install github.com/ornew/pego/cmd/pego@latest # command-line tool
Go 1.27.1 or later. No dependencies outside the standard library.
#Use It
From Go
p, err := pego.CompileSource(grammarSource, "main") // grammar errors come with positions
node, err := p.Parse(input) // positions in code points by default
p.Parse(input, pego.WithUnit(pego.Bytes)) // ... or in UTF-8 bytes
p.Parse(input, pego.WithBackend(pego.Bytecode)) // pick a backend
p.Parse(input, pego.RecognizeOnly()) // validate without building a tree
json.Marshal(node) // nodes encode with types and positions
err = p.ParseStream(r, func(n *pego.Node) error { return nil }) // one #stream element at a time
doc, _ := p.NewDocument(text)
doc.Edit(10, 12, "new text") // replace [10, 12)
node, err = doc.Parse() // reuses rule results the edit did not reach
data, _ := p.MarshalBinary() // save; pego.LoadParser(data) skips parsing and checking
From the command line
pego parse -g grammar.pego -i 'abc=12' # parse and print the tree (JSON, or -f sexpr)
pego parse -g grammar.pego -check < input.txt # validate only
pego explain -g grammar.pego < input.txt # which rule calls make up a syntax error
pego trace -g grammar.pego -i 'abc=12' # every rule call as an indented tree (-f json for tools)
pego profile -g grammar.pego < input.txt # cost per rule, with hints on wasted work
pego lint -g grammar.pego # likely mistakes: dead alternatives, unused captures, ...
pego lsp # language server for editors (errors, formatting, rename, ...)
pego fmt -w grammar.pego # format in place, comments kept
pego fix -w grammar.pego # rewrite older syntax to the current one
pego compile -g grammar.pego -o grammar.pegoc # precompile
pego gen -g grammar.pego -module example.com/calc -o calc # generate a standalone Go parser
pego gen -g grammar.pego -module example.com/calc -types -o calc # ... with Go types for the grammar's types
pego gen -g grammar.pego -module example.com/calc -recognize -o calc # ... with Recognize, which validates without a tree
pego gen -lang ts -g grammar.pego -o ts-parser # generate a TypeScript parser into a directory
pego gen -lang ts -g grammar.pego -types -o ts-parser # also emit types.ts and parseAST
pego gen -g grammar.pego -module example.com/calc -max-depth 600000 -o calc # set its nested rule-call default
pego sample -g grammar.pego -n 20 -coverage # generate inputs the grammar accepts (tests, fuzz seeds)
pego edit-fuzz -g grammar.pego -minimize -save-failures out # check Document edits against fresh parses
Run pego without arguments for every command and flag.
#Ready-Made Parsers
parsers/ has parsers for common languages, written in PEGO, generated into Go and tested against the
languages' conformance suites and reference implementations. Each is a module of its own that depends only on the
standard library and PEGO's runtime package. Until PEGO publishes a release, use them from a checkout of the
repository; go get does not fetch them from another module yet (see Ready-Made Parsers):
import "github.com/ornew/pego/parsers/json"
v, err := json.Decode(`{"name": "pego", "tags": ["peg", "operators-rule"]}`)
| Language | Module | Specification |
|---|---|---|
| CEL | github.com/ornew/pego/parsers/cel |
The CEL language definition |
| CSV | github.com/ornew/pego/parsers/csv |
RFC 4180 |
| CUE | github.com/ornew/pego/parsers/cue |
The CUE language specification |
| DuckDB SQL | github.com/ornew/pego/parsers/duckdb |
DuckDB 1.5.6's SQL dialect |
| Go | github.com/ornew/pego/parsers/golang |
The Go specification (go1.27), as go/parser parses it |
| JSON | github.com/ornew/pego/parsers/json |
RFC 8259 |
| Python | github.com/ornew/pego/parsers/python |
Python 3.14 |
| TypeScript | github.com/ornew/pego/parsers/typescript |
TypeScript 5.9 and TSX |
| XML | github.com/ornew/pego/parsers/xml |
XML 1.0 (Fifth Edition), non-validating |
| YAML | github.com/ornew/pego/parsers/yaml |
YAML 1.2.2 |
#Examples
Small grammars with tests that show the features live in examples/: a calculator (Operators Rule and left-recursive), an indentation-based outline format and a small programming language. The complete parsers of parsers/, such as the ones for Go and Python 3, are the larger examples.
#Contributing
Bug reports, ideas and pull requests are welcome. Run go test ./... before sending changes, and follow the
commit message guidelines. Report security vulnerabilities privately, as the
security policy describes.
#License
Documentation
Getting Started With PEGO
From a first grammar to typed trees and operator precedence, step by step.
PEGO Guides
Task-oriented guides to trees, expressions, errors, context, runtimes and code generation.
The PEGO Specifications
The normative definition of the PEGO grammar language.
Go API Reference
The Go API and the command-line tool, generated from the source.
Examples
Small grammars that show the features: a calculator, an outline and a small language.
Design Records
The decisions behind PEGO and their rationale.