章节目录

边界:这套 record 提供什么,不提供什么

本节阅读量:

第六章建立的是一套小而完整的堆上聚合数据机制。进入总结前,把语言能力、动态错误与实现限制集中列清楚,避免把“已经有通用布局”误解为“已经有完整对象系统”。

record 有零个或多个匿名字段

合法创建形式包括:

1
2
3
(record)
(record 10)
(record 10 20 30)

字段数量没有固定为二。字段没有名字,只能通过非负位置读取:

1
2
(get value 0)
(get value 2)

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 决定:

1
2
(get (record 10 20 30) 2)
(get (record 10 20) 2)

第一段成功得到 30;第二段越界。解释器报告错误;编译后的第二个程序在读内存前跳到错误出口,以状态 70 结束。

空 record 是普通的独立对象

空 record 不是整数 0,不是函数,也不是全局 singleton:

1
2
3
4
(record? (record))
(size (record))
(if (record) 42 0)
(eq? (record) (record))

结果依次是 1、0、42、0。最后一行创建两个对象,所以身份不同。若想共享同一个空对象,应绑定并复用引用:

1
2
(let empty (record)
  (eq? empty empty))

结果是 1。

本章没有把空 record 定义成 Lisp 的 nil,也没有加入 empty?。需要判断字段数为零时,使用:

1
(eq? (size r) 0)

record 不可变

创建后,字段数量和字段 Value 都不能修改。本章没有:

1
2
3
4
set-field!
push
pop
append

不可变不等于没有对象身份。两个内容相同的 record 仍可能是两个对象;两个变量也可能共享同一个对象。

第八章加入 set! 时,改变的是变量或捕获 cell,不会倒过来把第六章 record 改成可变容器。

record?、size 与 get 的失败规则不同

record? 是总操作,对本章任意 Value 都返回整数结果:

1
2
3
(record? 42)
(record? (lambda x x))
(record? (record 42))

结果依次是 0、0、1。

size 和 get 则要求对象种类正确:

1
2
(size 42)
(get 42 0)

两段程序都会发生类型错误。

get 还可能在种类正确后发生边界错误:

1
(get (record) 0)

解释器分别给出说明性诊断:

1
2
3
error: size expected a record value
error: get expected a record value
error: record field index out of bounds

生成代码把这三类 size/get 失败收敛到同一个 exit(70) 路径,不打印诊断文字。状态 70 是本章后端约定,不是新的源语言异常值。

heap-object tag 不等于 record kind

机器值低位 4 表示“引用堆对象”,不是“必然引用 record”。具体 kind 在对象 header 低 8 位中:

1
2
3
Value tag       heap object
header kind     record
header count    field count

因此:

  • record? 先检查 heap tag,再检查 header kind;
  • size/get 两层都检查,失败则安全停止;
  • 后续 closure、cell 能共享 heap tag 与 header 公式,同时拥有不同 kind。

当前源码没有任何操作能伪造其他 heap kind,但从第一版就保持这层边界,能避免第七章再次推翻表示。

字段可以保存任意现有 Value

record 不是整数数组。每个字段保存完整 tagged Value,所以可以混合:

1
2
3
4
(record
    42
    (record)
    (lambda x (+ x 1)))

嵌套 record 形成对象图,函数字段可以被读取后调用。record 本身也能作为函数实参或返回值。

本章函数仍沿用第五章边界:普通 lambda 不能捕获任意外层变量;真正的 closure 在下一章实现。record 可以装下一个现有函数值,却不会自动赋予它捕获环境。

eq? 是身份比较,不是结构比较

对 record:

1
2
3
4
5
(let p (record 1 2 3)
  (eq? p p))

(eq? (record 1 2 3)
     (record 1 2 3))

结果依次是 1、0。

解释器比较 shared_ptr 是否指向同一个 RecordValue,生成代码比较完整 tagged pointer。两条路线都不递归比较字段。

函数依旧不能参加 eq?。record 的某个字段是函数,不影响 record 自己按身份比较;只有直接把函数值作为 eq? operand 时才报错。

生成代码只补齐 record 相关检查

第六章有意为 record?、size 与 get 实现 heap tag、header kind 和边界逻辑,因为这些检查正是本章对象表示的一部分。

这不表示所有旧操作都已有完整动态检查。编译路径仍要求实际执行到的算术收到整数、函数调用收到函数,函数不参加 eq?。错误程序应优先用解释器观察具体诊断。

malloc 分配,但还没有回收

含 n 个字段的 record 调用:

1
malloc(8 * (1 + n))

连空 record 也调用 malloc(8)。生成代码不调用 free,也没有垃圾回收器;对象一直存活到进程结束。

解释器使用 std::shared_ptr<RecordValue>,没有引用的 C++ 对象会自动释放。这是两条实现路线的内存管理差异,不是源语言可观察语义的差异。

第九章会用受管理堆与 GC 替换编译后端的简单 malloc 策略。本章 header 中已有 kind 和 count,正是为了让后续运行时知道对象多大、payload 在哪里。

源语言没有 vector、struct 或 class

正文代码会出现三种 C++ vector:

1
2
3
vector<unique_ptr<Expr>>   AST 字段子树
vector<Value>              解释器字段值
vector<Operand>            IR 字段 operand

它们都是宿主实现容器。源语言没有 vector 索引表达式、具名 struct 字段、class、方法或继承。

record 已经足够承担本章教学目标:可变大小分配、payload、嵌套对象图、身份、类型判断和边界检查。额外的数据抽象可以建立在这些基础之上,不需要在这一章增加新的表面语法家族。

CLI 与链接职责不变

mini compile 只输出汇编:

1
./mini compile examples/record_get.lang -o out.s

由用户显式调用 cc 链接 malloc 和 exit:

1
cc out.s -o out

本章没有 build/link 编排,也没有通用 FFI。后端引用少量 C 库符号不改变 compile 命令的职责边界。


6.6 汇编:分配可变大小对象,并在读取前检查

上一节

6.8 小结:从一个 Value 到一张对象图

下一节