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 3 hours ago
看完了 Crafting Interpreters 第 21 章,Global Variables。 本章支持了全局变量。 变量是通过变量声明语句引入的,为了支持变量,就得支持语句。本章支持了 exprStmt、printStmt、varDecl。 vm 里增加了 `globals` hash table,用于保存全局变量。增加了 3 条字节码指令,`OP_DEFINE_GLOBAL`、`OP_GET_GLOBAL` 和 `OP_SET_GLOBAL`,用于全局变量的定义、读和写。 clox 编译时生成字节码指令,运行时执行字节码指令。我们需要关注,编译时和运行时是如何相互配合的,尤其是栈的状态。 以全局变量的声明语句为例。 编译时,遇到全局变量声明语句 `var abc = <expr>;` 将变量名放到 constants 数组里面,拿到对应 index。 然后生成对 `=` 右边的表达式求值的指令。 再生成 2 个字节的字节码指令 `OP_DEFINE_GLOBAL index`,定义全局变量。 最终指令序列类似 `<eval expr> OP_DEFINE_GLOBAL index`。 而运行时,先执行 `<eval expr>`,即对 `=` 右边的表达式值的求值指令,并将结果放到栈顶。 接下来执行 `OP_DEFINE_GLOBAL index`。先根据 index 从 constants 数组里拿到全局变量的名字。再从栈顶拿到值,并在 `vm.globals` hash table 中为全局变量设置值。 `OP_GET_GLOBAL` 和 `OP_SET_GLOBAL` 指令也是类似。 赋值语句的 parsing 容易出问题。clox 是 single-pass 的 compiler,没有 AST,而且只能 look-ahead 一个 token,一边 parse 一边 codegen。但是,也要把 context 保存下来,比如 C 调用栈,及其他状态里面,并据此延迟 codegen。即,推迟产生不可逆的结果,直到一个 token 的 lookahead 足以消除歧义。 View quoted note →
yfaming's avatar
yfaming yesterday
SOTA 大模型的网络安全能力太强悍,最近我看到两个从事 crypto swap 业务的网站被迫停业,boltz.exchange 和 atomiq.exchange。 通知见 。 它们都提到,作为小公司,无法抵挡 AI-assisted attack。 Boltz 2026-08-12 宣布彻底投降,5 个个人的创业公司根本无法应对,而且已经有攻击成功了。好在用户资金未受损失。 Boltz 把公司卖了,收购方将继续以 Boltz 品牌运营。但创始人全部退出。 我感觉,Boltz 应该遭受了很大损失,最后只剩下 Boltz 品牌还有些价值,最后变现一把。 不披露损失金额,只是为了保护品牌,避免恐慌。 Boltz 08-03 宣布暂停服务,到 08-12 就彻底投降。可以猜想,短短一周内发生的事件挺惊心动魄的。 atomiq.exchange 2026-08-20 宣布遭受攻击,暂停服务。 没有提到是否有资金损失,但强调了用户资金没受影响。 看来还没有服气。 在我看来,atomiq.exchange 的业务模式不对,天然会被对手方套利。 攻击方即便不是为了攻入系统盗币,仅仅套利就可以让它不断失血。 这里我要炫耀一下,在 2024 年我刚知道 atomiq.exchange 时,就意识到它存在这个根本缺陷了。 关键词是,free options。 Boltz 只做 BTC on-chain 和闪电网络之间的兑换,不做跨币种兑换,也是同样原因。 我怀疑,atomiq.exchange 创始人其实清楚问题所在,毕竟也算专业人士。 只是轻视了这个问题。 它的文档列过聘请第三方安全公司做的代码审计报告,也只字不提 free options。我感觉是在故意避开。 对于 atomiq.exchange 的未来,我很不乐观。 而这一切的开端,是 2026 年 4 月 Anthropic 发布的 Mythos。此后各家发布的大模型,在网络安全能力方面,似乎都越来越强大了。 据说,现在开源项目都在排除打补丁。7 月底 Linux 一口气发布了 440 个 CVE 安全漏洞编号,打破了历史记录。 现在,我们正处于新一轮再平衡中,不知要经过多久,才能达成新的均衡,形成新的安全最佳实践。 而在此之前,项目方都是脆弱的。 加密货币、DeFi 什么的,都是最佳攻击目标。毕竟,得手就等到拿到钱。 我现在最担心的是 DeFi。 相信攻击活动正在没日没夜地进行中。
yfaming's avatar
yfaming yesterday
看完了 Crafting Interpreters 第 20 章,Hash Tables。 这一章仍然在构建基础设施,Hash Table。 用一句话总结,本章实现的 hash table,hash function 采用 FNV-1a,处理哈希冲突采用 open addressing 方法,删除时采用 tombstone 标记。 最后,借助 Hash Table 实现了 compiler/interpreter 非常常用的技术,string interning。 View quoted note →
yfaming's avatar
yfaming 2 days ago
#DeFi实盘 2026-08-23 当前市值 168.43,净值 0.822。本周 LP 收益 1.14。 BTC price = 77186;SOL price = 94.97。 # 本周概况 这周终于雄起了! 大饼二饼三饼价格通通猛涨 20%+,交易量也狂增,推动本周 LP 收益增长 5 倍。 # 杂感 随着时间的流逝,LP 收益的作用逐渐体现出来了。 实盘以来的 LP 收益总额达到了 18.51,相当于 9%。正是在 LP 收益的保护之下,虽然今年 SOL 价格下跌 24%,但净值只下跌了 18%。 不过,实盘自带一定杠杆。 所以,截至上周末,今年 SOL 价格下跌 40%,净值下跌 39%。LP 收益被杠杆放大的亏损抵消了。 不过,随着时间的拉长,LP 收益的作用将越来越显著。 image
yfaming's avatar
yfaming 3 days ago
看完了 Crafting Interpreters 第 19 章,Strings。 这一章实现了 String 并让 + 号支持了字符串拼接。 这个功能看起来非常简单,但其实是 clox 的一个大的升级。在本章之前,运行时栈支持的数据类型 `Value` 只支持三个类型,nil、bool、number,它们大小固定且比较小。但是,我们需要支持其他类型的值,它们大小不固定,占用空间可能会很大。它们更适合分配在堆上。 为此,给 Value 扩充一个 variant,Object。 Object (`Obj`) 统一用于保存比较大,或者大小不固定的值,它占用的内存分配在堆上。`Value` 里只保留一个指针。Lox 里的 string、实例、函数等等,都要用 Obj 表示。 定义 `Obj` struct 时,作者使用了 C 编程的另一种技法,作者称为 struct inheritance。 先定义一个"基类” struct `Obj`,里面记录公共的信息,比如类型信息。 然后,每个具体的 Obj 类型,定义一个 struct,比如 `ObjString` 等,它的第一个字段类型为 `Obj`。其他字段用于保存这个类型独有的信息。 C 语言标准规定,struct 的字段在内存中的顺序,应当与声明的顺序一致。而且,当 struct 嵌套,即以 struct 作为另一 struct 的字段时,内部结构体的字段会直接展开到内存中。 这意味着,ObjString 的前面那部分内存,和 Obj 的是完全一样的。所以,我们可以将 struct 的指针安全地转换为指向其第一个字段的指针,反之亦然。 最后,关于内存的释放。 Lox 是有 GC 功能的。作者的经验是,实现需要 GC 的语言时,要尽早实现 GC。后面再加会非常麻烦。不过,本书直到 26 章才实现 GC,在此之前,只是进行了一些基础设施构建工作。 clox 从一开始就比较注重内存管理,memory.{c,h} 里面,把内存的分配和释放,都统一到了自定义的 reallocate 函数。早早收口,后续添加 GC 的成本也比较低。 具体到 Object 上,使用了 intrusive list,侵入式链表。所以的 Object 组成一个链表,在 vm 退出时,遍历链表,统一进行内存释放操作。这是在实现 GC 前的低配操作。 对于普通的链表,容器知道数据,但数据不知道容器。即,链表的节点有一个字段用于容纳数据,但数据不知道其所属的链表。 而侵入式链表反过来,数据知道容器。即,数据 struct 同时就是链表节点,它有指针(prev/next)指向其他 struct 节点。 由于侵入式链表中,链接结构存在于数据对象中,所以,一个节点只进行一次内存分配即可。而普通链表,则需要两次内存分配,链表节点一次,数据对象一次。侵入式链表会侵入业务类型,一个 struct 所属的链表是固定的。 View quoted note →
yfaming's avatar
yfaming 4 days ago
lno1pgghjenpd45kue6qvdhkjmn0wvhxjmckyypp998l7ktwf9ad9ypv6hcevulfqgy48kgxyhtgcghfrdg6ghqr95c
yfaming's avatar
yfaming 4 days 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 6 days 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 1 week 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 1 week ago
#DeFi实盘 2026-08-16 当前市值 124.4,净值 0.6071。本周 LP 收益 0.19。 BTC price = 62957;SOL price = 75.15。 # 本周概况 还是很冷清。 # 杂感 《一九四二》里说,人饿的时候不想说话。😂 image
yfaming's avatar
yfaming 1 week 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 1 week 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 1 week 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 1 week 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 2 weeks 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 2 weeks 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 3 weeks 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 3 weeks 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 →