鸚鵡虛擬機器/運算子和表示式
自上而下的解析有一些問題。首先,它很慢,因為它可能需要嘗試許多替代方案才能找到匹配項。每次在匹配上失敗時,它都需要返回到前面的規則並再次嘗試。另一個問題是,對於具有許多小令牌、運算子優先順序和許多子規則的語句,解析樹可能會變得非常大。考慮一個簡單的數學表示式“1 * 2 + 5”和以下語法
EXPRESSION := TERM ADD_OP TERM TERM := (NUMBER MUL_OP NUMBER) | NUMBER ADD_OP := '+' | '-' MUL_OP := '*' | '/'
我們上面的等式建立了一個非常複雜的解析樹
EXPRESSION
TERM
NUMBER 2
MUL_OP "*"
NUMBER 5
ADD_OP "+"
TERM
NUMBER 5
對於像數學表示式這樣的東西,我們希望使用自下而上的解析器而不是自上而下的解析器。一種特殊的自下而上的解析器型別,稱為 **運算子優先順序表解析器**,在這些任務中特別有效且適合。為了建立運算子優先順序表,PGE 有一個名為 optable 的特殊工具。
透過建立一個 is optable 的規則來建立一個運算子。這是一個例子
rule expression is optable { ... }
... 不是省略,而是規則的一部分。如果沒有 ... 三個點,你的規則將無法工作。這被稱為 _yada yada yada_ 運算子,用於定義函式原型。PGE 將使用您指定的選項為您例項化該函式。關鍵字 is optable 告訴解析器轉換為自下而上的模式以解析表示式
在運算子中,使用 proto 關鍵字而不是 rule 或 token 關鍵字來指定令牌。proto 定義必須定義幾件事:運算子的名稱和型別、運算子的優先順序以及用於執行特定操作的 PIR 程式碼。以下是一些示例
proto 'infix:+' is precidence('1') is pirop('add') { ... }
proto 'prefix:-' is tighter('infix:+') is pirop('neg') { ... }
proto 'postfix:++' is equal('prefix:-') { ... }
這些示例展示瞭如何解析三個常見運算子:兩個值之間的“+”,負數的負值(例如“-123”)和字尾自增運算子(“x++”)。這裡有很多重要的關鍵字,這甚至不是你製作完整表示式解析器所需的所有關鍵字。
運算子有四種類型:**字首**、**字尾**、**中綴**和 **環繞**。以下是一些示例
| 型別 | 示例 | 用途 |
|---|---|---|
| 字首 | prefix:-
|
-123 |
| 字尾 | postfix:*
|
x* |
| 中綴 | infix:+
|
x+y |
| 環繞 | circumfix:()
|
(x) |
| 字尾環繞 | postcircumfix:[]
|
x[y] |
| 三元 | ternary:?
|
x?y:z |
如前所述,自下而上的解析器會遇到一個稱為移進-歸約衝突的特定問題。移進-歸約衝突發生在解析器無法確定是移進新的輸入令牌還是歸約它已有的令牌時。以下是從 C 程式語言中的一個示例
-x + 2 * y
我們知道,因為我們從小就被教導了運算子優先順序,所以表示式是這樣分組的
(-x) + (2 * y)
但是,計算機除非我們告訴它運算子遵循的優先順序,否則不知道這一點。例如,在不知道運算子作用的順序的情況下,解析器可能會按照先到先得的方式對錶達式進行分組,如下所示
((-x) + 2) * y
或者它可能會移進所有令牌並從右側開始歸約
-(x + (2 * y)
關鍵是,我們需要建立規則來告訴運算子優先順序解析器如何解析這些衝突。以下是可以指定的優先順序規則
| 程式碼 | 效果 |
|---|---|
is precidence('1')
|
設定基本優先順序。引號中的數字是任意的。重要的是這是一個常量,其他運算子可以相對於此定義。 |
is equiv('OPERATOR')
|
這裡,單詞 OPERATOR 將被替換為現有的運算子,例如 is operator('infix:+') 這表明一個運算子與另一個運算子具有相同的優先順序。所有具有相同優先順序的運算子都是從左到右計算的。 |
is tighter('OPERATOR')
|
該運算子比 OPERATOR 繫結更緊密。在表示式 -x + y 中,我們期望 prefix:- 比 infix:+ 繫結更緊密。 |
is looser('OPERATOR')
|
與 is tighter() 類似,但相反。表明該運算子比 OPERATOR 繫結得更鬆散 |
我們在示例中沒有顯示,但也可以確定運算子是從左到右還是從右到左起作用。我們使用 is assoc 關鍵字定義這一點。下表總結了這些規則
| 結合性 | 解釋 |
|---|---|
is assoc('right')
|
|
is assoc('left')
|
|
is assoc('list')
|
|
is assoc('chain')
|
|
is assoc('none')
|
有三種方法可以指定運算子的行為。第一種是將運算子與 PIR 操作碼關聯。第二種是將運算子與 PAST 節點型別關聯。最後,指定運算子動作的第三種方法是建立一個函式。前兩種方法分別可以使用 is pirop() 和 is pasttype() 說明符執行。
對於簡單的操作,建立和呼叫函式沒有意義。對於可以使用 Parrot 已經定義的 PASM 操作碼非常快地執行的簡單算術運算,尤其如此。使用程式碼 is pirop('opname') 將運算子實現為單個操作碼。
一些運算子,例如邏輯運算子,可以使用 PAST 已經提供的節點型別來最好地實現。使用 is pasttype('TYPE') 形式來指定要使用的 PAST 節點型別。
預設情況下,除非您將運算子指定為 is pirop 或 is pasttype,否則運算子將預設為函式。因此,大多數運算子預設為函式,這對於打算允許運算子過載的語言來說非常有用。
在這本書中,我們將偶爾使用術語“Proto”、“Proto Regex”或“Protoregex”,它在 Perl 6 文件中也經常使用。Proto類似於規則,但它們提供了類似於多重分派的功能:你可以擁有多個相同名稱的規則,這些規則取決於傳遞給它的運算元。透過將運算子宣告為 proto,你為它命名,並且還允許將來對語言進行新增。對於使用運算子過載的語言來說,這是一個重要的功能。
Protos 使用 proto 關鍵字定義。它們可以在自上而下或自下而上的解析器中使用,但由於它們最常作為運算子過載的支援機制,因此它們最常在自下而上的運算子優先順序解析器中使用。要定義一個新的 proto,請使用以下形式
proto NAME OPTIONS { ... }
在這個宣告中,{ ... } 是一段程式碼:你實際上必須在花括號之間鍵入三個點(省略號)。這樣做是為了提醒解析器這不是規則本身的實現,而實際上只是一個簽名或其他函式的前向宣告。例如,我們可以定義一個 proto MyProto
proto MyProto { ... }
在另一個檔案中,我們可以定義函式來實現它
.sub MyProto # Insert the parameter list and function logic here .sub
.sub MyProto # Another MyProto! # Insert a different parameter list and different function logic here .end
術語是運算子之間的值。在以下表達式中
1 + 2 * 3
數字 1、2 和 3 都是術語。術語使用普通規則解析,並使用以下語法載入到運算子優先順序表中
proto 'term:' is precedence('1') is parsed(&term) { ... }
當然,你可以根據需要設定優先順序,但通常最好使術語儘可能緊湊地解析。is parsed() 屬性指定術語應該使用名為“term”的規則進行解析。你可以使用普通的規則語法來定義這個術語。& 符號是 NQP 用於引用子例程的符號。在本例中,所討論的子例程是用於解析術語的語法方法(但它可以想象成任何其他東西)。在下一章中,我們將更多地瞭解 NQP 和符號。
術語是解析器從自下而上解析切換回自上而下解析的方式。
建立解析器後,我們需要建立與它們關聯的方法來構建解析樹所需的 PAST 節點。幸運的是,這些方法很容易建立,通常最好的選擇是使用其他語言實現中的基本模板。
術語的定義與任何其他規則一樣,不需要任何特殊語法。但是,表示式需要一些特殊的注意。