用 Rust 实现编译器 (二): 重构, 重构, 重构, 重构
最近一段时间编译器的进度是这样的.

终于碰到了 Rust 最头疼的点了: 管理引用与生命周期. 而在编译器中当然会有很多这样的问题, 因为许多数据 (例如变量/类名, 类型信息等) 会被重复在多个地方被使用, 就经常会有数据共享的问题. 下面按照时间顺序梳理我的几次重构, 用于警示自己不要再发病了.
混沌阶段: 随意引用和智能指针
在写 semantic check 的最开始还没有意识到数据共享和引用会带来什么生命周期的问题, 只是模糊中有一些概念. 所以当时的做法是全部字符串用一个 &'a str
包起来, 然后类型等信息就用 Rc<Info<'a>>
, 表用 Rc<RefCell<Table<'a>>>
. 这样一开始写的时候完全没有遇到任何问题, 顶多就是不知道 ASTNode
里面应该怎样生成 Info
, 纠结了半天, 后来去掉一个生命周期的标注就很神奇地通过了.
然而, 到了把 parser 和 semantic 两个板块和在一起的时候, 问题就出现了! 由于 SymbolCollector
需要将 ASTNode
中的许多信息提取出来临时存储, 这就导致了他必须要获得 ASTNode
的可变引用, 然而这些信息里面有些被标明了生命周期 'a
, 所以 Collector
也要获得 'a
的可变引用, 也就是把 ASTNode
的整个生命周期夺走了, 这当然是不可接受的!
那就没办法了, 重构吧.
小修小补: 分模块划分生命周期
ChatGPT 告诉我上面这种情况可以通过细分生命周期来解决, 于是我就这么做了, 然后惊奇地发现: Parser
和 Collector
不能划分到两个生命周期! 原来是 ASTNode
并没有获得其自身信息到完整所有权, 还有一个 &'a str
指向的是 Parser
的生命周期, 因此 ASTnode
没办法跳出 Parser
.
那好像得重构更多了…
摆烂阶段: Copy anything
一个解决资源共享最简单的方案当然就是存储多份副本, 我就采取了这种方案. 把每一个字符串都用 String
存, 每一个 Info
都用 Rc<>
套壳. 这样一来, 就完全没有所谓的信息共享了. 但是作为一个 C/C++ 程序员, 我写着写着就无法容忍自己写出的 copy on share
代码, 这样还不如去写 Java 呢.
然后开始有脑子的重构.
试图动脑: 开始规划生命周期的细节
到这里才算是对 Rust 的生命周期有了一点粗浅的认知, 于是开始规划生命周期的细节. 首先各类信息的最终所有者当然都应该是 ASTNode
, 所有就在这里面存储原始变量, 其他地方的字符串和 Info
都存为 &'a
的引用 (毕竟不可变), Table
还是照旧用 Rc<RefCell<Table<'a>>>
存储. 这样一来, ASTNode
就可以在 Parser
和 Collector
之间传递了, 也不会有生命周期的问题.
很遗憾, 编译器告诉我, 我还是过分乐观了. 即使我这样做, 编译器也无法识别出来 Collector
只需要短时间的可变引用, 于是这里又得变成 unsafe. 前面初始化 Info
的时候问题就更大了, 这里因为存在自身引用, 只能进行生命周期强转, 不行的是, 在强转完以后很快节点就发生了所有权转移, 此前强转得到的引用不出意外地失效了… 看来所有权也同样是一个需要精心设计的东西…
思考: Rust 真的适合用来写编译器吗
The reason not to go for Rust is simple: Lifetimes make flexible data abstraction rather annoying, and you really, really don’t need that fine-grained control over allocation. GC is perfectly fine.
在 Reddit 看到的一句话, 姑且先在这里记录下来. 希望后面能找到上述问题的解决方案.