求值器(evaluator)

在有了一个程序的语法树之后,我们该做什么呢?当然是执行程序了!而这就是求值器的功能。我们将语法树和作用域对象传递给求值器,执行器就会求解语法树中的表达式,然后返回整个过程的结果。

  1. const specialForms = Object.create(null);
  2. function evaluate(expr, scope) {
  3. if (expr.type == "value") {
  4. return expr.value;
  5. } else if (expr.type == "word") {
  6. if (expr.name in scope) {
  7. return scope[expr.name];
  8. } else {
  9. throw new ReferenceError(
  10. `Undefined binding: ${expr.name}`);
  11. }
  12. } else if (expr.type == "apply") {
  13. let {operator, args} = expr;
  14. if (operator.type == "word" &&
  15. operator.name in specialForms) {
  16. return specialForms[operator.name](expr.args, scope);
  17. } else {
  18. let op = evaluate(operator, scope);
  19. if (typeof op == "function") {
  20. return op(...args.map(arg => evaluate(arg, scope)));
  21. } else {
  22. throw new TypeError("Applying a non-function.");
  23. }
  24. }
  25. }
  26. }

求值器为每一种表达式类型都提供了相应的处理逻辑。字面值表达式产生自身的值(例如,表达式100的求值为数值100)。对于绑定而言,我们必须检查程序中是否实际定义了该绑定,如果已经定义,则获取绑定的值。

应用则更为复杂。若应用有特殊形式(比如if),我们不会求解任何表达式,而是将表达式参数和环境传递给处理这种形式的函数。如果是普通调用,我们求解运算符,验证其是否是函数,并使用求值后的参数调用函数。

我们使用一般的 JavaScript 函数来表示 Egg 的函数。在定义特殊格式fun时,我们再回过头来看这个问题。

evaluate的递归结构类似于解析器的结构。两者都反映了语言自身的结构。我们也可以将解析器和求值器集成到一起,在解析的同时求解表达式,但将其分离为两个阶段使得程序更易于理解。

这就是解释 Egg 所需的全部代码。这段代码非常简单,但如果不定义一些特殊的格式,或向环境中添加一些有用的值,你无法使用该语言完成很多工作。