第六章:record 与堆上的数据
本节阅读量:前五章的程序已经能计算整数、做条件选择,也能创建和调用函数。但一个值仍然很难携带“一组数据”:想让函数同时返回三个结果,或者把若干相关值作为一个整体传递,目前都做不到。
这一章加入不可变 record:
|
|
record 可以包含任意数量的字段,包括零个字段。字段本身仍是完整的语言值,因此可以继续放整数、函数或另一个 record:
|
|
我们同时加入三种观察 record 的操作:
|
|
它们分别回答:一个值是不是 record、record 有多少个字段,以及指定位置保存了什么。
先运行几个程序
进入本章代码目录并构建:
|
|
创建并显示一个 record:
|
|
输出是:
|
|
读取字段:
|
|
输出是 2。再试嵌套对象:
|
|
输出是 40。
这里出现了本书此前没有处理过的运行时问题:record 的字段数量由程序决定,整个对象不一定能放进一个 64 位机器字。解释器可以使用 C++ 对象表示它;编译器生成的程序则需要在堆上为它申请空间。
本章建立的语言接口
本章新增以下表达式:
|
|
例如:
|
|
它们依次得到 1、0、0、3 和 30。
get 的 index 是源码中的非负整数字面量,不是运行时表达式。(get r 2) 是合法语法;执行时只有当 r 确实是 record,并且至少有三个字段,读取才会成功。
这使接口保持了清楚的分工:
- parser 确认 index 是非负整数字面量;
- 解释器和生成代码确认对象种类;
- 解释器和生成代码根据对象的实际长度检查边界。
从一个机器字走向堆对象
第五章可以把整数或函数引用放进一个 64 位机器字。本章把所有值统一成 tagged value:
|
|
record 值仍然只占一个机器字,但这个机器字保存的是带 tag 的堆地址。真正的数据位于堆上:
|
|
header 同时记录对象种类与 payload 数量:
|
|
对于 record,payload_count 就是字段数。于是:
|
|
总分配大小也不再固定:
|
|
空 record 仍有一个 header,因此需要 8 字节;二字段 record 需要 24 字节;三字段 record 需要 32 字节。
这套布局是本章真正要建立的基础。第七章会在同一套“header + payload”协议上实现 closure,但不会把 closure 假装成源语言 record。
AST、IR 与汇编仍然逐层负责
同一个程序会依次经过:
|
|
以字段读取为例:
|
|
AST 保留 record 的字段子树和字面量 index;IR 明确表达“创建对象”和“读取字段”;汇编阶段才决定 tag、header、malloc、偏移与失败出口。compile 不会绕过 IR 直接递归 AST。
可以直接观察前两层:
|
|
编译、链接和观察结果
生成汇编:
|
|
compile 只输出 x86-64 System V ABI 的 AT&T 汇编,不调用 cc,也不负责链接。Linux、WSL 和 Intel Mac 可以继续手动执行:
|
|
Apple Silicon Mac 需要明确链接 x86-64 目标:
|
|
record_get.lang 最终得到整数 2,因此退出状态是 2。顶层若直接返回 record,main 仍返回 0;要观察整个 record 的文本形式,应使用 mini run。
本章为编译后的 size 与 get 加入 heap-object tag 和 record header-kind 检查,并为 get 加入边界检查。检查失败时,生成程序以状态 70 结束。它不打印解释器式错误文字;这一小步的重点是让错误程序安全停止,而不是建立完整异常系统。
本章路线
我们按以下顺序推进:
- 先定清可变长度、空 record、求值顺序、身份相等和错误语义。
- 设计统一 tagged value、通用 header 和可变大小布局。
- 用
std::vector<std::unique_ptr<Expr>>表示数量不定的 AST 子树。 - 让解释器创建共享的
RecordValue,实现 predicate、长度与安全读取。 - 在结构化 IR 中加入 record 相关 op,保持字段从左到右 lowering。
- 生成
malloc、header、payload 循环、tag 检查和边界检查。 - 最后明确本章的错误、内存管理和后续章节边界。
范围说明
源语言新增的是 record,不是 vector、struct、class 或对象系统。正文出现的 C++ std::vector 只是编译器和解释器保存“不定数量元素”的实现容器;读者写的程序里没有 vector API。
record 在本章中不可变:创建后只能观察,不能更新。生成代码使用 malloc,暂时不回收堆对象;垃圾回收留到后面的专门章节。