章节目录

解释器:函数值和一次新的求值环境

本节阅读量:

AST 已经能表示 lambda 和调用,但它们还没有运行时含义。解释器需要解决三个问题:

1
2
3
表达式现在可能返回整数,也可能返回函数,返回类型怎样表示?
lambda 暂不执行 body,函数值里要保存什么?
调用发生时,参数怎样进入 body,又怎样隔离调用位置的局部变量?

代码位于:

1
2
code/04_functions/src/interp/interpreter.h
code/04_functions/src/interp/interpreter.cpp

第一遍建议先读 Value、lambda 和 call 三部分,抓住运行时语义;shared_ptr、裸指针和完整 eval_expr 可以在第二遍作为 C++ 实现细节检查。

Value 从整数扩展为两种可能

前三章的 eval 可以直接返回 long。现在同一个表达式位置可能得到数字或函数,所以头文件把结果统一表示为 Value:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#pragma once

#include "front/ast.h"

#include <memory>
#include <string>

namespace mini {

struct Function;

struct Value {
    enum class Kind {
        number,
        function,
    };

    static Value make_number(long value);
    static Value make_function(std::shared_ptr<Function> value);

    Kind kind;
    long number_value = 0;
    std::shared_ptr<Function> function_value;
};

Value eval(const Expr& expr);
std::string format_value(const Value& value);

} // namespace mini

kind 说明当前真正保存的是哪一种值。只有 kind == number 时才读取 number_value,只有 kind == function 时才使用 function_value。

这里继续用显式 kind + switch 穷举值的种类;需要检查某一种操作的前置条件时,再用 if。没有借助 C++ 的继承层次隐藏变体,这和 AST、后面的 IR 保持同一种阅读方式。

Function 保存参数名和 body

本章没有闭包,所以函数对象只有两个字段:

1
2
3
4
struct Function {
    std::string param;
    const Expr* body; // Non-owning; the parsed AST outlives eval().
};

这里的 Function 是解释器用来表示源语言函数值的 C++ 数据结构,不是一个会被 C++ 直接调用的函数。

注意,它没有保存创建 lambda 时的 Env。这正是函数暂时不能读取外层变量的实现原因。

环境现在保存 Value

变量不再只绑定整数:

1
using Env = std::vector<std::pair<std::string, Value>>;

lookup() 仍然从后向前寻找最近的同名绑定,但返回的是 Value 副本:

1
2
3
4
5
6
7
8
Value lookup(const Env& env, const std::string& name) {
    for (auto it = env.rbegin(); it != env.rend(); ++it) {
        if (it->first == name) {
            return it->second;
        }
    }
    throw std::runtime_error("undefined variable: " + name);
}

因此 let 的实现不需要分成“整数 let”和“函数 let”。右边算出哪种 Value,环境就保存哪种 Value。

数字检查和真值判断集中到辅助函数

加法和 eq? 只接受数字。与其在每个分支里重复检查,解释器先定义:

1
2
3
4
5
6
long expect_number(const Value& value, const char* context) {
    if (value.kind != Value::Kind::number) {
        throw std::runtime_error(std::string(context) + " expected a number");
    }
    return value.number_value;
}

例如函数出现在加法左边时,错误会是:

1
addition left operand expected a number

if 接受两种 Value,规则集中在 is_truthy():

1
2
3
4
5
6
7
8
9
bool is_truthy(const Value& value) {
    switch (value.kind) {
    case Value::Kind::number:
        return value.number_value != 0;
    case Value::Kind::function:
        return true;
    }
    throw std::runtime_error("unknown value kind");
}

所以 0 为假,非零整数和函数都为真。

lambda 分支只创建值

1
2
3
4
5
6
7
case ExprKind::lambda: {
    const auto& lambda_expr = static_cast<const LambdaExpr&>(expr);
    auto function = std::make_shared<Function>();
    function->param = lambda_expr.param;
    function->body = lambda_expr.body.get();
    return Value::make_function(function);
}

这里做了三件事:复制参数名、借用 body 指针、包装成函数 Value。没有调用 eval_expr(*lambda_expr.body, env),也没有把当前 env 放进 Function。

