三、語法分析器(Parser)﹣語法分析(Syntactic analysis)(下)

在上一節中,我們處理了 “var” “if” “while” ,大家應該學會了如何處理 expression block 了,這一節我們將會學習如何處理運算符。處理運算符有兩個基本概念要掌握,第一是運算符的運算次序,第二是運算表達式的表示法。

運算次序(Order or operations)

在數學算式裡,我們有個法則叫做 “先乘除,後加減”,意思就是乘除的運算次序比加減高,同樣地在程式語言中,我們也需要定義一下運算符的運算次序。在 Wescript 中,我們會用常用的運算符次序,即如下圖所示:

取自維基

西傑不會處理這裡所列的所有運算符,只會處理第一章所列的運算符,其他就留給讀者們當練習吧。

表示法

西傑相信大部份人都會使用 infix notation(可能用完也不知道它的名稱原來就是 infix notation ),但其實這世界上還有兩種運算式表示法, prefix notation 和 postfix notation ,看看例子大家就會明白這是什麼概念。

Infix notation: 1 + 2 * 3

Prefix notation: + 1 * 2 3

Postfix notation: 1 2 3 * +

我們日常寫程式時使用的都是 infix notation,但是對電腦來說,postfix notation 比較容易處理(通常會用一個 stack 來處理),因此我們的 Parser 將會讀進 Infix notation,到 compile 時就會轉為 postfix notation。

分項(或稱運算元 operand )

開始寫程式了,第一件事情是要分項,什麼是分項呢?就是把一句 expression 斬成多個項目,怎樣才算是一個項目呢?看看下圖:

項目包括以上所列的幾種:數值(數字和布林值),負數值,包著一堆子項目的括號,變數。根據這個分類法,我們可以改寫出 parse 項目的 function 。

Parser.prototype.parseOperand = function () {
    var token = this.nextToken();
    var operandNode;
    switch (token) {
        case Token.tokens.INTLITERAL_TOKEN:
            operandNode = new IntNode(this.currentToken.text);
            break;
        case Token.tokens.BOOLLITERAL_TOKEN:
            operandNode = new BoolNode(this.currentToken.text);
            break;
        case Token.tokens.IDENTIFIER_TOKEN:
            operandNode = new IdentifierNode(this.currentToken.text);
            break;
        case Token.tokens.LEFTPAREN_TOKEN:
            operandNode = new ParenNode(this.parseCompoundExpression(0));
            //consume the right paren )
            if (this.lookahead() == Token.tokens.RIGHTPAREN_TOKEN) {
                this.nextToken();
            } else {
                Errors.push({
                    type: Errors.SYNTAX_ERROR,
                    msg: "Missing right paren \")\"",
                    line: this.scanner.currLine
                });
            }
            break;
        case Token.tokens.MINUS_TOKEN:
            operandNode = new NegateNode(this.parseOperand());
            break;
        default:
            //not valid
            Errors.push({
                type: Errors.SYNTAX_ERROR,
                msg: "Unexpected token",
                line: this.scanner.currLine
            });
            return null;
    }
    return operandNode;
}

頭三款分別為數字,布林值和 identifier (即變數),只是很簡單的讀進一個 token 再建立一個 node 。遇到左括號時就有點特別了,這時我們要 parseCompoundExpression ,意思就是要處理更複雜的 expression (例如: 1 + 2 * 3 ),我們待會兒就會寫這個 function 。接著當我們遇到 “-” 時,我們就要再讀多一個項目,因為這個 “-” 代表負數值,我們要建立一個 NegateNode 。最後如果都不是以上幾款的話就算是 syntax error 了。

之前我們是在 parseExpression 中處理數值的,現在我們要從中抽起,放到 parseOperand 當中!

parseCompoundExpression 的 algorithm 如下,先大約看一看,下面再解釋。

