鸚鵡虛擬機器/不完全Perl
鸚鵡上 NQP 編譯器的原始碼可以在 Parrot 儲存庫的
compilers/nqp 目錄中找到。不完全Perl (NQP) 是 Perl 6 語言的一個子集的實現,最初是為了幫助引導 Perl 6 的實現而設計的。換句話說,Perl 6 開發人員正在使用 Perl 6 語言本身的一個子集來編寫 Perl 6 編譯器。這個引導是透過首先使用 PIR 編寫一個小的 NQP 編譯器來完成的。NQP 編譯器完成後,就可以用 NQP 編寫程式,而無需完全用 PIR 編寫。
然而,NQP 不僅僅是為 Perl 6 預留的工具。其他語言也使用 NQP 作為輕量級實現語言。NQP 的一個主要優點是它不依賴於任何可能會隨著時間推移而發生變化的外部程式碼庫。但是,由於其佔用的空間很小,NQP 往往缺乏高階程式語言的許多功能,並且在最初學習程式設計時,不使用一些常見的構造可能會很困難。
本節需要事先了解 Perl 6 程式設計。 |
在這裡,我們將討論 NQP 程式設計的一些基礎知識。有經驗的 Perl 程式設計師,即使是熟悉 Perl 5 但不一定熟悉 Perl 6 的程式設計師,也會發現其中大部分內容都是簡單的複習。
NQP **不是 perl5 或 perl 6**。這一點再怎麼強調也不為過。NQP 中缺少了許多 Perl 的功能。有時,這意味著你需要以硬編碼的方式完成一些任務。在 NQP 中,我們使用 := 運算子,稱為 **繫結運算子**。與正常的變數賦值不同,繫結不會將值從一個“容器”複製到另一個。相反,它在兩個變數之間建立了一個連結,並且從那時起,它們就成為同一個容器的別名。這類似於在 C 中複製指標的方式不會複製所指向的資料。
NQP 中的變數通常具有三種基本型別之一:標量、陣列和雜湊表。標量是單個值,例如整數、浮點數或字串。陣列是標量的列表,透過整數索引訪問。雜湊表是使用字串(稱為鍵)作為索引的標量列表。所有變數名都在前面都有一個 **符號**。符號是一個標點符號,例如 "$"、"@" 或 "%",它表示變數的型別。
標量變數具有 "$" 符號。以下是標量值的示例
$x := 5; $mystring := "string"; $pi := 3.1415;
陣列使用 "@" 符號。我們可以像這樣使用陣列
@myarray[1] := 5; @b[2] := @a[3];
請注意,NQP 沒有像 Perl6 那樣擁有 列表上下文。這意味著你無法進行列表賦值,例如
@b := (1, 2, 3); # WRONG! $b := (1, 2, 3); # CORRECT
NQP 被設計為精簡,儘可能少地支援 Perl6 的開發。上面的行也可以寫成
@b[0] := 1; @b[1] := 2; @b[2] := 3;
我們將在頁面下面更詳細地討論這一點。雜湊表以 "%" 符號為字首
%myhash{'mykey'} := 7
%mathconstants{'pi'} := 3.1415;
%mathconstants{'2pi'} := 2 * %mathconstants{'pi'};
對於不熟悉 Perl 的人來說,雜湊表也被稱為字典(在 Python 中)或關聯陣列。基本上,它們類似於陣列,但使用字串索引而不是整數索引。
正如我們之前提到的,NQP 中沒有“陣列上下文”這樣的東西,Perl 5 程式設計師可能已經期待過。Perl 語言的一個重要功能是它能夠感知上下文,並且它會根據你是在標量上下文還是陣列上下文中以不同的方式處理事物。如果沒有它,它真的就不是 perl。這就是為什麼他們稱之為 NQP,因為它類似於 perl,但不是 完全的 perl。在 NQP 中,你無法編寫以下任何一個
@a := (1, 2, 3); |
錯誤! |
%b := ("a" => "b", "c" => "d");
|
錯誤! |
所有變數(雜湊表、標量和陣列)都可以使用關鍵字“my”宣告為詞法變數,或使用關鍵字“our”宣告為全域性變數。對於已經閱讀過 PIR 部分的讀者來說,“my”變數對應於 .lex 指令,以及 store_lex 和 find_lex 指令。“our”變數對應於 set_global 和 find_global 指令。以下是一個示例
| 這段 NQP 程式碼 | 翻譯成(大致)以下 PIR 程式碼 |
|---|---|
my $x; my @y; my %z; |
set_lex "$x", "" $P1 = new 'ResizablePMCArray' set_lex "@y", $P1 $P2 = new 'Hash' set_lex "%z", $P2 |
同樣,對於“our”
| 這段 NQP 程式碼 | 翻譯成(大致)以下 PIR 程式碼 |
|---|---|
our $x; our @y; our %z; |
set_global "$x", "" $P1 = new 'ResizablePMCArray' set_global "@y", $P1 $P2 = new 'Hash' set_global "%z", $P2 |
NQP 擁有 PIR 中缺少的所有高階控制結構。我們以 PIR 所沒有的方式擁有迴圈和 If/Then/Else 分支。由於這是一種類似 Perl 的語言,NQP 所擁有的迴圈是多種多樣的,並且相對來說是高階的。
在分支方面,我們有
- If/Then/Else
if ($key eq 'foo') {
THEN DO SOME FOO STUFF
}
elsif ($key eq 'bar') {
THEN DO THE BAR-RELATED STUFF
}
else {
OTHERWISE DO THIS
}
- Unless/Then/Else
- For
- "For" 迴圈遍歷列表並將
$_設定為當前索引,就像在 perl5 中一樣。NQP 中沒有帶有起始點和步長操作的 c 風格迴圈,儘管 Perl 5 和 Perl 6 中都有類似的構造。以下是一個基本的 for 迴圈
for (1,2,3) {
Do something with $_
}
- 精確地翻譯成以下 PIR 程式碼
.sub 'for_statement'
.param pmc match
.local pmc block, past
$P0 = match['EXPR']
$P0 = $P0.'item'()
$P1 = match['block']
block = $P1.'item'()
block.'blocktype'('sub')
.local pmc params, topic_var
params = block[0]
$P3 = get_hll_global ['PAST'], 'Var'
topic_var = $P3.'new'('name'=>'$_', 'scope'=>'parameter')
params.'push'(topic_var)
block.'symbol'('$_', 'scope'=>'lexical')
$P2 = get_hll_global ['PAST'], 'Op'
$S1 = match['sym']
past = $P2.'new'($P0, block, 'pasttype'=>$S1, 'node'=>match)
match.'result_object'(past)
.end
你還可以像這樣遍歷雜湊表的鍵
for (keys %your_hash) {
DO SOMETHING WITH %your_hash{$_}
}
其中 keys %your_hash 建立了 %your_hash 中所有鍵的列表,並遍歷此列表,將 $_ 設定為儲存當前鍵。
- While
- "While" 迴圈類似於 for 迴圈。在 NQP 中,while 迴圈如下所示
while(EXIT_CONDITION) {
LOOP_CONTENTS
}
- 在大致上變成以下 PIR 程式碼
loop_top: if(!EXIT_CONDITION) goto loop_end LOOP_CONTENTS goto loop_top loop_end:
- Do/While
- "do/while" 迴圈類似於 while 迴圈,除了條件在迴圈結束時測試,而不是在開始時測試。這意味著迴圈至少執行一次,並且如果條件不滿足,可能會執行更多次。在 NQP 中
do {
LOOP_CONTENTS
} while(EXIT_CONDITION);
- 在 PIR 中
loop_top: LOOP_CONTENTS if(!EXIT_CONDITION) goto loop_end goto loop_top loop_end:
NQP 支援一小部分用於操作變數的運算子。
| 運算子 | 用途 |
|---|---|
+, - |
標量加法和減法 |
*, / |
標量乘法和除法 |
% |
整數模 |
$( ... ) |
將引數轉換為標量 |
@( ... ) |
將引數視為陣列 |
%( ... ) |
將引數視為雜湊表 |
~ |
字串連線 |
eq |
字串相等比較 |
ne |
字串不相等比較 |
:= |
繫結 |
>, <, >=, <=, ==, != |
相等和不相等運算子 |
當語法規則匹配並且執行 {*} 規則時,會生成一個名為 **匹配物件** 的特殊型別的雜湊表物件,並傳遞給關聯的 NQP 方法。此匹配物件被賦予了特殊的名稱 $/。你可以給它取一個不同的名字,但你會失去使 $/ 變數如此特殊的許多功能。
通常,當你在雜湊表中引用物件時,你會使用 { } 花括號。例如
my %hash;
%hash{'key'} = "value";
當您要從雜湊引用中呼叫值時,您需要執行更復雜的操作。
$hashref->{'key'} = "value";
在 NQP(以及 Perl 6)中,尖括號會神奇地“自動引用”它們內部的內容。因此,您可以寫
<field>來代替{'field'}或<'field'>。但是,使用特殊的預設匹配物件,您可以使用< > 尖括號。因此,無需編寫
$/->{'key'}
我們可以編寫更簡潔的程式碼
$<key>
雜湊物件的鍵對應於語法中使用的子規則的名稱。因此,如果我們有語法規則
rule my_rule {
<first> <second> <third> <andmore>
}
我們的匹配物件將具有以下欄位
$<first> $<second> $<third> $<andmore>
如果我們對任何一個欄位有多個值,例如
rule my_rule {
<first> <second> <first> <second>
}
現在,$<first>和$<second>都是兩個元素的陣列。此外,我們可以將此行為擴充套件到語法中的重複運算子
rule my_rule {
<first>+ <second>*
}
現在,$<first>和$<second>都是陣列,它們的長度指示每個匹配了多少個專案。您可以使用 + 運算子或scalar()函式來獲取匹配的專案數量。
我們想要建立一個簡單的解析器,檢測“Hello”或“Goodbye”這兩個詞語。如果輸入了這兩個詞語中的任何一個,我們想要打印出成功訊息和詞語。如果兩個詞語都沒有輸入,我們列印錯誤資訊。為了從輸入中挑選出詞語,我們將使用內建的子規則<ident>。
rule TOP {
<ident>
$
{*}
}
在這個語法規則中,我們尋找一個單個識別符號(對於我們的目的,它將是一個詞語),後面跟著檔案結尾。一旦我們有了這些,我們就建立匹配物件並呼叫我們的 Action 方法
method TOP($/) {
if($<ident> eq "Hello") {
say("success! we found Hello");
}
elsif($<ident> eq "Goodbye") {
say("success! we found Goodbye");
}
else {
say("failure, we found: " ~ $<ident>);
}
make PAST::Stmts.new();
}
由於 HLLCompiler 類期望我們的 Action 方法返回一個 PAST 節點,我們必須建立一個空的 stmts 節點並返回它。當我們在輸入上執行這個解析器時,它將有三種可能的結果
- 我們收到了一個“Hello”或“Goodbye”,系統將打印出成功方法。
- 我們收到了不同的詞語,我們將收到錯誤訊息。
- 我們收到了太多詞語,太少詞語,或者不是詞語的東西。這將導致解析錯誤。
試試看!
這是一個簡單的例子,展示瞭如何建立一個程式將八進位制數轉換為二進位制數。我們從mk_language_shell.pl中的基本語言 shell 開始
語法檔案
grammar Oct2Bin::Grammar is PCT::Grammar;
rule TOP {
<octdigit>+
[ $ || <panic: Syntax error> ]
{*}
}
token octdigit {'0'|'1'|'2'|'3'|'4'|'5'|'6'|'7'}
動作檔案
class Oct2Bin::Grammar::Actions;
method TOP($/) {
my @table;
@table[0] := '000';
@table[1] := '001';
@table[2] := '010';
@table[3] := '011';
@table[4] := '100';
@table[5] := '101';
@table[6] := '110';
@table[7] := '111';
my $string := "";
for $<octdigit> {
$string := $string ~ @table[$_];
}
say( $string );
make PAST::Stmts.new( );
}
注意,在我們的動作檔案中,我們不得不一次例項化一個查詢表中的元素?這是因為 NQP 對陣列沒有完全的理解。還要注意,我們讓 TOP 方法返回一個空的 PAST::Stmts 節點,以抑制 PCT 關於沒有 PAST 節點的警告。
NQP 不是編寫伴隨語法的動作方法的唯一方法。它由於很多原因而是一個有吸引力的工具,但它不是唯一的選擇。動作方法也可以用 PIR 或 PASM 編寫。這就是 NQP 編譯器本身的實現方式。這是一個關於 PIR 動作可能是什麼樣子的例子
.sub 'block' :method
.param pmc match
.param string key
.local pmc past
$P0 = get_hll_global ['PAST'], 'Stmts'
past = $P0.'new'('node' => match)
...
match.'result_object'(past) # make $past;
.end