Blaise 是一个从零构建的现代 Object Pascal 编译器项目,在 v0.10 阶段已完成自举并进入多文件编译阶段。与 Free Pascal 和 Delphi 不同,Blaise 采用「Pipeline-First」策略:先打通最小可行编译管道,再逐步扩展功能。本文聚焦其原生后端架构中的三个关键工程决策:QBE 后端的选型逻辑、当前 whole-programme 编译策略的权衡,以及 ARC 内存模型对多线程运行时的影响。
QBE 后端:轻量优先的架构选择
Blaise 的当前后端基于 QBE—— 一个约 13,000 行 C 代码的轻量级编译器后端,支持 x86_64、ARM64 和 RISC-V 目标。项目采用「QBE First, LLVM Second」的分阶段策略,这一选择背后有明确的工程考量。
选型权衡矩阵
| 维度 | QBE | LLVM |
|---|---|---|
| 代码复杂度 | 极低(13k 行) | 极高(百万行级) |
| 编译速度 | 快(开发循环友好) | 慢(链接和优化耗时) |
| API 稳定性 | 高(版本固定) | 中(存在 API 变更) |
| 优化能力 | 良好 | 工业级 |
| 目标平台 | x86_64/ARM64/RISC-V | 10+ 平台含 WASM |
Blaise 将 QBE 源码直接内嵌在 vendor/qbe/ 目录中,从源码构建并固定版本,彻底消除 API 变更风险。编译器通过 ICodeGen 接口抽象后端操作,使未来添加 LLVM 后端时只需实现同一接口,无需改动前端解析器或类型检查器。
type
ICodeGen = interface
procedure BeginModule(const Name: string);
procedure EmitFunction(AFunc: TASTFunction);
procedure EmitGlobal(AGlobal: TASTGlobal);
procedure Finalise(const OutputPath: string);
end;
这种设计允许 Blaise 在 v0.10 阶段专注于语言语义和类型系统的正确性,将工业级优化和跨平台支持推迟至 Phase 6 的 LLVM 后端实现。
Whole-Programme 编译:当前的增量编译策略
Blaise v0.10 采用 whole-programme 编译模式:每次构建时从源码重新编译所有单元,生成单一的 QBE IR 输出,不生成 .ppu 缓存文件。这一策略在自举阶段具有明确优势,但也带来编译时间随代码规模线性增长的挑战。
依赖解析机制
TUnitLoader 模块负责解析 uses 子句,通过后序 DFS(深度优先搜索)进行依赖排序,并检测循环依赖。语义分析器通过 AnalyseUnitForExport 将接口段符号提升至全局作用域,供后续单元引用。代码生成器使用 AppendUnit/AppendProgram 将多单元 IR 累积到单一输出缓冲区,确保字符串字面量标签全局唯一。
当前阶段的权衡
| 策略 | 优势 | 局限 |
|---|---|---|
| Whole-Programme | 实现简单、无缓存失效问题、字节级可重现 | 编译时间 O (n)、无增量能力 |
| 计划中的增量编译 | 快速反馈、适合 LSP 场景 | 需依赖追踪和缓存失效逻辑 |
根据设计文档,Phase 7(LSP + VS Code 扩展阶段)将引入增量编译支持,目标是实现「fast feedback」的 IDE 体验。当前开发者可通过 --emit-ir 标志查看 QBE 中间表示,用于调试编译器本身的行为。
ARC 运行时与多线程考量
Blaise 采用统一的自动引用计数(ARC)内存模型,覆盖字符串、类和接口。这一决策消除了 Delphi 中 TObject 与 TInterfacedObject 的分裂,但也对运行时设计和多线程支持提出了特殊要求。
内存布局
UTF-8 字符串在堆上的布局为:4 字节引用计数 + 4 字节长度 + 4 字节容量 + N 字节 UTF-8 数据 + 1 字节 NUL。字符串变量在栈上仅为 8 字节指针。编译器在赋值点和作用域退出处自动插入 _StringAddRef / _StringRelease 调用。
多线程安全
ARC 的引用计数操作需要线程安全。Blaise 的运行时库(blaise_arc.c)使用原子操作实现引用计数增减,确保在多线程环境下的正确性。[Weak] 属性用于打破循环引用,通过 _WeakAssign / _WeakClear / _WeakZeroSlots 实现弱引用语义。
异常处理模型
Blaise 使用基于 setjmp/longjmp 的异常处理机制。编译器为每个 try 块分配 512 字节的栈帧,插入 _PushExcFrame 和 setjmp 调用。在异常路径上,编译器通过 EmitExcPathArcCleanup 生成 ARC 清理代码,确保在抛出异常前释放作用域内的字符串和对象引用。
工程实践建议
基于 Blaise v0.10 的架构特点,以下是针对编译器开发者和使用者的实践建议:
构建优化
- 利用 PasBuild 的多模块布局隔离编译单元,减少单次编译范围
- 在开发阶段使用
--emit-ir检查生成的 QBE IR,定位代码生成问题 - 关注
scripts/rolling-bootstrap.sh的滚动自举脚本,理解版本间的兼容性边界
内存管理
- 优先使用
[Weak]打破对象图中的循环,避免内存泄漏 - 理解
Free作为_ClassRelease同义词的语义,逐步迁移显式释放代码 - 在多线程场景中注意共享对象的引用计数竞争,必要时使用同步原语
迁移准备
- Blaise 计划在 Phase 8 推出迁移分析器,评估现有 FPC/Delphi 代码的兼容性
- 当前应避免使用
with语句、旧式object类型和 COM GUID,这些特性已被明确移除
资料来源
- Blaise 官方仓库与设计文档:https://github.com/graemeg/blaise
- QBE 编译器后端项目:https://c9x.me/compile/
内容声明:本文无广告投放、无付费植入。
如有事实性问题,欢迎发送勘误至 i@hotdrydog.com。