Parser.prototype.getBindingPower = function (token) {
    switch (token) {
        case Token.tokens.PLUS_TOKEN:
        case Token.tokens.MINUS_TOKEN:
            return 120;
        case Token.tokens.MULT_TOKEN:
        case Token.tokens.DIV_TOKEN:
            return 130;
    }
    return -1;
}
Parser.prototype.createOperatorNode = function (operatorToken) {
    switch (operatorToken) {
        case Token.tokens.PLUS_TOKEN:
            return new OperatorPlusNode();
            break;
        case Token.tokens.MINUS_TOKEN:
            return new OperatorMinusNode();
            break;
        case Token.tokens.MULT_TOKEN:
            return new OperatorMultNode();
            break;
        case Token.tokens.DIV_TOKEN:
            return new OperatorDivNode();
            break;
    }
}
Parser.prototype.parseCompoundExpression = function (rightBindingPower) {
    var operandNode = this.parseOperand();
    if (operandNode == null) {
        return operandNode;
    }
    var compoundExpressionNode = new CompoundNode();
    compoundExpressionNode.push(operandNode);
    var operator = this.lookahead();
    var leftBindingPower = this.getBindingPower(operator);
    if (leftBindingPower == -1) {
        //not an operator
        return compoundExpressionNode;
    }
    while (rightBindingPower < leftBindingPower) {
        operator = this.nextToken();
        compoundExpressionNode.push(this.createOperatorNode(operator));
        var node = this.parseCompoundExpression(leftBindingPower);
        compoundExpressionNode.push(node);
        var oper = this.lookahead();
        leftBindingPower = this.getBindingPower(oper);
        if (leftBindingPower == -1) {
            //not an operator
            return compoundExpressionNode;
        }
    }
    return compoundExpressionNode;
}

這個 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 較強,所以這句判斷成立:

while (rightBindingPower < leftBindingPower) { //0 < 120

注意不要搞錯左和右,這裡的左是指運算符的左面,而不是項目的左面 接下來就是要 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 使用,看看代碼。

case Token.tokens.IDENTIFIER_TOKEN:
    operandNode = new IdentifierNode(this.currentToken.text);
    if (this.lookahead() == Token.tokens.MINUSMINUS_TOKEN) {
        //post decrement
        this.nextToken();
        operandNode = new PostDecrementNode(operandNode);
    } else if (this.lookahead() == Token.tokens.PLUSPLUS_TOKEN) {
        //post increment
        this.nextToken();
        operandNode = new PostIncrementNode(operandNode);
    }
break;

首先處理 post-in/decrement ,方法很簡單,就是當我們遇到 identifier 時,我們就會 lookahead 一下,看看有沒有 “++” 或者 “–” ,有的話就用 PostIn/DecrementNode 來取代原本的 operandNode ,沒有就自然不用理會啦。

case Token.tokens.PLUSPLUS_TOKEN:
    if (this.lookahead() == Token.tokens.IDENTIFIER_TOKEN) {
        this.nextToken();
        operandNode = new PreIncrementNode(newIdentifierNode(this.currentToken.text));
    } else {
        Errors.push({
            type: Errors.SYNTAX_ERROR,
            msg: "Expecting an identifier for pre-increment expression",
            line: this.scanner.currLine
        });
        return null;
    }
    break;

之後要增加 PreIn/Decrement 的處理,就是增加一個 case 到 parseOperand 中,遇到 “++”“–” 就會 lookahead 一下看看有沒有 identifier ,有的話就是 PreIn/Decrement ,沒有的話應該算是 syntax error 了吧。

看看 console.log ,還是運作正常,那就大功告成啦!!!

Put it together

現在看看完整的 Parse tree(console.log)

總結

兩個星期的 Syntactic analysis 課程終於完結了,不知道大家吸收了多少,其實編寫 Parser 說難不難,說易不易,基本來說就是一堆 mutual recursion,把大問題斬件成為多個小問題,逐點擊破就可以了,只要大家思緒清晰的話,其實不難(當然要思緒清晰也確實不易)。

大家可以放心,最難捱的時刻已經過了,下一章會比這章容易一點,大家如果仍然未搞清這章的思路的話不妨留下你的問題,西傑一定會儘量解答,拜拜,下星期再見。

results matching ""

    No results matching ""