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

The Praxis Book

Praxis is a small, statically typed, garbage-collected programming language for Advent of Code-style puzzle solving. It is procedural and expression-oriented, it infers essentially every type, and it is built around three things a puzzle solver spends the evening doing: reading a strange input format, manipulating data, and finding out why the program just fell over.

A whole program can be three lines:

var numbers = read lines(int)

out(numbers.sum())
$ praxis run sum.px --input sum.in
60

There is no main, no imports, no annotations. read lines(int) is not a library call — read is an expression in the language and lines(int) is a parser written in a small DSL the compiler checks and compiles alongside the rest of your program. numbers is a Vec[Int] because that is what the parser produces, and the compiler worked that out rather than being told.

The three things this book is mostly about

Reading input. Puzzle input is never a data format anybody would choose. Praxis makes the shape of the file the shape of the code. Structure is written outside backticks, where whitespace does not matter; the literal text of the input is written inside them, where it does:

var moves = read lines(`{dir:word} {amount:int}`)

var horizontal = 0
var depth = 0

for move in moves {
    match move.dir {
        "forward" => { horizontal = horizontal + move.amount }
        "down" => { depth = depth + move.amount }
        "up" => { depth = depth - move.amount }
        _ => {}
    }
}

out(horizontal * depth)
$ praxis run dive.px --input dive.in
150

moves is a Vec of records with a dir field and an amount field. Nothing declared that record; the template’s captures are where it came from. Reading input is the part of the book that covers this, and the cookbook has a recipe for every input shape Advent of Code has thrown so far.

Type inference. Praxis is statically typed and almost none of the types are written down. Inference runs over your whole program including the parsers, so a mistake about the shape of the input is a compile error rather than a surprise at line 400. The editor shows you what it concluded: fn foo(a, b) reads as fn foo(a: Int, b: Int) in VS Code, and you can accept the hint to write the annotation into the file. Type inference covers the model, what generalizes, and how to read the errors when the compiler disagrees with you.

The crash debugger. Praxis has no exceptions and no error handling. An index out of bounds, a missing key, an integer overflow, a parse mismatch or a failed assertion stops the program and hands you the wreckage:

error: program faulted: index out of bounds

Backtrace:
#0   window_sum
#1   <entry>

  locals:
    values: Vec[Int] = [12, 7, 41]
    start: Int = 1
  temps:
    <tmp#3: Int> @ "values[start]" = 7
    <tmp#5: Int> @ "start + 1" = 2
    <tmp#6: Int> @ "values[start + 1]" = 41
    <tmp#9: Int> @ "start + 2" = 3

Those are not only the locals — they are the intermediate values of the expression that faulted, each labelled with the source text it came from. In a terminal you get a prompt instead of an exit code, and can walk the frames, print expressions against the captured state, look at the input near the parser’s cursor, then fix the file and reload without losing your input. The crash debugger is the part of the book that covers it.

What Praxis is not

It does not build standalone binaries; the compiler and the runtime are one executable and your program is JIT-compiled every time you run it. There is no ownership, no lifetimes, no manual memory management. There are no user-visible traits, no operator overloading, no macros, and no exceptions. There is no concurrency. There is no package registry.

These are deliberate, and when two of the goals conflict there is an order that settles it: correctness and diagnostics first, then input ergonomics, then edit-run-debug speed, then language simplicity, and only then runtime performance.

How this book is arranged

  • Getting started — build the compiler, run a program, learn the command surface.
  • The language — the whole surface, from bindings to pattern matching to the collection set, with the prelude and the method catalog as reference tables at the end.
  • Reading input — the read expression and its DSL.
  • Type inference — what the compiler works out, and why it sometimes will not.
  • The crash debugger — the fault model, and the prompt you get instead of an exit code.
  • Tooling — the language server, the VS Code extension, and an index of every diagnostic code.
  • Under the hood — for the reader who wants to change the compiler rather than use it.
  • Appendix A has complete programs; Appendix B has the grammar.

Every example here was run

No code block in this book that shows a program and its output was written by hand. Each one is a real file under docs/book/examples/, and

docs/book/examples/verify.sh

re-runs every one of them against the compiler in this repository and diffs the result against the output printed in the chapter. That includes the programs that are supposed to fail: the diagnostics and the debugger transcripts are captured the same way. If the language changes, this book breaks loudly.