鹦鹉虚拟机/Squaak 教程/窥探编译器内部
第 1 集: 介绍
第 2 集: 窥探编译器内部
第 3 集: Squaak 细节和第一步
第 4 集: PAST 节点和更多语句
第 5 集: 变量声明和作用域
第 6 集: 作用域和子程序
第 7 集: 运算符和优先级
第 8 集: 哈希表和数组
第 9 集: 总结和结论
在第一集中,我们介绍了 Parrot 编译器工具,并使用 Parrot 发行版提供的 shell 脚本生成了一种非常简单的语言。我们还宣布了 Squaak,一种专门为本教程开发的简单编程语言。Squaak 将作为案例研究,展示 PCT 如何作为一组非常有效的工具来实现 Parrot 的语言。Squaak 的特性列表已经列出。如果你感觉幸运,你甚至可以尝试完成上一集末尾的练习。
在本集中,我们将更深入地了解生成的编译器。我们将查看编译过程的不同阶段,并展示基于 PCT 的编译器中正在发生的事情。
还记得我们之前是如何调用编译器的吗?我们可以传递一个文件,或者在不使用命令行参数的情况下调用编译器,在这种情况下,编译器将进入交互模式。考虑第一种情况,传递文件 test.sq,就像我们之前做的那样
$ parrot squaak.pbc test.sq
当像这样调用编译器时,文件 test.sq 会被编译,生成的代码(字节码)会立即被 Parrot 执行。你可能会好奇,这是如何工作的呢?脚本的解释是通过一系列转换完成的,从脚本源开始,最终以 Parrot 可执行的格式结束。使用 PCT(基于 HLL::Compiler 类)构建的编译器可以接受一个 target 选项,以显示其中一个中间表示。此选项可以具有以下值,对应于 HLLCompiler 对象的四个默认编译阶段
- --target=parse
- --target=past
- --target=post
- --target=pir
这是一个使用 target 选项设置为 “parse” 的示例,它将打印输入的解析树到标准输出
$ parrot squaak.pbc --target=parse test.sq
在交互模式下,输入此内容
say 42;
将打印此解析树(不带行号)
1 "parse" => PMC 'Regex;Match' => "say 42;\n" @ 0 { 2 <statementlist> => PMC 'Regex;Match' => "say 42;\n" @ 0 { 3 <statement> => ResizablePMCArray (size:1) [ 4 PMC 'Regex;Match' => "say 42" @ 0 { 5 <statement_control> => PMC 'Regex;Match' => "say 42" @ 0 { 6 <EXPR> => ResizablePMCArray (size:1) [ 7 PMC 'Regex;Match' => "42" @ 4 { 8 <integer> => PMC 'Regex;Match' => "42" @ 4 { 9 <decint> => PMC 'Regex;Match' => "42" @ 4 10 <VALUE> => \parse[0][0] 11 } 12 } 13 ] 14 <sym> => PMC 'Regex;Match' => "say" @ 0 15 } 16 } 17 ] 18 } 19 }
当更改 target 选项的值时,输出会变成输入的不同表示。现在何不尝试一下?
因此,一个 HLL::Compiler 对象有四个编译阶段:解析、构建鹦鹉抽象语法树 (PAST)、构建鹦鹉操作码语法树 (POST)、生成鹦鹉中间表示 (PIR)。编译完成后,生成的 PIR 会立即执行。
如果你的编译器需要额外的阶段,你可以将它们添加到你的 HLL::Compiler 对象中。对于 Squaak,我们不需要这样做,但有关详细信息,请查看 compilers/pct/src/PCT/HLLCompiler.pir。
我们现在将更详细地讨论每个编译阶段。前两个阶段,解析输入和构建 PAST 是同时执行的。因此,我们将一起讨论它们。
在解析阶段,使用 Perl 6 的扩展正则表达式(称为规则,有关详细信息,请参阅 概要 5)分析输入。当一个规则匹配某个输入字符串时,就会创建一个所谓的匹配对象。匹配对象是一个组合的数组和哈希表,这意味着它可以通过整数和字符串进行索引。由于规则通常由其他(子)规则组成,因此很容易检索匹配的某个部分。例如,此规则
rule if_statement { 'if' <expression> 'then' <statement> 'end' {*} }
有两个子规则:expression 和 statement。if_statement 规则的匹配对象表示从 if 到 end 的整个字符串。当您只对 expression 或 statement 部分感兴趣时,您可以通过子规则的名称(在本例中分别为 expression 和 statement)索引匹配对象来检索它。
在解析阶段,PAST 会被构建。有一组小的 PAST 节点类型,例如,PAST::Var 用于表示变量(标识符,例如 print),PAST::Val 用于表示文字值(例如,“hello” 和 42),等等。稍后我们将更详细地讨论各种 PAST 节点。
现在,你可能想知道,PAST 的构建究竟是在哪个点发生的?这就是上面显示的 if_statement 规则中“if”字符串下面的特殊符号 {*} 的作用。这些特殊标记指示应该调用解析操作。这样的解析操作只是一个方法,它具有与它所在的规则相同的名称(在本例中为:if_statement)。因此,在解析阶段,会执行多个解析操作,每个操作都构建一个表示输入字符串的总 PAST 的一部分。稍后将对此进行更多解释。
鹦鹉抽象语法树只是同一个输入字符串(被编译的程序)的不同表示。它是一个方便的数据结构,可以将其转换为不同的东西(例如可执行的 Parrot 代码),但也用于进行各种分析,例如编译时类型检查。
在构建 PAST 的解析阶段之后,HLL::Compiler
将此 PAST 转换为称为 鹦鹉操作码语法树 (POST) 的东西。POST 表示也是一个树结构,但这些节点处于更低的抽象级别。例如,在 PAST 级别上,有一个节点类型用于表示 while
语句(构建为 PAST::Op.new( :pasttype('while') )
)。while 语句的模板通常包含多个标签和跳转指令。在 POST 级别上,相同的 while 语句由一组节点表示,每个节点表示一个指令或一个标签。因此,将 POST 转换为可执行的东西比从 PAST 级别进行转换要容易得多。
通常,作为 PCT 的用户,您不需要了解 POST 节点的详细信息,这就是我们不会详细讨论它的原因。使用 target 选项查看 POST 的样子。
在第四(也是最后)阶段,POST 被转换为鹦鹉中间表示 (PIR)。如前所述,将 POST 转换为可执行的东西相当简单,因为 POST 节点已经表示单独的指令和标签。同样,正常使用 PCT 不需要您了解此转换的任何细节。
我们已经建立了基于 PCT 的编译器的通用数据流,它包含四个阶段:
- 源代码到解析树
- 解析树到 PAST
- PAST 到 POST
- POST 到 PIR
我们注意到前两个阶段在解析阶段完成。现在,当您阅读本教程时,您可能对使用 PCT 为 Parrot 实现您喜欢的语言感兴趣。我们已经看到语言语法是用 Perl 6 规则表达的。那么其他转换呢?
嗯,在本节的前面,我们提到了“解析操作”这个术语,这些操作创建 PAST 节点。在您为每个语法规则编写了解析操作后,您就完成了!
没错。一旦您正确构建了 PAST,您的编译器就可以生成可执行的 PIR 代码,这意味着您刚刚为 Parrot 实现了自己的第一种语言。当然,您仍然需要实现任何特定于语言的库,但这无关紧要。
基于 PCT 的编译器已经知道如何将 PAST 转换为 POST,以及如何将 POST 转换为 PIR。这些转换阶段已由 PCT 提供。
在本节中,我们更深入地了解了基于 PCT 的编译器的内部机制。我们讨论了四个编译阶段,它们将输入字符串(根据您的定义,可能是程序或脚本)转换为 PAST、POST,最后转换为可执行的 PIR 代码。接下来的部分是“有趣的部分”:我们将为 Parrot 实现 Squaak。我们将逐步实现解析器和解析操作。最后,我们将演示 John Conway 的“生命游戏”在 Parrot 上运行,该游戏是用 Squaak 实现的。
从下一节开始,练习会更有趣。现在,浏览编译器的源文件,看看您是否理解 Grammar.pg 中的语法规则与 Actions.pm 中的方法之间的关系会很有帮助。
实验本节中描述的 --target 选项也很有用。如果您不了解 PIR,现在是做一些准备的时候了。关于 PIR 的信息很多,请参阅参考资料部分以获取详细信息。同时,如果您有任何建议、问题等,请随时留言。
- PIR 语言规范:docs/pdds/pdd19_pir.pod
- PIR 书籍:docs/book/pir