边界:这套 record 提供什么,不提供什么
本节阅读量:第六章建立的是一套小而完整的堆上聚合数据机制。进入总结前,把语言能力、动态错误与实现限制集中列清楚,避免把“已经有通用布局”误解为“已经有完整对象系统”。
record 有零个或多个匿名字段
合法创建形式包括:
|
|
字段数量没有固定为二。字段没有名字,只能通过非负位置读取:
|
|
get 的 index 是 parser-time 字面量,不是运行时表达式。
index 语法与边界是两件事
parser 接受只含十进制数字的 index token:
| 源码 | 语法结果 |
|---|---|
(get r 0) |
接受,index 为 0 |
(get r 2) |
接受,index 为 2 |
(get r 01) |
接受,index 为 1 |
(get r -0) |
拒绝 |
(get r -1) |
拒绝 |
(get r index) |
拒绝 |
(get r (+ 1 1)) |
拒绝 |
parser 接受 index 2,不代表所有 record 都有 field 2。边界由对象的实际 header count 决定:
|
|
第一段成功得到 30;第二段越界。解释器报告错误;编译后的第二个程序在读内存前跳到错误出口,以状态 70 结束。
空 record 是普通的独立对象
空 record 不是整数 0,不是函数,也不是全局 singleton:
|
|
结果依次是 1、0、42、0。最后一行创建两个对象,所以身份不同。若想共享同一个空对象,应绑定并复用引用:
|
|
结果是 1。
本章没有把空 record 定义成 Lisp 的 nil,也没有加入 empty?。需要判断字段数为零时,使用:
|
|
record 不可变
创建后,字段数量和字段 Value 都不能修改。本章没有:
|
|
不可变不等于没有对象身份。两个内容相同的 record 仍可能是两个对象;两个变量也可能共享同一个对象。
第八章加入 set! 时,改变的是变量或捕获 cell,不会倒过来把第六章 record 改成可变容器。
record?、size 与 get 的失败规则不同
record? 是总操作,对本章任意 Value 都返回整数结果:
|
|
结果依次是 0、0、1。
size 和 get 则要求对象种类正确:
|
|
两段程序都会发生类型错误。
get 还可能在种类正确后发生边界错误:
|
|
解释器分别给出说明性诊断:
|
|
生成代码把这三类 size/get 失败收敛到同一个 exit(70) 路径,不打印诊断文字。状态 70 是本章后端约定,不是新的源语言异常值。
heap-object tag 不等于 record kind
机器值低位 4 表示“引用堆对象”,不是“必然引用 record”。具体 kind 在对象 header 低 8 位中:
|
|
因此:
record?先检查 heap tag,再检查 header kind;size/get两层都检查,失败则安全停止;- 后续 closure、cell 能共享 heap tag 与 header 公式,同时拥有不同 kind。
当前源码没有任何操作能伪造其他 heap kind,但从第一版就保持这层边界,能避免第七章再次推翻表示。
字段可以保存任意现有 Value
record 不是整数数组。每个字段保存完整 tagged Value,所以可以混合:
|
|
嵌套 record 形成对象图,函数字段可以被读取后调用。record 本身也能作为函数实参或返回值。
本章函数仍沿用第五章边界:普通 lambda 不能捕获任意外层变量;真正的 closure 在下一章实现。record 可以装下一个现有函数值,却不会自动赋予它捕获环境。
eq? 是身份比较,不是结构比较
对 record:
|
|
结果依次是 1、0。
解释器比较 shared_ptr 是否指向同一个 RecordValue,生成代码比较完整 tagged pointer。两条路线都不递归比较字段。
函数依旧不能参加 eq?。record 的某个字段是函数,不影响 record 自己按身份比较;只有直接把函数值作为 eq? operand 时才报错。
生成代码只补齐 record 相关检查
第六章有意为 record?、size 与 get 实现 heap tag、header kind 和边界逻辑,因为这些检查正是本章对象表示的一部分。
这不表示所有旧操作都已有完整动态检查。编译路径仍要求实际执行到的算术收到整数、函数调用收到函数,函数不参加 eq?。错误程序应优先用解释器观察具体诊断。
malloc 分配,但还没有回收
含 n 个字段的 record 调用:
|
|
连空 record 也调用 malloc(8)。生成代码不调用 free,也没有垃圾回收器;对象一直存活到进程结束。
解释器使用 std::shared_ptr<RecordValue>,没有引用的 C++ 对象会自动释放。这是两条实现路线的内存管理差异,不是源语言可观察语义的差异。
第九章会用受管理堆与 GC 替换编译后端的简单 malloc 策略。本章 header 中已有 kind 和 count,正是为了让后续运行时知道对象多大、payload 在哪里。
源语言没有 vector、struct 或 class
正文代码会出现三种 C++ vector:
|
|
它们都是宿主实现容器。源语言没有 vector 索引表达式、具名 struct 字段、class、方法或继承。
record 已经足够承担本章教学目标:可变大小分配、payload、嵌套对象图、身份、类型判断和边界检查。额外的数据抽象可以建立在这些基础之上,不需要在这一章增加新的表面语法家族。
CLI 与链接职责不变
mini compile 只输出汇编:
|
|
由用户显式调用 cc 链接 malloc 和 exit:
|
|
本章没有 build/link 编排,也没有通用 FFI。后端引用少量 C 库符号不改变 compile 命令的职责边界。