Chapter 30 - Helpful Hints for Debugging and Bug-Proofing

As with any programming language, error avoidance is the best debugging strategy. Take advantage of the online documentation (available with most systems) and test functions, or even parts of functions, as you write them.

Still, you'll inevitably face the unexpected error, and you should know how to use the debugger. More often than not, a quick look at the error location as shown by the debugger will point out an obvious problem.

Some problems, though, are not obvious; your program will run without error, but produce incorrect results. When examination of the code does not reveal an error, you can rely upon built in Lisp tools to expose the details of your program's operation and find the error during execution.

Finding the cause of an error

There are two ways to notice an error. The intrusion of the Lisp debugger is the most obvious. The debugger will appear whenever your program causes Lisp to signal an error. This is often the result of something obvious, like trying to perform arithmetic on NIL or trying to FUNCALL an object that is not a function.

Your program's failure to produce expected results is also an error, even though the debugger never appears. In this case, your program doesn't make any mistakes in its use of Lisp, but the successful sequence of Lisp operations doesn't do what you had intended. Another possibility is that your program will fail to terminate at all.

The best defense against all of these problems is to write short, clear function definitions and test each one as soon as you've written it. I find it helpful to write one or more test cases and include them as comments (bracketed by #| and |#) in the same file.

Reading backtraces, compiler settings for debugging

Every Lisp debugger will provide at least two important pieces of information: an error message and a stack backtrace.

The error message describes how the program failed. Normally, this is a description of an error encountered while executing some built in Lisp function. If your program calls ERROR, the debugger will display the message you specify.

The stack backtrace describes where your program failed by displaying the call stack at the point of the error. The function which signalled the error will be at the "top" of the stack. Below that is the function that called the function which signalled the error, and so on all the way to (and sometimes beyond) the listener's read-eval-print loop.

The debugger relies upon certain information provided by the compiler or interpreter. Although the details vary among implementations, it's safe to say that compiler optimizations that improve speed or reduce space tend to reduce the amount of information available to the debugger. You can change these optimizations while debugging your program:

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

If you execute this before compiling your program, the debugger should be able to give you more useful information. You should consult your vendor's documentation to learn about additional implementation-specific controls. If your Lisp system gives you a choice between using a compiler and using an interpreter, you'll probably find that the interpreter causes the debugger to give you better information.

Simple debugging tools

If your program runs to completion but produces incorrect results, or if it runs but fails to terminate, then you'll need some additional tools. The first of these tools should be familiar to all programmers: insert a call to the debugger or (more commonly) insert a print statement.

BREAK, PRINT

BREAK causes your program to call the debugger. Once inside the debugger you can examine the call stack. Most debuggers also allow you to examine values local to each active function on the call stack; by looking at these values at a critical point during your program's execution, you may find an important clue as to why your program malfunctions.

The debugger will allow you to continue from a break. You may find it helpful -- if you don't yet understand the cause of a problem -- to correct one or more wrong values before continuing; with other BREAK forms inserted at key points in your program, this strategy may lead you to a place where the error is apparent.

Of course, you can always insert PRINT forms at key locations in your program and examine the resulting output. In Lisp, this is most useful when you need to get a feel for what's happening deep inside some function. For example, you might have a complex calculation to determine whether a sequence of code is executed or not. A PRINT can tell you as the program runs.

Don't forget that you can use FORMAT to print the values of several variables together with explanatory text. And with either PRINT or FORMAT, be careful that you do not change the meaning of the code by inserting the debugging statement. Remember that some flow-control forms (e.g. IF and UNWIND-PROTECT) expect a single form at certain places. Also beware of wrapping PRINT around a value-returning form; this won't work if the value-receiving form expects multiple values.

Power tools for tough problems

Lisp provides additional debugging tools to help you observe the dynamic behavior of your program.

TRACE, STEP, ADVISE, WATCH

TRACE allows you to observe each call and return from a specific function, no matter where the function appears in your program. To trace a function, invoke TRACE with the name of the function. You can do this for as many functions as needed. You can also pass several function names to TRACE.

When your program runs a traced function, it will print the name of the function on entry and exit. Most TRACE implementations will also print the function arguments on entry and returned values on exit.

To discontinue tracing of a function, pass its name to UNTRACE. To discontinue tracing of all traced functions, evaluate (UNTRACE).

See Chapter 16 for an example of TRACE.

STEP allows you to interactively control evaluation of an expression. If you step a function invocation, you should be able to examine each subform of the function's definition just before it is evaluated. STEP implementations vary widely, so you should consult your vendor's documentation for further details. In general, the same optimizations and controls that aid the debugger will also aid the stepper.

STEP is a very labor-intensive way to debug a program, since you must tell its user interface to evaluate each subform. This is reasonable for straight-line code, but quickly becomes tedious in the presence of looping or recursion.

Some Lisp implementations provide two additional tools, ADVISE and WATCH, that can be of use during debugging.

ADVISE modifies a function without changing its source code. ADVISE can usually examine the advised function's arguments, execute its own code, execute the advised function, examine the advised function's return values, and modify the returned values. For debugging purposes, ADVISE can be used to implement conditional BREAKs and TRACEs, or to temporarily patch incorrect behavior in one part of a program while you're debugging another part.

