你已经进入了 Lisp 的世界,但是会有这么些疑问:我要怎么调试查看代码的运行?与其他的平台相比 Lisp 的交互怎么样?除了栈追踪外,debugger 还能输出什么?

打印调试信息

当然,可以使用流行的“打印调试信息”咯。现在,来回顾以下打印函数吧。

print 会输出一个可读的相对应的参数,这就意味着 print 打印出来的信息可以被 Lisp 重新读取。

princ 专注于美化输出。

(format t "~a" ...),可以通过美化的格式,输出字符串(当第一个参数为 t 时,输出到标准输出流)并返回 nil,而 format nil ... 不输出任何东西同时返回字符串。通过不同的格式控制,可以同时输出多个变量值。更多关于 format 的格式,详见第3章:format

日志

日志从由打印调试信息发展而来的。;)

log4cl 这个日志库用的人比较多,但日志库并不止这一个。加载 log4cl:

  1. (ql:quickload :log4cl)

加载好后来创建个简单的变量吧:

  1. (defvar *foo* '(:a :b :c))

可以使用 log4cl 的别名 log,之后就可以很简单的使用了:

  1. (log:info *foo*)
  2. ;; <INFO> [13:36:49] cl-user () - *FOO*: (:A :B :C)

可以将字符串与表达式混合使用,下面是有 format 格式控制与没有格式控制的区别:

  1. (log:info "foo is " *foo*)
  2. ;; <INFO> [13:37:22] cl-user () - foo is *FOO*: (:A :B :C)
  3. (log:info "foo is ~{~a~}" *foo*)
  4. ;; <INFO> [13:39:05] cl-user () - foo is ABC

通过与 log4slime 库的配合使用,可以交互地修改日志级别:

  • 全局globally
  • 单个库
  • 单个函数
  • 以及 CLOS 方法和 CLOS 的继承(before 和 after 方法)

当输出比较多时,需要关掉一些确定的函数或包的日志,来缩小搜索范围时,使用 log4slime 就会很方便。甚至可以保存该配置,然后在其他的环境或电脑上重复使用。

这些操作都是可以通过命令、键盘快捷键以及菜单或鼠标点击来完成。

23. Debugging - 图1

强烈建议阅读 log4cl 的手册。

使用强大的 REPL (Read Evaluate Print Loop)

Lisp 的乐趣在其出色的REPL。REPL 的存在大大的推迟了使用其他的调试工具(假如REPL在日常工作没有中消除这些工具的话)。

每当定义好了一个函数后,就可以到 REPL 中运行一遍。在 Slime 中,使用 C-c C-c 快捷键来执行函数(C-c C-k 用来执行整个窗口(buffer)的代码),可以通过 C-c C-z(貌似我的配置不行,可能是被弃用了) 来切换 REPL。最后,通过 (in-package :your-package-name) 命令切换到自定义的包中。

反馈是实时的。既不要将代码重新编译一遍,也不用重启任何进程,更不需要在 shell 中创建一个主函数然后定义命令行参数(之后会介绍如何做)。

通常,需要生成一些数据来测试函数。这就是 REPL 存在的艺术了,这也将称为新手的一种习惯。诀窍在于在函数的 #+nil 声明中放入测试数据,这样你就可以手动去编译了:

  1. #+nil
  2. (progn
  3. (defvar *test-data* nil)
  4. (setf *test-data* (make-instance 'foo …)))

当加载这个文件是,*test-data* 不存在,但可以通过 C-c C-c 手动创建。

可以像上面那样定义测试函数。

有些人可能更喜欢在注释中 #| … |# 定义

综上所述,有时间一定要记得写测试单元 ;)

inspect 和 describe

这两个命令的结果都一样,输出对象的信息,而 inspect 是可交互的。

  1. (inspect *foo*)
  2. The object is a proper list of length 3.
  3. 0. 0: :A
  4. 1. 1: :B
  5. 2. 2: :C
  6. > q

当然,如果编辑器支持的话,可以在 REPL 中右击对象,然后 inspect。之后对象的信息就会显示在屏幕上,然后就可以深入数据结构内部,甚至修改数据结构。

现在来粗略地看下更有趣的数据结构,对象:

  1. (defclass foo ()
  2. ((a :accessor foo-a :initform '(:a :b :c))
  3. (b :accessor foo-b :initform :b)))
  4. ;; #<STANDARD-CLASS FOO>
  5. (make-instance 'foo)
  6. ;; #<FOO {100F2B6183}>

#<FOO 对象上右击,选择 “inspect”,将会看到个交互面板(在 Slime 中操作):

  1. #<FOO {100F2B6183}>
  2. --------------------
  3. Class: #<STANDARD-CLASS FOO>
  4. --------------------
  5. Group slots by inheritance [ ]
  6. Sort slots alphabetically [X]
  7. All Slots:
  8. [ ] A = (:A :B :C)
  9. [ ] B = :B
  10. [set value] [make unbound]

当在属性 A 那里单击或是在那一行按回车,可以进一步的查看其内容:

  1. #<CONS {100F5E2A07}>
  2. --------------------
  3. A proper list:
  4. 0: :A
  5. 1: :B
  6. 2: :C

交互式 debugger

每当异常发生时(具体参见第12章:异常处理),都会弹出个交互的调试器。

调试其中会显示错误信息,可执行的操作以及回溯信息。回顾一下 restarts:

  • the restarts are programmable, we can create our own
  • in Slime, press v on a stack trace frame to view the corresponding
    source file location
  • hit enter on a frame for more details
  • we can explore the functionality with the menu that should appear
    in our editor. See the “break” section below for a few
    more commands (eval in frame, etc).

通常情况下,编译器会优化一些东西,这会减少调试器可用的信息量。例如,有时无法看到中间变量的计算。可以改变优化选项:

  1. (declaim (optimize (speed 0) (space 0) (debug 3)))

然后重新编译代码。

追踪

trace 可以查看函数何时被调用,接收了什么参数,以及返回值时多少。

  1. (defun factorial (n)
  2. (if (plusp n)
  3. (* n (factorial (1- n)))
  4. 1))
  1. (trace factorial)
  2. (factorial 2)
  3. 0: (FACTORIAL 3)
  4. 1: (FACTORIAL 2)
  5. 2: (FACTORIAL 1)
  6. 3: (FACTORIAL 0)
  7. 3: FACTORIAL returned 1
  8. 2: FACTORIAL returned 1
  9. 1: FACTORIAL returned 2
  10. 0: FACTORIAL returned 6
  11. 6
  12. (untrace factorial)

不想追踪所有的函数时,只要运行 (untrace)

在 Slime 中,快捷键 C-c M-t 就是用来追踪或不追踪函数的。

如果没有看到递归调用,可能时编译器进行了优化。可以在追踪函数前定义以下代码:

  1. (declaim (optimize (debug 3)))

输出将打印到 *trace-output* (参见 CLHS)

在 Slime 中,也可以将交互式的追踪对话框 M-x slime-trace-dialog 绑定为 C-c T

追踪方法调用

在 SBCL 中,通过使用 (trace foo :methods t) 来追踪联合方法(before,after,around方法)的执行顺序。如:

  1. (trace foo :methods t)
  2. (foo 2.0d0)
  3. 0: (FOO 2.0d0)
  4. 1: ((SB-PCL::COMBINED-METHOD FOO) 2.0d0)
  5. 2: ((METHOD FOO (FLOAT)) 2.0d0)
  6. 3: ((METHOD FOO (T)) 2.0d0)
  7. 3: (METHOD FOO (T)) returned 3
  8. 2: (METHOD FOO (FLOAT)) returned 9
  9. 2: ((METHOD FOO :AFTER (DOUBLE-FLOAT)) 2.0d0)
  10. 2: (METHOD FOO :AFTER (DOUBLE-FLOAT)) returned DOUBLE
  11. 1: (SB-PCL::COMBINED-METHOD FOO) returned 9
  12. 0: FOO returned 9
  13. 9

详见第15章: CLOS

Step

step 是个和 trace 一样作用范围的交互命令。像这样:

  1. (step (factorial 2))

会有出现一个包含可用的 restarts 的交互面板:

  1. Evaluating call:
  2. (FACTORIAL 2)
  3. With arguments:
  4. 2
  5. [Condition of type SB-EXT:STEP-FORM-CONDITION]
  6. Restarts:
  7. 0: [STEP-CONTINUE] Resume normal execution
  8. 1: [STEP-OUT] Resume stepping after returning from this function
  9. 2: [STEP-NEXT] Step over call
  10. 3: [STEP-INTO] Step into call
  11. 4: [RETRY] Retry SLIME REPL evaluation request.
  12. 5: [*ABORT] Return to SLIME's top level.
  13. --more--
  14. Backtrace:
  15. 0: ((LAMBDA ()))
  16. 1: (SB-INT:SIMPLE-EVAL-IN-LEXENV (LET ((SB-IMPL::*STEP-OUT* :MAYBE)) (UNWIND-PROTECT (SB-IMPL::WITH-STEPPING-ENABLED #))) #S(SB-KERNEL:LEXENV :FUNS NIL :VARS NIL :BLOCKS NIL :TAGS NIL :TYPE-RESTRICTIONS ..
  17. 2: (SB-INT:SIMPLE-EVAL-IN-LEXENV (STEP (FACTORIAL 2)) #<NULL-LEXENV>)
  18. 3: (EVAL (STEP (FACTORIAL 2)))

Stepping 很有用,然后,这也意味这需要对函数进行精简。

中断

当调用break时,程序会进入到 debugger,在里面就可以检查调用栈了。

Slime 中的断点

查看 SLDB 的菜单,里面包含了导航键和可用的操作。如下:

  • e (sldb-eval-in-frame) prompts for an expression and evaluates
    it in the selected frame. This is how we can explore our
    intermediate variables
  • d is similar with the addition of pretty printing the result

Once we are in a frame and detect a suspicious behavior, we can even
re-compile a function at runtime and resume the program execution from
where it stopped (using the “step-continue” restart).

Advise 和 watch

advisewatch 在一些解释器中可以使用,如 CCL 中的(advisewatch) 以及 LispWorks. SBCL 中也有,但是没有外部调用接口。advise 是在不改变源代码的情况下修改函数,或者是说在函数执行前或执行后做一些操作,和 CLOS 中的方法绑定一样(before,after,around方法)。

当线程想要望一个被监视的对象中写入时, watch 就会抛出异常。可以在 GUI 中与监视的对象的显示相结合。

也有一个可移植层的非发布库 cl-advice

测试单元

最后,可能需要对单独地对函数进行自动测试。参见第26章:测试测试框架及三方库 列表。

远程调试

下面是讲解怎么进行远程调试。

具体步骤是先要在远程服务器上启动 Swank 服务,创建一个 ssh 隧道,然后通过编辑器(Emacs)连接到 Swank 服务。之后,就可以直接在运行的示例(远程机器)上浏览执行代码了。

先来定义一个一直打印的函数吧。

需要的话,可以先将依赖导入:

  1. (ql:quickload '(:swank :bordeaux-threads))
  1. ;; a little common lisp swank demo
  2. ;; while this program is running, you can connect to it from another terminal or machine
  3. ;; and change the definition of doprint to print something else out!
  4. (require :swank)
  5. (require :bordeaux-threads)
  6. (defparameter *counter* 0)
  7. (defun dostuff ()
  8. (format t "hello world ~a!~%" *counter*))
  9. (defun runner ()
  10. (bt:make-thread (lambda ()
  11. (swank:create-server :port 4006)))
  12. (format t "we are past go!~%")
  13. (loop while t do
  14. (sleep 5)
  15. (dostuff)
  16. (incf *counter*)))
  17. (runner)

在服务器上,可以这样运行:

  1. sbcl --load demo.lisp

然后再通过 ssh 远程连接这台开发服务器:

  1. ssh -L4006:127.0.0.1:4006 username@example.com

上面的命令会已加密的方式,通过本机的 4006 端口( swanks 只接受从 localhost 的连接)访问 example.com 服务器的 4006 端口

按下 M-x slime-connect 之后输入 4006 就可以启动 swank 进行连接了。

然后就可以添加新的代码:

  1. (defun dostuff ()
  2. (format t "goodbye world ~a!~%" *counter*))
  3. (setf *counter* 0)

和平时一样,使用 C-c C-c 或是 M-x slime-eval-region 来执行这段代码,然后就能看到输出了。

以下是在1999年时,Ron Garret 在地球上调试 Deep Space 1 宇宙飞船:

we were able to debug and fix a race condition that had not shown up during ground testing. (Debugging a program running on a $100M piece of hardware that is 100 million miles away is an interesting experience. Having a read-eval-print loop running on the spacecraft proved invaluable in finding and fixing the problem.

参考