章节目录

解释器:把自由变量放进函数值

本节阅读量:

语言语义已经确定:闭包在定义位置保存自由变量,调用时从这份环境开始执行函数体。解释器可以直接用 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 求值函数体。发生的事情只有:

  1. 找出自由变量;
  2. 在当前定义环境中查到它们的 Value;
  3. 创建 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 保存的定义环境开始。

对于普通闭包,执行环境最终是:

1
[x -> 40, y -> 2]

函数体得到 42。

为什么不能使用调用点环境

运行:

1
2
cd code/07_closures
./mini run examples/lexical_scope.lang

调用 f 时,调用点环境里最近的 x 是 100。但 callee.closure_value->env 保存的是定义位置的 x -> 40,所以输出:

1
42

若把 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

输出:

1
15

参数可以遮蔽 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:把代码标签和捕获值配成一对

下一节