Анализатор уравнения (выражения) с приоритетом?


105

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

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

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

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

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

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

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

Грамматика:

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

Язык:

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

Пример кода

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

Связанный вопрос

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

-Адам


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

Насколько я понимаю, вам нужно разобрать только арифметическое выражение. Использовать обратную польскую нотацию
mishadoff 01

Ответы:


69

Трудный путь

Вам нужен синтаксический анализатор с рекурсивным спуском .

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

1+11*5

чтобы сделать это вручную, вам нужно будет прочитать 1, затем увидеть плюс и начать совершенно новый «сеанс» рекурсивного синтаксического анализа, начиная с 11... и обязательно проанализировать его 11 * 5на свой собственный фактор, получив дерево синтаксического анализа с 1 + (11 * 5).

Все это кажется настолько болезненным, что даже пытаться объяснить, особенно с дополнительным бессилием C. Видите ли, после разбора 11, если бы вместо * на самом деле был +, вам пришлось бы отказаться от попытки составить термин и вместо этого проанализировать 11сам как фактор. Моя голова уже взрывается. Это возможно с рекурсивной приличной стратегией, но есть способ получше ...

Легкий (правильный) путь

Если вы используете инструмент GPL, такой как Bison, вам, вероятно, не нужно беспокоиться о проблемах с лицензированием, поскольку код C, созданный bison, не покрывается GPL (IANAL, но я почти уверен, что инструменты GPL не заставляют GPL сгенерированный код / ​​двоичные файлы; например, Apple компилирует код, например, Aperture с GCC, и они продают его без необходимости использования указанного кода GPL).

Скачать Bison (или аналогичный, ANTLR и т. Д.).

Обычно есть пример кода, на котором вы можете просто запустить bison и получить желаемый код на C, который демонстрирует этот калькулятор с четырьмя функциями:

http://www.gnu.org/software/bison/manual/html_node/Infix-Calc.html

Посмотрите на сгенерированный код и убедитесь, что это не так просто, как кажется. Кроме того, преимущества использования такого инструмента, как Bison, заключаются в следующем: 1) вы чему-то учитесь (особенно если вы читаете книгу о драконах и изучаете грамматику), 2) вы избегаете попыток NIH изобрести колесо. С настоящим инструментом-генератором синтаксического анализатора у вас действительно есть надежда на масштабирование позже, показывая другим людям, которых вы знаете, что синтаксические анализаторы - это область инструментов синтаксического анализа.


Обновить:

Здесь есть много дельных советов. Мое единственное предупреждение против пропуска инструментов синтаксического анализа или использования алгоритма Shunting Yard или ручного рекурсивного приличного парсера - это маленькие игрушечные языки 1 могут когда-нибудь превратиться в большие реальные языки с функциями (sin, cos, log) и переменными, условиями и для петли.

Flex / Bison может оказаться излишним для небольшого простого интерпретатора, но одноразовый синтаксический анализатор + оценщик может вызвать проблемы в дальнейшем, когда необходимо внести изменения или добавить функции. Ваша ситуация будет отличаться, и вам нужно будет использовать свое суждение; просто не наказывайте других людей за свои грехи [2] и создавайте неадекватный инструмент.

Мой любимый инструмент для парсинга

