Unterschied zwischen einem LL- und einem rekursiven Abstiegsparser?

78

Ich habe kürzlich versucht, mir selbst beizubringen, wie Parser (für Sprachen / kontextfreie Grammatiken) funktionieren, und das meiste davon scheint sinnvoll zu sein, abgesehen von einer Sache. Ich konzentriere mich insbesondere auf LL (k) -Grammatiken , für die die beiden Hauptalgorithmen der LL-Parser (unter Verwendung der Stapel- / Analysetabelle) und der Parser für rekursiven Abstieg (einfach unter Verwendung der Rekursion) zu sein scheinen .

Soweit ich sehen kann, funktioniert der rekursive Abstiegsalgorithmus für alle LL (k) -Grammatiken und möglicherweise mehr, während ein LL-Parser für alle LL (k) -Grammatiken funktioniert. Ein rekursiver Abstiegsparser ist jedoch deutlich einfacher zu implementieren als ein LL-Parser (genau wie ein LL-Parser einfacher als ein LR-Parser ist).

Meine Frage ist also, welche Vorteile / Probleme bei der Verwendung eines der Algorithmen auftreten können. Warum könnte man jemals LL über rekursiven Abstieg wählen, da es mit denselben Grammatiken arbeitet und schwieriger zu implementieren ist?

Noldorin
quelle
Hey, kannst du das bitte erklären? Sie schreiben anscheinend den LL-Parser (mithilfe der Stapel- / Analysetabelle) und den Parser für rekursiven Abstieg (einfach mithilfe der Rekursion). . Diese Antwort besagt, dass alle Top-Down-Parser LL-Parser sind. Ich bin verwirrt
Max Koretskyi
2
@ MaximKoretskyi Das stimmt definitiv nicht. LL-Parser sind eine Teilmenge von Top-Down-Parsern.
Noldorin
danke, kannst du vielleicht bitte deine antwort auf meine frage posten?
Max Koretskyi

Antworten:

104

LL ist normalerweise eine effizientere Analysetechnik als rekursiver Abstieg. Tatsächlich ist ein naiver Parser mit rekursivem Abstieg im schlimmsten Fall tatsächlich O (k ^ n) (wobei n die Eingabegröße ist). Einige Techniken wie das Auswendiglernen (was einen Packrat- Parser ergibt ) können dies verbessern und die vom Parser akzeptierte Grammatikklasse erweitern, aber es gibt immer einen Kompromiss zwischen Leerzeichen. LL-Parser sind (meines Wissens) immer lineare Zeit.

Auf der anderen Seite haben Sie Recht mit Ihrer Intuition, dass Parser mit rekursivem Abstieg eine größere Klasse von Grammatiken verarbeiten können als LL. Rekursiver Abstieg kann jede Grammatik verarbeiten, die LL (*) ( dh unbegrenzter Lookahead) ist, sowie eine kleine Menge mehrdeutiger Grammatiken. Dies liegt daran, dass rekursiver Abstieg tatsächlich eine direkt codierte Implementierung von PEGs oder Parser Expression Grammatik (en) ist . Insbesondere ist der disjunktive Operator ( a | b) nicht kommutativ, was bedeutet, dass a | bdies nicht gleich ist b | a. Ein Parser mit rekursivem Abstieg versucht jede Alternative der Reihe nach. Wenn also adie Eingabe übereinstimmt, ist sie auch dann erfolgreich, wenn b sie mit der Eingabe übereinstimmt. Dies ermöglicht klassische "längste Übereinstimmungen" wie das Baumelnelse Problem einfach durch korrekte Reihenfolge der Disjunktionen zu lösen.

Nach alledem ist es möglich , einen LL (k) -Parser unter Verwendung eines rekursiven Abstiegs zu implementieren, so dass er in linearer Zeit ausgeführt wird. Dies erfolgt durch im Wesentlichen Inlining der Vorhersagesätze, so dass jede Analyseroutine die geeignete Produktion für eine gegebene Eingabe in konstanter Zeit bestimmt. Leider verhindert eine solche Technik, dass eine ganze Klasse von Grammatiken behandelt wird. Sobald wir mit dem prädiktiven Parsen beginnen, sind Probleme wie das Baumeln elsenicht mehr so ​​einfach zu lösen .

