跳轉到內容

鸚鵡虛擬機器/Squaak 教程/PAST 節點和更多語句

來自華夏公益教科書,開放的書籍,開放的世界

上一集介紹了 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-then-else

[編輯 | 編輯原始碼]

我們現在要實現的第一個語句是 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”,這意味著以下順序

  1. 解析 TOP
  2. 解析 statement
  3. 解析 if_statement
  4. 解析 expression
  5. 解析 integer
  6. 建立PAST::Val( :value(42) )
  7. 解析 block
  8. 解析 statement
  9. 解析 assignment
  10. 解析 identifier
  11. 建立PAST::Var( :name('x'))
  12. 解析 integer
  13. 建立PAST::Val( :value(1) )
  14. 建立PAST::Op( :pasttype('bind') )
  15. 建立PAST::Block(在 action 方法 block 中)
  16. 建立PAST::Op( :pasttype('if') )
  17. 建立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 的良好介紹
華夏公益教科書