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

Command reference

Fifteen commands, and help lists all of them. They are the same fifteen on either surface: the transcripts here show the Praxis crash> prompt, and the full-screen debugger runs each of them from its : line.

This is the list for a program that faulted. A program stopped at a :bp marker gets a shorter one — it has a continue, and it has no p, restart or reload.

Crash debugger commands:
  bt              show the numbered backtrace
  frame N         select frame N
  up              move the selection toward the caller
  down            move the selection toward the callee
  locals          show the selected frame's locals
  p EXPR          evaluate a read-only expression
  type EXPR       show the inferred expression type
  heap EXPR       inspect a value with its type
  source [N]      show the selected (or Nth) frame's source
  input           show the input near the active parser cursor
  parser          show the active input parser near the fault
  restart         rerun the program with the same input
  reload          recompile source and rerun with the same input
  help            show this message
  quit            exit the debugger

A command is one line: the first word is the command and the rest is its argument, whitespace-trimmed at both ends. There is no history, no completion, no multi-line input and no abbreviation — b is not bt. A blank line does nothing. A word that is not a command gets

unknown command `frobnicate`. Type `help` for the list.

Three commands have aliases: bt/backtrace, help/?, quit/exit/q. End-of-file is quit.

Almost every transcript below is against the same three-frame program, which divides by the difference between two equal readings:

var depths = read lines(int)

fn ratio(a, b) {
    a / (b - a)
}

fn step(values, i) {
    ratio(values[i], values[i + 1])
}

var total = 0
for i in 0..depths.len() - 1 {
    total = total + step(depths, i)
}
out(total)

Its input is 10, 20, 20. The fault banner each session opens with is the same one every time, so it is cut from most of the transcripts below; each block starts at Entered crash debugger.. See entering the debugger for what the banner says.


bt, backtrace

Print every frame in the snapshot, innermost first, then a line naming the selected one. The number sits in a three-column field so the names line up.

#{N}   {function name}
  (frame {selected} selected)

frame N

Select frame N. Prints frame N: name. An N past the end is an error and leaves the selection alone; an N that is not a number is a usage line.

up, down

up moves one frame toward the caller (a higher number), down one frame toward the callee. Each prints the newly selected frame the way frame N does, or refuses at the end of the chain:

already at the outermost frame
already at the innermost frame

All four navigation commands in one session:

Entered crash debugger. 3 frame(s). Type `help` for commands.
Praxis crash> bt
#0   ratio
#1   step
#2   <entry>
  (frame 0 selected)
Praxis crash> frame 1
frame 1: step
Praxis crash> bt
#0   ratio
#1   step
#2   <entry>
  (frame 1 selected)
Praxis crash> up
frame 2: <entry>
Praxis crash> up
already at the outermost frame
Praxis crash> down
frame 1: step
Praxis crash> down
frame 0: ratio
Praxis crash> down
already at the innermost frame
Praxis crash> frame 9
error: frame 9 out of range (0..=2)
Praxis crash> frame x
usage: frame N
Praxis crash> quit

locals

Print the selected frame’s slots, in two labeled sections.

  locals:
    {name}: {Type} = {value}
  temps:
    <tmp#{id}: {Type}> @ "{expression}" = {value}

locals: is the bindings you wrote — a var, a parameter, a for variable and a name a pattern introduces, all of which are bindings in the same sense. temps: is the compiler’s intermediates, each tagged with its per-frame id, its static type, and — this is the useful part — the source expression it materialized. A value is rendered through the same descriptor out uses. A slot nothing was written into is <uninit>.

Either section is omitted when it is empty; a frame with no slots at all prints (no locals in this frame). Temps that hold nothing and explain nothing (no value and no source span) are dropped rather than shown as noise.

Unlike the banner, locals caps no number of slots: it prints the whole frame. Each individual value is bounded, though, and cut at an element boundary with ... marking the remainder — so a Vec of ten thousand elements reads [0, 1, 2, ...] instead of burying the rest of the frame under itself.

Entered crash debugger. 3 frame(s). Type `help` for commands.
Praxis crash> locals
  locals:
    a: Int = 20
    b: Int = 20
  temps:
    <tmp#3: Int> @ "b - a" = 0
    <tmp#4: Int> @ "a / (b - a)" = <uninit>
Praxis crash> frame 1
frame 1: step
Praxis crash> locals
  locals:
    values: Vec[Int] = [10, 20, 20]
    i: Int = 1
  temps:
    <tmp#3: Int> @ "values[i]" = 20
    <tmp#4: Int> @ "1" = 1
    <tmp#5: Int> @ "i + 1" = 2
    <tmp#6: Int> @ "values[i + 1]" = 20
    <tmp#7: Int> @ "ratio(values[i], values[i + 1])" = <uninit>
