205 49 21MB
German Pages 287 [288] Year 1975
de Gruyter Lehrbuch Schneider, C o m p i l e r
Compiler Aufbau und Arbeitsweise
von
Hans-Jürgen Schneider
W DE
G Walter de Gruyter • Berlin • New York 1975
Dr. rer. nat. Hans Jürgen Schneider, o. Prof. für Informatik (Programmier- und Dialogsprachen sowie Compiler), Mitvorstand des Instituts für Mathematische Maschinen und Datenverarbeitung der Universität Erlangen-Nürnberg
I S B N 3 11 0 0 2 0 5 8 0 0 © Copyrigth 1975 by Walter de Gruyter & Co., vormals G. J. Göschen'sche Verlagshandlung, J. Guttentag, Verlagsbuchhandlung Georg Reimer, Karl J. Trübner, Veit & Comp., Berlin 30. Alle Rechte, insbesondere das Reicht der Vervielfältigung und Verbreitung sowie der Übersetzung, vorbehalten. Kein Teil des Werkes darf in irgendeiner Form (durch Photokopie, Mikrofilm oder ein anderes Verfahren) ohne schrifliche Genehmigung des Verlages reproduziert oder unter Verwendung elektronischer Systeme verarbeitet, vervielfältigt oder verbreitet werden. Printed in Germany. Satz: IBM-Composer, Walter de Gruyter, Berlin. - Druck: Karl Gerike, Berlin. Bindearbeiten: Lüderitz & Bauer, Berlin.
Vorwort
Dieses Buch ist aus Vorlesungen entstanden, die der Autor an den Universitäten Erlangen-Nürnberg und Saarbrücken gehalten hat. Der Aufbau des Buches entspricht dem der Vorlesungen für Studenten der Informatik vom 6. Semester an. Das Buch soll jedoch einen breiteren Leserkreis ansprechen, wobei in erster Linie an Studenten und bereits im Berufsleben stehende Absolventen „verwandter" Fachrichtungen (z. B. Mathematik, Elektrotechnik, Physik) gedacht ist. Da die Informatik eine sehr junge Wissenschaft ist, sind heute viele Positionen, die einen Informatiker erfordern, mit Absolventen der genannten Fachrichtungen besetzt, die sich erst im Rahmen ihrer Berufstätigkeit die erforderlichen Informatik-Kenntnisse angeeignet haben. Um auch diesen Personenkreis als Leser zu gewinnen, ist das Buch gegenüber der Vorlesung um einige Abschnitte erweitert und setzt nur gute Programmierkenntnisse in einer problemorientierten, möglichst blockstrukturierten Programmiersprache, die Grundkenntnisse über den Aufbau einer Rechenanlage und die Kenntnis der Schreibweise mathematischer Formeln voraus. Dies gehört heute zur Grundausbildung in vielen Fachrichtungen sowohl der Hochschulen als auch der Fachhochschulen. Darstellung und Stoffauswahl gehen davon aus, daß nur ganz wenige der Leser in die Situation kommen werden, einen vollständigen Compiler zu entwerfen. In den meisten Fällen werden Kenntnisse über Compiler benötigt, um effektiver zu programmieren oder Code-Prozeduren an problemorientierte Programme anzuschließen. Eine weitere Gruppe von Lesern wird mit der Erweiterung von Compilern (neue Sprachelemente) und der Erstellung einzelner Compilerteile beschäftigt sein. Alle diese Leser sind an ein vorhandenes Compiler-Konzept gebunden. Daher werden im ersten Teil dieses Buches (Kap. 0—3) verschiedene Methoden nebeneinander erläutert; auch der Beispiel-Compiler (Kap. 4) ist in jeder dieser Methoden formulierbar. In den zweiten Teil (Kap. 5—7) geht die spezielle Übersetzungsmethode nicht ein, und der dritte wird nur für syntaxgesteuerte Methoden benötigt. Ein besonderer Dank gilt meinen Mitarbeitern W. Altmann, W. Brügmann, H. Göttler, M. Nagl, G. Stöth und D. Weber, die mit großem Fleiß die verschiedenen Entwicklungsstadien des Manuskriptes prüften und durch ihre kritischen Anmerkungen zur vorliegenden Form einen erheblichen Beitrag geleistet haben, und Frau J. Bischoff für die sorgfältige Anfertigung des Manuskriptes. Erlangen, im Sommer 1975
H. J. Schneider
Übersicht über die Abhängigkeit der einzelnen Abschnitte voneinander Zeigt ein Pfeil von a nach b, so besagt dies, daß a Voraussetzungen für das Verständnis von b enthält.
Inhalt
0. Einführung
15
0.1 Programmiersprachen 0.1.1 Begriff 0.1.2 Maschinensprachen 0.1.3 Maschinenorientierte Programmiersprachen 0.1.4 Problemorientierte Programmiersprachen 0.1.5 Einfluß des Kontextes bei problemorientierten Programmiersprachen
15 15 16 16 17 18
0.2 Kompilation und Interpretation 0.2.1 Unterschied von Kompilation und Interpretation 0.2.2 Kompilation 0.2.3 Aufgaben des Compilers 0.2.4 Interpretation
19 19 19 20 21
.
0.3 Arbeitsphasen eines Compilers 0.3.1 Zerlegung in Phasen 0.3.2 Lexikalische Aufbereitung 0.3.3 Syntaktische Analyse 0.3.4 Weitergehende Zerlegung der syntaktischen Analyse 0.3.5 Abschließende Phasen
21 21 22 23 24 24
0.4 Weiterführende Konzepte 0.4.1 Maschinenunabhängige Compiler-Konzepte 0.4.2 Bootstrapping 0.4.3 Inkrementelle Compiler 0.4.4 Weitere Möglichkeiten
25 25 25 27 28
0.5 Literaturhinweise 0.5.1 Lehrbücher und Übersichtsartikel zur Programmierung 0.5.2 Entwicklungstendenzen bei Programmiersprachen 0.5.3 Spezielle Programmiersprachen 0.5.4 Lehrbücher und Übersichtsartikel über Compiler 0.5.5 Maschinenunabhängige Konzepte 0.5.6 Weitere Möglichkeiten
28 28 28 29 30 30 30
1 . Problemstellung und grundlegende Übersetzungstechniken
32
1.1 Problemstellung 1.1.1 Ein Beispiel 1.1.2 Zerlegung einer Anweisung 1.1.3 Maschinenorientierte Fassung des Beispiels 1.1.4 Übersetzung von links nach rechts 1.1.5 Compiler-Erstellung
32 32 32 33 34 35
1.2 Definition des Sprachumfangs 1.2.1 Syntax und Semantik 1.2.2 Zur Definition der Syntax 1.2.3 Metasprache nach van Wijngaarden 1.2.4 Definition der arithmetischen Wertzuweisung 1.2.5 Darstellung einzelner Symbole
35 35 36 37 37 38
8
Inhalt 1.3 Sequentielle Formelübersetzung 1.3.1 Einführung des Kellerungsprinzips 1.3.2 Die Fälle in einer Übergangsmatrix 1.3.3 Die Übersetzung mittels Übergangsmatrix 1.3.4 Die Ausgänge der Fallunterscheidung 1.3.5 Die Übersetzung des Beispiels 1.3.6 Zusammenfassung der Keller 1.3.7 Programm zum Übersetzungsverfahren
38 38 39 40 41 43 43 44
1.4 Vorrangverfahren 1.4.1 Ausnutzung des Operatorenvorrangs 1.4.2 Vorrangalgorithmus zur Übersetzung von Wertzuweisungen 1.4.3 Weiterführende Vorrangkonzepte 1.4.4 Konstruktion von Vorrangfunktionen
45 45 46 48 49
1.5 Verfahren des rekursiven Abstiegs 1.5.1 Grundgedanke 1.5.2 Erkennung von Primärausdrücken 1.5.3 Erkennung von Faktoren, Tennen und Ausdrücken 1.5.4 Erkennung der Wertzuweisung 1.5.5 Compiler-Compiler
49 49 50 51 52 53
1.6 Die Produktionssprache von Floyd und Evans 1.6.1 Grundgedanke 1.6.2 Floyd-Evans-Befehle 1.6.3 Beispiel 1.6.4 Abkürzungen 1.6.5 Übersetzung der Wertzuweisungen mit einem FE-Programm
54 54 55 57 58 59
2. Grundlegende Techniken auf der maschinenorientierten Seite
61
2.1 Kellerung der Zwischenergebnisse 2.1.1 Zerlegung einer Anweisung 2.1.2 Zwischenergebniskeller 2.1.3 Maschinenbefehle 2.1.4 Beispiel (Ein-Adreß-Version) 2.1.5 Beispiel (Zwei-Adreß-Version)
61 61 61 62 63 64
2.2 Die Code-Generierung . . : 2.2.1 Aufgaben der Routine GENERATE 2.2.2 Globale Information für GENERATE 2.2.3 Programm fur GENERATE
65 65 65 67
2.3 Realisierung des Zwischenergebniskellers 2.3.1 Grundgedanke 2.3.2 V o r - u n d Nachteile beider Verfahren 2.3.3 Indirekte und modifizierte Adressen 2.3.4 Indexregister 2.3.5 Lösung für beide Verfahren
68 68 69 70 71 72
2.4 Operandenarten und Artanpassung 2.4.1 Verfügbare Operatoren 2.4.2 Artanpassungen in höheren Programmiersprachen 2.4.3 Generierung der Artanpassungen 2.4.4 Berücksichtigung der Artanpassungen in der Symbolliste 2.4.5 Dereferenzierung
73 73 74 74 75 75
2.5 Informationsvektoren 2.5.0 Indizierte Variable
76 76
Inhalt
9 2.5.1 2.5.2 2.5.3 2.5.4 2.5.5 2.5.6
Speicherung von Feldern Speicherabbildungsfunktion Versorgung der Speicherabbildungsfunktion mit den Indexwerten . . . Beispiel Die Übersetzung der indizierten Variablen Die Übersetzung der Deklarationen
2.6 Dynamische Speicherverwaltung 2.6.1 Charakterisierung der verschiedenen Speicherbereiche 2.6.2 Beispiel 2.6.3 Die Verwaltung des freien Speichers 2.6.4 Beispiel 2.6.5 Die Übersetzung der Blockstruktur 3. Lexikalische Analyse
77 78 79 80 81 82 82 82 83 84 85 86 87
3.1 Die Erkennung von Identifikatoren und Konstanten 3.1.0 Aufgabenstellung 3.1.1 Verwaltung des Eingabetextes 3.1.2 Identifikatoren 3.1.3 Ganzzahlige Konstante 3.1.4 Übergang zu reellwertigen Konstanten
87 87 87 88 89 89
3.2 Ein automatentheoretisches Modell 3.2.1 Grundgedanke 3.2.2 Automat zur Erkennung von Identifikatoren und Konstanten 3.2.3 Automatentafel 3.2.4 Bearbeitung der Identifikatoren und ganzzahligen Konstanten 3.2.5 Bearbeitung der reellwertigen Konstanten
90 90 90 92 94 95
3.3 Begrenzungssymbole 3.3.1 Problemstellung 3.3.2 Bearbeitung der Begrenzungssymbole (Lochkartendarstellung) 3.3.3 Zeichenketten und Kommentare
96 96 97 98
3.4 Aufbau der Symbolliste 3.4.1 Der Gültigkeitsbereich einzelner Symbole 3.4.2 Beispiel 3.4.3 Symbolliste bei Blockstruktur 3.4.4 Beispiel 3.4.5 Suchen in einer blockstrukturierten Symbolliste 4. Ein Beispiel-Compiler
99 99 100 103 103 104 107
4.0 Überblick 4.0.1 Sprachumfang 4.0.2 Einteilung der Phasen 4.0.3 Implementierung
107 107 109 110
4.1 Phase 4:1.0 4.1.1 4.1.2 4.1.3 4.1.4 4.1.5 4.1.6
111 III 113 115 116 118 118 119
I Rechnerabhängige Prozeduren Prozedur NEXT SYMBOL Liste der Begrenzer Hauptprogramm Identifikatoren Ganzzahlige Konstanten Reellwertige Objekte
10
Inhalt 4.2 Phasell 4.2.0 Übersicht 4.2.1 Kellerprozeduren 4.2.2 Symbolliste 4.2.3 Programmübersicht und Blockstruktur 4.2.4 Einfache Deklarationen 4.2.5 Feldvereinbarungen 4.2.6 Marken 4.2.7 Anweisungen
120 120 121 123 125 127 128 130 130
4.3 Phase 4.3.0 4.3.1 4.3.2 4.3.3 4.3.4 4.3.5 4.3.6 4.3.7 4.3.8 4.3.9
III Übersicht Kellerprozeduren Identifizierung der Identifikatoren und Adressenzuweisung Programmübersicht Blockstruktur Marken und Sprunganweisungen Deklarationen Feldvereinbarungen und indizierte Variable Indexgrenzen und Indizes Ausdrücke und Anweisungen
131 131 132 133 135 137 139 140 142 144 146
4.4 Phase 4.4.0 4.4.1 4.4.2 4.4.3 4.4.4 4.4.5 4.4.6 4.4.7
IV Übersicht Kommandobearbeitung Identifizierung der Operatoren Programmübersicht Wertzuweisungen Aktionen Indexgrenzen, Indizes und Klammern Adressierung
148 148 149 152 154 155 157 160 161
5. Die Übersetzung verschiedener Datenarten
162
5.1 Aspekte der Optimierung bei arithmetischen Ausdrücken 5.1.1 Gemeinsame Teilausdrücke 5.1.2 Heuristische Optimierungen 5.1.3 Registereinsparung 5.1.4 Operatoren für zusammengesetzte Operanden
162 162 163 164 165
5.2 Behandlung von Präfix-Arten 5.2.1 Arten verschiedener Genauigkeit 5.2.2 Arten verschiedener Referenzstufen 5.2.3 Aufbau der Symbolliste 5.2.4 Übersetzung der Präfixarten
166 166 167 167 169
5.3 Behandlung weiterer zusammengesetzter Arten 5.3.1 Feldarten 5.3.2 Verbundarten 5.3.3 Erweiterung uer Symbolliste 5.3.4 Vereinigung von Arten
170 170 171 172 173
5.4 Blockunabhängige Speicherverwaltung 5.4.0 Übersicht 5.4.1 COMMON und EXTERNAL 5.4.2 Programmierte Speicherverwaltung 5.4.3 Halde
174 174 175 175 176
Inhalt
11
6. Übersetzung der Kontrollstrukturen
177
6.1 Vergleiche und boolesche Formeln 6.1.1 Sprachumfang 6.1.2 Kaskadenübersetzung 6.1.3 Beispiel 6.1.4 Zielcode 6.1.5 Boolesche Wertzuweisung
177 177 178 179 180 180
6.2 Bedingte Anweisungen 6.2.1 Bedingungen auf Maschinenebene 6.2.2 Zielcode 6.2.3 Beispiel 6.2.4 Übersetzung des linken Operanden 6.2.5 Übersetzung des rechten Operanden
181 181 182 183 186 187
6.3 Laufschleifen (Zyklen) 6.3.1 Darstellung der Schleifen mit Abfragen 6.3.2 Zielcode 6.3.3 Lauflisten 6.3.4 Rekursive Adressenberechnung
188 189 190 191 191
6.4 Fallunterscheidungen 6.4.1 Fallunterscheidungen 6.4.2 Verteiler 6.4.3 Markenvariable
193 193 194 195
6.5 Vorwärtssprünge 6.5.1 Vormerkliste 6.5.2 Verkettete Vormerkungen
196 196 196
7. Unterprogramm 7.1 Unterprogrammaufruf 7.1.1 Aktueller Parameter 7.1.2 Parameteraufruf über die Adresse 7.1.3 Beispiel 7.1.4 Parameteraufruf über den Wert 7.1.5 Parameteraufruf über den Namen 7.1.6 Parameterbehandlung bei ALGOL 60 7.2 Unterprogrammdeklarationen 7.2.1 Sprachumfang 7.2.2 Rückkehradresse 7.2.3 Spezielle Probleme 7.2.4 Speicherverwaltung 7.2.5 Speicherverwaltung bei rekursiven Prozeduren 8. Formale Sprachen 8.1 Grundbegriffe 8.1.0 Backus-Naur-Form 8.1.1 Beispiel 8.1.2 Ableitbarkeit 8.1.3 Abgekürzte Notation der Beispiele 8.1.4 Abstrakte Beispiele 8.1.5 Syntaxbaum 8.1.6 Mehrdeutige Produktionensysteme
199 199 199 201 202 204 205 206 208 208 208 209 210 211 213 213 213 213 214 215 215 216 218
12
Inhalt 8.2 Chomsky-Sprachen 8.2.1 Konkatenation von Wörtern 8.2.2 Operationen mit Wortmengen 8.2.3 Iteration 8.2.4 Semi-Thue-Systeme 8.2.5 Chomsky-Grammatik
219 219 219 220 221 221
8.3 Chomsky-Hierarchie 8.3.1 Notwendigkeit von Einschränkungen 8.3.2 Kontextsensitive und monotone Grammatiken 8.3.3 Kontextfreie Grammatiken 8.3.4 Lineare Grammatiken
222 222 223 223 224
8.4 Kontextfreie Grammatiken 8.4.1 Kontextunabhängigkeit der Ableitungen 8.4.2 Kontextfreie Grammatiken mit Löschung 8.4.3 Reduzierte Grammatiken 8.4.4 Mengentheoretische Operationen
225 225 225 226 228
8.5 Zwei-Stufen-Grammatiken 8.5.1 Attribut-Grammatiken 8.5.2 Beispiel 8.5.3 Darstellung kontextsensitiver Sprachen 8.5.4 Grammatiken mit Kontrollmenge 8.5.5 Beispiel
229 229 230 231 232 233
9. Modelle zur Rekognition 9.1 Endliche Automaten 9.1.1 Definition des endlichen Automaten 9.1.2 Beispiel 9.1.3 Nichtdeterministische Automaten 9.2 Kellerautomaten 9.2.1 9.2.2 9.2.3 9.2.4 9.2.5
Definition des Kellerautomaten Angenommene Wortmenge Beispiele: Annahme durch leeren Keller Beispiele: Annahme durch markierten Zustand Weitere Resultate
9.3 Rekognitionsstrategien 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.3.6 9.3.7
Rekognitionsverfahren Kanonische und antikanonische Ableitungen Bottom-up- und Top-down-Strategie Beschreibung der Top-down-Strategie Beschreibung der Bottom-up-Strategie Sackgassen Left-corner-Strategie
10. Rekognitionsverfahren 10.1 Vorrangverfahren 10.1.1 Operatorenvorrang 10.1.2 Allgemeine Vorrangrelationen 10.1.3 Beschreibung des Vorrangverfahrens 10.1.4 Beispiel 10.1.5 Weitere Resultate
234 234 234 235 236 236 236 238 238 240 241 241 241 242 243 244 246 248 249
252 252 252 253 254 256 259
Inhalt
13
10.2 Verfahren mit beschränktem Kontext 10.2.1 (m,n)-beschränkter Kontext 10.2.2 Beispiel 10.2.3 Prüfung der Beschränktheitsbedingung 10.2.4 (1,1 )-LR-beschränkter Kontext 10.2.5 Beispiel 10.2.6 LR(k)-Grammatiken
259 259 260 261 262 263 264
10.3 Verfahren des rekursiven Abstiegs 10.3.1 PM-Befehle 10.3.2 Beispiel 10.3.3 Konstruktion von PM-Programmen 10.3.4 Back-up-freie PM-Programme 10.3.5 Beseitigung linksrekursiver Symbole
264 265 266 267 268 270
Literatur
272
Namen- und Sachverzeichnis
281
0. Einführung
Einer elektronischen Rechenanlage gegenüber muß man sich einerseits klar und unmißverständlich ausdrücken, andererseits will man dabei eine dem Arbeitsgebiet angepaßte Terminologie benützen. Das Ergebnis dieses Widerstreites ist eine Vielzahl verschiedener Programmiersprachen. In erster Linie werden sie als Verständigungsmittel zwischen Programmierer und Rechenanlage konzipiert. Je mehr sie sich jedoch der Terminologie eines bestimmten Anwendungsgebietes nähern, desto besser eignen sie sich zur Dokumentation von Algorithmen, das heißt als Verständigungsmittel zwischen verschiedenen Programmierern, beispielsweise zwischen Autor und Lesern dieses Buches. Jedes Sprachelement, das einem Anwendungsgebiet entnommen ist, bedeutet in der Regel einen Schritt hinweg von den Gegebenheiten der realen Rechenanlagen. Dieser Prozeß, der dem Programmierer entgegenkommt, muß vor der Ausführung eines Programmes wieder umgekehrt werden, das heißt: das in einer „problemorientierten" Programmiersprache geschriebene Programm muß in eine „maschinenorientierte" Form überfuhrt werden. Schon Rutishauser hat festgestellt [Rutishauser 1952], daß die Rechenanlage fähig ist, diesen Übersetzungsvorgang selbst durchzufuhren. Ein Programm für diese Aufgabe nennen wir Compiler. Im vorliegenden Buch sollen einige der Methoden auf möglichst elementarer Ebene dargestellt werden, die in der Zwischenzeit zur Lösung dieser Aufgaben entwickelt wurden.
0.1 Programmiersprachen 0.1.1 Begriff Wir können nach ihrer Orientierung drei Typen von Programmiersprachen unterscheiden: Maschinensprachen, maschinenorientierte Sprachen, problemorientierte Sprachen. Diese Begriffe wollen wir im folgenden näher erläutern und dabei DIN 44300 zugrunde legen. Unter einer Programmiersprache ist eine zum Abfassen von Programmen geschaffene Sprache zu verstehen. Sprache ist in diesem Zusammenhang ein System von endlich vielen Symbolen und endlich vielen Regeln, wobei die Regeln
16
0. Einführung
in irgendeiner Weise festlegen, welche Symbolfolge ein „richtiges" Programm bildet und welche nicht 1 . Die Tatsache, daß die Regeln fest vorgegeben sind, bedeutet nicht, daß damit die sprachlichen Ausdrucksmöglichkeiten auf einen mehr oder weniger kleinen Bereich eingeengt sein müssen. Es können vielmehr Regeln in der Sprache enthalten sein, die die Menge der Symbole und die Ausdrucksmöglichkeiten in gewissem Rahmen zu erweitern gestatten. 0.1.2 Maschinensprachen Maschinensprachen sind Programmiersprachen, die zur Abfassung von Arbeitsvorschriften nur Anweisungen zulassen, die unmittelbar Befehlswörter einer digitalen Rechenanlage sind. Es handelt sich hierbei ohne Zweifel um Programmiersprachen, deren Handhabung sehr unbequem ist, weil der Programmierer nicht nur die Maschinenstruktur, sondern auch die interne Darstellung aller Informationen beherrschen muß. Die Unhandlichkeit rührt nicht zuletzt daher, daß maschinenintern die Informationen binär dargestellt werden, wobei zur externen Notation nur das Oktal- und das Sedezimalsystem zur Verfügung stehen 2 . Die Verwendung von Dezimalzahlen erfordert bereits einen echten Konvertierungsprozeß (siehe Nr. 4.1), während Oktal- oder Sedezimalzahlen durch Shiftbefehle in die interne Darstellung gebracht werden können. Maschinensprachen finden heute nur noch in besonderen Fällen Verwendung: Einmal werden in ihnen alle diejenigen Programme notiert, die wegen ihrer Bedeutung oder häufigen Benutzung hardwaremäßig verwirklicht werden; hierzu gehören etwa die sogenannten Urprogramme, die den Start des Systems bewirken, gelegentlich auch Programme für den Verkehr mit peripheren Einheiten. Zum anderen werden von Assemblern (und manchmal auch von Compilern) die Programme bis auf gewisse externe Bezüge und Adressentranslationen direkt in die Maschinensprache übersetzt. 0.1.3 Maschinenorientierte Programmiersprachen Die Anweisungen maschinenorientierter Programmiersprachen weisen die gleiche oder eine ähnliche Struktur auf wie die Befehlswörter der zugrunde liegenden Rechenanlage. Sie unterscheiden sich von den Maschinensprachen durch: die Verwendung von Dezimalzahlen, mnemotechnische Bezeichnungen für die Operationen, die Verwendung symbolischer Adressen, die Verwendung von Makrobefehlen. 1 Wir haben „richtig" in Anführungsstriche gesetzt, weil ein Programm, das im Sinne der Programmiersprache richtig ist, im Hinblick auf die zu lösende Aufgabenstellung durchaus noch falsch sein kann. (Man denke etwa an die Vertauschung von + und -.) 2 Vgl. [Güntsch, Schneider 1972, Kap. 3]
0.1
Programmiersprachen
17
Wie bei den Maschinensprachen ist auch hier die Bedeutung, das heißt die Wirkung, einer Anweisung vom übrigen Programm unabhängig. Zu den Unterschieden noch einige Hinweise: Mnemotechnische Bezeichnung der Operationen heißt, daß beispielsweise eine Addition mit A oder ADD gekennzeichnet wird (statt mit der maschinenorientierten Codierung, wie etwa 42). Die Speicherplätze im Zentralspeicher einer Rechenanlage sind fortlaufend numeriert, so daß die Variablen oder Konstanten unter der Nummer (Adresse) ihres Speicherplatzes angesprochen werden können. In einer maschinenorientierten Sprache ist es möglich, den gewünschten Speicherplatz mit einem beliebigen Identifikator zu kennzeichnen, auf den man sich dann in den Befehlswörtern beziehen kann, z.B. A XI statt 42 1A84 3 . Unter Makrobefehlen versteht man Anweisungen der maschinenorientierten Sprache, die sich maschinenintern nicht durch einen Befehl realisieren lassen. Sie stehen insbesondere für Dienstleistungen zur Verfügung, etwa zur Bereitstellung des nächsten Wortes eines Eingabebereichs oder zur Anforderung bzw. Freigabe eines reservierten Speicherbereichs. Besonders günstig ist die Möglichkeit, selbst neue Makrobefehle definieren zu können. 0.1.4 Problemorientierte Programmiersprachen Problemorientierte Programmiersprachen dienen dazu, Programme aus einem bestimmten Anwendungsbereich unabhängig von einer bestimmten Rechenanlage abzufassen, und lehnen sich an eine in dem betreffenden Bereich übliche Schreiboder Sprechweise an. Aus diesem Grunde richten sich die zulässigen Sprachelemente und Konstruktionen mehr nach dem Anwendungsbereich, für den die Sprache eingesetzt werden soll, als nach den Rechenanlagen, die die Programme ausführen müssen. Daher rührt einerseits ihre besondere Eignung als Dokumentationssprachen für Algorithmen, andererseits aber auch der Zwang zu einem komplizierten Übersetzungsvorgang, dem Kompilieren*, der das Programm in eine der Rechenanlage verständliche, maschineninterne Form überführt, oder zur schrittweisen Interpretation. Einem Sprachelement eines problemorientierten Programms können — im Gegensatz zu den maschinenorientierten Programmen — verschiedene Folgen von Maschinenbefehlen entsprechen, da seine Bedeutung von Angaben an anderer Stelle des Programms abhängen kann. (Vgl. Nr. 0.1.5)
3 Eine maschineninterne Darstellung, die die Ziffern 0, 1 , . . ., 9 und die Buchstaben A, B , . . . , F benutzt, heißt sedezimal (oder hexadezimal). 4 engl.: to compile = zusammentragen, sammeln. Die ersten Compiler übersetzten nur verhältnismäßig einfache Formeln. Ihre Hauptaufgabe bestand darin, verschiedene Programme und Unterprogramme zu einem Ganzen zusammenzufassen.
0. Einfuhrung
18
Aus der großen Zahl problemorientierter Programmiersprachen können wir hier nur einige angeben: technisch-wissenschaftliche Probleme: ALGOL, F O R T R A N , P L / 1 , BASIC, J O S S , kaufmännische Probleme: COBOL, P L / 1 , Systemprogrammierung: P L 3 6 0 , B L I S S , P S 4 4 0 , BCPL, SAL, A L G O L W, PASCAL, linguistische Probleme: COM IT, Simulierung: SIMCOM, SIMSCRIPT, SIMULA, Symbolmanipulation: SNOBOL, FORMAC, Matrixoperationen u.a.: APL, Listenverarbeitung: LISP, SLIP, Realzeitaufgaben: J O V I A L , Werkzeugmaschinensteuerung: APT. Eine Übersicht über die neueren Entwicklungstendenzen der problemorientierten Sprachen gibt [Cheatham 1971], Bei [Ledgard 1 9 7 1 ] findet der Leser eine systematische Einführung in die Sprachelemente, die man heute standardmäßig in problemorientierten Programmiersprachen erwartet.
0.1.5 Einfluß des Kontextes bei problemorientierten Programmiersprachen Daß in einem problemorientiert geschriebenen Programm die Bedeutung eines Symbols von anderen Programmstellen abhängen kann, sei an zwei Beispielen aus A L G O L 6 0 und P L / 1 erläutert. Wir betrachten in einem ALGOL-Programm . . . else M: . . . Aus diesem Programmstück ist nicht zu ersehen, ob M eine Marke oder eine Variable bezeichnet. Dies kann erst durch Betrachten eines größeren Kontextes geklärt werden: eine Marke ist es etwa im Fall: if l¥=o then X := 3 * K / I else M: X := MAX(I,K) dagegen eine Variable im Fall: array A[if K M then I else M : M] In einem PL/ 1-Programm ist aus dem Programmstück . . . X = A(I)... nicht zu entnehmen, ob A eine Funktion ist, bei deren Aufruf unter anderem I oder eine weitere, im Aufruf nicht auftretende Variable verändert werden kann,
0.2 Kompilation und Interpretation
19
oder ob es sich um ein Feld handelt, so daß der Index durch diese Anweisung unverändert bleibt. Zur Klärung dieser Frage muß auf die Vereinbarungen (Deklarationen) zurückgegriffen werden, die an beliebiger Stelle des Programms stehen können.
0.2 Kompilation und Interpretation 0.2.1 Unterschied von Kompilation und Interpretation Wie wir bereits in Nr. 0.1.4 festgestellt haben, kann die Rechenanlage ein in einer problemorientierten Form geschriebenes Programm nicht unmittelbar ausführen. Die einzelnen Anweisungen sind vielmehr zunächst in die Maschinensprache der Rechenanlage zu übertragen. Beachten wir, daß ein Programm aus einer Folge von Einzelaktionen (Anweisungen oder Teilen davon) zusammengesetzt ist, so ergeben sich verschiedene Möglichkeiten der Vorgehensweise, von denen aus systematischen Gründen die beiden Extremfälle am interessantesten sind: (a)
Alle Aktionen werden zuerst übersetzt. Ist der Übersetzungsvorgang beendet, so werden alle Aktionen in der nun vorliegenden Form (Maschinenprogramm) ausgeführt. In diesem Fall sprechen wir von Kompilation. Nur diese wollen wir im vorliegenden Buch ausführlich behandeln.
(b)
Eine Aktion wird übersetzt und unmittelbar ausgeführt; sodann wird die nächste Aktion übersetzt und ausgeführt usw. Hier sprechen wir von Interpretation. Die Maschinenprogrammfassung der einzelnen Aktionen wird nicht aufbewahrt und ist wegen der gegenseitigen Verklammerung der einzelnen Aktionen überhaupt nicht vollständig erzeugbar.
0.2.2 Kompilation Wir betrachten etwas näher den Fall der Kompilation: Der Compiler, der selbst ein Maschinenprogramm ist, erwartet als Eingabe Text, der in einer bestimmten problemorientierten Sprache geschrieben ist. Er verarbeitet diesen Text (Quellenprogramm) und erzeugt seinerseits als Ausgabe maschinenorientierte (oder unmittelbar Maschinen-) Anweisungen, die denselben Algorithmus beschreiben wie der ursprüngliche Text 5 . Das so entstandene Maschinenprogramm kann anschließend seinerseits auf der Rechenanlage ausgeführt werden. Wir betrachten ein Beispiel aus ALGOL 60: if A>B then S := S+3*B eise Z := Z+1 s
Ob zwei Programme übereinstimmen, ist nicht durch ein allgemeines Verfahren entscheidbar, so daß es kein Compiler-Prüfprogramm geben kann.
20
0. Einfühlung
Der Compiler kann hierfür etwa folgendes maschinenorientierte 6 Programm erzeugen:
L: M:
H1 := A-B; ( H K o ) jump L; H2 := 3*B; S := S+H2; jump M; Z := Z+1; ...
Es sei in diesem Zusammenhang besonders vermerkt, daß der Test „H upb PROGRAM then int N = upb PROGRAM; [0:1] instruct X; X[0:N] := PROGRAM; for J from N + l to I do X[J] := nil; PROGRAM := X fi nil bedeutet, daß dieses Feld leer bleibt, und ist von 0 zu unterscheiden.
1.2 Definition des Sprachumlangs
35
1.1.5 Compiler-Erstellung Heute kann man etwa vier Methoden unterscheiden, einen Compiler zu erstellen: (a) (b) (c)
(d)
Der Compiler wird vollständig von Hand programmiert. Die zu übersetzende Programmiersprache wird formal definiert und der Übersetzungsalgorithmus hieraus formal abgeleitet. Die Programmiersprache wird formal definiert, die Übersetzung einzelner Sprachstrukturen von Hand programmiert, ihr gegenseitiger Aufruf jedoch aus der formalen Sprachdefinition abgeleitet. Man verfugt über ein (mehr oder weniger formales) System, in dem sowohl die Sprachdefinition als auch die Übersetzung einzelner Sprachelemente beschrieben werden kann, und ein Programm, das die Anweisungen dieses Systems interpretieren oder übersetzen kann.
Selbstverständlich hängt in den Fällen (b), (c) und (d) der resultierende Übersetzer wesentlich davon ab, welche formalen Systeme verwandt werden. Bei der Auswahl der Vorgehensweise ist besonders zu berücksichtigen, welche Nachteile in Kauf zu nehmen man bereit ist und auf welche Vorteile man Wert legt. Maßgebende Kriterien können sein: 1. Ist eine effektive Ausnutzung der Maschinengegebenheiten möglich? Dies kann bei den Methoden (a) und (c) bejaht werden. 2. Sind Änderungen leicht möglich? Dies ist eher bei (b), (c) und (d) der Fall, (a) scheidet hier völlig aus. 3. Kann der Compiler leicht von einer Rechenanlage auf eine andere übertragen werden? (d) und (b) kommen dieser Forderung entgegen, (d) läßt insbesondere zu, daß der Übersetzer auf einer fremden Rechenanlage erstellt wird; dies ist oft bei Kleinrechnern zweckmäßig und bei noch in der Entwicklung befindlichen Rechnern notwendig. Im weiteren Verlauf dieses Kapitels werden wir typische Vertreter dieser vier Methoden am Beispiel der arithmetischen Wertzuweisung erläutern.
1.2 Definition des Sprachumfangs 1.2.1 Syntax und Semantik Bevor wir das erste Übersetzungsverfahren angeben, müssen wir zunächst einmal den Sprachumfang präzise festlegen, der übersetzt werden soll. Eine Angabe wie „arithmetische Wertzuweisung" genügt hier nicht, weil dadurch die genaue Form der Anweisung noch nicht beschrieben ist. Auch eine umgangssprachliche Definition ist nicht zu empfehlen, weil dabei erfahrungsgemäß etwas ausgefallenere Konstruktionen nicht eindeutig erfaßt werden.
36
1. Problemstellung und grundlegende Übersetzungstechniken
Zunächst wollen wir uns auf die syntaktische Definition beschränken. Unter Syntax wollen wir die Vorschriften über den korrekten Aufbau der Programme verstehen. Sie legt fest, in welcher Weise Sprachel^mente aus anderen Sprachelementen zusammengesetzt sind, in welcher Reihenfolge die Konstituenten auftreten und welche Trennsymbole erforderlich sind. Unter Semantik wollen wir die Vorschriften über die Bedeutung der Programme verstehen. Sie regelt die Bedeutung einzelner oder zusammengesetzter Sprachelemente und die Bedeutungsunterschiede, die sich aus dem Zusammenhang ergeben, in dem ein Sprachelement steht. Sie erklärt auch die Bedeutung einzelner Operationszeichen. (Etwa + bedeutet die Addition ganzer Zahlen.) 1.2.2 Zur Definition der Syntax Allen Naturwissenschaftlern ist bekannt, daß viele Mißverständnisse zwischen Autor und Leser vermieden werden können, wenn man Text durch Formeln ersetzt. Man hat daher schon früh begonnen, auch die Beschreibung der Programmiersprachen zu formalisieren, wobei sich insbesondere der Bereich der Syntax anbietet [Backus 1959]. Dennoch bereitete auch die vollständige Formalisierung der Syntax Schwierigkeiten. Sowohl bei der Definition von ALGOL 60 als auch bei der Definition von ALGOL 68 wurde manche zur Syntax gehörende Vorschrift mit Worten wiedergegeben, so etwa das Verbot, innerhalb eines Blockes eine Bezeichnung in verschiedener Bedeutung zu verwenden. Die Formalisierung der Semantik begann sich erst viel später zu entwickeln. Man findet hierzu eine Reihe von Arbeiten in den Sammelbänden [Steel 1966] und [Engeler 1971]. Ein besonders erfolgversprechender Ansatz ist die Wiener Definitionssprache [Lucas, Walk 1969]. Einen Überblick gibt [Lukas 1971], Dort findet der Leser weitere Literaturhinweise. Überwiegend umgangssprachlich ist FORTRAN beschrieben. Zur Definition von PL/1 wurden teilweise formale Hilfsmittel benützt, die aus den bei COBOL gebräuchlichen abgeleitet sind. Eine formale Definition wurde nachträglich angegeben [Lucas, Walk 1969]. Den bisher nachhaltigsten Einfluß auf die Entwicklung der formalen Sprachdefinition hatten die Programmiersprachen der ALGOLFamilie, die sich der Hilfsmittel der kombinatorischen Systeme bedienen: Grundlage war die Backus-Naur-Form (BNF) [Backus 1959], die den aus der Theorie der formalen Sprachen bekannten kontextfreien Grammatiken entspricht. (Eine Einführung in diese Theorie bietet [Maurer 1969].) Der größte Teil der Syntax von ALGOL 60 ist mit diesem Hilfsmittel definiert, wobei jedoch semantische Mehrdeutigkeiten verbleiben [Knuth 1967] 7 . 7 Die Syntax von ALGOL 68 wird durch eine mehrstufige Erweiterung der BNF beschrieben, die an Smullyans formale Systeme erinnert [Smullyan 1961]. Durch diese Erweiterung war es möglich, viele Aspekte in die Formalisierung einzubeziehen, die bei ALGOL 60 durch Worte beschrieben wurden. Ein formales System zur Beschreibung der ALGOL 68-Syntax sind die Affix-Grammatiken [Köster 1971].
1.2 Definition des Sprachumfangs
37
1.2.3 Metasprache nach van Wijngaarden Wir wollen festlegen, daß eine arithmetische Wertzuweisung besteht aus einer Variablen als Ziel, einem Trennzeichen (Zuweisungszeichen), einem arithmetischen Ausdruck als Quelle, wobei diese Reihenfolge verbindlich ist. Eine derartige Festlegung kann in folgender Form geschehen: assignation: destination, becomes symbol, source. Ein Doppelpunkt trennt den zu definierenden Begriff von der Definition; besteht sie aus mehr als einer Komponenten, so werden diese durch Kommata voneinander getrennt. Ein Punkt schließt die Definition ab. Soweit nicht durch den Zusatz „symbol" gekennzeichnet ist, daß es sich bei diesem Begriff um ein spezielles Symbol handelt, das keiner weiteren Definition mehr bedarf, müssen alle in der Definition auftretenden Begriffe ihrerseits definiert werden: destination:
variable.
Bei der Definition der Quelle wollen wir — entsprechend dem Beispiel aus Nr. 1.1.1 — zulassen, daß nicht nur ein Ausdruck, sondern eine weitere Wertzuweisung auftreten darf. Derartige Alternativen werden durch ein Semikolon voneinander getrennt: source: assignation; expression. Wir haben damit alle vorerst benötigten Hilfsmittel zusammengestellt. Das Definitionsschema ist eine Metasprache, in der wir terminale Begriffe, die hier durch „. . . symbol" gekennzeichnet sind und keiner weiteren Definition bedürfen, und nichtterminale Begriffe, die ihrerseits definiert werden müssen, wohl zu unterscheiden haben.
1.2.4 Definition der arithmetischen Wertzuweisung Wir können nun die vorerst als Beispiel dienende Sprache der arithmetischen Wertzuweisungen, an der wir verschiedene Übersetzungsverfahren demonstrieren wollen, exakt definieren: assignation: destination, becomes symbol, source. destination: variable. source: assignation; expression. expression: term; expression, adding operator, term. adding operator: plus symbol; minus symbol.
38
1. Problemstellung und grundlegende Übersetzungstechniken
term: factor; term, multiplying operator, factor. multiplying operator: times symbol; divided by symbol. factor: primary; factor, up symbol, primary. primary: variable; number; open symbol, expression, close symbol. variable: letter. number: digit; number, digit. Wir ersparen es uns, den Begriff „letter" durch 26 Regeln der Form letter: letter a symbol; letter b symbol;. .. näher zu definieren. Ebenso ist „digit" nicht näher definiert. Weiterhin ist es im Augenblick nicht erforderlich, allgemeinere Formen von Variablen zu betrachten 1.2.5 Darstellung einzelner Symbole Die Definition unserer Beispielsprache ist vollständig, wenn wir die Darstellung der einzelnen Symbole angeben. Diese Darstellung hängt selbstverständlich vom Zeichenvorrat des verfügbaren Eingabegerätes (Locher, Datenstation mit Sichtgerät, Fernschreibmaschine) ab, so daß es ratsam ist, alternative Darstellungen zuzulassen. Zu unserer Beispielsprache betrachten wir die folgenden Möglichkeiten: Symbol becomes symbol plus symbol minus symbol times symbol divided by symbol up symbol open symbol close symbol
Darstellung + -
X *
/
t **
( )
1.3 Sequentielle Formelübersetzung 1.3.1 Einführung des Kellerungsprinzips Wenn wir versuchen, eine Wertzuweisung wie die aus Nr. 1.1.1 von links nach rechts zu übersetzen, und dabei einen bestimmten Operator erreicht haben, so hängt die Entscheidung, ob wir als nächstes die Maschinenbefehle für diesen Operator erzeugen dürfen, auch von dem folgenden Operator ab. Bindet der folgende nämlich enger als der betrachtete, so kann dieser noch nicht übersetzt
1.3 Sequentielle Formelübersetzung
39
werden, weil sein zweiter Operand die Auswertung des nachfolgenden Operators erfordert: c + d* . . . Wir müssen dann den noch nicht übersetzbaren Operator (hier: +) Zwischenspeichern und zunächst versuchen, die Maschinenbefehle zur Berechnung des zweiten Operanden zu generieren. Erst wenn der Teilausdruck d*e/(f-g>
übersetzt ist, kehren wir zu dem gespeicherten Operator zurück. Wegen der runden Klammern wiederholt sich dieser Vorgang bei der Übersetzung des Teilausdrucks, so daß weitere Operatoren gespeichert werden müssen. Diese müssen jedoch bereits weiterverarbeitet sein, wenn wir zu dem zuerst gespeicherten Operator zurückkehren. Der benutzte Zwischenspeicher arbeitet also nach dem LIFOPrinzip (last in — first out). Wir sprechen dann von einem Kellerspeicher (pushdown-store), dessen Charakteristikum es ist, daß nur das zuletzt abgelegte Element betrachtet wird. Da neben den Operatoren auch die Klammern und andere syntaktische Symbole für die Reihenfolge der Abarbeitung eine Rolle spielen, ist es besser, statt von Operatoren allgemeiner von Begrenzern (delimiter) zu sprechen. 1.3.2 Die Fälle in einer Übergangsmatrix Ist eine Wertzuweisung bis zu einem bestimmten Begrenzer übersetzt worden, so kann man die möglichen Situationen in folgenden fünf Fällen zusammenfassen [Samelson, Bauer 1959]. 1. Die Operanden zu dem zuletzt gekellerten Operator sind beide übersetzt. Die Übersetzung dieses Operators kann erfolgen. 2. Der zweite Operand zu dem zuletzt gekellerten Operator ist noch unvollständig. Also müssen zunächst weiter rechts stehende Operatoren übersetzt werden. 3. Der Inhalt eines Klammerpaares ist übersetzt. Beide Klammern können entfernt werden. Dabei ist zu beachten, daß in unserem Algorithmus die öffnende Klammer im Keller stand, die schließende jedoch noch nicht. 4. Die Übersetzung der Anweisung ist beendet. 5. Die vorliegende Aufeinanderfolge der Begrenzer ist unzulässig. Der Fall, daß als folgender Begrenzer eine öffnende Klammer auftritt, muß nicht getrennt behandelt werden, weil Fall 2 dies abdeckt. Daher müßte oben besser von Begrenzern gesprochen werden. Welcher der fünf Fälle jeweils vorliegt, kann aus einer Übergangsmatrix (transition matrix) entnommen werden; dabei ist die Zeile entsprechend dem Inhalt des letzten Platzes im Kellerspeicher, die Spalte entsprechend dem nachfolgenden Begrenzer zu wählen:
40
1. Problemstellung und grundlegende Übersetzungstechniken
Anfang :=
+ -
*
/ t
(
:=
+
-
*
/
t
(
)
Schlußzeichen
2 2 5 5 5 5 5 5
5 2 1 1 1 1 1 2
5 2 1 1 1 1 1 2
5 2 2 2 1 1 1 2
5 2 2 2 1 1 1 2
5 2 2 2 2 2 1 2
5 2 2 2 2 2 2 2
5 5 1 1 1 1 1 3
4 1 1 1 1 1 1 5
Als Anfangs- und Schlußzeichen können wir das Semikolon verwenden. Eine eigene Zeile für die schließende Klammer haben wir nicht vorgesehen, weil die schließende Klammer nicht im Keller auftreten kann. Selbstverständlich können Zeilen oder Spalten gleichen Inhalts eingespart werden. Die einzelnen Matrixelemente lassen sich leicht auffinden, wenn man die maschineninterne Codierung der Begrenzer günstig wählt: Pos. 3
Pos. 2
Pos. 1
Pos. 1 enthalte die Nummer der zugehörigen Spalte, Pos. 2 die der zugehörigen Zeile. (Pos. 3 verbleibt für zusätzliche Angaben.) Ist die Übergangsmatrix zeilenweise gespeichert und die Zahl der Spalten eine Zweierpotenz, was durch Anfügen leerer Spalten erreicht werden kann, so schneide man Pos. 2 aus der Codierung des zuletzt gekellerten Begrenzers und Pos. 1 aus der des folgenden Begrenzers heraus. Setzt man beide zusammen, so erhält man die laufende Nummer des gesuchten Matrixelementes. (Die Numerierung muß bei 0 beginnen.) 1.3.3 Die Übersetzung mittels Übergangsmatrix Den Übersetzungsalgorithmus haben wir durch ein Ablaufdiagramm beschrieben. Parallel zu dem Keller der noch nicht weiterverarbeiteten Begrenzungszeichen (DELSTACK) wird ein Keller der noch nicht weiterverarbeiteten Operanden (OPSTACK) benötigt. Obwohl bei der Übersetzung eines binären Operators zwei Operanden aus diesem Keller entfernt werden, wird sein Zähler Q nur um 1 erniedrigt, weil die Ausführung der jetzt generierten Befehle einen neuen Operanden erzeugen wird. Daher ist ÜB Operandenkeller ein Hinweis auf dieses Resultat einzusetzen. Hierfür wird die bisher vom linken Operanden benötigte Position Q-l benutzt. In diesem Zusammenhang ist eine Bemerkung besonders wichtig: Im Operandenkeller sind nicht die Operanden selbst abgelegt (nur im Fall von Konstanten wäre
1.3 Sequentielle Formelübersetzung
41
dies möglich), da diese erst zur Laufzeit des Programms, nicht aber zur Übersetzungszeit, bestimmt werden können. Stattdessen enthält der Operandenkeller andere Hinweise, die den jeweiligen Operanden eindeutig charakterisieren: Adressen oder laufende Nummern in einer Liste. Zwei Stellen des Ablaufdiagramms verweisen auf spätere Kapitel: Die Übersetzung eines Operators stellt uns die Frage nach der Darstellung der einzelnen Sprachelemente in der Maschinensprache, die Bereitstellung des nächsten Symbols setzt die lexikalische Analyse voraus. 1.3.4 Die Ausgänge der Fallunterscheidung Der bereits betrachteten Fallunterscheidung entsprechen dann die folgenden Aktionen: 1. Da beide Operanden vorliegen, wir aber andererseits jeden Teilausdruck zum frühest möglichen Zeitpunkt übersetzen, sind diese Operanden durch die beiden letzten Elemente des Operandenkellers gegeben. Demnach kann die Befehlsfolge generiert werden, die zur Ausführung des in DELSTACK[P] stehenden Operators mit den in OPSTACK[Q-l] und OPSTACK[Q] bezeichneten Operanden nötig ist. Für diesen Vorgang schaffen wir uns eine eigene Routine GENERATE, die im 2. Kapitel näher zu erläutern sein wird. Der so übersetzte Teilausdruck kann rechter Operand zu einem weiteren gekellerten Operator sein, so daß noch nicht zum nächsten Symbol übergegangen werden kann. 2. Das laufende Symbol ist ein Operator, dessen rechter Operand noch nicht abgearbeitet ist. Der Operator wird im Keller der Begrenzungszeichen abgelegt. Dieser Fall kann ohne Änderung auch zum Ablegen einer öffnenden Klammer verwandt werden. 3. Ein Klammerpaar ist überflüssig geworden; der Zähler P des Kellers der Begrenzungszeichen wird erniedrigt (Streichen der öffnenden Klammer). Die schließende Klammer wird durch den Übergang zum nächsten Symbol entfernt. 4. Da der Zweig 1 auch für die Wertzuweisungen verwandt wird, verbleibt im Operandenkeller der Hinweis auf die zuletzt verarbeitete linke Seite bei Beendigung der Übersetzung einer Anweisung. Dieser muß abschließend entfernt werden. 5. Die Aufeinanderfolge der Begrenzungszeichen läßt auf einen syntaktischen Fehler schließen. Eine Möglichkeit weiterzuübersetzen besteht darin, das nächste Semikolon zu suchen und mit der nächsten Anweisung zu beginnen oder eine Fehlerkorrektur anzubringen, falls die Art des Fehlers offensichtlich ist.
42
1. Problemstellung und grundlegende Übersetzungstechniken
Abb. zu 1.3.3. Übersetzung arithmetischer Wertzuweisungen mit einer Übergangsmatrix und 2 Kellern
43
1.3 Sequentielle Formelübersetzung
1.3.5 Die Übersetzung des Beispiels Wir verfolgen ausführlich die Übersetzung des Beispiels aus Nr. 1.1.1, wobei wir nur die Situationen protokollieren, die beim Durchlaufen der zentralen Verzweigung bestehen: OPSTACK 1
2 3 4
DELSTACK 5 6
7
1
2 3
4
C U RR 5
6
7
SYMB
Fall
b
=
:= :=
2
a a
b c
=
=
+
2
a
b c
d
=
=
+
*
2
a
b c
d
=
=
+
a
b c
Z1
=
=
+
a
b c
Z1
=
=
+
a
b c
Z1 f
=
=
+
a
b c
Z1 f
=
=
+
a
b c
Z1 22
=
=
+
a
b c
Z1
=
=
+
a
b c
Z
=
=
+
a
b
=
=
a
b
=
=
a
b
=
=
a
b
=
a
e
3
h
g
*
/ / ( / ( / ( /
+
a
Generierte Übersetzung
2
/ /
2
1
(
2
-
2
) )
3
+
1
+
1
+
2
1
1 1 1
Z1 := d * e
z 2 :=f-g ¿ 3 := ZT/ZJ 24 := c+z 3 25 := z 4 + h b = 25 a := b
4
Im Fall 1 haben wir jeweils den Teilausdruck in der letzten Spalte notiert, der in dieser Situation übersetzt werden kann.
1.3.6 Zusammenfassung der Keller Der aufmerksame Leser hat bemerkt, daß das Verfahren in dieser Form nicht prüft, ob die Operanden innerhalb der Anweisung an den richtigen Stellen stehen. (Man teste etwa das Verfahren mit der fehlerhaften Symbolfolge a := b+c(*d-e).) Eine derartige Prüfung läßt sich im oberen Teil des Ablaufdiagramms einbauen. Eine einfachere und übersichtlichere Lösung ergibt sich jedoch durch Zusammenfassen der beiden Keller OPSTACK und DELSTACK zu einem einzigen Keller [siehe z.B. Gries 1968]. Die Kontrolle kann dann von der Routine GENERATE übernommen werden: zu diesem Zeitpunkt müssen sich die erforderlichen Operanden links und rechts von dem zu übersetzenden Operator im Keller befinden 8 . * Zu früheren Zeitpunkten können sich auf diesen Positionen beispielsweise noch Klammern befinden.
1. Problemstellung und grundlegende Übersetzungstechniken
44
Wir wollen in Nr. 1.3.7 den Algorithmus in dieser Form notieren. In dem zusammengefaßten Keller legen wir Elemente verschiedener Arten ab, wobei uns auch verschiedenartige Informationen interessieren: Wir können etwa Vereinbarungen der Gestalt struct operand = (address A D D R , . ..), struct delimiter = (string INDICATION, int LINE, C O L U M N
)
voraussetzen. Dabei deuten Pünktchen an, daß wir über die weiterhin erforderlichen Informationen bisher nichts gesagt haben. Bei den Begrenzern geben wir neben ihrer Bezeichnung 9 die jeweilige Zeilen- und Spaltennummer in der Übergangsmatrix an. 1.3.7 P r o g r a m m z u m Übersetzungsverfahren Mit den Deklarationen: union element = (operand, delimiter); int Q; element C U R R S Y M B ; delimiter LAST D E L I M I T E R ; [1:0 flex] element STACK; int P:=0; und den den Keller verwaltenden Prozeduren PUSHDOWN: P := P+1; STACK[P] := C U R R S Y M B 1 0 und POPUP: if P > 1 then P := P-1 else E R R O R fi und B R A C K E T DELETION: if P > 1 then STACK [P-1 ] := STACK[P]; P := P-1 else E R R O R fi; erhalten wir folgenden Algorithmus: begin
C U R R S Y M B :=(„:", 1, 9); PUSHDOWN; GOON: C U R R S Y M B := N E X T S Y M B O L ; if operand :: C U R R S Y M B 1 1 then PUSHDOWN; goto GOON fi;
9 10 11
Zur Verbesserung der Lesbarkeit sei diese mitgefiihrt, obwohl dies nicht erforderlich ist. Vgl. Fußnote 5. Der Test liefert den Wert true, wenn CURRSYMB einen Wert der Art Operand besitzt.
1.4 Vorrangverfahien
REP:
45
Q := P; while operand :: STACK [Q] do Q : = Q-1; LAST D E L I M I T E R ::= STACK[Q]; case T R A N S M A T [ L I N E of LAST D E L I M I T E R , C O L U M N of CURRSYMB] in begin GENERATE(Q); goto REP end, begin PUSHDOWN; goto GOON end, begin B R A C K E T DELETION; goto GOON end, POPUP, ERROR esac12
end. TRANSMAT soll die Werte der Übergangsmatrix enthalten, wie sie in 1.3.2 beschrieben ist. Wir gehen ferner davon aus, daß die Routine GENERATE, der wir die Kontrolle der Operandenzahl überlassen haben, dann auch den Keller auf den neuesten Stand bringt.
1.4 Vorrangverfahren 1.4.1 Ausnutzung des Operatorenvorrangs Das in 1.3 beschriebene Verfahren läßt sich leicht auf Programmiersprachen üblicher Größenordnung erweitern. Dabei wird die Übergangsmatrix jedoch nicht nur durch Vergrößerung der Zeilen- und Spaltenzahl umfangreicher, sondern auch dadurch, daß die Zahl der erforderlichen Fallunterscheidungen erheblich steigt. Man kann die Zahl der Fallunterscheidungen aber auch bei Erweiterungen der Programmiersprache durch eine Systematisierung des Verfahrens konstant halten, wenn man das von den arithmetischen Ausdrücken her bekannte Konzept des Operatorenvörrangs auf weitere Programmstrukturen verallgemeinert. Es gibt dann nur vier Möglichkeiten beim Vergleich zweier benachbarter Begrenzer 1 3 : (a) (b) (c) (d)
Der linke (im Keller befindliche) Begrenzer ist gegenüber dem rechten (gerade betrachteten) vorrangig: > Gegenüber dem linken hat der rechte Begrenzer Vorrang: < Die beiden Begrenzer sind gleichrangig: Die beiden Begrenzer dürfen in dieser Reihenfolge nicht aufeinander folgen.
Bei der Aufstellung der Vorrangmatrix fällt auf, daß die Berücksichtigung anderer Begrenzer als der arithmetischen Operatoren zu einer Abhängigkeit von der 12 case i in . . . , . . . , . . . , . . . , . . . esac bezeichnet eine Fallunterscheidung, bei der die i-te der aufgezählten (evtl. zusammengesetzten) Anweisungen ausgeführt wird. 13 Der Unterschied ist beim vorliegenden, einführenden Beispiel nicht eklatant.
46
1. Problemstellung und grundlegende Übersetzungstechniken
Reihenfolge führt. Unter den arithmetischen Operatoren besitzt das Multiplikationssymbol stets Vorrang gegenüber dem Additionssymbol, wobei die Reihenfolge keine Rolle spielt. Dagegen ist eine öffnende Klammer gegenüber anderen Operatoren nur vorrangig, wenn sie rechts von ihnen steht: Im Beispiel a/(b/c) besitzt die öffnende Klammer Vorrang gegenüber der ersten Division, ist aber der zweiten untergeordnet. 1.4.2 Vorrangalgorithmus zur Übersetzung von Wertzuweisungen Greifen wir das Beispiel von Nr. 1.3.2 auf, so ergibt sich folgende Vorrangmatrix 1 4 := *
:=
+ -
*
/ t
(
<
> > > >
> > > >
> >
> >
> > >
> > > > > >
=
=
Die leeren Felder bedeuten die unzulässigen Übergänge. Wie man sieht, kann man mit der Vorrangmatrix auch festlegen, daß bei zwei aufeinanderfolgenden Additionszeichen das erste, bei zwei aufeinanderfolgenden Ergibtzeichen dagegen das zweite zuerst auszuwerten ist. Das Ablaufdiagramm ist parallel dem von Nr. 1.3.3 aufgebaut. Eine Besonderheit entsteht dadurch, daß die Fertigstellung einer Anweisung nicht mehr durch einen eigenen Zweig erkannt wird, sondern im gleichen Zweig erfolgt wie die Abarbeitung der Klammern. Die Anweisung ist vollständig übersetzt, wenn in diesem Zweig der Keller der Begrenzungszeichen leer geworden ist 1 5 . Der Fall der gleichrangigen Begrenzer erfordert beim weiteren Ausbau der Programmiersprache noch einige Ergänzungen. Betrachten wir in diesem Zusammenhang die eckigen Klammern: wenn der von ihnen eingeschlossene Indexausdruck übersetzt ist, darf man die Klammern nicht ersatzlos streichen; wir werden später sehen, daß vielmehr weitere Maschinenbefehle zu generieren sind, die ein Ansprechen des gewünschten Feldelementes ermöglichen.
14
Es gibt Symbole, die aufgrund des Verfahrens nicht im Keller abgelegt werden (z.B. die schließende Klammer) und daher in keiner Zeile erscheinen, und andererseits Symbole, die erst während des Übersetzungsvorgangs erzeugt werden und daher in keiner Spalte erscheinen. 15 Im rechten Zweig kann er nicht leer werden, da die Matrix in der Zeile für das Semikolon kein >-Zeichen enthält.
1.4 Vorrangverfahien
47
Abb. zu 1.4.2. Übersetzung arithmetischer Wertzuweisungen mit einer Operatorenvorrangmatrix und 2 Kellern
48
1. Problemstellung und grundlegende Übersetzungstechniken
1.4.3 Weiterführende Vorrangkonzepte Die Vorrangmatrix erlaubt uns, den Speicherbedarf für jedes Matrixelement auf zwei Binärstellen zu beschränken, so daß sich der Speicheraufwand in einem vertretbaren Rahmen hält 1 6 . Zwei Gründe können jedoch Schwierigkeiten bereiten: einmal kann bei wortorientierten Rechenanlagen der Zugriff zu den Binärstellen im Innern eines Wortes umständlich, d.h. langsam sein; zum andern ist eine Matrix unhandlich, wenn die Definition zusätzlicher Operatoren (allgemeiner noch: zusätzlicher Begrenzer) im Programm erlaubt wird [Yoneda 1971; Suzuki 1971], Oft lassen sich jedoch die Vorrangbeziehungen durch zwei Funktionen ausdrücken, die jedem Symbol zwei natürliche Zahlen zuordnen: s
;
left(s) right(s)
1 1
:= 2 3
+
-
#
/
t
(
)
4 3
4 3
6 5
6 5
8 7
1 9
1 1
left bezieht sich auf den Fall, daß das Symbol im Keller steht, right auf den Fall, daß es sich um das gerade betrachtete Symbol handelt. Vorrang besitzt dasjenige Symbol, dem auf seiner Position die größere Zahl zugeordnet ist. Sei beispielsweise der letzte Begrenzer im Keller ein Additions-, das folgende Symbol ein Multiplikationszeichen, so ist left(+) = 4
right(*) = 5
Demnach hat in dieser Situation das Multiplikationszeichen Vorrang. Ist s der letzte Begrenzer im Keller und ist s' das gerade betrachtete Symbol, so ist s < s ' => left(s) < right(s') s = s' => left(s) = right(s') s > s ' => left(s) > right(s') Man beachte, daß die Umkehrung nicht gilt, da jetzt für jedes Symbolpaar eine Beziehung besteht. Das Verfahren der Vorrangfunktionen besitzt gegenüber der Verwendung der Vorrangmatrix Vor- und Nachteile. Zu den Vorteilen gehört der geringere Speicherbedarf: enthält die Programmiersprache n Begrenzungszeichen, so sind nur 2n Speicherplätze erforderlich 1 7 . Zu den Nachteilen gehört, daß syntaktische
16
Im Falle von ALGOL 60 benötigt man etwa eine Matrix von 90 X 30 Elementen, so daß sich ein Speicherbedarf von 675 Bytes bzw. 169 Maschinenwörtern (zu je 32 Binärstellen) ergibt. Eine andere Möglichkeit, mit zwei Binärstellen pro Matrixelement auszukommen, ist in [Schneider 1969] angegeben. 17 Im Beispiel von Fußnote 16 ergeben sich 180 Bytes.
1.5 Verfahren des rekursiven Abstiegs
49
Fehler schlechter zu erkennen sind: folgt auf ein Ergibtzeichen eine schließende Klammer, so wird dies noch nicht unmittelbar als Fehler erkannt; vielmehr hat wegen 2 > 1 das Ergibtzeichen Vorrang. Erst nach einigen weiteren Schritten gerät die Abarbeitung in eine Sackgasse. 1.4.4 Konstruktion von Vorrangfunktionen Ist die Vorrangmatrix gegeben, so lassen sich die Vorrangfunktionen — falls es solche gibt — nach folgendem Verfahren bestimmen: 1. Man gebe einen beliebigen Wert für alle left(s) und right(s) vor, z.B. 1. 2. Man prüfe der Reihe nach für jedes Element PM(s,s. ) der Vorrangmatrix, ob die geforderte Relation zwischen left(s) und right(s') erfüllt ist. Wenn nein, erhöhe man den zu kleinen Wert left(s) oder right(s') auf die kleinste natürliche Zahl, mit der die Relation erfüllt ist. Beispiel: Hat man anfangs für alle s left(s) := right(s) := 1
gesetzt, so ist left(semicolon) < right(becomes)
nicht erfüllt; man ändert dann left(becomes) := 2.
3. Wurde in Schritt 2. ein Funktionswert verändert, so wird der Schritt 2. wiederholt. Wurde in Schritt 2. kein Wert mehr verändert, hat man ein Paar von Vorrangfunktionen gefunden. 4. Das Verfahren muß als erfolglos abgebrochen werden, wenn ein left(s) oder ein right(s) - ausgehend vom Wert 1 - den Wert 2n überschreitet, wobei n die Anzahl der betrachteten Symbole ist. (In diesem Fall liegt ein Zyklus vor.)
1.5 Verfahren des rekursiven Abstiegs 1.5.1 Grundgedanke Die Frage, ob ein Compiler logisch richtig ist, ist allgemein nicht entscheidbar und auch im Einzelfall sehr schwierig nachzuweisen. Ist die Programmiersprache schon formal definiert, so wird diese Überprüfung erheblich vereinfacht, wenn die Struktur des Compilers der der formalen Definition genau entspricht. Ein bewährtes Verfahren zur Erreichung dieses Zieles ist die Methode des rekursiven Abstieges: Jedem nichtterminalen Symbol wird eine Routine zugeordnet, die der
50
1. Problemstellung und grundlegende Übersetzungstechniken
Reihe nach die rechten Seiten seiner Definition überprüft und die für die Übersetzung erforderlichen Konsequenzen zieht. Dabei werden insbesondere die Routinen flir weitere nichtterminale Symbole aufgerufen. Ist beispielsweise definiert (gegenüber 1.2.4 vereinfacht): assignation: variable, becomes symbol,
expression.
so wird die Routine A S S I G N A T I O N zunächst die Routine V A R I A B L E aufrufen, nach deren (erfolgreicher) Beendigung das dann folgende Symbol daraufhin überprüfen, ob es sich um ein Ergibtzeichen handelt, und schließlich die Routine EXPRESSION aufrufen. Da die nichtterminalen Symbole rekursiv verwendet werden können, können sich diese Routinen mittelbar oder unmittelbar selbst aufrufen. 1.5.2 Erkennung von Primärausdrücken Wir betrachten zunächst die Routine zur Erkennung eines Primärausdruckes (PRIMARY). Gemäß den Produktionen von Nr. 1.2.4 sind dabei die folgenden Alternativen zu berücksichtigen: primary: variable; number; open symbol, expression, close
symbol.
Daraus läßt sich unmittelbar folgende Routine ableiten: proc P R I M A R Y =: begin if C U R R S Y M B = OPEN S Y M B O L then C U R R S Y M B := N E X T S Y M B O L ; EXPRESSION; if C U R R S Y M B CLOSE S Y M B O L then ERROR fi; C U R R S Y M B := N E X T S Y M B O L eise if LETTER (CURRSYMB) then V A R I A B L E eise if D I G I T ( C U R R S Y M B ) then NUMBER eise E R R O R fi fi fi end Dabei setzen wir voraus, daß C U R R S Y M B global geeignet deklariert ist, N E X T S Y M B O L das nächste Symbol aus dem Eingabebereich entnimmt und LETTER bzw. DIGIT zwei boolesche Prozeduren sind, die eine entsprechende Klassifizierung der Symbole erlauben. V A R I A B L E und NUMBER sind einschlägige Routinen, die Variablen bzw. Zahlen erkennen und diese in einem Keller STACK ablegen. Wir werden im folgenden bemerken, daß STACK bei diesem Verfahren nur Operanden enthält und daher einfacher deklariert werden kann als in Nr. 1.3.7 1 8 . 1 ' Durch den gegenseitigen Aufruf der Routinen wird der Keller der Begrenzungszeichen implizit im Rahmen der Verschachtelung der Gültigkeitsbereiche lokal vereinbarter Variablen verwirklicht.
51
1.5 Verfahren des rekursiven Abstiegs
1.5.3 Erkennung von Faktoren, Termen und Ausdrücken Auf der nächsthöheren Ebene haben wir eine Routine FACTOR anzugeben. Die Produktionen factor: primary; factor, up symbol,
primary.
erfordern eine Umformung, weil beide Alternativen letzten Endes mit einem Primärausdruck beginnen, weil aus factor ebenfalls ein Primärausdruck ableitbar ist: proc FACTOR =: begin P R I M A R Y ; while C U R R S Y M B = UP S Y M B O L do begin C U R R S Y M B := N E X T S Y M B O L ; PRIMARY; GENERATEfUP SYMBOL); comment using STACK[LAST-1] and STACK [LAST] as operands. End of comment end end Die Routine G E N E R A T E ist entsprechend der in Nr. 1.3.7 erwähnten zu verstehen. Sie kellert insbesondere in STACK [ LAST-1] einen Hinweis auf das sich ergebende Resultat. Eine wörtliche Übernahme der Routine aus Nr. 1.3.7 ist jedoch nicht möglich, da wir beim vorliegenden Verfahren die zu generierende Operation explizit angeben müssen und uns nicht auf den Keller beziehen können, in dem sich keine Begrenzer befinden 1 9 . Ganz analog sind die Routinen TERM und EXPRESSION aufgebaut: proc TERM =: begin FACTOR; while C U R R S Y M B = T I M E S S Y M B O L or C U R R S Y M B = D I V I D E D BY S Y M B O L do begin delimiter Z ::= C U R R S Y M B ; C U R R S Y M B := NEXTSYMBOL; FACTOR; GENERATE(Z) end end; 15 Wir werden im Laufe des Buches noch an vielen Stellen bemerken, daß die den verschiedenen Verfahren gemeinsamen Routinen zur Keller- und Listenbearbeitung jeweils den Gegebenheiten des Verfahrens angepaßt werden müssen. Wir verwenden trotz großer Bedenken die gleiche Bezeichnung, um deutlich zu machen, daß die analoge Leistung erbracht wird.
52
1. Problemstellung und grundlegende Übersetzungstechniken
proc EXPRESSION =: begin TERM; while CURRSYMB = PLUS SYMBOL or CURRSYMB = MINUS SYMBOL do begin delimiter Z := CURRSYMB; CURRSYMB := NEXTSYMBOL; TERM; GENERATE(Z) end end; Die Einführung der zusätzlichen Größe Z wird erforderlich, weil CURRSYMB durch den zweiten Aufruf von FACTOR bzw. TERM bereits auf den nachfolgenden Begrenzer eingestellt wird. (Vgl. Fußnote 18.) 1.5.4 Erkennung der Wertzuweisung Die Produktionen assignation: destination, becomes symbol, source, destination: variable lassen sich zusammenfassen zu: assignation: variable, becomes symbol, source. Dementsprechend haben wir: proc ASSIGNATION =: begin VARIABLE; if CURRSYMB = BECOMES SYMBOL then CURRSYMB := NEXTSYMBOL; SOURCE else ERROR fi; POPUP end. Die letzte Anweisung ist entsprechend Zweig 4 von Nr. 1.3.3 erforderlich. Die Generierung der eigentlichen Wertzuweisung haben wir der Routine SOURCE überlassen, bei der eine Schwierigkeit dadurch eintritt, daß bei ihrem Aufruf unklar ist, ob eine weitere Zielvariable folgt oder bereits der abschließende Ausdruck: 2 0
20 Eine Vereinfachung ergibt sich, wenn man - wie bei ALGOL 68 - zuläßt, daß eingeklammerte Wertzuweisungen als Primärausdrücke und andererseits Ausdrücke als selbständige „Anweisungen" auftreten dürfen.
1.5 Verfahren des rekursiven Abstiegs
53
proc SOURCE =: begin EXPRESSION; if C U R R S Y M B = BECOMES S Y M B O L then if not R E F E R E N C E (MODE of STACK[LAST]) then ERROR fi; C U R R S Y M B := N E X T S Y M B O L ; SOURCE fi; GENE RATE (BECOMES SYMBOL) end Wir haben uns dadurch geholfen, daß wir die Routine für den umfangreicheren Fall (EXPRESSION) aufrufen und bei der Rückkehr fragen, ob der spezielle Fall vorliegt. Wir müssen entsprechend der Abfrage die Deklaration von operand (vgl. Nr. 1.3.6) erweitern: struct operand = (address A D D R , opmode M O D E , . . .) Als Modi sehen wir die Basismodi int (nur dieser tritt vorerst im Beispiel auf), real, bool, die Konstante bzw. Zwischenergebnisse bezeichnen, und die abgeleiteten Modi ref int, ref real, ref bool für die entsprechenden Variablen vor. Die boolesche Prozedur R E F E R E N C E testet, ob einer der letzten vorliegt. 21
1.5.5 Compiler-Compiler Betrachtet man die Parallelität zwischen den Produktionen von Nr. 1.2.4 und den Prozeduren von Nr. 1.5.2 bis 1.5.4, so ist der Gedanke naheliegend, den Übersetzungsvorgang, der aus den Produktionen die Prozeduren generiert, zu automatisieren, das heißt: wir wünschen uns ein Programm, dem wir die Produktionen als Daten übergeben und dessen Ausgabe ein entsprechender Satz Prozeduren ist. Ein solches Programm nennen wir Compiler-Compiler. Soll dieser maschinenunabhängig sein, so müssen möglichst alle von uns in den Prozeduren eingesetzten Aktionen bereits in den Produktionen formuliert werden. Dazu gehören auch die Prozedurparameter und lokalen Variablen. Wir betrachten ein vereinfachtes Beispiel: assignation: variable, becomes symbol, source, source - mode: expression + mode, (becomes symbol, testmode + mode + ref, source; generate + becomes), expression + mode -m-z: term + mode, rep: adding operator + z, term + m, generate + z, make + mode + unref, : rep. adding operator + z: plus symbol, make + z + plus; minus symbol, make + z + minus. 21
Außerdem gehören zu den abgeleiteten Modi die indizierten Variablen.
1. Problemstellung und grundlegende Übersetzungstechniken
54
Dieses Stück eines Compilers ist in der Kosterschen „Compiler-descriptionlanguage" (CDL) geschrieben [Koster 1972] und entspricht genau den in den vorhergehenden Abschnitten definierten Routinen ASSIGNATION, S O U R C E und EXPRESSION. In der Routine SOURCE wird als lokale Variable M O D E eingeführt, der durch EXPRESSION, wo sie als Parameter auftritt, ein Wert zugewiesen wird. Dieser kann in der Routine TESTMODE überprüft werden. Wir müssen diesen Weg gehen, da wir keinen unmittelbaren Zugriff zum Keller haben: auch der Operandenkeller wird nun implizit durch den gegenseitigen Aufruf der Routinen realisiert. Nicht nur hinter Symbolen wie testmode oder becomes symbol verbergen sich boolesche Prozeduren. Da eine Alternative abgebrochen werden soll, wenn einer ihrer Bestandteile nicht aufgefunden werden kann, sind auch ASSIGNATION, SOURCE, EXPRESSION, TERM usw. in boolesche Prozeduren zu überführen: die von uns mit ERROR gekennzeichneten Ausgänge liefern den Wert false. (Dagegen bezeichnen die Symbole generate und make Aktionen, hinter denen sich Routinen verbergen, die keinen Funktionswert liefern.) In der CDL bedeutet also jedes Symbol eine Routine. Ihre formalen bzw. aktuellen Parameter werden durch Pluszeichen, ihre lokalen Variablen durch Minuszeichen angehängt. In dem von uns angegebenen Beispiel werden außerdem noch Rücksprünge: Marke: . . . :Marke und überspringbare Teilstücke benutzt, die in runde Klammern eingeschlossen werden.
1.6 Die Produktionssprache von Floyd und Evans 1.6.1 Grundgedanke Vergleichen wir die beiden Algorithmen von Nr. 1.3.3 und Nr. 1.5.5, so fällt folgender Unterschied auf: im ersten Algorithmus mußten wir alle Einzelheiten von Hand programmieren; der Algorithmus enthielt aber keine rekursiven Elemente. Für den letzten Algorithmus stand ein Programmiersystem zur Verfügung, mit dessen Hilfe der Compiler in Analogie zur formalen Sprach de finition leicht zu formulieren war: dafür enthielt der so erzeugte Compiler — entsprechend der Sprachdefinition - eine Vielzahl rekursiver Prozeduren, die seine Geschwindigkeit und den erforderlichen Speicherplatz beeinflussen. Eine dazwischen liegende Lösung würde in der Formalisierung des ersten Verfahrens bestehen. Die Anweisungen eines derartigen Programmiersystems müßten ebenfalls aus der formalen Sprachdefinition ableitbar sein, wobei wegen der Vermeidung rekursiver Prozeduren notwendigerweise größere Umformungen erforderlich sind als bei der Verwen-
1.6 Die Produktionssprache von Floyd und Evans
55
dung der Kosterschen CDL. Ferner muß das Programmiersystem über alle organisatorischen Maßnahmen des ersten Verfahrens (vgl. 1.3.3 und 1.3.7) verfugen: (a) (b) (c) (d)
(e)
die Übernahme eines weiteren Symbols aus der Folge der Eingabezeichen in den bereits betrachteten Teil der Zeichenkette (Keller), das Entfernen von Symbolen aus dem Keller, das Ersetzen von Symbolen des Kellers durch neue Symbole, den Vergleich, ob im Keller eine bestimmte Folge von Symbolen vorliegt, da hiervon — entsprechend den Fallunterscheidungen von Nr. 1.3.2 — die auszuführenden Aktionen abhängen, eine Sprunganweisung, die gestattet, von der linearen Folge der Anweisungen abzuweichen.
1.6.2 Floyd-Evans-Befehle Ein derartiges Programmiersystem ist Floyds Produktionssprache [Floyd 1961], die von Evans modifiziert und erstmals bei der Erstellung eines Compilers eingesetzt wurde [Evans 1964; Feldman 1964]. Jede Anweisung der Produktionssprache ist ein 6-tupel, bestehend aus: 1. einer Marke, die es ermöglicht, diese Anweisung anzuspringen, oder einem leeren Feld, 2. einer Symbolfolge, die mit den letzten Symbolen des Kellers (einschließlich des gerade betrachteten Symbols) übereinstimmen muß, wenn die weiteren Teile der Anweisung ausgeführt werden sollen, 3. einer Symbolfolge, die die unter 2. genannte im Keller ersetzt, wobei eine leere Symbolfolge das Streichen der unter 2. genannten bedeutet 2 2 , 4. dem Namen eines Programmstückes, das die Übersetzung bewirkt (Semantik-Routine), 5. einem Stern, der nicht mit dem Multiplikationszeichen verwechselt werden darf, wenn ein weiteres Symbol der Eingabefolge übernommen werden soll, bzw. einem leeren Feld im anderen Fall, 6. einer Marke, die die als nächstes auszuführende Anweisung bezeichnet, bzw. einem leeren Feld, wenn in der hingeschriebenen Reihenfolge fortgesetzt werden soll. In der hingeschriebenen Reihenfolge wird auch dann fortgesetzt, wenn die im 2. Feld angegebene Symbolfolge nicht angetroffen wird; ferner kann das im 4. Feld aufgerufene Programm einen Ausgang besitzen, so daß die reguläre Fortsetzung über das 6. Feld entfällt. Dies ist etwa bei Fehlerroutinen der Fall.
" Die meisten Veröffentlichungen hierzu interpretieren die leere Zeichenkette auf Position 3 so, daß dann keine Änderung des Kellerinhalts eintritt. Aus systematischen Gründen (vgl. Kap. 9) ist die hier angegebene Definition günstiger.
56
1. Problemstellung und grundlegende Übersetzungstechniken
Im Gegensatz zu unseren früheren Verfahren wird hier vorausgesetzt, daß sich auch das gerade betrachtete Symbol (CURRSYMB) bereits im Keller befindet. Dies erleichtert die Schreibweise der Floyd-Evans-Programme; bei getrenntem Ausfuhren wäre eine weitere Spalte erforderlich. Bei der Implementierung kann es jedoch u.U. günstiger sein, das gerade betrachtete Symbol noch nicht im Keller abzulegen. Beim Vergleich mit 2. entspricht ihm dann das jeweils letzte dort aufgeführte Symbol.
Abb. zu 1.6.2. Interpretation eines Floyd-Evans-Befehls
In der Abbildung ist die Verarbeitung einer derartigen PL-Anweisung als Ablaufdiagramm angegeben. Von der ursprünglichen Definition weichen wir insofern ab, als wir die Semantik-Routine vor der durch das 3. Feld definierten Ersetzung
57
1.6 Die Produktionssprache von Floyd und Evans
aufrufen, so daß sie auf den bisherigen Kellerinhalt zurückgreifen kann 2 3 . Dem Diagramm liegt eine PL-Anweisung der Form (Ml, X j X 2 ...Xn,Y1Y2...Ym,
SR, c, M2)
zugrunde. Dabei bezeichne Ml die Marke der Anweisung. X2X2 • • • Xn müssen mit den letzten n Symbolen des Kellers übereinstimmen, damit die Anweisung anwendbar ist: Xn mit dem letzten, X n.j mit dem vorletzten usw. Ist die Anweisung anwendbar, so wird die Semantikroutine SR durchlaufen, und im Keller die Symbolfolge X j X 2 • . • Xn durch Y1Y2.. . Ym ersetzt: Ym ist nun das letzte Kellersymbol. Ist c nicht leer, so wird das nächste Symbol der zu untersuchenden Zeichenkette ebenfalls in den Keller übernommen. Im Diagramm sei der Keller durch SjS2 . .. Sp gegeben. Als Begrenzungszeichen am Anfang und Ende einer Zeichenkette dienen i— und—i. 1.6.3 Beispiel Wir wollen als Beispiel einen Ausschnitt aus einem PL-Programm zur Bearbeitung der Wertzuweisungen gemäß Nr. 1.2.4 angeben. Wir betrachten die Produktionen mit expression(E) als linker Seite: 2 4 T* T / EAOT + E AO T EAOT ) T+ TT)
T* T / E+ EE) E+ EE)
* *
GENERATE GENERATE GENERATE
ERROR Beide Produktionen enden auf T (term). Bei der Entscheidung, ob sie überhaupt anwendbar sind, ist das nächste Symbol heranzuziehen: im Fall der beiden ersten Zeilen kann das Symbol „term" noch nicht zu „expression" reduziert werden, weil aus dem folgenden Zeichen hervorgeht, daß zunächst dort zu reduzieren ist. Die Reduktion von „term" zu „expression" ist nur in den Fällen der Zeilen 6—8 möglich. Bevor jedoch auf einen'dieser Fälle geschlossen werden darf, ist zu 23
Die ursprüngliche Definition geht davon aus, daß die für die Semantik-Routinen wesentlichen Informationen in einem zweiten Keller gespeichert sind. Dieser läßt sich jedoch einsparen, wenn man zusätzliche Informationen analog Nr. 1.5.5 in die PL einbaut. 24 Es sei noch einmal darauf hingewiesen, daß der Stern in der 5. Spalte nicht mit dem Multiplikationszeichen verwechselt werden darf. Eigentlich hätte man ein druckbares Zeichen verwenden müssen, das in keiner Programmiersprache auftritt.
58
1. Problemstellung und grundlegende Übersetzungstechniken
testen, ob die „längere" Produktion vorliegt. Daher spielt die Reihenfolge, in der diese Anweisungen durchlaufen werden, eine große Rolle. Waren alle Vergleiche erfolglos, so liegt ein Fehler vor. Nehmen wir beispielsweise an, daß sich im Keller die Symbolfolge v := (T + befinde. Wir verwenden eine gegenüber 1.2.4 abgekürzte Notation.) Dann sind die beiden ersten PL-Anweisungen nicht anwendbar, weil diese als letztes Kellersymbol ein Multiplikations- bzw. Divisionszeichen verlangen. Bei der dritten und vierten PL-Anweisung stimmen zwar jeweils die beiden letzten der geforderten Symbole mit dem vorgegebenen Kellerinhalt überein, nicht jedoch das drittletzte. Bei der fünften weicht wieder das letzte ab; erst bei der sechsten PL-Anweisung haben wir Erfolg. Der neue Kellerinhalt lautet: v := (E +
1.6.4 Abkürzungen Die Parallelität einiger dieser Zeilen legt den Gedanken nahe, Abkürzungen für besondere Symbolgruppen einzuführen. (Wenn man davon ausgeht, daß die Reduktionen soweit links wie möglich durchgeführt werden, ist das letzte Symbol im Keller, d.h. das zuletzt eingelesene, stets terminal.) In unserem Beispiel bieten sich in Anlehnung an die entsprechenden nichtterminalen Symbole an: 'AO'={+,-> 'MO' = { * , / } 'ANY' = Menge aller Zeichen Damit läßt sich das Beispiel folgendermaßen kürzen, wobei jedoch die Erkennung einiger Fehler um einige Schritte hinausgezögert wird:
T 'MO' E AO T 'ANY' T 'ANY' 'ANY'
T 'MO' E 'ANY' E 'ANY'
«
GENERATE ERROR
Es ist genau zu unterscheiden zwischen dem Symbol AO und der Symbolgruppenbezeichnung 'AO'. Übereinstimmende Symbolgruppenbezeichnungen in der 2. und 3. Spalte bedeuten, daß in beiden Spalten der gleiche Repräsentant einzusetzen ist.
59
1.6 Die Produktionssprache von Floyd und Evans
1.6.5 Übersetzung der Wertzuweisungen mit einem FE-Programm Für die in Nr. 1.2.4 definierte Sprache geben wir in der Tabelle ein FE-Programm an, wobei G E N B I N und G E N A S S die Übersetzungsroutinen für binäre Operatoren und Wertzuweisungen sind (Aufteilung des früher benutzten G E N E R A T E ) . Zur Vereinfachung betrachten wir v (variable) und n (number) als terminale Symbole. Tabelle zu 1.6.5. FE-Programm zur Übersetzung arithmetischer Wertzuweisungen.
I—
START
I—
'ANY' L1
V
#
n
n
*
(
(
PRIM
FACT
TERM
EXPR
SRCE ASSG
v := n := v 'ANY' n 'ANY' 'ANY'
D := P 'ANY' P 'ANY'
L1 PRIM PRIM
ERROR3
Ft T MO F ' A N Y ' F 'ANY' 'ANY'
Ft T 'ANY' T 'ANY'
T * T/ E AO T 'ANY' T 'ANY' 'ANY'
T MO T MO E 'ANY' E 'ANY'
E + E (E ) E'ANY'
E AO E AO P S
'ANY'
*
ERROR2
F 'ANY' F 'ANY'
1—A—i
*
L2 L2 L1
ERROR1
FtP ' A N Y ' P 'ANY' 'ANY'
D := S —i D := A — i
L1
ERRORO
V
'ANY' L2
*
FACT FACT
GENBIN ERROR4 *
GENBIN
L1 TERM TERM
ERROR4 * *
GENBIN
L1 L1 EXPR EXPR
ERROR4 * * *
LI L1 PRIM SRCE
ERROR5 A —1 A —1
GENASS GENASS STOP ERROR6
ASSG
60
1. Problemstellung und grundlegende Übersetzungstechniken
Die in 1.6.3 betrachteten Anweisungen finden wir unter der Marke TERM. Der erfolgreiche Ausgang führt auf die Marke EXPR. Dort ist bereits die erste PL-Anweisung anwendbar: da die 5. Spalte nicht leer ist, wird neben den durch die 3. Spalte beschriebenen Kellerveränderungen noch das nächste Zeichen in den Keller übernommen: v := ( E AO n Danach wird bei L1 fortgesetzt. Der Leser vervollständige selbst das Beispiel! Die Fehlerroutine ERROR6 enthält insbesondere den Fall, daß links von einem Ergibtzeichen arithmetische Operatoren oder Klammern auftreten. Beim Einsetzen der Fehlerroutinen waren wir sehr vorsichtig, so sind E R R O R 3 und ERROR4 logisch überhaupt nicht erreichbar, da die zuvor durchlaufenen Anweisungen derartige Konstellationen im Keller nicht hinterlassen. Die Erfahrung zeigt jedoch, daß bei der Veränderung von Compilern (z.B. der Erweiterung) oft Fehler gemacht werden, so daß in solchen Fällen diese Ausgänge doch auftreten könnten.
2. Grundlegende Techniken auf der maschinenorientierten Seite
2.1 Kellerung der Zwischenergebnisse 2.1.1 Zerlegung einer Anweisung Bei der Erläuterung des Übersetzungsvorganges (Nr. 1.3.5) haben wir eine Übersetzung generiert, die überflüssige Zwischenspeicherplätze benützt. Wir betrachten als Beispiel j : = a : = k - b*c/((d*e + f*g)/(h*i)). Solange wir die Übersetzungsalgorithmen ohne besondere Vorkehrungen anwenden, können wir nicht feststellen, ob Teilausdrücke übereinstimmen; wenn der gleiche Teilausdruck an verschiedenen Stellen einer Wertzuweisung auftritt, wird er in allen Fällen unabhängig übersetzt und bei Ausfuhrung des Programms entsprechend o f t berechnet. 1 Diese Strategie bewirkt aber auch, daß jedes Zwischenergebnis genau einmal weiterverarbeitet wird. Im Anschluß daran steht der für dieses Zwischenergebnis benötigte Speicherplatz für weitere, erst dann auszuwertende Zwischenergebnisse wieder zur Verfügung. Nutzen wir diese Kenntnisse aus, so erhalten wir für unser Beispiel die folgende Übersetzung: Z
1 2 Z 3 z 2 z 3 z 2 z
= b*c = d*e = f*g = z2+z3 = h*i
= z2 /z3 = z,/z2 Z 1 = k-Z, a = Z1 j := a Weitere Einsparungen lassen sich durch Änderung der Reihenfolge bei der Auswertung der Operationen erzielen. Hierauf wollen wir erst in Nr. 5.1.4 eingehen. 2.1.2 Zwischenergebniskeller Verfolgt man diese Beispiele genauer, so erkennt man, daß auch hier — wie schon bei der Übersetzung (vgl. Nr. 1.5) — das Kellerungsprinzip vorliegt: Wird ein neues Zwischenergebnis berechnet, erhöht sich der Index; bei den Verknüpfungen 1
Daher haben wir alle Operanden im Beispiel verschieden bezeichnet.
62
2. Grundlegende Techniken auf der maschinenorientierten Seite
werden die Zwischenergebnisse mit den höchsten Indizes (evtl. nur eines) verwendet. Gegenüber den bisher betrachteten Symbolkellern (Operanden-, Begrenzerkeller) besteht jedoch ein wesentlicher Unterschied: Die Symbolkeller werden während des Übersetzungsvorgangs benötigt, d.h. der Compiler legt dort Elemente ab, auf die er später zurückgreifen will. Dagegen wird Speicherplatz für den Zwischenergebniskeller erst während des Programmlaufes benötigt, d.h. nicht der Compiler, sondern das Programm legt dort Elemente ab 2 . Der Compiler muß sich jedoch während des Übersetzungsvorgangs mit der Verwaltung des Kellers beschäftigen und insbesondere feststellen: (a) (b) (c)
wieviele Speicherplätze der Zwischenergebniskeller maximal benötigt 3 , in welcher Reihenfolge die Zwischenergebnisse dort während des Programmlaufs gespeichert sein werden, unter welcher Adresse folglich welches Ergebnis gekellert sein wird; diese Angabe ist nur dann relevant, wenn die Adressen bei der Übersetzung explizit (bzw. bis auf „konstante" Verschiebung) in die Befehle eingesetzt werden müssen.
Da insbesondere (b) und (c) für die Routine GENERATE (vgl. Nr. 1.3.3 bzw. 1.3.7) relevant sind, die die einzelnen maschinenorientierten Anweisungen erzeugt, wird man ihr zweckmäßigerweise diese Aufgaben übertragen. 2.1.3 Maschinenbefehle Bei der Übersetzung problemorientierter Programmiersprachen können wir davon ausgehen, daß sich die Operanden im Hauptspeicher der Rechenanlage befinden. Vor Ausführen einer Rechenoperation müssen daher die beteiligten Operanden in das Rechenwerk und nach ihrer Ausführung das Ergebnis wieder zurücktransportiert werden. Wie wir gesehen haben, sind oft Zwischenergebnisse zu berechnen, die wenig später weiterverarbeitet werden. Für die Übersetzung ist daher wesentlich, ob im Rechenwerk nur ein Ergebnisregister (Akkumulator) zur Verfügung steht oder mehrere, in denen dann auch Zwischenergebnisse aufbewahrt werden können. Die Verwaltung der Zwischenspeicherplätze hängt also wesentlich von der Struktur der jeweiligen Rechenanlage, ihrer Maschinenbefehle und von den verfügbaren Rechenwerksregistern ab. Ist nur ein Akkumulator vorhanden, so wird zusätzlich vorausgesetzt, daß sich jeweils ein Operand einer binären Operation bereits dort befindet, so daß nur die Adresse des anderen anzugeben ist: Ein-Adreß-Maschine. Die Befehle haben also die Form: struct instruct = (string OPT, address OPD). 2
Bei diesen Elementen handelt es sich nämlich um Zahlenwerte oder andere Objekte, die das Programm erst berechnen wird und die der Compiler noch gar nicht kennen kann. 3 Dies ist nur für jedes einzelne Programm oder Unterprogramm möglich, ohne daß dabei der Bedarf der von diesem seinerseits noch aufgerufenen Unterprogramm berücksichtigt wird.
2.1 Kellerung der Zwischenergebnisse
63
Liegen mehrere Akkumulatoren vor, so muß außer der Adresse des zweiten Operanden angegeben werden, in welchem Akkumulator sich der erste Operand befindet. Er kann dann nach der Operation das Ergebnis aufnehmen. Damit ist diese erste Adresse auf die vorhandenen Akkumulatoren oder Register beschränkt, während die zweite beliebig ist 4 : struct instruct = (string OPT, register OP1, address OP2). Dabei lassen wir als Objekte der Art register nur ACO, A C 1 , . . ., ACn zu und unterteilen die Objekte der Art address gemäß: union address = (register, memory address) Die Objekte der Art memory address seien durch den Adressenraum des Hauptspeichers gegeben und werden in Nr. 2.3.3 näher erläutert. 5
2.1.4 Beispiel (Ein-Adreß-Version) Wir notieren das Beispiel aus Nr. 2.1.1 in der Ein-Adreß-Version, wobei wir die Anweisungen aus Nr. 2.1.1, die sich ergebenden Befehle und eine lesbarere, aber redundantere Form dieser Befehle angeben: o
z
:= b*c
(„:=" ,
AC
= B
AC
=
AC*C
(„=:" , Z 1 )
Z1
=
AC
(..:=" ,
AC
= D
AC
=
AC*E
(„=:" , Z 2 )
Z2
=
AC
(.,:=" ,
AC
= F
AC
=
(„+", Z 2 )
AC
=AC+Z2
(„='•" , Z 2 )
Z2
=
AC
3 := h * i
(„:=" , H )
AC
=
H
I)
AC
= AC*I
2 := z 2 / z 3
(„*", UV. UV,
Z2)
AC
=
ZI)
AC
= Z1/AC
L-i".
K)
AC
=
i
1
(„•".
2 3
z
2 := d « e
4 5 6
z 3 := f*g
7 8
z
2 := z 2 + z 3
z
11 12 13 14
D) E)
F)
L * " , G)
9 10
(„*",
B) C)
z
Z1
:=
Z1
:= k - z .
15
a :=
16
j := a
Z1
z,/z2
(.,=:",
A)
(„=:", J)
AC*G
Z2/AC
K-AC
A := A C J
:= A C
4 Im allgemeinen wird bereits durch unterschiedliche Bezeichnung des Operationsteils festgelegt, ob sich der zweite Operand in einem Register oder im Hauptspeicher befindet. 5 Aus didaktischen Erwägungen beschränken wir uns auf die hier angegebene, sehr einfache Befehlsstruktur.
2. Grundlegende Techniken auf der maschinenorientierten Seite
64
Hierzu sind noch einige Erläuterungen nötig. Zu 7:
Ein Transport in einen Speicherplatz Z3 ist nicht erforderlich, weil das Ergebnis in der unmittelbar folgenden Instruktion weiterverarbeitet wird. Zu 8: Wir machen von der Kommutativität der Addition Gebrauch. Zu 12, 13, 14: Bei nichtkommutativen Operatoren benötigen wir auch die Variante mit vertauschten Operanden, was oben durch ein i (= invers) ausgedrückt wird. 6 Zu 16: Hier wird ausgenutzt, daß bei Ausfuhren der 15. Instruktion der Wert von a in AC erhalten bleibt. 2.1.5 Beispiel (Zwei-Adreß-Version) Das gleiche Beispiel wird in der von uns betrachteten Zwei-Adreß-Version etwas kürzer, weil einige Abspeicher-Operationen eingespart werden können. Wir setzen die Existenz dreier Akkumulatoren voraus: 0 1 2 3 4 5 6 7 8 9 10 11 12 13
Z
1 := b*c
z
2 := d*e
z
3 := f*g
z
2 := z 2 + z 3 3 := h*i
z
2 := z 2 / z 3 1 := ZT/Z2 Z 1 := k- Z l a : = Z1 i := a z
Z
(,,: =
ACO, B) , ACO, C) („*" AC1, D) („'• = , AC1, E) („*" („: = AC2, F)
.= oder. .=
( ) (/
In ALGOL 68 steht A+:=l für A : = A + 1. Die Aufgabe wird nur dann organisatorisch komplizierter, wenn auch der Zwischenraum nicht als Trennzeichen zugelassen ist: Ob DO 121 = 1, 10 mit dem Wortsymbol DO oder mit dem Identifikator DO 121 beginnt, kann dann erst beim Komma entschieden werden. 12
97
3.3 Begrenzungssymbole
In der Tabelle haben wir eine Lochstreifen- und eine Lochkartendarstellung für die üblichen Begrenzer angegeben; alle nicht aufgeführten Begrenzer sollen als Wortsymbole mit dem Apostroph als Fluchtsymbol dargestellt werden. 1 3
3.3.2 Bearbeitung der Begrenzungssymbole (Lochkartendarstellung) Die Floyd-Evans-Produktionen zur Erkennung der Begrenzungssymbole in Lochkartendarstellung haben wir in einer Tabelle zusammengestellt, die als Ergänzung des FE-Programms von Nr. 3.2.3 aufzufassen ist: 1 4 Zustand zo
Z12
Z13 Zl4
geänderter Keller
Keller
Folgezustand
SLASH STAR POINT OPEN
SLASH
*
TIMES POINT OPEN
»
APOSTROPHE COLON ANY
APOSTROPHE COLON
SLASH CLOSE SLASH A N Y DIVIDEDBY A N Y
BUS DIVIDEDBY A N Y ANY
POINT POINT POINT POINT POINT
COLON SEMICOLON BECOMES DIGIT
POINT COMMA EQUAL DIGIT ANY
OPEN SLASH OPEN A N Y APOSTROPHE APOSTROPHE APOSTROPHE APOSTROPHE ANY
z0 » • •
DL1
SUB ANY LETTER DIGIT DIGIT PLUS PLUS MINUS MINUS
LETTER APOSTROPHE COLON EQUAL COLON A N Y
BECOMES ANY
*
z 13 z 14 z16 zo
Zo DL2 1S
Zo zo z0 Z0 z6
MA6 ERROR
Zo z0
DL2 ID1 SC3 SC3 SC3 ERROR ID2 DL3 ERROR
ANY Zl6
Semantikroutine
*
• *
Z1S Z8 Z8 Z8 Zl5 Zo Zo
DL2
z0
1 3 Unter einem Fluchtsymbol versteht man ein spezielles Steuerzeichen. Es veranlaßt, daß die folgenden Buchstaben oder Ziffern durch einen besonderen Programmteil verarbeitet werden. 1 4 Bei der Zusammenfügung sind an den Nahtstellen noch einige Änderungen erforderlich. 1 5 Die dritte Zeile im Zustand z , , ist erforderlich, weil die Semantik-Routine D L 2 im Keller D I V I D E D B Y vorfinden soll.
3. Lexikalische Analyse
98
Zur Vereinfachung haben wir im Rahmen von Wortsymbolen nur Buchstaben zugelassen; das Symbol 10 haben wir durch das Apostroph dargestellt. Betrachten wir die Erkennung der Symbolfolge . .=, beginnend beim Zustand z 0 . Der erste Punkt ist bereits in den Keller gelesen, so daß als Folgezustand z 1 2 festgestellt wird. Nunmehr liegen zwei Punkte vor, die im Keller durch den Doppelpunkt ersetzt werden; gleichzeitig wird nach z 0 zurückgekehrt. (Diese Rückkehr haben wir immer dann vorgesehen, wenn Symbole zusammengesetzt wurden.) Der Vergleich COLON führt unter Einlesen des nächsten Symbols in den Zustand z 1 6 . Dort stellen wir fest, daß ein Ergibt-Zeichen vorliegt, und kehren nach z 0 zurück. Über den Ausgang ANY wird die Routine DL1 aufgerufen. Es treten nur drei neue Routinen auf: DL1 besagt, daß das letzte Symbol des Kellers das nächste Begrenzungszeichen ist; im Fall DL2 ist das gesuchte Begrenzungszeichen das vorletzte Symbol des Kellers. (Dieser Fall liegt dann vor, wenn wir Begrenzungszeichen mit übereinstimmenden Anfangsstücken zu betrachten haben.) DL3 schließlich sucht die gefundene Bezeichnung in der vorgegebenen Liste der Wortsymbole. 16 An dieser Stelle wirkt sich gegenüber der ursprünglichen FE-Fassung als wesentliche Vereinfachung aus, daß die Semantikroutinen noch auf den bisherigen Kellerinhalt zugreifen können (vgl. Nr. 1.6.2). Wäre dies nicht möglich, so müßten wir die Routinen zerlegen, so daß jede genau einen Begrenzer bearbeitet, oder während der gesamten Analyse einen zweiten Keller als Arbeitskeller dieser Routinen mitfuhren. 3.3.3 Zeichenketten und Kommentare Zwei Aspekte sind bisher noch nicht berücksichtigt: die Zeichenketten und die Kommentare. Die Zeichenketten stellen uns vor keine wesentlichen Schwierigkeiten. Sie werden Zeichen für Zeichen gelesen, in einer fortlaufenden Zeichenkettenliste abgespeichert und im intern dargestellten Programmtext durch ein entsprechendes Kennzeichen, die laufende Nummer (oder Adresse) in dieser Zeichenkettenliste und die Längenangabe ersetzt. 17 Eine Speicherplatzersparnis ergibt sich, wenn man gemeinsame Teile verschiedener Zeichenketten überlagert [Gatzhammer 1972], Aus Gründen der Übersichtlichkeit enthält unser Beispielcompiler (Kap. 4) keine Zeichenketten. Ein anderes Problem bilden die Kommentare. Wir nehmen an, daß die Folge c o m m e n t . . . comment 16
In Sprachen, in denen neue Wortsymbole eingeführt werden können, liegt nur ein Teil der Liste vor, der Rest muß vom Compiler selbst aufgebaut werden. 17 Unter gewissen Voraussetzungen kann die Längenangabe auch in der Zeichenkettenliste gespeichert werden.
3.4 Aufbau der Symbolliste
99
wobei die Pünktchen für eine beliebige Symbolfolge ohne comment stehen, übergangen wird. Ein Kommentar kann an jeder beliebigen Stelle des Programms eingeschoben werden, auch innerhalb von Identifikatoren oder Konstanten. 18 Zunächst bietet sich folgende Lösung an: Das FE-Programm von Nr. 3.2.2 wird im Teil z 0 um einen Test auf comment erweitert, der zu einem neuen Zustand führt. In diesem verbleibt das FE-Programm, ohne eine Semantik-Routine aufzurufen, bis erneut das Symbol comment erscheint. (Hierzu muß beim Auftreten eines Apostroph analog Zi 4 /zi 5 vorgegangen werden.) Damit im Zustand z 0 auf comment abgefragt werden kann, darf selbstverständlich die APOSTROPHEAbfrage in z 1 5 den Keller nicht löschen. Die relative Kompliziertheit dieser Lösung spricht fiir die Einfuhrung eines Unterprogrammes. Ein weiteres Argument in dieser Richtung ist die mögliche Unterbrechung von Identifikatoren durch Kommentare. Dann werden nämlich durch erneuten Aufruf der Routinen ID1 und ID2 noch benötigte Informationen zerstört. Im Beispielcompiler haben wir daher ein zweistufiges FE-Programm simuliert: Auf der oberen Stufe werden nur die Identifikatoren und Konstanten bearbeitet; ein Stern in der 5. Spalte bedeutet das Lesen des nächsten Zeichens der Eingabe, wobei der Begriff „nächstes Zeichen" nicht im Sinne des Zeichenvorrats des Eingabemediums, sondern im Sinne des Zeichenvorrats der Programmiersprache zu verstehen ist. Dies realisieren wir durch den Aufruf der Routine NEXTSYMBOL. In ihr simulieren wir das in 3.3.2 angegebene FE-Programm, wobei wir beim Auftreten des comment-Symbols nicht zurückspringen, sondern auf das nächste comment-Symbol warten.
3.4 Aufbau der Symbolliste 3.4.1 Der Gültigkeitsbereich einzelner Symbole Wir haben schon mehrfach von der Symbolliste gesprochen, aus der der Compiler während der Übersetzung von Anweisungen Informationen über die auftretenden Symbole, speziell über die Identifikatoren und Operatoren, entnimmt. Der Aufbau dieser Liste hängt wesentlich vom Gültigkeitsbereich der Symbole ab. Prinzipiell stehen hierfür die folgenden Möglichkeiten zur Verfügung: (a)
(b)
Allgemeine Gültigkeit: Es gibt keine den Gültigkeitsbereich einzelner Symbole einschränkende Programmstruktur. Alle Symbole sind überall gültig. INTERNAL-EXTERNAL-Konzept: Das Programm ist in einzelne Programmsegmente gegliedert, die es gestatten, zwischen internen (lokalen) und externen (globalen) Symbolen zu unter-
' * Da ein Wortsymbol als ein einziges Symbol aufgefaßt wird, kann ein Einschub von Kommentaren in Wortsymbole außer Betracht bleiben.
100
3. Lexikalische Analyse
scheiden. Alle Symbole gelten zunächst nur in dem Segment, in dem sie deklariert sind. Durch besondere Kennzeichnung als externes Symbol kann jedoch erreicht werden, daß Symbole eines Programmsegmentes mit allen externen Symbolen gleicher Bezeichnung in anderen Segmenten übereinstimmen. (c) Blockstruktur: Die Gültigkeitsbereiche der Deklarationen sind nach dem Klammerungsprinzip ineinander geschachtelt. Jeder Block ist vollständig in einem übergeordneten enthalten; Überlappung ist nicht zulässig. Jedes Symbol ist in dem Block gültig, in dem der Identifikator deklariert ist, einschließlich aller darin enthaltenen Blöcke, soweit dort keine neue Deklaration für ein Symbol mit gleichem Identifikator vorliegt. (d) Kombination der Blockstruktur mit dem INTERNAL-EXTERNAL-Konzept. Der erste Fall tritt bei sehr einfachen Assemblern auf, den zweiten findet man bei den üblichen Assemblern und FORTRAN. Mit diesen beiden Fällen wollen wir uns nicht weiter beschäftigen. Der dritte Fall ist typisch für die Sprachen der ALGOL-Familie; den vierten bietet PL/1. 3.4.2 Beispiel Wir betrachten ein Beispiel zum kompliziertesten, also zum vierten Fall: eine PL/l-Programmstruktur, die wir aus systematischen Gründen in ALGOL-Form notieren, wobei wir die Deklaration globaler Symbole durch ext kennzeichnen. Wir geben in der Abbildung 3.4.2/1 nur die für den Aufbau der Symbolliste wesentlichen Programmstücke an und lassen insbesondere die meisten Anweisungen weg. 19 Zur klareren Bezugnahme haben wir die für die Erläuterung relevanten Zeilen fortlaufend numeriert und die Blockstruktur durch Einrahmen der einzelnen Blöcke sichtbar gemacht. In der Abb. 3.4.2/2 geben wir die Gültigkeitsbereiche der einzelnen Deklarationen an. 2 0 Die Routinen zum Aufbau und der Bearbeitung der Symbolliste müssen die folgenden Aufgaben bewältigen: (a) Suchen eines Identifikators in dem Teil der Symbolliste, der einem bestimmten Block entspricht: dies ist beim definierenden Auftreten des Identifikators erforderlich, um Mehrfach-Deklarationen zu erkennen. (b) Suchen eines Identifikators in dem Teil der Symbolliste, der zum gerade betrachteten Block und allen diesen umfassenden Blöcken gehört, wobei die aufsteigende Reihenfolge einzuhalten ist: dies ist beim angewandten Auftreten des Identifikators erforderlich, um die zugehörige Deklaration festzustellen. 19 Das Beispiel stammt (mit einigen Änderungen) aus [Lecht 1968]. Da die Prozeduren bzgl. des Gültigkeitsbereichs der Deklarationen den Blöcken gleich gestellt sind, sind sie abweichend vom sonstigen Aufbau des Buches - hier bereits berücksichtigt. 20 Über die Berücksichtigung von impliziten Deklarationen und DEFAULT-Bedingungen: vgl. [Wolf 1972].
3.4 Aufbau der Symbolliste
101
Soll der Compiler nur aus einer Phase bestehen, so machen diejenigen Identifikatoren Schwierigkeiten, bei denen wenigstens ein angewandtes Auftreten dem definierenden vorangeht. Dies trifft hauptsächlich auf vorwärtszeigende Sprunganweisungen zu (über ein Verbot der Sprunganweisungen siehe [Leavenworth 1972] und die dort angegebene Literatur.) Die Schwierigkeiten, die sich ergeben, wenn Informationen benötigt werden, die noch nicht vorhanden sind, lassen sich mit einer Vormerktechnik lösen: 0 1 2
begin int I; proc A . . . begin int M; ext int N;
3 4
proc B . . . begin real N;
5
C:
6
begin ext int N; . . . 1 := 1+1;. . . end; . . .; D;. . .
7 end;
8
proc D . . . begin real M;ext [1:1 OJrealF; end;
9 10 11
int I; . . . E;. . . end;
12 13
proc E . . . begin ext int N; real I;
14
begin
15 16
G:
end; end;
Abb. 3.4.2/1
begin ext [1:10] real F; end;
102
3. Lexikalische Analyse
(C)
Vormerkung von Identifikatoren, die im laufenden Block noch nicht deklariert sind, bis zur Entscheidung über die zugehörige Deklaration: dies ist beim angewandten Auftreten erforderlich, wenn das definierende noch folgen kann. Löschen von Vormerkungen und Eintragen der gewünschten Information an allen vorgemerkten Stellen: dies kann beim definierenden Auftreten geschehen, wenn ein angewandtes Auftreten voranging. Löschen von Vormerkungen, die am Blockschluß noch verblieben sind, falls im übergeordneten Block bereits eine Deklaration vorliegt, oder Übertragung als Vormerkung in den nächst höheren Block, sofern auch dort noch keine Deklaration vorliegt.
(d) (e)
Deklaration Zeile 0 1 2
3 4 5 8 9
Gültigkeitsbereich IdentiP A B C D fikator
I A M N
12 13
E N I F G F
14 15 16
*
*
*
*
*
*
*
*
F
G
0
0
0
*
*
0
0
0
*
0
*
*
*
identisch mit N in Zeile 5 und 13
* *
* *
0
*
*
*
0 *
M F I
E
*
B N C N D
10
Bemerkungen
bereits in Zeile 7 benutzt
* *
*
*
*
*
*
0
*
0
*
*
*
*
*
*
0
0
* *
* *
* *
*
* *
0
identisch mit F in Zeile 16 bereits in Zeile 6 benutzt
*
*
* = gültig aufgrund dieser Vereinbarung 0 = gültig aufgrund einer anderen Vereinbarung, die aber dasselbe Objekt bezeichnet. P = äußerster Block (Programm) Abb. 3.4.2/2
3.4 Aufbau der Symbolliste
103
3.4.3 Symbolliste bei Blockstruktur Zur Bearbeitung der beiden einfachen Fälle — allgemeine Gültigkeit und INTERNAL-EXTERNAL-Konzept - genügt eine einfache Aneinanderreihung aller Eintragungen in der Symbolliste. Dieses Verfahren scheitert, wenn wir es mit einer Blockstruktur zu tun haben. Wir benötigen dann Teillisten, die jeweils die Deklarationen eines Blockes zusammenfassen, obwohl diese im Programm nicht zusammenhängend auftreten müssen. So finden wir im Beispiel 3.4.2/1 die Deklarationen des äußersten Blockes in den Zeilen 0, 1 und 12; dazwischen sind vier weitere Blöcke zu bearbeiten. Als Lösung bietet sich der Einsatz verketteter Listen an, bei denen jedes Element den Index seines Nachfolgers enthält. Sollen die Teillisten für jeden Block jedoch zusammenhängend gespeichert werden, so kann man entweder bei Eintritt in einen neuen Block auf Verdacht einige Listenpositionen für den Rest des vorhergehenden freilassen oder bei Abschluß jedes Blockes eine Umordnung vornehmen, in deren Verlauf alle noch nicht abgeschlossenen Teillisten an das Ende gerückt werden. Am einfachsten wird der Listenaufbau, wenn man nach Beendigung eines Blockes die zugehörige Teilliste nicht mehr benötigt und sie daher streichen kann. Da keine Überlappungen möglich sind, ist die Bedingung des Zusammenhangs dann automatisch erfüllt. Der gleiche Effekt ergibt sich, wenn man Daten auf Hintergrundspeicher auslagern kann: man lagert fortlaufend die Teillisten beendigter Blöcke aus und erreicht so, daß sowohl in der verbliebenen Liste noch nicht beendeter Blöcke als auch in der endgültigen (ausgelagerten) Gesamtliste die Teillisten der einzelnen Blöcke zusammenhängend gespeichert sind. 3.4.4 Beispiel In der Abbildung geben wir die Symbolliste zu dem Beispiel 3.4.2/1 in der Reihenfolge an, die sich bei Umordnung oder Auslagerung ergibt. Hierzu noch folgende Bemerkungen: 1. Den Anfang der Liste bilden in der Regel die Standardsymbole, die in den Programmen ohne besondere Deklaration verwandt werden dürfen. Wir lassen die Liste daher (willkürlich) bei Nr. 9 beginnen. 2. Symbole, die sich auf umfassendere Gültigkeitsbereiche beziehen, sind durch entsprechende Verweise auf eine Liste der externen Symbole gekennzeichnet. 3. Werden bereits während des ersten Durchgangs die Symbole im Programmtext durch Verweise auf die Symbolliste ersetzt, so muß darauf geachtet werden, daß in Zeile 6 des Programms der Identifikator I nicht durch einen Verweis auf die Deklaration von Zeile 0 ersetzt wird. Vielmehr ist I — wie alle Symbole, deren Deklaration noch folgen kann — in der Teilliste des neuen Blockes (Blockes Nr. 4) vorzumerken, am Blockende in die Teilliste von Block Nr. 3 und schließlich in die von Block Nr. 2 zu übertragen. Diese Technik sei nur der Vollständigkeit halber erwähnt.
104
3. Lexikalische Analyse
BlockNr. 4 3
Identi- Externfikator Liste N 1 — N C -
Beschreibung Angaben über Art, Adresse
12 13 14 15 16 17 18
5
M F M N B D I
usw.
19 20 21 22 23
8 7 6
24 25 26
1
Nr. 9 10 11
2
F G N I F I A E
—
2 —
1 — — -
2 —
1 — —
— -
Abb. zu 3.4.4. Umgeordnete Symbolliste zum Beispiel aus Nr. 3.4.2
3.4.5 Suchen in einer blockstrukturierten Symbolliste Sollen nun in einer späteren Phase, wenn die Symbolliste vollständig ist, einzelne Symbole in der Symbolliste aufgesucht werden, so ist folgendermaßen zu verfahren: 21 1. Der Block, in dem das Symbol auftritt, wird als laufender Block bezeichnet. 2. Das Symbol wird in der Teilliste gesucht, die zum laufenden Block gehört. 3. Wird das Symbol dort gefunden, wird es im Programm durch einen Verweis auf diese Eintragung in der Symbolliste ersetzt und der Suchvorgang ist beendet. 4. Wird das Symbol dort nicht gefunden, so wird der kleinste, den laufenden Block umfassende Block zum laufenden Block und das Verfahren mit 2. fortgesetzt. Das Verfahren wird mit Fehlermeldung abgebrochen, wenn der äußerste Block erreicht worden war. Dieser Algorithmus, den wir in der Abbildung durch ein Ablaufdiagramm beschreiben, benötigt eine Liste aller Blöcke, ihrer gegenseitigen Verschachtelung und der 21
Eine Übersicht über Tabellensuchverfahren findet der Leser z.B. in [Price 1971].
105
3.4 Aufbau der Symbolliste
Hinweise auf den Anfang und die Länge (oder das Ende) des zugehörigen Teils der Symbolliste: 22 struct block = (int FIRST SYMBOL, LAST SYMBOL, SURROUNDING BLOCK). In der aus derartigen Elementen gebildeten Blockliste [0: 1 flex] block BLOCKLIST erscheinen die Blöcke in der gleichen Reihenfolge wie die Blockanfänge im Programm. Um das gesamte Programm kann man sich einen die Standardvereinbarungen enthaltenden Block 0 denken, für den in unserem Beispiel in der Symbolliste 9 Positionen reserviert sind. In Block 1 sind die in der Symbolliste unter Nr. 2 4 - 2 6 eingetragenen Identifikatoren deklariert usw. Block Nr.
FIRST SYMBOL
LAST SYMBOL
SURROUNDING BLOCK
0 1 2 3 4 5 6 7 8
1 24 14 10 9 12 21 20 19
9 26 18 11 9 13 23 20 19
-1 0 1 2 3 2 1 6 7
Abb. 3.4.5/1. Blockliste zum Beispiel aus Nr. 3.4.2
"
Vgl. [Gries, Paul, Wiehle 1965].
106
Abb. 3.4.5/2. Suchen in einer blockstrukturierten Symbolliste
3. Lexikalische Analyse
4. Ein Beispiel-Compiler
4.0 Überblick Im vorliegenden Kapitel wollen wir einen Compiler für eine sehr einfache Beispielsprache angeben. Bei diesem Compiler soll auf Effektivität verzichtet werden: wir legen weder Wert auf eine schnelle Übersetzung noch auf die Erzeugung eines schnellen Programms. Bei der Erstellung dieses Compilers lag das Gewicht vielmehr auf der Übersichtlichkeit, so daß die Studierenden den Informationsfluß leichter verfolgen können. Maßnahmen, die diesem Ziel dienten, waren: - Zerlegung des Compilers in vier Phasen, deren jede nur einen bestimmten Teil der Übersetzung ausfuhrt; - Zerlegung der Übergangsmatrix in eine Folge nacheinander zu durchlaufender Abfragen, so daß dem Leser aufgrund der Abfrage, die jedem Programm stück vorausgeht, das letzte Kellersymbol und das nächste Symbol der Eingabe bekannt sind; - Verlagerung aller Aktionen des Compilers in Prozeduren, auch wenn diese nur einmal aufgerufen werden, und Zusammenfassung inhaltlich zusammengehörender Prozeduren an einer Stelle; - Verwendung sich selbst erklärender Identifikatoren.
4.0.1 Sprachumfang Der Compiler übersetzt Programme mit folgendem Sprachumfang: (a) (b)
(c)
Konstanten der Arten int und real in Standarddarstellung, z.B. 14 oder 3.14. Deklarationen für Namen, die einen Bezug auf derartige Konstante erlauben, und für Felder in der ALGOL 68-Schreibweise, jedoch ohne flex und ohne die Möglichkeit, Feldgrenzen wegzulassen. Marken und goto-Anweisungen der Form goto MARKE.
(d) (e)
Arithmetische Ausdrücke unter Verwendung von +, -, *, / , ' / ' . m ° d u n d runder Klammern 1 . Wertzuweisungen unter Verwendung arithmetischer Ausdrücke als rechte Seiten.
1 '/' bezeichne die ganzzahlige Division -r und m o d die Bestimmung des dabei entstehenden Restes.
4. Ein Beispiel-Compiler
108
(f)
(g)
(h)
Eine Wertzuweisung hat als Wert ihre linke Seite. Mehrfachzuweisungen sind ebenso zulässig, wie die Verwendung von eingeklammerten Wertzuweisungen anstelle von Operanden in Ausdrücken. Auch Blöcke dürfen in Wertzuweisungen auftreten. Der Wert eines Blockes ist der Wert des letzten Ausdruckes bzw. der letzten Anweisung. Klammerung ist auch durch die Wortsymbole begin und end zulässig. In diesem Fall dürfen Deklarationen folgen (Blockstruktur), jedoch nur bis zur ersten auf begin folgenden Marke. Der Unterschied zwischen runden Klammern und dem Paar begin — end besteht nur darin, daß das zweite die Blockstruktur definiert, während das erste hierfür ohne Bedeutung ist. Semikolon als Trennzeichen aufeinanderfolgender Anweisungen. Zwischen Deklarationen ist neben dem Semikolon auch das Komma zulässig.
Bei dieser Sprache handelt es sich um eine sehr stark eingeschränkte, echte Teilmenge von ALGOL 68. Ein Beispielprogramm: begin int A, B, real C; int D, [1:10] real X, [1:10] int X1; A := B := 1; C := 3.5; X[1]:=5; D := 10; X[B] := 2*C - D/(X[5] := 6*C); [0: 3*D, 1: X1 [ 1 ]] int Y, Z; M: X[(D+A)'/'2] := D*X[1] + begin int A, C; A := Y[B,4] + (C := 4); begin real A, E; A := 3.0; E := D/3 end; C := C+1; goto M; A end * begin int D, X; Y[(X: = 1), 2 ] : = D : = 3 ; goto M; D end; goto M M:
end; Durch die Einbeziehung der Sprunganweisungen ohne gleichzeitige Berücksichtigung der bedingten Anweisungen sind dieses Programm und der definierte Sprachumfang nicht sinnvoll 2 .
2
Die Sprunganweisungen wurden berücksichtigt, um ihren Zusammenhang mit der Blockstruktur zu verdeutlichen.
4.0 Überblick
109
4.0.2 Einteilung der Phasen Die verschiedenen Aufgaben sind folgendermaßen auf die vier nacheinander ablaufenden Phasen verteilt: (a)
(b)
(c)
(d)
Die erste Phase beschränkt sich auf die Symbolidentifikation, soweit diese bereits möglich ist. Die syntaktischen Begrenzungszeichen werden aufgrund einer vorgegebenen Liste identifiziert, die Konstanten in einer Liste gesammelt. Die Identifikatoren werden auf eine vorgegebene Zeichenzahl gekürzt, aber noch nicht weiter bearbeitet; auch die Operationszeichen werden nur als solche erkannt und erst später identifiziert (Vgl. 3. und 4. Phase). Die zweite Phase sucht die Blockstruktur und alle Deklarationen einschließlich der Marken heraus. Sie baut die Liste der Blöcke und der Identifikatoren auf und ordnet jedem Identifikator seine Art zu. Die dritte Phase ersetzt alle Identifikatoren in der internen Programmversion aufgrund der nun vorliegenden Symbolliste durch den Index, unter dem der Identifikator in der Symbolliste eingetragen ist. Sie ersetzt ferner die Felddeklarationen und die indizierten Variablen durch Unterprogrammaufrufe, die goto-Anweisungen durch Sprungbefehle, begin, end und Marken fuhren zur Einsetzung von Unterprogrammaufrufen für die dynamische Speicherverwaltung, (begin und end können danach auf die Bedeutung runder Klammern reduziert werden.) Die letzte Phase schließlich ersetzt die Operationen in arithmetischen Ausdrücken und die Wertzuweisungen durch Maschinenbefehle. Erst in dieser Phase ist die Identifizierung der Operationen möglich, da die Art ihrer Operanden zur Identifizierung benötigt wird (s. 4.4.0).
Abb. zu 4.0.2. Datenfluß zwischen den Phasen des Beispiel-Compilers
110
4. Ein Beispiel-Compiler
Alle diese Gesichtspunkte sind natürlich in den Kapiteln 1 bis 3 bereits behandelt worden. Für das Verständnis der einzelnen Compilerteile wird es nötig sein, die entsprechenden Abschnitte hinzuzuziehen. In der Abbildung ist der Datenfluß zwischen den Phasen dargestellt. Der Leser möge sich beim Studium der folgenden Teile des Kapitels diese Abbildung immer wieder ins Gedächtnis zurückrufen.
4.0.3 Implementierung Auf einige Einzelheiten, die bei der Implementierung des Beispiel-Compilers eine wichtige Rolle spielen, wollen wir hier nicht eingehen, weil sie den Anfänger eher verwirren. Hierzu zählen: — die interne Codierung der Symbole innerhalb der einzelnen Phasen, — die Datenübergabe von einer Phase zur nächsten. Während man bei Compilern, die im täglichen Gebrauch eingesetzt werden sollen, bei der Interndarstellung der Daten auf eine möglichst geringe Zahl von Binärstellen Wert legt, werden im Beispiel-Compiler dafür Ganzwörter verwandt, die die Symbole im Klartext beschreiben. (Dadurch können die mit dem BeispielCompiler arbeitenden Studenten Kontrollausgaben, z.B. des Kellerinhaltes, unmittelbar lesen.) Zur Initialisierung der Codierung und zur Datenübergabe dienen Prozeduren, auf deren genaue Beschreibung wir im folgenden ebenfalls verzichten. Der Beispiel-Compiler liegt bei Abschluß der Arbeiten am Manuskript als ALGOL 60-Programm für die beiden dem Autor an der Universität ErlangenNürnberg zugänglichen Rechenanlagen (CONTROL DATA 3300, TELEFUNKEN RECHNER 440) vor. Er wird in den Übungen zur Compilertechnik eingesetzt. Die Übungsaufgaben sind von der Art: — Erweiterung des angenommenen Sprachumfangs, — Neuformulierung einzelner Phasen in einer anderen Technik, — Formulierung einzelner Phasen für eine andere Sprache. Die Beschreibung des Beispiel-Compilers in diesem Buch erfolgt aus Gründen der Übersichtlichkeit in ALGOL 68.
111
4.1 Phase I
4.1 Phase I 4.1.0 Rechnerabhängige Prozeduren Wir benötigen in dieser Phase, die in unserem Beispiel-Compiler die Aufgaben des Kap. 3 zu lösen hat, einige Prozeduren, auf deren Details wir wegen ihrer Rechnerabhängigkeit nicht eingehen wollen: (a)
(b)
(c)
NEXT CHARACTER ordnet der globalen Variablen CHARACTER das nächste Eingabezeichen zu. Liegt ein Kartenwechsel vor, so wird das Zeichen LINE END eingeschoben. Die Prozedur verwaltet die Eingabedatei und zählt insbesondere die Zeilennummern im Programm zur Verwendung in der Prozedur ERROR. Sie hat keine formalen Parameter. NEXT RELEVANT CHARACTER ruft NEXT CHARACTER so o f t auf, bis CHARACTER weder das Zeichen BLANK (Zwischenraum) noch das Zeichen LINE END enthält. OUTSYMBOL mit den Parametern string GROUP, union(int, string) S Y M B O L hinterlegt für die 2. Phase in einer geeigneten Datei ein Symbol. Dabei handelt es sich nicht mehr um Zeichen im Sinne des Zeichenvorrats eines Eingabegerätes, sondern um eine vom konkreten Eingabegerät unabhängige Darstellung. (Die Form dieser Darstellung ist ausschließlich eine Frage der Optimierung; hier stellen wir die Symbole durch Zeichenketten dar.) Jede Eintragung umfaßt eine Klassifizierung (SYNTACTIC, INT, REAL, ACTION, DECLARER, LINE NUMBER, IDENTIFIER 3 ) und das Symbol als Text (im Fall IDENTIFIER oder ACTION) oder als Index bezüglich der Liste, in der das Symbol abgelegt ist (Konstantenliste, Begrenzerliste). In dieser Datei entsteht so eine „interne" Version des Programms. Die vierte Zeile des in 4.0.1 angegebenen Beispiels lautet etwa in Lochkartenversion: (/0. .3*D, 1. .X1(/1/)/) 'INT' Y, Z„ OUTSYMBOL hinterlegt in der 1. Internversion hierfür, z.B.:
3
Weitere Klassifizierungen werden in den späteren Phasen eingeführt.
4. Ein Beispiel-Compiler
112
abgelegt SYNTACTIC INT SYNTACTIC INT ACTION IDENTIFIER SYNTACTIC INT SYNTACTIC IDENTIFIER (d)
(e)
!
5
1
7
!
10
Erkl.
abgelegt
[
SYNTACTIC INT SYNTACTIC SYNTACTIC DECLARER IDENTIFIER SYNTACTIC LINE NUMBER4 IDENTIFIER SYNTACTIC
0
i 6 J TIMES
3
i D
D
! 11 i
*
>
l
1
1 X
X
! 10
Erkl. 5 1 6 6 1 Y 11 5 Z 8
[
1
] ]
INT Y >
Z 5
O U T L I S T S schließt die Datei mit der 1. Internversion durch ein spezielles Symbol F I L E E N D ab und baut eine zweite Datei (Listendatei) auf, in die die Liste der Konstanten eingetragen wird. E R R O R mit dem Parameter string MESSAGE erzeugt eine Fehlermeldung unter Angabe der Zeilennummer im Quellenprogramm und des für M E S S A G E aktuell eingesetzten Textes und bewirkt den Abbruch des Compilerlaufs 5 . Man beachte, daß das in vielen Fällen erforderliche Vorweglesen der nächsten Eingabezeichen eine leichte Verschiebung der Zeilen-Nummern bewirken kann. Im Beispiel wird die Zeilennummer 5 bereits vor dem Identifikator Z eingefügt, da dessen Ende erst durch das Semikolon angezeigt wird. Die Bearbeitung dieses Symbols veranlaßt jedoch N E X T R E L E V A N T C H A R A C T E R , bereits das erste Zeichen der 5. Zeile zu lesen. (Selbstverständlich läßt sich dieser E f f e k t mit einigem A u f w a n d ausschalten.)
(f)
L E T T E R und DIGIT mit dem Parameter char CHARACTER
(g)
prüfen, ob dem Parameter ein Buchstabe bzw. eine Ziffer zugewiesen ist; D I G V A L bestimmt zu einer dem Parameter C H A R A C T E R zugewiesenen Ziffer den zahlenmäßigen Wert. C O N C A T E N A T E mit den Parametern ref string WORD, char NEW hängt an den Parameter WORD das Zeichen NEW an.
Siehe Bemerkung in e). Soll der Compiler nach Erkennen eines Fehlers weiterarbeiten, so kann als zweiter Parameter eine Marke vorgesehen werden, die im Fehlerfall anzuspringen ist.
4
5
4.1 Phase I
113
4.1.1 Prozedur NEXT SYMBOL Wir benötigen eine Prozedur NEXT SYMBOL, welche die vom gewählten Eingabegerät abhängigen Eingabezeichen in die Basissymbole der betrachteten Programmiersprache umformt, z.B. wird die Zeichenfolge Doppelpunkt, Gleichheitszeichen umgewandelt in ein Ergibtzeichen. Der folgenden Fassung liegen die üblichen Lochkartenkonventionen 6 zugrunde (vgl. 3.3.1), wobei zusätzlich zugelassen wird, daß Zeichen, für die eine Ersatzdarstellung existiert, auch als gesonderte Zeichen vorhanden sind (etwa : neben ..). Die Prozedur NEXT SYMBOL benutzt die globalen Variablen CHARACTER (laufendes Zeichen) und STATE (Kommentar oder nicht) und setzt voraus, daß eine Vielzahl an Konstanten entsprechend ihrer Bezeichnung vorbesetzt sind (z.B. ACTION, SLASH). Kommentare werden in der Prozedur NEXT SYMBOL abgearbeitet, d.h. einfach überlesen. Sie dürfen daher überall — auch in Identifikatoren oder Konstanten — stehen, außer in Zeichen, die im Sinne der Sprachdefinition als ein Zeichen aufzufassen sind. Sie werden durch comment.. . comment eingeschlossen und im Programm teil TEST STATE von Symbolen außerhalb eines Kommentars unterschieden. Beim ersten Aufruf von NEXT SYMBOL erhält CHARACTER das erste Zeichen des eingegebenen Programms, das verschieden von Zwischenraum oder Kartenwechsel ist. Nach Ausführung der weiteren Aufrufe von NEXT SYMBOL enthält CHARACTER stets das nächste relevante Zeichen des eingegebenen Programms. Im folgenden Programm entsprechen die Marken den Zuständen aus Nr. 3.3.2: proc NEXT SYMBOL = () string: begin string SYMBOL, int COUNT; START: SYMBOL := CHARACTER; ZO: if SYMBOL = FILE END then goto EXIT fi; NEXT RELEVANT CHARACTER; if SYMBOL = SLASH then goto Z11 fi; if SYMBOL = POINT then goto Z12 fi; if SYMBOL = OPEN then goto Z13 fi; if SYMBOL = APOSTROPHE then SYMBOL := CHARACTER; NEXT RELEVANT CHARACTER; goto Z14 fi; if SYMBOL = COLON then goto Z16 fi; goto TEST STATE; 6
Das Apostroph werde nur als Fluchtsymbol für Wortsymbole genutzt, nicht an Stelle der ,„.
4. Ein Beispiel-Compiler
114
Z I 1:
Z12:
Z13:
Z14: Z15:
if C H A R A C T E R = C L O S E then S Y M B O L := BUS; N E X T R E L E V A N T C H A R A C T E R fi; goto T E S T S T A T E ; if C H A R A C T E R = POINT then S Y M B O L := C O L O N ; goto ZO fi; if C H A R A C T E R = C O M M A then S Y M B O L := S E M I C O L O N ; N E X T R E L E V A N T C H A R A C T E R ; goto T E S T S T A T E fi; if C H A R A C T E R = E Q U A L then S Y M B O L := B E C O M E S ; N E X T R E L E V A N T C H A R A C T E R ; goto T E S T S T A T E fi; goto T E S T S T A T E ; if C H A R A C T E R = S L A S H then S Y M B O L := SUB; N E X T R E L E V A N T C H A R A C T E R fi; goto T E S T S T A T E ; I D E N T 1 (COUNT); if C H A R A C T E R = A P O S T R O P H E then I D E N T 3 ( S Y M B O L , C O U N T ) ; N E X T R E L E V A N T C H A R A C T E R ; goto T E S T S T A T E
fi; if C H A R A C T E R = F I L E E N D then ERROR(„incomplete word symbol") fi; I D E N T 2 ( S Y M B O L , C H A R A C T E R , COUNT); N E X T R E L E V A N T C H A R A C T E R ; goto Z15; Z16: if C H A R A C T E R = E Q U A L then S Y M B O L := B E C O M E S ; N E X T R E L E V A N T C H A R A C T E R fi; TEST STATE: if S T A T E = IN C O M M E N T then if S Y M B O L = C O M M E N T then S T A T E := N O R M A L fi; goto S T A R T else if S Y M B O L = C O M M E N T then S T A T E := IN C O M M E N T ; goto S T A R T fi EXIT: end;
fi; SYMBOL
Die Prozeduren IDENT1, IDENT2 und IDENT3 werden in Nr. 4.1.4 erläutert. Hier sollte noch einmal daraufhingewiesen werden, daß kein optimales Programm angestrebt wurde: erfahrene Programmierer haben wahrscheinlich bemerkt, daß Z16 mit den letzten Zeilen von Z12 übereinstimmt.
4.1 Phase I
115
4.1.2 Liste der Begrenzer Da die Begrenzer hier als jeweils ein Symbol aufgefaßt werden, auch wenn sie aus mehreren Einzelzeichen (im Sinne des Zeichenvorrats des Eingabegeräts) bestehen, erscheint die Zusammenstellung der Begrenzer bereits hier in der Prozedur NEXT SYMBOL. Für die Liste der Begrenzungszeichen des gewählten Sprachumfangs benötigen wir ein dreispaltiges Feld (s. Abb.), das uns gestattet, Aktionen, Deklaratoren 7 und die übrigen syntaktischen Zeichen zu unterscheiden. (Weitere Gruppen sind bei einem Ausbau des Sprachumfangs möglich.) Die Angaben der zweiten und dritten Spalte werden an die nächste Phase weitergegeben. (Die Aktionen können dagegen erst vollständig identifiziert werden, wenn die Art der Operanden bekannt ist.)
NO
INDICATION
DELIMITER LIST GROUP
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
BEGIN END OPEN CLOSE SUB BUS BECOMES SEMICOLON GOTO COLON COMMA INT REAL PLUS MINUS TIMES SLASH WSSLASH MOD
SYNTACTIC SYNTACTIC SYNTACTIC SYNTACTIC SYNTACTIC SYNTACTIC SYNTACTIC SYNTACTIC SYNTACTIC SYNTACTIC SYNTACTIC DECLARER DECLARER ACTION ACTION ACTION ACTION ACTION ACTION
CODE 1 2 3 4 5 6 7 8 9 10 11 1 2 PLUS MINUS TIMES DIVIDED BY INT DIV MOD
Abb. zu 4.1.2
Zur Behandlung der Begrenzungszeichen im Programm sind die folgenden Deklarationen erforderlich:
7
int wird als Deklaiator für die Art ref int aufgefaßt.
4. Ein Beispiel-Compiler
116
struct delimiter = (string I N D I C A T I O N , string G R O U P , union (int, string) C O D E ) ;
[1: L E N G T H D E L I M I T E R L I S T ] delimiter D E L I M I T E R L I S T comment Die Elemente dieses Feldes sind gemäß den Angaben der Tabelle zu besetzen
comment;
proc E X A M I N E D E L I M I T E R = (string S Y M B O L ) :
begin for I from 1 by 1 to L E N G T H D E L I M I T E R L I S T do
if I N D I C A T I O N of D E L I M I T E R L I S T [ I ] = S Y M B O L
then O U T S Y M B O L ( G R O U P of D E L I M I T E R L I S T [ I ] , CODE of D E L I M I T E R
LISTfl]);
goto E X I T fi;
E R R O R ( „ u n k n o w n or incorrect delimiter");
EXIT:
end;
4.1.3 Hauptprogramm Für das H a u p t p r o g r a m m der Phase I verbleibt danach nur n o c h d a s Z u s a m m e n stellen der I d e n t i f i k a t o r e n u n d K o n s t a n t e n , entsprechend 3 . 2 . 4 und 3 . 2 . 5 . Die U n t e r p r o g r a m m e erläutern wir in den f o l g e n d e n A b s c h n i t t e n . Z u Beginn erfolgen einige Initialisierungen: int L E N G T H C O N S T A N T , S T A T E , C O U N T ; string S Y M B O L , F O L L O W I N G S Y M B O L ; ENVIRONMENT
INITIALIZATION;
CODE I N I T I A L I Z A T I O N ; L E N G T H C O N S T L I S T :=0; S T A T E := N O R M A L ; NEXT RELEVANT CHARACTER; S T A R T : S Y M B O L := N E X T S Y M B O L ;
ZO:
if S Y M B O L = F I L E E N D then goto O U T fi; if L E T T E R ( S Y M B O L ) then I D E N T 1 ( C O U N T ) ; goto Z1 fi; if S Y M B O L = Z E R O then INT4; goto Z 3 fi; if D I G I T ( S Y M B O L ) then INT1;goto Z 2 fi; if S Y M B O L = P O I N T then M A N 6 ; goto Z6 fi; EXAMINE DELIMITER(SYMBOL);goto START;
4.1 Phase I Z1:
117
F O L L O W I N G S Y M B O L := N E X T S Y M B O L ; if L E T T E R ( F O L L O W I N G S Y M B O L ) then I D E N T 2 ( S Y M B O L , F O L L O W I N G S Y M B O L , C O U N T ) ; goto Z1 fi; if D I G I T f F O L L O W I N G S Y M B O L ) then I D E N T 2 ( S Y M B O L , F O L L O W I N G S Y M B O L , C O U N T ) ; goto Z1 fi; IDENT3(SYMBOL, COUNT); IDENT4; S Y M B O L := F O L L O W I N G SYMBOL; goto ZO;
Z2:
F O L L O W I N G S Y M B O L := N E X T SYMBOL; if L E T T E R ( F O L L O W I N G S Y M B O L ) then E R R O R („letter in arithmetic constant") fi; if D I G I T f F O L L O W I N G S Y M B O L ) then INT2; goto Z2 fi; if F O L L O W I N G S Y M B O L = P O I N T then M A N 1 ; goto Z4 fi; INT3; S Y M B O L := F O L L O W I N G S Y M B O L ; goto ZO;
Z3:
F O L L O W I N G S Y M B O L := N E X T S Y M B O L ; if L E T T E R ( F O L L O W I N G S Y M B O L ) then E R R O R („letter in arithmetic constant") fi; if F O L L O W I N G S Y M B O L = Z E R O then goto Z 3 fi; if D I G I T f F O L L O W I N G S Y M B O L ) then INT2; goto Z 2 fi; if F O L L O W I N G S Y M B O L = P O I N T then M A N 1 ; goto Z 6 fi; INT5; S Y M B O L := F O L L O W I N G S Y M B O L ; goto ZO;
Z4:
F O L L O W I N G S Y M B O L := N E X T S Y M B O L ; if D I G I T ( F O L L O W I N G S Y M B O L ) then M A N 2 ; goto Z5 fi; ERROR („missing fractional part in real constant");
Z5:
F O L L O W I N G S Y M B O L := N E X T S Y M B O L ; if D I G I T ( F O L L O W I N G S Y M B O L ) then M A N 2 ; goto Z 5 fi; if LETTER ( F O L L O W I N G S Y M B O L ) then E R R O R („letter in fractional part") fi; M A N 4 ; S Y M B O L := F O L L O W I N G SYMBOL; goto ZO;
Z6:
F O L L O W I N G S Y M B O L := N E X T S Y M B O L ; if F O L L O W I N G S Y M B O L = Z E R O then M A N 3 ; goto Z 7 fi; if D I G I T f F O L L O W I N G S Y M B O L ) then M A N 2 ; goto Z 5 fi; E R R O R („missing fractional part in real constant");
2.1 \
F O L L O W I N G S Y M B O L := S Y M B O L ; if F O L L O W I N G S Y M B O L = Z E R O then M A N 3 ; goto Z7 fi; if D I G I T f F O L L O W I N G S Y M B O L ) then M A N 2 ; goto Z5 fi; if L E T T E R f F O L L O W I N G S Y M B O L ) then E R R O R („letter in fractional part") fi; M A N 5 ; S Y M B O L := F O L L O W I N G SYMBOL; goto ZO;
OUT:
OUTLISTS
118
4 . Ein Beispiel-Compiler
Zur Vereinfachung haben wir den Skalenfaktor nicht behandelt und einige der möglichen Fehlermeldungen weggelassen.
4.1.4 Identifikatoren Zur Bearbeitung der Identifikatoren dienen die folgenden Routinen: proc I D E N T 1 = (ref int C O U N T E R ) : C O U N T E R := 1 proc I D E N T 2 = (ref string S Y M B O L , char NEW, ref int C O U N T E R ) : if C O U N T E R < I D T M A X L E N G T H then C O U N T E R := C O U N T E R + 1; C O N C A T E N A T E ( S Y M B O L , N E W ) f proc I D E N T 3 = (ref string S Y M B O L , int C O U N T E R ) : for I from C O U N T E R + 1 by 1 to I D T M A X L E N G T H do CONCATENATE(SYMBOL, BLANK); proc I D E N T 4 = (): O U T S Y M B O L O D E N T I F I E R , S Y M B O L ) ; Die letzte dieser Prozeduren hat keine Parameter; die angegebenen Klammern dürfen daher fehlen. Bei den ersten drei Prozeduren sind die Parameter erforderlich, weil sowohl NEXT SYMBOL, als auch das Hauptprogramm diese Prozeduren aufrufen, wobei verschiedene Namen als aktuelle Parameter verwandt werden. Dem Namen IDENTIFIER ist als Wert die Zeichenkette zugewiesen, die aus den gleichen Buchstaben besteht 8 .
4.1.5 Ganzzahlige Konstanten Zur Bearbeitung der Konstanten dienen die folgenden Deklarationen: struct constant = (string M O D E , union (int, real) V A L U E ) ; [1: M A X L E N G T H C O N S T A N T L I S T ] constant C O N S T A N T L I S T ; int L E N G T H C O N S T L I S T , I N T S Y M B O L , C O U N T E R , O V E R F L O W C O U N T E R ; proc I N T 1 = (): begin I N T S Y M B O L := D I G V A L ( S Y M B O L ) ; C O U N T E R := 1; O V E R F L O W C O U N T E R := 0 end; proc I N T 2 = (): if C O U N T E R < M A X L E N G T H A R I T H M E T I C then C O U N T E R := C O U N T E R + 1; INT S Y M B O L := 1 0 * I N T S Y M B O L + DIGVAL(FOLLOWING SYMBOL) else O V E R F L O W C O U N T E R := O V E R F L O W C O U N T E R + 1 fi; ' Für die richtige Funktion des Beispiel-Compilers würde es genügen, irgendeinen Wert zu verwenden, der von denen der anderen Gruppen verschieden ist. Um die Internversion, die von den einzelnen Phasen erstellt werden, lesbar zu halten, wurden die hier angegebenen (redundanten) Darstellungen verwendet.
119
4.1 Phase!
proc INT3 = (): if OVERFLOW COUNTER * 0 then ERROR („overflow in integer constant") else INT5 fi; proc INT4 = (): COUNTER := OVERFLOW COUNTER := INT S Y M B O L := 0; proc INT5 = (): begin for I from 1 by 1 to LENGTH CONSTLIST do if CONSTANT LIST[I] = (INT, INT SYMBOL) then OUTSYMBOLONT, I); goto EXITfi; LENGTH CONSTLIST := LENGTH CONSTLIST +1; if LENGTH CONSTLIST > M A X LENGTH CONSTANT LIST then ERROR(„overflow list of constants") fi; CONSTANT LIST[LENGTH CONSTLIST] := (INT, INT SYMBOL); OUTSYMBOLONT, LENGTH CONSTLIST); EXIT: end;
Zu INT3 ist noch zu sagen, daß das Hauptprogramm so angelegt ist, daß die Überschreitung der zulässigen Stellenzahl nur dann nach INT3 und damit zum Fehlerausgang fuhrt, wenn kein Punkt folgt. INT5 sucht in der Liste aller Konstanten, ob die neue Konstante bereits darin enthalten ist. In diesem Fall wird neben der Klassifizierung die laufende Nummer ausgegeben, unter der sie gefunden wurde. Andernfalls wird vor dieser Ausgabe die Liste um eine Eintragung verlängert.
4.1.6 Reellwertige Objekte Die im vorigen Abschnitt deklarierten Objekte werden auch zur Bearbeitung der reellen Konstanten verwandt. Daneben benötigen wir: int FR ACT COUNTER; real R E A L SYMBOL; proc MAN1 = (): FR ACT COUNTER := 0; proc MAN2 = (): if COUNTER < M A X LENGTH A R I T H M E T I C then COUNTER := COUNTER + 1; FRACT COUNTER := FRACT COUNTER + 1; INT S Y M B O L :=10*INT S Y M B O L + D IG VAL( FOL LOWING SYMBOL) fi;
120
4. Ein Beispiel-Compiler
proc M A N 3 = (): F R A C T C O U N T E R := F R A C T C O U N T E R + 1; proc M A N 4 = (): begin R E A L S Y M B O L := INT S Y M B O L * 1 0 t O V E R F L O W C O U N T E R / 10tFRACT COUNTER; MAN7 end; proc M A N 5 = (): begin R E A L S Y M B O L := 0.0; M A N 7 end; proc M A N 6 = (): C O U N T E R := O V E R F L O W C O U N T E R := F R A C T C O U N T E R := INT S Y M B O L := 0; proc M A N 7 = (): begin for I from 1 by 1 to L E N G T H C O N S T L I S T d o if C O N S T A N T LIST[I] = ( R E A L , R E A L S Y M B O L ) then O U T S Y M B O L f R E A L , I); goto E X I T fi; L E N G T H C O N S T L I S T := L E N G T H C O N S T L I S T +1; if L E N G T H C O N S T L I S T > M A X L E N G T H C O N S T A N T L I S T then E R R O R („overflow list of constants") fi; C O N S T A N T L I S T [ L E N G T H C O N S T L I S T ] := ( R E A L , R E A L S Y M B O L ) ; OUTSYMBOLfREAL, LENGTH CONSTLIST); EXIT: end; Die Behandlung des Skalenfaktors haben wir weggelassen.
4.2 Phase II 4.2.0 Übersicht In der zweiten Phase (vgl. Nr. 3.4) suchen wir die Blockstruktur auf und stellen alle Deklarationen und Marken zusammen. Insbesondere achten wir auch darauf, daß alle Deklarationen eines Blockes vor dem ersten Auftreten eines Sprungzieles stehen. Wir verlangen aber nicht, daß alle Deklarationen vor der ersten Anweisung stehen. So ist sichergestellt, daß alle Felddeklarationen nur einmal durchlaufen werden; die Verwendung indizierter Variabler vor der entsprechenden Felddeklaration kann dadurch aber nicht verhindert werden. Als Blöcke behandeln wir alle durch begin und end geklammerten Programmteile
4.2 Phasell
121
und nur diese. Das bedeutet, daß Sprunge in andersartig geklammerte Programmteile (runde und eckige Klammern) möglich sind, die wir nur als zusammengesetzte Anweisungen im Sinne von ALGOL 60 betrachten. Darüber hinaus kontrollieren wir in der vorliegenden Fassung nicht, ob zur Laufzeit durch Sprunganweisungen Deklarationen übersprungen werden. Diese Vereinfachungen dienen lediglich dazu, den Compiler kürzer und für den Anfänger überschaubarer zu halten. In der Abbildung zu 4.0.2 haben wir festgehalten, daß die von der ersten Phase ausgegebene Internversion des Programms Eingabe für die Phase II ist. Als Ausgabe dieser Phase treten die Blockliste und die Symbolliste auf, die wir zusätzlich in der Datei ablegen, die in der ersten Phase die Konstantenliste aufnahm. A l s Hilfsroutinen benötigen wir in dieser Phase:
(a)
(b)
(c) (d)
INSYMBOL als Gegenstück zu dem unter 4.1.0 (c) aufgeführten OUTSYMBOL: Die während der ersten Phase zusammengestellten und in einer Datei abgelegten Symbole werden in der Reihenfolge eingelesen, wie sie von der ersten Phase abgespeichert werden und dem aktuellen Parameter zugewiesen, der dabei auf drei Komponenten ergänzt wird, damit die Kellerroutinen dieser Phase mit denen der späteren Phasen übereinstimmen. OUTIDENTIFIER baut die nach Blöcken geordnete Symbolliste in einer externen Datei auf. Wir benützen dabei die Auslagerungstechnik, wie sie in 3.4.3/3.4.4 erläutert wurde. Als Parameter ist die laufende Nummer anzugeben, unter der der auszugebende Identifikator in der programminternen Symbolliste steht. OUTLISTS schließt die ausgegebene Symbolliste ab und fügt in dieser Datei die Blockliste an. Die Routine ERROR wird wie in der 1. Phase benutzt.
4.2.1 Kellerprozeduren Mehrere Routinen benötigen wir zur Bearbeitung des Arbeitskellers. Diese Routinen schreiben wir sofort in einer Form, in der sie auch während der späteren Phasen verwendet werden. (Z.B. wird der mit LAST DELIMITER zusammenhängende Teil erst ab Phase III benötigt und die MODE-Komponente der Kellersymbole erst in Phase IV.) Wir gehen von folgenden Deklarationen aus: struct stacksymbol = (string GROUP, union (int, string) I N D I C A T I O N , int MODE); [0: M A X S T A C K ] stacksymbol STACK; int LAST D E L I M I T E R , LAST C O U N T E R , LAST, I N D E X IDLIST, F I N A L I N D E X ;
122
4 . Ein Beispiel-Compiler
MAXSTACK sei geeignet vorgegeben, die übrigen Objekte folgendermaßen vorbesetzt:
STACK[0] := EMPTY; LAST := LAST D E L I M I T E R := 0; LAST COUNTER := U N D E F I N E D ; Um im Keller Veränderungen vorzunehmen, benutzen wir nur die folgenden Routinen:
proc PUSHDOWN = (stacksymbol SYMBOL): begin LAST := LAST + 1; if LAST > M A X S T A C K then E R R O R („stack overflow") fi; STACK [LAST] := S Y M B O L ; if GROUP of S Y M B O L = COUNTER then LAST COUNTER := LAST fi; if D E L I M I T E R ( G R O U P of S Y M B O L ) then LAST D E L I M I T E R := LAST fi end; proc POPUP = (): begin if L A S T > 0 then LAST := LAST -1 else E R R O R („popup on empty stack") fi; for I from LAST by -1 to 0 do if DE LIM ITER (GROUP of STACK[I]) then L A S T D E L I M I T E R := I; goto L1 fi; LI: for I from LAST by -1 to 0 do if GROUP of S T A C K [ S T A C K ] = COUNTER then LAST COUNTER := I; goto L2 fi; LAST COUNTER := U N D E F I N E D ; L2: end;9 Als Begrenzer wertet die Routine DELIMITER die Symbole der Gruppen SYNTACTIC, DECLARER und das Symbol EMPTY. COUNTER ist eine neue Gruppe eines speziellen Kellersymbols, das zur Zählung der Indexgrenzenpaare eingeführt wird. Diese Zählung erfolgt durch:
proc COUNT = (): INDICATION of S T A C K f L A S T COUNTER] := I N D I C A T I O N of STACK [LAST COUNTER] + 1;
Die Routine ist nicht optimal, da bekannte Eigenschaften über die Reihenfolge der Speicherung im Keller nicht genutzt werden. 9
4.2 P h a s e n
123
Für Felddeklarationen benötigen wir: proc C O M B I N E D E C L A R A T O R S = (): if I N D I C A T I O N of STACK [ L A S T ] = ROW D E C L A R A T O R and I N D I C A T I O N of S Y M B O L * ROW D E C L A R A T O R then I N D I C A T I O N of S T A C K [ L A S T ] : = ROW OF + I N D I C A T I O N of S Y M B O L else ERROR(„illegal declarer sequence") fi;
In der letzten Routine wird davon ausgegangen, daß ROW OF eine geeignete Zahl zugeordnet ist. Wählt man etwa 5, so wird 1 0 [] int durch 6 [] real durch 7 codiert. Diese Routine läßt sich leicht erweitern, wenn neben Feldern noch andere zusammengesetzte Objektarten verwandt werden sollen.
4.2.2 Symbolliste Eine wesentliche Aufgabe dieser Phase ist die Zusammenstellung aller Deklarationen und Marken. Ziel ist dabei der Aufbau der Symbolliste. Wir benötigen zur Manipulation der programminternen Symbolliste zwei Routinen: - Eintragen eines weiteren Symbols (durch NEW DECLARATION), - Auslagern am Blockende (durch REORDER IDLIST). Die programminterne Symbolliste sei deklariert durch: struct listsymbol = (string I N D I C A T I O N , int M O D E , D I M E N S I O N , ADDRESS); [1: M A X I D L I S T ] listsymbol I D L I S T ;
Die ADDRESS-Komponente wird in der zweiten Phase noch nicht benötigt. Ferner benutzen wir: - INDEX IDLIST als Zählgröße beim Aufbau der Symbolliste IDLIST - FINAL INDEX als Zählgröße in der umgeordneten externen Symbolliste. Beide sind zu Beginn des Programms auf Null zu setzen. Zur Speicherung der Blockstruktur benötigen wir die Blockliste: [ 1 : M A X BLOCK N U M B E R ] struct (int F I R S T S Y M B O L , LAST S Y M B O L , S U R R O U N D I N G BLOCK) B L O C K L I S T ; int C U R R E N T BLOCK, BLOCK N U M B E R ;
10
Siehe Abbildung zu 4.1.2, in der die Codes von int bzw. real mit 1 bzw. 2 angegeben sind.
4. Ein Beispiel-Compiler
124
Dabei zählt BLOCK NUMBER die Gesamtzahl der bisher eröffneten Blöcke, CURRENT BLOCK gibt den Index des gerade bearbeiteten Blockes in der Blockliste an. Beide sind anfangs auf Null zu setzen. proc NEW D E C L A R A T I O N = (string NEW S Y M B O L , int D E C L A R E R ) : begin for I from F I R S T S Y M B O L of B L O C K L I S T [ C U R R E N T B L O C K ] by 1 to I N D E X I D L I S T d o if INDICATION of I D L I S T f l ] = NEW S Y M B O L then E R R O R („double declaration") fi; I N D E X I D L I S T := I N D E X I D L I S T + 1; if I N D E X I D L I S T > M A X I D L I S T then E R R O R („overflow of identifier list") fi; I D L I S T [ I N D E X I D L I S T ] := (NEW S Y M B O L , D E C L A R E R , 0, nil); if R O W E D ( D E C L A R E R ) then D I M E N S I O N of I D L I S T [ I N D E X I D L I S T ] end;
::= INDICATION of S T A C K [ L A S T C O U N T E R ] f i 1 1
ROWED ist eine boolesche Prozedur, die prüft, ob der Parameter ein Felddeklarator ist. In der hier bearbeiteten, eingeschränkten Sprachversion ist dies eine einfache Abfrage. proc R E O R D E R I D L I S T = (): begin int MIN; MIN := F I R S T S Y M B O L of B L O C K L I S T [ C U R R E N T B L O C K ] ; F I R S T S Y M B O L of B L O C K L I S T f C U R R E N T B L O C K ] := F I N A L I N D E X + 1; for I from MIN by 1 to I N D E X I D L I S T d o begin F I N A L I N D E X := F I N A L I N D E X + 1; OUTIDENTIFIER(I)
end;
end; I N D E X I D L I S T := MIN - 1; L A S T S Y M B O L of B L O C K L I S T f C U R R E N T B L O C K ] := F I N A L I N D E X
Diese Routine speichert den zu einem abgeschlossenen Block gehörenden Teil der programminternen Symbolliste in die nach Blöcken geordnete. Die in der Blockliste notierten Eintragungen über Anfang und Ende dieses Teils beziehen sich nun auf die externe Symbolliste; die interne wird um den ausgelagerten Teil gekürzt.
11 Die Konformitätsprüfung ist erforderlich, da die rechte Seite als Union deklariert ist. LAST COUNTER zeigt stets auf ein Element, dessen INDICATION-Komponente von der Art int ist.
4.2 Phasell
125
4.2.3 Programmübersicht und Blockstruktur Die im folgenden zu besprechenden Programm teile 12 erscheinen im Programm nicht in der hier angegebenen Reihenfolge, da wir die Teile zusammenhängend betrachten wollen, die inhaltlich zusammengehören. Um dem Leser den Überblick zu erleichtern, sind die Programmteile fortlaufend numeriert; die Übergangsmatrix in der Abbildung (vgl. auch 1.3.3) gibt an, in welchem Zusammenhang die einzelnen Programmteile ausgeführt werden. Dabei gibt das Symbol in der ersten Spalte das jeweils zuletzt gekellerte Symbol (oder seine Gruppe) und das Symbol in der ersten Zeile das nächste, bereits gelesene Symbol an: 1. Spalte: genau die möglichen Kellersymbole, 1. Zeile: nur die relevanten Eingaben. Die Abfragen, die den leeren Feldern in der Matrix entsprechen, fuhren lediglich zu einem Fehlerausgang und sind im folgenden weggelassen. Zur Bearbeitung der Blockstruktur benötigen wir die folgenden Programmteile, wobei wir die Blockliste entsprechend Nr. 3.4.5 aufbauen: stacksymbol S Y M B O L ; READ N E X T SYMBOL: INSYMBOL(SYMBOL);
Nr.
INDICATION
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
A E A C M D X A B C D X XI Y Z M
MODE ref real ref real ref int ref int label ref int ref int ref int ref int ref real ref int row of ref row of ref row of ref row of ref label
DIMENSION
real int int int
0 0 0 0 0 0 0 0 0 0 0 1 1 2 2 0
Symbolliste zum Beispiel aus Nr. 4.0.1 12 Um die Richtigkeit der Programmstruktur zu gewährleisten, müssen neben den begin-endPaaren auch alle anderen Symbole mit klammernder Wirkung betrachtet werden. Dies geschieht in den späteren Phasen.
126
4. Ein Beispiel-Compiler FIRST
LAST
SURROUNDING
SYMBOL
SYMBOL
BLOCK
1
8
16
2
3
5
1
3
1
2
2
4
6
7
1
Mr INT.
0
Blockliste zum Beispiel aus Nr. 4.0.1
Eingabe Keller begin no more 1 declaration / declarer dclsub
[ identifier statement
begin 1 1 1 1 1 1
end 2
declarer identifier 3 12
2
12 5 11 11
4
14 16
17
[
1
9 9 14 17
18
sonst 19
18
19
11 11 14 15
11 11 14 17
Komma
6
8 10
7 11
17
17
11 13
Abb. zu 4.2.3 Übergangsmatrix der Phase II (7)
if S Y M B O L = ( S Y N T A C T I C , BEGIN, nil) then PUSH D O W N (SYMBOL); BLOCK N U M B E R := BLOCK N U M B E R + 1; if BLOCK N U M B E R > M A X BLOCK N U M B E R then ERROR („overflow of blocklist") fi; BLOCK LISTfBLOCK N U M B E R ] := ( I N D E X I D L I S T + 1, nil, C U R R E N T BLOCK); C U R R E N T BLOCK := BLOCK N U M B E R ; goto R E A D N E X T S Y M B O L
© f i ; if ( S T A C K [ L A S T ] = ( S Y N T A C T I C , B E G I N , nil) or S T A C K f L A S T ] = ( S Y N T A C T I C , NO M O R E D E C L A R A T I O N , nil)) and S Y M B O L = ( S Y N T A C T I C , E N D , nil) then R E O R D E R I D L I S T ; POPUP; C U R R E N T BLOCK := S U R R O U N D I N G BLOCK of B LOCK L IST[C U R R E N T B L O C K ] ; goto R E A D N E X T S Y M B O L fi;
4.2 Phasell
127
Das Symbol NO MORE DECLARATION wird eingeführt, um Deklarationen nach einer Marke zu verhindern. (Siehe auch 4.2.6.)
4.2.4 Einfache Deklarationen Wir betrachten als nächstes diejenigen Teile des Algorithmus, die sich mit den Deklarationen befassen. Dabei klammern wir die Felddeklarationen zunächst noch aus. Zum Verständnis der hier angegebenen Programmstücke muß man jedoch wissen, daß nach Abarbeitung der abschließenden eckigen Klammer einer Felddeklaration im Keller stehen: — an vorletzter Stelle: ein Symbol der neu eingeführten Gruppe COUNTER, das die Anzahl der Indexgrenzenpaare angibt und im Keller unter dem Index LAST COUNTER steht, - an letzter Stelle: ein Symbol der Gruppe DECLARER, das zuletzt gekellert wurde. Zur Bearbeitung der Deklaratoren dienen dann die beiden folgenden Programmstücke: (T)
if STACK [LAST] = (SVNTACTIC, BEGIN, nil) and GROUP of SYMBOL = DECLARER then PUSHDOWN (SYMBOL); goto READ NEXT SYMBOL
®
fi;
if GROUP of STACK [LAST] = DECLARER and GROUP of SYMBOL = DECLARER then COMBINE DECLARATORS; goto READ NEXT SYMBOL fi; Das nächste Programmstück verläßt (der Übersichtlichkeit halber) die sonst eingehaltene Methode, nur die Marken READ NEXT SYMBOL und REPETITION anzuspringen. Es bearbeitet unmittelbar die Liste aller Identifikatoren, die auf einen Deklarator folgt. Deklarationen dürfen sowohl durch Komma als auch durch Semikolon voneinander getrennt sein 1 3 . Sind zwei Deklarationen mit dem gleichen Deklarator durch Komma voneinander getrennt, so darf die Wiederholung des Deklarators unterbleiben (Sprung nach LIST).
13 V o n der möglichen Vertauschung der Reihenfolge oder der parallelen Verarbeitung, falls das Komma Trennzeichen ist, machen wir jedoch keinen Gebrauch.
128
(T)
4. Ein Beispiel-Compiler
if G R O U P of S T A C K [ L A S T ] = D E C L A R E R and GROUP of S Y M B O L = I D E N T I F I E R then LIST: NEW D E C L A R A T I O N ( I N D I C A T I O N of S Y M B O L , R E F + I N D I C A T I O N of S T A C K [ L A S T ] ) ; INSYMBOL(SYMBOL); if S Y M B O L = ( S Y N T A C T I C , COMMA, nil) then I N S Y M B O L (SYMBOL); if GROUP of S Y M B O L = I D E N T I F I E R then goto L I S T fi fi; POPUP; comment Hier wird der Deklarator aus dem Keller entfernt. Handelt es sich um eine Felddeklaration, muß auch der Zähler für die Grenzenpaare entfernt werden: comment; if L A S T = L A S T C O U N T E R then POPUP fi; if S Y M B O L = ( S Y N T A C T I C , SEMICOLON, nil) then goto R E A D N E X T S Y M B O L else if G R O U P of S Y M B O L = D E C L A R E R or S Y M B O L = ( S Y N T A C T I C , SUB, nil) then goto R E P E T I T I O N else ERROR(„missing semicolon") fi fi fi;
In NEW DECLARATION wird auch der Wert des Zählers der Grenzenpaare, d.h. die Dimension des Feldes, in die Symbolliste eingetragen. Analog ROW OF sei auch REF eine feste ganze Zahl zugewiesen.
4.2.5 Feldvereinbarungen Die Feldvereinbarungen erwarten wir in der ALGOL 68-Notation: [ u ^ o v . . . u r : o r ] mode identifier Um unnötige Fallunterscheidungen in den einzelnen Programm teilen zu vermeiden, verwandeln wir SUB in DCLSUB, sobald erkannt wurde, daß es den Beginn einer Felddeklaration darstellt. Gleichzeitig wird im Keller eine Eintragung vorgenommen, die die Anzahl der Indexgrenzenpaare zählt:
4.2 Phasen
(T)
©
129
if S T A C K [ L A S T ] = (SYNTACTIC, BEGIN, nil) and S Y M B O L = (SYNTACTIC, SUB, nil) then PUSH DOWN ((COUNTER, 1, nil)); PUSH DOWN ((SYNTACTIC, DCLSUB, nil)); goto R E A D N E X T S Y M B O L fi;
®
if S T A C K [ L A S T ] = (SYNTACTIC, DCLSUB, nil) and S Y M B O L = (SYNTACTIC, COMMA, nil) then COUNT; goto R E A D N E X T S Y M B O L fi; if STACK [LAST] = (SYNTACTIC, DCLSUB, nil) and S Y M B O L = (SYNTACTIC, BUS, nil) then POPUP; PUSH DOWN ((DECLARER, ROW D E C L A R A T O R , nil)); goto R E A D N E X T S Y M B O L fi;
Die einzelnen Indexgrenzenpaare zwischen den eckigen Klammern und Kommata interessieren noch nicht. Jedoch ist ein Mitzählen evtl. enthaltener eckiger Klammern erforderlich 1 4 :
(T)
®
if (STACK[LAST] = (SYNTACTIC, DCLSUB, nil) or S T A C K [ L A S T ] = (SYNTACTIC, SUB, nil)) and S Y M B O L = (SYNTACTIC, SUB, nil) then PUSH DOWN (SYMBOL); goto R E A D N E X T S Y M B O L fi;
if STACK [LAST] = (SYNTACTIC, SUB, nil) and S Y M B O L = (SYNTACTIC, BUS, nil) then POPUP; goto R E A D N E X T S Y M B O L fi; Symbole werden nicht beachtet: Alle anderen (n)
if STACK [LAST] = (SYNTACTIC, DCLSUB, nil) or S T A C K [LAST] = (SYNTACTIC, SUB, nil) then goto R E A D N E X T S Y M B O L fi;
Am Rande sei vermerkt, daß keine Änderung dieser Phase erforderlich ist, wenn wir virtuelle Felddeklaratoren (Spezifikatoren in der ALGOL 68-Terminologie) zulassen, z.B. [,] int. 14
Die begin-end-Paare werden durch Nr. 4.2.3 bereits erfaßt.
130
4. Ein Beispiel-Compiler
4.2.6 Marken Neben den Deklarationen benötigen wir, damit die Symbolliste vollständig ist, noch die Marken. Da auch Anweisungen mit einem Identifikator beginnen können, muß zur Erkennung der Marken der Identifikator zunächst gekellert und das nächste Eingabezeichen betrachtet werden 1 5 : (T2)
m )
if (STACK[LAST] = (SYNTACTIC, BEGIN, nil) or STACK [LAST] = (SYNTACTIC, NO MORE DECLARATION, nil)) and GROUP of SYMBOL = IDENTIFIER then PUSHDOWN (SYMBOL); goto READ NEXT SYMBOL fi; if GROUP of STACK [LAST] = IDENTIFIER and SYMBOL = (SYNTACTIC, COLON, nil) then NEW DECLARATION) INDICATION of STACK [LAST], LABEL); POPUP; if STACK[LAST] * (SYNTACTIC, NO MORE DECLARATION, nil) then POPUP; PUSH DOWN ((SYNTACTIC, NO MORE DECLARATION, nil)) fi; goto READ NEXT SYMBOL fi;
Der Austausch von BEGIN gegen NO MORE DECLARATION im Keller erfolgt, damit kein Sprung in den Deklarationsteil möglich ist 1 6 . Falls keine Marke vorliegt, ist der Identifikator die Einleitung einer Anweisung: (l4)
if GROUP of STACK [LAST] = IDENTIFIER then POPUP; PUSH DOWN ((SYNTACTIC, STATEMENT, nil)); goto REPETITION fi;
Dieser Fall leitet bereits über zum nächsten Abschnitt.
4.2.7 Anweisungen In der vorliegenden Phase interessieren wir uns noch nicht für das Innere von Anweisungen, sofern dort keine weiteren Blöcke begonnen werden:
15 Bei der Erkennung der Marken würde die Einführung weiterer Zustände bei dem in 4 . 1 . 3 erwähnten Kellerautomaten einen geringen Vorteil bringen. 16 Soll auch umgekehrt kein Sprung aus dem Deklarationsteil möglich sein, so ergibt sich die Schwierigkeit, daß nicht kontrollierbar ist, ob ein Prozeduraufruf zum Verlassen des Deklarationsteils fuhrt.
4.3 Phase III
131
if S T A C K f L A S T ] = (SYNTACTIC, S T A T E M E N T , nil) and S Y M B O L = (SYNTACTIC, S E M I C O L O N , nil) then POPUP; goto R E A D N E X T S Y M B O L fi; if S T A C K [LAST] = (SYNTACTIC, S T A T E M E N T , nil) and S Y M B O L = ( S Y N T A C T I C , END, nil) then POPUP; goto R E P E T I T I O N fi; Das Innere einer Anweisung wird überlesen: (iT)
if S T A C K [LAST] = (SYNTACTIC, S T A T E M E N T , nil) then goto R E A D N E X T S Y M B O L fi;
Damit der Sonderfall einer leeren Anweisung erfaßt ist, benötigen wir: (l?)
if ( S T A C K f L A S T ] = (SYNTACTIC, BEGIN, nil) or S T A C K [LAST] = (SYNTACTIC, NO M O R E D E C L A R A T I O N , nil)) and S Y M B O L = ( S Y N T A C T I C , S E M I C O L O N , nil) then goto R E A D N E X T S Y M B O L fi;
Alle Anweisungen und Ausdrücke, die nicht mit einem Identifikator beginnen, z.B. öffnende Klammer oder goto, müssen ebenfalls überlaufen werden; dies wird erreicht durch: (l9)
if S T A C K f L A S T ] = (SYNTACTIC, BEGIN, nil) or S T A C K f L A S T ] = (SYNTACTIC, NO M O R E D E C L A R A T I O N , nil) then PUSH DOWN ((SYNTACTIC, S T A T E M E N T , nil)); goto R E A D N E X T S Y M B O L fi;
4.3 Phase III 4.3.0 Übersicht In der dritten Phase werden erstmals Informationen der von Phase I generierten Programmfassung verändert. Außerdem werden die von Phase II erstellte, geordnete Symbolliste und die Blockliste benutzt. Die Phase erzeugt eine neue Programmfassung, in der folgende Änderungen vorgenommen sind:
4. Ein Beispiel-Compiler
132
(a)
(b)
(c)
(d)
(e)
In der internen Programmversion werden die Identifikatoren durch einen Verweis auf die Stelle der Symbolliste ersetzt, die dem jeweiligen Identifikator entspricht. Felddeklarationen und indizierte Variable werden durch Prozeduraufrufe ersetzt, die zur Laufzeit den Informationsvektor aufbauen bzw. die Speicherabbildungsfunktion auswerten (vgl. 2.5.). Wir fügen den Versorgungsblock jeweils unmittelbar an den Prozeduraufruf an, wobei wir das Arbeiten mit dem Arbeitskeller der vorliegenden Phase vereinfachen, wenn wir die Indexgrenzen bzw. Indizes in umgekehrter Reihenfolge in den Versorgungsblock eintragen. Als letztes folgt im Versorgungsblock ein Befehl, der die Adresse des Informationsvektors liefert. Um diesen Befehl zu finden, muß im Aufruf die Anzahl der Indexgrenzen bzw. der Indizes angegeben werden. Die Deklarationen für einfache Variable erscheinen in der ausgegebenen, neuen Programmfassung nicht mehr. Alle Deklarationen fuhren zur Eintragung einer Adresse in der Symbolliste, die relativ zum Anfang des Datenbereichs zu verstehen ist. begin und end werden durch Unterprogrammsprünge auf die dynamische Speicherverwaltung ersetzt. In Phase IV haben sie nur noch die Funktion runder Klammern. Daher werden neben den Unterprogrammaufrufen runde Klammern in die neue Programmfassung aufgenommen. goto wird durch einen Sprungbefehl mit noch offenem Sprungziel ersetzt, die Marken durch einen speziellen „Befehl" LOC, der daraufhinweisen soll, daß später die Adresse dieser Stelle festgestellt werden muß. Gleichzeitig wird hier ein Unterprogrammsprung zur dynamischen Speicherverwaltung eingeschoben (vgl. 2.6.).
Im Zusammenhang mit der Blockstruktur und den Indexausdrücken schieben wir an einzelnen Stellen Kommandos ein, die die 4. Phase veranlassen, an diesen Stellen Informationen einzutragen, die der 3. Phase noch nicht bekannt sind. Dies kann erst bei der Beschreibung der Phase IV vollständig klar werden. Die Routinen INSYMBOL und OUTSYMBOL sind analog zur Phase II aufgebaut; OUTINSTRUCTION und OUTCOMMAND unterscheiden sich von OUTSYMBOL nur durch die Anzahl der Parameter und die Tatsache, daß sie ihre Ausgaben durch INSTRUCTION bzw. COMMAND kennzeichnen. 4.3.1 Kellerprozeduren Neben den bereits in 4.2.1 betrachteten Routinen zur Verwaltung des Kellers benötigen wir weitere: Im Zusammenhang mit dem Passieren von Ausdrücken, die sich auf der Position von Indizes oder Indexgrenzen befinden, schieben wir vor einer bereits gekellerten Variablen oder Konstanten den Vermerk STATEMENT ein:
4.3 Phase III
133
proc I N S E R T = (stacksymbol SYMBOL): begin stacksymbol A U X I L I A R Y ; A U X I L I A R Y := STACK [LAST] POPUP; PUSH DOWN (SYMBOL); PUSH DOWN ( A U X I L I A R Y ) end; In den Programmstücken 17 und 18 werden wir umgekehrt das vorletzte Kellerelement entfernen: proc E R A S E = (): if L A S T * LAST D E L I M I T E R then STACK [LAST D E L I M I T E R ] POPUP else ERROR(„missing operand") fi; Im Zusammenhang mit der Erstellung von Versorgungsblöcken kopieren wir die letzten Elemente des Kellers in die neue Ausgabedatei: proc COPY STACK = (int NUMBER): from 1 by 1 to N U M B E R do begin O U T C O M M A N D (PAR A M E T E R , G R O U P of STACKfLAST], I N D I C A T I O N of STACK[LAST]); POPUP end; Das letzte Kellersymbol wird als erstes, das mit der Kellertiefe NUMBER als letztes weggespeichert. PARAMETER gibt der Kommando-Interpretation der nächsten Phase einen Hinweis, daß hier ein Parameter eines Versorgungsblockes fertigzustellen ist.
4.3.2 Identifizierung der Identifikatoren und Adressenzuweisung Die Routine INSYMBOL ist in dieser dritten Phase beim Lesen von Identifikatoren durch die nun mögliche Identifizierung erweitert, d.h. in der zweiten Komponente des Eingabesymbols wird die Bezeichnung durch den entsprechenden Index in der Symbolliste ersetzt. Dabei werden die in der zweiten Phase zusammengestellten Listen BLOCKLIST und IDLIST benutzt:
134
4. Ein Beispiel-Compiler
proc IDENT IDENTIFICATION = (): begin intCB, MIN, MAX; CB := CURRENT BLOCK; REP: MIN := FIRST SYMBOL of BLOCKLIST[CB]; MAX := LAST SYMBOL of BLOCKLIST[CB]; for I from MIN by 1 to MAX do if INDICATION of SYMBOL = INDICATION of IDLIST[I] then INDICATION of SYMBOL := I; goto EXIT fi; CB := SURROUNDING BLOCK of BLOCKLIST[CB]; if CB * 0 then goto REP fi; ERROR („undeclared identifier"); EXIT: end; Man vergleiche hierzu 3.4.5. Die Symbolliste IDLIST wird auch von einer Prozedur benutzt, die beim Bearbeiten der Deklarationen Speicherplatzzuteilungen vornimmt. Wir gehen hier davon aus, daß Objekte der Arten ref int und ref real jeweils einen Speicherplatz benötigen; für die Informationsvektoren von Feldern reservieren wir den nach 2.5.6
Nr.
INDICATION
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
A E A C M D X A B C D X XI Y Z M
MODE ref real ref real ref int ref int label ref int ref int ref int ref int ref real ref int row of ref row of ref row of ref row of ref label
real int int int
DIMENSION
ADDRESS
0 0 0 0 0 0 0 0 0 0 0 1 1 2 2 0
12 13 10 11 undefined 10 11 0 1 2 3 4 5 6 8 undefined
Abb. zu 4.3.2. Symbolliste zum Beispiel aus Nr. 4 . 0 . 1 (nach der 3. Phase)
4 . 3 Phase III
135
minimal zulässigen Bereich von r Speicherplätzen, wobei r die Anzahl der Indexpositionen ist (virtuelle Anfangsadresse und r-1 Distanzgrößen). Ferner ist zum folgenden anzumerken, daß wir entsprechend 2.6.1 diese Variablen und die Informationsvektoren in einen scheinbar statischen Bereich legen, da wir Prozeduren außer Betracht lassen: proc A D D R E S S A S S I G N M E N T = (): begin int I; I ::= I N D I C A T I O N of S Y M B O L ; comment I N D I C A T I O N of S Y M B O L kann in Fällen, die an dieser Stelle keine Rolle spielen, von der Art string sein. comment; A D D R E S S of IDLISTfl] := N E X T A D D R E S S ; if D I M E N S I O N of IDLISTfl] = 0 then N E X T A D D R E S S := N E X T A D D R E S S + 1 else N E X T A D D R E S S := N E X T A D D R E S S + D I M E N S I O N of I D L I S T f l ] fi end; NEXT ADDRESS muß mit 0 initialisiert sein. 4.3.3 Programmübersicht Auch diese Phase arbeitet als Kellerautomat; die Übergangsmatrix ist in der Abbildung angegeben. Viele Programmstücke fragen jedoch nicht das zuletzt gekellerte Symbol ab, sondern den zuletzt gekellerten Begrenzer. Die in der Übergangsmatrix mit einem Stern gekennzeichneten Programmteile fragen beides ab. Da einige der in 4 . 3 . 4 - 4 . 3 . 9 angegebenen Programmstücke nicht alle Bedingungen bezüglich Keller und folgendem Symbol abfragen, ist die Reihenfolge nicht beliebig. (In diesem Sinn ist auch ANY zu verstehen.) Eine getestete Reihenfolge ist: READ NEXT SYMBOL: REPETITION:
I N S Y M B O L (SYMBOL);
und dann die Programmstücke BEGIN - END (2) OPEN - CLOSE (27) BEGIN - DECLARER (7) DECLARER - IDENTIFIER (8) DECLARER - COMMA (9) DECLARER - SEMICOLON (10) B E G I N - S U B (12) INDEX - IDENTIFIER/CONSTANT (16)
136
4. Ein Beispiel-Compiler
INDEX - OPERAND - COLON/COMMA (17) INDEX - OPERAND - BUS (18) DCLSUB - LIST OF OPERANDS - BUS (13) DECLARER - DECLARER (11) GOTO - IDENTIFIER (6) ANY - IDENTIFIER (3) BEGIN/OPEN - IDENTIFIER - COLON (4) SUB - LIST OF OPERANDS - BUS (15) INDEX - ANY (19) IDENTIFIER - SUB (14) IDENTIFIER - ANY (20) STATEMENT - CLOSE/END (21) STATEMENT - COMMA/BUS/COLON (22) STATEMENT - SEMICOLON (23) STATEMENT - OPERAND - ANY (24) BEGIN - GOTO (5) ANY - BEGIN (1) ANY - OPEN (26) ANY - ANY (27) Zu Beginn sind die üblichen Größen zu initialisieren. Die leeren Felder der Ubergangsmatrix führen zu einer Fehlermeldung.
1 1 1 1
26 26
19
19
1
22
26
12
11 14 19
22
13 15 18 20
5 5
3 3 8
16 6 3
16
19
9
O o g CO 10
17 20
23
ANY
o
CO g
6 TIMES
> 3 *D 11
XEDNI INDEX SAVE ACCUMULATOR USE INFVECTOR PARAMETER LOAD ADDRESS ARRAY ELEMENT XEDNI SAVE ACCUMULATOR CREATEINFVECTOR PARAMETER PARAMETER PARAMETER PARAMETER LOAD ADDRESS COPY INFVECTOR LOAD ADDRESS
INT VAR
1 1 5 13
>
X[l]
4 EXPR INT EXPR INT VAR
7 6
VAR
8
1 Deklaration Y ^ Dekla? ration > Z
4.3.9 Ausdrücke und Anweisungen Wie schon in der vorhergehenden Phase werden alle übrigen Teile des eingegebenen Programms, z.B. die arithmetischen Ausdrücke, unverändert von der Eingabe in die Ausgabe übernommen: (21)
if GROUP of S T A C K [ L A S T ] = IDENTIFIER then 0 U T S Y M BOL(STACK[ LAST]); POPUP; if STACK [LAST] * (SYNTACTIC, STATEMENT, nil) then PUSH DOWN ((SYNTACTIC, STATEMENT, nil)) fi; goto REPETITION fi;
4.3 Phase III
(22)
if S T A C K [LAST] = (SYNTACTIC, STATEMENT, nil) and (SYMBOL = (SYNTACTIC, END, nil) or S Y M B O L = (SYNTACTIC, CLOSE, nil) then POPUP; goto REPETITION fi,
@
if S T A C K [ L A S T ] = (SYNTACTIC, S T A T E M E N T , nil) and S Y M B O L = (SYNTACTIC, SEMICOLON, nil) then POPUP; OUTSYMBOL(SYMBOL); goto R E A D N E X T S Y M B O L fi;
@
if STACK [LAST D E L I M I T E R ] = (SYNTACTIC, S T A T E M E N T , nil) and LAST D E L I M I T E R * LAST then OUTSYMBOL(STACK[LAST]); POPUP; goto REPETITION fi; Damit die Klammerstruktur auch innerhalb der Indexausdrücke stimmt, sind noch nötig:
(25)
if STACK [LAST] = (SYNTACTIC, OPEN, nil) and S Y M B O L = (SYNTACTIC, CLOSE, nil) then POPUP; O U T S Y M BO L (SYMBOL); goto R E A D N E X T S Y M B O L fi;
@
if S Y M B O L = (SYNTACTIC, OPEN, nil) then PUSH DOWN (SYMBOL); OUTSYM BO L (SYMBOL); goto R E A D N E X T S Y M B O L fi;
@
OUTSYMBOL(SYMBOL); goto R E A D N E X T S Y M B O L ;
147
148
4. Ein Beispiel-Compiler
4.4 Phase IV 4.4.0 Übersicht In der vierten Phase werden die arithmetischen Operationen und die Wertzuweisungen bearbeitet. In dieser Phase tritt erstmals der Zwischenergebniskeller in Erscheinung; an den Stellen, wo wir ihn bereits in der vorhergehenden Phase benötigt hätten, haben wir uns mit den Kommandos geholfen, die wir in den internen Programmtext (Ausgabe der Phase III) eingefügt haben. Die in den vorangehenden Phasen standardmäßig benutzten Prozeduren und der Keller STACK bleiben erhalten. Nur einige Änderungen sind erforderlich 24 : - INSYMBOL wird erweitert durch die Abfrage, ob ein Kommando oder eine Instruktion gelesen wurde. Findet diese Prozedur in der Ausgabe von Phase III ein Kommando, so wird mit den restlichen Komponenten die Prozedur COMMAND INTERPRETATION aufgerufen und anschließend INSYMBOL wiederholt (siehe 4.4.1). Wird eine Instruktion gelesen, so wird diese unverändert in die Ausgabe der Phase IV übernommen und INSYMBOL wiederholt. Ebenso wird mit Zeilennummern verfahren. — Neben der in Phase II erstellten Symbolliste wird die Liste der zulässigen Aktionen zu Beginn der Phase IV von einer Datei eingelesen. Die Liste der Aktionen ist dabei folgendermaßen deklariert: struct action = (string I N D I C A T I O N , int ADIC, LMODE, RMODE, RESMODE, string CODE, INVCODE, int NEW LMODE, NEW RMODE, P R I O R I T Y ) ; [1: M A X A C T L I S T ] action ACTLIST;
Die Komponente INDICATION enthält die Bezeichnung des Operators, wie sie in der 1. Phase zusammengestellt wurde. ADIC gibt die Anzahl der Operanden 25 an; wir haben dies vorgesehen, obwohl in der vorliegenden Version nur dyadische (binäre) Operatoren behandelt werden. (Die Berücksichtigung der monadischen Operatoren kann analog 2.2.3 erfolgen und würde keine neuen Gesichtspunkte ergeben.) LMODE und RMODE schreiben die Art des linken und des rechten Operanden vor; RESMODE liefert die Art des Ergebnisses. CODE und INVCODE enthalten die Befehlsteile der Maschinenbefehle für den Fall, daß sich der linke bzw. der rechte Operand im Akkumulator befindet (Vgl. 2.2.2.). Diese Komponenten bleiben frei, wenn zunächst eine Artanpassung erforderlich ist. Ist NEW
24 Da jede Phase als ein Programm für sich geschrieben ist, können die gleichen Identifikatoren verwandt werden. Dies gilt auch für die Marken R E A D N E X T SYMBOL und REPETITION. 25 1 = monadisch, 2 = dyadisch
4 . 4 Phase IV
149
LMODE oder NEW RMODE von 0 verschieden, so bedeutet dies, daß auf der linken bzw. rechten Operandenposition eine Artanpassung nötig ist, die aufgrund des eingeschränkten Sprachumfangs nur von int nach real gehen k a n n 2 6 . Schließlich gibt P R I O R I T Y die Vorrangstufe des Operators an. Tabelle zu 4.4.0. ACTLIST
Z
O H
o) Wörter w 0 , w x , . . ., w n gibt mit x = wa
(Vl3144, es gilt jedoch beispielsweise nicht 1 -> 123. 8.2.5 Chomsky-Grammatik Bei der Ableitung der Wörter einer Wortmenge ist es häufig sinnvoll, Hilfssymbole zu benutzen, die in den endgültig abgeleiteten Wörtern nicht mehr auftreten. " Vgl. [Chomsky 1959 a; b]. An Lehrbüchern auf diesem Gebiet seien erwähnt: [Ginsburg 1966; Groß, Lentin 1971; Hopcroft, Ullman 1969; Hotz, Claus 1972; Maurer 1969]. Wir geben hier nur einen kleinen Ausschnitt aus der Theorie.
222
8. Formale Sprachen
Ohne Beschränkung der Allgemeinheit ist es möglich, alle Wörter aus einem einzigen solchen Symbol abzuleiten: Eine formale Sprache (Chomsky-Sprache) wird beschrieben durch ein Quadrupel G = (T G , N G , P G , S G ) mit (a) (b) c
( )
T g und N G sind Alphabete, T G n N G = , V G := T G U N G . S
G
GN
G
( V G , P G ) = o i s t ein Semi-Thue-System,
G
definiert durch
G nennen wir dann Chomsky-Grammatik der formalen Sprache. Die Elemente von T g heißen terminale Symbole, die von N G nichtterminale, V G ist das Alphabet der Grammatik, S G das Startsymbol (Satzsymbol, Axiom). Der Index G fällt weg, wenn keine Mißverständnisse möglich sind. Wörter, die nur aus terminalen Symbolen bestehen, heißen terminale Wörter. Ist G eine Chomsky-Grammatik, so heißt L(G) :=
Wortmenge oder Satzmenge der durch G gegebenen Sprache. Jedes wEV G mit * S G w heißt Satz form. Zwei formale Sprachen heißen schwach äquivalent, wenn ihre Satzmengen übereinstimmen. Gelegentlich benötigen wir Begrenzungszeichen i— und —i. Dann definieren wir die Satzmenge durch 1— S H -*• w H und müssen bei den Produktionen darauf achten, daß die Begrenzungszeichen nicht gelöscht oder weitere erzeugt werden.
8.3 Chomsky-Hierarchie 8.3.1 Notwendigkeit von Einschränkungen Man kann zeigen, daß das „Wortproblem" für den allgemeinsten Fall der formalen Sprachen nicht entscheidbar ist, d.h.: Es gibt keinen Algorithmus, der für alle G = (T,N,P,S) *
und alle w£V* entscheidet, ob S G w gilt oder nicht. Daher ist auch die Prüfung der syntaktischen Korrektheit mit einem allgemeinen Verfahren unmöglich. Das legt nahe, die Produktionen spezielleren Formvorschriften zu unterwerfen. Allgemein legen wir fest: wird für Produktionen eine Formvorschrift (F) definiert, so heißt eine Grammatik von Typ (F), wenn alle Produktionen der Grammatik (F) erfüllen. Eine Satzmenge heißt (F)-Menge, wenn es eine sie definierende Grammatik vom Typ (F) gibt. Folgt aus ( F 2 ) stets ( F j ) , d.h. ist F 2 eine Verschärfung von ( F ^ , so ist jede (F 2 )-Menge auch (F t )-Menge. Existiert zu einer Satzmenge eine Grammatik vom Typ ( F , ) , so kann es sehr wohl auch eine schwach äquivalente Grammatik vom Typ ( F 2 ) geben, die schärferen Bedingungen genügt.
223
8.3 Chomsky-Hierarchie
8.3.2 Kontextsensitive und monotone Grammatiken Bereits die erste Einschränkung, die [Chomsky 1959a; b] angab, führt zu einer Sprachklasse, in der das Wortproblem entscheidbar ist: Eine Produktion (u,v) heißt kontextsensitiv, wenn gilt: (3cj,c2eV*)
(3AeN) (JyeV+) (u =CJAC2 a v =
ciyc2)
Beim Ableitungsschritt mit einer kontextsensitiven Produktion wird statt einer Teilkette nur ein Symbol ersetzt; diese Ersetzung erfolgt jedoch nur, wenn das Symbol in einem vorgegebenen Kontext erscheint. Als Kontext dürfen beliebige Zeichenketten Ci und c 2 vorgeschrieben werden.9 Ein wesentlicher Unterschied gegenüber dem allgemeinen Fall besteht darin, daß beim Ableitungsprozeß niemals eine Zeichenkette auftreten kann, die kürzer ist als die vorhergehende {monotone Grammatik). Man kann zeigen, daß jede monotone Satzmenge kontextsensitiv ist und umgekehrt und daß das Wortproblem in dieser Klasse entscheidbar ist. Daraus folgt bereits, daß es formale Sprachen mit nicht-kontext-sensitiver Satzmenge gibt. Ein typischer Vertreter der kontextsensitiven Mengen ist die Menge {a"bnan:
n>l)>
die durch folgendes Produktionensystem definiert werden kann: {(S,aSA), (S,aB), (BA, bBa), (aA, Aa), (B, ba)}> Der Ableitungsprozeß der einzelnen Wörter läuft folgendermaßen ab: es wird entweder gleichaB und darausaba (n=\) oder zunächst anBAn'1 (n>l) erzeugt. Dann werden am linken Rand des Teilworts BA"'1 in gleicher Anzahl b und a erzeugt, wobei jeweils ein A entfernt und das nächste durch Vertauschen mit den erzeugten a an dasB herangeführt wird. Zum Schluß wird das B durch ba ersetzt. Eine Produktion heißt kontextsensitiv mit Löschung, wenn gilt (3cltc2eV*)
(3AEN) (3y&V*) (U=CJAC2 *
v
=
ciyc2).
Der Unterschied besteht also darin, daß y das leere Wort sein darf und somit im Ableitungsprozeß Verkürzungen möglich sind. Jede Satzmenge einer formalen Sprache ist bereits Satzmenge einer kontextsensitiven mit Löschung. 8.3.3 Kontextfreie Grammatiken Von besonderer Bedeutung im Bereich der Programmiersprachen sind die Grammatiken, bei denen in der Produktion die Ersetzung eines Symbols nicht von einem Kontext abhängt: Eine Produktion (u,v) heißt kontextfrei, wenn gilt 9
Der von unserer Regelung aus Nr. 8 . 1 . 4 abweichende Buchstabe c soll an „ c o n t e x t " erinnern.
224
8. Formale Sprachen
uGN X vGV+, und sie heißt kontextfrei mit Löschung, wenn v€V* sein darf. Da die Ersetzung eines Symbols nun in beliebigem Kontext erfolgen darf, ist ein Zusammenhang zwischen Ersetzungen an verschiedenen Stellen einer Zeichenkette nicht herstellbar. Man kann zeigen, daß das Beispiel von 8.3.2 nicht kontextfrei dargestellt werden kann. Die Klasse der kontextfreien Sprachen ist daher echt in der Klasse der kontextsensitiven enthalten. Ein typisches Beispiel für eine kontextfreie Sprache ist {anbn
:n>l}
mit der Produktionenmenge: 4/S, aSb), (S, ab)Y oder in der bekannten Notation: S: aSb; ab. Die kontextfreien Sprachen mit Löschung unterscheiden sich nur dadurch von den kontextfreien, daß ihre Wortmengen e enthalten können. 8.3.4 Lineare Grammatiken Die Stelle eines Wortes, wo während des Ableitungsprozesses Ersetzungen vorgenommen werden können, ist bei kontextfreien Grammatiken dann eindeutig bestimmt, wenn in jeder Satzform höchstens ein nichtterminales Symbol auftritt. Dies ist der Fall, wenn auf der rechten Seite jeder Produktion höchstens ein nichtterminales Symbol auftritt: Eine Produktion (A,v)6P heißt linear, wenn gilt: AGN A v€T*NT* U T + und linear mit Löschung, wenn v€T* sein darf. Das in 8.3.3 angegebene Beispiel einer kontextfreien Sprache ist bereits linear. Ein Beispiel einer kontextfreien, aber nicht linearen Menge ist dagegen {wjwj
D
D
c w2w2 •' vv^, u>2 G^a,b}*\.
Im Zusammenhang mit der Automatentheorie tritt als weiterer Spezialfall die Klasse der einseitig linearen Sprachen auf, die den endlichen, erkennenden Automaten entsprechen (s. Kap. 9): Eine Produktion (A, v) heißt rechtslinear, wenn gilt: AeN * vGT*NUTrh und linkslinear, wenn gilt AGNX
vGNT*UT*
bzw. rechtslinear (linkslinear) mit Löschung, wenn v€T* sein darf. Die Ableitung
225
8.4 Kontextfreie Grammatiken
kann also nur am rechten oder linken Ende des Wortes Veränderungen bewirken. Man kann zeigen, daß die Klasse der rechtslinearen mit der der linkslinearen Mengen übereinstimmt. Wir nennen diese Klasse die einseitig linearen Mengen. Die einseitig linearen Mengen mit Löschung heißen gelegentlich reguläre Mengen. Die in 8.3.3 angegebene lineare Menge ist nicht einseitig linear. Ein typisches Beispiel fiir eine einseitig lineare Menge ist die in Nr. 3.1.2 definierte Menge der Identifikatoren.
8.4 Kontextfreie Grammatiken 8.4.1 Kontextunabhängigkeit der Ableitungen Wie schon bemerkt, spielen die kontextfreien Grammatiken bei der Definition von Programmiersprachen eine besondere Rolle. Die Unabhängigkeit der Ableitungsschritte vom Kontext erlaubt es, Zerlegungen eines Wortes in daraus ableitbaren Wörtern wiederzufinden, d.h. ist G eine kontextfreie Grammatik (mit Löschung), y = y i y 2 • • • y n e i n Wort und z ein aus y ableitbares Wort, so gibt es (evtl. leere) Wörter Zj, z 2 , . . . , z n mit z = zxz2
• - • z„
und (VKKn)
(yt
•
z,/
Man kann also z so zerlegen, daß bereits jedes Teilwort aus dem entsprechenden Teilwort von y ableitbar ist. Während diese Aussage typisch für die kontextfreien Sprachen ist und für nicht kontextfreie Sprachen i.a. nicht gilt, ist die Umkehrung ¡tt 4k (Vlschon von einer kontextfreien Grammatik erzeugt, d.h. der einzige Unterschied liegt darin, daß kontextfreie Sprachen mit Löschung das leere Wort enthalten können. Enthält insbesondere L das leere Wort nicht, so kann L auch von einer kontextfreien Grammatik (ohne Löschung) erzeugt werden. Bei der Definition von Programmiersprachen können wir also auf
226
8. Formale Sprachen
Produktionen mit e als rechter Seite zurückgreifen, ohne die Klasse der kontextfreien Mengen (ohne Löschung) zu verlassen, wenn nur gesichert ist, daß S ^ e *nicht gilt 10 . Ob S -»• e, d.h. e€L(G) gilt, ist durch ein allgemeingültiges Verfahren entscheidbar. Man kann nämlich schrittweise alle nichtterminalen Symbole bestimmen, aus denen e ableitbar ist. Wir betrachten hierzu ein Beispiel: S: A: B: D:
aSa; AB; DA bAb; AB; e. cBc; e. ac.
Zunächst ist e aus A und B ableitbar, daher auch aus AB und demnach aus S. Da S zu der so gefundenen Menge gehört, ist e€L(G). Man kann nun in einfacher Weise eine Grammatik zur Beschreibung von L(G)^(e}> finden: Man läßt in der gegebenen Grammatik alle Produktionen mit e als rechter Seite weg und fugt alle Produktionen hinzu, die sich dadurch gewinnen lassen, daß man auf der rechten Seite vorhandener Produktionen nicht-terminale Symbole wegläßt, aus denen e ableitbar ist. Im obigen Beispiel trifft dies etwa auf die folgenden Produktionen zu: A: bAb; AB. Da sowohl aus A als auch aus B die leere Zeichenkette ableitbar ist, ist neben bAb noch bb zu betrachten und neben AB noch A und B. In diesem Fall dürfen wir A und B nicht gleichzeitig weglassen, weil sonst neue Produktionen mit e als rechter Seite entstehen. Die ursprünglichen Produktionen müssen natürlich erhalten bleiben, weil aus den Symbolen A, B und S noch andere Wörter als e ableitbar sind 11 . Zum obigen Beispiel ergibt sich auf diese Weise, wobei die triviale Produktion (A,A) weggelassen ist: S: A: B: D:
aSa; AB; DA; aa; A; B; D. bAb; AB; bb; B. cBc; cc. ac.
8.4.3 Reduzierte Grammatiken Im Beispiel von 8.3.3 haben wir eine unendliche Anzahl ableitbarer Wörter dadurch erreicht, daß ein nicht-terminales Symbol rekursiv war, d.h. in einem aus ihm ableitbaren Wort erneut auftritt. Ist L(G) unendlich, so ist dies stets notwen10 In der Literatur werden gelegentlich die kontextfreien Grammatiken mit Löschung als kontextfrei bezeichnet und die kontextfreien ausdrücklich als kontextfrei ohne Löschung. 11 Aufgrund der Aussage dieses Abschnitts läßt sich zu jeder kontextfreien Grammatik mit Löschung eine andere finden, bei der e höchstens als rechte Seite zu S auftritt.
8.4 Kontextfreie Grammatiken
227
dig. In diesem Fall enthält G wenigstens ein nichtterminales Symbol E, so daß E -»• xEy gilt, wobei x und y nicht gleichzeitig leer sind 12 . E heißt dann einbettendes Symbol. Die Umkehrung gilt jedoch nur für Grammatiken, die keine überflüssigen Symbole und Produktionen enthalten. Andernfalls könnte z.B. eine Produktion (E, xEy) auftreten, ohne daß E in der Ableitung irgendeines Wortes aus L(G) vorkommt. G heißt reduziert, wenn entweder L(G) = (6 und G = ($,{S}, L(G) * t und (a)
A 6 V = > (3u,vGV*) (S -»• uAv)
(b)
AGN=>(lter*)(A^t)
ist oder
ist, d.h. jedes Symbol tritt in irgendeiner Ableitung aus S auf, und aus jedem nichtterminalen Symbol kann wenigstens ein terminales Wort abgeleitet werden. Eine äquivalente reduzierte Grammatik zu einer gegebenen Grammatik ist effektiv konstruierbar: Zunächst sind alle nichtterminalen Symbole bestimmbar, aus denen terminale Wörter abgeleitet werden können. Hierzu geht man von den nichtterminalen Symbolen aus, die als linke Seite einer Produktion mit einem terminalen Wort als rechter Seite auftreten. Dann werden fortlaufend diejenigen nichtterminalen Symbole hinzugenommen, zu denen es Produktionen gibt, auf deren rechter Seite ausschließlich terminale oder bereits ausgewählte nichtterminale Symbole stehen. So wird die Menge der nichtterminalen Symbole aufgebaut, die die Bedingung (b) erfüllen. S ist genau dann nicht in der so konstruierten Menge, wenn L(G) = i> ist. Wir streichen dann alle Produktionen, die andere nichtterminale Symbole enthalten. Anschließend wird umgekehrt von S ausgegangen, und es werden alle Symbole bestimmt, die auf der rechten Seite einer Produktion mit S als linker Seite auftreten; danach werden die rechten Seiten derjenigen Produktionen untersucht, die ein so gefundenes nichtterminales Symbol als linke Seite besitzen usw. Alle Symbole, die auf diese Weise nicht erreicht werden, sind ebenfalls überflüssig; Produktionen, die sie enthalten, werden gestrichen. Wir betrachten ein einfaches Beispiel: S: aSb; ab; AB. A: aAa;aSa. B: bBa. Von den terminalen Symbolen ausgehend, finden wir zunächst S und dann A; B wird auf diesem Weg nicht erreicht, so daß bleibt: S: aSb; ab. A: aAa;aSa. 12
Ist xy=6, so bringt die Ableitung kein neues Wort hervor.
228
8. Formale Sprachen
Ausgehend von S, finden wir nur a, b und wieder S, so daß als reduzierte Grammatik S:
aSb.ab.
bleibt. Wir werden in Zukunft stets reduzierte Grammatiken betrachten. 8.4.4 Mengentheoretische Operationen Führen die kontextfreien Sprachen bei Anwendung mengentheoretischer Operationen wieder zu kontextfreien Sprachen? Wir erhalten: Sind L, L!, L 2 kontextfreie Sprachen (mit Löschung), so sind auch L ^ U I ^ , L J L 2 , L R und L + kontextfrei (mit Löschung); L* ist stets kontextfrei mit Löschung. In allen Fällen ist eine entsprechende Grammatik effektiv konstruierbar. Wir betrachten zur Erläuterung solcher Konstruktionsverfahren ein Beispiel: Seien G! = ({a,b},{Si}, P , , S ^ und G 2 = ({a>,{S2}, P 2 , S 2 ) zwei Grammatiken mit den Produktionsmengen P j : A: aAb; ab. und P 2 : C: Ca; a. d.h. L ( G 1 ) = { a n b n : n>l>, L(G 2 ) ={a k : k>l>. Dann erhalten wir L(Gj) • L(G 2 ), indem wir G 3 = ({a,b]-,{S 1; S 2 ,83)^3,83) mit Px U P 2 U < S 3 , S , S 2 ) } betrachten. Dabei ist S 3 ein neu eingeführtes Startsymbol. Bei der Konstruktion muß man darauf achten, daß N x HN 2 = $ ist. Analog erhält man die Vereinigung L(G 1 )UL(G 2 ) durch Hinzunahme zweier Produktionen S4 : Sj; S2. Die übrigen Konstruktionen verlaufen analog. Für L R ist jede Produktion (u,v) durch (u,v R ) zu ersetzen. Aus diesen Ausfuhrungen folgt, daß Lj ={a"b"ak k
und L2 = {a b"a
n
: n, k>l > : n,k>l }>
kontextfrei sind. Wie wir in 8.3.8 erwähnt haben, ist aber L ! n L 2 nicht kontextfrei: Ist T ein Alphabet, das mehr als ein Symbol enthält 1 3 , so ist die Klasse der 13 Die kontextfreien Sprachen über einem einelementigen Alphabet stimmen mit den regulären überein.
8.5
Zwei-Stufen-Grammatiken
229
kontextfreien Sprachen (mit Löschung) nicht abgeschlossen bezüglich der Durchschnittsbildung und der Komplementbildung bzgl. T + (bzw. T*). Darüber hinaus ist es prinzipiell unmöglich, einen für alle kontextfreien Sprachen gültigen Algorithmus anzugeben, der prüft, ob der Durchschnitt zweier Sprachen kontextfrei ist oder nicht. Man kann zeigen, daß ein solcher Algorithmus nicht existiert.
8.5 Zwei-Stufen-Grammatiken Bei der in 8.1.3 zuerst angegebenen Grammatik zur Definition arithmetischer Ausdrücke ist der Vorrat an Operationen begrenzt durch die in diesen Produktionen explizit angegebenen Operatoren. Wie wir gesehen haben, enthalten moderne, universelle Programmiersprachen die Möglichkeit, zusätzliche Operatoren zu deklarieren 14 . Dies ist insbesondere dann sinnvoll, wenn neue Datenarten deklariert werden dürfen. Da auch die Einordnung der neu deklarierten Operatoren in die Vorrangregelung dem Programmierer überlassen werden muß, ist die in Nr. 8.1.3 zuerst angegebene Grammatik, in der der Operatorenvorrang durch die Produktionen geregelt wird, nicht mehr adäquat. Als Ausweg bietet sich die dort als zweite angegebene Grammatik an, wobei die Mehrdeutigkeit durch Berücksichtigung des Operatorvorrangs ausgeschaltet werden kann.
8.5.1 Attribut-Grammatiken Ein geeignetes Modell bietet die Beschreibung ähnlich der von ALGOL 68 [van Wijngaarden 1969] an. Wir gehen aus von der mehrdeutigen Grammatik: expression: formula: Operand: primary:
formula; Operand, Operand, operator, Operand. formula; primary. variable; number; open symbol, expression, close symbol.
Wir machen sie dadurch eindeutig, daß wir die Symbole dieser Grammatik durch Zusätze näher beschreiben (Attribute), die jedem Operator und Operanden Prioritätsstufen zuordnen: expression: PRIORITY formula; PRIORITY Operand. PRIORITY formula: PRIORITY Operand, PRIORITY operator, PRIORITY plus one Operand. 14 Die Grammatik muß dann natürlich Produktionen enthalten, die die Konstruktion neuer Operatoren beschreiben.
230
8. Formale Sprachen
PRIORITY operand: PRIORITY formula; PRIORITY plus one operand, priority NINE plus one operand: primary, primary: variable; number; open symbol, expression, close symbol. Jede Produktion dieser Grammatik entspricht einer Schar von Produktionen, die in herkömmlicher Weise angewandt werden und die man erhält, wenn man das Attribut PRIORITY an allen Stellen der Produktion durch das gleiche Wort ersetzt, z. B.: priority one formula: priority one operand, priority one operator, priority one plus one operand. Metaproduktionen regeln, welche Wörter fur ein Attribut eingesetzt werden dürfen: PRIORITY: priority NUMBER. NUMBER: one; TWO; THREE; FOUR; FIVE; SIX; SEVEN; EIGHT; NINE. TWO: one plus one. 15 THREE: TWO plus one. FOUR: THREE plus one. NINE:
EIGHT plus one.
8.5.2 Beispiel Betrachten wir das Beispiel a+b*c, und setzen wir die Prioritätsstufen 1 bzw. 2 für + bzw. * voraus; wir erwarten, daß zunächst b*c zusammenzufassen ist. b und c lassen sich jedes für sich aus einem Primärausdruck ableiten und weiter aus einem Operanden der Priorität 10, genauer der Priorität one (plus one)9. Da der Operator * von der Priorität 2 sein soll, muß b*c durch Anwenden der folgenden Produktion entstanden sein: priority one plus one formula: priority one plus one operand, priority one plus one operator, priority one plus one plus one operand. 15 Die Schreibweise „one plus one" für TWO ist nötig, weil wir nur Wörter über einem Alphabet einsetzen, nicht aber rechnen können. (Das Alphabet ist hier .)
(1)
8.5 Zwei-Stufen-Grammatiken
231
Der Übergang von zu
priority one plus one operand priority one (plus one)9 operand
ist jedoch durch Anwendung mehrerer Produktionen möglich, die sich ableiten lassen aus: PRIORITY operand: PRIORITY plus operand.
(2)
Selbstverständlich können wir in analoger Weise zuerst a+b zusammenfassen. Da der Operator + von der Priorität 1 sein sollte, muß die Zeichenkette jedoch aus priority one formula abgeleitet worden sein. Dies kann jedoch nicht als linker Operand aus (1) abgeleitet worden sein, weil eine Umkehrung zu (2) in der Grammatik nicht existiert. 8.5.3 Darstellung kontextsensitiver Sprachen Wegen des Ubergangs zu den Primärausdrücken, deren Priorität festgelegt sein muß, kommen im obigen Beispiel für PRIORITY nur endlich viele Wörter in Frage. Wir betrachten nun noch ein Beispiel, bei dem für ein Attribut abzählbar viele Wörter eingesetzt werden dürfen. Gleichzeitig zeigt dieses Beispiel, daß wir auf diesem Weg den Bereich kontextfreier Sprachen verlassen können: Produktionen: S: left LENGTH, right LENGTH. left LENGTH plus one: a, left LENGTH, b. left one: a, b. right LENGTH plus one: a, right LENGTH, right one: a. Metaproduktionen: LENGTH: one; LENGTH plus one. Man überzeugt sich leicht, daß diese Grammatik die kontextsensitive Wortmenge ianb"a"
:
n>iy
erzeugt: Aus left und einem aus LENGTH abgeleiteten Attribut sind Wörter der Form anb" ableitbar, aus right und einem aus LENGTH abgeleiteten Attribut solche der Form ak\ die Übereinstimmung der Exponenten wird durch das Attribut in der ersten Produktion gesichert. Das hier beschriebene Konzept ist durch die Affixgrammatiken formalisiert worden [Koster 1971]. Man kann zeigen, daß auf diese Weise unter Verwendung
232
8. Formale Sprachen
kontextfreier Produktionen mit Löschung alle formalen Sprachen (im Sinne von 8.2.5) beschrieben werden können. Die Ursache dafür, daß auch nichtkontextfreie Wortmengen auf diese Weise erzeugt werden können, liegt in der gleichzeitigen Ersetzung der Attribute durch ein- und dasselbe Wort aus einer unendlichen Menge begründet. Die Attribute, die uns gestatten, Zusammenhänge zwischen verschiedenen Teilen eines Wortes herzustellen, ohne daß kontextfreie Produktionen hierzu imstande wären, sind uns auch compilertechnisch vertraut. Hierbei handelt es sich nämlich um die Informationen, die wir beispielsweise in der Symbolliste speichern und auf die wir bei der Abarbeitung etwa von Ausdrücken zurückgreifen.
8.5.4 Grammatiken mit Kontrollmenge Ein anderes Modell wurde von [Ginsburg, Spanier 1968] angegeben16: die Grammatiken mit Kontrollmenge. Eine solche ist gegeben durch ein Quintupel G = (T, N, F, ß, S) und eine Kontrollmenge K mit: (a) (b) (c)
(d)
T, N, F sind endliche Alphabete, V := NUT. Dabei sei F eine Menge von Produktionsnamen. SSN ß: F -*• V*X V * ist eine Abbildung, die jedem Namen eine Produktion zuordnet. (Zwei verschiedenen Namen kann die gleiche Produktion zugeordnet sein.) Im kontextfreien Fall gilt insbesondere: ß: F -*• NXV + . K Teilmenge von F * .
G' = (T, N, 0[F], S) ist eine Chomsky-Grammatik. Der Begriff der Ableitbarkeit wird nun ausschließlich unter Bezugnahme auf Folgen von Produktionsnamen definiert: Ist fSF, so gelte w z genau dann, wenn (3x,y,u,vGV*J
(w=xuy
z =xvy A ß(f) = (u,v)j
Wir definieren, daß stets w w gilt, wobei e nicht das leere Wort in V*, sondern das in F * bezeichne. Dann können wir rekursiv die Ableitbarkeit 17 bezüglich eines Wortes auf F * definieren: ist nämlich fGF, g^F*, so sei w^z :
(3z') (w-+z' A. z' -J z).
Die von G zusammen mit der Kontrollmenge K erzeugte Wortmenge ist dann gegeben durch " Vgl. auch [Rosenkrantz 1969; Greibach, Hopcroft 1969; Abraham 1 9 6 5 ; van der Walt 1971]. 1 7 Ginsburg, Spanier verwenden eine speziellere Form der Ableitung, die für Compilerprobleme jedoch von geringerem Interesse ist.
233
8.5 Zwei-Stufen-Grammatiken
L K ( G ) := {w: w e r * A (3gGK) (S
w)>
Man überzeuge sich, daß gilt L(G) = L F *(G). Dabei ist zu beachten, daß nicht jedes Wort aus F* zu einer Ableitung führt. Wird nämlich die Anwendung der durch f bezeichneten Produktion gefordert, so ist z -f z nur definiert, wenn z die linke Seite von ß ( f ) enthält. 8.5.5 Beispiel Als Beispiel betrachten wir wiederum < a n b n a n : n > 1>. Hierzu fuhren wir die Produktionsnamen F = {Pi,P2.P3»P4>P5}> e i n : f Pl P2 P3 P4
PS
m
(S,BA) (B,aBb) (A,aA) (B,ab) (A,a)
Damit sich die vorgegebene Wortmenge ergibt, müssen wir erreichen, daß die Produktionen p 2 und p 3 stets nur abwechselnd angewendet werden. Dies ist der Fall, wenn wir {P2Ps>*iP4Ps> vorgeben. Zu K gehören z. B. P1P2P3P4P5
undp1(p2ps)2P4P5•
Man kann zeigen, daß die kontextfreien Grammatiken mit Löschungen und Kontrollmengen, die von regulären Grammatiken erzeugt werden können (vgl. 8.3.4), ausreichen, um alle formalen Sprachen zu erzeugen.
9. Modelle zur Rekognition
9.1 Endliche Automaten 9.1.1 Definition des endlichen Automaten Wir müssen uns nun damit befassen, zu einer vorgegebenen Zeichenkette herauszufinden, ob sie zu der von einer bestimmten Grammatik erzeugten Wortmenge gehört oder nicht, und - wenn ja - die Ableitung zu rekonstruieren (Rekognition). Uns ist natürlich daran gelegen, nicht jeden Einzelfall gesondert zu behandeln, so daß wir formale Verfahren anstreben. Ein formales Modell für eine einfache Rekognitionsstrategie ist der endliche Automat. (Vgl. Abb. zu 3.2.2.) Ein endlicher erkennender Automat wird dargestellt durch ein Quintupel E = (X, Z, S, z 0 , F), wobei X,Z endliche Mengen sind (Eingabealphabet, Zustandsmenge), 5: ZXX -»• Z eine Abbildung ist (Überführungsfunktion), z0GZ ein Anfangszustand1 und F^Z die Menge der markierten Zustände ist. Die Arbeitsweise des endlichen erkennenden Automaten wird so definiert, daß bei jedem Schritt ein Symbol vom Eingabeband gelesen und der durch 5 gegebene Zustandswechsel durchgeführt wird2. Dadurch wird jedem Wort aus X* über eine Folge von Zustandswechseln ein Endzustand durch folgende Vereinbarung zugeordnet: Ist 5: ZXX ->• Z Überführungsfunktion eines endlichen, erkennenden Automaten, so sei 5*: ZXX*->-Z definiert durch: (a)8*(z,e):=z (b) (Vw&X*) (VxeX) (8*(z,wx) := 8(8*(z,wJ,x)), insbesondere ist 5*(z,x) = 5(z,x) für alle xGX. Der Automat wird also aus dem Zustand z durch Eingabe von wx in den Zustand 6(z',x) überführt, nachdem er durch w von z in z' überführt wurde. Als die von E angenommene Wortmenge (Bandmenge) bezeichnen wir die Menge der Wörter, die den Automaten von zQ in einen Endzustand überführen: L(E):= {w:
8*(z0,w)&F>
Ein wesentliches Kennzeichen der Arbeit dieses Automaten (wie auch des später zu betrachtenden Kellerautomaten) ist die Tatsache, daß das Eingabeband sich stets in einer Richtung bewegt. Die auf ihm notierten Symbole werden von links nach rechts gelesen ohne die Möglichkeit des Zurückkehrens. 1 2
Der Automat wird daher auch initial genannt. Während des Übergangs kann die Semantik-Routine ausgeführt werden.
235
9.1 Endliche Automaten
Einzelheiten zur Theorie dieser Automaten findet der Leser beispielsweise bei [Brauer 1973; Böhling, Indermark 1969;Hackl 1972; Hotz, Walter 1969; Starke 1969]. 9.1.2 Beispiel Wir betrachten als Beispiel einen Automaten zur Erkennung der Wortmenge
ia nb m :n,m>l\U
{a n: n > o> U {b 2n: n> 1>
5 stellen wir, wie schon in Nr. 3.2.2, durch einen Graphen dar, dessen Knoten die Zustände bezeichnen und dessen Kanten die Ubergänge angeben (siehe Abbildung). Die markierten Zustände sind:
F:= {z0 , zlt z2 , z4 y Sie sind in der Abbildung durch Doppelkreise dargestellt. Wird beispielsweise a 3 b 2 gelesen, so werden der Reihe nach z Q , Zj, z 1 ; Zj, z 2 , z 2 durchlaufen. Der Zustand z 5 fungiert als Fehlerzustand. Er verhindert, daß z. B. Wörter der Form a n b m a k mit k > o angenommen werden. Ist die Eingabezeichenkette abgearbeitet, wenn sich der Automat im Zustand z 3 befindet, so war das eingegebene Wort von der Form b 2 n + 1 . Zwei endliche, erkennende Automaten E i und E 2 heißen dann und nur dann äquivalent, wenn L f E ^ = L(E 2 ) ist, d. h. wenn sie die gleiche Wortmenge annehmen. Insbesondere stimmt die Klasse der regulären Sprachen mit der Klasse der durch endliche, erkennende Automaten angenommenen Wortmengen überein, d. h. zu jeder regulären Wortmenge kann man einen endlichen Automaten konstruieren, der sie annimmt, und umgekehrt ist die Wortmenge jedes endlichen Automaten regulär.
Abb. zu 9.1.2. Erkennender Automat für die Wortmenge !>• u 0> u { b 2 n : n > 1>
9. Modelle zur Rekognition
236
9.1.3 Nichtdeterministische Automaten Häufig ist es sinnvoll, von einem Zustand aus mehrere Übergänge mit dem gleichen Eingabesymbol zuzulassen. Damit kommen wir zu einem weiteren Automatentyp: Ein endlicher nichtdeterministischer, erkennender Automat ist durch ein Quintupel N = (X,Z,5,Z0,F) gegeben, wobei X,Z und F wie bisher definiert sind, 5: ZXX-+ ^ (Z) eine Abbildung in die Potenzmenge von Z und Z 0 CZ die Menge der Anfangszustände ist. Der Unterschied zum bisherigen Modell liegt darin, daß 5(z,x) eine Menge möglicher Folgezustände ist (evtl. auch die leere3), so daß das Verhalten nicht mehr eindeutig ist. Dem muß auch bei der Ausdehnung der Überfuhrungsfunktion auf ZXX* Rechnung getragen werden: Ist 6: ZXX-*-1^ (Z) Überführungsfunktion eines endlichen, nichtdeterministischen, erkennenden Automaten, so sei 6*: ZxX* (Z) definiert durch: (a) &*(z,e):= (b) (VwGX*) (Vx&X) ß*(z,wx) :=
U
z'&*(z,w)
8(z',x))
Statt des im deterministischen Fall einzigen Zustandes z , der sich bei Eingabe von w ergibt, sind nun alle möglichen z zu betrachten und die passenden Folgezustände zusammenzufassen. In der Menge der Endzustände, in die ein Eingabewort den Automaten überführen kann, können sich sowohl solche aus F, als auch solche aus Z-F befinden; ist wenigstens ein Zustand aus F darunter, so wollen wir das Wort als angenommen betrachten: L(N) := {w: wGX* * (3:zoeZJ
(8*(z0,w) DF *)}
Obwohl die Klasse der nichtdeterministischen, endlichen Automaten von der Definition her allgemeiner ist als die der deterministischen, können die ersten nicht mehr leisten: Zu jedem endlichen, nichtdeterministischen, erkennenden Automaten N = (X,Z,5,Z0,F) gibt es einen endlichen, deterministischen, erkennenden Automaten E = (X,Z',6', ZQ,F'), der die gleiche Wortmenge annimmt. Der nichtdeterministische Automat kann jedoch etwas einfacher gebaut sein.
9.2 Kellerautomaten 9.2.1 Definition des Kellerautomaten Das Modell des endlichen Automaten ist in Compilern nur an wenigen Stellen einsetzbar, z. B. bei der Erkennung der Konstanten. Für die Formalisierung der Kellerungstechnik, die wir bereits bei der Bearbeitung der arithmetischen Ausdrücke eingesetzt haben, muß der Automatenbegriff verallgemeinert werden: Ein 3
Üblicherweise heißt der Automat dann unvollständig. Diese Unterscheidung spielt in unserem Zusammenhang keine Rolle.
9.2 Kellerautomaten
237
Kellerautomat ist gegeben durch P = (Z,X,K,5,z 0 ,k 0 ,F), wobei Z,X und K endliche Mengen sind (Zustände, Eingabesymbole, Kellersymbole), 5 eine Abbildung von ZXKX(XU {e}) in die endlichen Untermengen von ZXK* und z 0 €Z (Anfangszustand), k o e K (Anfangskellersymbol) und FQZ (markierte Zustände). Wir haben hier unmittelbar den nichtdeterministischen Kellerautomaten definiert. Die noch formal zu definierende Arbeitsweise des Kellerautomaten versteht man besser, wenn man folgendes Bildchen vor Augen behält:
i
i
i
Kelle rwort-«—i i i i
i
Eingabewort > i i i i
i
i
Zustand
Der Automat liest ein Symbol des Eingabewortes und bestimmt aus diesem und dem letzten Symbol 4 des Kellerwortes den Folgezustand und eine (anstelle des gelesenen Kellersymbols) in den Keller zu speichernde Zeichenkette. Wenn diese leer ist, kommt ein „zuvor verdecktes" Kellersymbol zum Vorschein. Um die einzelnen Arbeitsschritte des Automaten formal zu beschreiben, fassen wir die im Automaten gespeicherte Information, d. h. Zustand und Kellerinhalt, und den noch nicht gelesenen Teil des Eingabewortes zusammen: Unter einer Konfiguration (z,u,w) verstehen wir ein Element aus ZXK*XX*. (z',uv,w) heißt Folgekonfiguration von (z,uk,aw), geschrieben: (z,uk,aw) t- (z',uv,w) dann und nur dann, wenn (z',v)G6(z,k,a) ist. Dabei sind kGK und u,vGK*. v=e bedeutet, daß der Keller gekürzt wird, u ist dabei der „tiefer" liegende Kellerinhalt, der nicht gelesen und nicht verändert wird. In der Compilertechnik spielen — z. B. bei der Abarbeitung von Klammerpaaren5 — die autonomen Schritte eine Rolle: ist in der obigen Formel nämlich a=e, so güt (z,uk,w) i- (z',uv,w), falls (z',v)G6(z,k,e) ist. Bei diesen Schritten wird das Eingabeband also nicht weitergerückt. (z,k,w) £ 4
(z',k',w'j
Oft spricht man auch vom „obersten" Kellersymbol. Dem liegt die Vorstellung eines von unten nach oben gestapelten Kellers zugrunde. 5 Vgl. Nr. 1.3.4, Fall 3.
238
9. M o d e l l e z u r R e k o g n i t i o n
gilt, wenn es eine Folge von Konfigurationen mit (z,u,w)
= ta i- ti
t- t2
i- . ..
i-tn
=
(z',u',w)
gibt.
9.2.2 Angenommene Wortmenge Wir betrachten zwei Möglichkeiten, den Begriff der angenommenen Wortmenge zu definieren: Ist P ein Kellerautomat, so heißt T(P)
die durch
:=
{w^X*:
markierten
N(P)
:=
(3z&F) Zustand
(3uGK*)
(z0,k0,w)
angenommene
{ w G F : (3zGZ)
((z0,k0,w)
£
(z,y,e))}
Wortmenge £
und
(z,e,e)fr
die durch leeren Keller angenommene Wortmenge. Werden diejenigen Wörter als angenommen betrachtet, nach deren Einlesen der Keller leer ist, spielen die markierten Zustände keine Rolle. (z„,k 0 ,w) heißt Startkonfiguration des Kellerautomaten, k 0 Startkellersymbol. Es ist erforderlich, damit der Keller am Anfang nicht leer ist. Ist der Keller leer, so ist 5 nicht definiert 6 . Verlangt man, daß bei jedem Arbeitstakt des Kellerautomaten eine eindeutige Folgekonfiguration existiert, so genügt nicht, daß alle 5(z,k,a) höchstens ein Element enthalten. Vielmehr dürfen auch autonome Übergänge nicht gleichzeitig möglich sein: Ein Kellerautomat P heißt dann und nur dann deterministisch, wenn gilt (Vz&Z)
(Vk^K){(ya&XU;
(/8(z,k,aj/
X(8(z,k,e)
Va&Xj
) d e f i n i e r e n , k ö n n t e n nur W o r t m e n g e n der F o r m L * ( m i t L c X * ) durch leeren Keller a n g e n o m m e n w e r d e n .
239
9 . 2 Kellerautomaten
5 kann dann folgendermaßen definiert sein: S(z0,k0,x) := {(za,x)} für alle xe{a,b} 8fz0,x,y) := {(za,xy)} für alle x,yE{a,b} 8(za,x,c) •'= {(zj,x)} für alle x€{a,b} b(z0,k0,c) := {{zj.eJl 8(zj,x,xJ := {(zj,e)} für alle x&{a,by In allen anderen Fällen sei S(z,x,y) die leere Menge, das heißt: wenn sich der Kellerautomat z. B. im Zustand z t befindet und im Keller ein anderes Symbol liest als auf dem Eingabeband, so bleibt der Automat stehen, ohne daß das Eingabewort abgearbeitet ist 7 . Der Automat speichert zunächst w im Keller (Zustand z 0 , zweite Zeile der Tabelle), wechselt bei c in den Zustand z t (3. Zeile) und streicht dann passende Symbole im Keller und auf dem Eingabeband weg (5. Zeile). Selbstverständlich können wir auch — analog dem Beispiel beim erkennenden Automaten — einen Fehleizustand einfuhren: &(zj, x, y) := {(z2,xj\ b(z2, x, y) := {fz2,xj}
für alle x^{a,b\, für alle xe{a,by,
y£{a,b,c\, xl=y ye{a,b,cy
Treten nicht übereinstimmende Symbole auf, so wechselt der Automat in den Zustand z 2 , in dem der Keller konstant bleibt, so daß dort keine Wörter angenommen werden. Da bei der Eingabe eines Wortes aus N(P2) := wz«
S ->• z
(Top-down) w
bzw.
(Bottom-up)
ergänzt werden kann. Die Präzedenzverfahren sind Beispiele sackgassenfreier Rekognitionsverfahren für alle Grammatiken, die die Präzedenzbedingungen erfüllen. Sollen Sackgassen berücksichtigt werden, so sind besondere Vorkehrungen zur Sicherung der Daten an den Verzweigungsstellen erforderlich. Systematische Übersichten finden sich beispielsweise bei [Knuth 1971] und [Floyd 1964]. Immer dann, wenn die Fortsetzung des Verfahrens in einer bestimmten Situation nicht eindeutig ist, werden die zu diesem Zeitpunkt relevanten Daten — insbesondere der Kellerinhalt und die Stellung des Zeigers im Eingabewort — kopiert, und dann wird einer der möglichen Wege weiterverfolgt. Führt er nach einigen Schritten zu einem toten Ende, d.h. der Kellerinhalt hat einen Inhalt, der in der vorliegenden Grammatik nicht ableitbar ist, so wird dieser Weg abgebrochen. Diese Situation wäre z.B. erfüllt, wenn im 10. Schritt des Beispiels zu 9.3.5 fälschlicherweise die Produktion (E,T) erkannt würde; es ergäbe sich dann der Kellerinhalt E+E. Nach Abbruch eines Weges wird der zuletzt kopierte Zustand im Keller und auf dem Eingabeband wiederhergestellt und ein anderer der möglichen Wege weiterverfolgt (Back-up). 9.3.7 Left-corner-Strategie Zum Schluß soll noch kurz die Left-comer-Stategie erläutert werden [Rosenkrantz, Lewis 1970]. Hierbei handelt es sich um eine Mischform, die viele pragmatisch geschriebene Compiler verwenden [Irons 1961]. Um die Reihenfolge zu beschreiben, in der die in der Ableitung angewandten Produktionen aufgefunden werden, müssen wir in der Abb. zu 9.3.3 den Teü p des Strukturbaumes unterteilen: — p j sei deijenige Teil des Strukturbaumes, der das am weitesten links stehende nichtterminale Symbol von Vj und den daraus entspringenden Teilbaum umfaßt. — p 2 sei der übrige Teil von p
9. Modelle zur Rekognition
250
In der Abbildung geben wir den Strukturbaum zu den in Nr. 9.3.2 betrachteten Ableitungen an, wobei wir die nichtterminalen Symbole in der Reihenfolge numeriert haben, in der sie als linke Seite wiedererkannter Produktionen auftreten. Zunächst werden in Bottom-up-Manier die nichtterminalen Symbole 1 - 2 — 3 - 4 erkannt. Aufgrund des Vorliegens ihrer linken unteren Ecke wird die Produktion (E,E+T) erkannt. Für den Rest des aus ihrer linken Seite entspringenden Teilbaumes liegen neue Symbole vor, auf die das nächste Stück des Eingabewortes zu reduzieren ist. Diese Situation kennen wir von der Top-down-Strategie. Die zugehörigen Teilableitungen werden jedoch wieder von unten nach oben konstruiert (Symbole 6—7—8—9), wobei die bereits bekannten Ziele zum Aussortieren von Sackgassen dienen (selective bottom-to-top parsing [Griffiths, Petrick 1965]).
E (12)
E (5)
E (4)
+
+
T (9)
T (3)
T (8)
F (2)
*
T (15)
F (14)
F (11)
P (13)
F (7)
P (10)
v
P(l)
P(6)
v
v
v
Abb. zu 9.3.7. Reihenfolge des Auffindens nichtterminaler Symbole bei Left-corner-Verfahren
9.3 Rekognitionsstrategien
251
Nachdem mit den Symbolen 1 0 - 1 1 der zu Symbol 5 gehörige Teilbaum abgeschlossen wird, kann die Wurzel des Baumes (Symbol 12) bestimmt werden. Der rechte Ast vervollständigt den Strukturbaum. Allgemein kann man also sagen: Die Produktion p wird dann bestimmt, wenn p ! vollständig erkannt ist (Bottom-up), aber bevor etwas aus p 2 bestimmt wird. Bei der Bestimmung von p 2 liegen bereits die Ziele vor, auf die dieser Teil der Zeichenkette zu reduzieren ist (Top-down). Da sich der Vorgang rekursiv wiederholt, ist auch s so zu zerlegen, daß seine linke untere Ecke bereits bekannt ist. (Dies gilt jedoch nur, wenn q nicht leer ist, d.h. links von der gesuchten Produktion noch Symbole stehen.) Stellt man die Frage, welche Strategie zu welchem Zweck günstig ist, so kann man mit der gebotenen Vorsicht etwa folgendes festhalten: - Pragmatisch geschriebene Compiler können dank der Spezialisierung auf eine bestimmte Sprache und den sich hieraus ergebenden Kenntnissen mit der Leftcorner-Strategie recht effektiv werden. - Für Compiler, die mit Tabellen oder Übergangsmatrizen arbeiten, die formal aus der Grammatik abgeleitet werden sollen, sind Bottom-up-Verfahren recht gut geeignet. - Für die automatische Compiler-Erzeugung (Compiler-Compiler) scheinen Topdown-Verfahren besonders geeignet.
10. Rekognitionsverfahren
10.1 Vorrangverfahren 10.1.1 Operatorenvorrang In Kap. 9 haben wir Rekognitionsstrategien kennengelernt, die noch kein Rekognitionsverfahren beschreiben, weil die Kriterien noch nicht vollständig sind, die zur Erkennung einer Produktion fiihren. Zur Ausfüllung dieser Lücke betrachten wir zunächst eine Methode, die uns bereits in Nr. 1.4 begegnet ist. Dort haben wir ein Bottom-up-Verfahren angegeben, das zur Entscheidung, ob die Primphrase, das ist die rechte Seite der als nächstes zu erkennenden Produktion bereits im Keller liegt, den Vorrang der arithmetischen Operatoren benutzte. Die Vorrangbeziehungen wurden dabei auf weitere terminale Symbole (wie Semikolon, Ergibtzeichen und Klammern) ausgedehnt. Da wir in 8.1 festgestellt haben, daß kontextfreie Grammatiken in der Lage sind, den Operatorenvorrang widerzuspiegeln, können wir auch umgekehrt wieder aus der Sprachdefinition ablesen, welche der drei Beziehungen - formal handelt es sich um Relationen - zwischen zwei Symbolen gilt, und dabei alle terminalen Symbole einbeziehen [Floyd 1963], Zwischen aufeinander folgenden terminalen Symbolen innerhalb einer Primphrase gilt die Relation Die Relation < zeigt an, daß die nächste Primphrase weiter rechts in der zu bearbeitenden Zeichenkette zu suchen ist, und > bedeutet, daß sie weiter links steht. Bei Abarbeitung von links nach rechts ist die nächste Primphrase durch die erste Folge terminaler Symbole gegeben, zwischen denen der Reihe nach die Relationen
gelten. (Zwischen den terminalen Symbolen kann dann noch je ein nichtterminales stehen.) Damit das Verfahren eindeutig abläuft, darf zwischen zwei terminalen Symbolen nie mehr als eine dieser Relationen gelten. Andernfalls ist ggf. die Grammatik zu ändern. (Vgl. das Beispiel in 10.1.4) In dieser Form kann das Verfahren dann in jeder der Phasen 2, 3 und 4 des Beispiel-Compilers, den wir in Kap. 4 beschrieben haben, angewandt werden.
10.1 Vorrangverfahren
253
10.1.2 Allgemeine Vorrangrelationen Beschränkt man die Vorrangrelationen nicht auf terminale Symbole, so kann man ihrer Definition folgenden Gedanken zugrundelegen [Wirth, Weber 1966]: X = Y : Es existiert eine Produktion ( A , . . . XY . . .)• + X < Y : Es existiert eine Produktion ( A , . . . XZ . . .) und Z ->• Yw. X •> Y : Es existiert eine Produktion ( A , . . . WZ . . . ) und W ^ W ' X A Z * Y W A
YGT
Die Bedingung YGT bedeutet beim Vorgehen von links nach rechts keine Einschränkung, weil stets die am weitesten links stehende Primphrase erkannt wird und die Symbole, die in der Eingabezeichenkette weiter rechts stehen, noch unverändert, also terminal, sind. In der Abbildung sind einige der möglichen Situationen an Hand von Syntaxbäumen dargestellt.
Abb. zu 10.1.2
10. Rekognitionsverfahien
254
Zur formalen Definition geben wir zunächst drei Hilfsrelationen m i t folgenden Bedeutungen an: AaB
°
Es gibt eine aus A direkt ableitbare Zeichenkette, die mit B beginnt.
AA*B °
Es gibt eine aus A ableitbare Zeichenkette, die mit B beginnt 1 .
AgjB o
Es gibt eine aus B direkt ableitbare Zeichenkette, die m i t A endet.
AJUB
Es gibt eine Produktion, in der A und B unmittelbar nebeneinander stehen.
a + , a * , c j + sind definiert, weil Relationen Mengen von Paaren sind. Anschaulich findet man u> + , indem man den Syntaxbaum von unten nach oben durchläuft, und a + , indem man von oben nach unten läuft. Formal ist: a
:= { ( A , B ) : ( 3 y e V * ) ( ( A , B y ) € P ) >
co := { ( A 3 ) : ( 3 y G V * ) ( ( B , y A ) 6 P ) > ß
•= < ( A , B ) : ( 3 x , y G V * X 3 C G V ) ( ( C , x A B y ) G P ) > U
Dabei sind S das Startsymbol und l— und —l die Begrenzungszeichen, die wir lediglich zur Vermeidung zusätzlicher Fallunterscheidungen benötigen. Man überzeuge sich davon, daß A = A'
t'vxwz'
Ist eine kontextfreie Grammatik sowohl (m,n)-erkennbar als auch (m,n)-reduzierbar 5 , so sprechen wir von einer Grammatik von (m,n)-beschränktem Kontext [Floyd 1964], Setzen wir antikanonische Ableitungen voraus, sprechen wir von (m,n)-links-rechts-beschränktem Kontext (LR-beschränktem Kontext). 10.2.2 Beispiel Als Beispiel betrachten wir die Grammatik: S: aAd; bAB. A: c; Ac. B: d. Die erzeugte Satzmenge ist: 1} Ob am rechten Rand einer Zeichenkette die Produktion (B,d) angewendet wurde, hängt vom 1. Symbol des Wortes ab, das beliebig weit von d entfernt sein kann, also ist die Grammatik nicht von (m,n)-beschränktem Kontext. Jedoch ist diese Grammatik von (2,0)-LR-beschränktem Kontext. Man kann nämlich ein Linksrechts-bottom-up-Rekognitionsverfahren auf der Grundlage folgender Entscheidungen angeben:
5 Man beachte, dali aus der (m,n)-Reduzierbarkeit nicht unbedingt die (m.n)-Erkennbarkeit folgt.
10.2 Verfahren mit beschränktem Kontext
Kellerinhalt .. . a ...b . . . ac .. .bc . . . Ac . . . aAd .. . M d ...bAB
261
Produktion —
-
(A,c) (A,c) (A,Ac) (S,aAd) (B,d) (S,bAB)
In der linken Spalte haben wir diejenigen Symbole unterstrichen, die als Kontext eine Entscheidung über die Reduzierbarkeit herbeiführen. Tritt die in der linken Spalte angegebene Zeichenkette am Ende des Kellers auf, so muß die Produktion der rechten Spalte in dem wiederaufzufindenden Ableitungsprozeß angewandt worden sein, in den ersten beiden Fällen ist die Primphrase noch nicht im Keller; es muß weiter gelesen werden. 10.2.3 Prüfung der Beschränktheitsbedingung Betrachten wir im allgemeinsten Fall eine Produktion (A,x) und ein Wort der Form . . . vxw . . .
|v| =m, |w| =n
Entweder ist x aus A abgeleitet worden oder Teile von x sind anders abgeleitet worden. Welche anderen Möglichkeiten könnten bzgl. der Lage der Primphrase bestehen, wenn x in diesem Kontext nicht durch Anwendung von (A,x) entstanden ist? Je nachdem ob am rechten und/oder linken Rand von x weitere Symbole hinzugenommen werden oder fehlen, ergeben sich 16 Möglichkeiten, wie Teile von x in anderer Weise als durch (A,x) entstanden sein können. Seien t,z£V*, ggf. also auch leer, XjX 2 = x und x 3 x 4 x 5 = x echte Zerlegungen von x, sowie v l \ 2 = v und w t w 2 = w echte Zerlegungen des Kontextes. Dann können wir diese 16 Möglichkeiten durch Angabe derjenigen Teilwörter charakterisieren, die im Syntaxbaum aus einem Knoten entspringen und x oder Teile davon enthalten 6 . (al) (bl) (cl) (dl)
tVXj (a2)
tvx (a3)
V 2 Xj (b2)
V2X (b3) V 2 XWj (b4) v 2 x w z (c3) XW| (c4) x w z X
Xl x4
(c2) (d2) x 2
tvxw 1 (a4)
(d3) X 2 W ,
tvx WZ
(d4) X2WZ
Dem Fall (al) z.B. entspricht die Entstehungs-Möglichkeit S
. . . Bx 2 w . . . /\ (B,yxj)GP a y -»•... v
6 Damit die Aussage sinnvoll wird, müssen wir einen Knoten betrachten, der möglichst weit vom Startsymbol entfernt ist.
10. Rekognitionsverfahren
262
wobei die Pünktchen beliebige Zeichenketten darstellen. Das Teilwort t finden wir in diesem Beispiel, evtl. zerlegt, in den Pünktchen vor B und vor v. Die Primphrase, die ein Stück von x enthält, ist y x t . Diese liegt noch nicht vor, sondern wird erst gewonnen durch Reduktion von v und eines Teiles von t, der auch leer sein darf. Analoge Entstehungsmöglichkeiten kann man für alle 16 Möglichkeiten aufstellen, wobei im Fall (3c) zusätzlich A * B sein muß, damit eine abweichende Entstehungsmöglichkeit vorliegt. Wird Eindeutigkeit für das Rekognitionsverfahren verlangt, so darf keine der aufgeführten Entstehungsmöglichkeiten in der Grammatik existieren; auf diese Weise erhält man 16 Bedingungen. Ist für ein konkretes x keine der 16 Bedingungen erfüllt, so muß x im Kontext vxw aus (A,x) entstanden sein. Alle auftretenden Bedingungen sind entscheidbar, so daß auch die Eigenschaft einer Grammatik, von (m,n)-beschränktem Kontext zu sein, für gegebene m,n entscheidbar ist. Ob zu einer Grammatik derartige m,n existieren, ist dagegen nicht entscheidbar.
10.2.4 (1,1)-LR-beschränkter Kontext Man erhält wesentlich weniger Möglichkeiten und einfachere Bedingungen, wenn man Einschränkungen macht: Wird bei der Rekognition von links nach rechts vorgegangen, so spielen nur antikanonische Ableitungen eine Rolle (vgl. 9.3.2). Demnach kann sich keine Primphrase links vom äußersten rechten Symbol von x befinden: die Fälle (al), (bl), ( c l ) und ( d l ) sind ausgeschlossen. Beschränken wir uns außerdem auf (l,l)-Kontext, d.h. wollen wir links und rechts der Primphrase nur je ein Symbol betrachten 7 , so gibt es keine echten Zerlegungen von v und w; es entfallen die Möglichkeiten der 2. Zeile und die der 3. Spalte: (b2), (b3), (b4), (a3), (c3) und (d3). Es bleiben nur noch die Möglichkeiten (a2), (a4), (c2), (c4), (d2) und (d4). Die Entstehungsmöglichkeiten bei (1,1 )-LR-beschränkten! Kontext sind also: (a2')
S * . . . Bw . . .
( a 4 ' ) . (c2') (c4')
S
B ...
a ( B , . . . vx)GP a ( B , . . . vxy)GP A y ' w . . .
. . . vBw . . .
a (B,x)€P a B*A
. vB . . .
a (B,xy)GP A y *
(d2')
S->...vx1Bw...
A(B,x2)eP
(d4')
S^-. .. v x ^ . . .
a (B,x 2 y)EP A y
w... w .. .
Man kann das Nichterfülltsein dieser Bedingungen folgendermaßen interpretieren: — Der rechte Rand der Primphrase stimmt mit dem rechten Rand des Kellerinhalts überein (a4, c4, d4). — Der linke Rand der Primphrase ist eindeutig (a2, d2). — Die Produktion ist eindeutig (c2). 7
Dies reicht bei Programmiersprachen häufig aus (vgl. Vorrangverfahren).
263
10.2 Verfahren mit beschränktem Kontext
10.2.5 Beispiel Wir b e t r a c h t e n auch weiterhin nur den Fall des ( l , l ) - L R - b e s c h r ä n k t e n Kontextes. Ist A £ N , so verstehen wir u n t e r dem K o n t e x t C(A) die Menge aller Zeichenpaare, zwischen denen A in einer antikanonisch abgeleiteten S a t z f o r m a u f t r e t e n k a n n : C(A) :={(X,Y) : ( 3 x , y e V * ) ^ - S - H * x X A Y y ) } 8 R Die K o n s t r u k t i o n dieser Mengen k a n n nach folgendem Schema erfolgen: Ist eine P r o d u k t i o n gegeben der F o r m :
so n i m m in C(A) alle (X,Y) m i t YGT auf, falls gilt:
( Z , . . . XAD . . . ) ( Z , . . . XA) (Z,AD . . . ) (Z,A)
Da*Y Z|ua*Y Xfia*Z a Da*Y C(Z)
Dabei sind a und/u die Relationen aus 10.1.2. Rekursiv lassen sich die C(A) für alle A € N bestimmen. Insbesondere ist (i—, —i)GC(S). Für das Beispiel aus 10.1.4 mit E als Startsymbol erhalten wir die folgende Liste: C(E)
C(T)
i—
+
i— —i ( + i— +
(
)
(
i—
—i
( +
+ +
)
+
)
C(P)
C(F)
+ +
—i *
i— —i
(
+
i
*
i—
(
)
*
( + + +
+ +
*
(
*
l_
*
+
*
*
)
*
t
+
t
i— —i
(
t t
(
i—
+ +
-i *
>—
+ +
(
)
(
*
+
i *
* *
)
*
t
+ +
+
t
( >—
t t
t
t
Zu C(T) gehören einmal die vier Elemente von C(E), da eine P r o d u k t i o n ( E , T ) existiert (4. Zeile des Konstruktionsschemas). Das E l e m e n t (+,+) wird aufgrund der 2. Zeile des Konstruktionsschemas a u f g e n o m m e n : In der P r o d u k t i o n (E,E+T) übernimmt T die Rolle von A, + die von X u n d E die von Z ; da in der gleichen P r o d u k t i o n + auf E folgt, gilt Eß+, u n d + ü b e r n i m m t so die Rolle von Y. Tritt z.B. *vt auf, so k a n n aufgrund der Zugehörigkeit des K o n t e x t e s ( * , t ) zu C(P) mit der P r o d u k t i o n (P,v) reduziert werden, ebenso anschließend m i t (F,P). Dagegen ist nicht m e h r m i t (T,F) reduzierbar, da der K o n t e x t ( * , t ) nicht zu C(T) gehört. Wie alle bisher in diesem Abschnitt betrachteten Verfahren, k a n n auch dieses in dem Ablaufdiagramm von 9.3.5 zur Bestimmung der benötigten P r o d u k t i o n eingesetzt werden. 8
Der Ableitungspfeil mit dem Zusatz R bezeichne antikanonische Ableitungen.
10. Rekognitionsverfahren
264
10.2.6 LR(k)-Grammatiken Eine Verallgemeinerung dieses Konzepts sind die LR(k)-Grammatiken, die die größte bekannte Klasse kontextfreier Grammatiken darstellt, die sich mit deterministischen Kellerautomaten erkennen lassen. Im Gegensatz zu den bisher betrachteten Verfahren wird hier der gesamte Kellerinhalt als linker Kontext berücksichtigt, unabhängig von der Länge; auf der rechten Seite werden die nächsten k Symbole in die Entscheidung einbezogen. Eine Grammatik heißt analog 10.2.1 LR(k)-erkennbar, wenn für alle (A,x)€P und alle |w| =k gilt: * * , S -»• vAwz ->• vxwz A S vxwz R
R
R
und LR(k)-reduzierbar, wenn S
R
vAwz -»• vxwz A S * vA'wz' -*• vxwz' R
R
=>A = A' gilt [Knuth 1965], Die Eigenschaft, LR(k)-Grammatik zu sein, ist wiederum für gegebenes k entscheidbar, jedoch nicht, ob es überhaupt ein derartiges k gibt. Eine wesentliche Eigenschaft ist, daß es zu jeder LR(k)-Grammatik (k>2) eine schwach äquivalente LR(k-l)-Grammatik gibt. Man kann daher zu jeder LR(k)-Grammatik eine schwach äquivalente LR(1)-Grammatik konstruieren. Die Grammatiken der meisten üblichen Programmiersprachen lassen sich durch geringfügige Änderungen den LR(1)-Bedingungen unterwerfen. Die erste Grammatik aus 10.1.4 ist nicht LR(0), da etwa die Reduktion von F zu T nur zulässig ist, wenn * oder + folgt, nicht aber bei t . Die Grammatik ist jedoch LR(1), da sie bereits von (l,l)-LR-beschränktem Kontext ist. Man überlegt sich leicht, daß die Grammatik G mit S: b; aAc. A: b; aSc. nicht von (n,m)-LR-beschränktem Kontext ist, welches n und m man auch immer wählt 9 . Dagegen ist diese Grammatik sogar LR(0)!
10.3 Verfahren des rekursiven Abstiegs Knuth [1971] hat eine systematische Darstellung des Verfahrens des rekursiven Abstiegs angegeben, die wir hier kurz skizzieren wollen. Für zusätzliche Einzelheiten sei auf diese Arbeit und die dort angegebene Literatur hingewiesen. Insbesondere gibt Knuth Kriterien an, für welche Grammatiken das Verfahren in der Lage ist, alle existierenden Ableitungen wieder aufzufinden. Um derartige Kriterien 9
L(G) = [ a p b c p : p > 0 ] . Ist p gerade, so wurde zuletzt (S,b) angewandt, bei ungeradem p (A,b).
10.3 Verfahren des rekursiven Abstiegs
265
formal zu gewinnen, genügt die informelle Beschreibung des Verfahrens (siehe 1.5) nicht. Als Normierung gibt Knuth eine programmierbare „parsing machine" (PM) an; man könnte dies etwa mit „Syntaxerkennungs-Maschine" übersetzen. Im Gegensatz zu 10.1 und 10.2 handelt es sich hier um ein Top-down-Verfahren. 10.3.1 PM-Befehle Ein PM-Befehl ist ein Tripel (Bool, AT, AF), bestehend aus einer Bedingung Bool und zwei Aktionen AT und AF, die abhängig vom Resultat der Bedingung ausgeführt werden. Jeder Befehl kann mit einer Marke versehen werden. Als Bedingungen sind nur Marken und terminale Symbole 10 zulässig; als Marken treten die nichtterminalen Symbole auf. Ist x t x 2 - - - x r die zu untersuchende Zeichenkette (x h €T) und zeigt der Index h auf das gerade betrachtete Symbol ( l < h < r ) , so ist die Interpretation folgende: 1. (a, AT, AF) mit dem terminalen Symbol a entspricht: i f x h = a t h e n h : = h + l ; AT eise AF fi Dieser Vorgang ist uns bereits von der Besprechung der Top-down-Strategie in 9.3.4 her bekannt. Im dortigen Ablaufdiagramm handelt es sich um den mittleren Zweig. 2. (A, AT, AF) mit einem nichtterminalen Symbol A if A then AT eise AF fi Dabei bewirkt A den Aufruf der booleschen Prozedur, die im Programm an der mit A markierten Stelle beginnt. A kann seinerseits weitere Prozeduren aufrufen. Für AT und AF können die folgenden Aktionen eingesetzt werden: (a)
(b)
(c) (d)
T (= true): Die gegenwärtig bearbeitete Prozedur wird mit dem Wert true beendet. Es erfolgt Rücksprung aus dem Unterprogramm, und das Programm wird in der Zeile fortgesetzt, die beim Aufruf des Unterprogramms angefangen war. F (= false): Die gegenwärtig bearbeitete Prozedur wird mit dem Wert false beendet. Der Rücksprung erfolgt (wie unter (a)) unter gleichzeitigem Zurücksetzen von h auf den Wert, den es zu Beginn der Prozedur besaß. Marke: Fortsetzung des Verfahrens an der durch die Marke gekennzeichneten Stelle. leer: Das Verfahren wird mit dem nächsten PM-Befehl fortgesetzt.
Im folgenden Beispiel sind nicht alle diese Möglichkeiten benutzt.
10
In der Originalarbeit werden die Marken in eckige Klammern gesetzt.
266
10. Rekognitionsverfahren
10.3.2 Beispiel Die schematische Übertragung einer Grammatik (Backus-Naur-Form) in ein PMProgramm führt bei linksrekursiven Symbolen zu Endlosschleifen im PMProgramm: Aufgrund der Produktion (EXPR, EXPR+TERM) ist in die Prozedur zur Erkennung von EXPR die PM-Befehlsfolge EXPR
EXPR + TERM
F F F
T
aufzunehmen, wobei die Bedingung im ersten Befehl zum Wiederaufruf des gleichen Befehls fuhrt. Wir betrachten daher nicht unser Standardbeispiel, sondern den von Knuth angegebenen Fall stark vereinfachter Boolescher Ausdrücke: S: B —i. B: R; (B).
R: E=E. E: a; b; (E+E).
In der Abbildung ist das resultierende PM-Programm angegeben. Zur Übung können wir die Bearbeitung des Wortes (a = (b+a)) —i verfolgen: Nr.
Marke
Bedingung
AT
AF
0 1
S
B —1
T
F F
2 3 4 5
B
R
T
6 7 8
R
9 10 11 12 13 14 15
E
(
T
F F F
E
T
F F F
a b
T T
B
) E =
(
E + E
)
T
F F F F F
10.3 Verfahren des rekursiven Abstiegs
267
Zunächst wird h = l gesetzt und das Programm bei S gestartet. Im Befehl 0 wird die Prozedur B aufgerufen; diese ruft R und diese E auf. In Befehl 9 wird das erste Zeichen des Eingabewortes mit a verglichen; da es sich um eine öffnende Klammer handelt, wird zum nächsten Befehl übergegangen und weiter zum Befehl 11. Dort fällt der Vergleich positiv aus, so daß der Zeiger im zu untersuchenden Wort erhöht wird (h=2). Im folgenden Befehl wird erneut die Prozedur E aufgerufen. Gleich ihr erster Befehl führt zu einem erfolgreichen Rücksprung, da der Zeiger auf a steht. (Er rückt dabei auf = vor.) Der neue Zeigerstand bewirkt jedoch in Befehl 13 den Abbruch der Prozedur E, wobei der Zeiger auf den Stand zurückgesetzt wird, der bei dem zugehörigen Prozedureintritt (hier: dem ersten) galt. Wir sind also in eine Sackgasse gelaufen und kehren nun zu einer anderen Möglichkeit zurück (Back-up). Damit wird auch aufgrund von Befehl 6 die Prozedur R abgebrochen, so daß über das AF-Feld von Befehl 2 der Befehl 3 erreicht wird. Diese Alternative führt dann nach weiteren Schritten zum Ziel. Zum Schluß zeigt h hinter das Begrenzungszeichen. Der Erfolg ist daran erkennbar, daß S mit true verlassen wird. Da — wie wir gesehen haben — die PM-Maschine in der Lage ist, in gewissem Umfang aus Sackgassen herauszufinden, sprechen wir von einem Verfahren mit partiellen Back-up, jedoch sind nicht alle Sackgassen erkennbar, wie wir noch sehen werden. 10.3.3 Konstruktion von PM-Programmen Vergleichen wir in 10.3.2 die Grammatik und dasPM-Programm, so erkennen wir ein einfaches Prinzip zur Konstruktion von PM-Programmen aus kontextfreien Grammatiken: Wie in der BNF üblich, seien die Produktionen mit gleicher linke Seite zusammengefaßt: X: Y j ; Y 2 ; . . ., Y m ; Z j Z 2 . . . Z n . (Yj, Z,GV, m,n>0). Ferner sollen alle rechten Seiten (mit höchstens einer Ausnahme) aus genau einem Symbol bestehen (Standardform). Der Fall n=0 bedeutet, daß das leere Wort rechte Seite zu X ist. Im Fall n+m#0 gehört zu X das folgende PM-Programm:
268
10. Rekognit ionsverfahren
X
Y! Y2
T T
Ym Zi Z2
T
Zn-! Zn
*
F F
T
F F
Im Fall n> 0 bleibt das durch * gekennzeichnete Feld leer. Im Fall n=0 wird dort T eingesetzt. Zwar hat dann keines der Symbole Yj zum Ziel geführt, aber für X kann das leere Wort stehen. (Wird nicht die Standardform vorausgesetzt, so besteht keine Möglichkeit, unter Zurücksetzen des Zeigers mit einer anderen Alternative innerhalb der gleichen Prozedur zu beginnen, wenn ein anderes als das erste Symbol einer rechten Seite zur Erkennung einer Sackgasse führte.) Im Fall n = n = 0 (nur das leere Wort ist rechte Seite) darf der Zeiger im Eingabewort nicht erhöht werden, was bei positiv ausfallendem Vergleich mit terminalen Symbolen automatisch erfolgt. Daher ist ein Trick nötig. Wir schreiben für X folgende PM-Prozedur:
wobei Q ein neues Symbol und a ein beliebiges terminales Symbol ist. 10.3.4 Back-up-freie PM-Programme Jede kontextfreie Grammatik kann leicht in die Standardform überführt werden. Seien die Produktionen mit der linken Seite X: X:ul>u2>...;um;Z1Z2
.. . Z n .
(UjGV*). Dann werden nichtterminale Symbole Y j , . . ., Y m neu eingeführt und die gegebenen Produktionen ersetzt durch Y!; Y 2 ; . •.; Ym;Z1Z2 Y^ Uj. (i = 1,. .., m)
• • • Zn.
Dann ist jedes von dem zugehörigen PM-Programm erkannte Wort zwar in L(G) enthalten, aber es ist nicht entscheidbar, ob das PM-Programm alle Wörter von
10.3 Verfahren des rekursiven Abstiegs
269
L(G) akzeptiert. Betrachten wir etwa die Produktionen S: Y; aa. X: a; ba. so kann das nach dem angegebenen Verfahren konstruierte PM-Programm das Wort aa nicht erkennen. Von besonderem Interesse sind die PM-Programme ohne Back up. Dies ist genau dann der Fall, wenn der Zeiger auf dem Eingabewort niemals erniedrigt wird. Das bedeutet, daß F nur als Aktion beim ersten Symbol der letzten Alternative einer rechten Seite erscheinen darf. In allen anderen Fällen (Sackgassen) ist es durch eine spezielle Marke ERROR zu ersetzen, die den Abbruch des Verfahrens anzeigt. Damit ein so konstruiertes PM-Programm dann noch alle Wörter einer Sprache akzeptiert, muß ihre Grammatik vier Bedingungen erfüllen, wobei die Standardform vorausgesetzt wird: 1. Sie enthält keine linksrekursiven Symbole, d.h. keine Symbole, für die gilt A-» Aw.
*
2. Ist a(w) : = {aST: (3zGV*)(w az)}> die Menge der Symbole, mit denen eine aus w ableitbare Zeichenkette beginnen kann, so müssen a(Y t ) , a ( Y 2 ) , . . . , a ( Y m ) , a{ZlZ2 . • - Z n ) paarweise disjunkt sein. 3. Ist I/J(X) := {aGT: (3w,z€V*)(S wXaz)} die Menge aller Symbole, die in einer ableitbaren Zeichenkette auf X folgen können, und gilt Z j Z 2 . . . Z n -»• e, so müssen alle A(YJXV(X) = - M'(B).
Wir können dann ohne Änderung der erzeugten Wortmenge alle Produktionen der Form (A,B) mit B€N aus der Menge der Produktionen entfernen, wenn wir anstelle der (A,B) alle Produktionen (A,w) mit wGM(B) in P aufnehmen. Da Da B^M(B) ist, entsteht keine neue Produktion mit B als rechter Seite. Nach endlich vielen Wiederholungen mit den in Frage kommenden nichtterminalen Symbolen B bricht das Verfahren also ab. Im Falle unseres Standardbeispiels, das wir etwas vereinfachen zu: S: F; S+F. F: G; F*G. G: a; (S). erhalten wir mit M(G) = und M(F) = die neue Produktionenmenge S: S+F; F*G; a; (S). F: F*G; a; (S). G: a; (S). Nach dieser Vorbereitung führt folgendes Verfahren zu einer Grammatik ohne linksrekursive Symbole [Kurki-Suonio 1966]: Sei A ein linksrekursives Symbol und seien die Produktionen mit A als linker Seite: (*)
A: A £ j ; A £ 2 ; . •
; A £
m
• ; * ? „ •
Dabei beginne kein rj; mit A (n,m>0; | j , rjjSV*). Wir führen ein neues nichtterminales Symbol A' ein, streichen (*) aus der Grammatik und nehmen stattdessen die folgenden Produktionen auf: A: i ? ! ; . . . ; i j n ; i ) i A ' ; . . .;r? n A\ A': . . s l m i ^ A ' ; . . .;£mA'. Ferner werden alle Produktionen (B,A£), wobei B die Bedingung A -»• BI£ A B*A («¿>GV+) erfüllt, ersetzt durch B: t?I£;. . .; i? n |; T/ 1 A'|; . . . ; r ? n A ' l
10.3 Verfahren des rekursiven Abstiegs
271
Dieser Vorgang muß für jedes linksrekursive Symbol A wiederholt werden. Der Nachteil derartiger Umformungen liegt darin, daß die Struktur der Syntaxbäume wesentlich verändert wird und die Zahl der Produktionen drastisch steigt. Für das zuletzt betrachtete Beispiel erhalten wir etwa: S: S': F: F': G:
F*G; a; (S); F*GS'; aS'; (S)S'. +F; +FS'. a; (S); aF'; (S)F'. *G; *GF'. a; (S).
Wir haben hier diese Methode angegeben, weil sie leicht durchschaubar ist. Die resultierende Grammatik ist nicht effektiv.
Literatur Abraham, S. [1965]: Some questions of phrase structure grammars. In: Comput. Linguistics 4 (1965), p. 6 1 - 7 0 . Aho, A. V., P. J. Denning a. J. D. Ullman [1972]: Weak and mixed strategy parsing. In: J. Assoc. Comput. Mach. 19, 2 (1972), p. 2 2 5 - 2 4 3 . Anderson, J. P. [1964]: A note on some compiling algorithms. In: Commun. Assoc. Comp. Mach. 7,3 (1964), p. 1 4 9 - 1 5 0 . APT: The automatic programmed tool system. Dept. Elect. Engin., MIT Cambridge, Mass. 1959. Arden, B. W., B. A. Galler a. R. M. Graham [1962]: An algorithm for translating Boolean expressions. In: J. Assoc. Comput. Mach. 9 (1962), p. 2 2 2 - 2 3 9 . Backus, J. W. [1959]: The syntax and semantics of the proposed international algebraic language of the Zürich ACM-GAMM conference. In: Proc. Int. Conf. Inf. Proc. Paris 1959, p. 1 2 5 - 1 3 2 . München 1960. Baecker, H. D. [1970]: Implementing the ALGOL 68 heap. In: BIT 10, 4 (1970), p. 4 0 5 - 4 1 4 . Baer, J. L., a. D. P. Bovet [1968]: Compilation of arithmetic expressions for parallel computations. In: Proc. IFIP Congr. 1968, vol. I, p. 3 4 0 - 3 4 6 . Bagley, P. R. [1962]: Principles and problems of a universal computer-oriented language. In: Comput. J. 4 (1962), p. 3 0 5 - 3 1 2 . Bagwell, J. T. [1970]: Local optimizations. In: ACM SIGPLAN Notices 5, 7 (1970), p. 5 2 - 6 6 . Bates, F. a. M. L. Douglas [1967]: PL/l-Einführung in die Programmiersprache für den Selbstunterricht. Hanser, München 1967. Bauer, F. L. u. G. Goos [1971]: Informatik (2 Bde). Heidelberger Taschenbücher Bd 80 u. 91. Springer, Berlin/Heidelberg 1971. Bauer, H. R., S. Becker, S. L. Graham a. E. Satterthwaite [1969]: ALGOL W - Language description. Comp. Sc. Dept., Stanford Univ. 1969. Bayer, G. [1971/70]: Einführung in das Programmieren (2 Bde). Teil 1: Einführung in das Programmieren in ALGOL. Teil 2: Programmieren in einer Assemblersprache. De Gruyter, Berlin/New York 1971, 1970. Bayer, G. [1971a]: Programmierübungen in ALGOL 60. De Gruyter, Berlin/New York 1971. Beatty, J. C. [1972a]: An axiomatic approach to code optimization for expressions. In: J. Assoc. Comput. Mach. 19, 4 (1972), p. 6 1 3 - 6 4 0 und 20, 1 (1973), p. 188. Beatty, J. C. [1972b]: A global register assignment algorithm. In: R. Rustin (ed.): Design and optimization of compilers, p. 6 5 - 8 8 . Prentice Hall, Englewood Cliffs, N.J. 1972. Bell, J. R. [1969]: A new method for determining linear precedence functions for precedence grammars. In: Commun. Assoc. Comput. Mach. 12, 10 (1969), p. 5 6 7 - 5 6 9 . Berthaud, M. a. M. Griffiths [1973]: Incremental compilation and conversational interpretation. In: Ann. Rev. Autom. Progr. 7, 2 (1973), p. 9 5 - 1 1 4 . BLISS Reference Manual. Comp. Sc. Dept. Report. Carnegie-Mellon-University, Pittsburgh, Pa. 1970. Bobrow, D. G. a. B. Wegbreit [1973]: A model and stack implementation of multiple environments. In: Comm. Assoc. Comput. Mach. 16, 10 (1973), p. 5 9 1 - 6 0 3 . Böhling, K. M. u. K. Indermark [1969]: Endliche Automaten I. Bibl. Inst., Mannheim 1969. Bolliet, L. [1965]: L'évolution des techniques de compilation. 3 e Congrés de calcul et de traitement de l'information AFCALTI, Paris 1965, p. 2 1 - 5 8 . Bolliet, L. [1966]: L'écriture des compilateurs. In: Revue frani;. Traitement Information 9, 1 (1966), p. 4 7 - 7 3 .
Literatur
273
Bolliet, L. [1968]: Compiler writing techniques. In: F. Genuys (ed.): Programming languages, p. 1 1 3 - 2 8 9 . Bottenbruch, H. [1958]: Übersetzung von algorithmischen Formalsprachen in die Programmsprachen von Rechenmaschinen. In: Z. f. math. Logik u. Grundlagen d. Math. 4 (1958), p. 1 8 0 - 2 2 1 . Brauer, W. [1973]: Automatentheorie. Teubner, Stuttgart 1973. Breuer, M. A. [1969]: Generation of optimal code for expressions via factorization. In: Commun. Assoc. Comput. Mach. 12, 6 (1969), p. 3 3 3 - 3 4 0 . Brooker, R. A., D. Morris a. J. S. Röhl [1967]: Compiler-compiler facilities in ATLAS Autocode. In: Comput. J. 9 (1967), p. 3 5 0 - 3 5 2 . Brooker, R. A., D. Morris a. J. S. Röhl [1967]: Experience with the compiler-compiler. In: Comput. J. 9 (1967), p. 3 4 5 - 3 4 9 . Brooker, R. A. a. J. S. Röhl: Simply partitioned data structures: the compiler-compiler reexamined. In: N. L. Collins, D."Michie (eds.): Machine Intelligence I, p. 2 2 9 - 2 3 9 . Brown, S. A., C. E. Drayton a. B. Mittman [1963]: A description of the APT language. In: Commun. Assoc. Comput. Mach. 6, 11 (1963), p. 6 4 9 - 6 5 8 . Chartres, B. A. a. J. J. Florentin [1968]: A universal syntax-directed top-down analyzer. In: J. Assoc. Comput. Mach. 15 (1968), p. 4 4 7 - 4 6 4 . Cheatham, T. E. [1965]: The TGS-II translator-generator system. In: Proc. IFIP-Congr., New York 1965, p. 5 9 2 - 5 9 3 . Cheatham, T. E. [1971]: The recent evolution of programming languages. In: Preprints IFIP-Congr. 1971, Booklet I, p. 1 1 8 - 1 3 4 . Chomsky, N. [1959a]: On certain formal properties of grammars. In: Inf. Control 2, 2 (1959), p. 1 3 7 - 1 6 7 . Chomsky, N. [1959b]: A note on phrase structure grammars. In: Inf. Control 2, 4 (1959), p. 3 9 3 - 3 9 5 . Collins, N. L. a. D. Michie (eds.) [1967]: Machine intelligence I. Oliver and Boyd, London 1967. Conway, R. W. [1971]: Design and implementation of an error-correcting compiler for PL/1. Vortrag IBM-Seminar 1971, Stuttgart, unveröffentlicht. Conway, R. W. a. W. L. Maxwell [1963]: CORC - the Cornell computing language. In: Commun. Assoc. Comput. Mach. 6, 6 (1963), p. 3 1 7 - 3 2 1 . Culik, K. [1971a]: Combinatorial problems in the theory of complexity of algorithmic nets without cycles for simple computers. In: Aplikace Matematiky 16, 3 (1971), p. 1 8 8 - 2 0 2 . Culik, K. [1971b]: A note on complexity of algorithmic nets without cycles. In: Aplikace Matematiky 16, 4 (1971), p. 2 9 7 - 3 0 1 . Dahl, O. J.: Discrete event simulation languages. In: F. Genuys (ed.): Programming languages, p. 3 4 9 - 3 9 5 . Dahl, O. J., B. Myhrhaug a. K. Nygaard [1968]: SIMULA 67 - common base language. Norwegian Comput. Center, Oslo 1968. Denning, P. J. a. G. S. Graham [1973]: A note on subexpression ordering in the execution of arithmetic expressions. In: Commun. Assoc. Comput. Mach. 16, 11 (1973), p. 7 0 0 - 7 0 2 . Dijkstra, E. W. [1962]: A primer of ALGOL 60 programming. Academic Press, New York 1962. DIN 44300: Informationsverarbeitung - Begriffe. Beuth-Vertrieb GmbH, Berlin 1968. Earley, J. [1970]: An efficient context-free parsing algorithm. In: Commun. Assoc. Comput. Mach. 13, 2 (1970), p. 9 4 - 1 0 2 . Eickel, J. [1972]: Methoden der syntaktischen Analyse bei formalen Sprachen. In: Lect. Notes Econom. Math. Systems, vol. 78, p. 3 7 - 5 3 (2. Jahrestagung d. Gesellsch. f. Informatik, Karlsruhe 1972). Springer, Berlin 1973.
274
Literatur
Engeler, E. (ed.) [1971]: Symposium on semantics of algorithmic languages. In: Lecture Notes in Mathematics, vol. 188. Springer, Berlin 1971. Evans, Jr., A. [1964]: An ALGOL 60 compiler. In: Ann. Rev. Autom. Progr. 4 (1964). Farber, D. J., R. F. Griswold a. I. P. Polonsky [1966]: The SNOBOL 3 programming language. In: Bell Syst. Techn. J. 45, 6 (1966), p. 8 9 5 - 9 4 4 . Fateman, R. J. [1969]: Optimal code for serial and parallel computation. In: Commun. Assoc. Comput. Mach. 12, 12 (1969), p. 6 9 4 - 6 9 5 . Feldman, J. A. [1964]: A formal semantics for computer oriented languages. Doct. Thesis, Carnegie Inst. Techn., Pittsburgh 1964. Feldman, J. A. [1966]: A formal semantics for computer languages and its application in a compiler-compiler. In: Commun. Assoc. Comput. Mach. 9, 1 (1966), p. 3 - 9 . Feldman, J. a. D. Gries [1968]: Translator writing systems. In: Commun. Assoc. Comput. Mach. 1 1 , 2 (1968), p. 7 7 - 1 1 3 . Finkelstein, M. [1968]: A compiler optimization technique. In: Comp. J. 2, 1 (1968), p. 2 2 - 2 5 . Floyd, R. W. [1961a]: An algorithm for coding efficient arithmetic operations. In: Commun. Assoc. Comp. Mach. 4, 1 (1961), p. 4 2 - 5 1 . Floyd, R. W. [1961b]: A descriptive language for symbol manipulation. In: J. Assoc. Comput. Mach. 8, 4 (1961), p. 5 7 9 - 5 8 4 . Floyd, R. W. [1963]: Syntactic analysis and operator precedence. In: J. Assoc. Comput. Mach. 1 0 ( 1 9 6 3 ) , p. 3 1 6 - 3 3 3 . Floyd, R. W. [1964a]: Bounded context syntactic analysis. In: Commun. Assoc. Comput. Mach. 7, 2 (1964), p. 6 2 - 6 7 . Floyd, R. W. [1964b]: The syntax of programming languages. In: IEEE Transact. E C - 1 3 (1964), p. 3 4 6 - 3 5 3 . FORTRAN vs. Basic FORTRAN. In: Commun. Assoc. Comput. Mach. 7, 10 (1964), p. 5 9 1 - 6 2 5 . FORTRAN: Clarification of FORTRAN standards - initial progress. In: Commun. Assoc. Comput. Mach. 12, 5 (1969), p. 2 8 9 - 2 9 4 . FORTRAN: Clarification of FORTRAN standards - second report. In: Commun. Assoc. Comput. Mach. 14, 10 (1971), p. 6 2 8 - 6 4 2 . Fromme, T. [1959]: Ein Adressierprogramm fur die Z22. In: Elektron. Datenverarb., Folge 1 (1959), p. 4 9 - 5 2 . Gates, G. W. a. D. A. Poplawski [1973]: A simple technique for structured variable lookup. In: Commun. Assoc. Comput. Mach. 16, 9 (1973), p. 5 6 1 - 5 6 5 . Gatzhammer, P. [1972]: Erzeugung von optimalem Objektcode. In: Gesellsch. f. Informatik, Bericht No. 4 (1972), p. 3 0 9 - 3 3 6 . Genuys, F. (ed.) [1968]: Programming languages. Academic Press, New York 1968. Germain, C. B. [1970]: Das Programmier-Handbuch der IBM/360. Hanser, München 1970. Gerstmann, H. [1971]: Bemerkungen zur Definition von Untermengen universeller Programmiersprachen. Vortrag IBM-Seminar 1971, unveröffentlichtes Manuskript, van Gils, T. [1972]: The philosophy of an extensible language. Int. Comput. Symposium Venice 1972. Ginsburg, S. [1962]: An introduction to mathematical machine theory. Reading, Mass. 1962. Ginsburg, S. [1966]: Mathematical theory of context-free languages. McGraw-Hill, New York 1966. Ginsburg, S. a. E. H. Spanier [1968]: Control sets on grammars. In: Math. Syst. Theory 2 (1968), p. 1 5 9 - 1 7 7 . Glass, R. L. [1969]: An elementary discussion of compiler/interpreter writing. In: Comput. Surveys 1, 1 (1969), p. 5 5 - 7 7 .
Literatur
275
Goos, G., K. Lagally a. G. R. Sapper [1970]: PS 440 - eine niedere Programmiersprache. Rechenzentrum Techn. Univ. München, Bericht Nr. 7002, Mai 1970. Grau, A. A., U. Hill a. H. Langmaack [1967]: Translation of ALGOL 60 (Handbook of Automatic Computation, vol. Ib). Springer, Berlin 1967. Greibach, S. A. [1965]: A new normal form theorem for context-free phrase structure grammars. In: J. Assoc. Comput. Mach. 12, 1 (1965), p. 4 2 - 5 2 . Greibach, S. A. a. J. E. Hopcroft [1969]: Scattered context grammars. In: J. Comput. Syst. Sciences 3 (1969), p. 2 3 3 - 2 4 7 . Gries, D. [1968]: Use of transition matrices in compiling. In: Comm. Assoc. Comput. Mach. 11, 1 (1968), p. 2 6 - 3 4 . Gries, D. [1971]: Compiler construction for digital computers. Wiley, New York 1971. Gries, D., M. Paul a. H. R. Wiehle [1965]: Some techniques used in the ALCOR ILLINOIS 7090. In: Commun. Assoc. Comput. Mach. 8, 8 (1965), p. 4 9 6 - 5 0 0 . Griffiths, M., M. Peccoud a. M. Peltier [1968]: Incremental interactive compilation. In: Information Processing 68, Preprints, Booklet B, p. 3 3 - 3 7 . Griffiths, T. V. a. S. R. Petrick [1965]: On the relative efficiencies of context-free grammar recognizers. In: Commun. Assoc. Comput. Mach. 8, 5 (1965), p. 2 8 9 - 3 0 0 . Groß, M. u. A. Lentin [1971]: Mathematische Linguistik. Springer, Berlin 1971. Güntsch, F. R. u. W. Kuzenko [1957]: Programm- und Adressenorganisation für die Z22. Recheninstitut TU Berlin 1957. Güntsch, F. R. u. H. J. Schneider [1972]: Einführung in die Programmierung digitaler Rechenanlagen. De Gruyter, Berlin/New York 1972. Hackl, C. [1972]: 1972, 1973. Schaltwerk- und Automatentheorie (2 Bde). De Gruyter, Berlin/New York Hambeck, K. [1973]: Einführung in das Programmieren in COBOL. De Gruyter, Berlin/ New York 1973. Harrison, M. A. [1973]: On covers and precedence analysis Lect. Notes Comput. Science, vol. 1, p. 2 - 1 7 . (3. Jahrestagung Gesellsch. Informatik, Hamburg 1973) Springer, Berlin 1973. Hill, U., H. Langmaack, H. R. Schwarz u. G. Seegmüller [1962]: Efficient handling of subscripted variables in ALGOL 60 compilers. In: Proc. Symp. Symbol. Lang. Data Process., Rome 1962, p. 3 3 1 - 3 4 0 . Hill, U., H. Scheidig u. H. Woessner [1973]: An ALGOL 68 compiler. Abt. Math. TU München, Bericht No. 7301, 1973. Hopcroft, J. E. a. J. D. Ullman [1969]: Formal languages and their relation to automata. Addison-Wesley, Reading, Mass. 1969. Hopgood, F. R. A. [1969]: Compiling techniques. New York 1969. Horwitz, L. P., R. M. Karp, R. E. Miller a. S. Winograd [1966]: Index register allocation. In: J. Assoc. Comput. Mach. 13, 1 (1966), p. 4 3 - 6 1 . Hotz, G. [1974]: Schaltkreistheorie. De Gruyter, Berlin/New York 1974. Hotz, G. u. V. Claus [1972]: Automatentheorie und formale Sprachen III. BI-Taschenbücher Bd. 823a. Mannheim 1972. Hotz, G. u. H. Walter [1969]: Automatentheorie und formale Sprachen II. Bibl. Inst. Mannheim 1969. Huskey, H. D. a. W. H. Wattenburg [1961]: Compiling techniques for Boolean expressions and conditional statements in ALGOL 60. In: Commun. Assoc. Comput. Mach. 4, 1 (1961), p. 70 ff. Ichbiah, J. D. a. S. P. Morse [1970]: A technique for generating almost optimal Floyd-Evansproductions for precedence grammars. In: Commun. Assoc. Comput. Mach. 13, 8 (1970), p. 5 0 1 - 5 0 8 . Ingerman, P. Z. [1966]: A syntax-oriented translator. New York 1966.
276
Literatur
Irons, E. T. [1961]: A syntax directed compiler for ALGOL 60. In: Commun. Assoc. Comput. Mach. 4, 1 (1961), p. 5 1 - 5 5 . Neudruck in: S. Rosen (ed.): Programming systems and languages, p. 2 9 8 - 3 0 9 . McGraw-Hill, New York 1967. Irons, E. T. [1963]: An error-correcting parse algorithm. In: Commun. Assoc. Comput. Mach. 6, 11 (1963), p. 6 6 9 - 6 7 3 . Iverson, K. E. [1962]: A programming language. Wiley, New York 1962. Jorrand, P. u. D. Bert [1972]: On some basic concepts for extensible programming languages. Int. Comput. Sympos., Venice 1972, Preprint, p. 2 - 1 6 . Kampe, G. [1971]: SIMSCRIPT. Vieweg, Braunschweig 1971. Karr, H. W., H. Kleine a. H. M. Markowitz [1966]: Simscript 1.5. California Analysis Center, Santa Monica 1966. Kasami, T. a. K. Torii [1969]: A syntax-analysis procedure for unambiguous context-free grammars. In: J. Assoc. Comput. Mach. 16 (1969), p. 4 2 3 - 4 3 1 . Katzan, H. [1970]: APL programming and computer techniques. Van Nostrand, New York 1970. Katzan, W. [1969]: Batch, conversational, and incremental compilers. In: Proc. AFIPS 1969 SJCC, vol. 34, p. 4 7 - 5 6 . Kemeney, J. G. a. T. E. Kurtz [1967]: BASIC programming. Wiley, New York 1967. Kennedy, K. [1972]: Index register allocation in straight line-code and simple loops. In: R. Rustin (ed.): Design and optimization of compilers, p. 5 1 - 6 3 . Prentice Hall, Englewood Cliffs, N.J. 1972. Knuth, D. E. [1965]: On the translation of languages from left to right. In: Inf. Contr. 8 (1965), p. 6 0 7 - 6 3 9 . Knuth, D. E. [1967]: The remaining trouble spots in ALGOL 60. In: Commun. Assoc. Comput. Mach. 10 (1967), p. 6 1 1 - 6 1 8 . Knuth, D. E. [1971]: Top-down syntax analysis. In: Acta Informatica 1 (1971), p. 7 9 - 1 1 0 . Koster, C. H. A. [1971]: Affix grammars. In: Algol 68 implementation, p. 9 5 - 1 0 9 . Amsterdam 1971. Koster, C. H. A. [1972]: Towards a machine-independent ALGOL 68 translator. Gesellsch. fur Informatik, 2. Fachtagung Programmiersprachen 1972, p. 2 2 1 - 2 3 5 . Kuno, S. a. A. G. Oettinger [1962]: Multiple-path syntactic analyzer. In: C. M. Popplewell (ed.): Information Processing 62, p. 3 0 6 - 3 1 2 . North Holland, Amsterdam 1962. Kurki-Suonio, R. [1966]: On top-to-bottom recognition and left recursion. In: Commun. Assoc. Comput. Mach. 9, 7 (1966), p. 5 2 7 - 5 2 8 . Lang, C. A. [1969]: SAL-systems assembly language. In: AFIPS Conf. Proc. vol. 34 (1969 SJCC), p. 5 4 3 - 5 5 5 . Langmaack, H. [1973]: On correct procedure parameter transmission in higher programming languages. In: Acta Inf. 2 (1973), p. 1 1 0 - 1 4 2 . Leavenworth, B. M. [1972]: Programming with(out) the GOTO. In: ACM SIGPLAN Notices 7, 11 (1972), p. 5 4 - 5 8 . Lecht, C. P. [1968]: The programmer's PL/1. New York 1968. Ledgard, H. F. [1971]: Ten mini-languages: a study of topical issues in programming languages. In: Comput. Surveys 3, 3 (1971), p. 1 1 5 - 1 4 6 . Lee, J. A. [1967]: The anatomy of a compiler. New York 1967. Levy, J. P. [1972]: Automatic correction of syntax errors in programming languages. Gesellsch. f. Informatik, Bericht No. 4 (1972), p. 1 6 5 - 1 8 7 . Lindsey, C. H. a. S. G. van der Meulen [1971]: Informal introduction to ALGOL 68. NorthHolland, Amsterdam 1971. Lucas, P. [1971]: Formal definition of programming languages and systems. IFIP-Congress 1971 (Preprints), Booklet I, p. 1 1 0 - 1 1 7 .
Literatur
277
Lucas, P. a. K. Walk [1969]: On the formal definition of PL/1. In: Ann. Rev. Autom. Progr. 6, 3 (1969). Lynch, W. C. a. H. L. Pierson [1969]: A finite state transducer model for compiler lexical scanners. In: Information Processing 68, p. 4 4 8 - 4 5 5 . North-Holland, Amsterdam 1969. Mägerle, E. [1974]: Einführung in das Programmieren in BASIC. De Gruyter, Berlin/ New York 1974. Martin, D. F. [1972]: A Boolean matrix method for the computation of linear precedence functions. In: Commun. Assoc. Comput. Mach. 15, 6 (1972), p. 4 4 8 - 4 5 4 . Maurer, H. [1969]: Theoretische Grundlagen der Programmiersprachen. BI-Taschenbiicher, Mannheim 1969. Mavaddat, F. [1971]: Using stacks to detect expression parallelism. In: Proc. IFIP Congr. 1971, Preprints TA-4, p. 3 1 - 3 5 . McAfee, J. a. L. Presset [1972]: An algorithm for the design of simple precedence grammars. In: i. Assoc. Comput. Mach. 19, 3 (1972), p. 3 8 5 - 3 9 5 . McCarthy, J. et al. [1962]: LISP 1.5 Programmer's manual. MIT Comput. Center, Cambridge, Mass. 1962. McKeeman, W. M. [1965]: Peephole optimization. In: Commun. Assoc. Comp. Mach. 8, 7 (1965), p. 4 4 3 - 4 4 4 . McKeeman, W. M. [1966]: An approach to computer language design. Techn. Report CS48. Comp. Sc. Dept., Stanford Univ., Stanford, Calif. 1966. van der Meulen, S. G. u. P. Kiihling [1974]: Programmieren in ALGOL68 (2 Bde). I. Einführung in die Sprache. De Gruyter, Berlin/New York 1974. II. In Vorbereitung. Mickunas, M. D. a. V. B. Schneider [1973]: A parser-generating system for constructing compressed compilers. In: Commun. Assoc. Comput. Mach. 16, 11 (1973), p. 6 6 9 - 6 7 6 . Mock, O. et al. [ 1958]: The problem of program communication with changing machines, a proposed solution. In: Commun. Assoc. Comput. Mach. 1 (1958), p. 1 2 - 1 8 ; 1, 9 (1958), p. 9 - 1 5 . Morgan, H. L. [1970]: Spelling corrections in systems programs. In: Commun. Assoc. Comput. Mach. 13, 2 (1970), p. 9 0 - 9 4 . Morgan, H. L. a. R. A. Wagner [1971]: PL/C - the design of a high - performance compiler for PL/1. In: Proc. AFIPS 1971 SJCC, vol. 38, p. 5 0 3 - 5 1 0 . AFIPS Press, Montvale, N.J. 1971. Nakata, I. [1967]: On compiling algorithms for arithmetic expressions. In: Commun. Assoc. Comp. Mach. 10, 8 (1967), p. 4 9 2 - 4 9 4 . Naur, P. (ed.) [I960]: Report on the algorithmic language ALGOL 60. In: Numer. Math. 2 (1960), p. 1 0 6 - 1 3 6 ; Commun. Assoc. Comput. Mach. 3 (1960), p. 2 9 9 - 3 1 4 . Naur, P. (ed.) [1963]: Revised report on the algorithmic language ALGOL 60. In: Numer. Math. 4 (1963), p. 4 2 0 - 4 5 3 ; Commun. Assoc. Comput. Mach. 6 (1963), p. 1 - 1 7 ; Comput. J. 5 (1963), p. 3 4 9 - 3 6 7 . Niemeyer, G. [1973]: Einführung in das Programmieren in ASSEMBLER. De Gruyter, Berlin/New York 1973. PL/1: Language specifications. IBM Systems Reference Library. Pollack, B. W. [1972]: Compiler techniques - a bibliography. In: Comp. Reviews 13, 9 (1972), p. 4 2 7 - 4 5 0 . Price, C. E. [1971]: Table lookup techniques. In: Comput. Surveys 3, 2 (1971), p. 4 9 - 6 6 . Rain, M. [1973]: Operation expressions in MARY. In: SIGPLAN Not. 8, 1 (1973), p. 7 - 1 4 . Ramamoorthy, C. V. a. M. J. Gonzales [1971]: Subexpression ordering in the execution of arithmetic expressions. In: Commun. Assoc. Comput. Mach. 14, 7 (1971), p. 4 7 9 - 4 8 5 . Randal, J. M. (ed.) [1970]: Proceedings Symposium Compiler Optimization. ACM SIGPLAN Notices 5, 7 (1970).
278
Literatur
Randell, B. a. L. J. Rüssel [1964]: ALGOL 60 implementation. Academic Press, New York 1964. Redziejowski, R. R. [1969]: On arithmetic expressions and trees. In: Commun. Assoc. Comput. Mach. 12, 2 (1969), p. 8 1 - 8 4 . Rich, R. [1961]: APT - a common computer language. In: Ann. Review Automat. Programm. 2 (1961), p. 1 4 1 - 1 6 0 . Richards, M. [1969]: BCPL - a tool for compiler writing and system programming. In: AFIPS Conf. Proc. vol. 34 (1969), p. 5 5 7 - 5 6 6 . Roberts, P. S. a. C. S. Wallace [1971]: A microprogrammed lexical processor. In: C. V. Freiman (ed.): Information Processing 71, p. 5 7 7 - 5 8 1 . North-Holland, Amsterdam 1972. Rosen, S. [1967]: Programming systems and languages. McGraw-Hill, New York 1967. Rosen, S. [1972]: Programming systems and languages. In: Commun. Assoc. Comput. Mach. 15, 7 (1972), p. 5 9 1 - 6 0 0 . Rosenkrantz, D. J. [1969]: Programmed grammars and classes of formal languages. In: J. Assoc. Comput. Mach. 16 (1969), p. 1 0 7 - 1 3 1 . Rosenkrantz, D. J. a. P. M. Lewis II [1970]: Deterministic left corner parsing. In: Proc. IEEE Symp. Switching Automata Theory 11 (1970), p. 1 3 9 - 1 5 2 . Rutishauser, H. [1952]: Automatische Rechenplanfertigung bei programmgesteuerten Rechenmaschinen. In: Mitteilungen Inst. f. Angew. Math. ETH Zürich, Nr. 3 (1952). Samelson, K. u. F. L. Bauer [1959]: Sequentielle Formelübersetzung. In: Elektron. Rechenanl. 1 (1959), p. 1 7 6 - 1 8 2 . Samelson, K. a. F. L. Bauer [I960]: Sequential formula translation. In: Commun. Assoc. Comput. Mach. 3, 2 (1960), p. 7 6 - 8 3 . Sammet, J. E. [1961a]: A detailed description of COBOL. In: Ann. Review Automat. Programm. 2 (1961), p. 1 9 7 - 2 3 0 . Sammet, J. E. [1961b]: A method of combining ALGOL and COBOL. In: Proc. West. Joint Comp. 1961, p. 3 7 9 - 3 8 7 . Sammet, J. E. [1969]: Programming languages: History and fundamentals. Prentice-Hall, Englewood Cliffs, N.J. 1969. Sammet, J. E. [1972a]: Programming languages: History and future. In: Commun. Assoc. Comput. Mach. 15, 7 (1972), p. 6 0 1 - 6 1 0 . Sammet, J. E. [1972b]: An overview of programming languages for special application areas. In: AFIPS Conf. Proc. vol. 4 0 (1972 SJCC), p. 2 9 9 - 3 1 1 . Sammet, J. E. a. E. R. Bond [1964]: Introduction to FORMAC. In: IEEE Trans. EC-13, 4 (1964), p. 3 8 6 - 3 9 4 . Sanborn, T. G. [1959]: SIMCOM - the simulator compiler. In: Proc. East. Joint Comp. Conf. 1959, p. 1 3 9 - 1 4 2 . Scheidig, H. [1971]: Representation and equality of modes. In: Information Processing Letters 1, 2 (1971), p. 6 1 - 6 5 . Schmid, H. A. [1972]: A user oriented and efficient incremental compiler. In: Proc. Int. Comput. Sympos. Venice 1972, p. 2 5 9 - 2 6 9 . Schneider, H. J. [1969]: Zur Verwendung boolescher Übergangsmatrizen in Compilern. In: Elektron. Rechenanl. 11, 6 (1969), p. 3 3 6 - 3 3 7 . Schneider, H. J. u. D. Jurksch [1970]: Programmierung von Datenverarbeitungsanlagen. De Gruyter, Berlin 1970. Schulz, A. [1975]: Einführung in das Programmieren in PL/1. De Gruyter, Berlin/New York 1975. S. A. (ed.) [1971]: Proceedings of the international symposium on extensible Schuman, languages. In: SIGPLAN Notices 6, 12 (1971). Sethi, R. a. J. D. Ullman [1970]: The generation of optimal code for arithmetic expressions. In: J. Assoc. Comput. Mach. 17, 4 (1970), p. 7 1 5 - 7 2 8 .
Literatur
279
Shaw, C. J. [1963]: A specification of JOVIAL. In: Commun. Assoc. Comput. Mach. 6, 12 (1963), p. 7 2 1 - 7 3 5 . Shaw, C. J. [1964]: JOSS - a designer's view of an experimental on-line computing system. In: Proc. AFIPS 1964 FJCC, p. 4 5 5 - 4 6 4 . Siebert, H. [1974]: Höhere FORTRAN-Programmierung. De Gruyter, Berlin/New York 1974. Smullyan, R. M. [1961]: Theory of formal systems. In: Ann. Math. Studies, vol. 47, Princeton 1961. Spiess, W. E. u. F. G. Rheingans [1974]: Einführung in das Programmieren in FORTRAN. De Gruyter, Berlin/New York 1974. Spiess, W. E. u. G. Ehinger [1974]: Programmierübungen in FORTRAN. De Gruyter, Berlin/ New York 1974. Starke, P. H. [1969]: Abstrakte Automaten. Berlin 1969. Steel, T. B. [I960]: UNCOL. In: D a t a m a t i o n 6 , 1 (1960), p. 1 8 - 2 0 . Steel, T. B. [1961]: A first version of UNCOL. In: Proc. West. Joint Comp. Conf. 1961, p. 3 7 1 - 3 7 8 . Steel, T. B. (ed.) [1966]: Formal language description language. North-Holland, Amsterdam 1966. Stone, H. S. [1967]: One-pass compilation of arithmetic expressions for a parallel processor. In: Commun. Assoc. Comput. Mach. 10, 4 (1967), p. 2 2 0 - 2 3 3 . Suzuki, N. et al. [1971]: The implementation of ALGOL N. In: SIGPLAN Notices 6, 12 (1971), p. 1 5 - 2 2 . Thorlin, J. F. [1967]: Code generation for PIE (parallel instruction execution). In: Proc. AFIPS 1967 Spring Joint Comput. Conf., p. 6 4 1 - 6 4 3 . van der Walt, A. P. J. [1971]: Random context languages IFIP Congr. Amsterdam 1971, p. 6 6 - 6 8 . Wegner, P. [1972]: The Vienna definition language. In: Comp. Surveys 4, 1 (1972), p. 5 - 6 3 . Weizenbaum, J. [1963]: Symmetric list processor. In: Commun. Assoc. Comput. Mach. 6, 9 (1963), p. 5 2 4 - 5 4 4 . Welsh, J. [1972]: An implementation of PASCAL. Gesellsch. f. Informatik, Bericht No. 4 (1972), p. 2 4 5 - 2 5 4 . Welsh, J. a. C. Quinn [1972]: A PASCAL compiler for ICL 1900 series computer. In: Software - Practice and Experience 2, 1 (1972), p. 7 3 - 7 7 . van Wijngaarden, A. (ed.) [1969]: Report on the algorithmic language ALGOL 68. In: Numer. Math. 14 (1969), p. 7 9 - 2 1 8 . Wilhelm, R. [1973]: Syntax- und Semantikspezifikation in der Eingabesprache für einen Compiler-Compiler. Abt. Math. TU München, Bericht Nr. 7301, 1973. Wirth, N. [1965]: Find precedence functions (Algorithm 265). In: Commun. Assoc. Comput. Mach. 8, 10 (1965), p. 6 0 4 - 6 0 5 . Wirth, N. [1968]: PL 360 - A programming language for the 360 computers. In: J. Assoc. Comput. Mach. 15, 1 (1968), p. 3 7 - 7 4 . Wirth, N. [1971a]: The programming language PASCAL. In: Acta Informatica I , 1 (1971), p. 3 5 - 6 3 . Wirth, N. [1971b]: The design of a PASCAL compiler. In: Software - Practice and Experience 1, (1971), p. 3 0 9 - 3 3 3 . Wirth, N. a. C. A. R. Hoare [1966]: A contribution to the development of ALGOL. In: Commun. Assoc. Comput. Mach. 9, 6 (1966), p. 4 1 3 - 4 3 1 . Wirth, N. a. H. Weber [1966]: EULER: A generalization of ALGOL and its formal definition. In: Commun. Assoc. Comput. Mach. 9 (1966), p. 1 1 - 2 3 ; p, 8 9 - 9 9 . Wolf, P. [1972]: A generalized Backus-Naur-like metalanguage for the description of PL/1 attribute trees. In: Preprints Int. Comp. Sympos. Venice 1972, p. 2 5 0 - 2 5 8 .
280
Literatur
Wulf, W. A. [1971]: A systems programming language. Gesellsch. f. Informatik, 1. Fachtagung Programmiersprachen 1971, p. 1 4 - 4 2 . Lecture Notes Economics Math. Systems, vol. 75. Wulf, W. A., D. B. Russell a. A. N. Habermann [1971]: BLISS - a language for systems programming. In: Commun. Assoc. Comput. Mach. 14, 12 (1971), p. 7 8 0 - 7 9 0 . Yngve, V. A. [1958]: A programming language for mechanical translation. In: Mech. Translat. 5 (1958), p. 2 6 - 4 1 . Yoneda, N. [1971]: The description and the structure of ALGOL N. In: SIGPLAN Notices6, 12 (1971), p. 1 0 - 1 4 . Younger, H. H. [1967]: Recognition and parsing of contextfree languages in time n 3 . In: Inf. Contr. 10 (1967), p. 1 8 9 - 2 0 8 . Zemanek, H. [1966]: Semiotics and programming languages. In: Commun. Assoc. Comput. Mach. 9, 3 (1966), p. 1 3 9 - 1 4 3 . Zemanek, H. [1971]: Informale und formale Beschreibung. Vortrag IBM-Seminar 1971, unveröffentlicht. Zimmer, R. [1970]: Zusammenhänge zwischen formalen Sprachen und Syntaxerkennung. In: Angew. Informatik 13, 1 (1971), p. 2 - 1 1 .
Sachregister
ableitbar 214 Ableitung 214, 218, 242 Adreßaufruf 199, 201 Adressenberechnung 161 Adressenteil 33 Affix-Grammatik 36, 231 Akkumulator 62 Akkumulatorverwaltung 65, 151 aktuelle Parameter 199 ALGOL 18, 29, 36, 100, 191, 200, 206 ALGOL W 18,29 ALLOCATE 83, 174, 175 Alphabet 219 angenommene Wortmenge 234, 238 antikanonische Ableitung 242 Anweisung 130 APL 18,29 APT 18,29 Äquivalenz von Automaten 235 Äquivalenz von Sprachen 222 Arbeitskeller s. Keller arithmetischer Ausdruck 32, 37, 57, 107, 109, 148, 162 Artanpassung 73, 74, 75, 82, 163, 201 Artbezeichnung 169 Artdeklaration 172 Artenliste 168, 172, 173 assigned GOTO 193 Attribut 230 Ausgabebereich 83 Automatentafel 92 automatisch verwalteter Speicherbereich 82, 83, 84 autonomer Schritt 237
Blockstruktur 86, 100, 109, 120, 126, 132, 137, 208, 210 boolesche Formel 177,180,187 boolescher Gleichheitstest 181 boolesche Variable 187 boolesche Wertzuweisung 180 Bootstrapping 26 Bottom-up-Verfahren 243, 246, 247 by-to-Element 188, 189, 190 cases-Klausel 193 Chomsky-Grammatik 222 COBOL 18, 29, 36 Codegenerierung 20, 22, 65 COMIT 18,29 COMMON 174, 175 Compiler 15 Compilerbeschreibungssprache 25 Compiler-Compiler 25, 30, 53 Compiler description language 54 Compiler-Erstellung 35 computed GOTO 193
Back-up 249 Backus-Naur-Form 36, 213 BASIC 18,29 Basisadresse 70 Basisadressierung 212 BCPL 18,29 bedingter Sprungbefehl 183 Bedingung 178, 181, 182 Befehl 33 Begrenzer 39, 96, 97, 109, 115, 122 Begrenzerkeller 40, 43, 62 beschränkter Kontext 260 BLISS 1 8 , 2 9 , 7 8 Block 108 Blockliste 109, 121
echt ableitbar 214 Ein-Adreß-Code 33,63 einbettendes Symbol 227 einfache Adresse 60 Eingabebereich 82 Eingabegerät 22, 38 einseitig lineare Grammatik 225 endlicher Atuomat 90, 234 Endzustand 234 Ergebnisregister 62 erkennbar 260 erweiterbare Sprache 28, 31 Execute-Befehl 206 ext 100 EXTERNAL 99, 100, 174, 175
Deklaration 100, 107, 109, 120, 123, 127, 132, 140 Dereferenzierung 75, 156 deterministischer Kellerautomat 238, 241 Dialekt 22 dialogfähiger Compiler 27, 30 direkt ableitbar 214 direkte Adresse 167 Distanzadresse 70 Drei-Adreß-Code 33 dynamisches Feld 77, 83 dynamisch verwalteter Speicherbereich 82
282 Fallunterscheidung 193 fehlerkorrigierender Compiler 28, 31 Fehlermeldung 23, 41 Feld 76 Feldart 170 Felddeklaration 77, 82, 84, 86, 107, 123, 128, 132 flexible Feldgrenzen 77, 174, 175 Floyd-Evans-Programm 55, 92 Floyds Produktionssprache, s. Floyd-EvansProgramm Folgekonfiguration 237 FORMAC 18,29 formale Sprache 213 FORTRAN 18, 29, 36, 100, 200 FREE 83, 174, 175 garbage collection s. Speicherbereinigung gemeinsame Teilausdrücke 162 Genauigkeit 165 globale Variable 82 Grammatik mit Kontrollmenge 232 Halde 176 Hauptspeicher 62 heap 174, 175 Identifikator 88, 90, 94, 109, 118, 132 Implementierungssprache 25 Index 144, 160 Indexausdruck 163 Indexgrenze 129, 142, 144, 160 Indexregister 71 Indikant 169 indirekte Adressierung 70, 71, 76, 167 indizierte Variable 76, 81, 132, 142 Informationsvektor 8 0 , 8 3 , 1 3 2 , 1 7 0 Inkrement 27 INTERNAL 99, 100 Interpretation 1 7 , 1 9 , 2 1 Iteration 220 JOSS 18,29 JOVIAL 18, 29 kanonische Ableitung 242 Kaskadenübersetzung 178 Keller 39, 43, 121, 237 Kellerautomat 236, 237 Klammergebirge 34 Klammerstruktur 34, 147, 160 Kleenesche Sternoperation 220 Kleinrechner 24 kollateraler Ausdruck 163 kombinatorisches System 36 Kommando 131, 148, 149
Sachregister Kommentar 98 Kompilation 17, 19 Konfiguration 237 Konkatenation 209 Konstante 22, 82, 89, 90, 94, 95, 107, 109, 118, 119, 167 konstanter Teilausdruck 163 kontextfreie Grammatik 36, 223, 228 kontextfreie Grammatik mit Löschung 224, 225 kontextsensitive Grammatik 223 kontextsensitive Grammatik mit Löschung 223 kontrolliert verwalteter Speicherbereich 82, 83, 175 Laufliste 191 Laufschleife 188 Laufzeitpaket 82 Left-corner-Verfahren 249 lexikalische Aufbereitung 21, 22, 87 LIFO-Prinzip 39 lineare Adressenfortschaltung 191 lineare Grammatik 224 Linkskontext 242 linksrekursives Symbol 270 LISP 18, 29 lokale Variable 83 LR(k) -erkennbar 264 LR(k)-Grammatik 264 Makrobefehl 23 Marke 84, 107, 109, 120, 123, 130, 132, 139 Markenvariable 195 markierter Zustand 238 Maschinenbefehl 33, 62, 181 . maschinenorientierte Programm iersprache 15, 16 Maschinensprache 15, 16, 25 mehrdeutige Grammatik 218 Metaproduktion 230 Metasprache 37 modifizierte Adresse 70, 71, 172 monotone Grammatik 223 Name 22 Namensaufruf 200, 205 nichtdeterministischer endlicher Automat 236 nichtdeterministischer Kellerautomat 237 nichtterminales Symbol 37, 213, 215 offene Sprungziele 186 Operandenbereitstellung 65 Operandenkeller 40, 43, 62 Operationsteil 33
283
Sachregister Operationszeichen 109, 148, 152 Operator 22, 73 Operatorenvorrang 34, 45, 153, 177, 252 Optimierung 22, 24, 162, 163 Parallelisierung 163 Parameteraufrufprogramm 205, 206 parsing machine 265 partielles Back-up 267 PASCAL 18, 27, 29 Phase 21, 24 PL/I 18, 29, 36, 100, 191, 195 PL 360 18, 29 polnische Notation 23,217 Potenzierung 163 Präfixart 166, 169 Präzedenzverfahren s. Vorrangverfahren Primphrase 252 problemorientierte Programmiersprache 15, 17 Produkt 220 Produktion 213 Produktionensystem 213 Programmiersprache 15 Prozeduranweisung 199 PS440 18, 29 Quellenprogramm 19, 32 Quellensprache 25 Rechenwerk 62 Rechtskontext 242 reduzierbar 260 reduzierte Grammatik 227 Referenzierung 167 Registereinsparung 164 reguläre Menge 225 Rekognitionsverfahren 241 rekursive Adressenberechnung 191 rekursive Prozedur 209,211 rekursiver Abstieg 49, 264 rekursives Symbol 226 sackgassenfrei 249, 268 SAL 18, 29 Satzform 222 Satzmenge 222 schwache Vorranggrammatik 259 Semantik 25, 36 Semantik-Routine 55, 90, 92 Semi-Thue-System 221 sequentielle Formelübersetzung 34, 38, 42 SIMCON 18, 29 SIMSCRIPT 18, 29 SIMULA 18, 29 Skalenfaktor 95
Skip-Befehl 182 SLIP 18, 29 SNOBOL 18, 29 Speicherabbildungsfunktion 78, 132, 162, 171, 201 Speicherbereinigung 83, 176 Speicherung von Feldern 77 Speicherverwaltung 20, 22, 82, 109, 174, 175, 210, 211 Sprache 15 Sprachumfang 35 Sprunganweisung 84, 101, 107, 132, 139 statisch verwalteter Speicherbereich 82 step-until-Element 188 Strukturbaum 23, 216, 243 switch 193, 194 Symboldarstellung 38 Symbolidentifikation 21, 109 Symbolkeller 62 Symbolliste 75, 99, 103, 104, 121, 123, 167 syntaktische Analyse 20, 21, 23, 24 Syntax 25, 36 Teilfelder 170 terminales Symbol 37, 213, 215 Top-down-Verfahren 244, 245 TU-Befehl 206 Überführungsfunktion 234, 237 Übergangsmatrix 39, 90, 125, 135 Unterprogrammaufruf 199 Unterprogrammdeklaration 208 Unterprogrammrückkehradresse 208 Unterprogramm sprung 74 van Wijngaarden-Sprache 37, 229 Verbundart 171 Verbundobjekt 175 Vereinigung von Arten 173 Vergleich 177, 187 Vergleichsoperator 178 Versorgungsblock 79, 83, 132, 133, 199 Verteiler 194 virtuelle Anfangsadresse 79 Vormerktechnik 101, 196 vorrangerkennbar 256 Vorrangfunktion 48, 49, 259 Vorranggrammatik 256 Vorrangmatrix 45 Vorrangrelation 253 Vorrangverfahren 47, 249, 252, 254, 255 Vorwärtssprung 196 Wertaufruf 199, 204 Wertzuweisung 32, 37, 52, 57, 107, 109, 148, 155, 162, 208
284 while-Element 188, 189 Wiener Definitionssprache 36 Wort 219 Wortmenge 219, 222 Wortproblem 222 Wortsymbol 22
Zeichenkette 98 Zerlegungslemma 225
Sachregister Zielausdruck 194 Zielprogramm 32, 182, 190 zusammengesetzte Art 170 zusammengesetzter Operand 165 Zwei-Adreß-Code 33,64 Zwei-Stufen-Grammatik 229 Zwischenergebnis 3 3 , 6 1 , 1 6 4 Zwischenergebniskeller 61, 62, 68, 69, 72, 83, 148, 154, 208 Zwischensprache 25, 40
Register der in den Compiler-Algorithmen benutzten Bezeichnungen
action 148 ACTION 111 ACTION IDENTIFICATION 153 ACTLIST 148, 149 ADAPT LEFT 75 ADAPT RIGHT 75 ADDR 66 address 63 ADDRESS 123 ADDRESS ASSIGNMENT 135 ADIC 66, 75, 148 ARRAY ELEMENT 143, 149 ASSIGNATION 52 ASSIGNMENT 156 BASE 73 BLANK 111 block 105 BLOCK BEGIN 137 BLOCKEND 138 BLOCKLIST 105, 123, 133, 137 BLOCK NUMBER 123,124 BRACKET DELETION 44 CHAR 88 CHARACTER 111, 113 CODE 66, 75, 116, 148 COLUMN 66 COMBINE DECLARATORS 123 COMMAND INTERPRETATION 148, 150 CONCATENATE 112 constant 118 CONSTANT LIST 118 CONTROL COUNTER 94 COPY STACK 133 COUNT 122 COUNTER 142 CREATE INFYECTOR 142, 150 CREATE STORE 66, 69, 70, 71, 151 CST 154 CURRENT BLOCK 123,124 CURRSYMB 44, 50, 56 DCLSUB 128 DECLARER 111 DELETE STORE 66, 69, 70, 72, 151 delimiter 44, 66, 116 DELIMITER LIST 115,116 DELSTACK 40, 43
DESCRNUMB 66 DIGIT 50, 88, 112 DIGVAL 112 DIMENSION 123 DYNSTORE 85, 88 element 44 EOF SYMBOL 87 ERASE 133 ERROR 111, 112, 121 EXAMINE DELIMITER 116 EXPRESSION 52, 145 FACTOR 51 FILE END 112 FINAL INDEX 123 FIRST SYMBOL 105,123,137 FRACTCOUNTER 95 FREE 86 FREE ADDRESS 137 GENASS 59 GENBIN 59 GENERATE 41, 43, 51, 62, 65, 67, 81, 158 GET OPERAND INFORMATION 154 GET PRIORITY 153 GOTO STATEMENT 140 GROUP 111, 116, 121 IDENT1 118 IDENT2 118 IDENT3 118 IDENT4 118 IDENT IDENTIFICATION 134 IDENTIFIER 111 IDLIST 123, 133 INDEX 70, 142 INDEX IDLIST 123 INDICATION 66, 116, 121, 133, 148 IDIRECT 70 INPOINTER 87 INPOINTMAX 87 INSERT 133 instruct 33, 63 INSYMBOL 121, 132, 133, 148 INT 111 INT1 118 INT2 118 INT3 119
286
Register der in den Compiler-Algorithmen benutzten Bezeichnungen
INT4 119 INT5 119 INTERMEDIATE 66, 157 INTEXT 87 . INVCODE 66, 75, 148 LABEL OCCURRENCE 140 LAST COUNTER 122 LAST DELIMITER 4 4 , 1 2 2 LAST SYMBOL 1 0 5 , 1 2 3 , 1 3 7 LEFT ADDRESS 156 LEFT MODE 7 5 , 1 5 3 , 1 5 6 LEFT RANGE 156 LENGTH DELIMITER LIST 116 LETTER 50, 88, 112 LEVEL 86 LINE 66 LINEEND 8 7 , 1 1 1 LINE NUMBER 111 list symbol 123 LMODE 148 LOAD ACCUMULATOR 1 3 8 , 1 5 0 LOAD ACCUMULATOR PROC 152 LOCATION 140 MANI 119 MAN 2 119 MAN 3 120 MAN4 120 MAN5 120 MAN6 120 MAN 7 120 MAXACTLIST 148 MAXARITHMETIC 94 MAX BLOCK NUMBER 1 2 3 , 1 3 7 MAX IDENTIFIER 88 MAX IDLIST 123 MAXINT 89 MAX LENGTH CONSTANT LIST 118 MAXSTACK 121 MEMADDR 70 memory address 7C MESSAGE 112 MODE 66, 118, 121, 123 MODE RESULT 153 MODIFIED 70 NEW DECLARATION 124 NEW LEFT MODE 75 NEW LMODE 148 NEW RIGHT MODE 75 NEW RMODE 148, 149 NEXT ADDR 72 NEXT ADDRESS 8 4 , 8 5 , 8 6 NEXT CHAR 87 NEXT CHARACTER 111
NEXT INSTRUCTION 69 NEXT RELEVANT CHARACTER 111 NEXT SYMBOL 50, 99, 113 NO MORE DECLARATION 127 NUMBER 50 operand 44, 53, 66 operator 66, 75 OPERATOR 153 OPM1 66 OPM2 66 OPSTACK 4 0 , 4 3 OUTCOMMAND 132 OUTIDENTIFIER 121 OUTINSTRUCTION 132 OUTLISTS 1 1 2 , 1 2 1 OUTSYMBOL 1 1 1 , 1 3 2 PARAMETER 133, 150 POPUP 44, 122 PRIMARY 50 PRIORITY 148, 149 PROG 140 PROGRAM 33 PUSHDOWN 44, 122 REAL 111 REF 128, 156 REFERENCE 53 register 63 RELEASE DYNSTORE 1 3 8 , 1 4 0 REORDER IDLIST 124 RESERVE DYNSTORE 138 RESM 66 RESMODE 75, 148 RESULT 65 RIGHT ADDRESS 156 RIGHT MODE 75, 153, 156 RIGHT RANGE 156 RMODE 148 ROW DECLARATION 142 ROWED 124 ROW OF 123 SAF 80 SAVE ACCUMULATOR 138, 150 SAVE ACCUMULATOR PROC 151 SOURCE 54 STACK 43, 44, 50, 65, 121, 148 STACK PART 66, 75 STACK POINTER 72 stack symbol 121 STATE 113 STATEMENT 130, 131, 132 STK 154 SUB 128
Register der in den Compiler-Algorithmen benutzten Bezeichnungen SUBSCRIPTED VARIABLE 143 SURROUNDING BLOCK 105, 123, 137 SYMBOL 111,125 SYNTACTIC 111 TERM 51 TEST INDEX TEST MODE TRANSLATE TRANSMAT
MODE 145 53 OPERATION 158, 159 44
USE INF VECTOR 143, 150 VALUE 118 VAR 154 VARIABLE 50 WIDEN 156, 157 XEDNI 145
287
w DE
G
Walter de Gruyter Berlin-New York de Gruyter Lehrbücher
Fritz Rudolf Güntsch Hans-Jürgen Schneider Hartmut Wedekind
Einführung in die Programmierung digitaler Rechenautomaten 3., völlig neu bearbeitete Auflage Groß-Oktav. 320 Selten. Mit Textabbildungen und 1 Faltkarte. 1972. Gebunden DM 68,- ISBN 311 001969 8
Datenorganisation
2., verbesserte Auflage Groß-Oktav. 271 Seiten. Mit 152 Abbildungen. 1972. Gebunden DM 28,- ISBN 311 004151 0
s. G. van der Meuien Programmieren in ALGOL 68 Peter Kühling
Wolfgang E. Spiess Friedrich E. Rheingans
Harald Siebert
l: Einführung in die Sprache
Groß-Oktav. 228 Seiten. 1974. Plastik flexibel DM 28,ISBN 311 004698 9
Einführung in das Programmieren in FORTRAN
4., verbesserte Auflage Groß-Oktav. 217 Selten. Mit 19 Abbildungen und 13 Tabellen. 1974. Plastik flexibel DM 18,ISBN311 005747 6
Höhere FORTRAN-Programmierung
Eine Anleitung zum optimalen Programmleren In Zusammenarbeit mit der GES Gesellschaft für elektronische Systemforschung e. V., Bühl Groß-Oktav. 234 Seiten. 1974. Plastik-flexibel DM 24,ISBN311 0034751
Klaus Hambeck
Einführung in das Programmieren
in COBOL
Groß-Oktav. VIII, 162 Seiten. 1973. Plastik flexibel DM 18,ISBN 3 11 003625 8
Amo Schulz
Einführung in das Programmieren in PL 1
Groß-Oktav. 306 Seiten. Mit Abbildungen und Tabellen. 1975. Plastik flexibel DM 28,- ISBN 3 11 003970 2
Erich W. Mägerle
Einführung in das Programmieren in BASIC Groß-Oktav. 112 Seiten. 1974. Plastik flexibel DM 18,ISBN311 004801 9 Preisänderungen vorbehalten