求值器(evaluator)
在有了一个程序的语法树之后,我们该做什么呢?当然是执行程序了!而这就是求值器的功能。我们将语法树和作用域对象传递给求值器,执行器就会求解语法树中的表达式,然后返回整个过程的结果。
const specialForms = Object.create(null);
function evaluate(expr, scope) {
if (expr.type == "value") {
return expr.value;
} else if (expr.type == "word") {
if (expr.name in scope) {
return scope[expr.name];
} else {
throw new ReferenceError(
`Undefined binding: ${expr.name}`);
}
} else if (expr.type == "apply") {
let {operator, args} = expr;
if (operator.type == "word" &&
operator.name in specialForms) {
return specialForms[operator.name](expr.args, scope);
} else {
let op = evaluate(operator, scope);
if (typeof op == "function") {
return op(...args.map(arg => evaluate(arg, scope)));
} else {
throw new TypeError("Applying a non-function.");
}
}
}
}
求值器为每一种表达式类型都提供了相应的处理逻辑。字面值表达式产生自身的值(例如,表达式100
的求值为数值100
)。对于绑定而言,我们必须检查程序中是否实际定义了该绑定,如果已经定义,则获取绑定的值。
应用则更为复杂。若应用有特殊形式(比如if
),我们不会求解任何表达式,而是将表达式参数和环境传递给处理这种形式的函数。如果是普通调用,我们求解运算符,验证其是否是函数,并使用求值后的参数调用函数。
我们使用一般的 JavaScript 函数来表示 Egg 的函数。在定义特殊格式fun
时,我们再回过头来看这个问题。
evaluate
的递归结构类似于解析器的结构。两者都反映了语言自身的结构。我们也可以将解析器和求值器集成到一起,在解析的同时求解表达式,但将其分离为两个阶段使得程序更易于理解。
这就是解释 Egg 所需的全部代码。这段代码非常简单,但如果不定义一些特殊的格式,或向环境中添加一些有用的值,你无法使用该语言完成很多工作。