跳转到内容

鹦鹉虚拟机/Squaak 教程/作用域和子程序

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

第 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 实现非平凡的语句类型。

华夏公益教科书