元数据
用Go语言自制编译器
- 书名: 用Go语言自制编译器
- 作者: 索斯藤·鲍尔
- 简介: 本书是《用Go语言自制解释器》的续篇。在自制解释器时,你已经为Monkey语言实现了类C语法、变量绑定、基本数据类型、算术运算、内置函数、闭包等特性。是时候让Monkey继续成长了!在本书中,Monkey将继续“进化”,并最终成长为成熟的程序设计语言。在已有词法分析器、语法分析器和抽象语法树的基础上,你将为Monkey语言定义字节码指令,指定操作数,编写反汇编程序,构建执行字节码的虚拟机。通过动手学习,你将能够体验从无到有实现类C语言的乐趣。
- 出版时间 2022-06-01 00:00:00
- ISBN: 9787115591104
- 分类: 计算机-编程设计
- 出版社: 人民邮电出版社有限公司
高亮划线
1.2 虚拟机与物理机
-
📌 什么是“字”?它是计算机内存中的最小可寻址区域 ^3300018888-9-2755-2778
- ⏱ 2023-07-18 17:59:18
-
📌 通常每个寄存器只能存储一个字。 ^3300018888-9-7002-7017
- ⏱ 2023-07-18 20:52:03
-
📌 栈式虚拟机还是寄存器式虚拟机 ^3300018888-9-9977-10017
- ⏱ 2023-07-18 21:10:25
-
📌 虚拟机是领域特定的。 ^3300018888-9-12260-12270
- ⏱ 2023-07-18 21:20:29
-
📌 虚拟机执行字节码。 ^3300018888-9-13722-13731
- ⏱ 2023-07-18 21:22:22
-
📌 之所以叫字节码,是因为所有指令的操作码大小均为一字节。“操作码”是指令的“操作”部分。前文提到的PUSH就是一种操作码,不过在我们的示例代码中,它是一个多字节操作码,不是一字节的。在正常的实现中,PUSH只是一个引用操作码的名称,该操作码本身是一字节宽。 ^3300018888-9-13759-13915
- ⏱ 2023-07-18 21:24:07
-
📌 小端编码的意思是原始数据中的低位放在最前面并存储在最低的内存中。 ^3300018888-9-14773-14831
- ⏱ 2023-07-18 21:26:44
-
📌 汇编语言是字节码的可读版本,包含助记符和可读操作数,汇编器能将其转换为二进制字节码。 ^3300018888-9-15315-15357
- ⏱ 2023-07-18 22:18:26
-
📌 字节码是一种领域特定的语言。它是定制虚拟机的定制机器语言。 ^3300018888-9-15548-15577
- ⏱ 2023-07-18 22:20:22
3.1 栈清理
- 📌 第一,定义一个新的操作码,让虚拟机将栈顶元素弹出;第二,每一个表达式语句之后都执行这个新的操作码。 ^3300018888-16-1010-1059
- ⏱ 2023-07-22 08:05:32
4.2 编译条件语句
-
📌 先用一个虚假的偏移量,后面再来修改。 ^3300018888-23-5252-5270
- ⏱ 2023-07-22 11:08:33
-
📌 我们只想删除node.Consequence中的最后一条OpPop指令 ^3300018888-23-8145-8206
- ⏱ 2023-07-22 11:15:24
-
📌 首先更改编译器,以追踪发出的最后两条指令,包括追踪它们的操作码和它们被发出的位置。 ^3300018888-23-8464-8505
- ⏱ 2023-07-22 11:16:13
-
📌 我们会在编译node.Consequence之后修改OpJumpNotTruthy的操作数。 ^3300018888-23-11773-11819
- ⏱ 2023-07-22 11:24:57
-
📌 这个操作被称为回填 ^3300018888-23-11881-11890
- ⏱ 2023-07-22 11:25:28
4.4 欢迎回来,Null值
- 📌 导致当前崩溃的原因是条件语句之后发出的OpPop指令。由于这些指令并没产生任何值,因此当虚拟机强制从栈中弹出数据的时候就会崩溃。现在是时候将vm.Null压栈,改变这一情况了。 ^3300018888-25-3163-3251
- ⏱ 2023-07-22 12:27:29
5.1 计划
-
📌 在编译这段代码时,每遇到一个标识符,都会单独赋予它一个新的数字。如果标识符是已经遇见过的,就复用之前赋予的数字。 ^3300018888-27-1146-1202
- ⏱ 2023-07-22 17:54:53
-
📌 在虚拟机中,我们使用切片完成全局绑定的创建和检索。这个切片被称为“全局存储”,而且我们将使用OpSetGlobal指令和OpGetGlobal指令的操作数作为索引。在执行OpSetGlobal指令时,我们需要读取操作数,将栈顶元素弹出并将其保存到以操作数为索引的全局存储中。在执行OpGetGlobal指令时,我们需要以操作数为索引从全局存储中读取值并将其压栈。 ^3300018888-27-1818-2028
- ⏱ 2023-07-22 17:49:46
5.2 编译绑定
- 📌 符号表是解释器和编译器中用于将标识符与信息相关联的数据结构。 ^3300018888-28-4165-4202
- ⏱ 2023-07-22 18:03:51
5.3 在虚拟机中支持全局变量
- 📌 编译器处理完成之后,需要更新constants的引用。这是必要的,因为编译器内部使用append,这会导致之前分配的常量切片与后续的不一致。 ^3300018888-29-6275-6345
- ⏱ 2023-07-22 19:08:07
7.1 一个简单的函数
-
📌 ·object.CompiledFunction用来保存编译函数的指令,并将它们以常量的形式作为字节码的一部分从编译器传递给虚拟机。·code.OpCall用来让虚拟机开始执行位于栈顶部的*object.CompiledFunction。·code.OpReturnValue用来让虚拟机将栈顶的值返回到调用上下文并在此恢复执行。·code.OpReturn,与code.OpReturnValue类似,不同之处在于没有显式返回值,而是隐式返回vm.Null。 ^3300018888-36-6736-7053
- ⏱ 2023-07-22 23:55:47
-
📌 但是,如果直接用已有的*ast.FunctionLiteral的主体调用编译器的Compile方法,最终会一团糟:结果指令最终会与主程序的指令纠缠在一起。解决方案是在编译器中引入作用域。 ^3300018888-36-10583-10676
- ⏱ 2023-07-23 00:05:06
-
📌 解决这个问题的恰当时间是在函数体编译之后和离开作用域之前。那时,我们仍然可以访问刚刚发出的指令,进而可以检查最后一条指令是否是OpPop指令,并在必要时将其转换为OpReturnValue。 ^3300018888-36-23598-23693
- ⏱ 2023-07-23 00:34:39
-
📌 帧是调用帧或者栈帧的简称,指保存与执行相关的信息的数据结构。在编译器或解释器术语中,这有时也称为活动记录。在物理机上,帧并不独立于栈存在,而是栈的特定部分。它是存储返回地址、当前函数的参数及其局部变量的地方。由于它在栈中,因此帧在函数执行结束后很容易被弹栈。 ^3300018888-36-33526-33788
- ⏱ 2023-07-23 01:16:28
-
📌 在虚拟机上,不必使用栈来存储帧,因为此处不受标准化调用约定或其他真实的内容约束 ^3300018888-36-33899-33938
- ⏱ 2023-07-23 01:19:02
7.2 局部绑定
-
📌 基指针 ^3300018888-37-33534-33537
- ⏱ 2023-07-24 09:29:54
-
📌 帧指针 ^3300018888-37-33628-33631
- ⏱ 2023-07-24 09:29:49
7.3 参数
- 📌 将参数视为局部绑定 ^3300018888-38-9083-9092
- ⏱ 2023-07-24 11:47:25
8.2 做出改变:计划
-
📌 内置函数的定义既不在全局作用域内,也不在局部作用域内。它们在自己的作用域内。我们需要将该作用域引入编译器及其符号表,以便正确解析对内置函数的引用。 ^3300018888-41-740-813
- ⏱ 2023-07-24 14:26:58
-
📌 当编译器(在符号表的帮助下)检测到对内置函数的引用时,它将发出OpGetBuiltin指令。此指令中的操作数是object.Builtins中所引用函数的索引。 ^3300018888-41-964-1044
- ⏱ 2023-07-24 14:27:32
9.1 问题
- 📌 在旧的解释器中,将函数字面量转换为object.Function和跳出环境(在object.Function上设置Env字段)同时发生,甚至发生在同一代码块中。在新的Monkey实现中,这不仅发生在不同的时间,而且发生在不同的代码包中:在编译器中编译函数字面量,在虚拟机中构建环境。 ^3300018888-45-664-886
- ⏱ 2023-07-24 16:09:40
9.2 计划
-
📌 自由变量 ^3300018888-46-845-849
- ⏱ 2023-07-24 16:08:24
-
📌 自由变量既不是在当前局部作用域中定义的变量,也不是当前函数的参数。由于不受当前作用域的约束,因此它们是自由的。另一种定义解释是,自由变量是那些在局部使用但在封闭作用域内定义的变量。基于编译器和虚拟机实现闭包将围绕自由变量展开。 ^3300018888-46-1107-1256
- ⏱ 2023-07-24 16:16:19
-
📌 主要步骤是:在编译函数时检测对自由变量的引用,将引用的值放到栈中,将值和编译后的函数合并到一个闭包中,并将其留在栈中,以便随后在那里调用它。 ^3300018888-46-2386-2456
- ⏱ 2023-07-24 16:20:52
9.3 将一切视为闭包
-
📌 OpClosure有两个操作数 ^3300018888-47-1495-1536
- ⏱ 2023-07-24 16:29:39
-
📌 第一个操作数是常量索引。它用于指定在常量池中的哪个位置找到要转换为闭包的*object.CompiledFunction。 ^3300018888-47-1590-1677
- ⏱ 2023-07-24 16:30:04
-
📌 第二个操作数用于指定栈中有多少自由变量需要转移到即将创建的闭包中。 ^3300018888-47-1801-1860
- ⏱ 2023-07-24 16:30:35
-
📌 在所有函数加载到栈中的位置上,将OpConstant更改为OpClosure ^3300018888-47-4618-4656
- ⏱ 2023-07-24 16:43:15
9.4 编译和解析自由变量
-
📌 在外部函数中,应该使用OpGetLocal将a压栈,不过函数本身从未引用它。但由于它已经被内部函数引用,因此必须在虚拟机执行下一条指令OpClosure之前将a压栈。 ^3300018888-48-2240-2323
- ⏱ 2023-07-24 17:54:01
-
📌 FreeSymbols应包含封闭作用域的原始符号 ^3300018888-48-11657-11733
- ⏱ 2023-07-24 18:20:57
-
📌 需要知道在封闭作用域中如何访问这些符号 ^3300018888-48-11967-11986
- ⏱ 2023-07-24 18:23:01
9.6 递归闭包
-
📌 就目前情况而言,无法知道一个引用是否是自引用,因为我们从未捕获函数绑定的名称。但是其实可以捕获:在语法分析器中,可以判断let语句是否将函数字面量绑定到某个名称,如果是,则将绑定的名称保存到函数字面量中。 ^3300018888-50-12158-12289
- ⏱ 2023-07-25 08:45:19
-
📌 FunctionScope ^3300018888-50-14798-14811
- ⏱ 2023-07-25 09:16:04
-
📌 当前正在编译的函数名 ^3300018888-50-14835-14845
- ⏱ 2023-07-25 09:15:55
-
📌 当对一个名称进行语法分析并使用FunctionScope返回一个符号时,我们就知道它是当前的函数名,因此它是自引用。 ^3300018888-50-14846-14904
- ⏱ 2023-07-25 09:09:41
