實現一個簡單的解釋器(5)

譯自:https://ruslanspivak.com/lsbasi-part5/
(已得到做者受權)html

你如何處理和了解像建立解釋器或編譯器這樣複雜的事情?在開始時,一切看上去都像是一團亂七八糟的紗線,你須要解開纏結才能獲得完美的球。python

到達那裏的方法是將它解開一個線,一次解開一個結。不過有時候,你可能會以爲本身聽不懂某些內容,但必須繼續前進,我向你保證,若是你足夠堅持,它最終將「咔嗒」一聲被解開。git

在理解如何建立解釋器和編譯器的過程當中,最好的建議之一是閱讀文章中的解釋,閱讀代碼,而後本身編寫代碼,甚至在一段時間裏編寫相同的代碼,使你能徹底瞭解材料和代碼,而後繼續學習新主題。不要着急,只要放慢腳步,花點時間深入理解基本概念,這種方法雖然看似很慢,但會在將來得到回報。相信我。github

最後,最終將得到完美的毛線球,即便不是那麼完美,它也比什麼也不作或者快速瀏覽後的幾天內忘掉好多了。express

請記住:一步步理解這些知識點,並經過編寫代碼練習所學的知識:
編程

今天,你將使用前幾篇文章中得到的知識,學習如何解析和解釋具備任意數量的加,減,乘和除運算的算術表達式,你將編寫一個解釋器,該解釋器將可以對"14 + 2 * 3 - 6 / 2"之類的表達式求值。編程語言

在深刻學習並編寫一些代碼以前,咱們先討論一下運算符的結合性(associativity)和優先級(associativity)。ide

按照慣例7 + 3 + 1與(7 + 3)+ 1相同,而7 - 3 - 1至關於(7 - 3)- 1。 咱們都瞭解這些。若是咱們將7 - 3 - 1視爲7 -(3 - 1),則結果將會意外的變成5,而不是咱們預期的3。函數

在普通算術和大多數編程語言中,加,減,乘和除是左結合(left-associative)的:學習

7 + 3 + 1 is equivalent to (7 + 3) + 1
7 - 3 - 1 is equivalent to (7 - 3) - 1
8 * 4 * 2 is equivalent to (8 * 4) * 2
8 / 4 / 2 is equivalent to (8 / 4) / 2

什麼是運算符號的左結合性呢?

當表達式7 + 3 + 1中的3之類的操做數在兩側都帶有加號時,咱們須要約定來肯定哪一個運算符適用於3,是左邊的加號仍是右邊的加號?咱們說運算符加號左結合,是由於在存在兩側都帶有加號的操做數時,此時左邊的加號適用於此操做數,所以咱們說運算符加號是左結合的(The operator + associates to the left because an operand that has plus signs on both sides belongs to the operator to its left and so we say that the operator + is left-associative.),因此按照結合性慣例,7 + 3 + 1等於(7 + 3)+ 1。

咱們再來看7 + 5 * 2這個表達式,在操做數5的兩邊都有不一樣類型的運算符,該表達式等於7 +(5 * 2)仍是(7 + 5)* 2呢?咱們如何解決這種歧義?

在這種狀況下,結合性約定對咱們沒有幫助,由於它僅適用於加減法(+,-)或乘除法(*,/)這種同一類型的運算符。當咱們在同一表達式中具備不一樣種類的運算符時,咱們須要另外一種約定來解決歧義。咱們須要一個定義運算符相對優先級的約定。

咱們說若是運算符乘號在加號以前執行其操做數,則乘號具備更高的優先級(higher precedence)。在咱們所使用的運算中,乘法和除法的優先級高於加法和減法,因此表達式7 + 5 * 2等效於7 +(5 * 2),表達式7 - 8 / 4等效於7-(8 / 4)。

在含有優先級相同的運算符的表達式中,咱們僅使用結合性約定並從左到右執行運算符:

7 + 3 - 1 is equivalent to (7 + 3) - 1
8 / 4 * 2 is equivalent to (8 / 4) * 2

但願你不要由於這些運算符的結合性和優先級而感到無聊,咱們能夠利用這些約定來構造算術表達式的語法,以顯示算術運算符的結合性和優先級,而後,咱們能夠按照我在第4部分中概述的準則將語法轉換爲代碼,咱們的解釋器將可以處理運算符的優先級和結合性約定。

