解释器:环境与变量查找

本节阅读量:

第一章的解释器只需要递归求值:

1
2
Int(n)       -> n
Add(a, b)    -> eval(a) + eval(b)

第二章加入变量后,解释器不能只看当前节点。遇到 Var(x) 时,它还要知道:

1
当前哪些名字是可见的?

这份“当前可见绑定”的集合叫 environment,环境。

环境是什么

本章的环境可以先理解成一张从变量名到整数值的表:

1
2
x -> 40
y -> 2

代码在:

1
code/02_let/src/interp/interpreter.cpp

环境类型写成:

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

也就是一串绑定:

1
[("x", 40), ("y", 2)]

这里没有用 std::unordered_map,是因为同名遮蔽(shadowing)需要保留同名绑定的先后顺序。

lookup 从后往前找

变量引用的求值规则是:

1
eval(Var(name), env) = lookup(env, name)

对应代码:

1
2
3
4
5
6
7
8
long 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);
}

重点是从后往前找。

如果环境是:

1
[x -> 10, x -> 32]

后面的 x -> 32 更近,所以 lookup(x) 应该得到 32。这就是同名遮蔽的基础。

如果一路没找到,就说明变量没有绑定:

1
undefined variable: x

eval_expr 多了环境参数

第一章的 eval 只接收一个 Expr。第二章内部多了一个辅助函数:

 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
long eval_expr(const Expr& expr, Env& env) {
    switch (expr.kind) {
    case ExprKind::integer: {
        const auto& int_expr = static_cast<const IntExpr&>(expr);
        return int_expr.value;
    }
    case ExprKind::add: {
        const auto& add_expr = static_cast<const AddExpr&>(expr);
        return eval_expr(*add_expr.lhs, env) + eval_expr(*add_expr.rhs, env);
    }
    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);
        long value = eval_expr(*let_expr.value, env);
        env.push_back({let_expr.name, value});
        long result = eval_expr(*let_expr.body, env);
        env.pop_back();
        return result;
    }
    }

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

这里仍然沿用第一章的 kind + switch 风格。每一种表达式都有明确分支。

变量引用怎样求值

完整函数里的变量分支只做两件事:

1
2
1. 从 VarExpr 里拿出变量名。
2. 调用 lookup,在当前环境里查它的值。

VarExpr 本身不保存值。解释器每次遇到变量引用,都要根据当前环境重新查找,所以同一个名字在不同作用域里可以得到不同结果。

let 的求值顺序

let 的规则是:

1
2
3
4
5
6
eval((let x value body), env):
    v = eval(value, env)
    把 x -> v 放进 env
    result = eval(body, env)
    弹出 x -> v
    return result

对照前面的完整 eval_expr(),这里有两个要点。

第一,value 在旧环境里求值:

1
long value = eval_expr(*let_expr.value, env);

这时新绑定还没有放进环境。

第二,body 求完以后要 pop_back()

1
env.pop_back();

这表示离开这个 let 的作用范围。内层绑定不应该继续影响外层。

一个完整例子

程序:

1
(let x 40 (+ x 2))

求值过程:

1
2
3
4
5
6
7
8
eval value: 40 -> 40
push x -> 40
eval body: (+ x 2)
  eval x -> lookup x -> 40
  eval 2 -> 2
  40 + 2 -> 42
pop x -> 40
return 42

运行:

1
2
3
cd code/02_let
make
./mini run examples/let.lang

输出:

1
42

同名遮蔽怎样求值

再看一个有两个同名绑定的程序:

1
(let x 10 (+ (let x 32 x) x))

求值过程是:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
eval 外层 value: 10 -> 10
push x -> 10

eval 外层 body: (+ (let x 32 x) x)
  eval 左边: (let x 32 x)
    eval 内层 value: 32 -> 32
    push x -> 32
    eval 内层 body: x
      lookup 从后往前找到 x -> 32
    pop x -> 32
    左边结果是 32

  eval 右边: x
    lookup 找到外层 x -> 10

  32 + 10 -> 42

pop x -> 10
return 42

内层绑定存在时,环境里暂时有两个 x

1
[x -> 10, x -> 32]

lookup 从后往前找,所以内层 body 得到 32。离开内层 let 后,pop_back() 移除 x -> 32,外层的 x -> 10 又重新可见。

运行:

1
./mini run examples/shadow.lang

输出:

1
42

对外的 eval

对外接口仍然保持简单:

1
2
3
4
long eval(const Expr& expr) {
    Env env;
    return eval_expr(expr, env);
}

调用者不需要自己准备环境。完整程序开始求值时,环境是空的。

所以直接运行未绑定变量:

1
x

会在 lookup 里报错。这是正确行为:变量名只有进入某个 let 的 body 后才有值。


2.3 Parser:读出 identifier 和 let

上一节

2.5 IR:把用户名字变成内部名字

下一节