WATCH lets you specify variables to be displayed as your program executes. This is normally available only in Lisp implementations that provide a windowed user interface. Because of issues of variable scope and display update timing and overhead, WATCH is of limited value. Most Lisp implementations do not provide this tool.

Into the belly of the beast

As you debug your program, you may need to see the internal details of composite objects such as lists, structures, arrays, streams and CLOS instances. Lisp lets you do this whether the data has been defined by your program or by the Lisp runtime system.

INSPECT, DESCRIBE

DESCRIBE is a function that accepts any object as an argument and prints a description of that object. The form and content of the description may vary among Lisp implementations. DESCRIBE accepts an output stream as an optional second argument.

INSPECT is an interactive version of DESCRIBE. This is most useful for examining complex objects by "drilling down" into the implementation details of enclosed data elements.

Continuing from an error

When faced with the debugger, you will have a choice of restart actions depending upon how the error was signalled. ERROR requires that you abandon your program's executions. However, many internal Lisp functions use CERROR, which gives you a chance to continue from an error.

In most debuggers, you can do quite a few useful things before continuing from an error:

Problems with unwanted definitions

Unwanted definitions are not usually a problem in a Lisp program. You can get rid of function definitions using FMAKUNBOUND, variable values with MAKUNBOUND, and even symbols with UNINTERN. In practice, there's usually no need to use any of these; available memory is commonly large compared to the size of a few misdefined variables or functions, and they will be eliminated anyway the next time you restart your Lisp image and load your program.

Method definitions are an entirely different matter. Remember that methods must have congruent argument lists; if you change your mind during program development about a method's argument list -- perhaps you thought that it needed two arguments at first but then realized three arguments are really needed -- then you'll have to remove the old method definition before adding the new one. Some Lisp environments facilitate this redefinition:

? (defmethod baz (a b))
#<STANDARD-METHOD BAZ (T T)>
? (defmethod baz (a b c))
Error: Incompatible lambda list in #<STANDARD-METHOD BAZ (T T T)>
       for #<STANDARD-GENERIC-FUNCTION BAZ #x3D2CB66>.
Restart options:
  1. Remove 1 method from the generic-function and change its lambda list
  2. Top levl
  ? 

If you simply add a method that you later decide is no longer wanted, you'll need a way to remove the method. The least desirable technique is to restart your Lisp system and reload your program without the unwanted definition. Another approach, provided by some vendors, is to interactively remove the definition using a special editor command or a method browser. Failing all else, you can remove the method manually using REMOVE-METHOD:

(let* ((generic-function (symbol-function 'gf-name))
       (method (find-method generic-function
                            '(method-specializers)
                            (list (find-class parameter-class-name)
                                  ; one per argument 
                                  ...))))
  (remove-method generic-function method))

where gf-name is the name of the generic function (i.e. the name of the method), method-specializers is either empty or a method combination specifier, such as :BEFORE, :AFTER, or :AROUND, and parameter-class-name is the name of the class on which a particular method parameter is specialized.

Package problems; method definitions

Symbol conflicts across packages can be frustrating during development. If you have defined multiple packages for your program, you'll need to be careful to set the proper package (using IN-PACKAGE) before defining an object intended for that package. If you inadvertently create an object in the wrong package and then attempt to define it in the correct package, Lisp will signal an error if there is a "uses" relationship between the two packages. The proper response is to first remove the erroneous definition using UNINTERN.

You can also get into trouble with packages by having unexported classes defined in two packages and specializing a method based on the wrong class.

The problem with macros

Macros must always be defined before use. This is especially important when you redefine a macro during development: every piece of code that uses the redefined macro must be recompiled. You can help yourself avoid macro redefinition problems by reloading your source code after redefining any macro(s).

Runtime tests catch "can't happen cases" when they do...

When I read code, finding the phrase "can't happen" in a comment always raises a red flag. Usually, this statement is made after the programmer has examined the code's execution environment and intended use. Unfortunately, things change and "can't happen" cases do happen.

Lisp provides a very handy facility for checking "can't happen" statements at runtime. The ASSERT macro expects a form that will evaluate to true at runtime. If the form evaluates to NIL instead, ASSERT signals a continuable error, transferring control to the debugger. At the very least, this will help you to learn which assertion was violated so you can correct your program.

ASSERT accepts an optional list of value places that the user can interactively change to satisfy the assertion.

? (defun add-2 (n)
    (assert (numberp n) (n))
    (+ 2 n))
? (add-2 3)
5
? (add-2 'foo)
Error: Failed assertion (NUMBERP N)
Restart options:
  1. Change the values of some places, then retry the assertion
  2. Top level
  ? 1
  Value for N: 4
6
See Chapter 23 for additional information about ASSERT and other error detection and recovery techniques.

Use method dispatch rather than case dispatch

When your program needs to make a decision based on the type of an object, you have two choices. You can use TYPECASE or DEFMETHOD. Unless you have a circumstance that particularly warrants the use of TYPECASE (or its variants CTYPECASE and ETYPECASE) -- and especially if the set of types will change during normal program evolution or maintenance -- you should probably construct your program to operate on the individual types via generic functions. This more clearly exposes the intent of the program and eliminates the likelihood that you will forget to update a TYPECASE form during maintenance.


Contents | Cover
Chapter 29 | Chapter 31
Copyright © 1995-1999, David B. Lamkins
All Rights Reserved Worldwide

This book may not be reproduced without the written consent of its author. Online distribution is restricted to the author's site.