跳轉到內容

鸚鵡虛擬機器/鸚鵡中間表示

來自華夏公益教科書

鸚鵡中間表示

[編輯 | 編輯原始碼]

鸚鵡中間表示 (PIR) 在很多方面類似於C 程式語言:它比組合語言更高階,但仍然非常接近底層機器。使用 PIR 的好處是它比 PASM 更容易程式設計,但同時它也暴露了 Parrot 的所有底層功能。

PIR 在 Parrot 世界中具有雙重目的。首先,它被用作從高階語言自動生成的程式碼的目標。高階語言的編譯器會發出 PIR 程式碼,然後可以解釋和執行。第二個目的是作為一種低階的可讀程式語言,用它可以編寫基本元件和 Parrot 庫。在實踐中,PASM 僅作為 Parrot 位元組碼的可讀直接翻譯存在,很少被人類直接用於程式設計。PIR 幾乎完全用於為 Parrot 編寫低階軟體。

PIR 語法

[編輯 | 編輯原始碼]

PIR 語法在很多方面類似於 C 或 BASIC 等舊的程式語言。除了類似 PASM 的操作之外,還有一些控制結構和算術運算,這些運算簡化了人類讀者的語法。所有 PASM 都是合法的 PIR 程式碼,PIR 幾乎只是在原始 PASM 指令上覆蓋了一層精美的語法。如果可以,你應該始終使用 PIR 的語法而不是 PASM 的語法,以方便使用。

雖然 PIR 比 PASM 具有更多功能和更好的語法,但它本身並不是高階語言。PIR 仍然非常低階,並不真正適合用於構建大型系統。Parrot 上為語言和應用程式設計者提供了許多其他工具,PIR 實際上只需要在少數領域使用。最終,可能會建立足夠多的工具,以至於不再需要直接使用 PIR。

PIR 和高階語言

[編輯 | 編輯原始碼]

PIR 旨在幫助實現 Perl、TCL、Python、Ruby 和 PHP 等高階語言。正如我們之前討論過的,高階語言 (HLL) 與 PIR 有兩種可能的聯絡方式

  1. 我們使用 NQP 和 Parrot 編譯器工具 (PCT) 為 HLL 編寫編譯器。然後將此編譯器轉換為 PIR,然後轉換為 Parrot 位元組碼。
  2. 我們在 HLL 中編寫程式碼並進行編譯。編譯器會將程式碼轉換為名為 PAST 的樹狀中間表示,再轉換為名為 POST 的另一種表示,最後轉換為 PIR 程式碼。從這裡,PIR 可以直接解釋,或者可以進一步編譯為 Parrot 位元組碼。

因此,PIR 具有有助於編寫編譯器的功能,它還具有支援使用這些編譯器編寫的 HLL 的功能。

與 Perl 類似,PIR 使用 "#" 符號作為註釋的開始。註釋從 # 執行到當前行的末尾。PIR 還允許在檔案中使用 POD 文件。我們稍後將詳細討論 POD。

子程式

[編輯 | 編輯原始碼]

子程式從 .sub 指令開始,以 .end 指令結束。我們可以使用 .return 指令從子程式中返回值。以下是一個不帶引數並返回 π 的近似值的函式的簡短示例

.sub 'GetPi'
   $N0 = 3.14159
   .return($N0)
.end

請注意,子程式名稱是用單引號括起來的。這不是必需的,但它非常有用,應該儘可能地這樣做。我們將在下面討論這樣做的原因。

子程式呼叫

[編輯 | 編輯原始碼]

有兩種方法可以呼叫子程式:直接間接。在直接呼叫中,我們按名稱呼叫特定的子程式

 $N1 = 'GetPi'()

但是,在間接呼叫中,我們使用包含該子程式名稱的字串來呼叫子程式

$S0 = 'GetPi'
$N1 = $S0()

當我們開始使用命名變數時(我們將在下面更詳細地討論),問題就出現了。考慮以下程式碼片段,其中我們有一個名為 "GetPi" 的區域性變數

