跳轉至內容

鸚鵡虛擬機器/Squaak 教程/Squaak 細節和第一步

來自 Wikibooks,開放世界中的開放書籍

在之前的幾集中,我們介紹了 Parrot 編譯器工具 (PCT)。從高層次概述開始,我們快速建立了自己的小型指令碼語言 Squaak,使用 Parrot 提供的 Perl 指令碼。我們討論了基於 PCT 的編譯器的通用結構,以及每個預設的四個轉換階段。第三集是樂趣開始的地方。在本集中,我們將介紹 Squaak 的完整規範。在本集及後續集中,我們將逐步實現此規範,以易於理解的小增量進行。一旦你掌握了竅門,你就會注意到實現 Squaak 幾乎是微不足道的,最重要的是,非常有趣!所以,讓我們開始吧!

Squaak 語法

[編輯 | 編輯原始碼]

事不宜遲,以下是 Squaak 的完整語法規範。此規範使用以下元語法

   statement   indicates a non-terminal, named "statement"
   {statement} indicates zero or more statements
   [step]      indicates an optional step
   'do'        indicates the keyword 'do'

以下是 Squaak 的語法。起始符號是 program。

    program              ::= {stat-or-def}

    stat-or-def          ::= statement
                           | sub-definition

    statement            ::= if-statement
                           | while-statement
                           | for-statement
                           | try-statement
                           | throw-statement
                           | variable-declaration
                           | assignment
                           | sub-call
                           | do-block

    block                ::= {statement}

    do-block             ::= 'do' block 'end'

    if-statement         ::= 'if' expression 'then' block
                             ['else' block]
                             'end'

    while-statement      ::= 'while' expression 'do'
                             block 'end'

    for-statement        ::= 'for' for-init ',' expression [step]
                             'do'
                             block
                             'end'

    step                 ::= ',' expression

    for-init             ::= 'var' identifier '=' expression

    try-statement        ::= 'try' block 'catch' identifier
                             block
                             'end'

    throw-statement      ::= 'throw' expression

    sub-definition       ::= 'sub' identifier parameters
                             block
                             'end'

    parameters           ::= '(' [identifier {',' identifier}] ')'

    variable-declaration ::= 'var' identifier ['=' expression]

    assignment           ::= primary '=' expression

    sub-call             ::= primary arguments

    primary              ::= identifier postfix-expression*

    postfix-expression   ::= key
                           | index
                           | member

    key                  ::= '{' expression '}'

    index                ::= '[' expression ']'

    member               ::= '.' identifier

    arguments            ::= '(' [expression {',' expression}] ')'

    expression           ::= expression {binary-op expression}
                           | unary-op expression
                           | '(' expression ')'
                           | term

    term                 ::= float-constant
                           | integer-constant
                           | string-constant
                           | array-constructor
                           | hash-constructor
                           | primary

    hash-constructor     ::= '{' [named-field {',' named-field}] '}'

    named-field          ::= string-constant '=>' expression

    array-constructor    ::= '[' [expression {',' expression} ] ']'

    binary-op            ::= '+'  | '-'  | '/'  | '*'  | '%'  | '..'
                           | 'and | 'or' | '>'  | '>=' | '<'  | '<='
                           | '==' | '!='

    unary-op             ::= 'not' | '-'

哇,真多,不是嗎?實際上,與 C 等“現實世界”語言相比,這個語法相當小,更不用說 Perl 6 了。不過不用擔心,我們不會一次性實現所有內容,而是分步進行。更重要的是,練習部分包含了足夠的練習,讓你自己學習使用 PCT!這些練習的解答將在幾天後釋出(但你只需要幾個小時就能想出來)。

Squaak 語言的大部分內容都非常簡單;if 語句的執行方式完全符合你的預期。當我們討論語法規則(及其實現)時,會包含語義規範。這樣做是為了避免我編寫完整的語言手冊,這可能需要幾頁紙。

互動式 Squaak

[編輯 | 編輯原始碼]

儘管 Squaak 編譯器可以在互動模式下使用,但需要注意一點。當使用 'var' 關鍵字定義區域性變數時,此變數將在任何連續的命令中丟失。該變數僅對同一命令內的其他語句可用(命令是在你按下 Enter 鍵之前的語句集)。這與 PCT 的程式碼生成有關,將在以後修復。目前,請記住它不起作用。

讓我們開始吧!

[編輯 | 編輯原始碼]

在本集的剩餘部分,我們將實現語法的基本部分,例如基本資料型別和賦值。在本集結束時,你將能夠將簡單值賦給(全域性)變數。這不算多,但這是非常重要的一步。一旦這些基礎到位,你就會注意到新增特定的語法結構只需幾分鐘。

