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.
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 =>
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 antThe jar file is clojure-1.1.0-alpha-debug-repl-SNAPSHOT.jar
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.
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.
I haven't made any attempt to handle as they are changed.
Just haven't gotten around to it yet, (since I don't use letfn).
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