├── contents ├── 1 │ ├── 1.1.md │ ├── 1.2.md │ ├── 1.5.md │ ├── 1.4.md │ └── 1.3.md ├── 2 │ ├── 解析.png │ ├── AST.png │ ├── 解析2.png │ ├── 解析3.png │ ├── 解析4.png │ ├── 解析5.png │ ├── 解析6.png │ ├── AST节点.png │ ├── 2.3.md │ ├── 2.2.md │ ├── 2.1.md │ ├── 2.5.md │ ├── 2.9.md │ ├── 2.7.md │ └── 2.4.md ├── 3 │ ├── 3.1.md │ ├── 3.3.md │ ├── 3.2.md │ ├── 3.11.md │ ├── 3.4.md │ ├── 3.6.md │ ├── 3.7.md │ ├── 3.9.md │ ├── 3.8.md │ ├── 3.10.md │ └── 3.5.md ├── 4 │ ├── 4.1.md │ ├── 4.6.md │ ├── 4.3.md │ ├── 4.2.md │ ├── 4.5.md │ └── 4.4.md ├── Acknowledgments.md ├── Resources.md └── Introduction.md ├── .gitignore ├── cal.md ├── LICENSE └── README.md /contents/2/解析.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LixvYang/Writing-a-Interpreter-in-Go-Translation/HEAD/contents/2/解析.png -------------------------------------------------------------------------------- /contents/2/AST.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LixvYang/Writing-a-Interpreter-in-Go-Translation/HEAD/contents/2/AST.png -------------------------------------------------------------------------------- /contents/2/解析2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LixvYang/Writing-a-Interpreter-in-Go-Translation/HEAD/contents/2/解析2.png -------------------------------------------------------------------------------- /contents/2/解析3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LixvYang/Writing-a-Interpreter-in-Go-Translation/HEAD/contents/2/解析3.png -------------------------------------------------------------------------------- /contents/2/解析4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LixvYang/Writing-a-Interpreter-in-Go-Translation/HEAD/contents/2/解析4.png -------------------------------------------------------------------------------- /contents/2/解析5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LixvYang/Writing-a-Interpreter-in-Go-Translation/HEAD/contents/2/解析5.png -------------------------------------------------------------------------------- /contents/2/解析6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LixvYang/Writing-a-Interpreter-in-Go-Translation/HEAD/contents/2/解析6.png -------------------------------------------------------------------------------- /contents/2/AST节点.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LixvYang/Writing-a-Interpreter-in-Go-Translation/HEAD/contents/2/AST节点.png -------------------------------------------------------------------------------- /contents/Acknowledgments.md: -------------------------------------------------------------------------------- 1 | 我想用这些话来表达我对妻子支持我的感谢。 2 | 3 | 她是你阅读这篇文章的原因。 4 | 5 | 如果没有她的鼓励、对我的信任、帮助以及她在早上6点听我的机械键盘敲击声的意愿,这本书就不会存在。 6 | 7 | 感谢我的朋友 Christian、Felix 和 Robin 审阅了本书的早期版本,并为我提供了宝贵的反馈 ,建议和欢呼。 8 | 9 | 你们对这本书的改进超出了你的想象。 10 | |[> 介绍](Introduction.md)| 11 | |-| -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | -------------------------------------------------------------------------------- /contents/4/4.1.md: -------------------------------------------------------------------------------- 1 | # 4.1数据类型&函数 2 | 尽管我们的解释器工作得非常好并且有一些令人兴奋的特性,比如一流的函数和闭包,我们作为 Monkey 的用户唯一可用的数据类型是整数和布尔值。 这不是特别有用,而且比我们从其他编程语言中习惯的要少得多。 在本章中,我们将改变这一点。 我们将向解释器添加新的数据类型。 3 | 4 | 这项工作的伟大之处在于它再次带我们完成了整个解释器。 我们将添加新的标记类型,修改词法分析器,扩展解析器,最后为我们的评估器和对象系统添加对数据类型的支持。 5 | 6 | 更好的是,我们要添加的数据类型已经存在于 Go 中。 这意味着我们只需要让它们在 Monkey 中可用。 我们不需要从头开始实现它们,这非常方便,因为这本书不叫“在 Go 中实现通用数据结构”,我们可以专注于我们的解释器。 7 | 8 | 除此之外,我们还将通过添加一些新功能使解释器更加强大。 当然,作为我们解释器的用户,我们可以自己定义函数,但这些函数的功能有限。 这些称为内置函数的新函数将更加强大,因为它们可以访问 Monkey 编程语言的内部工作原理。 9 | 10 | 我们要做的第一件事是添加一个我们都知道的数据类型:字符串。 几乎每种编程语言都有它,Monkey 也应该有。 11 | |[< 3.11谁来倒垃圾?](../3/3.11.md)|[> 4.2字符串](4.2.md)| 12 | |-|-| -------------------------------------------------------------------------------- /cal.md: -------------------------------------------------------------------------------- 1 | ``` 2 | % date && cal 3 | Tue Jun 22 16:51:23 CST 2021 4 | Tue Jun 22 23:35:30 CST 2021 5 | Thu Jun 24 17:39:02 CST 2021 6 | Fri Jun 25 11:21:13 CST 2021 7 | Sat Jun 26 10:04:50 CST 2021 8 | June 2021 9 | Su Mo Tu We Th Fr Sa 10 | 1 2 3 4 5 11 | 6 7 8 9 10 11 12 12 | 13 14 15 16 17 18 19 13 | 20 21 22 23 24 25 26 14 | 27 28 29 30 15 | 16 | Sun Jul 11 12:36:05 CST 2021 17 | Mon Jul 12 17:30:11 CST 2021 18 | Mon Jul 12 23:02:27 CST 2021 19 | July 2021 20 | Su Mo Tu We Th Fr Sa 21 | 1 2 3 22 | 4 5 6 7 8 9 10 23 | 11 12 13 14 15 16 17 24 | 18 19 20 21 22 23 24 25 | 25 26 27 28 29 30 31 26 | 27 | Mon Sep 6 21:49:42 CST 2021 28 | ``` -------------------------------------------------------------------------------- /contents/3/3.1.md: -------------------------------------------------------------------------------- 1 | # 3.1赋予符号意义 2 | 我们终于到了这里。评估。在REPL中的E并且是生产源代码时解释器最后要做的事情。这是让代码变得有意义的地方。没有评估器的话,一个像1+2的表达式只是表达一系列字符,tokens或者一个树的结构。这并不意味着什么。评估,当然是1+2变成3.5>1变成true,5<1变成false并且puts("Hello World!")变成了我们都知道的友好信息。 3 | 4 | 解释其中的评估器定义了编程语言是如何做解析工作的。 5 | ```go 6 | let num = -5; 7 | if (num) { 8 | retuan a; 9 | } else { 10 | return b; 11 | } 12 | ``` 13 | 这是否返回a或b取决于解释器的评估过程的决定整数5是否为真。在某些语言中它是真实的,在其他语言中我们需要使用一个表达式来产生像5!=0 14 | 这样的布尔值。 15 | 16 | 考虑以下这样: 17 | ```go 18 | let one = fn() { 19 | printLine("one"); 20 | return 1; 21 | }; 22 | 23 | let two = fn() { 24 | printLine("two"); 25 | return 2; 26 | }; 27 | 28 | add(one(), two()); 29 | ``` 30 | 这是先输出一然后输出两个还是反过来?它取决于它解释器的实现以及它的顺序计算调用表达式中的参数。 31 | 32 | 在这个章节将会由许多像这样的小选择,在这儿我们得到决定Monkey将如何工作和我们的解析器如何评估Monkey源代码的。 33 | 34 | 在我告诉你编写解析器很有趣后,你可能会持怀疑态度,但请相信我:这就是最好的部分。这是Monkey编程语言诞生的地方,源代码加快并开始呼吸。 35 | |[< 2.9RPPL](../2/2.9.md)|[> 3.2评估策略](3.2.md)| 36 | |-|-| 37 | 38 | 39 | -------------------------------------------------------------------------------- /contents/2/2.3.md: -------------------------------------------------------------------------------- 1 | # 2.3为Monkey语言写一个解析器 2 | 当解析一个编程语言时,有两个主要的策略:自顶向下解析或自底向上解析。每种策略存在许多略不同的形式。例如,“递归下降解析”,“早期解析”或“预测解析”都是自顶向下解析的变体。 3 | 4 | 我们准备写的解析器是递归下降解析。并且尤其是,它是一个“自顶向下运算符优先级”解析器,有时也被称为“Pratt解析器”,以发明者Vaughan Pratt的名字命名。 5 | 6 | 7 | > 引用《编译原理》—— 自顶向下语法分析可以被看作是为输入串构造语法分析树的问题,它从语法分析树的根结点开始,按照先根次序深度优先地创建这棵语法分析树的各个结点。自顶向下语法分析也可以被看作寻找输入串的最左推导的过程。 8 | 9 | 我不会采用深入不同解析器的策略,因为这不是我有资格准确描述它们的地方。相反,我只想说,自顶向下和自底向上解析器的区别在于,前者从构建AST的根节点开始,然后下降,而后者相反。从上到下工作的递归下降解析器通常被推荐给新手解析,因为它密切反映了我们对AST及其构造的看法。我个人发现从根节点开始的递归方法非常好,即使在概念真正被理解之前需要编写一些代码。这是开始使用代码而不是钻研解析策略的另一个原因。 10 | 11 | 现在,当写一个我们自己的解析器,我们必须做一些权衡,是的。我们的解析器不会是有史以来最快的,我们不会有正确性的正式证明,它的错误恢复过程和错误语法的检测也不是万无一失的。如果没有对围绕解析的理论进行广泛研究,最后一个尤其难以正确。但是我们将要拥有的是一个完整的Monkey编程语言解析器,它对拓展和改进开放,易于理解并且进一步深入解析主题的良好开端,如果有人愿意的话。 12 | 13 | 我们从解析语句开始:let和return语句。当我们可以解析语句和解析器的基本结构时,我们将查看表达式以及如何解析它们(这是Vaughan Pratt发挥作用的地方)。之后我们扩展了解析器,使其能够解析Monkey编程语言的一个很大的子集。当我们进入目标时,我们为AST构建了必要结构。 14 | 15 | |[< 2.2为什么不是解析器生成器?](2.2.md)|[> 2.4解析器起步:解析LET语句](2.4.md)| 16 | |-|-| -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 lixin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /contents/1/1.1.md: -------------------------------------------------------------------------------- 1 | # 1.1词法分析 2 | 3 | 为了我们使用源代码,我们需要将其转换为更易于访问的形式。就像我们在编辑器中使用纯文本一样简单,当试图用一种编程语言将它转换成另一种编程语言时,它会变得很麻烦。因此我们需要做的是用其他更容易使用的形式表示我们的源代码。在评估之前,我们将更改源代码的表示两次: 4 | 5 | source code -> Tokens -> Abstract Syntax Tree 6 | 7 | 第一次转换,从源代码到Tokens称为词法分析。它由词法分析器(分词器/扫描器-有些使用一个词或另一个词来表示行为上的细微差别,我们在本书中可以忽略)完成。Tokens本身是小的、易于分类的数据结构,然后被提供给解析器,它进行第二次转换并将标记编程“抽象语法树”。 8 | 9 | 下面一个例子。这是给词法分析器的输入: 10 | ``` 11 | "let x = 5 + 5;" 12 | ``` 13 | 14 | 并且从词法分析器中得到的结果看起来有点像这样: 15 | ``` 16 | [ 17 | LET, 18 | IDENTIFIER("x"), 19 | EQUAL_SIGN,INTEGER(5), 20 | PLUS_SIGN,INTEGER(5), 21 | SEMICOLON 22 | ] 23 | ``` 24 | 25 | 全部的Tokens都有对应的源代码表示。在LET的情况下为"let",在PLUS_SIGN的情况下为"+",以此类推。例如像我们示例中IDENTIFIER 和 INTEGER还附加了它们所代表的具体值:5(不是"5"!)在INTEGER的情况下和IDENTTIFIER的情况下是"x"。但是究竟什么构成“Tokens”在不同的词法分析器实现之间有所不同。例如,一些词法分析器仅在解析阶段或者稍后将"5"转换成整数,而不是在构造标记时。 26 | 27 | 关于这个例子有一点需要注意:空白字符不会显示为Tokens。在我们的例子中没问题,因为空白长度在Monkey语言中并不重要。空格只是作为其他Tokens的分隔符。无所谓是否我们输入以下内容: 28 | ``` 29 | let x = 5; 30 | ``` 31 | 或者 32 | ``` 33 | let x = 5; 34 | ``` 35 | 换句话说,像Python,空白的长度很重要。那意味着词法分析器不能只**吃掉**空格和换行符。它必须将空白字符作为标记输出,以便解析器稍后可以理解它们(或者输出错误,当然,如果不够或者太多)。 36 | 37 | 一个生产就绪的词法分析器还可以将行号、列号和文件名附加到Tokens。为什么?例如,为了稍后再解析阶段输出更多有用的错误信息。它可以输出而不是"error: expected semicolon token": 38 | ``` 39 | "error: expected semicolon token. line 42, column 23, " 40 | ``` 41 | 我们将不会为此烦恼,不是因为它太复杂,而是因为它会脱离Token和词法分析器的基本简单性,使其更难以理解。 42 | 43 | **** 44 | |[< 介绍](../Introduction.md)|[> 1.2定义我们的Tokens](1.2.md)| 45 | |-|-| -------------------------------------------------------------------------------- /contents/3/3.3.md: -------------------------------------------------------------------------------- 1 | # 3.3一个Tree-Walking解释器 2 | 我们将要构建的是一个Tree-Walking解释器,我们将使用解析器为我们构建的 AST 并“即时”解释它,而无需任何预处理或编译步骤。 3 | 4 | 我们的解释器将很像经典的 Lisp 解释器。 我们将使用的设计很大程度上受到“计算机程序的结构和解释(SICP)”中介绍的解释器的启发,尤其是它对环境的使用。这并不意味着我们正在复制一个特定的解释器,并不是这样,我们宁愿使用一个你可以在很多其他地方看到的蓝图。如果你眯得足够自诩。这种特殊设计的流行的确有很好的理由:它是最简单的入门方式,易于理解和稍后的扩展。 5 | 6 | 我们实际上仅仅需要做两件事:一个tree-walking评估器和一种在我们的宿主语言Go中表示Monkey值的方法。评估器看起来很强大,但它将仅仅是一个名为“eval”的函数。它的工作是评估AST。这里是一个伪代码版本,它说明了“即使评估”和“tree-walking”在解释上下文中的含义。 7 | ```js 8 | function eval(astNode) { 9 | if (astNode is integerliteral) { 10 | return astNode.integerValue 11 | 12 | } else if (astNode is booleanLiteral) { 13 | return astNode.booleanValue 14 | 15 | } else if (astNode is infixExpression) { 16 | leftEvaluated = eval(astNode.Left) 17 | rightEvaluated = eval(astNode.Right) 18 | 19 | if astNode.Operator == "+" { 20 | return leftEvaluated + rightEvaluated 21 | } else if ast.Operator == "-" { 22 | return leftEvaluated - rightEvaluated 23 | } 24 | } 25 | } 26 | ``` 27 | 正如你看到的,`eval`是递归的。当`astNode`是`infixExpression`是true,eval调用它自己再次计算中缀表达式的左操作数和右操作数两次。这反过来可能会导致对另一个中缀表达式或整数文字或布尔值或者操作符的评估......我们已经在构建和测试AST时看到了递归。相同的概念在这里适用,只是在我们正在评估树而不是构建它。 28 | 29 | 查看这段伪代码,您可能会想象扩展此功能是多么容易。 这符合我们的优势。 我们将一点一点地构建我们自己的 Eval 函数,并在我们继续扩展我们的解释器时添加新的分支和功能。 30 | 31 | 但这段代码中最有趣的几行是 return 语句。 他们返回什么? 32 | 这里有两行将调用 eval 的返回值绑定到名称: 33 | 34 | ```js 35 | leftEvaluated = eval(astNode.Left) 36 | rightEvaluated = eval(astNode.Right) 37 | ``` 38 | 39 | eval 在这里返回什么? 返回值属于哪种类型? 这些问题的答案与“我们的解释器将拥有什么样的内部对象系统?”的答案相同。 40 | |[< 3.2评估策略](3.2.md)|[> 3.4表示对象](3.4.md)| 41 | |-|-| 42 | 43 | 44 | -------------------------------------------------------------------------------- /contents/4/4.6.md: -------------------------------------------------------------------------------- 1 | # 4.6总决赛 2 | 我们的 Monkey 解释器现在功能齐全。 它支持数学表达式、变量绑定、函数以及这些函数的应用、条件、返回语句甚至高级概念,如高阶函数和闭包。 然后是不同的数据类型:整数、布尔值、字符串、数组和哈希。 我们可以为自己感到自豪。 3 | 4 | 但是……但是……我们的解释器仍然没有通过所有编程语言测试中最基本的测试:打印一些东西。 是的,我们的 Monkey 翻译器无法与外界交流。 甚至像 Bash 和 Brainfuck 这样的编程语言流氓也设法做到了这一点。 我们必须做什么很清楚。 我们必须添加最后一个内置函数:puts。 5 | 6 | puts 将新行中的给定参数打印到 STDOUT。 它对作为参数传入的对象调用 Inspect() 方法并打印这些调用的返回值。 Inspect() 方法是 Object 接口的一部分,因此我们对象系统中的每个实体都支持它。 使用 puts 应该看起来像这样: 7 | ```js 8 | >> puts("Hello!") 9 | Hello! 10 | >> puts(1234) 11 | 1234 12 | >> puts(fn(x) { x * x }) 13 | fn(x) { 14 | (x * x) 15 | } 16 | ``` 17 | puts 是一个可变参数函数。 它接受无限数量的参数并将每个参数打印在单独的行上: 18 | ```js 19 | >> puts("hello", "world", "how", "are", "you") 20 | hello 21 | world 22 | how 23 | are 24 | you 25 | ``` 26 | 当然,puts 就是打印东西而不是产生值,所以我们需要确保它返回 NULL: 27 | ```js 28 | >> let putsReturnValue = puts("foobar"); 29 | foobar 30 | >> putsReturnValue 31 | null 32 | ``` 33 | 这也意味着我们的 REPL 将在我们期望的输出之外打印空值。 所以它看起来像这样: 34 | ```js 35 | >> puts("Hello!") 36 | Hello! 37 | null 38 | ``` 39 | 现在,这些信息和规范足以完成我们的最后一项任务。你准备好了吗? 40 | 41 | 在这里,这是本节一直在构建的内容,这是 puts 的完整、有效的实现: 42 | ```go 43 | // evaluator/builtins.go 44 | 45 | import ( 46 | "fmt" 47 | "monkey/object" 48 | "unicode/utf8" 49 | ) 50 | 51 | var builtins = map[string]*object.Builtin{ 52 | // [...] 53 | "puts": &object.Builtin{ 54 | Fn: func(args ...object.Object) object.Object { 55 | for _, arg := range args { 56 | fmt.Println(arg.Inspect()) 57 | } 58 | 59 | return NULL 60 | }, 61 | }, 62 | } 63 | ``` 64 | 有了这个,我们做到了。 我们完成了。 即使您之前对我们的小庆祝活动持谨慎态度并且对它们不屑一顾,现在也该去寻找一顶有趣的派对帽并戴上它了。 65 | 66 | 在第三章中,我们将 Monkey 编程语言带入了生活。 它开始呼吸。 通过我们的最后一次更改,我们让它说话了。 现在,Monkey 终于成为一种真正的编程语言: 67 | ```go 68 | $ go run main.go 69 | Hello mrnugget! This is the Monkey programming language! 70 | Feel free to type in commands 71 | >> puts("Hello World!") 72 | Hello World! 73 | null 74 | >> 75 | ``` 76 | |[< 4.5哈希对象](4.5.md)|[> 资源](../Resources.md)| 77 | |-|-| -------------------------------------------------------------------------------- /contents/1/1.2.md: -------------------------------------------------------------------------------- 1 | # 1.2定义我们的Tokens 2 | 我们必须要做的第一件事就是定义词法分析器输出的Tokens。我们将从几个标记定义开始,然后在拓展词法分析器时添加更多。 3 | 4 | 我们将在第一部中的词法的Monkey语言的子集如下所示: 5 | ``` 6 | let five = 5; 7 | let ten = 10; 8 | 9 | let add = fn(x,y) { 10 | x + y; 11 | }; 12 | 13 | let result = add(five,ten); 14 | ``` 15 | 让我们分析一下:这个例子中包含哪些类型的Tokens?首先,有像5和10这样的数字。这些是很明显的。然后我们有变量名称x,y,add和result。然后还有语言的这些部分不是数字,只是单词,但也没有变量名,像let和fn。当然,也有很多特殊字符:(,),{,}=,,,,;。数字只是整数,我们将这一对待它们并给他们一个单独的类型。在词法分析器或者解析器中,我们不关心数字是5还是10,我们只想知道它是否是数字。"变量名"也是如此:我们称它们为“标识符”,并且一视同仁。现在,换句话说,那些看起来像标识符但实际上不是标识符的词,因为它们是语言的一部分,被称为“关键字”。我们不会将这些组合在一起,因为无论我们是否遇到aletor afn,它们都会在解析阶段产生影响。 16 | 17 | 我们确定的最后一个类别也是如此:特殊字符。我们将分别对待它们中的每一个,因为源代码中是否有a(或者a)是一个很大的区别。让我们定义我们的Token数据结构。它需要哪些领域?正如我们刚刚看到的,我们肯定需要一个“类型”属性,例如,我们可以区分“整数”和“右括号”。并且它需要一个字段来保存Token的字面值,这样我们之后可以重用它并且"number"Tokens是a5或者a10的信息不会丢失。在一个newtoken包中,我们定义我们的Token类型和我们的TokenType类型。 18 | ```go 19 | // token/token.go 20 | package token 21 | 22 | type TokenType string 23 | 24 | type Token struct { 25 | Type TokenTyoe 26 | Literal string 27 | } 28 | ``` 29 | 我们定义TokenType类型为string。这允许我们使用多种不同的值作为Token类型,这反过来又允许我们区分不同类型的Tokens。使用字符串还有一个优点是易于调试,无需大量样板和辅助函数:我们可以只打印字符串。当然,使用一个字符串可能不会带来和使用一个`int`或者一个`byte`一样的性能,但对于本书而言,string是完美的。 30 | 31 | 正如我们刚刚看到的,Monkey语言中的不同Token类型数量有限。这意味着我们将可能定义TokenTypes为常量。在同一个文件内,我们添加这些内容: 32 | ```go 33 | const( 34 | ILLEGAL ="ILLEGAL" 35 | EOF="EOF" 36 | 37 | // Identifiers + literals 38 | IDENT ="IDENT"// add, foobar, x, y,... 39 | INT ="INT"// 1343456 40 | 41 | // Operators 42 | ASSIGN ="=" 43 | PLUS="+" 44 | 45 | // Delimiters 46 | COMMA="," 47 | SEMICOLON =";" 48 | LPAREN ="(" 49 | RPAREN =")" 50 | LBRACE ="{" 51 | RBRACE ="}" 52 | 53 | // Keywords 54 | FUNCTION ="FUNCTION" 55 | LET="LET" 56 | ) 57 | ``` 58 | 59 | 正如你所看到的两种特殊类型:ILLEGAL和EOF。我们在之前的例子中没有看到它们,但是我们仍然需要它们。ILLEGAL表示我们不知道的标记/字符,而EOF代表"文件结束"(end of file),它告诉我们的解析器稍后它可以停止了。到目前为止一切顺理。我们准备开始编写我们的词法分析器(lexer)。 60 | 61 | |[< 1.1词法分析](1.1.md)|[> 1.3词法分析器](1.3.md)| 62 | |-|-| 63 | 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [译]Writing-a-Compiler-in-Go-Translation 2 | ## 感谢作者Thorsten Ball 3 | 4 | - 译者:罹心 5 | - 本书英文名:Writing-a-Compiler-in-Go 6 | 7 | ## 目录:> 8 | - [介绍](contents/Introduction.md) 9 | - [致谢](contents/Acknowledgments.md) 10 | - [1 语法分析](https://github.com/LixvYang/Writing-a-Interpreter-in-Go-Translation/tree/main/contents/1) 11 | - [1.1](contents/1/1.1.md) 12 | - [1.2](contents/1/1.2.md) 13 | - [1.3](contents/1/1.3.md) 14 | - [1.4](contents/1/1.4.md) 15 | - [1.5](contents/1/1.5.md) 16 | - [2 解析](https://github.com/LixvYang/Writing-a-Interpreter-in-Go-Translation/tree/main/contents/2) 17 | - [2.1](contents/2/2.1.md) 18 | - [2.2](contents/2/2.2.md) 19 | - [2.3](contents/2/2.3.md) 20 | - [2.4](contents/2/2.4.md) 21 | - [2.5](contents/2/2.5.md) 22 | - [2.6](contents/2/2.6.md) 23 | - [2.7](contents/2/2.7.md) 24 | - [2.8](contents/2/2.8.md) 25 | - [2.9](contents/2/2.9.md) 26 | - [3 评估](https://github.com/LixvYang/Writing-a-Interpreter-in-Go-Translation/tree/main/contents/3) 27 | - [3.1](contents/3/3.1.md) 28 | - [3.2](contents/3/3.2.md) 29 | - [3.3](contents/3/3.3.md) 30 | - [3.4](contents/3/3.4.md) 31 | - [3.5](contents/3/3.5.md) 32 | - [3.6](contents/3/3.6.md) 33 | - [3.7](contents/3/3.7.md) 34 | - [3.8](contents/3/3.8.md) 35 | - [3.9](contents/3/3.9.md) 36 | - [3.10](contents/3/3.10.md) 37 | - [3.11](contents/3/3.11.md) 38 | - [4 扩展解释器](https://github.com/LixvYang/Writing-a-Interpreter-in-Go-Translation/tree/main/contents/5) 39 | - [4.1](contents/4/4.1.md) 40 | - [4.2](contents/4/4.2.md) 41 | - [4.3](contents/4/4.3.md) 42 | - [4.4](contents/4/4.4.md) 43 | - [4.5](contents/4/4.5.md) 44 | - [4.6](contents/4/4.6.md) 45 | - [资源](contents/Resources.md) 46 | ## 阅读以及PDF下载 47 | - [在github上阅读本书)](contents/Acknowledgments.md) 48 | - [英文版PDF下载](book.pdf) 49 | 50 | ## 免责声明 51 | 本人纯属兴趣翻译此书。本人承诺绝不以此译文以任何形式牟利。也坚决拒绝其他任何人以此牟利。**本译文只做学习与交流用途**。[@Lixin](https://github.com/lixvyang)保留对译文的署名权以及其他权力,若有人以此译文进行侵权或者违反知识产权行为与本人无关。 52 | 53 | ## MIT License 54 | Copyright (c) 2021 Lixin 55 | 56 | -------------------------------------------------------------------------------- /contents/3/3.2.md: -------------------------------------------------------------------------------- 1 | # 3.2评估策略 2 | 评估也是解析器实现(无论它们解释的是哪种语言)分歧最大的地方。评估时有很多不同的策略可供选择源代码。我已经在本书中的介绍中暗示了这一点,我们在那里简要介绍了这一点看看不同的解析器架构。既然我们到了这里,掌握了AST,如何处理它以及如何评估我们这颗闪亮的树的问题比以往任何时候都重要。因此再次查看不同的选项是值得的。 3 | 4 | 在我们开始之前,同样值得注意的是,解释器和编译器之间的界限是模糊的。解析器的概念是不会留下可执行文件的东西(与编译器相反,编译器可以留下可执行文件)在查看时变得非常模糊在现实世界和高度优化的编程语言实现中。 5 | 6 | 话虽如此,处理AST的最明显和最经典的选择是解释它。遍历AST,访问每一个节点并执行节点表示的操作:打印一个字符串,添加两个数字,执行一个函数的主题——一切都在运行中。以这种方式工作的解释器成为“tree-walking 解释器”并且是解释器的原型。有时它们的评估步骤之前是重写AST的小优化(例如删除未使用的变量绑定)或将其转换为另一个更适合的中间表示(IR)递归和重复评估。 7 | 8 | 其他的解析器也遍历AST,但不是解析AST它自己,它首先转换它到字节码。字节码是另一种AST的IR并且一个非常密集的IR。精确的格式以及由哪些操作码(构成字节码的指令)组成取决于客机和主机编程语言。但总的来说,操作码与大多数汇编语言的助记符非常相似。可以肯定地说,大多数字节符定义都包含用于push和pop堆栈操作的操作码。 9 | 10 | 但字节码不是自然的机器码,也不是汇编语言。不能也不会由操作系统和运行解释器的机器的CPU执行。相反,它由虚拟机解释,这是解释器的一部分。就像VMWare意义和VirtualBox模拟真实机器和CPU,这些虚拟机模拟机器理解这种特殊的字节码格式。这种方法可以产生很好的性能。 11 | 12 | 这种策略的变化根本不涉及AST。解析器不是构建AST,而是直接发出字节码。现在,我们还在讨论解释器还是编译器?发出字节码然后被解释(或者我们应该说:“执行”?)是一种编译形式吗?我告诉过你:这条线变得模糊了。为了让它更加模糊,考虑一下:一些编程语言的实现解析源代码,构建一个AST并将这个AST转换为字节码。但是,不是直接在虚拟机中执行字节码指定的操作,而是在字节码执行之前,虚拟机将字节码编译为本机机器代码-恰到好处。这成为JIT(即“即时”)解释器/编译器。 13 | 14 | 其他人跳过编译为字节码。它们递归地遍历AST,但在执行它特定的分支之前,接天被编译为本地机器代码。然后被执行。再一次,“即时”。 15 | 16 | 对此的一个轻微变化是一种混合解释模式,其中解释器递归地评估 AST,并且只有在多次评估 AST 的特定分支后,才将分支编译为机器代码。 17 | 18 | 很美妙,不是吗?如此不同的方式去运行关于评估的工作,如此多的曲折和变化。 19 | 20 | 选择哪种策略在很大程度上取决于性能和可移植性需求、正在解释的语言以及您愿意走多远。递归评估AST的树遍历解释器可能是所有方法中最慢的,但易于构建、扩展、推理并且与它实现的语言一样可移植。 21 | 22 | 编译为字节码并使用虚拟机来评估所述字节码的解释器会快很多。 但也更复杂,也更难构建。 将 JIT 编译与机器代码混合在一起,现在如果您希望解释器在 ARM 和 x86 CPU 上工作,您还需要支持多种机器架构。 23 | 24 | 所有这些可选项将会被找到在真实世界的编程语言中。并且大多情况下,所选择的方法随着语言的生命周期而改变。Ruby就是一个很好的例子。直到升级至1.8版本,解释器都是一个树遍历解释器,在遍历它的同时执行AST。但是在1.9版本中切换到了虚拟机架构。现在Ruby解释器解析源代码,构建一个AST,然后将该AST编译成字节码,然后在虚拟机中执行。性能提升是巨大的。 25 | 26 | WebKit JavaScript 引擎 JavaScriptCore 及其名为“Squirrelfish”的解释器也使用 AST 遍历和直接执行作为其方法。 然后在 2008 年转向虚拟机和字节码解释。 现在引擎有四个 (!) 不同的 JIT 编译阶段,它们在解释程序的生命周期中的不同时间启动——这取决于程序的哪个部分需要最佳性能。 27 | 28 | 另一个例子是Lua。Lua编程语言的主要实现最初作为编译器编译为字节码并在基于寄存器的虚拟机中执行字节码的解释器。在第一次发布12年后,该语言的另一个实现诞生了:LuaJIT。LuaJIT的创建者Mike Pall的明确目标是创建最快的Lua实现。他做到了。通过JIT将密集的字节格式编译为针对不同架构的高度优化的机器代码,LuaJIT实现在每个基准测试中都击败了原始的Lua。不仅仅是一点,不;有时快50倍。 29 | 30 | 所以,所有的解释器最初都都是从最小规模开始的,还有改进的余地。这正是我们要做的。有很多方法可以构建更快的解释器,但不一定是最容易理解的方法。我们在这里学习,理解并能够建立我们的工作。 31 | |[< 3.1赋予符号意义](3.1.md)|[> 3.3一个Tree-Walking解释器](3.3.md)| 32 | |-|-| 33 | 34 | 35 | -------------------------------------------------------------------------------- /contents/2/2.2.md: -------------------------------------------------------------------------------- 1 | # 2.2为什么不是解析器生成器? 2 | 3 | 也许您已经听说过解析器生成器,例如 yacc、bison 或 ANTLR 工具。解析器生成器是一种工具,当输入语言的正式描述时,将生成解析器作为其输出。 这个输出是可以被编译/解释的代码,并且可以用源代码作为输入来生成语法树。 4 | 5 | 有许多解析器生成器,它们接收的格式和它们产生的输出语言不同。他们中的大多数无法使用上下文语法(CFG)作为输入。CFG是一组规则,用于描述如何在语言中形成正确(根据语法有效)的句子。CFG最常见的符号形式是Backus-Naur形式(BFN)或者Extended Backus-Naur形式 (EBNF)。 6 | ``` 7 | PrimaryExpression ::= "this" 8 | | ObjectLiteral 9 | | ( "(" Expression ")" ) 10 | | Identifier 11 | | ArrayLiteral 12 | | Litera 13 | Literal ::= ( 14 | | 15 | | 16 | | 17 | | 18 | | ) 19 | Identifier ::= 20 | ArrayLiteral ::= "[" ( ( Elision )? "]" 21 | | ElementList Elision "]" 22 | | ( ElementList )? "]") 23 | ElementList ::= ( Elision )? AssignmentExpression 24 | ( Elision AssignmentExpression )* 25 | Elision ::= ( "," )+ 26 | ObjectLiteral ::= "{" ( PropertyNameAndValueList )? "}" 27 | PropertyNameAndValueList ::= PropertyNameAndValue ( "," PropertyNameAndValue 28 | | "," )* 29 | PropertyNameAndValue ::= PropertyName ":" AssignmentExpression 30 | PropertyName ::= Identifier 31 | | 32 | | 33 | ``` 34 | 这是 BNF 中 EcmaScript 语法的[完整描述](https://tomcopeland.blogs.com/EcmaScript.html)的一部分。 例如,解析器生成器会将类似的东西转换成可编译的 C 代码。 35 | 36 | 可能你已经听说过你应该使用一个解析器生成器代替写一个解析器。“跳过这一部分”,他们说,“这是一个已解决的问题。”这种推荐的原因是解析器非常适合自动生成。解析是计算机科学中最容易理解的分支之一,真正聪明的人已经在解析问题上投入了大量的时间,他们的工作成果是CFG、BNF、EBNF、解析器生成器和其中使用的高级解析技术。为什么不你不应该利用它? 37 | 38 | 我不认为学习写你自己的解析器浪费时间。我实际上觉得它非常有价值。只有在你写过自己的解析器之后才能尝试,或者至少尝试一下,你将会看到解析器生成的优点,他们拥有的缺点和他们的问题。对我来说,解析器生成器的概念只有在我写完第一个解析器后才会明白。 我看着它,然后才真正了解如何自动生成此代码。 39 | 40 | 大多数人,推荐使用解析器生成器的人,当其他人想要开始使用解析器和编译器时,只是因为他们之前自己编写过解析器。他们已经看到了可用的问题和解决方案,并决定最好使用现有工具来完成这项工作。它们是正确的——当你想要完成某件事并且处于生产环境中时,正确性和稳健性是优先事项。当然,那时你不应该尝试编写自己的解析器,尤其是如果你以前从未编写过。 41 | 42 | 但我们是来学习的,我们想了解解析器是如何工作的,此外,我认为这非常有趣。 43 | 44 | |[< 2.1解析器](2.1.md)|[> 2.3为Monkey语言写一个解析器](2.3.md)| 45 | |-|-| -------------------------------------------------------------------------------- /contents/1/1.5.md: -------------------------------------------------------------------------------- 1 | # 1.5开始一个REPL 2 | Monkey语言需要一个REPL。REPL指的是"Read Eval Print Loop"并且你可能从其他的解释语言中明白它是什么:Python有REPL,Ruby有,每个Javascript运行时也有,大多数Lisps也有,还有其它语言。有时REPL被称为"控制台",有时被称为"交互模式"。概念是一样的,REPL读取输入,将其发送给解释器进行评估,打印解释器的结果/输出并再一次启动。阅读,评估,打印,循环。 3 | 4 | 我们还不知道如何完全“评估”Monkey源代码。我们只有隐藏在“Eval”后面过程的一部分:我们可以对Monkey源代码进行标记。但是我们也知道如何阅读和打印一些东西,我认为循环不会造成问题。 5 | 6 | 这里有一个标记Monkey源代码和打印tokens的REPL。之后,我们将拓展它并且添加解析器和评估器。 7 | 8 | ```go 9 | // repl/repl.go 10 | 11 | package repl 12 | 13 | import ( 14 | "bufio" 15 | "fmt" 16 | "io" 17 | "monkey/lexer" 18 | "monkey/token" 19 | ) 20 | 21 | const PROMPT = ">> " 22 | 23 | func Start(in io.Reader, out io.Writer) { 24 | scanner := bufio.NewScanner(in) 25 | 26 | for { 27 | fmt.Printf(PROMPT) 28 | scanned := scanner.Scan() 29 | if !scanned { 30 | return 31 | } 32 | 33 | line := scanner.Text() 34 | l := lexer.New(line) 35 | 36 | for tok := l.NextToken(); tok.Type != token.EOF; tok = l.NextToken() { 37 | fmt.Printf("%+v\n", tok) 38 | } 39 | } 40 | } 41 | ``` 42 | 这些代码非常简单:从输入来源读取,知道遇到一个换行符,将刚刚读取的行传递给我们的词法分析器的实例,最后打印词法分析器给我们的所有tokens,直到我们遇到EOF。在main.go文件中(直到现在我们一直缺少它!)我们欢迎REPL的用户并启动它。 43 | 44 | ```go 45 | // main.go 46 | 47 | package main 48 | 49 | import ( 50 | "fmt" 51 | "monkey/repl" 52 | "os" 53 | "os/user" 54 | ) 55 | 56 | func main() { 57 | user, err := user.Current() 58 | if err != nil { 59 | panic(err) 60 | } 61 | fmt.Printf("Hello %s! This is the Monkey programming language!\n", 62 | user.Username) 63 | fmt.Printf("Feel free to type in commands\n") 64 | repl.Start(os.Stdin, os.Stdout) 65 | } 66 | ``` 67 | 68 | 然后我们可以开始交互式地生产tokens: 69 | ```go 70 | $ go run main.go 71 | Hello lixin yang! This is the Monkey programming language! 72 | Feel free to type in commands 73 | >> let add = fn(x,y){ x + y }; 74 | {Type:LET Literal:let} 75 | {Type:IDENT Literal:add} 76 | {Type:= Literal:=} 77 | {Type:FUNCTION Literal:fn} 78 | {Type:( Literal:(} 79 | {Type:IDENT Literal:x} 80 | {Type:, Literal:,} 81 | {Type:IDENT Literal:y} 82 | {Type:) Literal:)} 83 | {Type:{ Literal:{} 84 | {Type:IDENT Literal:x} 85 | {Type:+ Literal:+} 86 | {Type:IDENT Literal:y} 87 | {Type:} Literal:}} 88 | {Type:; Literal:;} 89 | >> 90 | ``` 91 | 92 | 完美:)现在我们可以开始解析这些tokens了。 93 | |[< 1.4拓展我们的Token集和词法分析器](1.4.md)|[> 2.1解析器](../2/2.1.md)| 94 | |-|-| 95 | -------------------------------------------------------------------------------- /contents/3/3.11.md: -------------------------------------------------------------------------------- 1 | # 3.11谁来倒垃圾? 2 | 在本书的开头,我向你保证,我们不会走任何捷径,用我们自己的双手,从头开始,不使用任何第三方工具,构建一个功能齐全的解释器。我们做到了! 但现在我有一个小小的告白。 3 | 4 | 考虑一下当我们在解释器中运行这段 Monkey 代码时会发生什么: 5 | ```go 6 | let counter = fn(x) { 7 | if (x > 100) { 8 | return true; 9 | } else { 10 | let foobar = 9999; 11 | counter(x + 1); 12 | } 13 | }; 14 | 15 | counter(0); 16 | ``` 17 | 显然,它会在对计数器的主体进行 101 次评估后返回“true”。 但是直到最后一次对计数器返回的递归调用为止,发生了很多事情。 18 | 19 | 第一件事是评估 if-else-expression 条件:x > 100。如果产生的值不真实,则评估 if-else-expression 的替代项。 在替代方案中,整数文字 9999 被绑定到名称 foobar,该名称不再被引用。 然后计算 x + 1。 然后将调用 Eval 的结果传递给另一个对 counter 的调用。 然后一切重新开始,直到 x > 100 评估为 TRUE。 20 | 21 | 重点是:在每次调用 counter 时,都会分配很多对象。 或者就我们的 Eval 函数和我们的对象系统而言:每次对计数器主体的评估都会导致大量 object.Integer 被分配和实例化。 未使用的 9999 整数文字和 x + 1 的结果是显而易见的。 但即使是文字 100 和 1 也会在每次计算 counter 主体时产生新的 object.Integers。 22 | 23 | 如果我们修改我们的 Eval 函数来跟踪 &object.Integer{} 的每个实例,我们会看到运行这个小代码片段会导致大约 400 个分配的 object.Integers。 24 | 25 | 这有什么问题? 26 | 27 | 我们的对象存储在内存中。 我们使用的对象越多,我们需要的内存就越多。 即使与其他程序相比,示例中的对象数量很少,但内存也不是无限的。 28 | 29 | 每次调用来计算我们的解释器进程的内存使用量都应该增加,直到它最终耗尽内存并且操作系统杀死它。 但是如果我们在运行上面的代码片段时监控内存使用情况,我们会看到它不会稳定上升也不会下降。 相反,它会增加和减少。 为什么? 30 | 31 | 这个问题的答案是我必须承认的核心:我们正在重用 Go 的垃圾收集器作为我们的访客语言的垃圾收集器。 我们不需要自己写。 32 | 33 | Go 的垃圾收集器 (GC) 是我们不会耗尽内存的原因。 它为我们管理内存。 即使我们从上面多次调用计数器函数并因此添加更多未使用的整数文字和对象分配,我们也不会耗尽内存。 因为 GC 会跟踪哪些 object.Integer 仍然可以被我们访问,哪些不能。 当它注意到某个对象不再可访问时,它会再次使该对象的内存可用。 34 | 35 | 上面的示例生成了许多在调用 counter 后无法访问的整数对象:文字 1 和 100 以及无意义的 9999 绑定到 foobar。 计数器返回后无法访问这些对象。 在 1 和 100 的情况下,很明显它们是不可达的,因为它们没有绑定到名称。 但是即使是绑定到 foobar 的 9999 也是无法访问的,因为函数返回时 foobar 超出了范围。 为评估 counter 主体而构建的环境被破坏(也被 Go 的 GC 破坏,请注意!)以及 foobar 绑定。 36 | 37 | 这些无法访问的对象是无用的并且占用内存。 这就是 GC 收集它们的原因 38 | 并释放他们使用的内存。 39 | 40 | 这对我们来说非常方便!这为我们节省了很多工作!如果我们用像 C 这样的语言编写解释器,而我们没有 GC,我们需要自己实现一个来为解释器的用户管理内存。 41 | 42 | 这样一个假设的 GC 需要做什么?简而言之:跟踪对象分配和对对象的引用,为将来的对象分配留出足够的内存,并在不再需要时将内存归还。最后一点是垃圾收集的全部内容。没有它,程序会“泄漏”并最终耗尽内存。 43 | 44 | 有无数种方法可以完成上述所有任务,涉及不同的算法和实现。例如,有基本的“标记和清除”算法。为了实现它必须决定 GC 是否是分代 GC,或者它是停止世界 GC 还是并发 GC,或者它如何组织内存和处理内存碎片。在决定了所有这些之后,一个有效的实施仍然是一项艰巨的工作。 45 | 46 | 但也许你会问自己:好的,所以我们有 Go 的 GC 可用。但是我们不能为访客语言编写我们自己的 GC 并使用它吗? 47 | 48 | 抱歉不行。 我们必须禁用 Go 的 GC 并找到一种方法来接管它的所有职责。 说起来容易做起来难。 这是一项艰巨的任务,因为我们还必须自己负责分配和释放内存——使用一种默认情况下完全禁止的语言。 49 | 50 | 这就是为什么我决定不在本书中添加“让我们在 Go 的 GC 旁边编写我们自己的 GC”部分,而是重用 Go 的 GC。 垃圾收集本身是一个巨大的话题,添加解决现有 GC 的维度超出了本书的范围。 但是,我仍然希望本节让您大致了解 GC 的作用以及它解决哪些问题。 也许您现在知道如果要将我们在这里构建的解释器翻译成另一种没有垃圾收集的宿主语言该怎么办。 51 | 52 | 有了这个……我们就完成了! 我们的解释器工作。 剩下的就是扩展它并通过添加更多数据类型和函数使其更有用。 53 | |[< 3.10函数和函数调用](3.10.md)|[> 4.1数据类型&函数](../4/4.1.md)| 54 | |-|-| -------------------------------------------------------------------------------- /contents/2/2.1.md: -------------------------------------------------------------------------------- 1 | # 2.1解析器 2 | 每个已经编程过的人可能已经同说过关于解析器,主要是遇到"parser error"错误。或者可能听到或者甚至说一些像“我们需要解析它”,“在它接着解析后”,“解析因为输入这个而崩溃”。单词"parser"和"compiler""interpreter"和"programming language"一样常见。每个人都知道parsers的存在,它们必须,对吗?因为其他人会对解析错误负责? 3 | 4 | 但是究竟什么是解析器?他的工作是什么并且它是怎样工作的?**Wikipedia**如是说: 5 | > 解析器是一个软件组件,它获取输入数据(通常是文本)并构建数据结构——通常是某种解析树、抽象语法树或其他层次结构——给出输入的结构表示,在过程中检查正确的语法。 [...] 解析器之前通常有一个单独的词法分析器,它从输入字符序列中创建标记; 6 | 7 | 对于有关计算机科学主题的维基百科文章,此摘录非常容易理解。 我们甚至可以在那里看到我们的词法分析器! 8 | 9 | 解析器将其输入转换为表示输入的数据结构。这听起来很抽象,所以让我们用一个例子来说明这一点。这是一小段Javascript: 10 | ```javascript 11 | >varinput='{"name": "Thorsten", "age": 28}'; 12 | >varoutput=JSON.parse(input); 13 | >output 14 | {name:'Thorsten',age:28} 15 | >output.name 16 | 'Thorsten' 17 | >output.age 18 | 28 19 | > 20 | ``` 21 | 我们的输入只是一些文本,字符。我们然后通过它传递给隐藏在JSON.parse函数后面的解析器并接受输出值。这个输出是表示输入的数据结构:一个Javascript对象,有两个字段,name和age,它们的值也对应于输入。我们现在可以很容易地使用这个数据结构,如访问名称和年龄字段所示。 22 | 23 | “但是”,我听到你说,“一个JSON解析器与一个编程语言的解析器不同!它们不一样!”我能看到你在哪里说出了这段话,但不,它们没什么不同。至少在概念层面上没什么不同。一个JSON解析器将文本作为输入并构建表示输入的数据结构。这正是编程语言的解析器所做的。不同之处在于,在JSON解析器的情况下,您可以在查看输入时看到数据结构,而如果你看这个 24 | ```javascript 25 | if ((5+2*3)==91) { return computeStuff(input1,input2);} 26 | ``` 27 | 用数据结构表示这一点并不是很明显。这就是为什么,至少对我来说,它们在更深的概念层面上似乎不同。我的猜测是,这种概念差异的感知主要是对于编程语言解析器及其产生的数据结构缺乏熟悉。与比解析编程语言相比,我在编写JSON,使用解析器解析它并检查解析器的输出方面有更多的经验。作为编程语言的用户,我们很少看到解析的源代码及其内部表示或与之交互。Lisp程序员是规则的例外——在Lisp中,用于表示源代码的数据结构是Lisp用户使用的数据结构。解析后的源代码作为程序中的数据很容易访问。“代码就是数据,数据就是代码”是你经常从Lisp程序员那里听到的。 28 | 29 | 所以,为了将我们对编程语言解析器的概念理解提升到我们对序列化语言(如JSON、YAML、TOML、INI等)解析器的熟悉和直观水平,我们需要了解它们产生的数据结构。 30 | 31 | 在大多数解释器和编译器中,用于源代码内部表示的数据结构称为“语法树”或“抽象语法树(简称AST)”。抽象基于源代码中可见的某些细节是省略的。分号、换行符、空格、注释、大括号、方括号和圆括号——根据语言和解析器,这些细节不在 AST 中表示,而只是在构造它时指导解析器。 32 | 33 | 需要注意的是,没有一种真正的,通用的AST格式被每个解析器使用。它们的实现都非常相似,概念相同,但它们在细节上有所不同。具体实现取决于被解析的编程语言。 34 | 35 | 一个小例子应该可以让事情很简单,让我们假设我们已经有了以下源代码: 36 | ``` 37 | if (3 * 5 > 10) { 38 | return "hello"; 39 | } else { 40 | return "goodbye"; 41 | } 42 | ``` 43 | 并且让我们使用Javascript,有一个MagicLexer,一个MagicParser并且AST是由Javascript对象构建的,那么解析步骤可能会产生这样的结果: 44 | ```javascript 45 | >varinput='if (3 * 5 > 10) { return "hello"; } else { return "goodbye"; }'; 46 | >vartokens=MagicLexer.parse(input); 47 | >MagicParser.parse(tokens) 48 | { 49 | type:"if-statement", 50 | condition:{ 51 | type:"operator-expression", 52 | operator:">", 53 | left:{ 54 | type:"operator-expression", 55 | operator:"*", 56 | left:{type:"integer-literal",value:3}, 57 | right:{type:"integer-literal",value:5} 58 | }, 59 | right:{type:"integer-literal",value:10} 60 | }, 61 | consequence:{ 62 | type:"return-statement", 63 | returnValue:{type:"string-literal",value:"hello"} 64 | }, 65 | alternative:{ 66 | type:"return-statement", 67 | returnValue:{type:"string-literal",value:"goodbye"} 68 | } 69 | } 70 | ``` 71 | 正如你所见,解析器的输出,AST,是非常抽象的:没有括号,没有分号,也没有大括号。但它非常准确地代表了源代码,你不觉得吗?我敢打赌您现在可以在回顾源代码时“看到”AST结构! 72 | 73 | 所以这就是解析器做的,它们**吃掉**源代码作为输入(还有文本或tokens)并且产生与源代码相映的数据结构。当建立数据结构,它们不可避免地分析输入,检查它是否符合预期的结构。因此,解析的过程也称为句法分析。 74 | 75 | 在本章节,我们将写下我们的Monkey语言的解析器。它的输入将会是我们上一个章节定义的tokens,生产通过我们已经写的词法分析器。我们将定义我们自己的AST,定制我们Monkey编程语言的需求,并在递归解析tokens的同时构造此AST实例。 76 | |[< 1.5开始一个REPL](../1/1.5.md)|[> 2.2为什么不是解析器生成器?](2.2.md)| 77 | |-|-| 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /contents/2/2.5.md: -------------------------------------------------------------------------------- 1 | # 2.5解析返回语句 2 | 我之前说过,我们将充实我们的空荡的ParseProgram方法。现在是 3 | 时候了。我们将解析return语句。第一步,就像之前的let语句一样 4 | 它们,是在ast包中定义我们可以表示的必要结构AST中的return语句。 5 | 6 | 下面是Monkey中的返回语句: 7 | ```go 8 | return 5; 9 | return 10; 10 | return add(15); 11 | ``` 12 | 有了let语句的经验,我们很容易发现这些语句背后的结构: 13 | ``` 14 | return ; 15 | ``` 16 | 返回语句仅有关键字return和表达式组成。这使得ast.ReturnStatement的定义非常简单: 17 | ```go 18 | // ast/ast.go 19 | type ReturnStatement struct { 20 | Token token.Token // the 'return' token 21 | ReturnValue Expression 22 | } 23 | 24 | func (rs *ReturnStatement) statementNode() {} 25 | func (rs *ReturnStatement) TokenLiteral() string { return rs.Token.Literal } 26 | ``` 27 | 这个节点没有你之前看过的任何东西:它有一个用于初始token的字段和一个包含要返回的表达式的ReturnValue字段。我们现在将再次跳过表达式的解析和分号,但稍后会回到这一点。statementNode和TokenLiteral方法用于完成Node和Statement接口,看起来与*ast.LetStatement上定义的方法相同。我们接下来编写的测试看起来也与let语句的测试非常相似: 28 | ```go 29 | // parser/parser_test.go 30 | 31 | func TestReturnStatement(t *testing.T) { 32 | input :=` 33 | return 5; 34 | return 10; 35 | return 993322; 36 | ` 37 | l := lexer.New(input) 38 | p := new(l) 39 | 40 | program := p.ParseProgram() 41 | checkParserErrors(t,p) 42 | 43 | if len(program.Statements) != 3 { 44 | t.Fatalf("program.Statements does not contain 3 statements. got=%d", 45 | len(program.Statements)) 46 | } 47 | 48 | for _, stmt := range program.Statements { 49 | returnStmt, ok := stmt.(*ast.ReturnStatement) 50 | if !ok { 51 | t.Errorf("stmt not *ast.returnStatement. got=%T", stmt) 52 | continue 53 | } 54 | if returnStmt.TokenLiteral() != "return" { 55 | t.Errorf("returnStmt.TokenLiteral not 'return', got %q", 56 | returnStmt.TokenLiteral()) 57 | } 58 | } 59 | } 60 | ``` 61 | 当然,一旦表达式解析到位,这些测试用例也必须扩展。但是没关系,测试不是一成不变的。 但事实上,他们失败了: 62 | ```go 63 | $ go test ./parser 64 | --- FAIL: TestReturnStatements (0.00s) 65 | parser_test.go:77: program.Statements does not contain 3 statements. got=0 66 | FAIL 67 | FAIL monkey/parser 0.007s 68 | ``` 69 | 所以让我们改变我们的ParseProgram方法以将token.RETURN标记也考虑在内使它们通过: 70 | ```go 71 | // parser/parser.go 72 | 73 | func (p *Parser) parseStatement() ast.Statement { 74 | switch p.curToken.Type { 75 | case token.LET: 76 | return p.parseLetStatement() 77 | case token.RETURN: 78 | return p.parseReturnStatement() 79 | default: 80 | return nil 81 | } 82 | } 83 | ``` 84 | 在向你展示之前,我可以对parseReturnStatement方法进行大量模糊处理。但是,好吧,我不会,因为它很小。没什么可模糊表达的。 85 | ```go 86 | // parser/parser.go 87 | 88 | func (p *Parser) parseReturnStatement() *ast.ReturnStatement { 89 | stmt := &ast.ReturnStatement{Token: p.curToken} 90 | 91 | p.nextToken() 92 | 93 | // TODO: We're skipping the expressions until we 94 | // encounter a semicolon 95 | for !p.curTokenIs(token.SEMICOLON) { 96 | p.nextToken() 97 | } 98 | 99 | return stmt 100 | } 101 | ``` 102 | 我告诉过你:它很小。它仅仅做的事情就是构造了一个ast.ReturnStatement,使用当前token它作为token坐在上面。然后它通过调用nextToken()为接下来的表达式提供解析器,最后,有警察出来了。它跳过每个表达式,直到遇到分号。就是这样,我们的测试通过了: 103 | ```go 104 | $ go test ./parser 105 | ok monkey/parser 0.009s 106 | ``` 107 | 是时候再一次庆祝了!我们现在解析了Monkey语言中所有的语句。好吧:仅仅只有两个语句。let和return语句。语言的其余部分仅由表达式组成。这就是我们接下来要解析的内容。 108 | 109 | |[< 2.4解析器起步:解析LET语句](2.4.md)|[> 2.6解析表达式](2.6.md)| 110 | |-|-| -------------------------------------------------------------------------------- /contents/2/2.9.md: -------------------------------------------------------------------------------- 1 | # RPPL 2 | 到目前为止,我们的 REPL 更像是一个 RLPL,一个 read-lex-print-loop。 我们还不知道如何评估代码,所以用“evaluate”替换“lex”仍然是不可能的。 但我们现在最肯定知道的是解析。 是时候用“parse”替换“lex”并构建一个RPPL。 3 | ```go 4 | // repl/repl.go 5 | 6 | func Start(in io.Reader, out io.Writer) { 7 | scanner := bufio.NewScanner(in) 8 | 9 | for { 10 | fmt.Printf(PROMPT) 11 | scanned := scanner.Scan() 12 | if !scanned { 13 | return 14 | } 15 | 16 | line := scanner.Text() 17 | l := lexer.New(line) 18 | p := parser.New(l) 19 | 20 | program := p.ParseProgram() 21 | if len(p.Errors()) != 0 { 22 | printParserErrors(out, p.Errors()) 23 | continue 24 | } 25 | 26 | io.WriteString(out, program.String()) 27 | io.WriteString(out, "\n") 28 | } 29 | } 30 | 31 | func printParserErrors(out io.Writer, errors []string) { 32 | for _, msg := range errors { 33 | io.WriteString(out, "\t"+msg+"\n") 34 | } 35 | } 36 | ``` 37 | 在这里,我们扩展循环以解析我们刚刚在 REPL 中输入的行。 然后通过调用其 String 方法打印解析器的输出 *ast.Program,该方法递归调用属于该程序的所有语句的 String 方法。 现在我们可以使用解析器进行旋转 - 在命令行上交互: 38 | ```go 39 | $ go run main.go 40 | Hello mrnugget! This is the Monkey programming language! 41 | Feel free to type in commands 42 | >> let x = 1 * 2 * 3 * 4 * 5 43 | let x = ((((1 * 2) * 3) * 4) * 5); 44 | >> x * y / 2 + 3 * 8 - 123 45 | ((((x * y) / 2) + (3 * 8)) - 123) 46 | >> true == false 47 | (true == false) 48 | >> 49 | ``` 50 | 51 | 很好! 现在,我们可以使用任何基于字符串的 AST 表示来输出,而不是调用 String。 我们可以添加一个 PrettyPrint 方法来打印 AST 节点的类型并正确地指定其子节点,或者我们可以使用 ASCII 颜色代码,或者我们可以打印 ASCII 图形,或者……重点是:天空是极限。 但是我们的 RPPL 仍然有一个巨大的缺点。 52 | 53 | 这是解析器遇到错误: 54 | ```go 55 | $ go run main.go 56 | Hello mrnugget! This is the Monkey programming language! 57 | Feel free to type in commands 58 | >> let x 12 * 3; 59 | expected next token to be =, got INT instead 60 | >> 61 | ``` 62 | 这不是一个很好的错误信息。 我的意思是,它可以完成工作,是的,但它不是很好,是吗? 这Monkey 编程语言值得更好的。 这是一个更用户友好的 printParseError增强用户体验的功能: 63 | ```go 64 | // repl/repl.go 65 | 66 | const MONKEY_FACE = ` __,__ 67 | .--. .-" "-. .--. 68 | / .. \/ .-. .-. \/ .. \ 69 | | | '| / Y \ |' | | 70 | | \ \ \ 0 | 0 / / / | 71 | \ '- ,\.-"""""""-./, -' / 72 | ''-' /_ ^ ^ _\ '-'' 73 | | \._ _./ | 74 | \ \ '~' / / 75 | '._ '-=-' _.' 76 | '-----' 77 | ` 78 | 79 | func printParserErrors(out io.Writer, errors []string) { 80 | io.WriteString(out, MONKEY_FACE) 81 | io.WriteString(out, "Woops! We ran into some monkey business here!\n") 82 | io.WriteString(out, " parser errors:\n") 83 | for _, msg := range errors { 84 | io.WriteString(out, "\t"+msg+"\n") 85 | } 86 | } 87 | ``` 88 | 这样更好! 如果我们现在遇到任何解析器错误,我们就会看到一只猴子,这真的超出了任何人的要求: 89 | ```go 90 | $ go run main.go 91 | Hello mrnugget! This is the Monkey programming language! 92 | Feel free to type in commands 93 | >> let x 12 * 3 94 | __,__ 95 | .--. .-" "-. .--. 96 | / .. \/ .-. .-. \/ .. \ 97 | | | '| / Y \ |' | | 98 | | \ \ \ 0 | 0 / / / | 99 | \ '- ,\.-"""""""-./, -' / 100 | ''-' /_ ^ ^ _\ '-'' 101 | | \._ _./ | 102 | \ \ '~' / / 103 | '._ '-=-' _.' 104 | '-----' 105 | Woops! We ran into some monkey business here! 106 | parser errors: 107 | expected next token to be =, got INT instead 108 | >> 109 | ``` 110 | 转念一想……无论如何,是时候开始评估我们的 AST 了。 111 | |[< 2.8扩展解析器](2.8.md)|[> 3.1赋予符号意义](../3/3.1.md)| 112 | |-|-| -------------------------------------------------------------------------------- /contents/3/3.4.md: -------------------------------------------------------------------------------- 1 | # 3.4表示对象 2 | 等一下,什么?你从来没说过Monkey是面向对象的!是的,我从来没说但它不是。 为什么我们需要“对象系统”呢? 那么称之为“价值系统”或“对象表示”。关键是,我们需要定义我们的“eval”函数返回什么。 我们需要一个系统来表示我们的 AST 所代表的值或我们在内存中评估 AST 时生成的值。 3 | 4 | 让我们来评估下面的Monkey代码: 5 | ``` 6 | let a = 5; 7 | //[...] 8 | a + a; 9 | ``` 10 | 如您所见,我们将整数文字 5 绑定到名称 a。 然后事情就发生了。 没关系。 重要的是,当我们稍后遇到 a + a 表达式时,我们需要访问 a 绑定到的值。 为了计算 a + a,我们需要得到 5。在 AST 中,它表示为 *ast.IntegerLiteral,但是我们如何在计算 AST 的其余部分时跟踪和表示 5? 11 | 12 | 在解释性语言中构建值的内部表示时,有很多不同的选择。关于这个话题,有很多智慧在世界解释器和编译器的代码库中传播。每个解释器都有关于自己的方式来表示值,总是与之前的解决方案略有不同,根据需求进行了调整解释型语言。 13 | 14 | 有些使用宿主语言的本机类型(整数、布尔值等)来表示解释语言中的值,而不是用任何东西包装。 在其他语言中,值/对象仅表示为指针,而在某些编程语言中,本机类型和指针是混合的。 15 | 16 | 为什么种类这么多?一方面,宿主语言不同。你如何表示解释语言的字符串取决于如何在解释器的语言中表示字符串用Ruby编写的解释器不能像用C编写的解释器那样表示值。 17 | 18 | 并且不仅仅是宿主语言不同,而是语言解释器的实现不同。一些解释器语言可能只需要原始数据类型的表示,如整形、字符或字节。但在其他情况下,您将拥有列表、字典、函数或复合数据类型。这些差异导致对价值表示的要求截然不同。 19 | 20 | 除了宿主语言和解释语言,对设计影响最大的值表示的实现是由此产生的执行速度和内存在评估程序时消耗。如果你想构建一个快速的解释器,你无法摆脱缓慢而臃肿的对象系统。如果你要自己写出垃圾收集器,您需要考虑它如何跟踪系统中的值。但是,另一方面,如果你不关心性能,那么保持简单是有意义的并且与易于理解,直到出现进一步的要求。 21 | 22 | 关键是:有很多不同的方式来表示宿主语言中解释语言的值。 了解这些不同表示的最好(也可能是唯一)方法是实际通读一些流行解释器的源代码。 我衷心推荐 **Wren 源代码**,其中包括两种类型的值表示,通过使用编译器标志启用/禁用。 23 | 24 | 除了宿主语言中值的表示之外,还有一个问题是如何将这些值及其表示暴露给解释语言的用户。 这些值的“公共 API”是什么样的? 25 | 26 | 例如,Java 向用户提供“原始数据类型”(int、byte、short、long、float、double、boolean、char)和引用类型。 原始数据类型在 Java 实现中没有大量表示,它们紧密映射到它们的本地对应物。 另一方面,引用类型是对宿主语言中定义的复合数据结构的引用。 27 | 28 | 在 Ruby 中,用户无权访问“原始数据类型”,没有像原生值类型那样存在,因为一切都是对象,因此包装在内部表示中。 Ruby 在内部不区分字节和类 Pizza 的实例:两者都是相同的值类型,包装不同的值。 29 | 30 | 有无数种方法可以向编程语言的用户公开数据。 选择哪一个取决于语言设计,同样取决于性能要求。 如果你不关心性能,一切都会好起来的。 但是如果你这样做了,你需要做出一些明智的决定来实现你的目标。 31 | 32 | ## 我们对象系统的基础 33 | 由于我们仍然关心我们的 Monkey 解释器的性能,我们选择了简单的方法:我们将在评估 Monkey 源代码时遇到的每个值表示为对象,我们设计的接口。 每个值都将被包装在一个结构体中,该结构体实现了这个 Object 接口。 34 | 35 | 在一个新object包我们定义了`Object`接口和ObjectType类型: 36 | ```go 37 | package object 38 | 39 | type ObjectType string 40 | 41 | type Object interface { 42 | Type() ObjectType 43 | Inspect() string 44 | } 45 | ``` 46 | 这非常简单,看起来很想我们在带有Token和TokenType类型的token包中所作的。除了像Token这样的结构体之外,Object类型是一个接口。原因是每个值都需要不同的内部表示,并且定义两种不同的结构体类型比尝试将布尔值和整数放入同一个结构体字段更容易。 47 | 48 | 这时候我们只需要三个数据类型在Monkey解释器中:null,booleans和integers。让我们开始用实现integer表示和构建我们的对象系统。 49 | ## integer 50 | object.Integer类型和你预期的一样简短: 51 | ```go 52 | // object/object.go 53 | 54 | import ( 55 | "fmt" 56 | ) 57 | 58 | type Integer struct { 59 | Value int64 60 | } 61 | 62 | func (i *Integer) Inspect() string { return fmt.Sprintf("%d", i.Value) } 63 | ``` 64 | 无论何时我们输入一个integer字段在源代码中我们首先返回它到一个ast.IntegerLiteral并且然后,当评估这个AST节点时,我们返回一个object.Integer,将值保存在我们的结构中并传递对该结构的引用。 65 | 66 | 为了让object.Integer实现object.Object接口,它仍然需要一个Type方法返回它的ObjectType。就像我们对token.TokenType所作的一样,我们为每个ObjectType定义常量: 67 | ```go 68 | // object/object.go 69 | import "fmt" 70 | 71 | type ObjectType string 72 | 73 | const ( 74 | INTEGER_OBJ = "INTEGER" 75 | ) 76 | 77 | ``` 78 | 正如我所说的,这几乎就是我们在token包所作的。有了它我们就可以将Type()方法添加到*object.Integer: 79 | ```go 80 | // object/object.go 81 | func (i *Integer) Type() ObjectType { return INTEGER_OBJ } 82 | ``` 83 | 我们完成了Integer! 转换成另一种数据类型:布尔值。 84 | ## 布尔值 85 | 如果你期待本节的大事,我很抱歉让你失望。 object.Boolean 非常小: 86 | ```go 87 | // object/object.go 88 | 89 | const ( 90 | // [...] 91 | BOOLEAN_OBJ = "BOOLEAN" 92 | ) 93 | type Boolean struct { 94 | Value bool 95 | } 96 | func (b *Boolean) Type() ObjectType { return BOOLEAN_OBJ } 97 | func (b *Boolean) Inspect() string { return fmt.Sprintf("%t", b.Value) } 98 | ``` 99 | 只是一个包装单个值的结构体,一个布尔值。 100 | 101 | 我们已经接近完成对象系统的基础,现在我们需要做的最后的事是在我们开始我们的 Eval 函数之前,是表示一个不存在的值。 102 | 103 | ## Null 104 | Tony Hoare 在 1965 年引入了对 ALGOL W 语言的空引用,并称这是他的“十亿美元的错误”。 自从引入以来,无数系统因引用“null”而崩溃,该值表示没有值。 至少可以说,Null(或某些语言中的“nil”)并没有最好的声誉。 105 | 106 | 我和自己争论 Monkey 是否应该为 null。 一方面,是的,如果该语言不允许 null 或 null 引用,则使用起来会更安全。 但另一方面,我们并不是要重新发明轮子,而是要学习一些东西。 而且我发现在我可以使用 null 时,只要有机会使用它,我就会三思而后行。 有点像在你的车里放一些爆炸性的东西会让你开得更慢、更小心。 它真的让我很欣赏编程语言设计中的选择。 这是我认为值得的。 因此,让我们实现 Null 类型,并在以后使用它时保持仔细观察和稳定。 107 | ```go 108 | // object/object.go 109 | const ( 110 | // [...] 111 | NULL_OBJ = "NULL" 112 | ) 113 | type Null struct{} 114 | 115 | func (n *Null) Type() ObjectType { return NULL_OBJ } 116 | func (n *Null) Inspect() string { return "null" } 117 | ``` 118 | object.Null 和 object.Boolean 和 object.Integer 一样是一个结构体,只是它不包装任何值。 它代表没有任何值。 119 | 120 | 添加 object.Null 后,我们的对象系统现在能够表示布尔值、整数和空值。 这足以开始使用 Eval。 121 | |[< 3.3一个Tree-Walking解释器](3.3.md)|[> 3.5评估表达式](3.5.md)| 122 | |-|-| 123 | 124 | -------------------------------------------------------------------------------- /contents/3/3.6.md: -------------------------------------------------------------------------------- 1 | # 3.6条件句 2 | 您会惊讶于在我们的评估器中添加对条件的支持是多么容易。 他们实施的唯一困难是决定何时评估什么。 因为这就是条件语句的全部意义所在:只根据条件来评估某事。 考虑一下: 3 | ```go 4 | if (x > 10) { 5 | puts("everything okay!"); 6 | } else { 7 | puts("x is too low!"); 8 | shutdownSystem(); 9 | } 10 | ``` 11 | 当评估这里的if-else最重要的事情就是只评估正确的分支。如果情况正确,我们就必须禁止评估else-分支,只评估if-分支。并且如果不正确,我们就只评估else-分支。 12 | 13 | 换句话说:如果条件 x > 10 不是……那么,我们只能评估这个条件的 else 分支……好吧,当它不是什么时? 我们是否应该评估后果,“everything okay!” 分支,仅当条件表达式生成真或生成“真”、不假或不为空的东西时? 14 | 15 | 这就是这个问题的难点,因为这是一个设计决策,准确地说是一个语言设计决策,会产生广泛的后果。 16 | 17 | 在 Monkey 的情况下,条件的结果部分将在条件为“真”时进行评估。 “真实”的意思是:它不是null,也不是false。 它不一定是真的。 18 | ```js 19 | let x = 10; 20 | if (x) { 21 | puts("everything okay!"); 22 | } else { 23 | puts("x is too high!"); 24 | shutdownSystem(); 25 | } 26 | ``` 27 | 在这个例子中“everything okay!”应该被打印,为什么呢?因为x被绑定为10,评估10并且10不是null或者不是false。 28 | 29 | 既然我们已经讨论过这个,我们可以把这个规范变成一组测试用例: 30 | ```go 31 | // evaluator/evaluator_test.go 32 | 33 | func TestIfElseExpressions(t *testing.T) { 34 | tests := []struct { 35 | input string 36 | expected interface{} 37 | }{ 38 | {"if (true) { 10 }", 10}, 39 | {"if (false) { 10 }", nil}, 40 | {"if (1) { 10 }", 10}, 41 | {"if (1 < 2) { 10 }", 10}, 42 | {"if (1 > 2) { 10 }", nil}, 43 | {"if (1 > 2) { 10 } else { 20 }", 20}, 44 | {"if (1 < 2) { 10 } else { 20 }", 10}, 45 | } 46 | for _, tt := range tests { 47 | evaluated := testEval(tt.input) 48 | integer, ok := tt.expected.(int) 49 | if ok { 50 | testIntegerObject(t, evaluated, int64(integer)) 51 | } else { 52 | testNullObject(t, evaluated) 53 | } 54 | } 55 | } 56 | 57 | func testNullObject(t *testing.T, obj object.Object) bool { 58 | if obj != NULL { 59 | t.Errorf("object is not NULL. got=%T (%+v)", obj, obj) 60 | return false 61 | } 62 | return true 63 | } 64 | ``` 65 | 这个测试函数也明确了我们已经讨论过的情况。当一个情况没有评估为一个值时,它返回NULL,等等: 66 | ```go 67 | if(false){ 10 } 68 | ``` 69 | else 缺失,因此条件应该产生 NULL。 70 | 71 | 我们必须做一些类型断言和转换思考,以在我们预期的字段中允许 nil,当然,但测试是可读的,并且清楚地显示了所需的和特此指定的行为。 它们也会失败,因为我们不返回任何 *object.Integers 或 NULL: 72 | ```go 73 | $ go test ./evaluator 74 | --- FAIL: TestIfElseExpressions (0.00s) 75 | evaluator_test.go:125: object is not Integer. got= () 76 | evaluator_test.go:153: object is not NULL. got= () 77 | evaluator_test.go:125: object is not Integer. got= () 78 | evaluator_test.go:125: object is not Integer. got= () 79 | evaluator_test.go:153: object is not NULL. got= () 80 | evaluator_test.go:125: object is not Integer. got= () 81 | evaluator_test.go:125: object is not Integer. got= () 82 | FAIL 83 | FAIL monkey/evaluator 0.007s 84 | ``` 85 | 早些时候我告诉过你,你会惊讶于实现对条件的支持是多么容易。不相信我吗? 好吧,看看让测试通过所需的少量代码: 86 | ```go 87 | // evaluator/evaluator.go 88 | 89 | func Eval(node ast.Node) object.Object { 90 | // [...] 91 | case *ast.BlockStatement: 92 | return evalStatements(node.Statements) 93 | 94 | case *ast.IfExpression: 95 | return evalIfExpression(node) 96 | // [...] 97 | } 98 | 99 | func evalIfExpression(ie *ast.IfExpression) object.Object { 100 | condition := Eval(ie.Condition) 101 | 102 | if isTruthy(condition) { 103 | return Eval(ie.Consequence) 104 | } else if ie.Alternative != nil { 105 | return Eval(ie.Alternative) 106 | } else { 107 | return NULL 108 | } 109 | } 110 | 111 | func isTruthy(obj object.Object) bool { 112 | switch obj { 113 | case NULL: 114 | return false 115 | case TRUE: 116 | return true 117 | case FALSE: 118 | return false 119 | default: 120 | return true 121 | } 122 | } 123 | ``` 124 | 正如我所说:唯一困难的是决定要评估什么。 该决定封装在 evalIfExpression 中,其中行为的逻辑非常清晰。 isTruthy 同样具有表现力。 除了这两个函数之外,我们还在 Eval switch 语句中添加了 *ast.BlockStatement 的 case 分支,因为 *ast.IfExpression 的 .Consequence 和 .Alternative 都是块语句。 125 | 126 | 我们添加了两个新的、简洁的函数,以清晰的方式显示 Monkey 编程语言的语义,重用了我们已有的另一个函数,并通过这样做增加了对条件的支持并使测试通过。 我们的解释器现在支持 if-else 表达式! 我们现在离开计算器领域,直奔编程语言领域: 127 | ```go 128 | $ go run main.go 129 | Hello mrnugget! This is the Monkey programming language! 130 | Feel free to type in commands 131 | >> if (5 * 5 + 10 > 34) { 99 } else { 100 } 132 | 99 133 | >> if ((1000 / 2) + 250 * 2 == 1000) { 9999 } 134 | 9999 135 | >> 136 | ``` 137 | |[< 3.5评估表达式](3.5.md)|[> 3.7return语句](3.7.md)| 138 | |-|-| 139 | -------------------------------------------------------------------------------- /contents/Resources.md: -------------------------------------------------------------------------------- 1 | # 资源 2 | ## Books 3 | 4 | - Abelson, Harold and Sussman, Gerald Jay with Sussman, Julie. 1996. Structure and 5 | Interpretation of Computer Programs, Second Edition. MIT Press. 6 | - Appel, Andrew W.. 2004. Modern Compiler Implementation in C. Cambridge 7 | University Press. 8 | - Cooper, Keith D. and Torczon Linda. 2011. Engineering a Compiler, Second Edition. Morgan Kaufmann. 9 | - Grune, Dick and Jacobs, Ceriel. 1990. Parsing Techniques. A Practical Guide.. 10 | Ellis Horwood Limited. 11 | - Grune, Dick and van Reeuwijk, Kees and Bal Henri E. and Jacobs, Ceriel J.H. Jacobs and 12 | Langendoen, Koen. 2012. Modern Compiler Design, Second Edition. Springer 13 | - Nisan, Noam and Schocken, Shimon. 2008. The Elements Of Computing Systems. 14 | MIT Press. 15 | 16 | ## Papers 17 | 18 | - Ayock, John. 2003. A Brief History of Just-In-Time. In ACM Computing Surveys, Vol. 35, No. 2, June 2003 19 | - Ertl, M. Anton and Gregg, David. 2003. The Structure and Performance of Efficient 20 | Interpreters. In Journal Of Instruction-Level Parallelism 5 (2003) 21 | - Ghuloum, Abdulaziz. 2006. An Incremental Approach To Compiler Construction. 22 | In Proceedings of the 2006 Scheme and Functional Programming Workshop. 23 | - Ierusalimschy, Robert and de Figueiredo, Luiz Henrique and Celes Waldemar. The Implementation of Lua 5.0. https://www.lua.org/doc/jucs05.pdf 24 | - Pratt, Vaughan R. 1973. Top Down Operator Precedence. Massachusetts Institute 25 | of Technology. 26 | - Romer, Theodore H. and Lee, Dennis and Voelker, Geoffrey M. and Wolman, Alec and 27 | Wong, Wayne A. and Baer, Jean-Loup and Bershad, Brian N. and Levy, Henry M.. 1996. 28 | The Structure and Performance of Interpreters. In ASPLOS VII Proceedings 29 | of the seventh international conference on Architectural support for programming languages and operating systems. 30 | - Dybvig, R. Kent. 2006. The Development of Chez Scheme. In ACM ICFP ’06 31 | - 32 | ## Web 33 | - Jack W. Crenshaw - Let’s Build a Compiler! - http://compilers.iecc.com/crenshaw/tutorfinal.pdf 34 | - Douglas Crockford - Top Down Operator Precedence - http://javascript.crockford.com/ 35 | tdop/tdop.html 36 | - Bob Nystrom - Pratt Parsers: Expression Parsing Made Easy - http://journal.stuffwithstuff.com/2011/03/19/pratt-parsers-expression-parsing-made-easy/ 37 | - Shriram Krishnamurthi and Joe Gibbs Politz - Programming Languages: Application and 38 | Interpretation - http://papl.cs.brown.edu/2015/ 39 | - A Python Interpreter Written In Python - http://aosabook.org/en/500L a-python-interpreter-written-in-python. 40 | html 41 | - Dr. Dobbs - Bob: A Tiny Object-Oriented Language - http://www.drdobbs.com/ 42 | open-source/bob-a-tiny-object-oriented-language/184409401 43 | - Nick Desaulniers - Interpreter, Compiler, JIT - https://nickdesaulniers.github.io/blog/ 44 | 2015/05/25/interpreter-compiler-jit/ 45 | - Peter Norvig - (How to Write a (Lisp) Interpreter (in Python)) - http://norvig.com/lispy. 46 | html 47 | - Fredrik Lundh - Simple Town-Down Parsing In Python - http://effbot.org/zone/ 48 | simple-top-down-parsing.htm 49 | - Mihai Bazon - How to implement a programming language in JavaScript - http: 50 | //lisperator.net/pltut/ 51 | - Mary Rose Cook - Little Lisp interpreter - https://www.recurse.com/blog/21-little-lisp-interpreter 52 | - Peter Michaux - Scheme From Scratch - http://peter.michaux.ca/articles/scheme-from-scratch-introduction 53 | - Make a Lisp - https://github.com/kanaka/mal 54 | - Matt Might - Compiling Scheme to C with closure conversion - http://matt.might.net/ 55 | articles/compiling-scheme-to-c/ 56 | - Rob Pike - Implementing a bignum calculator - https://www.youtube.com/watch?v= 57 | PXoG0WX0r_E 58 | - Rob Pike - Lexical Scanning in Go - https://www.youtube.com/watch?v=HxaD_trXwRE 59 | ## Source Code 60 | - The Wren Programming Language - https://github.com/munificent/wren 61 | - Otto - A JavaScript Interpreter In Go - https://github.com/robertkrimen/otto 62 | - The Go Programming Language - https://github.com/golang/go 63 | - The Lua Programming Language (1.1, 3.1, 5.3.2) - https://www.lua.org/versions.html 64 | - The Ruby Programming Language - https://github.com/ruby/ruby 65 | - c4 - C in four functions - https://github.com/rswier/c4 66 | - tcc - Tiny C Compiler - https://github.com/LuaDist/tcc 67 | - 8cc - A Small C Compiler - https://github.com/rui314/8cc 68 | - Fedjmike/mini-c - https://github.com/Fedjmike/mini-c 69 | - thejameskyle/the-super-tiny-compiler - https://github.com/thejameskyle/the-super-tiny-compiler 70 | - lisp.c - https://gist.github.com/sanxiyn/523967 71 | 72 | # Feedback 73 | 如果您发现拼写错误,发现代码有问题,有建议或只是一个问题,请随时给我发送电子邮件: 74 | **me@thorstenball.com** -------------------------------------------------------------------------------- /contents/Introduction.md: -------------------------------------------------------------------------------- 1 | ## 介绍 2 | 3 | 介绍的的第一句话应该是:解释器很神奇。当然,最早的某位不愿透露姓名的评论者说:看起来很愚蠢。好吧,Christian,我不这样认为。我仍然认为解释器很神奇!让我来告诉你为什么。 4 | 5 | 从表面上看,它们很简单:输入文本,输出内容。它们是将其他程序作为输入并产生某些东西的程序。很简单吧?但你越想它,它就越迷人。看似随机的字母,数字和特殊字符,被输入解释器就突然变得有意义。解释器赋予了它们意义!看起来胡说八道是有道理的。而计算机是一个基于理解0和1的机器,现在却可以理解并且对我们输入奇奇怪怪的语音进行响应——这要归功于在阅读时翻译这种语言的解释器。 6 | 7 | 我一直在反问自己:这究竟是怎样运行的?第一次这个问题在脑海中形成时,我就已经知道,只有通过编写自己的解释器才能解决这个问题。所以我就这样去做了。 8 | 9 | 有许多关于解释器的书,文章,博客文章和教程。不过,在大多数情况下,它们都无外乎这两种情况:它们要么巨长,要么就理论很深,并且是针对于对解释器有所了解的人。要么就很简短,仅仅只提供对解释器简短的介绍,使用外部工具作为黑盒子并且仅仅只关心“玩具解释器”。 10 | 11 | 后一类资源更是令人难过,因为它们的解释器知识用非常简单的语法解释语言。我不想走捷径,我真的想理解解释器是怎样工作的,其中包括了解语法分析器和解析器的工作原理。尤其是对于类似C语言机器花括号和分号,我都不知道如何开始解析它们。学术教科书当然有我寻找的答案,但在它们冗长的理论解释和数学解释背后,我很难理解。 12 | 13 | 我想要的东西是900页关于编译器的书和解释如何用50行Ruby代码编写Lisp解释器的博客与文章之间的内容。 14 | 15 | 所以我写了这本书,给你和我。这是一本我希望有的书。这是一本适合喜欢深入原理的人的书,对于喜欢通过了解事物真正运作方式来学习的人。 16 | 17 | 在这本书中我们将写下我们自己的解释器对于我们自己的编程语言——从头开始。我们将不会使用第三方工具和库。结果不会是准备生产的,它不会具有成熟解释器的性能,当然,基于语言建立的解释器将会缺少功能,但是我们会学到很多东西。 18 | 19 | 很难对解释器进行描述,因为它的种类很多而且各不相同。可以说的是它们都有的基本属性是获取源代码进行评估,而不会产生一些可以执行可见的中间结果。这与编译器形成对比,编译器可以获取源代码并以底层系统可以理解的另一种语言进行输出。 20 | 21 | 一些解释器是真的微小,甚至不需要解析。它们知识立即解释输入,我的意思是看看众多的Brainfun之一。 22 | 23 | 另一方面是更复杂的解释器类型。其中一些不只是评估它们的输入,而是将其编译成字节码在内部表示,然后对其进行评估。更先进的是JIT解释器,它编译输入的即时语调机器代码然后执行。 24 | 25 | 但是,在两个类别之间,有一些解释器可以解析源代码,从中构建一个抽象语法树(AST),然后评估语法树。这种类型的解释器有时被称为“行走的树”解释器,因为它运行在AST上并且解释它。 26 | 27 | 我们将在本书中构建的是这样一个树形解释器。 28 | 29 | 我们将构建我们自己的词法分析器、我们自己的解析器、我们自己的表达树和我们自己的评估器。我们将看一看什么是“令牌”,什么是抽象语法树,如何构建这样的树,如何评估它以及如何使用新的数据结构和内置函数拓展我们的语言。 30 | 31 | ## Monkey编程语言和解释器 32 | 每个解释器是构建特定的编程语言而构建的。这就是你“实现”编程语言方式。没有编译器或解释器,编程语言只不过是一种想法或规范。 33 | 34 | 我们将解析和评估我们的自己的Monkey编程语言。这是一个为本书特别设计的语言。它唯一的实现是我们将在本书中构建的——我们的解释器。 35 | 36 | 列出一系列功能,Monkey具有以下: 37 | - C-like语法 38 | - 变量绑定 39 | - 整数和布尔值 40 | - 算术表达式 41 | - 内置函数 42 | - 一等和高阶函数 43 | - 闭包 44 | - 字符串数据结构 45 | - 数组数据结构 46 | - 哈希数据结构 47 | 48 | 我们将在本书的其余部分详细了解并实现这些功能中的每一个。但是现在,让我们看看Monkey是什么样子。 49 | 50 | 这里是我们如何变量绑定在Monkey中: 51 | 52 | ``` 53 | let age = 1; 54 | let name = "Monkey"; 55 | let resulet = 10 * (20/2); 56 | ``` 57 | 58 | 除了整形,布尔值和字符串,Monkey解释器我们也将绑定支持数组和哈希,这是将整数数组绑定到名称的样子: 59 | 60 | ``` 61 | let myArray=[1,2,3,4,5]; 62 | ``` 63 | 并且这里是哈希,这儿值和键是联系的: 64 | ``` 65 | let thorsten = {"name":"Thorsten","age":20}; 66 | ``` 67 | 访问数组中的元素和哈希完成索引表达式: 68 | ``` 69 | myArray[0] // => 1 70 | thorsten["name"] // =>"Toristen" 71 | ``` 72 | 73 | `let`声明也可以用来绑定函数的名称,这是一个添加两个数的小函数: 74 | ``` 75 | let add = fn(a,b) { return a + b; }; 76 | ``` 77 | 78 | 但是Monkey不仅仅支持`return`声明。隐式返回值也是可能的,这意味着我们可以省略返回值,如果我们想要: 79 | ``` 80 | let add = fn(a,b) { a + b; }; 81 | ``` 82 | 83 | 并且调用函数和你期待的一样简单: 84 | ``` 85 | add(1,2) 86 | ``` 87 | 88 | 一个更加复杂的函数,例如一个斐波那契函数返回结果,可能像这样: 89 | ``` 90 | let fibonacci = fn(x){ 91 | if(x==0) { 0 92 | } else { 93 | if (x==1) { 94 | 1 95 | } else 96 | {fibonacci(x-1) + fibonacci(x-2); 97 | } 98 | } 99 | }; 100 | ``` 101 | 注意递归调用`fibonacci`它自己! 102 | 103 | Monkey也支持特殊的函数类型,称为高阶函数。这些是将其他函数作为参数的函数。 下面是一个例子: 104 | ``` 105 | let twice = fn(f,x){ 106 | return f(f(x)); 107 | }; 108 | 109 | let addTwo = fn(x){ 110 | return x+2; 111 | }; 112 | 113 | twice(addTwo,2);// >= 6 114 | ``` 115 | 116 | 这里`twice`接受两个参数,另一个叫做`addTwo`函数和整数2。它使用 第一个2 作为参数调用 addTwo 两次,然后使用第一次调用的返回值调用。 最后一行产生6。 117 | 118 | 是的,我们能使用函数作为另一个函数调用的参数。在Monkey中函数只是一个值,像整形和字符串。该功能称为“一流功能”。 119 | 120 | 我们将在本书构建的解释器将实现这些所有的功能。它将在REPL中标记和解析Monkey源码,构建称为抽象语法树的代码的内部表示,然后评估该树。它将有几个主要部分: 121 | - 此法分析器 122 | - 解析器 123 | - 抽象语法树(AST) 124 | - 内部对象系统 125 | - 评估器 126 | 127 | 我们将完全按照这个顺序从下往上构建这些部分。或者更好地说:从源码开始,以输出结束。这种方法的缺点是它不会在第一章结束后产生一个简单的"Hello Word"。优点是更容易理解所有部分如何组合在一起以及数据如何在程序中流动。 128 | 129 | 但是为什么这个名字?为什么叫Monkey?好吧,因为monkey是华丽,优雅,迷人和有趣动物。就像我们的解释器。 130 | 131 | 并且为什么是这个书名呢? 132 | 133 | ## 为什么使用Go? 134 | 如果你读到这里都没有注意到标题和其中的"in Go"字样,首先:恭喜,这很了不起。第二:我们将用Go实现我们的解释器。为什么用Go呢? 135 | 136 | 我喜欢用Go写代码,我享受使用Go语言、它的标准库和它提供的工具所带来的乐趣。除此之外,我认为Go语言有一些很适合本书的属性。 137 | 138 | Go是真正地易于阅读和快速理解。你将不需要破译我在本书中给你展示的Go代码。即使你不是一个有经验的Go程序员。我敢打赌,即使你从未写过一行Go语言,你也可以跟这本书学习。 139 | 140 | 另一个原因就是Go语言提供的强大工具。本书集中于我们所写的解释器——它背后的思想和概念及其实现。借助Go的通用格式风格,感谢`gofmt`和内置的测试框架,我们可以专注于我们的解释器而不必担心第三方库、工具和依赖性。除了Go语言提供的工具之外,我们不会再本书中使用任何其他工具。 141 | 142 | 但我认为更重要的是,本书中呈现的 Go 代码与其他可能更底层的语言(如 C、C++ 和 Rust)密切相关。 可能这是 Go 本身的原因,它专注于简单性、精简的魅力以及缺乏其他语言中没有且难以翻译的编程语言结构。或者也许是因为我选择为这本书编写 Go 的方式。无论哪种方式,都不会有任何元编程技巧来学习两周后没人能理解的捷径,也不会出现需要笔、纸和“实际上,这很容易”这句话来解释的宏伟的面向对象设计和模式。 143 | 144 | 所有这些原因使得在此处实现的代码易于去理解(在概念和技术层面上),并且可重复使用。如果你在阅读本书之后,选择用另一种编程语言编写自己的解释器,本书也会派上用场。我想通过这本书为你理解和构建解释器提供一个起点,我认为代码反映了这一点。 145 | 146 | ## 怎样使用本书? 147 | 本书既不是一个参考书,也不是表述附录中代码解释器实现概念充满理论的论文。本书旨在从头到尾阅读,我建议您按照阅读、输入和修改呈现代码进行阅读。 148 | 149 | 每一章都在其之前的代码和文章中构建。在每个章节中,我们融合了我们的解释器的一部分,每个部分都紧密相连。为了使代码更容易衔接,这本书是一个被叫`code`的文件夹,它包含,好吧,code。 如果您的书的副本没有文件夹,您可以在此处下载 150 | 151 | [https://interpreterbook.com/waiig_code_1.3.zip](https://interpreterbook.com/waiig_code_1.3.zip) 152 | 153 | `code`文件夹分为许多子文件夹,每个章节一个,其中包含响应章节的最后结果。 154 | 155 | 有时我只会暗示在代码中的东西,而不是代码本身(因为它要么像测试文件的一样占用太多的空间,要么就是有很多细节)-你可以在对应的章节中找到这些代码。 156 | 157 | 你需要哪些准备哪些工具?不多:一个文本剪辑器和Go语言。任何Go1.0以上的版本都可以工作,但作为一个免责声明和以防未来的变化:在撰写本文时,我正在使用Go 1.7。 158 | 159 | 我还建议使用`direnv`,那可以改变你终端的环境根据`.envrc`文件。本书附带的`code`文件夹中的每个子文件夹都包含这样一个`.envrc`文件,该文件为该子文件夹正确设置了GOPATH。这让我们可以轻松处理不同章节的代码。 160 | 161 | 让我们开始吧! 162 | 163 | 随着那个方式,让我们开始! 164 | |[< Acknowledgments](Acknowledgments.md)|[> 1.1词法分析](1/1.1.md)| 165 | |-|-| 166 | -------------------------------------------------------------------------------- /contents/3/3.7.md: -------------------------------------------------------------------------------- 1 | # 3.7return语句 2 | 现在,您在标准计算器上找不到一些东西:return 语句。Monkey 有它们,就像许多其他语言一样。 它们可以在函数体中使用,也可以在 Monkey 程序中作为顶级语句使用。 但是它们用在何处并不重要,因为它们的工作方式不会改变:return 语句停止对一系列语句的求值,并留下它们的表达式求值后的值。 3 | 4 | 这是 Monkey 程序中的顶级 return 语句: 5 | ```go 6 | 5 * 5 * 5; 7 | return 10; 8 | 9 * 9 * 9; 9 | ``` 10 | 当评估这段程序时应该返回10.如果这里的语句是在函数体内,调用函数应该评估为10.重要的应该是后一行,`9 * 9 * 9`表达式,将不会被评估。 11 | 12 | 有几种不同的方法来实现 return 语句。 在某些宿主语言中,我们可以使用 goto 或异常。 但是在 Go 中,“rescue”或“catch”并不容易,而且我们真的没有选择以干净的方式使用 goto。 这就是为什么,为了支持 return 语句,我们将通过我们的评估器传递一个“返回值”。 每当我们遇到 return 时,我们都会将它应该返回的值包装在一个对象中,以便我们可以跟踪它。 我们需要跟踪它,以便我们以后可以决定是否停止评估。 13 | 14 | 这是所述对象的实现。 这是 object.ReturnValue: 15 | ```go 16 | // object/object.go 17 | 18 | const ( 19 | // [...] 20 | RETURN_VALUE_OBJ = "RETURN_VALUE" 21 | ) 22 | 23 | type ReturnValue struct { 24 | Value Object 25 | } 26 | 27 | func (rv *ReturnValue) Type() ObjectType { return RETURN_VALUE_OBJ } 28 | func (rv *ReturnValue) Inspect() string { return rv.Value.Inspect() } 29 | ``` 30 | 由于这只是另一个对象的包装器,因此这里没有什么令人惊讶的。 object.ReturnValue 的有趣之处在于它何时以及如何使用。 31 | 32 | 以下测试展示了我们在以下上下文中对 return 语句的期望一段Monkey程序: 33 | ```go 34 | // evaluator/evaluator_test.go 35 | 36 | func TestReturnStatements(t *testing.T) { 37 | tests := []struct { 38 | input string 39 | expected int64 40 | }{ 41 | {"return 10;", 10}, 42 | {"return 10; 9;", 10}, 43 | {"return 2 * 5; 9;", 10}, 44 | {"9; return 2 * 5; 9;", 10}, 45 | } 46 | 47 | for _, tt := range tests { 48 | evaluated := testEval(tt.input) 49 | testIntegerObject(t, evaluated, tt.expected) 50 | } 51 | } 52 | ``` 53 | 为了让这些测试通过,我们必须更改已有的 evalStatements 函数,并将 *ast.ReturnStatement 的 case 分支添加到 Eval: 54 | ```go 55 | // evaluator/evaluator.go 56 | 57 | func Eval(node ast.Node) object.Object { 58 | // [...] 59 | case *ast.ReturnStatement: 60 | val := Eval(node.ReturnValue) 61 | return &object.ReturnValue{Value: val} 62 | // [...] 63 | } 64 | 65 | func evalStatements(stmts []ast.Statement) object.Object { 66 | var result object.Object 67 | 68 | for _, statement := range stmts { 69 | result = Eval(statement) 70 | 71 | if returnValue, ok := result.(*object.ReturnValue); ok { 72 | return returnValue.Value 73 | } 74 | } 75 | return result 76 | } 77 | ``` 78 | 此更改的第一部分是对 *ast.ReturnValue 的评估,我们在其中评估与 return 语句关联的表达式。 然后我们将调用 Eval 的结果包装在我们的新 object.ReturnValue 中,以便我们可以跟踪它。 79 | 80 | 在 evalStatements 中,evalProgramStatements 和 evalBlockStatements 使用它来评估一系列语句,我们检查最后的评估结果是否是这样的 object.ReturnValue,如果是,我们停止评估并返回解包的值。 这很重要。 我们不返回 object.ReturnValue,而只返回它包装的值,这是用户期望返回的值。 81 | 82 | 不过有一个问题。 有时我们必须更长时间地跟踪 object.ReturnValues 并且无法在第一次遇到时解开它们的值。 块语句就是这种情况。 看看这个: 83 | ```go 84 | if (10 > 1) { 85 | if (10 > 1) { 86 | return 10; 87 | } 88 | return 1; 89 | } 90 | ``` 91 | 这段程序应该返回10,但随着我们当下的实现,它却是返回1。一小段测试证明了结论: 92 | ```go 93 | // evaluator/evaluator_test.go 94 | func TestReturnStatements(t *testing.T) { 95 | tests := []struct { 96 | input string 97 | expected int64 98 | }{ 99 | // [...] 100 | { 101 | ` 102 | if (10 > 1) { 103 | if (10 > 1) { 104 | return 10; 105 | } 106 | return 1; 107 | } 108 | `, 109 | 10, 110 | }, 111 | } 112 | // [...] 113 | } 114 | ``` 115 | 这段测试有明确的失败信息: 116 | ```go 117 | $ go test ./evaluator 118 | --- FAIL: TestReturnStatements (0.00s) 119 | evaluator_test.go:159: object has wrong value. got=1, want=10 120 | FAIL 121 | FAIL monkey/evaluator 0.007s 122 | ``` 123 | 我打赌你已经发现了我们当下这段程序实现的问题所在。但是如果你想让我把它说出来,它来了:如果我们有嵌套的块语句(这在 Monkey 程序中是完全合法的!)我们不能第一眼看到 object.ReturnValue 的值,因为我们需要 进一步跟踪它,以便我们可以在最外面的块语句中停止执行。 124 | 125 | 非嵌套块语句适用于我们当前的实现。 但是为了让嵌套语句工作,我们必须做的第一件事就是接受我们不能重用我们的 evalStatements 函数来评估块语句。 这就是为什么我们要将其重命名为 evalProgram 并使其不那么通用。 126 | ```go 127 | // evaluator/evaluator.go 128 | func Eval(node ast.Node) object.Object { 129 | // [...] 130 | case *ast.Program: 131 | return evalProgram(node) 132 | // [...] 133 | } 134 | func evalProgram(program *ast.Program) object.Object { 135 | var result object.Object 136 | 137 | for _, statement := range program.Statements { 138 | result = Eval(statement) 139 | 140 | if returnValue, ok := result.(*object.ReturnValue); ok { 141 | return returnValue.Value 142 | } 143 | } 144 | return result 145 | } 146 | ``` 147 | 为了评估 *ast.BlockStatement,我们引入了一个名为 evalBlockStatement 的新函数: 148 | ```go 149 | // evaluator/evaluator.go 150 | 151 | func Eval(node ast.Node) object.Object { 152 | // [...] 153 | case *ast.BlockStatement: 154 | return evalBlockStatement(node) 155 | // [...] 156 | } 157 | 158 | func evalBlockStatement(block *ast.BlockStatement) object.Object { 159 | var result object.Object 160 | 161 | for _, statement := range block.Statements { 162 | result = Eval(statement) 163 | 164 | if result != nil && result.Type() == object.RETURN_VALUE_OBJ { 165 | return result 166 | } 167 | } 168 | 169 | return result 170 | } 171 | ``` 172 | 这里我们明确不解包返回值,只检查每个评估结果的 Type()。 如果它是 object.RETURN_VALUE_OBJ,我们只需返回 *object.ReturnValue,而不解包其 .Value,因此它会在可能的外部块语句中停止执行并冒泡到 evalProgram,在那里它最终被解包。 (当我们实现函数调用的评估时,最后一部分会发生变化。) 173 | 174 | 然后测试通过: 175 | ```go 176 | $ go test ./evaluator 177 | ok monkey/evaluator 0.007s 178 | ``` 179 | 执行返回语句。 现在我们绝对不再构建计算器了。而且由于 evalProgram 和 evalBlockStatement 在我们的脑海中仍然如此新鲜,让我们继续研究它们。 180 | 181 | |[< 3.6条件句](3.6.md) | [> 3.8中止!中止!有错误!,或:错误处理](3.8.md)| 182 | |----------|----------| 183 | -------------------------------------------------------------------------------- /contents/3/3.9.md: -------------------------------------------------------------------------------- 1 | # 3.9绑定和环境 2 | 接下来我们将通过添加对 let 语句的支持来为我们的解释器添加绑定。但我们不仅需要支持 let 语句,不,我们还需要支持标识符的评估。 假设我们已经评估了以下代码: 3 | ```js 4 | let x = 5 * 5; 5 | ``` 6 | 仅添加对该语句的评估的支持是不够的。 我们还需要确保在解释了上面的行之后 x 的计算结果为 10。 7 | 8 | 因此,我们在本节中的任务是评估 let 语句和标识符。 我们通过评估 let 语句的值生成表达式并跟踪指定名称下的生成值来评估 let 语句。 为了评估标识符,我们检查是否已经有一个绑定到名称的值。 如果我们这样做,标识符会评估为这个值,如果我们不这样做,我们会返回一个错误。 9 | 10 | 听起来是个好计划? 好的,让我们开始进行一些测试: 11 | ```go 12 | // evaluator/evaluator_test.go 13 | 14 | func TestLetStatements(t *testing.T) { 15 | tests := []struct { 16 | input string 17 | expected int64 18 | }{ 19 | {"let a = 5; a;", 5}, 20 | {"let a = 5 * 5; a;", 25}, 21 | {"let a = 5; let b = a; b;", 5}, 22 | {"let a = 5; let b = a; let c = a + b + 5; c;", 15}, 23 | } 24 | 25 | for _, tt := range tests { 26 | testIntegerObject(t, testEval(tt.input), tt.expected) 27 | } 28 | } 29 | ``` 30 | 测试用例断言这两件事应该起作用:评估 let 语句中产生值的表达式和评估绑定到名称的标识符。 但是我们还需要测试以确保在尝试评估未绑定标识符时会出现错误。 为此,我们可以简单地扩展我们现有的 TestErrorHandling 函数: 31 | ```go 32 | // evaluator/evaluator_test.go 33 | 34 | func TestErrorHandling(t *testing.T) { 35 | tests := []struct { 36 | input string 37 | expectedMessage string 38 | }{ 39 | // [...] 40 | { 41 | "foobar", 42 | "identifier not found: foobar", 43 | }, 44 | } 45 | // [...] 46 | } 47 | ``` 48 | 我们如何让这些测试通过? 显然,我们要做的第一件事就是为 *ast.LetStatement 添加一个新的 case 分支到 Eval。 在这个分支中,我们需要评估 let 语句的表达式,对吗? 所以让我们从那个开始: 49 | ```go 50 | // evaluator/evaluator.go 51 | func Eval(node ast.Node) object.Object { 52 | // [...] 53 | case *ast.LetStatement: 54 | val := Eval(node.Value) 55 | if isError(val) { 56 | return val 57 | } 58 | // Huh? Now what? 59 | // [...] 60 | } 61 | ``` 62 | 评论是对的:现在呢? 我们如何跟踪值? 我们有值,我们也有我们应该绑定它的名称,node.Name.Value。 我们如何将一个与另一个联系起来? 63 | 64 | 这就是所谓的环境发挥作用的地方。 环境是我们用来通过将它们与名称相关联来跟踪价值的东西。 “环境”这个名字是一个经典的名字,在许多其他解释器中使用,尤其是 Lispy 解释器。 但是,尽管名称听起来很复杂,但其核心环境是将字符串与对象相关联的哈希映射。 这正是我们将用于实现的内容。 65 | 66 | 我们将向对象包添加一个新的 Environment 结构。 是的,现在它真的只是地图周围的一个薄包装: 67 | ```go 68 | // object/environment.go 69 | 70 | package object 71 | 72 | func NewEnvironment() *Environment { 73 | s := make(map[string]Object) 74 | return &Environment{store: s} 75 | } 76 | 77 | type Environment struct { 78 | store map[string]Object 79 | } 80 | 81 | func (e *Environment) Get(name string) (Object, bool) { 82 | obj, ok := e.store[name] 83 | return obj, ok 84 | } 85 | 86 | func (e *Environment) Set(name string, val Object) Object { 87 | e.store[name] = val 88 | return val 89 | } 90 | ``` 91 | 让我猜猜你在想什么:为什么不使用地图? 为什么是包装纸? 我保证,一旦我们在下一节开始实现函数和函数调用,一切都会变得有意义。这是我们稍后将建立的基础。 92 | 93 | 事实上, object.Environment 的用法本身是不言自明的。 但是我们如何在 Eval 中使用它呢? 我们如何以及在哪里跟踪环境? 我们通过使它成为一个Eval 的参数: 94 | ```go 95 | // evaluator/evaluator.go 96 | 97 | func Eval(node ast.Node, env *object.Environment) object.Object { 98 | // [...] 99 | } 100 | ``` 101 | 有了这个改变,任何东西都不再编译了,因为我们必须改变对 Eval 的每次调用以利用环境。 不仅是Eval本身对Eval的调用,还有evalProgram、evalIfExpression等函数中的调用。 这比其他任何事情都需要更多的手动编辑器工作,因此我不会通过在此处显示更改列表来使您感到厌烦。 102 | 103 | 当然,在我们的 REPL 和我们的测试套件中对 Eval 的调用也需要使用环境。在 REPL 中,我们使用单一环境: 104 | ```go 105 | // repl/repl.go 106 | func Start(in io.Reader, out io.Writer) { 107 | scanner := bufio.NewScanner(in) 108 | env := object.NewEnvironment() 109 | 110 | for { 111 | // [...] 112 | evaluated := evaluator.Eval(program, env) 113 | if evaluated != nil { 114 | io.WriteString(out, evaluated.Inspect()) 115 | io.WriteString(out, "\n") 116 | } 117 | } 118 | } 119 | ``` 120 | 我们在这里使用的环境 env 在对 Eval 的调用之间持续存在。 如果没有,将值绑定到 REPL 中的名称将没有任何效果。 一旦评估下一行,关联就不会在新环境中。 121 | 122 | 不过,这正是我们在测试套件中想要的。 我们不想为每个测试函数和每个测试用例保留状态。 每次调用 testEval 都应该有一个新的环境,这样我们就不会遇到由测试运行顺序引起的涉及全局状态的奇怪错误。 每次调用 Eval 都会得到一个全新的环境: 123 | ```go 124 | // evaluator/evaluator_test.go 125 | 126 | func testEval(input string) object.Object { 127 | l := lexer.New(input) 128 | p := parser.New(l) 129 | program := p.ParseProgram() 130 | env := object.NewEnvironment() 131 | 132 | return Eval(program, env) 133 | } 134 | ``` 135 | 随着更新的 Eval 调用再次编译测试,我们可以开始让它们通过,这在 *object.Environemnt 可用的情况下并不难。 在 *ast.LetStatement 的 case 分支中,我们可以使用我们已有的名称和值并将它们保存在当前环境中: 136 | ```go 137 | // evaluator/evaluator.go 138 | 139 | func Eval(node ast.Node, env *object.Environment) object.Object { 140 | // [...] 141 | case *ast.LetStatement: 142 | val := Eval(node.Value, env) 143 | if isError(val) { 144 | return val 145 | } 146 | env.Set(node.Name.Value, val) 147 | // [...] 148 | } 149 | ``` 150 | 现在我们在评估 let 语句时向环境添加关联。 但是在评估标识符时,我们也需要将这些值取出来。 这样做也很容易: 151 | ```go 152 | // evaluator/evaluator.go 153 | 154 | func Eval(node ast.Node, env *object.Environment) object.Object { 155 | // [...] 156 | case *ast.Identifier: 157 | return evalIdentifier(node, env) 158 | // [...] 159 | } 160 | func evalIdentifier( 161 | node *ast.Identifier, 162 | env *object.Environment, 163 | ) object.Object { 164 | val, ok := env.Get(node.Value) 165 | if !ok { 166 | return newError("identifier not found: " + node.Value) 167 | } 168 | 169 | return val 170 | } 171 | ``` 172 | evalIdentifier 将在下一节中扩展。 现在它只是检查一个值是否与当前环境中的给定名称相关联。 如果是这种情况,它会返回值,否则报错。 173 | 174 | 看一下: 175 | ```go 176 | $ go test ./evaluator 177 | ok monkey/evaluator 0.007s 178 | ``` 179 | 是的,你是对的,这正是这意味着:我们现在坚定地站在编程语言领域。 180 | ```go 181 | $ go run main.go 182 | Hello mrnugget! This is the Monkey programming language! 183 | Feel free to type in commands 184 | >> let a = 5; 185 | >> let b = a > 3; 186 | >> let c = a * 99; 187 | >> if (b) { 10 } else { 1 }; 188 | 10 189 | >> let d = if (c > a) { 99 } else { 100 }; 190 | >> d 191 | 99 192 | >> d * c * a; 193 | 245025 194 | ``` 195 | |[< 3.8中止! 中止! 有错误!,或:错误处理](3.8.md)|[> 3.10函数和函数调用](3.10.md)| 196 | |-|-| -------------------------------------------------------------------------------- /contents/1/1.4.md: -------------------------------------------------------------------------------- 1 | # 1.4拓展我们的Token集和词法分析器 2 | 为了避免以后编写解析器时在包之间跳转的需要,我们需要去拓展我们的词法分析器以便于他能够识别更多Monkey语言和输出更多的Tokens。因此,在本节中,我们将添加对==,!,!=,-,、,*,<,>和关键字true,false,if,else和return的支持。 3 | 4 | 我们需要添加的新Tokens,建立和输出可以被分类作为以下三种之一:一个字符的Token(例如,-),两个字符的token(例如==)和关键字token(例如 return)。我们也知道如何处理一个字符tokens和关键字tokens,因此在额外添加对两个字符token之前,我们首先添加对这两者的支持。。 5 | 6 | 添加对-,/,*,<和>的支持是微不足道的。我们需要做的第一件事当然是修改我们的测试用例在lexer/lexer_test.go中输入以包含这些字符。就像以前我们做过的一样。在本章节随附的代码中,您还可以找到扩展测试表,我不会再本章节的其余部分展示,以节省空间并避免让你感到无聊。 7 | ```go 8 | // lexer/lexer_test.go 9 | 10 | func TestNextToken(t *testing*T) { 11 | input := `let five = 5; 12 | let ten = 10; 13 | 14 | let add = fn(x,y) { 15 | x + y; 16 | }; 17 | 18 | let result = add(five,ten); 19 | !-/*5; 20 | 5 < 10 > 5; 21 | ` 22 | 23 | // [...] 24 | } 25 | ``` 26 | 27 | 注意,虽然输入看起来像真实的Monkey语言片段,但有些行的确没有意义,像`!-/*5`这样的胡言乱语。没关系,词法分析器的工作不是告诉我们代码是否有意义,有效或包含错误。那是以后的阶段。词法分析器应该只将此输入转换为tokens。出于这个原因,我为词法分析器编写的测试用例还改了所有的tokens并且还尝试引发一对一的错误,文件末尾的边缘情况、换行符处理、多位数字解析等。这就是为什么“代码”看起来像胡言乱语。 28 | 29 | 运行测试我们得到undefined:errors,因为测试包含对未定义TokenTypes的引用,为了修复他们,我们添加以下常量给token/token.go: 30 | ```go 31 | //token/token.go 32 | 33 | const ( 34 | //[...] 35 | 36 | // Operators 37 | ASSIGN = "=" 38 | PLUS = "+" 39 | MINUS = "-" 40 | BANG = "!" 41 | ASTERISK = "*" 42 | SLASH = "/" 43 | 44 | LT = "<" 45 | GT = ">" 46 | 47 | //[...] 48 | ) 49 | ``` 50 | 51 | 随着我们添加了新常量,测试仍然失败,因为我们没有返回具有预期TokenType的Tokens。 52 | 53 | ```go 54 | $ go test ./lexer 55 | --- FAIL: TestNextToken (0.00s) 56 | lexer_test.go:84: tests[36] - tokentype wrong. expected="!", got="ILLEGAL" 57 | FAIL 58 | FAIL monkey/lexer 0.007s 59 | ``` 60 | 将这些测试从失败变为通过,需要我们在Lexer的NextToken()方法中拓展我们的switch语句: 61 | ```go 62 | // lexer/lexer.go 63 | 64 | func(l *Lexer) NextToken() token.Token { 65 | // [... 66 | case'=': 67 | tok = newToken(token.ASSIGN,l.ch) 68 | case '+': 69 | tok = newToken(token.PLUS, l.ch) 70 | case '-': 71 | tok = newToken(token.MINUS, l.ch) 72 | case'!': 73 | tok = newToken(token.BANG,l.ch) 74 | case '/': 75 | tok = newToken(token.SLASH, l.ch) 76 | case '*': 77 | tok = newToken(token.ASTERISK, l.ch) 78 | case '<': 79 | tok = newToken(token.LT, l.ch) 80 | case '>': 81 | tok = newToken(token.GT, l.ch) 82 | case ';': 83 | tok = newToken(token.SEMICOLON, l.ch) 84 | case ',': 85 | tok = newToken(token.COMMA, l.ch) 86 | // [...] 87 | } 88 | ``` 89 | 现在我们添加了tokens,并且对switch语句的case进行了重新排序以反映token/token.go中的常量结构。这个小改动让我们的测试通过了: 90 | ```go 91 | $ go test ./lexer 92 | ok monkey/lexer 0.007s 93 | ``` 94 | 新的一个字符的tokens已经成功添加了。下一步:添加新的关键字true,false,if,else和return。 95 | 96 | 同样,第一步是拓展我们测试中的输入以包含这些新的关键字。TestNextToken里新的输入看起来是这样的: 97 | ```go 98 | // lexer/lexer_test.go 99 | 100 | func TestNextToken(t *testing*T) { 101 | input := `let five = 5; 102 | let ten = 10; 103 | 104 | let add = fn(x,y) { 105 | x + y; 106 | }; 107 | 108 | let result = add(five,ten); 109 | !-/*5; 110 | 5 < 10 > 5; 111 | if (5 < 10) { 112 | return true; 113 | } else { 114 | return flase; 115 | } 116 | ` 117 | // [...] 118 | ``` 119 | 由于测试期望引用的新关键字未定义,测试甚至没有编译。修复此内容,意味着只添加新常量,在这种情况下,将关键字添加到查找表LookupIdent()。 120 | ```go 121 | // token/token.go 122 | 123 | const ( 124 | // [...] 125 | 126 | // Keywords 127 | FUNCTION = "FUNCTION" 128 | LET = "LET" 129 | TRUE = "TRUE" 130 | FALSE = "FALSE" 131 | IF = "IF" 132 | ELSE = "ELSE" 133 | RETURN = "RETURN" 134 | ) 135 | 136 | var keywords = map[string]TokenType{ 137 | "fn": FUNCTION, 138 | "let": LET, 139 | "true": TRUE, 140 | "false": FALSE, 141 | "if": IF, 142 | "else": ELSE, 143 | "return": RETURN, 144 | } 145 | ``` 146 | 事实证明,我们不仅通过修复对未定义变量的引用来修复编译错误,我们甚至还通过了测试: 147 | ```go 148 | $ go test ./lexer 149 | ok monkey/lexer 0.0 150 | ``` 151 | 词法分析器现在认识了新的关键字,必要的变化是微不足道的,易于去预测和改变。我想说的是适当地拍拍背,我们做的不错! 152 | 153 | 但是在我们到下一个章节并且开始我们的解析器之前,我们仍然需要拓展词法分析器为了它能够识别两个字符的tokens。我们想要支持的tokens看起来像源代码里的:==和!=。 154 | 155 | 首先瞥一眼你可能会认为,为什么不添加新的case在我们的switch语句里并且和之前一起做?因为我们的switch语句将当前字符l.ch作为表达式与case比较,我们不能只添加新的case,例如case "=="-编译器不会让我们这样做。我们无法将我们的l.ch字节与诸如"=="之类的字符串进行比较。 156 | 157 | 我们可以做的是重用现有的'='和'!'分支并扩展它们。所以我们要做的是从输入中向前看,然后确定是否未=或==返回一个token。再一次扩展了lexer/lexer_test.go的输入后,它看起来像这样: 158 | ```go 159 | // lexer/lexer_test.go 160 | 161 | func TestNextToken(t *testing*T) { 162 | input := `let five = 5; 163 | let ten = 10; 164 | 165 | let add = fn(x,y) { 166 | x + y; 167 | }; 168 | 169 | let result = add(five,ten); 170 | !-/*5; 171 | 5 < 10 > 5; 172 | if (5 < 10) { 173 | return true; 174 | } else { 175 | return flase; 176 | } 177 | 178 | 10 == 10; 179 | 10 != 9; 180 | ` 181 | // [...] 182 | ``` 183 | 184 | 在我们开始我们switch语句之前,我们需要添加新的辅助函数定义peekChar给*Lexer: 185 | ```go 186 | // lexer/lexer.go 187 | 188 | func (l *Lexer) peekChar() byte { 189 | if l.readPosition >= len(l.input) { 190 | return 0 191 | } else { 192 | return l.input[l.readPosition] 193 | } 194 | } 195 | ``` 196 | 197 | peekChar和readChar一样简单,除了它不增加l.positon和l.readPosition。我们只想在输入中“窥视”而不是在其中移动,所以我们直到调用readChar会返回什么。大多数语法分析器和解析器都有这样一个向前看的“窥视”功能,大多数情况下它只返回紧邻的下一个字符。解析不同语言的难度通常归结为你必须向前看(或向后看!)理解它的源代码。 198 | 199 | 随着peekChar添加,使用更新的测试输入的代码不能编译。当然,由于我们在测试中引用了未定义的token常量。修复它,事实证明,很容易: 200 | ```go 201 | // token/token.go 202 | 203 | const ( 204 | // [...] 205 | EQ = "==" 206 | NOT_EQ = "!=!" 207 | 208 | //[...] 209 | ) 210 | ``` 211 | 修复了词法分析器测试中对token.EQ和token.NOT_EQ引用后,运行 212 | go test 现在会返回正确的失败信息: 213 | ```go 214 | $ go test ./lexer 215 | --- FAIL: TestNextToken (0.00s) 216 | lexer_test.go:118: tests[66] - tokentype wrong. expected="==", got="=" 217 | FAIL 218 | FAIL monkey/lexer 0.007s 219 | ``` 220 | 当词法分析器在输入中遇到==时,它会创建两个token.ASSIGN tokens代替一个token.EQ token。解决方法是使用我们的新peekChar方法。在switch分支中在'='和'!'中,我们向前看。如果下一个令牌是也是=我们创建token.EQ或一个token.NOT_EQ token。 221 | ```go 222 | // lexer/lexer.go 223 | 224 | func (l *Lexer) NextToken() token.Token { 225 | // [...] 226 | switch l.ch { 227 | case '=': 228 | if l.peekChar() == '=' { 229 | ch := l.ch 230 | l.readChar() 231 | tok = token.Token{Type: token.EQ, Literal: string(ch) + string(l.ch)} 232 | } else { 233 | tok = newToken(token.ASSIGN, l.ch) 234 | } 235 | // [...] 236 | case '!': 237 | if l.peekChar() == '=' { 238 | ch := l.ch 239 | l.readChar() 240 | tok = token.Token{Type: token.NOT_EQ, Literal: string(ch) + string(l.ch)} 241 | } else { 242 | tok = newToken(token.BANG, l.ch) 243 | } 244 | // [...] 245 | } 246 | } 247 | ``` 248 | 249 | 请注意,我们在再一次调用l.readChar()之前将l.ch保存在局部变量中。这样我们就不会丢失当前字符并且可以安全地推进词法分析器,因此使它带有l.position和l.readPosition的NextToken()处于正确状态。如果我们要开始在Monkey中支持更多的两个字符的标记,我们可能应该在一个名为makeTwoCharToken的方法中抽象出行为,如果它找到正确的标记,它会查看并前进。因为那两个分支看起来非常相似。现在虽然==和!=是Monkey中唯一的两个字符标记,所以让我们保持原样并再次运行我们的测试以确保它有效: 250 | ```go 251 | $ go test ./lexer 252 | ok monkey/lexer 0. 253 | ``` 254 | 它们通过了,我们做到了!词法分析器现在可以生产扩展的tokens,我们已经准备就绪写我们的解析器。但是在我们写之前,让我们在接下来的章节中铺设另一块基石... 255 | 256 | |[< 1.3词法分析器](1.3.md)|[> 1.5开始一个REPL](1.5.md)| 257 | |-|-| -------------------------------------------------------------------------------- /contents/4/4.3.md: -------------------------------------------------------------------------------- 1 | # 4.3内置函数 2 | 在本节中,我们将向解释器添加内置函数。 它们被称为“内置”,因为它们不是由解释器的用户定义的,也不是 Monkey 代码——它们直接内置在解释器中,内置在语言本身中。 3 | 4 | 这些内置函数被我们定义,用Go,并将 Monkey 的世界与我们的解释器实现的世界联系起来。许多语言实现都提供了这样的功能,以便为语言的用户提供语言“内部”没有提供的功能。 5 | 6 | 这是一个例子:一个返回当前时间的函数。 为了获得当前时间,可以询问内核(或另一台计算机等)。 询问和与内核交谈通常是通过称为系统调用的东西来完成的。 但是,如果编程语言不提供用户自己进行此类系统调用,那么语言实现,无论是编译器还是解释器,都必须提供一些东西来代替用户进行这些系统调用。 7 | 8 | 因此,我们将要添加的内置函数再次由解释器的实现者定义。 解释器的用户可以调用它们,但我们定义它们。 这些功能可以做什么,我们保持开放。 它们唯一的限制是它们需要接受零个或多个 object.Object 作为参数并返回一个 object.Object。 9 | ```go 10 | // object/object.go 11 | 12 | type BuiltinFunction func(args ...Object) Object 13 | ``` 14 | 这是可调用 Go 函数的类型定义。 但是由于我们需要将这些内置函数提供给我们的用户,我们需要将它们放入我们的对象系统中。 我们通过包装它们来做到这一点: 15 | ```go 16 | // object/object.go 17 | 18 | const ( 19 | // [...] 20 | BUILTIN_OBJ = "BUILTIN" 21 | ) 22 | 23 | type Builtin struct { 24 | Fn BuiltinFunction 25 | } 26 | func (b *Builtin) Type() ObjectType { return BUILTIN_OBJ } 27 | func (b *Builtin) Inspect() string { return "builtin function" } 28 | ``` 29 | 没有什么可反对的。如您所见,内置。 这显然只是一个包装。 但是结合 object.BuiltinFunction 就足以让我们开始了。 30 | ## LEN 31 | 我们要添加到解释器中的第一个内置函数是 len。 它的工作是返回字符串中的字符数。 不可能将这个函数定义为 Monkey 的用户。 这就是为什么我们需要内置它。 我们想要从 len 得到的是: 32 | ```go 33 | >> len("Hello World!") 34 | 12 35 | >> len("") 36 | 0 37 | >> len("Hey Bob, how ya doin?") 38 | 21 39 | ``` 40 | 我认为这使得 len 背后的想法非常清楚。 事实上很清楚,我们可以很容易地为它编写一个测试: 41 | ```go 42 | // evaluator/evaluator_test.go 43 | func TestBuiltinFunctions(t *testing.T) { 44 | tests := []struct { 45 | input string 46 | expected interface{} 47 | }{ 48 | {`len("")`, 0}, 49 | {`len("four")`, 4}, 50 | {`len("hello world")`, 11}, 51 | {`len(1)`, "argument to `len` not supported, got INTEGER"}, 52 | {`len("one", "two")`, "wrong number of arguments. got=2, want=1"}, 53 | } 54 | 55 | for _, tt := range tests { 56 | evaluated := testEval(tt.input) 57 | 58 | switch expected := tt.expected.(type) { 59 | case int: 60 | testIntegerObject(t, evaluated, int64(expected)) 61 | case string: 62 | errObj, ok := evaluated.(*object.Error) 63 | if !ok { 64 | t.Errorf("object is not Error. got=%T (%+v)", 65 | evaluated, evaluated) 66 | continue 67 | } 68 | if errObj.Message != expected { 69 | t.Errorf("wrong error message. expected=%q, got=%q", 70 | expected, errObj.Message) 71 | } 72 | } 73 | } 74 | } 75 | ``` 76 | 所以这里我们有几个测试用例来运行 len :一个空字符串、一个普通字符串和一个包含空格的字符串。 字符串中是否有空格真的不重要,但你永远不会知道,所以我把测试用例放进去。 最后两个测试用例更有趣:我们要确保 len 返回一个 *object.Error 当使用整数或错误数量的参数调用时。 如果我们运行测试,我们可以看到调用 len 会给我们一个错误,但不是我们测试用例中预期的错误: 77 | ```go 78 | $ go test ./evaluator 79 | --- FAIL: TestBuiltinFunctions (0.00s) 80 | evaluator_test.go:389: object is not Integer. got=*object.Error\ 81 | (&{Message:identifier not found: len}) 82 | evaluator_test.go:389: object is not Integer. got=*object.Error\ 83 | (&{Message:identifier not found: len}) 84 | evaluator_test.go:389: object is not Integer. got=*object.Error\ 85 | (&{Message:identifier not found: len}) 86 | evaluator_test.go:371: wrong error message.\ 87 | expected="argument to `len` not supported, got INTEGER",\ 88 | got="identifier not found: len" 89 | FAIL 90 | FAIL monkey/evaluator 0.007s 91 | ``` 92 | len 无法找到,考虑到我们还没有定义它,这并不令人费解。 93 | 94 | 为了做到这一点,我们要做的第一件事是提供一种可以找到内置函数的方法。 一种选择是将它们添加到传递给 Eval 的顶级 object.Environment。 但相反,我们将保留一个单独的内置函数环境: 95 | ```go 96 | // evaluator/builtins.go 97 | 98 | package evaluator 99 | 100 | import "monkey/object" 101 | 102 | var builtins = map[string]*object.Builtin{ 103 | "len": &object.Builtin{ 104 | Fn: func(args ...object.Object) object.Object { 105 | return NULL 106 | }, 107 | }, 108 | } 109 | ``` 110 | 为了利用这一点,我们需要编辑我们的 evalIdentifier 函数以在给定的标识符未绑定到当前环境中的值时查找内置函数作为回退: 111 | ```go 112 | // evaluator/evaluator.go 113 | 114 | func evalIdentifier( 115 | node *ast.Identifier, 116 | env *object.Environment, 117 | ) object.Object { 118 | if val, ok := env.Get(node.Value); ok { 119 | return val 120 | } 121 | 122 | if builtin, ok := builtins[node.Value]; ok { 123 | return builtin 124 | } 125 | 126 | return newError("identifier not found: " + node.Value) 127 | } 128 | ``` 129 | 所以现在查找len标识符的时候找到了len,调用还不行: 130 | ```go 131 | $ go run main.go 132 | Hello mrnugget! This is the Monkey programming language! 133 | Feel free to type in commands 134 | >> len() 135 | ERROR: not a function: BUILTIN 136 | >> 137 | ``` 138 | 运行测试会给我们同样的错误。 我们需要教我们的 applyFunction 关于 *object.Builtin 和 object.BuiltinFunction: 139 | ```go 140 | // evaluator/evaluator.go 141 | func applyFunction(fn object.Object, args []object.Object) object.Object { 142 | switch fn := fn.(type) { 143 | case *object.Function: 144 | extendedEnv := extendFunctionEnv(fn, args) 145 | evaluated := Eval(fn.Body, extendedEnv) 146 | return unwrapReturnValue(evaluated) 147 | 148 | case *object.Builtin: 149 | return fn.Fn(args...) 150 | 151 | default: 152 | return newError("not a function: %s", fn.Type()) 153 | } 154 | } 155 | ``` 156 | 除了移动现有的行,这里的变化是添加了 case *object.Builtin 分支,我们称之为 object.BuiltinFunction。 这样做就像使用一样简单 157 | args 切片作为参数并调用函数。 158 | 159 | 需要注意的是,我们在调用内置函数时不需要 unwrapReturnValue。 那是因为我们从不从这些函数中返回 *object.ReturnValue。 160 | 161 | 现在,测试正确地抱怨调用 len 时返回 NULL: 162 | ```go 163 | $ go test ./evaluator 164 | --- FAIL: TestBuiltinFunctions (0.00s) 165 | evaluator_test.go:389: object is not Integer. got=*object.Null (&{}) 166 | evaluator_test.go:389: object is not Integer. got=*object.Null (&{}) 167 | evaluator_test.go:389: object is not Integer. got=*object.Null (&{}) 168 | evaluator_test.go:366: object is not Error. got=*object.Null (&{}) 169 | evaluator_test.go:366: object is not Error. got=*object.Null (&{}) 170 | FAIL 171 | FAIL monkey/evaluator 0.007s 172 | ``` 173 | 这意味着调用 len 是有效的! 只是它只返回NULL。 但是解决这个问题就像编写任何其他 Go 函数一样简单: 174 | ```go 175 | // evaluator/builtins.go 176 | 177 | import ( 178 | "monkey/object" 179 | "unicode/utf8" 180 | ) 181 | 182 | var builtins = map[string]*object.Builtin{ 183 | "len": &object.Builtin{ 184 | Fn: func(args ...object.Object) object.Object { 185 | if len(args) != 1 { 186 | return newError("wrong number of arguments. got=%d, want=1", 187 | len(args)) 188 | } 189 | 190 | switch arg := args[0].(type) { 191 | case *object.String: 192 | return &object.Integer{Value: int64(len(arg.Value))} 193 | default: 194 | return newError("argument to `len` not supported, got %s", 195 | args[0].Type()) 196 | } 197 | }, 198 | }, 199 | } 200 | ``` 201 | 这个函数最重要的部分是调用 Go 的 len 并返回一个新分配的 object.Integer。 除此之外,我们还进行了错误检查,以确保我们不能使用错误数量的参数或不支持类型的参数调用此函数。 唉,我们的测试通过了: 202 | ```go 203 | $ go test ./evaluator 204 | ok monkey/evaluator 0.007s 205 | ``` 206 | 这意味着我们可以在 REPL 中试驾 len: 207 | ```go 208 | $ go run main.go 209 | Hello mrnugget! This is the Monkey programming language! 210 | Feel free to type in commands 211 | >> len("1234") 212 | 4 213 | >> len("Hello World!") 214 | 12 215 | >> len("Woooooohooo!", "len works!!") 216 | ERROR: wrong number of arguments. got=2, want=1 217 | >> len(12345) 218 | ERROR: argument to `len` not supported, got INTEGER 219 | ``` 220 | 完美的! 我们的第一个内置函数可以运行并准备就绪。 221 | |[< 4.2字符串](4.2.md)|[> 4.4数组](4.4.md)| 222 | |-|-| 223 | -------------------------------------------------------------------------------- /contents/3/3.8.md: -------------------------------------------------------------------------------- 1 | # 3.8 - 中止! 中止! 有错误!,或:错误处理 2 | 还记得我们之前返回的所有 NULL 并且我说你不应该担心,我们会回来的吗? 我们到了。 是时候在 Monkey 中实现一些真正的错误处理了,以免为时已晚,我们不得不退缩太多。 诚然,我们必须稍微退一步并纠正以前的代码,但不多。 我们没有在解释器中首先实现错误处理,因为老实说,我认为首先实现表达式比错误处理有趣得多。 但是我们现在需要添加它,否则在不久的将来调试和使用我们的解释器会变得太麻烦。 3 | 4 | 首先,让我们定义“真正的错误处理”是什么意思。 它不是用户定义的异常。 这是内部错误处理。 错误的操作符、不受支持的操作以及在执行过程中可能出现的其他用户或内部错误的错误。 5 | 6 | 至于此类错误的实现:这听起来可能很奇怪,但错误处理的实现方式几乎与处理 return 语句的方式相同。 这种相似性的原因很容易找到:errors 和 return 语句都停止了对一系列语句的评估。 7 | 8 | 我们需要的第一件事是一个错误对象: 9 | ```go 10 | // object/object.go 11 | 12 | const ( 13 | // [...] 14 | ERROR_OBJ = "ERROR" 15 | ) 16 | 17 | type Error struct { 18 | Message string 19 | } 20 | 21 | func (e *Error) Type() ObjectType { return ERROR_OBJ } 22 | func (e *Error) Inspect() string { return "ERROR: " + e.Message } 23 | ``` 24 | 如您所见, object.Error 非常非常简单。 它只包装一个用作错误消息的字符串。 在生产就绪的解释器中,我们希望将堆栈跟踪附加到此类错误对象,添加其来源的行号和列号,并提供的不仅仅是一条消息。 这并不难,只要行号和列号由词法分析器附加到标记上。 由于我们的词法分析器没有这样做,为了简单起见,我们只使用了一个错误消息,它通过给我们一些反馈和停止执行仍然对我们有很大帮助。 25 | 26 | 我们现在将在几个地方添加对错误的支持。 稍后,随着我们解释器能力的增强,我们将在适当的地方添加更多。 现在,这个测试函数显示了我们期望错误处理做什么: 27 | ```go 28 | // evaluator/evaluator_test.go 29 | func TestErrorHandling(t *testing.T) { 30 | tests := []struct { 31 | input string 32 | expectedMessage string 33 | }{ 34 | { 35 | "5 + true;", 36 | "type mismatch: INTEGER + BOOLEAN", 37 | }, 38 | { 39 | "5 + true; 5;", 40 | "type mismatch: INTEGER + BOOLEAN", 41 | }, 42 | { 43 | "-true", 44 | "unknown operator: -BOOLEAN", 45 | }, 46 | { 47 | "true + false;", 48 | "unknown operator: BOOLEAN + BOOLEAN", 49 | }, 50 | { 51 | "5; true + false; 5", 52 | "unknown operator: BOOLEAN + BOOLEAN", 53 | }, 54 | { 55 | "if (10 > 1) { true + false; }", 56 | "unknown operator: BOOLEAN + BOOLEAN", 57 | }, 58 | { 59 | ` 60 | if (10 > 1) { 61 | if (10 > 1) { 62 | return true + false; 63 | } 64 | return 1; 65 | } 66 | `, 67 | "unknown operator: BOOLEAN + BOOLEAN", 68 | }, 69 | } 70 | 71 | for _, tt := range tests { 72 | evaluated := testEval(tt.input) 73 | 74 | errObj, ok := evaluated.(*object.Error) 75 | if !ok { 76 | t.Errorf("no error object returned. got=%T(%+v)", 77 | evaluated, evaluated) 78 | continue 79 | } 80 | 81 | if errObj.Message != tt.expectedMessage { 82 | t.Errorf("wrong error message. expected=%q, got=%q", 83 | tt.expectedMessage, errObj.Message) 84 | } 85 | } 86 | } 87 | ``` 88 | 当我们运行测试时,我们又遇到了我们的老朋友 NULL: 89 | ```go 90 | $ go test ./evaluator 91 | --- FAIL: TestErrorHandling (0.00s) 92 | evaluator_test.go:193: no error object returned. got=*object.Null(&{}) 93 | evaluator_test.go:193: no error object returned.\ 94 | got=*object.Integer(&{Value:5}) 95 | evaluator_test.go:193: no error object returned. got=*object.Null(&{}) 96 | evaluator_test.go:193: no error object returned. got=*object.Null(&{}) 97 | evaluator_test.go:193: no error object returned.\ 98 | got=*object.Integer(&{Value:5}) 99 | evaluator_test.go:193: no error object returned. got=*object.Null(&{}) 100 | evaluator_test.go:193: no error object returned.\ 101 | got=*object.Integer(&{Value:10}) 102 | FAIL 103 | FAIL monkey/evaluator 0.007s 104 | ``` 105 | 但也有意想不到的 *object.Integers。 这是因为这些测试用例实际上断言了两件事:错误是为不受支持的操作创建的,错误会阻止任何进一步的评估。 当测试因返回 *object.Integer 而失败时,评估没有正确停止。 106 | 107 | 创建错误并在 Eval 中传递它们很容易。 我们只需要一个辅助函数来帮助我们创建新的 *object.Errors 并在我们认为应该时返回它们: 108 | ```go 109 | // evaluator/evaluator.go 110 | func newError(format string, a ...interface{}) *object.Error { 111 | return &object.Error{Message: fmt.Sprintf(format, a...)} 112 | } 113 | ``` 114 | 这个 newError 函数在我们之前不知道该做什么的每个地方都有用处,而是返回 NULL: 115 | ```go 116 | // evaluator/evaluator.go 117 | 118 | func evalPrefixExpression(operator string, right object.Object) object.Object { 119 | switch operator { 120 | // [...] 121 | default: 122 | return newError("unknown operator: %s%s", operator, right.Type()) 123 | } 124 | } 125 | 126 | func evalInfixExpression( 127 | operator string, 128 | left, right object.Object, 129 | ) object.Object { 130 | switch { 131 | // [...] 132 | case left.Type() != right.Type(): 133 | return newError("type mismatch: %s %s %s", 134 | left.Type(), operator, right.Type()) 135 | default: 136 | return newError("unknown operator: %s %s %s", 137 | left.Type(), operator, right.Type()) 138 | } 139 | } 140 | 141 | func evalMinusPrefixOperatorExpression(right object.Object) object.Object { 142 | if right.Type() != object.INTEGER_OBJ { 143 | return newError("unknown operator: -%s", right.Type()) 144 | } 145 | // [...] 146 | } 147 | 148 | func evalIntegerInfixExpression( 149 | operator string, 150 | left, right object.Object, 151 | ) object.Object { 152 | // [...] 153 | switch operator { 154 | // [...] 155 | default: 156 | return newError("unknown operator: %s %s %s", 157 | left.Type(), operator, right.Type()) 158 | } 159 | } 160 | ``` 161 | 通过这些更改,失败的测试用例数量减少到只有两个: 162 | ```go 163 | $ go test ./evaluator 164 | --- FAIL: TestErrorHandling (0.00s) 165 | evaluator_test.go:193: no error object returned.\ 166 | got=*object.Integer(&{Value:5}) 167 | evaluator_test.go:193: no error object returned.\ 168 | got=*object.Integer(&{Value:5}) 169 | FAIL 170 | FAIL monkey/evaluator 0.007s 171 | ``` 172 | 该输出告诉我们创建错误不会造成问题,但停止评估仍然存在。 我们已经知道去哪里看,不是吗? 是的,没错:evalProgram 和 evalBlockStatement。 这是两个函数的全部内容,新增了对错误处理的支持: 173 | ```go 174 | // evaluator/evaluator.go 175 | 176 | func evalProgram(program *ast.Program) object.Object { 177 | var result object.Object 178 | 179 | for _, statement := range program.Statements { 180 | result = Eval(statement) 181 | 182 | switch result := result.(type) { 183 | case *object.ReturnValue: 184 | return result.Value 185 | case *object.Error: 186 | return result 187 | } 188 | } 189 | return result 190 | } 191 | 192 | func evalBlockStatement(block *ast.BlockStatement) object.Object { 193 | var result object.Object 194 | 195 | for _, statement := range block.Statements { 196 | result = Eval(statement) 197 | 198 | if result != nil { 199 | rt := result.Type() 200 | if rt == object.RETURN_VALUE_OBJ || rt == object.ERROR_OBJ { 201 | return result 202 | } 203 | } 204 | } 205 | return result 206 | } 207 | ``` 208 | 做到了。 评估在正确的地方停止,测试现在通过: 209 | ```go 210 | $ go test ./evaluator 211 | ok monkey/evaluator 0.010s 212 | ``` 213 | 我们还需要做最后一件事。 每当我们在 Eval 内部调用 Eval 时,我们都需要检查错误,以防止错误被传递,然后在远离它们的源头的地方冒泡: 214 | ```go 215 | // evaluator/evaluator.go 216 | 217 | func isError(obj object.Object) bool { 218 | if obj != nil { 219 | return obj.Type() == object.ERROR_OBJ 220 | } 221 | return false 222 | } 223 | 224 | func Eval(node ast.Node) object.Object { 225 | switch node := node.(type) { 226 | 227 | // [...] 228 | case *ast.ReturnStatement: 229 | val := Eval(node.ReturnValue) 230 | if isError(val) { 231 | return val 232 | } 233 | return &object.ReturnValue{Value: val} 234 | 235 | // [...] 236 | case *ast.PrefixExpression: 237 | right := Eval(node.Right) 238 | if isError(right) { 239 | return right 240 | } 241 | return evalPrefixExpression(node.Operator, right) 242 | 243 | case *ast.InfixExpression: 244 | left := Eval(node.Left) 245 | if isError(left) { 246 | return left 247 | } 248 | 249 | right := Eval(node.Right) 250 | if isError(right) { 251 | return right 252 | } 253 | 254 | return evalInfixExpression(node.Operator, left, right) 255 | // [...] 256 | } 257 | 258 | func evalIfExpression(ie *ast.IfExpression) object.Object { 259 | condition := Eval(ie.Condition) 260 | if isError(condition) { 261 | return condition 262 | } 263 | // [...] 264 | } 265 | ``` 266 | 就是这样。 错误处理到位。 267 | |[< 3.7return语句](3.7.md)|[> 3.9绑定和环境](3.9.md)| 268 | |-|-| 269 | -------------------------------------------------------------------------------- /contents/4/4.2.md: -------------------------------------------------------------------------------- 1 | # 4.2字符串 2 | 在 Monkey 中,字符串是一个字符序列。 它们是一流的值,可以绑定到标识符,在函数调用中用作参数并由函数返回。 它们看起来就像许多其他编程语言中的字符串:用双引号括起来的字符。 3 | 4 | 除了数据类型本身,在本节中,我们还将通过支持字符串的中缀运算符 + 来添加对字符串连接的支持。 5 | 6 | 最后,我们将能够做到这一点: 7 | ```go 8 | $ go run main.go 9 | Hello mrnugget! This is micro, your own programming language! 10 | Feel free to type in commands 11 | >> let firstName = "Thorsten"; 12 | >> let lastName = "Ball"; 13 | >> let fullName = fn(first, last) { first + " " + last }; 14 | >> fullName(firstName, lastName); 15 | Thorsten Ball 16 | ``` 17 | ## 在我们的词法分析器中支持字符串 18 | 我们要做的第一件事是向词法分析器添加对字符串文字的支持。 字符串的基本结构是这样的: 19 | ```go 20 | "" 21 | ``` 22 | 这并不太难,对吧? 由双引号括起来的字符序列。 23 | 24 | 我们想要从词法分析器中得到的是每个字符串文字的单个标记。 因此,在“Hello World”的情况下,我们需要一个token,而不是",Hello,World 和"的token。 字符串文字的单个token使得在我们的解析器中处理它们变得更加容易,我们将大部分工作移到词法分析器中的一个小方法中。 25 | 26 | 当然,使用多个token的方法也是有效的,并且在某些情况下/解析器中可能是有益的。 我们可以使用 " 围绕 token.IDENT token。但在我们的例子中,我们将镜像我们已经拥有的 token.INT 整数标记,并在token的 .Literal 字段中携带字符串文字本身。 27 | 28 | 清楚了这一点,是时候再次处理我们的token和词法分析器了。 从第一章开始我们就没有碰过那些,但我相信我们会做得很好。 29 | 30 | 我们需要做的第一件事是在我们的token包中添加一个新的 STRING 令牌类型: 31 | ```go 32 | // token/token.go 33 | 34 | const ( 35 | // [...] 36 | STRING = "STRING" 37 | // [...] 38 | ) 39 | ``` 40 | 有了这个,我们可以为我们的词法分析器添加一个测试用例,以查看是否正确支持字符串。为此,我们只需扩展 TestNextToken 测试函数中的输入: 41 | ```go 42 | // lexer/lexer_test.go 43 | 44 | func TestNextToken(t *testing.T) { 45 | input := `let five = 5; 46 | let ten = 10; 47 | 48 | let add = fn(x, y) { 49 | x + y; 50 | }; 51 | 52 | let result = add(five, ten); 53 | !-/*5; 54 | 5 < 10 > 5; 55 | 56 | if (5 < 10) { 57 | return true; 58 | } else { 59 | return false; 60 | } 61 | 10 == 10; 62 | 10 != 9; 63 | "foobar" 64 | "foo bar" 65 | ` 66 | 67 | tests := []struct { 68 | expectedType token.TokenType 69 | expectedLiteral string 70 | }{ 71 | // [...] 72 | {token.STRING, "foobar"}, 73 | {token.STRING, "foo bar"}, 74 | {token.EOF, ""}, 75 | } 76 | // [...] 77 | } 78 | ``` 79 | 输入现在还有两行包含我们想要转换为标记的字符串文字。有“foobar”来确保字符串文字的词法工作和“foo bar”确保它即使在文字中有空格也能工作。 80 | 81 | 当然,测试失败了,因为我们还没有在 Lexer 中改变任何东西: 82 | ```go 83 | $ go test ./lexer 84 | --- FAIL: TestNextToken (0.00s) 85 | lexer_test.go:122: tests[73] - tokentype wrong. expected="STRING",\ 86 | got="ILLEGAL" 87 | FAIL 88 | FAIL monkey/lexer 0.006s 89 | ``` 90 | 修复测试比您想象的要容易。 我们需要做的就是在我们的词法分析器中的 switch 语句中添加一个 case 分支,并添加一个小的辅助方法: 91 | ```go 92 | // lexer/lexer.go 93 | 94 | func (l *Lexer) NextToken() token.Token { 95 | // [...] 96 | 97 | switch l.ch { 98 | // [...] 99 | case '"': 100 | tok.Type = token.STRING 101 | tok.Literal = l.readString() 102 | // [...] 103 | } 104 | 105 | // [...] 106 | } 107 | 108 | func (l *Lexer) readString() string { 109 | position := l.position + 1 110 | for { 111 | l.readChar() 112 | if l.ch == '"' || l.ch == 0 { 113 | break 114 | } 115 | } 116 | return l.input[position:l.position] 117 | } 118 | ``` 119 | 这些变化真的没有什么神秘之处。 一个新的 case 分支和一个名为 readString 的辅助函数,它调用 readChar 直到遇到双引号结束或输入的结尾。 120 | 121 | 如果你觉得这太简单了,可以随意让 readString 报告错误,而不是在到达输入末尾时简单地返回。 或者您可以添加对字符转义的支持,以便像 `"hello \"world\""`、`"hello\n world"`和`"hello\t\t\tworld"` 这样的字符串文字工作。 122 | 123 | 同时,我们的测试通过了: 124 | ```go 125 | $ go test ./lexer 126 | ok monkey/lexer 0.006s 127 | ``` 128 | 很好!我们的词法分析器现在知道了如何处理字符字段。是时候去教解析器如何去做同样的事情了。 129 | ## 解析字符串 130 | 为了让我们的解析器将 token.STRING 转换为字符串文字 AST 节点,我们需要定义所述节点。 值得庆幸的是,定义再简单不过了。 它看起来与 ast.IntegerLiteral非常相似,不同之处在于 Value 字段现在包含一个字符串而不是一个int64。 131 | ```go 132 | // ast/ast.go 133 | 134 | type StringLiteral struct { 135 | Token token.Token 136 | Value string 137 | } 138 | func (sl *StringLiteral) expressionNode() {} 139 | func (sl *StringLiteral) TokenLiteral() string { return sl.Token.Literal } 140 | func (sl *StringLiteral) String() string { return sl.Token.Literal } 141 | ``` 142 | 当然,字符字段是表达式而不是语句。它们评估字符。 143 | 144 | 有了这个定义,我们可以编写一个小的测试用例,确保解析器知道如何处理 token.STRING 标记并输出 *ast.StringLiterals: 145 | ```go 146 | // parser/parser_test.go 147 | 148 | func TestStringLiteralExpression(t *testing.T) { 149 | input := `"hello world";` 150 | 151 | l := lexer.New(input) 152 | p := New(l) 153 | program := p.ParseProgram() 154 | checkParserErrors(t, p) 155 | 156 | stmt := program.Statements[0].(*ast.ExpressionStatement) 157 | literal, ok := stmt.Expression.(*ast.StringLiteral) 158 | if !ok { 159 | t.Fatalf("exp not *ast.StringLiteral. got=%T", stmt.Expression) 160 | } 161 | 162 | if literal.Value != "hello world" { 163 | t.Errorf("literal.Value not %q. got=%q", "hello world", literal.Value) 164 | } 165 | } 166 | ``` 167 | 运行测试会导致众所周知的解析器错误类型: 168 | ```go 169 | $ go test ./parser 170 | --- FAIL: TestStringLiteralExpression (0.00s) 171 | parser_test.go:888: parser has 1 errors 172 | parser_test.go:890: parser error: "no prefix parse function for STRING found" 173 | FAIL 174 | FAIL monkey/parser 0.007s 175 | ``` 176 | 我们以前见过很多次,我们知道如何解决它。 我们所要做的就是为 token.STRING 令牌注册一个新的 prefixParseFn。 这个解析函数然后返回一个 *ast.StringLiteral: 177 | ```go 178 | // parser/parser.go 179 | 180 | func New(l *lexer.Lexer) *Parser { 181 | // [...] 182 | p.registerPrefix(token.STRING, p.parseStringLiteral) 183 | // [...] 184 | } 185 | 186 | func (p *Parser) parseStringLiteral() ast.Expression { 187 | return &ast.StringLiteral{Token: p.curToken, Value: p.curToken.Literal} 188 | } 189 | ``` 190 | 新的三行!这使得测试通过了: 191 | ```go 192 | $ go test ./parser 193 | ok monkey/parser 0.007s 194 | ``` 195 | 所以现在我们的词法分析器将字符串文字转换为 token.STRING 标记,解析器将它们转换为 *ast.StringLiteral 节点。 我们现在准备对我们的对象系统和评估器进行更改。 196 | ## 评估字符串 197 | 在我们的对象系统中表示字符串就像表示整数一样简单。 如此简单的最大原因是我们重用了 Go 的字符串数据类型。 想象一下,向来宾语言添加一种无法用宿主语言的内置数据结构表示的数据类型。 例如:C 中的字符串。这需要做更多的工作。 但相反,我们所要做的就是定义一个包含字符串的新对象: 198 | ```go 199 | // object/object.go 200 | 201 | const ( 202 | // [...] 203 | STRING_OBJ = "STRING" 204 | ) 205 | 206 | type String struct { 207 | Value string 208 | } 209 | 210 | func (s *String) Type() ObjectType { return STRING_OBJ } 211 | func (s *String) Inspect() string { return s.Value } 212 | ``` 213 | 现在我们需要扩展我们的评估器,让它在 object.String 对象中变成 *ast.StringLiteral。确保这有效的测试很小: 214 | ```go 215 | // evaluator/evaluator_test.go 216 | 217 | func TestStringLiteral(t *testing.T) { 218 | input := `"Hello World!"` 219 | 220 | evaluated := testEval(input) 221 | str, ok := evaluated.(*object.String) 222 | if !ok { 223 | t.Fatalf("object is not String. got=%T (%+v)", evaluated, evaluated) 224 | } 225 | 226 | if str.Value != "Hello World!" { 227 | t.Errorf("String has wrong value. got=%q", str.Value) 228 | } 229 | } 230 | ``` 231 | 对 Eval 的调用还没有返回 *object.String 而是 nil: 232 | ```go 233 | $ go test ./evaluator 234 | --- FAIL: TestStringLiteral (0.00s) 235 | evaluator_test.go:317: object is not String. got= () 236 | FAIL 237 | FAIL monkey/evaluator 0.007s 238 | ``` 239 | 让这个测试通过需要比解析器更少的行。 就两个: 240 | ```go 241 | // evaluator/evaluator.go 242 | 243 | func Eval(node ast.Node, env *object.Environment) object.Object { 244 | // [...] 245 | case *ast.StringLiteral: 246 | return &object.String{Value: node.Value} 247 | // [...] 248 | } 249 | ``` 250 | 这使得测试通过了并且我们能使用字符串在我们的REPL: 251 | ```js 252 | $ go run main.go 253 | Hello mrnugget! This is the Monkey programming language! 254 | Feel free to type in commands 255 | >> "Hello world!" 256 | Hello world! 257 | >> let hello = "Hello there, fellow Monkey users and fans!" 258 | >> hello 259 | Hello there, fellow Monkey users and fans! 260 | >> let giveMeHello = fn() { "Hello!" } 261 | >> giveMeHello() 262 | Hello! 263 | ``` 264 | 我们现在在解释器中完全支持字符串! 很好! 或者我应该说…… 265 | ```js 266 | >> "This is amazing!" 267 | This is amazing! 268 | ``` 269 | ## 字符串连接 270 | 拥有可用的字符串数据类型很棒。 但是除了创建字符串之外,我们还不能对字符串做太多事情。 让我们改变它! 在本节中,我们将向解释器添加字符串连接。 我们将通过添加对带有字符串操作数的 + 中缀运算符的支持来实现这一点。 271 | 272 | 这个测试完美地描述了我们想要的: 273 | ```go 274 | // evaluator/evaluator_test.go 275 | func TestStringConcatenation(t *testing.T) { 276 | input := `"Hello" + " " + "World!"` 277 | 278 | evaluated := testEval(input) 279 | str, ok := evaluated.(*object.String) 280 | if !ok { 281 | t.Fatalf("object is not String. got=%T (%+v)", evaluated, evaluated) 282 | } 283 | 284 | if str.Value != "Hello World!" { 285 | t.Errorf("String has wrong value. got=%q", str.Value) 286 | } 287 | } 288 | ``` 289 | 我们还可以扩展我们的 TestErrorHandling 函数以确保我们只添加对 + 运算符的支持,仅此而已: 290 | ```go 291 | // evaluator/evaluator_test.go 292 | 293 | func TestErrorHandling(t *testing.T) { 294 | tests := []struct { 295 | input string 296 | expectedMessage string 297 | }{ 298 | // [...] 299 | { 300 | `"Hello" - "World"`, 301 | "unknown operator: STRING - STRING", 302 | }, 303 | // [...] 304 | } 305 | 306 | // [...] 307 | } 308 | ``` 309 | 这个测试用例已经是绿色的,它更像是规范和回归测试,而不是作为实现的指南。 但是我们的连接测试失败了: 310 | ```go 311 | $ go test ./evaluator 312 | --- FAIL: TestStringConcatenation (0.00s) 313 | evaluator_test.go:336: object is not String. got=*object.Error\ 314 | (&{Message:unknown operator: STRING + STRING}) 315 | FAIL 316 | FAIL monkey/evaluator 0.007s 317 | ``` 318 | 我们需要修改的地方是 eval Infix Expression。 在这里,我们需要向现有的 switch 语句添加一个新分支,当两个操作数都是字符串时,该分支会被评估: 319 | ```go 320 | // evaluator/evaluator.go 321 | 322 | func evalInfixExpression( 323 | operator string, 324 | left, right object.Object, 325 | ) object.Object { 326 | switch { 327 | // [...] 328 | case left.Type() == object.STRING_OBJ && right.Type() == object.STRING_OBJ: 329 | return evalStringInfixExpression(operator, left, right) 330 | // [...] 331 | } 332 | } 333 | ``` 334 | evalStringInfixExpression 是可能的最小实现: 335 | ```go 336 | // evaluator/evaluator.go 337 | 338 | func evalStringInfixExpression( 339 | operator string, 340 | left, right object.Object, 341 | ) object.Object { 342 | if operator != "+" { 343 | return newError("unknown operator: %s %s %s", 344 | left.Type(), operator, right.Type()) 345 | } 346 | 347 | leftVal := left.(*object.String).Value 348 | rightVal := right.(*object.String).Value 349 | return &object.String{Value: leftVal + rightVal} 350 | } 351 | ``` 352 | 这里的第一件事是检查正确的操作符。 如果它是受支持的 + 我们解开字符串对象并构造一个新的字符串,它是两个操作数的串联。 353 | 354 | 如果我们想支持更多的字符串运算符,这是添加它们的地方。 此外,如果我们想支持字符串与 == 和 != 的比较,我们也需要在这里添加它。 指针比较不适用于字符串,至少不是我们想要的方式:对于字符串,我们要比较值而不是指针。 355 | 356 | 就是这样! 我们的测试通过: 357 | ```go 358 | $ go test ./evaluator 359 | ok monkey/evaluator 0.007s 360 | ``` 361 | 我们现在可以使用字符串文字,传递它们,将它们绑定到名称,从函数返回它们并连接它们: 362 | ```js 363 | >> let makeGreeter = fn(greeting) { fn(name) { greeting + " " + name + "!" } }; 364 | >> let hello = makeGreeter("Hello"); 365 | >> hello("Thorsten"); 366 | Hello Thorsten! 367 | >> let heythere = makeGreeter("Hey there"); 368 | >> heythere("Thorsten"); 369 | Hey there Thorsten! 370 | ``` 371 | 好的! 我想说字符串现在在我们的解释器中工作得很好。 但是我们仍然可以添加其他东西来与它们一起工作...... 372 | |[< 4.1数据类型&函数](4.1.md)|[> 4.3内置函数](4.3.md)| 373 | |-|-| -------------------------------------------------------------------------------- /contents/2/2.7.md: -------------------------------------------------------------------------------- 1 | # Pratt解析是如何运行的? 2 | parseExpression 方法背后的算法及其解析函数的组合 Vaughan Pratt 在他的“自上而下的运算符优先级”论文中详细描述了优先级。 但是他的实现和我们的实现是有区别的。 3 | 4 | Partt 不使用一个解析器结构并且也不传递定义在*Parser上方法。它也不适用maps,当然他也没有使用Go。他的论文比Go早36年发表。然后还有命名差异:我们所说的prefixParseFns是“nuds”(对于“null denotations”)。infixParseFns是“leds”(用于“左表示”)。 5 | 6 | 尽管用伪代码表示,我们的parseExpression方法看起来与Pratt论文中的代码惊人地相似。它使用相同的方法,几乎没有任何变化。我们将跳过回答其工作原理的理论,只关心它是如何工作的以及如何工作通过查看示例,所有部分(parseExpression、解析函数和优先级)都组合在一起。假设我们正在解析以下表达式语句: 7 | ```go 8 | 1 + 2 + 3; 9 | ``` 10 | 在这里大的挑战现在不是每个运算符和AST中的结果数,而是正确嵌套AST的节点。我们想要的是一个AST(序列化为字符串)看起来像这样: 11 | 12 | ```go 13 | ((1 + 2) + 3) 14 | ``` 15 | AST需要两个*ast.InfixExpression节点。树种较高的 *ast.InfixExpression应该将整数文字3作为其右边子节点,而其左节点需要是另一个 *ast.InfixExpression。第二个 *ast.InfixExpression然后需要分别将整数文字1和2作为其左子节点和右节点。像这样: 16 | 17 | ![AST节点](AST节点.png) 18 | 19 | 并且这就是我们解析器的精确输出的东西当它解析`1 + 2 + 3;`但是怎么样?我们将回答这个问题在下一个段落。我们将近距离看一下我们解析器做了什么当parseExpressionStatement被第一次调用。在阅读一下段落时打开代码并没有错。 20 | 21 | 所以我们启程了。这里是当我们解析1 + 2 + 3;时发生的东西。 22 | 23 | parseExpressionStatement调用parseExpression(LOWEST)。p.curToken和p.peekToken分别是1和第一个+号; 24 | 25 | ![解析](解析.png) 26 | 27 | parseExpression将要做的第一件事是检查是否有一个prefixParseFn联系和当前是否为token.INT的p.curToken.Type。并且,当然有parseIntegerLiteral。所以它调用parseIntegerLiteral,它返回*ast.IntegerLiteral。parseExpression将此分配给leftExp。 28 | 29 | 然后来自新的for循环在parseExpression。它情况的评估是正确的: 30 | ```go 31 | for !p.peekTokenIs(token.SEMICOLON) && precedence < p.peekPrecedence() { 32 | // [...] 33 | } 34 | ``` 35 | p.peekToken不是一个token.SEMICOLON并且peekPrecedence(返回+的token的优先级)高于传递给parseExpression的参数,后者是LOWEST。这是我们的在此定义优先级再次刷新我们的记忆: 36 | ```go 37 | // parser/parser.go 38 | const ( 39 | _ int = iota 40 | LOWEST 41 | EQUALS // == 42 | LESSGREATER // > or < 43 | SUM // + 44 | PRODUCT // * 45 | PREFIX // -X or !X 46 | CALL // myFunction(X) 47 | ) 48 | ``` 49 | 所以评估是正确的情况并且parseExpression执行循环体,它看起来像这样: 50 | ```go 51 | infix := p.infixParseFns[p.peekToken.Type] 52 | if infix == nil { 53 | return leftExp 54 | } 55 | 56 | p.nextToken() 57 | 58 | leftExp = infix(leftExp) 59 | ``` 60 | 现在它获取 p.peekToken.Type 的 infixParseFn,它是在 *Parser 上定义的 parseInfixExpression。 在调用它并将其返回值分配给 leftExp(重用 leftExp 变量!)之前,它会推进token,因此它们现在看起来像这样: 61 | 62 | ![解析2](解析2.png) 63 | 64 | 对于处于这种状态的tokens,它调用 parseInfixExpression 并传入已解析的 *ast.IntegerLiteral(在 for 循环外分配给 leftExp)。 解析 InfixExpression 中接下来发生的事情是事情变得有趣的地方。 这里又是方法: 65 | ```go 66 | // parser/parser.go 67 | func (p *Parser) parseInfixExpression(left ast.Expression) ast.Expression { 68 | expression := &ast.InfixExpression{ 69 | Token: p.curToken, 70 | Operator: p.curToken.Literal, 71 | Left: left, 72 | } 73 | 74 | precedence := p.curPrecedence() 75 | p.nextToken() 76 | expression.Right = p.parseExpression(precedence) 77 | 78 | return expression 79 | } 80 | ``` 81 | 需要注意的是,左边是我们已经解析的 *ast.IntegerLiteral ,它代表了1。 82 | 83 | parseInfixExpression保存了p.curToken的优先级(第一个+token),推进token并调用parseExpression-传入刚刚保存的优先级。所以现在parseExpression被第二次调用,tokens如下: 84 | ![解析3](解析3.png) 85 | parseExpression 再次做的第一件事是为 p.curToken 寻找一个 prefixParseFn。 并且再一次 parseIntegerLiteral。 但是现在 for 循环的条件不评估为真:优先级(传递给 parseExpression 的参数)是第一个 + 运算符的优先级1 + 2 + 3,不小于 p.peekToken 的优先级,第二个 + 运算符。 他们是是平等的。 for 循环体不执行, *ast.IntegerLiteral 表示 2 返回。 86 | 87 | 现在回到 parseInfixExpression 中,将 parseExpression 的返回值分配给 Right 新构造的 *ast.InfixExpression 的字段。 所以现在我们有这个: 88 | ![解析4](解析4.png) 89 | 这个 *ast.InfixExpressions 由 parseInfixExpression 返回,现在我们回到对 parseExpression 的最外层调用,这里的优先级仍然是最低的。 我们回到了开始的地方,再次评估 for 循环的条件。 90 | ```go 91 | for !p.peekTokenIs(token.SEMICOLON) && precedence < p.peekPrecedence() { 92 | // [...] 93 | } 94 | ``` 95 | 这次评估仍然是正确的,因为优先级是LOWEST并且peekPrecedence现在返回在我们表达式里的第二个+,它的优先级更高。parseExpression评估第二次循环的循环体。现在leftExp不是代表1的 *ast.IntegerLiteral,而是 parseInfixExpression返回的 *ast.InfixExpression,代表 96 | 1 + 2。 97 | 98 | 在循环体中,parseExpression 获取 parseInfixExpression 作为 p.peekToken.Type(第二个 +)的 infixParseFn,推进标记并以 leftExp 作为参数调用 parseInfixExpression。 parseInfixExpression 依次再次调用 parseExpression,这 99 | 返回最后一个 *ast.IntegerLiteral(表示表达式中的 3)。 100 | 101 | 毕竟,在循环体的末尾,leftExp 看起来像这样: 102 | ![解析](解析5.png) 103 | 104 | 运算符和操作数嵌套正确! 而我们的tokens看起来像这样: 105 | ![解析](解析6.png) 106 | 107 | for循环的评估情况是错误的: 108 | ```go 109 | for !p.peekTokenIs(token.SEMICOLON) && precedence < p.peekPrecedence() { 110 | // [...] 111 | } 112 | ``` 113 | 114 | 现在p.peekTokenIs(token.SEMICOLON) 评估是正确的,这会阻止循环体再次执行。 115 | 116 | (对 p.peekTokenIs(token.SEMICOLON) 的调用不是绝对必要的。我们的 peekPrecedence 如果找不到 p.peekToken.Type 的优先级,方法将返回 LOWEST 作为默认值 - token.SEMICOLON token就是这种情况。 但我认为它使分号的行为作为表达式结束分隔符更明确和更容易理解。) 117 | 118 | 并且,这就是,for循环结束了,leftExp返回了。我们回到parseExpressionStatement并且到了结尾和正确处理*ast.InfixExpression。并且这在 *ast.ExpressionStatement 中用作表达式。 119 | 120 | 现在我们知道了我们的解析器如何正确解析`1 + 2 + 3`,这非常令人兴奋,不是吗?我认为优先级和peekPrecedence是有尤其令人兴奋的。 121 | 122 | 但是考虑一下“真正的优先级问题”?在我们的例子中每个操作符(+)有相同的优先级。不同优先级的实现怎么做呢?我们不能只是默认情况下使用 LOWEST 并为所有运算符使用称为 HIGHEST 的东西? 123 | 124 | 不,因为那样会给我们一个错误的AST,目标是使设计具有较高优先级的运算符表达式比具有较低优先级运算符的表达式在树中更深。这是通过parseExpression中的优先级值(参数)完成的。 125 | 126 | 当 parseExpression 被调用时,优先级的值代表当前 parseExpression 调用的当前“右侧绑定能力”。 “右侧绑定能力”是什么意思? 好吧,它越高,当前表达式(未来的窥视标记)右侧的token/运算符/操作数就越多,我们可以“绑定”到它,或者像我想的那样,“吸入”。 127 | 128 | 如果我们目前的右侧绑定能力是最高可能的值,我们到目前为止解析的 129 | (分配给 leftExp)永远不会传递给与下一个运算符(或token)。 它永远不会成为“左”子节点。 因为 for 循环的条件从不评估为真。 130 | 131 | 存在右侧绑定能力的对应物,它被称为(你猜对了!)“左绑定能力”。 但是哪个值表示这种左绑定能力? 既然 parseExpression 中的 precedence 参数代表的是当前的右绑定能力,那么下一个运算符的左绑定能力从何而来? 简单地说:从我们对 peekPrecedence 的调用开始。 此调用返回的值代表 p.peekToken 的下一个运算符的左绑定能力。 132 | 133 | 这一切都归结为我们的 for 循环的优先级 < p.peekPrecedence() 条件。 这条件检查下一个操作符/token的左绑定能力是否高于我们当前的 134 | 右侧绑定能力。 如果是,那么我们到目前为止解析的内容会被下一个操作符“吸入”,从从左到右,最终被传递到下一个运算符的 infixParseFn。 135 | 136 | 一个例子,让我们看一下我们解析表达式语句`-1 + 2;`我们想要的AST现在是(-1) + 2,而不是-(1+2)。使用的第一个方法(在 parseEx pressionStatement 和 parseExpression 之后)是我们与 token.MINUS 关联的 prefixParseFn:解析前缀表达式。 为了刷新我们对 parsePrefixExpression 的记忆,它是完整的: 137 | ```go 138 | // parser/parser.go 139 | func (p *Parser) parsePrefixExpression() ast.Expression { 140 | expression := &ast.PrefixExpression{ 141 | Token: p.curToken, 142 | Operator: p.curToken.Literal, 143 | } 144 | p.nextToken() 145 | expression.Right = p.parseExpression(PREFIX) 146 | return expression 147 | } 148 | ``` 149 | 这将 PREFIX 作为优先级传递给 parseExpression,将 PREFIX 转换为该 parseExpression 调用的右绑定功能。 根据我们的定义,PREFIX 是一个非常高的优先级。 这样做的结果是 parseExpression(PREFIX) 永远不会解析 -1 中的 1 并将其传递给另一个 infixParseFn。 precedence < p.peekPrecedence() 永远不会为真。在这种情况下,意味着没有其他 infixParseFn 会将我们的 1 作为左臂。 相反,1 作为前缀表达式的“右”臂返回。 只是 1,而不是其他一些表达紧随其后,需要解析。 150 | 151 | 回到对 parseExpression 的外部调用(其中我们将 parsePrefixExpression 称为 prefixParseFn),紧跟在第一个 leftExp := prefix() 之后,优先级的值仍然是 LOWEST。 因为这是我们在最外层调用中使用的值。 我们的右侧绑定能力仍然是最低的。 p.peekToken 现在是 -1 + 2 中的 +。 152 | 153 | 我们现在坐在 for 循环的条件上并评估它以确定我们是否 154 | 应该执行循环体。 事实证明 + 运算符的优先级 155 | (由 p.peekPrecedence() 返回)高于我们当前的右侧绑定能力。 我们到目前为止解析的(-1 前缀表达式)现在传递给与 + 关联的 infixParseFn。+ 的左绑定力“吸收”了我们到目前为止解析的内容并将其用作“左臂”它正在构建的 AST 节点。 156 | 157 | + 的 infixParseFn 是 parseInfixExpression,它现在使用 + 的优先级作为调用 parseExpression 时的权利约束力。 它不使用 LOWEST,因为那会导致另一个+具有更高的左束缚力并“吸走”我们的“右臂”。 如果 158 | 确实如此,那么像 a + b + c 这样的表达式会导致 (a + (b + c)),这不是我们想要的。 我们想要 ((a + b) + c)。 159 | 160 | 前缀运算符的高优先级起作用了。 它甚至适用于中缀运算符。在运算符优先级 1 + 2 * 3 的经典示例中,* 的左绑定能力为高于+的右结合力。 解析这将导致 2 被传递给与 * 标记关联的 infixParseFn。 161 | 162 | 值得注意的是,在我们的解析器中,每个标记都具有相同的左右绑定力。 我们只是使用一个值(在我们的优先级表中)作为两者。 该值的含义取决于上下文。 163 | 164 | 如果运算符应该是右关联而不是左关联(在 + 的情况下导致 (a + (b + c)) 而不是 ((a + b) + c),那么我们必须使用更小的“右绑定能力”在解析运算符表达式的“右臂”时。 如果你考虑 ++ 和 -- 其他语言中的运算符,它们可以用在前缀和后缀位置,您 可以理解为什么有时为运算符设置不同的左右绑定权限是有用的。 165 | 166 | 由于我们没有为运算符定义单独的左右绑定能力,而仅使用一个值,我们不能仅仅改变一个定义来实现这一点。 但是,作为一个例子,让 +右关联我们可以在调用 parseExpression 时递减它的优先级: 167 | ```go 168 | // parser/parser.go 169 | 170 | func (p *Parser) parseInfixExpression(left ast.Expression) ast.Expression { 171 | expression := &ast.InfixExpression{ 172 | Token: p.curToken, 173 | Operator: p.curToken.Literal, 174 | Left: left, 175 | } 176 | 177 | precedence := p.curPrecedence() 178 | p.nextToken() 179 | expression.Right = p.parseExpression(precedence) 180 | // ^^^ decrement here for right-associativity 181 | return expression 182 | } 183 | ``` 184 | 出于演示目的,让我们暂时更改此方法,看看会发生什么: 185 | ```go 186 | // parser/parser.go 187 | func (p *Parser) parseInfixExpression(left ast.Expression) ast.Expression { 188 | 189 | expression := &ast.InfixExpression{ 190 | Token: p.curToken, 191 | Operator: p.curToken.Literal, 192 | Left: left, 193 | } 194 | 195 | precedence := p.curPrecedence() 196 | p.nextToken() 197 | 198 | if expression.Operator == "+" { 199 | expression.Right = p.parseExpression(precedence - 1) 200 | } else { 201 | expression.Right = p.parseExpression(precedence) 202 | } 203 | 204 | return expression 205 | } 206 | ``` 207 | 进行此更改后,我们的测试告诉我们 + 正式右结合: 208 | ```go 209 | $ go test -run TestOperatorPrecedenceParsing ./parser 210 | --- FAIL: TestOperatorPrecedenceParsing (0.00s) 211 | parser_test.go:359: expected="((a + b) + c)", got="(a + (b + c))" 212 | parser_test.go:359: expected="((a + b) - c)", got="(a + (b - c))" 213 | parser_test.go:359: expected="(((a + (b * c)) + (d / e)) - f)",\ 214 | got="(a + ((b * c) + ((d / e) - f)))" 215 | FAIL 216 | ``` 217 | 这标志着我们深入了解 parseExpression 的结束。 如果你还在 218 | 不确定和无法理解它是如何工作的,别担心,我也有同感。 什么真正有帮助正在将跟踪语句放在 Parser 的方法中以查看发生了什么解析某些表达式。 在本章附带的代码文件夹中,我包含了一个名为 ./parser/parser_tracing.go 的文件,我们之前没有看过。 该文件包括当试图理解解析器的作用时,两个函数定义非常有用:跟踪和不跟踪。 像这样使用它们: 219 | ```go 220 | // parser/parser.go 221 | 222 | func (p *Parser) parseExpressionStatement() *ast.ExpressionStatement { 223 | defer untrace(trace("parseExpressionStatement")) 224 | // [...] 225 | } 226 | func (p *Parser) parseExpression(precedence int) ast.Expression { 227 | defer untrace(trace("parseExpression")) 228 | // [...] 229 | } 230 | func (p *Parser) parseIntegerLiteral() ast.Expression { 231 | defer untrace(trace("parseIntegerLiteral")) 232 | // [...] 233 | } 234 | func (p *Parser) parsePrefixExpression() ast.Expression { 235 | defer untrace(trace("parsePrefixExpression")) 236 | // [...] 237 | } 238 | func (p *Parser) parseInfixExpression(left ast.Expression) ast.Expression { 239 | defer untrace(trace("parseInfixExpression")) 240 | // [...] 241 | } 242 | ``` 243 | 包含这些跟踪语句后,我们现在可以使用我们的解析器并查看它的作用。 这里是解析测试套件中的表达式语句-1 * 2 + 3时的输出: 244 | ```go 245 | $ go test -v -run TestOperatorPrecedenceParsing ./parser 246 | === RUN TestOperatorPrecedenceParsing 247 | BEGIN parseExpressionStatement 248 | BEGIN parseExpression 249 | BEGIN parsePrefixExpression 250 | BEGIN parseExpression 251 | BEGIN parseIntegerLiteral 252 | END parseIntegerLiteral 253 | END parseExpression 254 | END parsePrefixExpression 255 | BEGIN parseInfixExpression 256 | BEGIN parseExpression 257 | BEGIN parseIntegerLiteral 258 | END parseIntegerLiteral 259 | END parseExpression 260 | END parseInfixExpression 261 | BEGIN parseInfixExpression 262 | BEGIN parseExpression 263 | BEGIN parseIntegerLiteral 264 | END parseIntegerLiteral 265 | END parseExpression 266 | END parseInfixExpression 267 | END parseExpression 268 | END parseExpressionStatement 269 | --- PASS: TestOperatorPrecedenceParsing (0.00s) 270 | PASS 271 | ok monkey/parser 0.008s 272 | ``` 273 | |[> 2.6解析表达式](2.6.md)|[> 2.8扩展解析器](2.8.md)| 274 | |-|-| -------------------------------------------------------------------------------- /contents/1/1.3.md: -------------------------------------------------------------------------------- 1 | # 1.3词法分析器 2 | 在我们开始coding之前,让我们理清一下本部分的目标。我们将写下我们自己的词法分析器,它将**吃掉**源代码作为输入并且输出它认识的Token。 3 | 4 | 它不需要缓冲区或保存Tokens,因为只有一个被称为`NextToken()`的方法,它可以输出下一个Token。 5 | 6 | 这意味着我们将用我们的源代码初始化词法分析器,然后重复调用NextToken()来遍历源代码,一个标记一个标记,一个字符,一个字符。我们还将通过使用字符串作为我们源代码的类型来简化这里的工作。注意:在生产环境中,将文件名和行号附加到Token上是有意义的,它可以更好地追钟语法分析和解析错误。所以最好用`io.Reader`初始化语法分析器和文件名。但是因为这会增加更多的复杂性,我们不会在这里处理,我们将从小开始,只是用字符串并忽略文件名和行号。 7 | 8 | 考虑到这一点,我们现在意识到我们的词法分析器需要做的很清楚。所以让我们创建一个新package并添加第一个测试,我们可以连续运行该测试以获取有关词法分析器工作状态的反馈。我们从这里开始,以扩展测试用例,为词法分析器添加更多功能: 9 | ```go 10 | // lexer/lexer_test.go 11 | package lexer 12 | 13 | import ( 14 | "testing" 15 | "monkey/token" 16 | ) 17 | 18 | func TestNextToken(t *testing.T){ 19 | input := `=+(){},;` 20 | 21 | tests := []struct{ 22 | expectedType token.TokenType 23 | expectedLiteral string 24 | }{ 25 | {token.ASSIGN,"="}, 26 | {token.PLUS,"+"}, 27 | {token.LPAREN,"("}, 28 | {token.RPAREN,")"}, 29 | {token.LBRACE,"{"}, 30 | {token.RBRACE,"}"}, 31 | {token.COMMA,","}, 32 | {token.SEMICOLON,";"}, 33 | {token.EOF,""}, 34 | } 35 | 36 | l := New(input) 37 | for i, tt := range tests { 38 | tok := l.NextToken() 39 | 40 | if tok.Type != tt.expectedType { 41 | t.Fatalf("tests[%d] - tokentype wrong. expected=%q, got=%q", 42 | i, tt.expectedType, tok.Type) 43 | } 44 | 45 | if tok.Literal != tt.expectedLiteral { 46 | t.Fatalf("tests[%d] - literal wrong. expected=%q, got=%q", 47 | i, tt.expectedLiteral, tok.Literal) 48 | } 49 | } 50 | } 51 | ``` 52 | 当然,这个测试失败了——我们还没有写任何代码: 53 | ``` 54 | $ go test ./lexer 55 | # monkey/lexer 56 | lexer/lexer_test.go:27: undefined: New 57 | FAIL monkey/lexer [build f 58 | ``` 59 | 60 | 所以让我们通过定义返回*Lexer的New()函数。 61 | ```go 62 | // lexer/lexer.go 63 | package lexer 64 | 65 | import "monkey/token" 66 | 67 | type Lexer struct { 68 | input string 69 | position int // current position in input (points to current char) 70 | readPosition int // current reading position in input (after current char) 71 | ch byte // current char under examination 72 | } 73 | 74 | func New(input string) *Lexer { 75 | l := &Lexer{input: input} 76 | return l 77 | } 78 | ``` 79 | 80 | 许多在Lexer中的字段是不言自明的。有一些可能会引起困扰的是`position`和`readPosition`.这两个将用于它们作为索引来访问输入的字符。例如`l.input[l.readPosition]`。这两个指针指向我们的输入字符串的原因是我们需要能够进一步“窥视”输入并查看当前字符以查看接下来会发送什么。`readPosition`总是指向输入中的“下一个”字。`positions`指向输入中对应于ch的字符。 81 | 82 | 第一个名为`readChar()`的辅助方法应该使这些字段更容易理解: 83 | ```go 84 | //lexer/lexer.go 85 | func (l *Lexer) readChar() { 86 | if l.readPosition >= len(l.input) { 87 | l.ch = 0 88 | } else { 89 | l.ch = l.input[l.readPosition] 90 | } 91 | l.position = l.readPosition 92 | l.readPosition += 1 93 | } 94 | ``` 95 | 96 | readChar的目的是为我们提供下一个字符并提高我们在输入字符串中的位置。它做的第一件事是检查我们是否到达输入的末尾。如果是,它就设置l.ch为0,这是"NUL"字符的ASCII码,对我们来说表示“我们还没有读取任何东西”或“文件结束”。但是如果我们还没有到达输入的结尾,它会通过访问l.input[l.readPosition]来设置l.ch到下一个字符。 97 | 98 | 在l.position之后被更新为l.readPosition并且l.readPosition+1。这样,l.readPosition总是指向我们要从next开始读取的下一个位置,而l.position总是指向我们上次读取的位置。这很快就会派上用场。 99 | 100 | 在谈论readChar时指出,词法分析器仅支持ASCII字符而不是完成的Unicode范围。为什么?因为这让我们保持简单并专注于我们的解释器的基本部分。为了完全支持Unicode和UTF-8,我们需要将l.ch从一个字节更改为rune并更改我们读取下一个字符的方式,因为它们现在可能是多个字节宽。使用l.input[l.readPosition]将不再起作用。然后我们还需要更改一些我们稍后会看到的其他方法和函数。因此在Monkey中完全支持Unicode(和表情符号)作为练习留给读者。 101 | 102 | 让我们在New函数中使用readChar以便我们的*Lexer在任何人调用NextToken()之前处于完全工作的状态,并且l.ch,l.position和l.readPosition也以及初始化。 103 | 104 | ```go 105 | //lexer/lexer.go 106 | func New(input string) *Lexer { 107 | l := &Lexer{input:input} 108 | l.readChar() 109 | return l 110 | } 111 | ``` 112 | 113 | 我们的测试现在告诉我们,调用New(输入)不会遇到问题,但仍然缺少NextToken()方法。让我们通过添加第一个版本来解决: 114 | ```go 115 | //lexer/lexer.go 116 | import "monkey/token" 117 | 118 | func (l *Lexer) NextToken() token.Token { 119 | var tok token.Token 120 | 121 | switch l.ch { 122 | case'=': 123 | tok = newToken(token.ASSIGN, l.ch) 124 | case ';': 125 | tok = newToken(token.SEMICOLON, l.ch) 126 | case '(': 127 | tok = newToken(token.LPAREN, l.ch) 128 | case ')': 129 | tok = newToken(token.RPAREN, l.ch) 130 | case ',': 131 | tok = newToken(token.COMMA, l.ch) 132 | case '+': 133 | tok = newToken(token.PLUS, l.ch) 134 | case '{': 135 | tok = newToken(token.LBRACE, l.ch) 136 | case '}': 137 | tok = newToken(token.RBRACE, l.ch) 138 | case 0: 139 | tok.Literal = "" 140 | tok.Type = token.EOF 141 | } 142 | 143 | l.readChar() 144 | return tok 145 | } 146 | 147 | func newToken(tokenType token.TokenType, ch byte) token.Token { 148 | return token.Token{Type: tokenType, Literal: string(ch)} 149 | } 150 | ``` 151 | 这就是newToken()方法的基本结构。我们查看当前正在检查的字符(l.ch)并根据它是哪个字符返回一个标记。在返回Token之前,我们将指针推进到输入中,因此当我们再一次调用NextToken()时,l.ch字段已经更新。一个名为newToken的小函数帮助我们初始化这些Token。 152 | 153 | 运行测试我们可以看到它通过了: 154 | ```go 155 | $ go test ./lexer 156 | ok monkey/lexer 0.007s 157 | ``` 158 | 159 | 很棒!现在让我们延长测试案例,以便开始类似于Monkey的源代码。 160 | 161 | ```go 162 | // lexer/lexer_test.go 163 | func TestNextToken(t *testing.T) { 164 | input := `let five = 5; 165 | let ten = 10; 166 | 167 | let add = fn(x, y) { 168 | x + y; 169 | }; 170 | 171 | let result = add(five, ten); 172 | ` 173 | 174 | tests := []struct { 175 | expectedType token.TokenType 176 | expectedLiteral string 177 | }{ 178 | {token.LET, "let"}, 179 | {token.IDENT, "five"}, 180 | {token.ASSIGN, "="}, 181 | {token.INT, "5"}, 182 | {token.SEMICOLON, ";"}, 183 | {token.LET, "let"}, 184 | {token.IDENT, "ten"}, 185 | {token.ASSIGN, "="}, 186 | {token.INT, "10"}, 187 | {token.SEMICOLON, ";"}, 188 | {token.LET, "let"}, 189 | {token.IDENT, "add"}, 190 | {token.ASSIGN, "="}, 191 | {token.FUNCTION, "fn"}, 192 | {token.LPAREN, "("}, 193 | {token.IDENT, "x"}, 194 | {token.COMMA, ","}, 195 | {token.IDENT, "y"}, 196 | {token.RPAREN, ")"}, 197 | {token.LBRACE, "{"}, 198 | {token.IDENT, "x"}, 199 | {token.PLUS, "+"}, 200 | {token.IDENT, "y"}, 201 | {token.SEMICOLON, ";"}, 202 | {token.RBRACE, "}"}, 203 | {token.SEMICOLON, ";"}, 204 | {token.LET, "let"}, 205 | {token.IDENT, "result"}, 206 | {token.ASSIGN, "="}, 207 | {token.IDENT, "add"}, 208 | {token.LPAREN, "("}, 209 | {token.IDENT, "five"}, 210 | {token.COMMA, ","}, 211 | {token.IDENT, "ten"}, 212 | {token.RPAREN, ")"}, 213 | {token.SEMICOLON, ";"}, 214 | {token.EOF, ""}, 215 | } 216 | //[...] 217 | } 218 | ``` 219 | 220 | 值得注意的是,此测试用例中的输入已更改。它看起来像是Monkey语言的一个子集。它包含我们已经成功转换为标记的所有符号,以及现在导致我们的测试失败的新事物:标识符,关键字和数字。让我们从标识符和关键字开始。我们的词法分析器需要做的是识别当前字符是否是字母,如果是,它需要读取标识符/关键字的其余部分,直到遇到非字母字符。读完那个标识符/关键字后,我们需要找出它是标识符还是关键字,这样我们就可以使用正确的token.TokenType。第一步是拓展我们的switch语句: 221 | ```go 222 | // lexer/lexer.go 223 | 224 | import "monkey/token" 225 | 226 | func (l *Lexer) NextToken() token.Token { 227 | var tok token.Token 228 | 229 | switch l.ch{ 230 | //[...] 231 | default: 232 | if isLetter(l.ch){ 233 | tok.Literal = l.readIdentifier() 234 | return tok 235 | } else { 236 | tok = newToken(token.ILLEGAL,l.ch) 237 | } 238 | } 239 | //[...] 240 | } 241 | 242 | func (l *Lexer) readIdentifier() string { 243 | position := l.position 244 | for isLetter(l.ch) { 245 | l.readChar() 246 | } 247 | return l.input[position:l.position] 248 | } 249 | 250 | func isLetter(ch byte) bool { 251 | return 'a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z' || ch == '_' 252 | } 253 | ``` 254 | 我们已经在switch语句中添加了`default`默认分支,因此只要 l.ch 不是可识别的字符之一,我们就可以检查标识符。我们还添加了token.ILLEGAL Token的生成。如果我们到了那里,我们真的不知道如何正确处理当前的字符和将其声明为token.ILLEGAL。 255 | 256 | isLetter函数只是检查给定的参数是否为字母。这听起来很简单,但值得注意的是,改变这个函数对我们的解释器能够解析的语言的影响比人们对这样一个小函数的期望更大。正如你所见,在我们实例中,它包含检查ch=='_',这意味着我们将把_视为一个字母并允许它出现在标识符和关键字中。这意味着我们可以使用像foo_bar这样的变量名。其他编程语言甚至允许!、and、?、in标识符。如果你也想允许这些标识符,偷偷加进去:) 257 | 258 | readIdentifier函数就像它的名字那样:它读入一个标识符并推进我们的词法分析器的位置,直到遇到一个非字母字符。 259 | 260 | 在switch1的`default`分支中我们使用readIdentifier来设置当前token的文字字段。但它的类型呢?现在我们以及读取了标识符像let,fn或foobar这样的标识符,我们需要能够区分用户定义的标识符和语言关键字。我们需要一个函数来为我们拥有的token文字返回正确的token类型。有什么比token包添加这样的功能更好的地方吗? 261 | 262 | ```go 263 | // token/token.go 264 | 265 | var Keywords = map[string]TokenType{ 266 | "fn": FUNCTION, 267 | "let": LET, 268 | } 269 | 270 | func LookupIdent(ident string) TokenType { 271 | if tok,ok := keywords[ident];ok{ 272 | return tok 273 | } 274 | return IDENT 275 | } 276 | ``` 277 | LookupIdent检查关键字表以查看给定的关键字是否实际上是关键字。如果是,则返回关键字的TokenType常量。如果不是,我们就返回token,IDENT,它是所有用户定义标识符的TokenType。 278 | 279 | 有了这个,我们就可以完成标识符和关键字的语法分析: 280 | 281 | ```go 282 | // lexer/lexer.go 283 | func (l *Lexer) NextToken() token.Token { 284 | var tok token.Token 285 | 286 | switch l.ch{ 287 | //[...] 288 | default: 289 | if isLetter(l.ch) { 290 | tok.Literal = l.readIdentifier() 291 | return tok 292 | } else { 293 | tok = newToken(token.ILLEGAL,l.ch) 294 | } 295 | } 296 | //[...] 297 | } 298 | ``` 299 | 这里的提前退出,我们的`return tok`语句是必要的,因为当调用readIdentifier时,我们会重复调用readChar()并将我们的readPosition和position字段推进到当前字符的后一个字符去。所以我们不需要在switch语句之后再一次调用readChar()。 300 | 301 | 现在允许我们的测试,我们可以看到let被正确识别,但测试仍然失败: 302 | ```go 303 | $ go test ./lexer 304 | --- FAIL: TestNextToken (0.00s) 305 | lexer_test.go:70: tests[1] - tokentype wrong. expected="IDENT", got="ILLEGAL" 306 | FAIL 307 | FAIL monkey/lexer 0.008s 308 | ``` 309 | 问题是我们想要的下一个token:在其文字字段中带有"five"的IDENT标记。相反,我们得到了一个ILLEGAL token。这是为什么?因为"let"和"five"之间的空格字符。但是在Monkey中,空格只是作为标记的分隔符并没有意义,所以我们需要完全跳过它。 310 | 311 | ```go 312 | // lexer/lexer.go 313 | func (l *Lexer) NextToken() token.Token { 314 | var tok token.Token 315 | 316 | l.skipWhitespace() 317 | switch l.ch { 318 | //[...] 319 | } 320 | 321 | func (l *Lexer) skipWhitespace() { 322 | for l.ch == ' ' || l.ch == '\t' || l.ch == '\n' || l.ch == '\r' { 323 | l.readChar() 324 | } 325 | } 326 | ``` 327 | 在很多解析器中都可以找到这个小辅助函数。有时它被称为`eatWhitespace`,有时被称为`consumeWhitespace`并且有时候则是完全不同的东西。这些函数实际上跳过哪些字符取决于被词法分析的语言。例如,某些语言实现确实会为换行符创造token,如果它们不在token流中的正确位置,则会引发解析错误。我们跳过换行符以使稍后的解析步骤更容易一些。 328 | 329 | 使用skipWhitespace()在对应的位置,词法分析器将遍历`let five = 5;`,部分测试输入中的5。没错,它还不知道如何将数字转换为token。是时候补充一下了。 330 | 331 | 当我们之前为标识符所作的那样,我们现在需要向switch语句的默认分支添加更多功能。 332 | 333 | ```go 334 | //lexer/lexer.go 335 | func (l *Lexer) NextToken() token.Token { 336 | var tok token.Token 337 | 338 | l.skipWhitespace() 339 | 340 | switch l.ch { 341 | // [...] 342 | default: 343 | if isLetter(l.ch) { 344 | tok.Literal = l.readIdentifier() 345 | tok.Type = token.LookupIdent(tok.Literal) 346 | return tok 347 | } else if isDigit(l.ch) { 348 | tok.Type = token.INT 349 | tok.Literal = l.readNumber() 350 | return tok 351 | } else { 352 | tok = newToken(token.ILLEGAL, l.ch) 353 | } 354 | } 355 | // [...] 356 | } 357 | 358 | func (l *Lexer) readNumber() string { 359 | position := l.position 360 | for isDigit(l.ch) { 361 | l.readChar() 362 | } 363 | return l.input[position:l.position] 364 | } 365 | 366 | func isDigit(ch byte) bool { 367 | return '0' <= ch && ch <= '9' 368 | } 369 | ``` 370 | 正如你们所看到的,添加的代码与读取标识符和关键字相关的部分非常相似,readNumber方法与readIdentifier完全相同,只是它使用了isDigit而不是isLetter。我们可能可以通过将字符识别函数作为参数传递来概括这一点,但为了简单起见,和易于理解,我们不会这样做。 371 | 372 | isDigit函数和isLetter简单,它只是返回传入的字节是否是介于0和9之间的拉丁数字。 373 | 374 | 添加此内容后,我们的测试通过: 375 | ```go 376 | $ go test ./lexer 377 | ok monkey/lexer 0.008s 378 | ``` 379 | 我不知道是否你注意到了,但是我们简化了许多在readNumber的东西。我们仅仅读取整型,那浮点型呢?或者十六进制的数字?八进制表示?我们忽略了他们,只Monkey不支持这些。当然,这也是本书的的教育目标和有限的范围。是时候弹出香槟并且庆祝:我们成功地将测试用例中使用的Monkey语言的一小部分变成了tokens。 380 | 381 | 有了这次胜利,我们很容易拓展词法分析器,这样它就可以标记更多的Monkey源代码。 382 | |[< 1.2定义我们的Tokens](1.2.md)|[> 1.4拓展我们的Token集和词法分析器](1.4.md)| 383 | |-|-| 384 | -------------------------------------------------------------------------------- /contents/3/3.10.md: -------------------------------------------------------------------------------- 1 | # 3.10函数和函数调用 2 | 这是我们一直在努力的方向。 这是第三幕。 我们将向解释器添加对函数和函数调用的支持。 完成本节后,我们将能够在 REPL 中执行此操作: 3 | ```js 4 | >> let add = fn(a, b, c, d) { return a + b + c + d }; 5 | >> add(1, 2, 3, 4); 6 | 10 7 | >> let addThree = fn(x) { return x + 3 }; 8 | >> addThree(3); 9 | 6 10 | >> let max = fn(x, y) { if (x > y) { x } else { y } }; 11 | >> max(5, 10) 12 | 10 13 | >> let factorial = fn(n) { if (n == 0) { 1 } else { n * factorial(n - 1) } }; 14 | >> factorial(5) 15 | 120 16 | ``` 17 | 如果这没有给你留下深刻印象,那么看看这个。 传递函数、高阶函数和闭包也可以: 18 | ```go 19 | >> let callTwoTimes = fn(x, func) { func(func(x)) }; 20 | >> callTwoTimes(3, addThree); 21 | 9 22 | >> callTwoTimes(3, fn(x) { x + 1 }); 23 | 5 24 | >> let newAdder = fn(x) { fn(n) { x + n } }; 25 | >> let addTwo = newAdder(2); 26 | >> addTwo(2); 27 | 4 28 | ``` 29 | 是的,没错,我们将能够做到所有这些。 30 | 31 | 为了从我们目前所处的位置到达那里,我们需要做两件事:在我们的对象系统中定义函数的内部表示,并添加对 Eval 函数调用的支持。 32 | 33 | 不过别担心。 这很简单。 我们在上一节中所做的工作现在得到了回报。 我们可以重用和扩展我们已经构建的很多东西。 在本节中的某个时刻,您会看到很多东西刚刚开始融合在一起。 34 | 35 | 既然“一次一步”把我们带到了这里,现在没有理由放弃这个策略。 第一步是处理函数的内部表示。 36 | 37 | 在内部表示函数的需要来自这样一个事实,即 Monkey 中的函数被视为任何其他值:我们可以将它们绑定到名称,在表达式中使用它们,将它们传递给其他函数,从函数中返回它们等等。 和其他值一样,函数在我们的对象系统中需要一个表示,所以我们可以传递、分配和返回它们。 38 | 39 | 但是我们如何在内部将一个函数表示为一个对象呢? 我们对 ast.FunctionLiteral 的定义给了我们一个起点: 40 | ```go 41 | // ast/ast.go 42 | 43 | type FunctionLiteral struct { 44 | Token token.Token // The 'fn' token 45 | Parameters []*Identifier 46 | Body *BlockStatement 47 | } 48 | ``` 49 | 我们不需要函数对象中的 Token 字段,但 Parameters 和 Body 是有意义的。 如果没有函数体,我们无法评估函数,如果我们不知道函数具有哪些参数,我们也无法评估函数体。 除了参数和正文之外,我们还需要在新的函数对象中添加第三个字段: 50 | ```go 51 | // object/object.go 52 | const ( 53 | // [...] 54 | FUNCTION_OBJ = "FUNCTION" 55 | ) 56 | 57 | type Function struct { 58 | Parameters []*ast.Identifier 59 | Body *ast.BlockStatement 60 | Env *Environment 61 | } 62 | 63 | func (f *Function) Type() ObjectType { return FUNCTION_OBJ } 64 | func (f *Function) Inspect() string { 65 | var out bytes.Buffer 66 | 67 | params := []string{} 68 | for _, p := range f.Parameters { 69 | params = append(params, p.String()) 70 | } 71 | out.WriteString("fn") 72 | out.WriteString("(") 73 | out.WriteString(strings.Join(params, ", ")) 74 | out.WriteString(") {\n") 75 | out.WriteString(f.Body.String()) 76 | out.WriteString("\n}") 77 | 78 | return out.String() 79 | } 80 | ``` 81 | object.Function 的此定义具有 Parameters 和 Body 字段。 但它也有 Env,一个包含指向对象的指针的字段.Environment,因为 Monkey 中的函数带有它们自己的环境。 这允许闭包,它“关闭”它们定义的环境,以后可以访问它。 当我们开始使用 Env 字段时,这会更有意义。 你会看到的。 82 | 83 | 定义完成后,我们现在可以编写一个测试来断言我们的解释器知道如何构建函数: 84 | ```go 85 | // evaluator/evaluator_test.go 86 | 87 | func TestFunctionObject(t *testing.T) { 88 | input := "fn(x) { x + 2; };" 89 | 90 | evaluated := testEval(input) 91 | fn, ok := evaluated.(*object.Function) 92 | if !ok { 93 | t.Fatalf("object is not Function. got=%T (%+v)", evaluated, evaluated) 94 | } 95 | 96 | if len(fn.Parameters) != 1 { 97 | t.Fatalf("function has wrong parameters. Parameters=%+v", 98 | fn.Parameters) 99 | } 100 | 101 | if fn.Parameters[0].String() != "x" { 102 | t.Fatalf("parameter is not 'x'. got=%q", fn.Parameters[0]) 103 | } 104 | 105 | expectedBody := "(x + 2)" 106 | 107 | if fn.Body.String() != expectedBody { 108 | t.Fatalf("body is not %q. got=%q", expectedBody, fn.Body.String()) 109 | } 110 | } 111 | ``` 112 | 此测试函数断言,对函数字面量求值会返回正确的 *object.Function,并具有正确的参数和正确的主体。 该函数的环境稍后将在其他测试中隐式地进行测试。 只需将几行代码以新的 case 分支的形式添加到 Eval 即可使此测试通过: 113 | ```go 114 | // evaluator/evaluator.go 115 | 116 | func Eval(node ast.Node, env *object.Environment) object.Object { 117 | // [...] 118 | case *ast.FunctionLiteral: 119 | params := node.Parameters 120 | body := node.Body 121 | return &object.Function{Parameters: params, Env: env, Body: body} 122 | // [...] 123 | } 124 | ``` 125 | 很简单,对吧? 测试通过。 我们只是重用了 AST 节点的 Parameters 和 Body 字段。请注意我们在构建函数对象时如何使用当前环境。 126 | 127 | 通过相对较低级别的测试,从而确保我们正确构建了函数的内部表示,我们可以转向函数应用的主题。 这意味着,扩展我们的解释器以便我们可以调用函数。 对此的测试更具可读性和更容易编写: 128 | ```go 129 | // evaluator/evaluator_test.go 130 | 131 | func TestFunctionApplication(t *testing.T) { 132 | tests := []struct { 133 | input string 134 | expected int64 135 | }{ 136 | {"let identity = fn(x) { x; }; identity(5);", 5}, 137 | {"let identity = fn(x) { return x; }; identity(5);", 5}, 138 | {"let double = fn(x) { x * 2; }; double(5);", 10}, 139 | {"let add = fn(x, y) { x + y; }; add(5, 5);", 10}, 140 | {"let add = fn(x, y) { x + y; }; add(5 + 5, add(5, 5));", 20}, 141 | {"fn(x) { x; }(5)", 5}, 142 | } 143 | 144 | for _, tt := range tests { 145 | testIntegerObject(t, testEval(tt.input), tt.expected) 146 | } 147 | } 148 | ``` 149 | 这里的每个测试用例都做同样的事情:定义一个函数,将其应用于参数,然后对产生的值进行断言。 但是由于它们的细微差别,它们测试了多个重要的事情:隐式返回值、使用 return 语句返回值、在表达式中使用参数、多个参数以及在将参数传递给函数之前评估参数。 我们还在此处测试了 *ast.CallExpression 的两种可能形式。 一个函数是一个标识符,它计算为一个函数对象,第二个函数是一个函数文字。 整洁的事情是它并不重要。 我们已经知道如何评估标识符和函数文字: 150 | ```go 151 | // evaluator/evaluator.go 152 | 153 | func Eval(node ast.Node, env *object.Environment) object.Object { 154 | // [...] 155 | case *ast.CallExpression: 156 | function := Eval(node.Function, env) 157 | if isError(function) { 158 | return function 159 | } 160 | // [...] 161 | } 162 | ``` 163 | 是的,我们只是使用 Eval 来获取我们想要调用的函数。 无论是 *ast.Identifier 还是 *ast.FunctionLiteral:Eval 都会返回一个 *object.Function(当然,如果没有错误)。 164 | 165 | 但是我们如何调用这个 *object.Function 呢? 第一步是评估调用表达式的参数。 原因很简单: 166 | ```js 167 | let add = fn(x, y) { x + y }; 168 | add(2 + 2, 5 + 5); 169 | ``` 170 | 这里我们希望将 4 和 10 作为参数传递给 add 函数,而不是表达式 2 + 2 和 5 + 5。评估参数只不过是评估表达式列表并跟踪生成的值。 但是我们也必须在遇到错误时立即停止评估过程。 这将我们引向以下代码: 171 | ```go 172 | // evaluator/evaluator.go 173 | 174 | func Eval(node ast.Node, env *object.Environment) object.Object { 175 | // [...] 176 | case *ast.CallExpression: 177 | function := Eval(node.Function, env) 178 | if isError(function) { 179 | return function 180 | } 181 | args := evalExpressions(node.Arguments, env) 182 | if len(args) == 1 && isError(args[0]) { 183 | return args[0] 184 | } 185 | // [...] 186 | } 187 | 188 | func evalExpressions( 189 | exps []ast.Expression, 190 | env *object.Environment, 191 | ) []object.Object { 192 | var result []object.Object 193 | 194 | for _, e := range exps { 195 | evaluated := Eval(e, env) 196 | if isError(evaluated) { 197 | return []object.Object{evaluated} 198 | } 199 | result = append(result, evaluated) 200 | } 201 | 202 | return result 203 | } 204 | ``` 205 | 这里没什么好看的。 我们只是迭代一个 ast.Expressions 列表,并在当前环境的上下文中评估它们。 如果遇到错误,我们将停止评估并返回错误。 这也是我们决定从左到右评估参数的部分。 希望我们不会在 Monkey 中编写代码来断言参数评估的顺序,但如果我们这样做,我们就处于编程语言设计的保守和安全方面。 206 | 207 | 所以! 现在我们有了函数和求值参数列表,我们如何“调用函数”? 我们如何将函数应用于参数? 208 | 209 | 显而易见的答案是我们必须评估函数体,它只是一个块语句。 我们已经知道如何评估这些,那么为什么不直接调用 Eval 并将其传递给函数体呢? 一个字:争论。 函数体可以包含对函数参数的引用,仅在当前环境中评估函数体会导致对未知名称的引用,这会导致错误,这不是我们想要的。 在当前环境下,按原样评估身体是行不通的。 210 | 211 | 我们需要做的是改变评估函数的环境,所以函数体中对参数的引用解析为正确的参数。 但是我们不能只是将这些参数添加到当前环境中。 这可能会导致之前的绑定被覆盖,这不是我们想要的。 我们希望这样做: 212 | ```js 213 | let i = 5; 214 | let printNum = fn(i) { 215 | puts(i); 216 | }; 217 | 218 | printNum(10); 219 | puts(i); 220 | ``` 221 | 使用打印行的 puts 函数,这应该打印两行,分别包含 10 和 5。 如果我们在评估 printNum 的主体之前覆盖当前环境,最后一行也会导致打印 10。 222 | 223 | 因此,将函数调用的参数添加到当前环境以使其在函数体中可访问是行不通的。 相反,我们需要做的是保留以前的绑定,同时提供新的绑定——我们称之为“扩展环境”。 224 | 225 | 扩展环境意味着我们创建一个 object.Environment 的新实例,并带有指向它应该扩展的环境的指针。 通过这样做,我们用现有的环境包围了一个新鲜空旷的环境。 226 | 227 | 当调用新环境的 Get 方法并且它本身没有与给定名称关联的值时,它会调用封闭环境的 Get。 这就是它正在扩展的环境。 如果封闭环境找不到值,它会调用自己的封闭环境,依此类推,直到不再有封闭环境,我们可以安全地说我们有一个“错误:未知标识符:foobar”。 228 | ```go 229 | // object/environment.go 230 | 231 | package object 232 | func NewEnclosedEnvironment(outer *Environment) *Environment { 233 | env := NewEnvironment() 234 | env.outer = outer 235 | 236 | return env 237 | } 238 | 239 | func NewEnvironment() *Environment { 240 | s := make(map[string]Object) 241 | return &Environment{store: s, outer: nil} 242 | } 243 | 244 | type Environment struct { 245 | store map[string]Object 246 | outer *Environment 247 | } 248 | 249 | func (e *Environment) Get(name string) (Object, bool) { 250 | obj, ok := e.store[name] 251 | if !ok && e.outer != nil { 252 | obj, ok = e.outer.Get(name) 253 | } 254 | return obj, ok 255 | } 256 | 257 | func (e *Environment) Set(name string, val Object) Object { 258 | e.store[name] = val 259 | return val 260 | } 261 | ``` 262 | object.Environment 现在有一个名为 outer 的新字段,它可以包含对另一个 object.Environment 的引用,它是封闭环境,它正在扩展。 NewEnclosedEnvironment 函数使创建这样一个封闭环境变得容易。 获取方法也被改变了。 它现在也检查给定名称的封闭环境。 263 | 264 | 这种新行为反映了我们对变量作用域的看法。 有内部作用域和外部作用域。 如果在内部作用域中找不到某些内容,则会在外部作用域中查找。 外部作用域包含内部作用域。 并且内部范围扩展了外部范围。 265 | 266 | 使用我们更新的 object.Environment 功能,我们可以正确评估函数体。 请记住,问题在于:将函数调用的参数绑定到函数的参数名称时,可能会覆盖环境中现有的绑定。 现在,我们不是覆盖绑定,而是创建一个由当前环境包围的新环境,并将我们的绑定添加到这个新的空环境中。 267 | 268 | 但是我们不会使用当前环境作为封闭环境,不。 相反,我们将使用 *object.Function 携带的环境。 还记得那个吗? 这就是我们定义函数的环境。 269 | 270 | 这是 Eval 的更新版本,它可以完全正确地处理函数调用: 271 | ```go 272 | // evaluator/evaluator.go 273 | func Eval(node ast.Node, env *object.Environment) object.Object { 274 | // [...] 275 | case *ast.CallExpression: 276 | function := Eval(node.Function, env) 277 | if isError(function) { 278 | return function 279 | } 280 | args := evalExpressions(node.Arguments, env) 281 | if len(args) == 1 && isError(args[0]) { 282 | return args[0] 283 | } 284 | 285 | return applyFunction(function, args) 286 | // [...] 287 | } 288 | 289 | func applyFunction(fn object.Object, args []object.Object) object.Object { 290 | function, ok := fn.(*object.Function) 291 | if !ok { 292 | return newError("not a function: %s", fn.Type()) 293 | } 294 | 295 | extendedEnv := extendFunctionEnv(function, args) 296 | evaluated := Eval(function.Body, extendedEnv) 297 | return unwrapReturnValue(evaluated) 298 | } 299 | 300 | func extendFunctionEnv( 301 | fn *object.Function, 302 | args []object.Object, 303 | ) *object.Environment { 304 | env := object.NewEnclosedEnvironment(fn.Env) 305 | 306 | for paramIdx, param := range fn.Parameters { 307 | env.Set(param.Value, args[paramIdx]) 308 | } 309 | 310 | return env 311 | } 312 | func unwrapReturnValue(obj object.Object) object.Object { 313 | if returnValue, ok := obj.(*object.ReturnValue); ok { 314 | return returnValue.Value 315 | } 316 | 317 | return obj 318 | } 319 | ``` 320 | 在新的 applyFunction 函数中,我们不仅检查我们是否真的有一个 *object.Function ,而且还将 fn 参数转换为 *object.Function 引用,以便访问函数的 .Env 和 .Body 字段(哪个对象 .Object 没有定义)。 321 | 322 | extendFunctionEnv 函数创建一个新的 *object.Environment,它被函数的环境包围。 在这个新的封闭环境中,它将函数调用的参数绑定到函数的参数名称。 323 | 324 | 这个新封闭和更新的环境就是评估函数体的环境。 如果此评估的结果是 *object.ReturnValue,则解包。 这是必要的,否则返回语句会冒泡通过几个函数并停止所有函数的评估。 但我们只想停止对最后一个被调用函数体的评估。 这就是为什么我们需要解开它,这样 evalBlockStatement 就不会停止评估“外部”函数中的语句。 我还在之前的 TestReturnStatements 函数中添加了一些测试用例,以确保它有效。 325 | 326 | 那些是最后丢失的部分。 什么? 真的吗? 是的! 看看这个: 327 | ```go 328 | $ go test ./evaluator 329 | ok monkey/evaluator 0.007s 330 | $ go run main.go 331 | Hello mrnugget! This is the Monkey programming language! 332 | Feel free to type in commands 333 | >> let addTwo = fn(x) { x + 2; }; 334 | >> addTwo(2) 335 | 4 336 | >> let multiply = fn(x, y) { x * y }; 337 | >> multiply(50 / 2, 1 * 2) 338 | 50 339 | >> fn(x) { x == 10 }(5) 340 | false 341 | >> fn(x) { x == 10 }(10) 342 | true 343 | ``` 344 | 什么? 是的! 有用! 我们现在终于可以定义和调用函数了! 有句话叫“这没什么好写的”。 嗯,这是! 但在我们戴上派对帽子之前,值得仔细研究一下函数与其环境之间的交互以及它对函数应用的意义。 因为我们所看到的并不是我们所能做的,还有更多。 345 | 346 | 所以,我敢打赌,有一个问题仍然困扰着您:“为什么要扩展函数的环境而不是当前环境?” 简短的回答是这样的: 347 | ```go 348 | // evaluator/evaluator_test.go 349 | 350 | func TestClosures(t *testing.T) { 351 | input := ` 352 | let newAdder = fn(x) { 353 | fn(y) { x + y }; 354 | }; 355 | let addTwo = newAdder(2); 356 | addTwo(2);` 357 | 358 | testIntegerObject(t, testEval(input), 4) 359 | } 360 | ``` 361 | 测试通过。是的: 362 | ```go 363 | $ go run main.go 364 | Hello mrnugget! This is the Monkey programming language! 365 | Feel free to type in commands 366 | >> let newAdder = fn(x) { fn(y) { x + y } }; 367 | >> let addTwo = newAdder(2); 368 | >> addTwo(3); 369 | 5 370 | >> let addThree = newAdder(3); 371 | >> addThree(10); 372 | 13 373 | ``` 374 | Monkey 有闭包,它们已经在我们的解释器中工作。 多么酷啊? 确切地。 很酷。 但是闭包和最初的问题之间的联系可能还不是很清楚。闭包是“关闭”它们定义环境的函数。它们携带自己的环境,无论何时被调用,它们都可以访问它。两者 上面例子中的重要几行是: 375 | ```js 376 | let newAdder = fn(x) { fn(y) { x + y } }; 377 | let addTwo = newAdder(2); 378 | ``` 379 | 这里的 newAdder 是一个高阶函数。 高阶函数是返回其他函数或接收它们作为参数的函数。 在这种情况下,newAdder 返回另一个函数。 但不仅仅是任何函数:一个闭包。 addTwo 绑定到以 2 作为唯一参数调用 newAdder 时返回的闭包。 380 | 381 | 是什么让 addTwo 成为闭包? 它在调用时可以访问的绑定。 382 | 383 | 当 addTwo 被调用时,它不仅可以访问调用的参数,即 y 参数,而且还可以访问在 newAdder(2) 调用时绑定到的值 x,即使该绑定很长时间不在 范围并且不再存在于当前环境中: 384 | ```js 385 | >> let newAdder = fn(x) { fn(y) { x + y } }; 386 | >> let addTwo = newAdder(2); 387 | >> 388 | ERROR: identifier not found: 389 | ``` 390 | x 不绑定到我们顶级环境中的值。 但是 addTwo 仍然可以访问它: 391 | ```js 392 | >> addTwo(3); 393 | 5 394 | ``` 395 | 396 | 换句话说:闭包 addTwo 仍然可以访问在其定义时为当前环境的环境。这是评估 newAdder 主体的最后一行的时间。最后一行是函数字面量。请记住:当评估函数文字时,我们构建一个 object.Function 并在其 .Env 字段中保留对当前环境的引用。 397 | 398 | 当我们稍后评估 addTwo 的主体时,我们不会在当前环境中评估它,而是在函数的环境中评估它。我们通过扩展函数的环境并将其传递给 Eval 而不是当前环境来做到这一点。为什么?所以它仍然可以访问它。为什么?所以我们可以使用闭包。为什么?因为他们太棒了,我爱他们! 399 | 400 | 既然我们在谈论令人惊奇的事情,值得一提的是,我们不仅支持从其他函数返回函数,还支持在函数调用中接受函数作为参数。是的,函数在 Monkey 中是一等公民,我们可以像传递任何其他值一样传递它们: 401 | ```js 402 | >> let add = fn(a, b) { a + b }; 403 | >> let sub = fn(a, b) { a - b }; 404 | >> let applyFunc = fn(a, b, func) { func(a, b) }; 405 | >> applyFunc(2, 2, add); 406 | 4 407 | >> applyFunc(10, 2, sub); 408 | 8 409 | ``` 410 | 这里我们将 add 和 sub 函数作为参数传递给 applyFunc。 然后 applyFunc 毫无问题地调用这个函数:func 参数解析为函数对象,然后使用两个参数调用该函数对象。 没有更多了,一切都已经在我们的解释器中工作了。 411 | 412 | 我知道您现在在想什么,这是您要发送的消息的模板: 413 | 414 | **亲爱的 NAME_OF_FRIEND,还记得我说过有一天我会成为一个人并做一些伟大的事情人们会记住我吗? 嗯,今天是一天。 415 | 我的 Monkey 解释器可以工作,它支持函数、高阶函数、闭包和整数以及算术和长话短说:我的生活从未如此快乐!** 416 | 417 | 我们做到了。 我们构建了一个完整的 Monkey 解释器,支持函数和函数调用、高阶函数和闭包。 加油,庆祝! 我会在这里等的。 418 | 419 | |[< 3.9绑定和环境](3.9.md)|[> 3.11谁来倒垃圾?](3.11.md)| 420 | |-|-| -------------------------------------------------------------------------------- /contents/2/2.4.md: -------------------------------------------------------------------------------- 1 | # 2.4解析器起步:解析LET语句 2 | 在Monkey中,变量绑定语句是如下的形式: 3 | ``` 4 | let x = 5; 5 | let y = 10; 6 | let foobar = add(5,5); 7 | let barfoo = 5 * 5 / 10 + 18 - add(5,5) + multiply(124); 8 | let anotherName = barfoo; 9 | ``` 10 | 这些语句被称为“let 语句”,并且给定一个名字绑定一个值。`let x = 5;`绑定了5给x。在这个章节中我们的工作是正确解析let语句。 11 | 12 | 所以现在,我们将跳过解析产生给定变量绑定值的表达式,稍后再回来——只要我们知道如何让它们自己解析表达式。 13 | 14 | 正确解析let语句是什么意思?这意味着解析器生成一个AST来准确地表示原始let语句中包含的信息。这听起来很合理,但我们还没有AST,也不知道它应该是什么样子。所以我们的第一个任务是仔细查看Monkey源代码,看看它是如何构建的,这样我们就可以定义AST中能够准确表示let语句的必要部分。这是一个完全有效的用Monkey编写的程序: 15 | ``` 16 | let x = 10; 17 | let y = 15; 18 | 19 | let add = fn(a,b) { 20 | return a + b; 21 | }; 22 | ``` 23 | Monkey中的代码是一系列语句。在这个例子中哦我们能看见三个语句,三个变量绑定-let语句-如下形式: 24 | ``` 25 | let = ; 26 | ``` 27 | Monkey中的let语句由两个变化组成:标识符和表达式。在上面的例子中,x,y和add是标识符。10,15和函数文字是表达式。 28 | 29 | 在我们继续之前,有必要就语句和表达式之间的不同说几句。表达式会产生值,语句不会。让x=5不会产生值,而5会(它产生的值为5)。return5;语句不会产生值,但add(5,5)会。这种区别——表达式产生值,语句不会——根据你问的人而变化,但它足以满足我们的需求。 30 | 31 | 一句话精确到为一个表达式或一个语句,它产生就分为无值和有值,取决于编程语言。在某些语言中,函数字面量(例如:fn(x,y){return x + y;})是表达式,可以允许任何其他表达式的任何地方使用。在其他编程语言中,尽管函数文字只能是程序顶层的函数声明语句的一部分。一些语言也有“if 表达式”,其中条件是表达式并产生一个值。 这完全取决于语言设计者所做的选择。 正如你将看到的,Monkey 中的很多东西都是表达式,包括函数字面量。 32 | 33 | 回到我们的AST,看一下下面的例子,我们可以看到它需要两个不同的根类型:表达式(expressions)和语句(statements)。开始看我们的AST: 34 | ```go 35 | // ast.ast.go 36 | 37 | package ast 38 | 39 | type Node interface { 40 | TokenLiteral() string 41 | } 42 | 43 | type Statement interface { 44 | Node 45 | statementNode() 46 | } 47 | 48 | type Expression interface { 49 | Node 50 | expressionNode() 51 | } 52 | ``` 53 | 在这里我们有三个接口叫Node,Statement和Expression。我们每个AST的节点必须实现Node接口,意味着它必须提供一个TokenLiteral()方法来返回与其关联的token的字面值。TokenLiteral()将仅用于调试和测试。我们将要构建的AST仅由相互连接的节点组成——毕竟它是一棵树。其中一些节点实现了Statement和一些Expression接口。这些接口分别称为statementNode和expressionNode的虚拟方法。它们不是绝对必要的,但通过指导Go编译器并可能导致它在我们使用语句时抛出错误来帮助我们,反之亦然。 54 | 55 | 这是我们实现的第一个Node: 56 | ```go 57 | // ast/ast.go 58 | 59 | type Program struct { 60 | Statements []Statement 61 | } 62 | 63 | func (p *Program) TokenLiteral() string { 64 | if len(p.Statements) > 0 { 65 | return p.Statements[0].TokenLiteral() 66 | } else { 67 | return "" 68 | } 69 | } 70 | ``` 71 | Program 节点将会变成我们解析器产生的每个AST根节点。每条生动的Monkey编程都是一系列语句。这些语句包含在Program.Statements中,它只是实现Statement接口的AST节点的一部分 72 | 73 | 定义了AST构造的这些基本结构后,让我们考虑一下以`let x = 5;`形式的变量绑定节点可能是什么样子。它应该有哪些字段?绝对是变量名称之一。并且它还需要一个指向等号右侧表达式的字段。它需要能够指向任何表达式。它不能只指向一个字面值(这种情况下字面量是5),因为每个表达式在等号后都是有效的:`let x = 5 * 5`和`let y = add(2,2) * 5`一样有效。然后节点还需要跟踪AST节点关联的token,所以我们可以实现TokenLiteral()方法。。这产生了三个字段:一个用于标识符,一个用于在let语句中产生值的表达式,一个用于token。 74 | ```go 75 | // ast/ast.go 76 | 77 | import "monkey/token" 78 | 79 | //[...] 80 | 81 | type LetStatement struct { 82 | Token token.Token // the token.LET token 83 | Name *Identifier 84 | Value Expression 85 | } 86 | 87 | func (ls *LetStatement) statementNode() {} 88 | func (ls *LetStatement) TokenLiteral() string { return ls.Token.Literal } 89 | 90 | type Identifier struct { 91 | Token token.Token // the token.IDENT token 92 | Value string 93 | } 94 | 95 | func (i *Identifier) expressionNode() {} 96 | func (i *Identifier) TokenLiteral() string { return i.Token.Literal } 97 | ``` 98 | LetStatement已经有了我们需要的字段:用于保存绑定标识符和用于生成值的表达式的值的名称。 两种方法statementNode和TokenLiteral分别满足Statement和Node接口。 99 | 100 | 为了保存绑定的标识符,让x=5;中的x,我们有Identifier结构类型,它实现了Expression接口。但是let语句中的标识符不会产生值,对吗?所以为什么它是一个表达式呢?这是为了让事情变得简单。Monkey程序其他部分的标识符会产生值,例如:let x = valueProducingIdentifier;。并且为了保持不同节点类型的数量较少,我们将在此处使用I标识符来表示变量绑定中的名称,然后重用它,将标识符表示为完整表达式的一部分或完整表达式。 101 | 102 | 随着`Program`,`letStatement`和`Identifier`定义了这样一段Monkey的源代码: 103 | ``` 104 | let x = 5; 105 | ``` 106 | 可以由一个看起来这样的AST代表: 107 | ![AST](AST.png) 108 | 109 | 现在我们知道了它看起来是怎样的,下一步是构建这样一个AST,没有进一步的ado这里是我们解析器的开头: 110 | ```go 111 | // parser/parser.go 112 | 113 | package parser 114 | 115 | import ( 116 | "monkey/ast" 117 | "monkey/lexer" 118 | "monkey/token" 119 | ) 120 | 121 | type Parser struct { 122 | l *lexer.Lexer 123 | 124 | curToken token.Token 125 | peekToken token.Token 126 | ) 127 | 128 | func New(l *lexer.Lexer) *Parser{ 129 | p := &Parser{;:l} 130 | 131 | // Read two tokens, so curToken and peekToken are both set 132 | p.nextToken() 133 | p.nextToken() 134 | 135 | return p 136 | } 137 | 138 | func (p *Parser) nextToken() { 139 | p.curToken = p.peekToken 140 | p.peekToken = p.l.NextToken() 141 | } 142 | 143 | func (p *Parser) ParseProgram() *ast.Program { 144 | return nil 145 | } 146 | 147 | ``` 148 | 解析器有三个字段,l,curToken和peekToken。l是指向词法分析器实例的指针,我们在其上反复调用NextToken()以获取输入中的下一个token。curToken和peekToken就像我们的词法分析器具有的两个"指针"一样:position和peekPosition。但是相反指向输入中的一个字符,它们指向当前和下一个标记。两者都很重要:我们需要查看curToken,也就是当前未检查的token,来决定下一步做什么,如果curToken没有给我们足够的信息,我们还需要peekToken来做这个决定。想想一行值包含5;。然后curToken是token.INT,我们需要peekToken来决定我们是在行尾还是在算数表达式的开头。 149 | 150 | New函数是不言自明的,而nextToken方法是一个小帮手,它同时推进了curToken和peekToken。但是目前ParseProgram是空的。 151 | 152 | 现在在我们开始写我们的测试并填写ParseProgram方法之前,我想给你展示构建在递归下降解析器背后的基本思想和结构。这使得以后更容易理解我们自己的解析器。下面是这种解析器伪代码的主要部分。仔细阅读并尝试了解parseProgram函数发送了什么: 153 | ``` 154 | function parseProgram() { 155 | program = newProgramASTNode() 156 | 157 | advanceTokens() 158 | for (currentToken() != EOF_TOKEN) { 159 | statement=null 160 | 161 | if(currentToken()==LET_TOKEN) { 162 | statement = parseLetStatement() 163 | } else if (currentToken() == RETURN_TOKEN){ 164 | statement = parseReturnStatement() 165 | } else if(currentToken() == IF_TOKEN) { 166 | statement = parseIfStatement() 167 | } 168 | 169 | if (statement != null) { 170 | program.Statements.push(statement) 171 | } 172 | advanceTokens() 173 | } 174 | return program 175 | } 176 | 177 | function parseLetStatement() { 178 | advanceTokens() 179 | 180 | identifier = parseIdentifier() 181 | 182 | advanceTokens() 183 | 184 | if currentToken() != EQUAL_TOKEN { 185 | parseError("no equal sign!") 186 | return null 187 | } 188 | 189 | advanceTokens() 190 | 191 | value = parseExpression() 192 | 193 | variableStatement = newVariableStatementASTNode() 194 | variableStatement.identifier = identifier 195 | variableStatement.value = value 196 | return variableStatement 197 | } 198 | 199 | function parseIdentifier() { 200 | identifier = newIdentifierASTNode() 201 | identifier.token = currentToken() 202 | return identifier 203 | } 204 | 205 | function parseExpression() { 206 | if (currentToken() == INTEGER_TOKEN) { 207 | if (nextToken() == PLUS_TOKEN) { 208 | return parseOperatorExpression() 209 | } else if (nextToken() == SEMICOLON_TOKEN) { 210 | return parseIntegerLiteral() 211 | } 212 | } else if (currentToken() == LEFT_PAREN) { 213 | return parseGroupedExpression() 214 | } 215 | // [...] 216 | } 217 | function parseOperatorExpression() { 218 | operatorExpression = newOperatorExpression() 219 | 220 | operatorExpression.left = parseIntegerLiteral() 221 | operatorExpression.operator = currentToken() 222 | operatorExpression.right = parseExpression() 223 | 224 | return operatorExpression() 225 | } 226 | // [...] 227 | ``` 228 | 229 | 由于这是伪代码,当然有很多遗漏。 但是递归下降解析背后的基本思想就在那里。 入口点是parseProgram,它构造了AST的根节点(newProgramASTNode())。 然后它通过调用其他函数来构建子节点和语句,这些函数知道基于当前标记构建哪个 AST 节点。这些其他函数再次递归地相互调用。 230 | 231 | 其中最递归的部分是在 parseExpression 中,并且只是被暗示了。 但是我们已经可以看到,为了解析像 5 + 5 这样的表达式,我们需要先 parse5 + 然后再次调用 parseExpression() 来解析其余的,因为 + 之后可能是另一个运算符表达式,像这样:5 + 5 * 10. 稍后我们将详细介绍表达式解析,因为它可能是解析器中最复杂但也是最漂亮的部分,大量使用了“Pratt 解析”。 232 | 233 | 但是现在,我们已经可以看到解析器必须做什么。 它反复推进token并检查当前token以决定下一步做什么:调用另一个解析函数或抛出错误。 然后每个函数完成它的工作并可能构造一个 AST 节点,以便 parseProgram() 中的“主循环”可以推进标记并决定再次做什么 234 | 235 | 如果您查看该伪代码并认为“嗯,这实际上很容易理解”我有个好消息要告诉您:我们的 ParseProgram 方法和解析器看起来非常相似!让我们开始工作吧! 236 | 237 | 再一次,我们从之前的测试开始 我们充实了ParseProgram。 这是一个测试用例,以确保 let 语句的解析有效: 238 | ```go 239 | // parser/parser_test.go 240 | package parser 241 | 242 | import ( 243 | "monkey/ast" 244 | "monkey/lexer" 245 | "testing" 246 | ) 247 | 248 | func TestLetStatements(t *testing.T) { 249 | input := ` 250 | let x = 5; 251 | let y = 10; 252 | let foobar = 838383; 253 | ` 254 | l := lexer.New(input) 255 | p := New(l) 256 | 257 | program := p.ParseProgram() 258 | if program == nil { 259 | t.Fatalf("ParseProgram() returned nil") 260 | } 261 | if len(program.Statements) != 3 { 262 | t.Fatalf("program.Statements does not contain 3 statements. got=%d", 263 | len(program.Statements)) 264 | } 265 | 266 | tests := []struct { 267 | expectedIdentifier string 268 | }{ 269 | {"x"}, 270 | {"y"}, 271 | {"foobar"}, 272 | } 273 | 274 | for i, tt := range tests { 275 | stmt := program.Statements[i] 276 | if !testLetStatement(t, stmt, tt.expectedIdentifier) { 277 | return 278 | } 279 | } 280 | } 281 | 282 | func testLetStatement(t *testing.T, s ast.Statement, name string) bool { 283 | if s.TokenLiteral() != "let" { 284 | t.Errorf("s.TokenLiteral not 'let'. got=%q", s.TokenLiteral()) 285 | return false 286 | } 287 | 288 | letStmt, ok := s.(*ast.LetStatement) 289 | if !ok { 290 | t.Errorf("s not *ast.LetStatement. got=%T", s) 291 | return false 292 | } 293 | 294 | if letStmt.Name.Value != name { 295 | t.Errorf("letStmt.Name.Value not '%s'. got=%s", name, letStmt.Name.Value) 296 | return false 297 | } 298 | 299 | if letStmt.Name.TokenLiteral() != name { 300 | t.Errorf("s.Name not '%s'. got=%s", name, letStmt.Name) 301 | return false 302 | } 303 | 304 | return true 305 | } 306 | ``` 307 | 测试用例遵循与词法分析器的测试以及我们将要编写的几乎所有其他单元测试相同的原则:我们提供 Monkey 源代码作为输入,然后对我们想要的 AST 设置期望——这是由解析器生成的 - 看起来像。 我们通过检查尽可能多的 AST 节点字段来确保没有遗漏任何内容。 我发现解析器是一个错误的基地,它的测试和断言越多越好。 308 | 309 | 我选择不模仿或存根词法分析器并提供源代码作为输入而不是tokens,因为这使测试更具可读性和理解性。当然,词法分析器中的错误会导致解析器的测试失败并产生不必要的错误,但我认为风险太小了,尤其是根据使刻度源代码作为输入的优势来判断。 310 | 311 | 这个测试用例有两个值得注意的地方。第一个是我们忽略了*ast.LetStatement的字段值,为什么我么们不检查整数字(5,10...)是否被解析正确?回答是,我们即将这样做。但首先我们要保证let的解析语句起作用并忽略值。 312 | 313 | 第二个是辅助函数testLetStatement。它可能看起来过于工程化使用一个分离函数,但我们很快就需要这个函数。然后就是将会使我们的测试用例比散落的类型转换的行和行更具可读性。 314 | 315 | 还有一点:我们将不会在本章查看所有解析器测试,因为它们太长了。但是本书的代码将包含这些。 316 | 317 | 话虽如此,但测试如期失败: 318 | ```go 319 | $ go test ./parser 320 | --- FAIL: TestLetStatements (0.00s) 321 | parser_test.go:20: ParseProgram() returned nil 322 | FAIL 323 | FAIL monkey/parser 0.007s 324 | ``` 325 | 326 | 是时候填充在Paser中的ParseProgram()方法了。 327 | ```go 328 | // parser/parser.go 329 | 330 | func (p *Parser) ParseProgram() *ast.Program { 331 | program := &ast.Program{} 332 | program.Statements = []ast.Statement{} 333 | 334 | for p.curToken.Type != token.EOF { 335 | stmt := p.parseStatement() 336 | if stmt != nil { 337 | program.Statements = append(program.Statements, stmt) 338 | } 339 | p.nextToken() 340 | } 341 | 342 | return program 343 | } 344 | ``` 345 | 这看起来是不是很像我们之前看到的parseProgram()伪代码函数?看!我告诉你了!它的作用也是一样的。 346 | 347 | ParseProgram做的第一件事是构造AST的根节点,一个*ast.Program.它然后遍历每一个tokens,直到遇到token.EOF标记。它通过重复调用nextToken来实现这一点,它同时推进p.curToken和p.peekToken。每次迭代中,它都会调用parseStatement,它的工作是解析语句。如果parseStatement返回nil以外的东西,一个ast.Statement,它的返回值被添加到AST根节点的切片。当没有任何东西可以解析时,将返回 *ast.Program根节点。 348 | 349 | parseStatement方法如下所示: 350 | ```go 351 | // parser/parser.go 352 | 353 | func (p *Parser) parseStatement() ast.Statement { 354 | switch p.curToken.Type { 355 | case token.LET: 356 | return p.parseLetStatement() 357 | default: 358 | return nil 359 | } 360 | } 361 | ``` 362 | 别担心,switch语句将会有跟多的分支。但是现在,仅仅只有一个叫parseLetStatement语句当它的情况为token.LET token时。并且parseLetStatement是我们的测试由红色变为绿色的方法: 363 | ```go 364 | // parser/parser.go 365 | 366 | func (p *Parser) parseLetStatement() *ast.LetStatement { 367 | stmt := &ast.LetStatement{Token: p.curToken} 368 | 369 | if !p.expectPeek(token.IDENT) { 370 | return nil 371 | } 372 | 373 | stmt.Name = &ast.Identifier{Token: p.curToken, Value: p.curToken.Literal} 374 | 375 | if !p.expectPeek(token.ASSIGN) { 376 | return nil 377 | } 378 | 379 | // TODO: We're skipping the expressions until we 380 | // encounter a semicolon 381 | for !p.curTokenIs(token.SEMICOLON) { 382 | p.nextToken() 383 | } 384 | 385 | return stmt 386 | } 387 | 388 | func (p *Parser) curTokenIs(t token.TokenType) bool { 389 | return p.curToken.Type == t 390 | } 391 | func (p *Parser) peekTokenIs(t token.TokenType) bool { 392 | return p.peekToken.Type == t 393 | } 394 | 395 | func (p *Parser) expectPeek(t token.TokenType) bool { 396 | if p.peekTokenIs(t) { 397 | p.nextToken() 398 | return true 399 | } else { 400 | return false 401 | } 402 | } 403 | ``` 404 | 它工作了!测试变绿了! 405 | ```go 406 | $ go test ./parser 407 | ok monkey/parser 0.007s 408 | ``` 409 | 我们能解析let语句了!太美妙了,但是等等,怎么样? 410 | 411 | 让我们从parseLetStatement开始。它使用当前所在的token(token.LET)构造一个*ast.LetStatement节点,然后在通过调用expectPeek对下一个token进行断言的同时推进token。首先它需要一个token.IDENT token,然后它使用它来构造一个 *ast.Identifier节点,然后它期待一个等号,最后它跳过等号后面的表达式,直到遇到一个分号。表达式的跳过将被替换,当然,之后我们知道如何解析它们。 412 | 413 | curTokenls和peekTokenls方法不需要太多解释。它们是有用的方法,我们将在充实解析器时一次又一次地看到它们。我们已经可以用!p.curTokenls(token.EOF)替换ParseProgram中的for循环的p.curToken.Type != token.EOF条件。 414 | 415 | 与其剖析这些微小的方法,不如让我们谈谈expectPeek。expectPeek方法是几乎所有解析器共享的"断言函数"之一。它们的主要目的是通过检查下一个token的类型来强制执行token顺序的正确性。我们的expextPeek在这里检查peekToken的类型,只有当类型正确时,它才会通过调用nextToken来推进token。正如你将看到的,这是解析器经常做的事情。 416 | 417 | 但是如果我们在expectPeek中遇到一个不是与其类型的token会发生什么?目前,我们只返回nil,它在ParseProgram中被忽略,导致整个由于输入错误,语句被忽略。默默地,你大概可以想象这使调试变得非常困难。由于没有人喜欢艰难的调试,我们需要向我们的解析器添加错误处理。 418 | 419 | 值得庆幸的是,我们需要做的改变很小: 420 | ```go 421 | // parser/parser.go 422 | type Parser struct { 423 | // [...] 424 | errors []string 425 | // [...] 426 | } 427 | 428 | func New(l *lexer.Lexer) *Parser { 429 | p := &Parser{ 430 | l: l, 431 | errors: []string{}, 432 | } 433 | // [...] 434 | } 435 | 436 | func (p *Parser) Errors() []string { 437 | return p.errors 438 | } 439 | 440 | func (p *Parser) peekError(t token.TokenType) { 441 | msg := fmt.Sprintf("expected next token to be %s, got %s instead", 442 | t, p.peekToken.Type) 443 | p.errors = append(p.errors, msg) 444 | } 445 | ``` 446 | Parser现在有了errors字段,它只是一个字符串切片。该字段在New函数中初始化并且辅助函数peekError现在可用于在错误时添加错误。peekToken的类型与预期不符。使用Errors方法,我们可以检查解析器遇到任何错误。解析器遇到任何错误。 447 | 448 | 扩展测试套件以使用它与你期望的一样简单: 449 | 450 | ```go 451 | // parser/parser_test.go 452 | 453 | func TestLetStatements(t *testing.T) { 454 | // [...] 455 | 456 | program := p.ParseProgram() 457 | checkParserErrors(t, p) 458 | 459 | // [...] 460 | } 461 | func checkParserErrors(t *testing.T, p *Parser) { 462 | errors := p.Errors() 463 | if len(errors) == 0 { 464 | return 465 | } 466 | 467 | t.Errorf("parser has %d errors", len(errors)) 468 | for _, msg := range errors { 469 | t.Errorf("parser error: %q", msg) 470 | } 471 | t.FailNow() 472 | } 473 | ``` 474 | 新的checkParseErrors辅助函数只是检查解析器是否有错误。如果有的话,打印出错误测试并停止执行当前的测试。很直接。但是我们的解析器没有错误的话,通过改变expectPeek,我们可以自动添加每次我们对下一个token的期望错误时都会出现错误: 475 | ```go 476 | // parser/parser.go 477 | 478 | func (p *Parser) expectPeek(t token.TokenType) bool { 479 | if p.peekTokenIs(t) { 480 | p.nextToken() 481 | return true 482 | } else { 483 | p.peekError(t) 484 | return false 485 | } 486 | } 487 | ``` 488 | 如果我们现在改变我们的测试用例输入: 489 | ```go 490 | input := ` 491 | let x = 5; 492 | let y = 10; 493 | let foobar = 838383; 494 | ` 495 | ``` 496 | 到此输入无效,其中缺少token 497 | ```go 498 | input := ` 499 | let x 5; 500 | let = 10; 501 | let 838383; 502 | ` 503 | ``` 504 | 我们能运行我们的测试来看我们的新的解析错误: 505 | ```go 506 | $ go test ./parser 507 | --- FAIL: TestLetStatements (0.00s) 508 | parser_test.go:20: parser has 3 errors 509 | parser_test.go:22: parser error: "expected next token to be =,\ 510 | got INT instead" 511 | parser_test.go:22: parser error: "expected next token to be IDENT,\ 512 | got = instead" 513 | parser_test.go:22: parser error: "expected next token to be IDENT,\ 514 | got INT instead" 515 | FAIL 516 | FAIL monkey/parser 0.007s 517 | ``` 518 | 正如你所看到的,我们的解析器在这里展示了一个简洁的小功能:它为我们遇到的每个错误语句提供错误信息。它不会在第一个退出,这可能会为我们节省一次又一次重新运行解析过程以捕获所有语法错误的繁重工作。这非常有用-即使缺少行号和列号。 519 | |[< 2.3为Monkey语言写一个解析器](2.3.md)|[> 2.5解析返回语句](2.5.md)| 520 | |-------|-----| 521 | 522 | -------------------------------------------------------------------------------- /contents/3/3.5.md: -------------------------------------------------------------------------------- 1 | # 3.5评估表达式 2 | 好吧,我们开始。让我们开始编写Eval!我们有了AST和一个新的对象系统,允许我们跟踪执行Monkey源代码时遇到值。是时候最终评估AST了。 3 | 4 | 以下是 Eval 的签名在其第一个版本中的样子: 5 | ```go 6 | func Eval(node ast.Node) object.Object 7 | ``` 8 | Eval将第一个ast.Node作为输入并返回一个object.Object。记住我们在ast包中定义的每个节点都满足ast.Node接口,因此可以传递给Eval,这允许我们递归地适用Eval并在评估AST的一部分时调用自身。每个AST节点都需要不同形式的评估,而Eval是我们决定这些形式是什么样子的地方。例如,假设我们将*ast.Program节点传递给Eval。然后Eval应该做的是通过使用单个调用自身来评估每个 *ast.Program.Statements语句。外部调用Eval的返回值是上次调用的返回值。 9 | 10 | 我们将从实现自我评估表达式开始。 这就是我们在 Eval 的土地上所说的文字。 具体来说,布尔和整数文字。 它们是 Monkey 中最容易评估的结构,因为它们对自己进行评估。 如果我在 REPL 中输入 5,那么 5 也是应该出现的。 如果我输入 true 那么 true 就是我想要的。、 11 | 12 | 听起来很容易吧?所以,让我们把“输入 5,取回 5”变成现实。 13 | 14 | ## 整数字段 15 | 在编写代码之前,这到底是什么意思? 我们得到一个单独的表达式语句作为输入,它只包含一个整数文字,并希望对其求值以便返回整数本身。 16 | 17 | 翻译成我们系统的语言,这意味着,给定一个 *ast.IntegerLiteral,我们的 Eval 函数应该返回一个 *object.Integer,其 Value 字段包含与 *ast.IntegerLiteral.Value 相同的整数。 18 | 19 | 我们能简单地写一个测试为我们的新evaluator包: 20 | ```go 21 | // evaluator/evaluator_test.go 22 | package evaluator 23 | 24 | import ( 25 | "monkey/lexer" 26 | "monkey/object" 27 | "monkey/parser" 28 | "testing" 29 | ) 30 | 31 | func TestEvalIntegerExpression(t *testing.T) { 32 | tests := []struct { 33 | input string 34 | expected int64 35 | }{ 36 | {"5", 5}, 37 | {"10", 10}, 38 | } 39 | 40 | for _, tt := range tests { 41 | evaluated := testEval(tt.input) 42 | testIntegerObject(t, evaluated, tt.expected) 43 | } 44 | } 45 | 46 | func testEval(input string) object.Object { 47 | l := lexer.New(input) 48 | p := parser.New(l) 49 | program := p.ParseProgram() 50 | 51 | return Eval(program) 52 | } 53 | 54 | func testIntegerObject(t *testing.T, obj object.Object, expected int64) bool { 55 | result, ok := obj.(*object.Integer) 56 | if !ok { 57 | t.Errorf("object is not Integer. got=%T (%+v)", obj, obj) 58 | return false 59 | } 60 | if result.Value != expected { 61 | t.Errorf("object has wrong value. got=%d, want=%d", 62 | result.Value, expected) 63 | return false 64 | } 65 | return true 66 | } 67 | ``` 68 | 这么小的测试需要很多代码,不是吗? 与我们的解析器测试一样,我们在这里建立我们的测试基础设施。 TestEvalIntegerExpression 测试需要增长并且它的当前的结构使这非常容易。 testEval 和 testIntegerObject 也会有很多用处。 69 | 70 | 测试的核心是在 testEval 中调用 Eval。 我们接受我们的输入,将它传递给词法分析器,将词法分析器传递给解析器并返回一个 AST。 然后,这是新的,我们将 AST 传递给 Eval。 我们对 Eval 的返回值进行断言。 在这种情况下,我们希望返回值是一个带有正确 .Value 的 *object.Integer。 换句话说:我们希望 5 评估为 5。 71 | 72 | 当然,测试失败因为我们还没有定义Eval。但是我们已经知道了Eval应该戏曲一个ast.Node作为参数并且返回一个object.Object。并且每当它遇到 *ast.IntegerLiteral 时,它应该返回一个带有正确 .Value 的 *object.Integer。 将其转换为代码并在评估器包中使用此行为定义我们的新 Eval,我们得到: 73 | ```go 74 | // evaluator/evaluator.go 75 | package evaluator 76 | 77 | import ( 78 | "monkey/ast" 79 | "monkey/object" 80 | ) 81 | 82 | func Eval(node ast.Node) object.Object { 83 | switch node := node.(type) { 84 | case *ast.IntegerLiteral: 85 | return &object.Integer{Value: node.Value} 86 | } 87 | 88 | return nil 89 | } 90 | ``` 91 | 这里没有特殊的,它所做的就是我们所说的。除了它不能运行。测试仍然失败了因为Eval返回nil而不是一个*object.Integer。 92 | ```go 93 | $ go test ./evaluator 94 | --- FAIL: TestEvalIntegerExpression (0.00s) 95 | evaluator_test.go:36: object is not Integer. got= () 96 | evaluator_test.go:36: object is not Integer. got= () 97 | FAIL 98 | FAIL monkey/evaluator 0.006s 99 | ``` 100 | 失败的原因是我们从未在 Eval 中遇到过 *ast.IntegerLiteral。 我们不遍历 AST。 我们应该始终从树的顶部开始,接收一个 *ast.Program,然后遍历其中的每个节点。 而这正是我们在这里没有做的。 我们只是在等待 *ast.IntegerLiteral。 解决方法是实际遍历树并评估 *ast.Program 的每个语句: 101 | ```go 102 | // evaluator/evaluator.go 103 | 104 | func Eval(node ast.Node) object.Object { 105 | switch node := node.(type) { 106 | // Statements 107 | case *ast.Program: 108 | return evalStatements(node.Statements) 109 | 110 | case *ast.ExpressionStatement: 111 | return Eval(node.Expression) 112 | 113 | // Expressions 114 | case *ast.IntegerLiteral: 115 | return &object.Integer{Value: node.Value} 116 | } 117 | 118 | return nil 119 | } 120 | 121 | func evalStatements(stmts []ast.Statement) object.Object { 122 | var result object.Object 123 | 124 | for _, statement := range stmts { 125 | result = Eval(statement) 126 | } 127 | return result 128 | } 129 | ``` 130 | 随着这个改变,我们评估每个在Monkey程序语句。如果语句是*ast.ExpressionStatement我们评估它的表达式。这反映了我们得到的AST结构从一行输入,如5:由一个语句、一个表达式语句组成的程序(不是return语句,也不是let语句)以整数文字作为其表达式。 131 | ```go 132 | $ go test ./evaluator 133 | ok monkey/evaluator 0.006s 134 | ``` 135 | 测试通过了!我们能评估整数字段了!大家好,如果我们输入一个数字,就会出现一个数字,我们只需要几千行代码和测试就可以做到这一点!好吧,当然,它看起来并不多。但这是一个开始。我们开始看到评估是如何改变工作的以及我们如何扩展我们的评估器。Eval的结构不会改变,我们只会添加和扩展它。 136 | 137 | 在我们的自我评估表达式列表中,接下来是布尔文字。 但在我们这样做之前,我们应该庆祝我们的第一次评估成功并善待自己。 让我们把 E 放在 REPL 中! 138 | ## 完成REPL 139 | 到目前为止,我们的 REPL 中的 E 缺失,我们只有一个 RPPL - 一个读取解析打印循环。 现在我们有了 Eval,我们可以构建一个真正的 Read-Evaluate-Print-Loop!在 repl 包中使用评估器就像你想象的一样简单: 140 | ```go 141 | // repl/repl.go 142 | import ( 143 | // [...] 144 | "monkey/evaluator" 145 | ) 146 | // [...] 147 | 148 | func Start(in io.Reader, out io.Writer) { 149 | scanner := bufio.NewScanner(in) 150 | 151 | for { 152 | fmt.Printf(PROMPT) 153 | scanned := scanner.Scan() 154 | if !scanned { 155 | return 156 | } 157 | 158 | line := scanner.Text() 159 | l := lexer.New(line) 160 | p := parser.New(l) 161 | 162 | program := p.ParseProgram() 163 | if len(p.Errors()) != 0 { 164 | printParserErrors(out, p.Errors()) 165 | continue 166 | } 167 | 168 | evaluated := evaluator.Eval(program) 169 | if evaluated != nil { 170 | io.WriteString(out, evaluated.Inspect()) 171 | io.WriteString(out, "\n") 172 | } 173 | } 174 | } 175 | ``` 176 | 我们将程序传递给 Eval,而不是打印程序(解析器返回的 AST)。 如果 Eval 返回一个非 nil 值,一个 object.Object,我们将打印其 Inspect() 方法的输出。 在 *object.Integer 的情况下,它将是它包装的整数的字符串表示。 177 | 178 | 有了这个,我们现在有了一个有效的 REPL: 179 | ```go 180 | $ go run main.go 181 | Hello mrnugget! This is the Monkey programming language! 182 | Feel free to type in commands 183 | >> 5 184 | 5 185 | >> 10 186 | 10 187 | >> 999 188 | 999 189 | >> 190 | ``` 191 | 感觉很棒,这就是它吗?词法分析、解析、评估——都在那里。 我们已经走了很长一段路。 192 | ## 布尔字段 193 | 布尔字段就像他们的整数对应物一样,对自己求值。true评估为`true`,false评估为`false`。在 Eval 中实现它就像添加对整数文字的支持一样简单。 测试同样无聊: 194 | ```go 195 | // evaluator/evaluator_test.go 196 | func TestEvalBooleanExpression(t *testing.T) { 197 | tests := []struct { 198 | input string 199 | expected bool 200 | }{ 201 | {"true", true}, 202 | {"false", false}, 203 | } 204 | for _, tt := range tests { 205 | evaluated := testEval(tt.input) 206 | testBooleanObject(t, evaluated, tt.expected) 207 | } 208 | } 209 | func testBooleanObject(t *testing.T, obj object.Object, expected bool) bool { 210 | result, ok := obj.(*object.Boolean) 211 | if !ok { 212 | t.Errorf("object is not Boolean. got=%T (%+v)", obj, obj) 213 | return false 214 | } 215 | if result.Value != expected { 216 | t.Errorf("object has wrong value. got=%t, want=%t", 217 | result.Value, expected) 218 | return false 219 | } 220 | return true 221 | } 222 | ``` 223 | 一旦我们支持更多导致布尔值的表达式,我们就会扩展测试切片。 现在,我们只确保在输入 true 或 false 时得到正确的输出。 测试失败: 224 | ```go 225 | $ go test ./evaluator 226 | --- FAIL: TestEvalBooleanExpression (0.00s) 227 | evaluator_test.go:42: Eval didn't return BooleanObject. got= () 228 | evaluator_test.go:42: Eval didn't return BooleanObject. got= () 229 | FAIL 230 | FAIL monkey/evaluator 0.006s 231 | ``` 232 | 使其通过就像从 *ast.IntegerLiteral 复制 case 分支并更改两个标识符一样简单: 233 | ```go 234 | // evaluator/evaluator.go 235 | func Eval(node ast.Node) object.Object { 236 | // [...] 237 | case *ast.Boolean: 238 | return &object.Boolean{Value: node.Value} 239 | // [...] 240 | } 241 | ``` 242 | 这就对了!让我们在REPL里试一试: 243 | ```go 244 | $ go run main.go 245 | Hello mrnugget! This is the Monkey programming language! 246 | Feel free to type in commands 247 | >> true 248 | true 249 | >> false 250 | false 251 | >> 252 | ``` 253 | 很棒,但是,让我问你:每次遇到 true 或 false 时,我们都在创建一个新的 object.Boolean 是荒谬的,不是吗? 两个true之间没有区别。 false也是一样。 为什么每次都使用新实例? 只有两个可能的值,所以让我们引用它们而不是创建新的。 254 | ```go 255 | // evaluator/evaluator.go 256 | 257 | var ( 258 | TRUE = &object.Boolean{Value: true} 259 | FALSE = &object.Boolean{Value: false} 260 | ) 261 | func Eval(node ast.Node) object.Object { 262 | // [...] 263 | case *ast.Boolean: 264 | return nativeBoolToBooleanObject(node.Value) 265 | // [...] 266 | } 267 | func nativeBoolToBooleanObject(input bool) *object.Boolean { 268 | if input { 269 | return TRUE 270 | } 271 | return FALSE 272 | } 273 | ``` 274 | 现在我们的包中只有两个 object.Boolean 实例:TRUE 和 FALSE,我们引用它们而不是分配新的 object.Booleans。 这更有意义,并且是我们无需大量工作即可获得的微小性能改进。 当我们在做的时候,让我们也处理 null。 275 | 276 | ## NULL 277 | 正如只有一个真和一个假一样,应该只有一个对空值的引用。空值没有变化。 没有有点但不完全空,没有半空,也没有基本上与另一个空相同。 要么是这个 null,要么不是。 因此,让我们创建一个可以在整个评估器中引用的 NULL,而不是创建新的 object.Nulls。 278 | ```go 279 | // evaluator/evaluator.go 280 | var ( 281 | NULL = &object.Null{} 282 | TRUE = &object.Boolean{Value: true} 283 | FALSE = &object.Boolean{Value: false} 284 | ) 285 | ``` 286 | 这就是它在这里的全部。现在我们有一个我们指出的NULL。 287 | 288 | 有了整数文字和我们的 NULL、TRUE 和 FALSE 三个组合,我们就可以评估运算符表达式了。 289 | ## 前缀表达式 290 | Monkey 支持的最简单的运算符表达式形式是前缀表达式或一元运算符表达式,其中一个操作数跟在运算符之后。 在我们的解析器中,我们处理了很多语言结构,比如前缀表达式,因为这是解析它们的最简单方法。 但在本节中,前缀表达式只是一个运算符和一个操作数的运算符表达式。 Monkey 支持其中两个前缀运算符: ! 和 -。 291 | 292 | 评估运算符表达式(尤其是带有前缀运算符和一个操作数)并不难。我们将分步进行,并一点一点地构建所需的行为。 但我们也需要密切关注。 我们即将实施的措施具有深远的影响。 请记住:在评估过程中,输入语言获得意义; 我们正在定义 Monkey 编程语言的语义。 运算符表达式求值的微小变化可能会导致语言中似乎完全不相关的部分出现意外情况。 测试帮助我们确定所需的行为,并作为我们的规范。 293 | 294 | 我们将开始通过实现对!操作符的支持。测试展示了运算符应该“转换”对布尔值并将其取反。 295 | ```go 296 | // evaluator/evaluator_test.go 297 | func TestBangOperator(t *testing.T) { 298 | tests := []struct { 299 | input string 300 | expected bool 301 | }{ 302 | {"!true", false}, 303 | {"!false", true}, 304 | {"!5", false}, 305 | {"!!true", true}, 306 | {"!!false", false}, 307 | {"!!5", true}, 308 | } 309 | 310 | for _, tt := range tests { 311 | evaluated := testEval(tt.input) 312 | testBooleanObject(t, evaluated, tt.expected) 313 | } 314 | } 315 | ``` 316 | 正如我说的,这儿我们决定我们的语言如何工作的地方。`!true`和`!false`表达式和它们的预期结果似乎是常识,但!5可能是其他语言设计人员认为应该返回错误的地方。但是我们在这里要说的是5是“真实的”。 317 | 318 | 测试没有通过,当然,因为Eval返回nil而不是TRUE或者FALSE。第一步是评估前缀表达式是评估它的操作符并且然后使用评估操作符的结果: 319 | ```go 320 | // evaluator/evaluator.go 321 | 322 | func Eval(node ast.Node) object.Object { 323 | // [...] 324 | case *ast.PrefixExpression: 325 | right := Eval(node.Right) 326 | return evalPrefixExpression(node.Operator, right) 327 | // [...] 328 | } 329 | ``` 330 | 在第一次调用 Eval 之后,right 可能是 *object.Integer 或 *object.Boolean 甚至可能是 NULL。 然后我们将这个正确的操作数传递给 evalPrefixExpression,它检查是否支持该运算符: 331 | ```go 332 | // evaluator/evaluator.go 333 | 334 | func evalPrefixExpression(operator string, right object.Object) object.Object { 335 | switch operator { 336 | case "!": 337 | return evalBangOperatorExpression(right) 338 | default: 339 | return NULL 340 | } 341 | } 342 | ``` 343 | 如果不支持该运算符,则返回 NULL。 那是最好的选择吗? 也许,也许不是。 目前,这绝对是最简单的选择,因为我们还没有实现任何错误处理。 344 | 345 | evalBangOperatorExpression 函数是 ! 指定: 346 | ```go 347 | // evaluator/evaluator.go 348 | func evalBangOperatorExpression(right object.Object) object.Object { 349 | switch right { 350 | case TRUE: 351 | return FALSE 352 | case FALSE: 353 | return TRUE 354 | case NULL: 355 | return TRUE 356 | default: 357 | return FALSE 358 | } 359 | } 360 | ``` 361 | 然后测试通过了。 362 | ```go 363 | $ go test ./evaluator 364 | ok monkey/evaluator 0.007s 365 | ``` 366 | 让我们移动-前缀操作符,我们能扩展我们的TestEvalIntegerExpression测试函数合并它: 367 | ```go 368 | // evaluator/evaluator_test.go 369 | 370 | func TestEvalIntegerExpression(t *testing.T) { 371 | tests := []struct { 372 | input string 373 | expected int64 374 | }{ 375 | {"5", 5}, 376 | {"10", 10}, 377 | {"-5", -5}, 378 | {"-10", -10}, 379 | } 380 | // [...] 381 | } 382 | ``` 383 | 出于两个原因,我选择扩展此测试而不是仅为 - 前缀运算符编写新的测试函数。 首先,整数是前缀位置的 - 运算符唯一支持的操作数。 其次,因为这个测试函数应该扩展到包含所有整数运算,以便有一个地方以清晰整洁的方式显示所需的行为。 384 | 385 | 我们必须扩展我们之前编写的 evalPrefixExpression 函数才能使测试用例通过。 switch 语句中需要一个新的分支: 386 | ```go 387 | // evaluator/evaluator.go 388 | func evalPrefixExpression(operator string, right object.Object) object.Object { 389 | switch operator { 390 | case "!": 391 | return evalBangOperatorExpression(right) 392 | case "-": 393 | return evalMinusPrefixOperatorExpression(right) 394 | default: 395 | return NULL 396 | } 397 | } 398 | ``` 399 | evalMinusPrefixOperatorExpression函数看起来像这样: 400 | ```go 401 | // evaluator/evaluator.go 402 | func evalMinusPrefixOperatorExpression(right object.Object) object.Object { 403 | if right.Type() != object.INTEGER_OBJ { 404 | return NULL 405 | } 406 | 407 | value := right.(*object.Integer).Value 408 | return &object.Integer{Value: -value} 409 | } 410 | ``` 411 | 我们在这里做的第一件事是检查操作数是否是整型。如果不是我们返回NULL。但如果是,我们提取*object.Integer 的值。 然后我们分配一个新对象来包装这个值的否定版本 412 | 413 | 那不是有很多代码不是吗?但继续,它工作了: 414 | ```go 415 | $ go test ./evaluator 416 | ok monkey/evaluator 0.007s 417 | ``` 418 | 优秀! 现在我们可以在 REPL 中给我们的前缀表达式一个旋转,然后再转到它们的中缀朋友: 419 | ```go 420 | $ go run main.go 421 | Hello mrnugget! This is the Monkey programming language! 422 | Feel free to type in commands 423 | >> -5 424 | -5 425 | >> !true 426 | false 427 | >> !-5 428 | false 429 | >> !!-5 430 | true 431 | >> !!!!-5 432 | true 433 | >> -true 434 | null 435 | ``` 436 | 很棒! 437 | ## 中缀操作符 438 | 复习一下,以下是 Monkey 支持的八个中缀运算符: 439 | ```go 440 | 5 + 5; 441 | 5 - 5; 442 | 5 * 5; 443 | 5 / 5; 444 | 445 | 5 > 5; 446 | 5 < 5; 447 | 5 == 5; 448 | 5 != 5; 449 | ``` 450 | 这八个运算符可以分为两组:一组运算符生成布尔值作为其结果,另一组不生成布尔值。 我们将从实现对第二组的支持开始:+、-、*、/。 并且首先仅与整数操作数结合使用。 一旦成功,我们将在运算符的任一侧添加对布尔值的支持 451 | 452 | 测试基础设施已经就位。 我们将使用这些新运算符的测试用例扩展我们的 TestEvalIntegerExpression 测试函数: 453 | ```go 454 | // evaluator/evaluator_test.go 455 | func TestEvalBooleanExpression(t *testing.T) { 456 | tests := []struct { 457 | input string 458 | expected bool 459 | }{ 460 | {"true", true}, 461 | {"false", false}, 462 | {"1 < 2", true}, 463 | {"1 > 2", false}, 464 | {"1 < 1", false}, 465 | {"1 > 1", false}, 466 | {"1 == 1", true}, 467 | {"1 != 1", false}, 468 | {"1 == 2", false}, 469 | {"1 != 2", true}, 470 | } 471 | // [...] 472 | } 473 | ``` 474 | 在 evalIntegerInfixExpression 中添加几行是让这些测试通过所需要的全部内容: 475 | ```go 476 | // evaluator/evaluator.go 477 | func evalIntegerInfixExpression( 478 | operator string, 479 | left, right object.Object, 480 | ) object.Object { 481 | leftVal := left.(*object.Integer).Value 482 | rightVal := right.(*object.Integer).Value 483 | switch operator { 484 | // [...] 485 | case "<": 486 | return nativeBoolToBooleanObject(leftVal < rightVal) 487 | case ">": 488 | return nativeBoolToBooleanObject(leftVal > rightVal) 489 | case "==": 490 | return nativeBoolToBooleanObject(leftVal == rightVal) 491 | case "!=": 492 | return nativeBoolToBooleanObject(leftVal != rightVal) 493 | default: 494 | return NULL 495 | } 496 | } 497 | ``` 498 | 我们已经用于布尔文字的 nativeBoolToBooleanObject 函数现在在我们需要根据未包装值之间的比较返回 TRUE 或 FALSE 时找到了一些重用。 499 | 500 | 就是这样! 好吧,至少对于整数。 当两个操作数都是整数时,我们现在完全支持八个中缀运算符。 本节剩下的是添加对布尔操作数的支持。 501 | 502 | Monkey 仅支持相等运算符 == 和 != 的布尔操作数。 它不支持加、减、除和乘布尔值。 也不支持使用 < 或 > 检查 true 是否大于 false。 这将我们的任务简化为仅添加对两个运算符的支持。 503 | 504 | 如您所知,我们要做的第一件事就是添加测试。 而且,和以前一样,我们可以扩展现有的测试功能。 在这种情况下,我们将使用 TestEvalBooleanExpression 并为 == 和 != 运算符添加测试用例: 505 | ```go 506 | // evaluator/evaluator_test.go 507 | func TestEvalBooleanExpression(t *testing.T) { 508 | tests := []struct { 509 | input string 510 | expected bool 511 | }{ 512 | // [...] 513 | {"true == true", true}, 514 | {"false == false", true}, 515 | {"true == false", false}, 516 | {"true != false", true}, 517 | {"false != true", true}, 518 | {"(1 < 2) == true", true}, 519 | {"(1 < 2) == false", false}, 520 | {"(1 > 2) == true", false}, 521 | {"(1 > 2) == false", true}, 522 | } 523 | // [...] 524 | } 525 | ``` 526 | 严格来说,只有前五个案例是测试新的和期望的行为所必需的。 但是让我们也加入其他四个来检查生成的布尔值之间的比较。 527 | 528 | 到现在为止还挺好。 这里没有什么令人惊讶的。 只是另一组失败的测试: 529 | ```go 530 | $ go test ./evaluator 531 | --- FAIL: TestEvalBooleanExpression (0.00s) 532 | evaluator_test.go:121: object is not Boolean. got=*object.Null (&{}) 533 | evaluator_test.go:121: object is not Boolean. got=*object.Null (&{}) 534 | evaluator_test.go:121: object is not Boolean. got=*object.Null (&{}) 535 | evaluator_test.go:121: object is not Boolean. got=*object.Null (&{}) 536 | evaluator_test.go:121: object is not Boolean. got=*object.Null (&{}) 537 | evaluator_test.go:121: object is not Boolean. got=*object.Null (&{}) 538 | evaluator_test.go:121: object is not Boolean. got=*object.Null (&{}) 539 | evaluator_test.go:121: object is not Boolean. got=*object.Null (&{}) 540 | evaluator_test.go:121: object is not Boolean. got=*object.Null (&{}) 541 | FAIL 542 | FAIL monkey/evaluator 0.007s 543 | ``` 544 | 这里有一些让这些测试通过的好方法: 545 | ```go 546 | // evaluator/evaluator.go 547 | 548 | func evalInfixExpression( 549 | operator string, 550 | left, right object.Object, 551 | ) object.Object { 552 | switch { 553 | // [...] 554 | case operator == "==": 555 | return nativeBoolToBooleanObject(left == right) 556 | case operator == "!=": 557 | return nativeBoolToBooleanObject(left != right) 558 | default: 559 | return NULL 560 | } 561 | } 562 | ``` 563 | 恩,那就对了。 我们只在现有的 evalInfixExpression 中添加四行,测试就通过了。 我们在这里使用指针比较来检查布尔值之间的相等性。 这是有效的,因为我们总是使用指向我们对象的指针,而在布尔值的情况下,我们只使用两个:TRUE 和 FALSE。 因此,如果某事物具有与 TRUE(即内存地址)相同的值,那么它就是真的。 这也适用于 NULL。 564 | 565 | 这不适用于我们稍后可能添加的整数或其他数据类型。 在 *object.Integer 的情况下,我们总是分配 object.Integer 的新实例,因此使用新的指针。 我们不能将这些指针与不同的实例进行比较,否则 5 == 5 将是错误的,这不是我们想要的。 在这种情况下,我们希望显式比较值而不是包装这些值的对象。 566 | 567 | 这就是为什么整数操作数的检查必须在 switch 语句中更高并且比这些新添加的 case 分支更早匹配。 只要我们照顾其他在到达这些指针比较之前的操作数类型我们很好并且它有效。 568 | 569 | 十年后,当 Monkey 是一门著名的编程语言,而关于研究忽略了设计编程语言的业余爱好者的讨论仍在进行中,我们既富有又出名时,有人会在 StackOverflow 上问为什么 Monkey 中的整数比较比布尔比较慢。 答案将由你或我写,我们中的一个人会说 Monkey 的对象系统不允许对整数对象进行指针比较。 在进行比较之前,它必须解开该值。 因此布尔值之间的比较更快。 我们将添加一个“来源:我写的”。 到我们答案的底部,并获得闻所未闻的业力。 570 | 571 | 我离题了。 回到主题,让我说:哇! 我们做到了! 我知道,我对我的赞美非常慷慨,可以很容易地找到庆祝的理由,但如果有时间开香槟,那就是现在。 是的,我们做到了。 看看我们的解释器现在可以做什么: 572 | ```go 573 | $ go run main.go 574 | Hello mrnugget! This is the Monkey programming language! 575 | Feel free to type in commands 576 | >> 5 * 5 + 10 577 | 35 578 | >> 3 + 4 * 5 == 3 * 1 + 4 * 5 579 | true 580 | >> 5 * 10 > 40 + 5 581 | true 582 | >> (10 + 2) * 30 == 300 + 20 * 3 583 | true 584 | >> (5 > 5 == true) != false 585 | false 586 | >> 500 / 2 != 250 587 | false 588 | ``` 589 | 所以,现在我们有一个功能齐全的计算器,可以做更多的事情。让我们给他更多。让我们让它看起来更像一种编程语言。 590 | |[< 3.4表示对象](3.4.md)|[> 3.6条件句](3.6.md)| 591 | |-|-| 592 | 593 | -------------------------------------------------------------------------------- /contents/4/4.5.md: -------------------------------------------------------------------------------- 1 | # 4.5哈希 2 | 我们要添加的下一个数据类型称为“哈希”。 Monkey 中的哈希有时在其他编程语言中被称为散列、映射、哈希映射或字典。 它将键映射到值。 3 | 4 | 为了在 Monkey 中构造哈希,可以使用哈希文字:用大括号括起来的以逗号分隔的键值对列表。 每个键值对使用冒号来区分键和值。 这是使用哈希文字的样子。 5 | ```js 6 | >> let myHash = {"name": "Jimmy", "age": 72, "band": "Led Zeppelin"}; 7 | >> myHash["name"] 8 | Jimmy 9 | >> myHash["age"] 10 | 72 11 | >> myHash["band"] 12 | Led Zeppelin 13 | ``` 14 | 在这个例子中,myHash 包含三个键值对。 键都是字符串。 而且,如您所见,我们可以使用索引运算符表达式再次从哈希中获取值,就像我们可以使用数组一样。 除了在这个例子中索引值是字符串,它不适用于数组。 这甚至不是唯一可用作哈希键的数据类型: 15 | ```js 16 | >> let myHash = {true: "yes, a boolean", 99: "correct, an integer"}; 17 | >> myHash[true] 18 | yes, a boolean 19 | >> myHash[99] 20 | correct, an integer 21 | ``` 22 | 这也是有效的。事实上,除了字符串,整数和布尔值,我们还可以使用任何表达式作为索引运算符表达式中的索引: 23 | ```js 24 | >> myHash[5 > 1] 25 | yes, a boolean 26 | >> myHash[100 - 1] 27 | correct, an integer 28 | ``` 29 | 只要这些表达式的计算结果为字符串、整数或布尔值,它们就可以用作哈希键。 这里 5 > 1 的计算结果为 true , 100 - 1 的计算结果为 99,它们都是有效的并映射到 myHash 中的值。 30 | 31 | 不出所料,我们的实现将使用 Go 的 map 作为 Monkey 哈希的底层数据结构。 但是由于我们想要交替使用字符串、整数和布尔值作为键,我们需要在普通的旧映射之上构建一些东西以使其工作。 当我们扩展我们的对象系统时,我们会谈到这一点。 但首先我们必须将哈希文字转换为tokens。 32 | ## 词法分析哈希文字 33 | 我们如何将哈希文字转换为token? 我们需要在词法分析器中识别和输出哪些标记,以便我们以后可以在解析器中使用它们? 这是上面的哈希文字: 34 | ```js 35 | {"name": "Jimmy", "age": 72, "band": "Led Zeppelin"} 36 | ``` 37 | 除了字符串文字之外,这里还使用了四个重要的字符:{,},和:。 我们已经知道如何对前三个进行词法分析。 我们的词法分析器将它们分别转换为 token.LBRACE、token.RBRACE 和 token.COMMA。 这意味着,我们在本节中要做的就是将 : 变成一个token。 为此,我们首先需要在token包中定义必要的token类型: 38 | ```go 39 | // token/token.go 40 | 41 | const ( 42 | // [...] 43 | COLON = ":" 44 | // [...] 45 | ) 46 | ``` 47 | 接下来,我们将为需要 token.COLON 的词法分析器的 NextToken 方法添加一个新测试: 48 | ```go 49 | // lexer/lexer_test.go 50 | 51 | func TestNextToken(t *testing.T) { 52 | input := `let five = 5; 53 | let ten = 10; 54 | 55 | let add = fn(x, y) { 56 | x + y; 57 | }; 58 | 59 | let result = add(five, ten); 60 | !-/*5; 61 | 5 < 10 > 5; 62 | 63 | if (5 < 10) { 64 | return true; 65 | } else { 66 | return false; 67 | } 68 | 69 | 10 == 10; 70 | 10 != 9; 71 | "foobar" 72 | "foo bar" 73 | [1, 2]; 74 | {"foo": "bar"} 75 | ` 76 | 77 | tests := []struct { 78 | expectedType token.TokenType 79 | expectedLiteral string 80 | }{ 81 | // [...] 82 | {token.LBRACE, "{"}, 83 | {token.STRING, "foo"}, 84 | {token.COLON, ":"}, 85 | {token.STRING, "bar"}, 86 | {token.RBRACE, "}"}, 87 | {token.EOF, ""}, 88 | } 89 | 90 | // [...] 91 | } 92 | ``` 93 | 我们可以避免在测试输入中添加一个 : ,但是在稍后阅读和最终调试测试时使用我们在这里所做的哈希文字提供了更多的上下文。 94 | 95 | 将 : 转换为 token.COLON 非常简单: 96 | ```go 97 | // lexer/lexer.go 98 | 99 | func (l *Lexer) NextToken() token.Token { 100 | // [...] 101 | case ':': 102 | tok = newToken(token.COLON, l.ch) 103 | // [...] 104 | } 105 | ``` 106 | 只有两个新行,词法分析器现在吐出 token.COLON: 107 | ```go 108 | $ go test ./lexer 109 | ok monkey/lexer 0.006s 110 | ``` 111 | Boom!词法分析器现在返回 token.LBRACE、token.RBRACE、token.COMMA 和新的 token.COLON。 这就是我们解析哈希文字所需的全部内容。 112 | ## 解析哈希文字 113 | 在我们开始处理我们的解析器甚至编写测试之前,让我们看看哈希文字的基本语法结构: 114 | ```js 115 | { : , : , ... } 116 | ``` 117 | 这是一个逗号分隔的对列表。 每对由两个表达式组成。 一个产生哈希键,一个产生值。 键与值之间用冒号分隔。 该列表由一对花括号括起来。 118 | 119 | 当我们把它变成一个 AST 节点时,我们必须跟踪键值对。 现在我们将如何做到这一点? 是的,我们将使用映射,但是该映射中的键和值是什么类型的? 120 | 121 | 这是因为很多不同的表达式都可以产生字符串、整数或布尔值。 不仅仅是它们的字面形式。 在解析阶段强制执行哈希键的数据类型会阻止我们做这样的事情: 122 | ```go 123 | let key = "name"; 124 | let hash = {key: "Monkey"}; 125 | ``` 126 | 这里 key 的计算结果为“name”,因此作为哈希键是完全有效的,即使它是一个标识符。为了实现这一点,我们需要允许任何表达式作为键和任何表达式作为哈希文字中的值。 至少在解析阶段。 接着我们的 ast.HashLiteral 定义如下所示: 127 | ```go 128 | // ast/ast.go 129 | 130 | type HashLiteral struct { 131 | Token token.Token // the '{' token 132 | Pairs map[Expression]Expression 133 | } 134 | func (hl *HashLiteral) expressionNode() {} 135 | func (hl *HashLiteral) TokenLiteral() string { return hl.Token.Literal } 136 | func (hl *HashLiteral) String() string { 137 | var out bytes.Buffer 138 | 139 | pairs := []string{} 140 | for key, value := range hl.Pairs { 141 | pairs = append(pairs, key.String()+":"+value.String()) 142 | } 143 | 144 | out.WriteString("{") 145 | out.WriteString(strings.Join(pairs, ", ")) 146 | out.WriteString("}") 147 | 148 | return out.String() 149 | } 150 | ``` 151 | 现在我们已经清楚哈希文字的结构并定义了 ast.HashLiteral,我们可以为我们的解析器编写测试: 152 | ```go 153 | // parser/parser_test.go 154 | 155 | func TestParsingHashLiteralsStringKeys(t *testing.T) { 156 | input := `{"one": 1, "two": 2, "three": 3}` 157 | 158 | l := lexer.New(input) 159 | p := New(l) 160 | program := p.ParseProgram() 161 | checkParserErrors(t, p) 162 | 163 | stmt := program.Statements[0].(*ast.ExpressionStatement) 164 | hash, ok := stmt.Expression.(*ast.HashLiteral) 165 | if !ok { 166 | t.Fatalf("exp is not ast.HashLiteral. got=%T", stmt.Expression) 167 | } 168 | 169 | if len(hash.Pairs) != 3 { 170 | t.Errorf("hash.Pairs has wrong length. got=%d", len(hash.Pairs)) 171 | } 172 | 173 | expected := map[string]int64{ 174 | "one": 1, 175 | "two": 2, 176 | "three": 3, 177 | } 178 | 179 | for key, value := range hash.Pairs { 180 | literal, ok := key.(*ast.StringLiteral) 181 | if !ok { 182 | t.Errorf("key is not ast.StringLiteral. got=%T", key) 183 | } 184 | 185 | expectedValue := expected[literal.String()] 186 | 187 | testIntegerLiteral(t, value, expectedValue) 188 | } 189 | } 190 | ``` 191 | 当然,我们还必须确保正确解析空哈希文字,因为这种边缘情况是编程中所有脱发的根源: 192 | ```go 193 | // parser/parser_test.go 194 | 195 | func TestParsingEmptyHashLiteral(t *testing.T) { 196 | input := "{}" 197 | 198 | l := lexer.New(input) 199 | p := New(l) 200 | program := p.ParseProgram() 201 | checkParserErrors(t, p) 202 | 203 | stmt := program.Statements[0].(*ast.ExpressionStatement) 204 | hash, ok := stmt.Expression.(*ast.HashLiteral) 205 | if !ok { 206 | t.Fatalf("exp is not ast.HashLiteral. got=%T", stmt.Expression) 207 | } 208 | 209 | if len(hash.Pairs) != 0 { 210 | t.Errorf("hash.Pairs has wrong length. got=%d", len(hash.Pairs)) 211 | } 212 | } 213 | ``` 214 | 我还添加了两个类似于 TestHashLiteralStringKeys 的测试,但使用整数和布尔值作为哈希键,并确保解析器将它们分别转换为 *ast.IntegerLiteral 和 *ast.Boolean。 然后是第五个测试函数,它确保哈希文字中的值可以是任何表达式,甚至是运算符表达式。 它看起来像这样: 215 | ```go 216 | // parser/parser_test.go 217 | 218 | func TestParsingHashLiteralsWithExpressions(t *testing.T) { 219 | input := `{"one": 0 + 1, "two": 10 - 8, "three": 15 / 5}` 220 | 221 | l := lexer.New(input) 222 | p := New(l) 223 | program := p.ParseProgram() 224 | checkParserErrors(t, p) 225 | 226 | stmt := program.Statements[0].(*ast.ExpressionStatement) 227 | hash, ok := stmt.Expression.(*ast.HashLiteral) 228 | if !ok { 229 | t.Fatalf("exp is not ast.HashLiteral. got=%T", stmt.Expression) 230 | } 231 | 232 | if len(hash.Pairs) != 3 { 233 | t.Errorf("hash.Pairs has wrong length. got=%d", len(hash.Pairs)) 234 | } 235 | 236 | tests := map[string]func(ast.Expression){ 237 | "one": func(e ast.Expression) { 238 | testInfixExpression(t, e, 0, "+", 1) 239 | }, 240 | "two": func(e ast.Expression) { 241 | testInfixExpression(t, e, 10, "-", 8) 242 | }, 243 | "three": func(e ast.Expression) { 244 | testInfixExpression(t, e, 15, "/", 5) 245 | }, 246 | } 247 | 248 | for key, value := range hash.Pairs { 249 | literal, ok := key.(*ast.StringLiteral) 250 | if !ok { 251 | t.Errorf("key is not ast.StringLiteral. got=%T", key) 252 | continue 253 | } 254 | testFunc, ok := tests[literal.String()] 255 | if !ok { 256 | t.Errorf("No test function for key %q found", literal.String()) 257 | continue 258 | } 259 | 260 | testFunc(value) 261 | } 262 | } 263 | ``` 264 | 那么所有这些测试功能的表现如何呢? 不那么好,说实话。 我们遇到了很多失败和解析器错误: 265 | ```go 266 | $ go test ./parser 267 | --- FAIL: TestParsingEmptyHashLiteral (0.00s) 268 | parser_test.go:1173: parser has 2 errors 269 | parser_test.go:1175: parser error: "no prefix parse function for { found" 270 | parser_test.go:1175: parser error: "no prefix parse function for } found" 271 | --- FAIL: TestParsingHashLiteralsStringKeys (0.00s) 272 | parser_test.go:1173: parser has 7 errors 273 | parser_test.go:1175: parser error: "no prefix parse function for { found" 274 | [... more errors ...] 275 | --- FAIL: TestParsingHashLiteralsBooleanKeys (0.00s) 276 | parser_test.go:1173: parser has 5 errors 277 | parser_test.go:1175: parser error: "no prefix parse function for { found" 278 | [... more errors ...] 279 | --- FAIL: TestParsingHashLiteralsIntegerKeys (0.00s) 280 | parser_test.go:967: parser has 7 errors 281 | parser_test.go:969: parser error: "no prefix parse function for { found" 282 | [... more errors ...] 283 | --- FAIL: TestParsingHashLiteralsWithExpressions (0.00s) 284 | parser_test.go:1173: parser has 7 errors 285 | parser_test.go:1175: parser error: "no prefix parse function for { found" 286 | [... more errors ...] 287 | FAIL 288 | FAIL monkey/parser 0.008s 289 | ``` 290 | 这听起来可能令人难以置信,但有个好消息:只需要一个函数就可以通过所有这些测试。 确切地说,是一个 prefixParseFn。 由于哈希文字的 token.LBRACE 位于前缀位置,就像数组文字的 token.LBRACKET 一样,我们可以将 parseHashLiteral 方法定义为 prefixParseFn: 291 | ```go 292 | // parser/parser.go 293 | 294 | func New(l *lexer.Lexer) *Parser { 295 | // [...] 296 | p.registerPrefix(token.LBRACE, p.parseHashLiteral) 297 | // [...] 298 | } 299 | 300 | func (p *Parser) parseHashLiteral() ast.Expression { 301 | hash := &ast.HashLiteral{Token: p.curToken} 302 | hash.Pairs = make(map[ast.Expression]ast.Expression) 303 | 304 | for !p.peekTokenIs(token.RBRACE) { 305 | p.nextToken() 306 | key := p.parseExpression(LOWEST) 307 | 308 | if !p.expectPeek(token.COLON) { 309 | return nil 310 | } 311 | 312 | p.nextToken() 313 | value := p.parseExpression(LOWEST) 314 | 315 | hash.Pairs[key] = value 316 | 317 | if !p.peekTokenIs(token.RBRACE) && !p.expectPeek(token.COMMA) { 318 | return nil 319 | } 320 | } 321 | 322 | if !p.expectPeek(token.RBRACE) { 323 | return nil 324 | } 325 | 326 | return hash 327 | } 328 | ``` 329 | 它可能看起来很吓人,但 parseHashLiteral 中没有我们以前没见过的东西。 它仅通过检查结束标记.RBRACE 并调用 parseExpression 两次来遍历键值表达式对。 那和 hash.Pairs 的填充是这个方法最重要的部分。 它很好地完成了它的工作: 330 | ```go 331 | $ go test ./parser 332 | ok monkey/parser 0.006s 333 | ``` 334 | 我们所有的解析器测试都通过了! 从我们添加的测试数量来看,我们可以合理地确定我们的解析器现在知道如何解析哈希文字。 这意味着我们现在来到了向解释器添加哈希的最有趣的部分:在对象系统中表示它们并评估哈希文字。 335 | # 哈希对象 336 | 除了扩展词法分析器和解析器之外,添加新的数据类型也意味着在对象系统中表示它。 我们成功地为整数、字符串和数组做到了这一点。 但是,虽然实现这些其他数据类型只是意味着定义一个具有正确类型的 .Value 字段的结构,但哈希需要更多的努力。 让我解释一下原因。 假设我们定义了一个新的 object.Hash 类型,如下所示: 337 | ```go 338 | type Hash struct { 339 | Pairs map[Object]Object 340 | } 341 | ``` 342 | 这是基于 Go 的 map 实现 Hash 数据类型的最明显的选择。 但是有了这个定义,我们将如何填充 Pairs 映射? 更重要的是,我们如何从中获得价值? 343 | 344 | 考虑这段 Monkey 代码: 345 | ```js 346 | let hash = {"name": "Monkey"}; 347 | hash["name"] 348 | ``` 349 | 假设我们正在评估这两行并使用上面的 object.Hash 定义。 在评估第一行中的哈希文字时,我们将每个键值对放入 map[Object]Object 映射中,导致 .Pairs 具有以下映射:一个 *object.String 与 .Value 被"name"映射 到一个 *object.String 与 .Value 是“Monkey”。 350 | 351 | 到现在为止还挺好。 但是问题出现在我们使用索引表达式尝试访问"Monkey"字符串的第二行。 352 | 353 | 在第二行中,索引表达式的"name"字符串字面量计算为一个新的、新分配的 *object.String。 即使这个新的 *object.String 在其 .Value 字段中也包含“name”,就像 Pairs 中的另一个 *object.String 一样,我们不能使用新的来检索“Monkey”。 354 | 355 | 原因是它们是指向不同内存位置的指针。 它们指向的内存位置的内容相同("name")这一事实无关紧要。 比较这些指针会告诉我们它们不相等。 这意味着使用新创建的 *object.String 作为键不会让我们得到"Monkey"。 这就是指针和它们之间的比较在 Go 中的工作方式。 356 | 357 | 这是一个示例,演示了我们在使用上面的 object.Hash 实现时会遇到的问题: 358 | ```go 359 | name1 := &object.String{Value: "name"} 360 | monkey := &object.String{Value: "Monkey"} 361 | 362 | pairs := map[object.Object]object.Object{} 363 | pairs[name1] = monkey 364 | 365 | fmt.Printf("pairs[name1]=%+v\n", pairs[name1]) 366 | // => pairs[name1]=&{Value:Monkey} 367 | 368 | name2 := &object.String{Value: "name"} 369 | fmt.Printf("pairs[name2]=%+v\n", pairs[name2]) 370 | // => pairs[name2]= 371 | 372 | fmt.Printf("(name1 == name2)=%t\n", name1 == name2) 373 | // => (name1 == name2)=false 374 | ``` 375 | 作为这个问题的解决方案,我们可以遍历 .Pairs 中的每个键,检查它是否是 *object.String 并将其 .Value 与索引表达式中键的 .Value 进行比较。 我们会通过这种方式找到匹配的值,但是这种方法将给定键的查找时间从 O(1) 变成了 O(n),首先违背了使用哈希的全部目的。 376 | 377 | 另一种选择是将 Pairs 定义为 map[string]Object,然后使用 *object.String 的 .Value 作为键。 这有效,但不适用于整数和布尔值。 378 | 379 | 不,我们需要的是一种为对象生成哈希的方法,我们可以轻松地比较这些哈希并将其用作 object.Hash 中的哈希键。 我们需要能够为 *object.String 生成一个哈希键,该哈希键与另一个具有相同 .Value 的 *object.String 的哈希键相当并相等。 *object.Integer 和 *object.Boolean 也是如此。 但是 *object.String 的散列键永远不能 等于 *object.Integer 或 *object.Boolean 的散列键。 在类型之间,哈希键必须始终不同。 380 | 381 | 我们可以在对象系统中的一组测试函数中表达所需的行为: 382 | ```go 383 | // object/object_test.go 384 | package object 385 | 386 | import "testing" 387 | 388 | func TestStringHashKey(t *testing.T) { 389 | hello1 := &String{Value: "Hello World"} 390 | hello2 := &String{Value: "Hello World"} 391 | diff1 := &String{Value: "My name is johnny"} 392 | diff2 := &String{Value: "My name is johnny"} 393 | 394 | if hello1.HashKey() != hello2.HashKey() { 395 | t.Errorf("strings with same content have different hash keys") 396 | } 397 | 398 | if diff1.HashKey() != diff2.HashKey() { 399 | t.Errorf("strings with same content have different hash keys") 400 | } 401 | 402 | if hello1.HashKey() == diff1.HashKey() { 403 | t.Errorf("strings with different content have same hash keys") 404 | } 405 | } 406 | ``` 407 | 这正是我们想要的 HashKey() 方法。 不仅针对*object.String,还针对 *object.Boolean 和 *object.Integer,这就是为什么它们也存在相同的测试函数的原因。 为了阻止测试失败,我们需要在三种类型中的每一种上实现 HashKey() 方法: 408 | ```go 409 | // object/object.go 410 | 411 | import ( 412 | // [...] 413 | "hash/fnv" 414 | ) 415 | 416 | type HashKey struct { 417 | Type ObjectType 418 | Value uint64 419 | } 420 | 421 | func (b *Boolean) HashKey() HashKey { 422 | var value uint64 423 | 424 | if b.Value { 425 | value = 1 426 | } else { 427 | value = 0 428 | } 429 | 430 | return HashKey{Type: b.Type(), Value: value} 431 | } 432 | 433 | func (i *Integer) HashKey() HashKey { 434 | return HashKey{Type: i.Type(), Value: uint64(i.Value)} 435 | } 436 | 437 | func (s *String) HashKey() HashKey { 438 | h := fnv.New64a() 439 | h.Write([]byte(s.Value)) 440 | return HashKey{Type: s.Type(), Value: h.Sum64()} 441 | } 442 | ``` 443 | 每个 HashKey() 方法都返回一个 HashKey。 正如您在其定义中所见,HashKey 没什么特别的。 Type 字段包含一个 ObjectType,因此有效地将 HashKeys“范围”到不同的对象类型。 Value 字段保存实际的哈希值,它只是一个整数。 由于它只是两个整数,我们可以使用 == 运算符轻松地将一个 HashKey 与另一个 HashKey 进行比较。 这也使 HashKey 可用作 Go 映射中的键。 444 | 445 | 我们之前演示的问题是通过使用这个新定义的 HashKey 和 HashKey() 方法解决的: 446 | ```go 447 | name1 := &object.String{Value: "name"} 448 | monkey := &object.String{Value: "Monkey"} 449 | 450 | pairs := map[object.HashKey]object.Object{} 451 | pairs[name1.HashKey()] = monkey 452 | 453 | fmt.Printf("pairs[name1.HashKey()]=%+v\n", pairs[name1.HashKey()]) 454 | // => pairs[name1.HashKey()]=&{Value:Monkey} 455 | 456 | name2 := &object.String{Value: "name"} 457 | fmt.Printf("pairs[name2.HashKey()]=%+v\n", pairs[name2.HashKey()]) 458 | // => pairs[name2.HashKey()]=&{Value:Monkey} 459 | 460 | fmt.Printf("(name1 == name2)=%t\n", name1 == name2) 461 | // => (name1 == name2)=false 462 | 463 | fmt.Printf("(name1.HashKey() == name2.HashKey())=%t\n", 464 | name1.HashKey() == name2.HashKey()) 465 | // => (name1.HashKey() == name2.HashKey())=true 466 | ``` 467 | 这正是我们想要的! HashKey 定义和 HashKey() 方法实现解决了我们在原始 Hash 定义中遇到的问题。 它们还使测试通过: 468 | ```go 469 | $ go test ./object 470 | ok monkey/object 0.008s 471 | ``` 472 | 现在我们可以定义 object.Hash 并使用这个新的 HashKey 类型: 473 | ```go 474 | // object/object.go 475 | 476 | const ( 477 | // [...] 478 | HASH_OBJ = "HASH" 479 | ) 480 | 481 | type HashPair struct { 482 | Key Object 483 | Value Object 484 | } 485 | 486 | type Hash struct { 487 | Pairs map[HashKey]HashPair 488 | } 489 | 490 | func (h *Hash) Type() ObjectType { return HASH_OBJ } 491 | ``` 492 | 这增加了 Hash 和 HashPair 的定义。 HashPair 是 Hash.Pairs 中值的类型。 您可能想知道为什么我们使用它而不只是将 Pairs 定义为 map[HashKey]Object。 493 | 494 | 原因是Hash的Inspect()方法。 当我们稍后在 REPL 中打印 Monkey 哈希时,我们希望打印哈希中包含的值及其键。 而仅仅打印 HashKeys 并不是很有用。 所以我们通过使用 HashPairs 作为值来跟踪生成 HashKeys 的对象,我们保存原始键对象和它映射到的值对象。 这样我们就可以调用关键对象的 Inspect() 方法来为 *object.Hash 生成 Inspect() 输出。 这里说的是 Inspect() 方法: 495 | ```go 496 | // object/object.go 497 | 498 | func (h *Hash) Inspect() string { 499 | var out bytes.Buffer 500 | 501 | pairs := []string{} 502 | for _, pair := range h.Pairs { 503 | pairs = append(pairs, fmt.Sprintf("%s: %s", 504 | pair.Key.Inspect(), pair.Value.Inspect())) 505 | } 506 | 507 | out.WriteString("{") 508 | out.WriteString(strings.Join(pairs, ", ")) 509 | out.WriteString("}") 510 | 511 | return out.String() 512 | } 513 | ``` 514 | Inspect() 方法并不是跟踪生成 HashKey 的对象的唯一原因。 如果我们要为 Monkey 哈希实现类似范围函数的东西,这也是必要的,它迭代哈希中的键和值。 或者,如果我们想添加一个 firstPair 函数,该函数将给定哈希的第一个键和值作为数组返回。 或者如果我们想要......你明白了。 跟踪键非常有用,尽管目前只有 Inspect() 方法有好处。 515 | 516 | 就是这样! 这就是 object.Hash 的整个实现。 但是当我们仍然打开object包时,我们应该做一件小事: 517 | ```go 518 | // object/object.go 519 | 520 | type Hashable interface { 521 | HashKey() HashKey 522 | } 523 | ``` 524 | 我们可以在我们的评估器中使用这个接口来检查给定对象是否可用作哈希键,当我们评估哈希文字或索引表达式时。 目前仅由 *object.String、 *object.Boolean 和 *object.Integer 实现。 当然,在继续之前我们还可以做一件事:我们可以通过缓存它们的返回值来优化 HashKey() 方法的性能,但这对于注重性能的读者来说听起来是一个很好的练习。 525 | ## 评估哈希文字 526 | 我们即将开始评估哈希文字,我会完全诚实地对你说:向我们的解释器添加哈希最困难的部分已经结束。 从此一帆风顺。 因此,让我们享受旅程,放松并编写测试: 527 | ```go 528 | // evaluator/evaluator_test.go 529 | 530 | func TestHashLiterals(t *testing.T) { 531 | input := `let two = "two"; 532 | { 533 | "one": 10 - 9, 534 | two: 1 + 1, 535 | "thr" + "ee": 6 / 2, 536 | 4: 4, 537 | true: 5, 538 | false: 6 539 | }` 540 | 541 | evaluated := testEval(input) 542 | result, ok := evaluated.(*object.Hash) 543 | if !ok { 544 | t.Fatalf("Eval didn't return Hash. got=%T (%+v)", evaluated, evaluated) 545 | } 546 | 547 | expected := map[object.HashKey]int64{ 548 | (&object.String{Value: "one"}).HashKey(): 1, 549 | (&object.String{Value: "two"}).HashKey(): 2, 550 | (&object.String{Value: "three"}).HashKey(): 3, 551 | (&object.Integer{Value: 4}).HashKey(): 4, 552 | TRUE.HashKey(): 5, 553 | FALSE.HashKey(): 6, 554 | } 555 | 556 | if len(result.Pairs) != len(expected) { 557 | t.Fatalf("Hash has wrong num of pairs. got=%d", len(result.Pairs)) 558 | } 559 | 560 | for expectedKey, expectedValue := range expected { 561 | pair, ok := result.Pairs[expectedKey] 562 | if !ok { 563 | t.Errorf("no pair for given key in Pairs") 564 | } 565 | 566 | testIntegerObject(t, pair.Value, expectedValue) 567 | } 568 | } 569 | ``` 570 | 这个测试函数显示了当 Eval 遇到 *ast.HashLiteral 时我们想要什么:一个新的 *object.Hash,其正确数量的 HashPairs 映射到其 Pairs 属性中匹配的 HashKeys。 571 | 572 | 它还显示了我们的另一个要求:字符串、标识符、中缀运算符表达式、布尔值和整数——它们都应该可以用作键。 真的任何表情。 只要它产生一个实现 Hashable 接口的对象,它就应该可以用作哈希键。 然后是价值观。 它们也可以由任何表达式产生。 我们在这里通过断言 10 - 9 的计算结果为 1、6 / 2 到 3 等等来对此进行测试。 573 | 574 | 测试如期失败: 575 | ```go 576 | $ go test ./evaluator 577 | --- FAIL: TestHashLiterals (0.00s) 578 | evaluator_test.go:522: Eval didn't return Hash. got= () 579 | FAIL 580 | FAIL monkey/evaluator 0.008s 581 | ``` 582 | 不过,我们知道如何让它通过。 我们需要为 *ast.HashLiterals 使用另一个 case 分支扩展我们的 Eval 函数。 583 | ```go 584 | // evaluator/evaluator.go 585 | 586 | func Eval(node ast.Node, env *object.Environment) object.Object { 587 | // [...] 588 | case *ast.HashLiteral: 589 | return evalHashLiteral(node, env) 590 | // [...] 591 | } 592 | ``` 593 | 这里的 evalHashLiteral 函数可能看起来很吓人,但相信我,它不会咬人: 594 | ```go 595 | // evaluator/evaluator.go 596 | func evalHashLiteral( 597 | node *ast.HashLiteral, 598 | env *object.Environment, 599 | ) object.Object { 600 | pairs := make(map[object.HashKey]object.HashPair) 601 | for keyNode, valueNode := range node.Pairs { 602 | key := Eval(keyNode, env) 603 | if isError(key) { 604 | return key 605 | } 606 | 607 | hashKey, ok := key.(object.Hashable) 608 | if !ok { 609 | return newError("unusable as hash key: %s", key.Type()) 610 | } 611 | 612 | value := Eval(valueNode, env) 613 | if isError(value) { 614 | return value 615 | } 616 | 617 | hashed := hashKey.HashKey() 618 | pairs[hashed] = object.HashPair{Key: key, Value: value} 619 | } 620 | 621 | return &object.Hash{Pairs: pairs} 622 | } 623 | ``` 624 | 当迭代 node.Pairs 时, keyNode 是第一个被评估的。 除了检查对 Eval 的调用是否产生错误之外,我们还对评估结果进行类型断言:它需要实现 object.Hashable 接口,否则它不能用作哈希键。 这正是我们添加 Hashable 定义的原因。 625 | 626 | 然后我们再次调用 Eval 来评估 valueNode。 如果对 Eval 的调用也没有产生错误,我们可以将新产生的键值对添加到我们的对映射中。 我们通过调用 HashKey() 为恰当命名的 hashKey 对象生成一个 HashKey 来做到这一点。 然后我们初始化一个新的 HashPair,指向键和值并将其添加到对中。 627 | 628 | 这就是全部。 测试现在正在通过: 629 | ```go 630 | $ go test ./evaluator 631 | ok monkey/evaluator 0.007s 632 | ``` 633 | 这意味着我们已经可以开始在 REPL 中使用哈希文字了: 634 | ```go 635 | $ go run main.go 636 | Hello mrnugget! This is the Monkey programming language! 637 | Feel free to type in commands 638 | >> {"name": "Monkey", "age": 0, "type": "Language", "status": "awesome"} 639 | {age: 0, type: Language, status: awesome, name: Monkey} 640 | ``` 641 | 棒极了! 但是我们还不能从哈希中取出元素,这有点降低了它们的用处: 642 | ```js 643 | >> let bob = {"name": "Bob", "age": 99}; 644 | >> bob["name"] 645 | ERROR: index operator not supported: HASH 646 | ``` 647 | 这就是我们现在要解决的问题。 648 | ## 使用哈希评估索引表达式 649 | 还记得我们在评估器中添加到 evalIndexExpression 的 switch 语句吗? 你还记得我告诉你我们要添加另一个案例分支吗? 好吧,我们来了! 650 | 651 | 但首先我们需要添加一个测试函数,以确保通过索引表达式访问哈希中的值有效: 652 | ```go 653 | // evaluator/evaluator_test.go 654 | 655 | func TestHashIndexExpressions(t *testing.T) { 656 | tests := []struct { 657 | input string 658 | expected interface{} 659 | }{ 660 | { 661 | `{"foo": 5}["foo"]`, 662 | 5, 663 | }, 664 | { 665 | `{"foo": 5}["bar"]`, 666 | nil, 667 | }, 668 | { 669 | `let key = "foo"; {"foo": 5}[key]`, 670 | 5, 671 | }, 672 | { 673 | `{}["foo"]`, 674 | nil, 675 | }, 676 | { 677 | `{5: 5}[5]`, 678 | 5, 679 | }, 680 | { 681 | `{true: 5}[true]`, 682 | 5, 683 | }, 684 | { 685 | `{false: 5}[false]`, 686 | 5, 687 | }, 688 | } 689 | 690 | for _, tt := range tests { 691 | evaluated := testEval(tt.input) 692 | integer, ok := tt.expected.(int) 693 | if ok { 694 | testIntegerObject(t, evaluated, int64(integer)) 695 | } else { 696 | testNullObject(t, evaluated) 697 | } 698 | } 699 | } 700 | ``` 701 | 就像在 TestArrayIndexExpressions 中一样,我们确保使用索引运算符表达式产生正确的值 - 只是这次使用哈希。 当从哈希中检索值时,这里的不同测试用例使用字符串、整数或布尔哈希键。 所以,本质上,测试真正断言的是各种数据类型实现的HashKey方法被正确调用。 702 | 703 | 并且为了确保使用对象作为未实现 object.Hashable 的哈希键会产生错误,我们可以向我们的 TestErrorHandling 测试函数添加另一个测试:\ 704 | ```go 705 | // evaluator/evaluator_test.go 706 | 707 | func TestErrorHandling(t *testing.T) { 708 | tests := []struct { 709 | input string 710 | expectedMessage string 711 | }{ 712 | // [...] 713 | { 714 | `{"name": "Monkey"}[fn(x) { x }];`, 715 | "unusable as hash key: FUNCTION", 716 | }, 717 | } 718 | // [...] 719 | } 720 | ``` 721 | 运行`go test`现在结果如期失败: 722 | ```go 723 | $ go test ./evaluator 724 | --- FAIL: TestErrorHandling (0.00s) 725 | evaluator_test.go:228: no error object returned. got=*object.Null(&{}) 726 | --- FAIL: TestHashIndexExpressions (0.00s) 727 | evaluator_test.go:611: object is not Integer. got=*object.Null (&{}) 728 | evaluator_test.go:611: object is not Integer. got=*object.Null (&{}) 729 | evaluator_test.go:611: object is not Integer. got=*object.Null (&{}) 730 | evaluator_test.go:611: object is not Integer. got=*object.Null (&{}) 731 | evaluator_test.go:611: object is not Integer. got=*object.Null (&{}) 732 | FAIL 733 | FAIL monkey/evaluator 0.007s 734 | ``` 735 | 这意味着我们已经准备好在 evalIndexEx pression 的 switch 语句中添加另一个 case 分支: 736 | ```go 737 | // evaluator/evaluator.go 738 | 739 | func evalIndexExpression(left, index object.Object) object.Object { 740 | switch { 741 | case left.Type() == object.ARRAY_OBJ && index.Type() == object.INTEGER_OBJ: 742 | return evalArrayIndexExpression(left, index) 743 | case left.Type() == object.HASH_OBJ: 744 | return evalHashIndexExpression(left, index) 745 | default: 746 | return newError("index operator not supported: %s", left.Type()) 747 | } 748 | } 749 | ``` 750 | 新的 case 分支调用了一个新函数:evalHashIndexExpression。 我们已经知道 evalHashIndexExpression 必须如何工作,因为我们之前成功测试了 object.Hashable 接口的使用 - 在我们的测试中和评估哈希文字时。 所以这里没有惊喜: 751 | ```go 752 | // evaluator/evaluator.go 753 | 754 | func evalHashIndexExpression(hash, index object.Object) object.Object { 755 | hashObject := hash.(*object.Hash) 756 | 757 | key, ok := index.(object.Hashable) 758 | if !ok { 759 | return newError("unusable as hash key: %s", index.Type()) 760 | } 761 | 762 | pair, ok := hashObject.Pairs[key.HashKey()] 763 | if !ok { 764 | return NULL 765 | } 766 | 767 | return pair.Value 768 | } 769 | ``` 770 | 将 evalHashIndexExpression 添加到 switch 语句使测试通过: 771 | ```go 772 | $ go test ./evaluator 773 | ok monkey/evaluator 0.007s 774 | ``` 775 | 我们现在可以成功地从我们的哈希中检索值! 不相信我? 认为测试在骗我们? 我伪造了测试输出? 不能吗? 整本书都是li..什么? 不,看这个 776 | ```go 777 | $ go run main.go 778 | Hello mrnugget! This is the Monkey programming language! 779 | Feel free to type in commands 780 | >> let people = [{"name": "Alice", "age": 24}, {"name": "Anna", "age": 28}]; 781 | >> people[0]["name"]; 782 | Alice 783 | >> people[1]["age"]; 784 | 28 785 | >> people[1]["age"] + people[0]["age"]; 786 | 52 787 | >> let getName = fn(person) { person["name"]; }; 788 | >> getName(people[0]); 789 | Alice 790 | >> getName(people[1]); 791 | Anna 792 | ``` 793 | |[< 4.4数组](4.4.md)|[> 4.6总决赛](4.6.md)| 794 | |-|-| -------------------------------------------------------------------------------- /contents/4/4.4.md: -------------------------------------------------------------------------------- 1 | # 4.4数组 2 | 我们将在本节中添加到 Monkey 解释器的数据类型是数组。 在 Monkey 中,数组是可能不同类型元素的有序列表。 数组中的每个元素都可以单独访问。 数组是通过使用它们的字面形式构造的:逗号分隔的元素列表,用括号括起来。 3 | 4 | 初始化一个新数组,绑定给它一个名字并且通过独立的元素将会是这样: 5 | ```js 6 | >> let myArray = ["Thorsten", "Ball", 28, fn(x) { x * x }]; 7 | >> myArray[0] 8 | Thorsten 9 | >> myArray[2] 10 | 28 11 | >> myArray[3](2); 12 | 4 13 | ``` 14 | 正如你所见,Monkey数组的确不关心它们元素的类型。Monkey 中每个可能的值都可以是数组中的一个元素。 在这个例子中,myArray 包含两个字符串,一个整数和一个函数。 15 | 16 | 如最后三行所示,通过数组中的索引访问单个元素是通过一个新的运算符完成的,称为索引运算符:`array[index]`。 17 | 18 | 在本节中,我们还将为新添加的 len 函数添加对数组的支持,并添加更多用于数组的内置函数: 19 | ```js 20 | >> let myArray = ["one", "two", "three"]; 21 | >> len(myArray) 22 | 3 23 | >> first(myArray) 24 | one 25 | >> rest(myArray) 26 | [two, three] 27 | >> last(myArray) 28 | three 29 | >> push(myArray, "four") 30 | [one, two, three, four] 31 | ``` 32 | 我们在解释器中实现 Monkey 数组的基础是一个 []object.Object 类型的 Go 切片。 这意味着我们不必实现新的数据结构。 我们可以重用 Go 的切片。 33 | 34 | 听起来真棒?好的!我们要做的第一件事是教我们的词法分析器一些新的标记。 35 | ## 在我们的词法分析器中支持数组 36 | 为了正确解析数组文字和索引运算符,我们的词法分析器需要能够识别比当前更多的标记。 在 Monkey 中构造和使用数组所需的所有标记是 [,] 和 ,。 词法分析器已经知道了,所以我们只需要添加对 [ and ] 的支持。 37 | 38 | 第一步是在token包中定义这些新的token类型: 39 | ```go 40 | // token/token.go 41 | 42 | const ( 43 | // [...] 44 | LBRACKET = "[" 45 | RBRACKET = "]" 46 | // [...] 47 | ) 48 | ``` 49 | 第二步是扩展词法分析器的测试套件,这很容易,因为我们之前已经做过很多次了: 50 | ```go 51 | // lexer/lexer_test.go、 52 | 53 | func TestNextToken(t *testing.T) { 54 | input := `let five = 5; 55 | let ten = 10; 56 | 57 | let add = fn(x, y) { 58 | x + y; 59 | }; 60 | 61 | let result = add(five, ten); 62 | !-/*5; 63 | 5 < 10 > 5; 64 | 65 | if (5 < 10) { 66 | return true; 67 | } else { 68 | return false; 69 | } 70 | 71 | 10 == 10; 72 | 10 != 9; 73 | "foobar" 74 | "foo bar" 75 | [1, 2]; 76 | ` 77 | tests := []struct { 78 | expectedType token.TokenType 79 | expectedLiteral string 80 | }{ 81 | // [...] 82 | {token.LBRACKET, "["}, 83 | {token.INT, "1"}, 84 | {token.COMMA, ","}, 85 | {token.INT, "2"}, 86 | {token.RBRACKET, "]"}, 87 | {token.SEMICOLON, ";"}, 88 | {token.EOF, ""}, 89 | } 90 | // [...] 91 | } 92 | ``` 93 | 输入再次扩展为包括新标记(在本例中为 [1, 2]),并添加了新测试以确保词法分析器的 NextToken 方法确实返回 token.LBRACKET 和 ken.RBRACKET。 94 | 95 | 让测试通过就像将这四行添加到我们的 NextToken() 方法一样简单。 是的,只有四个: 96 | ```go 97 | // lexer/lexer.go 98 | 99 | func (l *Lexer) NextToken() token.Token { 100 | // [...] 101 | case '[': 102 | tok = newToken(token.LBRACKET, l.ch) 103 | case ']': 104 | tok = newToken(token.RBRACKET, l.ch) 105 | // [...] 106 | } 107 | ``` 108 | 好吧!测试通过了: 109 | ```go 110 | $ go test ./lexer 111 | ok monkey/lexer 0.006s 112 | ``` 113 | 在我们的解析器中,我们现在将使用 token.LBRACKET 和 token.RBRACKET 来解析数组。 114 | ## 解析数组文字 115 | 正如我们之前看到的,Monkey 中的数组文字是一个逗号分隔的表达式列表,由一个左括号和一个右括号括起来。 116 | ```go 117 | [1, 2, 3 + 3, fn(x) { x }, add(2, 2)] 118 | ``` 119 | 是的,数组文字中的元素可以是任何类型的表达式。 整数文字、函数文字、中缀或前缀表达式。 120 | 121 | 如果这听起来很复杂,请不要担心。 我们已经知道如何解析逗号分隔的表达式列表——函数调用参数就是这样。 而且我们还知道如何解析由匹配标记包围的内容。 换句话说:让我们开始吧! 122 | 123 | 我们要做的第一件事是为数组文字定义 AST 节点。 由于我们已经为此准备了必要的部分,因此定义是不言自明的: 124 | ```go 125 | // ast/ast.go 126 | 127 | type ArrayLiteral struct { 128 | Token token.Token // the '[' token 129 | Elements []Expression 130 | } 131 | func (al *ArrayLiteral) expressionNode() {} 132 | func (al *ArrayLiteral) TokenLiteral() string { return al.Token.Literal } 133 | func (al *ArrayLiteral) String() string { 134 | var out bytes.Buffer 135 | 136 | elements := []string{} 137 | for _, el := range al.Elements { 138 | elements = append(elements, el.String()) 139 | } 140 | 141 | out.WriteString("[") 142 | out.WriteString(strings.Join(elements, ", ")) 143 | out.WriteString("]") 144 | 145 | return out.String() 146 | } 147 | ``` 148 | 以下测试函数确保解析数组文字会导致返回 *ast.ArrayLiteral。 (我还为空数组文字添加了一个测试函数,以确保我们不会遇到令人讨厌的边缘情况) 149 | ```go 150 | // parser/parser_test.go 151 | 152 | func TestParsingArrayLiterals(t *testing.T) { 153 | input := "[1, 2 * 2, 3 + 3]" 154 | 155 | l := lexer.New(input) 156 | p := New(l) 157 | program := p.ParseProgram() 158 | checkParserErrors(t, p) 159 | 160 | stmt, ok := program.Statements[0].(*ast.ExpressionStatement) 161 | array, ok := stmt.Expression.(*ast.ArrayLiteral) 162 | if !ok { 163 | t.Fatalf("exp not ast.ArrayLiteral. got=%T", stmt.Expression) 164 | } 165 | 166 | if len(array.Elements) != 3 { 167 | t.Fatalf("len(array.Elements) not 3. got=%d", len(array.Elements)) 168 | } 169 | 170 | testIntegerLiteral(t, array.Elements[0], 1) 171 | testInfixExpression(t, array.Elements[1], 2, "*", 2) 172 | testInfixExpression(t, array.Elements[2], 3, "+", 3) 173 | } 174 | ``` 175 | 只是为了确保表达式的解析确实有效,测试输入包含两个不同的中缀运算符表达式,即使整数或布尔文字就足够了。除此之外,该测试非常无聊,并断言解析器确实返回了具有正确元素数量的 *ast.ArrayLiteral。 176 | 177 | 为了让测试通过,我们需要在我们的解析器中注册一个新的 prefixParseFn,因为数组文字的开头 token.LBRACKET 位于前缀位置。 178 | ```go 179 | // parser/parser.go 180 | 181 | func New(l *lexer.Lexer) *Parser { 182 | // [...] 183 | p.registerPrefix(token.LBRACKET, p.parseArrayLiteral) 184 | 185 | // [...] 186 | } 187 | 188 | func (p *Parser) parseArrayLiteral() ast.Expression { 189 | array := &ast.ArrayLiteral{Token: p.curToken} 190 | 191 | array.Elements = p.parseExpressionList(token.RBRACKET) 192 | 193 | return array 194 | } 195 | ``` 196 | 我们之前添加了 prefixParseFns,所以这部分并不是很令人兴奋。 这里有趣的是名为 parseExpressionList 的新方法。 此方法是 parseCallArguments 的修改和通用版本,我们之前在parseCallExpression 中使用它来解析逗号分隔的参数列表: 197 | ```go 198 | // parser/parser.go 199 | func (p *Parser) parseExpressionList(end token.TokenType) []ast.Expression { 200 | list := []ast.Expression{} 201 | 202 | if p.peekTokenIs(end) { 203 | p.nextToken() 204 | return list 205 | } 206 | 207 | p.nextToken() 208 | list = append(list, p.parseExpression(LOWEST)) 209 | 210 | for p.peekTokenIs(token.COMMA) { 211 | p.nextToken() 212 | p.nextToken() 213 | list = append(list, p.parseExpression(LOWEST)) 214 | } 215 | 216 | if !p.expectPeek(end) { 217 | return nil 218 | } 219 | 220 | return list 221 | } 222 | ``` 223 | 同样,我们之前已经在名称 parseCallArguments 下看到了这一点。 唯一的变化是这个新版本现在接受一个结束参数,它告诉方法哪个标记表示列表的结尾。 更新后的 parseCallExpression 方法,我们之前在其中使用了 parseCallArguments,现在看起来像这样: 224 | ```go 225 | // parser/parser.go 226 | 227 | func (p *Parser) parseCallExpression(function ast.Expression) ast.Expression { 228 | exp := &ast.CallExpression{Token: p.curToken, Function: function} 229 | exp.Arguments = p.parseExpressionList(token.RPAREN) 230 | return exp 231 | } 232 | ``` 233 | 唯一的变化是使用 token.RPAREN 调用 parseExpressionList(表示参数列表的结尾)。 我们可以通过更改几行来重用一个相对较大的方法。 很好! 最好的? 测试通过: 234 | ```go 235 | $ go test ./parser 236 | ok monkey/parser 0.007s 237 | ``` 238 | 我们可以将“解析数组文字”标记为“完成”。 239 | ## 解析索引运算符表达式 240 | 为了在 Monkey 中完全支持数组,我们不仅需要能够解析数组文字,还需要能够解析索引运算符表达式。 也许“索引操作符”这个名字并不响亮,但我打赌你知道它是什么。 索引运算符表达式如下所示: 241 | ```js 242 | myArray[0]; 243 | myArray[1]; 244 | myArray[2]; 245 | ``` 246 | 至少这是基本形式,但还有很多。 查看这些示例以发现它们背后的结构: 247 | ```js 248 | [1, 2, 3, 4][2]; 249 | 250 | let myArray = [1, 2, 3, 4]; 251 | myArray[2]; 252 | 253 | myArray[2 + 1]; 254 | 255 | returnsArray()[1]; 256 | ``` 257 | 是的,你完全正确! 基本结构是这样的: 258 | ```js 259 | [] 260 | ``` 261 | 这似乎很简单。 我们可以定义一个新的 AST 节点,称为 ast.IndexExpression,它反映了这个结构: 262 | ```go 263 | // ast/ast.go 264 | 265 | type IndexExpression struct { 266 | Token token.Token // The [ token 267 | Left Expression 268 | Index Expression 269 | } 270 | 271 | func (ie *IndexExpression) expressionNode() {} 272 | func (ie *IndexExpression) TokenLiteral() string { return ie.Token.Literal } 273 | func (ie *IndexExpression) String() string { 274 | var out bytes.Buffer 275 | 276 | out.WriteString("(") 277 | out.WriteString(ie.Left.String()) 278 | out.WriteString("[") 279 | out.WriteString(ie.Index.String()) 280 | out.WriteString("])") 281 | 282 | return out.String() 283 | } 284 | ``` 285 | 需要注意的是,Left 和 Index 都只是表达式。 左边是被访问的对象,我们已经看到它可以是任何类型:标识符、数组文字、函数调用。 索引也是如此。 它可以是任何表达式。 从语法上讲,它是哪个没有区别,但从语义上讲,它必须产生一个整数。 286 | 287 | Left 和 Index 都是表达式这一事实使解析过程更容易,因为我们可以使用 parseExpression 方法来解析它们。 但首先要注意! 这是确保我们的解析器知道如何返回 *ast.IndexExpression 的测试用例: 288 | ```go 289 | // parser/parser_test.go 290 | func TestParsingIndexExpressions(t *testing.T) { 291 | input := "myArray[1 + 1]" 292 | 293 | l := lexer.New(input) 294 | p := New(l) 295 | program := p.ParseProgram() 296 | checkParserErrors(t, p) 297 | 298 | stmt, ok := program.Statements[0].(*ast.ExpressionStatement) 299 | indexExp, ok := stmt.Expression.(*ast.IndexExpression) 300 | if !ok { 301 | t.Fatalf("exp not *ast.IndexExpression. got=%T", stmt.Expression) 302 | } 303 | 304 | if !testIdentifier(t, indexExp.Left, "myArray") { 305 | return 306 | } 307 | 308 | if !testInfixExpression(t, indexExp.Index, 1, "+", 1) { 309 | return 310 | } 311 | } 312 | ``` 313 | 现在,此测试仅断言解析器为包含索引表达式的单个表达式语句输出正确的 AST。 但同样重要的是解析器正确处理索引运算符的优先级。 索引运算符必须在所有运算符中具有最高优先级。 确保这一点就像扩展我们现有的 Test OperatorPrecedenceParsing 测试函数一样简单: 314 | ```go 315 | // parser/parser_test.go 316 | func TestOperatorPrecedenceParsing(t *testing.T) { 317 | tests := []struct { 318 | input string 319 | expected string 320 | }{ 321 | // [...] 322 | { 323 | "a * [1, 2, 3, 4][b * c] * d", 324 | "((a * ([1, 2, 3, 4][(b * c)])) * d)", 325 | }, 326 | { 327 | "add(a * b[2], b[1], 2 * [1, 2][1])", 328 | "add((a * (b[2])), (b[1]), (2 * ([1, 2][1])))", 329 | }, 330 | } 331 | // [...] 332 | } 333 | ``` 334 | *ast.IndexExpression 的 String() 输出中的附加 ( 和 ) 在编写这些测试时帮助我们,因为它们使索引运算符的优先级可见。 在这些添加的测试用例中,我们期望索引运算符的优先级高于调用表达式甚至中缀表达式中的 * 运算符的优先级。 335 | 336 | 测试失败是因为解析器对索引表达式一无所知: 337 | ```go 338 | $ go test ./parser 339 | --- FAIL: TestOperatorPrecedenceParsing (0.00s) 340 | parser_test.go:393: expected="((a * ([1, 2, 3, 4][(b * c)])) * d)",\ 341 | got="(a * [1, 2, 3, 4])([(b * c)] * d)" 342 | parser_test.go:968: parser has 4 errors 343 | parser_test.go:970: parser error: "expected next token to be ), got [ instead" 344 | parser_test.go:970: parser error: "no prefix parse function for , found" 345 | parser_test.go:970: parser error: "no prefix parse function for , found" 346 | parser_test.go:970: parser error: "no prefix parse function for ) found" 347 | --- FAIL: TestParsingIndexExpressions (0.00s) 348 | parser_test.go:835: exp not *ast.IndexExpression. got=*ast.Identifier 349 | FAIL 350 | FAIL monkey/parser 0.007s 351 | ``` 352 | 即使测试抱怨缺少 prefixParseFn,我们想要的是 infixParseFn。 是的,索引运算符表达式在每一侧的操作数之间并没有真正的单个运算符。 但是为了不费吹灰之力地解析它们,像它们一样行事是有好处的,就像我们对调用表达式所做的那样。 具体来说,这意味着将 `myArray[0]` 中的 [ 视为中缀运算符,将 myArray 视为左操作数,将 0 视为右操作数。 353 | 354 | 这样做使实现非常适合我们的解析器: 355 | ```go 356 | // parser/parser.go 357 | 358 | func New(l *lexer.Lexer) *Parser { 359 | // [...] 360 | 361 | p.registerInfix(token.LBRACKET, p.parseIndexExpression) 362 | 363 | // [...] 364 | } 365 | 366 | func (p *Parser) parseIndexExpression(left ast.Expression) ast.Expression { 367 | exp := &ast.IndexExpression{Token: p.curToken, Left: left} 368 | 369 | p.nextToken() 370 | 371 | exp.Index = p.parseExpression(LOWEST) 372 | 373 | if !p.expectPeek(token.RBRACKET) { 374 | return nil 375 | } 376 | return exp 377 | } 378 | ``` 379 | 很整洁! 但这并不能解决我们的测试: 380 | ```go 381 | $ go test ./parser 382 | --- FAIL: TestOperatorPrecedenceParsing (0.00s) 383 | parser_test.go:393: expected="((a * ([1, 2, 3, 4][(b * c)])) * d)",\ 384 | got="(a * [1, 2, 3, 4])([(b * c)] * d)" 385 | parser_test.go:968: parser has 4 errors 386 | parser_test.go:970: parser error: "expected next token to be ), got [ instead" 387 | parser_test.go:970: parser error: "no prefix parse function for , found" 388 | parser_test.go:970: parser error: "no prefix parse function for , found" 389 | parser_test.go:970: parser error: "no prefix parse function for ) found" 390 | --- FAIL: TestParsingIndexExpressions (0.00s) 391 | parser_test.go:835: exp not *ast.IndexExpression. got=*ast.Identifier 392 | FAIL 393 | FAIL monkey/parser 0.008s 394 | ``` 395 | 那是因为我们的 Pratt 解析器背后的整个想法取决于优先级的想法,而我们还没有定义索引运算符的优先级: 396 | ```go 397 | // parser/parser.go 398 | 399 | const ( 400 | _ int = iota 401 | // [...] 402 | INDEX // array[index] 403 | ) 404 | 405 | var precedences = map[token.TokenType]int{ 406 | // [...] 407 | token.LBRACKET: INDEX, 408 | } 409 | ``` 410 | INDEX 的定义是 const 块中的最后一行,这一点很重要。 由于 iota,这使 INDEX 成为所有定义的优先级常量的最高值。 在 precedences 中添加的条目为 token.LBRACKET 提供了最高的优先级,INDEX。 而且,它确实有奇效: 411 | ```go 412 | $ go test ./parser 413 | ok monkey/parser 0.007s 414 | ``` 415 | 词法分析器完成,解析器完成。 评估区见! 416 | ## 评估数组文字 417 | 评估数组文字并不难。 将 Monkey 数组映射到 Go 的切片让生活变得美好、甜蜜。 我们不必实现新的数据结构。 我们只需要定义一个新的 object.Array 类型,因为这就是数组字面量的计算结果。 而 418 | object.Array 的定义很简单,因为 Monkey 中的数组很简单:它们只是一个对象列表。 419 | ```go 420 | // object/object.go 421 | 422 | const ( 423 | // [...] 424 | ARRAY_OBJ = "ARRAY" 425 | ) 426 | 427 | type Array struct { 428 | Elements []Object 429 | } 430 | 431 | func (ao *Array) Type() ObjectType { return ARRAY_OBJ } 432 | func (ao *Array) Inspect() string { 433 | var out bytes.Buffer 434 | 435 | elements := []string{} 436 | for _, e := range ao.Elements { 437 | elements = append(elements, e.Inspect()) 438 | } 439 | 440 | out.WriteString("[") 441 | out.WriteString(strings.Join(elements, ", ")) 442 | out.WriteString("]") 443 | 444 | return out.String() 445 | } 446 | ``` 447 | 当我说这个定义最复杂的地方是 Inspect 方法时,我想你会同意我的观点。 即使是那个也很容易理解。 448 | 449 | 这是数组文字的评估器测试: 450 | ```go 451 | // evaluator/evaluator_test.go 452 | 453 | func TestArrayLiterals(t *testing.T) { 454 | input := "[1, 2 * 2, 3 + 3]" 455 | 456 | evaluated := testEval(input) 457 | result, ok := evaluated.(*object.Array) 458 | if !ok { 459 | t.Fatalf("object is not Array. got=%T (%+v)", evaluated, evaluated) 460 | } 461 | 462 | if len(result.Elements) != 3 { 463 | t.Fatalf("array has wrong num of elements. got=%d", 464 | len(result.Elements)) 465 | } 466 | 467 | testIntegerObject(t, result.Elements[0], 1) 468 | testIntegerObject(t, result.Elements[1], 4) 469 | testIntegerObject(t, result.Elements[2], 6) 470 | } 471 | ``` 472 | 我们可以重用一些现有的代码来让这个测试通过,就像我们在解析器中所做的那样。 同样,我们重用的代码最初是为调用表达式编写的。 这是评估 *ast.ArrayLiterals 并生成数组对象的 case 分支: 473 | ```go 474 | // evaluator/evaluator.go 475 | 476 | func Eval(node ast.Node, env *object.Environment) object.Object { 477 | // [...] 478 | 479 | case *ast.ArrayLiteral: 480 | elements := evalExpressions(node.Elements, env) 481 | if len(elements) == 1 && isError(elements[0]) { 482 | return elements[0] 483 | } 484 | return &object.Array{Elements: elements} 485 | } 486 | 487 | // [...] 488 | } 489 | ``` 490 | 这难道不是编程的一大乐趣吗? 重用现有代码,而不必将其变成超级通用、过度设计的宇宙飞船。 测试通过,我们可以在 REPL 中使用数组文字来生成数组: 491 | ```js 492 | $ go run main.go 493 | Hello mrnugget! This is the Monkey programming language! 494 | Feel free to type in commands 495 | >> [1, 2, 3, 4] 496 | [1, 2, 3, 4] 497 | >> let double = fn(x) { x * 2 }; 498 | >> [1, double(2), 3 * 3, 4 - 3] 499 | [1, 4, 9, 1] 500 | >> 501 | ``` 502 | 很神奇,不是吗? 但是我们还不能做的是使用索引运算符访问数组的单个元素。 503 | ## 评估索引运算符表达式 504 | 好消息:比评估索引表达式更难的是解析它们。 我们已经这样做了。 剩下的唯一问题是访问和检索数组中的元素时可能会出现一对一错误。 但为此,我们只需在我们的测试套件中添加一些测试: 505 | ```go 506 | // evaluator/evaluator_test.go 507 | 508 | func TestArrayIndexExpressions(t *testing.T) { 509 | tests := []struct { 510 | input string 511 | expected interface{} 512 | }{ 513 | { 514 | "[1, 2, 3][0]", 515 | 1, 516 | }, 517 | { 518 | "[1, 2, 3][1]", 519 | 2, 520 | }, 521 | { 522 | "[1, 2, 3][2]", 523 | 3, 524 | }, 525 | { 526 | "let i = 0; [1][i];", 527 | 1, 528 | }, 529 | { 530 | "[1, 2, 3][1 + 1];", 531 | 3, 532 | }, 533 | { 534 | "let myArray = [1, 2, 3]; myArray[2];", 535 | 3, 536 | }, 537 | { 538 | "let myArray = [1, 2, 3]; myArray[0] + myArray[1] + myArray[2];", 539 | 6, 540 | }, 541 | { 542 | "let myArray = [1, 2, 3]; let i = myArray[0]; myArray[i]", 543 | 2, 544 | }, 545 | { 546 | "[1, 2, 3][3]", 547 | nil, 548 | }, 549 | { 550 | "[1, 2, 3][-1]", 551 | nil, 552 | }, 553 | } 554 | 555 | for _, tt := range tests { 556 | evaluated := testEval(tt.input) 557 | integer, ok := tt.expected.(int) 558 | if ok { 559 | testIntegerObject(t, evaluated, int64(integer)) 560 | } else { 561 | testNullObject(t, evaluated) 562 | } 563 | } 564 | } 565 | ``` 566 | 好吧,我承认,这些测试可能看起来有些过分。 我们在这里隐式测试的很多东西已经在其他地方测试过了。 但是测试用例是那么容易写! 他们是如此易读! 我喜欢这些测试。 567 | 568 | 记下这些测试指定的所需行为。 它们包含一些我们尚未讨论的内容:当我们使用超出数组边界的索引时,我们将返回 NULL。 在这种情况下,某些语言会产生错误,而某些语言会返回空值。 我选择返回NULL。 569 | 570 | 正如预期的那样,测试失败了。 不仅如此,它们还在爆炸: 571 | ```go 572 | $ go test ./evaluator 573 | --- FAIL: TestArrayIndexExpressions (0.00s) 574 | evaluator_test.go:492: object is not Integer. got= () 575 | evaluator_test.go:492: object is not Integer. got= () 576 | evaluator_test.go:492: object is not Integer. got= () 577 | evaluator_test.go:492: object is not Integer. got= () 578 | evaluator_test.go:492: object is not Integer. got= () 579 | evaluator_test.go:492: object is not Integer. got= () 580 | panic: runtime error: invalid memory address or nil pointer dereference 581 | [signal SIGSEGV: segmentation violation code=0x1 addr=0x28 pc=0x70057] 582 | [redacted: backtrace here] 583 | FAIL monkey/evaluator 0.011s 584 | ``` 585 | 那么我们如何解决这个问题并评估索引表达式呢? 正如我们所见,索引运算符的左操作数可以是任何表达式,而索引本身可以是任何表达式。 这意味着我们需要先评估两者,然后才能评估“索引”本身。 否则,我们会尝试访问标识符或函数调用的元素,但这是行不通的。 586 | 587 | 这是 *ast.IndexExpression 的 case 分支,它对 Eval 进行了这些所需的调用: 588 | ```go 589 | // evaluator/evaluator.go 590 | 591 | func Eval(node ast.Node, env *object.Environment) object.Object { 592 | // [...] 593 | case *ast.IndexExpression: 594 | left := Eval(node.Left, env) 595 | if isError(left) { 596 | return left 597 | } 598 | index := Eval(node.Index, env) 599 | if isError(index) { 600 | return index 601 | } 602 | return evalIndexExpression(left, index) 603 | 604 | // [...] 605 | } 606 | ``` 607 | 这是它使用的 evalIndexExpression 函数: 608 | ```go 609 | // evaluator/evaluator.go 610 | 611 | func evalIndexExpression(left, index object.Object) object.Object { 612 | switch { 613 | case left.Type() == object.ARRAY_OBJ && index.Type() == object.INTEGER_OBJ: 614 | return evalArrayIndexExpression(left, index) 615 | default: 616 | return newError("index operator not supported: %s", left.Type()) 617 | } 618 | } 619 | ``` 620 | if 条件在这里可以很好地完成 switch 语句的工作,但我们将在本章稍后添加另一个 case 分支。 除了错误处理(为此我还添加了一个测试),这个函数没有任何有趣的事情发生。 操作的核心在evalArrayIndexExpression 中: 621 | ```go 622 | // evaluator/evaluator.go 623 | 624 | func evalArrayIndexExpression(array, index object.Object) object.Object { 625 | arrayObject := array.(*object.Array) 626 | idx := index.(*object.Integer).Value 627 | max := int64(len(arrayObject.Elements) - 1) 628 | 629 | if idx < 0 || idx > max { 630 | return NULL 631 | } 632 | 633 | return arrayObject.Elements[idx] 634 | } 635 | ``` 636 | 这里我们实际上从数组中检索具有指定索引的元素。 除了小类型断言和转换舞蹈之外,这个函数非常简单:它检查给定的索引是否超出范围,如果是这种情况,则返回 NULL,否则返回所需的元素。 就像我们在测试中指定的那样,现在正在通过: 637 | ```go 638 | $ go test ./evaluator 639 | ok monkey/evaluator 0.007s 640 | ``` 641 | 好的,现在深呼吸,放松一下,看看这个: 642 | ```go 643 | $ go run main.go 644 | Hello mrnugget! This is the Monkey programming language! 645 | Feel free to type in commands 646 | >> let a = [1, 2 * 2, 10 - 5, 8 / 2]; 647 | >> a[0] 648 | 1 649 | >> a[1] 650 | 4 651 | >> a[5 - 3] 652 | 5 653 | >> a[99] 654 | null 655 | ``` 656 | 从数组中检索元素有效! 甜的! 我只能在这里重复一遍:实现这个语言特性是多么容易,不是吗? 657 | ## 为数组添加内置函数 658 | 我们现在可以使用数组字面量来构造数组。 我们可以使用索引表达式访问单个元素。 仅这两件事就使数组非常有用。 但为了让它们更有用,我们需要添加一些内置函数,使使用它们更方便。 在本小节中,我们将完全做到这一点。 659 | 660 | 我不会在本节中展示任何测试代码和测试用例。 原因是这些特定的测试占用了空间而没有添加任何新内容。 我们用于测试内置函数的“框架”已经与 TestBuiltinFunctions 一起就位,并且添加的测试遵循现有方案。 您可以在随附的代码中找到它们。 661 | 662 | 我们的目标是添加新的内置函数。 但是我们实际上要做的第一件事不是添加一个新的,而是改变一个现有的功能。 我们需要为 len 添加对数组的支持,它目前只支持字符串: 663 | ```go 664 | // evaluator/builtins.go 665 | var builtins = map[string]*object.Builtin{ 666 | "len": &object.Builtin{ 667 | Fn: func(args ...object.Object) object.Object { 668 | if len(args) != 1 { 669 | return newError("wrong number of arguments. got=%d, want=1", 670 | len(args)) 671 | } 672 | 673 | switch arg := args[0].(type) { 674 | case *object.Array: 675 | return &object.Integer{Value: int64(len(arg.Elements))} 676 | case *object.String: 677 | return &object.Integer{Value: int64(len(arg.Value))} 678 | default: 679 | return newError("argument to `len` not supported, got %s", 680 | args[0].Type()) 681 | } 682 | }, 683 | }, 684 | // [...] 685 | } 686 | ``` 687 | 唯一的变化是为 *object.Array 添加了 case 分支。 有了这些,我们就可以开始添加新功能了。 耶! 688 | 689 | 这些新的内置函数中的第一个是第一个。 首先返回给定数组的第一个元素。 是的,调用 `myArray[0]` 做同样的事情。 但可以说第一个更漂亮。 这是它的实现: 690 | ```go 691 | // evaluator/builtins.go 692 | 693 | var builtins = map[string]*object.Builtin{ 694 | // [...] 695 | 696 | "first": &object.Builtin{ 697 | Fn: func(args ...object.Object) object.Object { 698 | if len(args) != 1 { 699 | return newError("wrong number of arguments. got=%d, want=1", 700 | len(args)) 701 | } 702 | if args[0].Type() != object.ARRAY_OBJ { 703 | return newError("argument to `first` must be ARRAY, got %s", 704 | args[0].Type()) 705 | } 706 | 707 | arr := args[0].(*object.Array) 708 | if len(arr.Elements) > 0 { 709 | return arr.Elements[0] 710 | } 711 | 712 | return NULL 713 | }, 714 | }, 715 | } 716 | ``` 717 | 伟大的! 这样可行! 首先是什么? 你是对的,我们要添加的下一个函数是 last 调用的。 718 | last 的目的是返回给定数组的最后一个元素。 在索引运算符方面,它返回 `myArray[len(myArray)-1]`。 事实证明,最后实施并不比先实施困难多少——谁会想到这一点? 这里是: 719 | ```go 720 | // evaluator/builtins.go 721 | 722 | var builtins = map[string]*object.Builtin{ 723 | // [...] 724 | 725 | "last": &object.Builtin{ 726 | Fn: func(args ...object.Object) object.Object { 727 | if len(args) != 1 { 728 | return newError("wrong number of arguments. got=%d, want=1", 729 | len(args)) 730 | } 731 | if args[0].Type() != object.ARRAY_OBJ { 732 | return newError("argument to `last` must be ARRAY, got %s", 733 | args[0].Type()) 734 | } 735 | 736 | arr := args[0].(*object.Array) 737 | length := len(arr.Elements) 738 | if length > 0 { 739 | return arr.Elements[length-1] 740 | } 741 | 742 | return NULL 743 | }, 744 | }, 745 | } 746 | ``` 747 | 我们要添加的下一个函数在 Scheme 中称为 cdr。 在其他一些语言中,它有时被称为尾部。 我们将称之为休息。 rest 返回一个新数组,其中包含作为参数传递的数组的所有元素,但第一个除外。 这是使用它的样子: 748 | ```js 749 | >> let a = [1, 2, 3, 4]; 750 | >> rest(a) 751 | [2, 3, 4] 752 | >> rest(rest(a)) 753 | [3, 4] 754 | >> rest(rest(rest(a))) 755 | [4] 756 | >> rest(rest(rest(rest(a)))) 757 | [] 758 | >> rest(rest(rest(rest(rest(a))))) 759 | null 760 | ``` 761 | 它的实现很简单,但请记住,我们正在返回一个新分配的数组。 我们不会修改传递给 rest 的数组: 762 | ```go 763 | // evaluator/builtins.go 764 | 765 | var builtins = map[string]*object.Builtin{ 766 | // [...] 767 | 768 | "rest": &object.Builtin{ 769 | Fn: func(args ...object.Object) object.Object { 770 | if len(args) != 1 { 771 | return newError("wrong number of arguments. got=%d, want=1", 772 | len(args)) 773 | } 774 | 775 | if args[0].Type() != object.ARRAY_OBJ { 776 | return newError("argument to `rest` must be ARRAY, got %s", 777 | args[0].Type()) 778 | } 779 | 780 | arr := args[0].(*object.Array) 781 | length := len(arr.Elements) 782 | if length > 0 { 783 | newElements := make([]object.Object, length-1, length-1) 784 | copy(newElements, arr.Elements[1:length]) 785 | return &object.Array{Elements: newElements} 786 | } 787 | 788 | return NULL 789 | }, 790 | }, 791 | } 792 | ``` 793 | 我们将要构建到解释器中的最后一个数组函数称为 push。 它将一个新元素添加到数组的末尾。 但是,关键是,它不会修改给定的数组。 相反,它分配一个新数组,该数组具有与旧数组相同的元素加上新的推送元素。 数组在 Monkey 中是不可变的。 这是推动行动: 794 | ```js 795 | >> let a = [1, 2, 3, 4]; 796 | >> let b = push(a, 5); 797 | >> a 798 | [1, 2, 3, 4] 799 | >> b 800 | [1, 2, 3, 4, 5] 801 | ``` 802 | 并且这里是它的实现: 803 | ```go 804 | // evaluator/builtins.go 805 | 806 | var builtins = map[string]*object.Builtin{ 807 | // [...] 808 | 809 | "push": &object.Builtin{ 810 | Fn: func(args ...object.Object) object.Object { 811 | if len(args) != 2 { 812 | return newError("wrong number of arguments. got=%d, want=2", 813 | len(args)) 814 | } 815 | 816 | if args[0].Type() != object.ARRAY_OBJ { 817 | return newError("argument to `push` must be ARRAY, got %s", 818 | args[0].Type()) 819 | } 820 | 821 | arr := args[0].(*object.Array) 822 | length := len(arr.Elements) 823 | 824 | newElements := make([]object.Object, length+1, length+1) 825 | copy(newElements, arr.Elements) 826 | newElements[length] = args[1] 827 | 828 | return &object.Array{Elements: newElements} 829 | }, 830 | }, 831 | } 832 | ``` 833 | ## 测试驱动数组 834 | 我们现在有了数组字面量、索引运算符和一些处理数组的内置函数。是时候带他们去兜风了。 让我们看看他们能做什么。 835 | 836 | 首先,rest 和 push 我们可以构建一个地图函数: 837 | ```go 838 | let map = fn(arr, f) { 839 | let iter = fn(arr, accumulated) { 840 | if (len(arr) == 0) { 841 | accumulated 842 | } else { 843 | iter(rest(arr), push(accumulated, f(first(arr)))); 844 | } 845 | }; 846 | 847 | iter(arr, []); 848 | }; 849 | ``` 850 | 使用 map 我们可以做这样的事情: 851 | ```js 852 | >> let a = [1, 2, 3, 4]; 853 | >> let double = fn(x) { x * 2 }; 854 | >> map(a, double); 855 | [2, 4, 6, 8] 856 | ``` 857 | 这不是很神奇吗? 还有更多! 基于相同的内置函数,我们还可以定义一个 reduce 函数: 858 | ```js 859 | let reduce = fn(arr, initial, f) { 860 | let iter = fn(arr, result) { 861 | if (len(arr) == 0) { 862 | result 863 | } else { 864 | iter(rest(arr), f(result, first(arr))); 865 | } 866 | }; 867 | 868 | iter(arr, initial); 869 | }; 870 | ``` 871 | reduce,reduce 可用于定义 sum 函数: 872 | ```js 873 | let sum = fn(arr) { 874 | reduce(arr, 0, fn(initial, el) { initial + el }); 875 | }; 876 | ``` 877 | 它的作用就像一个魅力: 878 | ```js 879 | >> sum([1, 2, 3, 4, 5]); 880 | 15 881 | ``` 882 | 你可能知道,我不喜欢拍拍自己的背,但我只想说:holy monkey! 看看我们的解释器能做什么! map功能?! reduce?! 我们已经走了很长很长的路! 883 | 884 | 而且这还不是全部! 我们现在可以做的还有很多,我敦促您探索数组数据类型和少数内置函数为我们提供的可能性。 但是你知道你应该先做什么吗? 休息一段时间,向你的朋友和家人吹嘘这件事,享受赞美和赞美。 当你回来时,我们将添加另一种数据类型。 885 | |[< 内置函数](4.3.md)|[> 4.5哈希](4.5.md)| 886 | |-|-| 887 | --------------------------------------------------------------------------------