当编译更复杂的 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)的上下文中,这可能会成为一个真正的问题,因为核心异步为其状态机生成大量的 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)