小结:从一个 Value 到一张对象图
本节阅读量:第六章增加的不只是 (record ...) 语法。我们第一次让生成程序按照源码决定对象大小,在堆上创建对象,并让一个 64 位 tagged Value 引用它。
现在语言可以表达:
|
|
record 不可变、长度可变、允许为空,并按对象身份参与 eq?。
语言语义已经闭合
四种操作的规则是:
| 表达式 | 行为 |
|---|---|
(record e...) |
字段从左到右求值,每次创建独立对象 |
(record? e) |
对任意 Value 返回整数 1 或 0 |
(size e) |
record 返回字段数,其他值报错 |
(get e i) |
record 且 i < size 时返回字段,否则报错 |
index i 是非负整数字面量。parser 接受 (get r 2) 和 (get r 01),拒绝负数、变量与表达式 index;实际越界由运行时根据对象 header 判断。
空 record 没有特殊例外:
|
|
结果依次是 0、1、0。它是真正分配的零字段对象,所以两次创建具有不同身份。
AST、解释器和 IR 各有一种 vector
可变字段数量需要 C++ 容器,但三个阶段保存的内容不同:
|
|
这三种 std::vector 都是实现细节。源语言没有增加 vector、struct、class 或方法系统。
统一机器表示分成 tag 与 header
一个机器字先区分大类:
|
|
heap-object tag 4 不专属于 record。对象自己的 header 再记录:
|
|
本章 HeapKind::record = 1。因此 record 的布局是:
|
|
通用公式:
|
|
(record) 分配 8 字节并写 header 1;(record 40 2) 分配 24 字节并写 header 513;这些都是同一公式的实例,不再是固定 record 布局。
解释器与编译器保持同一行为
解释器使用 shared_ptr<RecordValue> 表达共享身份,用 vector<Value> 保存字段。生成代码使用带 tag 的堆地址和 payload 机器字。
两条路线都实现:
- 字段从左到右求值;
- 每次 record 表达式独立创建,包括空 record;
- 字段可保存任意已有 Value;
- record 在条件中为真;
eq?比较身份,不比较结构;record?同时识别空和非空 record;size/get检查对象种类;get在读取前检查实际边界。
解释器在错误时给出文字诊断;编译后的 size/get 失败调用 exit(70)。不同的错误呈现方式没有改变成功程序与失败条件的语义。
编译路径没有绕过 IR
两条命令共享同一 lowering:
|
|
record 相关 IR 形状是:
|
|
汇编 emitter 根据字段 operand 数量计算 malloc 字节数和 header,并发出有序 payload stores。
安全读取依赖正确的检查顺序
record?:
|
|
size/get:
|
|
任何时候都不能先把普通整数当地址解引用,也不能先越界 load 再检查。
可以运行的检查
构建并运行核心示例:
|
|
观察前端和 IR:
|
|
生成并手动链接:
|
|
Apple Silicon Mac 的链接命令使用 cc -arch x86_64。record_get.lang 的最终整数是 2,退出状态应为 2。
完成本章后的自检
读者应当能够解释:
- 为什么空 record 仍要分配 header;
- 为什么
std::vector没有成为源语言特性; - 为什么字段求值必须先于 record op;
- 为什么 record Value 保存引用而不是内联全部 payload;
- 为什么低位
4只能说明 heap object; record?为什么还要检查 header kind;size为什么要把裸 count 重新编码成 tagged integer;get为什么必须在 load 前检查index < count;- 为什么两个内容相同的 record 可能不相等;
- 为什么嵌套 record 是对象图,而不是一块扁平结构。
为第七章留下的接口
record 已经建立通用的 heap object 协议:
|
|
第七章实现 closure 时可以复用这套协议,但 closure 仍是不同 kind:它的 payload 包含代码入口和捕获值,调用语义也不同。下一章真正的新困难应集中在自由变量、捕获环境和调用,而不必重新发明堆对象的基本布局。
第六章至此完成了从“值都能放进一个机器字”到“一个机器字可以安全引用整张对象图”的跨越。