Я попытаюсь выразить это в терминах непрофессионала.
Если вы думаете с точки зрения дерева разбора (не AST, а посещения синтаксического анализатора и расширения входных данных), левая рекурсия приводит к дереву, которое растет влево и вниз. Правильная рекурсия с точностью до наоборот.
Например, общая грамматика в компиляторе - это список элементов. Давайте возьмем список строк («красный», «зеленый», «синий») и проанализируем его. Я мог бы написать грамматику несколькими способами. Следующие примеры являются рекурсивными непосредственно влево или вправо, соответственно:
arg_list: arg_list:
STRING STRING
| arg_list ',' STRING | STRING ',' arg_list
Деревья для этих разбора:
(arg_list) (arg_list)
/ \ / \
(arg_list) BLUE RED (arg_list)
/ \ / \
(arg_list) GREEN GREEN (arg_list)
/ /
RED BLUE
Обратите внимание, как оно растет в направлении рекурсии.
Это на самом деле не проблема, это нормально, если вы хотите написать левую рекурсивную грамматику ... если ваш инструмент синтаксического анализа может с этим справиться. Анализаторы снизу вверх справляются с этим просто отлично. Так могут более современные парсеры LL. Проблема с рекурсивными грамматиками заключается не в рекурсии, а в рекурсии без улучшения синтаксического анализатора или в рекурсии без использования токена. Если мы всегда потребляем хотя бы 1 токен, когда выполняем рекурсию, мы в конце концов достигаем конца разбора. Левая рекурсия определяется как рекурсивная без потребления, что представляет собой бесконечный цикл.
Это ограничение является чисто реализацией реализации грамматики с наивным анализатором LL сверху вниз (анализатор рекурсивного спуска). Если вы хотите придерживаться левой рекурсивной грамматики, вы можете справиться с ней, переписав производство так, чтобы оно потребляло как минимум 1 токен перед повторением, так что это гарантирует, что мы никогда не застрянем в непроизводительном цикле. Для любого правила грамматики, которое является леворекурсивным, мы можем переписать его, добавив промежуточное правило, которое выравнивает грамматику только до одного уровня прогнозирования, потребляя токен между рекурсивными производствами. (ПРИМЕЧАНИЕ. Я не говорю, что это единственный или предпочтительный способ переписать грамматику, я просто указываю на обобщенное правило. В этом простом примере лучшим вариантом является использование праворекурсивной формы). Поскольку этот подход является обобщенным, Генератор парсера может реализовать это без участия программиста (теоретически). На практике, я считаю, что ANTLR 4 теперь делает именно это.
Для приведенной выше грамматики реализация LL, отображающая левую рекурсию, будет выглядеть следующим образом. Парсер начнёт с предсказания списка ...
bool match_list()
{
if(lookahead-predicts-something-besides-comma) {
match_STRING();
} else if(lookahead-is-comma) {
match_list(); // left-recursion, infinite loop/stack overflow
match(',');
match_STRING();
} else {
throw new ParseException();
}
}
На самом деле мы имеем дело с «наивным исполнением», т.е. мы изначально сделали предикат для данного предложения, затем рекурсивно вызвали функцию для этого прогноза, и эта функция наивно вызывает тот же прогноз снова.
Анализаторы снизу вверх не имеют проблемы рекурсивных правил в обоих направлениях, потому что они не разбирают начало предложения, они работают, собирая предложение вместе.
Рекурсия в грамматике является проблемой, только если мы производим сверху вниз, т.е. наш парсер работает, "расширяя" наши прогнозы, когда мы потребляем токены. Если вместо расширения мы свернемся (производство «сокращено»), как в анализаторе снизу вверх LALR (Yacc / Bison), то рекурсия любой из сторон не является проблемой.
::=
сExpression
наTerm
, и если бы вы сделали то же самое после первой||
, это больше не было бы леворекурсивным? Но что, если бы вы только сделали это после::=
, но не так||
, это все равно было бы леворекурсивным?