GetPi = 'MyOtherFunction'
$N0 = GetPi()

在此程式碼片段中,我們呼叫 "GetPi" 函式(因為我們執行了 GetPi() 呼叫)還是呼叫 "MyOtherFunction" 函式(因為 GetPi 變數包含值 'MyOtherFunction')?簡短的回答是,我們將呼叫 "MyOtherFunction" 函式,因為區域性變數名稱在這類情況下優先於函式名稱。但是,這有點令人困惑,不是嗎?為了避免這種混亂,人們使用了一些標準來使它更容易

$N0 = GetPi()
僅用於間接呼叫
$N0 = 'GetPi'()
用於所有直接呼叫

透過堅持這種約定,我們避免了以後的所有可能混淆。

子程式引數

[編輯 | 編輯原始碼]

可以使用 .param 指令宣告子程式的引數。以下是一些示例

 .sub 'MySub'
   .param int myint
   .param string mystring
   .param num mynum
   .param pmc mypmc

在引數宣告中,.param 指令必須位於函式的頂部。你不能在 .sub.param 指令之間放置註釋或其他程式碼。以下是上面的相同示例

 .sub 'MySub'
   # These are my params:
   .param int myint
   .param string mystring
   .param num mynum
   .param pmc mypmc
錯誤!

命名引數

[編輯 | 編輯原始碼]

像上面一樣按嚴格順序傳遞的引數稱為位置引數。位置引數透過它們在函式呼叫中的位置來區分。將位置引數放在不同的順序會導致不同的效果,或者可能會導致錯誤。Parrot 支援第二種型別的引數,命名引數。引數不是按它們在字串中的位置傳遞,而是按名稱傳遞,可以按任意順序。以下是一個示例

.sub 'MySub'
   .param int yrs :named("age")
   .param string call :named("name")
   $S0 = "Hello " . call
   $S1 = "You are " . yrs
   $S1 = $S1 . " years old
   print $S0
   print $S1
.end

.sub main :main
   'MySub'("age" => 42, "name" => "Bob")
.end

在上面的示例中,我們也可以輕鬆地顛倒順序

.sub main :main
   'MySub'("name" => "Bob", "age" => 42)    # Same!
.end

命名引數非常有用,因為你無需擔心變數的確切順序,尤其是在引數列表變得很長時。

可選引數

[編輯 | 編輯原始碼]

函式可以宣告可選引數,呼叫者可以指定也可以不指定。為此,我們使用 `:optional` 和 `:opt_flag` 修飾符。

.sub 'Foo'
  .param int bar :optional
  .param int has_bar :opt_flag

在這個例子中,如果呼叫者提供了 `bar`,引數 `has_bar` 將被設定為 1,否則為 0。下面是一些示例程式碼,它們接收兩個數字並將其加在一起。如果未提供第二個引數,則第一個數字加倍。

.sub 'AddTogether'
  .param num x
  .param num y :optional
  .param int has_y :opt_flag

  if has_y goto ive_got_y
  y = x
ive_got_y:
  $N0 = x + y
  .return($N0)
.end

然後我們將使用以下方式呼叫此函式:

  'AddTogether'(1.0, 1.5)  #returns 2.5
  'AddTogether'(3.0)       #returns 6.0

貪婪引數

[編輯 | 編輯原始碼]

子例程可以接受任意數量的引數,這些引數可以載入到陣列中。可以接受可變數量的輸入引數的引數稱為 `:slurpy` 引數。貪婪引數將載入到陣列 PMC 中,您可以在函式內部迴圈遍歷它們,如果您願意的話。以下是一個簡短的示例。

.sub 'PrintList'
  .param list :slurpy
  print list
.end

.sub 'PrintOne'
  .param item
  print item
.end

.sub main :main
  PrintList(1, 2, 3) # Prints "1 2 3"
  PrintOne(1, 2, 3)  # Prints "1"
