DEBUGGING WITH CHEZ SCHEME by Gary T. Leavens Department of Computer Science, Iowa State University August 14, 1993 This document describes how to use the debugging facilities of Chez Scheme. More details can be found in the ``Chez Scheme System Manual'' (revision 2.2, Feb. 1992) which is on reserve at the library, and from which this material is drawn. Personally, I rarely use these facilities. I find it more helpful to hand-simulate my program, or to think about what it is doing instead of using the debugger. When necessary, I usually add some writeln statements as described in section 2.5 of Scheme and the Art of Programming. You can do this too, as it saves you from the trouble of learning the Chez Scheme debugger. Besides avoiding complications, you may note that the technique of adding writeln statements is language independent (it will work for C++, Pascal, etc. too), but the Chez Scheme debugger is specific to Chez Scheme. However, as always, more powerful tools are often useful... 1. WHAT HAPPENED? Consider the following (buggy) Scheme procedure. (define member? (lambda (item ls) (or (member? item (cdr ls)) (equal? (car ls) item)))) Let's try it out. > (member? 3 '(1 2 3)) Error in cdr: () is not a pair. Type (debug) to enter the debugger. Here's the basic thing a debugger should do for you, give you information about what happened. First, of course, look at the error message, it already tells me what happened, and at some point to fix the bug you will have to make the same deduction: using the program to explain how the error could have occurred. But more likely you'll need more information. 1.1 TRACING An extremely helpful facility is Chez Scheme's trace procedure. Let's try it. > (trace member?) (member?) > (member? 3 '(1 2 3)) (member? 3 (1 2 3)) | (member? 3 (2 3)) | | (member? 3 (3)) | | | (member? 3 ()) Error in cdr: () is not a pair. Type (debug) to enter the debugger. The first call, (trace member?) told Chez Scheme to trace all calls to the procedure member?, which was the argument of trace. Next we called (member? 3 '(1 2 3)), which starts running, and shows us the trace. The tracing first shows us the call again: (member? 3 (1 2 3)) but note that the list (1 2 3) is not quoted. What you are seeing is the value of the second argument to member?, which is (1 2 3). To be clearer, consider the following. > (define test-list '(1 2 3)) > test-list (1 2 3) > (member? 3 test-list) (member? 3 (1 2 3)) | (member? 3 (2 3)) | | (member? 3 (3)) | | | (member? 3 ()) Error in cdr: () is not a pair. Type (debug) to enter the debugger. See how the value of test-list is shown in the first line of the trace? (member? 3 (1 2 3)) Now let's look at the next line. | (member? 3 (2 3)) It too shows a call to member, this time a recursive call. The nesting level is shown by the indentation and the number of vertical bars (|) to the left. This last call has a smaller list for its second argument, the list (2 3). Similarly, the next call has (3) as its second argument. And the last call before the error has () as its second argument. Exercise: can you explain what caused the error now? We can get still more information by tracing procedures that member? calls. For example, lets trace cdr. > (trace cdr) (cdr) > (define test-list '(1 2 3)) > (member? 3 test-list) (member? 3 (1 2 3)) | (cdr (1 2 3)) | (2 3) | (member? 3 (2 3)) | | (cdr (2 3)) | | (3) | | (member? 3 (3)) | | | (cdr (3)) | | | () | | | (member? 3 ()) | | | | (cdr ()) Error in cdr: () is not a pair. Type (debug) to enter the debugger. It should be pretty clear what's happening now, so let's just explain a bit more of the trace output. The first 3 lines (member? 3 (1 2 3)) | (cdr (1 2 3)) | (2 3) show the first call to member?, and within member? a call to cdr, with argument (1 2 3). This call to cdr returns the list (2 3). The vertical bars on the left tell you that the call to (cdr (1 2 3)) happened ``inside'' the call of (member? 3 (1 2 3)); that is, after it the call of (member? 3 (1 2 3)) started and before it returned a result. Nothing interesting happened inside the call of cdr, so we just saw its result. If you are seeing too much happening (a common problem in debugging) you may want to stop tracing certain procedures. This can be done using untrace. > (untrace cdr) (cdr) > (member? 3 test-list) (member? 3 (1 2 3)) | (member? 3 (2 3)) | | (member? 3 (3)) | | | (member? 3 ()) Error in cdr: () is not a pair. Type (debug) to enter the debugger. You can untrace all traced procedures by calling untrace without any arguments. > (untrace) (member?) > (member? 3 test-list) Error in cdr: () is not a pair. Type (debug) to enter the debugger. Let's summarize trace and untrace (trace proc-name) turns on tracing of procedure named proc-name (untrace proc-name) turns off tracing of procedure proc-name (untrace) turns off all tracing 1.2 INSPECTING You'll notice that in most error messages, we are instructed to Type (debug) to enter the debugger. Let's see how to use the debugger to find out what happened. Our first task will be to try to get some of the information we got from the trace, namely how the procedure got to the point of the error, and what its arguments were. > (member? 3 test-list) Error in cdr: () is not a pair. Type (debug) to enter the debugger. > (debug) debug> ? Type e to exit interrupt handler and continue r to reset scheme a to abort scheme n to enter new cafe i to inspect current continuation s to display statistics debug> i # : In the above, I typed ``?'' (and a return) to see what my options were. (Typing ``?'', ``help'' or ``h'' is a generally useful tactic for getting Unix programs to give you some more help.) It turns out that we want to inspect the ``current continuation'' to find out what happened. The term ``continuation'' means the rest of the computation (see Chapter 16 of Scheme and the Art of Programming); that contains all information about where the computation is currently. After typing ``i'' and a return to the debugger, we enter the ``inspector'' which shows us some representation of what we are inspecting (#) and then gives us a prompt. The prompt is the colon (:) at the far right of the last line of the example above. To see where we are, we can use the inspector's command ``sf'' (which is short for show-frames). # : sf 0: # 1: # 2: # 3: # 4: # 5: # This shows the calls that are pending. Call 0 is where the computation is currently. We can see more about it by typing ``show'' at the inspector's prompt. # : show continuation: # free variables: 0: (()) 1: "~s is not a pair" 2: cdr 3: # Apparently this is the procedure (error) that prints error messages in Chez Scheme. Notice how variable 1 is something like our message (cdr is not a pair). Looking back to the output of ``sf'', it seems like numbers 1-4 are calls to member? that are pending. (Compare with the traces above.) Number 5 is the oldest; the top-level continuation is the Scheme read-eval-print loop that prompts you with ``>''. So, as will always be the case, number 1 has the most interest, as it is in our code. To see continuation number 1, we need to use the command ``down'' (which can be abbreviated (d). Once we are ``inspecting'' continuation 1, we can use ``show'' to see the code and its arguments. # : down # : show continuation: # procedure code: (lambda (item ls) ((...) (...))) call code: (cdr ls) free variables: 0. ls: () 1. item: 3 Here one can see that what is happening is a call to cdr with argument ls. The problem is that ls has value (), as can be seen from the line labeled 0. (The continuation is waiting to take the result of this call and do something with it.) We can see earlier continuations by doing ``down'' and ``show'' again. # : down # : show continuation: # procedure code: (lambda (item ls) ((...) (...))) call code: (member? item (cdr ls)) free variables: 0. ls: (3) 1. item: 3 There are a variety of other things that the inspector can do at this point, but you can use ``?'', ``??'' and ``help'' to find out the details. For now we have to leave. To exit the inspector, type ``q'' (and return), then type ``r'' (and return) to reset Scheme and exit the debugger. # : q debug> r > 2. HOW DO I STOP THIS LOOP? The ``r'' command of the debugger also comes in useful in another situation, getting back to Scheme after you have interrupted an infinite loop. Let's consider the following overly-simple infinite loop. > (define oh-no! (lambda () (oh-no!))) When one calls oh-no! the computation gets into an infinite loop. This can be stopped by typing the Unix interrupt character (C-c). > (oh-no!) ^C debug> Doing that stops the infinite loop, and puts you in the Chez Scheme debugger. To get out, you can type ``r'' (and a return). See above if you want to find out what happened to get you into the loop. debug> r > Now you are back to the Scheme interpreter's prompt ">". 3. WHAT CODE DO I HAVE LOADED? The easiest way to be sure what code you have loaded is to load it again. However, you may need to find out what code you have before you do that, to explain some error. This is a bit tricky, but something can be done. Consider the following example. >(define id (lambda (x) x)) > (inspect id) # : code (lambda (x) x) # : quit You can see code by using the procedure ``inspect'' in Chez Scheme. Once you start it, by calling it with the name of the procedure whose code you want to see, you can see the code by typing ``code'' (without the quotes) at the colon (:) prompt (followed by a return). You can get out of the inspector by typing ``quit'' (without quotes). You can abbreviate these commands to single letters if you wish (``code'' to ``c'' and ``quit'' to ``q''). This doesn't look too tricky, but that's because id is so short. For longer procedures the inspector is less informative. Consider the following example. > (load "member.ss") > (inspect member?) # : c (lambda (item ls) (if (null? ls) #f ...)) : Notice the ... in the body, that's there to tell you there is more code, but the inspector doesn't want to print it just yet, as it would take up too much space on the line. To see more of it you can use the ``tail'' command. (lambda (item ls) (if (null? ls) #f ...)) : tail 2 ((if (null? ls) #f ((lambda (...) (...)) (equal? (...) item)))): Notice that this is like taking 2 cdrs of the list of code. You can also use car and cdr to move around in this list. Some of the commands can be seen by typing a question mark (?) at the prompt. ((if (null? ls) #f ((lambda (...) (...)) (equal? (...) item)))): ? length(l) ........... display list length car ................. inspect car of pair cdr ................. inspect cdr of pair ref(r) .............. inspect [nth] car tail ................ inspect [nth] cdr show(s) ............. show [n] elements of list ?? .................. display more options ((if (null? ls) #f ((lambda (...) (...)) (equal? (...) item)))): q Another complication is that the code that is shown to you is NOT exactly the same as what you typed, it has some things expanded, no comments, and is not formatted the way you formatted it. Some of the code may even be reorganized (for what is called optimization, to make it run faster).