1. 程式人生 > >實現一個簡單的直譯器(3)

實現一個簡單的直譯器(3)

譯自:https://ruslanspivak.com/lsbasi-part3/
(已獲得作者授權)

我今天早上醒來,心想:“為什麼我們學習新技能如此困難?”

我不認為這僅僅是因為不夠努力,我認為原因之一可能是我們花費大量時間和精力來通過閱讀和觀看獲取知識,而沒有足夠的時間通過實踐將知識轉化為技能。以游泳為例,你可以花費大量時間閱讀數百本有關游泳的書籍,與經驗豐富的游泳者和教練交談數小時,觀看所有可用的培訓視訊,但是當你第一次跳入泳池時,你仍然會像石頭一樣沉下去。

不管我們有多麼瞭解我們的學科,其實並不重要,重要的是將這些知識付諸實踐,這樣才能將其轉化為技能。為了幫助你進行練習,我在第一部分和第二部分中都添加了練習,我保證會在今天的文章和以後的文章中增加更多練習:)

好吧,讓我們開始吧!

到目前為止,你已經瞭解如何解釋兩個整數相加或相減的算術表示式,例如"7 + 3"或"12 - 9"。今天,我將討論如何解析(識別)和解釋包含任意數量的正負運算子的算術表示式,例如"7 - 3 + 2 - 1"。

我們可以用以下語法圖(syntax diagram)表示本文中將要處理的算術表示式:

什麼是語法圖?語法圖是程式語言的語法規則(syntax rules)的圖形表示(graphical representation)。基本上,語法圖直觀地顯示了程式語言中允許哪些語句,哪些不允許。

語法圖非常易於閱讀:只需遵循箭頭指示的路徑,有的路徑表示選擇,有的路徑表示迴圈。

我們來閱讀上面的語法圖:一個term後面可選地跟上加號或者減號,然後又跟上另一個term,然後又可選地帶上加號或減號,之後可以繼續迴圈。你可能想知道什麼是term,在這篇文章中,term只是一個整數。

語法圖主要用於兩個目的:

1、它們以圖形方式表示程式語言的規範(語法)(grammar)。
2、它們可以用來幫助編寫解析器(parser),我們可以按照簡單的規則將圖表對映(map)為程式碼。

你已經瞭解到,識別Token流中的短語的過程稱為解析(parsing),執行該工作的直譯器或編譯器部分稱為解析器(parser),解析也稱為語法分析(syntax analysis),我們也將解析器稱為語法分析器(syntax analyzer)。

根據上面的語法圖,以下所有算術表示式都是有效的:

  • 3
  • 3 + 4
  • 7-3 + 2-1

由於不同程式語言中算術表示式的語法規則非常相似,因此我們可以使用Python Shell來“測試(test)”語法圖。 啟動你的Python Shell,親自看看:

>>> 3
3
>>> 3 + 4
7
>>> 7 - 3 + 2 - 1
5

這裡並不意外,和我們與預想的一樣。

注意,表示式"3 +"不是一個有效(valid)的算術表示式,因為根據語法圖,加號後必須加上一個term(整數),否則是語法錯誤,再次使用Python Shell嘗試一下:

>>> 3 +
  File "<stdin>", line 1
    3 +
      ^
SyntaxError: invalid syntax

能夠使用Python Shell進行測試非常好,但我們更想將上面的語法圖對映為程式碼,並使用我們自己的直譯器進行測試。

可以從前面的文章(第1部分和第2部分)中知道expr函式實現瞭解析器(parser)和直譯器(interperter),解析器僅識別結構,以確保它與某些規範相對應,並且一旦解析器成功識別(解析)了該表示式,直譯器便會實際計算(evaluate)該表示式(expression)。

以下程式碼段顯示了與該圖相對應的解析器程式碼。語法圖中的矩形框成為解析整數的term函式,而expr函式僅遵循語法圖流程(syntax diagram flow):

def term(self):
    self.eat(INTEGER)

def expr(self):
    # set current token to the first token taken from the input
    self.current_token = self.get_next_token()

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

可以看到expr首先呼叫term函式,然後expr函式有一個while迴圈,可以執行零次或多次,在迴圈內,解析器根據Token(是加號還是減號)進行選擇,可以看出上面的程式碼確實遵循了算術表示式的語法圖流程。

