鹦鹉虚拟机/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