Лучшим инструментом в мире для этой работы является библиотека Parsec (для рекурсивных приличных парсеров), которая поставляется с языком программирования Haskell. Она очень похожа на BNF или на какой-то специализированный инструмент или предметно-ориентированный язык для синтаксического анализа (пример кода [3]), но на самом деле это обычная библиотека в Haskell, что означает, что она компилируется на том же этапе сборки, что и остальные. вашего кода Haskell, и вы можете написать произвольный код Haskell и вызвать его в своем парсере, и вы можете смешивать и сопоставлять другие библиотеки в одном и том же коде . (Встраивание подобного языка синтаксического анализа в язык, отличный от Haskell, кстати, приводит к множеству синтаксических ошибок. Я сделал это на C #, и он работает довольно хорошо, но не так красиво и лаконично.)

Ноты:

1 Ричард Столлман говорит, почему не следует использовать Tcl

Главный урок Emacs состоит в том, что язык расширений не должен быть просто «языком расширений». Это должен быть настоящий язык программирования, предназначенный для написания и поддержки значимых программ. Потому что люди захотят это сделать!

[2] Да, я навсегда испорчен этим «языком».

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

[3] Фрагмент синтаксического анализатора Haskell с использованием Parsec: калькулятор с четырьмя функциями, расширенный показателями, круглыми скобками, пробелами для умножения и константами (такими как pi и e).

aexpr   =   expr `chainl1` toOp
expr    =   optChainl1 term addop (toScalar 0)
term    =   factor `chainl1` mulop
factor  =   sexpr  `chainr1` powop
sexpr   =   parens aexpr
        <|> scalar
        <|> ident

powop   =   sym "^" >>= return . (B Pow)
        <|> sym "^-" >>= return . (\x y -> B Pow x (B Sub (toScalar 0) y))

toOp    =   sym "->" >>= return . (B To)

mulop   =   sym "*" >>= return . (B Mul)
        <|> sym "/" >>= return . (B Div)
        <|> sym "%" >>= return . (B Mod)
        <|>             return . (B Mul)

addop   =   sym "+" >>= return . (B Add) 
        <|> sym "-" >>= return . (B Sub)

scalar = number >>= return . toScalar

ident  = literal >>= return . Lit

parens p = do
             lparen
             result <- p
             rparen
             return result

9
Чтобы подчеркнуть мою точку зрения, обратите внимание, что разметка в моем сообщении не анализируется правильно (и это зависит от разметки, отображаемой статически, и той, которая отображается в предварительном просмотре WMD). Было несколько попыток исправить это, но я думаю, что ПАРСЕР НЕПРАВИЛЬНЫЙ. Сделайте всем одолжение и проведите разбор правильно!
Джаред Апдайк,

155

Алгоритм шунтирование двор является правильным инструментом для этого. Википедия действительно сбивает с толку, но в основном алгоритм работает так:

Скажем, вы хотите оценить 1 + 2 * 3 + 4. Интуитивно вы «знаете», что сначала должны выполнить 2 * 3, но как получить этот результат? Ключ состоит в том, чтобы понять, что, просматривая строку слева направо, вы оцените оператор, когда оператор, который следующий за ним имеет более низкий (или равный) приоритет. В контексте примера вот что вы хотите сделать:

  1. Посмотри: 1 + 2, ничего не делай.
  2. Теперь посмотрите на 1 + 2 * 3, по-прежнему ничего не делайте.
  3. Теперь посмотрите на 1 + 2 * 3 + 4, теперь вы знаете, что 2 * 3 нужно вычислить, потому что следующий оператор имеет более низкий приоритет.

Как это реализовать?

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

Возвращаясь к примеру, это работает так:

N = [] Операции = []

  • Прочтите 1. N = [1], Ops = []
  • Читать +. N = [1], Ops = [+]
  • Читать 2. N = [1 2], Ops = [+]
  • Прочтите *. N = [1 2], Ops = [+ *]
  • Прочтите 3. N = [1 2 3], Ops = [+ *]
  • Читать +. N = [1 2 3], Ops = [+ *]
    • Вытяните 3, 2 и выполните 2 *3, а результат перенесите на N. N = [1 6], Ops = [+]
    • +остается ассоциативным, поэтому вы также хотите убрать 1, 6 и выполнить +. N = [7], Ops = [].
    • Наконец, вставьте [+] в стек операторов. N = [7], Ops = [+].
  • Прочтите 4. N = [7 4]. Операции = [+].
  • У вас закончился ввод, поэтому вы хотите очистить стеки сейчас. После чего вы получите результат 11.

Вот это не так уж и сложно, правда? И он не вызывает никаких грамматик или генераторов парсеров.


6
На самом деле вам не нужны две стопки, если вы можете видеть вторую часть стопки, не открывая верхнюю. Вместо этого вы можете использовать один стек, в котором чередуются числа и операторы. Фактически это точно соответствует тому, что делает генератор парсера LR (например, bison).
Крис Додд

2
Действительно хорошее объяснение алгоритма, который я только что реализовал. Также вы не конвертируете его в постфикс, что тоже хорошо. Добавить поддержку скобок тоже очень просто.
Giorgi

4
Упрощенную версию алгоритма маневрового двора можно найти здесь: andreinc.net/2010/10/05/… (с реализациями на Java и python)
Андрей Чобану

1
Спасибо за это, именно то, что мне нужно!
Джо Грин

Большое спасибо за упоминание о левом - ассоциативном. Я придерживался тернарного оператора: как разбирать сложные выражения с вложенными "?:". Я понял, что оба '?' и ':' должны иметь одинаковый приоритет. А если мы интерпретируем "?" как справа - ассоциативно и ':' как слева - ассоциативно, этот алгоритм работает с ними очень хорошо. Кроме того, мы можем свернуть 2 оператора, только когда они оба слева - ассоциативны.
Владислав

25

http://www.engr.mun.ca/~theo/Misc/exp_parsing.htm

Очень хорошее объяснение разных подходов:

  • Распознавание с рекурсивным спуском
  • Алгоритм маневровой площадки
  • Классическое решение
  • Восхождение на приоритетность

Написано простым языком и псевдокодом.

Мне нравится лазание по приоритету.


Ссылка вроде не работает. Лучшим ответом было бы перефразирование каждого метода так, чтобы, когда эта ссылка исчезнет, ​​часть этой полезной информации сохранилась бы здесь.
Адам Уайт

18

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


16

Давным-давно я составил свой собственный алгоритм синтаксического анализа, который не нашел ни в одной книге по синтаксическому анализу (например, в «Книге дракона»). Глядя на указатели на алгоритм маневровой станции, я вижу сходство.

Около двух лет назад я написал об этом сообщение вместе с исходным кодом Perl на http://www.perlmonks.org/?node_id=554516 . Его легко перенести на другие языки: первая реализация, которую я сделал, была на ассемблере Z80.

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

Обновить Поскольку больше людей могут читать (или запускать) Javascript, я повторно реализовал свой синтаксический анализатор в Javascript после реорганизации кода. Весь парсер занимает менее 5 КБ кода Javascript (около 100 строк для парсера, 15 строк для функции-оболочки), включая отчеты об ошибках и комментарии.

Вы можете найти живую демонстрацию на http://users.telenet.be/bartl/expressionParser/expressionParser.html .

// operator table
var ops = {
   '+'  : {op: '+', precedence: 10, assoc: 'L', exec: function(l,r) { return l+r; } },
   '-'  : {op: '-', precedence: 10, assoc: 'L', exec: function(l,r) { return l-r; } },
   '*'  : {op: '*', precedence: 20, assoc: 'L', exec: function(l,r) { return l*r; } },
   '/'  : {op: '/', precedence: 20, assoc: 'L', exec: function(l,r) { return l/r; } },
   '**' : {op: '**', precedence: 30, assoc: 'R', exec: function(l,r) { return Math.pow(l,r); } }
};

// constants or variables
var vars = { e: Math.exp(1), pi: Math.atan2(1,1)*4 };

// input for parsing
// var r = { string: '123.45+33*8', offset: 0 };
// r is passed by reference: any change in r.offset is returned to the caller
// functions return the parsed/calculated value
function parseVal(r) {
    var startOffset = r.offset;
    var value;
    var m;
    // floating point number
    // example of parsing ("lexing") without aid of regular expressions
    value = 0;
    while("0123456789".indexOf(r.string.substr(r.offset, 1)) >= 0 && r.offset < r.string.length) r.offset++;
    if(r.string.substr(r.offset, 1) == ".") {
        r.offset++;
        while("0123456789".indexOf(r.string.substr(r.offset, 1)) >= 0 && r.offset < r.string.length) r.offset++;
    }
    if(r.offset > startOffset) {  // did that work?
        // OK, so I'm lazy...
        return parseFloat(r.string.substr(startOffset, r.offset-startOffset));
    } else if(r.string.substr(r.offset, 1) == "+") {  // unary plus
        r.offset++;
        return parseVal(r);
    } else if(r.string.substr(r.offset, 1) == "-") {  // unary minus
        r.offset++;
        return negate(parseVal(r));
    } else if(r.string.substr(r.offset, 1) == "(") {  // expression in parens
        r.offset++;   // eat "("
        value = parseExpr(r);
        if(r.string.substr(r.offset, 1) == ")") {
            r.offset++;
            return value;
        }
        r.error = "Parsing error: ')' expected";
        throw 'parseError';
    } else if(m = /^[a-z_][a-z0-9_]*/i.exec(r.string.substr(r.offset))) {  // variable/constant name        
        // sorry for the regular expression, but I'm too lazy to manually build a varname lexer
        var name = m[0];  // matched string
        r.offset += name.length;
        if(name in vars) return vars[name];  // I know that thing!
        r.error = "Semantic error: unknown variable '" + name + "'";
        throw 'unknownVar';        
    } else {
        if(r.string.length == r.offset) {
            r.error = 'Parsing error at end of string: value expected';
            throw 'valueMissing';
        } else  {
            r.error = "Parsing error: unrecognized value";
            throw 'valueNotParsed';
        }
    }
}

function negate (value) {
    return -value;
}

function parseOp(r) {
    if(r.string.substr(r.offset,2) == '**') {
        r.offset += 2;
        return ops['**'];
    }
    if("+-*/".indexOf(r.string.substr(r.offset,1)) >= 0)
        return ops[r.string.substr(r.offset++, 1)];
    return null;
}

function parseExpr(r) {
    var stack = [{precedence: 0, assoc: 'L'}];
    var op;
    var value = parseVal(r);  // first value on the left
    for(;;){
        op = parseOp(r) || {precedence: 0, assoc: 'L'}; 
        while(op.precedence < stack[stack.length-1].precedence ||
              (op.precedence == stack[stack.length-1].precedence && op.assoc == 'L')) {  
            // precedence op is too low, calculate with what we've got on the left, first
            var tos = stack.pop();
            if(!tos.exec) return value;  // end  reached
            // do the calculation ("reduce"), producing a new value
            value = tos.exec(tos.value, value);
        }
        // store on stack and continue parsing ("shift")
        stack.push({op: op.op, precedence: op.precedence, assoc: op.assoc, exec: op.exec, value: value});
        value = parseVal(r);  // value on the right
    }
}

function parse (string) {   // wrapper
    var r = {string: string, offset: 0};
    try {
        var value = parseExpr(r);
        if(r.offset < r.string.length){
          r.error = 'Syntax error: junk found at offset ' + r.offset;
            throw 'trailingJunk';
        }
        return value;
    } catch(e) {
        alert(r.error + ' (' + e + '):\n' + r.string.substr(0, r.offset) + '<*>' + r.string.substr(r.offset));
        return;
    }    
}

11

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

Редактировать:

Тот факт, что вы не понимаете вопрос грамматики и что «вы написали это от руки», очень вероятно, объясняет, почему у вас проблемы с выражениями формы «1 + 11 * 5» (то есть с приоритетом оператора) . Например, поиск в Google "грамматики для арифметических выражений" должен дать несколько хороших указателей. Такую грамматику не нужно усложнять:

<Exp> ::= <Exp> + <Term> |
          <Exp> - <Term> |
          <Term>

<Term> ::= <Term> * <Factor> |
           <Term> / <Factor> |
           <Factor>

<Factor> ::= x | y | ... |
             ( <Exp> ) |
             - <Factor> |
             <Number>

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

Предлагаю вам взглянуть , например, на эту ветку.

Почти во всех введениях в грамматику / синтаксический анализ арифметические выражения рассматриваются как пример.

Обратите внимание, что использование грамматики вовсе не означает использования определенного инструмента ( а-ля Yacc, Bison, ...). В самом деле, вы наверняка уже используете следующую грамматику:

<Exp>  :: <Leaf> | <Exp> <Op> <Leaf>

<Op>   :: + | - | * | /

<Leaf> :: <Number> | (<Exp>)

(или что-то в этом роде), не зная об этом!


8

Вы думали об использовании Boost Spirit ? Он позволяет писать на C ++ грамматики, подобные EBNF, следующим образом:

group       = '(' >> expression >> ')';
factor      = integer | group;
term        = factor >> *(('*' >> factor) | ('/' >> factor));
expression  = term >> *(('+' >> term) | ('-' >> term));

1
+1 И в итоге все является частью Boost. Грамматика калькулятора находится здесь: spirit.sourceforge.net/distrib/spirit_1_8_5/libs/spirit/example/… . Реализация калькулятора находится здесь: spirit.sourceforge.net/distrib/spirit_1_8_5/libs/spirit/example/… . И документация находится здесь: spirit.sourceforge.net/distrib/spirit_1_8_5/libs/spirit/doc/… . Я никогда не пойму, почему люди до сих пор реализуют собственные мини-парсеры.
Stephan

5

Как вы задаете свой вопрос, в рекурсии нет никакой необходимости. Ответ состоит в трех вещах: нотация Postfix плюс алгоритм Shunting Yard плюс оценка выражения Postfix:

1). Обозначение Postfix = придумано, чтобы устранить необходимость в явной спецификации приоритета. Читайте больше в сети, но вот суть: инфиксное выражение (1 + 2) * 3, хотя людям легко читать и обрабатывать, не очень эффективно для вычислений через машину. Что? Простое правило, которое гласит: «Переписать выражение путем кэширования с приоритетом, затем всегда обрабатывать его слева направо». Таким образом, инфикс (1 + 2) * 3 становится постфиксом 12 + 3 *. POST, потому что оператор всегда ставится ПОСЛЕ операндов.

