◦ ◦◦◦ ◦◦◦◦ ◦ ◦ ◦◦◦ ◦◦◦◦ ◦ ◦◦ Organisatorisches TECHNISCHE UNIVERSITÄT MÜNCHEN FAKULTÄT FÜR INFORMATIK Master oder Bachelor ab 6. Semester mit 5 ECTS Voraussetzungen Compilerbau I Informatik 1 & 2 Theoretische Informatik Technische Informatik Grundlegende Algorithmen Michael Petter, Axel Simon Aufbauende Vorlesungen Virtuelle Maschinen Programmoptimierung Programmiersprachen Praktikum Compilerbau Hauptseminare SoSe 2010 Material: Aufzeichnung der Vorlesungen über TTT die Folien selbst Literaturliste online (→ Bibliothek) Tools zur Visualisierung der Virtuellen Maschinen Tools, um Komponenten eines Compilers zu generieren 1 / 398 Organisatorisches 2 / 398 Vorläufiger Inhalt Grundlagen Reguläre Ausdrücke und Automaten Zeiten: Von der Spezifizierung mit mehreren Regulären Ausdrücken zur Implementation mit Automaten Vorlesung: Mi. 14:15-15:45 Uhr Übung: wird per Onlineabstimmung geklärt Reduzierte kontextfreie Grammatiken und Kellerautomaten Bottom-Up Syntaxanalyse Abschlussprüfung Attributsysteme Klausur, Termin über TUM-online Typüberprüfung Erfolgreiches Lösen der Aufgaben wird als Bonus von 0.3 angerechnet. Codegenerierung für Stackmaschinen Registerverteilung Einfache Optimierung 4 / 398 3 / 398 Interpreter Programm Themengebiet: Eingabe Einführung Interpreter Ausgabe Vorteil: Keine Vorberechnung auf dem Programmtext erforderlich ⇒ keine/geringe Startup-Zeit Nachteil: Während der Ausführung werden die Programm-Bestandteile immer wieder analysiert ⇒ längere Laufzeit 5 / 398 6 / 398 Prinzip eines Übersetzers: Übersetzer Übersetzer Programm Eine Vorberechnung auf dem Programm gestattet u.a. Code eine geschickte(re) Verwaltung der Variablen; Erkennung und Umsetzung globaler Optimierungsmöglichkeiten. Code Maschine Nachteil Ausgabe Die Übersetzung selbst dauert einige Zeit Eingabe Vorteil Zwei Phasen: 1 Übersetzung des Programm-Texts in ein Maschinen-Programm; 2 Ausführung des Maschinen-Programms auf der Eingabe. Die Ausführung des Programme wird effizienter ⇒ lohnt sich bei aufwendigen Programmen und solchen, die mehrmals laufen. 8 / 398 7 / 398 Übersetzer Übersetzer allgemeiner Aufbau eines Übersetzers: Analyse Analyse Interndarstellung Synthese Compiler Programmtext Code Interndarstellung Synthese Compiler Programmtext Compiler Compiler allgemeiner Aufbau eines Übersetzers: Code 9 / 398 Übersetzer 9 / 398 Übersetzer Die Analyse-Phase ist selbst unterteilt in mehrere Schritte Die Analyse-Phase ist selbst unterteilt in mehrere Schritte Programmtext Programmtext Analyse Analyse Scanner (annotierter) Syntaxbaum lexikalische Analyse: Aufteilung in Sinneinheiten Token-Strom (annotierter) Syntaxbaum 9 / 398 9 / 398 Übersetzer Übersetzer Die Analyse-Phase ist selbst unterteilt in mehrere Schritte Die Analyse-Phase ist selbst unterteilt in mehrere Schritte Programmtext lexikalische Analyse: Aufteilung in Sinneinheiten Scanner Token-Strom Parser syntaktische Analyse: Erkennen der hierarchischen Struktur Syntaxbaum lexikalische Analyse: Aufteilung in Sinneinheiten Token-Strom Analyse Analyse Scanner Programmtext Parser syntaktische Analyse: Erkennen der hierarchischen Struktur Syntaxbaum Type Checker... semantische Analyse: Überprüfung/Ermittlung semantischer Eigenschaften (annotierter) Syntaxbaum (annotierter) Syntaxbaum 9 / 398 9 / 398 Die lexikalische Analyse Themengebiet: Programmtext Scanner Token-Strom Lexikalische Analyse 10 / 398 Die lexikalische Analyse xyz + 42 11 / 398 Die lexikalische Analyse Scanner xyz + 42 xyz + 42 Scanner xyz + 42 Ein Token ist eine Folge von Zeichen, die zusammen eine Einheit bilden. Tokens werden in Klassen zusammen gefasst. Zum Beispiel: → xyz, pi, ... Konstanten wie 42, 3.14, ”abc”, ... Operatoren wie +, ... → reservierte Worte wie → 11 / 398 Namen (Identifier) wie → if, int, ... 11 / 398 Die lexikalische Analyse Die lexikalische Analyse Scanner xyz + 42 I Sind Tokens erst einmal klassifiziert, kann man die Teilwörter vorverarbeiten: O C xyz + 42 Wegwerfen irrelevanter Teile wie Leerzeichen, Kommentare,... Aussondern von Pragmas, d.h. Direktiven an den Compiler, die nicht Teil des Programms sind, wie include-Anweisungen; Ersetzen der Token bestimmter Klassen durch ihre Bedeutung / Interndarstellung, etwa bei: Ein Token ist eine Folge von Zeichen, die zusammen eine Einheit bilden. Tokens werden in Klassen zusammen gefasst. Zum Beispiel: → Namen (Identifier) wie → xyz, pi, ... → Konstanten wie 42, 3.14, ”abc”, ... Operatoren wie +, ... → reservierte Worte wie → → Namen: die typischerweise zentral in einer Symbol-Tabelle verwaltet, evt. mit reservierten Worten verglichen (soweit nicht vom Scanner bereits vorgenommen) und gegebenenfalls durch einen Index ersetzt werden. ⇒ Sieber if, int, ... 12 / 398 11 / 398 Die lexikalische Analyse Die lexikalische Analyse - Generierung: Diskussion: Vorteile Produktivität Die Komponente lässt sich schneller herstellen Scanner und Sieber werden i.a. in einer Komponente zusammen gefasst, indem man dem Scanner nach Erkennen eines Tokens gestattet, eine Aktion auszuführen Korrektheit Die Komponente realisiert (beweisbar) die Spezifikation. Scanner werden i.a. nicht von Hand programmiert, sondern aus einer Spezifikation generiert: Spezifikation Konstanten; Generator Effizienz Der Generator kann die erzeugte Programmkomponente mit den effizientesten Algorithmen ausstatten. Scanner 14 / 398 13 / 398 Die lexikalische Analyse - Generierung: Die lexikalische Analyse - Generierung: Vorteile ... in unserem Fall: Produktivität Die Komponente lässt sich schneller herstellen Korrektheit Die Komponente realisiert (beweisbar) die Spezifikation. Effizienz Der Generator kann die erzeugte Programmkomponente mit den effizientesten Algorithmen ausstatten. Spezifikation Generator Scanner Einschränkungen Spezifizieren ist auch Programmieren — nur eventuell einfacher Generierung statt Implementierung lohnt sich nur für Routine-Aufgaben ... und ist nur für Probleme möglich, die sehr gut verstanden sind 14 / 398 15 / 398 Die lexikalische Analyse - Generierung: Lexikalische Analyse ... in unserem Fall: Kapitel 1: 0 0 | [1-9][0-9]* Generator Grundlagen: Reguläre Ausdrücke [1−9] [0−9] Spezifikation von Token-Klassen: Reguläre Ausdrücke; Generierte Implementierung: Endliche Automaten + X 15 / 398 Reguläre Ausdrücke Reguläre Ausdrücke Grundlagen Programmtext benutzt ein endliches Alphabet Eingabe-Zeichen, z.B. ASCII 16 / 398 Grundlagen Σ von Programmtext benutzt ein endliches Alphabet Eingabe-Zeichen, z.B. ASCII Σ von Die Menge der Textabschnitte einer Token-Klasse ist i.a. regulär. Die Menge der Textabschnitte einer Token-Klasse ist i.a. regulär. Reguläre Sprachen kann man mithilfe regulärer Ausdrücke spezifizieren. Reguläre Sprachen kann man mithilfe regulärer Ausdrücke spezifizieren. Die Menge EΣ der (nicht-leeren) regulären Ausdrücke ist die kleinste Menge E mit: ∈E a∈E ( neues Symbol nicht aus Σ); für alle a ∈ Σ; (e1 | e2 ), (e1 · e2 ), e1 ∗ ∈ E 17 / 398 sofern e1 , e2 ∈ E. 17 / 398 Reguläre Ausdrücke ... im Beispiel: ((a · b∗ )·a) (a | b) ((a · b)·(a · b)) Stephen Kleene, Madison Wisconsin, 1909-1994 18 / 398 19 / 398 Reguläre Ausdrücke Reguläre Ausdrücke ... im Beispiel: ... im Beispiel: ((a · b∗ )·a) (a | b) ((a · b)·(a · b)) ((a · b∗ )·a) (a | b) ((a · b)·(a · b)) Achtung: Achtung: Wir unterscheiden zwischen Zeichen a, 0, $,... und Meta-Zeichen (, |, ),... Um (hässliche) Klammern zu sparen, benutzen wir Operator-Präzedenzen: ∗ und lassen “·” weg Wir unterscheiden zwischen Zeichen a, 0, $,... und Meta-Zeichen (, |, ),... Um (hässliche) Klammern zu sparen, benutzen wir Operator-Präzedenzen: ∗ >·>| e? e+ 19 / 398 Reguläre Ausdrücke und verzichten auf “” ≡ ( | e) ≡ (e · e∗ ) 19 / 398 Beachte: Die Operatoren (_)∗ , ∪, · sind die entsprechenden Operationen auf Wort-Mengen: Spezifikationen benötigen eine Semantik ...im Beispiel: Spezifikation abab a|b ab∗ a >·>| und lassen “·” weg Reale Spezifikations-Sprachen bieten zusätzliche Konstrukte: (L)∗ L1 · L2 Semantik {abab} {a, b} {abn a | n ≥ 0} = = {w1 . . . wk | k ≥ 0, wi ∈ L} {w1 w2 | w1 ∈ L1 , w2 ∈ L2 } Für e ∈ EΣ definieren wir die spezifizierte Sprache [[e]] ⊆ Σ∗ induktiv durch: [[]] = {} [[a]] = {a} [[e∗ ]] = ([[e]])∗ [[e1 |e2 ]] = [[e1 ]] ∪ [[e2 ]] [[e1 ·e2 ]] = [[e1 ]] · [[e2 ]] 21 / 398 20 / 398 Beachte: Reguläre Ausdrücke Die Operatoren (_)∗ , ∪, · sind die entsprechenden Operationen auf Wort-Mengen: (L)∗ L1 · L2 = = Beispiel: {w1 . . . wk | k ≥ 0, wi ∈ L} {w1 w2 | w1 ∈ L1 , w2 ∈ L2 } Identifier in Java: le = [a-zA-Z_\$] di = [0-9] Id = {le} ({le} | {di})* Reguläre Ausdrücke stellen wir intern als markierte geordnete Bäume dar: * (ab|)∗ | . a b Innere Knoten: Operator-Anwendungen; Blätter: einzelne Zeichen oder . 21 / 398 22 / 398 Reguläre Ausdrücke Beispiel: Reguläre Ausdrücke Beispiel: Identifier in Java: Identifier in Java: le = [a-zA-Z_\$] di = [0-9] Id = {le} ({le} | {di})* le = [a-zA-Z_\$] di = [0-9] Id = {le} ({le} | {di})* Float = {di}* (\.{di}|{di}\.) {di}*((e|E)(\+|\-)?{di}+)? Float = {di}* (\.{di}|{di}\.) {di}*((e|E)(\+|\-)?{di}+)? Bemerkungen: “le” und “di” sind Zeichenklassen. Definierte Namen werden in “{”, “}” eingeschlossen. Zeichen werden von Meta-Zeichen durch “\” unterschieden. 22 / 398 22 / 398 Endliche Automaten Lexikalische Analyse im Beispiel: Kapitel 2: a Grundlagen: Endliche Automaten b 23 / 398 24 / 398 Endliche Automaten im Beispiel: a b Knoten: Zustände; Michael O. Rabin, Stanford University Kanten: Übergänge; Beschriftungen: konsumierter Input 24 / 398 Dana S. Scott, Carnegy Mellon University, Pittsburgh 25 / 398 Endliche Automaten Endliche Automaten Definition Definition Ein nicht-deterministischer endlicher Automat (NFA) ist ein Tupel A = (Q, Σ, δ, I, F) wobei: Ein nicht-deterministischer endlicher Automat (NFA) ist ein Tupel A = (Q, Σ, δ, I, F) wobei: Q Σ I⊆Q F⊆Q δ eine endliche Menge von Zuständen; ein endliches Eingabe-Alphabet; die Menge der Anfangszustände; die Menge der Endzustände und die Menge der Übergänge (die Übergangs-Relation) ist. Q Σ I⊆Q F⊆Q δ eine endliche Menge von Zuständen; ein endliches Eingabe-Alphabet; die Menge der Anfangszustände; die Menge der Endzustände und die Menge der Übergänge (die Übergangs-Relation) ist. Für NFAs gilt: Definition Ist δ : Q × Σ → Q eine Funktion und #I = 1, heißt A deterministisch (DFA). 26 / 398 Endliche Automaten 26 / 398 Endliche Automaten Berechnungen sind Pfade im Graphen. Berechnungen sind Pfade im Graphen. akzeptierende Berechnungen führen von I nach F. akzeptierende Berechnungen führen von I Ein akzeptiertes Wort ist die Beschriftung eines akzeptierenden Pfades ... a nach F. Ein akzeptiertes Wort ist die Beschriftung eines akzeptierenden Pfades ... a b b 27 / 398 Endliche Automaten Berry-Sethi Verfahren – Umwandlung von Regex in NFA Dazu definieren wir den transitiven Abschluss δ ∗ von δ als kleinste Menge δ 0 mit: (p, , p) ∈ δ 0 (p, xw, q) ∈ δ 0 und sofern 27 / 398 (p, x, p1 ) ∈ δ Satz: Für jeden regulären Ausdruck e kann (in linearer Zeit) ein NFA konstruiert werden, der die Sprache [[e]] akzeptiert. und (p1 , w, q) ∈ δ 0 . δ ∗ beschreibt für je zwei Zustände, mit welchen Wörtern man vom einen zum andern kommt Idee: Die Menge aller akzeptierten Worte, d.h. die von A akzeptierte Sprache können wir kurz beschreiben als: Der Automat verfolgt (konzepionell mithilfe einer Marke “•”), im Syntaxbaum des regulären Ausdrucks, wohin man in e mit der Eingabe w gelangen kann. L(A) = {w ∈ Σ∗ | ∃ i ∈ I, f ∈ F : (i, w, f ) ∈ δ ∗ } 28 / 398 29 / 398 Berry-Sethi Verfahren Berry-Sethi Verfahren ... im Beispiel: ... im Beispiel: . . (a|b)∗ a(a|b) w : . * | b . * a | a = bbaa b a a | | b a b a 30 / 398 Berry-Sethi Verfahren 30 / 398 Berry-Sethi Verfahren ... im Beispiel: ... im Beispiel: . w = bbaa . : w : . * | b . * a | a = bbaa b a a | | b a b a 30 / 398 Berry-Sethi Verfahren 30 / 398 Berry-Sethi Verfahren ... im Beispiel: ... im Beispiel: . w = bbaa . : w : . * b . * a | a = bbaa | a a | b a 30 / 398 b | a b 30 / 398 Berry-Sethi Verfahren Berry-Sethi Verfahren ... im Beispiel: ... im Beispiel: . w = bbaa . : w : . * | b . * a | a = bbaa b a a | | b a b a 30 / 398 Berry-Sethi Verfahren 30 / 398 Berry-Sethi Verfahren ... im Beispiel: ... im Beispiel: . w = bbaa . : w : . * b . * a | a = bbaa | a a | b | b a b a 30 / 398 Berry-Sethi Verfahren 30 / 398 Berry-Sethi Verfahren ... im Beispiel: Beachte: Gelesen wird nur an den Blättern. . Die Navigation im Baum erfolgt ohne Lesen, d.h. mit -Übergängen. Für eine formale Konstruktion müssen wir die Automatenzustände bezeichnen. * Dazu benutzen wir (hier) einfach den Teilausdruck des Knotens im Syntaxbaum | . a | Leider gibt es eventuell mehrere gleiche Teilausdrücke ==⇒ a Wir numerieren die Blätter durch ... 31 / 398 b a b 32 / 398 Berry-Sethi Verfahren Berry-Sethi Verfahren ... im Beispiel: ... im Beispiel: . . . * a | a b 0 | 2 1 . * a | b 3 0 4 2 1 a | a 3 b 4 a b 32 / 398 Berry-Sethi Verfahren Berry-Sethi Verfahren Die Konstruktion: Zustände: Anfangszustand: Endzustand: Übergangsrelation: 32 / 398 Diskussion: •r, r• r Knoten von e; •e; e•; Für Blätter r ≡ i x benötigen wir: (•r, x, r•). Die meisten Übergänge dienen dazu, im Ausdruck zu navigieren Der Automat ist i.a. nichtdeterministisch Die übrigen Übergänge sind: r r1 | r2 r1 · r2 Übergänge (•r, , •r1 ) (•r, , •r2 ) (r1 •, , r•) (r2 •, , r•) (•r, , •r1 ) (r1 •, , •r2 ) (r2 •, , r•) r r1∗ r1 ? Übergänge (•r, , r•) (•r, , •r1 ) (r1 •, , •r1 ) (r1 •, , r•) (•r, , r•) (•r, , •r1 ) (r1 •, , r•) 34 / 398 33 / 398 Berry-Sethi Verfahren Berry-Sethi Verfahren: 1. Schritt empty[r] = t Diskussion: gdw. ∈ [[r]] Die meisten Übergänge dienen dazu, im Ausdruck zu navigieren Der Automat ist i.a. nichtdeterministisch ⇒ ... im Beispiel: Strategie: Vermeide die Generierung der -Übergänge . Benötigte Knoten-Eigenschaften: empty kann der Teilausdruck r einlesen? first die Menge der Lesezustände unterhalb von r, die potentiell als erste bei einem Abstieg erreicht werden. * next die Menge der Lesezustände rechts von r, die potentiell beim Wiederaufstieg von r erreicht werden. | last die Menge der Lesezustände unterhalb von r, die beim Abstieg potentiell als letzte erreicht werden. 0 Idee: Generiere diese Attribute für die Knoten über DFS! 34 / 398 a . 2 1 b | a 3 a 4 b 35 / 398 Berry-Sethi Verfahren: 1. Schritt empty[r] = t gdw. Berry-Sethi Verfahren: 1. Schritt empty[r] = t ∈ [[r]] ... im Beispiel: gdw. ∈ [[r]] ... im Beispiel: . . . * f | 2 . * f | a | f f 2 | a f f f f f f f f 0 1 3 4 0 1 3 4 a b a b a b a b 35 / 398 Berry-Sethi Verfahren: 1. Schritt empty[r] = t gdw. 35 / 398 Berry-Sethi Verfahren: 1. Schritt empty[r] = t ∈ [[r]] ... im Beispiel: gdw. ∈ [[r]] ... im Beispiel: f . . t f t f * . * . f | f f 2 | a f | f f 2 | a f f f f f f f f 0 1 3 4 0 1 3 4 a b a b a b a b 35 / 398 Berry-Sethi Verfahren: 1. Schritt Berry-Sethi Verfahren: 2. Schritt Die Menge der ersten Lesezustände: Die Menge der Lesezustände, die von •r aus (d.h. beim Absteigen in den Teilbaum r) mit aufeinanderfolgenden -Transitionen erreicht werden können: first[r] = {i in r | (•r, , • i x ) ∈ δ ∗ , x 6= } Implementierung: DFS post-order Traversierung Für Blätter r ≡ i x ist 35 / 398 ... im Beispiel: f empty[r] = (x ≡ ). . Andernfalls: empty[r1 | r2 ] = empty[r1 ] ∨ empty[r2 ] empty[r1 · r2 ] = empty[r1 ] ∧ empty[r2 ] empty[r1∗ ] = t empty[r1 ?] = t t f * . f | 36 / 398 f f 2 | a f f f f 0 1 3 4 a b a b 37 / 398 Berry-Sethi Verfahren: 2. Schritt Berry-Sethi Verfahren: 2. Schritt Die Menge der ersten Lesezustände: Die Menge der Lesezustände, die von •r aus (d.h. beim Absteigen in den Teilbaum r) mit aufeinanderfolgenden -Transitionen erreicht werden können: first[r] = {i in r | (•r, , • i x ) ∈ δ ∗ , x 6= } Die Menge der ersten Lesezustände: Die Menge der Lesezustände, die von •r aus (d.h. beim Absteigen in den Teilbaum r) mit aufeinanderfolgenden -Transitionen erreicht werden können: first[r] = {i in r | (•r, , • i x ) ∈ δ ∗ , x 6= } ... im Beispiel: ... im Beispiel: f f . . t f * . 2 f f | 0 f 0 f 1 2a f 1 a | 3 f 3 b f * . 2 f 01 f 4 f 4 a t | 0 f 0 b 1 2a f 1 a 34 f | 3 f 3 b 4 f 4 a b 37 / 398 Berry-Sethi Verfahren: 2. Schritt 37 / 398 Berry-Sethi Verfahren: 2. Schritt Die Menge der ersten Lesezustände: Die Menge der Lesezustände, die von •r aus (d.h. beim Absteigen in den Teilbaum r) mit aufeinanderfolgenden -Transitionen erreicht werden können: first[r] = {i in r | (•r, , • i x ) ∈ δ ∗ , x 6= } Die Menge der ersten Lesezustände: Die Menge der Lesezustände, die von •r aus (d.h. beim Absteigen in den Teilbaum r) mit aufeinanderfolgenden -Transitionen erreicht werden können: first[r] = {i in r | (•r, , • i x ) ∈ δ ∗ , x 6= } ... im Beispiel: ... im Beispiel: 012 f f . 01 t * | 0 1 2a f 1 a * 34 f | 3 f 3 b . 01 t . 2 f 01 f 0 f 2 f a 4 | 0 f 0 b . 2 f 01 f 4 f 2 f 1 2a f 1 a 34 f | 3 f 3 b 4 f 4 a b 37 / 398 Berry-Sethi Verfahren: 2. Schritt 37 / 398 Berry-Sethi Verfahren: 3. Schritt Implementierung: Die Menge nächster Lesezustände: Die Menge der Lesezustände in einem Teilbaum rechts neben r•, die möglicherweise als nächstes erreicht werden. next[r] = {i | (r•, , • i x ) ∈ δ ∗ , x 6= } DFS post-order Traversierung ... im Beispiel: Für Blätter r ≡ i x ist 012 f first[r] = {i | x 6≡ }. Andernfalls: first[r1 | r2 ] first[r1 · r2 ] first[r1∗ ] first[r1 ?] . 01 t first[r 1 ] ∪ first[r2 ] first[r1 ] ∪ first[r2 ] first[r1 ] = first[r1 ] = first[r1 ] = = * falls empty[r1 ] = t falls empty[r1 ] = f | 0 38 / 398 a . 2 f 01 f 0 f 2 f 2 1 f 1 b a 34 f | 3 f 3 a 4 f 4 b 39 / 398 Berry-Sethi Verfahren: 3. Schritt Berry-Sethi Verfahren: 3. Schritt Die Menge nächster Lesezustände: Die Menge der Lesezustände in einem Teilbaum rechts neben r•, die möglicherweise als nächstes erreicht werden. next[r] = {i | (r•, , • i x ) ∈ δ ∗ , x 6= } Die Menge nächster Lesezustände: Die Menge der Lesezustände in einem Teilbaum rechts neben r•, die möglicherweise als nächstes erreicht werden. next[r] = {i | (r•, , • i x ) ∈ δ ∗ , x 6= } ... im Beispiel: ... im Beispiel: 012 f ∅ . 01 t * | 0 2 1 f 1 a 2 f ∅ a * 34 f ∅ | 3 ∅ f 3 b . 01 2 t . 2 f 01 f 0 f 012 f ∅ 4 a | 0 f 0 b 2 1 f 1 a . 2 34 f ∅ 34 f 01 f 4 f ∅ 2 f ∅ a | 3 ∅ f 3 b 4 f ∅ 4 a b 39 / 398 Berry-Sethi Verfahren: 3. Schritt 39 / 398 Berry-Sethi Verfahren: 3. Schritt Die Menge nächster Lesezustände: Die Menge der Lesezustände in einem Teilbaum rechts neben r•, die möglicherweise als nächstes erreicht werden. next[r] = {i | (r•, , • i x ) ∈ δ ∗ , x 6= } Die Menge nächster Lesezustände: Die Menge der Lesezustände in einem Teilbaum rechts neben r•, die möglicherweise als nächstes erreicht werden. next[r] = {i | (r•, , • i x ) ∈ δ ∗ , x 6= } ... im Beispiel: ... im Beispiel: 012 f ∅ 012 f ∅ . 01 2 f ∅ 2 t * | 0 f 0 a . 2 0 1 2 01 f 2 1 a | 3 ∅ f 3 b * 34 f ∅ 34 f 1 f . 01 2 f ∅ 2 t a 2 0 1 2 01 f 4 f ∅ 4 | 0 012 f 0 b 2 1 a f 012 1 a . 34 f ∅ 34 f | 3 ∅ f 3 b 4 f ∅ 4 a b 39 / 398 Berry-Sethi Verfahren: 3. Schritt 39 / 398 Berry-Sethi Verfahren: 4. Schritt Die Menge letzter Lesezustände: Die Menge der Lesezustände, die möglicherweise als letzte beim Wiederaufstieg aus dem Teilbaum unter r erreicht werden: last[r] = {i in r | ( i x •, , r•) ∈ δ ∗ , x 6= } Implementierung: DFS pre-order Traversierung ... im Beispiel: Für die Wurzel haben wir: 012 f ∅ next[e] = ∅ Ansonsten machen wir eine Fallunterscheidung über den Kontext: . 01 r r1 | r2 r1 · r2 r1∗ r1 ? 2 t Regeln next[r1 ] next[r2 ] next[r1 ] next[r2 ] next[r1 ] next[r1 ] = next[r] = next[r] first[r2 ] ∪ next[r] = first[r2 ] = next[r] = first[r1 ] ∪ next[r] = next[r] * 2 34 f 0 1 2 01 f falls empty[r2 ] = t falls empty[r2 ] = f | 0 012 f 0 40 / 398 a 2 1 a f 012 1 b 2 f ∅ . 34 f ∅ | 3 ∅ f 3 a 4 f ∅ 4 b 41 / 398 Berry-Sethi Verfahren: 4. Schritt Berry-Sethi Verfahren: 4. Schritt Die Menge letzter Lesezustände: Die Menge der Lesezustände, die möglicherweise als letzte beim Wiederaufstieg aus dem Teilbaum unter r erreicht werden: last[r] = {i in r | ( i x •, , r•) ∈ δ ∗ , x 6= } Die Menge letzter Lesezustände: Die Menge der Lesezustände, die möglicherweise als letzte beim Wiederaufstieg aus dem Teilbaum unter r erreicht werden: last[r] = {i in r | ( i x •, , r•) ∈ δ ∗ , x 6= } ... im Beispiel: ... im Beispiel: 012 f ∅ . 01 2 t 2 f ∅ * 22 0 1 2 01 f 0 0 . | 2 11 a f 012 1 a 3 3 | ∅ f 3 b . 01 01 2 t * 34 f ∅ 34 f 012 f 0 012 34 f ∅ 4 a 0 0 | 012 f 0 b a . 22 0 1 2 01 f 01 44 f ∅ 2 34 f ∅ 34 34 f ∅ 34 f 2 11 a f 012 1 33 | ∅ f 3 b a 44 f ∅ 4 b 41 / 398 Berry-Sethi Verfahren: 4. Schritt Berry-Sethi Verfahren: Integration Implementierung: Konstruktion: Erstelle Automat aus Attributen des Syntaxbaums: DFS post-order Traversierung Für Blätter r ≡ i ist x Zustände: {•e} ∪ {i• | i Blatt} Startzustand: •e last[r] = {i | x 6≡ }. Endzustände: last[e] {•e} ∪ last[e] Andernfalls: last[r1 | r2 ] last[r1 · r2 ] last[r1∗ ] last[r1 ?] 41 / 398 last[r1 ] ∪ last[r2 ] last[r1 ] ∪ last[r2 ] = last[r2 ] = last[r1 ] = last[r1 ] = falls empty[e] = f . sonst. Übergänge: (•e, a, i•) falls i ∈ first[e] und i mit a beschriftet ist. (i•, a, i0 •) falls i0 ∈ next[i] und i0 mit a beschriftet ist. falls empty[r2 ] = t falls empty[r2 ] = f Den resultierenden Automaten bezeichnen wir mit Ae . 42 / 398 43 / 398 Berry-Sethi Verfahren ... im Beispiel: a 0 3 a a a b a 2 b a b a 1 4 b Gerard Berry, Esterel Technologies Bemerkung: Ravi Sethi, Research VR, Lucent Technologies Die Konstruktion heißt Berry-Sethi- oder auch Glushkov-Konstruktion. Sie wird in XML zur Definition von Content Models benutzt Das Ergebnis ist vielleicht nicht, was wir erwartet haben ... 44 / 398 45 / 398 Der erwartete Automat: Teilmengenkonstruktion a 0 3 a ... im Beispiel: a a b a a, b 2 b a a a, b b a 1 4 b Bemerkung: in einen Zustand eingehende Kanten haben hier nicht unbedingt die gleiche Beschriftung Dafür ist die Berry-Sethi-Konstruktion direkter In Wirklichkeit benötigen wir aber deterministische Automaten ⇒ Teilmengen-Konstruktion 46 / 398 Teilmengenkonstruktion 47 / 398 Teilmengenkonstruktion a a 0 3 a ... im Beispiel: a 3 a ... im Beispiel: a b a 0 a a b a 2 2 b b a a b a b 1 a 4 1 b 4 b 02 02 a a a b 1 b 47 / 398 Teilmengenkonstruktion 47 / 398 Teilmengenkonstruktion a a 0 3 a ... im Beispiel: a ... im Beispiel: a b a 0 3 a a a 2 2 b b a a b a 1 b 4 4 b a 02 a 1 b a a b a 023 a 02 a 023 a b a a b a b b 1 1 b b 14 b 47 / 398 47 / 398 Teilmengenkonstruktion Teilmengenkonstruktion Satz: Satz: Zu jedem nichtdeterministischen Automaten A = (Q, Σ, δ, I, F) kann ein deterministischer Automat P(A) konstruiert werden mit Zu jedem nichtdeterministischen Automaten A = (Q, Σ, δ, I, F) kann ein deterministischer Automat P(A) konstruiert werden mit L(A) = L(P(A)) L(A) = L(P(A)) Konstruktion: Zustände: Teilmengen von Q; Anfangszustände: {I}; Endzustände: {Q0 ⊆ Q | Q0 ∩ F 6= ∅}; Übergangsfunktion: δP (Q0 , a) = {q ∈ Q | ∃ p ∈ Q0 : (p, a, q) ∈ δ}. 48 / 398 Teilmengenkonstruktion 48 / 398 Teilmengenkonstruktion Achtung: Achtung: Leider gibt es exponentiell viele Teilmengen von Q Leider gibt es exponentiell viele Teilmengen von Q Um nur nützliche Teilmengen zu betrachten, starten wir mit der Menge QP = {I} und fügen weitere Zustände nur nach Bedarf hinzu ... Um nur nützliche Teilmengen zu betrachten, starten wir mit der Menge QP = {I} und fügen weitere Zustände nur nach Bedarf hinzu ... d.h., wenn sie von einem Zustand in QP aus erreichbar sind Trotz dieser Optimierung kann der Ergebnisautomat riesig sein ... was aber in der Praxis (so gut wie) nie auftritt d.h., wenn sie von einem Zustand in QP aus erreichbar sind Trotz dieser Optimierung kann der Ergebnisautomat riesig sein ... was aber in der Praxis (so gut wie) nie auftritt In Tools wie grep wird deshalb zu der DFA zu einem regulären Ausdruck nicht aufgebaut ! Stattdessen werden während der Abbarbeitung der Eingabe genau die Mengen konstruiert, die für die Eingabe notwendig sind ... 49 / 398 49 / 398 Teilmengenkonstruktion Teilmengenkonstruktion a a 0 3 a ... im Beispiel: a a ... im Beispiel: a b a b a b 0 a a 2 b b a a b a 1 b 4 a 1 b 02 a b a b a b 2 3 a 4 b 023 02 023 1 14 a 1 14 50 / 398 50 / 398 Teilmengenkonstruktion Teilmengenkonstruktion a a 0 3 a ... im Beispiel: a a ... im Beispiel: a b a b a b 0 a a 2 b b a a b a b 1 a 4 1 b 02 a b a b a b 2 3 a 4 b 02 023 a 023 a b b a 1 1 14 14 50 / 398 Teilmengenkonstruktion 50 / 398 Bemerkungen: a 0 3 a ... im Beispiel: a a b a a b a b Bei einem Eingabewort der Länge Mengen konstruiert 2 b a b 4 b 02 werden maximal O(n) Ist eine Menge bzw. eine Kante des DFA einmal konstuiert, heben wir sie in einer Hash-Tabelle auf. Bevor wir einen neuen Übergang konstruieren, sehen wir erst nach, ob wir diesen nicht schon haben a 1 n 023 a b a 1 14 51 / 398 50 / 398 Bemerkungen: Lexikalische Analyse Bei einem Eingabewort der Länge Mengen konstruiert n werden maximal O(n) Kapitel 3: Ist eine Menge bzw. eine Kante des DFA einmal konstuiert, heben wir sie in einer Hash-Tabelle auf. Bevor wir einen neuen Übergang konstruieren, sehen wir erst nach, ob wir diesen nicht schon haben Design eines Scanners Zusammenfassend finden wir: Satz: Zu jedem regulären Ausdruck e kann ein deterministischer Automat A = P(Ae ) konstruiert werden mit L(A) = [[e]] 51 / 398 52 / 398 Design eines Scanners Design eines Scanners Eingabe (vereinfacht): e1 e2 ... ek Eingabe (vereinfacht): eine Menge von Regeln: { action1 } { action2 } e1 e2 { actionk } ek eine Menge von Regeln: ... Ausgabe: ... ... ... { action1 } { action2 } { actionk } ein Programm, das von der Eingabe ein maximales Präfix w e1 | . . . | ek erfüllt; das minimale i ermittelt, so dass für w actioni ausführt. liest, das w ∈ [[ei ]]; 53 / 398 Implementierung: 53 / 398 Implementierung: Idee (Fortsetzung): Idee: Der Scanner verwaltet zwei Zeiger hA, Bi und die zugehörigen Zustände hqA , qB i... Der Zeiger A merkt sich die letzte Position in der Eingabe, nach der ein Zustand qA ∈ F erreicht wurde; Konstruiere den DFA P(Ae ) = (Q, Σ, δ, {q0 }, F) zu dem Ausdruck e = (e1 | . . . | ek ); Der Zeiger B verfolgt die aktuelle Position. Definiere die Mengen: F1 F2 Fk = = ... = {q ∈ F | q ∩ last[e1 ] 6= ∅} {q ∈ (F\F1 ) | q ∩ last[e2 ] 6= ∅} s t d o u t . w r i {q ∈ (F\(F1 ∪ . . . ∪ Fk−1 )) | q ∩ last[ek ] 6= ∅} t e l n A ( " H a l l o " ) ; B Für Eingabe w gilt: δ ∗ (q0 , w) ∈ Fi genau dann wenn der Scanner für w actioni ausführen soll 54 / 398 Implementierung: 55 / 398 Implementierung: Idee (Fortsetzung): Idee (Fortsetzung): Ist der aktuelle Zustand qB = ∅ , geben wir Eingabe bis zur Postion A aus und setzen: B := A; A := ⊥; qB := q0 ; qA := ⊥ Der Scanner verwaltet zwei Zeiger hA, Bi und die zugehörigen Zustände hqA , qB i... Der Zeiger A merkt sich die letzte Position in der Eingabe, nach der ein Zustand qA ∈ F erreicht wurde; Der Zeiger B verfolgt die aktuelle Position. w r i t e l n ( " H a l w r i l o " ) ; t e l n A 4 A B ⊥ ⊥ 55 / 398 ( " H a l l o " ) ; B 4 56 / 398 Implementierung: Implementierung: Idee (Fortsetzung): Idee (Fortsetzung): Ist der aktuelle Zustand qB = ∅ , geben wir Eingabe bis zur Postion A aus und setzen: B qB := A; := q0 ; w r i A qA t e l n A 4 Ist der aktuelle Zustand qB = ∅ , geben wir Eingabe bis zur Postion A aus und setzen: B qB := ⊥; := ⊥ ( " H a l := A; := q0 ; A qA l o " ) ; := ⊥; := ⊥ ( " H a l B ∅ w r i l o " ) ; A B ⊥ q0 t e l n 56 / 398 Erweiterung: Zustände 56 / 398 Eingabe (verallgemeinert): hstatei e1 e2 { action1 { action2 yybegin(state1 ); } yybegin(state2 ); } { actionk yybegin(statek ); } yybegin (statei ); setzt den Zustand auf { ... ek Gelegentlich ist es nützlich, verschiedene Scanner-Zustände zu unterscheiden. } In unterschiedlichen Zuständen sollen verschiedene Tokenklassen erkannt werden können. Der Aufruf statei . In Abhängigkeit der gelesenen Tokens kann der Scanner-Zustand geändert werden Beispiel: eine Menge von Regeln: Der Startzustand ist (z.B. bei JFlex) YYINITIAL. ... im Beispiel: Kommentare hYYINITIALi hCOMMENTi Innerhalb eines Kommentars werden Identifier, Konstanten, Kommentare, ... nicht erkannt 00 { } /∗00 ∗ /00 . | \n 00 { yybegin(COMMENT); } { yybegin(YYINITIAL); } { } 58 / 398 57 / 398 Bemerkungen: “.” Themengebiet: matcht alle Zeichen ungleich “\n”. Für jeden Zustand generieren wir den entsprechenden Scanner. Die Methode yybegin (STATE); verschiedenen Scannern um. schaltet zwischen den Syntaktische Analyse Kommentare könnte man auch direkt mithilfe einer geeigneten Token-Klasse implementieren. Deren Beschreibung ist aber ungleich komplizierter Scanner-Zustände sind insbesondere nützlich bei der Implementierung von Präprozessoren, die in einen Text eingestreute Spezifikationen expandieren sollen. 59 / 398 60 / 398 Die syntaktische Analyse Token-Strom Die syntaktische Analyse Parser Syntaxbaum Token-Strom Die syntaktische Analyse versucht, Tokens zu größeren Programmeinheiten zusammen zu fassen. Parser Syntaxbaum Die syntaktische Analyse versucht, Tokens zu größeren Programmeinheiten zusammen zu fassen. Solche Einheiten können sein: 61 / 398 Diskussion: Ausdrücke; → bedingte Verzweigungen; → Statements; → Schleifen; ... 61 / 398 Diskussion: Auch Parser werden i.a. nicht von Hand programmiert, sondern aus einer Spezifikation generiert: Spezifikation → Generator Auch Parser werden i.a. nicht von Hand programmiert, sondern aus einer Spezifikation generiert: Parser E→E{op}E Generator Spezifikation der hierarchischen Struktur: kontextfreie Grammatiken Generierte Implementierung: Kellerautomaten + X 62 / 398 62 / 398 Grundlagen: Syntaktische Analyse Kontextfreie Grammatiken Programme einer Programmiersprache können unbeschränkt viele Tokens enthalten, aber nur endlich viele Token-Klassen Kapitel 1: Als endliches Terminal-Alphabet T wählen wir darum die Menge der Token-Klassen. Grundlagen: Kontextfreie Grammatiken Die Schachtelung von Programm-Konstrukten lässt sich elegant mit Hilfe von kontextfreien Grammatiken beschreiben ... 63 / 398 64 / 398 Grundlagen: Kontextfreie Grammatiken Programme einer Programmiersprache können unbeschränkt viele Tokens enthalten, aber nur endlich viele Token-Klassen Als endliches Terminal-Alphabet T wählen wir darum die Menge der Token-Klassen. Die Schachtelung von Programm-Konstrukten lässt sich elegant mit Hilfe von kontextfreien Grammatiken beschreiben ... Definition: Eine kontextfreie Grammatik (CFG) ist ein 4-Tupel G = (N, T, P, S) mit: N die Menge der Nichtterminale, T die Menge der Terminale, Noam Chomsky, MIT John Backus, IBM (Erfinder von Fortran) P die Menge der Produktionen oder Regeln, und S∈N das Startsymbol 64 / 398 Konventionen 65 / 398 Konventionen Die Regeln kontextfreier Grammatiken sind von der Form: A→α Die Regeln kontextfreier Grammatiken sind von der Form: mit A ∈ N , α ∈ (N ∪ T)∗ A→α mit A ∈ N , α ∈ (N ∪ T)∗ ... im Beispiel: Spezifizierte Sprache: S S → → aSb {an bn | n ≥ 0} 66 / 398 66 / 398 Konventionen ... weitere Beispiele: S → hstmti hstmti → hifi | hwhilei | hrexpi; hifi → if ( hrexpi ) hstmti else hstmti hwhilei → while ( hrexpi ) hstmti hrexpi → int | hlexpi | hlexpi = hrexpi hlexpi → name | ... Die Regeln kontextfreier Grammatiken sind von der Form: A→α mit A ∈ N , α ∈ (N ∪ T)∗ ... im Beispiel: Spezifizierte Sprache: S S → → aSb | ... {an bn | n ≥ 0} Konventionen: In Beispielen ist die Spezifikation der Nichtterminale und Terminale i.a. implizit: Nichtterminale sind: A, B, C, ..., hexpi, hstmti, ...; Terminale sind: a, b, c, ..., int, name, ...; 66 / 398 67 / 398 ... weitere Beispiele: S hstmti hifi hwhilei hrexpi hlexpi → → → → → → hstmti hifi | hwhilei | hrexpi; if ( hrexpi ) hstmti else hstmti while ( hrexpi ) hstmti int | hlexpi | hlexpi = hrexpi name | ... Weitere Grammatiken: E E T F | ... → → → → E+E 0 | E+T 0 | T∗F 0 | (E )0 | E∗E 1 | ( E ) 2 T1 F1 name 1 | int 2 | name 3 | int 4 Weitere Konventionen: Für jedes Nichtterminal sammeln wir die rechten Regelseiten und listen sie gemeinsam auf Die j-te Regel für A bezeichnen (j ≥ 0). Die beiden Grammatiken beschreiben die gleiche Sprache können wir durch das Paar (A, j) 68 / 398 67 / 398 Ableitung Grammatiken sind Wortersetzungssysteme. Die Regeln geben die möglichen Ersetzungsschritte an. Eine Folge solcher Ersetzungsschritte α0 → . . . → αm heißt Ableitung. Weitere Grammatiken: E E T F → → → → E+E 0 | E+T 0 | T∗F 0 | (E )0 | E∗E 1 | ( E ) 2 T1 F1 name 1 | int 2 | name 3 ... im Beispiel: | int 4 E Die beiden Grammatiken beschreiben die gleiche Sprache 69 / 398 68 / 398 Ableitung Ableitung Grammatiken sind Wortersetzungssysteme. Die Regeln geben die möglichen Ersetzungsschritte an. Eine Folge solcher Ersetzungsschritte α0 → . . . → αm heißt Ableitung. ... im Beispiel: E → Grammatiken sind Wortersetzungssysteme. Die Regeln geben die möglichen Ersetzungsschritte an. Eine Folge solcher Ersetzungsschritte α0 → . . . → αm heißt Ableitung. ... im Beispiel: E+T 69 / 398 E → → E+T T+T 69 / 398 Ableitung Ableitung Grammatiken sind Wortersetzungssysteme. Die Regeln geben die möglichen Ersetzungsschritte an. Eine Folge solcher Ersetzungsschritte α0 → . . . → αm heißt Ableitung. ... im Beispiel: E → → → Grammatiken sind Wortersetzungssysteme. Die Regeln geben die möglichen Ersetzungsschritte an. Eine Folge solcher Ersetzungsschritte α0 → . . . → αm heißt Ableitung. ... im Beispiel: E+T T+T T∗F+T E → → → → E+T T+T T∗F+T T ∗ int + T 69 / 398 Ableitung Ableitung Grammatiken sind Wortersetzungssysteme. Die Regeln geben die möglichen Ersetzungsschritte an. Eine Folge solcher Ersetzungsschritte α0 → . . . → αm heißt Ableitung. ... im Beispiel: 69 / 398 E → → → → → Grammatiken sind Wortersetzungssysteme. Die Regeln geben die möglichen Ersetzungsschritte an. Eine Folge solcher Ersetzungsschritte α0 → . . . → αm heißt Ableitung. ... im Beispiel: E+T T+T T∗F+T T ∗ int + T F ∗ int + T E → → → → → → E+T T+T T∗F+T T ∗ int + T F ∗ int + T name ∗ int + T 69 / 398 Ableitung Ableitung Grammatiken sind Wortersetzungssysteme. Die Regeln geben die möglichen Ersetzungsschritte an. Eine Folge solcher Ersetzungsschritte α0 → . . . → αm heißt Ableitung. ... im Beispiel: 69 / 398 E → → → → → → → Grammatiken sind Wortersetzungssysteme. Die Regeln geben die möglichen Ersetzungsschritte an. Eine Folge solcher Ersetzungsschritte α0 → . . . → αm heißt Ableitung. ... im Beispiel: E+T T+T T∗F+T T ∗ int + T F ∗ int + T name ∗ int + T name ∗ int + F 69 / 398 E → → → → → → → → E+T T+T T∗F+T T ∗ int + T F ∗ int + T name ∗ int + T name ∗ int + F name ∗ int + int 69 / 398 Ableitung Ableitung Grammatiken sind Wortersetzungssysteme. Die Regeln geben die möglichen Ersetzungsschritte an. Eine Folge solcher Ersetzungsschritte α0 → . . . → αm heißt Ableitung. Grammatiken sind Wortersetzungssysteme. Die Regeln geben die möglichen Ersetzungsschritte an. Eine Folge solcher Ersetzungsschritte α0 → . . . → αm heißt Ableitung. Definition Definition Eine Ableitung → ist eine Relation auf Wörtern über N ∪ T, mit Eine Ableitung → ist eine Relation auf Wörtern über N ∪ T, mit ... im Beispiel: α → α0 E → → → → → → → → ... im Beispiel: E+T T+T T∗F+T T ∗ int + T F ∗ int + T name ∗ int + T name ∗ int + F name ∗ int + int gdw. α = α1 A α2 ∧ α0 = α1 β α2 für ein A → β ∈ P α → α0 69 / 398 Ableitung E+T T+T T∗F+T T ∗ int + T F ∗ int + T name ∗ int + T name ∗ int + F name ∗ int + int gdw. α = α1 A α2 ∧ α0 = α1 β α2 für ein A → β ∈ P Den reflexiven und transitiven Abschluss von → schreiben wir: →∗ 69 / 398 Bemerkungen: Die Relation → hängt von der Grammatik ab Die Relation → In jedem Schritt einer Ableitung können wir: ∗ → → → → → → → → Ableitung Bemerkungen: ∗ E eine Stelle auswählen, wo wir ersetzen wollen, sowie ∗ eine Regel, wie wir ersetzen wollen. Die von G ∗ spezifizierte Sprache ist: ∗ hängt von der Grammatik ab In jedem Schritt einer Ableitung können wir: eine Stelle auswählen, wo wir ersetzen wollen, sowie eine Regel, wie wir ersetzen wollen. Die von G ∗ spezifizierte Sprache ist: L(G) = {w ∈ T ∗ | S →∗ w} L(G) = {w ∈ T | S → w} Achtung: Die Reihenfolge, in der disjunkte Teile abgeleitet werden, ist unerheblich 70 / 398 Ableitungsbaum 70 / 398 Spezielle Ableitungen Ableitungen eines Symbols stellt man als Ableitungsbaum dar: ... im Beispiel: E →0 →1 →0 →2 →1 →1 →1 →2 E+T T+T T∗F+T T ∗ int + T F ∗ int + T name ∗ int + T name ∗ int + F name ∗ int + int E 0 Beachte: E 1 + T 0 T 1 F 1 ∗ Neben beliebigen Ableitungen betrachtet man solche, bei denen stets das linkeste (bzw. rechteste) Vorkommen eines Nichtterminals ersetzt wird. T 1 F 2 F 2 int Diese heißen Links- (bzw. Rechts-) Ableitungen und werden durch Index L bzw. R gekennzeichnet. int Links-(bzw. Rechts-) Ableitungen entsprechen einem links-rechts (bzw. rechts-links) preorder-DFS-Durchlauf durch den Ableitungsbaum name Ein Ableitungsbaum für A ∈ N: innere Knoten: Regel-Anwendungen Wurzel: Regel-Anwendung für A Blätter: Terminale oder Die Nachfolger von (B, i) entsprechen der rechten Seite der Regel Reverse Rechts-Ableitungen entsprechen einem links-rechts postorder-DFS-Durchlauf durch den Ableitungsbaum 71 / 398 72 / 398 Spezielle Ableitungen Spezielle Ableitungen ... im Beispiel: ... im Beispiel: E 0 E 1 + T 0 ∗ T 1 F 2 T 1 E 1 F 2 T 0 int ∗ T 1 int F 1 E 0 T 1 F 2 F 2 int int F 1 name + name Links-Ableitung: (E, 0) (E, 1) (T, 0) (T, 1) (F, 1) (F, 2) (T, 1) (F, 2) 73 / 398 Spezielle Ableitungen 73 / 398 Spezielle Ableitungen ... im Beispiel: ... im Beispiel: E 0 E 1 + T 0 ∗ T 1 F 2 T 1 E 1 F 2 T 0 int + T 1 F 2 F 2 int int F 1 name Links-Ableitung: Rechts-Ableitung: ∗ T 1 int F 1 E 0 name Links-Ableitung: (E, 0) (E, 1) (T, 0) (T, 1) (F, 1) (F, 2) (T, 1) (F, 2) Rechts-Ableitung: (E, 0) (T, 1) (F, 2) (E, 1) (T, 0) (F, 2) (T, 1) (F, 1) Reverse Rechts-Ableitung: (E, 0) (E, 1) (T, 0) (T, 1) (F, 1) (F, 2) (T, 1) (F, 2) (E, 0) (T, 1) (F, 2) (E, 1) (T, 0) (F, 2) (T, 1) (F, 1) (F, 1) (T, 1) (F, 2) (T, 0) (E, 1) (F, 2) (T, 1) (E, 0) 73 / 398 Eindeutige Grammatiken Eindeutige Grammatiken Die Konkatenation der Blätter des Ableitungsbaums t wir auch mit yield(t) . ... im Beispiel: bezeichnen Definition: Die Grammatik G heißt eindeutig, falls es zu jedem w ∈ T ∗ maximal einen Ableitungsbaum t von S gibt mit yield(t) = w. E 0 E 1 + T 0 T 1 F 1 73 / 398 ∗ T 1 ... in unserem Beispiel: F 2 F 2 E E T F int int name → → → → E+E 0 | E+T 0 | T∗F 0 | (E )0 | E∗E 1 | ( E ) 2 T1 F1 name 1 | int 2 | name 3 | int 4 Die zweite ist eindeutig, die erste nicht liefert die Konkatenation: name ∗ int + int . 74 / 398 75 / 398 Fazit: Syntaktische Analyse Ein Ableitungsbaum repräsentiert eine mögliche hierarchische Struktur eines Worts. Kapitel 2: Bei Programmiersprachen sind wir nur an Grammatiken interessiert, bei denen die Struktur stets eindeutig ist Grundlagen: Kellerautomaten Ableitungsbäume stehen in eins-zu-eins-Korrespondenz mit Links-Ableitungen wie auch (reversen) Rechts-Ableitungen. Links-Ableitungen entsprechen einem Topdown-Aufbau des Ableitungsbaums. Reverse Rechts-Ableitungen entsprechen einem Bottom-up-Aufbau des Ableitungsbaums. 76 / 398 77 / 398 Grundlagen: Kellerautomaten Durch kontextfreie Grammatiken spezifizierte Sprachen können durch Kellerautomaten (Pushdown Automata) akzeptiert werden: Friedrich L. Bauer, TUM Klaus Samelson, TUM 1957: Patent auf die Verwendung von Stapelspeicher zur Übersetzung von Programmiersprachen Der Keller wird z.B. benötigt, um korrekte Klammerung zu überprüfen 78 / 398 Beispiel: Zustände: 0, 1, 2 Anfangszustand: 0 Endzustände: 0, 2 79 / 398 Beispiel: 0 1 11 12 a a b b 11 11 2 2 Zustände: 0, 1, 2 Anfangszustand: 0 Endzustände: 0, 2 0 1 11 12 a a b b 11 11 2 2 Konventionen: Wir unterscheiden nicht zwischen Kellersymbolen und Zuständen Das rechteste / oberste Kellersymbol repräsentiert den Zustand Jeder Übergang liest / modifiziert einen oberen Abschnitt des Kellers 80 / 398 80 / 398 Kellerautomaten Kellerautomaten Definition: Definition: Formal definieren wir deshalb einen Kellerautomaten (PDA) als ein Tupel: M = (Q, T, δ, q0 , F) mit: Formal definieren wir deshalb einen Kellerautomaten (PDA) als ein Tupel: M = (Q, T, δ, q0 , F) mit: Q eine endliche Menge von Zuständen; T Q eine endliche Menge von Zuständen; das Eingabe-Alphabet; q0 ∈ Q F⊆Q T der Anfangszustand; die Menge der Endzustände und δ ⊆ Q+ × (T ∪ {}) × Q∗ das Eingabe-Alphabet; q0 ∈ Q F⊆Q eine endliche Menge von Übergängen der Anfangszustand; die Menge der Endzustände und δ ⊆ Q+ × (T ∪ {}) × Q∗ eine endliche Menge von Übergängen Mithilfe der Übergänge definieren wir Berechnungen von Kellerautomaten Der jeweilige Berechnungszustand (die aktuelle Konfiguration) ist ein Paar: (γ, w) ∈ Q∗ × T ∗ bestehend aus dem Kellerinhalt und dem noch zu lesenden Input. 81 / 398 ... im Beispiel: 81 / 398 ... im Beispiel: 0 1 11 12 Zustände: 0, 1, 2 Anfangszustand: 0 Endzustände: 0, 2 a a b b 11 11 2 2 0 1 11 12 Zustände: 0, 1, 2 Anfangszustand: 0 Endzustände: 0, 2 (0 , a a b b 11 11 2 2 a a a b b b) 82 / 398 ... im Beispiel: 82 / 398 ... im Beispiel: 0 1 11 12 Zustände: 0, 1, 2 Anfangszustand: 0 Endzustände: 0, 2 (0 , a a a b b b) ` a a b b 11 11 2 2 0 1 11 12 Zustände: 0, 1, 2 Anfangszustand: 0 Endzustände: 0, 2 (1 1 , a a b b b) (0 , 82 / 398 a a a b b b) ` ` a a b b 11 11 2 2 (1 1 , a a b b b) (1 1 1 , a b b b) 82 / 398 ... im Beispiel: ... im Beispiel: Zustände: 0, 1, 2 Anfangszustand: 0 Endzustände: 0, 2 (0 , a a a b b b) 0 1 11 12 a a b b 11 11 2 2 Zustände: 0, 1, 2 Anfangszustand: 0 Endzustände: 0, 2 ` (1 1 , a a b b b) ` (1 1 1 , a b b b) ` (1 1 1 1 , b b b) (0 , a a a b b b) 0 1 11 12 ` (1 1 , ` (1 1 1 , ` (1 1 1 1 , ` (1 1 2 , a a b b 11 11 2 2 a a b b b) a b b b) b b b) b b) 82 / 398 ... im Beispiel: 82 / 398 ... im Beispiel: Zustände: 0, 1, 2 Anfangszustand: 0 Endzustände: 0, 2 (0 , a a a b b b) 0 1 11 12 ` (1 1 , ` (1 1 1 , ` (1 1 1 1 , ` (1 1 2 , ` (1 2 , a a b b 11 11 2 2 Zustände: 0, 1, 2 Anfangszustand: 0 Endzustände: 0, 2 a a b b b) a b b b) b b b) b b) b) (0 , a a a b b b) 0 1 11 12 ` (1 1 , ` (1 1 1 , ` (1 1 1 1 , ` (1 1 2 , ` (1 2 , ` (2 , a a b b 11 11 2 2 a a b b b) a b b b) b b b) b b) b) ) 82 / 398 Ein Berechnungsschritt wird durch die Relation beschrieben, wobei 2 82 / 398 Ein Berechnungsschritt wird durch die Relation beschrieben, wobei ` ⊆ (Q∗ × T ∗ ) (α γ, x w) ` (α γ 0 , w) für (γ, x, γ 0 ) ∈ δ 2 ` ⊆ (Q∗ × T ∗ ) (α γ, x w) ` (α γ 0 , w) für (γ, x, γ 0 ) ∈ δ Bemerkungen: Die Relation ` hängt natürlich vom Kellerautomaten M ab Die reflexive und transitive Hülle von ` bezeichnen wir mit `∗ Dann ist die von M akzeptierte Sprache: L(M) = {w ∈ T ∗ | ∃ f ∈ F : (q0 , w) `∗ (f , )} 83 / 398 83 / 398 Deterministischer Kellerautomat Ein Berechnungsschritt wird durch die Relation beschrieben, wobei 2 ` ⊆ (Q∗ × T ∗ ) Definition: (α γ, x w) ` (α γ 0 , w) für (γ, x, γ 0 ) ∈ δ Der Kellerautomat M heißt deterministisch, falls jede Konfiguration maximal eine Nachfolge-Konfiguration hat. Bemerkungen: Die Relation ` hängt natürlich vom Kellerautomaten M ab Das ist genau dann der Fall wenn für verschiedene Übergänge (γ1 , x, γ2 ) , (γ10 , x0 , γ20 ) ∈ δ gilt: Ist γ1 ein Suffix von γ10 , dann muss x 6= x0 ∧ x 6= 6= x0 sein. Die reflexive und transitive Hülle von ` bezeichnen wir mit ` ∗ Dann ist die von M akzeptierte Sprache: L(M) = {w ∈ T ∗ | ∃ f ∈ F : (q0 , w) `∗ (f , )} Wir akzeptieren also mit Endzustand und leerem Keller 84 / 398 83 / 398 Deterministischer Kellerautomat Kellerautomaten Definition: Der Kellerautomat M heißt deterministisch, falls jede Konfiguration maximal eine Nachfolge-Konfiguration hat. Satz: Zu jeder kontextfreien Grammatik G = (N, T, P, S) kann ein Kellerautomat M konstruiert werden mit L(G) = L(M) . Das ist genau dann der Fall wenn für verschiedene Übergänge (γ1 , x, γ2 ) , (γ10 , x0 , γ20 ) ∈ δ gilt: Ist γ1 ein Suffix von γ10 , dann muss x 6= x0 ∧ x 6= 6= x0 sein. Der Satz ist für uns so wichtig, dass wir zwei Konstruktionen für Automaten angeben, motiviert durch die beiden speziellen Ableitungsmöglichkeiten: ... im Beispiel: 0 1 11 12 a a b b MGL zur Bildung von Linksableitungen 11 11 2 2 MGR zur Bildung von reversen Rechtsableitungen ... ist das natürlich der Fall 84 / 398 85 / 398 Syntaktische Analyse Kapitel 3: Top-down Parsing Marcel-Paul Schützenberger (1920-1996), Paris Anthony G. Öttinger, Präsident der ACM 1966-68 1961: On context free languages and pushdownautomata 86 / 398 87 / 398 Item-Kellerautomat Item-Kellerautomat Unser Beispiel: Konstruktion: Item-Kellerautomat S MGL Rekonstruiere eine Linksableitung. AB [S0 → • S] [S → • A B] [A → • a] [S → • A B] [A → a •] [S → A • B] [B → • b] [S → A • B] [B → b •] [S0 → • S] [S → A B •] ==⇒ Die Zustände sind jetzt Items (= Regeln mit Punkt): A → αβ ∈ P Der Punkt gibt an, wieweit die Regel bereits abgearbeitet wurde 88 / 398 Item-Kellerautomat B → b a b [S0 → • S] [S0 → S •] [S0 → • S] [S → • A B] [S → • A B] [A → • a] [A → a •] [S → A • B] [S → A • B] [B → • b] [B → b •] [S → A B •] [S0 → S •] 89 / 398 Item-Kellerautomat Der Item-Kellerautomat MGL Shifts: Reduce: hinzu Anfangszustand: Endzustand: Verifiziere sukzessive, dass die gewählte Regel mit der Eingabe übereinstimmt. Expansionen: A → a Dann konstruieren wir: Expandiere Nichtterminale mithilfe einer Regel. [A →α • β] , → Wir fügen eine Regel: S0 → S hat drei Arten von Übergängen: ... im Beispiel: ([A → α • B β], , [A → α • B β] [B → • γ]) für A → α B β, B → γ ∈ P ([A → α • a β], a, [A → α a • β]) für A → α a β ∈ P ([A → α • B β] [B → γ•], , [A → α B • β]) für A → α B β, B → γ ∈ P S 0 Items der Form: [A → α •] heißen auch vollständig Der Item-Kellerautomat schiebt den Punkt einmal um den Ableitungsbaum herum ... A 0 B 0 a b 90 / 398 91 / 398 Item-Kellerautomat Item-Kellerautomat ... im Beispiel: ... im Beispiel: S 0 S 0 A 0 B 0 A 0 B 0 a b a b 91 / 398 91 / 398 Item-Kellerautomat Item-Kellerautomat ... im Beispiel: ... im Beispiel: S 0 S 0 A 0 B 0 A 0 B 0 a b a b 91 / 398 91 / 398 Item-Kellerautomat Item-Kellerautomat ... im Beispiel: ... im Beispiel: S 0 S 0 A 0 B 0 A 0 B 0 a b a b 91 / 398 91 / 398 Item-Kellerautomat Item-Kellerautomat ... im Beispiel: ... im Beispiel: S 0 S 0 A 0 B 0 A 0 B 0 a b a b 91 / 398 91 / 398 Item-Kellerautomat Diskussion: Die Expansionen einer Berechnung bilden eine Linksableitung Leider muss man bei den Expansionen nichtdeterministisch zwischen verschiedenen Regeln auswählen Zur Korrektheit der Konstruktion zeigt man, dass für jedes Item [A → α • B β] gilt: ([A → α • B β], w) `∗ ([A → α B • β], ) gdw. Philip M. Lewis, SUNY B →∗ w Richard E. Stearns, SUNY 1968: Syntax-Directed Transduction LL-Parsing basiert auf dem Item-Kellerautomaten und versucht, die Expansionen deterministisch zu machen ... 92 / 398 Item-Kellerautomat Beispiel: S→ Topdown Parsing | aSb Problem: Die Übergänge des zugehörigen Item-Kellerautomat: 0 1 2 3 4 5 6 7 8 9 93 / 398 [S0 → • S] [S0 → • S] [S → • a S b] [S → a • S b] [S → a • S b] [S → a • S b] [S → •] [S → a • S b] [S → a S b•] [S → a S • b] [S0 → • S] [S → •] [S0 → • S] [S → a S b•] a b Konflikte zwischen Übergängen verhindern die Implementierung des Item-Kellerautomaten als deterministischer Kellerautomat. [S0 → • S] [S → •] [S0 → • S] [S → • a S b] [S → a • S b] [S → a • S b] [S → •] [S → a • S b] [S → • a S b] [S → a S • b] [S → a S • b] [S → a S b•] [S0 → S•] [S0 → S•] Konflikte gibt es zwischen den Übergängen (0, 1) bzw. zwischen (3, 4). 94 / 398 Topdown Parsing 95 / 398 Topdown Parsing Problem: Problem: Konflikte zwischen Übergängen verhindern die Implementierung des Item-Kellerautomaten als deterministischer Kellerautomat. Konflikte zwischen Übergängen verhindern die Implementierung des Item-Kellerautomaten als deterministischer Kellerautomat. Idee 1: GLL Parsing Idee 1: GLL Parsing Bei jedem Konflikt wird virtuell der Stack komplett kopiert und parallel weiter geführt. Bei jedem Konflikt wird virtuell der Stack komplett kopiert und parallel weiter geführt. Idee 2: Recursive Descent & Backtracking Tiefensuche nach einer passenden Ableitung. 95 / 398 95 / 398 Topdown Parsing Struktur des LL(1)-Parsers: Problem: Konflikte zwischen Übergängen verhindern die Implementierung des Item-Kellerautomaten als deterministischer Kellerautomat. Idee 1: GLL Parsing δ Bei jedem Konflikt wird virtuell der Stack komplett kopiert und parallel weiter geführt. Ausgabe M Idee 2: Recursive Descent & Backtracking Tiefensuche nach einer passenden Ableitung. Der Parser sieht ein Fenster der Länge 1 der Eingabe; Idee 3: Recursive Descent & Lookahead er realisiert im Wesentlichen den Item-Kellerautomaten; Konflikte werden durch Betrachten des/der nächsten Zeichens gelöst. die Tabelle M[q, w] enthält die jeweils zuwählende Regel 95 / 398 Topdown Parsing 96 / 398 Topdown Parsing Idee: Idee: Benutze den Item-Kellerautomaten. Benutze den Item-Kellerautomaten. Benutze das nächste Zeichen, um die Regeln für die Expansionen zu bestimmen Benutze das nächste Zeichen, um die Regeln für die Expansionen zu bestimmen Eine Grammatik heißt LL(1) , falls dies immer eindeutig möglich ist. Eine Grammatik heißt LL(1) , falls dies immer eindeutig möglich ist. Definition: Eine reduzierte Grammatik heißt dann LL(1), falls für je zwei verschiedene Regeln A → α , A → α0 ∈ P und jede Ableitung S →∗L u A β mit u ∈ T ∗ gilt: First1 (α β) ∩ First1 (α0 β) = ∅ 97 / 398 Topdown Parsing 97 / 398 Topdown Parsing Beispiel 1: Beispiel 1: S → E → if ( E ) S else S | while ( E ) S | E; id ist LL(1), da First1 (E) = {id} S → E → if ( E ) S else S | while ( E ) S | E; id ist LL(1), da Beispiel 2: S → E → First1 (E) = {id} if ( E ) S else S | if ( E ) S | while ( E ) S | E; id ... ist nicht LL(k) für jedes k > 0. 98 / 398 98 / 398 Vorausschau-Mengen Vorausschau-Mengen Definition: Definition: Für eine Menge L ⊆ T ∗ definieren wir: Für eine Menge L ⊆ T ∗ definieren wir: First1 (L) = { | ∈ L} ∪ {u ∈ T | ∃ v ∈ T ∗ : uv ∈ L} First1 (L) = { | ∈ L} ∪ {u ∈ T | ∃ v ∈ T ∗ : uv ∈ L} Beispiel: Beispiel: ab aabb aaabbb ab aabb aaabbb die Präfixe der Länge 1 99 / 398 Vorausschau-Mengen 99 / 398 Vorausschau-Mengen Rechenregeln: Rechenregeln: First1 (_) ist verträglich mit Vereinigung und Konkatenation: First1 (_) ist verträglich mit Vereinigung und Konkatenation: First1 (∅) First1 (L1 ∪ L2 ) First1 (L1 · L2 ) = = = := First1 (∅) First1 (L1 ∪ L2 ) First1 (L1 · L2 ) ∅ First1 (L1 ) ∪ First1 (L2 ) First1 (First1 (L1 ) · First1 (L2 )) First1 (L1 ) First1 (L2 ) 1 − Konkatenation = = = := ∅ First1 (L1 ) ∪ First1 (L2 ) First1 (First1 (L1 ) · First1 (L2 )) First1 (L1 ) First1 (L2 ) 1 − Konkatenation Beobachtung: Seien L1 , L2 ⊆ T ∪ {} mit L1 6= ∅ 6= L2 . Dann ist: L1 falls 6∈ L1 L1 L2 = (L1 \{}) ∪ L2 sonst Sind alle Regeln von G produktiv, sind alle Mengen First1 (A) nichtleer. 100 / 398 Vorausschau-Mengen Vorausschau-Mengen Für α ∈ (N ∪ T)∗ sind wir interessiert an der Menge: Für α ∈ (N ∪ T)∗ sind wir interessiert an der Menge: First1 (α) = First1 ({w ∈ T ∗ | α →∗ w}) Idee: Behandle separat: 100 / 398 First1 (α) = First1 ({w ∈ T ∗ | α →∗ w}) Idee: Behandle separat: F Sei empty(X) = true gdw. X →∗ . Sj F (X1 . . . Xm ) = i=1 F (Xi ) falls empty(X1 ) ∧ . . . ∧ empty(Xj−1 ) F Sei empty(X) = true gdw. X →∗ . Sj F (X1 . . . Xm ) = i=1 F (Xi ) falls empty(X1 ) ∧ . . . ∧ empty(Xj−1 ) Definiere die -freien First1 -Mengen als Ungleichungssystem F (a) = {a} falls F (A) ⊇ F (Xj ) falls 101 / 398 a∈T A → X1 . . . Xm ∈ P, empty(X1 ) ∧ . . . ∧ empty(Xj−1 ) 101 / 398 Vorausschau-Mengen Vorausschau-Mengen im Beispiel... im Beispiel... E T F wobei 0 1 → E+T | T → T ∗F 0 | F 1 → ( E ) 0 | name 1 E T F | int 2 empty(E) = empty(T) = empty(F) = false . wobei → E+T 0 | T 1 → T ∗F 0 | F 1 → ( E ) 0 | name 1 | int 2 empty(E) = empty(T) = empty(F) = false . ... erhalten wir: F (S0 ) ⊇ F (E) F (E) ⊇ F (T) F (T) ⊇ F (F) F (E) ⊇ F (E) F (T) ⊇ F (T) F (F) ⊇ { ( , name, int} 102 / 398 Schnelle Berechnung von Vorausschau-Mengen 102 / 398 Schnelle Berechnung von Vorausschau-Mengen Beobachtung: c Die Form der Ungleichungen dieser Ungleichungssysteme ist: bzw. x w y 3 x w d für Variablen x, y und d ∈ D. Solche Systeme heißen reine Vereinigungs-Probleme Diese Probleme können mit linearem Aufwand gelöst werden 0 1 2 c Konstruiere den Variablen-Abhängigkeitsgraph zum Ungleichungssystem. c x0 x1 x2 x3 b Vorgehen: D = 2{a,b,c} im Beispiel: a 3 ⊇ {a} ⊇ {b} ⊇ {c} ⊇ {c} x1 ⊇ x0 x2 ⊇ x1 x3 ⊇ x2 x1 ⊇ x3 a b 0 1 x3 ⊇ x3 2 c 103 / 398 Schnelle Berechnung von Vorausschau-Mengen 104 / 398 Schnelle Berechnung von Vorausschau-Mengen c c 3 3 a b a b 0 1 0 1 2 c 2 Vorgehen: c Vorgehen: Konstruiere den Variablen-Abhängigkeitsgraph zum Ungleichungssystem. Innerhalb einer starken Zusammenhangskomponente (→ Tarjan) haben alle Variablen den gleichen Wert Konstruiere den Variablen-Abhängigkeitsgraph zum Ungleichungssystem. Innerhalb einer starken Zusammenhangskomponente (→ Tarjan) haben alle Variablen den gleichen Wert Hat eine SZK keine eingehenden Kanten, erhält man ihren Wert, indem man die kleinste obere Schranke aller Werte in der SZK berechnet 104 / 398 104 / 398 Schnelle Berechnung von Vorausschau-Mengen Schnelle Berechnung von Vorausschau-Mengen a b c 3 a 0 ... für unsere Beispiel-Grammatik: 1 2 First1 : Vorgehen: Konstruiere den Variablen-Abhängigkeitsgraph zum Ungleichungssystem. Innerhalb einer starken Zusammenhangskomponente (→ Tarjan) haben alle Variablen den gleichen Wert Hat eine SZK keine eingehenden Kanten, erhält man ihren Wert, indem man die kleinste obere Schranke aller Werte in der SZK berechnet Gibt es eingehende Kanten, muss man zusätzlich die Werte an deren Startknoten hinzufügen (, int, name S’ E T F 105 / 398 104 / 398 Item-Kellerautomat als LL(1)-Parser zurück zum Beispiel: Item-Kellerautomat als LL(1)-Parser Ist G eine LL(1)-Grammatik, können wir eine Vorausschau-Tabelle mit Items und Nichtterminalen indizieren: Wir setzen M[B, w] = i genau dann wenn (B, i) die Regel S B → γ ist und: w ∈ First1 (γ) {First1 (β) | S0 →∗L u B β } . S → | aSb Die Übergänge des zugehörigen Item-Kellerautomat: 0 1 2 3 4 5 6 7 8 9 [S0 → • S] [S0 → • S] [S → • a S b] [S → a • S b] [S → a • S b] [S → a • S b] [S → •] [S → a • S b] [S → a S b•] [S → a S • b] [S0 → • S] [S → •] [S0 → • S] [S → a S b•] a b [S0 → • S] [S → •] [S0 → • S] [S → • a S b] [S → a • S b] [S → a • S b] [S → •] [S → a • S b] [S → • a S b] [S → a S • b] [S → a S • b] [S → a S b•] [S0 → S•] [S0 → S•] i0 S 0 ... im Beispiel: S → | aSb S 0 a 1 i1 A1 b 0 in An i B w ∈ First1 ( 106 / 398 in An i B Im Beispiel: β0 Ungleichungssystem für Follow1 (B) = Follow1 (S) ⊇ {} Follow1 (B) ⊇ F (Xj ) falls Follow1 (B) ⊇ Follow1 (A) falls {First1 (β) | S S→ | aSb Die Übergänge des zugehörigen Item-Kellerautomat: β w ∈ First1 ( ) →∗L ) 107 / 398 β1 γ 0 β Item-Kellerautomat als LL(1)-Parser i0 S 0 i1 A1 S β1 γ Konflikte gibt es zwischen den Übergängen (0, 1) bzw. zwischen (3, 4). Item-Kellerautomat als LL(1)-Parser β0 uBβ } A → α B X1 . . . Xm ∈ P, empty(X1 ) ∧ . . . ∧ empty(Xj−1 ) A → α B X1 . . . Xm ∈ P, empty(X1 ) ∧ . . . ∧ empty(Xm ) 0 1 2 3 4 5 6 7 8 9 [S0 → • S] [S0 → • S] [S → • a S b] [S → a • S b] [S → a • S b] [S → a • S b] [S → •] [S → a • S b] [S → a S b•] [S → a S • b] [S0 → • S] [S → •] [S0 → • S] [S → a S b•] a b Vorausschautabelle: S 108 / 398 0 a 1 [S0 → • S] [S → •] [S0 → • S] [S → • a S b] [S → a • S b] [S → a • S b] [S → •] [S → a • S b] [S → • a S b] [S → a S • b] [S → a S • b] [S → a S b•] [S0 → S•] [S0 → S•] b 0 109 / 398 Topdown-Parsing Topdown-Parsing Diskussion Diskussion Einer praktischen Implementation von LL(1)-Parsern über recursive Descent steht nichts im Wege Einer praktischen Implementation von LL(1)-Parsern über recursive Descent steht nichts im Wege Jedoch kann nur eine Teilmenge der deterministisch kontextfreien Sprachen auf diesem Weg gelesen werden. Jedoch kann nur eine Teilmenge der deterministisch kontextfreien Sprachen auf diesem Weg gelesen werden. Ausweg: Schritt von LL(1) auf LL(k) Die Größe der auftretenden Mengen steigt mit k rapide Leider reichen auch LL(k) Parser nicht aus, um alle deterministisch kontextfreien Sprachen zu erkennen. In praktischen Systemen wird darum meist nur der Fall k = 1 implementiert ... 110 / 398 110 / 398 Syntaktische Analyse Kapitel 4: Bottom-up Analyse Donald E. Knuth, Stanford 112 / 398 111 / 398 Bottom-up Analyse Bottom-up Analyse Achtung: Achtung: Viele Grammatiken sind nicht LL(k) ! Viele Grammatiken sind nicht LL(k) ! Eine Grund dafür ist: Eine Grund dafür ist: Definition Definition Die Grammatik G heißt links-rekursiv, falls Die Grammatik G heißt links-rekursiv, falls A →+ A β für ein A ∈ N , β ∈ (T ∪ N)∗ A →+ A β Beispiel: 113 / 398 E T F für ein A ∈ N , β ∈ (T ∪ N)∗ → E+T 0 | T 1 → T ∗F 0 | F 1 → ( E ) 0 | name 1 | int 2 ... ist links-rekursiv 113 / 398 Bottom-up Analyse Bottom-up Analyse Satz: Satz: Ist die Grammatik G reduziert und links-rekursiv, dann ist G nicht LL(k) für jedes k. Ist die Grammatik G reduziert und links-rekursiv, dann ist G nicht LL(k) für jedes k. Beweis: Beweis: Sei A → A β | α ∈ P und A erreichbar von S Sei A → A β | α ∈ P und A erreichbar von S Annahme: G ist LL(k) Annahme: G ist LL(k) S A n A A n β β γ Firstk ( ) 114 / 398 Bottom-up Analyse 114 / 398 Bottom-up Analyse Satz: Satz: Ist die Grammatik G reduziert und links-rekursiv, dann ist G nicht LL(k) für jedes k. Ist die Grammatik G reduziert und links-rekursiv, dann ist G nicht LL(k) für jedes k. Beweis: Beweis: S Sei A → A β | α ∈ P und A erreichbar von S A n A Annahme: G ist LL(k) A S Sei A → A β | α ∈ P und A erreichbar von S A n A Annahme: G ist LL(k) β n β A γ A α Firstk ( β n β γ β α ) Firstk ( 114 / 398 Bottom-up Analyse ) 114 / 398 Bottom-up Analyse Satz: Satz: Ist die Grammatik G reduziert und links-rekursiv, dann ist G nicht LL(k) für jedes k. Ist die Grammatik G reduziert und links-rekursiv, dann ist G nicht LL(k) für jedes k. Beweis: Beweis: Sei A → A β | α ∈ P und A erreichbar von S Sei A → A β | α ∈ P und A erreichbar von S Annahme: G ist LL(k) Annahme: G ist LL(k) ⇒Firstk (α β n γ) ∩ Firstk (α β n+1 γ) = ∅ ⇒Firstk (α β n γ) ∩ Firstk (α β n+1 γ) = ∅ Fall 1: β →∗ — Widerspruch !!! Fall 2: β →∗ w 6= ==⇒ Firstk (α β k γ) ∩ Firstk (α β k+1 γ) 6= ∅ 114 / 398 114 / 398 Shift-Reduce-Parser Shift-Reduce-Parser Beispiel: S A B Idee: Wir verzögern die Entscheidung ob wir reduzieren bis wir wissen, ob die Eingabe einer rechten Seite entspricht! Konstruktion: Der Kellerautomat: Shift-Reduce-Parser MGR Zustände: Anfangszustand: Endzustand: q0 a A b AB q0 S Die Eingabe wird sukzessive auf den Keller geschoben. Liegt oben auf dem Keller eine vollständige rechte Seite (ein Handle) vor, wird dieses durch die zugehörige linke Seite ersetzt (reduziert) → → → AB a b q0 , f , a, b, A, B, S; q0 f a q0 a A b Ab B S f 115 / 398 Shift-Reduce-Parser Shift-Reduce-Parser Konstruktion: Allgemein konstruieren wir einen Automaten Konstruktion: Allgemein konstruieren wir einen Automaten MGR = (Q, T, δ, q0 , F) mit: Q = T ∪ N ∪ {q0 , f } 116 / 398 (q0 , f MGR = (Q, T, δ, q0 , F) mit: neu); Q = T ∪ N ∪ {q0 , f } F = {f }; Übergänge: δ = (q0 , f neu); F = {f }; Übergänge: {(q, x, q x) | q ∈ Q, x ∈ T} ∪ // {(q α, , q A) | q ∈ Q, A → α ∈ P} ∪ // {(q0 S, , f )} // Shift-Übergänge Reduce-Übergänge Abschluss δ = {(q, x, q x) | q ∈ Q, x ∈ T} ∪ // {(q α, , q A) | q ∈ Q, A → α ∈ P} ∪ // {(q0 S, , f )} // Shift-Übergänge Reduce-Übergänge Abschluss Beispiel-Berechnung: (q0 , a b) ` ` ` (q0 a , b) ` (q0 A b , ) ` (q0 S, ) ` (q0 A, b) (q0 A B , ) (f , ) 117 / 398 Shift-Reduce-Parser 117 / 398 Bottom-up Analyse Offenbar gilt: Idee: Die Folge der Reduktionen entspricht einer reversen Rechtsableitung für die Eingabe Dazu versuchen wir, für den Shift-Reduce-Parser MGR Reduktionsstellen zu identifizieren ... Zur Korrektheit zeigt man, dass gilt: (, w) `∗ (A, ) gdw. Der Shift-Reduce-Kellerautomat MGR nicht-deterministisch Wir rekonstruieren reverse Rechtsableitungen! A →∗ w die Betrachte eine Berechnung dieses Kellerautomaten: (q0 α γ, v) ` (q0 α B, v) `∗ (q0 S, ) ist i.a. auch α γ nennen wir zuverlässiges Präfix für das vollständige Item [B → γ•] . Um ein deterministisches Parse-Verfahren zu erhalten, muss man die Reduktionsstellen identifizieren ==⇒ LR-Parsing 118 / 398 119 / 398 Bottom-up Analyse Dann ist αγ Bottom-up Analyse zuverlässig für [B → γ•] gdw. A0 i0 Dann ist S →∗R α B v αγ zuverlässig für [B → γ•] gdw. A0 i0 A1 i1 α1 A2 i2 α2 B i αm A1 i1 α1 A2 i2 α2 S →∗R α B v B i αm γ γ ... wobei α = α1 . . . αm ... wobei α = α1 . . . αm 120 / 398 Bottom-up Analyse Umgekehrt können wir zu jedem möglichen Wort α0 die Menge aller möglicherweise später passenden Regeln ermitteln ... 120 / 398 Charakteristischer Automat Das Item [B → γ • β] α0 = α γ : heißt gültig für α0 gdw. S →∗R α B v mit Beobachtung: Die Menge der zuverlässigen Präfixe aus (N ∪ T)∗ für (vollständige) Items kann mithilfe eines endlichen Automaten, aus dem Kellerinhalt des Shift-Reduce-Parsers berechnet werden: A0 i0 A1 i1 α1 Zustände: Items A2 i2 α2 Anfangszustand: [S0 → • S] Endzustände: {[B → γ•] | B → γ ∈ P} B i αm γ Übergänge: (1) (2) β ... wobei α = α1 . . . αm ([A → α • X β],X,[A → α X • β]), ([A → α • B β],, [B → • γ]), X ∈ (N ∪ T), A → α X β ∈ P; A → α B β , B → γ ∈ P; Den Automaten c(G) nennen wir charakteristischen Automaten für G. :-) 122 / 398 121 / 398 Charakteristischer Automat im Beispiel: E T F Charakteristischer Automat im Beispiel: → E+T 0 | T 1 → T ∗F 0 | F 1 → ( E ) 0 | int 2 E T F E S’ E E S’ E E+T S’ + E E E E +T E T E E+ T E E E+T T T T T∗F E+T T T ∗F E ∗ T F T T∗ F T T∗F T F ( F (E) T∗F F ( E) T E int E T T T ∗F T F F ( E) F int + F (E ) F ( ) F (E) F (E) int F E +T T E E+ T T T∗ F F (E ) E E+T ∗ T T∗F F (E) F F T F E T F T E T E T S’ E T E → E+T 0 | T 1 → T ∗F 0 | F 1 → ( E ) 0 | int 2 E ) int F int F 123 / 398 int 123 / 398 Kanonischer LR(0)-Automat Kanonischer LR(0)-Automat Den kanonischen LR(0)-Automaten LR(G) erhalten wir aus c(G) , indem wir: 1 nach jedem lesenden Übergang beliebig viele -Übergänge einschieben 2 die Teilmengenkonstruktion anwenden. + 1 6 int E ... im Beispiel: int F q0 9 + int 4 F int 0 T Dazu konstruieren wir: 3 * 11 = {[S0 → • E], {[E → • E + T], {[E → • T], {[T → • T ∗ F]} {[T → • F], {[F → • ( E ) ], {[F → • int]} ( ( F E 5 T ( ) q1 = δ(q0 , E) = {[S0 → E•], {[E → E • + T]} q2 = δ(q0 , T) = {[E → T•], {[T → T • ∗ F]} q3 = δ(q0 , F) = {[T → F•]} q4 = δ(q0 , int) = {[F → int•]} 8 ( T 2 7 * F 10 124 / 398 Kanonischer LR(0)-Automat 125 / 398 Kanonischer LR(0)-Automat Beachte: q5 q6 = = δ(q0 , ( ) δ(q1 , +) = = {[F → ( • E ) ], {[E → • E + T], {[E → • T], {[T → • T ∗ F], {[T → • F], {[F → • ( E ) ], {[F → • int]} {[E → E + • T], {[T → • T ∗ F], {[T → • F], {[F → • ( E ) ], {[F → • int]} q7 = δ(q2 , ∗) = {[T → T ∗ • F], {[F → • ( E ) ], {[F → • int]} Der kanonische LR(0)-Automat kann auch direkt aus der Grammatik konstruiert werden. Man benötigt die Hilfsfunktion: δ∗ (-Abschluss) q8 = δ(q5 , E) = {[F → ( E • ) ]} {[E → E • + T]} q9 = δ(q6 , T) = {[E → E + T•], {[T → T • ∗ F]} δ∗ (q) = q ∪ {[B → • γ] | ∃ [A → α • B0 β 0 ] ∈ q , ∃ β ∈ (N ∪ T)∗ : B0 →∗ B β} Dann definiert man: q10 = δ(q7 , F) = {[T → T ∗ F•]} Anfangszustand δ∗ {[S0 → • S]} q11 = δ(q8 , ) ) = {[F → ( E ) •]} Zustände: Mengen von Items; Endzustände: {q | ∃ A → α ∈ P : [A → α •] ∈ q} Übergänge: δ(q, X) = δ∗ {[A → α X • β] | [A → α • X β] ∈ q} 127 / 398 126 / 398 LR(0)-Parser LR(0)-Parser Idee zu einem Parser: Der Parser verwaltet ein zuverlässiges Präfix α = X1 . . . Xm auf dem Keller und benutzt LR(G) , um Reduktionsstellen zu entdecken. Er kann mit einer Regel A → γ reduzieren, falls [A → γ •] für α gültig ist ... im Beispiel: Optimierung: Damit der Automat nicht immer wieder neu über den Kellerinhalt laufen muss, kellern wir anstelle der Xi jeweils die Zustände! Reduktion mit A → γ führt zum Poppen der obersten |γ| Zustände und Fortsetzung mit dem Zustand auf dem Keller unter Eingabe A. q1 = {[S0 → E •], {[E → E • + T]} q2 = {[E → T •], {[T → T • ∗ F]} q9 = {[E → E + T •], {[T → T • ∗ F]} q3 = {[T → F •]} q10 = {[T → T ∗ F •]} q4 = {[F → int •]} q11 = {[F → ( E ) •]} Die Endzustände q1 , q2 , q9 enthalten mehr als ein gültiges Item ⇒ nicht deterministisch! Achtung: Dieser Parser ist nur dann deterministisch, wenn jeder Endzustand des kanonischen LR(0)-Automaten keine Konflikte enthält. 128 / 398 129 / 398 LR(0)-Parser LR(0)-Parser Die Konstruktion des LR(0)-Parsers: Zustände: Q ∪ {f } Zur Korrektheit: Man zeigt: (f neu) Anfangszustand: q0 Die akzeptierenden Berechnungen des LR(0)-Parsers stehen in eins-zu-eins Beziehung zu denen des Shift-Reduce-Parsers MGR . Endzustand: f Übergänge: Shift: Reduce: (p, a, p q) (p q1 . . . qm , , p q) Finish: wobei (q0 p, , f ) falls falls falls Wir folgern: q = δ(p, a) 6= ∅ [A → X1 . . . Xm •] ∈ qm , q = δ(p, A) [S0 → S•] ∈ p ==⇒ ==⇒ LR(G) = (Q, T, δ, q0 , F) . Die akzeptierte Sprache ist genau L(G) Die Folge der Reduktionen einer akzeptierenden Berechnung für ein Wort w ∈ T liefert eine reverse Rechts-Ableitung von G für w 130 / 398 LR(0)-Parser 131 / 398 LR(k)-Grammatik Achtung: Leider ist der LR(0)-Parser im allgemeinen nicht-deterministisch Idee: Benutze k-Vorausschau, um Konflikte zu lösen. Wir identifizieren zwei Gründe: Definition: Reduce-Reduce-Konflikt: [A → γ •] , [A0 → γ 0 •] ∈ q mit Shift-Reduce-Konflikt: [A → γ •] , [A0 → α • a β] ∈ q Die reduzierte kontextfreie Grammatik G heißt LR(k)-Grammatik, falls für Firstk (w) = Firstk (x) aus: S →∗R α A w → α β w folgt: α = α0 ∧ A = A0 ∧ w0 = x ∗ 0 0 0 S →R α A w → α β x A 6= A0 ∨ γ 6= γ 0 mit a∈T für einen Zustand q ∈ Q . Solche Zustände nennen wir LR(0)-ungeeignet. 133 / 398 132 / 398 LR(k)-Grammatik LR(k)-Grammatik im Beispiel: im Beispiel: (1) S→A | B A→aAb | 0 (1) B→aBbb | 1 ... ist nicht LL(k) für jedes k — aber LR(0) : Sei S →∗R α X w → α β w . Formen: Dann ist α β A , B , an a A b , an a B b b , an 0 , an 1 S→A | B A→aAb | 0 B→aBbb | 1 ... ist nicht LL(k) für jedes k — aber LR(0) : von einer der Sei S →∗R α X w → α β w . Formen: Dann ist α β A , B , an a A b , an a B b b , an 0 , an 1 (n ≥ 0) (2) S→aAc von einer der (n ≥ 0) A→Abb | b ... ist ebenfalls LR(0) : Sei S →∗R α X w → α β w . Formen: Dann ist α β von einer der ab , aAbb , aAc 134 / 398 134 / 398 LR(k)-Grammatik LR(k)-Grammatik im Beispiel: im Beispiel: (3) S→aAc LR(1) : ... ist nicht LR(0), aber A→bbA | b Für S →∗R α X w → α β w von einer der Formen: mit {y} = Firstk (w) ist (3) S→aAc LR(1) : Für S →∗R α X w → α β w von einer der Formen: αβy a b2n b c , a b2n b b A c , a A c ... ist nicht LR(0), aber A→bbA | b mit {y} = Firstk (w) ist αβy a b2n b c , a b2n b b A c , a A c (4) S→aAc k ≥ 0: ... ist nicht LR(k) für jedes A→bAb | b Betrachte einfach die Rechtsableitungen: S →∗R a bn A bn c → a bn b bn c 135 / 398 LR(1)-Items Sei 135 / 398 LR(1)-Items S i0 k = 1. A1 i1 γ0 Idee: Wir statten Items mit 1-Vorausschau aus γ1 A m im Ein LR(1)-Item ist dann ein Paar: [B → α • β, x] , x ∈ Follow1 (B) = Dieses Item ist gültig für γ α falls: S →∗R γ B w mit [ B i γm {First1 (ν) | S →∗ µ B ν} α β {x} = First1 (w) ... wobei γ0 . . . γm = γ 136 / 398 LR(1)-Items 137 / 398 Der charakteristische LR(1)-Automat S i0 Der Automat c(G, 1) : A1 i1 γ0 γ1 Zustände: LR(1)-Items A m im Anfangszustand: [S0 → • S, ] Endzustände: {[B → γ•, x] | B → γ ∈ P, x ∈ Follow1 (B)} B i γm α Übergänge: (1) ([A → α • X β, x],X,[A → α X • β, x]), X ∈ (N ∪ T) (2) ([A → α • B β, x],, [B → • γ, x0 ]), A → α B β , B → γ ∈ P, x0 ∈ First1 (β) {x}; β ... wobei γ0 . . . γm = γ Die Menge der gültigen LR(1)-Items für zuverlässige Präfixe berechnen wir wieder mithilfe eines endlichen Automaten 137 / 398 138 / 398 Der charakteristische LR(1)-Automat Der kanonische LR(1)-Automat Den kanonischen LR(1)-Automaten LR(G, 1) erhält man aus c(G, 1) , indem man nach jedem Übergang beliebig viele liest und dann den Automaten deterministisch macht ... Der Automat c(G, 1) : Zustände: LR(1)-Items Anfangszustand: [S0 → • S, ] Endzustände: {[B → γ•, x] | B → γ ∈ P, x ∈ Follow1 (B)} Übergänge: (1) ([A → α • X β, x],X,[A → α X • β, x]), X ∈ (N ∪ T) (2) ([A → α • B β, x],, [B → • γ, x0 ]), A → α B β , B → γ ∈ P, x0 ∈ First1 (β) {x}; Dieser Automat arbeitet wie c(G) — verwaltet aber zusätzlich ein 1-Präfix aus dem Follow1 der linken Seiten. 139 / 398 138 / 398 Der kanonische LR(1)-Automat Der kanonische LR(1)-Automat Den kanonischen LR(1)-Automaten LR(G, 1) erhält man aus c(G, 1) , indem man nach jedem Übergang beliebig viele liest und dann den Automaten deterministisch macht ... Man kann ihn aber auch direkt aus der Grammatik konstruieren Wie bei LR(0) benötigt man eine Hilfsfunktion: Den kanonischen LR(1)-Automaten LR(G, 1) erhält man aus c(G, 1) , indem man nach jedem Übergang beliebig viele liest und dann den Automaten deterministisch macht ... Man kann ihn aber auch direkt aus der Grammatik konstruieren Wie bei LR(0) benötigt man eine Hilfsfunktion: δ∗ (q) = q ∪ {[B → • γ, x] | ∃ [A → α • B0 β 0 , x0 ] ∈ q , β ∈ (N ∪ T)∗ : B0 →∗ B β ∧ x ∈ First1 (β β 0 ) {x0 }} δ∗ (q) = q ∪ {[B → • γ, x] | ∃ [A → α • B0 β 0 , x0 ] ∈ q , β ∈ (N ∪ T)∗ : B0 →∗ B β ∧ x ∈ First1 (β β 0 ) {x0 }} Dann definiert man: Zustände: Mengen von LR(1)-Items; Anfangszustand: δ∗ {[S0 → • S, ]} Endzustände: {q | ∃ A → α ∈ P : [A → α •, x] ∈ q} Übergänge: δ(q, X) = δ∗ {[A → α X • β, x] | [A → α•X β, x] ∈ q} 139 / 398 139 / 398 Der kanonische LR(1)-Automat Der kanonische LR(1)-Automat im Beispiel: q0 q1 q2 {[S0 → • E ], {[E → • E + T ], {[E → • T ], {[T → • T ∗ F ], {[T → • F ], {[F → • ( E ) ], {[F → • int ]} = = = im Beispiel: δ(q0 , E) δ(q0 , T) = = {[S0 → E • {[E → E • + T {[E → T • {[T → T • ∗ F ], ]} ], q3 = δ(q0 , F) q4 = δ(q0 , int) q5 = δ(q0 , ( ) = {[T → F • = {[F → ( • E ) {[E → • E + T {[E → • T {[T → • T ∗ F {[T → • F {[F → • ( E ) {[F → • int q0 ]} {[F → int • {[S0 → • E, {}], {[E → • E + T, {, +}], {[E → • T, {, +}], {[T → • T ∗ F, {, +, ∗}], {[T → • F, {, +, ∗}], {[F → • ( E ) , {, +, ∗}], {[F → • int, {, +, ∗}]} = ]} ], ], ], ], ], q1 = δ(q0 , E) = ], ]} ]} 140 / 398 q2 = δ(q0 , T) = {[S0 → E • {[E → E • + T {[E → T • {[T → T • ∗ F ], ]} ], q3 = δ(q0 , F) q4 = δ(q0 , int) q5 = δ(q0 , ( ) = {[T → F • ]} {[F → int • = {[F → ( • E ) {[E → • E + T {[E → • T {[T → • T ∗ F {[T → • F {[F → • ( E ) {[F → • int ]} ], ], ], ], ], ], ]} ]} 140 / 398 Der kanonische LR(1)-Automat Der kanonische LR(1)-Automat im Beispiel: q0 {[S0 → • E, {}], {[E → • E + T, {, +}], {[E → • T, {, +}], {[T → • T ∗ F, {, +, ∗}], {[T → • F, {, +, ∗}], {[F → • ( E ) , {, +, ∗}], {[F → • int, {, +, ∗}]} = q1 = q2 = im Beispiel: δ(q0 , E) δ(q0 , T) = = q3 = δ(q0 , F) q4 = δ(q0 , int) q5 = δ(q0 , ( ) = {[T → F • , {, +, ∗}]} q0 {[S0 → • E, {}], {[E → • E + T, {, +}], {[E → • T, {, +}], {[T → • T ∗ F, {, +, ∗}], {[T → • F, {, +, ∗}], {[F → • ( E ) , {, +, ∗}], {[F → • int, {, +, ∗}]} = {[F → int • , {, +, ∗}]} = {[S0 → E • , {}], {[E → E • + T, {, +}]} {[E → T • , {, +}], {[T → T • ∗ F, {, +, ∗}]} {[F → ( • E ) {[E → • E + T {[E → • T {[T → • T ∗ F {[T → • F {[F → • ( E ) {[F → • int ], ], ], ], ], q1 = δ(q0 , E) = {[S0 → E • , {}], {[E → E • + T, {, +}]} q2 = δ(q0 , T) = {[E → T • , {, +}], {[T → T • ∗ F, {, +, ∗}]} ], ]} q3 = δ(q0 , F) q4 = δ(q0 , int) q5 = δ(q0 , ( ) = {[T → F • , {, +, ∗}]} {[F → int • , {, +, ∗}]} = {[F → ( • E ) , {, +, ∗}], {[E → • E + T, { ) , +}], {[E → • T, { ) , +}], {[T → • T ∗ F, { ) , +, ∗}], {[T → • F, { ) , +, ∗}], {[F → • ( E ) , { ) , +, ∗}], {[F → • int, { ) , +, ∗}]} 140 / 398 Der kanonische LR(1)-Automat 140 / 398 Der kanonische LR(1)-Automat im Beispiel: q05 q6 = = δ(q5 , ( ) δ(q1 , +) = = im Beispiel: {[F → ( • E ) {[E → • E + T {[E → • T {[T → • T ∗ F {[T → • F {[F → • ( E ) {[F → • int {[E → E + • T {[T → • T ∗ F {[T → • F {[F → • ( E ) {[F → • int ], q7 = ], δ(q2 , ∗) = ], ], ], ], ], ]} q05 = δ(q5 , ( ) = ]} = δ(q5 , E) = {[F → ( E • ) {[E → E • + T ]} ]} q9 = δ(q6 , T) = {[E → E + T • {[T → T • ∗ F ], ], ], ], ], q8 ], ]} {[T → T ∗ • F {[F → • ( E ) {[F → • int ]} q10 = δ(q7 , F) = {[T → T ∗ F • ]} q11 = δ(q8 , ) ) = {[F → ( E ) • ]} q6 = δ(q1 , +) = {[F → ( • E ) , { ) , +, ∗}], q7 {[E → • E + T, { ) , +}], {[E → • T, { ) , +}], {[T → • T ∗ F, { ) , +, ∗}], {[T → • F, { ) , +, ∗}], q8 {[F → • ( E ) , { ) , +, ∗}], {[F → • int, { ) , +, ∗}]} q9 {[E → E + • T ], {[T → • T ∗ F ], ], q10 {[T → • F {[F → • ( E ) ], {[F → • int ]} q11 = δ(q2 , ∗) = δ(q5 , E) = {[F → ( E • ) {[E → E • + T ]} ]} = δ(q6 , T) = {[E → E + T • {[T → T • ∗ F ], = δ(q7 , F) = {[T → T ∗ F • ]} = δ(q8 , ) ) = {[F → ( E ) • ]} q6 = = δ(q5 , ( ) δ(q1 , +) = = ]} 141 / 398 Der kanonische LR(1)-Automat im Beispiel: q05 ], ], ]} = 141 / 398 Der kanonische LR(1)-Automat {[T → T ∗ • F {[F → • ( E ) {[F → • int im Beispiel: {[F → ( • E ) , { ) , +, ∗}], q7 {[E → • E + T, { ) , +}], {[E → • T, { ) , +}], {[T → • T ∗ F, { ) , +, ∗}], {[T → • F, { ) , +, ∗}], q8 {[F → • ( E ) , { ) , +, ∗}], {[F → • int, { ) , +, ∗}]} q9 {[E → E + • T, {, +}], {[T → • T ∗ F, {, +, ∗}], {[T → • F, {, +, ∗}], q10 {[F → • ( E ) , {, +, ∗}], {[F → • int, {, +, ∗}]} q11 = δ(q2 , ∗) = {[T → T ∗ • F {[F → • ( E ) {[F → • int ], ], q05 = δ(q5 , ( ) = ]} = δ(q5 , E) = {[F → ( E • ) {[E → E • + T ]} ]} = δ(q6 , T) = {[E → E + T • {[T → T • ∗ F ], ]} = δ(q7 , F) = {[T → T ∗ F • ]} = δ(q8 , ) ) = {[F → ( E ) • ]} 141 / 398 q6 = δ(q1 , +) = {[F → ( • E ) , { ) , +, ∗}], q7 {[E → • E + T, { ) , +}], {[E → • T, { ) , +}], {[T → • T ∗ F, { ) , +, ∗}], {[T → • F, { ) , +, ∗}], q8 {[F → • ( E ) , { ) , +, ∗}], {[F → • int, { ) , +, ∗}]} q9 {[E → E + • T, {, +}], {[T → • T ∗ F, {, +, ∗}], {[T → • F, {, +, ∗}], q10 {[F → • ( E ) , {, +, ∗}], {[F → • int, {, +, ∗}]} q11 = δ(q2 , ∗) = {[T → T ∗ • F, {, +, ∗}], {[F → • ( E ) , {, +, ∗}], {[F → • int, {, +, ∗}]} = δ(q5 , E) = {[F → ( E • ) , {, +, ∗}]} {[E → E • + T, { ) , +}]} = δ(q6 , T) = {[E → E + T • , {, +}], {[T → T • ∗ F, {, +, ∗}]} = δ(q7 , F) = {[T → T ∗ F • , {, +, ∗}]} = δ(q8 , ) ) = {[F → ( E ) • , {, +, ∗}]} 141 / 398 Der kanonische LR(1)-Automat Der kanonische LR(1)-Automat im Beispiel: + 1 im Beispiel: int E int q02 = δ(q05 , T) = {[E → T•, { ) , +}], q07 {[T → T • ∗ F, { ) , +, ∗}]} q03 = δ(q05 , F) = {[F → F•, { ) , +, ∗}]} q04 = δ(q05 , int) = {[F → int•, { ) , +, ∗}]} q06 = δ(q8 , +) = {[E → E + • T, { ) , +}], q09 {[T → • T ∗ F, { ) , +, ∗}], {[T → • F, { ) , +, ∗}], {[F → • ( E ) , { ) , +, ∗}], q010 {[F → • int, { ) , +, ∗}]} q011 = q08 δ(q9 , ∗) {[T → T ∗ • F, { ) , +, ∗}], {[F → • ( E ) , { ), +, ∗}], {[F → • int, { ) , +, ∗}]} = = δ(q05 , E) = {[F → ( E • ) , { ) , +, ∗}]} {[E → E • + T, { ) , +}]} = δ(q06 , T) = {[E → E + T•, { ) , +}], {[T → T • ∗ F, { ) , +, ∗}]} = δ(q07 , F) = {[T → T ∗ F•, { ) , +, ∗}]} = δ(q08 , ) ) = {[F → ( E ) •, { ) , +, ∗}]} F 9 + int 4 int 0 T 6 F 3 * 11 ( ( F ) E 5 8 ( T T 2 7 * F 10 142 / 398 Der kanonische LR(1)-Automat im Beispiel: int int int F 5 T 9’ Im Beispiel hat sich die Anzahl der Zustände fast verdoppelt ... und es kann noch schlimmer kommen + F 4’ int ( int ( Diskussion: T 6’ int int 3 9 + 4 F T 6 E 0 Der kanonische LR(1)-Automat + 1 143 / 398 3’ E ( T( F 5’ 2 ( T F ) ) 8’ 7 ( * 2’ * 11’ (8 E Die Konflikte in den Zuständen q1 , q2 , q9 sind nun aufgelöst ! z.B. haben wir für: * 11 F mit: 10 7’ * q9 = {[E → E + T•, {, +}], {[T → T • ∗ F, {, +, ∗}]} F {, +} ∩ (First1 (∗ F) {, +, ∗}) = {, +} ∩ {∗} = ∅ 10’ 143 / 398 Der LR(1)-Parser: 144 / 398 Der LR(1)-Parser: Mögliche Aktionen sind: shift // Shift-Operation reduce (A → γ) // Reduktion mit Ausgabe error // Fehler action ... im Beispiel: Ausgabe E T F goto Die goto-Tabelle kodiert die Zustandsübergänge: goto[q, X] = δ(q, X) ∈ Q Die action-Tabelle beschreibt für jeden Zustand q und möglichen Look-ahead w die erforderliche Aktion. 145 / 398 → → → E+T 0 | T 1 T ∗F 0 | F 1 ( E ) 0 | int 1 action q1 q2 q02 q3 q03 q4 q04 q9 q09 q10 q010 q11 q011 S0 , 0 E, 1 int ( ) + E, 1 T, 1 T, 1 F, 1 F, 1 E, 0 E, 0 T, 0 T, 0 F, 0 F, 0 T, 1 T, 1 F, 1 F, 1 E, 0 E, 0 T, 0 T, 0 F, 0 F, 0 ∗ s s s T, 1 T, 1 F, 1 F, 1 s s T, 0 T, 0 F, 0 F, 0 146 / 398 Der kanonische LR(1)-Automat Allgemein: Der kanonische LR(1)-Automat Allgemein: Wir identifizieren zwei Konflikte: Reduce-Reduce-Konflikt: [A → γ •, x] , [A0 → γ 0 •, x] ∈ q Wir identifizieren zwei Konflikte: Reduce-Reduce-Konflikt: [A → γ •, x] , [A0 → γ 0 •, x] ∈ q mit A 6= A0 ∨ γ 6= γ 0 Shift-Reduce-Konflikt: [A → γ •, x] , [A0 → α • a β, y] ∈ q mit a ∈ T und x ∈ {a} . mit A 6= A0 ∨ γ 6= γ 0 Shift-Reduce-Konflikt: [A → γ •, x] , [A0 → α • a β, y] ∈ q mit a ∈ T und x ∈ {a} k Firstk (β) k {y} . für einen Zustand q ∈ Q . für einen Zustand q ∈ Q . Solche Zustände nennen wir jetzt LR(1)-ungeeignet Solche Zustände nennen wir jetzt LR(k)-ungeeignet 147 / 398 Spezielle LR(k)-Teilklassen 147 / 398 Spezielle LR(k)-Teilklassen Satz: Satz: Eine reduzierte kontextfreie Grammatik G ist genau dann LR(k) wenn der kanonische LR(k)-Automat LR(G, k) keine LR(k)-ungeeigneten Zustände besitzt. Eine reduzierte kontextfreie Grammatik G ist genau dann LR(k) wenn der kanonische LR(k)-Automat LR(G, k) keine LR(k)-ungeeigneten Zustände besitzt. Diskussion: Unser Beispiel ist offenbar LR(1) Im Allgemeinen hat der kanonische LR(k)-Automat sehr viel mehr Zustände als LR(G) = LR(G, 0) Man betrachtet darum i.a. Teilklassen von LR(k)-Grammatiken, bei denen man nur LR(G) benutzt ... 148 / 398 Spezielle LR(k)-Teilklassen Simple-LR(k) Satz: Idee 1: Benutze Followk -Mengen zur Konflikt-Lösung Eine reduzierte kontextfreie Grammatik G ist genau dann LR(k) wenn der kanonische LR(k)-Automat LR(G, k) keine LR(k)-ungeeigneten Zustände besitzt. Reduce-Reduce-Konflikt: Falls für [A → γ •] , [A0 → γ 0 •] ∈ q Shift-Reduce-Konflikt: Falls für [A → γ •] , [A0 → α • a β] ∈ q Unser Beispiel ist offenbar LR(1) Im Allgemeinen hat der kanonische LR(k)-Automat sehr viel mehr Zustände als LR(G) = LR(G, 0) Die Zuordnung ist unabhängig vom Zustand Die Zuordnung hängt vom Zustand ab mit a∈T, Followk (A) ∩ ({a} Firstk (β) Followk (A0 )) 6= ∅ Man betrachtet darum i.a. Teilklassen von LR(k)-Grammatiken, bei denen man nur LR(G) benutzt ... Zur Konflikt-Auflösung ordnet man den Items in den Zuständen Vorausschau-Mengen zu: 2 mit A 6= A0 ∨ γ 6= γ 0 , Followk (A) ∩ Followk (A0 ) 6= ∅ Diskussion: 1 148 / 398 für einen Zustand q ∈ Q. ==⇒ Simple LR(k) ==⇒ LALR(k) Dann nennen wir den Zustand q SLR(k)-ungeeignet 148 / 398 149 / 398 Simple-LR(k) Simple-LR(k) Definition: Definition: Die reduzierte Grammatik G nennen wir SLR(k) (simple LR(k)) falls der kanonische LR(0)-Automat LR(G) keine SLR(k)-ungeeigneten Zustände enthält. Die reduzierte Grammatik G nennen wir SLR(k) (simple LR(k)) falls der kanonische LR(0)-Automat LR(G) keine SLR(k)-ungeeigneten Zustände enthält. ... im Beispiel: Bei unserer Beispiel-Grammatik treten Konflikte möglicherweise in den Zuständen q1 , q2 , q9 auf: q1 = {[S0 → E•], {[E → E • + T]} Follow1 (S0 ) ∩ {+} {. . .} = {} ∩ {+} = ∅ 150 / 398 Simple-LR(k) 150 / 398 Lookahead-LR(k) Idee 2: Berechne Followk -Mengen für jedes Item Idee 2: – abhängig vom Zustand q Definition: Die reduzierte Grammatik G nennen wir SLR(k) (simple LR(k)) falls der kanonische LR(0)-Automat LR(G) keine SLR(k)-ungeeigneten Zustände enthält. Für [A → α • β] ∈ q definieren wir: Λk (q, [A → α • β]) ... im Beispiel: = // {Firstk (w) | S0 →∗R γ A w ∧ δ(q0 , γ α) = q} ⊆ Followk (A) Bei unserer Beispiel-Grammatik treten Konflikte möglicherweise in den Zuständen q1 , q2 , q9 auf: q1 = {[S0 → E•], {[E → E • + T]} q2 = {[E → T•], {[T → T • ∗ F]} q9 = {[E → E + T•], {[T → T • ∗ F]} Follow1 (S0 ) ∩ {+} {. . .} = {} ∩ {+} = ∅ Follow1 (E) ∩ {∗} {. . .} = = Follow1 (E) ∩ {∗} {. . .} = {, +, ) } ∩ {∗} = ∅ {, +, ) } ∩ {∗} ∅ 150 / 398 Lookahead-LR(k) 151 / 398 Lookahead-LR(k) Idee 2: Berechne Followk -Mengen für jedes Item Idee 2: – abhängig vom Zustand q Idee 2: Berechne Followk -Mengen für jedes Item Idee 2: – abhängig vom Zustand q Für [A → α • β] ∈ q definieren wir: Für [A → α • β] ∈ q definieren wir: Λk (q, [A → α • β]) = // {Firstk (w) | S0 →∗R γ A w ∧ δ(q0 , γ α) = q} ⊆ Followk (A) Reduce-Reduce-Konflikt: [A → γ •] , [A0 → γ 0 •] ∈ q mit 0 A 6= A0 ∨ γ 6= γ 0 Λk (q, [A → α • β]) = // {Firstk (w) | S0 →∗R γ A w ∧ δ(q0 , γ α) = q} ⊆ Followk (A) Reduce-Reduce-Konflikt: [A → γ •] , [A0 → γ 0 •] ∈ q wobei: 0 mit 0 Λk (q, [A → γ •]) ∩ Λk (q, [A → γ •]) 6= ∅ A 6= A0 ∨ γ 6= γ 0 wobei: 0 Λk (q, [A → γ •]) ∩ Λk (q, [A → γ •]) 6= ∅ Shift-Reduce-Konflikt: [A → γ •] , [A0 → α • a β] ∈ q mit a∈T wobei: 0 Λk (q, [A → γ •]) ∩ ({a} Firstk (β) Λk (q, [A → α • a β])) 6= ∅ Solche Zustände nennen wir jetzt LALR(k)-ungeeignet 151 / 398 151 / 398 Lookahead-LR(k) Lookahead-LR(k) Definition: Definition: Die reduzierte Grammatik G nennen wir LALR(k), falls der kanonische LR(0)-Automat LR(G) keine LALR(k)-ungeeigneten Zustände enthält Die reduzierte Grammatik G nennen wir LALR(k), falls der kanonische LR(0)-Automat LR(G) keine LALR(k)-ungeeigneten Zustände enthält Bevor wir Beispiele betrachten, überlegen wir erst, wie die Mengen Λk (q, [A → α • β]) berechnet werden können Bevor wir Beispiele betrachten, überlegen wir erst, wie die Mengen Λk (q, [A → α • β]) berechnet werden können Idee: Stelle ein Ungleichungssystem für Λk auf ! Λk (q0 , [S0 → • S]) Λk (q, [A → α X • β]) Λk (q, [A → • γ]) ⊇ ⊇ ⊇ {} Λk (p, [A → α • X β]) Firstk (β) Λk (q, [B → α • A β]) falls falls δ(p, X) = q [B → α • A β] ∈ q 152 / 398 Lookahead-LR(k) im Beispiel: Lookahead-LR(k) S → A → B → im Beispiel: AbB | B a | bB A Der kanonische LR(0)-Automat hat dann die folgenden Zustände: q0 q1 0 = = δ(q0 , S) = 152 / 398 {[S → • S], {[S → • A b B], {[A → • a], {[A → • b B], {[S → • B], {[B → • A]} q2 = δ(q0 , a) = {[A → a•]} q3 = δ(q0 , b) = {[A → b • B], {[B → • A], {[A → • a], {[A → • b B]} {[S0 → S•]} q4 δ(q0 , B) = {[S → B•]} = q5 = δ(q0 , A) = {[S → A • b B], {[B → A•]} q6 = δ(q3 , A) = {[B → A•]} q7 = δ(q3 , B) = {[A → b B•]} Shift-Reduce-Konflikt: q8 = δ(q5 , b) = {[S → A b • B], {[B → • A], {[A → • a], {[A → • b B]} q9 = δ(q8 , B) = {[S → A b B•]} q5 = {[S → A • b B], {[B → A•]} Auflösung mit SLR(1) schlägt fehl: Follow1 (B) ∩ {b} {. . .} = {, b} ∩ {b} = 6 ∅ 153 / 398 Lookahead-LR(k) 154 / 398 Spezielle Bottom-up Verfahren mit LR(G) im Beispiel: 2 1 Konfliktzustand: q5 = {[S → A • b B], {[B → A•]} 6 S b 0 Diskussion: A a a a B 3 Das Beispiel ist folglich nicht SLR(1), aber LALR(1) A 7 b Das Beispiel ist nicht so an den Haaren herbei gezogen, wie es scheint ... b B A 4 5 b 8 9 Umbenennung: A⇒L B Ausschnitt des Ungleichungssystems für Λ1 : Λ1 (q5 , [B → A•]) ⊇ Λ1 (q0 , [B → • A]) Λ1 (q0 , [B → • A]) Λ1 (q0 , [S → • B]) Λ1 (q0 , [S0 → • S]) ⊇ ⊇ ⊇ Λ1 (q0 , [S → • B]) Λ1 (q0 , [S0 → • S]) {} B⇒R S L R a⇒id b⇒∗ / = liefert: → L=R | R → id | ∗ R → L ... d.h. ein Fragment der Grammatik für C-Ausdrücke Auflösung mit LALR(1): Λ1 (q5 , [B → A•]) ∩ First1 (b) {. . .} = {} ∩ {b} {. . .} = ∅ 155 / 398 156 / 398 LALR(1) Syntaktische Analyse Für k = 1 lassen sich die Mengen Λk (q, [A → α • β]) wieder effizient berechnen Kapitel 5: Das verbesserte Ungleichungssystem für Λ1 : Λ1 (q0 , [S0 → • S]) ⊇ Λ1 (q, [A → α X • β]) ⊇ Λ1 (q, [A → • γ]) ⊇ Λ1 (q, [A → • γ]) ⊇ ==⇒ Zusammenfassung {} Λ1 (p, [A → α • X β]) F (Xj ) falls δ(p, X) = q falls [B → α • A X1 . . . Xm ] ∈ q und empty(X1 ) ∧ . . . ∧ empty(Xj−1 ) Λ1 (q, [B → α • A X1 . . . Xm ]) falls [B → α • A X1 . . . Xm ] ∈ q und empty(X1 ) ∧ . . . ∧ empty(Xm ) wieder ein reines Vereinigungsproblem ! 158 / 398 157 / 398 Parsing Verfahren Parsing Verfahren determinististische Sprachen determinististische Sprachen = LR(1) = ... = LR(k) = LR(1) = ... = LR(k) LALR(k) SLR(k) LALR(k) LR(0) SLR(k) reguläre Sprachen LR(0) LL(1) LL(k) Diskussion: Alle kontextfreien Sprachen, die sich mit einem deterministischen Kellerautomaten parsen lassen, können durch eine LR(1)-Grammatik beschrieben werden. reguläre Sprachen LL(1) Durch LR(0)-Grammatiken lassen sich alle präfixfreien deterministisch kontextfreien Sprachen beschreiben LL(k) Die Sprachklassen zu LL(k)-Grammatiken bilden dagegen eine Hierarchie innerhalb der deterministisch kontextfreien Sprachen. 160 / 398 159 / 398 Lexikalische und syntaktische Analyse: Lexikalische und syntaktische Analyse: Vom Regulären Ausdruck zum Endlichen Automaten Prinzip von Spezifikation und Realisierung: 012 34 f ∅ 0 0 | [1-9][0-9]* Generator * | 0 0 2 11 a f 012 012 f 0 [0−9] 1 a 0 2 34 f ∅ . 22 34 f 0 1 2 01 f 01 [1−9] a . 01 01 2 t 33 3 b a a a b 34 34 f ∅ ∅ f 3 a | a 2 b a 44 f ∅ 4 b a 1 4 b b Vom Endlichen Automaten zum Scanner E→E{op}E Generator 02 023 a w r i t e l n ( " H a l l o " ) ; b a 1 161 / 398 A 4 14 B ∅ 162 / 398 Lexikalische und syntaktische Analyse: Lexikalische und syntaktische Analyse: Vom charakteristischen zum kanonischen Automaten: E Berechnung von Vorausschau-Mengen: F (S ) ⊇ F (E) ⊇ F (T) ⊇ 0 F (E) F (T) F (F) F (E) ⊇ F (E) F (T) ⊇ F (T) F (F) ⊇ { ( , name, int} S’ E E E+T E T T T∗F S’ E E E +T E T T T ∗F T F + + E (, int, name a b c S’ E T 1 T E E+ T E E+T int T 1 ∗ T∗ F int int F T T T∗F F 0 F 3 T F ( F (E) F int F ( E) F int F ( E F (E ) E 5 ) F (E) T T 2 * Vom Item-Kellerautomat zum LL(1)-Parser: i0 S 0 i1 A 1 A 0 B 0 in A n i B a b 7 F 10 Vom Shift-Reduce-Parser zum LR(1)-Parser: β0 S i0 + 1 β1 δ int γ1 A m im 0 F ( α 3 F 5 B i γm T β T 6’ int int 3’ E ( T( F 5’ 2 ( T 2’ 9’ + F 4’ ( int M ) 9 + 4 int Ausgabe T 6 int E A1 i1 γ0 β γ w ∈ First1 ( ) 8 ( ( int S 0 * 11 ( F 2 9 + 4 3 a 0 T 6 int E T F int * 11 F ) E * 11’ (8 action ) 8’ ( * 7 * F 10 7’ F 10’ Ausgabe goto 163 / 398 164 / 398 Semantische Analyse Scanner und Parser akzeptieren Programme mit korrekter Syntax. nicht alle lexikalisch und syntaktisch korrekten Programme machen Sinn Themengebiet: Die semantische Analyse 166 / 398 165 / 398 Semantische Analyse Semantische Analyse Scanner und Parser akzeptieren Programme mit korrekter Syntax. Scanner und Parser akzeptieren Programme mit korrekter Syntax. nicht alle lexikalisch und syntaktisch korrekten Programme machen Sinn der Compiler kann einige dieser sinnlosen Programme erkennen nicht alle lexikalisch und syntaktisch korrekten Programme machen Sinn der Compiler kann einige dieser sinnlosen Programme erkennen diese Programme werden als fehlerhaft zurückgewiesen Sprachdefinition definiert was fehlerhaft bedeutet diese Programme werden als fehlerhaft zurückgewiesen Sprachdefinition definiert was fehlerhaft bedeutet semantische Analysen sind hierzu erforderlich, die Bezeichner eindeutig machen; die Typen von Variablen ermitteln; 166 / 398 166 / 398 Semantische Analyse Semantische Analyse Scanner und Parser akzeptieren Programme mit korrekter Syntax. Scanner und Parser akzeptieren Programme mit korrekter Syntax. nicht alle lexikalisch und syntaktisch korrekten Programme machen Sinn der Compiler kann einige dieser sinnlosen Programme erkennen nicht alle lexikalisch und syntaktisch korrekten Programme machen Sinn der Compiler kann einige dieser sinnlosen Programme erkennen diese Programme werden als fehlerhaft zurückgewiesen Sprachdefinition definiert was fehlerhaft bedeutet diese Programme werden als fehlerhaft zurückgewiesen Sprachdefinition definiert was fehlerhaft bedeutet semantische Analysen sind hierzu erforderlich, die semantische Analysen sind hierzu erforderlich, die Bezeichner eindeutig machen; die Typen von Variablen ermitteln; Bezeichner eindeutig machen; die Typen von Variablen ermitteln; semantische Analysen dienen auch dazu semantische Analysen dienen auch dazu Möglichkeiten zur Programm-Optimierung zu finden; über möglicherweise fehlerhafte Programme zu warnen Möglichkeiten zur Programm-Optimierung zu finden; über möglicherweise fehlerhafte Programme zu warnen ; semantische Analysen attributieren den Syntaxbaum 166 / 398 166 / 398 Attributierte Grammatiken Die semantische Analyse viele Berechnungen der semantischen Analyse als auch der Code-Generierung arbeiten auf dem Syntaxbaum. was an einem Knoten lokal berechnet wird, hängt nur von dem Typ des Knotens ab eine lokale Berechnung Kapitel 1: greift auf bereits berechnete Informationen von Nachbarknoten zu und berechnen daraus neue Informationen für den lokalen Knoten und andere Nachbarknoten Attributierte Grammatiken damit die Eingabe-Werte eines Knotens bei der Berechnung bereits vorliegen, müssen die Knoten des Syntaxbaums in einer bestimmten Reihenfolge durchlaufen werden 168 / 398 167 / 398 Beispiel: Berechnung des Prädikats empty[r] Attributierte Grammatiken viele Berechnungen der semantischen Analyse als auch der Code-Generierung arbeiten auf dem Syntaxbaum. Betrachte den Syntaxbaum des regulären Ausdrucks (a|b)*a(a|b): was an einem Knoten lokal berechnet wird, hängt nur von dem Typ des Knotens ab eine lokale Berechnung . greift auf bereits berechnete Informationen von Nachbarknoten zu und berechnen daraus neue Informationen für den lokalen Knoten und andere Nachbarknoten . * damit die Eingabe-Werte eines Knotens bei der Berechnung bereits vorliegen, müssen die Knoten des Syntaxbaums in einer bestimmten Reihenfolge durchlaufen werden | 0 Definition a 2 1 b | a 3 a 4 b Eine attributierte Grammatik ist eine CFG erweitert um: Attribute für jedes Nichtterminal; lokale Attribut-Gleichungen. 168 / 398 169 / 398 Beispiel: Berechnung des Prädikats empty[r] Beispiel: Berechnung des Prädikats empty[r] Betrachte den Syntaxbaum des regulären Ausdrucks (a|b)*a(a|b): Betrachte den Syntaxbaum des regulären Ausdrucks (a|b)*a(a|b): . . . * | 2 . * f f | a f | 2 f | a f f f f f f f f 0 1 3 4 0 1 3 4 a b a b a b a b 169 / 398 Beispiel: Berechnung des Prädikats empty[r] 169 / 398 Beispiel: Berechnung des Prädikats empty[r] Betrachte den Syntaxbaum des regulären Ausdrucks (a|b)*a(a|b): Betrachte den Syntaxbaum des regulären Ausdrucks (a|b)*a(a|b): f . . t f t f * . * . f | f f 2 | a f | f f 2 | a f f f f f f f f 0 1 3 4 0 1 3 4 a b a b a b a b ; Werte von empty[r] werden von unten nach oben berechnet. 169 / 398 Idee zur Implementierung 169 / 398 Idee zur Implementierung für jeden Knoten führen wir ein Attribut empty ein. die Attribute werden in einer depth-first post-order Traversierung berechnet: für jeden Knoten führen wir ein Attribut empty ein. die Attribute werden in einer depth-first post-order Traversierung berechnet: an einem Blatt lässt sich der Wert des Attributs unmittelbar ermitteln das Attribut an einem inneren Knoten hängt nur von den Attributen der Nachfolger ab an einem Blatt lässt sich der Wert des Attributs unmittelbar ermitteln das Attribut an einem inneren Knoten hängt nur von den Attributen der Nachfolger ab das empty ist ein synthetisches Attribut das empty ist ein synthetisches Attribut Im Allgemeinen: Definition Ein Attribut ist synthetisch: Wert wird von den Blättern zur Wurzel propagiert inherit: Wert wird von der Wurzel zu den Blättern propagiert 170 / 398 170 / 398 Berechnungsregeln für empty Spezifizierung von allgemeinen Attributsystemen Das empty Attributs ist rein synthetisch, daher kann es durch einfache strukturelle Induktion definiert werden. wir benötigen einen einfachen und flexiblen Mechanismus, mit dem wir über allgemeine Attribute an einem Knoten und seinen Nachfolgern reden können. Wie das Attribut lokal zu berechnen ist, ergibt sich aus dem Typ des Knotens: der Einfachheit halber benutzen wir fortlaufende Indizes: für Blätter: r ≡ i andernfalls: x definieren wir empty[r1 | r2 ] = empty[r1 · r2 ] = empty[r1∗ ] = empty[r1 ?] = empty[0] : empty[i] : empty[r] = (x ≡ ). das Attribut des aktuellen Knotens das Attribut des i-ten Sohns (i > 0) empty[r1 ] ∨ empty[r2 ] empty[r1 ] ∧ empty[r2 ] t t 172 / 398 171 / 398 Spezifizierung von allgemeinen Attributsystemen Das empty Attributs ist rein synthetisch, daher kann es durch einfache strukturelle Induktion definiert werden. Die lokalen Berechnungen der Attributwerte müssen von einem globalen Algorithmus zusammen gesetzt werden Dazu benötigen wir: wir benötigen einen einfachen und flexiblen Mechanismus, mit dem wir über allgemeine Attribute an einem Knoten und seinen Nachfolgern reden können. 1 der Einfachheit halber benutzen wir fortlaufende Indizes: empty[0] : empty[i] : Beobachtungen: 2 eine Besuchsreihenfolge der Knoten des Baums; eine lokale Berechnungsreihenfolgen Die Auswertungsstrategie muss mit den Attribut-Abhängigkeiten kompatibel sein das Attribut des aktuellen Knotens das Attribut des i-ten Sohns (i > 0) ... im Beispiel: x | · ∗ ? empty[0] empty[0] empty[0] empty[0] empty[0] : : : : : := := := := := (x ≡ ) empty[1] ∨ empty[2] empty[1] ∧ empty[2] t t 172 / 398 Beobachtungen: 173 / 398 Beobachtung: Die lokalen Berechnungen der Attributwerte müssen von einem globalen Algorithmus zusammen gesetzt werden Dazu benötigen wir: 1 2 eine Besuchsreihenfolge der Knoten des Baums; eine lokale Berechnungsreihenfolgen Zur Ermittlung einer Auswertungsstrategie reicht es nicht, sich die lokalen Attribut-Abhängigkeiten anzusehen. Die Auswertungsstrategie muss mit den Attribut-Abhängigkeiten kompatibel sein Es kommt auch darauf an, wie sie sich global zu einem Abhängigkeitsgraphen zusammen setzen. Im Beispiel sind die Abhängigkeiten stets von den Attributen der Söhne zu den Attributen des Vaters gerichtet. ; post-order depth-first Traversierung möglich Wir geben Abhängigkeiten als gerichtete Kanten an: empty empty | Die Variablen-Abhängigkeiten können aber auch komplizierter sein. empty ; Pfeil zeigt in die Richtung des Informationsflusses 173 / 398 174 / 398 Simultane Berechnung von mehreren Attributen RE Auswertung: Regeln für Alternative Berechne empty, first, next von regulären Ausdrücken: x | empty[0] := (x ≡ ) first[0] := {x | x 6= } // (keine Gleichung für next ) : root: empty[0] := first[0] := next[0] := next[1] := : empty[0] := first[0] := next[1] := next[2] := : empty[1] first[1] ∅ next[0] f f root e x e | e n n f f empty[1] ∨ empty[2] first[1] ∪ first[2] next[0] next[0] e n f e n n f e n 175 / 398 RE Auswertung: Regeln für Konkatenation · 176 / 398 RE Auswertung: Regeln für Kleene-Stern und ‘?’ empty[0] := empty[1] ∧ empty[2] first[0] := first[1] ∪ (empty[1] ? first[2] : ∅) next[1] := first[2] ∪ (empty[2] ? next[0]: ∅) next[2] := next[0] : f f e e : empty[0] := t first[0] := first[1] next[1] := first[1] ∪ next[0] ? : empty[0] := t first[0] := first[1] next[1] := next[0] n . n ∗ f e f e f e * n n f e n f e ? n n 177 / 398 Problem bei allgemeinen Attributen Berechnung der Abhängigkeiten Betrachte Abhängigkeiten in jedem möglichen Baum: Eine Auswertungsstrategie kann es nur dann geben, wenn die Variablen-Abhängigkeiten in jedem attributierten Baum azyklisch sind Es ist DEXPTIME-vollständig, herauszufinden, ob keine zyklischen Variablenabhängigkeiten vorkommen können [Jazayeri, Odgen, Rounds, 1975] h Beispiel: Grammatik S → a | b; Attributenabhängigkeiten: h h h i a j i k S 178 / 398 j h S j i a j k h h S j i b j k Idee: Berechne Abhänigkeitsgraphen für jedes Symbol T ∪ N. j Initialisiere S(s) = ∅ für s ∈ N und setze S(s) = {G} wobei G der Abhängigkeitsgraph von s ∈ T ist. k h i b j Für jede Regel eines Nicht-Terminals N mit RHS s1 . . . sp , erweitere S(N) durch Graphen die entstehen, wenn die Kanten von S(s1 ), . . . S(sp ) in die Kinderattribute eingesetzt werden. k Enthält keiner der berechneten Graphen einen Zyklus, so kann jeder Ableitungsbaum berechnet werden. 179 / 398 180 / 398 Stark azyklische Attributierung Idee: Berechne einen Abhängigkeitsgraphen für jedes Symbol. Von den Abhängigkeiten zur Strategie Mögliche Strategien: Beobachtung: Die Relation → wird zur partiellen Ordnung. Wir starten mit der trivialen Ordung v = → Die aktuelle Ordnung setzen wir an den Sohn-Knoten in die lokalen Abhängigkeitsgraphen ein. Ergibt sich ein Kreis, geben wir auf. Andernfalls fügen wir alle Beziehungen a v b hinzu, für die es jetzt einen Pfad von a[0] nach b[0] gibt. Lässt sich v nicht mehr vergrößern, hören wir auf. Im Beispiel: h h S i j j k 182 / 398 181 / 398 Von den Abhängigkeiten zur Strategie Von den Abhängigkeiten zur Strategie Mögliche Strategien: 1 Mögliche Strategien: Der Benutzer soll die Strategie spezifizieren 1 Der Benutzer soll die Strategie spezifizieren 2 Berechne eine Strategie anhand der Abhängigkeiten Berechne eine linear Ordnung aus der partielle Ordnung → bzw. v. Werte die Attribute anhand dieser linearen Ordnung aus. Die lokalen Abhängigkeitsgraphen zusammen mit der linearen Ordnung erlauben die Berechnung einer Strategie. n f e 182 / 398 Von den Abhängigkeiten zur Strategie 182 / 398 Linearisierung der Abhängigkeitsordnung Mögliche Strategien: 1 Der Benutzer soll die Strategie spezifizieren 2 Berechne eine Strategie anhand der Abhängigkeiten Mögliche automatische Strategien: Berechne eine linear Ordnung aus der partielle Ordnung → bzw. v. Werte die Attribute anhand dieser linearen Ordnung aus. Die lokalen Abhängigkeitsgraphen zusammen mit der linearen Ordnung erlauben die Berechnung einer Strategie. n f e 3 Betrachte fixe Strategie und erlaube nur entsprechende Attribute Frage: Wie berechnet man eine sinnvolle Linearisierung? 182 / 398 183 / 398 Linearisierung der Abhängigkeitsordnung Linearisierung der Abhängigkeitsordnung Mögliche automatische Strategien: 1 Mögliche automatische Strategien: Bedarfsgetriebene Auswertung 1 Beginne mit der Berechnung eines Attributs. Sind die Argument-Attribute noch nicht berechnet, berechne rekursiv deren Werte Besuche die Knoten des Baum nach Bedarf Bedarfsgetriebene Auswertung Beginne mit der Berechnung eines Attributs. Sind die Argument-Attribute noch nicht berechnet, berechne rekursiv deren Werte Besuche die Knoten des Baum nach Bedarf 2 Auswertung in Pässen: Minimiere die Anzahl der Besuche an jedem Knoten. Organisiere die Auswertung in Durchläufen durch den Baum. Berechne für jeden Durchlauf eine Besuchsstrategie für die Knoten zusammen mit einer lokalen Strategie für jeden Knoten-Typ Betrachte Beispiel für bedarfsgetriebene Auswertung 183 / 398 Beispiel bedarfsgetriebene Auswertung 183 / 398 Beispiel bedarfsgetriebene Auswertung Berechne next der Blätter von ((a|b)∗ a(a|b)): Berechne next der Blätter von ((a|b)∗ a(a|b)): | : next[1] next[2] := next[0] := next[0] · : next[1] := first[2] ∪ (empty[2] ? next[0]: ∅) next[2] := next[0] | : next[1] next[2] · : next[1] := first[2] ∪ (empty[2] ? next[0]: ∅) next[2] := next[0] . . . * a | a b 0 := next[0] := next[0] . * | 2 a 1 n a | b 3 a 4 b 0 n | 2 a 1 n b n 3 n 4 184 / 398 Beispiel bedarfsgetriebene Auswertung Beispiel bedarfsgetriebene Auswertung Berechne next der Blätter von ((a|b)∗ a(a|b)): Berechne next der Blätter von ((a|b)∗ a(a|b)): | : next[1] next[2] := next[0] := next[0] · : next[1] := first[2] ∪ (empty[2] ? next[0]: ∅) next[2] := next[0] 0 next[1] next[2] · : next[1] := first[2] ∪ (empty[2] ? next[0]: ∅) next[2] := next[0] n . a b : n n e f a n 3 | n e f a | b n a 4 184 / 398 0 n . * 2 e f 1 := next[0] := next[0] . * a | n . | 184 / 398 b n e f 1 e f 2 a n 3 | n e f b n 4 184 / 398 Bedarfsgetriebene Auswertung Bedarfsgetriebene Auswertung Diskussion: Diskussion: Die Reihenfolge hängt i.a. vom zu attributierenden Baum ab. Die Reihenfolge hängt i.a. vom zu attributierenden Baum ab. Der Algorithmus muss sich merken, welche Attribute er bereits berechnete Der Algorithmus muss sich merken, welche Attribute er bereits berechnete Der Algorithmus besucht manche Knoten unnötig oft. Der Algorithmus ist nicht-lokal Der Algorithmus besucht manche Knoten unnötig oft. Der Algorithmus ist nicht-lokal Ansatz nur im Prinzip günstig: Berechnung der Berechnungsstrategie aufwendig Berechnung der Attribute oft billiger Reihenfolge der Attributberechnung oft dem Programmierer klar 185 / 398 185 / 398 Diskussion Diskussion Annahme: Jedes Attribut ist entweder inherit oder synthetisch. Dann gilt: Annahme: Jedes Attribut ist entweder inherit oder synthetisch. Dann gilt: ein Attribut eines Knotens in einer stark azyklische Attributierungen lässt sich stets in einem Durchlauf berechnen ein Attribut eines Knotens in einer stark azyklische Attributierungen lässt sich stets in einem Durchlauf berechnen man braucht folglich für stark azyklische Attributierungen maximal so viele Pässe, wie es Attribute gibt man braucht folglich für stark azyklische Attributierungen maximal so viele Pässe, wie es Attribute gibt hat man einen Baum-Durchlauf zur Berechnung eines Attributes, kann man überprüfen, ob er geeignet ist, gleichzeitig weitere Attribute auszuwerten ; Optimierungsproblem hat man einen Baum-Durchlauf zur Berechnung eines Attributes, kann man überprüfen, ob er geeignet ist, gleichzeitig weitere Attribute auszuwerten ; Optimierungsproblem ... im Beispiel: empty und first lassen sich gemeinsam berechnen. next muss in einem weiteren Pass berechnet werden ; lasse den Benutzer festlegen, wie Attribute ausgewertet werden 186 / 398 Implementierung der lokalen Auswertung: Pre-Order vs. Post-Order 186 / 398 Praktische Implementierung der Nummerierung Idee: Betrachte Beispiel: Nummerierung der Blätter eines Baums: Führe Hilfsattribute pre und post ein mit pre reichen wir einen Zählerstand nach unten (inherites Attribut) mit post reichen wir einen Zählerstand wieder nach oben (synthetisches Attribut) . . * a | a 0 b 1 | 2 a 3 b Root: pre[0] := 0 pre[1] := pre[0] post[0] := post[1] Node: pre[1] := pre[0] pre[2] := post[1] post[0] := post[2] Leaf: post[0] 4 187 / 398 := pre[0] + 1 188 / 398 Die lokalen Attribut-Abhängigkeiten: pre pre post post pre pre Die lokalen Attribut-Abhängigkeiten: pre post post pre post post pre pre die Attributierung ist offenbar stark azyklisch post post die Attributierung ist offenbar stark azyklisch ein innerer Knoten berechnet inherite Attribute bevor in einen Kindknoten abgestiegen wird (pre-order traversal) synthetische Attribute nach der Rückkehr von einem Kindknoten (post-order traversal) 189 / 398 Die lokalen Attribut-Abhängigkeiten: pre post post pre 189 / 398 L-Attributierung Definition pre pre Ein Knoten mit Attributen A ist L-attributiert wenn jedes Attribut ai [n] nur von aj [m] abhängt wobei m < n und ai , aj ∈ A. post post die Attributierung ist offenbar stark azyklisch ein innerer Knoten berechnet inherite Attribute bevor in einen Kindknoten abgestiegen wird (pre-order traversal) synthetische Attribute nach der Rückkehr von einem Kindknoten (post-order traversal) man kann alle Attribute in einer depth-first Traversierung von links nach rechts auswerten (mit pre- und post-order Berechnung) So etwas nennen wir L-Attributierung. 189 / 398 L-Attributierung 190 / 398 Praktische Benutzung Definition Ein Knoten mit Attributen A ist L-attributiert wenn jedes Attribut ai [n] nur von aj [m] abhängt wobei m < n und ai , aj ∈ A. Symboltabellen, Typ-Überprüfung / Inferenz und (einfache) Codegenerierung können durch Attributierung berechnet werden In diesen Anwendungen werden stets Syntaxbäume annotiert. Die Knotenbeschriftungen entsprechen den Regeln einer kontextfreien Grammatik Idee: Knoten können in Typen eingeteilt werden — entsprechend den Nichtterminalen auf der linken Seite partitioniere alle Attribute A = A1 ∪ . . . ∪ An so dass für alle Attribute in Ai jeder Knoten L-attributiert ist Unterschiedliche Nichtterminale benötigen evt. unterschiedliche Mengen von Attributen. für jede Attributmenge Ai führe eine depth-first Traversierung durch ; Bestimme Attribute mit Hinblick auf wenige Traversierungen 190 / 398 191 / 398 Praktische Implementierung In objekt-orientierten Sprachen benutze visitor pattern: Klasse mit Methode für jedes Nicht-Terminal der Grammatik public abstract class Regex { public abstract Regex accept(RegexVisitor v); } Durch Überschreiben der folgenden Methoden wird eine Attribut-spezifische Auswertung implementiert public interface RegexVisitor { public abstract boolean preLeft(OrEx orEx); public abstract boolean preRight(OrEx orEx); public abstract Regex post(OrEx orEx); ... public abstract Regex visit(Const const1); } Vordefiniert ist die Traversierung des Ableitungsbaumes public class OrEx extends Regex { public Regex accept(RegexVisitor v) { ... return v.post(this); } } Ausblick In der Übung: implementiere visitor pattern für C Grammatik prüfe, ob jeder Bezeichner deklariert wurde 193 / 398 192 / 398 Symboltabellen Die semantische Analyse Betrachte folgenden Java Kode: void foo() { int A; void bar() { double A; A = 0.5; write(A); } A = 2; bar(); write(A); } Kapitel 2: Symboltabellen innerhalb des Rumpfs von bar wird die Definition von A durch die lokale Definition verdeckt für die Code-Erzeugung benötigen wir für jede Benutzung eines Bezeichners die zugehörige Definitionsstelle statische Bindung bedeutet, dass die Definition eines Namens A an allen Programmpunkten innerhalb des gesamten Blocks gültig ist. sichtbar ist sie aber nur in denjenigen Teilbereichen, in denen keine weitere Definition von A gültig ist 195 / 398 194 / 398 Gültigkeitsbereiche von Bezeichnern void foo() { int A; void bar() { double A; A = 0.5; write(A); } A = 2; bar(); write(A); } Gültigkeitsbereiche von Bezeichnern void foo() { int A; void bar() { double A; A = 0.5; write(A); Gültigkeitsbereich von int A } A = 2; bar(); write(A); Gültigkeitsbereich von double A } 196 / 398 196 / 398 Gültigkeitsbereiche von Bezeichnern Sichtbarkeitsregeln in objektorientierten Prog.spr. 1 2 3 4 5 6 7 8 9 10 void foo() { int A; void bar() { double A; A = 0.5; write(A); } A = 2; bar(); write(A); Gültigkeitsbereich von double A public class Foo { protected int x = 17; protected int y = 5; private int z = 42; public int b() { return 1; } } class Bar extends Foo { protected double y = 0.5; public int b(int a) { return a; } } Beobachtung: } Verwaltungs von Bezeichnern kann kompliziert werden... 196 / 398 Sichtbarkeitsregeln in objektorientierten Prog.spr. 1 2 3 4 5 6 7 8 9 10 public class Foo { protected int x = 17; protected int y = 5; private int z = 42; public int b() { return 1; } } class Bar extends Foo { protected double y = 0.5; public int b(int a) { return a; } } 197 / 398 Dynamische Auflösung von Funktionen public class Foo { protcted int foo() { return 1; } } class Bar extends Foo { protected int foo() { return 2; } public int test(boolean b) { Foo x = (b) ? new Foo() : new Bar(); return x.foo(); } } Beobachtung: Beobachtungen: private Members sind nur innerhalb der aktuellen Klasse gültig protected Members sind innerhalb der Klasse, in den Unterklassen sowie innerhalb des gesamten package als Foo::x gültig Methoden b gleichen Namens sind stets verschieden, wenn ihre Argument-Typen verschieden sind ; overloading 197 / 398 198 / 398 Dynamische Auflösung von Funktionen Dynamische Auflösung von Funktionen public class Foo { protcted int foo() { return 1; } } class Bar extends Foo { protected int foo() { return 2; } public int test(boolean b) { Foo x = (b) ? new Foo() : new Bar(); return x.foo(); } } public class Foo { protcted int foo() { return 1; } } class Bar extends Foo { protected int foo() { return 2; } public int test(boolean b) { Foo x = (b) ? new Foo() : new Bar(); return x.foo(); } } Beobachtungen: Beobachtungen: der Typ von x ist Foo oder Bar, je nach Wert von b der Typ von x ist Foo oder Bar, je nach Wert von b x.foo() ruft entweder foo in Zeile 2 oder Zeile 5 auf x.foo() ruft entweder foo in Zeile 2 oder Zeile 5 auf Entscheidung wird zur Laufzeit gefällt (hat nichts mit Namensauflösung zu tun) 198 / 398 198 / 398 Überprüfung von Bezeichnern Überprüfung von Bezeichnern Aufgabe: Finde zu jeder Benutzung eines Bezeichners Aufgabe: Finde zu jeder Benutzung eines Bezeichners Idee: Idee: die zugehörige Deklaration 1 die zugehörige Deklaration ersetze Bezeichner durch eindeutige Nummern 1 ersetze Bezeichner durch eindeutige Nummern das Vergleichen von Nummern ist schneller das Ersetzen von gleichen Namen durch eine Nummer spart Speicher 2 finde zu jedem Benutzung eines Bezeichners die Deklaration 2 finde zu jedem Benutzung eines Bezeichners die Deklaration 199 / 398 199 / 398 Überprüfung von Bezeichnern (1) Ersetze Bezeichner durch eindeutige Namen Aufgabe: Finde zu jeder Benutzung eines Bezeichners die zugehörige Deklaration Idee für Algorithmus: Idee: 1 ersetze Bezeichner durch eindeutige Nummern Eingabe: Folge von Strings Ausgabe: 1 Folge von Nummern das Vergleichen von Nummern ist schneller das Ersetzen von gleichen Namen durch eine Nummer spart Speicher 2 2 Tabelle, die zu Nummern die Strings auflistet Wende diesen Algorithmus im Scanner auf jeden Bezeichner an. finde zu jedem Benutzung eines Bezeichners die Deklaration für jede Deklaration eines Bezeichners muss zur Laufzeit des Programs Speicher reserviert werden bei Programmiersprachen ohne explizite Deklaration wird implizit eine Deklaration bei der ersten Benutzung eines Bezeichners erzeugt 199 / 398 Beispiel für die Anwendung des Algorithmus Eingabe: das das Ausgabe: 0 schwein schwein 1 2 3 ist dem dem 1 schwein menschen 4 0 1 3 ist 5 Spezifikation der Implementierung was Idee: benutze eine partielle Abbildung: S : String→int verwalte einen Zähler int count = 0; für die Anzahl der bereits gefundenen Wörter wurst 2 200 / 398 6 und Damit definieren wir eine Funktion int getIndex(String w): 0 1 2 3 4 5 6 int getIndex(String w) { if (S (w) ≡ undefined) { S = S ⊕ {w 7→ count}; return count++; else return S (w); } das schwein ist dem was menschen wurst 201 / 398 202 / 398 Datenstrukturen für partielle Abbildungen Datenstrukturen für partielle Abbildungen Ideen: Ideen: Liste von Paaren (w, i) ∈ String × int : O(1) O(n) ; zu teuer Liste von Paaren (w, i) ∈ String × int : O(1) O(n) ; zu teuer balancierte Bäume : O(log(n)) O(log(n)) ; zu teuer 203 / 398 Datenstrukturen für partielle Abbildungen 203 / 398 Datenstrukturen für partielle Abbildungen Ideen: Ideen: Liste von Paaren (w, i) ∈ String × int : O(1) O(n) ; zu teuer balancierte Bäume : O(log(n)) O(log(n)) Hash Tables : O(1) O(1) Liste von Paaren (w, i) ∈ String × int : O(1) O(n) ; zu teuer balancierte Bäume : O(log(n)) O(log(n)) ; zu teuer Hash Tables : O(1) O(1) zumindest im Mittel ; zu teuer zumindest im Mittel Caveat: In alten Compilern war der Scanner das langsamste Glied in der Kette. Heute der Zeitverbrauch des Scanners vernachlässigbar. 203 / 398 Implementierung mit Hash-Tabellen 203 / 398 Berechnung einer Hash-Tabelle am Beispiel Mit lege ein Feld M von hinreichender Größe m an wähle eine Hash-Funktion H : String → [0, m − 1] mit den Eigenschaften: m=7 und H0 erhalten wir: 0 H(w) ist leicht zu berechnen H streut die vorkommenden Wörter gleichmäßig über [0, m − 1] 1 2 Mögliche Wahlen: 3 = (x0 + xr−1 ) % m Pr−1 = ( i=0 xi · pi ) % m = (x0 + p · (x1 + p · (. . . + p · xr−1 · · · ))) % m für eine Primzahl p (z.B. 31) 4 5 6 schwein 1 menschen 5 was 4 ist 2 das 0 dem 3 wurst 6 Das Argument-Wert-Paar (w, i) legen wir dann in M[H(w)] ab Um den Wert des Worts w zu finden, müssen wir w mit allen Worten x vergleichen, für die H(w) = H(x) 204 / 398 205 / 398 Überprüfung von Bezeichnern: (2) Symboltabellen Überprüfe die korrekte Benutzung von Bezeichnern: Durchmustere den Syntaxbaum in einer geeigneten Reihenfolge, die Beispiel: Keller von Symboltabellen { int a, b; b = 5; if (b>3) { int a, c; a = 3; c = a + 1; b = c; } else { int c; c = a + 1; b = c; } b = a + b; jede Definition vor ihren Benutzungen besucht die jeweils aktuell sichtbare Definition zuletzt besucht für jeden Bezeichner verwaltet man einen Keller der gültigen Definitionen trifft man bei der Durchmusterung auf eine Deklaration eines Bezeichners, schiebt man sie auf den Keller verlässt man den Gültigkeitsbereich, muss man sie wieder vom Keller werfen trifft man bei der Durchmusterung auf eine Benutzung, schlägt man die letzte Deklaration auf dem Keller nach findet man keine Deklaration, haben wir einen Fehler gefunden Abbildung von Namen auf Zahlen: 0 1 2 a b c } 206 / 398 Der zugehörige Syntaxbaum 207 / 398 Der zugehörige Syntaxbaum d Deklaration d Deklaration b Basis-Block b Basis-Block a Zuweisung a Zuweisung b b b b d d b d 0 int a b 1 int b d b b 0 int a if = = > 1 b 1 int if b 5 > b b 1 3 1 b d d b 3 a 2 int = = 2 int = = 1 1 d a 0 int 2 int b d 2 int a b d a 0 int b b 1 d b 5 2 1 2 1 2 2 208 / 398 Praktische Implementierung: Sichtbarkeit 208 / 398 Praktische Implementierung: Sichtbarkeit Das Auflösen von Bezeichnern kann durch L-attributierte Grammatik erfolgen Das Auflösen von Bezeichnern kann durch L-attributierte Grammatik erfolgen Benutzt man eine Listen-Implementierung der Symboltabelle und verwaltet Marker für Blöcke, kann man auf das Entfernen von Deklarationen am Ende eines Blockes verzichten a b vor if-Anweisung 209 / 398 209 / 398 Praktische Implementierung: Sichtbarkeit Praktische Implementierung: Sichtbarkeit Das Auflösen von Bezeichnern kann durch L-attributierte Grammatik erfolgen Das Auflösen von Bezeichnern kann durch L-attributierte Grammatik erfolgen Benutzt man eine Listen-Implementierung der Symboltabelle und verwaltet Marker für Blöcke, kann man auf das Entfernen von Deklarationen am Ende eines Blockes verzichten Benutzt man eine Listen-Implementierung der Symboltabelle und verwaltet Marker für Blöcke, kann man auf das Entfernen von Deklarationen am Ende eines Blockes verzichten a b a c a b vor if-Anweisung then-Zweig a b a c a b c a b vor if-Anweisung then-Zweig else-Zweig 209 / 398 Praktische Implementierung: Sichtbarkeit 209 / 398 Mehrfach- und Vorwärtsdeklarationen Manche Programmiersprachen verbieten eine Mehrfachdeklaration des selben Namens innerhalb eines Blocks. Das Auflösen von Bezeichnern kann durch L-attributierte Grammatik erfolgen Benutzt man eine Listen-Implementierung der Symboltabelle und verwaltet Marker für Blöcke, kann man auf das Entfernen von Deklarationen am Ende eines Blockes verzichten a b a c a b c a b vor if-Anweisung then-Zweig else-Zweig Anstelle erst die Namen durch Nummern zu ersetzen und dann die Zuordnung von Benutzungen zu Definitionen vorzunehmen, kann man auch gleich eindeutige Nummern vergeben Anstatt ein eindeutige Nummern zu vergeben wird in der Praxis auch gerne ein Zeiger zum Deklarationsknoten gespeichert 209 / 398 Mehrfach- und Vorwärtsdeklarationen 210 / 398 Mehrfach- und Vorwärtsdeklarationen Manche Programmiersprachen verbieten eine Mehrfachdeklaration des selben Namens innerhalb eines Blocks. Manche Programmiersprachen verbieten eine Mehrfachdeklaration des selben Namens innerhalb eines Blocks. weise jedem Block eine eindeutige Nummer zu weise jedem Block eine eindeutige Nummer zu speichere für jede Deklaration auch die Nummer des Blocks, zu der sie gehört speichere für jede Deklaration auch die Nummer des Blocks, zu der sie gehört gibt es eine weitere Deklaration des gleichen Namens mit derselben Blocknummer, muss ein Fehler gemeldet werden 210 / 398 210 / 398 Mehrfach- und Vorwärtsdeklarationen Mehrfach- und Vorwärtsdeklarationen Manche Programmiersprachen verbieten eine Mehrfachdeklaration des selben Namens innerhalb eines Blocks. Manche Programmiersprachen verbieten eine Mehrfachdeklaration des selben Namens innerhalb eines Blocks. weise jedem Block eine eindeutige Nummer zu weise jedem Block eine eindeutige Nummer zu speichere für jede Deklaration auch die Nummer des Blocks, zu der sie gehört speichere für jede Deklaration auch die Nummer des Blocks, zu der sie gehört gibt es eine weitere Deklaration des gleichen Namens mit derselben Blocknummer, muss ein Fehler gemeldet werden gibt es eine weitere Deklaration des gleichen Namens mit derselben Blocknummer, muss ein Fehler gemeldet werden Wie zuvor können eindeutige Block Nummern auch durch Zeiger auf die Blöcke implementiert werden. Wie zuvor können eindeutige Block Nummern auch durch Zeiger auf die Blöcke implementiert werden. Um rekursive Datenstrukturen zu ermöglichen erlauben Sprachen Vorwärtsdeklarationen: struct list0 { int info; struct list1* next; } struct list1 { int info; struct list0* next; } 210 / 398 210 / 398 Deklaration von Funktionsnamen Deklaration von Funktionsnamen Auf gleiche Weise müssen auch Funktionsnamen verwaltet werden. bei einer rekursiven Funktion muss der Name vor der Durchmusterung des Rumpfes in die Tabelle eingetragen werden Auf gleiche Weise müssen auch Funktionsnamen verwaltet werden. bei einer rekursiven Funktion muss der Name vor der Durchmusterung des Rumpfes in die Tabelle eingetragen werden int fac(int i) { return i*fac(i-1); } int fac(int i) { return i*fac(i-1); } bei wechselseitig rekursiven Funktionsdefinitionen gilt dies für alle Funktionsnamen. Beispiel in ML und C: fun | | and | | odd 0 = false odd 1 = true odd x = even (x-1) even 0 = true even 1 = false even x = odd (x-1) int even(int x); int odd(int x) { return (x==0 ? 0 : (x==1 ? 1 : odd(x-1))); } int even(int x) { return (x==0 ? 1 : (x==1 ? 0 : even(x-1))); } 211 / 398 Überladung von Namen 211 / 398 Überladung von Namen Ähnliche Abhängigkeiten gelten für objekt-orientierte Sprachen: Ähnliche Abhängigkeiten gelten für objekt-orientierte Sprachen: bei objekt-orientierter Sprache mit Vererbung zwischen Klassen sollte die übergeordnete Klasse vor der Unterklasse besucht werden bei objekt-orientierter Sprache mit Vererbung zwischen Klassen sollte die übergeordnete Klasse vor der Unterklasse besucht werden bei Überladung muss simultan die Signatur verglichen werden eventuell muss eine Typüberprüfung vorgenommen werden 212 / 398 212 / 398 Überladung von Namen Mehrere Sorten von Bezeichnern Einige Programmiersprachen unterscheiden verschiedene Bezeichner: Ähnliche Abhängigkeiten gelten für objekt-orientierte Sprachen: C: Variablennamen und Typnamen bei objekt-orientierter Sprache mit Vererbung zwischen Klassen sollte die übergeordnete Klasse vor der Unterklasse besucht werden bei Überladung muss simultan die Signatur verglichen werden Java: Klassen, Methoden, Felder Haskell: Typnamen, Variablen, Operatoren mit Prioritäten eventuell muss eine Typüberprüfung vorgenommen werden Sobald Namen von Bezeichnern aufgelöst sind, kann die semantische Analyse fortsetzen mit der Typprüfung (oder Typinferenz, siehe Vorlesung „Programmiersprachen”). 212 / 398 Mehrere Sorten von Bezeichnern 213 / 398 Mehrere Sorten von Bezeichnern Einige Programmiersprachen unterscheiden verschiedene Bezeichner: Einige Programmiersprachen unterscheiden verschiedene Bezeichner: C: Variablennamen und Typnamen C: Variablennamen und Typnamen Java: Klassen, Methoden, Felder Java: Klassen, Methoden, Felder Haskell: Typnamen, Variablen, Operatoren mit Prioritäten Haskell: Typnamen, Variablen, Operatoren mit Prioritäten In einigen Fällen verändern Deklarationen in der Sprache die Bedeutung eines Bezeichners: In einigen Fällen verändern Deklarationen in der Sprache die Bedeutung eines Bezeichners: Der Parser informiert den Scanner über eine neue Deklaration Der Parser informiert den Scanner über eine neue Deklaration Der Scanner generiert verschiedene Token für einen Bezeichner Der Scanner generiert verschiedene Token für einen Bezeichner Der Parser generiert einen Syntaxbaum, der von den bisherigen Deklarationen abhängt Der Parser generiert einen Syntaxbaum, der von den bisherigen Deklarationen abhängt Interaktion zwischen Parser und Scanner: problematisch! 213 / 398 Fixity-Deklaration in Haskell Fixity-Deklaration in Haskell Haskell erlaubt beliebige binäre Operatoren aus (?!^&|=+-_*/)+ . In Haskell Standard-Bibliothek: infixr infixl infixl infix 8 7 6 4 Haskell erlaubt beliebige binäre Operatoren aus (?!^&|=+-_*/)+ . In Haskell Standard-Bibliothek: infixr infixl infixl infix ^ *,/ +,==,/= Grammatik ist generisch: Exp0 → Exp0 LOp0 Exp1 | Exp1 ROp0 Exp0 | Exp1 Op0 Exp1 | Exp1 .. . Exp9 Exp → | | | → | 213 / 398 8 7 6 4 ^ *,/ +,==,/= Grammatik ist generisch: Exp0 → Exp0 LOp0 Exp1 | Exp1 ROp0 Exp0 | Exp1 Op0 Exp1 | Exp1 .. . Exp9 LOp9 Exp Exp ROp9 Exp9 Exp Op9 Exp Exp ident | num ( Exp0 ) Exp9 Exp 214 / 398 → | | | → | Exp9 LOp9 Exp Exp ROp9 Exp9 Exp Op9 Exp Exp ident | num ( Exp0 ) Parser trägt Deklarationen in Tabelle ein Scanner erzeugt: Operator - wird zum Token LOp6 . Operator * wird zum Token LOp7 . Operator == wird zum Token Op4 . etc. Parser erkennt 3-4*5-6 als (3-(4*5))-6 214 / 398 Fixity-Deklarationen in Haskell: Beobachtungen Fixity-Deklarationen in Haskell: Beobachtungen Nicht ohne Probleme: Nicht ohne Probleme: Scanner hat einen Zustand, der vom Parser bestimmt wird ; nicht mehr kontextfrei, braucht globale Datenstruktur Scanner hat einen Zustand, der vom Parser bestimmt wird ; nicht mehr kontextfrei, braucht globale Datenstruktur ein Stück Kode kann mehrere Semantiken haben syntaktische Korrektheit hängt evtl. von importierten Modulen ab 215 / 398 215 / 398 Fixity-Deklarationen in Haskell: Beobachtungen Fixity-Deklarationen in Haskell: Beobachtungen Nicht ohne Probleme: Nicht ohne Probleme: Scanner hat einen Zustand, der vom Parser bestimmt wird ; nicht mehr kontextfrei, braucht globale Datenstruktur Scanner hat einen Zustand, der vom Parser bestimmt wird ; nicht mehr kontextfrei, braucht globale Datenstruktur ein Stück Kode kann mehrere Semantiken haben ein Stück Kode kann mehrere Semantiken haben syntaktische Korrektheit hängt evtl. von importierten Modulen ab syntaktische Korrektheit hängt evtl. von importierten Modulen ab Fehlermeldungen des Parsers schwer verständlich Fehlermeldungen des Parsers schwer verständlich Im GHC Haskell Compiler werden alle Operatoren als LOp0 gelesen und der AST anschliessend transformiert. 215 / 398 Typbezeichner und Variablen in C Typbezeichner und Variablen in C Die C Grammatik unterscheidet zwischen Terminal typedef-name und identifier. Betrachte folgende Liste von Deklarationen: Die C Grammatik unterscheidet zwischen Terminal typedef-name und identifier. Betrachte folgende Liste von Deklarationen: typedef struct { int x,y } point_t; point_t origin; Relevante C Grammatik: declaration → declaration-specifier → | declarator → 215 / 398 typedef struct { int x,y } point_t; point_t origin; Relevante C Grammatik: declaration → declaration-specifier → | declarator → (declaration-specifier)+ declarator ; static | volatile · · · typedef void | char | char · · · typedef-name identifier | · · · (declaration-specifier)+ declarator ; static | volatile · · · typedef void | char | char · · · typedef-name identifier | · · · Problem: Parser trägt point_t in Typen-Tabelle ein wenn die declaration Regel reduziert wird 216 / 398 216 / 398 Typbezeichner und Variablen in C Typbezeichner und Variablen in C Die C Grammatik unterscheidet zwischen Terminal typedef-name und identifier. Betrachte folgende Liste von Deklarationen: Die C Grammatik unterscheidet zwischen Terminal typedef-name und identifier. Betrachte folgende Liste von Deklarationen: typedef struct { int x,y } point_t; point_t origin; Relevante C Grammatik: declaration → declaration-specifier → | declarator → typedef struct { int x,y } point_t; point_t origin; Relevante C Grammatik: declaration → declaration-specifier → | declarator → (declaration-specifier)+ declarator ; static | volatile · · · typedef void | char | char · · · typedef-name identifier | · · · Problem: (declaration-specifier)+ declarator ; static | volatile · · · typedef void | char | char · · · typedef-name identifier | · · · Problem: Parser trägt point_t in Typen-Tabelle ein wenn die declaration Regel reduziert wird Parser trägt point_t in Typen-Tabelle ein wenn die declaration Regel reduziert wird Parserzustand hat mindestens ein Lookahead-Token Parserzustand hat mindestens ein Lookahead-Token Scanner hat point_t in zweiter Zeile also schon als identifier gelesen 216 / 398 Typbezeichner und Variablen in C: Lösungen Typbezeichner und Variablen in C: Lösungen Relevante C Grammatik: declaration declaration-specifier declarator → → | → 216 / 398 Relevante C Grammatik: (declaration-specifier)+ declarator ; static | volatile · · · typedef void | char | char · · · typedef-name identifier | · · · declaration declaration-specifier declarator Lösung schwierig: → → | → (declaration-specifier)+ declarator ; static | volatile · · · typedef void | char | char · · · typedef-name identifier | · · · Lösung schwierig: versuche, Lookahead-Token im Parser zu ändern 217 / 398 Typbezeichner und Variablen in C: Lösungen Typbezeichner und Variablen in C: Lösungen Relevante C Grammatik: declaration declaration-specifier declarator → → | → 217 / 398 Relevante C Grammatik: (declaration-specifier)+ declarator ; static | volatile · · · typedef void | char | char · · · typedef-name identifier | · · · declaration declaration-specifier declarator Lösung schwierig: → → | → (declaration-specifier)+ declarator ; static | volatile · · · typedef void | char | char · · · typedef-name identifier | · · · Lösung schwierig: versuche, Lookahead-Token im Parser zu ändern versuche, Lookahead-Token im Parser zu ändern füge folgende Regel zur Grammatik hinzu: typedef-name → identifier füge folgende Regel zur Grammatik hinzu: typedef-name → identifier registriere den Typnamen früher 217 / 398 217 / 398 Typbezeichner und Variablen in C: Lösungen Typbezeichner und Variablen in C: Lösungen Relevante C Grammatik: declaration declaration-specifier declarator Relevante C Grammatik: (declaration-specifier) declarator ; static | volatile · · · typedef void | char | char · · · typedef-name identifier | · · · declaration declaration-specifier + → → | → declarator Lösung schwierig: → → | → (declaration-specifier)+ declarator ; static | volatile · · · typedef void | char | char · · · typedef-name identifier | · · · Lösung schwierig: versuche, Lookahead-Token im Parser zu ändern versuche, Lookahead-Token im Parser zu ändern füge folgende Regel zur Grammatik hinzu: typedef-name → identifier füge folgende Regel zur Grammatik hinzu: typedef-name → identifier registriere den Typnamen früher registriere den Typnamen früher separiere Regel für typedef Produktion separiere Regel für typedef Produktion rufe alternative declarator Produktion auf, die identifier als Typenamen registriert 217 / 398 217 / 398 Ziel der Typ-Überprüfung Die semantische Analyse In modernen (imperativen / objektorientierten / funktionalen) Programmiersprachen besitzen Variablen und Funktionen einen Typ, z.B. int, void*, struct { int x; int y; }. Kapitel 3: Typ-Überprüfung Typen sind nützlich für: die Speicherverwaltung; die Vermeidung von Laufzeit-Fehlern In imperativen / objektorientierten Programmiersprachen muss der Typ bei der Deklaration spezifiziert und vom Compiler die typ-korrekte Verwendung überprüft werden. 218 / 398 Typ-Ausdrücke Typ-Namen Ein Typ-Name ist ein Synonym für einen Typ-Ausdruck. In C kann man diese mit Hilfe von typedef einführen. Typ-Namen sind nützlich Typen werden durch Typ-Ausdrücke beschrieben. Die Menge T der Typausdrücke enthält: 1 Basis-Typen: int, char, float, void, ... 2 Typkonstruktoren, die auf Typen angewendet werden 219 / 398 als Abkürzung: typedef struct { int x; int y; } point_t; Beispiele für Typkonstruktoren: Verbunde: struct { t1 a1 ; . . . tk ak ; } zur Konstruktion rekursiver Typen: Zeiger: t * Felder: t [] in C kann/muss zusätzlich eine Größe spezifiziert werden die Variable muss zwischen t und [n] stehen Funktionen: t (t1 , . . . , tk ) in C muss die Variable zwischen t und (t1 , . . . , tk ) stehen. in ML würde man diesen Typ anders herum schreiben: t1 ∗ . . . ∗ tk → t Erlaubt in C: Lesbarer: struct list { int info; struct list* next; } typedef struct list list_t; struct list { int info; list_t* next; } list_t* head; struct list* head; wir benutzen (t1 , . . . , tk ) als Tupel-Typen 220 / 398 221 / 398 Typ-Prüfung Typ-Prüfung am Syntax-Baum Prüfe Ausdruck *a[f(b->c)]+2: Aufgabe: + Gegeben: eine Menge von Typ-Deklarationen Γ = {t1 x1 ; . . . tm xm ; } Überprüfe: Kann ein Ausdruck e mit dem Typ t versehen werden? ∗ 2 [ ] Beispiel: ( ) a f struct list { int info; struct list* next; }; int f(struct list* l) { return 1; }; struct { struct list* c;}* b; int* a[11]; . ∗ c b Idee: traversiere den Syntaxbaum bottom-up für Bezeichner schlagen wir in Γ den richtigen Typ nach Konstanten wie 2 oder 0.5 sehen wir den Typ direkt an die Typen für die inneren Knoten erschießen wir mit Hilfe von Typ-Regeln Betrachte den Ausdruck: *a[f(b->c)]+2; 222 / 398 Typ-Systeme Typ-System für C-ähnliche Sprachen Formal betrachten wir Aussagen der Form: Weitere Regeln für diverse Typausdrücke: Γ `e : t // Array: (In der Typ-Umgebung Γ hat e den Typ t) Array: Axiome: Struct: Const: Γ ` c : tc Var: Γ ` x : Γ(x) (tc (x Typ der Konstante c) Variable) App: Regeln: Ref: 223 / 398 Op: Γ `e : t Γ ` &e : t∗ Deref: Cast: Γ ` e : t∗ Γ ` ∗e : t Γ ` e1 : t ∗ Γ ` e2 : int Γ ` e1 [e2 ] : t Γ ` e1 : t [ ] Γ ` e2 : int Γ ` e1 [e2 ] : t Γ ` e : struct {t1 a1 ; . . . tm am ; } Γ ` e.ai : ti Γ ` e : t (t1 , . . . , tm ) Γ ` e1 : t1 . . . Γ ` em : tm Γ ` e(e1 , . . . , em ) : t Γ ` e1 : int Γ ` e2 : int Γ ` e1 + e2 : int Γ ` e : t1 t1 in t2 konvertierbar Γ ` (t2 ) e : t2 224 / 398 Beispiel: Typ-Prüfung 225 / 398 Beispiel: Typ-Prüfung Ausdruck *a[f(b->c)]+2 und Γ = { Ausdruck *a[f(b->c)]+2: struct list { int info; struct list* next; }; int f(struct list* l); struct { struct list* c;}* b; int* a[11]; + int }: int ∗ int ∗ [ ] 2 [ ] ( ) a f . ∗ c 2 int [ ] ( ) a int (struct list ∗) + ∗ ∗ int f int . struct {struct list ∗ c;} ∗ struct {struct list ∗ c;} ∗ b struct list ∗ c b 226 / 398 227 / 398 Gleichheit von Typen Strukturelle Typ-Gleichheit Alternative Interpretation von Gleichheit (gilt nicht in C): Zusammenfassung Typprüfung: Welche Regel an einem Knoten angewendet werden muss, ergibt sich aus den Typen für die bereits bearbeiteten Kinderknoten Semantisch können wir zwei rekursive Typen t1 , t2 als gleich betrachten, falls sie die gleiche Menge von Pfaden zulassen. Dazu muss die Gleichheit von Typen festgestellt werden. Beispiel: Typgleichheit in C: struct A {} und struct B {} werden als verschieden betrachtet struct list1 { int info; struct { int info; struct list1* next; }* next; } struct list { int info; struct list* next; } ; der Compiler kann die Felder von A und B nach Bedarf umordnen (in C nicht erlaubt) um einen Verband A um weitere Felder zu erweitern, muss er eingebettet werden: typedef struct B { struct A a; int field_of_B; } extension_of_A; Sei struct list* l oder struct list1* l. Beide erlauben l->info Nach typedef int C; haben C und int den gleichen Typ. l->next->info jedoch hat l jeweils einen anderen Typen in C. 228 / 398 Algorithmus zum Test auf Semantische Gleichheit Idee: 229 / 398 Regeln für Wohlgetyptheit t s∗ t∗ t Verwalte Äquivalenz-Anfragen für je zwei Typausdrücke s Sind die beiden Ausdrücke syntaktisch gleich, ist alles gut A t A= s t s t Andernfalls reduziere die Äquivalenz-Anfrage zwischen Äquivalenz-Anfragen zwischen (hoffentlich) einfacheren anderen Typausdrücken struct {s1 a1 ; ... sm am ; } Nehmen wir an, rekursive Typen würden mit Hilfe von Typ-Gleichungen der Form: A=t struct {t1 a1 ; ... tm am ; } s1 t1 sm tm eingeführt (verzichte auf ein Γ). Dann definieren wir folgende Regeln: 230 / 398 Beispiel: 231 / 398 Beweis Beispiel: A B = struct {int info; A ∗ next; } = struct {int info; struct {int info; B ∗ next; } ∗ next; } Wir fragen uns etwa, ob gilt: A B = = struct {int info; A ∗ next; } struct {int info; struct {int info; B ∗ next; } ∗ next; } struct{int info; A ∗ next; } struct{int info; A ∗ next; } struct {int info; A ∗ next; } = B Dazu konstruieren wir: int int B struct{int info; . . . ∗ next; } A ∗ ... ∗ A struct{int info; A ∗ next; } int int struct{int info; B ∗ next; } struct{int info; B ∗ next; } A∗ B∗ A B struct{int info; A ∗ next; } 232 / 398 B 233 / 398 Implementierung Implementierung Stoßen wir bei der Konstruktion des Beweisbaums auf eine Äquivalenz-Anfrage, auf die keine Regel anwendbar ist, sind die Typen ungleich Stoßen wir bei der Konstruktion des Beweisbaums auf eine Äquivalenz-Anfrage, auf die keine Regel anwendbar ist, sind die Typen ungleich Die Konstruktion des Beweisbaums kann dazu führen, dass die gleiche Äquivalenz-Anfrage ein weiteres Mal auftritt Die Konstruktion des Beweisbaums kann dazu führen, dass die gleiche Äquivalenz-Anfrage ein weiteres Mal auftritt Taucht eine Äquivalenz-Anfrage ein weiteres Mal auf, sind die Typen der Anfrage per Definition gleich Taucht eine Äquivalenz-Anfrage ein weiteres Mal auf, sind die Typen der Anfrage per Definition gleich Terminierung? Terminierung? die Menge D aller deklarierten Typen ist endlich es gibt höchstens |D|2 viele Äquivalenzanfragen wiederholte Anfragen sind automatisch erfüllt ; Terminierung ist gesichert 234 / 398 234 / 398 Überladung und Koersion Überladung und Koersion Manche Operatoren wie z.B. + sind überladen: Manche Operatoren wie z.B. + sind überladen: + besitzt mehrere mögliche Typen Zum Beispiel: int +(int,int), float +(float, float) aber auch float* +(float*, int), int* +(int, int*) + besitzt mehrere mögliche Typen Zum Beispiel: int +(int,int), float +(float, float) aber auch float* +(float*, int), int* +(int, int*) je nach Typ hat der Operator + ein unterschiedliche Implementation je nach Typ hat der Operator + ein unterschiedliche Implementation welche Implementierung ausgewählt wird, entscheiden die Argument-Typen welche Implementierung ausgewählt wird, entscheiden die Argument-Typen Koersion: Erlaube auch die Anwendung von + auf int und float. anstatt + für alle Argument-Kombinationen zu definieren, werden die Typen der Argumente konvertiert Konvertierung kann Code erzeugen (z.B. Umwandlung von int nach float) Konvertiert wird in der Regel auf die Supertypen, d.h. 5+0.5 hat Typ float (da float ≥ int) 235 / 398 235 / 398 Koersion von Integer-Typen in C: Promotion Koersion von Integer-Typen in C: Promotion C enthält spezielle Koersionsregeln für Integer: Promotion C enthält spezielle Koersionsregeln für Integer: Promotion unsigned char unsigned short ≤ ≤ int ≤ unsigned int signed char signed short . . . wobei eine Konvertierung über alle Zwischentypen gehen muss. unsigned char unsigned short ≤ ≤ int ≤ unsigned int signed char signed short . . . wobei eine Konvertierung über alle Zwischentypen gehen muss. Subtile Fehler möglich! Berechne Zeichenverteilung in char* str: char* str = "..."; int dist[256]; memset(dist, 0, sizeof(dist)); while (*str) { dist[(unsigned) *str]++; str++; }; Beachte: unsigned bedeutet unsigned int. 236 / 398 236 / 398 Teiltypen Teiltypen Auf den arithmetischen Basistypen char, int, long, etc. gibt es i.a. eine reichhaltige Teiltypen-Beziehungen. Dabei bedeutet t1 ≤ t2 , dass die Menge der Werte vom Typ t1 1 2 3 Auf den arithmetischen Basistypen char, int, long, etc. gibt es i.a. eine reichhaltige Teiltypen-Beziehungen. Dabei bedeutet t1 ≤ t2 , dass die Menge der Werte vom Typ t1 eine Teilmenge der Werte vom Typ t2 sind; in einen Wert vom Typ t2 konvertiert werden können; die Anforderungen an Werte vom Typ t2 erfüllen. eine Teilmenge der Werte vom Typ t2 sind; in einen Wert vom Typ t2 konvertiert werden können; die Anforderungen an Werte vom Typ t2 erfüllen. 1 2 3 Erweitere Teiltypen-Beziehungen der Basistypen auf komplexe Typen 237 / 398 Beispiel: Teiltypen 237 / 398 Regeln für Wohlgetyptheit von Teiltypen Betrachte: string extractInfo( struct { string info; } x) { return x.info; } t s∗ t∗ t s Wir möchten dass extractInfo für alle Argument-Strukturen funktioniert, die eine Komponente string info besitzen A t A= s s t t Wann t1 ≤ t2 gelten soll, beschreiben wir durch Regeln Die Idee ist vergleichbar zur Anwendbarkeit auf Unterklassen (aber allgemeiner) struct {s1 a1 ; ... sm am ; } struct {tj1 aj1 ; ... tjk ajk ; } sj1 tj1 sjk tjk 238 / 398 Regeln und Beispiel für Teiltypen s0 (s1 , . . . , sm ) s0 t0 t1 s1 Regeln und Beispiel für Teiltypen t0 (t1 , . . . , tm ) s0 (s1 , . . . , sm ) tm sm s0 t0 Beispiele: struct {int a; int b; } int (int) int (float) 239 / 398 t1 s1 t0 (t1 , . . . , tm ) tm sm Beispiele: struct {float a; } float (float) float (int) struct {int a; int b; } int (int) int (float) struct {float a; } float (float) float (int) Achtung: Bei Argumenten dreht sich die Anordnung der Typen gerade um 240 / 398 240 / 398 Regeln und Beispiel für Teiltypen s0 (s1 , . . . , sm ) s0 t0 Regeln und Beispiel für Teiltypen t0 (t1 , . . . , tm ) t1 s1 s0 (s1 , . . . , sm ) tm sm s0 t0 Beispiele: t0 (t1 , . . . , tm ) t1 s1 tm sm Beispiele: struct {int a; int b; } int (int) int (float) struct {float a; } float (float) float (int) ≤ 6≤ ≤ struct {int a; int b; } int (int) int (float) Achtung: ≤ 6 ≤ ≤ struct {float a; } float (float) float (int) Achtung: Bei Argumenten dreht sich die Anordnung der Typen gerade um Bei Argumenten dreht sich die Anordnung der Typen gerade um Im Unterschied zum Überschreiben in OO Sprachen wo gilt: Kovarianz der Argumente Kontravarianz der Rückgabewerte Im Unterschied zum Überschreiben in OO Sprachen wo gilt: Kovarianz der Argumente Kontravarianz der Rückgabewerte Diese Regeln können wir direkt benutzen, um auch für rekursive Typen die Teiltyp-Relation zu entscheiden 240 / 398 Teiltypen: Anwendung der Regeln (I) Prüfe ob S1 ≤ R1 : R1 S1 R2 S2 struct {int a; struct {int a; struct {int a; struct {int a; = = = = 240 / 398 Teiltypen: Anwendung der Regeln (II) Prüfe ob S2 ≤ S1 : R1 (R1 ) f ; } int b; S1 (S1 ) f ; } R2 (S2 ) f ; } int b; S2 (R2 ) f ; } S1 R1 int int f int S1 (S1 ) = = = = struct {int a; struct {int a; struct {int a; struct {int a; int R1 (R1 ) f S2 (R2 ) S1 (S1 ) S2 S1 S1 R2 a S1 R1 R1 (R1 ) f ; } int b; S1 (S1 ) f ; } R2 (S2 ) f ; } int b; S2 (R2 ) f ; } S2 S1 a, b a R1 S1 R2 S2 R1 S1 int int f S1 (S1 ) R2 (S2 ) S1 R2 S2 S1 241 / 398 Teiltypen: Anwendung der Regeln (III) Prüfe ob S2 ≤ R1 : R1 S1 R2 S2 = = = = struct {int a; struct {int a; struct {int a; struct {int a; Diskussion R1 (R1 ) f ; } int b; S1 (S1 ) f ; } R2 (S2 ) f ; } int b; S2 (R2 ) f ; } Um die Beweisbäume nicht in den Himmel wachsen zu lassen, werden Zwischenknoten oft ausgelassen S 2 R1 a int int Strukturelle Teiltypen sind sehr mächtig und deshalb nicht ganz leicht zu durchschauen. f S2 (R2 ) R1 (R1 ) S 2 R1 Java verallgemeinert Strukturen zu Objekten / Klassen. Teiltyp-Beziehungen zwischen Klassen müssen explizit deklariert werden R1 R2 a int 242 / 398 int Durch Vererbung wird sichergestellt, dass Unterklassen über die (sichtbaren) Komponenten der Oberklasse verfügen f R1 (R1 ) R2 (S2 ) R1 R2 S2 R1 Überdecken einer Komponente mit einer anderen gleichen Namens ist möglich — aber nur, wenn diese keine Methode ist 243 / 398 244 / 398 Codegenerierung: Überblick Vom AST des Programs erzeugen wir induktiv Instruktionen: für jedes Nicht-Terminal Symbol in der Grammatik gibt es eine Regel zur Generierung von Machineninstruktionen Themengebiet: der Code ist nur ein weiteres Attribut im Syntaxbaum die Codegenerierung benutzt die schon berechneten Attribute Die Synthesephase 245 / 398 Codegenerierung: Überblick 246 / 398 Codegenerierung: Überblick Vom AST des Programs erzeugen wir induktiv Instruktionen: Vom AST des Programs erzeugen wir induktiv Instruktionen: für jedes Nicht-Terminal Symbol in der Grammatik gibt es eine Regel zur Generierung von Machineninstruktionen für jedes Nicht-Terminal Symbol in der Grammatik gibt es eine Regel zur Generierung von Machineninstruktionen der Code ist nur ein weiteres Attribut im Syntaxbaum der Code ist nur ein weiteres Attribut im Syntaxbaum die Codegenerierung benutzt die schon berechneten Attribute die Codegenerierung benutzt die schon berechneten Attribute Um die Codeerzeugung zu spezifizieren, benötigt man Um die Codeerzeugung zu spezifizieren, benötigt man die Semantik der Ausgangssprache (C Standard) die Semantik der Ausgangssprache (C Standard) die Semantik der Maschineninstruktionen die Semantik der Maschineninstruktionen ; Wir definieren zunächst die Machineninstruktionen 246 / 398 246 / 398 Die C-Maschine (CMA) Die Synthesephase Wir erzeugen Code für die C-Maschine. Die C-Maschine ist eine virtuelle Machine. es existiert kein Prozessor, der die Instruktionen der CMA ausführen kann aber es steht ein Interpreter zur Verfügung es seht auch eine Visualisierungsumgebung zur Verfügung die CMA kennt keine double, float, char, short oder long Typen die CMA hat kennt keine Instruktionen um mit dem Betriebssystem zu kommunizieren (Eingabe/Ausgabe) Kapitel 1: Die C-Maschine 247 / 398 248 / 398 Die C-Maschine (CMA) Virtuelle Maschinen Wir erzeugen Code für die C-Maschine. Die C-Maschine ist eine virtuelle Machine. es existiert kein Prozessor, der die Instruktionen der CMA ausführen kann aber es steht ein Interpreter zur Verfügung es seht auch eine Visualisierungsumgebung zur Verfügung die CMA kennt keine double, float, char, short oder long Typen die CMA hat kennt keine Instruktionen um mit dem Betriebssystem zu kommunizieren (Eingabe/Ausgabe) Ein virtuelle Maschine definiert sich wie folgt: Jede virtuelle Maschine stellt einen Satz von Instruktionen zur Verfügung. Instruktionen werden auf der virtuellen Hardware ausgeführt. Die virtuelle Hardware ist eine Menge von Datenstrukturen, auf die die Instruktionen zugreifen ... und die vom Laufzeitsystem verwaltet werden. Für die CMa benötigen wir: C Die CMA ist realistischer als es auf den ersten Blick erscheint: die Einschränkungen können mit wenig Aufwand aufgehoben werden die Java Virtual Machine (JVM) ist sehr ähnlich zur CMA JVM Code kann mittlerweile von einigen eingebetteten Prozessoren direkt ausgeführt werden ein Interpreter für die CMA läuft auf jeder Platform 0 1 PC 0 SP S 249 / 398 248 / 398 Komponenten der CMA Komponenten der CMA Zur Implementierung der CMA verwenden wir folgende Strukturen: Zur Implementierung der CMA verwenden wir folgende Strukturen: S ist der (Daten-)Speicher, auf dem nach dem LIFO-Prinzip neue Zellen allokiert werden können ; Keller/Stack. S ist der (Daten-)Speicher, auf dem nach dem LIFO-Prinzip neue Zellen allokiert werden können ; Keller/Stack. SP (= b Stack Pointer) ist ein Register, das die Adresse der obersten belegten Zelle enthält. Vereinfachung: Alle Daten passen jeweils in eine Zelle von S. SP (= b Stack Pointer) ist ein Register, das die Adresse der obersten belegten Zelle enthält. Vereinfachung: Alle Daten passen jeweils in eine Zelle von S. C ist der Code-Speicher, der das Programm enthält. Jede Zelle des Felds C kann exakt einen virtuellen Befehl aufnehmen. Der Code-Speicher kann nur gelesen werden. PC (= b Program Counter) ist ein Register, das die Adresse des nächsten auszuführenden Befehls enthält. Vor Programmausführung enthält der PC die Adresse 0 ; C[0] enthält den ersten auszuführenden Befehl. 250 / 398 Die Ausführung von Programmen 250 / 398 Die Synthesephase Die Maschine lädt die Instruktion aus C[PC] in ein Instruktions-Register IR und führt sie aus. Vor der Ausführung eines Befehls wird der PC um 1 erhöht. Kapitel 2: while (true) { IR = C[PC]; PC++; execute (IR); } Ausdrucksauswertung Der PC muss vor der Ausführung der Instruktion erhöht werden, da diese möglicherweise den PC überschreibt Die Schleife (der Maschinen-Zyklus) wird durch Ausführung der Instruktion halt verlassen, die die Kontrolle an das Betriebssystem zurückgibt. Die weiteren Instruktionen führen wir nach Bedarf ein 251 / 398 252 / 398 Einfache Ausdrücke und Wertzuweisungen Einfache Ausdrücke und Wertzuweisungen Aufgabe: werte den Ausdruck (1 + 7) ∗ 3 aus Das heißt: erzeuge eine Instruktionsfolge, die Aufgabe: werte den Ausdruck (1 + 7) ∗ 3 aus Das heißt: erzeuge eine Instruktionsfolge, die den Wert des Ausdrucks ermittelt und dann den Wert des Ausdrucks ermittelt und dann oben auf dem Keller ablegt oben auf dem Keller ablegt Idee: berechne erst die Werte für die Teilausdrücke; merke diese Zwischenergebnisse oben auf dem Keller; wende dann den Operator an 253 / 398 Einfache Ausdrücke und Wertzuweisungen 253 / 398 Generelles Prinzip die Argumente für Instruktionen werden oben auf dem Keller erwartet; die Ausführung einer Instruktion konsumiert ihre Argumente; Aufgabe: werte den Ausdruck (1 + 7) ∗ 3 aus Das heißt: erzeuge eine Instruktionsfolge, die möglicherweise berechnete Ergebnisse werden oben auf dem Keller wieder abgelegt. den Wert des Ausdrucks ermittelt und dann oben auf dem Keller ablegt Idee: berechne erst die Werte für die Teilausdrücke; loadc q q merke diese Zwischenergebnisse oben auf dem Keller; wende dann den Operator an ; ähnlich wie postfix Notation des Taschenrechners aus der ersten Übung SP++; S[SP] = q; Die Instruktion loadc q benötigt keine Argumente, legt dafür aber als Wert die Konstante q oben auf dem Stack ab. 254 / 398 253 / 398 Binäre Operatoren Binäre Operatoren Operatoren mit zwei Argumenten werden wie folgt angewandt: 3 8 mul Operatoren mit zwei Argumenten werden wie folgt angewandt: 3 8 24 SP--; S[SP] = S[SP] ∗ S[SP+1]; mul 24 SP--; S[SP] = S[SP] ∗ S[SP+1]; mul erwartet zwei Argumente oben auf dem Stack, konsumiert sie und legt sein Ergebnis oben auf dem Stack ab. 255 / 398 255 / 398 Binäre Operatoren Vergleiche und unäre Operatoren Beispiel: Der Operator Operatoren mit zwei Argumenten werden wie folgt angewandt: 3 8 leq 7 3 24 mul leq 1 Einstellige Operatoren wie neg und not konsumieren dagegen ein Argument und erzeugen einen Wert: SP--; S[SP] = S[SP] ∗ S[SP+1]; mul erwartet zwei Argumente oben auf dem Stack, konsumiert sie und legt sein Ergebnis oben auf dem Stack ab. 8 analog arbeiten auch die übrigen binären arithmetischen und logischen Instruktionen add, sub, div, mod, and, or und xor, wie auch die Vergleiche eq, neq, le, leq, gr und geq. neg -8 S[SP] = – S[SP]; 255 / 398 Zusammensetzung von Instruktionen 256 / 398 Ausdrücke mit Variablen Variablen haben eine Speicherzellen in S: Beispiel: Erzeuge Code für 1 + 7: loadc 1 loadc 7 z: y: x: add Ausführung dieses Codes: loadc 1 1 loadc 7 7 1 add 8 257 / 398 Ausdrücke mit Variablen 258 / 398 Ausdrücke mit Variablen Variablen haben eine Speicherzellen in S: Variablen haben eine Speicherzellen in S: z: y: x: z: y: x: Die Zuordnung von Adressen zu Variablen kann während der Erstellung der Symboltabelle erfolgen. Die Adresse wird an dem Deklarationsknoten der Variable gespeichert. Die Zuordnung von Adressen zu Variablen kann während der Erstellung der Symboltabelle erfolgen. Die Adresse wird an dem Deklarationsknoten der Variable gespeichert. Jede Benutzung einer Variable hat als Attribut einen Zeiger auf den Deklarationsknoten der Variable. 258 / 398 258 / 398 Ausdrücke mit Variablen L-Wert und R-Wert Variablen haben eine Speicherzellen in S: Variablen können auf zwei Weisen verwendet werden. Beispiel: x = y + 1 Für y sind wir am Inhalt der Zelle, für x an der Adresse interessiert. z: y: x: L-Wert von x R-Wert von x Die Zuordnung von Adressen zu Variablen kann während der Erstellung der Symboltabelle erfolgen. Die Adresse wird an dem Deklarationsknoten der Variable gespeichert. Jede Benutzung einer Variable hat als Attribut einen Zeiger auf den Deklarationsknoten der Variable. codeR e ρ codeL e ρ = = Adresse von x Inhalt von x liefert den Code zur Berechnung des R-Werts von e in der Adress-Umgebung ρ analog für den L-Wert Achtung: Im folgenden benutzen wir Übersetzungsfunktionen die eine Funktion ρ nehmen, die für jede Variable x die (Relativ-)Adresse von x liefert. Die Funktion ρ heißt Adress-Umgebung (Address Environment). Nicht jeder Ausdruck verfügt über einen L-Wert (z.B.: x + 1). 259 / 398 258 / 398 Übersetzungschema für Ausdrücke Lesen von Variablen Wir definieren: codeR x ρ = codeR (e1 + e2 ) ρ = codeR (−e) ρ = codeR q ρ codeL x ρ = = ... codeR e1 ρ codeR e2 ρ add ... analog für die anderen binären Operatoren codeR e ρ neg ... analog für andere unäre Operatoren loadc q loadc (ρ x) codeL x ρ load Die Instruktion load lädt den Wert der Speicherzelle, deren Adresse oben auf dem Stack liegt. 13 load 13 13 S[SP] = S[S[SP]]; 260 / 398 Schreiben von Variablen 261 / 398 Ausdrücke mit Variablen codeR (x = e) ρ = codeR e ρ Beispiel: Code für e ≡x = y − 1 mit ρ = {x 7→ 4, y 7→ 7}. Dann liefert codeR e ρ: codeL x ρ store loadc 7 load Die Instruktion store schreibt den Inhalt der zweitobersten Speicherzelle in die Speicherzelle, deren Adresse oben auf dem Keller steht, lässt den geschriebenen Wert aber oben auf dem Keller liegen 13 store loadc 1 sub loadc 4 store Optimierungen: Einführung von Spezialbefehlen für häufige Befehlsfolgen, hier etwa: 13 loada q = 13 storea q = loadc q load loadc q store S[S[SP]] = S[SP-1]; SP--; 262 / 398 263 / 398 Anweisungen und Anweisungsfolgen Die Synthesephase Ist e ein Ausdruck, dann ist e; eine Anweisung (Statement). Anweisungen liefern keinen Wert zurück. Folglich muss der SP vor und nach der Ausführung des erzeugten Codes gleich sein: Kapitel 3: code e; ρ = codeR e ρ pop Anweisungen Die Instruktion pop wirft das oberste Element des Kellers weg ... 1 pop SP--; 264 / 398 265 / 398 Übersetzung von Anweisungsfolgen Die Synthesephase Der Code für eine Anweisungsfolge ist die Konkatenation des Codes for die einzelnen Anweisungen in der Folge: Kapitel 4: Bedingte Anweisungen code (s ss) ρ = code s ρ code ss ρ = // code ε ρ leere Folge von Befehlen Beachte: s ist Statement, ss ist eine Folge von Statements 266 / 398 267 / 398 Sprünge Die Synthesephase Um von linearer Ausführungsreihenfolge abzuweichen, benötigen wir Sprünge: Kapitel 5: jump A Kontrollfluss A PC PC PC = A; 268 / 398 269 / 398 Bedingte Sprünge Verwaltung von Kontrollfluss Zur Übersetzung von Kontrollfluss Anweisungen müssen Sprünge erzeugt werden. Ein bedingter Sprung verzweigt je nach Wert auf dem Keller: 1 bei der Übersetung eines if (c) Konstrukts ist nicht klar, an welcher Adresse gesprungen werden muss, wenn c falsch ist jumpz A PC 0 PC jumpz A A PC PC if (S[SP] == 0) PC = A; SP--; 270 / 398 Verwaltung von Kontrollfluss 271 / 398 Verwaltung von Kontrollfluss Zur Übersetzung von Kontrollfluss Anweisungen müssen Sprünge erzeugt werden. Zur Übersetzung von Kontrollfluss Anweisungen müssen Sprünge erzeugt werden. bei der Übersetung eines if (c) Konstrukts ist nicht klar, an welcher Adresse gesprungen werden muss, wenn c falsch ist Instruktionsfolgen können unterschiedlich arrangiert werden bei der Übersetung eines if (c) Konstrukts ist nicht klar, an welcher Adresse gesprungen werden muss, wenn c falsch ist Instruktionsfolgen können unterschiedlich arrangiert werden minimiere dabei die Anzahl der unbedingten Sprünge minimiere so dass weniger innerhalb Schleifen gesprungen wird ersetze weite Sprünge (far jumps) durch nahe Sprünge (near jumps) minimiere dabei die Anzahl der unbedingten Sprünge minimiere so dass weniger innerhalb Schleifen gesprungen wird ersetze weite Sprünge (far jumps) durch nahe Sprünge (near jumps) organisiere die Instruktionen in Blöcke ohne Sprünge 271 / 398 Verwaltung von Kontrollfluss 271 / 398 Basic Blöcke in der C-Maschine Zur Übersetzung von Kontrollfluss Anweisungen müssen Sprünge erzeugt werden. Die CMA verfügt nur über einen bedingten Sprung, jumpz. bei der Übersetung eines if (c) Konstrukts ist nicht klar, an welcher Adresse gesprungen werden muss, wenn c falsch ist Instruktionsfolgen können unterschiedlich arrangiert werden code ss minimiere dabei die Anzahl der unbedingten Sprünge minimiere so dass weniger innerhalb Schleifen gesprungen wird ersetze weite Sprünge (far jumps) durch nahe Sprünge (near jumps) c organisiere die Instruktionen in Blöcke ohne Sprünge Dazu definieren wir: Die ausgehenden Kanten haben eine spezielle Form: Definition Ein basic block ist eine Folge von Anweisungen ss, die keine Sprünge enthält eine ausgehenden Menge von Kanten zu anderen basic blocks wobei jede Kante mit einer Bedingung annotiert sein kann 271 / 398 272 / 398 Basic Blöcke in der C-Maschine Basic Blöcke in der C-Maschine Die CMA verfügt nur über einen bedingten Sprung, jumpz. Die CMA verfügt nur über einen bedingten Sprung, jumpz. code ss code ss c c Die ausgehenden Kanten haben eine spezielle Form: 1 Die ausgehenden Kanten haben eine spezielle Form: eine Kante (unbedingter Sprung), mit jump übersetzt 1 2 eine Kante (unbedingter Sprung), mit jump übersetzt eine Kante mit Bedingung c = 0 (jumpz), eine Kante ohne Bedinung (jump) 272 / 398 Basic Blöcke in der C-Maschine 272 / 398 Beschreibung von Kontrollfluss Übersetzungen Der Einfachheit halber verwenden wir symbolische Sprungziele zur Beschreibung der Übersetzung von Kontrollflussinstruktionen. Die CMA verfügt nur über einen bedingten Sprung, jumpz. Ein zweiten Durchlauf ist nötig, um symbolische Adressen durch absolute Code-Adressen zu ersetzen. code ss c Die ausgehenden Kanten haben eine spezielle Form: 1 2 3 eine Kante (unbedingter Sprung), mit jump übersetzt eine Kante mit Bedingung c = 0 (jumpz), eine Kante ohne Bedinung (jump) eine Liste von Kanten (jumpi) und eine default Kante (jump); wird verwendet für switch Anweisungen (kommt später) 272 / 398 Beschreibung von Kontrollfluss Übersetzungen 273 / 398 Beschreibung von Kontrollfluss Übersetzungen Der Einfachheit halber verwenden wir symbolische Sprungziele zur Beschreibung der Übersetzung von Kontrollflussinstruktionen. Der Einfachheit halber verwenden wir symbolische Sprungziele zur Beschreibung der Übersetzung von Kontrollflussinstruktionen. Ein zweiten Durchlauf ist nötig, um symbolische Adressen durch absolute Code-Adressen zu ersetzen. Ein zweiten Durchlauf ist nötig, um symbolische Adressen durch absolute Code-Adressen zu ersetzen. Alternativ könnten direkt relative Sprünge eingeführt werden: Alternativ könnten direkt relative Sprünge eingeführt werden: relative Sprünge haben Ziele relative zum aktuellen PC relative Sprünge haben Ziele relative zum aktuellen PC oft reichen kleinere Adressen aus (; near jumps) oft reichen kleinere Adressen aus (; near jumps) der Code wird relokierbar, d. h. kann im Speicher unverändert hin und her geschoben werden (position independent code, PIC) der Code wird relokierbar, d. h. kann im Speicher unverändert hin und her geschoben werden (position independent code, PIC) Der erzeugte Code wäre im Prinzip direkt ausführbar. Der erzeugte Code wäre im Prinzip direkt ausführbar. Basic Blöcke haben den Vorteil, dass sie später zur Programmoptimierung weiterverwendet werden können. (Z.B. Reduzierung von Sprüngen.) 273 / 398 273 / 398 Bedingte Anweisung, einseitig Zweiseitiges if Betrachten wir zuerst s ≡ if ( c ) ss. Wir beschreiben die Code-Erzeugung zunächst ohne Basic-Blöcke. Betrachte nun s ≡ if ( c ) tt else ee. Idee: Lege den Code zur Auswertung von c und ss hintereinander in den Code-Speicher; code Füge Sprung-Befehlen so ein, dass ein korrekter Kontroll-Fluss gewährleistet ist code s ρ = codeR c ρ jumpz A code ss ρ A : ... code c Die gleiche Strategie liefert: code s ρ code R für c = jumpz codeR c ρ jumpz jumpz A code für tt jump jump B A : code ee ρ B: ee code R für c code tt ρ code für ss code tt code für ee ... 274 / 398 Beispiel für if-Anweisung 275 / 398 Iterative Anweisungen Sei ρ = {x 7→ 4, y 7→ 7} und s ≡ if (x > y) x = x − y; else y = y − x; Betrachte schließlich die Schleife s ≡ while (e) s0 . Dafür erzeugen wir: (i) (ii) (iii) code s ρ = code R für e A : codeR e ρ Dann liefert code s ρ : jumpz jumpz B loada 4 loada 7 gr jumpz A loada 4 loada 7 sub storea 4 pop jump B (i) (ii) A: B: code s0 ρ loada 7 loada 4 sub storea 7 pop ... code für s’ jump A B: jump ... (iii) 276 / 398 Beispiel: Übersetzung von Schleifen for-Schleifen Die for-Schleife s ≡ for (e1 ; e2 ; e3 ) s0 ist äquivalent zu der Statementfolge e1 ; while (e2 ) {s0 e3 ; } – sofern s0 keine continue-Anweisung enthält. Darum übersetzen wir: Sei ρ = {a 7→ 7, b 7→ 8, c 7→ 9} und s das Statement: while (a > 0) {c = c + 1; a = a − b; } Dann liefert code s ρ die Folge: A: loada 7 loadc 0 gr jumpz B loada 9 loadc 1 add storea 9 pop 277 / 398 code s ρ loada 7 loada 8 sub storea 7 pop jump A B: ... 278 / 398 codeR e1 pop A : codeR e2 ρ jumpz B code s0 ρ codeR e3 ρ pop jump A B : ... = 279 / 398 Das switch-Statement Aufeinanderfolgende Alternativen Idee: Unterstütze Mehrfachverzweigung in konstanter Zeit Wir betrachten zunächst nur switch-Statements der folgenden Form: Benutze Sprungtabelle, die an der i-ten Stelle den Sprung an den Anfang der i-tem Alternative enthält. Eine Möglichkeit zur Realisierung besteht in der Einführung von indizierten Sprüngen. q s ≡ jumpi B case k − 1: ssk−1 break; default: ssk B+q PC switch (e) { case 0: ss0 break; case 1: ss1 break; .. . } PC Dann ergibt sich für s die Instruktionsfolge: PC = B + S[SP]; SP--; 280 / 398 Codeerzeugung für spezielle switch-Anweisungen 281 / 398 Semantik des Makros check 0 k B code s ρ = codeR e ρ check 0 k B C0 : Ck : code ss0 ρ jump D ... code ssk ρ jump D B: D: = dup loadc 0 geq jumpz A jump C0 ... jump Ck ... dup loadc k leq jumpz A A: jumpi B pop loadc k jumpi B Weil der R-Wert von e noch zur Indizierung benötigt wird, muss er vor jedem Vergleich kopiert werden. Dazu dient der Befehl dup. Das Macro check 0 k B überprüft, ob der R-Wert von e im Intervall [0, k] liegt, und führt einen indizierten Sprung in die Tabelle B aus. 3 Die Sprungtabelle enthält direkte Sprünge zu den jeweiligen Alternativen. dup 3 3 S[SP+1] = S[SP]; SP++; Am Ende jeder Alternative steht ein Sprung hinter das switch-Statement. Ist der R-Wert von e kleiner als 0 oder größer als k, ersetzen wir ihn vor dem indizierten Sprung durch k. 282 / 398 Übersetzung der Sprungtabelle 283 / 398 Allgemeine Übersetzung der switch-Anweisung Im Allgemeinen können die Werte der Alternativen weit auseinander liegen. Die Sprung-Tabelle liegt bei der beschriebenen Übersetzung hinter dem Code der Alternativen. Dies ist einfach zu realisieren, da die Sprungtabelle als L-Attribut beim Betrachten der Fälle generiert werden kann. generiere eine folge von expliziten Tests, wie für die if-Anweisung Soll die Sprungtabelle hinter das check Makro gelegt werden muss eventuell das switch-Statement zweimal durchsucht werden. Beginnt die Tabelle mit u statt mit 0, müssen wir den R-Wert von e um u vermindern, bevor wir ihn als Index benutzen. Sind sämtliche möglichen Werte von e sicher im Intervall [0, k], können wir auf check verzichten. 284 / 398 285 / 398 Allgemeine Übersetzung der switch-Anweisung Allgemeine Übersetzung der switch-Anweisung Im Allgemeinen können die Werte der Alternativen weit auseinander liegen. Im Allgemeinen können die Werte der Alternativen weit auseinander liegen. generiere eine folge von expliziten Tests, wie für die if-Anweisung generiere eine folge von expliziten Tests, wie für die if-Anweisung bei n verschiedenen Tests kann binäre Suche angewendet werden ; O(log n) Tests bei n verschiedenen Tests kann binäre Suche angewendet werden ; O(log n) Tests hat die Menge der getesteten Werte kleine Lücken (≤ 3), so ist Sprungtabelle effizienter und platzsparender 285 / 398 Allgemeine Übersetzung der switch-Anweisung 285 / 398 Allgemeine Übersetzung der switch-Anweisung Im Allgemeinen können die Werte der Alternativen weit auseinander liegen. Im Allgemeinen können die Werte der Alternativen weit auseinander liegen. generiere eine folge von expliziten Tests, wie für die if-Anweisung generiere eine folge von expliziten Tests, wie für die if-Anweisung bei n verschiedenen Tests kann binäre Suche angewendet werden ; O(log n) Tests bei n verschiedenen Tests kann binäre Suche angewendet werden ; O(log n) Tests hat die Menge der getesteten Werte kleine Lücken (≤ 3), so ist Sprungtabelle effizienter und platzsparender hat die Menge der getesteten Werte kleine Lücken (≤ 3), so ist Sprungtabelle effizienter und platzsparender eventuell kann man mehrere Sprungtabellen für verschiedene, zusammenhängende Mengen generieren eventuell kann man mehrere Sprungtabellen für verschiedene, zusammenhängende Mengen generieren ein Suchbaum mit Tabellen kann durch profiling anders angeordnet werden, so dass häufig genommene Pfade weniger Tests erfordern 285 / 398 Übersetzung in Basic Blocks 285 / 398 Übersetzung in Basic Blocks Problem: Wie verknüpft man die verschiedenen Basic Blöcke? Idee: Problem: Wie verknüpft man die verschiedenen Basic Blöcke? Idee: beim Übersetzen einer Funktion: erzeuge einen leeren Basic Block und speichere den Zeiger im Konten der Funktion beim Übersetzen einer Funktion: erzeuge einen leeren Basic Block und speichere den Zeiger im Konten der Funktion reiche diesen Basic Block zur Übersetzung der Anweisungen runter 286 / 398 286 / 398 Übersetzung in Basic Blocks Übersetzung in Basic Blocks Problem: Wie verknüpft man die verschiedenen Basic Blöcke? Idee: Problem: Wie verknüpft man die verschiedenen Basic Blöcke? Idee: beim Übersetzen einer Funktion: erzeuge einen leeren Basic Block und speichere den Zeiger im Konten der Funktion reiche diesen Basic Block zur Übersetzung der Anweisungen runter beim Übersetzen einer Funktion: erzeuge einen leeren Basic Block und speichere den Zeiger im Konten der Funktion reiche diesen Basic Block zur Übersetzung der Anweisungen runter eine Zuweisung wird am Ende des Basic Blocks angehängt eine Zuweisung wird am Ende des Basic Blocks angehängt eine zweiseitige if Anweisung erzeugt drei neue, leere Blöcke: 1 2 3 einen für den then-Zweig, verbunden mit aktuellem Block durch jumpz Kante einen für den else-Zweig, durch jump Kante verbunden einen für die nachfolgenden Anweisungen, verbunden mit thenund else-Zweig durch jump Kante 286 / 398 Übersetzung in Basic Blocks 286 / 398 Übersetzung in Basic Blocks Problem: Wie verknüpft man die verschiedenen Basic Blöcke? Idee: Problem: Wie verknüpft man die verschiedenen Basic Blöcke? Idee: beim Übersetzen einer Funktion: erzeuge einen leeren Basic Block und speichere den Zeiger im Konten der Funktion reiche diesen Basic Block zur Übersetzung der Anweisungen runter beim Übersetzen einer Funktion: erzeuge einen leeren Basic Block und speichere den Zeiger im Konten der Funktion reiche diesen Basic Block zur Übersetzung der Anweisungen runter eine Zuweisung wird am Ende des Basic Blocks angehängt eine zweiseitige if Anweisung erzeugt drei neue, leere Blöcke: eine Zuweisung wird am Ende des Basic Blocks angehängt eine zweiseitige if Anweisung erzeugt drei neue, leere Blöcke: 1 2 3 einen für den then-Zweig, verbunden mit aktuellem Block durch jumpz Kante einen für den else-Zweig, durch jump Kante verbunden einen für die nachfolgenden Anweisungen, verbunden mit thenund else-Zweig durch jump Kante 1 2 3 ähnlich für andere Konstrukte einen für den then-Zweig, verbunden mit aktuellem Block durch jumpz Kante einen für den else-Zweig, durch jump Kante verbunden einen für die nachfolgenden Anweisungen, verbunden mit thenund else-Zweig durch jump Kante ähnlich für andere Konstrukte Zur besseren Navigation sollten auch Rückwärtskanten zwischen den Blöcken eingefügt werden. 286 / 398 Felder Übersetzung von Felderzugriffen Beispiel: int[11] a; Das Feld a enthält 11 Elemente und benötigt darum 11 Zellen. ρ a ist die Adresse des Elements a[0]. 286 / 398 Erweitere codeL und codeR auf Ausdrücke mit indizierten Feldzugriffen. a[10] Sei t[c] a; die Deklaration eines Feldes a. a[0] Definiere Funktion | · | um den Platzbedarf eines Typs zu berechnen: 1 falls t ein Basistyp |t| = k · |t0 | falls t ≡ t0 [k] Dann ergibt sich für die Deklaration d ≡ t1 x1 ; . . . tk xk ; ρ x1 = 1 ρ xi = ρ xi−1 + |ti−1 | für i > 1 | · | kann zur Übersetzungszeit berechnet werden, also auch ρ. Beachte: | · | ist nötig zur Implementierung von C’s sizeof Operator 287 / 398 288 / 398 Übersetzung von Felderzugriffen Übersetzung von Felderzugriffen Erweitere codeL und codeR auf Ausdrücke mit indizierten Feldzugriffen. Erweitere codeL und codeR auf Ausdrücke mit indizierten Feldzugriffen. Sei t[c] a; die Deklaration eines Feldes a. Um die Adresse des Elements a[i]zu bestimmen, müssen wir ρ a + |t| ∗ (R-Wert von i) ausrechnen. Folglich: Sei t[c] a; die Deklaration eines Feldes a. Um die Adresse des Elements a[i]zu bestimmen, müssen wir ρ a + |t| ∗ (R-Wert von i) ausrechnen. Folglich: codeL e1 [e2 ] ρ = codeR e1 ρ codeR e2 ρ loadc |t| mul add codeL e1 [e2 ] ρ = codeR e1 ρ codeR e2 ρ loadc |t| mul add Bemerkung: In C ist ein Feld ein Zeiger. Ein deklariertes Feld a ist eine Zeiger-Konstante, deren R-Wert die Anfangsadresse von a ist. Formal setzen wir für ein Feld e codeR e ρ = codeL e ρ In C sind äquivalent (als L-Werte, nicht vom Typ): 2[a] a[2] a+2 288 / 398 Strukturen (Records) 288 / 398 Strukturen (Records) Achtung: Komponenten-Namen von Strukturen dürfen sich überlappen. Hier: Komponenten-Umgebung ρst bezieht sich auf den gerade übersetzten Struktur-Typ st. Sei struct int a; int b; x; Teil einer Deklarationsliste. x erhält die erste freie Zelle des Platzes für die Struktur als Relativ-Adresse. Für die Komponenten vergeben wir Adressen relativ zum Anfang der Struktur, hier a 7→ 0, b 7→ 1. Achtung: Komponenten-Namen von Strukturen dürfen sich überlappen. Hier: Komponenten-Umgebung ρst bezieht sich auf den gerade übersetzten Struktur-Typ st. Sei struct int a; int b; x; Teil einer Deklarationsliste. x erhält die erste freie Zelle des Platzes für die Struktur als Relativ-Adresse. Für die Komponenten vergeben wir Adressen relativ zum Anfang der Struktur, hier a 7→ 0, b 7→ 1. Sei allgemein t ≡struct int a; int b;. Dann ist |t| = k X i=1 |ti | ρ c1 = 0 ρ ci = ρ ci−1 + |ti−1 | für i > 1 Damit erhalten wir: codeL (e.c) ρ = codeL e ρ loadc (ρ c) add 289 / 398 289 / 398 Zeiger in C Die Synthesephase Mit Zeiger (-Werten) rechnen, heißt in der Lage zu sein, 1 Zeiger zu erzeugen, d.h. Zeiger auf Speicherzellen zu setzen; sowie 2 Zeiger zu dereferenzieren, d. h. durch Zeiger auf die Werte von Speicherzellen zugreifen. Erzeugen von Zeigern: Die Anwendung des Adressoperators & liefert einen Zeiger auf eine Variable, d. h. deren Adresse (= b L-Wert). Deshalb: Kapitel 6: Zeiger und dynamische Speicherverwaltung codeR (&e) ρ = codeL e ρ Beispiel: Sei struct int a; int b; x; mit ρ = {x 7→ 13, a 7→ 0, b 7→ 1} gegeben. Dann ist codeL (x.b) ρ 290 / 398 = loadc 13 loadc 1 add 291 / 398 Dereferenzieren von Zeigern Übersetzung von Dereferenzierungen (I) Sei ρ = {i 7→ 1, j 7→ 2, pt 7→ 3, a 7→ 0, b 7→ 7 }. Die Anwendung des Operators * auf den Ausdruck e liefert den Inhalt der Speicherzelle, deren Adresse der R-Wert von e ist: b: struct t { int a[7]; struct t *b; }; int i,j; struct t *pt; codeL (∗e) ρ = codeR e ρ Beispiel: Betrachte für Übersetze e ≡((pt -> b) -> a)[i+1] struct t { int a[7]; struct t *b; }; int i,j; struct t *pt; Dann ist: codeL e ρ Wegen e->a ≡ (*e).a gilt: codeL (e → a) ρ b: pt: j: i: den Ausdruck e ≡((pt -> b) -> a)[i+1] = = codeR e ρ loadc (ρ a) add codeR ((pt → b) → a) ρ codeR (i + 1) ρ loadc 1 mul add a: a: = codeR ((pt → b) → a) ρ loada 1 loadc 1 add loadc 1 mul add 292 / 398 Übersetzung von Dereferenzierungen (II) Der Heap Zeiger gestatten auch den Zugriff auf anonyme, dynamisch erzeugte Datenelemente, deren Lebenszeit nicht dem LIFO-Prinzip unterworfen ist. ; Wir benötigen einen potentiell beliebig großen Speicherbereich H: den Heap (Halde). Implementierung: Für dereferenzierte Felder (*e).a ist der R-Wert gleich dem L-Wert von e plus Offset von a. Deshalb erhalten wir: codeR ((pt → b) → a) ρ = codeR (pt → b) ρ loadc 0 add = Damit ergibt sich insgesamt die Folge: loada 3 loadc 7 add load loadc 0 add loada 1 loadc 1 add 293 / 398 loada 3 loadc 7 add load loadc 0 add S H 0 MAX SP loadc 1 mul add NP EP EP NP = b New Pointer; zeigt auf unterste belegte Haldenzelle. = b Extreme Pointer; zeigt auf die Zelle, auf die der SP maximal zeigen kann (innerhalb der aktuellen Funktion). 295 / 398 294 / 398 Invarianten des Heaps und des Stacks Invarianten des Heaps und des Stacks Stack und Heap dürfen sich nicht überschneiden Stack und Heap dürfen sich nicht überschneiden eine Überschneidung kann bei jeder Erhöhung von SP eintreten (Stack Overflow) oder bei einer Erniedrigung des NP eintreten (Out Of Memory) 296 / 398 296 / 398 Invarianten des Heaps und des Stacks Invarianten des Heaps und des Stacks Stack und Heap dürfen sich nicht überschneiden Stack und Heap dürfen sich nicht überschneiden eine Überschneidung kann bei jeder Erhöhung von SP eintreten (Stack Overflow) oder bei einer Erniedrigung des NP eintreten (Out Of Memory) eine Überschneidung kann bei jeder Erhöhung von SP eintreten (Stack Overflow) oder bei einer Erniedrigung des NP eintreten (Out Of Memory) im Gegensatz zum Stack Overflow kann ein Out Of Memory Fehler vom Programmierer abgefangen werden malloc liefert in diesem Fall NULL zurück, dass als (void*) 0 definiert ist im Gegensatz zum Stack Overflow kann ein Out Of Memory Fehler vom Programmierer abgefangen werden malloc liefert in diesem Fall NULL zurück, dass als (void*) 0 definiert ist EP reduziert die nötigen Überprüfungen auf Überschneidung auf den Funktionseintritt die Überprüfungen bei Heap-Allokationen bleiben erhalten 296 / 398 Dynamisch Allokierter Speicher Freigabe von Speicherplatz Es gibt eine weitere Arte, Zeiger zu erzeugen: ein Aufruf von malloc liefert einen Zeiger auf eine Heap-Zelle: codeR malloc (e) ρ = Ein mit malloc allokierter Bereich muss irgendwann mit free wieder freigegeben werden. Probleme: codeR e ρ new NP 296 / 398 Der freigegebene Speicherbereich könnte noch von anderen Zeigern referenziert (dangling references). NP Nach einiger Freigabe könnte der Speicher etwa so aussehen (fragmentation): n new n if (NP - S[SP] <= EP) S[SP] = NULL; else { NP = NP - S[SP]; S[SP] = NP; } frei 298 / 398 297 / 398 Mögliche Auswege: 1 2 Die Synthesephase Nimm an, der Programmierer weiß, was er tut. Verwalte dann die freien Abschnitte (etwa sortiert nach Größe) in einer speziellen Datenstruktur; ; malloc wird teuer Kapitel 7: Tue nichts, d.h.: code free (e); ρ = Funktionen codeR e ρ pop ; einfach und effizient, aber nicht für reaktive Progaramme 3 Benutze eine automatische, evtl. “konservative” Garbage-Collection, die gelegentlich sicher nicht mehr benötigten Heap-Platz einsammelt und dann malloc zur Verfügung stellt. 299 / 398 300 / 398 Aufbau einer Funktion Speicherverwaltung bei Funktionen Die Definition einer Funktion besteht aus int fac(int x) { if (x<=0) return 1; else return x*fac(x-1); } einem Namen, mit dem sie aufgerufen werden kann; einer Spezifikation der formalen Parameter; evtl. einem Ergebnistyp; int main(void) { int n; n = fac(2) + fac(1); printf("%d", n); } einem Anweisungsteil. In C gilt: codeR f ρ = loadc _f = Zu einem Ausführungszeitpunkt können mehrere Instanzen der gleichen Funktion aktiv sein, d. h. begonnen, aber noch nicht beendet sein. Der Rekursionsbaum im Beispiel: Anfangsadresse des Codes für f Beachte: main auch Funktions-Namen müssen eine Adresse zugewiesen bekommen da die Größe von Funktionen nicht vor der Übersetzung bekannt ist, müssen die Adressen der Funktionen anschließend eingetragen werden fac fac fac fac printf fac 301 / 398 Speicherverwaltung von Funktionsvariablen 302 / 398 Speicherverwaltung von Funktionsvariablen Die formalen Parameter und lokalen Variablen der verschiedenen Aufrufe der selben Funktion (Instanzen) müssen auseinander gehalten werden. Die formalen Parameter und lokalen Variablen der verschiedenen Aufrufe der selben Funktion (Instanzen) müssen auseinander gehalten werden. Idee zur Implementierung: Idee zur Implementierung: lege einen speziellen Speicherbereich für jeden Aufruf einer Funktion an. 303 / 398 Speicherverwaltung von Funktionsvariablen 303 / 398 Speicherverwaltung von Funktionsvariablen Die formalen Parameter und lokalen Variablen der verschiedenen Aufrufe der selben Funktion (Instanzen) müssen auseinander gehalten werden. Die formalen Parameter und lokalen Variablen der verschiedenen Aufrufe der selben Funktion (Instanzen) müssen auseinander gehalten werden. Idee zur Implementierung: Idee zur Implementierung: lege einen speziellen Speicherbereich für jeden Aufruf einer Funktion an. lege einen speziellen Speicherbereich für jeden Aufruf einer Funktion an. in sequentiellen Programmiersprachen können diese Speicherbereiche auf dem Keller verwaltet werden in sequentiellen Programmiersprachen können diese Speicherbereiche auf dem Keller verwaltet werden jede Instanz einer Funktion erhält dadurch einen Bereich auf dem Stack 303 / 398 303 / 398 Speicherverwaltung von Funktionsvariablen Kellerrahmen-Organisation SP Die formalen Parameter und lokalen Variablen der verschiedenen Aufrufe der selben Funktion (Instanzen) müssen auseinander gehalten werden. lokale Variablen Idee zur Implementierung: FP PCold FPold lege einen speziellen Speicherbereich für jeden Aufruf einer Funktion an. organisatorische Zellen EPold in sequentiellen Programmiersprachen können diese Speicherbereiche auf dem Keller verwaltet werden formale Parameter / Funktionswert jede Instanz einer Funktion erhält dadurch einen Bereich auf dem Stack FP = b Frame Pointer; zeigt auf die letzte organisatorische Zelle und wird zur Adressierung der formalen Parameter und lokalen Variablen benutzt. diese Bereiche heißen Keller-Rahmen (oder Stack Frame) 304 / 398 303 / 398 Adressierung von Variablen im Kellerrahmen Adressierung von Variablen im Kellerrahmen Die lokalen Variablen erhalten Relativadressen +1, +2, . . .. Die lokalen Variablen erhalten Relativadressen +1, +2, . . .. Die formalen Parameter liegen unterhalb der organisatorischen Zellen und haben deshalb negative Adressen relativ zu FP Die formalen Parameter liegen unterhalb der organisatorischen Zellen und haben deshalb negative Adressen relativ zu FP Diese Organisation ist besonders geeignet für Funktionsaufrufe mit variabler Argument-Anzahl wie z.B. printf. Diese Organisation ist besonders geeignet für Funktionsaufrufe mit variabler Argument-Anzahl wie z.B. printf. Den Speicherbereich für die Parameter recyclen wir zur Speicherung des Rückgabe werts der Funktion Vereinfachung: Der Rückgabewert passt in eine einzige Zelle. Den Speicherbereich für die Parameter recyclen wir zur Speicherung des Rückgabe werts der Funktion Vereinfachung: Der Rückgabewert passt in eine einzige Zelle. Die organisatorischen Zellen speichern die Register-Inhalte, die nach dem Funktions-Aufruf restauriert werden müssen. Bei einem Funktions-Aufruf muss der FP in eine organisatorische Zelle gerettet werden. 305 / 398 Adressierung von Variablen im Kellerrahmen Bestimmung der Adress-Umgebung Die lokalen Variablen erhalten Relativadressen +1, +2, . . .. Die formalen Parameter liegen unterhalb der organisatorischen Zellen und haben deshalb negative Adressen relativ zu FP Diese Organisation ist besonders geeignet für Funktionsaufrufe mit variabler Argument-Anzahl wie z.B. printf. Den Speicherbereich für die Parameter recyclen wir zur Speicherung des Rückgabe werts der Funktion Vereinfachung: Der Rückgabewert passt in eine einzige Zelle. Die organisatorischen Zellen speichern die Register-Inhalte, die nach dem Funktions-Aufruf restauriert werden müssen. Bei einem Funktions-Aufruf muss der FP in eine organisatorische Zelle gerettet werden. Unsere Übersetzungsaufgaben für Funktionen: 1 erzeuge Code für den Rumpf 2 erzeuge Code für Aufrufe 305 / 398 305 / 398 Variablen können in verschiedenen Symboltabellen liegen: 1 globale/externe, die außerhalb von Funktionen definiert werden; 2 lokale/interne/automatische (inklusive formale Parameter), die innerhalb von Funktionen definiert werden. Erweitere die Adress-Umgebung ρ so dass ρ Variablen auf Tuple (tag, a) ∈ {G, L} × Z abbildet. Idee: Das tag ist L: die Variable wurde in einer der lokalen Symboltabellen gefunden G: die Variable wurde in der globalen Symboltabelle gefunden Beispiel: int x, y; v ρ(v) void f(int v, int w) { x h , i int a; y h , i if (a>0) { v h , i int b; w h , i } else { a h , i int c; b h , i } c h , i } 306 / 398 Bestimmung der Adress-Umgebung Variablen können in verschiedenen Symboltabellen liegen: 1 globale/externe, die außerhalb von Funktionen definiert werden; 2 lokale/interne/automatische (inklusive formale Parameter), die innerhalb von Funktionen definiert werden. Erweitere die Adress-Umgebung ρ so dass ρ Variablen auf Tuple (tag, a) ∈ {G, L} × Z abbildet. Idee: Das tag ist L: die Variable wurde in einer der lokalen Symboltabellen gefunden G: die Variable wurde in der globalen Symboltabelle gefunden Beispiel: int x, y; v ρ(v) void f(int v, int w) { G ,0i x h int a; y hG,1i if (a>0) { v h L , -3 i int b; w h L , -4 i } else { a hL ,1i int c; b hL ,2i } c hL ,2i } Die Synthesephase Kapitel 8: Betreten und Verlassen von Funktionen 306 / 398 Position von Funktionsargumenten Arbeitsteilung beim Funktionsaufruf Die aktuellen Parameter werden von rechts nach links ausgewertet Der erste Parameter liegt direkt unterhalb der organisatorischen Zellen PC, FC, EP Für einen Prototypen τ f (τ1 x1 , . . . , τk xk ) setzen wir: x1 7→ (L, −2 − |τ1 |) Definition Sei f die aktuelle Funktion, die die Funktion g aufruft. f heißt caller xi 7→ (L, −2 − |τ1 | − . . . − |τi |) g heißt callee Die Reihenfolge ermöglicht Funktionen mit variabler Parameteranzahl (variadic functions) wie printf int printf(const char * format, ...); char *s = value "Hello %s!\nIt’s %i to %i!\n"; s "World" int main(void) { 5 printf(s ,"World", 5, 12); 12 return 0; } 307 / 398 Der Code für den Aufruf muss auf den Caller und den Callee verteilt werden. Die Aufteilung kann nur so erfolgen, dass der Teil, der von Informationen des Callers abhängt, auch dort erzeugt wird und analog für den Callee. ρ(pi ) hL, −3i hL, −4i hL, −5i hL, −6i Beobachtung: Den Platz für die aktuellen Parameter kennt nur der Caller: Beispiel: printf 308 / 398 Prinzip vom Funktionsaufruf und Rücksprung Aufruf-Sequenz Aktionen beim Betreten von g: Bestimmung der aktuellen Parameter Retten von FP, EP Bestimmung der Anfangsadresse von g Setzen des neuen FP Retten von PC und Sprung an den Anfang von g 6. Setzen des neuen EP 7. Allokieren der lokalen Variablen 1. 2. 3. 4. 5. Aktionen bei Verlassen von g: Berechnen des Rückgabewerts Abspeichern des Rückgabewerts Rücksetzen der Register FP, EP, SP Rücksprung in den Code von f , d. h. Restauration des PC 4. Aufräumen des Stack 0. 1. 2. 3. mark stehen in f call enter stehen in g alloc return slide 309 / 398 310 / 398 Damit erhalten wir für einen Aufruf für eine Funktion mit mindestens einem Parameter und einem Rückgabewert: codeR g(e1 , . . . , en ) ρ = codeR en ρ ... codeR e1 ρ mark codeR g ρ call slide (s − 1) wobei s der Platz für die aktuellen Parameter ist. Beachte: 311 / 398 Aufruf-Sequenz Aufruf-Sequenz Damit erhalten wir für einen Aufruf für eine Funktion mit mindestens einem Parameter und einem Rückgabewert: codeR g(e1 , . . . , en ) ρ Damit erhalten wir für einen Aufruf für eine Funktion mit mindestens einem Parameter und einem Rückgabewert: = codeR en ρ ... codeR e1 ρ mark codeR g ρ call slide (s − 1) codeR g(e1 , . . . , en ) ρ wobei s der Platz für die aktuellen Parameter ist. Beachte: = codeR en ρ ... codeR e1 ρ mark codeR g ρ call slide (s − 1) wobei s der Platz für die aktuellen Parameter ist. Beachte: Von jedem Ausdruck, der als aktueller Parameter auftritt, wird jeweils der R-Wert berechnet ; Call-by-Value-Parameter-Übergabe. Von jedem Ausdruck, der als aktueller Parameter auftritt, wird jeweils der R-Wert berechnet ; Call-by-Value-Parameter-Übergabe. Die Funktion g kann auch ein Ausdruck sein, dessen R-Wert die Anfangs-Adresse der aufzurufenden Funktion liefert 311 / 398 Berechnung von R-Werten 311 / 398 Kopien von Speicherbereichen Die move Instruktion kopiert k Elemente auf den Stack. Ähnlich deklarierten Feldern, werden Funktions-Namen als konstante Zeiger auf Funktionen aufgefasst. Dabei ist der R-Wert dieses Zeigers gleich der Anfangs-Adresse der Funktion. Achtung! Für eine Variable int (*)() g sind die beiden Aufrufe (*g)() und g() äquivalent. Die Dereferenzierungen eines Funktions-Zeigers ist unnötig und wird ignoriert. Bei der Parameter-Übergabe von Strukturen werden diese kopiert. k move k Folglich: codeR f ρ = codeR (∗e) ρ = codeR e ρ = f ein Funktions-Name e ein Funktions-Zeiger loadc (ρ f ) codeR e ρ codeL e ρ move k for (i = k-1; i≥0; i--) S[SP+i] = S[S[SP]+i]; SP = SP+k–1; e eine Struktur der Größe k Neuer Befehl: move 312 / 398 Retten von EP und FP Aufrufen einer Funktion Der Befehl call rettet den aktuellen Wert des PC als Fortsetzungs-Adresse und setzt FP und PC. Der Befehl mark legt Platz für Rückgabewert und organisatorische Zellen an und rettet FP und EP. FP EP 313 / 398 FP EP e e FP q e p call mark S[SP+1] = EP; S[SP+2] = FP; SP = SP + 2; PC PC p q tmp = S[SP]; S[SP] = PC; FP = SP; PC = tmp; 314 / 398 315 / 398 Entfernen der Parameter beim Rücksprung Übersetzung einer Funktionsdefinition Der Befehl slide kopiert den Rückgabewert an die korrekte Stelle: Entsprechend übersetzen wir eine Funktions-Definition: code 42 t f (specs){body} _f: m slide m enter q alloc k code ss ρf return 42 wobei q max k ρf tmp = S[SP]; SP = SP-m; S[SP] = tmp; = = = = ρ = // // setzen des EP Anlegen der lokalen Variablen // Verlassen der Funktion max + k wobei maximale Länge des lokalen Kellers Platz für die lokalen Variablen Adress-Umgebung für f erzeugt aus ρ, specs und body 316 / 398 Reservierung von Speicher auf dem Stack 317 / 398 Reservierung von Speicher für lokale Variablen Der Befehl enter q setzt den EP auf den neuen Wert. Steht nicht mehr genügend Platz zur Verfügung, wird die Programm-Ausführung abgebrochen. Der Befehl alloc k reserviert auf dem Keller Platz für die lokalen Variablen. EP q k enter q alloc k EP = SP + q; if (EP ≥ NP) Error (“Stack Overflow”); SP = SP + k; 319 / 398 318 / 398 Rücksprung aus einer Funktion Überblick Funktionsaufruf Der Befehl return gibt den aktuellen Keller-Rahmen auf. D.h. er restauriert die Register PC, EP und FP und hinterlässt oben auf dem Keller den Rückgabe-Wert. PC FP EP p return PC FP EP e v Aufruf: f (e1 , . . . , en ): codeR en ρ ... codeR e1 ρ mark codeR f ρ call slide (s − 1) p e v _f: PC = S[FP]; EP = S[FP-2]; if (EP ≥ NP) Error (“Stack Overflow”); SP = FP-3; FP = S[SP+2]; 320 / 398 enter q alloc k code ss ρf return 321 / 398 Zugriff auf Variablen, Parameter u. Rückgabewerte Zugriffe auf lokale Variablen oder formale Parameter erfolgen relativ zum aktuellen FP. Darum modifizieren wir codeL für Variablen-Namen. Für Makro-Befehle zum Zugriff auf lokale Variablen Als Optimierung führt man analog zu loada j und storea j die Befehle loadr j und storer j ein: definieren wir loadc j tag = G codeL x ρ = loadrc j tag = L ρ x = (tag, j) Der Befehl loadrc j berechnet die Summe von FP und j. FP loadrc j f FP f loadr j = loadrc j load storer j = loadrc j; store Der Code für return e; entspricht einer Zuweisung an eine Variable mit Relativadresse −3. f+j code return e; ρ SP++; S[SP] = FP+j; = codeR e ρ storer -3 return 322 / 398 323 / 398 Beispiel zur Übersetzung von Funktionen Beispiel: Für die Funktion int fac(int x) { if (x<=0) return 1; else return x*fac (x-1); } Themengebiet: erzeugen wir: _fac: enter q alloc 0 loadr -3 loadc 0 leq jumpz A loadc 1 storer -3 return jump B A: loadr -3 loadr -3 loadc 1 sub mark loadc _fac call slide 0 B: Übersetzung ganzer Programme mul storer -3 return return Dabei ist ρfac : x 7→ (L, −3) und q = 1 + 1 + 3 = 5. 324 / 398 Übersetzung von Programmen 325 / 398 Instruktionen für den Programmstart Dann definieren wir: Vor der Programmausführung gilt: SP = −1 FP = EP = 0 code p ∅ PC = 0 = NP = MAX Sei p ≡ V_defs F_def1 . . . F_defn , ein Programm, wobei F_defi eine Funktion fi definiert, von denen eine main heißt. _f1 : Der Code für das Programm p enthält: Code für die Funktions-Definitionen F_defi ; _fn : Code zum Anlegen der globalen Variablen; enter (k + 4) alloc (k + 1) mark loadc _main call slide k halt code F_def1 ρ .. . code F_defn ρ Code für den Aufruf von main() wobei ∅ ρ k die Instruktion halt. 326 / 398 = b = b = b leere Adress-Umgebung; globale Adress-Umgebung; Platz für globale Variablen 327 / 398 Bemerkung zum Rückgabewerte Bemerkung zum Rückgabewerte Nach alter C Norm darf es nur einen Rückgabewert geben. Nach alter C Norm darf es nur einen Rückgabewert geben. Rückgabewert überschreibt den ersten Parameter, slide entfernt die restlichen Parameter 328 / 398 Bemerkung zum Rückgabewerte 328 / 398 Bemerkung zum Rückgabewerte Nach alter C Norm darf es nur einen Rückgabewert geben. Nach alter C Norm darf es nur einen Rückgabewert geben. Rückgabewert überschreibt den ersten Parameter, slide entfernt die restlichen Parameter Rückgabewert überschreibt den ersten Parameter, slide entfernt die restlichen Parameter Was passiert, wenn es keine Parameter gibt? Was passiert, wenn es keine Parameter gibt? ; der Rückgabewert überschreibt eine Caller-lokale Variable 328 / 398 Bemerkung zum Rückgabewerte 328 / 398 Bemerkung zum Rückgabewerte Nach alter C Norm darf es nur einen Rückgabewert geben. Nach alter C Norm darf es nur einen Rückgabewert geben. Rückgabewert überschreibt den ersten Parameter, slide entfernt die restlichen Parameter Rückgabewert überschreibt den ersten Parameter, slide entfernt die restlichen Parameter Was passiert, wenn es keine Parameter gibt? Was passiert, wenn es keine Parameter gibt? ; der Rückgabewert überschreibt eine Caller-lokale Variable ; der Rückgabewert überschreibt eine Caller-lokale Variable Lösungen: Lösungen: erzwinge, dass jede Funktion mindestens einen formalen Parameter hat erzwinge, dass jede Funktion mindestens einen formalen Parameter hat Optimierung für Funktionen ohne Rückgabewert: füge keinen zusätzlichen Parameter ein ändere nur die Übersetzung beim Caller: falls slide -1 emittiert werden soll, erzeuge stattdessen slide 0 bzw. gar nichts 328 / 398 328 / 398 Peephole Optimization Übersetzung ganzer Programme Die Erzeugten Instruktionen enthalten viele redundante Sequenzen, z.B.: alloc 0 Kapitel 1: Maschinen mit Registern slide 0 loadc 1 mul Peephole Optimierung sucht nach solchen Mustern und ersetzt sie durch einfachere (in diesem Fall keine) Instruktionen. 329 / 398 Codegenerierung für moderne Prozessoren 330 / 398 Codegenerierung für moderne Prozessoren Die C-Maschine und die JVM sind nicht sehr schnell: Die C-Maschine und die JVM sind nicht sehr schnell: jede Operation (z.B. add) bezieht ihre Argumente und ihr Resultat über den Stack (; pro Addition drei Speicherzugriffe) 331 / 398 Codegenerierung für moderne Prozessoren 331 / 398 Codegenerierung für moderne Prozessoren Die C-Maschine und die JVM sind nicht sehr schnell: Die C-Maschine und die JVM sind nicht sehr schnell: jede Operation (z.B. add) bezieht ihre Argumente und ihr Resultat über den Stack (; pro Addition drei Speicherzugriffe) jede Operation (z.B. add) bezieht ihre Argumente und ihr Resultat über den Stack (; pro Addition drei Speicherzugriffe) Speicherzugriffe sind langsam (selbst mit cache) Speicherzugriffe sind langsam (selbst mit cache) jeder Zugriff auf den Stapel berechnet FP+k 331 / 398 331 / 398 Codegenerierung für moderne Prozessoren Codegenerierung für moderne Prozessoren Die C-Maschine und die JVM sind nicht sehr schnell: Die C-Maschine und die JVM sind nicht sehr schnell: jede Operation (z.B. add) bezieht ihre Argumente und ihr Resultat über den Stack (; pro Addition drei Speicherzugriffe) jede Operation (z.B. add) bezieht ihre Argumente und ihr Resultat über den Stack (; pro Addition drei Speicherzugriffe) Speicherzugriffe sind langsam (selbst mit cache) Speicherzugriffe sind langsam (selbst mit cache) jeder Zugriff auf den Stapel berechnet FP+k jeder Zugriff auf den Stapel berechnet FP+k pipelining ist schwierig: überlappen ∗(FP+k) und ∗(FP+k0 )? pipelining ist schwierig: überlappen ∗(FP+k) und ∗(FP+k0 )? Selbst für alt Prozessoren (6502, Z80, 8086, etc.) gilt: reale Prozessoren haben mindestens ein Register ein Akkumulator Register speichert das Ergebnis 331 / 398 331 / 398 Codegenerierung für moderne Prozessoren Moderne Register-Maschinen Entwicklung von Register-Maschinen: Die C-Maschine und die JVM sind nicht sehr schnell: jede Operation (z.B. add) bezieht ihre Argumente und ihr Resultat über den Stack (; pro Addition drei Speicherzugriffe) Speicherzugriffe sind langsam (selbst mit cache) jeder Zugriff auf den Stapel berechnet FP+k pipelining ist schwierig: überlappen ∗(FP+k) und ∗(FP+k0 )? Selbst für alt Prozessoren (6502, Z80, 8086, etc.) gilt: reale Prozessoren haben mindestens ein Register ein Akkumulator Register speichert das Ergebnis eine Berechnung wie x=y+1-x ist billiger das Zwischenergebnis von 1-x muss nicht auf den Stapel geschrieben werden die Konstante 1 kann muss nicht auf den Stapel geschrieben werden 332 / 398 331 / 398 Moderne Register-Maschinen Moderne Register-Maschinen Entwicklung von Register-Maschinen: Anzahl der Register war begrenzt durch Anzahl der Transistoren: Entwicklung von Register-Maschinen: Anzahl der Register war begrenzt durch Anzahl der Transistoren: nur wenige Register möglich Register hatten spezielle Funktionen, z.B. AX: Akkumulator, BX: zweites Argument/Resultat, CX: Zähler, DX: Datenzeiger mögliche Kontexte von Registern beschränkt (z.B. mul schreibt Ergebnis immer in AX:BX) nur wenige Register möglich Register hatten spezielle Funktionen, z.B. AX: Akkumulator, BX: zweites Argument/Resultat, CX: Zähler, DX: Datenzeiger mögliche Kontexte von Registern beschränkt (z.B. mul schreibt Ergebnis immer in AX:BX) Anzahl der Register ist heute begrenzt durch Instruktionslänge jede Instruktion trägt Bits um Quell- und Zielregister auszuwählen jedes Register kann universell eingesetzt werden 332 / 398 332 / 398 Moderne Register-Maschinen Moderne Register-Maschinen Entwicklung von Register-Maschinen: Anzahl der Register war begrenzt durch Anzahl der Transistoren: Entwicklung von Register-Maschinen: Anzahl der Register war begrenzt durch Anzahl der Transistoren: nur wenige Register möglich Register hatten spezielle Funktionen, z.B. AX: Akkumulator, BX: zweites Argument/Resultat, CX: Zähler, DX: Datenzeiger mögliche Kontexte von Registern beschränkt (z.B. mul schreibt Ergebnis immer in AX:BX) nur wenige Register möglich Register hatten spezielle Funktionen, z.B. AX: Akkumulator, BX: zweites Argument/Resultat, CX: Zähler, DX: Datenzeiger mögliche Kontexte von Registern beschränkt (z.B. mul schreibt Ergebnis immer in AX:BX) Anzahl der Register ist heute begrenzt durch Instruktionslänge Anzahl der Register ist heute begrenzt durch Instruktionslänge jede Instruktion trägt Bits um Quell- und Zielregister auszuwählen jedes Register kann universell eingesetzt werden jede Instruktion trägt Bits um Quell- und Zielregister auszuwählen jedes Register kann universell eingesetzt werden Die wichtigsten Aufgaben: früher: bilde Programmkonstrukte auf komplexe Instruktionen ab Die wichtigsten Aufgaben: früher: bilde Programmkonstrukte auf komplexe Instruktionen ab heute: halte möglichst viele Werte gleichzeitig in Registern heute: halte möglichst viele Werte gleichzeitig in Registern Semantik: Benutze M[·] für Speicherzugriff, Ri für Register, + für Addition (infix), := für Zuweisung, etc. 332 / 398 Instruktionsselektion: Übersicht 332 / 398 Adressierung im MC68000 Beispiel: Motorola MC68000 Dieser einfachste Prozessor der 680x0-Reihe besitzt Der MC68000 ist eine 2-Adress-Maschine, d.h. ein Befehl darf maximal 2 Argumente enthalten. Die Instruktion: 8 Daten- und 8 Adressregister; eine Vielzahl von Adressierungsarten Notation Dn An (An ) d(An ) d(An , Dm ) x x #x Beschreibung Datenregister direkt Adressregister direkt Adressregister indirekt Adressregister indirekt mit Displacement Adressregister indirekt mit Index und Displacement Absolut kurz Absolut lang Unmittelbar add D1 D2 Semantik Dn An M[An ] M[An + d] addiert die Inhalte von D1 und D2 und speichert das Ergebnis nach und D2 Die meisten Befehle lassen sich auf Bytes, Wörter (2 Bytes) oder Doppelwörter (4 Bytes) anwenden. Das unterscheiden wir durch Anhängen von .B, .W, .D an das Mnemonic (ohne Suffix: Wort) M[An + Dm + d] Die Ausführungszeit eines Befehls ergibt sich (i.a.) aus den Kosten der Operation plus den Kosten für die Adressierung der Operanden M[x] M[x] x 333 / 398 Kosten der Adressierung im MC68000 Kosten der Adressierung im MC68000 Eine add Instruktion benötigt 4 Zykel. Argumente verlängern diese Zeit wie folgt: Dn An (An ) d(An ) d(An , Dm ) x x #x Adressierungsart Datenregister direkt Adressregister direkt Adressregister indirekt Adressregister indirekt mit Displacement Adressregister indirekt mit Index und Displacement Absolut kurz Absolut lang Unmittelbar 334 / 398 Eine add Instruktion benötigt 4 Zykel. Argumente verlängern diese Zeit wie folgt: .B, .W 0 0 4 8 .D 0 0 8 12 Dn An (An ) d(An ) 10 14 d(An , Dm ) 8 12 4 12 16 8 x x #x Adressierungsart Datenregister direkt Adressregister direkt Adressregister indirekt Adressregister indirekt mit Displacement Adressregister indirekt mit Index und Displacement Absolut kurz Absolut lang Unmittelbar .B, .W 0 0 4 8 .D 0 0 8 12 10 14 8 12 4 12 16 8 ; intelligente Auswahl der Argumente können viele move Instruktionen und Zwischenergebnisse überflüssig machen. 335 / 398 335 / 398 Beispiel Instruktionsselektion Komplexe Instruktionen Betrachte die Kosten als Summe von Operation, 1. Argument und 2. Argument: o + a1 + a2 Die Instruktion benötigt: die verschieden Code-Sequenzen sind im Hinblick auf den Speicher und das Ergebnis äquivalent move.B 8(A1 , D1 .W), D5 4 + 10 + 0 = 14 Zykel sie unterscheiden sich im Hinblick auf den Wert des Registers A1 sowie die gesetzten Bedingungs-Codes ein konkreter Algorithmus muss solche Randbedingungen berücksichtigen Alternativ könnten wir erzeugen: adda adda move.B mit Gesamtkosten 32 adda move.B mit Gesamtkosten int b, i, a[100]; b = 2 + a[i]; Kosten: 8 + 8 + 0 = 16 Kosten: 8 + 0 + 0 = 8 Kosten: 4 + 4 + 0 = 8 #8, A1 D1 .W, A1 (A1 ), D5 Nehmen wir an, die Variablen werden relativ zu einem Framepointer A5 mit den Adressen −4, −6, −206 adressiert. Dann entspricht der Zuweisung das Stück Zwischen-Code: oder: Kosten: 8 + 0 + 0 = 8 Kosten: 4 + 8 + 0 = 12 D1 .W, A1 8(A1 ), D5 M[A5 − 4] = 2 + M[A5 − 206 + 2 · M[A5 − 6]]; 20 336 / 398 Instruktionsselektion für Anweisung 337 / 398 Instruktionsselektion für Anweisung Betrachte die Übersetzung von: Betrachte die Übersetzung von: M[A5 − 4] = 2 + M[A5 − 206 + 2 · M[A5 − 6]]; M[A5 − 4] = 2 + M[A5 − 206 + 2 · M[A5 − 6]]; Mögliche Auswahl an Instruktionen: move Der Ausdruck entspricht dem Syntaxbaum: = = M + 2 + A5 −6(A5 ), D1 M + M 2 + −4 + A5 A5 −4 + ∗ + −206 2 D1 M ∗ + M −206 A5 2 D1 + A5 M + −6 A5 −6 338 / 398 Instruktionsselektion für Anweisung 338 / 398 Instruktionsselektion für Anweisung Betrachte die Übersetzung von: Betrachte die Übersetzung von: M[A5 − 4] = 2 + M[A5 − 206 + 2 · M[A5 − 6]]; M[A5 − 4] = 2 + M[A5 − 206 + 2 · M[A5 − 6]]; Mögliche Auswahl an Instruktionen: add D1 , D1 Mögliche Auswahl an Instruktionen: move = = M M + 2 + A5 −206(A5 , D1 ), D2 M −4 + A5 + D1 + A5 −206 + −4 D1 D1 + A5 M M + ∗ 2 D2 2 −206 ∗ 2 D1 + A5 M + −6 A5 338 / 398 −6 338 / 398 Instruktionsselektion für Anweisung Instruktionsselektion für Anweisung Betrachte die Übersetzung von: Betrachte die Übersetzung von: M[A5 − 4] = 2 + M[A5 − 206 + 2 · M[A5 − 6]]; M[A5 − 4] = 2 + M[A5 − 206 + 2 · M[A5 − 6]]; Mögliche Auswahl an Instruktionen: addq #2, D2 Mögliche Auswahl an Instruktionen: move = = D2 M D2 −4 M A5 D1 A5 −206 D1 D2 M −4 + ∗ 2 + 2 + + + D2 M + 2 + A5 D2 , −4(A5 ) D1 + A5 M −206 ∗ 2 D1 M + A5 + −6 A5 −6 338 / 398 Mögliche Code-Sequenzen 338 / 398 Alternative Code-Sequenz Eine längere Code-Sequenz: move.L adda.L move mulu move.L adda.L adda.L move addq move.L adda.L move Im Beispiel: move add move addq move −6(A5 ), D1 D1 , D1 −206(A5 , D1 ), D2 #2, D2 D2 , −4(A5 ) Kosten: 12 Kosten: 4 Kosten: 14 Kosten: 4 Kosten: 12 Gesamtkosten : 46 A5 , A1 #−6, A1 (A1 ), D1 #2, D1 A5 , A2 #−206, A2 D1 , A2 (A2 ), D2 #2, D2 A5 , A3 #−4, A3 D2 , (A3 ) Kosten: Kosten: Kosten: Kosten: Kosten: Kosten: Kosten: Kosten: Kosten: Kosten: Kosten: Kosten: Gesamtkosten : 4 12 8 44 4 12 8 8 4 4 12 8 124 339 / 398 340 / 398 Kriterien zur Instruktionsselektion Kriterien zur Instruktionsselektion Komplexe Instruktionen sind i.A. vorteilhaft: Komplexe Instruktionen sind i.A. vorteilhaft: komplexere Adressierungsarten können den Code erheblich schneller und kürzer machen komplexere Adressierungsarten können den Code erheblich schneller und kürzer machen einfachere Befehlssequenzen benötigen mehr Hilfsregister einfachere Befehlssequenzen benötigen mehr Hilfsregister die komplexere Folge überschreibt auch weniger Hilfsregister die komplexere Folge überschreibt auch weniger Hilfsregister ; finde die schnellste Folge von Instruktionen für einen Syntaxbaum 341 / 398 341 / 398 Kriterien zur Instruktionsselektion Kriterien zur Instruktionsselektion Komplexe Instruktionen sind i.A. vorteilhaft: Komplexe Instruktionen sind i.A. vorteilhaft: komplexere Adressierungsarten können den Code erheblich schneller und kürzer machen komplexere Adressierungsarten können den Code erheblich schneller und kürzer machen einfachere Befehlssequenzen benötigen mehr Hilfsregister einfachere Befehlssequenzen benötigen mehr Hilfsregister die komplexere Folge überschreibt auch weniger Hilfsregister die komplexere Folge überschreibt auch weniger Hilfsregister ; finde die schnellste Folge von Instruktionen für einen Syntaxbaum ; finde die schnellste Folge von Instruktionen für einen Syntaxbaum Idee: Berechne alle möglichen Instruktionsfolgen, berechne deren Kosten und wähle dann die beste aus. beschreibe alle erlaubten Instruktionen mit einer Grammatik: Idee: Berechne alle möglichen Instruktionsfolgen, berechne deren Kosten und wähle dann die beste aus. beschreibe alle erlaubten Instruktionen mit einer Grammatik: 1 2 3 verfügbare Registerklassen: Nichtterminale Operatoren und Konstantenklassen: Terminale Instruktionen: Regeln 1 2 3 die Grammatik ist mehrdeutig, da viele Ableitungen den gleichen Baum beschreiben verfügbare Registerklassen: Nichtterminale Operatoren und Konstantenklassen: Terminale Instruktionen: Regeln die Grammatik ist mehrdeutig, da viele Ableitungen den gleichen Baum beschreiben wähle die billigste aller Ableitungen, die zu einem Baum führen wähle die billigste aller Ableitungen, die zu einem Baum führen ; inferiere alle möglichen Ableitungen durch tree parsing 341 / 398 Beispiel Grammtik für Instruktionen Beachte: Loads : D → M[A] D → M[A + A] Computations : D→c D→D+D 341 / 398 Beispiel für eine Ableitung der Grammatik Loads : D → M[A] D → M[A + A] Moves : D→A A →D Beispiel Instruktion: M[A + c] Computations : D→c D→D+D Moves : D→A A →D D zwei Registerklassen D (Data) und A (Address) wie vorher Arithmetik wird nur für Daten unterstützt Laden nur von Adressen in Addressregistern Zwischen Daten- und Adressregistern gibt es moves 342 / 398 Beispiel für eine Ableitung der Grammatik Loads : D → M[A] D → M[A + A] Beispiel Instruktion: M[A + c] Computations : D→c D→D+D 343 / 398 Beispiel für eine Ableitung der Grammatik Moves : D→A A →D Loads : D → M[A] D → M[A + A] Beispiel Instruktion: M[A + c] M M A D 343 / 398 Computations : D→c D→D+D Moves : D→A A →D 343 / 398 Beispiel für eine Ableitung der Grammatik Loads : D → M[A] D → M[A + A] Beispiel Instruktion: M[A + c] Computations : D→c D→D+D Beispiel für eine Ableitung der Grammatik Moves : D→A A →D Loads : D → M[A] D → M[A + A] Beispiel Instruktion: M[A + c] M M + + D D A Computations : D→c D→D+D Moves : D→A A →D D 343 / 398 Beispiel für eine Ableitung der Grammatik Loads : D → M[A] D → M[A + A] Beispiel Instruktion: M[A + c] A Computations : D→c D→D+D 343 / 398 Beispiel für eine Ableitung der Grammatik Moves : D→A A →D Loads : D → M[A] D → M[A + A] Beispiel Instruktion: M[A + c] M M + + c A Computations : D→c D→D+D Moves : D→A A →D c Alternativ wäre auch eine Ableitung mit der Regel D → M[A + A] möglich. 343 / 398 Instruktionsselektion durch tree-parsing 343 / 398 Instruktionsselektion durch tree-parsing Idee: Berechnung von möglichen Ableitungen zu einem Baum durch tree parsing: Idee: Berechnung von möglichen Ableitungen zu einem Baum durch tree parsing: berechne mögliche Ableitungen bottom-up, beginnend von den Blättern: berechne mögliche Ableitungen bottom-up, beginnend von den Blättern: prüfe, welche rechte Regelseite passt wende diese Regel rückwärts an füge linke Regelseite zum Vorgänger im Baum hinzu 344 / 398 344 / 398 Instruktionsselektion durch tree-parsing Instruktionsselektion durch tree-parsing Idee: Berechnung von möglichen Ableitungen zu einem Baum durch tree parsing: Idee: Berechnung von möglichen Ableitungen zu einem Baum durch tree parsing: berechne mögliche Ableitungen bottom-up, beginnend von den Blättern: berechne mögliche Ableitungen bottom-up, beginnend von den Blättern: prüfe, welche rechte Regelseite passt prüfe, welche rechte Regelseite passt wende diese Regel rückwärts an wende diese Regel rückwärts an füge linke Regelseite zum Vorgänger im Baum hinzu füge linke Regelseite zum Vorgänger im Baum hinzu oben angekommen, wähle die Ableitung mit den geringsten Kosten oben angekommen, wähle die Ableitung mit den geringsten Kosten steige hinab und emittiere Instruktionen für jede angewandte Regel steige hinab und emittiere Instruktionen für jede angewandte Regel Algorithmus ähnlich zu Cocke-Younger-Kasami (CYK)-Algorithmus zum Test ob eine Eingabe s in L(G) für G ∈ CFG. 344 / 398 Beispiel für Instruktionsselektion Loads : D → M[A] D → M[A + A] Computations : D→c D→D+D 344 / 398 Beispiel für Instruktionsselektion Moves : D→A A →D Loads : D → M[A] D → M[A + A] Betrachte: M[A + c] Computations : D→c D→D+D Moves : D→A A →D Betrachte: M[A + c] M M + A A0 , D1 c + A c 345 / 398 Beispiel für Instruktionsselektion Loads : D → M[A] D → M[A + A] Computations : D→c D→D+D 345 / 398 Beispiel für Instruktionsselektion Moves : D→A A →D Loads : D → M[A] D → M[A + A] Betrachte: M[A + c] Computations : D→c D→D+D Moves : D→A A →D Betrachte: M[A + c] M M A4 , D3 , A + A2 A0 , D1 A + A2 , D1 A0 , D1 c A 345 / 398 + A2 , D1 c 345 / 398 Beispiel für Instruktionsselektion Loads : D → M[A] D → M[A + A] Computations : D→c D→D+D Beispiel für Instruktionsselektion Moves : D→A A →D Loads : D → M[A] D → M[A + A] Betrachte: M[A + c] Computations : D→c D→D+D Moves : D→A A →D Betrachte: M[A + c] A6 , D5 A6 , D5 M 0 M A4 , D3 , A + A2 + A0 , D1 A4 , D3 , A + A2 A2 , D1 A + A0 , D1 c A A2 , D1 c 345 / 398 345 / 398 Beispiel für Instruktionsselektion Loads : D → M[A] D → M[A + A] Computations : D→c D→D+D Beispiel für Instruktionsselektion Moves : D→A A →D Loads : D → M[A] D → M[A + A] Betrachte: M[A + c] Computations : D→c D→D+D Moves : D→A A →D Betrachte: M[A + c] A6 , D5 0 A6 , D5 M 4 3 A ,D , A + A A0 , D1 A + 2 0 M A4 , D3 , A + A2 A2 , D1 A0 , D1 c A + A2 5,3 , D1 c 345 / 398 Praktische Umsetzung 345 / 398 Praktische Umsetzung Instruktionsselektion durch Anwendung aller möglichen Regeln: Instruktionsselektion durch Anwendung aller möglichen Regeln: kann nach verschiedene Kriterien optimieren (Laufzeit, Code-Größe, Parallelisierbarkeit, Stromverbrauch) kann nach verschiedene Kriterien optimieren (Laufzeit, Code-Größe, Parallelisierbarkeit, Stromverbrauch) muss schnell durchgeführt werden übersetze die Anwendung der Regeln in einen endlichen, deterministischen Baum-Automaten 346 / 398 346 / 398 Praktische Umsetzung Praktische Umsetzung Instruktionsselektion durch Anwendung aller möglichen Regeln: Instruktionsselektion durch Anwendung aller möglichen Regeln: kann nach verschiedene Kriterien optimieren (Laufzeit, Code-Größe, Parallelisierbarkeit, Stromverbrauch) muss schnell durchgeführt werden kann nach verschiedene Kriterien optimieren (Laufzeit, Code-Größe, Parallelisierbarkeit, Stromverbrauch) muss schnell durchgeführt werden übersetze die Anwendung der Regeln in einen endlichen, deterministischen Baum-Automaten übersetze die Anwendung der Regeln in einen endlichen, deterministischen Baum-Automaten funktioniert nur wenn jede Operation nur ein Ergebnis hat funktioniert nur wenn jede Operation nur ein Ergebnis hat Wir verzichten auf eine ausführliche Behandlung: 346 / 398 Praktische Umsetzung 346 / 398 Praktische Umsetzung Instruktionsselektion durch Anwendung aller möglichen Regeln: Instruktionsselektion durch Anwendung aller möglichen Regeln: kann nach verschiedene Kriterien optimieren (Laufzeit, Code-Größe, Parallelisierbarkeit, Stromverbrauch) muss schnell durchgeführt werden kann nach verschiedene Kriterien optimieren (Laufzeit, Code-Größe, Parallelisierbarkeit, Stromverbrauch) muss schnell durchgeführt werden übersetze die Anwendung der Regeln in einen endlichen, deterministischen Baum-Automaten übersetze die Anwendung der Regeln in einen endlichen, deterministischen Baum-Automaten funktioniert nur wenn jede Operation nur ein Ergebnis hat funktioniert nur wenn jede Operation nur ein Ergebnis hat Wir verzichten auf eine ausführliche Behandlung: Wir verzichten auf eine ausführliche Behandlung: auf modernen Prozessoren führen komplexe Instruktionen (wenn sie existieren) oft zu stalls in der Pipeline auf modernen Prozessoren führen komplexe Instruktionen (wenn sie existieren) oft zu stalls in der Pipeline effiziente komplexe Instruktionen wie SIMD sind nicht durch Instruktionsselektion erzeugbar 346 / 398 Praktische Umsetzung 346 / 398 Praktische Umsetzung Instruktionsselektion durch Anwendung aller möglichen Regeln: Instruktionsselektion durch Anwendung aller möglichen Regeln: kann nach verschiedene Kriterien optimieren (Laufzeit, Code-Größe, Parallelisierbarkeit, Stromverbrauch) muss schnell durchgeführt werden kann nach verschiedene Kriterien optimieren (Laufzeit, Code-Größe, Parallelisierbarkeit, Stromverbrauch) muss schnell durchgeführt werden übersetze die Anwendung der Regeln in einen endlichen, deterministischen Baum-Automaten übersetze die Anwendung der Regeln in einen endlichen, deterministischen Baum-Automaten funktioniert nur wenn jede Operation nur ein Ergebnis hat funktioniert nur wenn jede Operation nur ein Ergebnis hat Wir verzichten auf eine ausführliche Behandlung: Wir verzichten auf eine ausführliche Behandlung: auf modernen Prozessoren führen komplexe Instruktionen (wenn sie existieren) oft zu stalls in der Pipeline auf modernen Prozessoren führen komplexe Instruktionen (wenn sie existieren) oft zu stalls in der Pipeline effiziente komplexe Instruktionen wie SIMD sind nicht durch Instruktionsselektion erzeugbar moderne Prozessoren haben Universalregister effiziente komplexe Instruktionen wie SIMD sind nicht durch Instruktionsselektion erzeugbar moderne Prozessoren haben Universalregister die Anwendbarkeit von Instruktionen hängt nicht von ihren Argumenten ab Verbesserung des Codes können auch auf den emittierten Funktionen geschehen die Anwendbarkeit von Instruktionen hängt nicht von ihren Argumenten ab Verbesserung des Codes können auch auf den emittierten Funktionen geschehen ; generiere nur einfachen Register-Code und optimiere diesen 346 / 398 346 / 398 Motivation für die Register C-Maschine Übersetzung ganzer Programme Ein moderner RISC-Prozessor besitzt eine fixe Anzahl von Universalregistern. Kapitel 2: Codeerzeugung für die Register-C-Maschine 347 / 398 Motivation für die Register C-Maschine 348 / 398 Motivation für die Register C-Maschine Ein moderner RISC-Prozessor besitzt eine fixe Anzahl von Universalregistern. Ein moderner RISC-Prozessor besitzt eine fixe Anzahl von Universalregistern. arithmetische Operationen können oft nur auf diesen Registern ausgeführt werden arithmetische Operationen können oft nur auf diesen Registern ausgeführt werden Zugriffe auf den Speicher werden durch Adressen in Registern vorgenommen Zugriffe auf den Speicher werden durch Adressen in Registern vorgenommen Register müssen gesichert werden, wenn Funktionen aufgerufen werden Register müssen gesichert werden, wenn Funktionen aufgerufen werden Die Übersetzung von Code für einen solchen Prozessor muss: 348 / 398 Motivation für die Register C-Maschine Motivation für die Register C-Maschine Ein moderner RISC-Prozessor besitzt eine fixe Anzahl von Universalregistern. Ein moderner RISC-Prozessor besitzt eine fixe Anzahl von Universalregistern. arithmetische Operationen können oft nur auf diesen Registern ausgeführt werden arithmetische Operationen können oft nur auf diesen Registern ausgeführt werden Zugriffe auf den Speicher werden durch Adressen in Registern vorgenommen Zugriffe auf den Speicher werden durch Adressen in Registern vorgenommen Register müssen gesichert werden, wenn Funktionen aufgerufen werden Register müssen gesichert werden, wenn Funktionen aufgerufen werden Die Übersetzung von Code für einen solchen Prozessor muss: 1 Variablen und Funktionsargumente in Registern speichern 2 3 348 / 398 Die Übersetzung von Code für einen solchen Prozessor muss: 1 Variablen und Funktionsargumente in Registern speichern während eines Funktionsaufrufes die entsprechenden Register auf dem Keller sichern 2 beliebige Berechnungen mittels endlich vieler Register ausdrücken 3 während eines Funktionsaufrufes die entsprechenden Register auf dem Keller sichern beliebige Berechnungen mittels endlich vieler Register ausdrücken ; betrachte nur die ersten zwei Punkte (und behandle den letzten Punkt als separates Problem) 348 / 398 348 / 398 Prinzip der Register C-Maschine Die Registersätze der R-CMa Die R-CMa besitzt einen Stack, ein Code- und Heap-Segment wie die CMa, und zusätzlich zwei unbegrenzte Mengen an Registern. lokale Register sind R1 , R2 , . . . Ri , . . . globale Register sind R0 , R−1 , . . . Rj , . . . Die zwei Registersätze haben die folgenden Aufgaben: 1 0 1 PC 0 SP die lokalen Register Ri speichern lokale Variablen einer Funktion speichern temporäre Zwischenergebnisse können effizient auf den Stack gerettet werden C S Rloc R1 R6 Rglob R0 R−4 350 / 398 349 / 398 Die Registersätze der R-CMa Die Registersätze der R-CMa Die zwei Registersätze haben die folgenden Aufgaben: 1 Die zwei Registersätze haben die folgenden Aufgaben: die lokalen Register Ri 1 speichern lokale Variablen einer Funktion speichern temporäre Zwischenergebnisse können effizient auf den Stack gerettet werden 2 die lokalen Register Ri speichern lokale Variablen einer Funktion speichern temporäre Zwischenergebnisse können effizient auf den Stack gerettet werden die globalen Register Ri 2 speichern Parameter einer Funktion speichern den Rückgabewert einer Funktion die globalen Register Ri speichern Parameter einer Funktion speichern den Rückgabewert einer Funktion ; zur Verwaltung erweitern wir die Adressumgebung ρ 350 / 398 350 / 398 Adressumgebung Adressumgebung Eine Variable (global, lokal oder Parameter) kann in einem von vier verschiedenen Bereichen existieren. 1 Global: eine Variable ist global 2 Lokal: eine Variable liegt auf dem aktuellen Kellerrahmen 3 RG: eine Variable ist in einem globalen Register gespeichert 4 RL: eine Variable ist in einem lokalen Register gespeichert Eine Variable (global, lokal oder Parameter) kann in einem von vier verschiedenen Bereichen existieren. 1 Global: eine Variable ist global 2 Lokal: eine Variable liegt auf dem aktuellen Kellerrahmen 3 RG: eine Variable ist in einem globalen Register gespeichert 4 RL: eine Variable ist in einem lokalen Register gespeichert Entsprechend definieren wir ρ : Var → {G, L, RG, RL} × N wie folgt: ρ x = hG, ai: Variable x ist an absoluter Adresse a gespeichert ρ x = hL, ai: Variable x ist an Adresse FP + a gespeichert ρ x = hRG, ai: Variable x ist im globalen Register Ra gespeichert ρ x = hRL, ai: Variable x ist im lokalen Register Ra gespeichert 351 / 398 351 / 398 Adressumgebung Adressumgebung Eine Variable (global, lokal oder Parameter) kann in einem von vier verschiedenen Bereichen existieren. 1 Global: eine Variable ist global 2 Lokal: eine Variable liegt auf dem aktuellen Kellerrahmen 3 RG: eine Variable ist in einem globalen Register gespeichert 4 RL: eine Variable ist in einem lokalen Register gespeichert Eine Variable (global, lokal oder Parameter) kann in einem von vier verschiedenen Bereichen existieren. 1 Global: eine Variable ist global 2 Lokal: eine Variable liegt auf dem aktuellen Kellerrahmen 3 RG: eine Variable ist in einem globalen Register gespeichert 4 RL: eine Variable ist in einem lokalen Register gespeichert Entsprechend definieren wir ρ : Var → {G, L, RG, RL} × N wie folgt: ρ x = hG, ai: Variable x ist an absoluter Adresse a gespeichert ρ x = hL, ai: Variable x ist an Adresse FP + a gespeichert ρ x = hRG, ai: Variable x ist im globalen Register Ra gespeichert ρ x = hRL, ai: Variable x ist im lokalen Register Ra gespeichert Entsprechend definieren wir ρ : Var → {G, L, RG, RL} × N wie folgt: ρ x = hG, ai: Variable x ist an absoluter Adresse a gespeichert ρ x = hL, ai: Variable x ist an Adresse FP + a gespeichert ρ x = hRG, ai: Variable x ist im globalen Register Ra gespeichert ρ x = hRL, ai: Variable x ist im lokalen Register Ra gespeichert Beachte: eine Variable x kann nur einen Eintrag in ρ haben. Allerdings: Beachte: eine Variable x kann nur einen Eintrag in ρ haben. Allerdings: ρ kann unterschiedlich sein an verschiedenen Programmpunkten 351 / 398 Adressumgebung 351 / 398 Adressumgebung Eine Variable (global, lokal oder Parameter) kann in einem von vier verschiedenen Bereichen existieren. 1 Global: eine Variable ist global 2 Lokal: eine Variable liegt auf dem aktuellen Kellerrahmen 3 RG: eine Variable ist in einem globalen Register gespeichert 4 RL: eine Variable ist in einem lokalen Register gespeichert Eine Variable (global, lokal oder Parameter) kann in einem von vier verschiedenen Bereichen existieren. 1 Global: eine Variable ist global 2 Lokal: eine Variable liegt auf dem aktuellen Kellerrahmen 3 RG: eine Variable ist in einem globalen Register gespeichert 4 RL: eine Variable ist in einem lokalen Register gespeichert Entsprechend definieren wir ρ : Var → {G, L, RG, RL} × N wie folgt: ρ x = hG, ai: Variable x ist an absoluter Adresse a gespeichert ρ x = hL, ai: Variable x ist an Adresse FP + a gespeichert ρ x = hRG, ai: Variable x ist im globalen Register Ra gespeichert ρ x = hRL, ai: Variable x ist im lokalen Register Ra gespeichert Entsprechend definieren wir ρ : Var → {G, L, RG, RL} × N wie folgt: ρ x = hG, ai: Variable x ist an absoluter Adresse a gespeichert ρ x = hL, ai: Variable x ist an Adresse FP + a gespeichert ρ x = hRG, ai: Variable x ist im globalen Register Ra gespeichert ρ x = hRL, ai: Variable x ist im lokalen Register Ra gespeichert Beachte: eine Variable x kann nur einen Eintrag in ρ haben. Allerdings: ρ kann unterschiedlich sein an verschiedenen Programmpunkten d.h. x kann einem Register zugewiesen sein Beachte: eine Variable x kann nur einen Eintrag in ρ haben. Allerdings: ρ kann unterschiedlich sein an verschiedenen Programmpunkten d.h. x kann einem Register zugewiesen sein und an einem anderen Punkt einer Speicherzelle 351 / 398 Vergeben von Adressen an Variablen 351 / 398 Vergeben von Adressen an Variablen Vor der Codeerzeugung müssen jeder Variable eine Speicherzelle zugewiesen werden Vor der Codeerzeugung müssen jeder Variable eine Speicherzelle zugewiesen werden auf der R-CMa existieren genug Register für alle Variablen auf der R-CMa existieren genug Register für alle Variablen Felder (arrays) und dynamische Strukturen bleiben aber weiterhin im Speicher Felder (arrays) und dynamische Strukturen bleiben aber weiterhin im Speicher Variablen, deren Adresse genommen wird, müssen auch im Speicher bleiben 352 / 398 352 / 398 Vergeben von Adressen an Variablen Vergeben von Adressen an Variablen Vor der Codeerzeugung müssen jeder Variable eine Speicherzelle zugewiesen werden Vor der Codeerzeugung müssen jeder Variable eine Speicherzelle zugewiesen werden auf der R-CMa existieren genug Register für alle Variablen auf der R-CMa existieren genug Register für alle Variablen Felder (arrays) und dynamische Strukturen bleiben aber weiterhin im Speicher Variablen, deren Adresse genommen wird, müssen auch im Speicher bleiben Felder (arrays) und dynamische Strukturen bleiben aber weiterhin im Speicher Variablen, deren Adresse genommen wird, müssen auch im Speicher bleiben Register haben keine Adresse Register haben keine Adresse der Code p=&x kann nicht übersetzt werden, wenn x einem Register zugewiesen ist 352 / 398 Vergeben von Adressen an Variablen 352 / 398 Vergeben von Adressen an Variablen Vor der Codeerzeugung müssen jeder Variable eine Speicherzelle zugewiesen werden Vor der Codeerzeugung müssen jeder Variable eine Speicherzelle zugewiesen werden auf der R-CMa existieren genug Register für alle Variablen auf der R-CMa existieren genug Register für alle Variablen Felder (arrays) und dynamische Strukturen bleiben aber weiterhin im Speicher Variablen, deren Adresse genommen wird, müssen auch im Speicher bleiben Felder (arrays) und dynamische Strukturen bleiben aber weiterhin im Speicher Variablen, deren Adresse genommen wird, müssen auch im Speicher bleiben Register haben keine Adresse Register haben keine Adresse der Code p=&x kann nicht übersetzt werden, wenn x einem Register zugewiesen ist der Code p=&x kann nicht übersetzt werden, wenn x einem Register zugewiesen ist ermittle von welchen Variablen die Adresse genommen wurde weise diesen Variablen eine Speicherstelle auf dem Stack zu ermittle von welchen Variablen die Adresse genommen wurde weise diesen Variablen eine Speicherstelle auf dem Stack zu globale Variablen sind im Speicher angesiedelt 352 / 398 352 / 398 Vergeben von Adressen an Variablen Codeerzeugung für die R-CMa Funktionen zur Codeerzeugung: Vor der Codeerzeugung müssen jeder Variable eine Speicherzelle zugewiesen werden Sei s eine Anweisung und e ein Ausdruck. Definiere die folgenden drei Funktionen: auf der R-CMa existieren genug Register für alle Variablen codei s ρ: generiert Code für Anweisung s Felder (arrays) und dynamische Strukturen bleiben aber weiterhin im Speicher Variablen, deren Adresse genommen wird, müssen auch im Speicher bleiben codeiR e ρ: generiere Code der den R-Wert e nach Ri schreibt codeiL e ρ: generiere Code der den L-Wert e nach Ri schreibt Register haben keine Adresse der Code p=&x kann nicht übersetzt werden, wenn x einem Register zugewiesen ist ermittle von welchen Variablen die Adresse genommen wurde weise diesen Variablen eine Speicherstelle auf dem Stack zu globale Variablen sind im Speicher angesiedelt möglicherweise können globale Variablen temporär in lokalen Registern gehalten werden 352 / 398 353 / 398 Codeerzeugung für die R-CMa Codeerzeugung für die R-CMa Funktionen zur Codeerzeugung: Funktionen zur Codeerzeugung: Sei s eine Anweisung und e ein Ausdruck. Definiere die folgenden drei Funktionen: Sei s eine Anweisung und e ein Ausdruck. Definiere die folgenden drei Funktionen: codei s ρ: generiert Code für Anweisung s codei s ρ: generiert Code für Anweisung s codeiR e ρ: generiere Code der den R-Wert e nach Ri schreibt codeiR e ρ: generiere Code der den R-Wert e nach Ri schreibt codeiL codeiL e ρ: generiere Code der den L-Wert e nach Ri schreibt e ρ: generiere Code der den L-Wert e nach Ri schreibt wobei wobei i gibt das zuletzt benutzte Register an, i + 1, i + 2, . . . sind unbenutzt. i gibt das zuletzt benutzte Register an, i + 1, i + 2, . . . sind unbenutzt. Das Ergebnis von codeiR und codeiL wird immer in das temporäre Register Ri gespeichert. Das Ergebnis von codeiR und codeiL wird immer in das temporäre Register Ri gespeichert. Beachte: Es ist nicht möglich Code zu erzeugen, der direkt in ein Register k < i schreibt. ; Der erzeugte Code hat braucht mehr Register als notwendig. 353 / 398 Codeerzeugung für die R-CMa 353 / 398 Übersetzung von Ausdrücken Funktionen zur Codeerzeugung: Sei folgende Funktion gegeben: Sei s eine Anweisung und e ein Ausdruck. Definiere die folgenden drei Funktionen: codei s ρ: generiert Code für Anweisung s void f(void) { int x,y,z; x = y+3*z; } Ordne den Variablen die ersten drei Register zu: codeiR e ρ: generiere Code der den R-Wert e nach Ri schreibt codeiL e ρ: generiere Code der den L-Wert e nach Ri schreibt ρ = {x 7→ hRL, 1i, y 7→ hRL, 2i, z 7→ hRL, 3i} wobei Setzte t = 4 da R4 das erste freie Register ist. Erzeuge i gibt das zuletzt benutzte Register an, i + 1, i + 2, . . . sind unbenutzt. code4 x=y+3*z ρ Das Ergebnis von codeiR und codeiL wird immer in das temporäre Register Ri gespeichert. = code4R y+3*z ρ move R1 R4 Beachte: Es ist nicht möglich Code zu erzeugen, der direkt in ein Register k < i schreibt. ; Der erzeugte Code hat braucht mehr Register als notwendig. Die Beseitigung von temporären Registern überlassen wir späteren Optimierungen (i.e. Registerallokation) 354 / 398 353 / 398 Übersetzung von Ausdrücken Sei folgende Funktion gegeben: Übersetzung von Ausdrücken void f(void) { int x,y,z; x = y+3*z; } Sei folgende Funktion gegeben: Ordne den Variablen die ersten drei Register zu: Ordne den Variablen die ersten drei Register zu: ρ = {x 7→ hRL, 1i, y 7→ hRL, 2i, z 7→ hRL, 3i} ρ = {x 7→ hRL, 1i, y 7→ hRL, 2i, z 7→ hRL, 3i} Setzte t = 4 da R4 das erste freie Register ist. Erzeuge code4 x=y+3*z ρ code4R y+3*z ρ void f(void) { int x,y,z; x = y+3*z; } Setzte t = 4 da R4 das erste freie Register ist. Erzeuge code4 x=y+3*z ρ = code4R y+3*z ρ move R1 R4 = code5R 3*z ρ add R4 R2 R5 354 / 398 = code4R y+3*z ρ move R1 R4 code4R y+3*z ρ = code5R 3*z ρ add R4 R2 R5 code5R 3*z ρ = code6R 3 ρ mul R5 R6 R3 354 / 398 Übersetzung von Ausdrücken Sei folgende Funktion gegeben: Übersetzung von Ausdrücken void f(void) { int x,y,z; x = y+3*z; } Ordne den Variablen die ersten drei Register zu: Ordne den Variablen die ersten drei Register zu: ρ = {x 7→ hRL, 1i, y 7→ hRL, 2i, z 7→ hRL, 3i} ρ = {x 7→ hRL, 1i, y 7→ hRL, 2i, z 7→ hRL, 3i} Setzte t = 4 da R4 das erste freie Register ist. Erzeuge code4 x=y+3*z ρ void f(void) { int x,y,z; x = y+3*z; } Sei folgende Funktion gegeben: Setzte t = 4 da R4 das erste freie Register ist. Erzeuge code4 x=y+3*z ρ = code4R y+3*z ρ move R1 R4 = code4R y+3*z ρ move R1 R4 code4R y+3*z ρ = code5R 3*z ρ add R4 R2 R5 code4R y+3*z ρ = code5R 3*z ρ add R4 R2 R5 code5R 3*z ρ = code6R 3 ρ mul R5 R6 R3 code5R 3*z ρ = code6R 3 ρ mul R5 R6 R3 code6R 3 ρ code6R 3 ρ = loadc R6 3 354 / 398 Übersetzen von Ausdrücken = loadc R6 3 ; die Zuweisung x=y+3*z wird in nur vier Instruktionen loadc R6 3; mul R5 R6 R3 ; add R4 R2 R5 ; move R1 R4 übersetzt Übersetzen von Ausdrücken Sei op = {add, sub, div, mul, mod, le, gr, eq, leq, geq, and, or}. Die R-CMa kennt für jeden Operator op eine Instruktion Sei op = {add, sub, div, mul, mod, le, gr, eq, leq, geq, and, or}. Die R-CMa kennt für jeden Operator op eine Instruktion op Ri Rj Rk op Ri Rj Rk wobei Ri das Zielregister, Rj das erste und Rk das zweite Argument ist. wobei Ri das Zielregister, Rj das erste und Rk das zweite Argument ist. Entsprechend erzeugen wir den Code wie folgt: Entsprechend erzeugen wir den Code wie folgt: codeiR e1 op e2 ρ codeiR e1 ρ codei+1 R e2 ρ = 354 / 398 codeiR e1 op e2 ρ codeiR e1 ρ codei+1 R e2 ρ op Ri Ri Ri+1 = op Ri Ri Ri+1 Beispiel: Übersetze 3*4 mit i = 4: code4R 3*4 ρ code4R 3 ρ code5R 4 ρ mul R4 R4 R5 = 355 / 398 Übersetzen von Ausdrücken 355 / 398 Verwaltung temporärer Register Beachte, dass temporäre Register wiederverwendet werden: Übersetze 3*4+3*4 mit t = 4: Sei op = {add, sub, div, mul, mod, le, gr, eq, leq, geq, and, or}. Die R-CMa kennt für jeden Operator op eine Instruktion code4R 3*4+3*4 ρ = op Ri Rj Rk wobei Ri das Zielregister, Rj das erste und Rk das zweite Argument ist. mit codeiR 3*4 ρ Entsprechend erzeugen wir den Code wie folgt: codeiR e1 op e2 ρ = codeiR e1 ρ codei+1 R e2 ρ op Ri Ri Ri+1 = code4R 3*4+3*4 ρ = loadc Ri 3 loadc Ri+1 4 mul Ri Ri Ri+1 ergibt sich Beispiel: Übersetze 3*4 mit i = 4: code4R 3*4 ρ code4R 3*4 ρ code5R 3*4 ρ add R4 R4 R5 = loadc R4 3 loadc R5 4 mul R4 R4 R5 355 / 398 356 / 398 Verwaltung temporärer Register Semantik der Operatoren Beachte, dass temporäre Register wiederverwendet werden: Übersetze 3*4+3*4 mit t = 4: code4R 3*4+3*4 ρ = Die Operatoren haben die folgende Semantik: code4R 3*4 ρ code5R 3*4 ρ add R4 R4 R5 add Ri Rj Rk sub Ri Rj Rk div Ri Rj Rk mul Ri Rj Rk mod Ri Rj Rk mit codeiR 3*4 ρ = loadc Ri 3 loadc Ri+1 4 mul Ri Ri Ri+1 le Ri Rj Rk gr Ri Rj Rk eq Ri Rj Rk leq Ri Rj Rk geq Ri Rj Rk and Ri Rj Rk or Ri Rj Rk ergibt sich code4R 3*4+3*4 ρ = loadc R4 3 loadc R5 4 mul R4 R4 R5 loadc R5 3 loadc R6 4 mul R5 R5 R6 add R4 R4 R5 Ri ← Rj + Rk Ri ← Rj − Rk Ri ← Rj /Rk Ri ← Rj ∗ Rk Ri ← sgn(Rk )k wobei |Rj | = n|Rk | + k ∧ n ≥ 0, 0 ≤ k < |Rk | Ri ← if Rj < Rk then 1 else 0 Ri ← if Rj > Rk then 1 else 0 Ri ← if Rj = Rk then 1 else 0 Ri ← if Rj ≤ Rk then 1 else 0 Ri ← if Rj ≥ Rk then 1 else 0 Ri ← Rj & Rk // bit-wise and Ri ← Rj | Rk // bit-wise or 357 / 398 356 / 398 Semantik der Operatoren Übersetzung unärer Operatoren Die unären Operatoren op = {neg, not} nehmen zwei Register: Die Operatoren haben die folgende Semantik: add Ri Rj Rk sub Ri Rj Rk div Ri Rj Rk mul Ri Rj Rk mod Ri Rj Rk le Ri Rj Rk gr Ri Rj Rk eq Ri Rj Rk leq Ri Rj Rk geq Ri Rj Rk and Ri Rj Rk or Ri Rj Rk codeiR op e ρ Ri ← Rj + Rk Ri ← Rj − Rk Ri ← Rj /Rk Ri ← Rj ∗ Rk Ri ← sgn(Rk )k wobei |Rj | = n|Rk | + k ∧ n ≥ 0, 0 ≤ k < |Rk | Ri ← if Rj < Rk then 1 else 0 Ri ← if Rj > Rk then 1 else 0 Ri ← if Rj = Rk then 1 else 0 Ri ← if Rj ≤ Rk then 1 else 0 Ri ← if Rj ≥ Rk then 1 else 0 Ri ← Rj & Rk // bit-wise and Ri ← Rj | Rk // bit-wise or = codeiR e ρ op Ri Ri Beachte: alle Ergebnisse werden ≡232 berechnet 357 / 398 Übersetzung unärer Operatoren Übersetzung unärer Operatoren Die unären Operatoren op = {neg, not} nehmen zwei Register: codeiR op e ρ = 358 / 398 Die unären Operatoren op = {neg, not} nehmen zwei Register: codeiR e ρ op Ri Ri codeiR op e ρ Beachte: Wir verwenden dasselbe Register. = codeiR e ρ op Ri Ri Beachte: Wir verwenden dasselbe Register. Beispiel: Übersetze -4 nach R5 : code5R -4 ρ 358 / 398 = code5R 4 ρ neg R5 R5 358 / 398 Übersetzung unärer Operatoren Übersetzung unärer Operatoren Die unären Operatoren op = {neg, not} nehmen zwei Register: codeiR codeiR = op e ρ Die unären Operatoren op = {neg, not} nehmen zwei Register: codeiR op e ρ eρ op Ri Ri codeiR e ρ op Ri Ri = Beachte: Wir verwenden dasselbe Register. Beachte: Wir verwenden dasselbe Register. Beispiel: Übersetze -4 nach R5 : Beispiel: Übersetze -4 nach R5 : code5R -4 ρ = code5R -4 ρ loadc R5 4 neg R5 R5 = loadc R5 4 neg R5 R5 Die Operatoren haben die folgende Semantik: not Ri Rj net Ri Rj Ri ← if Rj = 0 then 1 else 0 Ri ← −Rj 358 / 398 Datentransfer Instruktionen der R-CMa (I) Datentransfer Instruktionen der R-CMa (II) Betrachte C-Ma Instruktionen für den Lesezugriff auf den Speicher: Instruktion load loadc c loadrc c loada c loadr c move k Betrachte C-Ma Instruktionen für das Schreiben in den Speicher: Semantik Intuition S[SP] ← S[S[SP]] lade Wert von Adresse SP ← SP + 1; S[SP] ← c lade Konstante SP ← SP + 1; S[SP] ← c + FP lade Konstante rel. zu FP loadc c; load lade globale Variable loadrc c; load lade lokale Variable ... kopiere k Werte auf Keller Die Varianten der R-CMa erweitern diese um Register-Argumente: Instruktion load Ri Rj loadc Ri c loadrc Ri c loada Ri c loadr Ri c move Ri Rj move k Rj Semantik Ri ← S[Rj ] Ri ← c Ri ← FP + c Ri ← S[c] Ri ← S[FP + c] Ri ← Rj k [S[SP + i] ← S[Rj + i]]i=0 Intuition lade Wert von Adresse lade Konstante lade Konstante rel. zu FP lade globale Variable lade lokale Variable transferiere Wert zw. Registern kopiere k Werte auf den Keller storea c storer c storea c storer c Semantik S[S[SP]] ← S[SP − 1]; SP ← SP − 1 loadc c; store loadrc c; store Intuition speichere Wert an Adresse schreibe globale Variable schreibe lokale Variable Die Varianten der R-CMa erweitern diese um Register-Argumente: Instruktion store Ri Rj storea c Ri storer c Ri Semantik S[Ri ] ← Rj S[c] ← Ri S[FP + c] ← Ri Intuition speichere Wert an Adresse schreibe globale Variable schreibe lokale Variable 360 / 398 Übersetzung von Variablen Betrachte C-Ma Instruktionen für das Schreiben in den Speicher: Semantik S[S[SP]] ← S[SP − 1]; SP ← SP − 1 loadc c; store loadrc c; store Instruktion store 359 / 398 Datentransfer Instruktionen der R-CMa (II) Instruktion store 358 / 398 Der Wert einer Variablen wird wie folgt in Register Ri geladen: if ρ x = hG, ai loada Ri a i loadr Ri a if ρ x = hL, ai codeR x ρ = move Ri Rj if ρ x = hr, ji ∧ r ∈ {RG, RL} Intuition speichere Wert an Adresse schreibe globale Variable schreibe lokale Variable Die Varianten der R-CMa erweitern diese um Register-Argumente: Instruktion store Ri Rj storea c Ri storer c Ri Semantik S[Ri ] ← Rj S[c] ← Ri S[FP + c] ← Ri Intuition speichere Wert an Adresse schreibe globale Variable schreibe lokale Variable Beachte: alle Instruktionen benutzen die Intel-Konvention (im Ggs. zur AT&T Konvention): op dst src1 . . . srcn . Vergleiche: Instruktion loada Ri c storea c Ri Semantik Intuition Ri ← S[c] lade globale Variable S[c] ← Ri schreibe globale Variable 360 / 398 361 / 398 Übersetzung von Variablen Übersetzung von Variablen Der Wert einer Variablen wird wie folgt in Register Ri geladen: if ρ x = hG, ai loada Ri a loadr Ri a if ρ x = hL, ai codeiR x ρ = move Ri Rj if ρ x = hr, ji ∧ r ∈ {RG, RL} Der Wert einer Variablen wird wie folgt in Register Ri geladen: if ρ x = hG, ai loada Ri a loadr Ri a if ρ x = hL, ai codeiR x ρ = move Ri Rj if ρ x = hr, ji ∧ r ∈ {RG, RL} Die Adresse einer Variablen wird wie folgt in Ri berechnet: loadc Ri a if ρ x = hG, ai codeiL x ρ = loadrc Ri a if ρ x = hL, ai Die Adresse einer Variablen wird wie folgt in Ri berechnet: loadc Ri a if ρ x = hG, ai codeiL x ρ = loadrc Ri a if ρ x = hL, ai Achtung: für ρ x = hRG, ji und ρ x = hRL, ji is codeiL nicht definiert! 361 / 398 Übersetzung von Variablen 361 / 398 Übersetzung von Variablen Der Wert einer Variablen wird wie folgt in Register Ri geladen: if ρ x = hG, ai loada Ri a i loadr Ri a if ρ x = hL, ai codeR x ρ = move Ri Rj if ρ x = hr, ji ∧ r ∈ {RG, RL} Der Wert einer Variablen wird wie folgt in Register Ri geladen: if ρ x = hG, ai loada Ri a i loadr Ri a if ρ x = hL, ai codeR x ρ = move Ri Rj if ρ x = hr, ji ∧ r ∈ {RG, RL} Achtung: für ρ x = hRG, ji und ρ x = hRL, ji is codeiL nicht definiert! Achtung: für ρ x = hRG, ji und ρ x = hRL, ji is codeiL nicht definiert! Beobachtung: Beobachtung: Die Adresse einer Variablen wird wie folgt in Ri berechnet: loadc Ri a if ρ x = hG, ai codeiL x ρ = loadrc Ri a if ρ x = hL, ai Die Adresse einer Variablen wird wie folgt in Ri berechnet: loadc Ri a if ρ x = hG, ai codeiL x ρ = loadrc Ri a if ρ x = hL, ai intuitiv: ein Register hat keine Adresse intuitiv: ein Register hat keine Adresse ein Register darf während der Codegenerierung nie als L-Wert auftreten 361 / 398 Übersetzung von Variablen 361 / 398 Übersetzung von Zuweisungen Zuweisungen wie x=2*y wurden auf der C-Ma übersetzt durch: Der Wert einer Variablen wird wie folgt in Register Ri geladen: if ρ x = hG, ai loada Ri a i loadr Ri a if ρ x = hL, ai codeR x ρ = move Ri Rj if ρ x = hr, ji ∧ r ∈ {RG, RL} das Ermitteln das R-Wertes von 2*y, das Ermitteln des L-Wertes von x und einem store Befehl. Analog definieren wir: Die Adresse einer Variablen wird wie folgt in Ri berechnet: loadc Ri a if ρ x = hG, ai codeiL x ρ = loadrc Ri a if ρ x = hL, ai codeiR e1 = e2 ρ = codeiR e2 ρ codei+1 e1 ρ L store Ri+1 Ri Achtung: für ρ x = hRG, ji und ρ x = hRL, ji is codeiL nicht definiert! Beobachtung: intuitiv: ein Register hat keine Adresse ein Register darf während der Codegenerierung nie als L-Wert auftreten dies hat Auswirkungen auf die Übersetzung von Zuweisungen 361 / 398 362 / 398 Übersetzung von Zuweisungen Übersetzung von Zuweisungen Zuweisungen wie x=2*y wurden auf der C-Ma übersetzt durch: Zuweisungen wie x=2*y wurden auf der C-Ma übersetzt durch: das Ermitteln das R-Wertes von 2*y, das Ermitteln des L-Wertes von x und einem store Befehl. das Ermitteln das R-Wertes von 2*y, das Ermitteln des L-Wertes von x und einem store Befehl. Analog definieren wir: codeiR Analog definieren wir: e1 = e2 ρ = codeiR e2 ρ codei+1 e1 ρ L codeiR e1 = e2 ρ = codeiR e2 ρ codei+1 e1 ρ L store Ri+1 Ri store Ri+1 Ri Achtung: undefiniertes Resultat, falls e1 ≡ x und ρ x = hRL, ji. Achtung: undefiniertes Resultat, falls e1 ≡ x und ρ x = hRL, ji. Definiere Spezialfall: Sei ρ x = hRL, ji oder ρ x = hRL, ji, dann definiere codeiR x = e2 ρ = codeiR e2 ρ move Rj Ri 362 / 398 Übersetzung von Zuweisungen 362 / 398 Zeiger und Datenstrukturen Zuweisungen wie x=2*y wurden auf der C-Ma übersetzt durch: Zeiger und Zeigerarithmetik wird in Registern ausgeführt: Generieren von Zeigern: das Ermitteln das R-Wertes von 2*y, das Ermitteln des L-Wertes von x und einem store Befehl. codeiR &e ρ = codeiL e ρ Analog definieren wir: codeiR e1 = e2 ρ = codeiR e2 ρ codei+1 e1 ρ L store Ri+1 Ri Achtung: undefiniertes Resultat, falls e1 ≡ x und ρ x = hRL, ji. Definiere Spezialfall: Sei ρ x = hRL, ji oder ρ x = hRL, ji, dann definiere codeiR x = e2 ρ = codeiR e2 ρ move Rj Ri Beachte: codeiR x = e2 ρ = codejR e2 ρ wäre inkorrekt! 363 / 398 362 / 398 Zeiger und Datenstrukturen Zeiger und Datenstrukturen Zeiger und Zeigerarithmetik wird in Registern ausgeführt: Generieren von Zeigern: codeiR &e ρ = Zeiger und Zeigerarithmetik wird in Registern ausgeführt: Generieren von Zeigern: codeiL e ρ codeiR &e ρ = codeiL e ρ Dereferenzieren von Zeigern zum Lesen (R-Wert): Dereferenzieren von Zeigern zum Lesen (R-Wert): codeiR ∗e ρ codeiR ∗e ρ = codeiL e ρ load Ri Ri = codeiL e ρ load Ri Ri Dereferenzieren von Zeigern zum Schreiben (L-Wert): codeiL ∗e ρ 363 / 398 = codeiR e ρ 363 / 398 Zeiger und Datenstrukturen Zeiger und Datenstrukturen Zeiger und Zeigerarithmetik wird in Registern ausgeführt: Generieren von Zeigern: codeiR &e ρ = Zeiger und Zeigerarithmetik wird in Registern ausgeführt: Generieren von Zeigern: codeiL e ρ codeiR &e ρ = codeiL e ρ Dereferenzieren von Zeigern zum Lesen (R-Wert): Dereferenzieren von Zeigern zum Lesen (R-Wert): codeiR ∗e ρ codeiR ∗e ρ = codeiL e ρ load Ri Ri Dereferenzieren von Zeigern zum Schreiben (L-Wert): codeiL ∗e ρ = codeiL e ρ load Ri Ri Dereferenzieren von Zeigern zum Schreiben (L-Wert): = codeiR e ρ codeiL ∗e ρ Beachte: eine Variable x (int oder struct), deren Adresse genommen wurde, muss im Speicher allokiert werden, d.h. ρ x = hL, oi oder ρ x = hG, oi = codeiR e ρ Beachte: eine Variable x (int oder struct), deren Adresse genommen wurde, muss im Speicher allokiert werden, d.h. ρ x = hL, oi oder ρ x = hG, oi auf ein Feld (array) wird immer mittels Zeiger zugegriffen, muss also auch im Speicher allokiert werden 363 / 398 Zeiger und Datenstrukturen Übersetzung ganzer Programme Zeiger und Zeigerarithmetik wird in Registern ausgeführt: Generieren von Zeigern: codeiR &e ρ = 363 / 398 codeiL e ρ Dereferenzieren von Zeigern zum Lesen (R-Wert): codeiR ∗e ρ = codeiL e ρ Kapitel 3: load Ri Ri Übersetzung von Anweisungen Dereferenzieren von Zeigern zum Schreiben (L-Wert): codeiL ∗e ρ = codeiR e ρ Beachte: eine Variable x (int oder struct), deren Adresse genommen wurde, muss im Speicher allokiert werden, d.h. ρ x = hL, oi oder ρ x = hG, oi auf ein Feld (array) wird immer mittels Zeiger zugegriffen, muss also auch im Speicher allokiert werden Optimierung: Speichere Elemente eines struct in Registern, während Zeigerzugriffe diese nicht verändern können 363 / 398 Übersetzung von Anweisungen 364 / 398 Übersetzung von Anweisungen Neben dem Ausdruck e1 = e2 müssen wir e1 = e2 auch als Anweisung übersetzen können: Neben dem Ausdruck e1 = e2 müssen wir e1 = e2 auch als Anweisung übersetzen können: Definiere dazu: codei e1 = e2 ρ = codeiR e1 = e2 ρ Das temporäre Register Ri wird dabei ignoriert. Allgemeiner: codei e ρ = codeiR e ρ 365 / 398 365 / 398 Übersetzung von Anweisungen Übersetzung von bedingten Anweisungen Neben dem Ausdruck e1 = e2 müssen wir e1 = e2 auch als Anweisung übersetzen können: code Definiere dazu: codei e1 = e2 ρ code c code tt = codeiR e1 = e2 ρ Übersetzung von if ( c ) tt else ee. Das temporäre Register Ri wird dabei ignoriert. Allgemeiner: codei e ρ = codeiR e ρ codei if(c) tt else ee ρ = Semantik PC ← A if Ri = 0 then PC ← A PC ← A + Ri codeiR c ρ jumpz Ri A Zur Übersetzung von Kontrollfluss-Anweisungen definiere folgende Befehle: Instruktion jump A jumpz Ri A jumpi Ri A ee codei tt ρ jump B Intuition springe zur Adresse A springe wenn Ri = 0 springe indirekt A : codei ee ρ B: 365 / 398 Übersetzung von Schleifen Übersetzung von Schleifen Übersetzung von while Schleifen: codei while(e) s ρ 366 / 398 Übersetzung von while Schleifen: = A : codeiR e ρ codei while(e) s ρ = A : codeiR e ρ jumpz Ri B jumpz Ri B codei s ρ codei s ρ jump A jump A B: B: Übersetzung von for Schleifen: codei for(e1 ; e2 ; e3 ) s ρ = codeiR e1 ρ A : codeiR e2 ρ jumpz Ri B codei s ρ codeiR e3 ρ jump A B: 367 / 398 Übersetzung von Fallunterscheidungen 367 / 398 Übersetzung von Fallunterscheidungen Sei ein switch s mit k aufeinanderfolgenden case Fällen gegeben: switch (e) { case c0 : s0 ; break; . . . case ck−1 : sk−1 ; break; default: s; break; } d.h. ci + 1 = ci+1 für i = [0, k − 1]. Sei ein switch s mit k aufeinanderfolgenden case Fällen gegeben: switch (e) { case c0 : s0 ; break; . . . case ck−1 : sk−1 ; break; default: s; break; } d.h. ci + 1 = ci+1 für i = [0, k − 1]. Definiere codei s ρ wie folgt: codei s ρ = codeiR e ρ checki c0 ck−1 B A0 : .. . codei s0 ρ jump D .. . B: .. . jump A0 .. . jump Ak−1 C: Ak−1 : codei sk−1 ρ jump D 368 / 398 368 / 398 Übersetztung des checki Makros Übersetzung von Fallunterscheidungen Sei ein switch s mit k aufeinanderfolgenden case Fällen gegeben: switch (e) { case c0 : s0 ; break; . . . case ck−1 : sk−1 ; break; default: s; break; } d.h. ci + 1 = ci+1 für i = [0, k − 1]. Definiere codei s ρ wie folgt: codei s ρ = codeiR e ρ checki c0 ck−1 B A0 : .. . codei s0 ρ jump D .. . B: .. . Das Makro checki l u B prüft ob l ≤ Ri < u. Sei k = u − l. wenn l ≤ Ri < u, springt es nach B + Ri − u wenn Ri < l oder Ri ≥ u springt es nach C jump A0 .. . jump Ak−1 C: Ak−1 : codei sk−1 ρ jump D checki l u B prüft ob l ≤ Ri < u gilt und springt entsprechend. 369 / 398 368 / 398 Übersetztung des checki Makros Übersetztung des checki Makros Das Makro checki l u B prüft ob l ≤ Ri < u. Sei k = u − l. Das Makro checki l u B prüft ob l ≤ Ri < u. Sei k = u − l. wenn Ri < l oder Ri ≥ u springt es nach C Wir definieren: wenn Ri < l oder Ri ≥ u springt es nach C Wir definieren: wenn l ≤ Ri < u, springt es nach B + Ri − u checki l u B wenn l ≤ Ri < u, springt es nach B + Ri − u checki l u B loadc Ri+1 l geq Ri+2 Ri Ri+1 jumpz Ri+2 E sub Ri Ri Ri+1 loadc Ri+1 k geq Ri+2 Ri Ri+1 jumpz Ri+2 D E : loadc Ri k D : jumpi Ri B = loadc Ri+1 l geq Ri+2 Ri Ri+1 jumpz Ri+2 E sub Ri Ri Ri+1 loadc Ri+1 k geq Ri+2 Ri Ri+1 jumpz Ri+2 D E : loadc Ri k D : jumpi Ri B = B: .. . jump A0 .. . jump Ak−1 C: Beachte: ein Sprung jumpi Ri B mit Ri = k landet bei C. 369 / 398 369 / 398 Registerverwaltung bei Funktionsaufrufen Übersetzung ganzer Programme Die zwei Registersätze (global und lokal) werden wie folgt verwendet: lokale Variablen werden in lokalen Registern gespeichert (RL) Zwischenergebnisse werden auch in lokalen Registern gespeichert Parameter werden in globalen Registern gespeichert (RG) Kapitel 4: Übersetzung von Funktionen 370 / 398 371 / 398 Registerverwaltung bei Funktionsaufrufen Registerverwaltung bei Funktionsaufrufen Die zwei Registersätze (global und lokal) werden wie folgt verwendet: lokale Variablen werden in lokalen Registern gespeichert (RL) Zwischenergebnisse werden auch in lokalen Registern gespeichert Parameter werden in globalen Registern gespeichert (RG) Konvention: Die zwei Registersätze (global und lokal) werden wie folgt verwendet: lokale Variablen werden in lokalen Registern gespeichert (RL) Zwischenergebnisse werden auch in lokalen Registern gespeichert Parameter werden in globalen Registern gespeichert (RG) Konvention: keines der globalen Registern wird während der Ausführung der aktuellen Funktion benutzt 371 / 398 Registerverwaltung bei Funktionsaufrufen 371 / 398 Registerverwaltung bei Funktionsaufrufen Die zwei Registersätze (global und lokal) werden wie folgt verwendet: lokale Variablen werden in lokalen Registern gespeichert (RL) Zwischenergebnisse werden auch in lokalen Registern gespeichert Parameter werden in globalen Registern gespeichert (RG) Konvention: keines der globalen Registern wird während der Ausführung der aktuellen Funktion benutzt das i te Argument einer Funktion wir in Register Ri übergeben Die zwei Registersätze (global und lokal) werden wie folgt verwendet: lokale Variablen werden in lokalen Registern gespeichert (RL) Zwischenergebnisse werden auch in lokalen Registern gespeichert Parameter werden in globalen Registern gespeichert (RG) Konvention: keines der globalen Registern wird während der Ausführung der aktuellen Funktion benutzt das i te Argument einer Funktion wir in Register Ri übergeben der Rückgabewert einer Funktion wird in R0 gespeichert 371 / 398 Registerverwaltung bei Funktionsaufrufen 371 / 398 Registerverwaltung bei Funktionsaufrufen Die zwei Registersätze (global und lokal) werden wie folgt verwendet: lokale Variablen werden in lokalen Registern gespeichert (RL) Zwischenergebnisse werden auch in lokalen Registern gespeichert Parameter werden in globalen Registern gespeichert (RG) Konvention: keines der globalen Registern wird während der Ausführung der aktuellen Funktion benutzt das i te Argument einer Funktion wir in Register Ri übergeben der Rückgabewert einer Funktion wird in R0 gespeichert alle lokalen Register werden von der aufrufenden Funktion gespeichert Die zwei Registersätze (global und lokal) werden wie folgt verwendet: lokale Variablen werden in lokalen Registern gespeichert (RL) Zwischenergebnisse werden auch in lokalen Registern gespeichert Parameter werden in globalen Registern gespeichert (RG) Konvention: keines der globalen Registern wird während der Ausführung der aktuellen Funktion benutzt das i te Argument einer Funktion wir in Register Ri übergeben der Rückgabewert einer Funktion wird in R0 gespeichert alle lokalen Register werden von der aufrufenden Funktion gespeichert Definition Sei f eine Funktion die g aufruft. Ein Register Ri heißt caller-saved, wenn f Ri sichert und g es überschreiben darf callee-saved, wenn f Ri nicht sichert und g es vor dem Rücksprung wiederherstellen muss 371 / 398 371 / 398 Übersetzung von Funktionsaufrufen Übersetzung von Funktionsaufrufen Ein Funktionsaufruf g(e1 , . . . en ) wird nun wie folgt übersetzt: codeiR Ein Funktionsaufruf g(e1 , . . . en ) wird nun wie folgt übersetzt: codeiR g(e1 , . . . en ) ρ = codeiR g(e1 , . . . en ) ρ = e1 ρ move R−1 Ri .. . codeiR en ρ move R−n Ri codeiR g ρ saveloc R1 Ri−1 mark call Ri restoreloc R1 Ri−1 move Ri R0 codeiR e1 ρ move R−1 Ri .. . codeiR en ρ move R−n Ri codeiR g ρ saveloc R1 Ri−1 mark call Ri restoreloc R1 Ri−1 move Ri R0 Neue Instruktionen: saveloc Ri Rj legt die Register Ri , Ri+1 . . . Rj auf dem Stapel ab (inklusive!) restoreloc Ri Rj nimmt die Register Rj , Rj−1 , . . . Ri vom Stapel runter 372 / 398 372 / 398 Übersetzung von Funktionsaufrufen Rückgabewerte einer Funktion Ein Funktionsaufruf g(e1 , . . . en ) wird nun wie folgt übersetzt: codeiR g(e1 , . . . en ) ρ = Die globalen Register werden auch benutzt um den Rückgabewert zu übermitteln: codeiR e1 ρ move R−1 Ri .. . codei return e ρ = codeiR e ρ move R0 Ri codeiR en ρ move R−n Ri i codeR g ρ saveloc R1 Ri−1 mark call Ri restoreloc R1 Ri−1 move Ri R0 return Neue Instruktionen: saveloc Ri Rj legt die Register Ri , Ri+1 . . . Rj auf dem Stapel ab (inklusive!) restoreloc Ri Rj nimmt die Register Rj , Rj−1 , . . . Ri vom Stapel runter Argumente auf dem Stack: push Ri pro Wert und pop k für k Werte 373 / 398 372 / 398 Rückgabewerte einer Funktion Rückgabewerte einer Funktion Die globalen Register werden auch benutzt um den Rückgabewert zu übermitteln: codei return e ρ = Die globalen Register werden auch benutzt um den Rückgabewert zu übermitteln: codeiR e ρ codei return e ρ = move R0 Ri return return Alternative ohne Rückgabewert: i code return ρ codeiR e ρ move R0 Ri Alternative ohne Rückgabewert: codei return ρ = return = return Globale Register werden ansonsten im Funktionsrumpf nicht benutzt: Vorteil: an jeder Stelle im Rumpf können andere Funktionen aufgerufen werden ohne globale Register retten zu müssen Nachteil: beim Eintritt in eine Funktion müssen die globalen Register gesichert werden 373 / 398 373 / 398 Übersetzung ganzer Funktionen Übersetzung ganzer Funktionen Die Übersetzung einer Funktion ist damit wie folgt definiert: 1 code tr f(args){decls ss} ρ = Die Übersetzung einer Funktion ist damit wie folgt definiert: code1 tr f(args){decls ss} ρ = enter q move Rl+1 R−1 .. . move Rl+n R−n codel+n+1 ss ρ0 return enter q move Rl+1 R−1 .. . move Rl+n R−n codel+n+1 ss ρ0 return Randbedinungen: Randbedinungen: saveloc speichert maximal q − 3 Register 374 / 398 Übersetzung ganzer Funktionen Übersetzung ganzer Funktionen Die Übersetzung einer Funktion ist damit wie folgt definiert: code1 tr f(args){decls ss} ρ = 374 / 398 Die Übersetzung einer Funktion ist damit wie folgt definiert: code1 tr f(args){decls ss} ρ = enter q move Rl+1 R−1 .. . move Rl+n R−n codel+n+1 ss ρ0 return enter q move Rl+1 R−1 .. . move Rl+n R−n codel+n+1 ss ρ0 return Randbedinungen: Randbedinungen: saveloc speichert maximal q − 3 Register saveloc speichert maximal q − 3 Register die lokalen Variablen sind in den Registern R1 , . . . Rl gespeichert die lokalen Variablen sind in den Registern R1 , . . . Rl gespeichert die Parameter der Funktion stehen in den Registern R−1 , . . . R−n 374 / 398 Übersetzung ganzer Funktionen Übersetzung ganzer Funktionen Die Übersetzung einer Funktion ist damit wie folgt definiert: code1 tr f(args){decls ss} ρ = 374 / 398 Die Übersetzung einer Funktion ist damit wie folgt definiert: code1 tr f(args){decls ss} ρ = enter q move Rl+1 R−1 .. . move Rl+n R−n codel+n+1 ss ρ0 return enter q move Rl+1 R−1 .. . move Rl+n R−n codel+n+1 ss ρ0 return Randbedinungen: Randbedinungen: saveloc speichert maximal q − 3 Register saveloc speichert maximal q − 3 Register ρ0 ist die um lokale Variablen decls und Funktions-Parameter args erweiterte Umgebung ρ ρ0 ist die um lokale Variablen decls und Funktions-Parameter args erweiterte Umgebung ρ die lokalen Variablen sind in den Registern R1 , . . . Rl gespeichert die Parameter der Funktion stehen in den Registern R−1 , . . . R−n die lokalen Variablen sind in den Registern R1 , . . . Rl gespeichert die Parameter der Funktion stehen in den Registern R−1 , . . . R−n Argumente auf dem Stack: zusätzlich alloc k 374 / 398 374 / 398 Übersetzung ganzer Funktionen Übersetzen ganzer Programme Ein Programm P = F1 ; . . . Fn muss eine main Funktion enthalten. Die Übersetzung einer Funktion ist damit wie folgt definiert: 1 code tr f(args){decls ss} ρ = code1 P ρ enter q move Rl+1 R−1 .. . move Rl+n R−n codel+n+1 ss ρ0 return Randbedinungen: saveloc speichert maximal q − 3 Register die lokalen Variablen sind in den Registern R1 , . . . Rl gespeichert die Parameter der Funktion stehen in den Registern R−1 , . . . R−n enter (k + 3) alloc k loadc R1 _main saveloc R1 R1 mark call R1 restoreloc R1 R1 halt _f1 : codei F1 ρ .. . = _fn : codei Fn ρ ρ0 ist die um lokale Variablen decls und Funktions-Parameter args erweiterte Umgebung ρ Argumente auf dem Stack: zusätzlich alloc k Sind die move Instruktionen immer nötig? 374 / 398 Übersetzen ganzer Programme Übersetzung der Fakultätsfunktion Betrachte: Ein Programm P = F1 ; . . . Fn muss eine main Funktion enthalten. 1 code P ρ 375 / 398 enter (k + 3) alloc k loadc R1 _main saveloc R1 R1 mark call R1 restoreloc R1 R1 halt _f1 : codei F1 ρ .. . int fac(int x) { if (x<=0) then return 1; else return x*fac(x-1); } = _fac: i=2 i _fn : code Fn ρ Diskussion: k sind die Anzahl der Stackplätze für globale Variablen saveloc R1 R0 wäre ausreichend (d.h. rette kein Register) ρ enthält die Adressen der Funktionen und globalen Variablen enter 5 move R1 R−1 move R2 R1 loadc R3 0 leq R2 R2 R3 jumpz R2 _A loadc R2 1 move R0 R2 return jump _B _A: i=3 i=4 i=3 3 mark+call save param. if (x<=0) to else return 1 _B: move R2 R1 move R3 R1 loadc R4 1 sub R3 R3 R4 move R−1 R3 loadc R3 _fac saveloc R1 R2 mark call R3 restoreloc R1 R2 move R3 R0 mul R2 R2 R3 move R0 R2 return return x*fac(x-1) x-1 fac(x-1) return x*... code is dead 375 / 398 Realistische Register Maschinen 376 / 398 Realistische Register Maschinen Die R-CMa ist geeignet, auf einfache Weise Code für eine Registermaschine zu erzeugen. Die R-CMa ist geeignet, auf einfache Weise Code für eine Registermaschine zu erzeugen. reale Maschinen haben nur eine fixe Anzahl an Registern 377 / 398 377 / 398 Realistische Register Maschinen Realistische Register Maschinen Die R-CMa ist geeignet, auf einfache Weise Code für eine Registermaschine zu erzeugen. Die R-CMa ist geeignet, auf einfache Weise Code für eine Registermaschine zu erzeugen. reale Maschinen haben nur eine fixe Anzahl an Registern reale Maschinen haben nur eine fixe Anzahl an Registern die unendliche Menge an virtuellen Registern muss auf eine endliche Menge realer Register abgebildet werden die unendliche Menge an virtuellen Registern muss auf eine endliche Menge realer Register abgebildet werden Idee: Benutze ein Register Ri , das zur Zeit nicht genutzt wird, für den Wert eines anderen Registers Rj 377 / 398 Realistische Register Maschinen 377 / 398 Realistische Register Maschinen Die R-CMa ist geeignet, auf einfache Weise Code für eine Registermaschine zu erzeugen. Die R-CMa ist geeignet, auf einfache Weise Code für eine Registermaschine zu erzeugen. reale Maschinen haben nur eine fixe Anzahl an Registern reale Maschinen haben nur eine fixe Anzahl an Registern die unendliche Menge an virtuellen Registern muss auf eine endliche Menge realer Register abgebildet werden die unendliche Menge an virtuellen Registern muss auf eine endliche Menge realer Register abgebildet werden Idee: Benutze ein Register Ri , das zur Zeit nicht genutzt wird, für den Wert eines anderen Registers Rj Idee: Benutze ein Register Ri , das zur Zeit nicht genutzt wird, für den Wert eines anderen Registers Rj falls das Programm mehr als die verfügbaren Register braucht, allokiere einige Variablen auf dem Stack und nicht mehr in Registern falls das Programm mehr als die verfügbaren Register braucht, allokiere einige Variablen auf dem Stack und nicht mehr in Registern Wir benötigen eine Lösung für die folgenden Probleme: 377 / 398 Realistische Register Maschinen 377 / 398 Realistische Register Maschinen Die R-CMa ist geeignet, auf einfache Weise Code für eine Registermaschine zu erzeugen. Die R-CMa ist geeignet, auf einfache Weise Code für eine Registermaschine zu erzeugen. reale Maschinen haben nur eine fixe Anzahl an Registern reale Maschinen haben nur eine fixe Anzahl an Registern die unendliche Menge an virtuellen Registern muss auf eine endliche Menge realer Register abgebildet werden die unendliche Menge an virtuellen Registern muss auf eine endliche Menge realer Register abgebildet werden Idee: Benutze ein Register Ri , das zur Zeit nicht genutzt wird, für den Wert eines anderen Registers Rj Idee: Benutze ein Register Ri , das zur Zeit nicht genutzt wird, für den Wert eines anderen Registers Rj falls das Programm mehr als die verfügbaren Register braucht, allokiere einige Variablen auf dem Stack und nicht mehr in Registern falls das Programm mehr als die verfügbaren Register braucht, allokiere einige Variablen auf dem Stack und nicht mehr in Registern Wir benötigen eine Lösung für die folgenden Probleme: Wir benötigen eine Lösung für die folgenden Probleme: bestimme, wann ein Register nicht benutzt ist bestimme, wann ein Register nicht benutzt ist weise mehreren virtuellen Registern das gleiche reale Register zu, wenn sie nicht gleichzeitig benutzt werden 377 / 398 377 / 398 Abstrakte Syntax Übersetzung ganzer Programme Anstatt auf R-CMa Instruktionen zu arbeiten, benutzen wir folgende Pseudo-Sprache: Kapitel 5: • • • • • • • Liveness Analyse x, y, . . . R = e; R = M[e]; M[e1 ] = e2 ; if (e) s1 else s2 goto L; stop // // // // // // // Register Zuweisungen Lesen vom Speicher Schreiben in Speicher bedingter Sprung Sprung oder Schleife Funktion- oder Programmende 379 / 398 378 / 398 Abstrakte Syntax Abstrakte Syntax Anstatt auf R-CMa Instruktionen zu arbeiten, benutzen wir folgende Pseudo-Sprache: • • • • • • • x, y, . . . R = e; R = M[e]; M[e1 ] = e2 ; if (e) s1 else s2 goto L; stop // // // // // // // Anstatt auf R-CMa Instruktionen zu arbeiten, benutzen wir folgende Pseudo-Sprache: Register Zuweisungen Lesen vom Speicher Schreiben in Speicher bedingter Sprung Sprung oder Schleife Funktion- oder Programmende • • • • • • • die Instruktionen können als Zwischensprache aufgefasst werden x, y, . . . R = e; R = M[e]; M[e1 ] = e2 ; if (e) s1 else s2 goto L; stop // // // // // // // Register Zuweisungen Lesen vom Speicher Schreiben in Speicher bedingter Sprung Sprung oder Schleife Funktion- oder Programmende die Instruktionen können als Zwischensprache aufgefasst werden ... oder als Abstraktion für R-CMa Instruktionen 379 / 398 Beispiel für Liveness 379 / 398 Beispiel für Liveness Eine liveness Analyse bestimmt, an welchen Programmpunkten ein Register benutzt wird. Eine liveness Analyse bestimmt, an welchen Programmpunkten ein Register benutzt wird. Betrachte: Betrachte: 1: 2: 3: x = y + 2; y = 5; x = y + 3; 1: 2: 3: x = y + 2; y = 5; x = y + 3; Der Wert von x an den Programmpunkten 1, 2 wird überschrieben bevor er benutzt wird. 380 / 398 380 / 398 Beispiel für Liveness Motivation für liveness Analyse Eine liveness Analyse ist nützlich: Zuweisungen zu toten Variablen können entfernt werden solch unnütze Zuweisungen können durch andere Transformationen entstehen Eine liveness Analyse bestimmt, an welchen Programmpunkten ein Register benutzt wird. Definition Betrachte: 1: 2: 3: x = y + 2; y = 5; x = y + 3; Sei π = s1 ; . . . sn eine Folge von Anweisungen. Eine Variable x ist live (lebendig) am Programmpunkt s1 wenn die Variablen X nach sn live sind und Der Wert von x an den Programmpunkten 1, 2 wird überschrieben bevor er benutzt wird. Daher sagen wir, x ist tot (dead) an diesen Programmpunkten. 380 / 398 Motivation für liveness Analyse 381 / 398 Motivation für liveness Analyse Eine liveness Analyse ist nützlich: Eine liveness Analyse ist nützlich: Zuweisungen zu toten Variablen können entfernt werden Zuweisungen zu toten Variablen können entfernt werden solch unnütze Zuweisungen können durch andere Transformationen entstehen solch unnütze Zuweisungen können durch andere Transformationen entstehen Definition Definition Sei π = s1 ; . . . sn eine Folge von Anweisungen. Eine Variable x ist live (lebendig) am Programmpunkt s1 wenn die Variablen X nach sn live sind und Sei π = s1 ; . . . sn eine Folge von Anweisungen. Eine Variable x ist live (lebendig) am Programmpunkt s1 wenn die Variablen X nach sn live sind und x ∈ X und π enthält keine Definition von x or x ∈ X und π enthält keine Definition von x or π kann zerlegt werden in π = π1 kπ2 so dass k ist eine Benutzung von x und π 1 enthält keine Definition von x. s k π1 381 / 398 Definition und Benutzung von Variablen Definition und Benutzung von Variablen Betrachte die Anweisungen s1 , . . . sn als Kanten in einem Graphen. Wir definieren Definition und Benutzung wie folgt: si ; Pos (e) Neg (e) x = e; x = M[e]; M[e1 ] = e2 ; Benutztung ∅ Vars (e) Vars (e) Vars (e) Vars (e) Vars (e1 ) ∪ Vars (e2 ) 381 / 398 Betrachte die Anweisungen s1 , . . . sn als Kanten in einem Graphen. Wir definieren Definition und Benutzung wie folgt: Definition ∅ ∅ ∅ {x} {x} ∅ si ; Pos (e) Neg (e) x = e; x = M[e]; M[e1 ] = e2 ; Beispiel: Benutztung ∅ Vars (e) Vars (e) Vars (e) Vars (e) Vars (e1 ) ∪ Vars (e2 ) 1: 2: 3: 382 / 398 Definition ∅ ∅ ∅ {x} {x} ∅ x = y + 2; y = 5; x = y + 3; 382 / 398 Lebende und tote Variablen Lebende und tote Variablen Definition Definition Eine Variable x, die nicht live ist am Programmpunkt s entlang des Pfades π relativ zu X ist dead (tot) bei s relativ zu X. Eine Variable x, die nicht live ist am Programmpunkt s entlang des Pfades π relativ zu X ist dead (tot) bei s relativ zu X. Beispiel: x = y + 2; 0 y = 5; 1 x = y + 3; 2 3 wobei X = ∅. Wie folgern: 0 1 2 3 live {y} ∅ {y} ∅ dead {x} {x, y} {x} {x, y} 383 / 398 383 / 398 Berechnung der liveness Information Berechnung der liveness Information Wir berechnen die liveness Information durch ausführen des Program mit einer abstrakten Semantik: Wir berechnen die liveness Information durch ausführen des Program mit einer abstrakten Semantik: Idee: Ersetze jedes Anweisung si einer Kante k = (u, si , v) durch eine Funktion [[k]]] , die die Menge der lebenden Variablen L bei v in die Menge der lebenden Variablen bei u überführt. 384 / 398 Berechnung der liveness Information Berechnung der liveness Information Wir berechnen die liveness Information durch ausführen des Program mit einer abstrakten Semantik: Idee: Ersetze jedes Anweisung si einer Kante k = (u, si , v) durch eine Funktion [[k]]] , die die Menge der lebenden Variablen L bei v in die Menge der lebenden Variablen bei u überführt. Wir berechnen die liveness Information durch ausführen des Program mit einer abstrakten Semantik: Idee: Ersetze jedes Anweisung si einer Kante k = (u, si , v) durch eine Funktion [[k]]] , die die Menge der lebenden Variablen L bei v in die Menge der lebenden Variablen bei u überführt. Sei L = 2Vars . Für alle k = (_, s, _) , definiere [[k]]] = [[s]]] wie folgt: [[;]]] L [[Pos(e)]]] L [[x = e;]]] L [[x = M[e];]]] L [[M[e1 ] = e2 ;]]] L 384 / 398 Sei L = 2Vars . Für alle k = (_, s, _) , definiere [[k]]] = [[s]]] wie folgt: = L = [[Neg(e)]]] L = L ∪ Vars(e) = (L\{x}) ∪ Vars(e) = (L\{x}) ∪ Vars(e) = L ∪ Vars(e1 ) ∪ Vars(e2 ) [[;]]] L [[Pos(e)]]] L [[x = e;]]] L [[x = M[e];]]] L [[M[e1 ] = e2 ;]]] L = L = [[Neg(e)]]] L = L ∪ Vars(e) = (L\{x}) ∪ Vars(e) = (L\{x}) ∪ Vars(e) = L ∪ Vars(e1 ) ∪ Vars(e2 ) Mit [[k]]] definieren wir nun die Semantik [[π]]] von Pfaden π = k1 . . . kr : [[π]]] = [[k1 ]]] ◦ . . . ◦ [[kr ]]] 384 / 398 384 / 398 Berechnung der liveness Information Berechnung der liveness Information Wir berechnen die liveness Information durch ausführen des Program mit einer abstrakten Semantik: Idee: Ersetze jedes Anweisung si einer Kante k = (u, si , v) durch eine Funktion [[k]]] , die die Menge der lebenden Variablen L bei v in die Menge der lebenden Variablen bei u überführt. Wir berechnen die liveness Information durch ausführen des Program mit einer abstrakten Semantik: Idee: Ersetze jedes Anweisung si einer Kante k = (u, si , v) durch eine Funktion [[k]]] , die die Menge der lebenden Variablen L bei v in die Menge der lebenden Variablen bei u überführt. Sei L = 2Vars . Für alle k = (_, s, _) , definiere [[k]]] = [[s]]] wie folgt: [[;]]] L [[Pos(e)]]] L [[x = e;]]] L [[x = M[e];]]] L [[M[e1 ] = e2 ;]]] L Sei L = 2Vars . Für alle k = (_, s, _) , definiere [[k]]] = [[s]]] wie folgt: = L = [[Neg(e)]]] L = L ∪ Vars(e) = (L\{x}) ∪ Vars(e) = (L\{x}) ∪ Vars(e) = L ∪ Vars(e1 ) ∪ Vars(e2 ) [[;]]] L [[Pos(e)]]] L [[x = e;]]] L [[x = M[e];]]] L [[M[e1 ] = e2 ;]]] L Mit [[k]]] definieren wir nun die Semantik [[π]]] von Pfaden π = k1 . . . kr : Beispiel mit X = ∅: x = y + 2; 1 Mit [[k]]] definieren wir nun die Semantik [[π]]] von Pfaden π = k1 . . . kr : [[π]]] = [[k1 ]]] ◦ . . . ◦ [[kr ]]] y = 5; 2 Beispiel mit X = ∅: x = y + 2; x = y + 2; M[y] = x; 3 4 = L = [[Neg(e)]]] L = L ∪ Vars(e) = (L\{x}) ∪ Vars(e) = (L\{x}) ∪ Vars(e) = L ∪ Vars(e1 ) ∪ Vars(e2 ) 5 1 [[π]]] = [[k1 ]]] ◦ . . . ◦ [[kr ]]] y = 5; 2 x = y + 2; M[y] = x; 3 4 5 ∅ 384 / 398 Berechnung der liveness Information Berechnung der liveness Information Wir berechnen die liveness Information durch ausführen des Program mit einer abstrakten Semantik: Idee: Ersetze jedes Anweisung si einer Kante k = (u, si , v) durch eine Funktion [[k]]] , die die Menge der lebenden Variablen L bei v in die Menge der lebenden Variablen bei u überführt. Wir berechnen die liveness Information durch ausführen des Program mit einer abstrakten Semantik: Idee: Ersetze jedes Anweisung si einer Kante k = (u, si , v) durch eine Funktion [[k]]] , die die Menge der lebenden Variablen L bei v in die Menge der lebenden Variablen bei u überführt. Sei L = 2Vars . Für alle k = (_, s, _) , definiere [[k]]] = [[s]]] wie folgt: [[;]]] L [[Pos(e)]]] L [[x = e;]]] L [[x = M[e];]]] L [[M[e1 ] = e2 ;]]] L Sei L = 2Vars . Für alle k = (_, s, _) , definiere [[k]]] = [[s]]] wie folgt: = L = [[Neg(e)]]] L = L ∪ Vars(e) = (L\{x}) ∪ Vars(e) = (L\{x}) ∪ Vars(e) = L ∪ Vars(e1 ) ∪ Vars(e2 ) [[;]]] L [[Pos(e)]]] L [[x = e;]]] L [[x = M[e];]]] L [[M[e1 ] = e2 ;]]] L Mit [[k]]] definieren wir nun die Semantik [[π]]] von Pfaden π = k1 . . . kr : ] Beispiel mit X = ∅: x = y + 2; 1 ] [[π]] = [[k1 ]] ◦ . . . ◦ [[kr ]] y = 5; 2 4 Beispiel mit X = ∅: x = y + 2; 5 {x, y} 1 ∅ [[;]]] L [[Pos(e)]]] L [[x = e;]]] L [[x = M[e];]]] L [[M[e1 ] = e2 ;]]] L Mit [[k]]] definieren wir nun die Semantik [[π]]] von Pfaden π = k1 . . . kr : Beispiel mit X = ∅: x = y + 2; 1 y = 5; 2 ∅ 4 {y} {x, y} Beispiel mit X = ∅: x = y + 2; 5 1 ∅ 5 {x, y} ∅ 384 / 398 = L = [[Neg(e)]]] L = L ∪ Vars(e) = (L\{x}) ∪ Vars(e) = (L\{x}) ∪ Vars(e) = L ∪ Vars(e1 ) ∪ Vars(e2 ) Mit [[k]]] definieren wir nun die Semantik [[π]]] von Pfaden π = k1 . . . kr : ] x = y + 2; M[y] = x; 3 4 {y} Sei L = 2Vars . Für alle k = (_, s, _) , definiere [[k]]] = [[s]]] wie folgt: = L = [[Neg(e)]]] L = L ∪ Vars(e) = (L\{x}) ∪ Vars(e) = (L\{x}) ∪ Vars(e) = L ∪ Vars(e1 ) ∪ Vars(e2 ) [[π]] = [[k1 ]] ◦ . . . ◦ [[kr ]] x = y + 2; M[y] = x; 3 Wir berechnen die liveness Information durch ausführen des Program mit einer abstrakten Semantik: Idee: Ersetze jedes Anweisung si einer Kante k = (u, si , v) durch eine Funktion [[k]]] , die die Menge der lebenden Variablen L bei v in die Menge der lebenden Variablen bei u überführt. Sei L = 2Vars . Für alle k = (_, s, _) , definiere [[k]]] = [[s]]] wie folgt: ] y = 5; 2 Berechnung der liveness Information Wir berechnen die liveness Information durch ausführen des Program mit einer abstrakten Semantik: Idee: Ersetze jedes Anweisung si einer Kante k = (u, si , v) durch eine Funktion [[k]]] , die die Menge der lebenden Variablen L bei v in die Menge der lebenden Variablen bei u überführt. ] [[π]]] = [[k1 ]]] ◦ . . . ◦ [[kr ]]] 384 / 398 Berechnung der liveness Information [[;]]] L [[Pos(e)]]] L [[x = e;]]] L [[x = M[e];]]] L [[M[e1 ] = e2 ;]]] L = L = [[Neg(e)]]] L = L ∪ Vars(e) = (L\{x}) ∪ Vars(e) = (L\{x}) ∪ Vars(e) = L ∪ Vars(e1 ) ∪ Vars(e2 ) Mit [[k]]] definieren wir nun die Semantik [[π]]] von Pfaden π = k1 . . . kr : ] x = y + 2; M[y] = x; 3 384 / 398 384 / 398 {y} [[π]]] = [[k1 ]]] ◦ . . . ◦ [[kr ]]] y = 5; 2 x = y + 2; M[y] = x; 3 ∅ 4 {y} {x, y} 5 ∅ 384 / 398 Lebendige Variablen eines Programms Lebendige Variablen eines Programms Die Menge der lebendigen Variablen bei u ist nun gegeben durch: [ L∗ [u] = {[[π]]] X | π : u →∗ stop} Die Menge der lebendigen Variablen bei u ist nun gegeben durch: [ L∗ [u] = {[[π]]] X | π : u →∗ stop} Intuitiv: die Menge der lebendigen Variablen am bei stop ist X 385 / 398 Lebendige Variablen eines Programms 385 / 398 Lebendige Variablen eines Programms Die Menge der lebendigen Variablen bei u ist nun gegeben durch: [ L∗ [u] = {[[π]]] X | π : u →∗ stop} Die Menge der lebendigen Variablen bei u ist nun gegeben durch: [ L∗ [u] = {[[π]]] X | π : u →∗ stop} Intuitiv: Intuitiv: die Menge der lebendigen Variablen am bei stop ist X die Menge der lebendigen Variablen am bei stop ist X führt ein Pfad von u nach stop auf dem x lebendig ist, so ist x bei u lebendig führt ein Pfad von u nach stop auf dem x lebendig ist, so ist x bei u lebendig es gibt unendlich viele Pfade von u nach stop 385 / 398 Lebendige Variablen eines Programms 385 / 398 Lebendige Variablen eines Programms Die Menge der lebendigen Variablen bei u ist nun gegeben durch: [ L∗ [u] = {[[π]]] X | π : u →∗ stop} Die Menge der lebendigen Variablen bei u ist nun gegeben durch: [ L∗ [u] = {[[π]]] X | π : u →∗ stop} Intuitiv: Intuitiv: die Menge der lebendigen Variablen am bei stop ist X die Menge der lebendigen Variablen am bei stop ist X führt ein Pfad von u nach stop auf dem x lebendig ist, so ist x bei u lebendig führt ein Pfad von u nach stop auf dem x lebendig ist, so ist x bei u lebendig es gibt unendlich viele Pfade von u nach stop es gibt unendlich viele Pfade von u nach stop Berechnung der Mengen L∗ [u]: Berechnung der Mengen L∗ [u]: 1 Stelle Gleichungssystem auf: L[stop] ⊇ X L[u] ⊇ [[k]]] (L[v]) 385 / 398 k = (u, _, v) edge 385 / 398 Lebendige Variablen eines Programms Beispiel der liveness Analyse Beachte: Die Information fließt Rückwärts bei der liveness Analyse. Die Menge der lebendigen Variablen bei u ist nun gegeben durch: [ L∗ [u] = {[[π]]] X | π : u →∗ stop} Beispiel: Fakultätsfunktion Intuitiv: x = M [I ]; die Menge der lebendigen Variablen am bei stop ist X 1 führt ein Pfad von u nach stop auf dem x lebendig ist, so ist x bei u lebendig y = 1; es gibt unendlich viele Pfade von u nach stop Neg(x > 1) Berechnung der Mengen L∗ [u]: 1 2 2 M [R] = y; k = (u, _, v) edge 4 7 Berechnung der Gleichungen propagieren der Mengen von rechts nach links. Da Var endlich ist, finden wir einen Fixpunkt. ⊇ ⊇ ⊇ ⊇ ⊇ ⊇ ⊇ ⊇ (L[1]\{x}) ∪ {I} L[2]\{y} (L[6] ∪ {x}) ∪ (L[3] ∪ {x}) (L[4]\{y}) ∪ {x, y} (L[5]\{x}) ∪ {x} L[2] L[7] ∪ {y, R} ∅ Pos(x > 1) 1 5 2 7 6 2 5 4 3 1 0 3 6 Stelle Gleichungssystem auf: L[stop] ⊇ X L[u] ⊇ [[k]]] (L[v]) L[0] L[1] L[2] L[3] L[4] L[5] L[6] L[7] 0 y = x ∗ y; x = x − 1; 386 / 398 385 / 398 Beispiel der liveness Analyse Beispiel der liveness Analyse Beachte: Die Information fließt Rückwärts bei der liveness Analyse. Beispiel: Fakultätsfunktion 0 x = M [I ]; 1 y = 1; Neg(x > 1) 6 2 ⊇ (L[1]\{x}) ∪ {I} ⊇ L[2]\{y} ⊇ (L[6] ∪ {x}) ∪ (L[3] ∪ {x}) ⊇ (L[4]\{y}) ∪ {x, y} ⊇ (L[5]\{x}) ∪ {x} ⊇ L[2] ⊇ L[7] ∪ {y, R} ⊇ ∅ Pos(x > 1) 3 M [R] = y; 7 L[0] L[1] L[2] L[3] L[4] L[5] L[6] L[7] 4 5 y = x ∗ y; x = x − 1; Beachte: Die Information fließt Rückwärts bei der liveness Analyse. 7 6 2 5 4 3 1 0 1 ∅ {y, R} {x, y, R} {x, y, R} {x, y, R} {x, y, R} {x, R} {I, R} Beispiel: Fakultätsfunktion x = M [I ]; 1 y = 1; Neg(x > 1) 2 L[0] L[1] L[2] L[3] L[4] L[5] L[6] L[7] 0 6 2 Pos(x > 1) 7 6 2 5 4 3 1 0 3 M [R] = y; 7 ⊇ ⊇ ⊇ ⊇ ⊇ ⊇ ⊇ ⊇ 4 5 y = x ∗ y; x = x − 1; 386 / 398 (L[1]\{x}) ∪ {I} L[2]\{y} (L[6] ∪ {x}) ∪ (L[3] ∪ {x}) (L[4]\{y}) ∪ {x, y} (L[5]\{x}) ∪ {x} L[2] L[7] ∪ {y, R} ∅ 1 ∅ {y, R} {x, y, R} {x, y, R} {x, y, R} {x, y, R} {x, R} {I, R} 2 dito 386 / 398 Registerfärbung: Motivation Übersetzung ganzer Programme Betrachte folgendes Program: 0 read(); read(); x = M[A]; y = x + 1; if (y) { z = x · x; M[A] = z; } else { t = −y · y; M[A] = t; } Kapitel 6: Registerfärbung 1 x = M[A]; 2 y = x + 1; Neg (y) 3 Pos (y) 4 6 5 7 t = −y · y; z=x·x M[A] = t; M[A] = z; 8 387 / 398 388 / 398 Registerfärbung: Motivation Registerfärbung: Motivation Betrachte folgendes Program: Betrachte folgendes Program: 0 0 read(); read(); x = M[A]; y = x + 1; if (y) { z = x · x; M[A] = z; } else { t = −y · y; M[A] = t; } read(); read(); x = M[A]; y = x + 1; if (y) { z = x · x; M[A] = z; } else { t = −y · y; M[A] = t; } 1 x = M[A]; 2 y = x + 1; Neg (y) 3 4 Pos (y) 6 t = −y · y; z=x·x 5 7 M[A] = t; M[A] = z; 1 x = M[A]; 2 y = x + 1; Neg (y) 3 Pos (y) 4 6 5 7 t = −y · y; z=x·x M[A] = t; 8 M[A] = z; 8 Das Programm benutzt 5 Variablen (Register). Das Programm benutzt 5 Variablen (Register). Muss der Keller benutzt werden bei einer 4-Register Maschine? 388 / 398 Registerfärbung: Motivation 388 / 398 Registerfärbung: Motivation Betrachte folgendes Program: Betrachte folgendes Program: 0 0 read(); read(); x = M[A]; y = x + 1; if (y) { z = x · x; M[A] = z; } else { t = −y · y; M[A] = t; } read(); read(); R = M[A]; y = R + 1; if (y) { R = R · R; M[A] = R; } else { R = −y · y; M[A] = R; } 1 x = M[A]; 2 y = x + 1; Neg (y) 3 4 Pos (y) 6 t = −y · y; z=x·x 5 7 M[A] = t; M[A] = z; 1 R = M[A]; 2 y = R + 1; Neg (y) 3 Pos (y) 4 6 5 7 R = −y · y; R=R·R M[A] = R; 8 M[A] = R; 8 Das Programm benutzt 5 Variablen (Register). Muss der Keller benutzt werden bei einer 4-Register Maschine? Nein, benutze ein Register R für x, t, z! Das Programm benutzt 5 Variablen (Register). Muss der Keller benutzt werden bei einer 4-Register Maschine? Nein, benutze ein Register R für x, t, z! 388 / 398 Wann reicht ein Register? 388 / 398 Wann reicht ein Register? Achtung: Mehrere Variablen können nur dann in einem Register gespeichert werden, wenn sich deren Lebensbereich (live range) nicht überlappen. Achtung: Mehrere Variablen können nur dann in einem Register gespeichert werden, wenn sich deren Lebensbereich (live range) nicht überlappen. Der Lebensbereich ist definiert durch 0 read(); 1 x = M[A]; 2 y = x + 1; Neg (y) 3 4 Pos (y) 6 t = −y · y; z=x·x 5 7 M[A] = t; L[x] = {u | x ∈ L[u]} 0 8 7 6 5 4 3 2 1 0 read(); L ∅ {A, z} {A, x} {A, t} {A, y} {A, x, y} {A, x} {A} ∅ 1 x = M[A]; 2 y = x + 1; Neg (y) 3 Pos (y) 4 6 5 7 t = −y · y; M[A] = z; z=x·x M[A] = t; 8 8 7 6 5 4 3 2 1 0 L ∅ {A, z} {A, x} {A, t} {A, y} {A, x, y} {A, x} {A} ∅ M[A] = z; 8 389 / 398 389 / 398 Wann reicht ein Register? Wann reicht ein Register? Achtung: Mehrere Variablen können nur dann in einem Register gespeichert werden, wenn sich deren Lebensbereich (live range) nicht überlappen. Der Lebensbereich ist definiert durch L[x] = {u | x ∈ L[u]} 0 read(); 1 2 y = x + 1; 3 Neg (y) Pos (y) 4 y t = −y · y; 6 5 7 t z=x·x M[A] = t; z L[x] = {u | x ∈ L[u]} 0 read(); L ∅ {A, z} {A, x} {A, t} {A, y} {A, x, y} {A, x} {A} ∅ 8 7 6 5 4 3 2 1 0 x = M[A]; x Achtung: Mehrere Variablen können nur dann in einem Register gespeichert werden, wenn sich deren Lebensbereich (live range) nicht überlappen. Der Lebensbereich ist definiert durch 1 live ranges: x = M[A]; 2 x M[A] = z; Pos (y) 4 y t = −y · y; 6 5 7 t A x y t z y = x + 1; 3 Neg (y) z=x·x M[A] = t; {1, . . . , 7} {2, 3, 6} {3, 4} {5} {7} z M[A] = z; 8 8 389 / 398 Interferenz Graph 389 / 398 Interferenz Graph Um herauszufinden, welche Lebensbereiche sich überlappen, konstruieren wir den Interferenz Graphen (interference graph) I = (Vars, EI ) wobei EI = {{x, y} | x 6= y, L[x] ∩ L[y] 6= ∅}. Um herauszufinden, welche Lebensbereiche sich überlappen, konstruieren wir den Interferenz Graphen (interference graph) I = (Vars, EI ) wobei EI = {{x, y} | x 6= y, L[x] ∩ L[y] 6= ∅}. EI enthält eine Kante für x 6= y wenn und nur wenn ein Programmpunk existiert, an dem x, y gleichzeitig lebendig sind. 0 0 read(); read(); 1 1 x = M[A]; x = M[A]; 2 x Neg (y) 2 Pos (y) 4 y t = −y · y; 6 5 7 t x y = x + 1; 3 Neg (y) z=x·x M[A] = t; z Pos (y) 4 y t = −y · y; 6 5 7 t M[A] = z; y = x + 1; 3 z=x·x M[A] = t; 8 z M[A] = z; 8 390 / 398 Interferenz Graph Interferenz Graph Um herauszufinden, welche Lebensbereiche sich überlappen, konstruieren wir den Interferenz Graphen (interference graph) I = (Vars, EI ) wobei EI = {{x, y} | x 6= y, L[x] ∩ L[y] 6= ∅}. Um herauszufinden, welche Lebensbereiche sich überlappen, konstruieren wir den Interferenz Graphen (interference graph) I = (Vars, EI ) wobei EI = {{x, y} | x 6= y, L[x] ∩ L[y] 6= ∅}. EI enthält eine Kante für x 6= y wenn und nur wenn ein Programmpunk existiert, an dem x, y gleichzeitig lebendig sind. interference graph: 0 390 / 398 EI enthält eine Kante für x 6= y wenn und nur wenn ein Programmpunk existiert, an dem x, y gleichzeitig lebendig sind. interference graph: 0 A read(); 1 x = M[A]; y x = M[A]; x 2 x Neg (y) 4 y t = −y · y; t t Pos (y) z Neg (y) 6 5 7 y = x + 1; 3 4 y t = −y · y; z=x·x M[A] = t; y x 2 x y = x + 1; 3 A read(); 1 z t M[A] = z; 6 z=x·x 5 7 M[A] = t; 8 t Pos (y) z M[A] = z; z Variablen, die nicht verbunden sind, können dem gleichen Register zugewiesen werden 8 390 / 398 390 / 398 Interferenz Graph Registerfärbung Um herauszufinden, welche Lebensbereiche sich überlappen, konstruieren wir den Interferenz Graphen (interference graph) I = (Vars, EI ) wobei EI = {{x, y} | x 6= y, L[x] ∩ L[y] 6= ∅}. EI enthält eine Kante für x 6= y wenn und nur wenn ein Programmpunk existiert, an dem x, y gleichzeitig lebendig sind. interference graph: 0 A read(); 1 x = M[A]; y x 2 x Neg (y) y = x + 1; 3 4 y t = −y · y; 6 5 7 t t Pos (y) z=x·x M[A] = t; z M[A] = z; 8 z Variablen, die nicht verbunden sind, können dem gleichen Register zugewiesen werden Gregory J. Chaitin, Sviatoslav Sergeevich Lavrov, University of Maine (1981) Russian Academy of Sciences (1962) Farbe == Register 390 / 398 391 / 398 Registerfärbung: Problemstellung Registerfärbung: Problemstellung Gegeben: ungerichteter Graph (V, E). Gegeben: ungerichteter Graph (V, E). Gesucht: Minimale Färbung (coloring), d.h. eine Abbildung c : V → N mit c(u) 6= c(v) für alle {u, v} ∈ E; Gesucht: Minimale Färbung (coloring), d.h. eine Abbildung c : V → N mit c(u) 6= F c(v) für alle {u, v} ∈ E; wobei {c(u) | u ∈ V} minimal ist! 392 / 398 392 / 398 Registerfärbung: Problemstellung Registerfärbung: Problemstellung Gegeben: ungerichteter Graph (V, E). Gegeben: ungerichteter Graph (V, E). Gesucht: Minimale Färbung (coloring), d.h. eine Abbildung c : V → N mit c(u) 6= F c(v) für alle {u, v} ∈ E; wobei {c(u) | u ∈ V} minimal ist! Gesucht: Minimale Färbung (coloring), d.h. eine Abbildung c : V → N mit c(u) 6= F c(v) für alle {u, v} ∈ E; wobei {c(u) | u ∈ V} minimal ist! Im Beispiel reichen drei Farben, aber: Im Beispiel reichen drei Farben, aber: Im Allgemeinen gibt es keine eindeutige minimale Färbung 392 / 398 392 / 398 Registerfärbung: Problemstellung Registerfärbung: Problemstellung Gegeben: ungerichteter Graph (V, E). Gegeben: ungerichteter Graph (V, E). Gesucht: Minimale Färbung (coloring), d.h. eine Abbildung c : V → N mit c(u) 6= F c(v) für alle {u, v} ∈ E; wobei {c(u) | u ∈ V} minimal ist! Gesucht: Minimale Färbung (coloring), d.h. eine Abbildung c : V → N mit c(u) 6= F c(v) für alle {u, v} ∈ E; wobei {c(u) | u ∈ V} minimal ist! Im Beispiel reichen drei Farben, aber: Im Beispiel reichen drei Farben, aber: Im Allgemeinen gibt es keine eindeutige minimale Färbung Im Allgemeinen gibt es keine eindeutige minimale Färbung Es ist NP-vollständig herauszufinden, ob eine Färbung mit höchstens k Farben existiert Es ist NP-vollständig herauszufinden, ob eine Färbung mit höchstens k Farben existiert ; benutze Heuristiken und/oder beschränke das Problem auf Spezialfälle 392 / 398 Lösungsweg Heuristik 392 / 398 Lösungsweg Heuristik Idee: benutze eine greedy Heuristik: Idee: benutze eine greedy Heuristik: starte irgendwo mit Farbe 1 393 / 398 Lösungsweg Heuristik 393 / 398 Lösungsweg Heuristik Idee: benutze eine greedy Heuristik: Idee: benutze eine greedy Heuristik: starte irgendwo mit Farbe 1 starte irgendwo mit Farbe 1 wähle die Farbe mit der kleinsten Nummer die ungleich aller benachbarten Knoten ist, die schon gefärbt sind wähle die Farbe mit der kleinsten Nummer die ungleich aller benachbarten Knoten ist, die schon gefärbt sind hat ein Knoten eine Farbe, färbe alle Nachbarn, die noch keine Farbe haben 393 / 398 393 / 398 Lösungsweg Heuristik Lösungsweg Heuristik Idee: benutze eine greedy Heuristik: Idee: benutze eine greedy Heuristik: starte irgendwo mit Farbe 1 starte irgendwo mit Farbe 1 wähle die Farbe mit der kleinsten Nummer die ungleich aller benachbarten Knoten ist, die schon gefärbt sind wähle die Farbe mit der kleinsten Nummer die ungleich aller benachbarten Knoten ist, die schon gefärbt sind hat ein Knoten eine Farbe, färbe alle Nachbarn, die noch keine Farbe haben hat ein Knoten eine Farbe, färbe alle Nachbarn, die noch keine Farbe haben betrachte alle Konten nacheinander betrachte alle Konten nacheinander forall (v ∈ V) c[v] = 0; forall (v ∈ V) color (v); void color (v) { if (c[v] 6= 0) return; neighbors = {u ∈ V | {u, v} ∈ E}; c[v] = {k > 0 | ∀ u ∈ neighbors : k 6= c(u)}; forall (u ∈ neighbors) if (c(u) = 0) color (u); } F 393 / 398 Diskussion der greedy Heuristik 393 / 398 Diskussion der greedy Heuristik der Algorithmus ist im Prinzip eine pre-order Tiefensuche der Algorithmus ist im Prinzip eine pre-order Tiefensuche die neue Farbe eines Knotens kann einfach berechnet werden, wenn die Nachbarn nach Farbe sortiert gespeichert werden 394 / 398 Diskussion der greedy Heuristik 394 / 398 Diskussion der greedy Heuristik der Algorithmus ist im Prinzip eine pre-order Tiefensuche der Algorithmus ist im Prinzip eine pre-order Tiefensuche die neue Farbe eines Knotens kann einfach berechnet werden, wenn die Nachbarn nach Farbe sortiert gespeichert werden die neue Farbe eines Knotens kann einfach berechnet werden, wenn die Nachbarn nach Farbe sortiert gespeichert werden theoretisch kann das Resultat beliebig schlechter als die optimale Lösung sein theoretisch kann das Resultat beliebig schlechter als die optimale Lösung sein in der Praxis ist der Algorithmus aber nicht zu schlecht 394 / 398 394 / 398 Diskussion der greedy Heuristik Diskussion der greedy Heuristik der Algorithmus ist im Prinzip eine pre-order Tiefensuche der Algorithmus ist im Prinzip eine pre-order Tiefensuche die neue Farbe eines Knotens kann einfach berechnet werden, wenn die Nachbarn nach Farbe sortiert gespeichert werden die neue Farbe eines Knotens kann einfach berechnet werden, wenn die Nachbarn nach Farbe sortiert gespeichert werden theoretisch kann das Resultat beliebig schlechter als die optimale Lösung sein theoretisch kann das Resultat beliebig schlechter als die optimale Lösung sein in der Praxis ist der Algorithmus aber nicht zu schlecht in der Praxis ist der Algorithmus aber nicht zu schlecht verschiedene Strategien wurden sogar patentiert verschiedene Strategien wurden sogar patentiert Der Algorithmus funktioniert besser, wenn die Lebensbereiche klein sind! 394 / 398 Diskussion der greedy Heuristik 394 / 398 Diskussion der greedy Heuristik der Algorithmus ist im Prinzip eine pre-order Tiefensuche der Algorithmus ist im Prinzip eine pre-order Tiefensuche die neue Farbe eines Knotens kann einfach berechnet werden, wenn die Nachbarn nach Farbe sortiert gespeichert werden die neue Farbe eines Knotens kann einfach berechnet werden, wenn die Nachbarn nach Farbe sortiert gespeichert werden theoretisch kann das Resultat beliebig schlechter als die optimale Lösung sein theoretisch kann das Resultat beliebig schlechter als die optimale Lösung sein in der Praxis ist der Algorithmus aber nicht zu schlecht in der Praxis ist der Algorithmus aber nicht zu schlecht verschiedene Strategien wurden sogar patentiert verschiedene Strategien wurden sogar patentiert Der Algorithmus funktioniert besser, wenn die Lebensbereiche klein sind! Der Algorithmus funktioniert besser, wenn die Lebensbereiche klein sind! Idee: Aufteilung der Lebensbereiche (live range splitting) Idee: Aufteilung der Lebensbereiche (live range splitting) Ausfürung: betrachte zunnächst nur die Aufteilung innheralb von Basisblöcken 394 / 398 394 / 398 Aufteilung von Lebensbereichen in Basisblöcken Beispiel: Betrachte die Registerfärbung des folgenden Blocks: A1 = x + y; M[A1 ] = z; x = x + 1; z = M[A1 ]; t = M[x]; A2 = x + t; M[A2 ] = z; y = M[x]; M[y] = t; L x, y, z x, z x x x, z x, z, t x, z, t x, t y, t Aufteilung von Lebensbereichen in Basisblöcken Beispiel: Betrachte die Registerfärbung des folgenden Blocks: A1 = x + y; M[A1 ] = z; x = x + 1; z = M[A1 ]; t = M[x]; A2 = x + t; M[A2 ] = z; y = M[x]; M[y] = t; 395 / 398 L x, y, z x, z x x x, z x, z, t x, z, t x, t y, t x z y t 395 / 398 Aufteilung von Lebensbereichen in Basisblöcken Beispiel: Betrachte die Registerfärbung des folgenden Blocks: A1 = x + y; M[A1 ] = z; x = x + 1; z = M[A1 ]; t = M[x]; A2 = x + t; M[A2 ] = z; y = M[x]; M[y] = t; L x, y, z x, z x x x, z x, z, t x, z, t x, t y, t Beispiel: Betrachte die Registerfärbung des folgenden Blocks: A1 = x + y; M[A1 ] = z; x1 = x + 1; z1 = M[A1 ]; t = M[x1 ]; A2 = x1 + t; M[A2 ] = z1 ; y1 = M[x1 ]; M[y1 ] = t; x z Aufteilung von Lebensbereichen in Basisblöcken y t L x, y, z x, z x x1 x1 , z1 x1 , z1 , t x1 , z1 , t x1 , t y1 , t x z y t Die Lebensbereiche von x und z können geteilt werden. 395 / 398 Aufteilung von Lebensbereichen in Basisblöcken Beispiel: Betrachte die Registerfärbung des folgenden Blocks: A1 = x + y; M[A1 ] = z; x1 = x + 1; z1 = M[A1 ]; t = M[x1 ]; A2 = x1 + t; M[A2 ] = z1 ; y1 = M[x1 ]; M[y1 ] = t; L x, y, z x, z x x1 x1 , z1 x1 , z1 , t x1 , z1 , t x1 , t y1 , t 395 / 398 Aufteilung von Lebensbereichen in Basisblöcken Beispiel: Betrachte die Registerfärbung des folgenden Blocks: x z A1 = x + y; M[A1 ] = z; x1 = x + 1; z1 = M[A1 ]; t = M[x1 ]; A2 = x1 + t; M[A2 ] = z1 ; y1 = M[x1 ]; M[y1 ] = t; y x1 z1 y1 t Die Lebensbereiche von x und z können geteilt werden. L x, y, z x, z x x1 x1 , z1 x1 , z1 , t x1 , z1 , t x1 , t y1 , t x z y x1 z1 y1 t Die Lebensbereiche von x und z können geteilt werden. Das Aufteilen macht es möglich, x1 und y1 dem gleichen Register zuzuordnen. 395 / 398 Betrachtung von Lebendigkeitsbereichen 395 / 398 Betrachtung von Lebendigkeitsbereichen Weiterführende Beobachtungen auf Basisblöcken: Weiterführende Beobachtungen auf Basisblöcken: Die Interferenzgraphen auf Basisblöcken können auch als Intervalgraphen dargestellt werden: 396 / 398 396 / 398 Betrachtung von Lebendigkeitsbereichen Betrachtung von Lebendigkeitsbereichen Weiterführende Beobachtungen auf Basisblöcken: Die Interferenzgraphen auf Basisblöcken können auch als Intervalgraphen dargestellt werden: Weiterführende Beobachtungen auf Basisblöcken: Die Interferenzgraphen auf Basisblöcken können auch als Intervalgraphen dargestellt werden: Größe der maximalen Clique entspricht Anzahl benötigter Farben Größe der maximalen Clique entspricht Anzahl benötigter Farben Minimale Färbung kann in polynomineller Zeit gefunden werden 396 / 398 Betrachtung von Lebendigkeitsbereichen 396 / 398 Betrachtung von Lebendigkeitsbereichen Weiterführende Beobachtungen auf Basisblöcken: Die Interferenzgraphen auf Basisblöcken können auch als Intervalgraphen dargestellt werden: Weiterführende Beobachtungen auf Basisblöcken: Die Interferenzgraphen auf Basisblöcken können auch als Intervalgraphen dargestellt werden: Größe der maximalen Clique entspricht Anzahl benötigter Farben Minimale Färbung kann in polynomineller Zeit gefunden werden Achtung: dies ist ein Spezialfall: Größe der maximalen Clique entspricht Anzahl benötigter Farben Minimale Färbung kann in polynomineller Zeit gefunden werden Achtung: dies ist ein Spezialfall: nehme an, der Basisblock bildet eine Schleife und 396 / 398 Betrachtung von Lebendigkeitsbereichen 396 / 398 Betrachtung von Lebendigkeitsbereichen Weiterführende Beobachtungen auf Basisblöcken: Die Interferenzgraphen auf Basisblöcken können auch als Intervalgraphen dargestellt werden: Weiterführende Beobachtungen auf Basisblöcken: Die Interferenzgraphen auf Basisblöcken können auch als Intervalgraphen dargestellt werden: Größe der maximalen Clique entspricht Anzahl benötigter Farben Minimale Färbung kann in polynomineller Zeit gefunden werden Achtung: dies ist ein Spezialfall: nehme an, der Basisblock bildet eine Schleife und eine Variable v ist dem Register Ri am Anfang und Rj am Ende zugeordnet Größe der maximalen Clique entspricht Anzahl benötigter Farben Minimale Färbung kann in polynomineller Zeit gefunden werden Achtung: dies ist ein Spezialfall: nehme an, der Basisblock bildet eine Schleife und eine Variable v ist dem Register Ri am Anfang und Rj am Ende zugeordnet benötige Tausch-Operationen (; sub-optimal) 396 / 398 396 / 398 Registerfärbung in der Fakultätsfunktion Registerfärbung in der Fakultätsfunktion int fac(int x) { if (x<=0) then return 1; else return x*fac(x-1); } int fac(int x) { if (x<=0) then return 1; else return x*fac(x-1); } Betrachte: def-use _fac: enter 5 move R1 R−1 move R2 R1 loadc R3 0 leq R2 R2 R3 jumpz R2 _A loadc R2 1 move R0 R2 return jump _B _A: −1 0 1 2 3 4 U D U D U D D U U D U D U _B: move R2 R1 move R3 R1 loadc R4 1 sub R3 R3 R4 move R−1 R3 loadc R3 _fac saveloc R1 R2 mark call R3 restoreloc R1 R2 move R3 R0 mul R2 R2 R3 move R0 R2 return return Betrachte: def-use liveness −1 0 1 2 3 4 U D U D D U U D D U D U U U D U D D U U D D U D U U _fac: enter 5 move R1 R−1 move R2 R1 loadc R3 0 leq R2 R2 R3 jumpz R2 _A loadc R2 1 move R0 R2 return jump _B _A: −1 0 1 2 3 4 > ⊥ > | | | | | | | | > ⊥ > | | > ⊥ ⊥ > ⊥ > ⊥ _B: move R2 R1 move R3 R1 loadc R4 1 sub R3 R3 R4 move R−1 R3 loadc R3 _fac saveloc R1 R2 mark call R3 restoreloc R1 R2 move R3 R0 mul R2 R2 R3 move R0 R2 return return −1 0 1 2 3 4 | | | | | | | | | | | | > | | | | | | ⊥ | | | > | | ⊥ > ⊥ _A: −1 0 1 2 3 4 > ⊥ > | > | | | > ⊥ ⊥ > ⊥ > ⊥ > ⊥ ⊥ > | | > ⊥ ⊥ > ⊥ 397 / 398 Unterteilung der Lebensbereiche verbesserungsfähig: int fac(int x) { if (x<=0) then return 1; else return x*fac(x-1); } enter 5 move R1 R−1 move R0 R1 loadc R−1 0 leq R0 R0 R−1 jumpz R2 _A loadc R2 1 move R0 R2 return jump _B > | | > ⊥ ⊥ > ⊥ Ausblick Betrachte: def-use liveness coloring _fac: | | | | | | | | | | > ⊥ ⊥ | 397 / 398 Registerfärbung in der Fakultätsfunktion > | | | | | | _B: move R2 R1 move R0 R1 loadc R−1 1 sub R0 R0 R−1 move R−1 R0 loadc R3 _fac saveloc R1 R2 mark call R3 restoreloc R1 R2 move R0 R0 mul R2 R2 R0 move R0 R2 return return −1 0 1 2 3 4 > | | > ⊥ ⊥ > ⊥ > | | > | | | | | | | | ⊥ ⊥ > | | ⊥ > ⊥ > ⊥ | | | | | | | | | | | | Betrachte den ganzen CFG einer Funktion > | | | | | | | | | | ⊥ ⊥ > | | ⊥ > ⊥ 397 / 398 Ausblick 398 / 398 Ausblick Unterteilung der Lebensbereiche verbesserungsfähig: Unterteilung der Lebensbereiche verbesserungsfähig: Betrachte den ganzen CFG einer Funktion Betrachte den ganzen CFG einer Funktion Problem: verschiedene Definitionen fliessen in den gleich Basisblock Problem: verschiedene Definitionen fliessen in den gleich Basisblock übersetze Funktionen in eine single static assignment Form 398 / 398 398 / 398 Ausblick Ausblick Unterteilung der Lebensbereiche verbesserungsfähig: Unterteilung der Lebensbereiche verbesserungsfähig: Betrachte den ganzen CFG einer Funktion Betrachte den ganzen CFG einer Funktion Problem: verschiedene Definitionen fliessen in den gleich Basisblock Problem: verschiedene Definitionen fliessen in den gleich Basisblock übersetze Funktionen in eine single static assignment Form übersetze Funktionen in eine single static assignment Form optimale Färbung möglich (allerdings müssen evtl. Register getauscht werden) optimale Färbung möglich (allerdings müssen evtl. Register getauscht werden) ; Vorlesung Programmoptimierung 398 / 398 Ausblick 398 / 398 Ausblick Unterteilung der Lebensbereiche verbesserungsfähig: Unterteilung der Lebensbereiche verbesserungsfähig: Betrachte den ganzen CFG einer Funktion Betrachte den ganzen CFG einer Funktion Problem: verschiedene Definitionen fliessen in den gleich Basisblock Problem: verschiedene Definitionen fliessen in den gleich Basisblock übersetze Funktionen in eine single static assignment Form übersetze Funktionen in eine single static assignment Form optimale Färbung möglich (allerdings müssen evtl. Register getauscht werden) optimale Färbung möglich (allerdings müssen evtl. Register getauscht werden) ; Vorlesung Programmoptimierung liveness-Analyse verbesserungsfähig: ; Vorlesung Programmoptimierung liveness-Analyse verbesserungsfähig: nach x ← y + 1 ist x nur lebendig wenn y lebendig ist nach x ← y + 1 ist x nur lebendig wenn y lebendig ist die Regeln für die liveness-Analyse können verbessert werden 398 / 398 Ausblick 398 / 398 Ausblick Unterteilung der Lebensbereiche verbesserungsfähig: Unterteilung der Lebensbereiche verbesserungsfähig: Betrachte den ganzen CFG einer Funktion Betrachte den ganzen CFG einer Funktion Problem: verschiedene Definitionen fliessen in den gleich Basisblock Problem: verschiedene Definitionen fliessen in den gleich Basisblock übersetze Funktionen in eine single static assignment Form übersetze Funktionen in eine single static assignment Form optimale Färbung möglich (allerdings müssen evtl. Register getauscht werden) optimale Färbung möglich (allerdings müssen evtl. Register getauscht werden) ; Vorlesung Programmoptimierung liveness-Analyse verbesserungsfähig: ; Vorlesung Programmoptimierung liveness-Analyse verbesserungsfähig: nach x ← y + 1 ist x nur lebendig wenn y lebendig ist nach x ← y + 1 ist x nur lebendig wenn y lebendig ist die Regeln für die liveness-Analyse können verbessert werden die Regeln für die liveness-Analyse können verbessert werden saveloc hält Register unnötig am Leben ; Zwischensprache saveloc hält Register unnötig am Leben ; Zwischensprache ; Vorlesung Programmoptimierung 398 / 398 398 / 398 Ausblick Ausblick Unterteilung der Lebensbereiche verbesserungsfähig: Unterteilung der Lebensbereiche verbesserungsfähig: Betrachte den ganzen CFG einer Funktion Betrachte den ganzen CFG einer Funktion Problem: verschiedene Definitionen fliessen in den gleich Basisblock Problem: verschiedene Definitionen fliessen in den gleich Basisblock übersetze Funktionen in eine single static assignment Form übersetze Funktionen in eine single static assignment Form optimale Färbung möglich (allerdings müssen evtl. Register getauscht werden) optimale Färbung möglich (allerdings müssen evtl. Register getauscht werden) ; Vorlesung Programmoptimierung liveness-Analyse verbesserungsfähig: ; Vorlesung Programmoptimierung liveness-Analyse verbesserungsfähig: nach x ← y + 1 ist x nur lebendig wenn y lebendig ist nach x ← y + 1 ist x nur lebendig wenn y lebendig ist die Regeln für die liveness-Analyse können verbessert werden die Regeln für die liveness-Analyse können verbessert werden saveloc hält Register unnötig am Leben ; Zwischensprache saveloc hält Register unnötig am Leben ; Zwischensprache ; Vorlesung Programmoptimierung Wie berechnet man die liveness Mengen zügig? ; Vorlesung Programmoptimierung Wie berechnet man die liveness Mengen zügig? ; Vorlesung Programmoptimierung 398 / 398 398 / 398