鹦鹉虚拟机/Squaak 教程/Squaak 细节和第一步
第 1 集: 简介
第 2 集: 窥探编译器内部
第 3 集: Squaak 细节和第一步
第 4 集: PAST 节点和更多语句
第 5 集: 变量声明和作用域
第 6 集: 作用域和子程序
第 7 集: 运算符和优先级
第 8 集: 哈希表和数组
第 9 集: 总结和结论
在之前的几集中,我们介绍了 Parrot 编译器工具 (PCT)。从高层次概述开始,我们快速创建了自己的小型脚本语言 Squaak,使用 Parrot 提供的 Perl 脚本。我们讨论了基于 PCT 的编译器的通用结构,以及每个默认的四个转换阶段。第三集是乐趣开始的地方。在本集中,我们将介绍 Squaak 的完整规范。在本集及后续集中,我们将逐步实现此规范,以易于理解的小增量进行。一旦你掌握了窍门,你就会注意到实现 Squaak 几乎是微不足道的,最重要的是,非常有趣!所以,让我们开始吧!
事不宜迟,以下是 Squaak 的完整语法规范。此规范使用以下元语法
statement indicates a non-terminal, named "statement" {statement} indicates zero or more statements [step] indicates an optional step 'do' indicates the keyword 'do'
以下是 Squaak 的语法。起始符号是 program。
program ::= {stat-or-def} stat-or-def ::= statement | sub-definition statement ::= if-statement | while-statement | for-statement | try-statement | throw-statement | variable-declaration | assignment | sub-call | do-block block ::= {statement} do-block ::= 'do' block 'end' if-statement ::= 'if' expression 'then' block ['else' block] 'end' while-statement ::= 'while' expression 'do' block 'end' for-statement ::= 'for' for-init ',' expression [step] 'do' block 'end' step ::= ',' expression for-init ::= 'var' identifier '=' expression try-statement ::= 'try' block 'catch' identifier block 'end' throw-statement ::= 'throw' expression sub-definition ::= 'sub' identifier parameters block 'end' parameters ::= '(' [identifier {',' identifier}] ')' variable-declaration ::= 'var' identifier ['=' expression] assignment ::= primary '=' expression sub-call ::= primary arguments primary ::= identifier postfix-expression* postfix-expression ::= key | index | member key ::= '{' expression '}' index ::= '[' expression ']' member ::= '.' identifier arguments ::= '(' [expression {',' expression}] ')' expression ::= expression {binary-op expression} | unary-op expression | '(' expression ')' | term term ::= float-constant | integer-constant | string-constant | array-constructor | hash-constructor | primary hash-constructor ::= '{' [named-field {',' named-field}] '}' named-field ::= string-constant '=>' expression array-constructor ::= '[' [expression {',' expression} ] ']' binary-op ::= '+' | '-' | '/' | '*' | '%' | '..' | 'and | 'or' | '>' | '>=' | '<' | '<=' | '==' | '!=' unary-op ::= 'not' | '-'
哇,真多,不是吗?实际上,与 C 等“现实世界”语言相比,这个语法相当小,更不用说 Perl 6 了。不过不用担心,我们不会一次性实现所有内容,而是分步进行。更重要的是,练习部分包含了足够的练习,让你自己学习使用 PCT!这些练习的解答将在几天后发布(但你只需要几个小时就能想出来)。
Squaak 语言的大部分内容都非常简单;if 语句的执行方式完全符合你的预期。当我们讨论语法规则(及其实现)时,会包含语义规范。这样做是为了避免我编写完整的语言手册,这可能需要几页纸。
尽管 Squaak 编译器可以在交互模式下使用,但需要注意一点。当使用 'var' 关键字定义局部变量时,此变量将在任何连续的命令中丢失。该变量仅对同一命令内的其他语句可用(命令是在你按下 Enter 键之前的语句集)。这与 PCT 的代码生成有关,将在以后修复。目前,请记住它不起作用。
在本集的剩余部分,我们将实现语法的基本部分,例如基本数据类型和赋值。在本集结束时,你将能够将简单值赋给(全局)变量。这不算多,但这是非常重要的一步。一旦这些基础到位,你就会注意到添加特定的语法结构只需几分钟。
首先,打开你的编辑器并打开文件 src/Squaak/Grammar.pm
和 src/Squaak/Actions.pm
。前者使用 Perl 6 规则实现解析器,后者包含解析操作,这些操作在解析阶段执行。
在文件 Grammar.pm
中,你会看到顶级规则,名为“TOP”。它位于……顶部。当调用解析器时,它将从此规则开始(规则只不过是语法类的某种方法)。
当我们生成这种语言(在第一集中)时,定义了一些默认规则。现在我们要进行一些小的更改,足以让我们开始。首先,将 statement 规则更改为
rule statement { <assignment> {*} }
并添加以下规则
rule assignment { <primary> '=' <expression> {*} } rule primary { <identifier> {*} } token identifier { <!keyword> <ident> {*} } token keyword { ['and'|'catch'|'do' |'else' |'end' |'for' |'if' |'not'|'or' |'sub' |'throw'|'try' |'var'|'while']>> }
现在,将规则“value”更改为此(重命名为“expression”)
rule expression { | <string_constant> {*} #= string_constant | <integer_constant> {*} #= integer_constant }
将规则“integer”重命名为“integer_constant”,将“quote”重命名为“string_constant”(以更好地匹配我们的语言规范)。
呼,信息量很大!让我们仔细看看一些可能看起来不熟悉的东西。第一个新东西在规则“identifier”中。你看到的是关键字“token”,而不是“rule”关键字。简而言之,token 不会跳过 token 中指定的不同部分之间的空格,而 rule 会跳过。目前,记住如果要匹配不包含任何空格的字符串(例如文字常量和标识符),请使用 token,如果你的字符串包含(并且应该包含)空格(例如 if 语句),请使用 rule 就足够了。我们将以一般意义上使用“rule”一词,它可以指代 token。有关规则和 token 的更多信息(以及第三种类型,称为“regex”),请参阅概要 5。
在 token“identifier”中,第一个子规则称为断言。它断言“identifier”不匹配规则关键字。换句话说,不能将关键字用作标识符。第二个子规则称为“ident”,它是类 PCT::Grammar 中的内置规则,该语法是该类的子类。
在 token“keyword”中,列出了 Squaak 的所有关键字。最后有一个“>>”标记,表示单词边界。如果没有此标记,则诸如“forloop”之类的标识符将被错误地取消资格,因为“for”部分将匹配规则关键字,而“loop”部分将匹配规则“ident”。但是,由于断言<!keyword> 为假(因为可以匹配“for”),因此字符串“forloop”不能作为标识符匹配。所需单词边界的存在可以防止这种情况。
最后一个规则是“expression”。表达式要么是字符串常量,要么是整数常量。无论哪种方式,都会执行一个操作。但是,当执行操作时,它不知道解析器匹配了什么;是字符串常量还是整数常量?当然,可以检查匹配对象,但考虑一下你有 10 个备选方案的情况,然后进行 9 次检查只是为了发现最后一个备选方案被匹配的情况效率低下(并且添加新的备选方案需要你更新此检查)。这就是为什么你会看到以“#=
”字符开头的特殊注释。使用此表示法,你可以指定一个键,该键将作为第二个参数传递给操作方法。正如我们将看到的,这使我们能够为诸如 expression 之类的规则编写非常简单高效的操作方法。(请注意,#=
和键名称之间有一个空格)。
在编写任何操作方法之前测试解析器很有用。这可以为你节省大量工作;如果你在编写语法规则后立即编写操作,并且只是在稍后发现必须更新解析器,那么你的操作方法可能也需要更新。在第 2 集中,我们看到了目标命令行选项。为了测试解析器,“parse”目标特别有用。指定此选项时,你的编译器将打印输入字符串的解析树,或打印语法错误。明智的做法是用正确和不正确的输入测试你的解析器,这样你才能确定你的解析器不会接受不应该接受的输入。
现在我们已经实现了 Squaak 语法的初始版本,是时候实现我们之前提到的解析动作了。这些动作写在名为 src/Squaak/Actions.pm
的文件中。如果你查看此文件中的方法,你会发现这里和那里 Match
对象 ($/)
,或者更确切地说,它的哈希字段(例如 $<statement>
)是在标量上下文中通过编写“$( ... )
”来评估的。
如概要 5 中所述,在标量上下文中评估 Match
对象会返回其结果对象。通常,结果对象是源文本中匹配的部分,但可以使用特殊的 make
函数将结果对象设置为其他值。
这意味着解析树中的每个节点(一个 Match
对象)也可以保存其 PAST 表示。因此,我们使用 make
函数设置解析树中当前节点的 PAST 表示,然后使用 $( ... )
运算符从中检索 PAST 表示。
概括来说,Match
对象 ($/)
及其任何子规则(例如 $<statement>
)表示解析树;当然,$<statement>
只表示 <statement> 规则匹配的解析树。因此,任何动作方法都可以访问与同名语法规则匹配的解析树,因为 Match
对象始终作为参数传递。在标量上下文中评估解析树会产生 PAST 表示(显然,此 PAST 对象应使用 make
函数设置)。
如果你正在学习本教程,我强烈建议你动手实践,完成练习。记住,学而不练等于没学(或者类似的话 :-)。本周的练习并不难,完成之后,你将实现我们的小型 Squaak 语言的第一部分。
在本节中,我们介绍了 Squaak 的完整语法。我们迈出了实现这门语言的第一步。第一个,也是目前唯一的语句类型是赋值语句。我们简要介绍了如何在解析阶段编写调用的动作方法。在下一节中,我们将更仔细地研究不同的 PAST 节点类型,并实现 Squaak 语言的更多部分。一旦我们所有基本部分都到位,添加语句类型将变得相当简单。在此期间,如果您有任何疑问或遇到困难,请随时发表评论或联系我。
本节的练习非常简单,可以帮助你开始实现 Squaak。
- 问题 1
根据我们在语法规则上所做的名称更改,重命名动作方法的名称。因此,“integer”变为“integer_constant”,“value”变为“expression”,依此类推。
- 问题 2
查看 statement 语法规则。目前,statement 由一个赋值语句组成。实现动作方法“statement”以检索此赋值语句的结果对象,并使用特殊的 make 函数将其设置为 statement 的结果对象。对 primary 规则执行相同的操作。
- 解答
method statement($/) { make $( $<assignment> ); }
请注意,在这一点上,statement 规则没有为每种类型的语句定义不同的 #= 键,因此我们不声明参数 $key。这将在以后更改。
method primary($/) { make $( $<identifier> ); }
- 问题 3
编写 identifier 规则的动作方法。作为此“匹配”的结果对象,应设置一个新的 PAST::Var 节点,其名称为匹配对象 ($/) 的字符串表示形式。目前,你可以将作用域设置为“package”。有关 PAST::Var 节点的详细信息,请参阅“pdd26: ast”。
- 解答
method identifier($/) { make PAST::Var.new( :name(~$/), :scope('package'), :node($/) ); }
- 问题 4
编写 assignment 规则的动作方法。检索“primary”和“expression”的结果对象,并创建一个 PAST::Op 节点,将表达式绑定到 primary。(查看 pdd26 以了解 PAST::Op 节点类型,并了解如何进行此类绑定)。
- 解答
method assignment($/) { my $lhs := $( $<primary> ); my $rhs := $( $<expression> ); $lhs.lvalue(1); make PAST::Op.new( $lhs, $rhs, :pasttype('bind'), :node($/) ); }
请注意,我们在 $lhs 上设置了 lvalue 标志。有关此标志的详细信息,请参阅 PDD26。
- 问题 5
在脚本或交互模式下运行你的编译器。使用 target 选项查看输入“x = 42”生成的 PIR 代码。
- 解答
.namespace .sub "_block10" new $P11, "Integer" assign $P11, 42 set_global "x", $P11 .return ($P11) .end
子程序中的前两行代码创建了一个对象来存储数字 42,第三行将此数字存储为“x”。PAST 编译器将始终生成一条指令以返回最后一个语句的结果,在本例中为 $P11。
- 帮助!我收到错误消息“no result object”。
这意味着结果对象未正确设置(显而易见!)。确保每个动作方法都被调用(检查每个规则的“{*}”标记),并且该规则存在一个动作方法,并且“make”用于设置相应的 PAST 节点。请注意,并非所有规则都有动作方法,例如“keyword”规则(这样做没有意义)。
- 在我们构建 Squaak 语法的一部分时,有时会采取捷径,暂时忽略某些规则。例如,你可能已经注意到我们现在忽略了浮点数常量。没关系。当我们需要它们时,将添加这些规则。
- pdd26: ast
- 概要 5:规则
- docs/pct/*.pod