【Interpreter】构建简单的解释器(第5部分)
简单翻译了下,方便查看,水平有限,喜欢的朋友去看 原文!
你是怎样弄明白像创建解释器或编译器这样复杂东西的? 一开始它看起来就像一团乱七八糟的毛线,你需要理顺毛线,才能得到完美的毛线球。
整理的方法就是一次整理一根毛线,每次解开一个结。尽管有的时候你可能感觉没办法马上理解某些东西,但是你必须坚持下去。如果你足够坚持,你最终会突然明白,我向你保证(Gee, if I put aside 25 cents every time I didn’t understand something right away I would have become rich a long time ago:)。
关于如何创建 解释器和编译器,可能我给的最好的建议之一就是 阅读文章中的解释,阅读代码,然后自己写代码,甚至可以在一段时间内重复写相同的代码直到对代码理解感到很自然,然后继续学习新章节。不要着急,慢慢的花时间去深入理解基本的概念。这种方法看似很慢,其实会有很多收获。相信我。
最终你将获得完美的毛线球。 而且,你知道吗? 即使它不是那么完美,它仍然比不去学习文章或者快速浏览文章几天后忘掉文章内容更好。
记住 —— 只要坚持去做:一段线、一个结,逐一去解开它,并且通过写代码去练习你所学到的内容,甚至写很多:
今天你将会用到前面章节学到的全部的知识,去学习怎么 解释 和 翻译 包含任意数量 加法、减法、乘法和除法 的算术表达式。你将会编写一个能够解释像 “14 + 2 * 3 - 6 / 2”一样算数表达式的解释器。
在深入研究和编写代码之前,我们先聊聊运算符的关联性和优先级。
按照惯例,7 + 3 + 1 与 ( 7 + 3 ) + 1 相同,而 7 - 3 - 1 相当于 ( 7 - 3 ) - 1。毫无疑问, 这些我们都一直认为是理所当然的。 如果我们将 7 - 3 - 1 变成 7 - ( 3 - 1 ),结果将是 5,而不是之前的 3。
在普通算术运算和大多数编程语言中,加法、减法、乘法和除法是左关联的:
7 + 3 + 1 is equivalent to (7 + 3) + 1
7 - 3 - 1 is equivalent to (7 - 3) - 1
8 * 4 * 2 is equivalent to (8 * 4) * 2
8 / 4 / 2 is equivalent to (8 / 4) / 2
运算符左关联 是什么意思呢?
如表达式 7 + 3 + 1 中的操作数 3 在左右两侧都有 加法运算符,我们需要一条规则来确定哪个操作符和 3 结合。是左侧的那个还是右侧的那个呢?运算符 + 关联到左侧,是因为两侧都有加号的操作数属于左侧的运算符,所以我们说运算符 + 是左关联的。 这就是为什么 7 + 3 + 1 相当于 ( 7 + 3 ) + 1 的关联性规则。
好了,如果表达式是 7 + 5 * 2,操作数 5 两侧的运算符不相同,该如何结合呢?表达式等于 7 + (5 * 2) 或 (7 + 5) * 2 吗?我们该如何解决这种歧义呢?
在这种情况下,关联性规则没有用处,因为它仅适用于一种运算符,可以是加减(+、 - )或乘除(*、/)。 当在同一个表达式中有不同类型的运算符时,我们需要别的规则来解决歧义。 我们需要一个定义运算符相对优先级的规则。
规则如下:我们定义如果运算符 * 在 + 之前取其操作数,那么它具有更高的优先级。 在我们知道和使用的算术表达式中,乘法和除法具有比加法和减法更高的优先级。 结果 表达式 7 + 5 * 2 相当于 7 + ( 5 * 2 ),表达式 7 - 8 / 4 相当于 7 - ( 8 / 4 )。
在有一个具有相同优先级运算符的表达式的情况下,我们只使用关联性规则并从左到右执行运算符:
7 + 3 - 1 is equivalent to (7 + 3) - 1
8 / 4 * 2 is equivalent to (8 / 4) * 2
我希望你不要误认为我是想通过讨论这么多操作符的关联性和优先级的问题来烦死你。 关于这些规则的好处是我们可以通过算术运算符的关联性和优先级的表来构造算术表达式的语法。 然后,我们可以按照 第4部分 中的规则将语法翻译成代码,除了关联性之外,我们的解释器还能够处理运算符的优先级。
下面是我们的优先级表格:
从表中可以看出,运算符 + 和 - 具有相同的优先级,并且它们都是左关联的。 还可以看到运算符 * 和 / 也是左关联的,它们之间具有相同的优先级,但具有比加法和减法运算符更高的优先级。
以下是如何从优先级表构造语法的规则:
- 对每个优先级定义一个非终结符。 非终结符产生式的主体应该包含该级别的算术运算符和下一个更高级别优先级的非终结符。
- 创建一个附加的非终结 因子 作为表达式的基础单元,在我们这个例子里就是一个数字。一般规则是,如果你有 N 个优先级别,那么你总共就需要有 N+1 个非终结符:一个非终结符对应一个优先级别,再加上一个非终结符也就是表达式的基础单元。
接下来让我们按照规则构造相关语法。
根据 规则1 我们需要定义两个非终结符:一个叫 expr 处理优先级 2,另一个叫 term 处理优先级 1。然后根据 规则2,我们需要定义一个整形的非终结符基本算数单元作为因子。
语法的起始符是 expr ,expr 的 产生式 包含一个代表着使用 2级操作符的实体,我们例子里使用的是 + 和 -,同时也包含下一个更高优先级的非终结符 term,优先级1:
term 的产生式包含代表使用 优先级1 操作符的实体,在我们这个例子中是 * 和 /,同时也包含表达式基本单元的整形非终结符因子:
factor 非终结符因子的产生式是:
你已经在之前的文章中看过以上产生式作为语法和语法图的一部分,但在这里我们将把它们组合成一个语法来处理运算符的关联性和优先级:
这是与上面的语法相对应的语法图:
上图中的每个矩形框都是对另一个图的“方法调用”。 如果使用表达式 7 + 5 * 2 为例,从顶部的 expr 开始向下走到最底部的 factor,您应该能够看到较高优先级的运算符 * 和 / 在位置靠下的图中,并且比位置靠上的图中的运算符 + 和 - 执行的更早。
为了驱动运算符指向 home 的优先级,让我们看看根据上面的语法和语法图完成的相同算术表达式7 + 5 * 2的分解。 这只是表明高优先级运算符在优先级较低的运算符之前执行的另一种方法:
为了把运算符优先级说的更明白一点,让我们来看一下同一个算术表达式 7 + 5 * 2 按照我们上面的语法和语法图来分解的过程。这只是另一种方式来表示高优先级运算符在低优先级运算符之前执行:
让我们按照 第四部分 的规则把语法翻译成代码,看一下新的解释器如何工作。
再次展示一下语法图:
下面是完整的 可以处理包含整数和任意数量的加法,减法,乘法和除法运算符的有效算术表达式的计算器代码。
以下是与 第四部分 代码相比的主要改变:
- Lexer 类 现在可以标记 +,-,*,/ (没有新改动,我们只是把以前的文章中代码组合到一个类中,支持所有这些 token)
- 回想一下,在与语法中定义的每个规则(产生式),R,变成了一个具有相同名称的方法并且引用的规则成为一个方法调用:R()。最终 Interpreter 类 现在有三个方法,对应于语法中的非终结符:expr,term,和 factor。
源代码:
# Token types
#
# EOF (end-of-file) token is used to indicate that
# there is no more input left for lexical analysis
INTEGER, PLUS, MINUS, MUL, DIV, EOF = (
'INTEGER', 'PLUS', 'MINUS', 'MUL', 'DIV', 'EOF'
)
class Token(object):
def __init__(self, type, value):
# token type: INTEGER, PLUS, MINUS, MUL, DIV, or EOF
self.type = type
# token value: non-negative integer value, '+', '-', '*', '/', or None
self.value = value
def __str__(self):
"""String representation of the class instance.
Examples:
Token(INTEGER, 3)
Token(PLUS, '+')
Token(MUL, '*')
"""
return 'Token({type}, {value})'.format(
type=self.type,
value=repr(self.value)
)
def __repr__(self):
return self.__str__()
class Lexer(object):
def __init__(self, text):
# client string input, e.g. "3 * 5", "12 / 3 * 4", etc
self.text = text
# self.pos is an index into self.text
self.pos = 0
self.current_char = self.text[self.pos]
def error(self):
raise Exception('Invalid character')
def advance(self):
"""Advance the `pos` pointer and set the `current_char` variable."""
self.pos += 1
if self.pos > len(self.text) - 1:
self.current_char = None # Indicates end of input
else:
self.current_char = self.text[self.pos]
def skip_whitespace(self):
while self.current_char is not None and self.current_char.isspace():
self.advance()
def integer(self):
"""Return a (multidigit) integer consumed from the input."""
result = ''
while self.current_char is not None and self.current_char.isdigit():
result += self.current_char
self.advance()
return int(result)
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.
"""
while self.current_char is not None:
if self.current_char.isspace():
self.skip_whitespace()
continue
if self.current_char.isdigit():
return Token(INTEGER, self.integer())
if self.current_char == '+':
self.advance()
return Token(PLUS, '+')
if self.current_char == '-':
self.advance()
return Token(MINUS, '-')
if self.current_char == '*':
self.advance()
return Token(MUL, '*')
if self.current_char == '/':
self.advance()
return Token(DIV, '/')
self.error()
return Token(EOF, None)
class Interpreter(object):
def __init__(self, lexer):
self.lexer = lexer
# set current token to the first token taken from the input
self.current_token = self.lexer.get_next_token()
def error(self):
raise Exception('Invalid syntax')
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.lexer.get_next_token()
else:
self.error()
def factor(self):
"""factor : INTEGER"""
token = self.current_token
self.eat(INTEGER)
return token.value
def term(self):
"""term : factor ((MUL | DIV) factor)*"""
result = self.factor()
while self.current_token.type in (MUL, DIV):
token = self.current_token
if token.type == MUL:
self.eat(MUL)
result = result * self.factor()
elif token.type == DIV:
self.eat(DIV)
result = result / self.factor()
return result
def expr(self):
"""Arithmetic expression parser / interpreter.
calc> 14 + 2 * 3 - 6 / 2
17
expr : term ((PLUS | MINUS) term)*
term : factor ((MUL | DIV) factor)*
factor : INTEGER
"""
result = self.term()
while self.current_token.type in (PLUS, MINUS):
token = self.current_token
if token.type == PLUS:
self.eat(PLUS)
result = result + self.term()
elif token.type == MINUS:
self.eat(MINUS)
result = result - self.term()
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
lexer = Lexer(text)
interpreter = Interpreter(lexer)
result = interpreter.expr()
print(result)
if __name__ == '__main__':
main()
将以上代码保存到 calc5.py 文件中或直接从 GitHub 下载。 像往常一样,亲自试一下解释器能否正确解释具有不同优先级的运算符的算术表达式。
这是在我笔记本电脑上的执行结果:
$ python calc5.py
calc> 3
3
calc> 2 + 7 * 4
30
calc> 7 - 8 / 4
5
calc> 14 + 2 * 3 - 6 / 2
17
接下来是今天的练习:
- 按照本文中的描述编写一个解释器,而不是参考文章中的代码。 为你的解释器写一些测试,并确保他们执行成功。
- 扩展解释器以处理包含括号的算术表达式,以便你的解释器可以处理深层嵌套的算术表达式,如:7 + 3 * ( 10 ) / ( 12 / ( 3 + 1 ) - 1 ) )
理解检测:
- 什么是操作符的左关联?
- + 和 - 是左关联还是右关联?那么 * 和 / 呢?
- + 的优先级比 * 高吗?
嘿,你又读到最后了!这真是棒极了。我下次还会带来一篇新文章,敬请关注。另外,和往常一样,不要忘记做练习。
以下是我推荐的书籍清单,可以帮助你学习解释器和编译器:
- Language Implementation Patterns: Create Your Own Domain-Specific and General Programming Languages (Pragmatic Programmers)
- Writing Compilers and Interpreters: A Software Engineering Approach
- Modern Compiler Implementation in Java
- Modern Compiler Design
- Compilers: Principles, Techniques, and Tools (2nd Edition)
原文链接:Let’s Build A Simple Interpreter. Part 5.
作者博客:Ruslan’s Blog
——2019-01-21——