最近在利用python做语法解析,使用了PLY,就是python里的lex+yacc。
一些使用心得记录一下:
- 以t_打头的lex规则如果出现冲突,并不完全是以出现的位置先后来确定优先级。从使用来看,正则匹配优先级更高,文本匹配优先级较低,比如下面的例子:
t_ID = r'[a-zA-Z_][a-zA-Z0-9_]*'
t_AND = 'and'
t_OR = 'or'
会导致将关键字and和or也解析为ID,这不是我们想要的行为。可以定义保留字字典来解决:
RESERVED = {
"and": "AND",
"or": "OR",
}
def t_ID(t):
r'[a-zA-Z_][a-zA-Z0-9_]*'
t.type = RESERVED.get(t.value, "ID")
return t
但是两个正则匹配之间,还是以出现的位置先后定优先级的,比如下面例子,如果掉个个,就不行了:
def t_FLOATING(t):
r'\d+\.\d+'
t.value = float(t.value)
return t
def t_INTEGER(t):
r'\d+'
t.value = int(t.value)
return t
如果INTEGER规则在先,那么10.0会解析为3个token:
(10, INTEGER) (‘.’, DOT) (0, INTEGER)
这就不是我们想要的了。
- 终结符用tokens变量表示,是一个tuple
- 算符优先级使用precedence来定义,这样在写语法规则的时候,可以将规则写得简单很多,比如java语言常用的算符优先级定义如下:
precedence = (
('left', 'COMMA'),
('right','ASSIGN'),
('left','AND','OR'),
('left','EQ','NE'),
('left','LE','LT', 'GE', 'GT'),
('left','PLUS','MINUS'),
('left','MULT','DIV', 'MOD'),
('right', 'POWER'),
('right', 'NOT')
)
越靠前的,算符优先级越低,同一层的算符优先级相同。当优先级相同的算符连续出现时,就要看是左结合还是右结合(代码里left和right所表示的含义),比如+和-是左结合,那么a + b - c执行顺序是(a+b)-c;再比如赋值是右结合,那么a=b=c的执行顺序是a=(b=c)。
- 语法是以p_打头的函数,语法规则写在函数的doc里,形如:
def p_var(p):
"""
var : HASH ID
| var member_ref ID
"""
if len(p) == 3:
p.parser.context.do_sth1(p[2])
else:
p.parser.context.do_sth2(p[3])
p是一条规则的节点集合。p.parser就是yacc解析器。我个人的习惯,会做一个自己的ParseContext,放到p.parser里,收集各语法节点信息。
最后,可以用一个类把lex和yacc都封装起来:
class MyInterpreter(object):
def __init__(self):
self.lexer = lex.lex()
self.parser = yacc.yacc()
# 这里的self.parser就是p.parser,可以把自己的ParseContext塞进去,记录必要的信息
self.parser.context = ParseContext()
def parse(self, code):
self.parser.context.clear()
# 这里需指定lexer
self.parser.parse(code, lexer=self.lexer)
return self.parser.context
一旦yacc.yacc()执行,PLY就会开始分析我们的语法,如果提示里出现shift/reduce conflict,一般要通过增加算符优先级或改变语法规则的写法来解决。例如下面的四则运算规则:
def p_arith_expr(p):
"""
arith_expr : expr PLUS expr
| expr MINUS expr
| expr MULT expr
| expr DIV expr
| expr MOD expr
| expr POWER expr
"""
如果不设置几个算符的优先级,肯定会出现shift/reduce conflict的。
还有一种情况,也可能出现shift/reduce conflict:
rule1
A : C
rule2
B : C
其实是C在reduce的时候产生了疑惑:到底该用rule1还是rule2做reduce?要解决该问题,可以把C后面的符号纳入规则,这样写:
rule1
A : C D
rule2
B : C E
- 常量字符串匹配规则。我们选一个较复杂的,字符串既可用单引号也可用双引号括起,如字符串内部还有单引号或双引号,用反斜杠转义。lex规则这么写:
def t_STR(t):
r""""([^\\"]|\\")*"|'([^\\']|\\')*'"""
t.value = t.value[1:-1].encode().decode("unicode-escape")
return t
拿到结果后,要注意做一个string-escape动作,把字符串内的反斜杠清掉。
验证的UT:
def test_lex_str(self):
self.assertEqual(self.parse_token("'abc'"), [('abc', 'STR')])
self.assertEqual(self.parse_token('"abc"'), [('abc', 'STR')])
self.assertEqual(self.parse_token("'ab\\'c'"), [("ab'c", 'STR')])
self.assertEqual(self.parse_token('"ab\\"c"'), [('ab"c', 'STR')])