Я візьму на себе удар, коли його вкладають у мирянина.
Якщо ви думаєте з точки зору дерева розбору (не 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
, і якщо ви зробили те ж саме після того , як перший||
, вона більше не залишиться рекурсією? Але що якби ви це робили тільки після::=
, але ні||
, це все одно залишилося б рекурсивним?