Skip to content

Lisp - Debugging

Common Lisp is renowned for its powerful and interactive debugging capabilities. Rather than just crashing, a Lisp program that encounters an error will typically enter an interactive debugger, allowing the developer to inspect the program’s state, fix the problem, and resume execution. This fosters a uniquely fluid and productive development workflow.

The Condition System and Interactive Debugger

Section titled “The Condition System and Interactive Debugger”

When an error occurs, Lisp signals a condition. In an interactive environment (like SLIME or a REPL), this action invokes the debugger. The debugger presents a rich context about the error, including a stack backtrace and a set of possible restarts.

Consider this code that will cause a division-by-zero error.

(defun divide (a b)
(/ a b))
(defun calculate (x)
(let ((y 0))
(divide x y)))
;; This call will trigger the debugger
(calculate 10)

Running (calculate 10) would drop you into a session that looks something like this:

Error: Division by zero in / of (10 0).
[Condition of type DIVISION-BY-ZERO]
Restarts:
0: [RETRY] Retry the division operation.
1: [USE-VALUE] Use a different value for the result of (DIVIDE X Y).
2: [ABORT] Exit to the top-level REPL.
Backtrace:
0: (/)
1: (DIVIDE 10 0)
| locals: A=10, B=0
2: (CALCULATE 10)
| locals: X=10, Y=0
[1] (DIVIDE 10 0) >

From this debugger prompt, you can:

  • Inspect State: The backtrace shows the call stack. In modern environments, you can inspect the values of local variables (A, B, X, Y) in each frame.
  • Evaluate Expressions: You can type Lisp code to further probe the program’s state.
  • Invoke Restarts: You can choose a restart to handle the error. For example, typing (use-value 42) at the prompt would make the (divide x y) call return 42 and the program would continue executing.

The trace macro allows you to monitor the entry and exit of specific functions, printing their arguments and return values. This is invaluable for understanding program flow without manually adding print statements.

;; A simple recursive factorial function
(defun factorial (n)
(if (zerop n)
1
(* n (factorial (1- n)))))
;; Start tracing the function
(trace factorial)
;; Call the function
(factorial 3)
;; Stop tracing
(untrace factorial)
0: (FACTORIAL 3)
1: (FACTORIAL 2)
2: (FACTORIAL 1)
3: (FACTORIAL 0)
3: FACTORIAL returned 1
2: FACTORIAL returned 1
1: FACTORIAL returned 2
0: FACTORIAL returned 6

The step macro lets you walk through the evaluation of a Lisp form, one step at a time. It shows each sub-form and its result, providing a fine-grained view of the evaluation process.

;; Stepping through a simple arithmetic expression
(step (+ (* 2 3) (1+ 4)))

While the interactive debugger is great for development, production code needs to handle errors gracefully. The handler-case macro is the standard tool for this. It allows you to execute code and define handlers for specific condition types that might be signaled.

(defun safe-divide (a b)
"Divides a by b, returning NIL if division by zero occurs."
(handler-case (/ a b)
;; This clause runs only if a DIVISION-BY-ZERO condition is signaled.
(division-by-zero (c)
(format *error-output* "Error: Caught condition ~a~%" c)
nil))) ; Return nil on error
(safe-divide 10 2)
;=> 5
(safe-divide 10 0)
;=> Error: Caught condition #<DIVISION-BY-ZERO ...>
;=> NIL