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

Appendix A: Complete programs

Seven programs, in rough order of size. Every one is a real file under docs/book/examples/appendix/, re-run by docs/book/examples/verify.sh against the input and the output printed here. So this is not a sketch of what a Praxis program might look like; it is what target/release/praxis does with that text.

Run any of them:

$ praxis run docs/book/examples/appendix/depths.px --input docs/book/examples/appendix/depths.in

The first four are puzzle-sized. The fifth is a whole puzzle in thirty lines. The sixth is a breadth-first search written twice, and the seventh is a bytecode interpreter that retires 1.15 million instructions on the input shown.

depths.px — one integer per line

The floor of the language. A whole day’s input is lines(int), and the type Vec[Int] comes from the parser expression rather than from an annotation. The three lines of output are len, sum, and a pipeline that zips the vector with itself offset by one and counts the pairs that increase.

This program has no fn main, and there is none to write: a file is a program and its top-level statements run in order (A file is a program).

// The smallest complete program that reads input: one integer per line.
//
// `read lines(int)` is the whole parser. Its type — `Vec[Int]` — is derived
// from the parser expression, so nothing is annotated. What follows is three
// pipelines over that vector.

var depths = read lines(int)

out(depths.len())
out(depths.sum())
// A window over consecutive pairs: how many readings are larger than the one
// before. `zip` pairs the vector with itself offset by one, and `count` takes
// the predicate.
out(depths.zip(depths.skip(1)).count(|p| p.1 > p.0))

depths.in:

199
200
208
210
200
207
240
269
260
263

Output:

10
2256
7

Explained in The read expression, Atomic parsers and Pipelines.

calories.px — blank-line-separated sections

The other structural shape every puzzle set contains: groups separated by blank lines. sections(lines(int)) nests two structural parsers, and the nesting is the type — Vec[Vec[Int]], one inner vector per group.

sorted_by_key(|t| 0 - t) is how this program writes a descending sort. There is a second spelling, sorted().reversed(), and the example keeps the first: negating the key is one pass over the group where sorting and then reversing is two.

// A sections day: blank-line-separated groups of integers.
//
// `sections(lines(int))` nests two structural parsers, so the result is
// `Vec[Vec[Int]]` — one inner vector per group. Nothing in the program says
// that type; it is read off the parser expression.

var groups = read sections(lines(int))

out(groups.len())
// The largest group total, and the sum of the three largest.
var totals = groups.map(|g| g.sum())
out(totals.max())
out(totals.sorted_by_key(|t| 0 - t).take(3).sum())

calories.in:

1000
2000
3000

4000

5000
6000

7000
8000
9000

10000

Output:

5
24000
45000

Explained in Structural parsers and How a parser gets its type.

toboggan.px — a character grid

A grid day. read grid(char) yields a Grid[Char], indexed map[x, y] — a subscript taking two arguments, which is why a subscript’s index list is an argument list rather than a single expression.

Two details here are worth naming. '#' is how a program writes a character it chose, and "#"[0] — subscripting a one-character Text — still names the same Char, which is what a program reaches for when the character came out of text it did not write down. And trees_on_slope takes the grid as a parameter even though map is in scope at the file level — a fn does not capture the bindings around it, and reading one from inside a function is N007 with a message telling you to pass it in (and, when the function is recursive, saying why a closure is not an option).

// A grid day: count the trees hit by descending a slope of (right 3, down 1).
//
// `read grid(char)` yields a `Grid[Char]`, indexed `map[x, y]` — a subscript
// with two arguments, which is why a subscript's index list is an argument list
// and not a single expression. The map repeats horizontally forever, so the
// column wraps with `%`.

var map = read grid(char)

fn trees_on_slope(map, right, down) {
    var x = 0
    var y = 0
    var hits = 0
    while y < map.height() {
        if map[x % map.width(), y] == '#' {
            hits = hits + 1
        }
        x = x + right
        y = y + down
    }
    hits
}

out(map.width())
out(map.height())
out(trees_on_slope(map, 3, 1))
// Part two multiplies five slopes together.
var product = 1
for slope in [(1, 1), (3, 1), (5, 1), (7, 1), (1, 2)] {
    product = product * trees_on_slope(map, slope.0, slope.1)
}
out(product)

toboggan.in:

..##.......
#...#...#..
.#....#..#.
..#.#...#.#
.#...##..#.
..#.##.....
.#.#.#....#
.#........#
#.##...#...
#...##....#
.#..#...#.#

