Warum sollte im Compiler-Design die Linksrekursion in Grammatiken beseitigt werden? Ich lese, dass es daran liegt, dass es eine unendliche Rekursion verursachen kann, aber gilt das nicht auch für eine richtige rekursive Grammatik?
Warum sollte im Compiler-Design die Linksrekursion in Grammatiken beseitigt werden? Ich lese, dass es daran liegt, dass es eine unendliche Rekursion verursachen kann, aber gilt das nicht auch für eine richtige rekursive Grammatik?
Antworten:
Linksrekursive Grammatiken sind nicht unbedingt eine schlechte Sache. Diese Grammatiken können einfach mit einem Stapel analysiert werden, um die bereits analysierten Phrasen zu verfolgen, wie dies beim LR-Parser der Fall ist .
Denken Sie daran, dass eine linksrekursive Regel einer CF-Grammatik die folgende Form hat:
mit ein Element von und ein Element von . (Siehe die vollständige formale Definition für das Tupel dort ).
Normalerweise ist eine Folge von Terminals und Nicht-Terminals, und es gibt eine andere Regel für bei der nicht auf der rechten Seite angezeigt wird.
Immer wenn der Grammatik-Parser (aus dem Lexer) ein neues Terminal empfängt, wird dieses Terminal auf den Stapel geschoben. Diese Operation wird als Verschiebung bezeichnet .
Jedes Mal, wenn die rechte Seite einer Regel mit einer Gruppe aufeinanderfolgender Elemente am oberen Rand des Stapels abgeglichen wird, wird diese Gruppe durch ein einzelnes Element ersetzt, das den neu abgeglichenen Ausdruck darstellt. Dieser Ersatz wird als Reduktion bezeichnet .
Bei richtigen rekursiven Grammatiken kann der Stapel unbegrenzt wachsen, bis eine Reduzierung eintritt, wodurch die Analysemöglichkeiten drastisch eingeschränkt werden. Bei rekursiven Links kann der Compiler jedoch früher (in der Tat so bald wie möglich) Reduzierungen generieren. Weitere Informationen finden Sie im Wikipedia-Eintrag .
Betrachten Sie diese Regel:
example : 'a' | example 'b' ;
Stellen Sie sich nun einen LL-Parser vor, der versucht, eine nicht übereinstimmende Zeichenfolge wie 'b'
diese Regel zu finden. Da 'a'
nicht übereinstimmt, wird versucht, eine Übereinstimmung zu erzielen example 'b'
. Aber um das zu tun, muss es passen example
... und genau das hat es auch versucht. Es könnte für immer stecken bleiben, um zu sehen, ob es übereinstimmen kann, weil es immer versucht, den gleichen Strom von Token mit der gleichen Regel abzugleichen.
Um dies zu verhindern, müssten Sie entweder die Verschachtelung von rechts aus analysieren (was meines Erachtens recht ungewöhnlich ist und das Problem stattdessen durch eine richtige Rekursion lösen würde), den zulässigen Verschachtelungsgrad künstlich begrenzen oder eine Übereinstimmung herstellen ein Token, bevor die Rekursion beginnt, damit es immer einen Basisfall gibt (nämlich, in dem alle Token verbraucht wurden und es immer noch keine vollständige Übereinstimmung gibt). Da eine rechtsrekursive Regel bereits die dritte Regel ausführt, besteht nicht dasselbe Problem.
(Ich weiß, dass diese Frage mittlerweile ziemlich alt ist, aber für den Fall, dass andere die gleiche Frage haben ...)
Fragen Sie im Zusammenhang mit Parsern rekursiver Abstammung? Zum Beispiel für die Grammatik expr:: = expr + term | term
, warum so etwas (links rekursiv):
// expr:: = expr + term
expr() {
expr();
if (token == '+') {
getNextToken();
}
term();
}
ist problematisch, aber nicht das (richtig rekursiv)?
// expr:: = term + expr
expr() {
term();
if (token == '+') {
getNextToken();
expr();
}
}
Es sieht so aus, als ob sich beide Versionen von expr()
call selbst nennen. Der wichtige Unterschied ist jedoch der Kontext - dh das aktuelle Token, wenn dieser rekursive Aufruf erfolgt.
Im linken rekursiven Fall expr()
ruft er sich ständig mit demselben Token auf und es werden keine Fortschritte erzielt. In dem richtigen rekursiven Fall verbraucht es einen Teil der Eingabe in dem Aufruf von term()
und dem PLUS-Token, bevor es den Aufruf von erreicht expr()
. Zu diesem Zeitpunkt kann der rekursive Aufruf den Ausdruck aufrufen und dann beendet werden, bevor der if-Test erneut erreicht wird.
Betrachten Sie zum Beispiel das Parsen von 2 + 3 + 4. Der linke rekursive Parser ruft expr()
unendlich lange auf, während er sich auf dem ersten Token befindet, während der rechte rekursive Parser "2 +" verbraucht, bevor er expr()
erneut aufruft . Der zweite Aufruf expr()
entspricht "3 +" und ruft expr()
nur mit den 4 übrig. Die 4 stimmen mit einem Begriff überein und das Parsen wird ohne weitere Aufrufe von beendet expr()
.
Aus dem Bison-Handbuch:
"Jede Art von Sequenz kann entweder mit der linken oder der rechten Rekursion definiert werden, aber Sie sollten immer die linke Rekursion verwenden , da sie eine Sequenz aus einer beliebigen Anzahl von Elementen mit begrenztem Stapelspeicherplatz analysieren kann. Die rechte Rekursion belegt Speicherplatz auf dem Bison-Stapel in proportional zur Anzahl der Elemente in der Sequenz, da alle Elemente auf den Stapel verschoben werden müssen, bevor die Regel auch nur einmal angewendet werden kann.
http://www.gnu.org/software/bison/manual/html_node/Recursion.html
Es hängt also vom Algorithmus des Parsers ab, aber wie in anderen Antworten angegeben, funktionieren einige Parser möglicherweise einfach nicht mit Linksrekursion