這是咱們的優先級表(precedence table):

從表中能夠看出,運算符加號和減號具備相同的優先級,而且它們都是左結合的,還能夠看到,運算符乘號和除號也是左結合的,它們之間也具備相同的優先級,可是比加減運算符具備更高的優先級。

如下是有關如何根據優先級表構造語法的規則:

一、爲每一類優先級定義一個非終結符。非終極符的產生式主體應包含該類優先級的算術運算符和下一類更高優先級的非終結符。( The body of a production for the non-terminal should contain arithmetic operators from that level and non-terminals for the next higher level of precedence.)

二、爲基本的表達單位(在本文下爲整數)建立一個附加的非終結符factor。通常規則是,若是具備N類優先級,則總共將須要N + 1個非終結符:每類級別一個非終結符(N個)再加上一個運算基本單位的非終結符factor(1個)。(Create an additional non-terminal factor for basic units of expression, in our case, integers. The general rule is that if you have N levels of precedence, you will need N + 1 non-terminals in total: one non-terminal for each level plus one non-terminal for basic units of expression.)

繼續!

讓咱們遵循規則並構建語法。

根據規則1,咱們將定義兩個非終結符:一個用於級別2的稱爲expr的非終結符和一個用於級別1的稱爲term的非終結符,經過規則2,咱們將爲算術的基本單位定義一個非終結符factor來表達整數。

新語法的起始符號將爲expr,expr的產生式將包含一個表示使用級別2的運算符主體,在本例中爲加號和減號,並將包含更高級別優先級的非終結符term。
級別2:

term產生式將包含一個使用級別1運算符的主題,即運算符乘號和除號,而且它也包含基本表達式單位(整數)的非終結符factor:

非終結符factor的產生式爲:

你已經在以前的文章看見過以上產生式的語法和語法圖,在這裏,考慮到結合性和優先級,咱們將它們組合成一個語法:

這是與上面的語法相對應的語法圖:

圖中的每一個矩形框都是對另外一個圖的「函數調用(method call)」。 若是你採用表達式7 + 5 * 2並從頂部expr開始,而後一直走到最底部的factor,那麼應該可以看到較高優先級的運算符乘號和除號在較低的圖中,運算符加號和減號在較高的圖中,而且高優先級的運算符先執行。

讓咱們看一下根據上面的語法和語法圖來對算術表達式7 + 5 * 2計算的分解步驟,這是顯示高優先級運算符先於低優先級運算符執行的另外一種方式:

好的,讓咱們按照第4部分中的指導方法將語法轉換爲代碼,而後看看咱們的新解釋器如何工做。

這是語法:

這是計算器的完整代碼,能夠處理包含任意數量的加,減,乘和除整數運算的有效算術表達式。

與第4部分中的代碼相比,如下是主要改變的地方:

一、Lexer類如今能夠對+,-,*和/進行標記化(這裏沒有什麼新鮮的,咱們只是將之前文章中的代碼組合到一個支持全部這些Token的類中)
二、回想一下,在語法中定義的每一個規則(產生式)R都會轉換爲具備相同名稱的函數,而且對該規則的引用會成爲函數調用:R(), 因此Interpreter類如今具備對應於語法中三種非終結符函數:expr,term和factor。
源代碼:

# Token types
#
# EOF (end-of-file) token is used to indicate that
# there is no more input left for lexical analysis
INTEGER, PLUS, MINUS, MUL, DIV, EOF = (
    'INTEGER', 'PLUS', 'MINUS', 'MUL', 'DIV', 'EOF'
)


class Token(object):
    def __init__(self, type, value):
        # token type: INTEGER, PLUS, MINUS, MUL, DIV, or EOF
        self.type = type
        # token value: non-negative integer value, '+', '-', '*', '/', or None
        self.value = value

    def __str__(self):
        """String representation of the class instance.

        Examples:
            Token(INTEGER, 3)
            Token(PLUS, '+')
            Token(MUL, '*')
        """
        return 'Token({type}, {value})'.format(
            type=self.type,
            value=repr(self.value)
        )

    def __repr__(self):
        return self.__str__()


