Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Templates and captures

A backtick template describes one piece of input by looking like it. The characters between the backticks are the fixed text the input must have, and {...} marks the places where the interesting parts are.

read lines(`{x1:int},{y1:int} -> {x2:int},{y2:int}`)

That reads 0,9 -> 5,9 and produces { x1: 0, y1: 9, x2: 5, y2: 9 }, one per line. The template is the whole specification: no split, no trim, no index arithmetic, and the record’s fields are the names you wrote.

Two things are going on in it.

  • Literal text,, -> — must be matched by the input. Punctuation and words match exactly; a run of spaces has its own rules.
  • A capture{x1:int} — hands a stretch of input to a parser and keeps what it produces.

Named and anonymous captures

A capture is {name:parser} or just {parser}. Which one you write decides the shape of the result, and the rule is read off the template’s own parts.

templateresult
no capturesUnit
one anonymous capturethe captured value itself
two or more anonymous capturesa tuple, in order
named capturesan anonymous record with those field names
// The four shapes, read off the template's own parts: no capture is Unit, one
// anonymous capture is that value, two or more are a tuple, and named captures
// are an anonymous record.
out(parse("hello", `hello`))
out(parse("42", `{int}`))
out(parse("1,2", `{int},{int}`))
out(parse("1,2,x", `{int},{int},{word}`))
out(parse("x=1", `{name:word}={v:int}`))
Unit
42
(1, 2)
(1, 2, x)
{ name: x, v: 1 }

Anonymous captures are for when the position says everything — coordinate pairs, two-column tables. Named captures are for everything else, and they are what makes the rest of the program readable: s.x1 beats s.0 the moment there are more than two of them.

The tuple is an ordinary tuple, all the way down into the collection that holds it:

// A multi-capture template's value is an ordinary tuple, all the way into the
// collection that holds it: it renders and compares like one built by hand.
var pairs = read lines(`{int},{int}`)
var same = Vec()
same.push((1, 2))
same.push((3, 4))
out(pairs)
out(pairs == same)

Over 1,2\n3,4\n:

[(1, 2), (3, 4)]
true

The record is an anonymous record: its type is its field names and their types, and nothing had to be declared.

Named and anonymous captures may not be mixed in one template — the result would have to be a record and a tuple at once.

// Named and anonymous captures may not be mixed in one template: the shape
// would have to be a record and a tuple at once.
out(read lines(`{x:int},{int}`))
error[I020]: named and anonymous captures may not be mixed in one template

  template-mixed-captures.px:3:16
  3 | out(read lines(`{x:int},{int}`))
    |                ^^^^^^^^^^^^^^^ named and anonymous captures may not be mixed in one template

praxis: 1 error(s)

Two captures with the same name is I021, for the same reason a record cannot have two fields called x.

A capture body is a parser expression

The : in {name:parser} is followed by a whole parser expression, not just an atomic name. Constructor calls, string arguments, and templates of their own all go inside the braces.

// A capture body is a whole parser expression, not just an atomic name: a
// constructor call, a call with a string argument, a `}` inside that string,
// and a template of its own all sit inside `{...}`.
out(parse("Monkey 0: 79, 98", `Monkey {id:int}: {items:csv(int)}`))
out(parse("a-b-c", `{parts:sep("-", word)}`))
out(parse("}", `{c:one_of("}")}`))
out(parse("at 3,4", `at {p:`{x:int},{y:int}`}`))
{ id: 0, items: [79, 98] }
{ parts: [a, b, c] }
{ c: } }
{ p: { x: 3, y: 4 } }

The scanner finds the end of a capture by tracking depth rather than by looking for the first }, so a } inside a string, a , inside a call, and a nested backtick run all stay inside the capture where you wrote them. That is why line three works: one_of("}") is a legal body, and the } in its argument does not close anything.

A name is split off only at a : at depth zero, so {g:choice(A: word, B: int)} is a capture named g whose body is a choice — the colons inside the call are not candidates.

Whitespace around the name is trimmed: { n :int} names n. The name itself must be an identifier, by the same rule that decides what a Praxis binding may be called, so {2x:int} is I011. A body naming nothing the compiler recognizes is I012{value:intr} reports unknown parser intr and suggests int — and a body calling a constructor that does not exist is I013. There is no default: a capture whose kind is unrecognized fails the compile rather than quietly becoming an int.

A capture is bounded by what follows it

A capture does not take everything it could. It is handed a region that ends where the run of literal parts after it can first match, and it must fill that region. “Earliest” is what makes text non-greedy, and it applies to every capture, not only the text ones.

The bound is the earliest position at which the whole run of literal parts up to the next capture can match. A run that can match the empty string — nothing at all, or a \s* — constrains nothing, and then the capture takes the rest of its region.

