练习:把对象布局真正画出来
本节阅读量:这些练习按语言语义、AST、解释器、IR、机器布局和安全检查逐步推进。建议先在纸上写出答案,再保存为 .lang 文件运行;只看最终退出状态,很容易跳过本章真正重要的对象模型。
可以统一在本章目录操作:
|
|
练习 1:预测可变长度 record
先不要运行,写出以下表达式的解释器输出:
|
|
|
|
|
|
|
|
|
|
|
|
预期依次是:
|
|
检查自己有没有把空 record 当成整数 0,或者把 size 得到的字段数从 1 开始计算。
练习 2:区分合法 index 与合法访问
判断哪些程序会在 parser 阶段失败,哪些会形成 AST 后在运行时报错:
|
|
答案要点:
- index
2是合法非负字面量,但对(record 10 20)越界; 01合法并转换为 index1,结果是20;-0、-1、变量和表达式 index 都被 parser 拒绝。
解释为什么 parser 不能仅根据 (get ... 2) 判定越界:对象表达式可能是变量、函数调用或条件,它的实际长度要到运行时才知道。
练习 3:观察四种 AST
分别保存并运行 mini ast:
|
|
|
|
|
|
|
|
|
|
预期核心 dump 是:
|
|
回答:
- 为什么
RecordExpr需要vector<unique_ptr<Expr>>? - 为什么
GetExpr::index是std::size_t,而不是unique_ptr<Expr>? - AST 中的空 vector 与运行时空 record 有什么区别?
练习 4:确认左到右求值与“全部成功后分配”
观察:
|
|
解释器应先报告 record index 越界,而不是 undefined variable: unknown。这说明第二个字段没有在第一个字段之前求值。
再阅读 ExprKind::record 的解释器分支,指出:
- 哪一行决定字段求值顺序;
- 哪一行才真正创建
RecordValue; - 为什么某个字段失败后不会留下可观察的半成品 record。
练习 5:空对象也有身份
预测:
|
|
|
|
|
|
预期依次为 0、1、1。
画出第三段程序中的对象图。应有一个空 record 对象和一个含两个字段的外层对象;外层两个 payload 都保存同一个 tagged heap pointer,而不是复制两个空对象。
练习 6:手算 header、大小和偏移
填写表格:
| 表达式 | payload count | header | 分配字节数 |
|---|---|---|---|
(record) |
? | ? | ? |
(record 10) |
? | ? | ? |
(record 10 20 30) |
? | ? | ? |
(record 1 2 3 4 5) |
? | ? | ? |
使用:
|
|
答案是:
| 表达式 | payload count | header | 分配字节数 |
|---|---|---|---|
(record) |
0 | 1 | 8 |
(record 10) |
1 | 257 | 16 |
(record 10 20 30) |
3 | 769 | 32 |
(record 1 2 3 4 5) |
5 | 1281 | 48 |
再写出五字段对象的 field 0、field 2、field 4 偏移。使用:
|
|
结果应是 8、24、40。
练习 7:不要混淆三个 vector
把下面三种 C++ 类型分别填到正确阶段,并说明元素代表什么:
|
|
然后回答:源程序能否写 (vector 1 2 3)?能否为 record 声明字段名?能否在 record 创建后 push_back?
三个答案都是否定的。C++ vector 只解决“实现中如何保存数量不定的有序元素”,没有扩大源语言表面能力。
练习 8:手写嵌套 record 的 IR
对下面程序先手写 IR:
|
|
按照当前临时值编号,应得到:
|
|
最终结果是 33。注意内层空 record op 必须先于外层 record op;即使它没有字段,也不能省略分配。
把程序保存后运行:
|
|
练习 9:追踪三字段 record 汇编
编译:
|
|
在汇编中找出:
|
|
解释三个字段为什么分别保存 81、161、241,而 header 中保存的 count 却是裸 3。
最后指出字段地址如何由:
|
|
表示 raw_address + 8 + index * 8。
练习 10:为什么 record? 检查两层
把 record? 的汇编控制流画成:
|
|
回答两个问题:
- 为什么收到整数时不能直接清 tag 后读取 header?
- 当前只有 record 一种 heap object,为什么仍要检查 header kind?
第二问的关键是:低位 4 被定义为通用 heap-object tag。下一章加入 closure 后,closure 也可能通过第一层检查,但不应让 (record? closure) 得到 1。
练习 11:验证编译后的失败状态
分别准备三个程序:
|
|
|
|
|
|
对每个程序生成、链接并运行:
|
|
Apple Silicon Mac 把链接命令改为:
|
|
三者退出状态都应为 70。再分别使用 mini run,确认解释器能区分 size 类型错误、get 类型错误和 get 越界。
解释为什么公共错误标签调用 exit(70),而不是简单令 %rax = 70 后 retq:错误可能发生在生成函数内部,普通返回会继续执行调用者。
练习 12:用 record 编码一条小链
本章没有专门的 list、cons、nil、car 或 cdr,但可变长度 record 已足以编码一个简单链:
|
|
补全并运行:
|
|
结果应为 42。
这道题展示的是 record 的组合能力,不是要求把源语言改造成完整 Lisp 列表系统。请画出四个独立堆对象,并标出每个非空节点的 field 1 如何指向下一个对象。
完成标准
完成练习后,你应能不看代码回答:
(record)为什么既有 size0又必须分配 8 字节;- 为什么
(get r 2)可能成功,也可能越界; - 为什么
01合法而-0不合法; - 三个 C++ vector 分别保存什么;
- header 中 kind 与 count 怎样拆分;
- heap-object tag 与 record kind 为什么不是一回事;
size如何把裸 count 变成 tagged integer;get的检查为什么必须先于 load;- 两个内容相同的 record 为什么可能不相等;
- 可变长度布局怎样为下一章 closure 减少重复工作。