call 分支建立全新环境

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
case ExprKind::call: {
    const auto& call_expr = static_cast<const CallExpr&>(expr);
    Value callee = eval_expr(*call_expr.callee, env);
    if (callee.kind != Value::Kind::function) {
        throw std::runtime_error("function call expected a function");
    }

    Value argument = eval_expr(*call_expr.argument, env);
    Env call_env; // No outer bindings until the closure chapter.
    call_env.push_back({callee.function_value->param, argument});
    return eval_expr(*callee.function_value->body, call_env);
}

前两次 eval_expr 使用调用位置的 env,因为 callee 和 argument 都写在调用者的表达式里。真正进入 body 时则改用 call_env:

1
2
调用者 env       用来求 callee 和 argument
call_env         起初只含 parameter -> argument,用来求 body

这也解释了函数作为参数为什么自然成立。若 argument 本身是函数,call_env 保存的只是一个 Kind::function 的 Value,函数体随后可以查出并调用它。

不要把 call_env 换成调用者 env 的副本。那会让函数错误地读取调用位置碰巧存在的局部变量,变成动态作用域;同时仍然没有实现“记住 lambda 创建位置”的闭包。

最后一行递归调用 C++ 的 eval_expr 来等待函数体求值。源语言没有 return 节点;函数体产生的 Value 会直接成为整个 CallExpr 的结果。

第二遍再看:shared_ptr 和裸指针分别拥有谁

理解完调用语义后,再看两个指针的生命周期:

1
2
std::shared_ptr<Function>   共享拥有 Function 小对象
const Expr* body            只借用 AST 中已经存在的函数体

shared_ptr 只解决宿主 C++ 对象的寿命,不会自动保存源语言环境,因此它不意味着已经实现了闭包。

环境查找和 let 都会复制 Value。如果一个 Value 保存函数,复制其中的 shared_ptr 会让多个 Value 安全地指向同一个 Function;最后一个副本销毁时,Function 才被释放。

body 指向的 AST 仍由解析结果的根 unique_ptr 独占:

1
2
auto expr = mini::parse(source);
std::cout << mini::format_value(mini::eval(*expr)) << "\n";

当前 CLI 会让 expr 活过整个 eval 和 format_value,所以函数值借用的 body 一直有效。解释器不能释放这个指针;如果以后允许 Value 活得比 AST 更久,就要重新设计这层所有权。

完整的 eval_expr

有了 Value 后,旧分支也要相应修改。下面是本章完整的求值分派,和 code/04_functions 中的实现一致。第一遍可以只对照 lambda 和 call 两支,或者先跳到后面的“完整推演贯穿示例”;末尾的 input、output 只服务于章末可选实验,也可以暂时跳过:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
Value eval_expr(const Expr& expr, Env& env) {
    switch (expr.kind) {
    case ExprKind::integer: {
        const auto& int_expr = static_cast<const IntExpr&>(expr);
        return Value::make_number(int_expr.value);
    }
    case ExprKind::add: {
        const auto& add_expr = static_cast<const AddExpr&>(expr);
        long lhs = expect_number(eval_expr(*add_expr.lhs, env), "addition left operand");
        long rhs = expect_number(eval_expr(*add_expr.rhs, env), "addition right operand");
        return Value::make_number(lhs + rhs);
    }
    case ExprKind::variable: {
        const auto& var_expr = static_cast<const VarExpr&>(expr);
        return lookup(env, var_expr.name);
    }
    case ExprKind::let: {
        const auto& let_expr = static_cast<const LetExpr&>(expr);
        Value value = eval_expr(*let_expr.value, env);
        env.push_back({let_expr.name, value});
        Value result = eval_expr(*let_expr.body, env);
        env.pop_back();
        return result;
    }
    case ExprKind::equal: {
        const auto& eq_expr = static_cast<const EqExpr&>(expr);
        long lhs = expect_number(eval_expr(*eq_expr.lhs, env), "eq? left operand");
        long rhs = expect_number(eval_expr(*eq_expr.rhs, env), "eq? right operand");
        return Value::make_number(lhs == rhs ? 1 : 0);
    }
    case ExprKind::conditional: {
        const auto& if_expr = static_cast<const IfExpr&>(expr);
        if (is_truthy(eval_expr(*if_expr.condition, env))) {
            return eval_expr(*if_expr.then_branch, env);
        }
        return eval_expr(*if_expr.else_branch, env);
    }
    case ExprKind::lambda: {
        const auto& lambda_expr = static_cast<const LambdaExpr&>(expr);
        auto function = std::make_shared<Function>();
        function->param = lambda_expr.param;
        function->body = lambda_expr.body.get();
        return Value::make_function(function);
    }
    case ExprKind::call: {
        const auto& call_expr = static_cast<const CallExpr&>(expr);
        Value callee = eval_expr(*call_expr.callee, env);
        if (callee.kind != Value::Kind::function) {
            throw std::runtime_error("function call expected a function");
        }

        Value argument = eval_expr(*call_expr.argument, env);
        Env call_env; // No outer bindings until the closure chapter.
        call_env.push_back({callee.function_value->param, argument});
        return eval_expr(*callee.function_value->body, call_env);
    }
    case ExprKind::input: {
        return Value::make_number(mini_read_int());
    }
    case ExprKind::output: {
        const auto& output_expr = static_cast<const OutputExpr&>(expr);
        long value = expect_number(eval_expr(*output_expr.value, env), "cout argument");
        return Value::make_number(mini_write_int(value));
    }
    }

    throw std::runtime_error("unknown expression kind");
}

