IR:把用户名字变成内部名字

本节阅读量:

解释器处理变量时,可以在 environment 里直接保存运行时值:

1
x -> 40

编译器不能把源码里的 x 原样带到汇编里。汇编没有这种用户变量名。

第二章的编译器先做一件中间工作:

1
用户变量名 -> IR 内部名字

例如:

1
x -> x0

后面的汇编生成器再把 IR 名字放到栈槽里:

1
x0 -> -8(%rbp)

普通 let 的 IR

源码:

1
(let x 40 (+ x 2))

IR:

1
2
3
x0 = 40
t.0 = x0 + 2
return t.0

这里的 x0let 绑定的内部名字,t.0 是加法结果的临时名字。点号是有意保留给编译器临时值的:源码 identifier 不允许点号,绑定内部名由“源码名字 + 数字”组成,因此用户变量 t 会变成 t0,不会和 t.0 冲突。

例如:

1
(+ (let t 1 t) (+ 2 3))

会得到互不冲突的内部名字:

1
2
3
4
t0 = 1
t.0 = 2 + 3
t.1 = t0 + t.0
return t.1

为什么不直接继续叫 x?因为 shadowing 会出现多个同名用户变量:

1
(let x 10 (+ (let x 32 x) x))

如果 IR 里都叫 x,内层和外层就混在一起了。

IR 新增 copy 操作

第一章的 IR 只有加法:

1
t.0 = 40 + 2

第二章需要表示:

1
x0 = 40

这一步不是加法,只是把一个操作数保存到一个内部名字里,所以 IR 增加了 copy

1
2
3
4
enum class OpKind {
    copy,
    add,
};

打印 IR 时,copy 写成:

1
dst = lhs

例如:

1
x0 = 40

Lowerer 里的环境

代码在:

1
code/02_let/src/compile/ir.cpp

IR lowerer 也有一个环境:

1
std::vector<std::pair<std::string, Operand>> env_;

它保存的不是运行时值,而是:

1
用户变量名 -> IR 操作数

例如:

1
x -> temp(x0)

查找逻辑和解释器类似:

1
2
3
4
5
6
7
8
Operand lookup(const std::string& name) const {
    for (auto it = env_.rbegin(); it != env_.rend(); ++it) {
        if (it->first == name) {
            return it->second;
        }
    }
    throw std::runtime_error("undefined variable: " + name);
}

同样是从后往前找,用来支持 shadowing。

变量引用如何 lowering

变量引用分支很短:

1
2
3
4
case ExprKind::variable: {
    const auto& var_expr = static_cast<const VarExpr&>(expr);
    return lookup(var_expr.name);
}

可以读成:

1
2
Var(x) 本身不生成新 IR。
它只是查出 x 当前对应哪个操作数。

如果环境里有:

1
x -> x0

那么 Var(x) lowering 后就返回操作数 x0

let 如何 lowering

let 分支按顺序完成这些步骤:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
case ExprKind::let: {
    const auto& let_expr = static_cast<const LetExpr&>(expr);
    Operand value = lower_expr(*let_expr.value, program);
    std::string local = let_expr.name + std::to_string(next_local_++);
    program.ops.push_back({OpKind::copy, local, std::move(value), Operand::integer(0)});
    env_.push_back({let_expr.name, Operand::temp(local)});
    Operand result = lower_expr(*let_expr.body, program);
    env_.pop_back();
    return result;
}

按顺序读:

1
2
3
4
5
6
7
1. 先 lower value,得到 value 的结果在哪里。
2. 生成一个内部名字,例如 x0。
3. 追加 copy 操作:x0 = value。
4. 在环境里加入:用户名 x -> IR 名字 x0。
5. lower body。
6. 离开 let,弹出这个绑定。
7. 返回 body 的结果操作数。

这里的 value 在加入新绑定之前 lower,正好对应解释器规则:

1
2
let 的 value 在旧环境里求值。
let 的 body 在扩展后的环境里求值。

当前 Op 结构为了保持第一章的小模型,所有操作都有 lhsrhs 字段。copy 只使用 lhsrhs 是占位;真正决定怎么打印和生成汇编的是 OpKind::copy

shadowing 的 IR

运行:

1
2
3
cd code/02_let
make
./mini ir examples/shadow.lang

输出:

1
2
3
4
x0 = 10
x1 = 32
t.0 = x1 + x0
return t.0

对应关系是:

1
2
外层 x -> x0
内层 x -> x1

IR 把这个关系写得很清楚。下一节会把 x0x1t.0 这些 IR 名字分配到栈槽。


2.4 解释器:环境与变量查找

上一节

2.6 汇编:为 copy 增加翻译规则

下一节