Praxis crash> quit

Frame 1’s temps read as a small trace of the call that faulted: values[i] was 20, values[i + 1] was 20, and the call whose result they were arguments to never returned a value.

One known rough edge in this output: shadowing. Two bindings that shadow each other print as two lines with the same name, in declaration order, and nothing distinguishes them:

var count = 1
if count > 0 {
    var count = count + 40
    var zero = 0
    out(count / zero)
}
Entered crash debugger. 1 frame(s). Type `help` for commands.
Praxis crash> locals
  locals:
    count: Int = 1
    count: Int = 41
    zero: Int = 0
  temps:
    <tmp#1: Int> @ "1" = 1
    <tmp#3: Int> @ "0" = 0
    <tmp#4: Bool> @ "count > 0" = true
    <tmp#6: Int> @ "40" = 40
    <tmp#7: Int> @ "count + 40" = 41
    <tmp#9: Int> @ "0" = 0
    <tmp#11: Int> @ "count / zero" = <uninit>
    <tmp#12: Unit> @ "out(count / zero)" = <uninit>
Praxis crash> p count
41
Praxis crash> quit

The frame’s metadata does carry a distinct symbol id for each count, and this renderer does not print it — so the order of the lines is what tells you which is which, and p count answers for the inner one only. Read the outer one off locals. See bindings and shadowing.

p EXPR

Evaluate EXPR against the selected frame and print the result. The expression is ordinary Praxis; the frame’s named locals are in scope; the result is printed through its descriptor, one line, no type.

p compiles and runs real code — it synthesizes a function whose parameters are the locals your expression mentions, type-checks it, JIT-compiles it, and calls it against the live heap. What it will not do is change anything: a mutating method or a call to one of your own functions is refused before it runs. Empty argument prints usage: p EXPR.

Entered crash debugger. 3 frame(s). Type `help` for commands.
Praxis crash> p a
20
Praxis crash> p b - a
0
Praxis crash> type a
Int
Praxis crash> frame 1
frame 1: step
Praxis crash> p values
[10, 20, 20]
Praxis crash> type values
Vec[Int]
Praxis crash> p values.len() * 2
6
Praxis crash> p values.push(9)
error: method `push` is impure (may mutate state) — `p` rejects mutating expressions
Praxis crash> quit

p b - a answering 0 is the whole diagnosis of this crash in one line.

Errors come back as error: and a message. A name the frame does not bind gives:

error: type error: `xs` is not defined

Evaluating expressions covers what p accepts, what the purity gate rejects, and where the limits are.

type EXPR

The same pipeline as p, stopped after type-checking: it prints the inferred type of EXPR and never JIT-compiles or runs it. Usage line is usage: type EXPR.

Because nothing runs, type also skips the purity gate — type values.push(9) answers Unit where p values.push(9) refuses. It is the safe way to ask what a method would give you back.

heap EXPR

p with the type in front, separated by : .

{Type}: {value}

The value part is the same text p prints, through the same recursive descriptor — a map of vectors comes back with the vectors in it either way. What heap adds is the type, which is what you want when the question is what shape a structure has rather than what number it holds. Against a different program:

var counts = Map[Text, Vec[Int]]()
counts["ada"] = [1, 2, 3]
counts["alan"] = [4, 5]
out(counts["ada"].sum() + counts["turing"].sum())
Entered crash debugger. 1 frame(s). Type `help` for commands.
Praxis crash> p counts
{ada: [1, 2, 3], alan: [4, 5]}
Praxis crash> heap counts
Map[Text, Vec[Int]]: {ada: [1, 2, 3], alan: [4, 5]}
Praxis crash> heap counts["ada"]
Vec[Int]: [1, 2, 3]
Praxis crash> type counts
Map[Text, Vec[Int]]
Praxis crash> quit

heap is p and type in one line. It runs the expression, so the purity gate applies to it too. Usage line is usage: heap EXPR.

source [N]

Print the selected frame’s function — the function’s name, a file:line:column header, and the source lines with a caret rule under the extent the frame covers. With an argument, print frame N instead, without changing the selection.

Entered crash debugger. 3 frame(s). Type `help` for commands.
Praxis crash> source
ratio:
  <debug>:3:1
  3 | fn ratio(a, b) {
    | ^^^^^^^^^^^^^^^^...
  4 |     a / (b - a)
    | ^^^^^^^^^^^^^^^...
  5 | }
    | ^
Praxis crash> source 1
step:
  <debug>:7:1
  7 | fn step(values, i) {
    | ^^^^^^^^^^^^^^^^^^^^...
  8 |     ratio(values[i], values[i + 1])
    | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^...
  9 | }
    | ^
Praxis crash> quit

Three things about that output are worth knowing before they surprise you.

