Programmiersprachen und ihre¨Ubersetzer Skript zur Vorlesung im

Werbung
Programmiersprachen und ihre Übersetzer
Skript zur Vorlesung im Sommersemester 2004
Robert Giegerich
Technische Fakultät
Universität Bielefeld
Postfach 10 01 31
33501 Bielefeld
15. April 2005
Inhalt
1 Einleitung
1.1 Ziele der Vorlesung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.2 Allgemeine Hinweise zur Literatur . . . . . . . . . . . . . . . . . . . . . . . . . .
1.3 Zu diesem Skriptum . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2 Sprachen, Interpreter, Übersetzer
2.1 Sprachen und Interpreter . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.1.1 Semantik von Sprachen . . . . . . . . . . . . . . . . . . . . . . . .
2.1.2 Die Interpreter-Gleichung . . . . . . . . . . . . . . . . . . . . . .
2.1.3 Beispiel für eine einfache Sprache und einen Interpreter . . . . . .
2.2 Konstruktion von Übersetzern . . . . . . . . . . . . . . . . . . . . . . . .
2.2.1 Die Übersetzer-Gleichung . . . . . . . . . . . . . . . . . . . . . .
2.2.2 Beispielübersetzer: Norma2 → Haskell . . . . . . . . . . . . . . .
2.3 Partielle Auswertung und ihr Einsatz zur Konstruktion von Übersetzern .
2.3.1 Nutzung des Interpreters zur Übersetzerkonstruktion . . . . . . .
2.3.2 Die Partieller-Auswerter-Gleichung . . . . . . . . . . . . . . . . .
2.3.3 Ein Beispiel zur partiellen Auswertung . . . . . . . . . . . . . . .
2.3.4 Konstruktion des Norma2-Übersetzers durch partielle Auswertung
2.4 Bootstrapping . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.4.1 Kombination von Übersetzern und Interpretern . . . . . . . . . .
2.4.2 Bootstrapping am Beispiel P ascal . . . . . . . . . . . . . . . . . .
3 Syntaxorientierte Übersetzung
3.1 Konkrete und abstrakte Syntax . . . . . . . . . . . . . . . . . . . . .
3.2 Signaturen, Terme, Algebren . . . . . . . . . . . . . . . . . . . . . . .
3.3 Das einfache Ableitungsschema . . . . . . . . . . . . . . . . . . . . .
3.3.1 Der Ableitungsbegriff . . . . . . . . . . . . . . . . . . . . . . .
3.3.2 Übersetzung arithmetischer Ausdrücke in Kellermaschinencode
3.4 Das erweiterte Ableitungsschema . . . . . . . . . . . . . . . . . . . .
3.4.1 Schranken des einfachen Ableitungsschemas . . . . . . . . . .
3.4.2 Ein Beispielübersetzer nach dem Ableitungsschema in Haskell
3.4.3 Quell- und Zielsprache . . . . . . . . . . . . . . . . . . . . . .
3.4.4 Compilezeitaktionen und ihre Datentypen . . . . . . . . . . .
3.4.5 Der Compiler . . . . . . . . . . . . . . . . . . . . . . . . . . .
I
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
1
1
4
6
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
7
7
7
8
9
11
11
12
13
13
14
15
18
22
22
23
.
.
.
.
.
.
.
.
.
.
.
26
26
29
32
33
33
35
35
36
36
37
39
4 Syntaxanalyse
4.1 Problemstellung: Umkehrung der Term-Notation . . . . . . . . . .
4.2 Lexikalische Analyse mit endlichen Automaten . . . . . . . . . . .
4.3 Syntax-Analyse durch Combinator-Parsing . . . . . . . . . . . . .
4.3.1 Von der Notation zur Grammatik . . . . . . . . . . . . . .
4.3.2 Parsen mit Combinatoren . . . . . . . . . . . . . . . . . .
4.3.3 Erweiterungen: Zusatzargumente und Kontextbedingungen
4.4 Literatur zum Kapitel Syntaxanalyse . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
5 Übersetzung imperativer Sprachen
5.1 Prozedurmechanismen in P ascal-artigen Sprachen . . . . . . . . . . . . . . . .
5.1.1 Semantische Aspekte . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5.1.2 Lokaler Aufbau des Prozedurspeicherbereichs (englisch: activation area)
5.1.3 Codeerzeugung für den Umgebungswechsel (Aufrufstelle) . . . . . . . .
5.1.4 Codeerzeugung für Prozedurdeklaration Q . . . . . . . . . . . . . . . .
5.2 Ablegung von Variablen mit komplexer Struktur im Speicher . . . . . . . . . .
5.2.1 Records . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5.2.2 Statische Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5.2.3 Dynamische Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5.2.4 Vereinigungstypen . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5.2.5 Records in Pascal (variant records) . . . . . . . . . . . . . . . . . . . .
5.3 Die Implementierungsabbildung in Haskell . . . . . . . . . . . . . . . . . . . .
6 Codeerzeugung
6.1 Überblick zur maschinenspezifischen Codeerzeugung
6.2 Die Zielmaschine TM . . . . . . . . . . . . . . . . .
6.3 Die Zielsprache TL des Codeerzeugers . . . . . . . .
6.3.1 Verfeinerung der Zielsprache . . . . . . . . .
6.3.2 Repräsentation der Zielsprache TL . . . . .
6.4 Spezifikation des Code-Selektors durch Ableitung .
6.4.1 Ableitung von TL nach IL2 . . . . . . . . .
6.4.2 Vorgehen zur Lösung von cs prog t = ip .
6.5 Syntaxanalyse für Baumgrammatiken . . . . . . . .
6.6 Implementierung des Code-Selektors . . . . . . . .
6.6.1 Von der Grammatik zum Parser . . . . . . .
6.6.2 Tuning des Parsers . . . . . . . . . . . . . .
6.6.3 Die modifizierte Baumgrammatik . . . . . .
6.6.4 Der verbesserte Codeselektor . . . . . . . . .
6.6.5 Zusammenfassung zur Codeauswahl . . . . .
6.7 Lokale Registerallokation und Code-Ausgabe . . . .
6.7.1 Kostenanalyse . . . . . . . . . . . . . . . . .
6.7.2 Code-Ausgabe . . . . . . . . . . . . . . . . .
6.7.3 Lokale Registerallokation . . . . . . . . . . .
6.7.4 Zusammenfassung des Codeerzeugers . . . .
II
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
44
44
46
49
49
50
56
59
.
.
.
.
.
.
.
.
.
.
.
.
60
60
60
63
64
67
68
68
69
69
70
72
73
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
79
79
81
85
85
85
87
87
91
92
97
98
102
104
105
109
110
110
111
113
115
Kapitel 1
Einleitung
1.1
Ziele der Vorlesung
Diese Vorlesung richtet sich an Studierende der “Naturwissenschaftlichen Informatik” (NWI)
an der Universität Bielefeld. NWI ist ein Studiengang der Angewandten Informatik, mit besonderer Ausrichtung auf die Naturwissenschaften. Während in einem klassischen InformatikStudiengang die Bedeutung einer Vorlesung zum Übersetzerbau sich unmittelbar aus der zentralen Rolle der Programmiersprachen und der ihrer zuverlässigen und effizienten Implementierung
ergibt, bedarf es einer kurzen Begründung, warum die Techniken des Übersetzerbaus auch in
einer Angewandten Informatik von erheblichem Lehrwert sind. Schließlich geht die Vorlesung
nicht davon aus, daß ihre Hörer später einmal tatsächlich an der Implementierung einer neuen
Programmiersprache beteiligt sind.
“Programmiersprachen und ihre Übersetzer” beziehen ihren Stellenwert für den Angenwandten
Informatiker im wesentliche aus zwei Tatsachen.
• Wegen seiner zentralen Bedeutung ist das Problem der Implementierung von Programmiersprachen eng mit der Entwicklung der Informatik verknüpft. Es ist eines der ältesten,
schwierigsten, aber zugleich eines der am besten verstandenden Teilgebiete der Informatik. Die Anforderungen an Zuverlässigkeit und Effizienz sind bei Übersetzern besonders
hoch. Deshalb wurde eine Vielzahl von formalen Methoden für die diversen Teilprobleme
der Übersetzung entwickelt, die zum Teil eine automatische Erzeugung von Übersetzern
oder Übersetzerteilen gestatten. Das Paradigma der Erzeugung von Programmen aus
Problembeschreibungen kann am Übersetzerbau beispielhaft studiert werden.
• Viele Anwendungsprobleme lassen sich als Sprach-Implementierungsprobleme auffassen,
und viele der Techniken des Übersetzerbaus lassen sich mit großem Gewinn dort einsetzen,
wo der Laie sie nie vermuten, geschweige denn selbst entdecken würde.
Der Informatiker sieht sich bei seiner Arbeit zumeist in der Situation, dass ein zu lösendes
Problem zunächst nicht präzise spezifiziert ist. Ein Hauptteil der Problemlösung besteht daher
in der Modellierung, das heißt in einer geeigneten Formalisierung des Problems. Eine Formalisierung bedeutet in der Informatik immer die Definition einer formalen Sprache, bestehend
aus Notation (Syntax) und der Bedeutung der Notation (Semantik) innerhalb einer formalen
Modellwelt. Der zweite Teil der Problemlösung besteht dann in der Implementierung dieser
1
Sprache. In den meisten Fällen wird die Sprache eine Teilsprache einer gebräuchlichen Hochsprache sein, so dass Problemlösungen als Programme und Problemausprägungen als Daten für
diese Programme gegeben sind. In anderen Fällen ist es natürlicher, eine spezielle Sprache zu
entwickeln und Problemausprägungen als Programme und Daten dieser Sprache zu formulieren. Ein solches System enthält dann einen Interpretierer oder Übersetzer für diese Sprache.
Die Grenzen zwischen diesen beiden Formen sind fließend. Wenn die Eingabedaten für ein in
einer Standardsprache geschriebenes Programm komplexe Struktur und Semantik haben, so
kann man sie durchaus als eine eigenständige Sprache betrachen. In jedem Fall enthält das verarbeitende Programm dann komplexe Einlese-, Analyse- und Synthesemodule, ähnlich denen
eines Interpretierers oder Übersetzers für eine Hochsprache.
Die unmittelbar einsichtigen Probleme mit formalen Sprachen1 sind die folgenden:
1. Reale Maschinen verstehen nur primitive Sprachen.
2. Probleme im Zusammenhang mit Korrektheit, Modifizierbarkeit, Verstehbarkeit von Programmen können in primitiven Sprachen nicht zufriedenstellend gelöst werden, sondern
erfordern höhere Abstraktionsebenen.
3. Verschiedene Anwendungsprobleme wie auch unterschiedliche Auffassungen in der formalen Modellierung haben zu einer breiten Diversifikation bei Programmiersprachen geführt.
4. Es gibt viele verschiedene Typen von (konkreten wie abstrakten) Rechenmaschinen.
Die Vorlesung beschäftigt sich mit folgenden Fragestellungen:
1. Wie macht man Hochsprachen dem Rechner verständlich? In jedem Fall sollte dieser Vorgang vom Rechner selbst unterstützt werden. Wir werden nur solche Sprachen betrachten,
wo die Sprachimplementierung völlig vom Rechner selbst in Form von Interpretern und
Übersetzern durchgeführt wird.
2. Wie nutzt man den Rechner effektiv? Wichtigste Eigenschaft der Zielmaschine für die
Sprachimplementierung ist der Befehlsvorrat des Prozessors. Man unterscheidet heute
RISC– (reduced instruction set computer) und CISC– (complex instruction set computer)
Prozessoren; innerhalb dieser Klassen unterscheiden sich die Prozessoren im Wesentlichen
kaum, aber im Detail erheblich. Für die Codeerzeugung des Übersetzers kommt es gerade
auf jedes Detail an ...
Wie man sieht, hat das Problem Implementierung von Programmiersprachen“ vielfältige Aus”
prägungen. Für die Vorlesung müssen wir uns die Frage stellen: Wie können wir aus der Vielzahl
der Einzelprobleme und Problemvarianten bei Sprachimplementierungen allgemein anwendbare
Methoden und Techniken abstrahieren?
Diese Vorlesung will folgende grundlegenden Inhalte vermitteln:
1
In dieser Vorlesung werden wir nur über formale Sprachen sprechen, Sprachen, für die es eine mathematisch
exakte Definition der Syntax und Semantik gibt. Im folgenden werden wir das Adjektiv formal“ daher weglassen.
”
2
Die Programme in diesen Sprachen sind nicht immer ausführbar, so dass eine vollständig automatische
Implementierung nicht in allen Fällen existiert.
2
Abbildung 1.1: Sprachfamilien
imperative
Sprachen
Fortran,
Algol,
Pascal,
Modula,
Basic,
C,
Ada,
...
funktionale
Sprachen
LISP,
FP,
Hope,
KRC,
ML,
Miranda,
Haskell,
...
logische
Sprachen2
Prolog,
Aussagenlogik,
Prädikatenlogik,
Spezifikationssprachen wie
CLEAR,
ASL, OBJ,
Larch,
...
objektorientierte
Sprachen
Simula
Smalltalk-80,
C++,
CLASCAL,
Java,
...
Anwendersprachen
VHDL (VLSI-Entwurf),
SQL (Datenbanken),
VAL2 (Robotersteuerung),
MARVIN (CompilerErzeugung),
LATEX(Textverarbeitung),
SGML, HTML, XML,
Dokumentenverarbeitung
...
1. Generell versuchen wir einen Überblick der wichtigsten Techniken des Übersetzerbaus zu
geben. Oft gibt es natürlich unterschiedliche, konkurrierende Techniken für die gleiche
Teilaufgabe. Meistens geben wir Techniken den Vorzug, die wegen ihrer Allgemeinheit
oder einfachen Anwendbarkeit Bedeutung auch über den Übersetzerbau hinaus haben.
2. Am Anfang eines jeden Programmes, also auch bei Übersetzern und Interpretern, muss
eine Problemspezifikation stehen. Die Spezifikation einer Programmiersprache besteht aus
der Definition ihrer Syntax und Semantik. In Kapitel 2 werden diese Begriffe und die
sich daraus ergebenden Korrektheitsbegriffe für Interpreter und Übersetzer auf einer sehr
hohen Abstraktionsebene eingeführt. Auf dieser Ebene werden Zusammenhänge und Unterschiede zwischen Übersetzung und Interpretation diskutiert. Es wird gezeigt, wie man
durch partielle Auswertung Übersetzer erhalten kann. Als weitere grundlegende Technik
zur Erstellung von Übersetzern wird das sogenannte bootstrapping besprochen.
3. Kapitel 3 beschäftigt sich mit Methoden und zugehörigen mathematischen (hier: algebraischen) Modellen zur Fassung von Syntax, Semantik und Übersetzungsfunktionen. Wir
werden die abstrakte Syntax von Programmen durch Signaturen und Terme beschreiben.
Die Semantik einer Sprache ist dann eine Klasse von Algebren zu der entsprechenden
Signatur. Grundlegende Mechanismen für die Definition von Übersetzungen sind “Ubersetzungsschemata.
Als Metasprache3 verwenden wir Haskell, eine lazy funktionale Sprache. Eine Einführung
in Haskell gibt Richard Bird: Introduction to Functional Programming in Haskell, Prenti”
ce Hall 1998“. Funktionale Programmiersprachen sind besonders gut zur Programmierung
von Übersetzern und Interpretierern geeignet. Datenstrukturen, wie sie zur Repräsentation der abstrakten Syntax von Objektprogrammen gebraucht werden, sind hier als Grundtypen vorhanden. Die Programmierung von Interpretierern, aber auch die Wahrung von
3
Eine Sprache, in der man Programme schreibt, die andere Programme als Daten akzeptieren und manipulieren, heißt Metasprache. Die Sprache, deren Programme manipuliert werden, ist die Objektsprache des Systems.
Dabei kann die Objektsprache gleichzeitig Metasprache ihrer eigenen Implementierung sein.
3
Modularität bei Interpretierern und Übersetzern, wird entscheidend erleichtert durch die
Verwendung von Funktionen höherer Ordnung. Syntaxorientierte Programmiertechniken
werden direkt ermöglicht durch musterorientierte Definitionen von Funktionen. Übersetzer sind ja im allgemeinen komplexe Programmsysteme, so dass gerade ihrer Modularität
große Bedeutung zukommt.
4. In Kapitel 4 werden Fragen der syntaktischen Analyse behandelt. Es wird die Problematik
des Übergangs von der konkreten zur abstrakten Syntax aufgezeigt. Wir geben an, wie
man von einer kontextfreien Grammatik unmittelbar zu einem prädiktiven Parser in einer
funktionalen Sprache gelangt. (Zugleich ist die hier entwickelte Kombinator-Notation für
kontextfreie Grammatiken ein Beispiel einer anwendungsspezifischen Sprache.) Spezielle
Sprachklassen (LL(k), LALR(k)) und die zugehörigen Syntaxanalysetechniken werden
nicht besprochen.
5. Kapitel 5 geht dann auf die wesentlichen Techniken und Datenstrukturen ein, die für
die Übersetzung bzw. Interpretation von imperativen, Pascal-artigen Sprachen im Laufe
der Zeit entwickelt worden sind. Bei den imperativen Sprachen werden wir uns auf die
Implementierung rekursiver und geschachtelter Prozeduren und auf die Abbildung von
Datentypen für von-Neumann-artige Registermaschinen konzentrieren.
6. Kapitel 6 behandelt Fragen der Codeerzeugung. Zentrales Thema ist die Spezifikation
der Code-Auswahl durch Umkehrung von Ableitungen, und ihre Implementierung durch
Parser für Baumgrammatiken.
Nicht behandelt werden in dieser Vorlesung:
• Fragen des Entwurfs von Programmiersprachen;
• spezifische Probleme bei der Implementierung von logischen, funktionalen und objektorientierten Programmiersprachen;
• abstrakte Maschinen - ihr Einsatz als Zielsprache des Übersetzers wird nebenbei erwähnt,
ihre systematische Entwicklung wird jedoch nicht betrachtet.
• diverse Techniken der Codeoptimierung;
• Codeerzeugung für parallele Rechnerarchitekturen;
• Einbettung von Interpretierern und Übersetzern in Betriebssysteme und Programmierumgebungen.
1.2
Allgemeine Hinweise zur Literatur
Das Gebiet des Übersetzerbaus ist eines der ältesten innerhalb der Informatik. Entsprechend
umfangreich ist auch die Literatur auf diesem Gebiet. Dies gilt vor allem für Arbeiten aus dem
Bereich der Implementierung klassischer Programmiersprachen der Algol-Pascal-Familie. Wir
wollen hier daher nur einige wichtige Textbücher nennen.
4
• Aho, A.V., Sethi, R. und Ullman, J.D.: Compilers—Principles, Techniques, and Tools.
Addison-Wesley, 1985, 796 S.
Dieses Buch ist eine ausführliche Sammlung der Konzepte, Techniken und Methoden, die man
in der Praxis des Übersetzerbaus braucht. Es behandelt allerdings nur Pascal-artige Objektsprachen, diese dafür sehr ausführlich. Großen Raum nehmen hier die Techniken zur lexikalischen
und syntaktischen Analyse, Codeerzeugung und -optimierung ein. Dieses Buch sei insbesondere empfohlen zur Ergänzung und Vertiefung des Teils über die Implementierung imperativer
Sprachen dieser Vorlesung. Seit einiger Zeit gibt es dieses Buch auch in deutscher Übersetzung,
ebenfalls bei Addison-Wesley.
• Wirth, N.: Übersetzerbau, Teubner-Verlag, 2. Auflage.
Das Buch enthält einen vollständigen Beispielübersetzer für eine einfache Teilsprache von Pascal. Er ist nach dem recursive-descent“-Schema strukturiert und in Modula2 programmiert.
”
Das Buch ist sehr leicht lesbar, enthält aber auch keinerlei formale Modellierungen und keine
fortgeschrittenen Techniken.
• Waite, W.M., Goos, G.: Compiler Construction, Springer 1983.
• U.Kastens : Übersetzerbau. Oldenbourg-Verlag, 1990
Diese Bücher propagieren die Methode der Attributgrammatiken, eine Art funktionale Programmiersprache speziell für den Übersetzerbau, und geben ausführliche Beispiele für ihren
praktischen Einsatz.
• Henderson, P.: Functional Programming - Application and Implementation, Prentice Hall,
Englewood Cliffs, New Jersey, 1980.
• Peyton Jones, S. L.: The Implementation of Functional Programming Languages. PrenticeHall 1987, 445 S.
• Fields, A. J. und Harrison, P. G.: Functional Programming. Addison-Wesley 1988, 602 S.
Die drei letztgenannten Bücher sind sehr empfehlenswerte Ergänzungs- und Vertiefungslektüre
über die Implementierung funktionaler Sprachen.
Sehr umfassend und vor allem aktueller als die vorgenannten ist das Lehrbuch
• Wilhelm, R. und Maurer, D.: Übersetzerbau – Theorie, Konstruktion, Generierung. SpringerLehrbuch, Springer 1992, 608 S.
Es behandelt funktionale, logische und imperative Sprachen und insbesondere auch Techniken
der Compiler-Generierung und der Abstrakten Interpretation.
5
1.3
Zu diesem Skriptum
Dieses Skriptum blickt auf eine lange Geschichte zurück; seine erste Fassung war die einer
Vorlesung von H. Ganzinger an der Universität Dortmund, mit einem Codeerzeugungs-Kapitel
von mir. Dazwischen liegen viele Überarbeitungen und Ergänzungen. Die vorliegende Fassung
dieses Skriptums enthält gegenüber früheren zwei neue Kapitel. Außerdem wurden alle BeispielProgramme von LISP und Miranda nach Haskell umgestellt. Das MiniPascal-Beispiel wurde
erweitert.
Ein kompletter MiniPascal-Compiler liegt im Anhang vor; er wurde von Peter Steffen erstellt.
Bielefeld, im Oktober 2001
6
Kapitel 2
Sprachen, Interpreter, Übersetzer
In diesem Kapitel werden die Begriffe
• Interpreter
• Übersetzer
• partielle Auswertung
• bootstrapping“
”
auf sehr hoher Abstraktionsebene eingeführt, und ihr Zusammenhang wird erklärt.
2.1
2.1.1
Sprachen und Interpreter
Semantik von Sprachen
Im folgenden ist A eine fest vorgegebene Menge. → bezeichnet partielle Funktionen.
Def.: Eine Programmiersprache L ist eine partielle Funktion
L : A → (A∗ → A).
Def.: Der Definitionsbereich von L (d. h. die Teilmenge von A, auf der die partielle Funktion
L total ist) heißt die Menge der zulässigen L-Programme, bezeichnet mit L-Prog.
Pragmatik:
• Hier wird nichts darüber gesagt, wie eine Programmiersprache definiert wird. Selbst die
Syntax der Sprache ist nur implizit definiert. Praktisch wird L-Prog durch eine Grammatik
mit zusätzlichen semantischen Einschränkungen an die Sprache festgelegt.
• A ist groß genug, um alle syntaktischen Objekte (Programme, Programmein- und ausgaben, usw.) zu repräsentieren, z.B. A = ASCII ∗ .
7
• L(l), nachfolgend geschrieben als Ll, ist eine Funktion Ll : A∗ → A und soll die Ein/Ausgabefunktion des Programmes l repräsentieren. Ll ist die Semantik des syntaktischen
Objektes l ∈ L-Prog.
• Nach unseren Voraussetzungen über A hätten wir den Typ von L auch als L : A →
(A → A) definieren können, da man alle Eingaben eines Programms sich auch als ein
Element aus A vorstellen kann. Schließlich sind ja ganze Programme Elemente von A.
Später allerdings werden wir wichtige Unterschiede zwischen den verschiedenen Eingaben
machen; daher unsere Wahl.
• Ein l ∈ A ist ein syntaktisches Objekt. Es kann durchaus mehr als eine Semantik L1 l 6= L2 l
für verschiedene Sprachen L1 und L2 besitzen.
Auf dieser Abstraktionsebene werden nicht erklärt:
• syntaktische Struktur von Programmen, Ein- und Ausgabewerten
• Aspekte der syntaktischen und semantischen Analyse: Wie erkennt man für l ∈ A, ob
l ∈ L-Prog? Üblicherweise ist L-Prog ⊂ A entscheidbar.
• E/A-Funktionen Ll : A∗ → A sind in der Informatik immer partiell rekursiv.
• Strukturierung der Definition von L, etwa anhand der Programmstruktur
Def.: L-Eingabe(l) = {(x1 , ..., xn ) ∈ A∗ | Ll(x1 , ..., xn ) ∈ A} sei der Definitionsbereich von Ll,
für l ∈ L-Prog.
L-Eingabe(l) ist also die Menge der zulässigen Eingabedaten für l unter Semantik L. Analog
die Ausgabe eines Programms:
Def.: L-Ausgabe(l) = Ll(L-Eingabe(l))
2.1.2
Die Interpreter-Gleichung
Def.: Ein Interpreter i für die Sprache L, geschrieben in einer Sprache I, ist ein Programm
i ∈ I-Prog mit:
Ii(l, x1 , ..., xn ) = Ll(x1 , ..., xn ), für alle l ∈ L-Prog und alle (x1 , ..., xn ) ∈ L-Eingabe(l)1 .
L bezeichne die Menge aller Interpreter für L, die in der Sprache I geschrieben sind.
I
Ein Interpreter i für L akzeptiert also als Eingabe L-Programme l und deren Eingabe. Er simuliert das Verhalten von l auf dieser Eingabe, d.h. produziert genau die Ausgabe, die gemäß
der Semantik Ll vorgeschrieben ist. An Stellen, an denen Ll undefiniert ist, braucht auch das
Interpreterverhalten nicht definiert zu sein. Ebenfalls ist nichts über die Situation ausgesagt, in
der unzulässige L-Programme in i eingegeben werden. Praktisch wünscht man sich in einer solchen Situation, dass der Interpreter nicht einfach undefiniert ist, sondern mit einer brauchbaren
Fehlermeldung reagiert. Dieses Verhalten ist aber nicht durch L vorgegeben.
1
Es ist L-Eingabe(l) ⊂ A∗ . Zeichenreihen x1 ... xn ∈ A∗ schreiben wir auch in Tupelschreibweise als
(x1 , ..., xn ). a = b“ muss gelesen werden als a und b sind definiert und gleich“.
”
”
8
2.1.3
Beispiel für eine einfache Sprache und einen Interpreter
Die zu interpretierende Sprache sei die Sprache L = Norma2 (aus ([Bir 76]). Die Norma2Programme werden so notiert, dass sie gleichzeitig auch als Haskell-Listen angesehen werden
können.
Ein Norma2-Programm ist eine Liste von Instruktionen [ins0 , ins1 , . . . , insn ], wobei insj ∈
{ IncX, DecX, IncY, DecY, (Goto 1m ), (IfX0 1m ), (IfY0 1m ) | m ≥ 0, wobei 1m =
[1, . . . 1]) (Liste mit m Einsen)}.
Semantik von L = Norma2 Es handelt sich um Programme für eine 2-Registermaschine
mit den Registern X und Y. Zahlen werden unär als Listen von Einsen codiert. (Goto 1m ) bzw.
(IfX0 1m ) und (IfY0 1m ) sind unbedingte bzw. bedingte Sprünge zur m-ten Instruktion im
Programm. Die Numerierung der Instruktionen beginnt bei 0. Eingabe ist die Zahl im Register
X, die Ausgabe die Zahl in Y. Y ist zu Anfang mit 0 = [] initialisiert.
Die Programmausführung beginnt mit der Instruktion 0, und endet, wenn das Ende der Befehlsliste erreicht wird.
Norma2-Interpreter in Haskell Der Interpreter mit Namen Execute wird über eine Hilfsfunktion Run definiert. Execute hat als Parameter das zu interpretierende Programm p und
dessen Eingabe x, mit der das Register X zu initialisieren ist. Run hat vier Parameter – das
gesamte Programm p, in welchem bei der Interpretation von Sprungbefehlen das Sprungziel
gesucht wird (Hilfsfunktion Hop), das Restprogramm r, dessen erste Instruktion die nächste zu
simulierende Instruktion ist, und die aktuellen Werte x und y der Register X und Y.
> module Norma2 where
>
>
>
>
>
type Label = String
type Value = String
data Norma2Ins = IncX | DecX | IncY | DecY | Goto Label |
IfX0 Label | IfY0 Label
deriving Show
type Norma2Prog = [Norma2Ins]
> execute:: Norma2Prog -> Value -> Value
> execute p x = run p p x []
>
>
>
>
>
>
>
>
>
>
>
run::Norma2Prog->Norma2Prog->Value->Value->Value
run _ [] _ y = y
run p (IncX:is) x y = run p is (’1’:x) y
run p (IncY:is) x y = run p is x (’1’:y)
run p (DecX:is) (’1’:x) y = run p is x y
run p (DecY:is) x (’1’:y) = run p is x y
run p (Goto l:is) x y = run p (hop p l) x y
run p (IfX0 l:is) x y = if (null x) then run p (hop p l) x
else run p is
x
run p (IfY0 l:is) x y = if (null y) then run p (hop p l) x
else run p is
x
> hop::Norma2Prog-> Label -> Norma2Prog
> hop p [] = p
> hop (_:is) (’1’:l) = hop is l
9
y
y
y
y
> n2p::Norma2Prog
> n2p = [IfX0 "11111", IncY, IncY, DecX, Goto ""]
Rechnung des Norma2-Interpreters auf einem Beispiel
p1
z
}|
{
p = [IfX0 [1,1,1,1,1,1], IncY, IncY, |DecX, (Goto
[])}]
{z
|
(Execute p [1,1])
(Run p p [1,1] [])
(Run p p_1 [1,1] [])
(Run p p_2 [1,1] [1])
(Run p p_3 [1,1] [1,1])
(Run p [(Goto []), [1] [1,1])
(Run p (Hop p []) [1] [1,1])
(Run p p [1] [1,1])
(Run p p [] [1,1,1,1])
->
->
->
->
->
->
->
->
->
...
...
...
...
...
{z
p3
p2
}
->
->
->
->
->
... ->
[1,1,1,1]
Eigenschaften und Struktur des Norma-2-Interpreters:
1. Der Interpreter ist eine natürliche formale Definition der operationalen Semantik von
Norma2.
2. Die wichtigste Funktion (Run) hat folgenden Typ
Run : Umgebung × Restprogramm → Ergebnis
|
{z
}
P arameter 1,3,4
|
{z
Kontext
}
|
{z
2. P arameter
}
Parameter 1: statischer Teil der Umgebung (bleibt unverändert), das Gesamtprogramm,
Parameter 3 und 4: dynamischer Teil der Umgebung, Werte von X und Y.
3. Der Interpreter ist syntaxorientiert und durch strukturelle Rekursion über dem Quellprogramm programmiert. Syntaxorientierte Vorgehensweise heißt: Interpretation eines Konstruktes = Kombination der Interpretation seiner Komponenten. Der Rekursivität der
Quellprogrammsyntax entspricht die Rekursivität des Interpreters.
4. Die Interpretation großer Programme beinhaltet verschiedene Gründe für Ineffizienz:
• Die Analyse der Programmstruktur zur Interpretationszeit: jedesmal wenn eine Norma2Instruktions ausgeführt werden soll, muss durch Pattern Matching festgestellt werden, um welchen Befehl es sich handelt,
z. B. run p ((Goto l): ) x y = run p (hop p l) x y
• Die Berechnung von statischen Größen, d. h. Größen, die von den Eingabedaten
unabhängig sind, wird zur Interpretationszeit wiederholt durchgeführt.
z. B. (Hop p [1,1,1,1,1]) = [] oder (Hop p []) = p
• Während der erstgenannte Punkt die asymptotische Effizienz eines Programs unberührt lässt (zumindest im Norma2-Beispiel), fügt ihr der zweite Punkt einen Faktor |p| (Programmlänge) hinzu.
10
5. Interpreter sind oft in höheren Programmiersprachen geschrieben. Ihre Effizienz ist von
der des Programms nicht zu trennen und hängt ihrerseits von der Qualität des Übersetzers
ihrer Implementierungssprache ab. (Oder gar von der des Interpreters, der diese Sprache
interpretiert, und dessen Effizienz wiederum abhängt von ... )
Übersetzer vermeiden diese Quellen der Ineffizienz. Übersetzter Code ist, pauschal gesagt, etwa 10 mal so schnell wie ein interpretiertes Programm. (Hier gibt es allerdings eine große
Schwankungsbreite.) Trotzdem haben Interpreter ihre Existenzberechtigung und werden vielfach eingesetzt.
1. Sie sind viel einfacher zu erstellen als Übersetzer. Einfacher heißt hier auch schneller und
zuverlässiger. Zudem sind sie maschinenunabhängig in dem Grade, wie es ihre Implementierungssprache ist. Aus diesem Grund sind sie unersetzbar für die Entwicklung und
Verbreitung neuer Programmiersprachen.
2. Für den Anwender sind Interpreter vorteilhaft in der Entwicklungs- und Testphase von
Programmen. Man spart die Übersetzungszeit nach jeder Fehlerbeseitigung. Zudem sind
sie meist einfacher zu bedienen und um Testhilfen zu ergänzen.
3. Der Code für eine abstrakte Zielmaschine, die dann durch einen Interpreter simuliert wird,
ist oft wesentlich kompakter als “echter” Maschinencode. Dies ist wichtig, wenn der Code
über das Netz geladen wird, wie bei Java.
2.2
2.2.1
Konstruktion von Übersetzern
Die Übersetzer-Gleichung
Def.: Gegeben seien Sprachen Q (Quellsprache) und Z (Zielsprache). Ein C-Programm heißt
Übersetzer von Q nach Z, falls
1. C-Eingabe(c) = Q-Prog
2. C-Ausgabe(c) ∈ Z-Prog
3. Für alle q ∈ Q-Prog, für alle (x1 , ..., xn ) ∈ Q-Eingabe(q) :
Qq(x1 , ..., xn ) = Z(Cc(q))(x1 , ..., xn )
Q→Z
bezeichne die Menge aller in C geschriebenen Übersetzer (Compiler) von Q nach Z.
C
Eigentlich sollte der Übersetzer für jede Eingabe aus A ein definiertes Ergebnis liefern; oben wird
in Punkt 1 aber Fehlererkennung mit undefiniertem Verhalten des Übersetzers gleichgesetzt.
Selbst wenn man versehentlich z. B. den C-Compiler auf einem Java-Programm startet, sollte
man eine brauchbare Fehlermeldung erhalten.
11
2.2.2
Beispielübersetzer: N orma2 → Haskell
Hier ist also Q = Norma2, C = Z = Haskell, d. h. wir übersetzen die maschinennahe Sprache
Norma2 in die Hochsprache Haskell. (Bei tatsächlichen Anwendungen liegt oft eine umgekehrte Situation vor, man übersetzt eine Programmiersprache (etwa Haskell) in eine imperative, maschinennahe Sprache. Dieser Fall wird uns in den späteren Kapiteln noch ausführlich
beschäftigen.)
Übersetzungsschema:
[ins_0, ..., ins_n] -->
Execute1 x
= Run x []
Run x y
= Übersetzung von [ins_0, ..., ins_n]
Run1 x y
= Übersetzung von [ins_1, ..., ins_n]
...
Run1^n x y
= Übersetzung von [ins_n]
Run1^(n+1) x y = y ( = Übersetzung von [])
Die beiden Register von Norma2 werden auf die beiden Argumente der Run-Funktionen abgebildet. Das Programm selbst verschwindet natürlich als Argument. Instruktionen GOTO [1,...,1]
werden in Aufrufe von Run1...1 übersetzt. Unser Compiler wird alle diese Run-Funktionen erzeugen, obwohl man sich leicht überlegen kann, dass nur die gebraucht werden, deren erste
Instruktion auch Ziel eines Sprungbefehls ist.
Das Haskell-Programm für den Übersetzer c ∈ Norma2 → Haskell sieht zunächst so aus:
Haskell
> module Norma2Compiler where
> compile:: Norma2Prog -> String
> compile p = unlines (header ++ "> execute1 x = run x [] where": cRest p []) where
>
header = ["Compiled by Norma2 Compiler",
>
"",
>
"Norma2 Program",
>
"--------------",
>
show p,
>
"",
>
"Target Program",
>
"--------------",
>
""]
> cRest:: Norma2Prog -> Label -> [String]
> cRest [] l = [genRun [] l]
> cRest (i:is) l = genRun (i:is) l:cRest is (’1’:l)
> genRun:: Norma2Prog -> Label->String
> genRun p l = ">
" ++ "run" ++ l ++ " x y = " ++ genBody p "x" "y"
Die bisherigen Compilerfunktionen generieren einen Kopftext, das Listing des Quellprogramms,
den festen Rahmentext des Zielprogrammes und die Namen der Run-Funktionen. Dabei wird
auf die richtige Einrückung gemäß Haskells Layout-Regel (Abseitsregel) geachtet. Die HaskellFunktion unlines konkateniert eine Liste von Strings und fügt dabei Zeilenumbrüche ein. Die
eigentliche Arbeit verrichtet die Funktion GenBody. Sie wird aufgerufen mit der zu übersetzenden Befehlsfolge, und den Strings, die die Argumente der Run-Funktionen bezeichnen. Diese
12
beiden Strings sind die Keime für Haskell-Ausdrücke, in denen sich letztlich Operationen auf
den Norma2-Registern wiederfinden.
>
>
>
>
genBody:: Norma2Prog -> String -> String -> String
genBody [] _ y = y
genBody (IncX:is) x y = genBody is ("(’1’:"++ x ++ ")") y
genBody (IncY:is) x y = genBody is x ("(’1’:"++ y ++ ")")
> genBody (DecX:is) x y = genBody is ("(tail " ++ x ++ ")") y
> genBody (DecY:is) x y = genBody is x (" tail (" ++ y ++ ")")
> genBody (Goto l:is) x y = "run" ++ l ++ " " ++ x ++ " " ++ y
> genBody (IfX0 l:is) x y
>
= " if (null " ++ x ++ ") then (run" ++ l ++ " " ++
>
x ++ " " ++ y ++ ") else (" ++ genBody is x y ++ ")"
> genBody (IfY0 l:is) x y
>
= " if (null " ++ y ++ ") then (run" ++ l ++ " " ++
>
x ++ " " ++ y ++ ") else (" ++ genBody is x y ++ ")"
Die Schwierigkeit beim Lesen (und beim Schreiben) von GenBody besteht eigentlich nur darin,
ausgeführten und erzeugten Code auseinanderzuhalten. Ein Aufruf des Compilers auf unserem
Programmbeispiel
writeFile "prog.lhs" (compile n2p)
ergibt das folgende Zielprogramm in der Datei prog.lhs:
Compiled by Norma2 Compiler
Norma2 Program
-------------[IfX0 "11111", IncY, IncY, DecX, Goto ""]
Target Program
-------------> execute1 x = run x [] where
>
run x y = if (null x) then (run11111 x y) else (run (tail x) (’1’:(’1’:y)))
>
run1 x y = run (tail x) (’1’:(’1’:y))
>
run11 x y = run (tail x) (’1’:y)
>
run111 x y = run (tail x) y
>
run1111 x y = run x y
>
run11111 x y = y
2.3
2.3.1
Partielle Auswertung und ihr Einsatz zur Konstruktion von Übersetzern
Nutzung des Interpreters zur Übersetzerkonstruktion
Nachdem wir im vorigen Abschnitt ein Beispiel für einen Übersetzer kennengelernt haben,
wollen wir nun eine Methode angeben, die es gestattet, solche Übersetzer systematisch — sei
es von Hand oder mit Unterstützung eines Rechners oder Programms zu erzeugen. Wir werden
zur Konstruktion des Übersetzers das Hilfsmittel der partiellen Auswertung anwenden.
13
Q
Problemstellung: Gegeben: i ∈ I ein Interpreter für Q in I
Gesucht: c ∈
2.3.2
Q→Z
ein Übersetzer Q → Z in C.
C
Die Partieller-Auswerter-Gleichung
Eine Lösungsidee für das eben gestellte Problem ist: Für i gilt:
Ii(q, x1 , · · · , xn ) = Qq(x1 , · · · , xn ), für alle (x1 , · · · , xn ) ∈ Q-Eingabe(q).
Man transformiere für jedes gegebene q ∈ Q-Prog i in ein Z-Programm RestProg(i, q), für das
Z(RestProg(i,q)) (x1 , · · · , xn ) = Ii(q, x1 , · · · , xn )
gilt. Bei dieser Transformation setzt man q in das Interpreter-Programm i als Konstante ein
und wertet dann i, wo es vernünftig“ ist und i nur von q, aber nicht von den Eingaben xi
”
abhängt, aus. Der Vorgang heißt partielle Auswertung von i auf q. Mit einem C-Programm
c, für das Cc(q) = RestProg(i,q) gilt, hat man dann einen Übersetzer der gewünschten Form
gefunden.
Def.: (Partieller Auswerter) Gegeben seien Sprachen Q und Z. Ein partieller Auswerter
peval ist ein Programm in E-Prog, so dass
1. E-Ausgabe(peval) ⊂ Z-Prog
2. Z(E peval (q, x1 , · · · , xj )) (xj+1 , · · · , xn ) = Qq(x1 , · · · , xn ),
∀q ∈ Q-Prog, ∀(x1 , · · · , xn ) ∈ Q-Eingabe(q), wobei 0 ≤ j ≤ n.
Ein partieller Auswerter ist also ein E-Programm, welches ein Q-Programm und ein Teil dessen
Eingaben in ein Z-Programm transformiert. Wenn wir für q (in der obigen Definition) alle oder
keine Eingaben xj für die partielle Auswertung vorgeben, nimmt peval verschiedene Rollen“
”
an :
Q→Z
E
j = n ⇒ peval wäre Interpreter, dessen Ergebnis allerdings noch gemäß Z auf der leeren
Eingabe auszuführen wäre.
j = 0 ⇒ peval wäre Übersetzer ∈
Redeweisen
• E peval (q, x1 , · · · , xj ) heißt das Restprogramm von q nach Fixierung der Eingabedaten
x1 , · · · , xj .
• x1 , · · · , xj heißen die statischen Daten von q.
• xj+1 , · · · , xn heißen die dynamischen Daten von q.
14
Pragmatik Man stelle sich vor, dass q auf sehr vielen verschiedenen Eingaben (x1 , · · · , xn )
gerechnet werden soll, wobei allerdings die ersten j Daten immer dieselben sind (z. B. j = 1, q
Interpreter, x1 Quellprogramm, x2 · · · xn Daten für x1 ).
In jeder ordentlichen“ Sprache I gibt es einen partiellen Auswerter für I nach I (also Z =
”
Q = E = I). Für die Sprachen der Turing-Programme steckt diese Aussage im Smn -Theorem
von Kleene.
Für I = Haskell wäre die Ersetzung von
p
x_1 ... x_n
f_1 x_1 ... x_n
f_2 x_1 ... x_n
...
f_n x_1 ... x_n
= f_1 x_1 ... x_n where
= body_1
= body_2
= body_n
===>
restprog p a_1 ... a_j
g
x_j’ ... x_n
f_1 x_1
...
x_n
f_2 x_1
...
x_n
...
f_n x_1
...
x_n
=
=
=
=
g where
f a_1 ... a_j x_j’ ... x_n
body_1
body_2
...
= body_n
eine mögliche, allerdings triviale, partielle Auswertung von p für die ersten Ausdrücke a 1
... a j als erste j Argumente. Hier wird ja nur formell ein Name für das Restprogramm
eingeführt, dessen Definition dann doch wieder auf den ursprünglichen und unveränderten Code
zurückgreift.
Wenn die Programmiersprache I jedoch, wie dies bei der Sprache Haskell der Fall ist, die
Eigenschaft der “Full Laziness” aufweist, tritt zur Ausführungszeit des Programmes der Effekt
der partiellen Auswertung auf. Der Rumpf der Funktion g enthält Teilausdrücke, die nicht
von den Parametern x0j ...xn abhängen. Beim ersten Aufruf von g werden diese zu ihrem Wert
reduziert. Diese Werte stehen nun anstelle der reduzierten Ausdrücke im Rumpf von g, gemäß
dem Dogma der Laziness, daß kein Ausdruck mehrfach berechnet wird. So wird bei weiteren
Aufrufen von g mit diesem partiell ausgewerteten Rumpf weitergerechnet.
Wir haben das Konzept der partiellen Auswertung so eingeführt, dass die Zielsprache Z der
partiellen Auswertung von i, und damit die Zielsprache des Übersetzers, beliebig ist. In der
Praxis ist der Fall Z = I besonders interessant, weil er einfacher zu realisieren ist. Dann kann
die partielle Auswertung
l → RestP rog (i, l)
als unvollständiges Reduzieren des Interpreters durch Anwendung der sprachdefinierenden Reduktionsregeln angesehen werden.
2.3.3
Ein Beispiel zur partiellen Auswertung
Sei nachfolgend L = I = Haskell. Betrachte das Programm
> power :: Int -> Int -> Int
> power n x = if n == 0 then 1
>
else if even n then square (power (n ‘div‘ 2) x)
>
else
x * (power (n
1) x)
> square x
= x * x
15
das xn in logarithmischer Zeit berechnet. Folgendes Programm für power 3 wäre im obigen
Sinn ein Restprogramm von power, wenn man seinen ersten Parameter n auf 3 fixiert:
> power_3 x = x * square x
Regeln zur partiellen Auswertung von Haskell Wir können power 3 aus dem Aufruf
power 3 x und der Definition von power erhalten, wenn wir folgende Programmtransformationen (Reduktionsregeln) in geeigneter Weise anwenden:
1. Haskell-Auswertungsregeln (HASKELL)
Anwendung von Gleichungen (APPL), case-Regel, where- und let-Regel, Ausrechnen vordefinierter Funktionen wie +, * etc..
2. Spezialisierung (SPEZ)
Sei f y 1 ... y k x 1 ... x n = body die Definition von f und seien c 1 ... c k Konstante. Dann wird, sofern nicht bereits vorhanden, eine Spezialisierung von f hinzugefügt:
f_c_1_...c_k x_1 ... x_n = body_c_1...c_k
wobei der neue Rumpf aus dem alten durch Einsetzen der c i für die y i hervorgeht.
Aufrufe von f werden dann wie folgt ersetzt:
f c_1 ... c_k e_1 ... e_n ===> f_c_1_...c_k e_1 ... e_n
Natürlich müssen im allgemeinen die Konstanten ci nicht gerade den Anfang der aktuellen
Parameterliste des Aufrufs von f bilden.
3. Algebraische Eigenschaften der primitiven Funktionen, die nicht als Rechenregeln vorliegen müssen:
Eigenschaft
head (a:x)
tail (a:x)
1 * a
0 * a
a - 0
=
=
=
=
=
Name
a
x
a
0
a
------
(HEAD)
(TAIL)
(EINS)
(NULL)
(NULL2)
Im Unterschied zu Haskell-Rechenregeln können diese Gleichungen in beide Richtungen
angewandt werden.
Im Gegensatz zur standardmäßigen Haskell-Auswertungsstrategie (left-most outermost graph
reduction) werden die Regeln nun beliebig angewendet, auch in Rümpfen von Funktionsdefinitionen, aber natürlich nicht innerhalb von String-Konstanten. (Diese Konstanten sind ja die
Daten des Programmes, deren Semantik nicht mit der von Haskell-Programmen übereinstimmen muss.) Wenn wir nun Reduktionsregeln (HASKELL) oder variablenlöschende algebraische
Reduktionen (NULL) überall und in beliebiger Reihenfolge anwenden dürfen, so sind die transformierten Programme im allgemeinen nur schwach äquivalent mit dem Original. D. h. es könnte
sein, dass das Original nicht terminiert, während die transformierte Variante sehr wohl terminiert. Diese Verbesserung“ im Terminierungsverhalten wollen wir aber hinnehmen. Zugleich
”
wird damit klar, daß es im allgemeinen nicht einfach ist, die Terminierung der partiellen Auswertung selbst zu garantieren.
16
Beispiel für eine Ableitung in diesem Regelsystem
g x = power 3 x
power n x = if n == 0 then
else if even n
else
===> (SPEZ)
g x = power_3 x
power_3 x = if 3 == 0 then
else if even 3
else
power n x = ...
===> (HASKELL)
g x = power_3 x
power_3 x = x *
power n x = ...
(power 2
1
then square (power (n div 2) x)
x * (power (n - 1) x)
1
then square (power (3 div 2) x)
x * (power (3 - 1) x)
x)
===> (APPL-Definition von power)
g x = power_3 x
power_3 x = x * (if 2 == 0 then 1
else if even 2 then square (power (2 div 2) x)
else
x * (power (2 - 1) x))
power n x = ...
===> (HASKELL)
g x = power_3 x
power_3 x = x *
power n x = ...
(square (power 1 x))
===> (APPL-Definition von power)
g x = power_3 x
power_3 x = x * (square (if 1 == 0 then 1
else if even 1 then square (power (1 div 2) x)
else
x * (power (1 - 1) x)))
power n x = ...
===>(HASKELL)
g x = power_3 x
power_3 x = x *
power n x = ...
(square
x *
(power 0 x)
===> (APPL-Defintion von power), (HASKELL) (EINS)
g x = power_3 x
power_3 x = x * (square x)
power n x = ...
Wenn die Spezialisierung power 3 gebildet wird, darf die Definition von power nicht gelöscht
werden, denn sie wird noch mehrfach gebraucht. Das Programm wächst also zunächst. Am Ende
allerdings kommt kein Aufruf der allgemeinen power-Funktion mehr vor, so dass sie wegfallen
kann.
Probleme bei der Wahl der Transformationsstrategie Wenn man partielle Auswertung
von Hand durchführt, erst recht aber bei der Implementierung eines partiellen Auswerters, stellt
die Strategie das eigentliche Problem dar.
17
1. Nichtdeterminismus:
Wann sind (SPEZ) oder (APPL) anzuwenden? Wann ist es günstiger, sie nicht anzuwenden?
Mögliche Nachteile von (APPL):
• Mehrfachberechnung von gemeinsamen Teilausdrücken
square x ===> x * x
Durch diese Anwendung von (APPL-square) wird der Ausdruck x verdoppelt und
daher auch zweimal ausgerechnet.
• Code wächst an, wenn Aufruf durch den Body einer Funktion ersetzt wird. Es steht
nicht fest, ob dieser Code letztlich wieder kleingerechnet werden kann.
Mögliche Nachteile von (SPEZ):
• Code wächst an. Zum Beispiel hätte wir auch die Spezialisierungen power 2 oder
power 1 erzeugen können, jedoch ohne Vorteil, und nur das Programm verlängert.
2. Nichtterminierung
• Vorsicht im Falle vordefinierter partieller (d.h. nicht immer terminierender) Funktionen
• Eine Folge von (APPL)-Anwendungen kann nicht terminieren, selbst wenn alle Parameter konstant sind
• Wenn statische Daten wachsen, könnte man unendlich viele Spezialisierungen mittels
(SPEZ) einführen
2.3.4
Konstruktion des Norma2-Übersetzers durch partielle Auswertung
Wir demonstrieren die Entwicklung des Norma2 -Compilers am Beispiel der partiellen Auswertung des Norma2-Interpreters auf dem Norma2-Programm n2p. Wir gehen aus von
n2p = [IfX0 "11111", IncY, IncY, DecX, Goto ""]
und dem Aufruf execute n2p x.
Zweimal stellt sich die Frage nach der Einführung einer Spezialisierung:
• Aufruf run q q x [] in execute:
Einführung von:
run_n2p_n2p x y
= Body_run mit p ===> n2p, q ===> n2p
• Aufruf run n2p (hop n2p "11111" ) x y in Body Run wobei
[]:
Einführung von:
run_n2p_nil x y = Body_run mit p ===> n2p, q ===> []
18
(Hop q "11111") =
Die partielle Auswertung dieser Rümpfe liefert, wenn man alle run-Aufrufe, die keinem Sprungbefehl entsprechen, auffaltet, gemäß Regel (APPL):
run_n2p_n2p x y = if x = [] then run_q_nil x y
else run n2p n2p (tail x) (’1’:’1’:y)
Fügt man eine Funktion execute2 x = execute n2p x als Startfunktion“ hinzu und faltet
”
man den Execute-Aufruf gemäß (APPL) auf, so erhält man insgesamt, wenn man nun noch die
nicht mehr aufgerufenen Funktionen execute, hop und run löscht, als mögliches Ergebnis der
partiellen Auswertung von
execute2 x =
execute p x =
run p r x y =
hop p ins
=
Execute n2p x
Run p p x []
Body_run
Body_hop
das Programm
> execute2 x = run_n2p_n2p x [] where
>
run_n2p_n2p x y = if (null x) then (run_n2p_nil x y)
>
else (run_n2p_n2p (tail x) (’1’:(’1’:y)))
>
run_n2p_nil x y = y
Dies ist genau das Zielprogramm, welches der handgeschriebene Übersetzer produziert, abgesehen von dem Header-Text und den anderslautenden Namen der run-Funktionen.
Die Ausführung des durch partielle Auswertung entstandenen Restprogramms zeigt folgende
Merkmale, verglichen mit denen des Interpreters:
1. Das Restprogramm treibt keine Analyse des Quellprogramms mehr. Die syntaktische
Korrektheit ist garantiert, wenn die partielle Auswertung nicht zu error geführt hat.
2. Das Restprogramm braucht bei Sprüngen nicht mehr die den Markendefinitionen zugehörigen Restprogramme zu berechnen. Das Ergebnis dieser semantischen Analyse ist
im Restprogramm fest eingebaut und wird repräsentiert durch die Aufrufstruktur seiner
Funktionen. hop fällt restlos weg.
3. Das Restprogramm ist unter Umständen optimiert in bezug auf das Quellprogramm.
Z. B. würde eine Anweisungsfolge IncX, DecX wie die leere Anweisung übersetzt werden (peephole-Optimierung), da bei der partiellen Auswertung der zwischenzeitlich daf ür
konstruierte Term tail (’1’:x) zu x reduziert würde. Diese Eigenschaft hat unser handgeschriebener Übersetzer nicht!
Demnach zeigt sich folgendes Aufgabenspektrum eines Übersetzers:
1. syntaktische Analyse
2. semantische Analyse (Zuordnung von Deklarationen zu Anwendungen, Typprüfung, Überprüfung von Kontextbedingungen)
3. Optimierung
bei imperativen Sprachen:
19
z. B. Entfernen von totem Code, Entfernen von Zuweisungen an tote Variablen, Aufrollen
von Schleifen, Auffinden von invariantem Code in Schleifen;
bei funktionalen Sprachen:
z. B.: call-by-name → call-by-value mit lazy evaluation, d.h. Argumente von Funktionen
nur einmal auswerten, auch wenn sie im Funktionsrumpf mehrfach auftreten (= callby-value), aber möglichst nicht auswerten, wenn sie im Rumpf gar nicht vorkommen
(ein Effekt von call-by-name),
Auffalten von Funktionsaufrufen,
Erkennen von gemeinsamen Unterausdrücken
4. Vorbereitung der Codeerzeugung (Speicherverteilung für Variablen, Registerverteilung)
5. Codeerzeugung (Codeauswahl, peephole-Optimierung)
Der partielle Auswerter als Übersetzer Hat man tatsächlich einen partiellen Auswerter
als Programm vorliegen, so erhält man einen Compiler nach dem Schema des vorhin von Hand
durchgeführten Beispiels.
Gegeben sei ein partieller Auswerter peval ∈ E-Prog für I-Programme, der Restprogramme in
I bezüglich des ersten Eingabedatums liefert, d. h.
a) E-Eingabe(peval) = I-Prog ×A
b) E peval (i, x1 ) = ix1 ∈ I-Prog, für alle i ∈ I-Prog
c) Iix1 (x2 , · · · , xn ) = Ii (x1 , · · · , xn ), für alle (x1 , · · · , xn ) ∈ I-Eingabe(i)
Forderungen b) und c) ergeben zusammen:
I (E peval (i, x1 )) (x2 , · · · , xn ) = Ii (x1 , · · · , xn )
Somit liefert peval angesetzt auf einen L-Interpreter i für jedes Quellprogramm l ∈ L ein
äquivalentes Zielprogramm il ∈ I, d. h. I (E peval (i, l))(x1 , · · · , xn ) = Iil (x1 , · · · , xn ) = Ll
(x1 , · · · , xn ).
Ein E-Programm, welches einfach den Aufruf von peval mit Argumenten i und Eingabe l
ausführt, ist ein Übersetzer ∈ L → I . Für den Spezialfall E = Haskell ist also das Programm
E
c mit der Definition c l = peval i l ein Übersetzer ∈ L → I .
Haskell
In der Literatur ist tatsächlich eine P rolog-Implementierung vorgestellt worden [KC 84], bei der
so vorgegangen wurde. Hier war L = P rolog, E = I = Lisp. Allerdings wurde festgestellt, dass
der derart konstruierte Übersetzer etwa um den Faktor 100 langsamer ist als ein vergleichbarer
handgeschriebener. Zum Teil muß diese Ineffezienz sicher darauf beruhen, daß dieser Übersetzer
bei jedem Lauf neu den Interpreter analysiert, als ob er ihn zum ersten Mal sähe. Die Abhilfe
liegt auf der Hand und wird in [JSS 85] vorgeschlagen:
Übersetzererzeugung durch Selbstanwendung partieller Auswerter Wir brauchen
einen partiellen Auswerter, der auf die partielle Auswertung des Interpreters (zusammen mit
wechselnden Zielprogrammen) spezialisiert ist. Setze E = I, d. h. schreibe den peval in der
Sprache, für die er Programme partiell auswertet. Setze peval auf sich selbst und den Interpreter
20
i an, um einen schnelleren Übersetzer zu erhalten. Dieser ist gleichzeitig ein auf das partielle
Auswerten von i spezialisierter Auswerter.
Sei c := I peval (peval, i), wobei peval ein partieller Auswerter, geschrieben in I für IProgramme (und eine Eingabe), der wieder I-Programme als Ergebnis liefert, und i ∈ L .
I
Es gilt c ∈ I-Prog. Mit der Definition z := Ic (l) ∈ I-Prog folgt:
Iz(x1 · · · xn ) =
=
=
=
I(Ic(l))(x1 · · · xn )
I(I(Ipeval(peval, i))(l))(x1 · · · xn )
I(Ipeval(i, l)))(x1 · · · xn ), (da peval partieller Auswerter)
Ii(l, x1 , · · · , xn ), (da peval partieller Auswerter)
= Ll(x1 , · · · , xn ),
(da i ∈ L )
I
Insgesamt: c ∈ L → I .
I
Hier wird, im Gegensatz zum obigen Schema, die aufwendige, allgemeine partielle Auswertung
nur bei der Erzeugung des Übersetzers c einmal zur Auswertung von peval (Selbstanwendung !)
auf der Eingabe i durchgeführt. Oben wurde der partielle Auswerter bei jeder Übersetzung eines
L-Programms aufgerufen. Der so gewonnene Übersetzer c läuft nun akzeptabel schnell.
Übung: Man begründe, warum cc := I peval (peval, peval) als ein sogenannter CompilerCompiler betrachtet werden kann. (Compiler-Compiler heißen Programme, die aus Sprachbzw. Übersetzerbeschreibungen als Eingabe einen Übersetzer als Ausgabe erzeugen. Die Sprachbeschreibung für L kann z.B. in Form eines Interpreters i ∈ L vorliegen.)
I
Einen nicht trivialen Fall von partieller Auswertung lernt man mit der Technik der Algebraischen Dynamischen Programmierung kennen (Vorlesung Algorithmen auf Sequenzen II). Dort
werden Algorithmen der dynamischen Programmierung in Form von Grammatiken aufgeschrieben, ähnlich der EBNF-Notation. Wesentlicher Vorteil der Notation ist Freiheit von Indices
( No subscripts, no errors!“). Die Symbole der Grammatik-Notation sind zugleich als Funktio”
nen höherer Ordnung definiert. Setzt man diese Definitionen ein und wertet die Grammatik
partiell aus, erhält man Matrix-Rekurrenzen, die man ohne Funktionen höherer Ordnung, also
z. B. sehr einfach in C programmieren kann.
Partielle Auswertung ist eine mächtige Technik, um effiziente Programme zu entwickeln. Andererseits hängt der Erfolg einer partiellen Auswertung sehr davon ab, wie man das urspüngliche
Programm aufgeschrieben hat. Wirklich clevere algorithmische Ideen entdeckt die partielle Auswertung nicht; insbesondere erfindet sie keine neuen Datenstrukturen.
Übung: Kann man aus einem naiven Programm find x t zur exakten Suche nach einem
Wort x in einem Text t z.B. einen endlichen Automaten oder den Knuth-Morris-Pratt-Algorithmus
entwickeln? Beide beruhen ja auf einer Vorverarbeitung des Suchwortes, also dem Übergang
von find x t zu find x t für gegebenes x.
21
2.4
Bootstrapping
In vielen Fällen ist es auch sinnvoll, Interpreter, Übersetzer und Kombinationen solcher Programme miteinander zu verbinden, etwa durch Hintereinanderausführung, oder einen Übersetzer als Eingabe eines zweiten zu verwenden.
2.4.1
Kombination von Übersetzern und Interpretern
Wir nehmen an, dass eine Menge nicht weiter spezifizierter L-Programme gegeben sei und
bezeichnen diese durch
L.
L und einen Prozessor für M haben (z. B. könnte M die
Wenn wir einen Interpreter m ∈ M
Sprache einer realen Maschine sein), so können wir jedes l ∈ L mit Hilfe von m ausführen:
L
Eingabe → L → Ausgabe
M
Q→Z
sein.
L
Folgende Darstellung bezeichnet dann die Situation, in der der Übersetzer von Q nach Z selbst
interpretiert wird.
Q→Z
L
L
M
z. B. könnte L =
Die nächste Skizze zeige an, dass in manchen Situationen die Zielprogramme interpretiert werden. (Letztlich wird auch ein Maschinenprogramm Z durch die reale Maschine M interpretiert.)
Q→Z Z
M
L
Mehrere Ebenen von Interpretation werden repräsentiert durch:
L
M
M
P
Folgende Zeichnung
Q → IL IL → Z
L
S
steht für schrittweises Übersetzen von Q nach Z über eine Zwischenform in der Sprache IL.
22
2.4.2
Bootstrapping am Beispiel P ascal
Wirth hat die Mächtigkeit und den Anwendungsbereich von Pascal (und später auch von
Modula-2) demonstriert, indem er einen Übersetzer von Pascal nach P-Code, eine Sprache
für eine abstrakte Kellermaschine, in Pascal geschrieben hat. Ergebnis ist also ein Übersetzer
h ∈ Pascal → P-Code
Pascal
Dieser allein ist natürlich noch nicht sehr nützlich. De facto wurde h in einer Teilsprache
Pascal-S ⊂ Pascal geschrieben, also
h ∈ Pascal → P-Code
Pascal-S
Wenn man voraussetzt, dass P-Code bereits übersetzt bzw. interpretiert werden kann, ist es
sinnvoll, einen Übersetzer
c ∈ Pascal → P-Code
P-Code
zu erzeugen. Es gibt zwei Möglichkeiten c zu erhalten.
Möglichkeit 1: Erzeugung von c mit Hilfe eines Interpreters
Hat man einen Interpreter m ∈ Pascal-S , so liefert dessen Anwendung auf h einen Übersetzer
M
Pascal → P-Code , den wir nochmals auf h anwenden, um c zu erhalten.
M
1. (Mm h) (h) = Pascal-S h (h), wegen m ∈ Pascal-S
M
und h ∈ Pascal-S-Prog ⊆ Pascal-Prog = Pascal-Eingabe (h).
2. P-Code (Pascal-S h (q))(x1 · · · xn ) = Pascal q (x1 · · · xn ), für alle q ∈ Pascal-Prog wegen
h ∈ Pascal → P-Code
Pascal-S
3. Pascal h(q) = Pascal-S h(q), für alle q ∈ Pascal-Prog, wegen der Teilspracheneigenschaft.
4. Für alle q ∈ Pascal-Prog gilt wegen (2) und (3) :
P-Code (Pascal h(q))(x1 · · · xn ) = Pascal q (x1 · · · xn )
Insgesamt, für alle p ∈ Pascal-Prog:
P-Code (P-Code ((Mm h) (h))(q))(x1 · · · xn ) =(1)
P-Code (P-Code (Pascal-S h(h))(q))(x1 · · · xn ) =(2)
P-Code (Pascal h (q))(x1 · · · xn ) =(4)
Pascal q (x1 · · · xn )
Somit ist c = (Mm h)(h) tatsächlich ein Element von Pascal → P-Code .
P-Code
Hierbei hätte man in der Wahl von M, der Implementierungssprache für den Pascal-S-Interpreter,
freie Hand.
23
Möglichkeit 2: Erzeugung von c unter Verwendung eines Hilfsübersetzers
Man schreibe einen Übersetzer
t0 ∈ Pascal-S → P-Code
P-Code
von Hand. t0 darf sehr primitiv sein. Es müssen nur die korrekten Pascal-S-Programme in
äquivalente P-Code-Programme übersetzt werden. Sowohl Fehlerbehandlung wie auch Codequalität brauchen keinen gehobenen Ansprüchen zu genügen. Wie wir später sehen werden,
erbt der Übersetzer c diese Eigenschaften nicht von t0 , sondern von h. t0 kann man z. B. gewinnen durch Übersetzung von h von Hand, indem man die Arbeitsweise von h simuliert. Dann
wendet man t0 auf h an.
h
Pascal
→
P-Code
Pascal-S Pascal-S
→
P-Code
P-Code
liefert
t0
t1 ∈ Pascal → P-Code
P-Code
t1 ist also bereits ein Übersetzer vom geforderten Typ. t1 verhält sich, etwa in bezug auf Fehlerbehandlung und Codeerzeugung, wie h. Er ist aber selbst ineffizient, wenn t0 schlechten P-Code
erzeugt. Abhilfe verschafft ein nächster Lauf, wobei t1 wieder auf h angewendet wird.
h
Pascal
→
P-Code
Pascal-S Pascal
→
P-Code
P-Code
liefert
t1
t2 ∈ Pascal → P-Code
P-Code
t2 übersetzt wie h, 2 ist aber nun selbst durch einen Übersetzer produziert worden, der so gut
wie h übersetzt. Die Codequalität von durch h erzeugten Zielprogrammen bestimmt nun sowohl
Effizienz, wie auch die Codequalität von t2 .
Ein weiterer Lauf bringt nicht mehr, außer dass er eventuelle Fehler in h oder t0 zu Tage bringen
könnte. Eine weitere Anwendung von t2 auf h, also P-Code t2 (h)
h
Pascal
→
P-Code
Pascal-S Pascal
→
P-Code
P-Code
t2
muss ein syntaktisch (textuell) zu t2 identisches Programm produzieren (Beweis: Übung).
2
Man beweise zur Übung, dass P-Code t1 (q) = P-Code t2 (q), für alle q ∈ Pascal-Prog.
24
Anwendungen von Bootstrapping
Das Beispiel zeigt, dass es sinnvoll sein kann, einen Übersetzer h : Q → Z in einer Teilmenge der Quellsprache Q’ ⊂ Q zu schreiben. Durch bootstrapping“ mit einem Hilfsübersetzer
”
Q’ → Z
Q’
oder einem Interpreter m ∈
kommt man in die Situation, dass Verbest0 ∈
Z
M
serungen am Q-Übersetzer, aber auch Veränderungen an der Quellsprache Q (z. B. Spracherweiterungen), auf Q’-Ebene (d. h. durch adäquates Ändern von h) erlangt werden, ohne dass
auf der normalerweise niedrigen Sprachebene Z programmiert werden muss. Dies ist besonders
hilfreich in der Entwicklungsphase einer Sprache oder eines Übersetzers.
25
Kapitel 3
Syntaxorientierte Übersetzung
3.1
Konkrete und abstrakte Syntax
Programmiersprachen bestehen aus einer relativ kleinen Anzahl von Konzepten. Solche Konzepte sind etwa Typ- oder Klassendefinitionen, Typdeklarationen, Funktionsdefinitionen (sei es
durch Gleichungen oder durch Anweisungen mit Kontrollstruktur), Audrücke, spezielle Notationen für vordefinierte Datentypen, sowie Angaben zur Programmstruktur (Module, Pakete).
Jedes dieser Konzepte hat seine Semantik. Eine Sprache ist um so einfacher zu lernen, zu verstehen und zu nutzen, je weniger Konzepte sie braucht, und je freier diese kombiniert werden
können (und dabei immer noch verständlich bleiben).
Konkrete Programme entstehen durch Kombination dieser Konzepte, und diese Kombination
ist immer rekursiv. Diesen Aufbau nennt man die abstrakte Syntax des Programms. Die lineare
Zeichenkette, die wir als Programmtext eingeben, verschleiert diesen rekursiven Aufbau eher
als dass sie ihn aufzeigt. Die Aufgabe der Syntaxanalyse ist es, diese Struktur zu erkennen.
Die Syntax von Programmiersprachen wird traditionell durch kontextfreie Grammatiken beschrieben. Nicht zu Unrecht sind kontextfreie Grammatiken deswegen eines der meistgenutzten
formalen Konzepte der Informatik. Trotzdem ist diese Tradition nicht unproblematisch, und
eine kleine Kritik sei erlaubt.
Genau betrachtet, löst die kontextfreie Grammatik zwei Aufgaben:
• Es wird eine konkrete lineare Notation angegeben, in der die rekursiv verschachtelten
Sprachkonstrukte aufzuschreiben sind, so dass die abstrakte Syntax aus der linearen
Aufschreibung (re-)konstruiert werden kann. Die diversen syntaktischen Klammern —
Symbole wie {, } oder begin, end in Anweisungen, (, ) in Ausdrücken oder auch das
traditionelle Semicolon dienen ausschließlich diesem Zweck.
• Zugleich wird implizit die abstrakte Syntax festgelegt — es wird zum Beispiel gesagt,
ob Typdeklarationen Ausdrücke enthalten dürfen, ob Prozeduren geschachtelt definiert
werden dürfen, und dass die Angaben zur Modulstruktur am Anfang des Programms
stehen müssen.
Viel einfacher wäre das Lernen von Programmiersprachen, wenn diese beiden Aspekte getrennt
beschrieben wären. Hinter den Regeln der zweiten Art stehen wesentliche Entscheidungen des
Sprachdesigns, wichtige methodische oder semantische Erwägungen, etwa: Man will verlangen,
26
dass Angaben zur Modulstruktur am Programmanfang auftreten, und nicht versteckt in der
Mitte. Man will keine Deklaration neuer Typen innerhalb von Ausdrücken zulassen, weil dies
zu unübersichtlichen Programmen führt. Man will in imperativen Sprachen keine Funktionen
in Datentypen zulassen, weil man in diesen Sprachen dafür keine vernünftige Semantik angeben
kann. Und so weiter.
Ganz anders sind die Regelungen der ersten Art - hier geht es nur um die Aufschreibung.
Was heißt da “nur” — die konkrete Syntax ist von erheblicher praktischer und strategischer
Bedeutung. Zum Beispiel: Wie will man die abstrakte Syntax zum Ausdruck bringen? Will
man wirklich syntaktische Klammern einführen, oder verwendet man dafür das Layout (Abseitsregel)? Hier scheiden sich die Geister. Dabei kommt es auf Ästhetik an oder die Nähe
zu vertrauten Notationen. So sucht Java die syntaktische Nähe zu C, um C-Programmierer
anzusprechen, auch wenn die beiden Sprachen semantisch viel verschiedener sind als sie syntaktisch aussehen. Die funktionale Sprache Miranda gilt als ein notationelles Juwel; sie orientiert
sich weitgehend an Notationen, die aus der Mathematik vertraut sind. Die Sprache Haskell,
im Grunde eine Weiterentwicklung von Miranda, durfte sich aus rechtlichen Gründen nicht zu
nahe an der Miranda-Notation anlehnen... Und so weiter.
Es wäre also generell nicht verkehrt, eine Trennung zwischen abstrakter und konkreter Syntax
einer Programmiersprache explizit zu machen. Vom Standpunkt des Übersetzerbaus her braucht
man sie ohnehin.
Die Notwendigkeit einer effizienten und eindeutigen Rekonstruktion der abstrakten Syntax aus
der konkreten Syntax führt auch dazu, dass die kontextfreie Grammatik recht umständlich
aussieht. Der aus ihr hervorgehende konkrete Syntaxbaum (d. h. der Ableitungsbaum gemäß
der Produktionen der Grammatik) ist wesentlich detaillierter als die Programmdarstellung in
abstrakter Syntax. Obwohl man den konkreten Syntaxbaum im Prinzip auch als die interne
Darstellung der abstrakten Syntax betrachten und benutzen kann, werden dadurch die Übersetzungsschemata unnötig verkompliziert.
Ein Beispiel zur konkreten und abstrakten Syntax Einfache arithmetische Ausdrücke
können repräsentiert werden durch den Datentyp Expression:
> module Ableitung where
> infixr :+:
> data Expression = Times Expression Expression |
>
Plus Expression Expression |
>
Minus Expression Expression |
>
AsExp Identifier
> type Identifier = String
Verwendet man die Haskell -übliche Notation, kann man damit einen Ausdruck wie
x * (y + z)
in der Form
Times (AsExp "x") (Plus (AsExp "y")(AsExp "z"))
schreiben. Als Baumstruktur sieht der Ausdruck so aus:
27
times
X
XXX
X
asExp
plus
HH
H
x
asExp
asExp
y
z
Natürlich will man Programme weder in Baumdarstellung, noch in der Präfix-Notation eingeben. Die übliche Schreibweise ist eine Zeichenreihe mit infix-Operatoren, und wenn nötig
Klammern. Wenn man also eine kontextfreie, eindeutige Grammatik für arithmetische Ausdrücke in üblicher Notation, d. h. mit den Regeln ∗ vor +“, mit Infixnotation für ∗ und +
”
und runden Klammern zur Klammerung haben möchte, so ist folgende, relativ komplizierte
Grammatik eine Lösung dieses Problems:
E→E+T
E→E−T
E→T
T→T∗F
T→F
F → (E)
F → Id
Id → x | y | z
Als Syntaxbaum für x ∗ (y + z) ergibt sich gemäß obiger Grammatik
E
T````
``
∗
FPP
P
P
T
F
(
E
HH
H
+
)
T
Id
E
x
T
F
F
Id
Id
z
y
der sehr viel an semantisch irrelevanter Struktur enthält, z. B. die Kettenregeln E → T , T → F
und F → Id. Er enthält sowohl Operatoren wie Nonterminals, wo Operatoren alleine ausreichend sind, und semantisch irrelevante Terminalsymbole, wie die Klammern. Solche irrelevanten
Details verschwinden in der abstrakten Syntax. Die konkrete Syntax, vorgegeben durch die hier
obige Grammatik, regelt, wie der Benutzer Programme schreiben darf (oder auch muss), und
definiert zugleich die Aufgabe der Syntaxanalyse, auf die wir in Kapitel 4 ausführlich zurückkommen.
28
Übersetzungsschemata Für den Übersetzer (und damit den Übersetzerbauer) ist die wesentliche Programmdarstellung die abstrakte Syntax. Programme sind Daten in einem rekursiv
definierten, algebraischen Datentyp. Die Syntaxanalyse erzeugt diese Programmrepräsentation,
alle weiteren Übersetzungsphasen analysieren und transformieren sie, bis am Ende die Codeausgabe das Zielprogramm entweder als ASCII-Text, oder als Binärcode ausgibt. Da das Programm
eine rekursive Datenstruktur ist, folgen alle Übersetzungsphasen im wesentlichen dem Schema
der strukturellen Rekursion; allerdings sind, je nach Schwierigkeit des Übersetzungsschritts,
ausgefeiltere Formen dieses Schemas notwendig. Von solchen Übersetzungsschemata handelt
das vorliegende Kapitel.
Übersetzer, die sich nicht rekursiv an der Programmstruktur orientieren, sind schlechterdings
nicht vorstellbar. Der Terminus “Syntaxorientierte Übersetzung” drückt daher eine Selbstverständlichkeit aus. Er hat historische Berechtigung: In ihm schwingt die Erleichterung mit, als
die Disziplin in den 60er Jahren entdeckt hatte, dass mit der formalen Beschreibung der Syntax zugleich eine brauchbare Vorgabe für die Strukturierung des gesamten Übersetzers gegeben
war. Wir behalten daher diesen historischen Titel des Kapitels bei, das man etwas moderner
mit “Übersetzungsschemata” überschreiben würde.
Drei solche Schemata werden wir kennenlernen:
• das einfache Ableitungsschema,
• das erweiterte Ableitungsschema,
• attributierte Grammatiken.
In dem meisten Büchern zum Übersetzerbau wird die Syntaxanalyse vor den Übersetzungsschemata behandelt. Dafür gibt es eigentlich nur den zufälligen Grund, dass der Übersetzungsprozess
mit der syntaktischen Analyse des Programms beginnt. Wir gehen andersherum vor, weil die
Übersetzungsschemata das Kernproblem des Übersetzerbaus darstellen.
3.2
Signaturen, Terme, Algebren
Aus der funktionalen Programmierung kennen wir den Begriff des algebraischen Datentyps. Er
ist gegeben durch eine Menge von Konstruktoren von fester Stelligkeit und gegebenen Argumenttypen. Nullstellige Konstruktoren nennt man auch Konstanten. Die Daten, die diesem Typ
angehören, sind alle Formeln oder auch Ausdrücke, die sich korrekt aus diesen Konstruktoren
bilden lassen. Daher auch der Name “Konstruktor”: Ein Konstruktor ist eine Funktion, die
auf Formeln operiert, sozusagen aus kleineren Formeln größere konstruiert. In der Theorie der
Programmiersprachen betrachtet man, nur wenig abstrakter, den Begriff der Signatur. Hier hat
man anstelle der Konstruktoren Operationssymbole, aus denen man in gleicher Weise Formeln
bilden kann.
Def.: Signaturen
Eine Signatur Σ besteht aus einer Menge von Operationssymbolen und einer Menge von
Sorten. Die Sorten sind den Operationen als Argument- und Ergebnistypen zugeordnet.
Wohltypisierte Formeln aus diesen Operatoren werden Terme genannt. Mit TΣ bezeichnen
wir die Menge aller Terme zur Signatur Σ, und mit TΣ (V ) diejenigen Terme zur Signatur
Σ, die auch Variablen aus einer Variablenmenge V enthalten können.
29
Beim Signaturbegriff lässt man also offen, für welche Funktionen die Operatoren stehen, und
was die zugrundeliegenden Wertemengen sind. Die Sorten in einer Signatur sind nur abstrakte
Platzhalter für (noch) nicht spezifizierte Wertemengen. Zusätzlich zum üblichen Signaturbegriff legen wir fest, dass in jeder Signatur eine Sorte besonders ausgezeichnet ist, die wir die
“Programmsorte” nennen. (Sie spielt die Rolle des Axioms in einer kontextfreien Grammatik.)
Def.: Σ-Algebren
Ist Σ eine Signatur, so ist eine Σ-Algebra eine Menge von Wertemengen zusammen mit
einer Menge von Funktionen, die wir als die Interpretation der Sorten und Operationen
der Signatur auffassen. Für die Interpretation eines Terms t in einer Algebra A schreiben
wir tA , sA für die Interpretation der Sorte s, und so weiter.
Natürlich kann jede Signatur Σ durch viele verschiedene Σ-Algebren interpretiert werden. Die
einfachste Interpretation ist stets der algebraische Datentyp, der gerade zu jeder Sorte einen
passenden Datentyp und zu jeder Operation der Signatur einen gleichnamigen Konstruktor
angibt. Diesen Datentyp bezeichnen wir als die Termalgebra der Signatur; er ist isomorph zu
TΣ .
Erst durch die Interpretation durch eine Σ-Algebra bekommt ein Term aus TΣ eine Bedeutung.
Dazu ein vierfaches Beispiel:
Beispiel: Signatur für arithmetische Ausdrücke
Sorten
Exp 1 , Id
Operationen
x:
y:
z:
asExp:
plus:
times:
minus:
→
→
→
Id
→
Exp Exp →
Exp Exp →
Exp Exp →
Id
Id
Id
Exp
Exp
Exp
Exp
Implementierung der Term-Algebra in Haskell Der Haskell -Datentyp Expression stellt
mit der naheliegenden Zuordnung der Sorten und Operationen (Expression für Exp, Plus für
plus, etc.) gerade die Termalgebra zu dieser Signatur dar; es fehlen nur die Interpretationen für
die Identifier x, y, z.
data Expression = Times Expression Expression |
Plus Expression Expression |
Minus Expression Expression |
AsExp Identifier
type Identifier = String
1
Unter den Sorten wird die ausgezeichnete Programmsorte in den nachfolgenden Beispielen fettgedruckt.
30
> x
> y
> z
=
=
=
"x"
"y"
"z"
Damit ist die Interpretation des Terms
times(asExp(x), plus(asExp(y), asExp(z)))
gerade der Haskell -Wert
Times (AsExp "x")(Plus (AsExp "y") (AsExp "z")).
Hier passiert also nicht besonders viel. Hier sind jedoch weitere Interpretationen zur gleichen
Signatur:
Interpretation in einer Algebra der natürlichen Zahlen Wir interpretieren die arithmetischen Ausdrücke in einer Algebra IN (Nat = Menge der natürlichen Zahlen) wie folgt:
IdIN = ExpIN = Menge Nat der natürlichen Zahlen.
plusIN :
timesIN :
asExpIN :
minusIN :
xIN :
yIN :
zIN :
(n1 , n2 ) ∈ ExpIN × ExpIN
(n1 , n2 ) ∈ ExpIN × ExpIN
n ∈ IdIN
(n1 , n2 ) ∈ ExpIN × ExpIN
3 ∈ IdIN
17 ∈ IdIN
25 ∈ IdIN
7→
7
→
7
→
7
→
n1 + n2 ∈ ExpIN
n1 ∗ n2 ∈ ExpIN
n ∈ ExpIN
n1 − n2 ∈ ExpIN
Damit ergibt sich z. B. für t = times(asExp(x), plus(asExp(y), asExp(z))) die Auswertung
tIN = xIN ∗ (yIN + zIN ) = 3 ∗ (17 + 25) = 126. Dies ist aber nicht die einzige Interpretationsmöglichkeit für arithmetische Ausdrücke.
Interpretation in einer Boole‘schen Algebra Genauso erlaubt (und auch praktisch sinnvoll) ist eine Interpretation innerhalb der Booleschen Algebra Bool:
IdB = ExpB = Bool = {T, F }
plusB :
timesB :
minusB :
asExpB :
xB :
yB :
zB :
(b1 , b2 )
(b1 , b2 )
(b1 , b2 )
b
7→
7
→
7
→
7→
=
=
=
b1 ∨ b2
b1 ∧ b2
b1 ∨ ¬ b2
b
T
F
T
Dann ergibt sich für denselben Term t die Auswertung tB = T ∧ (F ∨ T ) = T .
31
Interpretation in der Notations-Algebra Wir kommen nun auf die Frage zurück, wie
Programme als Zeichenreihen darstellbar sind: Wir interpretieren die Signatur in der StringAlgebra; jeder Sorte der Signatur wird durch Strings interpretiert, und jeder Term durch seine
Darstellung als Zeichenreihe.
IdString = ExpString = String
> t
= times (asExp x) (plus (asExp y) (asExp z)) where
>
>
>
>
plus
times
minus
asExp
>
>
>
x
y
z
a b = "(" ++ a ++ " + " ++ b ++ ")"
a b =
a ++ " * " ++ b
a b = "(" ++ a ++ " - " ++ b ++ ")"
a
=
a
=
=
=
"x"
"y"
"z"
Dann ergibt sich für denselben Term t die Auswertung
"x * (y + z)"
Diese Notation ist aber noch nicht so flexibel wie man sie sich in Programmiersprachen wünscht,
da manchmal unnötige Klammern gesetzt werden.
Interpretation in einer Algebra der Kellermaschinenprogramme Im nächsten Abschnitt werden wir als weiteres Beispiel eine Interpretation in einer Algebra von Programmen
für eine einfache Kellermaschine kennenlernen.
Zusammenfassung Wir können jetzt zusammenfassend die Begriffe Sprache, Syntax und
Semantik aus Kapitel 2 präzisieren. Eine Sprache L : A → (A∗ → A) wird repräsentiert
durch ein Tripel (Σ, S, I), Σ Signatur, S und I Σ-Algebren. Dabei ist S die Notation der
Programmiersprache, d.h. eine Interpretation von Σ in einer Algebra der Zeichenreihen. Es ist
dann L-Prog = {lS |l ∈ TΣ }, d.h. die Programme sind genau die Notationen der Σ-Terme der
Programmsorte. Die Semantik L der Sprache ist gegeben als L(l) = lI , für jedes Programm
l ∈ TΣ , also als das Ergebnis der Auswertung von l in I. Damit sind natürlich nur solche I
zugelassen, die der Programmsorte P eine Menge von Funktionen des Typs A∗ → A zuordnen2 .
3.3
Das einfache Ableitungsschema
Da wir jetzt Programme einer Sprache L als Terme in TΣL gemäß einer zugeordneten Signatur
ΣL sehen, betrachten wir nun Übersetzer c als Abbildungen
c : TΣQ → TΣZ
2
Letztlich können wir jedes Objekt v, wenn nötig, als eine solche Funktion interpretieren. Dazu müssen
wir aus v nur eine Funktion machen, die die leere Zeichenreihe aus A∗ auf v abbildet und die für jede nichtleere Zeichenreihe aus A+ undefiniert ist. So werden Programme beschrieben, die keine Eingabe benötigen.
Mit dieser Deutung wird dann auch aus unserem Beispiel der arithmetischen Ausdrücke die Definition einer
Programmiersprache.
32
wenn ΣQ bzw. ΣZ die Signaturen von Quell- bzw. Zielsprache sind. So wie wir die Programme (Terme) induktiv definiert haben, so wollen wir nun auch die Übersetzungsfunktion durch
Induktion definieren. Dies wird im wesentlichen bedeuten, daß für jeden Q-Operator f ein Übersetzungsschema spezifiziert wird. Eine mögliche Form der induktiven Übersetzerdefinition wird
als Übersetzung durch Ableitung bezeichnet. Der Übersetzer heißt dann Signaturmorphismus
oder auch derivor.
3.3.1
Der Ableitungsbegriff
Def.: Eine Ableitung
c : TΣQ → TΣZ
konstruiert eine ΣQ -Algebra über TΣZ . Jeder ΣQ -Sorte s wird eine ΣZ -Sorte sc zugeordnet, jedem ΣQ -Operationssymbol f eine Funktion fc . Die FUnktion fc wird dabei durch
einen Term aus TΣZ (V ) beschrieben.
“Einfach” sind solche Ableitungen, weil sie eine rein syntaktische Transformation sind. Jeder
ΣQ -Term wird durch ersetzen seiner Konstruktoren gemäß c durch einen ΣZ -Term ersetzt.
Alle bisherigen Interpretationen sind Ableitungen. Betrachten wir zum Beispiel die Interpretation von Exp in der Notations-Algebra. Die Zielsignatur (hier die Zeichenreihen) enthält
unter anderem die Operation ++. Diese Operation ist selbst nicht Interpretation einer Operation aus ΣQ , wird aber benutzt, um die interpretierenden Funktionen zu definieren. Die
Interpretation lautet ja gerade timesN = ftimes mit ftimes (a, b) = a ++ ” ∗ ” ++ b. Also gilt
z. B. c(times(a, b)) = ftimes (c(a), c(b)).
Natürlich ist der Interpretationsbegriff allgemeiner als der Ableitungsbegriff, da die Funktion
ftimes im allgemeinen auf beliebige Art definiert sein kann. Ableitungen lassen sich besonders
einfach funktional implementieren:
> c :: Expression -> String
>
>
>
>
>
>
c
c
c
c
(Times a b) =
c a ++ " * " ++ c b
(Plus a b) = "(" ++ c a ++ " + " ++ c b ++ ")"
(Minus a b) = "(" ++ c a ++ " - " ++ c b ++ ")"
(AsExp i)
= c i where
c:: Identifier -> String
c i
= i
3.3.2
Übersetzung arithmetischer Ausdrücke in Kellermaschinencode
Wir betrachten eine einfache Kellermaschine mit drei Registern und 0-Adress-Befehlen als Ziel
der Übersetzung.
> data Code = ADD | NEG | MUL |
>
PUSH Reg | Code :+: Code
> data Reg = R1 | R2 | R3
deriving Show
deriving Show
Der Befehl PUSH lädt einen Registerinhalt auf den Keller. Die Befehle ADD und MUL addieren
bzw. multiplizieren auf dem Keller, NEG invertiert das Vorzeichen des obersten Kellerelementes.
Weitere Instruktionen brauchen wir für unser einfaches Beispiel nicht. Der Konstruktor :+:
hängt Programmabschnitte aneinander. Hier ist die Übersetzung e2c:
33
>
>
>
>
>
>
>
>
>
e2c
e2c
e2c
e2c
e2c
:: Expression -> Code
(Times a b) = e2c a :+:
(Plus a b) = e2c a :+:
(Minus a b) = e2c a :+:
(AsExp a)
= PUSH (e2c
e2c:: Identifier -> Reg
e2c "x"
= R1
e2c "y"
= R2
e2c "z"
= R3
e2c b :+: MUL
e2c b :+: ADD
e2c b :+: NEG :+: ADD
a) where
Diese Übersetzerdefinition liefert z. B. für
> t1 = Times (AsExp "x") (Minus (AsExp "y") (AsExp "z"))
===>
e2c(t1) = PUSH R1 :+: (PUSH R2 :+: PUSH R3 :+: NEG :+: ADD) :+: MUL
wobei die runden Klammern in c(t) bedeutungslos sind, aber noch die Struktur des Quellprogramms wiederspiegeln.
Blickt man zurück auf die Übersetzergleichung aus Kapitel 2, so kann man diese nun wesentlich
detaillierter formulieren. Wir haben nun eine konkrete Repräsentation für Ziel- und Quellprogramme als Terme der jeweiligen Signatur, und eine konkrete Vorstellung von dem Übersetzer,
nämlich als Ableitung.
Def.: Wie bisher seien ΣQ , ΣZ und c gegeben. IQ und IZ seien Σ-Algebren für ΣQ und ΣZ . c ist
ein korrekter Übersetzer von der Quellsprache (ΣQ , IQ ) in die Zielsprache (ΣZ , IZ ), falls
für alle t ∈ TΣQ von der Programmsorte tIQ ⊂ (c(t))IZ gilt, d. h. falls für alle a1 · · · an ∈ A∗
a = tIQ (a1 , ..., an ) ⇒ a = (c(t))IZ (a1 , ..., an ).
D.h. für alle Q-Programme ( = Terme t von Q-Programmsorte) ist die Interpretation
(c(t))IZ der Übersetzung c(t) eine E/A-Funktion, die gleich der E/A-Funktion des Quellprogrammes ist oder aber mehr spezifiziert ist als diese, d. h. auch an manchen Stellen,
an denen die E/A-Funktion des Quellprogrammes undefiniert ist, definierte Ergebnisse
liefert. Diese Situation tritt immer dann ein, wenn der Übersetzer (semantische) Fehler
im Programm erkennt und sie meldet oder korrigiert.
Übung: Man definiere für die Zielsprache (Stackmaschinencode) des Beispiels über arithmetische Ausdrücke eine Semantik, die die Intuition trifft und beweise die Korrektheit des Übersetzers.
Interessant ist noch die Beobachtung, daß die Übersetzung durch Ableitung unter Komposition
abgeschlossen ist, d.h. für Ableitungen c1 : ΣQ1 → ΣZ1 = ΣQ2 und c2 : ΣQ2 → ΣZ2 ist c3 = c2 ◦c1
eine Ableitung von ΣQ1 nach ΣZ2 (man wende einfach die Regeln von c2 auf die Definition von
c1 an). Damit kann man Übersetzungen modular (eventuell über mehrere Zwischensprachen)
spezifizieren.
Wir haben gesehen, daß Ableitungen c als Übersetzer gesehen werden können und haben wiederholt, was die Korrektheit von c relativ zur vorgegebenen Interpretation IQ bzw. IZ von
ΣQ und ΣZ bedeutet. Der Ableitung ist durch folgenden Tatbestand charakterisiert: Sind ΣQ ,
34
ΣZ und c gegeben, kann man zu jeder Interpretation IZ von ΣZ eine Interpretation IQ für
ΣQ ableiten (konstruieren), so daß c ein korrekter Übersetzer von (ΣQ , IQ ) in (ΣZ , IZ ) ist. Die
Konstruktion von ΣZ , IZ und c für ΣQ liefert eine denotationale Semantik für ΣQ .
In der Praxis liegt die Aufgabe allerdings umgekehrt vor: Wir haben die Interpretationen IQ
und IZ vorgegeben, d. h. wir kennen die Semantik der Quell- und der Zielsprache. Gesucht
wird ein Übersetzer, der die obige Gleichung erfüllt. Dies kann man auch so ausdrücken, dass
die gegebene Interpretation und die aus dem Übersetzer abgeleitete Interpretation äquivalent
sein müssen.
3.4
Das erweiterte Ableitungsschema
3.4.1
Schranken des einfachen Ableitungsschemas
Ableitungen sind Übersetzungsschemata, die völlig kontextunabhängig arbeiten. Die Probleme,
die sich hier in der Praxis ergeben, sind folgende:
1. Viele Konstrukte in Programmiersprachen erfordern ein kontextabhängiges Übersetzen.
Immer dann, wenn Programme Deklarationen enthalten, die die Art von Objekten festlegen, müssen angewandte Auftreten von Objektbezeichnern in Abhängigkeit von der
Objektart spezifisch übersetzt werden. So kann z. B. in Pascal ein Bezeichner x einen
Typ, eine Konstante, eine Variable, eine Prozedur oder eine Funktion bezeichnen, wobei
dann in den meisten Fällen noch genauere Informationen über das Objekt gebraucht werden, um die Übersetzung definieren zu können. Technisch bedient man sich dabei einer
Symboltabelle, die diese Informationen enthält. Bei der Definition einer Ableitung können
wir jedoch keine zusätzlichen Argumente, wie Symboltabellen, benutzen.
Ein Übersetzer c, der den Kontext berücksichtigt, ist vom Typ
c : Quellprogramm × Symboltabelle → Zielprogramm.
Ableitungen c sind dagegen immer vom Typ
c : Quellprogramm → Zielprogramm.
2. Bisher können wir nur in einfacher Weise Operatoren der Quellsprache durch Terme in
der Zielsprache ersetzen. Ein Übersetzer nimmt jedoch nicht nur solche syntaktischen
Umformungen vor, er muss oft auch semantische Werte wirklich ausrechnen. Dies betrifft
Aufgaben, wie die Überprüfung von Kontextbedingungen oder die Vorbereitung der Codeerzeugung, z. B. Adresszuordnung oder Berechnung von Optimierungsinformationen.
In realistischen Beispielen müssen wir daher davon ausgehen, dass die durch Ableitung erzeugten Zielterme nicht die endgültigen Zielprogramme darstellen, sondern erst durch eine nachfolgende Auswertung (auf der leeren Eingabe) in die tatsächlichen Zielprogramme übergehen.
Mit anderen Worten, die Terme c(t) sind zu interpretieren als Programme in einer Sprache C
von Übersetzungszeitaktionen. Die Ausführung von c(t) gemäß C (zur Übersetzungszeit; auf der
leeren Eingabe) liefert erst das eigentliche Zielprogramm.
Solche Zweistufigkeit erhöht Modularität: Die Sprache ΣC der Compilezeitaktionen kann man
für eine ganze Sprachfamilie entwerfen!
35
Ein Beispielübersetzer nach dem Ableitungsschema in Haskell
3.4.2
Erweitertes Ableitungsschema:
Ableitung
−→
TΣQ
c
7−→
t
|{z}
Quellsprache
3.4.3
TΣC
c(t)
|{z}
Ausf ührung
−→
TΣZ
C
7−→
C(c(t))()
|
Sprache der
Compilezeit−
aktionen
{z
}
Zielsprache
Quell- und Zielsprache
Quellsprache – abstrakte Syntax: Als Quellsprache nehmen wir eine Sprache von whileProgrammen mit Variablendeklarationen. Folgender algebraischer Datentyp definiert ihre abstrakte Syntax:
> module Whileprog_abl where
> infixr :*:
> infixr :+:
> infixr :-:
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
data Prog
data Stat
data
data
data
type
type
=
=
Cprog Stat
deriving
Stat :*: Stat |
Cdecl Ident Typ |
Cassign Ident Expr |
Cif Expr Stat Stat |
Cwhile Expr Stat |
Cskip
deriving
Typ
= Cbool | Cint
deriving
Expr = Cneg Expr |
Cnot Expr |
Cbinop Expr Binop Expr |
Cvar Ident |
Cconst IntConst |
Ctrue |
Cfalse
deriving
Binop = Cplus | Cminus | Ctimes | Cdiv | Cless | Cleq |
Cgt
| Cgteq | Ceq
| Cneq | Cor
| Cand deriving
Ident
= String
IntConst = String
Show
Show
(Show, Eq)
Show
(Show, Eq)
binoplist = [("+", Cplus), ("-", Cminus),("*", Ctimes), ("/", Cdiv),
("<", Cless),("<=", Cleq), (">", Cgt), (">=", Cgteq),
("=", Ceq), ("~=", Cneq), ("$or", Cor), ("$and", Cand)]
Ein Beispielprogramm wäre
> p = Cprog ((Cdecl "x" Cint) :*:
>
(Cdecl "y" Cint) :*:
>
(Cwhile (Cbinop (Cvar "x") Cgt (Cconst "0"))
>
((Cassign "x" (Cbinop (Cvar "x") Cminus (Cconst "1"))) :*:
>
(Cassign "y" (Cbinop (Cvar "y") Cplus (Cconst "2"))))))
36
Zielsprache – abstrakte Syntax:
Als Zielsprache unseres Übersetzers wollen wir die Assemblersprache einer idealisierten Kellermaschine annehmen. Ihre Syntax sei wie folgt definiert:
> data Instr = Instr :+: Instr
>
Store Int
>
Load Int
>
LoadConst Int
>
LoadBConst Bool
>
Uop Oper
>
Bop Oper
>
Define Int
>
Goto Int
>
JumpF Int
>
Nop
>
> type Oper = String
|
|
|
|
|
|
|
|
|
|
deriving Show
Die Kellermaschine hat zwei Speicher: einen Kellerspeicher und einen Variablen-Speicher. Letzterer wird durch positive ganze Zahlen adressiert. Zur Semantik dieser Assemblersprache: Folgen von Instruktionen (Konstruktor Sq) werden von links nach rechts ausgeführt, sofern kein
Sprungbefehl vorliegt. Sprungbefehle Goto(l) bzw. JumpF(l) springen zu derjenigen Instruktion, die durch einen voranstehenden (Pseudo-)Befehl Define(l) markiert ist. Marken sind
durch Zahlen bezeichnet. Bei JumpF wird nur gesprungen, falls das oberste Kellerelement den
Wert False enthält. Binäre und unäre Operationen wie >, −, + erwarten ihre Operanden
oben auf dem Keller und ersetzen sie durch das Operationsergebnis. Die Ladebefehle laden ihre
Operanden in den Keller. Load(a) lädt den Inhalt der Speicherzelle mit Adresse a. Umgekehrt
löscht Store(a) das oberste Element im Keller und speichert dessen Inhalt nach a.
Ein Beispielprogramm, dieses wird sich gerade als Übersetzung unseres Quellprogrammes p
ergeben, wäre:
(Define 1)
:+:
(Load 0)
:+:
(LoadConst 0) :+:
(Bop ">")
:+:
(JumpF 0)
:+:
(Load 0)
:+:
(LoadConst 1) :+:
(Bop "-")
:+:
(Store 0)
:+:
(Load 2)
:+:
(LoadConst 2) :+:
(Bop "+")
:+:
(Store 2)
:+:
(Goto 1)
:+:
(Define 0)
3.4.4
Compilezeitaktionen und ihre Datentypen
Die Ausführung einer elementaren Compilezeitaktion a bewirkt eine Transformation des Compilerzustandes, d. h. der Datenstrukturen des Compilers. Daher sind solche Aktionen Funktionen
37
vom Typ
a : Compilerzustand → Compilerzustand
oder
a : P arameter → Compilerzustand → Compilerzustand.
Im zweiten Fall dient der zusätzliche Parameter einer Präzisierung der Aktion. Ein Compilerzustand ist eine Belegung der Compilervariablen mit Werten. In diesem Beispiel benutzt der
Compiler 6 Variablen (im folgenden Haskell -Programm treten diese Variablen als Parameter
der Aktionen auf):
-
Symboltabelle
Labelstack
Typestack
Adressenzähler
Markennamenzähler
Code file
st
ls
ts
az
lz
code
Bevor wir die Aktionen (oder Zustandstransformationen) programmieren, werden wir zunächst
die Datenstrukturen dieser Zustandsvariablen (einschließlich der primitiven Operationen) genauer spezifizieren. Wir werden insbesondere den Polymorphismus in Haskell dahingehend nutzen, um die Stacks Instanzen eines polymorphen Stack-Typs zu machen. Der Typ Symboltabelle
wird unabhängig von der Natur der Identifikatoren sein. In Bezug auf Fehlerbehandlung werden
wir keine Anstrengungen unternehmen, sondern mit Fehlerabbruch die Übersetzung beenden.
Dazu nutzen wir die Ausnahmebehandlung (exception handling) von Haskell. Mit error s wird
die Auswertung eines Ausdruckes abgebrochen und der String s (vom Typ [char]) ausgegeben.
1. Keller für Marken und Typangaben
>
>
>
>
>
>
>
>
>
data Stack a
= Empty | Push (Stack a) a
pop Empty
pop (Push as a)
= error "Compiler stack underflow (pop)"
= as
top Empty i
top (Push as a) n
| n == 1
| otherwise
= error "Compiler stack underflow (top)"
= a
= top as (n-1)
2. Die Symboltabelle enthält Einträge der Form (Identifier,Typ,Adresse)
> type Symtab a b c
>
> lookupid [] i
> lookupid ((x,t,n):st)
>
| x == i
>
| otherwise
= [(a, b, c)]
= error "undeclared identifier (lookupid)"
i
= (t,n)
= lookupid st i
38
3. Funktionen zur Typprüfung
> resultType2 b Cint Cint
>
| member [Cplus,Cminus,Ctimes,Cdiv] b
>
| member [Cless,Cleq,Cgt,Cgteq,Ceq,Cneq] b
> resultType2 b Cbool Cbool
>
| member [Cor,Cand] b
> resultType2 b x y
= Cint
= Cbool
= Cbool
= error "incompatible types"
> member l x = elem x l
3.4.5
Der Compiler
Signatur ΣC der Compilezeit-Aktionen
> type Digits =
>
> data Action =
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
String
PushType Typ |
PushVarType Ident |
TestFor Typ |
TestForEqualTypes |
TcNeg |
TcNot |
TcBinop Binop |
NewLabel |
DisposeLabel |
EnterDecl Ident Typ |
-- Aktionen zur Typpruefung
-- Aktionen zur Markenverwaltung
-- Aufbau Symboltabelle
GenStore Ident |
GenLoad Ident |
GenLoadConst Int |
GenLoadBConst Bool |
GenUop Oper |
GenBop Oper |
GenDefine Int |
GenGoto Int |
GenJumpF Int |
GenNop |
-- Codeerzeugende Aktionen,
-- eine pro Zielmaschineninstruktion
Action :-: Action
-- kombinierte Aktionen
deriving Show
Übersetzung durch Ableitung
> compileBinop b = head [s| (s,b’)<- binoplist, b’ == b]
>
> compileExp (Cconst n) = (PushType Cint) :-: (GenLoadConst (numval n)) where
>
numval x = foldl q 0 (map digitToInt x) where
>
q x y = 10 * x + y
>
>
>
>
>
compileExp
compileExp
compileExp
compileExp
(Ctrue)
(Cfalse)
(Cvar i)
(Cnot e)
=
=
=
=
(PushType Cbool)
(PushType Cbool)
(PushVarType i)
(compileExp e)
:-:
:-:
:-:
:-:
:-:
(GenLoadBConst True)
(GenLoadBConst False)
(GenLoad i)
TcNot
(GenUop "~")
39
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
compileExp (Cneg e) =
(compileExp e)
compileExp (Cbinop e1 b e2) =
(compileExp e1)
(TcBinop b)
compileStat
compileStat
compileStat
compileStat
(Cskip)
(s1 :*: s2)
(Cdecl i t)
(Cassign i e)
compileStat (Cwhile e s)
compileStat (Cif e s1 s2)
compileProg (Cprog s)
:-: TcNeg
:-: (GenUop "-")
:-: (compileExp e2) :-:
:-: (GenBop (compileBinop b))
=
=
=
=
GenNop
(compileStat s1) :-: (compileStat s2)
EnterDecl i t
(PushVarType i) :-: (compileExp e) :-:
TestForEqualTypes :-: (GenStore i)
= NewLabel :-: NewLabel :-:
(GenDefine 1) :-: (compileExp e) :-:
(TestFor Cbool) :-: (GenJumpF 2) :-:
(compileStat s) :-: (GenGoto 1) :-:
(GenDefine 2) :-:
DisposeLabel :-: DisposeLabel
= NewLabel :-: NewLabel :-:
(compileExp e)
:-:
(TestFor Cbool) :-: (GenJumpF 1) :-:
(compileStat s1) :-: (GenGoto 2)
:-:
(GenDefine 1) :-: (compileStat s2) :-:
(GenDefine 2) :-: DisposeLabel :-:
DisposeLabel
= (compileStat s)
Interpretation der Compilezeit-Aktionen Jede Aktion wird durch eine gleichnamige
Funktion (kleingeschrieben) interpretiert. Diese Funktionen transformieren den Compilerzustand. Der :-:-Konstruktor wird durch den andThen-Kombinator realisiert, der die Komposition
solcher Zustandstransformationen beschreibt.
Der Gesamtcompiler leitet erst eine Termdarstellung der Compilezeitaktionen ab, führt sie dann
aus, indem er ihre Interpretation auf den Startzustand anwendet und aus den resultierenden
Compilerzustand den Zielcode extrahiert.
>
>
>
>
>
>
>
>
>
>
>
>
>
type CompState = (Symtab Ident Typ Int, LabelStack, TypeStack,
AdressCounter, LabelCounter, Instr)
type
type
type
type
LabelStack
TypeStack
AdressCounter
LabelCounter
=
=
=
=
Stack Int
Stack Typ
Int
Int
compile p = code
where actions = compileProg p
(st,ts,ls,az,lz,code) = ip actions startState
startState = ([],Empty,Empty,0,0,Nop)
0. Die Interpretationsfunktion
> ip :: Action ->
>
CompState ->
CompState
40
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
ip
ip
ip
ip
ip
ip
ip
ip
ip
ip
(PushType t)
(PushVarType i)
(TestFor t)
TestForEqualTypes
TcNeg
TcNot
(TcBinop b)
NewLabel
DisposeLabel
(EnterDecl i t)
=
=
=
=
=
=
=
=
=
=
pushType t
pushVarType i
testFor t
testForEqualTypes
tcNeg
tcNot
tcBinop b
newLabel
disposeLabel
enterDecl (i,t)
ip
ip
ip
ip
ip
ip
ip
ip
ip
ip
(GenStore i)
(GenLoad i)
(GenLoadConst n)
(GenLoadBConst b)
(GenUop o)
(GenBop b)
(GenDefine n)
(GenGoto n)
(GenJumpF n)
GenNop
=
=
=
=
=
=
=
=
=
=
genStore i
genLoad i
genLoadConst n
genLoadBConst b
genUop o
genBop b
genDefine n
genGoto n
genJumpF n
genNop
ip (a1 :-: a2)
= (ip a1) ‘andThen‘ (ip a2)
1. Aktionen zur Typprüfung
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
pushType ty (st,ls,ts,az,lz,code)
= (st,ls,Push ts ty, az,lz,code)
pushVarType x (st,ls,ts,az,lz,code)
= (st,ls,Push ts (fst(lookupid st x)),az,lz,code)
testFor ty (st,ls,(Push ts t),az,lz,code)
| ty == t
= (st,ls,ts,az,lz,code)
| otherwise
= error "Typfehler"
tcNeg (st,ls,(Push ts t),az,lz,code)
| t == Cint
= (st,ls,(Push ts t),az,lz,code)
| otherwise
= error "Typfehler"
tcNot (st,ls,(Push ts t),az,lz,code)
| t == Cbool
= (st,ls,(Push ts t),az,lz,code)
| otherwise
= error "Typfehler"
tcBinop b (st,ls,(Push (Push ts t1) t2),az,lz,code)
= (st,ls,(Push ts(resultType2 b t1 t2)),az,lz,code)
testForEqualTypes (st,ls,(Push (Push ts t1) t2),az,lz,code)
| t1 == t2
= (st,ls,ts,az,lz,code)
2. Aktionen zur Markengenerierung
> newLabel (st,ls,ts,az,lz,code) = (st,(Push ls lz),ts,az,lz+1,code)
> disposeLabel (st,(Push ls l),ts,az,lz,code) = (st,ls,ts,az,lz,code)
41
3. Aktionen zur Verwaltung der Symboltabelle
> enterDecl (x,t) (st,ls,ts,az,lz,code)
>
= ((x,t,az):st,ls,ts,az+2,lz,code)
4. Aktionen zur Codeerzeugung
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
genLoadConst n (st,ls,ts,az,lz,code)
= (st,ls,ts,az,lz,code :+: LoadConst n)
genLoadBConst b (st,ls,ts,az,lz,code)
= (st,ls,ts,az,lz,code :+: LoadBConst b)
genLoad x (st,ls,ts,az,lz,code)
= (st,ls,ts,az,lz,code :+: Load a)
where (t,a) = lookupid st x
genNop (st,ls,ts,az,lz,code)
= (st,ls,ts,az,lz,code :+:
Nop)
genDefine l (st,ls,ts,az,lz,code)
= (st,ls,ts,az,lz,code :+:
genGoto l (st,ls,ts,az,lz,code)
= (st,ls,ts,az,lz,code :+:
Define (top ls l))
Goto (top ls l))
genJumpF l (st,ls,ts,az,lz,code)
= (st,ls,ts,az,lz,code :+: JumpF (top ls l))
genStore x (st,ls,ts,az,lz,code)
= (st,ls,ts,az,lz,code :+: Store a)
where (t,a) = lookupid st x
genUop op (st,ls,ts,az,lz,code)
= (st,ls,ts,az,lz,code :+: Uop op)
genBop b (st,ls,ts,az,lz,code)
= (st,ls,ts,az,lz,code :+: Bop b)
5. Kombination der Aktionen
> andThen f g = g . f
42
Wir wollen nun die Übersetzung des Beispielprogrammes p von S. 36 ausführlich beschreiben:
compileProgram(p)
als ΣC -Term3
EnterDecl("x",Cint)
:+: (EnterDecl("y",Cint)
:+: NewLabel
:+: NewLabel
:+: (GenDefine 1)
:+: (PushVarType "x")
:+: (GenLoad "x")
:+: (PushType Cint)
:+: (GenLoadConst 0)
:+: (TcBinop Cgt)
:+: (GenBop "> ")
:+: (TestFor Cbool)
:+: (GenJumpF 2)
:+: (PushVarType "x")
:+: (PushVarType "x")
:+: (GenLoad "x")
:+: (PushType Cint)
:+: (GenLoadConst 1)
:+: (TcBinop Cminus)
:+: (GenBop "-")
:+: TestForEqualTypes
:+: (GenStore "x")
:+: (PushVarType "y")
:+: (PushVarType "y")
:+: (GenLoad "y")
:+: (PushType Cint)
:+: (GenLoadConst 2)
:+: (TcBinop Cplus)
:+: (GenBop "+")
:+: TestForEqualTypes
:+: (GenStore "y")
:+: (GenGoto 1)
:+: (GenDefine 2)
:+: DisposeLabel
:+: DisposeLabel
Transformation des Compilerzustandes
bei Ausführung der Aktionen4
st=ls=ts=[], az=lz=0
st=(("x",Cint,0)), az=2
st=(("y",Cint,2),("x",Cint,0)), az=4
ls=[0], lz=1
ls=[1,0], lz=2
→ (Define 1)
ts=[Cint]
→ (Load 0)
ts=[Cint,Cint]
→ (LoadConst 0)
ts=[Cbool]
→ (Bop ">")
ts=[]
→ (JumpF 0)
ts=[Cint]
ts=[Cint,Cint]
→ (Load 0)
ts=[Cint,Cint,Cint]
→ (LoadConst 1)
ts=[Cint,Cint]
→ (Bop "-")
ts=[]
→ (Store 0)
ts=[Cint]
ts=[Cint,Cint]
→ (Load 2)
ts=[Cint,Cint,Cint]
→ (LoadConst 2)
ts=[Cint,Cint]
→ (Bop "+")
ts=[]
→ (Store 2)
→ (Goto 1)
→ (Define 0)
ls=[0]
ls=[]
5
Die Notation: [i1 ,..., ik ] steht für Stack-Inhalte push(i1,...,push(ik,empty)...).
→ (instruktion“ steht für code:=code :+: instruktion.
”
43
Kapitel 4
Syntaxanalyse
Im Kapitel über syntaxorientierte Übersetzung haben wir angemerkt, dass das Erkennen der
syntaktischen Struktur eines Programms aus seiner linearen Notation eine nichttriviale Aufgabe
ist. Andererseits sind die dabei zu lösenden Probleme recht gut bekannt, so dass es zahlreiche
Werkzeuge gibt, die die Erstellung von Syntaxanalysatoren unterstützen. (LEX und YACC sind
die bekanntesten.) Typischerweise werden die lexikalischen Symbole durch reguläre Ausdrücke,
die Sprache selbst durch eine kontextfreie Grammatik beschrieben. Die besagten Werkzeuge generieren aus diesen Beschreibungen Analysatoren, die auf dem Modell der endlichen Automaten
bzw. der Kellerautomaten basieren.
Die Konstruktion solcher Werkzeuge gehört in eine Spezialvorlesung über Compiler-erzeugende
Systeme, ihre Benutzung dagegen lässt sich im Bedarfsfall leicht erlernen, wenn man ein grundlegendes Verständnis der Aufgabe der Syntaxanalyse mitbringt. Dieses Grundverständnis soll
das vorliegende Kapitel vermitteln. Zugleich werden wir mit dem Combinator-Parsing eine
Programmiertechnik zur Syntaxanalyse kennenlernen, die es uns erlaubt, von einer Grammatik
unmittelbar zu einem ausführbaren Syntaxanalysator zu gelangen. Diese Technik eignet sich
auch in Situationen, wie sie außerhalb des Compilerbaus auftreten, etwa im Zusammenhang
mit mehrdeutigen Grammatiken natürlicher Sprachen.
4.1
Problemstellung: Umkehrung der Term-Notation
Notation der WHILE-Programme Wir betrachten wieder die Signatur WHILE der whileProgramme aus Kapitel 3, und geben zunächst ihre Notation an, das heißt ihre Interpretation
in einer Algebra der Zeichenreihen. Wir benutzen dazu das einfache Ableitungsschema, wie in
Abschnitt 3.3 bereits an der Signatur EXP vorgeführt.
Die Ableitung wird in Haskell durch die Funktionen noteProg, noteExp, noteStat und
noteBinop beschrieben.
> noteProg (Cprog s)
>
>
>
>
>
>
noteStat
noteStat
noteStat
noteStat
noteStat
noteStat
(Cskip)
(s1 :*: s2)
(Cdecl i t)
(Cassign i e)
(Cwhile e s)
(Cif e s1 s2)
= noteStat s
=
=
=
=
=
=
" Skip "
noteStat s1 ++ "; " ++
" var " ++ i ++ ":" ++
i ++ ":=" ++ noteExp e
" while " ++ noteExp e
" if " ++ noteExp e ++
44
noteStat s2
noteTyp t
++ " do " ++ noteStat s
" then " ++ noteStat s1
>
>
>
>
>
>
>
>
>
>
>
++ " else " ++ noteStat s2
noteBinop b = head [s| (s,b’)<- binoplist, b’ == b]
noteExp
noteExp
noteExp
noteExp
noteExp
noteExp
noteExp
(Cconst n) = n
(Ctrue) =
"True"
(Cfalse) =
"False"
(Cvar i) =
i
(Cnot e) =
"~"
(Cneg e) =
"-"
(Cbinop e1 b e2) = noteExp e1 ++ noteBinop b ++ noteExp e2
> noteTyp Cint = "int"
> noteTyp Cbool = "bool"
Sieht man sich die obige Notation näher an, erkennt man das Fehlen der Möglichkeit, Ausdrücke zu klammern. Dies kommt daher, dass wir von der abstrakten Syntax ausgehen, die
solche Klammerung nicht nötig hat, und daher dafür auch keinen Konstruktor vorsieht. Darauf
kommen wir später zurück.
Für unser Beispielprogramm p erhält man in dieser Notation:
> p = Cprog ((Cdecl "x" Cint) :*:
>
(Cdecl "y" Cint) :*:
>
(Cwhile (Cbinop (Cvar "x") Cgt (Cconst "0"))
>
((Cassign "x" (Cbinop (Cvar "x") Cminus (Cconst "1"))) :*:
>
(Cassign "y" (Cbinop (Cvar "y") Cplus (Cconst "2"))))))
noteProg p ==> " var x:int; var y:int; while x>0 do x:=x-1; y:=y+2 end ."
Strategien und Teilaufgaben der Syntaxanalyse Syntaxanalyse ist die Aufgabe der Umkehrung der Notationsableitung: Gegeben einen String s, suchen wir einen Programm-Term p,
so dass noteProg(p) = s. Im Prinzip kann dies auf zwei Arten geschehen:
1. bottom-up: Ausgehend von s wird versucht, die Gleichungen von noteProg, noteStat
etc. in Rückwärtsrichtung anzuwenden.
2. top-down: Ausgehend von einer Variablen p versuchen wir, in der Gleichung noteProg(p) =
s sukzessive p so zu instantiieren, dass es sich mittels der Gleichungen von noteProg,
noteStat etc. immer weiter reduzieren läss t, bis die Normalform s entsteht.
Beide Vorgehensweisen sind zunächst hochgradig nicht-deterministisch, da an der gleichen Stelle
verschiedene Regelanwendungen möglich sind und einige in (u.U. unendliche) Sackgassen führen
können. Praktisch brauchbare Verfahren für eindeutige Sprachen rekonstruieren die gesuchte
Ableitung deterministisch und verarbeiten dabei s streng sequentiell von links nach rechts.
Wo in der Notationsableitung der Operator ++ auftritt, stehen in einem Programmtext stattdessen sogenannte Trennzeichen (Leerzeichen, Zeilenvorschub, Tabulator), die auch ganz wegfallen
können, solange dadurch keine aufeinander folgenden Symbole verschmelzen. Das Erkennen dieser Trennzeichen, und das Zusammensetzen der lexikalischen Symbole trennt man üblicherweise
als Lexikalische Analyse (heute oft auch Lexing genannt) von der eigentlichen Syntaxanalyse
(Parsing) ab, deren Aufgabe es ist, aus einer Sequenz von lexikalischen Symbolen (Token) den
abstrakten Syntaxbaum zu konstruieren.
45
4.2
Lexikalische Analyse mit endlichen Automaten
Die lexikalischen Grundsymbole sind in der Regel sehr einfach aufgebaut und lassen sich durch
reguläre Ausdrücke beschreiben. Daraus wird ein endlicher Automat konstruiert, der genau
diese Symbole (oder Symbolzeichen) akzeptiert und als sogenannte Token an die Syntaxanalyse
weiterreicht.
Def.: Endlicher Automat
Ein deterministischer endlicher Automat (DFA) ist ein 5-Tupel (A, S, s0 , F, δ), wobei gilt:
A
S
s0 ∈ S
F ⊆S
δ :S×A→S
ist
ist
ist
ist
ist
ein Alphabet
eine endliche Zustandsmenge
der Startzustand
die Menge der akzeptierenden Zustände
die Übergangsfunktion
δ beschreibt den Zustandsübergang für ein Eingabezeichen. δ wird fortgesetzt auf Zeichenreihen:
δ ∗ (s, ε) = s,
δ ∗ (s, aw) = δ ∗ (δ(s, a), w) für a ∈ A, w ∈ A∗
Der Automat startet im Zustand s0 , verarbeitet die ganze Eingabe t zeichenweise und
akzeptiert t, falls der letzte erreichte Zustand ein akzeptierender Zustand ist:
Ein DFA (A, S, s0 , F, δ) akzeptiert t, falls
akzept(t) = True mit akzept(t) = (δ ∗ (s0 , t) ∈ F )
δ wird oft als Tabelle oder als Übergangsdiagramm angegeben, woraus sich A und S
implizit ergeben. Startzustand sei im folgenden Diagramm 0, akzeptierende Zustände
sind durch einen doppelten Kreis gekennzeichnet.
Beispiel: Übergangsdiagramm eines endlichen Automaten für die Sprache (a∗ b) ∪ (b∗ a)
a
?
* 3
a
b
?
a,b b
- 4
1
>
Za,b
a Z
?
ZZ
~
0
7
>
a,b
Zb
Z
ZZ
~
- 6
2 a
Hb
HH
a
6
HH j
H
b 5
46
Durch die akzeptierenden Zustände unterscheidet der Automat, ob eine Zeichenreihe der Bauart
(a∗ b) (Zustände 2,4) oder (b∗ a) (Zustände 1,6) verarbeitet wurde.
Endliche Automaten lassen sich in imperativen oder funktionalen Sprachen sehr einfach und
effizient implementieren.
Beispiel: Der obige endliche Automat als Haskell-Programm
> akzept xs = akzState(deltaStar 0 xs)
>
where akzState = member[1,2,4,6]
>
deltaStar = foldl delta
>
delta 0 ’a’ = 1
>
delta 0 ’b’ = 2
>
delta 1 ’a’ = 3
>
delta 1 ’b’ = 4
>
delta 2 ’a’ = 6
>
delta 2 ’b’ = 5
>
delta 3 ’a’ = 3
>
delta 3 ’b’ = 4
>
delta 4 x = 7
>
delta 5 ’a’ = 6
>
delta 5 ’b’ = 5
>
delta 6 x = 7
>
delta 7 x = 7
Der endliche Automat stellt das abstrakte Modell des Erkennens lexikalischer Symbole dar.
Daneben muss ein Programm zur lexikalischen Analyse (englisch: Scanner) noch weitere Funktionen erfüllen:
• Entfernen der bedeutungslosen Trennzeichen und der Kommentare,
• Unterscheiden von reservierten Wörtern und sonstigen Bezeichnern (die ja syntaktisch in
die gleiche Symbolklasse fallen),
• Bereitstellung des konkreten Bezeichners oder einer Zahl zur weiteren Verarbeitung
Wir wollen nun einen Scanner für die lexikalische Analyse unserer WHILE-Programme entwickeln. Die Funktion scanner soll eine Liste von Eingabezeichen in eine Liste von lexikalischen Symbolen (englisch: token) umsetzen. Die Symbole stellen wir selbst als Zeichenreihen
dar. Wir fügen dabei reservierten Wörtern einen Präfix hinzu, um sie von sonstigen Bezeichnern
zu unterscheiden. Es soll also z.B. gelten:
scanner "begin var x : int;" ==> ["$begin", "$var", "x", ":", "$int", ";"]
Der folgende endliche Automat beschreibt die lexikalischen Symbole unserer Beispielsprache. An
den Kanten stehen (disjunkte) Bedingungen, die angeben, welcher Übergang unter den aktuellen
Eingabezeichen stattfindet. Dabei fass t singleOp die Übergänge unter den Operatoren
’+’, ’-’, ’*’, ’/’, ’=’, ’;’
zusammen. Alle Zustände außer start sind akzeptierende Zustände, wobei wir festlegen, dass
jeweils das längstmögliche Symbol erkannt wird. Während also die Zustände singleOp und
assign sofort akzeptieren, muss im Zustand idOrResWord, number und colon das folgende
Eingabezeichen noch mitbetrachtet werden.
47
?
idOrResWord
letter ?
number
digit
’’
singleOp
*
singleOp
’\n’
?
’:’’=’
start
colon
H
@HH
J
J@ HH
’<’
’=’
j
H
J@
less
J@
J @’>’
’=’
R
greater
J @
J
J’∼’
’=’
^
J
not
letter,digit
digit
- assign
- less equal
greater
equal
- not equal
Im folgenden Programm spaltet die Funktion ftoken die Eingabe in ein Paar (erstes Symbol,
Resteingabe) auf. Die Funktion scanner wandelt mittels ftoken die Gesamteingabe um in eine
lexikalische Symbolfolge.
>
>
>
>
>
>
type Token = String
scanner :: String -> [Token]
scanner [] = []
scanner (c : cs) = case ftoken(c : cs) of
("",[]) -> []
(t, ts) -> t:scanner ts
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
ftoken :: String -> (Token, String)
ftoken []
= ("",[])
-- token "" bedeutet end-of-file
ftoken (’ ’ : cs)
= ftoken cs
ftoken (’\n’ : cs)
= ftoken cs
ftoken (’<’ : ’=’ : cs) = ("<=", cs)
ftoken (’>’ : ’=’ : cs) = (">=", cs)
ftoken (’~’ : ’=’ : cs) = ("~=", cs)
ftoken (’:’:’=’ : cs)
= (":=", cs)
ftoken (’:’: cs)
= (":", cs)
ftoken (’;’: cs)
= (";", cs)
ftoken (c : cs)
| singleop c
= ([c],cs)
| isAlpha c && reswords(c:i) = (’$’:c:i,ys)
| isAlpha c
= (c:i,ys)
| isDigit c
= (c:n,zs)
where (i,ys) = splitwhile isAlphaNum cs
(n,zs) = splitwhile isDigit cs
ftoken x
= error ("Lexical error: Illegal Character " ++ take 1 x)
singleop
singleop
reswords
reswords
:: Char -> Bool
= member "+ - * / < = > ~"
:: String -> Bool
= member["and", "begin", "bool", "do", "end", "else", "false", "if",
"int", "or", "skip", "then", "true", "var", "while"]
> splitwhile p []
= ([], [])
48
> splitwhile p (c : cs)
> | p c
= (c : ys, zs)
> | otherwise
= ([], c : cs)
> where (ys, zs)
= splitwhile p cs
>
> p1 = "begin while x > 0 do x:=x-1; y:=y+1 end"
> member xs x = elem x xs
Wegen der Lesbarkeit haben wir für die lexikalischen Symbole eine Codierung als Zeichenreihen
benutzt. Effizienter ist es u.U., die Symbole durch einen numerischen Code und ihre textuelle
Darstellung als Index in eine vom Scanner angelegte Hashtabelle zu ersetzen.
Außerdem wird man in einem realistischen Scanner im Datentyp token auch die Position (Zeile,
Spalte) des erkannten Symbols repräsentieren. Dies braucht man für exakte Positionsangaben
in etwaigen Fehlermeldungen des Compilers oder auch zur Anwendung der Abseits-Regel.
Die Benutzung der Haskell-internen error-Funktion ist eine eigentlich unzulässige Vereinfachung. Schließlich ist es ja kein Fehler des Scanners, wenn die Eingabe ein illegales Zeichen
enthält. Für eine korrekte Fehlerbehandlung sollte ein Fehlertoken erzeugt werden, wieder mit
Positionsangaben, aus dem der Parser dann die Fehlermeldung generiert.
4.3
4.3.1
Syntax-Analyse durch Combinator-Parsing
Von der Notation zur Grammatik
Von der Notationsableitung kann man direkt zu einer kontextfreien Grammatik in der üblichen Schreibweise übergehen. Die Sorten der abstrakten Syntax tauchen darin als Nichtterminalsymbole auf; die Terminalsymbole sind die in der Notationsableitung konkret eingesetzten
Zeichenketten. Unüblich ist es in der Theorie der formalen Sprachen, den Produktionen Namen
zu geben. Wir benennen die Produktionen mit dem zuständigen Konstruktor der abstrakten
Syntax.
Cprog:
Cseq:
Cdecl:
Cint:
Cbool:
Cassign:
Cif:
Cwhile:
Cskip:
Cneg:
Cnot:
Cbinop:
Cvar:
Cconst:
Prog -> "begin" Stat "end"
Stat -> Stat ";" Stat
Stat -> "var" Id ":" Type
Type -> "int"
Type -> "bool"
Stat -> Id ":=" Exp
Stat -> "if" Exp "then" Stat "else" Stat
Stat -> "while" Exp "do" Stat
Stat -> "skip"
Exp -> "-" Exp
Exp -> "~" Exp
Exp -> Exp Binop Exp
Exp -> Id
Exp -> Int
Die so erhaltene Grammatik ist zur Syntaxanalyse meist nicht direkt geeignet, da sie mehrdeutig
ist. Zur Analyse wird eine konkrete Syntax benötigt, die in unserem Fall zwei Randbedingungen
erfüllen muss:
• die syntaktische Struktur soll eindeutig sein, also z.B. festlegen, ob
1 + 2 ∗ 3 als (1 + 2) ∗ 3 oder 1 + (2 ∗ 3) zu analysieren ist.
49
• die Grammatik soll nicht linksrekursiv sein, d.h. Ableitungen der Art X →+ X · · · sind
verboten, weil wir einen top-down-Parser entwickeln werden.
Wir werden in zwei Schritten vorgehen. Zunächst zeigen wir, wie man von einer Grammatik zu
einem Parser kommt. Anschließend betrachten wir Transformationen der Grammatik bzw. der
Sprache, um Probleme der Linksrekursion und der Mehrdeutigkeit zu lösen.
4.3.2
Parsen mit Combinatoren
Der polymorphe Parser-Typ Allgemein ist ein Parser eine Funktion vom Typ
> type Parser a b = [a] -> [(b, [a])]
Darin steht a für den Typ der Token, die die Parser-Eingabe darstellen, und b für den Ergebnistyp der Analyse (z.B. verschiedene Sorten der abstrakten Syntax).
Parser sind spezifiziert durch die folgende Eigenschaft:
p :: Parser a b ist ein korrekter Parser, der Programme q ::[a] in konkreter Syntax in ihre
abstrakte Syntax t :: b überführt falls gilt: member (p q) (t,r) ⇐⇒ (noteB t) ++ r =
q für beliebiges r::[a]. Dabei ist (noteB t) die Notation von t wie oben beschrieben.
Ein Parser liefert also neben dem Strukturbaum des analysierten Eingabeteils noch die Resteingabe als Ergebnis. Dabei ist es möglich, dass ein Parser unterschiedlich lange Präfixe der
Eingabe erkennt. Dies lassen wir zu und sehen daher eine Liste von Ergebnissen vor. Diese Liste
wird leer sein, wenn kein Präfix der Eingabe erkannt werden kann. Für jede WHILE-Sorte X
werden wir einen Parser pX angeben.
Parser für Terminalsymbole Zum Erkennen von bestimmten Terminalsymbolen verwenden wir spezielle Parser:
> symbol
:: Eq a => a -> Parser a a
> symbol s []
= []
> symbol s (t:ts) = [(t,ts) | s == t]
Damit erkennt der Parser symbol ":=" genau das Symbol ":=" am Beginn seiner Eingabe.
Reservierte Wörter erkennt der Parser rword, der die Vorarbeit unseres Scanners berücksichtigt.
> rword
> rword s
:: Token -> Parser Token Token
= symbol (’$’:s)
Es folgen Parser für Namen, Zahlen und Operatoren:
>
>
>
>
>
>
>
>
>
>
>
pId :: Parser Token Ident
pId [] = []
pId (t : ts) = [(t,ts) | isAlpha (head t)]
pInt :: Parser Token String
pInt [] = []
pInt (t : ts) = [(t,ts) | isDigit (head t)]
pBinop :: Parser Token Token
pBinop [] = []
pBinop (t : ts) = [(t,ts) | member (map fst binoplist) t]
50
Parser-Kombinatoren Parser werden sequentiell und alternativ verknüpft durch die Kombinatoren ~~~ (“then”) und ||| (“oder”): Der |||-Kombinator verbindet alternative Parser
für das gleiche Nichtterminal.
> (|||)
:: Parser a b -> Parser a b -> Parser a b
> (p1 ||| p2) inp = (p1 inp) ++ (p2 inp)
Der ~~~-Kombinator schaltet Parser hintereinander, wie z.B. in:
pStat = rword "var" ~~~ pId ~~~ symbol ":" ~~~ pType
Zugleich müssen wir dafür sorgen, dass als Ergebnis der Syntaxanalyse der abstrakte Syntaxbaum aufgebaut wird. Es soll also im obigen Beispiel (abgesehen von der Resteingabe) der
Parser
pId
das Ergebnis "x"
pType das Ergebnis Cint
pStat das Ergebnis Cdecl "x" Cint
liefern. Dazu gibt es den <<<-Kombinator, der es erlaubt eine Funktion anzugeben, die mit den
Parserergebnissen gefüttert wird:
>
>
>
>
(<<<)
(f <<< p) inp
(><<)
(f ><< p) inp
::
=
::
=
(b -> c) -> Parser a b -> Parser a c
[(f t, out) | (t, out) <- p inp]
c -> Parser a b -> Parser a c
[(f, out)| (_,out) <- p inp]
Dabei ist ><< eine Sonderform von <<< für den Fall, dass die Funktion f eine Konstante ist, die
das Parserergebnis zwar voraussetzt, aber nicht in die Ausgabe einbaut. Wir können nun das
Zusammenspiel von <<< und ~~~ definieren: Der Kombinator ~~~ setzt voraus, dass die Ergebnisse des linken Parsers Funktionen sind, die auf die Ergebnisse des rechten Parsers angewandt
werden können.
> (~~~)
:: Parser a (b -> c) -> Parser a b -> Parser a c
> (p1 ~~~ p2) inp = [((t1 t2), out) | (t1,r1) <- p1 inp, (t2, out) <- p2 r1]
> (-~~)
:: Parser a b -> Parser a c -> Parser a c
> (p1 -~~ p2) inp = [(t2, out) | (t1, r1) <- p1 inp, (t2, out)<- p2 r1]
> (~~-)
:: Parser a b -> Parser a c -> Parser a b
> (p1 ~~- p2) inp = [(t1, out) | (t1, r1) <- p1 inp, (t2, out)<- p2 r1]
Für die abstrakte Syntax sind die erkannten Wortsymbole irrelevant. Die Varianten des ~~~Kombinators, sind so definiert, dass sie das linke bzw. rechte Teilergebnis vergessen.
Alle Infix-Operatoren sind als links-assoziierend definiert. Damit sind die beiden folgenden
Zeilen äquivalent:
pStat =
Cdecl <<< (rword "var" -~~ pId) ~~- symbol ":"
~~~ pType
pStat = ((Cdecl <<< (rword "var" -~~ pId)) ~~- (symbol ":")) ~~~ pType
Auch kann man in den obigen Beispielen den -~~-Kombinator wie folgt vermeiden (und damit
die extra Klammern):
pStat =
Cdecl ><< rword "var" ~~~ pId
~~-
51
symbol ":"
~~~ pType
Dazu muss man zeigen, dass allgemein die Gleichung gilt:
f <<< (p -~~ q)
=
f ><< p ~~~ q
Damit liefert (in Schritten) zum Beispiel
(rword "var" -~~ pId) ["$var", "x", ":", "$int", usw.] ==>
[ ("x",[":", "$int", usw.])]
Cdecl <<< (rword "var" -~~ pId) ["$var", "x", ":", "$int", usw.] ==>
[ (Cdecl "x",[":", "$int", usw.])]
((Cdecl <<< (rword "var" -~~ pId)) ~~- symbol ":") ["$var", "x", ":", "$int", usw.] ==>
[ (Cdecl "x",["$int", usw.])]
(((Cdecl <<< (rword "var" -~~ pId)) ~~- symbol ":") ~~~ pType) ["$var", "x", ":", "$int", usw.] ==>
[ (Cdecl "x" Cint,[usw.])]
Beachte dass in den Zwischenschritten das Teilergebnis (Cdecl "x") eine Funktion ist, genau
wie dies von ~~~ vorausgesetzt wird.
Der erste Kombinator-Parser für WHILE-Programme Der Parser ist eine direkte
Übertragung der Grammatik in die Kombinator-Schreibweise:
> compile inp
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
= [p | (p,[]) <- (pProg . scanner) inp]
where
pProg:: Parser Token Prog
pProg = Cprog ><< rword "begin" ~~~ pStat ~~- rword "end"
pStat :: Parser
pStat =
Cdecl
><<
Cassign <<<
Cif
><<
Cwhile
Cskip
(:*:)
Token Stat
rword "var" ~~~ pId
pId ~~- symbol ":="
rword "if" ~~~ pExp
~~~~~
~~-
symbol ":"
pExp
rword "then"
rword "else"
><< rword "while" ~~~ pExp ~~- rword "do"
><< rword "skip"
<<< pStat ~~- symbol ";" ~~~ pStat
~~~ pType
|||
|||
~~~ pStat ~~~~~ pStat
|||
~~~ pStat
|||
|||
pExp :: Parser Token Expr
pExp = Cconst <<< pInt
|||
Ctrue ><< rword "true"
|||
Cfalse ><< rword "false"
|||
Cnot
><< symbol "~" ~~~ pExp
|||
Cneg
><< symbol "-" ~~~ pExp
|||
Cvar
<<< pId
|||
Cbinop <<< pExp ~~~ pBinop ~~~ pExp
pType :: Parser Token Typ
pType = Cint ><< rword "int" |||
Cbool ><< rword "bool"
Die Funktion Cbinop wählt den zum erkannten Operator gehörenden Konstruktor aus:
> Cbinop :: Expr -> String -> Expr -> Expr
> Cbinop e opS e’ = Cbinop e op e’
>
where op = head[opC | (opS’, opC) <- binoplist, opS == opS’]
52
Behandlung von Linksrekursion und Mehrdeutigkeit Der nun entstandene Parser pProg
zeigt, warum sich die aus den Notationsklauseln entstandene kontextfreie Grammatik noch nicht
ganz als Beschreibung der konkreten Syntax, und auch nicht zur Syntaxanalyse eignet:
• Die Grammatik enthält viele Mehrdeutigkeiten. Syntaktisch verschiedene Programmterme wie
Cplus x (Cmul y z)) und (Cmul (Cplus x y) z)
haben die gleiche Notation, nämlich x + y * z. Ähnliches gilt für den Programmtext
while x > 0 do x := x + 1; x := x - 1
worin die zweite Zuweisung zum Schleifenrumpf gehören kann oder auch nicht. Wir erhalten zum Beispiel:
pStat (scanner "while x > 0 do skip;skip") ==>
[(Cwhile (Cbinop (Cvar "x") Cgt (Cconst "0")) Cskip, [";", "$skip"]),
(Cwhile (Cbinop (Cvar "x") Cgt (Cconst "0")) (Cskip :*: Cskip), [])
ERROR: Control stack overflow
Hier sieht man zunächst die Mehrdeutigkeit — es gibt zwei abstrakte Syntaxstrukturen
für die while-Anweisung, die auch beide gefunden werden. Auf den darauf folgenden stack
overflow kommen wir gleich zurück.
Bei der Analyse natürlicher Sprachen muss man mit Mehrdeutigkeiten leben, da sie erlaubt sind und erst semantisch entschieden wird, welches die richtige Struktur ist. Gerade
in einem solchen Fall ist unsere Parser-Implementierung geeignet, weil sie eine Liste aller
möglichen Strukturen berechnet.
Bei der Implementierung von Programmiersprachen dagegen verlangt man eine eindeutige
Syntax. Um dies zu erreichen, werden wir die Notationsklauseln ändern.
• Die Grammatik ist linksrekursiv, und weil unser top-down-Parser direkt von der Grammatik abgeschrieben ist, gerät er durch Produktionen wie
Exp ->
Stat ->
Exp Binop Exp
Stat ";" Stat
in endlose Rekursion. Daher der obige control stack overflow. Hier müssen wir die
Grammatik umformen, ohne die beschriebene Sprache zu ändern.
Die abstrakte Syntax bleibt im folgenden unverändert. Die Grammatik wird in folgender Hinsicht modifiziert:
• Für Folgen von Anweisungen wird das neue Nichtterminal StatList eingeführt. Die Produktion für den seq-Konstruktor wird rechtsrekursiv geschrieben.
• Die Mehrdeutigkeit bezüglich des Schleifenrumpfs wird durch ein zusätzliches end beseitigt. Ebenso verfahren wir mit dem if-then-else.
• Exp wird aufgespalten in Nichtterminalsymbole Exp, ArithExp, Term, Factor, um Präzedenzen der Operatoren auszudrücken.
53
Die modifizierte Grammatik sieht nun so aus:
Cprog:
Cseq:
Prog
-> "begin" StatList "end"
StatList -> Stat ";" StatList
StatList -> Stat
Cdecl:
Stat -> "var" Id ":" Type
Cassign: Stat -> Id ":=" Exp
Cif:
Stat -> "if" Exp "then" StatList "else" StatList "end"
Cwhile: Stat -> "while" Exp "do" StatList "end"
Cskip:
Stat -> "skip"
Cint:
Type -> "int"
Cbool:
Type -> "bool"
Verfeinerungen von Cbinop: Exp -> Exp Binop Exp:
Cbinop:
Cbinop:
Cbinop:
Ctrue:
Cfalse:
Cneg:
Cnot:
Cvar:
Cconst:
Exp
-> ArithExp Relop ArithExp | ArithExp
ArithExp -> Term
Addop Term
| Term
Term
->
Factor
Mulop Factor
| Factor
Addop -> "+" | "-" | "or"
Mulop -> "*" | "/" | "and"
Relop -> "<" | ">" | "=" | "<=" | ">=" | "~="
Factor -> "true"
Factor -> "false"
Factor -> "-" Factor
Factor -> "~" Factor
Factor -> Id
Factor -> Int
Factor -> "(" Exp ")"
In dieser Grammatik haben wir allerdings die ursprüngliche Sprache eingeschränkt. Ausdrücke
wie z.B. a + b + c sind nun ausgeschlossen. Dieses Problem wird im nächsten Abschnitt aufgegriffen.
Der Kombinator-Parser für die modifizierte Grammatik In der Implementierung werden die verschiedenen Differenzierungen der bisherigen Nonterminals weiterhin durch die gleiche
Sorte der abstrakten Syntax realisiert.
Neue Parser kommen hinzu für StatList, ArithExp, . . . , die anderen werden leicht modifiziert.
> compiler inp
>
>
>
>
>
>
>
>
>
>
>
>
>
>
pProg
pProg
= [p | (p,[]) <- (pProg . scanner) inp]
where
:: Parser Token Prog
= Cprog ><< rword "begin" ~~~ pStatList ~~- rword "end"
pStatList :: Parser Token Stat
pStatList = (:*:) <<< pStat ~~- symbol ";" ~~~ pStatList |||
pStat
pStat
pStat
Cdecl
Cassign
Cif
Cwhile
:: Parser Token Stat
=
><< rword "var" ~~~ pId
<<< pId ~~- symbol ":="
><< rword "if" ~~~ pExp
~~- symbol ":" ~~~ pType
|||
~~~ pExp
|||
~~- rword "then" ~~~ pStatList ~~rword "else" ~~~ pStatList ~~- rword "end" |||
><< rword "while" ~~~ pExp
~~- rword "do"
~~~
54
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
pStatList ~~- rword "end"
Cskip
pExp
pExp
:: Parser Token Expr
= Cbinop <<< pArithExp ~~~ pRelop ~~~ pArithExp |||
pArithExp
pArithExp :: Parser Token Expr
pArithExp = Cbinop <<< pTerm ~~~ pAddop ~~~ pTerm
pTerm
pTerm
pTerm
:: Parser Token Expr
= Cbinop <<< pFactor ~~~ pMulop ~~~ pFactor
pFactor
pFactor
pFactor
:: Parser Token Expr
= Ctrue ><< rword "true"
Cfalse ><< rword "false"
Cnot
><< symbol "~" ~~~ pFactor
Cneg
><< symbol "-" ~~~ pFactor
Cvar
<<< pId
Cconst <<< pInt
symbol "(" -~~ pExp ~~- symbol ")"
||| symbol "$or"
pMulop :: Parser Token String
pMulop = symbol "*" ||| symbol "/"
||| symbol "$and"
pRelop :: Parser Token String
pRelop = symbol "<" ||| symbol ">" ||| symbol "="
symbol "<=" ||| symbol ">=" ||| symbol "~="
:: Parser Token Typ
= Cint ><< rword "int"
Cbool ><< rword "bool"
|||
|||
|||
|||
|||
|||
|||
|||
pAddop :: Parser Token String
pAddop = symbol "+" ||| symbol "-"
pType
pType
|||
><< rword "skip"
|||
|||
> p11 = "begin while x > 0 do x:=x-1 end; y:=y+1 end"
Damit erhalten wir nun wie erwartet
compiler p11 ==>
[Cprog ((Cwhile (Cbinop (Cvar "x") Cgt (Cconst "0")) (Cassign "x" (Cbinop (Cvar "x") Cminus (Cconst "1")))
und compiler p1 => [], da das Programm p1 wegen des fehlenden end nach dem Schleifenrumpf nun syntaktisch falsch ist.
Eigenschaften von Kombinator-Parsern Kombinator-Parser sind schnell erstellt und sehr
fehlersicher. Ihre Laufzeit ist jedoch nur akzeptabel, wenn wenig Mehrdeutigkeiten und Sackgassen auftreten. Andernfalls wird sie exponentiell, da Teile der Eingabe mehrfach analysiert
werden. Eine Technik für effiziente Kombinator-Parser findet man in [?].
55
4.3.3
Erweiterungen: Zusatzargumente und Kontextbedingungen
Wenn der Parser semantische Informationen mitberechnen soll oder wenn die Struktur der
konkreten Syntax zu sehr von der abstrakten abweicht, ist es nötig, Parser mit zusätzlichen
Argumenten auszustatten. Sollen während der Syntaxanalyse bereits (einfache) Kontextbedingungen überprüft werden, können Parser mit Filtern kombiniert werden. Dazu führen wir
weitere Kombinatoren follow und suchthat ein.
Beispiel: Eine Grammatik für Ausdrücke Als Beispiel betrachten wir noch einmal die
Analyse von Ausdrücken, in etwas modifizierter Form.
• Wir heben die Einschränkungen in der Struktur von Ausdrücken auf, die im vorigen
Abschnitt eingeführt wurden.
• (Positive, ganze) Zahlen werden in der abstrakten Syntax durch ihre Werte dargestellt
(Typ num), kleine“ und große“ Zahlen werden durch die Konstruktoren Sint bzw. Lint
”
”
unterschieden. Der Parser muss also die Konvertierung und Unterscheidung vornehmen.
• Identifier sind zu Buchstaben vereinfacht. Zahlen beschreiben wir durch die Grammatik
mit, wodurch die Scanner-Ebene überflüssig wird.
Abstrakte Syntax für Ausdrücke:
> data Expr’ = Plus Expr’ Expr’ | Mul Expr’ Expr’ | Var Char | Lint Int | Sint Int
> type Number = String
Die übliche Grammatik für Ausdrücke ...
E
T
F
Id
Int
Dig
->
->
->
->
->
->
E "+"
T "*"
"(" E
’a’ |
Dig |
’0’ |
T |
F |
")"
’b’
Dig
’1’
T
F
| Id | Int
| ’c’ | ... ’z’
Int
| ’2’ | ’3’ | ’4’ | ’5’ | ’6’ | ’7’ | ’8’ | ’9’
... führt zum folgenden linksrekursiven Parser, der noch keine Unterscheidung zwischen großen
und kleinen Zahlen vornimmt:
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
type Tok
= Char
exComp1
:: [Tok] -> [Expr’]
exComp1 cs = [t | (t,[]) <- pE cs]
pE:: Parser Tok Expr’
pE = pT ||| Plus <<< pE ~~- symbol ’+’ ~~~ pT
pT:: Parser Tok Expr’
pT = pF ||| Mul <<< pT ~~- symbol ’*’ ~~~ pF
pF:: Parser Tok Expr’
pF = symbol ’(’ -~~ pE ~~- symbol ’)’
(Lint . numval) <<< pInt’
||| Var <<< pId’ |||
pId’:: Parser Tok Char
pId’ [] = []
56
>
>
>
>
>
>
>
>
>
pId’ (c:cs) = [(c,cs)| ’a’<= c && c <= ’z’]
pInt’:: Parser Tok String
pInt’ = (:[]) <<< pDig |||
(:) <<< pDig ~~~ pInt’
pDig:: Parser Tok Char
pDig [] = []
pDig (c:cs) = [(c,cs)| isDigit c]
Dieser Parser terminiert für keinen Ausdruck, weil die linksrekursiven Alternativen für E und
T immer anwendbar sind:
pE "a+b*c" ==>
[(Var ’a’,"+b*c") <<...interrupt>>
pE "a*b+c" ==>
[(Var ’a’,"*b+c"),(Mul (Var ’a’) (Var ’b’),"+c") <<...interrupt>>
Entfernen der Linksrekursion Die Grammatik wird umgeformt, um die Linksrekursion zu
entfernen:
E -> T TR
TR -> "+" T TR |
T -> F FR
FR -> "*" F FR |
F -> "(" E ")" |
Id ->’a’ | ’b’ |
Int -> Dig | Dig
Dig -> ’0’ | ’1’
eps
eps
Id | Int
’c’ | ... ’z’
Int
| ’2’ | ’3’ | ’4’ | ’5’ | ’6’ | ’7’ | ’8’ | ’9’
Die Syntaxbäume dieser Grammatik haben eine etwas merkwürdige Struktur, da die beiden
Summanden eines +“ nicht auf gleicher Höhe stehen, sondern im Grunde sogar die falsche,
”
rechtsverschachtelte Struktur vorliegt. Eine solche Struktur ist für die weitere Übersetzung
unbrauchbar.
Erweiterte Parser und weitere Kombinatoren Die Parser für die modifizierte Syntax
brauchen ein zusätzliches Argument, um den gewünschten abstrakten Syntaxbaum aufbauen
zu können. Ihr Typ sei EParser:
> type EParser c a b = c -> Parser a b
Für die richtige Verwendung dieses Arguments sorgt der follow-Kombinator, eine Verallgemeinerung des then-Kombinators:
> follow
:: Parser a b -> (EParser b a c) -> Parser a c
> (p1 ‘follow‘ ep2) inp = [(t,out) | (t1,rest)<-p1 inp, (t,out) <- ep2 t1 rest]
Zur Überprüfung von Kontextbedingungen führen wir den suchthat-Kombinator ein, der eine
Filteroperation auf die Parserausgabe anwendet.
> suchthat
:: Parser a b -> (b -> Bool) -> Parser a b
> (p ‘suchthat‘ b) inp = [(t,r) | (t,r) <- p inp, b t]
Der Parser succeed e erkennt die leere Zeichenreihe und liefert e als Ergebnis.
> succeed
:: b -> Parser a b
> succeed e ts = [(e,ts)]
57
Der Parser für die modifizierte Grammatik Damit erhalten wir nun den Parser für die
modifizierte Grammatik.
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
exComp2 :: [Tok] -> [Expr’]
exComp2 cs = [t | (t,[]) <- pE cs] where
pE :: Parser Tok Expr’
pE = pT ‘follow‘ pTR
pT :: Parser Tok Expr’
pT = pF ‘follow‘ pFR
pTR
:: EParser Expr’ Tok Expr’
pTR t = ((Plus t) ><< symbol ’+’ ~~~ pT) ‘follow‘ pTR
succeed t
|||
pFR
:: EParser Expr’ Tok Expr’
pFR f = ((Mul f) ><< symbol ’*’ ~~~ pF) ‘follow‘ pFR
succeed f
|||
pF
pF
:: Parser Tok Expr’
= symbol ’(’ -~~ pE ~~- symbol ’)’ ||| Var <<< pId’ |||
((Lint . numval) <<< pInt’) ‘suchthat‘ large
|||
((Sint . numval) <<< pInt’) ‘suchthat‘ small
small,large:: Expr’ -> Bool
small (Sint n) = (n< 4096)
large (Lint n) = (n>= 4096)
e1 = "a+b*42+d"
e2 = "a*b+42*(d+e)+42000"
Beispiele:
exComp1 e1 ==>
ERROR: Control stack overflow
(wegen Linksrekursion)
exComp2 e1 ==>
[Plus (Plus (Var ’a’) (Mul (Var ’b’) (Sint 42))) (Var ’d’)]
exComp2 e2 ==>
[Plus (Plus (Mul (Var ’a’)
(Var ’b’))
(Mul (Sint 42) (Plus (Var ’d’)
(Var ’e’))))
(Lint 42000)]
In allen Beispielen haben wir die Parser so geschrieben, dass die Programme unmittelbar der
Grammatik entsprechen. Im allgemeinen ist es natürlich möglich, die Effizienz der Parser zu
steigern durch die üblichen Programmiertechniken wie z.B. die Benutzung von where–Klauseln.
Auch weitere Grammatiktransformationen wie das Ausklammern gemeinsamer Anfänge von
Alternativen sorgen dafür, dass der Parser manche Konstrukte nicht mehrfach analysiert. Für
den Parser pInt ergibt sich z.B.
> pInt’’:: Parser Tok String
> pInt’’ = (:) <<< pDig ~~~ (pInt’ ||| succeed [])
was sich allerdings in diesem Falle nicht lohnt, weil die wiederholte Analyse mittels pDig so gut
wie nichts kostet.
58
4.4
Literatur zum Kapitel Syntaxanalyse
Alle angegebenen Lehrbücher zum Übersetzerbau behandeln Probleme der Syntaxanalyse mehr
oder weniger ausführlich. Die hier vorgestellte Technik des Combinator-Parsing wird in den
folgenden Arbeiten besprochen:
Graham Hutton: Parsing using combinators. In K. Davis, J. Hughes (eds.): Proceedings of
the 1989 Glasgow Workshop on Functional Programming, Springer 1989
Ralf Hinze: Eine Einführung in die funktionale Programmierung mit Miranda. Teubner, 1991
D. Swierstra: LL(k) Parser Combinators
59
Kapitel 5
Übersetzung imperativer Sprachen
5.1
Prozedurmechanismen in P ascal-artigen Sprachen
Problem: Prozeduren in P ascal sind statisch geschachtelt und können dynamisch rekursiv sein.
Variablen werden im Hauptprogramm1 bzw. in den Prozeduren deklariert (var-Deklarationen,
bzw. formale Prozedurparameter). In inneren Prozeduren kann auf globale, d.h. in umfassenden Prozeduren deklarierte Variablen zugegriffen werden. Variablen müssen auf Speicherzellen
der Maschine abgebildet werden. In diesem Abschnitt geht es um diese Speicherverteilung f ür
Variablen unter den Gesichtspunkten:
• Speicherökonomie
• effiziente Adressierung der Variablen
Wegen der potentiellen Rekursivität von Prozeduren ist der benötigte Speicherumfang statisch
nicht bekannt und es kann keine absolute Zuordnung von Variablennamen zu Speicheradressen geben. Rekursive Aufrufe erzeugen ja neue Exemplare der lokalen Variablen, ohne dass
man deswegen die Exemplare der früheren Aufrufe freigeben könnte. Den Ausgangspunkt der
Überlegungen zu Implementierungstechniken bilden die semantischen Verhältnisse in P ascal.
5.1.1
Semantische Aspekte
Bei P ascal-Prozeduren müssen wir drei Aspekte unterscheiden :
• der Text P einer Prozedurdeklaration
• ein Prozedurobjekt OP , welches durch Auswertung einer Prozedurdeklaration entsteht,
und schließlich
• der Aufruf AOP eines Prozedurobjektes OP .
Wenn wir bei Aufrufen AOP nicht am spezifischen Exemplar OP von P interessiert sind, so
sprechen wir auch einfach von einer Inkarnation IP von P . Entsprechend unterscheiden wir
bei Variablen die Variablendeklaration von dem Exemplar der Variablen, welches durch
Auswertung der Deklaration zur Programmlaufzeit entsteht.
Damit hat P ascal folgende semantische Eigenschaften :
1
Im Folgenden wollen wir das Hauptprogramm immer zu den Prozeduren zählen.
60
1. Beim Aufruf AOP eines Prozedurobjektes OP werden neue Exemplare Ex der lokalen
Variablen x (einschließlich formale Parameter) und neue Prozedurobjekte OQ entsprechend
dem Deklarationsteil der Prozedurdeklaration P erzeugt. AOP heißt dann der statische
Vorgänger dieser OQ und aller ihrer Aufrufe AOQ .
2. Die Bindung von Namen zu Deklarationen ist statisch und hierarchisch, d.h. ist x ein
angewandtes Auftreten eines Namens im Text der Prozedur P , so gibt es eine kleinste
textuell umfassende Prozedurdeklaration P ’ (einschließlich P ), die eine Deklaration Dx
für x enthält. (Andernfalls ist das Programm statisch-semantisch inkorrekt.) Dx in P ’
heißt die am Auftreten von x in P sichtbare Deklaration von x.
3. Gemäß 1) hat jede Inkarnation IP (P 6= Hauptprogramm) einen statischen Vorgänger
IQ . Letzterer ist in jedem Fall eine Inkarnation der P textuell unmittelbar umfassenden
Prozedur Q. Der statische Vorgänger von IQ ist dann eine Inkarnation IR der Q direkt
umfassenden Prozedur R, usw. Durch diese statische Vorgängerbeziehung wird damit,
ausgehend von IP , eine statische Verweiskette
SV (IP ) = IP , IQ , IR , . . . , IHauptprogramm
definiert, in der genau eine Inkarnation jeder umfassenden Prozedur vorkommt. Die Reihenfolge des Vorkommens entspricht der Schachtelung von innen nach außen. Beachte,
dass gemäß unserer Terminologie Inkarnationen Aufrufe von Prozedurobjekten sind, d.h.
statische Verweisketten sind Listen der Form
AOP , AOQ , AOR , . . . , AOHauptprogramm ,
wobei es allerdings zum Hauptprogramm nur genau ein Exemplar OHauptprogramm geben
kann.
4. Ist nun Dx in P ’ die an diesem Auftreten von x in P sichtbare Deklaration für x, so ist
beim Zugriff auf x während der Ausführung eines Aufrufs AOP von P gerade das Exemplar
Ex von x gemeint, welches in der auf der statischen Vorgängerkette von AOP liegenden
Inkarnation von P ’ erzeugt wurde. Definiert man die Prozedurschachtelungstiefe pst(P )
einer Prozedur P durch
(
pst(P ) =
0
falls P = Hauptprogramm
pst(Q) + 1 falls P in Q deklariert,
so findet man diese Inkarnation gerade als (pst(P ) − pst(P ’))-tes Element (Zählung mit
0 beginnend) der statischen Verweiskette von AOP .
Wir wollen an einem Beispiel beobachten, dass die Unterscheidung zwischen Prozeduren und
Prozedurobjekten (bzw. -exemplaren) notwendig ist.
61
Beispiel :
procedure Q (procedure R; y : integer);
var x : integer;
procedure P;
begin writeln(x); x := 5 end;
begin
x := 1;
if y = 0
then begin R; P end
else begin x := 2; Q(P,0) end
end
Bei jedem Aufruf Q(. . . , 1) entsteht folgende Sequenz von Aktionen :
Q1 (. . . , 1)
x1 := 1
x1 := 2
Q2 (P 1 , 0)
x2 := 1
P1 1
writeln(x1 ) → ”2”
x1 := 5
P1 2
writeln(x2 ) → ”1”
x2 := 5
{ erster Aufruf Q1 von Q; es entstehen neue Exemplare x1 von x und P 1
von P , Q1 wird statischer Vorgänger von P 1 }
{ zweiter Aufruf Q2 von Q; es entstehen x2 und P 2 , Q2 wird
statischer Vorgänger von P 2 }
{ erster Aufruf P1 1 von P 1 , denn R ist an P 1 gebunden }
{ da Q1 statischer Vorgänger von P1 1 }
{ P ist in Q2 an P 2 gebunden }
{ da Q2 statischer Vorgänger von P1 2 }
Das Beispiel zeigt, dass die Unterscheidung von verschiedenen Exemplaren einer Prozedur, wie
P 1 und P2 , notwendig ist, weil es für den Zugriff auf die globale Variable x wichtig zu wissen
ist, zu welchem rekursiven Aufruf von Q das x gehört. Wenn wir uns also vorstellen, dass jede
Inkarnation IQ von Q ein eigenes Exemplar von P erzeugt, dessen in Q deklarierte globale
Variablen gerade an die von IQ erzeugten Exemplare gebunden sind, so ist dieses Bindungsproblem eindeutig gelöst. Die verschiedenen Exemplare einer Prozedur unterscheiden sich also im
allgemeinen in der Bindung der globalen Variablen.
Wichtig ist nun, dass in P ascal der statische Vorgänger AOP eines Aufrufs AOQ zum Zeitpunkt
des Aufrufs AOQ noch lebt, d.h. insbesondere der für seine lokalen Variablen allokierte Speicher
noch nicht freigegeben sein kann. Umgekehrt, wird AOP beendet, so kann es danach auch keine
Aufrufe AOQ mehr geben, die AOP als statischen Vorgänger haben. Dies liegt daran, dass OQ
nur so an Aufrufsstellen außerhalb P transportiert werden kann, indem OQ als aktueller Parameter über eine in P beginnende Folge von Prozeduraufrufen zu dieser Aufrufstelle geleitet
wird. Solange wie diese Aufrufsfolge nicht vollständig beendet ist, lebt AOP noch. Könnten
Prozeduren als Ergebnisse von Prozeduren entstehen (wie dies in funktionalen Sprachen der
62
Fall ist), so wäre die Situation grundlegend anders. Dies bedeutet, dass die Exemplare der lokalen Variablen in AOP nach Beendigung von AOP nicht mehr zugegriffen werden können und
daher der zugehörige Speicher freigegeben werden kann. Die Grundidee für eine ökonomische
Speicherverteilung ist daher, den Speicher kellerartig mit den Prozeduraufrufen pulsieren zu
lassen. Zu Beginn eines Aufrufs AOP werden neue Speicherzellen oben auf dem Keller für die
neuen Exemplare der lokalen Variablen (einschließlich Parameter) reserviert. Bei Beendigung
von AOP wird der Kellerpegel auf den alten Stand vor AOP zurückgesetzt.
Dies ergibt damit folgendes Schema für Variablenzugriffe:
Sei x ein angewandtes Auftreten einer Variablen in der Prozedur P . Sei Q die Prozedur, die
die zugehörige Deklaration enthält. Dann gilt (bzw. wird so festgelegt):
1. pst(Q) ≤ pst(P ) wegen der Sichtbarkeitsregeln
2. Ist IP eine Inkarnation von P , so ist, falls pst(Q) < pst(P ), das (pst(P ) − pst(Q))-te
Element2 in SV (IP ) die Inkarnation von Q, die das Exemplar von x erzeugt hat, auf
welches in IP zugegriffen wird. Für pst(P ) = pst(Q) ist P = Q und x eine lokale Variable
(oder ein formaler Parameter) von P .
3. Für diese in 2. bestimmte Inkarnation IQ von Q gibt es einen eigenen Speicherbereich
für die lokalen Variablen. Eine Basisadresse BAIQ zeige auf eine feste Stelle in diesem
Speicherbereich. Die Adresse des an x gebundenen Objekts ist dann
Adressex = BAIQ + RelAdr(Q,x)
Dabei ist BAIQ für jede Inkarnation IQ spezifisch und damit eine dynamische Größe.
RelAdr(Q,x) ist eine Adresse von x relativ zur Basisadresse einer jeden Inkarnation von Q.
Sie ist nur von Q, nicht von IQ abhängig, also statisch, d.h. vom Übersetzer auszurechnen.
5.1.2
Lokaler Aufbau des Prozedurspeicherbereichs (englisch: activation area)
Gegeben sei eine Prozedur P auf Schachtelungstiefe k. Sei weiter (IP , IPk−1 , . . . , IP0 ) die statische Verweiskette für eine Inkarnation IP von P . Dann ist eine Belegung des Speicherbereichs
für die Inkarnation IP gemäß folgendem Schema sinnvoll:
2
Zählung bei 0 beginnend
63





BR
•
- BA(IP ) :
BA(IP0 )
BA(IP1 )
BA(IPk−1 )


































)
TOP •
(aktuelle) Parameter
BA dynamischer Vorgänger
Rücksprungadresse
Display (stat. Verweiskette)
statische Anteile der lokalen
Variablen einschließlich Hilfsvariablen
dynamische Anteile der lokalen
Variablen und Hilfsvariablen
-
Hierbei zeigt also die Basisadresse immer auf den Anfang des Display. 3 Die Basisadresse der
laufenden Inkarnation und das Ende des belegten Speichers werden in geeigneten Registern BR
(Basisregister) und TOP (Stack Top) gehalten.4
Adressierungsschema für Variablen und Parameter
(
Ax =
BR + reladrx
für lokale Variablen und Parameter
M[BR + i] + reladrx für glob. Var. und Par., die auf pst i deklariert sind
Konvention: Schreibweise wie bei P ascal, also implizites cont“ bei allen Registern und Spei”
cherzellen, die in Ausdrücken auftreten. In BR + i steht BR also für den Inhalt des Registers
BR. M[a] ist der Inhalt der Speicherzelle mit Adresse a.
Für die Adressierung benötigen wir also die statischen Größen Relativadresse reladrx und
Prozedurschachtelungstiefe i, sowie die dynamisch bestimmte aktuelle Basisadresse BAIP , die
über das Register BR adressiert wird.
5.1.3
Codeerzeugung für den Umgebungswechsel (Aufrufstelle)
Da wir nun wissen, wie der Prozedurspeicher organisiert sein soll, können wir die Codeerzeugung für Prozedurrümpfe und -aufrufe besprechen. Die erzeugten Codestücke müssen bei ihrer
Ausführung zur Laufzeit diese Speicherstrukturen erzeugen. Dazu gehen wir davon aus, dass ein
hardwareunterstützter Keller mit zugehörigen push- und pop-Operationen existiert. Das oben
erwähnte Register T OP zeige automatisch immer auf das oberste Kellerelement.
3
Damit werden die aktuellen Parameter mit negativem offsets relativ zu BA adressiert. Display und lokale
Variablen werden dagegen mit positiven offsets angesprochen. (Rücksprungadresse und dynamischer Vorgänger
werden erst beim Rücksprung wieder gebraucht.)
4
Gängige Alternative zum Display: nur die Basisadresse des statischen Vorgängers abspeichern und die
Adressierung über eine Folge von Dereferenzierungen vornehmen.
64
Codeerzeugung für Prozeduraufrufe Q(. . .) in Prozedur P
Im folgenden bedeutet
werden.
push(aktuelle Parameter̄);
, dass hier weiter unten noch konkrete Codestücke eingesetzt
push(BR);
{impliziter Parameter: BA des dynamischen Vorgängers}
HR := BASV Q ;
{Sicherung der BA des statischen Vorgängers im Hilfsregister HR}
call codeQ ;
{Prozeduransprung mit Kellerung der Rücksprungadresse
LRück. Symmetrisch dazu nimmt man an, dass Q mit einem
Sprung verlassen wird, der sein Argument oben auf dem Keller
erwartet und es selbst löscht.}
LRück:
BR := pop;
{dynamischer Vorgänger von Q(= BA von IP )}
pop(P L);
{P L ist die statisch bekannte Länge des Speicherbereichs für die
Parameter}
Hierbei sind für BASV Q und codeQ zwei Fälle zu unterscheiden, die in unterschiedlichem Code
resultieren:
A) Q ist kein Parameter
B) Q ist formaler Parameter einer umfassenden Prozedur S
Fall A):
Q ist keine (formale) Parameterprozedur:
Es sei l = pst(P ) — eine statisch festgelegte Größe. In diesem Fall ist pstQ = pst(Q) und codeQ
ebenfalls statisch an der Aufrufstelle bestimmbar. Es ist dann:
a) pstQ ≤ l oder
b) pstQ = l + 1 , falls Q in P deklariert
(
M[BR + pstQ − 1] im Falle a)
BR
im Falle b)
Im Falle A) b) ist der statische gleich dem dynamischen Vorgänger.
und somit BASV Q =
Fall B): Q ist formaler Parameter einer umfassenden Prozedur S
Da in diesem Fall das aufzurufenden Prozedurobjekt nur dynamisch bestimmt ist, müssen die
charakteristischen Größen BAstat. Vorgänger und Codeanfangsadresse beim Aufruf von S bestimmt
und übergeben werden.
65
B.1) S = P , d.h. Q ist formaler Parameter der aufrufenden Prozedur P
Beispiel:
begin HP
int X;
proc R;
begin int X;
proc A;
begin
X := 1;
end
2: call P (A);
end;
proc P(proc Q);
begin
3 : call Q;
end;









pst = 2









pst = 1
1: call R
end;
call P (A) an 2: führt zu einem call A an 3: Der statische Vorgänger ist hier R, welches nicht
auf der statischen Verweiskette für die A aufrufende Prozedur P (einschließlich P ) liegt.
⇒ Der statische Vorgänger einer aufzurufenden Prozedur liegt nicht auf der Kette der statischen
Vorgänger der aufrufenden Prozedur. Allerdings ist zum Zeitpunkt der Übergabe des aktuellen
Prozedurparameters A an den formalen Parameter Q diese Information bekannt, wenn sie für
A bekannt ist. Dies legt die Implementierung eines Prozedurobjektes als ein Paar
codeA
BAstatischer
V orgänger von A
nahe.
66
Der Kellerzustand nach Ansprung von A:
• •
•
- BA
HP :
- BAR :
- BAP :
BAA :
..
.
X
BAHP
Rück 1
BAHP
X
CodeAdrA
BAR
BAR
Rück 2
BAHP
BAP
Rück 3
BAHP
BAR
Beschreibung des aktuellen
Parameters A in P (A)
statischer Vorgänger von A
Dynamische Vorgänger DV der Aufrufe: DV (A) = P , DV (P ) = R, DV (R) = HP ;
Statische Vorgänger SV : SV (A) = R, SV (R) = HP , SV (P ) = HP
Damit, wenn reladrQ (statisch! Im Beispiel = −4) die Relativadresse des formalen Parameters
Q in S = P ist, gilt für das Codeschema an der Aufrufstelle von Q:
BASV Q = M[BR + reladrQ + 1]
codeQ = M[BR + reladrQ ]
B.2) S umfasst die aufrufende Prozedur P echt, d.h. Q ist globaler formaler Parameter
⇒
Adressierung der Größen BASV Q und codeQ wie bei allen globalen Größen
Muster :
über die pst
)S und das Display von P nach bekanntem
(
0
codeQ
= M[M[BR + pstS ] + reladrQ +
]
BASV Q
1
Im Unterschied zu skalaren Parametern werden bei Prozeduren also zwei Werte übergeben.
5.1.4
Codeerzeugung für Prozedurdeklaration Q
Annahmen:
• L = Länge des Speicherbereichs für die statischen Anteile der lokalen Variablen von Q
• Register HR enthält die Basisadresse BASV Q des statischen Vorgängers von Q bei Ansprung von Q. Damit enthält es die Anfangsadresse des zu kopierenden Displays.
67
Es ergibt sich folgendes Codeschema:
codeQ :
{Marke für die Prozedur Q}
push(M[HR + i])
, 0 ≤ i < pstQ − 1;
push(HR);
{Damit ist das Display erstellt.}
BR := T OP − pstQ ;
{Einstellung der aktuellen Basisadresse}
T OP +:= L;
{Speicherreservierung für die lokalen Variablen}
< Code für Rumpf von Q >
T OP := BR − 1;
{T OP zeigt jetzt auf die Rücksprungadresse}
return;
{≡ goto M[pop]}
5.2
Ablegung von Variablen mit komplexer Struktur im
Speicher
Hier soll nun in Abhängigkeit vom Typ die Speicherablage für die Komponenten einer Variablen
behandelt werden. Wir skizzieren, wie der Speicherbedarf bedarf und die Relativadressen reladr
für Komponenten ausgerechnet werden. Wir behandeln records, arrays und Vereinigungstypen,
wie sie in P ascal und anderen, verwandten Sprachen üblich sind. Aspekte der Packung (z.B.
packed arrays) werden nicht berücksichtigt.
5.2.1
t
Records
= record
r1 : t1 ; · · · ; rk : tk ;
end
Sequentielles Ablegen der Komponenten:
r1
···
rk
Also:
bedarf (t) =
k
X
bedarf (ti )
i=1
reladr(rj ) =
j−1
X
i=1
68
bedarf (ti )
5.2.2
Statische Arrays
t = array [u1 ..o1 , . . . , uk ..ok ] of t0
ui, oi zur Übersetzungszeit bekannt;
u i ≤ oi , 1 ≤ i ≤ k
Zeilenweises Ablegen
bedarf (t) = (
k
Y
(oi − ui + 1)) ∗ bedarf (t0 )
i=1
reladr(element[i1 , . . . , ik ]) = bedarf (t0 ) ∗ (dk . . . d2 (i1 − u1 ) + dk . . . d3 (i2 − u2 ) + · · · + (ik − uk )) ,
wenn di = oi − ui + 1
Spaltenweises Ablegen
- gleicher Platzbedarf
- reladr(element[i1 , . . . , ik ]) = bedarf (t0 ) ∗ ((i1 − u1 ) + (i2 − u2 )d1 + · · · + (ik − uk )d1 . . . dk−1)
Für beide Fälle kann man zur schelleren Berechnung der reladr die Subtraktionen sparen, indem
man eine fiktive [0, . . . , 0]-Adresse (im folgenden null adr) verwendet. null adr ist statisch
berechenbar:
null adr = reladr(element[0, , . . . , 0])
Für die anderen Adressen ergibt sich dann z.B. bei zeilenweisem Ablegen:
reladr(element[i1 , . . . , ik ]) = bedarf (t0 ) ∗ (dk . . . d2 i1 + dk . . . d3 i2 + · · · + ik − null adr)
Im folgenden Abschnitt sehen wir, dass wir durch Ausklammerung der di noch mehr Rechenzeit
sparen können.
5.2.3
Dynamische Arrays
Man spricht von dynamischen arrays, wenn die ui und oi zur Abarbeitungszeit der Deklaration
(etwa bei Block oder Prozedureintritt) berechnet werden, aber zur Übersetzungszeit noch unbekannt sind. Nach dem Abarbeiten der Deklaration bleibt die Größe des arrays unverändert.
Dynamische arrays haben eine statisch bekannte Dimension k ≥ 1. Entsprechend gliedert sich
der Speicher für ein dynamisches array in einen statischen und einen dynamischen Anteil.
Statischer Anteil:
AA
u1
o1
···
uk
ok
69
d.h. bedarf (t) = 2k + 1 . In dieser Liste statischer Länge werden zur Laufzeit die Grenzen und
die Anfangsadresse des eigentlichen Arrays eingetragen. Diese Liste heißt dope vector (DV ) des
Arrays.
Aktionen bei Bekanntwerden der Grenzen
(1) Eintragen der Grenzen in den dope vector
for i in 1 . . . k do
DV [ 2i−1 ] := ui ;
DV [ 2i ]
:= oi ;
endfor ;
DV [ 0 ] := cont(TOP)+1;
{ Adresse der ersten freien Zelle im Speicher}
(2) Reservieren des array-Speichers
TOP +:= (d1 . . . dk ) ∗ bedarf (t0 )
{ Elementtyp t0 }
(3) Einstellen der fiktiven [0,. . . ,0]-Adresse
DV [ 0 ] − := (dk . . . d2 u1 + dk . . . d3 u2 + . . . + uk ) ∗ bedarf (t0 )
(4) Adressierung von array-Elementen
Es sei r die Relativadresse des DV des array a bezüglich Basisadresse BA. Dann ergibt sich als
Adresse für a[i1 , . . . , ik ], bei zeilenweisem Abspeichern des array:
adresse = M[BA + r] + dk . . . d2 i1 + . . . + dk ik−1 + ik
Damit ergeben sich bei dieser Art der Adressierung k(k − 1)/2 Multiplikationen und (k − 1)Additionen.
Durch Umformung erhalten wir aber auch nach dem Hornerschema
adresse = M[BA + r] + (. . . (i1 d2 + i2 )d3 + . . . + ik−1 )dk + ik
und damit nur (k − 1)-Additionen und (k − 1)-Multiplikationen. Dies ist für k ≥ 3 strikt besser.
Analog für spaltenweises Abspeichern.
5.2.4
Vereinigungstypen
t = t1 + t2
Überlappen des Speichers für die t1 - bzw. t2 -Alternative:
bedarf (t) = max(bedarf (t1 ), bedarf (t2 )).
In Pascal bilden die Varianten eines varianten records einen solchen Vereinigungstyp (siehe
unten).
Eine ähnlich überlappende Speicherstruktur ergibt sich in Sprachen mit Blockkonzept (z.B.
Algol, Simula, Lisp) auch für die Variablen paralleler Blöcke. In folgendem Beispiel sind die
beiden Blöcke, in denen v und w, bzw. temp deklariert sind, parallel.
70
Beispiel
begin
proc sort (n: integer);
begin
a: array [1 .. n] of integer;
<———— 1
z: integer;
...
begin v,w : integer; . . . end
...
begin
temp: array [1 .. 2 ∗ n + 1] of integer;
.
.
. (∗)
end;
end;
intarr: array [1 .. 5] of integer;
l: integer ;
...
sort(l − 1); L1: . . .
...
end
71
<——– 2
Aufbau des Speichers an der Stelle (∗)
BA0 :
,→
BA1 :
(fiktive Adresse a[0])
(fiktive Adresse von temp[0])
TOP nach Aufruf von SORT
TOP nach Abarbeiten von 1
→
T1 :
→
T2 :



-< Return → System >- 




intarr[1]



 Hauptintarr[2]
intarr[3]
intarr[4]
intarr[5]
l
l−1
BA0
L1
BA0
T1 − 1
1
n
z
v | T2 − 1
w|1
2∗n+1



























Parameter n



BA für Hauptprog. 



Rücksprungadresse 



}Display







DV von Array a 












 Pro-
DV von Array temp ze
dur




sort





Inhalt von Array a 










..
.






















Inhalt v. Array temp



















..
.
TOP nach Abarbeiten von 2
programm
→
Die Speicherzellen für v und w werden also für Teile des dope vector von temp wiederverwendet.
5.2.5
Records in Pascal (variant records)
R: record
s1 : t1 ;
···
sk : tk ;
case sk+1 : tk+1 of
a1 : (s11 : t11 ; . . . ; s1n1 : t1n1 );
···
am : (sm1 : tm1 ; . . . ; smnm : tmnm )
end
72
Schema :
alle Felder hintereinander (evtl. gepackt und in beliebiger Reihenfolge)
Varianten übereinander (wie Vereinigungtypen)
s1
R:
..
.
s11
sk
sk+1
···
..
.
s1n1
⇒
⇒
5.3
sm1
..
.
















max{ Platz für Variante ai | 1 ≤ i ≤ m }





smnm 









Zur Übersetzungszeit bekannt:
Relativadressen rsi , rsij für die verschiedenen Komponenten.
R.si liegt bei R + rsi und R.sij bei R + rsij
Die Implementierungsabbildung in Haskell
In diesem Kapitel haben wir betrachtet, wie wir das semantisch analysierte Quellprogramm
auf einer abstrakten Maschine realisieren. Diesen Übergang nennt man Implementierung“ im
”
engeren Sinne. Wir beschreiben ihn durch eine Ableitung im erweiterten Schema in Haskell.
Die abstrakte Maschine ist charakterisiert durch den Laufzeitkeller, der mittels der Register
BR, HR, TOP verwaltet wird. Dagegen treten im analysierten Quellprogramm Größen wie Prozedurschachtelungstiefen, Parameter, unterschiedliche Prozedurarten etc. auf.
Wir geben für das analysierte Quellprogramm eine Zwischensprache IL1, für das abstrakte
Maschinenprogramm eine Zwischensprache IL2 an.
IL1 unterscheidet sich von der abstrakten Syntax aus Kapitel 3 dadurch, dass Definitionen
(außer bei Prozeduren) verschwunden sind und Namen durch ihre wesentlichen Attribute ersetzt
sind. Den If- und While-Anweisungen wurden Marken beigefügt.
IL2 unterscheidet sich von der in Kapitel 6 eingeführten Zielsprache dadurch, dass beliebige
Ausdrücke über Zellen und Operationen gebildet weden können, ohne Rücksicht auf vorhandene
Maschinenbefehle.
Die Implementierungsfunktion“ impl: IL1 -> IL2 entspricht dem erweiterten Arbeitssche”
ma. Compilezeitaktionen (die gleich ausgeführt werden) sind Funktionen wie (:), concat,
map, (+1).
impl wird realisiert durch die folgende Familie von impl -Funktionen:
>impl_prog
>impl_stat
>impl_var
>impl_expr
::
::
::
::
Iprog1
Pst ->
Pst ->
Pst ->
-> Iprog2
Stat1 -> [Stat2]
Var1
-> Var2
Expr1 -> Expr2
73
>impl_cond :: Pst -> Cond1 -> Cond2
>impl_aktpar:: Pst -> Aktpar -> [Stat2]
Für impl stat bis impl aktpar wird im ersten Argument als Kontextinformation die aktuelle
Prozedurschachtelungstiefe mitgegeben, die zur Bestimmung der Basisadresse, der statischen
Vorgänger und zum Display-Aufbau benötigt wird.
Die Zwischensprache IL1
>type
>type
>type
>type
>type
>
>data
>data
>
>
>
>
>data
>
>data
>data
>
>data
>
>data
>
>data
Reladr
Pst
Label
Parlength
Varlength
=
=
=
=
=
Int
Int
Int
Int
Int
Iprog1 = Iprog1 Varlength [Stat1]
deriving
Stat1 = Assign1 Var1 Expr1 |
IfThen Cond1 [Stat1] Label |
While Label Cond1 [Stat1] Label |
ProcDef Label Varlength Pst [Stat1] |
Call Procid [Aktpar]
deriving
Procid = Proc Label Parlength Pst | FormProcPar Parlength Pst Reladr
deriving
Var1
= Var Pst Reladr Vkind
deriving
Vkind = IsValue | IsVarPar
deriving
Show
Show
Show
Show
Show
Aktpar = ValPar Expr1 | VarPar Var1 | ProcPar Procid
deriving Show
Cond1
= Lessop1 Expr1 Expr1 | Leqop1
deriving Show
Expr1
= Addop1 Expr1 Expr1 | Const Int | VarExp Var1
Expr1 Expr1
deriving Show
Die Zwischensprache IL2
>data
>data
>
>
>
>
>data
>
>data
>
>data
Iprog2 = Iprog2 [Stat2]
Stat2 = Ass Var2 Expr2 |
Jump Label | Cjump Bool Label Cond2 |
DefLabel Label |
Pcall Label | FPcall Expr2 | Return
deriving Show
Var2
= M Expr2 | BR | HR | TOP | Push
deriving Show
Cond2
= Lessop2 Expr2 Expr2 | Leqop2
deriving Show
Expr2
= A Expr2 Expr2 | C Int | D Var2 | Pop | LabExp Label
Expr2 Expr2
Die Implementierungsabbildung impl: IL1 -> IL2
>cmap::(a ->[b])->[a]->[b]
>cmap f = concat . map f
>impl_prog (Iprog1 vl ss)
>
= Iprog2 ([Ass TOP (C vl), Ass BR (C 0), Jump (-1)] ++
>
cmap (impl_stat 0) ss1 ++ [DefLabel (-1)] ++
>
cmap (impl_stat 0) ss2)
>
where (ss1, ss2) = splitProc ss
74
deriving Show
deriving Show
>
>impl_var aktpst (Var p r IsValue)
>
| p < aktpst
= M (A (D(M(A (D BR) (C p))))(C r))
>
| p == aktpst = M (A (D BR) (C r))
>
>impl_var aktpst (Var p r IsVarPar)
>
= M(D(impl_var aktpst (Var p r IsValue)))
>
>impl_expr aktpst (Addop1 e1 e2)
>
= A (impl_expr aktpst e1) (impl_expr aktpst e2)
>impl_expr aktpst (Const n) = C n
>impl_expr aktpst (VarExp v) = D (impl_var aktpst v)
>
>impl_cond aktpst (Lessop1 e1 e2)
>
= Lessop2 (impl_expr aktpst e1) (impl_expr aktpst e2)
>impl_cond aktpst (Leqop1 e1 e2)
>
= Leqop2 (impl_expr aktpst e1) (impl_expr aktpst e2)
>
>impl_aktpar aktpst (ValPar e)
>
= [Ass Push (impl_expr aktpst e)]
>impl_aktpar aktpst (VarPar v)
>
= [Ass Push a | M a <- [impl_var aktpst v]]
>impl_aktpar aktpst (ProcPar (Proc l pl p))
>
= [Ass Push (LabExp l), Ass Push sv]
>
where sv
>
| p <= aktpst
= D (M (A (D BR) (C (p-1))))
>
| p == aktpst+1
= (D BR)
>impl_aktpar aktpst (ProcPar (FormProcPar pl p r))
>
= [Ass Push l, Ass Push sv]
>
where sv = D (impl_var aktpst (Var p (r+1) IsValue))
>
l = D (impl_var aktpst (Var p
r
IsValue))
>
>impl_stat aktpst (Assign1 v e)
>
= [Ass (impl_var aktpst v) (impl_expr aktpst e)]
>
>impl_stat aktpst (IfThen c ss l)
>
= [Cjump False l (impl_cond aktpst c)] ++
>
cmap (impl_stat aktpst) ss ++ [DefLabel l]
>
>impl_stat aktpst (While lt c ss le)
>
= [DefLabel lt, Cjump False le (impl_cond aktpst c)] ++
>
cmap (impl_stat aktpst) ss ++ [Jump lt, DefLabel le]
>
>impl_stat aktpst (ProcDef l vl p ss)
>
= cmap (impl_stat p) ss1 ++ [DefLabel l] ++
>
setBR ++ display ++ allocvars ++
>
cmap (impl_stat p) ss2 ++ [deallocate, Return]
>
where setBR = [Ass BR (A (D TOP) (C 1))]
>
display = [Ass Push (D (M (A (D HR) (C i)))) |
>
i<-[0..(p-2)]] ++ [Ass Push (D HR)]
>
allocvars = [Ass TOP (A(D TOP) (C vl))]
>
deallocate = Ass TOP (A (D BR) (C (-1)))
>
(ss1, ss2) = splitProc ss
>
>impl_stat aktpst (Call(Proc l pl p) pars)
75
>
>
>
>
>
>
>impl_stat
>
>
>
>
>
>pushDV
>restoreDV
>popPars s
= cmap (impl_aktpar aktpst) pars ++
[pushDV, loadSV, Pcall l, restoreDV, popPars pl]
where loadSV
| p <= aktpst
= Ass HR (D(M(A (D BR) (C(p-1)))))
| p == aktpst+1 = Ass HR (D BR)
aktpst (Call(FormProcPar pl p r) pars)
= cmap (impl_aktpar aktpst) pars ++
[pushDV, loadSV, call, restoreDV, popPars pl]
where loadSV = Ass HR (D(impl_var aktpst (Var p (r+1) IsValue)))
call
= FPcall (D(impl_var aktpst (Var p r IsValue)))
= Ass Push (D BR)
= Ass BR Pop
= Ass TOP (A (D TOP) (C(-s)))
>splitProc []
=
>splitProc (ProcDef w x y z : r) =
>
>splitProc x
=
([],[])
(ProcDef w x y z : r1, r2)
where (r1,r2) = splitProc r
([], x)
Test Section
>v1 = Var 1 1000 IsValue
>v2 = Var 5 5000 IsValue
>a1 = Var 4 (-9) IsValue
>a2 = Var 4 (-3) IsVarPar
>s1 = Assign1 v2 (VarExp v2)
>s2 = Assign1 v1 (Const 99)
>c1 = Leqop1 (VarExp a1) (VarExp a2)
>d1 = ProcDef 88 100 2 [ s2 ]
>pd1 = Proc 88 3 2
>pd2 = FormProcPar 4 2 (-7)
>i0 = Iprog1 0 [Assign1 v1 (VarExp v2), Assign1 v2 (VarExp v1),
>
Assign1 a1 (VarExp v2), Assign1 a2 (VarExp v2)]
>i1 = Iprog1 0 [While 333 c1 [s1] 334]
>i2 = Iprog1 0 [d1, Call pd1 [ValPar (VarExp v2), (VarPar v2), ValPar(VarExp a2)]]
>i3 = Iprog1 0 [d1, Call pd2 [ProcPar pd1, ProcPar pd2]]
>ip (Iprog1 vl ss) = showi2 (Iprog2 (cmap (impl_stat 5) ss))
>showi2 (Iprog2 ss) = "Iprog2:\n" ++ cmap showinst ss
>
>showinst:: Stat2 -> [Char]
>showinst i = show i ++ "\n"
Testergebnisse
Main> i0
Iprog1 0 [Assign1
Assign1
Assign1
Assign1
(Var
(Var
(Var
(Var
1
5
4
4
1000
5000
(-9)
(-3)
IsValue) (VarExp (Var 5 5000 IsValue)),
IsValue) (VarExp (Var 1 1000 IsValue)),
IsValue) (VarExp (Var 5 5000 IsValue)),
IsVarPar) (VarExp (Var 5 5000 IsValue))]
Main> ip i0
Iprog2:
76
Ass
Ass
Ass
Ass
(M
(M
(M
(M
(A
(A
(A
(D
(D
(D
(D
(M
(M (A (D BR) (C 1)))) (C 1000))) (D (M (A (D BR) (C 5000))))
BR) (C 5000))) (D (M (A (D (M (A (D BR) (C 1)))) (C 1000))))
(M (A (D BR) (C 4)))) (C (-9)))) (D (M (A (D BR) (C 5000))))
(A (D (M (A (D BR) (C 4)))) (C (-3)))))) (D (M (A (D BR) (C 5000))))
Main> i1
Iprog1 0 [While 333 (Leqop1 (VarExp (Var 4 (-9) IsValue))
(VarExp (Var 4 (-3) IsVarPar)))
[Assign1 (Var 5 5000 IsValue) (VarExp (Var 5 5000 IsValue))]
334]
Main> ip i1
Iprog2:
DefLabel 333
Cjump False 334 (Leqop2 (D (M (A (D (M (A (D BR) (C 4)))) (C (-9)))))
(D (M (D (M (A (D (M (A (D BR) (C 4)))) (C (-3))))))))
Ass (M (A (D BR) (C 5000))) (D (M (A (D BR) (C 5000))))
Jump 333
DefLabel 334
Main> i2
Iprog1 0 [ProcDef 88 100 2
[Assign1 (Var 1 1000 IsValue) (Const 99)],
Call (Proc 88 3 2) [ValPar (VarExp (Var 5 5000 IsValue)),
VarPar (Var 5 5000 IsValue),
ValPar (VarExp (Var 4 (-3) IsVarPar))]]
Main> ip i2
Iprog2:
DefLabel 88
Ass BR (A (D TOP) (C 1))
Ass Push (D (M (A (D HR) (C 0))))
Ass Push (D HR)
Ass TOP (A (D TOP) (C 100))
Ass (M (A (D (M (A (D BR) (C 1)))) (C 1000))) (C 99)
Ass TOP (A (D BR) (C (-1)))
Return
Ass Push (D (M (A (D BR) (C 5000))))
Ass Push (A (D BR) (C 5000))
Ass Push (D (M (D (M (A (D (M (A (D BR) (C 4)))) (C (-3)))))))
Ass Push (D BR)
Ass HR (D (M (A (D BR) (C 1))))
Pcall 88
Ass BR Pop
Ass TOP (A (D TOP) (C (-3)))
Main> i3
77
Iprog1 0 [ProcDef 88 100 2
[Assign1 (Var 1 1000 IsValue) (Const 99)],
Call (FormProcPar 4 2 (-7))
[ProcPar (Proc 88 3 2),
ProcPar (FormProcPar 4 2 (-7))]]
Main> ip i3
Iprog2:
DefLabel 88
Ass BR (A (D TOP) (C 1))
Ass Push (D (M (A (D HR) (C 0))))
Ass Push (D HR)
Ass TOP (A (D TOP) (C 100))
Ass (M (A (D (M (A (D BR) (C 1)))) (C 1000))) (C 99)
Ass TOP (A (D BR) (C (-1)))
Return
Ass Push (LabExp 88)
Ass Push (D (M (A (D BR) (C 1))))
Ass Push (D (M (A (D (M (A (D BR) (C 2)))) (C (-7)))))
Ass Push (D (M (A (D (M (A (D BR) (C 2)))) (C (-6)))))
Ass Push (D BR)
Ass HR (D (M (A (D (M (A (D BR) (C 2)))) (C (-6)))))
FPcall (D (M (A (D (M (A (D BR) (C 2)))) (C (-7)))))
Ass BR Pop
Ass TOP (A (D TOP) (C (-4)))
78
Kapitel 6
Codeerzeugung
6.1
Überblick zur maschinenspezifischen Codeerzeugung
Rechnerarchitektur Nun weiß man ja Dank Church’s These, dass alle Rechenmaschinen
(außer den ganz schlichten Modellen wie etwa endliche Automaten) im Prinzip gleichmächtig
sind, also alle Turing-berechenbaren Funktionen implementieren können. Auf den genauen Befehlsvorrat kommt es also “nur” in der folgenden Hinsicht an:
• Geschwindigkeit des Prozessors — dabei kommmt es nicht nur auf die Taktrate an, sondern auch darauf, wieviel Rechnung pro Takteinheit geleistet wird.
• Nutzbarkeit des Prozessors — d.h. wie schwierig ist es, für diesen Befehlsvorrat zuverlässigen und effizienten Code zu erzeugen oder auch (in den ganz frühen Jahren) von Hand
zu schreiben.
In den frühen Jahren der Informatik beobachtete man die Entwicklung von Rechnern mit immer mächtigeren Befehlssätzen. Heute nennt man diese CISC-Architekturen was für complex
instruction set computer steht. Ziele der CISC-Evolution: Effizienz durch mächtige Instruktionen (oft mikroprogrammiert). Aufwärts-Kompatibilität über mehrere Prozessorgenerationen.
Beispiele:
IBM 360/370
PDP11, VAX
Motorola 680x0, x ∈ {0, 1, 2, 3, 4, 5, 6}
Intel 80x86, x ∈ {ε, 1, 2, 3, 4}
Extreme Umsetzungen dieser Idee waren HLL– (high-level-language) Architekturen, deren komplexe Befehle direkt Sprachkonstrukten einer Programmiersprache entsprechen (Beispiel: Burroughs 6800 für ALGOL 60).
In den späten 70er Jahren formierte sich die Kritik an der CISC-Entwicklung: Komplexe Instruktionen
• sind selten anwendbar, und werden von Übersetzern oft ganz ignoriert,
• verlangsamen auch die Durchführung einfacher Instruktionen,
• verteuern Prozessoren und verlängern Entwicklungszeiten.
79
So wurde als radikale Trendwende das RISC-Konzept (reduced instruction set computer ) entwickelt, mit den folgenden Prinzipien:
• extrem einfacher Befehlsvorrat,
• Prozessor-interne Parallelität (pipelining), in Verbindung mit speziellen CodeerzeugungsTechniken, um diese zu nutzen,
• hohe Taktfrequenz, u.a. durch Wegfall der Mikroprogramm-Ebene,
• Ausnutzung neuester Technologie dank kurzer Entwicklungszeiten.
Beispiele sind MIPS, Motorola 88000, SPARC.
RISC-Prozessoren erreichen hohe Geschwindigkeiten durch Prozessor-interne Parallelität, zum
Teil auch durch Weglassen von Hardware-Sperren gegen Konflikte (Beispiel: Ein Befehl schreibt
in ein Register, während ein anderer daraus liest). Dem Compiler fällt die Aufgabe zu, die
Parallelität auszunutzen und Konflikte auszuschließen.
Bei der noch nicht ganz erloschenen Debatte RISC vs. CISC darf man zwei Punkte nicht
übersehen:
1. Entscheidend ist nicht allein die hohe Rechenleistung eines Prozessors, sondern auch die
rechtzeitige Verfügbarkeit guter und zuverlässiger Compiler.
2. Der aktuelle Vorteil der RISC-Prozessoren beruht auf der Tatsache, dass sich die Grundtechnologie noch stürmisch entwickelt. Erst wenn diese Entwicklung langsamer wird, lässt
sich die Frage nach der besseren“ Architektur wieder stellen.
”
Aufgaben der maschinenspezifischen Codeerzeugung Durch die Ersetzung von Quellprogramm-Variablen durch Adressierungsbäume der abstrakten Maschine und durch Einsetzen
der abstrakten Maschinenoperationen für Prozeduraufrufe und Parameterübergabe liegt das
Programm in IL2 nun in einer maschinenorientierten Form vor. Es enthält jedoch noch keine
spezifische Annahme über eine Zielmaschine.
Alle Zielmaschinen-spezifischen Aufgaben des Compilers fasst man unter dem Titel Codeer”
zeugung“ zusammen; man spricht auch vom back-end“ des Compilers.
”
Zur Codeerzeugung gehören die folgenden Aufgaben:
• Code-Selektion: Auswahl konkreter Maschinenbefehle und Adressierungsarten
• Register-Allokation: Zwischenergebnisse und ausgewählte Variablen werden auf Register
der Zielmaschine abgebildet.
• Code-Anordnung (instruction scheduling): Ausnutzung der Parallelität bei Pipeline- und
VLIW-Architekturen
• Peephole-Optimierung: Verbesserungen an zufällig aufretenden Befehlsgruppen, wie etwa
Sprungketten oder Laden eines Wertes, der zufällig noch in einem Register steht.
80
Ziel ist stets die Übertragung des Zwischensprachenprogramms in möglichst effizienten Maschinencode. Möglichst effizient“ ist ein weiter Begriff. Die verschiedenen Teilaufgaben der
”
Code-Erzeugung sind ein Tummelplatz NP-vollständiger und härterer Probleme; optimaler“
”
Code wird eigentlich nie erzeugt. Man überlegt sich leicht, dass alle Entscheidungen innerhalb
der obigen vier Telaufgaben von einander abhängen. Als Phasen-Anordnungs-Problem (phase
ordering problem) ist die folgende Erfahrungstatsache bekannt:
Es gibt keine sequentielle Anordnung der o.g. Aufgaben der Codeerzeugung, die es
erlaubt, optimalen Code zu produzieren.
Von daher scheint es notwendig, alle Teilaufgaben verzahnt auszuführen, was aber zu einer
sehr komplizierten Codeerzeuger-Struktur führt. In der Tat liegt es häufig an den Back-ends,
wenn neue Compiler erst nach Jahren fehlerfreien Code erzeugen. Eine in der Praxis häufige
Phasenreihenfolge ist: globale Registerallokation — Code-Auswahl — Code-Anordnung mit
lokaler Registerallokation — Peephole-Optimierung.
Wir werden uns auf die Phase der Code-Selektion konzentrieren und spezielle Compilertechniken und die anderen Aufgaben nur kurz ansprechen. Die Code-Selektion ist der Kern der
Codeerzeugung; hier wird festgelegt, wie die abstrakte Maschine, die IL2 zugrundeliegt, auf die
reale Zielmaschine abgebildet wird. Es muss sichergestellt werden, dass alle IL2-Programme
übersetzt werden können, dass unter allen Umständen korrekter Coder erzeugt wird, und dass
zumindest im Prinzip die in der jeweiligen Situation besten Codierungen gefunden werden
können.
Der Rest des Codeerzeugungs-Kapitels ist aufgebaut wie folgt:
• Vorstellung des Befehlsvorrats der Zielmaschine (6.2),
• Ergänzung der Zielmaschine aus der Sicht des Codeerzeugers (6.3),
• Spezifikation der Code-Auswahl durch Ableitung (6.4),
• Implementierung der Code-Auswahl (6.6) durch Syntaxanalyse für Baum-Grammatiken
(6.5),
• lokale Registerallokation und Code-Ausgabe (6.7).
6.2
Die Zielmaschine TM
Unsere Zielmaschine TM ist ein einfacher CISC-Prozessor, angelehnt an die PDP11, Mutter der
VAX-Familie und Vorbild vieler späterer CISC-Rechner. Die Beschreibung erfolgt in Anlehnung
an die Maschinenbeschreibungssprache ISP [BN71].
Register und Speicher
M[0 : 216 − 1] < 15 : 0 >
R[0 : 7] < 15 : 0 >
CC< 0 : 0 >
SP := R[6]
PC := R[7]
216 Speicherworte der Länge 16 bit
8 Register
Condition-Code-Indikator
Hardware-Kellerpegel
Befehlszähler
81
Adressierungsarten, d.h. zulässige Operanden in Befehlen
Reg(i):
Regdef(i):
Bdisp(i,d):
Bdispdef(i,d):
Imm(d):
Preinc(i):
Postdec(i):
Label(l):
R[i]
Register
M [ R[i] ]
Speicher mit Adresse im Register
M [ R[i] + d ],
Basisadresse + Relativadresse
15
15
d ∈ −2 · · · 2 − 1
M [ M [ R[i] + d ] ]
Bdisp mit Indirektion
15
15
Konstante, die im Befehlswort gespeichert wird
d ∈ −2 · · · 2 − 1
R[i] := R[i] + 1 ; M [ R[i ] ] preincrement ( push“)
”
M [ R[i] ]; R[i] := R[i] − 1
postdecrement ( pop“)
”
<l>
symbolische Marke, wird vom Assembler ersetzt
Die Adressierungsarten Preinc und Postdec nutzen das angegebene Register als Kellerpegel, der
damit immer auf das oberste gekellerte Element zeigt. Der Keller wächst also von den niedrigen
zu den höheren Adressen hin. Bei vielen Prozessoren wird der Hardware-unterstützte Systemkeller in der umgekehrten Richtung abgelegt; dementsprechend werden dann die komplementären
Adressierungsarten Predec und Postinc verwendet.
Operandenklassen
Bei einem gegebenen Befehl sind oft nicht alle Adressierungsarten für die Operanden zulässig
(technisch bedingt) oder sinnvoll (logisch bedingt). Ein Beispiel für den ersten Fall ist, wenn
gewisse Befehle nur auf Registern, nicht aber auf Operanden im Speicher operieren können. Ein
Beispiel für den zweiten Fall ist eine Konstante, die natürlich nicht Zieloperand eines Befehls
sein kann. So ist für jeden Operanden eines Befehls die Operandenklasse, d.h. die Menge der hier
zulässigen Adressierungsarten definiert. Ein Befehlsvorrat heißt orthogonal, wenn man dabei
mit insgesamt wenigen verschiedenen Operandenklassen auskommt. Orthogonalität bedeutet
leichteres Spiel für die Codeerzeugung.
Wir fassen die Adressmodi von TM zu vier Operandenklassen zusammen:
sop
op
target
ea
=
=
=
=
{Reg, Regdef, Bdisp, Bdispdef, Preinc}
{Reg, Regdef, Bdisp, Bdispdef, Postdec, Imm}
ea ∪ {Label}
{Regdef, Bdisp, Bdispdef}
schreibbare Operanden
lesbare Operanden
effektive Adress-Operanden
Sprungziele
Befehle
Unsere TM ist eine zwei-Adress-Maschine — bei zweistelligen Operationen wird einer der beiden Operanden mit dem Ergebnis überschrieben. Der überschriebene Operand wird im Befehl
als rechter Operand notiert, semantisch ist er aber der linke Operand der durch den Befehl
implementierten mathematischen Operation. In der Beschreibung der Befehle geben wir als
82
Operanden die zulässigen Operandenklassen an:
Befehl
MOV op sop
MEA ea sop
ADD op sop
SUB op sop
MUL op sop
DIV op sop
Bedeutung
Kommentar
sop := op
sop :=<effektive Adresse von ea>
sop := sop + op
Integer Arithmetik
sop := sop − op
sop := sop * op
sop := sop / op
CGT op2 op1
CGEQ op2 op1
CEQ op2 op1
CNE op2 op1
CLT op2 op1
CLEQ op2 op1
CC
CC
CC
CC
CC
CC
JT target
JF target
JMP target
JSR target
if CC then PC := target
if ¬ CC then PC := target
PC := target
SP := SP + 1;
M[ SP ] := PC;
PC := target
PC := M[ SP ] ; SP := SP − 1
RET
:=
:=
:=
:=
:=
:=
op1
op1
op1
op1
op1
op1
> op2
≥ op2
= op2
6= op2
< op2
≤ op2
Vergleiche
Programmverzweigungen
Jump SubRoutine
Speicherplatzbedarf für Code:
Der genaue Aufbau eines Befehlswortes im Speicher — welche Bits enthalten den Befehlscode,
wie sind Adressierungsarten repräsentiert, etc. — interessiert uns hier nicht. Allerdings wird
die Länge des erzeugten Codes oft als ein Optimierungsziel gesehen, und wir legen daher den
Speicherbedarf der TM-Befehle fest:
Befehl (Opcode + Adressmodi)
zusätzlich pro Operand
reg(i), regdef(i), preinc(i), postdec(i):
bdisp(i,d), bdispdef(i,d), imm(α), label:
1 Wort
0 Wörter
1 Wort
Also brauchen Befehle, deren Operanden Register sind, weniger Speicher (und Zeit).
Beispiele:
MOV Imm(880) Bdisp(5,100)
RET
MOV Reg(1) Preinc(6)
3 Wörter
1 Wort
1 Wort
Abarbeitung der Operanden von 2-Adressbefehlen
OP aa2 aa1 wird abgearbeitet in der Reihenfolge:
(1) werte aa2 aus und hole Wert, falls nötig
83
(2) werte aa1 aus und hole Wert, falls nötig
(3) führe Operation OP aus
(4) Speichere Resultat an die in (2) bestimmte Zielzelle, falls nötig
Beispiele:
ADD Postdec(6) Regdef(6)
CLT Postdec(6) Postdec(6)
Addition auf Keller
beide Operanden vom Keller gelöscht
Auch andere Varianten kommen vor. Zum Beispiel könnten auch beide Operanden parallel
beschafft werden, was aber besondere Anforderungen an die Ausstattung der CPU stellt. Gelgentlich ist die Abarbeitungsreihenfolge
• versehentlich oder auch bewusst vom Hersteller nicht spezifiziert und
• unterschiedlich innerhalb einer Prozessorfamilie.
Deshalb sollten Befehle wie MOV Postdec(5) Reg(5) nicht erzeugt werden, weil das Resultat
von der Abarbeitungsreihenfolge abhängt. (Übung: Was sind die möglichen Ergebnisse?) Beim
Befehl ADD x y fordern wir wegen dieser Probleme, dass die Adressierungsart y, die für Quellund Zieloperand benutzt wird, keine Seiteneffekte hat. Preinc/Postdec werden deshalb hier
ausgeschlossen.
Zwei Beispiele für Codeauswahl
(1) R[0] := R[1] + 23
(1a) MOV (Reg 1) (Reg 0)
ADD (Imm 23) (Reg 0)
Hier wird die Zielzelle zugleich als Hilfszelle benutzt. Diese Möglichkeit beruht auf der
Beobachtung, dass der alte Wert von R[0] im Ausdruck (R[1] + 23) nicht gebraucht wird.
(1b) MEA (Bdisp 1 23) (Reg 0)
Die bestmögliche Codierung. In speziellen Situationen kann MEA einen 3-Adress-Befehl
ersetzen.
(2) M[333] := M [ M [R[5] + 2] + 200]
MOV (Imm 333) (Reg 0)
MOV (Bdisp 5 2) (Reg 1)
MOV (Bdisp 1 200) (Regdef 0)
Hier werden (Reg 0) und (Reg 1) als Hilfszellen benutzt, um die Speicheroperanden adressierbar zu machen. Während eine absolute“ Adressierung wie M[333] in unserer Zielma”
schine TM nicht vorkommt, kann man beim Quelloperanden an eine globale Variable der
Prozedurschachtelungstiefe 2, Relativadresse 200 denken — vorausgesetzt, R[5] enthält
die aktuelle Basisadresse.
84
6.3
6.3.1
Die Zielsprache TL des Codeerzeugers
Verfeinerung der Zielsprache
Im vorigen Abschnitt haben wir die Zielmaschine so beschrieben, wie dies z.B. im Manual des
Herstellers geschieht. Aus der Sicht des Übersetzers ist eine verfeinerte Sicht der Zielmaschine
erforderlich. Dabei können Spezialfälle von Befehlen in getrennte Befehle aufgespalten werden,
oder es werden Befehlssequenzen zu einem neuen Befehl zusammengefasst. In der letzten Phase
der Codeerzeugung, der Codeausgabe, werden diese künstlichen“ Befehle dann wieder auf die
”
wirklichen“ Maschinenbefehle zurückgeführt.
”
In unserem Fall betreffen die Modifikationen die Berechnung von Sprungadressen und den
Umgang mit Hilfszellen für Zwischenergebnisse.
Sprungbefehle verwenden symbolische Marken, die bereits bei der Übersetzung nach IL1 erzeugt werden. Einzige Ausnahme ist der Aufruf formaler Parameterprozeduren, bei dem die
Zieladresse dem aktuellen Parameter (Prozedurobjekt) aus dem Keller entnommen wird. Wir
führen für diese Situation einen eigenen Befehl JSRI (Jump to SubRoutine Indirect) ein und
vereinfachen die Operandenklassen der Sprungbefehle:
JT label
JF label
JMP label
JSR label
JSRI ea
Adressierungsarten und Befehle können Ausdrücke sehr einfacher Bauart berechnen. Das IL2Programm kann dagegen beliebig komplexe Ausdrücke enthalten. Aufgabe der Code-Auswahl
ist es gerade, diese komplexen Ausdrücke entsprechend zu zerlegen. Dabei fallen Zwischenergebnisse an, die in Hilfszellen (temporaries) abgelegt werden m üssen. Diese Hilfszellen (denen
keine Variablen des Quellprogramms entsprechen) werden vom Code-Selektor eingesetzt und
verwaltet. Eine radikale Lösung ist die in Kapitel 3 benutzte Technik, bei der alle Hilfszellen
auf einem Keller verwaltet werden. Hat die Zielmaschine dagegen Register, möchte man diese
verwenden. Ein Problem ist die Anzahl: In den meisten Situationen kommt man mit relativ wenigen Hilfszellen aus — andererseits können Ausdrücke im Prinzip beliebig groß sein und daher
mehr Hilfszellen benötigen, als dem Code-Selektor zur Verfügung stehen ( Register-Trauma“).
”
Dann muss doch auf die Keller-Technik zurückgegriffen werden.
Die Abbildung der Hilfszellen auf konkrete Register verschieben wir zunächst und führen eine
weitere Adressierungsart ein:
Tmp(i,x): Hilfszelle mit Nr. i und Zwischenergebnis x.
Das Besondere dieser Adressierungsart ist, dass diese Zellen nicht als Zieloperand der Befehle
auftreten, sondern der Ausdruck, der das Zwischenergebnis berechnet, selbst als Argument
der Adressierungsart geschrieben wird. Von allen Befehlen (z.B. ADD, MEA) bildet man nun
Varianten (z.B. Tadd, Tmea), die ihr Resultat als Zwischenergebnis liefern. Ebenso führen wir
Varianten von Adressierungsarten ein, die Zwischenergebnisse als Teiladressen benutzen.
6.3.2
Repräsentation der Zielsprache TL
Bevor wir unsere Zielsprachen-Signatur als Datentyp in Haskell angeben, noch ein Wort zur
Implementierung der Operandenklassen. Konzeptionell gesehen sind Operandenklassen Mengen
85
von Adressierungsarten, wären also als Vereinigungstyp zu realisieren. Dies geht in Haskell nur
unter Einführung zusätzlicher Konstruktoren, etwa
data Sop = Sop1 Reg | Sop2 Regdef | Sop3 Bdisp ...
Die Vielzahl solcher Projektionen macht Zielprogramme sehr unübersichtlich. Wir implementieren daher alle Operandenklassen durch den gleichen Datentyp und müssen später darauf
achten, dass der Code-Selektor nicht illegale Befehle wie MOV Preinc(6) Imm(9) oder MEA
Reg(0) Imm(9) erzeugt.
Zur Verkürzung der Beispiele behalten wir ab sofort nur noch den Additionsbefehl als Stellvertreter aller arithmetischen Operationen bei. Bei den Vergleichsoperationen beschränken wir
uns auf CLT und CLEQ.
Die Zielsprache TL
>module Kap6 where
>type Label
>type Regno
>type Disp
= Int
= Int
= Int
>data Tprog =
Tprog [Instr] deriving Show
>data Instr =
MOV Op Sop |
>
MEA Ea Sop
|
>
ADD Op Sop
|
>
JT
Label Cc
|
>
JF
Label Cc
|
>
JMP Label
|
>
JSRI Ea
|
>
JSR Label
|
>
RET
|
>
MovLab Label Sop |
>
DefLab Label
deriving Show
>data Cc
>
= CLT Op Op |
CLEQ Op Op
deriving Show
Befehlsvarianten, die Zwischenergebnisse berechnen:
>data Ir =
>
>
Tmov Op |
Tmea Ea |
Tadd Op Trg
deriving Show
Die Operandenklasse Opnds wird eingeführt als Klasse aller Adressierungsarten. Die wirklichen
Operandenklassen der Zielmaschine werden als Typsynonyme erklärt.
>type
>type
>type
>type
Op
Sop
Trg
Ea
=
=
=
=
Opnds
Opnds
Opnds
Opnds
-----
ausgenommen
ausgenommen
nur Tmp und
ausgenommen
>data Opnds = Imm Int |
>
Reg Regno |
>
Regdef Regno |
>
Bdisp Regno Disp |
Preinc
Imm, Postdec,
Reg
Imm, Reg, Tmp, Tmpdef, Postdec, Preinc
Tmp Regno Ir |
Tmpdef Regno Ir |
TBdisp Regno Ir Disp |
86
>
>
>
Bdispdef Regno Disp |
Preinc |
Postdec
TBdispdef Regno Ir Disp |
deriving Show
Die hier eingeführte Zielsprache weicht in verschiedener Hinsicht von der wirklichen“ Zielma”
schine ab:
• Marken sind noch symbolische Markennummern und werden erst in einer späteren Assemblerphase ersetzt.
• Die Repräsentation erlaubt Operanden an gewissen Positionen, die dort nicht adressierbar
sind.
• Preinc/Postdec soll nur im Zusammenhang mit dem Systemkellerpegel benutzt werden.
Daher erhalten diese Konstruktoren keine Registernummer als Argument.
• Die Tatsache, dass die TM-Befehle keine 3-Adress-Befehle sind, ist im Falle Tadd unberücksichtigt. Dies wird bei der Registerallokation zu beachten sein.
Die Programmbeispiele aus Abschnitt 6.1 sehen als TL-Ausdrücke nun folgendermaßen aus:
(1) R[0] := R[1] + 23
(1a) [ MOV (Reg 1) (Reg 0), ADD (Imm 23) (Reg 0) ] oder, wenn wir (Reg 0) mit (Tmp
0) identifizieren,
[ ADD (Imm 23) (Tmp 0 (Tmov (Reg 1))) ]
(1b) [ MEA (Bdisp 1 23) (Reg 0) ]
(2) M[333] := M [ M [R[5] + 2] + 200]
[ MOV (TBdisp 1 (Tmov (Bdisp 5 2)) 200) (Tmpdef 0 (Tmov (Imm 333))) ]
Durch die Verwendung von TBdisp, Tmov und Tmpdef im Zusammenhang mit den Hilfszellen hat dieses TL-Programm (wie auch die zweite Variante von (1a)) eine dem zugehörigen IL-Programm ähnliche Baumstruktur. In dieser Form werden TL-Programme
zunächst erzeugt. Die Phase der Code-Ausgabe erzeugt daraus eine Liste flacher“ Be”
fehle, in diesem Fall
[ MOV (Imm 333) (Reg 0), MOV (Bdisp 5 2) (Reg 1), MOV (Bdisp 1 200) (Regdef
0)]
6.4
6.4.1
Spezifikation des Code-Selektors durch Ableitung
Ableitung von TL nach IL2
Nun wird der Zusammenhang zwischen Zwischen– und Zielsprache hergestellt. Jeder IL2Ausdruck bietet viele Möglichkeiten, ihn zu übersetzen; hier geht es zunächst darum, alle diese
Möglichkeiten zu erfassen. Am übersichtlichsten ist es, für jedes Zielsprachenkonstrukt anzugeben, was ihm in der Zwischensprache entspricht. Formal gesagt, geben wir eine Ableitung
cs: TL -> IL2 an. Analog zur Notationsableitung im Kapitel Syntaxanalyse ist dies eine Abbildung in die falsche Richtung“, und die Aufgabe der Code-Selektion ist es, eine Umkehrung
”
87
dieser Ableitung zu konstruieren. Natürlich ist die Ableitung cs nicht injektiv, was die Tatsache widerspiegelt, dass einem IL2-Programm viele TL-Programme entsprechen und der CodeSelektor — möglichst geschickt — auswählen muss. Dagegen verlangen wir, dass die Ableitung
surjektiv ist, da es sonst IL-Programme gäbe, für die unser so spezifizierter Code-Selektor keine
TL-Programme finden kann.
Wir legen zunächst fest, welche Funktionen die Register von TM für die Implementierung
von IL2 übernehmen sollen. IL2 sieht drei Register BA, HR und TOP in unterschiedlichen
Funktionen vor. Die Register von TM werden eingesetzt wie folgt:
R[7] ist der Befehlszähler von TM und steht für Programmdaten nicht zur Verfügung;
R[6] ist (hardware-mäßig) der Kellerpegel und wird mit TOP identifiziert. Die TL-Operationen
preinc/postdec beziehen sich immer auf R[6], ebenso wie sich die IL-Operationen Push,
Pop auf TOP beziehen;
R[5] wird als Register BR festgelegt;
R[0] - R[4] stehen dem Code-Selektor als Hilfszellen für Zwischenergebnisse zur Verfügung;
R[4] wird außerdem als Register HR benutzt.
Die Ableitung cs: TL -> IL2
Wir wiederholen die Repräsentation von IL2:
>data
>data
>
>
>
>
>
>
>data
>
>
>data
>
>
>data
>
Iprog2 = Iprog2 [Stat2]
Stat2 = Ass Var2 Expr2 |
Jump Label
| Cjump Bool Label Cond2 |
DefLabel Label |
Pcall Label
| FPcall Expr2 | Return |
Write2 Expr2
| Read2 Var2
deriving Show
Var2
=
M Expr2 | BR | HR | TOP | Push
deriving (Eq, Show)
Cond2
= Lessop2 Expr2 Expr2 | Leqop2
deriving Show
Expr2
= A Expr2 Expr2 | C Int | D Var2 | Pop | LabExp Label
deriving (Eq, Show)
Expr2 Expr2
---------- Notation von IL2
>showil2 (Iprog2 ss) = "Iprog2:\n" ++ concatMap showinst ss
>showinst:: Stat2 -> String
>showinst i = show i ++ "\n"
Die Ableitung cs besteht aus der folgenden Familie von Funktionen:
88
>cs_prog
>cs_instr
>cs_ir
>cs_cc
>cs_op
>cs_sop
>cs_trg
>cs_ea
>cs_regno
::
::
::
::
::
::
::
::
::
Tprog
Instr
Ir
Cc
Op
Sop
Trg
Ea
Regno
->
->
->
->
->
->
->
->
->
>cs_prog (Tprog is)
>cs_instr (MOV s t)
>cs_instr (MEA s t)
>cs_instr (ADD s t)
>
| noSideEff t
>cs_instr (JT l b)
>cs_instr (JF l b)
>cs_instr (JMP l)
>cs_instr (JSRI a)
>cs_instr (JSR l)
>cs_instr RET
>cs_instr (DefLab l)
Iprog2
Stat2
Expr2
Cond2
Expr2
Var2
Expr2
Expr2
Var2
= Iprog2 (map cs_instr is)
= Ass (cs_sop t) (cs_op s)
= Ass (cs_sop t) (cs_ea s)
=
=
=
=
=
=
=
=
Ass (cs_sop t)(A (cs_op t) (cs_op s))
Cjump True l (cs_cc b)
Cjump False l (cs_cc b)
Jump l
FPcall (cs_ea a)
Pcall l
Return
DefLabel l
>cs_cc (CLT s1 s2) = Lessop2 (cs_op s2) (cs_op s1)
>cs_cc (CLEQ s1 s2) = Leqop2 (cs_op s2) (cs_op s1)
>cs_ir (Tmov s)
= cs_op s
>cs_ir (Tmea s)
= cs_ea s
>cs_ir (Tadd s t) = A (cs_trg t) (cs_op s)
>cs_op
>cs_op
>cs_op
>cs_op
>cs_op
>cs_op
>cs_op
>cs_op
>cs_op
>cs_op
(Imm n)
(Reg r)
(Regdef r)
(Bdisp r d)
(Bdispdef r d)
Postdec
(Tmp r i)
(Tmpdef r i)
(TBdisp r i d)
(TBdispdef r i d)
=
=
=
=
=
=
=
=
=
=
C n
D (cs_regno r)
D (M (D (cs_regno r)))
D (M (A (D (cs_regno r)) (C d)))
D (M (D (M (A (D (cs_regno r)) (C d)))))
Pop
cs_ir i
D (M (cs_ir i))
D (M (A (cs_ir i) (C d)))
D (M (D (M (A (cs_ir i) (C d)) )))
>cs_trg (Tmp r i) = cs_ir i
>cs_trg (Reg r)
= D (cs_regno r)
>cs_sop
>cs_sop
>cs_sop
>cs_sop
>cs_sop
>cs_sop
>cs_sop
>cs_sop
(Reg r)
=
(Regdef r)
=
(Bdisp r d)
=
(Bdispdef r d)
=
(Tmpdef r i)
=
(TBdisp r i d)
=
(TBdispdef r i d) =
Preinc
=
>cs_ea (Regdef r)
cs_regno r
M (D (cs_regno r))
M (A (D (cs_regno r)) (C d))
M (D (M (A (D (cs_regno r)) (C d))))
M (cs_ir i)
M (A (cs_ir i) (C d))
M (D (M (A (cs_ir i) (C d))))
Push
= D (cs_regno r)
89
>cs_ea
>cs_ea
>cs_ea
>cs_ea
(Bdisp r d)
(Bdispdef r d)
(TBdisp r i d)
(TBdispdef r i d)
=
=
=
=
A (D (cs_regno r)) (C d)
D (M (A (D (cs_regno r)) (C d)))
(A (cs_ir i) (C d))
D (M (A (cs_ir i) (C d)))
>cs_regno 4 = HR
>cs_regno 5 = BR
>cs_regno 6 = TOP
Beispiele
>p1
>p2
>p3
>p4
>p5
>p6
>
>p7
>
=
=
=
=
=
=
ADD (Imm 42) (Reg 5)
MEA (Bdisp 5 42) (Reg 5)
MOV (Tmp 0 (Tadd (Imm 42) (Reg 5))) (Reg 5)
MOV (Tmp 0 (Tmea (Bdisp 5 42)) ) (Reg 5)
MEA (TBdisp 0 (Tmov (Reg 5)) 42) (Tmpdef 0 (Tmov (Imm 33)))
(MOV (Tmp 0 (Tmea (TBdispdef 9 (Tmov (Imm 444)) 4))) (Tmpdef 9
(Tmov (Imm 333))))
= (MOV (Tmp 0 (Tmov (TBdispdef 0 (Tmov (Imm 444)) 4))) (Tmpdef 9
(Tmov (Imm 333))))
Kap6> map cs_instr [p1,p2,p3,p4,p5] =>
[Ass BR (A (D BR) (C 42)),
Ass BR (A (D BR) (C 42)),
Ass BR (A (D BR) (C 42)),
Ass BR (A (D BR) (C 42)),
Ass (M (C 33))(A (D BR) (C 42))]
Kap6> map cs_instr [p6,p7]
[Ass (M (C 333)) (D (M (A (C 444) (C 4)))),
Ass (M (C 333)) (D (M (D (M (A (C 444) (C 4))))))]
Die Ableitung cs erklärt“ die TL-Konstrukte durch ihnen entsprechende IL2-Ausdrücke. Hilfs”
zellen im TL-Programm werden durch cs einfach verschluckt, was der Tatsache entspricht,
dass in IL2 Ausdrücke beliebig verschachtelbar sind. In p5 etwa werden zwei solche Hilfszellen
eingesetzt. Es gilt z.B. für die Zielzelle
cs_op (Tmp_def 0 (Tmov (Imm 33))) = D (M (C 333)).
Die Code-Selektion muss die Notwendigkeit einer Hilfzelle erkennen, dann anders kann hier kein
Code erzeugt werden, da die Zielmaschine keine absolute Adressierung kennt.
Dank der nun erhaltenen Spezifikation können wir formal angeben, was die Aufgabe der CodeSelektion ist:
Definition: Code-Selektion für ip ∈ TIL2
Sei X eine Menge von Variablen, die für die Adressen von Hilfszellen stehen. Der Codeselektor
muss die Notwendigkeit des Einfügens von Hilfszellen erkennen, braucht aber keine konkreten
Adressen für diese Zellen einzusetzen. Seine Aufgabe ist daher:
Gegeben ein IL2-Programm ip ∈ TIL2 , bestimme ein TL-Programm t ∈ TT L (X) mit cs tprog(t)
= ip.
Betrachtet man zusätzlich eine Kostenfunktion cost auf TL-Programmen, so sollte cost(t)
minimal sein unter allen TL-Programmen, die die obige Gleichung erfüllen.
90
6.4.2
Vorgehen zur Lösung von cs prog t = ip
Seien t, t 1, t 2, . . . Variable geeigneter TL-Sorten, und sei
>ip = Iprog2 [(Ass BR (A (D BR) (C 42)))]
(1) Es muss gelten
t = Tprog t 1 und cs stat(t 1) = (Ass BR (Add2 (D BR) (C 42)))
(2) t 1 = ADD t 2 t 3 und
cs sop t 3 = BR
cs op t 3 = (D BR)
cs op t 2 = (C 42)
(2b) t 1 = MEA t 4 t 3 und
cs sop t 3 = BR
cs ea t 4 = A (D BR) (C 42)
(2c) t 1 = MOV t 6 t 3
cs sop t 3 = BR
cs op t 6 = A (D BR) (C 42)
(3) In (2a) – (2c) muss gelten
t 3 = Reg t 7 und cs regno (t 7) = BR also
t 3 = Reg 5
(4a) Für t 3 = Reg 5 gilt cs op (Reg 5) = D BR.
t 2 = Imm (42)
Erste Lösung: t = ADD (Imm 42) (Reg 5)
(4b) t 4 = Bdisp t 7 t 9
t 7 = Reg 5 (nach 3)
t 9 = 42
Zweite Lösung: t = MEA (Bdisp 5 42) (Reg 5)
(4c) t 6 = Tmp t 10 t 11
Keine Bedingung über t 10!
cs ir t 11 = A (D BR) (C 42)
Dies führt zu weiteren Lösungen
t = MOV (Tmp t 10, (Tadd (Imm 42) (Reg 5))) (Reg 5)
t = MOV (Tmp t 10, (Tmea (Bdisp 5 42) (Reg 5))) (Reg 5)
u.v.a
Daraus ergibt sich die folgende Lösungsidee:
(1) Man sucht eine Überdeckung des IL-Programms mit den Mustern, die als rechte Seiten
von cs auftreten.
(2) Welche Muster ineinander eingesetzt werden dürfen, ist durch die TL-Sorten der Variablen
in den cs-Gleichungen festgelegt.
91
(3) Wir ordnen jeder cs-Gleichung, z.B.
cs instr (MEA a t) = Ass (cs sop t) (cs ea a)
eine Produktion in einer Baumgrammatik Gcs zu:
instr ->
Ass
@
sop
@
@
ea
(4) Wird eine Gcs -Ableitung für ein IL2-Programm gefunden, so wird aus TL-Operatoren
entsprechend den angewandten Produktionen das TL-Programm zusammengesetzt.
Wir haben also nun ein Syntaxanalyseproblem für Baumgrammatiken zu lösen.
6.5
Syntaxanalyse für Baumgrammatiken
Formale Sprachen sind üblicherweise als Sprachen von Zeichenreihen definiert, also als L ⊆ A∗
bei gegebenem Alphabet A. Analog dazu definiert man Baumsprachen in der älteren Literatur
über einem Operator-Alphabet, wobei den Operatoren eine feste Stelligkeit zugeordnet wird,
die die Anzahl der Unterbäume angibt. Wir kennen bereits den Signaturbegriff, der Mengen von
Termen definiert, und definieren daher Baumsprachen als Teilmengen von TΣ für eine gegebene
Signatur Σ. Der Vorteil diese Sichtweise ist, wie wir sehen werden, dass wir Ableitungen in
Baumgrammatiken ihrerseits als Terme einer (anderen) Signatur darstellen können.
Definition Eine reguläre Baumgrammatik G über einer Signatur Q ist ein Tripel (S, P, Ax)
mit
S
Menge von Nichtterminalsymbolen
P
Menge von Produktionen der Form N → t mit N ∈ S und t ∈ TQ (S)
Ax ∈ S das Axiom
Für jedes N ∈ S gibt es eine Sorte sN ∈ SQ mit L(N) ⊆ TQ :: sN .
Den Ableitungsbegriff N →∗ t und die Sprache L(G) definiert man analog zu Grammatiken über
Zeichenreihen. Als Sprachklasse sind die so definierten regulären Baumsprachen den regulären
Sprachen der Chomsky-Hierarchie verwandt. Für sie ist z.B. L1 ⊆ L2 entscheidbar, was für
kontextfreie Sprachen bekanntlich nicht gilt. Kontextfreie Baumgrammatiken sind erheblich
komplizierter zu definieren (warum?), und werden selten benutzt.
Ableitungen in String-Grammatiken stellt man gerne als Bäume dar. Es ist naheliegend, Ableitungen in Baumgrammatiken selbst wieder als Terme einer sogenannten Zielsignatur darzustellen. Der Baumparser liest dann Bäume der Quellsignatur Q und liefert ihre Ableitungen als
Bäume der Zielsignatur Z.
Definition Einer Baumgrammatik G über Q ordnen wir eine Signatur Z = (S, FZ ) zu:
Für jede Produktion p : N → t(N1 , . . . , Nk ), k ≥ 0 enthält FZ einen Operator fp : N1 , . . . , Nk →
N.
Eine G-Ableitung aus N ist damit eindeutig repräsentiert durch einen Term t ∈ TZ :: N.
Wir nennen Q und Z Quell- und Zielsignatur des Syntaxanalyseproblems für G.
92
Beispiel: Ableitungen, Mehrdeutigkeit, Sackgassen
> data E =
F E E
| H E
| A
> data U =
> data L =
> data R =
P1 L R | P2 U R | P3 L
P4
| P5 L L
P6
| P7 U
deriving Show
-- signature Q
deriving Show
deriving Show
deriving Show
-- signature Z
Grammatik G = ({u,l,r},P,u) mit Produktionen P wie folgt:
P1 - P3: u ->
|
F
H
|
F
JJ
JJ
r
u
l
A
|
JJ
P6 - P7: r ->
A
u
hat die Ableitungen
P1
P2
%@
% @
%@
% @
H
F
P5
A
P4
P3
P4 P3
P5
P6
%@
% @
A
q2 =
P7
%@
% @
%@
% @
A
l
|
%@
% @
H
l
F
l
F
H
r
P4 - P5: l ->
q1 =
H
P4 P4
P4
hat keine Ableitungen. Damit gilt
F
%@
% @
H
H
H
F
q2 ∈
/ L(G).
%@
% @
A A
A
Der Versuch, eine top-down Ableitung für q2 zu finden, zeigt die Existenz von Sackgassen:
93
Die Teilableitung
erzeugt
P1
?
%@
% @
F
%@
% @
P7
H
H
P3
l
F
%@
% @
P5
A
A
%@
% @
P4
die Teilableitung
P4
erzeugt
P2
F
%@
% @
%@
% @
P3
P7
H
H
?
?
l
u
Beide lassen sich nicht fortsetzen zu q2.
Wir entwickeln einen top-down Parser für mehrdeutige Baumgrammatiken. Um einen Baum t
aus einem Nichtterminalsymbol N abzuleiten, betrachten wir die Bäume auf den rechten Seiten
der Produktionen für N. Wir suchen diejenigen, die als Muster an der Wurzel von t passen. Die
Unterbäume von t, die den Variablen (d.h. den Nichtterminalsymbolen) im Muster entsprechen,
müssen dann rekursiv aus diesen abgeleitet werden.
Für jedes N ∈ Sz enthält der Parser eine Funktion p N : sN → [N], so dass gilt:
z ∈ p N(q)
gdw.
z ist Ableitung von q.
Seien alle Produktionen für N gegeben durch N → p1 | . . . |pn , für n ≥ 1. Dann wird p N
definiert durch n + 1 Gleichungen.
Für 1 ≤ i ≤ n und pi = pi (N1 , . . . , Nni ) hat die i. Gleichung die Form
p N(pi (x1 , . . . , xni )) = [fpi (y1 , . . . , yni )|y1 ← p N1 (x1 ), . . . yni ← p Nni (xni )]
++ subcasei,i+1 ++ . . . ++subcasei,n
Im wesentlichen“ ist die i. Gleichung für das Erkennen des i. Musters für N, also pi verantwort”
lich. Die anschließend betrachteten subcasei,j behandeln den Fall, dass pj für i < j ebenfalls
passt, seine“ Gleichung aber wegen der deterministischen Semantik funktionaler Sprachen
”
nicht ausgewählt werden kann.
.
subcasei,j = [fpj y1 . . . ynj | pj (x01 . . . x0nj ) = pi (x1 . . . xni );
y1 ← p N1 x01 ; . . . ; ynj ← p Nnj x0nj ]
.
Die Bedingung (=) vergleicht pj mit q und bindet im Erfolgsfall zugleich die x0 -Variablen an
die entsprechenden Unterbäume von q 1 . Teile von q, die durch pi , aber nicht durch pj überdeckt
werden, werden durch den Vergleich an die x0 -Variablen gebunden. Die Generatoren rufen den
Parser rekursiv für die Unterbäume auf.
1
.
Eine Bedingung der Form pj (x0 ) = pi (x) müssen wir in Haskell durch pj (x0 ) ← [pi (x)] umschreiben.
94
.
Präziser gesprochen berechnet (p1 = p2 ) den most general unifier (MGU) der Terme p1 und
p2 . Damit bezeichnet man die einfachste Substitution, die die Terme p1 und p2 gleich macht.
Durch diese Unifikation ist der allgemeinste Term bestimmt, auf den sowohl p1 als auch p2 als
Muster passen.
Die (n + 1)-te Gleichung für N heißt Sackgassen-Gleichung und dient dem Fall, dass keine
Ableitung N →∗ q existiert. Sie hat die Form
p N = [ ].
Implementierung des Beispiels:
Parser-Funktionen
> p_u :: E -> [U]
> p_l :: E -> [L]
> p_r :: E -> [R]
> p_u (F (H x1) x2) = [P1 y1 y2
| y1 <- p_l x1, y2 <- p_r x2] ++
>
[P2 y1’ y2’ | F x1’ (H x2’) <- [F (H x1) x2],
>
y1’ <- p_u (H x1),
>
y2’ <- p_r(x2’)] ++
>
[]
(Für p1 = F(H x1)x2 und p2 = F x1’ (H x2’) und p3 = H x1’ ist µ = [x1’ <- H x1,
x2 <- H x2’] der mgu von p1 und p2. Dagegen sind p1 und p3 nicht unifizierbar und subcase1,3
ist leer.
Im folgenden lassen wir triviale Bedingungen und leere subcases“ weg und schreiben die ver”
bleibenden Bedingungen nach Haskell um. Die obige Gleichung schreiben wir noch einmal —
vereinfacht — hin.
>
>
>
>
>
>
>
>
>
>
>
>
p_u (F(H x1) x2)
= [P1 y1 y2 | y1 <- p_l x1, y2 <- p_r x2] ++
[P2 y1’ y2’ | H x2’ <- [x2],
y1’ <- p_u (H x1), y2’ <- p_r x2’]
p_u
p_u
p_u
p_l
p_l
p_l
p_r
p_r
=
=
=
=
=
=
=
=
(F x1 (H x2))
(H x)
_
A
(F x1 x2)
_
A
x
[P2 y1 y2
[P3 y | y
[]
[P4]
[P5 y1 y2
[]
[P6]
[P7 y | y
| y1 <- p_u x1, y2 <- p_r x2]
<- p_l x]
| y1 <- p_l x1, y2 <- p_l x2]
<- p_u x]
Für p r haben wir die Sackgassen-Gleichung weggelassen, da sie wegen der zweiten Gleichung
unerreichbar ist.
p u q liefert nun die Liste aller G-Ableitungen aus u für q. Durch die Umordnung der Alternativen in N → p1 | . . . |pn können wir erreichen, dass gewisse ( billigere“) Ableitungen stets vor
”
anderen gefunden werden. Bei der Anordnung der Generatoren achtet man darauf, dass Sackgassen möglichst früh erkannt werden.
95
Das Zyklenproblem
Enthält G zyklische Kettenproduktionen, d.h. für ein N ∈ Sz gilt N →+ N, so gibt es für
jedes t ∈ TQ mit N →+ t unendlich viele Ableitungen von wachsender Länge. Jede reguläre
Baumgrammatik G lässt sich in eine Grammatik G0 umformen, so dass L(G) = L(G0 ) und G0
keine zyklischen Kettenproduktionen enthält. Dies werden wir im Abschnitt 6.5 auch tun.
Arbeitet man aus irgendeinem Grund trotzdem mit der zirkulären Grammatik, so hat dies
Konsequenzen für unsere Parser-Implementierung. Ist vs unendlich, so gilt ja
[(u, v) | u <- us, v <- vs] = [(us!!0, v) | v <- vs]
vs ++ us = vs
Das heißt, dass der Parser gewisse Lösungen verlieren würde. Betrachten wir eine Klausel der
Form
p_u (F (H x1) x2) = [P1 y1 y2
| y1 <- p_l x1, y2 <- p_r x2] ++
[P2 y1’ y2’ | H x2’ <- [x2],
y1’ <- p_u (H x1), y2’ <- p_r x2’]
(1) Für y1 und y2 kann es nun unendlich viele Lösungen geben. Ebenso möglicherweise für
y1’, y2’. Damit p u eine Liste aller Lösungen liefert, müssen wir Diagonalisierung zum
Aufbau der Ergebnislisten verwenden. Wir dürfen auch zwei Listen nicht einfach konkatenieren, sondern müssen ++ durch den +|+ -Operator ersetzen, der abwechselnd Elemente
aus beiden Teillisten entnimmt. Hier die Haskell-Definitionen von diag und +|+.
Alternating List Merge and Diagonalizing Cartesian Product
---------------------------------------------------------module Diag where
> infix +|+
> []
+|+ ys = ys
> (x:xs) +|+ ys = x:(ys +|+ xs)
> diag xs ys = d
>
d xa xr ys =
>
case xr of
>
>
>
>
>
[] xs ys where
zip xa ys ++
[] ->
case ys of []
(y:ys’)
(x:xr’) ->
case ys of []
(y:ys’)
-> []
-> d xa [] ys’
-> []
-> d (x:xa) xr’ (y:ys’)
(2) Bei der Anordnung der Muster sollten die trivialen Muster, die Kettenproduktionen entsprechen, stets als letzte Alternative aufgeführt werden.
(3) Trotz dieser Maßnahmen kann es sein, dass der Parser nicht terminiert. Dieser Fall tritt
ein, wenn durch zyklische Kettenproduktionen Sackgassen von unbeschränkter Länge
möglich sind. Beispiel dazu: Für die Mini-Grammatik z -> A | z mit dem Parser p z A
= P1, p z x = p z x terminiert p z nicht auf der Eingabe B.
Top-down Baumparser sind einfach zu realisieren, aber ihre Laufzeit ist nichtlinear in der Höhe
des Eingabebaumes. Wenn Lösungen zu verschiedenen Startproduktionen konstruiert werden,
96
wird ein Unterbaum u.U. mehrfach abgeleitet. Werkzeuge zur Erstellung von Code-Generatoren
generieren bottom-up Baumparser. Diese haben linearen Aufwand. Allerdings ist die Größe
der Parser und damit der Generierungsaufwand exponentiell in der Anzahl der Produktionen.
Die Erfahrung zeigt aber, dass für die in der Codeerzeugung verwendeten Grammatiken das
exponentielle Wachstum nicht auftritt.
Beispiele:
> q1 = F (H (F A A)) (H A)
> q2 = F (H (H A))
(H (F A A))
Main> p_u q1
[P1 (P5 P4 P4) (P7 (P3 P4)),P2 (P3 (P5 P4 P4)) P6]
Main> p_u q2
[]
6.6
Implementierung des Code-Selektors
Wir haben nun alle Techniken beieinander, um einen Codeselektor systematisch zu entwickeln:
Die Spezifikation des Codeselektors beschreibt bereits die Übersetzung von TL nach IL2, also in
die falsche Richtung, zu der eine Inverse zu konstruieren ist. Aus dieser Spezifikation entwickeln
wir eine Baumgrammatik GT L : Die Sorten der Zielmaschine werden zu den Nichtterminalsymbolen, die IL2-Terme auf den rechten Seiten der Gleichungen werden zu den rechten Seiten
der Produktionen. An ihren Blättern stehen wieder TL-Sorten als Nichtterminale. Weil die
Codeselektor-Spezifikation dem Ableitungsschema2 genügt, gibt es zu jedem TL-Konstruktor
genau eine Regel, und wir können TL als Zielsignatur des Baumparsers ansehen: Jede vom
Baumparser gelieferte Ableitung des IL2-Programms ist ein TL-Programm.
Um die Korrektheit eines so konstruierten Codeselektors müssen wir uns also keine Sorgen
machen. Allerdings ist die Aufgabe dennoch nicht einfach, einerseits wegen der Größe der
Grammatik, andererseits wegen möglicher Zyklen sowie der Frage, welche der produzierten
Übersetzungsvorschläge wir letztlich akzeptieren wollen.
Aus der Codeselektor-Spezifikation erhalten wir die folgende Baumgrammatik GT L :
Tprog -> Iprog2 [ Instr ]
Instr -> Ass Sop Op | Ass Sop Ea | Ass Sop (A Sop Op) |
Cjump True Label Cc | Cjump False Label Cc |
Jump Label | FPcall Ea | Pcall Label
Return | DefLabel Label
Cc ->
Lessop2 Op Op
Ir ->
Op
Op ->
C Int | D Regno | D (M (D Regno)) |
D (M (A (D Regno) (C Disp))) |
D (M (D (M (A (D Regno) (C Disp))))) |
| Ea
| Leqop2 Op Op
| A Trg Op
2
Bedauerlich, aber aufgrund der etablierten Terminologie nicht zu vermeiden ist die zweimalige Benutzung
des Begriffes Ableitung“, einmal für das in Kapitel 3 vorgestellte einfachste Übersetzungsschema, ein andermal
”
für die Herleitung eines Wortes oder Baumes mittels der Produktionen einer Grammatik. Dass man die Ableitung
in einer Baumgrammatik nutzen kann, um die Inverse einer Ableitung im andern Sinne zu finden, hat bei der
Etablierung dieser Begriffe sicher niemand gesehen.
97
Pop |
D (M Ir)
Ir
| A Ir (C Disp)
| D (M (A Ir (C Disp)))
|
Trg ->
D Regno
Sop ->
Regno | M (D Regno) | M (A (D Regno) (C Disp)) |
M (D (M (A (D Regno) (C Disp)))) | Push |
M Ir | M (A Ir (C Disp)) | M (D (M (A Ir (C Disp))))
Ea ->
D Regno | Ir |
A (D Regno) (C Disp) | A Ir (C Disp) |
D (M (A (D r) (C Disp))) | D (M (A Ir (C Disp)))
Regno -> HR
| BR
| Ir
| TOP
Satz: TIL2 ⊆ L(GT L )
Beweis: Übung.
Damit steht fest, dass ein korrekter GT L -Parser für jedes IL2-Programm ein TL-Programm
findet.
6.6.1
Von der Grammatik zum Parser
Beim Übergang von der Ableitung cs zur Grammatik verschwindet die Tatsache, dass ADD ein
2-Adress-Befehl ist. In der Produktion instr -> Ass Sop (A Sop Op) steht das Nichtterminal sop beide Male für den gleichen Operanden, der einmal als Wert, einmal als Zieloperand
benutzt wird. In der Parserklausel für ADD führen wir daher für das Muster Ass t1(A t2 s)
den Wächter t2 == D t1 hinzu. (Für Tadd ist die 2-Adress-Form erst bei der Zuteilung von
Hilfszellen zu berücksichtigen.) Die Bedingung, dass t1 bzw. t2 nicht mit Seiteneffekt adressiert
werden darf, übernehmen wir aus der entsprechenden Zeile von cs instr.
Bei der Anordnung der Klauseln folgen wir einer greedy-Heuristik: Produktionen, die größere
Teile der Eingabe überdecken, werden zuerst versucht, weil man so insgesamt mit weniger
Instruktionen auskommt. Alternativen, die Hilfszellen benutzen, stehen nach denen, die ohne
auskommen. Durch Verwendung diagonalisierender Listenkonstruktionen erreichen wir, dass
Teillösungen etwa in der Reihenfolge aufsteigender Kosten generiert werden.
Alle Alternativen in der ersten Gleichung für p inst müssen den Zieloperanden t1 aus sop
ableiten. Wir führen eine where-Klausel ein, damit dies nicht mehrfach geschieht.
Mit diesen Festlegungen erhalten wir den folgenden Baumparser als Zwischenprodukt, das
später weiterentwickelt wird. Er steht daher nicht in der Codesammlung zu dieser Vorlesung
zur Verfügung. Der Systematik halber stehen an den Stellen, wo Diagonalisierung erforderlich
ist, hier Listenbeschreibungen mit dem Operator (//) anstelle von (|). Später wird dafür dann
diag verwendet.
type Treeparser a b =
p_tprog :: Treeparser
p_instr :: Treeparser
p_cc
:: Treeparser
p_ir
:: Treeparser
p_op
:: Treeparser
p_sop
:: Treeparser
(a -> [b])
Iprog2 Tprog
Stat2 Instr
Cond2 Cc
Expr2 Ir
Expr2 Op
Var2 Sop
98
p_trg
p_ea
p_regno
p_tprog
:: Treeparser
:: Treeparser
:: Treeparser
(Iprog2 ss) =
Expr2 Trg
Expr2 Ea
Var2 Regno
[Tprog is | is <- choices(map p_instr ss)]
p_instr (Ass t1 (A t2 s))
= [ADD u v1 // t2 = D t1; u <- p_op s; v1 <- vs; noSideEff v1 ]
++ [MEA a v // a <- p_ea (A t2 s); v <- vs]
++ [MOV u v // u <- p_op (A t2 s); v <- vs]
where vs = p_sop t1
p_instr (Ass t s)
= [MEA a v // a <- p_ea s; v <- vs]
++ [MOV u v // u <- p_op s; v <- vs]
where vs = p_sop t
p_instr (Cjump True l b) = [JT l c | c <- p_cc b]
p_instr (Cjump False l b) = [JF l c | c <- p_cc b]
p_instr
p_instr
p_instr
p_instr
p_instr
p_instr
(Jump l)
(FPcall a)
(Pcall l)
Return
(DefLabel l)
x = []
=
=
=
=
=
[JMP l]
[JSRI x | x <- p_ea a]
[JSR l]
[RET]
[DefLab l]
p_cc (Lessop2 s2 s1) = [CLT u v // u<- p_op s1; v <- p_op s2]
p_cc (Leqop2 s2 s1) = [CLEQ u v // u<- p_op s1; v <- p_op s2]
p_cc w = []
p_ir (A t s)
= [Tadd u v // v <- p_trg t; u <- p_op s]
++ [Tmea u | u <- p_ea (A t s)]
++ [Tmov u | u <- p_op (A t s)]
p_ir x = [Tmea u | u <- p_ea x]
++[Tmov u | u <- p_op x]
p_op (D(M(D(M(A (D r) (C d))))))
= [Bdispdef u d | u <- p_regno r]
++ [TBdispdef 9 u d | u <- p_ir (D r)]
++ [Tmpdef 0 i | i <- p_ir (D(M(A (D r) (C d))))]
++ [Tmp 0 i | i <- p_ir (D(M(D(M(A (D r) (C d))))))]
++ [Regdef u | u<- p_regno (M(A (D r) (C d)))]
++ [Reg u | u <- p_regno (M(D(M(A (D r) (C d)))))]
leer!
leer!
p_op k (D(M(D(M(A o (C d))))))
= [TBdispdef 9 u d | u <- p_ir o]
++ [Tmpdef 0 i | i<- p_ir (D(M(A o (C d))))]
++ [Tmp 0 i | k=1; i <- p_ir (D(M(D(M(A o (C d))))))]
++ [Regdef u | u<- p_regno (M(A o (C d)))]
leer!
++ [Reg u| u <- p_regno (M(D(M(A o (C d)))))]
leer!
p_op (D(M(A (D r) (C d))))
99
= [Bdisp u d | u <- p_regno r]
++ [TBdisp 0 i d | i <- p_ir (D r)]
++ [Tmpdef 0 i | i <- p_ir (A (D r) (C d))]
++ [Tmp 0 i | i <- p_ir (D(M(A (D r) (C d))))]
++ [Reg u | u <- p_regno (M(A (D r) (C d)))]
leer!
p_op (D(M(A o (C d))))
= [TBdisp 0 i d | i <- p_ir o]
++ [Tmpdef 0 i | i <- p_ir (A o (C d))]
++ [Tmp 0 i | k=1; i <- p_ir (D(M(A o (C d))))]
++ [Reg u | u <- p_regno (M(A o (C d)))]
leer!
p_op (D(M(D r)))
= [Regdef u | u <- p_regno r]
++ [Tmpdef 0 i | i <- p_ir (D r)]
++ [Tmp 0 i | i<- p_ir (D(M(D r)))]
++ [Reg u | u <- p_regno (M(D r))]
leer!
p_op (C n)
= [Imm n]
++[Tmp 0 i | i <- p_ir (C n)]
p_op Pop = [Postdec]
++ [Tmp 0 i | i <- p_ir Pop]
p_op (D(M (A x (C
= [TBdispdef
++ [Tmpdef
++ [Tmp 0i
d))))
0 i d | i <- p_ir x]
0 i | i <- p_ir (A x (C d))]
| i<- p_ir (D(M (A x (C d))))]
p_op (D(M x))
= [Tmpdef 0 i | i <- p_ir x]
++ [Tmp 0 i | i <- p_ir (D(M x))]
p_op (D r)
= [Reg u | u <- p_regno r]
++ [Tmp 0 i | i <- p_ir (D r)]
p_op x = [Tmp 0 i | i <- p_ir x]
p_trg (D r)
= [Reg u | u <- p_regno r]
++ [Tmp 0 i | i <- p_ir (D r)]
p_trg x = [Tmp 0 i | i <- p_ir x]
p_sop (M(D(M(A (D r) (C d)))))
= [Bdispdef u d | u <- p_regno r]
++ [TBdispdef 9 u d | u <- p_ir (D r)]
++ [Tmpdef 9 u | u <- p_ir (D(M(A (D r) (C d))))]
++ [Regdef u | u <- p_regno (M(A (D r) (C d)))]
++ [Reg u | u <- p_regno (M(D(M(A (D r) (C d)))))]
p_sop (M(D(M(A i (C d)))))
= [TBdispdef 9 u d | u <- p_ir i]
100
leer!
leer!
++ [Tmpdef 9 u | u <- p_ir (D(M(A i (C d))))]
++ [Regdef u | u <- p_regno (M(A i (C d)))]
++ [Reg u | u <- p_regno (M(D(M(A i (C d)))))]
p_sop (M (D r))
= [Regdef u | u <- p_regno r]
++ [Tmpdef 9 u | u <- p_ir (D r)]
++ [Reg u | u <- p_regno (M (D r))]leer!
p_sop (M(A (Dr) (C d)))
= [Bdisp u d | u <- p_regno r]
++ [TBdisp 9 u d | u <- p_ir (D r)]
++ [Tmpdef 9 u | u <- p_ir (A (Dr) (C d))]
++ [Reg u | u <- p_regno (M(A (Dr) (C d)))]
p_sop (M(A i (C d)))
= [TBdisp 9 u d | u <- p_ir i]
++ [Tmpdef 9 u | u <- p_ir (A i (C d))]
++ [Reg u | u <- p_regno (M(A i (C d)))]
leer!
leer!
leer!
leer!
p_sop (M i)
= [Tmpdef 9 u | u <- p_ir i]
p_sop Push
= [Preinc]
++ [Reg u| u <- p_regno Push]
p_sop r = [Reg u | u <- p_regno r]
leer!
|| schliesst Sackgassenklausel mit ein
p_ea (D (M (A (D r) (C d))))
= [Bdispdef u d| u <-p_regno r]
++ [TBdispdef 9 i d | i <- p_ir (D r)]
++ [Regdef u | u <- p_regno (M (A (D r) (C d)))]
p_ea (D (M (A i (C d))))
= [TBdispdef 9 u d | u <- p_ir i]
p_ea (A (D r) (C d))
= [Bdisp u d | u <- p_regno r]
++ [TBdisp 9 i d | i <- p_ir (D r)]
p_ea (A i (C d))
= [TBdisp 9 u d | u <- p_ir i]
p_ea (D(M (D r)))
= [Regdef u | u <- p_regno r]
p_ea w = []
p_regno
p_regno
p_regno
p_regno
HR
BR
TOP
w
=
=
=
=
leer!
[4]
[5]
[6]
[]
Obwohl noch nicht fertig, lässt sich dieser Parser schon ausprobieren. Daran erkennt man einiges, das noch verbessert werden muss.
Beispiele
>test k = take k . p_instr
i1 = Ass BR (A (D HR) (C 99))
i2 = Ass (M (A (D BR) (C 333))) (A (D(M (A (D BR) (C 333)))) (C 42))
101
i3 = Ass BR (D BR)
i4 = Ass BR (A (C 44) (C 55))
i5 = Ass (M (C 333)) (D (M (A (C 444) (C 4))))
main> test 5 i1
First 5 solutions:
MEA (Bdisp 4 99) (Reg 5)
MEA (TBdisp 9 (Tmov (Reg
MEA (TBdisp 9 (Tmov (Tmp
MEA (TBdisp 9 (Tmov (Tmp
MEA (TBdisp 9 (Tmov (Tmp
(Reg 5)
main> test 5 i2
First 5 solutions:
ADD (Imm 42) (Bdisp 5
ADD (Tmp 0 (Tmov (Imm
ADD (Tmp 0 (Tmov (Tmp
ADD (Tmp 0 (Tmov (Tmp
ADD (Tmp 0 (Tmov (Tmp
(Bdisp 5 333)
main> test 5 i3
First 5 solutions:
MOV (Reg 5) (Reg 5)
MOV (Tmp 0 (Tmov (Reg
MOV (Tmp 0 (Tmov (Tmp
MOV (Tmp 0 (Tmov (Tmp
MOV (Tmp 0 (Tmov (Tmp
(Reg 5)
4)) 99)
0 (Tmov
0 (Tmov
0 (Tmov
(Reg
(Reg
(Tmp
(Tmp
333)
42))) (Bdisp
0 (Tmov (Imm
0 (Tmov (Tmp
0 (Tmov (Tmp
5)
4)))) 99) (Reg 5)
0 (Tmov (Reg 4)))))) 99) (Reg 5)
0 (Tmov (Tmp 0 (Tmov (Reg 4)))))))) 99)
5 333)
42))))) (Bdisp 5 333)
0 (Tmov (Imm 42))))))) (Bdisp 5 333)
0 (Tmov (Tmp 0 (Tmov (Imm 42)))))))))
5))) (Reg 5)
0 (Tmov (Reg 5))))) (Reg 5)
0 (Tmov (Tmp 0 (Tmov (Reg 5))))))) (Reg 5)
0 (Tmov (Tmp 0 (Tmov (Tmp 0 (Tmov (Reg 5)))))))))
main> test 5 i4
First 5 solutions:
MEA (TBdisp 9 (Tmov (Imm
MEA (TBdisp 9 (Tmov (Tmp
MEA (TBdisp 9 (Tmov (Tmp
MEA (TBdisp 9 (Tmov (Tmp
(Reg 5)
MEA (TBdisp 9 (Tmov (Tmp
(Imm44)))))))))) 55)
44)) 55) (Reg 5)
0 (Tmov (Imm 44)))) 55) (Reg 5)
0 (Tmov (Tmp 0 (Tmov (Imm 44)))))) 55) (Reg 5)
0 (Tmov (Tmp 0 (Tmov (Tmp 0 (Tmov (Imm 44)))))))) 55)
0 (Tmov (Tmp 0 (Tmov (Tmp 0 (Tmov (Tmp 0 (Tmov
(Reg 5)
Die Testbeispiele zeigen, dass beliebig viele uninteressante Lösungen existieren, während manche kostengünstige Lösungen spät (oder nie) gefunden werden, wie zum Beispiel im Falle von
i1 die Lösung MOV (Tmp 0 (Tadd (Imm 99) (Reg4)))(Reg 5).
6.6.2
Tuning des Parsers
Der Parser wird in dreierlei Hinsicht modifiziert – (1) der Eliminierung von trivialen Sackgassen,
(2) der Umordnung von Lösungen und Eliminierung von uninteressanten Lösungen und (3) dem
Aufbrechen von Zyklen.
(1) Triviale Sackgassen sind subcases, von denen sich unabhängig von der Eingabe zeigen
lässt, dass sie leer sind, z.B.
[Regdef u | u <- p regno (M (A o ((d)))]
102
(2) Durch Einführen der alternierenden Konkatenation (+|+) anstelle von (++) sorgen wir
dafür, dass kostengünstige Lösungen früher konstruiert werden. Daneben gibt es eine
Menge von Lösungen, die wir ganz ausschließen möchten, weil stets eine bessere existiert.
(2a) Es ist nutzlos und erhöht die Kosten, wenn Zwischenergebnisse von einer Hilfszelle in die
andere geladen werden. Beweis:
cs op (Tmp (i (Tmov (Tmp j x))) = cs ir x = cs op (Tmp j x)
(2b) MOV und MEA haben gleiche Befehlskosten. Es kann stets MEA Regdef i x durch MOV Reg
i ersetzt werden. Beweis:
cs_instr (MEA (Regdef i) x) =
=
cs_instr (MOV (Reg i) x)
=
=
Ass
Ass
Ass
Ass
(cs_sop
(cs_sop
(cs_sop
(cs_sop
x)
x)
x)
x)
(cs_ea (Regdef i))
D (cs_regno i)
(cs_op (Reg i))
D (cs_regno i)
Ebenso für die Adressierungsarten Bdispdef und Bdisp, Tmpdef und Tmp, TBdispdef
und TBdisp und analog für die Befehle Tmov und Tmea.
(3) Die Grammatik GT L enthält zyklische Kettenproduktionen. Wir modifizieren sie zu G0T L
so, dass
– L(G0T L ) = L(GT L )
– G0T L zyklenfrei ist, und
– G0T L weiterhin alle interessanten“ Ableitungen von GT L erlaubt.
”
Kettenproduktionen in GT L :
@
R @
Op
Trg
6
Pfeile ohne Anfangs/Endpunkt stehen für
Produktionen, die keine Kettenproduktionen sind.
=
?
Ir
.....
.... R
..
.
- .Ea
. ..
....
.... .R
.
Wir betrachten die elementaren Zyklen:
Op -> Ir -> Op entspricht Tmp Ir (Tmov x :: Op) :: Op.
Ein ohnehin adressierbarer Operand wird in eine Hilfszelle geladen. (Verallgemeinerung
von (2a)).
Ir -> Op -> Ir entspricht Tmov (Tmp r x :: Ir) :: Ir.
Ein Zwischenergebnis wird gespeichert, obwohl es im Kontext verwendet werden kann.
Ir -> Ea -> Ir entspricht Tmea (Tmp r x :: Ir) :: Ir.
vgl. oben.
Wir führen für Op und Ea Varianten Op1, Op2 und Ea1, Ea2 ein:
103
q
JJ
^
> Op1 ?
Trg
Op2
A
A
6
A
Ea1
A
HH
j
U
A
9
Ir
Ea2
I
@
@
HH
j
6.6.3
q
Wiederholungen der ehemaligen Zyklen
Op -> Ir -> Op2 und Ea1 -> Ir -> Ea2
sind nun unmöglich. Zudem ist Ea2 nur
noch von Ir aus erreichbar, und gemäß
(2b) können wir seine Ausgänge für
Regdef, Bdispdef, TBdispdef und (sowieso) Tmpdef streichen.
q
Die modifizierte Baumgrammatik
Die Modifikationen ergeben die folgende Grammatik:
Tprog -> Iprog2 [ Instr ]
Instr -> Ass Sop Op_1 | Ass Sop Ea | Ass Sop (A Sop Op_1) |
Cjump True Label Cc | Cjump False Label Cc |
Jump Label| FPcall Ea | Pcall Label
Return | DefLabel Label
Cc ->
Lessop2 Op_1 Op_1| Leqop2 Op_1 Op_1
Ir ->
Op_2 | Ea_2 | A Trg Op_1
Durch Aufspaltung von Op in Op1, Op2 wird der Zyklus op -> Ir -> Op gebrochen: für i =
1, 2:
Op_i -> C Int| D Regno | D (M (D Regno)) |
D (M (A (D Regno) (C Disp))) |
D (M (D (M (A (D Regno) (C Disp))))) |
Pop |
D (M Ir) | A Ir (C Disp)| D (M (A Ir (C Disp)))
Op_1 -> Ir
Trg ->
D Regno| Ir
Sop ->
Regno| M (D Regno)| M (A (D Regno) (C Disp)) |
M (D (M (A (D Regno) (C Disp)))) | Push |
M Ir | M (A Ir (C Disp)) | M (D (M (A Ir (C Disp))))
Ea wird in Ea 1 (für JSRI) und Ea 2 für (MEA und Tmea) aufgespalten. Die Produktionen zu
Regdef, Tmpdef, Bdispdef und TBdispdef wurden bei Ea2 entfernt, weil sich Tmea (Regdef
j) stets gleichwertig durch Tmov (Reg j) etc. ersetzen lässt. Für i = 1,2
Ea_i -> A (D Regno) (C Disp) | A Ir (C Disp)
Ea_1 -> D Regno | Ir | D (M (A (D r) (C Disp))) | D (M (A Ir (C Disp)))
Regno -> HR| BR| TOP
Der Index eines Nichtterminals wird durch ein zusätzliches Argument des Parsers realisiert, wie
z.B. in
p op :: Int -> Treeparser Expr2 Op.
In den Gleichungen für p op k wird k = 1 als zusätzliches Guard eingesetzt, um die Produktionen für op 1 und op 2 zu unterscheiden.
104
6.6.4
Der verbesserte Codeselektor
Wir brauchen noch zwei Definitionen: zipWith verarbeitet Operandenpaare aus diagonaliserten
Listen, NoSideEff überprüft Operanden auf Freiheit von Seiteneffekten durch Verwendung von
Postdec/Preinc. Diese Prüfung wird im Zusammenhang mit dem ADD-Befehl benötigt.
>zipWith’ f xypairs = [f x y | (x,y) <- xypairs]
>noSideEff:: Opnds -> Bool
>noSideEff (Preinc) = False
>noSideEff (Postdec) = False
>noSideEff (Tmp r i ) = noSideEff’ i
>noSideEff (Tmpdef r i) = noSideEff’ i
>noSideEff (TBdisp r i d) = noSideEff’ i
>noSideEff (TBdispdef r i d) = noSideEff’ i
>noSideEff _ = True
>noSideEff’ (Tmov o) = noSideEff o
>noSideEff’ (Tmea o) = noSideEff o
>noSideEff’ (Tadd o1 o2) = noSideEff o1 && noSideEff o2
Einige Parsertypen haben sich geändert, da wir indizierte Nonterminalsymbole nicht durch zwei
Parser, sondern durch einen Parser mit dem Index als Zusatzargument realisieren.
>type Treeparser a b = (a -> [b])
>p_tprog :: Treeparser Iprog2 Tprog
>p_instr :: Treeparser Stat2 Instr
>p_cc
:: Treeparser Cond2 Cc
>p_ir
:: Treeparser Expr2 Ir
>p_op
:: Int -> Treeparser Expr2 Op
>p_sop
:: Treeparser Var2 Sop
>p_trg
:: Treeparser Expr2 Trg
>p_ea
:: Int -> Treeparser Expr2 Ea
>p_regno :: Treeparser Var2 Regno
>p_tprog (Iprog2 ss) = [Tprog is | is <- choices (map p_instr ss)]
> where choices
= map (take 1)
>p_instr (Ass t1 (A t2 s))
>
= (zipWith’ ADD (diag [u | t2 == D t1, u <- p_op 1 s]
>
[v1 | v1 <- vs, noSideEff v1])
>
-- der Fall
x := x + d
>
>
>
>p_instr
>
>p_instr
>
>
>
>
>p_instr
>p_instr
>
+|+ zipWith’ MEA (diag (p_ea 2
++ zipWith’ MOV (diag (p_op 1
where vs = p_sop t1
(Ass Push (LabExp l))
= [MovLab l Preinc]
(Ass t s)
= zipWith’ MEA (diag (p_ea 2 s)
++ zipWith’ MOV (diag (p_op 1
where vs = p_sop t
(A t2 s)) vs))
(A t2 s)) vs)
vs)
s) vs)
(Cjump True l b) = [JT l c | c <- p_cc b]
(Cjump False l b) = [JF l c | c <- p_cc b]
105
>p_instr
>p_instr
>p_instr
>p_instr
>p_instr
>p_instr
(Jump l)
(FPcall a)
(Pcall l)
Return
(DefLabel l)
x = []
=
=
=
=
=
[JMP l]
[JSRI x | x <- p_ea 1 a]
[JSR l]
[RET]
[DefLab l]
>p_cc (Lessop2 s2 s1) = zipWith’ CLT (diag (p_op 1 s1) (p_op 1 s2))
>p_cc (Leqop2 s2 s1) = zipWith’ CLEQ (diag (p_op 1 s1) (p_op 1 s2))
>p_cc w = []
>p_ir (A t s)
>
= (zipWith’
>
+|+ [Tmea
>
++ [Tmov
>
>
>p_ir x = [Tmea u |
>
++ [Tmov
Tadd (diag (p_trg t) (p_op 1 s))
u | u <- p_ea 2 (A t s)])
u | u <- p_op 2 (A t s)]
u <- p_ea 2 x]
u | u <- p_op 2 x]
>p_op k (D(M(D(M(A (D r) (C d))))))
>
= [Bdispdef u d | u <- p_regno r]
>
++ [TBdispdef 3 u d | u <- p_ir (D r)]
>
++ [Tmpdef 3 i | i<- p_ir (D(M(A (D r) (C d))))]
>
++ [Tmp 3 i | k==1, i <- p_ir (D(M(D(M(A (D r) (C d))))))]
>p_op k (D(M(D(M(A o (C d))))))
>
= [TBdispdef 3 u d | u <- p_ir o]
>
++ [Tmpdef 3 i | i<- p_ir (D(M(A o (C d))))]
>
++ [Tmp 3 i | k==1, i <- p_ir (D(M(D(M(A o (C d))))))]
>p_op k (D(M(A (D r) (C d))))
>
= [Bdisp u d | u <- p_regno r]
>
++ [TBdisp 3 i d | i <- p_ir (D r)]
>
++ [Tmpdef 3 i | i <- p_ir (A (D r) (C d))]
>
++ [Tmp 3 i | k==1, i <- p_ir (D(M(A (D r) (C d))))]
>p_op k (D(M(A o (C d))))
>
= [TBdisp 3 i d | i <- p_ir o]
>
++ [Tmpdef 3 i | i <- p_ir (A o (C d))]
>
++ [Tmp 3 i | k==1, i <- p_ir (D(M(A o (C d))))]
>p_op k (D(M(D r)))
>
= [Regdef u | u <- p_regno r]
>
++ [Tmpdef 3 i | i <- p_ir (D r)]
>
++ [Tmp 3 i | k==1, i<- p_ir (D(M(D r)))]
>p_op k (C n)
>
= [Imm n]
>
++[Tmp 3 i | k==1, i <- p_ir (C n)]
>p_op k Pop = [Postdec]
>
++ [Tmp 3 i | k==1, i <- p_ir Pop]
106
>p_op
>
>
>
>
>p_op
>
>
>
>p_op
>
>
>
>p_op
k (D(M (A x (C
= [TBdispdef
++ [Tmpdef
++ [Tmp 3
d))))
3 i d | i <- p_ir x]
3 i | i <- p_ir (A x (C d))]
i | i<- p_ir (D(M (A x (C d))))]
k (D(M x))
= [Tmpdef 3 i | i <- p_ir x]
++ [Tmp 3 i | k==1, i <- p_ir (D(M x))]
k (D r)
= [Reg u | u <- p_regno r]
++ [Tmp 3 i | k==1, i <- p_ir (D r)]
k x = [Tmp 3 i | k==1, i <- p_ir x]
>p_trg (D r) = [Tmp 3 i | i <- p_ir (D r)]
>p_trg x = [Tmp 3 i | i <- p_ir x]
>p_sop (M(D(M(A (D r) (C d)))))
>
= [Bdispdef u d | u<- p_regno r]
>
++ [TBdispdef 3 u d | u <- p_ir (D r)]
>
++ [Tmpdef 3 u | u <- p_ir (D(M(A (D r) (C d))))]
>p_sop (M(D(M(A i (C d)))))
>
= [TBdispdef 3 u d | u <- p_ir i]
>
++ [Tmpdef 3 u | u <- p_ir (D(M(A i (C d))))]
>p_sop
>
>
>p_sop
>
>
>
>p_sop
>
>
>
>p_sop
>
>
>p_sop
>
>p_sop
(M (D r))
= [Regdef u | u<- p_regno r]
++ [Tmpdef 3 u | u <- p_ir (D r)]
(M(A (D r) (C d)))
= [Bdisp u d | u<- p_regno r]
++ [TBdisp 3 u d | u<- p_ir (D r)]
++ [Tmpdef 3 u | u <- p_ir (A (D r) (C d))]
(M(A i (C d)))
= [TBdisp 3 u d | u<- p_ir i]
++ [Tmpdef 3 u | u <- p_ir (A i (C d))]
(M i)
= [Tmpdef 3 u | u <- p_ir i]
Push
= [Preinc]
r = [Reg u | u <- p_regno r] -- schliesst Sackgassenklausel ein
>p_ea k (D (M (A (D r) (C d))))
>
= [Bdispdef u d| k == 1, u<-p_regno r]
>
++ [TBdispdef 3 i d | k==1, i <- p_ir (D r)]
>
++ [Tmpdef 3 u | k == 1, u <- p_ir (D (M (A (D r) (C d))))]
>p_ea k (D (M (A i (C d))))
>
= [TBdispdef 3 u d | k==1, u <- p_ir i]
>
++ [Tmpdef 3 u | k==1, u <- p_ir (D (M (A i (C d))))]
107
>p_ea
>
>
>
>p_ea
>
>
>p_ea
>
>
>p_ea
k (A (D r) (C d))
= [Bdisp u d | u<- p_regno r]
++ [TBdisp 3 i d | i <- p_ir (D r)]
++ [Tmpdef 3 u | k==1, u <- p_ir (A (D r) (C d))]
k (A i (C d))
= [TBdisp 3 u d | u <- p_ir i]
++ [Tmpdef 3 u | k == 1, u <- p_ir (A i (C d))]
k (D r)
= [Regdef u |k==1, u <- p_regno r]
k i = [Tmpdef 3 u | k == 1, u <- p_ir i] -- schliesst Sackgassenklausel ein
>p_regno
>p_regno
>p_regno
>p_regno
HR
BR
TOP
w
=
=
=
=
[4]
[5]
[6]
[]
Test Section
>e1 = A (D BR) (C 42)
>e2 = A (D HR) (D TOP)
>e11 = A e1 e1
>v1 = M e1
>v2 = (C 42)
>i1
>i2
>i3
>i4
>i5
>i6
=
=
=
=
=
=
Ass
Ass
Ass
Ass
Ass
Ass
BR (A (D HR) (C 99))
(M (A (D BR) (C 333))) (A (D(M (A (D BR) (C 333)))) (C 42))
BR (D BR)
BR (A (C 44) (C 55))
(M (C 333)) (D (M (A (C 444) (C 4))))
TOP (A (D TOP) (C (-1)))
main> test 8 i1
First 8 for: Ass BR (A (D HR) (C 99))
MEA (Bdisp 4 99) (Reg 5)
MEA (TBdisp 3 (Tmov (Reg 4)) 99) (Reg 5)
MOV (Tmp 0 (Tadd (Imm 99) (Tmp 0 (Tmov (Reg 4))))) (Reg 5)
MOV (Tmp 0 (Tmea (Bdisp 4 99))) (Reg 5)
MOV (Tmp 0 (Tadd (Tmp 0 (Tmov (Imm 99))) (Tmp 0 (Tmov (Reg 4))))) (Reg 5)
MOV (Tmp 0 (Tmea (TBdisp 3 (Tmov (Reg 4)) 99))) (Reg 5)
main> test 8 i2
First 8 for: Ass (M (A (D BR) (C 333))) (A (D (M (A (D BR) (C 333)))) (C 42))
ADD (Imm 42) (Bdisp 5 333)
MEA (TBdisp 3 (Tmov (Bdisp 5 333)) 42) (Bdisp 5 333)
ADD (Tmp 0 (Tmov (Imm 42))) (Bdisp 5 333)
MEA (TBdisp 3 (Tmov (Bdisp 5 333)) 42) (TBdisp 3 (Tmov (Reg 5)) 333)
MEA (TBdisp 3 (Tmov (TBdisp 0 (Tmov (Reg 5)) 333)) 42) (Bdisp 5 333)
MEA (TBdisp 3 (Tmov (Bdisp 5 333)) 42) (Tmpdef 3 (Tadd (Imm 333)
(Tmp 0 (Tmov (Reg 5)))))
MEA (TBdisp 3 (Tmov (TBdisp 0 (Tmov (Reg 5)) 333)) 42) (TBdisp 3
(Tmov (Reg 5)) 333)
MEA (TBdisp 3 (Tmov (Tmpdef 0 (Tadd (Imm 333) (Tmp 0 (Tmov (Reg 5)))))) 42)
108
(Bdisp 5 333)
main> test 8 i3
First 8 for: Ass BR (D BR)
MOV (Reg 5) (Reg 5)
MOV (Tmp 0 (Tmov (Reg 5))) (Reg 5)
main> test 8 i4
First 8 for: Ass BR (A (C 44) (C 55))
MEA (TBdisp 3 (Tmov (Imm 44)) 55) (Reg 5)
MOV (Tmp 0 (Tadd (Imm 55) (Tmp 0 (Tmov (Imm 44))))) (Reg 5)
MOV (Tmp 0 (Tmea (TBdisp 3 (Tmov (Imm 44)) 55))) (Reg 5)
MOV (Tmp 0 (Tadd (Tmp 0 (Tmov (Imm 55))) (Tmp 0 (Tmov (Imm 44))))) (Reg 5)
main> test 8 i5
First 8 for: Ass (M (C 333)) (D (M (A (C 444) (C 4))))
MOV (TBdisp 0 (Tmov (Imm 444)) 4) (Tmpdef 3 (Tmov (Imm 333)))
MOV (Tmpdef 0 (Tadd (Imm 4) (Tmp 0 (Tmov (Imm 444))))) (Tmpdef 3 (Tmov
(Imm 333)))
MOV (Tmpdef 0 (Tmea (TBdisp 3 (Tmov (Imm 444)) 4))) (Tmpdef 3 (Tmov
(Imm 333)))
MOV (Tmpdef 0 (Tadd (Tmp 0 (Tmov (Imm 4))) (Tmp 0 (Tmov (Imm 444)))))
(Tmpdef 3 (Tmov (Imm 333)))
MOV (Tmp 0 (Tmov (TBdisp 0 (Tmov (Imm 444)) 4))) (Tmpdef 3 (Tmov
(Imm 333)))
MOV (Tmp 0 (Tmov (Tmpdef 0 (Tadd (Imm 4) (Tmp 0 (Tmov (Imm 444)))))))
(Tmpdef 3 (Tmov (Imm 333)))
MOV (Tmp 0 (Tmov (Tmpdef 0 (Tmea (TBdisp 3 (Tmov (Imm 444)) 4)))))
(Tmpdef 3 (Tmov (Imm 333)))
MOV (Tmp 0 (Tmov (Tmpdef 0 (Tadd (Tmp 0 (Tmov (Imm 4))) (Tmp 0 (Tmov
(Imm 444))))))) (Tmpdef 3 (Tmov (Imm 333)))
main> test 8 i6
First 8 for: Ass TOP (A (D TOP) (C (-1)))
ADD (Imm (-1)) (Reg 6)
MEA (Bdisp 6 (-1)) (Reg 6)
ADD (Tmp 0 (Tmov (Imm (-1)))) (Reg 6)
MEA (TBdisp 3 (Tmov (Reg 6)) (-1)) (Reg 6)
MOV (Tmp 0 (Tadd (Imm (-1)) (Tmp 0 (Tmov (Reg 6))))) (Reg 6)
MOV (Tmp 0 (Tmea (Bdisp 6 (-1)))) (Reg 6)
MOV (Tmp 0 (Tadd (Tmp 0 (Tmov (Imm (-1)))) (Tmp 0 (Tmov (Reg 6))))) (Reg 6)
MOV (Tmp 0 (Tmea (TBdisp 3 (Tmov (Reg 6)) (-1)))) (Reg 6)
6.6.5
Zusammenfassung zur Codeauswahl
Mit den Schritten
• Erweiterung der Zielsprache
• Problemsepzifikation durch Ableitung der Zielsprache in die Zwischensprache
• Überführung der Ableitung in eine Baumgrammatik
• Konstruktion eines Baumparsers
109
• Tuning des Baumparsers mit Kosten-Heuristik
sind wir nun bei einem Code-Selektor angelangt, der alle interessanten Lösungen eines Codeauswahlproblems konstruiert.
Durch die Anordnung der Gleichungen und die Verwendung des (+|+)-Operators haben wir
erreicht, dass kostengünstige Lösungen tendenziell vor den teureren gefunden werden. (Übung:
Konstruiere das einfachste IL2-Programm, dessen billigste Codierung nicht unter den ersten
drei der vom Code-Selektor gelieferten Lösungen ist.)
6.7
Lokale Registerallokation und Code-Ausgabe
Um einen elementaren Codeerzeuger zu komplettieren, fehlen uns noch drei Phasen:
• Lokale Registerallokation teilt den Hilfszellen reale Maschinenregister zu und entscheidet
bei Registerknappheit, welche Hilfszellen auf dem Keller allokiert werden.
• Code-Ausgabe führt die erweiterten Maschinenoperationen (Tadd, TBdisp,. . .) auf die
ursprünglichen (ADD, Bdisp,. . .) zurück und linearisiert den TL-Term zu einer flachen
Instruktionsliste.
• Kostenanalyse bewertet die gefundenen Alternativen gemäß der gewählten Kostenfunktion.
6.7.1
Kostenanalyse
Zur Kostenanalyse dient eine Funktion cost:: tlprog -> Int. Sie addiert die Kosten der
einzelnen Befehle und Adressierungsarten eines TL-Programms, wobei als Kostenmaß Speicherplatzbedarf des Codes und/oder Ausführungszeiten herangezogen werden. Die Definition der
Funktion cost selbst ist einfach und wir geben sie hier nicht an. (Vergleiche dazu die Kostenbetrachtung in Abschnitt 6.1.)
Die interessante Frage ist, wann die Kostenanalyse durchgeführt wird. Die Codeselektion liefert
eine endliche Anzahl möglicher Codierungen ohne Berücksichtigung des Registerbedarfs. Es
gibt drei mögliche Zielpunkte für die Kostenanalyse:
• Nach der lokalen Registerallokation: Nach der Registerallokation ist klar, welche Hilfszellen in Registern, welche im Speicher untergebracht sind. Man erhält also exakte Kosten.
Führt man die Registerallokation für alle Alternativen durch, kann man mit Sicherheit
die günstigste auswählen.
• Nach der Codeselektion und vor der Registerallokation: Hier erhält man eine Kostenabschätzung nach unten bzw. oben, wenn man annimmt, dass alle Hilfszellen in Registern
bzw. im Speicher allokiert werden. Als Heuristik wählt man die Alternative mit der kleinsten unteren Schranke und führt nur für diese die Registerallokation durch. Kommt die
Registerallokation ohne Speicherzellen aus, hat man tatsächlich das Optimale Programm
gefunden, andernfalls war die Auswahl möglicherweise suboptimal. Diese Heuristik ist also insbesondere dann sehr gut, wenn relativ viele Register zur Verfügung stehen, so dass
der suboptimale Fall selten eintritt.
110
• Gleichzeitig mit der Codeselektion: Hat man sich für die im obigen Fall beschriebene Heuristik entschieden, kann man die Kostenanalyse mit der Codeselektion verschränken. Man
projiziert die mit den TL-Operatoren assoziierten Kosten auf die entsprechenden Produktionen der Baumgrammatik. Von allen Ableitungen N →∗ p, die für ein IL2-Programm
p gefunden werden, wird nur die mit den minimalen Kosten beibehalten. Verwendet man
einen bottom-up Baumparser, so kann die Kostenanalyse (unter geringen Einschränkungen an die Kostenfunktion, z.B. Monotonie) in die Übergangstabellen des Baumparsers
eincodiert werden. Diese Methode verwenden die Codeerzeuger-Generatoren BEG und
BURG. Sie konstruieren also ohne zusätzlichen Aufwand bei der Codeselektion die Alternative mit der kleinsten unteren Kostenschranke.
6.7.2
Code-Ausgabe
Die Code-Ausgabe3 ist im wesentlichen eine Linearisierung eines TL-Programms in PostfixForm: Aus
Tprog [ MOV (Tmp 3 (Tadd (Imm 99)(Tmp 3 (Tmov (Bdisp 5 100)))))
(Bdisp 5 200)]
wird
Tprog [ MOV (Bdisp 5 100) (Reg 3),
ADD (Imm 99) (Reg 3),
MOV (Reg 3) (Bdisp 5 200)]
Die Adressierungsart Tmp wurde hier also auf Reg abgebildet. Hat die Registerallokation im
obigen Beispiel anstelle von Registernummer 3 die Nummer eines nicht existierenden Registers
eingesetzt, so zeigt dies Allokation auf dem Keller an, und die Code-Ausgabe muss neben der
Linearisierung die Adressierungsart Tmp durch die geeigneten Kelleradressierungsarten ersetzen:
Tprog [ MOV (Bdisp 5 100) Preinc,
ADD (Imm 99) (Regdef 6),
MOV Postdec (Bdisp 5 200)]
Bei der Code-Ausgabe werden die Operatoren der erweiterten Zielmaschine (vgl. 6.2) auf die
wirklich verfügbaren Operatoren zurückgeführt nach dem Schema:
Tadd -> ADD
Tmov -> MOV
Tmea -> MEA
Tmp -> Reg
Bdisp -> Bdisp
Bdispdef -> Bdispdef
Tmpdef -> Regdef
bzw.
Preinc, (Regdef 6), Postdec
Im Falle von Tmpdef, TBdisp und TBdispdef ist keine Allokation auf dem Keller möglich,
da die Zielmaschine keine Adressierungsart wie z.B. Postdecdef zur Verfügung sellt. Daraus
ergibt sich die Forderung an die Registerallokation: Im Zusammenhang mit Tmpdef, TBdisp,
TBdispdef muss stets ein Register allokiert werden.
3
Wir betrachten die Code-Ausgabe vor der im Compiler vorangehenden Phase der Registerallokation, weil
sich hier noch Randbedingungen für die Registerallokation ergeben. Wer hätte das gedacht?
111
Die Code-Ausgabe wird durch die folgende Familie von Funktionen realisiert:
lin_tprog
lin_instr
lin_opnds
lin_ir
::
::
::
::
Tprog
Instr
Opnds
Ir
->
->
->
->
Tprog
[Instr]
([Instr], Opnds)
([Instr], Opnds -> Instr)
Ist (is, aa) = lin opnds o, so ist is die Liste der Instruktionen, die nötig sind, um o adressierbar zu machen. aa ist die Adressierungsart, unter der o nun verwendet werden kann. Ist
(is, f) = lin ir x, so ist is die Liste der Instruktionen zur Berechnung von x, ausgenommen die letzte. f ist die letzte Instruktion, aber nur teilweise parametrisiert, da der Zieloperand
noch fehlt. (Er steht ja über dem Zwischenergebnis.) Ist z.B. x = Tmea (Bdisp 5 42), so ist
is = [] und f = MEA (Bdisp 5 42).
Implementierung der Code-Ausgabe
>cmap f = concat . map f
>stack = 77 :: Int
--
temporary No. for stack allocation
>lin_tprog:: Tprog -> Tprog
>lin_tprog (Tprog is) = Tprog (cmap lin_instr is)
>lin_instr:: Instr -> [Instr]
>lin_instr (MOV s t) = tcode ++ scode ++ [MOV as at]
>
where (tcode,at) = lin_opnds t
>
(scode,as) = lin_opnds s
>lin_instr (ADD s t) = tcode ++ scode ++ [ADD as (stacktransform at)]
>
where (tcode,at) = lin_opnds t
>
(scode,as) = lin_opnds s
>
stacktransform Preinc = Regdef 6
>
stacktransform op = op
>lin_instr (MEA s t) = tcode ++ scode ++ [MEA as at]
>
where (tcode,at) = lin_opnds t
>
(scode,as) = lin_opnds s
>lin_instr (JSRI s) = scode ++ [JSRI as]
>
where (scode,as) = lin_opnds s
>lin_instr x
= [x]
>lin_opnds :: Opnds -> ([Instr], Opnds)
>lin_opnds (Tmp r i)
>
| r == stack
= (icode ++ [f Preinc], Postdec)
>
| otherwise
= (icode ++ [f (Reg r)], Reg r)
>
where (icode,f) = lin_ir i
>lin_opnds (Tmpdef r i) = (icode ++ [f (Reg r)], Regdef r)
>
where (icode,f) = lin_ir i
>lin_opnds (TBdisp r i d) = (icode ++ [f (Reg r)], Bdisp r d)
>
where (icode,f) = lin_ir i
>lin_opnds (TBdispdef r i d) = (icode ++ [f (Reg r)], Bdispdef r d)
>
where (icode,f) = lin_ir i
>lin_opnds anyother = ([],anyother)
>lin_ir:: Ir -> ([Instr],(Opnds -> Instr))
>lin_ir (Tmov s) = (scode, MOV as)
112
>
>lin_ir (Tmea s) =
>
>lin_ir (Tadd s t)
>
>
>
>
6.7.3
where (scode,as) = lin_opnds s
(scode, MEA as)
where (scode,as) = lin_opnds s
= (tcode ++ scode, addtmp as)
where (scode,as) = lin_opnds
(tcode,at) = lin_opnds
addtmp q Preinc = ADD
addtmp q y
= ADD
s
t
q (Regdef 6)
q y
Lokale Registerallokation
Die Codeselektion setzt in (Tmp i x), (TBdisp i d) etc. keine Registernummern ein4 . Die
Registerallokation trägt in einem Durchlauf durch das TL-Programm hier die Nummern verfügbarer Register ein. Sind alle Registernummern belegt, wird die fiktive Registernummer stack
eingetragen, was Allokierung der Hilfszelle auf dem Keller anzeigt. Diese Technik, die Registerallokation ohne5 vorausschauende Analyse eines ganzen Ausdrucks vornimmt, nennt man
Allokation on-the-fly“.
”
Vorgaben:
• Verfügbar sind Register 0, 1, 2, 3, 4.
• Registernummer stack = 77 bedeutet Stack-Allokation.
• Im Zusammenhang mit Tmpdef, TBdisp und TBdispdef muss immer ein Register verfügbar
sein.
Die dritte Vorgabe ist nur einzulösen, weil das für Tmpdef etc. benötigte Register stets sofort
wieder freigegeben werden kann; wenn man nämlich ggf. (Tmpdef 4 x) durch (Tmp stack
(Tmov (Tmpdef 4 x))) ersetzt.
Wir verwenden die folgende Strategie:
(1) Register 0,1,2,3 werden vergeben, solange verfügbar.
(2) Register 4 kann ebenfalls vergeben werden, wenn es im unmittelbar folgenden Befehl
wieder frei wird.
(3) Sind Register 0 – 3 vergeben und (2) gilt nicht, so wird
(3.1) Tmp i x auf dem Stack allokiert, und
(3.2) für Tmpdef wird Register 4 allokiert und mittels der obigen Transformation ein Zwischenspeichern des Operanden auf dem Keller eingefügt, wodurch Register 4 wieder
frei wird.
Zur Unterscheidung (2) führen wir ein: Ein Operand o :: Opnds ist Sticky, falls er
• ein Register belegt (durch seinen Wert oder seine Adresse) oder
4
Bei Implementierung in einer logischen Programmiersprache würde i einfach eine ungebundene Variable
sein. In der Haskell-Implementierung müssen wir einen (beliebigen) Wert einsetzen.
5
Fast ohne, wie wir gleich sehen werden.
113
• einen nach ihm auszuwertenden Bruder-Operanden hat, zu dessen Auswertung möglicherweise ein Register benötigt wird.
Andernfalls heißt der Operand Transient.
Tadd
Tadd
@
Bdisp
@
5
@
@
100 i
2. Transient
@
@
@
Tmpdef
@
@
@
TBdisp
@
@
Tmpdef
@
@
@
@
x
1. Transient
@
@
100
i
Tmov
i
x
1. Sticky
Imm
33
2. Transient
Tmpdef i x ist der linke Summand und wird zuerst ausgewertet. Im ersten Fall ist er Transient,
im zweiten Sticky.
Es ist also eine gewisse Vorausschau in den Bruder-Operanden nötig, um festzustellen, ob ein
Operand Sticky oder Transient ist. Diese Vorausschau wird nur durchgeführt, wenn Register
0 – 3 belegt sind; sie lohnt sich in dem Fall, wo man mit Hilfe des reservierten“ Registers 4
”
gerade noch ohne Hilfszellen auskommt.
Der Register-Allokator ist implementiert durch die folgende Familie von Funktionen:
a_tprog::
a_instr::
a_opnds::
a_ir ::
Tprog
Inst
Opnds
Ir
->
->
->
->
Tprog
Instr
[Regno] -> Position -> (Opnds, [Regno])
[Regno] -> Ir
Das zweite Argument für a opnds und a ir gibt die Liste der verfügbaren Register an. (Für
a instr ist dies überflüssig, weil auf dieser Ebene stets alle Register verfügbar sind.)
Ist (o’, rs’) = a opnds o rs p, so gibt p die Kontextinformation an, ob o Sticky oder Transient ist. o’ ist die Rekonstruktion von o mit eingetragenen Registernummern (und ggf. transformiert, falls p = Sticky, um Register 4 nicht zu blockieren). irs’ enthält die Register aus rs,
abzüglich der durch o’ belegten.
Implementierung der Register-Allokation
>allregs = [0,1,2,3]::[Int]
>data Position
=
Sticky | Transient
>a_tprog:: Tprog -> Tprog
>a_tprog (Tprog is) = Tprog (map a_instr is)
>a_instr:: Instr -> Instr
>a_instr (MOV s t) = MOV as at
>
where (at,rs) = a_opnds t allregs Sticky
114
>
(as, x) = a_opnds s rs Transient
>a_instr (MEA s t) = MEA as at
>
where (at,rs) = a_opnds t allregs Sticky
>
(as, x) = a_opnds s rs Transient
>a_instr (ADD s t) = ADD as at
>
where (at,rs) = a_opnds t allregs Sticky
>
(as, x) = a_opnds s rs Transient
>a_instr (JSRI s) = JSRI as
>
where (as, x) = a_opnds s allregs Transient
>a_instr x
= x
>a_opnds:: Opnds -> [Regno] -> Position -> (Opnds,[Regno])
>a_opnds (Tmp n i) [] Sticky
= (Tmp stack (a_ir i []), [])
>a_opnds (Tmp n i) [] Transient = (Tmp 4 (a_ir i []), [])
>a_opnds (Tmp n i) (r:rs) p
= (Tmp r (a_ir i (r:rs)), rs)
>a_opnds (Tmpdef n i) [] Sticky
>a_opnds (Tmpdef n i) [] Transient
>a_opnds (Tmpdef n i) (r:rs) p
= (Tmp stack (Tmov (Tmpdef 4 (a_ir i []))),[])
= (Tmpdef 4 (a_ir i []),[])
= (Tmpdef r (a_ir i (r:rs)),rs)
>a_opnds (TBdisp n i d) [] Sticky
= (Tmp stack (Tmov ( TBdisp 4 (a_ir i []) d)),[])
>a_opnds (TBdisp n i d) [] Transient = (TBdisp 4 (a_ir i []) d,[])
>a_opnds (TBdisp n i d) (r:rs) p
= (TBdisp r (a_ir i (r:rs)) d,rs)
>a_opnds (TBdispdef n i d) [] Sticky
= (Tmp stack (Tmov ( TBdispdef 4 (a_ir i []) d)),[])
>a_opnds (TBdispdef n i d) [] Transient = (TBdispdef 4 (a_ir i []) d,[])
>a_opnds (TBdispdef n i d) (r:rs) p
= (TBdispdef r (a_ir i (r:rs)) d, rs)
>a_opnds anyother rs
p = (anyother,rs)
>a_ir:: Ir -> [Regno]
>a_ir (Tmov o) rs
=
>a_ir (Tmea o) rs
=
>a_ir (Tadd s t) rs =
>
>
>
-> Ir
(Tmov ao) where (ao,x) = a_opnds
(Tmea ao) where (ao,x) = a_opnds
(Tadd as at)
where (at,rs’) = a_opnds t rs
(as, x) = a_opnds s rs’
pos
= prime s
>prime
>prime
>prime
>prime
>prime
>prime
>prime
>prime
>prime
6.7.4
(Reg r)
(Imm n)
(Bdisp r d)
(Bdispdef r d)
Preinc
Postdec
(Tmp r (Tadd s t))
(Tmp r (Tmea s))
others
=
=
=
=
=
=
=
=
=
o rs Transient
o rs Transient
pos
pos
Transient
Transient
Transient
Transient
Transient
Transient
prime s
prime s
Sticky
Zusammenfassung des Codeerzeugers
>codegen :: Iprog2 ->Tprog
>codegen = lin_tprog . a_tprog . head . p_tprog
115
6.7 Literatur zum Kapitel Codeerzeugung
[BDB90] A. Balachandran, D. M. Dhamdhere, and S. Biswas. Efficient retargetable code generation using bottom-up tree pattern matching. Computer Languages, 15(3):127–140,
1990.
[BEG89] Gesellschaft für Informatik. BEG - A Back End Generator, 1989.
[BN71]
C. Gordon Bell and Allen Newell. Computer Structures: Readings and Examples.
McGraw-Hill, Inc., 1971. ISBN 07-004357-4.
[ESL89] Emmelmann, Schroeer, and Landwehr. Beg - a generator for efficient back ends. In
Proceedings of the International Conference on Programming Language Design and
Implementation, pages 227–237. Association for Computing Machinery (ACM), July
1989. Issue 24(7) of SIGPLAN Notices.
[FHP92] Christopher W. Fraser, Robert R. Henry, and Todd A. Proebsting. BURG – fast
optimal instruction selection and tree parsing. ACM SIGPLAN Notices, 27(4):68–76,
April 1992.
[FSW91] Ferdinand, Seidel, and Wilhelm. “Tree Automata for Code Selection”. In Robert Giegerich and Susan L. Graham, editors, “Code Generation — Concepts, Tools, Techniques”, Proceedings of the International Workshop on Code Generation, Dagstuhl,
Germany, 20-24 May 1991, Workshops in Computing, pages 30–50. Springer-Verlag,
1991. ISBN 3-540-19757-5 and 3-387-19757-5.
[Gie90]
Robert Giegerich. Code selection by inversion of order-sorted derivors. Theoretical
Computer Science, 73:177–211, 1990.
[GS88]
Robert Giegerich and Karl Schmal. Code selection techniques: Pattern matching, tree
parsing and inversion of derivors. In Proceedings of the 2nd European Symposium on
Programming, volume 300 of Lecture Notes in Computer Science (LNCS), pages 247–
268, March 1988.
[PL87]
Eduardo Pelegri-Llopart. Rewrite Systems, Pattern Matching and Code Generation.
PhD thesis, UC Berkeley, 1987. EECS-Report.
[PLG88] Eduardo Pelegri-Llopart and Susan L. Graham. Optimal code generation for expression trees: An application of burs theory. In Conference Record of the Fifteenth Annual
ACM Symposium on Principles of Programming Languages, pages 294–308, 1988.
116
Minipas.lhs
MiniPas - Uebersetzungssystem
Zusammenfassung der Uebersetzungsphasen
--------------------------------------ASCII
AST
IL1
IL2
TL
TL
TL
---------------
Parser ---------------->
semantische Analyse --->
Implementierung ------->
Codeauswahl ----------->
Registerallokation ---->
Linearisierung -------->
symb. Marken entf. ---->
AST
IL1
IL2
TL
TL
TL
TL
(abstrakte Syntax)
(abstraktes Programm ohne Bezeichner)
(Abstraktion auf Stackmaschine)
("Maschineninstruktions-Baeume")
(Baeume mit Registern)
(linearisierte TL-Programme)
(interpretierbare TL-Programme)
>
AST.lhs
Abstrakte Syntax : AST
---------------------> module AST where
--------------------------------------------------------------------------Abstrakte Syntax
-------------------------------------------------------------------------->data
>data
>
>
>
>data
>
>
>
>
>
>
>
>
>data
>
>
>
>data
>
>data
>
>
>
Prog
Decl
=
=
Stat
=
Par
=
Partyp =
Arg
=
Cprog Decl Stat
deriving Show
Cdeclseq Decl Decl |
Cdecl Ident Typ |
Cproc Ident Par Decl Stat |
Cemptydecl
deriving Show
Cseq Stat Stat |
Cassign Ident Expr |
Cif Expr Stat |
Cwhile Expr Stat |
Ccall Ident Arg |
Cwrite Expr |
Cread Ident |
Cskip
deriving Show
Cparseq Par Par |
Cpar Partyp Ident Typ |
Cprocpar Ident Par |
Cemptypar
Cvarpar | Cvalpar
deriving Show
deriving (Eq, Show)
Cargseq Arg Arg |
Carg Expr |
Cemptyarg
deriving Show
117
>data
>
>data
>
>
>
>
>data
>
>type
>type
Typ
=
Cbool | Cint
deriving (Eq, Show)
Expr
=
Binop
=
Cbinop Expr Binop Expr |
Cvar Ident |
Cconst IntConst |
Ctrue |
Cfalse
Cplus | Cless | Cleq
deriving Show
deriving (Show, Eq)
Ident
=
IntConst =
String
String
---------- Ableitung Prog -> String
(Notation von AST) ---------------
>showast :: Prog -> String
>showast (Cprog d p) = "Program\n" ++ ast_decl 1 d ++ seq ++ ast_statn 1 p ++ "End"
>
where seq = case d of
>
Cemptydecl -> ""
>
otherwise -> ";\n\n"
>ast_decl
>ast_decl
>ast_decl
>
>
>
>
>
>
>
>
>ast_decl
n (Cdeclseq d1 d2) = ast_decl n d1 ++ ";\n" ++ ast_decl n d2
n (Cdecl i t)
= spc n ++ "Var " ++ i ++ " : " ++ ast_typ t
n (Cproc i p d s) = "\n" ++ spc n ++ "Proc " ++ i ++ pars ++
"\n" ++ ast_decl (n+1) d ++ seq ++
ast_statn (n+1) s ++ spc n ++ "End"
where seq = case d of
Cemptydecl -> ""
otherwise -> ";\n\n"
pars = case p of
Cemptypar -> ""
otherwise -> "(" ++ ast_par p ++ ")"
n (Cemptydecl)
= ""
>ast_statn n (Cseq s1 s2) = ast_stat n s1 ++ ";\n" ++ ast_stat n s2 ++ "\n"
>ast_statn n x
= ast_stat n x ++ "\n"
>ast_stat n (Cseq s1 s2)
>ast_stat
>ast_stat
>
>ast_stat
>
>ast_stat
>
>ast_stat
>ast_stat
>ast_stat
>ast_stat
>ast_par
>ast_par
>ast_par
>ast_par
= ast_stat n s1 ++ ";\n" ++ ast_stat n s2
n (Cassign i e) = spc n ++ i ++ " := " ++ ast_expr e
n (Cif e s)
= spc n ++ "If (" ++ ast_expr e ++ ") Then\n" ++
ast_statn (n+1) s ++ spc n ++ "End"
n (Cwhile e s) = spc n ++ "While (" ++ ast_expr e ++ ") Do\n" ++
ast_statn (n+1) s ++ spc n ++ "End"
n (Ccall i Cemptyarg)
= spc n ++ i
n (Ccall i p)
= spc n ++ i ++ "(" ++ ast_arg p ++ ")"
n (Cwrite e)
= spc n ++ "Write " ++ ast_expr e
n (Cread i)
= spc n ++ "Read " ++ i
n Cskip
= ""
(Cparseq p1 p2)
(Cpar Cvarpar i t)
(Cpar Cvalpar i t)
(Cprocpar i Cemptypar)
=
=
=
=
ast_par p1 ++ ", " ++ ast_par p2
"Var " ++ i ++ ":" ++ ast_typ t
i ++ ":" ++ ast_typ t
"Proc " ++ i
118
>ast_par (Cprocpar i p)
>ast_par Cemptypar
>ast_expr
>ast_expr
>ast_expr
>ast_expr
>ast_expr
>
(Cvar i)
(Cconst i)
(Ctrue)
(Cfalse)
(Cbinop e1
=
=
=
=
b
= "Proc " ++ i ++ "(" ++ ast_par p ++ ")"
= ""
i
i
"True"
"False"
e2) = ast_expr e1 ++ s ++ ast_expr e2
where s = head [ f | (f,bs) <- binoplist, bs == b]
>ast_arg (Cargseq e1 e2) = ast_arg e1 ++ "," ++ ast_arg e2
>ast_arg (Carg e)
= ast_expr e
>ast_arg Cemptyarg
= ""
>ast_typ Cint = "Int"
>ast_typ Cbool = "Bool"
>spc n = replicate (3*n) ’ ’
>binoplist =
[("+", Cplus), ("<", Cless), ("<=", Cleq)]
>
IL1.lhs
Zwischensprache IL1
------------------>module IL1 where
--------------------------------------------------------------------------Zwischensprache IL1
-------------------------------------------------------------------------->type
>type
>type
>type
>type
>
>data
>data
>
>
>
>
>
>
Reladr
Pst
Label
Parlength
Varlength
=
=
=
=
=
Int
Int
Int
Int
Int
Iprog1 = Iprog1 Varlength [Stat1]
Stat1 = Assign1 Var1 Expr1 |
IfThen Cond1 [Stat1] Label |
While Label Cond1 [Stat1] Label |
ProcDef Label Varlength Pst [Stat1] |
Call Procid [Aktpar] |
Write1 Expr1 |
Read1 Var1
119
deriving Show
deriving Show
>data
>
>
>data
>data
>
>data
>
>data
>
>data
Procid = Proc Label Parlength Pst | FormProcPar Parlength Pst Reladr
deriving Show
Var1
Vkind
= Var Pst Reladr Vkind
= IsValue | IsVarPar
deriving (Eq, Show)
deriving (Eq, Show)
Aktpar = ValPar Expr1 | VarPar Var1 | ProcPar Procid
deriving Show
Cond1
= Lessop1 Expr1 Expr1 | Leqop1
deriving Show
Expr1
= Addop1 Expr1 Expr1 | Const Int | VarExp Var1
Expr1 Expr1
deriving (Eq, Show)
>exprtest :: Expr1 -> Expr1 -> Bool
>exprtest a b = a == b
---------- Ableitung Iprog1 -> String
(Notation von IL1) -------------
>showil1 :: Iprog1 -> String
>showil1 (Iprog1 vl s) = "Prog vl:" ++ show vl ++ "\n" ++
>
concat [ il1_stat 0 s’ | s’ <- s]
>il1_stat n (Assign1 v e) = spc n ++ "Ass " ++ il1_var v ++ " := "
>
++ il1_expr e ++ "\n"
>il1_stat n (IfThen c s l) = spc n ++ "If (" ++ il1_cond c ++ ") \n"
>
++ concat [ il1_stat (n+1) s’ | s’ <- s ]
>
++ spc n ++ " " ++ show l ++ "\n"
>il1_stat n (While l1 c s l2) = spc n ++ "While " ++ show l1
>
++ " (" ++ il1_cond c ++ ") \n"
>
++ concat [ il1_stat (n+1) s’ | s’ <- s
>
++ spc n ++ " " ++ show l2 ++ "\n"
>il1_stat n (ProcDef l v p s) = spc n ++ "Proc " ++ show l
>
++ ", vl:" ++ show v ++ ", ps:"
>
++ show p ++ " \n"
>
++ concat [ il1_stat (n+1) s’ | s’ <- s
>
++ spc n ++ "\n"
>il1_stat n (Call p a) = spc n ++ "Call " ++ il1_procid p ++ " ["
>
++ fill (",\n" ++ spc (n+9) ++ " ")
>
[ il1_aktpar a’ | a’ <- a ] ++ "]" ++ "\n"
>il1_stat n (Write1 e) = spc n ++ "Write " ++ il1_expr e ++ "\n"
>il1_stat n (Read1 v) = spc n ++ "Read " ++ il1_var v ++ "\n"
>il1_procid (Proc l pa ps) = "Proc pl:" ++ show pa ++ " ps:"
>
++ show ps ++ " l:" ++ show l
>il1_procid (FormProcPar pl ps r) = "FormProc pl:" ++ show pl ++
>
" ps:" ++ show ps ++ " rel:"
>
++ show r
>il1_var (Var p r v) = "(ps:" ++ show p ++ " rel:" ++ show r
>
++ " " ++ il1_vkind v ++ ")"
>il1_vkind IsValue = "IsVal"
>il1_vkind IsVarPar = "IsVar"
>il1_aktpar (ValPar e) = "ValPar " ++ il1_expr e
>il1_aktpar (VarPar v) = "VarPar " ++ il1_var v
>il1_aktpar (ProcPar p) = "ProcPar (" ++ il1_procid p ++ ")"
>il1_cond (Lessop1 e1 e2) = "(" ++ il1_expr e1 ++ "<" ++ il1_expr e2 ++
>il1_cond (Leqop1 e1 e2) = "(" ++ il1_expr e1 ++ "<=" ++ il1_expr e2 ++
120
]
]
")"
")"
>il1_expr (Addop1 e1 e2) = "(" ++ il1_expr e1 ++ "+" ++ il1_expr e2 ++ ")"
>il1_expr (Const n) = "(" ++ show n ++ ")"
>il1_expr (VarExp v) = il1_var v
>fill :: [a] -> [[a]] -> [a]
>fill str [] = []
>fill str l = (concat . map (++ str) . init) l ++ last l
>spc n = replicate (3*n) ’ ’
>
IL2.lhs
Zwischensprache IL2
------------------>module IL2 where
--------------------------------------------------------------------------Zwischensprache IL2
-------------------------------------------------------------------------->type Label = Int
>data
>data
>
>
>
>
>
>
>data
>
>
>data
>
>
>data
>
Iprog2 = Iprog2 [Stat2]
Stat2 = Ass Var2 Expr2 |
Jump Label
| Cjump Bool Label Cond2 |
DefLabel Label |
Pcall Label
| FPcall Expr2 | Return |
Write2 Expr2
| Read2 Var2
deriving Show
Var2
=
M Expr2 | BR | HR | TOP | Push
deriving (Eq, Show)
Cond2
= Lessop2 Expr2 Expr2 | Leqop2
deriving Show
Expr2
= A Expr2 Expr2 | C Int | D Var2 | Pop | LabExp Label
deriving (Eq, Show)
Expr2 Expr2
---------- Notation von IL2
>showil2 (Iprog2 ss) = "Iprog2:\n" ++ concatMap showinst ss
>showinst:: Stat2 -> String
>showinst i = show i ++ "\n"
>
TL.lhs
Die Zielsprache : TL
121
------------------->module TL where
--------------------------------------------------------------------------Zielsprache
--------------------------------------------------------------------------Definition der Maschinenbefehle.
Marken und Sprungziele (vom Typ Label) sind bis nach der Codelinearisierung symbolische Int-Konstanten.
Durch relabel (Relabel.lhs) werden dann die Marken entfernt und die
symbolischen Sprungziele durch absolute Programmadressen ersetzt.
In und Out bilden die Ein-/Ausgabe-Schnittstelle. Mit In a wird ein
Wert in die ueber a adressierte Speicherzelle geladen. Mit Out v
wird der ueber v adressierte Wert ausgegeben.
>type Label =
>type Regno =
>type Disp =
Int
Int
Int
>data Tprog =
>
>data Instr =
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>data Cc
=
>
>
Tprog [Instr]
deriving Show
MOV Op Sop
|
MEA Ea Sop
|
ADD Op Sop
|
JT
Label Cc |
JF
Label Cc |
JMP Label
|
JSRI Ea
|
JSR Label
|
RET
|
MovLab Label
|
DefLab Label
|
Out Op
|
In
Sop
deriving Show
CLT Op Op |
CLEQ Op Op
deriving Show
Befehlsvarianten, die Zwischenergebnisse berechnen:
>data Ir
>
>
>
=
Tmov Op |
Tmea Ea |
Tadd Op Trg
deriving Show
Die Operandenklasse Opnds wird eingefuehrt als Klasse aller Adressierungsarten.
Die wirklichen Operandenklassen der Zielmaschine werden als Typsynonyme erklaert.
>type Op
=
Opnds
-- ausgenommen Preinc
122
>type
>type
>type
>type
Sop
Trg
Tmp
Ea
=
=
=
=
>data Opnds =
>
>
>
>
>
>
>
Opnds
Opnds
Opnds
Opnds
-----
ausgenommen Imm, Postdec,
nur Tmp und Reg
nur Tmp
ausgenommen Imm, Reg, Tmp, Tmpdef, Postdec, Preinc
Imm Int |
Reg Regno |
Regdef Regno |
Bdisp Regno Disp |
Bdispdef Regno Disp |
Preinc |
Postdec
deriving Show
Tmp Regno Ir |
Tmpdef Regno Ir |
TBdisp Regno Ir Disp |
TBdispdef Regno Ir Disp |
>showtl (Tprog ss) = "Tprog:\n" ++ concatMap showinstr (zip [0..] ss)
>showinstr:: (Int, Instr) -> String
>showinstr i = show i ++ "\n"
>
Synana.lhs
Parser fuer MiniPas
-------------------
>module Synana (syn_ana, scanner) where
>import AST
--------------------------------------------------------------------------MiniPas - Grammatik
--------------------------------------------------------------------------Prog
-> "Program" DeclList StatList "End"
DeclList -> Decl DeclR ";" |
eps
DeclR
-> ";" Decl DeclR |
eps
Decl
-> "Var" Id ":" Type |
"Proc" Id ParList DeclList StatList "End"
StatList -> Stat StatR |
eps
StatR
-> ";" Stat StatR |
eps
Stat
-> Id ":=" Exp |
"If" RelExp "Then" StatList "End" |
"While" RelExp "Do" StatList "End" |
Id ArgList |
"Write" Exp |
"Read" Id
ParList -> "(" Par ParR ")" |
eps
ParR
-> "," Par ParR |
123
Par
ArgList
ArgR
Type
RelExp
Exp
TermR
Term
RelOp
eps
-> "Var" Id ":" Type |
Id ":" Type |
"Proc" Id ParList |
-> "(" Exp ArgR ")" |
eps
-> "," Exp ArgR |
eps
-> "Int" | "Bool"
-> Exp RelOp Exp
-> Term TermR
-> "+" Term TermR |
eps
-> Id | Int | "True" | "False"
-> "<" | "<="
--------------------------------------------------------------------------Parser
-------------------------------------------------------------------------->type Parser a b
= [a] -> [(b, [a])]
>type Eparser c a b = c
-> Parser a b
>syn_ana = fst . head . pProg . scanner
where
>
>
>
pProg :: Parser Token Prog
pProg = Cprog <<< (rword "Program" -~~ pDeclList) ~~~ pStatList ~~rword "End"
>
>
>
>
>
>
>
>
pDeclList :: Parser Token Decl
pDeclList = pList pDecl nothing (symbol ";") (symbol ";") Cdeclseq Cemptydecl |||
succeed Cemptydecl
>
>
>
>
>
>
>
>
>
>
>
>
>
>
pStatList :: Parser Token Stat
pStatList = pList pStat nothing nothing (symbol ";") Cseq Cskip
pDecl :: Parser Token Decl
pDecl = (Cdecl <<< (rword "Var" -~~ pId) ~~~ (symbol ":" -~~ pType))
(Cproc <<< (rword "Proc" -~~ pId) ~~~ pParList ~~~
pDeclList ~~~ (pStatList ~~- rword "End"))
pStat :: Parser Token Stat
pStat = (Cassign <<< pId ~~~ (symbol ":=" -~~ pExp))
(Cif
<<< (rword "If" -~~ symbol "(" -~~ pRelExp) ~~~
(symbol ")" -~~ rword "Then" -~~ pStatList
~~- rword "End"))
(Cwhile <<< (rword "While" -~~ symbol "(" -~~ pRelExp) ~~~
(symbol ")" -~~ rword "Do" -~~ pStatList
~~- rword "End"))
(Ccall
<<< pId ~~~ pArgList)
(Cwrite <<< (rword "Write" -~~ pExp))
(Cread
<<< (rword "Read" -~~ pId))
124
|||
|||
|||
|||
|||
|||
>
>
>
>
>
>
>
pParList :: Parser Token Par
pParList = pList pPar (symbol "(") (symbol ")") (symbol ",") Cparseq Cemptypar
>
>
>
pArgList :: Parser Token Arg
pArgList = pList (Carg <<< pExp) (symbol "(") (symbol ")") (symbol ",")
Cargseq Cemptyarg
>
>
pRelExp :: Parser Token Expr
pRelExp = crelop <<< pExp ~~~ pRelop ~~~ pExp
>
>
>
>
>
>
pExp :: Parser Token Expr
pExp = pTerm ‘follow‘ pTermR
>
>
>
>
>
pTerm :: Parser
pTerm = (Cvar
(Cconst
(Ctrue
(Cfalse
>
>
>
pType :: Parser Token Typ
pType = (Cint
><< rword "Int")
(Cbool ><< rword "Bool")
>
>
>
>
>
>
>
>
>
>
>
>
pId :: Parser Token Ident
pId []
= []
pId (t:ts)
= [(t,ts) | isAlpha (head t)]
>
>
>
symbol :: (Eq a) => a -> Parser a a
symbol s []
= []
symbol s (t:ts) = [(t,ts) | s == t]
>
>
rword :: Token -> Parser Token Token
rword s = symbol (’$’:s)
>
>
nothing :: Parser Token Token
nothing x = [("",x)]
pPar :: Parser Token
pPar = (Cpar Cvarpar
(Cpar Cvalpar
(Cprocpar
Par
<<< (rword "Var" -~~ pId) ~~~ (symbol ":" -~~ pType))
<<< pId ~~~ (symbol ":" -~~ pType))
<<< (rword "Proc" -~~ pId) ~~~ pParList)
|||
|||
pTermR :: Eparser Expr Token Expr
pTermR t = ((Cbinop t Cplus <<< (symbol "+" -~~ pTerm)) ‘follow‘ pTermR) |||
succeed t
pInt
pInt
pInt
pInt
Token Expr
<<< pId)
<<< pInt)
><< rword "True")
><< rword "False")
|||
|||
|||
|||
:: Parser Token IntConst
[]
= []
((’-’:tf:tr):ts) = [(’-’:tf:tr, ts)]
((tf:tr):ts)
= [(tf:tr, ts) | isDigit tf]
pRelop :: Parser Token Token
pRelop []
= []
pRelop (t:ts) = [(t, ts) | elem t (map fst reloplist)]
125
>succeed :: b -> Parser a b
>succeed e ts = [(e,ts)]
>crelop :: Expr -> String -> Expr -> Expr
>crelop e opS e’ = Cbinop e op e’
>
where op = head [opC | (opS’, opC) <- reloplist, opS == opS’]
>reloplist =
[("<", Cless), ("<=", Cleq)]
----- Kombinatoren
>(|||)
:: Parser a b -> Parser a b -> Parser a b
>(p1 ||| p2) inp = (p1 inp) ++ (p2 inp)
>
>(~~~)
:: Parser a (b -> c) -> Parser a b -> Parser a c
>(p1 ~~~ p2) inp = [((t1 t2), out) | (t1, r1) <- p1 inp,
>
(t2, out) <- p2 r1 ]
>
>(-~~)
:: Parser a b -> Parser a c -> Parser a c
>(p1 -~~ p2) inp = [(t2, out) | (t1, r1) <- p1 inp,
>
(t2, out) <- p2 r1 ]
>
>(~~-)
:: Parser a b -> Parser a c -> Parser a b
>(p1 ~~- p2) inp = [(t1, out) | (t1, r1) <- p1 inp,
>
(t2, out) <- p2 r1 ]
>
>(<<<)
:: (b -> c) -> Parser a b -> Parser a c
>(f <<< p) inp = [ (f t, out) | (t, out) <- p inp ]
>
>(><<)
:: c -> Parser a b -> Parser a c
>(f ><< p) inp = [(f, out) | (_, out) <- p inp ]
>
>follow
:: Parser a b -> (Eparser b a c) -> Parser a c
>(p1 ‘follow‘ ep2) inp = [ (t,out) | (t1,rest) <- p1 inp,
>
(t,out)
<- ep2 t1 rest ]
>type Lparser a b = Parser a b -> Parser a a -> Parser a a -> Parser a a ->
>
(b->b->b) -> b -> Parser a b
Lparser a b : Parser von a nach Sequenz von b.
Parameter : 1. Parser fuer b
2. Parser fuer Begrenzer vor der Sequenz
3. Parser fuer Begrenzer hinter der Sequenz
4. Parser fuer Separator innerhalb der Sequenz
5. Sequenz-Konstruktor zum Aufbau der abstr. Syntax
126
6. Ende-Konstruktor
"
Beispiel pParList :: Parser Token Par
pParList = pList pPar
(symbol "(")
(symbol ")")
(symbol ",")
Cparseq
Cemptypar
>pList :: Lparser
>pList p lp rp sp
>
= ((lp -~~
>
(succeed
>
where pR
>
a b
seqOp endOp
(p ‘follow‘ pR)) ~~- rp) |||
endOp)
inp = ((seqOp inp <<< (sp -~~ p)) ‘follow‘ pR) |||
succeed inp
--------------------------------------------------------------------------Scanner
--------------------------------------------------------------------------Der Scanner trennt die Eingabe in
-
Symbole
Reservierte Worte
Benutzer-Namen
Zahlen
:
:
:
:
"<=", ":", ";", ...
"$Program", "$If", "$Then", ...
"n", "F", "Hallo", ...
"-23", "12", ...
auf.
>type Token = String
>scanner :: String -> [Token]
>scanner []
= []
>scanner (c : cs) = t : scanner ts
>
where (t, ts) = ftoken (c:cs)
>
>ftoken :: String -> (Token, String)
>ftoken [] = error "input exhausted (ftoken)"
>ftoken (’ ’ : cs)
= ftoken cs
>ftoken (’\n’ : cs)
= ftoken cs
>ftoken (’<’ : ’=’ : cs) = ("<=", cs)
>ftoken (’:’ : ’=’ : cs) = (":=", cs)
>ftoken (c : cs)
| elem c "+<:();,"
= ([c], cs)
>
| isAlpha c && resword (c:i) = (’$’:c:i,ys)
>
| isAlpha c
= (c:i,ys)
>
| isDigit c || c == ’-’
= (c:n,ys)
>
where (i,ys) = span diglet cs
>
(n,zs) = span isDigit cs
>
>resword :: String -> Bool
>resword x = elem x ["Program", "Var", "Proc", "End", "If", "Then", "While",
127
>
"Do", "Int", "Bool", "Write", "Read", "True", "False"]
>diglet d = isAlpha d || isDigit d
---------- Testdaten ------------------------------------------------->tp1
>tp2
= "Program Var n : Int; n := n+1 End"
= "Program Proc P(Var n:Int) k:=n+m End; P(m) End"
---------- Testlaeufe ------------------------------------------------Synana> syn_ana tp1
Cprog (Cdecl "n" Cint) (Cassign "n" (Cbinop (Cvar "n") Cplus (Cconst "1")))
Synana> syn_ana tp2
Cprog (Cproc "P" (Cpar Cvarpar "n" Cint) Cemptydecl
(Cassign "k" (Cbinop (Cvar "n") Cplus (Cvar "m"))))
(Ccall "P" (Carg (Cvar "m")))
>
Semana.lhs
Semantische Analyse - sem_ana : AST -> IL1
------------------------------------------->module Semana (sem_ana) where
>import AST hiding (spc)
>import IL1 hiding (spc)
----------------------------------------------------------------------Semantische Analyse
----------------------------------------------------------------------Ueberpruefung semantischer Fehler :
- mehrfache Deklaration von Bezeichnern
- fehlende Deklaration von Bezeichnern
- Typueberpruefung
- in Zuweisungen
- bei der Parameteruebergabe
- in Ausdruecken
Generierung :
- Uebergang von Bezeichnern zu Prozedurschachtelungstiefe
und Relativadresse
- Bestimmung
- der Laenge des Bereichs lokaler Variablen fuer
Prozeduren und das Hauptprogramm
- der Prozedurschachtelungstiefe fuer Prozeduren
- der Laenge der Parameteruebergabe bei Prozeduraufrufen
128
- Vergabe von Marken fuer Prozeduren und Kontrollstrukturen
(While, If, ...)
>type
>type
>type
>type
>data
Vartab
Proctab
Vardef
Procdef
Pardef
=
=
=
=
=
[(Pst,[Vardef])]
[(Pst,[Procdef])]
(Ident,Reladr,Vkind,Typ)
(Ident,Reladr,Partyp,Label,Parlength,[Pardef])
Expression Typ | Variable Typ | Procedure
deriving Eq
>sem_ana = implProg
>implProg
>implDecl
>
>implStat
>
>implPar
>implParf
>implArg
>
>implExpr
>implCond
::
Prog -> (Iprog1)
:: (Pst, Vartab, Proctab, Varlength, Label)
-> Decl -> (Vartab, Proctab, Varlength, Label, [Stat1])
:: (Vartab, Proctab, Label)
-> Stat -> (Label, [Stat1])
:: (Reladr)
-> Par -> ([Vardef], [Procdef], Reladr, [Pardef])
::
Par -> (Parlength, [Pardef])
:: ([Pardef], Vartab, Proctab)
-> Arg -> ([Aktpar], [Pardef])
:: (Vartab)
-> Expr -> (Expr1, Typ)
:: (Vartab)
-> Expr -> (Cond1, Typ)
>implProg (Cprog d s)
>
= Iprog1 vl (proccode ++ bodycode)
>
where (vars, procs, vl, lb, proccode)
>
= implDecl (0, [(0,[])], [(1,[])], 0, 1000) d
>
(lb1, bodycode) = implStat (vars, procs, lb) s
>implDecl (ps, vars, procs, vl, lb) (Cdeclseq d1 d2)
>
= (vars2, procs2, vl2, lb2, code1 ++ code2)
>
where (vars2, procs2, vl2, lb2, code2)
>
= implDecl (ps, vars1, procs1, vl1, lb1) d2
>
(vars1, procs1, vl1, lb1, code1)
>
= implDecl (ps, vars, procs, vl, lb) d1
>
>implDecl (ps, vars, (psp,r):procs, vl, lb) (Cproc id p d s)
>
| uni && newid = (vars, procs3, vl, lb2, [ProcDef lb vl1 (ps+1) (proccode ++ bodycode)])
>
| not uni
= error ("formal parameters are not unique")
>
| otherwise
= error ("procedure " ++ id ++ " already defined")
>
where
procs3
= (psp, thisproc : r) : procs
>
(vars2, procs2, vl1, lb1, proccode)
>
= implDecl (ps+1, vars1, procs1, 0, lb+1) d
>
(lb2, bodycode)
>
= implStat (vars2, procs2, lb1) s
>
procs1
= (psp+1, []) : (psp, formprocs ++ (thisproc : r))
>
: procs
>
vars1
= (ps+1, formvars):vars
>
thisproc = (id, (-1), Cvalpar, lb, (-3)-rel, param)
>
(formvars,formprocs,rel,param) = implPar (-3) p
>
uni
= unique ([ id | (id,r,v,t) <- formvars ] ++
129
>
[ id | (id,r,p,l,pl,pa) <- formprocs ])
>
newid
= not (elem id [ id’ | (id’,r,p,l,pl,pa) <- r ])
>
>implDecl (ps, (psv,r):vars, procs, vl, lb) (Cdecl id t)
>
| not (elem id [ id’ | (id’,r,v,t) <- r ])
>
= ((psv,(id,vl+ps,IsValue,t):r):vars, procs, vl+1, lb, [])
>
| otherwise
>
= error ("variable " ++ id ++ " already defined")
>
>implDecl (ps, v, p, vl, lb) (Cemptydecl) = (v, p, vl, lb, [])
>implStat
>
>
>
>
>implStat
>
>
>
>
>
>
>implStat
>
>
>
>
>implStat
>
>
>
>
>implStat
>
>
>
>
>
>
>
>implStat
>
>
>
>implStat
>
>
>
>implStat
(v, p, l) (Cseq s1 s2)
= (l2, code1 ++ code2)
where (l2, code2) = implStat (v, p, l1) s2
(l1, code1) = implStat (v, p, l) s1
(vars, p, l) (Cassign id e)
| tv == te
= (l, [Assign1 lhs rhs])
| otherwise = error "incompatible types in assignment"
where lhs
= Var psv rel pt
(rhs,te)
= implExpr vars e
(psv,rel,pt,tv) = lookUpVar vars id
(vars, p, l) (Cif e s)
= (l1, [IfThen cond code1 l])
where (cond, t)
= implCond vars e
(l1, code1) = implStat (vars, p, l+1) s
(vars, p, l) (Cwhile e s)
= (l1, [While l cond code1 (l+1)])
where (cond, t)
= implCond vars e
(l1, code1) = implStat (vars, p, l+2) s
(vars, procs, l) (Ccall id e)
| r == []
= (l, [Call pid apar])
| otherwise = error ("too few parameters in call of procedure " ++ id)
where pid
| pt == Cvalpar = Proc lab pl psp
| otherwise
= FormProcPar pl psp rel
(apar,r) = implArg (pm, vars, procs) e
(psp,rel,pt,lab,pl,pm) = lookUpProc procs id
(vars, p, l) (Cwrite e)
= (l, [Write1 e1])
where (e1, t) = implExpr vars e
(vars, p, l) (Cread id)
= (l, [Read1 (Var psv rel pt)])
where (psv, rel, pt, tv) = lookUpVar vars id
(v, p, l) (Cskip) = (l, [])
>implPar rel (Cparseq p1 p2) = (fv1 ++ fv2, fp1 ++ fp2, rel2, pm1 ++ pm2)
>
where (fv1,fp1,rel2,pm1) = implPar rel1 p1
130
>
>implPar
>
>implPar
>
>implPar
>
>
>implPar
(fv2,fp2,rel1,pm2) = implPar rel p2
rel (Cpar Cvalpar id t)
= ([(id,rel,IsValue,t)], [], rel-1, [Expression t])
rel (Cpar Cvarpar id t)
= ([(id,rel,IsVarPar,t)], [], rel-1, [Variable t])
rel (Cprocpar id p)
= ([], [(id,rel-1,Cvarpar,-1,pl,pm)], rel-2, [Procedure])
where (pl,pm) = implParf p
rel (Cemptypar) = ([], [], rel, [])
>implParf
>
>
>implParf
>implParf
>implParf
>implParf
>implArg
>
>
>
>implArg
>
>
>
>implArg
>
>
>
>implArg
>
>
>
>implArg
>implArg
>implArg
>
>implArg
>
>implArg
>
(Cparseq p1 p2)
= (pl1+pl2, pm1 ++ pm2)
where (pl1,pm1) = (implParf p1)
(pl2,pm2) = (implParf p2)
(Cpar Cvalpar id t) = (1,[Expression t])
(Cpar Cvarpar id t) = (1,[Variable t])
(Cprocpar id p)
= (2,[Procedure])
(Cemptypar)
= (0,[])
(param, vars, procs) (Cargseq e1 e2)
= (e1’ ++ e2’, p2)
where (e1’,p1) = implArg (param, vars, procs) e1
(e2’,p2) = implArg (p1, vars, procs) e2
((Variable t : r), vars, procs) (Carg (Cvar id))
| t == t’
= ([VarPar (Var ps rel pt)], r)
| otherwise = error "wrong parameter type"
where (ps,rel,pt,t’) = lookUpVar vars id
((Procedure : r), vars, procs) (Carg (Cvar id))
| pt == Cvalpar = ([ProcPar (Proc lab pl ps)], r)
| otherwise
= ([ProcPar (FormProcPar pl ps rel)], r)
where (ps,rel,pt,lab,pl,pm) = lookUpProc procs id
((Expression t : r), vars, procs) (Carg e)
| t == t’
= ([ValPar e’], r)
| otherwise = error "wrong parameter type"
where (e’,t’) = implExpr vars e
(param, vars, procs) Cemptyarg = ([],param)
([], vars, procs) x = error "too many parameters in procedure-call"
((Procedure : r), vars, procs) x
= error "parameter is not a procedure"
((Variable t : r), vars, procs) x
= error "parameter is not a variable"
((Expression t : r), vars, procs) x
= error "parameter is not an expression"
>implExpr
>
>
>implExpr
>
>implExpr
>implExpr
>implExpr
vars (Cbinop e1 b e2) = ((exprop2 b) e1’ e2’, typcheck2 t1 t2 b)
where (e1’,t1) = implExpr vars e1
(e2’,t2) = implExpr vars e2
vars (Cvar id) = (VarExp (Var ps rel pt), t)
where (ps,rel,pt,t) = lookUpVar vars id
vars (Cconst c) = (Const (numval c), Cint)
vars (Ctrue)
= (Const 1, Cbool)
vars (Cfalse)
= (Const 0, Cbool)
>implCond vars (Cbinop e1 b e2) = ((condop2 b) e1’ e2’, typcheck2 t1 t2 b)
131
>
>
where (e1’,t1) = implExpr vars e1
(e2’,t2) = implExpr vars e2
>lookUpVar :: Vartab -> Ident -> (Pst,Reladr,Vkind,Typ)
>lookUpVar []
id = error ("undeclared variable " ++ id)
>lookUpVar ((ps,l):r) id | not (null x) = head x
>
| otherwise
= lookUpVar r id
>
where x = [ (ps,rel,pt,t) |
>
(id’,rel,pt,t) <- l, id == id’ ]
>lookUpProc :: Proctab -> Ident -> (Pst,Reladr,Partyp,Label,Parlength,[Pardef])
>lookUpProc []
id = error ("undeclared procedure " ++ id)
>lookUpProc ((ps,l):r) id | not (null x) = head x
>
| otherwise
= lookUpProc r id
>
where x = [ (ps,rel,pt,lb,pl,pm) |
>
(id’,rel,pt,lb,pl,pm) <- l, id == id’ ]
>isVar
>isVar
>isVar
>isVar
>
:: Vartab -> Int
[]
l id
vars
0 id
((ps,vs):r) l id
>isProc
>isProc
>isProc
>isProc
>
>
:: Proctab ->
[]
l
procs
0
((ps,vs):r) l
-> Ident -> Bool
= False
= False
| [ id’ | (id’,r,p,t) <- vs, id == id’ ] /= [] = True
| otherwise
= isVar r (l-1) id
Int -> Ident -> Bool
id = False
id = False
id | [ id’ | (id’,r,pt,lb,pl,pm) <- vs,
id == id’ ] /= []
| otherwise
= True
= isProc r (l-1) id
>typcheck2 :: Typ -> Typ -> Binop -> Typ
>typcheck2 Cint Cint b | b == Cplus
= Cint
>
| elem b [Cless,Cleq]
= Cbool
>typcheck2 x y b
= error "incompatible types"
>exprop2
>exprop2
>
>
>
>condop2
>condop2
>
>
>
>split
>split
>split
>split
:: Binop -> (Expr1->Expr1->Expr1)
op | not (null x) = head x
| otherwise
= error "illegal arithmetic expression"
where x = [ il1op | (op’,il1op) <- l, op == op’]
l = [(Cplus,Addop1)]
:: Binop -> (Expr1->Expr1->Cond1)
op | not (null x) = head x
| otherwise
= error "expression is not a condition"
where x = [ il1op | (op’,il1op) <- l, op == op’ ]
l = [(Cless,Lessop1),(Cleq,Leqop1)]
:: [Stat1] -> [Stat1]
(ProcDef l v p s : r)
(x : r)
[]
->
s1
s1
s1
[Stat1] -> ([Stat1],[Stat1])
s2 = split r (s1 ++ [ProcDef l v p s]) s2
s2 = split r s1 (s2 ++ [x])
s2 = (s1,s2)
>unique :: (Eq a) => [a] -> Bool
>unique l = mkset l == l
>numval :: (Num a, Read a) => String -> a
132
>numval cs = read cs
>mkset :: (Eq a) => [a] -> [a]
>mkset [] = []
>mkset (a:x) = a:filter (/=a) (mkset x)
----- Testdaten ------------------------------------------------------p1 = Program
Var x : Int;
Var y : Int;
Var z : Int;
Read x;
Read y;
z := x;
While (z<=y) Do
Write z;
z := z+1
End
End
>p1 = Cprog (Cdeclseq (Cdeclseq
>
(Cdecl "x" Cint)
>
(Cdecl "y" Cint))
>
(Cdecl "z" Cint))
>
(Cseq (Cseq (Cseq
>
(Cread "x")
>
(Cread "y"))
>
(Cassign "z" (Cvar "x")))
>
(Cwhile (Cbinop (Cvar "z") Cleq (Cvar "y"))
>
(Cseq (Cwrite (Cvar "z"))
>
(Cassign "z" (Cbinop (Cvar "z") Cplus (Cconst "1"))))))
p2 = Program
Var x : Int;
Proc P(a: Int)
Write a
End;
x := 5;
P(x)
End
>p2 = Cprog (Cdeclseq
>
(Cdecl "x" Cint)
>
(Cproc "P" (Cpar Cvalpar "a" Cint) Cemptydecl
>
(Cwrite (Cvar "a"))))
>
(Cseq
>
(Cassign "x" (Cconst "5"))
>
(Ccall "P" (Carg (Cvar "x"))))
133
---------- Testlaeufe -------------------------------------------------Semana> sem_ana p1
Iprog1 3 [Read1 (Var 0 0 IsValue),
Read1 (Var 0 1 IsValue),
Assign1 (Var 0 2 IsValue) (VarExp (Var 0 0 IsValue)),
While 1000 (Leqop1 (VarExp (Var 0 2 IsValue))
(VarExp (Var 0 1 IsValue)))
[Write1 (VarExp (Var 0 2 IsValue)),
Assign1 (Var 0 2 IsValue) (Addop1 (VarExp (Var 0 2 IsValue))
(Const 1))] 1001]
Semana> sem_ana p2
Iprog1 1 [ProcDef 1000 0 1
[Write1 (VarExp (Var 1 (-3) IsValue))],
Assign1 (Var 0 0 IsValue) (Const 5),
Call (Proc 1000 1 1) [ValPar (VarExp (Var 0 0 IsValue))]]
>
Semsyn.lhs
Semantische Synthese - sem_syn : IL1 -> IL2
(Implementierungsabbildung)
-------------------------------------------->module Semsyn (sem_syn) where
>import IL1
>import IL2 hiding (Label)
----------------------------------------------------------------------Semantische Synthese
----------------------------------------------------------------------Die Implementierungsabbildung impl wird realisiert durch die
folgende Familie von impl_-Funktionen:
>impl_prog ::
>impl_stat ::
>impl_var
::
>impl_expr ::
>impl_cond ::
>impl_aktpar::
Iprog1
Pst ->
Pst ->
Pst ->
Pst ->
Pst ->
-> Iprog2
Stat1 ->
Var1
->
Expr1 ->
Cond1 ->
Aktpar ->
[Stat2]
Var2
Expr2
Cond2
[Stat2]
Fuer stat bis aktpar wird im ersten Argument als Kontextinformation
die aktuelle Prozedurschachtelungstiefe mitgegeben, die zur Bestimmung
der Basisadresse, der statischen Vorgaenger und zum Display-Aufbau
benoetigt wird.
>sem_syn = impl_prog
>impl_prog (Iprog1 vl ss)
>
= Iprog2 ([Ass TOP (C vl), Ass BR (C 0), Jump (-1)] ++
>
concatMap (impl_stat 0) ss1 ++ [DefLabel (-1)] ++
134
>
concatMap (impl_stat 0) ss2)
>
where (ss1, ss2) = splitProc ss
>
>impl_var aktpst (Var p r IsValue)
>
| p < aktpst = M (A (D(M(A (D BR) (C p))))(C r))
>
| p == aktpst = M (A (D BR) (C r))
>
>impl_var aktpst (Var p r IsVarPar)
>
= M(D(impl_var aktpst (Var p r IsValue)))
>
>impl_expr aktpst (Addop1 e1 e2)
>
= A (impl_expr aktpst e1) (impl_expr aktpst e2)
>impl_expr aktpst (Const n) = C n
>impl_expr aktpst (VarExp v) = D (impl_var aktpst v)
>
>impl_cond aktpst (Lessop1 e1 e2)
>
= Lessop2 (impl_expr aktpst e1) (impl_expr aktpst e2)
>impl_cond aktpst (Leqop1 e1 e2)
>
= Leqop2 (impl_expr aktpst e1) (impl_expr aktpst e2)
>
>impl_aktpar aktpst (ValPar e)
>
= [Ass Push (impl_expr aktpst e)]
>impl_aktpar aktpst (VarPar v)
>
= [Ass Push a | M a <- [impl_var aktpst v]]
>impl_aktpar aktpst (ProcPar (Proc l pl p))
>
= [Ass Push (LabExp l), Ass Push sv]
>
where sv | p <= aktpst
= D (M (A (D BR) (C (p-1))))
>
| p == aktpst+1 = (D BR)
>impl_aktpar aktpst (ProcPar (FormProcPar pl p r))
>
= [Ass Push l, Ass Push sv]
>
where sv = D (impl_var aktpst (Var p (r+1) IsValue))
>
l = D (impl_var aktpst (Var p
r
IsValue))
>
>impl_stat aktpst (Assign1 v e)
>
= [Ass (impl_var aktpst v) (impl_expr aktpst e)]
>
>impl_stat aktpst (IfThen c ss l)
>
= [Cjump False l (impl_cond aktpst c)] ++
>
concatMap (impl_stat aktpst) ss ++ [DefLabel l]
>
>impl_stat aktpst (While lt c ss le)
>
= [DefLabel lt, Cjump False le (impl_cond aktpst c)] ++
>
concatMap (impl_stat aktpst) ss ++ [Jump lt, DefLabel le]
>
>impl_stat aktpst (ProcDef l vl p ss)
>
= concatMap (impl_stat p) ss1 ++ [DefLabel l] ++
>
setBR ++ display ++ allocvars ++
>
concatMap (impl_stat p) ss2 ++ [deallocate, Return]
>
where setBR = [Ass BR (A (D TOP) (C 1))]
>
display = [Ass Push (D (M (A (D HR) (C i)))) |
>
i<-[0..(p-2)]] ++ [Ass Push (D HR)]
>
allocvars = [Ass TOP (A(D TOP) (C vl))]
>
deallocate = Ass TOP (A (D BR) (C (-1)))
>
(ss1, ss2) = splitProc ss
>
135
>impl_stat
>
>
>
>
>
>impl_stat
>
>
>
>
>
>impl_stat
>
>
>impl_stat
>
>
>pushDV
>restoreDV
>popPars s
aktpst (Call(Proc l pl p) pars)
= concatMap (impl_aktpar aktpst) pars ++
[pushDV, loadSV, Pcall l, restoreDV, popPars pl]
where loadSV | p <= aktpst
= Ass HR (D(M(A (D BR) (C(p-1)))))
| p == aktpst+1 = Ass HR (D BR)
aktpst (Call(FormProcPar pl p r) pars)
= concatMap (impl_aktpar aktpst) pars ++
[pushDV, loadSV, call, restoreDV, popPars pl]
where loadSV = Ass HR (D(impl_var aktpst (Var p (r+1) IsValue)))
call
= FPcall (D(impl_var aktpst (Var p r IsValue)))
aktpst (Write1 e)
= [Write2 (impl_expr aktpst e)]
aktpst (Read1 v)
= [Read2 (impl_var aktpst v)]
= Ass Push (D BR)
= Ass BR Pop
= Ass TOP (A (D TOP) (C(-s)))
>splitProc []
=
>splitProc (ProcDef w x y z : r) =
>
>splitProc x
=
([],[])
(ProcDef w x y z : r1, r2)
where (r1,r2) = splitProc r
([], x)
---------- Testdaten ------------------------------------------------->v1 = Var 1 1000 IsValue
>v2 = Var 5 5000 IsValue
>a1 = Var 4 (-9) IsValue
>a2 = Var 4 (-3) IsVarPar
>s1 = Assign1 v2 (VarExp v2)
>s2 = Assign1 v1 (Const 99)
>c1 = Leqop1 (VarExp a1) (VarExp a2)
>d1 = ProcDef 88 100 2 [ s2 ]
>pd1 = Proc 88 3 2
>pd2 = FormProcPar 4 2 (-7)
>i0 = Iprog1 10 [Assign1 v1 (VarExp v2), Assign1 v2 (VarExp v1),
>
Assign1 a1 (VarExp v2), Assign1 a2 (VarExp v2)]
>i1 = Iprog1 10 [While 333 c1 [s1] 334]
>i2 = Iprog1 10 [d1, Call pd1 [ValPar (VarExp v2), (VarPar v2), ValPar (VarExp a2)]]
>i3 = Iprog1 10 [d1, Call pd2 [ProcPar pd1, ProcPar pd2]]
>ip (Iprog1 vl ss) = putStr (showil2 (Iprog2 (concatMap (impl_stat 5) ss)))
---------- Testlaeufe ------------------------------------------------Semsyn> ip i1
Iprog2:
DefLabel 333
Cjump False 334 (Leqop2 (D (M (A (D (M (A (D BR) (C 4)))) (C (-9)))))
136
(D (M (D (M (A (D (M (A (D BR) (C 4)))) (C (-3))))))))
Ass (M (A (D BR) (C 5000))) (D (M (A (D BR) (C 5000))))
Jump 333
DefLabel 334
Semsyn> ip i2
Iprog2:
DefLabel 88
Ass BR (A (D TOP) (C 1))
Ass Push (D (M (A (D HR) (C 0))))
Ass Push (D HR)
Ass TOP (A (D TOP) (C 100))
Ass (M (A (D (M (A (D BR) (C 1)))) (C 1000))) (C 99)
Ass TOP (A (D BR) (C (-1)))
Return
Ass Push (D (M (A (D BR) (C 5000))))
Ass Push (A (D BR) (C 5000))
Ass Push (D (M (D (M (A (D (M (A (D BR) (C 4)))) (C (-3)))))))
Ass Push (D BR)
Ass HR (D (M (A (D BR) (C 1))))
Pcall 88
Ass BR Pop
Ass TOP (A (D TOP) (C (-3)))
Semsyn> ip i3
Iprog2:
DefLabel 88
Ass BR (A (D TOP) (C 1))
Ass Push (D (M (A (D HR) (C 0))))
Ass Push (D HR)
Ass TOP (A (D TOP) (C 100))
Ass (M (A (D (M (A (D BR) (C 1)))) (C 1000))) (C 99)
Ass TOP (A (D BR) (C (-1)))
Return
Ass Push (LabExp 88)
Ass Push (D (M (A (D BR) (C 1))))
Ass Push (D (M (A (D (M (A (D BR) (C 2)))) (C (-7)))))
Ass Push (D (M (A (D (M (A (D BR) (C 2)))) (C (-6)))))
Ass Push (D BR)
Ass HR (D (M (A (D (M (A (D BR) (C 2)))) (C (-6)))))
FPcall (D (M (A (D (M (A (D BR) (C 2)))) (C (-7)))))
Ass BR Pop
Ass TOP (A (D TOP) (C (-4)))
>
Codesel.lhs
Code-Selektion - code_sel : IL2 -> TL
---------------------------------------
>module Codesel (code_sel, p_instr, lined) where
137
>import IL2
>import TL hiding (Label)
----------------------------------------------------------------------Baumparser fuer die Code-Selektion
----------------------------------------------------------------------Modifizierte Version:
- Endliche Anzahl von Loesungen durch Unterbrechung
der Kettenproduktionen in der Baumgrammatik.
- Ausschluss uninteressanter Loesungen.
>choices
>choices
>choices
>choices
:: [[a]]
[]
[xs]
y
-> [[a]]
= []
= [[x] | x <- xs]
= diag y
diag diagonalisiert eine Liste von Listen. pairDiag tut dies fuer zwei
Listen und gibt das Ergebnis als Liste von Paaren zurueck. Hiermit
kann gewaehrleistet werden, dass die Listenbeschreibungen der Parser
im Falle unendlich vieler Loesungen dennoch einen Strom aller
Loesungen liefern (in Miranda kann dies ueber diagonalisierende
Listenbeschreibungen erreicht werden).
>diag :: [[a]] -> [[a]]
>diag a = (foldr (diag2 []) [[]] a) where
>
>
>
>
>
diag2
diag2
diag2
diag2
diag2
::
[]
_
zs
zs
[a] ->
[]
_
(x:xs)
[]
[a] ->
_
[]
ys
(_:ys)
[[a]] -> [[a]]
= []
= []
= (zipWith (:) (x:zs) ys) ++ diag2 (x:zs) xs ys
= (zipWith (:) zs
ys) ++ diag2 zs
[] ys
>pairDiag x = map (makePair) y where
> makePair t = (t!!0, t!!1)
> y
= diag x
mix wird statt ++ verwendet, um Listen elementweise zu mischen.
>mix::[a] -> [a] -> [a]
>infixr 9 ‘mix‘
>[]
‘mix‘ ys
= ys
>(x:xs) ‘mix‘ ys
= x:(ys ‘mix‘ xs)
noSideEff ueberprueft Operanden auf Freiheit von Seiteneffekten
durch Verwendung von Postdec/Preinc. Wird im Zusammenhang mit
ADD-Befehl benoetigt.
>noSideEff :: Opnds -> Bool
>noSideEff (Preinc)
>noSideEff (Postdec)
= False
= False
138
>noSideEff
>noSideEff
>noSideEff
>noSideEff
>noSideEff
(Tmp r i)
=
(Tmpdef r i)
=
(TBdisp r i d)
=
(TBdispdef r i d) =
otherOps
=
>noSideEff’ (Tmov o)
>noSideEff’ (Tmea o)
>noSideEff’ (Tadd o1 o2)
noSideEff’
noSideEff’
noSideEff’
noSideEff’
True
i
i
i
i
= noSideEff o
= noSideEff o
= noSideEff o1 && noSideEff o2
Die Modifikationen ergeben die folgende Grammatik:
Tprog -> Iprog2 [ Instr ]
Instr -> Ass Sop Op_1 | Ass Sop Ea | Ass Sop (A Sop Op_1) |
Cjump True Label Cc
| Cjump False Label Cc |
Jump Label
| FPcall Ea | Pcall Label
Return
| DefLabel Label
Cc ->
Lessop2 Op_1 Op_1
| Leqop2 Op_1 Op_1
Ir ->
Op_2 | Ea_2 | A Trg Op_1
Durch Aufspaltung von Op in Op1, Op2 wird der Zyklus Op -> Ir -> Op gebrochen:
fuer i = 1,2:
Op_i -> C Num | D
D (M (A (D
D (M (D (M
Pop |
D (M Ir)
Op_1 -> Ir
Trg ->
D Regno
Regno | D (M (D Regno)) |
Regno) (C Disp))) |
(A (D Regno) (C Disp))))) |
| A Ir (C Disp)
| D (M (A Ir (C Disp)))
| Ir
Sop ->
Regno | M (D Regno) | M (A (D Regno) (C Disp))
M (D (M (A (D Regno) (C Disp)))) | Push |
M Ir | M (A Ir (C Disp)) | M (D (M (A Ir (C Disp))))
|
Ea wird in Ea_1 (fuer JSRI) und Ea_2 fuer MEA und Tmea aufgespalten.
Die Produktionen zu Regdef, Tmpdef, Bdispdef und TBdispdef wurden
bei ea2 entfernt, weil sich Tmea (Regdef i) stets gleichwertig durch
Tmov (Reg i) etc. ersetzen laesst.
fuer i = 1,2:
Ea_i ->
A (D Regno) (C Disp) | A Ir (C Disp)
Ea_1 ->
D Regno | Ir | D (M (A (D R) (C Disp))) | D (M (A Ir (C Disp)))
Regno -> Hr
| Br
| Top
Deklaration der Parser:
>type Treeparser a b = (a -> [b])
>p_tprog :: Treeparser Iprog2 Tprog
139
>p_instr
>p_cc
>p_ir
>p_op
>p_sop
>p_trg
>p_ea
>p_regno
::
::
::
::
::
::
::
::
Treeparser Stat2 Instr
Treeparser Cond2 Cc
Treeparser Expr2 Ir
Int -> Treeparser Expr2 Op
Treeparser Var2 Sop
Treeparser Expr2 Trg
Int -> Treeparser Expr2 Ea
Treeparser Var2 Regno
>code_sel = choice . p_tprog
-- Codeauswahl
>choice
-- Codeauswahl-Funktion
= head
>p_tprog (Iprog2 ss) = [Tprog is | is <- choices (map p_instr ss)]
>p_instr (Ass t1 (A t2 s))
>
= ([ADD u v1 | t2 == D t1, (u,v1) <- pairDiag ([(p_op 1 s)] ++ [vs]), noSideEff v1] ‘mix‘
>
-- x := x + d
>
[MEA a v | (a, v) <- pairDiag ([p_ea 2 (A t2 s)] ++ [vs])]) ++
>
[MOV u v | (u, v) <- pairDiag ([p_op 1 (A t2 s)] ++ [vs])]
>
where vs = p_sop t1
>p_instr (Ass Push (LabExp l))
>
= [MovLab l]
>p_instr (Ass t s)
>
= [MEA a v | (a, v) <- pairDiag ([p_ea 2 s] ++ [vs])] ++
>
[MOV u v | (u, v) <- pairDiag ([p_op 1 s] ++ [vs])]
>
where vs = p_sop t
>
>p_instr (Write2 s) = [Out u | u <- p_op 1 s]
>p_instr (Read2 t) = [In v | v <- p_sop t]
>
>p_instr (Cjump True l b) = [JT l c | c <- p_cc b]
>p_instr (Cjump False l b) = [JF l c | c <- p_cc b]
>
>p_instr (Jump l)
= [JMP l]
>p_instr (FPcall a)
= [JSRI x | x <- p_ea 1 a]
>p_instr (Pcall l)
= [JSR l]
>p_instr Return
= [RET]
>p_instr (DefLabel l) = [DefLab l]
>p_instr x = []
>p_cc (Lessop2 s2 s1) = [CLT u v | (u, v) <- pairDiag ([p_op 1 s1] ++ [p_op 1 s2])]
>p_cc (Leqop2 s2 s1) = [CLEQ u v | (u, v) <- pairDiag ([p_op 1 s1] ++ [p_op 1 s2])]
>p_cc w = []
>p_ir (A t s)
>
= ([Tadd u v | (v, u) <- pairDiag ([p_trg t] ++ [p_op 1 s])]
>
‘mix‘ [Tmea u | u <- p_ea 2 (A t s)])
>
++ [Tmov u | u <- p_op 2 (A t s)]
>
>
>p_ir x = [Tmea u | u <- p_ea 2 x]
>
++ [Tmov u | u <- p_op 2 x]
140
>p_op k (D(M(D(M(A (D r) (C d))))))
>
= [Bdispdef u d | u <- p_regno r]
>
++ [TBdispdef 3 u d | u <- p_ir (D r)]
>
++ [Tmpdef 3 i | i<- p_ir (D(M(A (D r) (C d))))]
>
++ [Tmp 3 i | k==1, i <- p_ir (D(M(D(M(A (D r) (C d))))))]
>p_op k (D(M(D(M(A o (C d))))))
>
= [TBdispdef 3 u d | u <- p_ir o]
>
++ [Tmpdef 3 i | i<- p_ir (D(M(A o (C d))))]
>
++ [Tmp 3 i | k==1, i <- p_ir (D(M(D(M(A o (C d))))))]
>p_op k (D(M(A (D r) (C d))))
>
= [Bdisp u d | u <- p_regno r]
>
++ [TBdisp 3 i d | i <- p_ir (D r)]
>
++ [Tmpdef 3 i | i <- p_ir (A (D r) (C d))]
>
++ [Tmp 3 i | k==1, i <- p_ir (D(M(A (D r) (C d))))]
>p_op k (D(M(A o (C d))))
>
= [TBdisp 3 i d | i <- p_ir o]
>
++ [Tmpdef 3 i | i <- p_ir (A o (C d))]
>
++ [Tmp 3 i | k==1, i <- p_ir (D(M(A o (C d))))]
>p_op k (D(M(D r)))
>
= [Regdef u | u <- p_regno r]
>
++ [Tmpdef 3 i | i <- p_ir (D r)]
>
++ [Tmp 3 i | k==1, i<- p_ir (D(M(D r)))]
>p_op k (C n)
>
= [Imm n]
>
++[Tmp 3 i | k==1, i <- p_ir (C n)]
>p_op k Pop = [Postdec]
>
++ [Tmp 3 i | k==1, i <- p_ir Pop]
>p_op
>
>
>
>
>p_op
>
>
>
>p_op
>
>
>
>p_op
k (D(M (A x (C
= [TBdispdef
++ [Tmpdef
++ [Tmp 3
d))))
3 i d | i <- p_ir x]
3 i | i <- p_ir (A x (C d))]
i | i<- p_ir (D(M (A x (C d))))]
k (D(M x))
= [Tmpdef 3 i | i <- p_ir x]
++ [Tmp 3 i | k==1, i <- p_ir (D(M x))]
k (D r)
= [Reg u | u <- p_regno r]
++ [Tmp 3 i | k==1, i <- p_ir (D r)]
k x = [Tmp 3 i | k==1, i <- p_ir x]
>p_trg (D r) = [Tmp 3 i | i <- p_ir (D r)]
>p_trg x = [Tmp 3 i | i <- p_ir x]
>p_sop (M(D(M(A (D r) (C d)))))
141
>
>
>
= [Bdispdef u d | u<- p_regno r]
++ [TBdispdef 3 u d | u <- p_ir (D r)]
++ [Tmpdef 3 u | u <- p_ir (D(M(A (D r) (C d))))]
>p_sop (M(D(M(A i (C d)))))
>
= [TBdispdef 3 u d | u <- p_ir i]
>
++ [Tmpdef 3 u | u <- p_ir (D(M(A i (C d))))]
>p_sop
>
>
>p_sop
>
>
>
>p_sop
>
>
>
>p_sop
>
>
>p_sop
>
>p_sop
>p_ea
>
>
>
>p_ea
>
>
>p_ea
>
>
>
>p_ea
>
>
>p_ea
>
>
>p_ea
(M (D r))
= [Regdef u | u<- p_regno r]
++ [Tmpdef 3 u | u <- p_ir (D r)]
(M(A (D r) (C d)))
= [Bdisp u d | u<- p_regno r]
++ [TBdisp 3 u d | u<- p_ir (D r)]
++ [Tmpdef 3 u | u <- p_ir (A (D r) (C d))]
(M(A i (C d)))
= [TBdisp 3 u d | u<- p_ir i]
++ [Tmpdef 3 u | u <- p_ir (A i (C d))]
(M i)
= [Tmpdef 3 u | u <- p_ir i]
Push
= [Preinc]
r = [Reg u | u<- p_regno r] -- schliesst Sackgassenklausel ein
k (D (M (A (D r) (C d))))
= [Bdispdef u d| k == 1, u<-p_regno r]
++ [TBdispdef 3 i d | k==1, i <- p_ir (D r)]
++ [Tmpdef 3 u | k == 1, u <- p_ir (D (M (A (D r) (C d))))]
k (D (M (A i (C d))))
= [TBdispdef 3 u d | k==1, u <- p_ir i]
++ [Tmpdef 3 u | k==1, u <- p_ir (D (M (A i (C d))))]
k (A (D r) (C d))
= [Bdisp u d | u<- p_regno r]
++ [TBdisp 3 i d | i <- p_ir (D r)]
++ [Tmpdef 3 u | k==1, u <- p_ir (A (D r) (C d))]
k (A i (C d))
= [TBdisp 3 u d | u <- p_ir i]
++ [Tmpdef 3 u | k == 1, u <- p_ir (A i (C d))]
k (D r)
= [Regdef u |k==1, u <- p_regno r]
k i = [Tmpdef 3 u | k == 1, u <- p_ir i] -- schliesst Sackgassenklausel ein
>p_regno
>p_regno
>p_regno
>p_regno
HR
BR
TOP
w
=
=
=
=
[4]
[5]
[6]
[]
---------- Testdaten ------------------------------------------------->e1
=
A (D BR) (C 42)
142
>e2
>e11
>v1
>v2
>r =
>d =
>i1
>i2
>i3
>i4
= A (D HR) (D TOP)
= A e1 e1
= M e1
= (C 42)
BR
11
=
=
=
=
Ass
Ass
Ass
Ass
BR
(M
BR
BR
(A
(A
(D
(A
(D HR) (C 99))
(D BR) (C 333))) (A (D(M (A (D BR) (C 333)))) (C 42))
BR)
(C 44) (C 55))
>test n d = putStr ("First " ++ show n ++ " for: " ++ show d ++ "\n"
>
++ concatMap lined [is| Tprog is <- take n (p_tprog (Iprog2 [d]))])
>lined:: [Instr] -> String
>lined = (++ "\n") . concatMap ((++ " ") . show)
---------- Testlaeufe ------------------------------------------------Codesel> test 8 i1
First 8 for: Ass BR (A (D HR) (C 99))
MEA (Bdisp 4 99) (Reg 5)
MEA (TBdisp 3 (Tmov (Reg 4)) 99) (Reg 5)
MOV (Tmp 3 (Tadd (Imm 99) (Tmp 3 (Tmov (Reg 4))))) (Reg 5)
MOV (Tmp 3 (Tmea (Bdisp 4 99))) (Reg 5)
MOV (Tmp 3 (Tadd (Tmp 3 (Tmov (Imm 99))) (Tmp 3 (Tmov (Reg 4))))) (Reg 5)
MOV (Tmp 3 (Tmea (TBdisp 3 (Tmov (Reg 4)) 99))) (Reg 5)
Codesel> test 8 i2
First 8 for: Ass (M (A (D BR) (C 333))) (A (D (M (A (D BR) (C 333)))) (C 42))
ADD (Imm 42) (Bdisp 5 333)
MEA (TBdisp 3 (Tmov (Bdisp 5 333)) 42) (Bdisp 5 333)
ADD (Imm 42) (TBdisp 3 (Tmov (Reg 5)) 333)
MEA (TBdisp 3 (Tmov (Bdisp 5 333)) 42) (TBdisp 3 (Tmov (Reg 5)) 333)
ADD (Tmp 3 (Tmov (Imm 42))) (Bdisp 5 333)
MEA (TBdisp 3 (Tmov (TBdisp 3 (Tmov (Reg 5)) 333)) 42) (Bdisp 5 333)
ADD (Imm 42) (Tmpdef 3 (Tadd (Imm 333) (Tmp 3 (Tmov (Reg 5)))))
MEA (TBdisp 3 (Tmov (Bdisp 5 333)) 42) (Tmpdef 3 (Tadd (Imm 333)
(Tmp 3 (Tmov (Reg 5)))))
Codesel> test 8 i3
First 8 for: Ass BR (D BR)
MOV (Reg 5) (Reg 5)
MOV (Tmp 3 (Tmov (Reg 5))) (Reg 5)
Codesel> test 8 i4
First 8 for: Ass BR (A (C 44) (C 55))
MEA (TBdisp 3 (Tmov (Imm 44)) 55) (Reg 5)
MOV (Tmp 3 (Tadd (Imm 55) (Tmp 3 (Tmov (Imm 44))))) (Reg 5)
MOV (Tmp 3 (Tmea (TBdisp 3 (Tmov (Imm 44)) 55))) (Reg 5)
MOV (Tmp 3 (Tadd (Tmp 3 (Tmov (Imm 55))) (Tmp 3 (Tmov (Imm 44))))) (Reg 5)
>
143
Alloc.lhs
Registerallokation - reg_alloc : TL -> TL und
Linearisierung
- code_lin : TL -> TL
------------------------------------------------>module Alloc (reg_alloc, code_lin) where
>import IL2
>import TL
hiding (Label)
>import Codesel
----------------------------------------------------------------------Linearisierung
----------------------------------------------------------------------Linearisierung von TL-Baeumen in TL-Befehlslisten.
>stack :: Int
>stack = 77
-- signalisiert Stack-Allokation
>code_lin = lin_tprog
>lin_tprog:: Tprog -> Tprog
>lin_tprog (Tprog is) = Tprog (concatMap lin_instr is)
>lin_instr:: Instr ->
>lin_instr (MOV s t)
>
>
>lin_instr (MEA s t)
>
>
>lin_instr (ADD s t)
>
>
>
>
>lin_instr (JF l cmp)
>
>
>
>lin_instr (JT l cmp)
>
>
>
>lin_instr (JSRI s)
>
>lin_instr (Out s)
>
>lin_instr (In t)
>
>lin_instr (JMP l)
>lin_instr (JSR l)
[Instr]
= tcode ++ scode ++ [MOV as at]
where (tcode,at) = lin_opnds t
(scode,as) = lin_opnds s
= tcode ++ scode ++ [MEA as at]
where (tcode,at) = lin_opnds t
(scode,as) = lin_opnds s
= tcode ++ scode ++ [ADD as (stacktransform at)]
where (tcode,at) = lin_opnds t
(scode,as) = lin_opnds s
stacktransform Preinc = Regdef 6
stacktransform op = op
= s2code ++ s1code ++ [JF l (c as1 as2)]
where (s1code,as1) = lin_opnds s1
(s2code,as2) = lin_opnds s2
(c,s1,s2) = getCC cmp
= s2code ++ s1code ++ [JT l (c as1 as2)]
where (s1code,as1) = lin_opnds s1
(s2code,as2) = lin_opnds s2
(c,s1,s2) = getCC cmp
= scode ++ [JSRI as]
where (scode,as) = lin_opnds s
= scode ++ [Out as]
where (scode,as) = lin_opnds s
= tcode ++ [In at]
where (tcode,at) = lin_opnds t
= [JMP l]
= [JSR l]
144
>lin_instr
>lin_instr
>lin_instr
>lin_instr
(RET)
= [RET]
(MovLab l) = [MovLab l]
(DefLab l) = [DefLab l]
x
= error ("lin_instr : instr = " ++ show x)
>lin_opnds
>lin_opnds
>
>
>lin_opnds
>
>lin_opnds
>
>lin_opnds
>
>lin_opnds
:: Opnds -> ([Instr], Opnds)
(Tmp r i)
| r == stack = (icode ++ [f Preinc], Postdec)
| otherwise = (icode ++ [f (Reg r)], Reg r)
where (icode,f) = lin_ir i
(Tmpdef r i)
= (icode ++ [f (Reg r)], Regdef r)
where (icode,f) = lin_ir i
(TBdisp r i d)
= (icode ++ [f (Reg r)], Bdisp r d)
where (icode,f) = lin_ir i
(TBdispdef r i d) = (icode ++ [f (Reg r)], Bdispdef r d)
where (icode,f) = lin_ir i
anyother = ([],anyother)
>lin_ir:: Ir -> ([Instr],(Opnds -> Instr))
>lin_ir (Tmov s)
= (scode, MOV as)
>
where (scode,as) = lin_opnds
>lin_ir (Tmea s)
= (scode, MEA as)
>
where (scode,as) = lin_opnds
>lin_ir (Tadd s t) = (tcode ++ scode, addtmp as)
>
where (scode,as) = lin_opnds
>
(tcode,at) = lin_opnds
>
addtmp q Preinc = ADD
>
addtmp q y
= ADD
s
s
s
t
q (Regdef 6)
q y
Zerlegen eines Condition-Code-Befehls
>getCC :: Cc -> (Op->Op->Cc, Op, Op)
>getCC (CLT s1 s2) = (CLT,s1,s2)
>getCC (CLEQ s1 s2) = (CLEQ,s1,s2)
----------------------------------------------------------------------Registerallokation
----------------------------------------------------------------------Die Register 0,1,2,3 sind verfuegbar. Register 4 bleibt zunaechst
reserviert, um Adressierungsengpaesse zu vermeiden.
>allregs:: [Int]
>allregs = [0,1,2,3]
>data Position = Sticky | Transient
>reg_alloc = a_tprog
>a_tprog :: Tprog -> Tprog
>a_tprog (Tprog is) = Tprog (map a_instr is)
145
>a_instr
>a_instr
>
>
>a_instr
>
>
>a_instr
>
>
>a_instr
>
>
>
>a_instr
>
>
>
>a_instr
>
>a_instr
>
>a_instr
>
>a_instr
>a_instr
>a_instr
>a_instr
>a_instr
>a_instr
:: Instr -> Instr
(MOV s t) = MOV as at
where (at,rs) = a_opnds t allregs
Sticky
(as, x) = a_opnds s rs
Transient
(MEA s t) = MEA as at
where (at,rs) = a_opnds t allregs
Sticky
(as, x) = a_opnds s rs
Transient
(ADD s t) = ADD as at
where (at,rs) = a_opnds t allregs
Sticky
(as, x) = a_opnds s rs
Transient
(JF l cmp) = JF l (c as1 as2)
where (as2,rs) = a_opnds s2 allregs Sticky
(as1, x) = a_opnds s1 rs
Transient
(c,s1,s2) = getCC cmp
(JT l cmp) = JT l (c as1 as2)
where (as2,rs) = a_opnds s2 allregs Sticky
(as1, x) = a_opnds s1 rs
Transient
(c,s1,s2) = getCC cmp
(JSRI s)
= JSRI as
where (as, x) = a_opnds s allregs
Transient
(Out s)
= Out as
where (as, x) = a_opnds s allregs
Transient
(In t)
= In at
where (at, x) = a_opnds t allregs
Transient
(JMP l)
= JMP l
(JSR l)
= JSR l
(RET)
= RET
(MovLab l) = MovLab l
(DefLab l) = DefLab l
x
= error ("a_instr : instr = " ++ show x)
>a_opnds :: Opnds -> [Regno] -> Position -> (Opnds,[Regno])
>a_opnds (Tmp n i) [] Sticky
>a_opnds (Tmp n i) [] Transient
>a_opnds (Tmp n i) (r:rs) p
= (Tmp stack (a_ir i []),[])
= (Tmp 4 (a_ir i []),[])
= (Tmp r (a_ir i (r:rs)),rs)
>a_opnds (Tmpdef n i) [] Sticky
>a_opnds (Tmpdef n i) [] Transient
>a_opnds (Tmpdef n i) (r:rs) p
= (Tmp stack (Tmov (Tmpdef 4 (a_ir i []))),[])
= (Tmpdef 4 (a_ir i []),[])
= (Tmpdef r (a_ir i (r:rs)),rs)
>a_opnds (TBdisp n i d) [] Sticky
= (Tmp stack (Tmov (TBdisp 4 (a_ir i []) d)),[])
>a_opnds (TBdisp n i d) [] Transient = (TBdisp 4 (a_ir i []) d,[])
>a_opnds (TBdisp n i d) (r:rs) p
= (TBdisp r (a_ir i (r:rs)) d,rs)
>a_opnds (TBdispdef n i d) [] Sticky
= (Tmp stack (Tmov (TBdispdef 4 (a_ir i []) d)),[])
>a_opnds (TBdispdef n i d) [] Transient = (TBdispdef 4 (a_ir i []) d,[])
>a_opnds (TBdispdef n i d) (r:rs) p
= (TBdispdef r (a_ir i (r:rs)) d,rs)
>a_opnds
>a_opnds
>a_opnds
>a_opnds
>a_opnds
(Imm n)
(Reg n)
(Regdef n)
(Bdisp n d)
(Bdispdef n d)
rs
rs
rs
rs
rs
p
p
p
p
p
=
=
=
=
=
(Imm n, rs)
(Reg n, rs)
(Regdef n, rs)
(Bdisp n d, rs)
(Bdispdef n d, rs)
146
>a_opnds (Preinc)
rs p = (Preinc, rs)
>a_opnds (Postdec)
rs p = (Postdec, rs)
>a_opnds x rs p = error ("a_opnds : op = " ++ show x ++ ", regs = " ++ show rs)
>a_ir
>a_ir
>
>a_ir
>
>a_ir
>
>
>
:: Ir -> [Regno] -> Ir
(Tmov x)
rs = Tmov ax
where (ax,rs’) = a_opnds x rs Transient
(Tmea x)
rs = Tmea ax
where (ax,rs’) = a_opnds x rs Transient
(Tadd x y) rs = Tadd ax ay
where (ay,rs’) = a_opnds y rs pos
(ax,rs’’) = a_opnds x rs’ pos
pos
= prime x
>prime
>prime
>prime
>prime
>prime
>prime
>prime
>prime
>prime
(Reg r)
(Imm n)
(Bdisp r d)
(Bdispdef r d)
Preinc
Postdec
(Tmp r (Tadd s t))
(Tmp r (Tmea s))
others
=
=
=
=
=
=
=
=
=
Transient
Transient
Transient
Transient
Transient
Transient
prime s
prime s
Sticky
---------- Testdaten ------------------------------------------------->e1
>e2
>e11
>v1
>v2
>r =
>d =
= A (D BR) (C 42)
= A (D HR) (D TOP)
= A e1 e1
= M e1
= (C 42)
BR
11
>i2 = Ass (M (A (D BR) (C 333))) (A (D(M (A (D BR) (C 333)))) (C 42))
>i10 = Ass BR (A (C 1) (A (C 2) (A (C 3) (A (C 4) (A (C 5) (C 6))))))
>sal n ip = putStr (lpr [ lin_tprog (a_tprog (Tprog [t])) | t <- take n (p_instr ip) ])
>lpr:: [Tprog] -> String
>lpr ts = concat [lined is | Tprog is <- ts]
---------- Testlaeufe -------------------------------------------------Alloc> sal 4 i2
ADD (Imm 42) (Bdisp 5 333)
MOV (Bdisp 5 333) (Reg 0)
MEA (Bdisp 0 42) (Bdisp 5 333)
MOV (Imm 42) (Reg 0)
ADD (Reg 0) (Bdisp 5 333)
147
MOV (Reg 5) (Reg 0)
MOV (Bdisp 0 333) (Reg 0)
MEA (Bdisp 0 42) (Bdisp 5 333)
Alloc> sal 4 i10
MOV (Imm 1) (Reg
MOV (Imm 2) (Reg
MOV (Imm 3) (Reg
MOV (Imm 4) (Reg
MOV (Imm 5) (Reg
ADD (Imm 6) (Reg
ADD (Reg 4) (Reg
ADD (Reg 3) (Reg
ADD (Reg 2) (Reg
ADD (Reg 1) (Reg
MOV (Reg 0) (Reg
0)
1)
2)
3)
4)
4)
3)
2)
1)
0)
5)
MOV
MOV
MOV
MOV
MOV
MEA
ADD
ADD
ADD
ADD
MOV
(Imm 1) (Reg 0)
(Imm 2) (Reg 1)
(Imm 3) (Reg 2)
(Imm 4) (Reg 3)
(Imm 5) (Reg 4)
(Bdisp 4 6) Preinc
Postdec (Reg 3)
(Reg 3) (Reg 2)
(Reg 2) (Reg 1)
(Reg 1) (Reg 0)
(Reg 0) (Reg 5)
MOV
MOV
MOV
MOV
MOV
MOV
ADD
ADD
ADD
ADD
ADD
MOV
(Imm 1)
(Imm 2)
(Imm 3)
(Imm 4)
(Imm 5)
(Imm 6)
Postdec
Postdec
(Reg 3)
(Reg 2)
(Reg 1)
(Reg 0)
(Reg 0)
(Reg 1)
(Reg 2)
(Reg 3)
Preinc
Preinc
(Regdef 6)
(Reg 3)
(Reg 2)
(Reg 1)
(Reg 0)
(Reg 5)
>
Relabel.lhs
Entfernen der symbolischen Marken - relabel : TL -> TL
--------------------------------------------------------
>module Relabel (relabel) where
>import TL
hiding (Label)
148
Die symbolische Marken werden entfernt. In Spruengen werden die
symbolischen Sprungziele durch ihre absolute Programmadresse ersetzt.
Version : Programmierung mit Unbekannten
>type Label
= Int
>type InsCounter = Int
>type LabelInfo = [(Label, InsCounter)]
>relabel :: Tprog -> Tprog
>relabel (Tprog p)
>
= Tprog p’
>
where
>
(p’, info) = renum p 0
>
>
renum (JT l x : pr) ic
>
= (JT (lookup l) x : pr’, info’)
>
where (pr’, info’) = renum pr (ic+1)
>
renum (JF l x : pr) ic
>
= (JF (lookup l) x : pr’, info’)
>
where (pr’, info’) = renum pr (ic+1)
>
renum (JMP l : pr) ic
>
= (JMP (lookup l) : pr’, info’)
>
where (pr’, info’) = renum pr (ic+1)
>
renum (JSR l : pr) ic
>
= (JSR (lookup l) : pr’, info’)
>
where (pr’, info’) = renum pr (ic+1)
>
renum (MovLab l : pr) ic
>
= (MOV (Imm (lookup l)) Preinc : pr’, info’)
>
where (pr’, info’) = renum pr (ic+1)
>
renum (DefLab l : pr) ic
>
= (pr’, (l,ic):info’)
>
where (pr’, info’) = renum pr ic
>
renum (x : pr) ic
>
= (x : pr’, info’)
>
where (pr’, info’) = renum pr (ic+1)
>
renum [] ic = ([], [])
>
>
lookup l = head [ addr | (l’,addr) <- info, l == l’ ]
>module Minipas (
> module Minipas,
> module AST, module IL1, module IL2, module TL, module Synana,
> module Semana, module Semsyn, module Codesel, module Alloc,
> module Relabel) where
>import
>import
>import
>import
AST hiding (spc)
IL1 hiding (Label)
IL2 hiding (Label)
TL
>import Synana
-- ASCII -> AST
-----
Abstrakte Syntax
Zwischensprache 1
Zwischensprache 2
Zielsprache
(syn_ana
:: String -> Prog)
149
>import
>import
>import
>import
>
Semana
Semsyn
Codesel
Alloc
------
>import Relabel
>import Interpret
>top
>to1
>to2
>tom
>toa
>tol
>tor
=
=
=
=
=
=
=
showast
showil1
showil2
showtl
showtl
showtl
showtl
AST -> IL1
IL1 -> IL2
IL2 -> TL
TL -> TL
TL -> TL
(sem_ana
(sem_syn
(code_sel
(reg_alloc
(code_lin
::
::
::
::
::
Prog -> Prog1)
Iprog2 -> [Tprog])
Iprog2-> Tprog)
Tprog -> Tprog)
Tprog -> Tprog)
-- TL -> TL
(relabel
:: Tprog -> Tprog)
-- Interpreter fuer TL (inter :: Tprog -> IO ())
.
syn_ana
.
sem_ana.syn_ana
.
sem_syn.sem_ana.syn_ana
.
code_sel.sem_syn.sem_ana.syn_ana
.
reg_alloc.code_sel.sem_syn.sem_ana.syn_ana
.
code_lin.reg_alloc.code_sel.sem_syn.sem_ana.syn_ana
. relabel.code_lin.reg_alloc.code_sel.sem_syn.sem_ana.syn_ana
>compile :: String -> Tprog
>compile = relabel . code_lin . reg_alloc . code_sel . sem_syn . sem_ana . syn_ana
>run
>run
:: String -> IO ()
= inter . compile
-- Eingabe : Programm als String
>runf :: String -> IO ()
-- Eingabe : Dateiname als String
>runf f = do is <- readFile f
>
run is
---------- Testdaten ------------------------------------------------------->showCompile :: String -> IO ()
-- gibt das kompilierte Programm aus
>showCompile f = do is <- readFile f
>
case (compile is) of
>
Tprog p -> sequence_ (map (putStr . ((++) "\n") . show) p)
>pname i = "beispiele/program" ++ show i ++ ".mp"
>p i = do is <- readFile (i)
>
putStrLn (is)
---------- Testlaeufe ------------------------------------------------------Minipas> p (pname 2)
Program
Var x : Int;
Proc P(a: Int)
Write a
End;
x := 5;
P(x)
End
150
Minipas> runf (pname 2)
5
-----------------------------------------Minipas> p (pname 3)
Program
Var y : Int;
Var x : Int;
Proc P
x := x+1
End;
Read y;
x := 0;
While (x < y) Do
Write x;
P
End
End
Minipas> runf (pname 3)
5
0 1 2 3 4
-----------------------------------------Minipas> p (pname 5)
Program
Var y : Int;
Var x : Int;
Proc P
Proc Q
Write x;
x := x+1
End;
x := x+x+1;
Q
End;
Read y;
x := 0;
While (x < y) Do
Write x;
P
End
End
Minipas> runf (pname 5)
7
0 1 2 5 6 13
151
-----------------------------------------Minipas> p (pname 7)
Program
Var n : Int;
Var m : Int;
Proc T(Proc U)
Write 1;
m := 2;
n := n+m;
U
End;
Proc P(Proc F(Proc W))
Var n : Int;
Proc S
Write 2;
n := n+3
End;
Proc Q(Proc R)
Write 3;
n := n+7;
T(S);
Write n;
R
End;
Write 4;
n := 12;
F(S);
Q(S)
End;
Write 5;
n := 3;
P(T);
Write n;
Write m
End
Minipas> runf (pname 7)
5 4 1 2 3 1 2 25 2 7 2
-----------------------------------------Minipas> p (pname 9)
Program
Proc P1(Var a:Int)
Proc P2(Var a:Int, Var b:Int)
Proc P3(Var a:Int, Var b:Int, Var c:Int)
Proc P4(Var a:Int, Var b:Int, Var c:Int, Var d:Int)
Proc P5(Var a:Int, Var b:Int, Var c:Int, Var d:Int, Var e:Int)
Write a;
Write b;
Write c;
Write d;
Write e
152
End;
Var n:Int;
n:= 4;
P5(a,b,c,d,n);
Write a;
Write b;
Write c;
Write d
End;
Var n:Int;
n:= 3;
P4(a,b,c,n);
Write a;
Write b;
Write c
End;
Var n:Int;
n:= 2;
P3(a,b,n);
Write a;
Write b
End;
Var n:Int;
n:= 1;
P2(a,n);
Write a
End;
Var n:Int;
n:= 0;
P1(n)
End
Minipas> runf (pname 9)
0 1 2 3 4 0 1 2 3 0 1 2 0 1 0
-----------------------------------------Minipas>
Program
Var e
Var m
Var n
p (pname 10)
: Int;
: Int;
: Int;
Proc Mult(m:Int, n:Int, Var e:Int)
Var i : Int;
i := 0;
e := 0;
While (i < m) Do
153
e := e + n;
i := i + 1
End
End;
Read m;
Read n;
Mult (m, n, e);
Write e
End
Minipas> runf (pname 10)
4
7
28
154
Herunterladen