解析器本身不解釋(interpret)任何內容:如果識別不出來表示式,它會丟擲語法錯誤。
讓我們修改expr函式並新增直譯器程式碼:

def term(self):
    """Return an INTEGER token value"""
    token = self.current_token
    self.eat(INTEGER)
    return token.value

def expr(self):
    """Parser / Interpreter """
    # set current token to the first token taken from the input
    self.current_token = self.get_next_token()

    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

由於直譯器需要計算(evaluate)表示式,因此對term函式進行了修改以返回整數值,並且對expr函式進行了修改以在適當的位置執行加法和減法並返回解釋的結果。
即使程式碼非常簡單,我還是建議你花一些時間來研究它。

現在我們來看完整的直譯器程式碼。

這是新版本計算器的原始碼,它可以處理包含任意數量整數的加減運算的有效算術表示式:

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


class Token(object):
    def __init__(self, type, value):
        # token type: INTEGER, PLUS, MINUS, 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, '+')
        """
        return 'Token({type}, {value})'.format(
            type=self.type,
            value=repr(self.value)
        )

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


class Interpreter(object):
    def __init__(self, text):
        # client string input, e.g. "3 + 5", "12 - 5 + 3", etc
        self.text = text
        # self.pos is an index into self.text
        self.pos = 0
        # current token instance
        self.current_token = None
        self.current_char = self.text[self.pos]

    ##########################################################
    # Lexer code                                             #
    ##########################################################
    def error(self):
        raise Exception('Invalid syntax')

    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, '-')

            self.error()

        return Token(EOF, None)

    ##########################################################
    # Parser / Interpreter code                              #
    ##########################################################
    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.get_next_token()
        else:
            self.error()

    def term(self):
        """Return an INTEGER token value."""
        token = self.current_token
        self.eat(INTEGER)
        return token.value

    def expr(self):
        """Arithmetic expression parser / interpreter."""
        # set current token to the first token taken from the input
        self.current_token = self.get_next_token()

        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
        interpreter = Interpreter(text)
        result = interpreter.expr()
        print(result)


if __name__ == '__main__':
    main()

將以上程式碼儲存到calc3.py檔案中,或直接從GitHub下載,它可以處理之前顯示的語法圖中得出的算術表示式。

這是我在筆記本上的執行效果:

$ python calc3.py
calc> 3
3
calc> 7 - 4
3
calc> 10 + 5
15
calc> 7 - 3 + 2 - 1
5
calc> 10 + 1 + 2 - 3 + 4 + 6 - 15
5
calc> 3 +
Traceback (most recent call last):
  File "calc3.py", line 147, in <module>
    main()
  File "calc3.py", line 142, in main
    result = interpreter.expr()
  File "calc3.py", line 123, in expr
    result = result + self.term()
  File "calc3.py", line 110, in term
    self.eat(INTEGER)
  File "calc3.py", line 105, in eat
    self.error()
  File "calc3.py", line 45, in error
    raise Exception('Invalid syntax')
Exception: Invalid syntax

記住我在文章開頭提到的,現在該做練習了:

1、為僅包含乘法和除法的算術表示式繪製語法圖,例如"7 * 4 / 2 * 3"。
2、修改計算器的原始碼以解釋僅包含乘法和除法的算術表示式,例如"7 * 4 / 2 * 3"。
3、從頭開始編寫一個直譯器,處理諸如"7 - 3 + 2 - 1"之類的算術表示式。使用你喜歡的任何程式語言,並在不看示例的情況下將其寫在腦海中,請考慮所涉及的元件:一個詞法分析器,它接受輸入並將其轉換為Token流;解析器,將從詞法分析器提供的Token流中嘗試識別該流中的結構;以及在解析器成功解析(識別)有效算術表示式後生成結果的直譯器。將這些組合在一起,花一些時間將學到的知識翻譯成可用於算術表示式的直譯器。

最後再來複習回憶一下:

1、什麼是語法圖?
2、什麼是語法分析?
3、什麼是語法分析器?

你看!你一直閱讀到最後,感謝你今天在這裡閒逛,不要忘了做練習,:)敬請期待