DEBUGGING WITH SCM by Gary T. Leavens Department of Computer Science File $Date: 2000/08/17 21:28:04 $ This document describes how to use the debugging facilities of the SCM interpreter. Aside from trace, I seldom use more advanced debugging facilities. I find it more helpful to hand-simulate my program, or to think about what it is doing instead of using the Portable Scheme Debugger (PSD). When necessary, I usually add some displayln statements (see section 2.5 of "Scheme and the Art of Programming", which is on reserve). You can do this too, as it saves you from the trouble of learning the PSD system. Besides avoiding complications, you may note that the technique of adding print statements is language independent (it will work for C++, Pascal, etc. too). However, as always, more powerful tools are often useful, and using "trace" (see 1.2 below) is faster than adding print statements. 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: cdr: Wrong type in arg1 () ;STACK TRACE ... Here's the basic thing an interpreter should do for you, give you information about what happened. First, of course, look at the error message. Note first of all, that it says that the procedure "cdr" was called with a type of argument it didn't expect. The value of the argument is (). This is the same thing that happens when you type the following. > (cdr '()) ERROR: cdr: Wrong type in arg1 () ;STACK TRACE ... Looking at the error message from the expression (member? 3 '(1 2 3)) thus may be enough to tell you what happened. At some point to fix the bug you will have to use this information to deduce how the error could have occurred. But you may need more information. 1.1 THE STACK BACKTRACE The current SCM interpreter (when built with the CAUTIOUS feature) automatically prints a stack backtrace for you when the interpreter encounters an error. For example, what you actually see in the first example above is the following. > (member? 3 '(1 2 3)) ERROR: cdr: Wrong type in arg1 () ;STACK TRACE +; (#@cdr #@0+1) 0; ((#@or (#@member? #@0+0 (#@cdr #@0+1)) (equal? (car ls) item)) ... 4; (#@member? 3 (#@quote (1 2 3))) This stack trace has the current expression being evaluated at the top (with the plus sign, +, next to it). This is showing us that it is trying to take the cdr of something, which in this case has the empty list, (), as its value. The name #@cdr is an internal name for the built-in procedure cdr. The argument, #@0+1, is an internal lexical address) form of the argument. In this case it refers to the formal parameter "ls" in the definition of "member?". (It's at lexical depth 0, meaning it's in the same procedure, and is the formal parameter numbered 1, counting the list starting from 0.) The current stack frame, with the 0 at the left, is the body of "member?". You can tell that it is evaluating the "or" special form, and has not yet started on the second argument to "or", because (equal? (car ls item)) has not yet been converted to an internal form (with the #@ stuff). This stack frame is trying to get the value of "(cdr ls)". The "4" to the left of the oldest stack frame, which is our call to "member?", would normally mean that it is 4 levels down in the stack. In this case, because of Scheme's tail recursion optimization, stack frames have been reused, which is why there are not 3 other stack frames visible. If you look at the code, this kind of information should tell you where the error is occuring, and a bit about what has happened so far. This may be enough to localize the error. Exercise: Looking back at the definition of "member?", can you explain what caused the error now? But you still may want more information about what arguments are being passed... 1.1 TRACING One help, which saves you time in putting in print statements, is the SCM trace procedure. (For more information on trace, see http://swissnet.ai.mit.edu/~jaffer/scm_3.html#SEC30.) This is automatically available for those using the ui54 interpreter. (If not, type (require 'trace) first.) Let's try it. > (trace member?) # > (member? 3 '(1 2 3)) "CALLED" member? 3 (1 2 3) "CALLED" member? 3 (2 3) "CALLED" member? 3 (3) "CALLED" member? 3 () ERROR: cdr: Wrong type in arg1 () ;STACK TRACE ... The first call, (trace member?) told 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) "CALLED" member? 3 (1 2 3) "CALLED" member? 3 (2 3) "CALLED" member? 3 (3) "CALLED" member? 3 () ERROR: cdr: Wrong type in arg1 () ;STACK TRACE ... See how the value of test-list is shown in the first line of the trace? "CALLED" member? 3 (1 2 3) Now let's look at the next line. "CALLED" member? 3 (2 3) It too shows a call to member, this time a recursive call. The nesting level is shown by the indentation. This 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: Looking back at the definition of "member?", 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) # > (member? 3 test-list) "CALLED" member? 3 (1 2 3) "CALLED" cdr (1 2 3) "RETURNED" cdr (2 3) "CALLED" member? 3 (2 3) "CALLED" cdr (2 3) "RETURNED" cdr (3) "CALLED" member? 3 (3) "CALLED" cdr (3) "RETURNED" cdr () "CALLED" member? 3 () "CALLED" cdr () ERROR: cdr: Wrong type in arg1 () ;STACK TRACE ... 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 "CALLED" member? 3 (1 2 3) "CALLED" cdr (1 2 3) "RETURNED" cdr (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 indentation tells you that the call to (cdr (1 2 3)) happened ``inside'' the call of (member? 3 (1 2 3)); that is, after 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. (Note that untrace is calling cdr, and until it's done, we see the effects of the still existing trace to cdr.) > (untrace cdr) "CALLED" cdr (untrace cdr) ... "RETURNED" cdr #[proc] # > (member? 3 test-list) "CALLED" member? 3 (1 2 3) "CALLED" member? 3 (2 3) "CALLED" member? 3 (3) "CALLED" member? 3 () ERROR: cdr: Wrong type in arg1 () ;STACK TRACE ... You can untrace all traced procedures by calling untrace without any arguments. > (untrace) (member?) > (member? 3 test-list) ERROR: cdr: Wrong type in arg1 () ;STACK TRACE ... 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 DEBUGGING If you use emacs, there is a debugger you can use. It's called PSD (for Portable Scheme Debugger). To find out how to use it, point your web browser at the URL http://www.cs.tut.fi/staff/pk/scheme/psd/article/article.html