Computer programmers use the word bug for an error in a computer program. Once in a while, a bug is caused by a defect in the hardware of a computer, but more often bugs result from mistakes in the specification, design, or composition of the programs themselves. In such cases, the computer is doing exactly what it was told to do, but the programmer told it to do the wrong thing.
Ideally, such mistakes would never occur in the first place, and the interactive design of Scheme helps to prevent them by encouraging a programming style in which each new user-defined procedure is tested thoroughly before it is added to a program. It is usually straightforward to perform a large variety of tests on a new procedure and to confirm that each test has the desired result. Often it is useful to copy the invocations that you use to test a procedure into the comment that precedes and documents the source code for that procedure, so that they can be re-run easily if the procedure is later modified or adapted for use in a different context.
Even so, programmers are human, and it is not uncommon for a bug somehow to pass through the testing process without being found and corrected, in which case it may be discovered only at a much later stage, when discrepancies are detected in a large program that may include dozens or hundreds of other user-defined procedures. Moreover, even when the programmer discovers during testing that something is wrong with a procedure he has written, it is not always obvious whether the problem lies in the new procedure or in some previously written procedure that it invokes, so finding and correcting a bug is sometimes quite laborious.
Novice programmers sometimes approach the task of finding and correcting a bug by trial and error, making successive small changes in the source code (``tweaking'' it), and reloading and re-testing it after each change, without giving much thought to the probable cause of the bug or to how making the change will affect its operation. This approach to debugging is ineffective, for two reasons:
Tweaking is time-consuming. Novice programmers tend to have a naive confidence that the next small change in the source code, whatever it is, will fix the problem. This is seldom the case. If you detect a bug in a procedure, and the first tweak doesn't fix it, the next twelve tweaks probably won't either -- so don't bother with them. Push yourself away from the keyboard and study the context. Don't make even one more change in the source code until you're ready to test a well-thought-out hypothesis about the cause of the error. (This is also a good time to make a separate copy of the procedure, in XEmacs, so that you can backtrack to the current version if subsequent experimentation requires extensive temporary rewriting.)
Tweaking usually fixes only a specific, local problem. Very often a bug is a symptom of a general misunderstanding on the part of the programmer, one that affects the operation of the procedure in cases other than the one being tested. Unless you address this general problem, tweaking a procedure in such a way that it passes the particular test that it formerly failed is likely to make your program worse instead of better.
In the course of the semester, we'll explore several alternatives to this approach to debugging. Today we're going to look at a ``software tool'' that the designer of Chez Scheme provided as an aid to debugging: the Chez Scheme interactive inspector. The point of using this tool is to collect more information about the context in which an error has occurred, so that one's hypotheses about its cause are more likely to be correct. In other words, the interactive inspector does not fix bugs, but rather tries to put you in a better position to fix them.
You may have noticed that when the Chez Scheme interactive interface detects an error, it prints out a message that ends with the line
Type (debug) to enter the debugger.
What this means is that Chez Scheme has saved a lot of information about the error and its context and is inviting you to investigate. If you decline this invitation, Chez Scheme will eventually discard the information and re-use the part of the computer's memory in which it was stored. If you accept it, here's what will happen:
> (debug) debug>
Here the prompt debug> indicates that your next command will be read and processed by the Chez Scheme debugger rather than by the usual interactive interface. The debugger has by comparison a very limited repertoire of abilities, and we'll investigate only three possibilities:
If you respond to the debug> prompt by typing the letter r (and pressing the <Enter> key), the debugger will shut itself down and return you to the usual interactive interface. This is how you usually leave the debugger.
If you type a and press <Enter>, both the debugger and Chez Scheme shut down completely. (This is probably not what you want; I mention this possibility only so that you won't be baffled if you do it by mistake.)
If you type i and press <Enter>, you'll start up the Chez Scheme inspector, which is the tool mentioned above that allows you to scrutinize the context of the error.
Here's what the inspector looks like when it starts up:
debug> i #<system continuation in error> :
It presents you with the not-very-enlightening statement that when the error was reported, Chez Scheme was executing a procedure named error and was about to continue by restarting the ``system'' -- generating a new prompt and waiting for more input. The colon near the right side of the window is the inspector's prompt; the inspector is waiting for instructions on what you'd like to see at this point.
The inspector arranges the information about the context of the error into frames, one frame for each procedure that had been invoked but had not yet completed its work when the error occurred. The inspector command depth will tell you how many such frames there are, and sf (``show frames'') will give you a list of them.
Let's take a particular example. Start up Chez Scheme and give it the following procedure definition:
(define quot-and-rem
(lambda (dividend divisor)
(list (quotient dividend divisor)
(remainder dividend divisor))))
This procedure takes two arguments, both integers, and divides the first one by the second; it returns a list of two integers, the quotient and the remainder resulting from the division. Here it is in action:
> (quot-and-rem 38 5) (7 3) > (quot-and-rem -110 12) (-9 -2) > (quot-and-rem 53 0) Error in quotient: undefined for 0. Type (debug) to enter the debugger.
No problem with the first two examples, but in the third one we indirectly asked for a division by zero and got an error message.
Reproduce the third procedure call above in your running Chez Scheme to get the same error message.
Start the Chez Scheme debugger; invoke the Chez Scheme inspector and ask to see the list of frames.
If you do exercise 2 correctly, you should see the following list, which should be read from the bottom up:
0: #<system continuation in error> 1: #<system continuation in quotient> 2: #<continuation in quot-and-rem> 3: #<top level continuation>
At the ``top level,'' we typed in a call to quot-and-rem; that
procedure was invoked but never returned, because the error occurred before
the value was computed. The quot-and-rem procedure invoked
quotient to get the first item for the list; the
quotient procedure also never returned, because it couldn't
complete a division by zero. Instead, it invoked the error
procedure, which stored all this information away, printed out the
appropriate error message, and turned things back over to the Chez Scheme
interactive interface; technically, error hasn't returned yet
either.
In this case, there's no question about what caused the error, but let's
look at what other information Chez Scheme can give us about its context.
Since the culprit seems to have been the quotient procedure,
which is one frame down from ``system continuation in error,''
let's look at that frame.
To descend to that frame, type the inspector command d (``down'') at the inspector prompt:
#<system continuation in error> : d #<system continuation in quotient> :
Then type s (``show'') to get the information stored in this frame:
#<system continuation in quotient> : s continuation: #<continuation in quot-and-rem> free variables: 0: 0 1: 53 2: #<system procedure quotient>
The ``continuation'' entry identifies the frame to which the program
intended to return after completing the call to the procedure described in
the current frame. (It describes how the program would have
continued if the error had not occurred.) The list of ``free
variables'' gives you the values of the parameters for the procedure
invocation, starting at the right end: parameter #0, which is
divisor, had the value 0, and parameter #1, which is
dividend, had the value 53. The value listed here as #2 is
the procedure that was invoked. Remember that in Scheme a procedure is
also a datum stored in a variable; in this case it is a ``system
procedure'' (that is, a predefined procedure of Chez Scheme) stored in the
variable quotient.
Descend to the next frame and look at the information stored about the
call to the quot-and-rem procedure:
#<system continuation in quotient> : d #<continuation in quot-and-rem> : s continuation: #<top level continuation> procedure code: (lambda (#:dividend #:divisor) (list (...) ...)) call code: (quotient #:dividend #:divisor) free variables: 0. divisor: 0 1. dividend: 53
This time the ``continuation'' entry tells you that
quot-and-rem was invoked directly from the top level. Since
quot-and-rem is a user-defined procedure rather than a
built-in one, the inspector can show you a version of the
lambda-expression used to define it. Because that
lambda-expression was too long to fit on a single line, the inspector
abbreviated it by substituting three dots for some of the subexpressions.
You can ignore the #: prefix on the parameters; this is just a
mechanism that Chez Scheme uses internally to make sure that parameters
don't get mixed up with any similarly named predefined variables.
The ``call code'' entry reproduces the part of the
quot-and-rem procedure in which the error occurred; as we
already know, it occurred inside the call to quotient.
Finally, the ``free variables'' entry lists the values of the parameters of
the quot-and-rem procedure. This time the names of the
parameters are known to the inspector (frames for calls to user-defined
procedures contain a lot more information that frames for calls to
predefined procedures for technical reasons), and the procedure itself is
not listed in this entry because it was fully described in the ``procedure
code'' entry.
The inspector command u (``up'') can be used to move up to the next higher frame if you want to review it, and t (``top'') can always be used to return to the top frame, the one at which you entered the inspector.
The inspector provides concise on-line help, in the form of two lists of commands, either of which it will reprint on request. If you type a question mark at the inspector prompt, it displays a list of commonly used inspector commands that are available in the current context. If you type ?? -- two question marks in a row -- it displays a list of inspector commands that are always available in any context while the inspector is running.
Use the ? and ?? commands now to view the on-line help.
Exit from the inspector by typing q (``quit'') and from the debugger by typing r (``reset'').
The file /home/stone/courses/scheme/debugging-example.ss contains an intentionally incorrect program. Let's try to find and correct the bugs.
Start XEmacs and open a new file called multi-juggle.ss. Use XEmacs's Insert File operation to insert a copy of /home/stone/courses/scheme/debugging-example.ss into this new file. Read the file and save it.
Start Chez Scheme and load multi-juggle.ss into it. Try the
procedure call (multi-juggle '(jump quick spot)) and notice
that an error occurs before any value is returned.
Start the inspector and examine the context in which the error occurred. Does this give you any additional information about the nature and cause of the error?
Fix the error by editing the program in the XEmacs window, save and reload
multi-juggle.ss, and test it again. The program actually contains
more than one bug, so you may find that the multi-juggle
procedure still doesn't return the correct value. (Unfortunately, it is
commonplace in programming to find that one bug conceals the existence of
another.) Study the program and either revise it until it is correct or
discard it and write different procedures to do the same job. The latter
is often the best course of action when bugs seem especially persistent and
elusive.
This document is available on the World Wide Web as
http://www.math.grin.edu/courses/Scheme/spring-1998/debugging-in-Chez-Scheme.html
created February 2, 1997
last revised June 21, 1998
Henry Walker (walker@math.grin.edu) and John David Stone (stone@math.grin.edu)