Beruflich Dokumente
Kultur Dokumente
Auch wenn Informatiker(-innen) heutzutage selten damit betraut werden, selber einen
Compiler zu entwickeln, ist die Kenntnis der grundlegenden Methoden des Compiler-
baus von immenser Bedeutung. Bei jeder Eingabe, Analyse und/oder Transformation
von Daten können die Techniken des Compilerbaus eingesetzt werden. Sie sollten daher
zum Rüstzeug eines jeden Informatikers gehören.
Die Vorlesung greift in vielen Teilen auf die Vorlesung Compilerbau zurück, die Prof.
Dr. Klaus Indermark an der RWTH Aachen gehalten hat. Kapitel 7 basiert auf dem
Kapitel 5 Übersetzung objektorientierter Sprachen“ des Buches Übersetzerbau von R.
”
Wilhelm und D. Maurer, das im Springer Verlag erschienen ist. Dieses Skript wurde
unter Mitwirkung meines Mitarbeiters Jost Berthold erstellt, dem ich an dieser Stelle
für seine Unterstützung herzlich danke. Ich danke meinem Mitarbeiter Mischa Die-
terle sowie Philipp Legrum, Sascha Feld, Jan Gehlhaar und weiteren Hörerinnen und
Hörern der Vorlesung, die mich auf Druckfehler in der Vorgängerversion dieses Skriptes
aufmerksam gemacht haben.
Auf dem Deckblatt ist Mr Happy abgebildet, nach dem der Haskell-Parsergenerator
Happy benannt wurde.
i
Inhaltsverzeichnis
1 Einleitung 1
1.1 Grundlegende Begriffe . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.2 Struktur eines Compilers bzw. Übersetzungsvorgangs . . . . . . . . . . 3
1.3 Bootstrapping . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
1.3.1 Hochziehen eines Compilers . . . . . . . . . . . . . . . . . . . . 7
1.3.2 Verbesserung der Effizienz . . . . . . . . . . . . . . . . . . . . . 8
1.4 Vom Interpreter zum Compiler-Compiler mit partiellen Auswertern . . 10
1.4.1 Übersetzung durch partielle Auswertung . . . . . . . . . . . . . 10
1.4.2 Erzeugung eines Compilers . . . . . . . . . . . . . . . . . . . . . 11
1.4.3 Erzeugung eines Compiler-Compilers . . . . . . . . . . . . . . . 11
1.5 Aufbau der Vorlesung . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
2 Lexikalische Analyse 13
2.1 Endliche Automaten und reguläre Ausdrücke . . . . . . . . . . . . . . . 14
2.1.1 Reguläre Ausdrücke . . . . . . . . . . . . . . . . . . . . . . . . . 14
2.1.2 Nichtdeterministische endliche Automaten . . . . . . . . . . . . 15
2.1.3 Deterministische Automaten, Potenzmengenkonstruktion . . . . 16
2.1.4 Satz von Kleene . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
2.2 Lexikalische Analyse mit endlichen Automaten . . . . . . . . . . . . . . 18
2.2.1 Principle of longest match . . . . . . . . . . . . . . . . . . . . . 19
2.2.2 Principle of first match . . . . . . . . . . . . . . . . . . . . . . . 20
2.2.3 Berechnung der flm-Analyse . . . . . . . . . . . . . . . . . . . . 20
2.3 Praktische Aspekte der Scannerkonstruktion . . . . . . . . . . . . . . . 22
2.4 Scannergeneratoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
3 Syntaktische Analyse 27
3.1 Kontextfreie Grammatiken und Kellerautomaten . . . . . . . . . . . . . 27
3.1.1 Links-/Rechtsanalyse . . . . . . . . . . . . . . . . . . . . . . . . 29
3.1.2 Der Top-Down-Analyseautomat (TDA) . . . . . . . . . . . . . . 30
3.2 Top-Down-Analyse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
3.2.1 LL(k)-Grammatiken . . . . . . . . . . . . . . . . . . . . . . . . 34
3.2.2 Ein deterministischer LL(1)-TDA . . . . . . . . . . . . . . . . . 38
3.2.3 Eliminierung von Linksrekursion und Linksfaktorisierung . . . . 40
3.2.4 Komplexität der LL(1)-Analyse . . . . . . . . . . . . . . . . . . 41
3.3 Top-Down Analyse mit rekursiven Prozeduren . . . . . . . . . . . . . . 41
3.3.1 Festlegung des Typs der Parserfunktionen . . . . . . . . . . . . 42
3.3.2 Elementare Parserfunktionen . . . . . . . . . . . . . . . . . . . . 42
3.3.3 Parserkombinatoren . . . . . . . . . . . . . . . . . . . . . . . . . 43
3.3.4 Von der Grammatik zum RD-Parser . . . . . . . . . . . . . . . 45
3.3.5 Einbindung von lookahead-Informationen . . . . . . . . . . . . . 46
3.3.6 Die Parser-Kombinator-Bibiothek Parsec . . . . . . . . . . . . . 46
3.4 Bottom-Up-Analyse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
3.4.1 Der Bottom-Up-Analyseautomat . . . . . . . . . . . . . . . . . 50
3.4.2 LR(k)-Grammatiken . . . . . . . . . . . . . . . . . . . . . . . . 53
ii
INHALTSVERZEICHNIS
3.4.3 LR(0)-Analyse . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
3.4.4 Der LR(0)-Analyseautomat . . . . . . . . . . . . . . . . . . . . 57
3.4.5 SLR(1)-Analyse . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
3.4.6 LR(1)- und LALR(1)-Analyse . . . . . . . . . . . . . . . . . . . 61
3.4.7 Mehrdeutige Grammatiken . . . . . . . . . . . . . . . . . . . . . 65
3.4.8 Der Parsergenerator Happy . . . . . . . . . . . . . . . . . . . . 68
3.5 Konkrete und abstrakte Syntax . . . . . . . . . . . . . . . . . . . . . . 69
4 Semantische Analyse 71
4.1 Attributgrammatiken . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
4.2 Zirkularitäten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
4.3 Verfahren zur Lösung von Attributgleichungssystemen . . . . . . . . . . 77
4.3.1 Termersetzung . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
4.3.2 Wertübergabe . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
4.3.3 Uniforme Berechnung . . . . . . . . . . . . . . . . . . . . . . . . 78
4.3.4 Kopplung an die Syntaxanalyse . . . . . . . . . . . . . . . . . . 78
4.4 S-Attributgrammatiken . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
4.5 L-Attributgrammatiken . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
6 Codeoptimierung 114
6.1 Gleitfenster-Optimierungen . . . . . . . . . . . . . . . . . . . . . . . . 114
6.2 Allgemeine Analysetechniken . . . . . . . . . . . . . . . . . . . . . . . . 115
6.3 Drei-Adress-Code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 116
6.4 Basisblockdarstellung und Datenflussgraphen . . . . . . . . . . . . . . . 118
6.5 Lokale Analyse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 121
6.5.1 Lokale Konstantenpropagation — Vorwärtsanalyse . . . . . . . 123
6.5.2 Nutzlose Anweisungen — Rückwärtsanalyse . . . . . . . . . . . 123
6.5.3 Elimination gemeinsamer Teilausdrücke — Vorwärtsanalyse . . 124
6.6 Fallstudie: Code-Optimierung am Beispiel von Quicksort . . . . . . . . 126
6.6.1 Elimination lokaler gemeinsamer Teilausdrücke . . . . . . . . . . 127
6.6.2 Global gemeinsame Teilausdrücke . . . . . . . . . . . . . . . . . 128
6.6.3 Kopienpropagation und Elimination nutzloser Anweisungen . . . 130
iii
6.6.4 Codeverschiebung . . . . . . . . . . . . . . . . . . . . . . . . . . 130
6.6.5 Induktionsvariablen und Operatorreduktion . . . . . . . . . . . 131
6.7 Globale Analyse: Datenflussanalyse . . . . . . . . . . . . . . . . . . . . 132
6.8 Taxonomie von Datenflussproblemen . . . . . . . . . . . . . . . . . . . 134
Literatur 147
iv
1. Einleitung
1 Einleitung
Unter Compilerbau oder auch Übersetzerbau versteht man die Entwicklung von Compi-
lern, d.h. von Übersetzern höherer Programmiersprachen in Maschinencode. Die Vor-
lesung behandelt allgemeine sprachunabhängige Techniken und Methoden des Compi-
lerbaus und ihre theoretischen Grundlagen.
Ein Compiler ist ein Programm zur Übersetzung von Programmen einer
höheren Programmiersprache (Quellprogramme) in äquivalente Programme
einer Zielsprache (Zielprogramme).
Unter einer höheren Programmiersprache versteht man dabei imperative und objekt-
orientierte Programmiersprachen oder deklarative, d.h. funktionale oder Logik- Pro-
grammiersprachen. In den imperativen Programmiersprachen bestehen Programme aus
Wertzuweisungen, Kontrollstrukturen, Datenstrukturen und Programmstrukturen wie
Modulen oder Klassen. In deklarativen Sprachen bilden Ausdrücke, Funktionen und
Prädikate, Rekursion und algebraische Strukturen zentrale Elemente. Der Sprachtyp
der Quellsprache spielt vor allem in den “späten Phasen” der Übersetzung, d.h. bei der
Codegenerierung eine zentrale Rolle, während die Techniken und Methoden der frühen
Übersetzungsphasen, bei denen die Quellsprache erkannt und analysiert wird, weitge-
hend sprachunabhängig sind. Hier geht wesentlich die Theorie der formalen Sprachen
(nach Chomsky) und die pragmatische Umsetzung von diesbezüglichen Aufgaben in
Algorithmen ein. Dies erklärt auch, warum Compilertechniken in vielen Anwendungen
von Bedeutung sind, in denen Beschreibungssprachen für spezielle Zwecke gebraucht
werden, etwa Dokumentbeschreibungssprachen wie LATEX oder HTML oder Datenbank-
anfragesprachen wie SQL, um nur zwei Beispiele zu nennen. Die Analysetechniken des
Compilerbaus und die zugehörigen Werkzeuge können hier eine wichtige Unterstützung
bieten.
Die Zielsprache eines Compilers kann prinzipiell eine beliebige Programmiersprache
sein. Meist handelt es sich allerdings um eine Maschinensprache (native code), eine
Assemblersprache oder eine systemnahe Sprache wie etwa C. Bei sogenannten Cross-
Compilern ist die Zielsprache eine andere höhere Programmiersprache, wie etwa Java.
Verschiedene Aspekte von Programmiersprachen spielen bei der Übersetzung eine Rol-
le:
1
1. Einleitung
eine intuitive Vorstellung über den Ablauf der Auswertung, während etwa die
Formulierung
2
1.2 Struktur eines Compilers bzw. Übersetzungsvorgangs
3
1. Einleitung
Programm in Quellsprache
lexikalische Analyse
S ?
y
m Syntaxanalyse
b
o
l ?
t
a semantische Analyse
b
e ⇑ Analyse
l
l ⇓ Synthese
e ?
Zwischencode-Erzgg.
⇑ Frontend
⇓ Backend
?
Codeoptimierung
Codeerzeugung
Programm in Zielsprache
4
1.2 Struktur eines Compilers bzw. Übersetzungsvorgangs
Abbildung 2: Syntaxbaum
a := 2;
b := a*a+1;
Der Scanner (lexikalische Analyse) erkennt aus dieser Eingabe (mit den hier dargestell-
ten Zeilenwechseln) zunächst die folgenden lexikalischen Einheiten, die sogenannten
Symbole:
id(‘‘var’’) sep id(‘‘a’’) comma id(‘‘b’’) colon sep id(‘‘int’’) sem sep sep
id(‘‘a’’) sep becomes sep num(‘‘2’’) sem sep
id(‘‘b’’) sep becomes sep id(‘‘a’’) mul id(‘‘a’’) add num(‘‘1’’) sem sep
Außerdem trägt der Scanner in die Symboltabelle die Bezeichner a und b etwa an den
Positionen 1 und 2 ein und kodiert die Bezeichner durch Verweise auf die Symbolta-
belle. Reservierte Symbole und eventuelle Compiler-Direktiven werden erkannt sowie
Leerzeichen und Kommentare eliminiert. Der für diese Aufgaben zuständige Teil des
Scanners wird Sieber genannt. Als Ausgabe des Scanners ergibt sich etwa die Symbol-
folge:
var id(1) comma id(2) colon int sem id(1) becomes num(‘‘2’’) sem
id(2) becomes id(1) mul id(1) add num(‘‘1’’) sem
5
1. Einleitung
Die semantische Analyse überprüft die statischen semantischen Eigenschaften des Pro-
gramms wie Typkorrektheit, Deklariertheit der Bezeichner. Gegebenenfalls werden ver-
schiedene Analysen durchgeführt, etwa eine Abhängigkeitsanalyse oder eine Typinfe-
renz.
Im Beispiel liefert der Declarations-Unterbaum Informationen über deklarierte Bezeich-
ner und deren Typ:
und der Adresszuordnung id(1) 7→ 0, id(2) 7→ 1 könnte der Zwischencode dann etwa
wie folgt aussehen:
LOADI R1 2 ; Zahl 2
STORE 0 R1 ; a := 2
LOAD R1 0 ; unnoetig! (Optimierung)
MUL R1 0 ; a*a
ADDI R1 1 ; a*a+1 eventuell billiger: INC R1
STORE 1 R1 ; b := a*a+1
/
Compiler bezeichnet man je nach Anzahl der Läufe, die das Programm durch das
Quellprogramm vornimmt, als One-Pass- bzw. n-Pass-Compiler.
6
1.3 Bootstrapping
1.3 Bootstrapping
Unter Bootstrapping versteht man die Benutzung eines Compilers zur eigenen Überset-
zung, also selbstreferenziell. Voraussetzung ist, dass der Compiler in der Quellsprache
geschrieben ist (Implementierungssprache = Quellsprache). Bootstrapping dient zum
einen der Erweiterung von eingeschränkten Compilern für Kernsprachen auf den vollen
Sprachumfang, zum anderen dem Erzeugen effizienterer Compiler oder neuer Compi-
lerversionen. Zur Darstellung werden sog. T-Diagramme benutzt (Q=Quellsprache, I
= Implementierungssprache, Z = Zielsprache).
Q → Z
Q → M
Q− → M
Q−
Q− ist eine einfache Teilsprache von Q, in der aber der Compiler ausgedrückt
werden kann.
2. Ein erster Compiler von Q− nach M wird von Hand oder mithilfe von 1) durch
Übertragung in eine Sprache mit existierendem Compiler auf M (beispielsweise
C) erzeugt werden.
Q− → M
7
1. Einleitung
3. Das Ergebnis von 1 wird nun mit 2 übersetzt, es ergibt den ersten vollwertigen
Q− -Compiler.
Q− → M Q− → M
Q− Q− → M M
Q → M Q → M
Q− Q− → M M
Q → M Q → M
Q Q → M M
6. Der Compiler kann nun weiterentwickelt und anschließend mit der vorigen Version
übersetzt werden
Q → Mslow Q → Mslow
Q Mslow
Allerdings sind sowohl die Compilation als auch der erzeugte Code langsam, was durch
den Index slow angezeigt wird. Ziel ist die Entwicklung eines schnellen Compilers, der
auch schnellen Code erzeugt:
8
1.3 Bootstrapping
Q → Mfast
Mfast
Q → Mfast
Q → Mfast Q → Mfast
Q Q → Mslow Mslow
Mslow
3. Zum Abschluss wird mit dem erzeugten Compiler nochmals übersetzt, um einen
schnellen Compiler zu bekommen.
Q → Mfast Q → Mfast
Q Q → Mfast Mfast
Mslow
Dies führt zu einem schnellen Compiler, der zudem schnellen Code erzeugt.
9
1. Einleitung
10
1.4 Vom Interpreter zum Compiler-Compiler mit partiellen Auswertern
Damit folgt, dass der spezialisierte Interpreter intp spez ein zu dem Q-Programm p äqui-
valentes I-Programm ist. Die partielle Auswertung des Interpreters entspricht somit
einem Compilationsvorgang von Q nach I.
Durch die Selbstanwendung des partiellen Auswerters gelingt es damit, einen Interpre-
ter in einen Compiler umzuwandeln. Den Prozess der Selbstanwendung kann man noch
weiter fortsetzen.
Das Problem dabei ist, dass der partielle Auswerter I-Programme verarbeiten und
gleichzeitig in I implementiert sein muss, ebenso wie der Interpreter. Man muss al-
so entweder bereits einen funktionierenden I-Compiler besitzen, oder I muss sowohl
ausführbar als auch für die Implementierung geeignet sein. Die meisten existierenden
partiellen Auswerter erfüllen diese Anforderungen nicht. Insbesondere die Selbstappli-
kation ist schwierig zu realisieren.
11
1. Einleitung
Die bedarfgesteuerte Auswertung von Haskell hat zudem den Vorteil, dass trotz un-
abhängiger Entwicklung und Programmierung der einzelnen Compilerteile automatisch
eine verzahnte Abarbeitung der verschiedenen Compilerphasen erfolgt.
12
2. Lexikalische Analyse
2 Lexikalische Analyse
Die lexikalische Analyse wird durch einen sogenannten Scanner durchgeführt. Dieses
Programm erhält als Eingabe das Quellprogramm als Zeichenfolge p ∈ Σ∗ über dem
Zeichenvorrat Σ (beispielsweise ASCII oder Unicode). Aufgabe der lexikalischen Ana-
lyse ist das Zerlegen der Folge von Eingabezeichen (Q-Programm) in eine Symbolfolge,
die hinterher einer syntaktischen Analyse unterzogen wird.
Die Elemente von Σ bzw. Char heißen lexikalische Atome. Symbole sind syntaktische
Atome, d.h. spezielle Folgen lexikalischer Atome. Ein Programm p besitzt aufgrund
der Pragmatik der Programmiersprache eine lexikalische Struktur, die sogenannte Mi-
krosyntax. Um die Lesbarkeit und Wartbarkeit von Programmen zu verbessern, kann
die natürliche Sprache für Bezeichner und Schlüsselwörter verwendet werden, für Zah-
len und Formeln kann die übliche mathematische Notation eingesetzt werden. Mittels
Leerzeichen, Zeilenwechsel und Einrücken kann der Programmtext strukturiert werden.
Kommentare dienen zur Erläuterung von Programmteilen. All diese pragmatischen
Aspekte sind für die Semantik und die Übersetzung von Programmen irrelevant. Diese
ist meist syntaxorientiert, d.h. sie folgt dem hierarchischen Programmaufbau.
Zu den Teilaufgaben eines Scanners zählen
1. Zerlegung des Quellprogramms p ∈ Σ∗ in eine Folge von Lexemen, das sind Folgen
lexikalischer Atome, die syntaktische Atome (Symbole) darstellen.
Für die syntaktische Analyse ist der Unterschied von Lexemen oft ohne Bedeutung.
Zum Beispiel müssen Bezeichner nicht unterschieden werden. Lexeme werden daher zu
Symbolklassen oder Token zusammengefasst. Die syntaktische Analyse bearbeitet eine
Tokenfolge. Für die semantische Analyse und Codegenerierung werden Symbole durch
zusätzliche Attribute identifiziert:
Die lexikalische Analyse hat demnach die Aufgabe, ein Programm in eine Folge von
Lexemen zu zerlegen und diese Folge in eine Folge von Symbolen zu transformieren.
Typische Symbolklassen sind beispielsweise:
• Bezeichner: Folge von Buchstaben und Ziffern, die mit einem Buchstaben beginnt
und kein Schlüsselwort ist, zur Bezeichnung von Konstanten, Variablen, Typen,
Funktionen, Klassen. . .
13
2. Lexikalische Analyse
14
2.1 Endliche Automaten und reguläre Ausdrücke
α = (ε | − | +) · (1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9) · (0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9)∗ | 0
beschreibt die reguläre Sprache der ganzzahligen Konstanten mit oder ohne Vorzeichen.
/
δ : Q × (Σ ∪ {ε}) −→ ℘(Q).
Der Automat hat einen Startzustand q0 sowie bestimmte Endzustände, die in F zusam-
mengefasst werden. Die Erkennung eines Worts besteht darin, ausgehend vom Start-
zustand mit den durch δ definierten Transitionen und der Eingabe des Wortes einen
der Endzustände zu erreichen. Zur Präzisierung dieses Erkennungsprozesses definieren
wir weiter:
Definition 3 (Epsilon-Hülle, von einem NFA erkannte Sprache) Sei T ⊆ Q,
dann ist die ε-Hülle von T, in Zeichen ε̂(T ), die kleinste Menge, für die gilt:
1. T ⊆ ε̂(T )
2. Mit q ∈ ε̂(T ) gilt auch δ(q, ε) ⊆ ε̂(T )
Die Funktion δ kann damit wie folgt zu der erweiterten Transitionsfunktion
δ̄ : ℘(Q) × Σ∗ −→ ℘(Q)
fortgesetzt werden:
15
2. Lexikalische Analyse
1. δ(T, ε) = ε̂(T )
[
2. δ(T, wa) = ε̂ δ(q, a)
q∈δ̄(T,w)
0
0...9
ε, +, − 1...9
1 3 4
δ :: Q × Σ → Q
und die erweiterte Transitionsfunktion δ̄ hat für DFAs den Typ Q × Σ∗ → Q. Somit
gilt für DFAs
L(A) := w ∈ Σ∗ | δ̄(q0 , w) ∈ F .
Ein DFA löst das Wortproblem in Linearzeit.
16
2.1 Endliche Automaten und reguläre Ausdrücke
Qd = δ̄({q0 }, w) | w ∈ Σ∗ ⊆ ℘(Q)
q0d = ε̂({q0 })
F d = T ∈ Qd | T ∩ F 6= ∅
sowie δ d : Qd × Σ → Qd
δ d (T, a) = δ̄(T, a) für alle T ∈ Qd
Beispiel: Zu dem oben angegebenen NFA erhält man mit der Potenzmengenkon-
struktion den folgenden DFA:
{1, 3} 0 {2} Σ
∅ Σ
1...9 +, −
+, −
0, +, −
/
Der Beweis wird konstruktiv und induktiv über den Aufbau der regulären Ausdrücke
geführt. Wir nehmen an, dass wir stets Automaten der Gestalt
konstruieren, also mit einem Startzustand, in den keine Transition zurückführt, und
mit genau einem Endzustand, aus dem keine Transition wieder herausführt.
Seien a ∈ Σ, α, β ∈ RA(Σ).
A(Λ)
17
2. Lexikalische Analyse
A(a)
a
A(α|β) Wir können den Automaten A(α|β) mit einigen ε-übergängen und neuem Start-
und Endzustand konstruieren:
ε β ε
ε ε
α
A(α · β)
ε α ε β ε
ε
A(α∗ )
ε α ε
Das Wortproblem lässt sich damit “kanonisch” mit der sog. DFA-Methode lösen:
α ∈ RA(Σ) 7→(1) A(α) ∈ NF A(Σ) 7→(2) Ad (α) ∈ DF A(Σ)
Nach dem Satz von Kleene wird zum regulären Ausdruck ein NFA konstruiert, der
anschließend in einen äquivalenten DFA umgewandelt wird. Der DFA kann noch mi-
nimiert werden (hier nicht besprochen, funktioniert durch sukzessives Verschmelzen
von äquivalenten Zuständen). Der Platzaufwand für den NFA ist O(|α|), denn der
Automat hat genau diese Anzahl Zustände. Die Potenzmengenkonstruktion bildet im
schlechtestens Fall O(2|α| ) Zustände, danach entscheidet aber der DFA in linearer Zeit,
ob w ∈ [[α]] oder nicht (Aufwand O(|w|) mit Eingabe w). Minimierung bringt keine
weiteren Zeitvorteile, höchstens einen Platzgewinn.
Die sog. NFA-Methode verzichtet auf die vollständige Potenzmengenkonstruktion. Dies
führt zu einer Verbesserung des Platzbedarfs auf O(| α |), aber zu einer Erhöhung
des Erkennungsaufwands. Die Potenzmengenkonstruktion wird (quasi “nach Bedarf”)
nur für den Lauf des Eingabewortes w durch A ∈ NF A(Σ)“ durchgeführt, indem
”
δ̄({q0 }, w)) (sukzessiv) berechnet wird. Der Zeitbedarf ergibt sich dabei zu O(|α|×|w|),
denn für jedes Zeichen von w muss ein Bild unter δ̄ berechnet werden (Aufwand O(|α|)).
18
2.2 Lexikalische Analyse mit endlichen Automaten
Hier existieren nicht nur einer, sondern viele reguläre Ausdrücke, die jeweils ein Token
charakterisieren.
Bei Eingabe von w soll der Scanner also v ausgeben, was die lexikalische Struktur von w
in Symbolen wiedergibt (daneben wird die Symboltabelle aufgebaut). Im allgemeinen
sind weder die Zerlegung von w noch die Analyse von w eindeutig bestimmt, weshalb
man auf bestimmte Konventionen zurückgreift, um Eindeutigkeit zu forcieren.
Seien stets w, ∆, {α1 , . . . , αn } gegeben wie in Definition 5.
w = w1 · · · wj · x · y ∧ wj ∈ [[αp ]] ∧ wj · x ∈ [[αq ]] ⇒ x = ε.
Das bedeutet, dass kein wj mit Folgezeichen anders zerlegt werden könnte. Die einzige
Zeichenkette, um die wj verlängert werden kann, um wiederum ein αq zu finden, dem
die verlängerte Zeichenkette zugeordnet werden kann, ist das leere Wort ε.
Zum Beweis betrachte man zwei lm-Zerlegungen. Falls diese sich an einer (ersten) Stelle
unterscheiden, ergibt sich ein Widerspruch zur Definition. Dadurch wird die Zerlegung
von w eindeutig, sofern sie überhaupt existiert.
lm-Zerlegungen sind durch Anwendungen motiviert: Anfangsstücke von Bezeichnern
sind beispielsweise ebenfalls Bezeichner.
1
Der Scanner “frisst” (engl. munch: mampfen, fressen) immer eine maximale Zahl von Eingabezei-
chen.
19
2. Lexikalische Analyse
Für das First-Match“-Prinzip wird die Menge F der Endzustände in disjunkte Teile
”
zerlegt, so dass stets klar ist, welches Symbol erkannt wurde.
n
]
F = F (k) mit
k=1
(1) (n) (i) (i)
(q , . . . , q )∈F ⇔ q ∈ Fi und q (j) 6∈ Fj für alle 1 ≤ j < i
20
2.2 Lexikalische Analyse mit endlichen Automaten
Die Priorität der Symbole wird also in der Zerlegung wiedergegeben, indem stets das
Symbol mit der höchsten Priorität über die Mengenzugehörigkeit des Endzustands
entscheidet.
21
2. Lexikalische Analyse
0
(S, uaq w, σ)
falls q 0 ∈ P \ F
Lookahead-Modus: (S, uqaw, σ) ` (Sj , q 0 w, σ) falls q 0 ∈ F (j)
(N, q uaw, σ · S)
0 falls q 0 6∈ P
Der Aufwand des Backtracking-Automaten zur Analyse von w ist im schlechtesten Fall
O(|w|2), weil im Falle P = Q stets bis zum Ende der Eingabe gelesen werden muss,
bevor ein Symbol definitiv erkannt ist.
Beispiel: Für die regulären Ausdrücke α1 = abc und α2 = (abc)∗ d und die Eingabe
w = (abc)m bestimmt der Backtrack-Automat die flm-Analyse S1m . Hierzu wird aller-
dings im Lookahead-Modus nach dem Erkennen jedes Treffers die gesamte Resteingabe
bis zum Ende gelesen, um zu überprüfen, ob nicht ein längerer Treffer existiert. /
Zur Verbesserung der Laufzeit kann man mit der sog. Tabularmethode2 arbeiten, welche
die erfolglosen Lookahead-Versuche protokolliert und abkürzt. Es wird eine Markie-
”
rungsmatrix“ aus Zustand und Eingabeposition aufgebaut, in der festgehalten wird,
wann ein Lookahead bis zum Ende zu keinem Ergebnis führt. Falls diese Situation er-
neut auftritt, wird der Lookahead sofort abgebrochen, wodurch der Scanner insgesamt
eine in der Eingabelänge lineare Laufzeit erreicht.
22
2.4 Scannergeneratoren
Ein Sieber dient der Beseitigung überflüssiger Tokens wie Blanks, Kommentare etc.
Symbolklassen mit mehreren Lexemen erfordern zusätzlich eine Attributberechnung
mit erneutem Lesen des Eingabestrings. Zu Dezimalzahlen muss etwa der Zahlwert
berechnet werden. Bezeichner müssen in die Symboltabelle eingetragen werden. Dabei
ist es notwendig die Gleichheit von Bezeichnern zu überprüfen. Als Attribut wird meist
der Symboltabellenindex des Bezeichners gewählt.
2.4 Scannergeneratoren
Das in den meisten Fällen eingesetzte Verfahren ist der tabellengesteuerte Scanner,
dessen Tabelle aus einer Definition der Mikrosyntax mit regulären Ausdrücken auto-
matisch erstellt wird. Die Tabelle und ein generischer Scanner-Treiber liefern zusammen
den gewünschten Scanner und müssen nicht neu implementiert werden, wenn sich die
Sprache geringfügig ändert.
23
2. Lexikalische Analyse
Alle Werkzeuge arbeiten so, dass sie aus einer Definition der Symbolklassen (und einem
generischen Standardscanner) einen Scanner in einer bestimmten Programmiersprache
erzeugen, wobei direkte Anweisungen in der Programmiersprache eingebaut werden, die
ausgeführt werden, sobald ein Symbol erkannt wurde (z.B. geschieht das Umwandeln
einer numerischen Konstante in ihren Zahlwert bereits hier).
reguläre Scanner
Sprachbeschreibung −→ Scannergenerator −→ (tabellengesteuerter
allgemeiner Treiber)
Bekannt ist das Unix/Linux-Werkzeug lex bzw. die GNU-Version flex, welches C-Code
erzeugt. Für Haskell-Code existiert ein ähnliches Werkzeug, alex, das (abgesehen vom
eingebetteten Code) die gleichen Eingabedateien verarbeitet. Die Funktionalität von
lex und alex sind vergleichbar. Die reguläre Sprachdefinition wird in einen DFA trans-
formiert, wobei ein NFA als Zwischenprodukt aufgebaut wird. Aus dem DFA werden
anschließend Tabellen zur Steuerung des allgemeinen tabellengesteuerten Scanners mit
Einbettung der benutzerdefinierten Routinen.
Die Scannererzeugung mit einem Scannergenerator erfolgt in den folgenden Schritten:
reguläre
Sprachbeschreibung −→ lex/alex −→ lex.yy.c/tokens.hs
tokens.x
3. Der erzeugte Scanner kann im einfachsten Fall eine Zeichenfolge in eine Symbol-
folge übersetzen:
Die in Abbildung 3 angegebene Datei spezifiziert einen einfachen Scanner für Zahlen-
folgen und Bezeichner. Leerzeichenfolgen werden herausgesiebt. Die Spezifikation hat
den folgenden grundsätzlichen Aufbau:
3
oder ein Object-Modul, das in anderen Programmteilen verwendet werden kann
24
2.4 Scannergeneratoren
{
module Main where
}
%wrapper "basic"
tokens :-
$white+ ;
$digit+ { \s -> Int (read s) }
$alpha [$alpha $digit \_ \’]* { \s -> Var s }
{
data Token = Int Int | Var String
deriving (Eq,Show)
Wrapper: Alex stellt verschiedene Scannertypen zur Verfügung, die über die Wrap-
per-Deklaration vom Benutzer ausgewählt werden können. Je nach Wrapper-
Wahl wird verschiedener Scannercode durch Alex erzeugt. Die folgenden Wrapper
stehen zur Verfügung:
• Die basic Variante erzeugt einen Scanner mit dem Typ String -> [Token].
Dabei hängt der Resultattyp Token von den Aktionen ab, die in der Spezi-
fikation den regulären Ausdrücken zugeordnet werden. Diese sollten im Fall
des basic wrappers den Typ String -> Token haben.
• Die posn Variante erweitert den basic Scanner dadurch, dass die Zeilen-
und Spaltennummern der erkannten Token mit verwaltet werden. Die den
25
2. Lexikalische Analyse
• Am flexibelsten ist der monad Wrapper, der über eine Zustandsmonade ver-
schiedene Zusatzinformationen verwaltet.
Reguläre Ausdrücke: Den zentralen Teil der Spezifikation bilden die regulären Aus-
drücke und die ihnen zugeordneten Aktionen. Nach der Eingangszeile identifier
:- folgen endliche viele Regeln der Form
Der Aufruf alex tokens.x erzeugt eine Datei tokens.hs , die mit einem Haskell-
Compiler in ausführbaren Code übersetzt werden kann. Mit der Option -i erzeugt
Alex außerdem eine Datei tokens.info, in der der erzeugte DFA durch seine Zu-
standsübergänge beschrieben wird.
Der Alex-Scannergenerator bietet eine Reihe von weiteren Möglichkeiten der Zeichen-
kettenverarbeitung. über Links- und Rechtskontextangaben und sogenannte Startcodes
kann die Zuordnung einer Zeichenkette zu einer Symbolklasse vom Kontext der Zeichen-
kette abhängig gemacht werden. Interessierte Leserinnen und Leser seien für weitere
Informationen zu Alex auf die Webseite http://www.haskell.org/alex verwiesen.
26
3. Syntaktische Analyse
3 Syntaktische Analyse
Die Aufgabe der Syntaxanalyse ist die Zerlegung der Symbolfolge, die der Scanner
ausgibt, in syntaktische Einheiten. Falls eine solche Zerlegung nicht möglich ist, sollen
die (dann vorhandenen) syntaktischen Fehler ausgegeben werden. Syntaktische Ein-
heiten sind etwa Variablen, Ausdrücke, Funktionsdefinitionen, Typdeklarationen oder
Anweisungen. Der sogenannte Parser erzeugt wegen der Schachtelung syntaktischer
Einheiten aus der linearen Symbolfolge, die er als Eingabe erhält, einen Syntaxbaum
(oder er erkennt einen Syntaxfehler).
Die Syntax wird mit Hilfe von kontextfreien Grammatiken beschrieben und mit Keller-
automaten (Stackmaschinen) analysiert. Das Problem ist insbesondere die effiziente de-
terministische Simulation der im allgemeinen nichtdeterministischen Konzepte. Im all-
gemeinen Fall können beliebige kontextfreie Sprachen mit dem Cocke-Younger-Kasami
(CYK)-Verfahren analysiert werden. Dieses hat eine Platzkomplexität von O(n2 ) und
eine Zeitkomplexität von O(n3 ) in der Länge n des Eingabewortes. Ein Compiler sollte
aber idealerweise in linearer Zeit arbeiten können, weshalb man sich auf bestimmte
Klassen von Grammatiken beschränkt und einen lookahead benutzt, um einen linearen
Platz- und Zeitaufwand für die syntaktische Analyse zu garantieren.
Grundsätzlich unterscheidet man zwischen der
Top-Down-Analyse, bei der der Syntaxbaum von der Wurzel zu den Blättern in
Form einer Linksanalyse konstruiert wird, und der
Bottom-Up-Analyse, bei der der Syntaxbaum von den Blättern zur Wurzel hin
aufgebaut und dabei eine gespiegelte Rechtsanalyse erzeugt wird.
Links- und Rechtsanalysen sind lineare Darstellungen eines Ableitungsbaums, die durch
bestimmte Traversierungsvorschriften ermittelt werden. Sie bestehen aus Produktio-
nenfolgen. Bei einer Linksanalyse wird der Syntaxbaum top-down depth-first left-to-
right (also in Präordnung von links nach rechts) durchlaufen, während er bei einer
Rechtsanalyse top-down von rechts nach links traversiert wird.
27
3. Syntaktische Analyse
Die Sprache dieser Grammatik ist L(G) = {an bn | n ∈ N}. Eine mögliche Ableitung
ist:
S ⇒ aSb ⇒ aaSbb ⇒ aaaSbbb ⇒ aaaεbbb = aaabbb
Dies ist sowohl eine Links- als auch eine Rechtsableitung, da wir jeweils nur ein Non-
terminalsymbol in jeder Satzform erhalten. Eine Ableitung wird meist in einem Ablei-
tungsbaum dargestellt, der die syntaktische Struktur des abgeleiteten Wortes zeigt:
S
@ S
@@ A
A
a S b A
@ S
L A
@@ L A
a S b L A
@ S L A
@@ B L A
B A
a S b B L
S L A
B L A
ε a a a ε b b b
28
3.1 Kontextfreie Grammatiken und Kellerautomaten
Hier ist derselbe Ableitungsbaum in zwei alternativen Darstellungen gezeigt. Wenn der
Ableitungsbaum für ein Terminalwort eindeutig ist, sind es auch die Links- und Rechts-
ableitung, während eine Ableitung als linearisierte Darstellung eines Ableitungsbaums
aufgrund verschiedener Baumtraversierungen nicht eindeutig ist. /
Definition 10 (Eindeutigkeit/Mehrdeutigkeit)
Eine Grammatik G heißt eindeutig, falls es zu jedem w ∈ L(G) genau einen Ablei-
tungsbaum bzw. genau eine Linksableitung bzw. genau eine Rechtsableitung gibt.
G heißt mehrdeutig, falls G nicht eindeutig ist.
3.1.1 Links-/Rechtsanalyse
Zur Darstellung der Links- und Rechtsableitung bzw. der Links- und Rechtsanalyse
eines Wortes werden häufig Nummernfolgen verwendet.
Definition 11 (Links-/Rechtsanalyse)
Sei G = (N, Σ, S, P ) eine kontextfreie Grammatik mit |P | = p ∈ N. Sei [p] := {1, . . . , p}
und π : [p] → P eine Aufzählung von P , d.h. eine bijektive Abbildung. Es sei πi :=
π(i) ∈ P .
i i
Wir schreiben wAα ⇒l wγα bzw. αAw ⇒r αγw, falls mit der Produktion π(i) = A →
γ abgeleitet wurde.
Weiterhin sei für z = i1 . . . ik ∈ [p]+ (k ≥ 1)
z i i i
α ⇒l β :⇔ α ⇒1 l α1 ⇒2 l . . . ⇒k l αk = β für passende α1 , . . . , αk
ε
und ⇒l sei der leere Linksableitungsschritt.
z
z ∈ [p]∗ heißt Linksanalyse von α, falls S ⇒l α und analog Rechtsanalyse von α, falls
z
S ⇒r α.
Mit diesen Begriffen können wir das Problem der Syntaxanalyse allgemein formulieren:
Problem der Syntaxanalyse: Gegeben ist eine Grammatik G und ein Wort w ∈ Σ∗ .
Gesucht ist eine Links- oder Rechtsanalyse von w, falls w ∈ L(G), ansonsten eine
verständliche Fehlermeldung.
GAE : E → E + T | T (1, 2)
T → T ∗F | F (3, 4)
F → (E) | a | b | c (5 − 8)
Weiter sei das Wort w =(a)*c gegeben. Wir wollen feststellen, ob w ∈ L(GAE ) ist. Es
kann die in Abbildung 4 angegebene Linksableitung mit dem entsprechenden, ebenfalls
29
3. Syntaktische Analyse
E
2
T
@ 3
@
2
E ⇒T @
T * F
3
⇒T ∗F 4 8
4
⇒F ∗F
5 F c
⇒ (E) ∗ F
@5
2
⇒ (T ) ∗ F @
4 ( E )
⇒ (F ) ∗ F 2
6
⇒ (a) ∗ F T
8 4
⇒ (a) ∗ c
F
6
a
Abbildung 4: Linksableitung und Ableitungsbaum
Üblicherweise verwendet man reduzierte Grammatiken; grob gesagt solche, die keine
überflüssigen Elemente enthalten.
30
3.1 Kontextfreie Grammatiken und Kellerautomaten
Σ∗
|{z} × (Σ ∪ N)∗ × [p]∗
| {z } |{z}
Eingabeband Kellerspeicher Ausgabe
(Kellerspitze links!)
wobei die folgenden Alphabete unterschieden werden
Eingabealphabet: Σ
Kelleralphabet: Σ ∪ N (disjunkte Vereinigung!)
Ausgabealphabet: [p]
Der Automat beginnt mit der Startkonfiguration: (w, S, ε). Es werden zwei Arten von
Transitionen unterschieden:
Ableitungsschritt: (w, Aα, z) ` (w, βα, zi) falls πi = A → β ∈ P
Vergleichsschritt: (aw, aα, z) ` (w, α, z) für a ∈ Σ.
Ziel der Analyse ist es, eine Endkonfiguration (ε, ε, z) zu erreichen. Der Automat arbei-
tet nichtdeterministisch, falls in der Grammatik verschiedene Regeln für die Ersetzung
eines Nonterminals existieren: A → β | γ.
Beispiel: (Fortsetzung)
Zur Bestimmung der Linksanalyse des Ausdrucks (a)∗c (siehe Abbildung 4) durchläuft
der Automat die folgende Konfigurationsfolge:
( (a) ∗ c , E , ε )
` ( (a) ∗ c , T , 2 )
` ( (a) ∗ c , T ∗ F , 23 )
` ( (a) ∗ c , F ∗ F , 234 )
` ( (a) ∗ c , (E) ∗ F , 2345 )
` ( a) ∗ c , E) ∗ F , 2345 )
` ( a) ∗ c , T ) ∗ F , 23452 )
` ( a) ∗ c , F ) ∗ F , 234524 )
` ( a) ∗ c , a) ∗ F , 2345246 )
` ( )∗c , ) ∗ F , 2345246 )
` ( ∗c , ∗F , 2345246 )
` ( c , F , 2345246 )
` ( c , c , 23452468 )
` ( ε , ε , 23452468 )
Dies ist eine erfolgreiche Konfigurationsfolge. Es gibt viele Läufe des Automaten, die
in Sackgassen enden. /
31
3. Syntaktische Analyse
32
3.2 Top-Down-Analyse
z0
Hieraus folgt, dass γβ ⇒l v. Laut Induktionsvoraussetzung gilt somit: (v, γβ, ε) `∗
(ε, ε, z 0 ). Somit folgt:
Wie bereits erläutert, arbeitet der TDA nichtdeterministisch. Falls mehrere Alternati-
ven existieren, ein Nonterminal durch eine rechte Seite zu ersetzen, ist kein Kriterium
für eine Auswahl gegeben. Bei einer deterministischen Simulation müssen alle Alter-
nativen untersucht werden. Dies ist aber für die Praxis zu ineffizient. Eine effizientere
Implementierung kann erreicht werden, wenn man durch eine Vorausschau (lookahead)
auf dem Eingabeband entscheiden kann, welche Alternative bei einem Ableitungsschritt
die richtige ist. Dies ist für die im folgenden betrachteten LL(k)-Grammatiken möglich.
3.2 Top-Down-Analyse
Das Ziel für die folgenden Betrachtungen besteht darin, den Nichtdeterminismus des
TDAs durch einen lookahead von k Symbolen auf der Eingabe zu beseitigen (k ∈
IN). Die Bezeichnung LL(k) ist eine Abkürzung für LeftToRight-Linksanalyse mit k-
lookahead. Für Sprachen, die durch LL(k)-Grammatiken erzeugt werden können, lassen
sich effiziente Top-Down-Parser konstruieren (die ja eine Linksanalyse erzeugen).
Zur Formalisierung des k-lookaheads werden zunächst die sogenannten first-Mengen
definiert:
Die Menge first k (α) enthält also die Präfixe bis zur Länge k aller Wörter, die sich aus α
ableiten lassen (dies sind evtl. die Wörter selbst, falls sie kürzer sind). Einige einfache
Folgerungen sind:
Folgerungen:
Die Idee ist nun, aus der Kenntnis eines Präfixes v der Eingabe die richtige Regel
auswählen zu können. Denn aus dem Nonterminal auf der Kellerspitze muss ein Präfix
der Eingabe ableitbar sein. Grammatiken, die dies erlauben, gehören einer bestimmten
Klasse an, um die es im folgenden geht.
33
3. Syntaktische Analyse
3.2.1 LL(k)-Grammatiken
Definition 15 (LL(k)-Grammatik) Sei k ∈ IN.
G ∈ CF G(Σ) heißt LL(k)-Grammatik, in Zeichen G ∈ LL(k), falls für alle Linksablei-
tungen der Form
( ∗
∗ ⇒l wβα ⇒l wx
S ⇒l wAα ∗ mit first k (x) = first k (y) gilt: β = γ
⇒l wγα ⇒l wy
Der nächste Linksableitungsschritt für wAα ist durch die nächsten k auf w folgenden
Eingabesymbole bestimmt. Für LL(k)-Grammatiken kann der nichtdeterministische
TDA mit einem lookahead von k Zeichen auf der Eingabe effizient deterministisch simu-
liert werden. Die obige Definition von LL(k)-Grammatiken ist allerdings nicht gut ge-
eignet, um damit zu arbeiten. Der folgende Satz charakterisiert die LL(k)-Grammatiken
auf andere Weise.
Satz 4 (Alternative Charakterisierung von LL(k)-Grammatiken)
G ∈ LL(k) ⇐⇒ Für alle Linksableitungen der Form
(
∗ ⇒l wβα
S ⇒l wAα mit β 6= γ
⇒l wγα
34
3.2 Top-Down-Analyse
follow k (A) enthält Wörter bis zur Länge k, die in einer Ableitung auf den aus A
abgeleiteten Teil folgen können. Die follow-Mengen sind nur für Nonterminale definiert.
Sie fassen alle möglichen Rechtskontexte zusammen. Im Fall k = 1 kann man mit diesen
Mengen eine sehr einfache Charakterisierung von LL(1)-Grammatiken angeben.
• la(A → β) ⊆ Σ ∪ {ε}
∗
• ε ∈ la(A → β) ⇐⇒ ε ∈ follow 1 (A) und β ⇒ ε
∗
• a ∈ Σ, a ∈ la(A → β) ⇐⇒ a ∈ first 1 (β) oder (β ⇒ ε und a ∈ follow 1 (A))
1. Fall: c = ε.
∗ ∗
Dann muss gelten: β ⇒ ε, γ ⇒ ε und ε ∈ follow 1 (A) d.h.
(
∗ ⇒l wβα
S ⇒l wAα mit ε ∈ first 1 (βα) ∩ first 1 (γα)
⇒l wγα
35
3. Syntaktische Analyse
∗
wobei a ∈ first 1 (γα) wegen γ ⇒ ε. Wie oben folgt der Widerspruch.
∗
3. a ∈ first(γ), β ⇒ ε, a ∈ follow 1 (A).
wie unter 2.
∗ ∗
4. β ⇒ ε, γ ⇒ ε, a ∈ follow 1 (A).
analog zu 2.
(
∗ ⇒l wβα
(⇐) Sei S ⇒l wAα mit β 6= γ.
⇒l wγα
Nach Voraussetzung gilt: first 1 (βfollow 1 (A)) ∩ first 1 (γfollow 1 (A)) = ∅.
Damit folgt wegen first 1 (βα) ⊆ first 1 (βfollow 1 (A)), dass
Für k = 1 kann der lokale Rechtskontext durch follow 1 (A) verallgemeinert werden.
Die Berechnung der lookahead-Mengen erfolgt induktiv. Es ist hilfreich als Hilfsopera-
tion die Konkatenation modulo k, in Zeichen: ⊕k , zu definieren:
⊕k : Σ∗ × Σ∗ → Σ∗
mit u ⊕k w 7→ v mit {v} = first k (uw).
X ⊕k Y := {v | ∃u ∈ X, w ∈ Y : v = u ⊕k w}.
36
3.2 Top-Down-Analyse
37
3. Syntaktische Analyse
1. la(E → T E 0 ) = {a, (}
2. la(E 0 → +T E 0 ) = {+}
3. la(E 0 → ε) = follow 1 (E 0 ) = {ε} ⊕1 follow 1 (E) = follow 1 (E) = {ε, )}
4. la(T → F T 0) = {a, (}
5. la(T 0 → ∗F T 0 ) = {∗}
6. la(T 0 → ε) = follow 1 (T 0 ) = follow 1 (T ) = first 1 (E 0 follow 1 (E)) = {+, ε, )}
7. la(F → (E)) = {(}
8. la(F → a) = {a}.
38
3.2 Top-Down-Analyse
Beispiel: Der TDA zu der modifizierten Grammatik G0AE hat die folgende Analyse-
tabelle:
Keller
Eingabe E E’ T T’ F a ( ) + * ε
a TE’,1 FT’,4 a,8 pop
( TE’,1 FT’,4 (E),7 pop
) ε,3 ε,6 pop
+ +TE’,2 ε,6 pop
* *FT’,5 pop
ε ε,3 ε,6 accept
Alle nicht ausgefüllten Felder führen zu einem Fehler. Die Tabelle ergibt sich aus den
lookahead-Mengen für die einzelnen Produktionen. Beispielsweise ist der lookahead für
die Regel 6 la(T 0 → ε) = {+, ), ε}, womit sich die Einträge für (+, T 0 ), (), T 0 ) und
(ε, T 0) ergeben.
Zur Erkennung der Eingabe (a∗a) durchläuft der deterministische Automat die folgende
Konfigurationsfolge:
( (a ∗ a) , E , ε )
( (a ∗ a) , T E0 , 1 )
( (a ∗ a) , F T 0E 0 , 14 )
( (a ∗ a) , (E)T 0 E 0 , 147 )
( a ∗ a) , E)T 0 E 0 , 147 )
( a ∗ a) , T E 0 )T 0 E 0 , 1471 )
( a ∗ a) , F T 0E 0 )T 0 E 0 , 14714 )
( a ∗ a) , aT 0 E 0 )T 0 E 0 , 147148 )
( ∗a) , T 0 E 0 )T 0 E 0 , 147148 )
( ∗a) , ∗F T 0E 0 )T 0 E 0 , 1471485 )
( a) , F T 0E 0 )T 0 E 0 , 1471485 )
( a) , aT 0 E 0 )T 0 E 0 , 14714858 )
( ) , T 0 E 0 )T 0 E 0 , 14714858 )
( ) , E 0 )T 0 E 0 , 147148586 )
( ) , )T 0 E 0 , 1471485863 )
( , T 0E 0 , 1471485863 )
( , E0 , 14714858636 )
( , ε , 147148586363 )
/
Der lookahead auf der Eingabe beseitigt nicht nur den Nichtdeterminismus des TDAs,
sondern erlaubt auch eine frühzeitige Fehlererkennung.
Für die Konstruktion von Parsern nach der Top-Down-Methode genügt es, (1) die
la-Mengen der Grammatik-Produktionen zu berechnen, (2) den LL(1)-Test durch-
zuführen, (3) die Analysetabelle aufzustellen und (4) einen tabellengesteuerten Parser
zu generieren.
39
3. Syntaktische Analyse
entstanden. /
40
3.3 Top-Down Analyse mit rekursiven Prozeduren
41
3. Syntaktische Analyse
Parser überführen eine Tokensequenz bzw. Zeichenkette in einen Syntaxbaum, der die
grammatikalische Struktur der Zeichenkette verdeutlicht:
Im allgemeinen konsumiert ein Parser nicht unbedingt die gesamte Eingabe, sondern
nur einen Teil und liefert den nicht konsumierten Teil mit dem Ergebnis zurück:
Da Parser auch scheitern können, ist es sinnvoll, als Resultattyp eine Liste zu wählen,
um Scheitern durch die leere Liste und Erfolg durch eine einelementige Liste zu re-
präsentieren:
Der Tokentyp und der Resultattyp sind nun durch Typvariablen token und tree re-
präsentiert. Im folgenden werden zur vereinfachten Schreibweise meist die Typvariablen
a und b verwendet.
2. zero :: Parser a b
zero inp = [] -- zero = \ inp -> []
schlägt unabhängig von der Eingabe immer fehl.
3. item :: Parser a a
item [] = [] -- Fehlschlag
item (c:cs) = [(c,cs)]
konsumiert bei nicht-leerem Eingabestring das erste Zeichen und liefert dieses als
Resultat zurück.
42
3.3 Top-Down Analyse mit rekursiven Prozeduren
3.3.3 Parserkombinatoren
Parserkombinatoren kombinieren elementare Parser entsprechend möglicher Gramma-
tik-Konstruktionen:
Sequenz — Konkatenation
Auswahl — Alternative Regeln
Wiederholung — Sternoperator
Der bind-Operator nimmt einen Parser und eine Abbildung vom Ergebnistyp dieses
Parsers auf Parser mit dem selben Eingabetyp. Zunächst wird der erste Parser aufge-
rufen, p inp. Dies liefert eine Liste von Paaren (v,inp’). Für jedes Element der Liste
wird (f v) :: Parser a c gebildet und auf die restliche Eingabe inp’ angewendet.
Dies liefert Listen vom Typ [(c,[a])], die mittels concat :: [[a]] -> [a] zu einer
Liste zusammengefügt werden.
Für beliebige Funktionen f gilt etwa: bind zero f = zero.
entwickelt werden, der ein einzelnes Zeichen erkennt, wenn es dem als Parameter über-
gebenen Prädikat genügt, und sonst scheitert.
Das Prädikat isDigit :: Char -> Bool liefert True, falls das Argument eine Ziffer
ist. Dementsprechend folgt:
• Ziffern
43
3. Syntaktische Analyse
• Kleinbuchstaben
• Großbuchstaben
Durch Komposition von sat-Parsern können Sequenzen von Zeichen erkannt werden,
beispielsweise zwei aufeinanderfolgende Kleinbuchstaben:
Es folgt beispielsweise:
twolower "abcd" =>* [("ab","cd")]
twolower "aBcd" =>* [] /
Ein mittels plus kombinierter Parser liefert eventuell eine Resultatliste der Länge > 1,
falls p und q die Eingabe erfolgreich durchlaufen können.
Beispiel: Ein Parser für alphanumerische Zeichen, also Buchstaben und Ziffern, kann
wie folgt definiert werden:
Durch rekursive Verwendung von Parsern können Zeichenketten beliebiger Länge ana-
lysiert werden:
44
3.3 Top-Down Analyse mit rekursiven Prozeduren
Beispiel: Ein Parser für Wörter, also Buchstabenfolgen, hat die folgende rekursive
Definition:
Es werden also alle möglichen Zerlegungen der Eingabekette in ein Wort und einen
Rest ermittelt. Dadurch dass die Erkennung des leeren Wortes (Parser result "")
als zweite Alternative angegeben ist, wird als erste mögliche Zerlegung die maximal
mögliche ermittelt. Ist man nur an dieser interessiert, wird durch die bedarfsgesteuerte
Auswertung in Haskell auch nur diese berechnet. /
Dem im vorigen Beispiel gezeigten Parser liegt ein allgemeineres Parserschema zugrun-
de, das die wiederholte Anwendung desselben Parsers beschreibt:
45
3. Syntaktische Analyse
Die den Symbolen bzw. Symbolfolgen einer rechten Regelseite zugeordneten Parser
werden mittels des bind-Operators verknüpft. Die Ergebnisse werden gesammelt und
zu einem Gesamtergebnis kombiniert. Die Parserfunktionen zu den verschiedenen rech-
ten Regelseiten eines Nonterminals werden schließlich mittels des plus-Kombinators
vereinigt. Abbildung 5 zeigt einen auf diese Weise erzeugten recursive-descent parser“
”
für arithmetische Ausdrücke. Hier werden die Operatoren bind und plus als rechtsas-
soziative Infixoperatoren definiert, wobei (&&&)=bind eine höhere Priorität erhält als
(|||)=plus. Letzteres spart Klammern. Als Ergebnis liefern die Parserfunktionen eine
Linksanalyse in Form einer Nummernfolge.
infixr 6 &&&
infixr 4 |||
(|||) = plus; (&&&) = bind
Der Aufruf parseRD "(a*a)" liefert das Ergebnis 147148586363. Bei einer fehlerhaften
Eingabe, etwa (aa) erfolgt ein durch die error-Funktion ausgelöster Laufzeitfehler mit
der Ausgabe syntax error in expression. Diese Fehlermeldung ist wenig spezifisch.
Eine Verbesserung der Fehlermeldungen und eine zielgerichtetere Abarbeitung wird
durch die Einbindung eines 1-lookaheads in die Parserfunktionen erzielt.
ausgegeben, wobei die erwarteten Symbole die Elemente der lookahead-Menge sind,
während die gefundenen Symbole der aktuellen Eingabe entsprechen. Eine solche Feh-
lermeldung hilft dem Benutzer den aufgetretenen Syntaxfehler genau zu lokalisieren.
Fehler werden auf diese Weise frühzeitig erkannt.
Bei mehreren Regelalternativen dient die Funktion lachoice der Auswahl des zu-
gehörigen Parsers mithilfe der lookahead-Information. Hier wird zunächst getestet, ob
das nächste Eingabesymbol in den lookahead-Mengen vorkommt. In diesem Fall wird
mittels der Funktion findParser der dazugehörige Parser bestimmt und aufgerufen.
Anderenfalls wird eine entsprechende Fehlermeldung ausgegeben.
Abbildung 7 zeigt die Modifikation des RD-Parsers für arithmetische Ausdrücke unter
Berücksichtigung der auf Seite 37 bestimmten lookahead-Mengen der Grammatik G0AE .
46
3.3 Top-Down Analyse mit rekursiven Prozeduren
-- E -> TH
expr = term &&& \lt ->
hexpr &&& \le ->
result (1:lt++le)
-- T -> FG
term = factor &&& \lf ->
hterm &&& \lt ->
result (4:lf++lt)
-- F -> (E) | a
factor = ((tok "(") &&& \c ->
expr &&& \le ->
(tok ")") &&& \c ->
result (7:le)) ||| (tok "a" &&& \ c -> result [8])
47
3. Syntaktische Analyse
findParser :: (Show a, Eq a)
=> [[[a]]] -> [Parser a b] -> Parser a b
findParser (la:las) (p:ps) []
| elem [] la = p []
| otherwise = findParser las ps []
findParser (la:las) (p:ps) inp@(c:cs)
| elem [c] la = p inp
| otherwise = findParser las ps inp
findParser [] ps inp = error "No Parser found - not possible!"
48
3.3 Top-Down Analyse mit rekursiven Prozeduren
49
3. Syntaktische Analyse
3.4 Bottom-Up-Analyse
Die Bottom-Up-Analyse arbeitet umgekehrt zur Top-Down-Analyse: Statt aus dem
Startsymbol die Eingabe schrittweise herzuleiten, wird die Herleitung “rückwärts voll-
zogen”. Es erfolgt eine Bottom-Up-Berechnung des Ableitungsbaums in Form einer
gespiegelten Rechtsanalyse durch einen Kellerautomaten, der zwei Arten von Transi-
tionen durchführen kann und daher auch Shift-Reduce-Parser genannt wird:
Beispiel: Wir betrachten wieder die Grammatik GAE mit den von 1 bis 6 durchnum-
merierten Regeln:
E → E + T | T (1, 2)
T → T ∗ F | F (3, 4)
F → (E) | a (5, 6)
und verfolgen eine gespiegelte Rechtsanalyse des Ausdrucks (a) ∗ a in Abbildung 8. Die
Position des Lesekopfes auf dem Eingabeband wird durch % angezeigt, die Position
der Kellerspitze durch - . Das Beispiel zeigt, dass die Kellerspitze und die Position
des Eingabelesekopfes immer direkt benachbart sind und die Satzform in zwei Teile
zerlegen: ein bereits gelesenes Präfix, das auf dem Keller bereits reduziert wurde und
den noch nicht gelesenen Teil.
Die Rechtsanalyse endet, wenn auf dem Keller das Startsymbol reduziert werden konnte
und die Eingabe vollständig gelesen wurde. Die ausgegebene Nummernfolge entspricht
dann einer gespiegelten Rechtsanalyse, im Beispiel: 64254632. /
Eingabealphabet: Σ
Kelleralphabet: N ∪Σ
Ausgabealphabet: [p]
50
3.4 Bottom-Up-Analyse
Shift
-% (a) ∗ a −→ (-% a) ∗ a
Shift
−→ (a-% ) ∗ a
Reduce
−→ (F-% ) ∗ a Ausgabe: 6
Reduce
−→ (T-% ) ∗ a 4
Reduce
−→ (E-% ) ∗ a 2
Shift
−→ (E)-% ∗ a
Reduce
−→ F-% ∗ a 5
Reduce
−→ T-% ∗ a 4
Shift
−→ T ∗-% a
Shift
−→ T ∗ a-%
Reduce
−→ T ∗ F-% 6
Reduce
−→ T-% 3
Reduce
−→ E-% 2
(Σ ∪ N)∗ × Σ∗
|{z} × [p]∗
| {z } |{z}
Kellerspeicher Eingabeband Ausgabe
(Kellerspitze rechts!)
Der Automat beginnt mit der Startkonfiguration: (ε, w, ε). Es werden zwei Transitionen
unterschieden:
Im Shift-Schritt wird ein Zeichen von der Eingabe auf den Keller geschoben. Im Reduce-
Schritt wird eine rechte Regelseite auf dem Keller durch das Nonterminal der linken
Seite ersetzt. Ziel ist es, bei leerer Eingabe zum Startsymbol auf dem Keller reduziert
zu haben, also eine Endkonfiguration (S, ε, z) zu erreichen.
51
3. Syntaktische Analyse
Beispiel: (Fortsetzung) Für die zuvor betrachtete Beispielanalyse des Ausdrucks (a)∗
a bzgl. der Grammatik GAE ergibt sich die folgende Konfigurationsfolge des Bottom-
Up-Analyseautomaten:
( ε , (a) ∗ a , ε )
Shift ` ( ( , a) ∗ a , ε )
Shift ` ( (a , )∗a , ε )
Reduce ` ( (F , )∗a , 6 )
Reduce ` ( (T , ) ∗ a , 64 )
Reduce ` ( (E , ) ∗ a , 642 )
Shift ` ( (E) , ∗a , 642 )
Reduce ` ( F , ∗a , 6425 )
Reduce ` ( T , ∗a , 64254 )
Shift ` ( T∗ , a , 64254 )
Shift ` ( T ∗a , ε , 64254 )
Reduce ` ( T ∗F , ε , 642546 )
Reduce ` ( T , ε , 6425463 )
Reduce ` ( E , ε , 64254632 )
/
2. Shift-Reduce-Konflikt: Falls auf der Kellerspitze die rechte Seite einer Regel
liegt und noch Symbole auf der Eingabe sind, kann ein Reduktionsschritt oder
ein Shiftschritt durchgeführt werden.
3. Reduce-Reduce-Konflikte:
(a) linker Henkelrand: Eine auf der Kellerspitze befindliche rechte Regelseite
nennt man einen Henkel (engl. handle). Befindet sich auf dem Keller die
Satzform αβ und existieren in der Grammatik Regeln A → αβ und B → β,
so kann mit beiden Regeln reduziert werden.
(b) linke Regelseite: Tritt eine Satzform β als rechte Regelseite in verschie-
denen Regeln auf, A → β und B → β, so kann β zu A oder B reduziert
werden.
Das erste Problem lässt sich einfach lösen: wir “startseparieren” die Grammatiken
durch Hinzunahme einer zusätzlichen Startregel.
Definition 22 (Startseparierte Grammatik) G ∈ CF G(Σ) heißt startsepariert,
falls das Startsymbol S nur in einer Regel S → A mit A 6= S auftritt.
52
3.4 Bottom-Up-Analyse
Es ist offensichtlich, dass sich jede Grammatik G ∈ CF G(Σ) durch Hinzunahme eines
neuen Startsymbols S 0 und einer zusätzlichen Startregel S 0 → S in eine äquivalente
startseparierte Grammatik überführen lässt. Im folgenden sei generell vorausgesetzt,
dass die betrachteten Grammatiken startsepariert sind.
Für startseparierte Grammatiken ist das Analyseende eindeutig bestimmt. Die End-
konfiguration (S 0 , ε, z) kann wegen ε-Regeln zwar Folgekonfigurationen haben, aber
wegen der Startsepariertheit nicht erneut in eine Endkonfiguration übergehen.
Um den weiteren Nichtdeterminismus zu beseitigen, führen wir die Klasse der soge-
nannten LR(k)-Grammatiken ein, bei denen ein k-lookahead auf der Eingabe zur Ent-
scheidung bei Konfliktsituationen verwendet werden kann.
3.4.2 LR(k)-Grammatiken
Definition 23 (LR(k)-Grammatik) Sei G ∈ CF G(Σ), startsepariert durch S 0 → S
und k ≥ 0. G ist LR(k)-Grammatik (G ∈ LR(k)), falls für alle Rechtsableitungen der
Form ( ∗
⇒r αAw ⇒r αβw
S ∗ 0 0 0
mit first k (w) = first k (v)
⇒r α A w ⇒r αβv
gilt: A = A0 , α = α0 , w 0 = v
Das heißt (da rückwärts abgeleitet wird): Wenn auf dem Stack die Satzform αβ liegt
und der lookahead first k (w) ist, kann nur mit einer einzigen Regel reduziert werden,
nämlich A → β. Falls es sich nicht um eine LR(k)-Grammatik handelt, könnten dage-
gen z.B. die Regeln A → β und A0 → xβ existieren und α = α0 x sein. Die Bezeichnung
LR(k) ist motiviert durch Left-to-right-Rechtsanalyse mit k-lookahead“.
”
Im Fall einer LR(k)-Grammatik kann der Analyseautomat also unter Betrachtung eines
k-lookaheads deterministisch entscheiden, mit welcher Produktion reduziert werden
muss. Dies ist speziell im Fall k = 0 interessant, wo allein aufgrund des Kellerinhalts
entschieden werden kann.
3.4.3 LR(0)-Analyse
Für eine LR(0)-Analyse werden aus dem Kellerinhalt αβ Informationen abstrahiert,
die für die Entscheidung der nächsten Transition des Bottom-Up-Analyseautomaten
ausreicht. Dies ist vergleichbar zur Abstraktion von Rechtskontexten durch follow-
Mengen.
Üblicherweise teilt man den Keller dabei in zwei Teile αβ, wobei der Henkel“ β der Teil
”
ist, welcher reduziert werden kann. Hier ist weiter die Anmerkung angebracht, dass es
53
3. Syntaktische Analyse
sich nicht um einen Keller im eigentlichen Sinn handelt, weil mehrere Elemente gelesen
und auf einmal ersetzt werden. Allerdings ist auch dieses Verhalten mit einem Keller-
automaten im engeren Sinn durch eine Erweiterung der Menge der Kontrollzustände
simulierbar.
Aus der obigen Definition folgen unmittelbar die folgenden Aussagen:
1. LR(0)(γ) ist endlich für alle γ ∈ (N ∪ Σ)∗ .
n o
2. LR(0)(G) := LR(0)(γ) | γ ∈ (N ∪ Σ)∗ ist endlich.
Beispiel: Wir betrachten erneut die (nun startseparierte) Grammatik für arithmeti-
sche Ausdrücke:
(s)
GAE : E0 → E (0)
E → E + T | T (1, 2)
T → T ∗ F | F (3, 4)
F → (E) | a (5, 6)
Die Ermittlung der LR(0)-Mengen beginnt mit LR(0)(ε):
I0 := LR(0)( ε ) = {[E 0 → ·E]} ∪ Abschluss(E), wobei
54
3.4 Bottom-Up-Analyse
Ausgehend von den Symbolen rechts vom Punkt in LR(0)(ε) werden nun alle Mengen
für Satzformen der Länge 1 gebildet. Weil wir von LR(0)(ε) ausgehen, betrachten wir
nur die notwendigen Teile.
I1 := LR(0)( E) = {[E 0 → E·], [E → E · +T ]}
I2 := LR(0)( T ) = {[E → T ·], [T → T · ∗F ]}
I3 := LR(0)( F ) = {[T → F ·]}
I4 := LR(0)( () = {[F → (·E)]} ∪ Abschluss(E)
I5 := LR(0)( a) = {[F → a·]}
Hier bestehen Shift-Reduce-Konflikte (in den Mengen für E und T), die Grammatik
ist also keine LR(0)-Grammatik. Wir werden später sehen, dass sich die Konflikte mit
einem lookahead von 1 beseitigen lassen.
Nun werden die nötigen Mengen für Satzformen der Länge 2 bestimmt. Wiederum
werden alle Satzformen betrachtet, die in der vorigen Stufe einen Shift implizieren.
I6 := LR(0)( E + ) = {[E → E + ·T ]} ∪ Abschluss(T )
I7 := LR(0)( T ∗ ) = {[T → T ∗ ·F ]} ∪ Abschluss(F )
I8 := LR(0)( (E ) = {[F → (E·)], [E → E · +T ]}
LR(0)( (T ) = {[E → T ·], [T → T · ∗F ]} = I2
LR(0)( (F ) = {[T → F ·]} = I3
LR(0)( (( ) = {[F → (·E)]} ∪ Abschluss(E) = I4
LR(0)( (a ) = {[F → a·]} = I5
I9 := LR(0)( E + T ) = {[E → E + T ·], [T → T · ∗F ]}
LR(0)( E + F ) = {[T → F ·]} = I3
LR(0)( E + ( ) = {[F → (·E)]} ∪ Abschluss(E) = I4
LR(0)( E +a ) = {[F → a·]} = I5
I10 = LR(0)( T ∗F ) = {[T → T ∗ F ·]}
LR(0)( T ∗( ) = {[F → (·E)]} ∪ Abschluss(E) = I4
LR(0)( T ∗a ) = {[F → a·]} = I5
I11 = LR(0)( (E) ) = {[F → (E)·]}
LR(0)( (E + ) = I6
LR(0)( E +T ∗ ) = I7
Dies sind alle Informationsmengen, die sich für dieses Beispiel ergeben. Wie bereits
(s)
gesagt, ist GAE nicht LR(0), da sich Shift/Reduce-Konflikte in I1 , I2 und I9 zeigen. /
Mit den LR(0)-Mengen haben wir also die Möglichkeit geschaffen, im Falle einer LR(0)-
Grammatik eindeutige Entscheidungen über Shift- und Reduce-Operationen im Ana-
lyseautomaten aus dem Kellerinhalt abzuleiten. LR(0)(γ) liefert die Shift-/Reduce-
Entscheidung für den Bottom-Up-Analyseautomaten mit Kellerinschrift γ. Daher wird
im folgenden statt γ die LR(0)-Menge von γ auf dem Keller gespeichert. Als neues Kel-
leralphabet ergibt sich somit LR(0)(G). Wesentlich dabei ist, dass LR(0)(γX) durch
LR(0)(γ) und X bestimmt, aber unabhängig von γ ist. Daher können wir die folgende
Übergangsfunktion goto definieren.
55
3. Syntaktische Analyse
wird festgelegt durch: goto(I, X) := I 0 genau dann, wenn ein γ ∈ (Σ ∪ N)∗ existiert
mit I = LR(0)(γ) und I 0 = LR(0)(γX).
Diese Definition wäre ohne die Klassenbildung nicht eindeutig, würde also keine Funk-
tion beschreiben. Durch die Zusammenfassung von Auskünften zu Mengen ist aber
gewährleistet, dass bei der Existenz zweier solcher γ die resultierende Menge die glei-
che ist:
Seien γ, γ 0 ∈ (Σ ∪ N)∗ mit γ 6= γ 0 ∧ LR(0)(γ) = LR(0)(γ 0 ). Betrachte [A → α · β] ∈
LR(0)(γX). Dann folgt:
(s)
Beispiel: (Fortsetzung) Für die Grammatik GAE erhält man die folgende goto-Funk-
tion:
56
3.4 Bottom-Up-Analyse
Zustandsmenge: Q := {[A → α · β] | A → αβ ∈ P }
Eingabealphabet: X := Σ∪N
Startzustand: q0 := [S 0 → ·S]
Endzustände: F := Q (ohne Bedeutung)
Transitionen: δ :: Q × X −→ IP (Q)
mit [A → αX · β] ∈ δ([A → α · Xβ], X)
[B → ·β] ∈ δ([A → α · Bβ], ε) falls B → β ∈ P
Mit der zweiten Bedingung an δ wird der Abschluss vollzogen. Die ε-Hülle eines Zu-
stands (LR(0)-Auskunft) ist genau die LR(0)-Menge, welche ihn enthält. Die andere
Bedingung entspricht der Shift-Operation und korrespondiert ebenfalls mit der Defini-
tion der LR(0)-Menge.
Die Potenzmengenkonstruktion liefert zu diesem Automaten einen deterministischen
[ = (Q̂, X, δ̂, qˆ0 , Q̂ \ {∅}) mit
endlichen Automaten A(G)
Definition 26 (action-Funktion)
LR(0)(G) −→ {shift, error , accept} ∪ {red i | i ∈ [p]}
red i falls πi = A → α ∧ [A → α·] ∈ I
act :: shift falls ∃X ∈ (Σ ∪ N) : [A → α · Xβ] ∈ I
I 7−→
accept falls [S 0 → S·] ∈ I
error sonst d.h. falls I = ∅
Die Funktionen goto und act bilden die LR(0)-Analysetabelle von G, die den LR(0)-
Analyseautomaten bestimmt.
Eingabealphabet : Σ
Kelleralphabet : Γ := LR(0)(G)
Ausgabealphabet : ∆ := [p] ∪ {error}
57
3. Syntaktische Analyse
Γ∗
|{z} Σ∗ × |{z}
× |{z} ∆∗ .
Keller (Spitze rechts!) Eingabe Ausgabe
Reduce: Wir verwenden die Notation: wa1 . . . an |n := w, d.h. w̃|n bedeutet, dass
die letzten n Zeichen von w̃ entfernt werden.
˜ 0 , w, zi)
(αI, w, z) ` (α̃II
˜ goto(I,
falls act(I) = red i, πi = A → X1 . . . Xn , αI|n = α̃I, ˜ A) = I 0 .
Satz 10 Wenn LR(0)(G) konfliktfrei, also act eindeutig ist, so arbeitet der LR(0)-
Analyseautomat deterministisch und es gilt für w ∈ Σ∗ und z ∈ [p]∗ :
←
−
z
• (I0 , w, ε) `∗ (ε, ε, z) genau dann, wenn S ⇒r w (←
−
z ist Rechtsanalyse von w)
• (I0 , w, ε) `∗ (ε, ε, z · error) genau dann, wenn w 6∈ L(G).
3.4.5 SLR(1)-Analyse
In der Praxis findet man oft Grammatiken mit widersprüchlichen LR(0)-Mengen. In
diesem Fall möchte man mit möglichst geringem Aufwand trotzdem eine determini-
stische Analyse ermöglichen, also den lookahead wenn möglich nicht komplett in die
Analyse einbauen. Dazu existiert der Ansatz der sogenannten SLR(1)-Analyse (Sim-
ple LR(1)), wo der lookahead benutzt wird, um Konflikte in den LR(0)-Informationen
durch das nächste Eingabesymbol zu beseitigen.
Der SLR(1)-Analyse liegen die folgenden Beobachtungen zugrunde:
∗
1. [A → β1 · aβ2 ] ∈ LR(0)(αβ1 ) =⇒ S ⇒r αAw ⇒r αβ1 a
|{z} β2 w
|{z}
Keller Eingabesymbol
Der Shift sollte nur stattfinden, falls die restliche Eingabe mit dem Zeichen a
beginnt.
∗
aw =⇒ a ∈ follow 1 (A)
2. [A → β·] ∈ LR(0)(αβ) =⇒ S ⇒r αAaw ⇒r αβ |{z}
|{z}
Keller Eingabe
Der Reduce-Schritt mit A → β sollte nur bei a ∈ follow 1 (A) stattfinden.
(s)
Beispiel: Im Beispiel der Grammatik für arithmetische Ausdrücke GAE lassen sich
(s)
alle Konflikte auf diese Weise auflösen, d.h. GAE ∈ SLR(1):
Die Shift-Reduce-Konflikte in den Mengen I1 und I2 lassen sich wie folgt beseitigen:
58
3.4 Bottom-Up-Analyse
I1 = LR(0)( E ) = { [E 0 → E·] , [E → E · +T ] }
| {z } | {z }
accept shif t
bei Eingabeende bei Eingabe von +
I2 = LR(0)( T ) = { [E → T ·] , [T → T · ∗F ] }
| {z } | {z }
red 2 shif t
bei Eingabe von +, ), ε bei Eingabe von ∗
I9 = LR(0)( E + T ) = { [E → E + T ·] , [T → T · ∗F ] }
| {z } | {z }
red 1 shif t
bei Eingabe von +, ), ε bei Eingabe von ∗
Die Berücksichtigung des 1-lookaheads auf der Eingabe ermöglicht eine genauere Kon-
trolle der Aktionen und eine frühe Fehlererkennung. /
Eine Grammatik G, für die act(I, a) stets eindeutig ist, heißt SLR(1)-Grammatik, in
Zeichen: G ∈ SLR(1).
Beispiel: (Fortsetzung)
(s)
GAE : E 0 → E (0)
E → E + T | T (1, 2)
T → T ∗ F | F (3, 4)
F → (E) | a (5, 6)
Es gilt:
follow 1 (E 0 ) = {ε}, follow 1 (E) = {ε, +, )}, follow 1 (T ) = follow 1 (F ) = {ε, +, ), ∗}.
Mit den zuvor berechneten LR(0)-Informationen (siehe Seite 54) ergibt sich die folgende
(s)
SLR(1)-Analysetabelle zu GAE :
59
3. Syntaktische Analyse
action goto
+ ∗ ( ) a ε + ∗ ( ) a E T F
I0 shift shift I4 I5 I1 I2 I3
I1 shift accept I6
I2 red 2 shift red 2 red 2 I7
I3 red 4 red 4 red 4 red 4
I4 shift shift I4 I5 I8 I2 I3
I5 red 6 red 6 red 6 red 6
I6 shift shift I4 I5 I9 I3
I7 shift shift I4 I5 I10
I8 shift shift I6 I11
I9 red 1 shift red 1 red 1 I7
I10 red 3 red 3 red 3 red 3
I11 red 5 red 5 red 5 red 5
( I0 , a+a , ε )
` (shift) ( I0 I5 , +a , ε )
` (red 6 ) ( I0 I3 , +a , 6 )
` (red 4 ) ( I0 I2 , +a , 64 )
` (red 2 ) ( I0 I1 , +a , 642 )
` (shift) ( I0 I1 I6 , a , 642 )
` (shift) ( I0 I1 I6 I5 , ε , 642 )
` (red 6 ) ( I0 I1 I6 I3 , ε , 6426 )
` (red 4 ) ( I0 I1 I6 I9 , ε , 64264 )
` (red 1 ) ( I0 I1 , ε , 642641 )
` (accept) ( ε , ε , 642641 )
Der Automat akzeptiert die Eingabe und produziert die gespiegelte Rechtsanalyse
642641 als Ausgabe. /
Beispiel: Ein anderes Beispiel zeigt, dass SLR(1) nicht alle Probleme löst: Die fol-
gende Grammatik GLR ist eindeutig, aber GLR 6∈ SLR(1).
GLR : S 0 → S (0)
S → L = R | R (1, 2)
L → ∗R | id (3, 4)
R → L (5)
60
3.4 Bottom-Up-Analyse
Hier zeigt sich ein Shift-Reduce-Konflikt in I2 , GLR ist also nicht LR(0). Wegen =∈
follow 1 (L) ⊆ follow 1 (R) (weil R → L) ist dieser Konflikt nicht SLR(1)-lösbar. Durch
die Überschneidung der follow-Menge follow 1 (R) mit dem Rechtskontext von L kann
der Konflikt in diesem Fall nicht aufgelöst werden. In der Ableitung wird das “=” aber
nur nach vorhergehendem “∗” rechts von R auftreten, was im SLR(1)-Ansatz nicht
beachtet wird. /
Beispiel: (Fortsetzung)
Zu der Grammatik GLR ergeben sich die folgenden LR(1)-Informationen:
Der Rechtskontext gibt an, unter welchem lookahead eine Reduktion durchzuführen
ist. In der LR(1)-Information LR(1)(L) wird klar, dass die Reduktion bei lookahead
ε durchzuführen ist, während der lookahead “=” einen Shift impliziert. Der Konflikt,
der sich bei der SLR(1)-Analyse ergab, wird also durch die genauere LR(1)-Analyse
aufgelöst. /
61
3. Syntaktische Analyse
action goto
Zustand = ∗ id ε = ∗ id S L R
I0 shift shift I4 I5 I1 I2 I3
I1 accept
I2 shift red 5 I6
I3 red 2
I4 shift shift I4 I5 I8 I7
I5 red 4 red 4
I6 shift shift I4 I5 I8 I9
I7 red 3 red 3
I8 red 5 red 5
I9 red 1
Der Übergang von LR(1)- zu LALR(1)-Grammatiken führt i.a. zu einer großen Reduk-
tion der Zustandszahl. Bei höheren Programmiersprachen wie etwa Algol erreicht man
beispielsweise eine Reduktion von 10000 auf 100 Zustände. Es gibt aber auch LR(1)-
Grammatiken, die nicht LALR(1) sind, d.h. bei denen beim Übergang zu LALR(1)-
Informationen reduce-reduce-Konflikte auftreten können.
62
3.4 Bottom-Up-Analyse
GLR : S 0 → S (0)
S → L = R | R (1, 2)
L → ∗R | id (3, 4)
R → L (5)
LR(0)-Mengen: LALR(1)-Mengen:
I0 : [S 0 → ·S] I0 : [S 0 → ·S, ε]
[S → ·L = R] [S → ·L = R, ε]
[S → ·R] [S → ·R, ε]
[L → · ∗ R] [L → · ∗ R, = /ε]
[L → ·id] [L → ·id, = /ε]
[R → ·L] [R → ·L, ε]
I1 : [S 0 → S·] I1 : [S 0 → S·, ε]
I2 : [S → L· = R] I2 : [S → L· = R, ε]
[R → L·] [R → L·, ε]
I3 : [S → R·] I3 : [S → R·, ε]
I4 : [L → ∗ · R] I4 : [L → ∗ · R, = /ε]
[R → ·L] [R → ·L, = /ε]
[L → · ∗ R] [L → · ∗ R, = /ε]
[L → ·id] [L → ·id, = /ε]
I6 : [S → L = ·R] I6 : [S → L = ·R, ε]
[R → ·L] [R → ·L, ε]
[L → · ∗ R] [L → · ∗ R, ε]
[L → ·id] [L → ·id, ε]
I9 : [S → L = R·] I9 : [S → L = R·, ε]
63
3. Syntaktische Analyse
G : S0 → S
S → aAd | bBd | aBe | bAe
A → c
B → c
LR(1)-Mengen:
I0 : [S 0 → ·S, ε]
I6 : [A → c·, d]
[S → ·aAd, ε]
[B → c·, e]
[S → ·bBd, ε]
[S → ·aBe, ε]
I7 : [S → bB · d, ε]
[S → ·bAe, ε]
I8 : [S → bA · e, ε]
I1 : [S 0 → S·, ε]
I2 : [S → a · Ad, ε] I9 : [A → c·, e]
[S → a · Be, ε] [B → c·, d]
[A → ·c, d]
[B → ·c, e]
I10 : [S → aAd·, ε]
I3 : [S → b · Bd, ε]
I11 : [S → aBe·, ε]
[S → b · Ae, ε]
[A → ·c, e]
I12 : [S → bBd·, ε]
[B → ·c, d]
I13 : [S → bAe·, ε]
I4 : [S → aA · d, ε]
I14 : ∅
I5 : [S → aB · e, ε]
64
3.4 Bottom-Up-Analyse
Gm
AE : E → E + E | E ∗ E | (E) | a
Diese Grammatik hat eine sehr einfache Form. Wegen ihrer Mehrdeutigkeit ist sie keine
LR(k)-Grammatik. Die Präzedenzregel “∗ vor +” macht die Analyse von a ∗ a + a
eindeutig. Statt diese Regel aber in der verwendeten Grammatik zu verankern, kann
sie auch durch den Parser realisiert werden. Dies wird z.B. mit den Präzedenzstufen in
C so gemacht. Bei der Analyse von a + a + a wird durch die Assoziativität der Addition
festgelegt, wie zu reduzieren ist.
Die LR(0)-Informationen zu der obigen Grammatik haben die in Abbildung 11 ange-
gebene Gestalt. Die Informationen I1 , I7 und I8 weisen shift-reduce-Konflikte auf. Der
Konflikt in I1 ist SLR(1)-lösbar. Die Konflikte in I7 und I8 sind auf die Mehrdeutigkeit
zurückzuführen und daher nicht auflösbar. Beachten Sie, dass {∗, +} ⊆ follow (E).
Wie man in der Analysetabelle bzw. der action-Funktion in Abbildung 11 sieht, wurden
die Konflikte in I7 und I8 durch die Präzedenz ∗ m + — act(I7 , ∗) = shift, act(I8 , +) =
red 2 — bzw. durch die Festlegung von Linksassoziativität (Linksklammerung) —
act(I7 , +) = red 1, act(I8 , ∗) = red 2 aufgelöst.
Damit durchläuft der Analyseautomat für die Eingabe a + a ∗ a die folgende Konfigu-
rationsfolge
( I0 , a+a∗a , ε )
( I0 I3 , +a ∗ a , ε )
( I0 I1 , +a ∗ a , 4 )
( I0 I1 I4 , a∗a , 4 )
( I0 I1 I4 I3 , ∗a , 4 )
( I0 I1 I4 I7 , ∗a , 4 )
( I0 I1 I4 I7 I5 , a , 4 )
( I0 I1 I4 I7 I5 I3 , ε , 4 )
( I0 I1 I4 I7 I5 I8 , ε , 44 )
( I0 I1 I4 I7 , ε , 442 )
( I0 I1 , ε , 4421 )
( ε , ε , 4421 )
65
3. Syntaktische Analyse
Gm
AE : E → E + E | E ∗ E | (E) | a
I0 : [E 0 → ·E] I5 : [E → E ∗ ·E]
[E → ·E + E] [E → ·E + E]
[E → ·E ∗ E] [E → ·E ∗ E]
[E → ·(E)] [E → ·(E)]
[E → ·a] [E → ·a]
I1 : [E 0 → E·] I6 : [E → (E·)]
[E → E · +E] [E → E · +E]
[E → E · ∗E] [E → E · ∗E]
I2 : [E → (·E)] I7 : [E → E + E·]
[E → ·E + E] [E → E · +E]
[E → ·E ∗ E] [E → E · ∗E]
[E → ·(E)]
[E → ·a] I8 : [E → E ∗ E·]
[E → E · +E]
I3 : [E → a·] [E → E · ∗E]
I4 : [E → E + ·E] I9 : [E → (E)·]
[E → ·E + E]
[E → ·E ∗ E]
[E → ·(E)]
[E → ·a]
action goto
a + ∗ ( ) ε a + ∗ ( ) E
I0 shift shift I3 I2 I1
I1 shift shift accept I4 I5
I2 shift shift I3 I2 I6
I3 red 4 red 4 red 4 red 4
I4 shift shift I3 I2 I7
I5 shift shift I3 I2 I8
I6 shift shift shift I4 I5 I9
I7 red 1 shift red 1 red 1 I5
I8 red 2 red 2 red 2 red 2
I9 red 3 red 3 red 3 red 3
66
3.4 Bottom-Up-Analyse
I0 : [S 0 → ·S] I3 : [S → a·]
[S → ·if then S]
[S → ·if then S else S] I4 : [S → if then S·]
[S → ·a] [S → if then S · else S]
action goto
if then else a ε if then else a S
I0 shift shift I2 I3 I1
I1 accept
I2 shift shift I2 I3 I4
I3 red 3 red 3
I4 shift(!) red 2 I5
I5 shift shift I2 I3 I6
I6 red 1 red 1
Beispiel: Ein weiteres klassisches Beispiel ist die Paarung von if und else bei Schach-
telung: Gif : S → if then S | if then S else S | a
67
3. Syntaktische Analyse
http://www.haskell.org/happy/
zu finden.
68
3.5 Konkrete und abstrakte Syntax
S → id := E
| begin S; S end
| if B then S
E → E + E | id | num
B → E<E
Zu der Anweisung begin id := num + id; if id < num then id := id + num end
erhält man den folgenden konkreten Syntaxbaum:
π = A0 → w0 A1 w1 A2 . . . wn−1 An wn entspricht Fπ :: A1 × . . . An → A0
69
3. Syntaktische Analyse
Beispiel: (Fortsetzung)
Für die zuvor betrachtete Grammatik GS erhält man die folgenden Operationssymbole:
assign : E → S
seq : S×S → S
cond : B×S → S
plus : E×E → E
id : → E
num : → E
less : E×E → B
seq
H
HH
H
assign cond
@
@
plus less assign
CC CC
num id id num plus
@
@
id num
Alternativ können anstelle von Operationssymbolen die Regeln selbst verwendet wer-
den. Dann erhält man einen sogenannten Regelbaum, der zum abstrakten Syntaxbaum
isomorph ist. Der oben spezifizierte Happy-Parser erzeugt ebenfalls bereits einen ab-
strakten Syntaxbaum anstelle des konkreten Ableitungsbaums für die arithmetischen
Ausdrücke.
70
4. Semantische Analyse
4 Semantische Analyse
Neben der (kontextfreien) Syntax existieren kontextabhängige Eigenschaften des Pro-
gramms, die während der Analyse geprüft werden müssen. Verlangt die Sprache etwa,
dass jeder Bezeichner deklariert sein muss oder alle Operationen typsicher sein müssen,
so muss diese Bedingung nach bzw. während der Syntaxanalyse zusätzlich geprüft wer-
den, ist aber nicht durch kontextfreie Grammatiken beschreibbar.
Es handelt sich um die sog. statische Semantik des Programms, d.h. die laufzeitun-
abhängigen kontextabhängigen Eigenschaften. Sie werden üblicherweise mit Hilfe von
sog. Attributgrammatiken spezifiziert. Der Ausgangspunkt ist eine kontextfreie Gram-
matik, deren Produktionen zusätzlich mit semantischen Informationen (Attributen)
versehen sind, die den Syntaxbaum ergänzen. Der sich ergebende attributierte Ablei-
tungsbaum dient als Ausgangspunkt für die Synthesephase (Codegenerierung).
In Attributgrammatiken werden Nonterminale A ∈ N mit Attributen versehen, zu de-
ren Berechnung semantische Regeln mit den Produktionen der Grammatik verknüpft
werden. Man unterscheidet synthetische Attribute, die bottom-up berechnet werden,
und inherite Attribute, die top-down berechnet werden. Die Kombination von synthe-
tischen und inheriten Attributberechnungen ermöglicht einen beliebigen Informations-
austausch im Ableitungsbaum.
Typische Attributwerte sind etwa die Symboltabelle, Typinformationen, Codeteile oder
Fehlerinformationen. Attributgrammatiken sind vielfältig einsetzbar. Sie beschreiben
eine syntaxgerichtete Programmierung und können auch bei Programmanalysen für
Optimierungen oder bei der Codegenerierung angewendet werden.
GB : L → LB | B
B → 0 |1
Aus dem Startsymbol L wird eine Liste von Bits erzeugt. Der dezimale Wert der Zahl
kann parallel zur Syntaxanalyse sukzessiv aus den Anfangswerten 0 und 1 für die einzel-
nen Bits berechnet werden. Hierzu wird den Nonterminalen L und B ein synthetisches
Attribut v (value) zugeordnet und zu den Regeln der Grammatik werden die folgenden
semantischen Regeln zur Berechnung von v festgelegt:
Dabei bezeichnet v.0 den Wert des Attributs v für das Nonterminal links, v.n den Wert
des Attributs v des n-ten Zeichens (nicht nur Nonterminale!) auf der rechten Seite. Wie
man sieht, wird in diesem Beispiel das Attribut v des Startsymbols aus den Werten
der einzelnen Bits (Bottom-Up im Syntaxbaum) ermittelt.
71
4. Semantische Analyse
Wir erweitern die Grammatik um ein neues Startsymbol N, aus dem auch binäre
Festkommazahlen erzeugt werden können.
GB : N → L | L.L
L → LB | B
B → 0 |1
Der Wert einer Kommazahl muss aus den Attributen der beiden Nonterminale L
der rechten Seite der Regel N → L.L berechnet werden, wobei die Stellenlänge des
Nachkomma-Nonterminals eine Rolle spielt. Diese Länge kann mit einem weiteren syn-
thetischen Attribut l (bottom-up) berechnet werden. Es wird nur für das Nonterminal
L benötigt:
GB : N → L v.0 = v.1
N → L.L v.0 = v.1 + v.3 ∗ 2−l.3
L → LB v.0 = 2 ∗ v.1 + v.2 l.0 = l.1 + 1
L → B v.0 = v.1 l.0 = 1
B → 0 v.0 = 0
B → 1 v.0 = 1
Ein attributierter Ableitungsbaum für die Binärzahl 10.1 hat die folgende Gestalt:
N v : 2.5
@
v:2 @@ v:1
l:2 L • L l:1
@
v:1 @
l:1 L B v:0 B v:1
v:1 B 0 1
Die obige Attributierung der Binärzahlengrammatik hat den Nachteil, dass die einzel-
nen Bits unabhängig von ihrer Wertigkeit bewertet werden. Die Wertigkeit hängt von
der Position eines Bits innerhalb der Binärzahl ab. Mit einem zusätzlichen inheriten
Attribut p (Position) für B und L kann die Attributierung von GB so geändert werden,
dass jedem Bit direkt der Wert entsprechend seiner Position zugeordnet wird:
72
4.1 Attributgrammatiken
In Kombination der inheriten und synthetischen Attribute zeigt sich das Grundpro-
blem attributierter Grammatiken: die Reihenfolge der Auswertung ist nicht durch die
Definition festgelegt. In diesem Beispiel ist es sinnvoll, zunächst das synthetische At-
tribut l bottom-up, anschließend das inherite Attribut p top-down und schließlich das
synthetische Attribut v bottom-up zu ermitteln.
Für die Binärzahl 10.1 ergibt sich der folgende alternativ attributierte Ableitungsbaum:
N v : 2.5
v:2 @
@
l:2 @ v : 0.5
p:0 L • L l:1
p : −1
@
v:2 @
l:1 L B v:0 B v : 0.5
p:1 p:0 p : −1
v:2 B 0 1
p:1
/
Im allgemeinen Fall lässt sich keine Auswertungsreihenfolge angeben, wohl aber ein
Gleichungssystem mit allen Attributen bestimmen, das es zu lösen gilt. Lösbarkeits-
und Eindeutigkeitseigenschaften des Systems sowie der zur Lösung erforderliche Auf-
wand charakterisieren Klassen von Attributgrammatiken. Im folgenden definieren wir
Attributgrammatiken formal.
4.1 Attributgrammatiken
Definition 30 (Attributgrammatiken) Seien G = (N, Σ, P, S) ∈ CF G(Σ) und Att
eine Menge von Attributen. Sei außerdem eine Familie von Wertebereichen für die
Attribute gegeben: A = (A(α) | α ∈ Att).
Sei att eine Attributzuordnung att :: (N ∪ Σ) −→ IP (Att), die jedem Symbol der
Grammatik eine Menge von Attributen zuordnet.
·
Sei Att = Syn ∪ Inh eine (disjunkte) Zerlegung der Attributmenge in synthetische
und inherite Attribute. Damit zerfällt auch att in zwei Teilabbildungen:
syn : (N ∪ Σ) −→ IP (Syn) mit syn(X) = att(X) ∩ Syn
inh : (N ∪ Σ) −→ IP (Inh) mit inh(X) = att(X) ∩ Inh.
73
4. Semantische Analyse
mit f : Aα1 × . . . × Aαn −→ Aα , n ≥ 0 und α.i ∈ OV arπ ; αj .ij ∈ IV arπ für alle
j ∈ {1, . . . , n}.
Sei Eπ eine Menge von solchen Attributgleichungen von π, so dass jede Ausgabevariable
aus OV arπ genau einmal vorkommt (d.h. definiert wird, da nur links möglich). Dann
heißt
A = hG, {Eπ | π ∈ P }i
eine Attributgrammatik, in Zeichen: A ∈ AG.
Definition 31 (Attributgleichungssystem)
Sei Kn(t) die Knotenmenge von t, dann bestimmt Kn(t) die Menge V art = {α.k | α ∈
att(X); k ∈ Kn(t) markiert mit X}, die Menge der aktuellen Attributvariablen von t.
Wird an einem inneren Knoten k0 ∈ Kn(t) die Regel π : X0 → X1 · · · Xr angewandt
und sind k1 , . . . , kr die entsprechenden Nachfolgerknoten von k0 , so erhält man das
Attributgleichungssystem Ek0 von k0 aus Eπ durch die Indexsubstitution (i 7→ ki |0 ≤
i ≤ r) bei den Attributvariablen. Das Attributgleichungssystem Et von t ergibt sich
durch die Vereinigung: [
Et := Ek
(innerer Kn.)
k∈Kn(t)
Es stellt sich allerdings generell die Frage nach der eindeutigen Lösbarkeit von Attri-
butgleichungssystemen. Sie ist i.a. nicht gegeben.
4.2 Zirkularitäten
Das folgende Beispiel zeigt, dass Attributgleichungssysteme keine, genau eine oder
mehrere Lösungen besitzen können.
74
4.2 Zirkularitäten
Beispiel: Wir konstruieren eine einfache Attributgrammatik, für die bestimmte At-
tributgleichungssysteme pathologisch sind, da sie einen Zirkelbezug enthalten. G ent-
halte Regeln A → uBv (B sei i-tes Symbol der rechten Regelseite.) und B → w. Sei
α ∈ syn(B) und β ∈ inh(B). Wir betrachten die folgenden Attributgleichungen:
A
HH
H
Für einen Teilbaum der Gestalt u Bk v ergibt sich in Et die zirkuläre
w
(
α.k = g(β.k)
Abhängigkeit .
β.k = f (α.k)
Sei Aα = Aβ = N und g = idN . Dann können die folgenden drei Fälle unterschieden
werden:
75
4. Semantische Analyse
Solche Zirkularitäten sind unerwünscht. Sie entstehen erst in Et und sind in Eπ noch
nicht vorhanden.
Definition 32 (Zirkuläre Attributgrammatik) Eine Attributgrammatik A ∈ AG
heißt zirkulär, wenn es einen Ableitungsbaum t mit zirkulärem Attributgleichungssy-
stem Et in G gibt, d.h. eine aktuelle Attributvariable hängt von sich selbst ab.
Zirkularität ist entscheidbar (mit Aufwand O(2n )). Wir stellen einen Test vor, der “star-
ke Nichtzirkularität” entscheidet. Dabei benutzen wir als Hilfsmittel einen Abhängig-
keitsgraphen DGt zum Attributgleichungssystem Et . Die Knotenmenge entspricht der
Menge der Attributvariablen in t: Kn(DGt ) = V art , während die Kantenmenge angibt,
welche Abhängigkeiten zwischen den Attributvariablen bestehen. Es gilt:
E(DGt ) = {(x, y) | y = f (. . . x . . .) ∈ Et } .
Eine Kante von x nach y bedeutet, dass x direkt von y abhängt. Der Abhängigkeits-
graph zu Et setzt sich aus einzelnen bekannten Bestandteilen zusammen, nämlich den
Teilgraphen für die jeweils verwendeten Produktionen (wobei diese formale Parameter
haben, die durch Substitution in die aktuell verwendete Form gebracht werden). Die
Bestandteile kann man also ebenfalls als Graphen {DGπ | π ∈ P } betrachten und den
Gesamtgraphen analog zum Ableitungsbaum konstruieren.
Beispiel: Wir betrachten wiederum die Grammatik für binäre Gleitkommazahlen.
Wir zeigen hier nur die Abhängigkeitsgraphen für eine Auswahl von Produktionen:
N → L.L: B → 1:
N vu u B
p vu
3]
BB
0 J J B
6
J
uL uu uLJu uB
B
. ? 1
p vl p vl
L → LB: L → B:
pu L vu lu u L u u
p v l
Z
> > BB
K
A 6 6
Z A B
Z A
u u u Z
~ uB A u B u u
Z B 1
L ?B
p v l p v p v
Aus diesen Bestandteilen kann man nun den Abhängigkeitsgraphen zusammensetzen,
wobei jeweils die Variablen an der Oberseite mit den Variablen der Unterseite des
darüber liegenden Bausteins identifiziert werden (genau dies ist die oben formulierte
Substitution). /
Falls nun A ∈ AG zirkulär ist, existiert ein Ableitungsbaum, für den DGt einen Zyklus
enthält. Ein systematisches Testverfahren bestimmt alle möglichen Unterhalbverbin-
dungen zu Nonterminalen und testet dann alle Regelgraphen mit den Unterhalbver-
bindungen auf Zykel. Können auf diese Weise keine Zykel festgestellt werden, handelt es
sich um eine sogenannte stark nicht-zirkuläre Attributgrammatik. Es gibt Grammati-
ken, bei denen das skizzierte Testverfahren einen Zykel findet, obwohl keine Zirkulärität
vorliegt. Der Grund hierfür ist die Vereinigung aller möglichen Unterhalbverbindungen
zu Nonterminalen.
76
4.3 Verfahren zur Lösung von Attributgleichungssystemen
4.3.1 Termersetzung
Dieses Verfahren geht top-down nach dem Prinzip “call by name” vor. Die Attribut-
variablen werden sukzessive durch die rechten Seiten ihrer definierenden Gleichungen
ersetzt.
Beispiel: Wir betrachten das reguläre Gleichungssystem:
x1 = f (x2 , x3 )
x = g(x )
2 3
x3 = h(x4 )
x4 = a
4.3.2 Wertübergabe
Dieses Verfahren arbeitet bottom-up nach dem “call-by-value”-Prinzip. Beginnend mit
xi = ⊥ ∀Attribute xi werden alle Variablen gleichzeitig iterativ bestimmt, indem ihre
Definitionen sukzessiv eingesetzt werden.
t0 = (⊥, . . . , ⊥)
ti+1 = (τ1 , . . . , τn )[xj 7→ tij ∀j∈{1,...,n}]
Dabei werden alle Funktionen als strikt angenommen, also alle Terme mit ⊥ sind
identisch mit ⊥.
Beispiel: Im obigen Beispiel ergibt sich:
x1 ⊥ f (⊥, ⊥) ⊥ ⊥ f (g(h(a)), h(a))
x2 ⊥ g(⊥) ⊥ ⊥ g(h(a))
→ = → → ... →
x3 ⊥ h(⊥) ⊥ h(a) h(a)
x4 ⊥ a a a a
/
Interessanter als die Berechnung für konkrete Ableitungsbäume sind Verfahren, die sich
auf die Grammatik selbst beziehen und allgemeine Lösungsverfahren implementieren.
Wir sprechen zwei Verfahren an, wobei wir das erste nur kurz umreißen.
77
4. Semantische Analyse
4.4 S-Attributgrammatiken
Definition 33 (S-Attributgrammatik) Sei A ∈ AG. A heißt S-attributierte Gram-
matik, A ∈ S − AG, falls Inh = ∅, in A also keine inheriten Attribute vorkommen.
Für S-attributierte Grammatiken lassen sich die Attributwerte mit einem Bottom-Up-
Lauf berechnen. Dies kann während der Reduce-Schritte auf dem Stack erfolgen: Wird
z.B. mit der Regel A → BcD reduziert, befinden sich auf dem Stack die Symbole B, c, D
und – so die Annahme – potenzielle synthetische Attributwerte xB , xc , xD sind bereits
berechnet worden. Synthetische Attribute von A können nur von diesen Attributwerten
abhängen und können beim Reduktionsschritt berechnet und wiederum auf dem Stack
gespeichert werden.
Beispiel: Stackcode für arithmetische Ausdrücke kann mit einem synthetischen At-
tribut c für Code ermittelt werden:
E → (E + E) c.0 = c.2;c.4;ADD
| (E ∗ E) c.0 = c.2;c.4;MULT
| id c.0 = LOAD c.1
| num c.0 = LIT c.1
Die Attributwerte c.1 sind lexikalische Attribute, die mit dem Token vom Scanner
übergeben werden:
78
4.5 L-Attributgrammatiken
4.5 L-Attributgrammatiken
Definition 34 (L-Attributgrammatiken)
A = (G, hEπ | π ∈ P i) ist eine L-Attributgrammatik (A ∈ LAG) (L für “Left-to-
Right”), falls für jede Attributgleichung α.i = f (. . . β.j . . .) gilt: α ∈ Inh, β ∈ Syn ⇒
j < i.
Das bedeutet, das inherite Attribut α darf nur von synthetischen Attributen abhängen,
die in den Regeln links von ihm stehen. Eine depth-first“- Traversierung des Syntax-
”
baums von links nach rechts wird also zunächst β.j berechnen können, woraus sich
danach α.i mit i > j ergeben kann.
Es folgt:
• SAG ⊆ LAG.
@
I
@
@
@
@
- @
-
I
@
@ I
@
@ @
I
@
-@ -@ -@
@
I
@
-@
Die Berechnung der Attribute einer L-Attributgrammatik kann mit einem Top-Down-
Parser verbunden werden, bei dem die ersetzten Nonterminale auf dem Stack verblei-
ben. Erst nach der Berechnung der synthetischen Attribute werden sie entfernt. Wir
definieren diesen modifizierten Automaten.
79
4. Semantische Analyse
Das Ziel ist eine Erweiterung der Top-Down-Analyse zur Berechnung der synthetischen
Wurzelattribute. Hierzu wird die Expansion (Ableitung) von A ∈ N auf dem Analy-
sekeller so gestaltet, dass eine spätere Reduktion mit Berechnung der synthetischen
Attribute möglich ist. Beim Top-Down-Lauf werden die inheriten Attribute berechnet,
beim Bottom-Up-Lauf die synthetischen Attribute.
Dieser modifizierte TDA behält die vorigen Zustände auf dem Stack und benutzt des-
halb LR(0)-Auskünfte anstelle des einfachen (N ∪ Σ) als Kelleralphabet. Die partiellen
Abbildungen valπ sind die Stelle, wo die bisher ermittelten Attributwerte gespeichert
sind; ihr Definitionsbereich erweitert sich mit jedem Schritt. Am Ende der Analyse sind
die synthetischen Attributwerte der Wurzel S und somit aller Nachfolger bestimmt.
80
4.5 L-Attributgrammatiken
S → AB s.0 = suc(s.2)
i.1 = 0
i.2 = suc(s.1)
A → aA s.0 = suc(s.2)
i.2 = suc(i.0)
Damit kann das synthetische Attribut der Wurzel ausgegeben werden. Die Berechnung
ist in Abbildung 14 veranschaulicht.
/
Der LL-Parser weiß schon bei der Expansion eines Nichtterminals, welche Produktion
ausgewählt wird und kennt damit die für diese Alternative generierte Adressierung der
Attributvorkommen.
Die Kopplung der Attributauswertung einer L-attributierten Grammatik an einen LR-
Parser ist schwierig, da dieser erst bei der Reduktion weiß, welche Regel angewendet
wird. Alle Attributauswertungsaktionen werden daher an Reduktionen geknüpft (siehe
Buch von Wilhelm/Maurer).
81
4. Semantische Analyse
Abbildung 14: Veranschaulichung einer Syntaxanalyse mit Attributauswertung
Diese Abbildung wurde dankenswerterweise von Philipp Legrum zur Verfügung gestellt.
82
5. Übersetzung in Zwischencode (Synthesephase)
• liefert eine allgemeine Schnittstelle, an die sich Backends für verschiedene Archi-
tekturen anschließen können (Modularisierung, Portabilität),
Zwischencode wird seit der Entwicklung der ersten Pascal-Compiler eingesetzt (P-
Code). In Java-Compilern wird die Java Virtual Machine eingesetzt, in Prolog-Compi-
lern die Warren Abstract Machine (WAM) und im Glasgow Haskell Compiler die STG
Machine. Das Compiler-Backend überführt den bereits maschinennahen Zwischencode
schließlich in konkreten Maschinencode. Wichtige Teilaufgaben sind dabei die Register-
zuteilung sowie die Auswahl und Anordnung der Instruktionen.
In Abbildung 15 sind die Strukturen von höheren Programmiersprachen, Maschinenco-
de sowie Zwischencode gegenübergestellt. Der Zwischencode übernimmt Grundstruk-
turen von Programmiersprachen und beschreibt eine Realisierung abstrakter Konzepte
mit elementaren Kontroll- und Speicherstrukturen.
Wir beschreiben im folgenden die Übersetzung in einen Zwischencode inkrementell in
vier Stufen, wobei in jeder Stufe weitere Konzepte imperativer Sprachen hinzukommen.
3. Datenstrukturen
83
5. Übersetzung in Zwischencode (Synthesephase)
dieses Kapitel. Die vorkommenden Bezeichnungen wie AExp, Cmd werden als Mengen
aller nachstehend definierten Konstrukte aufgefasst und im folgenden in diesem Sinn
weiter verwendet.
Weiter wollen wir davon ausgehen, dass alle in Γ verwendeten Bezeichner vorher in ∆
deklariert und paarweise verschieden sind (kontextabhängige Eigenschaften).
Die im folgenden definierte Semantik ist denotationell, beschreibt also mit Abbildun-
gen die Effekte der Programmausführung (Zustandsänderungen in Speicher und Um-
gebung).
Die verwendeten semantischen Grundbereiche sind:
84
5.1 Übersetzung von Ausdrücken und Anweisungen
Der Zustandsraum ist die Menge der Abbildungen, welche den Speicherzustand be-
schreiben. Die Umgebung bildet die deklarierten Bezeichner auf die Speicherplätze ab.
Beides sind partielle Abbildungen (Notation: 99K); es ist weder jeder Bezeichner de-
klariert, noch jeder Speicherplatz belegt. Die Speicherplätze sind nur indirekt über die
Umgebung zugänglich.
baut die Umgebung aus den Deklarationen auf. Da wir die Deklaration aller benutzten
Bezeichner gefordert haben, ergibt sich die Umgebung vollständig daraus.
D[[ε]]ρ := ρ
D[[const I1 = Z1 ; . . . ; In = Zn ]]ρ := ρ [I1 /Z1 , . . . , In /Zn ]
D[[var I1 , I2 , . . . , In ]]ρ := ρ[I1 /α1 , . . . , In /αn ]
Aus der Deklarationssemantik der Variablendeklaration ist ersichtlich, dass eine einfa-
che Speicherverwaltung verwendet wird, bei der die ersten n Adressen sukzessive an
die Variablenbezeichner vergeben werden.
85
5. Übersetzung in Zwischencode (Synthesephase)
wird induktiv über die Struktur der Ausdrücke definiert, wobei die Semantik der
verfügbaren Operationen vorausgesetzt wird. Da die Bezeichner evtl. nicht deklariert
wurden, ist diese Abbildung partiell.
E[[Z]]ρσ := (
Z
ρ(I) falls ρ(I) ∈ Z
E[[I]]ρσ :=
σ(ρ(I)) falls ρ(I) ∈ Loc
E[[(E1 op E2 )]]ρσ := E[[E1 ]]ρσ [[op]] E[[E2 ]]ρσ
Bei der Semantik von Variablenbezeichnern I mit ρ(I) ∈ Loc unterscheidet man den
sogenannten l-Wert ρ(I), d.h. den dem Bezeichner zugeordneten Speicherplatz, der un-
veränderlich (statisch) ist, und den r-Wert σ(ρ(I)), d.h. den Inhalt des Speicherplatzes,
der während eines Programmlaufs veränderlich (dynamisch) ist. Die Bezeichnungen l-
und r-Wert beziehen sich auf die Bedeutung eines Variablenbezeichners im Kontext
einer Wertzuweisung: Auf der linken Seite ist der l-Wert gemeint, auf der rechten Seite
der r-Wert.
Die Semantik Boolescher Ausdrücke kann analog zu der arithmetischer Ausdrücke er-
folgen:
B :: BExp × Env × S 99K {true, false}.
Hierbei kann man allerdings strikte und nicht-strikte Varianten unterscheiden:
2. nicht-strikt:
Die strikten Booleschen Junktoren erfordern die Auswertung beider Argumente. Sie
werden etwa in ISO-Pascal verwendet. Zur Implementierung der sequentiellen Semantik
genügt eventuell die Auswertung des ersten Arguments. Die sequentiellen Operatoren
werden in C implementiert. Die parallele Semantik erfordert eine parallele oder ver-
zahnte (Reißverschlussprinzip) Auswertung beider Argumente und wird daher selten
implementiert.
Beispiel: Welche Semantik bei den Booleschen Ausdrücken vorausgesetzt wird, kann
sich auch auf den Programmierstil auswirken. So arbeitet die folgende Anweisungsfolge
nur korrekt, wenn die logische Konjunktion and sequentiell implementiert ist.
86
5.1 Übersetzung von Ausdrücken und Anweisungen
Falls e nicht in a vorkommt und and strikt ist, kommt es zu einem Laufzeitfehler
wegen Indexüberschreitung. Bei sequentiellem and wird die Schleife in diesem Fall mit
i = n + 1 verlassen. /
M :: P rog × S 99K S
M[[∆Γ]]σ = C[[Γ]](D[[∆]]ρ∅ )σ
BZ := N (Befehlszähler)
DK := Z∗ (Datenkeller mit Zahlen, Kellerspitze rechts)
HS := ZN := {h : N → Z} (Hauptspeicher, als Abbildung)
87
5. Übersetzung in Zwischencode (Synthesephase)
C[[RELOP]](m, d : z1 : z2 , h) := (m + 1, d(: z, h)
1 falls z1 [[relop]]z2 = true
mit z =
0 sonst.
C[[AND]](m, d : z1 : z2 , h) := (m + 1, d : z, h) mit z = min{z1 , z2 }
C[[OR]](m, d : z1 : z2 , h) := (m + 1, d : z, h) mit z = max{z1 , z2 }
C[[NOT]](m, d : z1 , h) := (m + 1, d : z, h) mit z = 1 − z1
88
5.1 Übersetzung von Ausdrücken und Anweisungen
Die Semantik eines solchen Programms P wird als Iteration der Befehle entsprechend
dem Befehlszähler definiert:
I :: MA-Code × ZR 99K ZR
(
I[[P ]] (C[[Bm ]](m, d, h)) falls a1 ≤ m < ap
I[[P ]](m, d, h) :=
(m, d, h) sonst.
Das Programm wird beendet, wenn der Befehlszähler sich nicht mehr im Programm-
bereich befindet. Ansonsten wird die Ausführungsfunktion rekursiv mit dem resultie-
renden Maschinenzustand wieder aufgerufen.
Zu diesem Zweck verwenden wir als Hilfsmittel eine Symboltabelle, um die deklarierten
Bezeichner zu verwalten. Diese wird während des Scanning/Parsing aufgebaut, was wir
aber an dieser Stelle konkretisieren.
verläuft in naheliegender Weise. Der Parameter vom Typ Adr ist jeweils die aktu-
elle Codeposition ∈ N. Streng genommen muss bei der Übersetzung fortlaufend die
Länge des erzeugten Codes berechnet und mitgeführt werden. Wir verzichten hier zur
Vereinfachung der Definitionen auf die Angabe dieser Komponente und erlauben die
89
5. Übersetzung in Zwischencode (Synthesephase)
ct(Γ1 , st, a0 + 1)
a00 : JMP (a000 )
ct(Γ2 , st, a00 + 1)
a000 : NOOP ; (nur Sprungziel)
ct(Γ, st, a0 + 1)
a00 : JMP a
Damit ergibt sich trans(∆Γ) := ct(Γ, up(∆, st∅), 1), wobei st∅ ≡ ⊥.
Beispiel: (Übersetzung von PSA in MA-Code)
Es sei das folgende Programm P gegeben:
const I = 3, J = 10;
var K, L;
K := I + L;
if K > L then K := K-L else K := 1
90
5.1 Übersetzung von Ausdrücken und Anweisungen
gilt dann:
M[[P ]]σ = σ 0 ⇐⇒ I[[trans(P )]](1, ε, hσ ) = (m, ε, hσ0 ).
91
5. Übersetzung in Zwischencode (Synthesephase)
Jumping Code. Erfolgt die Übersetzung Boolescher Ausdrücke (oben nicht wieder-
gegeben) analog zu der der arithmetischen Ausdrücken, so wird eine strikte Semantik
realisiert. Wir geben im folgenden eine alternative Übersetzung von Booleschen Aus-
drücken an, den sogenannten Jumping Code, bei der längerer Code erzeugt wird, der
aber zu erheblich kürzeren Laufzeiten führen kann. Der Jumping Code realisiert eine
sequentielle nicht-strikte Semantik.
Die Sprungziele für eine positive und eine negative Auswertung des Booleschen Aus-
drucks innerhalb von Verzweigungen und Schleifen werden bereits vorher berechnet
und bei der Übersetzung der Booleschen Ausdrücke mit eingearbeitet:
bt0 :: BExp × T ab × Adr 3 −→ MCode
92
5.2 Übersetzung von Blöcken und Prozeduren
angegebene Jumping Code zwar aus 10 Befehlen, falls aber beispielsweise σ(I) = 0
gilt, so werden nur 5 Befehle ausgeführt.
Standardübersetzung Jumping Code
(strikte Semantik)) (nicht-strikte Semantik))
LOAD I; LOAD I;
LIT 0; LIT 0;
EQ; EQ;
NOT; JPFALSE a;
LOAD J; JMP a_false;
LOAD I; a: LOAD J;
GREATER; LOAD I;
AND GREATER;
JPFALSE a_false;
JMP a_true
/
93
5. Übersetzung in Zwischencode (Synthesephase)
94
5.2 Übersetzung von Blöcken und Prozeduren
in/out X; Es gilt:
const C = 10;
var Y; 1. static scope: Beim Prozeduraufruf A()
proc A; im Anweisungsteil von B bezeichnet X je-
var Y, Z; weils die Ein-/Ausgabevariable X und Z
proc B; die lokale Variable Z von A.
var X,Z; 2. dynamic scope: Beim Prozeduraufruf
[... A() ...] A() im Anweisungsteil von B bezeichnet X
[... B() ... D() ...] die lokale Variable von B und Z die lokale
proc D; Variable von A.
[... A() ...]
[... A() ...]. 3. D kann in A aufgerufen werden, obwohl
die Deklaration textuell später erfolgt.
/
Semantik von PSP. Die semantischen Bereiche Z der ganzen Zahlen, Loc der Spei-
cherplätze (locations) und S der Speicherzustände werden unverändert von PSA über-
nommen. Hinzu kommt der neue Bereich der Speichertransformationen (continuations)
als semantische Werte für Prozeduren. In der Umgebung werden Prozedurbezeichnern
Speichertransformationen als Semantik zugeordnet. Die semantischen Grundbereiche
von PSP sind dementsprechend:
Speicherplätze (locations) Loc := {α1 , α2 , α3 , . . .}
Zustandsraum (states) S := {σ | σ : Loc 99K Z}
Speichertransf. (continuations) C := {θ | θ : S 99K S}
Umgebung (environment) Env := {ρ | ρ : Ide 99K Z ∪ Loc ∪ C}
Während die Ausdruckssemantiken unverändert von PSA übernommen werden können,
müssen die Deklarationssemantik und die Anweisungssemantik erweitert werden. Durch
die geschachtelten Prozeduren und Blöcke muss die Umgebung dynamisch erweitert
werden und neuer Speicherplatz für lokale Variablen bestimmt werden. Hierzu erhält
die Deklarationssemantik den Zustand als Parameter und modifiziert diesen durch die
Initialisierung neuer Variablen:
Deklarationssemantik
D :: Decl × Env × S 99K Env × S
D[[∆C ∆V ∆P ]]ρσ := D[[∆P ]](D[[∆V ]](D[[∆C ]]ρ)σ)
D[[ε]]ρσ := ρσ
D[[const I1 = Z1 ; . . . ; In = Zn ]]ρσ := ρ [I1 /Z1, . . . , In /Zn ] σ
D[[var I1 , I2 , . . . , In ]]ρσ := ρ[I1 7→ αj+1 , . . . , In 7→ αj+n ]
σ[αj+1 7→ 0, . . . , αj+n 7→ 0]
wobei j höchster Index eines belegten
Speicherplatzes in σ sei,
falls σ = ∅, sei j = 0
D[[proc I1 ; B1 ; . . . proc In ; Bn ; ]]ρσ := ρ[I1 7→ θ1 , . . . , In 7→ θn ]σ
95
5. Übersetzung in Zwischencode (Synthesephase)
Dabei sei für 1 ≤ i ≤ n: θi (σ) := BL[[Bi ]]ρ[I1 7→ θ1 , . . . , In 7→ θn ]σ. Dies definiert eine
static scope“-Semantik, da ρ die Deklarationsumgebung der Prozeduren ist.
”
Eine Variablendeklaration hat den Seiteneffekt, dass die neuen Speicherplätze mit Null
initialisiert werden. Dies dient der Feststellung freier Speicherplätze.
Anweisungssemantik
Die Semantik der Anweisungen wird lediglich um die Semantik für einen Prozeduraufruf
erweitert: Es gilt:
C[[I()]]ρσ := θ(σ) falls ρ(I) = θ ∈ C
Hier ist ρ die Aufrufumgebung von I(), während ρ(I) in der Deklarationsumgebung von
I berechnet wurde. Wollte man eine dynamic scope“-Semantik definieren, so müsste
”
man in der Aufrufumgebung den Aufruf I() durch den Prozedurrumpf ersetzen.
Blocksemantik
BL[[∆Γ]]ρσ := C[[Γ]](D[[∆]]ρ σ)
Die Blocksemantik entspricht der Programmsemantik von PSA, nur dass beim Betreten
eines Blocks die Umgebung nicht leer ist, sondern bereits mit umgebendem Kontext
belegt. Die Umgebung wird durch lokale Deklarationen erweitert und die Anweisungen
damit ausgeführt.
Programmsemantik
M :: P rog × Zn 99K Zn
angelegt, über die der Zugriff auf globale Variablen erfolgt. Die abstrakte Maschine
MA aus dem vorigen Abschnitt wird daher so modifiziert, dass der Hauptspeicher als
Prozedurkeller verwaltet wird. Die neue Maschine nennen wir entsprechend MP . MP
ist eine abstrakte maschine mit einem Datenkeller und einem Prozedurkeller, die in
einem vorher reservierten Speicherbereich aufeinander zuwachsen.
Der Zustandsraum ZR der MP hat die Gestalt ZR = BZ × DK × P K mit
BZ := N (Befehlszähler)
DK := Z∗ (Datenkeller mit Zahlen, Kellerspitze rechts)
P K := Z∗ (Prozedurkeller mit Kellerspitze links)
wobei:
Der statische Verweis muss bei jedem Erzeugen eines Aktivierungsblocks anhand der
Differenz zwischen Aufruf- und Deklarationsniveau, die die Länge der Verweiskette
angibt, bestimmt werden. Als Hilfsfunktion wird die folgende Funktion base definiert,
die bezüglich der Niveaudifferenz den Beginn der Deklarationsumgebung bestimmt:
base : P K × N → N
(p, 0) 7→ 1
(p, d + 1) 7→ base(p, d) + p.base(p, d)
An p.base(p, d) steht dabei jeweils der statische Verweis sv vom vorherigen Aufruf. Die
Niveaudifferenz gibt an, wie oft man den statischen Verweisen folgen muss, um zur
Deklarationsumgebung zu gelangen. Falls d = 0 ist die Aufrufumgebung die Deklara-
tionsumgebung. Falls d + 1 > 0 ermittelt man rekursiv die Deklarationsumgebung für
das Level d und folgt dann dem statischem Verweis, d.h. dem ersten Eintrag des ent-
sprechenden Aktivierungsblocks, p.base(p, d) um den Aktivierungsblock der Umgebung
mit dem Level d + 1 unterhalb der aktuellen Umgebung zu finden.
97
5. Übersetzung in Zwischencode (Synthesephase)
in/out X;
const C = 10;
var Y;
proc A;
var Y, Z;
proc B;
var X,Z;
[... A() ...]
[... B() ... D() ...]
proc D;
[... A() ...]
[... A() ...].
Bei der Ausführung des Programms hat der Prozedurkeller nach dem zweiten Aufruf
von A die folgende Struktur:
?
? ? ?
? ? ? ?
15 4 5 4 5 4 4 3 0 0 0 12
sv dv ra Y Z sv dv ra X Z sv dv ra Y Z sv dv ra Y sv dv ra X
A() B() A() Main I/O
Der zweite Aufruf von A hat eine Niveaudifferenz von 2 zum Deklarationsniveau der
Prozedur A. Der statische Verweis des obersten Aktivierungsblocks wurde somit wie
folgt berechnet:
base(p, 0) = 1
base(p, 1) = 1 + p.1 = 6
base(p, 2) = 6 + p.6 = 11
Es folgt:
sv = base(p, 2) + |{z}
2 + |{z}
2 = 15.
dv,ra lv
/
Prozedurkellerbefehle
• CALL (ca, diff, lv) erzeugt einen Aktivierungsblock mit Platz für lv lo-
kalen Variablen auf dem Prozedurkeller und springt zum Codeanfang ca des
Prozedurrumpfes. diff sei die Differenz zwischen Aufruf- und Deklarations-
niveau der Prozedur.
98
5.2 Übersetzung von Blöcken und Prozeduren
Transportbefehle dienen zum Laden und Speichern von Variablenwerten vom Pro-
zedurkeller auf den Datenkeller bzw. vom Datenkeller in den Prozedurkeller.
Der Zugriff auf Variablen erfolgt wegen der dynamischen Speicherverwaltung re-
lativ. Die Lade- und Speicherbefehle erhalten als Parameter die Niveaudifferenz
diff zwischen Auftreten und Deklaration der Variablen sowie den Offset off als
relative Adresse im Aktivierungsblock.
• LOAD (diff,off)
C[[LOAD(diff , off )]](m, d, p) := (m + 1, d : p.(base(p, diff ) + 2 + off ), p)
• STORE (diff,off)
C[[STORE(diff , off )]](m, d : z, p) := (m + 1, d, p[base(p, diff ) + 2 + off 7→ z])
Die Menge der MP-Programme sei analog zu der Menge der MA-Programme definiert.
<SyntaxBereich> × Tab
|{z} × Adr
|{z} × Lev
|{z} 99K MP-Code
Symboltabelle Anfangsadresse Blockschachtelungstiefe
99
5. Übersetzung in Zwischencode (Synthesephase)
Die Einträge für Variablen- und Prozedurdeklarationen sind neu. Zu jeder Variablende-
klaration werden das Deklarationsniveau dl ∈ Lev und die Offsetadresse der Variablen
innerhalb des Aktivierungsblocks off ∈ Off := N in der Symboltabelle gespeichert.
Für Prozeduren werden die Startadresse a ∈ Adr , das Deklarationsniveau dl ∈ Lev
und die Anzahl lokaler Variablen lv ∈ Size := N gemerkt. Die leere Symboltabelle wird
wie bisher mit st∅ bezeichnet.
erhält als weitere Parameter eine Anfangsadresse sowie das aktuelle Niveau. Es gilt:
Die Hilfsfunktion size : Block → N bestimme die Anzahl der lokal deklarierten Varia-
blen in einem Block. Statt einer direkten Adressberechnung mittels einer Codelängen-
funktion verwenden wir symbolische Adressen a1 , . . . , an für die Anfangsadressen der
Prozedurcodes.
(1, ε, 0 : 0 : 0 : z1 : . . . : zn )
mit Startadresse 1 des MP-Codes, leerem Datenkeller ε und I/O-Block auf dem Proze-
durkeller festgelegt. Bei der Übersetzung wird entsprechend die folgende Anfangssym-
boltabelle verwendet:
100
5.2 Übersetzung von Blöcken und Prozeduren
Blockübersetzung.
bt(∆Γ, st, a, l) := dt(∆, up(∆, st, a1 , l), a1 , l);
ct(Γ, up(∆, st, a1 , l), a, l);
a0 : RET
Dabei erzeugt dt Code für die Prozedurrümpfe in ∆ und ct erzeugt mit der angegebenen
Startadresse den Code für den Anweisungsteil des Blocks. Dieser Code endet mit einem
Rücksprung.
101
5. Übersetzung in Zwischencode (Synthesephase)
Übersetzung von Ausdrücken. Die Funktion et muss nur für Bezeichner neu de-
finiert werden:
(
a:LIT z falls st(I) = (const, z)
et(I, st, a, l) :=
a:LOAD(l − dl, off ) falls st(I) = (var, dl, off )
Damit folgt:
dt(∆, st, a1 , 1) = bt(∆F ΓF , st, a11 , 2);
= ct(ΓF , st, a11 , 2)
a3 : RET
und
ct(Γ, st, aΓ , 1) = aΓ : LIT 1; .
STORE (0, 1);
CALL (a11 , 0, 0);
LOAD (0, 1);
STORE (1, 1);
Es bleibt die Übersetzung der Prozedur F:
ct(ΓF , st, a11 , 2) = et(1 < X, st, a11 , 2);
a4 : JPFALSE a5 ;
ct(begin . . . end, st, a4 + 1, 2)
a5 :
Es gilt:
102
5.2 Übersetzung von Blöcken und Prozeduren
trans(in/out X; ∆Γ)
= 1: CALL (17, 0, 1);
2: JMP 0;
a11 = 3 : LIT 1;
4: LOAD (2, 1);
5: LESS;
a4 = 6 : JPFALSE 16;
7: LOAD (1, 1);
8: LOAD (2, 1);
9: MULT;
10 : STORE (1, 1);
11 : LOAD (2, 1);
12 : LIT 1;
13 : SUB;
14 : STORE (2, 1);
15 : CALL (3, 1, 0);
a3 = a5 = 16 : RET;
aΓ = 17 : LIT 1;
18 : STORE (0, 1);
19 : CALL (3, 0, 0);
20 : LOAD (0, 1);
21 : STORE (1, 1);
a2 = 22 : RET.
103
5. Übersetzung in Zwischencode (Synthesephase)
m ∈ BZ d ∈ DK p ∈ PK
1 ε 0:0:0:3
17 ε 4:3:2:0:0:0:0:3
18 1 4:3:2:0:0:0:0:3
19 ε 4:3:2:1:0:0:0:3
3 ε 3 : 2 : 20 : 4 : 3 : 2 : 1 : 0 : 0 : 0 : 3
4 1 3 : 2 : 20 : 4 : 3 : 2 : 1 : 0 : 0 : 0 : 3
5 1:3 3 : 2 : 20 : 4 : 3 : 2 : 1 : 0 : 0 : 0 : 3
6 1 3 : 2 : 20 : 4 : 3 : 2 : 1 : 0 : 0 : 0 : 3
7 ε 3 : 2 : 20 : 4 : 3 : 2 : 1 : 0 : 0 : 0 : 3
8 1 3 : 2 : 20 : 4 : 3 : 2 : 1 : 0 : 0 : 0 : 3
9 1:3 3 : 2 : 20 : 4 : 3 : 2 : 1 : 0 : 0 : 0 : 3
10 3 3 : 2 : 20 : 4 : 3 : 2 : 1 : 0 : 0 : 0 : 3
11 ε 3 : 2 : 20 : 4 : 3 : 2 : 3 : 0 : 0 : 0 : 3
12 3 3 : 2 : 20 : 4 : 3 : 2 : 3 : 0 : 0 : 0 : 3
13 3:1 3 : 2 : 20 : 4 : 3 : 2 : 3 : 0 : 0 : 0 : 3
14 2 3 : 2 : 20 : 4 : 3 : 2 : 3 : 0 : 0 : 0 : 3
15 ε 3 : 2 : 20 : 4 : 3 : 2 : 3 : 0 : 0 : 0 : 2
3 ε 6 : 2 : 16 : 3 : 2 : 20 : 4 : 3 : 2 : 3 : 0 : 0 : 0 : 2
4 1 6 : 2 : 16 : 3 : 2 : 20 : 4 : 3 : 2 : 3 : 0 : 0 : 0 : 2
5 1:2 6 : 2 : 16 : 3 : 2 : 20 : 4 : 3 : 2 : 3 : 0 : 0 : 0 : 2
6 1 6 : 2 : 16 : 3 : 2 : 20 : 4 : 3 : 2 : 3 : 0 : 0 : 0 : 2
7 ε 6 : 2 : 16 : 3 : 2 : 20 : 4 : 3 : 2 : 3 : 0 : 0 : 0 : 2
8 3 6 : 2 : 16 : 3 : 2 : 20 : 4 : 3 : 2 : 3 : 0 : 0 : 0 : 2
9 3:2 6 : 2 : 16 : 3 : 2 : 20 : 4 : 3 : 2 : 3 : 0 : 0 : 0 : 2
10 6 6 : 2 : 16 : 3 : 2 : 20 : 4 : 3 : 2 : 3 : 0 : 0 : 0 : 2
11 ε 6 : 2 : 16 : 3 : 2 : 20 : 4 : 3 : 2 : 6 : 0 : 0 : 0 : 2
12 2 6 : 2 : 16 : 3 : 2 : 20 : 4 : 3 : 2 : 6 : 0 : 0 : 0 : 2
13 2:1 6 : 2 : 16 : 3 : 2 : 20 : 4 : 3 : 2 : 6 : 0 : 0 : 0 : 2
14 1 6 : 2 : 16 : 3 : 2 : 20 : 4 : 3 : 2 : 6 : 0 : 0 : 0 : 2
15 ε 6 : 2 : 16 : 3 : 2 : 20 : 4 : 3 : 2 : 6 : 0 : 0 : 0 : 1
3 ε 9 : 2 : 16 : 6 : 2 : 16 : 3 : 2 : 20 : 4 : 3 : 2 : 6 : 0 : 0 : 0 : 1
4 1 9 : 2 : 16 : 6 : 2 : 16 : 3 : 2 : 20 : 4 : 3 : 2 : 6 : 0 : 0 : 0 : 1
5 1:1 9 : 2 : 16 : 6 : 2 : 16 : 3 : 2 : 20 : 4 : 3 : 2 : 6 : 0 : 0 : 0 : 1
6 0 9 : 2 : 16 : 6 : 2 : 16 : 3 : 2 : 20 : 4 : 3 : 2 : 6 : 0 : 0 : 0 : 1
16 ε 9 : 2 : 16 : 6 : 2 : 16 : 3 : 2 : 20 : 4 : 3 : 2 : 6 : 0 : 0 : 0 : 1
16 ε 6 : 2 : 16 : 3 : 2 : 20 : 4 : 3 : 2 : 6 : 0 : 0 : 0 : 1
16 ε 3 : 2 : 20 : 4 : 3 : 2 : 6 : 0 : 0 : 0 : 1
20 ε 4:3:2:6:0:0:0:1
21 6 4:3:2:6:0:0:0:1
22 ε 4:3:2:6:0:0:0:6
0 ε 0:0:0:6
104
5.3 PSPP: Übersetzung von parametrisierten Prozeduren
Der Beweis dieses Satzes wurde von M.Mohnen in der Zeitschrift Fundamentae Infor-
maticae7 publiziert.
105
5. Übersetzung in Zwischencode (Synthesephase)
ZRM P P := STACK × IC × SP × FP × R,
wobei
• R := SAdr Indexregister
106
5.3 PSPP: Übersetzung von parametrisierten Prozeduren
.. ↑ hohe Adressen
.
par1
)
.. aktuelle Wert- und
. Variablenparameter
parp+q
loc1
Stack Pointer .. lokale Variablen
Q .
Q
Q
Qs
Q locn
Befehl Bedeutung
CALL ca SP ← SP − 1; hSPi ← IC + 1; IC ← ca
RET k IC ← hSPi; SP ← SP + |k {z
+ 2}
pars+sv+ra
PUSH Z SP ← SP − 1; hSPi ← Z
PUSH FP SP ← SP − 1; hSPi ← F P
PUSH hFPi SP ← SP − 1; hSPi ← hFPi
PUSH hR + oi SP ← SP − 1; hSPi ← hR + oi
POP FP FP ← hSPi; SP ← SP + 1
POP hni Stack[n] ← hSPi; SP ← SP + 1
SUB SP,n SP ← SP − n
LOAD FP, SP FP ← SP
LOAD R, hni R ← Stack[n]
LOAD R, hR + 2i R ← hR + 2i
107
5. Übersetzung in Zwischencode (Synthesephase)
tet der RET-Befehl, dass auf der Kellerspitze die Rücksprungadresse steht, die in das
Instruktionszählerregister gespeichert wird. Anschließend wird der Kellerzeiger inkre-
mentiert, was das Entfernen der Prozedurparameter, des statischen Verweises und der
Rücksprungadresse vom Keller bewirkt. Elementare PUSH- und POP-Befehle stehen
nun zur Verfügung, um den Keller zu modifizieren. Diese ersetzen auch die bisherigen
LOAD- und STORE-Befehle.
Vor einem Prozeduraufruf müssen die aktuellen Parameter sowie der statische Verweis
auf dem Keller erzeugt werden. Zur Berechnung des statischen Verweises wird das
Indexregister verwendet. Mittels der neuen LOAD-Befehle kann die Verweiskette im
Keller verfolgt werden und schließlich der aktuelle Inhalt des Indexregisters mittels eines
PUSH-Befehls auf den Keller geschrieben werden. Nach einem Prozeduraufruf muss
der Rahmenzeiger mittels PUSH als dynamischer Verweis gesichert und mittels POP
neu gesetzt werden und mittels SUB Speicherplatz für die lokalen Prozedurvariablen
reserviert werden.
Insgesamt erfolgt der Aufbau eines Aktivierungsblocks in zwei Codeabschnitten:
Code für Prozeduraufruf:
108
5.3 PSPP: Übersetzung von parametrisierten Prozeduren
Wertparameter werden wie lokale Variablen behandelt. Lokale Variablen erhalten nun
negative Offsets durch den Bezug auf den Rahmenzeiger, während Wertparameter po-
sitive Offsets haben. Im Unterschied zu MP werden nun beliebige ganze Zahlen als
Offsets verwendet:
Off := Z.
Die Anfangstabelle stI/O enthält für die Ein-/Ausgabe-Variablen Ij entsprechend fol-
gende Einträge:
stI/O (Ij ) := (var, 0, −j).
Der Aufbau der Symboltabelle erfolgt wie bisher mit der Funktion up. Der einzige Un-
terschied besteht darin, dass für lokale Variablen negative Offsets eingetragen werden.
Beachten Sie, dass lokale Variablen durch up in die Symboltabelle eingetragen werden,
während dies für Parametervariablen durch die Übersetzungsfunktion dt geschieht.
Programmübersetzung. Bei der Übersetzung von Programmen wird vor dem CALL-
Befehl, der nur noch die Rücksprungadresse auf dem Keller speichert, mittels des Befeh-
les PUSH FP ein initialer statischer Verweis auf den Ein-/Ausgabe-Aktivierungsblock
auf dem Keller erzeugt.
trans(in/out I1 , . . . , In ; B) := 1 : PUSH FP %sv
2 : CALL aΓ %ra
3 : JMP 0; %ST OP
bt(B, stI/O , aΓ , 1, |{z}
0 ).
# P arameter
Blockübersetzung. Der Code für die Ausführung des Anweisungsteils eines Blocks
wird eingerahmt durch einen Entry- und Exit-Code. Im Entry-Code wird der Akti-
vierungsblock durch Anlegen des dynamischen Verweises und Reservieren von Spei-
cherplatz für die lokalen Variablen fertig gestellt. Der Befehl PUSH FP schreibt den
aktuellen Rahmenzeiger auf den Keller und lädt den aktuellen Kellerzeiger als neuen
Rahmenzeiger. Mittels des SUB-Befehls wird der Kellerzeiger nun dekrementiert, um
Platz für die lokalen Variablen zu schaffen. Beachten Sie, dass keine Initialisierung der
lokalen Variablen erfolgt. Der Exit-Code setzt den Kellerzeiger mittels LOAD SP, FP
auf die Stelle, an der der dynamische Verweis, d.h. der vorherige Inhalt von FP, steht.
Hierdurch wird der gesamte Kellerinhalt oberhalb dieser Zelle gelöscht. Der Befehl POP
FP restauriert den vorherigen Rahmenzeiger und entfernt dessen Wert vom Keller, so
109
5. Übersetzung in Zwischencode (Synthesephase)
dass nun die Rücksprungadresse auf der Kellerspitze steht. Der RET-Befehl verwendet
diese zum Rücksprung und entfernt außerdem die Parameter vom Keller. Die Anzahl
r der Parameter wird der Funktion bt als neuer Parameter übergeben. Damit kann der
RET-Befehl den gesamten restlichen Aktivierungsblock vom Keller entfernen.
110
5.3 PSPP: Übersetzung von parametrisierten Prozeduren
111
5. Übersetzung in Zwischencode (Synthesephase)
112
5.3 PSPP: Übersetzung von parametrisierten Prozeduren
6 Codeoptimierung
6.1 Gleitfenster-Optimierungen
Unter Gleitfenster-Optimierungen (engl. peephole - Schlüsselloch) versteht man eine
sehr einfache, aber effektive Optimierungsform. Mit einem Gleitfenster wird jeweils ein
Ausschnitt meist aufeinanderfolgender Instruktionen betrachtet. Dabei wird versucht,
für die jeweils im Gleitfenster befindlichen Instruktionen eine kürzere oder schnellere
Codefolge zu bestimmen. Typische Peephole-Optimierungen sind:
2. Kontrollflussoptimierungen
Eine typische Kontrollflussoptimierung ist die Komprimierung von Sprungfolgen:
JMP L1; ... L1: JMP L2; ... kann modifiziert werden zu
JMP L2; ... L1: JMP L2; .... Die mit der Sprungmarke L1 markierte Sprun-
ganweisung ist dann eventuell nicht mehr erreichbar und kann eliminiert werden.
Eine analoge Transformation ist möglich, falls der erste Sprungbefehl bedingt ist.
114
6.2 Allgemeine Analysetechniken
ja
nein
E ¬E
ja
A nein
A
A
E A
¬E
A
A
Eine Approximation, die nie ein definitives Ergebnis liefert, ist trivialerweise sicher,
aber natürlich ein sinnloser Grenzfall. Man sollte stets bemüht sein, möglichst genaue
Approximationen zu entwickeln.
Spekulative Approximationen werden häufig bei Verifikationen eingesetzt. Hier gilt:
Nicht gemeldete Fehler sind schlimmer als gemeldete Nicht-Fehler“.
”
115
6. Codeoptimierung
ja nein
E ¬E
6.3 Drei-Adress-Code
Die meisten heutigen Maschinen sind Registermaschinen, so dass als Zwischencode
meist sog. Drei-Adress-Code statt der Datenkellertechnik verwendet wird. In einem
Drei-Adress-Code sind die elementaren Anweisungen Zuweisungen der Form
x := y op z.
x, y und z sind drei Adressen bzw. Variablenbezeichner. Sie sind Exemplare eines unbe-
grenzten Registervorrats. Auf den rechten Zuweisungsseiten treten keine geschachtelten
Ausdrücke auf. Alle Zwischenergebnisse von komplexen Ausdrücken sind im Code als
Zuweisung erkennbar.
6.1 Definition (Drei-Adress-Anweisungen und -Programme)
Seien Var eine Menge von Variablenbezeichnern, Lab eine Menge von Sprungmar-
ken, Proc eine Menge von Prozedurbezeichnern und Op eine Menge von binären
und unären Operationssymbolen.
Die Menge der Drei-Adress-Anweisungen Instr enthält
116
6.3 Drei-Adress-Code
• Zuweisungen
x := y op z
x := op y (x, y, z ∈ Var, op ∈ Op)
x := y
• Sprünge )
goto l
(x, y ∈ Var, op ∈ Op, l ∈ Lab)
if x op y goto l
• Prozeduraufrufe
call p, x1 , . . . , xn
call p, x1 , . . . , xn → y
(p ∈ Proc, x1 , . . . , xn , y, x ∈ Var
return
return x
Sei LInstr := {l : i | l ∈ Lab, i ∈ Instr} die Menge der Anweisungen mit Marke.
Die Menge Prog der Drei-Adress-Programme ist definiert durch:
• Prozedurparameter: a, a0 , a1 . . .
• globale Variablen: g, g0 , g1 . . .
Beispiel: Ein Drei-Adress-Programm zur Multiplikation von Zahlen ist etwa gegeben
durch
117
6. Codeoptimierung
Eine Übersetzung von Drei-Adress-Code in MPP-Code ist sehr einfach. Die Überset-
zung von PSPP (ohne Variablenparameter) in Drei-Adress-Code erfordert
• das Entschachteln von Ausdrücken
E1 op E2 hCode für E1 mit Resultat in x1 i
hCode für E2 mit Resultat in x2 i
x3 := x1 op x2
• das Flachklopfen von Prozeduraufrufen
p(E1 , . . . , En ) hCode für E1 mit Resultat in x1 i
..
.
hCode für En mit Resultat in xn i
call p, x1 , . . . , xn
Beispiel: Die Übersetzung der folgenden C-Prozedur mult ergibt den oben angege-
benen Drei-Adress-Code:
118
6.4 Basisblockdarstellung und Datenflussgraphen
wobei gilt:
1. Das Abwickeln der Graphen ergibt w, bis auf das Vertauschen unabhängiger
Blöcke und das Hinzufügen zusätzlicher Label:
2. Jedes σ(l) = i1 . . . in ist ein Basisblock, d.h. nur der letzte Befehl darf ein
Sprungbefehl sein:
r := 0
? ?
L1 : if a1 > 0 goto L2
A
A
AU
119
6. Codeoptimierung
• Es gibt keine Sprünge innerhalb von Basisblöcken. Nur am Ende ist ein Sprung
zugelassen, aber nicht notwendig.
• Es gibt keine Sprünge von anderen Basisblöcken in das Innere eines Basisblocks,
sondern nur an den Anfang.
Sprünge sind bedingte oder unbedingte Sprungbefehle und Prozedurrücksprünge. Pro-
zeduraufrufe werden nicht als Sprünge betrachtet, damit Basisblöcke möglichst groß
und die Graphen entsprechend klein werden. Da Prozeduren in der Regel am Ende
ihrer Ausführung die Kontrolle zurückgeben, können sie als komplexe Operationen ge-
sehen werden. Dies ist natürlich nur legitim, wenn die Prozeduren seiteneffektfrei sind.
Ansonsten müssen auch Prozeduraufrufe wie Sprünge behandelt werden.
Man bestimmt die Basisblockdarstellung zu einem Drei-Adress-Programm wie folgt:
1. Bestimme zunächst alle Instruktionen, die Anfänge von Basisblöcken sind. Man
nennt diese Instruktionen Leader.
Die Menge der Leader umfasst
2. Zu jedem Leader wird der zugehörige Basisblock konstruiert. Dieser umfasst die
Folge aller Anweisungen bis zum nächsten Leader oder bis zum Ende einer Pro-
zedur.
3. Die Kanten zwischen den Basisblöcken werden entlang der Sprünge und sequen-
tiellen Fortsetzungen nach bedingten Sprüngen eingefügt.
Dieses Verfahren liefert einen Basisblockgraphen mit minimaler Knotenzahl. Jede Zer-
teilung von Blöcken liefert wieder einen Basisblockgraphen. Der minimale Graph ist
bis auf Isomorphie eindeutig.
120
6.5 Lokale Analyse
if a1 == 0 goto L1
@
@
@R
@
L1 : r := a2 x := a1 − 1
call add, x, a2 → r
? r := r + 1
L2 : return r goto L2
Beispiel: Ein typisches Beispiel für eine Vorwärtsanalyse ist die Konstanten-
propagation. Ein Ausdruck heißt genau dann konstant, wenn er unabhängig von
der Ausführung des Programms immer den gleichen Wert hat.
Wir betrachten die Sequenz
...
x := 3
y := x + 4
...
Offensichtlich ist x konstant. y ist konstant, falls die Zuweisung an y kein Sprung-
ziel ist, sich also innerhalb eines Basisblocks befindet. /
Beispiel: Ein Beispiel für eine Rückwärtsanalyse ist die Bestimmung nutzloser
Zuweisungen (dead code elimination). Wir betrachten die Sequenz
...
x := y + z
y := y + z
x := y − z
...
Befindet sich diese Sequenz innerhalb eines Basisblocks, so ist die erste Zuweisung
an x nutzlos, weil der zugewiesene Wert nicht verwendet wird, bevor er wieder
überschrieben wird. /
121
6. Codeoptimierung
Eine lokale Analyse kann wie folgt mathematisch modelliert werden. Zur Vereinfachung
verzichten wir hier auf die Betrachtung von Prozeduraufrufen. Der Ausgangspunkt ist
die Modellierung der zu analysierenden Eigenschaft als Menge M. Handelt es sich um
eine Eigenschaft von Variablen, so wird häufig M als Menge aller Abbildungen von
Variablen auf abstrakte Werte gewählt: M = V ar → AbsV al.
Ein Startwert m0 ∈ M beschreibt die Anfangssituation für die Analyse. Bei einer
Vorwärtsanalyse ist dies die Situation vor der Ausführung des Basisblocks. Bei einer
Rückwärtsanalyse beschreibt m0 die Situation nach Ausführung des Basisblocks.
Die einzelnen Instruktionen modifizieren die Werte von M, also die Eigenschaften m ∈
M. Eine abstrakte Semantik
A : Instr → M → M
formalisiert dies. Die abstrakte Semantik muss nur für Zuweisungen definiert werden,
denn Sprünge haben nur Auswirkungen auf den Kontrollfluss, nicht aber auf die Ei-
genschaften, d.h. die abstrakte Semantik entspricht der Identitätsfunktion idM :
A[[goto L]] = A[[if x op y goto L]] = A[[return x]] = A[[return]] = idM
Instruktionssequenzen i1 . . . in können einfach durch Komposition der abstrakten Se-
mantik der einzelnen Instruktionen interpretiert werden, d.h. soll die Eigenschaft vor
bzw. nach Auswertung der Instruktion ik bestimmt werden, so gilt:
• Vorwärtsanalyse: A : Instr + → M → M mit
A[[i1 . . . ik−1 ]] := A[[ik−1 ]] ◦ . . . ◦ A[[i1 ]].
122
6.5 Lokale Analyse
i1 x := 3
i2 y := x + 4
i3 z := z + y
Es folgt
• A[[i1 ]]m0 = m0 [x 7→ 3]
• A[[i1 ; i2 ]]m0 = m0 [x 7→ 3, y 7→ 7]
• A[[i1 ; i2 ; i3 ]]m0 = m0 [x 7→ 3, y 7→ 7]
x := 3
y := 7
z := z + y
/
123
6. Codeoptimierung
m(x) = 1 bedeutet für eine Zuweisung x := y op z, dass die Zuweisung nutzlos ist, weil
der Wert von x bis zur nächsten Zuweisung an x nicht verwendet wird. Der Startwert
bedeutet, dass am Ende eines Blocks davon ausgegangen wird, dass alle Variablen
verwendet werden, also nützlich sind.
3. Die abstrakte Zuweisungssemantik wird wie folgt definiert:
A[[x := y op z]]m := m[x 7→ 1, y 7→ 0, z 7→ 0].
Dies bedeutet, dass vorherige x-Werte nutzlos werden, während y und z gebraucht
werden. Beachten Sie, dass die Reihenfolge relevant ist, da x, y und z nicht
verschieden sein müssen.
Beispiel: Wir betrachten die Sequenz
i1 x := y + z
i2 y := y + z
i3 x := y − z
Es folgt
• A[[i3 ]]m0 = m0 [x 7→ 1, y 7→ 0, z 7→ 0] = m0 [x 7→ 1]
• A[[i2 ; i3 ]]m0 = m0 [x 7→ 1][y 7→ 1, y 7→ 0, z 7→ 0] = m0 [x 7→ 1]
• A[[i1 ; i2 ; i3 ]]m0 = m0 [x 7→ 1][x 7→ 1, y 7→ 0, z 7→ 0] = m0 [x 7→ 1]
Wegen A[[i2 ; i3 ]]m0 = m0 [x 7→ 1] kann die erste Zuweisung an x (Instruktion i1 ) entfernt
werden. /
124
6.5 Lokale Analyse
2. Als Startwert m0 wird der leere Graph festgelegt. Ziel der abstrakten Semantik
ist der Abhängigkeitsgraph des gesamten Blocks.
− {d}
∗ {a} A
AAU
A[[i1 ]]m0 = A A[[i1 ; i2 ]]m0 = d
AA
U
∗ {a}
b c A
AAU
b c
+ {f}
− {d} @
@
A @
− {d}
AA
U A
A[[i1 ; i2 ; i3 ]]m0 = d ∗ {a,e} A[[i1 ; i2 ; i3 ; i4 ]]m0 = AAU
A d ∗ {a,e}
AA
U A
b c
AAU
b c
125
6. Codeoptimierung
+ {f}
@
@@
− {d}
A
A[[i1 ; i2 ; i3 ; i4 ; i5 ]]m0 =
AA
U
d ∗ {a,e,i}
A
AA
U
b c
Final ergibt sich der folgende Graph
+ {f,c}
@
@@
− {d}
A
A[[i1 ; i2 ; i3 ; i4 ; i5 ; i6 ]]m0 = AAU
d ∗ {a,e,i}
A
AAU
b c
126
6.6 Fallstudie: Code-Optimierung am Beispiel von Quicksort
i = m-1 t7 = 4*i
j = n t8 = 4*j
t1 = 4*n t9 = a[t8]
v = a[t1] a[t7] = t9
L1: i = i+1 t10 = 4*j
t2 = 4*i a[t10] = x
t3 = a[t2] goto L1
if t3<v goto L1 L3: t11 = 4*i
L2: j = j-1 x = a[t11]
t4 = 4*j t12 = 4*i
t5 = a[t4] t13 = 4*n
if t5>v goto L2 t14 = a[t13]
if i>=j goto L3 a[t12] = t14
t6 = 4*i t15 = 4*n
x = a[t6] a[t15] = x
In diesem Code wird angenommen, dass die einzelnen Array-Werte als Worte gespei-
chert werden. Die indizierte Adressierung erfolgt allerdings byteweise. Daher werden
die Array-Indizes jeweils durch Multiplikation mit 4 in Byteadressen überführt. Zu
diesem Code erhält man die in Abbildung 23 gezeigte Basisblockdarstellung.
127
6. Codeoptimierung
y L3: y L3:
t6 = 4*i t6 = 4*i t11 = 4*i t11 = 4*i
x = a[t6] x = a[t6] x = a[t11] x = a[t11]
t7 = 4*i t12 = 4*i
t8 = 4*j t8 = 4*j t13 = 4*n t13 = 4*n
t9 = a[t8] t9 = a[t8] t14 = a[t13] t14 = a[t13]
a[t7] = t9 a[t6] = t9 a[t12] = t14 a[t11] = t14
t10 = 4*j t15 = 4*n
a[t10] = x a[t8] = x a[t15] = x a[t13] = x
goto L1 goto L1
Die doppelten Berechnungen werden jeweils ersatzlos gestrichen. Referenzen auf die ent-
sprechenden Variablen werden ersetzt durch die Variable, die den Wert bereits enthält,
d.h. t6 wird z.B. für t7 eingesetzt.
Andere lokale Optimierungen sind in diesem Beispiel nicht möglich. Als Ausgangspunkt
für die globale Optimierung erhält man entsprechend den in Abbildung 24 angegebenen
lokal optimierten Datenflussgraphen.
An diesem Beispiel erläutern wir im folgenden eine Reihe typischer globaler Optimie-
rungen.
Wenn sich ein Teilausdruck auf keinem möglichen Pfad durch den Datenflussgraphen
ändert, kann die Eliminierung gemeinsamer Teilausdrücke auch global durchgeführt
128
6.6 Fallstudie: Code-Optimierung am Beispiel von Quicksort
Obwohl der Wert von t1 = 4*n sich bei der Ausführung des Codes nicht ändert, darf
man hingegen die Zuweisung t14 = a[t1] nicht durch t14 = v ersetzen, obwohl der
Wert von v während der gesamten Ausführung unverändert bleibt. Der Grund hierfür
ist, dass es möglich ist, dass das Array a zwischen dem ersten Zugriff auf a[t1] in
der Zuweisung v = a[t1] und dem späteren Zugriff in der Zuweisung t14 = a[t1]
modifiziert wurde. Dies ist bei den oben beschriebenen Ersetzungen nicht möglich. Es
ergibt sich der in Abbildung 25 gezeigte Datenflussgraph.
129
6. Codeoptimierung
?
L2: j = j-1
t4 = 4*j
t5 = a[t4]
if t5>v goto L2
6.6.4 Codeverschiebung
.
.
Beispiel: Wir betrachten die Sequenz L : .
x := y op z
.
.
.
if ...goto L
130
6.6 Fallstudie: Code-Optimierung am Beispiel von Quicksort
?
L2: j = j-1
t4 = 4*j
t5 = a[t4]
if t5>v goto L2
Eine Variable x heißt Induktionsvariable einer Schleife, falls sich der Wert von x bei
Zuweisung um eine (positive oder negative) Konstante c erhöht. Dies muss in einer
Inkrementierung pro Schleifendurchlauf geschehen. Häufig existieren mehrere Indukti-
onsvariablen per Schleife. Manchmal ist es möglich, mehrere Induktionsvariablen durch
eine zu ersetzen.
Unter Operatorreduktion (engl. strength reduction) versteht man das Ersetzen von teu-
ren Operationen durch billigere. Standardbeispiele sind die Verwendung von Additio-
nen oder Shift-Operationen statt Multiplikationen, falls möglich, oder die Verwendung
von speziellen Inkrementierungsbefehlen statt allgemeiner Addition.
131
6. Codeoptimierung
i = m-1
j = n
t1 = 4*n
- if i >= j goto L3
v = a[t1]
...............
@
t2 = 4*i @
t4 = 4*j R
@
a[t2] = t5 L3: t14 = a[t1]
a[t4] = t3 a[t2] = t14
?
L1: t2 = t2+4 goto L1 a[t1] = t3
t3 = a[t2]
if t3<v goto L1
?
L2: t4 = t4-4
t5 = a[t4]
if t5>v goto L2
Im Laufbeispiel (siehe Abbildung 26) erkennt man schnell, dass die Variablen i und j
in den Blöcken L1 und L2 Induktionsvariablen sind. Deswegen können die wiederholten
Multiplikationen von i und j mit 4 durch billigere Additionen t2 := t2+4 bzw. t4
:= t4-4 ersetzt werden. Hierfür ist es allerdings erforderlich, die Variablen t2 und
t4 im Anfangsblock zu initialisieren. Jetzt sind sowohl i und j als auch t2 und t4
Induktionsvariablen. Nach diesen Änderungen kommen i und j nur noch im Vergleich
des bedingten Sprungbefehls nach Block L2 vor. Nutzt man aus, dass i >= j genau
dann gilt, wenn t2 >= t4, können i und j vollständig aus den Blöcken L1 und L2
eliminiert werden. Dies führt zu dem in Abbildung 27 gezeigten Endergebnis unserer
Folge von Optimierungen.
Diese Fallstudie hat gezeigt, dass Optimierungen meistens von innen nach außen durch-
geführt werden sollten und dass Optimierungen weitere Optimierungsmöglichkeiten
nach sich ziehen können.
1. Festlegung der Modellierung und Analyse des lokalen Verhaltens der Basisblöcke
Ergebnis dieses Schrittes sind die
• Menge M und
132
6.7 Globale Analyse: Datenflussanalyse
m1 m2
f1 f2
@
@ m3
R
@
f3
m3 = f1 (m1 ) f2 (m2 )
: M × M → M ist die meet“-Funktion, die über verschiedene Pfade im Da-
”
tenflussgraphen bestimmte Informationen zusammenführt.
Beispiel: Wir stellen ein Gleichungssystem für eine Vorwärtsanalyse des Daten-
flussgraphen zum Quicksort-Programm (siehe Abbildung 27) auf. Zur Repräsen-
tation der abstrakten Werte zu Beginn der Basisblöcke führen wir die folgenden
Variablen ein:
Basisblock Anfang L1 L2 Sprung äußere Schleife L3 Ende
Variable m0 m1 m2 mS mB m3 mEXIT
Das Gleichungssystem in 6 Unbekannten hat dann die folgende Struktur:
m1 = fA (m0 ) m2 fB (mB )
m2 = fL1 (m1 ) mS
mS = fL2 (m2 )
mB = fS (mS )
m3 = fS (mS )
mEXIT = fL3 (m3 )
/
133
6. Codeoptimierung
Beispiel: ∀ ∃
Bei sichtbaren Definitionen fragt man nach der Existenz eines Pfades, auf dem ein
Zuweisungswert erhalten bleibt. Wichtige Ausdrücke sind solche, die auf jeden Fall,
d.h. bei jedem Programmlauf berechnet werden. Bei lebendigen Variablen fragt man,
ob ein Pfad existiert, auf dem eine Variable benutzt wird. /
134
7. Übersetzung objektorientierter Sprachen
7.1.1 Vererbung
Eine Klasse B erbt von einer Klasse A, falls alle Attribute und Methoden von A in B
übernommen werden. B kann darüber hinaus weitere Attribute und Methoden enthal-
ten, sowie unter gewissen Umständen ererbte Methoden überschreiben. Man sagt, B
wurde von A abgeleitet bzw. A ist Basisklasse von B.
Man unterscheidet
einfache Vererbung (single inheritance): Jede Klasse hat höchstens eine direkte
Oberklasse.
135
7. Übersetzung objektorientierter Sprachen
::GraphischesObjekt
-verschieben(double,double)
-skalieren(double)
::Linienzug ::GeschlossenesObjekt
-länge() -flaeche()
-verschieben(double,double)
-skalieren(double)
::Polygon ::Ellipse
-flaeche() -flaeche()
-verschieben(double,double)
-skalieren(double)
::Rechteck ::Dreieck
-flaeche()
können. Ebenso ist die Methode flaeche der Subklasse GeschlossenesObjekt nicht
allgemein definierbar. Erst Ellipse und Polygon ermöglichen eine Berechnung der
entsprechenden Methode. Polygon erbt dabei von zwei Oberklassen. Die Fläche eines
Rechtecks lässt sich effizienter berechnen als für ein beliebiges Polygon. Die Methode
der Oberklasse wird daher überschrieben. /
Subtypen. Falls Klasse B von Klasse A erbt, ist der Typ von B Subtyp des Typs von
A. Jedes Objekt vom Typ B ist gleichermaßen Objekt vom Typ A. Es gilt die
Als Laufzeittyp eines Objekts bezeichnet man die kleinste Klasse, zu der ein Objekt
gehört. Der Laufzeittyp ist eindeutig bestimmt. Ein Objekt gehört zu jeder Oberklasse
seines Laufzeittyps.
136
7.2 Übersetzung von Methoden
Methoden können auf Parameterpositionen Objekte mit verschiedenen Typen und da-
mit unterschiedlichem Aufbau akzeptieren. Dabei gilt die
7.1.2 Generizität
Ein weiterer Aspekt von Objektorientierung ist die Möglichkeit generischer Definitio-
nen, wie man sie etwa mit den Klassen-Templates in C++ oder in Javas generischen
Containerklassen findet. Generizität erlaubt es, Mehrfachdefinitionen von Funktionen
für unterschiedliche Typen zu vermeiden, indem Funktionsdefinitionen oder Klassen-
definitionen mit Typen parametrisiert werden können.
Beispiel: Sei etwa m Methode einer C++-Klasse C, dann wird für Methodentypen,
Aufrufe und Attributzugriffe das Objekt als zusätzlicher Parameter eingebaut:
Methode Funktionsprozedur
Typ: <returntype> m(<args>) → <returntype> fm(C &this,<args>)
Aufruf: obj.m(<args>) → fm (obj,<args>)
Attribut-
zugriff: field → this.field
137
7. Übersetzung objektorientierter Sprachen
Da die Namen von Methoden in verschiedenen Klassen gleich sein könnten, müssen bei
einer solchen Übersetzung eindeutige Namen für die Prozeduren fm generiert werden
(sog. name mangling). Dies kann unter Verwendung des Klassennamens und seiner
Länge erfolgen, außerdem ist es sinnvoll, die Typsignatur mit im Namen zu kodieren.
Eine mögliche Kodierung ist etwa wie folgt:
fm = <Methodenname>__<LaengeKlassenname><Klassenname><Typcode>
In unserem Beispiel mit geometrischen Objekten erhielte beispielsweise die Prozedur
zur Methode skalieren(double) in Klasse GraphischesObjekt den Namen
skalieren 17GraphischesObjektd.
Diese Namensgebung (exemplarisch: gcc, Version 2.x) ist nicht eindeutig: beispielsweise
erhalten die Methode m in Klasse a 1a und die Methode m 5a in Klasse a den gleichen
Namen (nämlich welchen?), falls sie die gleiche Typsignatur haben.8
Das Hauptproblem bei einfacher Vererbung (wie überhaupt) ist die effiziente Realisie-
rung der dynamischen Methodenbindung. Wird etwa ein Rechteck als Linienzug an eine
Methode übergeben, welche wiederum dessen skalieren-Methode aufruft, so muss die
spezialisierte Version für Rechtecke verwendet werden, nicht das allgemeinere Skalieren
eines Linienzugs.
Der Compiler erzeugt für jede Klasse eine Methodentabelle (in C++: virtual function
table). Diese enthält Einträge für alle Methoden einer Klasse und aller Oberklassen,
die dynamisch gebunden werden können (und in C++ als virtual deklariert sind).
Zur Laufzeit besitzt jedes Objekt (im Heap) als erste Komponente einen Zeiger auf die
Methodentabelle seiner kleinsten“ Klasse (d.h. seines Laufzeittyps). Einem Methoden-
”
namen wird der entsprechende Index in der Methodentabelle zugeordnet, in Subklassen
überschriebene Methoden erhalten den gleichen Index.
138
7.3 Übersetzung einfacher Vererbung
::GraphischesObjekt
-verschieben(double,double)
-skalieren(double)
::Linienzug
punkte : Liste<Punkt>
-laenge()
-verschieben(double,double)
-skalieren(double)
::Rechteck
seite1:double
seite2:double
-flaeche()
-laenge()
-skalieren(double)
definierten Methode verschieben. Für die Erzeugung der Methodentabellen kann zur
Übersetzungszeit Programmcode generiert werden.
Für jedes Objekt gilt, dass die Sicht als GraphischesObjekt ein Präfix der Sicht als
Linienzug oder Rechteck ist:
( Rechteckobjekt im Heap
Zeiger auf Methodentab. } Sicht als graph. Objekt
Sicht als Linienzug
punkte
seite1
seite2
Der zusätzliche Aufwand für die Implementierung einfacher Vererbung besteht in ei-
nem Zeiger (auf die Methodentabelle) pro Objekt und eine Methodentabelle pro Klasse
(Platz) sowie einem zusätzlichen Dereferenzierungsschritt (Zeit) je Aufruf einer Metho-
de.
139
7. Übersetzung objektorientierter Sprachen
:: B1 :: B2
:: C
Exemplarisch kann man sich die Klasse Polygon als Subklasse von GeschlossenesObjekt
und Linienzug vorstellen, welche beide von GraphischesObjekt erben.
Die folgenden Probleme treten bei mehrfacher Vererbung auf:
2. Das wiederholte Erben in C (Polygon) auf verschiedenen Wegen, also über B1 und
B2, von einer gemeinsamen Oberklasse A, im Beispiel GraphischesObjekt.
• die Festlegung einer festen Ordnung (etwa durch die textuell frühere Angabe) in
einer Vererbungshierarchie und somit eines Hauptvorfahren einer Klasse, dessen
Methoden Priorität haben. Die Durchsuchung der Vererbungshierarchie erfolgt
dann in einer festen Reihenfolge, beispielsweise C → B1 → B2 → A. Die erste
passende Definition gilt.
Dieser Ansatz wird etwa in OO-Erweiterungen von Lisp verwendet. Eine Gefahr
besteht darin, dass die Suchreihenfolge einem Programmierer womöglich nicht
bewusst ist, insbesondere wenn Mehrfachdeklarationen unabsichtlich eingebaut
wurden.
• Umbenennng ererbter Methoden und Attribute d.h. Konflikte sind explizit durch
den Programmierer zu lösen.
Auf diese Weise wird das Problem in Eiffel gelöst.
140
7.4 Behandlung von Mehrfachvererbung
• Angabe einer expliziten Instanz durch spezielle Sprachkonstrukte, die die Verer-
bung verdeutlichen
In C++ etwa stellt ein Aufruf Linienzug::skalieren() einen expliziten Bezug
zur entsprechenden Oberklasse her.
Die Sprachansätze bestimmen die Vorgehensweise des Compilers bei der Verwaltung
und Organisation der Symboltabelle.
ad 2: Zum doppelten Erben von einer gemeinsamen Oberklasse zweier direkter Ober-
klassen existieren zwei diametrale Ansätze: eine tatsächlich mehrfache Instanzierung
oder eine Reduktion auf eine einzige Instanz.
1. mehrfache Instanzierung
A ADD(B1) A ADD(B2) ADD(C)
A
B1
B2
C
2. einfache Instanzierung
A ADD(B1) ADD(B2) ADD(C)
A
B1
B2
C
Diese Varianten schließen einander aus, können aber beide sinnvoll eingesetzt werden.
141
7. Übersetzung objektorientierter Sprachen
... -verschieben(double,double)
-skalieren(double)
::Linienzug ::GeschlossenesObjekt
::SampleHistogram
-laenge() -flaeche()
... -verschieben(double,double)
-skalieren(double)
::TemperaturUndDruck
::Polygon
-Temperaturstatistik()
-Druckhistogramm() -flaeche()
Falls Polygon nun dieses Attribut doppelt instanziert, müssen bei jeder Operation die
beiden Attribute konsistent gehalten werden. Insbesondere müssen ererbte Methoden
wie verschieben dafür erneut überschrieben werden. Hier ist die Lösung mit einfacher
Instanzierung eindeutig die sinnvollere. /
:: B1 :: B2
:: C
Das Objekt der Subklasse C besteht dann aus Kopien der Komponenten aller Ober-
klassen und zusätzlich dem durch die Subklasse hinzugefügten Anteil. Die B1-Sicht
auf ein C-Objekt lässt sich problemlos als Präfix herstellen. Problematisch ist hinge-
gen die Bereitstellung einer B2-Sicht auf ein C-Objekt. Ein einfacher Ansatz besteht
darin, die B2-Methodentabelle zu kopieren und für die B2-Sicht einen zusätzlichen Zei-
ger auf die Kopie der B2-Methodentabelle vor den B2-Attributen einzufügen. Dies ist
142
7.5 Unabhängige Mehrfachvererbung
C-Objekt Methodentabelle
C-Ref./ zum C-Objekt
B1-Ref. B1-
B1 Methoden
B2-Ref.
B2-
B2 Methoden
C-
C Methoden
Methodentabelle
für die B2-Sicht
B2-
Methoden
in Abbildung 31 veranschaulicht. Der Compiler berechnet einen festen Offset für die
Bestimmung der B2-Sicht aus der C-Sicht.
Dieser einfache Ansatz hat folgende Probleme. Zum einen ist der Offset zur Berechnung
der passenden Sicht eines Objektes zur Compilezeit nicht immer bekannt. C-Methoden
benötigen immer eine C-Sicht von Objekten. Falls eine Methode aus B2 in C über-
schrieben wird, benötigt diese also eine C-Sicht auf das Objekt! Jede Methode in der
Methodentabelle für B2-Methoden muss somit ihrerseits wieder einen Offset enthalten,
mit dem die passende Objektsicht hergestellt werden kann. JB: Achtung, Diskrepanz
zw. Quelle und Darstellung. Wilh./Maurer befasst sich durchgehend mit doppelter
Vererbung
Ein weiterer Schwachpunkt ist, dass
der Speicherplatzbedarf bei vielen Ver- C-Ref./
erbungen exponenziell steigt, denn die B1-Ref.
B1-
Methodentabelle der B2-Sicht wird je- B1 Methoden
desmal kopiert. Dies kann vermieden
B2-Ref.
werden, indem die C-Methodentabelle C-
von vorn herein verteilt gespeichert B2 Methoden
C-Metho-
wird (siehe nebenstehende Abbildung); dentabelle
B2-Methoden werden an anderer Stelle C
gesucht als B1- und C-Methoden. Da- B2-
bei ist zu beachten, dass die Methoden- Methoden
tabellen für B1 und B2 ebenfalls wieder
verteilt gespeichert sind.
Pseudocode für einen Methodenaufruf. Wird nun für ein Objekt c eine Methode
c.m(args) aufgerufen, so gehört zu dieser Methode ein Index j in einem Teil, etwa Teil
i, der verteilten Methodentabelle. Der Zeiger auf Teil i besitzt einen bekannten Offset
143
7. Übersetzung objektorientierter Sprachen
oi in der C-Instanz. Die Methodentabelle enthält bei j einen Verweis auf die implemen-
tierende Prozedur sowie einen Offset für die von der Prozedur benötigte Sicht auf das
Objekt. Der folgende Pseudocode beschreibt die Realisierung eines Methodenaufrufs:
Der Overhead für eine solche Implementierung eines Methodenaufrufs beträgt somit
eine Addition, eine Dereferenzierung, zwei indizierte Zugriffe und eine Subtraktion.
:: A
:: B1 :: B2
:: C
In C sollen die über B1 und B2 vererbten Komponenten von A nur einmal auftreten. Bei
solcher einfachen Instanzierung bestehen die folgenden Probleme:
• Was geschieht, wenn sowohl B1, als auch B2 eine von A ererbte Methode überschrei-
ben? Die einfache Instanzierung müsste sich für die eine oder andere entscheiden.
Einzig sinnvoll ist in diesem Fall, dass die Methode auch in C überschrieben
werden muss.
144
7.6 Abhängige Mehrfachvererbung
Die Bindung von Komponentenadressen muss analog zur dynamischen Bindung von
Methoden (über die Methodentabelle) dynamisch mittels einer Indextabelle erfolgen:
die C-Referenz liefert eine Indextabelle, welche (zur Laufzeit) die Offsets aller Kompo-
nenten liefert (i.a. zusammen mit den Methoden).
Wie bei den Methoden werden hier zunächst mehrere Indextabellen für verschiedene
Sichten benötigt. Aber die Tabelle für die B2-Sicht auf C unterscheidet sich vom ent-
sprechenden Teil der C-Sicht nur durch eine konstante Differenz, nämlich der Stelle, an
der sich der Zeiger auf diese Tabelle befindet (der Zugriff wird ja generell mit Offsets,
also relativ zur Adresse der Tabelle selbst, realisiert). Hierdurch ist eine Kombination
von Methoden- und Indextabelle möglich.
Hier beträgt der Overhead eine Addition, zwei Dereferenzierungen, zwei indizierte Zu-
griffe sowie eine Subtraktion.
Übersetzung eines Attributzugriffs c.a. j sei der Index von Attribut a in der
Indextabelle.
145
7. Übersetzung objektorientierter Sprachen
Der Overhead besteht aus zwei Dereferenzierungen, einem indizierten Zugriff und einer
Subtraktion. Oft werden it und o fest sein und in Registern bereitgehalten, so dass
der Overhead drastisch reduziert werden kann.
146
Literatur
Grundlagen
Drachenbuch: Aho/Lam/Sethi/Ullman: Compiler — Prinzipien, Techniken, and Werk-
zeuge, Pearson Studium 2008.
Aho/Ullman: The Theory of Parsing, Translation, and Compiling, Vol. I: Parsing, Vol.
II: Compiling, Prentice Hall 1972.
Waite/Goos: Compiler Construction, Springer Verlag 1994.
Wilhelm/Maurer: Übersetzerbau: Theorie, Konstruktion, Generierung, Springer Verlag
1992.
Wirth: Grundlagen und Techniken des Compilerbaus, Addison-Wesley 1996.
Appel: Modern Compiler Implementation in ML/Java/C++, Addison-Wesley 1998.
Bauer/Höllerer: Übersetzung objektorientierter Programmiersprachen, Springer Verlag
1998.
Funktionale Techniken
Reade: Elements of Functional Programming, Addison-Wesley 1989.
Peña: Compiler Construction in a Functional Setting, Univ. Complutense de Madrid
1999.
Jones: Hugs, the interpreter for Haskell, http://www.haskell.org/hugs/
The Glasgow Haskell Compiler, http://www.haskell.org/ghc/
• Gill/Marlow: Happy: the parser generator for Haskell, Univ. of Glasgow, 1996,
http://www.haskell.org/happy
147