Skip to content

Lisp - 调试

Common Lisp 以其强大且交互式的调试能力而闻名。当 Lisp 程序遇到错误时,它通常不会直接崩溃,而是会进入一个交互式调试器 (debugger),允许开发者检查程序状态、修复问题并恢复执行。这促进了一种独特流畅且高效的开发工作流程。

当错误发生时,Lisp 会发出 (signal) 一个条件 (condition)。在交互式环境(如 SLIME 或 REPL)中,此操作会调用调试器。调试器会提供丰富的错误上下文,包括一个栈回溯 (stack backtrace) 和一组可能的重启选项 (restarts)。

考虑这段会引发除零错误的 Lisp 代码。

(defun divide (a b)
(/ a b))
(defun calculate (x)
(let ((y 0))
(divide x y)))
;; 此调用将触发调试器
(calculate 10)

运行 (calculate 10) 会让你进入一个大致如下所示的会话:

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) >

在此调试器提示符下,你可以:

  • 检查状态 (Inspect State):栈回溯 (backtrace) 显示了调用栈 (call stack)。在现代环境中,你可以检查每个帧 (frame) 中局部变量 (A、B、X、Y) 的值。
  • 评估表达式 (Evaluate Expressions):你可以输入 Lisp 代码来进一步探查程序的状态。
  • 调用重启选项 (Invoke Restarts):你可以选择一个重启选项来处理错误。例如,在提示符下输入 (use-value 42) 会使 (divide x y) 调用返回 42,程序将继续执行。

trace 宏允许你监控特定函数的进入和退出,并打印它们的参数和返回值。这对于理解程序流程而不必手动添加 print 语句来说是无价的。

;; 一个简单的递归阶乘函数
(defun factorial (n)
(if (zerop n)
1
(* n (factorial (1- n)))))
;; 开始追踪函数
(trace factorial)
;; 调用函数
(factorial 3)
;; 停止追踪
(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

step 宏允许你一步一步地遍历 Lisp 表达式 (form) 的求值过程。它显示每个子表达式及其结果,提供了求值过程的细粒度视图。

;; 单步执行一个简单的算术表达式
(step (+ (* 2 3) (1+ 4)))

虽然交互式调试器对于开发来说非常有用,但生产代码需要优雅地处理错误。handler-case 宏是实现这一功能的标准工具。它允许你执行代码并为可能发出的特定条件类型定义处理程序 (handler)。

(defun safe-divide (a b)
"Divides a by b, returning NIL if division by zero occurs."
(handler-case (/ a b)
;; 此子句仅在发出 DIVISION-BY-ZERO 条件时运行。
(division-by-zero (c)
(format *error-output* "Error: Caught condition ~a~%" c)
nil))) ; 错误时返回 nil
(safe-divide 10 2)
;=> 5
(safe-divide 10 0)
;=> Error: Caught condition #<DIVISION-BY-ZERO ...>
;=> NIL