roc 编译器 spec_constr 调用模式特化的发散风险与有界化改造方案
roc 编译器 spec_constr 调用模式特化的发散风险与有界化改造方案【免费下载链接】rocA fast, friendly, functional language.项目地址: https://gitcode.com/GitHub_Trending/ro/roc导读本文基于 roc 仓库中 projects/small/spec-constr-specialization-limits.md 这份设计文档深入剖析spec_constr调用模式特化pass 在优化构建下缺少 spec 数量、形状深度与燃料上限所引发的编译期无限循环问题一个普通 Roc 函数在--optspeed下可以让编译器永远越特化越深直至内存耗尽。文章会先结合源码还原发散的具体路径再给出四项工程化修复方案按源函数设置 spec 预算、为shapeFromValue增加形状深度上限、选择最具体 spec、暴露静默命中计数并补充对应回归测试与评估方法帮助你既理解该 pass 的设计动机也能直接落地修复。1. 背景spec_constr 在编译流水线中的位置roc 编译器从源码到后端的流水线大致为parse → canonicalize → type-check → postcheck: Monotype IR → Monotype Lifted闭包提升优化构建下同时运行 spec_constr 调用模式特化 → Lambda Solved → Lambda Mono decisions → LIR → ARC → backends其中Monotype Lifted阶段由 src/postcheck/monotype_lifted/ 下的 pass 构成。spec_constr.zig是其中的调用模式特化 pass它在直接调用点记录构造子形状的调用模式call pattern为每个模式预留独立的 worker 函数 id然后在符号化Value环境下把每个源函数克隆成按模式特化的 worker克隆过程中会折叠字段读取、已知 match 以及已知调用的内联。该 pass 的启用条件与内联模式绑定在 src/lir/checked_pipeline.zig 中inline_mode默认是.none而 spec_constr 只在inline_mode ! .none时运行procedure_usage的收集分支见 src/lir/checked_pipeline.zig。也就是说只要使用优化构建dev/speed该 pass 就一定会跑这正是发散风险影响面大的原因。值得强调的是特化是纯优化当这个调用不值得特化时安全回退就是把原始直接调用保留在输出中。因此下文所有加上限的方案都不会牺牲正确性——顶多是在病态形状上少做优化而病态形状今天根本编译不出来。2. 问题为什么缺少上限会导致编译期发散设计文档指出spec_constr没有任何 spec 数量、形状深度或燃料上限在src/postcheck/monotype_lifted/spec_constr.zig全文件中不存在limit/fuel/cap/max_*标识符。其祖先 GHC 的 SpecConstr 之所以提供-fspec-constr-count和深度限制正是因为调用模式特化可以不断制造更深的模式并最终发散。本实现同样存在这一发散路径且是具体可复现的。2.1 发散的三要素把文档中列出的机制与源码逐一对应排水循环drain loopcreateSpecializations以while (wrote_spec)循环写入特化spec_constr.zig。写入一个特化可能追加新的 spec从而让循环继续存活。克隆期间产出新 spec新 spec 由ensureCallPatternForValues在克隆过程中产生spec_constr.zig——只要发现新的构造子形状模式就记录一个Spec。去重是精确结构相等patternEql/shapeEql见 spec_constr.zig因此严格更深的形状永远不会被去重掉新形状必会新增 spec。递归自调用的产卵点当被调方已经在inline_stack上时这是防止无限内联的守卫克隆回退到普通路径cloneExprPlain→cloneCallProc→ensureCallPatternForValues。也就是说防止无限内联的递归守卫恰好成为无限特化的燃料。形状增长克隆会用已知Value替换被特化的参数如果函数体的递归调用把该参数包进一个构造子得到的值就深了一层shapeFromValuespec_constr.zig没有深度上限产出的Shape更深于是又记录了一个新 spec下一轮继续加深——构成一个永不停歇的棘轮ratchet。另外所有形状与值都存放在pass.arena中仅在 pass 结束时释放所以非终止同时意味着无界内存不仅是死循环而且是死循环 内存泄漏式增长。2.2 一个可复现的发散程序文档给出了一个普通 Roc 即可表达的失败场景一个匹配递归 tag-union 参数、并在递归调用中把参数重新包深一层的函数Tree : [Leaf, Node(Tree, I64)] deepen |t, n| when t is Leaf - if n 0 then Leaf else deepen(Node(Leaf, n), n - 1) Node(_, _) - if n 0 then t else deepen(Node(t, n), n - 1)t被解构因此它的参数位置是可特化的而每次递归调用都传入Node(已知 t, _)——于是Node(Leaf)、Node(Node(Leaf))、Node(Node(Node(Leaf)))……的 spec 被无界地生成一个编译期无限循环。2.3 安全边界什么情况不会发散消费式递归consumer recursion只传递子部分如 list/tree fold形状会缩小或稳定不会发散设计意图中的 Stream/Iter 管道静态、有限的嵌套深度天然安全风险特定于匹配-重建-加深match-and-rebuild-deeper这种模式。2.4 次要质量缺陷最不具体 spec 先匹配同一套机制里还有一个正确性中立但影响优化质量的问题rewriteCallProcspec_constr.zig与cloneCallProc按插入顺序选择第一个匹配的 spec因此更早记录的更泛化模式会遮蔽更晚记录的更具体模式——调用最终绑定到不够特化的 worker丢掉本可获得的优化。2.5 项目缘起该项目源于 2026-07 对 postcheck 与其生产化来源cor的lss原型的对比审查spec_constr 在cor中没有对应物因此缺少流水线其余部分获得过的参考实现级审查。3. 解决方案设计3.1 按源函数设置 spec 预算引入一个 comptime 常量GHC 的默认 count 是 3可从接近该值起步再按语料库调优。ensureCallPatternForValues在记录前检查该源函数已记录的 spec 数量达到上限 → 不再记录调用按普通直接调用落低while (wrote_spec)排水循环因此在结构上终止总 spec 数 ≤ 函数数 × 预算。注意源码中已经存在一层入场检查ensureCallPatternForValues会先检查self.plans[raw].source.size.admits()且recordCallPatternForValues会调用newSpecAdmissionspec_constr.zig新方案是在这些既有守卫之上叠加数量预算维度。3.2shapeFromValue的形状深度上限构造子嵌套超过一个较小的 comptime 深度例如 8时将该子树退化为不透明的 any value 形状而不是继续生成更深模式。这带来两个收益在预算之内就杀死加深棘轮保留下来的 spec 足够浅更可能在未来的调用中再次匹配深层 spec 几乎不会再被任何调用命中。3.3 最具体 spec 选择当多个已记录 spec 匹配一个调用时优先选择最具体的总形状最深而不是第一个插入的。有了预算之后每次调用只需少量比较成本可忽略。3.4 不隐藏静默上限命中在 Debug 构建计数器中累计预算命中次数并通过该 pass 现有的 debug 报告机制暴露遵循 no-silent-caps 纪律这样跑一遍语料库就能看到预算实际绑定了多少次——避免上限在真实程序上悄悄生效导致特化消失而无人知晓。4. 成功标准上述deepen复现程序在--optspeed下有界时间内编译完成、内存有界、运行结果正确任何源函数记录的 spec 总数不超过预算排水循环的终止不再依赖形状格shape lattice的有限性而是结构性终止现有语料库examples、roc-parser 套件、snapshot 语料在--optspeed下产出的二进制不变或者仅在最具体 spec 选择修复挑出更优 worker 的地方发生变化。5. 如何评估结果5.1 正确性维度上限只会留下残余直接调用——永远不会错误地折叠、重定向或特化跨优化级别一致性解释器 vs--optdevvs--optspeed在复现程序与语料库上的一致性是基准事实终止是结构性的预算算术保证而非经验性的。5.2 性能维度语料库编译时间在噪声范围内不变预算不应在真实程序上绑定——用 3.4 的计数器确认受益于特化的基准Stream/Iter 管道运行时不变预算必须足够慷慨让预期特化全部生效用 CI 基准验证。6. 待新增的测试按文档要求先写发散回归测试并在未修改的树上确认它会挂起或触发超时spec_constr_deepen把 match-and-rebuild-deeper 程序放到roc build --optspeed下沿用 src/cli/test/parallel_cli_runner.zig 约定的 CLI 测试超时机制断言构建成功且运行输出正确。消费式递归对照组一个只解构的 list/tree fold必须仍然发生特化通过 debug 计数器或已发射 spec 数量断言——钉死上限不会阉割该 pass。最具体选择一个同时以Cons(1, Nil)形状和Cons(x, xs)形状调用的函数断言更深形状的调用绑定到更深的 specpass 的单元级测试或输出形状断言。预算绊线tripwire单元测试为一个函数构造超过预算数量的不同调用模式断言 spec 数量 预算且所有调用仍然落低残余直接调用。7. 关联项目spec-constr-static-match-soundness已落地文档已移除同一 pass 的 match 判定健全性两个项目都触及bindPatToValue附近的代码可共享测试脚手架落地顺序不限。store-generation-counters已落地文档已移除已落地的 GuardedList 工作已加固本 pass 的边遍历边变更iterate-while-mutate风险本项目解决的是它的终止性风险。8. 给后续维护者的关键源码索引关注点位置排水循环createSpecializationsspec_constr.zig克隆期间产 specensureCallPatternForValuesspec_constr.zig形状推导shapeFromValue含 4096 步共享预算无深度上限spec_constr.zig调用改写首个匹配 specrewriteCallProcspec_constr.zig精确结构去重patternEql/shapeEqlspec_constr.zigpass 启用条件inline_mode ! .nonesrc/lir/checked_pipeline.zig一个值得注意的现状细节shapeFromValue已经有 4096 次节点访问的工作预算shape_work_budgetspec_constr.zig超出后退化为.any——它保证单次形状推导有界但不能阻止每轮加深一格的跨轮增长这正是深度上限方案 2要补齐的维度。【免费下载链接】rocA fast, friendly, functional language.项目地址: https://gitcode.com/GitHub_Trending/ro/roc创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
上一篇/下一篇内容由系统自动关联
返回资讯列表 →