Уравнение (выражение) парсер с приоритетом? - PullRequest
98 голосов
/ 26 августа 2008

Я разработал синтаксический анализатор уравнений с использованием простого стекового алгоритма, который будет обрабатывать двоичные (+, -, |, &, *, / и т. Д.) Операторы, унарные (!) Операторы и скобки.

Однако использование этого метода оставляет мне все, что имеет одинаковый приоритет - оно оценивается слева направо, независимо от оператора, хотя приоритет можно применять с помощью скобок.

Так что сейчас "1 + 11 * 5" возвращает 60, а не 56, как можно было бы ожидать.

Хотя это подходит для текущего проекта, я хочу иметь программу общего назначения, которую можно использовать для более поздних проектов.

Отредактировано для ясности:

Что такое хороший алгоритм для анализа уравнений с приоритетом?

Меня интересует что-то простое для реализации, и я понимаю, что могу сам кодировать, чтобы избежать проблем с лицензированием доступного кода.

Грамматика:

Я не понимаю вопроса грамматики - я написал это от руки. Это достаточно просто, и я не вижу необходимости в YACC или Bison. Мне просто нужно вычислить строки с помощью таких уравнений, как «2 + 3 * (42/13)».

Язык:

Я делаю это на C, но меня интересует алгоритм, а не решение для конкретного языка. Уровень C достаточно низок, поэтому в случае необходимости будет легко перейти на другой язык.

Пример кода

Я разместил тестовый код для простого синтаксического анализатора выражений , о котором я говорил выше. Требования проекта изменились, и поэтому мне никогда не приходилось оптимизировать код для производительности или пространства, так как он не был включен в проект. Это в оригинальной многословной форме, и должно быть легко понятно. Если я сделаю что-то еще с точки зрения приоритета операторов, я, вероятно, выберу макрос взломать , потому что он по простоте соответствует остальной части программы. Если я когда-нибудь буду использовать это в реальном проекте, я пойду на более компактный / быстрый парсер.

Смежный вопрос

Умный дизайн математического парсера?

-Adam

Ответы [ 22 ]

1 голос
/ 10 марта 2014

Я знаю, что это поздний ответ, но я только что написал крошечный парсер, который позволяет всем операторам (префикс, постфикс и инфикс-левый, инфикс-правый и неассоциативный) иметь произвольный приоритет.

Я собираюсь расширить это для языка с произвольной поддержкой DSL, но я просто хотел отметить, что не нужны пользовательские парсеры для приоритета операторов, можно использовать обобщенный парсер, который не нуждается в таблицах в все, и просто ищет приоритет каждого оператора, как он выглядит. Люди упоминали пользовательские парсеры Pratt или парсинговые парсеры, которые могут принимать недопустимые входные данные - этот не нужно настраивать и (если нет ошибки) не примет неправильный ввод. В некотором смысле он не завершен, он был написан для проверки алгоритма, и его входные данные представлены в форме, которая потребует некоторой предварительной обработки, но есть комментарии, которые проясняют это.

Обратите внимание, что некоторые распространенные типы операторов отсутствуют, например, операторы, используемые для индексации, например таблица [index] или вызов функции функции (выражение-параметра, ...) Я собираюсь добавить их, но думать о них как о постфиксных операторах, где то, что находится между разделителями '[' и ']' или '(' и ')', анализируется с другим экземпляром синтаксического анализатора выражений. Извините, что пропустил это, но постфиксная часть включена - добавление остальных, вероятно, почти удвоит размер кода.

Поскольку синтаксический анализатор - это всего лишь 100 строк кода ракетки, возможно, я должен просто вставить его сюда, надеюсь, это не дольше, чем позволяет stackoverflow.

Несколько подробностей о произвольных решениях:

Если постфиксный оператор с низким приоритетом конкурирует за те же блоки инфикса, что и префиксный оператор с низким приоритетом, префиксный оператор побеждает. Это не подходит в большинстве языков, так как большинство не имеют постфиксных операторов с низким приоритетом. - например: ((данные a) (слева 1 +) (до 2 нет) (данные b) (сообщение 3!) (слева 1 +) (данные c)) a + not b! + c, где not - префиксный оператор и! постфиксный оператор и оба имеют более низкий приоритет чем +, поэтому они хотят сгруппировать несовместимыми способами либо как (а + не б!) + с или как a + (не b! + c) в этих случаях префиксный оператор всегда побеждает, поэтому вторым является способ синтаксического анализа

Неассоциативные инфиксные операторы действительно существуют, поэтому вам не нужно притворяться, что операторы, которые возвращают разные типы, чем они имеют смысл, имеют смысл вместе, но без разных типов выражений для каждого, это кладжа. Таким образом, в этом алгоритме неассоциативные операторы отказываются ассоциироваться не только с собой, но и с любым оператором с таким же приоритетом. Это распространенный случай, так как <<= ==> = и т. Д. Не ассоциируются друг с другом в большинстве языков.

