Those are useful Common Lisp debugging tricks. Did you know about trace options?
We see how trace
accepts options. Especially, we see how we can break
and invoke the interactive debugger before or after a function call, how we can break on a condition (“this argument equals 0”) and how we can enrich the trace output. But we only scratch the surface, more options are documented on their upstream documentation:
Table of Contents
- Trace - basics
- Trace options - break and invoke the debugger
- break on a condition
- Other options
- Closing remark
Trace - basics
But let’s first see a recap’ of the trace
macro. Compared to the previous Cookbook content, we just added that (trace)
alone returns a list of traced functions.
trace allows us to see when a function was called, what arguments it received, and the value it returned.
(defun factorial (n)
(if (plusp n)
(* n (factorial (1- n)))
1))
To start tracing a function, just call trace
with the function name
(or several function names):
(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)
To untrace all functions, just evaluate (untrace)
.
To get a list of currently traced functions, evaluate (trace)
with no arguments.
In Slime we have the shortcut C-c M-t
to trace or untrace a
function.
If you don’t see recursive calls, that may be because of the compiler’s optimizations. Try this before defining the function to be traced:
(declaim (optimize (debug 3))) ;; or C-u C-c C-c to compile with maximal debug settings.
The output is printed to *trace-output*
(see the CLHS).
In Slime, we also have an interactive trace dialog with M-x
slime-trace-dialog
bound to C-c T
.
But we can do many more things than calling trace
with a simple argument.
Trace options - break and invoke the debugger
trace
accepts options. For example, you can use :break t
to invoke
the debugger at the start of the function, before it is called (more on break below):
(trace factorial :break t)
(factorial 2)
We can define many things in one call to trace
. For instance,
options that appear before the first function name to trace are
global, they affect all traced functions that we add afterwards. Here,
:break t
is set for every function that follows: factorial
, foo
and bar
:
(trace :break t factorial foo bar)
On the contrary, if an option comes after a function name, it acts as
a local option, only for its preceding function. That’s how we first
did. Below foo
and bar
come after, they are not affected by :break
:
(trace factorial :break t foo bar)
But do you actually want to break
before the function call or just
after it? With :break
as with many options, you can choose. These
are the options for :break
:
:break form ;; before
:break-after form
:break-all form ;; before and after
form
can be any form that evaluates to true. You can add any custom logic here.
Note that we explained the trace function of SBCL. Other
implementations may have the same feature with another syntax and
other option names. For example, in LispWorks it is “:break-on-exit”
instead of “:break-after”, and we write (trace (factorial :break t))
.
Below are some other options but first, a trick with :break
.
break on a condition
The argument to an option can be any form. Here’s a trick, on SBCL, to
get the break window when we are about to call factorial
with 0. (sb-debug:arg 0)
refers to n
, the first argument.
CL-USER> (trace factorial :break (equal 0 (sb-debug:arg 0)))
;; WARNING: FACTORIAL is already TRACE'd, untracing it first.
;; (FACTORIAL)
Running it again:
CL-USER> (factorial 3)
0: (FACTORIAL 3)
1: (FACTORIAL 2)
2: (FACTORIAL 1)
3: (FACTORIAL 0)
breaking before traced call to FACTORIAL:
[Condition of type SIMPLE-CONDITION]
Restarts:
0: [CONTINUE] Return from BREAK.
1: [RETRY] Retry SLIME REPL evaluation request.
2: [*ABORT] Return to SLIME's top level.
3: [ABORT] abort thread (#<THREAD "repl-thread" RUNNING {1003551BC3}>)
Backtrace:
0: (FACTORIAL 1)
Locals:
N = 1 <---------- n is still 1, we break before the call with 0.
Other options
Trace on conditions
:condition
enables tracing only if the condition in form
evaluates to true.
:condition form
:condition-after form
:condition-all form
If :condition is specified, then trace does nothing unless Form evaluates to true at the time of the call. :condition-after is similar, but suppresses the initial printout, and is tested when the function returns. :condition-all tries both before and after.
Trace if called from another function
:wherein
can be super useful:
:wherein Names
If specified, Names is a function name or list of names. trace does nothing unless a call to one of those functions encloses the call to this function (i.e. it would appear in a backtrace.) Anonymous functions have string names like “DEFUN FOO”.
Enrich the trace output
:report Report-Type
If Report-Type is trace (the default) then information is reported by printing immediately. If Report-Type is nil, then the only effect of the trace is to execute other options (e.g. print or break). Otherwise, Report-Type is treated as a function designator and, for each trace event, funcalled with 5 arguments: trace depth (a non-negative integer), a function name or a function object, a keyword (:enter, :exit or :non-local-exit), a stack frame, and a list of values (arguments or return values).
See also :print
to enrich the trace output:
In addition to the usual printout, the result of evaluating Form is printed at the start of the function, at the end of the function, or both, according to the respective option. Multiple print options cause multiple values to be printed.
Example:
(defparameter *counter* 0)
(defun factorial (n)
(incf *counter*)
(if (plusp n)
(* n (factorial (1- n)))
1))
CL-USER> (trace factorial :print *counter*)
CL-USER> (factorial 3)
(FACTORIAL 3)
0: (FACTORIAL 3)
0: *COUNTER* = 0
1: (FACTORIAL 2)
1: *COUNTER* = 1
2: (FACTORIAL 1)
2: *COUNTER* = 2
3: (FACTORIAL 0)
3: *COUNTER* = 3
3: FACTORIAL returned 1
2: FACTORIAL returned 1
1: FACTORIAL returned 2
0: FACTORIAL returned 6
6
Closing remark
As they say:
it is expected that implementations extend TRACE with non-standard options.
and we didn’t list all available options or parameters, so you should check out your implementation’s documentation.
For more debugging tricks see the Cookbook and the links in it, the Malisper series have nice GIFs.
I am also preparing a short screencast to show what we can do inside the debugger, stay tuned!