6.5 LR-Parser per Hand entwerfen

Betrachten wir die kontextfreie Grammatik G für arithmetische Ausdrücke mit den Variablen x,y,z und strenger Klammerung über dem Alphabet Σ={x,y,z,(,),+,}:

Sx | y | zS(S+S)S(SS)

Sie kann also (x+(yz)) ableiten aber eben nicht (x+y+z). Wie können wir nun einen Parser für G schreiben? Also einen Algorithmus, der ein Wort wΣ nimmt und einen Ableitungsbaum konstruiert (falls wL(G))? Wenn wir uns an das LL-Paradigma halten und eine Linksableitung bauen wollen, dann stoßen wir schon ganz am Anfang auf ein Problem: wenn zum Beispiel

w=((((

dann wissen wir nicht, ob wir als ersten Schritt S(S+S) oder S(SS) tätigen sollen. Das geht auch nicht, wenn wir k Zeichen vorauslesen dürfen, weil der (((-Präfix ja länger als k sein kann. Nein, wir müssen anders vorgehen. Wir könnten beispielsweise die Grammatik ändern:

Sx | y | zS(SOS)O+ | 

Das geht aber nicht immer:

Beispiel 6.5.1 Betrachten wir die recht einfache Sprache

L2:={am+kbmc | m1,k0} ,

also beliebig viele a's, gefolgt von gleich vielen oder weniger b's (aber mindestens einem), abgeschlossen mit einem c . Eine Grammatik ist schnell geschrieben:

(1)SaS(2)SXc(3)XaXb(4)Xab

Wenn wir jetzt die ersten k Zeichen lesen: aaaa, dann gibt es keinen Weg, zu entscheiden, ob danach gleich viele oder weniger b's folgen werden, ob wir also SsS oder SXc anwenden sollen. Das lässt sich auch nicht durch Umschreiben der Grammatik lösen. Wir müssen lesen, bis wir ein b sehen.

Das LR-Paradigma

Wir brauchen einen Paradigmenwechsel. Das LL-Paradigma war ja, mit S zu starten und, geleitet von den nächsten k Zeichen, zu entscheiden, welche Ableitungsregel als nächstes anzuwenden ist. Hierbei haben wir immer versucht, für das am weitesten links stehende Nichtterminal eine Regel zu finden. Wir beschreiben nun ein ganz anderes Vorgehen: wir lesen das Eingabewort v von links nach rechts, unterhalten also einen Stack, auf dem ein Präfix γ von v liegt, bis wir am rechten Ende die rechte Seite einer Produktionsregel erkennen - bis also γ=αβ und es eine Produktion Xβ gibt. Dann ersetzen wir αβ durch αX. Unser Stack enthält nun keine Präfix von v mehr, sondern eine Wortform γ. Zusammen mit dem ungelesenen Teil w des Eingabewortes ergibt das eine Wortform γw. Solange es eine Rechtsableitung Sγwv gibt, sind wir auf dem richtigen Weg. Am Besten betrachten wir ein Beispiel für L2={am+kbmc | m1,k0}. Die Farbe grau bedeutet hier, dass wir das Eingabezeichen noch nicht gelesen haben.

Betrachten wir noch ein Beispiel, nun für die etwas nützlichere Grammatik der streng geklammerten arithmetischen Ausdrücke.

Wenn wir uns nun die Ableitung ansehen, die wir gefunden haben:

S(S+S)(S+(S*S))(S+(S*z))(S+(y*z))(x+(y*z))

dann sehen wir, dass es sich um eine Rechtsableitung handelt. Daher der Name LR-Parsing: wir beginnen links (daher das L) und suchen eine Rechtsableitung (daher das R), allerdings in umgekehrter Reihenfolge. Statt von S ausgehend w abzuleiten, also Sw, versuchen wir w zu S zu reduzieren, also wS. Allerdings ist das nicht immer so einfach: manchmal ist nicht auf den ersten Blick erkennbar, welche Produktionsregel wir (rückwärts) anwenden sollen. Hier ein etwas konstruiertes Beispiel:

SXYzXaXa | bXb | cYYa | Yb | a | b

Die erzeugte Sprache ist

L(G)={vcvRwz | v,w{a,b}}

Betrachten wir das Eingabewort acaba. Wir schreiben nun immer den bis jetzt gelesenen / geparsten Teil des Wortes, gefolgt von dem ungelesen Teil in grau und dahinter in Klammern , was wir als nächstes tun, also das nächste Zeichen lesen oder eine Regel anwenden.

(lesen)acabaz(lesen)acabaz(reduzieren per Xc)acabaz(lesen)aXabaz(reduzieren per XaXa)aXabaz(lesen)Xbaz(reduzieren per Yb)Xbbaz(lesen)XYaz(reduzieren per YYa)XYaz(lesen)XYz(reduzieren per SXYz)XYz(fertig)S

Es stellen sich einige Fragen: woher wissen wir zum Beispiel bei XYaz, dass wir per YYa reduzieren müssen und nicht per Ya? Wir könnten ja auch auf XYaXYY reduzieren. Oder in Schritt 2, bei acabaz. Da könnten wir ja gleich aY reduzieren.

Beobachtung. 6.5.2 Die Reduktion XYaXYY kann nicht richtig sein, weil XYY nie als Präfix in einer Rechtsableitung vorkommen kann. Genauer gesagt: es gibt kein wΣ, so dass

SXYYwXYaw

eine Rechtsableitung ist.

Wenn wir Glück haben, gibt es immer höchstens eine Reduktionsregel αβwαXw, so dass SαXwαβw in einer Rechtsableitung vorkommen kann. Das hängt von der Grammatik ab. Aber selbst dann brauchen wir einen Algorithmus, der uns sagen kann, ob XYaXYY ein korrekter Reduktionsschritt ist. Dies scheint komplexer, als w?L zu entscheiden, ist aber einfacher!

Ein zweites Problem ist, dass wir eben manchmal kein Glück haben und es mehrere plausible Reduktionsschritte geben kann. Ein Beispiel wäre die obere Grammatik, leicht abgewandelt:

(beachten Sie: oben hatten wir SXYz)SXYXaXa | bXb | cYYa | Yb | a | b

Wenn wir jetzt als einfaches Beispiel cab ableiten wollen:

(lesen)cab(reduzieren per Xc)cab(lesen)Xab(reduzieren per Ya)Xaab(reduzieren per Ya)XYb

Jetzt liegt die Wortform XY auf unserem Stack und wir haben zwei Möglichkeiten: wir könnten reduzieren, also XYS, oder das nächste Zeichen lesen. Ersteres wäre inkorrekt:

XYbSb ,

wobei Sb eben keine Wortform ist, die in einer Rechtsableitung vorkommen könnte. Das wissen wir aber erst, wenn wir b gelesen haben. Wäre das Eingabewort nämlich nur ca, dann wäre XYS tatsächlich die korrekte Reduktion. In der ersten Grammatik hatten wir als "Work-around" ein z am Ende angehängt, um das Wortende zu erkennen. Im Allgemeinen ist es aber leichter, dem Parser zu erlauben, das nächste Zeichen zu lesen.

All diese Gedanken theoretisch rigoros zu formulieren ist einigermaßen herausvordernd. Daher werden wir erst einmal für eine Grammatik arithmetischer Ausdrücke einen Parser in Java implementieren.