你已经进入了 Lisp 的世界,但是会有这么些疑问:我要怎么调试查看代码的运行?与其他的平台相比 Lisp 的交互怎么样?除了栈追踪外,debugger 还能输出什么?
打印调试信息
当然,可以使用流行的“打印调试信息”咯。现在,来回顾以下打印函数吧。
print
会输出一个可读的相对应的参数,这就意味着 print
打印出来的信息可以被 Lisp 重新读取。
princ
专注于美化输出。
(format t "~a" ...)
,可以通过美化的格式,输出字符串(当第一个参数为 t
时,输出到标准输出流)并返回 nil,而 format nil ...
不输出任何东西同时返回字符串。通过不同的格式控制,可以同时输出多个变量值。更多关于 format 的格式,详见第3章:format
日志
日志从由打印调试信息发展而来的。;)
log4cl 这个日志库用的人比较多,但日志库并不止这一个。加载 log4cl:
(ql:quickload :log4cl)
加载好后来创建个简单的变量吧:
(defvar *foo* '(:a :b :c))
可以使用 log4cl 的别名 log
,之后就可以很简单的使用了:
(log:info *foo*)
;; <INFO> [13:36:49] cl-user () - *FOO*: (:A :B :C)
可以将字符串与表达式混合使用,下面是有 format
格式控制与没有格式控制的区别:
(log:info "foo is " *foo*)
;; <INFO> [13:37:22] cl-user () - foo is *FOO*: (:A :B :C)
(log:info "foo is ~{~a~}" *foo*)
;; <INFO> [13:39:05] cl-user () - foo is ABC
通过与 log4slime
库的配合使用,可以交互地修改日志级别:
- 全局globally
- 单个库
- 单个函数
- 以及 CLOS 方法和 CLOS 的继承(before 和 after 方法)
当输出比较多时,需要关掉一些确定的函数或包的日志,来缩小搜索范围时,使用 log4slime
就会很方便。甚至可以保存该配置,然后在其他的环境或电脑上重复使用。
这些操作都是可以通过命令、键盘快捷键以及菜单或鼠标点击来完成。
强烈建议阅读 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
声明中放入测试数据,这样你就可以手动去编译了:
#+nil
(progn
(defvar *test-data* nil)
(setf *test-data* (make-instance 'foo …)))
当加载这个文件是,*test-data*
不存在,但可以通过 C-c C-c
手动创建。
可以像上面那样定义测试函数。
有些人可能更喜欢在注释中 #| … |#
定义
综上所述,有时间一定要记得写测试单元 ;)
inspect 和 describe
这两个命令的结果都一样,输出对象的信息,而 inspect
是可交互的。
(inspect *foo*)
The object is a proper list of length 3.
0. 0: :A
1. 1: :B
2. 2: :C
> q
当然,如果编辑器支持的话,可以在 REPL 中右击对象,然后 inspect
。之后对象的信息就会显示在屏幕上,然后就可以深入数据结构内部,甚至修改数据结构。
现在来粗略地看下更有趣的数据结构,对象:
(defclass foo ()
((a :accessor foo-a :initform '(:a :b :c))
(b :accessor foo-b :initform :b)))
;; #<STANDARD-CLASS FOO>
(make-instance 'foo)
;; #<FOO {100F2B6183}>
在 #<FOO
对象上右击,选择 “inspect”,将会看到个交互面板(在 Slime 中操作):
#<FOO {100F2B6183}>
--------------------
Class: #<STANDARD-CLASS FOO>
--------------------
Group slots by inheritance [ ]
Sort slots alphabetically [X]
All Slots:
[ ] A = (:A :B :C)
[ ] B = :B
[set value] [make unbound]
当在属性 A 那里单击或是在那一行按回车,可以进一步的查看其内容:
#<CONS {100F5E2A07}>
--------------------
A proper list:
0: :A
1: :B
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).
通常情况下,编译器会优化一些东西,这会减少调试器可用的信息量。例如,有时无法看到中间变量的计算。可以改变优化选项:
(declaim (optimize (speed 0) (space 0) (debug 3)))
然后重新编译代码。
追踪
trace 可以查看函数何时被调用,接收了什么参数,以及返回值时多少。
(defun factorial (n)
(if (plusp n)
(* n (factorial (1- n)))
1))
(trace factorial)
(factorial 2)
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
6
(untrace factorial)
不想追踪所有的函数时,只要运行 (untrace)
。
在 Slime 中,快捷键 C-c M-t
就是用来追踪或不追踪函数的。
如果没有看到递归调用,可能时编译器进行了优化。可以在追踪函数前定义以下代码:
(declaim (optimize (debug 3)))
输出将打印到 *trace-output*
(参见 CLHS)
在 Slime 中,也可以将交互式的追踪对话框 M-x slime-trace-dialog
绑定为 C-c T
。
追踪方法调用
在 SBCL 中,通过使用 (trace foo :methods t)
来追踪联合方法(before,after,around方法)的执行顺序。如:
(trace foo :methods t)
(foo 2.0d0)
0: (FOO 2.0d0)
1: ((SB-PCL::COMBINED-METHOD FOO) 2.0d0)
2: ((METHOD FOO (FLOAT)) 2.0d0)
3: ((METHOD FOO (T)) 2.0d0)
3: (METHOD FOO (T)) returned 3
2: (METHOD FOO (FLOAT)) returned 9
2: ((METHOD FOO :AFTER (DOUBLE-FLOAT)) 2.0d0)
2: (METHOD FOO :AFTER (DOUBLE-FLOAT)) returned DOUBLE
1: (SB-PCL::COMBINED-METHOD FOO) returned 9
0: FOO returned 9
9
详见第15章: CLOS。
Step
step 是个和 trace
一样作用范围的交互命令。像这样:
(step (factorial 2))
会有出现一个包含可用的 restarts 的交互面板:
Evaluating call:
(FACTORIAL 2)
With arguments:
2
[Condition of type SB-EXT:STEP-FORM-CONDITION]
Restarts:
0: [STEP-CONTINUE] Resume normal execution
1: [STEP-OUT] Resume stepping after returning from this function
2: [STEP-NEXT] Step over call
3: [STEP-INTO] Step into call
4: [RETRY] Retry SLIME REPL evaluation request.
5: [*ABORT] Return to SLIME's top level.
--more--
Backtrace:
0: ((LAMBDA ()))
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 ..
2: (SB-INT:SIMPLE-EVAL-IN-LEXENV (STEP (FACTORIAL 2)) #<NULL-LEXENV>)
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 variablesd
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
advise 和 watch 在一些解释器中可以使用,如 CCL 中的(advise 和 watch) 以及 LispWorks. SBCL 中也有,但是没有外部调用接口。advise
是在不改变源代码的情况下修改函数,或者是说在函数执行前或执行后做一些操作,和 CLOS 中的方法绑定一样(before,after,around方法)。
当线程想要望一个被监视的对象中写入时, watch
就会抛出异常。可以在 GUI 中与监视的对象的显示相结合。
也有一个可移植层的非发布库 cl-advice。
测试单元
最后,可能需要对单独地对函数进行自动测试。参见第26章:测试 和 测试框架及三方库 列表。
远程调试
下面是讲解怎么进行远程调试。
具体步骤是先要在远程服务器上启动 Swank 服务,创建一个 ssh 隧道,然后通过编辑器(Emacs)连接到 Swank 服务。之后,就可以直接在运行的示例(远程机器)上浏览执行代码了。
先来定义一个一直打印的函数吧。
需要的话,可以先将依赖导入:
(ql:quickload '(:swank :bordeaux-threads))
;; a little common lisp swank demo
;; while this program is running, you can connect to it from another terminal or machine
;; and change the definition of doprint to print something else out!
(require :swank)
(require :bordeaux-threads)
(defparameter *counter* 0)
(defun dostuff ()
(format t "hello world ~a!~%" *counter*))
(defun runner ()
(bt:make-thread (lambda ()
(swank:create-server :port 4006)))
(format t "we are past go!~%")
(loop while t do
(sleep 5)
(dostuff)
(incf *counter*)))
(runner)
在服务器上,可以这样运行:
sbcl --load demo.lisp
然后再通过 ssh 远程连接这台开发服务器:
ssh -L4006:127.0.0.1:4006 username@example.com
上面的命令会已加密的方式,通过本机的 4006 端口( swanks 只接受从 localhost 的连接)访问 example.com 服务器的 4006 端口
按下 M-x slime-connect
之后输入 4006 就可以启动 swank 进行连接了。
然后就可以添加新的代码:
(defun dostuff ()
(format t "goodbye world ~a!~%" *counter*))
(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.
参考
- “How to understand and use Common Lisp”, chap. 30, David Lamkins (book download from author’s site)
- Malisper: debugging Lisp series
- Two Wrongs: debugging Common Lisp in Slime
- Slime documentation: connecting to a remote Lisp
- cvberrycom: remotely modifying a running Lisp program using Swank
- Ron Garret: Lisping at the JPL
- the Remote Agent experiment: debugging code from 60 million miles away (youtube) (“AMA” on reddit)