解释器:把自由变量放进函数值
本节阅读量:
语言语义已经确定:闭包在定义位置保存自由变量,调用时从这份环境开始执行函数体。解释器可以直接用 C++ 数据结构表达这件事,不需要先考虑机器地址和字节偏移。
Closure 保存四项信息
第七章的解释器闭包是:
1
2
3
4
5
6
|
struct Closure {
std::string param;
const Expr* body;
std::string self_name;
std::vector<std::pair<std::string, Value>> env;
};
|
各字段的作用是:
| 字段 |
含义 |
param |
单个参数的源码名字 |
body |
调用时要求值的 AST 子树 |
self_name |
letrec 的递归名字;普通 lambda 为空 |
env |
自由变量名字及其创建时 Value |
Value::Kind 继续区分 number、closure 和 record。解释器中的 closure 与 record 使用不同 C++ 对象类型;通用 heap tag 是编译后机器表示的协议,不需要照搬成 C++ 指针位运算。
只复制自由变量环境
当前环境仍是按词法嵌套顺序保存的列表:
1
|
using Env = std::vector<std::pair<std::string, 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);
}
|
capture_environment 按自由变量列表的稳定顺序取值:
1
2
3
4
5
6
7
8
9
|
Env capture_environment(const Env& env,
const std::vector<std::string>& names) {
Env captures;
captures.reserve(names.size());
for (const auto& name : names) {
captures.push_back({name, lookup(env, name)});
}
return captures;
}
|
若当前环境还有十个与函数无关的绑定,而 names 只有 x,闭包中只保存 x。这与编译器只分配必要捕获槽的做法一致。
求值普通 lambda
解释器的 AST dispatcher 使用显式 ExprKind + switch。普通 lambda 分支是:
1
2
3
4
5
6
7
8
|
case ExprKind::lambda: {
const auto& lambda = static_cast<const LambdaExpr&>(expr);
auto closure = std::make_shared<Closure>();
closure->param = lambda.param;
closure->body = lambda.body.get();
closure->env = capture_environment(env, free_vars(lambda));
return Value::make_closure(closure);
}
|
这里没有调用 eval_expr 求值函数体。发生的事情只有:
- 找出自由变量;
- 在当前定义环境中查到它们的 Value;
- 创建 closure Value。
普通 lambda 的 self_name 保持为空。
用一个环境快照走一遍
程序:
1
2
|
(let x 40
((lambda y (+ x y)) 2))
|
执行到 lambda 时:
1
2
3
4
|
当前环境 [x -> 40]
自由变量 [x]
闭包环境 [x -> 40]
参数名 y
|
离开外层 let 之前立即调用不是闭包成立的前提。即使 closure 被放进 record、由另一个函数返回,shared_ptr<Closure> 仍保持 C++ 对象存活,env 中也仍有 x -> 40。
调用先检查 callee
call 分支按语言规定的顺序执行:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
case ExprKind::call: {
const auto& call = static_cast<const CallExpr&>(expr);
Value callee = eval_expr(*call.callee, env);
if (callee.kind != Value::Kind::closure) {
throw std::runtime_error("function call expected a function");
}
Value argument = eval_expr(*call.argument, env);
Env call_env = callee.closure_value->env;
if (!callee.closure_value->self_name.empty()) {
call_env.push_back({callee.closure_value->self_name, callee});
}
call_env.push_back({callee.closure_value->param, argument});
return eval_expr(*callee.closure_value->body, call_env);
}
|
注意这里没有把调用点的 env 复制给函数体。它只用于求 callee 和 argument。函数体从 closure 保存的定义环境开始。
对于普通闭包,执行环境最终是:
函数体得到 42。
为什么不能使用调用点环境
运行:
1
2
|
cd code/07_closures
./mini run examples/lexical_scope.lang
|
调用 f 时,调用点环境里最近的 x 是 100。但 callee.closure_value->env 保存的是定义位置的 x -> 40,所以输出:
若把 call 分支错误地写成 Env call_env = env;,这个测试就会输出 102。
递归闭包不保存自己的 shared_ptr
letrec 需要让函数体里的名字重新得到同一个 closure。最直接但不理想的做法,是让 Closure::env 保存指向自身的 Value;那会形成 shared_ptr 引用环。
第七章改用 self_name:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
case ExprKind::letrec: {
const auto& letrec = static_cast<const LetRecExpr&>(expr);
auto closure = std::make_shared<Closure>();
closure->param = letrec.param;
closure->body = letrec.function_body.get();
closure->self_name = letrec.name;
closure->env = capture_environment(env, free_vars(letrec));
Value function = Value::make_closure(closure);
env.push_back({letrec.name, function});
Value result = eval_expr(*letrec.body, env);
env.pop_back();
return result;
}
|
创建时只保存真正的外层自由变量。每次 call 已经拿到了实际 callee,因此调用时再加入:
1
|
call_env.push_back({callee.closure_value->self_name, callee});
|
递归能力来自实际 callee,不需要 closure payload 指回自己。
递归闭包的环境变化
对 recursive_closure.lang:
1
2
3
4
5
6
|
(let base 10
(letrec adddown n
(if (eq? n 0)
base
(+ 1 (adddown (sub1 n))))
(adddown 5)))
|
创建 closure 时:
1
2
|
self_name adddown
captured env [base -> 10]
|
第一次调用时:
1
|
[base -> 10, adddown -> actual callee, n -> 5]
|
递归调用收到的 callee 仍是同一个 closure Value,每次都按相同规则建立新环境,直到 n 为零。运行:
1
|
./mini run examples/recursive_closure.lang
|
输出:
参数可以遮蔽 self
环境加入顺序先 self、后 param,因此参数是更内层绑定:
1
2
3
|
(letrec f f
f
(f 42))
|
调用环境的尾部是:
1
2
|
f -> actual callee
f -> 42
|
反向查找先找到参数 42。运行:
1
|
./mini run examples/self_param_shadow.lang
|
输出 42。
嵌套闭包自然复用同一规则
外层 closure 执行函数体时,参数和捕获值已经进入 call_env。若函数体里再遇到 lambda,新的 capture_environment 就从这份环境取值:
1
2
3
4
5
|
(let x 40
(((lambda y
(lambda z (+ x (+ y z))))
1)
1))
|
内层 closure 捕获 x -> 40 和 y -> 1。不需要给嵌套闭包增加另一套特殊解释器路径。
closure 与 record 在 Value 层严格区分
record? 只检查 Value::Kind::record:
1
2
|
Value value = eval_expr(*predicate.expr, env);
return Value::make_number(value.kind == Value::Kind::record ? 1 : 0);
|
所以:
1
|
./mini run examples/record_predicate_closure.lang
|
输出 0。
size、get 和 call 分别检查自己需要的 kind。统一 Value 不等于取消种类边界。
函数仍不能参与 eq?
解释器在比较前明确拒绝任一 closure operand:
1
2
3
4
5
|
if (lhs.kind == Value::Kind::closure ||
rhs.kind == Value::Kind::closure) {
throw std::runtime_error(
"eq? does not support function values");
}
|
运行:
1
|
./mini run examples/function_eq_error.lang
|
会得到错误信息,而不是比较两个 shared_ptr 地址。record 身份比较继续比较 record_value 是否指向同一个对象。
解释器与编译器保存的内容一一对应
两条路线外形不同,但语义关系明确:
| 解释器 Closure |
编译后的 closure object |
body |
code pointer |
env[i].second |
capture slot i |
param |
IrFunction::param 与 %rsi |
self_name |
IrFunction::self 与 %rdi |
解释器保存源码名字方便查找;编译器使用 alpha-renamed IR 名字和固定偏移。两者都只保存自由变量,都在调用时加入 self 和参数,也都执行定义位置决定的函数体环境。
7.2 自由变量:只带走以后真正需要的名字
上一节
7.4 结构化 IR:把代码标签和捕获值配成一对
下一节