2). Вычисление постфиксного выражения. Легко. Считывать числа из строки постфикса. Складывайте их в стопку, пока не появится оператор. Проверить тип оператора - одинарный? двоичный? высшее? Извлеките из стека столько операндов, сколько необходимо для оценки этого оператора. Оцените. Положите результат обратно в стек! И ур почти готов. Продолжайте делать это, пока в стеке не будет только одна запись = значение, которое ищет ur.

Давайте сделаем (1 + 2) * 3, который в постфиксе равен «12 + 3 *». Прочтите первое число = 1. Поместите его в стек. Читайте дальше. Число = 2. Вставьте его в стек. Читайте дальше. Оператор. Который из? +. Какой? Двоичный = требует двух операндов. Пополнить стек дважды = argright равно 2, а argleft равно 1. 1 + 2 равно 3. Вставить 3 обратно в стек. Читать далее из строки постфикса. Это номер. 3. нажмите. Читайте дальше. Оператор. Который из? *. Какой? Binary = нужно два числа -> дважды вытолкнуть стек. Первый раз в argright, второй раз в argleft. Оцените операцию - 3 умножить на 3 будет 9. Положите 9 в стек. Прочтите следующий постфиксный символ. Это ноль. Конец ввода. Pop stack onec = это ваш ответ.

