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