譯自:https://ruslanspivak.com/lsbasi-part1/
(已獲作者授權)
“如果你不知道編譯器的工作方式,那么你將不知道計算機的工作方式。如果你不能100%確定是否知道編譯器的工作方式,那么你將不知道它們的工作方式。”
——史蒂夫·耶格
無論你是新手還是經驗豐富的軟件開發人員,如果你不知道編譯器和解釋器的工作方式,那么你也不知道計算機的工作方式,就是這么簡單。
那么,你知道編譯器和解釋器如何工作嗎? 你是否100%確定知道它們的工作原理? 如果沒有的話:
或者,如果你確實不知道,並且你為此感到不安的話:
別擔心。如果你堅持不懈地學習本系列文章,並與我一起實現解釋器和編譯器,你將最終了解它們是如何工作的。
你為什么要學習解釋器和編譯器?我會給你三個理由。
1、要編寫解釋器或編譯器,你必須具有很多需要結合使用的技能。編寫解釋器或編譯器將幫助你提高這些技能,並成為更好的軟件開發人員。同樣,你學到的技能對於編寫任何軟件(不僅僅是解釋器或編譯器)都很有用。
2、你想知道計算機如何工作。通常解釋器和編譯器看起來像魔術,你不應該對這種魔術感到滿意。你想揭露實現解釋器和編譯器的過程的神秘性,了解它們的工作方式並控制一切。
3、你要創建自己的編程語言或特定於某一領域的語言(domain specific language)。如果創建一個,則你還需要為其創建解釋器或編譯器。最近,人們對新的編程語言重新產生了興趣。你幾乎可以每天看到一種新的編程語言:Elixir,Go和Rust等。
好的,但是解釋器和編譯器是什么?
解釋器或編譯器的目標是將某種高級語言的源程序轉換為其他形式。很模糊,不是嗎?請耐心等待,在本系列的后面部分,你將確切地了解源程序被翻譯成什么。
此時,你可能還想知道解釋器和編譯器之間的區別是什么。就本系列而言,如果我們將源程序翻譯成機器語言,則它是編譯器。如果我們在不先將其翻譯成機器語言的情況下處理和執行源程序,則它就是解釋器。看起來像這樣:
我希望到現在為止,你已經確信要學習並實現解釋器和編譯器。
你和我將為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>
為了使你的簡單計算器正常工作而不會引發異常,你的輸入需要遵循某些規則:
1、輸入中僅允許一位數(single digit)的整數
2、目前唯一支持的算術運算是加法
3、輸入中的任何地方都不允許有空格
這些限制是使計算器簡單化所必需的。不用擔心,你很快就會使它變得非常復雜。
好的,現在讓我們深入了解一下解釋器的工作原理以及它如何計算算術表達式。
在命令行上輸入表達式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函數將引發異常。
讓我們來回顧一下解釋器為解析算術表達式所做的事情:
1、解釋器接受輸入字符串,例如"3+5"
2、解釋器調用expr函數以在詞法分析器get_next_token返回的Token流中找到預期的結構。它嘗試查找的結構的形式為INTEGER PLUS INTEGER。查找到結構后,它就將輸入字符解釋為把兩個類型為INTEGER的Token的值加起來,也就是將兩個整數3和5相加。
恭喜你!剛剛學習了如何構實現你的第一個解釋器!
現在該做練習了:
1、修改代碼以允許輸入中包含多位數(multiple-digit)的整數,例如"12+3"
2、添加一種跳過空格的方法,以便計算器可以處理帶有"12 + 3"之類帶有空格的字符輸入
3、修改代碼,使計算器能夠處理減法,例如"7-5"
最后再來復習回憶一下:
1、什么是解釋器?
2、什么是編譯器?
3、解釋器和編譯器有什么區別?
4、什么是Token?
5、將輸入分解為Token的過程的名稱是什么?
6、解釋器中進行詞法分析的部分是什么?
今天到這就結束了,在下一篇文章中,我們將擴展計算器以處理更多的算術表達式,敬請關注。
PS:
這也是我第一次翻譯技術文章,如有錯誤和不恰當的地方,希望大家能及時批評指正,謝謝!