Compilerbau - Technische Universität Braunschweig

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