Go to the first, previous, next, last section, table of contents.

Debugging

This chapter is out of date and currently under revision.

This chapter is adapted from Don't Panic: A 6.001 User's Guide to the Chipmunk System, by Arthur A. Gleckler.

Even computer software that has been planned carefully and written well may not always work correctly. Mysterious creatures called bugs may creep in and wreak havoc, leaving the programmer to clean up the mess. Some have theorized that a program fails only because its author made a mistake, but experienced computer programmers know that bugs are always to blame. This is why the task of fixing broken computer software is called debugging.

It is impossible to prove the correctness of any non-trivial program; hence the Cynic's First Law of Debugging:

Programs don't become more reliable as they are debugged; the bugs just get harder to find.

Scheme is equipped with a variety of special software for finding and removing bugs. The debugging tools include facilities for tracing a program's use of specified procedures, for examining Scheme environments, and for setting breakpoints, places where the program will pause for inspection.

Many bugs are detected when programs try to do something which is impossible, like adding a number to a symbol, or using a variable which does not exist; this type of mistake is called an error. Whenever an error occurs, Scheme prints an error message and starts a new REPL. For example, using a nonexistent variable foo will cause Scheme to respond

1 ]=> foo

;Unbound variable: foo
;To continue, call RESTART with an option number:
; (RESTART 3) => Specify a value to use instead of foo.
; (RESTART 2) => Define foo to a given value.
; (RESTART 1) => Return to read-eval-print level 1.

2 error> 

Sometimes, a bug will never cause an error, but will still cause the program to operate incorrectly. For instance,

(prime? 7)   =>   #f

In this situation, Scheme does not know that the program is misbehaving. The programmer must notice the problem and, if necessary, start the debugging tools manually.

There are several approaches to finding bugs in a Scheme program:

Only experience can teach how to debug programs, so be sure to experiment with all these approaches while doing your own debugging. Planning ahead is the best way to ward off bugs, but when bugs do appear, be prepared to attack them with all the tools available.

Subproblems and Reductions

Understanding the concepts of reduction and subproblem is essential to good use of the debugging tools. The Scheme interpreter evaluates an expression by reducing it to a simpler expression. In general, Scheme's evaluation rules designate that evaluation proceeds from one expression to the next by either starting to work on a subexpression of the given expression, or by reducing the entire expression to a new (simpler, or reduced) form. Thus, a history of the successive forms processed during the evaluation of an expression will show a sequence of subproblems, where each subproblem may consist of a sequence of reductions.

For example, both (+ 5 6) and (+ 7 9) are subproblems of the following combination:

(* (+ 5 6) (+ 7 9))