首先,開啟你的編輯器並開啟檔案 src/Squaak/Grammar.pmsrc/Squaak/Actions.pm。前者使用 Perl 6 規則實現解析器,後者包含解析操作,這些操作在解析階段執行。

在檔案 Grammar.pm 中,你會看到頂級規則,名為“TOP”。它位於……頂部。當呼叫解析器時,它將從此規則開始(規則只不過是語法類的某種方法)。

當我們生成這種語言(在第一集中)時,定義了一些預設規則。現在我們要進行一些小的更改,足以讓我們開始。首先,將 statement 規則更改為

   rule statement {
       <assignment>
       {*}
   }

並新增以下規則

    rule assignment {
        <primary> '=' <expression>
        {*}
    }

    rule primary {
        <identifier>
        {*}
    }

    token identifier {
        <!keyword> <ident>
        {*}
   }

    token keyword {
        ['and'|'catch'|'do'   |'else' |'end' |'for' |'if'
        |'not'|'or'   |'sub'  |'throw'|'try' |'var'|'while']>>
    }

現在,將規則“value”更改為此(重新命名為“expression”)

   rule expression {
       | <string_constant> {*}        #= string_constant
       | <integer_constant> {*}       #= integer_constant
   }

將規則“integer”重新命名為“integer_constant”,將“quote”重新命名為“string_constant”(以更好地匹配我們的語言規範)。

呼,資訊量很大!讓我們仔細看看一些可能看起來不熟悉的東西。第一個新東西在規則“identifier”中。你看到的是關鍵字“token”,而不是“rule”關鍵字。簡而言之,token 不會跳過 token 中指定的不同部分之間的空格,而 rule 會跳過。目前,記住如果要匹配不包含任何空格的字串(例如文字常量和識別符號),請使用 token,如果你的字串包含(並且應該包含)空格(例如 if 語句),請使用 rule 就足夠了。我們將以一般意義上使用“rule”一詞,它可以指代 token。有關規則和 token 的更多資訊(以及第三種類型,稱為“regex”),請參閱概要 5。

在 token“identifier”中,第一個子規則稱為斷言。它斷言“identifier”不匹配規則關鍵字。換句話說,不能將關鍵字用作識別符號。第二個子規則稱為“ident”,它是類 PCT::Grammar 中的內建規則,該語法是該類的子類。

在 token“keyword”中,列出了 Squaak 的所有關鍵字。最後有一個“>>”標記,表示單詞邊界。如果沒有此標記,則諸如“forloop”之類的識別符號將被錯誤地取消資格,因為“for”部分將匹配規則關鍵字,而“loop”部分將匹配規則“ident”。但是,由於斷言<!keyword> 為假(因為可以匹配“for”),因此字串“forloop”不能作為識別符號匹配。所需單詞邊界的存在可以防止這種情況。

最後一個規則是“expression”。表示式要麼是字串常量,要麼是整數常量。無論哪種方式,都會執行一個操作。但是,當執行操作時,它不知道解析器匹配了什麼;是字串常量還是整數常量?當然,可以檢查匹配物件,但考慮一下你有 10 個備選方案的情況,然後進行 9 次檢查只是為了發現最後一個備選方案被匹配的情況效率低下(並且新增新的備選方案需要你更新此檢查)。這就是為什麼你會看到以“#=”字元開頭的特殊註釋。使用此表示法,你可以指定一個鍵,該鍵將作為第二個引數傳遞給操作方法。正如我們將看到的,這使我們能夠為諸如 expression 之類的規則編寫非常簡單高效的操作方法。(請注意,#= 和鍵名稱之間有一個空格)。

測試解析器

[編輯 | 編輯原始碼]

在編寫任何操作方法之前測試解析器很有用。這可以為你節省大量工作;如果你在編寫語法規則後立即編寫操作,並且只是在稍後發現必須更新解析器,那麼你的操作方法可能也需要更新。在第 2 集中,我們看到了目標命令列選項。為了測試解析器,“parse”目標特別有用。指定此選項時,你的編譯器將列印輸入字串的解析樹,或列印語法錯誤。明智的做法是用正確和不正確的輸入測試你的解析器,這樣你才能確定你的解析器不會接受不應該接受的輸入。

然後……行動!

[編輯 | 編輯原始碼]

現在我們已經實現了 Squaak 語法的初始版本,是時候實現我們之前提到的解析動作了。這些動作寫在名為 src/Squaak/Actions.pm 的檔案中。如果你檢視此檔案中的方法,你會發現這裡和那裡 Match 物件 ($/) ,或者更確切地說,它的雜湊欄位(例如 $<statement>)是在標量上下文中透過編寫“$( ... )”來評估的。

如概要 5 中所述,在標量上下文中評估 Match 物件會返回其結果物件。通常,結果物件是源文字中匹配的部分,但可以使用特殊的 make 函式將結果物件設定為其他值。

這意味著解析樹中的每個節點(一個 Match 物件)也可以儲存其 PAST 表示。因此,我們使用 make 函式設定解析樹中當前節點的 PAST 表示,然後使用 $( ... ) 運算子從中檢索 PAST 表示。

概括來說,Match 物件 ($/) 及其任何子規則(例如 $<statement>)表示解析樹;當然,$<statement> 只表示 <statement> 規則匹配的解析樹。因此,任何動作方法都可以訪問與同名語法規則匹配的解析樹,因為 Match 物件始終作為引數傳遞。在標量上下文中評估解析樹會產生 PAST 表示(顯然,此 PAST 物件應使用 make 函式設定)。

如果你正在學習本教程,我強烈建議你動手實踐,完成練習。記住,學而不練等於沒學(或者類似的話 :-)。本週的練習並不難,完成之後,你將實現我們的小型 Squaak 語言的第一部分。

接下來是什麼?

[編輯 | 編輯原始碼]

在本節中,我們介紹了 Squaak 的完整語法。我們邁出了實現這門語言的第一步。第一個,也是目前唯一的語句型別是賦值語句。我們簡要介紹瞭如何在解析階段編寫呼叫的動作方法。在下一節中,我們將更仔細地研究不同的 PAST 節點型別,並實現 Squaak 語言的更多部分。一旦我們所有基本部分都到位,新增語句型別將變得相當簡單。在此期間,如果您有任何疑問或遇到困難,請隨時發表評論或聯絡我。

本節的練習非常簡單,可以幫助你開始實現 Squaak。

問題 1

根據我們在語法規則上所做的名稱更改,重新命名動作方法的名稱。因此,“integer”變為“integer_constant”,“value”變為“expression”,依此類推。

問題 2

檢視 statement 語法規則。目前,statement 由一個賦值語句組成。實現動作方法“statement”以檢索此賦值語句的結果物件,並使用特殊的 make 函式將其設定為 statement 的結果物件。對 primary 規則執行相同的操作。

解答
   method statement($/) {
       make $( $<assignment> );
   }

請注意,在這一點上,statement 規則沒有為每種型別的語句定義不同的 #= 鍵,因此我們不宣告引數 $key。這將在以後更改。

    method primary($/) {
        make $( $<identifier> );
    }
問題 3

編寫 identifier 規則的動作方法。作為此“匹配”的結果物件,應設定一個新的 PAST::Var 節點,其名稱為匹配物件 ($/) 的字串表示形式。目前,你可以將作用域設定為“package”。有關 PAST::Var 節點的詳細資訊,請參閱“pdd26: ast”。

解答
method identifier($/) {
    make PAST::Var.new( :name(~$/),
                        :scope('package'),
                        :node($/) );
}
問題 4

編寫 assignment 規則的動作方法。檢索“primary”和“expression”的結果物件,並建立一個 PAST::Op 節點,將表示式繫結到 primary。(檢視 pdd26 以瞭解 PAST::Op 節點型別,並瞭解如何進行此類繫結)。

解答
   method assignment($/) {
       my $lhs := $( $<primary> );
       my $rhs := $( $<expression> );
       $lhs.lvalue(1);
       make PAST::Op.new( $lhs, $rhs,
                          :pasttype('bind'),
                          :node($/) );
   }

請注意,我們在 $lhs 上設定了 lvalue 標誌。有關此標誌的詳細資訊,請參閱 PDD26。

問題 5

在指令碼或互動模式下執行你的編譯器。使用 target 選項檢視輸入“x = 42”生成的 PIR 程式碼。

解答
   .namespace
   .sub "_block10"
       new $P11, "Integer"
       assign $P11, 42
       set_global "x", $P11
       .return ($P11)
   .end

子程式中的前兩行程式碼建立了一個物件來儲存數字 42,第三行將此數字儲存為“x”。PAST 編譯器將始終生成一條指令以返回最後一個語句的結果,在本例中為 $P11。

一些說明

[編輯 | 編輯原始碼]
  • 幫助!我收到錯誤訊息“no result object”。

這意味著結果物件未正確設定(顯而易見!)。確保每個動作方法都被呼叫(檢查每個規則的“{*}”標記),並且該規則存在一個動作方法,並且“make”用於設定相應的 PAST 節點。請注意,並非所有規則都有動作方法,例如“keyword”規則(這樣做沒有意義)。

  • 在我們構建 Squaak 語法的一部分時,有時會採取捷徑,暫時忽略某些規則。例如,你可能已經注意到我們現在忽略了浮點數常量。沒關係。當我們需要它們時,將新增這些規則。

參考文獻

[編輯 | 編輯原始碼]
  • pdd26: ast
  • 概要 5:規則
  • docs/pct/*.pod
華夏公益教科書