译自: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: 这也是我第一次翻译技术文章,若有错误和不恰当的地方,但愿你们能及时批评指正,谢谢!