鹦鹉虚拟机/Squaak 教程/PAST 节点和更多语句
第 1 集: 简介
第 2 集: 窥探编译器内部
第 3 集: Squaak 细节和第一步
第 4 集: PAST 节点和更多语句
第 5 集: 变量声明和作用域
第 6 集: 作用域和子程序
第 7 集: 运算符和优先级
第 8 集: 哈希表和数组
第 9 集: 总结和结论
上一集介绍了 Squaak 的完整语法规范,我们终于开始着手实现。如果你正在做练习,你现在已经有了基本的赋值功能;字符串和整数可以被分配到(全局)变量中。本集将重点介绍一些语句类型的实现,并解释关于不同 PAST 节点类型的几个要点。
鹦鹉抽象语法树 (PAST) 代表用 Squaak(或任何其他移植到鹦鹉的语言)编写的程序,它由节点组成。在上一集,我们已经看到了代表字符串和整数字面量、标识符和“运算符”节点(PAST::Op
)的节点,在本例中是赋值。
其他运算符代表其他高级语言结构,如条件语句、循环和子程序调用。根据节点类型,PAST 节点可以接受子节点。例如,代表 if 语句的 PAST 节点可以最多有三个子节点。第一个子节点代表条件;如果为真,则评估第二个子节点。如果条件评估为假,并且存在第三个子节点,则评估这个第三个子节点(else 部分)。
如果 PAST 代表子程序调用,则子节点以不同的方式评估。在这种情况下,第一个子节点代表要调用的子程序(除非在这个节点上设置了:name
属性,但我们将在后面的章节中详细介绍),所有其他子节点都作为参数传递给该子程序。
子节点的 PAST 节点类型通常并不重要。例如,考虑一种语言,其中简单表达式是一个语句
42
你可能想知道这段代码生成了什么。其实很简单:创建一个新的PAST::Val
节点(某种类型,对于这个例子来说是 'Integer
'),并将该值分配给这个节点。看起来写这样的代码可能有点混乱,因为它实际上并没有做任何事情(请注意,这不是有效的 Squaak 输入)
if 42 then "hi" else "bye" end
但是同样,这也能正常工作;“then” 和“else” 块被编译成将特定字面量加载到PAST::Val
节点并将其保留在那里的指令。如果你的语言允许这样的语句,那就可以了。
我想要说明的是,所有 PAST 节点都是平等的。如果你将一个节点设置为其他父节点的子节点,你不需要考虑节点类型。每个 PAST 节点都会被编译成若干条 PIR 指令。
现在你对 PAST 节点有了更多了解,让我们动手实现更多语句类型。在本集的剩余部分,我们将处理 if 语句和 throw 语句。
我们现在要实现的第一个语句是 if 语句。if 语句通常有三个部分(但这当然取决于编程语言):条件表达式、“then” 部分和“else” 部分。用 Perl 6 规则和 PAST 实现它几乎是微不足道的
rule if_statement {
'if' <expression> 'then' <block>
['else' $<else>=<block> ]?
'end'
{*}
}
rule block {
<statement>*
{*}
}
rule statement {
| <assignment> {*} #= assignment
| <if_statement> {*} #= if_statement
}
请注意,可选的else
块存储在匹配对象的“else
” 字段中。如果我们没有编写这段$<else>=
代码,那么<block>
将是一个数组,其中block[0]
是“then
” 部分,block[1]
是可选的else
部分。将可选的else
块分配到不同的字段,使得 action 方法更容易阅读。
还要注意,statement 规则已经更新了;statement 现在要么是赋值,要么是 if 语句。因此,action 方法 statement 现在接受一个键参数。相关的 action 方法如下所示
method statement($/, $key) {
# get the field stored in $key from the $/ object,
# and retrieve the result object from that field.
make $( $/{$key} );
}
method block($/) {
# create a new block, set its type to 'immediate',
# meaning it is potentially executed immediately
# (as opposed to a declaration, such as a
# subroutine definition).
my $past := PAST::Block.new( :blocktype('immediate'),
:node($/) );
# for each statement, add the result
# object to the block
for $<statement> {
$past.push( $( $_ ) );
}
make $past;
}
method if_statement($/) {
my $cond := $( $<expression> );
my $then := $( $<block> );
my $past := PAST::Op.new( $cond, $then,
:pasttype('if'),
:node($/) );
if $<else> {
$past.push( $( $<else>[0] ) );
}
make $past;
}
这很简单,对吧?首先,我们获取条件表达式和 then 部分的结果对象。然后,创建一个新的PAST::Op
节点,并将:pasttype
设置为 'if',这意味着该节点代表一个 if 语句。然后,如果有“else” 块,则检索该块的结果对象并将其添加为 PAST 节点的第三个子节点。最后,使用 make 函数设置结果对象。
此时,有必要花几句话谈谈make
函数、解析操作以及整个 PAST 如何由各个解析操作创建。再看看 if_statement 的 action 方法。在前两行中,我们请求条件表达式和“then” 块的结果对象。这些结果对象是在什么时候创建的?我们如何才能确定它们存在?答案在于解析操作执行的顺序。触发解析操作调用的特殊{*}
符号通常放在rule
的末尾。对于这个输入字符串:“if 42 then x = 1 end
”,这意味着以下顺序
- 解析 TOP
- 解析 statement
- 解析 if_statement
- 解析 expression
- 解析 integer
- 创建
PAST::Val( :value(42) )
- 解析 block
- 解析 statement
- 解析 assignment
- 解析 identifier
- 创建
PAST::Var( :name('x'))
- 解析 integer
- 创建
PAST::Val( :value(1) )
- 创建
PAST::Op( :pasttype('bind') )
- 创建
PAST::Block
(在 action 方法 block 中) - 创建
PAST::Op( :pasttype('if') )
- 创建
PAST::Block
(在 action 方法 TOP 中)
正如你所见,PAST 节点首先在解析树的叶子中创建,以便以后在解析树中更高层的 action 方法可以检索它们。
“throw” 语句的语法规则非常简单,但讨论解析操作很有用,因为它展示了生成自定义 PIR 指令的使用方法。首先是语法规则
rule throw_statement { 'throw' <expression> {*} }
我假设你现在已经知道如何更新“statement” 规则。throw 语句将被编译成鹦鹉的“throw” 指令,该指令接受一个参数。为了生成自定义的鹦鹉指令,可以在创建 PAST::Op 节点时在 :pirop 属性中指定该指令。任何子节点都将作为参数传递给该指令,因此我们需要将被抛出的表达式的结果对象传递给代表“throw” 指令的 PAST::Op 节点的子节点。
method throw_statement($/) { make PAST::Op.new( $( $<expression> ), :pirop('throw'), :node($/) ); }
在本期中,我们实现了 Squaak 的另外两种语句类型。您应该对 PAST 节点是如何以及何时创建的、以及如何从它们检索子(解析)树有一个大致的了解。在下一期中,我们将更深入地探讨变量作用域和子例程。
与此同时,我可以想象有些东西还不够清楚。如果您迷路了,请随时留言,我会尽力回答(在我的知识范围内)。
- 问题 1
我们展示了 if 语句是如何实现的。while 语句和 try 语句非常相似。实现它们。查看 pdd26 以了解您应该创建哪些 PAST::Op
节点。
- 解决方案
while 语句很简单。
method while_statement($/) { my $cond := $( $<expression> ); my $body := $( $<block> ); make PAST::Op.new( $cond, $body, :pasttype('while'), :node($/) ); }
try 语句稍微复杂一些。以下是语法规则和动作方法。
rule try_statement { 'try' $<try>=<block> 'catch' <exception> $<catch>=<block> 'end' {*} } rule exception { <identifier> {*} } method try_statement($/) { ## get the try block my $try := $( $<try> ); ## create a new PAST::Stmts node for ## the catch block; note that no ## PAST::Block is created, as this ## currently has problems with the ## exception object. For now this will ## do. my $catch := PAST::Stmts.new( :node($/) ); $catch.push( $( $<catch> ) ); ## get the exception identifier; ## set a declaration flag, the scope, ## and clear the viviself attribute. my $exc := $( $<exception> ); $exc.isdecl(1); $exc.scope('lexical'); $exc.viviself(0); ## generate instruction to retrieve ## the exception objct (and the ## exception message, that is passed ## automatically in PIR, this is stored ## into $S0 (but not used). my $pir := " .get_results (%r, $S0)\n" ~ " store_lex '" ~ $exc.name() ~ "', %r"; $catch.unshift( PAST::Op.new( :inline($pir), :node($/) ) ); ## do the declaration of the exception ## object as a lexical here: $catch.unshift( $exc ); make PAST::Op.new( $try, $catch, :pasttype('try'), :node($/) ); } method exception($/) { our $?BLOCK; my $past := $( $<identifier> ); $?BLOCK.symbol( $past.name(), :scope('lexical') ); make $past; }
我们没有在“catch
”关键字后放置“identifier
”,而是将其作为一个单独的规则,并有自己的动作方法。这使我们能够在解析 catch
代码块之前,将标识符插入当前代码块(try 代码块)的符号表中。
首先,检索 try
代码块的 PAST 节点。然后,检索 catch 代码块,并将其存储在一个 PAST::Stmts
节点中。这是必要的,以便我们可以确保检索异常对象的指令在异常处理程序中排在最前面。
然后,我们检索异常标识符的 PAST 节点。我们正在设置它的作用域,一个告诉 PAST 编译器这是一个声明的标志,并且我们清除viviself
属性。viviself
属性将在后面的章节中讨论;如果您还没有阅读,只需记住 viviself
属性(如果设置)将确保所有声明的变量都被初始化。我们必须在这里清除此属性,以确保此异常对象不被初始化,因为这将由检索抛出异常对象的指令完成,将在后面讨论。
在 PIR 中,我们可以使用 .get_results
指令检索抛出的异常。您也可以生成 get_results
指令(注意缺少的点),但这要容易得多。目前,在 PIR 中,检索异常对象时,您必须始终指定一个变量(或寄存器)来存储异常对象,以及一个字符串变量(或寄存器)来存储异常消息。异常消息实际上存储在异常对象中。我们使用 $S0
来存储异常消息,并且之后会忽略它。现在只需记住,如果您想检索异常对象,您还必须指定一个存储异常消息的位置。没有特殊的 PAST 节点来生成这些指令,因此我们使用所谓的内联 PAST::Op
节点。我们将要生成的指令存储在一个字符串中,并将该字符串存储在一个 PAST::Op
节点的inline
属性中。创建后,此节点将被unshift
到表示异常处理程序的 PAST::Stmts
节点上。之后,声明将被存储在该 PAST::Stmts
节点中,以便此声明排在最前面。
最后,我们有表示 try 代码块的代码块,以及一个表示异常处理程序的 PAST::Stmts
节点。两者都被用来创建一个 PAST::Op
节点,其 pasttype
被设置为内置的“try
”类型。
- 问题 2
以交互模式启动 Squaak,并指定目标选项以显示生成的 PIR 指令。查看生成哪些指令和标签,看看您是否能够识别哪些指令构成条件表达式,哪些表示“then”代码块,哪些表示“else”代码块(如果有)。
- 解决方案
以交互模式启动 Squaak,并指定目标选项以显示生成的 PIR 指令。查看生成哪些指令和标签,看看您是否能够识别哪些指令构成条件表达式,哪些表示“then”代码块,哪些表示“else”代码块(如果有)。
> if 1 then else end .namespace .sub "_block16" new $P18, "Integer" assign $P18, 1 ## this is the condition: if $P18, if_17 ## this is invoking the else-block: get_global $P21, "_block19" newclosure $P21, $P21 $P20 = $P21() set $P18, $P20 goto if_17_end ## this is invoking the then-block: if_17: get_global $P24, "_block22" newclosure $P24, $P24 $P23 = $P24() set $P18, $P23 if_17_end: .return ($P18) .end .namespace .sub "_block22" :outer("_block16") .return () .end .namespace .sub "_block19" :outer("_block16") .return () .end
- PDD26:AST
- docs/art/*.pod 用于对 PIR 的良好介绍