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

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

“如果你不知道編譯器是怎麼工作的,那你就不知道電腦是怎麼工作的。如果你不能百分百確定,那就是不知道它們是如何工作的。”
— Ruslan Spivak


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

“如果你不知道編譯器是怎麼工作的,那你就不知道電腦是怎麼工作的。如果你不能百分百確定,那就是不知道它們是如何工作的。” –Steve Yegge

就是這樣。想一想。你是萌新還是一個資深的軟體開發者實際上都無關緊要:如果你不知道編譯器compiler直譯器interpreter是怎麼工作的,那麼你就不知道電腦是怎麼工作的。就這麼簡單。

所以,你知道編譯器和直譯器是怎麼工作的嗎?我是說,你百分百確定自己知道他們怎麼工作嗎?如果不知道。

或者如果你不知道但你非常想要瞭解它。

不用擔心。如果你能堅持跟著這個系列做下去,和我一起構建一個直譯器和編譯器,最後你將會知道他們是怎麼工作的。並且你會變成一個自信滿滿的快樂的人。至少我希望如此。

為什麼要學習編譯器和直譯器?有三點理由。

☉ 要寫出一個直譯器或編譯器,你需要有很多的專業知識,並能融會貫通。寫一個直譯器或編譯器能幫你加強這些能力,成為一個更厲害的軟體開發者。而且,你要學的技能對編寫軟體非常有用,而不是僅僅侷限於直譯器或編譯器。
☉ 你確實想要瞭解電腦是怎麼工作的。通常直譯器和編譯器看上去很魔幻。你或許不習慣這種魔力。你會想去揭開構建直譯器和編譯器那層神秘的面紗,瞭解它們的原理,把事情做好。
☉ 你想要建立自己的程式語言或者特定領域的語言。如果你建立了一個,你還要為它建立一個直譯器或者編譯器。最近,興起了對新的程式語言的興趣。你能看到幾乎每天都有一門新的程式語言橫空出世:Elixir,Go,Rust,還有很多。

好,但什麼是直譯器和編譯器?

直譯器 和 編譯器 的任務是把用高階語言寫的源程式翻譯成其他的格式。很奇怪,是不是?忍一忍,稍後你會在這個系列學到到底把源程式翻譯成什麼東西。

這時你可能會奇怪直譯器和編譯器之間有什麼區別。為了實現這個系列的目的,我們規定一下,如果有個翻譯器把源程式翻譯成機器語言,那它就是 編譯器。如果一個翻譯器可以處理並執行源程式,卻不用把它翻譯器機器語言,那它就是 直譯器。直觀上它看起來像這樣:

我希望你現在確信你很想學習構建一個編譯器和直譯器。你期望在這個教程裡學習直譯器的哪些知識呢?

你看這樣如何。你和我一起為 Pascal[1] 語言的一個大子集做一個簡單的直譯器。在這個系列結束的時候你能做出一個可以執行的 Pascal 直譯器和一個像 Python 的 pdb[2] 那樣的原始碼級別的除錯器。

你或許會問,為什麼是 Pascal?一方面,它不是我為了這個系列而提出的一個虛構的語言:它是真實存在的一門程式語言,有很多重要的語言結構。有些陳舊但有用的計算機書籍使用 Pascal 程式語言作為示例(我知道對於選擇一門語言來構建直譯器,這個理由並不令人信服,但我認為學一門非主流的語言也不錯 :))。

這有個 Pascal 中的階乘函式示例,你將能用自己的直譯器解釋程式碼,還能夠用可互動的原始碼級除錯器進行除錯,你可以這樣創造:

  1. program factorial;

  2. function factorial(n: integer): longint;

  3. begin

  4.    if n = 0 then

  5.        factorial := 1

  6.    else

  7.        factorial := n * factorial(n - 1);

  8. end;

  9. var

  10.    n: integer;

  11. begin

  12.    for n := 0 to 16 do

  13.        writeln(n, '! = ', factorial(n));

  14. end.

這個 Pascal 直譯器的實現語言會使用 Python,但你也可以用其他任何語言,因為這裡展示的思想不依賴任何特殊的實現語言。好,讓我們開始幹活。準備好了,出發!

