Wie gelangen wir vom Parser zum Compiler? Wie fügen wir zum Parser geeignete Übersetzungsaktionen hinzu, um das gewünschte Übersetzungsergebnis zu erzielen?
Am Beispiel der Auswertung von Additions-/Subtraktions-Ausdrücken wird im Folgenden eine formale Methode vorgestellt, um systematisch eine Grammatik-Produktion in eine Compiler-Funktion zu überführen.
Wir nehmen den Parser zur Auswertung von Additions-/Subtraktions-Ausdrücken als Grundlage, um nunmehr die eingegebenen Ausdrücke nicht nur zu parsen, sondern syntaxgesteuert in ihren Wert zu übersetzen. Zum Beispiel liefert der Ausdruck "9-3-4" als Ergebnis den Wert 2.
Eine Grammatik, die für Recursive-Descent-Übersetzung geeignet ist, lautet folgendermaßen:
expr | number rest | |
rest | + number rest | - number rest | ε |
Leider hat die Beseitigung der Linksrekursionen in der Grammatik auch die Auswertungsreihenfolge geändert. Der Ableitungsbaum für den Ausdruck "9-3-4" zeigt, dass von rechts nach links zusammengefasst wird (Bild 1a). Dennoch ist es möglich, den Ausdruck korrekt auszuwerten, jedoch nur, indem auch Datenfluss von oben nach unten im Baum stattfindet. Bild 1b zeigt den entsprechenden Datenflussgraphen.
Bild 1: Ableitungsbaum und Datenflussgraph für den Ausdruck "9-3-4"
Aus der Analyse des Datenflussgraphen werden geeignete Übersetzungsaktionen hergeleitet, die zu den Parser-Funktionen hinzugefügt werden. Im Unterschied zu den Parser-Funktionen erzeugen die Compiler-Funktionen nun jeweils einen Wert und bekommen gegebenenfalls einen Wert als Parameter übergeben.1)
Wir beschränken uns im Folgenden auf die Übersetzungsaktionen in der Compiler-Funktion, die der Produktion rest - number rest entspricht.
Betrachten wir beispielsweise das erste Vorkommen von rest im Datenflussgraphen von Bild 1b. Die entsprechende Compiler-Funktion rest bekommt die 9 als Parameter übergeben. Sie ruft number auf und subtrahiert den Rückgabewert 3 von 9. Das Ergebnis 6 übergibt sie als Parameter an den rekursiven Aufruf von rest. Den Rückgabewert 2 dieses rekursiven Aufrufs gibt sie ihrerseits als Rückgabewert zurück.
Das folgende Bild 2 zeigt den Ableitungsbaum für eine allgemeine Produktion P0 → P1 P2 P3 und den zugehörigen Datenflussgraphen der Übersetzungsaktionen (die Verallgemeinerung für P0 → P1 ... Pn mit beliebigem n ist offensichtlich).
Bild 2: Ableitungsbaum und Datenflussgraph für die allgemeine Produktion P0 → P1 P2 P3
Die entsprechenden Übersetzungsaktionen werden zu der Produktion P0 → P1 P2 P3 nach folgendem Schema hinzugefügt:
Produktion | Übersetzungsaktionen | ||||||||||||||||||||||||
|
|
Die vi und si sind Programmvariablen. Die Programmvariable v0 ist der Parameter der Funktion P0 und die Programmvariable s4 der Rückgabewert der Funktion P0. Man nennt v0 das vererbte Attribut und s4 das synthetisierte Attribut der Produktion. Die Funktionen fi enthalten die Übersetzungsaktionen. Die Pi(vi) sind Funktionsaufrufe, die zu den Zeichen Pi gehören. Ist Pi ein Terminalzeichen, so wird die Funktion match(Pi) aufgerufen.
Konkret angewandt auf die oben angegebene Produktion rest - number rest ergibt sich das folgende Schema, indem die entsprechenden Zeichen für Pi und die gewünschten Übersetzungsaktionen für fi eingesetzt werden. Nicht benötigte Variablen sind weggelassen worden.
Produktion | Übersetzungsaktionen | ||||||||||||||||||||||||||||
|
|
Welche Übersetzungsaktionen konkret einzusetzen sind, lässt sich wie oben gesehen am einfachsten durch Analyse des Datenflussgraphen eines Beispielausdrucks bestimmen.
So ergeben sich die Berechnungen, die bei Anwendung der Produktion rest - number rest auszuführen sind: Die Funktion rest vermindert den Wert, den sie von oben bekommt (v0 = s0 = 9), um den Wert, den sie von number bekommt (s2 = 3). Hieraus ergibt sich die Übersetzungsaktion v3 = s0 – s2. Das Ergebnis (v3 = 6) übergibt sie als Parameter einem weiteren Aufruf von rest. Den Wert, den sie von diesem Aufruf von rest zurückerhält (s3 = 2), gibt sie ihrerseits zurück (s4 = 2).
Die direkte Implementation der Übersetzungsaktionen liefert folgendes Programm (Variablendeklarationen weggelassen):
Die Implementation lässt sich vereinfachen, indem anstelle der Variablen gleich die entsprechenden Ausdrücke eingesetzt werden, etwa wie folgt:
Wir hatten gesehen, dass sich Linksrekursion auch durch Iteration beseitigen lässt. Das Ergebnis ist die Grammatik
expr | number rest | |
rest | (+ number | - number)* | |
number | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
Wir beschränken uns wiederum auf die Produktion rest (- number)*.
Die allgemeine, formale Vorgehensweise für das Hinzufügen von Übersetzungsaktionen bei einer Produktion vom Typ P0 → (P1 P2)* ergibt sich aus folgendem Schema. Zu beachten ist, dass in den sich wiederholenden Anweisungen innerhalb von ( ... )* zum Schluss wieder die Variable s0 verwendet wird.
Übersetzungsaktionen | ||||||||||||
|
Wir wenden das Schema nun konkret auf die Produktion rest (- number)* an, mit rest für P0, - für P1 und number für P2. Nicht benötigte Variablen sind weggelassen worden.
Übersetzungsaktionen | ||||||||||||
|
Die direkte Implementation der Übersetzungsaktionen liefert folgendes Programm. Die Wiederholung der Anweisungen wird durch eine While-Schleife gesteuert.
Die Implementation lässt sich vereinfachen, indem anstelle der Variablen gleich die entsprechenden Ausdrücke eingesetzt werden:
Aus der Untersuchung des Datenflussgraphen, der einem konkreten Ableitungsbaum entspricht, lassen sich geeignete Übersetzungsaktionen ableiten. Indem diese jeweils in das formale Schema eingesetzt werden, ergibt sich die entsprechende Compiler-Funktion.
Wir haben ein solches formales Schema sowohl für die rekursive Form einer Produktion als auch für die iterative Form angegeben. Das formale Schema enthält eine Vielzahl von Variablen für die jeweiligen Zwischenergebnisse. In der konkreten Implementierung können meist etliche dieser Variablen eingespart werden.
Aufgabe 1: Wenden Sie das formale Vorgehen bei der Konstruktion eines Übersetzers für mehrstellige ganze Zahlen an. Der Übersetzer soll eine als String gegebene Zahl in ihren Zahlenwert übersetzen, z.B. den String "00365" in den Wert 365. Verwenden Sie dabei folgende Grammatik:
number | digit moredigits | |
moredigits | digit* | |
digit | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
1) Der Begriff Wert ist nicht wie in diesem Beispiel auf einen Zahlenwert beschränkt, sondern der Wert kann auch ein String oder eine Referenz auf ein beliebiges Objekt sein.
Weiter mit: [up]