Output:

11
11
7
336

Explained in Grids and graphs, Text and Char and Functions and closures.

pipeline.px — records from a template, then closures

A named-capture template turns each line into a record: {name:word}: {score:int} produces {name: Text, score: Int}, and the whole read is a vector of those. Field access is e.name, and the record type was never declared.

above is a function that returns a closure. cut is captured and the closure outlives the call that built it — which is the difference between a closure and a fn, and the reason trees_on_slope above had to take its grid as a parameter.

// A closure pipeline over records read from a template.
//
// The template `{name:word}: {score:int}` names its captures, so each line
// parses into a record `{name: Text, score: Int}` and the whole read is a
// `Vec[{name: Text, score: Int}]`. Everything after that is pipeline
// combinators and closures — including one closure returned from a function,
// which captures the parameter it was built with.

var entries = read lines(`{name:word}: {score:int}`)

// A function that returns a closure. `cut` is captured by value; the closure
// outlives the call that made it.
fn above(cut) {
    |e| e.score > cut
}

var passing = above(50)

out(entries.len())
out(entries.filter(passing).map(|e| e.name))
out(entries.map(|e| e.score).fold(0, |a, s| a + s))
out(entries.sorted_by_key(|e| 0 - e.score).take(2).map(|e| e.name))
// `frequencies` counts, and a `Counter` reads absent keys as zero.
var initials = entries.map(|e| e.name[0]).frequencies()
out(initials['a'])
out(initials['z'])

pipeline.in:

ada: 91
alan: 47
grace: 88
alonzo: 63
edsger: 12

Output:

5
[ada, grace, alonzo]
301
[ada, grace]
3
0

The last two lines are the Counter rule: a key that was counted reads its count, and a key that was never inserted reads 0 instead of faulting.

Explained in Templates and captures, Records without names, Functions and closures and Collections.

segments.px — a whole puzzle in thirty lines

What a finished puzzle solution looks like at full size, top-level statements and all: the template read, a function over the records it produced, Counter[(Int, Int)]() with an explicit type argument, counts[point] += 1 storing through a subscript, 0..=distance, a trailing comma in the max(…) call, and two out calls at file scope.

The one type argument is not forced by anything: Counter() on its own infers (Int, Int) from counts[point] += 1 and prints the same two answers. It is written out because this is the only shape a type argument has — a compiler-owned constructor name, a bracket list, and then the call. Nothing else in the program is annotated.

// segments — the shape of a whole puzzle in thirty lines: a template read, a
// function over the records it produced, a `Counter` keyed by a tuple, a
// compound assignment through a subscript, an inclusive range, and two calls
// printing the two parts. Nothing in it is annotated.
//
// Input: line segments, `x1,y1 -> x2,y2`. Output: the number of points covered
// by two or more segments, first ignoring diagonals and then including them.
var segments = read lines(`{x1:int},{y1:int} -> {x2:int},{y2:int}`)

fn overlaps(segments, diagonals) {
    var counts = Counter[(Int, Int)]()

    for segment in segments {
        var dx = sign(segment.x2 - segment.x1)
        var dy = sign(segment.y2 - segment.y1)

        if !diagonals && dx != 0 && dy != 0 {
            continue
        }

        var distance = max(
            abs(segment.x2 - segment.x1),
            abs(segment.y2 - segment.y1),
        )

        for step in 0..=distance {
            var point = (
                segment.x1 + dx * step,
                segment.y1 + dy * step,
            )
            counts[point] += 1
        }
    }

    counts.values().count(|n| n >= 2)
}

out(overlaps(segments, false))
out(overlaps(segments, true))

segments.in:

0,9 -> 5,9
8,0 -> 0,8
9,4 -> 3,4
2,2 -> 2,1
7,0 -> 7,4
6,4 -> 2,0
0,9 -> 2,9
3,4 -> 1,4
0,0 -> 8,8
5,5 -> 8,2

Output:

5
12

!diagonals && dx != 0 && dy != 0 leans on the precedence table: ! binds tighter than every infix operator, && binds looser than !=, and && is left-associative — so it reads as ((!diagonals) && (dx != 0)) && (dy != 0). The whole table is in Appendix B.

Explained in Templates and captures, Collections and Bindings and shadowing.

maze.px — breadth-first search, twice

