鸚鵡虛擬機器/Squaak 教程/窺探編譯器內部
第 1 集: 介紹
第 2 集: 窺探編譯器內部
第 3 集: Squaak 細節和第一步
第 4 集: PAST 節點和更多語句
第 5 集: 變數宣告和作用域
第 6 集: 作用域和子程式
第 7 集: 運算子和優先順序
第 8 集: 雜湊表和陣列
第 9 集: 總結和結論
在第一集中,我們介紹了 Parrot 編譯器工具,並使用 Parrot 發行版提供的 shell 指令碼生成了一種非常簡單的語言。我們還宣佈了 Squaak,一種專門為本教程開發的簡單程式語言。Squaak 將作為案例研究,展示 PCT 如何作為一組非常有效的工具來實現 Parrot 的語言。Squaak 的特性列表已經列出。如果你感覺幸運,你甚至可以嘗試完成上一集末尾的練習。
在本集中,我們將更深入地瞭解生成的編譯器。我們將檢視編譯過程的不同階段,並展示基於 PCT 的編譯器中正在發生的事情。
還記得我們之前是如何呼叫編譯器的嗎?我們可以傳遞一個檔案,或者在不使用命令列引數的情況下呼叫編譯器,在這種情況下,編譯器將進入互動模式。考慮第一種情況,傳遞檔案 test.sq,就像我們之前做的那樣
$ parrot squaak.pbc test.sq
當像這樣呼叫編譯器時,檔案 test.sq 會被編譯,生成的程式碼(位元組碼)會立即被 Parrot 執行。你可能會好奇,這是如何工作的呢?指令碼的解釋是透過一系列轉換完成的,從指令碼源開始,最終以 Parrot 可執行的格式結束。使用 PCT(基於 HLL::Compiler 類)構建的編譯器可以接受一個 target 選項,以顯示其中一箇中間表示。此選項可以具有以下值,對應於 HLLCompiler 物件的四個預設編譯階段
- --target=parse
- --target=past
- --target=post
- --target=pir
這是一個使用 target 選項設定為 “parse” 的示例,它將列印輸入的解析樹到標準輸出
$ parrot squaak.pbc --target=parse test.sq
在互動模式下,輸入此內容
say 42;
將列印此解析樹(不帶行號)
1 "parse" => PMC 'Regex;Match' => "say 42;\n" @ 0 {
2 <statementlist> => PMC 'Regex;Match' => "say 42;\n" @ 0 {
3 <statement> => ResizablePMCArray (size:1) [
4 PMC 'Regex;Match' => "say 42" @ 0 {
5 <statement_control> => PMC 'Regex;Match' => "say 42" @ 0 {
6 <EXPR> => ResizablePMCArray (size:1) [
7 PMC 'Regex;Match' => "42" @ 4 {
8 <integer> => PMC 'Regex;Match' => "42" @ 4 {
9 <decint> => PMC 'Regex;Match' => "42" @ 4
10 <VALUE> => \parse[0][0]
11 }
12 }
13 ]
14 <sym> => PMC 'Regex;Match' => "say" @ 0
15 }
16 }
17 ]
18 }
19 }
當更改 target 選項的值時,輸出會變成輸入的不同表示。現在何不嘗試一下?
因此,一個 HLL::Compiler 物件有四個編譯階段:解析、構建鸚鵡抽象語法樹 (PAST)、構建鸚鵡操作碼語法樹 (POST)、生成鸚鵡中間表示 (PIR)。編譯完成後,生成的 PIR 會立即執行。
如果你的編譯器需要額外的階段,你可以將它們新增到你的 HLL::Compiler 物件中。對於 Squaak,我們不需要這樣做,但有關詳細資訊,請檢視 compilers/pct/src/PCT/HLLCompiler.pir。
我們現在將更詳細地討論每個編譯階段。前兩個階段,解析輸入和構建 PAST 是同時執行的。因此,我們將一起討論它們。
在解析階段,使用 Perl 6 的擴充套件正則表示式(稱為規則,有關詳細資訊,請參閱 概要 5)分析輸入。當一個規則匹配某個輸入字串時,就會建立一個所謂的匹配物件。匹配物件是一個組合的陣列和雜湊表,這意味著它可以透過整數和字串進行索引。由於規則通常由其他(子)規則組成,因此很容易檢索匹配的某個部分。例如,此規則
rule if_statement {
'if' <expression> 'then' <statement> 'end'
{*}
}
有兩個子規則:expression 和 statement。if_statement 規則的匹配物件表示從 if 到 end 的整個字串。當您只對 expression 或 statement 部分感興趣時,您可以透過子規則的名稱(在本例中分別為 expression 和 statement)索引匹配物件來檢索它。
在解析階段,PAST 會被構建。有一組小的 PAST 節點型別,例如,PAST::Var 用於表示變數(識別符號,例如 print),PAST::Val 用於表示文字值(例如,“hello” 和 42),等等。稍後我們將更詳細地討論各種 PAST 節點。
現在,你可能想知道,PAST 的構建究竟是在哪個點發生的?這就是上面顯示的 if_statement 規則中“if”字串下面的特殊符號 {*} 的作用。這些特殊標記指示應該呼叫解析操作。這樣的解析操作只是一個方法,它具有與它所在的規則相同的名稱(在本例中為:if_statement)。因此,在解析階段,會執行多個解析操作,每個操作都構建一個表示輸入字串的總 PAST 的一部分。稍後將對此進行更多解釋。
鸚鵡抽象語法樹只是同一個輸入字串(被編譯的程式)的不同表示。它是一個方便的資料結構,可以將其轉換為不同的東西(例如可執行的 Parrot 程式碼),但也用於進行各種分析,例如編譯時型別檢查。
在構建 PAST 的解析階段之後,HLL::Compiler 將此 PAST 轉換為稱為 鸚鵡操作碼語法樹 (POST) 的東西。POST 表示也是一個樹結構,但這些節點處於更低的抽象級別。例如,在 PAST 級別上,有一個節點型別用於表示 while 語句(構建為 PAST::Op.new( :pasttype('while') ))。while 語句的模板通常包含多個標籤和跳轉指令。在 POST 級別上,相同的 while 語句由一組節點表示,每個節點表示一個指令或一個標籤。因此,將 POST 轉換為可執行的東西比從 PAST 級別進行轉換要容易得多。
通常,作為 PCT 的使用者,您不需要了解 POST 節點的詳細資訊,這就是我們不會詳細討論它的原因。使用 target 選項檢視 POST 的樣子。
在第四(也是最後)階段,POST 被轉換為鸚鵡中間表示 (PIR)。如前所述,將 POST 轉換為可執行的東西相當簡單,因為 POST 節點已經表示單獨的指令和標籤。同樣,正常使用 PCT 不需要您瞭解此轉換的任何細節。
我們已經建立了基於 PCT 的編譯器的通用資料流,它包含四個階段:
- 原始碼到解析樹
- 解析樹到 PAST
- PAST 到 POST
- POST 到 PIR
我們注意到前兩個階段在解析階段完成。現在,當您閱讀本教程時,您可能對使用 PCT 為 Parrot 實現您喜歡的語言感興趣。我們已經看到語言語法是用 Perl 6 規則表達的。那麼其他轉換呢?
嗯,在本節的前面,我們提到了“解析操作”這個術語,這些操作建立 PAST 節點。在您為每個語法規則編寫了解析操作後,您就完成了!
沒錯。一旦您正確構建了 PAST,您的編譯器就可以生成可執行的 PIR 程式碼,這意味著您剛剛為 Parrot 實現了自己的第一種語言。當然,您仍然需要實現任何特定於語言的庫,但這無關緊要。
基於 PCT 的編譯器已經知道如何將 PAST 轉換為 POST,以及如何將 POST 轉換為 PIR。這些轉換階段已由 PCT 提供。
在本節中,我們更深入地瞭解了基於 PCT 的編譯器的內部機制。我們討論了四個編譯階段,它們將輸入字串(根據您的定義,可能是程式或指令碼)轉換為 PAST、POST,最後轉換為可執行的 PIR 程式碼。接下來的部分是“有趣的部分”:我們將為 Parrot 實現 Squaak。我們將逐步實現解析器和解析操作。最後,我們將演示 John Conway 的“生命遊戲”在 Parrot 上執行,該遊戲是用 Squaak 實現的。
從下一節開始,練習會更有趣。現在,瀏覽編譯器的原始檔,看看您是否理解 Grammar.pg 中的語法規則與 Actions.pm 中的方法之間的關係會很有幫助。
實驗本節中描述的 --target 選項也很有用。如果您不瞭解 PIR,現在是做一些準備的時候了。關於 PIR 的資訊很多,請參閱參考資料部分以獲取詳細資訊。同時,如果您有任何建議、問題等,請隨時留言。
- PIR 語言規範:docs/pdds/pdd19_pir.pod
- PIR 書籍:docs/book/pir