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 10 hours ago
#DeFi实盘 2026-09-06 当前市值 193.78,净值 0.9457。本周 LP 收益 0.4。 BTC price = 79699;SOL price = 106.13。 # 本周概况 本周价格和上周相比,变化不大。但交易没上周活跃,导致 LP 收益下降,算是回到了正常水平。 # 杂感 Solana 这周推进了一个改动,SIMD-0437:Incremental Rent Reduction。计划分阶段把账户的 rent-exempt minimum 降低 90%。09-03 已经上线了第一阶段。 简单地说,Solana 中,状态和信息是保存在「账户」里面的。比如说,保存 SOL 余额需要一个账户,保存 USDC 余额需要一个账户,保存 USDT 余额需要另一个账户。我们拥有的每一种币,都有一个单独的账户。如果使用了智能合约(Solana 中称为「程序」),也常常需要创建新的账户。 每账户里最低必须有一定的 SOL,否则账户无法创建。所需的最低 SOL 数量,即 rent-exempt minimum,由一个公式计算,与账户的大小(即所占用的字节数)有关。 SIMD-0437 这个提案,就是修改计算公式,使得我们的账户占用更少的 SOL。 Raydium 上线了一个页面,在这里我们可以回收多占用的 SOL。 image
yfaming's avatar
yfaming 11 hours ago
看完了 Crafting Interpreters 第 25 章,Garbage Collection。 这一章实现了垃圾回收 Garbage Collection,使用的是简单的 Mark-Sweep 算法。 正如其名字所示,mark-sweep 的工作分两个阶段。 - mark 标记。从 root 开始,遍历或追踪 root 引用的所有对象。这是对所有可达对象的经典图遍历。 - sweep 清理。标记阶段完成后,堆中所有可达对象都已被标记。这意味着任何未标记的对象都是不可达的,可以进行回收。我们遍历所有未标记的对象,并逐一释放它们。 root 根。 root 是指虚拟机可以直接访问的任何对象(无需通过其他对象的引用)。 大多数根是全局变量或位于栈上,但还有其他一些地方。就 clox 来说,包括: - vm 操作数栈上的值、调用栈上的值、全局变量 - compiler 使用的值 GC 的时机。现有文献对于如何进行 GC,没有给出好的答案。需要在 throughput 和 latency 之间权衡。 clox 是在内存的分配和释放函数 `reallocate` 中执行 gc 动作的。它采用了常见且简单的策略,当活动内存量的增加,我们会降低 gc 频率,以避免因重新遍历不断增长的活动对象堆而牺牲 thoughput。而当活动内存量的减少,我们会提高 gc 频率,以降低 latency。 有一类 gc bug 需要注意。clox 的 gc 可以遍历 VM 的操作数栈和调用栈,但无法访问 C 栈。如果 C 栈中有一个指针类型的局部变量,它指向的对象可能会被 gc 认为无法触达,从而错误地将其释放。一个办法是,把这个指针引用的对象放到操作数栈里面,这样,mark 阶段就会被识别为 root 从而不会被错误释放。 最后需要强调一下,clox 中所有需要在堆上分配的对象,都是由 `Obj` struct 表示,并由 `allocateObject` 完成分配。而且,`Obj` 对象形成 intrusive 链表,即 `VM.objects`。而内存的释放由 gc 负责,在 mark 阶段完成后,由 sweep 函数负责释放,它会释放内存并更新 `VM.objects`。这些基础设施,是 gc 算法的基础。 View quoted note →
yfaming's avatar
yfaming yesterday
**Closures are poor man's objects and vice versa.** 很有意思的说法。 Object 可以看作是状态 + 方法的集合。 Closure 可以捕获外层变量从而拥有状态,而本身就是方法。 在这个意义上,它们有很多相通之处。
yfaming's avatar
yfaming yesterday
本书作者似乎非常推崇 Lua 的实现,clox 也多处借鉴了 Lua 的设计,包括函数调用和 closure。 作者还特别提到,The Implementation of Lua 5.0 论文,是他最喜欢的论文之一。 今天简单浏览了一下这篇论文,有一种熟悉的感觉。 第 3 节 The Representation of Values,所用的 tagged union 技术,clox 也用上了。 第 5 节 Functions and Closures,更不用说,clox 就是借鉴了这一节所描述的 upvalue 技术。 另外注意到,Lua 与 clox 一样,也是手写 scanner 和 recursive descent parser。 当然,clox 与 Lua 还是有很多不同之处的。 Lua 是 register-base VM,而 clox 是 stack-based 的。Lua 支持 coroutine 但 clox 不支持,等等。 作为小七的嵌入式语言,Lua 5.0 在简洁性方面确实很极致。扣除空行和注释后只有 1.1w 行代码,而 Lua 虚拟机也只有 35 条指令。 https://www.lua.org/doc/jucs05.pdf View quoted note →
yfaming's avatar
yfaming yesterday
看完了 Crafting Interpreters 第 25 章,Closures。 本章支持了闭包。 所谓闭包,就是函数捕获了它的外层函数中定义的局部变量。即,closure = 函数 + 捕获的外层变量。 clox 需要解决以下问题。 第一,目前 compiler resolve 变量时,要么 resolve 为局部变量,要么 resolve 为全局变量。需要增加第三种情况,resolve 为外层局部变量。clox 借用 Lua 的术语,称为 upvalue。 第二,被捕获的变量的生命周期问题。目前局部变量定义在栈上,函数调用结束时会被清理掉。但是,有了 closure,被捕获的局部变量生命周期可能超越函数调用,需要将局部变量转移到堆上。 第三,由于 closure,函数也具有了运行时特征。一个函数需要捕获哪些外层变量,在编译期已经确定;但具体捕获的是哪一次函数调用产生的变量实例,则要到运行时时才能确定。同一个函数在不同场景下,捕获的变量是不同的。我们需要在运行时表示函数和捕获的变量(aka closure)。 我们从第三个问题入手,定义 `ObjClosure` struct,它包含 ObjFunction,以及表示所捕获的变量的 upvalue 数组。并且,将 CallFrame 里的 ObjFunction 替换为 ObjClosure,以进行统一。 然后解决第一个问题。通过 `Compiler.enclosing`,我们可以向外层函数层层查找,为此增加 `resolveUpvalue` 函数,并在 resolve 过程中,对被捕获的变量进行标记。 当一个函数编译完成时,我们就知道它捕获了哪些变量了。在函数定义的末尾,我们发出 `OP_CLOSURE` 指令,以及捕获的变量的信息。运行时 `OP_CLOSURE` 会把 ObjFunction 组装为 ObjClosure,并创建 upvalue 实例将捕获的变量收集到 `ObjClosure.upvalues` 数组中。我们还添加了 `OP_GET_UPVALUE`、`OP_SET_UPVALUE` 指令,用于读写捕获的变量。具体地,是通过 `ObjClosure.upvalues` 数组进行读写。 需要注意的是 clox 是 single-pass 的,它一边 parse 一边 codegen,不会生成 ast。这意味着,外层函数声明局部变量时,无法知道这个局部变量是否会被捕获。因此,它只能像对待普通的局部变量那样,将它放在栈上。但是,当某个 local 离开其作用域时,此时因为嵌套的内层函数全部编译完成了,被捕获的变量也被标记了,所以当某个 local 离开其作用域时,我们已经知道哪个局部变量被捕获了,可以据此分别处理了。 局部变量离开作用域时,compiler 之前会发出 `OP_POP` 指令,以清理栈。对于被捕获的局部变量,则改用 `OP_CLOSE_UPVALUE` 指令,把它转移到堆上。这就是第二个问题的范畴了。 但是,在解决第二个问题时,我们要注意 upvalue 的共享问题。 一个局部变量,可以被多个内层函数捕获。其中一个函数修改了它的值,对其他函数是可见的。 这意味着,upvalue 是共享的。为此,我们将 open upvalue (即仍然指向栈上变量的 upvalue) 做成 intrusive 链表,由 vm 持有。 `ObjClosure` 则持有 upvalue 的指针。执行 `OP_CLOSE_UPVALUE` 指令时,栈顶即为局部变量的值,我们将它赋值对应的 upvalue struct 字段(`closed`)。由 upvalue 本身就是分配在堆上的,所以这就完成了将局部变量从栈上转移到堆上的工作。 另外,我们看到,upvalue 在堆上分配后,从未被释放。这就是下一章 garbage collection 的工作了。 View quoted note →
yfaming's avatar
yfaming 6 days ago
看完了 Crafting Interpreters 第 24 章,Calls and Functions。 这一章为 clox 增加了函数支持。 函数的复杂度在于,它本身也是一种控制流机制。调用函数时,VM 要暂停当前指令,跳去执行被调用函数的指令,调用完成后,又要跳回来继续执行函数调用之后的指令。我们需要保存并恢复函数调用的上下文。 第 22 章实现局部变量时(那时还没有函数),我们发现,可以直接用栈的 index 来表示局部变量。 有了函数,这一点也需要修改。我们不能像之前那样用「绝对」的 index 表示局部变量,但可以用相对于调用上下文的「相对」index 表示局部变量。 在 Lox 中,函数是值。我们需要为函数定义一个 Object 类型,`ObjFunction`,它包含函数的元信息,比如名字,参数个数(arity),字节码指令。为了统一处理,clox 将 top-level 代码也编译为隐式的函数。从而 compiler 一次编译得到一个函数。 函数调用的上下文信息,定义为 call frame `CallFrame`。多个函数相互调用时,CallFrame 就形成一个栈,调用栈 call stack。虚拟机增加了一个字段,表示调用栈 `VM.frames`,它表示所有正在执行的函数。 调用函数时,形如 `sum(a, b, c)`。compiler parsing 时依次看到的是函数名和 N 个参数。vm 依次对它们求值,并放到栈上,然后通过 `OP_CALL <argCount>` 指令发起调用。`OP_CALL` 从栈上拿到被调用函数对象,从而拿到需要执行的字节码指令。然后从栈上拿到 N 个参数,作为函数参数。接着,创建一个 CallFrame 作为新的活动 frame,解释循环改为从它的 `ip` 读取指令,于是控制流自然转移到被调函数的第一条字节码。 函数的返回,则使用之前就有的 `OP_RETURN` 指令。 函数返回时,将返回值入栈。如果没有返回值,则返回值为 nil。 执行 `OP_RETURN` 时,先从栈顶 pop 出返回值。 然后根据 CallFrame 里的信息,重置栈顶指针,清除被调函数 callee 所使用的栈上空间。 然后将返回值入栈。 最后,清理调用栈,让调用方的 CallFrame 成为活动 frame,从此使控制回到调用方,恢复执行函数调用的下一条指令。 这样,就完成了函数调用和返回的逻辑了。 Lox 函数的代码,可能压根就没有使用 return 语句,也可能只有光秃秃的 return,没有返回值。compiler 会保证,这两种情况下,返回值为 nil。compiler 在每个函数结束的时候,生成 `OP_NIL OP_RETURN` 指令。没有使用 return 语句的函数,最终会执行这两条指令,从而返回 nil。对于光秃秃的 return 语句,编译时先生成 `OP_NIL` 指令,然后才是 `OP_RETURN` 指令,从而仍然返回 nil。 原生函数 为了支持原生函数,增加了一种 Object 类型,`ObjNative`。调用规则与普通函数相同,先将 ObjNative 对象入栈,然后 N 个参数依次入栈,然后用 `OP_CALL <argCount>` 发起调用。相同的调用规则,可以降低实现成本。执行 native 函数时,不会使用 Lox 的 call stack。clox 也提供了定义 native 函数的接口,`defineNative`。 View quoted note →
yfaming's avatar
yfaming 1 week ago
看完了 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 表达式留在栈上的值,避免违反栈的使用原则。 View quoted note →
yfaming's avatar
yfaming 1 week ago
看完了 Crafting Interpreters 第 22 章,Local Variables。 这一章实现了语句块和局部变量。 在 clox 中,局部变量是直接定义在栈上面,并且通过 index 访问。相比于 jlox 将局部变量定义在 Environment 里,并且通过 Environment 链查找局部变量,clox 的性能无疑要好很多。 为什么局部变量可以定义在栈上?我们可以从 clox 中栈的使用原则开始: - 表达式:求值时,产生一个值,并放到栈顶。 - 一般的语句(大部分语句,如 exprStmt、printStmt、block):执行前后,栈里的值的数量无变化。 - 局部变量声明语句,执行后栈顶增加一个值,它就是局部变量的值。 比如,假定声明局部变量的语句为 `var a = <expr>`,`<expr>` 产生的值保留在栈顶,栈顶的 index 即对应这个局部变量。根据上述原则,这个 index 对应的 stack 元素,将不会被覆盖。因此,后续使用局部变量时,可以用这个 index 表示。这样,定义局部变量时,其实不需要用专门的指令。 具体做法,compiler 需要维护作用域深度 scopeDepth 和定义的局部变量数组 locals。 编译时,进入和退出作用域时,需要增减 scopeDepth。 在局部作用域里,遇到变量声明语句,则在 locals 数组里记录变量的名字。而退出作用域时,则从 locals 数组里清理掉这个作用域里声明的局部变量,并且发出同样数量的 `OP_POP` 指令,将局部变量也从栈上清理掉。 如果遇到变量访问,则从 locals 数组尾部开始进行变量查找。如果找到则是局部变量,否则是全局变量。我们还要区分读、写,如果是赋值则为写,否则为读。据此,我们就可以区分是全局/局部变量的读/写了,并据此生成字节码指令。 读写局部变量的指令是 `OP_GET_LOCAL` 和 `OP_SET_LOCAL`。它们后面都跟一个 slot 参数,用于指令访问哪个局部变量。slot 就是栈的 index。 运行时就比较简单了。 `OP_GET_LOCAL slot` 将栈上 index 为 slot 的值放到栈顶。这个看似有点多余,因为局部变量已经在栈上了。但只能这样,因为 stack-based vm 的指令只查看栈顶部的数据。 `OP_SET_LOCAL slot` 则用栈顶元素栈覆盖栈上 index 为 slot 的元素,以实现赋值。 View quoted note →
yfaming's avatar
yfaming 1 week ago
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 1 week ago
看完了 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 weeks 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 2 weeks 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 2 weeks ago
lno1pgghjenpd45kue6qvdhkjmn0wvhxjmckyypp998l7ktwf9ad9ypv6hcevulfqgy48kgxyhtgcghfrdg6ghqr95c
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 →