Clojure宏展开阶段调用eval的异常行为相关问题咨询
问题描述
我发现,在Clojure宏展开阶段调用eval时,若其尝试求值的符号既不指向特殊形式,也不属于待求值代码的本地绑定,却属于宏展开后代码的本地绑定,即便该符号同时对应全局变量,也会抛出异常。
以下是该行为及其他相关场景的示例:
(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
我找到了关于该问题的简短讨论。我认为,根据Clojure官方求值规则说明,该场景下的符号应该求值为对应全局变量的值。实际上,若不使用Clojure内置的宏展开机制,改用macroexpand-all函数(比如clojure.tools.analyzer.jvm提供的版本,是我所知准确性最高的实现),得到的结果符合预期:
(require '[clojure.tools.analyzer.jvm :refer [macroexpand-all]]) (macroexpand-all '(let [x 1] (m0))) ;=> (let* [x 1] 0) ;此处为可行的 workaround: (defmacro mexa [form] (macroexpand-all form)) (mexa (let [x 1] (m0))) ;=> 0
请问该行为是否属于Clojure的bug?若不属于,其设计逻辑是什么?是否有官方文档对该行为进行说明?当“问题符号”出现在复杂表达式中时,是否可以优化对应的错误提示?
回答
这个行为不属于官方定义的Bug,是Clojure原生编译器宏展开阶段符号解析逻辑的已知边界限制,核心原因如下:
- Clojure原生编译器处理代码时,会先扫描当前作用域的所有本地绑定,再处理作用域内部的宏调用。为了提升展开效率,编译器不会区分子表达式里的符号是属于「宏展开阶段就要求值的内容」还是「宏展开后生成的代码里的内容」,只要当前作用域有和符号重名的本地绑定,就会统一把符号标记为本地绑定类型。
eval本身的设计就不支持访问本地绑定,它只能对全局作用域下的符号或者显式传入绑定上下文的符号求值。当宏内部eval要处理的符号被编译器提前标记为本地绑定类型时,eval就会直接抛出无法求值本地变量的异常。
针对你提出的几个问题的逐一解答:
- 是否为Bug:核心开发团队很早就知晓该场景的存在,但修改符号解析逻辑会影响现有大量宏的行为,兼容性成本过高,因此一直没有调整,属于设计上的取舍,不算正式Bug。
- 设计逻辑:本质是编译器在展开效率和边界场景正确性之间的取舍,优先保证绝大多数常用场景下的宏展开速度,牺牲了这种极少出现的重名场景的正确性。
- 官方文档说明:目前官方文档没有针对该特殊场景的专门说明,只在
eval的函数文档里提到了其无法访问本地绑定,以及宏展开阶段只能访问全局作用域的规则。 - 错误提示优化:目前原生编译器确实没有对该场景做特殊处理,报错信息要么是通用的「Can't eval locals」,要么是更模糊的类实例化异常。社区有相关提案要优化该场景的错误提示,比如检测到符号是宏内部
eval的求值目标且和外层本地绑定重名时,给出更明确的引导,但相关改动还没有并入正式发行版。
除了你提到的用macroexpand-all包裹的规避方案外,还有两种更轻量的处理方式:
- 在宏内调用
eval时,对要求值的符号使用完全限定名,比如(eval 'user/x),编译器就不会把它和外层的本地绑定做关联 - 除非必要,不要在宏展开阶段调用
eval,把求值逻辑后置到宏展开后生成的运行时代码里,可以完全规避该问题
内容的提问来源于stack exchange,提问作者peter pun
相关产品推荐
相关产品推荐

