解释器:沿着 AST 直接算结果
本节阅读量:解释器做的事很直接:
|
|
它不会生成汇编,也不会生成别的文件。
例如:
|
|
解释器最后直接得到:
|
|
先用人脑算一次
看这个表达式:
|
|
我们手算时会这样想:
|
|
换成更机械的步骤:
|
|
这就是解释器的核心。
先处理两种表达式
第一章的语言还很小,解释器先学会处理两种表达式。
这里写下的不是“某个实现碰巧这样算”,而是第一节语言规则对应的求值语义:所有合法实现都应该让整数得到自身,让加法得到两个子表达式结果之和。
整数表达式:
|
|
比如:
|
|
加法表达式:
|
|
比如:
|
|
如果左右两边本身还是加法,就继续用同样的规则。
这就是递归:解决一个大表达式时,先解决它的子表达式。
对应到 C++ 代码
代码在 code/01_numbers/src/interp/interpreter.cpp。
AST 节点有一个 kind 字段:
|
|
解释器先看 kind,再用 switch 选择求值规则。
这里的顺序很重要:
|
|
先看整数:
|
|
可以读成:
|
|
expr 的类型是通用的 Expr&。确认 kind 是 integer 之后,这一行:
|
|
可以先理解成:
|
|
再看加法:
|
|
可以读成:
|
|
这里也一样,先通过 kind 确认它是加法,再把通用的 Expr 当成具体的 AddExpr 来用。AST 的构造函数始终让 kind 和实际节点类型保持一致,解释器依赖的就是这条约定。
add_expr.lhs 和 add_expr.rhs 是 unique_ptr<Expr>,也就是“拥有子表达式的指针”。前面的 * 表示取出它指向的那个表达式节点:
|
|
把两次递归调用分别写成语句,也明确规定了从左到右的求值顺序。第一章的加法还没有副作用,交换顺序不会改变数值;后面加入可变状态以后,这个顺序会成为语言语义的一部分。
所以:
|
|
就是“递归求左子表达式的值”。
完整函数:
|
|
switch 在这里的作用是判断:
|
|
第一章只有两种可能:
|
|
后面章节会加更多节点。
跑一下解释器
运行:
|
|
输出:
|
|
再运行:
|
|
输出:
|
|
解释器在整条路径里的位置
现在,前半条路径已经走通:
|
|
解释器在 eval 返回时已经完成工作,它不会保存一份以后再执行的计划。编译器走的是另一条路:先把 AST 变成计算步骤,再把这些步骤写成汇编。下一节就从这份中间计算计划开始。