yfaming on Nostr: 看完了 Crafting Interpreters 第 17 章,Compiling Expressions。 ...
看完了 Crafting Interpreters 第 17 章,Compiling Expressions。
这一章完成了 compiler。compiler 负责 parsing + codegen 两项工作。
compiler 是在 parse 时直接生成 bytecode。而不是先 parse 得到 ast,然后 codegen 得到 bytecode。clox 将两个阶段合并为一个了。这样做的一个原因时,可以避免管理 ast 等资源的负担,毕竟在 C 语言中进行资源管理太麻烦了。
现在,clox 的执行流水线由三部分组成,scanner 生成 token 流,compiler 进行 parsing 并生成 bytecode,vm 执行 bytecode。
这一章只支持了对算术表达式的 parsing 和 codegen。但这次使用的是 Pratt parser,而非 jlox 的 recursive descent parsing。作为认为 Pratt parser 是 parse 表达式的最优雅的方式。
但注意,Pratt parser 适合的是 parse 表达式,而不是 parse 整个的源码。clox 整体仍然在用 recursive descent parsing,后面章节将会看到。
parsing expression 的核心函数是 parsePrecedence。它 parse 与给定优先级相等或者更高的表达式。Pratt parser 使用了了表驱动方式,为每个 token type 指定了其作为前缀操作符对应的处理函数(prefix fn),以及其作为中缀操作符时的处理函数与优先级(infix fn、precedence)。
从人肉理解表达式的角度来看,差不多也是同样逻辑。
当我们遇到一个中缀操作符时(+ - * / and or 等等),如果它后面的操作符优先级更高,则后面的部分应作为当前表达式的一部分,否则就应该属于另外一个表达式。
比如 1 + 2 * 3,遇到 + 时,后面的 * 优先级更高,所以后面的 2 * 3 属于当前的 + 表达式的一部分,得到的是 1 + (2 * 3)。
而对于 2 * 3 + 4,遇到 * 时,后面的 - 优先级更低,所以 + 号及后面的部分不属于当前的 * 表达式。
在 recursive descent parsing 中,我们是把 expression 的 grammar 规则按照操作符优先级拆分,每个规则负责一个优先级。低优先级的规则可以引用高优先级的规则,但反过来不行。
而在 Pratt parser 中,则是将这样的规则明确定义出来,放到表里面,使用时查表。这样可以使得 grammar 比较简洁。
不过,我对 Pratt parser 似乎还没能完全理解,需要找其他资料再学习一下。
接下来,开始 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 领域的不同主题。
Published at
2026-08-19 15:14:51 UTCEvent JSON
{
"id": "560be6644cc328f9f5f78c81a89c85b07fe2e3a69561db1deef29c04b41be0de",
"pubkey": "908fbc3babc322eea473a0ab1e6bf6b3adcf899288ac97b2fa79f34ad408d4e4",
"created_at": 1787152491,
"kind": 1,
"tags": [
[
"q",
"1aea59b1f714db6bebf6019f30c55e36795d9bbba666682c752163c90db1acff",
"wss://lang.relays.land/zh",
"908fbc3babc322eea473a0ab1e6bf6b3adcf899288ac97b2fa79f34ad408d4e4"
]
],
"content": "看完了 Crafting Interpreters 第 17 章,Compiling Expressions。\n\n这一章完成了 compiler。compiler 负责 parsing + codegen 两项工作。\n\ncompiler 是在 parse 时直接生成 bytecode。而不是先 parse 得到 ast,然后 codegen 得到 bytecode。clox 将两个阶段合并为一个了。这样做的一个原因时,可以避免管理 ast 等资源的负担,毕竟在 C 语言中进行资源管理太麻烦了。\n\n现在,clox 的执行流水线由三部分组成,scanner 生成 token 流,compiler 进行 parsing 并生成 bytecode,vm 执行 bytecode。\n\n这一章只支持了对算术表达式的 parsing 和 codegen。但这次使用的是 Pratt parser,而非 jlox 的 recursive descent parsing。作为认为 Pratt parser 是 parse 表达式的最优雅的方式。\n\n但注意,Pratt parser 适合的是 parse 表达式,而不是 parse 整个的源码。clox 整体仍然在用 recursive descent parsing,后面章节将会看到。\n\nparsing expression 的核心函数是 parsePrecedence。它 parse 与给定优先级相等或者更高的表达式。Pratt parser 使用了了表驱动方式,为每个 token type 指定了其作为前缀操作符对应的处理函数(prefix fn),以及其作为中缀操作符时的处理函数与优先级(infix fn、precedence)。\n\n从人肉理解表达式的角度来看,差不多也是同样逻辑。\n当我们遇到一个中缀操作符时(+ - * / and or 等等),如果它后面的操作符优先级更高,则后面的部分应作为当前表达式的一部分,否则就应该属于另外一个表达式。\n\n比如 1 + 2 * 3,遇到 + 时,后面的 * 优先级更高,所以后面的 2 * 3 属于当前的 + 表达式的一部分,得到的是 1 + (2 * 3)。\n而对于 2 * 3 + 4,遇到 * 时,后面的 - 优先级更低,所以 + 号及后面的部分不属于当前的 * 表达式。\n\n在 recursive descent parsing 中,我们是把 expression 的 grammar 规则按照操作符优先级拆分,每个规则负责一个优先级。低优先级的规则可以引用高优先级的规则,但反过来不行。\n\n而在 Pratt parser 中,则是将这样的规则明确定义出来,放到表里面,使用时查表。这样可以使得 grammar 比较简洁。\n\n不过,我对 Pratt parser 似乎还没能完全理解,需要找其他资料再学习一下。\n\nnostr:nevent1qvzqqqqqqypzpyy0hsa6hseza6j88g9tre4ldvade7ye9z9vj7e0570nft2q348yqyvhwumn8ghj7mrpdenjuun9d3shjuewd3skuep00f5qqgq6afvmrac5md47haspnucv2h3k09wehwaxve5zcafpv0ysmvdvlue2w2a0",
"sig": "f6c263b6d8769f526cc0889bfccad3ba811bef35a01c49486b2a64a6101c49c72b000617019b0339e016ee41056e301f6c9f5416c50545da5eff4442d8b67bb1"
}