Esc
Start typing to search...

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:

FlagOutput
--astParsed AST as JSON
--typesPer-binding inferred types
--bytecodeBytecode instruction table with source annotations
--execVM 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.