Как изменить грамматику калькулятора синтаксического анализатора, чтобы продолжить вычисления по предыдущему результату? - PullRequest
0 голосов
/ 17 сентября 2018

Я пытаюсь сделать калькулятор парсера в Python, используя PLY.Я начал с некоторого примера кода PLY и прошел путь оттуда.Я пытаюсь добавить функциональность для продолжения расчетов по предыдущему результату.Поэтому, если вы наберете «4 + 5», результат будет равен 9. Если вы наберете «* 2 - 3», новый результат должен быть 15, но с моим кодом это -9, потому что сначала он анализирует «2 - 3»,когда он должен сначала разобрать '9 * 2'.Эта проблема возникает, когда я использую умножение или деление в качестве первого оператора при выполнении вычисления с предыдущим результатом.

Как показано в моем фрагменте кода, я пытался дать приоритет выражениям, которые использовали предыдущий результат, но я все ещеимеют ту же проблему.

'r' - это переменная, в которой хранится предыдущий результат.

tokens = (
    'NUMBER',
)

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

precedence = (
    ('left', '+', '-'),
    ('right', 'RADD', 'RSUB'),
    ('left', '*', '/'),
    ('right', 'RMUL', 'RDIV'),
    ('right', 'UMINUS'),
)

def p_statement_expr(p):
    'statement : expression'
    p[0] = 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_cont(p):
    '''statement : '+' expression %prec RADD
                  | '-' expression %prec RSUB
                  | '*' expression %prec RMUL
                  | '/' expression %prec RDIV '''
    if p[1] == '+':
        p[0] = r + p[2]
    elif p[1] == '-':
        p[0] = r - p[2]
    elif p[1] == '*':
        p[0] = r * p[2]
    elif p[1] == '/':
        p[0] = r / p[2]   

def p_expression_uminus(p):
    "expression : '(' '-' expression ')' %prec UMINUS"

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

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

Я также попытался изменить грамматику на

p_expression_cont(p):
'''expression : '+' expression %prec MORADD
              | '-' expression %prec MORSUB
              | '*' expression %prec MORMUL
              | '/' expression %prec MORDIV '''

, котораярешил мою первоначальную проблему, но теперь что-то вроде '++ - * ++ / 23' можно разбирать, чего я явно не хочу.Как мне изменить мою грамматику, чтобы получить правильные вычисления при использовании предыдущего результата?

1 Ответ

0 голосов
/ 17 сентября 2018

Это легко выполнимо с помощью PLY (или любого другого генератора, похожего на yacc), но важно иметь некоторое представление о природе того, что вы пытаетесь проанализировать.

Интуитивно, такая строка продолжения, как* 2 - 3 должен быть проанализирован, как если бы он был записан как <prev> * 2 - 3, где <prev> - нетерминал, который каким-то образом представляет предыдущее значение.

Для простого случая мы могли бы просто определить

expression : '$'

, так что предыдущее значение представлено явным токеном $.Тогда остальная часть грамматики просто сработает.

Конечно, предполагается, что пользователю не нужно вводить $ (хотя, на самом деле, они могут найти его полезным. См. Ниже.)вместо этого нам нужен

expression: prev

, где нетерминал prev должен представлять пустую последовательность токенов.Нет проблем с тем, что нетерминал будет представлен нулевыми входными токенами, но, очевидно, нам нужно ограничить обстоятельства, при которых это может произойти.Поэтому мы не можем просто добавить:

prev :

к грамматике, потому что это приведет к проблеме, с которой вы уже столкнулись: она делает 2**4 действительным, как если бы это было 2*<prev>*4.

Очевидно, мы хотим, чтобы грамматика производила только пустые <prev> в самом начале оператора.Осталось только выяснить, как это сделать.

Давайте предположим, что у нас есть два вида expression: один вид допускает пустую левую часть, а другой нет.Как бы мы определили эти два нетерминала?

Второе - это просто обычное определение:

