第八章:让名字指向可以改变的位置
本节阅读量:前七章里,let 建立的绑定一旦完成,名字读到的 Value 就不会再变。闭包只要保存自由变量当时的 Value,离开定义位置以后仍然能正确工作。
本章加入可变状态。先看一个会打破旧模型的程序:
|
|
read 创建时,x 是 40;调用它之前,set! 又把 x 改成了 42。语言规定最终结果是:
|
|
如果闭包仍然把整数 40 复制进自己的环境,它就只能返回旧值。要让外层代码和闭包看见同一次修改,双方必须共享某个可以存放 Value 的位置。
第八章因此把变量模型从:
|
|
改成:
|
|
这不是多加一层无关的包装。它把三个原本容易混在一起的概念拆开了:
| 概念 | 例子 | 会不会改变 |
|---|---|---|
| 名字 | 源码里的 x |
同一词法绑定中的拼写不变 |
| 位置 | x 对应的 location 或 cell |
绑定建立后保持共享 |
| 值 | location 当前保存的 40、42 或 closure |
可以被 set! 替换 |
核心句子是:set! 不会让名字改指另一个绑定,而是替换这个名字当前 location 中保存的 Value。
本章增加两种表达式
表面语言新增:
|
|
set! 修改一个已经存在的词法绑定,并返回写入的新值。begin 先执行 first,丢弃它的结果,再执行并返回 second。有了明确的顺序,读者才能写出“先修改,再读取”的程序。
最小例子是:
|
|
求值过程可以写成:
|
|
结果为 42。
先把程序跑起来
进入第八章代码目录并构建:
|
|
输出:
|
|
再运行开头那个“修改以后由闭包读取”的例子:
|
|
输出同样是:
|
|
这个结果验证的不是普通加法,而是外层代码与闭包共享同一个 location。
counter 把共享状态变成函数自己的记忆
运行:
|
|
对应程序是:
|
|
第一次调用 counter 把 start 从 40 改为 41,第二次从同一个位置读到 41,再改为 42。返回的 closure 不只带着一个旧值;它带着以后仍能读取和修改的 location。
这也是状态开始有用的地方:一个函数可以在多次调用之间保留记忆,而不需要全局变量。
解释器和编译器采用同一语义
两条实现路线使用不同的数据结构,但表达的是同一件事:
|
|
解释器中的 Location 是 Store 向量的索引,适合直接说明语义。编译器中的 cell 是真正分配在堆上的对象,closure 捕获的是 tagged cell pointer。二者不必拥有相同的 C++ 类型,只要都保证所有使用者访问同一个位置。
IR 会把隐含的位置写出来
运行:
|
|
会看到:
|
|
这几行第一次明确区分了位置和值:
|
|
闭包捕获的也不再是 load 得到的当前内容,而是 x0.cell 本身。后面的 IR 一节会继续展开这条路径。
cell 延续第六章的堆对象协议
第六章建立了共同 heap-object tag 和 header,第七章让 closure 复用它。本章再增加第三种对象:
|
|
cell 只有一个 Value payload:
|
|
record、closure 和 cell 继续共用 pointer tag 4,具体种类由 header 判断。这一统一布局会成为下一章垃圾回收扫描对象图的基础。
编译和观察退出状态
生成汇编:
|
|
compile 仍然只输出 x86-64 AT&T 汇编,不负责调用链接器。Linux、WSL 和 Intel Mac 可以手动执行:
|
|
Apple Silicon Mac 需要生成 x86-64 可执行文件:
|
|
退出状态为 42。mini run 会显示完整 Value;手动链接后的示例仍沿用进程退出状态观察整数结果。
本章正文怎样展开
接下来的内容沿着一次完整实现逐层推进:
- 先确定
set!、二元begin、遮蔽和共享 cell 的语言行为; - 给 AST 和 parser 增加两个节点,并补全自由变量分析;
- 在解释器里真正拆开 Environment 与 Store;
- 在结构化 IR 中加入
cell、load、store; - 把 cell 落到统一的堆对象布局和 x86-64 指令;
- 用验收清单和练习检查共享、独立与递归状态。
每一步都只回答当前层的问题。parser 不负责分配 cell,解释器不需要模拟机器字节偏移,汇编生成器也不重新解释源码名字。
已有能力不会回退
第八章继续支持:
- 整数、负整数、加法、
sub1、变量和let; eq?、if、函数值、调用和letrec;- 词法闭包、嵌套闭包和外层自由变量捕获;
- 空及可变长度 record、
record?、size和安全get; - record 身份比较,以及 record 与 closure 互相保存。
第六章的 record 仍然不可变。本章没有 set-field!,set! 的目标只能是变量名。第四章的 cin、cout 实验也不会在这里重新出现。
学完以后应该能回答
读完本章,你应该能解释:
- 为什么只捕获当前 Value 无法实现可变闭包;
- name、Location 和 Value 分别是什么;
- 为什么
set!修改的是已有 location,而不是创建新绑定; - 为什么每次函数调用都需要新的参数 location;
- 为什么两个 closure 可以共享一个 cell,而两次 factory 调用又应得到独立 cell;
- 为什么可变
letrec名字需要让递归 closure 保存 self location; - cell 怎样复用 record 和 closure 已经使用的堆对象协议。