Python语法解析器PLY——lex and yacc in Python

        PLY是lex和yacc的python实现,包含了它们的大部分特性。PLY采用COC(Convention Over Configuration,惯例优于配置)的方式实现各种配置的组织,比如:强制词法单元的类型列表的名字为tokens,强制描述词法单元的规则的变量名为t_TOKENNAME等。本文主要是对PLY做一个基本的介绍。

一. 词法分析

1. 词法单元的类型列表(token list)

        词法单元的类型定义为tokens元组,元组的元素就是所有的词法单元类型。tokens元组定义了词法分析器可以产生的所有词法单元的类型,并且在语法分析时这个元组同样会用到,来识别终结符。

tokens = (
        'NUMBER',
        'PLUS',
        'MINUS',
        'TIMES',
        'DIVIDE',
        'LPAREN',
        'RPAREN',
     )
        上述tokens元组就包括了所有的词法单元,必须采用tokens作为元组的变量名,这是PLY强制规定的。

2. 词法单元模式定义

        接下来是各个词法单元的模式描述,可以采用正则表达式字符串或者函数来定义,但是必须采用t_TOKENNAME的模式命名,比如对于NUMBER类型的词法单元,变量名或是函数名必须是t_NUMBER。
        简单的正则表达式,当输入的字符序列符合这个正则表达式时,该序列就会被识别为该类型的词法单元:

t_PLUS = r'\+'
        当识别出词法单元时,还需要执行一些动作时,可以使用函数定义:

def t_NUMBER(t):
         r'\d+'     # 描述模式的正则表达式
         t.value = int(t.value)    
         return t     # 最后必须返回t,如果不返回,这个token就会被丢弃掉
        这种情况下,要把描述的正则表达式作为函数的doc,参数t是LexToken类型,表示识别出的词法单元,具有属性:
                value:默认就是识别出的字符串序列。
                type:词法单元的类型,就是在tokens元组中的定义的。
                line:词法单元在源代码中的行号。
                lexpos:词法单元在该行的列号。
        词法单元规则的添加顺序:
                1. 所有通过函数定义的token的添加顺序与其在词法定义文件中出现的顺序相同。
                2. 所有通过正则表达式定义的token,它们按照正则表达式字符串的长度由大至小排序。
        通过上面定义的顺序可以很好的处理左递归的文法。

3. 词法单元的值

        默认是根据词法单元模式识别出的字符串,但是token value可以是任意的python对象。强烈建议不使用其他属性名,如果需要复合数据可以将value赋值为tuple、list或dict。

4. 词法单元的丢弃(discarded tokens)
        可以添加ignore_前缀表示将丢弃匹配的token,比如:

t_ignore_COMMENT = r'#.*'
5. 行号和位置信息
        默认,lex.py是不提供行号信息,因为它不知如何识别一行。可以通过t_newline()规则来更新行号信息:
# Define a rule so we can track line numbers
def t_newline(t):
    r'\n+'
    t.lexer.lineno += len(t.value)
6. 字面量字符
         在词法分析器中定义literals变量可以声明一些字符,然后在yacc中直接使用。
                  literals = ['+', '-', '*', '/']     或     literals = '+-*/'
         literals只能是字符不能是字符串,并且这些字符在所有的正则表达式检查过过之后再执行,所以如果某个正则表达式的规则以literal字符开始,那么该literal字符永远不会被匹配。
         当字面量字符匹配成功后,那么该词法单元的type和value都是该字面量字符。

7. 错误处理

         当检测到非法字符时,t_error()用于处理词法错误。这种情况下t.value包含了所有没有处理的输入字符串。可以通过t.lexer.skip(1)跳过1个字符,然后继续解析。

8. 词法分析器的生成和使用

        通过调用ply.lex.lex()可以构建词法分析器,这个函数通过python的反射机制读取调用函数的全局环境以获取所有的规则字符串,然后生成词法分析器。当词法分析器生成后,可以通个下面两个方法实现词法分析:
                 lexer.input(data):重置lexer并保存输入字符串。
                 lexer.token():返回识别出的下一个token,如果解析完毕返回None。
         lexer实现了迭代器,所以一般的使用模式:

lexer = lex.lex()
lexer.input(data)
for token in lexer:
    # processing...

二. 语法分析

1. 文法规则的描述
        每个文法规则(grammar rule)被描述为一个函数,这个函数的文档字符串(doc string)描述了对应的上下文无关文法的规则。函数体用来实现规则的语义动作。每个函数都会接受一个参数p,这个参数是一个序列(sequence),包含了组成这个规则的所有的语法符号,p[i]是规则中的第i个语法符号。比如:

def p_expression_plus(p):
     'expression : expression PLUS expression'
     #    |             |      |        |
     #  p[0]          p[1]   p[2]     p[3]
    
     p[0] = p[1] + p[3]

        对于序列中的词法单元,p[i]的值就是该词法单元的值,也就是在词法分析器中赋值的p.value。而对于非终结符的值则取决于该规则解析时p[0]中存放的值,这个值可以使任意类型,比如tuple、dict、类实例等。

