Парсер приоритета операторов - Operator-precedence parser
В Информатика, парсер приоритета операторов это восходящий парсер что интерпретирует грамматика приоритета операторов. Например, большинство калькуляторы используйте парсеры приоритета операторов для преобразования из удобочитаемого инфиксная запись надеется порядок действий в формат, оптимизированный для оценки, например Обратная польская запись (РПН).
Эдсгер Дейкстра с алгоритм маневровой площадки обычно используется для реализации парсеров приоритета операторов.
Отношение к другим парсерам
Парсер приоритета операторов - это простой парсер сдвиг-уменьшение который способен анализировать подмножество LR (1) грамматики. Точнее, синтаксический анализатор приоритета операторов может анализировать все грамматики LR (1), в которых два последовательных нетерминалы и эпсилон никогда не появляются в правой части любого правила.
Парсеры приоритета операторов на практике используются нечасто; однако у них есть некоторые свойства, которые делают их полезными в более крупном дизайне. Во-первых, они достаточно просты для написания вручную, чего обычно не бывает с более сложными синтаксическими анализаторами сдвига вправо. Во-вторых, их можно написать для обращения к таблице операторов на время выполнения, что делает их подходящими для языков, которые могут добавлять или изменять свои операторы во время синтаксического анализа. (Пример: Haskell, который позволяет определять пользовательские инфиксные операторы с настраиваемой ассоциативностью и приоритетом; следовательно, в программе должен быть запущен синтаксический анализатор приоритета операторов. после анализ всех упомянутых модулей.)
Раку помещает парсер приоритета операторов между двумя парсеры рекурсивного спуска для достижения баланса скорости и динамизма. Это выражается в виртуальной машине для Раку, Попугай, как Парсер грамматики (PGE). GCC Синтаксические анализаторы C и C ++, которые представляют собой кодируемые вручную анализаторы рекурсивного спуска, ускоряются анализатором приоритета операторов, который может быстро проверять арифметические выражения. Парсеры приоритета операторов также встроены в компилятор компилятор -сгенерированные парсеры для заметного ускорения подхода рекурсивного спуска к синтаксическому анализу выражений.[1]
Метод восхождения по приоритету
Метод восхождения по приоритету - это компактный, эффективный и гибкий алгоритм синтаксического анализа выражений, который впервые был описан Мартином Ричардсом и Колином Уитби-Стревенсом.[2]
Грамматика выражения инфиксной записи в EBNF формат обычно выглядит так:
выражение ::= выражение-равенствовыражение-равенство ::= аддитивное выражение ( ( '==' | '!=' ) аддитивное выражение ) *аддитивное выражение ::= мультипликативное выражение ( ( '+' | '-' ) мультипликативное выражение ) *мультипликативное выражение ::= первичный ( ( '*' | '/' ) первичный ) *первичный ::= '(' выражение ')' | КОЛИЧЕСТВО | ПЕРЕМЕННАЯ | '-' первичный
При многих уровнях приоритета реализация этой грамматики с помощью синтаксического анализатора с прогнозирующим рекурсивным спуском может стать неэффективной. Например, для анализа числа может потребоваться пять вызовов функций: по одному для каждого нетерминального элемента в грамматике до достижения первичный.
Парсер приоритета операторов может сделать то же самое более эффективно.[1] Идея состоит в том, что мы можем оставить ассоциировать арифметические операции, пока мы находим операторы с таким же приоритетом, но мы должны сохранить временный результат для оценки операторов с более высоким приоритетом. Представленный здесь алгоритм не требует явного стека; вместо этого он использует рекурсивные вызовы для реализации стека.
Алгоритм не является чистым синтаксическим анализатором приоритета операторов, как алгоритм маневровой станции Дейкстры. Предполагается, что первичный нетерминал анализируется в отдельной подпрограмме, как в парсере рекурсивного спуска.
Псевдокод
Псевдокод для алгоритма следующий. Парсер начинается с функции parse_expression. Уровни приоритета больше или равны 0.
parse_expression () вернуть parse_expression_1 (parse_primary (), 0)
parse_expression_1 (lhs, min_precedence) смотреть вперед : = посмотреть следующий токен в то время как смотреть вперед бинарный оператор с приоритетом> = min_precedence op := смотреть вперед перейти к следующему токену rhs := parse_primary () смотреть вперед : = посмотреть следующий токен в то время как смотреть вперед бинарный оператор, приоритет которого больше, чем op's, или правоассоциативный оператор, приоритет которого равен op 's rhs := parse_expression_1 (rhs, приоритет просмотра вперед) смотреть вперед : = посмотреть следующий токен lhs : = результат применения op с операндами lhs и rhs вернуть lhs
Обратите внимание, что в случае такого производственного правила (где оператор может появляться только один раз):
выражение-равенство ::= аддитивное-выражение ('==' | '! =') аддитивное-выражение
алгоритм должен быть изменен, чтобы принимать только бинарные операторы, приоритет которых> min_precedence.
Пример выполнения алгоритма
Пример выполнения выражения 2 + 3 * 4 + 5 == 19 выглядит следующим образом. Мы даем приоритет 0 выражениям равенства, 1 - аддитивным выражениям, 2 - мультипликативным выражениям.
parse_expression_1 (lhs = 2, min_precedence = 0)
- лексема просмотра вперед - +, с приоритетом 1. вводится внешний цикл while.
- op равно + (приоритет 1) и ввод расширен
- rhs 3 года
- лексема просмотра вперед - *, с приоритетом 2. вход во внутренний цикл while.
parse_expression_1 (lhs = 3, min_precedence = 2)
- лексема просмотра вперед - *, с приоритетом 2. вводится внешний цикл while.
- op равно * (приоритет 2) и ввод расширен
- rhs это 4
- следующий токен - +, с приоритетом 1. внутренний цикл while не вводится.
- lhs присваивается 3 * 4 = 12
- следующий токен + с приоритетом 1. внешний цикл while остается.
- 12 возвращается.
- лексема просмотра вперед - +, с приоритетом 1. внутренний цикл while не вводится.
- lhs присваивается 2 + 12 = 14
- маркер просмотра вперед равен + с приоритетом 1. внешний цикл while не остается.
- op равно + (приоритет 1) и ввод расширен
- rhs 5 лет
- следующий токен == с приоритетом 0. внутренний цикл while не вводится.
- lhs присваивается 14 + 5 = 19
- следующий токен ==, с приоритетом 0. Внешний цикл while не остается.
- op == (приоритет 0) и ввод расширен
- rhs 19 лет
- следующий токен конец линии, который не является оператором. внутренний цикл while не вводится.
- lhs присваивается результат оценки 19 == 19, например 1 (как в стандарте C).
- следующий токен конец линии, который не является оператором. внешний цикл while остается.
1 возвращается.
Анализ Пратта
Другой синтаксический анализатор приоритета, известный как синтаксический анализ Пратта, был впервые описан Воном Праттом в статье 1973 г. «Приоритет операторов сверху вниз».[3], на основе рекурсивный спуск. Хотя это восхождение предшествует старшинству, его можно рассматривать как обобщение предшествующего лазания. [4]
Первоначально Пратт разработал парсер для реализации CGOL язык программирования, и он был рассмотрен более подробно в магистерской диссертации под его руководством.[5]
Учебники и реализации:
- Дуглас Крокфорд основал парсер JavaScript в JSLint по разбору Пратта.[6]
- Сравнение реализаций восхождения по приоритетам и синтаксического анализа Пратта в Python: «Pratt Parsing и Precedence Climbing - один и тот же алгоритм» (2016) Энди Чу
- Учебник по использованию Python: «Простой нисходящий синтаксический анализ в Python» (2008) Фредрика Лунда
- Учебник по использованию Ява: "Pratt Parsers: Expression Parsing Made Easy" (2011) Боба Нистрома, автора книги Crafting Interpreters
Альтернативные методы
Есть и другие способы применения правил приоритета операторов. Один из них - построить дерево исходного выражения, а затем применить к нему правила перезаписи дерева.
Такие деревья не обязательно должны быть реализованы с использованием структур данных, обычно используемых для деревьев. Вместо этого токены можно хранить в плоских структурах, таких как таблицы, путем одновременного создания списка приоритетов, в котором указывается, какие элементы в каком порядке обрабатывать.
Другой подход состоит в том, чтобы сначала полностью заключить выражение в круглые скобки, вставив ряд круглых скобок вокруг каждого оператора, чтобы они приводили к правильному приоритету даже при анализе с помощью линейного синтаксического анализатора слева направо. Этот алгоритм использовался в раннем FORTRAN I компилятор:[7]
Компилятор Fortran I расширил бы каждый оператор последовательностью круглых скобок. В упрощенной форме алгоритма это было бы
- заменять
+
и–
с))+((
и))-((
соответственно;- заменять
*
и/
с)*(
и)/(
соответственно;- Добавить
((
в начале каждого выражения и после каждой левой круглой скобки в исходном выражении; и- Добавить
))
в конце выражения и перед каждой правой круглой скобкой в исходном выражении.Хотя это и не очевидно, алгоритм был правильным, и, по словам Knuth, «Полученная формула правильно заключена в круглые скобки, хотите верьте, хотите нет».[8]
Пример кода простого приложения на C, которое обрабатывает скобки для основных математических операторов (+
, -
, *
, /
, ^
, (
и )
):
#включают <stdio.h>#включают <string.h>int основной(int argc, char *argv[]) { int я; printf("(((("); за (я=1;я!=argc;я++) { если (argv[я] && !argv[я][1]) { переключатель (*argv[я]) { кейс '(': printf("(((("); Продолжать; кейс ')': printf("))))"); Продолжать; кейс '^': printf(")^("); Продолжать; кейс '*': printf("))*(("); Продолжать; кейс '/': printf("))/(("); Продолжать; кейс '+': если (я == 1 || strchr("(^*/+-", *argv[я-1])) printf("+"); еще printf(")))+((("); Продолжать; кейс '-': если (я == 1 || strchr("(^*/+-", *argv[я-1])) printf("-"); еще printf(")))-((("); Продолжать; } } printf("% s", argv[я]); } printf(")))) п"); вернуть 0;}
Например, при компиляции и вызове из командной строки с параметрами
а * б + с ^ г / д
он производит
((((a)) * ((b))) + (((c) ^ (d)) / ((e))))
как вывод на консоль.
Ограничением этой стратегии является то, что унарные операторы должны иметь более высокий приоритет, чем инфиксные. «Отрицательный» оператор в приведенном выше коде имеет более высокий приоритет, чем возведение в степень. Запуск программы с этим входом
- а ^ 2
производит этот вывод
((((-a) ^ (2))))
что, вероятно, не то, что задумано.
использованная литература
- ^ а б Харвелл, Сэм (29 августа 2008 г.). "Парсер приоритета операторов". ANTLR3 Вики. Получено 2017-10-25.
- ^ Ричардс, Мартин; Уитби-Стревенс, Колин (1979). BCPL - язык и его компилятор. Издательство Кембриджского университета. ISBN 9780521219655.
- ^ Пратт, Воган. "Приоритет оператора сверху вниз." Материалы 1-го ежегодного симпозиума ACM SIGACT-SIGPLAN по принципам языков программирования (1973).
- ^ Норвелл, Теодор. «Анализ выражений методом рекурсивного спуска». www.engr.mun.ca.
Цель этого поста - [... начать] восхождение по приоритетам и его рефакторинг для использования шаблона команд, пока мы не дойдем до синтаксического анализатора Pratt. [Это автор, который ввел термин «восхождение по старшинству».]
- ^ Ван Де Вантер, Майкл Л. "Формализация и подтверждение правильности языковой системы CGOL. »(Магистерская диссертация). Технический отчет лаборатории компьютерных наук Массачусетского технологического института MIT-LCS-TR-147 (Кембридж, Массачусетс). 1975.
- ^ Крокфорд, Д. (21 февраля 2007 г.). «Приоритет оператора сверху вниз».
- ^ Падуя, Дэвид (2000). "Компилятор Fortran I" (PDF). Вычислительная техника в науке и технике. 2 (1): 70–75. Bibcode:2000CSE ..... 2a..70P. Дои:10.1109/5992.814661.
- ^ Кнут, Дональд Э. (1962). "ИСТОРИЯ ПИСАТЕЛЕЙ КОМПИЛЯТОРОВ". Компьютеры и автоматика. Эдмунд С. Беркли. 11 (12): 8–14.
внешняя ссылка
- Кларк, Кит (1992-05-26). "Re: компактный анализ выражений с рекурсивным спуском". Получено 2012-01-24.
- Пример кода C ++ Кейта Кларка для анализа инфиксных выражений с использованием метода восхождения по приоритету
- Самельсон, Клаус; Фридрих Л. Бауэр (Февраль 1960 г.). «Последовательный перевод формул». Коммуникации ACM. 3 (2): 76–83. Дои:10.1145/366959.366968.
- Парсер для выражения с инфиксной нотацией с использованием списков приоритета