Join Nostr
2026-08-29 04:21:04 UTC

yfaming on Nostr: 看完了 Crafting Interpreters 第 23 章,Jumping Back and Forth。 ...

看完了 Crafting Interpreters 第 23 章,Jumping Back and Forth。

本章添加了跳转指令,并实现了 if、while/for 等控制结构,以及 and 与 or 逻辑运算符。

在 jlox 中,控制流是隐式的。Lox 的 if/else、while 循环、return 语句等等,是由 Java 的 if/else、while 循环和异常实现的。在 interpreter 层面,我们没有显式地操作过控制流。

但是,在 clox 里我们可以显式操作了。`VM.ip` 字段,指向正在执行的字节码。顺序执行时,`ip` 是不断增加的,从而程序不断向后执行。如果修改 `VM.ip` 的值,就可以让程序的控制流向后或者向前跳。基于此,就可以实现各种控制流了。

实现 if 语句,`if (expr) thenBrach else elseBranch`,我们用到两个跳转指令,`OP_JUMP_IF_FALSE <offset>` 和 `OP_JUMP_IF_FALSE <offset>`。

它们需要 offset operand,决定 `VM.ip` 要跳多远。 offset operand 为 16 位的,而指令是 8 位的,它占了两个指令的大小。

`OP_JUMP_IF_FALSE` 是条件跳转。如果栈顶元素为 false 时,才跳转。`OP_JUMP` 为无条件跳转。所谓跳转,只是 `VM.ip += offset` 而已。

`if (expr) thenBrach else elseBranch` 生成的字节码指令序列为:

```
<expr bytecode>
OP_JUMP_IF_FALSE // 跳到 elseBranch
<thenBranch bytecode>
OP_JUMP // 跳到整个 if 语句之后
<elseBranch bytecode>
...
```

涉及跳转时,往往在后面的语句完成编译之后,我们才能确定 offset 的值。解决办法是经典的 backpatching。offset 先随便设一个值,后面语句的代码生成之后,再回来修改 offset 为正确值。


逻辑运算符 `and` 和 `or` 需要短路求值,也需要跳转指令的支持。同样使用 `OP_JUMP` 和 `OP_JUMP_IF_FALSE` 指令实现。略。


实现 while 循环时,我们添加了 `OP_LOOP <offset>` 指令,它往回跳。
其实它和 `OP_JMP` 差不多,作者也提到了,只是不愿意在处理位操作时还要处理符号什么的,所以才分两个指令。其他部分,其他和 if 语句的实现差不多。


`for` 循环的实现更麻烦些。

在 jlox 中,for 循环被当作语法糖,parse 之后,直接生成 while 循环对应的 ast 节点(`Stmt.While`)。而 clox 是 single-pass的,一边 parse 一边生成字节码指令,没有 ast 可用。无法像 jlox 那样改写为 while 循环。

更麻烦的是,clox 遇到一个语句时,需要立即生成字节码,如果生成的字节码顺序和执行顺序不同,就只好通过跳转指令来改变执行顺序。

for 循环包括 initializer、condition、increment 和 body。increment 出现在 body 之前,却要在 body 执行后,才执行。

因此,为 increment 生成字节码时,会更加麻烦。我们在 increment 的字节码前面,先要放一个 `OP_JUMP`,立即跳到 body,避免先于 body 执行。
而 increment 的字节码后面,还要放一个 `OP_LOOP`,以跳回到循环开始的地方(具体是 initializer 后面)。

这就导致,condition 之后有一个 `OP_JUMP_IF_FALSE`,increment 前面一个 `OP_JUMP` 后面一个 `OP_LOOP`,body 后面还有一个 `OP_LOOP`。一共 4 个跳转指令,理解成本就变高了。


另外,以上所有控制流语句,都需要注意,执行后,需要清理 condition 表达式留在栈上的值,避免违反栈的使用原则。

接下来,开始 Crafting Interpreters 第二部分,用 C 实现的 Lox 字节码解释器 clox。
在第一部分,用 Java 实现的 tree-walk interpreter jlox 中,我们完整实现了 Lox 解释器,包括 scanner、parser、resolver、interpreter 等等。
但是,第一部分的解释器利用了 Java 语言的许多特性。
比如,Lox 的值,全部用 Java 的 Object 类型表示。
Lox 里的 return,用 Java 的异常来实现。
而 Java 自带 GC,所以 Lox 中对象的生命周期,我们也完全没考虑过。

在第二部分,我们将使用 C 语言实现解释器。C 比 Java 更加 low-level,它不支持 OOP,不支持异常和 GC。
因此,这几个问题,我们就需要在 C 里面手动处理了。
而且,第二部分我们实现的是字节码解释器。我们需要定义字节码,并在 parsing 将 ast 编译为字节码。


用不同语言实现同一门语言,但采用略有不同的技术,可以加深我们的理解,并学习 compiler/interpreter 领域的不同主题。