Tracing and Debugging
The keel trace command exposes every phase of the compiler pipeline for a
source file: the parsed AST, inferred types, generated bytecode, and VM
execution. It is useful for understanding why a program behaves unexpectedly,
diagnosing type errors, and exploring what the compiler produces for a given
construct.
Basic Usage
The argument to keel trace is either a file path or an inline keel source string. If a file by that name exists it is read from disk; otherwise the string itself is compiled and traced.
keel trace "1 + 1" # Inline source — quick exploration
keel trace myfile.kl # File path
With no flags, all four output modes are enabled. You can enable individual modes with flags:
| Flag | Output |
|---|---|
--ast | Parsed AST as JSON |
--types | Per-binding inferred types |
--bytecode | Bytecode instruction table with source annotations |
--exec | VM execution trace (call-stack-aware, with register snapshots) |
keel trace myfile.kl --types --bytecode
Focusing on a Construct
The --focus flag narrows all output to instructions and AST nodes that
overlap a specific source location. It accepts either a literal string or a
line:col position:
# Focus on the first occurrence of the string "Blue" in the file
keel trace myfile.kl --bytecode --exec --focus "Blue"
# Focus on the construct at line 3, column 1
keel trace myfile.kl --bytecode --focus "3:1"
With --focus, the bytecode table only shows rows whose source map entry
overlaps the target span, and the exec trace only logs instructions from that
span.
AST Dump
--ast prints the parsed AST as pretty-printed JSON. Each top-level node
includes its source span:
keel trace myfile.kl --ast
Example output (abbreviated):
[
{
"node": {
"Stmt": {
"Let": {
"bindings": [
{
"pattern": { "Var": ["x", null] },
"expr": { "Binary": { "op": "Add", ... } }
}
]
}
}
},
"span": { "start": 0, "end": 13 }
}
]
Inferred Types
--types prints a table of every binding's inferred type after compilation:
keel trace myfile.kl --types
Example output:
[types]
x : Int
items : [String]
result : { name: String, count: Int }
process : String -> Int
This shows the same information as LSP hover, but works anywhere — in CI, over SSH, or without an editor.
Bytecode
--bytecode prints the generated instruction table annotated with source
fragments. Each row shows the instruction index, the instruction itself, and
the source location and fragment that generated it:
keel trace myfile.kl --bytecode
Example output:
idx │ instruction │ source
─────┼──────────────────────────────────────────────┼──────────────────────
0 │ MovRegConst(Reg(0), 0) │ 1:1 "let x ="
1 │ MovRegVar("x", Reg(0)) │ 1:1 "let x ="
2 │ MovRegConst(Reg(1), 1) │ 1:8 " 1 + "
The source fragment is truncated to fit the column and shows the span that the compiler was processing when it emitted the instruction.
Execution Trace
--exec runs the program and logs each instruction as it executes, along
with the call depth and a snapshot of non-empty registers. Exec trace output
goes to stderr so that program output remains on stdout.
Enable exec tracing with:
RUST_LOG=debug keel trace myfile.kl --exec
Example output:
0 | pc=0 MovRegConst(Reg(0), 0) regs={}
0 | pc=1 MovRegVar("x", Reg(0)) regs={R0=Int(3)}
0 | pc=2 MovRegConst(Reg(1), 1) regs={R0=Int(3)}
→ CALL depth=1 fn=<closure> args=[Int(3)]
1 | pc=0 AddRegRegReg(Reg(0), Reg(0), Reg(0)) regs={R0=Int(3)}
1 | pc=1 FunctionReturn(Reg(0)) regs={R0=Int(6)}
← RETURN depth=1 result=Int(6)
0 | pc=3 ...
The → CALL and ← RETURN markers appear at function call boundaries, with
indentation showing the nesting depth.
Type Inference Log
To see the full depth-indented type inference tree, set RUST_LOG to target
the type inference module:
RUST_LOG=keel_core::compiler::type_inference=debug keel run myfile.kl
Example output:
→ infer FunctionCall
→ infer Var
← Var : (a -> b) -> [a] -> [b]
→ infer Lambda
← Lambda : Int -> Int
← FunctionCall : [Int] -> [Int]
This makes visible exactly which infer and apply steps led to a type
error, and what the substitution was at each point.