IR:把用户名字变成内部名字
本节阅读量:解释器处理变量时,可以在 environment 里直接保存运行时值:
|
|
编译器不能让 CPU 在运行时直接查找源码里的字符串 x。最终汇编操作数必须落实为立即数、寄存器或内存地址,因此编译器要先解析这个名字指向哪个绑定。
第二章的编译器先做一件中间工作:
|
|
例如:
|
|
后面的汇编生成器再把 IR 名字放到栈槽里:
|
|
先把三个阶段放在一起,避免把它们当成同一张表:
| 阶段 | 什么时候存在 | 对应关系 | 用途 |
|---|---|---|---|
| 解释器环境 | 执行 mini run 时 |
x -> 40 |
查到变量当前的运行时值 |
| lowerer 环境 | 生成 IR 时 | x -> temp(x0) |
把源码名字解析为唯一 IR 结果 |
| 栈槽表 | 生成汇编时 | x0 -> -8(%rbp) |
把 IR 结果落实为内存位置 |
lowerer 环境只是编译期间使用的数据结构,不会被放进生成的程序。目标程序真正运行时,-8(%rbp) 这个位置才会保存数值 40。
普通 let 的 IR
源码:
|
|
IR:
|
|
这里的 x0 是 let 绑定的内部名字,t.0 是加法结果的临时名字。源码 identifier 不允许数字和点号,因此这两类内部名字都不会与用户能够写出的名字冲突。点号专门区分普通计算临时值:用户变量 t 的绑定名会变成 t0,不会和 t.0 混在一起。
例如:
|
|
会得到互不冲突的内部名字:
|
|
为什么不直接继续叫 x?因为 shadowing 会出现多个同名用户变量:
|
|
如果 IR 里都叫 x,内层和外层就混在一起了。
IR 新增 copy 操作
第一章的 IR 只有加法:
|
|
第二章需要表示:
|
|
生成 copy 是当前朴素 IR 的教学选择,不是实现不可变 let 的唯一办法。编译器理论上可以直接把 x 映射到 value 的操作数;这里仍然生成 x0 = value,是为了让每次绑定都有独立、可观察的内部身份,后续也能统一分配位置。第一版不做删除多余 copy 的优化。
这一步不是加法,只是把一个操作数保存到一个内部名字里,所以 IR 增加了 copy:
|
|
打印 IR 时,copy 写成:
|
|
例如:
|
|
lower_expr 的环境
代码在:
|
|
IR lowerer 也需要环境。为了和解释器里的 eval_expr(expr, env) 对齐,第二章开始把环境显式作为 lower_expr 的参数:
|
|
它保存的不是运行时值,而是:
|
|
例如:
|
|
这里的 OperandKind::temp 泛指“一个有名字的 IR 结果”。x0 是绑定的内部结果名,t.0 是计算产生的临时结果名;来源不同,但后续汇编生成器都可以把它们分配到栈槽。
查找逻辑和解释器类似:
|
|
同样是从后往前找,用来支持 shadowing。
变量引用如何 lowering
变量引用分支很短:
|
|
可以读成:
|
|
如果环境里有:
|
|
那么 Var(x) lowering 后就返回操作数 x0。
let 如何 lowering
let 分支按顺序完成这些步骤:
|
|
按顺序读:
|
|
这里的 value 在加入新绑定之前 lower,正好对应解释器规则:
|
|
第 6 步的 env.pop_back() 只结束源码名字在 lowerer 中的可见范围。它发生在编译期间,不会生成汇编 popq,也不会立即释放 x0 的栈槽。lowering 先生成整段 IR,汇编生成器随后为所有 op.dst 一次分配固定栈槽;目标函数返回时才整体收回栈帧。
当前 Op 结构为了保持第一章的小模型,所有操作都有 lhs 和 rhs 字段。copy 只使用 lhs,rhs 是占位;真正决定怎么打印和生成汇编的是 OpKind::copy。
从 IR 看 value 和 body 的环境
shadow_value.lang 最能看出新绑定加入的时机:
|
|
生成的 IR 是:
|
|
内层 value (+ x 1) 使用的是旧环境,所以读到外层 x0。等 t.0 算好以后,lowerer 才建立内层绑定 x1;内层 body 的 x 因而返回 x1。
shadowing 的 IR
运行:
|
|
输出:
|
|
对应关系是:
|
|
IR 把这个关系写得很清楚。下一节会把 x0、x1、t.0 这些 IR 名字分配到栈槽。