LR(k)-Analyse für Pragmatiker - Hu
Transcription
LR(k)-Analyse für Pragmatiker - Hu
LR(k)-Analyse für Pragmatiker Andreas Kunert Version 2.212 (18. Mai 2015) Humboldt-Universität zu Berlin Institut für Informatik / ZE Rechenzentrum (CMS) ii Version: 2.212 (18. Mai 2015) INHALTSVERZEICHNIS 1 Grundbegriffe 1.1 Grammatiken und Ableitbarkeit . . . . . . . . . . . . 1.1.1 Definition Grammatik . . . . . . . . . . . . . 1.1.2 Kontextfreie Grammatiken und Ableitbarkeit . 1.1.3 LL(k)- und LR(k)-Sprachen . . . . . . . . . . 1.1.4 Die Symbole ε und $ . . . . . . . . . . . . . . 1.2 FIRST-Mengen . . . . . . . . . . . . . . . . . . . . . 1.2.1 Definition . . . . . . . . . . . . . . . . . . . . 1.2.2 Algorithmus zur Berechnung von FIRST . . . 1.2.3 Beispiel . . . . . . . . . . . . . . . . . . . . . 1.3 FOLLOW-Mengen . . . . . . . . . . . . . . . . . . . . 1.3.1 Definition . . . . . . . . . . . . . . . . . . . . 1.3.2 Algorithmus zur Berechnung von FOLLOW . 1.3.3 Beispiel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 5 5 6 6 7 8 8 8 9 10 10 11 11 2 Bottom-Up-Syntaxanalyse 2.1 Aufbau einer Syntaxanalysetabelle . . . . . . . . . . . . . . . . . . . 2.2 Ablauf der Syntaxanalyse . . . . . . . . . . . . . . . . . . . . . . . . 2.3 Beispiel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 13 14 15 3 LR(0)-Syntaxanalyse 3.1 Prinzipieller Ablauf und Grundlagen 3.2 LR(0)-Elemente . . . . . . . . . . . . 3.3 Der Hüllenoperator CLOSURE0 . . . 3.3.1 Algorithmus zur Berechnung 3.3.2 Beispiel . . . . . . . . . . . . 3.4 Die Sprungoperation GOTO0 . . . . 3.4.1 Algorithmus zur Berechnung 17 17 18 19 19 19 20 20 Version: 2.212 (18. Mai 2015) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . iii I NHALTSVERZEICHNIS 3.4.2 Beispiel . . . . . . . . . . . . . . . . . . 3.5 Das Aufstellen des Zustandsübergangsgraphen . 3.5.1 Theorie . . . . . . . . . . . . . . . . . . 3.5.2 Beispiel . . . . . . . . . . . . . . . . . . 3.6 Das Erstellen der Syntaxanalysetabelle . . . . . 3.6.1 Algorithmus . . . . . . . . . . . . . . . . 3.6.2 Beispiel . . . . . . . . . . . . . . . . . . . . . . . . . 21 21 21 22 22 22 24 4 SLR(1)-Syntaxanalyse 4.1 Idee und Anwendung . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.2 Beispiel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27 27 28 5 LR(1)-Syntaxanalyse 5.1 Idee . . . . . . . . . . . . . . . . . . . . . . . . 5.2 LR(1)-Elemente . . . . . . . . . . . . . . . . . . 5.3 Der Hüllenoperator CLOSURE1 . . . . . . . . . 5.3.1 Algorithmus zur Berechnung . . . . . . 5.3.2 Beispiel . . . . . . . . . . . . . . . . . . 5.4 Die Sprungoperation GOTO1 . . . . . . . . . . 5.5 Das Aufstellen des Zustandsübergangsgraphen . 5.6 Das Erstellen der Syntaxanalysetabelle . . . . . . . . . . . . . 31 31 32 32 32 33 34 35 36 6 LALR(1)-Syntaxanalyse 6.1 Idee . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.2 Beispiel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37 37 38 7 LR(k)-Syntaxanalyse für k ≥ 2 7.1 Motivation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7.2 FIRSTk und FOLLOWk . . . . . . . . . . . . . . . . . . . . . . . . . . 7.2.1 Die Hilfsoperationen k-Präfix und k-Konkatenation . . . . . . 7.2.2 FIRSTk . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7.2.3 FOLLOWk . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7.3 LR(k)-Elemente, CLOSUREk und GOTOk . . . . . . . . . . . . . . . . 7.4 LR(k)-Zustandsübergangsgraphen und LR(k)-Syntaxanalysetabellen 7.5 LR(k)-Syntaxanalyse . . . . . . . . . . . . . . . . . . . . . . . . . . . 7.6 Beispiel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41 41 42 42 43 43 44 44 45 46 A Aufgaben A.1 Einfache Grammatiken . . . . . . . . . . . . . . . . . . . . . . . . . . A.2 Komplexere Grammatiken . . . . . . . . . . . . . . . . . . . . . . . . A.3 Real existierende Grammatiken . . . . . . . . . . . . . . . . . . . . . 49 50 56 60 B Literaturverzeichnis 63 iv . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Version: 2.212 (18. Mai 2015) VORWORT Die LR(k)-Analyse ist innerhalb der Informatik mit zwei wichtigen Erkenntnissen verbunden: 1. Sie stellt die heute gebräuchlichste Form der Syntaxanalyse dar. Dies ist zum einen dadurch begründet, dass die LR(k)-Analyse das mächtigste Verfahren unter den effizienten Syntaxanalysealgorithmen darstellt und zum anderen, dass es schon seit längerer Zeit Parsergeneratoren gibt, die einem die fehlerträchtige Tabellenkonstruktion abnehmen. 2. Man kann als Informatikstudent Nächte damit verbringen. Der vorliegende Text soll helfen, den Einstieg in die Materie etwas zu vereinfachen, indem er zuerst die notwendigen Grundbegriffe erklärt und anschließend die einzelnen Analyseverfahren (in der Theorie und am Beispiel) beschreibt. Dabei kann (und soll) dieser Text kein Compilerbaubuch ersetzen. Er sollte vielmehr als Ergänzung angesehen werden; genau die Stellen erklären, die im Buch unverständlich waren (und vielleicht genau an den Stellen Lücken aufweisen, wo das Buch eine gute Beschreibung liefert. . . ). Was dem Leser vielleicht relativ früh in diesem Text auffallen wird, ist die vergleichsweise lockere Sprache. Diese ist zum einen begründet durch die Zielgruppe (Studenten im dritten Semester) und zum anderen, dass es sich um einen Versuch meinerseits handelt (vielleicht der letzte), ein relativ trockenes Thema halbwegs nett lesbar zu beschreiben („unterhaltsam“ wäre etwas zu hoch gegriffen). Wie auch immer, es handelt sich um meinen ersten Versuch eines derartigen Pamphlets, so dass ich über Kommentare, Kritiken und vor allem Korrekturen und Verbesserungsvorschläge jeder Art dankbar bin. Dabei spielt es bei den letztgenannten Punkten keine Rolle, ob es sich um inhaltliche, grammatikalische oder Ausdrucksfehler handelt. Version: 2.212 (18. Mai 2015) 1 I NHALTSVERZEICHNIS Die jeweils letzte Version dieses Texts sollte irgendwo auf meiner Homepage unter www.informatik.hu-berlin.de/~kunert1 zu finden sein. Ich bitte darum, immer erst dort nachzusehen, ob der Fehler nicht eventuell bereits behoben wurde. Falls nicht, bitte einfach eine Mail an mich: [email protected] Als Abschluss würde ich mich gerne noch bei den Probelesern bedanken und zwar bei Dr. Klaus Ahrens, Prof. Klaus Bothe sowie den beiden angehenden DiplomInformatikern Glenn Schütze und Konrad Voigt. Andreas Kunert Berlin, 2003-06-02 Vorwort zur zweiten Version Es war der 17. Januar 2003, an dem zum ersten Mal eine Urversion des vorliegenden Skriptes kompiliert wurde. Es war wohl am frühen Nachmittag; eine genaue Zeit kann ich erst ab der 13. Kompilation geben (13:27:27 Uhr CET), da erst dann die von mir im makefile implementierte automatische Dokumentation der Kompilationshistorie funktionierte. Dabei hatte ich ursprünglich gar nicht vor, ein Lehr-Skript für Studenten zu schreiben, sondern wollte lediglich eine Notizensammlung anfertigen, mittels derer ich einmal jährlich (und zwar genau dann, wenn Compilerbau-Praktika anstehen) innerhalb kurzer Zeit wieder in den LR-Stoff „reinkomme“ und mir nicht jedes Mal erneut alles aus etlichen Quellen zusammensuchen muss. Was mich damals bewogen hat, dann doch mehr zu schreiben, kann ich heute leider nicht mehr sagen. . . Wie dem auch sei, am 11. August 2003 (13:35:42 CEST) war das Skript dann soweit gediehen, dass ich mich traute, ihm die Versionsnummer „1“ zu geben (erstes Major Release?). Im selben Zeitraum erfolgte auch die erste Veröffentlichung auf meiner Homepage. Seitdem ist eine Menge Zeit vergangen. Insbesondere in den ersten Jahren gab es zahlreiche Fehlerhinweise, was nicht unwesentlich mit meiner Zusage verbunden war, alle „erfolgreichen“ Fehlerfinder in zukünftigen Versionen dieses Skriptes namentlich zu verewigen.2 Die Zahl der gemeldeten Korrekturwünsche pro Jahr ist in der folgenden Zeit dann zwar deutlich zurückgegangen, insbesondere nachdem ich keine Compilerbau-Praktika mehr gehalten habe, hat aber nie den Wert null erreicht. Was demgegenüber deutlich zugenommen und mich nachvollziehbar sehr erfreut hat, ist die Menge von Danksagungsmails aus den mitunter entlegensten Ecken des deutschsprachigen Raumes. Diese brachten mich dazu, mal meinem Skript „hinterherzugooglen“3 mit folgenden, zumindest für mich überraschenden Ergebnissen (Achtung, es folgt eine schamlose Selbstbeweihräucherung!): 1 Kleines Update: inzwischen ist die URL kürzer: www.hu-berlin.de/~kunert Eine Zusage, die übrigens weiterhin gilt. 3 ein Wort, dass ich in der ersten Version dieses Skriptes sicherlich noch nicht benutzt hätte 2 2 Version: 2.212 (18. Mai 2015) I NHALTSVERZEICHNIS • das Skript wird offiziell in Compiler-Lehrveranstaltungen von vier Universitäten4 und einem Gymnasium eingesetzt, • das Skript wird in mehreren Foren-Einträgen empfohlen und Seminararbeiten referenziert, • Wikipedia kennt mein Skript5 Inzwischen arbeite ich als Datenbank- und System-Administrator am Rechenzentrum der Humboldt-Universität. Da ich allerdings das Lehren doch stark vermisst habe und mein Büro überdies nur ca. 200 m vom Institut für Informatik entfernt ist, halte ich seit diesem Semester auf freiwilliger Basis wieder Compilerbau-Praktika. Sowohl die Dankesmails, die Resonanz im Web als auch meine erneute Aufnahme der Lehrtätigkeit haben zu der vorliegenden Überarbeitung geführt, wobei der Großteil der Änderungen kosmetischer Natur (Wechsel der Rechtschreibung, interne Links, netteres Layout, . . . ) sind bzw. sich im Hintergrund (Aufbau der LATEX-Dateien, Versionsverwaltung, Einbindung in den umgezogenen Web-Auftritt, . . . ) abgespielt haben. Die Überarbeitung ist auch noch nicht wirklich abgeschlossen (wie ich mich kenne, wird sie es auch nie), hat aber zumindest einen Stand erreicht, der meiner bescheidenen Meinung nach eine neue Versionsnummer rechtfertigt. Zum Abschluss möchte ich mich bei allen bedanken, die mir freundliche Mails geschickt haben (ich beantworte definitiv jede Skript-bezogene Mail, es kann nur manchmal leider etwas länger dauern6 ) sowie bei allen, die mich dazu bewogen haben, das Skript erneut zu überarbeiten. Andreas Kunert Berlin, 2011-05-27 4 die Humboldt-Universität nicht mitgezählt . . . und ich lege Wert darauf, dass die dortigen Links nicht von mir stammen (auch wenn ich diese bei einem späteren Umzug meiner Homepage korrigiert habe) 6 mein bisheriger Negativrekord liegt bei drei Jahren 5 Version: 2.212 (18. Mai 2015) 3 I NHALTSVERZEICHNIS 4 Version: 2.212 (18. Mai 2015) KAPITEL 1 GRUNDBEGRIFFE In diesem Kapitel werden einige Grundbegriffe geklärt, die im späteren Verlauf benötigt werden. 1.1 Grammatiken und Ableitbarkeit Ich werde hier nicht sonderlich tief auf Grammatiken und deren Einteilung eingehen, da mit ziemlicher Sicherheit jeder, der sich diesen Text zu Gemüte führt, etwas damit anfangen kann. Es geht mir in diesem Kapitel primär darum, klarzustellen, welche Symbole ich standardmäßig für die einzelnen Elemente einer Grammatik benutzen werde. Dabei halte ich mich an die Notation, die im Buch von Uwe Schöning [Sch97] verwendet wird. 1.1.1 Definition Grammatik Eine (Typ-0-)Grammatik ist ein 4-Tupel G = (V, Σ, P, S), wobei die einzelnen Elemente folgende Bedeutung haben: V Σ P S die Menge der Variablen (auch Metasymbole oder Nichtterminale genannt – ich persönlich präferiere die Bezeichnung Metasymbole) die Menge der Terminalsymbole (Σ ∩ V = ∅) die Menge der Produktionen (P ⊆ (V ∪ Σ)+ × (V ∪ Σ)∗ ) das Startsymbol (S ∈ V ) Für die Elemente aus P hat sich statt der sonst üblichen Paarschreibweise (α, β) die intuitivere Notation α → β eingebürgert.1 1 Im weiteren Verlauf dieses Skriptes wird zwar die intuitive Notation benutzt, aber zur besseren Unterscheidung vom restlichen Text trotzdem ein Klammerpaar gesetzt, d. h. das genannte Beispiel hat in diesem Skript die folgende Form: (α → β). Version: 2.212 (18. Mai 2015) 5 KAPITEL 1. G RUNDBEGRIFFE 1.1.2 Kontextfreie Grammatiken und Ableitbarkeit Die Grammatikdefinition im vorherigen Abschnitt erschlägt zwar wirklich alle Grammatiken, ist für uns aber eher unpraktisch. Der Grund dafür liegt darin, dass man bei vielen Grammatiken (Typ-0) nicht entscheiden kann, ob ein gegebenes Wort Element der durch die Grammatik beschriebenen Sprache ist, oder diese Entscheidung zwar möglich, aber für praktische Anwendungen zu komplex ist (Typ1/kontextsensitiv)2 . Dummerweise ist es aber leider genau dieses sogenannte Wortproblem, das wir bei der Syntaxanalyse lösen müssen. Für uns Compilerbauer beginnen die Grammatiken ab Typ-2/kontextfrei interessant zu werden. Eine kontextfreie Grammatik ist eine Grammatik, für deren Produktionen gilt, dass sie auf der linken Seite immer aus genau einem Metasymbol bestehen. Für alle Produktionen (α → β) gilt also α ∈ V , so dass unsere Regelmenge P folgende Gestalt annimmt: P ⊆ V × (V ∪ Σ)∗ Für die später folgenden Definitionen benötigen wir noch eine wichtige Relation, und zwar die Ableitung (⇒). Es gilt (mit x, y, γ ∈ (V ∪ Σ)∗ , A ∈ V ): x ⇒ y :⇐⇒ ∃α,β,γ,A : x = αAβ ∧ y = αγβ ∧ (A, γ) ∈ P Diese Definition beschreibt die Ableitung einer Zeichenkette aus einer anderen, indem einfach ein Metasymbol mittels einer dem Metasymbol zugehörigen Regel durch die rechte Seite eben dieser Regel substituiert wird. Die reflexiv-transitive Hülle (⇒∗ ) wird intuitiv gebildet (x, y, wi ∈ (V ∪ Σ)∗ ): x ⇒∗ y :⇐⇒ (x = y) ∨ (x ⇒ y) ∨ (∃w1 ...wn : x ⇒ w1 ⇒ . . . ⇒ wn ⇒ y) Eine Zeichenkette y ist dann mittels der Hülle aus einer Zeichenkette x ableitbar, wenn die beiden entweder identisch sind oder aber sich y über einen oder mehrere Zwischenschritte aus x ableiten lässt. Mit Hilfe der obigen Definitionen können wir nun die Sprache L zu einer gegebenen Grammatik G definieren. Wie zu erwarten besteht diese aus allen Wörtern, die wir aus dem Startsymbol (direkt oder indirekt) ableiten können: L(G) := x ∈ Σ∗ S ⇒∗ x 1.1.3 LL(k)- und LR(k)-Sprachen Die Sprachen, die man durch kontextfreie Grammatiken beschreiben kann, nennt man naheliegenderweise kontextfreie Sprachen. Diese Sprachklasse ist für Compilerbauer deshalb so interessant, da man in ihr das Wortproblem in kubischer (also polynomialer) Zeit lösen kann.3 2 3 6 Für die Einteilung und Benennung der Grammatiken nach Noam Chomsky siehe [Sch97] Für genauere Details siehe Abschnitt 1.3.4 „Der CYK-Algorithmus“ in [Sch97]. Version: 2.212 (18. Mai 2015) 1.1. G RAMMATIKEN UND A BLEITBARKEIT Tragischerweise ist eine kubische Laufzeit für praktische Zwecke immer noch zu langsam, so dass wir uns im Compilerbau mit noch eingeschränkteren Sprach- bzw. Grammatikklassen auseinandersetzen. Die prominentesten Vertreter sind die sogenannten LL(k)- und die LR(k)-Sprachen. In beiden lässt sich das Wortproblem in linearer Zeit lösen. Eine der ersten Fragen, die dieses Schriftstück beantworten möchte und die gleichzeitig gerne in Compilerbauprüfungen auftaucht, lautet: „Wofür stehen die einzelnen Buchstaben in den Bezeichnungen LL(k) und LR(k)?“. Der erste Buchstabe (also in beiden Fällen ein „L“) steht für die Leserichtung bei der Syntaxanalyse – es wird in beiden Fällen von links nach rechts gelesen. Der zweite Buchstabe gibt an, ob bei der Syntaxanalyse eine Links- oder eine Rechtsableitung entsteht, d. h. ob in jedem Ableitungsschritt immer das am weitesten links oder rechts stehende Metasymbol substituiert wird. Das k steht für den sogenannten LOOKAHEAD. Dieser gibt an, wieviele Zeichen man in der Eingabe „vorausschauen“ muss, um in jedem Ableitungschritt eine deterministische Entscheidung treffen zu können, mittels welcher Substitution man fortfährt. Auf LL(k)-Sprachen und -Grammatiken soll an dieser Stelle nicht näher eingegangen werden, da dies den Rahmen eines LR(k)-Skripts ein wenig sprengen würde. Die verschiedenen Klassen von LR(k)-Grammatiken werden in den Kapiteln ab Kapitel 3 ausführlich beschrieben. Eine wichtige Teilmengenbeziehung von Sprachklassen soll an dieser Stelle aber noch erwähnt werden, da sie zum Grundwissen gehört: LL(k) ⊂ LR(k) = (det. kontextfreie Sprachen) ⊂ (kontextfreie Sprachen) 1.1.4 Die Symbole ε und $ Neben den bereits beschriebenen Definitionen benötigen wir zusätzlich noch die zweier spezieller Symbole. Zunächst wäre da das ε, das für das leere Wort (ein Wort bestehend aus null Terminalsymbolen) steht. Das ε tritt vor allem in Grammatikproduktionen der Form (X → ε) auf, was gleichbedeutend ist mit: „X kann in einem Ableitungsschritt mittels dieser Regel entfernt werden“. Metasymbole, die sich mittels einer oder mehrerer Grammatikregeln auf das ε reduzieren lassen werden als „ε-ableitbar“ bezeichnet. Das $ hingegen ist ein Terminalsymbol, das das Ende einer Eingabe bezeichnet, d. h. es ist das letzte Zeichen in allen Wörtern einer Sprache. In der Compilerbauliteratur gibt es verschiedene Ansichten, ob das $ explizit in den Grammatikregeln notiert werden sollte, oder nicht. In diesem Skript werde ich das $ implizit als Ende der Eingabe betrachten und es nicht extra in der Grammatik auftauchen lassen. Version: 2.212 (18. Mai 2015) 7 KAPITEL 1. G RUNDBEGRIFFE 1.2 FIRST-Mengen 1.2.1 Definition FIRST-Mengen werden eigentlich fast immer automatisch mit der LL(k)-Analyse in Verbindung gebracht (wo sie auch wirklich exzessiv genutzt werden), aber sie spielen auch bei der LR(k)-Analyse eine wichtige Rolle. Genaugenommen betrachten wir in diesem Kapitel nur einen Spezialfall von FIRST-Mengen und zwar die FIRST1 -Mengen (statt FIRST-Mengen müsste man eigentlich FIRSTk -Mengen schreiben). Da man in der Praxis jedoch fast ausschließlich mit FIRST1 -Mengen konfrontiert wird, ist es in der Compilerbauliteratur üblich, statt FIRST1 einfach FIRST zu schreiben und den allgemeinen Fall FIRSTk völlig zu ignorieren.4 Dieser Tradition möchte dieses Skript zur Hälfte folgen: Auch hier wird im folgenden die vereinfachte Notation FIRST statt FIRST1 benutzt, aber die FIRSTk -Mengen werden trotzdem noch behandelt und zwar in Kapitel 7. FIRST-Mengen werden von beliebigen Zeichenketten, bestehend aus Terminalund Metasymbolen, gebildet. Die FIRST-Menge einer solchen Zeichenkette gibt an, welche Terminalsymbole als jeweils erstes Symbol in einer von der Zeichenkette abgeleiteten Zeichenkette auftreten können (wobei alle möglichen Ableitungen berücksichtigt werden). Diese Formulierung ist natürlich schrecklich informal und bringt nur demjenigen etwas, der bereits eine Vorstellung von einer FIRST-Menge hat, daher folgt eine schöne, formale, zweistufige Definition:5 FIRST0 (V ∪ Σ)+ → P(Σ) FIRST0 (x) := y ∃α∈(Σ∪V )∗ : x ⇒∗ yα, y ∈ Σ : (V ∪ Σ)+ → P(Σ ∪ {ε}) ( FIRST0 (x) ∪ {ε} , wenn x ⇒∗ ε FIRST(x) := FIRST0 (x) , sonst FIRST : Die Definition von FIRST ist zwar relativ leicht nachvollziehbar, kann jedoch nur bedingt zur konstruktiven Berechnung der FIRST-Mengen verwendet werden. Der Grund besteht darin, dass bei rekursiver Regelanwendung unendlich viele Ableitungen in die Definition einer FIRST-Menge eingehen können. 1.2.2 Algorithmus zur Berechnung von FIRST Der folgende, dem Drachenbuch [ASU99] entnommene, Algorithmus zur FIRSTMengenberechnung scheint da schon wesentlich geeigneter zu sein. Zuerst werden für alle Meta- und Terminalsymbole die zugehörigen FIRSTMengen berechnet, indem die folgenden drei Schritte solange iterativ angewendet werden, bis sich keine FIRST-Menge mehr ändert: 4 5 8 Es gibt natürlich Ausnahmen, z. B. [Spe03] und [Wil97] Zur Notation: P steht für die Potenzmenge (P(X) := {Y |Y ⊆ X}) Version: 2.212 (18. Mai 2015) 1.2. FIRST-M ENGEN 1. wenn X ∈ Σ (also X ein Terminal), so ist FIRST(X) = {X} 2. gibt es eine Produktion der Form (X → ε), so füge ε zu FIRST(X) hinzu 3. gibt es eine Produktion der Form (X → Y1 Y2 . . . Yn ), so füge zunächst FIRST(Y1 ) \ {ε} zu FIRST(X) hinzu. Sollte gelten ε ∈ FIRST(Y1 ), d. h. Y1 lässt sich zu ε reduzieren, so füge auch FIRST(Y2 ) \ {ε} hinzu. Sollte wiederum Y2 ε-ableitbar sein, fahre wie gehabt fort, bis du auf ein Yi stößt, das nicht ε-ableitbar ist. Sollten alle Yi ε-ableitbar sein, dann (und nur dann) füge auch ε zu FIRST (X) hinzu. Anschließend kann man ohne weitere Probleme die FIRST-Menge jeder beliebigen Zeichenkette X1 X2 . . . Xn berechnen. Dazu nimmt man analog zum soeben praktizierten dritten Schritt erst FIRST(X1 ) \ {ε} zu FIRST (X1 X2 . . . Xn ) hinzu. Sollte X1 ε-ableitbar sein, nimm auch FIRST(X2 ) \ {ε} zu FIRST (X1 X2 . . . Xn ) hinzu, usw. Sollten alle Xi ε-ableitbar sein, dann (und nur dann) füge auch ε zu FIRST (X1 X2 . . . Xn ) hinzu. 1.2.3 Beispiel Zum Abschluss des Abschnitts soll ein Beispiel die Anwendung des Algorithmus verdeutlichen. Gegeben sei die folgende Grammatik G1 mit dem Startsymbol Z: (1) (2) (3) (4) (5) (6) Z S S A A A → → → → → → S Sb bAa aSc a aSb Wir wollen nun die FIRST-Mengen der einzelnen Grammatiksymbole berechnen (wobei die Berechnung der FIRST-Mengen der Terminalsymbole aufgrund der ersten Regel im Algorithmus zugegebenermaßen etwas witzlos ist). Fangen wir mit FIRST(A) an; die vierte Produktion liefert uns (A → aSc), worauf wir die dritte Regel anwenden können: A → |{z} a |{z} S |{z} c |{z} X Y1 Y2 Y3 Wir müssen also FIRST(Y1 ) = FIRST(a) zu FIRST(A) hinzunehmen. Dank der ersten Regel wissen wir, dass FIRST(a) = {a}. Da a nicht ε-ableitbar ist (oder anders: die FIRST-Menge von a kein ε enthält), müssen wir Y2 und Y3 (also S und c) nicht weiter betrachten. Die dritte Regel auf die Produktionen 5 und 6 angewendet, liefert uns keine weiteren Informationen (es wird stets a zu FIRST(A) hinzugefügt). Wendet man die Version: 2.212 (18. Mai 2015) 9 KAPITEL 1. G RUNDBEGRIFFE dritte Regel auf die dritte Produktion (S → bAa) an, so erfährt man, dass man b zu FIRST(S) hinzunehmen soll. Die zweite Produktion ist relativ nett: wir sollen FIRST(S) zu FIRST(S) hinzunehmen – dieser Schritt kann ignoriert werden. Zu guter Letzt wenden wir uns noch der ersten Produktion zu. Diese sorgt dafür, dass FIRST(S) zu FIRST(Z) hinzugenommen wird. Nun gehen wir noch einmal im Kopf alle Regeln des Algorithmus und alle Produktionen der Grammatik durch und stellen fest, dass bei keiner erneuten Anwendung einer Regel auf eine Produktion eine Änderung an der bereits berechneten FIRSTMenge erfolgt. Fasst man alle Berechnungen zusammen, so erhält man abschließend folgende Tabelle: X FIRST(X) Z {b} S {b} A {a} 1.3 FOLLOW-Mengen 1.3.1 Definition Neben den FIRST-Mengen haben auch die FOLLOW-Mengen im Compilerbau eine große Bedeutung. Auch sie werden exzessiv bei der LL(k)-Analyse benutzt und spielen dort insbesondere bei der Fehlerstabilisierung eine wichtige Rolle. Bei der LR(k)-Analyse werden sie vor allem beim SLR(1)-Verfahren benötigt. Analog zu den FIRST-Mengen spielen auch bei den FOLLOW-Mengen (eigentlich FOLLOWk -Mengen) die FOLLOW1 -Mengen eine so große Bedeutung, dass wir statt FOLLOW1 einfach nur FOLLOW schreiben und uns mit den FOLLOWk -Mengen erst in Kapitel 7 beschäftigen. Die FOLLOW-Menge eines Metasymbols (FOLLOW-Mengen können nur von Metasymbolen gebildet werden) enthält alle Terminalsymbole, die in irgendeinem Ableitungsschritt unmittelbar rechts von diesem Metasymbol stehen können. Etwas formaler drückt es die folgende Definition aus: FOLLOW0 V → P(Σ) FOLLOW (X) := y ∃α,β∈(Σ∪V )∗ : S ⇒∗ αXyβ, y ∈ Σ : 0 V → P (Σ ∪ {$}) ( FOLLOW0 (X) ∪ {$} FOLLOW(X) := FOLLOW0 (X) FOLLOW : , wenn S ⇒∗ αX , sonst Anmerkung: Die Definition ist deshalb zweistufig (mit dem Umweg über FOLLOW0 ) aufgebaut, da wir $ als nicht explizit in der Grammatik vorkommend definiert haben. In Compilerbaubüchern, die das $ bereits in den Grammatikregeln auftauchen lassen, wird dieser Umweg ausgelassen. 10 Version: 2.212 (18. Mai 2015) 1.3. FOLLOW-M ENGEN 1.3.2 Algorithmus zur Berechnung von FOLLOW Der folgende Algorithmus zur gleichzeitigen Ermittlung der FOLLOW-Mengen aller Metasymbole wurde ebenfalls dem Drachenbuch [ASU99] entnommen und besteht aus drei Schritten, die solange iterativ angewendet werden, bis sich keine der FOLLOW-Mengen mehr ändert: 1. nimm $ zu FOLLOW(S) hinzu (S sei das Startsymbol) 2. gibt es eine Produktion (A → αBβ), so nimm FIRST(β) \ {ε} zu FOLLOW(B) hinzu 3. gibt es eine Produktion (A → αB) oder (A → αBβ) mit β ⇒∗ ε, so füge FOLLOW(A) zu FOLLOW(B) hinzu Im Prinzip ist jede der drei Regeln leicht nachvollziehbar. Nichtsdestotrotz folgt noch ein Beispiel, um die Handhabung zu demonstrieren. 1.3.3 Beispiel Gegeben sei wieder die Grammatik G1 : (1) (2) (3) (4) (5) (6) Z S S A A A → → → → → → S Sb bAa aSc a aSb Wir wollen nun die FOLLOW-Mengen der drei Metasymbole berechnen. In der folgenden Abbildung, die die Berechnung darstellt, stehen jeweils über den Pfeilen zuerst die angewendeten Regeln und unmittelbar dahinter (in Klammern) die Produktionen, die Opfer der jeweiligen Regel wurden. X Z S A 2(3) =⇒ 2(4) =⇒ Version: 2.212 (18. Mai 2015) X Z S A X Z S A FOLLOW(X) {} {} {} FOLLOW(X) {$} {} {a} FOLLOW(X) {$} {b, c} {a} 1 =⇒ 2(2/6) =⇒ 3(1) =⇒ X Z S A X Z S A X Z S A FOLLOW(X) {$} {} {} FOLLOW(X) {$} {b} {a} 2(3) =⇒ 2(4) =⇒ FOLLOW(X) {$} {b, c, $} {a} 11 KAPITEL 1. G RUNDBEGRIFFE Der letzte Kasten stellt das Ende der Berechnung dar. Es wurden alle Regeln auf allen passenden Produktionen ausgeführt und eine erneute Anwendung wird keine neuen Elemente mehr in eine der FOLLOW-Mengen bringen. 12 Version: 2.212 (18. Mai 2015) KAPITEL 2 BOTTOM-UP-SYNTAXANALYSE In diesem Abschnitt soll es darum gehen, wie man mit Hilfe einer gegebenen Syntaxanalysetabelle (in manchen Compilerbaubüchern auch Parsertabellen genannt; engl.: parse table) überprüft, ob ein Wort Element einer durch eine Grammatik beschriebenen Sprache ist. Dazu simuliert man einen Kellerautomaten, d. h. einen Zustandsautomaten mit „angeschlossenem“ Kellerspeicher (auf gut deutsch: Stack), der sich in einem bestimmten Zustand befindet und, abhängig von diesem Zustand, dem obersten Symbol des Kellerspeichers und dem nächsten Eingabesymbol, eine Aktion ausführt und anschließend den Zustand wechselt, wobei die Aktion eine Schreiboperation auf dem Kellerspeicher zur Folge haben kann. 2.1 Aufbau einer Syntaxanalysetabelle Syntaxanalysetabellen bestehen aus zwei Teilen: der Aktionstabelle und der Sprungtabelle. In beiden werden die Zustände des Automaten in den Zeilenköpfen aufgelistet. In den Spaltenköpfen befinden sich bei der Aktionstabelle die Terminalsymbole und in der Sprungtabelle die Metasymbole. Da sich die Spaltenköpfe beider Tabellen nicht überschneiden und die Zeilenköpfe identisch sind, werden die beiden Tabellen oft zu einer Syntaxanalysetabelle zusammengefasst. Innerhalb der Aktionstabelle werden die Aktionen aufgelistet, die der Parser ausführen soll, wenn er sich in einem bestimmten Zustand (repräsentiert durch die Zeile) befindet und das nächste Eingabesymbol (repräsentiert durch die Spalte) liest. Diese Aktion kann eine der folgenden sein: Schieben (shift), Reduzieren (reduce), Akzeptieren (accept) oder Fehler (error). In der Sprungtabelle werden nur Zustandsnummern eingetragen. Diese geben dem Parser an, in welchem Zustand er fortfahren soll, wenn er die Reduktion zu einem bestimmten Metasymbol durchgeführt hat. Version: 2.212 (18. Mai 2015) 13 KAPITEL 2. B OTTOM -U P-S YNTAXANALYSE Abbildung 2.1 auf Seite 15 zeigt eine Syntaxanalysetabelle für Grammatik G1 . Wir interessieren uns in diesem Kapitel noch nicht dafür, wie diese Tabelle zustande gekommen ist, sondern nehmen sie einfach mal als gegeben hin. 2.2 Ablauf der Syntaxanalyse Mit Hilfe einer Syntaxanalysetabelle ein gegebenes Wort zu analysieren bedeutet einen Kellerautomaten zu simulieren. Auf dem Stack des Automaten werden sowohl Grammatiksymbole als auch Zustandsnummern gespeichert (genaugenommen kann man die Symbole auch weglassen – sie dienen jedoch sehr der Übersichtlichkeit). Man beginnt mit einem fast leeren Stack, auf dem sich lediglich ein spezielles Symbol für den Kellerboden (meist # oder $) und die Nummer des Startzustandes befinden. Anschließend werden die folgenden Anweisungen iterativ ausgeführt: 1. Lesen des obersten Stackelementes (sollte eine Zustandsnummer sein) 2. Lesen des nächsten Zeichens in der Eingabe 3. Nachschlagen in der Aktionstabelle, welche Aktion dieser Kombination aus Zustand und Eingabezeichen zugeordnet ist 4. Ausführen der Aktion Dabei bedeuten die verschiedenen möglichen Aktionen folgende Arbeitsschritte: shift n Zuerst wird das nächste Zeichen in der Eingabe aus dieser entfernt und auf den Stack gelegt. Anschließend wird n als neuer Zustand auf den Stack gepackt. reduce m Zunächst entfernt man doppelt soviele Elemente vom Stack, wie Symbole auf der rechten Seite von Produktion Nummer m stehen.1 Anschließend liest man die Zustandsnummer z, die jetzt ganz oben auf dem Stack liegt. Nun wird das Metasymbol der linken Seite der Produktion auf den Stack gelegt und in der Sprungtabelle nachgeschlagen, welche Zustandsnummer diesem Metasymbol beim Zustand z zugeordnet ist. Zu guter Letzt wird auch diese neue Zustandsnummer auf den Stack gelegt.2 Das Zeichen aus der Eingabe bleibt bei einer Reduktionsoperation unberührt und wird bei der folgenden Aktion erneut als Entscheidungskriterium benutzt. 1 Doppelt soviele, weil wir ja pro Zustand auch noch ein Grammatiksymbol mit auf den Stack schreiben. 2 Anmerkung: Die Zahl hinter dem r (z. B. 5 in r5) gibt wirklich nur die Nummer der anzuwendenden Produktion an. Es ist ein beliebter Anfängerfehler zu glauben, dahinter verberge sich eine Zustandsnummer (analog zur Schiebeoperation). 14 Version: 2.212 (18. Mai 2015) 2.3. B EISPIEL a 0 1 2 3 4 5 6 7 8 9 Aktionstabelle b c $ s3 s2 acc r2 r2 s6 s5 r5 r4 r6 Sprungtabelle A S Z 1 4 r3 s3 s9 r3 s8 r2 r2 r3 7 r2 Abbildung 2.1: Syntaxanalysetabelle für G1 accept Erreicht man diese Aktion, wurde das Eingabewort erfolgreich akzeptiert, und man ist fertig mit der Analyse. error Kommt man in die Verlegenheit, diese Aktion auszuführen, ist das Eingabewort nicht in der Sprache enthalten. Erroranweisungen befinden sich in Aktionstabellen in allen Zellen, in denen keine der anderen Aktionen steht (man lässt das explizite Hinschreiben von „error“ zugunsten der Übersichtlichkeit weg). 2.3 Beispiel Wir wollen nun versuchen, ein Wort der zur Grammatik G1 gehörenden Sprache zu analysieren. Die Syntaxanalysetabelle ist in Abbildung 2.1 zu sehen. Das zu analysierende Wort sei baab. Zunächst befinden sich auf dem Stack der Kellerboden ($) und die Anfangszustandsnummer (0). In der Eingabe steht das Wort, gefolgt vom Eingabeendesymbol (ebenfalls das Zeichen $3 ): Stack: $0 Eingabe: baab$ In der Aktionstabelle steht für den Zustand 0 und das Eingabezeichen b die Aktion s3, also ein Schieben des Eingabesymbols und des neuen Zustands 3 in den Stack: $0b3 3 aab$ Im Normalfall macht die Unterscheidung, wann der Kellerboden und wann das Eingabeende gemeint ist, keine Probleme. Version: 2.212 (18. Mai 2015) 15 KAPITEL 2. B OTTOM -U P-S YNTAXANALYSE Für die Kombination aus Zustand 3 und Eingabezeichen a kann man in der Aktionstabelle die Aktion s6 ausfindig machen: $0b3a6 ab$ Nun erfolgt die erste Reduktion (r5 – abgelesen in der Aktionstabelle bei Zustand 6 und Eingabezeichen a). Produktion 5 lautet: (A → a). Wir entfernen doppelt soviele Elemente aus dem Stack, wie die Produktion auf der rechten Seite Symbole hat (2 × 1 = 2 Elemente). Anschließend legen wir das Metasymbol der linken Seite (A) auf den Stack und bilden aus den beiden (nun) obersten Elementen des Stacks (Zustand 3 und Symbol A) mit Hilfe der Sprungtabelle den nächsten Zustand (4), den wir ebenfalls auf den Stack legen. Das Ergebnis der Bemühungen ist folgendes: $0b3A4 ab$ Wir fahren nun nach dem Schema immer weiter fort, bis wir auf die Aktion Akzeptieren (acc) stoßen. Die folgende Tabelle zeigt den gesamten Analyseablauf (also auch die bereits erläuterten drei Anfangsschritte). Die jeweils auszuführende Aktion steht in der dritten Spalte: $0 $0b3 $0b3a6 $0b3A4 $0b3A4a5 $0S1 $0S1b2 $0S1 baab$ aab$ ab$ ab$ b$ b$ $ $ shift 3 shift 6 reduce 5 (A → a) shift 5 reduce 3 (S → bAa) shift 2 reduce 2 (S → Sb) accept Das Analysieren eines Wortes mit Hilfe einer gegebenen Syntaxanalysetabelle ist also recht einfach. Etwas komplizierter ist es, die Tabelle selbst herzustellen – ein Thema, dem wir uns in den nächsten Kapiteln zuwenden werden. 16 Version: 2.212 (18. Mai 2015) KAPITEL 3 LR(0)-SYNTAXANALYSE In den nun folgenden Kapiteln soll es darum gehen, die Syntaxanalysetabelle zu einer gegebenen Grammatik G mittels verschiedener Methoden selbst herzustellen. Das in diesem Kapitel verwendete Verfahren ist das einfachste, aber leider auch am wenigsten mächtige der hier vorgestellten Herangehensweisen. 3.1 Prinzipieller Ablauf und Grundlagen Der Ablauf der Erstellung einer Syntaxanalysetabelle ist (unabhängig vom konkreten Verfahren) immer der folgende: 1. Hinzufügen einer neuen Startproduktion (S 0 → S) zu G, wobei S das alte Startsymbol und S 0 ein völlig neues Startsymbol ist. Hintergrund dieses Vorganges ist das Problem zu entscheiden, wann die Syntaxanalyse beendet ist. Während der Syntaxanalyse werden fortlaufend Produktionen der Grammatik in umgekehrter Richtung angewendet. Wenn man nun bei der Syntaxanalyse in einen Zustand gelangt, in dem mittels der (künstlichen) Startproduktion (S 0 → S) reduziert werden muss, weiß man, dass die gesamte Eingabe erfolgreich zum eigentlichen Startsymbol (S) reduziert wurde, d. h. das Wort in der Eingabe ist tatsächlich ein Element aus der Sprache der ursprünglichen Grammatik. 2. Bildung des Zustandsübergangsgraphen des zukünftigen Kellerautomaten (ein Beispiel eines solchen Graphen ist in Abbildung 3.1 auf Seite 23 zu sehen). 3. Aufstellen der Syntaxanalysetabelle mit Hilfe des Zustandsübergangsgraphen. Punkt 1 kann als trivial betrachtet werden. Wesentlich interessanter sind die Punkte 2 und 3. Widmen wir uns zunächst der Erstellung des Zustandsübergangsgraphen. Version: 2.212 (18. Mai 2015) 17 KAPITEL 3. LR(0)-S YNTAXANALYSE 3.2 LR(0)-Elemente Bevor wir mit der Konstruktion beginnen, müssen einige Grundlagen geschaffen werden. Fangen wir mit der Definition von sogenannten LR(0)-Elementen an. Sehr vereinfacht ausgedrückt ist ein LR(0)-Element nichts weiter als eine Grammatikproduktion, die irgendwo auf der rechten Seite einen Punkt enthält. Nehmen wir also z. B. die dritte Produktion aus G1 : (S → bAa), so können wir daraus die folgenden LR(0)-Elemente bilden: [S [S [S [S → .bAa] → bA.a] → b.Aa] → bAa.] So weit, so gut, aber was soll der Punkt nun eigentlich bedeuten? Im Prinzip stellt ein LR(0)-Element einen Zustand während einer laufenden Syntaxanalyse dar. Alle Symbole links vom Punkt befinden sich in diesem Zustand bereits auf dem Stack, während alle Zeichen rechts vom Punkt noch nicht eingelesen wurden. Befindet sich der Parser in einem durch ein LR(0)-Element charakterisierten Zustand, so können die folgenden drei disjunkten Fälle auftreten: 1. Der Punkt befindet sich ganz am rechten Ende des LR(0)-Elementes. Das bedeutet, die gesamte rechte Seite der zugrundeliegenden Regel befindet sich auf dem Stack; im nächsten Schritt kann also mittels dieser Regel reduziert werden (Reduce1 ). 2. Unmittelbar rechts vom Punkt befindet sich ein Terminalsymbol. In diesem Fall wird der Parser im folgenden Schritt prüfen, ob es sich beim nächsten Zeichen von der Eingabe um dieses Terminalsymbol handelt. Im Positivfall legt er es auf den Stack (Shift) und wechselt den Zustand. Für diesen Zustandswechsel definieren wir uns eine Sprungoperation. Im Negativfall wird eine Fehlermeldung generiert (Error). 3. Unmittelbar rechts vom Punkt befindet sich ein Metasymbol. In diesem Fall kommen wir auf direktem Weg nicht weiter (wir lesen schließlich keine Metasymbole von der Eingabe). Stattdessen müssen wir es schaffen, die nächsten Eingabezeichen zum benötigten Metasymbol zu reduzieren. Als Hilfestellung dafür (dieser Prozess kann selbstverständlich rekursiv über viele Ebenen gehen) definieren wir uns im folgenden einen Hüllenoperator. Bevor wir mit der nächsten Hilfskonstruktion fortfahren soll noch kurz ein hinreichend oft vorkommender Spezialfall erwähnt werden: Zu jeder Grammatikregel mit einem ε auf der rechten Seite existiert genau ein LR(0)-Element und zwar [X → .], d. h. man lässt das ε-Zeichen weg. 1 bzw. Accept, falls es sich bei der Regel um die künstlich hinzugefügte Startregel handelt 18 Version: 2.212 (18. Mai 2015) 3.3. D ER H ÜLLENOPERATOR CLOSURE0 3.3 Der Hüllenoperator CLOSURE0 3.3.1 Algorithmus zur Berechnung Beschäftigen wir uns zunächst mit dem Operator CLOSURE0 (Hülle). Dieser dient, wie oben im dritten Fall erwähnt, der Erkennung abgeleiteter Metasymbole während der Bearbeitung einer Regel. Die Bildung einer Hülle CLOSURE0 (I) zu einer Menge I von LR(0)-Elementen ist relativ einfach. Die Bildungsvorschrift besteht aus zwei Regeln: 1. füge I zu CLOSURE0 (I) hinzu (dieser Schritt dürfte keinen verwundern, der bei Hüllenoperatoren in Theoretischer Informatik I aufgepasst hat) 2. gibt es ein LR(0)-Element [A → α.Bβ] aus CLOSURE0 (I) und eine Produktion (B → γ), so füge [B → .γ] zu CLOSURE0 (I) hinzu Die Bildungsvorschrift anzuwenden, funktioniert also folgendermaßen: Zunächst fügt man alle Elemente aus I zu CLOSURE0 (I) hinzu. Anschließend überprüft man in allen Elementen aus CLOSURE0 (I) (also im ersten Schritt dasselbe wie I), ob sich Metasymbole unmittelbar rechts von einem Punkt befinden. Ist das der Fall, so fügt man alle Produktionen, die aus diesem Metasymbol ableiten, zu CLOSURE0 (I) hinzu, wobei der Punkt an den Anfang der rechten Seite gesetzt wird. Dabei ist zu beachten, dass es durchaus mehrere LR(0)-Elemente zu einer gleichen Produktion in CLOSURE0 (I) geben kann (also zwei Elemente, die sich nur in der Position des Punktes unterscheiden). Hat man alle in Frage kommenden LR(0)-Elemente hinzugefügt, überprüft man diese neu hinzugekommenen Elemente auf Metasymbole unmittelbar rechts vom Punkt. Sollten neue Metasymbole hinzugekommen sein, so werden auch deren Produktionen als LR(0)-Elemente zu CLOSURE0 (I) hinzugefügt. Diese Prozedur wiederholt man solange, bis kein neues Metasymbol mehr unmittelbar rechts von einem Punkt in irgendeinem LR(0)-Element aus CLOSURE0 (I) existiert. 3.3.2 Beispiel Nehmen wir einmal mehr Grammatik G1 und berechnen die Hülle des aus der Startproduktion entstandenen LR(0)-Elementes [Z → .S]. (1) (2) (3) (4) (5) (6) Z S S A A A → → → → → → S Sb bAa aSc a aSb Im ersten Schritt fügen wir alle Elemente aus {[Z → .S]} zur Hülle hinzu: [Z → .S] Version: 2.212 (18. Mai 2015) 19 KAPITEL 3. LR(0)-S YNTAXANALYSE Nun sehen wir nach, ob wir in einem der enthaltenen LR(0)-Elemente ein Metasymbol unmittelbar rechts vom Punkt finden. Wir finden das Metasymbol S, also fügen wir alle Produktionen der Grammatik, die S auf der linken Seite enthalten, zur Hülle hinzu, wobei wir den Punkt an den Anfang der rechten Seite setzen: [Z → .S] [S → .Sb] [S → .bAa] Nun prüfen wir bei allen den neu hinzugekommenen Elementen, ob ein Metasymbol direkt rechts von einem Punkt steht. Dies trifft hier nur auf das S zu, dessen Produktionen wir aber bereits hinzugenommen haben. Da wir also keine neuen Metasymbole mehr finden, mit denen wir fortfahren müssten, sind wir fertig (und können den Kasten schließen): [Z → .S] [S → .Sb] [S → .bAa] Wir haben also erfolgreich berechnet: CLOSURE0 [Z → .S] = [Z → .S], [S → .Sb], [S → .bAa] 3.4 Die Sprungoperation GOTO0 3.4.1 Algorithmus zur Berechnung Die zweite Hilfsoperation, die benötigt wird, ist GOTO0 (I, X). Diese erzeugt zu einem Zustand, der durch eine Menge I von LR(0)-Elementen charakterisiert wird, den Folgezustand unter der Annahme, dass das Symbol X ∈ Σ ∪ V erfolgreich gelesen bzw. hergeleitet wurde. Die Bildung ist nicht sonderlich kompliziert: Man nimmt zuerst alle Elemente aus I und fügt davon diejenigen zu GOTO0 (I, X) hinzu, in denen sich X unmittelbar rechts vom Punkt befindet, wobei man den Punkt hinter das X verschiebt. Anschließend wird von der neuen Menge die Hülle gebildet, und schon ist man fertig. Die semantische Bedeutung ist klar: Man hat ein benötigtes Zeichen X (rechts vom Punkt stehend) gelesen (bzw. im Falle eines Metasymbols hat man selbiges durch Reduktion erhalten) und wechselt jetzt in den Zustand, wo sich das Zeichen auf dem Stack (also links vom Punkt) befindet. Man kann die gesamte Definition selbstverständlich auch etwas formaler ausdrücken: n o GOTO0 (I, X) = CLOSURE0 [A → αX.β] [A → α.Xβ] ∈ I 20 Version: 2.212 (18. Mai 2015) 3.5. D AS AUFSTELLEN DES Z USTANDSÜBERGANGSGRAPHEN 3.4.2 Beispiel Nehmen wir noch einmal Grammatik G1 . Aufgabe sei nun, folgende Menge zu berechnen: GOTO0 [Z → .S], [S → .Sb], [S → .bAa] , b Zunächst suchen wir alle LR(0)-Elemente, die b unmittelbar rechts von einem Punkt stehen haben (genau eins: [S → .bAa]) und fügen sie zur Menge hinzu, wobei wir den Punkt eine Position nach rechts (also hinter das b) verschieben: [S → b.Aa] Der noch offene Kasten soll bereits andeuten, dass wir noch nicht fertig sind: Es fehlt noch die Hüllenbildung. Das einzige Metasymbol unmittelbar rechts von einem Punkt ist A, so dass wir also noch alle von A ableitenden Produktionen hinzunehmen müssen: [S → b.Aa] [A → .aSc] [A → .a] [A → .aSb] Nun sind wir auch schon fertig, da keine LR(0)-Elemente hinzugekommen sind, die weitere Metasymbole unmittelbar rechts des Punktes stehen haben. Damit haben wir also erfolgreich berechnet: GOTO0 [Z → .S], [S → .Sb], [S → .bAa] , b = [S → b.Aa], [A → .aSc], [A → .a], [A → .aSb] 3.5 Das Aufstellen des Zustandsübergangsgraphen 3.5.1 Theorie Hat man einmal die Bildung der Mengen GOTO0 und CLOSURE0 verinnerlicht, so erweist sich das Aufstellen des Zustandsübergangsgraphen als relativ einfache Übung. Man beginnt mit der künstlich eingefügten Startproduktion (S 0 → S). Von dem zugehörigen LR(0)-Element [S 0 → .S] bildet man nun die Hülle, interpretiert die erhaltene Menge von LR(0)-Elementen als Zustand und gibt ihm eine Nummer (welche ist völlig egal – 0 oder 1 scheinen für den Startzustand sinnvoll). Nun bildet man für jedes (Terminal- oder Meta-)Symbol X, das sich unmittelbar rechts vom Punkt in einem LR(0)-Element des Startzustandes befindet, die Menge GOTO0 . Die dabei entstehenden Mengen bekommen wieder jeweils eigene Nummern und werden durch Pfeile mit dem Startzustand verbunden, an die man das verwendete Symbol X schreibt. Nun fährt man mit diesen Zuständen wie mit dem Startzustand fort (also GOTO0 Mengenbildung und Pfeile malen). Sollte man beim Erstellen einer GOTO0 -Menge Version: 2.212 (18. Mai 2015) 21 KAPITEL 3. LR(0)-S YNTAXANALYSE feststellen, dass ein identischer Zustand bereits vorhanden ist, so erzeugt man keinen neuen, sondern verwendet stattdessen den bereits vorhandenen. Die gesamte Spielerei führt man solange fort, bis sich kein noch nicht betrachtetes Symbol mehr unmittelbar rechts von einem Punkt in irgendeinem LR(0)-Element irgendeines Zustandes befindet. Hat man dies erreicht, sollte man einen vollständigen LR(0)-Zustandsübergangsgraphen vor sich haben. 3.5.2 Beispiel Zum Beispiel ist nicht viel zu sagen, wenn Hüllen- und Sprungoperation bereits „sitzen“. Nehmen wir die Grammatik G1 und bilden davon den Zustandsübergangsgraphen. Wir beginnen mit der Regel (Z → S).2 Die Hülle des zur Startproduktion gehörenden LR(0)-Elementes [Z → .S] haben wir bereits in Abschnitt 3.3.2 ausgerechnet. Sie sieht folgendermaßen aus: [Z → .S] [S → .Sb] [S → .bAa] Nun bildet man die Sprungoperationen dieser Menge mit allen Symbolen, die in irgendeinem LR(0)-Element dieser Menge unmittelbar rechts von einem Punkt auftauchen (hier also S und b). Anschließend werden Pfeile, die mit dem entsprechenden Symbol beschriftet werden, von dem alten Zustand zu den einzelnen neuen Zuständen gezogen. Diesen Vorgang wiederholt man nun mit den neuen Zuständen, bis es nichts mehr zu tun gibt. In Abbildung 3.1 ist der vollständige Zustandsübergangsgraph zu sehen. Die Numerierung der einzelnen Zustände kann beliebig erfolgen, da sie auf die spätere Analyse keinen Einfluss hat. 3.6 Das Erstellen der Syntaxanalysetabelle 3.6.1 Algorithmus Hat man den Zustandsübergangsgraphen erfolgreich aufgestellt, so kann man mit Hilfe dessen die Syntaxanalysetabelle erzeugen. Das Verfahren an sich ist nicht schwer. Zunächst erstellt man eine leere Tabelle, deren Spaltenköpfe erst die Terminalsymbole (für die Aktionstabelle) und dann die Metasymbole (für die Sprungtabelle) enthalten. In die Zeilenköpfe trägt man die Zustandsnummern ein. Nun fängt man an, die Tabelle auszufüllen. Man beginnt in einem beliebigen Zustand (wobei es der Übersichtlichkeit halber sinnvoll ist, sie in der Reihenfolge durchzugehen, in der sie numeriert sind). Für jeden vom Zustand weggehenden 2 Z ist in G1 bereits von vornherein als künstliches Startsymbol S 0 eingebaut worden. 22 Version: 2.212 (18. Mai 2015) 3.6. D AS E RSTELLEN DER S YNTAXANALYSETABELLE S Z → .S S → .Sb S → .bAa 0 Z → S. S → S.b b 1 S → Sb. 2 b A S → b.Aa A → .aSc A → .a A → .aSb S → bA.a a 4 S → bAa. 5 3 a A → a.Sc b A → a. A → a.Sb S → .Sb S → .bAa S 6 A → aS.c A → aS.b S → S.b c 7 A → aSc. b A → aSb. S → Sb. 8 9 Abbildung 3.1: LR(0)-Zustandsübergangsgraph für Grammatik G1 Pfeil, der mit einem Terminalsymbol beschriftet ist, erstellt man einen Eintrag in der zugehörigen Tabellenzelle (die sich innerhalb der Aktionstabelle befinden sollte). Dieser Eintrag erhält als Aktion „shift“ und die Nummer des Zustandes, auf den der Pfeil verweist. Für jeden Pfeil, der mit einem Metasymbol beschriftet ist, erstellt man analog einen Eintrag in der Sprungtabelle, nur dass man die Aktion weglässt, also nur die Nummer des Folgezustandes notiert. Anschließend trägt man im Zustand, in dem die künstliche Startregel vollständig akzeptiert wird (also sich der Punkt am Ende der rechten Seite befindet ([Z → S.])), die Aktion accept beim Terminalsymbol $ ein. Wir hatten ja diese künstliche Produktion damit motiviert, dass wir bei ihrer Anwendung die Syntaxanalyse positiv beenden können. Nun bleiben noch die „echten“ Reduktionen übrig: Für jedes LR(0)-Element (aus allen Zuständen), in dem sich der Punkt am Ende der rechten Seite befindet, trägt man für alle Terminalsymbole in der dem Zustand entsprechenden Zeile als Aktion „reduce“ und die Nummer der zum LR(0)-Element gehörenden Regel (die man aus der Definition der Grammatik erhält) ein. Sollte man beim Eintragen einer Reduktionsaktion feststellen, dass in der zu beschreibenden Tabellenzelle bereits ein Eintrag vorhanden ist, so hat man einen Konflikt. Und zwar (je nachdem, ob sich in der Zelle eine Schiebe- oder Reduktionsanweisung befand) einen Schiebe/Reduktions-(shift-reduce bzw. s/r-) oder einen Reduktions/Reduktions-(reduce/reduce bzw. r/r-)konflikt. Sollte dieser Fall auftreten, so ist die gegebene Grammatik nicht vom Typ LR(0) und kann daher nicht mit diesem einfachen Verfahren behandelt werden. Version: 2.212 (18. Mai 2015) 23 KAPITEL 3. LR(0)-S YNTAXANALYSE Hat man hingegen alle Zustände vollständig betrachtet und beim Aufbau der Tabelle keinen Konflikt erhalten, so erhält man gleich zwei Dinge: erstens, das Wissen, dass die Grammatik wirklich vom Typ LR(0) ist und zweitens, eine (hoffentlich) korrekte Syntaxanalysetabelle, mit der man nun zur Syntaxanalyse schreiten kann. 3.6.2 Beispiel Versuchen wir nun selbst eine Syntaxanalysetabelle für G1 herzustellen. Als Hilfsmittel benötigen wir den Zustandsübergangsgraphen aus Abbildung 3.1 und die Definition der Grammatik mit durchnumerierten Regeln: (1) (2) (3) (4) (5) (6) Z S S A A A → → → → → → S Sb bAa aSc a aSb Wir fangen mit Zustand 0 an. Es gibt genau zwei Pfeile, die diesen Zustand verlassen (einen b-Pfeil nach Zustand 3 und einen S-Pfeil nach Zustand 1). Nach dem obigen Algorithmus muss die erste Zeile in unserer Tabelle also folgendermaßen aussehen: 0 Aktionstabelle a b c s3 $ Sprungtabelle A S Z 1 Da es in diesem Zustand keine vollständig akzeptierten Regeln (Punkt am Ende der rechten Seite) gibt, können wir mit dem nächsten Zustand fortfahren. In Zustand 1 gibt es nur einen ausgehenden Pfeil (b nach Zustand 2). Dieser veranlasst uns, in Zeile 1 der Tabelle in der b-Spalte ein „shift 2“ einzutragen. Nun gibt es jedoch in Zustand 1 noch die vollständig akzeptierte, künstliche Startregel [Z → S.]. Diese verpflichtet uns, in der Aktionstabelle in der $-Spalte der Zeile 1 ein „accept“ einzutragen. Die erste Reduktion taucht im Zustand 2 auf und zwar nach Regel 2 (das ist die Grammatikregel, die dem vollständig akzeptiertem LR(0)-Element [S → Sb.] entspricht). Die Reduktionsoperation wird in der dem Zustand entsprechenden Zeile bei allen Terminalsymbolen eingetragen. Im weiteren Verlauf stellt man fest, dass die Grammatik leider zu zwei Konflikten führt, d. h. Grammatik G1 lässt sich nicht mit dem LR(0)-Verfahren behandeln. Der Vollständigkeit halber (und um zu sehen, ob man den Tabellenaufbau verstanden hat) ist dennoch im folgenden die LR(0)-Syntaxanalysetabelle inklusive der Konflikte zu sehen: 24 Version: 2.212 (18. Mai 2015) 3.6. D AS E RSTELLEN DER S YNTAXANALYSETABELLE a 0 1 2 3 4 5 6 7 8 9 r2 s6 s5 r3 r5 r4 r2/r6 Aktionstabelle b c s3 s2 r2 r2 $ A Sprungtabelle S 1 Z acc r2 4 r3 s3/r5 s9 r4 r2/r6 r3 r5 s8 r4 r2/r6 r3 r5 7 r4 r2/r6 Wie wir sehen, hat die Grammatik G1 beim LR(0)-Verfahren in zwei Zuständen (den Zuständen 6 und 9) Konflikte (einen s/r- und einen r/r-Konflikt). Version: 2.212 (18. Mai 2015) 25 KAPITEL 3. LR(0)-S YNTAXANALYSE 26 Version: 2.212 (18. Mai 2015) KAPITEL 4 SLR(1)-SYNTAXANALYSE 4.1 Idee und Anwendung Das SLR(1)-Verfahren könnte man als „aufgebohrtes“ LR(0)-Verfahren bezeichnen. Es wird zwar zur Vermeidung von Konflikten bereits eine Art Symbolvorgriff verwendet, ohne jedoch auf die erst in Kapitel 5 behandelten (komplizierteren) LookaheadMengen zurückzugreifen. Das „S“ in SLR(1) steht übrigens für „simple“ und das nicht ohne Grund. Das Verfahren ist bis auf die Erstellung der Syntaxanalysetabelle mit dem LR(0)-Verfahren identisch, lediglich die Anzahl der (unnötigen) Reduktionen in der Syntaxanalysetabelle wird verringert. Die Idee beim SLR(1)-Verfahren ist folgende: In der LR(0)-Analyse wurde immer reduziert, wenn die rechte Seite einer Regel erfolgreich erkannt wurde. Das SLR(1)Verfahren nimmt nun eine zusätzliche Information hinzu: Bevor reduziert wird, fragt sich ein SLR(1)-Parser, ob das Symbol, was direkt hinter der erkannten rechten Seite steht, überhaupt dem Metasymbol auf der linken Seite folgen darf und reduziert nur in diesem Fall mit dieser Regel. Man benötigt also zu jedem Metasymbol eine Menge aller Symbole, die diesem folgen können – und genau diese Menge wird zufälligerweise (!?!) durch die bereits bekannte FOLLOW-Menge beschrieben (wir erinnern uns kurz an Abschnitt 1.3). Um also eine SLR(1)-Syntaxanalysetabelle aufzubauen, benötigen wir (im Unterschied zur LR(0)-Analyse) drei Dinge: den Zustandsübergangsgraphen, die Grammatik mit durchnumerierten Produktionen und die FOLLOW-Mengen aller Metasymbole. Der Aufbau funktioniert, was Schiebe- und Akzeptieren-Operationen in der Aktionstabelle sowie Zustandswechsel in der Sprungtabelle angeht, genauso wie bei LR(0), d. h. man geht Zustand für Zustand durch den Zustandsübergangsgraphen und erzeugt für die einzelnen Pfeile die entsprechenden Einträge. Version: 2.212 (18. Mai 2015) 27 KAPITEL 4. SLR(1)-S YNTAXANALYSE S Z → .S S → .Sb S → .bAa 0 Z → S. S → S.b b 1 S → Sb. 2 b A S → b.Aa A → .aSc A → .a A → .aSb S → bA.a a 4 S → bAa. 5 3 a A → a.Sc b A → a. A → a.Sb S → .Sb S → .bAa S 6 A → aS.c A → aS.b S → S.b c 7 A → aSc. b A → aSb. S → Sb. 8 9 Abbildung 4.1: LR(0)-Zustandsübergangsgraph für Grammatik G1 Nun wendet man sich allen LR(0)-Elementen zu, die einen Punkt am Ende der rechten Seite stehen haben, also vollständig akzeptiert wurden. Man trägt in der Aktionstabelle beim zugehörigen Zustand die Reduktion mittels der zugehörigen Regel bei allen Terminalsymbolen ein, die in der FOLLOW-Menge des Metasymbols auf der linken Seite der Regel enthalten sind. Dabei wird man feststellen, dass viele Konflikte des LR(0)-Verfahrens beim SLR(1)-Verfahren einfach verschwinden. Sollten dennoch welche übrig bleiben, ist die gegebene Grammatik leider nicht vom Typ SLR(1), man muss also ein noch mächtigeres Verfahren verwenden. Sollten hingegen keine Konflikte mehr auftauchen, so ist die Grammatik vom Typ SLR(1) und man hat eine funktionierende Syntaxanalysetabelle, die man zur Analyse verwenden kann. 4.2 Beispiel Betrachten wir einmal mehr Grammatik G1 . Die Aufgabe soll darin bestehen, eine SLR(1)-Syntaxanalysetabelle aufzubauen, nachdem der LR(0)-Ansatz ja aufgrund zweier Konflikte fehlgeschlagen ist. Der LR(0)-Zustandsübergangsgraph ist noch einmal in Abbildung 4.1 dargestellt, und es folgt auch noch einmal die Grammatik selbst: 28 Version: 2.212 (18. Mai 2015) 4.2. B EISPIEL (1) (2) (3) (4) (5) (6) Z S S A A A → → → → → → S Sb bAa aSc a aSb Bevor wir fortfahren, benötigen wir noch die FOLLOW-Mengen aller Metasymbole. Diese sind (siehe Abschnitt 1.3.3): FOLLOW(Z) = {$} FOLLOW(S) = {b, c, $} FOLLOW(A) = {a} Und nun frisch ans Werk: Da Zustand 0 keine vollständig erkannte Regel enthält, können wir die erste Zeile der Tabelle wie gehabt ausfüllen: a 0 Aktionstabelle b c s3 $ Sprungtabelle A S Z 1 In Zustand 1 fügen wir zunächst die Schiebeoperation ein: 0 1 Aktionstabelle a b c s3 s2 $ Sprungtabelle A S Z 1 Nun gibt es in diesem Zustand noch eine vollständig akzeptierte Regel, angezeigt durch das LR(0)-Element [Z → S.]. Da es sich dabei um die künstliche Startregel handelt, tragen wir beim Eingabeendezeichen ($) die Aktion „accept“ ein. Wir erhalten als vollständige zweite Zeile: 0 1 Aktionstabelle a b c $ s3 s2 acc Sprungtabelle A S Z 1 Im Zustand 2 taucht erstmals eine „echte“ Reduktion auf (S → Sb), die wir in den Spalten von b, c und $ (FOLLOW(S) = {b, c, $}) eintragen: a 0 1 2 Version: 2.212 (18. Mai 2015) Aktionstabelle b c $ s3 s2 acc r2 r2 r2 Sprungtabelle A S Z 1 29 KAPITEL 4. SLR(1)-S YNTAXANALYSE Führt man das gesamte Verfahren mit den restlichen Zuständen durch, so erhält man schließlich die folgende Syntaxanalysetabelle: a 0 1 2 3 4 5 6 7 8 9 Aktionstabelle b c $ s3 s2 acc r2 r2 r2 s6 s5 r5 r4 r6 Sprungtabelle A S Z 1 4 r3 s3 s9 r3 s8 r2 r2 r3 7 r2 Vergleicht man diese mit der LR(0)-Syntaxanalysetabelle auf Seite 24, so stellt man fest, dass mehrere Reduktionsoperationen entfernt und dadurch beide Konflikte beseitigt wurden. 30 Version: 2.212 (18. Mai 2015) KAPITEL 5 LR(1)-SYNTAXANALYSE 5.1 Idee In der bisher als Beispiel verwendeten Grammatik G1 waren wir bereits mit dem SLR(1)-Verfahren in der Lage, eine konfliktfreie Syntaxanalysetabelle aufzubauen. Im allgemeinen reicht dieses Verfahren jedoch leider noch nicht aus, um gängige Programmiersprachenkonstrukte zu behandeln. Die Lösung bringt das mächtigste aller (deterministischen) LR-Verfahren: das LR(1)-Verfahren (manchmal auch „kanonisches LR(1)-Verfahren“ genannt, um es besser von den anderen zu unterscheiden). Um mächtiger als die SLR(1)-Analyse sein zu können, benötigt das LR(1)Verfahren mehr Informationen. In der SLR(1)-Analyse wurde mit Hilfe der FOLLOW-Mengen entschieden, ob das nächste Terminalsymbol überhaupt dem Metasymbol, zu dem reduziert werden soll, folgen darf. Nur im Positivfall wurde wirklich reduziert. Die LR(1)-Analyse geht nun noch einen Schritt weiter. Zwar wird auch hier entsprechend einer Regel reduziert, wenn das nächste Symbol dem Metasymbol folgen kann. Dies geschieht aber nur dann, wenn dieses Metasymbol genau durch diese Regel entstanden ist. Mit anderen Worten, man betrachtet nicht mehr Mengen von Terminalsymbolen, die generell einem Metasymbol folgen können (FOLLOW-Mengen), sondern Mengen von Terminalsymbolen, die einem Metasymbol, nachdem es durch Anwendung einer bestimmten Regel entstanden ist, folgen können. Diese Mengen nennt man LOOKAHEAD-Mengen.1 Da diese LOOKAHEAD-Mengen nun pro Regel und nicht mehr pro Metasymbol erzeugt werden müssen, ist die Berechnung etwas komplizierter und wird während der Erstellung des Zustandsübergangsgraphen durchgeführt. 1 Achtung: LOOKAHEAD- und FOLLOW-Mengen werden von Studenten sehr gerne verwechselt! Version: 2.212 (18. Mai 2015) 31 KAPITEL 5. LR(1)-S YNTAXANALYSE 5.2 LR(1)-Elemente Doch zunächst müssen wir mehrere, bereits bekannte Grundkonstruktionen erweitern und zwar die LR(0)-Elemente sowie die darauf aufbauenden Operationen CLOSURE0 und GOTO0 . Ein LR(1)-Element ist ein Paar bestehend aus einem LR(0)-Element, welches in diesem Kontext „Kern“ genannt wird und einer LOOKAHEAD-Menge2 . Diese gibt pro LR(1)-Element an, welche Terminalsymbole dem Metasymbol auf der linken Seite der Regel (nach Entstehung durch Reduktion entsprechend dieser Regel) folgen können. Es ergibt sich, dass diese Menge nur dann aussagekräftig ist, wenn sich der Punkt im Kern am rechten Ende befindet, also die rechte Seite vollständig akzeptiert wurde. Ein Beispiel für ein LR(1)-Element aus Grammatik G1 ist: [Z → .S , {$}] Dieses Element kann wie folgt gelesen werden: Bei der Bearbeitung der Regel (Z → S) befinden wir uns in der Analyse noch vor dem S. Sollten wir irgendwann die gesamte rechte Seite (also hier nur das S) erkannt haben, so reduzieren wir nach der Regel, aber nur unter der Voraussetzung, dass das nächste Zeichen ein Element der LOOKAHEAD-Menge (also hier $) ist. Ein LR(1)-Zustandsübergangsgraph unterscheidet sich dadurch von einem LR(0)Zustandsübergangsgraphen, dass er statt aus LR(0)- aus LR(1)-Elementen aufgebaut ist. 5.3 Der Hüllenoperator CLOSURE1 5.3.1 Algorithmus zur Berechnung Um nun von einer Grammatik über einen LR(1)-Zustandsübergangsgraphen zur LR(1)-Syntaxanalysetabelle zu kommen, müssen noch die Hilfsoperationen CLOSURE0 und GOTO0 zu CLOSURE1 und GOTO1 erweitert werden. Dies ist notwendig, um die LOOKAHEAD-Mengen berechnen zu können. Die Menge CLOSURE1 (I) einer Menge I von LR(1)-Elementen wird wie folgt berechnet: 1. füge I zu CLOSURE1 (I) hinzu (also dasselbe wie bei CLOSURE0 ) 2. gibt es ein Element [A → α.Bβ , L] in CLOSURE1 (I) (wobei α = ε und/oder β = ε sein darf) und eine Produktion (B → γ), so füge auch [B → .γ , FIRST(βL)] zu CLOSURE1 (I) hinzu 2 In manchen Compilerbaubüchern korrekt übersetzt „Vorausschaumenge“ genannt 32 Version: 2.212 (18. Mai 2015) 5.3. D ER H ÜLLENOPERATOR CLOSURE1 Dem kritischen Leser werden an dieser Stelle gleich zwei problematische (da noch nicht eingeführte) Konstruktionen ins Auge stechen: zum einen die Konkatenation von einer Zeichenkette mit einer Menge (βL) und zum anderen die FIRSTMengenbildung von einer Menge (wir haben bisher nur die FIRST-Mengen von Zeichenketten definiert). Die Konkatenation von einer Zeichenkette β mit einer Menge von Zeichenketten L ist nicht weiter kompliziert: Das Ergebnis ist eine Menge von Zeichenketten, die alle durch die Konkatenation von β mit den einzelnen Elementen aus L entstanden sind. Wesentlich theoretischer und genauer drückt es die folgende Definition aus (β ∈ (V ∪ Σ)∗ , L ⊆ (V ∪ Σ)∗ ): βL = y ∃x∈L : y = βx Nun bleibt noch die Definition von FIRST-Mengen auf Mengen offen, diese ist aber erfreulicherweise recht intuitiv, da sie einfach die Vereinigung der FIRST-Mengen der einzelnen Elemente der gegebenen Menge darstellt: [ FIRST(L) = FIRST(α) α∈L Da insbesondere die zweite Regel der Hüllendefinition etwas komplizierter zu sein scheint, erfolgt nun die Beschreibung des gesamten Algorithmus zur Berechnung von CLOSURE1 (I) noch einmal mit Worten. Zunächst nimmt man alle Elemente aus I und fügt sie der Hülle hinzu. Nun betrachtet man alle Elemente der Hülle, die einen Punkt unmittelbar links von einem Metasymbol (B) stehen haben. Man fügt alle Produktionen der Art (B → . . .) als neue LR(1)-Elemente der Hülle hinzu, die man wie folgt bildet: Den Regelteil erhält man, indem man in der Produktion einen Punkt an den Anfang der rechten Seite setzt (also wie bei CLOSURE0 ). Die neue LOOKAHEAD-Menge ist gleich die FIRST-Menge aller Symbole, die dem Metasymbol folgen (β), verkettet mit der LOOKAHEAD-Menge des ursprünglichen LR(1)-Elementes (erst Verkettung, dann FIRST-Mengenbildung). Das heißt, wenn dem Metasymbol ein Terminalsymbol folgt, besteht die neue LOOKAHEAD-Menge aus genau diesem Terminalsymbol (Konsequenz aus der FIRST-Mengenbildung). Folgt dem Metasymbol kein weiteres Symbol, ist die neue LOOKAHEAD-Menge gleich der alten (da FIRST(εL) = FIRST(L)). Sollte hingegen dem Metasymbol eine andere Zeichenkette folgen, so ist die LOOKAHEAD-Menge gleich die FIRST-Menge von dieser Zeichenkette, wobei die alte LOOKAHEADMenge hinzugenommen wird, falls die Zeichenkette ε-ableitbar war. 5.3.2 Beispiel Betrachten wir noch einmal das LR(1)-Element [Z → .S , {$}] aus Grammatik G1 und bilden davon CLOSURE1 . Zunächst wissen wir, dass das Element selbst Teil der Hülle ist: Version: 2.212 (18. Mai 2015) 33 KAPITEL 5. LR(1)-S YNTAXANALYSE [Z → .S , {$}] Nun sehen wir, dass sich in einem Element der Hülle ([Z → .S , {$}]) ein Metasymbol (S) unmittelbar rechts von einem Punkt befindet. Diesem Metasymbol folgt kein weiteres Symbol. Daher fügen wir alle Produktionen, die ein S auf der linken Seite haben, als neue LR(1)-Elemente der Hülle hinzu. Die LOOKAHEAD-Menge dieser neuen Elemente ist gleich FIRST(ε$) = {$}. Damit sieht unsere Hülle jetzt so aus: [Z → .S , {$}] [S → .Sb , {$}] [S → .bAa , {$}] Nun schauen wir, ob in den neuen Regeln wieder ein Metasymbol unmittelbar rechts von einem Punkt steht und dem ist tatsächlich so ([S → .Sb , {$}]). Wir müssen also wieder alle von S abgeleiteten Produktionen als LR(1)-Elemente hinzunehmen, diesmal allerdings mit neuer LOOKAHEAD-Menge und zwar FIRST(b$) = {b}. Das Ergebnis ist: [Z → .S , {$}] [S → .Sb , {$}] [S → .bAa , {$}] [S → .Sb , {b}] [S → .bAa , {b}] Versucht man nun noch einmal den letzten Algorithmusschritt durchzuführen, stellt man fest, dass keine neuen Elemente mehr hinzukommen, d. h. der obige Kasten enthält CLOSURE1 ([Z → .S , {$}]). Die Notation ist jedoch noch verbesserungswürdig. Um (insbesondere bei großen LOOKAHEAD-Mengen) den Schreibaufwand zu reduzieren, ist es üblich, LR(1)-Elemente, die sich nur in den LOOKAHEAD-Mengen unterscheiden, zusammenzufassen. Wir bekommen damit als Endergebnis: [Z → .S , {$}] [S → .Sb , {b, $}] [S → .bAa , {b, $}] 5.4 Die Sprungoperation GOTO1 Im Gegensatz zur Hüllenoperation ist die Erweiterung der Sprungoperation minimal. GOTO1 ist wie folgt definiert: n o GOTO1 (I, X) = CLOSURE1 [A → αX.β , L] [A → α.Xβ , L] ∈ I Auf eine weitere Erklärung oder ein Beispiel verzichte ich hier, da GOTO1 bis auf das Kopieren der LOOKAHEAD-Menge mit GOTO0 identisch ist. 34 Version: 2.212 (18. Mai 2015) 5.5. D AS AUFSTELLEN DES Z USTANDSÜBERGANGSGRAPHEN {$} Z → .S S → .Sb {$, b} S → .bAa {$, b} 0 S Z → S. S → S.b b {$} {$, b} 1 S → Sb. {$, b} 2 b S → b.Aa A → .aSc A → .a A → .aSb {$, b} {a} {a} {a} 3 A S → bA.a {$, b} a 4 S → bAa. {$, b} 5 a A → a.Sc A → a. A → a.Sb S → .Sb S → .bAa {a} {a} {a} {b, c} {b, c} 6 S A → aS.c {a} A → aS.b {a} S → S.b {b, c} 7 c A → aSc. {a} b 8 A → aSb. {a} S → Sb. {b, c} 9 b S → b.Aa a A → .aSc A → .a A → .aSb {b, c} {a} {a} {a} 10 A S → bA.a {b, c} a 11 S → bAa. {b, c} 12 Abbildung 5.1: LR(1)-Zustandsübergangsgraph für Grammatik G1 5.5 Das Aufstellen des Zustandsübergangsgraphen Auch zum Aufstellen des Zustandsübergangsgraphen ist nicht viel neues zu berichten, da das Verfahren analog zum LR(0)-Verfahren funktioniert. Lediglich das Startelement ist nun ein LR(1)-Element und zwar (wenn S 0 das künstliche und S das eigentliche Startsymbol ist): [S 0 → .S , {$}] In Abbildung 5.1 ist der vollständige LR(1)-Zustandsübergangsgraph für Grammatik G1 angegeben. Man beachte, dass er mehr Zustände besitzt als sein LR(0)-Pendant, da mehrere Zustände auseinandergenommen wurden. Es ist bei der Konstruktion penibel darauf zu achten, dass zwei Zustände nur dann identisch sind, wenn sowohl die Regeln, als auch die LOOKAHEAD-Mengen aller enthaltenen LR(1)-Elemente identisch sind.3 3 wirklich sehr beliebte Fehlerquelle Version: 2.212 (18. Mai 2015) 35 KAPITEL 5. LR(1)-S YNTAXANALYSE 5.6 Das Erstellen der Syntaxanalysetabelle Das Aufstellen der Syntaxanalysetabelle ist eigentlich leichter als beim SLR(1)Verfahren, da alle benötigten Informationen (von der Numerierung der Grammatikregeln mal abgesehen) im Graphen enthalten sind. Die Tabelle wird bis auf die Reduktionsoperationen genauso aufgebaut, wie wir es bereits vom LR(0)/SLR(1)Verfahren her kennen. Reduktionsoperationen werden hingegen zwar auch wieder in den entsprechenden Zeilen (siehe LR(0)/SLR(1)) eingetragen, aber diesmal nur in den Spalten der Terminalsymbole, die in der LOOKAHEAD-Menge der entsprechenden Regel enthalten sind. Nehmen wir beispielsweise den Zustandsübergangsgraphen zu Grammatik G1 aus Abbildung 5.1 und schauen uns zusätzlich noch einmal die Definition von G1 (wegen der Numerierung der Produktionen) an: (1) (2) (3) (4) (5) (6) Z S S A A A → → → → → → S Sb bAa aSc a aSb Dann sieht die zugehörige Syntaxanalysetabelle wie folgt aus: 0 1 2 3 4 5 6 7 8 9 10 11 12 Aktionstabelle a b c $ s3 s2 acc r2 r2 s6 s5 r3 r3 r5 s10 s9 s8 r4 r6 r2 r2 s6 s12 r3 r3 Sprungtabelle A S Z 1 4 7 11 In der Syntaxanalysetabelle sieht man bereits den größten Nachteil des LR(1)Verfahrens: die Tabellen werden größer, als bei den anderen Verfahren (im allgemeinen sehr viel größer!). Dafür sind wir nun jedoch in der Lage mit allen relevanten Programmiersprachenkonstrukten umgehen zu können. 36 Version: 2.212 (18. Mai 2015) KAPITEL 6 LALR(1)-SYNTAXANALYSE 6.1 Idee Als allererstes sollte man, um Missverständnisse von vornherein zu vermeiden, folgendes festhalten: Der Name LALR, also lookahead-LR, ist einer der unglücklichsten, den man für dieses Verfahren wählen konnte, da sowohl LR(1) als auch SLR(1) mittels Symbolvorgriff arbeiten. Nachdem wir festgestellt haben, dass das mächtigste aller deterministischen LRVerfahren mit einem Zeichen Vorgriff, das LR(1)-Verfahren, doch eigentlich machbar ist, stellt sich natürlich die Frage, wozu man sich noch mit einem weiteren Verfahren quälen soll, das zudem weniger mächtig ist. Die Antwort wurde bereits in Ansätzen am Ende des vorherigen Abschnitts sichtbar: Die Syntaxanalysetabellen werden beim LR(1)-Verfahren zu groß. Allerdings konnte man in dem Beispiel auch sehen, dass sich einige Zustände verblüffend ähnlich (soll heißen: bis auf die LOOKAHEADMengen identisch) sind. Das LALR(1)-Verfahren funktioniert nun folgendermaßen: man generiert zunächst einen LR(1)-Zustandsübergangsgraphen. Anschließend fasst man alle Zustände zusammen, die sich ausschließlich in den LOOKAHEAD-Mengen unterscheiden (d. h. die jeweiligen Kerne sind identisch), indem man diese bei den entsprechenden LR(1)-Elementen vereinigt. Anschließend (oder besser: dabei) „biegt“ man auch die Pfeile entsprechend um. Im Ergebnis sollte man einen LALR(1)-Zustandsübergangsgraphen erhalten, der die gleichen Elemente wie der LR(0)-Zustandsübergangsgraph enthält, nur dass diese hier LOOKAHEAD-Mengen besitzen. Anmerkung: Man kann auch direkt aus einem LR(0)- einen LALR(1)-Zustandsübergangsgraphen durch bloßes Hinzufügen der LOOKAHEAD-Mengen generieren, was vor allem viele Parsergeneratoren praktizieren, der Algorithmus ist jedoch etwas komplizierter und weniger einsichtig, als der hier beschriebene. Version: 2.212 (18. Mai 2015) 37 KAPITEL 6. LALR(1)-S YNTAXANALYSE Z → .S {$} S → .Sb {$, b} S → .bAa {$, b} 0 {$} {$, b} 1 b A S → bA.a {$, b, c} 4/11 a S A → aS.c {a} A → aS.b {a} S → S.b {b, c} 7 S Z → S. S → S.b S → Sb. {$, b} 2 b S → b.Aa A → .aSc A → .a A → .aSb {$, b, c} {a} {a} {a} 3/10 S → bAa. {$, b, c} 5/12 a A → a.Sc b A → a. A → a.Sb S → .Sb S → .bAa {a} {a} {a} {b, c} {b, c} 6 c A → aSc. {a} b 8 A → aSb. {a} S → Sb. {b, c} 9 Abbildung 6.1: LALR(1)-Zustandsübergangsgraph für Grammatik G1 Die ganze Zusammenfasserei hat jedoch einen Nachteil: Es können dabei neue Konflikte entstehen und zwar r/r-Konflikte. Im allgemeinen lassen sich jedoch alle gängigen Programmiersprachenkonstrukte von LALR(1)-Parsern verarbeiten. Zum Aufbau der Syntaxanalysetabelle ist nichts weiter hinzuzufügen, da der Algorithmus mit dem aus dem LR(1)-Verfahren identisch ist. 6.2 Beispiel Zur Selbstkontrolle ist in Abbildung 6.1 der LALR(1)-Zustandsübergangsgraph zu Grammatik G1 zu sehen. Die daraus generierte LALR(1)-Syntaxanalysetabelle ist in Abbildung 6.2 dargestellt. 38 Version: 2.212 (18. Mai 2015) 6.2. B EISPIEL a 0 1 2 3/10 4/11 5/12 6 7 8 9 Aktionstabelle b c s3/10 s2 r2 $ r4 r6 Z acc r2 s6 s5/12 r5 Sprungtabelle A S 1 4/11 r3 s3/10 s9 r3 s8 r2 r2 r3 7 Abbildung 6.2: LALR(1)-Syntaxanalysetabelle für Grammatik G1 Version: 2.212 (18. Mai 2015) 39 KAPITEL 6. LALR(1)-S YNTAXANALYSE 40 Version: 2.212 (18. Mai 2015) KAPITEL 7 LR(K)-SYNTAXANALYSE FÜR K ≥ 2 7.1 Motivation Nachdem wir in den vergangenen Kapiteln verschiedene LR-Verfahren mit keinem und einem Zeichen Vorgriff betrachtet haben, wollen wir uns nun dem kanonischen LR(k)-Verfahren für beliebige k nähern. Bevor wir uns jedoch in die Tiefen dieses mächtigsten aller LR-Syntaxanalyseverfahren stürzen, sollten wir einige Vorbetrachtungen anstellen. In der verfügbaren Compilerbauliteratur sind Bücher, die sich umfassend mit LR(k)1 beschäftigen, eher selten zu finden, was vor allem daran liegt, dass LR(k)Parser mit k ≥ 2 in der Praxis eher selten benutzt werden. Dafür gibt es mehrere Gründe, und diese sollte jeder kennen, der beabsichtigt, einen LR(k)-Parser zu implementieren. Das Hauptargument gegen LR(k) liefert die deprimierende Erkenntnis, dass bei den LR(k)-Syntaxanalyseverfahren (im Gegensatz zu den LL(k)-Syntaxanalyseverfahren) nicht gilt, dass mit steigendem k die Anzahl der analysierbaren Sprachen wächst. Es gilt statt dessen folgender Satz: Zu jeder LR(k)-Sprache gibt es eine LR(1)-Grammatik. Als Konsequenz dieses Satzes ergibt sich, dass die einzelnen Mengen der LR(k)Sprachen für beliebige k ≥ 1 untereinander identisch sind (nur die Menge der LR(0)-Sprachen ist weniger mächtig). Des weiteren gibt es Algorithmen, die eine LR(k)-Grammatik in eine äquivalente LR(1)-Grammatik umformen.2 Man kann also schlussfolgern, dass sich jede mit dem 1 In diesem Abschnitt bedeutet LR(k) an manchen Stellen „LR(k) für beliebige k“ und an anderen Stellen „LR(k) für k ≥ 2“. Welches jeweils gemeint ist, sollte sich aus dem jeweiligen Kontext ergeben. 2 Genaugenommen gibt es einen Algorithmus, der eine LR(k)-Grammatik (k > 1) in eine LR(k − 1)Grammatik umwandelt. Iterativ angewendet ergibt sich der gewünschte Effekt. Version: 2.212 (18. Mai 2015) 41 KAPITEL 7. LR(k)-S YNTAXANALYSE FÜR k ≥ 2 LR(k)-Verfahren analysierbare Sprache auch mit dem bereits vorgestellten kanonischen LR(1)-Verfahren erschlagen lässt. Neben diesen theoretischen Überlegungen gibt es auch praktische Gründe, die gegen LR(k) sprechen: Der Aufwand der LR(k)-Analyse überwiegt dem der LR(1)Analyse und zwar umso mehr, je höher das k ist. Wieviel genau wird sich im Laufe des Kapitels herausstellen. Den genannten Argumenten zum Trotz gibt es aber auch Gründe, die für die allgemeine LR(k)-Analyse sprechen (ansonsten wäre dieses Kapitel auch ziemlich überflüssig) und zwar werden diese sehr schön in [Par96] beschrieben. Das Hauptargument besteht darin, dass man zwar zu jeder LR(k)-Grammatik eine LR(1)Grammatik aufstellen kann, aber zum einen deren Umfang deutlich größer als der der Ausgangsgrammatik ist und zum anderen bei ihrer Generierung ein Großteil der Struktur der Ausgangsgrammatik verlorengeht. Damit ist es zwar immer noch problemlos möglich, einen reinen Akzeptor (also einen Parser, der nur die Syntaxanalyse durchführt) zu konstruieren, aber in der Regel möchte man keine Akzeptoren, sondern Compiler implementieren. Letztere unterscheiden sich von reinen Akzeptoren dadurch, dass während der Syntaxanalyse semantische Aktionen durchgeführt werden (Syntaxbaumkonstruktion, Symboltabellenaufbau, Codegenerierung, . . . ). Diese bei der Konstruktion des Compilers sinnvoll in selbigen einzubauen ist schon bei einer „schönen“ Grammatik3 ein anspruchsvolles Problem; bei Grammatiken, die durch theoretische Umformungen entstanden sind, ist dies eine inakzeptable, da fehleranfällige und sehr aufwendige, Aufgabe. Zusammenfassend kann man also sagen, dass es mehrere (vor allem theoretische) Gründe gibt, die LR(k) überflüssig erscheinen lassen, es aber aufgrund praktischer Anforderungen trotzdem durchaus Sinn macht, sich mit der allgemeinen LR(k)Analyse zu beschäftigen. 7.2 FIRSTk und FOLLOWk Wie bereits mehrfach im Kapitel zu den Grundbegriffen (Kapitel 1) angedeutet, werden jetzt einige der dort vorgestellten Definitionen vom jeweiligen Spezialfall 1 auf den allgemeinen Fall k erweitert. Doch zunächst benötigen wir einige Hilfskonstruktionen, die uns die später folgenden Definitionen deutlich erleichtern. 7.2.1 Die Hilfsoperationen k-Präfix und k-Konkatenation Insbesondere für die formale Definition von FIRSTk -Mengen wird eine spezielle Hilfsoperation benötigt und zwar der sogenannte k-Präfix. Dieser ist für eine Zei3 Soll heißen: eine Grammatik, die die Sprache sinnvoll in semantische Einheiten gliedert. 42 Version: 2.212 (18. Mai 2015) 7.2. FIRSTk UND FOLLOWk chenkette α = a1 . . . an mit ai ∈ (Σ ∪ V ) wie folgt definiert: ( a1 . . . an α|k := a1 . . . ak (= α) , wenn n ≤ k , sonst Der k-Präfix für ein Wort α ist also eine Zeichenkette, bestehend aus den ersten k Zeichen von α bzw. α selbst, falls α aus weniger als k Symbolen besteht. Eine weitere Hilfsoperation ist die sogenannte k-Konkatenation (⊕k ). Diese beschreibt, welche Zeichenkette die ersten k Symbole einer Konkatenation von zwei gegebenen Zeichenketten bilden. Verständlicher und formaler ist die folgende Definition: α ⊕k β = (αβ)|k Beide Hilfsoperationen lassen sich nach den folgenden Definitionen auch auf Sprachen anwenden: α|k α ∈ L := α ⊕k β α ∈ L1 ∧ β ∈ L2 L|k := L1 ⊕k L2 7.2.2 FIRSTk Die FIRSTk -Mengen stellen eine Erweiterung der in Abschnitt 1.2 auf Seite 8 eingeführten FIRST1 -Mengen dar. Die FIRSTk -Menge einer gegebenen Zeichenkette X gibt an, welche Zeichenketten, bestehend aus k Symbolen, als Anfänge der von X abgeleiteten Zeichenketten auftreten können, wobei alle möglichen Ableitungen berücksichtigt werden (sollten Ableitungen von X mit weniger als k Symbolen existieren, gehören diese ebenfalls zu FIRSTk (X)). Mit Hilfe des k-Präfixes lassen sich FIRSTk -Mengen recht unproblematisch formal definieren: (V ∪ Σ)+ → P(Σ∗ )|k FIRSTk (x) := y|k x ⇒∗ y FIRSTk : 7.2.3 FOLLOWk Auch die FOLLOWk -Mengen stellen eine Erweiterung dar, und zwar die der in Abschnitt 1.3 auf Seite 10 vorgestellten FOLLOW1 -Mengen. Die FOLLOWk -Menge eines Metasymbols enthält alle aus k Terminalen bestehenden Zeichenketten, die in irgendeinem Ableitungsschritt unmittelbar rechts von diesem Metasymbol stehen können (wobei auch Zeichenketten aus weniger als k Terminalen berücksichtigt werden, wenn unmittelbar nach ihnen die Eingabe endet). Version: 2.212 (18. Mai 2015) 43 KAPITEL 7. LR(k)-S YNTAXANALYSE FÜR k ≥ 2 Dank der bereits erfolgten Definitionen von FIRSTk und k-Konkatenation lässt sich die Definition von FOLLOWk sehr elegant formulieren (α, β, γ ∈ (Σ ∪ V )∗ ): V → P (Σ∗ ) ⊕k {$} FOLLOWk (X) := γ S ⇒∗ αXβ ∧ γ ∈ FIRSTk (β) ⊕k {$} FOLLOWk : 7.3 LR(k)-Elemente, CLOSUREk und GOTOk Auch die LR(1)-Elemente müssen für das allgemeine LR(k)-Verfahren zu LR(k)Elementen erweitert werden. Jedes Konstrukt der folgenden Form heißt LR(k)Element: [X → α1 .α2 , L] (α1 , α2 ∈ (Σ ∪ V )∗ , L ⊆ P (Σ∗ ) ⊕k {$}), Ein LR(k)-Element besteht also aus einem Kern (dem LR(0)-Element) und einer Vorausschaumenge, wobei letztere nun aus Terminalketten besteht, die entweder genau k Zeichen lang ist oder aber kürzer ist und mit einem Eingabeendesymbol aufhört. Nach kurzem Überlegen wird man feststellen, dass die in Abschnitt 5.2 auf Seite 32 eingeführten LR(1)-Elemente wirklich einen Spezialfall der hier definierten LR(k)-Elemente bilden. Die Berechnung von CLOSUREk folgt analog zur Berechnung von CLOSURE1 , nur dass nun auf die allgemeinen Hilfskonstruktionen zurückgegriffen wird. Die beiden Schritte zur Berechnung von CLOSUREk (I) lauten: 1. füge I zu CLOSUREk (I) hinzu 2. gibt es ein Element [A → α.Bβ , L] in CLOSUREk (I) (wobei α = ε und/oder β = ε sein darf) und eine Produktion (B → γ), so füge auch [B → .γ , FIRSTk (βL)] zu CLOSUREk (I) hinzu Die Definitionen von GOTOk und GOTO1 unterscheiden sich ebenfalls nur marginal (es wird CLOSUREk statt CLOSURE1 benutzt): n o GOTOk (I, X) = CLOSUREk [A → αX.β , L] [A → α.Xβ , L] ∈ I 7.4 LR(k)-Zustandsübergangsgraphen und LR(k)-Syntaxanalysetabellen Die Konstruktion des LR(k)-Zustandsübergangsgraphen verläuft (wie man vermuten würde) analog zur Erstellung von LR(1)-Zustandsübergangsgraphen, nur dass man nun mit den Operationen CLOSUREk und GOTOk auf Mengen von LR(k)-Elementen arbeitet. 44 Version: 2.212 (18. Mai 2015) 7.5. LR(k)-S YNTAXANALYSE Die Erstellung der LR(k)-Syntaxanalysetabelle ist hingegen etwas komplizierter als die ihres LR(1)-Pendants. Der Unterschied zwischen den beiden Tabellen besteht darin, dass die Aktionstabelle der LR(k)-Syntaxanalysetabelle deutlich mehr Spalten enthält. Der Grund dafür liegt darin, dass in den Spaltenköpfen keine Terminalsymbole sondern alle möglichen Zeichenketten, bestehend aus jeweils k Terminalen, untergebracht sind. Dazu kommen noch alle möglichen Zeichenketten, die aus weniger als k Terminalen bestehen und mit einem $ enden. Als Anzahl s der Spalten der Aktionstabelle ergibt sich bei t Terminalen und einem LOOKAHEAD von k Symbolen (k ≥ 1): ( k+1 k t −1 X , t 6= 1 t−1 s= ti = k+1 , t=1 i=0 Die folgende Tabelle soll das Größenwachstum der Spaltenanzahl verdeutlichen. Man kann leicht nachvollziehen, warum in der Praxis versucht wird, sich auf möglichst kleine k, am besten k = 1, zu beschränken: aa a t 0 1 2 3 4 5 6 7 8 9 k aa a 1 2 3 4 5 6 7 8 9 1 2 3 4 5 6 7 8 9 10 1 3 7 13 21 31 43 57 73 91 1 4 15 40 85 156 259 400 585 820 1 5 31 121 341 781 1555 2801 4681 7381 1 6 63 364 1365 3906 9331 19608 37449 66430 1 7 127 1093 5461 19531 55987 137257 299593 597871 1 8 255 3280 21845 97656 335923 960800 2396745 5380840 1 9 511 9841 87381 488281 2015539 6725601 19173961 48427561 1 10 1023 29524 349525 2441406 12093235 47079208 153391689 435848050 Das Ausfüllen der Syntaxanalysetabelle geschieht wieder analog zum LR(1)Verfahren, nur dass man in der Aktionstabelle nun mit Zeichenketten statt mit einzelnen Terminalsymbolen hantieren muss. 7.5 LR(k)-Syntaxanalyse Hat man eine Syntaxanalysetabelle fertiggestellt, so kann man sich an die Syntaxanalyse wagen. Obwohl auch diese völlig analog zum LR(1)-Verfahren abläuft, ist sie beim LR(k)-Verfahren am Anfang etwas gewöhnungsbedürftig. Der Grund dafür liegt darin, dass man zwar in der Syntaxanalysetabelle mit Zeichenketten, in der Eingabe und im Kellerspeicher aber weiterhin mit einzelnen Terminalsymbolen arbeitet. In jedem Analyseschritt bestimmt sich die auszuführende Aktion mittels der Syntaxanalysetabelle aus dem aktuellen Zustand (oberstes Symbol im Kellerspeicher) und der Zeichenkette bestehend aus den ersten k Terminalsymbolen in der Eingabe Version: 2.212 (18. Mai 2015) 45 KAPITEL 7. LR(k)-S YNTAXANALYSE FÜR k ≥ 2 (bzw. der Eingabe selbst, falls diese kürzer als k sein sollte). Die so ermittelte Aktion wird dann genau wie beim LR(1)-Verfahren durchgeführt (dies gilt insbesondere für Schiebeoperationen: auch beim LR(k)-Verfahren wird nur ein einzelnes Terminalsymbol geschoben – die folgenden Terminale werden im folgenden Schritt wieder als LOOKAHEAD benutzt!). Der Syntaxanalyseablauf am Ende des folgenden Beispiels sollte Klarheit bringen. 7.6 Beispiel Wir besitzen nun alle notwendigen Informationen für die LR(k)-Syntaxanalyse, um aus Grammatiken LR(k)-Syntaxanalysetabellen zu generieren. Wir wollen uns im folgenden Beispiel mit der einfachsten Form der LR(k)-Syntaxanalyse für k > 1 beschäftigen: der LR(2)-Syntaxanalyse. Als Beispielgrammatik soll folgende Grammatik G2 dienen: (1) (2) (3) (4) S S X Y → → → → Y aa Xa b b Man kann sich schnell davon überzeugen, dass die Grammatik nicht vom Typ LR(1) ist, da der LR(1)-Zustandsübergangsgraph in einem Zustand einen reduce/reduce-Konflikt besitzt. Betrachten wir zunächst den durch die Hülle CLOSUREk ([Z → S , {$}]) gebildeten Startzustand: [Z → .S , [S → .Y aa , [S → .Xa , [X → .b , [Y → .b , {$} ] {$} ] {$} ] {a$}] {aa}] Der einzige Unterschied zum Startzustand des LR(1)-Zustandsübergangsgraphen besteht in den letzten beiden LR(2)-Elementen, deren LOOKAHEAD-Mengen nun aus Zeichenketten der Länge zwei bestehen (aa und a$). Betrachtet man den gesamten Zustandsübergangsgraphen (zu sehen in Abbildung 7.1), so stellt man fest, dass nur ein weiterer Zustand einer näheren Betrachtung bedarf und zwar Zustand 7: [X → b. , {a$}] [Y → b. , {aa}] An diesem Zustand kann man gut erkennen, warum die Grammatik nicht vom Typ LR(1) ist und warum LR(2) das Problem löst. 46 Version: 2.212 (18. Mai 2015) 7.6. B EISPIEL Z → .S S → .Yaa S → .Xa X → .b Y → .b S → X.a {$} {$} {$} {a$} {aa} S Z → S. {$} S → Y.aa {$} 0 Y X {$} 2 a S → Xa. 1 b {$} X → b. Y → b. 4 a S → Ya.a 3 {$} {a$} {aa} 7 5 a S → Yaa. {$} 6 Abbildung 7.1: LR(2)-Zustandsübergangsgraph für Grammatik G2 Begibt man sich nun an den Aufbau der Syntaxanalysetabelle, sollte man zu folgendem Ergebnis kommen (es ist relativ auffällig, dass die Tabelle durch die „Zweierzeichenkettenbildung“ etwas an Breite gewonnen hat): 0 1 2 3 4 5 6 7 aa ab Aktionstabelle a$ ba bb s7 s7 s3 s3 s3 b$ s7 $ Sprungtabelle S X Y 1 2 4 acc r2 s5 s6 s5 s6 s5 s6 r1 r4 r3 Um die Syntaxanalysetabelle zu prüfen (genaugenommen: um zu zeigen, dass sie zumindest für ein Beispiel funktioniert), soll im folgenden das Wort baa mit ihrer Hilfe abgeleitet werden. Der dabei entstehende Ablauf ist: $0 $0b7 $0Y 4 $0Y 4a5 $0Y 4a5a6 $0S1 Version: 2.212 (18. Mai 2015) baa$ aa$ aa$ a$ $ $ shift 7 reduce 4 (Y → b) shift 5 shift 6 reduce 1 (S → Y aa) accept 47 KAPITEL 7. LR(k)-S YNTAXANALYSE FÜR k ≥ 2 48 Version: 2.212 (18. Mai 2015) ANHANG A AUFGABEN Zur Selbstüberprüfung habe ich in diesem Anhang mehrere Übungsaufgaben zusammengestellt. So ich ausreichend Zeit finde, werde ich weitere hinzufügen. Für alle, die „Opfer“ einer von mir verfassten Klausuraufgabe wurden oder noch in diese Situation kommen können, weise ich zusätzlich bei den aus Klausuren entnommenen Grammatiken auf ihre Herkunft hin. Die Einteilung in einfache und kompliziertere Grammatiken ist natürlich rein subjektiv (schließlich erschlagen die in diesem Pamphlet beschriebenen Algorithmen alle LR-Grammatiken unabhängig von ihrem Umfang oder Aufbau). Im unmittelbar folgenden Abschnitt sind die Grammatiken enthalten, die mir selbst als eher einfach erschienen und daher z. B. als „Punktebringer“ in Klausuren verwendet wurden. Im direkt anschließenden Abschnitt folgen dann die etwas komplexeren Grammatiken, die entweder einige Spezialfälle enthalten oder einfach nur umfangreich sind. Diese Grammatiken tauchen in Klausuren in der Regel nur mit einer vereinfachten Aufgabenstellung auf (z. B. Analyse eines Wortes mit vorgegebener Syntaxanalysetabelle). Der letzte Abschnitt widmet sich schließlich den „real existierenden“ Grammatiken. Hier sind Grammatiken aufgelistet, die nicht frei erfunden sind, sondern wirklich so (oder so ähnlich) „in der freien Natur“ vorkommen. Bitte beim Vergleich der eigenen mit der abgedruckten Lösung beachten, dass die Zustände in den Zustandsübergangsgraphen (und damit die Zeilennummern in den Syntaxanalysetabellen) eventuell anders numeriert wurden. Version: 2.212 (18. Mai 2015) 49 A NHANG A. AUFGABEN A.1 Einfache Grammatiken Aufgabe 1.1 Erstellen Sie Zustandsübergangsgraph und Syntaxanalysetabelle nach dem LR(0)Verfahren. Woran erkennt man, dass es sich um eine LR(0)-Grammatik handelt? Erstellen Sie eine SLR(1)-, eine LR(1)- und eine LALR(1)-Syntaxanalysetabelle und zeigen Sie, dass die drei Tabellen den gleichen Inhalt haben. Diese Grammatik ist ein Abfallprodukt einer Klausurvorbereitung. Sie ist eine von vielen Grammatiken, die es nicht in die Klausur geschafft haben. (1) (2) (3) (4) S B B A → → → → aB bA a S Lösung 1.1 Die LR(0)-Syntaxanalysetabelle ist im folgenden abgebildet. Die Grammatik ist vom Typ LR(0), da es im Zustandsübergangsgraphen (und daher auch in der Tabelle) keine Konflikte gibt. Man beachte noch einmal in der Syntaxanalysetabelle, dass beim LR(0)-Verfahren Reduktionen immer jeweils eine ganze Zeile in der Aktionstabelle (die Spalten der Teminalsymbole) einnehmen. Mit anderen Worten: es wird immer reduziert unabhängig vom nächsten Terminalsymbol. 0 1 2 3 4 5 6 7 a s2 b s3 r3 r1 s2 r2 r4 s5 r3 r1 $ A B S 1 acc 4 r3 r1 6 r2 r4 7 r2 r4 Die SLR(1)/LALR(1)/LR(1)-Syntaxanalysetabelle sieht folgendermaßen aus: 0 1 2 3 4 5 6 7 50 a s2 b s3 s5 $ A B S 1 acc 4 r3 r1 s2 6 7 r2 r4 Version: 2.212 (18. Mai 2015) A.1. E INFACHE G RAMMATIKEN Aufgabe 1.2 Erstellen Sie Zustandsübergangsgraphen und Syntaxanalysetabelle nach dem SLR(1)-Verfahren. Diese Grammatik habe ich beim Durchsehen alter Praktikumsvorbereitungen gefunden. Sie ist zwar nicht sonderlich spektakulär, aber dennoch eine nette Übung für Zwischendurch. X → XaY X → Y Y → b (1) (2) (3) Lösung 1.2 Die Syntaxanalysetabelle fällt erwartungsgemäß sehr klein aus: a 0 1 2 3 4 5 $ b s3 s2 X 1 Y 5 acc s3 4 r3 r1 r2 r3 r1 r2 Aufgabe 1.3 Erstellen Sie einen LR(0)-Zustandsübergangsgraphen. Zeigen Sie, dass die Grammatik nicht vom Typ LR(0) ist und erstellen Sie dann eine Syntaxanalysetabelle nach dem SLR(1)-Verfahren. Bei dieser Grammatik handelt es sich um Grammatik G1 aus dem Text. Der geneigte Leser kann diese Aufgabe wahlweise als Test für das Verständnis der benötigten Algorithmen oder als Herausforderung an sein Erinnerungsvermögen betrachten. (1) (2) (3) (4) (5) S S A A A → → → → → Sb bAa aSc a aSb Lösung 1.3 Im LR(0)-Graphen sollten zwei Konflikte sichtbar sein: ein Reduce/Reduce-Konflikt und ein Shift/Reduce-Konflikt. Damit kann die Grammatik nicht vom Typ LR(0) sein. Die beiden Konflikte lassen sich jedoch bereits mit FOLLOW-Mengen (SLR(1)Verfahren) auflösen. Als Fleißarbeit kann man sich jetzt noch die Mühe machen und eine LALR(1)-Syntaxanalysetabelle aufstellen. Im Vergleich mit der SLR(1)- Version: 2.212 (18. Mai 2015) 51 A NHANG A. AUFGABEN Syntaxanalysetabelle kann man dann gut erkennen, wie die Anzahl der unnötigen Reduktionen verringert wird. Die SLR(1)-Syntaxanalysetabelle sieht folgendermaßen aus: a 0 1 2 3 4 5 6 7 8 9 b s3 s2 r1 c $ r1 acc r1 s6 s5 r4 r3 r5 A S 1 4 r2 s3 s9 r2 s8 r1 r1 r2 7 r1 Aufgabe 1.4 Erstellen Sie Zustandsübergangsgraphen und Syntaxanalysetabelle nach dem SLR(1)-Verfahren. Woran erkennt man, dass es sich um keine LR(0)-Grammatik handelt? Zeigen Sie, dass die LR(1)- und die LALR(1)-Syntaxanalysetabellen den gleichen Inhalt haben wie die SLR(1)-Syntaxanalysetabelle. Diese Grammatik ist eine Abwandlung einer Klausuraufgabe. In der Originalaufgabe lautete die vierte Regel (B → d). Außer der obigen Aufgabenstellung sollte in der Klausur auch noch das Wort bdc mit Hilfe der selbst erstellten Tabelle analysiert werden. (1) (2) (3) (4) S S A B → → → → bAc bBa d ε Lösung 1.4 Die SLR(1)/LALR(1)/LR(1)-Syntaxanalysetabelle ist im folgenden abgebildet. Dass es sich bei der Grammatik um keine LR(0)-Grammatik handelt, kann man in Zei- 52 Version: 2.212 (18. Mai 2015) A.1. E INFACHE G RAMMATIKEN le 2 der Tabelle gut sehen. Hier würde es beim Terminal d zu einem Shift/ReduceKonflikt kommen. a 0 1 2 3 4 5 6 7 b s2 c d $ A B 4 5 S 1 acc r4 s3 r3 s6 s7 r1 r2 Aufgabe 1.5 Erstellen Sie einen LR(0)-Zustandsübergangsgraphen und eine SLR(1)-Syntaxanalysetabelle. Benennen Sie alle konfliktbehafteten Zustände, in denen ein LR(0)-Parser versagen würde. Analysieren Sie mit Hilfe der von Ihnen erstellten Syntaxanalysetabelle das Wort aabab. Diese Aufgabenstellung habe ich 1:1 aus einer Klausur übernommen. Es gab für diese Aufgabe damals 14 Punkte, was bei einer Klausurgesamtpunktzahl von 100 und einer Gesamtdauer von 3 Stunden bedeutete, dass man sich für die Aufgabe ungefähr 25 Minuten (und 12 Sekunden) Zeit lassen konnte. Es gab allerdings zugegebenermaßen in der gleichen Klausur andere Aufgaben, bei denen Zeit nötiger war. . . (1) (2) (3) (4) (5) Version: 2.212 (18. Mai 2015) S S X X Y → → → → → aXb aY a S b b 53 A NHANG A. AUFGABEN Lösung 1.5 Im folgenden ist die SLR(1)-Syntaxanalysetabelle zu sehen; der konfliktbehaftete Zustand befindet sich in Zeile 8. 0 1 2 3 4 5 6 7 8 a s2 b s2 s8 s4 r1 $ X Y S 1 3 5 7 acc r1 s6 r5 r2 r3 r4 r2 Aufgabe 1.6 Erstellen Sie einen LR(0)-Zustandsübergangsgraphen. Fügen Sie in diesen nachträglich die LOOKAHEAD-Mengen ein (keine Angst: der LR(1)-Graph besitzt bei dieser Grammatik genau dieselben Zustände) und zeigen Sie, dass LOOKAHEAD-Mengen den einzigen bestehenden Konflikt beseitigen. Diese Grammatik habe ich dem Skript zur Vorlesung von Prof. Bothe entnommen. Sie ist ein sehr einfaches Beispiel für die Funktionsweise von LOOKAHEAD-Mengen. (1) (2) (3) (4) S S X Y → → → → X +X Y ∗Y a a Lösung 1.6 Die resultierende LR(1)/LALR(1)-Syntaxanalysetabelle ist im folgenden abgebildet. Im LR(0)-Zustandsübergangsgraphen gibt in einem Zustand (in meinem Fall Zustand 2) einen Konflikt zwischen (X→a) und (Y→a). Dieser wird durch die LOOKAHEAD-Mengen dahingehend gelöst, dass im Falle eines folgenden „+“ die 54 Version: 2.212 (18. Mai 2015) A.1. E INFACHE G RAMMATIKEN erste und im Falle eines folgenden „∗“ die zweite Regel angewendet wird. 0 1 2 3 4 5 6 7 8 9 10 a s2 ∗ + $ X 3 Y 6 S 1 acc r3 s4 r4 s8 5 r1 s7 s9 10 r3 r4 r2 Aufgabe 1.7 Erstellen Sie einen LR(1)-Zustandsübergangsgraphen und eine LR(1)-Syntaxanalysetabelle. Warum ist die Grammatik vom Typ LR(1)/LALR(1)? Ist sie auch vom Typ LR(0)/SLR(1)? Diese Grammatik ist wieder eine Wegwerfgrammatik einer Klausurvorbereitung. (1) (2) (3) (4) S S A B → → → → Aa bBa c c Lösung 1.7 Im folgenden ist die LR(1)-Syntaxanalysetabelle abgebildet. Man kann der Syntaxanalysetabelle leicht entnehmen, dass die Grammatik sogar LR(0) ist. a 0 1 2 3 4 5 6 7 8 Version: 2.212 (18. Mai 2015) b s5 c s4 $ A 2 B S 1 acc s3 r1 r3 s6 7 r4 s8 r2 55 A NHANG A. AUFGABEN A.2 Komplexere Grammatiken Aufgabe 2.1 Erstellen Sie Zustandsübergangsgraphen und Syntaxanalysetabelle nach dem LALR(1)-Verfahren. Diese Grammatik hat eine kleine Gemeinheit implementiert: Wenn man den LR(1)-Zustandsübergangsgraphen aufbaut, ist es an einer Stelle sehr verlockend (aber leider falsch), einen Pfeil zu einem vorherigen Zustand einzubauen – bei diesem Zustand bitte auf die LOOKAHEAD-Mengen achten! Diese Grammatik tauchte in einer Klausur als Punktebringer auf, allerdings mit einer stark vereinfachten Aufgabenstellung. In der Originalaufgabe wurde bereits die Syntaxanalysetabelle gegeben, und man sollte mit ihrer Hilfe das Wort abaaba analysieren. In dieser Aufgabe haben übrigens weit über 90% der Studenten volle Punktzahl erhalten. (1) (2) (3) (4) S B B A → → → → aBa bAb ε S Lösung 2.1 Die LALR(1)-Syntaxanalysetabelle sieht folgendermaßen aus: 0 1 2 3 4 5 6 7 8 a s2 b $ A B S 1 acc r3 s4 s5 r1 s2 3 r1 7 6 r4 s8 r2 Aufgabe 2.2 Erstellen Sie Zustandsübergangsgraphen und Syntaxanalysetabelle nach dem LR(1)und nach dem LALR(1)-Verfahren. Auch diese Grammatik wurde in einer Klausur als Punktebringer verwendet. Es wurde die gesamte Syntaxanalysetabelle vorgegeben, und man sollte das Wort abaabcaabc analysieren. Beim Aufstellen des Graphen wird einem ziemlich schnell klar, warum diese Grammatik in die Kategorie der etwas komplizierteren eingeordnet wurde. Insbesondere die Hüllenbildung kann bei einigen LOOKAHEAD-Mengen zum Alptraum werden. 56 Version: 2.212 (18. Mai 2015) A.2. KOMPLEXERE G RAMMATIKEN Wer es ganz schwierig haben möchte, kann mal versuchen, direkt den LALR(1)Graphen herzustellen (also ohne vorher den LR(1)-Graphen zu erzeugen). (1) (2) (3) (4) (5) S S X X Y → → → → → aXab Y bY a ε Sc Lösung 2.2 Da es diese Grammatik wirklich in sich hat, sind im folgenden nicht nur die LR(1)und LALR(1)-Syntaxanalysetabellen dargestellt. In den Abbildungen A.1 und A.2 sind auch noch die Zustandsübergangsgraphen zu sehen. Die LR(1)-Syntaxanalysetabelle sieht folgendermaßen aus: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Version: 2.212 (18. Mai 2015) a s4 r4 s6 b c $ s2 r5 r2 acc r5 r2 s8 X Y 3 S 1 9 11 5 s7 r1 s13 s10 r3 r5 r4 s15 r1 r2 s12 r5 s8 14 s16 r1 57 A NHANG A. AUFGABEN Z → .S S → .aXab S → .Y Y → .Sc {$} {$, c} {$, c} {$, c} S Y 0 a S → a.Xab X → .bYa X→. {$, c} {a} {a} X Z → S. Y → S.c {$} {$, c} S → Y. {$, c} S → aX.ab {$, c} c 1 {a} {a, c} {c} {c} a 5 S → a.Xab X → .bYa X→. {c} {a} {a} S → aXa.b {$, c} 4 2 6 b Y S 8 X → bY.a S → Y. {a} {c} Y → S.c {a, c} S → aX.ab {c} a b {$, c} 3 b X → b.Ya Y → .Sc S → .aXab S → .Y Y → Sc. X a 9 c 11 a 14 S → aXab. {$, c} X → bYa. {a} Y → Sc. {a, c} S → aXa.b {c} 13 7 10 12 15 b S → aXab. {c} 16 Abbildung A.1: LR(1)-Zustandsübergangsgraph für Aufgabe 2.2 In der LALR(1)-Syntaxanalysetabelle sind fünf Zustände weniger enthalten (siehe dazu auch den Zustandsübergangsgraphen): 0 1 2 3 4 5 6 7 8 9 10 11 58 a s4 b r5 r4 s6 c $ s2 r5 r2 acc r5 r2 s8 X Y 3 S 1 9 11 5 s7 r1 s4 s10 r3 r1 r2 s2 Version: 2.212 (18. Mai 2015) A.2. KOMPLEXERE G RAMMATIKEN Z → .S S → .aXab S → .Y Y → .Sc {$} {$, c} {$, c} {$, c} S Y 0 a S → a.Xab X → .bYa X→. {$, c} {a} {a} X Z → S. Y → S.c {$} {$, c} S → Y. {$, c} S → aX.ab {$, c} c 1 X → b.Ya Y → .Sc S → .aXab S → .Y {a} {a, c} {c} {c} {$, c, a} S → aXa.b {$, c} a 5 4 6 b Y S 8 2 3 b a Y → Sc. X → bY.a S → Y. {a} {c} Y → S.c {a, c} a 9 S → aXab. {$, c} X → bYa. {a} 7 10 c 11 Abbildung A.2: LALR(1)-Zustandsübergangsgraph für Aufgabe 2.2 Aufgabe 2.3 Erstellen Sie einen LR(1)-Zustandsübergangsgraphen und daraus eine LR(1)-Syntaxanalysetabelle. Zeigen Sie, dass die Grammatik zwar vom Typ LR(1) aber nicht vom Typ LALR(1) ist. Diese Grammatik habe ich vom Zustandsübergangsgraphen rückwärts entwickelt, da ich unbedingt einmal Studenten mit einer Nicht-LALR(1)-Aber-LR(1)-Grammatik konfrontieren wollte. Die Aufgabe tauchte dann auch in einer Klausur auf, fand dort allerdings weniger Anklang. . . (1) (2) (3) (4) (5) (6) S S S S X Y → → → → → → aXa aY b bXb bY a c c Lösung 2.3 Die LR(1)-Syntaxanalysetabelle ist im folgenden abgebildet. Im konstruierten Zustandsübergangsgraphen sollten einem genau zwei Zustände ins Auge springen, die man beim LALR(1)-Verfahren zusammenlegen würde. Bei dieser Zusammenlegung entsteht ein Reduce/Reduce-Konflikt, so dass diese Grammatik wirklich vom Typ Version: 2.212 (18. Mai 2015) 59 A NHANG A. AUFGABEN LR(1), aber nicht vom Typ LALR(1) ist. 0 1 2 3 4 5 6 7 8 9 10 11 12 13 a s2 b s3 $ c X Y 4 9 6 11 S 1 acc s8 s13 s5 r1 s7 r2 r5 r6 s10 r3 s12 r4 r6 r5 A.3 Real existierende Grammatiken Aufgabe 3.1 Erstellen Sie einen LR(1)-Zustandsübergangsgraphen und daraus eine LR(1)-Syntaxanalysetabelle. Diese Grammatik ist die vermutlich am häufigsten in der Compilerbauliteratur vorkommende. Es handelt sich um eine Ausdrucksgrammatik (Expression, Term, Faktor). Das besondere an dieser Grammatik ist, dass die damit erstellten Ableitungsbäume bereits die Vorrangsregelung (Punkt- vor Strichrechnung) automatisch enthalten. Die Grammatik ist in vielen realen Programmiersprachengrammatiken als Teilmenge enthalten. (1) (2) (3) (4) (5) (6) E E T T F F → → → → → → E+T T T ∗F F id (E) Lösung 3.1 Da diese Grammatik wirklich sehr fehleranfällig ist, ist in Abbildung A.3 der LR(1)Zustandsübergangsgraph zu sehen (der übrigens drei Leute zwei Tage lang beschäf- 60 Version: 2.212 (18. Mai 2015) A.3. R EAL EXISTIERENDE G RAMMATIKEN ( S → .E {$} E → .E+T {$,+} E → .T {$,+} T → .T∗F {$,+,∗} T → .F {$,+,∗} F → .(E) {$,+,∗} F → .id {$,+,∗} T {$,+} E → T. T → T.∗F {$,+,∗} 8 F T → F. E + 3 ( id E F → (.E) {$,+,∗} E → .E+T {+,)} E → .T {+,)} T → .T∗F {+,),∗} T → .F {+,),∗} F → .(E) {+,),∗} F → .id {+,),∗} 2 7 11 T ( T {+,)} E → T. T → T.∗F {+,),∗} ∗ 13 F 12 ( T F → (.E) {+,),∗} E → .E+T {+,)} E → .T {+,)} T → .T∗F {+,),∗} T → .F {+,),∗} F → .(E) {+,),∗} {+,),∗} F → .id ( F 6 + ( E → E+.T {+,)} T → .T∗F {+,),∗} T → .F {+,),∗} F → .(E) {+,),∗} F → .id {+,),∗} 16 F → (E). {$,+,∗} 9 {$,+,∗} 1 E → E+T. {$,+} T → T.∗F {$,+,∗} ∗ T → T∗.F {+,),∗} F → .(E) {+,),∗} {+,),∗} F → .id id T → T∗F. {+,),∗} 19 id E + F → (E.) {+,),∗} E → E.+T {+,)} 20 ) F → (E). {+,),∗} 21 T E → E+T. {+,)} T → T.∗F {+,),∗} ∗ 17 id F → id. 18 F 14 ) F → id. id 10 {+,),∗} F → (E.) {$,+,∗} E → E.+T {+,)} id T → T∗F. {$,+,∗} 5 F E → E+.T {$,+} T → .T∗F {$,+,∗} T → .F {$,+,∗} F → .(E) {$,+,∗} F → .id {$,+,∗} ( F {$,+,∗} F T → F. T → T∗.F {$,+,∗} F → .(E) {$,+,∗} F → .id {$,+,∗} 4 id 0 {$} S → E. E → E.+T {$,+} ∗ {+,),∗} 15 Abbildung A.3: LR(1)-Zustandsübergangsgraph für Aufgabe 3.1 Version: 2.212 (18. Mai 2015) 61 A NHANG A. AUFGABEN tigt hat). Die daraus resultierende Syntaxanalysetabelle (deren Erstellung nicht weniger aufwendig war) sieht folgendermaßen aus: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 62 + ∗ r5 r5 ( s2 ) s15 s8 r4 T 4 F 5 6 13 12 10 5 11 13 12 17 12 S s9 r6 r1 r3 r4 r2 r6 s8 r3 r4 s18 r5 r5 s1 s1 r6 r1 r3 r4 r2 s14 s15 s18 s15 r1 s14 r3 r6 20 r5 s14 r3 s16 r6 E 3 acc r2 r4 s2 s2 r1 $ r5 s14 s7 r2 r4 s16 id s1 s15 19 r3 s21 r6 Version: 2.212 (18. Mai 2015) ANHANG B LITERATURVERZEICHNIS [ASU99] Alfred V. Aho, Ravi Sethi, Jeffrey D. Ullmann: Compilerbau Teil 1 und 2, 2., durchgesehene Auflage, Oldenbourg Wissenschaftsverlag, München 1999 Aufgrund des Einbandes als „Drachenbuch“ bekannt. Die Bibel für jeden Compilerbauer – enthält prinzipiell alles, was man wissen muss. Leider ist das Buch als Einstieg in die Materie nur bedingt zu empfehlen, da zum einen die Reihenfolge der Themen an mehreren Stellen pädagogisch eher suboptimal ist und zum anderen viele Algorithmen und Sachverhalte unnötig kompliziert dargestellt werden. [Bot97] Klaus Bothe: Skript zur Vorlesung „Praktische Informatik III – Compilerbau“ WS1997/98, notiert von Ralf Heese, 1997 Durchaus lesenswertes Skript zur Compilerbauvorlesung von Prof. Bothe, das allerdings vermutlich nur von Hörern der Veranstaltung wirklich nachvollziehbar ist. [Hol90] Allen I. Holub: Compiler Design in C, Prentice-Hall, New Jersey, 1990 Sehr gutes Compilerbaubuch, das leider nicht mehr im Handel erhältlich ist. Besonders hervorzuheben sind vor allem zwei Tatsachen: Holub verwendet auch durchaus große Beispiele (Zustandsübergangsgraphen über zwei Buchseiten, . . . ) und er geht sehr detailliert auf Spezialfälle von Algorithmen ein. [Par96] Terence Parr and Russel Quong: LL and LR Translator Need k, SIGPLAN Notices Volume 31 #2, Februar 1996 online unter: http://www.antlr.org/article/needlook.html Netter Artikel zur Fragestellung, ob bzw. warum LR(k) für k ≥ 2 sinnvoll ist. Der erste Autor ist übrigens der Hauptprogrammierer von ANTLR und Version: 2.212 (18. Mai 2015) 63 B L ITERATURVERZEICHNIS gleichzeitig ein Beweis dafür, dass Compilerbauer Humor haben (man betrachte seine Homepage). [Sch97] Uwe Schöning: Theoretische Informatik – kurzgefaßt, Spektrum Akademischer Verlag, Heidelberg; Berlin 1997 Das prägnanteste (mir bekannte) Buch zur theoretischen Informatik. Eignet sich sehr gut zum Selbststudium und enthält wirklich gute Beispiele für jeden behandelten Aspekt. [Spe03] Michael Sperber, Peter Thiemann: Systematic Compiler Construction; 2003 Ein Skript, das mir sehr gut gefallen hat, da es zum einen sehr ausführlich und zum anderen verständlich ist. [Wil97] Reinhard Wilhelm, Dieter Maurer: Übersetzerbau, 2. überarbeitete und erweiterte Auf lage; Springer-Verlag Berlin, Heidelberg 2003 Das mit Abstand theoretischste Werk, das ich im Bereich Compilerbau kenne. Als Einstieg in die Materie meiner Meinung nach eher ungeeignet, aber ein sehr gutes Nachschlagewerk für alle, die es ganz genau wissen wollen. 64 Version: 2.212 (18. Mai 2015) DANKSAGUNG Hiermit möchte ich mich noch einmal bei allen Personen bedanken, die mir bei der Verbesserung dieses Schriftstückes geholfen haben. Da wären zum einen die eifrigen Probeleser/Korrektoren/Ideengeber: • Prof. Dr. Klaus Bothe • Dr. Klaus Ahrens • Prof. Dr. Lothar Schmitz (UniBw München) • Dr. Stefan Kirchner • Dr. Constanze Kurz • Dr. Julia Böttcher • Dr. Konrad Voigt • Glenn Schütze • Dr. Martin Stigge und zum anderen die Fehlerfinder (in chronologischer Reihenfolge): • Jörg Peters (Uni Karlsruhe) • Aylin Aydo˘ gan • Ay¸segül Gündo˘ gan • Laura Obretin • Ralf Heese Version: 2.212 (18. Mai 2015) 65 • Elena Filatova • Martin Sommerfeld • El˙zbieta Jasi´ nska • Frank Butzek • Matthias Kubisch • Kristian Klaus • Kornelius Kalnbach • Lukas Moll • Lars Sadau • Alexander Borisov • Kai Stierand (Uni Hamburg) • Richard Müller • Robert Müller • Moritz Kaltofen • Peter Hinkel (FSU Jena) • Andreas Rentschler (Uni Karlsruhe) • Mario Lehmann • Wolfgang Nádasi-Donner • Frederic Beister • Jochen Taeschner • sowie einer, der nicht namentlich erwähnt werden wollte Anmerkung: Jeder, der einen Fehler in diesem Skript findet, wird in kommenden Versionen ebenfalls auf dieser Seite verewigt. 66 Version: 2.212 (18. Mai 2015)