[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

5. Debugging

Parts of this chapter are 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 carefully planned and well written 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 that is impossible, like adding a number to a symbol, or using a variable that 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.

5.1 Subproblems and Reductions  
5.2 The Command-Line Debugger  
5.3 Debugging Aids  
5.4 Advising Procedures  


[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

5.1 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.


[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

5.2 The Command-Line Debugger

There are two debuggers available with MIT Scheme. One of them runs under Edwin, and is described in that section of this document (see section 7.6 The Edwin Debugger). The other is command-line oriented, does not require Edwin, and is described here.

The command-line 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 6 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.

What follows is a description of the commands available in the debugger. To understand how the debugger works, you need to understand that the debugger has an implicit state that is examined and modified by commands. The state consists of three pieces of information: a subproblem, a reduction, and an environment frame. Each of these parts of the implicit state is said to be selected; thus one refers to the selected subproblem, and so forth. The debugger provides commands that examine the selected state, and allow you to select different states.

Here are the debugger commands. Each of these commands consists of a single letter, which is to be typed by itself at the debugger prompt. It is not necessary to type RET after these commands.

Traversing subproblems
The debugger has several commands for traversing the structure of the continuation. It is useful to think of the continuation as a two-dimensional structure: a backbone consisting of subproblems, and associated ribs consisting of reductions. The bottom of the backbone is the most recent point in time; that is where the debugger is positioned when it starts. Each subproblem is numbered, with 0 representing the most recent time point, and ascending integers numbering older time points. The u command moves up to older points in time, and the d command moves down to newer points in time. The g command allows you to select a subproblem by number, and the h command will show you a brief summary of all of the subproblems.

Traversing reductions
If the subproblem description says that `The execution history for this subproblem contains N reductions', then there is a "rib" of reductions for this subproblem. You can see a summary of the reductions for this subproblem using the r command. You can move to the next reduction using the b command; this moves you to the next older reduction. The f command moves in the opposite direction, to newer reductions. If you are at the oldest reduction for a given subproblem and use the b command, you will move to the next older subproblem. Likewise, if you are at the newest reduction and use f, you'll move to the next newer subproblem.

Examining subproblems and reductions
The following commands will show you additional information about the currently selected subproblem or reduction. The t command will reprint the standard description (in case it has scrolled off the screen). The l command will pretty-print (using pp) the subproblem's expression.

Traversing environments
Nearly all subproblems and all reductions have associated environments. Selecting a subproblem or reduction also selects the associated environment. However, environments are structured as a sequence of frames, where each frame corresponds to a block of environment variables, as bound by lambda or let. These frames collectively represent the block structure of a given environment.

Once an environment frame is selected by the debugger, it is possible to select the parent frame of that frame (in other words, the enclosing block) using the p command. You can subsequently return to the original child frame using the s command. The s command works because the p command keeps track of the frames that you step through as you move up the environment hierarchy; the s command just retraces the path of saved frames. Note that selecting a frame using p or s will print the bindings of the newly selected frame.

Examining environments
The following commands allow you to examine the contents of the selected frame. The c command prints the bindings of the current frame. The a command prints the bindings of the current frame and each of its ancestor frames. The e command enters a read-eval-print loop in the selected environment frame; expressions typed at that REPL will be evaluated in the selected environment. To exit the REPL and return to the debugger, evaluate (abort->previous) or use restart. The v command prompts for a single expression and evaluates it in the selected environment. The w command invokes the environment inspector (where); quitting the environment inspector returns to the debugger. Finally, the o command pretty-prints the procedure that was called to create the selected environment frame.

Continuing the computation
There are three commands that can be used to restart the computation that you are examining. The first is the k command, which shows the currently active restarts, prompts you to select one, and passes control to the it. It is very similar to evaluating `(restart)'.

The other two commands allow you to invoke internal continuations. This should not be done lightly; invoking an internal continuation can violate assumptions that the programmer made and cause unexpected results. Each of these commands works in the same way: it prompts you for an expression, which is evaluated in the selected environment to produce a value. The appropriate internal continuation is then invoked with that value as its sole argument. The two commands differ only in which internal continuation is to be invoked.

The j command invokes the continuation associated with the selected subproblem. What this means is as follows: when the description of a subproblem is printed, it consists of two parts, and "expression" and a "subproblem being executed". The latter is usually marked in the former by the specific character sequence `###'. The internal continuation of the subproblem is the code that is waiting for the "subproblem being executed" to return a value. So, in effect, you are telling the program what the "subproblem being executed" will evaluate to, and bypassing further execution of that code.

The z command is slightly different. It instead invokes the continuation that is waiting for the outer "expression" to finish. In other words, it is the same as invoking the j command in the next frame up. So you can think of this as an abbreviation for the u command followed by the j command.

Wizard commands
The m, x, and y commands are for Scheme wizards. They are used to debug the MIT Scheme implementation. If you want to find out what they do, read the source code.

Miscellaneous commands
The i command will reprint the error message for the error that was in effect immediately before the debugger started. The q command quits the debugger, returning to the caller. And the ? command prints a brief summary of the debugger's commands.


[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

5.3 Debugging Aids

This section describes additional commands that are useful for debugging.

procedure+: bkpt datum argument ...
Sets a breakpoint. When the breakpoint is encountered, datum and the arguments are typed (just as for error) and a read-eval-print loop is entered. The environment of the read-eval-print loop is derived by examining the continuation of the call to bkpt; if the call appears in a non-tail-recursive position, the environment will be that of the call site. To exit from the breakpoint and proceed with the interrupted process, call the procedure continue. 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> (continue)

bar
;Value: done

procedure+: pp object [output-port [as-code?]]
The pp procedure is described in section `Output Procedures' in MIT Scheme Reference Manual. However, since this is a very useful debugging tool, we also mention it here. pp provides two very useful functions:

  1. pp will print the source code of a given procedure. Often, when debugging, you will have a procedure object but will not know exactly what procedure it is. Printing the procedure using pp will show you the source code, which greatly aids identification.

  2. pp will print the fields of a record structure. If you have a compound object pointer, print it using pp to see the component fields, like this:

     
    (pp (->pathname "~"))
    -| #[pathname 14 "/usr/home/cph"]
    -| (host #[host 15])
    -| (device unspecific)
    -| (directory (absolute "usr" "home"))
    -| (name "cph")
    -| (type ())
    -| (version unspecific)
    

    When combined with use of the #@ syntax, pp provides the functionality of a simple object inspector. For example, let's look at the fields of the host object from the above example:

     
    (pp #@15)
    -| #[host 15]
    -| (type-index 0)
    -| (name ())
    

procedure+: pa procedure
pa prints the arguments of procedure. This can be used to remind yourself, for example, of the correct order of the arguments to a procedure.

 
for-all?
 => #[compiled-procedure 40 ("boole" #x6) #xC #x20ECB0]

(pa for-all?)
-| (items predicate)

(pp for-all?)
-|(named-lambda (for-all? items predicate)
-|  (let loop ((items items))
-|    (or (null? items)
-|        (and (predicate (car items))
-|             (loop (cdr items))))))

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 associated environment: 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 procedure is supplied, where lets the user examine the closing environment of the procedure. This is useful for debugging procedure arguments and values.

procedure+: apropos string [environment [search-parents?]]
Search an environment for bound names containing string and print out the matching bound names. If environment is specified, it must be an environment or package name, and it defaults to the current REPL environment. The flag search-parents? specifies whether the environment's parents should be included in the search. The default is #f if environment is specified, and #t if environment is not specified.

 
(apropos "search")
-| #[package 47 (user)]
-| #[package 48 ()]
-| list-search-negative
-| list-search-positive
-| nt-fs-flag/case-sensitive-search
-| re-string-search-backward
-| re-string-search-forward
-| re-substring-search-backward
-| re-substring-search-forward
-| search-ordered-subvector
-| search-ordered-vector
-| search-protection-list
-| string-search-all
-| string-search-backward
-| string-search-forward
-| substring-search-all
-| substring-search-backward
-| substring-search-forward
-| vector-binary-search


[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

5.4 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 procedure
Causes an informative message to be printed whenever procedure 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.

 
(trace-entry fib)
(fib 3)
-| [Entering #[compound-procedure 19 fib]
-|     Args: 3]
-| [Entering #[compound-procedure 19 fib]
-|     Args: 1]
-| [Entering #[compound-procedure 19 fib]
-|     Args: 2]
=> 3

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

 
(trace-exit fib)
(fib 3)
-| [1
-|       <== #[compound-procedure 19 fib]
-|     Args: 1]
-| [2
-|       <== #[compound-procedure 19 fib]
-|     Args: 2]
-| [3
-|       <== #[compound-procedure 19 fib]
-|     Args: 3]
=> 3

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

 
(trace-both fib)
(fib 3)
-| [Entering #[compound-procedure 19 fib]
-|     Args: 3]
-| [Entering #[compound-procedure 19 fib]
-|     Args: 1]
-| [1
-|       <== #[compound-procedure 19 fib]
-|     Args: 1]
-| [Entering #[compound-procedure 19 fib]
-|     Args: 2]
-| [2
-|       <== #[compound-procedure 19 fib]
-|     Args: 2]
-| [3
-|       <== #[compound-procedure 19 fib]
-|     Args: 3]
=> 3

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

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

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

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

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

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

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

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

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

The following three procedures are valid only within the dynamic extent of a breakpoint. In other words, don't call them unless you are stopped inside a breakpoint.

procedure+: *proc*
Returns the procedure in which the breakpoint has stopped.

procedure+: *args*
Returns the arguments to the procedure in which the breakpoint has stopped. The arguments are returned as a newly allocated list.

procedure+: *result*
Returns the result yielded by the procedure in which the breakpoint has stopped. This is valid only when in an exit breakpoint.

The following procedures install advice procedures that are called when the advised procedure is entered or exited. An entry-advice procedure must accept three arguments: the advised procedure, a list of the advised procedure's arguments, and the advised procedure's application environment (that is, the environment in which the procedure's formal parameters are bound). An exit-advice procedure must accept four arguments: the advised procedure, a list of the advised procedure's arguments, the result yielded by the advised procedure, and the advised procedure's application environment.

Note that the trace and breakpoint procedures described above are all implemented by means of the more general advice procedures, so removing advice from an advised procedure will also remove traces and breakpoints.

procedure+: advise-entry procedure advice
Advice must be an entry-advice procedure. Advice is attached to procedure, so that whenever procedure is entered, advice is called.

procedure+: advise-exit procedure advice
Advice must be an exit-advice procedure. Advice is attached to procedure, so that whenever procedure returns, advice is called.

procedure+: advice procedure
Returns the advice procedures, if any, that are attached to procedure. This is returned as a list of two lists: the first list is all of the entry-advice procedures attached to procedure, and the second is all of the exit-advice procedures.

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

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

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


[ << ] [ >> ]           [Top] [Contents] [Index] [ ? ]

This document was generated by Chris Hanson on July, 18 2001 using texi2html