譯自:https://ruslanspivak.com/lsbasi-part1/
(已獲做者受權)python
「若是你不知道編譯器的工做方式,那麼你將不知道計算機的工做方式。若是你不能100%肯定是否知道編譯器的工做方式,那麼你將不知道它們的工做方式。」
——史蒂夫·耶格git
不管你是新手仍是經驗豐富的軟件開發人員,若是你不知道編譯器和解釋器的工做方式,那麼你也不知道計算機的工做方式,就是這麼簡單。
那麼,你知道編譯器和解釋器如何工做嗎? 你是否100%肯定知道它們的工做原理? 若是沒有的話:
github
或者,若是你確實不知道,而且你爲此感到不安的話:
express
別擔憂。若是你堅持不懈地學習本系列文章,並與我一塊兒實現解釋器和編譯器,你將最終了解它們是如何工做的。
編程
你爲何要學習解釋器和編譯器?我會給你三個理由。數組
一、要編寫解釋器或編譯器,你必須具備不少須要結合使用的技能。編寫解釋器或編譯器將幫助你提升這些技能,併成爲更好的軟件開發人員。一樣,你學到的技能對於編寫任何軟件(不只僅是解釋器或編譯器)都頗有用。
二、你想知道計算機如何工做。一般解釋器和編譯器看起來像魔術,你不該該對這種魔術感到滿意。你想揭露實現解釋器和編譯器的過程的神祕性,瞭解它們的工做方式並控制一切。
三、你要建立本身的編程語言或特定於某一領域的語言(domain specific language)。若是建立一個,則你還須要爲其建立解釋器或編譯器。最近,人們對新的編程語言從新產生了興趣。你幾乎能夠天天看到一種新的編程語言:Elixir,Go和Rust等。dom
好的,可是解釋器和編譯器是什麼?編程語言
解釋器或編譯器的目標是將某種高級語言的源程序轉換爲其餘形式。很模糊,不是嗎?請耐心等待,在本系列的後面部分,你將確切地瞭解源程序被翻譯成什麼。ide
此時,你可能還想知道解釋器和編譯器之間的區別是什麼。就本系列而言,若是咱們將源程序翻譯成機器語言,則它是編譯器。若是咱們在不先將其翻譯成機器語言的狀況下處理和執行源程序,則它就是解釋器。看起來像這樣:
函數
我但願到如今爲止,你已經確信要學習並實現解釋器和編譯器。
你和我將爲Pascal這門編程語言的大部分子集實現一個簡單的解釋器。在本系列的最後,你將擁有一個可運行的Pascal解釋器和一個源代碼調試器,例如Python的pdb。
你可能會問,爲何是Pascal?一方面,這不是我在本系列中提出的一種組合語言:它是一種真正的編程語言,具備許多重要的語言構造(language constructs),還有一些古老但有用的CS書籍在其示例中使用Pascal編程語言(我瞭解,這不是選擇咱們選擇實現Pascal解釋器的主要理由,但我認爲學習一門非主流(non-mainstream)的編程語言也是很好的:)
這是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.
咱們這裏將使用Python來實現Pascal解釋器,你也可使用任何所需的語言,由於實現解釋器的思路並不侷限於任何特定的編程語言。
好吧,讓咱們開始吧,預備,準備,開始!
咱們的首次嘗試是編寫簡單的算術表達式(arithmetic expressions)解釋器(也稱爲計算器),今天的目標很容易:使你的計算器可以處理個位數字的加法,好比3+5。 這是你的解釋器的源代碼:
# Token types # # EOF (end-of-file) token is used to indicate that # there is no more input left for lexical analysis INTEGER, PLUS, EOF = 'INTEGER', 'PLUS', 'EOF' class Token(object): def __init__(self, type, value): # token type: INTEGER, PLUS, or EOF self.type = type # token value: 0, 1, 2. 3, 4, 5, 6, 7, 8, 9, '+', 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" self.text = text # self.pos is an index into self.text self.pos = 0 # current token instance self.current_token = None def error(self): raise Exception('Error parsing input') 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. """ text = self.text # is self.pos index past the end of the self.text ? # if so, then return EOF token because there is no more # input left to convert into tokens if self.pos > len(text) - 1: return Token(EOF, None) # get a character at the position self.pos and decide # what token to create based on the single character current_char = text[self.pos] # if the character is a digit then convert it to # integer, create an INTEGER token, increment self.pos # index to point to the next character after the digit, # and return the INTEGER token 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): # 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 expr(self): """expr -> INTEGER PLUS INTEGER""" # set current token to the first token taken from the input self.current_token = self.get_next_token() # we expect the current token to be a single-digit integer left = self.current_token self.eat(INTEGER) # we expect the current token to be a '+' token op = self.current_token self.eat(PLUS) # we expect the current token to be a single-digit integer right = self.current_token self.eat(INTEGER) # after the above call the self.current_token is set to # EOF token # at this point INTEGER PLUS INTEGER sequence of tokens # has been successfully found and the method can just # return the result of adding two integers, thus # effectively interpreting client input result = left.value + right.value 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()
將以上代碼保存爲calc1.py,或直接從GitHub下載。 在開始深刻地研究代碼以前,請在命令行上運行並查看其運行狀況。
這是個人筆記本電腦上的一個運行效果(若是你使用的是Python3,則須要用input來替換raw_input):
$ python calc1.py calc> 3+4 7 calc> 3+5 8 calc> 3+9 12 calc>
爲了使你的簡單計算器正常工做而不會引起異常,你的輸入須要遵循某些規則:
一、輸入中僅容許一位數(single digit)的整數
二、目前惟一支持的算術運算是加法
三、輸入中的任何地方都不容許有空格
這些限制是使計算器簡單化所必需的。不用擔憂,你很快就會使它變得很是複雜。
好的,如今讓咱們深刻了解一下解釋器的工做原理以及它如何計算算術表達式。
在命令行上輸入表達式3+5時,解釋器將得到字符串"3+5"。爲了使解釋器真正理解如何處理該字符串,首先須要將輸入"3+5"分解爲Token。Token是具備類型(type)和值(value)的對象(object)。例如,對於字符"3",Token的類型將是INTEGER,而對應的值將是整數3。
將輸入字符串分解爲Token的過程稱爲詞法分析(lexical analysis)。所以,你的解釋器須要作的第一步是讀取字輸入字符並將其轉換爲Token流。解釋器執行此操做的部分稱爲詞法分析器(lexical analyzer),簡稱lexer。你可能還會遇到其餘的名稱,例如 scanner或者tokenizer,它們的含義都同樣:解釋器或編譯器中將字符輸入轉換爲Token流的部分。
Interpreter類的get_next_token函數是詞法分析器。每次調用它時,都會從字符輸入中得到下一個Token。讓咱們仔細看看這個函數,看看它如何完成將字符轉換爲Token。字符輸入存儲在text變量中,pos變量是該字符輸入的索引(index)(將字符串視爲字符數組)。 pos最初設置爲0,並指向字符"3"。函數首先檢查字符是否爲數字,若是是數字,則遞增pos並返回類型爲INTEGER、值爲整數3的Token:
pos如今指向文本中的"+"字符。下次調用該函數時,它將測試pos所指的字符是否爲數字,而後測試該字符是否爲加號,而後該函數遞增pos並返回一個新建立的Token,其類型爲PLUS,值爲"+":
pos如今指向字符"5"。當再次調用get_next_token函數時,將檢查它是否爲數字,以便遞增pos並返回一個新的Token,其類型爲INTEGER,值爲5:
如今pos索引已超過字符串"3+5"的末尾,若是再調用get_next_token函數的話,將返回一個類型爲EOF的Token:
試試看,親自看看計算器的詞法分析器如何工做:
>>> 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) >>>
所以,既然解釋器如今能夠訪問由輸入字符組成的Token流,那麼解釋器就須要對其進行處理:它須要在從Token流中查找結構(structure),解釋器但願在Token流中找到如下結構:
INTEGER-> PLUS-> INTEGER
也就是說,它嘗試查找Token序列:先是一個整數,後面跟加號,最後再跟一個整數。
負責查找和解釋該結構的函數爲expr。它驗證Token序列是否與預期的Token序列相對應,即INTEGER-> PLUS-> INTEGER。成功確認結構後,它會經過將PLUS左右兩側的Token的值相加來生成結果,從而成功解釋了傳遞給解釋器的算術表達式。
expr函數自己使用輔助函數(helper method)eat來驗證傳遞給eat函數的Token類型是否與當前正在處理的Token類型一致(match),在確保類型一致後,eat函數將獲取下一個Token並將其分配給current_token變量,從而有效地「消耗」已經驗證過的Token並在Token流中推動pos向前,若是Token流中的結構與預期的INTEGER PLUS INTEGER 序列不對應,那麼eat函數將引起異常。
讓咱們來回顧一下解釋器爲解析算術表達式所作的事情:
一、解釋器接受輸入字符串,例如"3+5"
二、解釋器調用expr函數以在詞法分析器get_next_token返回的Token流中找到預期的結構。它嘗試查找的結構的形式爲INTEGER PLUS INTEGER。查找到結構後,它就將輸入字符解釋爲把兩個類型爲INTEGER的Token的值加起來,也就是將兩個整數3和5相加。
恭喜你!剛剛學習瞭如何構實現你的第一個解釋器!
如今該作練習了:
一、修改代碼以容許輸入中包含多位數(multiple-digit)的整數,例如"12+3"
二、添加一種跳過空格的方法,以便計算器能夠處理帶有"12 + 3"之類帶有空格的字符輸入
三、修改代碼,使計算器可以處理減法,例如"7-5"
最後再來複習回憶一下:
一、什麼是解釋器?
二、什麼是編譯器?
三、解釋器和編譯器有什麼區別?
四、什麼是Token?
五、將輸入分解爲Token的過程的名稱是什麼?
六、解釋器中進行詞法分析的部分是什麼?
今天到這就結束了,在下一篇文章中,咱們將擴展計算器以處理更多的算術表達式,敬請關注。
PS: 這也是我第一次翻譯技術文章,若有錯誤和不恰當的地方,但願你們能及時批評指正,謝謝!