看完了 Crafting Interpreters 第 18 章,Types of Values。
本章将 vm 的运行时栈和常量表所支持的数据类型,从 double 升级为了用 tagged union 实现的 Value,从而可以支持 bool、number、nil 等类型。后续章节还将继续扩展。
这里使用了 tagged union 技法,本质上是用 C 来实现像 Rust 那样的 enum。大体做法是:用一个 enum 字段,标记类型,再用一个 union 字段,保存具体类型的值。根据不同的类型,使用 union 里的不同字段。
接下来,添加了操作布尔值和 nil 的指令,它们只是向 stack 里放入相应的值。这当然可以用 `OP_CONSTANT` 指令来完成,但是,用专用指令的话,可以使得生成的字节码代码少一个字节,性能更好。
最后,支持了「逻辑非」运算和比较运算。值得一提的是,支持比较运算时,只增加了三个指令 `OP_EQUAL`、`OP_GREATER`、`OP_LESS`。对于 `!=`,`<=`,`>=`,则由这三个指令与 `OP_NOT` 组合完成。作者强调,字节码指令与用户代码不需要一一对应,虚拟机有自由选择任何指令与代码序列,只要它们有正确的用户可见的行为即可。而从性能考虑,则不如每个运算符一个指令了。但是,作者强调的这一点,是值得注意的。
总体上这一章还是比较简单的。
View quoted note →
yfaming
yfaming@coinos.io
npub1jz8m...rg8y
- coder: Rust, Python, Racket(learning...)
- Nostr 中文圈 https://following.space/d/musdrpjpdmbr 值得关注的 nostr 中文用户都在这儿!
- 博客 https://yfaming.com/
看完了 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 似乎还没能完全理解,需要找其他资料再学习一下。
View quoted note →
今天看完了 Crafting Interpreters 第 16 章,Scanning on Demand。
Scanning 在 jlox 里已经详细讲过,而且本身算简单,因此这一章新东西不多。这里只记录一下与 jlox 的不同之处。
clox 的 TokenType enum 增加了 `TOKEN_ERROR`。scan 时遇到错误,就返回它。
clox 的 scanner 采用 lazy 方式,每次调用 `scanToken()` 函数时,返回一个 token。这样,可避免在 C 中管理资源的负担。而且 parser 只需要 lookahead 1 个 token,本就不必保留所有 token。
另外,对于 literal (字面量)的值,clox 在 scanning 阶段没有记录。而 jlox 记录在 `Token.literal` 字段里。
clox 在识别 identifier 和 keyword 时,采用了 Trie 的思路,效率更高。
所有的 keyword 都是合法的 identifier,scan 时需要区分出来。在 clox 和 jlox 中,都是先识别 identifier,然后看它是不是 keyword。如果是 keyword 就返回 keyword,否则就返回 identifier。这是一个「将 keyword 从 identifier 中挑出来」的过程。
jlox 将所有的 keyword 放到一个 HashMap 里,识别过程很简单。
clox 则使用了 trie 数据结构,更高效。它尽可能减少了对字符(串)的访问次数。
由于 keyword 数量有限(16 个),组成的 trie 也非常小。clox 直接用嵌套的 switch 语句来实现。代码看起来笨拙,但效率很高。作者提到 v8 也使用了这样的做法。
Trie 的核心思路是,尽可能减少对字符的访问。比如,如果 identifier 第一个字符是 d,则它不可能是任何 keyword。只比较一个字符就足够了。只有当 identifier 是 keyword 时,才需要比较所有字符。而反观 jlox,检查 identifier 是否在 keyword hash map 里,需要计算 identifier 的 hash code。计算时就得访问 identifier 的所有字符。
View quoted note →
#DeFi实盘 2026-08-16
当前市值 124.4,净值 0.6071。本周 LP 收益 0.19。
BTC price = 62957;SOL price = 75.15。
# 本周概况
还是很冷清。
# 杂感
《一九四二》里说,人饿的时候不想说话。😂