3). Shunting Yard используется для преобразования человеческого (легко) читаемого инфиксного выражения в постфиксное выражение (также легко читаемое человеком после некоторой практики). Легко кодировать вручную. См. Комментарии выше и в сети.


4

Какой язык вы хотите использовать? ANTLR позволит вам сделать это с точки зрения Java. Адриан Кун написал отличную статью о том, как написать исполняемую грамматику на Ruby; фактически, его пример - это почти в точности ваш пример арифметического выражения.


Я должен признать, что мои примеры, приведенные в сообщении в блоге, ошибаются левой рекурсией, то есть a - b - c оценивается как (a - (b -c)) вместо ((a -b) - c). Фактически, это напоминает мне о добавлении задачи, в которой я должен исправить сообщения в блоге.
akuhn

4

Это зависит от того, насколько «общим» вы хотите быть.

Если вы хотите, чтобы он был действительно действительно общим, например, чтобы иметь возможность анализировать математические функции, а также sin (4 + 5) * cos (7 ^ 3), вам, вероятно, понадобится дерево синтаксического анализа.

Я не думаю, что здесь уместно вставлять полную реализацию. Я бы посоветовал вам почитать одну из печально известных « Драконьих книг ».

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

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


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

1
Ух ты - приятно осознавать, что «Драконья книга» все еще обсуждается. Я помню, как изучал это - и читал все - в университете 30 лет назад.
Кот Шредингерс

