程序的語法

『我不生產代碼,我只是代碼的搬運工。』當然了,這是一個玩笑。說到代碼,我們要學習各種編程語言,學習如何讓編譯器能懂我們編寫的代碼。但是,編譯器是如何做到能聽懂我們的話的呢?按照我們既定的語句一行行的去執行,最終達到我們的目的。這篇文章,我會講一個很簡單的四則運算解釋器,通過使用 Python 實現它來一步步瞭解一個解釋器是如何工作的,它們又是怎麼得到我們想要的結果的。

語法

計算機語言同樣也是一種語言,是一種計算機能夠理解的語言,我們想要和計算機對話,就要去學習這種語言。和我們人類的語言一樣,計算機語言也是有語法的。以漢語爲例,我說『我是中國人』,因爲漢語的語法,你聽到這句話之後會知道『我』是主語,『是』是謂語,『中國人』是賓語,同時你又很清楚每一個成分的含義,最終理解了整句話的意思。

同樣,對於計算機語言也是一樣,有着程序的語法,一個解釋器知道哪個詞是操作數,哪個是操作符,哪個是關鍵字,它們都有着怎樣的含義和功能,通過解釋器的解釋,計算機明白了某行語句的意義,然後進行運算,得到最後的執行結果。

語法圖

語法圖就是用來表示一種編程語言語法規則設計的示意圖,它很直觀的顯示出了在一種編程語言中,允許使用的語句和不支持的語句。語法圖十分易於閱讀:只需跟隨箭頭指示的路徑。一些路徑表示選擇。另一些路徑表示循環。

一個簡單的語法圖

這裏我們舉一個語法圖的例子:

image

這個語法圖就是一個描述了簡單的加減運算的語法圖,term 在其中的意思就是一個操作數,一開始輸入一個操作數,有三條路徑可以選擇『+』,『-』和退出,如果進入了『+』、『-』路徑,則需要再輸入一個操作數,之後的路徑包括『+』、『-』和退出,如此循環,就能解釋一個簡單的加減法解釋器。

根據上邊的語法圖,下面的表達式都是合法的:

  • 4
  • 1 + 1
  • 1 + 4 - 3

下面的表達式不合法:

  • -
  • + -
  • 2 +
  • + 3 - 3

語法圖可以幫助我們:

  • 用圖的方式表示出一種計算機語言的設計規範
  • 可以幫助理解解釋器,將圖表表示成代碼

代碼實現(Python)

學習一樣東西最簡單的就是閱讀代碼(Read the Fucking Code!),因此我們先來看完整代碼然後再來分析一下,完整代碼在:https://github.com/luoyhang003/Pascal-Interpreter/blob/master/calc3.py

# Token Types:
# EOF: End-Of-File

INTEGER, PLUS, MINUS, EOF = 'INTEGER', 'PLUS', 'MINUS', 'EOF'