看完了 Crafting Interpreters 第 15 章,A Virtual Machine。
这一章实现了一个基于栈的虚拟机(stack-based virtual machine)。
基于栈的虚拟机,是虚拟机的两大主流架构之一,另一种是基于寄存器的虚拟机。CPython、JVM、WebAssembly 是基于栈的;而 Lua、Android 的 Dalvik、Linux 的 BPF 则是基于寄存器的。
基于栈的虚拟机,初看有些神秘,但其实非常简单。
顾名思义,它有一个栈保存运行时的状态。大部分指令默认从栈上取操作数(operand),并将计算结果保存到栈上。(这使得基于栈的虚拟机的字节码指令非常紧凑,因为不需要指定从哪里取数及存到哪里。)
具体到 clox 的虚拟机,VM struct,它有有一个栈,还有个 `ip` 指针,指向下一条指令。运行时,通过一个循环,不断从 ip 获取指令,然后通过一个 switch 语句进行指令分派,决定要执行的逻辑。
就这么简单。
这一章支持了 constant、return 及加减乘除指令,后续将不断扩展指令集,以及更重要的,将 Lox 代码编译为字节码指令。
View quoted note →
看完了 Crafting Interpreters 第 14 章,Chunks of Bytecode
从这一章开始,我们进入第二部分,用 C 语言实现一个基于字节码的 Lox 虚拟机。
这一章,我们定义了用来表示字节码的 enum OpCode,目前只有 `OP_CONSTANT` 和 `OP_RETURN` 两个指令。然后定义了 `Chunk` struct 表示整个字节码程序。`Chunk` 除了包括字节码,还包含一个常量池,和指令对应的行号。
本章还为我们的字节码实现了 disassembler (反汇编器),它将二进制的字节码指令转换为人类可读的文本指令。
View quoted note →
看完了 Crafting Interpreters 第 13 章,Inheritance。
这一章支持了类的继承。
Lox 只支持单继承,所以并不复杂。
文法层面,用 `<` 号表示继承,形如 `class Dog < Animal {...}`,与 Ruby 相同。
另外增加了 super 表达式。如 `super.method()` 会将 `super.method` parse 为 super 表达式,后面的 `()` 则解析为函数调用。
LoxClass 中增加一个字段记录类的 super class,查找方法时,也要顺着继承链依次向上查找。
在处理 class 声明时,为 super 创建专门作用域。进入 class 声明,立即创建一个作用域,并将 super 定义到里面。接下来,再创建另一个作用域,将 this 定义到里面。然后再定义方法。这样在方法的代码里面引用的 super 就是当前类的 super class 了。
至此,本书第一部分,用 Java 实现 tree-walk interpreter 宣告完成。
看了下我敲下的代码,排除注释、空行、测试等,只有 2002 行,简直不可思议。
只要 2000 行代码,就能从头实现一门功能基本齐全的语言,我以前从未想过!
这些代码,包括 lexer、parser、resolver 和 interpreter。
而这门语言的功能齐全,它支持变量、表达式、函数,还支持类和继承等 OOP 特性。
View quoted note →
今天看完了 Crafting Interpreters 第 12 章 Classes。
这一章支持了 class。作为动态类型语言,Lox 的 class 与 Python 的有些相似。用 `class` 声明类,用 `init` 方法初始化实例。
为此,增加了声明类的文法规则,并增加了 Get 表达式以支持访问实例的字段(和方法),增加了 Set 表达式以支持对实例的字段赋值。同时还支持了 This 表达式,以表示 `this`。
类用 `LoxClass` 表示,它保存着类的名字以及方法列表。方法用现成的 `LoxFunction` 表示。
同时 `LoxClass` 也实现了 `LoxCallable` 接口,以便创建类的实例。
类的实例用 `LoxInstance` 表示,它保存着所属的类,及字段值。
对于 `this` 的支持值得一提。
我们知道,Resolver 中的 scope 链,与 Interpreter 中的 Environment 链必须保持一致。scope 与 Environment 本质是同一个东西,只不过 scope 是编译时的,Environment 是运行时的。
Resolver 处理 class 声明时(`visitClassStmt`),创建了新的 scope,并将 this 定义在新的 scope 里。这也意味着,class 的 method 也是在新的 scope 中定义的。
但 `Interpreter.visitClassStmt` 却没有创建新的 Environment。而是在访问实例的属性时(`visitGetExpr`),如果这个属性对应的是一个 method,就调用 `LoxFunction.bind` 方法。`LoxFunction.bind` 会以 method 的 closure 为 enclosing Environment 创建新 Environment,把 this 绑定为对应实例, 然后组装为新的 LoxFunction 并返回。
这样的话,类的方法调用时,Environment 就与 Resolver 的 scope 一致了,而且确定了 this 所指向的值。
具体地,形如 `ins.method()` 的方法调用,`ins.method` 被 parse 为 `Expr.Get`。对 `ins.method` 求值,即对 `Expr.Get` 求值,就会执行这里所说的逻辑。得到新的 LoxFunction 后,再进行函数调用,就和其他函数没有什么区别了。
关于创建类的实例,其实包含两个步骤。
第一步是真正的创建实例,得到 LoxInstance,它是由 LoxClass 负责的。LoxClass 实现了 LoxCallable 接口,调用时返回一个 LoxInstance。
第二步是初始化,由用户提供的 init 方法完成。为了方便使用,Lox Interpreter 确保 init 方法返回实例,且禁止它返回其他值。
至此,本书第一部分,用 Java 实现的 tree-walk interpreter 只剩下最后一章 Inheritance 了。而我敲下的代码,刨除注释和空行,只有 1916 行。神奇!
View quoted note →
#DeFi实盘 2026-08-09
当前市值 127.39,净值 0.6217。本周 LP 收益 0.22。
BTC price = 65176;SOL price = 76.61。
# 本周概况
连着好一个月 LP 收益都在 0.2 左右,在最热的夏天感受最冷的寒冬。
# 杂感
Coldcard 硬件钱包漏洞事件,越来越有意思了,有人怀疑是内部人有意为之,越来越惊悚了。
最近专注看书,web3 关注少了。


