本章总结
本节阅读量:第二章在第一章的整数和加法之外,加入了两种表达式:
|
|
程序从此不只是在计算数字,也开始处理名字和作用域。
前端为此增加了 VarExpr 和 LetExpr。parser 在表达式位置看到 identifier 时生成变量节点,在列表 head 看到 let 时读出绑定名、value 和 body。AST 只保存结构,不在 parser 里查找或替换变量。
解释器增加了 environment:
|
|
解释 let 时,先在旧环境中计算 value,再加入新的绑定并计算 body,最后移除这个绑定。查找变量时从后往前找,因此更近的同名绑定会暂时遮住外层绑定。
编译器也有环境,但保存的内容不同:
|
|
每个 let 绑定都会得到一个新的内部名字。即使源码里有两个同名的 x,lowering 后也可以变成 x0 和 x1,不会混在一起。
三个容易混淆的层次可以这样比较:
| 层次 | 何时使用 | 保存什么 | x 的例子 |
|---|---|---|---|
| 解释器环境 | 解释程序时 | 源码名字到运行时值 | x -> 40 |
| lowerer 环境 | 生成 IR 时 | 源码名字到 IR 操作数 | x -> temp(x0) |
| 栈槽表 | 生成汇编时 | IR 名字到内存偏移 | x0 -> -8(%rbp) |
lowerer 离开 body 时弹出的是名字对应关系,不是目标程序的栈槽。当前汇编生成器在函数开头一次分配所有槽位,在函数返回时才整体收回。
为了表示一次绑定,IR 新增了 copy:
|
|
汇编生成器沿用第一章的栈槽分配和固定栈帧,只增加一个 copy 分支,经 %rax 把操作数保存到目标栈槽。shadowing 不需要新的汇编语法,因为不同绑定在 IR 中已经有不同名字,随后自然得到不同栈槽。
把同一个语言规则放在两条路径里比较:
| 问题 | 解释器 | 编译器 |
|---|---|---|
| 变量引用 | 从环境查到运行时值 | 从 lowerer 环境查到 IR 操作数 |
进入 let body |
压入 name -> value |
压入 name -> 内部名字 |
| 同名遮蔽 | 反向查找最近的运行时绑定 | 反向查找最近的内部名字 |
| 离开作用域 | 弹出解释器绑定 | 弹出编译期名字关系;栈槽暂不释放 |
它们实现的是同一套语言语义:
|
|
下一章会加入判断和 if。那时新的问题不再只是“名字指向哪里”,还包括“程序应该选择哪一条分支”。
本节目录