class Token(object):
    def __init__(self, type, value):
        # Token types: INTEGER, PLUS, MINUS, EOF
        self.type = type
        # Token value: 0,1,2,3,4,5,6,7,8,9,+,None
        self.value = value

    def __str__(self):
        """
        The format of the print infomation
        For examle:
        Token(INTEGER, 3)
        Token(PLUS, '+')
        Token(MINUS, '-')
        """
        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):
        # Process the whole input
        # e.g. 3+5
        self.text = text
        self.pos = 0
        self.current_token = None
        self.current_char = self.text[self.pos]

    def error(self):
        raise Exception('Error Parsing Error!')

    def advance(self):
        # Advance the pos and set current_char
        self.pos += 1

        if self.pos > len(self.text)-1:
            self.current_char = None
        else:
            self.current_char = self.text[self.pos]

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

    def integer(self):
        # Support mutidigit integer
        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:
        Parsing the input into tokens.
        Strategy:
        1. is pos past the end of the text?
        2. if so, return EOF
        3. get a character at pos, and decide its type depends on the single char
        4. if it is a space, advance the pos
        5. if it is a digit, then convert it to integer and return INTEGER token
        6. if it is a '+', then return PLUS token
        7. if it is a '-', then return MINUS token
        """

        while self.current_char is not None:

            if self.pos > len(self.text) - 1:
                return Token(EOF, None)

            current_char = self.text[self.pos]

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

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

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

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


            self.error()

        return Token(EOF, None)

    def eat(self, token_type):
        # compare the current token with the passed token type
        # if they match then eat the current token and assign 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's value
        token = self.current_token
        self.eat(INTEGER)
        return token.value





    def expr(self):
        # expr -> INTEGER PLUS INTEGER
        # expr -> INTEGER MINUS INTEGER

        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 += self.term()
            if token.type == MINUS:
                self.eat(MINUS)
                result -= self.term()

        return result

def main():
    while True:
        try:
            text = raw_input('cal> ')
        except EOFError:
            break
        if not text:
            continue

        interpreter = Interpreter(text)
        result = interpreter.expr()
        print(result)

if __name__ == '__main__':
    main()

在代碼中,我們首先定義了四種 Token:整數(Integer)、加法(+)、減法(-)、終止符(EOF)

代碼中主要分爲幾個主要部分:

  • term 方法,在表達式中解析出一個整數
    def term(self):
        # return an Integer Token's value
        token = self.current_token
        self.eat(INTEGER)
        return token.value
  • expr 方法,根據語法圖的流程來解析語句
    def expr(self):
        # expr -> INTEGER PLUS INTEGER
        # expr -> INTEGER MINUS INTEGER

        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 += self.term()
            if token.type == MINUS:
                self.eat(MINUS)
                result -= self.term()

        return result

expr 方法,首先調用 term 方法獲取一個整數,然後判斷後邊的 Token 是加法還是減法,之後再獲取一個整數,然後進行運算,然後判斷之後還有沒有運算符,如果沒有就返回結果,如果還有就重複以上步驟。我們看到 expr 方法是嚴格按照語法圖進行解釋的。

執行結果

cal> 1+2
3
cal> 1+4-3
2
cal> 1+ 5 -6
0
cal> 1 +
Traceback (most recent call last):
  File "calc3.py", line 176, in <module>
    main()
  File "calc3.py", line 172, in main
    result = interpreter.expr()
  File "calc3.py", line 155, in expr
    result += self.term()
  File "calc3.py", line 136, in term
    self.eat(INTEGER)
  File "calc3.py", line 131, in eat
    self.error()
  File "calc3.py", line 54, in error
    raise Exception('Error Parsing Error!')
Exception: Error Parsing Error!

文法

文法是另一種被廣泛使用的、用於指定一種程序設計語言語法的表示法,它叫做『上下文無關文法』,簡稱文法。

爲什麼要使用文法?

  • 文法以更簡明的方式說明一種程序設計語言的語法。比起來語法圖,文法十分簡潔。
  • 文法可以用作很好的文檔
  • 文法可以非常方便的轉換成代碼(非常方便~)

文法原理

我們以『3 * 4 / 5 * 2』這樣的算數乘除表達式爲例,它對應的文法表達式爲:

expr: factor ((MUL | DIV) factor)*

factor: INTEGER

一段文法由一系列的規則(rule)組成,在上述文法中,我們有兩條規則:expr: factor ((MUL | DIV) factor)*factor: INTEGER

一條規則由由一個非終結符(稱規則的頭或者左邊)一個冒號:一系列終結符非終結符(稱規則的主體或者右邊)組成

分析以上文法:

  • exprfactor這樣的變量叫做非終結符
  • MULDIVInteger這樣的標記稱爲終結符
  • | 表示『或』,多選一。(MUL | DIV)表示要麼 MUL(乘)要麼 DIV(除)
  • ( ) 一對圓括號表示把終結符非終結符組合起來,就像 (MUL | DIV) 一樣
  • ( )* 表示匹配組合裏面的內容 0 次或者多次,就像 while 循環

解釋一下 expr 規則:
expr 可以是一個 factor 可選地接着一個乘法或者除法運算符,再接着另一個 factor,依次可選地接着一個乘法或者除法運算符,再接着另一個 factor……

現在我們以解釋『3 * 4 / 5 * 2』這樣的算數乘除表達式爲例:

expr
factor ((MUL | DIV) factor)*
factor MUL factor ((MUL | DIV) factor)*
factor MUL factor DIV factor ((MUL | DIV) factor)*
factor MUL factor DIV factor MUL factor
INTEGER MUL INTEGER DIV INTEGER MUL INTEGER
 3       *   4       /   5       *   2

將文法映射稱爲爲代碼

  • 對於文法中定義的每一條規則 R,都可以在代碼中變成同名的方法 R()
  • 對於多個可選項 ( | ) 可以編程爲 if-else 語句
  • 可選的 ( )* 集合編程稱爲 while 語句,表示可以循環 0~n 次

因此,上述文法用代碼表示出來就是:

  • factor 方法
def factor(self):
    # return an Integer Token's value
    # factor: Integer
    token = self.current_token
    self.eat(INTEGER)
    return token.value
  • expr 方法
def expr(self):
    # Arithmetic expression parser
    # expr: factor( (MUL | DIV) factor)*
    # factor: Integer

    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

擴充我們的文法

加入加減運算

因爲,我們加入了加減運算,因此現在存在運算優先級問題,很顯然乘除運算優先級要高於加減運算

我們得到以下的優先級表格:

操作符 優先級 結合性
    -
2 左結合
    /
1 左結合

根據優先級表格構建文法:

  • 爲每個優先級定義一個非終結符。非終結符所在產生式的主體應該包含同等級的算術運算符和優先級高一級的非終結符
  • 爲表達式創建一個額外的非終結符 factor 作爲基本單位,在我們的例子中該基本單位是整數。通用的規則是如果你有 N 層優先級,那麼你總共需要 N + 1 個非終結符:每層優先級需要一個非終結符,加上一個用作表達式基本單位的非終結符。

更新我們的文法:

expr: term ((PLUS | MINUS) term)*

term: factor ((MUL | DIV) factor)*

factor: INTEGER

加入括號表達式

在之前,我們的基本單位只有 INTEGER,這次我們加入括號表達式。

更新後的文法爲:

expr: term ((PLUS | MINUS) term)*

term: factor ((MUL | DIV) factor)*

factor: INTEGER | LPAREN expr RPAREN

  • LPAREN 表示左括號,RPAREN 表示右括號
  • 括號表達式內的非終結符 expr 表示 expr 規則

下面我們以『 3 * (1 + 5)』爲例,來說明該文法是如何工作的:

image

最終的四則運算解釋器

# Token Types:
# EOF: End-Of-File
INTEGER, PLUS, MINUS, MUL, DIV, LPAREN, RPAREN, EOF = (
    'INTEGER', 'PLUS', 'MINUS', 'MUL', 'DIV', '(', ')', 'EOF'
)
class Token(object):
    def __init__(self, type, value):
        # Token types: INTEGER, PLUS, MINUS, MUL, DIV, EOF
        self.type = type
        # Token value: 0,1,2,3,4,5,6,7,8,9,,+,-,*,/,None
        self.value = value

    def __str__(self):
        """
        The format of the print infomation
        For examle:
        Token(INTEGER, 3)
        Token(PLUS, '+')
        Token(MINUS, '-')
        TOKEN(MUL, '*')
        TOEKN(LPAREN, ')')
        """
        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):
        # Process the whole input
        # e.g. 3+ 5 * 2 - 4/2
        self.text = text
        self.pos = 0
        self.current_char = self.text[self.pos]

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

    def advance(self):
        # Advance the pos and set current_char
        self.pos += 1

        if self.pos > len(self.text)-1:
            self.current_char = None
        else:
            self.current_char = self.text[self.pos]

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

    def integer(self):
        # Support mutidigit integer
        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:
        Parsing the input into tokens.
        Strategy:
        1. dictate current char
        2. if it is a space, advance the pos
        3. if it is a digit, then convert it to integer and return INTEGER token
        4. if it is a '+', then return PLUS token
        5. if it is a '-', then return MINUS token
        6. if it is a '*', then return MUL token
        7. if it is a '/', then return DIV token
        8. if it is a '(', then return LPAREN token
        9. if it is a ')', then return RPAREN token
        """

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

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

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

            self.error()

        return Token(EOF, None)

