Ein Übersetzer ist eine Erweiterung eines Parsers. Genau wie ein Parser analysiert ein Übersetzer die syntaktische Struktur eines Eingabewortes gemäß einer zugrunde liegenden Grammatik, führt aber nebenbei bestimmte Übersetzungsaktionen durch. Hierdurch rechnet er syntaxgesteuert das Eingabewort in einen bestimmten Wert einer Zielmenge um.
Ein Übersetzer berechnet also eine Abbildung von der Sprache, die durch die Grammatik erzeugt wird, in die Zielmenge. Darüber hinaus erzeugt der Übersetzer für alle Wörter, die nicht von der Grammatik erzeugt werden, eine Fehlermeldung.
Die Zielmenge kann auch wieder eine Sprache sein, aber genauso gut eine beliebige andere Menge. Im ersten einfachen Beispiel wird das Eingabewort in eine Zahl übersetzt. Andere Beispiele wären, einen regulären Ausdruck in einen endlichen Automaten zu übersetzen oder ein C-Programm in Maschinencode zu übersetzen1).
Um aus einem Parser einen Übersetzer zu erzeugen, ordnen Sie jeder Produktion der Grammatik bestimmte Übersetzungsaktionen zu. Der Übersetzer führt dann parallel zum Parsen die jeweiligen Übersetzungsaktionen aus. Die geeigneten Übersetzungsaktionen richten sich nach der gewünschten Abbildung in die Zielmenge; sie lassen sich durch Untersuchung der Ableitungsbäume von Wörtern ermitteln.
Im Programm unterscheidet sich ein Übersetzer von einem Parser dadurch, dass jede Funktion, die einer Variablen der Grammatik entspricht, einen Rückgabewert liefert, nämlich das Ergebnis der Übersetzung.
Um dieses Prinzip zu veranschaulichen, betrachten Sie zunächst ein sehr einfaches Beispiel. Dem allgemeinen Vorgehen zur Konstruktion eines Übersetzers wenden Sie sich dann später zu.
Als erstes formen Sie den allereinfachsten Parser für einstellige Zahlen mit Vorzeichen in einen Übersetzer um. Das Ergebnis der Übersetzung beispielsweise des Eingabewortes "-5" soll der Zahlenwert -5 sein.
Die Untersuchung eines Ableitungsbaums etwa für "-5" zeigt, dass der Zahlenwert, den number liefert, durch Multiplikation von -1 oder +1 mit dem Zahlenwert, den digit liefert, zustande kommt. Hierbei wird der Vorzeichenfaktor -1 oder +1 von sign geliefert. Bild 1 zeigt den Ableitungsbaum (a) und den entsprechenden Datenflussgraphen (b).
Bild 1: Ableitungsbaum für -5 und entsprechender Datenflussgraph
Somit ordnen wir den Produktionen der Grammatik die folgenden Übersetzungsaktionen hinzu:
number sign digit | return sign()*digit() | |
sign + | return +1 | |
sign - | return -1 | |
sign ε | return +1 | |
digit 0 | return 0 | |
| | |
digit 9 | return 9 |
Im Programm gehen Sie vom Parser aus und versehen jede Funktion, die einer Variablen der Grammatik entspricht, mit einem geeigneten Rückgabewert, der sich aus der Anwendung der jeweiligen Übersetzungsaktionen ergibt.
Die Funktion digit ergänzen Sie, indem Sie für eine jeweilige gelesene Ziffer die entsprechende Zahl als Rückgabewert zurückgeben.
In der Funktion sign geben Sie +1 zurück, wenn Sie ein Pluszeichen finden, bzw. -1, wenn Sie ein Minuszeichen finden, ansonsten +1.
Und schließlich fügen Sie in der Funktion number die Ergebnisse von sign und digit in geeigneter Weise zusammen:
In einem Modul SimpleNumberCompiler fassen Sie diese Funktionen zusammen. Die Hilfsfunktionen wie etwa comes oder consume befinden sich im Basismodul Parser.
Die Funktion parse im Basismodul Parser ändern Sie noch in der Weise, dass sie einen Wert zurückgeben, und benennen sie in compile um. Diese Funktion muss sich im Modul Parser befinden, da hier die globalen Variablen inputstring usw. definiert werden.
Im Hauptprogramm testen Sie den SimpleNumberCompiler dann folgendermaßen, hierbei rufen Sie die Funktion compile auf und speichern den Rückgabewert. Das Eingabewort und den Namen der Funktion, die dem Startsymbol der Grammatik entspricht, übergeben Sie als Parameter.
1) Übersetzer, die Programmcode in anderen Programmcode übersetzen, werden als Compiler bezeichnet