【深入淺出教你寫編譯器(Compiler)】三、語法分析器(Parser)﹣語法分析(Syntactic analysis)(下)在上一節中,我們處理了 “var” “if” “while”,大家應該學會了如何處理 expression block了,這一節我們將會學習如何處理運算符。處理運算符有兩個基本概念要掌握,第一是運算符的運算次序,第二是運算表達式的表示法。
運算次序(Order or operations)在數學算式裏,我們有個法則叫做“先乘除,後加減”,意思就是乘除的運算次序比加減高,同樣地在程式語言中,我們也需要定義一下運算符的運算次序。在 Wescript 中,我們會用常用的運算符次序,即如下圖所示: 西杰不會處理這裏所列的所有運算符,只會處理第一章所列的運算符,其他就留給讀者們當練習吧。 表示法西杰相信大部份人都會使用 infix notation(可能用完也不知道它的名稱原來就是 infix notation),但其實這世界上還有兩種運算式表示法,prefix notation 和 postfix notation,看看例子大家就會明白這是什麼概念。
我們日常寫程式時使用的都是 infix notation,但是對電腦來說,postfix notation 比較容易處理(通常會用一個 stack 來處理),因此我們的 Parser 將會讀進 Infix notation,到 compile 時就會轉為 postfix notation。 分項(或稱運算元 operand)開始寫程式了,第一件事情是要分項,什麼是分項呢?就是把一句 expression 斬成多個項目,怎樣才算是一個項目呢?看看下圖: 項目包括以上所列的幾種:數值(數字和布林值),負數值,包著一堆子項目的括號,變數。根據這個分類法,我們可以改寫出 parse 項目的 function。
頭三款分別為數字,布林值和 identifier(即變數),只是很簡單的讀進一個 token 再建立一個 node。遇到左括號時就有點特別了,這時我們要 parseCompoundExpression,意思就是要處理更複雜的 expression(例如: 1 + 2 * 3),我們待會兒就會寫這個 function。接著當我們遇到 “-” 時,我們就要再讀多一個項目,因為這個 “-” 代表負數值,我們要建立一個 NegateNode。最後如果都不是以上幾款的話就算是 syntax error 了。 之前我們是在 parseExpression 中處理數值的,現在我們要從中抽起,放到 parseOperand 當中! parseCompoundExpression 的 algorithm 如下,先大約看一看,下面再解釋。
這個 algorithm 是取自 Vaughan Pratt 的(好似係),但我是在 Eli Bendersky 那裏學的。看起來有點複雜了吧,放心,我慢慢地來 。 Binding Power由於運算符運算時有先後次序之分,我們的 compiler 也需要處理,而 Wescript compiler 處理的方法就是賦予每一個運算符一個 binding power,Binding Power 高的運算符可以佔有其相鄰的項目,如 Eli 的例子所示: B 比 A 強,所以佔有 E(唉,連電腦的世界裏也是弱肉強食……)。因此,我們需要先設定運算符的 binding power,這就是 getBindingPower 的工作了。到戲肉了,這裏我會用靜態動畫(靜態的動畫)來描述這個 algorithm 的工作。 開始時是這樣的,注意括號的 binding power 是 0,因為我把它當成項目,而不是普通的運算符。 現在開始執行,一開始的 rightBindingPower 是 0,讀取了一個項目(以圓形表示),由於 “+” 的 binding power 較強,所以這句判斷成立: 注意不要搞錯左和右,這裏的左是指運算符的左面,而不是項目的左面 接下來就是要 recursive 地再運行 parseCompoundExpression。 同樣地,讀取一個項目,檢查一下右面的運算符的 leftBindingPower,發現 rightBindingPower (120) 比 leftBindingPower (130) 小,於是就再運行一個新的 recursion。 接下來的 3 – 4 的處理方法跟上面相似,但有些分別,括號是當成一個項目來處理,所以 binding power 由 0 開始,當讀到 4 的時候,再讀一個運算符時,我們會讀到 “)” 右括號,當我們嘗試拿取它的 binding power 時就會拿到 -1,於是是次 recursion 直接終止。 然後括號也處理好了,也可以 return 了。 由於下一個運算符 “+” 的 binding power (120) 比現在的(130)小,所以也可以 return 了。 可不可以用 rightBindingPower <= leftBindingPower 而不是 rightBindingPower < leftBindingPower 呢? 不可以,想想這個例子,1 – 2 + 3,由於 “-” 和 “+” 的 binding power 一樣,如果用小於或等於的話,parse tree 將會當成 1 – (2 + 3) 現在連 2 * (3 – 4) 也可以 return 了,因為 1 + 2 的 “+” 的 binding power 並不比 + 5 的 “+” 小。返回之後,binding power 為 0 的那個 function 將會繼續讀取運算式直至讀取所有運算式為止。 就是這樣了,很簡單吧?不明白的多看幾次以上的例子吧,再不明白的話就留個言,西杰很快會跟進的了。現在看看這段程式是不是運作正常。 看看 console.log,程式運作正常。現在把餘下的運算符都編寫下來! 這段程式主要編寫了邏輯運算符,看看 console.log,還是運作正常。那我們現在寫餘下的 assignment 運算符吧。 “=” “+=” “-=” 其實沒有什麼特別處理方法,跟之前的運算符差不多,這裏就不詳述了。但是 “++” 和 “–” 就有特別了,它們是 unary 運算符,同時又可以作 pre-in/decrement 或 post-in/decrement,這樣要如何處理呢?西杰就把它們當成 operand 一樣來處理,而且一定要配合 identifier 使用,看看代碼。
首先處理 post-in/decrement,方法很簡單,就是當我們遇到 identifier 時,我們就會 lookahead 一下,看看有沒有 “++” 或者 “–”,有的話就用 PostIn/DecrementNode 來取代原本的 operandNode,沒有就自然不用理會啦。
之後要增加 PreIn/Decrement 的處理,就是增加一個 case 到 parseOperand 中,遇到 “++” 或 “–” 就會 lookahead 一下看看有沒有 identifier,有的話就是 PreIn/Decrement,沒有的話應該算是 syntax error 了吧。 看看 console.log,還是運作正常,那就大功告成啦!!! Put it together現在看看完整的 Parse tree(console.log) 總結兩個星期的 Syntactic analysis 課程終於完結了,不知道大家吸收了多少,其實編寫 Parser 說難不難,說易不易,基本來說就是一堆 mutual recursion,把大問題斬件成為多個小問題,逐點擊破就可以了,只要大家思緒清晰的話,其實不難(當然要思緒清晰也確實不易)。 大家可以放心,最難捱的時刻已經過了,下一章會比這章容易一點,大家如果仍然未搞清這章的思路的話不妨留下你的問題,西杰一定會盡量解答,拜拜,下星期再見。 |
|
来自: quasiceo > 《深入浅出教你写编译器》