PEGO

Lesson 9 of 10

Handling Big and Live Input

Two situations break the picture of "read the whole text, parse it, use the tree": the input is too big to hold, or the text is edited while it is being parsed. This lesson handles both with the configuration grammar.

What You Will Build

A stream parser that handles one entry at a time, and a program that reparses an edited document and shows how little it evaluates again.

Before You Start

Complete Building Typed Trees: you need config.pego and a Go module that requires github.com/ornew/pego.

Streaming

Mark a repetition at the top level of the start rule with #stream and each element is handed to your code and forgotten, so memory depends on the size of one element and not on the size of the file. Save as stream.pego:

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
}

@start
rule main = -ws entry* #stream $$

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]*

On the command line, -stream prints one line per element. Entries that matched before an error are printed, then the error is reported with positions in the whole input:

printf 'host = "localhost"\nport = 8080\ndebug = maybe\n' | pego parse -g stream.pego -stream -f sexpr
Entry{Key: Key("host"), Value: String("\"localhost\"")}
Entry{Key: Key("port"), Value: Number("8080")}
pego: 3:9: syntax error: expected "#", "\"", "false", "true", [ \t\r\n], [0-9]

Incremental Parsing

An editor changes a few characters and needs a new tree at once. A Document holds the text, applies edits and parses again, reusing the results that the edit did not affect. This program, which also reads the entries with ParseStream, parses example.conf-like text, changes 8080 to 9090 and reparses. Put stream.pego and config.pego next to it:

package main

import (
	_ "embed"
	"fmt"
	"log"
	"strings"

	"github.com/ornew/pego"
)

//go:embed stream.pego
var streamSource string

//go:embed config.pego
var configSource string

func main() {
	// Streaming: entries arrive one at a time and are forgotten.
	sp, err := pego.CompileSource(streamSource, "main")
	if err != nil {
		log.Fatal(err)
	}
	input := "host = \"localhost\"\nport = 8080\ndebug = true\n"
	err = sp.ParseStream(strings.NewReader(input), func(n *pego.Node) error {
		key := n.Field("Key").(*pego.Node)
		fmt.Printf("[%d,%d) %s\n", n.Start(), n.End(), key.Text())
		return nil
	})
	if err != nil {
		log.Fatal(err)
	}

	// Incremental parsing: edit a document and parse it again.
	cp, err := pego.CompileSource(configSource, "main")
	if err != nil {
		log.Fatal(err)
	}
	doc, err := cp.NewDocument(input)
	if err != nil {
		log.Fatal(err)
	}
	if _, err := doc.Parse(); err != nil {
		log.Fatal(err)
	}
	fmt.Println("first parse evaluated", doc.Stats().Evaluated, "rule bodies")

	at := strings.Index(input, "8080")
	if err := doc.Edit(at, at+4, "9090"); err != nil {
		log.Fatal(err)
	}
	cfg, err := doc.Parse()
	if err != nil {
		log.Fatal(err)
	}
	st := doc.Stats()
	fmt.Println("after the edit: evaluated", st.Evaluated, "reused", st.Reused)
	fmt.Println(cfg.Field("Entries").(*pego.Node).Elem(1).Field("Value").(*pego.Node).Text())
}
[0,19) host
[19,31) port
[31,44) debug
first parse evaluated 46 rule bodies
after the edit: evaluated 7 reused 7
9090

The first parse evaluates every rule body; after the edit only seven are evaluated again and seven results are reused. The result is always the one a fresh parse of the new text would give; reuse only changes the cost.

Next Steps

Working With the Tooling shows the tools that explain, trace and profile a grammar. The guides Streaming and Incremental Parsing describe what is emitted and when, writing grammars that reuse well, and limits. The cookbook has recipes for a file that does not fit in memory and reparsing on every keystroke.