Using the Chez Scheme debugger

Achtung! Chez Scheme and Petite Chez Scheme do not give the same debugging information. Petite Chez Scheme is often much less helpful than Chez Scheme, and is probably half the reason why nobody actually tries debugging programs in Chez Scheme.

Here’s a sample, buggy program:
[scheme](define myreverse
(lambda (ls)
(if (null? ls)
‘()
(append (myreverse (cdr ls)) (cons (car ls) ‘())))))

(define mylist (cons 1 (cons 2 (cons 3 (cons 4 5)))))

(display (myreverse mylist))[/scheme]

Let’s try loading it in Chez Scheme.

$ scheme
Chez Scheme Version 8.0
Copyright (c) 1985-2010 Cadence Research Systems

> (load "example.ss")
Exception in car: 5 is not a pair
Type (debug) to enter the debugger.
> 

Obviously, (debug) invokes the debugger. Once we get in there, however, it’s fairly cryptic.

debug> ?
Type i  to inspect the raise continuation (if available)
     s  to display the condition
     c  to inspect the condition
     e  or eof to exit the debugger, retaining error continuation
     q  to exit the debugger, discarding error continuation
debug>

Usually, inspecting the continuation is the most helpful thing to do, because once we’re in there, we can get a stack trace to find out where the error occurred. The command to print out the stack frames is sf.

debug> i
#<continuation in myreverse>                                      : sf
  0: #<continuation in myreverse>
  1: #<continuation in myreverse>
  2: #<continuation in myreverse>
  3: #<continuation in myreverse>
  4: #<continuation in myreverse>
  5: #<system continuation in map>
  6: #<system continuation in compile>
  7: #<system continuation>
  8: #<system continuation>
  9: #<system continuation in dynamic-wind>
  10: #<system continuation in dynamic-wind>
  11: #<system continuation in $reset-protect>
  12: #<system continuation in new-cafe>
#<continuation in myreverse>                                      : 

Knowing the stack trace, we can move down the stack frames using down, which is important if the debugger drops you in an error handler under the actual source of the problem. This information is often enough to debug your program. From here, we know that the error occurred when we were five instances deep in myreverse, so the problem was with the end of the list. But if you wanted more specifics, you can type s for show:

#<continuation in myreverse>                                      : s
  continuation:          #<continuation in myreverse>
  procedure code:        (lambda (ls) (if (null? ls) (quote ()) ...))
  call code:             (cdr ls)
  free variables:
  0. ls:                 5

Now we know for certain that we attempted (cdr ls) but ls was actually 5, and (cdr 5) is wrong.

Let’s try example2.ss:

[scheme](define pass-me-a-closure
(lambda (thunk)
(break)
(thunk)))

(pass-me-a-closure
(lambda ()
(let loop ((x 5)
(a 1))
(if (zero? x)
a
(loop (sub1 x) (* a x))))))[/scheme]

Notice that we’ve set a breakpoint in the code using (break). This triggers the debugger, but only if we’re running Scheme interactively. If you try running scheme example2.ss directly, it will exit when it hits the breakpoint.

At the break point, we do the usual inspect and sfow frames.

[erjiang@silo ~]$ scheme
Chez Scheme Version 8.4
Copyright (c) 1985-2011 Cadence Research Systems

> (load "example2.ss")
break> i
#<continuation in pass-me-a-closure>                              : sf
  0: #<continuation in pass-me-a-closure>
  1: #<system continuation>
  2: #<system continuation>
  3: #<system continuation in dynamic-wind>
  4: #<system continuation in dynamic-wind>
  5: #<system continuation in $reset-protect>
  6: #<system continuation in new-cafe>
#<continuation in pass-me-a-closure>                              : s
  continuation:          #<system continuation>
  procedure code:        (lambda (thunk) (break) (display (thunk)))
  call code:             (break)
  free variables:
  0. thunk:              #<procedure>
#<continuation in pass-me-a-closure>                              : 

Right now, it doesn’t actually tell us what thunk is besides just #<procedure>. We need to rnspect that particular free variable by its number or name by issuing r 0 or r thunk and then printing the code.

#<continuation in pass-me-a-closure>                              : r 0
#<procedure>                                                      : c
(lambda () ((letrec ((loop (lambda (...) (...)))) loop) 5 1))     :

Notice that the let-loop form has been expanded (remember, the macro expander has already run) and on top of that, the part we actually care about has been elided. Also, the current object is now the code, instead of #<procedure>. Because of that, we can pretty-print the current object, and it will pretty-print the entire object.

(lambda () ((letrec ((loop (lambda (...) (...)))) loop) 5 1))     : p

(lambda ()
  ((letrec ([loop (lambda (x a)
                    (if (zero? x) a (loop (sub1 x) (* a x))))])
     loop)
    5
    1))

You can always go back up to the parent object from here.

In Chez Scheme, inspect is also available as a procedure that will launch the debugger and inspect whatever is passed to it. For example, writing (inspect thunk) into the above code would immediately launch the debugger on thunk. Likewise, doing (call/cc inspect) will launch the debugger and inspect the current continuation (which you can then trace).

Now you should know how to enter the debugger, inspect the stack frames, and inspect variables inside the stack frames, including the procedure code and call code. When in doubt, randomly hit keys or ask for help? Expect a pop quiz over this.

Leave a Reply