作者 | Ruslan Spivak
譯者 | 周家未 (BriFuture) ? ? ? 共計翻譯:21 篇 貢獻時間:611 天
“如果你不知道編譯器是怎麼工作的,那你就不知道電腦是怎麼工作的。如果你不能百分百確定,那就是不知道它們是如何工作的。” –Steve Yegge
就是這樣。想一想。你是萌新還是一個資深的軟體開發者實際上都無關緊要:如果你不知道編譯器和直譯器是怎麼工作的,那麼你就不知道電腦是怎麼工作的。就這麼簡單。
所以,你知道編譯器和直譯器是怎麼工作的嗎?我是說,你百分百確定自己知道他們怎麼工作嗎?如果不知道。
或者如果你不知道但你非常想要瞭解它。
不用擔心。如果你能堅持跟著這個系列做下去,和我一起構建一個直譯器和編譯器,最後你將會知道他們是怎麼工作的。並且你會變成一個自信滿滿的快樂的人。至少我希望如此。
為什麼要學習編譯器和直譯器?有三點理由。
好,但什麼是直譯器和編譯器?
直譯器 和 編譯器 的任務是把用高階語言寫的源程式翻譯成其他的格式。很奇怪,是不是?忍一忍,稍後你會在這個系列學到到底把源程式翻譯成什麼東西。
這時你可能會奇怪直譯器和編譯器之間有什麼區別。為了實現這個系列的目的,我們規定一下,如果有個翻譯器把源程式翻譯成機器語言,那它就是 編譯器。如果一個翻譯器可以處理並執行源程式,卻不用把它翻譯器機器語言,那它就是 直譯器。直觀上它看起來像這樣:
我希望你現在確信你很想學習構建一個編譯器和直譯器。你期望在這個教程裡學習直譯器的哪些知識呢?
你看這樣如何。你和我一起為 Pascal[1] 語言的一個大子集做一個簡單的直譯器。在這個系列結束的時候你能做出一個可以執行的 Pascal 直譯器和一個像 Python 的 pdb[2] 那樣的原始碼級別的除錯器。
你或許會問,為什麼是 Pascal?一方面,它不是我為了這個系列而提出的一個虛構的語言:它是真實存在的一門程式語言,有很多重要的語言結構。有些陳舊但有用的計算機書籍使用 Pascal 程式語言作為示例(我知道對於選擇一門語言來構建直譯器,這個理由並不令人信服,但我認為學一門非主流的語言也不錯 :))。
這有個 Pascal 中的階乘函式示例,你將能用自己的直譯器解釋程式碼,還能夠用可互動的原始碼級除錯器進行除錯,你可以這樣創造:
program factorial;
function factorial(n: integer): longint;
begin
if n = 0 then
factorial := 1
else
factorial := n * factorial(n - 1);
end;
var
n: integer;
begin
for n := 0 to 16 do
writeln(n, '! = ', factorial(n));
end.
這個 Pascal 直譯器的實現語言會使用 Python,但你也可以用其他任何語言,因為這裡展示的思想不依賴任何特殊的實現語言。好,讓我們開始幹活。準備好了,出發!
你會從編寫一個簡單的算術運算式解析器,也就是常說的計算器,開始學習直譯器和編譯器。今天的標的非常簡單:讓你的計算器能處理兩個個位數相加,比如 3+5
。下麵是你的計算器的原始碼——不好意思,是直譯器:
# 標記型別
#
# EOF (end-of-file 檔案末尾)標記是用來表示所有輸入都解析完成
INTEGER, PLUS, EOF = 'INTEGER', 'PLUS', 'EOF'
class Token(object):
def __init__(self, type, value):
# token 型別: INTEGER, PLUS, MINUS, or EOF
self.type = type
# token 值: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, '+', 或 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):
# 使用者輸入字串, 例如 "3+5"
self.text = text
# self.pos 是 self.text 的索引
self.pos = 0
# 當前標記實體
self.current_token = None
def error(self):
raise Exception('Error parsing input')
def get_next_token(self):
"""詞法分析器(也說成掃描器或者標記器)
該方法負責把一個句子分成若干個標記。每次處理一個標記
"""
text = self.text
# self.pos 索引到達了 self.text 的末尾嗎?
# 如果到了,就傳回 EOF 標記,因為沒有更多的
# 能轉換成標記的輸入了
if self.pos > len(text) - 1:
return Token(EOF, None)
# 從 self.pos 位置獲取當前的字元,
# 基於單個字元判斷要生成哪種標記
current_char = text[self.pos]
# 如果字元是一個數字,就把他轉換成一個整數,生成一個 INTEGER # 標記,累加 self.pos 索引,指向數字後面的下一個字元,
# 並傳回 INTEGER 標記
if current_char.isdigit():
token = Token(INTEGER, int(current_char))
self.pos += 1
return token
if current_char == '+':
token = Token(PLUS, current_char)
self.pos += 1
return token
self.error()
def eat(self, token_type):
# 將當前的標記型別與傳入的標記型別作比較,如果他們相匹配,就
# “eat” 掉當前的標記並將下一個標記賦給 self.current_token,
# 否則丟擲一個異常
if self.current_token.type == token_type:
self.current_token = self.get_next_token()
else:
self.error()
def expr(self):
"""expr -> INTEGER PLUS INTEGER"""
# 將輸入中的第一個標記設定成當前標記
self.current_token = self.get_next_token()
# 我們期望當前標記是個位數。
left = self.current_token
self.eat(INTEGER)
# 期望當前標記是 ‘+’ 號
op = self.current_token
self.eat(PLUS)
# 我們期望當前標記是個位數。
right = self.current_token
self.eat(INTEGER)
# 上述操作完成後,self.current_token 被設成 EOF 標記
# 這時成功找到 INTEGER PLUS INTEGER 標記序列
# 這個方法就可以傳回兩個整數相加的結果了,
# 即高效的解釋了使用者輸入
result = left.value + right.value
return result
def main():
while True:
try:
# 要在 Python3 下執行,請把 ‘raw_input’ 換成 ‘input’
text = raw_input('calc> ')
except EOFError:
break
if not text:
continue
interpreter = Interpreter(text)
result = interpreter.expr()
print(result)
if __name__ == '__main__':
main()
把上面的程式碼儲存到 calc1.py
檔案,或者直接從 GitHub[3] 上下載。在你深入研究程式碼前,在命令列裡面執行它看看效果。試一試!這是我筆記本上的示例會話(如果你想在 Python3 下執行,你要把 raw_input
換成 input
):
$ python calc1.py
calc> 3+4
7
calc> 3+5
8
calc> 3+9
12
calc>
要讓你的簡易計算器正常工作,不丟擲異常,你的輸入要遵守以下幾個規則:
要讓計算器變得簡單,這些限制非常必要。不用擔心,你很快就會讓它變得很複雜。
好,現在讓我們深入它,看看直譯器是怎麼工作,它是怎麼評估出算術運算式的。
當你在命令列中輸入一個運算式 3+5
,直譯器就獲得了字串 “3+5”。為了讓直譯器能夠真正理解要用這個字串做什麼,它首先要把輸入 “3+5” 分到叫做 token
(標記)的容器裡。標記 是一個擁有型別和值的物件。比如說,對字元 “3” 而言,標記的型別是 INTEGER 整數,對應的值是 3。
把輸入字串分成標記的過程叫詞法分析。因此直譯器的需要做的第一步是讀取輸入字元,並將其轉換成標記流。直譯器中的這一部分叫做詞法分析器,或者簡短點叫 lexer。你也可以給它起別的名字,諸如掃描器或者標記器。它們指的都是同一個東西:直譯器或編譯器中將輸入字元轉換成標記流的那部分。
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 標記:
自己試一試,看看計算器裡的詞法分析器的執行:
>>> from calc1 import Interpreter
>>>
>>> interpreter = Interpreter('3+5')
>>> interpreter.get_next_token()
Token(INTEGER, 3)
>>>
>>> interpreter.get_next_token()
Token(PLUS, '+')
>>>
>>> interpreter.get_next_token()
Token(INTEGER, 5)
>>>
>>> interpreter.get_next_token()
Token(EOF, None)
>>>
既然你的直譯器能夠從輸入字元中獲取標記流,直譯器需要對它做點什麼:它需要在詞法分析器 get_next_token
中獲取的標記流中找出相應的結構。你的直譯器應該能夠找到流中的結構:INTEGER -> PLUS -> INTEGER。就是這樣,它嘗試找出標記的序列:整數後面要跟著加號,加號後面要跟著整數。
負責找出並解釋結構的方法就是 expr
。該方法檢驗標記序列確實與期望的標記序列是對應的,比如 INTEGER -> PLUS -> INTEGER。成功確認了這個結構後,就會生成加號左右兩邊的標記的值相加的結果,這樣就成功解釋你輸入到直譯器中的算術運算式了。
expr
方法用了一個助手方法 eat
來檢驗傳入的標記型別是否與當前的標記型別相匹配。在匹配到傳入的標記型別後,eat
方法會獲取下一個標記,並將其賦給 current_token
變數,然後高效地 “吃掉” 當前匹配的標記,並將標記流的虛擬指標向後移動。如果標記流的結構與期望的 INTEGER -> PLUS -> INTEGER 標記序列不對應,eat
方法就丟擲一個異常。
讓我們回顧下直譯器做了什麼來對算術運算式進行評估的:
expr
方法,在詞法分析器 get_next_token
傳回的標記流中找出結構。這個結構就是 INTEGER -> PLUS -> INTEGER 這樣的格式。在確認了格式後,它就透過把兩個整型標記相加來解釋輸入,因為此時對於直譯器來說很清楚,它要做的就是把兩個整數 3 和 5 進行相加。恭喜。你剛剛學習了怎麼構建自己的第一個直譯器!
現在是時候做練習了。
看了這篇文章,你肯定覺得不夠,是嗎?好,準備好做這些練習:
檢驗你的理解
在結束本文前,我衷心希望你能留下學習直譯器和編譯器的承諾。並且現在就開始做。不要把它留到以後。不要拖延。如果你已經看完了本文,就開始吧。如果已經仔細看完了但是還沒做什麼練習 —— 現在就開始做吧。如果已經開始做練習了,那就把剩下的做完。你懂得。而且你知道嗎?簽下承諾書,今天就開始學習直譯器和編譯器!
本人, ______,身體健全,思想正常,在此承諾從今天開始學習直譯器和編譯器,直到我百分百瞭解它們是怎麼工作的!
簽字人:
日期:
簽字,寫上日期,把它放在你每天都能看到的地方,確保你能堅守承諾。謹記你的承諾:
“承諾就是,你說自己會去做的事,在你說完就一直陪著你的東西。” —— Darren Hardy
好,今天的就結束了。這個系列的下一篇文章裡,你將會擴充套件自己的計算器,讓它能夠處理更複雜的算術運算式。敬請期待。
via: https://ruslanspivak.com/lsbasi-part1/
作者:Ruslan Spivak[5] 譯者:BriFuture 校對:wxy
本文由 LCTT 原創編譯,Linux中國 榮譽推出