解释器:函数值和一次新的求值环境
本节阅读量:
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
|
前两个程序都输出:
最后一个程序会在真正进入函数体并查找自由变量 x 时输出:
1
|
error: undefined variable: x
|
到这里,解释器路径已经闭合:AST 中的 lambda 变成函数 Value,AST 中的 call 建立参数环境并求值 body。下一步要让编译器用函数 IR 表达同一套有效程序语义。
4.3 Parser:特殊形式之外就是调用
上一节