解释器:用共享对象保存一组 Value
本节阅读量:
AST 中的 RecordExpr 保存字段表达式;解释器求值后需要保存的是这些表达式产生的 Value。两层不要混淆:
1
2
3
4
5
6
7
|
AST RecordExpr
vector<unique_ptr<Expr>>
生命周期属于语法树
运行时 RecordValue
vector<Value>
生命周期属于被求值程序
|
先构建并运行:
1
2
3
4
|
cd code/06_records
make
./mini run examples/record.lang
./mini run examples/record_get.lang
|
应依次看到:
Value 新增 record 种类
解释器沿用显式 kind:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
struct Value {
enum class Kind {
number,
function,
record,
};
static Value make_number(long value);
static Value make_function(std::shared_ptr<Function> value);
static Value make_record(std::shared_ptr<RecordValue> value);
Kind kind;
long number_value = 0;
std::shared_ptr<Function> function_value;
std::shared_ptr<RecordValue> record_value;
};
|
同一个 eval_expr 现在可能返回整数、函数或 record。消费者先查看 kind,再读取对应成员;不会把 record_value 是否为空当作隐含的种类标记。
RecordValue 保存可变数量的完整 Value
运行时对象定义为:
1
2
3
4
5
6
|
struct RecordValue {
explicit RecordValue(std::vector<Value> fields)
: fields(std::move(fields)) {}
std::vector<Value> fields;
};
|
这里的 std::vector<Value> 能保存零个或多个字段:
1
2
3
|
(record) -> fields.size() == 0
(record 40) -> fields.size() == 1
(record 40 2) -> fields.size() == 2
|
vector 是解释器内部的 C++ 容器,不是源语言 vector。源程序只能通过 record?、size 和 get 观察这份数据,不能调用 push_back,也不能修改字段。
每个元素是完整 Value。若字段是另一个 record,vector 中保存 shared_ptr 所在的 Value;若字段是函数,保存函数 Value。它没有把所有字段静态限制为同一种类型。
为什么使用 shared_ptr
record 是具有身份的共享对象。下面的 p 和 q 应指向同一个对象:
1
2
3
|
(let p (record 40 2)
(let q p
(eq? p q)))
|
复制 Value 时,std::shared_ptr<RecordValue> 也被复制,但 RecordValue 本身不被复制。因此两条引用共享同一身份,并得到结果 1。
两个分别求值的创建表达式则得到两个 shared_ptr 目标:
1
|
(eq? (record 1 2) (record 1 2))
|
结果是 0。空 record 也遵循同一规则;不存在全局共享的空对象。
从左到右求值字段
ExprKind::record 分支把字段依次求值:
1
2
3
4
5
6
7
8
9
10
|
case ExprKind::record: {
const auto& record_expr = static_cast<const RecordExpr&>(expr);
std::vector<Value> fields;
fields.reserve(record_expr.fields.size());
for (const auto& field : record_expr.fields) {
fields.push_back(eval_expr(*field, env));
}
return Value::make_record(
std::make_shared<RecordValue>(std::move(fields)));
}
|
几个细节值得停下来确认:
- 循环顺序就是源码字段顺序。
eval_expr 立即执行当前字段,不是把 AST 指针塞进对象。
reserve 只减少 C++ vector 扩容,不改变字段数量;真正加入字段的是 push_back。
- 所有字段成功后才创建
RecordValue。
- 空 vector 同样会创建新的
RecordValue,所以 (record) 是独立对象。
例如:
1
|
(record (+ 20 20) (sub1 3) (record))
|
解释器依次得到整数 40、整数 2、空 record 引用,最后才组成外层对象。
record? 不会报类型错误
predicate 分支只求值一次,再比较 kind:
1
2
3
4
5
6
7
|
case ExprKind::record_predicate: {
const auto& predicate_expr =
static_cast<const RecordPredicateExpr&>(expr);
Value value = eval_expr(*predicate_expr.expr, env);
return Value::make_number(
value.kind == Value::Kind::record ? 1 : 0);
}
|
返回值是普通的源语言整数,不是 C++ bool:
1
|
(+ (record? (record)) 41)
|
结果是 42。
对数字或函数求 record? 也会正常返回 0。
size 先检查 kind
size 只接受 record:
1
2
3
4
5
6
7
8
9
|
case ExprKind::record_size: {
const auto& size_expr = static_cast<const RecordSizeExpr&>(expr);
Value record = eval_expr(*size_expr.record, env);
if (record.kind != Value::Kind::record) {
throw std::runtime_error("size expected a record value");
}
return Value::make_number(
static_cast<long>(record.record_value->fields.size()));
}
|
检查必须发生在访问 record_value 之前。否则 (size 42) 会错误地解引用一个并不存在的 record 指针。
可以验证空对象与多字段对象:
1
2
|
(size (record))
(size (record 1 2 3 4))
|
结果依次是 0、4。
get 再检查实际边界
字段读取需要两次检查:
1
2
3
4
5
6
7
8
9
10
11
|
case ExprKind::field: {
const auto& get_expr = static_cast<const GetExpr&>(expr);
Value record = eval_expr(*get_expr.record, env);
if (record.kind != Value::Kind::record) {
throw std::runtime_error("get expected a record value");
}
if (get_expr.index >= record.record_value->fields.size()) {
throw std::runtime_error("record field index out of bounds");
}
return record.record_value->fields[get_expr.index];
}
|
顺序不能交换:只有确认 record.kind 后,读取 record_value->fields.size() 才安全;只有确认 index 在范围内,使用 fields[index] 才安全。
注意这里使用 >=。对于空 record,字段数为零,任何非负 index 都满足 index >= 0,因此全部越界。对于三字段 record,合法 index 是 0、1、2,index 3 已经越界。
get 返回 vector 中保存的完整 Value。字段若是 record 或函数,调用者得到的仍是对应种类的值:
1
|
((get (record (lambda x (+ x 1))) 0) 41)
|
结果是 42。
错误发生在对象表达式求值之后
下面的 index 已经通过 parser 检查:
解释器仍会先创建一字段 record,然后发现 index 越界。这符合语言规定的求值顺序。类似地,(size bad) 要先查找或求值 bad,随后才能检查结果 kind。
CLI 会捕获 std::runtime_error 并带上 error: 前缀。例如:
1
2
3
|
error: size expected a record value
error: get expected a record value
error: record field index out of bounds
|
身份相等只比较共享目标
eq? 先拒绝函数,再按 kind 分派:
1
2
3
4
5
6
7
8
9
|
case Value::Kind::number:
return Value::make_number(
lhs.number_value == rhs.number_value ? 1 : 0);
case Value::Kind::record:
return Value::make_number(
lhs.record_value == rhs.record_value ? 1 : 0);
case Value::Kind::function:
throw std::runtime_error(
"eq? does not support function values");
|
比较 shared_ptr 检查的是它们是否指向同一个 C++ 对象,不会递归比较 vector 内容。字段数量不同或相同都不影响这条规则。
整数与 record 的 kind 不同,eq? 返回 0;函数与任何值比较仍报错。这与前文既定语言边界一致。
truthiness 显式覆盖新 kind
加入 Value kind 时,所有 switch 消费者都要审查。is_truthy 的 record 分支是:
1
2
3
4
5
6
7
8
9
10
|
bool is_truthy(const Value& value) {
switch (value.kind) {
case Value::Kind::number:
return value.number_value != 0;
case Value::Kind::function:
case Value::Kind::record:
return true;
}
throw std::runtime_error("unknown value kind");
}
|
所有 record 都为真,包括 (record)。空字段数不是布尔假值。
递归显示所有字段
format_value 用循环生成与源码相近的文本:
1
2
3
4
5
6
7
|
case Value::Kind::record: {
std::string result = "(record";
for (const auto& field : value.record_value->fields) {
result += " " + format_value(field);
}
return result + ")";
}
|
初始字符串没有尾随空格,所以空 vector 自然得到 (record);非空对象则在每个字段前追加一个空格。
嵌套 record 会递归显示:
1
|
(record (record) (record 40 2))
|
显示内容相同不代表身份相同。format_value 服务于观察,eq? 服务于语言语义。
解释器检查点
现在解释器已经做到:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
RecordExpr
-> 左到右求值得到 vector<Value>
-> 创建独立 shared RecordValue
RecordPredicateExpr
-> 检查 Value::Kind
-> 返回 number 1/0
RecordSizeExpr
-> 检查 record
-> 返回 fields.size()
GetExpr
-> 检查 record
-> 检查实际边界
-> 返回一个完整字段 Value
|
下一节把相同动作降低成结构化 IR。IR 不保存 shared_ptr 或 C++ vector 对象本身,只保留后端需要执行的 record 操作和有序 operand 列表。
6.3 AST 与 parser:拥有数量不定的字段子树
上一节