Вопрос о том, как различные виды операторов (левые, префиксы и т. Д.) Разрывают связи по приоритету, не должен возникать, поскольку на самом деле не имеет смысла предоставлять операторам разных типов одинаковый приоритет. Этот алгоритм что-то делает в таких случаях, но я даже не удосужился выяснить, что именно, потому что такая грамматика - плохая идея.

#lang racket
;cool the algorithm fits in 100 lines!
(define MIN-PREC -10000)
;format (pre prec name) (left prec name) (right prec name) (nonassoc prec name) (post prec name) (data name) (grouped exp)
;for example "not a*-7+5 < b*b or c >= 4"
;which groups as: not ((((a*(-7))+5) < (b*b)) or (c >= 4))"
;is represented as '((pre 0 not)(data a)(left 4 *)(pre 5 -)(data 7)(left 3 +)(data 5)(nonassoc 2 <)(data b)(left 4 *)(data b)(right 1 or)(data c)(nonassoc 2 >=)(data 4)) 
;higher numbers are higher precedence
;"(a+b)*c" is represented as ((grouped (data a)(left 3 +)(data b))(left 4 *)(data c))

(struct prec-parse ([data-stack #:mutable #:auto]
                    [op-stack #:mutable #:auto])
  #:auto-value '())

(define (pop-data stacks)
  (let [(data (car (prec-parse-data-stack stacks)))]
    (set-prec-parse-data-stack! stacks (cdr (prec-parse-data-stack stacks)))
    data))

(define (pop-op stacks)
  (let [(op (car (prec-parse-op-stack stacks)))]
    (set-prec-parse-op-stack! stacks (cdr (prec-parse-op-stack stacks)))
    op))

(define (push-data! stacks data)
    (set-prec-parse-data-stack! stacks (cons data (prec-parse-data-stack stacks))))

(define (push-op! stacks op)
    (set-prec-parse-op-stack! stacks (cons op (prec-parse-op-stack stacks))))

(define (process-prec min-prec stacks)
  (let [(op-stack (prec-parse-op-stack stacks))]
    (cond ((not (null? op-stack))
           (let [(op (car op-stack))]
             (cond ((>= (cadr op) min-prec) 
                    (apply-op op stacks)
                    (set-prec-parse-op-stack! stacks (cdr op-stack))
                    (process-prec min-prec stacks))))))))

(define (process-nonassoc min-prec stacks)
  (let [(op-stack (prec-parse-op-stack stacks))]
    (cond ((not (null? op-stack))
           (let [(op (car op-stack))]
             (cond ((> (cadr op) min-prec) 
                    (apply-op op stacks)
                    (set-prec-parse-op-stack! stacks (cdr op-stack))
                    (process-nonassoc min-prec stacks))
                   ((= (cadr op) min-prec) (error "multiply applied non-associative operator"))
                   ))))))

(define (apply-op op stacks)
  (let [(op-type (car op))]
    (cond ((eq? op-type 'post)
           (push-data! stacks `(,op ,(pop-data stacks) )))
          (else ;assume infix
           (let [(tos (pop-data stacks))]
             (push-data! stacks `(,op ,(pop-data stacks) ,tos))))))) 

(define (finish input min-prec stacks)
  (process-prec min-prec stacks)
  input
  )

(define (post input min-prec stacks)
  (if (null? input) (finish input min-prec stacks)
      (let* [(cur (car input))
             (input-type (car cur))]
        (cond ((eq? input-type 'post)
               (cond ((< (cadr cur) min-prec)
                      (finish input min-prec stacks))
                     (else 
                      (process-prec (cadr cur)stacks)
                      (push-data! stacks (cons cur (list (pop-data stacks))))
                      (post (cdr input) min-prec stacks))))
              (else (let [(handle-infix (lambda (proc-fn inc)
                                          (cond ((< (cadr cur) min-prec)
                                                 (finish input min-prec stacks))
                                                (else 
                                                 (proc-fn (+ inc (cadr cur)) stacks)
                                                 (push-op! stacks cur)
                                                 (start (cdr input) min-prec stacks)))))]
                      (cond ((eq? input-type 'left) (handle-infix process-prec 0))
                            ((eq? input-type 'right) (handle-infix process-prec 1))
                            ((eq? input-type 'nonassoc) (handle-infix process-nonassoc 0))
                            (else error "post op, infix op or end of expression expected here"))))))))

;alters the stacks and returns the input
(define (start input min-prec stacks)
  (if (null? input) (error "expression expected")
      (let* [(cur (car input))
             (input-type (car cur))]
        (set! input (cdr input))
        ;pre could clearly work with new stacks, but could it reuse the current one?
        (cond ((eq? input-type 'pre)
               (let [(new-stack (prec-parse))]
                 (set! input (start input (cadr cur) new-stack))
                 (push-data! stacks 
                             (cons cur (list (pop-data new-stack))))
                 ;we might want to assert here that the cdr of the new stack is null
                 (post input min-prec stacks)))
              ((eq? input-type 'data)
               (push-data! stacks cur)
               (post input min-prec stacks))
              ((eq? input-type 'grouped)
               (let [(new-stack (prec-parse))]
                 (start (cdr cur) MIN-PREC new-stack)
                 (push-data! stacks (pop-data new-stack)))
               ;we might want to assert here that the cdr of the new stack is null
               (post input min-prec stacks))
              (else (error "bad input"))))))

(define (op-parse input)
  (let [(stacks (prec-parse))]
    (start input MIN-PREC stacks)
    (pop-data stacks)))

(define (main)
  (op-parse (read)))

(main)
0 голосов
/ 25 января 2018

Алгоритм может быть легко закодирован в C как синтаксический анализатор рекурсивного спуска.

#include <stdio.h>
#include <ctype.h>

/*
 *  expression -> sum
 *  sum -> product | product "+" sum
 *  product -> term | term "*" product
 *  term -> number | expression
 *  number -> [0..9]+
 */

typedef struct {
    int value;
    const char* context;
} expression_t;

expression_t expression(int value, const char* context) {
    return (expression_t) { value, context };
}

/* begin: parsers */

expression_t eval_expression(const char* symbols);

expression_t eval_number(const char* symbols) {
    // number -> [0..9]+
    double number = 0;        
    while (isdigit(*symbols)) {
        number = 10 * number + (*symbols - '0');
        symbols++;
    }
    return expression(number, symbols);
}

expression_t eval_term(const char* symbols) {
    // term -> number | expression
    expression_t number = eval_number(symbols);
    return number.context != symbols ? number : eval_expression(symbols);
}

expression_t eval_product(const char* symbols) {
    // product -> term | term "*" product
    expression_t term = eval_term(symbols);
    if (*term.context != '*')
        return term;

    expression_t product = eval_product(term.context + 1);
    return expression(term.value * product.value, product.context);
}

expression_t eval_sum(const char* symbols) {
    // sum -> product | product "+" sum
    expression_t product = eval_product(symbols);
    if (*product.context != '+')
        return product;

    expression_t sum = eval_sum(product.context + 1);
    return expression(product.value + sum.value, sum.context);
}

expression_t eval_expression(const char* symbols) {
    // expression -> sum
    return eval_sum(symbols);
}

/* end: parsers */

int main() {
    const char* expression = "1+11*5";
    printf("eval(\"%s\") == %d\n", expression, eval_expression(expression).value);

    return 0;
}

следующие библиотеки могут быть полезны: Юпана - строго арифметические операции; tinyexpr - арифметические операции + математические функции C + одна предоставлена ​​пользователем; mpc - комбинаторы синтаксического анализа

Объяснение

Давайте захватим последовательность символов, которые представляют алгебраическое выражение. Первый - это число, то есть десятичная цифра, повторяемая один или несколько раз. Мы будем называть такое обозначение правилом производства.

number -> [0..9]+

Оператор сложения с его операндами является еще одним правилом. Это либо number, либо любые символы, представляющие последовательность sum "*" sum.

sum -> number | sum "+" sum

Попробуйте заменить number на sum "+" sum, которое будет number "+" number, которое, в свою очередь, может быть расширено до [0..9]+ "+" [0..9]+, которое в итоге может быть уменьшено до 1+8, что является правильным выражением сложения.

Другие замены также приведут к правильному выражению: sum "+" sum -> number "+" sum -> number "+" sum "+" sum -> number "+" sum "+" number -> number "+" number "+" number -> 12+3+5

Постепенно мы можем напоминать набор правил производства он же грамматика , которые выражают все возможные алгебраические выражения.

expression -> sum
sum -> difference | difference "+" sum
difference -> product | difference "-" product
product -> fraction | fraction "*" product
fraction -> term | fraction "/" term
term -> "(" expression ")" | number
number -> digit+                                                                    

Чтобы контролировать приоритет оператора, измените положение его производственного правила относительно других. Посмотрите на грамматику выше и обратите внимание, что правило производства для * находится ниже +, это заставит product оценить до sum. Реализация просто объединяет распознавание образов с оценкой и, таким образом, близко отражает правила производства.

expression_t eval_product(const char* symbols) {
    // product -> term | term "*" product
    expression_t term = eval_term(symbols);
    if (*term.context != '*')
        return term;

    expression_t product = eval_product(term.context + 1);
    return expression(term.value * product.value, product.context);
}

Здесь мы сначала набираем term и возвращаем его, если после него нет символа * Это оставлено на усмотрение нашего производственного правила В противном случае - вычисляем символы после и возвращаем term.value * product.value это правильный выбор в нашем производственном правиле, т.е. term "*" product

...