PLY 解析器 - 意外行为

PLY parser - Unexpected behavior

我正在使用 PLY 创建一个计算器。我希望能够计算数值,也希望能够存储函数。

对于给定的任何函数,假设:fun(x) = x + 3 我将其存储在字典中,其中 fun(x) 是键,x+3 是值。键和值都存储为字符串。

我可以像这样调用函数 fun(x),它会 return x+3。在此示例中,我将用 9 替换值 'x'(模拟函数调用:fun(9))。它 return 应该是 12。

所以,现在我的问题是:当我尝试将此函数的 return 添加到 NUMBER 时:fun(x) + 2 我会收到语法错误,尽管 2 + fun(x) 工作正常美好的 !我完全不明白。

这是我的简化代码:

import ply.yacc as yacc
import ply.lex as lex

################################## LEXER ################################

tokens = (
    'FUNCTION',
    'VARIABLE',
    'NUMBER',
)

t_VARIABLE      = r'[a-zA-Z]+'
t_FUNCTION      = r'[a-zA-Z]{2,}\(([a-zA-Z]+)\)'
literals    = '+='
t_ignore    = " \t"

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

def t_error(t):
    print('Illegal character \'{}\''.format(t.value[0]))
    t.lexer.skip(1)

################################## PARSER ################################

functions = {}

def p_operations(p):
    """ expression : first
    first : NUMBER
    first : VARIABLE
    first : function
    """
    p[0] = p[1]

def p_plus(p):
    """ first : first '+' expression """
    if isinstance(p[1], str) or isinstance(p[3], str):
        p[0] = str(p[1]) + p[2] + str(p[3])
    else:
        p[0] = p[1] + p[3]

def p_function_assignation(p):
    '''expression : FUNCTION '=' expression'''
    functions[p[1]] = p[3]
    p[0] = p[3]


def p_function_expression(p):
    '''function : FUNCTION '''
    for key in functions.keys():
        if p[1] == key:
            p[0] = parser.parse(functions[key].replace('x', '9')). # I'm simulating here : fun(9)  
            break
    else:
        print("Variable '{}' not found".format(p[1]))

def p_error(t):
    print("Syntax error!")

################################## MAIN #################################

lexer = lex.lex() 
parser = yacc.yacc()

while True:
    try:
        question = raw_input('>>> ')
    except:
        question = input('>>> ')

    answer = parser.parse(question)
    if answer is not None:
        print(answer)

测试我的代码:

fun(x) = x + 3 => 将函数存储到字典中

fun(x) => 正确打印 12 (9 + 3)

2 + fun(x) => 按原样打印 14 (2 + 12)

fun(x) + 2 => 出现错误!

在您的 p_function_expression 中,您递归调用了 parse 方法:

p[0] = parser.parse(functions[key].replace('x', '9'))

(注意:在您输入的问题中,该行在最后一个括号后有一个无关的 . 语法错误。)

parse 的调用隐含地使用了全局词法分析器;即 lex.lexer 的当前值(这是 lex.lex() 创建的最后一个词法分析器)。但是 Ply 词法分析器(事实上,大多数词法分析器)是 stateful; the lexer maintains the current input string and the current input position, along with some other information (such as the current lexer state if you're using multiple states).

因此,对 parse 的递归调用使(全局)词法分析器指向字符串的末尾。因此,当外部 parse 尝试读取下一个标记(实际上是第二个下一个标记,因为它已经具有先行标记)时,它从词法分析器获取 EOF,这会产生语法错误。

您可以通过启用 parser debugging:

来查看
answer = parser.parse(question, debug=True)

此问题在 Ply manual 中有简要描述,其中指出您应该 clone 用于可重入词法分析的词法分析器。

不幸的是,Ply 手册没有提到 Ply parser 对象也是不可重入的。对于解析器,重入问题不太明显;它们实际上仅适用于语法错误恢复期间(除非您将自己的持久数据存储在解析器对象中,这是您被允许的)。解析器没有克隆方法,主要是因为解析表已经被预先计算和缓存,所以创建一个新的解析器并不像创建一个新的词法分析器那么昂贵。

简而言之,您需要的是使用新的解析器对象进行内部解析,该对象使用新的词法分析器对象:

p[0] = yacc.yacc().parse(functions[key].replace('x', '9'),
                         lexer=p.lexer.clone())

(解析器对象没有存储当前词法分析器的持久属性,但它在传递给解析器操作函数的参数中可用 p.lexer。请参阅 ever-helpful Ply manual。)

此外,您真的应该研究 Python 词典的用途。它们经过精确设计,因此您无需遍历所有条目即可找到所需的密钥。您可以在简单的 O(1) 操作中查找键。一个更简单的版本(如果你有多个函数也会更快)是:

def p_function_expression(p):
    '''function : FUNCTION '''
    if p[1] in functions:
        p[0] = yacc.yacc().parse(functions[p[1]].replace('x', '9'),
                                 lexer=p.lexer.clone())
    else:
        print("Variable '{}' not found".format(p[1]))

那两次查找函数名,还是两次线性运算。但是如果你只想查找一次,你可以使用字典的 get 方法,returns 默认值:

def p_function_expression(p):
    '''function : FUNCTION '''
    funcbody = functions.get(p[1], None)
    if funcbody is not None:
        p[0] = yacc.yacc().parse(funcbody.replace('x', '9'),
                                 lexer=p.lexer.clone())
    else:
        print("Variable '{}' not found".format(p[1]))

您也可以通过在捕获 KeyError.

try 块中查找名称来做到这一点

我认为不言而喻,这不是完成您为自己设定的任务的好方法。将函数体表示为预解析的 AST 会好得多,稍后可以使用实际参数对其进行评估。在这个模型中,您实际上根本不需要立即评估; 所有内容 都被解析为 AST,当(如果)您想评估解析后的文本时,您可以调用 AST 的 eval 方法来执行此操作。