章节目录

解释器:用共享对象保存一组 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

应依次看到:

1
2
(record 40 2)
2

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)));
}

几个细节值得停下来确认:

  1. 循环顺序就是源码字段顺序。
  2. eval_expr 立即执行当前字段,不是把 AST 指针塞进对象。
  3. reserve 只减少 C++ vector 扩容,不改变字段数量;真正加入字段的是 push_back。
  4. 所有字段成功后才创建 RecordValue。
  5. 空 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 检查:

1
(get (record 10) 3)

解释器仍会先创建一字段 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:拥有数量不定的字段子树

上一节

6.5 结构化 IR:保留一组有序字段

下一节