4

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

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


4

Я нашел это в PIClist об алгоритме маневровой станции :

Гарольд пишет:

Я помню, как давным-давно читал об алгоритме, который преобразовывал алгебраические выражения в RPN для облегчения вычисления. Каждое инфиксное значение, оператор или скобка было представлено железнодорожным вагоном на пути. Машины одного типа свернули на другую трассу, а другая продолжила движение прямо. Подробностей не припомню (очевидно!), Но всегда думал, что кодировать было бы интересно. Это было тогда, когда я писал ассемблерный код 6800 (а не 68000).

Это «алгоритм маневрового двора», который используют большинство машинных парсеров. См. Статью о парсинге в Википедии. Простой способ запрограммировать алгоритм маневровой площадки - использовать два стека. Один - это стек «push», а другой - стек «уменьшения» или «результата». Пример:

pstack = () // пустой rstack = () input: 1 + 2 * 3 Priority = 10 // наименьшее сокращение = 0 // не уменьшать

start: token '1': isnumber, положить в pstack (push) token '+': isoperator set priority = 2 if Priority <previous_operator_precedence then reduce () // см. ниже поместите '+' в pstack (push) token '2' : isnumber, помещаем в pstack (push) token '*': isoperator, устанавливаем приоритет = 1, помещаем в pstack (push) // проверяем приоритет как // над токеном '3': isnumber, помещаем в pstack (push) конец input, нужно уменьшить (цель пуста pstack) reduce () // готово