The filename is literally <debug>, not your file’s name: the snippet is rendered against a throwaway source map built from the text the session is holding. The line numbers are real.

The carets cover the frame’s extent, which is the whole function, not the faulting line. Each line is underlined to its own end and a ... marks that the span continues onto the next one. To find the faulting subexpression, read the @ "expr" annotations in locals — or use the full-screen debugger, whose source pane marks the faulting line and underlines the subexpression, having recovered both from those same annotations.

An out-of-range or non-numeric N is not an error — source 99 and source zz both fall back to the selected frame. And the entry frame’s extent is the whole file, so source on <entry> prints your program, entire.

A frame with no recorded span prints (no source span recorded for this frame).

input

Show the input around the point an input parser stopped. It answers for an input parse mismatch fault and says so for any other kind:

(no input context — not a parse failure)

parser

Show what the parser wanted at that point. Same rule: it answers for a parse fault and otherwise prints (no parser context — not a parse failure).

Both against the parse-fault program from the fault model:

var rows = read lines(`{name:word} {score:int}`)
out(rows.len())
Entered crash debugger. 1 frame(s). Type `help` for commands.
Praxis crash> input
input at offset 12..12:
  ada 36⏎alan oops⏎
Praxis crash> parser
expected: int
parser expression: <unknown parser>
Praxis crash> quit

parser expression: <unknown parser> is what you get for every parse failure: the failing parser’s source span is not threaded through to the runtime, so there is no expression text to print in its place. What the command gives you is the expected description. Inspecting the input parser goes into the detail.

restart

Re-run the same compiled code against the same input. No recompilation. The fault, snapshot and parse detail are cleared first, the original input bytes are re-installed, and the program’s entry point is called again.

If the re-run faults, the new snapshot replaces the old one and the frame cursor resets to 0:

program faulted: {kind}
{N} frame(s); frame 0 selected.

If it completes, the debugger prints program completed: {value} and stays at the prompt.

reload

Re-read the source file from disk, recompile it, and then do what restart does. The input bytes and the input filename are retained; only the code changes. This is the edit-and-retry loop: leave the debugger open in one window, fix the source in another, type reload.

A compilation that fails leaves everything as it was:

error: {diagnostic}
(session unchanged — the old snapshot is still active)

The old JIT code and the old snapshot are only discarded once the new compilation has succeeded, so a typo in your fix costs you nothing.

Entered crash debugger. 3 frame(s). Type `help` for commands.
Praxis crash> restart
program faulted: division by zero
3 frame(s); frame 0 selected.
Praxis crash> bt
#0   ratio
#1   step
#2   <entry>
  (frame 0 selected)
Praxis crash> reload
program faulted: division by zero
3 frame(s); frame 0 selected.
Praxis crash> quit

Nothing changed on disk between those two commands, so the program faulted identically twice. That is the point of the guarantee: a restart is reproducible, and a reload that reports a different fault reports it because you changed something.

One caveat about “the same input”. What is retained is what the program actually read. A program that faulted before its first read recorded nothing, so a reload that moves the read earlier in the program sees empty input rather than the original standard input — that stream is gone. Running with --input FILE avoids the question entirely.

help, ?

Print the command list at the top of this chapter.

quit, exit, q

Leave the debugger. The heap and the JIT are torn down in order, and praxis run exits 1, because the program still faulted.

The full set of error and usage lines, in one session:

Entered crash debugger. 3 frame(s). Type `help` for commands.
Praxis crash> help
Crash debugger commands:
  bt              show the numbered backtrace
  frame N         select frame N
  up              move the selection toward the caller
  down            move the selection toward the callee
  locals          show the selected frame's locals
  p EXPR          evaluate a read-only expression
  type EXPR       show the inferred expression type
  heap EXPR       inspect a value with its type
  source [N]      show the selected (or Nth) frame's source
  input           show the input near the active parser cursor
  parser          show the active input parser near the fault
  restart         rerun the program with the same input
  reload          recompile source and rerun with the same input
  help            show this message
  quit            exit the debugger
Praxis crash> frobnicate
unknown command `frobnicate`. Type `help` for the list.
Praxis crash> p
usage: p EXPR
Praxis crash> input
(no input context — not a parse failure)
Praxis crash> quit

What is not here

There is no step and no next, and at a fault there is no continue either: the faulting operation has no answer to resume with, and by the time you see the prompt every frame has unwound. restart and reload are the only ways to run anything again, and both start from the top.

continue does exist, at the other prompt. A :bp breakpoint stops a program that has not faulted, and that one has frames to return to — so Praxis stop> has continue and does not have p, restart or reload. The two prompts are the same commands minus whatever the situation cannot support.

There is also no way to change a value, at either prompt. p is read-only by construction, and the reason is the same one — a state that cannot be resumed cannot usefully be edited.