.end

貪婪引數吸收了所有函式引數的剩餘部分。因此,貪婪引數應該只作為函式的最後一個引數。任何在貪婪引數之後的引數都不會接收任何值,因為為它們傳遞的所有引數將被貪婪引數吸收。

扁平化引數陣列

[編輯 | 編輯原始碼]

如果您有一個包含函式資料的陣列 PMC,您可以傳入陣列 PMC。該陣列本身將成為單個引數,它將載入到函式中的單個數組 PMC 中。但是,如果您在使用陣列呼叫函式時使用 `:flat` 關鍵字,它將把陣列的每個元素傳遞到不同的引數中。以下是一個示例函式。

.sub 'ExampleFunction'
  .param pmc a
  .param pmc b
  .param pmc c
  .param pmc d :slurpy

我們有一個名為 `x` 的陣列,它包含三個整型 PMC:`[1, 2, 3]`。下面是兩個例子。

函式呼叫
'ExampleFunction'(x, 4, 5)
'ExampleFunction'(x :flat, 4, 5)
引數
  • a = `[1, 2, 3]`
  • b = 4
  • c = 5
  • d = `[]`
  • a = 1
  • b = 2
  • c = 3
  • d = `[4, 5]`

區域性變數

[編輯 | 編輯原始碼]

可以使用 `local` 指令定義區域性變數,使用類似於引數使用的語法。

   .local int myint
   .local string mystring
   .local num mynum
   .local pmc mypmc

除了區域性變數之外,您還可以使用 PIR 中的暫存器來儲存資料。

名稱空間

[編輯 | 編輯原始碼]

**名稱空間**是允許重用函式和變數名稱而不會與以前的化身發生衝突的構造。名稱空間還用於將類的所有方法放在一起,而不會與其他名稱空間中相同名稱的函式發生命名衝突。它們是在促進程式碼重用和減少命名汙染方面寶貴的工具。

在 PIR 中,名稱空間使用 `namespace` 指令指定。名稱空間可以使用鍵結構巢狀。

.namespace ["Foo"]
.namespace ["Foo";"Bar"]
.namespace ["Foo";"Bar";"Baz"]

根名稱空間可以使用一對空括號指定。

.namespace []  #Right! Enters the root namespace
.namespace     #WRONG! Brackets are required!

字串是 PIR 中的基本資料型別,非常靈活。字串可以指定為帶引號的文字或程式碼中的“Here文件”文字。

Here文件

[編輯 | 編輯原始碼]

Here文件字串文字已成為現代程式語言中常用的工具,用於指定非常長的多行字串文字。Perl 程式設計師會熟悉它們,但大多數 shell 程式設計師甚至現代 .NET 程式設計師也會熟悉它們。以下是 Here文件在 PIR 中的工作原理。

$S0 = << "TAG"
This is part of the Heredoc string. Everything between the
'<< "TAG"' is treated as a literal string constant. This string
ends when the parser finds the end marker.
TAG

Here文件允許輸入長多行字串,而無需使用大量凌亂的引號和連線操作。

編碼和字元集

[編輯 | 編輯原始碼]

可以指定帶引號的字串文字在特定字元集或編碼中進行編碼。

檔案包含

[編輯 | 編輯原始碼]

您可以使用 `include` 指令將外部 PIR 檔案包含到當前檔案中。例如,如果我們要將檔案“MyLibrary.pir”包含到當前檔案中,我們將編寫以下內容:

.include "MyLibrary.pir"

請注意,`include` 指令是一個原始文字替換函式。PIR 程式碼檔案不是像您從某些其他語言中期望的那樣自包含的。例如,新使用者相對常見的問題是名稱空間溢位概念。考慮兩個檔案 A.pir 和 B.pir。

A.pir B.pir
.namespace ["namespace 2"]
.namespace ["namespace 1"]

#here, we are in "namespace 1"

.include "A.pir"

#here we are in "namespace 2"