最后补上 Value 的构造入口、顶层空环境和显示方式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Value Value::make_number(long value) {
    Value result;
    result.kind = Kind::number;
    result.number_value = value;
    return result;
}

Value Value::make_function(std::shared_ptr<Function> value) {
    Value result;
    result.kind = Kind::function;
    result.function_value = std::move(value);
    return result;
}

Value eval(const Expr& expr) {
    Env env;
    return eval_expr(expr, env);
}

std::string format_value(const Value& value) {
    switch (value.kind) {
    case Value::Kind::number:
        return std::to_string(value.number_value);
    case Value::Kind::function:
        return "<function>";
    }
    throw std::runtime_error("unknown value kind");
}

format_value() 只提供可读表示,不会打印函数体,也不会调用函数。

完整推演贯穿示例

再走一次:

1
2
(let inc (lambda x (+ x 1))
    (inc 41))

每一步的环境和值如下:

1
2
3
4
5
6
7
8
9
步骤                         当前使用的环境                  结果
求 let 的 value              []                              Function(x, body)
进入 let 的 body             [inc -> Function(x, body)]      暂无
求调用的 callee:inc         [inc -> Function(x, body)]      同一个函数 Value 的副本
求调用的 argument:41        [inc -> Function(x, body)]      Number(41)
建立 call_env                [x -> Number(41)]               暂无
在 call_env 求 (+ x 1)       [x -> Number(41)]               Number(42)
调用返回                     回到 let 的求值过程             Number(42)
离开 let,弹出 inc           []                              Number(42)

这里同时出现了两个环境,但它们没有合并。inc 只在调用者环境里帮助找到函数;进入函数体后,参数 x 才是新环境的起点。

如果函数体还有自己的 let:

1
((lambda x (let y 2 (+ x y))) 40)

它会在 call_env 后面临时压入 y -> 2,算完再弹出,和第二章的 let 规则完全相同。

用运行结果检查语义

1
2
3
4
5
cd code/04_functions
make
./mini run examples/call.lang
./mini run examples/function_value.lang
./mini run examples/capture_error.lang

前两个程序都输出:

1
42

最后一个程序会在真正进入函数体并查找自由变量 x 时输出:

1
error: undefined variable: x

到这里,解释器路径已经闭合:AST 中的 lambda 变成函数 Value,AST 中的 call 建立参数环境并求值 body。下一步要让编译器用函数 IR 表达同一套有效程序语义。


4.3 Parser:特殊形式之外就是调用

上一节

4.5 IR:把函数体和函数值分开

下一节