чтобы уменьшить, вытолкните элементы из стека push и поместите их в стек результатов, всегда меняйте местами 2 верхних элемента в pstack, если они имеют форму 'оператор' число ':

pstack: '1' '+' '2' ' ' '3' rstack: () ... pstack: () rstack: '3' '2' ' ' '1' '+'

если бы выражение было:

1 * 2 + 3

тогда триггером сокращения было бы чтение токена '+', имеющего более низкий предшествующий, чем уже выдвинутый '*', поэтому он бы сделал:

pstack: '1' ' ' '2' rstack: () ... pstack: () rstack: '1' '2' ' ' '

а затем нажал "+", затем "3" и, наконец, уменьшил:

pstack: '+' '3' rstack: '1' '2' ' ' ... pstack: () rstack: '1' '2' ' ' '3' '+'

Таким образом, краткая версия такова: нажимайте числа, при нажатии операторов проверяется приоритет предыдущего оператора. Если он был выше, чем оператор, который должен быть отправлен сейчас, сначала уменьшите, а затем нажмите текущего оператора. Чтобы обработать парные символы, просто сохраните приоритет оператора «previous» и поставьте метку на pstack, которая сообщает алгоритму сокращения прекратить сокращение при решении внутренней пары пар пар. Закрывающая скобка запускает сокращение, как и конец ввода, а также удаляет открытую скобку из pstack и восстанавливает приоритет «предыдущей операции», так что синтаксический анализ может продолжаться после закрывающей скобки, на которой он остановился. Это можно сделать с рекурсией или без нее (подсказка: используйте стек для сохранения предыдущего приоритета при обнаружении '(' ...). Обобщенная версия этого заключается в использовании генератора синтаксического анализатора, реализованного в алгоритме маневрового двора, например. используя yacc, bison или taccle (tcl аналог yacc).

Питер

-Адам


4

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

#include <stdio.h>
int main(int argc, char *argv[]){
  printf("((((");
  for(int i=1;i!=argc;i++){
    if(argv[i] && !argv[i][1]){
      switch(argv[i]){
      case '^': printf(")^("); continue;
      case '*': printf("))*(("); continue;
      case '/': printf("))/(("); continue;
      case '+': printf(")))+((("); continue;
      case '-': printf(")))-((("); continue;
      }
    }
    printf("%s", argv[i]);
  }
  printf("))))\n");
  return 0;
}

Вызовите его как:

$ cc -o parenthesise parenthesise.c
$ ./parenthesise a \* b + c ^ d / e
((((a))*((b)))+(((c)^(d))/((e))))

Что потрясающе своей простотой и очень понятно.


