跳转到内容

鹦鹉虚拟机/Squaak 教程/窥探编译器内部

来自维基教科书,开放的书籍,开放的世界

在第一集中,我们介绍了 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 是同时执行的。因此,我们将一起讨论它们。

解析阶段:匹配对象和 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 到 POST

[编辑 | 编辑源代码]

在构建 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 被转换为鹦鹉中间表示 (PIR)。如前所述,将 POST 转换为可执行的东西相当简单,因为 POST 节点已经表示单独的指令和标签。同样,正常使用 PCT 不需要您了解此转换的任何细节。

现在带来好消息...

[编辑 | 编辑源代码]

我们已经建立了基于 PCT 的编译器的通用数据流,它包含四个阶段:

  1. 源代码到解析树
  2. 解析树到 PAST
  3. PAST 到 POST
  4. 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 的信息很多,请参阅参考资料部分以获取详细信息。同时,如果您有任何建议、问题等,请随时留言。

参考资料

[编辑 | 编辑源代码]
  1. PIR 语言规范:docs/pdds/pdd19_pir.pod
  2. PIR 书籍:docs/book/pir
华夏公益教科书