看完了 Crafting Interpreters 第 10 章,Function Calls
这一章实现了函数。
函数是用 `LoxCallable` interface 表示的。Native 函数,直接用 java 代码实现这个 LoxCallable interface 即可。Lox 代码定义的函数,在 parse 之后创建为 LoxFunction 实例(见 visitFunctionStmt)。
`return` 语句通过 Java 的异常实现。`return` 也是一种控制流语句,它从任意地方返跳回到函数调用边界。借助 Java 异常来实现,是非常省事的办法。否则,就需要自己显式操作控制流了,麻烦程度大增。Lox 不支持 continue 和 break,如果要支持的话,也可使用同样的办法。
另外发现,实现 `return` 时,没有检查 `return` 语句是否处于函数定义中。在解释器中可以直接输入 `return;`,它会正常执行,并因抛出的 Java 异常( `Return`)未被捕获而导致解释器退出。正常来说,应当在 semantic analysis 阶段检查出来并报错的。且看后续章节会不会涉及这些。(剧透:下一章就会解决。)
本章的精髓在于,如何使用 Environment 管理作用域。
Environment 是在第 8 章 Statements and State 为了支持语句块(bloc statement)而引入的。在这一章,它成为了正确实现函数的关键因素之一。现代语言几乎都支持作用域(scope),支持作用域嵌套,名字的 shadowing 等等机制。函数方面,往往还支持高阶函数和闭包。
Interpreter 类里面有 globals 和 environment 两个类型为 Environment 的字段。值得注意。
- globals 表示全局 Environment。
- Interpreter.environment 表示 Interpreter 的当前 environment。
刚开始运行时,它们是同一个对象。表示 Interpreter 处于全局作用域。
随着 Interpreter 进入/退出新的作用域,Interpeter.environment 会随之变化。
进入新的作用域时(executeBlock),会以当前 environment 创建新的 Environment,并以之取代 Interpreter 的当前 environment。
退出作用域时,Interpreter.environment 会恢复为之前的 Environment。
定义变量(visitVarStmt)和函数(visitFunctionStmt)时,均是将名字定义在 Interpreter.environment 即当前 Environment 里面。
这意味着,如果我们在全局定义变量和函数,那么它们就是全局作用域可见的;如果在某个局部作用域里定义变量和函数,它们就是局部作用域可见的。
```lox
{
var x = 1;
fun localFn() {
print "localFn";
}
localFn();
}
// Error: Undefined variable 'localFn'.
localFn();
```
比如,上面这段代码中,`localFn` 只在代码块里面可见。
我们还可以在函数内部定义函数,它们只在函数内部可见。
```lox
fun fn() {
var x = 1;
fun inner() {
print "inner";
}
inner();
}
// Error: Undefined variable 'inner'.
inner();
```
实现函数调用(`LoxFunction.call` 方法),也需要仔细考虑如何与 Environment 交互。执行函数调用需要在单独的 Environment 中进行,函数参数和局部变量,都需要在这个 Environment 中定义。但是,函数还需要引用其他名字,比如其他函数、全局变量等。
在这一章的初版实现中,函数调用的 Environment 的 enclosing Environment 为 Interpreter 的全局 Environment(`Interpreter.globals`)。这样,在函数内部就可以正确引用在全局作用域里定义的函数和变量了。但是,这样做是不够的。
它会导致一个问题,在语句块和函数内部定义的函数,无法正确引用语句块里和外层函数里的局部变量。比如上面两个示例中,`localFn` 和 `inner` 函数,都无法引用它外层的 `x` 变量。
看到这里也许会想,函数调用时,一定要用 `globals` Environment 吗?用解释器的当前 Environment (即 `Interpreter.environment`)应该能解决问题?答案仍然是否定的。它使得我们能够引用外层的名字,却破坏了「静态作用域」规则,使用得作用域变成动态的了:函数中名字的含义将由调用位置决定,而不是由函数在源码中的定义位置决定。
```lox
var lang = "Lox";
fun printLang{
print lang;
}
{
var lang = "Java";
printLang();
}
```
定义 `printLang` 时,根据它在源码中的位置,函数体里的 `lang` 会解析到全局作用域中的 `lang` 绑定。以后无论从哪里调用 `printLang`,它都不会改为访问调用者的局部 `lang`。
而如果我们修改实现的话,在语句块中调用 `printLang()` 时,将会打印出 Java。这就违背静态作用域的规则了。
这里的困境,与闭包有关。以下用这个经典例子来说明:
```lox
fun makeCounter() {
var i = 0;
fun count() {
i = i + 1;
print i;
}
return count;
}
var counter = makeCounter();
counter(); // "1".
counter(); // "2".
```
按照初版实现,在调用 `makeCounter` 时,会创建一个 Environment,并将 `i` 就是定义到这个 Environment 中。但调用完毕,这个 Environment 直接被丢弃,`i` 的绑定也无法查找了。
解决办法也简单,我们不要丢弃这个 Environment,而是要保存到内部嵌套函数 `count` 对应的 `LoxFunction` 对象里面。这个 Environment 称为 closure。而且函数调用时,也使用 closure 作为 enclosing Environment。这样,函数就可以使用外层作用域里定义的变量了(而不是使用 `Interpreter.globals` 作为 enclosing Environment)。
在执行语句块时,也会创建一个 Environment。对于在语句块中定义的函数,也是照此办理,逻辑是相同的。
而这个 closure Environment 最终仍然会引用全局 Environment,所以在全局作用域中定义的函数、变量等,也可正常访问。
至此,Lox 对于函数、作用域、闭包的支持,已经相当不错了。但是,closure 的实现仍有一些问题,这就留待下一章揭晓了。
View quoted note →
看完了 Crafting Interpreters 第 9 章,Control Flow。
这一章增加了逻辑表达式(and、or),if 语句,while 和 for 循环。
实现 for 循环时,介绍了一下语法糖 syntactic sugar,在 parse 时,就把 for 循环给改写为 while 循环语句了。这就是脱糖 desugaring 过程。
Lox 的循环不支持 continue 和 break 语句。所以大大降低了实现难度。
而因为同样原因,我感觉语法分析阶段做的事情非常少。基本上 parse 得到语法树(Stmt class)之后,就结束了,没有额外工作,感觉有所缺憾。希望后续章节能够涉及这方面。
View quoted note →
太夸张了🙈
View quoted note →
刷完 Crafting Interpreters 第 8 章,Statements and State。
这一章内容较多。增加了对语句的支持,包括表达式语句、print 语句、变量声明语句。同时增加了赋值表达式,并且支持了作用域。
实现作用域时使用的 Environment,与 Peter Novig 的 [(How to Write a (Lisp) Interpreter (in Python))](https://www.norvig.com/lispy.html) 经典文章里的,是同一思路。
最后讨论了一下变量声明是显式好,还是隐式好。结论是,显式更好。
Lox 相比真正编程语言,还缺少控制结构(if、while 等)、函数。这是接下来两章的内容。
现在的代码量只有 1059 行(不含注释和空行),已经做到这个程度,太神奇了。
随着内容的增加,感觉定义好 grammar 规则非常重要。事先要做好推演。
不然的话,越往后越容易出现问题。
View quoted note →
完成第 7 章 Evaluating Expressions。
这一章篇幅较短,完成之后,就得到一个支持加减乘除、字符串拼接和逻辑判断的最简版 Lox 解释器了,大体上相当于一个计算器。求值仍然借助 visitor 模式,对表达式树进行后序遍历。本身算是简单。
至此,Lox 解释器已经初具雏形。scanner 已经全部完成,后续不需要改动。parser 及 evaluation 过程,只支持了较简单的表达式。后续章节将不断扩充语言,支持语句、控制流、函数、类与继承等等。
View quoted note →
刷完了第 6 章 Parsing Expressions,感觉最大的障碍已经克服了。😂
Parsing 技术非常丰富庞杂,包括 LL(k)、LR(1)、LALR、Earley、Packrat、PEG 等算法,还有 parser combinator、parser generator 等等技术。龙书太注重理论探讨,曾经让我觉得,只有完整掌握这些这些算法,才能写 compiler。龙书误我。😂
Crafting Interpreters 则选择跳过理论分析,直接开始构建 recursive descent parser。并且告诉我们,Rust、GCC、V8 等项目都采用了 recursive descent parser,既然如此,还有什么好担心的?😎
Contex-free grammar 方面,也是面向实际需要,介绍了如何消除歧义、如何对文法规则分层以解决运算符优先级问题。
最后,跟着把代码敲完,就得到了一个完整的 parser 了。🎉
翻越一座大山,原来可以如此简单!
View quoted note →
今天刚刷完 Chapter 6 Parsing Expressions 😎
View quoted note →
狂推神作 Crafting Interpreters!
Compiler/interpreter 技术,一直有种神秘色彩。一方面,掌握它能让你在更高层面看待编程语言,甚至自己创造一门。另一方面,庞杂的技术和龙书这样的大部头书,又往往将人劝退。
幸好,有了 Crafting Interpreters。
它的定位出奇制胜,因为它选择了一条独特的路径,带着我们游览 compiler/interpreter 技术丛林。
这本书以实践为主,主线是从 0 构建 interpreter,理论只做最简要的介绍。非常适合我这种连龙书 Syntax Analysis 那一章都没撑过去的苦手。😂
跟随着它的路径,我们只用几千行代码,就实现了一门完整的编程语言(Lox)。而且是两次,一次用 Java 一次用 C。
第一部分用 Java 实现一个 tree-walk interpreter。每章一个主题,先是实现 scanner,然后 recursive descent parser。然后从表达式求值逐步扩充,直到完整支持语句、控制流、函数、类与继承等语言特性。
第二部分改用 C 语言,实现一个虚拟机。我们从设计字节码开始,并将前面实现的语言特性编译为字节码。
整个过程中,不依赖外部库,需要的东西全部自己动手。跟着书的进度,几百行代码之后我们得到一个 scanner,再几百行之后得到一个 parser,几千行之后你就实现了完整的编程语言:Lox。
剩下的,就是自由发挥的天地了。😎
Crafting Interpreters