Skip to content

后端与系统工具链

后端到底在做什么

到了后端阶段,前面的工作基本已经把程序逻辑整理清楚了。

后端这时候不该再重新猜程序想干什么了。前面几层已经把结构和语义整理过一遍,后端要做的是把这些结果翻成真正能继续构建的代码。

当前项目里,这个目标代码不是原生汇编,而是规范化 C。

为什么先输出 C

这件事值得反复强调,因为它决定了整个项目第一代的形态。

输出规范化 C 的好处包括:

  • 可以立刻借助系统编译器生成可执行文件
  • macOS 和 Linux 上都更容易工作
  • 中间产物更容易人工阅读
  • 不用马上处理 ABI、汇编语法和目标文件细节

所以这个后端更像一座桥,而且是一座先求稳、先能走人的桥,不是一步到位就奔着最炫的原生汇编去。

当前后端其实也有两种模式

1. IR Backend

如果程序走基础 int 子集路径,后端读取的是 IR。

此时它会把每条 IR 指令翻译成较直接的 C 语句,例如:

  • const -> 常量赋值
  • copy -> 普通赋值
  • binary -> 二元表达式
  • jump -> goto
  • cjump -> if ... goto ... else goto ...
  • return -> return

这一条路径的特点很简单:前面的 lowering 已经把控制流拆开了,后端不用再猜 ifwhilefor 的执行顺序。

2. AST Backend

如果程序用了 char、字符串、数组、指针等扩展特性,后端读取的就是 AST。

此时它不是按 IR 指令翻译,而是直接把 AST 重新渲染成 C 结构,例如:

  • 指针声明
  • 数组声明
  • 下标访问
  • 字符串字面量

这条路径会保留更多源码里的原始长相,因为这些高级特性现在还没有统一压进那套 IR。

什么叫“规范化 C”

这里的“规范化”不是说它写得多优雅,而是说它写得够规矩、够统一、够稳定。

例如:

  • 生成形式尽量统一
  • 局部变量和临时变量显式声明
  • 控制流直接展开
  • 输出风格更利于检查和测试

也就是说,这份 C 代码首先是写给编译器自己和调试者看的,其次才是写给人欣赏的。

为什么系统工具链仍然不可少

即使后端已经生成了 C,项目本身还没有直接产出最终可执行文件。

最后这一步仍然需要系统工具链来完成,例如 clangcc

它们负责处理:

  • 真正的 C 编译
  • 目标文件生成
  • 链接
  • 平台相关细节

这也就是 toolchain.py 真正在接手的地方。到了这里,项目自己的工作差不多已经做完了,剩下的是把生成结果安全地交给外部工具链。

ToolchainDriver 在做什么

当前 ToolchainDriver 主要做三件事:

  1. 选择后端并生成后端文本
  2. 把生成结果写到中间 .generated.c 文件
  3. 调用系统里的 C 编译器,产出最终可执行文件

这里把中间文件真的写到磁盘上,不是多此一举,而是很有用,因为:

  • 构建失败时可以直接翻生成代码
  • 你能看到编译器到底吐出了什么
  • 做回归测试和手工排错都方便

为什么这条设计很适合第一代

如果第一代就直接做原生汇编后端,需要同时解决:

  • 指令选择
  • 寄存器分配
  • 调用约定
  • 栈帧管理
  • 平台差异

那样复杂度会一下子冲上去,前面很多本来已经讲清楚的东西,也会被更底层的细节淹掉。

而当前设计的聪明地方就在于,它把难题拆开了:

  • 先把前端和中间层做扎实
  • 再用规范化 C 先跑通执行链
  • 以后如果需要,再把后端替换成更底层实现

这不是“保守”,更像是先把能跑通、能看懂、能验证的部分做扎实,再去碰更难啃的底层骨头。