Парсер PLY - Неожиданное поведение - PullRequest
0 голосов
/ 30 апреля 2020

Я создаю калькулятор с помощью PLY. Я хочу, чтобы иметь возможность вычислить числовое значение, но и хранить функции.

Для любой заданной функции, скажем: fun(x) = x + 3 Я храню ее в словаре, где fun(x) - это ключ, а x+3 - это значение. Ключ и значение хранятся в виде строк.

Я могу вызвать такую ​​функцию, как эта fun(x), и она вернет x+3. В этом примере я заменим значение «x» на 9 (имитирующий вызов функции: fun(9)). Он возвращает 12, как и должно быть.

Итак, теперь моя проблема: когда я пытаюсь добавить возврат этой функции в 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 => сохранить функцию в dict

fun(x) => правильно печатает 12 (9 + 3)

2 + fun(x) => печатает 14 как следует (2 + 12)

fun(x) + 2 => получить ошибку!

1 Ответ

1 голос
/ 30 апреля 2020

Внутри вашего p_function_expression вы делаете рекурсивный вызов метода parse:

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

(Примечание: как указано в вашем вопросе, в этой строке произошла синтаксическая ошибка из постороннего . после последней круглой скобки.)

Этот вызов parse неявно использует глобальный лексер; то есть текущее значение lex.lexer (которое является последним лексером, созданным lex.lex()). Но лексеры Ply (и, действительно, большинство лексеров) с состоянием ; лексер поддерживает текущую строку ввода и текущую позицию ввода, а также некоторую другую информацию (например, текущее состояние лексера, если вы используете несколько состояний ).

Как следствие, рекурсивный вызов parse оставляет (глобальный) лексер указанным в конце строки. Поэтому, когда внешний parse пытается прочитать следующий токен (фактически второй следующий токен, поскольку у него уже есть токен предпросмотра), он получает EOF от лексера, что вызывает синтаксическую ошибку.

Вы можете это можно увидеть, включив отладку парсера :

answer = parser.parse(question, debug=True)

Эта проблема кратко описана в руководстве Ply , где указано, что вам следует clone лексер для реентерабельный лексический анализ.

К сожалению, в руководстве Ply не упоминается, что объект Ply parser также не реентерабелен. В случае синтаксического анализатора проблемы повторного входа несколько менее очевидны; они действительно применяются только во время восстановления синтаксической ошибки (если только вы не сохраните свои постоянные данные в объекте синтаксического анализатора, что вам разрешено делать). У синтаксического анализатора нет метода клонирования, главным образом потому, что таблицы синтаксического анализа уже были предварительно вычислены и кэшированы, поэтому создание нового синтаксического анализатора не так дорого, как создание нового лексера.

Короче говоря, вам нужно было выполнить внутренний анализ с объектом синтаксического анализатора fre sh, который использует объект лексера fre sh:

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

(объект синтаксического анализатора не имеет постоянного атрибута, который хранит текущий лексер, но он доступен в аргументе, передаваемом функциям действия синтаксического анализатора как p.lexer. См. очень полезное руководство Ply .)

Кроме того, вы действительно должны изучить цель из Python словарей. Они точно разработаны таким образом, что вам не нужно l oop для всех записей, чтобы найти нужный ключ. Вы можете найти ключ в простой операции 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, который возвращает значение по умолчанию:

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]))

Вы также можете сделать это, посмотрев имя внутри try блок, который ловит KeyError.

Я думаю, это должно go, не говоря, что это , а не хороший способ выполнить sh задачу, которую вы поставили перед собой. Было бы гораздо лучше представить функциональные тела в виде предварительно проанализированного AST, который впоследствии можно было бы оценить с использованием фактических параметров. В этой модели вам вообще не требуется немедленная оценка; все анализируется в AST, и когда (если) вы хотите оценить проанализированный текст, вы вызываете для этого метод eval AST.

...