If (prime? n) is true, then (cons 'prime n) is a reduction for the following expression:

(if (prime? n)
    (cons 'prime n)
    (cons 'not-prime n))

This is because the entire subproblem of the if expression can be reduced to the problem (cons 'prime n), once we know that (prime? n) is true; the (cons 'not-prime n) can be ignored, because it will never be needed. On the other hand, if (prime? n) were false, then (cons 'not-prime n) would be the reduction for the if expression.

The subproblem level is a number representing how far back in the history of the current computation a particular evaluation is. Consider factorial:

(define (factorial n)
  (if (< n 2)
      1
      (* n (factorial (- n 1)))))

If we stop factorial in the middle of evaluating (- n 1), the (- n 1) is at subproblem level 0. Following the history of the computation "upwards," (factorial (- n 1)) is at subproblem level 1, and (* n (factorial (- n 1))) is at subproblem level 2. These expressions all have reduction number 0. Continuing upwards, the if expression has reduction number 1.

Moving backwards in the history of a computation, subproblem levels and reduction numbers increase, starting from zero at the expression currently being evaluated. Reduction numbers increase until the next subproblem, where they start over at zero. The best way to get a feel for subproblem levels and reduction numbers is to experiment with the debugging tools, especially debug.

The Debugger

There are three debuggers available with MIT Scheme. Two of them require and run under Edwin, and are described in that section of this document (see section Edwin). The third is command oriented, does not require Edwin, and is described here.

The debugger, called debug, is the tool you should use when Scheme signals an error and you want to find out what caused the error. When Scheme signals an error, it records all the information necessary to continue running the Scheme program that caused the error; the debugger provides you with the means to inspect this information. For this reason, the debugger is sometimes called a continuation browser.

Here is the transcript of a typical Scheme session, showing a user evaluating the expression (fib 10), Scheme responding with an unbound variable error for the variable fob, and the user starting the debugger:

1 ]=> (fib 10)

;Unbound variable: fob
;To continue, call RESTART with an option number:
; (RESTART 3) => Specify a value to use instead of fob.
; (RESTART 2) => Define fob to a given value.
; (RESTART 1) => Return to read-eval-print level 1.

2 error> (debug)

There are 6 subproblems on the stack.

Subproblem level: 0 (this is the lowest subproblem level)
Expression (from stack):
    fob
Environment created by the procedure: FIB
 applied to: (10)
The execution history for this subproblem contains 1 reduction.
You are now in the debugger.  Type q to quit, ? for commands.

3 debug> 

This tells us that the error occurred while trying to evaluate the expression fob while running (fib 10). It also tells us this is subproblem level 0, the first of 8 subproblems that are available for us to examine. The expression shown is marked "(from stack)", which tells us that this expression was reconstructed from the interpreter's internal data structures. Another source of information is the execution history, which keeps a record of expressions evaluated by the interpreter. The debugger informs us that the execution history has recorded some information for this subproblem, specifically a description of one reduction.

Debugging Aids

An important step in debugging is to locate the piece of code from which the error is signalled. The Scheme debugger contains a history examiner and an environment examiner to aid the user in locating a bug.

special form+: bkpt message irritant
Sets a breakpoint. When the breakpoint is encountered, message and irritant are typed and a read-eval-print loop is entered in the current environment. To exit from the breakpoint and proceed with the interrupted process, call the procedure proceed. Sample usage:

1 ]=> (begin (write-line 'foo)
             (bkpt 'test-2 'test-3)
             (write-line 'bar)
             'done)

foo
 test-2 test-3
;To continue, call RESTART with an option number:
; (RESTART 2) => Return from BKPT.
; (RESTART 1) => Return to read-eval-print level 1.

2 bkpt> (+ 3 3)

;Value: 6

2 bkpt> (proceed)

bar
;Value: done

procedure+: where [obj]
The procedure where enters the environment examination system. This allows environments and variable bindings to be examined and modified. where accepts one letter commands. The commands can be found by typing ? to the `where>' prompt. The optional argument, obj, is an object with an environment associated with it: an environment, a procedure, or a promise. If obj is omitted, the environment examined is the read-eval-print environment from which where was called (or an error or breakpoint environment if called from the debugger). If a compound procedure is supplied, where lets the user examine the environment of definition of the procedure. This is useful for debugging procedure arguments and values.

Advising Procedures

Giving advice to procedures is a powerful debugging technique. trace and break are useful examples of advice-giving procedures. Note that the advice system only works for interpreted procedures.

procedure+: trace-entry proc
Causes an informative message to be printed whenever the procedure proc is entered. The message is of the form

[Entering #[compound-procedure 1 foo]
    Args: val1
          val2
          ...]

where val1, val2 etc. are the evaluated arguments supplied to the procedure.

procedure+: trace-exit proc
Causes and informative message to be printed when procedure proc terminates. The message contains the procedure, its argument values, and the value returned by the procedure.

procedure+: trace-both proc
procedure+: trace proc
trace-both is the same as calling both trace-entry and trace-exit on proc. trace is the same as trace-both.

procedure+: untrace-entry [proc]
untrace-entry stops tracing the entry of proc. If proc is not given, the default is to stop tracing the entry of all entry-traced procedures.

procedure+: untrace-exit [proc]
Stops tracing the exit of proc. If proc is not included, the default is all exit-traced procedures.

procedure+: untrace [proc]
Stops tracing both the entry to and the exit from proc. If proc is not given, the default is all traced procedures.

procedure+: break-entry proc
Like trace-entry with the additional effect that a breakpoint is entered when procedure proc is invoked. Both proc and its arguments can be accessed by calling the procedures *proc* and *args*, respectively. Use restart or proceed to continue from a breakpoint.

procedure+: break-exit proc
Like trace-exit, except that a breakpoint is entered just prior to leaving the procedure proc. Proc, its arguments, and the result can be accessed by calling the procedures *proc*, *args*, and *result*, respectively. Use restart or proceed to continue from a breakpoint.

procedure+: break-both proc
procedure+: break proc
Sets a breakpoint at the beginning and end of proc. This is break-entry and break-exit combined.

procedure+: unbreak [proc]
Discontinues the entering of a breakpoint on the entry to and exit from the procedure proc. If proc is not given, the default is all breakpointed procedures.

procedure+: unbreak-entry [proc]
Discontinues the entering of a breakpoint on the entry to the procedure proc. If proc is not given, the default is all entry-breakpointed procedures.

procedure+: unbreak-exit [proc]
Discontinues the entering of a breakpoint on the exit from the procedure proc. If proc is not given, the default is all exit-breakpointed procedures.

procedure+: advise-entry proc advice
General entry-advising procedure. trace-entry and break-entry are examples of entry-advising procedures. advise-entry gives advice to proc. When proc is invoked, advice is passed three arguments: proc, a list of proc's argument values, and the current environment.

procedure+: advise-exit proc advice
The general exit-advising procedure. trace-exit and break-exit are examples of exit-advising procedures. Advice is a procedure that should accept four arguments: proc, its argument values, the result computed by proc, and the current environment. Advice is responsible for returning a value on behalf of proc. That is, the value returned by advice is the value returned by the advised procedure.

procedure+: advice proc
Returned the advice, if any, given to proc.

procedure+: unadvise-entry [proc]
Removes entry advice from proc. If proc is not given, the default is all entry-advised procedures.

procedure+: unadvise-exit [proc]
Removes exit advice from proc. If proc is not given, the default is all exit-advised procedures.

procedure+: unadvise [proc]
Removes all advice from proc. This is a combination of unadvise-entry and unadvise-exit. If proc is not given, the default is all advised procedures.

procedure+: *proc*
*proc* is a procedure which returns as its value the "broken" procedure. It is used only at a breakpoint set by the procedures break-exit and break-entry or procedures defined in terms of these procedures (like break-both and break).

procedure+: *args*
*args* is a procedure which returns as its value a list of the arguments supplied to the "broken" procedure. It is used only at a breakpoint set by the procedures break-exit and break-entry or procedures defined in terms of these procedures (like break-both and break).

procedure+: *result*
*result* is a procedure which returns as its value the result that was about to be returned by the "broken" procedure. It is used only at a breakpoint set by the procedure break-exit or procedures defined in terms of this procedure (like break-both and break).


Go to the first, previous, next, last section, table of contents.