Lesson 6 of 10
Parsing Context-Sensitive Input
A PEG matches what the grammar says, regardless of what came before. Some languages need memory: a closing tag must repeat the name of the opening tag, and a name may be limited in length. This lesson adds checks and memory to a grammar with predicates and variables.
What You Will Build
A grammar for nested tags such as <a><b>x</b></a> that rejects a mismatched closing tag with a clear message.
Before You Start
Complete Reporting Errors. This lesson uses @error messages.
Checking a Condition
A predicate is a Go-like expression in braces. It consumes no input, and the parse continues only if it is true.
Captured values are read with $name, and text($name) gives the matched text. Here a name may have at most eight
letters. Save as names.pego:
@start
rule main = name ("," name)* $$
rule name = n:[a-z]+ #atomic {len(text($n)) <= 8} @error(message="a name has at most 8 letters")
pego parse -g names.pego -f sexpr -i 'alpha,beta'
pego parse -g names.pego -f sexpr -i 'alpha,beta,verylongname'
(("alpha") [("," ("beta"))])
pego: 1:24: a name has at most 8 letters
A predicate has no value, so each name is a tuple with a single element, the text of the name. The predicate is evaluated after [a-z]+ matched, so the error is at the position after the name. A predicate that
fails records no expected item of its own, which is why the grammar attaches @error to it; without the label the
message would be a bare syntax error.
Remembering a Value
A variable is defined in a predicate with := and can be read by every rule that is called afterwards. The tags
grammar defines tag after the opening tag and compares it in the rule that matches the closing tag. Each element
defines its own tag, and the definition disappears when the rule returns, so nesting works without further code. Save
as tags.pego:
type Name = node[string]
type Chars = node[string]
type Element = { Name Name; Children []Content }
type Content ~ Element | Chars
@start
rule main Element = e:element $$ => $e
// The open tag defines `tag`; every rule called after that can read it.
var tag string
rule element Element = "<" n:name ">" {tag := text($n)} cs:content* close_tag => Element{Name: $n, Children: $cs.Value}
rule content Content = element / chars
rule chars Chars = ([^<]+) #atomic
// The close tag is parsed by another rule: it finds the name in `tag`.
rule close_tag = "</" n:name {text($n) == tag} @error(message="mismatched end tag") ">"
rule name Name = ([a-z]+) #atomic
pego parse -g tags.pego -f sexpr -i '<a><b>x</b><c>y</c></a>'
pego parse -g tags.pego -f sexpr -i '<a><b>x</a></b>'
pego parse -g tags.pego -f sexpr -i '<a><a>x</a>'
Element{Children: [Element{Children: [Chars("x")], Name: Name("b")} Element{Children: [Chars("y")], Name: Name("c")}], Name: Name("a")}
pego: 1:11: mismatched end tag
pego: 1:12: syntax error: expected "<", "</", [^<]
The mismatch is reported where the predicate ran, after the offending name. An unclosed element fails the ordinary way,
because content* stops at the end of the input and close_tag cannot match.
Troubleshooting
- A predicate that is not a comparison, such as
{len($s)}, succeeds for every value exceptfalse. Compare explicitly. { $a == $b }on two captures compares nodes, not text. Comparetext($a) == text($b).- A misspelled variable name makes the predicate fail silently, so check the names first when a predicate never succeeds.
Next Steps
Testing and Linting a Grammar turns the checks you made by hand into a suite. The guide Context-Sensitive Parsing covers variables, lookahead and attributes, with recipes for indentation blocks, here-documents and long brackets, and the cookbook has a recipe for indentation-based input.