Clojure debug-repl Tricks - Dec 16, 2009

Debugging Clojure Macros

It can be tough debugging macros in Clojure. Here's a quick demo of using the debug-repl to do so.

This is the standard "doto" macro from clojure.core, with two changes. I changed the name so you can define it in the user namespace, and I removed one character so that it no longer works. Experienced macro hackers will see my flaw fairly quickly, but that isn't the point. The debug-repl gives you a tool to play with macros definitions at the repl. Makes it easier to see what going on.

Here is the bad definition:

(defmacro doto-bad [x forms] (let [gx (gensym)] `(let [~gx ~x] ~@(map (fn [f] (if (seq? f) `(~(first f) ~gx ~@(next f)) `(~f ~gx))) forms) ~gx)))

Try to run it like so, and you get an exception:

user=> (doto-bad "abc" println) java.lang.RuntimeException: java.lang.RuntimeException: java.lang.IllegalArgumentException: Don't know how to create ISeq from: clojure.lang.Symbol (NO_SOURCE_FILE:0)

Try to macroexpand it to see what is going on, and you get an exception:

user=> (macroexpand '(doto-bad "abc" println)) java.lang.RuntimeException: java.lang.RuntimeException: java.lang.IllegalArgumentException: Don't know how to create ISeq from: clojure.lang.Symbol (NO_SOURCE_FILE:0)

Now redefine it to include the debug-repl:

user=> (defmacro doto-bad [x forms] (let [gx (gensym)] (debug-repl) `(let [~gx ~x] ~@(map (fn [f] (if (seq? f) `(~(first f) ~gx ~@(next f)) `(~f ~gx))) forms) ~gx))) #'user/doto-bad

Invoke the macro like normal to start the debug-repl, and examine the locals:

user=> (doto-bad "abc" println) dr-1-1022 => x "abc" dr-1-1022 => forms println dr-1-1022 => gx G__3709

See if you can regenerate the exception by eval'ing the main part of the macro:

dr-1-1022 => `(let [~gx ~x] ~@(map (fn [f] (if (seq? f) `(~(first f) ~gx ~@(next f)) `(~f ~gx))) forms) ~gx) java.lang.IllegalArgumentException: Don't know how to create ISeq from: clojure.lang.Symbol (clojure.core/let

Now narrow the scope of the problem by testing increasingly smaller chunks of code:

dr-1-1022 => (map (fn [f] (if (seq? f) `(~(first f) ~gx ~@(next f)) `(~f ~gx))) forms) java.lang.IllegalArgumentException: Don't know how to create ISeq from: clojure.lang.Symbol (dr-1-1022 => (map identity forms) java.lang.IllegalArgumentException: Don't know how to create ISeq from: clojure.lang.Symbol

Since we are pretty sure "map" and "identity" both work, the problem appears to be with "forms". Seems like it needs to be a seq rather than a symbol. We can show that changing that fixes the problem, without ever leaving the debug-repl:

(dr-1-1022 => forms println dr-1-1022 => (map identity [forms]) (println) dr-1-1022 => (map (fn [f] (if (seq? f) `(~(first f) ~gx ~@(next f)) `(~f ~gx))) [forms]) ((println G__3709)) dr-1-1022 => `(let [~gx ~x] ~@(map (fn [f] (if (seq? f) `(~(first f) ~gx ~@(next f)) `(~f ~gx))) [forms]) ~gx) (clojure.core/let [G__3709 "abc"] (println G__3709) G__3709)

Everything appears to be working now, so something must be wrong with our original definition of the "forms" arg. As you've probably surmised, I left the ampersand off of the forms def so that the arg was a symbol rather than a seq of symbols. Changing that back fixes the problem.

Hopefully, this demo has given you a better idea of the power of the debug repl. The most recent version is available here:

http://gist.github.com/255883

That's it for now. Send any comments/questions to George Jahad at "george-clojure at blackbirdsystems.net" or to the main clojure mailing list: http://groups.google.com/group/clojure