跳轉到內容

鸚鵡虛擬機器/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 實現非平凡的語句型別。

華夏公益教科書