expression : expression '+' expression
expression : expression '-' expression
expression : expression '*' expression
expression : expression '/' expression
expression : '-' atom                    # See note below
expression : atom
atom : NUMBER
atom : '(' expression ')'

(я выделил atom постановок по причинам, которые будут рассмотрены ниже).

А как насчет выражений, левый операнд которых может быть неявным?Они очень похожи:

expr_cont : expr_cont '+' expression
expr_cont : expr_cont '-' expression
expr_cont : expr_cont '*' expression
expr_cont : expr_cont '/' expression
expr_cont : atom

Но есть еще один случай:

expr_cont :

В первых четырех постановках мы говорим, что в expr_cont с оператором,правый операнд должен быть обычным expression, но левый операнд может быть выражением с неявным левым операндом.В последнем производстве мы допускаем неявный левый операнд (но только для этого вида выражения).Явные левые операнды - числа и выражения в скобках - одинаковы, поэтому мы можем использовать созданный выше нетерминал atom.


Примечание об унарном минусе: В исходной грамматике унарный минус допускался только в скобках, и его приоритет был неправильным.Первоначальное производство было:

expression: '(' '-' expression ')' %prec UMINUS

В этом производстве объявление приоритета совершенно бессмысленно, потому что само производство никогда не бывает неоднозначным.(Он однозначно окружен круглыми скобками, поэтому его необходимо уменьшить, когда встречаются закрывающие круглые скобки.) Применение унарного минуса в скобках также однозначно, но неверно: производство эффективно указывает, что унарный минус применяется ко всему выражениюзаключены в круглые скобки, так что (-4 - 2) будет иметь значение -2 (- (4-2)), а не ожидаемое -6.

Простое исправление, примененное в приведенной выше грамматике, явно указывает, что операнд в унарномминус atom, а не expression.Следовательно, -4 - 2 должен быть проанализирован как (-4) - 2, поскольку 4 - 2 не может быть atom.Однако это исправление устраняет требование, чтобы выражения, начинающиеся с унарного минуса, были заключены в скобки.

Тот факт, что унарный минус производится только в expression, а не expr_cont, означает, что выражения продолжения начинаются со знака минусбудет проанализирован как таковой, что было предположительно намерением ограничения.


Теперь реализация проста:

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

# We can use the same function for all unit productions which pass
# through the semantic value of the right-hand symbol.
# (A unit production is one whose right-hand side has a single symbol.)
def p_unit(p):
    '''statement  : expr_cont
       expression : atom
       expr_cont  : atom
       atom       : NUMBER
    '''
    p[0] = p[1]

# Personally, I would write this as four functions, one for each operator,
# rather than executing the chain of if statements every time.
def p_expression_binop(p):
    '''expression : expression '+' expression
                  | expression '-' expression
                  | expression '*' expression
                  | expression '/' expression
       expr_cont  : expr_cont  '+' expression
                  | expr_cont  '-' expression
                  | expr_cont  '*' expression
                  | expr_cont  '/' 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):
    '''expresion : '-' atom
    '''
    p[0] = -p[2]

def p_atom_group(p):
    '''atom : '(' expression ')'
    '''
    p[0] = p[2]

def p_expr_previous(p):
    '''expr_cont : 
    '''
    p[0] = r   # "previous" would be a better variable name than r

Как упоминалось выше, могут быть случаи, когда пользователю будет удобно иметь явный способ написания "предыдущего значения". Предположим, например, что они хотели увидеть результат <prev>²+1. Или предположим, что они просто хотели переопределить арифметический приоритет по умолчанию для вычисления (<prev> - 3) * 2. И то, и другое можно легко обеспечить, допустив использование, скажем, $ в качестве явного токена «предыдущего значения». После добавления его в список литералов потребуется изменить только последнюю функцию парсера:

def p_expr_previous(p):
    '''expr_cont  : 
                  | '$'
       expression : '$'
    '''
    p[0] = r   # "previous" would still be a better variable name than r
...