07.02.2013 Aufrufe

Vorlesungsskript - Institut für Programmierung und Reaktive Systeme

Vorlesungsskript - Institut für Programmierung und Reaktive Systeme

Vorlesungsskript - Institut für Programmierung und Reaktive Systeme

MEHR ANZEIGEN
WENIGER ANZEIGEN

Sie wollen auch ein ePaper? Erhöhen Sie die Reichweite Ihrer Titel.

YUMPU macht aus Druck-PDFs automatisch weboptimierte ePaper, die Google liebt.

TECHNISCHE UNIVERSITÄT CAROLO-WILHELMINA ZU BRAUNSCHWEIG<br />

<strong>Vorlesungsskript</strong><br />

Compilerbau<br />

Prof. Dr. Ursula Goltz Dr. Thomas Gehrke<br />

Dipl.-Inform. Malte Lochau<br />

1. April 2009<br />

<strong>Institut</strong> <strong>für</strong> <strong>Programmierung</strong> <strong>und</strong> <strong>Reaktive</strong> <strong>Systeme</strong>


Vorwort<br />

Das vorliegende Skript ist als Ausarbeitung der Compilerbau-Vorlesung entstanden,<br />

die ich seit dem Sommersemester 1992, zunächst an der Universität Hildesheim, seit<br />

1998 an der Technischen Universität Braunschweig halte. Die erste Version der Vorlesung<br />

war stark durch die Compilerbau-Vorlesung Prof. Dr. Klaus Indermark, die<br />

ich in den 80’er Jahren an der RWTH Aachen als Mitarbeiterin betreuen durfte,<br />

beeinflusst. Sie basiert außerdem zu großen Teilen auf dem klassischen ” Drachenbuch“<br />

[Aho08] von Aho et. al. Sehr hilfreich bei der Weiterentwicklung der Vorlesung<br />

war das zwischenzeitlich erschienene Buch von Reinhard Wilhelm <strong>und</strong> Dieter Maurer<br />

[WM96].<br />

Darüber hinaus haben im Laufe der Jahre viele Beteiligte zur Ausarbeitung <strong>und</strong> Weiterentwicklung<br />

dieses Skripts beigetragen, denen ich an dieser Stelle danken möchte.<br />

Zunächst danke ich Dr. Michaela Huhn <strong>und</strong> Dr. Peter Niebert, die mich bei der Konzeption<br />

der Vorlesung in Hildesheim hervorragend unterstützt haben. Besonderer<br />

Dank gebührt meinen Mitautoren Dr. Thomas Gehrke <strong>und</strong> Malte Lochau, die dieses<br />

Skript mit hoher Selbstständigkeit bearbeitet haben. Dr. Werner Struckmann <strong>und</strong><br />

Tilo Mücke haben durch Ergänzungen <strong>und</strong> hilfreiche Hinweise beigetragen. Jochen<br />

Kamischke hat uns als studentische Hilfskraft sehr gut unterstützt. Die Studierenden,<br />

die diese Vorlesung in den vergangenen Jahren gehört haben, haben durch ihre aktive<br />

Teilnahme ebenfalls zur Weiterentwicklung des Skripts beigetragen; auch Ihnen<br />

gebührt mein herzlicher Dank.<br />

Braunschweig, den 5. März 2009<br />

Ursula Goltz<br />

i


Inhaltsverzeichnis<br />

Verzeichnis der Abbildungen v<br />

Verzeichnis der Tabellen vii<br />

Listings viii<br />

1 Einführung 1<br />

1.1 Inhalte <strong>und</strong> Gliederung . . . . . . . . . . . . . . . . . . . . . . . . . . 1<br />

1.2 Höhere Programmiersprachen . . . . . . . . . . . . . . . . . . . . . . 2<br />

1.3 Implementierung von Programmiersprachen . . . . . . . . . . . . . . 3<br />

1.3.1 Interpreter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3<br />

1.3.2 Compiler . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3<br />

1.3.3 Virtuelle Maschinen als Zielplattform . . . . . . . . . . . . . . 4<br />

1.4 Umgebung eines Compilers . . . . . . . . . . . . . . . . . . . . . . . . 5<br />

1.5 Aufbau eines Compilers . . . . . . . . . . . . . . . . . . . . . . . . . 7<br />

1.5.1 Analyse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7<br />

1.5.2 Synthese . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11<br />

1.5.3 Front-End, Back-End . . . . . . . . . . . . . . . . . . . . . . . 12<br />

1.5.4 Läufe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12<br />

2 Lexikalische Analyse 14<br />

2.1 Terminologie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14<br />

2.2 Reguläre Sprachen <strong>und</strong> endliche Automaten . . . . . . . . . . . . . . 15<br />

2.2.1 Reguläre Sprachen . . . . . . . . . . . . . . . . . . . . . . . . 16<br />

2.2.2 Reguläre Ausdrücke . . . . . . . . . . . . . . . . . . . . . . . . 16<br />

2.2.3 Endliche Automaten . . . . . . . . . . . . . . . . . . . . . . . 18<br />

2.2.4 Reguläre Definitionen . . . . . . . . . . . . . . . . . . . . . . . 27<br />

2.3 Sieber . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30<br />

2.4 Fehlerbehandlung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32<br />

3 Syntaktische Analyse 35<br />

3.1 Kontextfreie Grammatiken . . . . . . . . . . . . . . . . . . . . . . . . 35<br />

3.1.1 Kontextfreie Grammatiken . . . . . . . . . . . . . . . . . . . . 36<br />

3.1.2 Ableitungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38<br />

3.1.3 Strukturbäume . . . . . . . . . . . . . . . . . . . . . . . . . . 40<br />

iii


Inhaltsverzeichnis<br />

3.1.4 Mehrdeutige Grammatiken . . . . . . . . . . . . . . . . . . . . 42<br />

3.2 Konstruktion von Parsern . . . . . . . . . . . . . . . . . . . . . . . . 46<br />

3.2.1 Kellerautomat . . . . . . . . . . . . . . . . . . . . . . . . . . . 46<br />

3.3 Top-Down-Syntaxanalyse . . . . . . . . . . . . . . . . . . . . . . . . . 49<br />

3.3.1 LL(k)-Grammatiken . . . . . . . . . . . . . . . . . . . . . . . 51<br />

3.3.2 Transformierung von Grammatiken . . . . . . . . . . . . . . . 59<br />

3.3.3 Erweiterte kontextfreie Grammatiken . . . . . . . . . . . . . . 67<br />

3.3.4 Fehlerbehandlung bei der Top-Down-Analyse . . . . . . . . . . 75<br />

3.4 Bottom-Up-Syntaxanalyse . . . . . . . . . . . . . . . . . . . . . . . . 80<br />

3.4.1 LR(k)-Grammatiken . . . . . . . . . . . . . . . . . . . . . . . 84<br />

3.4.2 Fehlerbehandlung bei der Bottom-Up-Analyse . . . . . . . . . 113<br />

3.5 Parser Generatoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117<br />

4 Semantische Analyse 118<br />

4.1 Attributierte Grammatiken . . . . . . . . . . . . . . . . . . . . . . . . 119<br />

4.2 Typüberprüfung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 128<br />

4.2.1 Typsysteme . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129<br />

4.2.2 Gleichheit von Typausdrücken . . . . . . . . . . . . . . . . . . 135<br />

4.2.3 Typumwandlungen . . . . . . . . . . . . . . . . . . . . . . . . 137<br />

5 Zwischencode-Erzeugung 141<br />

5.1 Abstrakte Keller-Maschinen . . . . . . . . . . . . . . . . . . . . . . . 142<br />

5.1.1 Syntaxbäume . . . . . . . . . . . . . . . . . . . . . . . . . . . 143<br />

5.1.2 Zwischencode <strong>für</strong> die Keller-Maschine . . . . . . . . . . . . . . 147<br />

5.1.3 Befehle zur Steuerung des Kontrollflusses . . . . . . . . . . . . 147<br />

5.2 Drei-Adreß-Code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 151<br />

5.2.1 Übersetzung von Syntaxbäumen in Drei-Adreß-Code . . . . . 153<br />

5.2.2 Übersetzung in Drei-Adreß-Code unter Verwendung von attributierten<br />

Grammatiken . . . . . . . . . . . . . . . . . . . . . . 155<br />

5.3 Vergleich der beiden Arten von Zwischencode . . . . . . . . . . . . . 157<br />

Literaturverzeichnis 158<br />

iv


Verzeichnis der Abbildungen<br />

1.1 Umgebung eines Compilers. . . . . . . . . . . . . . . . . . . . . . . . 6<br />

1.2 Phasen eines Compilers. . . . . . . . . . . . . . . . . . . . . . . . . . 7<br />

1.3 Übersetzung einer Zuweisung. . . . . . . . . . . . . . . . . . . . . . . 9<br />

1.4 Parse-Baum der Zuweisung. . . . . . . . . . . . . . . . . . . . . . . . 10<br />

2.1 Interaktion zwischen Scanner <strong>und</strong> Parser. . . . . . . . . . . . . . . . . 14<br />

2.2 Beispiel eines Übergangsgraphen. . . . . . . . . . . . . . . . . . . . . 20<br />

2.3 Konstruktion eines NEA zu einem regulärem Ausdruck. . . . . . . . . 21<br />

2.4 Beispiel einer NEA-Konstruktion. . . . . . . . . . . . . . . . . . . . . 21<br />

2.5 Beispiel zur Potenzmengenkonstruktion. . . . . . . . . . . . . . . . . 24<br />

2.6 DEA mit minimaler Zustandsmenge. . . . . . . . . . . . . . . . . . . 26<br />

2.7 Beispiel zur Minimalisierung. . . . . . . . . . . . . . . . . . . . . . . . 26<br />

2.8 Übergangsgraphen <strong>für</strong> die Symbole des Beispiels. . . . . . . . . . . . 31<br />

2.9 Analyse eines Programmausschnitts. . . . . . . . . . . . . . . . . . . 33<br />

3.1 Interaktion zwischen Scanner, Parser <strong>und</strong> restlichem Front-End. . . . 35<br />

3.2 Beispiel eines Strukturbaums. . . . . . . . . . . . . . . . . . . . . . . 41<br />

3.3 Konstruktion eines Strukturbaums. . . . . . . . . . . . . . . . . . . . 42<br />

3.4 Verschiedene Strukturbäume zu einem Satz. . . . . . . . . . . . . . . 43<br />

3.5 Mögliche Strukturbäume des “dangling else”-Problems. . . . . . . . . 44<br />

3.6 Lösung des “dangling else”-Problems. . . . . . . . . . . . . . . . . . . 45<br />

3.7 Schema eines Kellerautomaten. . . . . . . . . . . . . . . . . . . . . . 47<br />

3.8 Konstruktion des Strukturbaums anhand der Ausgabe des Parsers. . . 52<br />

3.9 Fehlerhafte Konstruktion eines Strukturbaums <strong>für</strong> die Eingabe a. . . 56<br />

3.10 Transformation in rechtsrekursive Grammatik. . . . . . . . . . . . . . 61<br />

3.11 Beispiel einer regulären Ableitung. . . . . . . . . . . . . . . . . . . . 69<br />

3.12 Übergangsgraphen <strong>für</strong> arithmetische Ausdrücke. . . . . . . . . . . . . 71<br />

3.13 Übergangsgraphen <strong>für</strong> Pascal-Typen. . . . . . . . . . . . . . . . . . . 73<br />

3.14 Beispiel eines recursive descent-Parsers. . . . . . . . . . . . . . . . . . 74<br />

3.15 Aufrufgraph einer recursive descent-Syntaxanalyse. . . . . . . . . . . 75<br />

3.16 Erzeugung eines Strukturbaums <strong>für</strong> einen arithmetischen Ausdruck. . 82<br />

3.17 Beispiel eines charakteristischen endlichen Automaten . . . . . . . . . 90<br />

3.18 Beispiel eines LR-DEA. . . . . . . . . . . . . . . . . . . . . . . . . . . 94<br />

3.19 Struktur einer LR(1)-Parse-Tabelle. . . . . . . . . . . . . . . . . . . . 104<br />

v


Verzeichnis der Abbildungen<br />

vi<br />

3.20 Zustandsmenge des LR-DEA der Grammatik zur Beschreibung der<br />

C-Zuweisung aus Beispiel 43. . . . . . . . . . . . . . . . . . . . . . . 106<br />

3.21 Zustandsmenge des charakteristischen endlichen Automaten mit LR(1)-<br />

Items zur Grammatik aus Beispiel 43. . . . . . . . . . . . . . . . . . . 107<br />

3.22 Charakteristischer endlicher LR(1)-Automat zur Grammatik aus Beispiel<br />

43. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108<br />

3.23 Zustandsmenge des charakteristischen endlichen Automaten mit SLR(1)-<br />

Items <strong>für</strong> die Grammatik aus Beispiel 43. . . . . . . . . . . . . . . . . 110<br />

3.24 Zustandsmenge des charakteristischen endlichen Automaten mit LALR(1)-<br />

Items <strong>für</strong> die Grammatik aus Beispiel 43. . . . . . . . . . . . . . . . . 111<br />

3.25 Charakteristischer endlicher LR(1)-Automat zur Grammatik aus Beispiel<br />

43. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112<br />

4.1 Beispiel eines attributierten Strukturbaums. . . . . . . . . . . . . . . 119<br />

4.2 Synthetische <strong>und</strong> inherite Attribute. . . . . . . . . . . . . . . . . . . . 120<br />

4.3 Berechnung von Typinformationen im Strukturbaum. . . . . . . . . . 121<br />

4.4 Attributierter Strukturbaum zur Analyse einer Binärzahl. . . . . . . . 123<br />

4.5 Attributierter Strukturbaum zur Analyse einer Binärzahl mit inheritem<br />

Attribut. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 124<br />

4.6 Darstellung der direkten Abhängigkeiten zwischen Attributvorkommen.126<br />

4.7 Beispiel eines Abhängigkeitsgraphen. . . . . . . . . . . . . . . . . . . 127<br />

4.8 “Verklebter” Abhängigkeitsgraph. . . . . . . . . . . . . . . . . . . . . 127<br />

4.9 Semantische Regeln <strong>für</strong> Beispielsprache. . . . . . . . . . . . . . . . . . 133<br />

4.10 Attributierter Strukturbaum <strong>für</strong> ein Beispielprogramm. . . . . . . . . 133<br />

4.11 Attributierter Strukturbaum <strong>für</strong> ein fehlerhaftes Beispielprogramm. . 134<br />

4.12 Funktion zur Überprüfung, ob zwei Typausdrücke identisch sind. . . . 135<br />

4.13 Attributierter Strukturbaum einer Zuweisung. . . . . . . . . . . . . . 139<br />

4.14 Attributierter Strukturbaum einer Zuweisung mit Attributabhängigkeiten.<br />

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 139<br />

4.15 Attributierter Strukturbaum einer Zuweisung mit Typfehler. . . . . . 140<br />

5.1 Einordnung der Zwischencode-Erzeugung. . . . . . . . . . . . . . . . 141<br />

5.2 Syntaxbaum . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 144<br />

5.3 Konstruktion eines Syntaxbaums aus Postfix-Notation. . . . . . . . . 145<br />

5.4 Auswertung eines Postfix-Ausdrucks mit Hilfe eines Stacks. . . . . . . 146<br />

5.5 Attributierter Strukturbaum mit Zwischencode. . . . . . . . . . . . . 148<br />

5.6 Attributierter Strukturbaum einer verschachtelten if-Anweisung. . . . 152<br />

5.7 Syntaxbaum mit temporären Namen. . . . . . . . . . . . . . . . . . . 154<br />

5.8 Syntaxbaum mit Attributen <strong>für</strong> die Erzeugung von Drei-Adreß-Code. 156


Verzeichnis der Tabellen<br />

2.1 Beispiele <strong>für</strong> Symbole, Muster <strong>und</strong> Lexeme. . . . . . . . . . . . . . . 15<br />

2.2 Übergangsrelation ∆ in Tabellenform. . . . . . . . . . . . . . . . . . . 20<br />

2.3 Reguläre Ausdrücke <strong>und</strong> die dazugehörigen Symbole <strong>und</strong> Attributwerte. 29<br />

3.1 Beispielableitung eines Top-Down-Parsers. . . . . . . . . . . . . . . . 50<br />

3.2 Beispiel einer Parse-Tabelle. . . . . . . . . . . . . . . . . . . . . . . . 64<br />

3.3 Parse-Tabelle <strong>für</strong> dangling-else-Grammatik. . . . . . . . . . . . . . . . 67<br />

3.4 Für Fehlerbehandlung modifizierte Parse-Tabelle. . . . . . . . . . . . 77<br />

3.5 Beispiel einer Ableitung mit Fehlerbehandlung. . . . . . . . . . . . . 78<br />

3.6 Beispielableitung eines Bottom-Up-Parsers. . . . . . . . . . . . . . . . 83<br />

vii


Listings<br />

viii<br />

NEA nach DEA . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23<br />

DEA Minimalisierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25<br />

Berechnung von FIRST 1–Mengen . . . . . . . . . . . . . . . . . . . . . . 57<br />

Berechnung von FOLLOW 1–Mengen . . . . . . . . . . . . . . . . . . . . . 58<br />

Transformation einer linksrekursiven in eine rechtsrekursive Grammatik . . 60<br />

Linksfaktorisierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62<br />

Konstruktion der Parse–Tabelle zu einer Grammatik . . . . . . . . . . . . 63<br />

Deterministische Top–Down–Analyse mit Parse–Tabelle . . . . . . . . . . . 65<br />

Konstruktion der FIRST–<strong>und</strong> FOLLOW–Mengen einer ELL(1)–Grammatik 69<br />

Beispiel eines recursive descent–Parsers . . . . . . . . . . . . . . . . . . . . 74<br />

LR–DEA–Konstruktion . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92<br />

Algorithmus LR(1)–GEN . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100<br />

Konstruktion der LR(1)–action–Tabelle . . . . . . . . . . . . . . . . . . . . 104<br />

LR(1)–Parse–Algorithmus . . . . . . . . . . . . . . . . . . . . . . . . . . . 105<br />

Konstruktion der SLR(1)–action–Tabelle . . . . . . . . . . . . . . . . . . . 109<br />

Ueberpruefung ob zwei Typausdruecke identisch sind . . . . . . . . . . . . 135


1 Einführung<br />

1.1 Inhalte <strong>und</strong> Gliederung<br />

Die Techniken zur Konstruktion von Übersetzern (Compiler) als der altehrwürdigen<br />

Disziplin der Informatik sind unverändert allgegenwärtig. Wie kaum ein anderes<br />

Gebiet der Informatik werden beim Compilerbau Themen aus Theorie <strong>und</strong> Praxis<br />

miteinander verb<strong>und</strong>en. Auf der einen Seite bilden Automatentheorie <strong>und</strong> formale<br />

Sprachen das theoretische F<strong>und</strong>ament bei der Implementierung von Programmiersprachen.<br />

Zugleich gehören aber auch praktische Fragestellungen beim Entwurf <strong>und</strong><br />

der Entwicklung von Programmiersprachen <strong>für</strong> konkrete Aufgabenstellungen zu diesem<br />

Beschäftigungsfeld. Schließlich muss sich der Compilerbauer auch mit den Ressourcen,<br />

Befehlssätzen etc. unterschiedlichster Rechnerarchitekturen möglicher Zielplattformen<br />

auseinander setzen.<br />

Auch bei aktuellen Themen <strong>und</strong> Problemstellungen der Informatik kommt man an<br />

Techniken des Compilerbaus nicht vorbei. Dazu zählen Themen wie virtuelle Maschinen,<br />

Parallelisierung, Speicherlokalität <strong>und</strong> Programmanalyse <strong>und</strong> -optimierung.<br />

Das vorliegende Skript ist in zwei Teile gegliedert. Der erste Teil befasst sich mit den<br />

Teilen des Compilers, die als Front-End bezeichnet werden. Dazu zählen alle Phasen<br />

des Übersetzungsvorgangs einer Eingabesprache, die unabhängig von der Zielsprache<br />

<strong>und</strong> Ausführungsplattform <strong>für</strong> übersetzte Programme erfolgen. Dazu zählen insbesondere<br />

die Analysephasen zur Überprüfung der Korrektheit des Eingabeprogrammes<br />

sowie die im Rahmen der einzelnen Übersetzungsschritte erzeugten Zwischendarstellungen<br />

des Eingabeprogrammes.<br />

Nach dem einführenden Kapitel, das die Gr<strong>und</strong>begriffe des Übersetzerbaus einführt,<br />

folgen aufeinander aufbauend die ausführliche Einführung der Konzepte der lexikalischen,<br />

syntaktischen <strong>und</strong> (statischen) semantischen Analyse. Die Vorgehensweise<br />

in den einzelnen Phasen wird jeweils durch Beispiele veranschaulicht, so zum Beispiel<br />

die Typüberprüfung im Rahmen der semantischen Analyse. Eine systematische<br />

Einführung in die theoretischen Gr<strong>und</strong>lagen von Typsystemen erfolgt dann im zweiten<br />

Teil des Skripts. Als Abschluss des ersten Teils werden verschiedene Techniken<br />

der Zwischencode-Erzeugung beschrieben, die den vorbereitenden Schritt <strong>für</strong> die anschließende<br />

Code-Generierung durch das Back-End darstellen.<br />

Der zweite Teil des Skripts ist dem Back-End des Compilers gewidmet, also den<br />

Zielplattform-spezifischen Phasen, in denen die Synthese des Zielprogrammes aus<br />

dem Quellprogramm erfolgt. Der Schwerpunkt wird in diesem Teil auf der Überset-


1 Einführung<br />

zung objektorientierter Programmiersprachen liegen, deren statische <strong>und</strong> dynamische<br />

Eigenschaften im Detail untersucht werden. Als exemplarische Zielplattform werden<br />

abstrakte bzw. virtuelle Maschine betrachtet. Die theoretischen Gr<strong>und</strong>lagen werden<br />

dann ausführlich an einer konkreten Programmiersprache, der objektorientierten<br />

Sprache Java, verdeutlicht. Den Abschluss bilden Optimierungstechniken <strong>für</strong> Übersetzer,<br />

wobei sowohl allgemeine Ansätze, als auch speziell objektorientierte Ansätze<br />

beschrieben werden.<br />

1.2 Höhere Programmiersprachen<br />

In der Anfangszeit der Informatik wurde die <strong>Programmierung</strong> von Rechnern in der<br />

jeweiligen Maschinensprache des entsprechenden Rechners vorgenommen. Aufgr<strong>und</strong><br />

der Rechnerorientierung dieser Sprachen war der Entwurf sowie die nachfolgende<br />

Anpassung <strong>und</strong> Änderung des Programmcodes äußerst aufwendig <strong>und</strong> mit hohen<br />

Kosten verb<strong>und</strong>en. Speicherzellen mußten direkt über ihre jeweiligen Adressen angesprochen<br />

werden. Wurden nachträglich Änderungen am Programm vorgenommen,<br />

mußten diese Adressen “von Hand” angepaßt werden.<br />

Eine erste Verbesserung wurde durch die Einführung der Assemblersprachen erzielt.<br />

Den Befehlen der Maschinensprache wurden kurze Buchstabenfolgen, sogenannte<br />

Mnemonics 1 , zugeordnet, die die Lesbarkeit von Programmtexten erhöhen. Das<br />

Programm zur Generierung des zu einem Assemblertext gehörenden Maschinenprogramms,<br />

ebenfalls Assembler genannt, erlaubte eine rudimentäre Überprüfung des<br />

Programmtextes auf Fehler. Außerdem wurde durch die Möglichkeit zur Vergabe<br />

symbolischer Adressen (labels) die Pflege von Programmen erleichtert.<br />

Trotz der Überlegenheit der Assemblersprachen gegenüber den Maschinensprachen<br />

besitzt die Programmerstellung mittels Assembler eine Reihe bedeutender Nachteile.<br />

Durch die direkte Zuordnung von Mnemonics zu Maschinenbefehlen ist der Abstraktionsgrad<br />

von Programmen gering, so daß sich die <strong>Programmierung</strong> immer noch an<br />

der Maschine <strong>und</strong> nicht am konkreten Problem orientieren muß. Assemblerprogramme<br />

sind aufgr<strong>und</strong> ihres Mangels an Strukturelementen nur schwer verständlich, was<br />

eine erschwerte Wartbarkeit von Programmen zur Folge hat. Durch den Mangel an<br />

Datenstrukturen ist die Handhabung der Daten eines Programms aufwendig. Außerdem<br />

sind Assemblerprogramme nur auf einem Maschinentyp einsetzbar, so daß<br />

Portierungen von Programmen auf andere Maschinentypen mit anderer Maschinensprache<br />

i. allg. nicht möglich sind.<br />

Um die Nachteile der Assemblersprachen zu vermeiden <strong>und</strong> um eine problembezogene<br />

<strong>Programmierung</strong> zu unterstützen, wurden die höheren Programmiersprachen eingeführt.<br />

Diese Sprachen abstrahieren von den Eigenschaften der verwendeten Rechner.<br />

Kontrollstrukturen wie z.B. Schleifen <strong>und</strong> Rekursion erlauben eine Steuerung<br />

2<br />

1 Mnemonik ist die Kunst, das Gedächtnis durch Hilfsmittel zu unterstützen.


1.3 Implementierung von Programmiersprachen<br />

des Programmflusses ohne die Verwendung von Sprungbefehlen. Das Konzept der<br />

Variablen <strong>und</strong> der Datentypen entlastet den Programmierer von der aufwendigen<br />

Speicherverwaltung. Durch den Abstraktionsgrad wird zudem die Portierung von<br />

Programmen auf andere Rechnerarchitekturen erleichtert.<br />

1.3 Implementierung von Programmiersprachen<br />

Um Programme einer höheren Programmiersprache auf einem Rechner ausführen zu<br />

können, muß diese Sprache auf diesem Rechner verfügbar gemacht (implementiert)<br />

werden. Die dazu existierenden Konzepte werden in zwei Klassen eingeteilt.<br />

1.3.1 Interpreter<br />

Ein Interpreter IL zur einer Programmiersprache L ist ein Programm, das als Eingabe<br />

ein Programm pL der Sprache L <strong>und</strong> eine Eingabefolge e erhält <strong>und</strong> eine Ausgabefolge<br />

a errechnet. Da bei der Interpretation von pL auch Fehler auftreten können, läßt sich<br />

die Funktionalität des Interpreters darstellen als<br />

IL : L × D ∗ → D ∗ ∪ {error},<br />

wenn sowohl Eingabe- wie auch Ausgabedaten aus einem gemeinsamen Bereich D<br />

stammen. Die Ausführung des Programms pL mit Eingabefolge e <strong>und</strong> Ausgabefolge<br />

a ist durch die Gleichung<br />

IL(pL,e) = a<br />

beschrieben.<br />

Die Arbeitsweise eines Interpreters ist gekennzeichnet durch eine gleichzeitige Bearbeitung<br />

des Programms pL <strong>und</strong> der Eingabe e. Dies führt dazu, daß der Interpreter<br />

bei jeder, also auch bei wiederholter, Ausführung eines Programmkonstrukts zuvor<br />

das Konstrukt analysieren muß. Daher kann der Interpreter auch keine globalen Informationen,<br />

etwa zur Optimierung der Speicherverwaltung, über pL verwenden.<br />

1.3.2 Compiler<br />

Um die aus der lokalen Sicht eines Interpreters auf das auszuführende Programm resultierenden<br />

Ineffizienzen zu vermeiden, werden beim Compiler die Verarbeitung des<br />

Programms <strong>und</strong> der Eingabe nacheinander durchgeführt. Zuerst wird das Programm<br />

pL ohne die Berücksichtigung von Eingabedaten analysiert <strong>und</strong> in eine andere Form<br />

überführt. Diese erlaubt die effizientere Ausführung des Programms mit beliebigen<br />

Eingabefolgen, ohne daß die Analyse <strong>und</strong> die Überführung des Programms wiederholt<br />

werden müssen.<br />

3


1 Einführung<br />

Wir nennen unsere zu übersetzende Sprache L im folgenden Quellsprache. Die Übersetzung<br />

besteht in der Überführung eines Programms pL der Quellsprache in ein<br />

Programm pM, wobei M die Maschinen- oder die Assemblersprache eines konkreten<br />

oder abstrakten Rechners ist. Wir nennen pM Zielprogramm <strong>und</strong> folglich M die<br />

Zielsprache des Übersetzers.<br />

Nach der Übersetzung wird das erzeugte Programm pM zur Laufzeit mit der Eingabefolge<br />

e ausgeführt. Dabei gehen wir davon aus, daß pM mit der Eingabe e die<br />

Ausgabe a erzeugt, wenn ein Interpreter <strong>für</strong> L mit IL(pL,e) = a dieselbe Ausgabe<br />

erzeugt. Wenn wir die Maschine, deren Maschinensprache unsere Zielsprache M ist,<br />

als Interpreter IM <strong>für</strong> M auffassen, so muß gelten:<br />

wenn IL(pL,e) = a, dann IM(pM,e) = a.<br />

Neben der Generierung des Zielprogramms wird während der Übersetzung eines Programms<br />

eine umfassende Überprüfung auf Fehler des zu analysierenden Programmtextes<br />

vorgenommen. Durch diese globale Analyse können manche Fehler bereits vor<br />

der Ausführung des Programms gef<strong>und</strong>en werden, während dies beim Interpreter aufgr<strong>und</strong><br />

seiner lokalen Sicht auf den Programmtext erst zur Laufzeit geschehen kann.<br />

Der Compiler erkennt nur Fehler eines Programmes, die von der konkreten Eingabe<br />

während eines Programmlaufs unabhängig sind. Hierzu gehören neben syntaktischen<br />

Fehlern auch semantische Fehler, z.B. Zugriffe auf nichtdeklarierte Variablen. Fehler,<br />

die aus der Eingabe e resultieren, können auch beim Compiler erst zur Laufzeit des<br />

Zielprogramms pM entdeckt werden.<br />

1.3.3 Virtuelle Maschinen als Zielplattform<br />

Bei der klassischen Implementierung einer Programmiersprache auf einer Zielplattform<br />

sind wir bisher sowohl bei der Verwendung eines Interpreters als auch eines<br />

Compilers von der Maschinensprache des realen Rechners als Zielsprache ausgegangen.<br />

Dieser Ansatz hat mehrere Nachteile:<br />

• Der Compiler-Entwickler muss <strong>für</strong> eine Vielzahl unterschiedlicher Instruktionssätze<br />

von Prozessoren entsprechende Back-Ends (siehe unten) zur Verfügung<br />

stellen.<br />

• Der Compiler-Entwickler möchte <strong>für</strong> eine effiziente Code-Erzeugung auf Befehle<br />

zurückgreifen, die eventuell nicht im Instruktionssatz enthalten sind. Das<br />

gilt insbesondere <strong>für</strong> die Umsetzung spezieller Konstrukte der Quellsprache,<br />

beispielsweise zur Behandlung von Ausnahmen.<br />

• Insbesondere möchte der Entwickler <strong>für</strong> die Übersetzung von Programmen entwickelte<br />

Ansätze trotz sich ändernder Instruktionssätze <strong>und</strong> Rechnerarchitekturen<br />

gleich bleibende Rahmenbedingungen vorfinden.<br />

4


1.4 Umgebung eines Compilers<br />

Aus diesen Gründen wird bei der Implementierung neuerer Programmiersprachen<br />

zunehmend dazu übergegangen, den Instruktionssatz einer idealisierten abstrakten<br />

oder virtuellen Maschine (VM) als Zielsprache des Compilers einzuführen. Die so<br />

definierte zusätzliche Abstraktionsschicht auf der Zielplattform hat mehrere Vorteile:<br />

• Die Implementierung der virtuellen Maschine <strong>für</strong> eine spezifische Plattform<br />

kann unabhängig von der Entwicklung des Compilers vorgenommen werden,<br />

solange beides mit Hinblick auf einen festen Instruktionssatz der VM erfolgt.<br />

• Für die VM erzeugte Code ist auf jedem System lauffähig, auf dem die VM<br />

implementiert ist (Portierbarkeit).<br />

• Der Befehlssatz der VM kann passend zu Paradigmen <strong>und</strong> Konstrukten der<br />

Quellsprache gewählt werden <strong>und</strong> erlaubt so eine effiziente Übersetzung der<br />

Quellprogramme. Beispiel: Der Java Bytecode (JBC) als Instruktionssatz der<br />

Java Virtual Machine (JVM) beinhaltet spezielle Befehle zum Umgang mit<br />

Objekten, da die Quellsprache Java objektorientiert ist.<br />

• Der durch den Compiler erzeugte Code wird nicht direkt auf dem Zielsystem<br />

ausgeführt, sondern auf einer Zwischenschicht, wodurch eine sicherere Ausführung<br />

gewährleistet werden kann (Sand-Boxing).<br />

Somit kann der Einsatz einer virtuellen Maschine <strong>für</strong> die Implementierung einer Programmiersprache<br />

als Kombination beider zuvor vorgestellten Ansätze gesehen werden:<br />

1. Die Übersetzung des Quellprogramms in ein Programm <strong>für</strong> die virtuelle Maschine<br />

erfolgt durch einen Compiler.<br />

2. Die virtuelle Maschine ist ein Interpreter <strong>für</strong> das durch den Compiler erzeugte<br />

Zielprogramm.<br />

In zweiten Teil der Vorlesung werden wir uns ausführlich mit den Konzepten virtueller<br />

Maschinen beschäftigen.<br />

1.4 Umgebung eines Compilers<br />

Zur Umgebung eines Compilers gehören i. allg. weitere Programme, die <strong>für</strong> die Übersetzung<br />

<strong>und</strong> die Ablauffähigkeit eines Programms benötigt werden (Abbildung 1.1<br />

[ASU99]).<br />

Am Beginn des Übersetzungsprozesses steht ein “rohes” Quellprogramm. Dieses<br />

Quellprogramm enthält neben dem eigentlichen Programm zusätzliche Meta-Anweisungen,<br />

die beschreiben, wie das Quellprogramm vor der Übersetzung mit dem Compiler<br />

modifiziert werden soll. Dabei kann es sich z.B. um die Definition von Makros<br />

(z.B. #define in C [KR90]), um die Generierung zusätzlicher Befehle zur Fehlersuche<br />

oder um das Einfügen weiterer Quelltexte (z.B. \include in L ATEX [Kop02]) handeln.<br />

Diese Modifikationen des Quelltextes werden von einem sogenannten Präprozessor<br />

5


1 Einführung<br />

"rohes" Quellprogramm<br />

Präprozessor<br />

Quellprogramm<br />

Compiler<br />

Assemblerprogramm<br />

Assembler<br />

relokatibler Maschinencode<br />

Lader / Binder<br />

ausführbarer Maschinencode<br />

Abbildung 1.1: Umgebung eines Compilers.<br />

vorgenommen.<br />

Nach der Behandlung des Quelltextes durch den Präprozessor kann der Compiler das<br />

Zielprogramm erzeugen. Wie zuvor erwähnt, handelt es sich bei dem Zielprogramm<br />

entweder um ein Maschinenspracheprogramm bzw. einen Assemblertext. Im zweiten<br />

Fall muß der Assemblertext nun in einem zusätzlichen Schritt durch den Assembler<br />

in Maschinencode übersetzt werden. Oft wird heutzutage vom Compiler statt des<br />

Assemblertextes ein C-Programm erzeugt, welches vom C-Compiler weiterverarbeitet<br />

wird.<br />

Der vom Compiler erzeugte Maschinencode ist i. allg. noch nicht ausführbar, da es<br />

sich um sogenannten relokatiblen Code handelt. In diesem Code sind die Sprungadressen<br />

noch nicht festgelegt, so daß der Code im Speicher frei verschiebbar ist.<br />

Außerdem müssen die Bibliotheken des jeweiligen Übersetzers noch zum erzeugten<br />

Maschinencode hinzugefügt werden. Diese Bibliotheken enthalten z.B. die Ein- <strong>und</strong><br />

Ausgaberoutinen der Programme (z.B. das Modul InOut in Modula-2 [Wir97]) sowie<br />

weitere Routinen, die zur Laufzeit eines Programmes benötigt werden. Es gibt zwei<br />

Verfahren zur Einbindung der Bibliotheken in ein Programm. Der Binder faßt den relokatiblen<br />

Maschinencode <strong>und</strong> den Code der Bibliotheken zu einem neuen Programm<br />

zusammen <strong>und</strong> ersetzt dabei die abstrakten Programmadressen des relokatiblen Codes<br />

durch die statischen Adressen der Unterprogramme der Bibliotheken. Dieses erzeugte<br />

Programm ist ohne die Unterstützung weiterer Programme ausführbar. Der<br />

6


Symboltabellenverwaltung<br />

Quellprogramm<br />

lexikalische<br />

Analyse<br />

syntaktische<br />

Analyse<br />

semantische<br />

Analyse<br />

Zwischencodeerzeugung<br />

Codeoptimierung<br />

Codeerzeugung<br />

Zielprogramm<br />

1.5 Aufbau eines Compilers<br />

Analyse<br />

Fehlerbehandlung<br />

Synthese<br />

Abbildung 1.2: Phasen eines Compilers.<br />

Lader lädt hingegen den relokatiblen Code <strong>und</strong> den Code der benötigten Bibliotheken<br />

in den Hauptspeicher <strong>und</strong> ersetzt die abstrakten Adressen dort dynamisch. Aus<br />

diesem Gr<strong>und</strong> muß der Lader bei jedem Aufruf des Zielprogramms verwendet werden.<br />

1.5 Aufbau eines Compilers<br />

Die Aufgabe eines Compilers läßt sich zunächst in zwei gr<strong>und</strong>legende Teilaufgaben<br />

zerlegen (Abbildung 1.2): die Analyse des Quellprogramms <strong>und</strong> die Synthese des<br />

Zielprogramms. Beide Aufgaben werden in einer Reihe von Phasen bearbeitet.<br />

1.5.1 Analyse<br />

In den Analysephasen wird das Quellprogramm in seine Bestandteile zerlegt. Dabei<br />

wird eine Überprüfung auf statische (also von der konkreten Eingabe eines Programmablaufs<br />

unabhängige) Korrektheit des zu analysierenden Programmtextes vorgenommen.<br />

Enthält das Programm erkennbare Fehler, werden entsprechende Fehler-<br />

7


1 Einführung<br />

meldungen an den Benutzer ausgegeben. Weiterhin wird eine Zwischendarstellung<br />

des Programms erzeugt, die nur noch die <strong>für</strong> die Synthesephasen benötigten Informationen<br />

des Programmtextes enthält.<br />

Im folgenden erläutern wir die Analysephasen aus Abbildung 1.2 [ASU99] an der<br />

Übersetzung der Zuweisung<br />

position := initial + rate ∗ 60.<br />

Dabei nehmen wir an, daß die Variablen position, initial <strong>und</strong> rate als Fließkomma-<br />

Variablen deklariert sind.<br />

Lexikalische Analyse (scanning): Die lexikalische Analyse dient der Zerlegung des<br />

Zeichenstroms der Eingabe in Symbole. Die Zuweisung wird dabei in folgende Symbole<br />

zerlegt:<br />

1. Bezeichner (position)<br />

2. Zuweisungssymbol<br />

3. Bezeichner (initial)<br />

4. Additionssymbol<br />

5. Bezeichner (rate)<br />

6. Multiplikationssymbol<br />

7. Konstante (60)<br />

Wird als Symbol ein Bezeichner erkannt, wird dieser Bezeichner in die Symboltabelle<br />

des Compilers eingetragen. Jedem Bezeichner wird eine eindeutige Nummer<br />

zugewiesen, in unserem Beispiel der Einfachheit halber gemäß der Reihenfolge des<br />

Auftretens im Quellprogramm. An die nachfolgenden Phasen wird nicht mehr der<br />

Bezeichner selbst, sondern die ihm zugeordnete Nummer weitergegeben (in Abbildung<br />

1.3 [ASU99] ist während der lexikalischen Analyse position durch id1 ersetzt<br />

worden, initial durch id2 <strong>und</strong> rate durch id3).<br />

Das Teilprogramm, das die lexikalische Analyse des Quelltextes durchführt, wird<br />

Scanner genannt.<br />

Syntaktische Analyse (parsing): In der syntaktischen Analyse werden Gruppen<br />

von Symbolen mit hierarchischer Struktur erkannt. Die Quellsprache wird durch die<br />

Regeln einer kontextfreien Grammatik definiert. Anhand der Produktionen dieser<br />

Grammatik wird die von der lexikalischen Analyse gelieferte Symbolfolge auf Korrektheit<br />

überprüft. Dabei wird ein sogenannter Strukturbaum (parse-tree) erzeugt,<br />

der die Analyse des Programmtextes gemäß den Regeln der Grammatik darstellt.<br />

Für die Analyse der Zuweisung nehmen wir die folgende kontextfreie Grammatik an<br />

(kursiv gedruckte Wörter sind Nichtterminalsymbole):<br />

8


id1<br />

id1<br />

position := initial + rate * 60<br />

lexikalische Analyse<br />

id1 := id2 + id3 ∗ 60<br />

syntaktische Analyse<br />

:=<br />

✟ ❛<br />

✟<br />

❛❛<br />

✘✘<br />

+<br />

✘ ����<br />

id2<br />

✦✦*<br />

❳❳❳ ✦ id3<br />

semantische Analyse<br />

:=<br />

60<br />

✟✟✟ ❳❳❳ +<br />

✟ ❳❳❳❳<br />

✟<br />

✟<br />

✟ *<br />

id2<br />

❛❛❛❛<br />

✟<br />

✟<br />

id3<br />

inttoreal<br />

Zwischencode-Erzeugung<br />

temp1 := inttoreal(60)<br />

temp2 := id3 * temp1<br />

temp3 := id2 + temp2<br />

id1 := temp3<br />

Code-Optimierung<br />

temp1 := id3 * 60.0<br />

id1 := id2 + temp1<br />

Code-Generierung<br />

MOVF id3, R2<br />

MULF #60.0, R2<br />

MOVF id2, R1<br />

ADDF R2, R1<br />

MOVF R1, id1<br />

60<br />

1<br />

2<br />

3<br />

1.5 Aufbau eines Compilers<br />

Symboltabelle<br />

position<br />

initial<br />

rate<br />

Abbildung 1.3: Übersetzung einer Zuweisung.<br />

...<br />

...<br />

...<br />

9


1 Einführung<br />

✧<br />

✧ ▲ ❵❵❵❵❵❵❵❵❵❵<br />

▲<br />

✏✏❵❵❵❵❵❵❵❵<br />

✏✏<br />

✏<br />

✟<br />

✟<br />

✟ ❆ Zuweisung<br />

Bezeichner<br />

position<br />

:= Ausdruck<br />

❚<br />

❚<br />

Ausdruck + Ausdruck<br />

❳❳❳❳❳❳ ❆<br />

Bezeichner Ausdruck * Ausdruck<br />

initial<br />

Bezeichner Zahl<br />

rate<br />

Abbildung 1.4: Parse-Baum der Zuweisung.<br />

Zuweisung → Bezeichner := Ausdruck<br />

Ausdruck → Bezeichner | Zahl |<br />

Ausdruck + Ausdruck |<br />

Ausdruck * Ausdruck<br />

Der Strukturbaum der Zuweisungsanweisung gemäß dieser Grammatik ist in Abbildung<br />

1.4 dargestellt.<br />

Der Strukturbaum enthält neben den Terminalsymbolen der Eingabe auch die Nichtterminalsymbole<br />

der Grammatik, die bei der Ableitung der Eingabe verwendet wurden.<br />

Diese Nichtterminalsymbole werden in den weiteren Phasen des Compilers nicht<br />

mehr benötigt. Daher wird als Endprodukt der syntaktischen Analyse ein Syntaxbaum<br />

erzeugt, wie er in Abbildung 1.3 dargestellt ist 2 .<br />

Das Teilprogramm zur syntaktischen Analyse heißt Parser.<br />

Semantische Analyse: Nach der Überprüfung auf syntaktische Korrektheit des<br />

Programms wird in der semantischen Analyse die “statische” Semantik des Quellprogramms<br />

analysiert. “Statisch” bedeutet in diesem Zusammenhang, daß die semantischen<br />

Merkmale untersucht werden, die nicht von den Eingabedaten abhängig<br />

<strong>und</strong> daher <strong>für</strong> alle dynamischen Ausführungen gleich sind. Zur semantischen Analyse<br />

gehören die Überprüfung auf korrekte Typisierung, die Einhaltung von Gültigkeitsbereichen<br />

<strong>und</strong> eventuelle Typanpassungen. Während der semantischen Analyse werden<br />

die Bezeichner in der Symboltabelle mit Attributen versehen. Hierzu gehören z.B. der<br />

Variablentyp <strong>und</strong> der Gültigkeitsbereich der Variablen.<br />

Im Beispiel in Abbildung 1.3 hatten wir angenommen, daß die drei Bezeichner Varia-<br />

2 Im Gegensatz zu unserer Terminologie werden die Begriffe Syntaxbaum <strong>und</strong> Strukturbaum in<br />

10<br />

[WM96] synonym verwendet.<br />

60


1.5 Aufbau eines Compilers<br />

blen vom Typ REAL darstellen. Bei der Typüberprüfung des Programms wird in der<br />

semantischen Analyse festgestellt, daß die ganze Zahl 60 mit dem Inhalt einer REAL-<br />

Variablen multipliziert werden soll. Daher wird in den Syntaxbaum die Information<br />

eingefügt, daß vor der Multiplikation eine Typumwandlung der Zahl vorgenommen<br />

werden muß.<br />

1.5.2 Synthese<br />

In den Synthesephasen wird das zum Quellprogramm gehörende Zielprogramm erzeugt.<br />

Dabei werden die Informationen, die in den Analysephasen über den Programmtext<br />

gesammelt wurden, verwendet.<br />

Zwischencode-Erzeugung: Vor der Erzeugung des eigentlichen Zielprogramms wird<br />

oft eine Zwischendarstellung des Programms generiert, die einerseits bereits maschinennah,<br />

andererseits noch an keiner konkreten Zielmaschine orientiert ist. Diese Zwischensprache<br />

wird Zwischencode genannt.<br />

In Abbildung 1.3 wird als Zwischencode ein Drei-Adreß-Code erzeugt. Jeder Befehl<br />

dieses Codes darf maximal drei Adressen verwenden. Zwei Adressen geben an, wo<br />

sich die Operanden des Befehls befinden. Die dritte Adresse bezeichnet den Speicherplatz,<br />

an dem das Ergebnis des Befehls abgelegt werden soll. Die Speicherzellen an<br />

den Adressen id1, id2 <strong>und</strong> id3 enthalten die Werte der zugehörigen Variablen. Die<br />

Adressen temp1, temp2 <strong>und</strong> temp3 bezeichnen temporäre Speicherplätze <strong>für</strong> Zwischenergebnisse.<br />

Code-Optimierung: Die Verwendung eines maschinenunabhängigen Zwischencodes<br />

bietet den Vorteil, daß auf dem erzeugten Zwischencode eine ebenfalls maschinenunabhängige<br />

Code-Optimierung vorgenommen werden kann. Bei dieser Optimierung<br />

wird der Zwischencode auf Red<strong>und</strong>anzen hin untersucht <strong>und</strong> in bezug auf Laufzeit<br />

<strong>und</strong> Speicherplatzverbrauch verbessert.<br />

Im Beispiel wird in der Code-Optimierung erkannt, daß statt der Umwandlung einer<br />

ganzen Zahl in eine REAL-Zahl gleich die entsprechende Fließkommakonstante im<br />

Code verwendet werden kann. Hierdurch entfallen ein temporärer Speicherplatz <strong>und</strong><br />

eine Konvertierungs-Operation. Außerdem kann das Ergebnis des Additionsbefehls<br />

direkt in id1 gespeichert werden, so daß die letzte Zuweisung entfällt.<br />

Code-Generierung: In dieser letzten Compilerphase wird das Zielprogramm <strong>für</strong><br />

die Zielmaschine erzeugt. Dabei wird jeder Befehl des optimierten Zwischencodes in<br />

eine kurze Sequenz von Maschinenbefehlen übersetzt. Nach Möglichkeit werden die<br />

Speicherplätze des Zwischencodes durch Register der konkreten Maschine ersetzt, um<br />

zeitaufwendige Zugriffe auf den Hauptspeicher zu vermeiden.<br />

11


1 Einführung<br />

Eventuell schließt sich an die Phase der Code-Generierung noch eine maschinenabhängige<br />

Code-Optimierung an, die Ineffizienzen im erzeugten Maschinencode beseitigt<br />

(z.B. überflüssige Kopierbefehle entfernt oder einzelne Maschinenbefehle durch<br />

effizientere Befehle mit derselben Wirkung ersetzt).<br />

1.5.3 Front-End, Back-End<br />

Bei der Einteilung des Compilers in Phasen werden häufig die Begriffe Front-End<br />

<strong>und</strong> Back-End verwendet. Das Front-End eines Compilers umfaßt alle zielsprachenunabhängigen<br />

Compilerphasen, das Back-End entsprechend alle quellsprachenunabhängigen<br />

Phasen des Compilers.<br />

Für die Portierung eines Compilers auf eine andere Zielsprache kann i. allg. das Front-<br />

End unverändert weiterverwendet werden, so daß nur das entsprechende Back-End<br />

neu implementiert werden muß. Im umgekehrten Fall kann die Verbindung mehrerer<br />

Front-Ends mit einem gemeinsamen Back-End sinnvoll sein, um innerhalb eines Programms<br />

Teilprogramme in einer jeweils <strong>für</strong> das Teilproblem optimalen Programmiersprache<br />

zu schreiben <strong>und</strong> aus diesen Teilprogrammen ein gemeinsames Zielprogramm<br />

zu erzeugen.<br />

1.5.4 Läufe<br />

Es ist üblich, mehrere Übersetzungsphasen in einem einzelnen Lauf (pass) zu implementieren.<br />

Ein Lauf steht dabei <strong>für</strong> einen Durchlauf durch eine Darstellung des<br />

Programms. Dabei kann es sich sowohl um den Quelltext als auch um eine interne<br />

Darstellung des Programms wie z.B. den Syntaxbaum handeln. Dabei bietet es<br />

sich an, Phasen, deren Arbeitsschritte eng miteinander verzahnt sind, in einem Lauf<br />

zusammenzufassen. Eine Möglichkeit wäre zum Beispiel die Integration von lexikalischer<br />

<strong>und</strong> syntaktischer Analyse in einem Lauf sowie der semantischen Analyse <strong>und</strong><br />

der Codegenerierung in einem zweiten Lauf.<br />

Einen Extremfall stellt der Ein-Pass-Compiler dar, der die Analyse des Quellprogramms<br />

<strong>und</strong> die Synthese des Zielprogramms während eines einzigen Durchlaufs<br />

durch den Programmtext durchführt. In diesem Fall muß gewährleistet sein, daß<br />

jeder Bezeichner vor seiner Verwendung deklariert wurde, da nachträgliche Änderungen<br />

am Zielprogramm nicht mehr möglich sind. Aus diesem Gr<strong>und</strong> ist in vielen<br />

Compilern <strong>für</strong> die Sprache Pascal [JW91] die Vordeklaration von Bezeichnern mit<br />

der forward-Anweisung vorgesehen.<br />

Andere Programmiersprachen wie z.B. Algol-68 [OT97] erlauben die Verwendung von<br />

Bezeichnern vor ihrer Deklaration, so daß <strong>für</strong> diese Sprachen die Implementierung<br />

mittels eines Ein-Pass-Compilers nicht möglich ist.<br />

Bei der Implementierung einer Sprache mittels einer virtuellen Maschine gilt: Der<br />

12


1.5 Aufbau eines Compilers<br />

vom Compiler erzeugte Code ähnelt dem idealisierten Zwischencode, der durch die<br />

virtuelle Maschine interpretiert wird, also auf Instruktionen der realen Hardware<br />

zum Zeit der Ausführung abgebildet wird. Somit kann die virtuelle Maschine auch<br />

als Middle-End bezeichnet werden. Das bedeutet in der Regel aber nicht, dass bei<br />

dieser Variante die Phase der Zwischencode-Erzeugung entfällt. Vielmehr existiert im<br />

Allgemeinen eine weitere Zwischendarstellung des Programms zwischen semantischer<br />

Analyse <strong>und</strong> dem Code <strong>für</strong> die virtuelle Maschine, auf deren Gr<strong>und</strong>lage Optimierungen<br />

unabhängig von der VM durchgeführt werden können.<br />

13


2 Lexikalische Analyse<br />

Die lexikalische Analyse arbeitet als erste Phase des Compilers direkt mit dem zu<br />

übersetzenden Programmtext (siehe Abbildung 2.1).<br />

Der Programmteil zur Durchführung der lexikalischen Analyse wird Scanner genannt.<br />

Der Scanner erfüllt die folgenden Aufgaben:<br />

• Das Quellprogramm wird zeichenweise gelesen <strong>und</strong> dabei in Symbole zerlegt.<br />

Bei dieser Zerlegung werden Leerzeichen, Kommentare, Zeilenenden etc. entfernt,<br />

so daß sie in den weiteren Compilerphasen nicht mehr beachtet werden<br />

müssen.<br />

• Die Bezeichner des Programms werden in der Reihenfolge ihres Auftretens im<br />

Quelltext mit erläuternden Informationen in die Symboltabelle eingefügt.<br />

• Für die eventuelle Ausgabe von Fehlermeldungen werden Informationen gesammelt<br />

(z.B. Zeilennummern).<br />

Ein wichtiger Gesichtspunkt bei der Realisierung eines Scanners ist Effizienz, da die<br />

nachfolgenden Phasen des Compilers direkt vom Scanner abhängig sind <strong>und</strong> deren<br />

Laufzeit daher durch einen langsamen Scanner negativ beeinflußt wird. Meist wird<br />

der Scanner als Unterprogramm des Parsers realisiert (eventuell als Coroutine). Der<br />

Scanner liefert jeweils nach Aufforderung durch den Parser ein Symbol.<br />

2.1 Terminologie<br />

In diesem Abschnitt führen wir die Begriffe Symbol, Muster <strong>und</strong> Lexem ein.<br />

Quellprogramm<br />

Symbol<br />

Scanner Parser<br />

nächstes Symbol<br />

anfordern<br />

Symboltabelle<br />

Abbildung 2.1: Interaktion zwischen Scanner <strong>und</strong> Parser.


2.2 Reguläre Sprachen <strong>und</strong> endliche Automaten<br />

Symbole sind die vom Scanner an den Parser zu liefernden Gr<strong>und</strong>einheiten der Programmiersprache.<br />

Mengen von gleichartigen Symbolen nennen wir Symbolklassen.<br />

Typische Symbolklassen sind die Menge der Integer-Konstanten <strong>und</strong> die Menge der<br />

Zeichenketten.<br />

Muster beschreiben die möglichen Auftreten eines Symbols im Quellprogramm. Die<br />

Zeichenfolgen im Programmtext, die Symbolen entsprechen, nennen wir Lexeme.<br />

Beispiele <strong>für</strong> Symbole <strong>und</strong> die zugehörigen Muster <strong>und</strong> Lexeme sind in Tabelle 2.1<br />

angeführt.<br />

Symbol Musterbeschreibung mögliche Lexeme<br />

if if if<br />

id Buchstabe, gefolgt von pi, D2<br />

Buchstaben oder Ziffern<br />

Tabelle 2.1: Beispiele <strong>für</strong> Symbole, Muster <strong>und</strong> Lexeme.<br />

2.2 Reguläre Sprachen <strong>und</strong> endliche Automaten<br />

Gr<strong>und</strong>lage <strong>für</strong> die lexikalische Analyse ist die Theorie der regulären Sprachen.<br />

Wir wiederholen zunächst einige wichtige Gr<strong>und</strong>begriffe der formalen Sprachen.<br />

Ein Alphabet Σ ist eine endliche Menge von Zeichen; z.B. {0, 1},<br />

{0,..., 9,A,...,Z} oder der ASCII-Zeichensatz.<br />

Ein Wort über einem Alphabet Σ ist eine endliche Folge von Zeichen aus dem Alphabet;<br />

z.B. 01001, A195, goltz@ips.cs.tu-bs.de. Das leere Wort bezeichnen wir mit<br />

ε. Die Menge aller Wörter über einem Alphabet Σ bezeichnen wir mit Σ ∗ .<br />

Eine Sprache über einem Alphabet ist eine Menge von Wörtern über dem Alphabet,<br />

z.B. ∅, {ε}, {A,B,C,AB,AC,ABC} sowie die Menge aller syntaktisch wohlgeformten<br />

Modula 2-Programme.<br />

Seien v <strong>und</strong> w Wörter über dem Alphabet Σ. Die Konkatenation von v <strong>und</strong> w,<br />

geschrieben vw, ist dasjenige Wort, das durch das Anhängen von w an v ensteht. Für<br />

v = compiler <strong>und</strong> w = bau ergibt sich als Konkatenation vw das Wort compilerbau.<br />

Es gilt εw = wε = w <strong>für</strong> beliebige Wörter w.<br />

Die Exponentiation von Wörtern ist wie folgt definiert:<br />

• w 0 = ε<br />

• w i = w i−1 w <strong>für</strong> i > 0. Es gilt w 1 = w.<br />

Operationen auf Sprachen: Seien L,M Sprachen. Dann sind die folgenden Operationen<br />

definiert:<br />

15


2 Lexikalische Analyse<br />

Vereinigung: L ∪ M := {w | w ∈ L ∨ w ∈ M}<br />

Konkatenation: LM := {vw | v ∈ L ∧ w ∈ M}<br />

Exponentiation: L 0 := {ε},<br />

L i := L i−1 L <strong>für</strong> i > 0<br />

Kleene-Abschluß: L ∗ := � ∞ i=0 L i<br />

Positiver Abschluß: L + := � ∞ i=1 L i<br />

Beispiel 1<br />

Seien L = {A,B,...,Z,a,b,...,z} <strong>und</strong> D = {0, 1,..., 9} Sprachen mit Wörtern der<br />

Länge 1. Dann ist<br />

L ∪ D die Sprache der Buchstaben <strong>und</strong> Ziffern,<br />

LD die Sprache, die lauter Wörter der Form Buchstabe Ziffer<br />

enthält,<br />

L 4 die Sprache aller Wörter mit genau vier Buchstaben über L,<br />

L ∗ die Sprache aller beliebig langen Wörter aus Buchstaben<br />

(inkl. ε),<br />

L((L ∪ D) ∗ ) die Sprache aller Wörter aus Buchstaben <strong>und</strong> Ziffern,<br />

die mit einem Buchstaben beginnen,<br />

D + die Sprache aller nicht-leeren Wörter aus Ziffern.<br />

2.2.1 Reguläre Sprachen<br />

Sei Σ Alphabet.<br />

Definition 1<br />

Die regulären Sprachen über Σ sind induktiv definiert durch<br />

• ∅ ist reguläre Sprache,<br />

• <strong>für</strong> alle a ∈ Σ ist {a} reguläre Sprache,<br />

• falls L1,L2 reguläre Sprachen, so sind auch L1 ∪ L2, L1L2 <strong>und</strong> L ∗ 1 reguläre<br />

Sprachen.<br />

Nichts sonst ist eine reguläre Sprache über Σ.<br />

Bemerkung: {ε} wird durch den ∗ -Operator aus ∅ gewonnen. Also ist {ε} regulär.<br />

2.2.2 Reguläre Ausdrücke<br />

Reguläre Ausdrücke sind spezielle Formeln, mit denen reguläre Sprachen definiert<br />

werden.<br />

Definition 2<br />

Die Menge der regulären Ausdrücke über Σ, reg(Σ), ist induktiv definiert durch<br />

16


2.2 Reguläre Sprachen <strong>und</strong> endliche Automaten<br />

• ∅ ∈ reg(Σ),<br />

• ε ∈ reg(Σ),<br />

• <strong>für</strong> jedes a ∈ Σ ist a ∈ reg(Σ),<br />

• falls r1,r2 ∈ reg(Σ), dann (r1|r2) ∈ reg(Σ), (r1r2) ∈ reg(Σ) <strong>und</strong> (r1) ∗ ∈<br />

reg(Σ).<br />

Bemerkung: Die Zeichen (, ), |, ∗ in regulären Ausdrücken sind Metazeichen. Sie sind<br />

keine Elemente des Alphabets Σ, sondern dienen als Operatoren zur Bildung der<br />

regulären Ausdrücke. Die Metazeichen müssen von den Zeichen des Alphabets zu<br />

unterscheiden sein, damit die von dem regulären Ausdruck beschriebene Sprache<br />

eindeutig zu bestimmen ist. Sind die Metazeichen im Alphabet Σ enthalten, wird<br />

die hieraus resultierende Doppeldeutigkeit durch eine spezielle Kennzeichnung der<br />

Metazeichen vermieden (siehe Beispiel 2).<br />

Die Sprache, die von einem regulären Ausdruck definiert wird, wird in der folgenden<br />

Definition eingeführt.<br />

Definition 3<br />

Sei r regulärer Ausdruck. Die Sprache L(r) ist induktiv definiert durch<br />

• L(∅) = ∅<br />

• L(ε) = {ε}<br />

• L(a) = {a}<br />

• L(( r1 | r2)) = L(r1) ∪ L(r2),<br />

L((r1 r2)) = L(r1)L(r2),<br />

L((r1) ∗ ) = (L(r1)) ∗<br />

Bemerkung: Es gilt: r ∈ reg(Σ) ⇔ L(r) ist reguläre Sprache.<br />

Wir verdeutlichen die regulären Ausdrücke anhand von Beispielen.<br />

Beispiel 2<br />

• a | b beschreibt {a} ∪ {b} = {a,b}<br />

• (a b) ∗ beschreibt ({a}{b}) ∗ = {ab} ∗ = {ε,ab,abab,...}<br />

• (A|...|Z|a|...|z) beschreibt {A,...,Z,a,...,z}<br />

• Sei Σ = {(, )}. Die Zeichen des Alphabets sind in den Metazeichen regulärer<br />

Ausdrücke enthalten. Daher kennzeichen wir die Metazeichen durch Unterstreichung.<br />

Damit beschreibt ( ( ) ∗ ) die Sprache, deren Wörter mit beliebig vielen<br />

öffnenden Klammern beginnen <strong>und</strong> mit einer schließenden Klammer enden:<br />

{), (), ((), (((),...}<br />

17


2 Lexikalische Analyse<br />

Konventionen: Um bei der Angabe regulärer Ausdrücke Klammern zu sparen <strong>und</strong><br />

Mehrdeutigkeiten zu vermeiden, ordnen wir den Operatoren dieser Ausdrücke Prioritäten<br />

zu.<br />

• • hat die höchste Priorität, so daß a|b ∗ <strong>und</strong> (a|b) ∗ unterschiedliche Sprachen<br />

beschreiben. Zudem ist der ∗-Operator linksassoziativ, d.h. a ∗∗ = (a ∗ ) ∗ .<br />

• Die Konkatenation besitzt die zweithöchste Priorität <strong>und</strong> ist ebenfalls linksassoziativ.<br />

• | hat die niedrigste Priorität ( (a|b) ∗ c vs. a|b ∗ c ) <strong>und</strong> ist ebenfalls linksassoziativ.<br />

Bemerkung: Unterschiedliche reguläre Ausdrücke können dieselbe Sprache beschreiben.<br />

So ist L((a|b)(a|b)) = {aa,ab,ba,bb} = L(aa|ab|ba|bb).<br />

Algebraische Eigenschaften: Für die Operatoren | <strong>und</strong> Konkatenation gelten die<br />

folgenden algebraischen Eigenschaften, wobei wir reguläre Ausdrücke genau dann<br />

gleichsetzen, wenn sie dieselbe Sprache beschreiben (r = s bedeutet L(r) = L(s)):<br />

• r|s = s|r (Kommutativität von |)<br />

• r|(s|t) = (r|s)|t (Assoziativität von |)<br />

• r(st) = (rs)t (Assoziativität der Konkatenation)<br />

2.2.3 Endliche Automaten<br />

Nach der Einführung der regulären Sprachen <strong>und</strong> der regulären Ausdrücke in den<br />

vorigen Abschnitten geben wir nun einen Mechanismus zur Erkennung von Wörtern<br />

regulärer Sprachen an. Hierzu verwenden wir die endlichen Automaten.<br />

Definition 4<br />

Ein nichtdeterministischer endlicher Automat (NEA) ist ein Tupel M = (Σ,Q, ∆,q0,F),<br />

wobei<br />

• Σ endliches Alphabet (das Eingabealphabet),<br />

• Q endliche Menge (von Zuständen),<br />

• q0 ∈ Q (der Anfangszustand),<br />

• F ⊆ Q (die Menge der Endzustände) <strong>und</strong><br />

• ∆ ⊆ Q × (Σ ∪ {ε}) × Q (die Übergangsrelation) ist.<br />

Definition 5<br />

Sei M = (Σ,Q, ∆,q0,F) ein NEA.<br />

Ein Paar (q,w),q ∈ Q,w ∈ Σ ∗ heißt Konfiguration von M, (q0,w) heißt Anfangskonfiguration,<br />

(qf,ε) mit qf ∈ F Endkonfiguration.<br />

Die Schritt-Relation ist eine binäre Relation ⊢M⊆ (Q × Σ ∗ ) × (Q × Σ ∗ ), definiert<br />

durch<br />

18


2.2 Reguläre Sprachen <strong>und</strong> endliche Automaten<br />

(q,aw) ⊢M (q ′ ,w) :⇔ (q,a,q ′ ) ∈ ∆ <strong>für</strong> q,q ′ ∈ Q <strong>und</strong> a ∈ Σ oder a = ε.<br />

⊢ ∗ M sei die reflexive transitive Hülle von ⊢M.<br />

Die von M akzeptierte Sprache ist L(M) = {w ∈ Σ ∗ | (q0,w) ⊢ ∗ M (qf,ε),<br />

qf ∈ F }.<br />

Ein endlicher Automat soll ein Eingabewort daraufhin überprüfen, ob es zu einer<br />

bestimmten Sprache gehört. Dabei wird die Eingabe von links nach rechts zeichenweise<br />

gelesen. Zu Beginn befindet sich der Automat im Anfangszustand q0 <strong>und</strong> der<br />

Eingabezeiger zeigt auf das erste Zeichen des Eingabewortes. Nach dem Lesen eines<br />

Zeichens wird das entsprechende Zeichen aus der Eingabe entfernt <strong>und</strong> der Automat<br />

geht in Abhängigkeit vom gelesenen Zeichen mittels der Übergangsrelation in einen<br />

neuen Zustand über. Weiterhin ist der Übergang in einen anderen Zustand ohne das<br />

Lesen eines Eingabezeichens möglich (ε-Übergang). Ein Übergang eines Automaten<br />

in einen anderen Zustand wird Schritt genannt. Ist die Eingabe vollständig gelesen<br />

<strong>und</strong> der Automat befindet sich in einem Endzustand, wird das gelesene Wort<br />

akzeptiert. Befindet sich der Automat nach dem vollständigen Lesen der Eingabe<br />

nicht in einem Endzustand oder ist in einem Zustand kein Übergang <strong>für</strong> das nächste<br />

Eingabezeichen möglich, wird das Eingabewort verworfen.<br />

Das Verhalten eines NEA wird also in jedem Schritt durch den aktuellen Zustand<br />

des Automaten <strong>und</strong> die restliche Eingabe bestimmt. Diese beiden Faktoren bilden<br />

zusammen die aktuelle Konfiguration des endlichen Automaten. Die Übergänge<br />

zwischen Konfigurationen werden durch die Schritt-Relation beschrieben.<br />

Der Automat erkennt die Worte, <strong>für</strong> die er durch eine Folge von Schritten aus der Anfangskonfiguration<br />

eine Endkonfiguration erreichen kann. Die Menge der von einem<br />

NEA erkannten Worte bildet die von ihm akzeptierte Sprache.<br />

Graphische Darstellung: Zur Verbesserung der Übersichtlichkeit werden NEAs<br />

durch Übergangsgraphen dargestellt. Die Knoten des Graphen repräsentieren die<br />

Zustände des Automaten. Die Kanten stellen die Zustandsübergänge des Automaten<br />

dar <strong>und</strong> sind mit dem Zeichen beschriftet, das während des Übergangs gelesen wurde<br />

(bzw. mit ε, falls kein Zeichen gelesen wurde).<br />

Beispiel 3<br />

Der Übergangsgraph in Abbildung 2.2 stellt einen NEA dar, der die Sprache L((a|b) ∗ abb) =<br />

{abb,aabb,babb,aaabb,ababb,...} akzeptiert.<br />

Ein NEA akzeptiert ein Eingabewort w genau dann, wenn es im Übergangsgraphen<br />

einen Pfad vom Startzustand in einen Endzustand gibt, so daß die gelesenen Eingabesymbole<br />

die Kanten des Pfades beschriften.<br />

Die Übergangsrelation ∆ eines NEA kann in Form einer Tabelle dargestellt werden.<br />

Die Tabelle 2.2 enthält die Übergangsrelation des in Abbildung 2.2 dargestellten<br />

Automaten.<br />

19


2 Lexikalische Analyse<br />

a<br />

a b b<br />

0 1 2 3<br />

b<br />

Abbildung 2.2: Beispiel eines Übergangsgraphen.<br />

Zustand / Eingabe a b ε<br />

0 {0, 1} {0} -<br />

1 - {2} -<br />

2 - {3} -<br />

3 - - -<br />

Tabelle 2.2: Übergangsrelation ∆ in Tabellenform.<br />

Satz 1<br />

Zu jedem regulären Ausdruck r gibt es einen nichtdeterministischen endlichen Automaten,<br />

der die von r beschriebene reguläre Sprache akzeptiert.<br />

Beweis<br />

Wir führen den Beweis konstruktiv durch, indem wir <strong>für</strong> jeden regulären Ausdruck<br />

eine Überführung in entsprechende “Automaten” angeben, wobei Kanten zunächst<br />

mit regulären Ausdrücken beschriftet sein dürfen. Handelt es sich bei dem Ausdruck r<br />

um ∅, der die leere Sprache beschreibt, besteht der Automat aus nur einem Zustand,<br />

der zugleich Endzustand ist, <strong>und</strong> enthält keine Übergänge. Andernfalls beginnen wir<br />

mit einem Graphen <strong>für</strong> den regulären Ausdruck r, wie er in in Abbildung 2.3 oben<br />

angegeben ist. Die Überführungsschritte <strong>für</strong> die einzelnen Operatoren sind in Abbildung<br />

2.3 aufgeführt. r,r1,r2 sind reguläre Ausdrücke. (A) beschreibt die Behandlung<br />

der Alternative, (K) der Konkatenation, (S) des Stern-Operators <strong>und</strong> (KL) die Behandlung<br />

von Klammern. �<br />

Beispiel 4<br />

In Abbildung 2.4 wird schrittweise der Automat <strong>für</strong> den regulären Ausdruck a(a|0) ∗<br />

konstruiert. Neben den einzelnen Konstruktionsschritten ist die Regel aus Abbildung<br />

2.3 angegeben, die in diesem Schritt verwendet wurde.<br />

20


q♥ r<br />

✓✏<br />

p♥<br />

✒✑<br />

r1|r2<br />

q♥ ♥p � q♥ ♥p<br />

r1r2<br />

q♥ p♥ � q♥ q1 ♥ ♥p<br />

2.2 Reguläre Sprachen <strong>und</strong> endliche Automaten<br />

♥ r<br />

q ♥p ♥q q1 ♥ q2 ♥ p♥<br />

∗ ε ε<br />

�<br />

♥ (r)<br />

q ♥p q♥ r<br />

�<br />

p♥<br />

r1<br />

r2<br />

r1<br />

ε<br />

r<br />

ε<br />

r2<br />

(A)<br />

(K)<br />

(S)<br />

(KL)<br />

Abbildung 2.3: Konstruktion eines NEA zu einem regulärem Ausdruck.<br />

✓✏<br />

♠a(a|0) 0 1♠<br />

✒✑<br />

∗<br />

✓✏<br />

♠ ♠ (a|0)<br />

0 2 1♠<br />

✒✑<br />

∗<br />

a<br />

0♠ a ♠ε 2<br />

(a|0)<br />

3♠ ε<br />

4♠ ε<br />

✓✏<br />

1♠<br />

✒✑<br />

0♠ a ♠ε 2<br />

a<br />

3♠ 0<br />

ε<br />

4♠ ε<br />

✓✏<br />

1♠<br />

✒✑<br />

ε<br />

ε<br />

(K)<br />

(S)<br />

(KL),(A)<br />

Abbildung 2.4: Beispiel einer NEA-Konstruktion.<br />

21


2 Lexikalische Analyse<br />

Da es sich bei dem mit dem Verfahren erzeugten Automaten um einen nichtdeterministischen<br />

endlichen Automaten handelt, ist eine direkte Umsetzung des Automaten<br />

in ein Programm aufgr<strong>und</strong> des Nichtdeterminismus nicht ohne weiteres möglich.<br />

Aus der Theorie der formalen Sprachen ist bekannt, daß es zu jedem NEA einen<br />

deterministischen endlichen Automaten (DEA) gibt, der dieselbe Sprache erkennt.<br />

Definition 6<br />

Sei M = (Q, Σ, ∆,q0,F) ein NEA. M heißt deterministischer endlicher Automat<br />

(DEA), wenn ∆ eine Funktion σ : Q × Σ → Q ist.<br />

In einem DEA treten keine ε-Übergänge auf. Weiterhin gibt es <strong>für</strong> jeden Zustand<br />

unter jeder Eingabe höchstens einen Folgezustand.<br />

Satz 2<br />

Wird eine Sprache L von einem NEA akzeptiert, so gibt es einen DEA, der L akzeptiert.<br />

Beweis<br />

Der Beweis wird konstruktiv geführt, indem wir ein Verfahren angeben, das zu einem<br />

NEA einen DEA generiert, der dieselbe Sprache erkennt. Dieses Verfahren wird<br />

Potenzmengenkonstruktion genannt.<br />

Die Potenzmengenkonstruktion verwendet die beiden folgenden Definitionen:<br />

Definition 7<br />

Sei M = (Q, Σ, ∆,q0,F) ein NEA <strong>und</strong> sei q ∈ Q. Die Menge der ε-Folgezustände<br />

von q ist<br />

ε − FZ(q) = {p | (q,ε) ⊢ ∗ M (p,ε)},<br />

also die Menge aller Zustände p, inklusive q, <strong>für</strong> die es einen ε-Weg im Übergangsgraphen<br />

zu M von q nach p gibt. Wir erweitern ε − FZ auf Mengen von Zuständen<br />

S ⊆ Q:<br />

ε − FZ(S) = �<br />

ε − FZ(q).<br />

Definition 8<br />

Sei M = (Q, Σ, ∆,q0,F) ein NEA.<br />

Der zu M gehörende DEA M ′ = (Q ′ , Σ,δ,q ′ 0,F ′ ) ist definiert durch:<br />

Q ′ = P(Q), die Potenzmenge von Q,<br />

q ′ 0 = ε − FZ(q0),<br />

F ′ = {S ∈ Q ′ | S ∩ F �= ∅} <strong>und</strong><br />

δ(S,a) = ε − FZ({p | (q,a,p) ∈ ∆ <strong>für</strong> q ∈ S}) <strong>für</strong> a ∈ Σ,S ∈ Q ′ .<br />

22<br />

q∈S


2.2 Reguläre Sprachen <strong>und</strong> endliche Automaten<br />

Der folgende Algorithmus konstruiert zu einem NEA M den zu M gehörenden DEA<br />

M ′ , wobei nicht erreichbare Zustände weggelassen werden.<br />

Algorithmus NEA nach DEA<br />

Eingabe: NEA M = (Q, Σ, ∆,q0,F)<br />

Ausgabe: DEA M ′ = (Q ′ , Σ,δ,q ′ 0,F ′ )<br />

1 q ′ 0 := ε − FZ(q0); Q ′ := {q ′ 0};\\<br />

2 marked(q ′ 0) := false; δ := ∅;\\<br />

3 while e x i s t i e r t S ǫ Q ′ and marked(S) = false do<br />

4 marked(S) := true;<br />

5 foreach a ǫ Σ do<br />

6 T := ε − FZ({p ǫ Q | (q,a,p) ǫ ∆ <strong>und</strong> q ǫ S})<br />

7 if T /∈ Q ′ then<br />

8 Q ′ := Q ′ ∪ {T }; (∗ neuer Zustand ∗)<br />

9 marked(T) := false<br />

10 fi ;\\<br />

11 δ := δ ∪ {(S,a) ↦→ T } (∗ neuer ”Ubergang ∗)<br />

12 od<br />

13 od<br />

Die Zustände von M ′ sind Mengen von Zuständen von M (daher der Name Potenzmengenkonstruktion).<br />

Zwei Zustände p <strong>und</strong> q von M fallen in dieselbe Zustandsmenge<br />

S (also in denselben Zustand von M ′ ), wenn es ein Wort w gibt, welches den NEA M<br />

sowohl nach p als auch nach q bringt. Nach Definition 8 erhält man den Folgezustand<br />

eines Zustands S in M ′ unter einem Zeichen a, indem man die Nachfolgezustände<br />

aller Zustände q ∈ S unter a zusammenfaßt <strong>und</strong> deren ε-Folgezustände hinzufügt.<br />

Wir verdeutlichen die Arbeitsweise der Potenzmengenkonstruktion, indem wir <strong>für</strong> den<br />

in Abbildung 2.4 erzeugten NEA einen DEA generieren, der ebenfalls die durch den<br />

regulären Ausdruck a(a|0) ∗ beschriebene Sprache erkennt. In Abbildung 2.5 [WM96]<br />

sind die einzelnen Schritte des Verfahrens dargestellt. Die Zustände des zu konstruierenden<br />

DEA sind mit 0 ′ , 1 ′ , 2 ′ <strong>und</strong> ⊘ benannt, wobei 0 ′ der Anfangszustand ist. ⊘ ist<br />

ein “Fehlerzustand”, der als Folgezustand eines Zustands q unter a verwendet wird,<br />

wenn es keinen Übergang im NEA unter a aus q heraus gibt. Sind <strong>für</strong> einen Zustand<br />

des DEA <strong>für</strong> alle möglichen Zeichen aus Σ die entsprechenden Nachfolgezustände des<br />

DEA berechnet, wird der Zustand markiert (in Abbildung 2.5 durch Unterstreichung<br />

dargestellt) <strong>und</strong> braucht nicht weiter behandelt zu werden. Endzustände des DEA<br />

sind die Zustände, in deren Menge von Zuständen des NEA ein Endzustand auftritt<br />

(1 ′ <strong>und</strong> 2 ′ sind Endzustände, da sie den NEA-Endzustand 1 beinhalten).<br />

�<br />

23


2 Lexikalische Analyse<br />

24<br />

0 ′ = {0}; Q ′ = {0 ′ }<br />

ausgewählter Zustand neues Q<br />

✗✔<br />

✎☞<br />

✎☞✘✘✘✘<br />

✖✕<br />

✍✌<br />

✍✌❍<br />

❍❍❍ ✎☞<br />

✍✌<br />

✗✔<br />

✎☞<br />

✎☞✘✘✘✘<br />

✖✕ ✍✌<br />

✍✌❝<br />

❝❝❝<br />

✎☞<br />

✗✔<br />

✎☞<br />

✖✕ ✍✌<br />

✍✌<br />

′ neuer (Teil-) DEA<br />

0 ′<br />

0 ′<br />

1 ′<br />

1 ′ 2 ′<br />

0<br />

a<br />

0 ⊘<br />

a<br />

a<br />

0<br />

0<br />

⊘<br />

a a<br />

′ {0 ′ , 1 ′ , ⊘} mit<br />

1 ′ = {1, 2, 3}<br />

1 ′ {0 ′ , 1 ′ , 2 ′ , ⊘} mit<br />

2 ′ = {1, 3, 4}<br />

2 ′ {0 ′ , 1 ′ , 2 ′ , ⊘}<br />

⊘ {0 ′ , 1 ′ , 2 ′ , ⊘}<br />

✗✔ ✎☞<br />

✎☞✘✘✘✘ ✖✕ ✍✌<br />

✍✌ 0<br />

❅<br />

❅❅❅<br />

✎☞<br />

✗✔ ✎☞<br />

✖✕ ✍✌<br />

✍✌<br />

✗✔ ✎☞<br />

✎☞✘✘✘✘ ✖✕ ✍✌<br />

✍✌<br />

❅<br />

❅❅❅<br />

✎☞<br />

✍✌<br />

✗✔ ✎☞<br />

✖✕ ✍✌<br />

′<br />

0 ′<br />

1 ′<br />

1 ′<br />

2 ′<br />

2 ′<br />

a<br />

0<br />

0<br />

0<br />

⊘ a<br />

a<br />

0<br />

0<br />

⊘<br />

a<br />

0<br />

Abbildung 2.5: Beispiel zur Potenzmengenkonstruktion.<br />

a<br />

0


2.2 Reguläre Sprachen <strong>und</strong> endliche Automaten<br />

Da der durch das Verfahren erzeugte DEA i. allg. nicht der “kleinstmögliche” Automat<br />

ist, wird im Anschluß an die Potenzmengenkonstruktion ein weiteres Verfahren,<br />

genannt Minimalisierung, durchgeführt, das zu dem erzeugten DEA einen weiteren<br />

DEA konstruiert, der dieselbe Sprache akzeptiert <strong>und</strong> über eine minimale Zustandsmenge<br />

verfügt. In dem durch die Potenzmengenkonstruktion erzeugten DEA kann<br />

es noch Zustände mit gleichem “Akzeptanzverhalten” geben. Zwei Zustände p <strong>und</strong> q<br />

besitzen das gleiche Akzeptanzverhalten, wenn der Automat aus p <strong>und</strong> q unter allen<br />

Eingabewörtern entweder immer oder nie in einen Endzustand geht. Die Minimalisierung<br />

beruht auf dem Zusammenfassen dieser Zustände zu einem gemeinsamen<br />

Zustand.<br />

Der folgende Algorithmus beschreibt das Verfahren der Minimalisierung.<br />

Algorithmus Minimalisierung.<br />

Eingabe: DEA M = (Q, Σ,δ,q0,F).<br />

Ausgabe: DEA Mmin = (Qmin, Σ,δmin,q0,min,Fmin) mit Qmin minimal.<br />

Methode: Die Zustandsmenge von M wird in eine Partition aufgeteilt, die schrittweise<br />

verfeinert wird. Für Zustände in verschiedenen Klassen einer Partition<br />

ist schon bekannt, daß sie verschiedenes Akzeptanzverhalten zeigen, d.h. daß es<br />

mindestens ein Wort w gibt, unter welchem aus einem der Zustände ein Endzustand<br />

erreicht wird <strong>und</strong> unter dem anderen nicht. Deshalb beginnt man mit der<br />

Partition Π = {F,Q − F }. In jedem Iterationsschritt des Verfahrens wird eine<br />

Klasse K aus der Partition entfernt <strong>und</strong> durch eine Menge von Verfeinerungen<br />

Ki ersetzt. Dabei gilt, daß <strong>für</strong> alle Zustände q in einer Verfeinerung Ki der<br />

Nachfolgezustand δ(q,a) unter einem Zeichen a in einer gemeinsamen Klasse<br />

K ′ i liegt. Wird eine solche Verfeinerung gef<strong>und</strong>en, wird die Variable changed auf<br />

true gesetzt. Der Algorithmus hält, wenn in einem Schritt die Partition nicht<br />

mehr verfeinert wurde. Da in jedem Iterationsschritt nur Klassen der aktuellen<br />

Partition evtl. in Vereinigungen neuer Klassen zerlegt werden, Q <strong>und</strong> damit<br />

auch P(Q) aber endlich sind, terminiert das Verfahren. Die Klassen der dann<br />

gef<strong>und</strong>enen Partition sind die Zustände von Mmin. Es gibt einen Übergang zwischen<br />

zwei neuen Zuständen P <strong>und</strong> R unter einem Zeichen a ∈ Σ, wenn es einen<br />

Übergang δ(p,a) = r mit p ∈ P <strong>und</strong> r ∈ R in M gab.<br />

1 Π = {F,Q − F };<br />

2 do changed := false ;<br />

3 Π ′ := Π;<br />

4 foreach K ∈ Π do<br />

5 Π ′ := (Π ′ − {K}) ∪ {{Ki}1≤i≤n} , <strong>und</strong> die Ki sind maximal mit<br />

6 K = �<br />

1≤i≤n Ki , <strong>und</strong> ∀a ∈ Σ : ∃K ′ i ∈ Π : ∀q ∈ Ki : δ(q,a) ∈ K ′ i<br />

7 if n > 1 then changed := true fi (∗ K wurde<br />

aufgespalten ∗)<br />

25


2 Lexikalische Analyse<br />

✎ ☞<br />

✍{0<br />

✌<br />

′ }<br />

a<br />

✗<br />

✎ ☞<br />

✔a<br />

✖<br />

✍{1<br />

✌<br />

✕<br />

′ , 2 ′ }<br />

Abbildung 2.6: DEA mit minimaler Zustandsmenge.<br />

Partition Klasse Aufspaltung<br />

{{0 ′ , ⊘}, {1 ′ , 2 ′ }}<br />

{0 ′ , ⊘} {0 ′ }, {⊘}<br />

{1 ′ , 2 ′ } nein<br />

{{0 ′ }, {⊘}, {1 ′ , 2 ′ }} keine weitere Aufspaltung<br />

{1 ′ , 2 ′ } bilden zusammen einen neuen Zustand<br />

{⊘} ist ein toter Zustand, da er nicht Endzustand ist <strong>und</strong> alle<br />

Übergänge aus ihm hinaus wieder in ihn hineingehen.<br />

8 od;<br />

9 Π := Π ′<br />

Abbildung 2.7: Beispiel zur Minimalisierung.<br />

10 until not changed ;<br />

11 Qmin = Π−(Tot ∪ Unerreichbar ) ;<br />

q0,min<br />

die Klasse in Π, in der q0 ist.<br />

die Klassen, die ein Element aus F enthalten.<br />

Fmin<br />

δmin(K,a) = K ′ wenn δ(q,a) = p mit q ∈ K <strong>und</strong> p ∈ K ′<br />

<strong>für</strong> ein <strong>und</strong> damit <strong>für</strong> alle a ∈ K.<br />

K ∈ Tot wenn K kein Endzustand ist <strong>und</strong> nur Übergänge<br />

in sich selbst enthält.<br />

K ∈ Unerreichbar wenn es keinen Weg vom Anfangszustand nach K gibt.<br />

Die Anwendung des Verfahrens der Minimalisierung auf den in Abbildung 2.5 konstruierten<br />

DEA liefert den in Abbildung 2.6 angegebenen DEA mit minimaler Zustandsmenge.<br />

In Abbildung 2.7 sind die dabei aufgetretenen Iterationsschritte angegeben.<br />

Mit Hilfe dieser drei Verfahren ist die Implementierung eines Scanners möglich:<br />

26<br />

1. Zuerst wird die zu erkennende Sprache, also die Menge der möglichen Lexeme<br />

<strong>für</strong> Symbole, durch reguläre Ausdrücke beschrieben.<br />

2. Im nächsten Schritt wird gemäß Satz 1 ein NEA konstruiert, der die von den<br />

regulären Ausdrücken beschriebene Sprache akzeptiert.<br />

0


2.2 Reguläre Sprachen <strong>und</strong> endliche Automaten<br />

3. Anschließend wird mit dem Verfahren der Potenzmengenkonstruktion ein DEA<br />

erzeugt, der dieselbe Sprache wie der NEA erkennt.<br />

4. Mit Hilfe des Verfahrens der Minimalisierung wird ein weiterer DEA konstruiert,<br />

der dieselbe Sprache wie der erste DEA erkennt, aber über eine minimale<br />

Zustandsmenge verfügt.<br />

5. Dieser minimale DEA kann nun als Gr<strong>und</strong>lage <strong>für</strong> die Implementierung des<br />

Scanners verwendet werden.<br />

2.2.4 Reguläre Definitionen<br />

Der Übersichtlichkeit halber ist es wünschenswert, regulären Ausdrücken Namen geben<br />

zu können, damit diese Namen in anderen regulären Ausdrücken verwendet<br />

werden können. Diese Namen können wie Zeichen aus dem Alphabet in regulären<br />

Ausdrücken auftreten.<br />

Definition 9<br />

Sei Σ Alphabet. Dann ist eine reguläre Definition eine Folge<br />

d1 → r1<br />

d2 → r2<br />

...<br />

dn → rn,<br />

wobei di jeweils ein eindeutiger Name, ri ∈ reg(Σ ∪ {d1,...,di−1}) ist.<br />

Die Einschränkung ri ∈ reg(Σ ∪ {d1,...,di−1}) gewährleistet, daß kein Name eines<br />

regulären Ausdrucks in einem anderen Ausdruck verwendet wird, bevor der Name<br />

deklariert wurde. Insbesondere sind hierdurch keine rekursiven Definitionen möglich.<br />

Beispiel 5<br />

letter → A | B | ... | Z | a | b | ... | z<br />

digit → 0 | 1 | ... | 9<br />

id → letter ( letter | digit ) ∗<br />

Diese reguläre Definition beschreibt (etwas vereinfacht) die Bezeichner, wie sie in<br />

Programmiersprachen auftreten. Bezeichner beginnen hier mit einem Buchstaben,<br />

dem eine beliebig lange Sequenz von Buchstaben <strong>und</strong> Zeichen folgen kann.<br />

Zur Verbesserung der Lesbarkeit werden folgende Abkürzungen vereinbart:<br />

Ein- oder mehrmaliges Auftreten: Sei r regulärer Ausdruck. Dann stehe r + <strong>für</strong> rr ∗ ,<br />

d.h. L(r + ) = L(r) + . Priorität <strong>und</strong> Assoziativität gelten wie <strong>für</strong> den ∗ -Operator.<br />

27


2 Lexikalische Analyse<br />

Null- oder einmaliges Auftreten: Sei r regulärer Ausdruck. Dann stehe r? <strong>für</strong> r |ε,<br />

d.h. L(r?) = L(r) ∪ {ε}.<br />

Beispiel 6<br />

Der reguläre Ausdruck a + beschreibt die Menge aller nicht-leeren Wörter, die nur<br />

aus a’s bestehen.<br />

a?b ∗ beschreibt die Menge aller Wörter, die entweder nur b’s oder genau ein a <strong>und</strong><br />

dann nur b’s enthalten.<br />

Beispiel 7<br />

Betrachten wir die Regeln einer kontextfreien Grammatik <strong>für</strong> einen Ausschnitt einer<br />

Programmiersprache:<br />

stmt → if expr then stmt<br />

| if expr then stmt else stmt<br />

| ε<br />

expr → term relop term<br />

| term<br />

term → id | num<br />

In der durch diese Regeln beschriebenen Programmiersprache treten folgende Symbole<br />

auf: if, then, else, relop, id, num. Der Scanner muß diese Symbole im Eingabestrom<br />

erkennen <strong>und</strong> herausfiltern. Die Symbole werden in Form einer regulären<br />

Definition spezifiziert:<br />

if → i f<br />

then → t h e n<br />

else → e l s e<br />

relop → = | < | | >=<br />

id → letter (letter | digit) ∗<br />

num → digit + (. digit + )? (E (+ | -)? digit + )?<br />

Dabei seien letter <strong>und</strong> digit wie in Beispiel 5 definiert. num ist entweder einfache<br />

Integer-Zahl oder eine Fließkommazahl, wobei Nachkommastellen <strong>und</strong> ein positiver<br />

bzw. negativer Exponent angegeben werden können.<br />

Schlüsselwörter sollen hier nicht als Bezeichner verwendet werden dürfen. Daher wissen<br />

wir, daß bei Erkennen der Zeichenkette t h e n das Symbol then <strong>und</strong> nicht id<br />

an den Parser weitergereicht werden muß.<br />

Lexeme sind durch Leerräume getrennt, dabei sind Leerräume nicht-leere Folgen von<br />

Leerzeichen <strong>und</strong> Zeilenwechseln:<br />

delim → blank | newline<br />

sep → delim +<br />

blank <strong>und</strong> newline müssen dann in Abhängigkeit von der Implementierung interpretiert<br />

werden.<br />

28


2.2 Reguläre Sprachen <strong>und</strong> endliche Automaten<br />

Regulärer Ausdruck Symbol Attributwert<br />

sep – –<br />

i f if –<br />

t h e n then –<br />

e l s e else –<br />

id id Verweis auf Eintrag in der Symboltabelle<br />

num num Wert der Zahl<br />

< relop LT<br />

relop GT<br />

>= relop GE<br />

Tabelle 2.3: Reguläre Ausdrücke <strong>und</strong> die dazugehörigen Symbole <strong>und</strong> Attributwerte.<br />

Unser Ziel ist nun die Erstellung eines Scanners, der im Eingabestrom ein Lexem <strong>für</strong><br />

das nächste Symbol identifiziert <strong>und</strong> als Ausgabe ein Paar aus Symbol <strong>und</strong> Attribut<br />

gemäß Tabelle 2.3 liefert.<br />

Bei der Erkennung der Lexeme tritt folgendes Problem auf: um die Lexeme eindeutig<br />

identifizieren zu können, muß der Scanner im Eingabestrom “vorausschauen” können.<br />

Zum Beispiel liefern<br />

then das Symbol then,<br />

thens das Symbol id,<br />

85 das Symbol num <strong>und</strong><br />

85a einen lexikalischen Fehler ( steht <strong>für</strong> ein Leerzeichen).<br />

Das Vorausschauen im Eingabestrom wird lookahead genannt. Im allgemeinen genügt<br />

ein lookahead von 1 nicht. Daher sollte die Eingabe in einen Eingabepuffer kopiert<br />

werden, in dem über einen Zeiger auf das jeweils aktuelle Zeichen zugegriffen werden<br />

kann. Hierdurch wird ein Zurückschreiben von Zeichen aus dem lookahead durch<br />

Zurücksetzen des Zeigers ermöglicht.<br />

Ein weiteres Problem tritt bei der Erkennung der Schlüsselwörter der zu übersetzenden<br />

Sprache auf. Die Codierung der Erkennung von Schlüsselwörtern in Automaten<br />

resultiert in einer sehr großen Anzahl von Zuständen. Deshalb werden i. allg.<br />

Schlüsselwörter zunächst wie Bezeichner behandelt. Erst später wird zwischen Bezeichnern<br />

<strong>und</strong> Schlüsselwörtern differenziert, so daß die jeweils entsprechende Information<br />

an den Parser geliefert wird.<br />

Ein mögliches Verfahren besteht darin, die Schlüsselwörter vorab mit entsprechenden<br />

Informationen in die Symboltabelle einzutragen, um eine Unterscheidung von den<br />

Einträgen <strong>für</strong> Bezeichner zu ermöglichen. Eine weitere Möglichkeit besteht in der<br />

29


2 Lexikalische Analyse<br />

Kombination des Scanners mit einem Sieber (siehe Abschnitt 2.3).<br />

Beispiel 8 (Fortführung von Beispiel 7)<br />

Wir geben nun <strong>für</strong> die Symbole Übergangsgraphen an, die beschreiben, wie der Scanner<br />

arbeitet, wenn er vom Parser mit der Ermittlung des nächsten Symbols beauftragt<br />

wurde. Dabei gehen wir davon aus, daß der benötigte Teil der Eingabe in einem<br />

Puffer steht, der über einen Zeiger den Zugriff auf das aktuelle Zeichen erlaubt. Am<br />

Anfang steht der Zeiger auf dem Zeichen hinter dem zuletzt gelesenen Lexem.<br />

In Abbildung 2.8 sind die Übergangsgraphen <strong>für</strong> die Symbole relop, id, num <strong>und</strong> sep<br />

angegeben. Vom Startzustand ausgehend wird je nach lookahead die entsprechende<br />

Kante ausgewählt. Wird ein Endzustand erreicht, wird das entsprechende Symbol an<br />

den Parser zurückgeliefert. Bei der Verwendung einer Kante wird das entsprechende<br />

Zeichen durch Weitersetzen des Zeigers aus der Eingabe gelöscht. Eventuell muß<br />

das gelesene Zeichen vom Scanner wieder in die Eingabe zurückgeschrieben werden<br />

(bzw. durch Rücksetzen des Zeigers wieder sichtbar gemacht werden), damit es <strong>für</strong><br />

das nächste Symbol verwendet werden kann. Dies wird durch die Markierung ∗ an<br />

den entsprechenden Endzuständen signalisiert. Das Lexem <strong>für</strong> das an den Parser<br />

gelieferte Symbol besteht dann aus den Zeichen, die bis zur Verwendung der other-<br />

Kante gelesen wurden. Auf diese Weise erkennt der Automat auch die Symbole <<br />

<strong>und</strong> >, obwohl die Zustände 1 <strong>und</strong> 6 keine Endzustände sind.<br />

Ist der Endzustand des Graphen <strong>für</strong> das Symbol id erreicht, wird vor der Rückgabe<br />

des Symbols durch die Funktion gettoken() überprüft, ob es sich bei der gelesenen<br />

Zeichenkette um ein Schlüsselwort handelt. Ist dies der Fall, wird das zugehörige<br />

Symbol an den Parser geliefert. Andernfalls wird das Symbol id weitergegeben <strong>und</strong><br />

der Bezeichner mittels install id() in die Symboltabelle eingetragen. Das Attribut des<br />

Symbols id enthält dann einen Verweis auf den Eintrag des Bezeichners.<br />

2.3 Sieber<br />

Bei der Implementierung eines Scanners wird oft die Identifizierung der <strong>für</strong> den Parser<br />

relevanten Symbole sowie die Erkennung von Schlüsselwörtern vom eigentlichen<br />

Scannen, also der Zerlegung der Eingabe, getrennt. Das entsprechende Teilprogramm,<br />

das diese Aufgaben übernimmt, wird Sieber genannt.<br />

Die Aufgaben des Siebers lassen sich wie folgt charakterisieren:<br />

30<br />

• Erkennung von Schlüsselwörtern: Der Scanner erkennt während der Zerlegung<br />

der Eingabe Bezeichner. Der Sieber untersucht, ob es sich bei einem<br />

Bezeichner um ein Schlüsselwort der zu erkennenden Sprache handelt. Ist dies<br />

der Fall, wird das zugehörige Symbol an den Parser weitergegeben. Handelt<br />

es sich nicht um ein Schlüsselwort, wird der Bezeichner in die Symboltabelle<br />

eingetragen (sofern noch kein Eintrag <strong>für</strong> ihn existiert) <strong>und</strong> das Symbol id


elop:<br />

Start<br />

id:<br />

Start<br />

num:<br />

Start<br />

sep:<br />

Start<br />

< =<br />

0 1 2<br />

=<br />

5<br />

3<br />

4<br />

> =<br />

6 7<br />

Buchstabe other<br />

9 10 11<br />

Ziffer<br />

><br />

other<br />

other<br />

8<br />

Buchstabe oder Ziffer<br />

Ziffer<br />

2.3 Sieber<br />

Ziffer . Ziffer E<br />

+ oder -<br />

Ziffer other<br />

12 13 14 15 16 17 18 19<br />

Begrenzer other<br />

20 21 22<br />

Begrenzer<br />

*<br />

*<br />

*<br />

return(relop,LE)<br />

return(relop,NE)<br />

return(relop,LT)<br />

return(relop,EQ)<br />

return(relop,GE)<br />

return(relop,GT)<br />

return(gettoken(),install_id())<br />

E Ziffer<br />

*<br />

Ziffer<br />

Abbildung 2.8: Übergangsgraphen <strong>für</strong> die Symbole des Beispiels.<br />

*<br />

31


2 Lexikalische Analyse<br />

mit einem Verweis auf den Eintrag als Attribut an den Parser geliefert. Die<br />

Funktionsweise des Siebers entspricht der Funktion gettoken() in Abbildung<br />

2.8.<br />

• Identifizierung der <strong>für</strong> den Parser relevanten Symbole: Nicht alle Symbole,<br />

die vom Scanner generiert werden, werden vom Parser benötigt. So ist<br />

z.B. die Weitergabe des Symbols sep nicht notwendig, da Informationen über<br />

Leerräume im Quelltext <strong>für</strong> die späteren Compilerphasen irrelevant sind. Dies<br />

gilt auch <strong>für</strong> die Kommentare, die sich in einem Quelltext befinden. Aus diesem<br />

Gr<strong>und</strong> wird die Weitergabe dieser Symbole vom Sieber unterb<strong>und</strong>en, so daß nur<br />

die <strong>für</strong> die weiteren Phasen relevanten Symbole an den Parser weitergegeben<br />

werden.<br />

Beispiel 9<br />

In Abbildung 2.9 sind die lexikalische Analyse <strong>und</strong> die semantische Analyse eines<br />

Programmausschnitts dargestellt. In der Phase (A) zerlegt die lexikalische Analyse<br />

den Zeichenstrom in Symbole. In Phase (B) wandelt der Sieber die Bezeichner, die<br />

Schlüsselwörter darstellen, in entsprechende Symbole um. Gleichzeitig werden die Bezeichner,<br />

die keine Schlüsselwörter darstellen, in die Symboltabelle eingetragen <strong>und</strong><br />

in der Symbolfolge durch ihren Index in dieser Tabelle ersetzt. Anschließend wird in<br />

Phase (C) die syntaktische Analyse durchgeführt <strong>und</strong> ein Strukturbaum erzeugt (siehe<br />

Kapitel 3).<br />

2.4 Fehlerbehandlung<br />

Enthält ein Quelltext lexikalische Fehler, soll der Scanner in der Lage sein, diese Fehler<br />

angemessen zu behandeln. Hierzu gehört zum einen die Ausgabe einer möglichst<br />

genauen Fehlermeldung, die Informationen über Art <strong>und</strong> Position (Zeilen- <strong>und</strong> Spaltennummern)<br />

des Fehlers enthält. Zum anderen soll der Scanner in der Lage sein,<br />

pro Analyse des Quelltexts möglichst viele Fehler zu finden, um die Anzahl der Compilerläufe<br />

zu vermindern. Daher ist es notwendig, daß der Scanner ein Verfahren<br />

beinhaltet, daß nach dem Finden eines Fehlers die weitere Analyse des Quelltexts<br />

erlaubt. Dieses Fortfahren mit der Analyse nach Auftreten eines Fehlers wird “Wiederaufsetzen”<br />

(recovery) genannt.<br />

Beim Scannen tritt eine Fehlersituation ein, wenn kein Präfix der Eingabe einem<br />

Symbolmuster entspricht. So kann in Beispiel 7 die Eingabe 2a keinem Symbolmuster<br />

entsprechen, da Bezeichner nicht mit einer Ziffer beginnen dürfen <strong>und</strong> Zahlen keine<br />

Buchstaben (außer E bzw. e in der Exponentialschreibweise) beinhalten können.<br />

Für das “Wiederaufsetzen” existieren verschiedene Verfahren:<br />

32<br />

• Panic Recovery: Bei diesem Verfahren werden nach Auftreten eines Fehlers<br />

solange Zeichen aus der Eingabe entfernt, bis wieder ein gültiges Lexem ge-


Abbildung 2.9: Analyse eines Programmausschnitts.<br />

33<br />

(C)<br />

(B)<br />

(A)<br />

IDLIST<br />

DECLIST<br />

DECL<br />

IDLIST<br />

TYP<br />

PROGRAM<br />

STATLIST<br />

STAT<br />

ASSIGN<br />

var id(1) com id(2) col int sem id(1) bec int("2") sem id(2) bec id(1) mul id(1) add int("1")<br />

id("var") sep id("a") com id("b") col id("int") sem sep id("a") bec int("2") sem sep id("b") bec id("a") mul id("a") add int("1") sem sep<br />

v a r a , b : i n t ; NL a : = 2 ; NL b : = a * a + 1 ; NL<br />

E<br />

T<br />

F<br />

STATLIST<br />

T<br />

F<br />

E<br />

T<br />

ASSIGN<br />

F<br />

E<br />

T<br />

F<br />

2.4 Fehlerbehandlung


2 Lexikalische Analyse<br />

f<strong>und</strong>en wird (z.B. ein Leerzeichen, ein Zeilenende oder ein Schlüsselwort). Da<br />

die Eingabe endlich ist <strong>und</strong> bei diesem Verfahren verkürzt wird, terminiert<br />

der Scanner immer. Nachteilig kann sich auswirken, daß eventuell größere Abschnitte<br />

aus der Eingabe entfernt werden. Falls diese Abschnitte selbst Fehler<br />

enthalten, werden sie bei der aktuellen Analyse nicht entdeckt.<br />

• Löschen oder Einfügen einzelner Zeichen: Einige Fehler treten in Quelltexten<br />

häufiger auf. Hierzu gehören z.B. fehlende Leerzeichen zwischen Lexemen.<br />

Daher kann es sinnvoll ein, bei Auftreten eines Fehlers ein solches Trennzeichen<br />

einzufügen <strong>und</strong> dann die Analyse fortzusetzen. Dabei besteht die Gefahr,<br />

daß bei wiederholtem Einsetzen vor dem aktuellen Eingabesymbol das<br />

Verfahren nicht mehr terminiert. Neben dem Einfügen ist das Löschen von<br />

Zeichen sinnvoll. Besteht die Fehlersituation im Auftreten eines nicht erlaubten<br />

Zeichens im Quelltext (z.B. ? in einem Modula 2-Programm), kann dieses<br />

Zeichen entfernt <strong>und</strong> dann die Analyse des Programms fortgeführt werden.<br />

• Ersetzen von Zeichen: Eine weitere Möglichkeit besteht in der Ersetzung von<br />

fehlerhaften Zeichen. Tritt z.B. während des Scannens einer Zahl ein Buchstabe<br />

auf, kann dieser Buchstabe durch eine beliebige Ziffer ersetzt werden, um die<br />

Behandlung der weiteren Eingabe zu ermöglichen.<br />

Weitere Methoden <strong>für</strong> das “Wiederaufsetzen” sind denkbar.<br />

34


3 Syntaktische Analyse<br />

Die Aufgabe der syntaktischen Analyse ist die Überprüfung der syntaktischen Struktur<br />

eines Programmtextes. Die Durchführung der syntaktischen Analyse wird von<br />

einem Parser genannten Programmteil übernommen (siehe Abbildung 3.1).<br />

Die syntaktische Analyse<br />

• überprüft, ob die vom Scanner (siehe Kapitel 2) gelieferte Symbolfolge von der<br />

Grammatik der Quellsprache erzeugt werden kann;<br />

• liefert aussagefähige Fehlermeldungen <strong>und</strong> versucht, den fehlerhaften Programmtext<br />

auf weitere Fehler hin zu untersuchen;<br />

• erzeugt eine Darstellung, die die syntaktische Struktur eines Programmtextes<br />

wiedergibt (Strukturbaum oder parse tree).<br />

Die theoretische Gr<strong>und</strong>lage der Syntaxanalyse sind die kontextfreien Grammatiken.<br />

3.1 Kontextfreie Grammatiken<br />

Wie in Kapitel 2 beschrieben, kann die lexikalische Struktur eines Programmtextes<br />

mit endlichen Automaten analysiert werden. Für die Definition der syntaktischen<br />

Struktur eines Programmtextes ist die Mächtigkeit der regulären Grammatiken jedoch<br />

nicht ausreichend. In Programmtexten treten häufig Klammerstrukturen auf<br />

(z.B. Klammern in arithmetischen Ausdrücken, begin <strong>und</strong> end), die von den regulären<br />

Grammatiken nicht erkannt werden können. Sei |w|a die Anzahl der Vorkommen des<br />

Zeichens a im Wort w. Die Sprache der wohlgeformten Klammerstrukturen wird<br />

Dycksprache genannt <strong>und</strong> ist wie folgt definiert:<br />

Quellprogramm<br />

Scanner<br />

Symbol<br />

nächstes Symbol<br />

anfordern<br />

Symboltabelle<br />

Parser<br />

parse tree<br />

Rest des Front-Ends<br />

Zwischendarstellung<br />

Abbildung 3.1: Interaktion zwischen Scanner, Parser <strong>und</strong> restlichem Front-End.<br />

35


3 Syntaktische Analyse<br />

{w | w ∈ (a|b) ∗ , |w|a = |w|b, ∀u,v,w = uv : |u|a ≥ |u|b}.<br />

Das Zeichen a entspricht der öffnenden Klammer, b analog der schließenden Klammer.<br />

Endliche Automaten können nicht “zählen”, d.h. sie sind nicht in der Lage, zu<br />

überprüfen, ob bisher mindestens so viele a’s wie b’s gelesen wurden <strong>und</strong> ob in einem<br />

Endzustand die Anzahl der a’s <strong>und</strong> b’s übereinstimmt. Aus diesem Gr<strong>und</strong> ist die<br />

Dycksprache nicht regulär. Ein weiteres Beispiel <strong>für</strong> eine nichtreguläre Sprache ist<br />

{a n b n | n ≥ 0} [Sch08]. Für die Syntaxanalyse wird also die Klasse der kontextfreien<br />

Grammatiken verwendet.<br />

Zu jeder regulären Sprache gibt es einen endlichen Automaten, der die Sprache erkennt.<br />

Analog kann man <strong>für</strong> jede kontextfreie Grammatik einen sogenannten Kellerautomaten<br />

konstruieren, der die von der Grammatik definierte Sprache akzeptiert.<br />

3.1.1 Kontextfreie Grammatiken<br />

Zunächst definieren wir die kontextfreien Grammatiken.<br />

Definition 10<br />

Eine kontextfreie Grammatik ist ein Quadrupel G = (VN,VT,P,S) mit<br />

• VN,VT endliche Mengen (VN Menge der Nichtterminalsymbole, VT Menge der<br />

Terminalsymbole),<br />

• P ⊆ VN × (VN ∪ VT) ∗ (Menge der Produktionen oder Regeln),<br />

• S ∈ VN (Startsymbol).<br />

Terminale stehen <strong>für</strong> die Symbole, die in den zu analysierenden Programmen wirklich<br />

auftreten, <strong>und</strong> entsprechen somit den vom Scanner in der lexikalischen Analyse<br />

erzeugten Symbolen.<br />

Notationen: Wir unterscheiden im folgenden die Nichtterminale <strong>und</strong> die Terminale<br />

in den Produktionen einer Grammatik durch folgende Konventionen:<br />

36<br />

• Terminale werden dargestellt durch<br />

1. a, b, c, ...,<br />

2. Operatorsymbole +, −, ...,<br />

3. Satzzeichen, Klammern etc.,<br />

4. Ziffern 0, ..., 9 <strong>und</strong><br />

5. fettgedruckte Zeichenketten, z.B. id, while.<br />

Nichtterminale werden dargestellt durch


3.1 Kontextfreie Grammatiken<br />

1. A, B, C, ... (S – bzw. die linke Seite der ersten Produktion einer Grammatik<br />

– Startsymbol) <strong>und</strong><br />

2. kursiv gesetzte Zeichenketten, z.B. expr, stmt, Anw, Anw Folge.<br />

• Weiterhin gelten folgende Vereinbarungen:<br />

– X, Y , Z, ... stehen <strong>für</strong> einzelne Grammatiksymbole (Nichtterminale oder<br />

Terminale),<br />

– u, v, w, ..., z stehen <strong>für</strong> Wörter über Terminalsymbolen <strong>und</strong><br />

– α, β, γ, ... stehen <strong>für</strong> Wörter über Grammatiksymbolen.<br />

• Für (A,α) ∈ P schreiben wir A → α (α = ε : ε-Produktionen). Mehrere<br />

Produktionen A → α1,...,A → αn <strong>für</strong> ein Nichtterminal A schreiben wir als<br />

A → α1 | ... | αn (Alternativen <strong>für</strong> A).<br />

Beispiel 10 (Arithmetische Ausdrücke)<br />

Die in Programmiersprachen häufig auftretenden arithmetischen Ausdrücke lassen<br />

sich (vereinfacht) wie folgt mit Hilfe einer kontextfreien Grammatik definieren:<br />

expr → expr op expr<br />

expr → ( expr )<br />

expr → − expr<br />

expr → id<br />

op → +<br />

op → −<br />

op → *<br />

op → /<br />

op → ↑<br />

Dabei sind expr <strong>und</strong> op Nichtterminale, expr ist das Startsymbol der Grammatik <strong>und</strong><br />

id, +, −, ∗, /, ↑, (, ) sind die Terminale.<br />

Mit den oben angegeben Schreibkonventionen läßt sich die Darstellung der Grammatik<br />

verkürzen zu<br />

E → E A E | ( E ) | - E | id<br />

A → + | - | * | / | ↑<br />

Beispiel 11 (Anweisungen einer imperativen Sprache)<br />

Die folgende kontextfreie Grammatik [WM96] definiert die Anweisungen einer einfachen<br />

imperativen Programmiersprache, deren Syntax ähnlich zu der von Pascal ist:<br />

37


3 Syntaktische Analyse<br />

Anw → If Anw |<br />

While Anw |<br />

Repeat Anw |<br />

Proz Aufruf |<br />

Wertzuweisung<br />

If Anw → if Bed then An Folge else An Folge fi |<br />

if Bed then An Folge fi<br />

While Anw → while Bed do An Folge od<br />

Repeat Anw → repeat An Folge until Bed<br />

Proz Aufruf → Name ( Ausdr Folge )<br />

Wertzuweisung → Name := Ausdr<br />

An Folge → Anw |<br />

An Folge ; Anw<br />

Ausdr Folge → Ausdr |<br />

Ausdr Folge , Ausdr<br />

Eine Anweisung ist entweder eine If-Anweisung, eine While- oder Repeat-Schleife,<br />

ein Prozeduraufruf oder eine Wertzuweisung. Wir nehmen an, daß die Nichtterminale<br />

Bed <strong>für</strong> Bedingungen, Ausdr <strong>für</strong> Ausdrücke sowie Name <strong>für</strong> Bezeichner vordefiniert<br />

sind. Das Nichtterminal An Folge beschreibt Anweisungsfolgen. Dieses Nichtterminal<br />

ist rekursiv definiert: Eine Anweisungsfolge ist entweder eine einzelne Anweisung<br />

oder eine (kürzere) Folge, an die eine Anweisung nach einem ; angehängt wird. Das<br />

Nichtterminal Ausdr Folge beschreibt analog Folgen von Ausdrücken, in denen die<br />

einzelnen Ausdrücke durch , getrennt sind.<br />

Beispiel 12 (Grammatik mit ε-Produktionen)<br />

Die folgende Grammatik definiert begin-end-Blöcke in Pascal:<br />

Block → begin opt An Folge end<br />

opt An Folge → An Folge | ε<br />

An Folge → Anw | An Folge ; Anw<br />

Ein Block ist eine mit begin <strong>und</strong> end geklammerte Anweisungsfolge. Blöcke können<br />

in Pascal auch leer sein, so daß das Nichtterminal opt An Folge (<strong>für</strong> “optionale Anweisungsfolge”)<br />

auch zum leeren Wort ε abgeleitet werden kann.<br />

3.1.2 Ableitungen<br />

Wir betrachten die Produktionen kontextfreier Grammatiken als Ersetzungsregeln,<br />

die aus Wörtern über VN ∪ VT neue Wörter über VN ∪ VT “produzieren”, indem<br />

Nichtterminale durch rechte Regelseiten ersetzt werden : σAτ ⇒ σατ (A → α Produktion).<br />

Definition 11<br />

Sei G = (VN,VT,P,S) eine kontextfreie Grammatik.<br />

38


3.1 Kontextfreie Grammatiken<br />

ϕ produziert ψ gemäß G direkt (ϕ ⇒G ψ) :⇔ es existieren Wörter σ,τ,α <strong>und</strong> ein<br />

Nichtterminal A mit ϕ = σAτ, ψ = σατ <strong>und</strong> A → α ∈ P.<br />

ϕ produziert ψ gemäß G (oder ψ ist aus ϕ gemäß G ableitbar) (ϕ ⇒ ∗ G ψ) :⇔ es<br />

existiert eine Folge von Wörtern ϕ1,...,ϕn(n ≥ 1) mit ϕ = ϕ1, ψ = ϕn <strong>und</strong> ϕi ⇒G<br />

ϕi+1 <strong>für</strong> 1 ≤ i < n. (⇒ ∗ G ist also die reflexive <strong>und</strong> transitive Hülle von ⇒G.)<br />

ϕ1,ϕ2,...,ϕn heißt dann eine Ableitung von ψ aus ϕ gemäß G.<br />

Wir schreiben auch ϕ ⇒ n−1<br />

G ψ (“in n − 1 Schritten”).<br />

Wenn aus dem Zusammenhang hervorgeht, auf welche Grammatik wir uns beziehen,<br />

lassen wir den Index G in ⇒G <strong>und</strong> ⇒ ∗ G weg.<br />

Beispiel 13 (Fortsetzung von Beispiel 10)<br />

Wir geben eine Ableitung des Wortes −(id+E) gemäß der in Beispiel 10 vorgestellten<br />

Grammatik an:<br />

E ⇒ −E ⇒ −(E) ⇒ −(EAE) ⇒ −(E + E) ⇒ −(id + E),<br />

also E ⇒ ∗ −(id + E).<br />

In einem Ableitungsschritt können mehrere anzuwendende Produktionen zur Auswahl<br />

stehen. So hätten wir im Schritt E ⇒ −E auch die Ableitung E ⇒ EAE<br />

auswählen können, was aber zur Folge hätte, daß wir das Wort −(id+E) nicht mehr<br />

hätten ableiten können. Ebenso kann eine Regel eventuell an verschiedenen Stellen<br />

des Wortes angewendet werden. So hätte im letzten Ableitungsschritt auch das Wort<br />

−(E + id) erzeugt werden können.<br />

Definition 12<br />

Die von G definierte (erzeugte) Sprache ist L(G) = {u ∈ V ∗<br />

T | S ⇒ ∗ G u}.<br />

Ein Wort x ∈ L(G) heißt ein Satz von G.<br />

Ein Wort α ∈ (VN ∪ VT) ∗ mit S ⇒ ∗ G α heißt eine Satzform von G.<br />

In Beispiel 13 treten nur Satzformen, aber keine Sätze auf.<br />

Definition 13<br />

Eine kontextfreie Sprache ist eine Sprache, die von einer kontextfreien Grammatik<br />

erzeugt werden kann. Zwei kontextfreie Grammatiken G1,G2 heißen äquivalent :⇔<br />

L(G1) = L(G2).<br />

Eine kontextfreie Grammatik enthält unter Umständen Nichtterminale, die nicht zur<br />

Erzeugung der Sprache beitragen.<br />

Definition 14<br />

39


3 Syntaktische Analyse<br />

• Ein Nichtterminal A heißt unerreichbar, wenn es keine Wörter α,β gibt mit<br />

S ⇒ ∗ αAβ. A heißt unproduktiv, wenn es kein Wort u gibt mit A ⇒ ∗ u.<br />

• Eine kontextfreie Grammatik G heißt reduziert, wenn sie weder unerreichbare<br />

noch unproduktive Nichtterminale enthält.<br />

Durch Elimination unerreichbarer <strong>und</strong> unproduktiver Nichtterminalsymbole aus der<br />

Grammatik entsteht eine äquivalente reduzierte Grammatik. Wir nehmen im folgenden<br />

immer an, daß die betrachteten Grammatiken reduziert sind.<br />

Definition 15<br />

Sei ϕ1,ϕ2,...,ϕn eine Ableitung von ϕ = ϕn aus S = ϕ1.<br />

ϕ1,ϕ2,...,ϕn heißt Linksableitung von ϕ (S ⇒ ∗ l ϕ), wenn beim Schritt von ϕi nach<br />

ϕi+1 jeweils das am weitesten links stehende Nichtterminal ersetzt wird.<br />

ϕ1,ϕ2,...,ϕn heißt Rechtsableitung von ϕ (S ⇒ ∗ r ϕ), wenn jeweils das am weitesten<br />

rechts stehende Nichtterminal ersetzt wird.<br />

Eine Satzform, die in einer Linksableitung (bzw. Rechtsableitung) auftritt, heißt Linkssatzform<br />

(bzw. Rechtssatzform).<br />

Beispiel 14 (Fortsetzung von Beispiel 13)<br />

Die in Beispiel 13 angegebene Ableitung ist weder eine Links- noch eine Rechtsableitung.<br />

Allerdings kann das Wort −(id+E) auch mit folgender Linksableitung erzeugt<br />

werden:<br />

E ⇒l −E ⇒l −(E) ⇒l −(EAE) ⇒l −(id A E) ⇒l −(id + E),<br />

also gilt E ⇒ ∗ l −(id + E).<br />

Zu jedem Satz einer kontextfreien Grammatik gibt es eine Linksableitung <strong>und</strong> eine<br />

Rechtsableitung. Wir betrachten im folgenden nur Ableitungen, in deren Schritten<br />

entweder immer links oder immer rechts ersetzt wird.<br />

3.1.3 Strukturbäume<br />

Neben der Überprüfung der Fehlerfreiheit eines Programmtextes ist die Generierung<br />

einer Zwischendarstellung des Programms, die die syntaktische Struktur des Quelltextes<br />

wiedergibt, eine Aufgabe der syntaktischen Analyse. Diese Zwischendarstellung<br />

kann in den folgenden Phasen der Übersetzung verwendet werden. Da<strong>für</strong> ist der<br />

Strukturbaum oder parse tree geeignet.<br />

Definition 16<br />

Sei G = (VN,VT,P,S) kontextfreie Grammatik. Sei B ein geordneter Baum, dessen<br />

Blätter über VN ∪ VT ∪ {ε} <strong>und</strong> dessen innere Knoten über VN markiert sind.<br />

B ist Strukturbaum <strong>für</strong> α ∈ (VN ∪ VT) ∗ <strong>und</strong> N ∈ VN, wenn gilt:<br />

40


-<br />

E<br />

E<br />

( E )<br />

E A E<br />

id<br />

Abbildung 3.2: Beispiel eines Strukturbaums.<br />

+<br />

3.1 Kontextfreie Grammatiken<br />

1. Ist n innerer Knoten, der mit dem Nichtterminal A markiert ist, <strong>und</strong> sind<br />

seine Kinder von links nach rechts mit X1,...,Xk ∈ VN ∪ VT markiert, so ist<br />

A → X1...Xk ∈ P. Ist sein einziges Kind markiert mit ε, so ist A → ε ∈ P.<br />

2. Die Wurzel von B ist mit N markiert.<br />

3. Front (Wort an den Blättern) von B ist α.<br />

Ein Strukturbaum <strong>für</strong> ein Wort α <strong>und</strong> das Startsymbol S heißt Strukturbaum <strong>für</strong> α.<br />

Ein Strukturbaum <strong>für</strong> ein Wort u ∈ V ∗<br />

T (alle seine Blätter sind mit Terminalen oder<br />

ε markiert) heißt vollständig.<br />

Beispiel 15<br />

In Abbildung 3.2 ist der Strukturbaum <strong>für</strong> die Ableitung aus Beispiel 14 dargestellt.<br />

Front des Baums ist die Satzform −(id+E). Der Strukturbaum ist nicht vollständig,<br />

da noch ein Blatt mit dem Nichtterminal E markiert ist. Wird dieses Blatt gemäß<br />

der Produktion E → id modifiziert, wird der Baum vollständig.<br />

Satz 3<br />

Jeder Satz einer kontextfreien Grammatik besitzt mindestens einen Strukturbaum.<br />

Beweis<br />

Der Satz besitzt eine Ableitung. Konstruiere dazu den Strukturbaum nach folgendem<br />

Verfahren.<br />

Sei ϕ1,ϕ2,...,ϕn eine Ableitung von α = ϕn aus N = ϕ1 gemäß G = (VN,VT,P,S).<br />

Dann existiert genau ein Strukturbaum <strong>für</strong> α <strong>und</strong> N gemäß G, der dieser Ableitung<br />

entspricht.<br />

41


3 Syntaktische Analyse<br />

E<br />

E<br />

- E<br />

- E<br />

-<br />

E<br />

E<br />

- E<br />

-<br />

( E ) ( E ) ( E )<br />

E A E<br />

E A E<br />

E A<br />

id id +<br />

E<br />

E<br />

E<br />

( E )<br />

Abbildung 3.3: Konstruktion eines Strukturbaums.<br />

Sei B 1 := N, also der Baum, der nur aus dem mit N markierten Knoten besteht.<br />

Sei B i−1 Strukturbaum <strong>für</strong> ϕi−1 (i > 1). Front von B i−1 ist ϕi−1. Sei ϕi−1 =<br />

X1X2...Xk ∈ (VN ∪ VT) ∗ . Sei ϕi aus ϕi−1 durch Ersetzen von Xj durch β = Y1Y2...Yl<br />

abgeleitet (also Xj → β ∈ P).<br />

Falls l > 0, sei B i der Baum, der aus B i−1 entsteht, indem das j-te Blatt (von links)<br />

l Nachfolgerknoten erhält, die von links nach rechts mit Y1,Y2,...,Yl markiert sind.<br />

Falls l = 0, erhält das j-te Blatt genau einen Nachfolger, der mit ε markiert wird.<br />

Der gesuchte Strukturbaum ist dann B := B n .<br />

�<br />

Beispiel 16<br />

Abbildung 3.3 enthält die schrittweise Konstruktion des Strukturbaums aus Abbildung<br />

3.2 gemäß der Ableitung aus Beispiel 14.<br />

Umgekehrt gilt: zu einem Strukturbaum gibt es mindestens eine Ableitung. Aber:<br />

ein Strukturbaum kann mehreren Ableitungen entsprechen (siehe obiges Beispiel).<br />

Allerdings gibt es zu jedem Strukturbaum <strong>für</strong> ein Wort w ∈ V ∗<br />

T eine eindeutige<br />

Links- <strong>und</strong> eine eindeutige Rechtsableitung.<br />

3.1.4 Mehrdeutige Grammatiken<br />

Während aus einem Strukturbaum eindeutig hervorgeht, welchen Satz er beschreibt,<br />

kann es zu einem Satz mehrere Strukturbäume geben. Erlauben die Produktionen<br />

42<br />

E<br />

E


E<br />

E A E<br />

id<br />

+<br />

E A E<br />

id<br />

3.1 Kontextfreie Grammatiken<br />

E<br />

E A E<br />

E A E<br />

* id id + id<br />

Abbildung 3.4: Verschiedene Strukturbäume zu einem Satz.<br />

einer Grammatik zu einem Satz mehrere Strukturbäume, ist die Grammatik mehrdeutig.<br />

Beispiel 17<br />

In Abbildung 3.4 sind zwei mögliche Strukturbäume <strong>für</strong> den Satz id + id ∗ id gegeben.<br />

Ihre unterschiedliche Struktur ist in der Wahlmöglichkeit zwischen verschiedenen<br />

Produktionen begründet. Es entstehen verschiedene Strukturbäume, die trotzdem dieselbe<br />

Satzform als Ergebnis besitzen.<br />

Definition 17<br />

Ein Satz u ∈ L(G) heißt mehrdeutig, wenn er mehr als einen Strukturbaum hat.<br />

Eine kontextfreie Grammatik G heißt mehrdeutig, wenn L(G) mindestens einen<br />

mehrdeutigen Satz enthält. Eine nicht mehrdeutige Grammatik nennen wir eindeutig.<br />

Zu jedem eindeutigen Satz einer kontextfreien Grammatik gibt es genau eine Links<strong>und</strong><br />

genau eine Rechtsableitung.<br />

Manchmal ist es günstig, Mehrdeutigkeiten durch Umformen der Grammatik in eine<br />

äquivalente Grammatik zu eliminieren (andere Lösungsmöglichkeiten werden wir<br />

später betrachten). Diese Umformung ist aber nicht <strong>für</strong> jede beliebige Grammatik<br />

möglich, da es Sprachen gibt, die keine eindeutige Grammatik besitzen [AU72]. Außerdem<br />

muß beim Umformen darauf geachtet werden, daß die syntaktische Struktur<br />

der zu erkennenden Sprache nicht verändert wird.<br />

Beispiel 18 (“dangling else”)<br />

Die folgende Grammatik beschreibt eine if-Anweisung, bei der ein optionaler else-<br />

Zweig angegeben werden kann.<br />

stmt → if expr then stmt<br />

| if expr then stmt else stmt<br />

| other<br />

*<br />

id<br />

43


3 Syntaktische Analyse<br />

stmt<br />

if expr then stmt<br />

E<br />

1<br />

if expr then stmt else<br />

stmt<br />

E<br />

2<br />

S<br />

1<br />

S<br />

2<br />

if expr then stmt else stmt<br />

E<br />

1<br />

if expr then stmt<br />

E<br />

2<br />

S<br />

1<br />

Abbildung 3.5: Mögliche Strukturbäume des “dangling else”-Problems.<br />

In Abbildung 3.5 sind zwei mögliche Strukturbäume <strong>für</strong> den Satz<br />

S<br />

2<br />

if E1 then if E2 then S1 else S2<br />

angegeben. Der obere Strukturbaum stellt eine Ableitung dar, in der zuerst die erste<br />

<strong>und</strong> dann die zweite Produktion der Grammatik angewendet wurden <strong>und</strong> das else<br />

somit zum zweiten if gehört. Im unteren Strukturbaum wurden zuerst die zweite<br />

<strong>und</strong> danach die erste Produktion angewendet <strong>und</strong> das else gehört daher zum ersten<br />

if. Beide Ableitungen sind aufgr<strong>und</strong> der Mehrdeutigkeit der Grammatik möglich. In<br />

Programmiersprachen (z.B. in Pascal) gilt aber die Konvention, daß ein else zum<br />

letzten noch freien then gehört. Daher stellt der obere Strukturbaum die gewünschte<br />

Ableitung dar.<br />

Um die Mehrdeutigkeit zu eliminieren, modifizieren wir die Grammatik. Dabei gehen<br />

wir von folgender Idee aus: die Anweisung zwischen einem then <strong>und</strong> einem else muß<br />

“geschlossen” sein, d.h. sie darf nicht mit einem freien then enden (sonst würde das<br />

else diesem then zugeordnet). Geschlossene Anweisungen (matched stmt) sind komplette<br />

if-then-else-Anweisungen, die nur geschlossene if-then-else-Anweisungen oder<br />

andere Anweisungen (dargestellt durch other) enthalten. Analog bezeichnen wir die<br />

nicht geschlossenen Anweisungen als unmatched stmt. Wir erhalten folgende Grammatik:<br />

44<br />

stmt


stmt<br />

unmatched_stmt<br />

if expr then stmt<br />

E<br />

1<br />

matched_stmt<br />

if expr then matched_stmt else matched_stmt<br />

stmt<br />

matched_stmt<br />

E S S<br />

2<br />

1 2<br />

if expr then matched_stmt else matched_stmt<br />

(*)<br />

E S<br />

1 2<br />

3.1 Kontextfreie Grammatiken<br />

Abbildung 3.6: Lösung des “dangling else”-Problems.<br />

stmt → matched stmt<br />

| unmatched stmt<br />

matched stmt → if expr then matched stmt else matched stmt<br />

| other<br />

unmatched stmt → if expr then stmt<br />

| if expr then matched stmt else unmatched stmt<br />

Diese Grammatik erzeugt dieselbe Sprache, läßt aber nur den ersten Strukturbaum<br />

zu (Abbildung 3.6). Im zweiten Strukturbaum in Abbildung 3.6 kann an der mit (*)<br />

markierten Position kein Teilbaum <strong>für</strong> if E2 then S1 eingefügt werden, da dort ein<br />

matched stmt erwartet wird.<br />

45


3 Syntaktische Analyse<br />

3.2 Konstruktion von Parsern<br />

Aus der Theorie der formalen Sprachen ist bekannt, daß nichtdeterministische Kellerautomaten<br />

genau die kontextfreien Sprachen erkennen [Sch08] (analog zu regulären<br />

Sprachen <strong>und</strong> endlichen Automaten). Also gibt es zu jeder kontextfreien Grammatik<br />

einen nichtdeterministischen Kellerautomaten.<br />

Im Gegensatz zu endlichen Automaten, bei denen die nichtdeterministischen <strong>und</strong> deterministischen<br />

Varianten die gleiche Mächtigkeit haben, besitzen nichtdeterministische<br />

Kellerautomaten eine größere Mächtigkeit als deterministische Kellerautomaten<br />

<strong>und</strong> erkennen somit mehr Sprachen als diese. Da wir Parser in Form eines Programms<br />

realisieren wollen, betrachten wir später Teilmengen der kontextfreien Sprachen, die<br />

sich mit deterministischen Kellerautomaten erkennen lassen.<br />

Im folgenden konstruieren wir zunächst nichtdeterministische Parser, die dann als<br />

Basis <strong>für</strong> deterministische Parse-Verfahren dienen.<br />

Es gibt zwei Konstruktionsverfahren eines nichtdeterministischen Kellerautomaten zu<br />

einer kontextfreien Grammatik. Entsprechend diskutieren wir zwei unterschiedliche<br />

Verfahren zur Konstruktion von Parsern. Beim Top-Down-Verfahren wird der Strukturbaum<br />

von der Wurzel zu den Blättern hin erzeugt, beim Bottom-Up-Verfahren<br />

entsprechend entgegengesetzt.<br />

3.2.1 Kellerautomat<br />

In Abbildung 3.7 ist das Schema eines nichtdeterministischen Kellerautomaten angegeben.<br />

Er besteht aus einer Kontrolleinheit, einem Keller <strong>für</strong> das Speichern von<br />

Werten, einem Eingabe- <strong>und</strong> einem Ausgabeband. Der Inhalt des Eingabebands wird<br />

mit dem Lesekopf zeichenweise von links nach rechts gelesen. In Abhängigkeit von<br />

dem gelesenen Zeichen <strong>und</strong> dem aktuellen Kellerinhalt führt die Kontrolleinheit einen<br />

Berechnungsschritt durch. Dabei können sowohl ein Zeichen auf das Ausgabeband<br />

geschrieben als auch der Inhalt des Kellers modifiziert werden. Hierbei wird immer<br />

an der Kellerspitze gelesen bzw. geschrieben.<br />

Definition 18<br />

Ein (nichtdeterministischer) Kellerautomat (mit Ausgabe) ist ein Tupel M = (Σ, Γ, ∆,z0,O)<br />

mit • Σ endliche Menge (Eingabealphabet),<br />

• Γ endliche Menge (Kelleralphabet),<br />

• zo ∈ Γ (Kellerstartsymbol),<br />

• O endliche Menge (Ausgabealphabet),<br />

• ∆ ⊆ ((Σ ∪ {ε}) × Γ) × (Γ ∗ × O ∗ ) (endliche Übergangsrelation).<br />

Jedes Element der Übergangsrelation beschreibt einen möglichen Schritt bei der Analyse<br />

des Eingabewortes.<br />

((Σ ∪ {ε}) × Γ) repräsentiert das aktuell vom Eingabeband gelesene Zeichen des<br />

46


Keller<br />

Eingabewort<br />

Kontrolle<br />

000 111<br />

3.2 Konstruktion von Parsern<br />

...<br />

Lesekopf<br />

Ausgabe<br />

Abbildung 3.7: Schema eines Kellerautomaten.<br />

Eingabealphabets Σ (ε falls keine Leseoperation durchgeführt wird) <strong>und</strong> das oberste<br />

Zeichen des Kellers (aus dem Kelleralphabet Γ). Bei der Durchführung eines Berechnungsschrittes<br />

(eines Übergangs des Kellerautomaten) wird die bisherige Kellerspitze<br />

entfernt <strong>und</strong> durch eine (evtl. auch leere) Folge von Kellerzeichen ersetzt sowie ein<br />

Zeichen auf das Ausgabeband geschrieben (repräsentiert durch (Γ ∗ × O ∗ )).<br />

Definition 19<br />

Sei M nichtdeterministischer Kellerautomat.<br />

Die Menge der Konfigurationen von M ist K = Σ ∗ × Γ ∗ × O ∗ .<br />

Die Anfangskonfiguration <strong>für</strong> die Analyse eines Wortes w ∈ Σ ∗ ist (w,z0,ε).<br />

Die Einzelschrittrelation ⊢M⊆ K × K ist definiert durch<br />

(aw,Y γ,u) ⊢M (w,βγ,ub) :⇔ ((a,Y ), (β,b)) ∈ ∆ (a ∈ Σ ∪ {ε}). ⊢ ∗ M ist die reflexive<br />

<strong>und</strong> transitive Hülle von ⊢M.<br />

Elemente von {ε} × {ε} × O ∗ heißen Endkonfigurationen.<br />

Die von M erkannte Sprache ist L(M) := {w ∈ Σ ∗ | (w,z0,ε) ⊢ ∗ M (ε,ε,σ), σ ∈ O ∗ }.<br />

Konfigurationen modellieren die Zustände während der Analyse eines Eingabewortes.<br />

Eine Konfiguration enthält die noch nicht gelesene Eingabe, den aktuellen Kellerinhalt<br />

<strong>und</strong> den Inhalt des Ausgabebandes. In der Anfangskonfiguration ist noch kein<br />

Zeichen der Eingabe gelesen, der Keller enthält nur das Kellerstartsymbol z0 <strong>und</strong><br />

das Ausgabeband ist leer. Endkonfigurationen sind alle Konfigurationen, in denen<br />

Eingabeband <strong>und</strong> Keller leer sind <strong>und</strong> somit das Eingabewort komplett gelesen <strong>und</strong><br />

der Kellerinhalt “verbraucht” wurde. Die Einzelschrittrelation beschreibt die Berechnungsschritte<br />

zwischen den Konfigurationen. (aw,Y γ,u) beschreibt den Zustand vor<br />

einem Schritt. Das aktuelle Symbol der Eingabe ist a ∈ Σ ∪ {ε}, die aktuelle Kellerspitze<br />

ist Y ∈ Γ <strong>und</strong> der Inhalt des Ausgabebands ist u ∈ O ∗ . Wenn es einen<br />

...<br />

47


3 Syntaktische Analyse<br />

Übergang ((a,Y ), (β,b)) in der Übergangsrelation ∆ des Kellerautomaten gibt, wird<br />

das a (eventuell ε) aus der Eingabe entfernt, die Kellerspitze Y wird durch ein Wort<br />

β ∈ Γ ∗ ersetzt <strong>und</strong> das Ausgabesymbol b wird an den bisherigen Inhalt des Ausgabebandes<br />

angehängt. Die von einem Kellerautomat erkannte Sprache ist die Menge<br />

der Wörter über dem Eingabealphabet, <strong>für</strong> die es eine Folge von Schritten von der<br />

entsprechenden Anfangskonfiguration in eine Endkonfiguration gibt.<br />

Wir erwähnten bereits, daß die Ausgabe eines Parsers ein Strukturbaum sein soll, der<br />

die syntaktische Struktur des Eingabeworts (also des Quellprogramms) darstellt. Aus<br />

diesem Gr<strong>und</strong> müssen wir die Konstruktion des Strukturbaums in das Ausgabealphabet<br />

des Kellerautomaten, der den Parser realisiert, codieren. Da wir immer Linksoder<br />

Rechtsableitungen betrachten, genügt es bei der Konstruktion eines Strukturbaums,<br />

die Reihenfolge zu kennen, in der die Produktionen der Grammatik während<br />

der Analyse der Eingabe angewendet wurden. Daher ordnen wir den Produktionen<br />

der Grammatik eindeutige Nummern zu <strong>und</strong> definieren die Menge dieser Nummern<br />

als Ausgabealphabet des Kellerautomaten.<br />

Definition 20<br />

Sei π : {1, 2,...,p} → P bijektive Funktion (Numerierung der Produktionen der<br />

Grammatik).<br />

Sei z = z1z2...zn−1 ∈ {1,...,p} ∗ ,α ∈ (VN ∪ VT) ∗ .<br />

z heißt Linksanalyse von α, wenn S = ϕ1,ϕ2,...,ϕn = α Linksableitung von α <strong>und</strong><br />

ϕi produziert ϕi+1 mit Regel zi.<br />

z heißt Rechtsanalyse von α, wenn S = ϕn,ϕn−1,...,ϕ1 = α Rechtsableitung von α<br />

<strong>und</strong> ϕi+1 produziert ϕi mit Regel zi.<br />

Eine Linksanalyse ist eine Ableitung S z1 z2<br />

⇒ ϕ2 ⇒ ... zn−2<br />

⇒ ϕn−1 ⇒ α.<br />

Eine Rechtsanalyse ist eine Ableitung S zn−1 zn−2<br />

⇒ ϕn−1 ⇒ ... z2 z1<br />

⇒ ϕ2 ⇒ α, enthält also die<br />

Regelnummern einer Rechtsableitung in umgekehrter Reihenfolge.<br />

Wir nehmen im folgenden an, daß die Produktionen der Grammatiken mit Regelnummern<br />

versehen sind.<br />

Es gibt zwei verschiedene Verfahren zur Analyse eines Wortes mit Kellerautomaten:<br />

• “Top-Down”: Dieses Verfahren entspricht der Konstruktion eines Strukturbaums<br />

von der Wurzel ausgehend. Diese Konstruktion realisiert eine Linksableitung,<br />

daher ist die Ausgabe des Parsers eine Linksanalyse.<br />

• “Bottom-Up”: Dieses Verfahren entspricht der Konstruktion eines Strukturbaums<br />

von den Blättern ausgehend. Diese Konstruktion realisiert eine Rechtsableitung<br />

in umgekehrter Reihenfolge; die Ausgabe des Parsers ist also eine<br />

Rechtsanalyse.<br />

In den folgenden Abschnitten werden die beiden Parse-Verfahren erläutert.<br />

48<br />

zn−1


3.3 Top-Down-Syntaxanalyse<br />

3.3 Top-Down-Syntaxanalyse<br />

Zunächst konkretisieren wir den in Definition 18 eingeführten Kellerautomaten <strong>für</strong><br />

die Top-Down-Analyse.<br />

Notation: Für ein Alphabet Σ sei Σ ≤k = �<br />

0≤i≤k Σ i die Menge der Wörter mit der<br />

maximalen Länge k (einschließlich ε).<br />

Definition 21<br />

Ein (nichtdeterministischer) Top-Down-Analyseautomat <strong>für</strong> eine kontextfreie Grammatik<br />

G = (VN,VT,P,S) ist ein Kellerautomat mit<br />

• Eingabealphabet VT,<br />

• Kelleralphabet Γ = VN ∪ VT,<br />

• Kellerstartsymbol S,<br />

• Ausgabealphabet {1,...,p},<br />

• Übergangsrelation ∆ ⊆ ((VT ∪ {ε}) × Γ) × (Γ ∗ × {1,...,p} ≤1 ), wobei<br />

((ε,A), (α,i)) ∈ ∆ :⇔ π(i) = A → α <strong>und</strong><br />

((a,a), (ε,ε)) ∈ ∆ <strong>für</strong> alle a ∈ VT.<br />

Die Übergangsrelation ∆ enthält zwei Arten von Regeln:<br />

1. Die Regeln der Form ((ε,A), (α,i)) beschreiben die Anwendung der i-ten Produktion<br />

der Grammatik. Befindet sich das Nichtterminal A der Produktion<br />

A → α auf dem Keller, wird dieses Symbol vom Keller entfernt <strong>und</strong> durch die<br />

rechte Seite α der Produktion ersetzt, so daß das erste Zeichen von α die neue<br />

Kellerspitze darstellt. Dabei wird kein Zeichen aus der Eingabe gelesen. Auf das<br />

Ausgabeband wird die Nummer i der ausgewählten Produktion geschrieben.<br />

2. Für jedes Terminalsymbol a ∈ VT wird eine Regel der Form ((a,a), (ε,ε)) in die<br />

Übergangsrelation aufgenommen. Ist das aktuelle Symbol der Eingabe gleich<br />

der aktuellen Kellerspitze, werden beide Zeichen entfernt <strong>und</strong> das Ausgabeband<br />

bleibt unverändert, da keine Produktion der Grammatik angewendet wurde.<br />

Diese beiden Arten von Regeln definieren die Arbeitsweise des Top-Down-Parsers.<br />

Mit den Regeln unter 1) stellt der Parser Hypothesen auf, wie der folgende Abschnitt<br />

des Programmtextes aussieht, indem er Nichtterminale auf dem Keller durch<br />

die zugehörigen rechten Regelseiten ersetzt. Mit den unter 2) beschriebenen Regeln<br />

werden diese Hypothesen überprüft, indem übereinstimmende Zeichen der Eingabe<br />

<strong>und</strong> des Kellers paarweise gelöscht werden. Ist die Eingabe komplett gelesen <strong>und</strong> der<br />

Keller leer, d.h. jedes Symbol der Eingabe konnte einem Kellersymbol zugeordnet<br />

werden, akzeptiert der Automat die Eingabe als Wort der zu erkennenden Sprache.<br />

49


3 Syntaktische Analyse<br />

Eingabe Kellerinhalt Ausgabe<br />

- ( id + id ) E<br />

- ( id + id ) - E 3<br />

( id + id ) E ε<br />

( id + id ) ( E ) 2<br />

id + id ) E ) ε<br />

id + id ) E A E ) 1<br />

id + id ) id A E ) 4<br />

+ id ) A E ) ε<br />

+ id ) + E ) 5<br />

id ) E ) ε<br />

id ) id ) 4<br />

) ) ε<br />

ε ε ε<br />

Tabelle 3.1: Beispielableitung eines Top-Down-Parsers.<br />

Beispiel 19 (Top-Down-Analyse arithmetischer Ausdrücke)<br />

Gegeben sei die folgende Grammatik zur Definition vereinfachter arithmetischer Ausdrücke<br />

mit Nummerierung der Produktionen:<br />

E → E A E (1)<br />

| ( E ) (2)<br />

| - E (3)<br />

| id (4)<br />

A → + (5)<br />

| - (6)<br />

| * (7)<br />

| / (8)<br />

| ↑ (9)<br />

Eine Top-Down-Ableitung des Satzes -(id+id) ist in Tabelle 3.1 angegeben. Die linke<br />

Spalte enthält die noch nicht gelesene Eingabe, die mittlere den aktuellen Kellerinhalt<br />

(mit der Kellerspitze links) <strong>und</strong> die rechte das Zeichen, das in dem jeweiligen Schritt<br />

auf das Ausgabeband geschrieben wird. Dieses Zeichen ist entweder die Nummer der<br />

angewendeten Produktion oder ε, falls das Symbol aus der Eingabe der aktuellen<br />

Kellerspitze entspricht <strong>und</strong> beide Symbole daher gelöscht werden können.<br />

Aufgr<strong>und</strong> des Nichtdeterminismus könnten auch jeweils andere Reihenfolgen der Produktionen<br />

bzw. andere Produktionen gewählt werden, wodurch das Eingabewort gegebenenfalls<br />

nicht erkannt werden würde.<br />

50


3.3 Top-Down-Syntaxanalyse<br />

Die Symbole in der Ausgabe-Spalte der Tabelle bilden eine Linksanalyse des Satzes<br />

-(id+id):<br />

E 3 ⇒ - E 2 ⇒ - ( E ) 1 ⇒ - ( E A E ) 4 ⇒ - ( id A E ) 5 ⇒ - ( id + E )<br />

4<br />

⇒ - ( id + id )<br />

Gemäß der Ausgabe der jeweils angewendeten Regel läßt sich der Strukturbaum<br />

schrittweise konstruieren (siehe Abbildung 3.8).<br />

3.3.1 LL(k)-Grammatiken<br />

Die Auswahl der anzuwendenden Produktion in einer Konfiguration ist aufgr<strong>und</strong> des<br />

Nichtdeterminismus nicht eindeutig, so daß die “richtige” Produktion unter mehreren<br />

Alternativen geraten werden muß. Eine deterministische Auswahl könnte durch die<br />

Integration von backtracking in den Parser erreicht werden. Allerdings ist backtracking<br />

unter dem Gesichtspunkt der Effizienz ungeeignet.<br />

Eine Ursache <strong>für</strong> den Nichtdeterminismus ist, daß die Eingabe erst nach der Auswahl<br />

einer Produktion mit dem Inhalt des Stacks verglichen wird. Daher werden wir Verfahren<br />

betrachten, die bei der Auswahl einer Produktion einen begrenzten Ausschnitt<br />

der Eingabe berücksichtigen <strong>und</strong> auf diese Weise die Auswahl deterministisch treffen<br />

können.<br />

Das Konzept der LL(k)-Grammatiken beruht auf dieser Berücksichtigung eines Teils<br />

der Eingabe bei der Auswahl der Produktionen. Das Vorausschauen in der Eingabe<br />

wird lookahead genannt. Bei einer LL(k)-Grammatik genügt ein lookahead von k<br />

Symbolen in der Eingabe, um beim deterministischen Top-Down-Parseverfahren die<br />

Produktionen auszuwählen (das erste L bedeutet, daß die Eingabe von links gelesen<br />

wird, das zweite L, daß eine Linksableitung konstruiert wird). Die LL(k)-Sprachen<br />

sind mit einer Top-Down-Analyse deterministisch zu erkennen. Allerdings bilden die<br />

LL(k)-Sprachen nur eine Teilmenge der kontextfreien Sprachen. Nicht alle kontextfreien<br />

Sprachen können durch LL(k)-Grammatiken beschrieben werden.<br />

Definition 22<br />

Sei Σ Alphabet, w = a1...an ∈ Σ ∗ (n ≥ 0), k ≥ 0.<br />

ist der k-Präfix von w.<br />

k : w =<br />

�<br />

w falls n ≤ k<br />

a1...ak sonst<br />

Definition 23<br />

Sei G = (VN,VT,P,S) kontextfreie Grammatik, k ≥ 0. G ist eine LL(k)-Grammatik,<br />

wenn gilt:<br />

51


3 Syntaktische Analyse<br />

1<br />

4<br />

E<br />

E<br />

- E<br />

- E<br />

-<br />

-<br />

E<br />

( E ) ( E ) ( E )<br />

E A E<br />

E A E<br />

E A<br />

E<br />

E<br />

( E )<br />

E A E<br />

id + id<br />

E<br />

3 2<br />

- E<br />

-<br />

4 5<br />

id id +<br />

E<br />

E<br />

E<br />

( E )<br />

Abbildung 3.8: Konstruktion des Strukturbaums anhand der Ausgabe des Parsers.<br />

52<br />

E<br />

E


S ⇒ ∗ l u Y α<br />

⇒l u β α ⇒ ∗ l u x<br />

⇒l u γ α ⇒ ∗ l u y<br />

3.3 Top-Down-Syntaxanalyse<br />

<strong>und</strong> k : x = k : y, dann β = γ (d.h. die Alternative der Auswahl der Produktion <strong>für</strong><br />

jedes Nichtterminal Y ist durch die ersten k Zeichen der restlichen Eingabe eindeutig<br />

festgelegt (bei festem Linkskontext u)).<br />

Beispiel 20<br />

Die Grammatik G1, gegeben durch die Produktionen<br />

stmt → if id then stmt else stmt fi<br />

| while id do stmt od<br />

| begin stmt end<br />

| id : = id<br />

ist eine LL(1)-Grammatik (wir betrachten in diesem Beispiel das Zuweisungssymbol<br />

:= als zwei separate Symbole : <strong>und</strong> =).<br />

Wann immer stmt in einer Ableitung auftritt, ist aufgr<strong>und</strong> des nächsten Eingabesymbols<br />

klar, welche Produktion anzuwenden ist. Die abzuleitenden Folgen von Terminalsymbolen<br />

beginnen alle mit unterschiedlichen Symbolen (if, while, begin oder<br />

id), so daß mit einem lookahead von 1 die anzuwendende Produktion eindeutig zu<br />

bestimmen ist.<br />

Eine solche Grammatik, bei der die rechten Seiten der Produktionen zu einem Nichtterminalsymbol<br />

alle mit unterschiedlichen Terminalsymbolen beginnen, heißt einfache<br />

LL(1)-Grammatik.<br />

Definition 24<br />

Sei G kontextfreie Grammatik ohne ε-Produktionen. Beginnt <strong>für</strong> jedes Nichtterminal<br />

N jede seiner Alternativen mit einem anderen Terminalsymbol, dann heißt G einfache<br />

LL(1)-Grammatik.<br />

Einerseits ist die Zugehörigkeit einer Grammatik zur Klasse der einfachen LL(1)-<br />

Grammatiken sehr einfach zu überprüfen, andererseits sind diese Grammatiken <strong>für</strong><br />

die Praxis i. allg. zu eingeschränkt.<br />

Beispiel 21 (Fortsetzung von Beispiel 20)<br />

Wir erweitern G1 zu G2 durch die zusätzlichen Produktionen<br />

stmt → id : stmt (* markierte Anweisung *)<br />

| id ( id ) (* Prozeduraufruf *)<br />

G2 ist nicht mehr einfach LL(1), denn mehrere Alternativen <strong>für</strong> stmt beginnen nun<br />

mit dem Terminalsymbol id.<br />

G2 ist auch nicht mehr LL(1):<br />

53


3 Syntaktische Analyse<br />

⇒l u id : = id α ⇒ ∗ l u x<br />

stmt ⇒ ∗ l u stmt α ⇒l u id : stmt α ⇒ ∗ l u y<br />

⇒l u id ( id ) α ⇒ ∗ l u z<br />

In den drei Satzformen ist der Anfang “u id” gleich, so daß nicht durch einen lookahead<br />

von 1 entschieden werden kann, welche Produktion anzuwenden ist.<br />

G2 ist auch keine LL(2)-Grammatik, da in den oberen beiden Satzformen die Anfangsstücke<br />

“u id :” identisch sind <strong>und</strong> daher auch durch einen lookahead von 2 die<br />

Auswahl nicht deterministisch getroffen werden kann. Wenn wir jedoch den Zuweisungsoperator<br />

:= als ein einzelnes Symbol betrachten, ist G2 eine LL(2)-Grammatik.<br />

Es gibt kontextfreie Grammatiken, die keine LL(k)-Grammatiken sind, auch wenn<br />

wir k beliebig groß wählen. Manchmal kann man eine solche Grammatik in eine<br />

äquivalente LL(k)-Grammatik transformieren. Allerdings gibt es auch kontextfreie<br />

Grammatiken, <strong>für</strong> die es keine äquivalente LL(k)-Grammatik <strong>für</strong> irgendein k gibt.<br />

Für praktische Zwecke, d.h. <strong>für</strong> die Beschreibung der Syntax der bekannten Programmiersprachen,<br />

gibt es fast immer LL(1)- oder LL(2)-Grammatiken.<br />

Wie können wir überprüfen, ob eine Grammatik LL(k) ist, ohne alle möglichen Ableitungen<br />

zu betrachten?<br />

Ein Hilfsmittel hierzu sind die sogenannten FIRST-Mengen. Diese Mengen beschreiben,<br />

mit welchen Anfangsstücken aus Satzformen herleitbare Terminalworte beginnen<br />

können.<br />

Definition 25<br />

Sei G = (VN,VT,P,S) kontextfreie Grammatik, k ∈ INI.<br />

FIRSTk : (VN ∪ VT) ∗ → P(V ≤k<br />

T ) ist definiert durch<br />

FIRSTk(α) = {k : w | α ⇒ ∗ w}.<br />

FIRSTk(α) ist also die Menge der Anfangsstücke der Länge k von Terminalwörtern,<br />

die aus α ableitbar sind. Falls kürzere Wörter ableitbar sind, dann sind diese auch<br />

in der FIRST-Menge enthalten, insbesondere eventuell auch ε.<br />

Mit Hilfe der FIRST-Mengen läßt sich die Klasse der LL(k)-Sprachen wie folgt<br />

charakterisieren:<br />

Satz 4 (LL(k)-Charakterisierung)<br />

Sei G = (VN,VT,P,S) kontextfreie Grammatik, k ≥ 0. G ist genau dann LL(k),<br />

wenn gilt:<br />

Sind A → β,A → γ verschiedene Produktionen in P, dann FIRSTk(βα)∩FIRSTk(γα) =<br />

∅ <strong>für</strong> alle α, u mit S ⇒ ∗ l uAα.<br />

54


3.3 Top-Down-Syntaxanalyse<br />

Beweis<br />

“⇒”:<br />

Annahme: G ist LL(k) <strong>und</strong> es existiert x ∈ FIRSTk(βα) ∩ FIRSTk(γα). Nach<br />

Definition 25 <strong>und</strong> da G reduziert:<br />

S ⇒ ∗ l u A α<br />

⇒l u β α ⇒ ∗ l u x y<br />

⇒l u γ α ⇒ ∗ l u x z<br />

(xy <strong>und</strong> xz bis zur Länge k oder ganz identisch; falls Länge von x ≤ k, dann y =<br />

z = ε).<br />

Da aber β �= γ (da A → β, A → γ verschiedene Produktionen), wäre G nicht LL(k),<br />

was aber ein Widerspruch zur Annahme darstellt.<br />

“⇐”:<br />

Annahme: G ist nicht LL(k) <strong>und</strong> FIRSTk(βα) ∩ FIRSTk(γα) = ∅ <strong>für</strong> alle α mit<br />

S ⇒ ∗ l uAα. Dann gibt es (da G reduziert)<br />

S ⇒ ∗ l u A α<br />

⇒l u β α ⇒ ∗ l u x<br />

⇒l u γ α ⇒ ∗ l u y<br />

mit k : x = k : y, wobei A → β,A → γ verschiedene Produktionen. Aber k : x = k :<br />

y ∈ FIRSTk(βα) ∩ FIRSTk(γα), was einen Widerspruch zur Annahme darstellt.<br />

�<br />

Aus dieser Charakterisierung sind weitere Kriterien <strong>für</strong> die Zugehörigkeit zu LL(k)<br />

ableitbar. Wir betrachten hier nur den <strong>für</strong> die Praxis besonders wichtigen Fall k = 1.<br />

Sei k = 1 <strong>und</strong> G eine kontextfreie Grammatik ohne ε-Produktionen. Dann sind<br />

FIRST1(βα) ∩ FIRST1(γα) allein aus β <strong>und</strong> γ bestimmbar (da β <strong>und</strong> γ aufgr<strong>und</strong><br />

der fehlenden ε-Produktionen nicht leer werden können <strong>und</strong> daher mindestens ein<br />

Zeichen generieren).<br />

Satz 5<br />

Sei G kontextfreie Grammatik ohne ε-Produktionen. G ist LL(1) genau dann, wenn<br />

<strong>für</strong> jedes Nichtterminal A mit A → α1 | ... | αn (A hat genau die Alternativen α1,<br />

..., αn) gilt: FIRST1(α1), ..., FIRST1(αn) paarweise disjunkt.<br />

Da ε-Produktionen in Grammatiken von Programmiersprachen üblicherweise auftreten,<br />

bedeutet das generelle Verbot von ε-Produktionen eine zu starke Einschränkung.<br />

Durch das Zulassen dieser Produktionen ist das Kriterium aus Satz 5 nicht mehr ausreichend.<br />

Dies wird im folgenden Beispiel verdeutlicht.<br />

Beispiel 22<br />

Wir betrachten eine kontextfreie Grammatik G mit folgenden Produktionen:<br />

55


3 Syntaktische Analyse<br />

Strukturbaum lookahead<br />

S a<br />

S<br />

A a<br />

A<br />

a<br />

S<br />

a<br />

Abbildung 3.9: Fehlerhafte Konstruktion eines Strukturbaums <strong>für</strong> die Eingabe a.<br />

S → A a<br />

A → a | ε<br />

Offensichtlich sind die FIRST1-Mengen der Alternativen der Nichtterminale disjunkt.<br />

Aber G ist nicht LL(1) gemäß Satz 4, denn mit α = a, β = a <strong>und</strong> γ = ε gilt:<br />

FIRST1(βα) ∩ FIRST1(γα) = FIRST1(aa) ∩ FIRST1(εa) = {a} �= ∅.<br />

Was würde beim Top-Down-Parsen passieren? In Abbildung 3.9 wird die schrittweise<br />

Konstruktion des Strukturbaums <strong>für</strong> das Eingabewort a dargestellt. Im ersten Schritt<br />

wird die erste Produktion angewendet. Diese Auswahl ist eindeutig, da das Startsymbol<br />

nur eine Alternative besitzt. Im zweiten Schritt muß das A abgeleitet werden.<br />

Dieses Nichtterminal besitzt zwei Alternativen. Durch einen Vergleich mit dem lookahead,<br />

der das a enthält, kann sich der Parser <strong>für</strong> die erste Alternative von A<br />

entscheiden. Der so konstruierte Syntaxbaum entspricht aber nicht dem Eingabewort<br />

a, sondern dem Satz aa. Der Parser hätte das A zu ε ableiten müssen.<br />

Das Problem bei der deterministischen Auswahl der Produktionen von A entsteht,<br />

da in der Produktion S → Aa das a auf A folgt. Betrachten wir noch einmal den<br />

Charakterisierungssatz (Satz 4) mit k = 1. Angenommen, es gilt β ⇒ ∗ ε. Falls<br />

auch γ ⇒ ∗ ε gilt, ist die Grammatik nicht LL(k), da der Parser durch keinen lookahead<br />

eine Entscheidung treffen kann. Wenn γ ⇒ ∗ ε nicht gilt, muß die Bedingung<br />

FIRST1(α) ∩ FIRST1(γ) = ∅ gelten <strong>für</strong> alle S ⇒ ∗ l uAα. Wir betrachten also alle<br />

möglichen Anfänge von dem, was auf A folgen kann. Hierzu definieren wir die sogenannten<br />

FOLLOW-Mengen.<br />

56<br />

a


Definition 26<br />

Sei G = (Vn,VT,P,S) kontextfreie Grammatik, k ∈ INI.<br />

FOLLOWk : (VN ∪ VT) ∗ → P(V ≤k<br />

T ) ist definiert durch<br />

FOLLOWk(α) = {w | S ⇒ ∗ βαγ <strong>und</strong> w ∈ FIRSTk(γ)}.<br />

3.3 Top-Down-Syntaxanalyse<br />

Insbesondere werden wir FOLLOW-Mengen <strong>für</strong> einzelne Nichtterminale betrachten.<br />

Für k = 1 bezeichnet FOLLOW1(A) die Menge der Terminale a, die in einer Ableitung<br />

auf das Nichtterminal A folgen können. Hierbei ist zu beachten, daß zwischen<br />

A <strong>und</strong> a während der Ableitung auch Nichtterminale gestanden haben können, die<br />

zu ε abgeleitet wurden.<br />

Mit Hilfe der FOLLOW1-Mengen können wir Satz 5 auf Grammatiken mit ε-Produktionen<br />

erweitern.<br />

Satz 6<br />

Eine kontextfreie Grammatik G ist genau dann LL(1), wenn <strong>für</strong> alle Alternativen<br />

A → α1 | ... | αn gilt:<br />

1. FIRST1(α1), ..., FIRST1(αn) paarweise disjunkt (insbesondere enthält nur<br />

eine dieser Mengen ε),<br />

2. falls αi ⇒ ∗ ε, dann FIRST1(αj) ∩ FOLLOW1(A) = ∅ <strong>für</strong> alle<br />

1 ≤ j ≤ n,j �= i.<br />

Wir geben nun die Algorithmen <strong>für</strong> die Berechnung von FIRST1- <strong>und</strong> FOLLOW1-<br />

Mengen an.<br />

Berechnung von FIRST1-Mengen: Sei G = (VN,VT,P,S) kontextfreie Grammatik.<br />

• Sei X ∈ VN ∪ VT. FIRST1(X) ist die kleinste Menge mit<br />

1. falls X Terminal, dann FIRST1(X) = {X},<br />

2. falls X → ε ∈ P, dann ε ∈ FIRST1(X),<br />

3. falls X → Y1Y2...Yk ∈ P:<br />

– Terminalsymbol a ∈ FIRST1(X), wenn a ∈ FIRST1(Yi) <strong>und</strong> ε ∈<br />

FIRST1(Yj) <strong>für</strong> 1 ≤ j < i,<br />

– ε ∈ FIRST1(X), wenn ε ∈ FIRST1(Yj) <strong>für</strong> 1 ≤ j ≤ k.<br />

• Sei α ∈ (VN ∪ VT) ∗ , α = X1X2...Xn. FIRST1(α) ist die kleinste Menge mit<br />

– FIRST1(X1)\{ε} ⊆ FIRST1(α),<br />

– falls ε ∈ FIRST1(X1), dann FIRST1(X2)\{ε} ⊆ FIRST1(α),<br />

– ...<br />

57


3 Syntaktische Analyse<br />

– falls ∀i, 1 ≤ i ≤ n − 1 : ε ∈ FIRST1(Xi), dann FIRST1(Xn)\{ε} ⊆<br />

FIRST1(α),<br />

– falls ∀i, 1 ≤ i ≤ n : ε ∈ FIRST1(Xi), dann ε ∈ FIRST1(α) (beachte, daß<br />

hier insbesondere FIRST1(ε) = {ε} folgt).<br />

Berechnung von FOLLOW1-Mengen: Sei G = (VN,VT,P,S) kontextfreie Grammatik.<br />

Zur Berechnung der FOLLOW1-Mengen werden simultan <strong>für</strong> alle Nichtterminale<br />

der Grammatik folgende Schritte durchgeführt:<br />

1. ε ∈ FOLLOW1(S),<br />

2. A → αBβ ∈ P, a ∈ FIRST1(β), dann a ∈ FOLLOW1(B) <strong>für</strong> a ∈ VT.<br />

3. A → αBβ ∈ P <strong>und</strong> ε ∈ FIRST1(β) (d.h. β ⇒ ∗ ε, evtl. sogar β = ε),<br />

dann FOLLOW1(A) ⊆ FOLLOW1(B).<br />

Manchmal wird ein spezielles Symbol $ �∈ VT verwendet, das an das Eingabewort<br />

angehängt wird <strong>und</strong> während der Analyse zur Erkennung des Eingabeendes dient. In<br />

diesem Fall wird Regel 1 durch $ ∈ FOLLOW(S) ersetzt.<br />

Im folgenden lassen wir den Index 1 bei den FIRST1- <strong>und</strong> FOLLOW1-Mengen weg.<br />

Beispiel 23 (Fortführung von Beispiel 22)<br />

Wir betrachten die Grammatik aus Beispiel 22 mit den folgenden Produktionen:<br />

S → A a<br />

A → a | ε<br />

• Berechnung von FIRST(S):<br />

Wir betrachten S → Aa <strong>und</strong> stellen fest, daß wir die Menge FIRST(A) benötigen.<br />

Aufgr<strong>und</strong> der Regeln <strong>für</strong> ein Nichtterminal A erhalten wir: a ∈ FIRST(A)<br />

<strong>und</strong> ε ∈ FIRST(A). Da a ∈ FIRST(A), gilt auch a ∈ FIRST(S). Wegen<br />

ε ∈ FIRST(A) muß auch die FIRST-Menge des a in S → Aa in die FIRST-<br />

Menge von S aufgenommen werden. Da FIRST(a) = {a}, ε /∈ FIRST(a) <strong>und</strong><br />

a bereits in FIRST(S), bleibt FIRST(S) unverändert: FIRST(S) = {a}.<br />

• Berechnung der FOLLOW-Mengen:<br />

Nach Regel 1 gilt ε ∈ FOLLOW(S). Nach Regel 2 gilt a ∈ FOLLOW(A), da A<br />

in S → Aa auftritt <strong>und</strong> a ∈ FIRST(a). Weitere Auftreten von Nichtterminalen<br />

in rechten Regelseiten kommen nicht vor. Die Regel 3 kann nicht angewendet<br />

werden, also gilt FOLLOW(A) = {a}.<br />

Damit gilt a ∈ FIRST(a) ∩ FOLLOW(A) <strong>und</strong> somit ist die zweite Bedingung von<br />

Satz 6 nicht erfüllt. Also ist die Grammatik nicht LL(1).<br />

58


3.3.2 Transformierung von Grammatiken<br />

3.3 Top-Down-Syntaxanalyse<br />

Es gibt zwei Klassen von kontextfreien Grammatiken, die in der Praxis zur Definition<br />

der Syntax von Programmiersprachen verwendet werden, aber nicht LL(k) sind <strong>für</strong><br />

beliebiges k. Dabei handelt es sich zum einen um mehrdeutige Grammatiken <strong>und</strong><br />

zum anderen um linksrekursive Grammatiken.<br />

Mehrdeutige Grammatiken wurden bereits in Abschnitt 3.1.4 eingeführt.<br />

Satz 7<br />

Sei G kontextfreie Grammatik. Ist G mehrdeutig, ist G nicht LL(k) <strong>für</strong> jedes k ≥ 0.<br />

Beweis<br />

Der Beweis folgt unmittelbar aus der Definition von LL(k). Wenn ein Terminalwort<br />

durch unterschiedliche Linksableitungen zu erkennen ist, kann die Auswahl zwischen<br />

Alternativen nicht durch irgendeinen lookahead entschieden werden. �<br />

Linksrekursive Grammatiken<br />

Definition 27<br />

Sei G kontextfreie Grammatik.<br />

Eine Produktion von G heißt direkt rekursiv, wenn sie die Form A → αAβ hat. Sie<br />

heißt direkt linksrekursiv, wenn α = ε, direkt rechtsrekursiv, wenn β = ε.<br />

Ein Nichtterminal A heißt rekursiv, wenn es eine Ableitung A ⇒ + αAβ gibt (⇒ + ist<br />

die transitive Hülle von ⇒, d.h. es muß mindestens ein Ableitungsschritt durchgeführt<br />

worden sein). A heißt linksrekursiv, wenn α = ε, rechtskursiv, wenn β = ε.<br />

G heißt linksrekursiv, wenn G mindestens ein linksrekursives Nichtterminal enthält,<br />

rechtsrekursiv, wenn G mindestens ein rechtsrekursives Nichtterminal enthält.<br />

Satz 8<br />

Sei G kontextfreie Grammatik. Ist G linksrekursiv, ist G nicht LL(k) <strong>für</strong> jedes k ≥ 0.<br />

Beweisskizze<br />

Wir betrachten hier nur den Fall k = 1 <strong>und</strong> die direkte Linksrekursion.<br />

G sei eine direkt linksrekursive Grammatik, d.h. G enthält eine Produktion der Form<br />

A → Aβ. Da G reduziert ist, gibt es außerdem eine weitere Produktion A → γ des<br />

Nichtterminals A. Wir unterscheiden zwei Fälle:<br />

• FIRST(γ) �= {ε}. Dann gilt FIRST(γ) ∩ FIRST(Aβ) �= ∅, da Aβ nach γβ<br />

abgeleitet werden kann. Nach Bedingung 1 von Satz 4 ist G daher keine LL(1)-<br />

Grammatik.<br />

• FIRST(γ) = {ε}. Dann muß nach Bedingung 1 von Satz 6 gelten, daß ε �∈<br />

FIRST(Aβ). Nach Bedingung 2 muß gelten: FIRST(Aβ)∩FOLLOW(A) = ∅.<br />

59


3 Syntaktische Analyse<br />

Es gilt aber: ∃a ∈ FIRST(β) (da sonst ε ∈ FIRST(Aβ)) <strong>und</strong> a ∈ FOLLOW(A),<br />

also ist a ∈ FIRST(Aβ) (da ε ∈ FIRST(γ) <strong>und</strong> daher auch ε ∈ FIRST(A)).<br />

Also ist G nicht LL(1).<br />

�<br />

Ein Beispiel, in dem Linksrekursion auftritt, ist die Definition der Syntax arithmetischer<br />

Ausdrücke.<br />

Beispiel 24<br />

Die folgende Grammatik beschreibt arithmetische Ausdrücke mit den Operatoren +<br />

<strong>und</strong> ∗ unter Berücksichtigung der korrekten Operatorprioritäten:<br />

E → E + T | T<br />

T → T * F | F<br />

F → ( E ) | id<br />

Die Nichtterminale E <strong>und</strong> T sind linksrekursiv. Daher ist diese Grammatik <strong>für</strong> kein<br />

k ≥ 0 eine LL(k)-Grammatik.<br />

Linksrekursion kann durch Transformation der Grammatik in eine äquivalente Grammatik<br />

eliminiert werden. Wir erläutern diese Transformation am Beispiel der direkten<br />

Linksrekursion.<br />

Transformation einer linksrekursiven in eine rechtsrekursive Grammatik<br />

Gegeben seien die Produktionen A → Aα|β (in Beispiel 24 etwa A = E,α = +T,β =<br />

T). Diese Produktionen erzeugen einen Strukturbaum wie in der oberen Hälfte von<br />

Abbildung 3.10 angegeben.<br />

Wir ersetzen die obigen Produktionen <strong>für</strong> A durch<br />

A → β A’<br />

A’ → α A’ | ε<br />

mit dem neuen Nichtterminal A ′ . Diese neue Grammatik ist rechtsrekursiv, was auf<br />

die LL(k)-Eigenschaft keinen Einfluß hat. Der entsprechende Strukturbaum ist in<br />

ebenfalls in Abbildung 3.10 dargestellt.<br />

Durch diese Transformation entstehen zusätzliche ε-Produktionen.<br />

Beispiel 25 (Fortsetzung von Beispiel 24)<br />

Die Transformation der Grammatik aus Beispiel 24 ergibt die folgende rechtsrekursive<br />

Grammatik:<br />

60


A<br />

A<br />

A<br />

β α α α<br />

A<br />

A’<br />

A’<br />

A<br />

A’<br />

3.3 Top-Down-Syntaxanalyse<br />

A’<br />

β α α α ε<br />

Abbildung 3.10: Transformation in rechtsrekursive Grammatik.<br />

E → T E’<br />

E’ → + T E’ | ε<br />

T → F T’<br />

T’ → * F T’ | ε<br />

F → ( E ) | id<br />

Linksfaktorisierung<br />

Eine weitere Möglichkeit, Grammatiken, die nicht die LL(k)-Eigenschaft besitzen,<br />

in eine äquivalente LL(k)-Grammatik zu transformieren, ist die Linksfaktorisierung.<br />

Dieses Verfahren beruht auf der Idee, die Entscheidung über die Auswahl der Regel<br />

hinauszuschieben. Dabei werden die gleichen Anfänge der Produktionen eines<br />

Nichtterminals zu einer Produktion zusammengefaßt. Produktionen der Form<br />

werden ersetzt durch<br />

A → αβ1 | αβ2<br />

61


3 Syntaktische Analyse<br />

A → αA ′<br />

A ′ → β1 | β2<br />

Der folgende Algorithmus führt eine Linksfaktorisierung durch.<br />

Algorithmus Linksfaktorisierung.<br />

Eingabe: Grammatik G<br />

Ausgabe: Äquivalente linksfaktorisierte Grammatik<br />

Verfahren:<br />

Für jedes Nichtterminalsymbol A: Bestimme längstes gemeinsames Präfix α<br />

zweier oder mehrerer rechter Seiten <strong>für</strong> A. Falls α �= ε, ersetze<br />

A → αβ1 | αβ2 | ... | αβn | γ<br />

(wobei γ <strong>für</strong> alle nicht mit α beginnenden rechten Seiten steht) durch<br />

A → αA ′ | γ<br />

A ′ → β1 | β2 | ... | βn<br />

(mit A ′ neues Nichtterminalsymbol). Wiederhole diese Transformation solange,<br />

bis es keine Alternativen mit gemeinsamen Präfixen mehr gibt.<br />

Beispiel 26<br />

Wir betrachten erneut das dangling-else-Problem. Wir geben die Produktionen der<br />

Grammatik in verkürzter Schreibweise an:<br />

S → i E t S | i E t S e S | a<br />

E → b<br />

Diese Grammatik ist nicht LL(1), da die ersten beiden Produktionen von S identische<br />

Präfixe i E t S besitzen <strong>und</strong> daher eine Auswahl der Alternative nicht möglich ist.<br />

Die Anwendung der Linksfaktorisierung ergibt die folgende äquivalente Grammatik:<br />

S → i E t S S’ | a<br />

S’ → e S | ε<br />

E → b<br />

In dieser Grammatik tritt das gemeinsame Präfix nur noch in einer Produktion<br />

auf, so daß bei der Auswahl der Produktionen von S mit einem lookahead von 1<br />

eine Entscheidung getroffen werden kann. Allerdings ist die neue Grammatik immer<br />

noch nicht LL(1), da <strong>für</strong> die Produktionen von S ′ gilt: ε ∈ FIRST(ε) <strong>und</strong><br />

e ∈ FIRST(eS) ∩ FOLLOW(S ′ ), da FOLLOW(S) ⊆ FOLLOW(S ′ ). Die Grammatik<br />

kann die LL(1)-Bedingung nicht erfüllen, da sie mehrdeutig ist (siehe Abschnitt<br />

3.3.2).<br />

62


Parse-Tabellen<br />

3.3 Top-Down-Syntaxanalyse<br />

Wir haben in den letzten Abschnitten gezeigt, daß mit Hilfe der LL(k)-Grammatiken<br />

eine deterministische Top-Down-Analyse der Eingabe möglich ist. Nun geben wir ein<br />

Verfahren an, wie wir zu einer gegebenen LL(1)-Grammatik einen deterministischen<br />

Parser bauen können. Hierzu werden die Parse-Tabellen verwendet.<br />

Eine Parse-Tabelle M <strong>für</strong> eine kontextfreie Grammatik G gibt an, welche Produktion<br />

<strong>für</strong> ein Nichtterminalsymbol bei einem bestimmten lookahead in Frage kommt.<br />

Die Zeilen der Tabelle sind mit den Nichtterminalen der Grammatik beschriftet, die<br />

Spalten mit den Terminalsymbolen <strong>und</strong> dem Endezeichen $. M[A,a] enthält dann die<br />

bei lookahead a anzuwendende Produktion <strong>für</strong> A bzw. error, falls keine Produktion<br />

von A unter dem aktuellen lookahead a anwendbar ist.<br />

Das folgende Verfahren wird zur Konstruktion einer LL(1)-Parse-Tabelle angewendet.<br />

Konstruktion der Parse-Tabelle zu einer Grammatik.<br />

Eingabe: Grammatik G mit FIRST- <strong>und</strong> FOLLOW-Mengen<br />

Ausgabe: Parse-Tabelle M <strong>für</strong> G<br />

Verfahren:<br />

1. Für jede Produktion A → α von G:<br />

a) Für jedes a ∈ FIRST(α),a �= ε: trage A → α in M[A,a] ein.<br />

b) Falls ε ∈ FIRST(α): <strong>für</strong> jedes b ∈ FOLLOW(A): trage A → α in<br />

M[A,b] ein (wobei b Terminalsymbol oder $).<br />

2. Trage in jedes noch <strong>und</strong>efinierte Feld error ein.<br />

Beispiel 27<br />

Wir betrachten die Grammatik aus Beispiel 25:<br />

E → T E’<br />

E’ → + T E’ | ε<br />

T → F T’<br />

T’ → * F T’ | ε<br />

F → ( E ) | id<br />

Wir konstruieren schrittweise die Parse-Tabelle aus Tabelle 3.2.<br />

Wir beginnen mit dem Startsymbol E. Im ersten Schritt berechnen wir die FIRST-<br />

Mengen der rechten Regelseiten von E, in diesem Fall also<br />

FIRST(T E ′ ) = FIRST(T) = FIRST(F T ′ ) = FIRST(F) = {(,id} (weder<br />

FIRST(T) noch FIRST(F) enthalten ε). Wir tragen also in die Spalten, die mit<br />

Terminalsymbolen aus FIRST(T E ′ ) beschriftet sind, die Produktion E → T E ′ ein.<br />

Da ε /∈ FIRST(T E ′ ), ist der Schritt (1b) <strong>für</strong> diese Produktion nicht anwendbar.<br />

63


3 Syntaktische Analyse<br />

id + * ( ) $<br />

E E → T E’ error error E → T E’ error error<br />

E’ error E’ → + T E’ error error E’ → ε E’ → ε<br />

T T → F T’ error error T → F T’ error error<br />

T’ error T’ → ε T’ → * F T’ error T’ → ε T’ → ε<br />

F F → id error error F → ( E ) error error<br />

Tabelle 3.2: Beispiel einer Parse-Tabelle.<br />

Nun betrachten wir das Nichtterminal E ′ : Für die Produktion E ′ → + T E ′ gilt:<br />

FIRST(+ T E ′ ) = {+}, also wird die Produktion im ersten Schritt in die entsprechende<br />

Spalte eingetragen.<br />

Aufgr<strong>und</strong> der Produktion E ′ → ε müssen wir die FOLLOW-Menge von E ′ nach<br />

Schritt (1b) des Verfahrens betrachten. Diese Menge sagt uns, ob der lookahead mit<br />

dem zusammenpaßt, was auf E ′ folgen kann. Es gilt: FOLLOW(E ′ ) = {), $}, daher<br />

wird die Produktion E ′ → ε in die entsprechenden Spalten der Tabelle eingetragen.<br />

Die Produktionen T → F T ′ <strong>und</strong> T ′ → ∗ F T ′ | ε werden analog behandelt. Dabei ist<br />

zu beachten, daß FOLLOW(T ′ ) = {+, ), $}.<br />

Die Behandlung der Produktionen des Nichtterminals F verläuft analog.<br />

Wir können dieses Konstruktionsverfahren auch als LL(1)-Test verwenden: Wenn ein<br />

Feld der Parse-Tabelle im Laufe des Verfahrens mehr als einen Eintrag erhält, ist die<br />

Grammatik nicht LL(1).<br />

Nachdem wir <strong>für</strong> eine LL(1)-Grammatik die zugehörige Parse-Tabelle erzeugt haben,<br />

benötigen wir noch ein Verfahren, das die deterministische Top-Down-Analyse mit<br />

Hilfe der Parse-Tabelle durchführt. Die Eingabe sei mit dem Eingabeendezeichen $<br />

abgeschlossen. Gleichzeitig verwenden wir dieses Zeichen auch als “unterstes” Symbol<br />

auf dem Keller, um einen leeren Keller anzuzeigen. Ein Parse-Verfahren <strong>für</strong> deterministische<br />

Top-Down-Analyse unter Verwendung dieser Konventionen ist im folgenden<br />

Algorithmus angegeben.<br />

Deterministische Top-Down-Analyse mit Parse-Tabelle.<br />

Eingabe: Parse-Tabelle M <strong>für</strong> Grammatik G <strong>und</strong> ein Eingabewort w<br />

Ausgabe: Linksableitung von w, falls w ∈ L(G), sonst Fehlermeldung<br />

Verfahren:<br />

Der Kellerautomat befinde sich in folgender Anfangskonfiguration: S$ liege auf<br />

dem Keller (Kellerspitze links), wobei S Startsymbol von G, w$ befinde sich<br />

im Eingabepuffer <strong>und</strong> der Eingabezeiger stehe auf dem ersten Symbol von w.<br />

Wir geben den Algorithmus in Form eines Pseudocode-Programms an:<br />

64


1 repeat<br />

3.3 Top-Down-Syntaxanalyse<br />

2 s e i X oberstes Kellersymbol <strong>und</strong> a aktuelles<br />

Eingabesymbol<br />

3 if X Terminal then<br />

4 if X = a then<br />

5 entferne X vom Keller <strong>und</strong> r”ucke Eingabezeiger<br />

vor<br />

6 else error<br />

7 else (∗ X i s t Nichtterminal ∗)<br />

8 if M[X,a] = X → Y1Y2...Yk then begin<br />

9 entferne X vom Keller , lege Yk,Yk−1,...,Y1 auf den<br />

Keller<br />

10 (Y1 neue Kellerspitze ) , gib Produktion<br />

X → Y1Y2...Yk aus<br />

11 end<br />

12 else error<br />

13 until X = $ (∗ Keller l e e r ∗)<br />

65


3 Syntaktische Analyse<br />

Beispiel 28 (Fortsetzung von Beispiel 27)<br />

Wir analysieren das Wort id + id mit Hilfe der in Tabelle 3.2 angegebenen Parse-<br />

Tabelle.<br />

Eingabe Keller Ausgabe<br />

id + id $ E $<br />

id + id $ T E ′ $ E → TE ′<br />

id + id $ F T ′ E ′ $ T → F T ′<br />

id + id $ id T ′ E ′ $ F → id<br />

+ id $ T ′ E ′ $ ε<br />

+ id $ E ′ $ T ′ → ε<br />

+ id $ + T E ′ $ E ′ → + T E ′<br />

id $ T E ′ $ ε<br />

id $ F T ′ E ′ $ T → F T ′<br />

id $ id T ′ E ′ $ F → id<br />

$ T ′ E ′ $ ε<br />

$ E ′ $ T ′ → ε<br />

$ $ E ′ → ε<br />

$ $ accept<br />

Nach dieser Analyse eines korrekten Eingabeworts analysieren wir das Eingabewort<br />

id + * id, das mit der Grammatik nicht erzeugt werden kann.<br />

Eingabe Keller Ausgabe<br />

id + * id $ E $<br />

id + * id $ T E ′ $ E → T E ′<br />

id + * id $ F T ′ E ′ $ T → F T ′<br />

id + * id $ id T ′ E ′ $ F → id<br />

+ * id $ T ′ E ′ $ ε<br />

+ * id $ E ′ $ T ′ → ε<br />

+ * id $ + T E ′ $ E ′ → + T E ′<br />

* id $ T E ′ $ ε<br />

* id $ T E ′ $ error<br />

Behandlung von mehrdeutigen Grammatiken<br />

Wir haben bereits gezeigt, daß mehrdeutige Grammatiken nicht die LL(k)-Eigenschaft<br />

besitzen <strong>und</strong> daher Eingaben mit ihnen nicht deterministisch analysierbar sind (siehe<br />

Satz 7). Als Beispiel <strong>für</strong> eine mehrdeutige Grammatik haben wir in Beispiel 18<br />

das dangling-else-Problem betrachtet. Obwohl die Grammatik <strong>für</strong> die if-then-else-<br />

Anweisung nicht LL(k) ist, wird sie in Übersetzern (z.B. <strong>für</strong> Pascal) verwendet. Wie<br />

kann man also eine mehrdeutige Grammatik parsen?<br />

66


3.3 Top-Down-Syntaxanalyse<br />

a b e i t $<br />

S S → a S → iEtSS ′<br />

S ′ S ′ → ε, S ′ → eS S ′ → ε<br />

E E → b<br />

Tabelle 3.3: Parse-Tabelle <strong>für</strong> dangling-else-Grammatik.<br />

Wir betrachten die linksfaktorisierte Grammatik <strong>für</strong> das dangling else aus Beispiel<br />

26,<br />

S → i E t S S’ | a<br />

S’ → e S | ε<br />

E → b,<br />

<strong>und</strong> konstruieren <strong>für</strong> diese Grammatik die zugehörige Parse-Tabelle M.<br />

Bei dieser Konstruktion ensteht ein doppelter Eintrag im Feld M[S ′ ,e]. Also ist<br />

die Grammatik nicht LL(1). Als pragmatische Lösung bietet sich an, die Produktion<br />

S ′ → ε aus dem Feld M[S ′ ,e] zu löschen, so daß gilt M[S ′ ,e] = {S ′ → eS}.<br />

Dies entspricht der üblichen Konvention, das else dem unmittelbar vorangehenden<br />

then zuzuordnen. Wenn wir uns in der innersten Schachtelung mehrerer if-then-<br />

Anweisungen befinden, <strong>und</strong> es steht ein else im lookahead, so soll dieses else dem<br />

innersten if zugeordnet werden.<br />

In diesem Beispiel ist durch geschickte Manipulation der Parse-Tabelle die deterministische<br />

Top-Down-Analyse ermöglicht worden. Es gibt aber keine allgemeine Regel<br />

<strong>für</strong> solche “Tricks”.<br />

3.3.3 Erweiterte kontextfreie Grammatiken<br />

Im vorherigen Abschnitt haben wir gesehen, daß linksrekursive Grammatiken nicht<br />

die LL(k)-Eigenschaft besitzen (siehe Satz 8). Bei der Beschreibung von Programmiersprachen<br />

treten linksrekursive Grammatiken aber relativ häufig auf, da sich mit<br />

ihnen “Auflistungen” sehr einfach beschreiben lassen (z.B. id+id+id in arithmetischen<br />

Ausdrücken, Anweisungsfolgen usw.). Aus diesem Gr<strong>und</strong> möchte man eine<br />

Lösung finden, die die einfache Beschreibung von Auflistungen erlaubt <strong>und</strong> trotzdem<br />

das deterministische Parsen ermöglicht. Diese Möglichkeit bieten die erweiterten<br />

kontextfreien Grammatiken. Sie beruhen auf folgender Idee: Da sich Auflistungen<br />

mit regulären Ausdrücken beschreiben lassen (durch den ∗ -Operator), lassen wir nun<br />

reguläre Ausdrücke auf der rechten Seite von Produktionen zu.<br />

Definition 28<br />

Eine erweiterte kontextfreie Grammatik ist ein Tupel G = (VN,VT,ρ,S), wobei VN,<br />

VT <strong>und</strong> S wie üblich definiert sind <strong>und</strong> ρ : VN → reg(VN ∪ VT) eine Abbildung der<br />

67


3 Syntaktische Analyse<br />

Nichtterminale in die Menge der regulären Ausdrücke über VN ∪ VT ist.<br />

Es ist möglich, ρ als Abbildung zu definieren, da verschiedene Alternativen <strong>für</strong> ein<br />

Nichtterminal mit Hilfe des Alternativoperators | in einem regulären Ausdruck zusammengefaßt<br />

werden können.<br />

Beispiel 29<br />

Betrachten wir die Grammatik S → a |b. Handelt es sich bei der Grammatik um eine<br />

“normale” kontextfreie Grammatik, besitzt das Nichtterminal S zwei Produktionen<br />

S → a <strong>und</strong> S → b, wobei das | als Trennsymbol zwischen den beiden Produktionen<br />

verwendet wird. Interpretieren wir die Grammatik als erweiterte kontextfreie Grammatik,<br />

besitzt S nur eine Produktion S → a | b, die den regulären Auswahloperator |<br />

enthält.<br />

Beispiel 30 (Arithmetische Ausdrücke)<br />

Wir geben eine erweiterte kontextfreie Grammatik <strong>für</strong> die Sprache der arithmetischen<br />

Audrücke aus Beispiel 24 an. Dabei ergänzen wir die Menge der Operatoren um −<br />

<strong>und</strong> /.<br />

S → E<br />

E → T {{ + | − } T } ∗<br />

T → F {{ ∗ | / } F } ∗<br />

F → ( E ) | id<br />

Hier verwenden wir die Zeichen { <strong>und</strong> } zur Klammerung der regulären Ausdrücke<br />

in den rechten Regelseiten der Grammatik. Die Linksrekursion der Grammatik aus<br />

Beispiel 24 wurde durch den regulären ∗ -Operator ersetzt. Ein Ausdruck (Nichtterminal<br />

E) besteht aus einem Term (Nichterminal T), dem beliebig viele (also auch<br />

keine) Terme, getrennt durch einen der Operatoren + oder −, folgen können. Die<br />

Auswahl zwischen + <strong>und</strong> − wird durch den regulären Auswahloperator | dargestellt.<br />

Analog zu Ableitungen <strong>für</strong> kontextfreie Grammatiken definieren wir Ableitungen <strong>für</strong><br />

erweiterte kontextfreie Grammatiken.<br />

Definition 29<br />

Sei G = (VN,VT,ρ,S) erweiterte kontextfreie Grammatik.<br />

Die Relation ⇒ R l ⊆ reg(VN ∪VT)×reg(VN ∪VT) (“leitet regulär links ab”) ist definiert<br />

durch<br />

68<br />

1. wXβ ⇒ R l wαβ <strong>für</strong> α = ρ(X),<br />

2. w(r1|...|rn)β ⇒ R l wriβ <strong>für</strong> 1 ≤ i ≤ n,<br />

3. w(r) ∗ β ⇒ R l wβ,<br />

w(r) ∗ β ⇒ R l wr(r) ∗ β.


S ⇒R l E<br />

⇒R l T {{+ | −}T } ∗<br />

⇒R l F {{∗ | /}F } ∗ {{+ | −}T } ∗<br />

⇒R l { ( E ) | id } {{∗ | /}F } ∗ {{+ | −}T } ∗<br />

⇒R l id {{∗ | /}F } ∗ {{+ | −}T } ∗<br />

⇒R l id {{+ | −}T } ∗<br />

⇒R l id {+ | −}T {{+ | −}T } ∗<br />

⇒R l id + T {{+ | −}T } ∗<br />

⇒R l id + F {{∗ | /}F } ∗ {{+ | −}T } ∗<br />

⇒R l id + { ( E ) | id } {{∗ | /}F } ∗ {{+ | −}T } ∗<br />

⇒R l id + id {{∗ | /}F } ∗ {{+ | −}T } ∗<br />

⇒R l id + id {∗ | /}F {{∗ | /}F } ∗ {{+ | −}T } ∗<br />

⇒R l id + id ∗ F {{∗ | /}F } ∗ {{+ | −}T } ∗<br />

⇒R l id + id ∗ { ( E ) | id } {{∗ | /}F } ∗ {{+ | −}T } ∗<br />

⇒R l id + id ∗ id {{∗ | /}F } ∗ {{+ | −}T } ∗<br />

⇒R l id + id ∗ id {{+ | −}T } ∗<br />

id + id ∗ id<br />

⇒ R l<br />

Abbildung 3.11: Beispiel einer regulären Ableitung.<br />

3.3 Top-Down-Syntaxanalyse<br />

⇒R∗ l sei die reflexive <strong>und</strong> transitive Hülle von ⇒R l . Die von G definierte Sprache ist<br />

dann L(G) = {w ∈ V ∗<br />

T | S ⇒R∗ l w}.<br />

Beispiel 31 (Fortsetzung von Beispiel 30)<br />

In Abbildung 3.11 ist die Ableitung des Satzes id+id∗id der Grammatik aus Beispiel<br />

30 angegeben.<br />

Für erweiterte kontextfreie Grammatiken gibt es analog die Klasse der erweiterten<br />

LL(k)-Grammatiken (kurz ELL(k)). Zur Berechnung der FIRST- <strong>und</strong> FOLLOW-<br />

Mengen der Symbole erweiterter kontextfreier Grammatiken kann das folgende Verfahren<br />

verwendet werden. Zuvor führen wir den Begriff der k-Konkatenation ein, der<br />

im Konstruktionsverfahren verwendet wird.<br />

Definition 30<br />

Die Abbildung ⊕k : V ∗<br />

T × V ∗<br />

T → V ≤k<br />

T , definiert durch u ⊕k v = k : uv heißt die k-<br />

Konkatenation.<br />

Analog läßt sich ⊕k auf Mengen von Wörtern definieren. Seien L,M Mengen von<br />

Wörtern. Dann ist L ⊕k M = {u ⊕k v | u ∈ L,v ∈ M}.<br />

Die k-Konkatenation von u <strong>und</strong> v berechnet den k-Präfix der Konkatenation von u<br />

<strong>und</strong> v.<br />

69


3 Syntaktische Analyse<br />

Konstruktion der FIRST- <strong>und</strong> FOLLOW-Mengen <strong>für</strong> eine ELL(1)-Grammatik.<br />

Die FIRST- <strong>und</strong> FOLLOW-Mengen <strong>für</strong> eine ELL(1)-Grammatik<br />

G = (VN,VT,P,S) werden wie folgt berechnet:<br />

• Sei X ∈ VN. FIRST(X) ist definiert durch:<br />

– Falls X → r ∈ P, wobei r regulärer Ausdruck über VN ∪ VT, dann<br />

FIRST(X) = FIRST(r).<br />

• Sei r regulärer Ausdruck über VN ∪VT. FIRST(r) ist induktiv definiert durch:<br />

– Falls r = ε, dann FIRST(r) = ε,<br />

– falls r = a mit a ∈ VT, dann FIRST(r) = {a},<br />

– falls r = r1 | r2, dann FIRST(r) = FIRST(r1) ∪ FIRST(r2),<br />

– falls r = r1r2, dann FIRST(r) = FIRST(r1) ⊕1 FIRST(r2),<br />

– falls r = (r1), dann FIRST(r) = FIRST(r1),<br />

– falls r = r ∗ 1, dann FIRST(r) = {ε} ∪ FIRST(r1).<br />

• Sei X ∈ VN. Als Hilfskonstrukt verwenden wir Fol(X,r), die Menge der Terminalwörter<br />

der Länge ≤ 1, die in einer erweiterten Satzform r – einem regulären<br />

Ausdruck über VN ∪ VT – direkt auf X folgen können.<br />

– Fol(X,ε) = ∅,<br />

– Fol(X,r) = ∅, falls X in r nicht vorkommt,<br />

– Fol(X,X) = {ε},<br />

– Fol(X,r1r2) = (Fol(X,r1) ⊕1 FIRST(r2)) ∪ Fol(X,r2),<br />

– Fol(X,r1|r2) = Fol(X,r1) ∪ Fol(X,r2),<br />

– Fol(X, (r)) = Fol(X,r),<br />

– Fol(X,r ∗ ) = Fol(X,r) ⊕1 FIRST(r ∗ ).<br />

• Nun definieren wir <strong>für</strong> X ∈ VN die Mengen FOLLOW(X) als die kleinsten<br />

Mengen mit:<br />

– Falls X = S, dann ε ∈ FOLLOW(X),<br />

– falls X → r ∈ P, dann Fol(Y,r) ⊆ FOLLOW(Y ) <strong>für</strong> alle Y ∈ VN,<br />

– falls X → r ∈ P <strong>und</strong> <strong>für</strong> ein Y gilt ε ∈ Fol(Y,r),<br />

dann FOLLOW(X) ⊆ FOLLOW(Y ).<br />

Bei den Definitionen ist zu beachten, daß ∅ ⊕1 M = ∅ <strong>für</strong> alle Mengen M.<br />

Übergangsgraphen <strong>für</strong> erweiterte kontextfreie Grammatiken<br />

Da in erweiterten kontextfreien Grammatiken die rechten Regelseiten reguläre Ausdrücke<br />

enthalten dürfen, stellen wir diese rechten Regelseiten analog zu Kapitel 2 als<br />

Übergangsgraphen dar. Diese können, wie in Kapitel 2 beschrieben, aus den regulären<br />

70


S:<br />

E:<br />

T:<br />

F:<br />

E<br />

T<br />

F<br />

ε<br />

ε<br />

T<br />

+<br />

F<br />

( E )<br />

id<br />

-<br />

*<br />

/<br />

3.3 Top-Down-Syntaxanalyse<br />

Abbildung 3.12: Übergangsgraphen <strong>für</strong> arithmetische Ausdrücke.<br />

Ausdrücken generiert werden.<br />

Beispiel 32 (Fortsetzung von Beispiel 30)<br />

In Abbildung 3.12 sind Übergangsgraphen <strong>für</strong> die rechten Regelseiten der erweiterten<br />

kontextfreien Grammatik aus Beispiel 30 zur Beschreibung der arithmetischen<br />

Ausdrücke angegeben.<br />

Wie in Abbildung 3.12 zu sehen ist, können Kanten der Übergangsgraphen auch mit<br />

Nichtterminalsymbolen beschriftet sein. Da jedem Nichtterminalsymbol ein eigener<br />

Übergangsgraph zugeordnet ist, stellt eine solche Kante eine Art von “Prozeduraufruf”<br />

des Graphen dar, der dem entsprechenden Nichtterminal zugeordnet ist. Auf<br />

diese Weise erhalten wir “rekursive endliche Automaten” (mit direkter oder indirekter<br />

Rekursion). Diese Übergangsgraphen entsprechen genau den üblichen Syntaxdiagrammen<br />

<strong>für</strong> Programmiersprachen.<br />

Syntaxdiagramme bilden die Basis <strong>für</strong> eine besonders einfache Implementierung von<br />

Parsern <strong>für</strong> LL(k)-Sprachen (insbesondere LL(1)) mit Hilfe von Programmiersprachen,<br />

die Rekursion erlauben. Diese Parser werden recursive descent-Parser genannt.<br />

Top-Down-Parsen durch “rekursiven Abstieg”<br />

Bisher haben wir Parse-Verfahren <strong>für</strong> LL(1)-Grammatiken mit Hilfe einer Datenstruktur<br />

“Keller” durch einen iterativen Algorithmus implementiert. Dieser verwen-<br />

71


3 Syntaktische Analyse<br />

det eine Parse-Tabelle, die Informationen über die nächste anzuwendende Produktion<br />

unter Berücksichtigung des lookaheads enthält.<br />

Nun betrachten wir das Verfahren der Top-Down-Analyse durch rekursiven Abstieg<br />

(recursive descent), welches die besonders einfache Implementierung eines LL(1)-<br />

Parsers erlaubt. Wir verwenden als Gr<strong>und</strong>lage dieses Verfahrens die oben erwähnten<br />

Syntaxdiagramme <strong>und</strong> erzeugen, analog zum Scanner, daraus ein Programm, welches<br />

jetzt aber rekursive Aufrufe enthält, da die Syntaxdiagramme ebenfalls Rekursion<br />

beinhalten.<br />

Für jedes Nichtterminal wird eine Prozedur nach folgendem Schema erzeugt:<br />

Beginne mit dem Anfangsknoten des Graphen. Betrachte die Ausgangskanten <strong>und</strong><br />

vergleiche das aktuelle Symbol im lookahead mit den FIRST-Mengen ihrer Beschriftungen.<br />

Eine ε-Kante wird nur dann benutzt, wenn das lookahead-Symbol in keiner<br />

dieser Mengen enthalten ist (in diesem Fall wird der lookahead mit den FOLLOW-<br />

Mengen der Beschriftungen verglichen). Wir unterscheiden die folgenden Fälle:<br />

• Falls die Kante mit einem Terminalsymbol beschriftet ist, das mit dem lookahead<br />

übereinstimmt, gehe zum Folgeknoten über (die Konstruktoren in den<br />

regulären Ausdrücken werden in Kontrollstrukturen wie sequentielle Komposition,<br />

if-, while-, repeat- <strong>und</strong> case-Anweisungen übersetzt) <strong>und</strong> fordere das<br />

nächste Symbol des Quellprogramms an.<br />

• Falls die Kante mit einem Nichtterminalsymbol beschriftet ist, rufe die entsprechende<br />

Prozedur auf.<br />

• Falls keine Kante anwendbar ist (auch keine ε-Kante), liegt ein Syntaxfehler<br />

vor.<br />

Das Programm beginnt die Syntaxanalyse durch den Aufruf der Prozedur <strong>für</strong> das<br />

Startsymbol.<br />

Beispiel 33<br />

Wir betrachten die Grammatik G zur Beschreibung einer Untermenge der Pascal-<br />

Typen mit den folgenden Produktionen:<br />

type → simple<br />

| ↑ id<br />

| array [ simple ] of type<br />

simple → integer<br />

| char<br />

| num dotdot num<br />

Für die Zeichenfolge “..” verwenden wir das Symbol dotdot, da diese Zeichenfolge<br />

als Einheit behandelt wird (wie “:=”) <strong>und</strong> somit schon vom Scanner als ein Symbol<br />

geliefert wird.<br />

Wir können G als “normale” oder als besonders einfache erweiterte kontextfreie<br />

Grammatik betrachten.<br />

72


type:<br />

simple:<br />

simple<br />

array [ simple ] of type<br />

integer<br />

char<br />

num dotdot num<br />

Abbildung 3.13: Übergangsgraphen <strong>für</strong> Pascal-Typen.<br />

3.3 Top-Down-Syntaxanalyse<br />

Die Syntaxdiagramme <strong>für</strong> G sind in Abbildung 3.13 dargestellt.<br />

Die Berechnung der FIRST-Mengen der Nichtterminalsymbole ergibt<br />

FIRST(simple) = {integer,char,num} <strong>und</strong><br />

FIRST(type) = FIRST(simple) ∪ {↑,array} =<br />

{integer,char,num, ↑,array}.<br />

Es treten keine ε-Kanten auf, also müssen die FOLLOW-Mengen nicht berücksichtigt<br />

werden. Anhand der Syntaxdiagramme können wir nun einen recursive descent-<br />

Parser implementieren (siehe Abbildung 3.14).<br />

Die Hilfsprozedur match prüft, ob ein Symbol mit dem aktuellen lookahead übereinstimmt.<br />

Ist dies der Fall, wird das nächste Symbol vom Scanner angefordert (durch<br />

den Aufruf von nexttoken) <strong>und</strong> in den lookahead geladen, andernfalls ist eine Fehlersituation<br />

eingetreten. In den Prozeduren type <strong>und</strong> simple wird die Auswahl zwischen<br />

den Ausgangskanten eines Knotens durch verschachtelte if-then-else-Anweisungen<br />

dargestellt. Als Bedingung wird dabei ein Vergleich des lookaheads mit der FIRST-<br />

Menge der jeweiligen Kantenbeschriftung verwendet. Ein Schritt, also die Verwendung<br />

einer Kante, wird durch die Prozedur match realisiert, indem sie zum einen<br />

überprüft, ob der aktuelle lookahead dem erwarteten Symbol entspricht, <strong>und</strong> zum anderen<br />

den aktuellen lookahead durch Anfordern des nächsten Symbols “verbraucht”.<br />

In Abbildung 3.15 ist der Aufrufgraph der Prozeduren des Parsers <strong>für</strong> die Analyse des<br />

Satzes array [ num dotdot num ] of integer angegeben. Die Aufrufe der Prozedur<br />

match bilden die Blätter des Aufrufgraphen, die der Prozeduren type <strong>und</strong> simple die<br />

inneren Knoten. Da jeder Aufruf von match der Überprüfung eines Terminalssymbols<br />

dient <strong>und</strong> der Aufruf von type <strong>und</strong> simple einem Nichtterminalsymbol entspricht, wird<br />

durch den Aufrufgraph der Prozeduren der Strukturbaum der Analyse implizit erzeugt.<br />

id<br />

73


3 Syntaktische Analyse<br />

1 procedure type ;<br />

2 begin<br />

3 if lookahead i s in {integer, char, num} then<br />

4 simple<br />

5 else<br />

6 if lookahead = ′ ↑ ′ then begin<br />

7 match( ′ ↑ ′ ) ; match(id ) ;<br />

8 end<br />

9 else<br />

10 if lookahead = array then begin<br />

11 match(array) ; match ( ’ [ ’ ) ; simple ;<br />

12 match ( ’ ] ’ ) ; match( of ) ; type<br />

13 end<br />

14 else error<br />

15 end;<br />

16<br />

17 procedure simple ;<br />

18 begin<br />

19 if lookahead = integer then<br />

20 match( integer )<br />

21 else<br />

22 if lookahead = char then<br />

23 match(char)<br />

24 else<br />

25 if lookahead = num then begin<br />

26 match(num) ; match(dotdot) ; match(num)<br />

27 end<br />

28 else error<br />

29 end;<br />

30<br />

31 procedure match ( t : token ) ;<br />

32 begin<br />

33 if lookahead = t then<br />

34 lookahead := nexttoken<br />

35 else error<br />

36 end };<br />

74<br />

Abbildung 3.14: Beispiel eines recursive descent-Parsers.


type<br />

match( array) match( ’[’) simple match( ’]’) match( of ) type<br />

match( num ) match( dotdot ) match( num ) simple<br />

3.3 Top-Down-Syntaxanalyse<br />

match( integer)<br />

Abbildung 3.15: Aufrufgraph einer recursive descent-Syntaxanalyse.<br />

Der recursive descent-Parser kommt ohne einen durch eine Datenstruktur explizit<br />

definierten Keller aus, da diese Aufgabe implizit vom Laufzeitsystem der Sprache, in<br />

der wir unseren Parser implementiert haben, übernommen wird.<br />

3.3.4 Fehlerbehandlung bei der Top-Down-Analyse<br />

Wir beschreiben zunächst die Fehlerbehandlung bei Parsern im allgemeinen. Die<br />

Fehlerbehandlung eines Parsers muß folgenden Kriterien genügen:<br />

• Fehler müssen zuverlässig festgestellt <strong>und</strong> durch verständliche Fehlermeldungen<br />

dem Benutzer mitgeteilt werden.<br />

• Nach einem gef<strong>und</strong>enen Fehler soll die Analyse des Quelltexts fortgeführt werden,<br />

um weitere Fehler im Programmtext entdecken zu können. Daher soll der<br />

Parser in einer Fehlersituation möglichst schnell wieder “aufsetzen”, d.h. mit<br />

der Analyse fortfahren. Dabei sollen sogenannte “Scheinfehler”, d.h. durch die<br />

Behandlung des ersten Fehlers verursachte Folgefehler, vermieden werden.<br />

• Die Fehlerbehandlung darf die schnelle Übersetzung korrekter Programme nicht<br />

behindern.<br />

Zu den Syntaxfehlern gehören<br />

• Zeichensetzungsfehler: zuviele oder zuwenige ’;’, ’;’ statt ’,’, usw.,<br />

• Operatorenfehler: z.B. ’=’ statt ’:=’,<br />

• Schlüsselwortfehler: fehlende oder falsch geschriebene Schlüsselwörter,<br />

• Fehler in der Klammerstruktur<br />

• ...<br />

Das Wiederaufsetzen (recovery) eines Parsers soll möglichst früh geschehen, d.h. es<br />

sollen möglichst wenige Symbole überlesen werden. Neben dem Wiederaufsetzen ist<br />

es durch heuristische Verfahren möglich, durch lokale Korrekturen, die der Parser<br />

vornimmt, die Analyse fortzusetzen.<br />

75


3 Syntaktische Analyse<br />

Wir betrachten verschiedene recovery-Strategien:<br />

Panic recovery: Die Idee bei dieser Strategie besteht darin, einen Teil der Eingabe<br />

nach Auftreten eines Fehlers zu überlesen <strong>und</strong> dann mit der Analyse fortzufahren.<br />

Das Ende des zu überlesenden Abschnitts der Eingabe wird durch synchronisierende<br />

Symbole angegeben. Synchronisierende Symbole sind z.B. die<br />

Symbole, die Anweisungen abschließen (etwa ’;’ oder end). Tritt während der<br />

Analyse einer Anweisung ein Fehler auf, wird der restliche Text der Anweisung<br />

überlesen (also alle Symbole bis zum nächsten synchronisierenden Symbol) <strong>und</strong><br />

die Analyse mit der nächsten Anweisung fortgesetzt. Der Nachteil dieser Methode<br />

besteht darin, daß eventuell relativ große Teile der Eingabe überlesen<br />

werden (<strong>und</strong> daher Fehler, die in diesen Abschnitten existieren, in der aktuellen<br />

Analyse nicht gef<strong>und</strong>en werden). Andererseits ist dieses Verfahren sicher,<br />

da die Eingabe stets verkürzt wird <strong>und</strong> das Verfahren daher terminiert.<br />

Konstrukt-orientierte recovery: Bei diesem Verfahren ist der Parser in der Lage,<br />

lokale Korrekturen im Quelltext beim Auftreten eines Fehlers vorzunehmen.<br />

Der Parser kann das Präfix der Eingabe durch eine modifizierte Symbolfolge<br />

ersetzen, so daß eine Fortsetzung der Analyse möglich ist. Zum Beispiel kann ein<br />

Parser, der bei der Analyse einer Zuweisung bemerkt, daß statt des Symbols ’:=’<br />

das Symbol ’=’ verwendet wurde, das ’=’ durch das ’:=’ ersetzen. Nachteilig ist<br />

bei dieser Strategie, daß die Gefahr von Endlos-Schleifen besteht, z.B. wenn der<br />

Parser immer wieder Symbole vor dem aktuellen Eingabesymbol einsetzt. Eine<br />

zusätzliche Schwierigkeit entsteht, wenn der Fehler tatsächlich früher auftritt,<br />

als er vom Parser bemerkt wird (z.B. wird ein fehlendes begin in Pascal erst<br />

bei der Analyse der folgenden Anweisung bemerkt). In diesem Fall nimmt der<br />

Parser die Korrekturversuche an der falschen Stelle innerhalb des Quelltextes<br />

vor.<br />

Fehlerproduktionen: Für die Behandlung von “Standard-Fehlern”, die in Programmiersprachen<br />

häufig auftreten, ist es manchmal hilfreich, die kontextfreie Grammatik<br />

um Produktionen <strong>für</strong> diese fehlerhaften Konstrukte zu erweitern. Tritt<br />

ein solcher Standard-Fehler auf, wird durch die Anwendung der zugehörigen<br />

Fehler-Produktion eine Behandlung des Fehlers vorgenommen.<br />

Globale Korrekturen: Diese Verfahren wenden spezielle Algorithmen an, die minimale<br />

Folgen von Änderungen finden, um die fehlerhafte Eingabe in eine korrekte<br />

Eingabe zu transformieren. Hierzu stehen Änderungsoperationen wie Einfügen,<br />

Löschen <strong>und</strong> Ändern von Symbolen zur Verfügung. Diese Verfahren sind im<br />

allgemeinen zu aufwendig <strong>und</strong> werden daher nur eingeschränkt in Parsern verwendet,<br />

z.B. <strong>für</strong> das Finden der Ersetzungsstrings bei der konstrukt-orientierten<br />

Fehlerbehandlung.<br />

76


3.3 Top-Down-Syntaxanalyse<br />

id + * ( ) $<br />

E E → T E’ E → T E’ synch synch<br />

E’ E’ → + T E’ E’ → ε E’ → ε<br />

T T → F T’ synch T → F T’ synch synch<br />

T’ T’ → ε T’ → * F T’ T’ → ε T’ → ε<br />

F F → id synch synch F → ( E ) synch synch<br />

Tabelle 3.4: Für Fehlerbehandlung modifizierte Parse-Tabelle.<br />

Recovery in Top-Down-Parsern<br />

LL(k)-Parser haben die Eigenschaft, Fehler im Quellprogramm zum frühestmöglichen<br />

Zeitpunkt zu entdecken. Fehler werden in dem Moment erkannt, in dem das bisher<br />

gelesene Präfix der Eingabe kein gültiges Wort sein kann (viable prefix-Eigenschaft).<br />

Ein Top-Down-Parser hat einen Fehler erkannt, wenn<br />

• das oberste Symbol des Kellers ein Terminalsymbol ungleich dem aktuellen<br />

lookahead ist,<br />

• oder wenn das oberste Symbol des Kellers ein Nichtterminal A ist, das Terminalsymbol<br />

a den aktuellen lookahead bildet <strong>und</strong> in der Parse-Tabelle M der<br />

Eintrag M[A,a] leer ist bzw. error enthält.<br />

Für die Strategie der panic recovery werden synchronisierende Symbole benötigt.<br />

Folgende Symbole eignen sich als synchronisierende Symbole:<br />

1. Für ein Nichtterminal A:<br />

• FOLLOW(A): der Parser überliest die Eingabe, bis er ein Element aus<br />

FOLLOW(A) findet. Außerdem wird A vom Keller entfernt (es wird dem<br />

Parser die korrekte Analyse des zu A gehörenden Abschnitts der Eingabe<br />

vorgetäuscht).<br />

• Symbole, die Konstrukte abschließen, etwa Anfangssymbole von höher stehenden<br />

Konstrukten (z.B. Schlüsselwörter, mit denen Anweisungen beginnen).<br />

Zum Beispiel werden in C Anweisungen durch ’;’ abgeschlossen.<br />

Fehlt im Quelltext ein ’;’, würde sonst der Beginn der nächsten Anweisung<br />

überlesen, da er nicht in der FOLLOW-Menge enthalten ist.<br />

• FIRST(A): hierdurch wird ein neuer Parse-Versuch <strong>für</strong> A möglich, falls<br />

beim Überlesen ein Symbol aus FIRST(A) gef<strong>und</strong>en wird.<br />

2. Für Terminale werden alle anderen Terminalsymbole als synchronisierende Symbole<br />

verwendet. Wenn das Terminalsymbol auf dem Keller nicht zum aktuellen<br />

lookahead paßt, wird es vom Keller entfernt <strong>und</strong> der Parser gibt eine Meldung<br />

aus, daß das Terminal in den Quelltext “eingefügt” wurde.<br />

Eine weitere Möglichkeit besteht darin, <strong>für</strong> Nichtterminale A mit Produktion A → ε<br />

77


3 Syntaktische Analyse<br />

78<br />

Eingabe Keller Ausgabe<br />

id * + id $ E $<br />

id * + id $ T E’ $ E → T E ′<br />

id * + id $ F T’ E’ $ T → F T ′<br />

id * + id $ id T’ E’ $ F → id<br />

* + id $ T’ E’ $ ε<br />

* + id $ * F T’ E’ $ T ′ → ∗ F T ′<br />

+ id $ F T’ E’ $ ε<br />

+ id $ T’ E’ $ error, F wird entfernt,<br />

da Eintrag synch<br />

+ id $ E’ $ T ′ → ε<br />

+ id $ + T E’ $ E ′ → + T E ′<br />

id $ T E’ $ ε<br />

id $ F T’ E’ $ T → F T ′<br />

id $ id T’ E’ $ F → id<br />

$ T’ E’ $ ε<br />

$ E’ $ T ′ → ε<br />

$ $ E ′ → ε<br />

$ $ accept<br />

Tabelle 3.5: Beispiel einer Ableitung mit Fehlerbehandlung.


3.3 Top-Down-Syntaxanalyse<br />

diese Produktion als “default” zu verwenden, d.h. wenn keine andere Produktion<br />

zum aktuellen lookahead paßt (dies verzögert eventuell die Fehlererkennung).<br />

Beispiel 34<br />

Wir modifizieren die Parse-Tabelle aus Tabelle 3.2 auf Seite 64, indem wir die error-<br />

Einträge aus der Tabelle entfernen. Als synchronisierende Symbole verwenden wir die<br />

FOLLOW-Mengen der Nichtterminale:<br />

FOLLOW(E) = {), $}, FOLLOW(T) = {+, ), $},<br />

FOLLOW(F) = {+, ∗, ), $}.<br />

In die Felder der Parse-Tabelle tragen wir in den Spalten, die mit den Symbolen<br />

aus der jeweiligen FOLLOW-Menge beschriftet sind, die Anweisung synch ein. Die<br />

übrigen Felder, die zuvor mit error beschriftet waren, lassen wir leer (Tabelle 3.4).<br />

Wir interpretieren die Parse-Tabelle wie folgt:<br />

• bei einem leeren Eintrag wird das aktuelle Eingabesymbol vom Parser überlesen,<br />

• bei einem Eintrag synch wird das Nichtterminal an der Kellerspitze vom Keller<br />

entfernt.<br />

Zusätzlich wird, falls das oberste Kellerelement ein Terminalsymbol ungleich dem<br />

lookahead ist, dieses Symbol vom Keller entfernt.<br />

Wir analysieren mit Hilfe der modifizierten Parse-Tabelle das Eingabewort id * +<br />

id (siehe Tabelle 3.5).<br />

Die fehlerhafte Eingabe wird durch die Fehlerbehandlung erfolgreich analysiert. Beim<br />

Erreichen des Fehlers wird eine Fehlermeldung ausgegeben <strong>und</strong> die Analyse durch das<br />

Löschen des Nichtterminals F vom Keller fortgesetzt.<br />

79


3 Syntaktische Analyse<br />

3.4 Bottom-Up-Syntaxanalyse<br />

Bei der Bottom-Up-Syntaxanalyse wird die Konstruktion des Strukturbaums einer<br />

Ableitung bei der Eingabe, also mit den Blättern des Baums, begonnen. Während<br />

der Analyse wird versucht, immer größere Abschnitte der Eingabe zu Nichtterminalen<br />

zusammenzufassen, bis am Ende der Analyse die gesamte Eingabe auf das Startsymbol<br />

der Grammatik reduziert wurde. Bei der Bottom-Up-Syntaxanalyse wird also<br />

der Strukturbaum von den Blättern zur Wurzel hin aufgebaut, während er bei der<br />

Top-Down-Analyse von der Wurzel ausgehend hin zu den Blättern konstruiert wird.<br />

Im Gegensatz zur Top-Down-Analyse, wo in jedem Schritt nur das oberste Symbol des<br />

Kellers verwendet werden konnte, erlauben wir bei der Bottom-Up-Syntaxanalyse zur<br />

Vereinfachung, daß auch mehrere Zeichen an der Kellerspitze gelesen werden können<br />

(<strong>für</strong> ein festes k ≥ 0). Dies ändert die Mächtigkeit des Modells der Kellerautomaten<br />

nicht, da das Lesen mehrerer Zeichen durch einen üblichen Kellerautomaten, der<br />

nur auf das oberste Kellersymbol zugreifen kann, simuliert werden kann (etwa unter<br />

Zuhilfenahme von k ∗ |Γ| Zuständen).<br />

Zur Erinnerung: <strong>für</strong> ein Alphabet Σ sei Σ≤k = �<br />

0≤i≤k Σi die Menge der Wörter mit<br />

maximaler Länge k einschließlich ε.<br />

Wie bei der Top-Down-Analyse beschreiben wir zunächst die allgemeine Bottom-Up-<br />

Analyse (auch Shift-Reduce-Verfahren genannt), bevor wir die Verfahren zur Realisierung<br />

der deterministischen Bottom-Up-Analyse einführen.<br />

Definition 31<br />

Der (nichtdeterministische) Bottom-Up-Analyseautomat <strong>für</strong> eine kontextfreie Grammatik<br />

G = (VN,VT,P,S) ist ein Kellerautomat mit<br />

• Eingabealphabet VT,<br />

• Kelleralphabet Γ = VN ∪ VT ∪ {$},<br />

• Kellerstartsymbol $,<br />

• Ausgabealphabet {1,...,p} <strong>und</strong> der<br />

• Übergangsrelation ∆ ⊆ ((VT ∪ {ε}) × Γ ≤k ) × (Γ ≤1 × {1,...,p} ≤1 ), wobei<br />

((ε, ← α), (A,i)) ∈ ∆ :⇔ π(i) = A → α (Reduktionsschritt),<br />

((a,ε), (a,ε)) ∈ ∆ <strong>für</strong> a ∈ VT (Shift-Schritt) <strong>und</strong><br />

((ε,S$), (ε,ε)) ∈ ∆ (Erkennungsende).<br />

Die Übergangsrelation enthält drei Arten von Regeln:<br />

80<br />

1. Die Regeln der Form ((ε, ← α), (A,i)) beschreiben einen Reduktionsschritt. Befindet<br />

sich die rechte Seite der i-ten Produktion A → α in umgekehrter Reihenfolge<br />

auf dem Keller ( ← α ist das zu α reverse Wort, auch handle genannt), wird die<br />

rechte Seite entfernt <strong>und</strong> durch das Nichtterminal A der Produktion ersetzt.<br />

Dabei wird die Nummer i der Produktion auf das Ausgabeband geschrieben


3.4 Bottom-Up-Syntaxanalyse<br />

<strong>und</strong> die Eingabe bleibt unverändert. Da die rechte Seite einer Produktion auf<br />

dem Keller zu einem Nichtterminalsymbol reduziert wird, wird die Anwendung<br />

der Regel als Reduktionsschritt bezeichnet.<br />

2. Für jedes Terminalsymbol der Grammatik wird eine Regel der Form ((a,ε), (a,ε))<br />

in die Übergangsrelation aufgenommen. Das aktuelle Symbol a der Eingabe<br />

wird aus der Eingabe entfernt <strong>und</strong> auf den Keller gelegt (das Symbol wird aus<br />

der Eingabe auf den Keller “geschoben”, daher wird die Anwendung eines solchen<br />

Übergangs als Shift-Schritt bezeichnet). Der Inhalt des Ausgabebandes<br />

wird nicht verändert.<br />

3. Die Regel((ε,S$), (ε,ε)) definiert das Erkennungsende der Analyse. Ist die Eingabe<br />

leer, d.h. alle Symbole der Eingabe wurden gelesen, <strong>und</strong> befinden sich auf<br />

dem Keller nur das Startsymbol der Grammatik <strong>und</strong> das Kellerstartsymbol $,<br />

ist die Analyse beendet <strong>und</strong> der Kellerautomat hat das Eingabewort akzeptiert.<br />

Durch die Übergangsrelation wird die Arbeitsweise des Bottom-Up-Parsers definiert.<br />

Die Eingabe wird symbolweise auf den Keller “geschoben”. Sobald auf dem Keller eine<br />

rechte Seite einer Produktion der Grammatik in umgekehrter Reihenfolge vorliegt,<br />

kann diese rechte Regelseite vom Keller entfernt <strong>und</strong> durch das Nichtterminalsymbol<br />

der linken Seite der Produktion ersetzt werden. Auf diese Weise werden immer größere<br />

Anfangsstücke der Eingabe auf den Keller gelegt <strong>und</strong> durch Reduktionsschritte in<br />

Nichtterminale umgewandelt. Die Analyse ist beendet, wenn die gesamte Eingabe<br />

gelesen, auf den Keller geschoben <strong>und</strong> zum Startsymbol der Grammatik reduziert<br />

werden konnte.<br />

Beispiel 35 (Bottom-Up-Analyse arithmetischer Ausdrücke)<br />

Wir betrachten wieder die Grammatik aus Beispiel 19 auf Seite 49:<br />

E → E A E (1)<br />

| ( E ) (2)<br />

| - E (3)<br />

| id (4)<br />

A → + (5)<br />

| - (6)<br />

| * (7)<br />

| / (8)<br />

| ↑ (9)<br />

Analog zu Beispiel 19 analysieren wir die Eingabe -(id+id). Die Bottom-Up-Analyse<br />

ist in Tabelle 3.6 angegeben.<br />

Die bei der Analyse entstandene Ausgabe ist eine Rechtsanalyse, d.h. sie stellt eine<br />

Rechtsableitung in umgekehrter Reihenfolge dar:<br />

E 3 ⇒ - E 2 ⇒ - ( E ) 1 ⇒ - ( E A E ) 4 ⇒ - ( E A id ) 5 ⇒ - ( E + id )<br />

81


3 Syntaktische Analyse<br />

4<br />

E<br />

5<br />

- ( id - ( id<br />

E A E<br />

- ( id + id<br />

1<br />

3<br />

E<br />

E A E<br />

- ( id + id<br />

-<br />

E<br />

E<br />

E<br />

E A E<br />

E A<br />

- ( id +<br />

2<br />

( id + id )<br />

4<br />

E<br />

E<br />

E A E<br />

- ( id + id )<br />

Abbildung 3.16: Erzeugung eines Strukturbaums <strong>für</strong> einen arithmetischen Ausdruck.<br />

82


Eingabe Kellerinhalt Ausgabe<br />

- ( id + id ) $<br />

( id + id ) - $ ε<br />

id + id ) ( - $ ε<br />

+ id ) id ( - $ ε<br />

+ id ) E ( - $ 4<br />

id ) + E ( - $ ε<br />

id ) A E ( - $ 5<br />

) id A E ( - $ ε<br />

) E A E ( - $ 4<br />

) E ( - $ 1<br />

ε ) E ( - $ ε<br />

ε E - $ 2<br />

ε E $ 3<br />

ε ε accept<br />

3.4 Bottom-Up-Syntaxanalyse<br />

Tabelle 3.6: Beispielableitung eines Bottom-Up-Parsers.<br />

4<br />

⇒ - ( id + id )<br />

In Abbildung 3.16 ist die Konstruktion des Strukturbaums gemäß den Analyseschritten<br />

der Tabelle dargestellt. Es entsteht derselbe Strukturbaum wie bei der Top-Down-<br />

Analyse in Abbildung 3.8, aber bei der Bottom-Up-Analyse wird der Strukturbaum<br />

“von unten nach oben” aufgebaut.<br />

Auch bei der allgemeinen Bottom-Up-Syntaxanalyse tritt Nichtdeterminismus auf.<br />

Wir betrachten die verschiedenen Situationen <strong>für</strong> Nichtdeterminismus:<br />

• Ein Shift-Schritt ist immer möglich, solange die Eingabe nicht leer ist. Wann<br />

soll eine Reduktion durchgeführt werden?<br />

• Das Erkennungsende ist unklar, falls das Startsymbol S in einer rechten Regelseite<br />

auftritt, z.B. in einer Grammatik mit den Produktionen S → A <strong>und</strong><br />

A → S. Wenn auf dem Keller ein S steht, soll die Analyse beendet werden oder<br />

muß das S zu einem A reduziert werden?<br />

• Eventuell stehen mehrere Reduktionsmöglichkeiten zur Auswahl, z.B. bei den<br />

Produktionen A → α <strong>und</strong> B → α. Wenn auf dem Keller ← α steht, wird dann zu<br />

A oder zu B reduziert? Ein weiteres Problem entsteht z.B. <strong>für</strong> die Grammatik<br />

A → Bb |b. Wenn auf dem Keller die Symbole bB liegen (b Kellerspitze), soll<br />

dann nur das b oder bB zu A reduziert werden?<br />

Um eine deterministische Bottom-Up-Syntaxanalyse durchführen zu können, beschränken<br />

wir uns auf die Klasse der Sprachen, die mit einem lookahead von k<br />

83


3 Syntaktische Analyse<br />

Symbolen deterministisch zu erkennen sind. Diese Sprachen werden LR(k)-Sprachen<br />

genannt.<br />

3.4.1 LR(k)-Grammatiken<br />

Analog zu den LL(k)-Grammatiken bei der Top-Down-Analyse gibt es <strong>für</strong> die Bottom-<br />

Up-Analyse eine Klasse von kontextfreien Grammatiken, die die deterministische<br />

Analyse von Eingabewörtern erlauben. Diese Grammatiken heißen LR(k)-Grammatiken<br />

(das L steht <strong>für</strong> das Lesen der Eingabe von links nach rechts, das R steht <strong>für</strong> die<br />

Konstruktion einer Rechtsableitung). Wie die LL(k)-Grammatiken verwenden die<br />

LR(k)-Grammatiken einen lookahead von k Symbolen, um die Auswahl der nächsten<br />

Aktion deterministisch treffen zu können.<br />

Wir haben zuvor verschiedene Arten des Nichtdeterminismus betrachtet. Dabei haben<br />

wir gesehen, daß das Erkennungsende unklar ist, sobald das Startsymbol der<br />

Grammatik in rechten Regelseiten auftritt. Um diese Art von Nichtdeterminismus zu<br />

vermeiden, betrachten wir im folgenden nur Grammatiken, die startsepariert sind.<br />

Definition 32<br />

Eine kontextfreie Grammatik G = (VN,VT,P,S) heißt startsepariert, wenn S nur in<br />

einer Produktion S → A, A �= S, vorkommt.<br />

Wir können im folgenden voraussetzen, daß die Grammatiken startsepariert sind,<br />

denn zu einer Grammatik G läßt sich eine äquivalente startseparierte Grammatik G ′<br />

wie folgt konstruieren:<br />

Sei S ′ Startsymbol von G ′ , <strong>und</strong> S ′ komme in G nicht vor. Die Produktionen von G ′<br />

sind die Produktionen von G <strong>und</strong> die Produktion S ′ → S. Damit ist G ′ startsepariert.<br />

Auf diese Weise kann das Shift-Reduce-Verfahren <strong>für</strong> G ′ akzeptieren, wenn der<br />

Kellerinhalt S ′ <strong>und</strong> die Eingabe leer ist.<br />

Für mehrdeutige Grammatiken gibt es zu mindestens einem Satz mehr als eine<br />

Rechtsableitung. Also können wir in diesem Fall auch kein deterministisches Shift-<br />

Reduce-Verfahren erwarten.<br />

Nehmen wir also <strong>für</strong> die folgenden Überlegungen an, daß die betrachtete Grammatik<br />

eindeutig ist <strong>und</strong> daher jede Satzform genau eine Rechtsableitung besitzt.<br />

Definition 33<br />

Sei S ⇒ ∗ r βXu ⇒r βαu eine Rechtsableitung einer kontextfreien Grammatik G. α<br />

heißt dann der Griff der Rechtssatzform βαu.<br />

In einer nicht mehrdeutigen Grammatik ist der Griff einer Rechtssatzform das eindeutig<br />

bestimmte Teilwort, welches in einer Bottom-Up-Analyse im nächsten Reduktionsschritt<br />

durch ein Nichtterminal ersetzt werden muß, um zu einer Rechtsableitung<br />

zu gelangen.<br />

84


3.4 Bottom-Up-Syntaxanalyse<br />

Sei S = α0 ⇒r α1 ⇒r ... ⇒r αm = w eine Rechtsableitung. Eine Grammatik G ist<br />

LR(k), wenn in jeder solchen Rechtsableitung <strong>und</strong> in jedem αi der Griff lokalisiert<br />

<strong>und</strong> die anzuwendende Produktion eindeutig bestimmt werden kann, indem man die<br />

αi von links bis höchstens k Symbole hinter dem Griff betrachtet. Wenn wir also die<br />

αi in αi = αβw aufteilen <strong>und</strong> es eine Produktion X → β gibt, dann ist der Schritt<br />

nach αi−1 = αXw eindeutig durch αβ <strong>und</strong> k : w bestimmt. Eine Grammatik G<br />

ist also genau dann LR(k), wenn der nächste anzuwendende Schritt aus der bisher<br />

gelesenen Eingabe (bis zum Ende des Griffs) <strong>und</strong> einem lookahead von k Symbolen<br />

nach dem Griff eindeutig zu bestimmen ist.<br />

Definition 34<br />

Sei G = (VN,VT,P,S) startseparierte kontextfreie Grammatik. G heißt LR(k)-Grammatik,<br />

wenn aus<br />

S<br />

⇒ ∗ r α X w ⇒r α β w<br />

⇒ ∗ r γ Y x ⇒r α β y<br />

<strong>und</strong> k : w = k : y folgt, daß α = γ <strong>und</strong> X = Y <strong>und</strong> x = y.<br />

Wenn k : w = k : y gilt, müssen also folgende Bedingungen erfüllt sein, damit G die<br />

LR(k)-Eigenschaft besitzt:<br />

• X = Y , d.h. das Nichtterminal, zu dem reduziert wird, muß eindeutig sein,<br />

• α = γ, d.h. der Griff befindet sich, von “vorne” gesehen, an derselben Position<br />

der Satzform,<br />

• x = y, d.h. der Griff befindet sich, von “hinten” gesehen, an derselben Position<br />

der Satzform.<br />

Durch die zweite <strong>und</strong> die dritte Bedingung ist die Zerlegung der Satzform eindeutig<br />

bestimmt.<br />

Beispiel 36<br />

1. Wir überprüfen, ob die Grammatik G mit den Produktionen<br />

S → a A c<br />

A → b A b | b<br />

die LR(1)-Eigenschaft besitzt. Dazu wenden wir Definition 34 an:<br />

S<br />

⇒ ∗ r<br />

α<br />

����<br />

ab<br />

⇒ ∗ r abb<br />

����<br />

γ<br />

X<br />

����<br />

A<br />

A<br />

����<br />

Y<br />

w<br />

����<br />

bc ⇒r<br />

bbc<br />

����<br />

x<br />

α<br />

����<br />

ab<br />

⇒r ab<br />

����<br />

α<br />

β<br />

����<br />

b<br />

b<br />

����<br />

β<br />

w<br />

����<br />

bc<br />

bbbc<br />

����<br />

y<br />

85


3 Syntaktische Analyse<br />

Es gilt 1 : w = 1 : y = b, aber α �= γ <strong>und</strong> x �= y. Also ist G keine LR(1)-<br />

Grammatik. Man kann zeigen, daß G <strong>für</strong> kein k eine LR(k)-Grammatik ist.<br />

2. Gegeben sei die Grammatik G ′ mit den folgenden Produktionen:<br />

S → a A c<br />

A → A b b | b<br />

Die Sprache L(G ′ ) = L(G) = {ab 2n+1 c | n ≥ 0} ist identisch mit der unter<br />

Punkt 1 erzeugten Sprache. Im Gegensatz zu G ist G ′ eine LR(0)-Grammatik.<br />

Wir betrachten die Rechtssatzformen, die während der Ableitung eines Wortes<br />

aus L(G ′ ) auftreten können:<br />

a) a A c<br />

b) a A b b b 2n c, n ≥ 0<br />

c) a b b 2n c, n ≥ 0<br />

In jedem dieser drei Fälle ist der Griff <strong>und</strong> die anzuwendende Produktion bereits<br />

durch den Präfix der Eingabe bis zum Ende des Griffs eindeutig bestimmt:<br />

a) trivial<br />

b) die Rechtssatzform läßt sich nur zu aAb 2n c in einer Rechtsableitung reduzieren<br />

(würden wir ein b zu einem A reduzieren, bekämen wir eine Rechtssatzform<br />

mit zwei A’s, die nicht weiter reduziert werden könnte, also ist<br />

der Griff Abb eindeutig). Dieser Fall ist durch das Präfix aAbb bestimmt.<br />

c) der Griff muß das erste b sein, da wir <strong>für</strong> weitere Ableitungsschritte das<br />

Nichtterminal A benötigen. Dieser Fall ist durch das Präfix ab bestimmt.<br />

Da die einzelnen Fälle durch das Präfix der Eingabe bis zum Ende des Griffs<br />

eindeutig bestimmt sind, ist kein weiterer lookahead notwendig. Also ist die<br />

Grammatik LR(0).<br />

3. Wir betrachten eine weitere Grammatik G ′′ , die dieselbe Sprache wie G <strong>und</strong> G ′<br />

erzeugt:<br />

S → a A c<br />

A → b b A | b<br />

Wir betrachten, analog zu 2., die möglichen Rechtssatzformen der Ableitungen:<br />

a) a A c<br />

b) a b 2n b b A c, n ≥ 0<br />

c) a b 2n b c, n ≥ 0<br />

Der Fall a) ist trivial. Die in den Fällen b) <strong>und</strong> c) auftretenden Rechtssatzformen<br />

lassen sich zu ab k bα mit k ≥ 0 zusammenfassen. Falls α = c, ist der Griff<br />

das letzte Auftreten von b. Sonst ist der Griff bbA, der sich mit α überschneidet.<br />

Also ist die Entscheidung über die anzuwendende Produktion mit einem<br />

lookahead von 1 eindeutig zu bestimmen. Daher ist G ′′ eine LR(1)-Grammatik.<br />

An diesem Beispiel sehen wir, daß es <strong>für</strong> eine Sprache mehrere Grammatiken geben<br />

86


kann, die sich in bezug auf die LR(k)-Eigenschaft unterscheiden.<br />

3.4 Bottom-Up-Syntaxanalyse<br />

Bevor wir detailliert auf das Konstruktionsverfahren <strong>für</strong> deterministische Bottom-<br />

Up-Parser eingehen, schildern wir kurz die Idee des Verfahrens.<br />

Idee <strong>für</strong> die Konstruktion des deterministischen Bottom-Up-Parsers <strong>für</strong> LR(k)-<br />

Grammatiken: LR(k)-Grammatiken besitzen die Eigenschaft, daß die Reduktion<br />

von αβw zu αAw durch αβ(k : w) vollständig bestimmt ist. Dies bedeutet, daß,<br />

wenn αβ auf dem Keller liegt <strong>und</strong> w in der Eingabe steht, man anhand von k : w<br />

entscheiden kann, ob ein Shift-Schritt oder ob <strong>und</strong> welcher Reduktionsschritt durchgeführt<br />

werden soll. Das Problem besteht darin, daß die Auswahl nicht nur vom<br />

Griff β, sondern auch dem weiteren Kellerinhalt, dem Präfix α, beeinflußt wird. Im<br />

allgemeinen gibt es jedoch unendlich viele mögliche Präfixe α. Daher ist es <strong>für</strong> die<br />

Konstruktion des Parsers notwendig, einen Weg zu finden, wie man die entsprechende<br />

Information endlich darstellen <strong>und</strong> in den Shift-Reduce-Algorithmus einbinden kann.<br />

Definition 35<br />

Sei S ⇒ ∗ r βXu ⇒r βαu eine Rechtsableitung zu einer kontextfreien Grammatik G.<br />

Dann heißt jedes Präfix von βα ein zuverlässiges Präfix von G.<br />

Ein zuverlässiges Präfix ist der Anfang einer Rechtssatzform, der sich nicht echt über<br />

den (bei einer nicht mehrdeutigen Grammatik eindeutigen) Griff hinaus erstreckt.<br />

In einem zuverlässigen Präfix ist also eine Reduktion höchstens am Ende möglich<br />

(sonst ist es bereits soweit wie möglich reduziert, da wir Rechtsableitungen betrachten).<br />

Zu einer Grammatik gibt es im allgemeinen unendlich viele zuverlässige Präfixe.<br />

Allerdings kann die Menge der zuverlässigen Präfixe endlich dargestellt werden. Wir<br />

zeigen im folgenden, daß die Menge der zuverlässigen Präfixe einer Grammatik durch<br />

einen endlichen Automaten darstellbar ist. Wir geben die Konstruktion dieses Automaten<br />

an <strong>und</strong> integrieren ihn in das Shift-Reduce-Verfahren.<br />

Definition 36<br />

Sei G kontextfreie Grammatik, A → αβ eine Produktion von G. Dann heißt das Tripel<br />

(A,α,β) ein (kontextfreies) Item (oder ein LR(0)-Element) von G. Wir schreiben das<br />

Item (A,α,β) als [A → α.β].<br />

Ein Item der Form [A → α.] heißt vollständig.<br />

ItG bezeichnet die Menge der Items von G.<br />

vollstItG bezeichnet die Menge der vollständigen Items von G.<br />

Eine Produktion A → XY Z erzeugt die Items [A → .XY Z], [A → X.Y Z], [A →<br />

XY.Z] <strong>und</strong> [A → XY Z.] (nur letzteres ist vollständig). Die Produktion A → ε<br />

erzeugt das Item [A → .].<br />

87


3 Syntaktische Analyse<br />

Items lassen sich wie folgt interpretieren: ein Item [A → α.β] repräsentiert einen<br />

Analysezustand, in dem beim Versuch, ein Wort <strong>für</strong> A zu erkennen, bereits ein Wort<br />

<strong>für</strong> α erkannt wurde.<br />

Die Menge der Items einer kontextfreien Grammatik ist endlich.<br />

Definition 37<br />

Ein Item [A → α.β] heißt gültig <strong>für</strong> ein zuverlässiges Präfix γα, wenn es eine Rechtsableitung<br />

S ⇒ ∗ r γAw ⇒r γαβw gibt.<br />

Gültige Items beschreiben Situationen während einer Rechtsanalyse. Das zuverlässige<br />

Präfix γα entspricht dem bisher bearbeiteten Teil der Eingabe. A → αβ könnte eine<br />

demnächst anzuwendende Produktion sein, wobei der Anteil α der rechten Seite<br />

bereits erzeugt wurde.<br />

Beispiel 37<br />

Gegeben sei die folgende startseparierte Grammatik <strong>für</strong> arithmetische Ausdrücke [WM96]:<br />

S → E<br />

E → E + T | T<br />

T → T * F | F<br />

F → ( E ) | id<br />

Wir geben <strong>für</strong> verschiedene Schritte der Ableitung einer Satzform zuverlässige Präfixe<br />

<strong>und</strong> <strong>für</strong> sie gültige Items an:<br />

a) S ⇒r E ⇒r E+T:<br />

E+ ist zuverlässiges Präfix <strong>und</strong> [E → E + .T] ist gültiges<br />

� �� �<br />

Griff<br />

Item (γ,w leer).<br />

b) S ⇒∗ r E+T ⇒r E+ ���� F : E+ ist wieder zuverlässiges Präfix <strong>und</strong> [T → .F] ist<br />

Griff<br />

gültiges Item (γ = E+).<br />

c) S ⇒∗ r E+F ⇒r E+id: E+ ist wieder zuverlässiges Präfix <strong>und</strong> [F → .id] ist<br />

gültiges Item (γ = E+).<br />

d) S ⇒ ∗ r (E+F) ⇒r (E+ (E)<br />

����<br />

Griff<br />

): (E+<br />

� �� �<br />

γ<br />

(<br />

����<br />

α<br />

Griff hinaus) <strong>und</strong> [F → (.E)] ist gültiges Item.<br />

ist zuverlässiges Präfix (nicht über den<br />

Satz 9<br />

Zu jedem zuverlässigen Präfix gibt es mindestens ein gültiges Item.<br />

Wir konstruieren nun einen endlichen Automaten, der zuverlässige Präfixe erkennt<br />

<strong>und</strong> darüber hinaus in seinen Zuständen Informationen über mögliche Reduktionen<br />

enthält.<br />

88


3.4 Bottom-Up-Syntaxanalyse<br />

Definition 38<br />

Sei G = (VN,VT,P,S ′ ) startseparierte kontextfreie Grammatik (S ′ → S sei die einzige<br />

Produktion, die S ′ enthält). Sei char(G) = (Qc,VN ∪ VT, ∆c,qc,Fc) mit<br />

Qc = ItG,<br />

qc = [S ′ → .S],<br />

Fc = vollstItG,<br />

∆c = {([A → α.Y β],Y, [A → αY.β]) | A → αY β ∈ P,Y ∈ VN ∪ VT }<br />

∪ {([A → α.Bβ],ε, [B → .γ]) | A → αBβ ∈ P,B → γ ∈ P }<br />

der charakteristische endliche Automat zu G.<br />

Die Zustände des charakteristischen endlichen Automaten werden durch die Items<br />

aus ItG gebildet. Endzustände sind die Zustände, die vollständigen Items entsprechen.<br />

Es gibt zwei Arten von Übergängen in diesem Automaten. Steht der “.”, der die<br />

aktuelle Analysesituation kennzeichnet, vor einem Symbol Y (Terminal- oder Nichtterminalsymbol),<br />

kann der Punkt hinter das Y bewegt werden. Dieser Übergang ist<br />

mit Y beschriftet <strong>und</strong> beschreibt, daß der Teil der Eingabe, der zu Y reduziert wird,<br />

analysiert wurde. Die zweite Art von Übergängen beschreibt die Ersetzung von Nichtterminalen<br />

durch ihre rechten Regelseiten. Steht der “.” vor einem Nichtterminal B,<br />

geht eine ε-Kante vom aktuellen Zustand zu einem neuen Zustand [B → .γ] über: B<br />

wurde durch die rechte Regelseite γ ersetzt, <strong>und</strong> es wurde noch kein Teilwort von γ<br />

analysiert (Expansionsübergänge).<br />

Beispiel 38 (Fortführung von Beispiel 37)<br />

Abbildung 3.17 enthält den charakteristischen endlichen Automaten <strong>für</strong> die Grammatik<br />

aus Beispiel 37. Jede “Zeile” aus Items entspricht dem “Schieben” des “.” durch<br />

eine Produktion der Grammatik. Am Ende der Zeilen befinden sich die vollständigen<br />

Items.<br />

Satz 10<br />

Ist γ ∈ (VN ∪ VT) ∗ <strong>und</strong> q ∈ Qc, dann gilt (qc,γ) ⊢ ∗ (q,ε) genau dann, wenn γ ein<br />

zuverlässiges Präfix <strong>und</strong> q ein gültiges Item <strong>für</strong> γ ist.<br />

Es gilt also:<br />

1. Der Automat erkennt genau zuverlässige Präfixe, wenn man alle Zustände als<br />

Endzustände betrachtet.<br />

2. Endzustände sind vollständige Items, das sind solche, die maximal langen zuverlässigen<br />

Präfixen entsprechen (d.h. wird während der Analyse ein vollständiges<br />

Item erreicht, muß reduziert werden). Wir können Items also wie folgt interpretieren:<br />

[A → α.β]: Griff ist unvollständig (später: Shift-Schritt)<br />

[A → α.]: Griff ist vollständig, also reduzieren.<br />

89


3 Syntaktische Analyse<br />

ε<br />

ε<br />

ε<br />

E<br />

[S .E] [S E.]<br />

ε<br />

ε ε<br />

E<br />

+ T<br />

[E .E+T] [E E.+T] [E E+.T] [E E+T.]<br />

ε<br />

ε<br />

T<br />

[E .T] [E T.]<br />

ε<br />

ε<br />

ε<br />

T * F<br />

[T .T*F] [T T.*F] [T T*.F] [T T*F.]<br />

ε<br />

ε<br />

F<br />

[T .F] [ T F.]<br />

ε<br />

ε<br />

( E )<br />

[F .(E)] [F (.E)] [F (E.)] [F (E).]<br />

ε<br />

id<br />

[F . id ] [F id .]<br />

Abbildung 3.17: Beispiel eines charakteristischen endlichen Automaten<br />

Die Items (<strong>und</strong> damit die Zustände des Automaten) enthalten somit Informationen<br />

<strong>für</strong> die Analyse.<br />

Beweis<br />

“⇒”:<br />

Induktion über die Länge der Berechnung. Bezeichne ⊢ n einen Übergang in char(G)<br />

in n Schritten.<br />

Induktionsanfang: (qc,γ) ⊢ 0 (qc,ε) gilt nur <strong>für</strong> γ = ε, ε ist zuverlässiges Präfix,<br />

qc = [S ′ → .S] ist gültiges Item <strong>für</strong> ε.<br />

Induktionsannahme: Die Behauptung gelte <strong>für</strong> alle Berechnungen der Länge kleiner<br />

als n. Sei nun (qc,γ) ⊢ n (q,ε).<br />

Induktionsschritt:<br />

1. Fall: (qc,γ) ⊢ n−1 (p,ε) ⊢ (q,ε), d.h. der letzte Schritt war ein ε-Übergang. Dann<br />

haben p <strong>und</strong> q die Form p = [Y → α.Xβ], q = [X → .δ]. Nach Induktionsannahme<br />

ist γ zuverlässiges Präfix <strong>und</strong> p gültig <strong>für</strong> γ. Dann ist nach Definition 37 auch q gültig<br />

<strong>für</strong> γ (sich vor X zu befinden bedeutet, sich vor δ zu befinden).<br />

2.Fall: γ = γ ′ X, (qc,γ ′ X) ⊢ n−1 (p,X) ⊢ (q,ε). Nach Induktionsannahme ist γ ′ zuverlässiges<br />

Präfix <strong>und</strong> p gültig <strong>für</strong> γ ′ , da (qc,γ ′ ) ⊢ n−1 (p,ε). Dann haben p <strong>und</strong> q die<br />

Form [Y → α.Xβ] bzw. [Y → αX.β]. Da p gültig ist <strong>für</strong> γ ′ , gibt es eine Rechtsableitung<br />

S ⇒ ∗ r δY u ⇒r δαXβu mit γ ′ = δα. Dann ist γ = γ ′ X ein zuverlässiges Präfix<br />

<strong>und</strong> q gültig <strong>für</strong> γ.<br />

90


3.4 Bottom-Up-Syntaxanalyse<br />

“⇐”:<br />

Induktion über die Länge von γ.<br />

Induktionsanfang: γ = ε: alle gültigen Items <strong>für</strong> das zuverlässige Präfix ε haben die<br />

Form q = [X → .α] mit S ′ ⇒ ∗ r Xβ (noch nichts erzeugt). Nach Konstruktion von<br />

char(G) gilt dann (qc,ε) ⊢ ∗ (q,ε) (nur Expansionsübergänge verwendet).<br />

Induktionsannahme: Die Behauptung gelte <strong>für</strong> alle zuverlässigen Präfixe der Länge<br />

kleiner als n <strong>und</strong> alle <strong>für</strong> sie gültigen Items q.<br />

Induktionsschritt: Sei γX zuverlässiges Präfix der Länge n <strong>und</strong> q gültiges Item <strong>für</strong><br />

γX, q = [A → β1.β2]. Dann gibt es eine Rechtsableitung S ⇒ ∗ r αAw ⇒r αβ1β2w <strong>und</strong><br />

γX = αβ1. Wir unterscheiden die folgenden beiden Fälle: entweder β1 �= ε, d.h. wir<br />

befinden uns noch in der Analyse <strong>für</strong> X; oder β1 = ε, d.h. γX = α, also ist X bereits<br />

auf dem Keller.<br />

1. Fall: β1 = γ ′ X, d.h. γ = αγ ′ . Dann ist [A → γ ′ .Xβ2] gültig <strong>für</strong> das zuverlässige<br />

Präfix γ. Nach Konstruktion von char(G) <strong>und</strong> Induktionsannahme gilt dann<br />

(qc,γX) ⊢ ∗ ([A → γ ′ .Xβ2],X) ⊢ ([A → γ ′ X.β2],ε).<br />

2. Fall: β1 = ε, d.h. α = γX (X “ist auf dem Keller”, d.h. es gab einen Schritt<br />

in der Rechtsanalyse, in dem das X dorthin gekommen ist). Wir betrachten in der<br />

Rechtsableitung S ′ ⇒ ∗ r αAw den Schritt, in dem das Vorkommen von X in α eingeführt<br />

wurde: S ⇒ ∗ r µBy ⇒r µνXρy mit µν = γ. Jeder spätere Schritt ersetzt<br />

nur Nichtterminale rechts von X (sonst handelt es sich nicht um eine Rechtsableitung).<br />

Dann ist [B → ν.Xρ] gültiges Item <strong>für</strong> das zuverlässige Präfix γ = µν. Nach<br />

Induktionsannahme <strong>und</strong> Konstruktion von char(G) gilt:<br />

(qc,γX) ⊢ ∗ ([B → ν.Xρ],X) ⊢ ([B → νX.ρ],ε) ⊢ ∗ ([A → β1.β2],ε)<br />

(beide Items sind gültig <strong>für</strong> γX). �<br />

Korollar 1<br />

Die Sprache der zuverlässigen Präfixe einer kontextfreien Grammatik ist regulär.<br />

Der charakteristische endliche Automat einer kontextfreien Grammatik ist nichtdeterministisch.<br />

Um den Automaten in der deterministischen Bottom-Up-Syntaxanalyse<br />

verwenden zu können, müssen wir ihn determinisieren. Hierzu kann das auf Seite 23<br />

angegebene Verfahren der Potenzmengenkonstruktion verwendet werden.<br />

Definition 39<br />

Der sich durch die Potenzmengenkonstruktion aus char(G) ergebende deterministische<br />

endliche Automat (Qd,VN ∪ VT,δd,qd,Fd) wird LR-DEA(G) genannt.<br />

Beispiel 39 (Fortführung von Beispiel 38)<br />

Wir führen das Verfahren der Potenzmengenkonstruktion <strong>für</strong> den charakteristischen<br />

endlichen Automaten aus Abbildung 3.17 durch.<br />

91


3 Syntaktische Analyse<br />

Der Startzustand des LR-DEA(G) ergibt sich laut dem Verfahren der Potenzmengenkonstruktion<br />

aus der Menge der ε-Folgezustände des Startzustands des NEA (also<br />

des char(G)). Daher ist der Startzustand<br />

S0 = {[S → .E], [E → .E+T], [E → .T], [T → .T ∗F], [T → .F],<br />

[F → .(E)], [F → .id]}.<br />

Der Folgezustand eines Zustands unter einem Symbol a ergibt sich aus den ε-Folgezuständen<br />

der Folgezustände der Elemente unter a im NEA. Die Übergänge [S → .E] E → [S →<br />

E.] <strong>und</strong> [E → .E+T] E → [E → E.+T] sind die einzigen Übergänge von Zuständen<br />

aus S0 unter E. Daher bilden wir aus den Folgezuständen einen neuen Zustand<br />

S1 = {[S → E.], [E → E.+T]}. Von den Zuständen in S1 gehen keine ε-Kanten<br />

aus, so daß keine weiteren Zustände in S1 aufgenommen werden müssen. Im LR-<br />

E<br />

DEA(G) gilt also: S0 → S1.<br />

Die übrigen Zustände des LR-DEA(G) werden analog gebildet. Abbildung 3.18 enthält<br />

den LR-DEA(G) <strong>für</strong> den char(G) aus Abbildung 3.17.<br />

Endzustände des LR-DEA(G) sind die Zustände, die Endzustände des char(G) (also<br />

vollständige Items) enthalten.<br />

Es gilt:<br />

1. Die Zustände des LR-DEA(G) einer Grammatik G zerlegen die Menge der zuverlässigen<br />

Präfixe in endlich viele disjunkte Teilmengen (die Teilmengen werden<br />

durch die Zustände dargestellt). Für jedes zuverlässige Präfix gibt es genau<br />

einen Zustand, in den der Parser durch die Analyse dieses Präfixes gelangt.<br />

2. Sei γ zuverlässiges Präfix, p(γ) der Zustand, in den der LR-DEA(G) mit γ<br />

übergeht. Dann enthält p(γ) genau alle gültigen Items <strong>für</strong> γ, also alle möglichen<br />

Analysesituationen, die am Ende der Analyse von γ vorliegen können<br />

(Eigenschaft der Potenzmengenkonstruktion).<br />

Der folgende Algorithmus gibt an, wie der LR-DEA(G) direkt aus der Grammatik<br />

G erzeugt werden kann.<br />

Direkte Konstruktion des LR-DEA(G).<br />

Eingabe: startseparierte kontextfreie Grammatik G = (VN,VT,P,S ′ )<br />

Ausgabe: LR-DEA(G) = (Qd,VN ∪ VT,δd,qd,Fd)<br />

Verfahren:<br />

1 var q,q ′ : set of item ;<br />

2<br />

3 function Start : set of item ;<br />

4 return ({[S ′ → .S]}) ;<br />

5<br />

6 function Closure (s : set of item) : set of item };<br />

92


7 begin<br />

8 q := s;<br />

3.4 Bottom-Up-Syntaxanalyse<br />

9 foreach ([X → α.Y β] ∈ q ∧ Y → γ ∈ P) ∧ ([Y → .γ] /∈ q) do<br />

10 q := q ∪ {[Y → .γ]}<br />

11 od };<br />

12 return q ;<br />

13 end;<br />

14<br />

15 function Successor (s : set of item , Y : VN ∪ VT ) : set of<br />

item ;<br />

16 return {[X → αY.β] | [X → α.Y β] ∈ s};<br />

17<br />

18 begin (∗ Hauptprogramm ∗)<br />

19 Qd := {Closure(Start)};<br />

20 δd := ∅;<br />

21 foreach (q ∈ Qd) ∧ (X ∈ VN ∪ VT) do<br />

22 q ′ := Closure(Successor(q,X));<br />

23 if q ′ �= ∅ then<br />

24 if q ′ /∈ Qd then<br />

25 Qd := Qd ∪ {q ′ }<br />

26 fi ;<br />

27 δd := δd ∪ {q X → q ′ }<br />

28 fi<br />

29 od<br />

30 end.<br />

Die Funktion Start liefert den Anfangszustand des LR-DEA(G). Die Funktion Closure<br />

liefert zu einer Menge s von Items eine Menge von Items, die die ε-Folgezustände<br />

der Zustände von s beinhalten. Successor berechnet <strong>für</strong> einen Zustand s, also eine<br />

Menge von Items, den Nachfolgezustand unter dem Symbol Y .<br />

Im Hauptprogramm wird zuerst die Zustandsmenge Qd des LR-DEA(G) mit dem<br />

Startzustand initialisiert. Der Startzustand enthält das Item [S ′ → .S] (durch Aufruf<br />

von Start) <strong>und</strong> dessen ε-Folgezustände (durch Aufruf von Closure). Dann werden<br />

in einer Schleife <strong>für</strong> jeden Zustand der aktuellen Menge Qd die Folgezustände unter<br />

dem jeweiligen Symbol X berechnet 1 . Ist dieser Zustand nicht leer <strong>und</strong> existiert er<br />

noch nicht in Qd, wird er in Qd aufgenommen. Die Übergangsrelation δd wird um<br />

den neuen Übergang erweitert.<br />

Wir konstruieren jetzt einen Kellerautomaten, in dem dieser endliche Automat “eingebaut”<br />

ist. Dieser Kellerautomat ist im allgemeinen noch nicht deterministisch. Zur<br />

1 foreach wird als “<strong>für</strong> jede Kombination aus q <strong>und</strong> X” interpretiert<br />

93


3 Syntaktische Analyse<br />

S<br />

0<br />

E<br />

T<br />

F<br />

(<br />

id<br />

S<br />

1<br />

S<br />

5<br />

S<br />

3<br />

F<br />

S<br />

4<br />

(<br />

T<br />

S<br />

2<br />

S0 = { [S → .E],<br />

[E → .E+T],<br />

[E → .T],<br />

[T → .T ∗F],<br />

[T → .F],<br />

[F → .(E)],<br />

[F → .id]}<br />

S1 = { [S → E.],<br />

[E → E.+T]}<br />

S2 = { [E → T.],<br />

[T → T.∗F]}<br />

S3 = { [T → F.]}<br />

S4 = { [F → (.E)],<br />

[E → .E+T],<br />

[E → .T],<br />

[T → .T ∗F],<br />

[T → .F],<br />

[F → .(E)],<br />

[F → .id]}<br />

94<br />

id<br />

+<br />

F<br />

E<br />

(<br />

id<br />

S<br />

6<br />

+<br />

T<br />

S<br />

8<br />

)<br />

(<br />

S<br />

11<br />

*<br />

id<br />

S5 = { [F → id.]}<br />

S6 = { [E → E+.T],<br />

[T → .T ∗F],<br />

[T → .F],<br />

[F → .(E)],<br />

[F → .id]}<br />

S7 = { [T → T ∗.F],<br />

[F → .(E)],<br />

[F → .id]}<br />

S8 = { [F → (E.)],<br />

[E → E.+T]}<br />

S9 = { [E → E+T.],<br />

[T → T.∗F]}<br />

S10 = { [T → T ∗F.]}<br />

S11 = { [F → (E).]}<br />

Abbildung 3.18: Beispiel eines LR-DEA.<br />

S<br />

9<br />

*<br />

F<br />

S S<br />

7 10


3.4 Bottom-Up-Syntaxanalyse<br />

Vereinfachung betrachten wir die Ausgabe des Automaten nicht.<br />

Bei den bisherigen Kellerautomaten wurde der Keller so dargestellt, daß sich die<br />

Kellerspitze links befand. Da sich bei der Bottom-Up-Analyse die bereits gelesenen<br />

Zeichen so auf dem Keller befinden, daß das zuletzt gelesenen Zeichen an der Kellerspitze<br />

steht, verwenden wir eine Darstellung von Kellerautomaten, bei der die<br />

Kellerspitze rechts angegeben wird. Auf diese Weise kann der Griff auf dem Keller<br />

ohne die sonst notwendige Invertierung dargestellt werden.<br />

Definition 40 Ein Kellerautomat (ohne Ausgabe) ist ein Tupel M = (Σ, Γ, ∆,z0)<br />

mit<br />

• Σ endliche Menge (Eingabealphabet),<br />

• Γ endliche Menge (Kelleralphabet),<br />

• z0 ∈ Γ (Kellerstartsymbol),<br />

• ∆ ⊆ ((Σ ∪ {ε}) × Γ ≤k ) × Γ ∗ ,k ∈ N (endliche Übergangsrelation).<br />

Die Menge der Konfigurationen von M ist K = Σ ∗ ×Γ ∗ , die Anfangskonfiguration <strong>für</strong><br />

w ∈ Σ ∗ lautet (w,z0), die Endkonfiguration ist (ε,ε). Die Schrittrelation ⊢M⊆ K×K<br />

ist definiert durch<br />

(aw,γα) ⊢M (w,γβ) gdw. ((a,α),β) ∈ ∆,a ∈ Σ ∪ {ε}.<br />

Definition 41<br />

Sei LR-DEA(G) = (Qd,VN ∪ VT,δd,qd,Fd).<br />

Sei K0 = (VT,Qd, ∆,qd) der Kellerautomat mit ∆ ⊆ ((VT ∪ {ε}) × Q ∗ d) × Q ∗ d<br />

definiert durch<br />

((a,q),qδd(q,a)) ∈ ∆ falls δd(q,a) definiert (Shift-Schritt),<br />

((ε,q0q1...qn),q0δd(q0,X)) ∈ ∆ falls [X → α.] ∈ qn, |α| = n (n ≥ 0) (Reduce-<br />

Schritt),<br />

((ε,qdq0),ε) ∈ ∆ falls [S ′ → S.] ∈ q0 (Erkennungsende).<br />

Eingabealphabet des Kellerautomaten K0 ist VT. Das Kelleralphabet von K0 bildet<br />

die Zustandsmenge Qd des LR-DEA(G), d.h. auf dem Keller von K0 können nur<br />

Zustände des LR-DEA(G) gespeichert werden. Das Kellerstartsymbol ist der Anfangszustand<br />

qd des LR-DEA(G). Die Übergangsrelation ∆ wird durch drei Arten<br />

von Übergängen gebildet:<br />

• In Übergängen der Form ((a,q),qδd(q,a)) wird das nächste Eingabezeichen a<br />

gelesen <strong>und</strong> der entsprechende Nachfolgezustand δd(q,a) auf den Keller gelegt<br />

(Kellerspitze ist rechts). Dies ist genau dann möglich, wenn der aktuelle Zustand<br />

q an der Spitze des Kellers mindestens ein Item der Form [X → ....a...] enthält.<br />

• In Übergängen der Form ((ε,q0q1...qn),q0δd(q0,X)) liegt eine Folge q0q1...qn von<br />

n + 1 Zuständen (q0 bezeichnet einen beliebigen Zustand) mit der Kellerspitze<br />

qn vor. Falls qn als aktueller Zustand ein vollständiges Item [X → α.] enthält<br />

95


3 Syntaktische Analyse<br />

<strong>und</strong> die Länge von α n Symbole beträgt, werden die n obersten Zustände vom<br />

Keller entfernt <strong>und</strong> durch den Nachfolgezustand δd(q0,X) ersetzt.<br />

• Analog zum früher angegebenen Bottom-Up-Automaten soll die Termination<br />

einer Analyse möglich sein, wenn “$S” auf dem Keller liegt, d.h. qd <strong>und</strong> ein Zustand<br />

q0, der [S ′ → S.] enthält. Diese Situation heißt Erkennungsende. Wegen<br />

der Startseparierung kommt [S ′ → S.] nur in einem Zustand vor. Der Übergang<br />

((ε,qdq0),ε) beschreibt den Akzeptanzschritt des Kellerautomaten. Ist die Eingabe<br />

leer <strong>und</strong> auf dem Keller liegen nur qd <strong>und</strong> q0, werden diese Zustände vom<br />

Keller entfernt. Da nun Eingabe <strong>und</strong> Keller leer sind, hält der Kellerautomat<br />

an.<br />

Der Kellerautomat “befindet sich” immer in dem Zustand, der gerade oben auf<br />

dem Keller liegt. qd spielt die Rolle des $ beim früher angegebenen Bottom-Up-<br />

Analyseautomaten (<strong>und</strong> gleichzeitig des Anfangszustands des Kellerautomaten), deshalb<br />

ist es richtig, daß qd immer auf dem Keller bleibt. Nur beim Erkennungsende<br />

wird qd vom Keller entfernt.<br />

Für jedes gelesene Symbol der Eingabe wird ein Zustand auf den Keller gelegt, <strong>und</strong><br />

zwar der Zustand, der vom aktuellen Zustand aus im LR-DEA(G) mit einer Kante<br />

zu erreichen ist, die mit dem gelesenen Symbol beschriftet ist. Enthält der aktuelle<br />

Zustand ein vollständiges Item, kann eine Reduktion durchgeführt werden. Dabei<br />

werden so viele Zustände vom Keller entfernt, wie die zugehörige Produktion Symbole<br />

auf der rechten Regelseite besitzt. Hierdurch wird ein Zustand q0 Kellerspitze.<br />

Nun wird der Folgezustand von q0 unter dem Nichtterminal X (also der linken Seite<br />

der Produktion) auf den Keller gelegt. Eine Eingabe wird erkannt, wenn der Kellerautomat<br />

nach einem Erkennungsende-Schritt mit leerer Eingabe <strong>und</strong> leerem Keller<br />

hält. Ein Problem tritt auf, wenn der Zustand q0 noch weitere Items enthält (siehe<br />

folgendes Beispiel), da dann der Automat nicht deterministisch das Ende der Analyse<br />

erkennen kann. Ein solcher Zustand ist ungeeignet (siehe Definition 42).<br />

Beispiel 40 (Fortführung von Beispiel 39)<br />

Wir geben die Übergangsrelation des Kellerautomaten K0 <strong>für</strong> den LR-DEA(G) aus<br />

Abbildung 3.18 an.<br />

96<br />

• Shift-Schritte:<br />

(( id,S0),S0S5) (( (,S0),S0S4) (( +,S1),S1S6)<br />

(( ∗,S2),S2S7) (( (,S4),S4S4) (( id,S4),S4S5)<br />

(( id,S6),S6S5) (( (,S6),S6S4) (( (,S7),S7S4)<br />

(( id,S7),S7S5) (( +,S8),S8S6) (( ),S8),S8S11)<br />

(( ∗,S9),S9S7)<br />

• Reduce-Schritte: (zu jedem Schritt wird die Produktion angegeben, die diesem<br />

Reduktionsschritt entspricht)


((ε,S0S1S6S9),S0S1) (E → E+T)<br />

((ε,S4S8S6S9),S4S8) (E → E+T)<br />

((ε,S0S2),S0S1) (E → T)<br />

((ε,S4S2),S4S8) (E → T)<br />

((ε,S0S2S7S10),S0S2) (T → T ∗F)<br />

((ε,S4S2S7S10),S4S2) (T → T ∗F)<br />

((ε,S6S9S7S10),S6S9) (T → T ∗F)<br />

((ε,S0S3),S0S2) (T → F)<br />

((ε,S4S3),S4S2) (T → F)<br />

((ε,S6S3),S6S9) (T → F)<br />

((ε,S0S4S8S11),S0S3) (F → (E))<br />

((ε,S4S4S8S11),S4S3) (F → (E))<br />

((ε,S6S4S8S11),S6S3) (F → (E))<br />

((ε,S7S4S8S11),S7S10) (F → (E))<br />

((ε,S0S5),S0S3) (F → id)<br />

((ε,S4S5),S4S3) (F → id)<br />

((ε,S6S5),S6S3) (F → id)<br />

((ε,S7S5),S7S10) (F → id)<br />

((ε,S0S1),ε) (S → E) (Erkennungsende),<br />

3.4 Bottom-Up-Syntaxanalyse<br />

In der folgenden Tabelle ist eine Ableitung des Kellerautomaten <strong>für</strong> das Eingabewort<br />

id+id*id angegeben (die Kellerspitze ist rechts):<br />

Eingabe<br />

id + id * id S0<br />

Kellerinhalt Ausgabe<br />

+ id * id S0 S5<br />

ε<br />

+ id * id S0 S3<br />

F → id<br />

+ id * id S0 S2<br />

T → F<br />

+ id * id S0 S1<br />

E → T<br />

id * id S0 S1 S6<br />

ε<br />

* id S0 S1 S6 S5 ε<br />

* id S0 S1 S6 S3 F → id<br />

* id S0 S1 S6 S9 T → F<br />

id S0 S1 S6 S9 S7 ε<br />

ε S0 S1 S6 S9 S7 S5 ε<br />

ε S0 S1 S6 S9 S7 S10 F → id<br />

ε S0 S1 S6 S9 T → T ∗F<br />

ε S0 S1 E → E+T<br />

ε ε accept<br />

Der Kellerautomat ist nichtdeterministisch, da im Zustand S1 des LR-DEA(G) neben<br />

dem Item [S → E.], das das Erkennungsende signalisiert, noch ein weiteres Item<br />

97


3 Syntaktische Analyse<br />

enthalten ist. Befindet sich der Automat im Zustand S1, muß nichtdeterministisch<br />

entschieden werden, ob das Erkennungsende oder ein Shift-Schritt ausgeführt werden<br />

soll. So hätte in der fünften Zeile der Tabelle mit der Ableitung auch das Erkennungsende<br />

ausgewählt werden können, was zu einem Fehler bei der Analyse geführt<br />

hätte.<br />

Der Kellerautomat K0 ist nichtdeterministisch, wenn ein Zustand q des LR-DEA(G)<br />

1. sowohl Shift- als auch Reduce-Übergänge erlaubt (Shift-Reduce-Konflikt),<br />

2. zwei verschiedene Reduce-Übergänge gemäß zweier verschiedener Produktionen<br />

hat (Reduce-Reduce-Konflikt).<br />

Im ersten Fall gibt es mindestens ein “Lese-Item” [X → α.aβ] <strong>und</strong> mindestens ein<br />

vollständiges Item in q. Im zweiten Fall gibt es mindestens zwei vollständige Items<br />

in q.<br />

Zustände, die solche Konflikte enthalten, nennen wir ungeeignet.<br />

Definition 42<br />

Sei Qd Zustandsmenge von LR-DEA(G). q ∈ Qd heißt ungeeignet, wenn q ein Item<br />

der Form [X → α.aβ], a ∈ VT, <strong>und</strong> ein vollständiges Item der Form [Y → γ.] enthält<br />

(Shift-Reduce-Konflikt) oder wenn q zwei verschiedene vollständige Items [Y → α.]<br />

<strong>und</strong> [Z → β.] enthält (Reduce-Reduce-Konflikt).<br />

Beispiel 41<br />

Die Zustände S1,S2 <strong>und</strong> S9 des LR-DEA(G) in Abbildung 3.18 sind ungeeignet, da<br />

jeder von ihnen einen Shift-Reduce-Konflikt enthält.<br />

Satz 11<br />

Eine kontextfreie Grammatik G ist genau dann eine LR(0)-Grammatik, wenn LR-<br />

DEA(G) keine ungeeigneten Zustände besitzt.<br />

Beweis<br />

“⇒”:<br />

Sei G LR(0)-Grammatik. Wir nehmen an, daß LR-DEA(G) einen ungeeigneten Zustand<br />

p enthält. Wir unterscheiden zwei Fälle:<br />

1. Fall: p enthält einen Reduce-Reduce-Konflikt. Dann hat p mindestens zwei verschiedene<br />

Reduce-Items [X → β.], [Y → δ.]. p ist eine nicht-leere Menge von zuverlässigen<br />

Präfixen zugeordnet (mit denen man nach p gelangt; nach Konstruktion<br />

von LR-DEA(G) sind alle Zustände erreichbar). Sei γβ ein solches zuverlässiges Präfix<br />

(nämlich das, das zu dem ersten Item paßt). Beide Reduce-Items sind gültig <strong>für</strong> γβ,<br />

d.h.<br />

98


S ′<br />

⇒ ∗ r γXw ⇒r γβw<br />

⇒ ∗ r νY y ⇒r νδy<br />

3.4 Bottom-Up-Syntaxanalyse<br />

mit γβ = νδ sind verschiedene Rechtsableitungen, was ein Widerspruch zur LR(0)-<br />

Eigenschaft ist.<br />

2. Fall: p enthält einen Shift-Reduce-Konflikt: analog.<br />

“⇐”:<br />

Wir nehmen an, daß LR-DEA(G) keine ungeeigneten Zustände enthält. Wir betrachten<br />

die Rechtsableitungen<br />

S ′<br />

⇒ ∗ r αXw ⇒r αβw<br />

⇒ ∗ r γY x ⇒r αβy<br />

<strong>und</strong> zeigen, daß α = γ,X = Y,x = y gilt. Mit αβ erreicht LR-DEA(G) einen Zustand<br />

p. Da p nicht ungeeignet, enthält p genau ein Reduce-Item, <strong>und</strong> zwar [X → β.], <strong>und</strong><br />

kein Shift-Item. p enthält alle <strong>für</strong> αβ gültigen Items, deshalb ist α = γ,X = Y <strong>und</strong><br />

x = y. �<br />

Mit dem oben konstruierten Kellerautomaten K0 haben wir also ein Parse-Verfahren<br />

<strong>für</strong> LR(0)-Grammatiken. Allerdings ist LR(0) <strong>für</strong> die Praxis zu eingeschränkt, obwohl<br />

LR(0) mächtiger ist als LL(0). Daher erweitern wir unser Konstruktionsverfahren <strong>für</strong><br />

den Fall k ≥ 1. Dazu müssen wir den lookahead miteinbeziehen, indem wir die Items<br />

um “passende” Vorausschaumengen erweitern. Die Auswahl des nächsten Schrittes<br />

kann dann in Abhängigkeit von diesen Vorausschaumengen getroffen werden.<br />

Definition 43<br />

Sei G kontextfreie Grammatik. [A → α.β,L] heißt LR(k)-Item von G, wenn A →<br />

αβ ∈ P <strong>und</strong> L ⊆ V ≤k<br />

T gilt. A → α.β heißt dann Kern, L Vorausschaumenge des<br />

Items.<br />

Ein LR(k)-Item [A → α.β,L] heißt gültig <strong>für</strong> ein zuverlässiges Präfix γα, falls es<br />

<strong>für</strong> alle u ∈ L eine Rechtsableitung S ⇒ ∗ r γAw ⇒r γαβw mit u = k : w gibt.<br />

Die bisher betrachteten Items sind LR(0)-Items, wenn wir [A → α.β, {ε}] mit [A →<br />

α.β] identifizieren.<br />

Analog zu den LR(0)-Items definieren wir Shift-Reduce- <strong>und</strong> Reduce-Reduce-Konflikte.<br />

Dabei werden die Vorausschaumengen mit einbezogen.<br />

Definition 44<br />

Sei I eine Menge von LR(1)-Items. I enthält einen Shift-Reduce-Konflikt, wenn es<br />

99


3 Syntaktische Analyse<br />

ein Item [X → α.aβ,L1] <strong>und</strong> ein Item [Y → γ.,L2] enthält, <strong>und</strong> es gilt a ∈ L2.<br />

I enthält einen Reduce-Reduce-Konflikt, wenn es zwei Items [X → α.,L1] <strong>und</strong> [Y →<br />

β.,L2] gibt mit L1 ∩ L2 �= ∅.<br />

Konstruktion von LR(1)-Parsern<br />

Wir ermöglichen jetzt dem Parser, bei seinen Entscheidungen in ungeeigneten Zuständen<br />

(im LR(0)-Sinn) den lookahead mit den Vorausschaumengen der LR(1)-Items zu vergleichen:<br />

• Enthält ein LR(1)-Parser-Zustand mehrere vollständige Items, so liegt trotzdem<br />

kein Reduce-Reduce-Konflikt vor, wenn ihre Vorausschaumengen disjunkt sind.<br />

• Enthält ein LR(1)-Parser-Zustand ein vollständiges Item [X → α.,L] <strong>und</strong> ein<br />

Shift-Item [Y → β.aγ,L ′ ], so liegt kein Shift-Reduce-Konflikt zwischen ihnen<br />

vor, wenn L das Symbol a nicht enthält.<br />

Im folgenden sei k = 1 <strong>und</strong> die zu analysierende Eingabe stets durch $ abgeschlossen.<br />

Daher sind die Vorausschaumengen immer Teilmengen von VT ∪ {$}.<br />

Analog zum auf Seite 92 angegebenen Algorithmus <strong>für</strong> LR(0)-Items läßt sich auch<br />

<strong>für</strong> LR(1)-Items ein Algorithmus LR(1)-GEN angeben, der die Berechnung des charakteristischen<br />

endlichen Automaten <strong>für</strong> eine LR(1)-Grammatik G durchführt.<br />

Algorithmus LR(1)-GEN.<br />

Eingabe: startseparierte kontextfreie Grammatik G = (VN,VT,P,S)<br />

Ausgabe: charakteristischer endlicher Automat eines kanonischen LR(1)-Parsers<br />

Verfahren:<br />

1 var q,q ′ : set of item ;<br />

2<br />

3 function Start : set of item ;<br />

4 return ({[S ′ → .S, {$}]}) ;<br />

5<br />

6 function Closure (s : set of item) : set of item ;<br />

7 begin<br />

8 q := s;<br />

9 foreach ([X → α.Y β,L] ∈ q ∧ Y → γ ∈ P) do<br />

10 if exist ([Y → .γ,L ′ ] ∈ q) then<br />

11 replace [Y → .γ,L ′ ] by [Y → .γ,L ′ ∪ (FIRST(βL)\{ε})]<br />

12 else q := q ∪ {[Y → .γ,FIRST(βL)\{ε}]}<br />

13 fi<br />

14 od;<br />

15 return q<br />

100


16 end;<br />

17<br />

3.4 Bottom-Up-Syntaxanalyse<br />

18 function Successor (s : set of item , Y : VN ∪ VT ) : set of<br />

item ;<br />

19 return {[X → αY.β,L] | [X → α.Y β,L] ∈ s};<br />

20<br />

21 begin (∗ Hauptprogramm ∗)<br />

22 Q := {Closure(Start)};<br />

23 δ := ∅;<br />

24 foreach (q ∈ Q) ∧ (X ∈ VN ∪ VT) {\ bf do<br />

25 q ′ := Closure(Successor(q,X));<br />

26 if q ′ �= ∅ then<br />

27 if q ′ /∈ Q then<br />

28 Q := Q ∪ {q ′ }<br />

29 fi ;<br />

30 δ := δ ∪ {q X → q ′ }<br />

31 fi<br />

32 od<br />

33 end.<br />

Zur Funktion Closure:<br />

Ist [X → α.Y β,L] gültig <strong>für</strong> ein zuverlässiges Präfix δα <strong>und</strong> ist Y → γ eine Alternative<br />

<strong>für</strong> Y , dann muß auch [Y → .γ] <strong>für</strong> δα gültig sein (nach Definition 43). Dann<br />

kann in einer Rechtssatzform jedes Symbol auf δαγ folgen, daß aus FIRST(βL) ist<br />

(β könnte leer sein). ε soll nicht in Vorausschaumengen auftreten, da die Eingabe<br />

mit $ abgeschlossen ist.<br />

Zur Funktion Successor:<br />

Die Vorausschaumengen der Items sind erst bei der Auswahl von Reduce-Items<br />

von Bedeutung. Aus diesem Gr<strong>und</strong> werden sie beim “Durchschieben”, also beim<br />

Übergang in einen Folgezustand, nicht modifiziert. Nach Definition 43 gilt: wenn<br />

[X → α.Y β,L] <strong>für</strong> γα gültig ist, dann ist [X → αY.β,L] gültig <strong>für</strong> γαY .<br />

Beispiel 42<br />

Wir betrachten wieder die aus Beispiel 37 auf Seite 88 bekannte Grammatik G zur<br />

Beschreibung arithmetischer Ausdrücke:<br />

S → E<br />

E → E + T | T<br />

T → T * F | F<br />

F → ( E ) | id<br />

In dem in Abbildung 3.18 angegebenen LR-DEA(G) sind die Zustände S1, S2 <strong>und</strong> S9<br />

ungeeignet, da sie jeweils einen Shift-Reduce-Konflikt enthalten.<br />

101


3 Syntaktische Analyse<br />

Wir berechnen die LR(1)-Items der Grammatik mit Hilfe des Algorithmus LR(1)-<br />

GEN auf Seite 100.<br />

Die Funktion Start liefert die Menge {[S → .E, {$}]}. Der Aufruf der Funktion Closure<br />

berechnet die ε-Folgezustände des Start-Items. Der “.” steht vor einem Nichtterminal<br />

E, also müssen wir die Items der Produktionen von E in den Anfangszustand<br />

des Automaten aufnehmen. Betrachten wir zunächst die Produktion E → E+T. Für<br />

die Vorausschaumenge dieses neuen Items ergibt sich β = ε <strong>und</strong> L = {$}, also ist<br />

das neue Item [E → .E+T, {$}]. In diesem Item steht der Punkt wieder vor dem<br />

Nichtterminal E, so daß wiederum die Items der Produktionen zu E aufgenommen<br />

werden müssen. Da das Item <strong>für</strong> den Kern E → .E+T schon in der Menge existiert,<br />

wird kein neues Item hinzugefügt, sondern die Vorausschaumenge aktualisiert.<br />

β = +T, also lautet das Item nun [E → .E+T, {$, +}]. Eine weitere Produktion von<br />

E ist E → T, so daß wir im ersten Schritt das Item [E → .T, {$}] <strong>und</strong> im zweiten<br />

das aktualisierte Item [E → .T, {$, +}] erhalten. Das letzte Item ergibt mit der Produktion<br />

T → T ∗F das neue Item<br />

[T → .T ∗F, {$, +, ∗}] <strong>und</strong> mit der Produktion T → F das Item<br />

[T → .F, {$, +, ∗}]. Letzteres ergibt mit der Produktion F → (E) das Item [F →<br />

.(E), {$, +, ∗}] (da β = ε in T → F). Dieses Item ergibt nichts neues, da der<br />

“.” hier vor einem Terminal steht. Entsprechend ergibt sich mit F → id das Item<br />

[F → .id, {$, +, ∗}].<br />

Wir nennen die Menge dieser Items, die wir durch den Aufruf Closure(Start) generiert<br />

haben, S ′ 0. Diese Menge entspricht dem Anfangszustand des Automaten zu G<br />

<strong>und</strong> wird als erstes Element in die Zustandsmenge Q aufgenommen.<br />

In den weiteren Schritten des Verfahrens werden die übrigen Zustände des Automaten<br />

berechnet, z.B.<br />

Successor(S ′ 0,E) = {[S → E., {$}], [E → E.+T, {$, +}]}<br />

= Closure(Successor(S ′ 0,E)).<br />

Diesen Zustand nennen wir S ′ 1. Die Berechnung der anderen Zustände erfolgt analog.<br />

Wir geben hier nur die Zustände S ′ 0, S ′ 1, S ′ 2 <strong>und</strong> S ′ 9 an:<br />

102


S ′ 0 = { [S → .E, {$}],<br />

[E → .E+T, {$, +}],<br />

[E → .T, {$, +}],<br />

[T → .T ∗F, {$, +, ∗}],<br />

[T → .F, {$, +, ∗}],<br />

[F → .(E), {$, +, ∗}],<br />

[F → .id, {$, +, ∗}]}<br />

S ′ 1 = { [S → E., {$}],<br />

[E → E.+T, {$, +}]}<br />

S ′ 2 = { [E → T., {$, +}],<br />

[T → T.∗F, {$, +, ∗}]}<br />

...<br />

S ′ 9 = { [E → E+T., {$, +}],<br />

[T → T.∗F, {$, +, ∗}]}<br />

3.4 Bottom-Up-Syntaxanalyse<br />

Im LR-DEA(G), der nur LR(0)-Items enthält, sind die Zustände S1, S2 <strong>und</strong> S9<br />

aufgr<strong>und</strong> von Shift-Reduce-Konflikten ungeeignet. Die Zustände S ′ 1, S ′ 2 <strong>und</strong> S ′ 9 des<br />

charakteristischen endlichen Automaten zu G enthalten diese Konflikte nicht mehr,<br />

da durch die Angabe von Vorausschaumengen (<strong>und</strong> damit der Einbeziehung des lookaheads)<br />

die Auswahl deterministisch getroffen werden kann. Zum Beispiel ist in S ′ 1 das<br />

+ nicht in der Vorausschaumenge des Reduce-Items enthalten, so daß bei einem + im<br />

lookahead ein Shift-Schritt <strong>und</strong> andernfalls ein Reduce-Schritt durchgeführt werden<br />

muß.<br />

Die Grammatik G zur Beschreibung arithmetischer Ausdrücke ist somit eine LR(1)-<br />

Grammatik.<br />

LR(1)-Parse-Tabellen<br />

Wie zuvor der LR-DEA(G) dient uns der charakteristische endliche Automat mit<br />

LR(1)-Items als Gr<strong>und</strong>lage <strong>für</strong> einen Parser. Wir wollen den Parser diesmal nicht als<br />

Kellerautomaten, sondern in algorithmischer Form angeben. Dazu verwenden wir,<br />

analog zur Top-Down-Analyse, eine Parse-Tabelle.<br />

Die Zeilen der Tabelle sind mit den Zuständen der Zustandsmenge Q des charakteristischen<br />

LR(1)-Automaten beschriftet. Die Tabelle ist in zwei Teiltabellen untergliedert:<br />

die action-Tabelle <strong>und</strong> die goto-Tabelle. Die Spalten der action-Tabelle sind<br />

mit den Terminalsymbolen <strong>und</strong> dem Endezeichen $ beschriftet. Das Feld action[q,a]<br />

gibt an, welche Aktion ausgeführt werden soll, wenn der Parser sich im Zustand q<br />

befindet <strong>und</strong> im lookahead das Zeichen a steht. Mögliche Aktionen sind<br />

103


3 Syntaktische Analyse<br />

Q q<br />

VT ∪ {$} VN ∪ VT<br />

a X<br />

Parser-Aktion <strong>für</strong><br />

(q,a)<br />

δ(q,X)<br />

action-Tabelle goto-Tabelle<br />

Abbildung 3.19: Struktur einer LR(1)-Parse-Tabelle.<br />

⎧<br />

⎪⎨<br />

action[q,a] =<br />

⎪⎩<br />

shift<br />

reduce X → α<br />

error<br />

accept<br />

Die goto-Tabelle dient zum Nachschlagen der Folgezustände des charakteristischen<br />

LR(1)-Automaten.<br />

Das folgende Verfahren konstruiert eine LR(1)-action-Tabelle.<br />

Konstruktion der LR(1)-action-Tabelle.<br />

Eingabe: LR(1)-Zustandsmenge Q<br />

Ausgabe: action-Tabelle<br />

Verfahren:<br />

104<br />

1. Für jeden Zustand q ∈ Q <strong>und</strong> <strong>für</strong> jedes LR(1)-Item [K,L] ∈ q:<br />

• falls K = S ′ → S. <strong>und</strong> L = {$}: trage accept in action[q, $] ein,<br />

• sonst<br />

– falls K = X → α., trage reduce X → α in action[q,a] <strong>für</strong> alle a ∈ L<br />

ein,<br />

– falls K = X → α.aβ, trage shift in action[q,a] ein.<br />

2. Trage in jedes noch <strong>und</strong>efinierte Feld error ein.


3.4 Bottom-Up-Syntaxanalyse<br />

Mit Hilfe der Parse-Tabelle kann der folgende Parse-Algorithmus die Analyse eines<br />

Eingabeworts deterministisch durchführen.<br />

LR(1)-Parse-Algorithmus.<br />

Eingabe: aus der action- <strong>und</strong> der goto-Tabelle bestehende LR(1)-Parse-Tabelle<br />

Ausgabe: Rechtsableitung von w, falls w ∈ L(G), sonst Fehlermeldung<br />

Verfahren:<br />

Anfangskonfiguration:<br />

q0 liegt auf dem Keller (q0 ist Anfangszustand des oben konstruierten charakteristischen<br />

endlichen Automaten), w$ steht im Eingabepuffer <strong>und</strong> der Eingabezeiger steht<br />

auf dem ersten Zeichen von links.<br />

1 repeat<br />

2 s e i q Zustand an der Spitze des Kellers <strong>und</strong> a aktuelles<br />

Eingabesymbol<br />

3 if action[q,a] = shift then<br />

4 lege goto[q,a] auf den Keller <strong>und</strong> r”ucke den<br />

Eingabezeiger vor<br />

5 else if action[q,a] = reduce X → α then begin<br />

6 entferne |α| Eintr”age vom Keller −− s e i nun q ′ oberster<br />

Kellereintrag ;<br />

7 lege goto[q ′ ,X] auf den Keller ; gib X → α aus<br />

8 end<br />

9 else if action[q,a] = accept then return<br />

10 else (∗ action[q,a] = error ∗) error<br />

11 end<br />

SLR(1), LALR(1)-Analyse<br />

Bei der Konstruktion eines LR(1)-Parsers ergibt sich das Problem, daß im allgemeinen<br />

mehr Zustände erzeugt werden, als dies bei der Generierung eines LR(0)-Parsers<br />

der Fall ist. Die Ursache hier<strong>für</strong> ist die in den Zuständen enthaltene Information über<br />

den Linkskontext, die sich über die Vorausschaumenge fortpflanzt <strong>und</strong> ein Aufsplitten<br />

der Zustände verursacht (siehe Definition 37).<br />

Beispiel 43 (vereinfachte C-Zuweisung)<br />

Wir betrachten die folgende Grammatik zur Beschreibung des (vereinfachten) Zuweisungsoperators<br />

der Sprache C [KR90]:<br />

S’ → S<br />

S → L = R | R<br />

L → * R | id<br />

R → L<br />

105


3 Syntaktische Analyse<br />

S0 = { [S ′ → .S],<br />

[S → .L=R],<br />

[S → .R],<br />

[L → .∗R],<br />

[L → .id],<br />

[R → .L]}<br />

S1 = { [S ′ → S.]}<br />

S2 = { [S → L.=R],<br />

[R → L.]}<br />

S3 = { [S → R.]}<br />

S4 = { [L → ∗.R],<br />

[R → .L],<br />

[L → .∗R],<br />

[L → .id]}<br />

S5 = { [L → id.]}<br />

S6 = { [S → L=.R],<br />

[R → .L],<br />

[L → .∗R],<br />

[L → .id]}<br />

S7 = { [L → ∗R.]}<br />

S8 = { [R → L.]}<br />

S9 = { [S → L=R.]}<br />

Abbildung 3.20: Zustandsmenge des LR-DEA der Grammatik zur Beschreibung der<br />

C-Zuweisung aus Beispiel 43.<br />

Nach Anwendung des Verfahrens zur Berechnung des LR-DEAs auf Seite 92 erhalten<br />

wir die in Abbildung 3.20 angegebene Zustandsmenge (zur Verbesserung der Lesbarkeit<br />

lassen wir die Mengenklammern bei einelementigen Vorausschaumengen weg).<br />

Aufgr<strong>und</strong> der Differenzierung nach Vorausschaumengen werden die Zustände des LR-<br />

DEA in mehrere Zustände des LR(1)-Automaten aufgesplittet. Zum Beispiel wird der<br />

Zustand S4 aus Abbildung 3.20 in die Zustände I4 <strong>und</strong> I11 in Abbildung 3.21 aufgeteilt,<br />

deren Items dieselben Kerne, aber unterschiedliche Vorausschaumengen besitzen. Der<br />

LR(1)-Automat ist in Abbildung 3.22 dargestellt.<br />

Daher ergeben sich die folgenden Beziehungen zwischen LR(0)- <strong>und</strong> LR(1)-Items:<br />

S0 ∼ I0 S1 ∼ I1 S2 ∼ I2 S3 ∼ I3 S4 ∼ I4 <strong>und</strong> I11<br />

S5 ∼ I5 <strong>und</strong> I12 S6 ∼ I6 S7 ∼ I7 <strong>und</strong> I13 S8 ∼ I8 <strong>und</strong> I10 S9 ∼ I9<br />

Im LR-DEA in Abbildung 3.20 tritt das LR(0)-Item [R → .L] in den Zuständen S0<br />

<strong>und</strong> S4 auf. Durch das Shiften eines L gelangen wir aus beiden Zuständen in den<br />

Zustand S8. In Abbildung 3.21 unterscheiden sich die LR(1)-Items [R → .L, $] in<br />

I0 <strong>und</strong> [R → .L, {=, $}] in I4 durch ihre Vorausschaumengen. Aus I0 kann ein L-<br />

Übergang nach I10 erfolgen, aus I4 ein Übergang nach I8. Befinden wir uns also im<br />

Zustand I10, wissen wir eindeutig, daß wir aus I0 gekommen sind. Befinden wir uns<br />

im Zustand I8, sind wir aus I4 gekommen. Auf diese Weise enthalten die Zustände<br />

106


I0 = { [S ′ → .S, $],<br />

[S → .L=R, $],<br />

[S → .R, $],<br />

[L → .∗R, {=, $}],<br />

[L → .id, {=, $}],<br />

[R → .L,$]}<br />

I1 = { [S ′ → S., $]}<br />

I2 = { [S → L.=R, $],<br />

[R → L.,$]}<br />

I3 = { [S → R., $]}<br />

I4 = { [L → ∗.R, {=, $}],<br />

[R → .L, {=, $}],<br />

[L → .∗R, {=, $}],<br />

[L → .id, {=, $}]}<br />

I5 = { [L → id., {=, $}]}<br />

I6 = { [S → L=.R, $],<br />

[R → .L,$],<br />

[L → .∗R, $],<br />

[L → .id, $]}<br />

I7 = { [L → ∗R., {=, $}]}<br />

I8 = { [R → L., {=, $}]}<br />

I9 = { [S → L=R., $]}<br />

I10 = { [R → L.,$]}<br />

I11 = { [L → ∗.R, $],<br />

[R → .L,$],<br />

[L → .∗R, $],<br />

[L → .id, $]}<br />

I12 = { [L → id.,$]}<br />

I13 = { [L → ∗R., $]}<br />

3.4 Bottom-Up-Syntaxanalyse<br />

Abbildung 3.21: Zustandsmenge des charakteristischen endlichen Automaten mit<br />

LR(1)-Items zur Grammatik aus Beispiel 43.<br />

des LR(1)-Parsers Informationen über den Linkskontext, also die bisherige Ableitung.<br />

Dies ist im LR-DEA nicht möglich. Wenn wir uns im Zustand S8 befinden, können<br />

wir nicht unterscheiden, ob wir uns zuvor in S0 oder in S4 bef<strong>und</strong>en haben.<br />

Die durch die Einführung von Vorausschaumengen entstehende Aufsplittung von<br />

Zuständen ist bei der Analyse von realen Programmiersprachen ein wirkliches Problem,<br />

da die Zustandsmenge drastisch ansteigen kann (von h<strong>und</strong>erten zu tausenden<br />

von Zuständen).<br />

Aus diesem Gr<strong>und</strong> verwendet man oft Teilklassen der LR(k)-Sprachen <strong>und</strong> Zustandsmengen,<br />

die die gleiche Größe haben wie die von LR(0)-Grammatiken. Diese Teilklassen<br />

sind die SLR(k) (simple LR(k)) <strong>und</strong> die LALR(k)-Sprachen (lookahead LR(k)).<br />

SLR(1)-Parser:<br />

Wir gehen vom LR-DEA eines LR(0)-Parsers aus. Die Idee bei den SLR(1)-Parsern<br />

ist, als Vorausschaumenge eines Items die FOLLOW1-Menge des Nichtterminals zu<br />

verwenden, das in dem betreffenden Item auf der linken Seite des Kerns steht.<br />

Der Aufbau einer SLR(1)-Parse-Tabelle ist zu dem einer LR(1)-Tabelle identisch<br />

(siehe Abbildung 3.19). Wir modifizieren das Konstruktionsverfahren <strong>für</strong> die action-<br />

Tabelle auf Seite 104 wie im folgenden angegeben. Der Parse-Algorithmus auf Seite<br />

107


3 Syntaktische Analyse<br />

S<br />

I I<br />

0 1<br />

L =<br />

R<br />

I I I<br />

id<br />

R<br />

*<br />

I<br />

R<br />

I I<br />

id<br />

I<br />

3<br />

2 6 9<br />

4 7<br />

5<br />

*<br />

L<br />

I 8<br />

*<br />

L<br />

id<br />

I<br />

L<br />

I I<br />

11<br />

R<br />

id<br />

Abbildung 3.22: Charakteristischer endlicher LR(1)-Automat zur Grammatik aus<br />

Beispiel 43.<br />

108<br />

I<br />

10<br />

12<br />

*<br />

13


105 wird unverändert übernommen.<br />

3.4 Bottom-Up-Syntaxanalyse<br />

Konstruktion der SLR(1)-action-Tabelle.<br />

Eingabe: LR(0)-Zustandsmenge Q<br />

Ausgabe: action-Tabelle<br />

Verfahren:<br />

1. Für jeden Zustand q ∈ Q <strong>und</strong> <strong>für</strong> jedes [K] ∈ q:<br />

• falls K = S ′ → S.: trage accept in action[q, $] ein,<br />

• sonst<br />

– falls K = X → α., trage reduce X → α in action[q,a] <strong>für</strong> alle a ∈<br />

FOLLOW(X) ein,<br />

– falls K = X → α.aβ, trage shift in action[q,a] ein.<br />

2. Trage in jedes noch <strong>und</strong>efinierte Feld error ein.<br />

Da <strong>für</strong> jedes Nichtterminal die FOLLOW1-Menge eindeutig bestimmt ist, kommt<br />

es nicht zum Aufsplitten von Zuständen, so daß die Zustandsmenge eines SLR(1)-<br />

Parsers gegenüber der eines LR(0)-Parsers unverändert bleibt.<br />

Nachteilig ist, daß nicht <strong>für</strong> alle LR(1)-Sprachen ein SLR(1)-Parser existiert, da durch<br />

die “Vergröberung” der Vorausschaumengen der Items ein Informationsverlust entsteht,<br />

wie im folgenden Beispiel zu sehen ist.<br />

Beispiel 44 (Fortsetzung von Beispiel 43)<br />

In Abbildung 3.20 ist S2 der einzige ungeeignete Zustand. Wir berechnen die Zustandsmenge<br />

des charakteristischen endlichen Automaten mit SLR(1)-Items (Abbildung<br />

3.23). Es gilt FOLLOW1(S) = {$} <strong>und</strong> FOLLOW1(L) = FOLLOW1(R) =<br />

{=, $}. Bei der Erweiterung des vollständigen Items [R → L.] um die Vorausschaumenge<br />

{=, $} bleibt der Shift-Reduce-Konflikt aus S2 in S ′ 2 erhalten, da das zu lesende<br />

Symbol “=” in der Vorausschaumenge des vollständigen Items enthalten ist. Daher<br />

ist die Grammatik in Beispiel 43 keine SLR(1)-Grammatik.<br />

LALR(1)-Parser:<br />

Bei der Konstruktion eines LR(1)-Parsers kann es Zustände geben, die bzgl. der<br />

Kerne der Items übereinstimmen (z.B. die Zustände I4 <strong>und</strong> I11 in Abbildung 3.21).<br />

Um die Zustandsmenge zu verkleinern, kann man versuchen, diese Zustände zu einem<br />

Zustand zusammenzufassen, indem man die Vorausschaumengen der Items mit<br />

gleichem Kern vereinigt. Die Zustandszahl eines Automaten, bei dem Zustände mit<br />

gleichen Kernen zusammengefaßt wurden, entspricht der Anzahl des LR(0)-Parsers.<br />

Ein solcher Parser heißt LALR(1)-Parser.<br />

109


3 Syntaktische Analyse<br />

S ′ 0 = { [S′ → .S, {$}],<br />

[S → .L=R, {$}],<br />

[S → .R, {$}],<br />

[L → .∗R, {=, $}],<br />

[L → .id, {=, $}],<br />

[R → .L, {=, $}]}<br />

S ′ 1 = { [S′ → S., {$}]}<br />

S ′ 2<br />

S ′ 3<br />

S ′ 4<br />

= { [S → L.=R, {$}],<br />

[R → L., {=, $}]}<br />

= { [S → R., {$}]}<br />

= { [L → ∗.R, {=, $}],<br />

[R → .L, {=, $}],<br />

[L → .∗R, {=, $}],<br />

[L → .id, {=, $}]}<br />

S ′ 5<br />

S ′ 6<br />

S ′ 7<br />

S ′ 8<br />

S ′ 9<br />

= { [L → id., {=, $}]}<br />

= { [S → L=.R, {$}],<br />

[R → .L, {=, $}],<br />

[L → .∗R, {=, $}],<br />

[L → .id, {=, $}]}<br />

= { [L → ∗R., {=, $}]}<br />

= { [R → L., {=, $}]}<br />

= { [S → L=R., {$}]}<br />

Abbildung 3.23: Zustandsmenge des charakteristischen endlichen Automaten mit<br />

SLR(1)-Items <strong>für</strong> die Grammatik aus Beispiel 43.<br />

Bei der Vereinigung der Zustände ist zu beachten, daß durch diese Vereinigung neue<br />

Konflikte entstehen können. Daher ist diese Methode nicht auf alle LR(1)-Sprachen<br />

anwendbar, so daß nicht alle LR(1)-Sprachen durch LALR(1)-Parser akzeptiert werden<br />

können.<br />

Die Vorausschaumenge eines Items in einem Zustand kann auch direkt ohne die<br />

vorherige Konstruktion eines LR(1)-Parsers berechnet werden:<br />

LAL(q, [X → α.]) = {a ∈ VT | S ′ ⇒ βXaw <strong>und</strong> δ ∗ d(qd,βα) = q}<br />

Dabei ist δd die Übergangsfunktion des LR-DEA(G). In LAL(q, [X → α.]) sind also<br />

nur noch Terminalsymbole, die auf X in einer Rechtssatzform folgen können <strong>und</strong> die<br />

den charakteristischen endlichen Automaten LR-DEA(G) in den Zustand q bringen.<br />

Die Definition von LAL(q, [X → α.]) ist nicht konstruktiv, da in ihr i. allg. unendliche<br />

Mengen von Rechtssatzformen auftreten. Ein Verfahren zur effizienten der<br />

Vorausschaumengen <strong>für</strong> LALR(1)-Parser ist in [WM96] angegeben.<br />

Beispiel 45 (Fortsetzung von Beispiel 43)<br />

Wir erzeugen einen LALR(1)-Automaten, indem wir die Zustände in Abbildung 3.21,<br />

deren Items dieselben Kerne besitzen, zu einem Zustand zusammenfassen <strong>und</strong> die zugehörigen<br />

Vorausschaumengen vereinigen (Abbildung 3.25). In Abbildung 3.20 ist S2<br />

der einzige ungeeigenete Zustand. Die Vorausschaumenge <strong>für</strong> das vollständige Item<br />

110


S ′′<br />

0 = { [S′ → .S, {$}],<br />

[S → .L=R, {$}],<br />

[S → .R, {$}],<br />

[L → .∗R, {=, $}],<br />

[L → .id, {=, $}],<br />

[R → .L, {$}]}<br />

S ′′<br />

1 = { [S′ → S., {$}]}<br />

S ′′<br />

2<br />

S ′′<br />

3<br />

S ′′<br />

4<br />

= { [S → L.=R, {$}],<br />

[R → L., {$}]}<br />

= { [S → R., {$}]}<br />

= { [L → ∗.R, {=, $}],<br />

[R → .L, {=, $}],<br />

[L → .∗R, {=, $}],<br />

[L → .id, {=, $}]}<br />

S ′′<br />

5<br />

S ′′<br />

6<br />

S ′′<br />

7<br />

S ′′<br />

8<br />

S ′′<br />

9<br />

3.4 Bottom-Up-Syntaxanalyse<br />

= { [L → id., {=, $}]}<br />

= { [S → L=.R, {$}],<br />

[R → .L, {$}],<br />

[L → .∗R, {$}],<br />

[L → .id, {$}]}<br />

= { [L → ∗R., {=, $}]}<br />

= { [R → L., {=, $}]}<br />

= { [S → L=R., {$}]}<br />

Abbildung 3.24: Zustandsmenge des charakteristischen endlichen Automaten mit<br />

LALR(1)-Items <strong>für</strong> die Grammatik aus Beispiel 43.<br />

in S ′′<br />

2 ist {$}. Damit ist der Shift-Reduce-Konflikt aus S2 im Zustand S ′′<br />

2 beseitigt.<br />

Da es keine weiteren ungeeigneten Zustände gibt, ist die Grammatik in Beispiel 43<br />

eine LALR(1)-Grammatik. Die Zustandsmenge des charakteristischen endlichen Automaten<br />

mit LALR(1)-Items ist in Abbildung 3.24 angegeben.<br />

Definition 45<br />

Eine kontextfreie Grammatik, <strong>für</strong> die das SLR- (LALR-) Verfahren keine ungeeigneten<br />

Zustände ergibt, nennen wir eine SLR- (LALR-) Grammatik.<br />

Das LALR(1)-Parseverfahren wird insbesondere in Parser-Generatoren wie z.B. yacc<br />

[MB92] verwendet. Diese Programme erzeugen <strong>für</strong> eine Eingabedatei, die eine Sprachbeschreibung<br />

in Form einer kontextfreien Grammatik (<strong>und</strong> eventuell zusätzliche Informationen)<br />

enthält, einen Parser, der die von der Grammatik beschriebene Sprache<br />

erkennt. Dieser Parser liegt oft in Form von Quelltext vor, so daß er in Programme<br />

integriert werden kann (yacc erzeugt Parser in der Sprache C [KR90]). Das LALR-<br />

Verfahren eignet sich besonders <strong>für</strong> Parser-Generatoren, da die Zustandsmenge des<br />

zugehörigen Automaten effizient berechenbar ist.<br />

Für weitere Informationen zu SLR- <strong>und</strong> LALR-Parsern verweisen wir auf [WM96]<br />

<strong>und</strong> [ASU99].<br />

111


3 Syntaktische Analyse<br />

S<br />

I I<br />

0 1<br />

L =<br />

R<br />

I I I<br />

id<br />

R<br />

*<br />

I<br />

R<br />

I I<br />

id<br />

I<br />

3<br />

2 6 9<br />

4 7<br />

5<br />

*<br />

L<br />

I 8<br />

*<br />

L<br />

id<br />

I<br />

L<br />

I I<br />

11<br />

R<br />

id<br />

Abbildung 3.25: Charakteristischer endlicher LR(1)-Automat zur Grammatik aus<br />

Beispiel 43.<br />

112<br />

I<br />

10<br />

12<br />

*<br />

13


3.4.2 Fehlerbehandlung bei der Bottom-Up-Analyse<br />

3.4 Bottom-Up-Syntaxanalyse<br />

Ein LR(k)-Parser erkennt einen Fehler, sobald er in der action-Tabelle auf einen Fehlereintrag<br />

trifft (nicht beim Nachsehen in der goto-Tabelle). Bei einer LR(1)-Analyse<br />

führt der Parser zwischen Vorliegen <strong>und</strong> Erkennen eines Fehlers keine weiteren Reduktionen<br />

durch, während die SLR(1)- <strong>und</strong> LALR(1)-Parser eventuell noch Reduktionen<br />

vornehmen (aufgr<strong>und</strong> der weniger differenzierten Vorausschaumengen). Bei<br />

allen Verfahren werden aber zwischen dem Vorliegen eines Fehlers <strong>und</strong> dem Erkennen<br />

der Fehlersituation durch den Parser keine Shift-Schritte mehr ausgeführt.<br />

Wie bei der Fehlerbehandlung der Top-Down-Analyse stehen mehrere Verfahren zur<br />

Auswahl.<br />

Panic recovery: Das Verfahren der panic recovery überliest beim Auftreten eines<br />

Fehlers einen Teil der Eingabe bis zu einem synchronisierenden Symbol.<br />

Zuerst wird der Keller von oben her durchsucht, bis ein Zustand s gef<strong>und</strong>en wird,<br />

zu dem es <strong>für</strong> ein Nichtterminal A einen Eintrag in der goto-Tabelle gibt. Es wird<br />

geprüft, ob die Verwendung dieses Eintrags eine Aktion des Parsers ermöglichen wird.<br />

Ist dies der Fall, wird der Keller bis zu diesem Zustand gelöscht (s selbst wird nicht<br />

entfernt) <strong>und</strong> goto[s,A] auf den Keller gelegt. Dann wird die Eingabe bis zu einem<br />

Symbol aus FOLLOW(A) gelöscht. Auf diese Weise täuscht der Parser vor, daß der<br />

zu A gehörende Teil der Eingabe erfolgreich analysiert wurde <strong>und</strong> daher die Analyse<br />

fortgesetzt werden kann. Auf die weiteren Verfahren zur Fehlerbehandlung gehen wir<br />

an dieser Stelle nicht ein <strong>und</strong> verweisen auf [WM96] <strong>und</strong> [ASU99].<br />

Beispiel 46 (Fortsetzung von Beispiel 42)<br />

Gegeben sei die Grammatik G zur Erzeugung arithmetischer Ausdrücke.<br />

S → E (1)<br />

E → E + T (2)<br />

E → T (3)<br />

T → T ∗ F (3)<br />

T → F (3)<br />

F → (E) (3)<br />

F → id (7)<br />

Wir wollen untersuchen, welche Aktionen ein LR-Parser mit Panic-Recovery-Strategie<br />

<strong>für</strong> G bei der Eingabe des fehlerhaften Wortes ”id+(∗id))+id” durchführt.<br />

Die Zustände:<br />

113


3 Syntaktische Analyse<br />

I0 : S → .E ,{$}<br />

E → .E + T ,{$, +}<br />

E → .T ,{$, +}<br />

T → .T ∗ F .{$, *, +}<br />

T → .F .{$, *, +}<br />

F → .id .{$, *, +}<br />

F → .(E) .{$, *, +}<br />

I1 : S → E. ,{$}<br />

E → E. + T ,{$, +}<br />

I2 : F → id. .{$, *, +}<br />

I3 : T → F. .{$, *, +}<br />

I4 : F → (.E) .{$, *, +}<br />

E → .E + T ,{), +}<br />

E → .T ,{), +}<br />

T → .T ∗ F .{), *, +}<br />

T → .F .{), *, +}<br />

F → .id .{), *, +}<br />

F → .(E) .{), *, +}<br />

I5 : E → E + .T ,{$, +}<br />

T → .T ∗ F .{$, *, +}<br />

T → .F .{$, *, +}<br />

F → .id .{$, *, +}<br />

F → .(E) .{$, *, +}<br />

I6 : F → (E.) .{$, *, +}<br />

E → E. + T ,{), +}<br />

I7 : E → E + T. ,{$, +}<br />

T → T. ∗ F .{$, *, +}<br />

I8 : T → T ∗ .F .{$, *, +}<br />

F → .id .{$, *, +}<br />

F → .(E) .{$, *, +}<br />

I9 : T → T ∗ F. .{$, *, +}<br />

I10 : E → T. ,{$, +}<br />

T → T. ∗ F .{$, *, +}<br />

I11 : F → (E). .{$, *, +}<br />

114<br />

I ′ 2 : F → id. .{), *, +}<br />

I ′ 3 : T → F. .{), *, +}<br />

I ′ 4 : F → (.E) .{), *, +}<br />

E → .E + T ,{), +}<br />

E → .T ,{), +}<br />

T → .T ∗ F .{), *, +}<br />

T → .F .{), *, +}<br />

F → .id .{), *, +}<br />

F → .(E) .{), *, +}<br />

I ′ 5 : E → E + .T ,{), +}<br />

T → .T ∗ F .{), *, +}<br />

T → .F .{), *, +}<br />

F → .id .{), *, +}<br />

F → .(E) .{), *, +}<br />

I ′ 6 : F → (E.) .{), *, +}<br />

E → E. + T ,{), +}<br />

I ′ 7 : E → E + T. ,{), +}<br />

T → T. ∗ F .{), *, +}<br />

I ′ 8 : T → T ∗ .F .{), *, +}<br />

F → .id .{), *, +}<br />

F → .(E) .{), *, +}<br />

I ′ 9 : T → T ∗ F. .{), *, +}<br />

I ′ 10 : E → T. ,{), +}<br />

T → T. ∗ F .{), *, +}<br />

I ′ 11 : F → (E). .{), *, +}


3.4 Bottom-Up-Syntaxanalyse<br />

action goto<br />

Zustand + * id ( ) $ + * id ( ) S E T F<br />

I0 sync sync s s sync sync I2 I4 I1 I10 I3<br />

I1 s acc I5<br />

I2 r7 r7 r7<br />

I3 r5 r5 r5<br />

I4 sync sync s s sync sync I ′ 2 I ′ 4 I6 I ′ 10 I ′ 3<br />

I5 sync sync s s sync sync I2 I4 I7 I3<br />

I6 s s I5 I11<br />

I7 r2 s r2 I8<br />

I8 sync sync s s sync sync I2 I4 I9<br />

I9 r4 r4 r4<br />

I10 r3 s r3 I8<br />

I11 r6 r6 r6<br />

I ′ 2 r7 r7 r7<br />

I ′ 3 r5 r5 r5<br />

I ′ 4 sync sync s s sync sync I ′ 2 I ′ 4 I ′ 6 I ′ 10 I ′ 3<br />

I ′ 5 sync sync s s sync sync I ′ 2 I ′ 4 I ′ 7 I ′ 3<br />

I ′ 6 s s I ′ 5 I11 ′<br />

I ′ 7 r2 s r2 I ′ 8<br />

I ′ 8 sync sync s s sync sync I ′ 2 I ′ 4 I ′ 9<br />

I ′ 9 r4 r4 r4<br />

I ′ 10 r3 s r3 I ′ 8<br />

I ′ 11 r6 r6 r6<br />

FOLLOW(S)= {$}<br />

FOLLOW(E)= {$, +, )}<br />

FOLLOW(T)= {$, +, ), *}<br />

FOLLOW(F)= {$, +, ), *}<br />

rX reduce nach Regel X<br />

s shift<br />

sync synchronisierendes Symbol <strong>für</strong> F<br />

acc akzeptieren<br />

Fehler 1: Keine Aktion möglich, aber zu dem Zustand 4 gibt es einen Sprungeintrag<br />

goto(I4,F) = I ′ 3. Die Eingabe wird bis zu dem synchronisierenden Symbol gelöscht<br />

(hier wird nichts gelöscht, da ∗ ∈ FOLLOW(F)). Es wird I ′ 3 auf den Stack gelegt.<br />

Fehler 2: Zu I8 gibt es einen Sprungeintrag zu I9 mit F. Dieser erlaubt jedoch keine<br />

weitere Aktion (action(I9, ′ ) ′ ) = ǫ). Deshalb wird stattdessen ) überlesen.<br />

115


3 Syntaktische Analyse<br />

Fehler 3: Da es einen Sprungeintrag gibt, kann synchronisiert werden. Das synchronisierende<br />

Symbol ist ’+’.<br />

Eingabe Keller Aktion<br />

id+(*id))+id$ I0 ǫ<br />

+(*id))+id$ I2I0 F → id<br />

F+(*id))+id$ I0 ǫ<br />

+(*id))+id$ I3I0 T → F<br />

T+(*id))+id$ I0 ǫ<br />

+(*id))+id$ I10I0 E → T<br />

E+(*id))+id$ I0 ǫ<br />

+(*id))+id$ I1I0 ǫ<br />

(*id))+id$ I5I1I0 ǫ<br />

*id))+id$ I4I5I1I0 sync = Fehler 1<br />

*id))+id$ I ′ 3I4I5I1I0 T → F<br />

T*id))+id$ I4I5I1I0 ǫ<br />

*id))+id$ I ′ 10I4I5I1I0 ǫ<br />

id))+id$ I ′ 8I ′ 10I4I5I1I0 ǫ<br />

))+id$ I ′ 2I ′ 8I ′ 10I4I5I1I0 F → id<br />

F))+id$ I ′ 8I ′ 10I4I5I1I0 ǫ<br />

))+id$ I ′ 9I ′ 2I ′ 8I ′ 10I4I5I1I0 T → T ∗ F<br />

T))+id$ I4I5I1I0 ǫ<br />

))+id$ I ′ 10I4I5I1I0 E → T<br />

E))+id$ I4I5I1I0 ǫ<br />

))+id$ I6I4I5I1I0 ǫ<br />

)+id$ I8I6I4I5I1I0 sync = Fehler 2, Überlesen<br />

+id$ I8I6I4I5I1I0 sync = Fehler 3<br />

+id$ I9I6I4I5I1I0 T → T ∗ F<br />

T+id$ I5I1I0 ǫ<br />

+id$ I7I5I1I0 E → E + T<br />

E+id$ I0 ǫ<br />

+id$ I1I0 ǫ<br />

id$ I5I1I0 ǫ<br />

$ I2I5I1I0 F → id<br />

F$ I5I1I0 ǫ<br />

$ I3I5I1I0 T → F<br />

T$ I5I1I0 ǫ<br />

$ I7I5I1I0 E → E + T<br />

E$ I0 ǫ<br />

$ I1I0 acc<br />

In [ASU99], Band 1, S. 311–313, befindet sich eine ähnliche Grammatik, in der <strong>für</strong><br />

116


jede fehlerhafte Aktion eine Fehlerroutine aufgerufen wird.<br />

3.5 Parser Generatoren<br />

3.5 Parser Generatoren<br />

Parser Generatoren dienen der automatischen Generierung eines Parsers <strong>für</strong> eine<br />

entsprechende Sprachspezifikation. Die Spezifikation wird in Form einer Grammatik<br />

(BNF, EBNF, ...) in einer generatorspeizifischen Sprache hinterlegt. Als Ausgabe<br />

erzeugt der Generator ein Programm, das einen Parser <strong>für</strong> diese Sprache in einer<br />

vorher angegeben Programmiersprache implementiert. Der Umfang der möglichen<br />

Zielsprachen ist abhängig vom jeweiligen Generator. Häufig ist der Parser Generator<br />

auch Teil eines Compiler-Generators (Compiler-Compiler) wobei in verschiedenen<br />

Implementationen gelegentlich eine Trennung zwischen Scanner-Generator (Lexer)<br />

<strong>und</strong> Parser-Generator (intern oder extern) vorgenommen wird. Hinzu kommt meistens<br />

auch noch ein Tree Parser der zur Verarbeitung des konstruierten Syntaxbaums<br />

dient.<br />

Die Vorteile solcher <strong>Systeme</strong> sind die Nachweisbarkeit der Korrektheit des generierten<br />

Compilers <strong>und</strong> das Einsparen aufwendiger <strong>und</strong> fehleranfälliger Programmierarbeit.<br />

Einige Beispiele <strong>für</strong> Generatoren sind:<br />

• Lex/Yacc [JL92][LYP]<br />

• ANTLR [ANT]<br />

• JavaCC [JAV]<br />

• Coco/R [HM]<br />

• SableCC [SAB]<br />

117


4 Semantische Analyse<br />

In der abschließenden Analysephase des Compilers wird, nach der Überprüfung der<br />

lexikalischen <strong>und</strong> syntaktischen Struktur des zu übersetzenden Quelltextes, die statische<br />

Semantik des Programmes untersucht.<br />

Man bezeichnet eine (nicht kontextfreie) Eigenschaft eines Konstrukts einer Programmiersprache<br />

als eine statische semantische Eigenschaft, wenn<br />

1. <strong>für</strong> jedes Vorkommen dieses Konstrukts in einem Programm der “Wert” dieser<br />

Eigenschaft <strong>für</strong> alle (dynamischen) Ausführungen des Konstrukts gilt,<br />

2. <strong>für</strong> jedes Vorkommen des Konstrukts in einem konkreten Programm diese Eigenschaft<br />

berechnet werden kann.<br />

Beispiele <strong>für</strong> statische semantische Eigenschaften sind<br />

• Definiertheit, Sichtbarkeit <strong>und</strong> Gültigkeit: hierbei wird überprüft, ob die im Programm<br />

verwendeten Bezeichner (z.B. Variablennamen <strong>und</strong> Namen von Unterprogrammen)<br />

zuvor deklariert wurden. Wird ein Bezeichner mehrfach in einem<br />

Programm deklariert (z.B. in verschiedenen Unterprogrammen), muß bei einem<br />

Auftreten eines Bezeichners die aktuell gültige Deklaration zugeordnet werden<br />

können.<br />

Die Überprüfung dieser Eigenschaften ist mit dem Ansatz der kontextfreien<br />

Grammatiken nicht durchführbar. Zum Beispiel läßt sich die Deklaration eines<br />

Bezeichners w <strong>und</strong> dessen Verwendung im folgenden Programmtext auf die<br />

Sprache {wcw | w ∈ (a|b) ∗ } zurückführen, die mit kontextfreien Grammatiken<br />

nicht erzeugt werden kann (siehe [Sch08]).<br />

• Typisierung: Den Werten, Variablen <strong>und</strong> Unterprogrammen eines Programms<br />

werden in den meisten Programmiersprachen Typen zugeordnet. Typen sind<br />

Wertemengen wie z.B. boolesche Werte, ganze Zahlen, Fließkommazahlen <strong>und</strong><br />

Zeichen. Zur semantischen Analyse gehört die Überprüfung auf korrekte Typisierung<br />

eines Programms. In “stark getypten” Programmiersprachen ist der<br />

Typ jedes Ausdrucks zur Übersetzungszeit bestimmbar.<br />

Weiterhin umfaßt die Überprüfung der Semantik<br />

118<br />

• Überprüfungen auf Eindeutigkeit (z.B. darf innerhalb einer case-Anweisung<br />

nicht zweimal dasselbe Label auftreten),<br />

• auf Namen bezogene Überprüfungen (z.B. muß in Ada [Bar83] der Name eines<br />

Blockes am Anfang <strong>und</strong> am Ende des Blockes auftreten).


E<br />

E + E<br />

integer<br />

real<br />

real<br />

4.1 Attributierte Grammatiken<br />

Abbildung 4.1: Beispiel eines attributierten Strukturbaums.<br />

Wir werden bei der Beschreibung der semantischen Analyse vor allem auf die Typüberprüfungen<br />

eingehen.<br />

4.1 Attributierte Grammatiken<br />

Als theoretisches Hilfsmittel <strong>für</strong> die semantische Analyse verwenden wir attributierte<br />

Grammatiken. Dabei wird der von der Syntaxanalyse erzeugte Strukturbaum, der<br />

die syntaktische Struktur des zu analysierenden Programmtextes darstellt, durch Zusatzinformationen<br />

ergänzt, die die Überprüfung der statischen semantischen Eigenschaften<br />

ermöglichen. Wir erhalten einen sogenannten attributierten Strukturbaum.<br />

Beispiel 46<br />

In Abbildung 4.1 ist ein Ausschnitt aus einem Strukturbaum angegeben, der die Anwendung<br />

eines Additionsoperators auf zwei Teilausdrücke beschreibt. Den Nichtterminalen<br />

E sind Attribute zugeordnet, die den Typ des Ausdrucks, <strong>für</strong> den das jeweilige<br />

Nichtterminal steht, angeben. Das Attribut Typ ist <strong>für</strong> alle Auftreten des Nichtterminals<br />

E definiert, aber jedes Auftreten von E besitzt einen eigenen Attributwert.<br />

Der Typ des linken Teilbaums ist integer, der des rechten Teilbaums ist real. Die<br />

semantische Analyse kann aus diesen Informationen ermitteln, daß der Typ des Gesamtausdrucks<br />

real 1 sein muß <strong>und</strong> ordnet der Wurzel des Baums den Attributwert<br />

real zu.<br />

Den Grammatiksymbolen werden also zusätzliche Plätze (Attribute) zur Aufnahme<br />

von Informationen zugeordnet.<br />

Die Berechnung der Attributwerte im attributierten Strukturbaum wird dabei durch<br />

semantische Regeln definiert. Jeder Produktion der Grammatik werden semantische<br />

Regeln zugeordnet, die beschreiben, wie die Attributwerte der in der Produktion<br />

vorkommenden Symbole berechnet werden.<br />

Wir unterscheiden folgende Begriffe:<br />

1 Andernfalls würden bei der Addition eventuelle Nachkommastellen verloren gehen<br />

119


4 Semantische Analyse<br />

Abbildung 4.2: Synthetische <strong>und</strong> inherite Attribute.<br />

• Attribute sind Grammatiksymbolen zugeordnete Plätze,<br />

• Attributvorkommen entsprechen dem Vorkommen der zugehörigen Grammatiksymbole<br />

in Produktionen,<br />

• Attributexemplare sind die konkreten Auftreten von Attributen in einem Strukturbaum<br />

(ihnen werden die Attributwerte zugeordnet).<br />

Wir unterscheiden zwei Arten von Attributen:<br />

• synthetische Attribute: die Werte dieser Attribute werden im Strukturbaum<br />

bottom-up, also von den Blättern ausgehend zur Wurzel hin, berechnet (siehe<br />

den linken Baum in Abbildung 4.2),<br />

• inherite Attribute: die Werte dieser Attribute werden im Strukturbaum topdown,<br />

also von der Wurzel ausgehend zu den Blättern hin, berechnet, wobei<br />

hier jeweils auch Werte von Geschwisterknoten verwendet werden dürfen (siehe<br />

den rechten Baum in Abbildung 4.2).<br />

Durch die Kombination von synthetischen <strong>und</strong> inheriten Attributen ist ein Informationstransfer<br />

zwischen Teilbäumen möglich.<br />

Beispiel 47<br />

Wir nehmen an, daß ein Programm (Nichtterminal P) aus einem Deklarationsblock<br />

(Nichtterminal D) <strong>und</strong> einem Anweisungsteil (Nichtterminal S) besteht, die durch<br />

ein Semikolon voneinander getrennt sind (die Produktion lautet also P → D;S).<br />

Im Deklarationsteil werden die Typen der Bezeichner, die im Anweisungsteil verwendet<br />

werden, deklariert. Innerhalb des Strukturbaums, der den Deklarationsblock<br />

beschreibt, werden die Typen der einzelnen Bezeichner durch synthetische Attribute<br />

“gesammelt” (siehe Abbildung 4.3). Im Anweisungsteil werden diese Typinformationen<br />

benötigt, daher werden sie als inherite Attribute bei der Analyse des Anweisungsteils<br />

miteinbezogen.<br />

Wir erläutern nun zunächst die Konzepte der attributierten Grammatiken anhand<br />

eines Beispiels. Wir betrachten eine kontextfreie Grammatik, die rationale Zahlen<br />

in Form von Binärziffern definiert. Durch die Angabe semantischer Regeln wird die<br />

Berechnung des Werts einer rationalen Zahl ermöglicht.<br />

120


P<br />

D ; S<br />

4.1 Attributierte Grammatiken<br />

Abbildung 4.3: Berechnung von Typinformationen im Strukturbaum.<br />

Beispiel 48 (Binärzahlen)<br />

Wir betrachten die folgende kontextfreie Grammatik zur Beschreibung von rationalen<br />

Zahlen im Binärformat (N ist Startsymbol) [KV97]:<br />

N → L | L . L<br />

L → B | L B<br />

B → 0 | 1<br />

Wir geben nun semantische Regeln an, die eine Attributierung zur Berechnung des<br />

Zahlwerts der Binärzahlen erlauben.<br />

Jedes Nichtterminalsymbol erhält ein Attribut v zur Aufnahme von rationalen Zahlen:<br />

B.v, N.v, L.v. Weiterhin benötigen wir ein Hilfsattribut l zur Aufnahme der Anzahl<br />

der Ziffern nach dem Dezimalpunkt: L.l.<br />

Tritt ein Nichtterminal in einer Produktion mehrfach auf, müssen wir die zugehörigen<br />

Attributvorkommen unterscheiden. So ergibt sich N.v in der Produktion N → L.L aus<br />

L1.v <strong>und</strong> L2.v. Generell gilt, daß bei mehrfachem Vorkommen eines Nichtterminals<br />

das linkeste dieser Vorkommen den Index 1 erhält (die linke Seite der Produktion<br />

wird miteinbezogen).<br />

Wir beschreiben die Berechung des Zahlwertes mit folgenden den Produktionen der<br />

Grammatik zugeordneten semantischen Regeln:<br />

B → 0 B.v := 0<br />

B → 1 B.v := 1<br />

L → B L.v := B.v<br />

L.l := 1<br />

L → LB L1.v := 2 ∗ L2.v + B.v<br />

L1.l := L2.l + 1<br />

N → L N.v := L.v<br />

N → L.L N.v := L1.v + L2.v/2 L2.l<br />

In Abbildung 4.4 ist der attributierte Strukturbaum <strong>für</strong> die Ableitung des Wortes<br />

1101.01 angegeben. Als Zahlwert <strong>für</strong> die Binärzahl 1101.01 ergibt sich 13.25.<br />

Die Berechnung erfolgt bottom-up, da nur synthetische Attribute verwendet werden,<br />

121


4 Semantische Analyse<br />

d.h. die Werte der Attribute auf der linken Seite der jeweiligen Produktion hängen<br />

nur von den Werten der Attribute auf der rechten Seite der Produktion ab.<br />

Unter Verwendung eines inheriten Attributs läßt sich eine andere Variante angeben.<br />

Wir verwenden das zusätzliche Attribut s (“Stelle”), das die Stelligkeit einer<br />

Binärziffer angibt. Die semantischen Regeln lauten:<br />

B → 0 B.v := 0<br />

B → 1 B.v := 2 B.s<br />

L → B L.v := B.v<br />

L.l := 1<br />

B.s := L.s<br />

L → LB L1.v := L2.v + B.v<br />

L1.l := L2.l + 1<br />

L2.s := L1.s + 1<br />

B.s := L1.s<br />

N → L N.v := L.v<br />

L.s := 0<br />

N → L.L N.v := L1.v + L2.v<br />

L1.s := 0<br />

L2.s := −L2.l<br />

Die Ableitung des Wortes 1101.01 ist in Abbildung 4.5 dargestellt. Das Attribut s<br />

ist ein inherites Attribut, das zu Beginn der Analyse an der Wurzel des linken Teilbaums<br />

auf 0 gesetzt <strong>und</strong> dann im Baum an die jeweiligen Kinder der Knoten weitergereicht<br />

wird. Der Wert <strong>für</strong> s an der Wurzel des rechten Teilbaums ergibt sich aus<br />

dem synthetischen Attribut l, das die Länge der im Teilbaum dargestellten Ziffernfolge<br />

repräsentiert. Bei der Auswertung werden also sowohl synthetische (v,l) als auch<br />

inherite Attribute (s) verwendet.<br />

Werden die Werte 0 <strong>und</strong> 1 im Strukturbaum durch ein Terminal num repräsentiert,<br />

kann num ein synthetisches Attribut <strong>für</strong> die jeweiligen Werte erhalten. Statt<br />

berechnet zu werden, wird der Attributwert <strong>für</strong> num dann entsprechend durch einen<br />

”Scanner” 2 geliefert. So kann die Berechnung <strong>für</strong> verschiedene Eingaben durchgeführt<br />

werden.<br />

Wir geben nun die formale Definition einer attributierten Grammatik an.<br />

Definition 46<br />

Ein Tupel (G,A,R) ist eine attributierte Grammatik genau dann, wenn<br />

• G kontextfreie Grammatik,<br />

• A = �<br />

A(X) Menge der Attribute (A endlich) <strong>und</strong><br />

X∈VN ∪VT<br />

2 im Übersetzungsfall durch den Scanner des Compilers<br />

122


v:3<br />

l:2<br />

L<br />

B<br />

1<br />

L<br />

v:1<br />

l:1<br />

v:1<br />

v:6<br />

l:3<br />

v:13<br />

l:4<br />

L<br />

N<br />

L .<br />

L<br />

B 0<br />

v:1<br />

1<br />

B<br />

v:0<br />

1<br />

B v:1<br />

v:13.25<br />

4.1 Attributierte Grammatiken<br />

Abbildung 4.4: Attributierter Strukturbaum zur Analyse einer Binärzahl.<br />

L<br />

B<br />

0<br />

v:0<br />

l:1<br />

v:0<br />

v:1<br />

l:2<br />

B<br />

1<br />

v:1<br />

123


4 Semantische Analyse<br />

s:2<br />

v:12<br />

l:2<br />

L<br />

B<br />

1<br />

s:1<br />

v:12<br />

l:3<br />

L<br />

s:3<br />

v:8<br />

l:1<br />

s:3<br />

v:8<br />

s:0<br />

v:13<br />

l:4<br />

L<br />

s:2<br />

v:4<br />

B<br />

N<br />

L .<br />

L<br />

s:1<br />

v:0<br />

1<br />

s:0<br />

v:1<br />

B<br />

0<br />

B<br />

1<br />

v:13.25<br />

s:-1<br />

v:0<br />

l:1<br />

s:-1<br />

v:0<br />

L<br />

B<br />

0<br />

s:-2<br />

v:0.25<br />

l:2<br />

B<br />

1<br />

s:-2<br />

v:0.25<br />

Abbildung 4.5: Attributierter Strukturbaum zur Analyse einer Binärzahl mit inheritem<br />

Attribut.<br />

124


• R = �<br />

p∈P R(p) die Menge der semantischen Regeln der Form<br />

Xi.a := f(Xi1.b1,...,Xik.bk)<br />

4.1 Attributierte Grammatiken<br />

mit Xi,Xi1,...,Xik ∈ {X0,...,Xn}, bj ∈ A(Xij) <strong>für</strong> die Produktion X0 →<br />

X1...Xn ist <strong>und</strong> wenn<br />

• <strong>für</strong> jedes Auftreten eines Grammatiksymbols X in einem Strukturbaum <strong>für</strong> ein<br />

w ∈ L(G) <strong>und</strong> <strong>für</strong> jedes a ∈ A(X) höchstens eine Regel in R zur Berechnung<br />

von X.a anwendbar ist.<br />

Die vierte Bedingung fordert, daß eine attributierte Grammatik eindeutig ist. Daher<br />

darf bei der Auswahl der anzuwendenden semantischen Regel kein Nichtdeterminismus<br />

entstehen. Aus diesem Gr<strong>und</strong> kann ein Attribut nicht zugleich synthetisch <strong>und</strong><br />

inherit sein, da dann eine solche Auswahl zwischen zwei möglichen Regeln (einer <strong>für</strong><br />

die synthetische <strong>und</strong> einer <strong>für</strong> die inherite Berechnung des Attributwerts) getroffen<br />

werden müßte <strong>und</strong> somit die Eindeutigkeit verletzt wäre.<br />

Neben den semantischen Regeln werden zu Produktionen P manchmal semantische<br />

Bedingungen der Form B(Xi.a,...,Xj.b) angegeben. Dabei ist B ein Prädikat, das<br />

Aussagen über die Werte von Attributvorkommen Xi.a, ..., Xj.b in P macht. Semantische<br />

Bedingungen liefern Informationen über Korrektheit bzgl. der statischen<br />

Semantik des bis dahin überprüften Teilbaums.<br />

Definition 47<br />

Sei p Produktion. Die Menge der definierenden Vorkommen von Attributen <strong>für</strong> p ist<br />

AF(p) = {Xi.a | Xi.a := f(...) ∈ R(p)}.<br />

Ein Attribut X.a heißt synthetisch genau dann, wenn ∃p : X → α mit X.a ∈ AF(p).<br />

Ein Attribut X.a heißt inherit genau dann, wenn ∃p : Y → αXβ mit X.a ∈ AF(p).<br />

AS(X) sei die Menge der synthetischen Attribute von X, AI(X) die Menge der<br />

inheriten Attribute von X.<br />

Die Menge der definierenden Vorkommen einer Produktion p sind die Attributvorkommen,<br />

denen durch eine semantische Regel zu p ein Wert zugewiesen wird. Ein<br />

Attribut X.a heißt synthetisch, wenn definierende Vorkommen von X nur in Produktionen<br />

auftreten, bei denen X auf der linken Seite vorkommt. Entsprechend ist<br />

<strong>für</strong> ein inherites Attribut Y.b gefordert, daß alle definierenden Vorkommen nur bei<br />

Produktionen auftreten, bei denen Y auf der rechten Seite vorkommt.<br />

Wir beschreiben nun informell die Semantik einer attributierten Grammatik.<br />

Semantik einer attributierten Grammatik<br />

Unter bestimmten Bedingungen wird <strong>für</strong> jeden Strukturbaum t der zugr<strong>und</strong>eliegenden<br />

kontextfreien Grammatik eine Zuordnung zu den Knoten von t festgelegt.<br />

125


4 Semantische Analyse<br />

Z.c<br />

Y.b<br />

Abbildung 4.6: Darstellung der direkten Abhängigkeiten zwischen<br />

Attributvorkommen.<br />

Sei t ein Strukturbaum einer attributierten Grammatik, sei n ein Knoten in t <strong>und</strong><br />

sei n mit X beschriftet.<br />

Für jedes Attribut a ∈ A(X) liegt an n ein Attributexemplar an vor. Dieses entspricht<br />

einem Attributvorkommen zu der an dieser Stelle im Strukturbaum angewandten<br />

Produktion (zur Erzeugung von n <strong>und</strong> seinen eventuellen Nachfolgern). Nach Definition<br />

46 gibt es dann höchstens eine dieser Produktion zugeordnete semantische Regel<br />

zur Berechnung von an.<br />

Da einer Produktion der Grammatik mehrere semantische Regeln zugeordnet sein<br />

können, zwischen denen Abhängigkeiten bestehen können, muß zur Berechnung der<br />

Attributwerte eine geeignete Strategie entwickelt werden.<br />

In einer Produktion einer Grammatik mit der semantischen Regel X.a := f(Z.c,Y.b)<br />

müssen Z.c <strong>und</strong> Y.b bekannt sein, wenn X.a berechnet wird. Diese Abhängigkeiten<br />

zwischen Attributvorkommen lassen sich mit gerichteten Graphen wie in Abbildung<br />

4.6 beschreiben.<br />

Definition 48<br />

Sei p : X0 → X1...Xn Produktion. Wir ordnen p den Graphen der direkten Abhängigkeiten<br />

DA(p) := {(Xi.a,Xj.b) | Xj.b := f(...,Xi.a,...) ∈ R(p)} zu.<br />

Eine Kante (Xi.a,Xj.b) ∈ DA(p) besagt, daß bei Anwendung der Produktion p der<br />

Wert von Xi.a bekannt sein muß, damit Xj.b berechnet werden kann.<br />

Die indirekten Abhängigkeiten zwischen Attributvorkommen erhält man durch Bildung<br />

der transitiven Hülle des Abhängigkeitsgraphen.<br />

Beispiel 49 (Fortführung von Beispiel 48)<br />

In Abbildung 4.7 ist der Graph der direkten Abhängigkeiten <strong>für</strong> die Produktion L →<br />

LB mit der zweiten Variante semantischer Regeln aus Beispiel 48 angegeben.<br />

Die Abhängigkeitsgraphen der einzelnen Produktionen können auch miteinander<br />

“verklebt” werden, so daß ein gemeinsamer Abhängigkeitsgraph <strong>für</strong> alle Attributvorkommen<br />

eines Ableitungsbaums entsteht. In Abbildung 4.8 ist dieser “verklebte”<br />

Graph <strong>für</strong> die Ableitung aus Beispiel 48 angegeben.<br />

126<br />

X.a


L2.v<br />

B.v<br />

L2.l<br />

L1.s<br />

❍ ❍❍❍<br />

✦ ✦✦✦✦<br />

✟ ✟✟✟<br />

❍ ❍❍❍<br />

L1.v<br />

L1.l<br />

L2.s<br />

B.s<br />

Abbildung 4.7: Beispiel eines Abhängigkeitsgraphen.<br />

v s l<br />

L<br />

v s<br />

v s l<br />

L<br />

v s<br />

L<br />

B<br />

1<br />

N<br />

L . v s l L<br />

B<br />

v s l B v s 0<br />

v s<br />

v s l<br />

1<br />

B<br />

1<br />

4.1 Attributierte Grammatiken<br />

v s l L B v s<br />

v s<br />

Abbildung 4.8: “Verklebter” Abhängigkeitsgraph.<br />

v<br />

B<br />

0<br />

1<br />

127


4 Semantische Analyse<br />

Ein Abhängigkeitsgraph <strong>für</strong> einen Ableitungsbaum darf keinen Zyklus enthalten, da<br />

die Attribute dann nicht auswertbar sind. Daher ist der Graph auf eventuelle Zirkularität<br />

zu überprüfen.<br />

Außerdem müssen zur Berechnung der Attribute “genügend” Regeln existieren.<br />

Definition 49<br />

Eine attributierte Grammatik ist vollständig genau dann, wenn <strong>für</strong> alle Produktionen<br />

p gilt:<br />

Wenn p : X → α, dann AS(X) ⊆ AF(p), wenn p : Y → αXβ, dann AI(X) ⊆<br />

AF(p). Weiterhin muß gelten AS(X) ∪ AI(X) = A(X).<br />

Jede vollständige <strong>und</strong> nicht-zirkuläre attributierte Grammatik G ist <strong>für</strong> jeden Ableitungsbaum<br />

von G auswertbar.<br />

Eine Möglichkeit der Auswertung der Attributvorkommen ist, <strong>für</strong> jedes Attributvorkommen<br />

einen eigenen Prozeß zu generieren. Alle Prozesse laufen parallel. Ein<br />

einzelner Prozeß berechnet den Wert des zugehörigen Attributvorkommens, sobald<br />

alle da<strong>für</strong> notwendigen Daten verfügbar sind. Dieses Auswertungsverfahren ist im<br />

allgemeinen zu aufwendig (in bezug auf Zeit <strong>und</strong> Speicherplatz). Effizientere Auswertungsstrategien<br />

werden in [ASU99] beschrieben.<br />

Die Auswertung ist <strong>für</strong> spezielle Klassen attributierter Grammatiken einfacher. Treten<br />

z.B. nur synthetische Attribute auf, kann ein Bottom-Up-Parser ohne Probleme<br />

die Attribute mit berechnen. Aus diesem Gr<strong>und</strong> sollte die gewählte Auswertungsstrategie<br />

möglichst im Einklang mit der Parsemethode gewählt werden.<br />

Beim Übersetzerbau können die semantischen Regeln Funktionen mit Seiteneffekten<br />

beinhalten (teilweise ohne tatsächliche Berechnung von Attributwerten). So ist<br />

es im allgemeinen notwendig, während der semantischen Analyse Typinformationen<br />

in die Symboltabelle einzufügen. Dabei werden z.B. den Bezeichnern, die der Scanner<br />

(bzw. der Sieber) in der lexikalischen Analyse in die Symboltabelle eingetragen<br />

hat, Typinformationen zugeordnet. Treten die Bezeichner im Anweisungsteil eines<br />

Programms auf, werden diese Typinformationen aus der Tabelle gelesen <strong>und</strong> <strong>für</strong> die<br />

Typüberprüfung verwendet.<br />

4.2 Typüberprüfung<br />

Die Typüberprüfung verifiziert, daß der Typ eines Konstrukts mit dem Typ “zusammenpaßt”,<br />

der aufgr<strong>und</strong> des Kontextes erwartet wird.<br />

Beispiele:<br />

128<br />

• die Operation mod erwartet integer-Operanden,<br />

• Dereferenzierung kann nur auf Zeiger angewendet werden,


• Indizieren ist nur bei Arrays erlaubt.<br />

4.2 Typüberprüfung<br />

Ein wichtiges Teilproblem bei der Typüberprüfung ist die Frage, wann Typen “zusammenpassen”.<br />

Typen sollen nicht nur auf Gleichheit, sondern auch auf Möglichkeiten<br />

zur Typkonversion hin überprüft werden. Zum Beispiel ist in vielen Sprachen eine<br />

Typkonversion von integer nach real möglich. So ist zum Beispiel <strong>für</strong> die Konstante<br />

1 im Ausdruck 1 + 1.5 eine Umwandlung des Typs integer nach real notwendig.<br />

Typinformationen werden auch bei der Codegenerierung benötigt. Zum Beispiel ist in<br />

den meisten Sprachen die Überladung des Operators + möglich, d.h. es wird dasselbe<br />

Operatorsymbol <strong>für</strong> die Addition von integer-Zahlen <strong>und</strong> <strong>für</strong> die Addition von real-<br />

Zahlen verwendet. Der Codegenerator muß anhand der Typinformation entscheiden,<br />

<strong>für</strong> welche Art von Addition er Code erzeugen soll.<br />

Einige Sprachen (vor allem aus der Familie der funktionalen Sprachen) erlauben<br />

Polymorphie [Thi97]. Eine Funktion ist polymorph, wenn sie auf Argumente unterschiedlichen<br />

Typs angewendet werden kann (die oben erwähnte +-Operation könnte<br />

man also auch als eine polymorphe Funktion auffassen). So ist zum Beispiel <strong>für</strong> eine<br />

Funktion zur Berechnung der Länge einer Liste der Typ der Listenelemente irrelevant.<br />

Andererseits möchte man nicht <strong>für</strong> jeden Listentyp (Zeichenliste, Zahlenliste<br />

usw.) eine eigene Funktion zur Längenberechnung definieren. Die Definition einer<br />

polymorphen Funktion ist daher <strong>für</strong> diese Aufgabe prädestiniert.<br />

4.2.1 Typsysteme<br />

Typsysteme enthalten Informationen über Typen in Programmiersprachen. Typinformationen<br />

in Programmiersprachen sind z.B.<br />

• in Pascal: “Wenn beide Operanden der arithmetischen Operatoren Addition,<br />

Subtraktion <strong>und</strong> Multiplikation vom Typ integer sind, dann ist das Ergebnis<br />

ebenfalls vom Typ integer.”,<br />

• in C: “Das Ergebnis des einstelligen Operators & ist ein Zeiger auf das Objekt,<br />

das durch den Operanden übergeben wurde. Wenn der Typ des Operanden t<br />

ist, dann ist ’Zeiger auf t’ der Typ des Ergebnisses.”.<br />

Das erste Beispiel zeigt, daß Ausdrücken ein Typ zugeordnet wird. Das zweite Beispiel<br />

zeigt, daß Typen eine Struktur haben, das heißt, daß man neue Typen aus gegebenen<br />

Typen konstruieren kann.<br />

Ein Typsystem umfaßt<br />

• Einfache Typen wie integer, boolean, character <strong>und</strong> real. Hierzu gehören auch<br />

Teilbereichstypen (z.B. (1..10)) <strong>und</strong> Aufzählungstypen<br />

(z.B. (rot,gelb,grün)).<br />

• Zusammengesetzte Typen wie Arrays, Records, Sets, Zeigertypen <strong>und</strong> Funktionstypen.<br />

129


4 Semantische Analyse<br />

Der Typ von Sprachkonstrukten wird durch Typausdrücke angegeben.<br />

Definition 50<br />

Ein Typausdruck ist<br />

• ein einfacher Typ<br />

oder<br />

• von der Form k(T1,...,Tn), wobei k ein Typkonstruktor <strong>und</strong><br />

T1,...,Tn Typausdrücke sind.<br />

Die Menge der einfachen Typen <strong>und</strong> der Typkonstruktoren hängt von der betrachteten<br />

Sprache ab. Zusammengesetzte Typen werden mit Hilfe der Typkonstruktoren<br />

gebildet.<br />

Beispiel 50<br />

Wir betrachten Beispiele <strong>für</strong> einfache Typen <strong>und</strong> Typkonstruktoren:<br />

130<br />

• Beispiele <strong>für</strong> einfache Typen: boolean, char, integer, real, type error (zeigt Typfehler<br />

an)<br />

• Beispiele <strong>für</strong> Typkonstruktoren:<br />

– Arrays: Sei T Typausdruck. array(I,T) ist ein Typausdruck, der den Typ<br />

von Arrays mit Elementen vom Typ T <strong>und</strong> Indexmenge I bezeichnet (eigentlich<br />

müßte <strong>für</strong> jede Indexmenge ein eigener Typkonstruktor arrayI(T)<br />

verwendet werden). Zum Beispiel wird nach Bearbeitung von “var A: array<br />

[1..10] of integer” mit A der Typausdruck array(1..10,integer) assoziiert.<br />

– Records: Seien T1,...,Tm Typen, n1,...,nm Komponentennamen. Dann ist<br />

record((n1×T1)×...×(nm×Tm)) Typausdruck (eigentlich sind die n1,...,nm<br />

wieder Teil des Typkonstruktors). Zum Beispiel wird <strong>für</strong> den Programmtext<br />

type row = record<br />

address : integer;<br />

lexeme : array [1..15] of char<br />

end;<br />

var table : array [1..101] of row<br />

der Typ<br />

T1 = record((address × integer) × (lexeme × array(1..15,char)),<br />

der dem Typ von row entspricht, <strong>und</strong> der Typ T2 = array(1..101,T1) von<br />

table erzeugt.


4.2 Typüberprüfung<br />

– Zeigertypen: Sei T Typausdruck. Dann ist pointer(T) Typausdruck. Wird<br />

im Beispiel <strong>für</strong> Record-Typen die Zeile “var p: ↑row” hinzugefügt, ist der<br />

Typ von p pointer(T1).<br />

Ein Typsystem ist nun eine Sammlung von Regeln zur Zuweisung von Typausdrücken<br />

zu Teilen eines Programms.<br />

Ein Typüberprüfer ist die Implementierung eines Typsystems in Form eines Programms.<br />

Dieses Programm ist in der Regel ein Teilprogramm der semantischen Analyse.<br />

Typsysteme sind manchmal nicht nur sprach-, sondern auch compilerspezifisch. Zum<br />

Beispiel schließt in Pascal der Typ eines Arrays die Indexmenge mit ein. Einige<br />

Compiler erlauben jedoch, Prozeduren zu deklarieren, die als Parameter Arrays ohne<br />

Betrachtung der konkreten Indexmenge erhalten 3 . Diese Compiler verwenden daher<br />

ihr eigenes Typsystem.<br />

Statische <strong>und</strong> dynamische Überprüfung von Typen<br />

Wir unterscheiden zwischen der statischen <strong>und</strong> der dynamischen Typüberprüfung.<br />

Die statische Typüberprüfung umfaßt alle Typuntersuchungen, die während der Übersetzungszeit<br />

eines Programms durch den Compiler in der semantischen Analyse vorgenommen<br />

werden können. Hierzu gehört z.B. die korrekte Typisierung von Ausdrücken.<br />

Die dynamische Typüberprüfung findet erst zur Laufzeit eines übersetzten<br />

Programms statt, da sie die Typinformationen analysiert, die von der Eingabe des<br />

jeweiligen Programmablaufs abhängig sind. Hierzu gehört z.B. die Einhaltung der<br />

Indexmenge bei Arrayzugriffen.<br />

Eine Programmiersprache heißt streng getypt, wenn eine vollständige statische Typüberprüfung<br />

eines Quelltextes zur Übersetzungszeit möglich ist (ohne Überprüfung der<br />

korrekten Indizierung bei Arrays). Streng getypte Sprachen sind z.B. Pascal [JW91],<br />

Modula-2 [Wir97], C [KR90] <strong>und</strong> Haskell [Po96]. Nicht streng getypt ist z.B. Smalltalk,<br />

da durch das late binding Typinformationen erst zur Laufzeit entstehen [GR83]<br />

<strong>und</strong> daher auch erst zur Laufzeit überprüft werden kann, ob eine Methode (oder ein<br />

Operator) <strong>für</strong> ein Objekt definiert ist.<br />

Beispiel 51 (Typüberprüfer <strong>für</strong> eine einfache Sprache)<br />

Wir definieren eine einfache Sprache, in der ein Programm aus einer Folge von Deklarationen<br />

besteht, auf die ein einzelner Ausdruck folgt. Die kontextfreie Grammatik<br />

dieser Sprache lautet:<br />

3 In Modula-2 als open arrays generell erlaubt.<br />

131


4 Semantische Analyse<br />

P → D ; E<br />

D → D ; D | id : T<br />

T → char | integer | array [ num ] of T | ↑ T<br />

E → literal | num | id | E mod E | E [ E ] | E ↑<br />

Als Typen verwenden wir einfache Typen (char, integer <strong>und</strong> type error <strong>für</strong> Typfehler)<br />

<strong>und</strong> Arrays <strong>und</strong> Zeiger als zusammengesetzte Typen. Alle Arrays sind mit Werten<br />

vom Typ integer indiziert; die Indexmengen beginnen stets mit 1. So liefert z.B. der<br />

Deklarationsteil array [256] of char den Typausdruck array(1..256,char). Zeigertypen<br />

werden mit dem Typkonstruktor pointer bezeichnet, so daß z.B. ↑integer den<br />

Typ pointer(integer) erhält. Die Dereferenzierung geschieht in Ausdrücken mit e ↑,<br />

wobei e einem Zeigertyp angehören muß.<br />

Wir geben nun das Typsystem <strong>für</strong> unsere Sprache in Form von semantischen Regeln<br />

an, die den Produktionen der Grammatik zugeordnet sind.<br />

Für die semantischen Regeln verwenden wir zwei Funktionen mit Seiteneffekten:<br />

• addtype(b,t) ordnet dem Bezeichner b in der Symboltabelle den Typ t zu. Wir<br />

nehmen an, daß die Bezeichner während der lexikalischen Analyse in die Symboltabelle<br />

eingefügt wurden. Jedes Symbol id besitzt ein synthetisches Attribut<br />

entry, das auf den entsprechenden Symboltabelleneintrag verweist.<br />

• lookup(b) liefert den <strong>für</strong> Bezeichner b in der Symboltabelle angegebenen Typ.<br />

Existiert in der Symboltabelle kein Eintrag <strong>für</strong> b, liefert lookup type error.<br />

Das Symbol num besitzt ein synthetisches Attribut val, das den Wert der von num<br />

repräsentierten Zahl enthält.<br />

Die semantischen Regeln des Typsystems sind in Abbildung 4.9 angegeben.<br />

In der Regel <strong>für</strong> Array-Zugriff wird nicht überprüft, ob der Index gültig ist, da dies<br />

erst von der dynamischen Typüberprüfung zur Laufzeit verifiziert werden kann.<br />

In Abbildung 4.10 ist der attributierte Strukturbaum <strong>für</strong> das Programm<br />

n : ↑ integer;<br />

n↑ mod 4<br />

angegeben. Dieses Programm enthält keinen Typfehler.<br />

Das Programm<br />

n : ↑ integer;<br />

n↑ mod x<br />

ist syntaktisch korrekt, enthält aber einen semantischen Fehler, da der Bezeichner x<br />

nicht deklariert ist. Das Typsystem ist in der Lage, diesen Fehler zu finden (siehe<br />

Abbildung 4.11). Beim Nachsehen in der Symboltabelle liefert die Funktion lookup<br />

den Typ type error, der im Baum weitergereicht wird. Auf diese Weise wird erkannt,<br />

daß der Ausdruck einen semantischen Fehler enthält.<br />

132


4.2 Typüberprüfung<br />

P → D ; E<br />

D → D ; D<br />

D → id : T addtype(id.entry,T.type)<br />

T → char T.type := char<br />

T → integer T.type := integer<br />

T → ↑T T1.type := pointer(T2.type)<br />

T → array [ num ] of T T1.type := array(1..num.val,T2.type)<br />

E → literal E.type := char<br />

E → num E.type := integer<br />

E → id E.type := lookup(id.entry)<br />

E → E mod E E1.type :=<br />

if E2.type = integer and E3.type = integer<br />

then integer<br />

else type error<br />

E → E [ E ] E1.type :=<br />

if E3.type = integer and E2.type = array(s,t)<br />

then t<br />

else type error<br />

E → E ↑ E1.type :=<br />

if E2.type =pointer(t) then t else type error<br />

Abbildung 4.9: Semantische Regeln <strong>für</strong> Beispielsprache.<br />

addtype(n,pointer(integer))<br />

D<br />

type:integer<br />

id : T<br />

E<br />

type:pointer(integer)<br />

entry:n<br />

P<br />

T<br />

type:integer<br />

integer<br />

; E<br />

mod<br />

E type:pointer(integer)<br />

id<br />

entry:n<br />

type:integer<br />

E<br />

num<br />

val:4<br />

type:integer<br />

Abbildung 4.10: Attributierter Strukturbaum <strong>für</strong> ein Beispielprogramm.<br />

133


4 Semantische Analyse<br />

addtype(n,pointer(integer))<br />

D<br />

type:integer<br />

id : T<br />

E<br />

type:pointer(integer)<br />

entry:n<br />

P<br />

T<br />

type:integer<br />

integer<br />

; E<br />

mod<br />

E type:pointer(integer) id<br />

entry:?<br />

id<br />

entry:n<br />

type:type_error<br />

E<br />

type:type_error<br />

Abbildung 4.11: Attributierter Strukturbaum <strong>für</strong> ein fehlerhaftes Beispielprogramm.<br />

Beispiel 52 (Fortführung von Beispiel 51)<br />

Wir erweitern die Sprache aus dem vorherigen Beispiel um Anweisungen. Anweisungen<br />

haben keinen Typ, daher führen wir einen neuen einfachen Typ void ein, der den<br />

Typ einer korrekt getypten Anweisung darstellt.<br />

Wir modifizieren die Grammatik aus Beispiel 51:<br />

P → D ; S<br />

D → D ; D | id : T<br />

T → boolean | char | integer | array [ num ] of T | ↑ T<br />

E → literal | num | id | E mod E | E [ E ] | E ↑<br />

S → id := E | if E then S | while E do S | S ; S<br />

Das Typsystem wird um die folgenden semantischen Regeln ergänzt.<br />

134


1 function sequiv (S ,T) : boolean ;<br />

2 begin<br />

3 if S <strong>und</strong> T vom selben Basistyp then<br />

4 return true<br />

5 else if } S = array( I , S1) and T = array( I ,T1) then<br />

6 return sequiv (S1 ,T1)<br />

7 else if S = pointer (S1) and T = pointer (T1) then<br />

8 return sequiv (S1 ,T1)<br />

9 else return false<br />

10 end<br />

4.2 Typüberprüfung<br />

Abbildung 4.12: Funktion zur Überprüfung, ob zwei Typausdrücke identisch sind.<br />

T → boolean T.type := boolean<br />

S → id := E S.type :=<br />

if lookup(id.entry) = E.type<br />

then void<br />

else type error<br />

S → if E then S S1.type :=<br />

if E.type = boolean<br />

then S2.type<br />

else type error<br />

S → while E do S S1.type :=<br />

if E.type = boolean<br />

then S2.type<br />

else type error<br />

S → S ; S S1.type :=<br />

if S2.type = void and S3.type = void<br />

then void<br />

else type error<br />

4.2.2 Gleichheit von Typausdrücken<br />

Im Beispiel 52 wurde in der semantischen Regel <strong>für</strong> die Wertzuweisung die Gleichheit<br />

von Typausdrücken gefordert (if lookup(id.entry) = E.type ...).<br />

Wir gehen im folgenden auf die Frage ein, wann zwei Typen gleich sind.<br />

In einem Typsystem ohne die Deklaration neuer Typnamen sind zwei Typen gleich,<br />

wenn ihre Typausdrücke identisch sind. Zur Überprüfung, ob zwei Typausdrücke<br />

identisch sind, läßt sich die rekursive Funktion sequiv in Abbildung 4.12 verwenden.<br />

135


4 Semantische Analyse<br />

Beispiel 53<br />

Ein Problem entsteht, wenn die Vergabe von Typnamen <strong>für</strong> neue Typen möglich ist.<br />

Betrachten wir die folgende Typdeklaration:<br />

type link = ↑ cell;<br />

var next : link;<br />

last : link;<br />

p : ↑ cell;<br />

q,r : ↑ cell;<br />

Haben next,last,p,q <strong>und</strong> r den gleichen Typ?<br />

Werden Typnamen verwendet, unterscheiden wir zwei Arten von Typgleichheit: Namensgleichheit<br />

<strong>und</strong> strukturelle Gleichheit.<br />

• Namensgleichheit: jeder Typname bezeichnet einen individuellen Typ. Daher<br />

sind die Typnamen Bestandteile der Typausdrücke.<br />

• Strukturelle Gleichheit: die Typnamen werden in Typausdrücken ersetzt, bevor<br />

die Funktion sequiv aus Abbildung 4.11 auf die Typausdrücke angewendet wird<br />

(Vorsicht bei rekursiven Typdefinitionen!).<br />

Beispiel 54 (Fortsetzung von Beispiel 53)<br />

Den in Beispiel 53 deklarierten Variablen werden die folgenden Typausdrücke zugeordnet:<br />

next : link<br />

last : link<br />

p : pointer(cell)<br />

q : pointer(cell)<br />

r : pointer(cell)<br />

Bei Namensgleichheit besitzen next <strong>und</strong> last den gleichen Typ, ebenso p, q <strong>und</strong> r. p<br />

<strong>und</strong> next hingegen sind nicht vom gleichen Typ.<br />

Bei struktureller Gleichheit werden die Typnamen durch die ihnen zugeordneten Typausdrücke<br />

ersetzt. Da link der Ausdruck pointer(cell) zugeordnet ist, besitzen alle<br />

fünf Variablen den gleichen Typ.<br />

In der Praxis ist das Problem der Typgleichheit manchmal noch komplizierter. Zum<br />

Beispiel wird in Implementierungen der Sprache Pascal <strong>für</strong> jede Deklaration ein<br />

impliziter Typname eingeführt. Daher würden die Typen der Variablen aus Beispiel<br />

53 lauten:<br />

next : link<br />

last : link<br />

p : np<br />

q : nqr<br />

r : nqr<br />

136


4.2 Typüberprüfung<br />

Die Variable p erhält den neuen impliziten Typnamen np. Die Variablen q <strong>und</strong> r<br />

erhalten den neuen impliziten Typnamen nqr, da sie zusammen in einer Deklaration<br />

vereinbart wurden. Dies führt dazu, daß p <strong>und</strong> q nicht mehr vom gleichen Typ sind,<br />

q <strong>und</strong> r jedoch schon.<br />

4.2.3 Typumwandlungen<br />

In einigen Fällen ist die Gleichheit von Typen eine zu starke Forderung, wie im<br />

folgenden Beispiel erläutert wird.<br />

Beispiel 55<br />

Im Programm<br />

var x,y : real;<br />

i : integer;<br />

y := x + i<br />

wird eine Addition einer Variable x vom Typ real mit einer Variable i vom Typ<br />

integer durchgeführt, deren Ergebnis wieder in einer real-Variable gespeichert wird.<br />

In einigen Programmiersprachen (z.B. in Oberon [WG92]) ist diese Addition erlaubt,<br />

obwohl die Typen von x <strong>und</strong> i unterschiedlich sind. In diesen Sprachen sind die<br />

Typen real <strong>und</strong> integer “verträglich”, d.h. der Wert der integer-Variable wird vor<br />

der Durchführung der Addition in einen real-Wert umgewandelt.<br />

Wir definieren die “Verträglichkeit” von Typen durch eine Relation coercible (engl.<br />

coercible = erzwingend).<br />

Definition 51<br />

Seien T1,T2 Typen. Die Beziehung coercible(T1,T2) legt fest, daß ein Objekt vom Typ<br />

T1 in ein Objekt vom Typ T2 transformiert werden kann, falls dies nötig ist.<br />

Die Notwendigkeit einer Transformation eines Objekts in ein Objekt eines “verträglichen”<br />

Typs wird vom Compiler in der semantischen Analyse erkannt, so daß er <strong>für</strong><br />

diese Umwandlung Code erzeugen kann.<br />

Beispiel 56 (Typüberprüfung <strong>für</strong> Ausdrücke)<br />

In diesem Beispiel betrachten wir ein Typsystem <strong>für</strong> Ausdrücke, das Typumwandlungen<br />

erlaubt. Jeder Ausdruck besitzt zwei Attribute <strong>für</strong> die Aufnahme von Typinfomationen:<br />

• Das synthetische Attribut premode enthält den Typ eines Ausdrucks gemäß<br />

seiner Zusammensetzung.<br />

• Das inherite Attribut postmode enthält den Typ eines Ausdrucks, der gemäß<br />

des Kontexts erwartet wird.<br />

137


4 Semantische Analyse<br />

Die folgende kontextfreie Grammatik definiert Wertzuweisungen, deren Ausdrücke<br />

nur den Additionsoperator enthalten dürfen.<br />

S → name := E<br />

E → name addop name<br />

addop → +<br />

name → id<br />

Als Typen lassen wir real <strong>und</strong> integer zu. Dabei soll gelten, daß ein integer-Wert in<br />

einen real-Wert umgewandelt werden kann, falls als Ergebnis des Gesamtausdrucks<br />

ein real-Wert erwartet wird.<br />

Es soll also gelten:<br />

coercible(integer,integer) = true<br />

coercible(integer,real) = true<br />

coercible(real,real) = true<br />

coercible(real,integer) = false<br />

Zur Beschreibung des Typsystems verwenden wir die folgenden semantischen Regeln:<br />

S → name := E name.postmode := name.premode<br />

E.postmode :=<br />

if name.premode = integer<br />

then integer<br />

else real<br />

E → name addop name name1.postmode := E.premode<br />

name2.postmode := E.premode<br />

addop.postmode := E.premode<br />

E.premode :=<br />

if coercible(name1.premode,integer)<br />

and coercible (name2.premode,integer)<br />

then integer<br />

else real<br />

addop → + +.operation :=<br />

if addop.postmode = integer<br />

then int addition<br />

else real addition<br />

name → id name.premode := lookup(id.entry)<br />

Anmerkungen zu den semantischen Regeln:<br />

138<br />

• Die Regel zur Berechnung von E.premode überprüft, ob der Wert des jeweiligen<br />

premode-Attributs der Nichtterminale name mit dem Typ integer verträglich<br />

ist. Ist dies der Fall, wird als Ergebnistyp des Ausdrucks integer verwendet,<br />

andernfalls real.<br />

• Zur Produktion E → name addop name kann eine semantische Bedingung angegeben<br />

werden, die durch einen Vergleich von E.premode <strong>und</strong> E.postmode


S<br />

name postmode:real :=<br />

E<br />

premode:real<br />

id<br />

entry:X<br />

postmode:integer<br />

premode:integer<br />

name addop<br />

name<br />

postmode:integer<br />

entry:I<br />

postmode:real<br />

premode:integer<br />

id +<br />

id<br />

operation:int_addition<br />

4.2 Typüberprüfung<br />

postmode:integer<br />

premode:integer<br />

entry:J<br />

Abbildung 4.13: Attributierter Strukturbaum einer Zuweisung.<br />

S<br />

name postmode:real :=<br />

E<br />

premode:real<br />

id<br />

entry:X<br />

postmode:integer<br />

premode:integer<br />

name addop<br />

name<br />

postmode:integer<br />

entry:I<br />

postmode:real<br />

premode:integer<br />

id +<br />

id<br />

operation:int_addition<br />

Typumwandlung<br />

postmode:integer<br />

premode:integer<br />

entry:J<br />

Abbildung 4.14: Attributierter Strukturbaum einer Zuweisung mit Attributabhängigkeiten.<br />

eine Überprüfung auf Vorliegen eines Typfehlers vornimmt. Es muß gelten<br />

coercible(E.premode,E.postmode), d.h. ein Fehler liegt vor, sobald der Ausdruck<br />

aufgr<strong>und</strong> seiner Struktur einen real-Wert liefert <strong>und</strong> aufgr<strong>und</strong> des Kontextes<br />

ein integer-Wert erwartet wird (coercible(real,integer) = false).<br />

• Das inherite Attribut +.operation wird als Information <strong>für</strong> die Codegenerierung<br />

benötigt. Durch die Überladung des Additionsoperators +, der sowohl die<br />

integer- als auch die real-Addition repräsentiert, kann der Codegenerator anhand<br />

des Werts dieses Attributs erkennen, <strong>für</strong> welche Art der Addition er Code<br />

erzeugen muß.<br />

In Abbildung 4.13 ist der attributierte Strukturbaum der Analyse der Zuweisung<br />

X:=I+J angegeben. Wir nehmen an, daß X eine Variable vom Typ real ist <strong>und</strong> I<br />

<strong>und</strong> J Variablen von Typ integer sind. In Abbildung 4.14 wurde der Strukturbaum um<br />

die Attributabhängigkeiten ergänzt. Für das Nichtterminal E unterscheiden sich die<br />

Attributwerte von postmode <strong>und</strong> premode. Da jedoch coercible(integer,real) = true<br />

gilt, kann eine Typumwandlung vorgenommen werden, so daß die Zuweisung korrekt<br />

getypt ist.<br />

139


4 Semantische Analyse<br />

S<br />

name<br />

postmode:integer<br />

:=<br />

E<br />

premode:integer<br />

id<br />

entry:X<br />

postmode:real<br />

premode:real<br />

entry:I<br />

name addop<br />

postmode:real<br />

id +<br />

postmode:integer<br />

premode:real<br />

operation:<br />

real_addition<br />

Typfehler<br />

name<br />

id<br />

postmode:real<br />

premode:integer<br />

entry:J<br />

Typumwandlung<br />

Abbildung 4.15: Attributierter Strukturbaum einer Zuweisung mit Typfehler.<br />

Wir ändern die den Variablen zugeordneten Typen: sei nun X vom Typ integer<br />

<strong>und</strong> I vom Typ real. Der attributierte Strukturbaum der Zuweisung ist Abbildung<br />

4.15 dargestellt. Die semantische Analyse entdeckt einen Typfehler, da das Attribut<br />

premode von E den Wert real, das Attribut postmode den Wert integer besitzt<br />

<strong>und</strong> coercible(real,integer) = false gilt. Vor der Feststellung des Fehlers wird <strong>für</strong><br />

die Variable J eine Typumwandlung durchgeführt, um den integer-Wert von J zum<br />

real-Wert von I addieren zu können. Aufgr<strong>und</strong> der Zusammensetzung des Ausdrucks<br />

erhält E.premode den Wert real. Aus dem Kontext der Zuweisung (X ist integer-<br />

Variable) erhält E.postmode den Wert integer, so daß ein Typfehler vorliegt, der bei<br />

der Verträglichkeitsprüfung dieser beiden Attribute bemerkt wird.<br />

140


5 Zwischencode-Erzeugung<br />

Nachdem in den Kapiteln 2, 3 <strong>und</strong> 4 die einzelnen Phasen der Analyse eines Quelltexts<br />

behandelt wurden, werden in den folgenden Kapiteln die Synthesephasen beschrieben.<br />

Die erste Synthesephase ist die Zwischencode-Erzeugung. Zwischencode ist Code <strong>für</strong><br />

eine abstrakte (oder virtuelle) Maschine. Abstrakte Maschinen werden im Gegensatz<br />

zu realen Maschinen durch Programme realisiert, die den vom Compiler erzeugten<br />

Zwischencode interpretieren. Da Zwischencode von abstrakten Maschinen ausgeführt<br />

wird <strong>und</strong> somit von einer konkreten Zielmaschine unabhängig ist, wird die<br />

Zwischencode-Erzeugung zum Front-End eines Compilers gezählt (Abbildung 5.1,<br />

siehe auch Seite 12).<br />

Die Erzeugung von Code <strong>für</strong> eine abstrakte Maschine statt <strong>für</strong> eine reale Maschine<br />

bietet mehrere Vorteile:<br />

• Übersetzte Programme können auf allen Rechnern <strong>und</strong> unter allen Betriebssystemen<br />

laufen, <strong>für</strong> die ein Programm existiert, das die abstrakte Maschine<br />

realisiert. Beispiele <strong>für</strong> die erhöhte Portabilität durch Verwendung einer abstrakten<br />

Maschine sind das UCSD-System [Güt85] mit der p-Maschine <strong>und</strong> das<br />

Java-System [LY99].<br />

• Neben der Portabilität übersetzter Programme wird die Portierung von Übersetzern<br />

wesentlich vereinfacht, da der Zwischencode-Generator nicht jedesmal<br />

neu implementiert werden muß, um an die veränderte Hard- <strong>und</strong> Software angepaßt<br />

zu werden.<br />

• Durch die Verwendung von Zwischencode ist eine maschinenunabhängige Co-<br />

Parser<br />

semantische Zwischen-<br />

Analyse<br />

codegenerator<br />

Zwischendarstellung<br />

Front-End Back-End<br />

Zwischencode<br />

Abbildung 5.1: Einordnung der Zwischencode-Erzeugung.<br />

Codegenerator<br />

141


5 Zwischencode-Erzeugung<br />

deoptimierung möglich. Diese kann z.B. Codeabschnitte, die nie durchlaufen<br />

werden können, aus dem Code entfernen.<br />

• Die Architektur heutiger Hardwaremaschinen orientiert sich an der Struktur<br />

des von-Neumann-Rechners. Diese beruht auf der Verwendung von Speicher<br />

in Form von Variablen <strong>und</strong> einer Steuerung des Programmflusses durch bedingte<br />

<strong>und</strong> unbedingte Sprünge. Im Gegensatz zu den imperativen Sprachen<br />

wie C [KR90] oder Modula-2 [Wir97] lassen sich funktionale Sprachen wie ML<br />

[HMM97, Pau96] <strong>und</strong> Haskell [Po96] sowie logische Sprachen wie Prolog [CM94]<br />

nur unter großem Aufwand auf diese Maschinen abbilden. Aus diesem Gr<strong>und</strong><br />

kann eine abstrakte Maschine, die in ihrer Struktur auf die Bearbeitung der<br />

Konstrukte solcher Sprachen hin optimiert ist, die Codeerzeugung wesentlich<br />

vereinfachen. Beispiele <strong>für</strong> solche Maschinen sind die MaMa <strong>für</strong> funktionale<br />

Sprachen <strong>und</strong> die Warren Abstract Machine <strong>für</strong> Prolog [WM96].<br />

Der wesentliche Nachteil abstrakter Maschinen ist der Verlust an Effizienz, der durch<br />

die Interpretation des Zwischencodes durch die abstrakte Maschine entsteht. Außerdem<br />

können bei größeren Programmen Speicherplatzprobleme entstehen, da der<br />

Speicher neben dem auszuführenden Programm auch die abstrakte Maschine enthalten<br />

muß. Allerdings ist durch die spezielle Struktur der abstrakten Maschine der<br />

Zwischencode meist kürzer als der Code <strong>für</strong> die reale Maschine, da ein Befehl des<br />

Quelltexts meist in eine kurze Folge von Befehlen der abstrakten Maschine übersetzt<br />

werden kann.<br />

Es gibt verschiedene Arten der Darstellung von Zwischencode. In den folgenden Abschnitten<br />

werden wir auf zwei verbreitete Arten eingehen.<br />

5.1 Abstrakte Keller-Maschinen<br />

Reale Hardware-Maschinen besitzen in der Regel eine große Anzahl von internen<br />

Registern, in denen die arithmetischen <strong>und</strong> logischen Operationen ausgeführt werden.<br />

Die dazu benötigten Werte werden zuvor, sofern sie sich noch nicht in Registern<br />

befinden, aus dem Hauptspeicher geladen. Das Ergebnis der Operation wird ebenfalls<br />

in Registern abgelegt <strong>und</strong> kann bei Bedarf in den Hauptspeicher geschrieben werden<br />

(siehe auch Kapitel ??).<br />

Im Gegensatz hierzu besitzen abstrakte Keller-Maschinen keine <strong>für</strong> den Benutzer<br />

verwendbaren Register. Statt dessen werden die Daten auf einem Keller abgelegt.<br />

Die arithmetischen <strong>und</strong> logischen Operationen finden stets auf den obersten Werten<br />

des Kellers statt, die bei der Durchführung einer Operation vom Keller entfernt<br />

<strong>und</strong> durch das Ergebnis ersetzt werden. Zur Erzeugung von Zwischencode <strong>für</strong> eine<br />

abstrakte Keller-Maschine benötigen wir das Konzept der Postfix-Notation, in der<br />

der Operator hinter seine Operanden geschrieben wird.<br />

142


Beispiel 57 (Übersetzung arithmetischer Ausdrücke)<br />

5.1 Abstrakte Keller-Maschinen<br />

Die Postfix-Notation eines Ausdrucks kann gemäß den folgenden Regeln berechnet<br />

werden:<br />

E = id ⇒ postfix(E) = E<br />

E = E1 op E2 ⇒ postfix(E) = postfix(E1) postfix(E2) op<br />

E = ( E1 ) ⇒ postfix(E) = postfix(E1)<br />

In Postfix-Ausdrücken sind Klammern überflüssig, da Position <strong>und</strong> Stelligkeit der<br />

Operanden nur eine Interpretation eines Ausdrucks zulassen.<br />

Wir geben ein Beispiel <strong>für</strong> eine Überführung des Infix-Ausdrucks (3 + 4) ∗ (5 − 2) in<br />

Postfix-Notation an:<br />

postfix((3 + 4) ∗ (5 − 2))<br />

= postfix((3 + 4)) postfix((5 − 2)) ∗<br />

= postfix(3 + 4) postfix(5 − 2) ∗<br />

= postfix(3) postfix(4) + postfix(5) postfix(2) − ∗<br />

= 3 4 + 5 2 − ∗<br />

Zur vereinfachten Übertragung eines Ausdrucks in Postfix-Notation führen wir Syntaxbäume<br />

ein.<br />

5.1.1 Syntaxbäume<br />

Syntaxbäume stellen wie die in Kapitel 3 eingeführten Strukturbäume die Struktur<br />

eines Quellprogramms dar. Syntaxbäume enthalten im Gegensatz zu Strukturbäumen<br />

aber keine Nichtterminale. Die Blätter von Syntaxbäumen sind mit Bezeichnern<br />

<strong>und</strong> Werten beschriftet, die inneren Knoten mit Operatorsymbolen. Auf diese Weise<br />

repräsentieren Syntaxbäume die hierarchische Struktur des Quellprogramms.<br />

Beispiel 58 (Arithmetische Ausdrücke mit negativen Operanden)<br />

Die folgende Grammatik beschreibt die Syntax von Zuweisungsoperatoren.<br />

S → id:=E<br />

E → E + E | E * E | -E | ( E ) | id<br />

Die Ausdrücke zur Berechnung des zuzuweisenden Wertes können ein negatives Vorzeichen<br />

besitzen. Wir nehmen an, daß die üblichen Prioritäten der arithmetischen<br />

Operatoren gelten.<br />

In Abbildung 5.2 ist der Syntaxbaum <strong>für</strong> die Anweisung a := b∗(−c)+b∗(−c) dargestellt.<br />

Die Wurzel des Baums repräsentiert den Zuweisungsoperator, der linke Teilbaum<br />

den Bezeichner, dem der berechnete Wert zugewiesen werden soll, <strong>und</strong> der<br />

rechte Teilbaum den auszuwertenden Ausdruck.<br />

143


5 Zwischencode-Erzeugung<br />

assign<br />

a +<br />

*<br />

b minus<br />

*<br />

b minus<br />

c c<br />

Abbildung 5.2: Syntaxbaum<br />

Die Postfix-Notation ist eine linearisierte Darstellung eines Syntaxbaumes. Durchläuft<br />

man den Baum nach dem Postorder-Verfahren (<strong>für</strong> jeden Knoten werden zuerst der<br />

linke <strong>und</strong> der rechte Teilbaum durchlaufen), ergibt sich die Darstellung des Ausdrucks<br />

in Postfix-Notation.<br />

Beispiel 59 (Fortsetzung von Beispiel 58)<br />

Ein Postorder-Durchlauf durch den Syntaxbaum in Abbildung 5.2 erzeugt daher die<br />

folgende Darstellung in Postfix-Notation:<br />

a b c minus ∗ b c minus ∗ + assign<br />

Ein Strukturbaum kann mit Hilfe attributierter Grammatiken in einen Syntaxbaum<br />

überführt werden. Hierzu werden als Attribute Zeiger verwendet, die die einzelnen<br />

Knoten des neu erzeugten Syntaxbaums miteinander verbinden.<br />

Beispiel 60 (Fortsetzung von Beispiel 59)<br />

Folgende attributierte Grammatik erzeugt den Syntaxbaum <strong>für</strong> den oben angegebenen<br />

Ausdruck:<br />

S → id:= E S.nptr := mknode(’assign’,mkleaf(id, id.place), E.nptr)<br />

E → E + E E1.nptr := mknode(’+’, E2.nptr, E3.nptr)<br />

E → E * E E1.nptr := mknode(’*’, E2.nptr, E3.nptr)<br />

E → -E E1.nptr := mknode(’minus’, E2.nptr)<br />

E → ( E ) E1.nptr := E2.nptr<br />

E → id E.nptr := mkleaf(id, id.place)<br />

Der generierte Syntaxbaum ist in Abbildung 5.3 dargestellt. Für jeden Bezeichner<br />

des Ausdrucks wird ein Blatt erzeugt (mit der Funktion mkleaf). Für die Operatoren<br />

werden mit der Funktion mknode innere Knoten generiert. Diese besitzen als Komponenten<br />

Zeiger, die auf den linken bzw. den rechten Teilbaum des jeweiligen Knotens<br />

verweisen.<br />

144


assign<br />

id a<br />

+<br />

* *<br />

id b id b<br />

minus minus<br />

id c id c<br />

5.1 Abstrakte Keller-Maschinen<br />

0<br />

1<br />

2<br />

3<br />

4<br />

5<br />

6<br />

7<br />

8<br />

9<br />

10<br />

11<br />

id<br />

id<br />

minus<br />

b<br />

c<br />

1<br />

* 0 2<br />

id b<br />

id c<br />

minus 5<br />

* 4 6<br />

+ 3 7<br />

id a<br />

assign 9 8<br />

Abbildung 5.3: Konstruktion eines Syntaxbaums aus Postfix-Notation.<br />

. . .<br />

145


5 Zwischencode-Erzeugung<br />

5<br />

9<br />

45<br />

Abbildung 5.4: Auswertung eines Postfix-Ausdrucks mit Hilfe eines Stacks.<br />

Syntaxbäume lassen sich auch in Form von Tabellen angeben. In der rechten Hälfte<br />

von Abbildung 5.3 ist eine solche Tabelle <strong>für</strong> den eben erzeugten Syntaxbaum angegeben.<br />

Die Zeiger werden nun durch die Zeilennummern repräsentiert.<br />

Die Postfix-Darstellung der Anweisung ist dem Zwischencode <strong>für</strong> eine Stackmaschine<br />

bereits sehr ähnlich, da diese Darstellung schon zur Auswertung mit Hilfe eines Stacks<br />

geeignet ist.<br />

Dabei wenden wir das folgende Verfahren an:<br />

• Wir durchlaufen den Postfix-Ausdruck von links nach rechts.<br />

• Ist das aktuelle Symbol ein Wert, wird dieser Wert auf den Stack gelegt <strong>und</strong><br />

bildet somit die neue Kellerspitze.<br />

• Handelt es sich bei dem aktuellen Symbol um einen Operator, werden so viele<br />

Werte vom Stack entfernt, wie der Operator zur Auswertung Operanden<br />

benötigt (die Anzahl der zu entfernenden Werte entspricht der Stelligkeit des<br />

Operators). Aus diesen Werten wird das Ergebnis berechnet <strong>und</strong> auf den Stack<br />

gelegt.<br />

Beispiel 61<br />

Wir berechnen den Ausdruck 9 * 5 + 2. Zuerst wird der Ausdruck in Postfix-Notation<br />

transformiert:<br />

postfix( 9 * 5 + 2 ) = postfix(( 9 * 5 ) + 2 )<br />

= 9 5 * 2 +<br />

Nun wird der generierte Postfix-Ausdruck mit Hilfe eines Stacks ausgewertet (siehe<br />

Abbildung 5.4). Zuerst wird der Wert 9 auf den Stack gelegt. Danach folgt der Wert<br />

5, der ebenfalls auf den Stack gelegt wird. Beim nächsten Symbol handelt es sich um<br />

den Operator *. Dieser besitzt die Stelligkeit 2, so daß zwei Werte vom Stack entfernt<br />

werden müssen. Aus diesen Werten wird das Ergebnis 45 berechnet <strong>und</strong> auf den Stack<br />

gelegt. Danach wird die 2 auf den Stack gelegt <strong>und</strong> durch den Additionsoperator mit<br />

dem Ergebnis der Multiplikation verknüpft.<br />

146<br />

2<br />

45<br />

47


5.1.2 Zwischencode <strong>für</strong> die Keller-Maschine<br />

5.1 Abstrakte Keller-Maschinen<br />

Nachdem wir im vorherigen Abschnitt das Konzept der Auswertung von Postfix-<br />

Ausdrücken erläutert haben, definieren wir nun den Befehlssatz der abstrakten Keller-<br />

Maschine. Weiterhin beschreiben wir die Erzeugung von Zwischencode durch attributierte<br />

Grammatiken.<br />

Wir nehmen an, daß der Befehlssatz der abstrakten Keller-Maschine die folgenden<br />

Befehle beinhaltet:<br />

ADD Addition der obersten beiden Stack-Elemente.<br />

Diese werden entfernt, <strong>und</strong> das Ergebnis wird auf den Stack gelegt.<br />

SUB Subtraktion.<br />

MUL Multiplikation.<br />

LOAD v Lege Wert v auf den Stack.<br />

; Sequentielle Komposition.<br />

Zur Erzeugung des Zwischencodes <strong>für</strong> Ausdrücke mit einer attributierten Grammatik<br />

führen wir ein synthetisches Attribut Code ein <strong>und</strong> definieren die Grammatik wie<br />

folgt:<br />

E → E + T E1.Code := E2.Code; T.Code; ADD<br />

E → E - T E1.Code := E2.Code; T.Code; SUB<br />

E → T E.Code := T.Code<br />

T → T * F T1.Code := T2.Code; F.Code; MUL<br />

T → F T.Code := F.Code<br />

F → ( E ) F.Code := E.Code<br />

F → 0 F.Code := LOAD 0<br />

.<br />

F → 9 F.Code := LOAD 9<br />

Bei der Übersetzung der Operatoren wird zunächst Code <strong>für</strong> die Auswertung der<br />

Operanden erzeugt. Nach Ausführung dieser Codeabschnitte befinden sich ihre Werte<br />

auf dem Stack <strong>und</strong> können dann durch den entsprechenden arithmetischen Befehl<br />

verknüpft werden.<br />

Beispiel 62 (Fortführung von Beispiel 61)<br />

Das anhand der attributierten Grammatik erzeugte Programm <strong>für</strong> die Stack-Maschine<br />

lautet dann:<br />

LOAD 9; LOAD 5; MUL; LOAD 2; ADD<br />

Der attributierte Strukturbaum ist in Abbildung 5.5 angegeben.<br />

5.1.3 Befehle zur Steuerung des Kontrollflusses<br />

Bisher enthielt der Befehlssatz nur die sequentielle Komposition zur Steuerung des<br />

Kontrollflusses. Nun führen wir als neue Zwischencodebefehle bedingte <strong>und</strong> unbeding-<br />

147


5 Zwischencode-Erzeugung<br />

148<br />

LOAD 9<br />

LOAD 9<br />

LOAD 9;LOAD 5;MUL<br />

T<br />

T *<br />

F<br />

9<br />

E<br />

E + T<br />

LOAD 9;LOAD 5;MUL<br />

F<br />

5<br />

LOAD 5<br />

LOAD 9;LOAD 5;MUL;LOAD 2;ADD<br />

F<br />

2<br />

LOAD 2<br />

LOAD 2<br />

Abbildung 5.5: Attributierter Strukturbaum mit Zwischencode.


5.1 Abstrakte Keller-Maschinen<br />

te Sprünge ein. Hierbei verwenden wir anstelle von Speicheradressen Sprungmarken<br />

zur Angabe des Ziels von Sprungbefehlen.<br />

Wir erweitern den in Abschnitt 5.1.2 eingeführten Befehlssatz um die folgenden Befehle:<br />

LABEL l Definition der Sprungmarke l.<br />

GOTO l Sprung zu der Anweisung nach Sprungmarke l<br />

GOFALSE l Entfernen des obersten Stack-Elements,<br />

Sprung falls 0.<br />

GOTRUE l Sprung falls �= 0.<br />

HALT Beenden der Programmausführung.<br />

Der Befehl LABEL definiert eine Sprungmarke, die von den Sprungbefehlen als<br />

Sprungziel verwendet werden kann. Wird ein Sprung ausgeführt, wird die Programmausführung<br />

mit dem der Marke folgenden Befehl fortgesetzt. GOTO ist ein unbedingter<br />

Sprung. Die Befehle GOFALSE <strong>und</strong> GOTRUE sind bedingte Sprünge, die einen<br />

Sprung in Abhängigkeit des Werts an der Kellerspitze ausführen. Hierbei wird eine<br />

semi-boolesche Logik 1 verwendet, in der 0 den Wert false repräsentiert <strong>und</strong> alle anderen<br />

Werte als true interpretiert werden. Befindet sich an der Kellerspitze der Wert<br />

0, führt GOFALSE den Sprung durch, andernfalls wird mit dem nächsten Befehl<br />

fortgefahren. Entsprechend springt GOTRUE, wenn die Kellerspitze einen anderen<br />

Wert als 0 enthält. In jedem Fall wird der oberste Kellerwert entfernt.<br />

Im folgenden betrachten wir die Darstellung der aus imperativen Sprachen bekannten<br />

Kontrollstrukturen if <strong>und</strong> while im Zwischencode. Weitere Konstrukte wie z.B. die<br />

repeat-Schleife lassen sich analog darstellen. Wir nehmen an, daß <strong>für</strong> die relationalen<br />

Operationen wie , = Zwischencodebefehle existieren, die die Werte 0 (<strong>für</strong> false)<br />

bzw. 1 (<strong>für</strong> true) auf den Stack legen.<br />

1 Wie in der Sprache C.<br />

149


5 Zwischencode-Erzeugung<br />

if-Anweisung <strong>und</strong> while-Schleife:<br />

Die folgende Grammatik definiert eine if-Anweisung ohne else-Alternative <strong>und</strong> die<br />

while-Schleife:<br />

stmt → if expr then stmt end |<br />

while expr do stmt end |<br />

stmt; stmt<br />

Die if-Anweisung läßt sich im Zwischencode durch folgendes Schema darstellen:<br />

Code <strong>für</strong> expr; (legt semi-boolschen Wert auf den Stack)<br />

GOFALSE out;<br />

Code <strong>für</strong> stmt;<br />

LABEL out<br />

Zunächst wird Code <strong>für</strong> die Berechnung des booleschen Ausdrucks generiert. Dieser<br />

legt als Ergebnis entweder 0 (<strong>für</strong> false) oder 1 (<strong>für</strong> true) auf den Keller. Ist der<br />

Wert 0, sollen Befehle nach dem then nicht ausgeführt werden. Dies wird durch den<br />

Sprungbefehl GOFALSE realisiert, der in diesem Fall an das Ende des Codes <strong>für</strong> die<br />

if-Anweisung verzweigt.<br />

Analog wird die while-Schleife durch das folgende Schema realisiert:<br />

LABEL test;<br />

Code <strong>für</strong> expr;<br />

GOFALSE out;<br />

Code <strong>für</strong> stmt;<br />

GOTO test;<br />

LABEL out<br />

Zunächst wird wieder Code <strong>für</strong> die Auswertung des Ausdrucks erzeugt. Legt dieser<br />

Code nach Ausführung den Wert 0 auf den Stack, wird der Schleifenrumpf nicht<br />

mehr ausgeführt, indem mit GOFALSE ans Ende der Schleife verzweigt wird. Wird<br />

der Schleifenrumpf ausgeführt, wird anschließend mit GOTO wieder an den Anfang<br />

der Schleife gesprungen <strong>und</strong> der Code <strong>für</strong> die Berechnung von expr erneut ausgeführt.<br />

Enthält ein Programm mehrere if- oder while-Anweisungen, müssen bei der Übersetzung<br />

jeder dieser Anweisungen neue Sprungmarken erzeugt werden. Daher nehmen<br />

wir im folgenden an, daß eine Funktion newlabel zur Verfügung steht, die in jedem<br />

Aufruf eine neue Sprungmarke generiert.<br />

Hinzu kommt, daß die Kontrollflußanweisungen auch verschachtelt auftreten dürfen.<br />

Daher muß eine Verwaltung der Sprungmarken gewährleisten, daß die Sprungbefehle<br />

<strong>und</strong> die zugehörigen Marken die Verschachtelung der Anweisungen korrekt widerspiegeln.<br />

Dies erreichen wir, indem wir spezielle Attribute <strong>für</strong> das Zwischenspeichern<br />

von Marken verwenden.<br />

Wir betrachten als Beispiel die Produktion <strong>für</strong> die if-Anweisung:<br />

150


5.2 Drei-Adreß-Code<br />

stmt → if expr then stmt end<br />

stmt1.out := newlabel<br />

stmt1.Code := expr.Code;<br />

GOFALSE stmt1.out;<br />

stmt2.Code;<br />

LABEL stmt1.out<br />

Wir verwenden ein Attribut out, das die von der Funktion newlabel erzeugte Sprungmarke<br />

speichert. Die GOFALSE- <strong>und</strong> LABEL-Befehle verwenden dann die in diesem<br />

Attribut gespeicherte Sprungmarke. Entsprechendes gilt <strong>für</strong> den Code der while-<br />

Schleife.<br />

Beispiel 63 (Geschachtelte if-Anweisung)<br />

Wir übersetzen eine geschachtelte if-Anweisung in Zwischencode <strong>für</strong> die abstrakte Kellermaschine.<br />

Hierbei nehmen wir an, daß die Funktion newlabel Zahlen als Sprungmarken<br />

generiert.<br />

Wir betrachten die folgende Anweisung:<br />

if e1 then if e2 then s end end ; other<br />

e1 <strong>und</strong> e2 repräsentieren Ausdrücke, s steht <strong>für</strong> eine Sequenz von Anweisungen. Der<br />

attributierte Strukturbaum <strong>für</strong> die Anweisung ist in Abbildung 5.6 dargestellt. Der<br />

Inhalt des Attributs Code wird durch Kästen angegeben, der Inhalt von out durch<br />

Kreise.<br />

5.2 Drei-Adreß-Code<br />

Eine weitere Variante von Zwischencode ist der Drei-Adreß-Code. Im Vergleich zum<br />

Zwischencode <strong>für</strong> abstrakte Keller-Maschinen ist er näher an der “Maschinensprache”<br />

realer Maschinen einzuordnen.<br />

Wie der Zwischencode <strong>für</strong> die Keller-Maschine besteht der Drei-Adreß-Code aus einer<br />

Folge von Befehlen. Jeder Befehl gehört zu einem der folgenden Formate:<br />

x := y op z op ist ein arithmetischer oder logischer binärer<br />

Operator<br />

x := op y op ist unärer Operator<br />

z.B. log. Negation, Schiebe-Operation<br />

x := y Kopieranweisung<br />

goto l unbedingter Sprung<br />

if x relop y goto l bedingter Sprung: relop ist ein Vergleichsoperator<br />

Weiterhin gibt es spezielle Befehle zur Daten-/Speicher-Behandlung <strong>und</strong> <strong>für</strong> Prozedur-<br />

Aufrufe.<br />

151


5 Zwischencode-Erzeugung<br />

Code(e1)<br />

GOFALSE1<br />

Code(e2)<br />

GOFALSE2<br />

Code(s)<br />

LABEL2<br />

LABEL1<br />

152<br />

Code<br />

out<br />

if then<br />

expr<br />

stmt<br />

Code(e1)<br />

if<br />

expr<br />

1<br />

2<br />

stmt<br />

; stmt<br />

end other<br />

stmt<br />

then end<br />

Code(e2)<br />

Code(e2)<br />

GOFALSE2<br />

Code(s)<br />

LABEL2<br />

stmt<br />

Code(e1)<br />

GOFALSE1<br />

Code(e2)<br />

GOFALSE2<br />

Code(s)<br />

LABEL2<br />

LABEL1<br />

Code(other)<br />

Code(s)<br />

Code(other)<br />

Abbildung 5.6: Attributierter Strukturbaum einer verschachtelten if-Anweisung.


5.2 Drei-Adreß-Code<br />

Die Bezeichnung “Drei-Adreß-Code” kommt daher, daß alle Befehle der oben angeführten<br />

Formate maximal drei Adressen enthalten (zwei Adressen <strong>für</strong> die Operanden<br />

des Befehls <strong>und</strong> eine Adresse <strong>für</strong> die Speicherung des Ergebnisses). Im Gegensatz<br />

zur abstrakten Keller-Maschine, bei der die Werte <strong>und</strong> Zwischenergebnisse auf dem<br />

Keller verwaltet wurden, werden beim Drei-Adreß-Code die Werte <strong>und</strong> Ergebnisse in<br />

einem Arbeitsspeicher gehalten. Jeder Drei-Adreß-Befehl liest seine Operanden aus<br />

dem Speicher <strong>und</strong> schreibt das errechnete Ergebnis ebenfalls in eine Speicherzelle.<br />

Da jeder Drei-Adreß-Befehl maximal zwei Operanden verknüpfen kann, müssen komplexe<br />

Ausdrücke in Folgen von Befehlen übersetzt werden. Die dabei entstehenden<br />

Zwischenergebnisse werden in Speicherzellen abgelegt, die durch temporäre Namen<br />

gekennzeichnet sind. “Temporär” bedeutet in diesem Zusammenhang, daß die Namen<br />

nicht Bezeichnern des Quellprogramms entsprechen.<br />

Beispiel 64<br />

Im folgenden wird eine Übersetzung des Ausdrucks x+y∗z angegeben:<br />

x + y * z → t1 := y * z<br />

t2 := x + t1<br />

Aufgr<strong>und</strong> der Operatorprioritäten wird die Multiplikation zuerst ausgeführt <strong>und</strong> ihr<br />

Ergebnis in einem Speicherplatz mit temporärem Namen t1 zwischengespeichert. Der<br />

zweite Befehl addiert den Wert der Speicherzelle x zum Zwischenergebnis <strong>und</strong> legt<br />

das Ergebnis im temporären Speicherplatz t2 ab.<br />

5.2.1 Übersetzung von Syntaxbäumen in Drei-Adreß-Code<br />

Bei der Übersetzung eines Syntaxbaums in Drei-Adreß-Code müssen zuerst die temporären<br />

Namen <strong>für</strong> die Ablage von Zwischenergebnissen vereinbart werden. Dies geschieht,<br />

indem die inneren Knoten des Syntaxbaums mit temporären Namen beschriftet<br />

werden.<br />

Beispiel 65<br />

Wir übersetzen die Anweisung a := b∗(−c)+b∗(−c) aus Beispiel 58 in Drei-Adreß-<br />

Code. Hierzu beschriften wir die inneren Knoten des Syntaxbaums aus Abbildung<br />

5.2, die Zwischenergebnisse repräsentieren, mit den temporären Namen t1 bis t5.<br />

(Abbildung 5.7).<br />

Nachdem die temporären Namen vergeben sind, kann aus dem Syntaxbaum Drei-<br />

Adreß-Code erzeugt werden. Hierzu wird der Baum im Postorder-Verfahren durchlaufen,<br />

so daß vor der Bearbeitung eines Knotens zuerst seine Teilbäume ausgewertet<br />

werden. Für jeden mit einem temporären Namen beschrifteten Knoten wird ein<br />

Drei-Adreß-Befehl generiert. Als Zieladresse des Befehls wird der temporäre Name<br />

153


5 Zwischencode-Erzeugung<br />

assign<br />

a +<br />

*<br />

t2<br />

b minus<br />

t1<br />

t5<br />

*<br />

t4<br />

b minus<br />

c c<br />

Abbildung 5.7: Syntaxbaum mit temporären Namen.<br />

verwendet. Die Operanden ergeben sich aus den Beschriftungen der Kinder des Knotens.<br />

Ebenso wird mit jedem Knoten verfahren, der einen Speicherplatz repräsentiert,<br />

auf den schreibend zugegriffen wird.<br />

Beispiel 66 (Fortsetzung von Beispiel 65)<br />

Ein Postorder-Durchlauf durch den in Abbildung 5.7 angegebenen Syntaxbaum nach<br />

dem eben erläuterten Verfahren generiert die folgende Sequenz von Drei-Adreß-Befehlen:<br />

t1 := -c<br />

t2 := b * t1<br />

t3 := -c<br />

t4 := b * t3<br />

t5 := t2 + t4<br />

a := t5<br />

Bemerkung: Der in dem Beispiel erzeugte Drei-Adreß-Code ist nicht optimal, da<br />

nicht beachtet wird, daß der Teilausdruck b∗(−c) zweimal auftritt. Aus diesem Gr<strong>und</strong><br />

ist die Verwendung der Speicherplätze t3 <strong>und</strong> t4 überflüssig <strong>und</strong> der vorletzte Befehl<br />

könnte zu t5 := t2 + t2 modifiziert werden. Auf diese Weise können zwei Befehle <strong>und</strong><br />

zwei temporäre Speicherplätze eingespart werden. Im Rahmen der Codegenerierung<br />

in Kapitel ?? geben wir ein Verfahren an, daß die beschriebene Optimierung <strong>für</strong><br />

mehrfach auftretende Teilausdrücke durchführt.<br />

154<br />

t3


5.2 Drei-Adreß-Code<br />

5.2.2 Übersetzung in Drei-Adreß-Code unter Verwendung von<br />

attributierten Grammatiken<br />

Wir beschreiben nun die Generierung von Drei-Adreßcode mit Hilfe von attributierten<br />

Grammatiken. Wir verwenden dabei die auf Seite 147 angegebene kontextfreie<br />

Grammatik zur Beschreibung von Ausdrücken. Das Attribut Code speichert den zu<br />

einem Nichtterminal generierten Drei-Adreß-Code. Das Attribut place enthält den<br />

Namen des Speicherplatzes, in dem das Ergebnis eines Befehls abgelegt werden soll.<br />

Wir nehmen an, daß eine Funktion newtemp existiert, die bei jedem Aufruf einen<br />

neuen temporären Namen generiert.<br />

S → id := E S.Code := E.Code;<br />

id.place := E.place<br />

E → E + E E1.place := newtemp<br />

E1.Code := E2.Code; E3.Code;<br />

E1.place := E2.place + E3.place<br />

E → E * E (analog)<br />

E → -E E1.place := newtemp<br />

E1.Code := E2.Code;<br />

E1.place := - E2.place<br />

E → (E) E1.place := E2.place<br />

E1.Code := E2.Code<br />

E → id E1.place := id.place<br />

E1.Code := ...<br />

Bei der Übersetzung eines arithmetischen Operatores wird zunächst ein neuer temporärer<br />

Name erzeugt. Der Code <strong>für</strong> diesen Operator besteht aus dem Code <strong>für</strong> die<br />

Berechnung der Operanden. Anschließend wird ein Befehl angefügt, der die Inhalte<br />

der Speicherzellen, die die Zwischenergebnisse beinhalten, mit dem Operator verknüpft.<br />

Das Ergebnis wird in den zuvor generierten Zwischenspeicher geschrieben.<br />

Beispiel 67<br />

In Abbildung 5.8 ist der attributierte Syntaxbaum <strong>für</strong> die Anweisung a := b * (<br />

- c ) + d angegeben. Die Codefolge aus Drei-Adreß-Befehlen wird schrittweise in<br />

einem Bottom-Up-Durchlauf zusammengesetzt, bis das Code-Attribut an der Wurzel<br />

die vollständige Befehlsfolge beinhaltet. Somit ergibt sich eine durch die Übersetzung<br />

folgende Sequenz von Drei-Adreß-Befehlen:<br />

t1 := - c<br />

t2 := b * t1<br />

t3 := t2 + d<br />

a := t3<br />

Die Übersetzung von Kontrollstrukturen erfolgt analog. Im Gegensatz zu den in<br />

Abschnitt 5.1.3 beschriebenen Befehlen GOFALSE <strong>und</strong> GOTRUE <strong>für</strong> die abstrak-<br />

155


5 Zwischencode-Erzeugung<br />

place = a<br />

S<br />

id :=<br />

E + E<br />

E * E<br />

id<br />

place = b<br />

place = b<br />

place = t2<br />

Code = t1 := -c;<br />

(<br />

-<br />

Code = t1 := -c; t2 := b * t1;<br />

t2 := b * t1<br />

E<br />

t3 := t2 + d; a := t3<br />

place = t1<br />

E<br />

id<br />

E<br />

Code = t1 := - c<br />

place = t1<br />

Code = t1 := - c<br />

place = t3<br />

Code = t1 := -c; t2 := b * t1;<br />

place = c<br />

place = c<br />

t3 := t2 + d<br />

id<br />

)<br />

place = d<br />

place = d<br />

Abbildung 5.8: Syntaxbaum mit Attributen <strong>für</strong> die Erzeugung von Drei-Adreß-Code.<br />

156


5.3 Vergleich der beiden Arten von Zwischencode<br />

te Keller-Maschine steht im Drei-Adreß-Code ein Befehl if x relop y goto l zur<br />

Verfügung, der Vergleichsoperation <strong>und</strong> Sprung in einem Befehl vereinigt.<br />

5.3 Vergleich der beiden Arten von Zwischencode<br />

In diesem Kapitel haben wir zwei unterschiedliche Arten von Zwischencode vorgestellt.<br />

Die Entscheidung <strong>für</strong> die Realisierung einer der beiden Methoden in einem<br />

Compiler hängt im wesentlichen vom angestrebten Ziel des Compilers ab.<br />

Die Darstellung von Zwischencode in Form von Code <strong>für</strong> abstrakte Keller-Maschinen<br />

ist geeignet, wenn eine weitere Übersetzung des Zwischencodes in die Maschinensprache<br />

einer realen Maschine nicht vorgesehen ist. Dies ist oft der Fall bei der Implementierung<br />

nicht-imperativer Programmiersprachen. Daher soll von der realen Hardware<br />

(Speicheradressen usw.) möglichst weit abstrahiert werden, um das Speichermanagement<br />

zu vereinfachen. Dies wird durch die Verwaltung der Daten <strong>und</strong> Variablen auf<br />

einem Stack erreicht.<br />

Der Drei-Adreß-Code hingegen ist gut geeignet <strong>für</strong> die Anwendung von Optimierungsverfahren.<br />

Daher bietet sich seine Verwendung an, wenn die Erstellung von<br />

Zwischencode nur als Vorstufe <strong>für</strong> die eigentliche Codegenerierung vorgesehen ist.<br />

Durch die Anwendung von Optimierungsverfahren kann eine maschinenunabhängige<br />

Codeoptimierung vorgenommen werden. Zudem ist der Drei-Adreß-Code “maschinennäher”<br />

als der Code <strong>für</strong> die Keller-Maschine, da er nicht im selben Maße von den<br />

Gegebenheiten der Hardware abstrahiert.<br />

157


Literaturverzeichnis<br />

[Aho08] Sethi <strong>und</strong> Ullman Aho. Compiler. Prinzipien, Techniken <strong>und</strong> Werkzeuge.<br />

Pearson Studium, 2008. i<br />

[AK97] Heiko Vogler Armin Kühnemann. Attributgrammatiken. Eine gr<strong>und</strong>legende<br />

Einführung. Vieweg+Teubner, 1997.<br />

[ANT] ANTLR Parser Generator (http://www.antlr.org/). 117<br />

[ASU99] Alfred V. Aho, Ravi Sethi, Jeffrey D. Ullman. Compilerbau, Teil 1+2.<br />

Oldenbourg, 1999. 5, 8, 111, 113, 116, 128<br />

[AU72] Alfred V. Aho, Jeffrey D. Ullman. The Theory of Parsing, Translation,<br />

and Compiling. Addison-Wesley, 1972. 43<br />

[Bar83] J.G.P. Barnes. Programmieren in ADA. Hanser, 1983. 118<br />

[CM94] William F. Clocksin, Christopher S. Mellish. Programmieren in Prolog.<br />

Springer, 1994. 142<br />

[Ger91] Uwe Gerlach. Das Transputerbuch: der Einstieg in die Welt der Transputer.<br />

Markt & Technik, 1991.<br />

[GR83] Adele Goldberg, David Robson. Smalltalk-80 – The Language and its<br />

Implementation. Addison-Wesley, 1983. 131<br />

[Güt85] Rainer Güting. Einführung in UCSD-Pascal. Schwann-Bagel, 1985. 141<br />

[HM] Albrecht Wöß University of Linz Hanspeter Mössenböck, Markus Löberbauer.<br />

The Compiler Generator Coco/R (http://ssw.jku.at/coco/). 117<br />

[HMM97] Robert Harper, David MacQueen, Robin Milner. Standard ML. Technischer<br />

Bericht ECS–LFCS–86–2, Mit Press, 1997. 142<br />

[Hum92] Robert L. Hummel. Die Intel-Familie: technisches Referenzhandbuch <strong>für</strong><br />

den 80x86 <strong>und</strong> 80x87. TLC The Learning Companie, 1992.<br />

[Int94] SPARC International. The SPARC architecture manual. Prentice Hall,<br />

1994.<br />

[JAV] javacc: JavaCCHome (https://javacc.dev.java.net/). 117<br />

[JL92] Doug Brown John Levine, Tony Mason. Lex and Yacc: UNIX Programming<br />

Tools (A Nutshell handbook). O’Reilly Media, 1992. 117<br />

[JW91] Kathleen Jensen, Niklaus Wirth. Pascal user manual and report: ISO<br />

Pascal standard. Springer, 1991. 12, 131<br />

[Kop02] Helmut Kopka. L ATEX– eine Einführung. Addison-Wesley, 2002. 5<br />

[KR90] Brian W. Kernighan, Dennis M. Ritchie. Programmieren in C. Hanser,<br />

158


Literaturverzeichnis<br />

1990. 5, 105, 111, 131, 142<br />

[KV97] Armin Kühnemann, Heiko Vogler. Attributgrammatiken – Eine gr<strong>und</strong>legende<br />

Einführung. Vieweg+Teubner, 1997. 121<br />

[LE89] Henry M. Levy, Richard H. Eckhouse. Computer programming and architecture:<br />

the VAX. Digital Press, 1989.<br />

[LY99] Tim Lindholm, Frank Yellin. The Java Virtual Machine Specification.<br />

Addison-Wesley, 1999. 141<br />

[LYP] The LEX & YACC Page (http://dinosaur.compilertools.net/). 117<br />

[MB92] Tony Mason, Doug Brown. lex & yacc. O’Reilly & Associates Inc., 1992.<br />

111<br />

[Mot91] Motorola. M68000: 8-/16-/32-bit microprocessors user’s manual.<br />

Prentice-Hall, 1991.<br />

[OT97] Peter W. O’Hearn, Robert D. Tennent. ALGOL-Like Languages.<br />

Birkhäuser, 1997. 12<br />

[Pau96] Laurence C. Paulson. ML for the Working Programmer. Cambridge<br />

University Press, 1996. 142<br />

[Po96] John Peterson, other. Report on the Programming Language Haskell –<br />

Version 1.3. Technischer Bericht, University of Glasgow, 1996. 131, 142<br />

[SAB] SableCC (http://sablecc.org/). 117<br />

[Sch08] Uwe Schöning. Theoretische Informatik kurz gefaßt. Spektrum Akademischer<br />

Verlag, 2008. 36, 46, 118<br />

[Thi97] Peter Thiemann. Gr<strong>und</strong>lagen der funktionalen <strong>Programmierung</strong>. Teubner,<br />

1997. 129<br />

[WG92] Niklaus Wirth, Jürg Gutknecht. Project Oberon – The Design of an<br />

Operating System and Compiler. Addison-Wesley, 1992. 137<br />

[Wir86] Niklaus Wirth. Compilerbau. Teubner, 1986.<br />

[Wir97] Niklaus Wirth. Programmieren in Modula-2. Springer, 1997. 6, 131, 142<br />

[WM96] Reinhard Wilhelm, Dieter Maurer. Übersetzerbau – Theorie, Konstruktion,<br />

Generierung. Springer, 1996. i, 10, 23, 37, 88, 110, 111, 113, 142<br />

[WMW85] Gerhard Goos William M. Waite. Compiler Construction. Springer-<br />

Verlag GmbH, 1985.<br />

159

Hurra! Ihre Datei wurde hochgeladen und ist bereit für die Veröffentlichung.

Erfolgreich gespeichert!

Leider ist etwas schief gelaufen!