Lisp - Debugging
Debugging in Common Lisp
Section titled “Debugging in Common Lisp”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.
Example: A Runtime Error
Section titled “Example: A Runtime Error”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)Simulated Debugger Interaction
Section titled “Simulated Debugger Interaction”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 return42and the program would continue executing.
Programmatic Tracing and Stepping
Section titled “Programmatic Tracing and Stepping”Tracing Function Calls with trace
Section titled “Tracing Function Calls with trace”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)Output
Section titled “Output” 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 6Stepping Through Evaluation with step
Section titled “Stepping Through Evaluation with step”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)))Robust Error Handling in Production
Section titled “Robust Error Handling in Production”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