The same search written both ways. The first is the loop every puzzle starts with: a Deque as the FIFO frontier, a Set of visited cells, a Map of distances. The second is the prelude’s bfs_distance, which takes a start state, a closure answering a state’s neighbours, and a closure saying whether a state is the goal — the graph is never built.

A cell is a (Int, Int) tuple. That is what lets it be a Set member and a Map key: a key has to be hashable and unable to change after it is stored, and a tuple of scalars is both. A Vec is not: a Vec[Int] key is Y014 — “a value of type Vec[Int] can change after it is stored, so it cannot be used as a key”. Hashable is not orderable: a tuple has no < — though the collections above still walk and print their tuple keys element-wise, because a container needs a reproducible order whatever the source language permits.

bfs_distance answers Option[Int], so the third line of output is Some(22) and not 22: a goal that cannot be reached has no distance.

// Breadth-first search over a maze read as a character grid.
//
// Two ways to write the same search. The first is the loop every puzzle starts
// with: a `Deque` as the FIFO frontier, a `Set` of visited cells, a `Map` of
// distances. The second is the prelude's `bfs_distance`, which takes the start,
// a closure answering the neighbours of a state, and a closure saying whether a
// state is the goal — the graph is never materialized.
//
// A cell is a `(Int, Int)` tuple, which is what lets it be a `Set` member and a
// `Map` key: tuples are values compared by their elements.

var maze = read grid(char)

fn open_cell(maze, p) {
    p.0 >= 0 && p.1 >= 0 && p.0 < maze.width() && p.1 < maze.height()
        && maze[p.0, p.1] != '#'
}

fn neighbours(maze, p) {
    var found = Vec()
    for step in [(1, 0), (-1, 0), (0, 1), (0, -1)] {
        var q = (p.0 + step.0, p.1 + step.1)
        if open_cell(maze, q) { found.push(q) }
    }
    found
}

var start = (0, 0)
var goal = (maze.width() - 1, maze.height() - 1)

// The explicit loop.
var frontier = Deque()
frontier.push_back(start)
var seen = Set()
seen.insert(start)
var dist = Map()
dist[start] = 0
var answer = -1
while frontier.len() > 0 {
    var here = frontier.pop_front()
    if here == goal {
        answer = dist[here]
        break
    }
    for next in neighbours(maze, here) {
        if !seen.contains(next) {
            seen.insert(next)
            dist[next] = dist[here] + 1
            frontier.push_back(next)
        }
    }
}
out(answer)
out(seen.len())

// The same answer from the prelude helper. It answers an `Option[Int]`,
// because a goal that is not reachable has no distance.
out(bfs_distance(start, |p| neighbours(maze, p), |p| p == goal))

maze.in:

.....#....
.###.#.##.
.#...#..#.
.#.#####.#
.#.......#
.#.#####.#
...#...#..
.###.#.##.
.....#....
.#####....

Output:

22
51
Some(22)

Explained in Grids and graphs, Collections and Enums and Option.

vm.px — a stack bytecode interpreter

The one program here that is not puzzle-sized. Ten opcodes as an enum with payloads, one match per executed instruction, an operand stack in a Deque, four registers in plain bindings, and a hand-assembled program in a Vec. On the input below it retires 1,150,005 instructions and finishes in about thirty milliseconds, compilation included.

It is copied from benchmarks/praxis/vm.px, where it is the dispatch benchmark — the closest thing in the benchmark set to a “simulate this machine” puzzle part.

Three rules are load-bearing. The match over Op is checked for exhaustiveness, so an opcode added to the enum and forgotten in the loop is a compile error rather than a runtime surprise. Push(k) in a pattern binds the payload. And prog[pc] is a bounds-checked subscript: a jump target past the end of the program faults with index out of bounds and enters the crash debugger, rather than reading whatever is there.

// vm — a stack bytecode interpreter.
//
// Ten opcodes as an `enum` with payloads, one `match` per executed instruction,
// an operand stack in a `Deque`, and four registers held in plain bindings.
// This is the "simulate this machine" half of a puzzle at full size: the loop
// below retires 1.15 million instructions on the input in `vm.in`, and the
// exhaustiveness check on the `match` is what says no opcode was forgotten.
//
// It is copied from `benchmarks/praxis/vm.px`, where it is the dispatch
// benchmark.
//
// The interpreted program computes a rolling modular hash over `0..limit`; the
// interpreter reports its result and the number of instructions it retired.
//
// Input: the interpreted loop's iteration count, as one integer on stdin.
// Output: the interpreted program's result, then the instruction count.

