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