Introducing the GoLisp Debugger

12 Nov 2014

by Dave Astels

As we use GoLisp for more and more general programming, the lack of debugging capabilities has become problematic. So, I decided to bite the bullet and write a debugger for it.

This document assumes that you have some understanding of how GoLisp’s eval loop and lexical scoping works.

Entering the debugger

There are several ways to enter the debugger:

  • evaluate (debug), either from the repl prompt or in code. The latter acts as a breakpoint.
  • if debugging on errors is enabled, the occurance of an error causes the debugger to be entered rather than outputting the error.
  • evaluating a function that has been tagged as ‘debug on entry’

Debugger related functions

(debug)

Enters the debugger REPL.

(debug-trace #t/#f)

Turns tracing on and off. When an sexpr is about to be evaluated or a value is returned, a line is printed showing:

  • the depth in the environment stack,
  • whether it’s an eval or a return
  • the code being evaled, or the value being returned

The part of the line following the stack depth is indented to reflect the depth.

  > (define (fact n) (if (< n 2) 1 (* n (fact (- n 1)))))
  ==> <function: fact>
  > (debug-trace #t)
    1: <- #t
  ==> #t

  > (fact 2)
    1: -> (fact 2)
    1: -> fact
    1: <- <function: fact>
    1: -> 2
    1: <- 2
    2: --> (if (< n 2) 1 (* n (fact (- n 1))))
    2: --> if
    2: <-- <prim: if, 0x406ba10>
    2: --> (< n 2)
    2: --> <
    2: <-- <prim: <, 0x4069260>
    2: --> n
    2: <-- 2
    2: --> 2
    2: <-- 2
    2: <-- #f
    2: --> (* n (fact (- n 1)))
    2: --> *
    2: <-- <prim: *, 0x40651a0>
    2: --> n
    2: <-- 2
    2: --> (fact (- n 1))
    2: --> fact
    2: <-- <function: fact>
    2: --> (- n 1)
    2: --> -
    2: <-- <prim: -, 0x4064cc0>
    2: --> n
    2: <-- 2
    2: --> 1
    2: <-- 1
    2: <-- 1
    3: ---> (if (< n 2) 1 (* n (fact (- n 1))))
    3: ---> if
    3: <--- <prim: if, 0x406ba10>
    3: ---> (< n 2)
    3: ---> <
    3: <--- <prim: <, 0x4069260>
    3: ---> n
    3: <--- 1
    3: ---> 2
    3: <--- 2
    3: <--- #t
    3: ---> 1
    3: <--- 1
    3: <--- 1
    2: <-- 1
    2: <-- 2
    2: <-- 2
    1: <- 2
  ==> 2

(debug-on-error #t/#f)

Turns on or off the ability to have evaluation stop and the debugger entered when an error occurs.

  > (define (foo x) (/ 10 x))
  ==> <function: foo>
  > (foo 1)
  ==> 10
  > (foo 0)
  Error in evaluation: 
  Evaling (foo 0). In 'foo': 
  Evaling (/ 10 x). Quotent: (10 0) -> Divide by zero.
  > (debug-on-error #t)
  ==> #t
  > (foo 0)
  ERROR!  Quotent: (10 0) -> Divide by zero.
  Eval (/ 10 x)
  D> 

(add-debug-on-entry func)

This adds func to the list of functions whose evaluation will cause the debugger to be entered. The debugger will be opened with evaluation stopped just before the function is called, in the environment frame where the call is being made.

  > (add-debug-on-entry make-sum)
  ==> ("make-sum")
  > (deriv '(* x y) 'x)
  Eval (make-sum (make-product (multiplier exp) (deriv (multiplicand exp) var)) ...
  D>

(remove-debug-on-entry func)

This removes func from the list of functions whose evaluation will cause the debugger to be entered.

  > (add-debug-on-entry make-sum)
  ==> ("make-sum")
  > (remove-debug-on-entry make-sum)
  ==> ()

(debug-on-entry)

Returns a list of names of functions marked as debug-on-entry.

  > (debug-on-entry)
  ==> ("make-sum" "make-product")

Using the debugger

When you are in the debugger, the command prompt changes to D>. Any of the commands in this section can be used.

Commands at the debugger prompt

:(+ func

This adds func to the list of functions whose evaluation will cause the debugger to be entered. This is a shortcut for add-debug-on-entry.

:(- func

This removes func from the list of functions whose evaluation will cause the debugger to be entered. This is a shortcut for remove-debug-on-entry.

:(

Lists functions marked as debug-on-entry.

  D> :(
  make-sum
  make-product

:?

This outputs a summary of all debugger commands.

:b

Outputs the environment stack, showing one line per frame. This shows the stack of frames created at runtime, not the arrangement of frames used for lexical scoping.

  D> :b

  Frame 0: Eval (/ 10 x)
  Frame 1: Eval (foo 0)

:c

Continue execution. This will either return you to the top level REPL or re-enter the debugger if the appropriate conditions occur (e.g. an error when debug-on-error is enabled).

  > (add-debug-on-entry make-sum)
  ==> ("make-sum")
  >  (deriv '(+ x 3) 'x)
  Eval (make-sum (deriv (addend exp) var) (deriv (augend exp) var))
  D> :c
  ==> 1
  > 

:d

Output the environment stack with all bindings in each.

  D> :d

  Frame 0: Eval (/ 10 x)
     x => 0

  Frame 1: Eval (foo 0)
     nil => ()
     foo => <function: foo>

:e on/off

Turns on or off the ability to have evaluation stop and the debugger entered when an error occurs. :e on is a shortcut for (debug-on-error #t)

:f frame#

do a full dump of a single environment frame

  D> :b

  Frame 0: Eval (/ 10 x)
  Frame 1: Eval (foo 0)

  D> :f 1
  Eval (foo 0)
     foo => <function: foo>
     nil => ()

:q

Quit GoLisp

:r sexpr

Return from the current evaluation using the specified value as its result. In this example we force the (/ 10 0) to return with the result 5.

  > (define (foo x) (+ 1 (/ 10 x)))
  ==> <function: foo>
  > (foo 0)
  ERROR!  Quotent: (10 0) -> Divide by zero.
  Eval (/ 10 x)
  D> :r 5
  ==> 6
  > 

:s

This causes the current evaluation to complete. At the start of the next evaluation the debugger will again take control. Note that this means the very next time the evaluator is called.

:t on/off

Turns tracing on and off. :t on is a shortcut for (debug-trace #t)

:u

Continue until the current lexical scope exits, returning to it’s parent scope.

Other things you can do in the debugger

The debugger REPL is a fully functional GoLisp REPL with the addition of the above commands. That means that you can evaluate arbitrary code. Of special interest is inspecting and changing values in the midst of stepping through code.

Code that you eval from the debugger prompt will not produce a trace, but will trigger debug-on-error and debug-on-entry checks.