Debugging in Chez Scheme

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:

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:

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.


Exercise 1

Reproduce the third procedure call above in your running Chez Scheme to get the same error message.


Exercise 2

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.


Exercise 3

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.


Exercise 4

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.


Exercise 5

Use the ? and ?? commands now to view the on-line help.


Exercise 6

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.


Exercise 7

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.


Exercise 8

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.


Exercise 9

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?


Exercise 10

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/~stone/courses/scheme/debugging-in-Chez-Scheme.html

created February 2, 1997
last revised May 31, 1998

Henry Walker (walker@math.grin.edu) and John David Stone (stone@math.grin.edu)