Какие грамматики можно анализировать с помощью рекурсивного спуска без возврата?

Согласно парсеру рекурсивного спуска в Википедии, рекурсивный спуск без возврата (также известный как предиктивный синтаксический анализ) возможно для грамматик LL (k).

В другом месте я читал, что реализация Lua использует такой парсер. Однако язык - не LL (k). Фактически, Lua по своей сути неоднозначен: a = f(g)(h)[i] = 1 означает a = f(g); (h)[i] = 1 или a = f; (g)(h)[i] = 1? Эта неоднозначность устраняется жадностью парсера (поэтому приведенное выше анализируется как ошибочный a = f(g)(h)[i]; = 1).

Этот пример, кажется, показывает, что интеллектуальные синтаксические анализаторы могут обрабатывать грамматики, которые не являются LL (k). Верно ли, что они действительно могут обрабатывать надмножество LL (k)? Если да, то есть ли способ узнать, входит ли данная грамматика в этот расширенный набор?

Другими словами, если я разрабатываю язык, который я хотел бы анализировать с помощью прогнозирующего синтаксического анализатора, нужно ли мне ограничивать язык до LL (k)? Или я могу применить более слабое ограничение?


person user200783    schedule 21.08.2017    source источник
comment
Я думаю, что Lua - это LL (k), иначе вы не будете разбирать x = y, потому что вам нужно посмотреть, есть ли = после x.   -  person Klaider    schedule 24.08.2017
comment
В настоящее время Википедия определяет, что ... парсер с рекурсивным спуском - это вид синтаксического анализатора сверху вниз, построенный из набора взаимно рекурсивных процедур (или нерекурсивного эквивалента), где каждая такая процедура реализует один из нетерминалов грамматики. .. Из этого определения нельзя сделать вывод, что рекурсивный спуск без возврата ... возможен только для грамматик LL (k). Стандартный пример - r.d. парсер для операторов if и if-else в стиле C. R.d. парсер существует и его легко написать; однако грамматика не является LL (k).   -  person Theodore Norvell    schedule 29.11.2018


Ответы (1)


TL;DR

Для подходящего определения того, что это за парсер рекурсивного спуска, абсолютно правильно, что только LL (k) языки могут быть проанализированы рекурсивным спуском.

Lua можно проанализировать с помощью синтаксического анализатора рекурсивного спуска именно потому, что язык - это LL (k); то есть для Lua существует грамматика LL (k). [Примечание 1]

1. Язык LL (k) может иметь грамматики, отличные от LL (k).

Язык называется LL (k), если существует грамматика LL (k), которая распознает язык. Это не означает, что каждая грамматика, распознающая язык, является LL (k); может быть любое количество грамматик не-LL (k), распознающих язык. Таким образом, тот факт, что некоторая грамматика не является LL (k), абсолютно ничего не говорит о самом языке.

2. Многие практические языки программирования описываются неоднозначной грамматикой.

В формальной теории языка язык неоднозначен по своей сути, только если Каждая грамматика языка неоднозначна. Вероятно, можно с уверенностью сказать, что ни один практический язык программирования не является неоднозначным по своей сути, поскольку практические языки программирования детерминированно анализируются (каким-то образом). [Заметка 2].

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

Например, многие языки (включая Lua) задокументированы с помощью грамматики, которая явно не включает приоритет операторов, что позволяет использовать простое правило для выражений:

exp ::= exp Binop exp | Unop exp | term

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

Другой распространенный пример неоднозначных справочных грамматик, которые можно устранить механически, - это двусмысленность "dangling else", обнаруженная в такие языки, как C (но не в Lua). Грамматика:

if-statement ::= "if" '(' exp ')' stmt
               | "if" '(' exp ')' stmt "else" stmt

конечно неоднозначно; намерение состоит в том, чтобы синтаксический анализ был «жадным». Опять же, двусмысленность не присуща. Существует механическое преобразование, которое дает однозначную грамматику, примерно такую:

matched-statement ::= matched-if-stmt | other-statement
statement         ::= matched-if-stmt | unmatched-if-stmt
matched-if-stmt   ::= "if" '(' exp ')' matched-statement "else" matched-statement 
unmatched-if-stmt ::= "if" '(' exp ')' statement
                    | "if" '(' exp ')' matched-statement "else" unmatched-if-stmt

Генераторы парсеров часто неявно выполняют это преобразование. (Для генератора синтаксического анализатора LR преобразование фактически реализуется путем удаления действий сокращения, если они конфликтуют с действием сдвига. Это проще, чем преобразование грамматики, но имеет точно такой же эффект.)

Итак, Lua (и другие языки программирования) не по своей сути неоднозначны; и поэтому они могут быть проанализированы с помощью алгоритмов синтаксического анализа, которые требуют однозначных детерминированных синтаксических анализаторов. В самом деле, может быть даже немного удивительно, что существуют языки, для которых все возможные грамматики неоднозначны. Как указано в цитированной выше статье Википедии, существование таких языков было доказано Рохитом Парихом в 1961 году; простой пример неоднозначного по своей сути контекстно-свободного языка:

{anbmcmdn|n,m≥0} ∪ {anbncmdm|n,m≥0}.

3. Жадный LL (1) парсинг Lua-операторов присваивания и вызова функций.

Как и в случае с конструкцией dangling else, описанной выше, устранение неоднозначности последовательностей операторов Lua выполняется только путем разрешения жадного синтаксического анализа. Интуитивно процедура проста; он основан на запрете двух последовательных операторов (без точки с запятой), где второй начинается с токена, который может продолжить первый.

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

Следующее подмножество (в значительной степени основанное на справочной грамматике) демонстрирует в точности двусмысленность, указанную в OP:

program        ::= statement-list
statement-list ::= Ø
                 | statement-list statement
statement      ::= assignment | function-call | block | ';'
block          ::= "do" statement-list "end"
assignment     ::= var '=' exp
exp            ::= prefixexp                          [Note 3]
prefixexp      ::= var | '(' exp ')' | function-call
var            ::= Name | prefixexp '[' exp ']'
function-call  ::= prefixexp '(' exp ')'

(Примечание: (Я использую Ø для представления пустой строки, а не ε, λ или %empty.)

Грамматика Lua as является леворекурсивной, поэтому она явно не является LL (k) (независимо от неоднозначности). Удаление левой рекурсии может быть выполнено механически; Я сделал достаточно этого здесь, чтобы продемонстрировать, что это подмножество LL (1). К сожалению, преобразованная грамматика не сохраняет структуру дерева синтаксического анализа, что является классической проблемой грамматик LL (k). Обычно восстановить правильное дерево синтаксического анализа во время синтаксического анализа с рекурсивным спуском несложно, и я не буду вдаваться в подробности.

Предоставить LL (1) версию exp просто, но результат устраняет различие между var (который может быть назначен) и function-call (который не может):

exp          ::= term exp-postfix
exp-postfix  ::= Ø
               | '[' exp ']' exp-postfix
               | '(' exp ')' exp-postfix 
term         ::= Name | '(' exp ')' 

Но теперь нам нужно воссоздать различие, чтобы иметь возможность анализировать как операторы присваивания, так и вызовы функций. Это прямолинейно (но не способствует пониманию синтаксиса, ИМХО):

a-or-fc-statement ::= term a-postfix
a-postfix         ::= '=' exp 
                    | ac-postfix
c-postfix         ::= Ø
                    | ac-postfix
ac-postfix        ::= '(' exp ')' c-postfix
                    | '[' exp ']' a-postfix

Чтобы сделать жадный синтаксический анализ однозначным, нам нужно запретить (из грамматики) любое вхождение S1 S2, где S1 заканчивается на exp, а S2 начинается с '('. Фактически, нам нужно различать разные типы операторов, в зависимости от от того, начинается ли оператор с ( и независимо от того, заканчивается ли оператор на exp. (На практике существует только три типа, потому что нет операторов, которые начинаются с ( и не заканчиваются exp. . [Примечание 4])

statement-list ::= Ø
                 | s1 statement-list
                 | s2 s2-postfix
                 | s3 s2-postfix
s2-postfix     ::= Ø
                 | s1 statement-list
                 | s2 s2-postfix
s1             ::= block | ';'
s2             ::= Name a-postfix
s3             ::= '(' exp ')' a-postfix

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

В наиболее распространенном использовании синтаксический анализатор рекурсивного спуска с предсказанием представляет собой реализацию алгоритма LL (k), в котором каждый нетерминал отображается на процедуру. Каждая нетерминальная процедура начинается с использования таблицы возможных опережающих последовательностей длиной k, чтобы решить, какую альтернативную продукцию для этого нетерминального устройства использовать, а затем просто «выполняет» производственный символ за символом: терминальные символы заставляют следующий входной символ использовать быть отвергнутым, если он совпадает, или сообщать об ошибке, если он не совпадает; нетерминальные символы вызывают вызов нетерминальной процедуры.

Таблицы упреждающих последовательностей могут быть построены с использованием наборов FIRST k и FOLLOW k. (Производство A→ω отображается на последовательность α терминалов, если α ∈ FIRSTkFOLLOWk(A)).) [Примечание 5]

С этим определением синтаксического анализа рекурсивного спуска рекурсивный анализатор спуска может обрабатывать точно и только языки LL (k). [Примечание 6]

Однако согласование парсеров LL (k) и рекурсивного спуска игнорирует важный аспект парсера рекурсивного спуска, а именно то, что это, прежде всего, программа, обычно написанная в некотором полном по Тьюрингу программировании. язык. Если этой программе позволено немного отклониться от жесткой структуры, описанной выше, она сможет анализировать гораздо больший набор языков, даже языки, которые не являются контекстно-независимыми. (См., Например, контекстную чувствительность языка C, указанную в примечании 2.)

В частности, очень легко добавить правило «по умолчанию» к таблице, отображающей опережающие просмотры в продуктах. Это очень заманчивая оптимизация, поскольку она значительно уменьшает размер таблицы просмотра вперед. Обычно правило по умолчанию используется для нетерминалов, альтернативы которых включают пустую правую часть, которая в случае грамматики LL (1) будет отображаться на любой символ в FOLLOW, установленном для нетерминальный. В этой реализации таблица просмотра вперед включает только просмотры из набора ПЕРВЫЙ, а синтаксический анализатор автоматически создает пустую правую часть, соответствующую немедленному возврату для любого другого символа. (Как и аналогичная оптимизация в парсерах LR (k), эта оптимизация может задерживать распознавание ошибок, но они все равно распознаются до того, как будет прочитан дополнительный токен.)

Анализатор LL (1) не может включать в себя нетерминал, допускающий значение NULL, чьи наборы FIRST и FOLLOW содержат общий элемент. Однако, если синтаксический анализатор рекурсивного спуска использует оптимизацию по «правилу по умолчанию», этот конфликт никогда не будет замечен во время построения синтаксического анализатора. Фактически, игнорирование конфликта позволяет создать «жадный» синтаксический анализатор на основе (определенных) недетерминированных грамматик.

Это чрезвычайно удобно, потому что, как мы видели выше, создание однозначной жадной грамматики - это большая работа и не приводит ни к чему, даже отдаленно напоминающему четкое изложение языка. Но модифицированный алгоритм рекурсивного синтаксического анализа не более мощный; он просто анализирует эквивалентную грамматику SLL (k) (без фактического построения этой грамматики).

Я не собираюсь предоставлять полное доказательство вышеупомянутого утверждения, но первым делом нужно заметить, что любой нетерминал можно переписать как дизъюнкцию новых нетерминалов, каждый с одним отдельным ПЕРВЫМ токен и, возможно, новый нетерминал с пустой правой стороной. Затем необходимо "всего лишь" удалить нетерминалы из набора FOLLOW нетерминалов, допускающих значение NULL, путем создания новых дизъюнкций.


Примечания

  1. Здесь я говорю о грамматике, которая работает с токенизированным потоком, в котором были удалены комментарии, а другие конструкции (например, строки, разделенные «длинными скобками») сокращены до одного токена. Без этого преобразования язык не был бы LL (k) (поскольку комментарии - которые могут быть сколь угодно длинными - мешают видимости лексемы просмотра вперед). Это позволяет мне также обойти вопрос о том, как длинные скобки можно распознать с помощью грамматики LL (k), который не имеет особого отношения к этому вопросу.

  2. Существуют языки программирования, которые невозможно детерминированно проанализировать с помощью контекстно-свободной грамматики. Самым известным примером, вероятно, является Perl, но есть также хорошо известная конструкция C (x)*y, которую можно анализировать только детерминированно, используя информацию о символе x - будь то имя переменной или псевдоним типа - и трудности правильного анализ выражений C ++ с использованием шаблонов. (См., Например, вопросы Почему нельзя проанализировать C ++ с помощью парсера LR (1)? и Является ли C ++ контекстно-зависимым или контекстно-зависимым?)

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

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

  5. Существуют детерминированные грамматики LL (k), которые не могут создать однозначные таблицы синтаксического анализа с использованием этого алгоритма, который Сиппу и Сойсалон-Сойнинен называют алгоритмом Strong LL (k). Можно расширить алгоритм, используя дополнительное состояние синтаксического анализа, подобное состоянию в анализаторе LR (k). Это может быть удобно для определенных грамматик, но не меняет определения языков LL (k). Как демонстрируют Sippu & Soisalon-Soininen, из любой грамматики LL (k) можно механически вывести грамматику SLL (k), которая дает точно такой же язык. (См. Теорему 8.47 в томе 2).

  6. Алгоритм рекурсивного определения - это точная реализация синтаксического анализатора LL (k) на основе канонического стека, где стек синтаксического анализатора неявно создается во время выполнения синтаксического анализатора с использованием комбинации текущего продолжения и стека записей активации.

person rici    schedule 25.08.2017
comment
Контекстно-свободные грамматики C ++ по своей сути неоднозначны. См. stackoverflow.com/questions/243383/ - person Ira Baxter; 25.08.2017
comment
@IraBaxter: Грамматика, не зависящая от контекста, неоднозначна, но не является неоднозначной по своей сути в смысле фразы, которую я использую. Скорее, они отражают тот факт, что однозначный синтаксический анализ обязательно зависит от контекста. Можно спорить о терминологии, но я не думаю, что она будет полезна в данном контексте; пригласите меня в чат, если хотите отстоять свою точку зрения. Я добавил примечание о двусмысленности CFG C / C ++ (и других контекстно-зависимых языков); Надеюсь, вы сочтете это адекватным. - person rici; 25.08.2017
comment
Я думаю, что сейчас мы достаточно согласованы: -} - person Ira Baxter; 25.08.2017
comment
Ах! Мой комментарий был правильным ... он сказал, что Луа не LL (k) - person Klaider; 25.08.2017
comment
Это фантастика, спасибо вам большое! Действительно интересно увидеть (подмножество) Lua, преобразованное в грамматику LL (1), которая правильно обрабатывает требование жадности - я не думал, что такая грамматика существует. Учитывая эту преобразованную грамматику, я смог использовать простой инструмент для автоматической генерации парсера LL (1). Я не думаю, что это было бы возможно (по крайней мере, с помощью этого инструмента) без выполнения преобразования. - person user200783; 29.08.2017
comment
Однако еще более интересно увидеть, как можно избежать необходимости выполнять это преобразование. Имеет смысл, что рекурсивный анализатор спуска может игнорировать наборы FOLLOW и просто немедленно возвращать для просмотра вперед, которых нет в наборе FIRST. (Я предполагаю, что нисходящий синтаксический анализатор на основе таблиц мог бы вести себя таким же образом, если бы таблица была построена соответствующим образом.) Однако я ранее не осознавал, что это поведение является причиной жадного синтаксического анализа, не говоря уже о том, чтобы считать его эквивалентом преобразование грамматики. - person user200783; 29.08.2017
comment
Если я правильно понимаю, то: рекурсивный спуск, даже с пустым правилом RHS по умолчанию, может анализировать только языки LL (k). Для точных реализаций алгоритма LL (k) требуется грамматика языка LL (k). Однако использование правила по умолчанию позволяет избежать явного создания такой грамматики (хотя она должна существовать). Это правило по умолчанию допускает жадный анализ некоторых (но не всех) грамматик, для которых простой инструмент LL (1) сообщает о конфликте наборов FIRST / FOLLOW. Очевидно, это не поможет в случае столкновения ПЕРВОГО сета. - person user200783; 29.08.2017
comment
Похоже, что сам Lua использует подход правил по умолчанию, чтобы избежать необходимости третьего этапа преобразования вашей грамматики. Он выполняет ваш первый этап, как описано, используя имя primaryexp. для вашего term. Интересно, что Lua вообще не выполняет второй шаг преобразования (воссоздает различие между var и function-call), вместо этого с помощью семантических действий, чтобы запретить присвоение function-call. - person user200783; 29.08.2017
comment
@ user200783: Как я уже упоминал, конкретная программа синтаксического анализа является программой, и она может не соответствовать на 100% модели рекурсивного спуска, хотя большая часть синтаксического анализатора является рекурсивным спуском. В частности, очень часто принимают надмножество языка, а затем используют семантические действия для более точного анализа. Это не обязательно означает, что грамматика не является LL (k); также может быть, что грамматика LL (k) неудобна для построения или использования (что было бы в случае операторов присваивания) .... - person rici; 29.08.2017
comment
Другой пример семантических действий - это фрагмент кода, с которым вы связались ранее, где при вызовах функций требуется, чтобы цель и открывающая скобка находились в одной строке. Это не может быть выполнено с помощью грамматики, потому что символы новой строки не передаются синтаксическому анализатору, но семантическое действие может (и делает) убедиться, что номер строки открытой круглой скобки такой же, как и у предыдущего токена. В конце концов, синтаксические анализаторы с чисто рекурсивным спуском довольно редки на практике. Почти всегда есть что-то, что не совсем соответствует модели. - person rici; 29.08.2017