檔案 A 的 `namespace` 指令溢位到檔案 B 中,這對大多數程式設計師來說違反直覺。

類和方法

[編輯 | 編輯原始碼]

我們將在本書的後面花很多時間討論類和麵向物件程式設計。但是,由於我們已經稍微討論過名稱空間和子例程,因此我們可以為那些後面的討論奠定一些基礎。

PIR 中的類包含該類的名稱空間、初始化器、建構函式和一系列方法。“方法”與普通子例程完全相同,除了三個區別之外。

  1. 它具有 `method` 標誌。
  2. 它是使用“點表示法”呼叫的:`Object.Method()`
  3. 用於呼叫方法的物件(在點的左側)儲存在方法中的“self”變數中。

要建立類,我們首先需要為該類建立一個名稱空間。在最簡單的類中,我們建立方法。我們稍後會討論初始化器和建構函式,但現在我們將堅持使用既不使用這些函式的簡單類。

.namespace ["MathConstants"]

.sub 'GetPi' :method
  $N0 = 3.14159
  .return($N0)
.end

.sub 'GetE' :method
  $N0 = 2.71828
  .return($N0)
.end

使用此類(我們可能將其儲存在“MathConstants.pir”中幷包含到我們的主檔案中),我們可以編寫以下內容。

.local pmc mathconst
mathconst = new 'MathConstants'
$N0 = mathconst.'GetPi'()     #$N0 contains the value 3.14159
$N1 = mathconst.'GetE'()      #$N1 contains the value 2.71828

我們將在稍後解釋更多混亂的細節,但這足以幫助您入門。

控制語句

[編輯 | 編輯原始碼]

PIR 是一種低階語言,因此它不支援程式設計師可能習慣的任何高階控制結構。PIR 支援兩種型別的控制結構:條件分支和無條件分支。

**無條件分支**由 **goto** 指令處理。

**條件分支**也使用 goto 命令,但與 **if** 或 **unless** 語句一起使用。只有當 if 條件為真或 unless 條件為假時,才執行跳轉。

HLL 名稱空間

[編輯 | 編輯原始碼]

每個 HLL 編譯器都有一個名稱空間,該名稱空間與該 HLL 的名稱相同。例如,如果我們要為 Perl 編寫一個編譯器,我們將建立名稱空間 .namespace ["Perl"]。如果我們不編寫編譯器,而是用純 PIR 編寫程式,我們將位於預設名稱空間 .namespace ["Parrot"] 中。要建立新的 HLL 編譯器,我們將使用 .HLL 指令來建立當前預設的 HLL 名稱空間。

.HLL "mylanguage", "mylanguage_group"

HLL 名稱空間中的所有內容對用該 HLL 編寫的程式都是可見的。例如,如果我們有一個位於“PHP”名稱空間中的 PIR 函式“Foo”,那麼用 PHP 編寫的程式可以像呼叫常規 PHP 函式一樣呼叫 Foo 函式。這聽起來可能有點複雜。以下是一個簡短的示例

PIR 程式碼 Perl 6 程式碼
.namespace ["perl6"]

.sub 'AddTwo'
  .param int a
  .param int b
  $I0 = a + b
  .return($I0)
.end
$x = AddTwo(4 + 5);

為了簡化,我們可以簡單地編寫 .namespace(不帶括號)以返回當前的 HLL 名稱空間。

多方法

[編輯 | 編輯原始碼]

多方法是共享相同名稱的一組子例程。例如,子例程“Add”可能具有不同的行為,具體取決於傳遞給它的引數是 Perl 5 浮點數、Parrot BigNum PMC 還是 Lisp Ratio。多重分派子例程與 PIR 中的任何其他子例程的宣告方式相同,除了它們還具有 :multi 標誌。當呼叫 Multi 時,Parrot 會載入具有相同名稱的 MultiSub PMC 物件,並開始比較引數。與接受的引數列表最匹配的子例程將被呼叫。“最佳匹配”例程相對來說比較高階。Parrot 使用曼哈頓距離按子例程與給定列表的接近程度對其進行排序,然後呼叫列表頂部的子例程。

