歡迎光臨
每天分享高質量文章

讓我們做個簡單的直譯器(二) | Linux 中國

讓我們再次深入瞭解直譯器和編譯器。
— Ruslan Spivak


致謝
編譯自 | https://ruslanspivak.com/lsbasi-part2/ 
 作者 | Ruslan Spivak
 譯者 | 周家未 (BriFuture) ? ? ? 共計翻譯:21 篇 貢獻時間:611 天

在一本叫做 《高效思考的 5 要素》 的書中,作者 Burger 和 Starbird 講述了一個關於他們如何研究 Tony Plog 的故事,他是一位舉世聞名的交響曲名家,為一些有才華的演奏者開創了一個大師班。這些學生一開始演奏複雜的樂曲,他們演奏的非常好。然後他們被要求演奏非常基礎簡單的樂曲。當他們演奏這些樂曲時,與之前所演奏的相比,聽起來非常幼稚。在他們結束演奏後,老師也演奏了同樣的樂曲,但是聽上去非常嫻熟。差別令人震驚。Tony 解釋道,精通簡單音符可以讓人更好的掌握複雜的部分。這個例子很清晰 —— 要成為真正的名家,必須要掌握簡單基礎的思想。

故事中的例子明顯不僅僅適用於音樂,而且適用於軟體開發。這個故事告訴我們不要忽視繁瑣工作中簡單基礎的概念的重要性,哪怕有時候這讓人感覺是一種倒退。儘管熟練掌握一門工具或者框架非常重要,瞭解它們背後的原理也是極其重要的。正如 Palph Waldo Emerson 所說:

“如果你只學習方法,你就會被方法束縛。但如果你知道原理,就可以發明自己的方法。”

有鑒於此,讓我們再次深入瞭解直譯器和編譯器。

今天我會向你們展示一個全新的計算器,與 第一部分[1] 相比,它可以做到:

☉ 處理輸入字串任意位置的空白符
☉ 識別輸入字串中的多位整數
☉ 做兩個整數之間的減法(目前它僅能加減整數)

新版本計算器的原始碼在這裡,它可以做到上述的所有事情:

  1. # 標記型別

  2. # EOF end-of-file 檔案末尾)標記是用來表示所有輸入都解析完成

  3. INTEGER, PLUS, MINUS, EOF = 'INTEGER', 'PLUS', 'MINUS', 'EOF'

  4. class Token(object):

  5.    def __init__(self, type, value):

  6.        # token 型別: INTEGER, PLUS, MINUS, or EOF

  7.        self.type = type

  8.        # token 值: 非負整數值, '+', '-', 或無

  9.        self.value = value

  10.    def __str__(self):

  11.        """String representation of the class instance.

  12.        Examples:

  13.            Token(INTEGER, 3)

  14.            Token(PLUS '+')

  15.        """

  16.        return 'Token({type}, {value})'.format(

  17.            type=self.type,

  18.            value=repr(self.value)

  19.        )

  20.    def __repr__(self):

  21.        return self.__str__()

  22. class Interpreter(object):

  23.    def __init__(self, text):

  24.        # 客戶端字元輸入, 例如. "3 + 5", "12 - 5",

  25.        self.text = text

  26.        # self.pos self.text 的索引

  27.        self.pos = 0

  28.        # 當前標記實體

  29.        self.current_token = None

  30.        self.current_char = self.text[self.pos]

  31.    def error(self):

  32.        raise Exception('Error parsing input')

  33.    def advance(self):

  34.        """Advance the 'pos' pointer and set the 'current_char' variable."""

  35.        self.pos += 1

  36.        if self.pos > len(self.text) - 1:

  37.            self.current_char = None  # Indicates end of input

  38.        else:

  39.            self.current_char = self.text[self.pos]

  40.    def skip_whitespace(self):

  41.        while self.current_char is not None and self.current_char.isspace():

  42.            self.advance()

  43.    def integer(self):

  44.        """Return a (multidigit) integer consumed from the input."""

  45.        result = ''

  46.        while self.current_char is not None and self.current_char.isdigit():

  47.            result += self.current_char

  48.            self.advance()

  49.        return int(result)

  50.    def get_next_token(self):

  51.        """Lexical analyzer (also known as scanner or tokenizer)

  52.        This method is responsible for breaking a sentence

  53.        apart into tokens.

  54.        """

  55.        while self.current_char is not None:

  56.            if self.current_char.isspace():

  57.                self.skip_whitespace()

  58.                continue

  59.            if self.current_char.isdigit():

  60.                return Token(INTEGER, self.integer())

  61.            if self.current_char == '+':

  62.                self.advance()

  63.                return Token(PLUS, '+')

  64.            if self.current_char == '-':

  65.                self.advance()

  66.                return Token(MINUS, '-')

  67.            self.error()

  68.        return Token(EOF, None)

  69.    def eat(self, token_type):

  70.        # 將當前的標記型別與傳入的標記型別作比較,如果他們相匹配,就

  71.        # eat 掉當前的標記並將下一個標記賦給 self.current_token

  72.        # 否則丟擲一個異常

  73.        if self.current_token.type == token_type:

  74.            self.current_token = self.get_next_token()

  75.        else:

  76.            self.error()

  77.    def expr(self):

  78.        """Parser / Interpreter

  79.        expr -> INTEGER PLUS INTEGER

  80.        expr -> INTEGER MINUS INTEGER

  81.        """

  82.        # 將輸入中的第一個標記設定成當前標記

  83.        self.current_token = self.get_next_token()

  84.        # 當前標記應該是一個整數

  85.        left = self.current_token

  86.        self.eat(INTEGER)

  87.        # 當前標記應該是 ‘+’ ‘-’

  88.        op = self.current_token

  89.        if op.type == PLUS:

  90.            self.eat(PLUS)

  91.        else:

  92.            self.eat(MINUS)

  93.        # 當前標記應該是一個整數

  94.        right = self.current_token

  95.        self.eat(INTEGER)

  96.        # 在上述函式呼叫後,self.current_token 就被設為 EOF 標記

  97.        # 這時要麼是成功地找到 INTEGER PLUS INTEGER,要麼是 INTEGER MINUS INTEGER

  98.        # 序列的標記,並且這個方法可以僅僅傳回兩個整數的加或減的結果,就能高效解釋客戶端的輸入

  99.        if op.type == PLUS:

  100.            result = left.value + right.value

  101.        else:

  102.            result = left.value - right.value

  103.        return result

  104. def main():

  105.    while True:

  106.        try:

  107.            # To run under Python3 replace 'raw_input' call

  108.            # with 'input'

  109.            text = raw_input('calc> ')

  110.        except EOFError:

  111.            break

  112.        if not text:

  113.            continue

  114.        interpreter = Interpreter(text)

  115.        result = interpreter.expr()

  116.        print(result)

  117. if __name__ == '__main__':

  118.    main()