class Lexer(object):
    def __init__(self, text):
        # client string input, e.g. "3 * 5", "12 / 3 * 4", etc
        self.text = text
        # self.pos is an index into self.text
        self.pos = 0
        self.current_char = self.text[self.pos]

    def error(self):
        raise Exception('Invalid character')

    def advance(self):
        """Advance the `pos` pointer and set the `current_char` variable."""
        self.pos += 1
        if self.pos > len(self.text) - 1:
            self.current_char = None  # Indicates end of input
        else:
            self.current_char = self.text[self.pos]

    def skip_whitespace(self):
        while self.current_char is not None and self.current_char.isspace():
            self.advance()

    def integer(self):
        """Return a (multidigit) integer consumed from the input."""
        result = ''
        while self.current_char is not None and self.current_char.isdigit():
            result += self.current_char
            self.advance()
        return int(result)

    def get_next_token(self):
        """Lexical analyzer (also known as scanner or tokenizer)

        This method is responsible for breaking a sentence
        apart into tokens. One token at a time.
        """
        while self.current_char is not None:

            if self.current_char.isspace():
                self.skip_whitespace()
                continue

            if self.current_char.isdigit():
                return Token(INTEGER, self.integer())

            if self.current_char == '+':
                self.advance()
                return Token(PLUS, '+')

            if self.current_char == '-':
                self.advance()
                return Token(MINUS, '-')

            if self.current_char == '*':
                self.advance()
                return Token(MUL, '*')

            if self.current_char == '/':
                self.advance()
                return Token(DIV, '/')

            self.error()

        return Token(EOF, None)


class Interpreter(object):
    def __init__(self, lexer):
        self.lexer = lexer
        # set current token to the first token taken from the input
        self.current_token = self.lexer.get_next_token()

    def error(self):
        raise Exception('Invalid syntax')

    def eat(self, token_type):
        # compare the current token type with the passed token
        # type and if they match then "eat" the current token
        # and assign the next token to the self.current_token,
        # otherwise raise an exception.
        if self.current_token.type == token_type:
            self.current_token = self.lexer.get_next_token()
        else:
            self.error()

    def factor(self):
        """factor : INTEGER"""
        token = self.current_token
        self.eat(INTEGER)
        return token.value

    def term(self):
        """term : factor ((MUL | DIV) factor)*"""
        result = self.factor()

        while self.current_token.type in (MUL, DIV):
            token = self.current_token
            if token.type == MUL:
                self.eat(MUL)
                result = result * self.factor()
            elif token.type == DIV:
                self.eat(DIV)
                result = result / self.factor()

        return result

    def expr(self):
        """Arithmetic expression parser / interpreter.

        calc>  14 + 2 * 3 - 6 / 2
        17

        expr   : term ((PLUS | MINUS) term)*
        term   : factor ((MUL | DIV) factor)*
        factor : INTEGER
        """
        result = self.term()

        while self.current_token.type in (PLUS, MINUS):
            token = self.current_token
            if token.type == PLUS:
                self.eat(PLUS)
                result = result + self.term()
            elif token.type == MINUS:
                self.eat(MINUS)
                result = result - self.term()

        return result


def main():
    while True:
        try:
            # To run under Python3 replace 'raw_input' call
            # with 'input'
            text = raw_input('calc> ')
        except EOFError:
            break
        if not text:
            continue
        lexer = Lexer(text)
        interpreter = Interpreter(lexer)
        result = interpreter.expr()
        print(result)


if __name__ == '__main__':
    main()

將以上代碼保存到calc5.py文件中,或直接從GitHub下載,嘗試一下,而後本身看看解釋器是否正確地計算了具備不一樣優先級運算符的算術表達式。

這是個人筆記本電腦上的運行效果:

$ python calc5.py
calc> 3
3
calc> 2 + 7 * 4
30
calc> 7 - 8 / 4
5
calc> 14 + 2 * 3 - 6 / 2
17

接下來是今天的練習:

一、在不瀏覽本文代碼的狀況下編寫本文所述的解釋器,完成後編寫一些測試,並確保它們經過。

二、擴展解釋器以處理包含括號的算術表達式,以便你的解釋器能夠計算深度嵌套的算術表達式,例如:7 + 3 *(10 /(12 /(3 + 1)-1))

最後再來複習回憶一下:

一、運算符的左結合是什麼意思?
二、運算符加號和減號是左結合仍是右結合? 乘號和除號呢?
三、運算符加號的優先級是否比運算符乘號高?

嘿,你一直閱讀到最後! 真的很棒 下次我會再寫一篇新文章,敬請期待。

相關文章
相關標籤/搜索