The Clojure debug-repl - Dec 7, 2009

Intro

Every time I stick a println into some Clojure code to debug it, I think to myself, "This is Lisp! I should be able to insert a repl here!"

The problem is of course that Clojure's eval function doesn't know about the surrounding lexical scope. So I started asking myself, what is the simplest change I could make to Clojure to support an eval that understands that scope? Then I tried to implement it.

Basically, here's what I came up with.

1. Modify the Clojure compiler so that when a flag is turned on, it stores references to the lexical scope in a dynamic var. Thus, each time the compiler creates a new lexical scope, it also emits the byte code to push a hash-map with the details onto the var. When that scope ends, the byte code for popping the hash-map off the var is emitted.

2. Then in Clojure proper, add a special version of eval that uses that var. It wraps the form being eval'ed in a "let" that emulates the original lexical scope, something like this:

        `(eval
            (let [~@(make-let-bindings
                     (:lexical-frames (var-get (resolve context))))]
              ~form))

With those two pieces, it's straight-forward creating a "debug-repl" that understands the surrounding lexical scope.

Use

The interface is pretty simple:

"(use 'clojure.debug)" loads it, and "(debug-repl)" invokes it. You'll need to turn on lexical frame capture by surrounding your code with "with-lexical-frame". You can also use the convenience macros, "defn-debug", "defmacro-debug", and "deftest-debug" to wrap the corresponding def form in "with-lexical-frame".

That's about it. When you enter the debug-repl, the regular repl prompt will be replaced with

dr =>

Some examples will make it clearer:


user=> (use 'clojure.debug)
user=> (with-lexical-frames
          (let [c 1 d 2]
            (defn a [b c]
              (debug-repl)
                   d)))
#'user/a
user=> (a 22 (java.io.File. "/"))
dr => b
22
dr => c
#<File />
dr => d
2
dr => (* b d)
44
dr => (seq (.listFiles c))
(#<File /lost+found> #<File /var> #<File /media> #<File /etc> #<File /cdrom> ...
dr => 2
user=>

Note that the function args and closure are displayed correctly above. ctrl-d exits back to the function that invoked the debug-repl.

You can also use the "get-context" macro to save the context to a global var and debug it separately after running the code that created it:


user=> (use 'clojure.debug)
user=>  (with-lexical-frames
          (let [c 1 d 2]
            (defn a [b c]
              (get-context saved-context)
              d)))
#'user/a
user=> (a 22 (java.io.File. "/"))
2
user=> (debug-repl saved-context)
dr => b
22
dr => c
#<File />
dr => d
2
dr => nil
user=>

For a more interesting example, try adding the debug-repl to the contrib repl-utils "member-details" function, (which is used by the "show" function) like so:

diff --git a/src/clojure/contrib/repl_utils.clj b/src/clojure/contrib/repl_utils.clj
index 2864179..fc64e04 100644
--- a/src/clojure/contrib/repl_utils.clj
+++ b/src/clojure/contrib/repl_utils.clj
@@ -41,3 +41,4 @@

-(defn- member-details [m]
+(use 'clojure.debug)
+(defn-debug member-details [m]
   (let [static? (Modifier/isStatic (.getModifiers m))
@@ -53,2 +54,3 @@
                    (str (.getSimpleName (.getType m))))))]
+    (debug-repl)
     (assoc (bean m)

Then explore it by invoking show like this:

user=> (use 'clojure.debug)
user=> (ns clojure.contrib.repl-utils)
clojure.contrib.repl-utils=> (use ' clojure.contrib.repl-utils)
clojure.contrib.repl-utils=> (show Object)
dr => ctor?
false
dr => m
#<Method public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException>
dr =>  (bean m)
{:genericReturnType void, :declaringClass java.lang.Object, :typeParameters ...
dr =>  [(not static?) method? (sortable text)]
[true true "wait : void (long)0000"]
dr =>     (assoc (bean m)
           :sort-val [(not static?) method? (sortable text)]
           :text text
           :member m)
{:sort-val [true true "wait : void (long)0000"], :genericReturnType void, :text ...
dr =>

Getting/Building the debug-repl source

The source is in the debug-repl branch of my Clojure fork on github. Get it like so:

git clone git://github.com/GeorgeJahad/clojure.git
cd clojure
git checkout --track -b debug-repl origin/debug-repl
ant
The jar file is clojure-1.1.0-alpha-debug-repl-SNAPSHOT.jar

Limitations

Doesn't work with Slime repl yet

The debug-repl doesn't currently integrate properly with the slime-repl, (I think because of how Slime manages IO redirection,) so you'll have to invoke it from a regular repl. This is high on my list of things to fix. Hope to have a solution soon.

Let frames aren't generated till the end of the binding vector

As a simplication, I chose for now not to post the lexical frame for a let binding until after the entire binding is complete. This means putting a debug-repl somewhere within a let binding vector is no different from putting it immediately before the start of that let binding. I want to fix this fairly soon.

Transients

I haven't made any attempt to handle as they are changed.

Letfn bindings aren't yet stored.

Just haven't gotten around to it yet, (since I don't use letfn).

Comments/Suggestions

Send any comments/suggestions to George Jahad at "george-clojure at blackbirdsystems.net" or to the main clojure mailing list: http://groups.google.com/group/clojure