Я разработал синтаксический анализатор уравнений, используя простой стековый алгоритм, который будет обрабатывать бинарные (+, -, |, &, *, / и т.д.) операторы, унарные (!) Операторы и круглые скобки.
Однако использование этого метода оставляет мне все, что имеет одинаковый приоритет - он оценивается слева направо независимо от оператора, хотя приоритет можно принудительно установить с помощью круглых скобок.
Итак, прямо сейчас «1 + 11 * 5» возвращает 60, а не 56, как можно было бы ожидать.
Хотя это подходит для текущего проекта, я хочу иметь рутину общего назначения, которую я могу использовать для последующих проектов.
Отредактировано для ясности:
Каков хороший алгоритм анализа уравнений с приоритетом?
Меня интересует что-то простое в реализации, и я понимаю, что могу писать код самостоятельно, чтобы избежать проблем с лицензированием с доступным кодом.
Грамматика:
Не понимаю грамматического вопроса - это я написал от руки. Это достаточно просто, я не вижу необходимости в YACC или Bison. Мне просто нужно вычислить строки с помощью таких уравнений, как «2 + 3 * (42/13)».
Язык:
Я делаю это на C, но меня интересует алгоритм, а не решение для конкретного языка. C достаточно низкий уровень, чтобы его можно было легко преобразовать на другой язык, если возникнет необходимость.
Пример кода
Я разместил тестовый код для простого парсера выражений, о котором говорил выше. Требования к проекту изменились, и поэтому мне никогда не приходилось оптимизировать код по производительности или пространству, поскольку он не был включен в проект. Он в исходной многословной форме и должен быть легко понятным. Если я сделаю что-нибудь еще с точки зрения приоритета операторов, я, вероятно, выберу макрокоманду, потому что он по простоте соответствует остальной программе. Однако, если я когда-нибудь использую это в реальном проекте, я выберу более компактный / быстрый парсер.
Связанный вопрос
-Адам
Ответы:
Трудный путь
Вам нужен синтаксический анализатор с рекурсивным спуском .
Чтобы получить приоритет, вам нужно думать рекурсивно, например, используя образец строки,
чтобы сделать это вручную, вам нужно будет прочитать
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
[2] Да, я навсегда испорчен этим «языком».
Также обратите внимание, что когда я отправил эту запись, предварительный просмотр был правильным, но неадекватный парсер SO съел мой закрытый тег привязки в первом абзаце , доказывая, что синтаксические анализаторы не являются чем-то, с чем можно шутить, потому что если вы используете регулярные выражения и один раз взломает вас возможно будет что-то тонкое и мелкое неправильно .
[3] Фрагмент синтаксического анализатора Haskell с использованием Parsec: калькулятор с четырьмя функциями, расширенный показателями, круглыми скобками, пробелами для умножения и константами (такими как pi и e).
источник
Алгоритм шунтирование двор является правильным инструментом для этого. Википедия действительно сбивает с толку, но в основном алгоритм работает так:
Скажем, вы хотите оценить 1 + 2 * 3 + 4. Интуитивно вы «знаете», что сначала должны выполнить 2 * 3, но как получить этот результат? Ключ состоит в том, чтобы понять, что, просматривая строку слева направо, вы оцените оператор, когда оператор, который следующий за ним имеет более низкий (или равный) приоритет. В контексте примера вот что вы хотите сделать:
Как это реализовать?
Вы хотите иметь два стека, один для чисел, а другой для операторов. Вы все время кладете числа в стек. Вы сравниваете каждый новый оператор с оператором наверху стека, если тот, который находится на вершине стека, имеет более высокий приоритет, вы извлекаете его из стека операторов, выталкиваете операнды из стека чисел, применяете оператор и отправляете результат в стек чисел. Теперь вы повторяете сравнение с оператором вершины стека.
Возвращаясь к примеру, это работает так:
N = [] Операции = []
*
. N = [1 2], Ops = [+ *]*
3, а результат перенесите на N. N = [1 6], Ops = [+]+
остается ассоциативным, поэтому вы также хотите убрать 1, 6 и выполнить +. N = [7], Ops = [].Вот это не так уж и сложно, правда? И он не вызывает никаких грамматик или генераторов парсеров.
источник
http://www.engr.mun.ca/~theo/Misc/exp_parsing.htm
Очень хорошее объяснение разных подходов:
Написано простым языком и псевдокодом.
Мне нравится лазание по приоритету.
источник
Там хорошая статья здесь о сочетании простого рекурсивного спуска парсер с оператором-старшинства разборе. Если вы недавно писали парсеры, это должно быть очень интересно и поучительно читать.
источник
Давным-давно я составил свой собственный алгоритм синтаксического анализа, который не нашел ни в одной книге по синтаксическому анализу (например, в «Книге дракона»). Глядя на указатели на алгоритм маневровой станции, я вижу сходство.
Около двух лет назад я написал об этом сообщение вместе с исходным кодом Perl на http://www.perlmonks.org/?node_id=554516 . Его легко перенести на другие языки: первая реализация, которую я сделал, была на ассемблере Z80.
Он идеально подходит для прямого вычисления с числами, но при необходимости вы можете использовать его для создания дерева синтаксического анализа.
Обновить Поскольку больше людей могут читать (или запускать) Javascript, я повторно реализовал свой синтаксический анализатор в Javascript после реорганизации кода. Весь парсер занимает менее 5 КБ кода Javascript (около 100 строк для парсера, 15 строк для функции-оболочки), включая отчеты об ошибках и комментарии.
Вы можете найти живую демонстрацию на http://users.telenet.be/bartl/expressionParser/expressionParser.html .
источник
Было бы полезно, если бы вы могли описать грамматику, которую в настоящее время используете для синтаксического анализа. Похоже, проблема в этом!
Редактировать:
Тот факт, что вы не понимаете вопрос грамматики и что «вы написали это от руки», очень вероятно, объясняет, почему у вас проблемы с выражениями формы «1 + 11 * 5» (то есть с приоритетом оператора) . Например, поиск в Google "грамматики для арифметических выражений" должен дать несколько хороших указателей. Такую грамматику не нужно усложнять:
может помочь, например, и может быть тривиально дополнен, чтобы позаботиться о некоторых более сложных выражениях (включая, например, функции или полномочия ...).
Предлагаю вам взглянуть , например, на эту ветку.
Почти во всех введениях в грамматику / синтаксический анализ арифметические выражения рассматриваются как пример.
Обратите внимание, что использование грамматики вовсе не означает использования определенного инструмента ( а-ля Yacc, Bison, ...). В самом деле, вы наверняка уже используете следующую грамматику:
(или что-то в этом роде), не зная об этом!
источник
Вы думали об использовании Boost Spirit ? Он позволяет писать на C ++ грамматики, подобные EBNF, следующим образом:
источник
Как вы задаете свой вопрос, в рекурсии нет никакой необходимости. Ответ состоит в трех вещах: нотация 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 используется для преобразования человеческого (легко) читаемого инфиксного выражения в постфиксное выражение (также легко читаемое человеком после некоторой практики). Легко кодировать вручную. См. Комментарии выше и в сети.
источник
Какой язык вы хотите использовать? ANTLR позволит вам сделать это с точки зрения Java. Адриан Кун написал отличную статью о том, как написать исполняемую грамматику на Ruby; фактически, его пример - это почти в точности ваш пример арифметического выражения.
источник
Это зависит от того, насколько «общим» вы хотите быть.
Если вы хотите, чтобы он был действительно действительно общим, например, чтобы иметь возможность анализировать математические функции, а также sin (4 + 5) * cos (7 ^ 3), вам, вероятно, понадобится дерево синтаксического анализа.
Я не думаю, что здесь уместно вставлять полную реализацию. Я бы посоветовал вам почитать одну из печально известных « Драконьих книг ».
Но если вам просто нужна поддержка приоритета , вы можете сделать это, сначала преобразовав выражение в постфиксную форму, в которой алгоритм, который вы можете копировать и вставлять, должен быть доступен из Google, или я думаю, вы можете закодировать его самостоятельно с помощью двоичного дерево.
Когда у вас есть постфиксная форма, то с этого момента это кусок пирога, поскольку вы уже понимаете, как помогает стек.
источник
Я бы посоветовал обмануть и использовать алгоритм маневрового двора. . Это простой способ написать простой синтаксический анализатор типа калькулятора, который учитывает приоритет.
Если вы хотите правильно разметить вещи и задействовать переменные и т. Д., Тогда я бы пошел дальше и написал парсер рекурсивного спуска, как предлагали другие здесь, однако, если вам просто нужен синтаксический анализатор в стиле калькулятора, этого алгоритма должно быть достаточно :-)
источник
Я нашел это в PIClist об алгоритме маневровой станции :
-Адам
источник
Еще один ресурс для синтаксического анализа приоритета - это запись парсера приоритета операторов в Википедии. Охватывает алгоритм маневрового двора Дейкстры и альтернативный алгоритм дерева, но, в частности, охватывает действительно простой алгоритм замены макросов, который можно тривиально реализовать перед любым невежественным парсером:
Вызовите его как:
Что потрясающе своей простотой и очень понятно.
источник
Я опубликовал исходный код сверхкомпактного (1 класс, <10 КиБ) Java Math Evaluator. на моем веб-сайте. Это рекурсивный анализатор спуска того типа, который вызвал черепной взрыв для плаката принятого ответа.
Он поддерживает полный приоритет, круглые скобки, именованные переменные и функции с одним аргументом.
источник
Я выпустил парсер выражений, основанный на алгоритме маневровой верфи Дейкстры, в соответствии с условиями Apache License 2.0 :
http://projects.congrace.de/exp4j/index.html
источник
Я реализовал парсер рекурсивного спуска на Java в проекте MathEclipse Parser . Его также можно использовать в качестве модуля Google Web Toolkit.
источник
В настоящее время я работаю над серией статей о создании синтаксического анализатора регулярных выражений как средства обучения шаблонам проектирования и удобочитаемому программированию. Вы можете взглянуть на читаемый код . В статье представлено наглядное использование алгоритма маневровых дворов.
источник
Я написал синтаксический анализатор выражений на F # и написал об этом здесь . Он использует алгоритм маневрового двора, но вместо преобразования инфикса в RPN я добавил второй стек для накопления результатов вычислений. Он правильно обрабатывает приоритет операторов, но не поддерживает унарные операторы. Я написал это для изучения F #, а не для изучения синтаксического анализа выражений.
источник
Решение Python с использованием pyparsing можно найти здесь . Синтаксический анализ инфиксной нотации с различными операторами с приоритетом является довольно распространенным явлением, поэтому в процесс pyparsing также входит
infixNotation
(ранееoperatorPrecedence
) построитель выражений. С его помощью вы можете легко определять логические выражения, используя, например, «И», «ИЛИ», «НЕ». Или вы можете расширить свою арифметику с четырьмя функциями, чтобы использовать другие операторы, такие как! для факториала или "%" для модуля, или добавьте операторы P и C для вычисления перестановок и комбинаций. Вы можете написать инфиксный синтаксический анализатор для матричной записи, который включает обработку операторов «-1» или «T» (для инверсии и транспонирования). Пример operatorPrecedence для 4-х функционального парсера (с добавленным '!' Для развлечения) здесь .источник
Я знаю, что это запоздалый ответ, но я только что написал крошечный парсер, который позволяет всем операторам (префиксным, постфиксным и инфиксным левым, инфиксным правым и неассоциативным) иметь произвольный приоритет.
Я собираюсь расширить это для языка с произвольной поддержкой 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 и т. Д.) Нарушают связь по приоритету, не должен подниматься, потому что на самом деле нет смысла давать операторам разных типов одинаковый приоритет. Этот алгоритм что-то делает в этих случаях, но я даже не удосужился выяснить, что именно, потому что такая грамматика изначально плохая идея.
источник
Вот простое рекурсивное решение, написанное на Java. Обратите внимание, что он не обрабатывает отрицательные числа, но вы можете добавить это, если хотите:
}
источник
Алгоритм можно легко закодировать на C как рекурсивный анализатор спуска.
могут быть полезны следующие библиотеки : yupana - строго арифметические операции; tinyexpr - арифметические операции + математические функции C + одна, предоставленная пользователем; mpc - комбинаторы парсеров
Объяснение
Давайте запишем последовательность символов, представляющих алгебраическое выражение. Первый - это число, то есть десятичная цифра, повторяемая один или несколько раз. Мы будем называть такое обозначение производственным правилом.
Другое правило - оператор сложения с его операндами. Это любой
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
Постепенно мы могли бы напоминать набор правил производства, также называемых грамматикой, которые выражают все возможные алгебраические выражения.
Чтобы управлять приоритетом оператора, измените положение его производственного правила относительно других. Посмотрите на грамматику выше и обратите внимание, что производственное правило для
*
размещено ниже,+
это заставитproduct
выполнить оценку раньшеsum
. Реализация просто объединяет распознавание образов с оценкой и, таким образом, точно отражает производственные правила.Здесь мы
term
сначала вычисляем и возвращаем его, если*
после него нет символа, это оставлено в нашем производственном правиле, в противном случае - оценивать символы после и возвращатьterm.value * product.value
это правильный выбор в нашем производственном правиле, т.е.term "*" product
источник