Programmiersprachen und Übersetzer Sommersemester 2010 10. Mai 2010 Das Parsing-Problem Gegeben ist eine kontext-freie Grammatik G = (N, T , P, S) und ein Wort w ∈ T ∗ . Frage: Ist w ∈ L(G )? In der Theorie auch als Wortproblem (für kontextfreie Sprachen) bekannt. � Das Problem ist entscheidbar, d.h. es gibt einen Algorithmus, der diese Frage beantwortet. � Der Algorithmus läuft in einer Zeit O(n3 ) mit |w | = n. Deterministisches Top-Down-Parsen Forderungen an das deterministische Top-Down-Parsing: 1. Man liest das vorgegebene Wort w nur einmal von links nach rechts. 2. Parallel dazu wird der Ableitungsbaum von oben nach unten und von links nach rechts erzeugt. Dies bedeutet, dass eine Linksableitung von w rekonstruiert wird. 3. Ist man an einem Knoten des Baumes angekommen, der mit einem nichterminalen Symbol A markiert ist und der als nächster gemäß obiger Regel 2) verarbeitet werden muss, so kann man mit Hilfe des nächsten zu lesenden Symbols von w (Lookahead Symbol) die anzuwendende A-Produktion eindeutig bestimmen. Anfangssituation beim Top-Down Parsing Startsymbol S ✟ der Grammatik ✟ ✙ ✟ Lesefenster, zeigt zu Anfang das erste Symbol in der Eingabe � (Lookahead Symbol) � � ✠ abzuleitendes Wort w Situation im Top-Down Parsing S � � � � � � � �❅ � ❅ ✡✡ ✡ ✡ ✡ ✣ ✡ ✡ bereits abgeleitetes Anfangsstück von w nächstes nichtterminales Symbol, auf das eine Produktion anzuwenden ist ✑ ✑ ❅✑ ✑ ✑ ✰ ❅ ❅ A ✑ ✑ ✰ ✑ Rest von w ❅ ■ ❅ Lesefenster Bemerkung 1. Nicht alle Grammatiken erlauben ein Parsen in dieser Art. 2. Es gibt kontextfreie Sprachen, für die es keine kontextfreie Grammatik gibt, die ein Parsen in der oben angegebenen Art ermöglicht. Beispiel Für die Sprache {an 0b n | n ≥ 0} ∪ {an 1b 2n | n ≥ 0} gibt es keine Grammatik, die ein deterministisches Top-Down Parsing in dieser Art ermöglicht. Eigenschaften von Grammatiken, die deterministisches Top-Down Parsing verhindern 1. Eine Situation, in der eine Grammatik die oben gestellten Forderungen nicht erfüllen kann, ist gegeben, wenn es verschiedene Produktionen mit der gleichen linken Seite gibt, deren rechte Seiten ein gleiches Anfangsstück (Präfix) haben. 2. Deterministisches Top-Down-Parsen ist offensichtlich auch nicht möglich, falls sogenannte linksrekursive Produktionen, d. h. Produktionen der Form A → Aα, α ∈ (N ∪ T )+ , auftreten. Entfernen gemeinsamer Präfixe Seien A → αβ1 | . . . | αβr | γ1 | . . . | γs alle A-Produktionen der Grammatik, wobei α �= ε und kein γi das Präfix α hat. Ersetze diese Produktionen durch A → αA� | γ1 | . . . | γs � A und → β1 | . . . | βr , wobei A� ein neues nichtterminales Symbol ist. Man kann leicht einsehen, dass die so umgeformte Grammatik die gleiche Sprache erzeugt. Entfernen linksrekursiver Produktionen Seien A → Aα1 | . . . | Aαm | β1 | . . . | βn alle A-Produktionen, wobei kein βi mit A beginnt und alle αi �= ε sind. Dann ersetze man diese Produktionen durch: A → β1 A� | . . . | βn A� � A � und � → α1 A | . . . | αm A | ε , wobei A� ein neues nichtterminales Symbol ist. Bemerkung Diese Umformungen müssen nicht unbedingt zu einer Grammatik führen, die deterministisches Parsing erlaubt. Definition der Funktionen First und Follow Definition Sei α ∈ (N ∪ T )∗ . Dann ist ∗ ∗ First(α) = {a | α =⇒ aβ, a ∈ T , β ∈ (N ∪ T )∗ } ∪ {ε | α =⇒ ε}. Bemerkung First(α) ist also die Menge derjenigen terminalen Zeichen, die bei Linksableitungen von α als erste auftreten. Kann man aus α das leere Wort ε herleiten, so ist ε ebenfalls in First(α). Definition Sei A ∈ N. Dann ist ∗ Follow(A) = {a | S =⇒ αAaβ, a ∈ T , α, β ∈ (N ∪ T )∗ } ∗ ∪ {$ | S =⇒ αA, α ∈ (N ∪ T )∗ }, wobei $ ein neues Symbol ist, das das Ende der Eingabe markiert. Bemerkung Follow(A) ist also die Menge derjenigen terminalen Zeichen, die bei einer Ableitung vom Startsymbol S aus direkt auf A folgen können. Folgt A kein Zeichen, d. h. A steht ganz rechts, so ist die Endmarkierung $ in Follow(A). LL(1) Grammatik Definition Eine kontextfreie Grammatik G = (N, T , P, S) heißt LL(1)-Grammatik (Lesen der Eingabe von Links nach rechts, Erzeugen einer Linksableitung und 1 (ein Zeichen) Lookahead), falls für alle A ∈ N gilt: Seien A → α1 | . . . | αn alle A-Produktionen in P. 1. First(α1 ), . . . , First(αn ) sind paarweise disjunkt, d. h. First(αi ) ∩ First(αj ) = ∅ falls i �= j. 2. Ist ε ∈ First(αj ), dann ist Follow(A) ∩ First(αi ) = ∅ für 1 ≤ i ≤ n, i �= j. Berechnung der First- und Follow-Funktion: Als erstes bestimmt man, welche nichtterminalen Symbole sich auf das leere Wort ableiten lassen, d.h. wir bestimmen die Menge ∗ Mε = {A | A ∈ N und A =⇒ ε}. Gegeben sei eine kontextfreie Grammatik G = (N, T , P, S). Algorithmus zur Berechnung von Mε : 1. Bestimme M0 = {A | A → ε ist in P} 2. Berechne Mi+1 = Mi ∪ {A | A → α ist in P und α ∈ Mi∗ } 3. ist Mi+1 = Mi , dann setze Mε = Mi . Danach ist es möglich, die First-Funktion zunächst einmal für alle nichtterminalen Symbole der Grammatik zu berechnen. Algorithmus zur Berechnung der First-Funktion: Berechnung von First(A) für alle A ∈ N. Man konstruiert einen Graphen Γ folgendermaßen 1. Jedes Symbol aus N ∪ T wird durch einen Knoten dargestellt. 2. Für jede Produktion A → X1 . . . Xn mit n ≥ 1, fügt man eine Kante von A nach Xi hinzu, falls X1 , . . . , Xi−1 ∈ Mε . 3. Setze First(A) = {a | a ∈ T und es gibt einen Weg von A nach a in Γ} ∪ {ε |falls A ∈ Mε }. Damit ist aber die First-Funktion auch für alle α ∈ (N ∪ T )∗ bestimmt. Es gilt 1. Ist α = ε, dann ist First(α) = {ε}. 2. Ist α = aβ mit a ∈ T , dann ist First(α) = {a}. 3. Ist α = Aβ � mit A ∈ N, dann ist First(A) falls A ∈ / Mε First(α) = (First(A) − {ε}) ∪ First(β) falls A ∈ Mε Beispiel Betrachte die Grammatik G = (N, T , P, S) mit N = {S, A, B, C }, T = {d, e, f , g , h, p, q} und den Produktionen S → ABCd, A → e | f | ε, B → g | h | ε und C → p | q. Beispiel Betrachte die Grammatik G = (N, T , P, S) mit N = {S, A, B, C }, T = {d, e, f , g , h, p, q} und den Produktionen S → ABCd, A → e | f | ε, B → g | h | ε und C → p | q. Es ist Mε = {A, B} und der zur Bestimmung der First-Funktion benötigte Graph Γ ist S d e A f B g C h p q Also gilt: First(S) = {e, f , g , h, p, q}, First(A) = {e, f , ε}, First(B) = {g , h, ε} und First(C ) = {p, q}. Beispiel Als weiteres Beispiel betrachten wir eine Grammatik mit nichtterminalen Symbolen {S, A, B, C , D} und terminalen Symbolen {a, b, c, d, g }. Die Produktionen sind S → AB, B → aAB | ε, A → CD, D → bCD | ε und C → cSd | g . S sei das Startsymbol. Es ist leicht einzusehen, dass Mε = {B, D} gilt. Der Graph zur Bestimmung der First-Funktion ist S B a A b c D d C g Es gilt also First(S) = First(A) = First(C ) = {c, g }, First(B) = {a, ε}, und First(D) = {b, ε}. Algorithmus zur Berechnung der Follow-Funktion Es wird wieder ein Graph Γ aufgebaut, der in diesem Fall für jedes Symbol in N ∪ T ∪ {$} einen Knoten hat. 1. Füge eine Kante von S nach $ hinzu. 2. Für jede Produktion A → αBβ mit A, B ∈ N, α, β ∈ (N ∪ T )∗ , füge eine Kante von B nach jedem a ∈ First(β), a ∈ T hinzu. Ist ε ∈ First(β) und A �= B, dann füge eine Kante von B nach A hinzu. 3. Setze Follow(A) = {a | a ∈ T ∪ {$} und es gibt einen Weg von A nach a in Γ}. Beispiel Der Graph zur Bestimmung der Follow-Funktion ist S $ B a A b D c C d g Follow(S) = Follow(B) = {d, $}, Follow(A) = Follow(D) = {a, d, $}, und Follow(C ) = {a, b, d, $}. Ein tabellengesteuerter Top-Down-Parser Idee: Der noch abzuleitenden Teils des Ableitungsbaumes wird in einem Stack gespeichert: S � ❅ � ❅ A b C ✓ ❙ ✓ ❙ d D G ✁ ❆ ✁ ❆ f E E ✛ d f ? Eingabe $ ... ✻Lesefenster G b C $ Stack Steuerwerk Aufbau eines tabellengesteuerten Top-Down Parsers a X .. . $ Eingabewort w $ ✛ ✲ endliches Steuerwerk ✛ ❄ Ausgabe, z. B. Liste der in einer Linksableitung angewendeten Produktionen ParsingTabelle M Die Parsing-Tabelle � Die Zeilen der Tabelle sind mit den nichtterminalen Symbolen der Grammatik markiert � Die Spalten sind mit den terminalen Symbolen und dem End-Symbol $ markiert. � Jeder Eintrag M(A, x) in M (A ∈ N, x ∈ T ∪ {$}) enthält entweder eine Produktion der Grammatik oder das Fehlersymbol #. Konstruktion der Parsing-Tabelle M: Für jede Produktion A → α in P führe die folgenden Schritte aus: 1. Für jedes a ∈ T in First(α) setze M(A, a) = A → α. 2. Ist ε in First(α), dann setze für jedes b mit b ∈ T ∪ {$} in Follow(A) M(A, b) = A → α. Die LL(1)-Bedingung garantiert, dass jeder Tabellenplatz höchstens einmal besetzt wird! Jeder dann noch undefinierte Eintrag in M wird auf # gesetzt. Beispiel Verwendet man die Grammatik mit den Produktionen 1: S → (S)R, 2: S → aR, 3: R → +S, 4: R → ∗S und 5: R → ε, so erhält man die folgende Parsingtabelle M: M a ( ) + S S → aR S → (S)R # # R # # R → ε R → +S ∗ # R → ∗S bzw. die vereinfachte Form: M S R a 2 # ( 1 # ) # 5 + # 3 ∗ # 4 $ # 5 $ # R→ε Arbeitsweise eines tabellengesteuerten Top-Down Parsers Initiale Situation � Auf dem Eingabeband steht das zu parsende Wort w gefolgt von der Endmarkierung $. � Das Lesefenster steht auf dem ersten Zeichen von w . � Der Stack enthält als unterstes Symbol $ und darüber das Startsymbol S der Grammatik. Arbeitsschritt Sei X das oberste Stacksymbol und a das Zeichen im Lesefenster der Eingabe. (i) Ist X = a = $, dann beende das Parsen. Das vorgelegte Wort ist aus der von der Grammatik erzeugten Sprache L(G ). (ii) Ist X ∈ T und gilt X = a, dann lösche X vom Stack und setze das Lesefenster ein Zeichen weiter. Ist X ∈ T und X �= a, dann beende das Parsen. Es gilt w �∈ L(G ). (iii) Ist X ∈ N, dann betrachte M(X , a). Enthält M(X , a) die Produktion X → A1 . . . Ar , dann lösche X und schreibe stattdessen Ar , . . . , A1 (in dieser Reihenfolge!) auf den Stack und gib die Produktion aus. Enthält M(X , a) dagegen das Fehlersymbol #, dann beende das Parsen. In diesem Fall gilt w �∈ L(G ). Konfigurationen Um die Arbeitsweise eines tabellengesteuerten Top-Down-Parsers etwas kompakter darzustellen, wird der momentane Stackinhalt und die restliche Eingabe ab dem Lookahead-Symbol als ein Paar von Wörtern (X . . . $, a . . . $) notiert, wobei X . . . $ den Stackinhalt mit dem obersten Symbol X und a . . . $ den Teil des Eingabewortes ab dem Lookahead-Symbol a darstellt. � Ein derartiges Paar nennt man eine Konfiguration des Parsers. � Jeder Schritt des Parser erzeugt so eine neue Konfiguration. Ein Schritt der Form (ii) soll durch (aY . . . $, ab . . . $) �−→ (Y . . . $, b . . . $) und ein Schritt der Form (iii) durch (i) (XY . . . $, a . . . $) �−→ (A1 . . . Ar Y . . . $, a . . . $) bezeichnet werden, wobei i die Nummer der angewendeten Produktion angibt.