實現一個簡單的直譯器(4)
阿新 • • 發佈:2020-03-02
譯自:https://ruslanspivak.com/lsbasi-part4/
(已獲得作者授權,個別語句翻譯的不到位,我會將原句跟在後邊作為參考)
你是在被動地學習這些文章中的材料還是在積極地實踐它?希望你一直在積極練習。
**孔子**曾經說過:
> “聞之我也野。”
![](https://img2020.cnblogs.com/blog/1133903/202003/1133903-20200302181338376-1209094891.png)
> “視之我也饒。”
![](https://img2020.cnblogs.com/blog/1133903/202003/1133903-20200302181349369-1598863245.png)
> “行之我也明。”
![](https://img2020.cnblogs.com/blog/1133903/202003/1133903-20200302181856311-845792661.png)
在上一篇文章中,我們學習瞭如何解析(識別)和解釋具有任意數量的加或減運算的算術表示式,例如"7 - 3 + 2 - 1",還了解了語法圖以及如何用它來表示(specify)程式語言的語法。
今天,你將學習如何解析和解釋具有任意數量的乘法和除法運算的算術表示式,例如"7 * 4 / 2 * 3"。本文中的除法將是整數除法,因此,如果表示式為"9 / 4",則答案將是整數:2。
今天,我還將談論廣泛用於表示某一程式語言語法的標記方法,稱為**上下文無關文法**或BNF(簡稱文法)(Backus-Naur形式)。在本文中,我將不使用純BNF表示法,而使用修改後的EBNF表示法。
我們為什麼要使用文法,這是幾個原因:
1、文法以簡潔的方式指定程式語言的語法,與語法圖不同,文法非常緊湊,在以後的文章中會越來越多地使用文法。
2、文法可以作為出色的文件。
3、即使你是從頭開始手動編寫解析器,文法也是一個不錯的起點。通常,你可以按照一組簡單的規則將文法轉換為程式碼。
4、有一套稱為解析器生成器的工具,可以接受文法作為輸入,並根據該文法自動生成解析器,我將在本系列的後面部分討論這些工具。
這是一個描述算術表示式的文法,例如"7 * 4 / 2 * 3"(這只是該文法可以生成的眾多表達式之一):
![](https://img2020.cnblogs.com/blog/1133903/202003/1133903-20200302181442030-885503881.png)
文法由一系列規則(rules)組成,也稱為productions,我們的文法有兩個規則:
![](https://img2020.cnblogs.com/blog/1133903/202003/1133903-20200302181509177-454692509.png)
一個規則由稱為**head**或**left-hand side**的非終結符(non-terminal)開始,然後跟上一個冒號(colon),最後由稱為**body**或**right-hand side**的終結符(terminal)和/或非終結符(non-terminal)組成:(A rule consists of a non-terminal, called the head or left-hand side of the production, a colon, and a sequence of terminals and/or non-terminals, called the body or right-hand side of the production)
(這裡我翻譯的很繞,直接看圖會更清楚一點)
![](https://img2020.cnblogs.com/blog/1133903/202003/1133903-20200302181521538-123422019.png)
在上面顯示的文法中,諸如MUL,DIV和INTEGER之類的標記稱為終結符(**terminals**),而諸如expr和factor之類的變數稱為非終結符(**non-terminals**),非終結符通常由一系列終結符和/或非終結符組成:(In the grammar I showed above, tokens like MUL, DIV, and INTEGER are called terminals and variables like expr and factor are called non-terminals. Non-terminals usually consist of a sequence of terminals and/or non-terminals)
![](https://img2020.cnblogs.com/blog/1133903/202003/1133903-20200302181544502-753325391.png)
第一條規則左側的非終結符稱為開始符(**start symbol**),在我們的文法中,開始符是expr:
![](https://img2020.cnblogs.com/blog/1133903/202003/1133903-20200302181558211-1917134829.png)
你可以將規則expr解釋為: “expr可以只是一個因數(**factor**),也可以可選地跟上乘法或除法運算子,然後再乘以另一個因數,之後又可選地跟上乘法或除法運算子,然後再乘以另一個因數,當然之後也可以繼續迴圈下去”(An expr can be a factor optionally followed by a multiplication or division operator followed by another factor, which in turn is optionally followed by a multiplication or division operator followed by another factor and so on and so forth)
是什麼因數(factor)?在本文中,它只是一個整數。
讓我們快速瀏覽一下文法中使用的符號及其含義:
1、"|",表示或者,因此(MUL | DIV)表示MUL或DIV。
2、"(…)",表示(MUL | DIV)中的終結符和/或非終結符的一個組(grouping)。
3、"(…)*",表示該組可以出現零次或多次。
如果你過去使用過正則表示式,那麼這些符號你應該非常熟悉。
文法通過解釋如何形成句子來定義語言(A grammar defines a language by explaining what sentences it can form),那麼我們如何使用文法來推匯出算術表示式呢?有這幾個步驟:首先從起始符expr開始,然後用該非終止符的規則主體重複替換非終止符,直到生成僅包含終止符的句子為止,這樣我們就通過文法來形成了語言。(first you begin with the start symbol expr and then repeatedly replace a non-terminal by the body of a rule for that non-terminal until you have generated a sentence consisting solely of terminals. Those sentences form a language defined by the grammar)
如果文法不能匯出某個算術表示式,則表示它不支援該表示式,那麼解析器在嘗試識別該表示式時將生成錯誤。
這裡有幾個例子,你可以看看來加深理解:
1、這是推導一個整數"3"的步驟:
![](https://img2020.cnblogs.com/blog/1133903/202003/1133903-20200302181610706-187643811.png)
2、這是推導"3 * 7"的步驟:
![](https://img2020.cnblogs.com/blog/1133903/202003/1133903-20200302181620919-1825492551.png)
3、這是推導"3 * 7 / 2"的步驟:
![](https://img2020.cnblogs.com/blog/1133903/202003/1133903-20200302181630596-334069216.png)
確實這部分包含很多理論!
當我第一次閱讀文法和相關術語時會感覺像這樣:
![](https://img2020.cnblogs.com/blog/1133903/202003/1133903-20200302181640180-1104443443.png)
我可以向你保證,我絕對不是這樣的:
![](https://img2020.cnblogs.com/blog/1133903/202003/1133903-20200302181649576-1482321762.png)
我花了一些時間來熟悉符號(notation),理解它如何工作方式以及它與解析器和詞法分析器之間的關係,我必須告訴你,由於它在編譯器相關文章中得到了廣泛的使用,從長遠來看,你一定會碰到它,所以我推薦你早點認識它:)
現在,讓我們將該文法對映到程式碼。
這是我們將文法轉換為原始碼的一些指導方法(**guideline**),遵循它們可以將文法從字面上轉換為有效的解析器:
1、語法中定義的每個規則R可以成為具有相同名稱的函式(method),並且對該規則的引用將成為方法呼叫:R(),函式的主體中的語句流也依照與同樣的指導方法。(Each rule, R, defined in the grammar, becomes a method with the same name, and references to that rule become a method call: R(). The body of the method follows the flow of the body of the rule using the very same guidelines.)
2、(a1 | a2 | aN)轉換為if-elif-else語句。(Alternatives (a1 | a2 | aN) become an if-elif-else statement.)
3、(…)*轉換while語句,可以迴圈零次或多次。(An optional grouping (…)* becomes a while statement that can loop over zero or more times.)
4、對Token的引用T轉換為對eat函式的呼叫:eat(T),也就是如果T的型別與當前的Token型別一致的話,eat函式將消耗掉T,然後從詞法分析器獲取一個新的Token並將其賦值給current_token這個變數。(Each token reference T becomes a call to the method eat: eat(T). The way the eat method works is that it consumes the token T if it matches the current lookahead token, then it gets a new token from the lexer and assigns that token to the current_token internal variable.)
準則總結如下所示:
![](https://img2020.cnblogs.com/blog/1133903/202003/1133903-20200302181700722-138143626.png)
讓我們按照上述指導方法將文法轉換為程式碼。
我們的語法有兩個規則:一個expr規則和一個因數規則。讓我們從因數規則(生產)開始。根據準則,需要建立一個稱為factor的函式(準則1),該函式需呼叫一次eat函式來消耗型別為INTEGER的Token(準則4):
```pascal
def factor(self):
self.eat(INTEGER)
```
很容易就可以寫出來。
繼續!
規則expr轉換為expr函式(準則1),規則的主體從對factor的引用開始,我們轉換為factor()函式呼叫,可選的組(…)*轉換為while迴圈,而(MUL | DIV)轉換為if-elif-else語句,將這些組合在一起,我們得到以下expr函式:
```pascal
def expr(self):
self.factor()
while self.current_token.type in (MUL, DIV):
token = self.current_token
if token.type == MUL:
self.eat(MUL)
self.factor()
elif token.type == DIV:
self.eat(DIV)
self.factor()
```
花一些時間看看我如何將語法對映到原始碼,確保你瞭解。
為了方便起見,我將上面的程式碼放入了parser.py檔案中,該檔案包含一個詞法分析器和一個不帶直譯器的分析器,你可以直接從[GitHub](https://github.com/Xlgd/simple_interpreter/blob/master/part4/parser.py)下載檔案並使用,它是互動式的,你可以在其中輸入表示式並檢視根據文法構建的解析器是否可以識別表示式。
這是我在計算機上的執行效果:
```
$ python parser.py
calc> 3
calc> 3 * 7
calc> 3 * 7 / 2
calc> 3 *
Traceback (most recent call last):
File "parser.py", line