Ich werde versuchen, es in Laienbegriffe zu fassen.
Wenn Sie in Bezug auf den Analysebaum denken (nicht den AST, sondern den Besuch und die Erweiterung der Eingabe durch den Parser), führt die linke Rekursion zu einem Baum, der nach links und unten wächst. Die richtige Rekursion ist genau das Gegenteil.
Eine gängige Grammatik in einem Compiler ist beispielsweise eine Liste von Elementen. Nehmen wir eine Liste von Zeichenfolgen ("rot", "grün", "blau") und analysieren sie. Ich könnte die Grammatik auf einige Arten schreiben. Die folgenden Beispiele sind direkt links bzw. rechts rekursiv:
arg_list: arg_list:
STRING STRING
| arg_list ',' STRING | STRING ',' arg_list
Die Bäume für diese Analyse:
(arg_list) (arg_list)
/ \ / \
(arg_list) BLUE RED (arg_list)
/ \ / \
(arg_list) GREEN GREEN (arg_list)
/ /
RED BLUE
Beachten Sie, wie es in Richtung der Rekursion wächst.
Dies ist kein wirkliches Problem, es ist in Ordnung, eine linksrekursive Grammatik schreiben zu wollen ... wenn Ihr Parser-Tool damit umgehen kann. Bottom-up-Parser kommen damit zurecht. So können modernere LL-Parser. Das Problem mit rekursiven Grammatiken ist nicht die Rekursion, sondern die Rekursion, ohne den Parser voranzutreiben, oder die Rekursion, ohne ein Token zu verbrauchen. Wenn wir beim Rekursieren immer mindestens 1 Token verbrauchen, erreichen wir schließlich das Ende der Analyse. Linke Rekursion ist definiert als Rekursion ohne Verbrauch, was eine Endlosschleife ist.
Diese Einschränkung ist lediglich ein Implementierungsdetail für die Implementierung einer Grammatik mit einem naiven Top-Down-LL-Parser (rekursiver Abstiegsparser). Wenn Sie sich an linksrekursive Grammatiken halten möchten, können Sie sich damit befassen, indem Sie die Produktion neu schreiben, um vor dem Rekursieren mindestens 1 Token zu verbrauchen. Auf diese Weise wird sichergestellt, dass wir nie in einer unproduktiven Schleife stecken bleiben. Für jede Grammatikregel, die linksrekursiv ist, können wir sie umschreiben, indem wir eine Zwischenregel hinzufügen, die die Grammatik auf nur eine Lookahead-Ebene reduziert und ein Token zwischen den rekursiven Produktionen verbraucht. (HINWEIS: Ich sage nicht, dass dies der einzige oder bevorzugte Weg ist, die Grammatik neu zu schreiben, sondern nur auf die verallgemeinerte Regel hinzuweisen. In diesem einfachen Beispiel ist die beste Option die Verwendung der rechtsrekursiven Form.) Da dieser Ansatz verallgemeinert ist, Ein Parser-Generator kann es implementieren, ohne den Programmierer (theoretisch) einzubeziehen. In der Praxis glaube ich, dass ANTLR 4 jetzt genau das tut.
Für die obige Grammatik würde die LL-Implementierung, die die linke Rekursion anzeigt, so aussehen. Der Parser würde mit der Vorhersage einer Liste beginnen ...
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();
}
}
In Wirklichkeit haben wir es wirklich mit "naiver Implementierung" zu tun, dh. Wir haben zunächst einen bestimmten Satz vorhergesagt und dann die Funktion für diese Vorhersage rekursiv aufgerufen, und diese Funktion ruft naiv dieselbe Vorhersage erneut auf.
Bottom-up-Parser haben nicht das Problem rekursiver Regeln in beide Richtungen, da sie den Satzanfang nicht wiederholen, sondern den Satz wieder zusammensetzen.
Rekursion in einer Grammatik ist nur dann ein Problem, wenn wir von oben nach unten produzieren, dh. Unser Parser "erweitert" unsere Vorhersagen, wenn wir Token verbrauchen. Wenn wir nicht expandieren, sondern kollabieren (Produktionen werden "reduziert"), wie in einem Bottom-Up-Parser von LALR (Yacc / Bison), ist die Rekursion beider Seiten kein Problem.
::=
vonExpression
auf geändertTerm
haben und dasselbe nach dem ersten getan haben||
, ist es nicht mehr rekursiv? Aber wenn Sie es erst danach tun würden::=
, aber nicht||
, wäre es immer noch rekursiv?