编译更复杂的core.async代码时,遇到难以理解的堆栈溢出错误的风险确实存在。
以下是一个这样的近期情况
https://dev.clojure.org/jira/browse/CLJS-3030?focusedCommentId=51115&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#comment-51115
可能的原因在于`cond`的宏展开方式。问题在于编译器堆栈深度与测试/表达式对的数目线性相关
{code:none}
> (macroexpand-1 '(cond (test1) (body1) (test2) (body2) (test3) (body3)))
(if (test1) (body1) (clojure.core/cond (test2) (body2) (test3) (body3)))
完整的宏展开会导致一系列的'if'语句,而每个新的测试/表达式对的解析都在编译器堆栈的更深层次发生。
这里[1]是一个最小化复现案例。
这是一个普遍的问题,但在core.async的背景下可能会有实际问题,因为core.async为它的状态机生成很大的`cond`形式,可能多达数百个状态。如果嵌套,问题会加剧。
--
最好以两种方式处理这个问题
a) 尝试实现一个更堆栈友好型的`cond`宏(我认为问题不在于core.async生成代码的方式)
或尝试实现一种“尾调用优化”的分析方式,这样在下降到最后子表时不会增长堆栈(如果可能的话)
b) 使用用户友好的解释处理堆栈溢出异常 - 例如跟踪对`analyze_form`的内部递归调用次数,并在出现异常时尝试传达可能发生的事情,并(理想情况下)确定是哪个形式/宏展开导致了这种堆栈增长
[1]
https://gist.github.com/darwin/8dda48153fcbd3bc3e2d700f3e0eea00
==> _compile.sh <==
#!/usr/bin/env bash
clj -Srepro -m cljs.main -co @compiler-opts.edn -c repro
==> compiler-opts.edn <==
{:source-paths ["."]}
:warnings {:single-segment-namespace false}
==> deps.edn <==
{:paths ["."]}
:deps {org.clojure/clojure {:mvn/version "1.10.0"}
org.clojure/clojurescript {:mvn/version "1.10.439"}}}
==> repro.clj <==
(ns repro)
(defmacro fat-cond [n]
(let [clauses (repeat n [`false `(cljs.core/do)]])
`(cond
~@(flatten clauses))))
==> repro.cljs <==
(ns repro
(:require-macros [repro :refer [fat-cond]]))
(fat-cond 10000)