章节目录

本章总结

本节阅读量:

第二章在第一章的整数和加法之外,加入了两种表达式:

1
2
变量引用
(let name value body)

程序从此不只是在计算数字,也开始处理名字和作用域。

前端为此增加了 VarExpr 和 LetExpr。parser 在表达式位置看到 identifier 时生成变量节点,在列表 head 看到 let 时读出绑定名、value 和 body。AST 只保存结构,不在 parser 里查找或替换变量。

解释器增加了 environment:

1
用户变量名 -> 运行时值

解释 let 时,先在旧环境中计算 value,再加入新的绑定并计算 body,最后移除这个绑定。查找变量时从后往前找,因此更近的同名绑定会暂时遮住外层绑定。

编译器也有环境,但保存的内容不同:

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

每个 let 绑定都会得到一个新的内部名字。即使源码里有两个同名的 x,lowering 后也可以变成 x0 和 x1,不会混在一起。

三个容易混淆的层次可以这样比较:

层次 何时使用 保存什么 x 的例子
解释器环境 解释程序时 源码名字到运行时值 x -> 40
lowerer 环境 生成 IR 时 源码名字到 IR 操作数 x -> temp(x0)
栈槽表 生成汇编时 IR 名字到内存偏移 x0 -> -8(%rbp)

lowerer 离开 body 时弹出的是名字对应关系,不是目标程序的栈槽。当前汇编生成器在函数开头一次分配所有槽位,在函数返回时才整体收回。

为了表示一次绑定,IR 新增了 copy:

1
x0 = 40

汇编生成器沿用第一章的栈槽分配和固定栈帧,只增加一个 copy 分支,经 %rax 把操作数保存到目标栈槽。shadowing 不需要新的汇编语法,因为不同绑定在 IR 中已经有不同名字,随后自然得到不同栈槽。

把同一个语言规则放在两条路径里比较:

问题 解释器 编译器
变量引用 从环境查到运行时值 从 lowerer 环境查到 IR 操作数
进入 let body 压入 name -> value 压入 name -> 内部名字
同名遮蔽 反向查找最近的运行时绑定 反向查找最近的内部名字
离开作用域 弹出解释器绑定 弹出编译期名字关系;栈槽暂不释放

它们实现的是同一套语言语义:

1
2
3
4
let 的 value 在旧环境中求值
let 的 body 在加入新绑定后的环境中求值
同名时,更近的绑定优先
离开作用域后,外层绑定重新可见

下一章会加入判断和 if。那时新的问题不再只是“名字指向哪里”,还包括“程序应该选择哪一条分支”。


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

上一节

2.8 练习

下一节


本节目录