yfaming's avatar
yfaming
yfaming@coinos.io
npub1jz8m...rg8y
- coder: Rust, Python, Racket(learning...) - Nostr 中文圈 https://following.space/d/musdrpjpdmbr 值得关注的 nostr 中文用户都在这儿! - 博客 https://yfaming.com/
yfaming's avatar
yfaming 2 weeks ago
看完了 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's avatar
yfaming 2 weeks ago
看完了 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 →
yfaming's avatar
yfaming 2 weeks ago
今天看完了 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 →
yfaming's avatar
yfaming 3 weeks ago
#DeFi实盘 2026-08-16 当前市值 124.4,净值 0.6071。本周 LP 收益 0.19。 BTC price = 62957;SOL price = 75.15。 # 本周概况 还是很冷清。 # 杂感 《一九四二》里说,人饿的时候不想说话。😂 image
yfaming's avatar
yfaming 3 weeks ago
看完了 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 →
yfaming's avatar
yfaming 3 weeks ago
看完了 Crafting Interpreters 第 14 章,Chunks of Bytecode 从这一章开始,我们进入第二部分,用 C 语言实现一个基于字节码的 Lox 虚拟机。 这一章,我们定义了用来表示字节码的 enum OpCode,目前只有 `OP_CONSTANT` 和 `OP_RETURN` 两个指令。然后定义了 `Chunk` struct 表示整个字节码程序。`Chunk` 除了包括字节码,还包含一个常量池,和指令对应的行号。 本章还为我们的字节码实现了 disassembler (反汇编器),它将二进制的字节码指令转换为人类可读的文本指令。 View quoted note →
yfaming's avatar
yfaming 3 weeks ago
看完了 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 →
yfaming's avatar
yfaming 3 weeks ago
今天看完了 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 →
yfaming's avatar
yfaming 0 months ago
#DeFi实盘 2026-08-09 当前市值 127.39,净值 0.6217。本周 LP 收益 0.22。 BTC price = 65176;SOL price = 76.61。 # 本周概况 连着好一个月 LP 收益都在 0.2 左右,在最热的夏天感受最冷的寒冬。 # 杂感 Coldcard 硬件钱包漏洞事件,越来越有意思了,有人怀疑是内部人有意为之,越来越惊悚了。 最近专注看书,web3 关注少了。 image
yfaming's avatar
yfaming 1 month ago
看完了 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 →
yfaming's avatar
yfaming 1 month ago
看完了 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 →
yfaming's avatar
yfaming 1 month ago
刷完 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 →
yfaming's avatar
yfaming 1 month ago
完成第 7 章 Evaluating Expressions。 这一章篇幅较短,完成之后,就得到一个支持加减乘除、字符串拼接和逻辑判断的最简版 Lox 解释器了,大体上相当于一个计算器。求值仍然借助 visitor 模式,对表达式树进行后序遍历。本身算是简单。 至此,Lox 解释器已经初具雏形。scanner 已经全部完成,后续不需要改动。parser 及 evaluation 过程,只支持了较简单的表达式。后续章节将不断扩充语言,支持语句、控制流、函数、类与继承等等。 View quoted note →
yfaming's avatar
yfaming 1 month ago
刷完了第 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 →
yfaming's avatar
yfaming 1 month ago
狂推神作 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。 剩下的,就是自由发挥的天地了。😎