之前答应过你,今天我们主要讨论一下贯穿全系列的最核心的数据结构。系好安全带准备出发!
到现在我们已经有了自己的解释器,只要表达式里面有加减乘除这种语言结构,它就能够解析成功。这种解释器叫此法导向解释器。它通常只扫描代码一遍,适用于简单的语言。如果想做到分析更复杂的Pascal语言的结构,我们需要构建IR,也就是中间表示。我们的解析器负责生成IR,我们的解释器负责解释IR。
业界的经验告诉我们,树形结构适合作为IR的数据结构。
我们快速复习一下树这个概念吧:
- 一棵树有很多节点,这些点是按照层级来组成。
- 除了根节点,每一个节点,都有一个父节点。
- 这张图里面,标星星的那个节点就是父节点的。二和七是他的两个孩子从左往右,依次排列。
- 没有孩子的节点,叫叶子节点。
- 有一个或者多个孩子的节点,叫中间节点,当然不包括根节点。
- 有个孩子,也有可能是一个完整的书,我们叫他自述。下面这张图片里。标志星星的节点和加号这个节点就组成了一个完整的子树。
- 计算机系的学生,画树的时候都是从上往下画的。
上面这个数就是表示,2×7+3。
我们用的IR有个名字,叫AST抽象语法书,或者抽象词法树。我们先讲讲怎么解析一个树型结构的数据,再讲讲为什么AST是这么适合表示IR,比解析树更合适。
什么是解析树?好,我们先看看解析树这个东西。比如 2 * 7 + 3的解析树是:
可以看到:
- 解析树忠实的记录了识别输入元素的规则。
- 树根是语法的起始符号。
- 每一个中间节点表示一个非终结符,也就是语法的规则函数,想expr、term或者factor。
- 叶子节点表示的是token。
我开发了一个小工具,genptdot.py ,可以把树用图画出来。
$ python genptdot.py "14 + 2 * 3 - 6 / 2" > \
parsetree.dot && dot -Tpng -o parsetree.png parsetree.dot
输出的图片是:
那什么是AST呢?
还是拿2 * 7 + 3来举例,AST和解析树的区别如下图所示:
AST更简单,更抓住了问题的本质。系统总结一下就是:
- AST把操作符作为根和中间节点,把操作树作为孩子。
- AST的中间节点不表示语法规则。
- AST忽略了真正词法的一些细节,这也是抽象两个字的含义,没有rule节点和括号节点。
- AST的结构看起来更密集一些。
7 + ((2 + 3))的AST如下表示:
那怎么表示次序呢?比如“X在Y之前” 。很简单,只需要把X放到Y的下面。
比如下面给出了更多的例子:
可以看到,优先级越高的运算符,在树种的位置越低。
下面是代码时间。
首先我们定义一个AST的节点类,从下面这个基类中扩展出来。
class AST(object):
pass
回想一下,AST表示的是操作符和操作数的模型。至今为止,我们已经有了四种操作符和整数这个操作数。我们可以给每一个操作符都创建一个节点,比如AddNode, SubNode, MulNode和 DivNode。但是,更好的做法是用一个BinOp类,来表示四个操作符。BinOP是双目操作符的简称,表示有两个操作数的操作符。
class BinOp(AST):
def __init__(self, left, op, right):
self.left = left
self.token = self.op = op
self.right = right
构造函数里面有left,op和right三个参数。left和right表示做操作数和右操作数。op表示操作符这个token,也就是Token(PLUS, ‘+’)或者Token(MINUS, ‘-‘)。
整数这个token是这么表示的:
class Num(AST):
def __init__(self, token):
self.token = token
self.value = token.value
2 * 7 + 3的AST生成过程是:
>>> from spi import Token, MUL, PLUS, INTEGER, Num, BinOp
>>>
>>> mul_token = Token(MUL, '*')
>>> plus_token = Token(PLUS, '+')
>>> mul_node = BinOp(
... left=Num(Token(INTEGER, 2)),
... op=mul_token,
... right=Num(Token(INTEGER, 7))
... )
>>> add_node = BinOp(
... left=mul_node,
... op=plus_token,
... right=Num(Token(INTEGER, 3))
... )
解释器的代码更新过后,可以生成AST并解析出最终的计算结果:
class AST(object):
pass
class BinOp(AST):
def __init__(self, left, op, right):
self.left = left
self.token = self.op = op
self.right = right
class Num(AST):
def __init__(self, token):
self.token = token
self.value = token.value
class Parser(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 | LPAREN expr RPAREN"""
token = self.current_token
if token.type == INTEGER:
self.eat(INTEGER)
return Num(token)
elif token.type == LPAREN:
self.eat(LPAREN)
node = self.expr()
self.eat(RPAREN)
return node
def term(self):
"""term : factor ((MUL | DIV) factor)*"""
node = self.factor()
while self.current_token.type in (MUL, DIV):
token = self.current_token
if token.type == MUL:
self.eat(MUL)
elif token.type == DIV:
self.eat(DIV)
node = BinOp(left=node, op=token, right=self.factor())
return node
def expr(self):
"""
expr : term ((PLUS | MINUS) term)*
term : factor ((MUL | DIV) factor)*
factor : INTEGER | LPAREN expr RPAREN
"""
node = self.term()
while self.current_token.type in (PLUS, MINUS):
token = self.current_token
if token.type == PLUS:
self.eat(PLUS)
elif token.type == MINUS:
self.eat(MINUS)
node = BinOp(left=node, op=token, right=self.term())
return node
def parse(self):
return self.expr()
AST的节点生成过程是这样的:每一个BinOp节点接受node变量当前的数值,作为左孩子。term和factor作为右孩子。下面是个好例子。
我又写了一个小工具来画一下表达式:
$ python genastdot.py "7 + 3 * (10 / (12 / (3 + 1) - 1))" > \ ast.dot && dot -Tpng -o ast.png ast.dot
genastdot.py 工具的链接是这儿。更多的练习——2 * 7 + 3:
其实,使用后序遍历(一种深度遍历)就可以从根节点出来,一直递归的遍历每一个节点的孩子节点,从左向右,一直往最深的子节点那遍历。
伪代码如下:
为什么我们要用后序遍历呢?因为优先级高的运算操作被放到了树的下面,这样我们必须把下面的都计算清楚了,才能继续计算上面的算式。
Visitor pattern的代码如下,把访问的核心代码封装成访问者模式的模板,是为了更好的解耦。
class NodeVisitor(object):
def visit(self, node):
method_name = 'visit_' + type(node).__name__
visitor = getattr(self, method_name, self.generic_visit)
return visitor(node)
def generic_visit(self, node):
raise Exception('No visit_{} method'.format(type(node).__name__))
And here is the source code of our Interpreter class that inherits from the NodeVisitor class and implements different methods that have the form visit_NodeType, where NodeType is replaced with the node’s class name like BinOp, Num and so on:
NodeVisitor的子类恰恰是我们的interpreter类。它额外实现了visit_NodeType的不同方法。这里的NodeType被节点的类型替换了,比如BinOp,Num等等的这些类名。
class Interpreter(NodeVisitor):
def __init__(self, parser):
self.parser = parser
def visit_BinOp(self, node):
if node.op.type == PLUS:
return self.visit(node.left) + self.visit(node.right)
elif node.op.type == MINUS:
return self.visit(node.left) - self.visit(node.right)
elif node.op.type == MUL:
return self.visit(node.left) * self.visit(node.right)
elif node.op.type == DIV:
return self.visit(node.left) / self.visit(node.right)
def visit_Num(self, node):
return node.value
对AST节点的访问逻辑,跟AST节点本身的逻辑彻底解耦了。你可以看到AST节点的类里面(也就是BinOP和Num类)并没有提供访问自己数据的代码。相反这些代码被放入了Interprerer类中。这个类就是访问代码的类。
还有,我们并没有用大量的if else语句,而是用了如下的语句:
def visit(node):
node_type = type(node).__name__
if node_type == 'BinOp':
return self.visit_BinOp(node)
elif node_type == 'Num':
return self.visit_Num(node)
elif ...
# ...
或者这样:
def visit(node):
if isinstance(node, BinOp):
return self.visit_BinOp(node)
elif isinstance(node, Num):
return self.visit_Num(node)
elif ...
NodeVisitor的 visit 方法很通用的,它根据节点的类型去分发不同的方法调用。比如如果节点的类型是BinOp,那visit方法就激活visit_BinOp方法。
Python 有个模块 ast 使用了相同的遍历机制。感兴趣就试试。
generic_visit 方法是备用方法,他会抛异常,来警告它遇到了一个没有对应visit_NodeType方法的节点。
我们尝试构建 2 * 7 + 3 的AST,并看看不同的visit方法是怎么计算出结果的。
>>> from spi import Token, MUL, PLUS, INTEGER, Num, BinOp
>>>
>>> mul_token = Token(MUL, '*')
>>> plus_token = Token(PLUS, '+')
>>> mul_node = BinOp(
... left=Num(Token(INTEGER, 2)),
... op=mul_token,
... right=Num(Token(INTEGER, 7))
... )
>>> add_node = BinOp(
... left=mul_node,
... op=plus_token,
... right=Num(Token(INTEGER, 3))
... )
>>> from spi import Interpreter
>>> inter = Interpreter(None)
>>> inter.visit(add_node)
17
我把根节点add_node传入visit方法,然后就连锁反应遍历了整个树,得到了17这个结果。
完整的interpreter代码如下:
""" SPI - Simple Pascal Interpreter """
###############################################################################
# #
# LEXER #
# #
###############################################################################
# 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, LPAREN, RPAREN, EOF = (
'INTEGER', 'PLUS', 'MINUS', 'MUL', 'DIV', '(', ')', 'EOF'
)
class Token(object):
def __init__(self, type, value):
self.type = type
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. "4 + 2 * 3 - 6 / 2"
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, '/')
if self.current_char == '(':
self.advance()
return Token(LPAREN, '(')
if self.current_char == ')':
self.advance()
return Token(RPAREN, ')')
self.error()
return Token(EOF, None)
###############################################################################
# #
# PARSER #
# #
###############################################################################
class AST(object):
pass
class BinOp(AST):
def __init__(self, left, op, right):
self.left = left
self.token = self.op = op
self.right = right
class Num(AST):
def __init__(self, token):
self.token = token
self.value = token.value
class Parser(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 | LPAREN expr RPAREN"""
token = self.current_token
if token.type == INTEGER:
self.eat(INTEGER)
return Num(token)
elif token.type == LPAREN:
self.eat(LPAREN)
node = self.expr()
self.eat(RPAREN)
return node
def term(self):
"""term : factor ((MUL | DIV) factor)*"""
node = self.factor()
while self.current_token.type in (MUL, DIV):
token = self.current_token
if token.type == MUL:
self.eat(MUL)
elif token.type == DIV:
self.eat(DIV)
node = BinOp(left=node, op=token, right=self.factor())
return node
def expr(self):
"""
expr : term ((PLUS | MINUS) term)*
term : factor ((MUL | DIV) factor)*
factor : INTEGER | LPAREN expr RPAREN
"""
node = self.term()
while self.current_token.type in (PLUS, MINUS):
token = self.current_token
if token.type == PLUS:
self.eat(PLUS)
elif token.type == MINUS:
self.eat(MINUS)
node = BinOp(left=node, op=token, right=self.term())
return node
def parse(self):
return self.expr()
###############################################################################
# #
# INTERPRETER #
# #
###############################################################################
class NodeVisitor(object):
def visit(self, node):
method_name = 'visit_' + type(node).__name__
visitor = getattr(self, method_name, self.generic_visit)
return visitor(node)
def generic_visit(self, node):
raise Exception('No visit_{} method'.format(type(node).__name__))
class Interpreter(NodeVisitor):
def __init__(self, parser):
self.parser = parser
def visit_BinOp(self, node):
if node.op.type == PLUS:
return self.visit(node.left) + self.visit(node.right)
elif node.op.type == MINUS:
return self.visit(node.left) - self.visit(node.right)
elif node.op.type == MUL:
return self.visit(node.left) * self.visit(node.right)
elif node.op.type == DIV:
return self.visit(node.left) / self.visit(node.right)
def visit_Num(self, node):
return node.value
def interpret(self):
tree = self.parser.parse()
return self.visit(tree)
def main():
while True:
try:
try:
text = raw_input('spi> ')
except NameError: # Python3
text = input('spi> ')
except EOFError:
break
if not text:
continue
lexer = Lexer(text)
parser = Parser(lexer)
interpreter = Interpreter(parser)
result = interpreter.interpret()
print(result)
if __name__ == '__main__':
main()
运行结果是:
$ python spi.py
spi> 7 + 3 * (10 / (12 / (3 + 1) - 1))
22
spi> 7 + 3 * (10 / (12 / (3 + 1) - 1)) / (2 + 3) - 5 - 3 + (8)
10
spi> 7 + (((3 + 2)))
12
今天所有这些的概念,他们之间的关系是: