В последние несколько дней я провел много исследований, чтобы лучше понять, почему существуют эти отдельные технологии, каковы их сильные и слабые стороны.
Некоторые из уже существующих ответов намекали на некоторые из их различий, но они не давали полной картины, и, казалось, были несколько самоуверенными, поэтому этот ответ был написан.
Эта экспозиция длинная, но важная. потерпите меня (или, если вы нетерпеливы, прокрутите до конца, чтобы увидеть блок-схему).
Чтобы понять различия между Parser Combinators и Parser Generator, сначала нужно понять разницу между различными видами синтаксического анализа, которые существуют.
анализ
Синтаксический анализ - это процесс анализа последовательности символов в соответствии с формальной грамматикой. (В вычислительной науке) синтаксический анализ используется, чтобы позволить компьютеру понимать текст, написанный на языке, обычно создавая дерево разбора , представляющее письменный текст, сохраняя значение различных письменных частей в каждом узле дерева. Это дерево синтаксического анализа может затем использоваться для различных целей, таких как перевод его на другой язык (используемый во многих компиляторах), интерпретация письменных инструкций напрямую (SQL, HTML), позволяющая таким инструментам, как Linters,
выполнять свою работу и т. д. Иногда дерево разбора не является явнымсгенерировано, но скорее действие, которое должно быть выполнено на каждом типе узла в дереве, выполняется непосредственно. Это повышает эффективность, но под водой все еще существует неявное дерево разбора.
Синтаксический анализ - это сложная вычислительная проблема. Было проведено более пятидесяти лет исследований по этому вопросу, но многое еще предстоит изучить.
Грубо говоря, существует четыре основных алгоритма, позволяющих компьютеру анализировать ввод:
- Разбор LL. (Не зависящий от контекста анализ сверху вниз.)
- Разбор LR. (Контекстный, восходящий анализ.)
- PEG + Packrat парсинг.
- Эрли Парсинг.
Обратите внимание, что эти типы синтаксического анализа являются очень общими, теоретическими описаниями. Существует несколько способов реализации каждого из этих алгоритмов на физических машинах с различными компромиссами.
LL и LR могут смотреть только на контекстно-свободные грамматики (то есть контекст вокруг записанных токенов не важен для понимания того, как они используются).
Разбор PEG / Packrat и разбор Earley используется гораздо реже: разбор Earley хорош тем, что он может обрабатывать намного больше грамматик (включая те, которые не обязательно не зависят от контекста), но он менее эффективен (как утверждает дракон) книга (раздел 4.1.1); я не уверен, что эти утверждения все еще точны).
Разбор грамматики выражения + синтаксический анализ пакетов - это метод, который относительно эффективен и может обрабатывать больше грамматик, чем как LL, так и LR, но скрывает неоднозначности, как будет быстро упомянуто ниже.
LL (слева направо, крайний левый вывод)
Это, возможно, самый естественный способ думать о разборе. Идея состоит в том, чтобы посмотреть на следующий токен во входной строке, а затем решить, какой из нескольких возможных рекурсивных вызовов следует использовать для создания древовидной структуры.
Это дерево строится «сверху вниз», что означает, что мы начинаем с корня дерева и проходим правила грамматики так же, как мы проходим через входную строку. Это также можно рассматривать как построение эквивалента 'postfix' для потока токенов 'infix', который читается.
Парсеры, выполняющие синтаксический анализ в стиле LL, могут быть написаны так, чтобы они выглядели очень похоже на указанную оригинальную грамматику. Это позволяет относительно легко понять, отладить и улучшить их. Классические парсер-комбинаторы - это не что иное, как «кусочки лего», которые можно собрать вместе для создания парсера в стиле LL.
LR (слева направо, крайний правый вывод)
Синтаксический анализ LR выполняется другим способом, снизу вверх: на каждом шаге верхний элемент (и) в стеке сравнивается со списком грамматики, чтобы увидеть, можно ли их уменьшить
до правила более высокого уровня в грамматике. Если нет, то следующий токен из входного потока сдвигается и помещается поверх стека.
Программа верна, если в конце мы получим один узел в стеке, который представляет начальное правило из нашей грамматики.
Смотреть вперед
В любой из этих двух систем иногда необходимо взглянуть на большее количество токенов на входе, прежде чем можно будет решить, какой выбор сделать. Это (0)
, (1)
, (k)
или (*)
-syntax вы видите после названия этих двух общих алгоритмов, таких как LR(1)
или LL(k)
. k
обычно означает «столько, сколько нужно вашей грамматике», в то время как *
обычно означает «этот синтаксический анализатор выполняет обратное отслеживание», который является более мощным / простым в реализации, но имеет гораздо более высокое использование памяти и времени, чем анализатор, который может просто продолжать анализ линейно.
Обратите внимание, что парсеры в стиле LR уже имеют много токенов в стеке, когда они могут решить «смотреть в будущее», поэтому у них уже есть больше информации для отправки. Это означает, что им часто требуется меньше «оглядки», чем синтаксический анализатор в стиле LL для той же грамматики.
LL vs. LR: двусмысленность
При чтении двух описаний выше, можно задаться вопросом, почему существует разбор в стиле LR, поскольку разбор в стиле LL кажется намного более естественным.
Однако у парсинга в стиле LL есть проблема: Left Recursion .
Очень естественно написать грамматику вроде:
expr ::= expr '+' expr | term
term ::= integer | float
Но синтаксический анализатор в стиле LL застревает в бесконечном рекурсивном цикле при синтаксическом анализе этой грамматики: при попытке expr
проверить крайнюю левую возможность правила он снова возвращается к этому правилу, не потребляя никакого ввода.
Есть способы решить эту проблему. Самое простое - переписать вашу грамматику, чтобы такой рекурсии больше не было:
expr ::= term expr_rest
expr_rest ::= '+' expr | ϵ
term ::= integer | float
(Здесь ϵ обозначает «пустую строку»)
Эта грамматика сейчас правильная рекурсивная. Обратите внимание, что это сразу намного сложнее для чтения.
На практике левая рекурсия может происходить косвенно со многими другими промежуточными этапами. Это усложняет проблему. Но попытка решить ее затрудняет чтение вашей грамматики.
В Разделе 2.5 Книги Дракона говорится:
У нас, кажется, есть конфликт: с одной стороны, нам нужна грамматика, которая облегчает перевод, с другой стороны, нам нужна существенно другая грамматика, которая облегчает синтаксический анализ. Решение состоит в том, чтобы начать с грамматики для легкого перевода и аккуратно преобразовать ее для облегчения анализа. Исключив левую рекурсию, мы можем получить грамматику, подходящую для использования в интеллектуальном переводчике с рекурсивным спуском.
У парсеров в стиле LR нет проблемы этой левой рекурсии, поскольку они строят дерево снизу вверх.
Тем не менее , мысленный перевод грамматики, подобной выше, в синтаксический анализатор в стиле LR (который часто реализуется как конечный автомат )
очень труден (и подвержен ошибкам), так как часто бывают сотни или тысячи состояний + переходы состояний для рассмотрения. Вот почему парсеры в стиле LR обычно генерируются генератором парсеров, который также известен как «компилятор компилятора».
Как разрешить неясности
Мы видели два метода для разрешения двусмысленности левой рекурсии выше: 1) переписать синтаксис 2) использовать LR-парсер.
Но есть и другие неясности, которые сложнее решить: что, если два разных правила одинаково применимы одновременно?
Некоторые общие примеры:
Парсеры как в LL-стиле, так и в LR-стиле имеют проблемы с этим. Проблемы с разбором арифметических выражений могут быть решены путем введения приоритета операторов. Аналогичным образом можно решить и другие проблемы, такие как Dangling Else, выбрав одно приоритетное поведение и придерживаясь его. (В C / C ++, например, висячее остальное всегда принадлежит ближайшему «если»).
Другим «решением» этого является использование грамматики выражений анализатора (PEG): это похоже на грамматику BNF, использованную выше, но в случае неоднозначности всегда «выбирайте первое». Конечно, это на самом деле не «решает» проблему, а скорее скрывает, что двусмысленность действительно существует: конечные пользователи могут не знать, какой выбор делает парсер, и это может привести к неожиданным результатам.
Больше информации, которая намного глубже, чем этот пост, в том числе, почему вообще невозможно узнать, не содержит ли ваша грамматика двусмысленности, и последствия этого - замечательная статья блога LL и LR в контексте: зачем разбирать инструменты сложны . Я очень рекомендую это; это очень помогло мне понять все, о чем я сейчас говорю.
50 лет исследований
Но жизнь продолжается. Оказалось, что «нормальным» синтаксическим анализаторам в стиле LR, реализованным в виде автоматов с конечным состоянием, часто нужны тысячи состояний + переходы, что было проблемой в размере программы. Таким образом, были написаны такие варианты, как Simple LR (SLR) и LALR (Look-forward LR), которые сочетают в себе другие методы, чтобы сделать автомат меньше, уменьшая объем дискового пространства и памяти в программах синтаксического анализа.
Кроме того, еще один способ устранения неопределенностей, перечисленных выше, заключается в использовании обобщенных методов, в которых в случае неоднозначности обе возможности сохраняются и анализируются: любой из них может быть не в состоянии выполнить разбор по линии (в этом случае другой возможностью является «правильный»), а также возвращая оба (и, таким образом, показывая, что существует неоднозначность) в случае, если они оба верны.
Интересно, что после описания алгоритма обобщенного LR оказалось, что аналогичный подход можно использовать для реализации синтаксических анализаторов обобщенного LL , который аналогично быстро ($ O (n ^ 3) $ временной сложности для неоднозначных грамматик, $ O (n) $ для полностью однозначных грамматик, хотя и с большим количеством бухгалтерии, чем простой (LA) парсер LR, что означает более высокий постоянный коэффициент), но опять же позволяет парсеру записываться в стиле рекурсивного спуска (сверху вниз), который намного более естественен написать и отладить.
Парсер Комбинаторы, Парсер Генераторы
Итак, с этой длинной экспозицией мы сейчас подходим к сути вопроса:
В чем отличие Parser Combinators и Parser Generator, и когда один из них должен использоваться поверх другого?
Это действительно разные виды зверей:
Парсер-комбинаторы были созданы потому, что люди писали нисходящие парсеры и понимали, что у многих из них было много общего .
Генераторы парсеров были созданы потому, что люди искали создание парсеров, у которых не было проблем, с которыми были парсеры в стиле LL (то есть парсеры в стиле LR), что оказалось очень трудно сделать вручную. К наиболее распространенным относятся Yacc / Bison, которые реализуют (LA) LR).
Интересно, что в наши дни ландшафт несколько запутан:
Можно написать Parser Combinators, которые работают с алгоритмом GLL , разрешая проблемы неоднозначности, которые были у классических синтаксических анализаторов в стиле LL, и в то же время столь же читабельны / понятны, как и все виды синтаксического анализа сверху вниз.
Генераторы парсеров также могут быть написаны для парсеров в стиле LL. ANTLR делает именно это и использует другую эвристику (Adaptive LL (*)) для устранения неоднозначностей, которые были у классических синтаксических анализаторов в стиле LL.
В общем, создание генератора синтаксического анализатора LR и отладка вывода генератора синтаксического анализатора в стиле (LA), работающего на вашей грамматике, затруднены из-за перевода исходной грамматики в LR-форму «наизнанку». С другой стороны, такие инструменты , как Yacc / Bison было много лет оптимизаций, и видел много использования в дикой природе, а это значит , что сейчас многие люди считают это как на способ сделать разбор и скептически относятся к новым подходам.
Какой из них использовать, зависит от того, насколько сложна ваша грамматика и насколько быстрым должен быть анализатор. В зависимости от грамматики один из этих методов (/ реализации различных методов) может быть быстрее, иметь меньший объем памяти, меньший объем диска или быть более расширяемым или более простым для отладки, чем другие. Ваш пробег может меняться .
Примечание: по теме лексического анализа.
Лексический анализ может использоваться как для Parser Combinators, так и для Parser Generator. Идея состоит в том, чтобы иметь «тупой» синтаксический анализатор, который очень легко реализовать (и, следовательно, быстро), который выполняет первый проход по вашему исходному коду, удаляя, например, повторяющиеся пробелы, комментарии и т. Д. И, возможно, «разбивая» на очень грубо говоря, различные элементы, которые составляют ваш язык.
Основным преимуществом является то, что этот первый шаг значительно упрощает реальный синтаксический анализатор (и из-за этого, возможно, быстрее). Основным недостатком является то, что у вас есть отдельный шаг перевода, и, например, создание отчетов об ошибках с номерами строк и столбцов усложняется из-за удаления пробелов.
В конце концов, лексер является «просто» другим парсером и может быть реализован с использованием любого из методов, описанных выше. Из-за своей простоты часто используются другие методы, кроме основного синтаксического анализатора, и, например, существуют дополнительные «генераторы лексера».
Tl; Dr:
Вот блок-схема, которая применима к большинству случаев:
javac
, Scala). Это дает вам максимальный контроль над состоянием внутреннего синтаксического анализатора, который помогает генерировать хорошие сообщения об ошибках (которые в последние годы ...