Vorlesungsskript - Institut für Programmierung und Reaktive Systeme
Vorlesungsskript - Institut für Programmierung und Reaktive Systeme
Vorlesungsskript - Institut für Programmierung und Reaktive Systeme
Sie wollen auch ein ePaper? Erhöhen Sie die Reichweite Ihrer Titel.
YUMPU macht aus Druck-PDFs automatisch weboptimierte ePaper, die Google liebt.
TECHNISCHE UNIVERSITÄT CAROLO-WILHELMINA ZU BRAUNSCHWEIG<br />
<strong>Vorlesungsskript</strong><br />
Compilerbau<br />
Prof. Dr. Ursula Goltz Dr. Thomas Gehrke<br />
Dipl.-Inform. Malte Lochau<br />
1. April 2009<br />
<strong>Institut</strong> <strong>für</strong> <strong>Programmierung</strong> <strong>und</strong> <strong>Reaktive</strong> <strong>Systeme</strong>
Vorwort<br />
Das vorliegende Skript ist als Ausarbeitung der Compilerbau-Vorlesung entstanden,<br />
die ich seit dem Sommersemester 1992, zunächst an der Universität Hildesheim, seit<br />
1998 an der Technischen Universität Braunschweig halte. Die erste Version der Vorlesung<br />
war stark durch die Compilerbau-Vorlesung Prof. Dr. Klaus Indermark, die<br />
ich in den 80’er Jahren an der RWTH Aachen als Mitarbeiterin betreuen durfte,<br />
beeinflusst. Sie basiert außerdem zu großen Teilen auf dem klassischen ” Drachenbuch“<br />
[Aho08] von Aho et. al. Sehr hilfreich bei der Weiterentwicklung der Vorlesung<br />
war das zwischenzeitlich erschienene Buch von Reinhard Wilhelm <strong>und</strong> Dieter Maurer<br />
[WM96].<br />
Darüber hinaus haben im Laufe der Jahre viele Beteiligte zur Ausarbeitung <strong>und</strong> Weiterentwicklung<br />
dieses Skripts beigetragen, denen ich an dieser Stelle danken möchte.<br />
Zunächst danke ich Dr. Michaela Huhn <strong>und</strong> Dr. Peter Niebert, die mich bei der Konzeption<br />
der Vorlesung in Hildesheim hervorragend unterstützt haben. Besonderer<br />
Dank gebührt meinen Mitautoren Dr. Thomas Gehrke <strong>und</strong> Malte Lochau, die dieses<br />
Skript mit hoher Selbstständigkeit bearbeitet haben. Dr. Werner Struckmann <strong>und</strong><br />
Tilo Mücke haben durch Ergänzungen <strong>und</strong> hilfreiche Hinweise beigetragen. Jochen<br />
Kamischke hat uns als studentische Hilfskraft sehr gut unterstützt. Die Studierenden,<br />
die diese Vorlesung in den vergangenen Jahren gehört haben, haben durch ihre aktive<br />
Teilnahme ebenfalls zur Weiterentwicklung des Skripts beigetragen; auch Ihnen<br />
gebührt mein herzlicher Dank.<br />
Braunschweig, den 5. März 2009<br />
Ursula Goltz<br />
i
Inhaltsverzeichnis<br />
Verzeichnis der Abbildungen v<br />
Verzeichnis der Tabellen vii<br />
Listings viii<br />
1 Einführung 1<br />
1.1 Inhalte <strong>und</strong> Gliederung . . . . . . . . . . . . . . . . . . . . . . . . . . 1<br />
1.2 Höhere Programmiersprachen . . . . . . . . . . . . . . . . . . . . . . 2<br />
1.3 Implementierung von Programmiersprachen . . . . . . . . . . . . . . 3<br />
1.3.1 Interpreter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3<br />
1.3.2 Compiler . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3<br />
1.3.3 Virtuelle Maschinen als Zielplattform . . . . . . . . . . . . . . 4<br />
1.4 Umgebung eines Compilers . . . . . . . . . . . . . . . . . . . . . . . . 5<br />
1.5 Aufbau eines Compilers . . . . . . . . . . . . . . . . . . . . . . . . . 7<br />
1.5.1 Analyse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7<br />
1.5.2 Synthese . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11<br />
1.5.3 Front-End, Back-End . . . . . . . . . . . . . . . . . . . . . . . 12<br />
1.5.4 Läufe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12<br />
2 Lexikalische Analyse 14<br />
2.1 Terminologie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14<br />
2.2 Reguläre Sprachen <strong>und</strong> endliche Automaten . . . . . . . . . . . . . . 15<br />
2.2.1 Reguläre Sprachen . . . . . . . . . . . . . . . . . . . . . . . . 16<br />
2.2.2 Reguläre Ausdrücke . . . . . . . . . . . . . . . . . . . . . . . . 16<br />
2.2.3 Endliche Automaten . . . . . . . . . . . . . . . . . . . . . . . 18<br />
2.2.4 Reguläre Definitionen . . . . . . . . . . . . . . . . . . . . . . . 27<br />
2.3 Sieber . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30<br />
2.4 Fehlerbehandlung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32<br />
3 Syntaktische Analyse 35<br />
3.1 Kontextfreie Grammatiken . . . . . . . . . . . . . . . . . . . . . . . . 35<br />
3.1.1 Kontextfreie Grammatiken . . . . . . . . . . . . . . . . . . . . 36<br />
3.1.2 Ableitungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38<br />
3.1.3 Strukturbäume . . . . . . . . . . . . . . . . . . . . . . . . . . 40<br />
iii
Inhaltsverzeichnis<br />
3.1.4 Mehrdeutige Grammatiken . . . . . . . . . . . . . . . . . . . . 42<br />
3.2 Konstruktion von Parsern . . . . . . . . . . . . . . . . . . . . . . . . 46<br />
3.2.1 Kellerautomat . . . . . . . . . . . . . . . . . . . . . . . . . . . 46<br />
3.3 Top-Down-Syntaxanalyse . . . . . . . . . . . . . . . . . . . . . . . . . 49<br />
3.3.1 LL(k)-Grammatiken . . . . . . . . . . . . . . . . . . . . . . . 51<br />
3.3.2 Transformierung von Grammatiken . . . . . . . . . . . . . . . 59<br />
3.3.3 Erweiterte kontextfreie Grammatiken . . . . . . . . . . . . . . 67<br />
3.3.4 Fehlerbehandlung bei der Top-Down-Analyse . . . . . . . . . . 75<br />
3.4 Bottom-Up-Syntaxanalyse . . . . . . . . . . . . . . . . . . . . . . . . 80<br />
3.4.1 LR(k)-Grammatiken . . . . . . . . . . . . . . . . . . . . . . . 84<br />
3.4.2 Fehlerbehandlung bei der Bottom-Up-Analyse . . . . . . . . . 113<br />
3.5 Parser Generatoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117<br />
4 Semantische Analyse 118<br />
4.1 Attributierte Grammatiken . . . . . . . . . . . . . . . . . . . . . . . . 119<br />
4.2 Typüberprüfung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 128<br />
4.2.1 Typsysteme . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129<br />
4.2.2 Gleichheit von Typausdrücken . . . . . . . . . . . . . . . . . . 135<br />
4.2.3 Typumwandlungen . . . . . . . . . . . . . . . . . . . . . . . . 137<br />
5 Zwischencode-Erzeugung 141<br />
5.1 Abstrakte Keller-Maschinen . . . . . . . . . . . . . . . . . . . . . . . 142<br />
5.1.1 Syntaxbäume . . . . . . . . . . . . . . . . . . . . . . . . . . . 143<br />
5.1.2 Zwischencode <strong>für</strong> die Keller-Maschine . . . . . . . . . . . . . . 147<br />
5.1.3 Befehle zur Steuerung des Kontrollflusses . . . . . . . . . . . . 147<br />
5.2 Drei-Adreß-Code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 151<br />
5.2.1 Übersetzung von Syntaxbäumen in Drei-Adreß-Code . . . . . 153<br />
5.2.2 Übersetzung in Drei-Adreß-Code unter Verwendung von attributierten<br />
Grammatiken . . . . . . . . . . . . . . . . . . . . . . 155<br />
5.3 Vergleich der beiden Arten von Zwischencode . . . . . . . . . . . . . 157<br />
Literaturverzeichnis 158<br />
iv
Verzeichnis der Abbildungen<br />
1.1 Umgebung eines Compilers. . . . . . . . . . . . . . . . . . . . . . . . 6<br />
1.2 Phasen eines Compilers. . . . . . . . . . . . . . . . . . . . . . . . . . 7<br />
1.3 Übersetzung einer Zuweisung. . . . . . . . . . . . . . . . . . . . . . . 9<br />
1.4 Parse-Baum der Zuweisung. . . . . . . . . . . . . . . . . . . . . . . . 10<br />
2.1 Interaktion zwischen Scanner <strong>und</strong> Parser. . . . . . . . . . . . . . . . . 14<br />
2.2 Beispiel eines Übergangsgraphen. . . . . . . . . . . . . . . . . . . . . 20<br />
2.3 Konstruktion eines NEA zu einem regulärem Ausdruck. . . . . . . . . 21<br />
2.4 Beispiel einer NEA-Konstruktion. . . . . . . . . . . . . . . . . . . . . 21<br />
2.5 Beispiel zur Potenzmengenkonstruktion. . . . . . . . . . . . . . . . . 24<br />
2.6 DEA mit minimaler Zustandsmenge. . . . . . . . . . . . . . . . . . . 26<br />
2.7 Beispiel zur Minimalisierung. . . . . . . . . . . . . . . . . . . . . . . . 26<br />
2.8 Übergangsgraphen <strong>für</strong> die Symbole des Beispiels. . . . . . . . . . . . 31<br />
2.9 Analyse eines Programmausschnitts. . . . . . . . . . . . . . . . . . . 33<br />
3.1 Interaktion zwischen Scanner, Parser <strong>und</strong> restlichem Front-End. . . . 35<br />
3.2 Beispiel eines Strukturbaums. . . . . . . . . . . . . . . . . . . . . . . 41<br />
3.3 Konstruktion eines Strukturbaums. . . . . . . . . . . . . . . . . . . . 42<br />
3.4 Verschiedene Strukturbäume zu einem Satz. . . . . . . . . . . . . . . 43<br />
3.5 Mögliche Strukturbäume des “dangling else”-Problems. . . . . . . . . 44<br />
3.6 Lösung des “dangling else”-Problems. . . . . . . . . . . . . . . . . . . 45<br />
3.7 Schema eines Kellerautomaten. . . . . . . . . . . . . . . . . . . . . . 47<br />
3.8 Konstruktion des Strukturbaums anhand der Ausgabe des Parsers. . . 52<br />
3.9 Fehlerhafte Konstruktion eines Strukturbaums <strong>für</strong> die Eingabe a. . . 56<br />
3.10 Transformation in rechtsrekursive Grammatik. . . . . . . . . . . . . . 61<br />
3.11 Beispiel einer regulären Ableitung. . . . . . . . . . . . . . . . . . . . 69<br />
3.12 Übergangsgraphen <strong>für</strong> arithmetische Ausdrücke. . . . . . . . . . . . . 71<br />
3.13 Übergangsgraphen <strong>für</strong> Pascal-Typen. . . . . . . . . . . . . . . . . . . 73<br />
3.14 Beispiel eines recursive descent-Parsers. . . . . . . . . . . . . . . . . . 74<br />
3.15 Aufrufgraph einer recursive descent-Syntaxanalyse. . . . . . . . . . . 75<br />
3.16 Erzeugung eines Strukturbaums <strong>für</strong> einen arithmetischen Ausdruck. . 82<br />
3.17 Beispiel eines charakteristischen endlichen Automaten . . . . . . . . . 90<br />
3.18 Beispiel eines LR-DEA. . . . . . . . . . . . . . . . . . . . . . . . . . . 94<br />
3.19 Struktur einer LR(1)-Parse-Tabelle. . . . . . . . . . . . . . . . . . . . 104<br />
v
Verzeichnis der Abbildungen<br />
vi<br />
3.20 Zustandsmenge des LR-DEA der Grammatik zur Beschreibung der<br />
C-Zuweisung aus Beispiel 43. . . . . . . . . . . . . . . . . . . . . . . 106<br />
3.21 Zustandsmenge des charakteristischen endlichen Automaten mit LR(1)-<br />
Items zur Grammatik aus Beispiel 43. . . . . . . . . . . . . . . . . . . 107<br />
3.22 Charakteristischer endlicher LR(1)-Automat zur Grammatik aus Beispiel<br />
43. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108<br />
3.23 Zustandsmenge des charakteristischen endlichen Automaten mit SLR(1)-<br />
Items <strong>für</strong> die Grammatik aus Beispiel 43. . . . . . . . . . . . . . . . . 110<br />
3.24 Zustandsmenge des charakteristischen endlichen Automaten mit LALR(1)-<br />
Items <strong>für</strong> die Grammatik aus Beispiel 43. . . . . . . . . . . . . . . . . 111<br />
3.25 Charakteristischer endlicher LR(1)-Automat zur Grammatik aus Beispiel<br />
43. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112<br />
4.1 Beispiel eines attributierten Strukturbaums. . . . . . . . . . . . . . . 119<br />
4.2 Synthetische <strong>und</strong> inherite Attribute. . . . . . . . . . . . . . . . . . . . 120<br />
4.3 Berechnung von Typinformationen im Strukturbaum. . . . . . . . . . 121<br />
4.4 Attributierter Strukturbaum zur Analyse einer Binärzahl. . . . . . . . 123<br />
4.5 Attributierter Strukturbaum zur Analyse einer Binärzahl mit inheritem<br />
Attribut. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 124<br />
4.6 Darstellung der direkten Abhängigkeiten zwischen Attributvorkommen.126<br />
4.7 Beispiel eines Abhängigkeitsgraphen. . . . . . . . . . . . . . . . . . . 127<br />
4.8 “Verklebter” Abhängigkeitsgraph. . . . . . . . . . . . . . . . . . . . . 127<br />
4.9 Semantische Regeln <strong>für</strong> Beispielsprache. . . . . . . . . . . . . . . . . . 133<br />
4.10 Attributierter Strukturbaum <strong>für</strong> ein Beispielprogramm. . . . . . . . . 133<br />
4.11 Attributierter Strukturbaum <strong>für</strong> ein fehlerhaftes Beispielprogramm. . 134<br />
4.12 Funktion zur Überprüfung, ob zwei Typausdrücke identisch sind. . . . 135<br />
4.13 Attributierter Strukturbaum einer Zuweisung. . . . . . . . . . . . . . 139<br />
4.14 Attributierter Strukturbaum einer Zuweisung mit Attributabhängigkeiten.<br />
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 139<br />
4.15 Attributierter Strukturbaum einer Zuweisung mit Typfehler. . . . . . 140<br />
5.1 Einordnung der Zwischencode-Erzeugung. . . . . . . . . . . . . . . . 141<br />
5.2 Syntaxbaum . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 144<br />
5.3 Konstruktion eines Syntaxbaums aus Postfix-Notation. . . . . . . . . 145<br />
5.4 Auswertung eines Postfix-Ausdrucks mit Hilfe eines Stacks. . . . . . . 146<br />
5.5 Attributierter Strukturbaum mit Zwischencode. . . . . . . . . . . . . 148<br />
5.6 Attributierter Strukturbaum einer verschachtelten if-Anweisung. . . . 152<br />
5.7 Syntaxbaum mit temporären Namen. . . . . . . . . . . . . . . . . . . 154<br />
5.8 Syntaxbaum mit Attributen <strong>für</strong> die Erzeugung von Drei-Adreß-Code. 156
Verzeichnis der Tabellen<br />
2.1 Beispiele <strong>für</strong> Symbole, Muster <strong>und</strong> Lexeme. . . . . . . . . . . . . . . 15<br />
2.2 Übergangsrelation ∆ in Tabellenform. . . . . . . . . . . . . . . . . . . 20<br />
2.3 Reguläre Ausdrücke <strong>und</strong> die dazugehörigen Symbole <strong>und</strong> Attributwerte. 29<br />
3.1 Beispielableitung eines Top-Down-Parsers. . . . . . . . . . . . . . . . 50<br />
3.2 Beispiel einer Parse-Tabelle. . . . . . . . . . . . . . . . . . . . . . . . 64<br />
3.3 Parse-Tabelle <strong>für</strong> dangling-else-Grammatik. . . . . . . . . . . . . . . . 67<br />
3.4 Für Fehlerbehandlung modifizierte Parse-Tabelle. . . . . . . . . . . . 77<br />
3.5 Beispiel einer Ableitung mit Fehlerbehandlung. . . . . . . . . . . . . 78<br />
3.6 Beispielableitung eines Bottom-Up-Parsers. . . . . . . . . . . . . . . . 83<br />
vii
Listings<br />
viii<br />
NEA nach DEA . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23<br />
DEA Minimalisierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25<br />
Berechnung von FIRST 1–Mengen . . . . . . . . . . . . . . . . . . . . . . 57<br />
Berechnung von FOLLOW 1–Mengen . . . . . . . . . . . . . . . . . . . . . 58<br />
Transformation einer linksrekursiven in eine rechtsrekursive Grammatik . . 60<br />
Linksfaktorisierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62<br />
Konstruktion der Parse–Tabelle zu einer Grammatik . . . . . . . . . . . . 63<br />
Deterministische Top–Down–Analyse mit Parse–Tabelle . . . . . . . . . . . 65<br />
Konstruktion der FIRST–<strong>und</strong> FOLLOW–Mengen einer ELL(1)–Grammatik 69<br />
Beispiel eines recursive descent–Parsers . . . . . . . . . . . . . . . . . . . . 74<br />
LR–DEA–Konstruktion . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92<br />
Algorithmus LR(1)–GEN . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100<br />
Konstruktion der LR(1)–action–Tabelle . . . . . . . . . . . . . . . . . . . . 104<br />
LR(1)–Parse–Algorithmus . . . . . . . . . . . . . . . . . . . . . . . . . . . 105<br />
Konstruktion der SLR(1)–action–Tabelle . . . . . . . . . . . . . . . . . . . 109<br />
Ueberpruefung ob zwei Typausdruecke identisch sind . . . . . . . . . . . . 135
1 Einführung<br />
1.1 Inhalte <strong>und</strong> Gliederung<br />
Die Techniken zur Konstruktion von Übersetzern (Compiler) als der altehrwürdigen<br />
Disziplin der Informatik sind unverändert allgegenwärtig. Wie kaum ein anderes<br />
Gebiet der Informatik werden beim Compilerbau Themen aus Theorie <strong>und</strong> Praxis<br />
miteinander verb<strong>und</strong>en. Auf der einen Seite bilden Automatentheorie <strong>und</strong> formale<br />
Sprachen das theoretische F<strong>und</strong>ament bei der Implementierung von Programmiersprachen.<br />
Zugleich gehören aber auch praktische Fragestellungen beim Entwurf <strong>und</strong><br />
der Entwicklung von Programmiersprachen <strong>für</strong> konkrete Aufgabenstellungen zu diesem<br />
Beschäftigungsfeld. Schließlich muss sich der Compilerbauer auch mit den Ressourcen,<br />
Befehlssätzen etc. unterschiedlichster Rechnerarchitekturen möglicher Zielplattformen<br />
auseinander setzen.<br />
Auch bei aktuellen Themen <strong>und</strong> Problemstellungen der Informatik kommt man an<br />
Techniken des Compilerbaus nicht vorbei. Dazu zählen Themen wie virtuelle Maschinen,<br />
Parallelisierung, Speicherlokalität <strong>und</strong> Programmanalyse <strong>und</strong> -optimierung.<br />
Das vorliegende Skript ist in zwei Teile gegliedert. Der erste Teil befasst sich mit den<br />
Teilen des Compilers, die als Front-End bezeichnet werden. Dazu zählen alle Phasen<br />
des Übersetzungsvorgangs einer Eingabesprache, die unabhängig von der Zielsprache<br />
<strong>und</strong> Ausführungsplattform <strong>für</strong> übersetzte Programme erfolgen. Dazu zählen insbesondere<br />
die Analysephasen zur Überprüfung der Korrektheit des Eingabeprogrammes<br />
sowie die im Rahmen der einzelnen Übersetzungsschritte erzeugten Zwischendarstellungen<br />
des Eingabeprogrammes.<br />
Nach dem einführenden Kapitel, das die Gr<strong>und</strong>begriffe des Übersetzerbaus einführt,<br />
folgen aufeinander aufbauend die ausführliche Einführung der Konzepte der lexikalischen,<br />
syntaktischen <strong>und</strong> (statischen) semantischen Analyse. Die Vorgehensweise<br />
in den einzelnen Phasen wird jeweils durch Beispiele veranschaulicht, so zum Beispiel<br />
die Typüberprüfung im Rahmen der semantischen Analyse. Eine systematische<br />
Einführung in die theoretischen Gr<strong>und</strong>lagen von Typsystemen erfolgt dann im zweiten<br />
Teil des Skripts. Als Abschluss des ersten Teils werden verschiedene Techniken<br />
der Zwischencode-Erzeugung beschrieben, die den vorbereitenden Schritt <strong>für</strong> die anschließende<br />
Code-Generierung durch das Back-End darstellen.<br />
Der zweite Teil des Skripts ist dem Back-End des Compilers gewidmet, also den<br />
Zielplattform-spezifischen Phasen, in denen die Synthese des Zielprogrammes aus<br />
dem Quellprogramm erfolgt. Der Schwerpunkt wird in diesem Teil auf der Überset-
1 Einführung<br />
zung objektorientierter Programmiersprachen liegen, deren statische <strong>und</strong> dynamische<br />
Eigenschaften im Detail untersucht werden. Als exemplarische Zielplattform werden<br />
abstrakte bzw. virtuelle Maschine betrachtet. Die theoretischen Gr<strong>und</strong>lagen werden<br />
dann ausführlich an einer konkreten Programmiersprache, der objektorientierten<br />
Sprache Java, verdeutlicht. Den Abschluss bilden Optimierungstechniken <strong>für</strong> Übersetzer,<br />
wobei sowohl allgemeine Ansätze, als auch speziell objektorientierte Ansätze<br />
beschrieben werden.<br />
1.2 Höhere Programmiersprachen<br />
In der Anfangszeit der Informatik wurde die <strong>Programmierung</strong> von Rechnern in der<br />
jeweiligen Maschinensprache des entsprechenden Rechners vorgenommen. Aufgr<strong>und</strong><br />
der Rechnerorientierung dieser Sprachen war der Entwurf sowie die nachfolgende<br />
Anpassung <strong>und</strong> Änderung des Programmcodes äußerst aufwendig <strong>und</strong> mit hohen<br />
Kosten verb<strong>und</strong>en. Speicherzellen mußten direkt über ihre jeweiligen Adressen angesprochen<br />
werden. Wurden nachträglich Änderungen am Programm vorgenommen,<br />
mußten diese Adressen “von Hand” angepaßt werden.<br />
Eine erste Verbesserung wurde durch die Einführung der Assemblersprachen erzielt.<br />
Den Befehlen der Maschinensprache wurden kurze Buchstabenfolgen, sogenannte<br />
Mnemonics 1 , zugeordnet, die die Lesbarkeit von Programmtexten erhöhen. Das<br />
Programm zur Generierung des zu einem Assemblertext gehörenden Maschinenprogramms,<br />
ebenfalls Assembler genannt, erlaubte eine rudimentäre Überprüfung des<br />
Programmtextes auf Fehler. Außerdem wurde durch die Möglichkeit zur Vergabe<br />
symbolischer Adressen (labels) die Pflege von Programmen erleichtert.<br />
Trotz der Überlegenheit der Assemblersprachen gegenüber den Maschinensprachen<br />
besitzt die Programmerstellung mittels Assembler eine Reihe bedeutender Nachteile.<br />
Durch die direkte Zuordnung von Mnemonics zu Maschinenbefehlen ist der Abstraktionsgrad<br />
von Programmen gering, so daß sich die <strong>Programmierung</strong> immer noch an<br />
der Maschine <strong>und</strong> nicht am konkreten Problem orientieren muß. Assemblerprogramme<br />
sind aufgr<strong>und</strong> ihres Mangels an Strukturelementen nur schwer verständlich, was<br />
eine erschwerte Wartbarkeit von Programmen zur Folge hat. Durch den Mangel an<br />
Datenstrukturen ist die Handhabung der Daten eines Programms aufwendig. Außerdem<br />
sind Assemblerprogramme nur auf einem Maschinentyp einsetzbar, so daß<br />
Portierungen von Programmen auf andere Maschinentypen mit anderer Maschinensprache<br />
i. allg. nicht möglich sind.<br />
Um die Nachteile der Assemblersprachen zu vermeiden <strong>und</strong> um eine problembezogene<br />
<strong>Programmierung</strong> zu unterstützen, wurden die höheren Programmiersprachen eingeführt.<br />
Diese Sprachen abstrahieren von den Eigenschaften der verwendeten Rechner.<br />
Kontrollstrukturen wie z.B. Schleifen <strong>und</strong> Rekursion erlauben eine Steuerung<br />
2<br />
1 Mnemonik ist die Kunst, das Gedächtnis durch Hilfsmittel zu unterstützen.
1.3 Implementierung von Programmiersprachen<br />
des Programmflusses ohne die Verwendung von Sprungbefehlen. Das Konzept der<br />
Variablen <strong>und</strong> der Datentypen entlastet den Programmierer von der aufwendigen<br />
Speicherverwaltung. Durch den Abstraktionsgrad wird zudem die Portierung von<br />
Programmen auf andere Rechnerarchitekturen erleichtert.<br />
1.3 Implementierung von Programmiersprachen<br />
Um Programme einer höheren Programmiersprache auf einem Rechner ausführen zu<br />
können, muß diese Sprache auf diesem Rechner verfügbar gemacht (implementiert)<br />
werden. Die dazu existierenden Konzepte werden in zwei Klassen eingeteilt.<br />
1.3.1 Interpreter<br />
Ein Interpreter IL zur einer Programmiersprache L ist ein Programm, das als Eingabe<br />
ein Programm pL der Sprache L <strong>und</strong> eine Eingabefolge e erhält <strong>und</strong> eine Ausgabefolge<br />
a errechnet. Da bei der Interpretation von pL auch Fehler auftreten können, läßt sich<br />
die Funktionalität des Interpreters darstellen als<br />
IL : L × D ∗ → D ∗ ∪ {error},<br />
wenn sowohl Eingabe- wie auch Ausgabedaten aus einem gemeinsamen Bereich D<br />
stammen. Die Ausführung des Programms pL mit Eingabefolge e <strong>und</strong> Ausgabefolge<br />
a ist durch die Gleichung<br />
IL(pL,e) = a<br />
beschrieben.<br />
Die Arbeitsweise eines Interpreters ist gekennzeichnet durch eine gleichzeitige Bearbeitung<br />
des Programms pL <strong>und</strong> der Eingabe e. Dies führt dazu, daß der Interpreter<br />
bei jeder, also auch bei wiederholter, Ausführung eines Programmkonstrukts zuvor<br />
das Konstrukt analysieren muß. Daher kann der Interpreter auch keine globalen Informationen,<br />
etwa zur Optimierung der Speicherverwaltung, über pL verwenden.<br />
1.3.2 Compiler<br />
Um die aus der lokalen Sicht eines Interpreters auf das auszuführende Programm resultierenden<br />
Ineffizienzen zu vermeiden, werden beim Compiler die Verarbeitung des<br />
Programms <strong>und</strong> der Eingabe nacheinander durchgeführt. Zuerst wird das Programm<br />
pL ohne die Berücksichtigung von Eingabedaten analysiert <strong>und</strong> in eine andere Form<br />
überführt. Diese erlaubt die effizientere Ausführung des Programms mit beliebigen<br />
Eingabefolgen, ohne daß die Analyse <strong>und</strong> die Überführung des Programms wiederholt<br />
werden müssen.<br />
3
1 Einführung<br />
Wir nennen unsere zu übersetzende Sprache L im folgenden Quellsprache. Die Übersetzung<br />
besteht in der Überführung eines Programms pL der Quellsprache in ein<br />
Programm pM, wobei M die Maschinen- oder die Assemblersprache eines konkreten<br />
oder abstrakten Rechners ist. Wir nennen pM Zielprogramm <strong>und</strong> folglich M die<br />
Zielsprache des Übersetzers.<br />
Nach der Übersetzung wird das erzeugte Programm pM zur Laufzeit mit der Eingabefolge<br />
e ausgeführt. Dabei gehen wir davon aus, daß pM mit der Eingabe e die<br />
Ausgabe a erzeugt, wenn ein Interpreter <strong>für</strong> L mit IL(pL,e) = a dieselbe Ausgabe<br />
erzeugt. Wenn wir die Maschine, deren Maschinensprache unsere Zielsprache M ist,<br />
als Interpreter IM <strong>für</strong> M auffassen, so muß gelten:<br />
wenn IL(pL,e) = a, dann IM(pM,e) = a.<br />
Neben der Generierung des Zielprogramms wird während der Übersetzung eines Programms<br />
eine umfassende Überprüfung auf Fehler des zu analysierenden Programmtextes<br />
vorgenommen. Durch diese globale Analyse können manche Fehler bereits vor<br />
der Ausführung des Programms gef<strong>und</strong>en werden, während dies beim Interpreter aufgr<strong>und</strong><br />
seiner lokalen Sicht auf den Programmtext erst zur Laufzeit geschehen kann.<br />
Der Compiler erkennt nur Fehler eines Programmes, die von der konkreten Eingabe<br />
während eines Programmlaufs unabhängig sind. Hierzu gehören neben syntaktischen<br />
Fehlern auch semantische Fehler, z.B. Zugriffe auf nichtdeklarierte Variablen. Fehler,<br />
die aus der Eingabe e resultieren, können auch beim Compiler erst zur Laufzeit des<br />
Zielprogramms pM entdeckt werden.<br />
1.3.3 Virtuelle Maschinen als Zielplattform<br />
Bei der klassischen Implementierung einer Programmiersprache auf einer Zielplattform<br />
sind wir bisher sowohl bei der Verwendung eines Interpreters als auch eines<br />
Compilers von der Maschinensprache des realen Rechners als Zielsprache ausgegangen.<br />
Dieser Ansatz hat mehrere Nachteile:<br />
• Der Compiler-Entwickler muss <strong>für</strong> eine Vielzahl unterschiedlicher Instruktionssätze<br />
von Prozessoren entsprechende Back-Ends (siehe unten) zur Verfügung<br />
stellen.<br />
• Der Compiler-Entwickler möchte <strong>für</strong> eine effiziente Code-Erzeugung auf Befehle<br />
zurückgreifen, die eventuell nicht im Instruktionssatz enthalten sind. Das<br />
gilt insbesondere <strong>für</strong> die Umsetzung spezieller Konstrukte der Quellsprache,<br />
beispielsweise zur Behandlung von Ausnahmen.<br />
• Insbesondere möchte der Entwickler <strong>für</strong> die Übersetzung von Programmen entwickelte<br />
Ansätze trotz sich ändernder Instruktionssätze <strong>und</strong> Rechnerarchitekturen<br />
gleich bleibende Rahmenbedingungen vorfinden.<br />
4
1.4 Umgebung eines Compilers<br />
Aus diesen Gründen wird bei der Implementierung neuerer Programmiersprachen<br />
zunehmend dazu übergegangen, den Instruktionssatz einer idealisierten abstrakten<br />
oder virtuellen Maschine (VM) als Zielsprache des Compilers einzuführen. Die so<br />
definierte zusätzliche Abstraktionsschicht auf der Zielplattform hat mehrere Vorteile:<br />
• Die Implementierung der virtuellen Maschine <strong>für</strong> eine spezifische Plattform<br />
kann unabhängig von der Entwicklung des Compilers vorgenommen werden,<br />
solange beides mit Hinblick auf einen festen Instruktionssatz der VM erfolgt.<br />
• Für die VM erzeugte Code ist auf jedem System lauffähig, auf dem die VM<br />
implementiert ist (Portierbarkeit).<br />
• Der Befehlssatz der VM kann passend zu Paradigmen <strong>und</strong> Konstrukten der<br />
Quellsprache gewählt werden <strong>und</strong> erlaubt so eine effiziente Übersetzung der<br />
Quellprogramme. Beispiel: Der Java Bytecode (JBC) als Instruktionssatz der<br />
Java Virtual Machine (JVM) beinhaltet spezielle Befehle zum Umgang mit<br />
Objekten, da die Quellsprache Java objektorientiert ist.<br />
• Der durch den Compiler erzeugte Code wird nicht direkt auf dem Zielsystem<br />
ausgeführt, sondern auf einer Zwischenschicht, wodurch eine sicherere Ausführung<br />
gewährleistet werden kann (Sand-Boxing).<br />
Somit kann der Einsatz einer virtuellen Maschine <strong>für</strong> die Implementierung einer Programmiersprache<br />
als Kombination beider zuvor vorgestellten Ansätze gesehen werden:<br />
1. Die Übersetzung des Quellprogramms in ein Programm <strong>für</strong> die virtuelle Maschine<br />
erfolgt durch einen Compiler.<br />
2. Die virtuelle Maschine ist ein Interpreter <strong>für</strong> das durch den Compiler erzeugte<br />
Zielprogramm.<br />
In zweiten Teil der Vorlesung werden wir uns ausführlich mit den Konzepten virtueller<br />
Maschinen beschäftigen.<br />
1.4 Umgebung eines Compilers<br />
Zur Umgebung eines Compilers gehören i. allg. weitere Programme, die <strong>für</strong> die Übersetzung<br />
<strong>und</strong> die Ablauffähigkeit eines Programms benötigt werden (Abbildung 1.1<br />
[ASU99]).<br />
Am Beginn des Übersetzungsprozesses steht ein “rohes” Quellprogramm. Dieses<br />
Quellprogramm enthält neben dem eigentlichen Programm zusätzliche Meta-Anweisungen,<br />
die beschreiben, wie das Quellprogramm vor der Übersetzung mit dem Compiler<br />
modifiziert werden soll. Dabei kann es sich z.B. um die Definition von Makros<br />
(z.B. #define in C [KR90]), um die Generierung zusätzlicher Befehle zur Fehlersuche<br />
oder um das Einfügen weiterer Quelltexte (z.B. \include in L ATEX [Kop02]) handeln.<br />
Diese Modifikationen des Quelltextes werden von einem sogenannten Präprozessor<br />
5
1 Einführung<br />
"rohes" Quellprogramm<br />
Präprozessor<br />
Quellprogramm<br />
Compiler<br />
Assemblerprogramm<br />
Assembler<br />
relokatibler Maschinencode<br />
Lader / Binder<br />
ausführbarer Maschinencode<br />
Abbildung 1.1: Umgebung eines Compilers.<br />
vorgenommen.<br />
Nach der Behandlung des Quelltextes durch den Präprozessor kann der Compiler das<br />
Zielprogramm erzeugen. Wie zuvor erwähnt, handelt es sich bei dem Zielprogramm<br />
entweder um ein Maschinenspracheprogramm bzw. einen Assemblertext. Im zweiten<br />
Fall muß der Assemblertext nun in einem zusätzlichen Schritt durch den Assembler<br />
in Maschinencode übersetzt werden. Oft wird heutzutage vom Compiler statt des<br />
Assemblertextes ein C-Programm erzeugt, welches vom C-Compiler weiterverarbeitet<br />
wird.<br />
Der vom Compiler erzeugte Maschinencode ist i. allg. noch nicht ausführbar, da es<br />
sich um sogenannten relokatiblen Code handelt. In diesem Code sind die Sprungadressen<br />
noch nicht festgelegt, so daß der Code im Speicher frei verschiebbar ist.<br />
Außerdem müssen die Bibliotheken des jeweiligen Übersetzers noch zum erzeugten<br />
Maschinencode hinzugefügt werden. Diese Bibliotheken enthalten z.B. die Ein- <strong>und</strong><br />
Ausgaberoutinen der Programme (z.B. das Modul InOut in Modula-2 [Wir97]) sowie<br />
weitere Routinen, die zur Laufzeit eines Programmes benötigt werden. Es gibt zwei<br />
Verfahren zur Einbindung der Bibliotheken in ein Programm. Der Binder faßt den relokatiblen<br />
Maschinencode <strong>und</strong> den Code der Bibliotheken zu einem neuen Programm<br />
zusammen <strong>und</strong> ersetzt dabei die abstrakten Programmadressen des relokatiblen Codes<br />
durch die statischen Adressen der Unterprogramme der Bibliotheken. Dieses erzeugte<br />
Programm ist ohne die Unterstützung weiterer Programme ausführbar. Der<br />
6
Symboltabellenverwaltung<br />
Quellprogramm<br />
lexikalische<br />
Analyse<br />
syntaktische<br />
Analyse<br />
semantische<br />
Analyse<br />
Zwischencodeerzeugung<br />
Codeoptimierung<br />
Codeerzeugung<br />
Zielprogramm<br />
1.5 Aufbau eines Compilers<br />
Analyse<br />
Fehlerbehandlung<br />
Synthese<br />
Abbildung 1.2: Phasen eines Compilers.<br />
Lader lädt hingegen den relokatiblen Code <strong>und</strong> den Code der benötigten Bibliotheken<br />
in den Hauptspeicher <strong>und</strong> ersetzt die abstrakten Adressen dort dynamisch. Aus<br />
diesem Gr<strong>und</strong> muß der Lader bei jedem Aufruf des Zielprogramms verwendet werden.<br />
1.5 Aufbau eines Compilers<br />
Die Aufgabe eines Compilers läßt sich zunächst in zwei gr<strong>und</strong>legende Teilaufgaben<br />
zerlegen (Abbildung 1.2): die Analyse des Quellprogramms <strong>und</strong> die Synthese des<br />
Zielprogramms. Beide Aufgaben werden in einer Reihe von Phasen bearbeitet.<br />
1.5.1 Analyse<br />
In den Analysephasen wird das Quellprogramm in seine Bestandteile zerlegt. Dabei<br />
wird eine Überprüfung auf statische (also von der konkreten Eingabe eines Programmablaufs<br />
unabhängige) Korrektheit des zu analysierenden Programmtextes vorgenommen.<br />
Enthält das Programm erkennbare Fehler, werden entsprechende Fehler-<br />
7
1 Einführung<br />
meldungen an den Benutzer ausgegeben. Weiterhin wird eine Zwischendarstellung<br />
des Programms erzeugt, die nur noch die <strong>für</strong> die Synthesephasen benötigten Informationen<br />
des Programmtextes enthält.<br />
Im folgenden erläutern wir die Analysephasen aus Abbildung 1.2 [ASU99] an der<br />
Übersetzung der Zuweisung<br />
position := initial + rate ∗ 60.<br />
Dabei nehmen wir an, daß die Variablen position, initial <strong>und</strong> rate als Fließkomma-<br />
Variablen deklariert sind.<br />
Lexikalische Analyse (scanning): Die lexikalische Analyse dient der Zerlegung des<br />
Zeichenstroms der Eingabe in Symbole. Die Zuweisung wird dabei in folgende Symbole<br />
zerlegt:<br />
1. Bezeichner (position)<br />
2. Zuweisungssymbol<br />
3. Bezeichner (initial)<br />
4. Additionssymbol<br />
5. Bezeichner (rate)<br />
6. Multiplikationssymbol<br />
7. Konstante (60)<br />
Wird als Symbol ein Bezeichner erkannt, wird dieser Bezeichner in die Symboltabelle<br />
des Compilers eingetragen. Jedem Bezeichner wird eine eindeutige Nummer<br />
zugewiesen, in unserem Beispiel der Einfachheit halber gemäß der Reihenfolge des<br />
Auftretens im Quellprogramm. An die nachfolgenden Phasen wird nicht mehr der<br />
Bezeichner selbst, sondern die ihm zugeordnete Nummer weitergegeben (in Abbildung<br />
1.3 [ASU99] ist während der lexikalischen Analyse position durch id1 ersetzt<br />
worden, initial durch id2 <strong>und</strong> rate durch id3).<br />
Das Teilprogramm, das die lexikalische Analyse des Quelltextes durchführt, wird<br />
Scanner genannt.<br />
Syntaktische Analyse (parsing): In der syntaktischen Analyse werden Gruppen<br />
von Symbolen mit hierarchischer Struktur erkannt. Die Quellsprache wird durch die<br />
Regeln einer kontextfreien Grammatik definiert. Anhand der Produktionen dieser<br />
Grammatik wird die von der lexikalischen Analyse gelieferte Symbolfolge auf Korrektheit<br />
überprüft. Dabei wird ein sogenannter Strukturbaum (parse-tree) erzeugt,<br />
der die Analyse des Programmtextes gemäß den Regeln der Grammatik darstellt.<br />
Für die Analyse der Zuweisung nehmen wir die folgende kontextfreie Grammatik an<br />
(kursiv gedruckte Wörter sind Nichtterminalsymbole):<br />
8
id1<br />
id1<br />
position := initial + rate * 60<br />
lexikalische Analyse<br />
id1 := id2 + id3 ∗ 60<br />
syntaktische Analyse<br />
:=<br />
✟ ❛<br />
✟<br />
❛❛<br />
✘✘<br />
+<br />
✘ ����<br />
id2<br />
✦✦*<br />
❳❳❳ ✦ id3<br />
semantische Analyse<br />
:=<br />
60<br />
✟✟✟ ❳❳❳ +<br />
✟ ❳❳❳❳<br />
✟<br />
✟<br />
✟ *<br />
id2<br />
❛❛❛❛<br />
✟<br />
✟<br />
id3<br />
inttoreal<br />
Zwischencode-Erzeugung<br />
temp1 := inttoreal(60)<br />
temp2 := id3 * temp1<br />
temp3 := id2 + temp2<br />
id1 := temp3<br />
Code-Optimierung<br />
temp1 := id3 * 60.0<br />
id1 := id2 + temp1<br />
Code-Generierung<br />
MOVF id3, R2<br />
MULF #60.0, R2<br />
MOVF id2, R1<br />
ADDF R2, R1<br />
MOVF R1, id1<br />
60<br />
1<br />
2<br />
3<br />
1.5 Aufbau eines Compilers<br />
Symboltabelle<br />
position<br />
initial<br />
rate<br />
Abbildung 1.3: Übersetzung einer Zuweisung.<br />
...<br />
...<br />
...<br />
9
1 Einführung<br />
✧<br />
✧ ▲ ❵❵❵❵❵❵❵❵❵❵<br />
▲<br />
✏✏❵❵❵❵❵❵❵❵<br />
✏✏<br />
✏<br />
✟<br />
✟<br />
✟ ❆ Zuweisung<br />
Bezeichner<br />
position<br />
:= Ausdruck<br />
❚<br />
❚<br />
Ausdruck + Ausdruck<br />
❳❳❳❳❳❳ ❆<br />
Bezeichner Ausdruck * Ausdruck<br />
initial<br />
Bezeichner Zahl<br />
rate<br />
Abbildung 1.4: Parse-Baum der Zuweisung.<br />
Zuweisung → Bezeichner := Ausdruck<br />
Ausdruck → Bezeichner | Zahl |<br />
Ausdruck + Ausdruck |<br />
Ausdruck * Ausdruck<br />
Der Strukturbaum der Zuweisungsanweisung gemäß dieser Grammatik ist in Abbildung<br />
1.4 dargestellt.<br />
Der Strukturbaum enthält neben den Terminalsymbolen der Eingabe auch die Nichtterminalsymbole<br />
der Grammatik, die bei der Ableitung der Eingabe verwendet wurden.<br />
Diese Nichtterminalsymbole werden in den weiteren Phasen des Compilers nicht<br />
mehr benötigt. Daher wird als Endprodukt der syntaktischen Analyse ein Syntaxbaum<br />
erzeugt, wie er in Abbildung 1.3 dargestellt ist 2 .<br />
Das Teilprogramm zur syntaktischen Analyse heißt Parser.<br />
Semantische Analyse: Nach der Überprüfung auf syntaktische Korrektheit des<br />
Programms wird in der semantischen Analyse die “statische” Semantik des Quellprogramms<br />
analysiert. “Statisch” bedeutet in diesem Zusammenhang, daß die semantischen<br />
Merkmale untersucht werden, die nicht von den Eingabedaten abhängig<br />
<strong>und</strong> daher <strong>für</strong> alle dynamischen Ausführungen gleich sind. Zur semantischen Analyse<br />
gehören die Überprüfung auf korrekte Typisierung, die Einhaltung von Gültigkeitsbereichen<br />
<strong>und</strong> eventuelle Typanpassungen. Während der semantischen Analyse werden<br />
die Bezeichner in der Symboltabelle mit Attributen versehen. Hierzu gehören z.B. der<br />
Variablentyp <strong>und</strong> der Gültigkeitsbereich der Variablen.<br />
Im Beispiel in Abbildung 1.3 hatten wir angenommen, daß die drei Bezeichner Varia-<br />
2 Im Gegensatz zu unserer Terminologie werden die Begriffe Syntaxbaum <strong>und</strong> Strukturbaum in<br />
10<br />
[WM96] synonym verwendet.<br />
60
1.5 Aufbau eines Compilers<br />
blen vom Typ REAL darstellen. Bei der Typüberprüfung des Programms wird in der<br />
semantischen Analyse festgestellt, daß die ganze Zahl 60 mit dem Inhalt einer REAL-<br />
Variablen multipliziert werden soll. Daher wird in den Syntaxbaum die Information<br />
eingefügt, daß vor der Multiplikation eine Typumwandlung der Zahl vorgenommen<br />
werden muß.<br />
1.5.2 Synthese<br />
In den Synthesephasen wird das zum Quellprogramm gehörende Zielprogramm erzeugt.<br />
Dabei werden die Informationen, die in den Analysephasen über den Programmtext<br />
gesammelt wurden, verwendet.<br />
Zwischencode-Erzeugung: Vor der Erzeugung des eigentlichen Zielprogramms wird<br />
oft eine Zwischendarstellung des Programms generiert, die einerseits bereits maschinennah,<br />
andererseits noch an keiner konkreten Zielmaschine orientiert ist. Diese Zwischensprache<br />
wird Zwischencode genannt.<br />
In Abbildung 1.3 wird als Zwischencode ein Drei-Adreß-Code erzeugt. Jeder Befehl<br />
dieses Codes darf maximal drei Adressen verwenden. Zwei Adressen geben an, wo<br />
sich die Operanden des Befehls befinden. Die dritte Adresse bezeichnet den Speicherplatz,<br />
an dem das Ergebnis des Befehls abgelegt werden soll. Die Speicherzellen an<br />
den Adressen id1, id2 <strong>und</strong> id3 enthalten die Werte der zugehörigen Variablen. Die<br />
Adressen temp1, temp2 <strong>und</strong> temp3 bezeichnen temporäre Speicherplätze <strong>für</strong> Zwischenergebnisse.<br />
Code-Optimierung: Die Verwendung eines maschinenunabhängigen Zwischencodes<br />
bietet den Vorteil, daß auf dem erzeugten Zwischencode eine ebenfalls maschinenunabhängige<br />
Code-Optimierung vorgenommen werden kann. Bei dieser Optimierung<br />
wird der Zwischencode auf Red<strong>und</strong>anzen hin untersucht <strong>und</strong> in bezug auf Laufzeit<br />
<strong>und</strong> Speicherplatzverbrauch verbessert.<br />
Im Beispiel wird in der Code-Optimierung erkannt, daß statt der Umwandlung einer<br />
ganzen Zahl in eine REAL-Zahl gleich die entsprechende Fließkommakonstante im<br />
Code verwendet werden kann. Hierdurch entfallen ein temporärer Speicherplatz <strong>und</strong><br />
eine Konvertierungs-Operation. Außerdem kann das Ergebnis des Additionsbefehls<br />
direkt in id1 gespeichert werden, so daß die letzte Zuweisung entfällt.<br />
Code-Generierung: In dieser letzten Compilerphase wird das Zielprogramm <strong>für</strong><br />
die Zielmaschine erzeugt. Dabei wird jeder Befehl des optimierten Zwischencodes in<br />
eine kurze Sequenz von Maschinenbefehlen übersetzt. Nach Möglichkeit werden die<br />
Speicherplätze des Zwischencodes durch Register der konkreten Maschine ersetzt, um<br />
zeitaufwendige Zugriffe auf den Hauptspeicher zu vermeiden.<br />
11
1 Einführung<br />
Eventuell schließt sich an die Phase der Code-Generierung noch eine maschinenabhängige<br />
Code-Optimierung an, die Ineffizienzen im erzeugten Maschinencode beseitigt<br />
(z.B. überflüssige Kopierbefehle entfernt oder einzelne Maschinenbefehle durch<br />
effizientere Befehle mit derselben Wirkung ersetzt).<br />
1.5.3 Front-End, Back-End<br />
Bei der Einteilung des Compilers in Phasen werden häufig die Begriffe Front-End<br />
<strong>und</strong> Back-End verwendet. Das Front-End eines Compilers umfaßt alle zielsprachenunabhängigen<br />
Compilerphasen, das Back-End entsprechend alle quellsprachenunabhängigen<br />
Phasen des Compilers.<br />
Für die Portierung eines Compilers auf eine andere Zielsprache kann i. allg. das Front-<br />
End unverändert weiterverwendet werden, so daß nur das entsprechende Back-End<br />
neu implementiert werden muß. Im umgekehrten Fall kann die Verbindung mehrerer<br />
Front-Ends mit einem gemeinsamen Back-End sinnvoll sein, um innerhalb eines Programms<br />
Teilprogramme in einer jeweils <strong>für</strong> das Teilproblem optimalen Programmiersprache<br />
zu schreiben <strong>und</strong> aus diesen Teilprogrammen ein gemeinsames Zielprogramm<br />
zu erzeugen.<br />
1.5.4 Läufe<br />
Es ist üblich, mehrere Übersetzungsphasen in einem einzelnen Lauf (pass) zu implementieren.<br />
Ein Lauf steht dabei <strong>für</strong> einen Durchlauf durch eine Darstellung des<br />
Programms. Dabei kann es sich sowohl um den Quelltext als auch um eine interne<br />
Darstellung des Programms wie z.B. den Syntaxbaum handeln. Dabei bietet es<br />
sich an, Phasen, deren Arbeitsschritte eng miteinander verzahnt sind, in einem Lauf<br />
zusammenzufassen. Eine Möglichkeit wäre zum Beispiel die Integration von lexikalischer<br />
<strong>und</strong> syntaktischer Analyse in einem Lauf sowie der semantischen Analyse <strong>und</strong><br />
der Codegenerierung in einem zweiten Lauf.<br />
Einen Extremfall stellt der Ein-Pass-Compiler dar, der die Analyse des Quellprogramms<br />
<strong>und</strong> die Synthese des Zielprogramms während eines einzigen Durchlaufs<br />
durch den Programmtext durchführt. In diesem Fall muß gewährleistet sein, daß<br />
jeder Bezeichner vor seiner Verwendung deklariert wurde, da nachträgliche Änderungen<br />
am Zielprogramm nicht mehr möglich sind. Aus diesem Gr<strong>und</strong> ist in vielen<br />
Compilern <strong>für</strong> die Sprache Pascal [JW91] die Vordeklaration von Bezeichnern mit<br />
der forward-Anweisung vorgesehen.<br />
Andere Programmiersprachen wie z.B. Algol-68 [OT97] erlauben die Verwendung von<br />
Bezeichnern vor ihrer Deklaration, so daß <strong>für</strong> diese Sprachen die Implementierung<br />
mittels eines Ein-Pass-Compilers nicht möglich ist.<br />
Bei der Implementierung einer Sprache mittels einer virtuellen Maschine gilt: Der<br />
12
1.5 Aufbau eines Compilers<br />
vom Compiler erzeugte Code ähnelt dem idealisierten Zwischencode, der durch die<br />
virtuelle Maschine interpretiert wird, also auf Instruktionen der realen Hardware<br />
zum Zeit der Ausführung abgebildet wird. Somit kann die virtuelle Maschine auch<br />
als Middle-End bezeichnet werden. Das bedeutet in der Regel aber nicht, dass bei<br />
dieser Variante die Phase der Zwischencode-Erzeugung entfällt. Vielmehr existiert im<br />
Allgemeinen eine weitere Zwischendarstellung des Programms zwischen semantischer<br />
Analyse <strong>und</strong> dem Code <strong>für</strong> die virtuelle Maschine, auf deren Gr<strong>und</strong>lage Optimierungen<br />
unabhängig von der VM durchgeführt werden können.<br />
13
2 Lexikalische Analyse<br />
Die lexikalische Analyse arbeitet als erste Phase des Compilers direkt mit dem zu<br />
übersetzenden Programmtext (siehe Abbildung 2.1).<br />
Der Programmteil zur Durchführung der lexikalischen Analyse wird Scanner genannt.<br />
Der Scanner erfüllt die folgenden Aufgaben:<br />
• Das Quellprogramm wird zeichenweise gelesen <strong>und</strong> dabei in Symbole zerlegt.<br />
Bei dieser Zerlegung werden Leerzeichen, Kommentare, Zeilenenden etc. entfernt,<br />
so daß sie in den weiteren Compilerphasen nicht mehr beachtet werden<br />
müssen.<br />
• Die Bezeichner des Programms werden in der Reihenfolge ihres Auftretens im<br />
Quelltext mit erläuternden Informationen in die Symboltabelle eingefügt.<br />
• Für die eventuelle Ausgabe von Fehlermeldungen werden Informationen gesammelt<br />
(z.B. Zeilennummern).<br />
Ein wichtiger Gesichtspunkt bei der Realisierung eines Scanners ist Effizienz, da die<br />
nachfolgenden Phasen des Compilers direkt vom Scanner abhängig sind <strong>und</strong> deren<br />
Laufzeit daher durch einen langsamen Scanner negativ beeinflußt wird. Meist wird<br />
der Scanner als Unterprogramm des Parsers realisiert (eventuell als Coroutine). Der<br />
Scanner liefert jeweils nach Aufforderung durch den Parser ein Symbol.<br />
2.1 Terminologie<br />
In diesem Abschnitt führen wir die Begriffe Symbol, Muster <strong>und</strong> Lexem ein.<br />
Quellprogramm<br />
Symbol<br />
Scanner Parser<br />
nächstes Symbol<br />
anfordern<br />
Symboltabelle<br />
Abbildung 2.1: Interaktion zwischen Scanner <strong>und</strong> Parser.
2.2 Reguläre Sprachen <strong>und</strong> endliche Automaten<br />
Symbole sind die vom Scanner an den Parser zu liefernden Gr<strong>und</strong>einheiten der Programmiersprache.<br />
Mengen von gleichartigen Symbolen nennen wir Symbolklassen.<br />
Typische Symbolklassen sind die Menge der Integer-Konstanten <strong>und</strong> die Menge der<br />
Zeichenketten.<br />
Muster beschreiben die möglichen Auftreten eines Symbols im Quellprogramm. Die<br />
Zeichenfolgen im Programmtext, die Symbolen entsprechen, nennen wir Lexeme.<br />
Beispiele <strong>für</strong> Symbole <strong>und</strong> die zugehörigen Muster <strong>und</strong> Lexeme sind in Tabelle 2.1<br />
angeführt.<br />
Symbol Musterbeschreibung mögliche Lexeme<br />
if if if<br />
id Buchstabe, gefolgt von pi, D2<br />
Buchstaben oder Ziffern<br />
Tabelle 2.1: Beispiele <strong>für</strong> Symbole, Muster <strong>und</strong> Lexeme.<br />
2.2 Reguläre Sprachen <strong>und</strong> endliche Automaten<br />
Gr<strong>und</strong>lage <strong>für</strong> die lexikalische Analyse ist die Theorie der regulären Sprachen.<br />
Wir wiederholen zunächst einige wichtige Gr<strong>und</strong>begriffe der formalen Sprachen.<br />
Ein Alphabet Σ ist eine endliche Menge von Zeichen; z.B. {0, 1},<br />
{0,..., 9,A,...,Z} oder der ASCII-Zeichensatz.<br />
Ein Wort über einem Alphabet Σ ist eine endliche Folge von Zeichen aus dem Alphabet;<br />
z.B. 01001, A195, goltz@ips.cs.tu-bs.de. Das leere Wort bezeichnen wir mit<br />
ε. Die Menge aller Wörter über einem Alphabet Σ bezeichnen wir mit Σ ∗ .<br />
Eine Sprache über einem Alphabet ist eine Menge von Wörtern über dem Alphabet,<br />
z.B. ∅, {ε}, {A,B,C,AB,AC,ABC} sowie die Menge aller syntaktisch wohlgeformten<br />
Modula 2-Programme.<br />
Seien v <strong>und</strong> w Wörter über dem Alphabet Σ. Die Konkatenation von v <strong>und</strong> w,<br />
geschrieben vw, ist dasjenige Wort, das durch das Anhängen von w an v ensteht. Für<br />
v = compiler <strong>und</strong> w = bau ergibt sich als Konkatenation vw das Wort compilerbau.<br />
Es gilt εw = wε = w <strong>für</strong> beliebige Wörter w.<br />
Die Exponentiation von Wörtern ist wie folgt definiert:<br />
• w 0 = ε<br />
• w i = w i−1 w <strong>für</strong> i > 0. Es gilt w 1 = w.<br />
Operationen auf Sprachen: Seien L,M Sprachen. Dann sind die folgenden Operationen<br />
definiert:<br />
15
2 Lexikalische Analyse<br />
Vereinigung: L ∪ M := {w | w ∈ L ∨ w ∈ M}<br />
Konkatenation: LM := {vw | v ∈ L ∧ w ∈ M}<br />
Exponentiation: L 0 := {ε},<br />
L i := L i−1 L <strong>für</strong> i > 0<br />
Kleene-Abschluß: L ∗ := � ∞ i=0 L i<br />
Positiver Abschluß: L + := � ∞ i=1 L i<br />
Beispiel 1<br />
Seien L = {A,B,...,Z,a,b,...,z} <strong>und</strong> D = {0, 1,..., 9} Sprachen mit Wörtern der<br />
Länge 1. Dann ist<br />
L ∪ D die Sprache der Buchstaben <strong>und</strong> Ziffern,<br />
LD die Sprache, die lauter Wörter der Form Buchstabe Ziffer<br />
enthält,<br />
L 4 die Sprache aller Wörter mit genau vier Buchstaben über L,<br />
L ∗ die Sprache aller beliebig langen Wörter aus Buchstaben<br />
(inkl. ε),<br />
L((L ∪ D) ∗ ) die Sprache aller Wörter aus Buchstaben <strong>und</strong> Ziffern,<br />
die mit einem Buchstaben beginnen,<br />
D + die Sprache aller nicht-leeren Wörter aus Ziffern.<br />
2.2.1 Reguläre Sprachen<br />
Sei Σ Alphabet.<br />
Definition 1<br />
Die regulären Sprachen über Σ sind induktiv definiert durch<br />
• ∅ ist reguläre Sprache,<br />
• <strong>für</strong> alle a ∈ Σ ist {a} reguläre Sprache,<br />
• falls L1,L2 reguläre Sprachen, so sind auch L1 ∪ L2, L1L2 <strong>und</strong> L ∗ 1 reguläre<br />
Sprachen.<br />
Nichts sonst ist eine reguläre Sprache über Σ.<br />
Bemerkung: {ε} wird durch den ∗ -Operator aus ∅ gewonnen. Also ist {ε} regulär.<br />
2.2.2 Reguläre Ausdrücke<br />
Reguläre Ausdrücke sind spezielle Formeln, mit denen reguläre Sprachen definiert<br />
werden.<br />
Definition 2<br />
Die Menge der regulären Ausdrücke über Σ, reg(Σ), ist induktiv definiert durch<br />
16
2.2 Reguläre Sprachen <strong>und</strong> endliche Automaten<br />
• ∅ ∈ reg(Σ),<br />
• ε ∈ reg(Σ),<br />
• <strong>für</strong> jedes a ∈ Σ ist a ∈ reg(Σ),<br />
• falls r1,r2 ∈ reg(Σ), dann (r1|r2) ∈ reg(Σ), (r1r2) ∈ reg(Σ) <strong>und</strong> (r1) ∗ ∈<br />
reg(Σ).<br />
Bemerkung: Die Zeichen (, ), |, ∗ in regulären Ausdrücken sind Metazeichen. Sie sind<br />
keine Elemente des Alphabets Σ, sondern dienen als Operatoren zur Bildung der<br />
regulären Ausdrücke. Die Metazeichen müssen von den Zeichen des Alphabets zu<br />
unterscheiden sein, damit die von dem regulären Ausdruck beschriebene Sprache<br />
eindeutig zu bestimmen ist. Sind die Metazeichen im Alphabet Σ enthalten, wird<br />
die hieraus resultierende Doppeldeutigkeit durch eine spezielle Kennzeichnung der<br />
Metazeichen vermieden (siehe Beispiel 2).<br />
Die Sprache, die von einem regulären Ausdruck definiert wird, wird in der folgenden<br />
Definition eingeführt.<br />
Definition 3<br />
Sei r regulärer Ausdruck. Die Sprache L(r) ist induktiv definiert durch<br />
• L(∅) = ∅<br />
• L(ε) = {ε}<br />
• L(a) = {a}<br />
• L(( r1 | r2)) = L(r1) ∪ L(r2),<br />
L((r1 r2)) = L(r1)L(r2),<br />
L((r1) ∗ ) = (L(r1)) ∗<br />
Bemerkung: Es gilt: r ∈ reg(Σ) ⇔ L(r) ist reguläre Sprache.<br />
Wir verdeutlichen die regulären Ausdrücke anhand von Beispielen.<br />
Beispiel 2<br />
• a | b beschreibt {a} ∪ {b} = {a,b}<br />
• (a b) ∗ beschreibt ({a}{b}) ∗ = {ab} ∗ = {ε,ab,abab,...}<br />
• (A|...|Z|a|...|z) beschreibt {A,...,Z,a,...,z}<br />
• Sei Σ = {(, )}. Die Zeichen des Alphabets sind in den Metazeichen regulärer<br />
Ausdrücke enthalten. Daher kennzeichen wir die Metazeichen durch Unterstreichung.<br />
Damit beschreibt ( ( ) ∗ ) die Sprache, deren Wörter mit beliebig vielen<br />
öffnenden Klammern beginnen <strong>und</strong> mit einer schließenden Klammer enden:<br />
{), (), ((), (((),...}<br />
17
2 Lexikalische Analyse<br />
Konventionen: Um bei der Angabe regulärer Ausdrücke Klammern zu sparen <strong>und</strong><br />
Mehrdeutigkeiten zu vermeiden, ordnen wir den Operatoren dieser Ausdrücke Prioritäten<br />
zu.<br />
• • hat die höchste Priorität, so daß a|b ∗ <strong>und</strong> (a|b) ∗ unterschiedliche Sprachen<br />
beschreiben. Zudem ist der ∗-Operator linksassoziativ, d.h. a ∗∗ = (a ∗ ) ∗ .<br />
• Die Konkatenation besitzt die zweithöchste Priorität <strong>und</strong> ist ebenfalls linksassoziativ.<br />
• | hat die niedrigste Priorität ( (a|b) ∗ c vs. a|b ∗ c ) <strong>und</strong> ist ebenfalls linksassoziativ.<br />
Bemerkung: Unterschiedliche reguläre Ausdrücke können dieselbe Sprache beschreiben.<br />
So ist L((a|b)(a|b)) = {aa,ab,ba,bb} = L(aa|ab|ba|bb).<br />
Algebraische Eigenschaften: Für die Operatoren | <strong>und</strong> Konkatenation gelten die<br />
folgenden algebraischen Eigenschaften, wobei wir reguläre Ausdrücke genau dann<br />
gleichsetzen, wenn sie dieselbe Sprache beschreiben (r = s bedeutet L(r) = L(s)):<br />
• r|s = s|r (Kommutativität von |)<br />
• r|(s|t) = (r|s)|t (Assoziativität von |)<br />
• r(st) = (rs)t (Assoziativität der Konkatenation)<br />
2.2.3 Endliche Automaten<br />
Nach der Einführung der regulären Sprachen <strong>und</strong> der regulären Ausdrücke in den<br />
vorigen Abschnitten geben wir nun einen Mechanismus zur Erkennung von Wörtern<br />
regulärer Sprachen an. Hierzu verwenden wir die endlichen Automaten.<br />
Definition 4<br />
Ein nichtdeterministischer endlicher Automat (NEA) ist ein Tupel M = (Σ,Q, ∆,q0,F),<br />
wobei<br />
• Σ endliches Alphabet (das Eingabealphabet),<br />
• Q endliche Menge (von Zuständen),<br />
• q0 ∈ Q (der Anfangszustand),<br />
• F ⊆ Q (die Menge der Endzustände) <strong>und</strong><br />
• ∆ ⊆ Q × (Σ ∪ {ε}) × Q (die Übergangsrelation) ist.<br />
Definition 5<br />
Sei M = (Σ,Q, ∆,q0,F) ein NEA.<br />
Ein Paar (q,w),q ∈ Q,w ∈ Σ ∗ heißt Konfiguration von M, (q0,w) heißt Anfangskonfiguration,<br />
(qf,ε) mit qf ∈ F Endkonfiguration.<br />
Die Schritt-Relation ist eine binäre Relation ⊢M⊆ (Q × Σ ∗ ) × (Q × Σ ∗ ), definiert<br />
durch<br />
18
2.2 Reguläre Sprachen <strong>und</strong> endliche Automaten<br />
(q,aw) ⊢M (q ′ ,w) :⇔ (q,a,q ′ ) ∈ ∆ <strong>für</strong> q,q ′ ∈ Q <strong>und</strong> a ∈ Σ oder a = ε.<br />
⊢ ∗ M sei die reflexive transitive Hülle von ⊢M.<br />
Die von M akzeptierte Sprache ist L(M) = {w ∈ Σ ∗ | (q0,w) ⊢ ∗ M (qf,ε),<br />
qf ∈ F }.<br />
Ein endlicher Automat soll ein Eingabewort daraufhin überprüfen, ob es zu einer<br />
bestimmten Sprache gehört. Dabei wird die Eingabe von links nach rechts zeichenweise<br />
gelesen. Zu Beginn befindet sich der Automat im Anfangszustand q0 <strong>und</strong> der<br />
Eingabezeiger zeigt auf das erste Zeichen des Eingabewortes. Nach dem Lesen eines<br />
Zeichens wird das entsprechende Zeichen aus der Eingabe entfernt <strong>und</strong> der Automat<br />
geht in Abhängigkeit vom gelesenen Zeichen mittels der Übergangsrelation in einen<br />
neuen Zustand über. Weiterhin ist der Übergang in einen anderen Zustand ohne das<br />
Lesen eines Eingabezeichens möglich (ε-Übergang). Ein Übergang eines Automaten<br />
in einen anderen Zustand wird Schritt genannt. Ist die Eingabe vollständig gelesen<br />
<strong>und</strong> der Automat befindet sich in einem Endzustand, wird das gelesene Wort<br />
akzeptiert. Befindet sich der Automat nach dem vollständigen Lesen der Eingabe<br />
nicht in einem Endzustand oder ist in einem Zustand kein Übergang <strong>für</strong> das nächste<br />
Eingabezeichen möglich, wird das Eingabewort verworfen.<br />
Das Verhalten eines NEA wird also in jedem Schritt durch den aktuellen Zustand<br />
des Automaten <strong>und</strong> die restliche Eingabe bestimmt. Diese beiden Faktoren bilden<br />
zusammen die aktuelle Konfiguration des endlichen Automaten. Die Übergänge<br />
zwischen Konfigurationen werden durch die Schritt-Relation beschrieben.<br />
Der Automat erkennt die Worte, <strong>für</strong> die er durch eine Folge von Schritten aus der Anfangskonfiguration<br />
eine Endkonfiguration erreichen kann. Die Menge der von einem<br />
NEA erkannten Worte bildet die von ihm akzeptierte Sprache.<br />
Graphische Darstellung: Zur Verbesserung der Übersichtlichkeit werden NEAs<br />
durch Übergangsgraphen dargestellt. Die Knoten des Graphen repräsentieren die<br />
Zustände des Automaten. Die Kanten stellen die Zustandsübergänge des Automaten<br />
dar <strong>und</strong> sind mit dem Zeichen beschriftet, das während des Übergangs gelesen wurde<br />
(bzw. mit ε, falls kein Zeichen gelesen wurde).<br />
Beispiel 3<br />
Der Übergangsgraph in Abbildung 2.2 stellt einen NEA dar, der die Sprache L((a|b) ∗ abb) =<br />
{abb,aabb,babb,aaabb,ababb,...} akzeptiert.<br />
Ein NEA akzeptiert ein Eingabewort w genau dann, wenn es im Übergangsgraphen<br />
einen Pfad vom Startzustand in einen Endzustand gibt, so daß die gelesenen Eingabesymbole<br />
die Kanten des Pfades beschriften.<br />
Die Übergangsrelation ∆ eines NEA kann in Form einer Tabelle dargestellt werden.<br />
Die Tabelle 2.2 enthält die Übergangsrelation des in Abbildung 2.2 dargestellten<br />
Automaten.<br />
19
2 Lexikalische Analyse<br />
a<br />
a b b<br />
0 1 2 3<br />
b<br />
Abbildung 2.2: Beispiel eines Übergangsgraphen.<br />
Zustand / Eingabe a b ε<br />
0 {0, 1} {0} -<br />
1 - {2} -<br />
2 - {3} -<br />
3 - - -<br />
Tabelle 2.2: Übergangsrelation ∆ in Tabellenform.<br />
Satz 1<br />
Zu jedem regulären Ausdruck r gibt es einen nichtdeterministischen endlichen Automaten,<br />
der die von r beschriebene reguläre Sprache akzeptiert.<br />
Beweis<br />
Wir führen den Beweis konstruktiv durch, indem wir <strong>für</strong> jeden regulären Ausdruck<br />
eine Überführung in entsprechende “Automaten” angeben, wobei Kanten zunächst<br />
mit regulären Ausdrücken beschriftet sein dürfen. Handelt es sich bei dem Ausdruck r<br />
um ∅, der die leere Sprache beschreibt, besteht der Automat aus nur einem Zustand,<br />
der zugleich Endzustand ist, <strong>und</strong> enthält keine Übergänge. Andernfalls beginnen wir<br />
mit einem Graphen <strong>für</strong> den regulären Ausdruck r, wie er in in Abbildung 2.3 oben<br />
angegeben ist. Die Überführungsschritte <strong>für</strong> die einzelnen Operatoren sind in Abbildung<br />
2.3 aufgeführt. r,r1,r2 sind reguläre Ausdrücke. (A) beschreibt die Behandlung<br />
der Alternative, (K) der Konkatenation, (S) des Stern-Operators <strong>und</strong> (KL) die Behandlung<br />
von Klammern. �<br />
Beispiel 4<br />
In Abbildung 2.4 wird schrittweise der Automat <strong>für</strong> den regulären Ausdruck a(a|0) ∗<br />
konstruiert. Neben den einzelnen Konstruktionsschritten ist die Regel aus Abbildung<br />
2.3 angegeben, die in diesem Schritt verwendet wurde.<br />
20
q♥ r<br />
✓✏<br />
p♥<br />
✒✑<br />
r1|r2<br />
q♥ ♥p � q♥ ♥p<br />
r1r2<br />
q♥ p♥ � q♥ q1 ♥ ♥p<br />
2.2 Reguläre Sprachen <strong>und</strong> endliche Automaten<br />
♥ r<br />
q ♥p ♥q q1 ♥ q2 ♥ p♥<br />
∗ ε ε<br />
�<br />
♥ (r)<br />
q ♥p q♥ r<br />
�<br />
p♥<br />
r1<br />
r2<br />
r1<br />
ε<br />
r<br />
ε<br />
r2<br />
(A)<br />
(K)<br />
(S)<br />
(KL)<br />
Abbildung 2.3: Konstruktion eines NEA zu einem regulärem Ausdruck.<br />
✓✏<br />
♠a(a|0) 0 1♠<br />
✒✑<br />
∗<br />
✓✏<br />
♠ ♠ (a|0)<br />
0 2 1♠<br />
✒✑<br />
∗<br />
a<br />
0♠ a ♠ε 2<br />
(a|0)<br />
3♠ ε<br />
4♠ ε<br />
✓✏<br />
1♠<br />
✒✑<br />
0♠ a ♠ε 2<br />
a<br />
3♠ 0<br />
ε<br />
4♠ ε<br />
✓✏<br />
1♠<br />
✒✑<br />
ε<br />
ε<br />
(K)<br />
(S)<br />
(KL),(A)<br />
Abbildung 2.4: Beispiel einer NEA-Konstruktion.<br />
21
2 Lexikalische Analyse<br />
Da es sich bei dem mit dem Verfahren erzeugten Automaten um einen nichtdeterministischen<br />
endlichen Automaten handelt, ist eine direkte Umsetzung des Automaten<br />
in ein Programm aufgr<strong>und</strong> des Nichtdeterminismus nicht ohne weiteres möglich.<br />
Aus der Theorie der formalen Sprachen ist bekannt, daß es zu jedem NEA einen<br />
deterministischen endlichen Automaten (DEA) gibt, der dieselbe Sprache erkennt.<br />
Definition 6<br />
Sei M = (Q, Σ, ∆,q0,F) ein NEA. M heißt deterministischer endlicher Automat<br />
(DEA), wenn ∆ eine Funktion σ : Q × Σ → Q ist.<br />
In einem DEA treten keine ε-Übergänge auf. Weiterhin gibt es <strong>für</strong> jeden Zustand<br />
unter jeder Eingabe höchstens einen Folgezustand.<br />
Satz 2<br />
Wird eine Sprache L von einem NEA akzeptiert, so gibt es einen DEA, der L akzeptiert.<br />
Beweis<br />
Der Beweis wird konstruktiv geführt, indem wir ein Verfahren angeben, das zu einem<br />
NEA einen DEA generiert, der dieselbe Sprache erkennt. Dieses Verfahren wird<br />
Potenzmengenkonstruktion genannt.<br />
Die Potenzmengenkonstruktion verwendet die beiden folgenden Definitionen:<br />
Definition 7<br />
Sei M = (Q, Σ, ∆,q0,F) ein NEA <strong>und</strong> sei q ∈ Q. Die Menge der ε-Folgezustände<br />
von q ist<br />
ε − FZ(q) = {p | (q,ε) ⊢ ∗ M (p,ε)},<br />
also die Menge aller Zustände p, inklusive q, <strong>für</strong> die es einen ε-Weg im Übergangsgraphen<br />
zu M von q nach p gibt. Wir erweitern ε − FZ auf Mengen von Zuständen<br />
S ⊆ Q:<br />
ε − FZ(S) = �<br />
ε − FZ(q).<br />
Definition 8<br />
Sei M = (Q, Σ, ∆,q0,F) ein NEA.<br />
Der zu M gehörende DEA M ′ = (Q ′ , Σ,δ,q ′ 0,F ′ ) ist definiert durch:<br />
Q ′ = P(Q), die Potenzmenge von Q,<br />
q ′ 0 = ε − FZ(q0),<br />
F ′ = {S ∈ Q ′ | S ∩ F �= ∅} <strong>und</strong><br />
δ(S,a) = ε − FZ({p | (q,a,p) ∈ ∆ <strong>für</strong> q ∈ S}) <strong>für</strong> a ∈ Σ,S ∈ Q ′ .<br />
22<br />
q∈S
2.2 Reguläre Sprachen <strong>und</strong> endliche Automaten<br />
Der folgende Algorithmus konstruiert zu einem NEA M den zu M gehörenden DEA<br />
M ′ , wobei nicht erreichbare Zustände weggelassen werden.<br />
Algorithmus NEA nach DEA<br />
Eingabe: NEA M = (Q, Σ, ∆,q0,F)<br />
Ausgabe: DEA M ′ = (Q ′ , Σ,δ,q ′ 0,F ′ )<br />
1 q ′ 0 := ε − FZ(q0); Q ′ := {q ′ 0};\\<br />
2 marked(q ′ 0) := false; δ := ∅;\\<br />
3 while e x i s t i e r t S ǫ Q ′ and marked(S) = false do<br />
4 marked(S) := true;<br />
5 foreach a ǫ Σ do<br />
6 T := ε − FZ({p ǫ Q | (q,a,p) ǫ ∆ <strong>und</strong> q ǫ S})<br />
7 if T /∈ Q ′ then<br />
8 Q ′ := Q ′ ∪ {T }; (∗ neuer Zustand ∗)<br />
9 marked(T) := false<br />
10 fi ;\\<br />
11 δ := δ ∪ {(S,a) ↦→ T } (∗ neuer ”Ubergang ∗)<br />
12 od<br />
13 od<br />
Die Zustände von M ′ sind Mengen von Zuständen von M (daher der Name Potenzmengenkonstruktion).<br />
Zwei Zustände p <strong>und</strong> q von M fallen in dieselbe Zustandsmenge<br />
S (also in denselben Zustand von M ′ ), wenn es ein Wort w gibt, welches den NEA M<br />
sowohl nach p als auch nach q bringt. Nach Definition 8 erhält man den Folgezustand<br />
eines Zustands S in M ′ unter einem Zeichen a, indem man die Nachfolgezustände<br />
aller Zustände q ∈ S unter a zusammenfaßt <strong>und</strong> deren ε-Folgezustände hinzufügt.<br />
Wir verdeutlichen die Arbeitsweise der Potenzmengenkonstruktion, indem wir <strong>für</strong> den<br />
in Abbildung 2.4 erzeugten NEA einen DEA generieren, der ebenfalls die durch den<br />
regulären Ausdruck a(a|0) ∗ beschriebene Sprache erkennt. In Abbildung 2.5 [WM96]<br />
sind die einzelnen Schritte des Verfahrens dargestellt. Die Zustände des zu konstruierenden<br />
DEA sind mit 0 ′ , 1 ′ , 2 ′ <strong>und</strong> ⊘ benannt, wobei 0 ′ der Anfangszustand ist. ⊘ ist<br />
ein “Fehlerzustand”, der als Folgezustand eines Zustands q unter a verwendet wird,<br />
wenn es keinen Übergang im NEA unter a aus q heraus gibt. Sind <strong>für</strong> einen Zustand<br />
des DEA <strong>für</strong> alle möglichen Zeichen aus Σ die entsprechenden Nachfolgezustände des<br />
DEA berechnet, wird der Zustand markiert (in Abbildung 2.5 durch Unterstreichung<br />
dargestellt) <strong>und</strong> braucht nicht weiter behandelt zu werden. Endzustände des DEA<br />
sind die Zustände, in deren Menge von Zuständen des NEA ein Endzustand auftritt<br />
(1 ′ <strong>und</strong> 2 ′ sind Endzustände, da sie den NEA-Endzustand 1 beinhalten).<br />
�<br />
23
2 Lexikalische Analyse<br />
24<br />
0 ′ = {0}; Q ′ = {0 ′ }<br />
ausgewählter Zustand neues Q<br />
✗✔<br />
✎☞<br />
✎☞✘✘✘✘<br />
✖✕<br />
✍✌<br />
✍✌❍<br />
❍❍❍ ✎☞<br />
✍✌<br />
✗✔<br />
✎☞<br />
✎☞✘✘✘✘<br />
✖✕ ✍✌<br />
✍✌❝<br />
❝❝❝<br />
✎☞<br />
✗✔<br />
✎☞<br />
✖✕ ✍✌<br />
✍✌<br />
′ neuer (Teil-) DEA<br />
0 ′<br />
0 ′<br />
1 ′<br />
1 ′ 2 ′<br />
0<br />
a<br />
0 ⊘<br />
a<br />
a<br />
0<br />
0<br />
⊘<br />
a a<br />
′ {0 ′ , 1 ′ , ⊘} mit<br />
1 ′ = {1, 2, 3}<br />
1 ′ {0 ′ , 1 ′ , 2 ′ , ⊘} mit<br />
2 ′ = {1, 3, 4}<br />
2 ′ {0 ′ , 1 ′ , 2 ′ , ⊘}<br />
⊘ {0 ′ , 1 ′ , 2 ′ , ⊘}<br />
✗✔ ✎☞<br />
✎☞✘✘✘✘ ✖✕ ✍✌<br />
✍✌ 0<br />
❅<br />
❅❅❅<br />
✎☞<br />
✗✔ ✎☞<br />
✖✕ ✍✌<br />
✍✌<br />
✗✔ ✎☞<br />
✎☞✘✘✘✘ ✖✕ ✍✌<br />
✍✌<br />
❅<br />
❅❅❅<br />
✎☞<br />
✍✌<br />
✗✔ ✎☞<br />
✖✕ ✍✌<br />
′<br />
0 ′<br />
1 ′<br />
1 ′<br />
2 ′<br />
2 ′<br />
a<br />
0<br />
0<br />
0<br />
⊘ a<br />
a<br />
0<br />
0<br />
⊘<br />
a<br />
0<br />
Abbildung 2.5: Beispiel zur Potenzmengenkonstruktion.<br />
a<br />
0
2.2 Reguläre Sprachen <strong>und</strong> endliche Automaten<br />
Da der durch das Verfahren erzeugte DEA i. allg. nicht der “kleinstmögliche” Automat<br />
ist, wird im Anschluß an die Potenzmengenkonstruktion ein weiteres Verfahren,<br />
genannt Minimalisierung, durchgeführt, das zu dem erzeugten DEA einen weiteren<br />
DEA konstruiert, der dieselbe Sprache akzeptiert <strong>und</strong> über eine minimale Zustandsmenge<br />
verfügt. In dem durch die Potenzmengenkonstruktion erzeugten DEA kann<br />
es noch Zustände mit gleichem “Akzeptanzverhalten” geben. Zwei Zustände p <strong>und</strong> q<br />
besitzen das gleiche Akzeptanzverhalten, wenn der Automat aus p <strong>und</strong> q unter allen<br />
Eingabewörtern entweder immer oder nie in einen Endzustand geht. Die Minimalisierung<br />
beruht auf dem Zusammenfassen dieser Zustände zu einem gemeinsamen<br />
Zustand.<br />
Der folgende Algorithmus beschreibt das Verfahren der Minimalisierung.<br />
Algorithmus Minimalisierung.<br />
Eingabe: DEA M = (Q, Σ,δ,q0,F).<br />
Ausgabe: DEA Mmin = (Qmin, Σ,δmin,q0,min,Fmin) mit Qmin minimal.<br />
Methode: Die Zustandsmenge von M wird in eine Partition aufgeteilt, die schrittweise<br />
verfeinert wird. Für Zustände in verschiedenen Klassen einer Partition<br />
ist schon bekannt, daß sie verschiedenes Akzeptanzverhalten zeigen, d.h. daß es<br />
mindestens ein Wort w gibt, unter welchem aus einem der Zustände ein Endzustand<br />
erreicht wird <strong>und</strong> unter dem anderen nicht. Deshalb beginnt man mit der<br />
Partition Π = {F,Q − F }. In jedem Iterationsschritt des Verfahrens wird eine<br />
Klasse K aus der Partition entfernt <strong>und</strong> durch eine Menge von Verfeinerungen<br />
Ki ersetzt. Dabei gilt, daß <strong>für</strong> alle Zustände q in einer Verfeinerung Ki der<br />
Nachfolgezustand δ(q,a) unter einem Zeichen a in einer gemeinsamen Klasse<br />
K ′ i liegt. Wird eine solche Verfeinerung gef<strong>und</strong>en, wird die Variable changed auf<br />
true gesetzt. Der Algorithmus hält, wenn in einem Schritt die Partition nicht<br />
mehr verfeinert wurde. Da in jedem Iterationsschritt nur Klassen der aktuellen<br />
Partition evtl. in Vereinigungen neuer Klassen zerlegt werden, Q <strong>und</strong> damit<br />
auch P(Q) aber endlich sind, terminiert das Verfahren. Die Klassen der dann<br />
gef<strong>und</strong>enen Partition sind die Zustände von Mmin. Es gibt einen Übergang zwischen<br />
zwei neuen Zuständen P <strong>und</strong> R unter einem Zeichen a ∈ Σ, wenn es einen<br />
Übergang δ(p,a) = r mit p ∈ P <strong>und</strong> r ∈ R in M gab.<br />
1 Π = {F,Q − F };<br />
2 do changed := false ;<br />
3 Π ′ := Π;<br />
4 foreach K ∈ Π do<br />
5 Π ′ := (Π ′ − {K}) ∪ {{Ki}1≤i≤n} , <strong>und</strong> die Ki sind maximal mit<br />
6 K = �<br />
1≤i≤n Ki , <strong>und</strong> ∀a ∈ Σ : ∃K ′ i ∈ Π : ∀q ∈ Ki : δ(q,a) ∈ K ′ i<br />
7 if n > 1 then changed := true fi (∗ K wurde<br />
aufgespalten ∗)<br />
25
2 Lexikalische Analyse<br />
✎ ☞<br />
✍{0<br />
✌<br />
′ }<br />
a<br />
✗<br />
✎ ☞<br />
✔a<br />
✖<br />
✍{1<br />
✌<br />
✕<br />
′ , 2 ′ }<br />
Abbildung 2.6: DEA mit minimaler Zustandsmenge.<br />
Partition Klasse Aufspaltung<br />
{{0 ′ , ⊘}, {1 ′ , 2 ′ }}<br />
{0 ′ , ⊘} {0 ′ }, {⊘}<br />
{1 ′ , 2 ′ } nein<br />
{{0 ′ }, {⊘}, {1 ′ , 2 ′ }} keine weitere Aufspaltung<br />
{1 ′ , 2 ′ } bilden zusammen einen neuen Zustand<br />
{⊘} ist ein toter Zustand, da er nicht Endzustand ist <strong>und</strong> alle<br />
Übergänge aus ihm hinaus wieder in ihn hineingehen.<br />
8 od;<br />
9 Π := Π ′<br />
Abbildung 2.7: Beispiel zur Minimalisierung.<br />
10 until not changed ;<br />
11 Qmin = Π−(Tot ∪ Unerreichbar ) ;<br />
q0,min<br />
die Klasse in Π, in der q0 ist.<br />
die Klassen, die ein Element aus F enthalten.<br />
Fmin<br />
δmin(K,a) = K ′ wenn δ(q,a) = p mit q ∈ K <strong>und</strong> p ∈ K ′<br />
<strong>für</strong> ein <strong>und</strong> damit <strong>für</strong> alle a ∈ K.<br />
K ∈ Tot wenn K kein Endzustand ist <strong>und</strong> nur Übergänge<br />
in sich selbst enthält.<br />
K ∈ Unerreichbar wenn es keinen Weg vom Anfangszustand nach K gibt.<br />
Die Anwendung des Verfahrens der Minimalisierung auf den in Abbildung 2.5 konstruierten<br />
DEA liefert den in Abbildung 2.6 angegebenen DEA mit minimaler Zustandsmenge.<br />
In Abbildung 2.7 sind die dabei aufgetretenen Iterationsschritte angegeben.<br />
Mit Hilfe dieser drei Verfahren ist die Implementierung eines Scanners möglich:<br />
26<br />
1. Zuerst wird die zu erkennende Sprache, also die Menge der möglichen Lexeme<br />
<strong>für</strong> Symbole, durch reguläre Ausdrücke beschrieben.<br />
2. Im nächsten Schritt wird gemäß Satz 1 ein NEA konstruiert, der die von den<br />
regulären Ausdrücken beschriebene Sprache akzeptiert.<br />
0
2.2 Reguläre Sprachen <strong>und</strong> endliche Automaten<br />
3. Anschließend wird mit dem Verfahren der Potenzmengenkonstruktion ein DEA<br />
erzeugt, der dieselbe Sprache wie der NEA erkennt.<br />
4. Mit Hilfe des Verfahrens der Minimalisierung wird ein weiterer DEA konstruiert,<br />
der dieselbe Sprache wie der erste DEA erkennt, aber über eine minimale<br />
Zustandsmenge verfügt.<br />
5. Dieser minimale DEA kann nun als Gr<strong>und</strong>lage <strong>für</strong> die Implementierung des<br />
Scanners verwendet werden.<br />
2.2.4 Reguläre Definitionen<br />
Der Übersichtlichkeit halber ist es wünschenswert, regulären Ausdrücken Namen geben<br />
zu können, damit diese Namen in anderen regulären Ausdrücken verwendet<br />
werden können. Diese Namen können wie Zeichen aus dem Alphabet in regulären<br />
Ausdrücken auftreten.<br />
Definition 9<br />
Sei Σ Alphabet. Dann ist eine reguläre Definition eine Folge<br />
d1 → r1<br />
d2 → r2<br />
...<br />
dn → rn,<br />
wobei di jeweils ein eindeutiger Name, ri ∈ reg(Σ ∪ {d1,...,di−1}) ist.<br />
Die Einschränkung ri ∈ reg(Σ ∪ {d1,...,di−1}) gewährleistet, daß kein Name eines<br />
regulären Ausdrucks in einem anderen Ausdruck verwendet wird, bevor der Name<br />
deklariert wurde. Insbesondere sind hierdurch keine rekursiven Definitionen möglich.<br />
Beispiel 5<br />
letter → A | B | ... | Z | a | b | ... | z<br />
digit → 0 | 1 | ... | 9<br />
id → letter ( letter | digit ) ∗<br />
Diese reguläre Definition beschreibt (etwas vereinfacht) die Bezeichner, wie sie in<br />
Programmiersprachen auftreten. Bezeichner beginnen hier mit einem Buchstaben,<br />
dem eine beliebig lange Sequenz von Buchstaben <strong>und</strong> Zeichen folgen kann.<br />
Zur Verbesserung der Lesbarkeit werden folgende Abkürzungen vereinbart:<br />
Ein- oder mehrmaliges Auftreten: Sei r regulärer Ausdruck. Dann stehe r + <strong>für</strong> rr ∗ ,<br />
d.h. L(r + ) = L(r) + . Priorität <strong>und</strong> Assoziativität gelten wie <strong>für</strong> den ∗ -Operator.<br />
27
2 Lexikalische Analyse<br />
Null- oder einmaliges Auftreten: Sei r regulärer Ausdruck. Dann stehe r? <strong>für</strong> r |ε,<br />
d.h. L(r?) = L(r) ∪ {ε}.<br />
Beispiel 6<br />
Der reguläre Ausdruck a + beschreibt die Menge aller nicht-leeren Wörter, die nur<br />
aus a’s bestehen.<br />
a?b ∗ beschreibt die Menge aller Wörter, die entweder nur b’s oder genau ein a <strong>und</strong><br />
dann nur b’s enthalten.<br />
Beispiel 7<br />
Betrachten wir die Regeln einer kontextfreien Grammatik <strong>für</strong> einen Ausschnitt einer<br />
Programmiersprache:<br />
stmt → if expr then stmt<br />
| if expr then stmt else stmt<br />
| ε<br />
expr → term relop term<br />
| term<br />
term → id | num<br />
In der durch diese Regeln beschriebenen Programmiersprache treten folgende Symbole<br />
auf: if, then, else, relop, id, num. Der Scanner muß diese Symbole im Eingabestrom<br />
erkennen <strong>und</strong> herausfiltern. Die Symbole werden in Form einer regulären<br />
Definition spezifiziert:<br />
if → i f<br />
then → t h e n<br />
else → e l s e<br />
relop → = | < | | >=<br />
id → letter (letter | digit) ∗<br />
num → digit + (. digit + )? (E (+ | -)? digit + )?<br />
Dabei seien letter <strong>und</strong> digit wie in Beispiel 5 definiert. num ist entweder einfache<br />
Integer-Zahl oder eine Fließkommazahl, wobei Nachkommastellen <strong>und</strong> ein positiver<br />
bzw. negativer Exponent angegeben werden können.<br />
Schlüsselwörter sollen hier nicht als Bezeichner verwendet werden dürfen. Daher wissen<br />
wir, daß bei Erkennen der Zeichenkette t h e n das Symbol then <strong>und</strong> nicht id<br />
an den Parser weitergereicht werden muß.<br />
Lexeme sind durch Leerräume getrennt, dabei sind Leerräume nicht-leere Folgen von<br />
Leerzeichen <strong>und</strong> Zeilenwechseln:<br />
delim → blank | newline<br />
sep → delim +<br />
blank <strong>und</strong> newline müssen dann in Abhängigkeit von der Implementierung interpretiert<br />
werden.<br />
28
2.2 Reguläre Sprachen <strong>und</strong> endliche Automaten<br />
Regulärer Ausdruck Symbol Attributwert<br />
sep – –<br />
i f if –<br />
t h e n then –<br />
e l s e else –<br />
id id Verweis auf Eintrag in der Symboltabelle<br />
num num Wert der Zahl<br />
< relop LT<br />
relop GT<br />
>= relop GE<br />
Tabelle 2.3: Reguläre Ausdrücke <strong>und</strong> die dazugehörigen Symbole <strong>und</strong> Attributwerte.<br />
Unser Ziel ist nun die Erstellung eines Scanners, der im Eingabestrom ein Lexem <strong>für</strong><br />
das nächste Symbol identifiziert <strong>und</strong> als Ausgabe ein Paar aus Symbol <strong>und</strong> Attribut<br />
gemäß Tabelle 2.3 liefert.<br />
Bei der Erkennung der Lexeme tritt folgendes Problem auf: um die Lexeme eindeutig<br />
identifizieren zu können, muß der Scanner im Eingabestrom “vorausschauen” können.<br />
Zum Beispiel liefern<br />
then das Symbol then,<br />
thens das Symbol id,<br />
85 das Symbol num <strong>und</strong><br />
85a einen lexikalischen Fehler ( steht <strong>für</strong> ein Leerzeichen).<br />
Das Vorausschauen im Eingabestrom wird lookahead genannt. Im allgemeinen genügt<br />
ein lookahead von 1 nicht. Daher sollte die Eingabe in einen Eingabepuffer kopiert<br />
werden, in dem über einen Zeiger auf das jeweils aktuelle Zeichen zugegriffen werden<br />
kann. Hierdurch wird ein Zurückschreiben von Zeichen aus dem lookahead durch<br />
Zurücksetzen des Zeigers ermöglicht.<br />
Ein weiteres Problem tritt bei der Erkennung der Schlüsselwörter der zu übersetzenden<br />
Sprache auf. Die Codierung der Erkennung von Schlüsselwörtern in Automaten<br />
resultiert in einer sehr großen Anzahl von Zuständen. Deshalb werden i. allg.<br />
Schlüsselwörter zunächst wie Bezeichner behandelt. Erst später wird zwischen Bezeichnern<br />
<strong>und</strong> Schlüsselwörtern differenziert, so daß die jeweils entsprechende Information<br />
an den Parser geliefert wird.<br />
Ein mögliches Verfahren besteht darin, die Schlüsselwörter vorab mit entsprechenden<br />
Informationen in die Symboltabelle einzutragen, um eine Unterscheidung von den<br />
Einträgen <strong>für</strong> Bezeichner zu ermöglichen. Eine weitere Möglichkeit besteht in der<br />
29
2 Lexikalische Analyse<br />
Kombination des Scanners mit einem Sieber (siehe Abschnitt 2.3).<br />
Beispiel 8 (Fortführung von Beispiel 7)<br />
Wir geben nun <strong>für</strong> die Symbole Übergangsgraphen an, die beschreiben, wie der Scanner<br />
arbeitet, wenn er vom Parser mit der Ermittlung des nächsten Symbols beauftragt<br />
wurde. Dabei gehen wir davon aus, daß der benötigte Teil der Eingabe in einem<br />
Puffer steht, der über einen Zeiger den Zugriff auf das aktuelle Zeichen erlaubt. Am<br />
Anfang steht der Zeiger auf dem Zeichen hinter dem zuletzt gelesenen Lexem.<br />
In Abbildung 2.8 sind die Übergangsgraphen <strong>für</strong> die Symbole relop, id, num <strong>und</strong> sep<br />
angegeben. Vom Startzustand ausgehend wird je nach lookahead die entsprechende<br />
Kante ausgewählt. Wird ein Endzustand erreicht, wird das entsprechende Symbol an<br />
den Parser zurückgeliefert. Bei der Verwendung einer Kante wird das entsprechende<br />
Zeichen durch Weitersetzen des Zeigers aus der Eingabe gelöscht. Eventuell muß<br />
das gelesene Zeichen vom Scanner wieder in die Eingabe zurückgeschrieben werden<br />
(bzw. durch Rücksetzen des Zeigers wieder sichtbar gemacht werden), damit es <strong>für</strong><br />
das nächste Symbol verwendet werden kann. Dies wird durch die Markierung ∗ an<br />
den entsprechenden Endzuständen signalisiert. Das Lexem <strong>für</strong> das an den Parser<br />
gelieferte Symbol besteht dann aus den Zeichen, die bis zur Verwendung der other-<br />
Kante gelesen wurden. Auf diese Weise erkennt der Automat auch die Symbole <<br />
<strong>und</strong> >, obwohl die Zustände 1 <strong>und</strong> 6 keine Endzustände sind.<br />
Ist der Endzustand des Graphen <strong>für</strong> das Symbol id erreicht, wird vor der Rückgabe<br />
des Symbols durch die Funktion gettoken() überprüft, ob es sich bei der gelesenen<br />
Zeichenkette um ein Schlüsselwort handelt. Ist dies der Fall, wird das zugehörige<br />
Symbol an den Parser geliefert. Andernfalls wird das Symbol id weitergegeben <strong>und</strong><br />
der Bezeichner mittels install id() in die Symboltabelle eingetragen. Das Attribut des<br />
Symbols id enthält dann einen Verweis auf den Eintrag des Bezeichners.<br />
2.3 Sieber<br />
Bei der Implementierung eines Scanners wird oft die Identifizierung der <strong>für</strong> den Parser<br />
relevanten Symbole sowie die Erkennung von Schlüsselwörtern vom eigentlichen<br />
Scannen, also der Zerlegung der Eingabe, getrennt. Das entsprechende Teilprogramm,<br />
das diese Aufgaben übernimmt, wird Sieber genannt.<br />
Die Aufgaben des Siebers lassen sich wie folgt charakterisieren:<br />
30<br />
• Erkennung von Schlüsselwörtern: Der Scanner erkennt während der Zerlegung<br />
der Eingabe Bezeichner. Der Sieber untersucht, ob es sich bei einem<br />
Bezeichner um ein Schlüsselwort der zu erkennenden Sprache handelt. Ist dies<br />
der Fall, wird das zugehörige Symbol an den Parser weitergegeben. Handelt<br />
es sich nicht um ein Schlüsselwort, wird der Bezeichner in die Symboltabelle<br />
eingetragen (sofern noch kein Eintrag <strong>für</strong> ihn existiert) <strong>und</strong> das Symbol id
elop:<br />
Start<br />
id:<br />
Start<br />
num:<br />
Start<br />
sep:<br />
Start<br />
< =<br />
0 1 2<br />
=<br />
5<br />
3<br />
4<br />
> =<br />
6 7<br />
Buchstabe other<br />
9 10 11<br />
Ziffer<br />
><br />
other<br />
other<br />
8<br />
Buchstabe oder Ziffer<br />
Ziffer<br />
2.3 Sieber<br />
Ziffer . Ziffer E<br />
+ oder -<br />
Ziffer other<br />
12 13 14 15 16 17 18 19<br />
Begrenzer other<br />
20 21 22<br />
Begrenzer<br />
*<br />
*<br />
*<br />
return(relop,LE)<br />
return(relop,NE)<br />
return(relop,LT)<br />
return(relop,EQ)<br />
return(relop,GE)<br />
return(relop,GT)<br />
return(gettoken(),install_id())<br />
E Ziffer<br />
*<br />
Ziffer<br />
Abbildung 2.8: Übergangsgraphen <strong>für</strong> die Symbole des Beispiels.<br />
*<br />
31
2 Lexikalische Analyse<br />
mit einem Verweis auf den Eintrag als Attribut an den Parser geliefert. Die<br />
Funktionsweise des Siebers entspricht der Funktion gettoken() in Abbildung<br />
2.8.<br />
• Identifizierung der <strong>für</strong> den Parser relevanten Symbole: Nicht alle Symbole,<br />
die vom Scanner generiert werden, werden vom Parser benötigt. So ist<br />
z.B. die Weitergabe des Symbols sep nicht notwendig, da Informationen über<br />
Leerräume im Quelltext <strong>für</strong> die späteren Compilerphasen irrelevant sind. Dies<br />
gilt auch <strong>für</strong> die Kommentare, die sich in einem Quelltext befinden. Aus diesem<br />
Gr<strong>und</strong> wird die Weitergabe dieser Symbole vom Sieber unterb<strong>und</strong>en, so daß nur<br />
die <strong>für</strong> die weiteren Phasen relevanten Symbole an den Parser weitergegeben<br />
werden.<br />
Beispiel 9<br />
In Abbildung 2.9 sind die lexikalische Analyse <strong>und</strong> die semantische Analyse eines<br />
Programmausschnitts dargestellt. In der Phase (A) zerlegt die lexikalische Analyse<br />
den Zeichenstrom in Symbole. In Phase (B) wandelt der Sieber die Bezeichner, die<br />
Schlüsselwörter darstellen, in entsprechende Symbole um. Gleichzeitig werden die Bezeichner,<br />
die keine Schlüsselwörter darstellen, in die Symboltabelle eingetragen <strong>und</strong><br />
in der Symbolfolge durch ihren Index in dieser Tabelle ersetzt. Anschließend wird in<br />
Phase (C) die syntaktische Analyse durchgeführt <strong>und</strong> ein Strukturbaum erzeugt (siehe<br />
Kapitel 3).<br />
2.4 Fehlerbehandlung<br />
Enthält ein Quelltext lexikalische Fehler, soll der Scanner in der Lage sein, diese Fehler<br />
angemessen zu behandeln. Hierzu gehört zum einen die Ausgabe einer möglichst<br />
genauen Fehlermeldung, die Informationen über Art <strong>und</strong> Position (Zeilen- <strong>und</strong> Spaltennummern)<br />
des Fehlers enthält. Zum anderen soll der Scanner in der Lage sein,<br />
pro Analyse des Quelltexts möglichst viele Fehler zu finden, um die Anzahl der Compilerläufe<br />
zu vermindern. Daher ist es notwendig, daß der Scanner ein Verfahren<br />
beinhaltet, daß nach dem Finden eines Fehlers die weitere Analyse des Quelltexts<br />
erlaubt. Dieses Fortfahren mit der Analyse nach Auftreten eines Fehlers wird “Wiederaufsetzen”<br />
(recovery) genannt.<br />
Beim Scannen tritt eine Fehlersituation ein, wenn kein Präfix der Eingabe einem<br />
Symbolmuster entspricht. So kann in Beispiel 7 die Eingabe 2a keinem Symbolmuster<br />
entsprechen, da Bezeichner nicht mit einer Ziffer beginnen dürfen <strong>und</strong> Zahlen keine<br />
Buchstaben (außer E bzw. e in der Exponentialschreibweise) beinhalten können.<br />
Für das “Wiederaufsetzen” existieren verschiedene Verfahren:<br />
32<br />
• Panic Recovery: Bei diesem Verfahren werden nach Auftreten eines Fehlers<br />
solange Zeichen aus der Eingabe entfernt, bis wieder ein gültiges Lexem ge-
Abbildung 2.9: Analyse eines Programmausschnitts.<br />
33<br />
(C)<br />
(B)<br />
(A)<br />
IDLIST<br />
DECLIST<br />
DECL<br />
IDLIST<br />
TYP<br />
PROGRAM<br />
STATLIST<br />
STAT<br />
ASSIGN<br />
var id(1) com id(2) col int sem id(1) bec int("2") sem id(2) bec id(1) mul id(1) add int("1")<br />
id("var") sep id("a") com id("b") col id("int") sem sep id("a") bec int("2") sem sep id("b") bec id("a") mul id("a") add int("1") sem sep<br />
v a r a , b : i n t ; NL a : = 2 ; NL b : = a * a + 1 ; NL<br />
E<br />
T<br />
F<br />
STATLIST<br />
T<br />
F<br />
E<br />
T<br />
ASSIGN<br />
F<br />
E<br />
T<br />
F<br />
2.4 Fehlerbehandlung
2 Lexikalische Analyse<br />
f<strong>und</strong>en wird (z.B. ein Leerzeichen, ein Zeilenende oder ein Schlüsselwort). Da<br />
die Eingabe endlich ist <strong>und</strong> bei diesem Verfahren verkürzt wird, terminiert<br />
der Scanner immer. Nachteilig kann sich auswirken, daß eventuell größere Abschnitte<br />
aus der Eingabe entfernt werden. Falls diese Abschnitte selbst Fehler<br />
enthalten, werden sie bei der aktuellen Analyse nicht entdeckt.<br />
• Löschen oder Einfügen einzelner Zeichen: Einige Fehler treten in Quelltexten<br />
häufiger auf. Hierzu gehören z.B. fehlende Leerzeichen zwischen Lexemen.<br />
Daher kann es sinnvoll ein, bei Auftreten eines Fehlers ein solches Trennzeichen<br />
einzufügen <strong>und</strong> dann die Analyse fortzusetzen. Dabei besteht die Gefahr,<br />
daß bei wiederholtem Einsetzen vor dem aktuellen Eingabesymbol das<br />
Verfahren nicht mehr terminiert. Neben dem Einfügen ist das Löschen von<br />
Zeichen sinnvoll. Besteht die Fehlersituation im Auftreten eines nicht erlaubten<br />
Zeichens im Quelltext (z.B. ? in einem Modula 2-Programm), kann dieses<br />
Zeichen entfernt <strong>und</strong> dann die Analyse des Programms fortgeführt werden.<br />
• Ersetzen von Zeichen: Eine weitere Möglichkeit besteht in der Ersetzung von<br />
fehlerhaften Zeichen. Tritt z.B. während des Scannens einer Zahl ein Buchstabe<br />
auf, kann dieser Buchstabe durch eine beliebige Ziffer ersetzt werden, um die<br />
Behandlung der weiteren Eingabe zu ermöglichen.<br />
Weitere Methoden <strong>für</strong> das “Wiederaufsetzen” sind denkbar.<br />
34
3 Syntaktische Analyse<br />
Die Aufgabe der syntaktischen Analyse ist die Überprüfung der syntaktischen Struktur<br />
eines Programmtextes. Die Durchführung der syntaktischen Analyse wird von<br />
einem Parser genannten Programmteil übernommen (siehe Abbildung 3.1).<br />
Die syntaktische Analyse<br />
• überprüft, ob die vom Scanner (siehe Kapitel 2) gelieferte Symbolfolge von der<br />
Grammatik der Quellsprache erzeugt werden kann;<br />
• liefert aussagefähige Fehlermeldungen <strong>und</strong> versucht, den fehlerhaften Programmtext<br />
auf weitere Fehler hin zu untersuchen;<br />
• erzeugt eine Darstellung, die die syntaktische Struktur eines Programmtextes<br />
wiedergibt (Strukturbaum oder parse tree).<br />
Die theoretische Gr<strong>und</strong>lage der Syntaxanalyse sind die kontextfreien Grammatiken.<br />
3.1 Kontextfreie Grammatiken<br />
Wie in Kapitel 2 beschrieben, kann die lexikalische Struktur eines Programmtextes<br />
mit endlichen Automaten analysiert werden. Für die Definition der syntaktischen<br />
Struktur eines Programmtextes ist die Mächtigkeit der regulären Grammatiken jedoch<br />
nicht ausreichend. In Programmtexten treten häufig Klammerstrukturen auf<br />
(z.B. Klammern in arithmetischen Ausdrücken, begin <strong>und</strong> end), die von den regulären<br />
Grammatiken nicht erkannt werden können. Sei |w|a die Anzahl der Vorkommen des<br />
Zeichens a im Wort w. Die Sprache der wohlgeformten Klammerstrukturen wird<br />
Dycksprache genannt <strong>und</strong> ist wie folgt definiert:<br />
Quellprogramm<br />
Scanner<br />
Symbol<br />
nächstes Symbol<br />
anfordern<br />
Symboltabelle<br />
Parser<br />
parse tree<br />
Rest des Front-Ends<br />
Zwischendarstellung<br />
Abbildung 3.1: Interaktion zwischen Scanner, Parser <strong>und</strong> restlichem Front-End.<br />
35
3 Syntaktische Analyse<br />
{w | w ∈ (a|b) ∗ , |w|a = |w|b, ∀u,v,w = uv : |u|a ≥ |u|b}.<br />
Das Zeichen a entspricht der öffnenden Klammer, b analog der schließenden Klammer.<br />
Endliche Automaten können nicht “zählen”, d.h. sie sind nicht in der Lage, zu<br />
überprüfen, ob bisher mindestens so viele a’s wie b’s gelesen wurden <strong>und</strong> ob in einem<br />
Endzustand die Anzahl der a’s <strong>und</strong> b’s übereinstimmt. Aus diesem Gr<strong>und</strong> ist die<br />
Dycksprache nicht regulär. Ein weiteres Beispiel <strong>für</strong> eine nichtreguläre Sprache ist<br />
{a n b n | n ≥ 0} [Sch08]. Für die Syntaxanalyse wird also die Klasse der kontextfreien<br />
Grammatiken verwendet.<br />
Zu jeder regulären Sprache gibt es einen endlichen Automaten, der die Sprache erkennt.<br />
Analog kann man <strong>für</strong> jede kontextfreie Grammatik einen sogenannten Kellerautomaten<br />
konstruieren, der die von der Grammatik definierte Sprache akzeptiert.<br />
3.1.1 Kontextfreie Grammatiken<br />
Zunächst definieren wir die kontextfreien Grammatiken.<br />
Definition 10<br />
Eine kontextfreie Grammatik ist ein Quadrupel G = (VN,VT,P,S) mit<br />
• VN,VT endliche Mengen (VN Menge der Nichtterminalsymbole, VT Menge der<br />
Terminalsymbole),<br />
• P ⊆ VN × (VN ∪ VT) ∗ (Menge der Produktionen oder Regeln),<br />
• S ∈ VN (Startsymbol).<br />
Terminale stehen <strong>für</strong> die Symbole, die in den zu analysierenden Programmen wirklich<br />
auftreten, <strong>und</strong> entsprechen somit den vom Scanner in der lexikalischen Analyse<br />
erzeugten Symbolen.<br />
Notationen: Wir unterscheiden im folgenden die Nichtterminale <strong>und</strong> die Terminale<br />
in den Produktionen einer Grammatik durch folgende Konventionen:<br />
36<br />
• Terminale werden dargestellt durch<br />
1. a, b, c, ...,<br />
2. Operatorsymbole +, −, ...,<br />
3. Satzzeichen, Klammern etc.,<br />
4. Ziffern 0, ..., 9 <strong>und</strong><br />
5. fettgedruckte Zeichenketten, z.B. id, while.<br />
Nichtterminale werden dargestellt durch
3.1 Kontextfreie Grammatiken<br />
1. A, B, C, ... (S – bzw. die linke Seite der ersten Produktion einer Grammatik<br />
– Startsymbol) <strong>und</strong><br />
2. kursiv gesetzte Zeichenketten, z.B. expr, stmt, Anw, Anw Folge.<br />
• Weiterhin gelten folgende Vereinbarungen:<br />
– X, Y , Z, ... stehen <strong>für</strong> einzelne Grammatiksymbole (Nichtterminale oder<br />
Terminale),<br />
– u, v, w, ..., z stehen <strong>für</strong> Wörter über Terminalsymbolen <strong>und</strong><br />
– α, β, γ, ... stehen <strong>für</strong> Wörter über Grammatiksymbolen.<br />
• Für (A,α) ∈ P schreiben wir A → α (α = ε : ε-Produktionen). Mehrere<br />
Produktionen A → α1,...,A → αn <strong>für</strong> ein Nichtterminal A schreiben wir als<br />
A → α1 | ... | αn (Alternativen <strong>für</strong> A).<br />
Beispiel 10 (Arithmetische Ausdrücke)<br />
Die in Programmiersprachen häufig auftretenden arithmetischen Ausdrücke lassen<br />
sich (vereinfacht) wie folgt mit Hilfe einer kontextfreien Grammatik definieren:<br />
expr → expr op expr<br />
expr → ( expr )<br />
expr → − expr<br />
expr → id<br />
op → +<br />
op → −<br />
op → *<br />
op → /<br />
op → ↑<br />
Dabei sind expr <strong>und</strong> op Nichtterminale, expr ist das Startsymbol der Grammatik <strong>und</strong><br />
id, +, −, ∗, /, ↑, (, ) sind die Terminale.<br />
Mit den oben angegeben Schreibkonventionen läßt sich die Darstellung der Grammatik<br />
verkürzen zu<br />
E → E A E | ( E ) | - E | id<br />
A → + | - | * | / | ↑<br />
Beispiel 11 (Anweisungen einer imperativen Sprache)<br />
Die folgende kontextfreie Grammatik [WM96] definiert die Anweisungen einer einfachen<br />
imperativen Programmiersprache, deren Syntax ähnlich zu der von Pascal ist:<br />
37
3 Syntaktische Analyse<br />
Anw → If Anw |<br />
While Anw |<br />
Repeat Anw |<br />
Proz Aufruf |<br />
Wertzuweisung<br />
If Anw → if Bed then An Folge else An Folge fi |<br />
if Bed then An Folge fi<br />
While Anw → while Bed do An Folge od<br />
Repeat Anw → repeat An Folge until Bed<br />
Proz Aufruf → Name ( Ausdr Folge )<br />
Wertzuweisung → Name := Ausdr<br />
An Folge → Anw |<br />
An Folge ; Anw<br />
Ausdr Folge → Ausdr |<br />
Ausdr Folge , Ausdr<br />
Eine Anweisung ist entweder eine If-Anweisung, eine While- oder Repeat-Schleife,<br />
ein Prozeduraufruf oder eine Wertzuweisung. Wir nehmen an, daß die Nichtterminale<br />
Bed <strong>für</strong> Bedingungen, Ausdr <strong>für</strong> Ausdrücke sowie Name <strong>für</strong> Bezeichner vordefiniert<br />
sind. Das Nichtterminal An Folge beschreibt Anweisungsfolgen. Dieses Nichtterminal<br />
ist rekursiv definiert: Eine Anweisungsfolge ist entweder eine einzelne Anweisung<br />
oder eine (kürzere) Folge, an die eine Anweisung nach einem ; angehängt wird. Das<br />
Nichtterminal Ausdr Folge beschreibt analog Folgen von Ausdrücken, in denen die<br />
einzelnen Ausdrücke durch , getrennt sind.<br />
Beispiel 12 (Grammatik mit ε-Produktionen)<br />
Die folgende Grammatik definiert begin-end-Blöcke in Pascal:<br />
Block → begin opt An Folge end<br />
opt An Folge → An Folge | ε<br />
An Folge → Anw | An Folge ; Anw<br />
Ein Block ist eine mit begin <strong>und</strong> end geklammerte Anweisungsfolge. Blöcke können<br />
in Pascal auch leer sein, so daß das Nichtterminal opt An Folge (<strong>für</strong> “optionale Anweisungsfolge”)<br />
auch zum leeren Wort ε abgeleitet werden kann.<br />
3.1.2 Ableitungen<br />
Wir betrachten die Produktionen kontextfreier Grammatiken als Ersetzungsregeln,<br />
die aus Wörtern über VN ∪ VT neue Wörter über VN ∪ VT “produzieren”, indem<br />
Nichtterminale durch rechte Regelseiten ersetzt werden : σAτ ⇒ σατ (A → α Produktion).<br />
Definition 11<br />
Sei G = (VN,VT,P,S) eine kontextfreie Grammatik.<br />
38
3.1 Kontextfreie Grammatiken<br />
ϕ produziert ψ gemäß G direkt (ϕ ⇒G ψ) :⇔ es existieren Wörter σ,τ,α <strong>und</strong> ein<br />
Nichtterminal A mit ϕ = σAτ, ψ = σατ <strong>und</strong> A → α ∈ P.<br />
ϕ produziert ψ gemäß G (oder ψ ist aus ϕ gemäß G ableitbar) (ϕ ⇒ ∗ G ψ) :⇔ es<br />
existiert eine Folge von Wörtern ϕ1,...,ϕn(n ≥ 1) mit ϕ = ϕ1, ψ = ϕn <strong>und</strong> ϕi ⇒G<br />
ϕi+1 <strong>für</strong> 1 ≤ i < n. (⇒ ∗ G ist also die reflexive <strong>und</strong> transitive Hülle von ⇒G.)<br />
ϕ1,ϕ2,...,ϕn heißt dann eine Ableitung von ψ aus ϕ gemäß G.<br />
Wir schreiben auch ϕ ⇒ n−1<br />
G ψ (“in n − 1 Schritten”).<br />
Wenn aus dem Zusammenhang hervorgeht, auf welche Grammatik wir uns beziehen,<br />
lassen wir den Index G in ⇒G <strong>und</strong> ⇒ ∗ G weg.<br />
Beispiel 13 (Fortsetzung von Beispiel 10)<br />
Wir geben eine Ableitung des Wortes −(id+E) gemäß der in Beispiel 10 vorgestellten<br />
Grammatik an:<br />
E ⇒ −E ⇒ −(E) ⇒ −(EAE) ⇒ −(E + E) ⇒ −(id + E),<br />
also E ⇒ ∗ −(id + E).<br />
In einem Ableitungsschritt können mehrere anzuwendende Produktionen zur Auswahl<br />
stehen. So hätten wir im Schritt E ⇒ −E auch die Ableitung E ⇒ EAE<br />
auswählen können, was aber zur Folge hätte, daß wir das Wort −(id+E) nicht mehr<br />
hätten ableiten können. Ebenso kann eine Regel eventuell an verschiedenen Stellen<br />
des Wortes angewendet werden. So hätte im letzten Ableitungsschritt auch das Wort<br />
−(E + id) erzeugt werden können.<br />
Definition 12<br />
Die von G definierte (erzeugte) Sprache ist L(G) = {u ∈ V ∗<br />
T | S ⇒ ∗ G u}.<br />
Ein Wort x ∈ L(G) heißt ein Satz von G.<br />
Ein Wort α ∈ (VN ∪ VT) ∗ mit S ⇒ ∗ G α heißt eine Satzform von G.<br />
In Beispiel 13 treten nur Satzformen, aber keine Sätze auf.<br />
Definition 13<br />
Eine kontextfreie Sprache ist eine Sprache, die von einer kontextfreien Grammatik<br />
erzeugt werden kann. Zwei kontextfreie Grammatiken G1,G2 heißen äquivalent :⇔<br />
L(G1) = L(G2).<br />
Eine kontextfreie Grammatik enthält unter Umständen Nichtterminale, die nicht zur<br />
Erzeugung der Sprache beitragen.<br />
Definition 14<br />
39
3 Syntaktische Analyse<br />
• Ein Nichtterminal A heißt unerreichbar, wenn es keine Wörter α,β gibt mit<br />
S ⇒ ∗ αAβ. A heißt unproduktiv, wenn es kein Wort u gibt mit A ⇒ ∗ u.<br />
• Eine kontextfreie Grammatik G heißt reduziert, wenn sie weder unerreichbare<br />
noch unproduktive Nichtterminale enthält.<br />
Durch Elimination unerreichbarer <strong>und</strong> unproduktiver Nichtterminalsymbole aus der<br />
Grammatik entsteht eine äquivalente reduzierte Grammatik. Wir nehmen im folgenden<br />
immer an, daß die betrachteten Grammatiken reduziert sind.<br />
Definition 15<br />
Sei ϕ1,ϕ2,...,ϕn eine Ableitung von ϕ = ϕn aus S = ϕ1.<br />
ϕ1,ϕ2,...,ϕn heißt Linksableitung von ϕ (S ⇒ ∗ l ϕ), wenn beim Schritt von ϕi nach<br />
ϕi+1 jeweils das am weitesten links stehende Nichtterminal ersetzt wird.<br />
ϕ1,ϕ2,...,ϕn heißt Rechtsableitung von ϕ (S ⇒ ∗ r ϕ), wenn jeweils das am weitesten<br />
rechts stehende Nichtterminal ersetzt wird.<br />
Eine Satzform, die in einer Linksableitung (bzw. Rechtsableitung) auftritt, heißt Linkssatzform<br />
(bzw. Rechtssatzform).<br />
Beispiel 14 (Fortsetzung von Beispiel 13)<br />
Die in Beispiel 13 angegebene Ableitung ist weder eine Links- noch eine Rechtsableitung.<br />
Allerdings kann das Wort −(id+E) auch mit folgender Linksableitung erzeugt<br />
werden:<br />
E ⇒l −E ⇒l −(E) ⇒l −(EAE) ⇒l −(id A E) ⇒l −(id + E),<br />
also gilt E ⇒ ∗ l −(id + E).<br />
Zu jedem Satz einer kontextfreien Grammatik gibt es eine Linksableitung <strong>und</strong> eine<br />
Rechtsableitung. Wir betrachten im folgenden nur Ableitungen, in deren Schritten<br />
entweder immer links oder immer rechts ersetzt wird.<br />
3.1.3 Strukturbäume<br />
Neben der Überprüfung der Fehlerfreiheit eines Programmtextes ist die Generierung<br />
einer Zwischendarstellung des Programms, die die syntaktische Struktur des Quelltextes<br />
wiedergibt, eine Aufgabe der syntaktischen Analyse. Diese Zwischendarstellung<br />
kann in den folgenden Phasen der Übersetzung verwendet werden. Da<strong>für</strong> ist der<br />
Strukturbaum oder parse tree geeignet.<br />
Definition 16<br />
Sei G = (VN,VT,P,S) kontextfreie Grammatik. Sei B ein geordneter Baum, dessen<br />
Blätter über VN ∪ VT ∪ {ε} <strong>und</strong> dessen innere Knoten über VN markiert sind.<br />
B ist Strukturbaum <strong>für</strong> α ∈ (VN ∪ VT) ∗ <strong>und</strong> N ∈ VN, wenn gilt:<br />
40
-<br />
E<br />
E<br />
( E )<br />
E A E<br />
id<br />
Abbildung 3.2: Beispiel eines Strukturbaums.<br />
+<br />
3.1 Kontextfreie Grammatiken<br />
1. Ist n innerer Knoten, der mit dem Nichtterminal A markiert ist, <strong>und</strong> sind<br />
seine Kinder von links nach rechts mit X1,...,Xk ∈ VN ∪ VT markiert, so ist<br />
A → X1...Xk ∈ P. Ist sein einziges Kind markiert mit ε, so ist A → ε ∈ P.<br />
2. Die Wurzel von B ist mit N markiert.<br />
3. Front (Wort an den Blättern) von B ist α.<br />
Ein Strukturbaum <strong>für</strong> ein Wort α <strong>und</strong> das Startsymbol S heißt Strukturbaum <strong>für</strong> α.<br />
Ein Strukturbaum <strong>für</strong> ein Wort u ∈ V ∗<br />
T (alle seine Blätter sind mit Terminalen oder<br />
ε markiert) heißt vollständig.<br />
Beispiel 15<br />
In Abbildung 3.2 ist der Strukturbaum <strong>für</strong> die Ableitung aus Beispiel 14 dargestellt.<br />
Front des Baums ist die Satzform −(id+E). Der Strukturbaum ist nicht vollständig,<br />
da noch ein Blatt mit dem Nichtterminal E markiert ist. Wird dieses Blatt gemäß<br />
der Produktion E → id modifiziert, wird der Baum vollständig.<br />
Satz 3<br />
Jeder Satz einer kontextfreien Grammatik besitzt mindestens einen Strukturbaum.<br />
Beweis<br />
Der Satz besitzt eine Ableitung. Konstruiere dazu den Strukturbaum nach folgendem<br />
Verfahren.<br />
Sei ϕ1,ϕ2,...,ϕn eine Ableitung von α = ϕn aus N = ϕ1 gemäß G = (VN,VT,P,S).<br />
Dann existiert genau ein Strukturbaum <strong>für</strong> α <strong>und</strong> N gemäß G, der dieser Ableitung<br />
entspricht.<br />
41
3 Syntaktische Analyse<br />
E<br />
E<br />
- E<br />
- E<br />
-<br />
E<br />
E<br />
- E<br />
-<br />
( E ) ( E ) ( E )<br />
E A E<br />
E A E<br />
E A<br />
id id +<br />
E<br />
E<br />
E<br />
( E )<br />
Abbildung 3.3: Konstruktion eines Strukturbaums.<br />
Sei B 1 := N, also der Baum, der nur aus dem mit N markierten Knoten besteht.<br />
Sei B i−1 Strukturbaum <strong>für</strong> ϕi−1 (i > 1). Front von B i−1 ist ϕi−1. Sei ϕi−1 =<br />
X1X2...Xk ∈ (VN ∪ VT) ∗ . Sei ϕi aus ϕi−1 durch Ersetzen von Xj durch β = Y1Y2...Yl<br />
abgeleitet (also Xj → β ∈ P).<br />
Falls l > 0, sei B i der Baum, der aus B i−1 entsteht, indem das j-te Blatt (von links)<br />
l Nachfolgerknoten erhält, die von links nach rechts mit Y1,Y2,...,Yl markiert sind.<br />
Falls l = 0, erhält das j-te Blatt genau einen Nachfolger, der mit ε markiert wird.<br />
Der gesuchte Strukturbaum ist dann B := B n .<br />
�<br />
Beispiel 16<br />
Abbildung 3.3 enthält die schrittweise Konstruktion des Strukturbaums aus Abbildung<br />
3.2 gemäß der Ableitung aus Beispiel 14.<br />
Umgekehrt gilt: zu einem Strukturbaum gibt es mindestens eine Ableitung. Aber:<br />
ein Strukturbaum kann mehreren Ableitungen entsprechen (siehe obiges Beispiel).<br />
Allerdings gibt es zu jedem Strukturbaum <strong>für</strong> ein Wort w ∈ V ∗<br />
T eine eindeutige<br />
Links- <strong>und</strong> eine eindeutige Rechtsableitung.<br />
3.1.4 Mehrdeutige Grammatiken<br />
Während aus einem Strukturbaum eindeutig hervorgeht, welchen Satz er beschreibt,<br />
kann es zu einem Satz mehrere Strukturbäume geben. Erlauben die Produktionen<br />
42<br />
E<br />
E
E<br />
E A E<br />
id<br />
+<br />
E A E<br />
id<br />
3.1 Kontextfreie Grammatiken<br />
E<br />
E A E<br />
E A E<br />
* id id + id<br />
Abbildung 3.4: Verschiedene Strukturbäume zu einem Satz.<br />
einer Grammatik zu einem Satz mehrere Strukturbäume, ist die Grammatik mehrdeutig.<br />
Beispiel 17<br />
In Abbildung 3.4 sind zwei mögliche Strukturbäume <strong>für</strong> den Satz id + id ∗ id gegeben.<br />
Ihre unterschiedliche Struktur ist in der Wahlmöglichkeit zwischen verschiedenen<br />
Produktionen begründet. Es entstehen verschiedene Strukturbäume, die trotzdem dieselbe<br />
Satzform als Ergebnis besitzen.<br />
Definition 17<br />
Ein Satz u ∈ L(G) heißt mehrdeutig, wenn er mehr als einen Strukturbaum hat.<br />
Eine kontextfreie Grammatik G heißt mehrdeutig, wenn L(G) mindestens einen<br />
mehrdeutigen Satz enthält. Eine nicht mehrdeutige Grammatik nennen wir eindeutig.<br />
Zu jedem eindeutigen Satz einer kontextfreien Grammatik gibt es genau eine Links<strong>und</strong><br />
genau eine Rechtsableitung.<br />
Manchmal ist es günstig, Mehrdeutigkeiten durch Umformen der Grammatik in eine<br />
äquivalente Grammatik zu eliminieren (andere Lösungsmöglichkeiten werden wir<br />
später betrachten). Diese Umformung ist aber nicht <strong>für</strong> jede beliebige Grammatik<br />
möglich, da es Sprachen gibt, die keine eindeutige Grammatik besitzen [AU72]. Außerdem<br />
muß beim Umformen darauf geachtet werden, daß die syntaktische Struktur<br />
der zu erkennenden Sprache nicht verändert wird.<br />
Beispiel 18 (“dangling else”)<br />
Die folgende Grammatik beschreibt eine if-Anweisung, bei der ein optionaler else-<br />
Zweig angegeben werden kann.<br />
stmt → if expr then stmt<br />
| if expr then stmt else stmt<br />
| other<br />
*<br />
id<br />
43
3 Syntaktische Analyse<br />
stmt<br />
if expr then stmt<br />
E<br />
1<br />
if expr then stmt else<br />
stmt<br />
E<br />
2<br />
S<br />
1<br />
S<br />
2<br />
if expr then stmt else stmt<br />
E<br />
1<br />
if expr then stmt<br />
E<br />
2<br />
S<br />
1<br />
Abbildung 3.5: Mögliche Strukturbäume des “dangling else”-Problems.<br />
In Abbildung 3.5 sind zwei mögliche Strukturbäume <strong>für</strong> den Satz<br />
S<br />
2<br />
if E1 then if E2 then S1 else S2<br />
angegeben. Der obere Strukturbaum stellt eine Ableitung dar, in der zuerst die erste<br />
<strong>und</strong> dann die zweite Produktion der Grammatik angewendet wurden <strong>und</strong> das else<br />
somit zum zweiten if gehört. Im unteren Strukturbaum wurden zuerst die zweite<br />
<strong>und</strong> danach die erste Produktion angewendet <strong>und</strong> das else gehört daher zum ersten<br />
if. Beide Ableitungen sind aufgr<strong>und</strong> der Mehrdeutigkeit der Grammatik möglich. In<br />
Programmiersprachen (z.B. in Pascal) gilt aber die Konvention, daß ein else zum<br />
letzten noch freien then gehört. Daher stellt der obere Strukturbaum die gewünschte<br />
Ableitung dar.<br />
Um die Mehrdeutigkeit zu eliminieren, modifizieren wir die Grammatik. Dabei gehen<br />
wir von folgender Idee aus: die Anweisung zwischen einem then <strong>und</strong> einem else muß<br />
“geschlossen” sein, d.h. sie darf nicht mit einem freien then enden (sonst würde das<br />
else diesem then zugeordnet). Geschlossene Anweisungen (matched stmt) sind komplette<br />
if-then-else-Anweisungen, die nur geschlossene if-then-else-Anweisungen oder<br />
andere Anweisungen (dargestellt durch other) enthalten. Analog bezeichnen wir die<br />
nicht geschlossenen Anweisungen als unmatched stmt. Wir erhalten folgende Grammatik:<br />
44<br />
stmt
stmt<br />
unmatched_stmt<br />
if expr then stmt<br />
E<br />
1<br />
matched_stmt<br />
if expr then matched_stmt else matched_stmt<br />
stmt<br />
matched_stmt<br />
E S S<br />
2<br />
1 2<br />
if expr then matched_stmt else matched_stmt<br />
(*)<br />
E S<br />
1 2<br />
3.1 Kontextfreie Grammatiken<br />
Abbildung 3.6: Lösung des “dangling else”-Problems.<br />
stmt → matched stmt<br />
| unmatched stmt<br />
matched stmt → if expr then matched stmt else matched stmt<br />
| other<br />
unmatched stmt → if expr then stmt<br />
| if expr then matched stmt else unmatched stmt<br />
Diese Grammatik erzeugt dieselbe Sprache, läßt aber nur den ersten Strukturbaum<br />
zu (Abbildung 3.6). Im zweiten Strukturbaum in Abbildung 3.6 kann an der mit (*)<br />
markierten Position kein Teilbaum <strong>für</strong> if E2 then S1 eingefügt werden, da dort ein<br />
matched stmt erwartet wird.<br />
45
3 Syntaktische Analyse<br />
3.2 Konstruktion von Parsern<br />
Aus der Theorie der formalen Sprachen ist bekannt, daß nichtdeterministische Kellerautomaten<br />
genau die kontextfreien Sprachen erkennen [Sch08] (analog zu regulären<br />
Sprachen <strong>und</strong> endlichen Automaten). Also gibt es zu jeder kontextfreien Grammatik<br />
einen nichtdeterministischen Kellerautomaten.<br />
Im Gegensatz zu endlichen Automaten, bei denen die nichtdeterministischen <strong>und</strong> deterministischen<br />
Varianten die gleiche Mächtigkeit haben, besitzen nichtdeterministische<br />
Kellerautomaten eine größere Mächtigkeit als deterministische Kellerautomaten<br />
<strong>und</strong> erkennen somit mehr Sprachen als diese. Da wir Parser in Form eines Programms<br />
realisieren wollen, betrachten wir später Teilmengen der kontextfreien Sprachen, die<br />
sich mit deterministischen Kellerautomaten erkennen lassen.<br />
Im folgenden konstruieren wir zunächst nichtdeterministische Parser, die dann als<br />
Basis <strong>für</strong> deterministische Parse-Verfahren dienen.<br />
Es gibt zwei Konstruktionsverfahren eines nichtdeterministischen Kellerautomaten zu<br />
einer kontextfreien Grammatik. Entsprechend diskutieren wir zwei unterschiedliche<br />
Verfahren zur Konstruktion von Parsern. Beim Top-Down-Verfahren wird der Strukturbaum<br />
von der Wurzel zu den Blättern hin erzeugt, beim Bottom-Up-Verfahren<br />
entsprechend entgegengesetzt.<br />
3.2.1 Kellerautomat<br />
In Abbildung 3.7 ist das Schema eines nichtdeterministischen Kellerautomaten angegeben.<br />
Er besteht aus einer Kontrolleinheit, einem Keller <strong>für</strong> das Speichern von<br />
Werten, einem Eingabe- <strong>und</strong> einem Ausgabeband. Der Inhalt des Eingabebands wird<br />
mit dem Lesekopf zeichenweise von links nach rechts gelesen. In Abhängigkeit von<br />
dem gelesenen Zeichen <strong>und</strong> dem aktuellen Kellerinhalt führt die Kontrolleinheit einen<br />
Berechnungsschritt durch. Dabei können sowohl ein Zeichen auf das Ausgabeband<br />
geschrieben als auch der Inhalt des Kellers modifiziert werden. Hierbei wird immer<br />
an der Kellerspitze gelesen bzw. geschrieben.<br />
Definition 18<br />
Ein (nichtdeterministischer) Kellerautomat (mit Ausgabe) ist ein Tupel M = (Σ, Γ, ∆,z0,O)<br />
mit • Σ endliche Menge (Eingabealphabet),<br />
• Γ endliche Menge (Kelleralphabet),<br />
• zo ∈ Γ (Kellerstartsymbol),<br />
• O endliche Menge (Ausgabealphabet),<br />
• ∆ ⊆ ((Σ ∪ {ε}) × Γ) × (Γ ∗ × O ∗ ) (endliche Übergangsrelation).<br />
Jedes Element der Übergangsrelation beschreibt einen möglichen Schritt bei der Analyse<br />
des Eingabewortes.<br />
((Σ ∪ {ε}) × Γ) repräsentiert das aktuell vom Eingabeband gelesene Zeichen des<br />
46
Keller<br />
Eingabewort<br />
Kontrolle<br />
000 111<br />
3.2 Konstruktion von Parsern<br />
...<br />
Lesekopf<br />
Ausgabe<br />
Abbildung 3.7: Schema eines Kellerautomaten.<br />
Eingabealphabets Σ (ε falls keine Leseoperation durchgeführt wird) <strong>und</strong> das oberste<br />
Zeichen des Kellers (aus dem Kelleralphabet Γ). Bei der Durchführung eines Berechnungsschrittes<br />
(eines Übergangs des Kellerautomaten) wird die bisherige Kellerspitze<br />
entfernt <strong>und</strong> durch eine (evtl. auch leere) Folge von Kellerzeichen ersetzt sowie ein<br />
Zeichen auf das Ausgabeband geschrieben (repräsentiert durch (Γ ∗ × O ∗ )).<br />
Definition 19<br />
Sei M nichtdeterministischer Kellerautomat.<br />
Die Menge der Konfigurationen von M ist K = Σ ∗ × Γ ∗ × O ∗ .<br />
Die Anfangskonfiguration <strong>für</strong> die Analyse eines Wortes w ∈ Σ ∗ ist (w,z0,ε).<br />
Die Einzelschrittrelation ⊢M⊆ K × K ist definiert durch<br />
(aw,Y γ,u) ⊢M (w,βγ,ub) :⇔ ((a,Y ), (β,b)) ∈ ∆ (a ∈ Σ ∪ {ε}). ⊢ ∗ M ist die reflexive<br />
<strong>und</strong> transitive Hülle von ⊢M.<br />
Elemente von {ε} × {ε} × O ∗ heißen Endkonfigurationen.<br />
Die von M erkannte Sprache ist L(M) := {w ∈ Σ ∗ | (w,z0,ε) ⊢ ∗ M (ε,ε,σ), σ ∈ O ∗ }.<br />
Konfigurationen modellieren die Zustände während der Analyse eines Eingabewortes.<br />
Eine Konfiguration enthält die noch nicht gelesene Eingabe, den aktuellen Kellerinhalt<br />
<strong>und</strong> den Inhalt des Ausgabebandes. In der Anfangskonfiguration ist noch kein<br />
Zeichen der Eingabe gelesen, der Keller enthält nur das Kellerstartsymbol z0 <strong>und</strong><br />
das Ausgabeband ist leer. Endkonfigurationen sind alle Konfigurationen, in denen<br />
Eingabeband <strong>und</strong> Keller leer sind <strong>und</strong> somit das Eingabewort komplett gelesen <strong>und</strong><br />
der Kellerinhalt “verbraucht” wurde. Die Einzelschrittrelation beschreibt die Berechnungsschritte<br />
zwischen den Konfigurationen. (aw,Y γ,u) beschreibt den Zustand vor<br />
einem Schritt. Das aktuelle Symbol der Eingabe ist a ∈ Σ ∪ {ε}, die aktuelle Kellerspitze<br />
ist Y ∈ Γ <strong>und</strong> der Inhalt des Ausgabebands ist u ∈ O ∗ . Wenn es einen<br />
...<br />
47
3 Syntaktische Analyse<br />
Übergang ((a,Y ), (β,b)) in der Übergangsrelation ∆ des Kellerautomaten gibt, wird<br />
das a (eventuell ε) aus der Eingabe entfernt, die Kellerspitze Y wird durch ein Wort<br />
β ∈ Γ ∗ ersetzt <strong>und</strong> das Ausgabesymbol b wird an den bisherigen Inhalt des Ausgabebandes<br />
angehängt. Die von einem Kellerautomat erkannte Sprache ist die Menge<br />
der Wörter über dem Eingabealphabet, <strong>für</strong> die es eine Folge von Schritten von der<br />
entsprechenden Anfangskonfiguration in eine Endkonfiguration gibt.<br />
Wir erwähnten bereits, daß die Ausgabe eines Parsers ein Strukturbaum sein soll, der<br />
die syntaktische Struktur des Eingabeworts (also des Quellprogramms) darstellt. Aus<br />
diesem Gr<strong>und</strong> müssen wir die Konstruktion des Strukturbaums in das Ausgabealphabet<br />
des Kellerautomaten, der den Parser realisiert, codieren. Da wir immer Linksoder<br />
Rechtsableitungen betrachten, genügt es bei der Konstruktion eines Strukturbaums,<br />
die Reihenfolge zu kennen, in der die Produktionen der Grammatik während<br />
der Analyse der Eingabe angewendet wurden. Daher ordnen wir den Produktionen<br />
der Grammatik eindeutige Nummern zu <strong>und</strong> definieren die Menge dieser Nummern<br />
als Ausgabealphabet des Kellerautomaten.<br />
Definition 20<br />
Sei π : {1, 2,...,p} → P bijektive Funktion (Numerierung der Produktionen der<br />
Grammatik).<br />
Sei z = z1z2...zn−1 ∈ {1,...,p} ∗ ,α ∈ (VN ∪ VT) ∗ .<br />
z heißt Linksanalyse von α, wenn S = ϕ1,ϕ2,...,ϕn = α Linksableitung von α <strong>und</strong><br />
ϕi produziert ϕi+1 mit Regel zi.<br />
z heißt Rechtsanalyse von α, wenn S = ϕn,ϕn−1,...,ϕ1 = α Rechtsableitung von α<br />
<strong>und</strong> ϕi+1 produziert ϕi mit Regel zi.<br />
Eine Linksanalyse ist eine Ableitung S z1 z2<br />
⇒ ϕ2 ⇒ ... zn−2<br />
⇒ ϕn−1 ⇒ α.<br />
Eine Rechtsanalyse ist eine Ableitung S zn−1 zn−2<br />
⇒ ϕn−1 ⇒ ... z2 z1<br />
⇒ ϕ2 ⇒ α, enthält also die<br />
Regelnummern einer Rechtsableitung in umgekehrter Reihenfolge.<br />
Wir nehmen im folgenden an, daß die Produktionen der Grammatiken mit Regelnummern<br />
versehen sind.<br />
Es gibt zwei verschiedene Verfahren zur Analyse eines Wortes mit Kellerautomaten:<br />
• “Top-Down”: Dieses Verfahren entspricht der Konstruktion eines Strukturbaums<br />
von der Wurzel ausgehend. Diese Konstruktion realisiert eine Linksableitung,<br />
daher ist die Ausgabe des Parsers eine Linksanalyse.<br />
• “Bottom-Up”: Dieses Verfahren entspricht der Konstruktion eines Strukturbaums<br />
von den Blättern ausgehend. Diese Konstruktion realisiert eine Rechtsableitung<br />
in umgekehrter Reihenfolge; die Ausgabe des Parsers ist also eine<br />
Rechtsanalyse.<br />
In den folgenden Abschnitten werden die beiden Parse-Verfahren erläutert.<br />
48<br />
zn−1
3.3 Top-Down-Syntaxanalyse<br />
3.3 Top-Down-Syntaxanalyse<br />
Zunächst konkretisieren wir den in Definition 18 eingeführten Kellerautomaten <strong>für</strong><br />
die Top-Down-Analyse.<br />
Notation: Für ein Alphabet Σ sei Σ ≤k = �<br />
0≤i≤k Σ i die Menge der Wörter mit der<br />
maximalen Länge k (einschließlich ε).<br />
Definition 21<br />
Ein (nichtdeterministischer) Top-Down-Analyseautomat <strong>für</strong> eine kontextfreie Grammatik<br />
G = (VN,VT,P,S) ist ein Kellerautomat mit<br />
• Eingabealphabet VT,<br />
• Kelleralphabet Γ = VN ∪ VT,<br />
• Kellerstartsymbol S,<br />
• Ausgabealphabet {1,...,p},<br />
• Übergangsrelation ∆ ⊆ ((VT ∪ {ε}) × Γ) × (Γ ∗ × {1,...,p} ≤1 ), wobei<br />
((ε,A), (α,i)) ∈ ∆ :⇔ π(i) = A → α <strong>und</strong><br />
((a,a), (ε,ε)) ∈ ∆ <strong>für</strong> alle a ∈ VT.<br />
Die Übergangsrelation ∆ enthält zwei Arten von Regeln:<br />
1. Die Regeln der Form ((ε,A), (α,i)) beschreiben die Anwendung der i-ten Produktion<br />
der Grammatik. Befindet sich das Nichtterminal A der Produktion<br />
A → α auf dem Keller, wird dieses Symbol vom Keller entfernt <strong>und</strong> durch die<br />
rechte Seite α der Produktion ersetzt, so daß das erste Zeichen von α die neue<br />
Kellerspitze darstellt. Dabei wird kein Zeichen aus der Eingabe gelesen. Auf das<br />
Ausgabeband wird die Nummer i der ausgewählten Produktion geschrieben.<br />
2. Für jedes Terminalsymbol a ∈ VT wird eine Regel der Form ((a,a), (ε,ε)) in die<br />
Übergangsrelation aufgenommen. Ist das aktuelle Symbol der Eingabe gleich<br />
der aktuellen Kellerspitze, werden beide Zeichen entfernt <strong>und</strong> das Ausgabeband<br />
bleibt unverändert, da keine Produktion der Grammatik angewendet wurde.<br />
Diese beiden Arten von Regeln definieren die Arbeitsweise des Top-Down-Parsers.<br />
Mit den Regeln unter 1) stellt der Parser Hypothesen auf, wie der folgende Abschnitt<br />
des Programmtextes aussieht, indem er Nichtterminale auf dem Keller durch<br />
die zugehörigen rechten Regelseiten ersetzt. Mit den unter 2) beschriebenen Regeln<br />
werden diese Hypothesen überprüft, indem übereinstimmende Zeichen der Eingabe<br />
<strong>und</strong> des Kellers paarweise gelöscht werden. Ist die Eingabe komplett gelesen <strong>und</strong> der<br />
Keller leer, d.h. jedes Symbol der Eingabe konnte einem Kellersymbol zugeordnet<br />
werden, akzeptiert der Automat die Eingabe als Wort der zu erkennenden Sprache.<br />
49
3 Syntaktische Analyse<br />
Eingabe Kellerinhalt Ausgabe<br />
- ( id + id ) E<br />
- ( id + id ) - E 3<br />
( id + id ) E ε<br />
( id + id ) ( E ) 2<br />
id + id ) E ) ε<br />
id + id ) E A E ) 1<br />
id + id ) id A E ) 4<br />
+ id ) A E ) ε<br />
+ id ) + E ) 5<br />
id ) E ) ε<br />
id ) id ) 4<br />
) ) ε<br />
ε ε ε<br />
Tabelle 3.1: Beispielableitung eines Top-Down-Parsers.<br />
Beispiel 19 (Top-Down-Analyse arithmetischer Ausdrücke)<br />
Gegeben sei die folgende Grammatik zur Definition vereinfachter arithmetischer Ausdrücke<br />
mit Nummerierung der Produktionen:<br />
E → E A E (1)<br />
| ( E ) (2)<br />
| - E (3)<br />
| id (4)<br />
A → + (5)<br />
| - (6)<br />
| * (7)<br />
| / (8)<br />
| ↑ (9)<br />
Eine Top-Down-Ableitung des Satzes -(id+id) ist in Tabelle 3.1 angegeben. Die linke<br />
Spalte enthält die noch nicht gelesene Eingabe, die mittlere den aktuellen Kellerinhalt<br />
(mit der Kellerspitze links) <strong>und</strong> die rechte das Zeichen, das in dem jeweiligen Schritt<br />
auf das Ausgabeband geschrieben wird. Dieses Zeichen ist entweder die Nummer der<br />
angewendeten Produktion oder ε, falls das Symbol aus der Eingabe der aktuellen<br />
Kellerspitze entspricht <strong>und</strong> beide Symbole daher gelöscht werden können.<br />
Aufgr<strong>und</strong> des Nichtdeterminismus könnten auch jeweils andere Reihenfolgen der Produktionen<br />
bzw. andere Produktionen gewählt werden, wodurch das Eingabewort gegebenenfalls<br />
nicht erkannt werden würde.<br />
50
3.3 Top-Down-Syntaxanalyse<br />
Die Symbole in der Ausgabe-Spalte der Tabelle bilden eine Linksanalyse des Satzes<br />
-(id+id):<br />
E 3 ⇒ - E 2 ⇒ - ( E ) 1 ⇒ - ( E A E ) 4 ⇒ - ( id A E ) 5 ⇒ - ( id + E )<br />
4<br />
⇒ - ( id + id )<br />
Gemäß der Ausgabe der jeweils angewendeten Regel läßt sich der Strukturbaum<br />
schrittweise konstruieren (siehe Abbildung 3.8).<br />
3.3.1 LL(k)-Grammatiken<br />
Die Auswahl der anzuwendenden Produktion in einer Konfiguration ist aufgr<strong>und</strong> des<br />
Nichtdeterminismus nicht eindeutig, so daß die “richtige” Produktion unter mehreren<br />
Alternativen geraten werden muß. Eine deterministische Auswahl könnte durch die<br />
Integration von backtracking in den Parser erreicht werden. Allerdings ist backtracking<br />
unter dem Gesichtspunkt der Effizienz ungeeignet.<br />
Eine Ursache <strong>für</strong> den Nichtdeterminismus ist, daß die Eingabe erst nach der Auswahl<br />
einer Produktion mit dem Inhalt des Stacks verglichen wird. Daher werden wir Verfahren<br />
betrachten, die bei der Auswahl einer Produktion einen begrenzten Ausschnitt<br />
der Eingabe berücksichtigen <strong>und</strong> auf diese Weise die Auswahl deterministisch treffen<br />
können.<br />
Das Konzept der LL(k)-Grammatiken beruht auf dieser Berücksichtigung eines Teils<br />
der Eingabe bei der Auswahl der Produktionen. Das Vorausschauen in der Eingabe<br />
wird lookahead genannt. Bei einer LL(k)-Grammatik genügt ein lookahead von k<br />
Symbolen in der Eingabe, um beim deterministischen Top-Down-Parseverfahren die<br />
Produktionen auszuwählen (das erste L bedeutet, daß die Eingabe von links gelesen<br />
wird, das zweite L, daß eine Linksableitung konstruiert wird). Die LL(k)-Sprachen<br />
sind mit einer Top-Down-Analyse deterministisch zu erkennen. Allerdings bilden die<br />
LL(k)-Sprachen nur eine Teilmenge der kontextfreien Sprachen. Nicht alle kontextfreien<br />
Sprachen können durch LL(k)-Grammatiken beschrieben werden.<br />
Definition 22<br />
Sei Σ Alphabet, w = a1...an ∈ Σ ∗ (n ≥ 0), k ≥ 0.<br />
ist der k-Präfix von w.<br />
k : w =<br />
�<br />
w falls n ≤ k<br />
a1...ak sonst<br />
Definition 23<br />
Sei G = (VN,VT,P,S) kontextfreie Grammatik, k ≥ 0. G ist eine LL(k)-Grammatik,<br />
wenn gilt:<br />
51
3 Syntaktische Analyse<br />
1<br />
4<br />
E<br />
E<br />
- E<br />
- E<br />
-<br />
-<br />
E<br />
( E ) ( E ) ( E )<br />
E A E<br />
E A E<br />
E A<br />
E<br />
E<br />
( E )<br />
E A E<br />
id + id<br />
E<br />
3 2<br />
- E<br />
-<br />
4 5<br />
id id +<br />
E<br />
E<br />
E<br />
( E )<br />
Abbildung 3.8: Konstruktion des Strukturbaums anhand der Ausgabe des Parsers.<br />
52<br />
E<br />
E
S ⇒ ∗ l u Y α<br />
⇒l u β α ⇒ ∗ l u x<br />
⇒l u γ α ⇒ ∗ l u y<br />
3.3 Top-Down-Syntaxanalyse<br />
<strong>und</strong> k : x = k : y, dann β = γ (d.h. die Alternative der Auswahl der Produktion <strong>für</strong><br />
jedes Nichtterminal Y ist durch die ersten k Zeichen der restlichen Eingabe eindeutig<br />
festgelegt (bei festem Linkskontext u)).<br />
Beispiel 20<br />
Die Grammatik G1, gegeben durch die Produktionen<br />
stmt → if id then stmt else stmt fi<br />
| while id do stmt od<br />
| begin stmt end<br />
| id : = id<br />
ist eine LL(1)-Grammatik (wir betrachten in diesem Beispiel das Zuweisungssymbol<br />
:= als zwei separate Symbole : <strong>und</strong> =).<br />
Wann immer stmt in einer Ableitung auftritt, ist aufgr<strong>und</strong> des nächsten Eingabesymbols<br />
klar, welche Produktion anzuwenden ist. Die abzuleitenden Folgen von Terminalsymbolen<br />
beginnen alle mit unterschiedlichen Symbolen (if, while, begin oder<br />
id), so daß mit einem lookahead von 1 die anzuwendende Produktion eindeutig zu<br />
bestimmen ist.<br />
Eine solche Grammatik, bei der die rechten Seiten der Produktionen zu einem Nichtterminalsymbol<br />
alle mit unterschiedlichen Terminalsymbolen beginnen, heißt einfache<br />
LL(1)-Grammatik.<br />
Definition 24<br />
Sei G kontextfreie Grammatik ohne ε-Produktionen. Beginnt <strong>für</strong> jedes Nichtterminal<br />
N jede seiner Alternativen mit einem anderen Terminalsymbol, dann heißt G einfache<br />
LL(1)-Grammatik.<br />
Einerseits ist die Zugehörigkeit einer Grammatik zur Klasse der einfachen LL(1)-<br />
Grammatiken sehr einfach zu überprüfen, andererseits sind diese Grammatiken <strong>für</strong><br />
die Praxis i. allg. zu eingeschränkt.<br />
Beispiel 21 (Fortsetzung von Beispiel 20)<br />
Wir erweitern G1 zu G2 durch die zusätzlichen Produktionen<br />
stmt → id : stmt (* markierte Anweisung *)<br />
| id ( id ) (* Prozeduraufruf *)<br />
G2 ist nicht mehr einfach LL(1), denn mehrere Alternativen <strong>für</strong> stmt beginnen nun<br />
mit dem Terminalsymbol id.<br />
G2 ist auch nicht mehr LL(1):<br />
53
3 Syntaktische Analyse<br />
⇒l u id : = id α ⇒ ∗ l u x<br />
stmt ⇒ ∗ l u stmt α ⇒l u id : stmt α ⇒ ∗ l u y<br />
⇒l u id ( id ) α ⇒ ∗ l u z<br />
In den drei Satzformen ist der Anfang “u id” gleich, so daß nicht durch einen lookahead<br />
von 1 entschieden werden kann, welche Produktion anzuwenden ist.<br />
G2 ist auch keine LL(2)-Grammatik, da in den oberen beiden Satzformen die Anfangsstücke<br />
“u id :” identisch sind <strong>und</strong> daher auch durch einen lookahead von 2 die<br />
Auswahl nicht deterministisch getroffen werden kann. Wenn wir jedoch den Zuweisungsoperator<br />
:= als ein einzelnes Symbol betrachten, ist G2 eine LL(2)-Grammatik.<br />
Es gibt kontextfreie Grammatiken, die keine LL(k)-Grammatiken sind, auch wenn<br />
wir k beliebig groß wählen. Manchmal kann man eine solche Grammatik in eine<br />
äquivalente LL(k)-Grammatik transformieren. Allerdings gibt es auch kontextfreie<br />
Grammatiken, <strong>für</strong> die es keine äquivalente LL(k)-Grammatik <strong>für</strong> irgendein k gibt.<br />
Für praktische Zwecke, d.h. <strong>für</strong> die Beschreibung der Syntax der bekannten Programmiersprachen,<br />
gibt es fast immer LL(1)- oder LL(2)-Grammatiken.<br />
Wie können wir überprüfen, ob eine Grammatik LL(k) ist, ohne alle möglichen Ableitungen<br />
zu betrachten?<br />
Ein Hilfsmittel hierzu sind die sogenannten FIRST-Mengen. Diese Mengen beschreiben,<br />
mit welchen Anfangsstücken aus Satzformen herleitbare Terminalworte beginnen<br />
können.<br />
Definition 25<br />
Sei G = (VN,VT,P,S) kontextfreie Grammatik, k ∈ INI.<br />
FIRSTk : (VN ∪ VT) ∗ → P(V ≤k<br />
T ) ist definiert durch<br />
FIRSTk(α) = {k : w | α ⇒ ∗ w}.<br />
FIRSTk(α) ist also die Menge der Anfangsstücke der Länge k von Terminalwörtern,<br />
die aus α ableitbar sind. Falls kürzere Wörter ableitbar sind, dann sind diese auch<br />
in der FIRST-Menge enthalten, insbesondere eventuell auch ε.<br />
Mit Hilfe der FIRST-Mengen läßt sich die Klasse der LL(k)-Sprachen wie folgt<br />
charakterisieren:<br />
Satz 4 (LL(k)-Charakterisierung)<br />
Sei G = (VN,VT,P,S) kontextfreie Grammatik, k ≥ 0. G ist genau dann LL(k),<br />
wenn gilt:<br />
Sind A → β,A → γ verschiedene Produktionen in P, dann FIRSTk(βα)∩FIRSTk(γα) =<br />
∅ <strong>für</strong> alle α, u mit S ⇒ ∗ l uAα.<br />
54
3.3 Top-Down-Syntaxanalyse<br />
Beweis<br />
“⇒”:<br />
Annahme: G ist LL(k) <strong>und</strong> es existiert x ∈ FIRSTk(βα) ∩ FIRSTk(γα). Nach<br />
Definition 25 <strong>und</strong> da G reduziert:<br />
S ⇒ ∗ l u A α<br />
⇒l u β α ⇒ ∗ l u x y<br />
⇒l u γ α ⇒ ∗ l u x z<br />
(xy <strong>und</strong> xz bis zur Länge k oder ganz identisch; falls Länge von x ≤ k, dann y =<br />
z = ε).<br />
Da aber β �= γ (da A → β, A → γ verschiedene Produktionen), wäre G nicht LL(k),<br />
was aber ein Widerspruch zur Annahme darstellt.<br />
“⇐”:<br />
Annahme: G ist nicht LL(k) <strong>und</strong> FIRSTk(βα) ∩ FIRSTk(γα) = ∅ <strong>für</strong> alle α mit<br />
S ⇒ ∗ l uAα. Dann gibt es (da G reduziert)<br />
S ⇒ ∗ l u A α<br />
⇒l u β α ⇒ ∗ l u x<br />
⇒l u γ α ⇒ ∗ l u y<br />
mit k : x = k : y, wobei A → β,A → γ verschiedene Produktionen. Aber k : x = k :<br />
y ∈ FIRSTk(βα) ∩ FIRSTk(γα), was einen Widerspruch zur Annahme darstellt.<br />
�<br />
Aus dieser Charakterisierung sind weitere Kriterien <strong>für</strong> die Zugehörigkeit zu LL(k)<br />
ableitbar. Wir betrachten hier nur den <strong>für</strong> die Praxis besonders wichtigen Fall k = 1.<br />
Sei k = 1 <strong>und</strong> G eine kontextfreie Grammatik ohne ε-Produktionen. Dann sind<br />
FIRST1(βα) ∩ FIRST1(γα) allein aus β <strong>und</strong> γ bestimmbar (da β <strong>und</strong> γ aufgr<strong>und</strong><br />
der fehlenden ε-Produktionen nicht leer werden können <strong>und</strong> daher mindestens ein<br />
Zeichen generieren).<br />
Satz 5<br />
Sei G kontextfreie Grammatik ohne ε-Produktionen. G ist LL(1) genau dann, wenn<br />
<strong>für</strong> jedes Nichtterminal A mit A → α1 | ... | αn (A hat genau die Alternativen α1,<br />
..., αn) gilt: FIRST1(α1), ..., FIRST1(αn) paarweise disjunkt.<br />
Da ε-Produktionen in Grammatiken von Programmiersprachen üblicherweise auftreten,<br />
bedeutet das generelle Verbot von ε-Produktionen eine zu starke Einschränkung.<br />
Durch das Zulassen dieser Produktionen ist das Kriterium aus Satz 5 nicht mehr ausreichend.<br />
Dies wird im folgenden Beispiel verdeutlicht.<br />
Beispiel 22<br />
Wir betrachten eine kontextfreie Grammatik G mit folgenden Produktionen:<br />
55
3 Syntaktische Analyse<br />
Strukturbaum lookahead<br />
S a<br />
S<br />
A a<br />
A<br />
a<br />
S<br />
a<br />
Abbildung 3.9: Fehlerhafte Konstruktion eines Strukturbaums <strong>für</strong> die Eingabe a.<br />
S → A a<br />
A → a | ε<br />
Offensichtlich sind die FIRST1-Mengen der Alternativen der Nichtterminale disjunkt.<br />
Aber G ist nicht LL(1) gemäß Satz 4, denn mit α = a, β = a <strong>und</strong> γ = ε gilt:<br />
FIRST1(βα) ∩ FIRST1(γα) = FIRST1(aa) ∩ FIRST1(εa) = {a} �= ∅.<br />
Was würde beim Top-Down-Parsen passieren? In Abbildung 3.9 wird die schrittweise<br />
Konstruktion des Strukturbaums <strong>für</strong> das Eingabewort a dargestellt. Im ersten Schritt<br />
wird die erste Produktion angewendet. Diese Auswahl ist eindeutig, da das Startsymbol<br />
nur eine Alternative besitzt. Im zweiten Schritt muß das A abgeleitet werden.<br />
Dieses Nichtterminal besitzt zwei Alternativen. Durch einen Vergleich mit dem lookahead,<br />
der das a enthält, kann sich der Parser <strong>für</strong> die erste Alternative von A<br />
entscheiden. Der so konstruierte Syntaxbaum entspricht aber nicht dem Eingabewort<br />
a, sondern dem Satz aa. Der Parser hätte das A zu ε ableiten müssen.<br />
Das Problem bei der deterministischen Auswahl der Produktionen von A entsteht,<br />
da in der Produktion S → Aa das a auf A folgt. Betrachten wir noch einmal den<br />
Charakterisierungssatz (Satz 4) mit k = 1. Angenommen, es gilt β ⇒ ∗ ε. Falls<br />
auch γ ⇒ ∗ ε gilt, ist die Grammatik nicht LL(k), da der Parser durch keinen lookahead<br />
eine Entscheidung treffen kann. Wenn γ ⇒ ∗ ε nicht gilt, muß die Bedingung<br />
FIRST1(α) ∩ FIRST1(γ) = ∅ gelten <strong>für</strong> alle S ⇒ ∗ l uAα. Wir betrachten also alle<br />
möglichen Anfänge von dem, was auf A folgen kann. Hierzu definieren wir die sogenannten<br />
FOLLOW-Mengen.<br />
56<br />
a
Definition 26<br />
Sei G = (Vn,VT,P,S) kontextfreie Grammatik, k ∈ INI.<br />
FOLLOWk : (VN ∪ VT) ∗ → P(V ≤k<br />
T ) ist definiert durch<br />
FOLLOWk(α) = {w | S ⇒ ∗ βαγ <strong>und</strong> w ∈ FIRSTk(γ)}.<br />
3.3 Top-Down-Syntaxanalyse<br />
Insbesondere werden wir FOLLOW-Mengen <strong>für</strong> einzelne Nichtterminale betrachten.<br />
Für k = 1 bezeichnet FOLLOW1(A) die Menge der Terminale a, die in einer Ableitung<br />
auf das Nichtterminal A folgen können. Hierbei ist zu beachten, daß zwischen<br />
A <strong>und</strong> a während der Ableitung auch Nichtterminale gestanden haben können, die<br />
zu ε abgeleitet wurden.<br />
Mit Hilfe der FOLLOW1-Mengen können wir Satz 5 auf Grammatiken mit ε-Produktionen<br />
erweitern.<br />
Satz 6<br />
Eine kontextfreie Grammatik G ist genau dann LL(1), wenn <strong>für</strong> alle Alternativen<br />
A → α1 | ... | αn gilt:<br />
1. FIRST1(α1), ..., FIRST1(αn) paarweise disjunkt (insbesondere enthält nur<br />
eine dieser Mengen ε),<br />
2. falls αi ⇒ ∗ ε, dann FIRST1(αj) ∩ FOLLOW1(A) = ∅ <strong>für</strong> alle<br />
1 ≤ j ≤ n,j �= i.<br />
Wir geben nun die Algorithmen <strong>für</strong> die Berechnung von FIRST1- <strong>und</strong> FOLLOW1-<br />
Mengen an.<br />
Berechnung von FIRST1-Mengen: Sei G = (VN,VT,P,S) kontextfreie Grammatik.<br />
• Sei X ∈ VN ∪ VT. FIRST1(X) ist die kleinste Menge mit<br />
1. falls X Terminal, dann FIRST1(X) = {X},<br />
2. falls X → ε ∈ P, dann ε ∈ FIRST1(X),<br />
3. falls X → Y1Y2...Yk ∈ P:<br />
– Terminalsymbol a ∈ FIRST1(X), wenn a ∈ FIRST1(Yi) <strong>und</strong> ε ∈<br />
FIRST1(Yj) <strong>für</strong> 1 ≤ j < i,<br />
– ε ∈ FIRST1(X), wenn ε ∈ FIRST1(Yj) <strong>für</strong> 1 ≤ j ≤ k.<br />
• Sei α ∈ (VN ∪ VT) ∗ , α = X1X2...Xn. FIRST1(α) ist die kleinste Menge mit<br />
– FIRST1(X1)\{ε} ⊆ FIRST1(α),<br />
– falls ε ∈ FIRST1(X1), dann FIRST1(X2)\{ε} ⊆ FIRST1(α),<br />
– ...<br />
57
3 Syntaktische Analyse<br />
– falls ∀i, 1 ≤ i ≤ n − 1 : ε ∈ FIRST1(Xi), dann FIRST1(Xn)\{ε} ⊆<br />
FIRST1(α),<br />
– falls ∀i, 1 ≤ i ≤ n : ε ∈ FIRST1(Xi), dann ε ∈ FIRST1(α) (beachte, daß<br />
hier insbesondere FIRST1(ε) = {ε} folgt).<br />
Berechnung von FOLLOW1-Mengen: Sei G = (VN,VT,P,S) kontextfreie Grammatik.<br />
Zur Berechnung der FOLLOW1-Mengen werden simultan <strong>für</strong> alle Nichtterminale<br />
der Grammatik folgende Schritte durchgeführt:<br />
1. ε ∈ FOLLOW1(S),<br />
2. A → αBβ ∈ P, a ∈ FIRST1(β), dann a ∈ FOLLOW1(B) <strong>für</strong> a ∈ VT.<br />
3. A → αBβ ∈ P <strong>und</strong> ε ∈ FIRST1(β) (d.h. β ⇒ ∗ ε, evtl. sogar β = ε),<br />
dann FOLLOW1(A) ⊆ FOLLOW1(B).<br />
Manchmal wird ein spezielles Symbol $ �∈ VT verwendet, das an das Eingabewort<br />
angehängt wird <strong>und</strong> während der Analyse zur Erkennung des Eingabeendes dient. In<br />
diesem Fall wird Regel 1 durch $ ∈ FOLLOW(S) ersetzt.<br />
Im folgenden lassen wir den Index 1 bei den FIRST1- <strong>und</strong> FOLLOW1-Mengen weg.<br />
Beispiel 23 (Fortführung von Beispiel 22)<br />
Wir betrachten die Grammatik aus Beispiel 22 mit den folgenden Produktionen:<br />
S → A a<br />
A → a | ε<br />
• Berechnung von FIRST(S):<br />
Wir betrachten S → Aa <strong>und</strong> stellen fest, daß wir die Menge FIRST(A) benötigen.<br />
Aufgr<strong>und</strong> der Regeln <strong>für</strong> ein Nichtterminal A erhalten wir: a ∈ FIRST(A)<br />
<strong>und</strong> ε ∈ FIRST(A). Da a ∈ FIRST(A), gilt auch a ∈ FIRST(S). Wegen<br />
ε ∈ FIRST(A) muß auch die FIRST-Menge des a in S → Aa in die FIRST-<br />
Menge von S aufgenommen werden. Da FIRST(a) = {a}, ε /∈ FIRST(a) <strong>und</strong><br />
a bereits in FIRST(S), bleibt FIRST(S) unverändert: FIRST(S) = {a}.<br />
• Berechnung der FOLLOW-Mengen:<br />
Nach Regel 1 gilt ε ∈ FOLLOW(S). Nach Regel 2 gilt a ∈ FOLLOW(A), da A<br />
in S → Aa auftritt <strong>und</strong> a ∈ FIRST(a). Weitere Auftreten von Nichtterminalen<br />
in rechten Regelseiten kommen nicht vor. Die Regel 3 kann nicht angewendet<br />
werden, also gilt FOLLOW(A) = {a}.<br />
Damit gilt a ∈ FIRST(a) ∩ FOLLOW(A) <strong>und</strong> somit ist die zweite Bedingung von<br />
Satz 6 nicht erfüllt. Also ist die Grammatik nicht LL(1).<br />
58
3.3.2 Transformierung von Grammatiken<br />
3.3 Top-Down-Syntaxanalyse<br />
Es gibt zwei Klassen von kontextfreien Grammatiken, die in der Praxis zur Definition<br />
der Syntax von Programmiersprachen verwendet werden, aber nicht LL(k) sind <strong>für</strong><br />
beliebiges k. Dabei handelt es sich zum einen um mehrdeutige Grammatiken <strong>und</strong><br />
zum anderen um linksrekursive Grammatiken.<br />
Mehrdeutige Grammatiken wurden bereits in Abschnitt 3.1.4 eingeführt.<br />
Satz 7<br />
Sei G kontextfreie Grammatik. Ist G mehrdeutig, ist G nicht LL(k) <strong>für</strong> jedes k ≥ 0.<br />
Beweis<br />
Der Beweis folgt unmittelbar aus der Definition von LL(k). Wenn ein Terminalwort<br />
durch unterschiedliche Linksableitungen zu erkennen ist, kann die Auswahl zwischen<br />
Alternativen nicht durch irgendeinen lookahead entschieden werden. �<br />
Linksrekursive Grammatiken<br />
Definition 27<br />
Sei G kontextfreie Grammatik.<br />
Eine Produktion von G heißt direkt rekursiv, wenn sie die Form A → αAβ hat. Sie<br />
heißt direkt linksrekursiv, wenn α = ε, direkt rechtsrekursiv, wenn β = ε.<br />
Ein Nichtterminal A heißt rekursiv, wenn es eine Ableitung A ⇒ + αAβ gibt (⇒ + ist<br />
die transitive Hülle von ⇒, d.h. es muß mindestens ein Ableitungsschritt durchgeführt<br />
worden sein). A heißt linksrekursiv, wenn α = ε, rechtskursiv, wenn β = ε.<br />
G heißt linksrekursiv, wenn G mindestens ein linksrekursives Nichtterminal enthält,<br />
rechtsrekursiv, wenn G mindestens ein rechtsrekursives Nichtterminal enthält.<br />
Satz 8<br />
Sei G kontextfreie Grammatik. Ist G linksrekursiv, ist G nicht LL(k) <strong>für</strong> jedes k ≥ 0.<br />
Beweisskizze<br />
Wir betrachten hier nur den Fall k = 1 <strong>und</strong> die direkte Linksrekursion.<br />
G sei eine direkt linksrekursive Grammatik, d.h. G enthält eine Produktion der Form<br />
A → Aβ. Da G reduziert ist, gibt es außerdem eine weitere Produktion A → γ des<br />
Nichtterminals A. Wir unterscheiden zwei Fälle:<br />
• FIRST(γ) �= {ε}. Dann gilt FIRST(γ) ∩ FIRST(Aβ) �= ∅, da Aβ nach γβ<br />
abgeleitet werden kann. Nach Bedingung 1 von Satz 4 ist G daher keine LL(1)-<br />
Grammatik.<br />
• FIRST(γ) = {ε}. Dann muß nach Bedingung 1 von Satz 6 gelten, daß ε �∈<br />
FIRST(Aβ). Nach Bedingung 2 muß gelten: FIRST(Aβ)∩FOLLOW(A) = ∅.<br />
59
3 Syntaktische Analyse<br />
Es gilt aber: ∃a ∈ FIRST(β) (da sonst ε ∈ FIRST(Aβ)) <strong>und</strong> a ∈ FOLLOW(A),<br />
also ist a ∈ FIRST(Aβ) (da ε ∈ FIRST(γ) <strong>und</strong> daher auch ε ∈ FIRST(A)).<br />
Also ist G nicht LL(1).<br />
�<br />
Ein Beispiel, in dem Linksrekursion auftritt, ist die Definition der Syntax arithmetischer<br />
Ausdrücke.<br />
Beispiel 24<br />
Die folgende Grammatik beschreibt arithmetische Ausdrücke mit den Operatoren +<br />
<strong>und</strong> ∗ unter Berücksichtigung der korrekten Operatorprioritäten:<br />
E → E + T | T<br />
T → T * F | F<br />
F → ( E ) | id<br />
Die Nichtterminale E <strong>und</strong> T sind linksrekursiv. Daher ist diese Grammatik <strong>für</strong> kein<br />
k ≥ 0 eine LL(k)-Grammatik.<br />
Linksrekursion kann durch Transformation der Grammatik in eine äquivalente Grammatik<br />
eliminiert werden. Wir erläutern diese Transformation am Beispiel der direkten<br />
Linksrekursion.<br />
Transformation einer linksrekursiven in eine rechtsrekursive Grammatik<br />
Gegeben seien die Produktionen A → Aα|β (in Beispiel 24 etwa A = E,α = +T,β =<br />
T). Diese Produktionen erzeugen einen Strukturbaum wie in der oberen Hälfte von<br />
Abbildung 3.10 angegeben.<br />
Wir ersetzen die obigen Produktionen <strong>für</strong> A durch<br />
A → β A’<br />
A’ → α A’ | ε<br />
mit dem neuen Nichtterminal A ′ . Diese neue Grammatik ist rechtsrekursiv, was auf<br />
die LL(k)-Eigenschaft keinen Einfluß hat. Der entsprechende Strukturbaum ist in<br />
ebenfalls in Abbildung 3.10 dargestellt.<br />
Durch diese Transformation entstehen zusätzliche ε-Produktionen.<br />
Beispiel 25 (Fortsetzung von Beispiel 24)<br />
Die Transformation der Grammatik aus Beispiel 24 ergibt die folgende rechtsrekursive<br />
Grammatik:<br />
60
A<br />
A<br />
A<br />
β α α α<br />
A<br />
A’<br />
A’<br />
A<br />
A’<br />
3.3 Top-Down-Syntaxanalyse<br />
A’<br />
β α α α ε<br />
Abbildung 3.10: Transformation in rechtsrekursive Grammatik.<br />
E → T E’<br />
E’ → + T E’ | ε<br />
T → F T’<br />
T’ → * F T’ | ε<br />
F → ( E ) | id<br />
Linksfaktorisierung<br />
Eine weitere Möglichkeit, Grammatiken, die nicht die LL(k)-Eigenschaft besitzen,<br />
in eine äquivalente LL(k)-Grammatik zu transformieren, ist die Linksfaktorisierung.<br />
Dieses Verfahren beruht auf der Idee, die Entscheidung über die Auswahl der Regel<br />
hinauszuschieben. Dabei werden die gleichen Anfänge der Produktionen eines<br />
Nichtterminals zu einer Produktion zusammengefaßt. Produktionen der Form<br />
werden ersetzt durch<br />
A → αβ1 | αβ2<br />
61
3 Syntaktische Analyse<br />
A → αA ′<br />
A ′ → β1 | β2<br />
Der folgende Algorithmus führt eine Linksfaktorisierung durch.<br />
Algorithmus Linksfaktorisierung.<br />
Eingabe: Grammatik G<br />
Ausgabe: Äquivalente linksfaktorisierte Grammatik<br />
Verfahren:<br />
Für jedes Nichtterminalsymbol A: Bestimme längstes gemeinsames Präfix α<br />
zweier oder mehrerer rechter Seiten <strong>für</strong> A. Falls α �= ε, ersetze<br />
A → αβ1 | αβ2 | ... | αβn | γ<br />
(wobei γ <strong>für</strong> alle nicht mit α beginnenden rechten Seiten steht) durch<br />
A → αA ′ | γ<br />
A ′ → β1 | β2 | ... | βn<br />
(mit A ′ neues Nichtterminalsymbol). Wiederhole diese Transformation solange,<br />
bis es keine Alternativen mit gemeinsamen Präfixen mehr gibt.<br />
Beispiel 26<br />
Wir betrachten erneut das dangling-else-Problem. Wir geben die Produktionen der<br />
Grammatik in verkürzter Schreibweise an:<br />
S → i E t S | i E t S e S | a<br />
E → b<br />
Diese Grammatik ist nicht LL(1), da die ersten beiden Produktionen von S identische<br />
Präfixe i E t S besitzen <strong>und</strong> daher eine Auswahl der Alternative nicht möglich ist.<br />
Die Anwendung der Linksfaktorisierung ergibt die folgende äquivalente Grammatik:<br />
S → i E t S S’ | a<br />
S’ → e S | ε<br />
E → b<br />
In dieser Grammatik tritt das gemeinsame Präfix nur noch in einer Produktion<br />
auf, so daß bei der Auswahl der Produktionen von S mit einem lookahead von 1<br />
eine Entscheidung getroffen werden kann. Allerdings ist die neue Grammatik immer<br />
noch nicht LL(1), da <strong>für</strong> die Produktionen von S ′ gilt: ε ∈ FIRST(ε) <strong>und</strong><br />
e ∈ FIRST(eS) ∩ FOLLOW(S ′ ), da FOLLOW(S) ⊆ FOLLOW(S ′ ). Die Grammatik<br />
kann die LL(1)-Bedingung nicht erfüllen, da sie mehrdeutig ist (siehe Abschnitt<br />
3.3.2).<br />
62
Parse-Tabellen<br />
3.3 Top-Down-Syntaxanalyse<br />
Wir haben in den letzten Abschnitten gezeigt, daß mit Hilfe der LL(k)-Grammatiken<br />
eine deterministische Top-Down-Analyse der Eingabe möglich ist. Nun geben wir ein<br />
Verfahren an, wie wir zu einer gegebenen LL(1)-Grammatik einen deterministischen<br />
Parser bauen können. Hierzu werden die Parse-Tabellen verwendet.<br />
Eine Parse-Tabelle M <strong>für</strong> eine kontextfreie Grammatik G gibt an, welche Produktion<br />
<strong>für</strong> ein Nichtterminalsymbol bei einem bestimmten lookahead in Frage kommt.<br />
Die Zeilen der Tabelle sind mit den Nichtterminalen der Grammatik beschriftet, die<br />
Spalten mit den Terminalsymbolen <strong>und</strong> dem Endezeichen $. M[A,a] enthält dann die<br />
bei lookahead a anzuwendende Produktion <strong>für</strong> A bzw. error, falls keine Produktion<br />
von A unter dem aktuellen lookahead a anwendbar ist.<br />
Das folgende Verfahren wird zur Konstruktion einer LL(1)-Parse-Tabelle angewendet.<br />
Konstruktion der Parse-Tabelle zu einer Grammatik.<br />
Eingabe: Grammatik G mit FIRST- <strong>und</strong> FOLLOW-Mengen<br />
Ausgabe: Parse-Tabelle M <strong>für</strong> G<br />
Verfahren:<br />
1. Für jede Produktion A → α von G:<br />
a) Für jedes a ∈ FIRST(α),a �= ε: trage A → α in M[A,a] ein.<br />
b) Falls ε ∈ FIRST(α): <strong>für</strong> jedes b ∈ FOLLOW(A): trage A → α in<br />
M[A,b] ein (wobei b Terminalsymbol oder $).<br />
2. Trage in jedes noch <strong>und</strong>efinierte Feld error ein.<br />
Beispiel 27<br />
Wir betrachten die Grammatik aus Beispiel 25:<br />
E → T E’<br />
E’ → + T E’ | ε<br />
T → F T’<br />
T’ → * F T’ | ε<br />
F → ( E ) | id<br />
Wir konstruieren schrittweise die Parse-Tabelle aus Tabelle 3.2.<br />
Wir beginnen mit dem Startsymbol E. Im ersten Schritt berechnen wir die FIRST-<br />
Mengen der rechten Regelseiten von E, in diesem Fall also<br />
FIRST(T E ′ ) = FIRST(T) = FIRST(F T ′ ) = FIRST(F) = {(,id} (weder<br />
FIRST(T) noch FIRST(F) enthalten ε). Wir tragen also in die Spalten, die mit<br />
Terminalsymbolen aus FIRST(T E ′ ) beschriftet sind, die Produktion E → T E ′ ein.<br />
Da ε /∈ FIRST(T E ′ ), ist der Schritt (1b) <strong>für</strong> diese Produktion nicht anwendbar.<br />
63
3 Syntaktische Analyse<br />
id + * ( ) $<br />
E E → T E’ error error E → T E’ error error<br />
E’ error E’ → + T E’ error error E’ → ε E’ → ε<br />
T T → F T’ error error T → F T’ error error<br />
T’ error T’ → ε T’ → * F T’ error T’ → ε T’ → ε<br />
F F → id error error F → ( E ) error error<br />
Tabelle 3.2: Beispiel einer Parse-Tabelle.<br />
Nun betrachten wir das Nichtterminal E ′ : Für die Produktion E ′ → + T E ′ gilt:<br />
FIRST(+ T E ′ ) = {+}, also wird die Produktion im ersten Schritt in die entsprechende<br />
Spalte eingetragen.<br />
Aufgr<strong>und</strong> der Produktion E ′ → ε müssen wir die FOLLOW-Menge von E ′ nach<br />
Schritt (1b) des Verfahrens betrachten. Diese Menge sagt uns, ob der lookahead mit<br />
dem zusammenpaßt, was auf E ′ folgen kann. Es gilt: FOLLOW(E ′ ) = {), $}, daher<br />
wird die Produktion E ′ → ε in die entsprechenden Spalten der Tabelle eingetragen.<br />
Die Produktionen T → F T ′ <strong>und</strong> T ′ → ∗ F T ′ | ε werden analog behandelt. Dabei ist<br />
zu beachten, daß FOLLOW(T ′ ) = {+, ), $}.<br />
Die Behandlung der Produktionen des Nichtterminals F verläuft analog.<br />
Wir können dieses Konstruktionsverfahren auch als LL(1)-Test verwenden: Wenn ein<br />
Feld der Parse-Tabelle im Laufe des Verfahrens mehr als einen Eintrag erhält, ist die<br />
Grammatik nicht LL(1).<br />
Nachdem wir <strong>für</strong> eine LL(1)-Grammatik die zugehörige Parse-Tabelle erzeugt haben,<br />
benötigen wir noch ein Verfahren, das die deterministische Top-Down-Analyse mit<br />
Hilfe der Parse-Tabelle durchführt. Die Eingabe sei mit dem Eingabeendezeichen $<br />
abgeschlossen. Gleichzeitig verwenden wir dieses Zeichen auch als “unterstes” Symbol<br />
auf dem Keller, um einen leeren Keller anzuzeigen. Ein Parse-Verfahren <strong>für</strong> deterministische<br />
Top-Down-Analyse unter Verwendung dieser Konventionen ist im folgenden<br />
Algorithmus angegeben.<br />
Deterministische Top-Down-Analyse mit Parse-Tabelle.<br />
Eingabe: Parse-Tabelle M <strong>für</strong> Grammatik G <strong>und</strong> ein Eingabewort w<br />
Ausgabe: Linksableitung von w, falls w ∈ L(G), sonst Fehlermeldung<br />
Verfahren:<br />
Der Kellerautomat befinde sich in folgender Anfangskonfiguration: S$ liege auf<br />
dem Keller (Kellerspitze links), wobei S Startsymbol von G, w$ befinde sich<br />
im Eingabepuffer <strong>und</strong> der Eingabezeiger stehe auf dem ersten Symbol von w.<br />
Wir geben den Algorithmus in Form eines Pseudocode-Programms an:<br />
64
1 repeat<br />
3.3 Top-Down-Syntaxanalyse<br />
2 s e i X oberstes Kellersymbol <strong>und</strong> a aktuelles<br />
Eingabesymbol<br />
3 if X Terminal then<br />
4 if X = a then<br />
5 entferne X vom Keller <strong>und</strong> r”ucke Eingabezeiger<br />
vor<br />
6 else error<br />
7 else (∗ X i s t Nichtterminal ∗)<br />
8 if M[X,a] = X → Y1Y2...Yk then begin<br />
9 entferne X vom Keller , lege Yk,Yk−1,...,Y1 auf den<br />
Keller<br />
10 (Y1 neue Kellerspitze ) , gib Produktion<br />
X → Y1Y2...Yk aus<br />
11 end<br />
12 else error<br />
13 until X = $ (∗ Keller l e e r ∗)<br />
65
3 Syntaktische Analyse<br />
Beispiel 28 (Fortsetzung von Beispiel 27)<br />
Wir analysieren das Wort id + id mit Hilfe der in Tabelle 3.2 angegebenen Parse-<br />
Tabelle.<br />
Eingabe Keller Ausgabe<br />
id + id $ E $<br />
id + id $ T E ′ $ E → TE ′<br />
id + id $ F T ′ E ′ $ T → F T ′<br />
id + id $ id T ′ E ′ $ F → id<br />
+ id $ T ′ E ′ $ ε<br />
+ id $ E ′ $ T ′ → ε<br />
+ id $ + T E ′ $ E ′ → + T E ′<br />
id $ T E ′ $ ε<br />
id $ F T ′ E ′ $ T → F T ′<br />
id $ id T ′ E ′ $ F → id<br />
$ T ′ E ′ $ ε<br />
$ E ′ $ T ′ → ε<br />
$ $ E ′ → ε<br />
$ $ accept<br />
Nach dieser Analyse eines korrekten Eingabeworts analysieren wir das Eingabewort<br />
id + * id, das mit der Grammatik nicht erzeugt werden kann.<br />
Eingabe Keller Ausgabe<br />
id + * id $ E $<br />
id + * id $ T E ′ $ E → T E ′<br />
id + * id $ F T ′ E ′ $ T → F T ′<br />
id + * id $ id T ′ E ′ $ F → id<br />
+ * id $ T ′ E ′ $ ε<br />
+ * id $ E ′ $ T ′ → ε<br />
+ * id $ + T E ′ $ E ′ → + T E ′<br />
* id $ T E ′ $ ε<br />
* id $ T E ′ $ error<br />
Behandlung von mehrdeutigen Grammatiken<br />
Wir haben bereits gezeigt, daß mehrdeutige Grammatiken nicht die LL(k)-Eigenschaft<br />
besitzen <strong>und</strong> daher Eingaben mit ihnen nicht deterministisch analysierbar sind (siehe<br />
Satz 7). Als Beispiel <strong>für</strong> eine mehrdeutige Grammatik haben wir in Beispiel 18<br />
das dangling-else-Problem betrachtet. Obwohl die Grammatik <strong>für</strong> die if-then-else-<br />
Anweisung nicht LL(k) ist, wird sie in Übersetzern (z.B. <strong>für</strong> Pascal) verwendet. Wie<br />
kann man also eine mehrdeutige Grammatik parsen?<br />
66
3.3 Top-Down-Syntaxanalyse<br />
a b e i t $<br />
S S → a S → iEtSS ′<br />
S ′ S ′ → ε, S ′ → eS S ′ → ε<br />
E E → b<br />
Tabelle 3.3: Parse-Tabelle <strong>für</strong> dangling-else-Grammatik.<br />
Wir betrachten die linksfaktorisierte Grammatik <strong>für</strong> das dangling else aus Beispiel<br />
26,<br />
S → i E t S S’ | a<br />
S’ → e S | ε<br />
E → b,<br />
<strong>und</strong> konstruieren <strong>für</strong> diese Grammatik die zugehörige Parse-Tabelle M.<br />
Bei dieser Konstruktion ensteht ein doppelter Eintrag im Feld M[S ′ ,e]. Also ist<br />
die Grammatik nicht LL(1). Als pragmatische Lösung bietet sich an, die Produktion<br />
S ′ → ε aus dem Feld M[S ′ ,e] zu löschen, so daß gilt M[S ′ ,e] = {S ′ → eS}.<br />
Dies entspricht der üblichen Konvention, das else dem unmittelbar vorangehenden<br />
then zuzuordnen. Wenn wir uns in der innersten Schachtelung mehrerer if-then-<br />
Anweisungen befinden, <strong>und</strong> es steht ein else im lookahead, so soll dieses else dem<br />
innersten if zugeordnet werden.<br />
In diesem Beispiel ist durch geschickte Manipulation der Parse-Tabelle die deterministische<br />
Top-Down-Analyse ermöglicht worden. Es gibt aber keine allgemeine Regel<br />
<strong>für</strong> solche “Tricks”.<br />
3.3.3 Erweiterte kontextfreie Grammatiken<br />
Im vorherigen Abschnitt haben wir gesehen, daß linksrekursive Grammatiken nicht<br />
die LL(k)-Eigenschaft besitzen (siehe Satz 8). Bei der Beschreibung von Programmiersprachen<br />
treten linksrekursive Grammatiken aber relativ häufig auf, da sich mit<br />
ihnen “Auflistungen” sehr einfach beschreiben lassen (z.B. id+id+id in arithmetischen<br />
Ausdrücken, Anweisungsfolgen usw.). Aus diesem Gr<strong>und</strong> möchte man eine<br />
Lösung finden, die die einfache Beschreibung von Auflistungen erlaubt <strong>und</strong> trotzdem<br />
das deterministische Parsen ermöglicht. Diese Möglichkeit bieten die erweiterten<br />
kontextfreien Grammatiken. Sie beruhen auf folgender Idee: Da sich Auflistungen<br />
mit regulären Ausdrücken beschreiben lassen (durch den ∗ -Operator), lassen wir nun<br />
reguläre Ausdrücke auf der rechten Seite von Produktionen zu.<br />
Definition 28<br />
Eine erweiterte kontextfreie Grammatik ist ein Tupel G = (VN,VT,ρ,S), wobei VN,<br />
VT <strong>und</strong> S wie üblich definiert sind <strong>und</strong> ρ : VN → reg(VN ∪ VT) eine Abbildung der<br />
67
3 Syntaktische Analyse<br />
Nichtterminale in die Menge der regulären Ausdrücke über VN ∪ VT ist.<br />
Es ist möglich, ρ als Abbildung zu definieren, da verschiedene Alternativen <strong>für</strong> ein<br />
Nichtterminal mit Hilfe des Alternativoperators | in einem regulären Ausdruck zusammengefaßt<br />
werden können.<br />
Beispiel 29<br />
Betrachten wir die Grammatik S → a |b. Handelt es sich bei der Grammatik um eine<br />
“normale” kontextfreie Grammatik, besitzt das Nichtterminal S zwei Produktionen<br />
S → a <strong>und</strong> S → b, wobei das | als Trennsymbol zwischen den beiden Produktionen<br />
verwendet wird. Interpretieren wir die Grammatik als erweiterte kontextfreie Grammatik,<br />
besitzt S nur eine Produktion S → a | b, die den regulären Auswahloperator |<br />
enthält.<br />
Beispiel 30 (Arithmetische Ausdrücke)<br />
Wir geben eine erweiterte kontextfreie Grammatik <strong>für</strong> die Sprache der arithmetischen<br />
Audrücke aus Beispiel 24 an. Dabei ergänzen wir die Menge der Operatoren um −<br />
<strong>und</strong> /.<br />
S → E<br />
E → T {{ + | − } T } ∗<br />
T → F {{ ∗ | / } F } ∗<br />
F → ( E ) | id<br />
Hier verwenden wir die Zeichen { <strong>und</strong> } zur Klammerung der regulären Ausdrücke<br />
in den rechten Regelseiten der Grammatik. Die Linksrekursion der Grammatik aus<br />
Beispiel 24 wurde durch den regulären ∗ -Operator ersetzt. Ein Ausdruck (Nichtterminal<br />
E) besteht aus einem Term (Nichterminal T), dem beliebig viele (also auch<br />
keine) Terme, getrennt durch einen der Operatoren + oder −, folgen können. Die<br />
Auswahl zwischen + <strong>und</strong> − wird durch den regulären Auswahloperator | dargestellt.<br />
Analog zu Ableitungen <strong>für</strong> kontextfreie Grammatiken definieren wir Ableitungen <strong>für</strong><br />
erweiterte kontextfreie Grammatiken.<br />
Definition 29<br />
Sei G = (VN,VT,ρ,S) erweiterte kontextfreie Grammatik.<br />
Die Relation ⇒ R l ⊆ reg(VN ∪VT)×reg(VN ∪VT) (“leitet regulär links ab”) ist definiert<br />
durch<br />
68<br />
1. wXβ ⇒ R l wαβ <strong>für</strong> α = ρ(X),<br />
2. w(r1|...|rn)β ⇒ R l wriβ <strong>für</strong> 1 ≤ i ≤ n,<br />
3. w(r) ∗ β ⇒ R l wβ,<br />
w(r) ∗ β ⇒ R l wr(r) ∗ β.
S ⇒R l E<br />
⇒R l T {{+ | −}T } ∗<br />
⇒R l F {{∗ | /}F } ∗ {{+ | −}T } ∗<br />
⇒R l { ( E ) | id } {{∗ | /}F } ∗ {{+ | −}T } ∗<br />
⇒R l id {{∗ | /}F } ∗ {{+ | −}T } ∗<br />
⇒R l id {{+ | −}T } ∗<br />
⇒R l id {+ | −}T {{+ | −}T } ∗<br />
⇒R l id + T {{+ | −}T } ∗<br />
⇒R l id + F {{∗ | /}F } ∗ {{+ | −}T } ∗<br />
⇒R l id + { ( E ) | id } {{∗ | /}F } ∗ {{+ | −}T } ∗<br />
⇒R l id + id {{∗ | /}F } ∗ {{+ | −}T } ∗<br />
⇒R l id + id {∗ | /}F {{∗ | /}F } ∗ {{+ | −}T } ∗<br />
⇒R l id + id ∗ F {{∗ | /}F } ∗ {{+ | −}T } ∗<br />
⇒R l id + id ∗ { ( E ) | id } {{∗ | /}F } ∗ {{+ | −}T } ∗<br />
⇒R l id + id ∗ id {{∗ | /}F } ∗ {{+ | −}T } ∗<br />
⇒R l id + id ∗ id {{+ | −}T } ∗<br />
id + id ∗ id<br />
⇒ R l<br />
Abbildung 3.11: Beispiel einer regulären Ableitung.<br />
3.3 Top-Down-Syntaxanalyse<br />
⇒R∗ l sei die reflexive <strong>und</strong> transitive Hülle von ⇒R l . Die von G definierte Sprache ist<br />
dann L(G) = {w ∈ V ∗<br />
T | S ⇒R∗ l w}.<br />
Beispiel 31 (Fortsetzung von Beispiel 30)<br />
In Abbildung 3.11 ist die Ableitung des Satzes id+id∗id der Grammatik aus Beispiel<br />
30 angegeben.<br />
Für erweiterte kontextfreie Grammatiken gibt es analog die Klasse der erweiterten<br />
LL(k)-Grammatiken (kurz ELL(k)). Zur Berechnung der FIRST- <strong>und</strong> FOLLOW-<br />
Mengen der Symbole erweiterter kontextfreier Grammatiken kann das folgende Verfahren<br />
verwendet werden. Zuvor führen wir den Begriff der k-Konkatenation ein, der<br />
im Konstruktionsverfahren verwendet wird.<br />
Definition 30<br />
Die Abbildung ⊕k : V ∗<br />
T × V ∗<br />
T → V ≤k<br />
T , definiert durch u ⊕k v = k : uv heißt die k-<br />
Konkatenation.<br />
Analog läßt sich ⊕k auf Mengen von Wörtern definieren. Seien L,M Mengen von<br />
Wörtern. Dann ist L ⊕k M = {u ⊕k v | u ∈ L,v ∈ M}.<br />
Die k-Konkatenation von u <strong>und</strong> v berechnet den k-Präfix der Konkatenation von u<br />
<strong>und</strong> v.<br />
69
3 Syntaktische Analyse<br />
Konstruktion der FIRST- <strong>und</strong> FOLLOW-Mengen <strong>für</strong> eine ELL(1)-Grammatik.<br />
Die FIRST- <strong>und</strong> FOLLOW-Mengen <strong>für</strong> eine ELL(1)-Grammatik<br />
G = (VN,VT,P,S) werden wie folgt berechnet:<br />
• Sei X ∈ VN. FIRST(X) ist definiert durch:<br />
– Falls X → r ∈ P, wobei r regulärer Ausdruck über VN ∪ VT, dann<br />
FIRST(X) = FIRST(r).<br />
• Sei r regulärer Ausdruck über VN ∪VT. FIRST(r) ist induktiv definiert durch:<br />
– Falls r = ε, dann FIRST(r) = ε,<br />
– falls r = a mit a ∈ VT, dann FIRST(r) = {a},<br />
– falls r = r1 | r2, dann FIRST(r) = FIRST(r1) ∪ FIRST(r2),<br />
– falls r = r1r2, dann FIRST(r) = FIRST(r1) ⊕1 FIRST(r2),<br />
– falls r = (r1), dann FIRST(r) = FIRST(r1),<br />
– falls r = r ∗ 1, dann FIRST(r) = {ε} ∪ FIRST(r1).<br />
• Sei X ∈ VN. Als Hilfskonstrukt verwenden wir Fol(X,r), die Menge der Terminalwörter<br />
der Länge ≤ 1, die in einer erweiterten Satzform r – einem regulären<br />
Ausdruck über VN ∪ VT – direkt auf X folgen können.<br />
– Fol(X,ε) = ∅,<br />
– Fol(X,r) = ∅, falls X in r nicht vorkommt,<br />
– Fol(X,X) = {ε},<br />
– Fol(X,r1r2) = (Fol(X,r1) ⊕1 FIRST(r2)) ∪ Fol(X,r2),<br />
– Fol(X,r1|r2) = Fol(X,r1) ∪ Fol(X,r2),<br />
– Fol(X, (r)) = Fol(X,r),<br />
– Fol(X,r ∗ ) = Fol(X,r) ⊕1 FIRST(r ∗ ).<br />
• Nun definieren wir <strong>für</strong> X ∈ VN die Mengen FOLLOW(X) als die kleinsten<br />
Mengen mit:<br />
– Falls X = S, dann ε ∈ FOLLOW(X),<br />
– falls X → r ∈ P, dann Fol(Y,r) ⊆ FOLLOW(Y ) <strong>für</strong> alle Y ∈ VN,<br />
– falls X → r ∈ P <strong>und</strong> <strong>für</strong> ein Y gilt ε ∈ Fol(Y,r),<br />
dann FOLLOW(X) ⊆ FOLLOW(Y ).<br />
Bei den Definitionen ist zu beachten, daß ∅ ⊕1 M = ∅ <strong>für</strong> alle Mengen M.<br />
Übergangsgraphen <strong>für</strong> erweiterte kontextfreie Grammatiken<br />
Da in erweiterten kontextfreien Grammatiken die rechten Regelseiten reguläre Ausdrücke<br />
enthalten dürfen, stellen wir diese rechten Regelseiten analog zu Kapitel 2 als<br />
Übergangsgraphen dar. Diese können, wie in Kapitel 2 beschrieben, aus den regulären<br />
70
S:<br />
E:<br />
T:<br />
F:<br />
E<br />
T<br />
F<br />
ε<br />
ε<br />
T<br />
+<br />
F<br />
( E )<br />
id<br />
-<br />
*<br />
/<br />
3.3 Top-Down-Syntaxanalyse<br />
Abbildung 3.12: Übergangsgraphen <strong>für</strong> arithmetische Ausdrücke.<br />
Ausdrücken generiert werden.<br />
Beispiel 32 (Fortsetzung von Beispiel 30)<br />
In Abbildung 3.12 sind Übergangsgraphen <strong>für</strong> die rechten Regelseiten der erweiterten<br />
kontextfreien Grammatik aus Beispiel 30 zur Beschreibung der arithmetischen<br />
Ausdrücke angegeben.<br />
Wie in Abbildung 3.12 zu sehen ist, können Kanten der Übergangsgraphen auch mit<br />
Nichtterminalsymbolen beschriftet sein. Da jedem Nichtterminalsymbol ein eigener<br />
Übergangsgraph zugeordnet ist, stellt eine solche Kante eine Art von “Prozeduraufruf”<br />
des Graphen dar, der dem entsprechenden Nichtterminal zugeordnet ist. Auf<br />
diese Weise erhalten wir “rekursive endliche Automaten” (mit direkter oder indirekter<br />
Rekursion). Diese Übergangsgraphen entsprechen genau den üblichen Syntaxdiagrammen<br />
<strong>für</strong> Programmiersprachen.<br />
Syntaxdiagramme bilden die Basis <strong>für</strong> eine besonders einfache Implementierung von<br />
Parsern <strong>für</strong> LL(k)-Sprachen (insbesondere LL(1)) mit Hilfe von Programmiersprachen,<br />
die Rekursion erlauben. Diese Parser werden recursive descent-Parser genannt.<br />
Top-Down-Parsen durch “rekursiven Abstieg”<br />
Bisher haben wir Parse-Verfahren <strong>für</strong> LL(1)-Grammatiken mit Hilfe einer Datenstruktur<br />
“Keller” durch einen iterativen Algorithmus implementiert. Dieser verwen-<br />
71
3 Syntaktische Analyse<br />
det eine Parse-Tabelle, die Informationen über die nächste anzuwendende Produktion<br />
unter Berücksichtigung des lookaheads enthält.<br />
Nun betrachten wir das Verfahren der Top-Down-Analyse durch rekursiven Abstieg<br />
(recursive descent), welches die besonders einfache Implementierung eines LL(1)-<br />
Parsers erlaubt. Wir verwenden als Gr<strong>und</strong>lage dieses Verfahrens die oben erwähnten<br />
Syntaxdiagramme <strong>und</strong> erzeugen, analog zum Scanner, daraus ein Programm, welches<br />
jetzt aber rekursive Aufrufe enthält, da die Syntaxdiagramme ebenfalls Rekursion<br />
beinhalten.<br />
Für jedes Nichtterminal wird eine Prozedur nach folgendem Schema erzeugt:<br />
Beginne mit dem Anfangsknoten des Graphen. Betrachte die Ausgangskanten <strong>und</strong><br />
vergleiche das aktuelle Symbol im lookahead mit den FIRST-Mengen ihrer Beschriftungen.<br />
Eine ε-Kante wird nur dann benutzt, wenn das lookahead-Symbol in keiner<br />
dieser Mengen enthalten ist (in diesem Fall wird der lookahead mit den FOLLOW-<br />
Mengen der Beschriftungen verglichen). Wir unterscheiden die folgenden Fälle:<br />
• Falls die Kante mit einem Terminalsymbol beschriftet ist, das mit dem lookahead<br />
übereinstimmt, gehe zum Folgeknoten über (die Konstruktoren in den<br />
regulären Ausdrücken werden in Kontrollstrukturen wie sequentielle Komposition,<br />
if-, while-, repeat- <strong>und</strong> case-Anweisungen übersetzt) <strong>und</strong> fordere das<br />
nächste Symbol des Quellprogramms an.<br />
• Falls die Kante mit einem Nichtterminalsymbol beschriftet ist, rufe die entsprechende<br />
Prozedur auf.<br />
• Falls keine Kante anwendbar ist (auch keine ε-Kante), liegt ein Syntaxfehler<br />
vor.<br />
Das Programm beginnt die Syntaxanalyse durch den Aufruf der Prozedur <strong>für</strong> das<br />
Startsymbol.<br />
Beispiel 33<br />
Wir betrachten die Grammatik G zur Beschreibung einer Untermenge der Pascal-<br />
Typen mit den folgenden Produktionen:<br />
type → simple<br />
| ↑ id<br />
| array [ simple ] of type<br />
simple → integer<br />
| char<br />
| num dotdot num<br />
Für die Zeichenfolge “..” verwenden wir das Symbol dotdot, da diese Zeichenfolge<br />
als Einheit behandelt wird (wie “:=”) <strong>und</strong> somit schon vom Scanner als ein Symbol<br />
geliefert wird.<br />
Wir können G als “normale” oder als besonders einfache erweiterte kontextfreie<br />
Grammatik betrachten.<br />
72
type:<br />
simple:<br />
simple<br />
array [ simple ] of type<br />
integer<br />
char<br />
num dotdot num<br />
Abbildung 3.13: Übergangsgraphen <strong>für</strong> Pascal-Typen.<br />
3.3 Top-Down-Syntaxanalyse<br />
Die Syntaxdiagramme <strong>für</strong> G sind in Abbildung 3.13 dargestellt.<br />
Die Berechnung der FIRST-Mengen der Nichtterminalsymbole ergibt<br />
FIRST(simple) = {integer,char,num} <strong>und</strong><br />
FIRST(type) = FIRST(simple) ∪ {↑,array} =<br />
{integer,char,num, ↑,array}.<br />
Es treten keine ε-Kanten auf, also müssen die FOLLOW-Mengen nicht berücksichtigt<br />
werden. Anhand der Syntaxdiagramme können wir nun einen recursive descent-<br />
Parser implementieren (siehe Abbildung 3.14).<br />
Die Hilfsprozedur match prüft, ob ein Symbol mit dem aktuellen lookahead übereinstimmt.<br />
Ist dies der Fall, wird das nächste Symbol vom Scanner angefordert (durch<br />
den Aufruf von nexttoken) <strong>und</strong> in den lookahead geladen, andernfalls ist eine Fehlersituation<br />
eingetreten. In den Prozeduren type <strong>und</strong> simple wird die Auswahl zwischen<br />
den Ausgangskanten eines Knotens durch verschachtelte if-then-else-Anweisungen<br />
dargestellt. Als Bedingung wird dabei ein Vergleich des lookaheads mit der FIRST-<br />
Menge der jeweiligen Kantenbeschriftung verwendet. Ein Schritt, also die Verwendung<br />
einer Kante, wird durch die Prozedur match realisiert, indem sie zum einen<br />
überprüft, ob der aktuelle lookahead dem erwarteten Symbol entspricht, <strong>und</strong> zum anderen<br />
den aktuellen lookahead durch Anfordern des nächsten Symbols “verbraucht”.<br />
In Abbildung 3.15 ist der Aufrufgraph der Prozeduren des Parsers <strong>für</strong> die Analyse des<br />
Satzes array [ num dotdot num ] of integer angegeben. Die Aufrufe der Prozedur<br />
match bilden die Blätter des Aufrufgraphen, die der Prozeduren type <strong>und</strong> simple die<br />
inneren Knoten. Da jeder Aufruf von match der Überprüfung eines Terminalssymbols<br />
dient <strong>und</strong> der Aufruf von type <strong>und</strong> simple einem Nichtterminalsymbol entspricht, wird<br />
durch den Aufrufgraph der Prozeduren der Strukturbaum der Analyse implizit erzeugt.<br />
id<br />
73
3 Syntaktische Analyse<br />
1 procedure type ;<br />
2 begin<br />
3 if lookahead i s in {integer, char, num} then<br />
4 simple<br />
5 else<br />
6 if lookahead = ′ ↑ ′ then begin<br />
7 match( ′ ↑ ′ ) ; match(id ) ;<br />
8 end<br />
9 else<br />
10 if lookahead = array then begin<br />
11 match(array) ; match ( ’ [ ’ ) ; simple ;<br />
12 match ( ’ ] ’ ) ; match( of ) ; type<br />
13 end<br />
14 else error<br />
15 end;<br />
16<br />
17 procedure simple ;<br />
18 begin<br />
19 if lookahead = integer then<br />
20 match( integer )<br />
21 else<br />
22 if lookahead = char then<br />
23 match(char)<br />
24 else<br />
25 if lookahead = num then begin<br />
26 match(num) ; match(dotdot) ; match(num)<br />
27 end<br />
28 else error<br />
29 end;<br />
30<br />
31 procedure match ( t : token ) ;<br />
32 begin<br />
33 if lookahead = t then<br />
34 lookahead := nexttoken<br />
35 else error<br />
36 end };<br />
74<br />
Abbildung 3.14: Beispiel eines recursive descent-Parsers.
type<br />
match( array) match( ’[’) simple match( ’]’) match( of ) type<br />
match( num ) match( dotdot ) match( num ) simple<br />
3.3 Top-Down-Syntaxanalyse<br />
match( integer)<br />
Abbildung 3.15: Aufrufgraph einer recursive descent-Syntaxanalyse.<br />
Der recursive descent-Parser kommt ohne einen durch eine Datenstruktur explizit<br />
definierten Keller aus, da diese Aufgabe implizit vom Laufzeitsystem der Sprache, in<br />
der wir unseren Parser implementiert haben, übernommen wird.<br />
3.3.4 Fehlerbehandlung bei der Top-Down-Analyse<br />
Wir beschreiben zunächst die Fehlerbehandlung bei Parsern im allgemeinen. Die<br />
Fehlerbehandlung eines Parsers muß folgenden Kriterien genügen:<br />
• Fehler müssen zuverlässig festgestellt <strong>und</strong> durch verständliche Fehlermeldungen<br />
dem Benutzer mitgeteilt werden.<br />
• Nach einem gef<strong>und</strong>enen Fehler soll die Analyse des Quelltexts fortgeführt werden,<br />
um weitere Fehler im Programmtext entdecken zu können. Daher soll der<br />
Parser in einer Fehlersituation möglichst schnell wieder “aufsetzen”, d.h. mit<br />
der Analyse fortfahren. Dabei sollen sogenannte “Scheinfehler”, d.h. durch die<br />
Behandlung des ersten Fehlers verursachte Folgefehler, vermieden werden.<br />
• Die Fehlerbehandlung darf die schnelle Übersetzung korrekter Programme nicht<br />
behindern.<br />
Zu den Syntaxfehlern gehören<br />
• Zeichensetzungsfehler: zuviele oder zuwenige ’;’, ’;’ statt ’,’, usw.,<br />
• Operatorenfehler: z.B. ’=’ statt ’:=’,<br />
• Schlüsselwortfehler: fehlende oder falsch geschriebene Schlüsselwörter,<br />
• Fehler in der Klammerstruktur<br />
• ...<br />
Das Wiederaufsetzen (recovery) eines Parsers soll möglichst früh geschehen, d.h. es<br />
sollen möglichst wenige Symbole überlesen werden. Neben dem Wiederaufsetzen ist<br />
es durch heuristische Verfahren möglich, durch lokale Korrekturen, die der Parser<br />
vornimmt, die Analyse fortzusetzen.<br />
75
3 Syntaktische Analyse<br />
Wir betrachten verschiedene recovery-Strategien:<br />
Panic recovery: Die Idee bei dieser Strategie besteht darin, einen Teil der Eingabe<br />
nach Auftreten eines Fehlers zu überlesen <strong>und</strong> dann mit der Analyse fortzufahren.<br />
Das Ende des zu überlesenden Abschnitts der Eingabe wird durch synchronisierende<br />
Symbole angegeben. Synchronisierende Symbole sind z.B. die<br />
Symbole, die Anweisungen abschließen (etwa ’;’ oder end). Tritt während der<br />
Analyse einer Anweisung ein Fehler auf, wird der restliche Text der Anweisung<br />
überlesen (also alle Symbole bis zum nächsten synchronisierenden Symbol) <strong>und</strong><br />
die Analyse mit der nächsten Anweisung fortgesetzt. Der Nachteil dieser Methode<br />
besteht darin, daß eventuell relativ große Teile der Eingabe überlesen<br />
werden (<strong>und</strong> daher Fehler, die in diesen Abschnitten existieren, in der aktuellen<br />
Analyse nicht gef<strong>und</strong>en werden). Andererseits ist dieses Verfahren sicher,<br />
da die Eingabe stets verkürzt wird <strong>und</strong> das Verfahren daher terminiert.<br />
Konstrukt-orientierte recovery: Bei diesem Verfahren ist der Parser in der Lage,<br />
lokale Korrekturen im Quelltext beim Auftreten eines Fehlers vorzunehmen.<br />
Der Parser kann das Präfix der Eingabe durch eine modifizierte Symbolfolge<br />
ersetzen, so daß eine Fortsetzung der Analyse möglich ist. Zum Beispiel kann ein<br />
Parser, der bei der Analyse einer Zuweisung bemerkt, daß statt des Symbols ’:=’<br />
das Symbol ’=’ verwendet wurde, das ’=’ durch das ’:=’ ersetzen. Nachteilig ist<br />
bei dieser Strategie, daß die Gefahr von Endlos-Schleifen besteht, z.B. wenn der<br />
Parser immer wieder Symbole vor dem aktuellen Eingabesymbol einsetzt. Eine<br />
zusätzliche Schwierigkeit entsteht, wenn der Fehler tatsächlich früher auftritt,<br />
als er vom Parser bemerkt wird (z.B. wird ein fehlendes begin in Pascal erst<br />
bei der Analyse der folgenden Anweisung bemerkt). In diesem Fall nimmt der<br />
Parser die Korrekturversuche an der falschen Stelle innerhalb des Quelltextes<br />
vor.<br />
Fehlerproduktionen: Für die Behandlung von “Standard-Fehlern”, die in Programmiersprachen<br />
häufig auftreten, ist es manchmal hilfreich, die kontextfreie Grammatik<br />
um Produktionen <strong>für</strong> diese fehlerhaften Konstrukte zu erweitern. Tritt<br />
ein solcher Standard-Fehler auf, wird durch die Anwendung der zugehörigen<br />
Fehler-Produktion eine Behandlung des Fehlers vorgenommen.<br />
Globale Korrekturen: Diese Verfahren wenden spezielle Algorithmen an, die minimale<br />
Folgen von Änderungen finden, um die fehlerhafte Eingabe in eine korrekte<br />
Eingabe zu transformieren. Hierzu stehen Änderungsoperationen wie Einfügen,<br />
Löschen <strong>und</strong> Ändern von Symbolen zur Verfügung. Diese Verfahren sind im<br />
allgemeinen zu aufwendig <strong>und</strong> werden daher nur eingeschränkt in Parsern verwendet,<br />
z.B. <strong>für</strong> das Finden der Ersetzungsstrings bei der konstrukt-orientierten<br />
Fehlerbehandlung.<br />
76
3.3 Top-Down-Syntaxanalyse<br />
id + * ( ) $<br />
E E → T E’ E → T E’ synch synch<br />
E’ E’ → + T E’ E’ → ε E’ → ε<br />
T T → F T’ synch T → F T’ synch synch<br />
T’ T’ → ε T’ → * F T’ T’ → ε T’ → ε<br />
F F → id synch synch F → ( E ) synch synch<br />
Tabelle 3.4: Für Fehlerbehandlung modifizierte Parse-Tabelle.<br />
Recovery in Top-Down-Parsern<br />
LL(k)-Parser haben die Eigenschaft, Fehler im Quellprogramm zum frühestmöglichen<br />
Zeitpunkt zu entdecken. Fehler werden in dem Moment erkannt, in dem das bisher<br />
gelesene Präfix der Eingabe kein gültiges Wort sein kann (viable prefix-Eigenschaft).<br />
Ein Top-Down-Parser hat einen Fehler erkannt, wenn<br />
• das oberste Symbol des Kellers ein Terminalsymbol ungleich dem aktuellen<br />
lookahead ist,<br />
• oder wenn das oberste Symbol des Kellers ein Nichtterminal A ist, das Terminalsymbol<br />
a den aktuellen lookahead bildet <strong>und</strong> in der Parse-Tabelle M der<br />
Eintrag M[A,a] leer ist bzw. error enthält.<br />
Für die Strategie der panic recovery werden synchronisierende Symbole benötigt.<br />
Folgende Symbole eignen sich als synchronisierende Symbole:<br />
1. Für ein Nichtterminal A:<br />
• FOLLOW(A): der Parser überliest die Eingabe, bis er ein Element aus<br />
FOLLOW(A) findet. Außerdem wird A vom Keller entfernt (es wird dem<br />
Parser die korrekte Analyse des zu A gehörenden Abschnitts der Eingabe<br />
vorgetäuscht).<br />
• Symbole, die Konstrukte abschließen, etwa Anfangssymbole von höher stehenden<br />
Konstrukten (z.B. Schlüsselwörter, mit denen Anweisungen beginnen).<br />
Zum Beispiel werden in C Anweisungen durch ’;’ abgeschlossen.<br />
Fehlt im Quelltext ein ’;’, würde sonst der Beginn der nächsten Anweisung<br />
überlesen, da er nicht in der FOLLOW-Menge enthalten ist.<br />
• FIRST(A): hierdurch wird ein neuer Parse-Versuch <strong>für</strong> A möglich, falls<br />
beim Überlesen ein Symbol aus FIRST(A) gef<strong>und</strong>en wird.<br />
2. Für Terminale werden alle anderen Terminalsymbole als synchronisierende Symbole<br />
verwendet. Wenn das Terminalsymbol auf dem Keller nicht zum aktuellen<br />
lookahead paßt, wird es vom Keller entfernt <strong>und</strong> der Parser gibt eine Meldung<br />
aus, daß das Terminal in den Quelltext “eingefügt” wurde.<br />
Eine weitere Möglichkeit besteht darin, <strong>für</strong> Nichtterminale A mit Produktion A → ε<br />
77
3 Syntaktische Analyse<br />
78<br />
Eingabe Keller Ausgabe<br />
id * + id $ E $<br />
id * + id $ T E’ $ E → T E ′<br />
id * + id $ F T’ E’ $ T → F T ′<br />
id * + id $ id T’ E’ $ F → id<br />
* + id $ T’ E’ $ ε<br />
* + id $ * F T’ E’ $ T ′ → ∗ F T ′<br />
+ id $ F T’ E’ $ ε<br />
+ id $ T’ E’ $ error, F wird entfernt,<br />
da Eintrag synch<br />
+ id $ E’ $ T ′ → ε<br />
+ id $ + T E’ $ E ′ → + T E ′<br />
id $ T E’ $ ε<br />
id $ F T’ E’ $ T → F T ′<br />
id $ id T’ E’ $ F → id<br />
$ T’ E’ $ ε<br />
$ E’ $ T ′ → ε<br />
$ $ E ′ → ε<br />
$ $ accept<br />
Tabelle 3.5: Beispiel einer Ableitung mit Fehlerbehandlung.
3.3 Top-Down-Syntaxanalyse<br />
diese Produktion als “default” zu verwenden, d.h. wenn keine andere Produktion<br />
zum aktuellen lookahead paßt (dies verzögert eventuell die Fehlererkennung).<br />
Beispiel 34<br />
Wir modifizieren die Parse-Tabelle aus Tabelle 3.2 auf Seite 64, indem wir die error-<br />
Einträge aus der Tabelle entfernen. Als synchronisierende Symbole verwenden wir die<br />
FOLLOW-Mengen der Nichtterminale:<br />
FOLLOW(E) = {), $}, FOLLOW(T) = {+, ), $},<br />
FOLLOW(F) = {+, ∗, ), $}.<br />
In die Felder der Parse-Tabelle tragen wir in den Spalten, die mit den Symbolen<br />
aus der jeweiligen FOLLOW-Menge beschriftet sind, die Anweisung synch ein. Die<br />
übrigen Felder, die zuvor mit error beschriftet waren, lassen wir leer (Tabelle 3.4).<br />
Wir interpretieren die Parse-Tabelle wie folgt:<br />
• bei einem leeren Eintrag wird das aktuelle Eingabesymbol vom Parser überlesen,<br />
• bei einem Eintrag synch wird das Nichtterminal an der Kellerspitze vom Keller<br />
entfernt.<br />
Zusätzlich wird, falls das oberste Kellerelement ein Terminalsymbol ungleich dem<br />
lookahead ist, dieses Symbol vom Keller entfernt.<br />
Wir analysieren mit Hilfe der modifizierten Parse-Tabelle das Eingabewort id * +<br />
id (siehe Tabelle 3.5).<br />
Die fehlerhafte Eingabe wird durch die Fehlerbehandlung erfolgreich analysiert. Beim<br />
Erreichen des Fehlers wird eine Fehlermeldung ausgegeben <strong>und</strong> die Analyse durch das<br />
Löschen des Nichtterminals F vom Keller fortgesetzt.<br />
79
3 Syntaktische Analyse<br />
3.4 Bottom-Up-Syntaxanalyse<br />
Bei der Bottom-Up-Syntaxanalyse wird die Konstruktion des Strukturbaums einer<br />
Ableitung bei der Eingabe, also mit den Blättern des Baums, begonnen. Während<br />
der Analyse wird versucht, immer größere Abschnitte der Eingabe zu Nichtterminalen<br />
zusammenzufassen, bis am Ende der Analyse die gesamte Eingabe auf das Startsymbol<br />
der Grammatik reduziert wurde. Bei der Bottom-Up-Syntaxanalyse wird also<br />
der Strukturbaum von den Blättern zur Wurzel hin aufgebaut, während er bei der<br />
Top-Down-Analyse von der Wurzel ausgehend hin zu den Blättern konstruiert wird.<br />
Im Gegensatz zur Top-Down-Analyse, wo in jedem Schritt nur das oberste Symbol des<br />
Kellers verwendet werden konnte, erlauben wir bei der Bottom-Up-Syntaxanalyse zur<br />
Vereinfachung, daß auch mehrere Zeichen an der Kellerspitze gelesen werden können<br />
(<strong>für</strong> ein festes k ≥ 0). Dies ändert die Mächtigkeit des Modells der Kellerautomaten<br />
nicht, da das Lesen mehrerer Zeichen durch einen üblichen Kellerautomaten, der<br />
nur auf das oberste Kellersymbol zugreifen kann, simuliert werden kann (etwa unter<br />
Zuhilfenahme von k ∗ |Γ| Zuständen).<br />
Zur Erinnerung: <strong>für</strong> ein Alphabet Σ sei Σ≤k = �<br />
0≤i≤k Σi die Menge der Wörter mit<br />
maximaler Länge k einschließlich ε.<br />
Wie bei der Top-Down-Analyse beschreiben wir zunächst die allgemeine Bottom-Up-<br />
Analyse (auch Shift-Reduce-Verfahren genannt), bevor wir die Verfahren zur Realisierung<br />
der deterministischen Bottom-Up-Analyse einführen.<br />
Definition 31<br />
Der (nichtdeterministische) Bottom-Up-Analyseautomat <strong>für</strong> eine kontextfreie Grammatik<br />
G = (VN,VT,P,S) ist ein Kellerautomat mit<br />
• Eingabealphabet VT,<br />
• Kelleralphabet Γ = VN ∪ VT ∪ {$},<br />
• Kellerstartsymbol $,<br />
• Ausgabealphabet {1,...,p} <strong>und</strong> der<br />
• Übergangsrelation ∆ ⊆ ((VT ∪ {ε}) × Γ ≤k ) × (Γ ≤1 × {1,...,p} ≤1 ), wobei<br />
((ε, ← α), (A,i)) ∈ ∆ :⇔ π(i) = A → α (Reduktionsschritt),<br />
((a,ε), (a,ε)) ∈ ∆ <strong>für</strong> a ∈ VT (Shift-Schritt) <strong>und</strong><br />
((ε,S$), (ε,ε)) ∈ ∆ (Erkennungsende).<br />
Die Übergangsrelation enthält drei Arten von Regeln:<br />
80<br />
1. Die Regeln der Form ((ε, ← α), (A,i)) beschreiben einen Reduktionsschritt. Befindet<br />
sich die rechte Seite der i-ten Produktion A → α in umgekehrter Reihenfolge<br />
auf dem Keller ( ← α ist das zu α reverse Wort, auch handle genannt), wird die<br />
rechte Seite entfernt <strong>und</strong> durch das Nichtterminal A der Produktion ersetzt.<br />
Dabei wird die Nummer i der Produktion auf das Ausgabeband geschrieben
3.4 Bottom-Up-Syntaxanalyse<br />
<strong>und</strong> die Eingabe bleibt unverändert. Da die rechte Seite einer Produktion auf<br />
dem Keller zu einem Nichtterminalsymbol reduziert wird, wird die Anwendung<br />
der Regel als Reduktionsschritt bezeichnet.<br />
2. Für jedes Terminalsymbol der Grammatik wird eine Regel der Form ((a,ε), (a,ε))<br />
in die Übergangsrelation aufgenommen. Das aktuelle Symbol a der Eingabe<br />
wird aus der Eingabe entfernt <strong>und</strong> auf den Keller gelegt (das Symbol wird aus<br />
der Eingabe auf den Keller “geschoben”, daher wird die Anwendung eines solchen<br />
Übergangs als Shift-Schritt bezeichnet). Der Inhalt des Ausgabebandes<br />
wird nicht verändert.<br />
3. Die Regel((ε,S$), (ε,ε)) definiert das Erkennungsende der Analyse. Ist die Eingabe<br />
leer, d.h. alle Symbole der Eingabe wurden gelesen, <strong>und</strong> befinden sich auf<br />
dem Keller nur das Startsymbol der Grammatik <strong>und</strong> das Kellerstartsymbol $,<br />
ist die Analyse beendet <strong>und</strong> der Kellerautomat hat das Eingabewort akzeptiert.<br />
Durch die Übergangsrelation wird die Arbeitsweise des Bottom-Up-Parsers definiert.<br />
Die Eingabe wird symbolweise auf den Keller “geschoben”. Sobald auf dem Keller eine<br />
rechte Seite einer Produktion der Grammatik in umgekehrter Reihenfolge vorliegt,<br />
kann diese rechte Regelseite vom Keller entfernt <strong>und</strong> durch das Nichtterminalsymbol<br />
der linken Seite der Produktion ersetzt werden. Auf diese Weise werden immer größere<br />
Anfangsstücke der Eingabe auf den Keller gelegt <strong>und</strong> durch Reduktionsschritte in<br />
Nichtterminale umgewandelt. Die Analyse ist beendet, wenn die gesamte Eingabe<br />
gelesen, auf den Keller geschoben <strong>und</strong> zum Startsymbol der Grammatik reduziert<br />
werden konnte.<br />
Beispiel 35 (Bottom-Up-Analyse arithmetischer Ausdrücke)<br />
Wir betrachten wieder die Grammatik aus Beispiel 19 auf Seite 49:<br />
E → E A E (1)<br />
| ( E ) (2)<br />
| - E (3)<br />
| id (4)<br />
A → + (5)<br />
| - (6)<br />
| * (7)<br />
| / (8)<br />
| ↑ (9)<br />
Analog zu Beispiel 19 analysieren wir die Eingabe -(id+id). Die Bottom-Up-Analyse<br />
ist in Tabelle 3.6 angegeben.<br />
Die bei der Analyse entstandene Ausgabe ist eine Rechtsanalyse, d.h. sie stellt eine<br />
Rechtsableitung in umgekehrter Reihenfolge dar:<br />
E 3 ⇒ - E 2 ⇒ - ( E ) 1 ⇒ - ( E A E ) 4 ⇒ - ( E A id ) 5 ⇒ - ( E + id )<br />
81
3 Syntaktische Analyse<br />
4<br />
E<br />
5<br />
- ( id - ( id<br />
E A E<br />
- ( id + id<br />
1<br />
3<br />
E<br />
E A E<br />
- ( id + id<br />
-<br />
E<br />
E<br />
E<br />
E A E<br />
E A<br />
- ( id +<br />
2<br />
( id + id )<br />
4<br />
E<br />
E<br />
E A E<br />
- ( id + id )<br />
Abbildung 3.16: Erzeugung eines Strukturbaums <strong>für</strong> einen arithmetischen Ausdruck.<br />
82
Eingabe Kellerinhalt Ausgabe<br />
- ( id + id ) $<br />
( id + id ) - $ ε<br />
id + id ) ( - $ ε<br />
+ id ) id ( - $ ε<br />
+ id ) E ( - $ 4<br />
id ) + E ( - $ ε<br />
id ) A E ( - $ 5<br />
) id A E ( - $ ε<br />
) E A E ( - $ 4<br />
) E ( - $ 1<br />
ε ) E ( - $ ε<br />
ε E - $ 2<br />
ε E $ 3<br />
ε ε accept<br />
3.4 Bottom-Up-Syntaxanalyse<br />
Tabelle 3.6: Beispielableitung eines Bottom-Up-Parsers.<br />
4<br />
⇒ - ( id + id )<br />
In Abbildung 3.16 ist die Konstruktion des Strukturbaums gemäß den Analyseschritten<br />
der Tabelle dargestellt. Es entsteht derselbe Strukturbaum wie bei der Top-Down-<br />
Analyse in Abbildung 3.8, aber bei der Bottom-Up-Analyse wird der Strukturbaum<br />
“von unten nach oben” aufgebaut.<br />
Auch bei der allgemeinen Bottom-Up-Syntaxanalyse tritt Nichtdeterminismus auf.<br />
Wir betrachten die verschiedenen Situationen <strong>für</strong> Nichtdeterminismus:<br />
• Ein Shift-Schritt ist immer möglich, solange die Eingabe nicht leer ist. Wann<br />
soll eine Reduktion durchgeführt werden?<br />
• Das Erkennungsende ist unklar, falls das Startsymbol S in einer rechten Regelseite<br />
auftritt, z.B. in einer Grammatik mit den Produktionen S → A <strong>und</strong><br />
A → S. Wenn auf dem Keller ein S steht, soll die Analyse beendet werden oder<br />
muß das S zu einem A reduziert werden?<br />
• Eventuell stehen mehrere Reduktionsmöglichkeiten zur Auswahl, z.B. bei den<br />
Produktionen A → α <strong>und</strong> B → α. Wenn auf dem Keller ← α steht, wird dann zu<br />
A oder zu B reduziert? Ein weiteres Problem entsteht z.B. <strong>für</strong> die Grammatik<br />
A → Bb |b. Wenn auf dem Keller die Symbole bB liegen (b Kellerspitze), soll<br />
dann nur das b oder bB zu A reduziert werden?<br />
Um eine deterministische Bottom-Up-Syntaxanalyse durchführen zu können, beschränken<br />
wir uns auf die Klasse der Sprachen, die mit einem lookahead von k<br />
83
3 Syntaktische Analyse<br />
Symbolen deterministisch zu erkennen sind. Diese Sprachen werden LR(k)-Sprachen<br />
genannt.<br />
3.4.1 LR(k)-Grammatiken<br />
Analog zu den LL(k)-Grammatiken bei der Top-Down-Analyse gibt es <strong>für</strong> die Bottom-<br />
Up-Analyse eine Klasse von kontextfreien Grammatiken, die die deterministische<br />
Analyse von Eingabewörtern erlauben. Diese Grammatiken heißen LR(k)-Grammatiken<br />
(das L steht <strong>für</strong> das Lesen der Eingabe von links nach rechts, das R steht <strong>für</strong> die<br />
Konstruktion einer Rechtsableitung). Wie die LL(k)-Grammatiken verwenden die<br />
LR(k)-Grammatiken einen lookahead von k Symbolen, um die Auswahl der nächsten<br />
Aktion deterministisch treffen zu können.<br />
Wir haben zuvor verschiedene Arten des Nichtdeterminismus betrachtet. Dabei haben<br />
wir gesehen, daß das Erkennungsende unklar ist, sobald das Startsymbol der<br />
Grammatik in rechten Regelseiten auftritt. Um diese Art von Nichtdeterminismus zu<br />
vermeiden, betrachten wir im folgenden nur Grammatiken, die startsepariert sind.<br />
Definition 32<br />
Eine kontextfreie Grammatik G = (VN,VT,P,S) heißt startsepariert, wenn S nur in<br />
einer Produktion S → A, A �= S, vorkommt.<br />
Wir können im folgenden voraussetzen, daß die Grammatiken startsepariert sind,<br />
denn zu einer Grammatik G läßt sich eine äquivalente startseparierte Grammatik G ′<br />
wie folgt konstruieren:<br />
Sei S ′ Startsymbol von G ′ , <strong>und</strong> S ′ komme in G nicht vor. Die Produktionen von G ′<br />
sind die Produktionen von G <strong>und</strong> die Produktion S ′ → S. Damit ist G ′ startsepariert.<br />
Auf diese Weise kann das Shift-Reduce-Verfahren <strong>für</strong> G ′ akzeptieren, wenn der<br />
Kellerinhalt S ′ <strong>und</strong> die Eingabe leer ist.<br />
Für mehrdeutige Grammatiken gibt es zu mindestens einem Satz mehr als eine<br />
Rechtsableitung. Also können wir in diesem Fall auch kein deterministisches Shift-<br />
Reduce-Verfahren erwarten.<br />
Nehmen wir also <strong>für</strong> die folgenden Überlegungen an, daß die betrachtete Grammatik<br />
eindeutig ist <strong>und</strong> daher jede Satzform genau eine Rechtsableitung besitzt.<br />
Definition 33<br />
Sei S ⇒ ∗ r βXu ⇒r βαu eine Rechtsableitung einer kontextfreien Grammatik G. α<br />
heißt dann der Griff der Rechtssatzform βαu.<br />
In einer nicht mehrdeutigen Grammatik ist der Griff einer Rechtssatzform das eindeutig<br />
bestimmte Teilwort, welches in einer Bottom-Up-Analyse im nächsten Reduktionsschritt<br />
durch ein Nichtterminal ersetzt werden muß, um zu einer Rechtsableitung<br />
zu gelangen.<br />
84
3.4 Bottom-Up-Syntaxanalyse<br />
Sei S = α0 ⇒r α1 ⇒r ... ⇒r αm = w eine Rechtsableitung. Eine Grammatik G ist<br />
LR(k), wenn in jeder solchen Rechtsableitung <strong>und</strong> in jedem αi der Griff lokalisiert<br />
<strong>und</strong> die anzuwendende Produktion eindeutig bestimmt werden kann, indem man die<br />
αi von links bis höchstens k Symbole hinter dem Griff betrachtet. Wenn wir also die<br />
αi in αi = αβw aufteilen <strong>und</strong> es eine Produktion X → β gibt, dann ist der Schritt<br />
nach αi−1 = αXw eindeutig durch αβ <strong>und</strong> k : w bestimmt. Eine Grammatik G<br />
ist also genau dann LR(k), wenn der nächste anzuwendende Schritt aus der bisher<br />
gelesenen Eingabe (bis zum Ende des Griffs) <strong>und</strong> einem lookahead von k Symbolen<br />
nach dem Griff eindeutig zu bestimmen ist.<br />
Definition 34<br />
Sei G = (VN,VT,P,S) startseparierte kontextfreie Grammatik. G heißt LR(k)-Grammatik,<br />
wenn aus<br />
S<br />
⇒ ∗ r α X w ⇒r α β w<br />
⇒ ∗ r γ Y x ⇒r α β y<br />
<strong>und</strong> k : w = k : y folgt, daß α = γ <strong>und</strong> X = Y <strong>und</strong> x = y.<br />
Wenn k : w = k : y gilt, müssen also folgende Bedingungen erfüllt sein, damit G die<br />
LR(k)-Eigenschaft besitzt:<br />
• X = Y , d.h. das Nichtterminal, zu dem reduziert wird, muß eindeutig sein,<br />
• α = γ, d.h. der Griff befindet sich, von “vorne” gesehen, an derselben Position<br />
der Satzform,<br />
• x = y, d.h. der Griff befindet sich, von “hinten” gesehen, an derselben Position<br />
der Satzform.<br />
Durch die zweite <strong>und</strong> die dritte Bedingung ist die Zerlegung der Satzform eindeutig<br />
bestimmt.<br />
Beispiel 36<br />
1. Wir überprüfen, ob die Grammatik G mit den Produktionen<br />
S → a A c<br />
A → b A b | b<br />
die LR(1)-Eigenschaft besitzt. Dazu wenden wir Definition 34 an:<br />
S<br />
⇒ ∗ r<br />
α<br />
����<br />
ab<br />
⇒ ∗ r abb<br />
����<br />
γ<br />
X<br />
����<br />
A<br />
A<br />
����<br />
Y<br />
w<br />
����<br />
bc ⇒r<br />
bbc<br />
����<br />
x<br />
α<br />
����<br />
ab<br />
⇒r ab<br />
����<br />
α<br />
β<br />
����<br />
b<br />
b<br />
����<br />
β<br />
w<br />
����<br />
bc<br />
bbbc<br />
����<br />
y<br />
85
3 Syntaktische Analyse<br />
Es gilt 1 : w = 1 : y = b, aber α �= γ <strong>und</strong> x �= y. Also ist G keine LR(1)-<br />
Grammatik. Man kann zeigen, daß G <strong>für</strong> kein k eine LR(k)-Grammatik ist.<br />
2. Gegeben sei die Grammatik G ′ mit den folgenden Produktionen:<br />
S → a A c<br />
A → A b b | b<br />
Die Sprache L(G ′ ) = L(G) = {ab 2n+1 c | n ≥ 0} ist identisch mit der unter<br />
Punkt 1 erzeugten Sprache. Im Gegensatz zu G ist G ′ eine LR(0)-Grammatik.<br />
Wir betrachten die Rechtssatzformen, die während der Ableitung eines Wortes<br />
aus L(G ′ ) auftreten können:<br />
a) a A c<br />
b) a A b b b 2n c, n ≥ 0<br />
c) a b b 2n c, n ≥ 0<br />
In jedem dieser drei Fälle ist der Griff <strong>und</strong> die anzuwendende Produktion bereits<br />
durch den Präfix der Eingabe bis zum Ende des Griffs eindeutig bestimmt:<br />
a) trivial<br />
b) die Rechtssatzform läßt sich nur zu aAb 2n c in einer Rechtsableitung reduzieren<br />
(würden wir ein b zu einem A reduzieren, bekämen wir eine Rechtssatzform<br />
mit zwei A’s, die nicht weiter reduziert werden könnte, also ist<br />
der Griff Abb eindeutig). Dieser Fall ist durch das Präfix aAbb bestimmt.<br />
c) der Griff muß das erste b sein, da wir <strong>für</strong> weitere Ableitungsschritte das<br />
Nichtterminal A benötigen. Dieser Fall ist durch das Präfix ab bestimmt.<br />
Da die einzelnen Fälle durch das Präfix der Eingabe bis zum Ende des Griffs<br />
eindeutig bestimmt sind, ist kein weiterer lookahead notwendig. Also ist die<br />
Grammatik LR(0).<br />
3. Wir betrachten eine weitere Grammatik G ′′ , die dieselbe Sprache wie G <strong>und</strong> G ′<br />
erzeugt:<br />
S → a A c<br />
A → b b A | b<br />
Wir betrachten, analog zu 2., die möglichen Rechtssatzformen der Ableitungen:<br />
a) a A c<br />
b) a b 2n b b A c, n ≥ 0<br />
c) a b 2n b c, n ≥ 0<br />
Der Fall a) ist trivial. Die in den Fällen b) <strong>und</strong> c) auftretenden Rechtssatzformen<br />
lassen sich zu ab k bα mit k ≥ 0 zusammenfassen. Falls α = c, ist der Griff<br />
das letzte Auftreten von b. Sonst ist der Griff bbA, der sich mit α überschneidet.<br />
Also ist die Entscheidung über die anzuwendende Produktion mit einem<br />
lookahead von 1 eindeutig zu bestimmen. Daher ist G ′′ eine LR(1)-Grammatik.<br />
An diesem Beispiel sehen wir, daß es <strong>für</strong> eine Sprache mehrere Grammatiken geben<br />
86
kann, die sich in bezug auf die LR(k)-Eigenschaft unterscheiden.<br />
3.4 Bottom-Up-Syntaxanalyse<br />
Bevor wir detailliert auf das Konstruktionsverfahren <strong>für</strong> deterministische Bottom-<br />
Up-Parser eingehen, schildern wir kurz die Idee des Verfahrens.<br />
Idee <strong>für</strong> die Konstruktion des deterministischen Bottom-Up-Parsers <strong>für</strong> LR(k)-<br />
Grammatiken: LR(k)-Grammatiken besitzen die Eigenschaft, daß die Reduktion<br />
von αβw zu αAw durch αβ(k : w) vollständig bestimmt ist. Dies bedeutet, daß,<br />
wenn αβ auf dem Keller liegt <strong>und</strong> w in der Eingabe steht, man anhand von k : w<br />
entscheiden kann, ob ein Shift-Schritt oder ob <strong>und</strong> welcher Reduktionsschritt durchgeführt<br />
werden soll. Das Problem besteht darin, daß die Auswahl nicht nur vom<br />
Griff β, sondern auch dem weiteren Kellerinhalt, dem Präfix α, beeinflußt wird. Im<br />
allgemeinen gibt es jedoch unendlich viele mögliche Präfixe α. Daher ist es <strong>für</strong> die<br />
Konstruktion des Parsers notwendig, einen Weg zu finden, wie man die entsprechende<br />
Information endlich darstellen <strong>und</strong> in den Shift-Reduce-Algorithmus einbinden kann.<br />
Definition 35<br />
Sei S ⇒ ∗ r βXu ⇒r βαu eine Rechtsableitung zu einer kontextfreien Grammatik G.<br />
Dann heißt jedes Präfix von βα ein zuverlässiges Präfix von G.<br />
Ein zuverlässiges Präfix ist der Anfang einer Rechtssatzform, der sich nicht echt über<br />
den (bei einer nicht mehrdeutigen Grammatik eindeutigen) Griff hinaus erstreckt.<br />
In einem zuverlässigen Präfix ist also eine Reduktion höchstens am Ende möglich<br />
(sonst ist es bereits soweit wie möglich reduziert, da wir Rechtsableitungen betrachten).<br />
Zu einer Grammatik gibt es im allgemeinen unendlich viele zuverlässige Präfixe.<br />
Allerdings kann die Menge der zuverlässigen Präfixe endlich dargestellt werden. Wir<br />
zeigen im folgenden, daß die Menge der zuverlässigen Präfixe einer Grammatik durch<br />
einen endlichen Automaten darstellbar ist. Wir geben die Konstruktion dieses Automaten<br />
an <strong>und</strong> integrieren ihn in das Shift-Reduce-Verfahren.<br />
Definition 36<br />
Sei G kontextfreie Grammatik, A → αβ eine Produktion von G. Dann heißt das Tripel<br />
(A,α,β) ein (kontextfreies) Item (oder ein LR(0)-Element) von G. Wir schreiben das<br />
Item (A,α,β) als [A → α.β].<br />
Ein Item der Form [A → α.] heißt vollständig.<br />
ItG bezeichnet die Menge der Items von G.<br />
vollstItG bezeichnet die Menge der vollständigen Items von G.<br />
Eine Produktion A → XY Z erzeugt die Items [A → .XY Z], [A → X.Y Z], [A →<br />
XY.Z] <strong>und</strong> [A → XY Z.] (nur letzteres ist vollständig). Die Produktion A → ε<br />
erzeugt das Item [A → .].<br />
87
3 Syntaktische Analyse<br />
Items lassen sich wie folgt interpretieren: ein Item [A → α.β] repräsentiert einen<br />
Analysezustand, in dem beim Versuch, ein Wort <strong>für</strong> A zu erkennen, bereits ein Wort<br />
<strong>für</strong> α erkannt wurde.<br />
Die Menge der Items einer kontextfreien Grammatik ist endlich.<br />
Definition 37<br />
Ein Item [A → α.β] heißt gültig <strong>für</strong> ein zuverlässiges Präfix γα, wenn es eine Rechtsableitung<br />
S ⇒ ∗ r γAw ⇒r γαβw gibt.<br />
Gültige Items beschreiben Situationen während einer Rechtsanalyse. Das zuverlässige<br />
Präfix γα entspricht dem bisher bearbeiteten Teil der Eingabe. A → αβ könnte eine<br />
demnächst anzuwendende Produktion sein, wobei der Anteil α der rechten Seite<br />
bereits erzeugt wurde.<br />
Beispiel 37<br />
Gegeben sei die folgende startseparierte Grammatik <strong>für</strong> arithmetische Ausdrücke [WM96]:<br />
S → E<br />
E → E + T | T<br />
T → T * F | F<br />
F → ( E ) | id<br />
Wir geben <strong>für</strong> verschiedene Schritte der Ableitung einer Satzform zuverlässige Präfixe<br />
<strong>und</strong> <strong>für</strong> sie gültige Items an:<br />
a) S ⇒r E ⇒r E+T:<br />
E+ ist zuverlässiges Präfix <strong>und</strong> [E → E + .T] ist gültiges<br />
� �� �<br />
Griff<br />
Item (γ,w leer).<br />
b) S ⇒∗ r E+T ⇒r E+ ���� F : E+ ist wieder zuverlässiges Präfix <strong>und</strong> [T → .F] ist<br />
Griff<br />
gültiges Item (γ = E+).<br />
c) S ⇒∗ r E+F ⇒r E+id: E+ ist wieder zuverlässiges Präfix <strong>und</strong> [F → .id] ist<br />
gültiges Item (γ = E+).<br />
d) S ⇒ ∗ r (E+F) ⇒r (E+ (E)<br />
����<br />
Griff<br />
): (E+<br />
� �� �<br />
γ<br />
(<br />
����<br />
α<br />
Griff hinaus) <strong>und</strong> [F → (.E)] ist gültiges Item.<br />
ist zuverlässiges Präfix (nicht über den<br />
Satz 9<br />
Zu jedem zuverlässigen Präfix gibt es mindestens ein gültiges Item.<br />
Wir konstruieren nun einen endlichen Automaten, der zuverlässige Präfixe erkennt<br />
<strong>und</strong> darüber hinaus in seinen Zuständen Informationen über mögliche Reduktionen<br />
enthält.<br />
88
3.4 Bottom-Up-Syntaxanalyse<br />
Definition 38<br />
Sei G = (VN,VT,P,S ′ ) startseparierte kontextfreie Grammatik (S ′ → S sei die einzige<br />
Produktion, die S ′ enthält). Sei char(G) = (Qc,VN ∪ VT, ∆c,qc,Fc) mit<br />
Qc = ItG,<br />
qc = [S ′ → .S],<br />
Fc = vollstItG,<br />
∆c = {([A → α.Y β],Y, [A → αY.β]) | A → αY β ∈ P,Y ∈ VN ∪ VT }<br />
∪ {([A → α.Bβ],ε, [B → .γ]) | A → αBβ ∈ P,B → γ ∈ P }<br />
der charakteristische endliche Automat zu G.<br />
Die Zustände des charakteristischen endlichen Automaten werden durch die Items<br />
aus ItG gebildet. Endzustände sind die Zustände, die vollständigen Items entsprechen.<br />
Es gibt zwei Arten von Übergängen in diesem Automaten. Steht der “.”, der die<br />
aktuelle Analysesituation kennzeichnet, vor einem Symbol Y (Terminal- oder Nichtterminalsymbol),<br />
kann der Punkt hinter das Y bewegt werden. Dieser Übergang ist<br />
mit Y beschriftet <strong>und</strong> beschreibt, daß der Teil der Eingabe, der zu Y reduziert wird,<br />
analysiert wurde. Die zweite Art von Übergängen beschreibt die Ersetzung von Nichtterminalen<br />
durch ihre rechten Regelseiten. Steht der “.” vor einem Nichtterminal B,<br />
geht eine ε-Kante vom aktuellen Zustand zu einem neuen Zustand [B → .γ] über: B<br />
wurde durch die rechte Regelseite γ ersetzt, <strong>und</strong> es wurde noch kein Teilwort von γ<br />
analysiert (Expansionsübergänge).<br />
Beispiel 38 (Fortführung von Beispiel 37)<br />
Abbildung 3.17 enthält den charakteristischen endlichen Automaten <strong>für</strong> die Grammatik<br />
aus Beispiel 37. Jede “Zeile” aus Items entspricht dem “Schieben” des “.” durch<br />
eine Produktion der Grammatik. Am Ende der Zeilen befinden sich die vollständigen<br />
Items.<br />
Satz 10<br />
Ist γ ∈ (VN ∪ VT) ∗ <strong>und</strong> q ∈ Qc, dann gilt (qc,γ) ⊢ ∗ (q,ε) genau dann, wenn γ ein<br />
zuverlässiges Präfix <strong>und</strong> q ein gültiges Item <strong>für</strong> γ ist.<br />
Es gilt also:<br />
1. Der Automat erkennt genau zuverlässige Präfixe, wenn man alle Zustände als<br />
Endzustände betrachtet.<br />
2. Endzustände sind vollständige Items, das sind solche, die maximal langen zuverlässigen<br />
Präfixen entsprechen (d.h. wird während der Analyse ein vollständiges<br />
Item erreicht, muß reduziert werden). Wir können Items also wie folgt interpretieren:<br />
[A → α.β]: Griff ist unvollständig (später: Shift-Schritt)<br />
[A → α.]: Griff ist vollständig, also reduzieren.<br />
89
3 Syntaktische Analyse<br />
ε<br />
ε<br />
ε<br />
E<br />
[S .E] [S E.]<br />
ε<br />
ε ε<br />
E<br />
+ T<br />
[E .E+T] [E E.+T] [E E+.T] [E E+T.]<br />
ε<br />
ε<br />
T<br />
[E .T] [E T.]<br />
ε<br />
ε<br />
ε<br />
T * F<br />
[T .T*F] [T T.*F] [T T*.F] [T T*F.]<br />
ε<br />
ε<br />
F<br />
[T .F] [ T F.]<br />
ε<br />
ε<br />
( E )<br />
[F .(E)] [F (.E)] [F (E.)] [F (E).]<br />
ε<br />
id<br />
[F . id ] [F id .]<br />
Abbildung 3.17: Beispiel eines charakteristischen endlichen Automaten<br />
Die Items (<strong>und</strong> damit die Zustände des Automaten) enthalten somit Informationen<br />
<strong>für</strong> die Analyse.<br />
Beweis<br />
“⇒”:<br />
Induktion über die Länge der Berechnung. Bezeichne ⊢ n einen Übergang in char(G)<br />
in n Schritten.<br />
Induktionsanfang: (qc,γ) ⊢ 0 (qc,ε) gilt nur <strong>für</strong> γ = ε, ε ist zuverlässiges Präfix,<br />
qc = [S ′ → .S] ist gültiges Item <strong>für</strong> ε.<br />
Induktionsannahme: Die Behauptung gelte <strong>für</strong> alle Berechnungen der Länge kleiner<br />
als n. Sei nun (qc,γ) ⊢ n (q,ε).<br />
Induktionsschritt:<br />
1. Fall: (qc,γ) ⊢ n−1 (p,ε) ⊢ (q,ε), d.h. der letzte Schritt war ein ε-Übergang. Dann<br />
haben p <strong>und</strong> q die Form p = [Y → α.Xβ], q = [X → .δ]. Nach Induktionsannahme<br />
ist γ zuverlässiges Präfix <strong>und</strong> p gültig <strong>für</strong> γ. Dann ist nach Definition 37 auch q gültig<br />
<strong>für</strong> γ (sich vor X zu befinden bedeutet, sich vor δ zu befinden).<br />
2.Fall: γ = γ ′ X, (qc,γ ′ X) ⊢ n−1 (p,X) ⊢ (q,ε). Nach Induktionsannahme ist γ ′ zuverlässiges<br />
Präfix <strong>und</strong> p gültig <strong>für</strong> γ ′ , da (qc,γ ′ ) ⊢ n−1 (p,ε). Dann haben p <strong>und</strong> q die<br />
Form [Y → α.Xβ] bzw. [Y → αX.β]. Da p gültig ist <strong>für</strong> γ ′ , gibt es eine Rechtsableitung<br />
S ⇒ ∗ r δY u ⇒r δαXβu mit γ ′ = δα. Dann ist γ = γ ′ X ein zuverlässiges Präfix<br />
<strong>und</strong> q gültig <strong>für</strong> γ.<br />
90
3.4 Bottom-Up-Syntaxanalyse<br />
“⇐”:<br />
Induktion über die Länge von γ.<br />
Induktionsanfang: γ = ε: alle gültigen Items <strong>für</strong> das zuverlässige Präfix ε haben die<br />
Form q = [X → .α] mit S ′ ⇒ ∗ r Xβ (noch nichts erzeugt). Nach Konstruktion von<br />
char(G) gilt dann (qc,ε) ⊢ ∗ (q,ε) (nur Expansionsübergänge verwendet).<br />
Induktionsannahme: Die Behauptung gelte <strong>für</strong> alle zuverlässigen Präfixe der Länge<br />
kleiner als n <strong>und</strong> alle <strong>für</strong> sie gültigen Items q.<br />
Induktionsschritt: Sei γX zuverlässiges Präfix der Länge n <strong>und</strong> q gültiges Item <strong>für</strong><br />
γX, q = [A → β1.β2]. Dann gibt es eine Rechtsableitung S ⇒ ∗ r αAw ⇒r αβ1β2w <strong>und</strong><br />
γX = αβ1. Wir unterscheiden die folgenden beiden Fälle: entweder β1 �= ε, d.h. wir<br />
befinden uns noch in der Analyse <strong>für</strong> X; oder β1 = ε, d.h. γX = α, also ist X bereits<br />
auf dem Keller.<br />
1. Fall: β1 = γ ′ X, d.h. γ = αγ ′ . Dann ist [A → γ ′ .Xβ2] gültig <strong>für</strong> das zuverlässige<br />
Präfix γ. Nach Konstruktion von char(G) <strong>und</strong> Induktionsannahme gilt dann<br />
(qc,γX) ⊢ ∗ ([A → γ ′ .Xβ2],X) ⊢ ([A → γ ′ X.β2],ε).<br />
2. Fall: β1 = ε, d.h. α = γX (X “ist auf dem Keller”, d.h. es gab einen Schritt<br />
in der Rechtsanalyse, in dem das X dorthin gekommen ist). Wir betrachten in der<br />
Rechtsableitung S ′ ⇒ ∗ r αAw den Schritt, in dem das Vorkommen von X in α eingeführt<br />
wurde: S ⇒ ∗ r µBy ⇒r µνXρy mit µν = γ. Jeder spätere Schritt ersetzt<br />
nur Nichtterminale rechts von X (sonst handelt es sich nicht um eine Rechtsableitung).<br />
Dann ist [B → ν.Xρ] gültiges Item <strong>für</strong> das zuverlässige Präfix γ = µν. Nach<br />
Induktionsannahme <strong>und</strong> Konstruktion von char(G) gilt:<br />
(qc,γX) ⊢ ∗ ([B → ν.Xρ],X) ⊢ ([B → νX.ρ],ε) ⊢ ∗ ([A → β1.β2],ε)<br />
(beide Items sind gültig <strong>für</strong> γX). �<br />
Korollar 1<br />
Die Sprache der zuverlässigen Präfixe einer kontextfreien Grammatik ist regulär.<br />
Der charakteristische endliche Automat einer kontextfreien Grammatik ist nichtdeterministisch.<br />
Um den Automaten in der deterministischen Bottom-Up-Syntaxanalyse<br />
verwenden zu können, müssen wir ihn determinisieren. Hierzu kann das auf Seite 23<br />
angegebene Verfahren der Potenzmengenkonstruktion verwendet werden.<br />
Definition 39<br />
Der sich durch die Potenzmengenkonstruktion aus char(G) ergebende deterministische<br />
endliche Automat (Qd,VN ∪ VT,δd,qd,Fd) wird LR-DEA(G) genannt.<br />
Beispiel 39 (Fortführung von Beispiel 38)<br />
Wir führen das Verfahren der Potenzmengenkonstruktion <strong>für</strong> den charakteristischen<br />
endlichen Automaten aus Abbildung 3.17 durch.<br />
91
3 Syntaktische Analyse<br />
Der Startzustand des LR-DEA(G) ergibt sich laut dem Verfahren der Potenzmengenkonstruktion<br />
aus der Menge der ε-Folgezustände des Startzustands des NEA (also<br />
des char(G)). Daher ist der Startzustand<br />
S0 = {[S → .E], [E → .E+T], [E → .T], [T → .T ∗F], [T → .F],<br />
[F → .(E)], [F → .id]}.<br />
Der Folgezustand eines Zustands unter einem Symbol a ergibt sich aus den ε-Folgezuständen<br />
der Folgezustände der Elemente unter a im NEA. Die Übergänge [S → .E] E → [S →<br />
E.] <strong>und</strong> [E → .E+T] E → [E → E.+T] sind die einzigen Übergänge von Zuständen<br />
aus S0 unter E. Daher bilden wir aus den Folgezuständen einen neuen Zustand<br />
S1 = {[S → E.], [E → E.+T]}. Von den Zuständen in S1 gehen keine ε-Kanten<br />
aus, so daß keine weiteren Zustände in S1 aufgenommen werden müssen. Im LR-<br />
E<br />
DEA(G) gilt also: S0 → S1.<br />
Die übrigen Zustände des LR-DEA(G) werden analog gebildet. Abbildung 3.18 enthält<br />
den LR-DEA(G) <strong>für</strong> den char(G) aus Abbildung 3.17.<br />
Endzustände des LR-DEA(G) sind die Zustände, die Endzustände des char(G) (also<br />
vollständige Items) enthalten.<br />
Es gilt:<br />
1. Die Zustände des LR-DEA(G) einer Grammatik G zerlegen die Menge der zuverlässigen<br />
Präfixe in endlich viele disjunkte Teilmengen (die Teilmengen werden<br />
durch die Zustände dargestellt). Für jedes zuverlässige Präfix gibt es genau<br />
einen Zustand, in den der Parser durch die Analyse dieses Präfixes gelangt.<br />
2. Sei γ zuverlässiges Präfix, p(γ) der Zustand, in den der LR-DEA(G) mit γ<br />
übergeht. Dann enthält p(γ) genau alle gültigen Items <strong>für</strong> γ, also alle möglichen<br />
Analysesituationen, die am Ende der Analyse von γ vorliegen können<br />
(Eigenschaft der Potenzmengenkonstruktion).<br />
Der folgende Algorithmus gibt an, wie der LR-DEA(G) direkt aus der Grammatik<br />
G erzeugt werden kann.<br />
Direkte Konstruktion des LR-DEA(G).<br />
Eingabe: startseparierte kontextfreie Grammatik G = (VN,VT,P,S ′ )<br />
Ausgabe: LR-DEA(G) = (Qd,VN ∪ VT,δd,qd,Fd)<br />
Verfahren:<br />
1 var q,q ′ : set of item ;<br />
2<br />
3 function Start : set of item ;<br />
4 return ({[S ′ → .S]}) ;<br />
5<br />
6 function Closure (s : set of item) : set of item };<br />
92
7 begin<br />
8 q := s;<br />
3.4 Bottom-Up-Syntaxanalyse<br />
9 foreach ([X → α.Y β] ∈ q ∧ Y → γ ∈ P) ∧ ([Y → .γ] /∈ q) do<br />
10 q := q ∪ {[Y → .γ]}<br />
11 od };<br />
12 return q ;<br />
13 end;<br />
14<br />
15 function Successor (s : set of item , Y : VN ∪ VT ) : set of<br />
item ;<br />
16 return {[X → αY.β] | [X → α.Y β] ∈ s};<br />
17<br />
18 begin (∗ Hauptprogramm ∗)<br />
19 Qd := {Closure(Start)};<br />
20 δd := ∅;<br />
21 foreach (q ∈ Qd) ∧ (X ∈ VN ∪ VT) do<br />
22 q ′ := Closure(Successor(q,X));<br />
23 if q ′ �= ∅ then<br />
24 if q ′ /∈ Qd then<br />
25 Qd := Qd ∪ {q ′ }<br />
26 fi ;<br />
27 δd := δd ∪ {q X → q ′ }<br />
28 fi<br />
29 od<br />
30 end.<br />
Die Funktion Start liefert den Anfangszustand des LR-DEA(G). Die Funktion Closure<br />
liefert zu einer Menge s von Items eine Menge von Items, die die ε-Folgezustände<br />
der Zustände von s beinhalten. Successor berechnet <strong>für</strong> einen Zustand s, also eine<br />
Menge von Items, den Nachfolgezustand unter dem Symbol Y .<br />
Im Hauptprogramm wird zuerst die Zustandsmenge Qd des LR-DEA(G) mit dem<br />
Startzustand initialisiert. Der Startzustand enthält das Item [S ′ → .S] (durch Aufruf<br />
von Start) <strong>und</strong> dessen ε-Folgezustände (durch Aufruf von Closure). Dann werden<br />
in einer Schleife <strong>für</strong> jeden Zustand der aktuellen Menge Qd die Folgezustände unter<br />
dem jeweiligen Symbol X berechnet 1 . Ist dieser Zustand nicht leer <strong>und</strong> existiert er<br />
noch nicht in Qd, wird er in Qd aufgenommen. Die Übergangsrelation δd wird um<br />
den neuen Übergang erweitert.<br />
Wir konstruieren jetzt einen Kellerautomaten, in dem dieser endliche Automat “eingebaut”<br />
ist. Dieser Kellerautomat ist im allgemeinen noch nicht deterministisch. Zur<br />
1 foreach wird als “<strong>für</strong> jede Kombination aus q <strong>und</strong> X” interpretiert<br />
93
3 Syntaktische Analyse<br />
S<br />
0<br />
E<br />
T<br />
F<br />
(<br />
id<br />
S<br />
1<br />
S<br />
5<br />
S<br />
3<br />
F<br />
S<br />
4<br />
(<br />
T<br />
S<br />
2<br />
S0 = { [S → .E],<br />
[E → .E+T],<br />
[E → .T],<br />
[T → .T ∗F],<br />
[T → .F],<br />
[F → .(E)],<br />
[F → .id]}<br />
S1 = { [S → E.],<br />
[E → E.+T]}<br />
S2 = { [E → T.],<br />
[T → T.∗F]}<br />
S3 = { [T → F.]}<br />
S4 = { [F → (.E)],<br />
[E → .E+T],<br />
[E → .T],<br />
[T → .T ∗F],<br />
[T → .F],<br />
[F → .(E)],<br />
[F → .id]}<br />
94<br />
id<br />
+<br />
F<br />
E<br />
(<br />
id<br />
S<br />
6<br />
+<br />
T<br />
S<br />
8<br />
)<br />
(<br />
S<br />
11<br />
*<br />
id<br />
S5 = { [F → id.]}<br />
S6 = { [E → E+.T],<br />
[T → .T ∗F],<br />
[T → .F],<br />
[F → .(E)],<br />
[F → .id]}<br />
S7 = { [T → T ∗.F],<br />
[F → .(E)],<br />
[F → .id]}<br />
S8 = { [F → (E.)],<br />
[E → E.+T]}<br />
S9 = { [E → E+T.],<br />
[T → T.∗F]}<br />
S10 = { [T → T ∗F.]}<br />
S11 = { [F → (E).]}<br />
Abbildung 3.18: Beispiel eines LR-DEA.<br />
S<br />
9<br />
*<br />
F<br />
S S<br />
7 10
3.4 Bottom-Up-Syntaxanalyse<br />
Vereinfachung betrachten wir die Ausgabe des Automaten nicht.<br />
Bei den bisherigen Kellerautomaten wurde der Keller so dargestellt, daß sich die<br />
Kellerspitze links befand. Da sich bei der Bottom-Up-Analyse die bereits gelesenen<br />
Zeichen so auf dem Keller befinden, daß das zuletzt gelesenen Zeichen an der Kellerspitze<br />
steht, verwenden wir eine Darstellung von Kellerautomaten, bei der die<br />
Kellerspitze rechts angegeben wird. Auf diese Weise kann der Griff auf dem Keller<br />
ohne die sonst notwendige Invertierung dargestellt werden.<br />
Definition 40 Ein Kellerautomat (ohne Ausgabe) ist ein Tupel M = (Σ, Γ, ∆,z0)<br />
mit<br />
• Σ endliche Menge (Eingabealphabet),<br />
• Γ endliche Menge (Kelleralphabet),<br />
• z0 ∈ Γ (Kellerstartsymbol),<br />
• ∆ ⊆ ((Σ ∪ {ε}) × Γ ≤k ) × Γ ∗ ,k ∈ N (endliche Übergangsrelation).<br />
Die Menge der Konfigurationen von M ist K = Σ ∗ ×Γ ∗ , die Anfangskonfiguration <strong>für</strong><br />
w ∈ Σ ∗ lautet (w,z0), die Endkonfiguration ist (ε,ε). Die Schrittrelation ⊢M⊆ K×K<br />
ist definiert durch<br />
(aw,γα) ⊢M (w,γβ) gdw. ((a,α),β) ∈ ∆,a ∈ Σ ∪ {ε}.<br />
Definition 41<br />
Sei LR-DEA(G) = (Qd,VN ∪ VT,δd,qd,Fd).<br />
Sei K0 = (VT,Qd, ∆,qd) der Kellerautomat mit ∆ ⊆ ((VT ∪ {ε}) × Q ∗ d) × Q ∗ d<br />
definiert durch<br />
((a,q),qδd(q,a)) ∈ ∆ falls δd(q,a) definiert (Shift-Schritt),<br />
((ε,q0q1...qn),q0δd(q0,X)) ∈ ∆ falls [X → α.] ∈ qn, |α| = n (n ≥ 0) (Reduce-<br />
Schritt),<br />
((ε,qdq0),ε) ∈ ∆ falls [S ′ → S.] ∈ q0 (Erkennungsende).<br />
Eingabealphabet des Kellerautomaten K0 ist VT. Das Kelleralphabet von K0 bildet<br />
die Zustandsmenge Qd des LR-DEA(G), d.h. auf dem Keller von K0 können nur<br />
Zustände des LR-DEA(G) gespeichert werden. Das Kellerstartsymbol ist der Anfangszustand<br />
qd des LR-DEA(G). Die Übergangsrelation ∆ wird durch drei Arten<br />
von Übergängen gebildet:<br />
• In Übergängen der Form ((a,q),qδd(q,a)) wird das nächste Eingabezeichen a<br />
gelesen <strong>und</strong> der entsprechende Nachfolgezustand δd(q,a) auf den Keller gelegt<br />
(Kellerspitze ist rechts). Dies ist genau dann möglich, wenn der aktuelle Zustand<br />
q an der Spitze des Kellers mindestens ein Item der Form [X → ....a...] enthält.<br />
• In Übergängen der Form ((ε,q0q1...qn),q0δd(q0,X)) liegt eine Folge q0q1...qn von<br />
n + 1 Zuständen (q0 bezeichnet einen beliebigen Zustand) mit der Kellerspitze<br />
qn vor. Falls qn als aktueller Zustand ein vollständiges Item [X → α.] enthält<br />
95
3 Syntaktische Analyse<br />
<strong>und</strong> die Länge von α n Symbole beträgt, werden die n obersten Zustände vom<br />
Keller entfernt <strong>und</strong> durch den Nachfolgezustand δd(q0,X) ersetzt.<br />
• Analog zum früher angegebenen Bottom-Up-Automaten soll die Termination<br />
einer Analyse möglich sein, wenn “$S” auf dem Keller liegt, d.h. qd <strong>und</strong> ein Zustand<br />
q0, der [S ′ → S.] enthält. Diese Situation heißt Erkennungsende. Wegen<br />
der Startseparierung kommt [S ′ → S.] nur in einem Zustand vor. Der Übergang<br />
((ε,qdq0),ε) beschreibt den Akzeptanzschritt des Kellerautomaten. Ist die Eingabe<br />
leer <strong>und</strong> auf dem Keller liegen nur qd <strong>und</strong> q0, werden diese Zustände vom<br />
Keller entfernt. Da nun Eingabe <strong>und</strong> Keller leer sind, hält der Kellerautomat<br />
an.<br />
Der Kellerautomat “befindet sich” immer in dem Zustand, der gerade oben auf<br />
dem Keller liegt. qd spielt die Rolle des $ beim früher angegebenen Bottom-Up-<br />
Analyseautomaten (<strong>und</strong> gleichzeitig des Anfangszustands des Kellerautomaten), deshalb<br />
ist es richtig, daß qd immer auf dem Keller bleibt. Nur beim Erkennungsende<br />
wird qd vom Keller entfernt.<br />
Für jedes gelesene Symbol der Eingabe wird ein Zustand auf den Keller gelegt, <strong>und</strong><br />
zwar der Zustand, der vom aktuellen Zustand aus im LR-DEA(G) mit einer Kante<br />
zu erreichen ist, die mit dem gelesenen Symbol beschriftet ist. Enthält der aktuelle<br />
Zustand ein vollständiges Item, kann eine Reduktion durchgeführt werden. Dabei<br />
werden so viele Zustände vom Keller entfernt, wie die zugehörige Produktion Symbole<br />
auf der rechten Regelseite besitzt. Hierdurch wird ein Zustand q0 Kellerspitze.<br />
Nun wird der Folgezustand von q0 unter dem Nichtterminal X (also der linken Seite<br />
der Produktion) auf den Keller gelegt. Eine Eingabe wird erkannt, wenn der Kellerautomat<br />
nach einem Erkennungsende-Schritt mit leerer Eingabe <strong>und</strong> leerem Keller<br />
hält. Ein Problem tritt auf, wenn der Zustand q0 noch weitere Items enthält (siehe<br />
folgendes Beispiel), da dann der Automat nicht deterministisch das Ende der Analyse<br />
erkennen kann. Ein solcher Zustand ist ungeeignet (siehe Definition 42).<br />
Beispiel 40 (Fortführung von Beispiel 39)<br />
Wir geben die Übergangsrelation des Kellerautomaten K0 <strong>für</strong> den LR-DEA(G) aus<br />
Abbildung 3.18 an.<br />
96<br />
• Shift-Schritte:<br />
(( id,S0),S0S5) (( (,S0),S0S4) (( +,S1),S1S6)<br />
(( ∗,S2),S2S7) (( (,S4),S4S4) (( id,S4),S4S5)<br />
(( id,S6),S6S5) (( (,S6),S6S4) (( (,S7),S7S4)<br />
(( id,S7),S7S5) (( +,S8),S8S6) (( ),S8),S8S11)<br />
(( ∗,S9),S9S7)<br />
• Reduce-Schritte: (zu jedem Schritt wird die Produktion angegeben, die diesem<br />
Reduktionsschritt entspricht)
((ε,S0S1S6S9),S0S1) (E → E+T)<br />
((ε,S4S8S6S9),S4S8) (E → E+T)<br />
((ε,S0S2),S0S1) (E → T)<br />
((ε,S4S2),S4S8) (E → T)<br />
((ε,S0S2S7S10),S0S2) (T → T ∗F)<br />
((ε,S4S2S7S10),S4S2) (T → T ∗F)<br />
((ε,S6S9S7S10),S6S9) (T → T ∗F)<br />
((ε,S0S3),S0S2) (T → F)<br />
((ε,S4S3),S4S2) (T → F)<br />
((ε,S6S3),S6S9) (T → F)<br />
((ε,S0S4S8S11),S0S3) (F → (E))<br />
((ε,S4S4S8S11),S4S3) (F → (E))<br />
((ε,S6S4S8S11),S6S3) (F → (E))<br />
((ε,S7S4S8S11),S7S10) (F → (E))<br />
((ε,S0S5),S0S3) (F → id)<br />
((ε,S4S5),S4S3) (F → id)<br />
((ε,S6S5),S6S3) (F → id)<br />
((ε,S7S5),S7S10) (F → id)<br />
((ε,S0S1),ε) (S → E) (Erkennungsende),<br />
3.4 Bottom-Up-Syntaxanalyse<br />
In der folgenden Tabelle ist eine Ableitung des Kellerautomaten <strong>für</strong> das Eingabewort<br />
id+id*id angegeben (die Kellerspitze ist rechts):<br />
Eingabe<br />
id + id * id S0<br />
Kellerinhalt Ausgabe<br />
+ id * id S0 S5<br />
ε<br />
+ id * id S0 S3<br />
F → id<br />
+ id * id S0 S2<br />
T → F<br />
+ id * id S0 S1<br />
E → T<br />
id * id S0 S1 S6<br />
ε<br />
* id S0 S1 S6 S5 ε<br />
* id S0 S1 S6 S3 F → id<br />
* id S0 S1 S6 S9 T → F<br />
id S0 S1 S6 S9 S7 ε<br />
ε S0 S1 S6 S9 S7 S5 ε<br />
ε S0 S1 S6 S9 S7 S10 F → id<br />
ε S0 S1 S6 S9 T → T ∗F<br />
ε S0 S1 E → E+T<br />
ε ε accept<br />
Der Kellerautomat ist nichtdeterministisch, da im Zustand S1 des LR-DEA(G) neben<br />
dem Item [S → E.], das das Erkennungsende signalisiert, noch ein weiteres Item<br />
97
3 Syntaktische Analyse<br />
enthalten ist. Befindet sich der Automat im Zustand S1, muß nichtdeterministisch<br />
entschieden werden, ob das Erkennungsende oder ein Shift-Schritt ausgeführt werden<br />
soll. So hätte in der fünften Zeile der Tabelle mit der Ableitung auch das Erkennungsende<br />
ausgewählt werden können, was zu einem Fehler bei der Analyse geführt<br />
hätte.<br />
Der Kellerautomat K0 ist nichtdeterministisch, wenn ein Zustand q des LR-DEA(G)<br />
1. sowohl Shift- als auch Reduce-Übergänge erlaubt (Shift-Reduce-Konflikt),<br />
2. zwei verschiedene Reduce-Übergänge gemäß zweier verschiedener Produktionen<br />
hat (Reduce-Reduce-Konflikt).<br />
Im ersten Fall gibt es mindestens ein “Lese-Item” [X → α.aβ] <strong>und</strong> mindestens ein<br />
vollständiges Item in q. Im zweiten Fall gibt es mindestens zwei vollständige Items<br />
in q.<br />
Zustände, die solche Konflikte enthalten, nennen wir ungeeignet.<br />
Definition 42<br />
Sei Qd Zustandsmenge von LR-DEA(G). q ∈ Qd heißt ungeeignet, wenn q ein Item<br />
der Form [X → α.aβ], a ∈ VT, <strong>und</strong> ein vollständiges Item der Form [Y → γ.] enthält<br />
(Shift-Reduce-Konflikt) oder wenn q zwei verschiedene vollständige Items [Y → α.]<br />
<strong>und</strong> [Z → β.] enthält (Reduce-Reduce-Konflikt).<br />
Beispiel 41<br />
Die Zustände S1,S2 <strong>und</strong> S9 des LR-DEA(G) in Abbildung 3.18 sind ungeeignet, da<br />
jeder von ihnen einen Shift-Reduce-Konflikt enthält.<br />
Satz 11<br />
Eine kontextfreie Grammatik G ist genau dann eine LR(0)-Grammatik, wenn LR-<br />
DEA(G) keine ungeeigneten Zustände besitzt.<br />
Beweis<br />
“⇒”:<br />
Sei G LR(0)-Grammatik. Wir nehmen an, daß LR-DEA(G) einen ungeeigneten Zustand<br />
p enthält. Wir unterscheiden zwei Fälle:<br />
1. Fall: p enthält einen Reduce-Reduce-Konflikt. Dann hat p mindestens zwei verschiedene<br />
Reduce-Items [X → β.], [Y → δ.]. p ist eine nicht-leere Menge von zuverlässigen<br />
Präfixen zugeordnet (mit denen man nach p gelangt; nach Konstruktion<br />
von LR-DEA(G) sind alle Zustände erreichbar). Sei γβ ein solches zuverlässiges Präfix<br />
(nämlich das, das zu dem ersten Item paßt). Beide Reduce-Items sind gültig <strong>für</strong> γβ,<br />
d.h.<br />
98
S ′<br />
⇒ ∗ r γXw ⇒r γβw<br />
⇒ ∗ r νY y ⇒r νδy<br />
3.4 Bottom-Up-Syntaxanalyse<br />
mit γβ = νδ sind verschiedene Rechtsableitungen, was ein Widerspruch zur LR(0)-<br />
Eigenschaft ist.<br />
2. Fall: p enthält einen Shift-Reduce-Konflikt: analog.<br />
“⇐”:<br />
Wir nehmen an, daß LR-DEA(G) keine ungeeigneten Zustände enthält. Wir betrachten<br />
die Rechtsableitungen<br />
S ′<br />
⇒ ∗ r αXw ⇒r αβw<br />
⇒ ∗ r γY x ⇒r αβy<br />
<strong>und</strong> zeigen, daß α = γ,X = Y,x = y gilt. Mit αβ erreicht LR-DEA(G) einen Zustand<br />
p. Da p nicht ungeeignet, enthält p genau ein Reduce-Item, <strong>und</strong> zwar [X → β.], <strong>und</strong><br />
kein Shift-Item. p enthält alle <strong>für</strong> αβ gültigen Items, deshalb ist α = γ,X = Y <strong>und</strong><br />
x = y. �<br />
Mit dem oben konstruierten Kellerautomaten K0 haben wir also ein Parse-Verfahren<br />
<strong>für</strong> LR(0)-Grammatiken. Allerdings ist LR(0) <strong>für</strong> die Praxis zu eingeschränkt, obwohl<br />
LR(0) mächtiger ist als LL(0). Daher erweitern wir unser Konstruktionsverfahren <strong>für</strong><br />
den Fall k ≥ 1. Dazu müssen wir den lookahead miteinbeziehen, indem wir die Items<br />
um “passende” Vorausschaumengen erweitern. Die Auswahl des nächsten Schrittes<br />
kann dann in Abhängigkeit von diesen Vorausschaumengen getroffen werden.<br />
Definition 43<br />
Sei G kontextfreie Grammatik. [A → α.β,L] heißt LR(k)-Item von G, wenn A →<br />
αβ ∈ P <strong>und</strong> L ⊆ V ≤k<br />
T gilt. A → α.β heißt dann Kern, L Vorausschaumenge des<br />
Items.<br />
Ein LR(k)-Item [A → α.β,L] heißt gültig <strong>für</strong> ein zuverlässiges Präfix γα, falls es<br />
<strong>für</strong> alle u ∈ L eine Rechtsableitung S ⇒ ∗ r γAw ⇒r γαβw mit u = k : w gibt.<br />
Die bisher betrachteten Items sind LR(0)-Items, wenn wir [A → α.β, {ε}] mit [A →<br />
α.β] identifizieren.<br />
Analog zu den LR(0)-Items definieren wir Shift-Reduce- <strong>und</strong> Reduce-Reduce-Konflikte.<br />
Dabei werden die Vorausschaumengen mit einbezogen.<br />
Definition 44<br />
Sei I eine Menge von LR(1)-Items. I enthält einen Shift-Reduce-Konflikt, wenn es<br />
99
3 Syntaktische Analyse<br />
ein Item [X → α.aβ,L1] <strong>und</strong> ein Item [Y → γ.,L2] enthält, <strong>und</strong> es gilt a ∈ L2.<br />
I enthält einen Reduce-Reduce-Konflikt, wenn es zwei Items [X → α.,L1] <strong>und</strong> [Y →<br />
β.,L2] gibt mit L1 ∩ L2 �= ∅.<br />
Konstruktion von LR(1)-Parsern<br />
Wir ermöglichen jetzt dem Parser, bei seinen Entscheidungen in ungeeigneten Zuständen<br />
(im LR(0)-Sinn) den lookahead mit den Vorausschaumengen der LR(1)-Items zu vergleichen:<br />
• Enthält ein LR(1)-Parser-Zustand mehrere vollständige Items, so liegt trotzdem<br />
kein Reduce-Reduce-Konflikt vor, wenn ihre Vorausschaumengen disjunkt sind.<br />
• Enthält ein LR(1)-Parser-Zustand ein vollständiges Item [X → α.,L] <strong>und</strong> ein<br />
Shift-Item [Y → β.aγ,L ′ ], so liegt kein Shift-Reduce-Konflikt zwischen ihnen<br />
vor, wenn L das Symbol a nicht enthält.<br />
Im folgenden sei k = 1 <strong>und</strong> die zu analysierende Eingabe stets durch $ abgeschlossen.<br />
Daher sind die Vorausschaumengen immer Teilmengen von VT ∪ {$}.<br />
Analog zum auf Seite 92 angegebenen Algorithmus <strong>für</strong> LR(0)-Items läßt sich auch<br />
<strong>für</strong> LR(1)-Items ein Algorithmus LR(1)-GEN angeben, der die Berechnung des charakteristischen<br />
endlichen Automaten <strong>für</strong> eine LR(1)-Grammatik G durchführt.<br />
Algorithmus LR(1)-GEN.<br />
Eingabe: startseparierte kontextfreie Grammatik G = (VN,VT,P,S)<br />
Ausgabe: charakteristischer endlicher Automat eines kanonischen LR(1)-Parsers<br />
Verfahren:<br />
1 var q,q ′ : set of item ;<br />
2<br />
3 function Start : set of item ;<br />
4 return ({[S ′ → .S, {$}]}) ;<br />
5<br />
6 function Closure (s : set of item) : set of item ;<br />
7 begin<br />
8 q := s;<br />
9 foreach ([X → α.Y β,L] ∈ q ∧ Y → γ ∈ P) do<br />
10 if exist ([Y → .γ,L ′ ] ∈ q) then<br />
11 replace [Y → .γ,L ′ ] by [Y → .γ,L ′ ∪ (FIRST(βL)\{ε})]<br />
12 else q := q ∪ {[Y → .γ,FIRST(βL)\{ε}]}<br />
13 fi<br />
14 od;<br />
15 return q<br />
100
16 end;<br />
17<br />
3.4 Bottom-Up-Syntaxanalyse<br />
18 function Successor (s : set of item , Y : VN ∪ VT ) : set of<br />
item ;<br />
19 return {[X → αY.β,L] | [X → α.Y β,L] ∈ s};<br />
20<br />
21 begin (∗ Hauptprogramm ∗)<br />
22 Q := {Closure(Start)};<br />
23 δ := ∅;<br />
24 foreach (q ∈ Q) ∧ (X ∈ VN ∪ VT) {\ bf do<br />
25 q ′ := Closure(Successor(q,X));<br />
26 if q ′ �= ∅ then<br />
27 if q ′ /∈ Q then<br />
28 Q := Q ∪ {q ′ }<br />
29 fi ;<br />
30 δ := δ ∪ {q X → q ′ }<br />
31 fi<br />
32 od<br />
33 end.<br />
Zur Funktion Closure:<br />
Ist [X → α.Y β,L] gültig <strong>für</strong> ein zuverlässiges Präfix δα <strong>und</strong> ist Y → γ eine Alternative<br />
<strong>für</strong> Y , dann muß auch [Y → .γ] <strong>für</strong> δα gültig sein (nach Definition 43). Dann<br />
kann in einer Rechtssatzform jedes Symbol auf δαγ folgen, daß aus FIRST(βL) ist<br />
(β könnte leer sein). ε soll nicht in Vorausschaumengen auftreten, da die Eingabe<br />
mit $ abgeschlossen ist.<br />
Zur Funktion Successor:<br />
Die Vorausschaumengen der Items sind erst bei der Auswahl von Reduce-Items<br />
von Bedeutung. Aus diesem Gr<strong>und</strong> werden sie beim “Durchschieben”, also beim<br />
Übergang in einen Folgezustand, nicht modifiziert. Nach Definition 43 gilt: wenn<br />
[X → α.Y β,L] <strong>für</strong> γα gültig ist, dann ist [X → αY.β,L] gültig <strong>für</strong> γαY .<br />
Beispiel 42<br />
Wir betrachten wieder die aus Beispiel 37 auf Seite 88 bekannte Grammatik G zur<br />
Beschreibung arithmetischer Ausdrücke:<br />
S → E<br />
E → E + T | T<br />
T → T * F | F<br />
F → ( E ) | id<br />
In dem in Abbildung 3.18 angegebenen LR-DEA(G) sind die Zustände S1, S2 <strong>und</strong> S9<br />
ungeeignet, da sie jeweils einen Shift-Reduce-Konflikt enthalten.<br />
101
3 Syntaktische Analyse<br />
Wir berechnen die LR(1)-Items der Grammatik mit Hilfe des Algorithmus LR(1)-<br />
GEN auf Seite 100.<br />
Die Funktion Start liefert die Menge {[S → .E, {$}]}. Der Aufruf der Funktion Closure<br />
berechnet die ε-Folgezustände des Start-Items. Der “.” steht vor einem Nichtterminal<br />
E, also müssen wir die Items der Produktionen von E in den Anfangszustand<br />
des Automaten aufnehmen. Betrachten wir zunächst die Produktion E → E+T. Für<br />
die Vorausschaumenge dieses neuen Items ergibt sich β = ε <strong>und</strong> L = {$}, also ist<br />
das neue Item [E → .E+T, {$}]. In diesem Item steht der Punkt wieder vor dem<br />
Nichtterminal E, so daß wiederum die Items der Produktionen zu E aufgenommen<br />
werden müssen. Da das Item <strong>für</strong> den Kern E → .E+T schon in der Menge existiert,<br />
wird kein neues Item hinzugefügt, sondern die Vorausschaumenge aktualisiert.<br />
β = +T, also lautet das Item nun [E → .E+T, {$, +}]. Eine weitere Produktion von<br />
E ist E → T, so daß wir im ersten Schritt das Item [E → .T, {$}] <strong>und</strong> im zweiten<br />
das aktualisierte Item [E → .T, {$, +}] erhalten. Das letzte Item ergibt mit der Produktion<br />
T → T ∗F das neue Item<br />
[T → .T ∗F, {$, +, ∗}] <strong>und</strong> mit der Produktion T → F das Item<br />
[T → .F, {$, +, ∗}]. Letzteres ergibt mit der Produktion F → (E) das Item [F →<br />
.(E), {$, +, ∗}] (da β = ε in T → F). Dieses Item ergibt nichts neues, da der<br />
“.” hier vor einem Terminal steht. Entsprechend ergibt sich mit F → id das Item<br />
[F → .id, {$, +, ∗}].<br />
Wir nennen die Menge dieser Items, die wir durch den Aufruf Closure(Start) generiert<br />
haben, S ′ 0. Diese Menge entspricht dem Anfangszustand des Automaten zu G<br />
<strong>und</strong> wird als erstes Element in die Zustandsmenge Q aufgenommen.<br />
In den weiteren Schritten des Verfahrens werden die übrigen Zustände des Automaten<br />
berechnet, z.B.<br />
Successor(S ′ 0,E) = {[S → E., {$}], [E → E.+T, {$, +}]}<br />
= Closure(Successor(S ′ 0,E)).<br />
Diesen Zustand nennen wir S ′ 1. Die Berechnung der anderen Zustände erfolgt analog.<br />
Wir geben hier nur die Zustände S ′ 0, S ′ 1, S ′ 2 <strong>und</strong> S ′ 9 an:<br />
102
S ′ 0 = { [S → .E, {$}],<br />
[E → .E+T, {$, +}],<br />
[E → .T, {$, +}],<br />
[T → .T ∗F, {$, +, ∗}],<br />
[T → .F, {$, +, ∗}],<br />
[F → .(E), {$, +, ∗}],<br />
[F → .id, {$, +, ∗}]}<br />
S ′ 1 = { [S → E., {$}],<br />
[E → E.+T, {$, +}]}<br />
S ′ 2 = { [E → T., {$, +}],<br />
[T → T.∗F, {$, +, ∗}]}<br />
...<br />
S ′ 9 = { [E → E+T., {$, +}],<br />
[T → T.∗F, {$, +, ∗}]}<br />
3.4 Bottom-Up-Syntaxanalyse<br />
Im LR-DEA(G), der nur LR(0)-Items enthält, sind die Zustände S1, S2 <strong>und</strong> S9<br />
aufgr<strong>und</strong> von Shift-Reduce-Konflikten ungeeignet. Die Zustände S ′ 1, S ′ 2 <strong>und</strong> S ′ 9 des<br />
charakteristischen endlichen Automaten zu G enthalten diese Konflikte nicht mehr,<br />
da durch die Angabe von Vorausschaumengen (<strong>und</strong> damit der Einbeziehung des lookaheads)<br />
die Auswahl deterministisch getroffen werden kann. Zum Beispiel ist in S ′ 1 das<br />
+ nicht in der Vorausschaumenge des Reduce-Items enthalten, so daß bei einem + im<br />
lookahead ein Shift-Schritt <strong>und</strong> andernfalls ein Reduce-Schritt durchgeführt werden<br />
muß.<br />
Die Grammatik G zur Beschreibung arithmetischer Ausdrücke ist somit eine LR(1)-<br />
Grammatik.<br />
LR(1)-Parse-Tabellen<br />
Wie zuvor der LR-DEA(G) dient uns der charakteristische endliche Automat mit<br />
LR(1)-Items als Gr<strong>und</strong>lage <strong>für</strong> einen Parser. Wir wollen den Parser diesmal nicht als<br />
Kellerautomaten, sondern in algorithmischer Form angeben. Dazu verwenden wir,<br />
analog zur Top-Down-Analyse, eine Parse-Tabelle.<br />
Die Zeilen der Tabelle sind mit den Zuständen der Zustandsmenge Q des charakteristischen<br />
LR(1)-Automaten beschriftet. Die Tabelle ist in zwei Teiltabellen untergliedert:<br />
die action-Tabelle <strong>und</strong> die goto-Tabelle. Die Spalten der action-Tabelle sind<br />
mit den Terminalsymbolen <strong>und</strong> dem Endezeichen $ beschriftet. Das Feld action[q,a]<br />
gibt an, welche Aktion ausgeführt werden soll, wenn der Parser sich im Zustand q<br />
befindet <strong>und</strong> im lookahead das Zeichen a steht. Mögliche Aktionen sind<br />
103
3 Syntaktische Analyse<br />
Q q<br />
VT ∪ {$} VN ∪ VT<br />
a X<br />
Parser-Aktion <strong>für</strong><br />
(q,a)<br />
δ(q,X)<br />
action-Tabelle goto-Tabelle<br />
Abbildung 3.19: Struktur einer LR(1)-Parse-Tabelle.<br />
⎧<br />
⎪⎨<br />
action[q,a] =<br />
⎪⎩<br />
shift<br />
reduce X → α<br />
error<br />
accept<br />
Die goto-Tabelle dient zum Nachschlagen der Folgezustände des charakteristischen<br />
LR(1)-Automaten.<br />
Das folgende Verfahren konstruiert eine LR(1)-action-Tabelle.<br />
Konstruktion der LR(1)-action-Tabelle.<br />
Eingabe: LR(1)-Zustandsmenge Q<br />
Ausgabe: action-Tabelle<br />
Verfahren:<br />
104<br />
1. Für jeden Zustand q ∈ Q <strong>und</strong> <strong>für</strong> jedes LR(1)-Item [K,L] ∈ q:<br />
• falls K = S ′ → S. <strong>und</strong> L = {$}: trage accept in action[q, $] ein,<br />
• sonst<br />
– falls K = X → α., trage reduce X → α in action[q,a] <strong>für</strong> alle a ∈ L<br />
ein,<br />
– falls K = X → α.aβ, trage shift in action[q,a] ein.<br />
2. Trage in jedes noch <strong>und</strong>efinierte Feld error ein.
3.4 Bottom-Up-Syntaxanalyse<br />
Mit Hilfe der Parse-Tabelle kann der folgende Parse-Algorithmus die Analyse eines<br />
Eingabeworts deterministisch durchführen.<br />
LR(1)-Parse-Algorithmus.<br />
Eingabe: aus der action- <strong>und</strong> der goto-Tabelle bestehende LR(1)-Parse-Tabelle<br />
Ausgabe: Rechtsableitung von w, falls w ∈ L(G), sonst Fehlermeldung<br />
Verfahren:<br />
Anfangskonfiguration:<br />
q0 liegt auf dem Keller (q0 ist Anfangszustand des oben konstruierten charakteristischen<br />
endlichen Automaten), w$ steht im Eingabepuffer <strong>und</strong> der Eingabezeiger steht<br />
auf dem ersten Zeichen von links.<br />
1 repeat<br />
2 s e i q Zustand an der Spitze des Kellers <strong>und</strong> a aktuelles<br />
Eingabesymbol<br />
3 if action[q,a] = shift then<br />
4 lege goto[q,a] auf den Keller <strong>und</strong> r”ucke den<br />
Eingabezeiger vor<br />
5 else if action[q,a] = reduce X → α then begin<br />
6 entferne |α| Eintr”age vom Keller −− s e i nun q ′ oberster<br />
Kellereintrag ;<br />
7 lege goto[q ′ ,X] auf den Keller ; gib X → α aus<br />
8 end<br />
9 else if action[q,a] = accept then return<br />
10 else (∗ action[q,a] = error ∗) error<br />
11 end<br />
SLR(1), LALR(1)-Analyse<br />
Bei der Konstruktion eines LR(1)-Parsers ergibt sich das Problem, daß im allgemeinen<br />
mehr Zustände erzeugt werden, als dies bei der Generierung eines LR(0)-Parsers<br />
der Fall ist. Die Ursache hier<strong>für</strong> ist die in den Zuständen enthaltene Information über<br />
den Linkskontext, die sich über die Vorausschaumenge fortpflanzt <strong>und</strong> ein Aufsplitten<br />
der Zustände verursacht (siehe Definition 37).<br />
Beispiel 43 (vereinfachte C-Zuweisung)<br />
Wir betrachten die folgende Grammatik zur Beschreibung des (vereinfachten) Zuweisungsoperators<br />
der Sprache C [KR90]:<br />
S’ → S<br />
S → L = R | R<br />
L → * R | id<br />
R → L<br />
105
3 Syntaktische Analyse<br />
S0 = { [S ′ → .S],<br />
[S → .L=R],<br />
[S → .R],<br />
[L → .∗R],<br />
[L → .id],<br />
[R → .L]}<br />
S1 = { [S ′ → S.]}<br />
S2 = { [S → L.=R],<br />
[R → L.]}<br />
S3 = { [S → R.]}<br />
S4 = { [L → ∗.R],<br />
[R → .L],<br />
[L → .∗R],<br />
[L → .id]}<br />
S5 = { [L → id.]}<br />
S6 = { [S → L=.R],<br />
[R → .L],<br />
[L → .∗R],<br />
[L → .id]}<br />
S7 = { [L → ∗R.]}<br />
S8 = { [R → L.]}<br />
S9 = { [S → L=R.]}<br />
Abbildung 3.20: Zustandsmenge des LR-DEA der Grammatik zur Beschreibung der<br />
C-Zuweisung aus Beispiel 43.<br />
Nach Anwendung des Verfahrens zur Berechnung des LR-DEAs auf Seite 92 erhalten<br />
wir die in Abbildung 3.20 angegebene Zustandsmenge (zur Verbesserung der Lesbarkeit<br />
lassen wir die Mengenklammern bei einelementigen Vorausschaumengen weg).<br />
Aufgr<strong>und</strong> der Differenzierung nach Vorausschaumengen werden die Zustände des LR-<br />
DEA in mehrere Zustände des LR(1)-Automaten aufgesplittet. Zum Beispiel wird der<br />
Zustand S4 aus Abbildung 3.20 in die Zustände I4 <strong>und</strong> I11 in Abbildung 3.21 aufgeteilt,<br />
deren Items dieselben Kerne, aber unterschiedliche Vorausschaumengen besitzen. Der<br />
LR(1)-Automat ist in Abbildung 3.22 dargestellt.<br />
Daher ergeben sich die folgenden Beziehungen zwischen LR(0)- <strong>und</strong> LR(1)-Items:<br />
S0 ∼ I0 S1 ∼ I1 S2 ∼ I2 S3 ∼ I3 S4 ∼ I4 <strong>und</strong> I11<br />
S5 ∼ I5 <strong>und</strong> I12 S6 ∼ I6 S7 ∼ I7 <strong>und</strong> I13 S8 ∼ I8 <strong>und</strong> I10 S9 ∼ I9<br />
Im LR-DEA in Abbildung 3.20 tritt das LR(0)-Item [R → .L] in den Zuständen S0<br />
<strong>und</strong> S4 auf. Durch das Shiften eines L gelangen wir aus beiden Zuständen in den<br />
Zustand S8. In Abbildung 3.21 unterscheiden sich die LR(1)-Items [R → .L, $] in<br />
I0 <strong>und</strong> [R → .L, {=, $}] in I4 durch ihre Vorausschaumengen. Aus I0 kann ein L-<br />
Übergang nach I10 erfolgen, aus I4 ein Übergang nach I8. Befinden wir uns also im<br />
Zustand I10, wissen wir eindeutig, daß wir aus I0 gekommen sind. Befinden wir uns<br />
im Zustand I8, sind wir aus I4 gekommen. Auf diese Weise enthalten die Zustände<br />
106
I0 = { [S ′ → .S, $],<br />
[S → .L=R, $],<br />
[S → .R, $],<br />
[L → .∗R, {=, $}],<br />
[L → .id, {=, $}],<br />
[R → .L,$]}<br />
I1 = { [S ′ → S., $]}<br />
I2 = { [S → L.=R, $],<br />
[R → L.,$]}<br />
I3 = { [S → R., $]}<br />
I4 = { [L → ∗.R, {=, $}],<br />
[R → .L, {=, $}],<br />
[L → .∗R, {=, $}],<br />
[L → .id, {=, $}]}<br />
I5 = { [L → id., {=, $}]}<br />
I6 = { [S → L=.R, $],<br />
[R → .L,$],<br />
[L → .∗R, $],<br />
[L → .id, $]}<br />
I7 = { [L → ∗R., {=, $}]}<br />
I8 = { [R → L., {=, $}]}<br />
I9 = { [S → L=R., $]}<br />
I10 = { [R → L.,$]}<br />
I11 = { [L → ∗.R, $],<br />
[R → .L,$],<br />
[L → .∗R, $],<br />
[L → .id, $]}<br />
I12 = { [L → id.,$]}<br />
I13 = { [L → ∗R., $]}<br />
3.4 Bottom-Up-Syntaxanalyse<br />
Abbildung 3.21: Zustandsmenge des charakteristischen endlichen Automaten mit<br />
LR(1)-Items zur Grammatik aus Beispiel 43.<br />
des LR(1)-Parsers Informationen über den Linkskontext, also die bisherige Ableitung.<br />
Dies ist im LR-DEA nicht möglich. Wenn wir uns im Zustand S8 befinden, können<br />
wir nicht unterscheiden, ob wir uns zuvor in S0 oder in S4 bef<strong>und</strong>en haben.<br />
Die durch die Einführung von Vorausschaumengen entstehende Aufsplittung von<br />
Zuständen ist bei der Analyse von realen Programmiersprachen ein wirkliches Problem,<br />
da die Zustandsmenge drastisch ansteigen kann (von h<strong>und</strong>erten zu tausenden<br />
von Zuständen).<br />
Aus diesem Gr<strong>und</strong> verwendet man oft Teilklassen der LR(k)-Sprachen <strong>und</strong> Zustandsmengen,<br />
die die gleiche Größe haben wie die von LR(0)-Grammatiken. Diese Teilklassen<br />
sind die SLR(k) (simple LR(k)) <strong>und</strong> die LALR(k)-Sprachen (lookahead LR(k)).<br />
SLR(1)-Parser:<br />
Wir gehen vom LR-DEA eines LR(0)-Parsers aus. Die Idee bei den SLR(1)-Parsern<br />
ist, als Vorausschaumenge eines Items die FOLLOW1-Menge des Nichtterminals zu<br />
verwenden, das in dem betreffenden Item auf der linken Seite des Kerns steht.<br />
Der Aufbau einer SLR(1)-Parse-Tabelle ist zu dem einer LR(1)-Tabelle identisch<br />
(siehe Abbildung 3.19). Wir modifizieren das Konstruktionsverfahren <strong>für</strong> die action-<br />
Tabelle auf Seite 104 wie im folgenden angegeben. Der Parse-Algorithmus auf Seite<br />
107
3 Syntaktische Analyse<br />
S<br />
I I<br />
0 1<br />
L =<br />
R<br />
I I I<br />
id<br />
R<br />
*<br />
I<br />
R<br />
I I<br />
id<br />
I<br />
3<br />
2 6 9<br />
4 7<br />
5<br />
*<br />
L<br />
I 8<br />
*<br />
L<br />
id<br />
I<br />
L<br />
I I<br />
11<br />
R<br />
id<br />
Abbildung 3.22: Charakteristischer endlicher LR(1)-Automat zur Grammatik aus<br />
Beispiel 43.<br />
108<br />
I<br />
10<br />
12<br />
*<br />
13
105 wird unverändert übernommen.<br />
3.4 Bottom-Up-Syntaxanalyse<br />
Konstruktion der SLR(1)-action-Tabelle.<br />
Eingabe: LR(0)-Zustandsmenge Q<br />
Ausgabe: action-Tabelle<br />
Verfahren:<br />
1. Für jeden Zustand q ∈ Q <strong>und</strong> <strong>für</strong> jedes [K] ∈ q:<br />
• falls K = S ′ → S.: trage accept in action[q, $] ein,<br />
• sonst<br />
– falls K = X → α., trage reduce X → α in action[q,a] <strong>für</strong> alle a ∈<br />
FOLLOW(X) ein,<br />
– falls K = X → α.aβ, trage shift in action[q,a] ein.<br />
2. Trage in jedes noch <strong>und</strong>efinierte Feld error ein.<br />
Da <strong>für</strong> jedes Nichtterminal die FOLLOW1-Menge eindeutig bestimmt ist, kommt<br />
es nicht zum Aufsplitten von Zuständen, so daß die Zustandsmenge eines SLR(1)-<br />
Parsers gegenüber der eines LR(0)-Parsers unverändert bleibt.<br />
Nachteilig ist, daß nicht <strong>für</strong> alle LR(1)-Sprachen ein SLR(1)-Parser existiert, da durch<br />
die “Vergröberung” der Vorausschaumengen der Items ein Informationsverlust entsteht,<br />
wie im folgenden Beispiel zu sehen ist.<br />
Beispiel 44 (Fortsetzung von Beispiel 43)<br />
In Abbildung 3.20 ist S2 der einzige ungeeignete Zustand. Wir berechnen die Zustandsmenge<br />
des charakteristischen endlichen Automaten mit SLR(1)-Items (Abbildung<br />
3.23). Es gilt FOLLOW1(S) = {$} <strong>und</strong> FOLLOW1(L) = FOLLOW1(R) =<br />
{=, $}. Bei der Erweiterung des vollständigen Items [R → L.] um die Vorausschaumenge<br />
{=, $} bleibt der Shift-Reduce-Konflikt aus S2 in S ′ 2 erhalten, da das zu lesende<br />
Symbol “=” in der Vorausschaumenge des vollständigen Items enthalten ist. Daher<br />
ist die Grammatik in Beispiel 43 keine SLR(1)-Grammatik.<br />
LALR(1)-Parser:<br />
Bei der Konstruktion eines LR(1)-Parsers kann es Zustände geben, die bzgl. der<br />
Kerne der Items übereinstimmen (z.B. die Zustände I4 <strong>und</strong> I11 in Abbildung 3.21).<br />
Um die Zustandsmenge zu verkleinern, kann man versuchen, diese Zustände zu einem<br />
Zustand zusammenzufassen, indem man die Vorausschaumengen der Items mit<br />
gleichem Kern vereinigt. Die Zustandszahl eines Automaten, bei dem Zustände mit<br />
gleichen Kernen zusammengefaßt wurden, entspricht der Anzahl des LR(0)-Parsers.<br />
Ein solcher Parser heißt LALR(1)-Parser.<br />
109
3 Syntaktische Analyse<br />
S ′ 0 = { [S′ → .S, {$}],<br />
[S → .L=R, {$}],<br />
[S → .R, {$}],<br />
[L → .∗R, {=, $}],<br />
[L → .id, {=, $}],<br />
[R → .L, {=, $}]}<br />
S ′ 1 = { [S′ → S., {$}]}<br />
S ′ 2<br />
S ′ 3<br />
S ′ 4<br />
= { [S → L.=R, {$}],<br />
[R → L., {=, $}]}<br />
= { [S → R., {$}]}<br />
= { [L → ∗.R, {=, $}],<br />
[R → .L, {=, $}],<br />
[L → .∗R, {=, $}],<br />
[L → .id, {=, $}]}<br />
S ′ 5<br />
S ′ 6<br />
S ′ 7<br />
S ′ 8<br />
S ′ 9<br />
= { [L → id., {=, $}]}<br />
= { [S → L=.R, {$}],<br />
[R → .L, {=, $}],<br />
[L → .∗R, {=, $}],<br />
[L → .id, {=, $}]}<br />
= { [L → ∗R., {=, $}]}<br />
= { [R → L., {=, $}]}<br />
= { [S → L=R., {$}]}<br />
Abbildung 3.23: Zustandsmenge des charakteristischen endlichen Automaten mit<br />
SLR(1)-Items <strong>für</strong> die Grammatik aus Beispiel 43.<br />
Bei der Vereinigung der Zustände ist zu beachten, daß durch diese Vereinigung neue<br />
Konflikte entstehen können. Daher ist diese Methode nicht auf alle LR(1)-Sprachen<br />
anwendbar, so daß nicht alle LR(1)-Sprachen durch LALR(1)-Parser akzeptiert werden<br />
können.<br />
Die Vorausschaumenge eines Items in einem Zustand kann auch direkt ohne die<br />
vorherige Konstruktion eines LR(1)-Parsers berechnet werden:<br />
LAL(q, [X → α.]) = {a ∈ VT | S ′ ⇒ βXaw <strong>und</strong> δ ∗ d(qd,βα) = q}<br />
Dabei ist δd die Übergangsfunktion des LR-DEA(G). In LAL(q, [X → α.]) sind also<br />
nur noch Terminalsymbole, die auf X in einer Rechtssatzform folgen können <strong>und</strong> die<br />
den charakteristischen endlichen Automaten LR-DEA(G) in den Zustand q bringen.<br />
Die Definition von LAL(q, [X → α.]) ist nicht konstruktiv, da in ihr i. allg. unendliche<br />
Mengen von Rechtssatzformen auftreten. Ein Verfahren zur effizienten der<br />
Vorausschaumengen <strong>für</strong> LALR(1)-Parser ist in [WM96] angegeben.<br />
Beispiel 45 (Fortsetzung von Beispiel 43)<br />
Wir erzeugen einen LALR(1)-Automaten, indem wir die Zustände in Abbildung 3.21,<br />
deren Items dieselben Kerne besitzen, zu einem Zustand zusammenfassen <strong>und</strong> die zugehörigen<br />
Vorausschaumengen vereinigen (Abbildung 3.25). In Abbildung 3.20 ist S2<br />
der einzige ungeeigenete Zustand. Die Vorausschaumenge <strong>für</strong> das vollständige Item<br />
110
S ′′<br />
0 = { [S′ → .S, {$}],<br />
[S → .L=R, {$}],<br />
[S → .R, {$}],<br />
[L → .∗R, {=, $}],<br />
[L → .id, {=, $}],<br />
[R → .L, {$}]}<br />
S ′′<br />
1 = { [S′ → S., {$}]}<br />
S ′′<br />
2<br />
S ′′<br />
3<br />
S ′′<br />
4<br />
= { [S → L.=R, {$}],<br />
[R → L., {$}]}<br />
= { [S → R., {$}]}<br />
= { [L → ∗.R, {=, $}],<br />
[R → .L, {=, $}],<br />
[L → .∗R, {=, $}],<br />
[L → .id, {=, $}]}<br />
S ′′<br />
5<br />
S ′′<br />
6<br />
S ′′<br />
7<br />
S ′′<br />
8<br />
S ′′<br />
9<br />
3.4 Bottom-Up-Syntaxanalyse<br />
= { [L → id., {=, $}]}<br />
= { [S → L=.R, {$}],<br />
[R → .L, {$}],<br />
[L → .∗R, {$}],<br />
[L → .id, {$}]}<br />
= { [L → ∗R., {=, $}]}<br />
= { [R → L., {=, $}]}<br />
= { [S → L=R., {$}]}<br />
Abbildung 3.24: Zustandsmenge des charakteristischen endlichen Automaten mit<br />
LALR(1)-Items <strong>für</strong> die Grammatik aus Beispiel 43.<br />
in S ′′<br />
2 ist {$}. Damit ist der Shift-Reduce-Konflikt aus S2 im Zustand S ′′<br />
2 beseitigt.<br />
Da es keine weiteren ungeeigneten Zustände gibt, ist die Grammatik in Beispiel 43<br />
eine LALR(1)-Grammatik. Die Zustandsmenge des charakteristischen endlichen Automaten<br />
mit LALR(1)-Items ist in Abbildung 3.24 angegeben.<br />
Definition 45<br />
Eine kontextfreie Grammatik, <strong>für</strong> die das SLR- (LALR-) Verfahren keine ungeeigneten<br />
Zustände ergibt, nennen wir eine SLR- (LALR-) Grammatik.<br />
Das LALR(1)-Parseverfahren wird insbesondere in Parser-Generatoren wie z.B. yacc<br />
[MB92] verwendet. Diese Programme erzeugen <strong>für</strong> eine Eingabedatei, die eine Sprachbeschreibung<br />
in Form einer kontextfreien Grammatik (<strong>und</strong> eventuell zusätzliche Informationen)<br />
enthält, einen Parser, der die von der Grammatik beschriebene Sprache<br />
erkennt. Dieser Parser liegt oft in Form von Quelltext vor, so daß er in Programme<br />
integriert werden kann (yacc erzeugt Parser in der Sprache C [KR90]). Das LALR-<br />
Verfahren eignet sich besonders <strong>für</strong> Parser-Generatoren, da die Zustandsmenge des<br />
zugehörigen Automaten effizient berechenbar ist.<br />
Für weitere Informationen zu SLR- <strong>und</strong> LALR-Parsern verweisen wir auf [WM96]<br />
<strong>und</strong> [ASU99].<br />
111
3 Syntaktische Analyse<br />
S<br />
I I<br />
0 1<br />
L =<br />
R<br />
I I I<br />
id<br />
R<br />
*<br />
I<br />
R<br />
I I<br />
id<br />
I<br />
3<br />
2 6 9<br />
4 7<br />
5<br />
*<br />
L<br />
I 8<br />
*<br />
L<br />
id<br />
I<br />
L<br />
I I<br />
11<br />
R<br />
id<br />
Abbildung 3.25: Charakteristischer endlicher LR(1)-Automat zur Grammatik aus<br />
Beispiel 43.<br />
112<br />
I<br />
10<br />
12<br />
*<br />
13
3.4.2 Fehlerbehandlung bei der Bottom-Up-Analyse<br />
3.4 Bottom-Up-Syntaxanalyse<br />
Ein LR(k)-Parser erkennt einen Fehler, sobald er in der action-Tabelle auf einen Fehlereintrag<br />
trifft (nicht beim Nachsehen in der goto-Tabelle). Bei einer LR(1)-Analyse<br />
führt der Parser zwischen Vorliegen <strong>und</strong> Erkennen eines Fehlers keine weiteren Reduktionen<br />
durch, während die SLR(1)- <strong>und</strong> LALR(1)-Parser eventuell noch Reduktionen<br />
vornehmen (aufgr<strong>und</strong> der weniger differenzierten Vorausschaumengen). Bei<br />
allen Verfahren werden aber zwischen dem Vorliegen eines Fehlers <strong>und</strong> dem Erkennen<br />
der Fehlersituation durch den Parser keine Shift-Schritte mehr ausgeführt.<br />
Wie bei der Fehlerbehandlung der Top-Down-Analyse stehen mehrere Verfahren zur<br />
Auswahl.<br />
Panic recovery: Das Verfahren der panic recovery überliest beim Auftreten eines<br />
Fehlers einen Teil der Eingabe bis zu einem synchronisierenden Symbol.<br />
Zuerst wird der Keller von oben her durchsucht, bis ein Zustand s gef<strong>und</strong>en wird,<br />
zu dem es <strong>für</strong> ein Nichtterminal A einen Eintrag in der goto-Tabelle gibt. Es wird<br />
geprüft, ob die Verwendung dieses Eintrags eine Aktion des Parsers ermöglichen wird.<br />
Ist dies der Fall, wird der Keller bis zu diesem Zustand gelöscht (s selbst wird nicht<br />
entfernt) <strong>und</strong> goto[s,A] auf den Keller gelegt. Dann wird die Eingabe bis zu einem<br />
Symbol aus FOLLOW(A) gelöscht. Auf diese Weise täuscht der Parser vor, daß der<br />
zu A gehörende Teil der Eingabe erfolgreich analysiert wurde <strong>und</strong> daher die Analyse<br />
fortgesetzt werden kann. Auf die weiteren Verfahren zur Fehlerbehandlung gehen wir<br />
an dieser Stelle nicht ein <strong>und</strong> verweisen auf [WM96] <strong>und</strong> [ASU99].<br />
Beispiel 46 (Fortsetzung von Beispiel 42)<br />
Gegeben sei die Grammatik G zur Erzeugung arithmetischer Ausdrücke.<br />
S → E (1)<br />
E → E + T (2)<br />
E → T (3)<br />
T → T ∗ F (3)<br />
T → F (3)<br />
F → (E) (3)<br />
F → id (7)<br />
Wir wollen untersuchen, welche Aktionen ein LR-Parser mit Panic-Recovery-Strategie<br />
<strong>für</strong> G bei der Eingabe des fehlerhaften Wortes ”id+(∗id))+id” durchführt.<br />
Die Zustände:<br />
113
3 Syntaktische Analyse<br />
I0 : S → .E ,{$}<br />
E → .E + T ,{$, +}<br />
E → .T ,{$, +}<br />
T → .T ∗ F .{$, *, +}<br />
T → .F .{$, *, +}<br />
F → .id .{$, *, +}<br />
F → .(E) .{$, *, +}<br />
I1 : S → E. ,{$}<br />
E → E. + T ,{$, +}<br />
I2 : F → id. .{$, *, +}<br />
I3 : T → F. .{$, *, +}<br />
I4 : F → (.E) .{$, *, +}<br />
E → .E + T ,{), +}<br />
E → .T ,{), +}<br />
T → .T ∗ F .{), *, +}<br />
T → .F .{), *, +}<br />
F → .id .{), *, +}<br />
F → .(E) .{), *, +}<br />
I5 : E → E + .T ,{$, +}<br />
T → .T ∗ F .{$, *, +}<br />
T → .F .{$, *, +}<br />
F → .id .{$, *, +}<br />
F → .(E) .{$, *, +}<br />
I6 : F → (E.) .{$, *, +}<br />
E → E. + T ,{), +}<br />
I7 : E → E + T. ,{$, +}<br />
T → T. ∗ F .{$, *, +}<br />
I8 : T → T ∗ .F .{$, *, +}<br />
F → .id .{$, *, +}<br />
F → .(E) .{$, *, +}<br />
I9 : T → T ∗ F. .{$, *, +}<br />
I10 : E → T. ,{$, +}<br />
T → T. ∗ F .{$, *, +}<br />
I11 : F → (E). .{$, *, +}<br />
114<br />
I ′ 2 : F → id. .{), *, +}<br />
I ′ 3 : T → F. .{), *, +}<br />
I ′ 4 : F → (.E) .{), *, +}<br />
E → .E + T ,{), +}<br />
E → .T ,{), +}<br />
T → .T ∗ F .{), *, +}<br />
T → .F .{), *, +}<br />
F → .id .{), *, +}<br />
F → .(E) .{), *, +}<br />
I ′ 5 : E → E + .T ,{), +}<br />
T → .T ∗ F .{), *, +}<br />
T → .F .{), *, +}<br />
F → .id .{), *, +}<br />
F → .(E) .{), *, +}<br />
I ′ 6 : F → (E.) .{), *, +}<br />
E → E. + T ,{), +}<br />
I ′ 7 : E → E + T. ,{), +}<br />
T → T. ∗ F .{), *, +}<br />
I ′ 8 : T → T ∗ .F .{), *, +}<br />
F → .id .{), *, +}<br />
F → .(E) .{), *, +}<br />
I ′ 9 : T → T ∗ F. .{), *, +}<br />
I ′ 10 : E → T. ,{), +}<br />
T → T. ∗ F .{), *, +}<br />
I ′ 11 : F → (E). .{), *, +}
3.4 Bottom-Up-Syntaxanalyse<br />
action goto<br />
Zustand + * id ( ) $ + * id ( ) S E T F<br />
I0 sync sync s s sync sync I2 I4 I1 I10 I3<br />
I1 s acc I5<br />
I2 r7 r7 r7<br />
I3 r5 r5 r5<br />
I4 sync sync s s sync sync I ′ 2 I ′ 4 I6 I ′ 10 I ′ 3<br />
I5 sync sync s s sync sync I2 I4 I7 I3<br />
I6 s s I5 I11<br />
I7 r2 s r2 I8<br />
I8 sync sync s s sync sync I2 I4 I9<br />
I9 r4 r4 r4<br />
I10 r3 s r3 I8<br />
I11 r6 r6 r6<br />
I ′ 2 r7 r7 r7<br />
I ′ 3 r5 r5 r5<br />
I ′ 4 sync sync s s sync sync I ′ 2 I ′ 4 I ′ 6 I ′ 10 I ′ 3<br />
I ′ 5 sync sync s s sync sync I ′ 2 I ′ 4 I ′ 7 I ′ 3<br />
I ′ 6 s s I ′ 5 I11 ′<br />
I ′ 7 r2 s r2 I ′ 8<br />
I ′ 8 sync sync s s sync sync I ′ 2 I ′ 4 I ′ 9<br />
I ′ 9 r4 r4 r4<br />
I ′ 10 r3 s r3 I ′ 8<br />
I ′ 11 r6 r6 r6<br />
FOLLOW(S)= {$}<br />
FOLLOW(E)= {$, +, )}<br />
FOLLOW(T)= {$, +, ), *}<br />
FOLLOW(F)= {$, +, ), *}<br />
rX reduce nach Regel X<br />
s shift<br />
sync synchronisierendes Symbol <strong>für</strong> F<br />
acc akzeptieren<br />
Fehler 1: Keine Aktion möglich, aber zu dem Zustand 4 gibt es einen Sprungeintrag<br />
goto(I4,F) = I ′ 3. Die Eingabe wird bis zu dem synchronisierenden Symbol gelöscht<br />
(hier wird nichts gelöscht, da ∗ ∈ FOLLOW(F)). Es wird I ′ 3 auf den Stack gelegt.<br />
Fehler 2: Zu I8 gibt es einen Sprungeintrag zu I9 mit F. Dieser erlaubt jedoch keine<br />
weitere Aktion (action(I9, ′ ) ′ ) = ǫ). Deshalb wird stattdessen ) überlesen.<br />
115
3 Syntaktische Analyse<br />
Fehler 3: Da es einen Sprungeintrag gibt, kann synchronisiert werden. Das synchronisierende<br />
Symbol ist ’+’.<br />
Eingabe Keller Aktion<br />
id+(*id))+id$ I0 ǫ<br />
+(*id))+id$ I2I0 F → id<br />
F+(*id))+id$ I0 ǫ<br />
+(*id))+id$ I3I0 T → F<br />
T+(*id))+id$ I0 ǫ<br />
+(*id))+id$ I10I0 E → T<br />
E+(*id))+id$ I0 ǫ<br />
+(*id))+id$ I1I0 ǫ<br />
(*id))+id$ I5I1I0 ǫ<br />
*id))+id$ I4I5I1I0 sync = Fehler 1<br />
*id))+id$ I ′ 3I4I5I1I0 T → F<br />
T*id))+id$ I4I5I1I0 ǫ<br />
*id))+id$ I ′ 10I4I5I1I0 ǫ<br />
id))+id$ I ′ 8I ′ 10I4I5I1I0 ǫ<br />
))+id$ I ′ 2I ′ 8I ′ 10I4I5I1I0 F → id<br />
F))+id$ I ′ 8I ′ 10I4I5I1I0 ǫ<br />
))+id$ I ′ 9I ′ 2I ′ 8I ′ 10I4I5I1I0 T → T ∗ F<br />
T))+id$ I4I5I1I0 ǫ<br />
))+id$ I ′ 10I4I5I1I0 E → T<br />
E))+id$ I4I5I1I0 ǫ<br />
))+id$ I6I4I5I1I0 ǫ<br />
)+id$ I8I6I4I5I1I0 sync = Fehler 2, Überlesen<br />
+id$ I8I6I4I5I1I0 sync = Fehler 3<br />
+id$ I9I6I4I5I1I0 T → T ∗ F<br />
T+id$ I5I1I0 ǫ<br />
+id$ I7I5I1I0 E → E + T<br />
E+id$ I0 ǫ<br />
+id$ I1I0 ǫ<br />
id$ I5I1I0 ǫ<br />
$ I2I5I1I0 F → id<br />
F$ I5I1I0 ǫ<br />
$ I3I5I1I0 T → F<br />
T$ I5I1I0 ǫ<br />
$ I7I5I1I0 E → E + T<br />
E$ I0 ǫ<br />
$ I1I0 acc<br />
In [ASU99], Band 1, S. 311–313, befindet sich eine ähnliche Grammatik, in der <strong>für</strong><br />
116
jede fehlerhafte Aktion eine Fehlerroutine aufgerufen wird.<br />
3.5 Parser Generatoren<br />
3.5 Parser Generatoren<br />
Parser Generatoren dienen der automatischen Generierung eines Parsers <strong>für</strong> eine<br />
entsprechende Sprachspezifikation. Die Spezifikation wird in Form einer Grammatik<br />
(BNF, EBNF, ...) in einer generatorspeizifischen Sprache hinterlegt. Als Ausgabe<br />
erzeugt der Generator ein Programm, das einen Parser <strong>für</strong> diese Sprache in einer<br />
vorher angegeben Programmiersprache implementiert. Der Umfang der möglichen<br />
Zielsprachen ist abhängig vom jeweiligen Generator. Häufig ist der Parser Generator<br />
auch Teil eines Compiler-Generators (Compiler-Compiler) wobei in verschiedenen<br />
Implementationen gelegentlich eine Trennung zwischen Scanner-Generator (Lexer)<br />
<strong>und</strong> Parser-Generator (intern oder extern) vorgenommen wird. Hinzu kommt meistens<br />
auch noch ein Tree Parser der zur Verarbeitung des konstruierten Syntaxbaums<br />
dient.<br />
Die Vorteile solcher <strong>Systeme</strong> sind die Nachweisbarkeit der Korrektheit des generierten<br />
Compilers <strong>und</strong> das Einsparen aufwendiger <strong>und</strong> fehleranfälliger Programmierarbeit.<br />
Einige Beispiele <strong>für</strong> Generatoren sind:<br />
• Lex/Yacc [JL92][LYP]<br />
• ANTLR [ANT]<br />
• JavaCC [JAV]<br />
• Coco/R [HM]<br />
• SableCC [SAB]<br />
117
4 Semantische Analyse<br />
In der abschließenden Analysephase des Compilers wird, nach der Überprüfung der<br />
lexikalischen <strong>und</strong> syntaktischen Struktur des zu übersetzenden Quelltextes, die statische<br />
Semantik des Programmes untersucht.<br />
Man bezeichnet eine (nicht kontextfreie) Eigenschaft eines Konstrukts einer Programmiersprache<br />
als eine statische semantische Eigenschaft, wenn<br />
1. <strong>für</strong> jedes Vorkommen dieses Konstrukts in einem Programm der “Wert” dieser<br />
Eigenschaft <strong>für</strong> alle (dynamischen) Ausführungen des Konstrukts gilt,<br />
2. <strong>für</strong> jedes Vorkommen des Konstrukts in einem konkreten Programm diese Eigenschaft<br />
berechnet werden kann.<br />
Beispiele <strong>für</strong> statische semantische Eigenschaften sind<br />
• Definiertheit, Sichtbarkeit <strong>und</strong> Gültigkeit: hierbei wird überprüft, ob die im Programm<br />
verwendeten Bezeichner (z.B. Variablennamen <strong>und</strong> Namen von Unterprogrammen)<br />
zuvor deklariert wurden. Wird ein Bezeichner mehrfach in einem<br />
Programm deklariert (z.B. in verschiedenen Unterprogrammen), muß bei einem<br />
Auftreten eines Bezeichners die aktuell gültige Deklaration zugeordnet werden<br />
können.<br />
Die Überprüfung dieser Eigenschaften ist mit dem Ansatz der kontextfreien<br />
Grammatiken nicht durchführbar. Zum Beispiel läßt sich die Deklaration eines<br />
Bezeichners w <strong>und</strong> dessen Verwendung im folgenden Programmtext auf die<br />
Sprache {wcw | w ∈ (a|b) ∗ } zurückführen, die mit kontextfreien Grammatiken<br />
nicht erzeugt werden kann (siehe [Sch08]).<br />
• Typisierung: Den Werten, Variablen <strong>und</strong> Unterprogrammen eines Programms<br />
werden in den meisten Programmiersprachen Typen zugeordnet. Typen sind<br />
Wertemengen wie z.B. boolesche Werte, ganze Zahlen, Fließkommazahlen <strong>und</strong><br />
Zeichen. Zur semantischen Analyse gehört die Überprüfung auf korrekte Typisierung<br />
eines Programms. In “stark getypten” Programmiersprachen ist der<br />
Typ jedes Ausdrucks zur Übersetzungszeit bestimmbar.<br />
Weiterhin umfaßt die Überprüfung der Semantik<br />
118<br />
• Überprüfungen auf Eindeutigkeit (z.B. darf innerhalb einer case-Anweisung<br />
nicht zweimal dasselbe Label auftreten),<br />
• auf Namen bezogene Überprüfungen (z.B. muß in Ada [Bar83] der Name eines<br />
Blockes am Anfang <strong>und</strong> am Ende des Blockes auftreten).
E<br />
E + E<br />
integer<br />
real<br />
real<br />
4.1 Attributierte Grammatiken<br />
Abbildung 4.1: Beispiel eines attributierten Strukturbaums.<br />
Wir werden bei der Beschreibung der semantischen Analyse vor allem auf die Typüberprüfungen<br />
eingehen.<br />
4.1 Attributierte Grammatiken<br />
Als theoretisches Hilfsmittel <strong>für</strong> die semantische Analyse verwenden wir attributierte<br />
Grammatiken. Dabei wird der von der Syntaxanalyse erzeugte Strukturbaum, der<br />
die syntaktische Struktur des zu analysierenden Programmtextes darstellt, durch Zusatzinformationen<br />
ergänzt, die die Überprüfung der statischen semantischen Eigenschaften<br />
ermöglichen. Wir erhalten einen sogenannten attributierten Strukturbaum.<br />
Beispiel 46<br />
In Abbildung 4.1 ist ein Ausschnitt aus einem Strukturbaum angegeben, der die Anwendung<br />
eines Additionsoperators auf zwei Teilausdrücke beschreibt. Den Nichtterminalen<br />
E sind Attribute zugeordnet, die den Typ des Ausdrucks, <strong>für</strong> den das jeweilige<br />
Nichtterminal steht, angeben. Das Attribut Typ ist <strong>für</strong> alle Auftreten des Nichtterminals<br />
E definiert, aber jedes Auftreten von E besitzt einen eigenen Attributwert.<br />
Der Typ des linken Teilbaums ist integer, der des rechten Teilbaums ist real. Die<br />
semantische Analyse kann aus diesen Informationen ermitteln, daß der Typ des Gesamtausdrucks<br />
real 1 sein muß <strong>und</strong> ordnet der Wurzel des Baums den Attributwert<br />
real zu.<br />
Den Grammatiksymbolen werden also zusätzliche Plätze (Attribute) zur Aufnahme<br />
von Informationen zugeordnet.<br />
Die Berechnung der Attributwerte im attributierten Strukturbaum wird dabei durch<br />
semantische Regeln definiert. Jeder Produktion der Grammatik werden semantische<br />
Regeln zugeordnet, die beschreiben, wie die Attributwerte der in der Produktion<br />
vorkommenden Symbole berechnet werden.<br />
Wir unterscheiden folgende Begriffe:<br />
1 Andernfalls würden bei der Addition eventuelle Nachkommastellen verloren gehen<br />
119
4 Semantische Analyse<br />
Abbildung 4.2: Synthetische <strong>und</strong> inherite Attribute.<br />
• Attribute sind Grammatiksymbolen zugeordnete Plätze,<br />
• Attributvorkommen entsprechen dem Vorkommen der zugehörigen Grammatiksymbole<br />
in Produktionen,<br />
• Attributexemplare sind die konkreten Auftreten von Attributen in einem Strukturbaum<br />
(ihnen werden die Attributwerte zugeordnet).<br />
Wir unterscheiden zwei Arten von Attributen:<br />
• synthetische Attribute: die Werte dieser Attribute werden im Strukturbaum<br />
bottom-up, also von den Blättern ausgehend zur Wurzel hin, berechnet (siehe<br />
den linken Baum in Abbildung 4.2),<br />
• inherite Attribute: die Werte dieser Attribute werden im Strukturbaum topdown,<br />
also von der Wurzel ausgehend zu den Blättern hin, berechnet, wobei<br />
hier jeweils auch Werte von Geschwisterknoten verwendet werden dürfen (siehe<br />
den rechten Baum in Abbildung 4.2).<br />
Durch die Kombination von synthetischen <strong>und</strong> inheriten Attributen ist ein Informationstransfer<br />
zwischen Teilbäumen möglich.<br />
Beispiel 47<br />
Wir nehmen an, daß ein Programm (Nichtterminal P) aus einem Deklarationsblock<br />
(Nichtterminal D) <strong>und</strong> einem Anweisungsteil (Nichtterminal S) besteht, die durch<br />
ein Semikolon voneinander getrennt sind (die Produktion lautet also P → D;S).<br />
Im Deklarationsteil werden die Typen der Bezeichner, die im Anweisungsteil verwendet<br />
werden, deklariert. Innerhalb des Strukturbaums, der den Deklarationsblock<br />
beschreibt, werden die Typen der einzelnen Bezeichner durch synthetische Attribute<br />
“gesammelt” (siehe Abbildung 4.3). Im Anweisungsteil werden diese Typinformationen<br />
benötigt, daher werden sie als inherite Attribute bei der Analyse des Anweisungsteils<br />
miteinbezogen.<br />
Wir erläutern nun zunächst die Konzepte der attributierten Grammatiken anhand<br />
eines Beispiels. Wir betrachten eine kontextfreie Grammatik, die rationale Zahlen<br />
in Form von Binärziffern definiert. Durch die Angabe semantischer Regeln wird die<br />
Berechnung des Werts einer rationalen Zahl ermöglicht.<br />
120
P<br />
D ; S<br />
4.1 Attributierte Grammatiken<br />
Abbildung 4.3: Berechnung von Typinformationen im Strukturbaum.<br />
Beispiel 48 (Binärzahlen)<br />
Wir betrachten die folgende kontextfreie Grammatik zur Beschreibung von rationalen<br />
Zahlen im Binärformat (N ist Startsymbol) [KV97]:<br />
N → L | L . L<br />
L → B | L B<br />
B → 0 | 1<br />
Wir geben nun semantische Regeln an, die eine Attributierung zur Berechnung des<br />
Zahlwerts der Binärzahlen erlauben.<br />
Jedes Nichtterminalsymbol erhält ein Attribut v zur Aufnahme von rationalen Zahlen:<br />
B.v, N.v, L.v. Weiterhin benötigen wir ein Hilfsattribut l zur Aufnahme der Anzahl<br />
der Ziffern nach dem Dezimalpunkt: L.l.<br />
Tritt ein Nichtterminal in einer Produktion mehrfach auf, müssen wir die zugehörigen<br />
Attributvorkommen unterscheiden. So ergibt sich N.v in der Produktion N → L.L aus<br />
L1.v <strong>und</strong> L2.v. Generell gilt, daß bei mehrfachem Vorkommen eines Nichtterminals<br />
das linkeste dieser Vorkommen den Index 1 erhält (die linke Seite der Produktion<br />
wird miteinbezogen).<br />
Wir beschreiben die Berechung des Zahlwertes mit folgenden den Produktionen der<br />
Grammatik zugeordneten semantischen Regeln:<br />
B → 0 B.v := 0<br />
B → 1 B.v := 1<br />
L → B L.v := B.v<br />
L.l := 1<br />
L → LB L1.v := 2 ∗ L2.v + B.v<br />
L1.l := L2.l + 1<br />
N → L N.v := L.v<br />
N → L.L N.v := L1.v + L2.v/2 L2.l<br />
In Abbildung 4.4 ist der attributierte Strukturbaum <strong>für</strong> die Ableitung des Wortes<br />
1101.01 angegeben. Als Zahlwert <strong>für</strong> die Binärzahl 1101.01 ergibt sich 13.25.<br />
Die Berechnung erfolgt bottom-up, da nur synthetische Attribute verwendet werden,<br />
121
4 Semantische Analyse<br />
d.h. die Werte der Attribute auf der linken Seite der jeweiligen Produktion hängen<br />
nur von den Werten der Attribute auf der rechten Seite der Produktion ab.<br />
Unter Verwendung eines inheriten Attributs läßt sich eine andere Variante angeben.<br />
Wir verwenden das zusätzliche Attribut s (“Stelle”), das die Stelligkeit einer<br />
Binärziffer angibt. Die semantischen Regeln lauten:<br />
B → 0 B.v := 0<br />
B → 1 B.v := 2 B.s<br />
L → B L.v := B.v<br />
L.l := 1<br />
B.s := L.s<br />
L → LB L1.v := L2.v + B.v<br />
L1.l := L2.l + 1<br />
L2.s := L1.s + 1<br />
B.s := L1.s<br />
N → L N.v := L.v<br />
L.s := 0<br />
N → L.L N.v := L1.v + L2.v<br />
L1.s := 0<br />
L2.s := −L2.l<br />
Die Ableitung des Wortes 1101.01 ist in Abbildung 4.5 dargestellt. Das Attribut s<br />
ist ein inherites Attribut, das zu Beginn der Analyse an der Wurzel des linken Teilbaums<br />
auf 0 gesetzt <strong>und</strong> dann im Baum an die jeweiligen Kinder der Knoten weitergereicht<br />
wird. Der Wert <strong>für</strong> s an der Wurzel des rechten Teilbaums ergibt sich aus<br />
dem synthetischen Attribut l, das die Länge der im Teilbaum dargestellten Ziffernfolge<br />
repräsentiert. Bei der Auswertung werden also sowohl synthetische (v,l) als auch<br />
inherite Attribute (s) verwendet.<br />
Werden die Werte 0 <strong>und</strong> 1 im Strukturbaum durch ein Terminal num repräsentiert,<br />
kann num ein synthetisches Attribut <strong>für</strong> die jeweiligen Werte erhalten. Statt<br />
berechnet zu werden, wird der Attributwert <strong>für</strong> num dann entsprechend durch einen<br />
”Scanner” 2 geliefert. So kann die Berechnung <strong>für</strong> verschiedene Eingaben durchgeführt<br />
werden.<br />
Wir geben nun die formale Definition einer attributierten Grammatik an.<br />
Definition 46<br />
Ein Tupel (G,A,R) ist eine attributierte Grammatik genau dann, wenn<br />
• G kontextfreie Grammatik,<br />
• A = �<br />
A(X) Menge der Attribute (A endlich) <strong>und</strong><br />
X∈VN ∪VT<br />
2 im Übersetzungsfall durch den Scanner des Compilers<br />
122
v:3<br />
l:2<br />
L<br />
B<br />
1<br />
L<br />
v:1<br />
l:1<br />
v:1<br />
v:6<br />
l:3<br />
v:13<br />
l:4<br />
L<br />
N<br />
L .<br />
L<br />
B 0<br />
v:1<br />
1<br />
B<br />
v:0<br />
1<br />
B v:1<br />
v:13.25<br />
4.1 Attributierte Grammatiken<br />
Abbildung 4.4: Attributierter Strukturbaum zur Analyse einer Binärzahl.<br />
L<br />
B<br />
0<br />
v:0<br />
l:1<br />
v:0<br />
v:1<br />
l:2<br />
B<br />
1<br />
v:1<br />
123
4 Semantische Analyse<br />
s:2<br />
v:12<br />
l:2<br />
L<br />
B<br />
1<br />
s:1<br />
v:12<br />
l:3<br />
L<br />
s:3<br />
v:8<br />
l:1<br />
s:3<br />
v:8<br />
s:0<br />
v:13<br />
l:4<br />
L<br />
s:2<br />
v:4<br />
B<br />
N<br />
L .<br />
L<br />
s:1<br />
v:0<br />
1<br />
s:0<br />
v:1<br />
B<br />
0<br />
B<br />
1<br />
v:13.25<br />
s:-1<br />
v:0<br />
l:1<br />
s:-1<br />
v:0<br />
L<br />
B<br />
0<br />
s:-2<br />
v:0.25<br />
l:2<br />
B<br />
1<br />
s:-2<br />
v:0.25<br />
Abbildung 4.5: Attributierter Strukturbaum zur Analyse einer Binärzahl mit inheritem<br />
Attribut.<br />
124
• R = �<br />
p∈P R(p) die Menge der semantischen Regeln der Form<br />
Xi.a := f(Xi1.b1,...,Xik.bk)<br />
4.1 Attributierte Grammatiken<br />
mit Xi,Xi1,...,Xik ∈ {X0,...,Xn}, bj ∈ A(Xij) <strong>für</strong> die Produktion X0 →<br />
X1...Xn ist <strong>und</strong> wenn<br />
• <strong>für</strong> jedes Auftreten eines Grammatiksymbols X in einem Strukturbaum <strong>für</strong> ein<br />
w ∈ L(G) <strong>und</strong> <strong>für</strong> jedes a ∈ A(X) höchstens eine Regel in R zur Berechnung<br />
von X.a anwendbar ist.<br />
Die vierte Bedingung fordert, daß eine attributierte Grammatik eindeutig ist. Daher<br />
darf bei der Auswahl der anzuwendenden semantischen Regel kein Nichtdeterminismus<br />
entstehen. Aus diesem Gr<strong>und</strong> kann ein Attribut nicht zugleich synthetisch <strong>und</strong><br />
inherit sein, da dann eine solche Auswahl zwischen zwei möglichen Regeln (einer <strong>für</strong><br />
die synthetische <strong>und</strong> einer <strong>für</strong> die inherite Berechnung des Attributwerts) getroffen<br />
werden müßte <strong>und</strong> somit die Eindeutigkeit verletzt wäre.<br />
Neben den semantischen Regeln werden zu Produktionen P manchmal semantische<br />
Bedingungen der Form B(Xi.a,...,Xj.b) angegeben. Dabei ist B ein Prädikat, das<br />
Aussagen über die Werte von Attributvorkommen Xi.a, ..., Xj.b in P macht. Semantische<br />
Bedingungen liefern Informationen über Korrektheit bzgl. der statischen<br />
Semantik des bis dahin überprüften Teilbaums.<br />
Definition 47<br />
Sei p Produktion. Die Menge der definierenden Vorkommen von Attributen <strong>für</strong> p ist<br />
AF(p) = {Xi.a | Xi.a := f(...) ∈ R(p)}.<br />
Ein Attribut X.a heißt synthetisch genau dann, wenn ∃p : X → α mit X.a ∈ AF(p).<br />
Ein Attribut X.a heißt inherit genau dann, wenn ∃p : Y → αXβ mit X.a ∈ AF(p).<br />
AS(X) sei die Menge der synthetischen Attribute von X, AI(X) die Menge der<br />
inheriten Attribute von X.<br />
Die Menge der definierenden Vorkommen einer Produktion p sind die Attributvorkommen,<br />
denen durch eine semantische Regel zu p ein Wert zugewiesen wird. Ein<br />
Attribut X.a heißt synthetisch, wenn definierende Vorkommen von X nur in Produktionen<br />
auftreten, bei denen X auf der linken Seite vorkommt. Entsprechend ist<br />
<strong>für</strong> ein inherites Attribut Y.b gefordert, daß alle definierenden Vorkommen nur bei<br />
Produktionen auftreten, bei denen Y auf der rechten Seite vorkommt.<br />
Wir beschreiben nun informell die Semantik einer attributierten Grammatik.<br />
Semantik einer attributierten Grammatik<br />
Unter bestimmten Bedingungen wird <strong>für</strong> jeden Strukturbaum t der zugr<strong>und</strong>eliegenden<br />
kontextfreien Grammatik eine Zuordnung zu den Knoten von t festgelegt.<br />
125
4 Semantische Analyse<br />
Z.c<br />
Y.b<br />
Abbildung 4.6: Darstellung der direkten Abhängigkeiten zwischen<br />
Attributvorkommen.<br />
Sei t ein Strukturbaum einer attributierten Grammatik, sei n ein Knoten in t <strong>und</strong><br />
sei n mit X beschriftet.<br />
Für jedes Attribut a ∈ A(X) liegt an n ein Attributexemplar an vor. Dieses entspricht<br />
einem Attributvorkommen zu der an dieser Stelle im Strukturbaum angewandten<br />
Produktion (zur Erzeugung von n <strong>und</strong> seinen eventuellen Nachfolgern). Nach Definition<br />
46 gibt es dann höchstens eine dieser Produktion zugeordnete semantische Regel<br />
zur Berechnung von an.<br />
Da einer Produktion der Grammatik mehrere semantische Regeln zugeordnet sein<br />
können, zwischen denen Abhängigkeiten bestehen können, muß zur Berechnung der<br />
Attributwerte eine geeignete Strategie entwickelt werden.<br />
In einer Produktion einer Grammatik mit der semantischen Regel X.a := f(Z.c,Y.b)<br />
müssen Z.c <strong>und</strong> Y.b bekannt sein, wenn X.a berechnet wird. Diese Abhängigkeiten<br />
zwischen Attributvorkommen lassen sich mit gerichteten Graphen wie in Abbildung<br />
4.6 beschreiben.<br />
Definition 48<br />
Sei p : X0 → X1...Xn Produktion. Wir ordnen p den Graphen der direkten Abhängigkeiten<br />
DA(p) := {(Xi.a,Xj.b) | Xj.b := f(...,Xi.a,...) ∈ R(p)} zu.<br />
Eine Kante (Xi.a,Xj.b) ∈ DA(p) besagt, daß bei Anwendung der Produktion p der<br />
Wert von Xi.a bekannt sein muß, damit Xj.b berechnet werden kann.<br />
Die indirekten Abhängigkeiten zwischen Attributvorkommen erhält man durch Bildung<br />
der transitiven Hülle des Abhängigkeitsgraphen.<br />
Beispiel 49 (Fortführung von Beispiel 48)<br />
In Abbildung 4.7 ist der Graph der direkten Abhängigkeiten <strong>für</strong> die Produktion L →<br />
LB mit der zweiten Variante semantischer Regeln aus Beispiel 48 angegeben.<br />
Die Abhängigkeitsgraphen der einzelnen Produktionen können auch miteinander<br />
“verklebt” werden, so daß ein gemeinsamer Abhängigkeitsgraph <strong>für</strong> alle Attributvorkommen<br />
eines Ableitungsbaums entsteht. In Abbildung 4.8 ist dieser “verklebte”<br />
Graph <strong>für</strong> die Ableitung aus Beispiel 48 angegeben.<br />
126<br />
X.a
L2.v<br />
B.v<br />
L2.l<br />
L1.s<br />
❍ ❍❍❍<br />
✦ ✦✦✦✦<br />
✟ ✟✟✟<br />
❍ ❍❍❍<br />
L1.v<br />
L1.l<br />
L2.s<br />
B.s<br />
Abbildung 4.7: Beispiel eines Abhängigkeitsgraphen.<br />
v s l<br />
L<br />
v s<br />
v s l<br />
L<br />
v s<br />
L<br />
B<br />
1<br />
N<br />
L . v s l L<br />
B<br />
v s l B v s 0<br />
v s<br />
v s l<br />
1<br />
B<br />
1<br />
4.1 Attributierte Grammatiken<br />
v s l L B v s<br />
v s<br />
Abbildung 4.8: “Verklebter” Abhängigkeitsgraph.<br />
v<br />
B<br />
0<br />
1<br />
127
4 Semantische Analyse<br />
Ein Abhängigkeitsgraph <strong>für</strong> einen Ableitungsbaum darf keinen Zyklus enthalten, da<br />
die Attribute dann nicht auswertbar sind. Daher ist der Graph auf eventuelle Zirkularität<br />
zu überprüfen.<br />
Außerdem müssen zur Berechnung der Attribute “genügend” Regeln existieren.<br />
Definition 49<br />
Eine attributierte Grammatik ist vollständig genau dann, wenn <strong>für</strong> alle Produktionen<br />
p gilt:<br />
Wenn p : X → α, dann AS(X) ⊆ AF(p), wenn p : Y → αXβ, dann AI(X) ⊆<br />
AF(p). Weiterhin muß gelten AS(X) ∪ AI(X) = A(X).<br />
Jede vollständige <strong>und</strong> nicht-zirkuläre attributierte Grammatik G ist <strong>für</strong> jeden Ableitungsbaum<br />
von G auswertbar.<br />
Eine Möglichkeit der Auswertung der Attributvorkommen ist, <strong>für</strong> jedes Attributvorkommen<br />
einen eigenen Prozeß zu generieren. Alle Prozesse laufen parallel. Ein<br />
einzelner Prozeß berechnet den Wert des zugehörigen Attributvorkommens, sobald<br />
alle da<strong>für</strong> notwendigen Daten verfügbar sind. Dieses Auswertungsverfahren ist im<br />
allgemeinen zu aufwendig (in bezug auf Zeit <strong>und</strong> Speicherplatz). Effizientere Auswertungsstrategien<br />
werden in [ASU99] beschrieben.<br />
Die Auswertung ist <strong>für</strong> spezielle Klassen attributierter Grammatiken einfacher. Treten<br />
z.B. nur synthetische Attribute auf, kann ein Bottom-Up-Parser ohne Probleme<br />
die Attribute mit berechnen. Aus diesem Gr<strong>und</strong> sollte die gewählte Auswertungsstrategie<br />
möglichst im Einklang mit der Parsemethode gewählt werden.<br />
Beim Übersetzerbau können die semantischen Regeln Funktionen mit Seiteneffekten<br />
beinhalten (teilweise ohne tatsächliche Berechnung von Attributwerten). So ist<br />
es im allgemeinen notwendig, während der semantischen Analyse Typinformationen<br />
in die Symboltabelle einzufügen. Dabei werden z.B. den Bezeichnern, die der Scanner<br />
(bzw. der Sieber) in der lexikalischen Analyse in die Symboltabelle eingetragen<br />
hat, Typinformationen zugeordnet. Treten die Bezeichner im Anweisungsteil eines<br />
Programms auf, werden diese Typinformationen aus der Tabelle gelesen <strong>und</strong> <strong>für</strong> die<br />
Typüberprüfung verwendet.<br />
4.2 Typüberprüfung<br />
Die Typüberprüfung verifiziert, daß der Typ eines Konstrukts mit dem Typ “zusammenpaßt”,<br />
der aufgr<strong>und</strong> des Kontextes erwartet wird.<br />
Beispiele:<br />
128<br />
• die Operation mod erwartet integer-Operanden,<br />
• Dereferenzierung kann nur auf Zeiger angewendet werden,
• Indizieren ist nur bei Arrays erlaubt.<br />
4.2 Typüberprüfung<br />
Ein wichtiges Teilproblem bei der Typüberprüfung ist die Frage, wann Typen “zusammenpassen”.<br />
Typen sollen nicht nur auf Gleichheit, sondern auch auf Möglichkeiten<br />
zur Typkonversion hin überprüft werden. Zum Beispiel ist in vielen Sprachen eine<br />
Typkonversion von integer nach real möglich. So ist zum Beispiel <strong>für</strong> die Konstante<br />
1 im Ausdruck 1 + 1.5 eine Umwandlung des Typs integer nach real notwendig.<br />
Typinformationen werden auch bei der Codegenerierung benötigt. Zum Beispiel ist in<br />
den meisten Sprachen die Überladung des Operators + möglich, d.h. es wird dasselbe<br />
Operatorsymbol <strong>für</strong> die Addition von integer-Zahlen <strong>und</strong> <strong>für</strong> die Addition von real-<br />
Zahlen verwendet. Der Codegenerator muß anhand der Typinformation entscheiden,<br />
<strong>für</strong> welche Art von Addition er Code erzeugen soll.<br />
Einige Sprachen (vor allem aus der Familie der funktionalen Sprachen) erlauben<br />
Polymorphie [Thi97]. Eine Funktion ist polymorph, wenn sie auf Argumente unterschiedlichen<br />
Typs angewendet werden kann (die oben erwähnte +-Operation könnte<br />
man also auch als eine polymorphe Funktion auffassen). So ist zum Beispiel <strong>für</strong> eine<br />
Funktion zur Berechnung der Länge einer Liste der Typ der Listenelemente irrelevant.<br />
Andererseits möchte man nicht <strong>für</strong> jeden Listentyp (Zeichenliste, Zahlenliste<br />
usw.) eine eigene Funktion zur Längenberechnung definieren. Die Definition einer<br />
polymorphen Funktion ist daher <strong>für</strong> diese Aufgabe prädestiniert.<br />
4.2.1 Typsysteme<br />
Typsysteme enthalten Informationen über Typen in Programmiersprachen. Typinformationen<br />
in Programmiersprachen sind z.B.<br />
• in Pascal: “Wenn beide Operanden der arithmetischen Operatoren Addition,<br />
Subtraktion <strong>und</strong> Multiplikation vom Typ integer sind, dann ist das Ergebnis<br />
ebenfalls vom Typ integer.”,<br />
• in C: “Das Ergebnis des einstelligen Operators & ist ein Zeiger auf das Objekt,<br />
das durch den Operanden übergeben wurde. Wenn der Typ des Operanden t<br />
ist, dann ist ’Zeiger auf t’ der Typ des Ergebnisses.”.<br />
Das erste Beispiel zeigt, daß Ausdrücken ein Typ zugeordnet wird. Das zweite Beispiel<br />
zeigt, daß Typen eine Struktur haben, das heißt, daß man neue Typen aus gegebenen<br />
Typen konstruieren kann.<br />
Ein Typsystem umfaßt<br />
• Einfache Typen wie integer, boolean, character <strong>und</strong> real. Hierzu gehören auch<br />
Teilbereichstypen (z.B. (1..10)) <strong>und</strong> Aufzählungstypen<br />
(z.B. (rot,gelb,grün)).<br />
• Zusammengesetzte Typen wie Arrays, Records, Sets, Zeigertypen <strong>und</strong> Funktionstypen.<br />
129
4 Semantische Analyse<br />
Der Typ von Sprachkonstrukten wird durch Typausdrücke angegeben.<br />
Definition 50<br />
Ein Typausdruck ist<br />
• ein einfacher Typ<br />
oder<br />
• von der Form k(T1,...,Tn), wobei k ein Typkonstruktor <strong>und</strong><br />
T1,...,Tn Typausdrücke sind.<br />
Die Menge der einfachen Typen <strong>und</strong> der Typkonstruktoren hängt von der betrachteten<br />
Sprache ab. Zusammengesetzte Typen werden mit Hilfe der Typkonstruktoren<br />
gebildet.<br />
Beispiel 50<br />
Wir betrachten Beispiele <strong>für</strong> einfache Typen <strong>und</strong> Typkonstruktoren:<br />
130<br />
• Beispiele <strong>für</strong> einfache Typen: boolean, char, integer, real, type error (zeigt Typfehler<br />
an)<br />
• Beispiele <strong>für</strong> Typkonstruktoren:<br />
– Arrays: Sei T Typausdruck. array(I,T) ist ein Typausdruck, der den Typ<br />
von Arrays mit Elementen vom Typ T <strong>und</strong> Indexmenge I bezeichnet (eigentlich<br />
müßte <strong>für</strong> jede Indexmenge ein eigener Typkonstruktor arrayI(T)<br />
verwendet werden). Zum Beispiel wird nach Bearbeitung von “var A: array<br />
[1..10] of integer” mit A der Typausdruck array(1..10,integer) assoziiert.<br />
– Records: Seien T1,...,Tm Typen, n1,...,nm Komponentennamen. Dann ist<br />
record((n1×T1)×...×(nm×Tm)) Typausdruck (eigentlich sind die n1,...,nm<br />
wieder Teil des Typkonstruktors). Zum Beispiel wird <strong>für</strong> den Programmtext<br />
type row = record<br />
address : integer;<br />
lexeme : array [1..15] of char<br />
end;<br />
var table : array [1..101] of row<br />
der Typ<br />
T1 = record((address × integer) × (lexeme × array(1..15,char)),<br />
der dem Typ von row entspricht, <strong>und</strong> der Typ T2 = array(1..101,T1) von<br />
table erzeugt.
4.2 Typüberprüfung<br />
– Zeigertypen: Sei T Typausdruck. Dann ist pointer(T) Typausdruck. Wird<br />
im Beispiel <strong>für</strong> Record-Typen die Zeile “var p: ↑row” hinzugefügt, ist der<br />
Typ von p pointer(T1).<br />
Ein Typsystem ist nun eine Sammlung von Regeln zur Zuweisung von Typausdrücken<br />
zu Teilen eines Programms.<br />
Ein Typüberprüfer ist die Implementierung eines Typsystems in Form eines Programms.<br />
Dieses Programm ist in der Regel ein Teilprogramm der semantischen Analyse.<br />
Typsysteme sind manchmal nicht nur sprach-, sondern auch compilerspezifisch. Zum<br />
Beispiel schließt in Pascal der Typ eines Arrays die Indexmenge mit ein. Einige<br />
Compiler erlauben jedoch, Prozeduren zu deklarieren, die als Parameter Arrays ohne<br />
Betrachtung der konkreten Indexmenge erhalten 3 . Diese Compiler verwenden daher<br />
ihr eigenes Typsystem.<br />
Statische <strong>und</strong> dynamische Überprüfung von Typen<br />
Wir unterscheiden zwischen der statischen <strong>und</strong> der dynamischen Typüberprüfung.<br />
Die statische Typüberprüfung umfaßt alle Typuntersuchungen, die während der Übersetzungszeit<br />
eines Programms durch den Compiler in der semantischen Analyse vorgenommen<br />
werden können. Hierzu gehört z.B. die korrekte Typisierung von Ausdrücken.<br />
Die dynamische Typüberprüfung findet erst zur Laufzeit eines übersetzten<br />
Programms statt, da sie die Typinformationen analysiert, die von der Eingabe des<br />
jeweiligen Programmablaufs abhängig sind. Hierzu gehört z.B. die Einhaltung der<br />
Indexmenge bei Arrayzugriffen.<br />
Eine Programmiersprache heißt streng getypt, wenn eine vollständige statische Typüberprüfung<br />
eines Quelltextes zur Übersetzungszeit möglich ist (ohne Überprüfung der<br />
korrekten Indizierung bei Arrays). Streng getypte Sprachen sind z.B. Pascal [JW91],<br />
Modula-2 [Wir97], C [KR90] <strong>und</strong> Haskell [Po96]. Nicht streng getypt ist z.B. Smalltalk,<br />
da durch das late binding Typinformationen erst zur Laufzeit entstehen [GR83]<br />
<strong>und</strong> daher auch erst zur Laufzeit überprüft werden kann, ob eine Methode (oder ein<br />
Operator) <strong>für</strong> ein Objekt definiert ist.<br />
Beispiel 51 (Typüberprüfer <strong>für</strong> eine einfache Sprache)<br />
Wir definieren eine einfache Sprache, in der ein Programm aus einer Folge von Deklarationen<br />
besteht, auf die ein einzelner Ausdruck folgt. Die kontextfreie Grammatik<br />
dieser Sprache lautet:<br />
3 In Modula-2 als open arrays generell erlaubt.<br />
131
4 Semantische Analyse<br />
P → D ; E<br />
D → D ; D | id : T<br />
T → char | integer | array [ num ] of T | ↑ T<br />
E → literal | num | id | E mod E | E [ E ] | E ↑<br />
Als Typen verwenden wir einfache Typen (char, integer <strong>und</strong> type error <strong>für</strong> Typfehler)<br />
<strong>und</strong> Arrays <strong>und</strong> Zeiger als zusammengesetzte Typen. Alle Arrays sind mit Werten<br />
vom Typ integer indiziert; die Indexmengen beginnen stets mit 1. So liefert z.B. der<br />
Deklarationsteil array [256] of char den Typausdruck array(1..256,char). Zeigertypen<br />
werden mit dem Typkonstruktor pointer bezeichnet, so daß z.B. ↑integer den<br />
Typ pointer(integer) erhält. Die Dereferenzierung geschieht in Ausdrücken mit e ↑,<br />
wobei e einem Zeigertyp angehören muß.<br />
Wir geben nun das Typsystem <strong>für</strong> unsere Sprache in Form von semantischen Regeln<br />
an, die den Produktionen der Grammatik zugeordnet sind.<br />
Für die semantischen Regeln verwenden wir zwei Funktionen mit Seiteneffekten:<br />
• addtype(b,t) ordnet dem Bezeichner b in der Symboltabelle den Typ t zu. Wir<br />
nehmen an, daß die Bezeichner während der lexikalischen Analyse in die Symboltabelle<br />
eingefügt wurden. Jedes Symbol id besitzt ein synthetisches Attribut<br />
entry, das auf den entsprechenden Symboltabelleneintrag verweist.<br />
• lookup(b) liefert den <strong>für</strong> Bezeichner b in der Symboltabelle angegebenen Typ.<br />
Existiert in der Symboltabelle kein Eintrag <strong>für</strong> b, liefert lookup type error.<br />
Das Symbol num besitzt ein synthetisches Attribut val, das den Wert der von num<br />
repräsentierten Zahl enthält.<br />
Die semantischen Regeln des Typsystems sind in Abbildung 4.9 angegeben.<br />
In der Regel <strong>für</strong> Array-Zugriff wird nicht überprüft, ob der Index gültig ist, da dies<br />
erst von der dynamischen Typüberprüfung zur Laufzeit verifiziert werden kann.<br />
In Abbildung 4.10 ist der attributierte Strukturbaum <strong>für</strong> das Programm<br />
n : ↑ integer;<br />
n↑ mod 4<br />
angegeben. Dieses Programm enthält keinen Typfehler.<br />
Das Programm<br />
n : ↑ integer;<br />
n↑ mod x<br />
ist syntaktisch korrekt, enthält aber einen semantischen Fehler, da der Bezeichner x<br />
nicht deklariert ist. Das Typsystem ist in der Lage, diesen Fehler zu finden (siehe<br />
Abbildung 4.11). Beim Nachsehen in der Symboltabelle liefert die Funktion lookup<br />
den Typ type error, der im Baum weitergereicht wird. Auf diese Weise wird erkannt,<br />
daß der Ausdruck einen semantischen Fehler enthält.<br />
132
4.2 Typüberprüfung<br />
P → D ; E<br />
D → D ; D<br />
D → id : T addtype(id.entry,T.type)<br />
T → char T.type := char<br />
T → integer T.type := integer<br />
T → ↑T T1.type := pointer(T2.type)<br />
T → array [ num ] of T T1.type := array(1..num.val,T2.type)<br />
E → literal E.type := char<br />
E → num E.type := integer<br />
E → id E.type := lookup(id.entry)<br />
E → E mod E E1.type :=<br />
if E2.type = integer and E3.type = integer<br />
then integer<br />
else type error<br />
E → E [ E ] E1.type :=<br />
if E3.type = integer and E2.type = array(s,t)<br />
then t<br />
else type error<br />
E → E ↑ E1.type :=<br />
if E2.type =pointer(t) then t else type error<br />
Abbildung 4.9: Semantische Regeln <strong>für</strong> Beispielsprache.<br />
addtype(n,pointer(integer))<br />
D<br />
type:integer<br />
id : T<br />
E<br />
type:pointer(integer)<br />
entry:n<br />
P<br />
T<br />
type:integer<br />
integer<br />
; E<br />
mod<br />
E type:pointer(integer)<br />
id<br />
entry:n<br />
type:integer<br />
E<br />
num<br />
val:4<br />
type:integer<br />
Abbildung 4.10: Attributierter Strukturbaum <strong>für</strong> ein Beispielprogramm.<br />
133
4 Semantische Analyse<br />
addtype(n,pointer(integer))<br />
D<br />
type:integer<br />
id : T<br />
E<br />
type:pointer(integer)<br />
entry:n<br />
P<br />
T<br />
type:integer<br />
integer<br />
; E<br />
mod<br />
E type:pointer(integer) id<br />
entry:?<br />
id<br />
entry:n<br />
type:type_error<br />
E<br />
type:type_error<br />
Abbildung 4.11: Attributierter Strukturbaum <strong>für</strong> ein fehlerhaftes Beispielprogramm.<br />
Beispiel 52 (Fortführung von Beispiel 51)<br />
Wir erweitern die Sprache aus dem vorherigen Beispiel um Anweisungen. Anweisungen<br />
haben keinen Typ, daher führen wir einen neuen einfachen Typ void ein, der den<br />
Typ einer korrekt getypten Anweisung darstellt.<br />
Wir modifizieren die Grammatik aus Beispiel 51:<br />
P → D ; S<br />
D → D ; D | id : T<br />
T → boolean | char | integer | array [ num ] of T | ↑ T<br />
E → literal | num | id | E mod E | E [ E ] | E ↑<br />
S → id := E | if E then S | while E do S | S ; S<br />
Das Typsystem wird um die folgenden semantischen Regeln ergänzt.<br />
134
1 function sequiv (S ,T) : boolean ;<br />
2 begin<br />
3 if S <strong>und</strong> T vom selben Basistyp then<br />
4 return true<br />
5 else if } S = array( I , S1) and T = array( I ,T1) then<br />
6 return sequiv (S1 ,T1)<br />
7 else if S = pointer (S1) and T = pointer (T1) then<br />
8 return sequiv (S1 ,T1)<br />
9 else return false<br />
10 end<br />
4.2 Typüberprüfung<br />
Abbildung 4.12: Funktion zur Überprüfung, ob zwei Typausdrücke identisch sind.<br />
T → boolean T.type := boolean<br />
S → id := E S.type :=<br />
if lookup(id.entry) = E.type<br />
then void<br />
else type error<br />
S → if E then S S1.type :=<br />
if E.type = boolean<br />
then S2.type<br />
else type error<br />
S → while E do S S1.type :=<br />
if E.type = boolean<br />
then S2.type<br />
else type error<br />
S → S ; S S1.type :=<br />
if S2.type = void and S3.type = void<br />
then void<br />
else type error<br />
4.2.2 Gleichheit von Typausdrücken<br />
Im Beispiel 52 wurde in der semantischen Regel <strong>für</strong> die Wertzuweisung die Gleichheit<br />
von Typausdrücken gefordert (if lookup(id.entry) = E.type ...).<br />
Wir gehen im folgenden auf die Frage ein, wann zwei Typen gleich sind.<br />
In einem Typsystem ohne die Deklaration neuer Typnamen sind zwei Typen gleich,<br />
wenn ihre Typausdrücke identisch sind. Zur Überprüfung, ob zwei Typausdrücke<br />
identisch sind, läßt sich die rekursive Funktion sequiv in Abbildung 4.12 verwenden.<br />
135
4 Semantische Analyse<br />
Beispiel 53<br />
Ein Problem entsteht, wenn die Vergabe von Typnamen <strong>für</strong> neue Typen möglich ist.<br />
Betrachten wir die folgende Typdeklaration:<br />
type link = ↑ cell;<br />
var next : link;<br />
last : link;<br />
p : ↑ cell;<br />
q,r : ↑ cell;<br />
Haben next,last,p,q <strong>und</strong> r den gleichen Typ?<br />
Werden Typnamen verwendet, unterscheiden wir zwei Arten von Typgleichheit: Namensgleichheit<br />
<strong>und</strong> strukturelle Gleichheit.<br />
• Namensgleichheit: jeder Typname bezeichnet einen individuellen Typ. Daher<br />
sind die Typnamen Bestandteile der Typausdrücke.<br />
• Strukturelle Gleichheit: die Typnamen werden in Typausdrücken ersetzt, bevor<br />
die Funktion sequiv aus Abbildung 4.11 auf die Typausdrücke angewendet wird<br />
(Vorsicht bei rekursiven Typdefinitionen!).<br />
Beispiel 54 (Fortsetzung von Beispiel 53)<br />
Den in Beispiel 53 deklarierten Variablen werden die folgenden Typausdrücke zugeordnet:<br />
next : link<br />
last : link<br />
p : pointer(cell)<br />
q : pointer(cell)<br />
r : pointer(cell)<br />
Bei Namensgleichheit besitzen next <strong>und</strong> last den gleichen Typ, ebenso p, q <strong>und</strong> r. p<br />
<strong>und</strong> next hingegen sind nicht vom gleichen Typ.<br />
Bei struktureller Gleichheit werden die Typnamen durch die ihnen zugeordneten Typausdrücke<br />
ersetzt. Da link der Ausdruck pointer(cell) zugeordnet ist, besitzen alle<br />
fünf Variablen den gleichen Typ.<br />
In der Praxis ist das Problem der Typgleichheit manchmal noch komplizierter. Zum<br />
Beispiel wird in Implementierungen der Sprache Pascal <strong>für</strong> jede Deklaration ein<br />
impliziter Typname eingeführt. Daher würden die Typen der Variablen aus Beispiel<br />
53 lauten:<br />
next : link<br />
last : link<br />
p : np<br />
q : nqr<br />
r : nqr<br />
136
4.2 Typüberprüfung<br />
Die Variable p erhält den neuen impliziten Typnamen np. Die Variablen q <strong>und</strong> r<br />
erhalten den neuen impliziten Typnamen nqr, da sie zusammen in einer Deklaration<br />
vereinbart wurden. Dies führt dazu, daß p <strong>und</strong> q nicht mehr vom gleichen Typ sind,<br />
q <strong>und</strong> r jedoch schon.<br />
4.2.3 Typumwandlungen<br />
In einigen Fällen ist die Gleichheit von Typen eine zu starke Forderung, wie im<br />
folgenden Beispiel erläutert wird.<br />
Beispiel 55<br />
Im Programm<br />
var x,y : real;<br />
i : integer;<br />
y := x + i<br />
wird eine Addition einer Variable x vom Typ real mit einer Variable i vom Typ<br />
integer durchgeführt, deren Ergebnis wieder in einer real-Variable gespeichert wird.<br />
In einigen Programmiersprachen (z.B. in Oberon [WG92]) ist diese Addition erlaubt,<br />
obwohl die Typen von x <strong>und</strong> i unterschiedlich sind. In diesen Sprachen sind die<br />
Typen real <strong>und</strong> integer “verträglich”, d.h. der Wert der integer-Variable wird vor<br />
der Durchführung der Addition in einen real-Wert umgewandelt.<br />
Wir definieren die “Verträglichkeit” von Typen durch eine Relation coercible (engl.<br />
coercible = erzwingend).<br />
Definition 51<br />
Seien T1,T2 Typen. Die Beziehung coercible(T1,T2) legt fest, daß ein Objekt vom Typ<br />
T1 in ein Objekt vom Typ T2 transformiert werden kann, falls dies nötig ist.<br />
Die Notwendigkeit einer Transformation eines Objekts in ein Objekt eines “verträglichen”<br />
Typs wird vom Compiler in der semantischen Analyse erkannt, so daß er <strong>für</strong><br />
diese Umwandlung Code erzeugen kann.<br />
Beispiel 56 (Typüberprüfung <strong>für</strong> Ausdrücke)<br />
In diesem Beispiel betrachten wir ein Typsystem <strong>für</strong> Ausdrücke, das Typumwandlungen<br />
erlaubt. Jeder Ausdruck besitzt zwei Attribute <strong>für</strong> die Aufnahme von Typinfomationen:<br />
• Das synthetische Attribut premode enthält den Typ eines Ausdrucks gemäß<br />
seiner Zusammensetzung.<br />
• Das inherite Attribut postmode enthält den Typ eines Ausdrucks, der gemäß<br />
des Kontexts erwartet wird.<br />
137
4 Semantische Analyse<br />
Die folgende kontextfreie Grammatik definiert Wertzuweisungen, deren Ausdrücke<br />
nur den Additionsoperator enthalten dürfen.<br />
S → name := E<br />
E → name addop name<br />
addop → +<br />
name → id<br />
Als Typen lassen wir real <strong>und</strong> integer zu. Dabei soll gelten, daß ein integer-Wert in<br />
einen real-Wert umgewandelt werden kann, falls als Ergebnis des Gesamtausdrucks<br />
ein real-Wert erwartet wird.<br />
Es soll also gelten:<br />
coercible(integer,integer) = true<br />
coercible(integer,real) = true<br />
coercible(real,real) = true<br />
coercible(real,integer) = false<br />
Zur Beschreibung des Typsystems verwenden wir die folgenden semantischen Regeln:<br />
S → name := E name.postmode := name.premode<br />
E.postmode :=<br />
if name.premode = integer<br />
then integer<br />
else real<br />
E → name addop name name1.postmode := E.premode<br />
name2.postmode := E.premode<br />
addop.postmode := E.premode<br />
E.premode :=<br />
if coercible(name1.premode,integer)<br />
and coercible (name2.premode,integer)<br />
then integer<br />
else real<br />
addop → + +.operation :=<br />
if addop.postmode = integer<br />
then int addition<br />
else real addition<br />
name → id name.premode := lookup(id.entry)<br />
Anmerkungen zu den semantischen Regeln:<br />
138<br />
• Die Regel zur Berechnung von E.premode überprüft, ob der Wert des jeweiligen<br />
premode-Attributs der Nichtterminale name mit dem Typ integer verträglich<br />
ist. Ist dies der Fall, wird als Ergebnistyp des Ausdrucks integer verwendet,<br />
andernfalls real.<br />
• Zur Produktion E → name addop name kann eine semantische Bedingung angegeben<br />
werden, die durch einen Vergleich von E.premode <strong>und</strong> E.postmode
S<br />
name postmode:real :=<br />
E<br />
premode:real<br />
id<br />
entry:X<br />
postmode:integer<br />
premode:integer<br />
name addop<br />
name<br />
postmode:integer<br />
entry:I<br />
postmode:real<br />
premode:integer<br />
id +<br />
id<br />
operation:int_addition<br />
4.2 Typüberprüfung<br />
postmode:integer<br />
premode:integer<br />
entry:J<br />
Abbildung 4.13: Attributierter Strukturbaum einer Zuweisung.<br />
S<br />
name postmode:real :=<br />
E<br />
premode:real<br />
id<br />
entry:X<br />
postmode:integer<br />
premode:integer<br />
name addop<br />
name<br />
postmode:integer<br />
entry:I<br />
postmode:real<br />
premode:integer<br />
id +<br />
id<br />
operation:int_addition<br />
Typumwandlung<br />
postmode:integer<br />
premode:integer<br />
entry:J<br />
Abbildung 4.14: Attributierter Strukturbaum einer Zuweisung mit Attributabhängigkeiten.<br />
eine Überprüfung auf Vorliegen eines Typfehlers vornimmt. Es muß gelten<br />
coercible(E.premode,E.postmode), d.h. ein Fehler liegt vor, sobald der Ausdruck<br />
aufgr<strong>und</strong> seiner Struktur einen real-Wert liefert <strong>und</strong> aufgr<strong>und</strong> des Kontextes<br />
ein integer-Wert erwartet wird (coercible(real,integer) = false).<br />
• Das inherite Attribut +.operation wird als Information <strong>für</strong> die Codegenerierung<br />
benötigt. Durch die Überladung des Additionsoperators +, der sowohl die<br />
integer- als auch die real-Addition repräsentiert, kann der Codegenerator anhand<br />
des Werts dieses Attributs erkennen, <strong>für</strong> welche Art der Addition er Code<br />
erzeugen muß.<br />
In Abbildung 4.13 ist der attributierte Strukturbaum der Analyse der Zuweisung<br />
X:=I+J angegeben. Wir nehmen an, daß X eine Variable vom Typ real ist <strong>und</strong> I<br />
<strong>und</strong> J Variablen von Typ integer sind. In Abbildung 4.14 wurde der Strukturbaum um<br />
die Attributabhängigkeiten ergänzt. Für das Nichtterminal E unterscheiden sich die<br />
Attributwerte von postmode <strong>und</strong> premode. Da jedoch coercible(integer,real) = true<br />
gilt, kann eine Typumwandlung vorgenommen werden, so daß die Zuweisung korrekt<br />
getypt ist.<br />
139
4 Semantische Analyse<br />
S<br />
name<br />
postmode:integer<br />
:=<br />
E<br />
premode:integer<br />
id<br />
entry:X<br />
postmode:real<br />
premode:real<br />
entry:I<br />
name addop<br />
postmode:real<br />
id +<br />
postmode:integer<br />
premode:real<br />
operation:<br />
real_addition<br />
Typfehler<br />
name<br />
id<br />
postmode:real<br />
premode:integer<br />
entry:J<br />
Typumwandlung<br />
Abbildung 4.15: Attributierter Strukturbaum einer Zuweisung mit Typfehler.<br />
Wir ändern die den Variablen zugeordneten Typen: sei nun X vom Typ integer<br />
<strong>und</strong> I vom Typ real. Der attributierte Strukturbaum der Zuweisung ist Abbildung<br />
4.15 dargestellt. Die semantische Analyse entdeckt einen Typfehler, da das Attribut<br />
premode von E den Wert real, das Attribut postmode den Wert integer besitzt<br />
<strong>und</strong> coercible(real,integer) = false gilt. Vor der Feststellung des Fehlers wird <strong>für</strong><br />
die Variable J eine Typumwandlung durchgeführt, um den integer-Wert von J zum<br />
real-Wert von I addieren zu können. Aufgr<strong>und</strong> der Zusammensetzung des Ausdrucks<br />
erhält E.premode den Wert real. Aus dem Kontext der Zuweisung (X ist integer-<br />
Variable) erhält E.postmode den Wert integer, so daß ein Typfehler vorliegt, der bei<br />
der Verträglichkeitsprüfung dieser beiden Attribute bemerkt wird.<br />
140
5 Zwischencode-Erzeugung<br />
Nachdem in den Kapiteln 2, 3 <strong>und</strong> 4 die einzelnen Phasen der Analyse eines Quelltexts<br />
behandelt wurden, werden in den folgenden Kapiteln die Synthesephasen beschrieben.<br />
Die erste Synthesephase ist die Zwischencode-Erzeugung. Zwischencode ist Code <strong>für</strong><br />
eine abstrakte (oder virtuelle) Maschine. Abstrakte Maschinen werden im Gegensatz<br />
zu realen Maschinen durch Programme realisiert, die den vom Compiler erzeugten<br />
Zwischencode interpretieren. Da Zwischencode von abstrakten Maschinen ausgeführt<br />
wird <strong>und</strong> somit von einer konkreten Zielmaschine unabhängig ist, wird die<br />
Zwischencode-Erzeugung zum Front-End eines Compilers gezählt (Abbildung 5.1,<br />
siehe auch Seite 12).<br />
Die Erzeugung von Code <strong>für</strong> eine abstrakte Maschine statt <strong>für</strong> eine reale Maschine<br />
bietet mehrere Vorteile:<br />
• Übersetzte Programme können auf allen Rechnern <strong>und</strong> unter allen Betriebssystemen<br />
laufen, <strong>für</strong> die ein Programm existiert, das die abstrakte Maschine<br />
realisiert. Beispiele <strong>für</strong> die erhöhte Portabilität durch Verwendung einer abstrakten<br />
Maschine sind das UCSD-System [Güt85] mit der p-Maschine <strong>und</strong> das<br />
Java-System [LY99].<br />
• Neben der Portabilität übersetzter Programme wird die Portierung von Übersetzern<br />
wesentlich vereinfacht, da der Zwischencode-Generator nicht jedesmal<br />
neu implementiert werden muß, um an die veränderte Hard- <strong>und</strong> Software angepaßt<br />
zu werden.<br />
• Durch die Verwendung von Zwischencode ist eine maschinenunabhängige Co-<br />
Parser<br />
semantische Zwischen-<br />
Analyse<br />
codegenerator<br />
Zwischendarstellung<br />
Front-End Back-End<br />
Zwischencode<br />
Abbildung 5.1: Einordnung der Zwischencode-Erzeugung.<br />
Codegenerator<br />
141
5 Zwischencode-Erzeugung<br />
deoptimierung möglich. Diese kann z.B. Codeabschnitte, die nie durchlaufen<br />
werden können, aus dem Code entfernen.<br />
• Die Architektur heutiger Hardwaremaschinen orientiert sich an der Struktur<br />
des von-Neumann-Rechners. Diese beruht auf der Verwendung von Speicher<br />
in Form von Variablen <strong>und</strong> einer Steuerung des Programmflusses durch bedingte<br />
<strong>und</strong> unbedingte Sprünge. Im Gegensatz zu den imperativen Sprachen<br />
wie C [KR90] oder Modula-2 [Wir97] lassen sich funktionale Sprachen wie ML<br />
[HMM97, Pau96] <strong>und</strong> Haskell [Po96] sowie logische Sprachen wie Prolog [CM94]<br />
nur unter großem Aufwand auf diese Maschinen abbilden. Aus diesem Gr<strong>und</strong><br />
kann eine abstrakte Maschine, die in ihrer Struktur auf die Bearbeitung der<br />
Konstrukte solcher Sprachen hin optimiert ist, die Codeerzeugung wesentlich<br />
vereinfachen. Beispiele <strong>für</strong> solche Maschinen sind die MaMa <strong>für</strong> funktionale<br />
Sprachen <strong>und</strong> die Warren Abstract Machine <strong>für</strong> Prolog [WM96].<br />
Der wesentliche Nachteil abstrakter Maschinen ist der Verlust an Effizienz, der durch<br />
die Interpretation des Zwischencodes durch die abstrakte Maschine entsteht. Außerdem<br />
können bei größeren Programmen Speicherplatzprobleme entstehen, da der<br />
Speicher neben dem auszuführenden Programm auch die abstrakte Maschine enthalten<br />
muß. Allerdings ist durch die spezielle Struktur der abstrakten Maschine der<br />
Zwischencode meist kürzer als der Code <strong>für</strong> die reale Maschine, da ein Befehl des<br />
Quelltexts meist in eine kurze Folge von Befehlen der abstrakten Maschine übersetzt<br />
werden kann.<br />
Es gibt verschiedene Arten der Darstellung von Zwischencode. In den folgenden Abschnitten<br />
werden wir auf zwei verbreitete Arten eingehen.<br />
5.1 Abstrakte Keller-Maschinen<br />
Reale Hardware-Maschinen besitzen in der Regel eine große Anzahl von internen<br />
Registern, in denen die arithmetischen <strong>und</strong> logischen Operationen ausgeführt werden.<br />
Die dazu benötigten Werte werden zuvor, sofern sie sich noch nicht in Registern<br />
befinden, aus dem Hauptspeicher geladen. Das Ergebnis der Operation wird ebenfalls<br />
in Registern abgelegt <strong>und</strong> kann bei Bedarf in den Hauptspeicher geschrieben werden<br />
(siehe auch Kapitel ??).<br />
Im Gegensatz hierzu besitzen abstrakte Keller-Maschinen keine <strong>für</strong> den Benutzer<br />
verwendbaren Register. Statt dessen werden die Daten auf einem Keller abgelegt.<br />
Die arithmetischen <strong>und</strong> logischen Operationen finden stets auf den obersten Werten<br />
des Kellers statt, die bei der Durchführung einer Operation vom Keller entfernt<br />
<strong>und</strong> durch das Ergebnis ersetzt werden. Zur Erzeugung von Zwischencode <strong>für</strong> eine<br />
abstrakte Keller-Maschine benötigen wir das Konzept der Postfix-Notation, in der<br />
der Operator hinter seine Operanden geschrieben wird.<br />
142
Beispiel 57 (Übersetzung arithmetischer Ausdrücke)<br />
5.1 Abstrakte Keller-Maschinen<br />
Die Postfix-Notation eines Ausdrucks kann gemäß den folgenden Regeln berechnet<br />
werden:<br />
E = id ⇒ postfix(E) = E<br />
E = E1 op E2 ⇒ postfix(E) = postfix(E1) postfix(E2) op<br />
E = ( E1 ) ⇒ postfix(E) = postfix(E1)<br />
In Postfix-Ausdrücken sind Klammern überflüssig, da Position <strong>und</strong> Stelligkeit der<br />
Operanden nur eine Interpretation eines Ausdrucks zulassen.<br />
Wir geben ein Beispiel <strong>für</strong> eine Überführung des Infix-Ausdrucks (3 + 4) ∗ (5 − 2) in<br />
Postfix-Notation an:<br />
postfix((3 + 4) ∗ (5 − 2))<br />
= postfix((3 + 4)) postfix((5 − 2)) ∗<br />
= postfix(3 + 4) postfix(5 − 2) ∗<br />
= postfix(3) postfix(4) + postfix(5) postfix(2) − ∗<br />
= 3 4 + 5 2 − ∗<br />
Zur vereinfachten Übertragung eines Ausdrucks in Postfix-Notation führen wir Syntaxbäume<br />
ein.<br />
5.1.1 Syntaxbäume<br />
Syntaxbäume stellen wie die in Kapitel 3 eingeführten Strukturbäume die Struktur<br />
eines Quellprogramms dar. Syntaxbäume enthalten im Gegensatz zu Strukturbäumen<br />
aber keine Nichtterminale. Die Blätter von Syntaxbäumen sind mit Bezeichnern<br />
<strong>und</strong> Werten beschriftet, die inneren Knoten mit Operatorsymbolen. Auf diese Weise<br />
repräsentieren Syntaxbäume die hierarchische Struktur des Quellprogramms.<br />
Beispiel 58 (Arithmetische Ausdrücke mit negativen Operanden)<br />
Die folgende Grammatik beschreibt die Syntax von Zuweisungsoperatoren.<br />
S → id:=E<br />
E → E + E | E * E | -E | ( E ) | id<br />
Die Ausdrücke zur Berechnung des zuzuweisenden Wertes können ein negatives Vorzeichen<br />
besitzen. Wir nehmen an, daß die üblichen Prioritäten der arithmetischen<br />
Operatoren gelten.<br />
In Abbildung 5.2 ist der Syntaxbaum <strong>für</strong> die Anweisung a := b∗(−c)+b∗(−c) dargestellt.<br />
Die Wurzel des Baums repräsentiert den Zuweisungsoperator, der linke Teilbaum<br />
den Bezeichner, dem der berechnete Wert zugewiesen werden soll, <strong>und</strong> der<br />
rechte Teilbaum den auszuwertenden Ausdruck.<br />
143
5 Zwischencode-Erzeugung<br />
assign<br />
a +<br />
*<br />
b minus<br />
*<br />
b minus<br />
c c<br />
Abbildung 5.2: Syntaxbaum<br />
Die Postfix-Notation ist eine linearisierte Darstellung eines Syntaxbaumes. Durchläuft<br />
man den Baum nach dem Postorder-Verfahren (<strong>für</strong> jeden Knoten werden zuerst der<br />
linke <strong>und</strong> der rechte Teilbaum durchlaufen), ergibt sich die Darstellung des Ausdrucks<br />
in Postfix-Notation.<br />
Beispiel 59 (Fortsetzung von Beispiel 58)<br />
Ein Postorder-Durchlauf durch den Syntaxbaum in Abbildung 5.2 erzeugt daher die<br />
folgende Darstellung in Postfix-Notation:<br />
a b c minus ∗ b c minus ∗ + assign<br />
Ein Strukturbaum kann mit Hilfe attributierter Grammatiken in einen Syntaxbaum<br />
überführt werden. Hierzu werden als Attribute Zeiger verwendet, die die einzelnen<br />
Knoten des neu erzeugten Syntaxbaums miteinander verbinden.<br />
Beispiel 60 (Fortsetzung von Beispiel 59)<br />
Folgende attributierte Grammatik erzeugt den Syntaxbaum <strong>für</strong> den oben angegebenen<br />
Ausdruck:<br />
S → id:= E S.nptr := mknode(’assign’,mkleaf(id, id.place), E.nptr)<br />
E → E + E E1.nptr := mknode(’+’, E2.nptr, E3.nptr)<br />
E → E * E E1.nptr := mknode(’*’, E2.nptr, E3.nptr)<br />
E → -E E1.nptr := mknode(’minus’, E2.nptr)<br />
E → ( E ) E1.nptr := E2.nptr<br />
E → id E.nptr := mkleaf(id, id.place)<br />
Der generierte Syntaxbaum ist in Abbildung 5.3 dargestellt. Für jeden Bezeichner<br />
des Ausdrucks wird ein Blatt erzeugt (mit der Funktion mkleaf). Für die Operatoren<br />
werden mit der Funktion mknode innere Knoten generiert. Diese besitzen als Komponenten<br />
Zeiger, die auf den linken bzw. den rechten Teilbaum des jeweiligen Knotens<br />
verweisen.<br />
144
assign<br />
id a<br />
+<br />
* *<br />
id b id b<br />
minus minus<br />
id c id c<br />
5.1 Abstrakte Keller-Maschinen<br />
0<br />
1<br />
2<br />
3<br />
4<br />
5<br />
6<br />
7<br />
8<br />
9<br />
10<br />
11<br />
id<br />
id<br />
minus<br />
b<br />
c<br />
1<br />
* 0 2<br />
id b<br />
id c<br />
minus 5<br />
* 4 6<br />
+ 3 7<br />
id a<br />
assign 9 8<br />
Abbildung 5.3: Konstruktion eines Syntaxbaums aus Postfix-Notation.<br />
. . .<br />
145
5 Zwischencode-Erzeugung<br />
5<br />
9<br />
45<br />
Abbildung 5.4: Auswertung eines Postfix-Ausdrucks mit Hilfe eines Stacks.<br />
Syntaxbäume lassen sich auch in Form von Tabellen angeben. In der rechten Hälfte<br />
von Abbildung 5.3 ist eine solche Tabelle <strong>für</strong> den eben erzeugten Syntaxbaum angegeben.<br />
Die Zeiger werden nun durch die Zeilennummern repräsentiert.<br />
Die Postfix-Darstellung der Anweisung ist dem Zwischencode <strong>für</strong> eine Stackmaschine<br />
bereits sehr ähnlich, da diese Darstellung schon zur Auswertung mit Hilfe eines Stacks<br />
geeignet ist.<br />
Dabei wenden wir das folgende Verfahren an:<br />
• Wir durchlaufen den Postfix-Ausdruck von links nach rechts.<br />
• Ist das aktuelle Symbol ein Wert, wird dieser Wert auf den Stack gelegt <strong>und</strong><br />
bildet somit die neue Kellerspitze.<br />
• Handelt es sich bei dem aktuellen Symbol um einen Operator, werden so viele<br />
Werte vom Stack entfernt, wie der Operator zur Auswertung Operanden<br />
benötigt (die Anzahl der zu entfernenden Werte entspricht der Stelligkeit des<br />
Operators). Aus diesen Werten wird das Ergebnis berechnet <strong>und</strong> auf den Stack<br />
gelegt.<br />
Beispiel 61<br />
Wir berechnen den Ausdruck 9 * 5 + 2. Zuerst wird der Ausdruck in Postfix-Notation<br />
transformiert:<br />
postfix( 9 * 5 + 2 ) = postfix(( 9 * 5 ) + 2 )<br />
= 9 5 * 2 +<br />
Nun wird der generierte Postfix-Ausdruck mit Hilfe eines Stacks ausgewertet (siehe<br />
Abbildung 5.4). Zuerst wird der Wert 9 auf den Stack gelegt. Danach folgt der Wert<br />
5, der ebenfalls auf den Stack gelegt wird. Beim nächsten Symbol handelt es sich um<br />
den Operator *. Dieser besitzt die Stelligkeit 2, so daß zwei Werte vom Stack entfernt<br />
werden müssen. Aus diesen Werten wird das Ergebnis 45 berechnet <strong>und</strong> auf den Stack<br />
gelegt. Danach wird die 2 auf den Stack gelegt <strong>und</strong> durch den Additionsoperator mit<br />
dem Ergebnis der Multiplikation verknüpft.<br />
146<br />
2<br />
45<br />
47
5.1.2 Zwischencode <strong>für</strong> die Keller-Maschine<br />
5.1 Abstrakte Keller-Maschinen<br />
Nachdem wir im vorherigen Abschnitt das Konzept der Auswertung von Postfix-<br />
Ausdrücken erläutert haben, definieren wir nun den Befehlssatz der abstrakten Keller-<br />
Maschine. Weiterhin beschreiben wir die Erzeugung von Zwischencode durch attributierte<br />
Grammatiken.<br />
Wir nehmen an, daß der Befehlssatz der abstrakten Keller-Maschine die folgenden<br />
Befehle beinhaltet:<br />
ADD Addition der obersten beiden Stack-Elemente.<br />
Diese werden entfernt, <strong>und</strong> das Ergebnis wird auf den Stack gelegt.<br />
SUB Subtraktion.<br />
MUL Multiplikation.<br />
LOAD v Lege Wert v auf den Stack.<br />
; Sequentielle Komposition.<br />
Zur Erzeugung des Zwischencodes <strong>für</strong> Ausdrücke mit einer attributierten Grammatik<br />
führen wir ein synthetisches Attribut Code ein <strong>und</strong> definieren die Grammatik wie<br />
folgt:<br />
E → E + T E1.Code := E2.Code; T.Code; ADD<br />
E → E - T E1.Code := E2.Code; T.Code; SUB<br />
E → T E.Code := T.Code<br />
T → T * F T1.Code := T2.Code; F.Code; MUL<br />
T → F T.Code := F.Code<br />
F → ( E ) F.Code := E.Code<br />
F → 0 F.Code := LOAD 0<br />
.<br />
F → 9 F.Code := LOAD 9<br />
Bei der Übersetzung der Operatoren wird zunächst Code <strong>für</strong> die Auswertung der<br />
Operanden erzeugt. Nach Ausführung dieser Codeabschnitte befinden sich ihre Werte<br />
auf dem Stack <strong>und</strong> können dann durch den entsprechenden arithmetischen Befehl<br />
verknüpft werden.<br />
Beispiel 62 (Fortführung von Beispiel 61)<br />
Das anhand der attributierten Grammatik erzeugte Programm <strong>für</strong> die Stack-Maschine<br />
lautet dann:<br />
LOAD 9; LOAD 5; MUL; LOAD 2; ADD<br />
Der attributierte Strukturbaum ist in Abbildung 5.5 angegeben.<br />
5.1.3 Befehle zur Steuerung des Kontrollflusses<br />
Bisher enthielt der Befehlssatz nur die sequentielle Komposition zur Steuerung des<br />
Kontrollflusses. Nun führen wir als neue Zwischencodebefehle bedingte <strong>und</strong> unbeding-<br />
147
5 Zwischencode-Erzeugung<br />
148<br />
LOAD 9<br />
LOAD 9<br />
LOAD 9;LOAD 5;MUL<br />
T<br />
T *<br />
F<br />
9<br />
E<br />
E + T<br />
LOAD 9;LOAD 5;MUL<br />
F<br />
5<br />
LOAD 5<br />
LOAD 9;LOAD 5;MUL;LOAD 2;ADD<br />
F<br />
2<br />
LOAD 2<br />
LOAD 2<br />
Abbildung 5.5: Attributierter Strukturbaum mit Zwischencode.
5.1 Abstrakte Keller-Maschinen<br />
te Sprünge ein. Hierbei verwenden wir anstelle von Speicheradressen Sprungmarken<br />
zur Angabe des Ziels von Sprungbefehlen.<br />
Wir erweitern den in Abschnitt 5.1.2 eingeführten Befehlssatz um die folgenden Befehle:<br />
LABEL l Definition der Sprungmarke l.<br />
GOTO l Sprung zu der Anweisung nach Sprungmarke l<br />
GOFALSE l Entfernen des obersten Stack-Elements,<br />
Sprung falls 0.<br />
GOTRUE l Sprung falls �= 0.<br />
HALT Beenden der Programmausführung.<br />
Der Befehl LABEL definiert eine Sprungmarke, die von den Sprungbefehlen als<br />
Sprungziel verwendet werden kann. Wird ein Sprung ausgeführt, wird die Programmausführung<br />
mit dem der Marke folgenden Befehl fortgesetzt. GOTO ist ein unbedingter<br />
Sprung. Die Befehle GOFALSE <strong>und</strong> GOTRUE sind bedingte Sprünge, die einen<br />
Sprung in Abhängigkeit des Werts an der Kellerspitze ausführen. Hierbei wird eine<br />
semi-boolesche Logik 1 verwendet, in der 0 den Wert false repräsentiert <strong>und</strong> alle anderen<br />
Werte als true interpretiert werden. Befindet sich an der Kellerspitze der Wert<br />
0, führt GOFALSE den Sprung durch, andernfalls wird mit dem nächsten Befehl<br />
fortgefahren. Entsprechend springt GOTRUE, wenn die Kellerspitze einen anderen<br />
Wert als 0 enthält. In jedem Fall wird der oberste Kellerwert entfernt.<br />
Im folgenden betrachten wir die Darstellung der aus imperativen Sprachen bekannten<br />
Kontrollstrukturen if <strong>und</strong> while im Zwischencode. Weitere Konstrukte wie z.B. die<br />
repeat-Schleife lassen sich analog darstellen. Wir nehmen an, daß <strong>für</strong> die relationalen<br />
Operationen wie , = Zwischencodebefehle existieren, die die Werte 0 (<strong>für</strong> false)<br />
bzw. 1 (<strong>für</strong> true) auf den Stack legen.<br />
1 Wie in der Sprache C.<br />
149
5 Zwischencode-Erzeugung<br />
if-Anweisung <strong>und</strong> while-Schleife:<br />
Die folgende Grammatik definiert eine if-Anweisung ohne else-Alternative <strong>und</strong> die<br />
while-Schleife:<br />
stmt → if expr then stmt end |<br />
while expr do stmt end |<br />
stmt; stmt<br />
Die if-Anweisung läßt sich im Zwischencode durch folgendes Schema darstellen:<br />
Code <strong>für</strong> expr; (legt semi-boolschen Wert auf den Stack)<br />
GOFALSE out;<br />
Code <strong>für</strong> stmt;<br />
LABEL out<br />
Zunächst wird Code <strong>für</strong> die Berechnung des booleschen Ausdrucks generiert. Dieser<br />
legt als Ergebnis entweder 0 (<strong>für</strong> false) oder 1 (<strong>für</strong> true) auf den Keller. Ist der<br />
Wert 0, sollen Befehle nach dem then nicht ausgeführt werden. Dies wird durch den<br />
Sprungbefehl GOFALSE realisiert, der in diesem Fall an das Ende des Codes <strong>für</strong> die<br />
if-Anweisung verzweigt.<br />
Analog wird die while-Schleife durch das folgende Schema realisiert:<br />
LABEL test;<br />
Code <strong>für</strong> expr;<br />
GOFALSE out;<br />
Code <strong>für</strong> stmt;<br />
GOTO test;<br />
LABEL out<br />
Zunächst wird wieder Code <strong>für</strong> die Auswertung des Ausdrucks erzeugt. Legt dieser<br />
Code nach Ausführung den Wert 0 auf den Stack, wird der Schleifenrumpf nicht<br />
mehr ausgeführt, indem mit GOFALSE ans Ende der Schleife verzweigt wird. Wird<br />
der Schleifenrumpf ausgeführt, wird anschließend mit GOTO wieder an den Anfang<br />
der Schleife gesprungen <strong>und</strong> der Code <strong>für</strong> die Berechnung von expr erneut ausgeführt.<br />
Enthält ein Programm mehrere if- oder while-Anweisungen, müssen bei der Übersetzung<br />
jeder dieser Anweisungen neue Sprungmarken erzeugt werden. Daher nehmen<br />
wir im folgenden an, daß eine Funktion newlabel zur Verfügung steht, die in jedem<br />
Aufruf eine neue Sprungmarke generiert.<br />
Hinzu kommt, daß die Kontrollflußanweisungen auch verschachtelt auftreten dürfen.<br />
Daher muß eine Verwaltung der Sprungmarken gewährleisten, daß die Sprungbefehle<br />
<strong>und</strong> die zugehörigen Marken die Verschachtelung der Anweisungen korrekt widerspiegeln.<br />
Dies erreichen wir, indem wir spezielle Attribute <strong>für</strong> das Zwischenspeichern<br />
von Marken verwenden.<br />
Wir betrachten als Beispiel die Produktion <strong>für</strong> die if-Anweisung:<br />
150
5.2 Drei-Adreß-Code<br />
stmt → if expr then stmt end<br />
stmt1.out := newlabel<br />
stmt1.Code := expr.Code;<br />
GOFALSE stmt1.out;<br />
stmt2.Code;<br />
LABEL stmt1.out<br />
Wir verwenden ein Attribut out, das die von der Funktion newlabel erzeugte Sprungmarke<br />
speichert. Die GOFALSE- <strong>und</strong> LABEL-Befehle verwenden dann die in diesem<br />
Attribut gespeicherte Sprungmarke. Entsprechendes gilt <strong>für</strong> den Code der while-<br />
Schleife.<br />
Beispiel 63 (Geschachtelte if-Anweisung)<br />
Wir übersetzen eine geschachtelte if-Anweisung in Zwischencode <strong>für</strong> die abstrakte Kellermaschine.<br />
Hierbei nehmen wir an, daß die Funktion newlabel Zahlen als Sprungmarken<br />
generiert.<br />
Wir betrachten die folgende Anweisung:<br />
if e1 then if e2 then s end end ; other<br />
e1 <strong>und</strong> e2 repräsentieren Ausdrücke, s steht <strong>für</strong> eine Sequenz von Anweisungen. Der<br />
attributierte Strukturbaum <strong>für</strong> die Anweisung ist in Abbildung 5.6 dargestellt. Der<br />
Inhalt des Attributs Code wird durch Kästen angegeben, der Inhalt von out durch<br />
Kreise.<br />
5.2 Drei-Adreß-Code<br />
Eine weitere Variante von Zwischencode ist der Drei-Adreß-Code. Im Vergleich zum<br />
Zwischencode <strong>für</strong> abstrakte Keller-Maschinen ist er näher an der “Maschinensprache”<br />
realer Maschinen einzuordnen.<br />
Wie der Zwischencode <strong>für</strong> die Keller-Maschine besteht der Drei-Adreß-Code aus einer<br />
Folge von Befehlen. Jeder Befehl gehört zu einem der folgenden Formate:<br />
x := y op z op ist ein arithmetischer oder logischer binärer<br />
Operator<br />
x := op y op ist unärer Operator<br />
z.B. log. Negation, Schiebe-Operation<br />
x := y Kopieranweisung<br />
goto l unbedingter Sprung<br />
if x relop y goto l bedingter Sprung: relop ist ein Vergleichsoperator<br />
Weiterhin gibt es spezielle Befehle zur Daten-/Speicher-Behandlung <strong>und</strong> <strong>für</strong> Prozedur-<br />
Aufrufe.<br />
151
5 Zwischencode-Erzeugung<br />
Code(e1)<br />
GOFALSE1<br />
Code(e2)<br />
GOFALSE2<br />
Code(s)<br />
LABEL2<br />
LABEL1<br />
152<br />
Code<br />
out<br />
if then<br />
expr<br />
stmt<br />
Code(e1)<br />
if<br />
expr<br />
1<br />
2<br />
stmt<br />
; stmt<br />
end other<br />
stmt<br />
then end<br />
Code(e2)<br />
Code(e2)<br />
GOFALSE2<br />
Code(s)<br />
LABEL2<br />
stmt<br />
Code(e1)<br />
GOFALSE1<br />
Code(e2)<br />
GOFALSE2<br />
Code(s)<br />
LABEL2<br />
LABEL1<br />
Code(other)<br />
Code(s)<br />
Code(other)<br />
Abbildung 5.6: Attributierter Strukturbaum einer verschachtelten if-Anweisung.
5.2 Drei-Adreß-Code<br />
Die Bezeichnung “Drei-Adreß-Code” kommt daher, daß alle Befehle der oben angeführten<br />
Formate maximal drei Adressen enthalten (zwei Adressen <strong>für</strong> die Operanden<br />
des Befehls <strong>und</strong> eine Adresse <strong>für</strong> die Speicherung des Ergebnisses). Im Gegensatz<br />
zur abstrakten Keller-Maschine, bei der die Werte <strong>und</strong> Zwischenergebnisse auf dem<br />
Keller verwaltet wurden, werden beim Drei-Adreß-Code die Werte <strong>und</strong> Ergebnisse in<br />
einem Arbeitsspeicher gehalten. Jeder Drei-Adreß-Befehl liest seine Operanden aus<br />
dem Speicher <strong>und</strong> schreibt das errechnete Ergebnis ebenfalls in eine Speicherzelle.<br />
Da jeder Drei-Adreß-Befehl maximal zwei Operanden verknüpfen kann, müssen komplexe<br />
Ausdrücke in Folgen von Befehlen übersetzt werden. Die dabei entstehenden<br />
Zwischenergebnisse werden in Speicherzellen abgelegt, die durch temporäre Namen<br />
gekennzeichnet sind. “Temporär” bedeutet in diesem Zusammenhang, daß die Namen<br />
nicht Bezeichnern des Quellprogramms entsprechen.<br />
Beispiel 64<br />
Im folgenden wird eine Übersetzung des Ausdrucks x+y∗z angegeben:<br />
x + y * z → t1 := y * z<br />
t2 := x + t1<br />
Aufgr<strong>und</strong> der Operatorprioritäten wird die Multiplikation zuerst ausgeführt <strong>und</strong> ihr<br />
Ergebnis in einem Speicherplatz mit temporärem Namen t1 zwischengespeichert. Der<br />
zweite Befehl addiert den Wert der Speicherzelle x zum Zwischenergebnis <strong>und</strong> legt<br />
das Ergebnis im temporären Speicherplatz t2 ab.<br />
5.2.1 Übersetzung von Syntaxbäumen in Drei-Adreß-Code<br />
Bei der Übersetzung eines Syntaxbaums in Drei-Adreß-Code müssen zuerst die temporären<br />
Namen <strong>für</strong> die Ablage von Zwischenergebnissen vereinbart werden. Dies geschieht,<br />
indem die inneren Knoten des Syntaxbaums mit temporären Namen beschriftet<br />
werden.<br />
Beispiel 65<br />
Wir übersetzen die Anweisung a := b∗(−c)+b∗(−c) aus Beispiel 58 in Drei-Adreß-<br />
Code. Hierzu beschriften wir die inneren Knoten des Syntaxbaums aus Abbildung<br />
5.2, die Zwischenergebnisse repräsentieren, mit den temporären Namen t1 bis t5.<br />
(Abbildung 5.7).<br />
Nachdem die temporären Namen vergeben sind, kann aus dem Syntaxbaum Drei-<br />
Adreß-Code erzeugt werden. Hierzu wird der Baum im Postorder-Verfahren durchlaufen,<br />
so daß vor der Bearbeitung eines Knotens zuerst seine Teilbäume ausgewertet<br />
werden. Für jeden mit einem temporären Namen beschrifteten Knoten wird ein<br />
Drei-Adreß-Befehl generiert. Als Zieladresse des Befehls wird der temporäre Name<br />
153
5 Zwischencode-Erzeugung<br />
assign<br />
a +<br />
*<br />
t2<br />
b minus<br />
t1<br />
t5<br />
*<br />
t4<br />
b minus<br />
c c<br />
Abbildung 5.7: Syntaxbaum mit temporären Namen.<br />
verwendet. Die Operanden ergeben sich aus den Beschriftungen der Kinder des Knotens.<br />
Ebenso wird mit jedem Knoten verfahren, der einen Speicherplatz repräsentiert,<br />
auf den schreibend zugegriffen wird.<br />
Beispiel 66 (Fortsetzung von Beispiel 65)<br />
Ein Postorder-Durchlauf durch den in Abbildung 5.7 angegebenen Syntaxbaum nach<br />
dem eben erläuterten Verfahren generiert die folgende Sequenz von Drei-Adreß-Befehlen:<br />
t1 := -c<br />
t2 := b * t1<br />
t3 := -c<br />
t4 := b * t3<br />
t5 := t2 + t4<br />
a := t5<br />
Bemerkung: Der in dem Beispiel erzeugte Drei-Adreß-Code ist nicht optimal, da<br />
nicht beachtet wird, daß der Teilausdruck b∗(−c) zweimal auftritt. Aus diesem Gr<strong>und</strong><br />
ist die Verwendung der Speicherplätze t3 <strong>und</strong> t4 überflüssig <strong>und</strong> der vorletzte Befehl<br />
könnte zu t5 := t2 + t2 modifiziert werden. Auf diese Weise können zwei Befehle <strong>und</strong><br />
zwei temporäre Speicherplätze eingespart werden. Im Rahmen der Codegenerierung<br />
in Kapitel ?? geben wir ein Verfahren an, daß die beschriebene Optimierung <strong>für</strong><br />
mehrfach auftretende Teilausdrücke durchführt.<br />
154<br />
t3
5.2 Drei-Adreß-Code<br />
5.2.2 Übersetzung in Drei-Adreß-Code unter Verwendung von<br />
attributierten Grammatiken<br />
Wir beschreiben nun die Generierung von Drei-Adreßcode mit Hilfe von attributierten<br />
Grammatiken. Wir verwenden dabei die auf Seite 147 angegebene kontextfreie<br />
Grammatik zur Beschreibung von Ausdrücken. Das Attribut Code speichert den zu<br />
einem Nichtterminal generierten Drei-Adreß-Code. Das Attribut place enthält den<br />
Namen des Speicherplatzes, in dem das Ergebnis eines Befehls abgelegt werden soll.<br />
Wir nehmen an, daß eine Funktion newtemp existiert, die bei jedem Aufruf einen<br />
neuen temporären Namen generiert.<br />
S → id := E S.Code := E.Code;<br />
id.place := E.place<br />
E → E + E E1.place := newtemp<br />
E1.Code := E2.Code; E3.Code;<br />
E1.place := E2.place + E3.place<br />
E → E * E (analog)<br />
E → -E E1.place := newtemp<br />
E1.Code := E2.Code;<br />
E1.place := - E2.place<br />
E → (E) E1.place := E2.place<br />
E1.Code := E2.Code<br />
E → id E1.place := id.place<br />
E1.Code := ...<br />
Bei der Übersetzung eines arithmetischen Operatores wird zunächst ein neuer temporärer<br />
Name erzeugt. Der Code <strong>für</strong> diesen Operator besteht aus dem Code <strong>für</strong> die<br />
Berechnung der Operanden. Anschließend wird ein Befehl angefügt, der die Inhalte<br />
der Speicherzellen, die die Zwischenergebnisse beinhalten, mit dem Operator verknüpft.<br />
Das Ergebnis wird in den zuvor generierten Zwischenspeicher geschrieben.<br />
Beispiel 67<br />
In Abbildung 5.8 ist der attributierte Syntaxbaum <strong>für</strong> die Anweisung a := b * (<br />
- c ) + d angegeben. Die Codefolge aus Drei-Adreß-Befehlen wird schrittweise in<br />
einem Bottom-Up-Durchlauf zusammengesetzt, bis das Code-Attribut an der Wurzel<br />
die vollständige Befehlsfolge beinhaltet. Somit ergibt sich eine durch die Übersetzung<br />
folgende Sequenz von Drei-Adreß-Befehlen:<br />
t1 := - c<br />
t2 := b * t1<br />
t3 := t2 + d<br />
a := t3<br />
Die Übersetzung von Kontrollstrukturen erfolgt analog. Im Gegensatz zu den in<br />
Abschnitt 5.1.3 beschriebenen Befehlen GOFALSE <strong>und</strong> GOTRUE <strong>für</strong> die abstrak-<br />
155
5 Zwischencode-Erzeugung<br />
place = a<br />
S<br />
id :=<br />
E + E<br />
E * E<br />
id<br />
place = b<br />
place = b<br />
place = t2<br />
Code = t1 := -c;<br />
(<br />
-<br />
Code = t1 := -c; t2 := b * t1;<br />
t2 := b * t1<br />
E<br />
t3 := t2 + d; a := t3<br />
place = t1<br />
E<br />
id<br />
E<br />
Code = t1 := - c<br />
place = t1<br />
Code = t1 := - c<br />
place = t3<br />
Code = t1 := -c; t2 := b * t1;<br />
place = c<br />
place = c<br />
t3 := t2 + d<br />
id<br />
)<br />
place = d<br />
place = d<br />
Abbildung 5.8: Syntaxbaum mit Attributen <strong>für</strong> die Erzeugung von Drei-Adreß-Code.<br />
156
5.3 Vergleich der beiden Arten von Zwischencode<br />
te Keller-Maschine steht im Drei-Adreß-Code ein Befehl if x relop y goto l zur<br />
Verfügung, der Vergleichsoperation <strong>und</strong> Sprung in einem Befehl vereinigt.<br />
5.3 Vergleich der beiden Arten von Zwischencode<br />
In diesem Kapitel haben wir zwei unterschiedliche Arten von Zwischencode vorgestellt.<br />
Die Entscheidung <strong>für</strong> die Realisierung einer der beiden Methoden in einem<br />
Compiler hängt im wesentlichen vom angestrebten Ziel des Compilers ab.<br />
Die Darstellung von Zwischencode in Form von Code <strong>für</strong> abstrakte Keller-Maschinen<br />
ist geeignet, wenn eine weitere Übersetzung des Zwischencodes in die Maschinensprache<br />
einer realen Maschine nicht vorgesehen ist. Dies ist oft der Fall bei der Implementierung<br />
nicht-imperativer Programmiersprachen. Daher soll von der realen Hardware<br />
(Speicheradressen usw.) möglichst weit abstrahiert werden, um das Speichermanagement<br />
zu vereinfachen. Dies wird durch die Verwaltung der Daten <strong>und</strong> Variablen auf<br />
einem Stack erreicht.<br />
Der Drei-Adreß-Code hingegen ist gut geeignet <strong>für</strong> die Anwendung von Optimierungsverfahren.<br />
Daher bietet sich seine Verwendung an, wenn die Erstellung von<br />
Zwischencode nur als Vorstufe <strong>für</strong> die eigentliche Codegenerierung vorgesehen ist.<br />
Durch die Anwendung von Optimierungsverfahren kann eine maschinenunabhängige<br />
Codeoptimierung vorgenommen werden. Zudem ist der Drei-Adreß-Code “maschinennäher”<br />
als der Code <strong>für</strong> die Keller-Maschine, da er nicht im selben Maße von den<br />
Gegebenheiten der Hardware abstrahiert.<br />
157
Literaturverzeichnis<br />
[Aho08] Sethi <strong>und</strong> Ullman Aho. Compiler. Prinzipien, Techniken <strong>und</strong> Werkzeuge.<br />
Pearson Studium, 2008. i<br />
[AK97] Heiko Vogler Armin Kühnemann. Attributgrammatiken. Eine gr<strong>und</strong>legende<br />
Einführung. Vieweg+Teubner, 1997.<br />
[ANT] ANTLR Parser Generator (http://www.antlr.org/). 117<br />
[ASU99] Alfred V. Aho, Ravi Sethi, Jeffrey D. Ullman. Compilerbau, Teil 1+2.<br />
Oldenbourg, 1999. 5, 8, 111, 113, 116, 128<br />
[AU72] Alfred V. Aho, Jeffrey D. Ullman. The Theory of Parsing, Translation,<br />
and Compiling. Addison-Wesley, 1972. 43<br />
[Bar83] J.G.P. Barnes. Programmieren in ADA. Hanser, 1983. 118<br />
[CM94] William F. Clocksin, Christopher S. Mellish. Programmieren in Prolog.<br />
Springer, 1994. 142<br />
[Ger91] Uwe Gerlach. Das Transputerbuch: der Einstieg in die Welt der Transputer.<br />
Markt & Technik, 1991.<br />
[GR83] Adele Goldberg, David Robson. Smalltalk-80 – The Language and its<br />
Implementation. Addison-Wesley, 1983. 131<br />
[Güt85] Rainer Güting. Einführung in UCSD-Pascal. Schwann-Bagel, 1985. 141<br />
[HM] Albrecht Wöß University of Linz Hanspeter Mössenböck, Markus Löberbauer.<br />
The Compiler Generator Coco/R (http://ssw.jku.at/coco/). 117<br />
[HMM97] Robert Harper, David MacQueen, Robin Milner. Standard ML. Technischer<br />
Bericht ECS–LFCS–86–2, Mit Press, 1997. 142<br />
[Hum92] Robert L. Hummel. Die Intel-Familie: technisches Referenzhandbuch <strong>für</strong><br />
den 80x86 <strong>und</strong> 80x87. TLC The Learning Companie, 1992.<br />
[Int94] SPARC International. The SPARC architecture manual. Prentice Hall,<br />
1994.<br />
[JAV] javacc: JavaCCHome (https://javacc.dev.java.net/). 117<br />
[JL92] Doug Brown John Levine, Tony Mason. Lex and Yacc: UNIX Programming<br />
Tools (A Nutshell handbook). O’Reilly Media, 1992. 117<br />
[JW91] Kathleen Jensen, Niklaus Wirth. Pascal user manual and report: ISO<br />
Pascal standard. Springer, 1991. 12, 131<br />
[Kop02] Helmut Kopka. L ATEX– eine Einführung. Addison-Wesley, 2002. 5<br />
[KR90] Brian W. Kernighan, Dennis M. Ritchie. Programmieren in C. Hanser,<br />
158
Literaturverzeichnis<br />
1990. 5, 105, 111, 131, 142<br />
[KV97] Armin Kühnemann, Heiko Vogler. Attributgrammatiken – Eine gr<strong>und</strong>legende<br />
Einführung. Vieweg+Teubner, 1997. 121<br />
[LE89] Henry M. Levy, Richard H. Eckhouse. Computer programming and architecture:<br />
the VAX. Digital Press, 1989.<br />
[LY99] Tim Lindholm, Frank Yellin. The Java Virtual Machine Specification.<br />
Addison-Wesley, 1999. 141<br />
[LYP] The LEX & YACC Page (http://dinosaur.compilertools.net/). 117<br />
[MB92] Tony Mason, Doug Brown. lex & yacc. O’Reilly & Associates Inc., 1992.<br />
111<br />
[Mot91] Motorola. M68000: 8-/16-/32-bit microprocessors user’s manual.<br />
Prentice-Hall, 1991.<br />
[OT97] Peter W. O’Hearn, Robert D. Tennent. ALGOL-Like Languages.<br />
Birkhäuser, 1997. 12<br />
[Pau96] Laurence C. Paulson. ML for the Working Programmer. Cambridge<br />
University Press, 1996. 142<br />
[Po96] John Peterson, other. Report on the Programming Language Haskell –<br />
Version 1.3. Technischer Bericht, University of Glasgow, 1996. 131, 142<br />
[SAB] SableCC (http://sablecc.org/). 117<br />
[Sch08] Uwe Schöning. Theoretische Informatik kurz gefaßt. Spektrum Akademischer<br />
Verlag, 2008. 36, 46, 118<br />
[Thi97] Peter Thiemann. Gr<strong>und</strong>lagen der funktionalen <strong>Programmierung</strong>. Teubner,<br />
1997. 129<br />
[WG92] Niklaus Wirth, Jürg Gutknecht. Project Oberon – The Design of an<br />
Operating System and Compiler. Addison-Wesley, 1992. 137<br />
[Wir86] Niklaus Wirth. Compilerbau. Teubner, 1986.<br />
[Wir97] Niklaus Wirth. Programmieren in Modula-2. Springer, 1997. 6, 131, 142<br />
[WM96] Reinhard Wilhelm, Dieter Maurer. Übersetzerbau – Theorie, Konstruktion,<br />
Generierung. Springer, 1996. i, 10, 23, 37, 88, 110, 111, 113, 142<br />
[WMW85] Gerhard Goos William M. Waite. Compiler Construction. Springer-<br />
Verlag GmbH, 1985.<br />
159