// Every capture is bounded by the run of literal parts that follows it, and
// the bound is the earliest place that whole run can match. A run that can
// match nothing — `\s*`, or nothing at all — is no bound.
out(parse("x y bar", `{a:text} bar`))
out(parse("x y bar", `{a:text}\s+bar`))
out(parse("x  bar", `{a:text}\s*bar`))
out(parse("aaa", `{a:text}a{b:rest}`))
{ a: x y }
{ a: x y }
{ a: x }
{ a: , b: aa }

Read those four in order:

  1. {a:text} bar stops a at the space before bar, not at the first space. The bound is where the run matches, and the run is a space and then bar.
  2. {a:text}\s+bar is the explicit spelling of the same policy and bounds a in the same place. What decides the bound is that the run must match something, not which way it was written.
  3. {a:text}\s*bar bounds a at x, because the earliest place the whole run can start is right after it: \s* eats the two spaces and bar lands. The spaces belong to the policy, not to a.
  4. {a:text}a{b:rest} stops at the first a, which is position zero, so a is empty. Non-greedy means non-greedy.

The last capture in a template has nothing after it, so nothing bounds it: it takes the rest of its region and stops where its own parser stops. That is why a root-level template does not fault on the file’s trailing newline.

A capture is offered the bytes at the cursor including its own leading whitespace — whether to skip that is the child’s decision, not the template’s. What the leading run does not do is bound the capture; see whitespace.

A template ends at the line it opens on

A raw newline may not appear inside a template. \n is how a template matches a line ending, and it is the only way — which is also how a template reaches a second line.

// A template ends at the line it opens on, so `\n` is the only way it matches
// a line ending — and the only way one reaches a second line. The escape
// matches CRLF as well as LF.
out(parse("1\n2\n", `{a:int}\n{b:int}`))
out(parse("1\r\n2\n", `{a:int}\n{b:int}`))
{ a: 1, b: 2 }
{ a: 1, b: 2 }

The escape matches CRLF as well as LF, which is the other half of the reason for the rule. A raw newline is whitespace but not a space, so it would match none of the whitespace policies and fall through to literal text — an LF-only match, silently hostile to a CRLF file. \n costs one character more and is right on both.

It also bounds the report when a template is left open. The run cannot outlive its line, so an unterminated backtick names one line instead of the rest of the file:

// The same rule bounds the report when a template is left open: the run ends
// at the line's end, so `T002` names one line and there is no cascade.
var v = read `{int`
out(v)
error[T002]: unterminated backtick template

  template-unterminated.px:3:14
  3 | var v = read `{int`
    |              ^^^^^^ unterminated backtick template

praxis: 1 error(s)

One error, not a cascade. The } closing the enclosing block is no longer swallowed by the token, so the parser and the type checker never see the damage.

A template is a parser expression everywhere or nowhere

Backticks mean “parser expression” and nothing else. A template outside read and parse has nothing to read from, so it is a diagnostic rather than a Text that happens to contain braces.

// A backtick template is a parser expression everywhere or nowhere.
// Outside `read` and `parse` it has nothing to read from, so it is an error
// rather than a `Text` that happens to contain braces.
var t = `n = {int}`
out(t)
error[Y023]: a backtick template is a parser expression; write `read` before it, or pass it to `parse(text, ...)`

  template-value-position.px:4:9
  4 | var t = `n = {int}`
    |         ^^^^^^^^^^^ a backtick template is a parser expression; write `read` before it, or pass it to `parse(text, ...)`

praxis: 1 error(s)

The alternative is a program that asked to parse an integer printing the word {int}, and type-checking while it did. The message names the fix, because the fix is always the same word.

A backtick is never a way to build text. "..." is the text literal, and the braces in it are interpolation"n = {n}" renders n. The two mechanisms share nothing but the character: a template’s {name:parser} names a capture in the input-parser DSL, while a literal’s {expr} is an ordinary Praxis expression rendered into text.

Escapes

Inside a template, \` is a backtick and \\ is a backslash. \n, \t, \x20, \s* and \s+ are whitespace policies and are covered in Whitespace, lines and positions. Anything else after a backslash is an error naming exactly the sequence you wrote.

A double quote in literal text is a double quote: a string literal is only a thing inside a capture body, so `He said "hi" {x:int}` is an ordinary template.

// Backticks and backslashes take ordinary escapes; a quote inside literal text
// is just a quote, because a string literal is only a thing inside a capture.
out(parse("a`b 3", `a\`b {x:int}`))
out(parse("a\\b 4", `a\\b {x:int}`))
out(parse("He said \"hi\" 5", `He said "hi" {x:int}`))
{ x: 3 }
{ x: 4 }
{ x: 5 }

Where templates go

A template is a parser expression, so it goes anywhere one does: as the whole operand of read, as the child of lines, sections, ws, sep or scan, as a block item, as a choice case, and — as above — inside another template’s capture.

The one thing to know about a template inside a block is that it is offered its own line plus one more for each \n it writes, where every other kind of item is offered the rest of the region. That rule is block’s, and it is why a template with a trailing capture does not swallow the item after it.

For what each of these produces as a type, see How a parser gets its type.