PEGO

Reading Trees

A successful parse returns a tree of *pego.Node. This guide shows how to read that tree from Go: values, positions, printing, JSON, reading a whole tree and typed values. Walking Trees visits and selects nodes without writing the recursion, and Output Schemas and Structural Queries describes and searches the output of a grammar. How a grammar shapes the tree is the subject of Shaping Trees; the exact node model is defined in Nodes and Values.

The examples use the grammar pairs.pego:

type Pair = { Key node[string]; Value node[string] }

@start
rule main = ws ps:pair+ $$ => $ps
rule pair Pair = k:key "=" v:value ";"? ws => Pair{Key: $k, Value: $v}
rule key = [a-zあ-ん]+ #atomic
rule value = [0-9]+ #atomic
rule ws = [ \t\n]*

Reading Values and Positions

A node is an opaque value that stands for what a rule produced, and node.Kind() tells which accessors apply:

Kind The node is Read it with
pego.KindString A string that the input matched, with a range Text()
pego.KindTuple, pego.KindList A tuple or a list, with a range Len(), Elem(i), Elems()
pego.KindError An error that recovery produced Text() (its message)
pego.KindStruct A struct that an action built Type(), Field(name), Fields()
pego.KindTupleValue, pego.KindListValue A tuple or a list that an action built, without a range Len(), Elem(i), Elems()
pego.KindValue Another value that an action built, such as an int or a bool Value()

Elem(i) returns nil for an optional element that did not match. node.Field(name) returns the value of a field of a struct, or nil if the field is absent or the node is nil, and ranging over node.Fields() yields each field name and value in the order the action wrote them. A field value is a *pego.Node, an int, a string, a bool or nil. Field and Fields also return the captures of a node, but only when the parser is compiled with source information (see Printing and JSON). node.IsTerminal() reports whether a node is a leaf: a string that the input matched, or an error.

The root of the grammar above is the list that $ps captured, and its elements are Pair structs:

for _, pair := range node.Elems() {
	key := pair.Field("Key").(*pego.Node)
	fmt.Printf("%s %q at %d-%d\n", pair.Type(), key.Text(), key.Start(), key.End())
}

first := node.Elem(0)
fmt.Println(first.Field("Value")) // "12"
for name, v := range first.Fields() {
	fmt.Println(name, v)
}
fmt.Println(first.Field("Nope") == nil) // true
Pair "abc" at 0-3
Pair "あい" at 8-10
"12"
Key "abc"
Value "12"
true

Because a field is an any, assert its type: .(*pego.Node) for nodes and captures, .(int) for integers.

Start() and End() return int. They delimit the input a node covers as a half-open range in the position unit of the parse. A rule that includes trailing white space covers it: the first Pair above ends at 8, after "; ". To get the matched text of any node, slice the input (a plain Go slice when you parse with pego.WithUnit(pego.Bytes)) or use text(...) in an action. Running Parsers explains how to choose a unit.

Printing and JSON

node.String(), which fmt.Println uses, renders the value of a node as an S-expression for tests and debugging, and pego parse -f sexpr prints it. A tuple is written in parentheses, a list in square brackets, a string in quotes, an absent optional as nil and a struct as Type{Field: value}; positions are not written. A node of a named node type (type Ident = node[string]) keeps its type name before its brackets, as Ident("x"), while a plain node[string] is just "x". A node also encodes with encoding/json:

out, err := json.Marshal(first)
if err != nil {
	return err
}
fmt.Println(string(out))
{"type":"Pair","start":0,"end":8,"fields":{"Key":{"start":0,"end":3,"value":"abc"},"Value":{"start":4,"end":6,"value":"12"}}}

A node of a string, a tuple or a list is {"start": S, "end": E, "value": V}, where V is a string, an array for a tuple or a list, or null for an optional that did not match. A struct is {"type": T, "start": S, "end": E, "fields": {...}}, with the range of the rule that built it, and an int or a bool that an action returned is a bare JSON number or boolean. The keys of fields are sorted, so the output is deterministic. This is also what pego parse prints by default. Nodes cannot be decoded back from JSON; the JSON is for inspection and for exchanging results with other programs.

Source information is opt-in. When the parser is compiled with pego.WithSourceInfo() (the -source-info flag of pego parse), each node also records the rule that produced it and the captures of its tuple, and Rule(), Field and Fields report them. The JSON gains the members "rule" and "captures", and the S-expression gains @rule suffixes and name: labels. The grammar kv.pego is rule main = k:key ":" v:num with key = [a-z]+ #atomic and num = [0-9]+ #atomic; for the input a:1 the S-expression is ("a" ":" "1") without source information and with it:

(k:"a"@key ":" v:"1"@num)@main

The forms are defined in Text Rendering and JSON Encoding.

Default values are also opt-in. A parser compiled with pego.WithDefaultValue() (the -default-value flag of pego parse) drops the value actions and the declared rule types before it compiles the grammar, so every rule returns the default node tree even when the grammar has actions. Predicates still run, and the rules whose values predicates read keep their actions. The exact rules are in Default Values.

Reading a Whole Tree

The program below parses a left-associative sum and prints every node with its range, reading struct fields by name:

// main.go
package main

import (
	"fmt"
	"log"
	"strings"

	"github.com/ornew/pego"
)

const src = `type Bin = {
    Left  Expr
    Op    node
    Right Expr
}
type Expr ~ node | Bin

@start
rule sum Expr = (
    first:num rest:(-ws op:op -ws n:num)*
) => foldl($first, $rest, (acc, i) => Bin{Left: acc, Op: i.$op, Right: i.$n})

rule num = [0-9]+ #atomic
rule op = "+" / "-"
rule ws = " "* `

func main() {
	p, err := pego.CompileSource(src, "sum")
	if err != nil {
		log.Fatal(err)
	}
	n, err := p.Parse("1 - 2 + 3")
	if err != nil {
		log.Fatal(err)
	}
	dump(n, 0)

	// Struct fields are read by name; a string node carries its text.
	right := n.Field("Right").(*pego.Node)
	fmt.Println("right operand:", right.Text(), right.Start(), right.End())
}

func dump(n *pego.Node, depth int) {
	indent := strings.Repeat("  ", depth)
	switch n.Kind() {
	case pego.KindStruct:
		fmt.Printf("%s%s [%d,%d)\n", indent, n.Type(), n.Start(), n.End())
		for name, v := range n.Fields() {
			if c, ok := v.(*pego.Node); ok && c != nil {
				fmt.Printf("%s  .%s\n", indent, name)
				dump(c, depth+2)
			}
		}
	case pego.KindString:
		fmt.Printf("%s%q [%d,%d)\n", indent, n.Text(), n.Start(), n.End())
	}
}
Bin [0,9)
  .Left
    Bin [0,5)
      .Left
        "1" [0,1)
      .Op
        "-" [2,3)
      .Right
        "2" [4,5)
  .Op
    "+" [6,7)
  .Right
    "3" [8,9)
right operand: 3 8 9

The inner Bin covers 1 - 2 and the outer one the whole input, even though the inner node was created inside the lambda. A tuple or list node would be visited with n.Elems(); an element is nil where an optional did not match.

Typed Values

Node is a generic representation. A generated Go parser built with -types also returns the result as values of Go types that mirror the types of the grammar, so a type switch replaces field lookups and type assertions. See Typed Values and the mapping defined in Generated Parsers.