2. 起始规则

        在yacc语法说明书中的第一条规则是文法的起始规则(Starting grammar symbol),也就是说会从该条规则开始解析。可以通过关键字参数传递给yacc以指定起始文法规则。

yacc.yacc(start = 'rule_name')
3. 解析错误处理
        规则p_error(p)用来捕捉语法错误。
        如果yacc在文法说明书中监测到错误,yacc会产生诊断信息并可能抛出异常,可以检测到的有:
                1. Duplicated function names (if more than one rule function have the same name in the grammar file).
                2. Shift/reduce and reduce/reduce conflicts generated by ambiguous grammars.
                3. Badly specified grammar rules.
                4. Infinite recursion (rules that can never terminate).
                5. Unused rules and tokens
                6. Undefined rules and tokens

4. parser的构建
        通过yacc.yacc()构建parser,这个函数可以构建LR parsing table。因为LR parsing table的构建是相当耗时,所以生成的table会被写入当前目录下的parsetab.py。此外,用于debug的parser.out文件也会被创建。在随后执行时,yacc会从parsetab.py中重新加载parsing table,除非yacc检测到底层的文法改变了,也就是源文件被修改了。

5. 有歧义的文法

        通常文法是有歧义的,比如:四则运算”3*4+5“,应该如何分组操作符?这个表达式的意思是(3*4)+5,还是3*(4+5)?当yacc遇到歧义的文法时,会报错"shift/reduce"冲突或者"reduce/reduce"冲突。遇到"shift/reduce"冲突是因为yacc在遇到一个词法单元时,不知道应该执行规约动作还是执行词法单元移动。比如四则运算文法:

expression : expression PLUS expression
           | expression MINUS expression
           | expression TIMES expression
           | expression DIVIDE expression
           | LPAREN expression RPAREN
           | NUMBER
        在yacc解析表达式"3*4+5"时,解析栈的状态变化:

Step Symbol Stack           Input Tokens            Action
---- ---------------------  ---------------------   -------------------------------
1    $                                3 * 4 + 5$    Shift 3
2    $ 3                                * 4 + 5$    Reduce : expression : NUMBER
3    $ expr                             * 4 + 5$    Shift *
4    $ expr *                             4 + 5$    Shift 4
5    $ expr * 4                             + 5$    Reduce: expression : NUMBER
6    $ expr * expr                          + 5$    SHIFT/REDUCE CONFLICT ????

        在第6步时,既可以将expr * expr规约为 expression TIMES expression,同时也可以根据规则expressiong PLUS expression将词法单元+移入解析栈中。所以出现了"shift/reduce"冲突。一般地,这种冲突是由于操作符的优先级和结合性导致的。

        默认情况下,在遇到"shift/reduce"冲突时,yacc会采用shift动作。为了解决这种歧义文法,yacc允许定义词法单元的优先级和结合性。通过定义precedence元组完成这个功能,比如为四则运算消除歧义:

precedence = (
    ('left', 'PLUS', 'MINUS'),
    ('left', 'TIMES', 'DIVIDE'),
)
        这个变量的意思是,PLUS/MINUS和TIMES/DIVIDE具有同样的优先级,并且都具有左结合性, 但是TIMES/DIVIDE的优先级高于PLUS/MINUS。也就是说precedence元组中元素的优先级按从先到后的顺序有小到大。通过这种方式就会给操作符对应的文法规则添加优先级和结合性,比如:

expression : expression PLUS expression                 # level = 1, left
           | expression MINUS expression                # level = 1, left
           | expression TIMES expression                # level = 2, left
           | expression DIVIDE expression               # level = 2, left
           | LPAREN expression RPAREN                   # level = None (not specified)
           | NUMBER                                     # level = None (not specified)
        出现"shift/reduce"冲突时,yacc可以根据规则的优先级和结合性进行处理,具体规则:

                1. 如果当前的词法单元的优先级高于解析栈中规则,那么执行shift动作。

                2. 如果当前的词法单元的优先级低于解析栈中规则,那么将栈中的规则进行规约。

                3. 在当前的词法单元和解析栈中规则的优先级相同的情况下,如果规则是左结合性,那么执行规约动作,否则执行shift。

                4. 如果没有提供优先级和结合性,那么默认执行shift动作。

        通过上述规则的约束后,解析"3*4+5"时,如果+是当前词法单元,*优先级高于+,所以此时应该执行reduce操作,这样就避免了优先级引起的歧义操作。

        当一个操作符有两个语义时,通过precedence变量就不能同时为这两个语义的操作符定义优先级,比如:"-"即表示减法,还表示负数。yacc通过"虚词法单元(fictitious tokens)",比如"3+4*-5",可以通过:

precedence = (
    ('left', 'PLUS', 'MINUS'),
    ('left', 'TIMES', 'DIVIDE'),
    ('right', 'UMINUS'),            # Unary minus operator
)
        其中UMINUS是虚词法单元,还需要为虚词法单元定义规则:

