Chapter 7
Generated Parsers
pego gen translates a grammar into the source of a standalone parser, in Go or
in TypeScript. A generated parser does not load or compile a grammar at run time
and does not depend on the PEGO engine. This chapter defines the generator, the
API of the generated Go and TypeScript code, the features each target supports
and where its behavior differs from the engine. Where this chapter does not state
a difference, a generated parser returns the same value, the same positions and
the same errors as the engine for the same grammar, input, start rule and
position unit.
Scope and Terminology
The engine is the pego.Parser that parses with a compiled grammar. A
generated parser is the source that gen.Go, gen.TypeScript (package
github.com/ornew/pego/gen) or pego gen writes. The generation options select optional parts of the
output. The generated package is the Go package named by -pkg; the
generated module is the TypeScript file. The generated Go code uses the runtime
package, which is the same for every grammar of one generator release, as a copy in
internal/runtime or imported from this module; the TypeScript module includes its own runtime.
Contract
The Generator
pego gen -g <grammar> [-lang go|ts] [-module <import path>] [-pkg <package>]
[-runtime embed|import|only] [-s <rule>] [-o <path>]
[-types] [-document] [-stream] [-recognize] [-observability]
[-source-info] [-default-value] [-nodoc] [-max-depth <n>]
| Flag | Field of gen.Go / gen.TypeScript |
Meaning |
|---|---|---|
-lang |
gen.Go or gen.TypeScript |
go (default) or ts |
-g |
the grammar argument of Generate |
The grammar source: .pego or .json. A compiled grammar holds no grammar and is an error |
-module |
Module |
Required for Go (with -runtime import only when -pkg is omitted); the import path of the generated package; rejected with -lang ts |
-pkg |
PackageName |
The package name; MUST be a valid Go identifier; default: the last element of the import path without a major version suffix (example.com/m/v2 gives m), and when that is not a valid identifier, generation fails and suggests -pkg; rejected with -lang ts and with -runtime only |
-runtime |
Runtime |
How the parser reaches the runtime. embed (default) writes a copy of the runtime: internal/runtime in Go, runtime.ts in TypeScript. import imports it: github.com/ornew/pego/runtime in Go, the module that -runtime-module names in TypeScript. only writes just the copy (GenerateRuntime) and takes no grammar and no other generation flag |
-runtime-module |
RuntimeModule (gen.TypeScript) |
TypeScript only, and required with -runtime import: the module specifier that parser.ts and types.ts import the runtime from, such as ../shared/runtime.ts. Rejected with -lang go and with any other -runtime |
-s |
the start rule argument of Generate |
The start rule, a rule marked @start. Default: the default start rule |
-o |
For Go, the output directory (required), which the command creates; it writes parser.go, api.go, with -types types/types.go, and, with embed, internal/runtime/*.go, and removes generated files of an earlier generation that it did not write again (only writes and removes only files of internal/runtime, so it never removes parser.go, api.go or types/types.go). A file that lacks the // Code generated by pego. header is never replaced or removed: the command fails before it writes anything. For TypeScript, the output directory (required): it writes parser.ts, with -types types.ts, and, with embed or only, runtime.ts |
|
-types |
Types |
Generate typed values (see Typed values) |
-document |
Document |
Generate the Go Document (Go only) |
-stream |
Stream |
Generate ParseStream (Go only; the start rule MUST contain a #stream repetition) |
-recognize |
Recognize |
Generate recognition without a tree |
-typed-only |
TypedOnly |
Go only; needs -types. Generate only the typed entry points (ParseAST, ParseRuleAST and their options); there is no Parse, ParseRule or Recognize. Rejected with -recognize, -document, -stream and -lang ts. A typed entry point that converts the Node of its rule keeps the unexported Node rule functions it runs |
-observability |
Observability |
Generate tracing and profiling, specified in Observability |
-source-info |
SourceInfo |
Make the parser record source information: the rule of each node and the names of captures. Without it, Node.Rule, Node.Field and Node.Fields of a parsed node are empty and the outputs omit them |
-default-value |
DefaultValue |
Make the parser return default values, as Default Values describes. Cannot be combined with -types, because the types come from the dropped actions |
-nodoc |
OmitPackageDoc |
Omit the package comment (Go only; rejected with -lang ts) |
-max-depth |
MaxDepth |
The default nested rule-call limit; zero selects 100,000, a positive value overrides it, a negative value fails generation. TypeScript also requires a safe integer |
- The output is deterministic: generating twice yields identical bytes. Omitted, zero and an explicit 100,000 depth default generate identical source.
- Go output is formatted as by
gofmt. Generated Go with the defaultembedruntime depends only on the standard library, and generated TypeScript has no dependencies. Generated TypeScript uses only erasable syntax and needs ES2020; it is type-checked under strict settings. - Grammar errors are reported with positions before any file is written, in the
same form as in other commands. An unknown start rule, an invalid package name,
a compiled grammar (
gen needs the grammar source),-streamwithout a#streamrepetition in the start rule and a grammar that contains#missingare errors. - A Go package may contain only one generated parser, because every generated file
defines the same names.
api.gore-exports the runtime names the users of the parser need, as aliases of the runtime's types and wrappers of its functions. - With
-typesthe Go types of the typed values are declared in the package<module>/types(the filetypes/types.go), which the parser package imports; a module path is required. The types package imports only the runtime, and declares a constantPegoSchema_<digest>that the parser package refers to once (see below). The parser package refers to the types astypes.Name, and the users of the parser import<module>/typesfor them. The grammar's types therefore share no namespace withparser.goandapi.go: a grammar type may be namedNode,Parser,Walk,ParseOptionor like any other export of the runtime.
Generated Go API
func Parse(input string, opts ...ParseOption) (*Node, error)
func ParseRule(name, input string, opts ...ParseOption) (*Node, error)
type ParseOption func(*parseOptions)
func WithUnit(unit PositionUnit) ParseOption
func WithMaxDepth(n int) ParseOption
func WithContext(ctx context.Context) ParseOption // with work controls
func WithWorkLimits(limits WorkLimits) ParseOption // with work controls
type PositionUnit int
const (
CodePoints PositionUnit = iota // the default
Bytes
)
Parseparses the whole input with the start rule.ParseRuledoes the same with the named start rule, a rule marked@start; another rule is the errorrule <name> is not a start rule; parsing can begin only at a rule marked @start, an unknown name the errorrule <name> is not defined, the name of a hidden rule the error that the rule is hidden, and an empty name selects the default start rule of the grammar. Both require the rule to consume the whole input. Generated parsers contain entry code only for start rules.Node,Fields,NodeField,SyntaxErrorandSyntaxErrorsare types of the generated package (aliases of the types of its runtime) with the members of their engine counterparts, defined in Nodes and Values and Parsing. They are different Go types from those of packagepego; errors are tested witherrors.Asagainst the generated types. Node JSON is identical to the engine's.- Options apply in order. A negative final
WithMaxDepthreturns a configuration error before matching, actions or named-rule lookup; zero restores the generated default. A nil option panics when applied. A call with no options uses code points and the generated depth default. - A generated parser is safe for concurrent use: each call has its own state.
- Errors in actions (such as division by zero) and exceeding the nesting limit
return plain errors as in the engine. The depth limit counts nested rule calls; a
larger limit does not guarantee stack capacity. The default error is
nesting too deep: more than 100000 rule calls. - A Go parser generated with work controls (
-work-controls,WorkControls) exportsWithContext,WithWorkLimits,WorkLimits,WorkLimitKind,WorkLimitError,ErrWorkLimit,InputWorkLimitandStepWorkLimit. A parser generated without them holds no code that charges work or reads a control: one form of each function; with-documentit is the form that serves a Document, without charges. A parser generated with work controls has the controlled form only, and a parse that sets no limit runs it unchanged. A parse that passes either option with a context or a limit, from the runtime package, returns an error that the parser was generated without work controls; no option is ignored. A nil context and a zeroWorkLimitsset no control and are accepted. WithContextandWithWorkLimitsapply the semantics of Cancellation and Work Limits to each invocation, with counters that restart for every call. They apply to named rules, recognition, typed parsing and conversion. The last context and the last completeWorkLimitswin; a nil context means no cancellation; zero limits are unlimited. An exhausted limit matchesErrWorkLimit, anderrors.Asretrieves a*WorkLimitErrorwithKind(InputWorkLimitorStepWorkLimit),Limit,UsedandRequested. An interrupted entry point returns its zero result, and recovery does not turn an interruption into a successful partial tree. Cancellation is cooperative.
Typed Values
With -types, the Go package also defines a Go type for each type of the grammar
and the functions
func ParseAST(input string, opts ...ParseOption) (T, error) // T: the Go type of the start rule's type
func ParseRuleAST(name, input string, opts ...ParseOption) (any, error)
ParseAST returns the errors of Parse. The mapping of types is:
| Grammar type | Go type |
|---|---|
int, string, bool |
The same |
Struct type T |
*T with the same fields and an embedded Span{Start, End int} |
node[T] |
*TNode[T'], with Span and Value of the Go type T' of T: *TNode[string] for a string node, whose Value is the text, and *TNode[any] for node[_] |
Tuple (A, B, ...) |
TupleN[A', B', ...] with the fields E0, E1, ...; Unit for () |
error (from #recover) |
*Error with Span, Text and Message |
Join U ~ A | B of node types |
An interface U implemented by *A, *B and *Error (and by every *TNode[T] if a member is a node type) |
Join of declared node types written in place (a field, a list element, a rule value) and declared by no alias: A | B (two or more distinct declared node types, with or without error), or A | error (one declared node type and error) |
An interface named after its members in the order of their first spelling in the file (declarations in source order, then the fields of a type), AOrB, or AOrError for one node type and error, implemented by *A, *B and *Error. The same set of members in any order is one interface, and a set that an alias declares is that alias. A type of the grammar with the generated name, or two different sets with the same name, is a generation error. A join with a member that is not a declared node type (node[string], node[_], a list, a scalar) has no interface and is any |
[]T |
A slice; an empty list is an empty slice and not nil |
*T |
*int, *string or *bool for basic types; otherwise the Go type of T, nil when absent |
| A rule whose type is not declared | The Go type of its value (an action's value maps by the rows above): *TNode[TupleN[E0, ..., En]] for a tuple of known elements, *TNode[[]E] for a list, *TNode[Unit] for the unit (); *TNode[any] when the elements are not known; a rule with a declared type, node[_] included, always has the Go type of that type |
_, joins with basic members, with node members that are not declared node types, or with members of different shapes, a value that closes a cycle of rules |
any, holding the converted value |
Alias Alias ~ T |
A Go alias to the value type of T |
TNode[T]isstruct { Span; Value T };TupleNisstruct { E0 E0; ...; En En }andUnitisstruct{}. A missing element of a tuple isnil. A list node'sValueis a slice.- A grammar type keeps its name. The types package declares, besides the grammar's
types,
TNode,Error,PegoHooksand, when used,Unit,TupleNandPegoNodeTypes, and a constantPegoSchema_<digest>; generation fails, naming the type, if a grammar type has one of these names. The digest covers the package's declarations, not its comments or formatting. The parser package refers to the constant once, so aparser.goand atypes/types.gogenerated from different schemas do not compile together; the runtime relies on this when it reads a string node as its named node type. A grammar type namedUnitis an error only when the generated code uses the empty tuple, and one namedTupleNonly when it uses a tuple of that arity. A struct field named like a field or method of the embeddedSpan(Start,End,Range) or like a method of the typed values (TName, ...) gets a trailing underscore in Go (Start_), soStartandEndalways read the span; the value still reads back under the grammar name. Every declared alias is exported. - After
#recover, an error value in a list of a join type stays in the list as*Error; where a struct type is expected it becomesnil. ParseRuleASTconsumes the whole input, applies options once before it looks up the name, and returns the selected start rule's typed value asany. Names thatParseRulerejects, with the same errors, and invalid options returnnilwith an error. A parse failure returns the zero value of the selected type.ParseRulecontinues to return*Node.- A generator MAY build typed values directly instead of converting the tree
that
Parsereturns when every value has a Go type of its own; the result is the same either way.
Typed TypeScript output (-types) exports an interface for every struct
and alias and the functions parseAST and parseASTWithOptions, which parse the
configured start rule and convert the node result. They return { value, error }:
a successful nil result has a null value and a null error; a failure has a null
value and the error; recovery keeps the converted value and SyntaxErrors. The
mapping is:
| Grammar type | TypeScript value |
|---|---|
| Struct | An interface with type (the grammar name), start, end and the fields; nullable where an absent or incompatible recovered value becomes nil |
node[T] |
TNode<T'>, with start, end and value of the TypeScript type T' of T; StringNode, which extends TNode<string>, for a string node, and TNode<unknown> for node[_] |
Tuple (A, B, ...) |
readonly [A', B', ...] |
[]T |
A readonly array, or null; null differs from an empty array |
*T |
A nullable target |
int |
number | bigint, with signed 64-bit arithmetic and safe-number canonicalization |
string, bool |
string, boolean; an absent non-optional scalar field is "", false or 0 |
| Declared alias | The complete target type |
| Join of declared node types, declared by an alias or written in place | The member interfaces, RecoveredError and null; tuple and list node members remain Node |
Join with a member that is not a declared node type, _ |
TypedValue, following the conversion of any in Go |
Names collide the same way as in Go, with trailing underscores. Typed parsing by rule name is not available in TypeScript.
Recognition
With -recognize, Go gains Recognize(input string, opts ...ParseOption) error and
TypeScript gains recognize and recognizeWithOptions. They check that the whole
input matches the start rule without building a tree and return the errors that
Parse returns, including SyntaxErrors for recovered errors. Actions whose values
are not read by predicates are skipped; actions that predicates read still run and
can report runtime errors.
Incremental Documents (Go Only)
With -document, the Go package gains NewDocument(text string, opts ...ParseOption) (*Document, error) and a
Document with these members: Parse, Edit(start, end, replacement), Text,
Stats and, with -types, ParseAST.
Editreplaces a half-open range in the position unit chosen at construction. It rejects an invalid range, and a position inside a UTF-8 character inBytesmode, without changing the text.Text()returns the exact current input.- Construction options become the defaults of later calls. A call MAY set the depth, the context and the work limits; it MUST NOT change the unit.
ParseStatshas the fieldsEvaluatedandReused.- A parse returns what a parse of the whole current text returns. An interruption discards memo history so that a later call can retry.
- A
Documentis for one goroutine. A returned tree can share nodes with later parses;Node.Clonemakes a detached copy that preserves cycles and repeated references. With-types,ParseASTreturns read-only values that share storage with the document and with earlier results. Caller mutation can therefore change what later results contain, as withNodevalues.CloneAST(value)returns an independent copy that shares nothing with the document or other results, and preserves aliases within the copy. Every slice in a returned value has capacity equal to its length, soappendon a result cannot write into another result. - The returned
SyntaxErrorvalues and theirExpectedandMessagesslices are owned by the caller. Parsing, diagnostic copying and typed publication share one work budget. - It supports a subset of the engine's Incremental Documents: there
is no
EditBatch, noVersion, no snapshots and no change notifications.
Streaming (Go Only)
With -stream, the Go package gains ParseStream(r io.Reader, emit func(*Node) error, opts ...ParseOption) error,
Iterate with StreamIterator and ErrStreamClosed, and the options
WithDiagnosticCallback and WithDiagnosticLimit with TruncatedSyntaxErrors. They
follow the contract of Streaming, with these differences: a nil
emit parses the stream without delivering elements, and the diagnostic options are
accepted only by the stream entry points (Parse, ParseRule, ParseAST,
Recognize, NewDocument and Document methods reject them). Parse treats
#stream as an ordinary repetition.
With -types as well, the package gains StreamValue, ParseStreamAST and
IterateAST with StreamASTIterator. StreamValue is the Go type of one element
of the #stream repetition, not of the start rule. Each element is converted on
its own when it is committed, so the value stays valid after the callback returns.
The conversion counts toward the work limits and observes the context.
Generated TypeScript API
export function parse(input: string | Uint8Array, unit?: Unit): ParseResult;
export function parseRule(name: string, input: string | Uint8Array, unit?: Unit): ParseResult;
export function parseWithOptions(input: string | Uint8Array, options?: ParseOptions): ParseResult;
export function parseRuleWithOptions(name: string, input: string | Uint8Array, options?: ParseOptions): ParseResult;
export interface ParseOptions {
unit?: Unit;
maxDepth?: number;
signal?: CancellationSignal | null; // with -work-controls only
maxInputWork?: number; // with -work-controls only
maxSteps?: number; // with -work-controls only
}
export interface ParseResult { node: Node | null; error: Error | null }
// With -work-controls:
export interface CancellationSignal { readonly aborted: boolean; readonly reason?: unknown }
export type ParseInterruptionKind = "canceled" | "input-work-limit" | "step-work-limit";
export class ParseInterruptionError extends Error {
readonly kind: ParseInterruptionKind;
readonly limit: number;
readonly used: number;
readonly requested: number;
readonly cause: unknown;
}
export type Unit = 0 | 1;
export const CodePoints: Unit; // the default
export const Bytes: Unit;
export function marshal(n: Node | null): string;
parsedoes not throw for bad input: it returns{ node, error }with a node and no error; no node and an error; or, after#recover, a node andSyntaxErrors. It throws only for defects that are not parse errors.Nodehastype,rule,start,end,text,children,fields,field(name),isTerminal(),toString()andtoJSON(), with the meaning given in Nodes and Values. A field value is aNode, a number, a bigint, a string, a boolean ornull.SyntaxErrorhaspos,line,col,expected,messagesandreason(), and itsmessageis the text of the engine's error;SyntaxErrorshaserrors.- The
SyntaxErrorvalues ofSyntaxErrors, the errors that#recoverrecovered from, carry only the header line instack; theerrorof a parse that failed keeps the stack of the point where it was made. marshal(node)returns exactly the bytes thatencoding/jsonwrites for the engine's node.JSON.stringifyyields the same keys in the same order, except that integers beyond the safe range are rounded and, inBytesmode, an invalid input byte is written as the surrogate that represents it.marshalandtoStringwrite trees of any depth.JSON.stringifyrecurses and can exhaust the stack for deep trees.maxDepthMUST be a positive safe integer or zero; negative values, fractions, NaN, infinity and unsafe integers return a configuration error in the result before matching. Omitted or zero selects the generated default.- A parser generated without
-work-controlsemits no code that charges work and sets no control, so a parse charges nothing, and itsParseOptionshas nosignal,maxInputWorkormaxSteps; it does not exportCancellationSignal,ParseInterruptionKindorParseInterruptionError. A call that passes a signal or a nonzero limit anyway returns an error in the result, before matching, that says the parser was generated without work controls; no option is ignored. An absent or null signal and zero limits set no control and are accepted. A parser generated with-work-controlshas the controlled form only, and a call that sets no limit runs it unchanged. The rest of this list describes the controlled form. signalis a structural cancellation signal: any object withabortedand an optionalreason, such as anAbortSignal.maxInputWorkandmaxStepsMUST be finite nonnegative safe integers, and zero means unlimited; other values return a configuration error in the result. The units of the limits are those of Cancellation and Work Limits.- An interrupted parse returns
nodeasnullanderroras aParseInterruptionErrorwhosekindis"canceled","input-work-limit"or"step-work-limit". For a limit,limit,usedandrequestedgive the accounting; for cancellation,causeis thereasonof the signal. Recovery does not turn an interruption into a partial result, and the parser keeps no reference to the signal after the call returns. Typed conversion (parseAST) and recognition report interruptions the same way. - A synchronous parse does not yield to the event loop: a signal that changes only through timers or other tasks of the same thread is not observed during the call. A signal that is already aborted is observed at the first poll, and the work limits do not depend on the event loop.
- With
-recognize,recognizeandrecognizeWithOptionsreturnnullif the input matches and anErrorotherwise. With-observability, the batch entry points accept the tracing and profiling options of Observability. - A generated TypeScript module exports the same names for every grammar, so two parsers are imported under different namespaces. A parser keeps no state between calls.
Input and Positions in TypeScript
Positions count Unicode code points (the default) or UTF-8 bytes (Bytes), as in the
engine, and not UTF-16 code units. The parser decodes the input first.
- A string is parsed as its UTF-8 encoding would be. A lone surrogate becomes U+FFFD.
- A
Uint8Arrayis parsed as UTF-8 bytes; an invalid sequence decodes to U+FFFD, one per byte, as in Go. InBytesmode, positions count the original bytes. - In
Bytesmode, text keeps the bytes: each invalid byte b is represented by the lone surrogate U+DC00+b in node text and intext(...).marshalwrites such a byte as U+FFFD. - String operations in actions follow Go:
+concatenates, comparisons order by code point, andlencounts code points or UTF-8 bytes. Integers are signed 64-bit and wrap on overflow; division truncates toward zero and%takes the sign of the dividend. An integer is anumberwhile it is a safe integer and abigintbeyond.
TypeScript Errors and the Stack
| Error | error |
|---|---|
| The input does not match | SyntaxError |
#recover skipped input |
SyntaxErrors, and node is set |
| A runtime error in an action | Error with the message of the engine |
| More nested rule calls than the limit (default 100,000) | Error: nesting too deep: more than 100000 rule calls |
| The JavaScript stack is exhausted before the limit | Error: nesting too deep: the JavaScript stack overflowed at N rule calls |
The host stack of a JavaScript runtime is far smaller than the stack of a Go program, so a TypeScript parser can reject nesting that the engine accepts.
Validation and Errors
| Condition | Result |
|---|---|
| Invalid package name, unknown start rule, grammar errors | Error before output |
#missing in the grammar |
Generation fails |
-stream or -document with -lang ts |
Generation fails |
-g, -s, -pkg or a generation option with -runtime only |
The command fails |
-runtime embed without -module; -runtime import with neither -module nor -pkg |
The command fails |
An existing parser.go, api.go, types/types.go or runtime file in -o that pego did not generate |
The command fails before writing |
WithTrace, WithProfile, WithTraceFilter or WithTraceMaxDepth passed to a parser generated without -observability |
The call returns an error before parsing |
-pkg or -nodoc with -lang ts |
Generation fails |
-stream without a #stream repetition in the start rule |
Generation fails |
Negative -max-depth |
Generation fails |
Compatibility and Support
| Feature | Generated Go | Generated TypeScript |
|---|---|---|
| Rule expressions, captures, atomic and discarded expressions | Supported | Supported |
| Operators Rules, left recursion (direct and indirect) | Supported | Supported |
| Predicates, variables, lookahead, cut | Supported | Supported |
| Actions, types, built-in functions | Supported | Supported |
@error, #recover, SyntaxErrors |
Supported | Supported |
#missing |
Not supported | Not supported |
| Position units | WithUnit |
unit |
| Another start rule | ParseRule |
parseRule |
| Recognition | Recognize with -recognize |
recognize with -recognize |
| Typed values | ParseAST, ParseRuleAST with -types |
parseAST, parseASTWithOptions with -types |
| Streaming | ParseStream, Iterate with -stream |
None; #stream is an ordinary repetition |
| Incremental documents | A subset with -document |
None |
| Cancellation and work limits | WithContext, WithWorkLimits |
signal, maxInputWork, maxSteps; ParseInterruptionError; with -work-controls |
| Nested rule-call limit | WithMaxDepth, -max-depth |
maxDepth, -max-depth; the host stack also bounds nesting |
Choice of backend, saved grammar, Parser.Grammar() |
Not applicable | Not applicable |
| Tracing and profiling | With -observability |
With -observability (batch parsers) |
A generated parser carries the runtime of the generator that produced it (embed) or imports it
(import), and does not follow later changes of the grammar or of the generator.
Conformance
The repository tests every grammar in examples/ and the grammars of the engine
tests against the generated Go and TypeScript parsers, in both position units and
with strings and bytes as input, and require identical trees (as JSON) and errors.
TypeScript output is type-checked with tsc --strict and additional strict flags.