Go to the first, previous, next, last section, table of contents.
This chapter is out of date and will be revised.
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 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.
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
.
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.
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.
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> (proceed) bar ;Value: done
pp
procedure is described in the MIT Scheme Reference Manual.
We just note here that, by default, MIT Scheme keeps debugging
information, so pp
is useful for looking at the source code of a
procedure (compiled or interpreted).
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.
1 ]=> for-all? ;Value 40: #[compiled-procedure 40 ("boole" #x6) #xC #x20ECB0] 1 ]=> (pa for-all?) (items predicate) ;No value 1 ]=> (pp for-all?) (named-lambda (for-all? items predicate) (let loop ((items items)) (or (null? items) (and (predicate (car items)) (loop (cdr items))))))
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.
#f
if package/env is specified, and #t
if
package/env is not specified.
1 ]=> (apropos "search") #[package 41 (user)] #[package 33 ()] list-search-negative list-search-positive search-ordered-subvector search-ordered-vector vector-binary-search ;No value
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.
[Entering #[compound-procedure 1 foo] Args: val1 val2 ...]
where val1, val2 etc. are the evaluated arguments supplied to the procedure.
trace-entry
and trace-exit
on
procedure. trace
is the same as trace-both
.
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.
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.
break-entry
and break-exit
combined.
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.
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.
unadvise-entry
and unadvise-exit
. If
procedure is not given, the default is all advised procedures.
Go to the first, previous, next, last section, table of contents.