def p_expr_uminus(p):
    'expression : MINUS expression %prec UMINUS'
    p[0] = -p[2]
        文法中的"%prec UMINUS"可以重写优先级设置,将MINUS expression作为UMINUS的规则。UMINUS不是词法单元或者规则,可以把它看成是优先级表中的特殊标记。当使用%prec标识符时,yacc就会把使用%prec的规则的优先级设置为在优先级表中对应的特殊标记的优先级。

        优先级表中也可以不指定结合性,比如比较运算,表达式”a < b < c“就是错误的。

precedence = (
    ('nonassoc', 'LESSTHAN', 'GREATERTHAN'),  # Nonassociative operators
    ('left', 'PLUS', 'MINUS'),
    ('left', 'TIMES', 'DIVIDE'),
    ('right', 'UMINUS'),            # Unary minus operator
)
        通过”nonassoc“指定操作符不具备结合性。

        "reduce/reduce"冲突就是解析栈中可以应用多个规则进行规约,这种冲突的解决就是选择第一个出现的规则进行规约。一般出现这种冲突主要是因为不同的规则集合可以产生相同的词法单元序列。比如:

assignment :  ID EQUALS NUMBER
           |  ID EQUALS expression
           
expression : expression PLUS expression
           | expression MINUS expression
           | expression TIMES expression
           | expression DIVIDE expression
           | LPAREN expression RPAREN
           | NUMBER
        对于表达式"x = 5",规则:

assignment  : ID EQUALS NUMBER
expression  : NUMBER
        这两个规则会产生reduce/reduce冲突。当栈中是x = 5时,可以讲5规约成”expressiong : NUMBER“,或者直接将"x = 5"规约成"assignment : ID EQUALS NUMBER"。在出现"reduce/reduce"冲突,yacc会输出详细的信息,比如:

WARNING: 1 reduce/reduce conflict
WARNING: reduce/reduce conflict in state 15 resolved using rule (assignment -> ID EQUALS NUMBER)
WARNING: rejected rule (expression -> NUMBER)
        上面已经对PLY的基本要素进行了介绍,下面看一下如何用PLY实现简单的四则运算。

三. 例子:四则运算

        文法:

statement : NAME "=" expression
          | expression
expression : expression '+' term
           | expression '-' term
term : term '*' factor
     | term '/' factor
factor : NUMBER
       | NAME
       | '-' expression
       | '(' expression ')'

        代码:

# -----------------------------------------------------------------------------
# calc.py
#
# A simple calculator with variables.   This is from O'Reilly's
# "Lex and Yacc", p. 63.
# -----------------------------------------------------------------------------

if sys.version_info[0] >= 3:
    raw_input = input

tokens = (
    'NAME','NUMBER',
    )

literals = ['=','+','-','*','/', '(',')']

# Tokens

t_NAME    = r'[a-zA-Z_][a-zA-Z0-9_]*'

def t_NUMBER(t):
    r'\d+'
    t.value = int(t.value)
    return t

t_ignore = " \t"

def t_newline(t):
    r'\n+'
    t.lexer.lineno += t.value.count("\n")
    
def t_error(t):
    print("Illegal character '%s'" % t.value[0])
    t.lexer.skip(1)
    
# Build the lexer
import ply.lex as lex
lex.lex()

# Parsing rules

precedence = (
    ('left','+','-'),
    ('left','*','/'),
    ('right','UMINUS'),
    )

# dictionary of names
names = { }

def p_statement_assign(p):
    'statement : NAME "=" expression'
    names[p[1]] = p[3]

def p_statement_expr(p):
    'statement : expression'
    print(p[1])

def p_expression_binop(p):
    '''expression : expression '+' expression
                  | expression '-' expression
                  | expression '*' expression
                  | expression '/' expression'''
    if p[2] == '+'  : p[0] = p[1] + p[3]
    elif p[2] == '-': p[0] = p[1] - p[3]
    elif p[2] == '*': p[0] = p[1] * p[3]
    elif p[2] == '/': p[0] = p[1] / p[3]

def p_expression_uminus(p):
    "expression : '-' expression %prec UMINUS"
    p[0] = -p[2]

def p_expression_group(p):
    "expression : '(' expression ')'"
    p[0] = p[2]

def p_expression_number(p):
    "expression : NUMBER"
    p[0] = p[1]

def p_expression_name(p):
    "expression : NAME"
    try:
        p[0] = names[p[1]]
    except LookupError:
        print("Undefined name '%s'" % p[1])
        p[0] = 0

def p_error(p):
    if p:
        print("Syntax error at '%s'" % p.value)
    else:
        print("Syntax error at EOF")

import ply.yacc as yacc
yacc.yacc()

while 1:
    try:
        s = raw_input('calc > ')
    except EOFError:
        break
    if not s: continue
    yacc.parse(s)
        下载并安装PLY:
> wget http://www.dabeaz.com/ply/ply-3.4.tar.gz
> tar -zxvf ply-3.4.tar.gz
> cd ply-3.4
> python setup.py install
       执行上面的py文件,就可以在命令行中输入表达式进行计算。后一篇文章会介绍如何通过PLY实现Mongodb的SQL查询语言,就是通过类似SQL的语言实现Mongodb操作。敬请期待!!


你可能感兴趣的:(Compiler,&,Interpreter,Python)