将构建系统集成到编译器中

Lobsters Hottest 工具

摘要

Lucas Ma 探索了在 OCaml 编译器中使用效应来创建按需编译器服务,通过反转文件查找控制权并管理全局状态快照,从而实现更灵活的编译。

<p><a href="https://lobste.rs/s/izkimy/retrofitting_build_system_into_compiler">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/08/18 02:13

# 在编译器中嵌入构建系统 来源:https://www.dra27.uk/blog/platform/2025/09/25/building-with-effects.html 今年夏天,Lucas Ma(https://github.com/lucasma8795)一直在研究将效果(effects)机制应用于 OCaml 编译器自身的各种构想(https://anil.recoil.org/ideas/effects-scheduling-ocaml-compiler)。他记录了部分探索过程与发现(https://lucasma8795.github.io/blog/)。这项工作的技术核心旨在实现按需将 OCaml 编译器作为库使用,从而创建一个更持久的“编译器服务”。这本身并非革命性创举,但在拥有 30 年历史、且最初设计为单次独立编译的代码库上实现这一点却相当困难。 Lucas 很快掌握了 OCaml 的构建系统,并首先着手泛化编译器内部的一个核心部分——`Load_path`。编译器使用它扫描各种“include”目录以查找文件,主要是类型信息。例如,若代码中包含对`Unix.stat`的调用,则类型检查器需要`Unix`模块的类型信息,这会驱使它从`Load_path`请求`unix.cmi`文件,随后希望将其解析为类似`~/.opam/switch/lib/ocaml/unix/unix.cmi`的路径。 效果机制为这种查找提供了优雅的控制反转方式,因为调用编译器的程序可以更改这些文件的查找方式。这也提供了向编译器“谎报”实际存在文件的机会,而这正是 Lucas 开始进行这项修改时首先尝试的事情。特别是,它允许我们忽略依赖图。编译模块时,OCaml 要求所有被引用的模块类型信息必须已预先编译。如果模块`bar.ml`有对应的接口文件`bar.mli`,且代码中引用了`Foo.value`,那么 OCaml 要求在编译`bar.ml`之前,`foo.mli`和`bar.mli`都已编译完成。然而,得益于这个效果机制的技巧,Lucas 可以让编译器仅从`bar.ml`*开始*。当遇到`Foo.value`时,会发出对`foo.cmi`的请求,在此原型中,编译器随即快速启动另一个实例来编译`foo.mli`,*然后*恢复`bar.ml`的编译,编译`bar.cmi`时同样会发生这个过程。也就是说,仅通过`ocamlc -c bar.ml`就能编译三个文件(`foo.mli`、`bar.mli`和`bar.ml`)。 这或许因其有朝一日能从 OCaml 源码树中移除像这样(https://github.com/ocaml/ocaml/blob/trunk/.depend)的庞大依赖文件而显得巧妙,但目前尚未*特别*令人兴奋。然而,效果机制给予我们的不仅是编译器操作的钩子。我们将整个挂起的编译过程打包在一个延续(continuation)中……这意味着同一个编译器“进程”现在可以去做其他事情。下一个技巧是让当前进程本身返回编译器,自行编译所需的接口文件,*然后*简单地恢复先前文件的延续。此时,30 年历史的代码库再次显现其影响。出于速度和空间考虑,编译器的许多部分,尤其是在类型检查器中,大量使用全局可变状态。特别是,编译流水线*不是*可重入的。幸运的是,得益于 Merlin 项目,类型检查器中存在一种获取所有这些全局状态快照的机制。Lucas 能够利用这一点,使得在编译器执行效果请求尚不存在的`.cmi`文件之前,先快照其所有全局状态,执行效果,待恢复后再恢复该状态。 利用这种机制来中断类型检查并开始处理其他任务,并非`Local_store`机制(https://github.com/ocaml/ocaml/blob/trunk/utils/local_store.mli)最初设计的用途,调试过程中还发现了一些未被“注册”的全局状态片段,但 Lucas 成功实现了一种构建 OCaml 字节码编译器的方式:无需任何预编译,只需向编译器提供所需`.ml`文件的列表即可。从工具链角度来看,我们实质上正在淘汰`ocamldep`(https://ocaml.org/manual/5.3/depend.html)。 到目前为止,这依然主要只是“巧妙”:一个单独的编译器进程(勉强)成功地重新编译了编译器。然而,这相当于使用`make -j1`进行编译——一种顺序、因而缓慢的构建方式。接下来才是精彩的部分——域(Domains)。在 Lucas 最终研究的版本中,启动了多个域,每个域并行开始编译编译器所需的其中一个`.ml`文件,调度器依次处理来自每个域的、当需要编译`.mli`文件时产生的效果,并分发这些任务。编译器中的`Local_store`机制在这里派上了用场——Lucas 扩展了它,以使用域本地存储(Domain Local Storage)(https://ocaml.org/manual/5.3/api/Domain.DLS.html),并结合快照功能。为简化起见,该原型在这些域之间没有共享。 到夏末,这项工作已*几乎*完成,其成果已远超我在有限时间内所预期的!正如这类探索中常见的情况,Lucas 的工作揭示了这一领域中一些此前我不太清楚的新方面。我此前一直在思考如何通过驱动程序向用户暴露这种多线程编译器,但越来越清楚地看到这并非必需——我们用于构建编译器本身的程序,当然不是编译器驱动程序,*而是一个构建系统*。在我看来,这有两点特别令人兴奋: 1. 它是一个*极其简单*的构建系统。希望当并行类型检查器的最后几个问题解决后(请继续阅读),我们可以补充说,它既极其简单**又高效**。 2. 它从根本上是可移植的。这为用 OCaml 自身轻松引导(bootstrapping)OCaml 创造了可能性。虽然以前使用`ocamlbuild`也实现过,但结果是一场维护噩梦。然而,多域效果调度方式的极致简洁性,正让这位长期与构建系统打交道的人跃跃欲试……

相似文章

关于编译器构建的观点

Lobsters Hottest

本文分享了关于现代编译器构建的个人观点,主张使用如OhmJS进行解析和Prolog进行类型检查的工具,重点强调简洁性和迭代开发。

为什么ML/OCaml适合编写编译器(1998)

Lobsters Hottest

这篇1998年的文章认为,ML和OCaml非常适合编写编译器,因为它们具有垃圾收集、尾递归优化以及带有模式匹配的代数数据类型等特性,这些特性简化了复杂编译器数据结构的处理。

自举 Rust 被认为有害

Lobsters Hottest

对 Rust 编译器自举过程的批判性分析,指出与 OCaml 等其他语言相比,其体积过大和依赖臃肿的问题,并主张采用更轻量的方法。