3
Довольно симпатичная жемчужина. Но его расширение (скажем, с применением функции, неявным умножением, префиксными и постфиксными операторами, необязательными аннотациями типов и т. Д.) Сломало бы все это. Другими словами, это элегантный прием.
Джаред Апдайк,

Я не вижу в этом смысла. Все это приводит к изменению проблемы синтаксического анализа приоритета оператора на проблему синтаксического анализа приоритета скобок.
Marquis of Lorne

@EJP конечно, но парсер в вопросе отлично обрабатывает скобки, так что это разумное решение. Однако если у вас есть парсер, которого нет, то вы правы, что это просто перемещает проблему в другую область.
Адам Дэвис,

4

Я опубликовал исходный код сверхкомпактного (1 класс, <10 КиБ) Java Math Evaluator. на моем веб-сайте. Это рекурсивный анализатор спуска того типа, который вызвал черепной взрыв для плаката принятого ответа.

Он поддерживает полный приоритет, круглые скобки, именованные переменные и функции с одним аргументом.




2

В настоящее время я работаю над серией статей о создании синтаксического анализатора регулярных выражений как средства обучения шаблонам проектирования и удобочитаемому программированию. Вы можете взглянуть на читаемый код . В статье представлено наглядное использование алгоритма маневровых дворов.


2

Я написал синтаксический анализатор выражений на F # и написал об этом здесь . Он использует алгоритм маневрового двора, но вместо преобразования инфикса в RPN я добавил второй стек для накопления результатов вычислений. Он правильно обрабатывает приоритет операторов, но не поддерживает унарные операторы. Я написал это для изучения F #, а не для изучения синтаксического анализа выражений.


2

Решение Python с использованием pyparsing можно найти здесь . Синтаксический анализ инфиксной нотации с различными операторами с приоритетом является довольно распространенным явлением, поэтому в процесс pyparsing также входит infixNotation(ранее operatorPrecedence) построитель выражений. С его помощью вы можете легко определять логические выражения, используя, например, «И», «ИЛИ», «НЕ». Или вы можете расширить свою арифметику с четырьмя функциями, чтобы использовать другие операторы, такие как! для факториала или "%" для модуля, или добавьте операторы P и C для вычисления перестановок и комбинаций. Вы можете написать инфиксный синтаксический анализатор для матричной записи, который включает обработку операторов «-1» или «T» (для инверсии и транспонирования). Пример operatorPrecedence для 4-х функционального парсера (с добавленным '!' Для развлечения) здесь .


1

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

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

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

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

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

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

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

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

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

1

Вот простое рекурсивное решение, написанное на Java. Обратите внимание, что он не обрабатывает отрицательные числа, но вы можете добавить это, если хотите:

public class ExpressionParser {

public double eval(String exp){
    int bracketCounter = 0;
    int operatorIndex = -1;

    for(int i=0; i<exp.length(); i++){
        char c = exp.charAt(i);
        if(c == '(') bracketCounter++;
        else if(c == ')') bracketCounter--;
        else if((c == '+' || c == '-') && bracketCounter == 0){
            operatorIndex = i;
            break;
        }
        else if((c == '*' || c == '/') && bracketCounter == 0 && operatorIndex < 0){
            operatorIndex = i;
        }
    }
    if(operatorIndex < 0){
        exp = exp.trim();
        if(exp.charAt(0) == '(' && exp.charAt(exp.length()-1) == ')')
            return eval(exp.substring(1, exp.length()-1));
        else
            return Double.parseDouble(exp);
    }
    else{
        switch(exp.charAt(operatorIndex)){
            case '+':
                return eval(exp.substring(0, operatorIndex)) + eval(exp.substring(operatorIndex+1));
            case '-':
                return eval(exp.substring(0, operatorIndex)) - eval(exp.substring(operatorIndex+1));
            case '*':
                return eval(exp.substring(0, operatorIndex)) * eval(exp.substring(operatorIndex+1));
            case '/':
                return eval(exp.substring(0, operatorIndex)) / eval(exp.substring(operatorIndex+1));
        }
    }
    return 0;
}

}


1

Алгоритм можно легко закодировать на 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;
}

могут быть полезны следующие библиотеки : yupana - строго арифметические операции; 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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.