Warum LL vor rekursivem Abstieg gewählt wird, ist hauptsächlich eine Frage der Effizienz und Wartbarkeit. Parser mit rekursivem Abstieg sind deutlich einfacher zu implementieren, aber normalerweise schwieriger zu pflegen, da die von ihnen dargestellte Grammatik in keiner deklarativen Form vorhanden ist. Die meisten nicht trivialen Parser-Anwendungsfälle verwenden einen Parser-Generator wie ANTLR oder Bison. Bei solchen Tools spielt es keine Rolle, ob der Algorithmus direkt codierter rekursiver Abstieg oder tabellengesteuertes LL (k) ist.

Interessanterweise lohnt es sich auch, den rekursiven Aufstieg zu untersuchen. Hierbei handelt es sich um einen Parsing-Algorithmus, der direkt nach der Art des rekursiven Abstiegs codiert ist, jedoch jede LALR-Grammatik verarbeiten kann. Ich würde mich auch mit Parser-Kombinatoren befassen, die eine funktionale Möglichkeit sind, Parser mit rekursivem Abstieg zusammenzustellen.

Daniel Spiewak
quelle
2
Sehr die Antwort, auf die ich gehofft hatte! :) Danke für all die Infos dort (einschließlich des letzten Teils, von dem ich nicht einmal wusste). Es wird wahrscheinlich etwas länger dauern, bis ich alle Konzepte verstanden habe, die Sie in dieser Antwort vorgestellt haben, aber Sie haben meine Frage sicherlich beantwortet und mich in die richtige Richtung für weitere Studien gelenkt. Die Hauptsache, die ich jetzt verschwommen bin, ist, wie PEGs sich auf rekursive Abstiegsparser beziehen und wie genau ein Parser-Kombinator verschiedene Parser kombiniert. Wenn Sie eines von beiden klären könnten, wäre ich Ihnen sehr dankbar.
Noldorin
6
Sätze vorhersagen: Das hängt wirklich von der verwendeten Strategie ab. Wenn Sie sich ausschließlich auf diese Vorhersagesätze verlassen und kein Backtracking durchführen, ist die Grammatikklasse genau LL (k), wobei k die Menge an Lookahead ist, die zur Berechnung der Vorhersagesätze verwendet wird. Sie können jedoch viele Vorteile des Predictive Parsing nutzen, indem Sie Predict Sets in RD einfügen, ohne das Backtracking vollständig zu eliminieren. Dies ermöglicht Parser, die alle Grammatiken akzeptieren, die normalerweise von RD verarbeitet werden, jedoch mit einem schnelleren Durchschnittsfall. Leider ist der Worst-Case für solche Parser immer noch exponentiell.
Daniel Spiewak
5
Die meisten Parser mit rekursivem Abstieg (auch handgeschriebene) verwenden Inline-Vorhersagesätze, um die Alternativen zu begrenzen und das Backtracking einzuschränken, ohne die Flexibilität einzuschränken. Das Endergebnis ist ein Parser, der für alles außer den pathologischsten Grammatiken nahezu linear ist und der immer noch die gesamte Klasse von PEGs akzeptiert.
Daniel Spiewak
4
Gutes Zeug. Ein Trottel: "Die meisten nicht trivialen Anwendungsfälle sind in einer Art Parser-Generator implementiert ...". Das ist nicht wahr. Die am häufigsten verwendeten Compiler und IDEs (C #, VB, Visual C ++ und GCC sind gute Beispiele) verwenden handgeschriebene Parser. Dies sind wohl einige der nicht trivialsten Verwendungen.
Scott Wisniewski
3
@DanielSpiewak Ich weiß, dass Sie diesen Kommentar vor Jahren gepostet haben, aber schon damals war er falsch;) GCC verwendete Bison für den C-Parser bis 3.x, wechselte aber 2008 soweit wie möglich zu einem handgeschriebenen Parser für rekursiven Abstieg Erzählen Sie von diesem gcc.gnu.org/wiki/New_C_Parser
Morten Jensen