Lesson 3 of 10
Building Typed Trees
The configuration grammar of the previous lesson produces a tree that mirrors the text. A program that uses the configuration wants a tree shaped like the data. This lesson declares types in the grammar, builds them with actions and reads them from Go.
What You Will Build
A config.pego grammar whose result is a Config with a list of Entry values, and a Go program that prints them.
Before You Start
Complete Parsing Structured Input: you need example.conf from it.
Declaring Types and Actions
A Config has a list of Entry values, each with a key and a typed value. In PEGO you declare these types in the
grammar and construct them with actions. Save as config.pego:
package config
type Key = node[string]
type Number = node[string]
type String = node[string]
type Bool = node[string]
type Value ~ Number | String | Bool
type Entry = {
Key Key
Value Value
}
type Config = {
Entries []Entry
}
@start
rule main Config = -ws es:entry* $$ => Config{Entries: $es.Value}
rule entry Entry = k:key -ws -"=" -ws v:value -ws => Entry{Key: $k, Value: $v}
rule value Value = number / string / bool
rule key Key = ([a-z_]+) #atomic
rule number Number = ([0-9]+) #atomic
rule string String = ("\"" [^"]* "\"") #atomic
rule bool Bool = ("true" / "false") #atomic
rule ws = (blank / comment)*
rule blank = [ \t\r\n]+
rule comment = "#" [^\n]*
pego parse -g config.pego -f sexpr < example.conf
Config{Entries: [Entry{Key: Key("host"), Value: String("\"localhost\"")} Entry{Key: Key("port"), Value: Number("8080")} Entry{Key: Key("debug"), Value: Bool("true")}]}
What is new:
package configis optional; it names the grammar's package and is used by the code generator (see Shipping a Parser). It must be the first item in the file.type Key = node[string]declares a named node type whose value is a string, for the leaves of the tree. A rule of that type matches the text with(...) #atomicand has no action: its node holds the text it matched. The text of aStringincludes its quotes, since it is the text the rule matched. The S-expression keeps the type name in front of the text:Key("host"), where a plainnode[string]prints just"host".type Value ~ Number | String | Boolis an alias of a join type: a value of any one of the members, much like a Go interface.~declares an alias, another name for a type;=defines a new struct type. Becausevalueis declared asValue, the compiler checks that each alternative is one of the members.type Entry = { ... }defines a struct type; fields are separated by newlines or semicolons. Field types can be lists ([]Entry), optional values (*T), or joins.rule name [Type] = expressionstates the optional type of a rule. You can omit it and PEGO infers the type from the body, but declaring the types of the rules that build your tree documents them and keeps mistakes close to where they happen.es:entry*captures the whole list of entries.$esis a node, so$es.Valueis its value, the list thatConfig{Entries: $es.Value}stores in theConfigstruct. Every node has the members.Start,.Endand.Value.
The compiler rejects ill-typed grammars with a position in the grammar file. In a copy of config.pego, swapping the
arguments in the entry action:
pego parse -g bad-type.pego -f sexpr -i 'a=1' # ... Entry{Key: $v, Value: $k}
pego: bad-type.pego:21:60: cannot use Number | String | Bool as Key in field Key of Entry
bad-type.pego:21:69: cannot use Key as Number | String | Bool in field Value of Entry
and misspelling a field name:
pego parse -g bad-field.pego -f sexpr -i 'a=1' # ... Entry{Key: $k, Valu: $v}
pego: bad-field.pego:21:69: Entry has no field Valu
Using the Tree From Go
The typed tree is what your program consumes. Put config.pego and an example.conf next to this main.go, which
embeds the grammar and prints the entries:
package main
import (
_ "embed"
"errors"
"fmt"
"log"
"os"
"github.com/ornew/pego"
)
//go:embed config.pego
var grammarSource string
func main() {
p, err := pego.CompileSource(grammarSource, "main")
if err != nil {
log.Fatal(err) // a mistake in the grammar
}
input, err := os.ReadFile(os.Args[1])
if err != nil {
log.Fatal(err)
}
cfg, err := p.Parse(string(input))
if err != nil {
var se *pego.SyntaxError
if errors.As(err, &se) {
fmt.Printf("line %d, column %d: %s\n", se.Line, se.Col, se.Message())
fmt.Println("expected:", se.Expected)
}
log.Fatal(err)
}
entries := cfg.Field("Entries").(*pego.Node)
for _, e := range entries.Elems() {
key := e.Field("Key").(*pego.Node)
value := e.Field("Value").(*pego.Node)
fmt.Printf("%-6s %-7s %s\n", key.Text(), value.Type(), value.Text())
}
}
go run . example.conf
host String "localhost"
port Number 8080
debug Bool true
node.Field(name) returns the value of a struct field as an any: a *pego.Node for node values, lists, tuples and
structs, or an int, string or bool. A list is a node whose elements Len(), Elem(i) and Elems() give.
A string node keeps the text it matched, which Text() returns. value.Type() tells you which member of the Value join
you got, so a switch on it is how you walk a join.
Next Steps
Parsing Expressions builds a calculator. The guides Declaring Types and Actions cover struct and join types, the built-in functions and positions, and Reading Trees covers reading a tree in Go.