Strange behavior of eval at macroexpansion time

Viewed 51

I have noticed that, when eval is called at macroexpansion time and it tries to evaluate a symbol that doesn't name a special form or a local binding in the evaluated code but names a local binding in the macroexpanded code, an exception is thrown, even if the symbol also names a global variable.

Here are some examples of this behavior and other more or less related ones:

(def x 0)

(defmacro m0 [] (eval 'x))
(let [x 1] (m0))
;=> CompilerException java.lang.UnsupportedOperationException: Can't eval locals
(let [y 1] (m0))
;=> 0

(defmacro m1 [] (eval '(inc x)))
(let [x 1] (m1))
;=> CompilerException java.lang.InstantiationException: user$eval572$eval573__574
(let [y 1] (m1))
;=> 1

(defmacro m2 [] (eval '(let [x 2] x)))
(let [x 1] (m2))
;=> 2

(defmacro m3 [] (load-string "x"))
(let [x 1] (m3))
;=> 0

(defmacro m4 [] x)
(let [x 1] (m4))
;=> 0

(let [x 1] (eval 'x))
;=> 0

I have found a short discussion about this issue here. I think that, according to this description of evaluation, a symbol in such a case should evaluate to the content of a global variable. In fact this is what happens if instead of Clojure's built-in macroexpansion mechanism we use some macroexpand-all function (let's say the one from clojure.tools.analyzer.jvm, which is the most accurate that I know of):

(require '[clojure.tools.analyzer.jvm :refer [macroexpand-all]])

(macroexpand-all '(let [x 1] (m0)))
;=> (let* [x 1] 0)

;Here is a workaround:
(defmacro mexa [form] (macroexpand-all form))
(mexa (let [x 1] (m0)))
;=> 0

So should this be considered a bug? If not, what could be the rationale? Shouldn't there be some documentation and explanation of this behavior somewhere and also a better error message when the "problematic" symbol occurs in a complex form?

1 Answers

I think all of this is consistent with the sources you linked. In the two failing cases, there is a local named x, and so when eval goes to evaluate the form, x resolves to a local. Since it has resolved to a local, it does not move on to look for a var of the same name to resolve to. After resolution comes evaluation, but you're not allowed to eval locals, leading to the exception. There's no backtracking to find a suitable var to fall back on.

Sure, a better error message would be nice, I agree. But eval is not much used in normal Clojure code, and so I am not surprised that a failure in a niche case isn't as well documented as it could be.

Related