你會從編寫一個簡單的算術運算式解析器,也就是常說的計算器,開始學習直譯器和編譯器。今天的標的非常簡單:讓你的計算器能處理兩個個位數相加,比如 3+5。下麵是你的計算器的原始碼——不好意思,是直譯器:

  1. # 標記型別

  2. #

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

  4. INTEGER, PLUS, EOF = 'INTEGER', 'PLUS', 'EOF'

  5. class Token(object):

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

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

  8.        self.type = type

  9.        # token 值: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, '+', None

  10.        self.value = value

  11.    def __str__(self):

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

  13.        Examples:

  14.            Token(INTEGER, 3)

  15.            Token(PLUS '+')

  16.        """

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

  18.            type=self.type,

  19.            value=repr(self.value)

  20.        )

  21.    def __repr__(self):

  22.        return self.__str__()

  23. class Interpreter(object):

  24.    def __init__(self, text):

  25.        # 使用者輸入字串, 例如 "3+5"

  26.        self.text = text

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

  28.        self.pos = 0

  29.        # 當前標記實體

  30.        self.current_token = None

  31.    def error(self):

  32.        raise Exception('Error parsing input')

  33.    def get_next_token(self):

  34.        """詞法分析器(也說成掃描器或者標記器)

  35.        該方法負責把一個句子分成若干個標記。每次處理一個標記

  36.        """

  37.        text = self.text

  38.        # self.pos 索引到達了 self.text 的末尾嗎?

  39.        # 如果到了,就傳回 EOF 標記,因為沒有更多的

  40.        # 能轉換成標記的輸入了

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

  42.            return Token(EOF, None)

  43.        # self.pos 位置獲取當前的字元,

  44.        # 基於單個字元判斷要生成哪種標記

  45.        current_char = text[self.pos]

  46.        # 如果字元是一個數字,就把他轉換成一個整數,生成一個 INTEGER # 標記,累加 self.pos 索引,指向數字後面的下一個字元,

  47.        # 並傳回 INTEGER 標記

  48.        if current_char.isdigit():

  49.            token = Token(INTEGER, int(current_char))

  50.            self.pos += 1

  51.            return token

  52.        if current_char == '+':

  53.            token = Token(PLUS, current_char)

  54.            self.pos += 1

  55.            return token

  56.        self.error()

  57.    def eat(self, token_type):

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

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

  60.        # 否則丟擲一個異常

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

  62.            self.current_token = self.get_next_token()

  63.        else:

  64.            self.error()

  65.    def expr(self):

  66.        """expr -> INTEGER PLUS INTEGER"""

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

  68.        self.current_token = self.get_next_token()

  69.        # 我們期望當前標記是個位數。

  70.        left = self.current_token

  71.        self.eat(INTEGER)

  72.        # 期望當前標記是 ‘+’

  73.        op = self.current_token

  74.        self.eat(PLUS)

  75.        # 我們期望當前標記是個位數。

  76.        right = self.current_token

  77.        self.eat(INTEGER)

  78.        # 上述操作完成後,self.current_token 被設成 EOF 標記

  79.        # 這時成功找到 INTEGER PLUS INTEGER 標記序列

  80.        # 這個方法就可以傳回兩個整數相加的結果了,

  81.        # 即高效的解釋了使用者輸入

  82.        result = left.value + right.value

  83.        return result

  84. def main():

  85.    while True:

  86.        try:

  87.            # 要在 Python3 下執行,請把 raw_input 換成 input

  88.            text = raw_input('calc> ')

  89.        except EOFError:

  90.            break

  91.        if not text:

  92.            continue

  93.        interpreter = Interpreter(text)

  94.        result = interpreter.expr()

  95.        print(result)

  96. if __name__ == '__main__':

  97.    main()

把上面的程式碼儲存到 calc1.py 檔案,或者直接從 GitHub[3] 上下載。在你深入研究程式碼前,在命令列裡面執行它看看效果。試一試!這是我筆記本上的示例會話(如果你想在 Python3 下執行,你要把 raw_input 換成 input):

  1. $ python calc1.py

  2. calc> 3+4

  3. 7

  4. calc> 3+5

  5. 8

  6. calc> 3+9

  7. 12

  8. calc>

要讓你的簡易計算器正常工作,不丟擲異常,你的輸入要遵守以下幾個規則:

◈ 只允許輸入個位數
◈ 此時支援的唯一一個運運算元是加法
◈ 輸入中不允許有任何的空格符號

要讓計算器變得簡單,這些限制非常必要。不用擔心,你很快就會讓它變得很複雜。

好,現在讓我們深入它,看看直譯器是怎麼工作,它是怎麼評估出算術運算式的。

當你在命令列中輸入一個運算式 3+5,直譯器就獲得了字串 “3+5”。為了讓直譯器能夠真正理解要用這個字串做什麼,它首先要把輸入 “3+5” 分到叫做 token(標記)的容器裡。標記token 是一個擁有型別和值的物件。比如說,對字元 “3” 而言,標記的型別是 INTEGER 整數,對應的值是 3。

把輸入字串分成標記的過程叫詞法分析lexical analysis。因此直譯器的需要做的第一步是讀取輸入字元,並將其轉換成標記流。直譯器中的這一部分叫做詞法分析器lexical analyzer,或者簡短點叫 lexer。你也可以給它起別的名字,諸如掃描器scanner或者標記器tokenizer。它們指的都是同一個東西:直譯器或編譯器中將輸入字元轉換成標記流的那部分。

Interpreter 類中的 get_next_token 方法就是詞法分析器。每次呼叫它的時候,你都能從傳入直譯器的輸入字元中獲得建立的下一個標記。仔細看看這個方法,看看它是如何完成把字元轉換成標記的任務的。輸入被存在可變文字中,它儲存了輸入的字串和關於該字串的索引(把字串想象成字元陣列)。pos 開始時設為 0,指向字元 ‘3’。這個方法一開始檢查字元是不是數字,如果是,就將 pos 加 1,並傳回一個 INTEGER 型別的標記實體,並把字元 ‘3’ 的值設為整數,也就是整數 3:

現在 pos 指向文字中的 ‘+’ 號。下次呼叫這個方法的時候,它會測試 pos 位置的字元是不是個數字,然後檢測下一個字元是不是個加號,就是這樣。結果這個方法把 pos 加 1,傳回一個新建立的標記,型別是 PLUS,值為 ‘+’。

pos 現在指向字元 ‘5’。當你再呼叫 get_next_token 方法時,該方法會檢查這是不是個數字,就是這樣,然後它把 pos 加 1,傳回一個新的 INTEGER 標記,該標記的值被設為整數 5:

因為 pos 索引現在到了字串 “3+5” 的末尾,你每次呼叫 get_next_token 方法時,它將會傳回 EOF 標記:

自己試一試,看看計算器裡的詞法分析器的執行:

  1. >>> from calc1 import Interpreter

  2. >>>

  3. >>> interpreter = Interpreter('3+5')

  4. >>> interpreter.get_next_token()

  5. Token(INTEGER, 3)

  6. >>>

  7. >>> interpreter.get_next_token()

  8. Token(PLUS, '+')

  9. >>>

  10. >>> interpreter.get_next_token()

  11. Token(INTEGER, 5)

  12. >>>

  13. >>> interpreter.get_next_token()

  14. Token(EOF, None)

  15. >>>

既然你的直譯器能夠從輸入字元中獲取標記流,直譯器需要對它做點什麼:它需要在詞法分析器 get_next_token 中獲取的標記流中找出相應的結構。你的直譯器應該能夠找到流中的結構:INTEGER -> PLUS -> INTEGER。就是這樣,它嘗試找出標記的序列:整數後面要跟著加號,加號後面要跟著整數。

負責找出並解釋結構的方法就是 expr。該方法檢驗標記序列確實與期望的標記序列是對應的,比如 INTEGER -> PLUS -> INTEGER。成功確認了這個結構後,就會生成加號左右兩邊的標記的值相加的結果,這樣就成功解釋你輸入到直譯器中的算術運算式了。

expr 方法用了一個助手方法 eat 來檢驗傳入的標記型別是否與當前的標記型別相匹配。在匹配到傳入的標記型別後,eat 方法會獲取下一個標記,並將其賦給 current_token 變數,然後高效地 “吃掉” 當前匹配的標記,並將標記流的虛擬指標向後移動。如果標記流的結構與期望的 INTEGER -> PLUS -> INTEGER 標記序列不對應,eat 方法就丟擲一個異常。

讓我們回顧下直譯器做了什麼來對算術運算式進行評估的:

◈ 直譯器接受輸入字串,比如說 “3+5”
◈ 直譯器呼叫 expr 方法,在詞法分析器 get_next_token 傳回的標記流中找出結構。這個結構就是 INTEGER -> PLUS -> INTEGER 這樣的格式。在確認了格式後,它就透過把兩個整型標記相加來解釋輸入,因為此時對於直譯器來說很清楚,它要做的就是把兩個整數 3 和 5 進行相加。

恭喜。你剛剛學習了怎麼構建自己的第一個直譯器!

現在是時候做練習了。

看了這篇文章,你肯定覺得不夠,是嗎?好,準備好做這些練習:

☉ 修改程式碼,允許輸入多位數,比如 “12+3”
☉ 新增一個方法忽略空格符,讓你的計算器能夠處理帶有空白的輸入,比如 “12 + 3”
☉ 修改程式碼,用 ‘-’ 號而非 ‘+’ 號去執行減法比如 “7-5”

檢驗你的理解

☉ 什麼是直譯器?
☉ 什麼是編譯器
☉ 直譯器和編譯器有什麼差別?
☉ 什麼是標記?
☉ 將輸入分隔成若干個標記的過程叫什麼?
☉ 直譯器中進行詞法分析的部分叫什麼?
☉ 直譯器或編譯器中進行詞法分析的部分有哪些其他的常見名字?

在結束本文前,我衷心希望你能留下學習直譯器和編譯器的承諾。並且現在就開始做。不要把它留到以後。不要拖延。如果你已經看完了本文,就開始吧。如果已經仔細看完了但是還沒做什麼練習 —— 現在就開始做吧。如果已經開始做練習了,那就把剩下的做完。你懂得。而且你知道嗎?簽下承諾書,今天就開始學習直譯器和編譯器!

本人, ______,身體健全,思想正常,在此承諾從今天開始學習直譯器和編譯器,直到我百分百瞭解它們是怎麼工作的!

簽字人:

日期:

簽字,寫上日期,把它放在你每天都能看到的地方,確保你能堅守承諾。謹記你的承諾:

“承諾就是,你說自己會去做的事,在你說完就一直陪著你的東西。” —— Darren Hardy

好,今天的就結束了。這個系列的下一篇文章裡,你將會擴充套件自己的計算器,讓它能夠處理更複雜的算術運算式。敬請期待。


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

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

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

贊(0)

分享創造快樂

© 2024 知識星球   網站地圖