鹦鹉虚拟机/Squaak 教程/作用域和子程序
第 1 集: 介绍
第 2 集: 深入编译器内部
第 3 集: Squaak 细节和第一步
第 4 集: PAST 节点和更多语句
第 5 集: 变量声明和作用域
第 6 集: 作用域和子程序
第 7 集: 运算符和优先级
第 8 集: 哈希表和数组
第 9 集: 总结和结论
在 第 5 集 中,我们学习了变量声明和作用域实现。我们当时涵盖了大量信息,但为了保持文章简短,没有讲完整的故事。在本集里,我们将介绍遗漏的部分,这也会导致子程序的实现。
在上一集里,我们把局部变量放入当前块的符号表中。正如我们之前所见,使用 do-block 语句,作用域可以嵌套。考虑这个例子
do var x = 42 do print(x) end end
在这个例子中,print 语句应该打印 42,即使 x 没有在引用它的作用域中声明。编译器是如何知道它仍然是一个局部变量的呢?这很简单:它应该在所有作用域中查找,从最内层的开始。只有当在任何作用域中找到该变量时,才应该将它的作用域设置为“词法”,以便生成正确的指令。
我想到的解决方案如下所示。请注意,我不能 100% 确定这是“最佳”解决方案,因为我对 PAST 编译器的个人理解有限。因此,虽然这个解决方案有效,但我可能会教你错误的“习惯”。请注意这一点。
method identifier($/) { our @?BLOCK; my $name := ~$<ident>; my $scope := 'package'; # default value # go through all scopes and check if the symbol # is registered as a local. If so, set scope to # local. for @?BLOCK { if $_.symbol($name) { $scope := 'lexical'; } } make PAST::Var.new( :name($name), :scope($scope), :viviself('Undef'), :node($/) ); }
你可能之前注意到了 viviself 属性。这个属性会导致额外的指令,这些指令会在变量不存在时初始化它。如你所知,全局变量在使用时会自动创建。我们之前提到过未初始化的变量的默认值为“Undef”:viviself 属性就是做这个的。对于局部变量,我们使用这种机制来设置(可选的)初始化值。当标识符是一个参数时,如果该参数所属的子程序被调用时没有收到值,那么该参数将被自动初始化。实际上,这意味着 Squaak 中的所有参数都是可选的!
我们之前已经提到过子程序,并介绍了 PAST::Block
节点类型。我们还简要提到了可以在 PAST::Block
节点上设置的 blocktype
属性,它指示该块是立即执行(例如,do-block 或 if 语句)还是表示声明(例如,子程序)。现在让我们看看子程序定义的语法规则
rule sub_definition { 'sub' <identifier> <parameters> <statement>* 'end' {*} } rule parameters { '(' [<identifier> [',' <identifier>]* ]? ')' {*} }
这相当直接,这些规则的操作方法也很简单,你将会看到。然而,首先让我们看看子定义的规则。为什么子体被定义为 <statement>*
而不是 <block>
?当然,子程序定义了一个新的作用域,这已经被 <block>
涵盖了。好吧,你说得对。但是,正如我们将会看到的那样,当创建一个新的 PAST::Block
节点时,我们已经太晚了!参数已经被解析,并且没有被放入块的符号表中。这是一个问题,因为参数很可能在子程序体中使用,而且由于它们没有被注册为局部变量(它们是),所以对参数的任何使用都不会被编译成获取任何参数的正确指令。
那么,我们如何以一种高效的方式解决这个问题呢?
解决方案很简单。参数只存在于子程序体中,由一个 PAST::Block
节点表示。为什么不在 parameters
规则的操作方法中创建 PAST::Block
节点呢?这样做,块就已经到位了,参数也会及时地被注册为局部符号。让我们看看操作方法。
method parameters($/) { our $?BLOCK; our @?BLOCK; my $past := PAST::Block.new( :blocktype('declaration'), :node($/) ); # now add all parameters to this block for $<identifier> { my $param := $( $_ ); $param.scope('parameter'); $past.push($param); # register the parameter as a local symbol $past.symbol($param.name(), :scope('lexical')); } # now put the block into place on the scope stack $?BLOCK := $past; @?BLOCK.unshift($past); make $past; } method sub_definition($/) { our $?BLOCK; our @?BLOCK; my $past := $( $<parameters> ); my $name := $( $<identifier> ); # set the sub's name $past.name( $name.name() ); # add all statements to the sub's body for $<statement> { $past.push( $( $_ ) ); } # and remove the block from the scope # stack and restore the current block @?BLOCK.shift(); $?BLOCK := @?BLOCK[0]; make $past; }
首先,让我们看看 parameters 的解析操作。首先,创建一个新的 PAST::Block
节点。然后,我们遍历标识符列表(可以为空),每个标识符代表一个参数。在获取参数的结果对象(只是一个标识符)后,我们将它的作用域设置为“parameter”,并将它添加到块对象中。之后,我们以“lexical”的作用域将参数注册为块对象的符号。参数只是局部变量的一种特殊类型,子程序中参数和声明的局部变量之间没有区别,除了参数通常会用子程序调用时传递的值进行初始化。
处理完参数后,我们将当前块(由我们的包变量 $?BLOCK
引用)设置为我们刚刚创建的 PAST::Block
节点,并将它推送到作用域栈(由我们的包变量 @?BLOCK
引用)上。
在整个子程序定义解析完后,会调用操作方法 sub_definition
。这将获取参数的结果对象,该对象是将代表子程序的 PAST::Block
节点。在获取子程序名称的结果对象后,我们将名称设置在块节点上,并将所有语句添加到块中。之后,我们将这个块节点从作用域栈(@?BLOCK
)中弹出,并恢复当前块($?BLOCK
)。
很简单,对吧?
定义了子程序后,你就会想要调用它。在 第 5 集 的练习中,我们已经提供了一些关于如何创建子程序调用的 PAST 节点的提示。在本节中,我们将提供完整的描述。首先,我们将介绍语法规则。
rule sub_call { <primary> <arguments> {*} }
这不仅允许你通过名称调用子程序,你还可以将子程序存储在数组或哈希字段中,然后从那里调用它们。让我们来看看操作方法,它非常直接。
method sub_call($/) { my $invocant := $( $<primary> ); my $past := $( $<arguments> ); $past.unshift($invocant); make $past; } method arguments($/) { my $past := PAST::Op.new( :pasttype('call'), :node($/) ); for $<expression> { $past.push( $( $_ ) ); } make $past; }
sub_call
方法的结果对象应该是一个 PAST::Op 节点(类型为 'call'),它包含多个子节点:第一个是调用者对象,所有剩余的子节点都是对该子程序调用的参数。
为了将参数的结果对象“移动”到 sub_call
方法中,我们在方法参数中创建了 PAST::Op 节点,然后由 sub_call
获取。在 sub_call
中,调用者对象被设置为第一个子节点(使用 unshift)。这很容易,不是吗? :-)
在本集里,我们完成了 Squaak 中作用域的实现,并实现了子程序。我们的语言进展顺利!在下一集里,我们将探索如何实现运算符和运算符优先级表,以便高效地解析表达式。
与此同时,如果你遇到任何问题或疑问,请随时留言!
- 问题 1
现在你应该对 Squaak 中作用域的实现有了一个很好的了解。我们还没有实现 for 语句,因为它需要适当的作用域处理才能实现。实现这个。查看第三集,了解定义 for 语句语法的 BNF 规则。在实现它的时候,你会遇到和我们实现子例程和参数时一样的困难。使用相同的技巧来实现 for 语句。
- 解答
首先,让我们看看 for 语句的 BNF
for-statement ::= 'for' for-init ',' expression [step] 'do' block 'end' step ::= ',' expression for-init ::= 'var' identifier '=' expression
将其转换为 Perl 6 规则非常容易
rule for_statement { 'for' <for_init> ',' <expression> <step>? 'do' <block> 'end' {*} } rule step { ',' <expression> {*} } rule for_init { 'var' <identifier> '=' <expression> {*} }
非常容易吧?让我们看一下语义。for 循环只是编写 while 循环的另一种方式,但在某些情况下更容易。这个
for var <ident> = <expr1>, <expr2>, <expr3> do <block> end
对应于
do var <ident> = <expr1> while <ident> <= <expr2> do <block> <ident> = <ident> + <expr3> end end
如果 <expr3>
缺失,则默认为值“1”。注意,步长表达式 (expr3
) 应该是正数;循环条件包含 <=
运算符。当你指定一个负数步长表达式时,循环变量只会减少值,这永远不会使循环条件为假(除非它溢出,但那是另一个问题;这甚至可能在 Parrot 中引发异常;我并不知道)。允许负数步长表达式会引入更多复杂性,我认为这对于本教程语言来说不值得。
注意,循环变量 <ident>
对 for 循环是本地的;这在等效的 while 循环中通过周围的 do/end 对来表示:一个新的 do/end 对定义了一个新的(嵌套的)作用域;在 end
关键字之后,循环变量不再可见。
让我们实现 for 语句的动作方法。正如练习说明中提到的,我们遇到了与子例程参数相同的情况。在这种情况下,我们正在处理对 for 语句是本地的循环变量。让我们看看 for_init
的规则
method for_init($/) { our $?BLOCK; our @?BLOCK; ## create a new scope here, so that we can ## add the loop variable ## to this block here, which is convenient. $?BLOCK := PAST::Block.new( :blocktype('immediate'), :node($/) ); @?BLOCK.unshift($?BLOCK); my $iter := $( $<identifier> ); ## set a flag that this identifier is being declared $iter.isdecl(1); $iter.scope('lexical'); ## the identifier is initialized with this expression $iter.viviself( $( $<expression> ) ); ## enter the loop variable into the symbol table. $?BLOCK.symbol($iter.name(), :scope('lexical')); make $iter; }
所以,正如我们在参数动作方法中为子例程创建了一个新的 PAST::Block
一样,我们在定义循环变量的动作方法中为 for 语句创建了一个新的 PAST::Block
。(猜猜我们为什么将 for-init 设置为一个子规则,而没有在 for 语句规则中放入“var <ident> = <expression>
”)。此块是循环变量存在的场所。循环变量被声明,使用 viviself
属性进行初始化,并被输入到新块的符号表中。注意,在创建新的 PAST::Block
对象之后,我们将它放到堆栈作用域上。
现在,for 语句的动作方法很长,所以我只会嵌入我的注释,这使得阅读起来更容易。
method for_statement($/) { our $?BLOCK; our @?BLOCK;
首先,获取 for 语句初始化规则的结果对象;这是 PAST::Var
对象,表示循环变量的声明和初始化。
my $init := $( $<for_init> );
然后,为循环变量创建一个新节点。是的,另一个(除了当前包含在 PAST::Block
中的)。这个节点在循环体代码结束时(每次迭代)更新循环变量时使用。与另一个节点的不同之处在于,它没有 isdecl
标志,也没有 viviself
子句,这会导致额外的指令检查变量是否为空(我们知道它不是空的,因为我们初始化了循环变量)。
## cache the name of the loop variable my $itername := $init.name(); my $iter := PAST::Var.new( :name($itername), :scope('lexical'), :node($/) );
现在,从作用域堆栈中检索 PAST::Block
节点,并将所有语句 PAST 节点推入它。
## the body of the loop consists of the statements written by the user and ## the increment instruction of the loop iterator. my $body := @?BLOCK.shift(); $?BLOCK := @?BLOCK[0]; for $<statement> { $body.push($($_)); }
如果存在一个步长,我们使用该值;否则,我们使用默认步长“1”。
负数步长将不起作用,但如果你感觉幸运,你可以继续尝试。这并不难,只是很多工作,我现在太懒了……嗯,我的意思是,我把这作为习题留给读者。
my $step; if $<step> { my $stepsize := $( $<step>[0] ); $step := PAST::Op.new( $iter, $stepsize, :pirop('add'), :node($/) ); } else { ## default is increment by 1 $step := PAST::Op.new( $iter, :pirop('inc'), :node($/) ); }
循环变量的递增是循环体的一部分,所以将递增语句添加到 $body
中。
$body.push($step);
循环条件使用 <=
运算符,并将循环变量与指定的最大值进行比较。
## while loop iterator <= end-expression my $cond := PAST::Op.new( $iter, $( $<expression> ), :name('infix:<=') );
现在我们有了循环条件和循环体的 PAST,所以现在创建一个 PAST 来表示(while
)循环。
my $loop := PAST::Op.new( $cond, $body, :pasttype('while'), :node($/) );
最后,循环变量的初始化应该在循环本身之前进行,所以创建一个 PAST::Stmts
节点来执行此操作
make PAST::Stmts.new( $init, $loop, :node($/) ); }
哇,我们做到了!这是一个很好的例子,说明如何使用 PAST 实现非平凡的语句类型。