在排序時,Parrot 會考慮角色和多重繼承。這使得它非常強大和靈活。

多方法、MultiSubs 和其他關鍵字

[編輯 | 編輯原始碼]

本頁上的詞彙可能開始變得有點複雜。在這裡,我們將列出一些用於描述 Parrot 中事物的術語。

子例程
具有名稱和引數列表的基本程式碼塊。
方法
屬於特定類並且可以在該類的物件上呼叫的基本程式碼塊。方法只是具有額外的隱式 self 引數的子例程。
多重分派
當多個子例程具有相同的名稱時,Parrot 會選擇最適合呼叫的子例程。
單一分派
當只有一個具有給定名稱的子例程時,Parrot 不需要進行任何複雜的排序或選擇。
MultiSub
一種 PMC 型別,它儲存可以按名稱呼叫並由 Parrot 排序/搜尋的子例程集合。
多方法
與 MultiSub 相同,但它被呼叫為方法而不是子例程。

PIR 宏和常量

[編輯 | 編輯原始碼]

PIR 允許使用文字替換宏功能,其概念類似於(但實現不同於)C 的預處理器中使用的宏功能。PIR 沒有支援條件編譯的預處理器指令。

宏常量

[編輯 | 編輯原始碼]

可以使用 .macro_const 關鍵字定義常數值。以下是一個示例

.macro_const PI 3.14

.sub main :main
  print .PI      #Prints "3.14"
.end

.macro_const 可以是整數常量、浮點數常量、字串文字或暫存器名稱。以下還有另一個示例

.macro_const MyReg S0
.macro_const HelloMessage "hello world!"

.sub main :main
  .MyReg = .HelloMessage
  print .MyReg
.end

這允許您為常見常量、字串或暫存器命名。

可以使用 .macro.endm 關鍵字建立基本的文字替換宏,分別標記宏的開始和結束。以下是一個快速示例

.macro SayHello
  print "Hello!"
.endm

.sub main :main
  .SayHello
  .SayHello
  .SayHello
.end

這個示例,正如應該顯而易見的那樣,列印了三遍“Hello!”。我們也可以為我們的宏提供引數,以便將它們包含在文字替換中

.macro CircleCircumference(r)
  $N0 = r * 3.1.4
  $N0 = $N0 * 2
  print $N0
.endm

.sub main :main
  .CircleCircumference(5)
  .CircleCircumference(10)
.end

宏區域性變數

[編輯 | 編輯原始碼]

如果我們想在宏中定義一個臨時變數怎麼辦?以下是一個想法

.macro PrintSomething
  .local string something
  something = "This is a message"
  print something
.endm

.sub main :main
  .PrintSomething
  .PrintSomething
.end

進行文字替換後,我們將得到以下結果

.sub main :main 
  .local string something
  something = "This is a message"
  print something
  .local string something
  something = "This is a message"
  print something
.end

在替換後,我們聲明瞭變數 something 兩次!與其這樣,我們可以使用 .macro_local 宣告來建立一個對宏本地的具有唯一名稱的變數

.macro PrintSomething
  .macro_local something
  something = "This is a message"
  print something

現在,相同的功能在進行文字替換後會轉換為以下內容

.sub main :main 
  .local string main_PrintSomething_something_1
  main_PrintSomething_something_1 = "This is a message"
  print main_PrintSomething_something_1
  .local string main_PrintSomething_something_2
  main_PrintSomething_something_2 = "This is a message"
  print main_PrintSomething_something_2
.end

請注意,區域性變數宣告現在是如何唯一的?它們依賴於引數的名稱、宏的名稱以及檔案中的其他資訊?這是一個可重用的方法,不會導致任何問題。


上一個 鸚鵡虛擬機器 下一個
Parrot_Assembly_Language Parrot_Magic_Cookies
華夏公益教科書