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