Hotdry.

Article

Blaise v0.10 原生后端架构:QBE 增量编译策略与 ARC 运行时权衡

剖析 Blaise 编译器 v0.10 的 QBE 后端选型逻辑、whole-programme 编译策略的取舍,以及 ARC 内存模型对多线程运行时的影响。

2026-06-10compilers

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 中 TObjectTInterfacedObject 的分裂,但也对运行时设计和多线程支持提出了特殊要求。

内存布局

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 字节的栈帧,插入 _PushExcFramesetjmp 调用。在异常路径上,编译器通过 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,这些特性已被明确移除

资料来源

compilers

内容声明:本文无广告投放、无付费植入。

如有事实性问题,欢迎发送勘误至 i@hotdrydog.com