IR 与 Lowering¶
为什么 AST 之后还要再来一层¶
很多人第一次看编译器都会问:AST 不是已经很结构化了吗,为什么还要继续变?
答案是,AST 更适合表达“语法结构”,但后端更喜欢“动作更明确”的表示。
例如 if、while、for 在 AST 里还是高层语句节点,但后端真正关心的是:
- 条件先怎么算
- 分支往哪里跳
- 循环从哪里开始
- 循环在哪里结束
这就是 IR 要出场的原因:AST 已经很清楚了,但对后端来说还不够“能直接开工”。
先用一个小例子感受 AST 和 IR 的差别¶
看这段代码:
在 AST 里,它更像这样:
这很适合表达“语法长什么样”,但还不够适合后端直接生成代码。
到了 IR,更接近:
t0 = load a
t1 = load b
t2 = binary < t0 t1
cjump t2 if_then_0 if_else_1
label if_then_0
return t0
jump if_end_2
label if_else_1
return t1
label if_end_2
这时程序已经不再像挂在墙上的结构图,而更像一张“先做什么、再做什么、跳到哪里去”的执行清单。
这个项目的 IR 长什么样¶
当前 IR 不是 SSA,也不是特别重型的设计,而是一种简洁的三地址风格表示。
它主要由这些元素组成:
- 全局变量
- 函数
- 临时变量
- 指令
- 标签
指令层面常见的有:
constcopyloadstoreunarybinaryjumpcjumpcallreturn
如果你把这套指令和 src/c_core_compiler/ir.py 对照着看,会发现它故意保持得不多。意思很明确:先让前后端之间有条能走通的路,不急着再发明一门更复杂的小语言。
为什么说它更接近“动作序列”¶
举个例子,AST 里的 if 可能只是一个节点,里面挂着条件、then 分支、else 分支。
但到了 IR,它会被显式展开成类似这种思路:
- 先求条件值
- 做条件跳转
- 落到 then 标签
- 执行 then 代码
- 跳到 end
- 如果有 else,再落到 else 标签
这样后端不需要理解高层控制流结构,只需要忠实输出显式动作。
这里其实就是 IR 的精髓:源码里那些“读的人脑补出来的执行顺序”,到了这里都得摊开写明白。
什么叫 lowering¶
lowering 可以理解成“把高层结构往低层结构展开”。
这个项目里的 IRBuilder 做的就是这件事。
它不是简单复制 AST,而是把 AST 改写成:
- 更显式的控制流
- 更显式的中间结果
- 更适合后端处理的形式
在当前项目里,这件事主要发生在:
src/c_core_compiler/ir_builder.py
你可以直接把这个文件理解成“把树拆成步骤表”的地方。
临时变量在这里为什么重要¶
表达式一旦拆开计算,就需要地方存中间结果。
例如:
一种典型展开方式会变成:
这里的 t0、t1 就是临时变量。
它们的存在让复杂表达式可以被拆成一系列小步骤。
如果没有临时变量,后端每次都得抱着整棵表达式树往下翻,既别扭,也不方便后面整理。
控制流结构是怎么被展开的¶
if¶
if 会被展开成:
- 条件求值
- 条件跳转
- then 标签
- else 标签
- end 标签
while¶
while 会被展开成:
- 条件标签
- 循环体标签
- 结束标签
并通过 jump / cjump 明确循环回边。
for¶
for 会被统一拆成四段:
- 初始化
- 条件
- 循环体
- 更新
这很重要,因为它把表面上一个语法结构,拆成后端更容易消费的标准形态。
也就是说,IRBuilder 不是照着源码誊写一遍,而是在主动把不同表面长相的语法,拧成后端更顺手的套路。
为什么 && 和 || 不能当普通二元运算处理¶
因为它们带短路语义。
例如:
如果 a 已经是假,就不应该继续求 b。
所以在 lowering 时,这类表达式不能只翻译成普通 binary。它必须通过分支和标签来保留“是否继续计算右边”的语义。
当前项目在 IRBuilder 里就专门把这件事显式展开了。
这也是为什么你第一次看 IR 时,常常会有一种感觉:源码明明就几行,怎么到了这里突然话这么多。不是编译器变笨了,而是它把原来藏在语法糖背后的执行顺序全摊出来了。
当前 IR 的边界在哪里¶
非常关键的一点是:当前 IR 主要覆盖基础 int 子集。
如果 AST 包含这些内容:
char- 数组
- 指针
- 字符串字面量
- 取地址或解引用
那么当前实现会判定“需要 AST Backend”,而不是继续往这套 IR 下降。
这说明当前 IR 还不是项目里所有语言特性的统一表示层。
这不是嘴上说说,代码里真有这层判断:ir_builder.py 会检查 AST 里有没有超出当前 IR 能力圈的东西,一旦有,就不硬降,直接切去 AST Backend。
这是不是问题¶
这不是 bug,而是阶段性边界。
更准确地说,当前项目做了一个工程选择:
- 先把 IR 路径在基础子集上打磨清楚
- 先保证复杂特性可用
- 之后再考虑把复杂特性并入统一 IR
这比表面上看着统一、实际上里面一团线头要稳得多。
所以你可以把当前 IR 理解成:
- 它已经足够解释基础
int子集的 lowering 思路 - 但它还不是整个项目所有语言特性的最终统一层
如何观察这一层¶
可以直接运行:
或者:
前者适合看线性 IR 文本,后者适合看控制流图。
如果你想真正理解这个编译器的“中间层思维”,这一层最值得仔细看。
一个很实用的学习方式是:
- 先拿
hello.c看最简单表达式和返回 - 再拿
control_flow_demo.c看标签和跳转 - 最后拿
pointer_demo.c对照“为什么这类特性没有进入当前 IR”
这样你会很清楚地看到:IR 不是换个术语吓人,它就是这个项目现在真正拿来承上启下的中间执行模型。