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

Inspecting the input parser

The most common way a puzzle program fails is not a bug in the program. It is that the input did not look the way you thought it did — a semicolon where a comma was promised, a stray letter in a column of digits, a blank line that is not blank. Praxis turns that into a fault of its own kind, input parse mismatch, and the debugger has two commands for it: input, which shows the input around the byte where the parser stopped, and parser, which shows what the parser wanted there.

Both are read-only and neither needs a frame selected; they read a record the runtime keeps of the parse, not the stack.

The fault carries the offset

Here is a program declaring a format that one line of its input does not have.

// Every line of the input is meant to be `x,y`. One of them is not.
var points = read lines(`{x:int},{y:int}`)
out(points.len())
12,7
5,3
9;1
4,4

The fault line already tells you most of it, before any command is typed:

error: program faulted: input parse mismatch
       at input offset 10..11: expected literal ","
       actual: 12,7⏎5,3⏎9;1⏎4,4⏎

Three facts, and they are the three you need. Offset 10..11 is where in the input the parser stopped, in bytes from the start — byte 10 is the ; on the third line. expected literal "," is what the template wanted at that point. And the actual line is a preview of the input around the offset, with newlines drawn as so the whole thing stays on one line.

The two commands print the same information one piece at a time, which is what you want once you have scrolled past the banner:

Praxis crash> input
input at offset 10..11:
  12,7⏎5,3⏎9;1⏎4,4⏎
Praxis crash> parser
expected: literal ","
parser expression: <unknown parser>
Praxis crash> bt
#0   <entry>
  (frame 0 selected)
Praxis crash> quit

parser expression: <unknown parser> is what that command prints for every parse failure. The input-parser interpreter does not carry a parser expression’s source span into the failure, so there is no source text to name there. What parser actually gives you is the expected description, which is the same one the fault line printed. Treat it as a shorthand, not as a second source of information.

Walking it back to the byte

An offset is not a line and a column, and for a real puzzle input you will want one. The preview helps you recognise the neighbourhood; the offset is what locates it exactly.

// One integer per line, twenty of them.
var depths = read lines(int)
out(depths.len())

The input is twenty three-digit numbers, one of which contains a capital O instead of a zero. Nothing about the fault says which:

Praxis crash> input
input at offset 49..51:
  06⏎107⏎108⏎109⏎110⏎111⏎1O2⏎113⏎114⏎115⏎116⏎117⏎1
Praxis crash> parser
expected: the rest of the line
parser expression: <unknown parser>
Praxis crash> quit

Read that carefully, because both halves of it are informative.

The preview is a window, not the input. It is at most 24 bytes on each side of the failure offset, clipped at the ends of the buffer, which is why it begins mid-number at 06. Do not count characters from the left of the preview to find your line; the window’s own start is arbitrary.

The span starts where the parser gave up, and that start is the fact to rely on. Here it is 49..51, two bytes wide, with expected: the rest of the line — the shape of a lines(...) element that matched something and then found the line was not over. int read the 1, wanted the line to end, and found O2 still sitting there, so the span covers what was left over.

The width means something different in each case, so do not read it as “how much matched”. It is zero when the parser matched nothing at all; it is the width of the expected text when a template literal did not match (expected literal "," at 10..11 above is one byte because , is one byte); it is the width of the unconsumed remainder when a line or region was not used up. Only the start is uniformly “the byte the parser was looking at when it stopped”.

To turn the offset into a line, count the newlines before it:

$ head -c 49 docs/book/examples/debugger-b/parse-depths.in | wc -l
      12
$ sed -n 13p docs/book/examples/debugger-b/parse-depths.in
1O2

Twelve newlines precede byte 49, so byte 49 is on line 13, and line 13 is 1O2 — the O is a letter. That is the whole bug, and the parser was right.

The raw input is also in the frame, if you would rather look at it than at a window. The temp that holds the input buffer is listed by locals, and its value is the entire text:

  temps:
    <tmp#1> = 100
101
102
103
104
105
106
107
108
109
110
111
1O2
113
114
115
116
117
118
119

For a real puzzle input that is thousands of lines and you will not want it. For a fixture you are debugging by hand, it is often quicker than switching windows.

The deepest failure is the one reported

A structural parser fails at several levels at once. sections(lines(csv(int))) can fail because a section did not end, because a line did not split, or because a token was not an integer — and the outer failures are always less informative than the inner one. The runtime keeps the failure whose input offset is furthest into the buffer, on the argument that the point at which parsing genuinely broke is the deepest point it reached.

// Blank-line-separated sections of comma-separated integers.
var groups = read sections(lines(csv(int)))
out(groups.len())
1,2,3
4,5,6

7,8,x
Praxis crash> input
input at offset 17..17:
  1,2,3⏎4,5,6⏎⏎7,8,x⏎
Praxis crash> parser
expected: int
parser expression: <unknown parser>
Praxis crash> quit

expected: int at offset 17 — the x, the third field of the second section’s only line — and not “expected a section” at offset 0. The span is zero-width here because int found no digits at all at that byte, which is the clearest kind of parse failure there is: the innermost parser, at the exact byte, saying what it wanted.

Locals during a parse failure

A frame that faulted inside read has a distinctive shape. The binding the read was going to fill is <uninit>, because the parse never produced a value to assign:

  locals:
    points: Vec[{ x: Int, y: Int }] = <uninit>

The type is still there, and it is worth reading. Vec[{ x: Int, y: Int }] is what the template `{x:int},{y:int}` derives — a vector of anonymous two-field records — and if that is not the type you expected, the parser you wrote is not the parser you meant, whatever the input says. See how a parser gets its type.

When there is nothing to inspect

The two commands are only meaningful for a parse failure. Every other fault kind gets a note saying so, including in a program that reads its input successfully and then fails at something else:

// The input parses. The fault comes later, from the program.
var depths = read lines(int)
out(depths[99])
Praxis crash> input
(no input context — not a parse failure)
Praxis crash> parser
(no parser context — not a parse failure)
Praxis crash> quit

That is a useful negative result and worth typing early: it tells you the input matched the parser, so whatever went wrong is in the program. The rest of the debugger — bt, locals, p — is where you go next.

What the fault does not tell you

Three limits, stated plainly so you do not go looking.

There is no partial value. A parse either produces its whole result or produces nothing, so the binding is <uninit> and there is no half-built structure behind it. You cannot ask for the two lines that did parse.

There is no caret. The compiler underlines a span in your source; input prints an offset and a preview, and you locate the column yourself.

And there is no parser span, as above — parser expression: <unknown parser>, every time. For a program with one read this costs nothing. For a program with several, the expected description is what tells them apart.