enum Op {
    Push(Int)
    Load(Int)
    Store(Int)
    Add
    Mul
    Mod
    Lt
    JmpZ(Int)
    Jmp(Int)
    Halt
}

var limit = read int

// The program, hand-assembled. Register 0 is the loop counter, register 1 the
// accumulator; the loop head is instruction 4 and the exit target is 27.
var prog = Vec()
prog.push(Push(0))      //  0
prog.push(Store(0))     //  1   i = 0
prog.push(Push(1))      //  2
prog.push(Store(1))     //  3   acc = 1
prog.push(Load(1))      //  4   <- loop head
prog.push(Push(31))     //  5
prog.push(Mul)          //  6   acc * 31
prog.push(Load(0))      //  7
prog.push(Push(7))      //  8
prog.push(Mul)          //  9   i * 7
prog.push(Push(13))     // 10
prog.push(Add)          // 11   i * 7 + 13
prog.push(Push(1000003)) // 12
prog.push(Mod)          // 13   (i * 7 + 13) % 1000003
prog.push(Add)          // 14   acc * 31 + that
prog.push(Push(1000003)) // 15
prog.push(Mod)          // 16
prog.push(Store(1))     // 17   acc = ...
prog.push(Load(0))      // 18
prog.push(Push(1))      // 19
prog.push(Add)          // 20
prog.push(Store(0))     // 21   i = i + 1
prog.push(Load(0))      // 22
prog.push(Push(limit))  // 23
prog.push(Lt)           // 24   i < limit
prog.push(JmpZ(27))     // 25
prog.push(Jmp(4))       // 26
prog.push(Load(1))      // 27   <- exit
prog.push(Halt)         // 28

var stack = Deque()
var pc = 0
var r0 = 0
var r1 = 0
var r2 = 0
var r3 = 0
var steps = 0
var running = true

while running {
    var op = prog[pc]
    pc = pc + 1
    steps = steps + 1
    match op {
        Push(k) => { stack.push_back(k) }
        Load(k) => {
            if k == 0 { stack.push_back(r0) }
            else if k == 1 { stack.push_back(r1) }
            else if k == 2 { stack.push_back(r2) }
            else { stack.push_back(r3) }
        }
        Store(k) => {
            var v = stack.pop_back()
            if k == 0 { r0 = v }
            else if k == 1 { r1 = v }
            else if k == 2 { r2 = v }
            else { r3 = v }
        }
        Add => {
            var b = stack.pop_back()
            var a = stack.pop_back()
            stack.push_back(a + b)
        }
        Mul => {
            var b = stack.pop_back()
            var a = stack.pop_back()
            stack.push_back(a * b)
        }
        Mod => {
            var b = stack.pop_back()
            var a = stack.pop_back()
            stack.push_back(a % b)
        }
        Lt => {
            var b = stack.pop_back()
            var a = stack.pop_back()
            if a < b { stack.push_back(1) } else { stack.push_back(0) }
        }
        JmpZ(t) => {
            var v = stack.pop_back()
            if v == 0 { pc = t }
        }
        Jmp(t) => { pc = t }
        Halt => { running = false }
    }
}

out(stack.pop_back())
out(steps)

vm.in:

50000

Output:

990539
1150005

The enum’s variants are separated by line breaks rather than commas. Either works: a comma or a line break separates the members of a struct or enum body, and a trailing comma closes the list either way — the same rule that separates statements and match arms.

Explained in Enums and Option, Pattern matching and The fault model.

Where the rest of the corpus lives

These seven are a selection. The repository carries larger sets, run by the test suite rather than by this book:

directorywhat is in it
tests/aoc-corpus/31 fixtures, one per input shape and per language feature, each with its .out and with an .in when it reads input
tests/input-parsers/12 fixtures for the read DSL: one constructor, template or whitespace rule each
benchmarks/praxis/eight larger programs: bfs, collatz, hashwork, mandelbrot, pipeline, primes, tree, vm
crates/praxis-cli/tests/fixtures/programs whose diagnostics are the fixture, and run/, whose programs are driven end to end

None of those directories is documentation. Where one of them disagrees with a chapter of this book, the chapter is the one that was checked against a running compiler.