只要能通过语法分析得到抽象语法树,程序的执行就简单了。解释器只需从抽象语法树的根节点开始遍历该树直至叶节点,并计算各节点的内容即可。这就是解释器的基本实现原理。

eval方法

说白了,就是给每个节点都定义一个eval方法,用于计算出当前节点的值。比如当前节点是一个表达式,它的eval方法就会递归调用子节点的eval方法。
而且不同类型的节点,对eval的实现有不同的定义。

一个算式

1
13 + x * 2

+运算符对应的eval方法计算流程:

  • 想要计算出这个算式的值,首先会调用 + 运算符的eval方法,然后会调用left().eval(env) 用于获取左侧节点的值,也就是13。
  • 接着调用右侧节点,返回 x * 2的值。细分一下,因为它是一个* 运算符,其内部和 + 运算符的eval差不多,会依次调用左右节点,并返回乘法运算结果。
  • 最后 + 节点的eval,将左右节点的值相加并返回。
  • 另外,eval里的env参数是环境对象,它用来记录变量名称和值的对应关系。(类似Python的字典)

环境

环境对象通过哈希表为变量的名称与值建立了对应关系。put 方法用于添加新的名值对,get 方法则能够以名称为键搜索哈希表。

各种类型的eval

作者用了一个叫GluonJ的系统,用来给各种节点类添加eval,这么做的目的是将eval和节点类分离,都写到一个java文件内方便管理。其实可以不用,直接在节点类源码中添加eval也行。

抽象语法树中表示整型字面量的叶节点是一个Number对象,它的eval返回与之对应的整型字面量。

用来表示变量名的叶节点是Name对象,它eval方法将通过env环境,找到该变量的值。

单目减法:

1
- 23

单目减法运算表达式的节点对象是一个 Negative 类的对象。该节点有一个子节点,用来表示减号右侧的子表达式。operand方法能够获得这一子节点。

含有 = 运算符的赋值表达式是一个例外,它不会递归调用子节点的 eval 方法。双目运算符的节点对象是一个 BinaryExpr 类的对象BinaryExpr 类的 eval 方法在遇到 = 运算符时将做特殊处理。

赋值表达式:

1
a = 7

右侧的值可以通过eval计算得到,左侧不行。因为左侧值的eval方法会返回变量a的当前值。左侧值需要一种名为L-value的特殊表达式计算。计算得到的左值将更新env环境中的数据。如果赋值表达式左侧不是一个变量,将会报错。

IfStmnt 类与 WhileStmnt 类的 eval 方法。代码块的计算结果就是最后执行的语句的计算结果。

以IfStmnt为例,它将首先调用 condition 方法,对返回的子节点递归调用 eval 方法。最终得到的返回值即是 if 语句中条件表达式的结算结果(就是条件判断的部分)。根据该结果选择对应的代码块,调用所执行代码块的eval。

测试解释器

在程序代码之后将显示多行计算结果。之所以会这样,是因为每一条语句都会在执行后输出结果``。第 3 行显示的是整个 while 语句的计算结果。最后一行是 sum 的值,即 1 至 9 相加的和。