class Interpreter(object):
    def __init__(self, lexer):
        self.lexer = lexer
        self.current_token = self.lexer.get_next_token()

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

    def eat(self, token_type):
        # compare the current token with the passed token type
        # if they match then eat the current token and assign 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 | LPAREN expr RPAREN
        token = self.current_token

        if token.type == INTEGER:
            self.eat(INTEGER)
            return token.value

        if token.type == LPAREN:
            self.eat(LPAREN)
            result = self.expr()
            self.eat(RPAREN)
            return result

    def term(self):
        # term: factor( (MUL | DIV) factor)*
        # factor: Integer
        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):
        # 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:
            text = raw_input('cal> ')
        except EOFError:
            break
        if not text:
            continue

        lexer = Lexer(text)
        interpreter = Interpreter(lexer)
        result = interpreter.expr()
        print(result)

if __name__ == '__main__':
    main()

運行結果

cal> 1+1
2
cal> 1*3
3
cal> 1+3*3
10
cal> 5*(1+5)
30
cal> (5+6)/(3-1)*2
10

參考資料


本文的版權歸作者 羅遠航 所有,採用 Attribution-NonCommercial 3.0 License。任何人可以進行轉載、分享,但不可在未經允許的情況下用於商業用途;轉載請註明出處。感謝配合!

發佈了105 篇原創文章 · 獲贊 237 · 訪問量 28萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章