一个简单的算术 LL(1) Parser
这篇文章探索如何手写一个简单的算术表达式 Parser。
问题
我们要解决的问题是把一个算术表达式字符串转化成语法树(Abstract Syntax Tree,AST),就像通用计算机语言一样,然后evaluate该AST。算术表达式包含:+ - * / ( )
以及数字。
LL(1) Parsing
这里我们采用LL(1)parsing。LL(1) parser 属于自顶向下的分析器,分析时不断用当前匹配的规则对分析器栈顶的元素进行替换。我们需要如下两个信息来进行分析:
- 分析器栈顶的词法单元(token),要么是终端字符(Terminal),要么是非终端字符(Non-Terminal)。比如
+
就是终端字符,而非终结字符就是语法规则左手侧的,比如 Exp
。
- 当前正在处理的终结词法单元
比如当前的栈顶单元是S
,而当前处理的终结字符是a
,同时我们以这样一个语法规则:S -> a P
。这时,我们需要将S
替换成a P
,其中S
和P
都是非终端字符,而a
是终端。
语法
1 2 3 4 5 6
| 1. Exp -> Exp [ + | - ] Exp2 2. Exp -> Exp2 3. Exp2 -> Exp2 [ * | / ] Exp3 4. Exp2 -> Exp3 5. Exp3 -> ( Exp ) 6. Exp3 -> Num
|
按照上述语法,我们展开2+3*4
:
1 2 3 4 5 6 7 8 9
| Exp 1. Exp + Exp2 2. Exp2 + Exp2 3. Exp3 + Exp2 6. Num + Exp2 3. Num + Exp2 * Exp3 4. Num + Exp3 * Exp3 6. Num + Num * Exp3 6. Num + Num * Num
|
不过目前这个语法,我们还不能用LL(1)
分析器分析,因为 LL1 要求只能每次看一个词法单元,而我们的语法的右手侧不是唯一的。我们需要重写语法:
1 2 3 4 5 6 7 8 9
| 1. S -> Exp $ 2. Exp -> Exp2 Exp' 3. Exp' -> [ + | - ] Exp2 Exp' 4. Exp' -> none 5. Exp2 -> Exp3 Exp2' 6. Exp2' -> [ * | - ] Exp3 Exp2' 7. Exp2' -> none 8. Exp3 -> num 9. Exp3 -> ( Exp )
|
实现
首先定义词法单元,和AST节点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| import enum import re
class TokenT(enum.Enum): T_NUM = 0 T_PLUS = 1 T_MINUS = 2 T_MULT = 3 T_DIV = 4 T_LPAR = 5 T_RPAR = 6 T_END = 7
class Node: def __init__(self, token_t, value=None): self.token_t = token_t self.value = value self.children = []
|
然后我们写词法器,Lexer:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| def lex(exp): mapping = { "+": TokenT.T_PLUS, "-": TokenT.T_MINUS, "*": TokenT.T_MULT, "/": TokenT.T_DIV }
tokens = [] for c in exp: if c in mapping: token_t = mapping[c] token = Node(token_t=token_t, value=c) elif re.match(r"\d", c): token = Node(TokenT.T_NUM, value=int(c)) else: raise ValueError(f"Unknow token {c}") tokens.append(token)
tokens.append(Node(TokenT.T_END)) return tokens
|
然后我们按照上面的语法写分析器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| def match(tokens, token): if tokens[0].token_t == token: return tokens.pop(0) else: raise
def parse_e(tokens): left_node = parse_e2(tokens)
while tokens[0].token_t in [TokenT.T_PLUS, TokenT.T_MINUS]: node = tokens.pop(0) node.children.append(left_node) node.children.append(parse_e2(tokens)) left_node = node
return left_node
def parse_e2(tokens): left_node = parse_num(tokens)
while tokens[0].token_t in [TokenT.T_MULT, TokenT.T_DIV]: node = tokens.pop(0) node.children.append(left_node) node.children.append(parse_num(tokens)) left_node = node
return left_node
def parse_num(tokens): if tokens[0].token_t == TokenT.T_NUM: return tokens.pop(0)
match(tokens, TokenT.T_LPAR) expression = parse_e(tokens) match(tokens, TokenT.T_RPAR)
return expression
def parse(inputs): tokens = lex(inouts) ast = parse_e(tokens) match(tokens, TokenT.T_END) return ast
|
当我们得到语法树后,就可以实现计算了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| ops = { TokenT.T_PLUS: lambda x,y: x+y, TokenT.T_MINUS: lambda x,y: x-y, TokenT.T_MULT: lambda x,y: x*y, TokenT.T_DIV: lambda x,y: int(x/y), }
def compute(node): if node.token_t == TokenT.T_NUM: return node.value left = compute(node.children[0]) right = compute(node.children[1]) return ops[node.token_t](left, right)
|
比如:
1 2 3 4 5
| strings = "1+2*(3-5)" ast = parse(strings) res = compute(ast) print(res)
|
Ref