鸚鵡虛擬機器/Squaak 教程/Squaak 細節和第一步
第 1 集: 簡介
第 2 集: 窺探編譯器內部
第 3 集: Squaak 細節和第一步
第 4 集: PAST 節點和更多語句
第 5 集: 變數宣告和作用域
第 6 集: 作用域和子程式
第 7 集: 運算子和優先順序
第 8 集: 雜湊表和陣列
第 9 集: 總結和結論
在之前的幾集中,我們介紹了 Parrot 編譯器工具 (PCT)。從高層次概述開始,我們快速建立了自己的小型指令碼語言 Squaak,使用 Parrot 提供的 Perl 指令碼。我們討論了基於 PCT 的編譯器的通用結構,以及每個預設的四個轉換階段。第三集是樂趣開始的地方。在本集中,我們將介紹 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 編譯器可以在互動模式下使用,但需要注意一點。當使用 'var' 關鍵字定義區域性變數時,此變數將在任何連續的命令中丟失。該變數僅對同一命令內的其他語句可用(命令是在你按下 Enter 鍵之前的語句集)。這與 PCT 的程式碼生成有關,將在以後修復。目前,請記住它不起作用。
在本集的剩餘部分,我們將實現語法的基本部分,例如基本資料型別和賦值。在本集結束時,你將能夠將簡單值賦給(全域性)變數。這不算多,但這是非常重要的一步。一旦這些基礎到位,你就會注意到新增特定的語法結構只需幾分鐘。
首先,開啟你的編輯器並開啟檔案 src/Squaak/Grammar.pm 和 src/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