把上面的程式碼儲存到 calc2.py 檔案中,或者直接從 GitHub[2] 上下載。試著執行它。看看它是不是正常工作:它應該能夠處理輸入中任意位置的空白符;能夠接受多位的整數,並且能夠對兩個整數做減法和加法。

這是我在自己的筆記本上執行的示例:

  1. $ python calc2.py

  2. calc> 27 + 3

  3. 30

  4. calc> 27 - 7

  5. 20

  6. calc>

與 第一部分[1] 的版本相比,主要的程式碼改動有:

☉ get_next_token 方法重寫了很多。增加指標位置的邏輯之前是放在一個單獨的方法中。
☉ 增加了一些方法:skip_whitespace 用於忽略空白字元,integer 用於處理輸入字元的多位整數。
☉ expr 方法修改成了可以識別 “整數 -> 減號 -> 整數” 片語和 “整數 -> 加號 -> 整數” 片語。在成功識別相應的片語後,這個方法現在可以解釋加法和減法。

第一部分[1] 中你學到了兩個重要的概念,叫做 標記token 和詞法分析lexical analyzer。現在我想談一談詞法lexeme、 解析parsing 和解析器parser

你已經知道了標記。但是為了讓我詳細的討論標記,我需要談一談詞法。詞法是什麼?詞法lexeme是一個標記token中的字元序列。在下圖中你可以看到一些關於標記的例子,這可以讓它們之間的關係變得清晰:

現在還記得我們的朋友,expr 方法嗎?我之前說過,這是數學運算式實際被解釋的地方。但是你要先識別這個運算式有哪些片語才能解釋它,比如它是加法還是減法。expr 方法最重要的工作是:它從 get_next_token 方法中得到流,並找出該標記流的結構,然後解釋已經識別出的片語,產生數學運算式的結果。

在標記流中找出結構的過程,或者換種說法,識別標記流中的片語的過程就叫解析parsing。直譯器或者編譯器中執行這個任務的部分就叫做解析器parser

現在你知道 expr 方法就是你的直譯器的部分,解析parsing解釋interpreting都在這裡發生 —— expr 方法首先嘗試識別(解析)標記流裡的 “整數 -> 加法 -> 整數” 或者 “整數 -> 減法 -> 整數” 片語,成功識別後 (解析了) 其中一個片語,這個方法就開始解釋它,傳回兩個整數的和或差。

又到了練習的時間。

☉ 擴充套件這個計算器,讓它能夠計算兩個整數的乘法
☉ 擴充套件這個計算器,讓它能夠計算兩個整數的除法
☉ 修改程式碼,讓它能夠解釋包含了任意數量的加法和減法的運算式,比如 “9 - 5 + 3 + 11”

檢驗你的理解:

☉ 詞法是什麼?
☉ 找出標記流結構的過程叫什麼,或者換種說法,識別標記流中一個片語的過程叫什麼?
☉ 直譯器(編譯器)執行解析的部分叫什麼?

希望你喜歡今天的內容。在該系列的下一篇文章裡你就能擴充套件計算器從而處理更多複雜的算術運算式。敬請期待。


via: https://ruslanspivak.com/lsbasi-part2/

作者:Ruslan Spivak[4] 譯者:BriFuture 校對:wxy

本文由 LCTT 原創編譯,Linux中國 榮譽推出

贊(0)

分享創造快樂

© 2024 知識星球   網站地圖