Compilerbau I

Werbung
 TECHNISCHE UNIVERSITÄT MÜNCHEN
FAKULTÄT
FÜR
INFORMATIK
Compilerbau I
Dr. Michael Petter, Dr. Axel Simon
SoSe 2012
1 / 184
Themengebiet:
Die semantische Analyse
2 / 184
Semantische Analyse
Scanner und Parser akzeptieren Programme mit korrekter Syntax.
nicht alle lexikalisch und syntaktisch korrekten Programme
machen Sinn
3 / 184
Semantische Analyse
Scanner und Parser akzeptieren Programme mit korrekter Syntax.
nicht alle lexikalisch und syntaktisch korrekten Programme
machen Sinn
der Compiler kann einige dieser sinnlosen Programme erkennen
diese Programme werden als fehlerhaft zurückgewiesen
Sprachdefinition definiert was fehlerhaft bedeutet
3 / 184
Semantische Analyse
Scanner und Parser akzeptieren Programme mit korrekter Syntax.
nicht alle lexikalisch und syntaktisch korrekten Programme
machen Sinn
der Compiler kann einige dieser sinnlosen Programme erkennen
diese Programme werden als fehlerhaft zurückgewiesen
Sprachdefinition definiert was fehlerhaft bedeutet
semantische Analysen sind hierzu erforderlich, die
Bezeichner eindeutig machen;
die Typen von Variablen ermitteln;
3 / 184
Semantische Analyse
Scanner und Parser akzeptieren Programme mit korrekter Syntax.
nicht alle lexikalisch und syntaktisch korrekten Programme
machen Sinn
der Compiler kann einige dieser sinnlosen Programme erkennen
diese Programme werden als fehlerhaft zurückgewiesen
Sprachdefinition definiert was fehlerhaft bedeutet
semantische Analysen sind hierzu erforderlich, die
Bezeichner eindeutig machen;
die Typen von Variablen ermitteln;
semantische Analysen dienen auch dazu
Möglichkeiten zur Programm-Optimierung zu finden;
über möglicherweise fehlerhafte Programme zu warnen
3 / 184
Semantische Analyse
Scanner und Parser akzeptieren Programme mit korrekter Syntax.
nicht alle lexikalisch und syntaktisch korrekten Programme
machen Sinn
der Compiler kann einige dieser sinnlosen Programme erkennen
diese Programme werden als fehlerhaft zurückgewiesen
Sprachdefinition definiert was fehlerhaft bedeutet
semantische Analysen sind hierzu erforderlich, die
Bezeichner eindeutig machen;
die Typen von Variablen ermitteln;
semantische Analysen dienen auch dazu
Möglichkeiten zur Programm-Optimierung zu finden;
über möglicherweise fehlerhafte Programme zu warnen
; semantische Analysen attributieren den Syntaxbaum
3 / 184
Die semantische Analyse
Kapitel 1:
Attributierte Grammatiken
4 / 184
Attributierte Grammatiken
viele Berechnungen der semantischen Analyse als auch der
Code-Generierung arbeiten auf dem Syntaxbaum.
was an einem (i.d.R. Nicht-Terminal) Knoten berechnet wird,
hängt nur von dem Typ des Knotens ab
eine lokale Berechnung
greift auf bereits berechnete Informationen von Nachbarknoten zu
und
berechnen daraus neue Informationen für den lokalen Knoten und
andere Nachbarknoten
5 / 184
Attributierte Grammatiken
viele Berechnungen der semantischen Analyse als auch der
Code-Generierung arbeiten auf dem Syntaxbaum.
was an einem (i.d.R. Nicht-Terminal) Knoten berechnet wird,
hängt nur von dem Typ des Knotens ab
eine lokale Berechnung
greift auf bereits berechnete Informationen von Nachbarknoten zu
und
berechnen daraus neue Informationen für den lokalen Knoten und
andere Nachbarknoten
Definition
Eine attributierte Grammatik ist eine CFG erweitert um:
Attribute für jedes Nichtterminal;
lokale Attribut-Gleichungen.
5 / 184
Attributierte Grammatiken
viele Berechnungen der semantischen Analyse als auch der
Code-Generierung arbeiten auf dem Syntaxbaum.
was an einem (i.d.R. Nicht-Terminal) Knoten berechnet wird,
hängt nur von dem Typ des Knotens ab
eine lokale Berechnung
greift auf bereits berechnete Informationen von Nachbarknoten zu
und
berechnen daraus neue Informationen für den lokalen Knoten und
andere Nachbarknoten
Definition
Eine attributierte Grammatik ist eine CFG erweitert um:
Attribute für jedes Nichtterminal;
lokale Attribut-Gleichungen.
damit die Eingabe-Werte eines Knotens bei der Berechnung
bereits vorliegen, müssen die Knoten des Syntaxbaums in einer
bestimmten Reihenfolge durchlaufen werden
5 / 184
Beispiel: Berechnung des Prädikats empty[r]
Betrachte den Syntaxbaum des regulären Ausdrucks (a|b)*a(a|b):
.
.
*
|
0
a
2
1
b
|
a
3
a
4
b
6 / 184
Beispiel: Berechnung des Prädikats empty[r]
Betrachte den Syntaxbaum des regulären Ausdrucks (a|b)*a(a|b):
.
.
*
f
|
f
0
2
f
a
1
|
a
f
b
3
f
a
4
b
6 / 184
Beispiel: Berechnung des Prädikats empty[r]
Betrachte den Syntaxbaum des regulären Ausdrucks (a|b)*a(a|b):
.
.
*
f
f
|
f
0
2
f
a
1
f
|
a
f
b
3
f
a
4
b
6 / 184
Beispiel: Berechnung des Prädikats empty[r]
Betrachte den Syntaxbaum des regulären Ausdrucks (a|b)*a(a|b):
.
t
f
*
.
f
f
|
f
0
2
f
a
1
f
|
a
f
b
3
f
a
4
b
6 / 184
Beispiel: Berechnung des Prädikats empty[r]
Betrachte den Syntaxbaum des regulären Ausdrucks (a|b)*a(a|b):
f
.
t
f
*
.
f
f
|
f
0
2
f
a
1
f
|
a
f
b
3
f
a
4
b
; Werte von empty[r] werden von unten nach oben berechnet.
6 / 184
Idee zur Implementierung
für jeden Knoten führen wir ein Attribut empty ein.
die Attribute werden in einer depth-first Traversierung berechnet:
an einem Blatt lässt sich der Wert des Attributs unmittelbar
ermitteln
das Attribut an einem inneren Knoten hängt nur von den Attributen
der Nachfolger ab
das empty ist ein synthetisches Attribut
kann durch pre- oder post-order Traversierung berechnet werden
7 / 184
Idee zur Implementierung
für jeden Knoten führen wir ein Attribut empty ein.
die Attribute werden in einer depth-first Traversierung berechnet:
an einem Blatt lässt sich der Wert des Attributs unmittelbar
ermitteln
das Attribut an einem inneren Knoten hängt nur von den Attributen
der Nachfolger ab
das empty ist ein synthetisches Attribut
kann durch pre- oder post-order Traversierung berechnet werden
Im Allgemeinen:
Definition
Ein Attribut ist
synthetisch: Wert wird von den Blättern zur Wurzel propagiert
inherit: Wert wird von der Wurzel zu den Blättern propagiert
7 / 184
Berechnungsregeln für empty
Wie das Attribut lokal zu berechnen ist, ergibt sich aus dem Typ des
Knotens:
für Blätter: r
andernfalls:
i
x
definieren wir
empty[r1 j r2 ]
empty[r1 r2 ]
empty[r1 ]
empty[r1 ?]
=
=
=
=
empty[r]
= (x ).
empty[r1 ] _ empty[r2 ]
empty[r1 ] ^ empty[r2 ]
t
t
8 / 184
Spezifizierung von allgemeinen Attributsystemen
Das empty Attributs ist rein synthetisch, daher kann es durch
einfache strukturelle Induktion definiert werden.
wir benötigen einen einfachen und flexiblen Mechanismus, mit
dem wir über allgemeine Attribute an einem Knoten und seinen
Nachfolgern reden können.
der Einfachheit halber benutzen wir fortlaufende Indizes:
empty[0] :
empty[i] :
das Attribut des aktuellen Knotens
das Attribut des i-ten Sohns (i > 0)
9 / 184
Spezifizierung von allgemeinen Attributsystemen
Das empty Attributs ist rein synthetisch, daher kann es durch
einfache strukturelle Induktion definiert werden.
wir benötigen einen einfachen und flexiblen Mechanismus, mit
dem wir über allgemeine Attribute an einem Knoten und seinen
Nachfolgern reden können.
der Einfachheit halber benutzen wir fortlaufende Indizes:
empty[0] :
empty[i] :
das Attribut des aktuellen Knotens
das Attribut des i-ten Sohns (i > 0)
... im Beispiel:
x
:
?
:
j :
:
:
empty[0]
empty[0]
empty[0]
empty[0]
empty[0]
:=
:=
:=
:=
:=
(x )
empty[1] _ empty[2]
empty[1] ^ empty[2]
t
t
9 / 184
Beobachtungen:
Die lokalen Berechnungen der Attributwerte müssen von einem
globalen Algorithmus zusammen gesetzt werden
Dazu benötigen wir:
1
2
eine Besuchsreihenfolge der Knoten des Baums;
eine lokale Berechnungsreihenfolgen
Die Auswertungsstrategie muss mit den Attribut-Abhängigkeiten
kompatibel sein
10 / 184
Beobachtungen:
Die lokalen Berechnungen der Attributwerte müssen von einem
globalen Algorithmus zusammen gesetzt werden
Dazu benötigen wir:
1
2
eine Besuchsreihenfolge der Knoten des Baums;
eine lokale Berechnungsreihenfolgen
Die Auswertungsstrategie muss mit den Attribut-Abhängigkeiten
kompatibel sein
Wir geben Abhängigkeiten als gerichtete Kanten an:
empty
empty
|
empty
; Pfeil zeigt in die Richtung des Informationsflusses
10 / 184
Beobachtung:
Zur Ermittlung einer Auswertungsstrategie reicht es nicht, sich
die lokalen Attribut-Abhängigkeiten anzusehen.
Es kommt auch darauf an, wie sie sich global zu einem
Abhängigkeitsgraphen zusammen setzen.
Im Beispiel sind die Abhängigkeiten stets von den Attributen der
Söhne zu den Attributen des Vaters gerichtet.
post-order depth-first Traversierung möglich
;
Die Variablen-Abhängigkeiten können aber auch komplizierter
sein.
11 / 184
Simultane Berechnung von mehreren Attributen
Berechne empty, first, next von regulären Ausdrücken:
x
:
empty[0]
first[0]
:
root:
f
e
empty[0]
first[0]
next[0]
next[1]
x
:= (x )
:= fx j x 6= g
==
(keine Gleichung für next )
:= empty[1]
:= first[1]
:= ;
:= next[0]
f
e
f
e
root
n
n
n
12 / 184
RE Auswertung: Regeln für Alternative
j
:
empty[0]
first[0]
next[1]
next[2]
f
f
e
:=
:=
:=
:=
|
e
n
f
n
e
n
13 / 184
RE Auswertung: Regeln für Alternative
j
:
empty[0]
first[0]
next[1]
next[2]
f
f
e
empty[1] _ empty[2]
first[1] [ first[2]
next[0]
next[0]
:=
:=
:=
:=
|
e
n
f
n
e
n
13 / 184
RE Auswertung: Regeln für Konkatenation
:
empty[0]
first[0]
next[1]
next[2]
:=
:=
:=
:=
f
f
e
e
n
.
n
f
e
n
14 / 184
RE Auswertung: Regeln für Konkatenation
:
empty[0]
first[0]
next[1]
next[2]
f
f
e
empty[1] ^ empty[2]
first[1] [ (empty[1] ? first[2] : ;)
first[2] [ (empty[2] ? next[0]: ;)
next[0]
:=
:=
:=
:=
e
n
.
n
f
e
n
14 / 184
RE Auswertung: Regeln für Kleene-Stern und ‘?’
f
e
f
e
:
empty[0]
first[0]
next[1]
:=
:=
:=
?
:
empty[0]
first[0]
next[1]
:=
:=
:=
*
n
f
e
n
f
e
?
n
n
15 / 184
RE Auswertung: Regeln für Kleene-Stern und ‘?’
f
e
f
e
:
empty[0]
first[0]
next[1]
:= t
:= first[1]
:= first[1] [ next[0]
?
:
empty[0]
first[0]
next[1]
:= t
:= first[1]
:= next[0]
*
n
f
e
n
f
e
?
n
n
15 / 184
Problem bei allgemeinen Attributen
Eine Auswertungsstrategie kann es nur dann geben, wenn die
Variablen-Abhängigkeiten in jedem attributierten Baum azyklisch
sind
Es ist DEXPTIME-vollständig, herauszufinden, ob keine
zyklischen Variablenabhängigkeiten vorkommen können
[Jazayeri, Odgen, Rounds, 1975]
Beispiel: Grammatik S ! a j b; Attributenabhängigkeiten:
h
h
h
i
a
j
i
k
S
j
j
k
h
i
b
j
k
16 / 184
Berechnung der Abhängigkeiten
Grammatik: S ! a j b.
h
h
h
i
a
j
i
k
S
j
j
k
h
i
b
j
k
17 / 184
Berechnung der Abhängigkeiten
Grammatik: S ! a j b.
h
h
h
a
i
j
j
S
i
j
k
k
h
b
i
j
k
Betrachte Abhängigkeiten in jedem möglichen Baum:
h
h
S
j
i
a
j
k
h
h
S
j
i
b
j
k
17 / 184
Berechnung der Abhängigkeiten
Grammatik: S ! a j b.
h
h
h
a
i
j
j
S
i
j
k
k
h
b
i
j
k
Betrachte Abhängigkeiten in jedem möglichen Baum:
h
h
S
j
i
a
j
k
h
Keiner der Graphen enthält einen Zyklus
kann berechnet werden.
h
S
j
i
b
j
k
; jeder Ableitungsbaum
17 / 184
Stark azyklische Attributierung
Ziel: Berechne hinreichende Bedingung, dass Attributierung
azyklisch ist.
Idee: Berechne einen Abhängigkeitsgraphen für jedes Symbol N.
Wir starten mit der lokalen Ordung vN = !
Betrachte jede Produktion N ! S1 Sn von N
Für jeden möglichen Sohn-Knoten Si an i-ter Stelle
finde Abhängigkeiten zwischen a[0] : : : z[0] von Si
füge diese in vN ein zwischen a[i] : : : z[i]
Hat vN einen Zykel, brechen wir erfolglos ab.
Lässt sich vN für kein N mehr vergrößern, hören wir auf.
Im Beispiel:
h
h
i
S
j
j
k
18 / 184
Stark azyklische Attributierung
Ziel: Berechne hinreichende Bedingung, dass Attributierung
azyklisch ist.
Idee: Berechne einen Abhängigkeitsgraphen für jedes Symbol N.
Wir starten mit der lokalen Ordung vN = !
Betrachte jede Produktion N ! S1 Sn von N
Für jeden möglichen Sohn-Knoten Si an i-ter Stelle
finde Abhängigkeiten zwischen a[0] : : : z[0] von Si
füge diese in vN ein zwischen a[i] : : : z[i]
Hat vN einen Zykel, brechen wir erfolglos ab.
Lässt sich vN für kein N mehr vergrößern, hören wir auf.
Im Beispiel:
h
h
i
S
j
j
k
18 / 184
Von den Abhängigkeiten zur Auswertungsstrategie
Mögliche Strategien:
19 / 184
Von den Abhängigkeiten zur Auswertungsstrategie
Mögliche Strategien:
1
Der Benutzer soll die Strategie spezifizieren
19 / 184
Von den Abhängigkeiten zur Auswertungsstrategie
Mögliche Strategien:
1
Der Benutzer soll die Strategie spezifizieren
2
Berechne eine Strategie anhand der Abhängigkeiten
Berechne eine linear Ordnung aus der partielle Ordnung ! bzw. v.
Werte die Attribute anhand dieser linearen Ordnung aus.
Die lokalen Abhängigkeitsgraphen zusammen mit der linearen
Ordnung erlauben die Berechnung einer Strategie.
n
f
e
19 / 184
Von den Abhängigkeiten zur Auswertungsstrategie
Mögliche Strategien:
1
Der Benutzer soll die Strategie spezifizieren
2
Berechne eine Strategie anhand der Abhängigkeiten
Berechne eine linear Ordnung aus der partielle Ordnung ! bzw. v.
Werte die Attribute anhand dieser linearen Ordnung aus.
Die lokalen Abhängigkeitsgraphen zusammen mit der linearen
Ordnung erlauben die Berechnung einer Strategie.
n
f
e
3
Betrachte fixe Strategie und erlaube nur entsprechende Attribute
Frage: Wie berechnet man eine sinnvolle Linearisierung?
19 / 184
Linearisierung der Abhängigkeitsordnung
Mögliche automatische Strategien:
20 / 184
Linearisierung der Abhängigkeitsordnung
Mögliche automatische Strategien:
1
Bedarfsgetriebene Auswertung
Beginne mit der Berechnung eines Attributs.
Sind die Argument-Attribute noch nicht berechnet, berechne
rekursiv deren Werte
Besuche die Knoten des Baum nach Bedarf
20 / 184
Linearisierung der Abhängigkeitsordnung
Mögliche automatische Strategien:
1
Bedarfsgetriebene Auswertung
Beginne mit der Berechnung eines Attributs.
Sind die Argument-Attribute noch nicht berechnet, berechne
rekursiv deren Werte
Besuche die Knoten des Baum nach Bedarf
2
Auswertung in Pässen:
Minimiere die Anzahl der Besuche an jedem Knoten.
Organisiere die Auswertung in Durchläufen durch den Baum.
Berechne für jeden Durchlauf eine Besuchsstrategie für die Knoten
zusammen mit einer lokalen Strategie für jeden Knoten-Typ
Betrachte Beispiel für bedarfsgetriebene Auswertung
20 / 184
Beispiel bedarfsgetriebene Auswertung
Berechne next der Blätter von ((ajb) a(ajb)):
j
:
next[1]
next[2]
:= next[0]
:= next[0]
:
next[1]
next[2]
:= first[2] [ (empty[2] ? next[0]: ;)
:= next[0]
.
.
*
|
a
0
a
b
1
|
2
a
3
b
4
21 / 184
Beispiel bedarfsgetriebene Auswertung
Berechne next der Blätter von ((ajb) a(ajb)):
j
:
next[1]
next[2]
:= next[0]
:= next[0]
:
next[1]
next[2]
:= first[2] [ (empty[2] ? next[0]: ;)
:= next[0]
.
n
.
*
|
a
0
a
b
1
n
|
2
a
n
3
n
b
n
4
21 / 184
Beispiel bedarfsgetriebene Auswertung
Berechne next der Blätter von ((ajb) a(ajb)):
j
:
next[1]
next[2]
:= next[0]
:= next[0]
:
next[1]
next[2]
:= first[2] [ (empty[2] ? next[0]: ;)
:= next[0]
n
.
|
a
0
n
.
*
a
b
n
e f
1
e f
2
a
n
3
|
n
e f
b
n
4
21 / 184
Beispiel bedarfsgetriebene Auswertung
Berechne next der Blätter von ((ajb) a(ajb)):
j
:
next[1]
next[2]
:= next[0]
:= next[0]
:
next[1]
next[2]
:= first[2] [ (empty[2] ? next[0]: ;)
:= next[0]
n
.
|
a
0
n
.
*
a
b
n
e f
1
e f
2
a
n
3
|
n
e f
b
n
4
21 / 184
Bedarfsgetriebene Auswertung
Diskussion:
Die Reihenfolge hängt i.a. vom zu attributierenden Baum ab.
Der Algorithmus muss sich merken, welche Attribute er bereits
berechnete
Der Algorithmus besucht manche Knoten unnötig oft.
Der Algorithmus ist nicht-lokal
22 / 184
Bedarfsgetriebene Auswertung
Diskussion:
Die Reihenfolge hängt i.a. vom zu attributierenden Baum ab.
Der Algorithmus muss sich merken, welche Attribute er bereits
berechnete
Der Algorithmus besucht manche Knoten unnötig oft.
Der Algorithmus ist nicht-lokal
Ansatz nur im Prinzip günstig:
Berechnungsstrategie ist dynamisch: Fehlersuche schwierig
Berechnung aller Attribute ist oft billiger
Meistens werden alle Attribute in allen Knoten benötigt
22 / 184
Diskussion
Dann gilt:
in jedem Durchlauf wird mindestens ein Attribut in einer stark
azyklischen Attributierung berechnet
man braucht folglich für stark azyklische Attributierungen
maximal so viele Pässe, wie es Attribute gibt
hat man einen Baum-Durchlauf zur Berechnung eines Attributes,
kann man überprüfen, ob er geeignet ist, gleichzeitig weitere
Attribute auszuwerten
Optimierungsproblem
;
23 / 184
Diskussion
Dann gilt:
in jedem Durchlauf wird mindestens ein Attribut in einer stark
azyklischen Attributierung berechnet
man braucht folglich für stark azyklische Attributierungen
maximal so viele Pässe, wie es Attribute gibt
hat man einen Baum-Durchlauf zur Berechnung eines Attributes,
kann man überprüfen, ob er geeignet ist, gleichzeitig weitere
Attribute auszuwerten
Optimierungsproblem
;
... im Beispiel:
empty und first lassen sich gemeinsam berechnen.
next muss in einem weiteren Pass berechnet werden
; lasse den Benutzer festlegen, wie Attribute ausgewertet werden
23 / 184
Implementierung der lokalen Auswertung:
Pre-Order vs. Post-Order
Betrachte Beispiel: Nummerierung der Blätter eines Baums:
.
.
*
|
a
0
a
b
1
|
2
a
3
b
4
24 / 184
Praktische Implementierung der Nummerierung
Idee:
Führe Hilfsattribute pre und post ein
mit pre reichen wir einen Zählerstand nach unten (inherites
Attribut)
mit post reichen wir einen Zählerstand wieder nach oben
(synthetisches Attribut)
Root:
pre[0]
pre[1]
post[0]
:= 0
:= pre[0]
:= post[1]
Node:
pre[1]
pre[2]
post[0]
:= pre[0]
:= post[1]
:= post[2]
Leaf:
post[0]
:= pre[0] + 1
25 / 184
Die lokalen Attribut-Abhängigkeiten:
pre
pre
post
post
pre
pre
post
post
die Attributierung ist offenbar stark azyklisch
26 / 184
Die lokalen Attribut-Abhängigkeiten:
pre
pre
post
post
pre
pre
post
post
die Attributierung ist offenbar stark azyklisch
ein innerer Knoten berechnet
inherite Attribute bevor in einen Kindknoten abgestiegen wird
(pre-order traversal)
synthetische Attribute nach der Rückkehr von einem Kindknoten
(post-order traversal)
26 / 184
Die lokalen Attribut-Abhängigkeiten:
pre
pre
post
post
pre
pre
post
post
die Attributierung ist offenbar stark azyklisch
ein innerer Knoten berechnet
inherite Attribute bevor in einen Kindknoten abgestiegen wird
(pre-order traversal)
synthetische Attribute nach der Rückkehr von einem Kindknoten
(post-order traversal)
man kann alle Attribute in einer depth-first Traversierung von links
nach rechts auswerten (mit pre- und post-order Berechnung)
So etwas nennen wir L-Attributierung.
26 / 184
L-Attributierung
Definition
Ein Knoten mit Attributen A ist L-attributiert wenn jedes inherite
Attribut ai [n] nur von aj [m] abhängt wobei m < n und ai ; aj 2 A.
27 / 184
L-Attributierung
Definition
Ein Knoten mit Attributen A ist L-attributiert wenn jedes inherite
Attribut ai [n] nur von aj [m] abhängt wobei m < n und ai ; aj 2 A.
Ursprung:
eine L-attributierte Grammatik kann während des Parsens
ausgewertet werden
27 / 184
L-Attributierung
Definition
Ein Knoten mit Attributen A ist L-attributiert wenn jedes inherite
Attribut ai [n] nur von aj [m] abhängt wobei m < n und ai ; aj 2 A.
Ursprung:
eine L-attributierte Grammatik kann während des Parsens
ausgewertet werden
Idee:
partitioniere alle Attribute A = A1 [ : : : [ An so dass für alle
Attribute in Ai jeder Knoten L-attributiert ist
für jede Attributmenge Ai führe eine depth-first Traversierung
durch
; Bestimme Attribute mit Hinblick auf wenige Traversierungen
27 / 184
Praktische Benutzung
Symboltabellen, Typ-Überprüfung / Inferenz und (einfache)
Codegenerierung können durch Attributierung berechnet werden
In diesen Anwendungen werden stets Syntaxbäume annotiert.
Die Knotenbeschriftungen entsprechen den Regeln einer
kontextfreien Grammatik
Knoten können in Typen eingeteilt werden — entsprechend den
Nichtterminalen auf der linken Seite
Unterschiedliche Nichtterminale benötigen evt. unterschiedliche
Mengen von Attributen.
28 / 184
Praktische Implementierung
In objekt-orientierten Sprachen benutze visitor pattern:
Klasse mit Methode für jedes Nicht-Terminal der Grammatik
public abstract class Regex {
public abstract void accept(Visitor v);
}
Durch Überschreiben der folgenden Methoden wird eine
Attribut-spezifische Auswertung implementiert
public interface Visitor {
public void visit(Dot re) { re.children(this); }
public void visit(Bar re) { re.children(this); }
...
public void visit(Token tok) {}
}
Vordefiniert ist die Traversierung des Ableitungsbaumes
public class OrEx extends Regex {
RegEx l,r;
public void accept(Visitor v) { v.visit(this); }
public void children(Visitor v) {
l.accept(v); r.accept(v);
}}
29 / 184
Die semantische Analyse
Kapitel 2:
Symboltabellen
30 / 184
Symboltabellen
Betrachte folgenden Java Kode:
void foo() {
int A;
void bar() {
double A;
A = 0.5;
write(A);
}
A = 2;
bar();
write(A);
}
innerhalb des Rumpfs von bar
wird die Definition von A durch die
lokale Definition verdeckt
für die Code-Erzeugung
benötigen wir für jede Benutzung
eines Bezeichners die zugehörige
Definitionsstelle
statische Bindung bedeutet, dass
die Definition eines Namens A an
allen Programmpunkten innerhalb
des gesamten Blocks gültig ist.
sichtbar ist sie aber nur in
denjenigen Teilbereichen, in
denen keine weitere Definition
von A gültig ist
31 / 184
Gültigkeitsbereiche von Bezeichnern
void foo() {
int A;
void bar() {
double A;
A = 0.5;
write(A);
}
A = 2;
bar();
write(A);
9
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
=
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
;
Gültigkeitsbereich von int A
}
32 / 184
Gültigkeitsbereiche von Bezeichnern
void foo() {
int A;
void bar() {
double A;
A = 0.5;
write(A);
9
>
=
>
;
Gültigkeitsbereich von double A
}
A = 2;
bar();
write(A);
}
32 / 184
Gültigkeitsbereiche von Bezeichnern
void foo() {
int A;
void bar() {
double A;
A = 0.5;
write(A);
9
>
=
>
;
Gültigkeitsbereich von double A
}
A = 2;
bar();
write(A);
}
Verwaltungs von Bezeichnern kann kompliziert werden...
32 / 184
Sichtbarkeitsregeln in objektorientierten Prog.spr.
1
2
3
4
5
6
7
8
9
10
public class Foo {
protected int x = 17;
protected int y = 5;
private int z = 42;
public int b() { return 1; }
}
class Bar extends Foo {
protected double y = 0.5;
public int b(int a) { return a; }
}
Beobachtung:
33 / 184
Sichtbarkeitsregeln in objektorientierten Prog.spr.
1
2
3
4
5
6
7
8
9
10
public class Foo {
protected int x = 17;
protected int y = 5;
private int z = 42;
public int b() { return 1; }
}
class Bar extends Foo {
protected double y = 0.5;
public int b(int a) { return a; }
}
Beobachtung:
private Members sind nur innerhalb der aktuellen Klasse
sichtbar
protected Members sind innerhalb der Klasse, in den
Unterklassen sowie innerhalb des gesamten package sichtbar
Methoden b gleichen Namens sind stets verschieden, wenn ihre
Argument-Typen verschieden sind
overloading
;
33 / 184
Dynamische Auflösung von Funktionen
public class Foo {
protcted int foo() { return 1; }
}
class Bar extends Foo {
protected int foo() { return 2; }
public int test(boolean b) {
Foo x = (b) ? new Foo() : new Bar();
return x.foo();
}
}
Beobachtungen:
34 / 184
Dynamische Auflösung von Funktionen
public class Foo {
protcted int foo() { return 1; }
}
class Bar extends Foo {
protected int foo() { return 2; }
public int test(boolean b) {
Foo x = (b) ? new Foo() : new Bar();
return x.foo();
}
}
Beobachtungen:
der Typ von x ist Foo oder Bar, je nach Wert von b
x.foo() ruft entweder foo in Zeile 2 oder Zeile 5 auf
34 / 184
Dynamische Auflösung von Funktionen
public class Foo {
protcted int foo() { return 1; }
}
class Bar extends Foo {
protected int foo() { return 2; }
public int test(boolean b) {
Foo x = (b) ? new Foo() : new Bar();
return x.foo();
}
}
Beobachtungen:
der Typ von x ist Foo oder Bar, je nach Wert von b
x.foo() ruft entweder foo in Zeile 2 oder Zeile 5 auf
Entscheidung wird zur Laufzeit gefällt (hat nichts mit
Namensauflösung zu tun)
34 / 184
Überprüfung von Bezeichnern
Aufgabe: Finde zu jeder Benutzung eines Bezeichners
die zugehörige Deklaration
Idee:
1
ersetze Bezeichner durch eindeutige Nummern
2
finde zu jedem Benutzung eines Bezeichners die Deklaration
35 / 184
Überprüfung von Bezeichnern
Aufgabe: Finde zu jeder Benutzung eines Bezeichners
die zugehörige Deklaration
Idee:
1
ersetze Bezeichner durch eindeutige Nummern
das Vergleichen von Nummern ist schneller
das Ersetzen von gleichen Namen durch eine Nummer spart
Speicher
2
finde zu jedem Benutzung eines Bezeichners die Deklaration
35 / 184
Überprüfung von Bezeichnern
Aufgabe: Finde zu jeder Benutzung eines Bezeichners
die zugehörige Deklaration
Idee:
1
ersetze Bezeichner durch eindeutige Nummern
das Vergleichen von Nummern ist schneller
das Ersetzen von gleichen Namen durch eine Nummer spart
Speicher
2
finde zu jedem Benutzung eines Bezeichners die Deklaration
für jede Deklaration eines Bezeichners muss zur Laufzeit des
Programs Speicher reserviert werden
bei Programmiersprachen ohne explizite Deklaration wird implizit
eine Deklaration bei der ersten Benutzung eines Bezeichners
erzeugt
35 / 184
(1) Ersetze Bezeichner durch eindeutige Namen
Idee für Algorithmus:
Eingabe: Folge von Strings
Ausgabe: 1 Folge von Nummern
2
Tabelle, die zu Nummern die Strings auflistet
Wende diesen Algorithmus im Scanner auf jeden Bezeichner an.
36 / 184
Beispiel für die Anwendung des Algorithmus
Eingabe:
das
das
Ausgabe:
0
schwein
schwein
1
ist
dem
dem
1
schwein
menschen
2
3
4
0
1
2
3
4
5
6
das
schwein
ist
dem
was
menschen
wurst
0
1
3
ist
5
was
wurst
2
6
und
37 / 184
Spezifikation der Implementierung
Idee:
benutze eine partielle Abbildung: S : String!int
verwalte einen Zähler int count = 0; für die Anzahl der bereits
gefundenen Wörter
Damit definieren wir eine Funktion int getIndex(String w):
int getIndex(String w) f
if (S (w) undefined) f
S = S fw 7! countg;
return count++;
else return S (w);
g
38 / 184
Datenstrukturen für partielle Abbildungen
Ideen:
Liste von Paaren
O(1)
O(n)
(w; i)
2 String int :
; zu teuer
39 / 184
Datenstrukturen für partielle Abbildungen
Ideen:
Liste von Paaren
O(1)
O(n)
(w; i)
balancierte Bäume :
O(log(n))
O(log(n))
2 String int :
; zu teuer
; zu teuer
39 / 184
Datenstrukturen für partielle Abbildungen
Ideen:
Liste von Paaren
O(1)
O(n)
(w; i)
balancierte Bäume :
O(log(n))
O(log(n))
Hash Tables :
O(1)
O(1)
2 String int :
; zu teuer
; zu teuer
zumindest im Mittel
39 / 184
Datenstrukturen für partielle Abbildungen
Ideen:
Liste von Paaren
O(1)
O(n)
(w; i)
balancierte Bäume :
O(log(n))
O(log(n))
Hash Tables :
O(1)
O(1)
2 String int :
; zu teuer
; zu teuer
zumindest im Mittel
Caveat: In alten Compilern war der Scanner das langsamste Glied in
der Kette. Heute der Zeitverbrauch des Scanners eher gering.
39 / 184
Implementierung mit Hash-Tabellen
lege ein Feld M von hinreichender Größe m an
wähle eine Hash-Funktion H : String ! [0; m 1] mit den
Eigenschaften:
H (w) ist leicht zu berechnen
H streut die vorkommenden Wörter gleichmäßig über [0; m
Mögliche Wahlen (~x = hx0 ; : : : xr
H0 (~x) =
H1 (~x) =
H2 (~x) =
1
1]
i):
(xP0 + xr 1 ) % m
( ri=01 xi pi ) % m
(x0 + p (x1 + p (: : : + p xr
für eine Primzahl p (z.B. 31)
1
))) % m
Das Argument-Wert-Paar (w; i) legen wir dann in M [H (w)] ab
40 / 184
Berechnung einer Hash-Tabelle am Beispiel
Mit
m=7
und
H0
erhalten wir:
0
1
2
3
4
5
6
schwein 1
menschen 5
was 4
ist
2
wurst 6
das 0
dem 3
Um den Wert des Worts w zu finden, müssen wir w mit allen Worten x
vergleichen, für die H (w) = H (x)
41 / 184
Überprüfung von Bezeichnern: (2) Symboltabellen
Überprüfe die korrekte Benutzung von Bezeichnern:
Durchmustere den Syntaxbaum in einer geeigneten Reihenfolge,
die
jede Definition vor ihren Benutzungen besucht
die jeweils aktuell sichtbare Definition zuletzt besucht
für jeden Bezeichner verwaltet man einen Keller der gültigen
Definitionen
trifft man bei der Durchmusterung auf eine Deklaration eines
Bezeichners, schiebt man sie auf den Keller
verlässt man den Gültigkeitsbereich, muss man sie wieder vom
Keller werfen
trifft man bei der Durchmusterung auf eine Benutzung, schlägt
man die letzte Deklaration auf dem Keller nach
findet man keine Deklaration, haben wir einen Fehler gefunden
42 / 184
Beispiel: Keller von Symboltabellen
{
int a, b;
b = 5;
if (b>3) {
int a, c;
a = 3;
c = a + 1;
b = c;
} else {
int c;
c = a + 1;
b = c;
}
b = a + b;
Abbildung von Namen auf Zahlen:
0
1
2
a
b
c
}
43 / 184
Der zugehörige Syntaxbaum
d Deklaration
b Basis-Block
a Zuweisung
b
b
d
b
d
b
0 int
a
b
1 int
if
=
>
1
b
5
b
b
b
1
3
d
b
d
a
2 int
a
d
0 int
=
2 int
=
1
1
2
2
44 / 184
Der zugehörige Syntaxbaum
d Deklaration
b Basis-Block
a Zuweisung
b
b
d
b
d
b
0 int
a
b
1 int
if
=
>
1
b
5
b
b
b
1
3
d
b
d
a
2 int
a
d
0 int
=
2 int
=
1
1
2
2
44 / 184
Praktische Implementierung: Sichtbarkeit
Das Auflösen von Bezeichnern kann durch L-attributierte
Grammatik erfolgen
45 / 184
Praktische Implementierung: Sichtbarkeit
Das Auflösen von Bezeichnern kann durch L-attributierte
Grammatik erfolgen
Benutzt man eine Listen-Implementierung der Symboltabelle und
verwaltet man Marker für Blöcke, kann man auf das Entfernen
von Deklarationen am Ende eines Blockes verzichten
a
b
vor if-Anweisung
45 / 184
Praktische Implementierung: Sichtbarkeit
Das Auflösen von Bezeichnern kann durch L-attributierte
Grammatik erfolgen
Benutzt man eine Listen-Implementierung der Symboltabelle und
verwaltet man Marker für Blöcke, kann man auf das Entfernen
von Deklarationen am Ende eines Blockes verzichten
a
b
a
c
a
b
vor if-Anweisung
then-Zweig
45 / 184
Praktische Implementierung: Sichtbarkeit
Das Auflösen von Bezeichnern kann durch L-attributierte
Grammatik erfolgen
Benutzt man eine Listen-Implementierung der Symboltabelle und
verwaltet man Marker für Blöcke, kann man auf das Entfernen
von Deklarationen am Ende eines Blockes verzichten
a
b
a
c
a
b
c
a
b
vor if-Anweisung
then-Zweig
else-Zweig
45 / 184
Praktische Implementierung: Sichtbarkeit
Das Auflösen von Bezeichnern kann durch L-attributierte
Grammatik erfolgen
Benutzt man eine Listen-Implementierung der Symboltabelle und
verwaltet man Marker für Blöcke, kann man auf das Entfernen
von Deklarationen am Ende eines Blockes verzichten
a
b
a
c
a
b
c
a
b
vor if-Anweisung
then-Zweig
else-Zweig
Anstelle erst die Namen durch Nummern zu ersetzen und dann
die Zuordnung von Benutzungen zu Definitionen vorzunehmen,
kann man auch gleich eindeutige Nummern vergeben
Anstatt ein eindeutige Nummern zu vergeben wird in der Praxis
auch gerne ein Zeiger zum Deklarationsknoten gespeichert
45 / 184
Mehrfach- und Vorwärtsdeklarationen
Manche Programmiersprachen verbieten eine Mehrfachdeklaration
des selben Namens innerhalb eines Funktionsrumpfes.
46 / 184
Mehrfach- und Vorwärtsdeklarationen
Manche Programmiersprachen verbieten eine Mehrfachdeklaration
des selben Namens innerhalb eines Funktionsrumpfes.
weise jedem Block eine eindeutige Nummer zu
speichere für jede Deklaration auch die Nummer des Blocks, zu
der sie gehört
46 / 184
Mehrfach- und Vorwärtsdeklarationen
Manche Programmiersprachen verbieten eine Mehrfachdeklaration
des selben Namens innerhalb eines Funktionsrumpfes.
weise jedem Block eine eindeutige Nummer zu
speichere für jede Deklaration auch die Nummer des Blocks, zu
der sie gehört
gibt es eine weitere Deklaration des gleichen Namens mit
derselben Blocknummer, muss ein Fehler gemeldet werden
46 / 184
Mehrfach- und Vorwärtsdeklarationen
Manche Programmiersprachen verbieten eine Mehrfachdeklaration
des selben Namens innerhalb eines Funktionsrumpfes.
weise jedem Block eine eindeutige Nummer zu
speichere für jede Deklaration auch die Nummer des Blocks, zu
der sie gehört
gibt es eine weitere Deklaration des gleichen Namens mit
derselben Blocknummer, muss ein Fehler gemeldet werden
Wie zuvor können eindeutige Block Nummern auch durch Zeiger auf
die Blöcke implementiert werden.
46 / 184
Mehrfach- und Vorwärtsdeklarationen
Manche Programmiersprachen verbieten eine Mehrfachdeklaration
des selben Namens innerhalb eines Funktionsrumpfes.
weise jedem Block eine eindeutige Nummer zu
speichere für jede Deklaration auch die Nummer des Blocks, zu
der sie gehört
gibt es eine weitere Deklaration des gleichen Namens mit
derselben Blocknummer, muss ein Fehler gemeldet werden
Wie zuvor können eindeutige Block Nummern auch durch Zeiger auf
die Blöcke implementiert werden.
Um rekursive Datenstrukturen zu ermöglichen erlauben Sprachen
Vorwärtsdeklarationen:
struct list1;
struct list0 {
int info;
struct list1* next;
}
struct list1 {
int info;
struct list0* next;
}
46 / 184
Deklaration von Funktionsnamen
Auf gleiche Weise müssen auch Funktionsnamen verwaltet werden.
bei einer rekursiven Funktion muss der Name vor der
Durchmusterung des Rumpfes in die Tabelle eingetragen werden
int fac(int i) {
return i*fac(i-1);
}
47 / 184
Deklaration von Funktionsnamen
Auf gleiche Weise müssen auch Funktionsnamen verwaltet werden.
bei einer rekursiven Funktion muss der Name vor der
Durchmusterung des Rumpfes in die Tabelle eingetragen werden
int fac(int i) {
return i*fac(i-1);
}
bei wechselseitig rekursiven Funktionsdefinitionen gilt dies für
alle Funktionsnamen. Beispiel in ML und C:
fun
|
|
and
|
|
odd 0 = false
odd 1 = true
odd x = even (x-1)
even 0 = true
even 1 = false
even x = odd (x-1)
int even(int x);
int odd(int x) {
return (x==0 ? 0 :
(x==1 ? 1 : even(x-1)));
}
int even(int x) {
return (x==0 ? 1 :
(x==1 ? 0 : odd(x-1)));
}
47 / 184
Überladung von Namen
Ähnliche Abhängigkeiten gelten für objekt-orientierte Sprachen:
bei objekt-orientierter Sprache mit Vererbung zwischen Klassen
sollte die übergeordnete Klasse vor der Unterklasse besucht
werden
48 / 184
Überladung von Namen
Ähnliche Abhängigkeiten gelten für objekt-orientierte Sprachen:
bei objekt-orientierter Sprache mit Vererbung zwischen Klassen
sollte die übergeordnete Klasse vor der Unterklasse besucht
werden
bei Überladung muss simultan die Signatur verglichen werden
eventuell muss eine Typüberprüfung vorgenommen werden
48 / 184
Überladung von Namen
Ähnliche Abhängigkeiten gelten für objekt-orientierte Sprachen:
bei objekt-orientierter Sprache mit Vererbung zwischen Klassen
sollte die übergeordnete Klasse vor der Unterklasse besucht
werden
bei Überladung muss simultan die Signatur verglichen werden
eventuell muss eine Typüberprüfung vorgenommen werden
Sobald Namen von Bezeichnern aufgelöst sind, kann die
semantische Analyse fortsetzen mit der Typprüfung (oder
Typinferenz, siehe Vorlesung „Programmiersprachen”).
48 / 184
Mehrere Sorten von Bezeichnern
Einige Programmiersprachen unterscheiden verschiedene
Bezeichner:
C: Variablennamen und Typnamen
Java: Klassen, Methoden, Felder
Haskell: Typnamen, Variablen, Operatoren mit Prioritäten
49 / 184
Mehrere Sorten von Bezeichnern
Einige Programmiersprachen unterscheiden verschiedene
Bezeichner:
C: Variablennamen und Typnamen
Java: Klassen, Methoden, Felder
Haskell: Typnamen, Variablen, Operatoren mit Prioritäten
In einigen Fällen verändern Deklarationen in der Sprache die
Bedeutung eines Bezeichners:
Der Parser informiert den Scanner über eine neue Deklaration
Der Scanner generiert verschiedene Token für einen Bezeichner
Der Parser generiert einen Syntaxbaum, der von den bisherigen
Deklarationen abhängt
49 / 184
Mehrere Sorten von Bezeichnern
Einige Programmiersprachen unterscheiden verschiedene
Bezeichner:
C: Variablennamen und Typnamen
Java: Klassen, Methoden, Felder
Haskell: Typnamen, Variablen, Operatoren mit Prioritäten
In einigen Fällen verändern Deklarationen in der Sprache die
Bedeutung eines Bezeichners:
Der Parser informiert den Scanner über eine neue Deklaration
Der Scanner generiert verschiedene Token für einen Bezeichner
Der Parser generiert einen Syntaxbaum, der von den bisherigen
Deklarationen abhängt
Interaktion zwischen Parser und Scanner: problematisch!
49 / 184
Fixity-Deklaration in Haskell
Haskell erlaubt beliebige binäre Operatoren aus (?!^&|=+-_*/)+ .
In Haskell Standard-Bibliothek:
infixr
infixl
infixl
infix
8
7
6
4
^
*,/
+,==,/=
Grammatik ist generisch:
Exp0 ::= Exp0 LOp0 Exp1
|
Exp1 ROp0 Exp0
|
Exp1 Op0 Exp1
|
Exp1
..
.
Exp9
::= Exp9 LOp9 Exp
|
|
|
Exp
::=
|
Exp ROp9 Exp9
Exp Op9 Exp
Exp
ident j num
( Exp0 )
50 / 184
Fixity-Deklaration in Haskell
Haskell erlaubt beliebige binäre Operatoren aus (?!^&|=+-_*/)+ .
In Haskell Standard-Bibliothek:
infixr
infixl
infixl
infix
8
7
6
4
^
*,/
+,==,/=
Grammatik ist generisch:
Exp0 ::= Exp0 LOp0 Exp1
|
Exp1 ROp0 Exp0
|
Exp1 Op0 Exp1
|
Exp1
..
.
Exp9
::= Exp9 LOp9 Exp
|
|
|
Exp
::=
|
Exp ROp9 Exp9
Exp Op9 Exp
Exp
ident j num
( Exp0 )
Parser trägt Deklarationen
in Tabelle ein
Scanner erzeugt:
Operator - wird zum
Token LOp6 .
Operator * wird zum
Token LOp7 .
Operator == wird zum
Token Op4 .
etc.
Parser erkennt 3-4*5-6
als (3-(4*5))-6
50 / 184
Fixity-Deklarationen in Haskell: Beobachtungen
Nicht ohne Probleme:
Scanner hat einen Zustand, der vom Parser bestimmt wird
nicht mehr kontextfrei, braucht globale Datenstruktur
;
51 / 184
Fixity-Deklarationen in Haskell: Beobachtungen
Nicht ohne Probleme:
Scanner hat einen Zustand, der vom Parser bestimmt wird
nicht mehr kontextfrei, braucht globale Datenstruktur
;
ein Stück Kode kann mehrere Semantiken haben
syntaktische Korrektheit hängt evtl. von importierten Modulen ab
51 / 184
Fixity-Deklarationen in Haskell: Beobachtungen
Nicht ohne Probleme:
Scanner hat einen Zustand, der vom Parser bestimmt wird
nicht mehr kontextfrei, braucht globale Datenstruktur
;
ein Stück Kode kann mehrere Semantiken haben
syntaktische Korrektheit hängt evtl. von importierten Modulen ab
Fehlermeldungen des Parsers schwer verständlich
51 / 184
Fixity-Deklarationen in Haskell: Beobachtungen
Nicht ohne Probleme:
Scanner hat einen Zustand, der vom Parser bestimmt wird
nicht mehr kontextfrei, braucht globale Datenstruktur
;
ein Stück Kode kann mehrere Semantiken haben
syntaktische Korrektheit hängt evtl. von importierten Modulen ab
Fehlermeldungen des Parsers schwer verständlich
Im GHC Haskell Compiler werden alle Operatoren als LOp0 gelesen
und der AST anschliessend transformiert.
51 / 184
Typbezeichner und Variablen in C
Die C Grammatik unterscheidet zwischen Terminal typedef-name
und identifier.
Betrachte folgende Liste von Deklarationen:
typedef struct { int x,y } point_t;
point_t origin;
Relevante C Grammatik:
declaration
!
declaration-specifier !
|
declarator
!
(declaration-specifier)+ declarator ;
static j volatile typedef
void j char j char typedef-name
identifier j 52 / 184
Typbezeichner und Variablen in C
Die C Grammatik unterscheidet zwischen Terminal typedef-name
und identifier.
Betrachte folgende Liste von Deklarationen:
typedef struct { int x,y } point_t;
point_t origin;
Relevante C Grammatik:
declaration
!
declaration-specifier !
|
declarator
!
(declaration-specifier)+ declarator ;
static j volatile typedef
void j char j char typedef-name
identifier j Problem:
Parser trägt point_t in Typen-Tabelle ein wenn die declaration
Regel reduziert wird
52 / 184
Typbezeichner und Variablen in C
Die C Grammatik unterscheidet zwischen Terminal typedef-name
und identifier.
Betrachte folgende Liste von Deklarationen:
typedef struct { int x,y } point_t;
point_t origin;
Relevante C Grammatik:
declaration
!
declaration-specifier !
|
declarator
!
(declaration-specifier)+ declarator ;
static j volatile typedef
void j char j char typedef-name
identifier j Problem:
Parser trägt point_t in Typen-Tabelle ein wenn die declaration
Regel reduziert wird
Parserzustand hat mindestens ein Lookahead-Token
52 / 184
Typbezeichner und Variablen in C
Die C Grammatik unterscheidet zwischen Terminal typedef-name
und identifier.
Betrachte folgende Liste von Deklarationen:
typedef struct { int x,y } point_t;
point_t origin;
Relevante C Grammatik:
declaration
!
declaration-specifier !
|
declarator
!
(declaration-specifier)+ declarator ;
static j volatile typedef
void j char j char typedef-name
identifier j Problem:
Parser trägt point_t in Typen-Tabelle ein wenn die declaration
Regel reduziert wird
Parserzustand hat mindestens ein Lookahead-Token
Scanner hat point_t in zweiter Zeile also schon als identifier
gelesen
52 / 184
Typbezeichner und Variablen in C: Lösungen
Relevante C Grammatik:
declaration
declaration-specifier
declarator
! (declaration-specifier)+ declarator ;
! static j volatile typedef
|
void j char j char typedef-name
! identifier j Lösung schwierig:
53 / 184
Typbezeichner und Variablen in C: Lösungen
Relevante C Grammatik:
declaration
declaration-specifier
declarator
! (declaration-specifier)+ declarator ;
! static j volatile typedef
|
void j char j char typedef-name
! identifier j Lösung schwierig:
versuche, Lookahead-Token im Parser zu ändern
53 / 184
Typbezeichner und Variablen in C: Lösungen
Relevante C Grammatik:
declaration
declaration-specifier
declarator
! (declaration-specifier)+ declarator ;
! static j volatile typedef
|
void j char j char typedef-name
! identifier j Lösung schwierig:
versuche, Lookahead-Token im Parser zu ändern
füge folgende Regel zur Grammatik hinzu:
typedef-name ! identifier
53 / 184
Typbezeichner und Variablen in C: Lösungen
Relevante C Grammatik:
declaration
declaration-specifier
declarator
! (declaration-specifier)+ declarator ;
! static j volatile typedef
|
void j char j char typedef-name
! identifier j Lösung schwierig:
versuche, Lookahead-Token im Parser zu ändern
füge folgende Regel zur Grammatik hinzu:
typedef-name ! identifier
registriere den Typnamen früher
53 / 184
Typbezeichner und Variablen in C: Lösungen
Relevante C Grammatik:
declaration
declaration-specifier
declarator
! (declaration-specifier)+ declarator ;
! static j volatile typedef
|
void j char j char typedef-name
! identifier j Lösung schwierig:
versuche, Lookahead-Token im Parser zu ändern
füge folgende Regel zur Grammatik hinzu:
typedef-name ! identifier
registriere den Typnamen früher
separiere Regel für typedef Produktion
53 / 184
Typbezeichner und Variablen in C: Lösungen
Relevante C Grammatik:
declaration
declaration-specifier
declarator
! (declaration-specifier)+ declarator ;
! static j volatile typedef
|
void j char j char typedef-name
! identifier j Lösung schwierig:
versuche, Lookahead-Token im Parser zu ändern
füge folgende Regel zur Grammatik hinzu:
typedef-name ! identifier
registriere den Typnamen früher
separiere Regel für typedef Produktion
rufe alternative declarator Produktion auf, die identifier als
Typenamen registriert
53 / 184
Ausblick
Implementierung von Symboltabellen für C
nächste Woche: Überprüfung von Typen
54 / 184
Die semantische Analyse
Kapitel 3:
Typ-Überprüfung
55 / 184
Ziel der Typ-Überprüfung
In modernen (imperativen / objektorientierten / funktionalen)
Programmiersprachen besitzen Variablen und Funktionen einen Typ,
z.B. int, void*, struct { int x; int y; }.
Typen sind nützlich für:
die Speicherverwaltung;
die Vermeidung von Laufzeit-Fehlern
In imperativen / objektorientierten Programmiersprachen muss der
Typ bei der Deklaration spezifiziert und vom Compiler die typ-korrekte
Verwendung überprüft werden.
56 / 184
Typ-Ausdrücke
Typen werden durch Typ-Ausdrücke beschrieben.
Die Menge T der Typausdrücke enthält:
1
Basis-Typen: int, char, float, void, ...
2
Typkonstruktoren, die auf Typen angewendet werden
Beispiele für Typkonstruktoren:
Verbunde: struct { t1 a1 ; : : : tk ak ; }
Zeiger: t *
Felder: t []
in C kann/muss zusätzlich eine Größe spezifiziert werden
die Variable muss zwischen t und [n] stehen
Funktionen: t (t1 ; : : : ; tk )
in C muss die Variable zwischen t und (t1 ; : : : ; tk ) stehen.
in ML würde man diesen Typ anders herum schreiben:
t1 : : : tk ! t
wir benutzen (t1 ; : : : ; tk ) als Tupel-Typen
57 / 184
Typ-Namen
Ein Typ-Name ist ein Synonym für einen Typ-Ausdruck.
In C kann man diese mit Hilfe von typedef einführen.
Typ-Namen sind nützlich
als Abkürzung:
typedef struct { int x; int y; } point_t;
zur Konstruktion rekursiver Typen:
Erlaubt in C:
Lesbarer:
struct list {
int info;
struct list* next;
}
typedef struct list list_t;
struct list {
int info;
list_t* next;
}
list_t* head;
struct list* head;
58 / 184
Typ-Prüfung
Aufgabe:
Gegeben: eine Menge von Typ-Deklarationen = ft1 x1 ; : : : tm xm ; g
Überprüfe: Kann ein Ausdruck e mit dem Typ t versehen werden?
Beispiel:
struct list { int info; struct list* next; };
int f(struct list* l) { return 1; };
struct { struct list* c;}* b;
int* a[11];
Betrachte den Ausdruck:
*a[f(b->c)]+2;
59 / 184
Typ-Prüfung am Syntax-Baum
Prüfe Ausdruck *a[f(b->c)]+2:
+
2
[ ]
( )
a
f
:
c
b
Idee:
traversiere den Syntaxbaum bottom-up
für Bezeichner schlagen wir in den richtigen Typ nach
Konstanten wie 2 oder 0:5 sehen wir den Typ direkt an
die Typen für die inneren Knoten erschießen wir mit Hilfe von
Typ-Regeln
60 / 184
Typ-Systeme
Formal betrachten wir Aussagen der Form:
`e :
==
(In der Typ-Umgebung
t
hat e den Typ t)
Axiome:
Const:
Var:
` c : tc
` x : (x )
(tc Typ der Konstante c)
(x Variable)
Regeln:
Ref:
`e : t
` &e : t
Deref:
` e : t
` e : t
61 / 184
Typ-System für C-ähnliche Sprachen
Weitere Regeln für diverse Typausdrücke:
Array:
Array:
Struct:
App:
Op:
Cast:
` e1 : t ` e2 : int
` e1 [e2 ] : t
` e1 : t [ ]
` e2 : int
` e1 [e2 ] : t
` e : struct ft1 a1 ; : : : tm am ; g
` e:ai : ti
` e : t (t1 ; : : : ; tm )
` e1 : t1 : : : ` em :
` e(e1 ; : : : ; em ) : t
` e1 : int
` e2 : int
` e1 + e2 : int
` e : t1 t1 in t2 konvertierbar
` (t2 ) e : t2
tm
62 / 184
Beispiel: Typ-Prüfung
Ausdruck *a[f(b->c)]+2 und
=f
struct list { int info; struct list* next; };
int f(struct list* l);
struct { struct list* c;}* b;
int* a[11];
g:
+
2
[ ]
( )
a
f
:
c
b
63 / 184
Beispiel: Typ-Prüfung
Ausdruck *a[f(b->c)]+2:
+
int
int int [ ]
2
int
[ ]
( )
a
int (struct list )
int
f
int
:
struct fstruct list c;g
struct fstruct list c;g b
struct list c
64 / 184
Gleichheit von Typen
Zusammenfassung Typprüfung:
Welche Regel an einem Knoten angewendet werden muss,
ergibt sich aus den Typen für die bereits bearbeiteten
Kinderknoten
Dazu muss die Gleichheit von Typen festgestellt werden.
Typgleichheit in C:
struct A {} und struct B {} werden als verschieden
betrachtet
; der Compiler kann die Felder von A und B nach Bedarf
umordnen (in C nicht erlaubt)
um einen Verband A um weitere Felder zu erweitern, muss er
eingebettet werden:
typedef struct B {
struct A a;
int field_of_B;
} extension_of_A;
Nach typedef int C; haben C und int den gleichen Typ.
65 / 184
Strukturelle Typ-Gleichheit
Alternative Interpretation von Gleichheit (gilt nicht in C):
Semantisch können wir zwei rekursive Typen t1 ; t2 als gleich
betrachten, falls sie die gleiche Menge von Pfaden zulassen.
Beispiel:
struct list {
int info;
struct list* next;
}
struct list1 {
int info;
struct {
int info;
struct list1* next;
}* next;
}
Sei struct list* l oder struct list1* l. Beide erlauben
l->info
l->next->info
jedoch hat l jeweils einen anderen Typen in C.
66 / 184
Algorithmus zum Test auf Semantische Gleichheit
Idee:
Verwalte Äquivalenz-Anfragen für je zwei Typausdrücke
Sind die beiden Ausdrücke syntaktisch gleich, ist alles gut
Andernfalls reduziere die Äquivalenz-Anfrage zwischen
Äquivalenz-Anfragen zwischen (hoffentlich) einfacheren anderen
Typausdrücken
Nehmen wir an, rekursive Typen würden mit Hilfe von
Typ-Gleichungen der Form:
A=t
eingeführt (verzichte auf ein ). Dann definieren wir folgende Regeln:
67 / 184
Regeln für Wohlgetyptheit
t
t
s t
A t
A= s
s
struct fs1 a1 ; ... sm am ; g
s1 t1
t
s
t
struct ft1 a1 ; ... tm am ; g
sm tm
68 / 184
Beispiel:
A
B
= struct fint info; A next; g
= struct fint info;
struct fint info; B next; g next; g
Wir fragen uns etwa, ob gilt:
struct fint info; A next; g = B
Dazu konstruieren wir:
69 / 184
Beweis Beispiel:
A
B
=
=
f
f
f
struct int info; A
struct int info;
struct int info; B
structfint info; A next; g
structfint info; A next; g
int
next; g
next; g next; g
B
structfint info; : : : next; g
A
int
A
structfint info; A next; g
int
int
:::
structfint info; B next; g
structfint info; B next; g
A B
A B
structfint info; A next; g
B
70 / 184
Implementierung
Stoßen wir bei der Konstruktion des Beweisbaums auf eine
Äquivalenz-Anfrage, auf die keine Regel anwendbar ist, sind die
Typen ungleich
Die Konstruktion des Beweisbaums kann dazu führen, dass die
gleiche Äquivalenz-Anfrage ein weiteres Mal auftritt
Taucht eine Äquivalenz-Anfrage ein weiteres Mal auf, sind die
Typen der Anfrage per Definition gleich
Terminierung?
71 / 184
Implementierung
Stoßen wir bei der Konstruktion des Beweisbaums auf eine
Äquivalenz-Anfrage, auf die keine Regel anwendbar ist, sind die
Typen ungleich
Die Konstruktion des Beweisbaums kann dazu führen, dass die
gleiche Äquivalenz-Anfrage ein weiteres Mal auftritt
Taucht eine Äquivalenz-Anfrage ein weiteres Mal auf, sind die
Typen der Anfrage per Definition gleich
Terminierung?
die Menge D aller deklarierten Typen ist endlich
es gibt höchstens jDj2 viele Äquivalenzanfragen
wiederholte Anfragen sind automatisch erfüllt
; Terminierung ist gesichert
71 / 184
Überladung und Koersion
Manche Operatoren wie z.B. + sind überladen:
+ besitzt mehrere mögliche Typen
Zum Beispiel: int +(int,int), float +(float, float)
aber auch float* +(float*, int), int* +(int, int*)
je nach Typ hat der Operator + ein unterschiedliche
Implementation
welche Implementierung ausgewählt wird, entscheiden die
Argument-Typen
72 / 184
Überladung und Koersion
Manche Operatoren wie z.B. + sind überladen:
+ besitzt mehrere mögliche Typen
Zum Beispiel: int +(int,int), float +(float, float)
aber auch float* +(float*, int), int* +(int, int*)
je nach Typ hat der Operator + ein unterschiedliche
Implementation
welche Implementierung ausgewählt wird, entscheiden die
Argument-Typen
Koersion: Erlaube auch die Anwendung von + auf int und float.
anstatt + für alle Argument-Kombinationen zu definieren, werden
die Typen der Argumente konvertiert
Konvertierung kann Code erzeugen (z.B. Umwandlung von int
nach float)
Konvertiert wird in der Regel auf die Supertypen, d.h. 5+0.5 hat
Typ float (da float int)
72 / 184
Koersion von Integer-Typen in C: Promotion
C enthält spezielle Koersionsregeln für Integer: Promotion
unsigned char
signed char
:::
unsigned short
signed short
int unsigned
int
wobei eine Konvertierung über alle Zwischentypen gehen muss.
73 / 184
Koersion von Integer-Typen in C: Promotion
C enthält spezielle Koersionsregeln für Integer: Promotion
unsigned char
signed char
:::
unsigned short
signed short
int unsigned
int
wobei eine Konvertierung über alle Zwischentypen gehen muss.
Subtile Fehler möglich! Berechne Zeichenverteilung in char* str:
char* str = "...";
int dist[256];
memset(dist, 0, sizeof(dist));
while (*str) {
dist[(unsigned) *str]++;
str++;
};
Beachte: unsigned bedeutet unsigned int.
73 / 184
Teiltypen
Auf den arithmetischen Basistypen char, int, long, etc. gibt
es i.a. eine reichhaltige Teiltypen-Beziehungen.
Dabei bedeutet t1 t2 , dass die Menge der Werte vom Typ t1
1
2
3
eine Teilmenge der Werte vom Typ t2 sind;
in einen Wert vom Typ t2 konvertiert werden können;
die Anforderungen an Werte vom Typ t2 erfüllen.
74 / 184
Teiltypen
Auf den arithmetischen Basistypen char, int, long, etc. gibt
es i.a. eine reichhaltige Teiltypen-Beziehungen.
Dabei bedeutet t1 t2 , dass die Menge der Werte vom Typ t1
1
2
3
eine Teilmenge der Werte vom Typ t2 sind;
in einen Wert vom Typ t2 konvertiert werden können;
die Anforderungen an Werte vom Typ t2 erfüllen.
Erweitere Teiltypen-Beziehungen der Basistypen auf komplexe Typen
74 / 184
Beispiel: Teiltypen
Betrachte:
string extractInfo( struct { string info; } x) {
return x.info;
}
Wir möchten dass extractInfo für alle Argument-Strukturen
funktioniert, die eine Komponente string info besitzen
Wann t1
t2 gelten soll, beschreiben wir durch Regeln
Die Idee ist vergleichbar zur Anwendbarkeit auf Unterklassen
(aber allgemeiner)
75 / 184
Regeln für Wohlgetyptheit von Teiltypen
t
t
s t
A t
A= s
s
t
s
t
struct fs1 a1 ; ... sm am ; g struct ftj1 aj1 ; ... tjk ajk ; g
s1 tj1
sm tjk
76 / 184
Regeln und Beispiel für Teiltypen
s0 (s1 ; : : : ; sm )
s0 t0
t1 s1
t0 (t1 ; : : : ; tm )
tm sm
Beispiele:
struct fint a; int b; g
int (int)
int (float)
struct ffloat a; g
float (float)
float (int)
77 / 184
Regeln und Beispiel für Teiltypen
s0 (s1 ; : : : ; sm )
s0 t0
t1 s1
t0 (t1 ; : : : ; tm )
tm sm
Beispiele:
struct fint a; int b; g
int (int)
int (float)
struct ffloat a; g
float (float)
float (int)
Achtung:
77 / 184
Regeln und Beispiel für Teiltypen
s0 (s1 ; : : : ; sm )
s0 t0
t0 (t1 ; : : : ; tm )
t1 s1
tm sm
Beispiele:
struct fint a; int b; g
int (int)
int (float)
6
struct ffloat a; g
float (float)
float (int)
Achtung:
Bei Funktionen gilt:
Rückgabewerte sind in normaler Teiltypen-Beziehung
bei Argumenten dreht sich die Anordnung der Typen um
77 / 184
Ko- und Kontravarianz
Definition
Sei s0 (s1 ; : : : sn ) t0 (t1 ; : : : tn ) zwei Funktionstypen, in
Teiltyp-Beziehung. Dann gilt:
t0
Kontravarianz der Argumente si ti für 1 < i n
Kovarianz der Rückgabewerte s0
78 / 184
Ko- und Kontravarianz
Definition
Sei s0 (s1 ; : : : sn ) t0 (t1 ; : : : tn ) zwei Funktionstypen, in
Teiltyp-Beziehung. Dann gilt:
t0
Kontravarianz der Argumente si ti für 1 < i n
Kovarianz der Rückgabewerte s0
Betrachte Beispiel aus funktionalen Sprachen:
int ! float ! int
int ! int ! float
78 / 184
Ko- und Kontravarianz
Definition
Sei s0 (s1 ; : : : sn ) t0 (t1 ; : : : tn ) zwei Funktionstypen, in
Teiltyp-Beziehung. Dann gilt:
t0
Kontravarianz der Argumente si ti für 1 < i n
Kovarianz der Rückgabewerte s0
Betrachte Beispiel aus funktionalen Sprachen:
int ! float ! int
int ! int ! float
Diese Regeln können wir direkt benutzen, um auch für rekursive
Typen die Teiltyp-Relation zu entscheiden
78 / 184
Teiltypen: Anwendung der Regeln (I)
Prüfe ob S1
R1 :
R1
S1
R2
S2
=
=
=
=
struct fint a;
struct fint a;
struct fint a;
struct fint a;
R1 (R1 ) f ; g
int b; S1 (S1 ) f ; g
R2 (S2 ) f ; g
int b; S2 (R2 ) f ; g
S1 R1
a
int
int
f
S1 (S1 )
R1 (R1 )
S1 R1
R1 S1
79 / 184
Teiltypen: Anwendung der Regeln (II)
Prüfe ob S2
S1 :
R1
S1
R2
S2
=
=
=
=
struct
struct
struct
struct
fint a; R1 (R1 ) f ; g
fint a; int b; S1 (S1 ) f ; g
fint a; R2 (S2 ) f ; g
fint a; int b; S2 (R2 ) f ; g
S2 S1
a, b
int
int
f
S2 (R2 )
S1 (S1 )
S2 S1
S1 R2
a
int
int
f
S1 (S1 )
R2 (S2 )
S1 R2
S2 S1
80 / 184
Teiltypen: Anwendung der Regeln (III)
Prüfe ob S2
R1 :
R1
S1
R2
S2
=
=
=
=
struct
struct
struct
struct
fint a; R1 (R1 ) f ; g
fint a; int b; S1 (S1 ) f ; g
fint a; R2 (S2 ) f ; g
fint a; int b; S2 (R2 ) f ; g
S 2 R1
a
int
int
f
S2 (R2 )
R1 (R1 )
S 2 R1
R1 R2
a
int
int
f
R1 (R1 )
R2 (S2 )
R1 R2
S2 R1
81 / 184
Diskussion
Um die Beweisbäume nicht in den Himmel wachsen zu lassen,
werden Zwischenknoten oft ausgelassen
Strukturelle Teiltypen sind sehr mächtig und deshalb nicht ganz
leicht zu durchschauen.
Java verallgemeinert Strukturen zu Objekten / Klassen.
Teiltyp-Beziehungen zwischen Klassen müssen explizit deklariert
werden
Durch Vererbung wird sichergestellt, dass Unterklassen über die
(sichtbaren) Komponenten der Oberklasse verfügen
Überdecken einer Komponente mit einer anderen gleichen
Namens ist möglich — aber nur, wenn diese keine Methode ist
82 / 184
Themengebiet:
Die Synthesephase
83 / 184
Codegenerierung: Überblick
Vom AST des Programs erzeugen wir induktiv Instruktionen:
für jedes Nicht-Terminal Symbol in der Grammatik gibt es eine
Regel zur Generierung von Machineninstruktionen
der Code ist nur ein weiteres Attribut im Syntaxbaum
die Codegenerierung benutzt die schon berechneten Attribute
84 / 184
Codegenerierung: Überblick
Vom AST des Programs erzeugen wir induktiv Instruktionen:
für jedes Nicht-Terminal Symbol in der Grammatik gibt es eine
Regel zur Generierung von Machineninstruktionen
der Code ist nur ein weiteres Attribut im Syntaxbaum
die Codegenerierung benutzt die schon berechneten Attribute
Um die Codeerzeugung zu spezifizieren, benötigt man
die Semantik der Ausgangssprache (C Standard)
die Semantik der Maschineninstruktionen
84 / 184
Codegenerierung: Überblick
Vom AST des Programs erzeugen wir induktiv Instruktionen:
für jedes Nicht-Terminal Symbol in der Grammatik gibt es eine
Regel zur Generierung von Machineninstruktionen
der Code ist nur ein weiteres Attribut im Syntaxbaum
die Codegenerierung benutzt die schon berechneten Attribute
Um die Codeerzeugung zu spezifizieren, benötigt man
die Semantik der Ausgangssprache (C Standard)
die Semantik der Maschineninstruktionen
; Wir definieren zunächst die Machineninstruktionen
84 / 184
Die Synthesephase
Kapitel 1:
Die Register C-Maschine
85 / 184
Die Register C-Maschine (R-CMa)
Wir erzeugen Code für die C-Maschine.
Die Register C-Maschine ist eine virtuelle Machine.
es existiert kein Prozessor, der die Instruktionen der CMA
ausführen kann
aber es steht ein Interpreter zur Verfügung
es seht auch eine Visualisierungsumgebung zur Verfügung
die R-CMa kennt keine double, float, char, short oder
long Typen
die R-CMa hat kennt keine Instruktionen um mit dem
Betriebssystem zu kommunizieren (Eingabe/Ausgabe)
die R-CMa hat eine unendliche Anzahl an Registern
86 / 184
Die Register C-Maschine (R-CMa)
Wir erzeugen Code für die C-Maschine.
Die Register C-Maschine ist eine virtuelle Machine.
es existiert kein Prozessor, der die Instruktionen der CMA
ausführen kann
aber es steht ein Interpreter zur Verfügung
es seht auch eine Visualisierungsumgebung zur Verfügung
die R-CMa kennt keine double, float, char, short oder
long Typen
die R-CMa hat kennt keine Instruktionen um mit dem
Betriebssystem zu kommunizieren (Eingabe/Ausgabe)
die R-CMa hat eine unendliche Anzahl an Registern
Die R-CMa ist realistischer als es auf den ersten Blick erscheint:
die Einschränkungen können mit wenig Aufwand aufgehoben
werden
die Java Virtual Machine (JVM) ist sehr ähnlich zur R-CMa ohne
Register
ein Interpreter für die R-CMa läuft auf jeder Platform
86 / 184
Virtuelle Maschinen
Ein virtuelle Maschine definiert sich wie folgt:
Jede virtuelle Maschine stellt einen Satz von Instruktionen zur
Verfügung.
Instruktionen werden auf der virtuellen Hardware ausgeführt.
Die virtuelle Hardware ist eine Menge von Datenstrukturen, auf
die die Instruktionen zugreifen
... und die vom Laufzeitsystem verwaltet werden.
der Interpreter ist Teil des Laufzeitsystems
87 / 184
Komponenten einer virtuellen Maschine
Beispiel für Java:
C
0 1
PC
0
SP
S
Eine virtuelle Maschine wie die JVM hat folgende Strukturen:
S: (Daten-)Speicher, auf dem nach dem LIFO-Prinzip neue
Zellen allokiert werden können
Keller/Stack.
SP: (=
b Stack Pointer) oberste belegte Zelle in S
am oberen Ende von S liegt der Heap
;
88 / 184
Komponenten einer virtuellen Maschine
Beispiel für Java:
C
0 1
PC
0
SP
S
Eine virtuelle Maschine wie die JVM hat folgende Strukturen:
S: (Daten-)Speicher, auf dem nach dem LIFO-Prinzip neue
Zellen allokiert werden können
Keller/Stack.
SP: (=
b Stack Pointer) oberste belegte Zelle in S
am oberen Ende von S liegt der Heap
C ist der Code-Speicher
;
Jede Zelle von C kann exakt einen virtuellen Befehl aufnehmen
C kann nur gelesen werden
PC (=
b Program Counter) Adresse des nächsten auszuführenden
Befehls
PC enthält zu Begin 0
88 / 184
Die Ausführung von Programmen
Die Maschine lädt die Instruktion aus C[PC] in ein
Instruktions-Register IR und führt sie aus.
Vor der Ausführung eines Befehls wird der PC um 1 erhöht.
while (true) {
IR = C[PC]; PC++;
execute (IR);
}
Der PC muss vor der Ausführung der Instruktion erhöht werden,
da diese möglicherweise den PC überschreibt
Die Schleife wird durch Ausführung der Instruktion halt
verlassen, die die Kontrolle an das Betriebssystem zurückgibt
89 / 184
Die Synthesephase
Kapitel 2:
Ausdrucksauswertung
90 / 184
Einfache Ausdrücke und Wertzuweisungen
Aufgabe: werte den Ausdruck (1 + 7) 3 aus
Das heißt: erzeuge eine Instruktionsfolge, die
den Wert des Ausdrucks ermittelt und dann
oben auf dem Keller ablegt
91 / 184
Einfache Ausdrücke und Wertzuweisungen
Aufgabe: werte den Ausdruck (1 + 7) 3 aus
Das heißt: erzeuge eine Instruktionsfolge, die
den Wert des Ausdrucks ermittelt und dann
oben auf dem Keller ablegt
Idee:
berechne erst die Werte für die Teilausdrücke;
merke diese Zwischenergebnisse oben auf dem Keller;
wende dann den Operator an
91 / 184
Generelles Prinzip
die Argumente für Instruktionen werden oben auf dem Keller
erwartet;
die Ausführung einer Instruktion konsumiert ihre Argumente;
möglicherweise berechnete Ergebnisse werden oben auf dem
Keller wieder abgelegt.
iconst q
q
SP++;
S[SP] = q;
Die Instruktion iconst q legt die int-Konstante q auf dem Stack ab.
92 / 184
Binäre Operatoren
Operatoren mit zwei Argumenten werden wie folgt angewandt:
3
8
24
imul
SP--;
S[SP] = S[SP] S[SP+1];
93 / 184
Binäre Operatoren
Operatoren mit zwei Argumenten werden wie folgt angewandt:
3
8
24
imul
SP--;
S[SP] = S[SP] S[SP+1];
imul erwartet zwei Argumente oben auf dem Stack, konsumiert
sie und legt sein Ergebnis oben auf dem Stack ab.
93 / 184
Binäre Operatoren
Operatoren mit zwei Argumenten werden wie folgt angewandt:
3
8
24
imul
SP--;
S[SP] = S[SP] S[SP+1];
imul erwartet zwei Argumente oben auf dem Stack, konsumiert
sie und legt sein Ergebnis oben auf dem Stack ab.
analog arbeiten auch die übrigen binären arithmetischen und
logischen Instruktionen iadd, isub, idiv, imod, etc.
93 / 184
Zusammensetzung von Instruktionen
Beispiel: Erzeuge Code für 1 + 7:
iconst 1
iconst 7
iadd
Ausführung dieses Codes:
iconst 1
1
iconst 7
7
1
iadd
8
94 / 184
Ausdrücke mit Variablen
Variablen haben eine Speicherzellen in S:
z:
y:
x:
95 / 184
Ausdrücke mit Variablen
Variablen haben eine Speicherzellen in S:
z:
y:
x:
Die Zuordnung von Adressen zu Variablen kann während der
Erstellung der Symboltabelle erfolgen. Die Adresse wird an dem
Deklarationsknoten der Variable gespeichert.
95 / 184
Ausdrücke mit Variablen
Variablen haben eine Speicherzellen in S:
z:
y:
x:
Die Zuordnung von Adressen zu Variablen kann während der
Erstellung der Symboltabelle erfolgen. Die Adresse wird an dem
Deklarationsknoten der Variable gespeichert.
Jede Benutzung einer Variable hat als Attribut einen Zeiger auf
den Deklarationsknoten der Variable.
95 / 184
Ausdrücke mit Variablen
Variablen haben eine Speicherzellen in S:
z:
y:
x:
Die Zuordnung von Adressen zu Variablen kann während der
Erstellung der Symboltabelle erfolgen. Die Adresse wird an dem
Deklarationsknoten der Variable gespeichert.
Jede Benutzung einer Variable hat als Attribut einen Zeiger auf
den Deklarationsknoten der Variable.
Im folgenden benutzen wir Übersetzungsfunktionen die eine
Funktion nehmen, die für jede Variable x die (Relativ-)Adresse
von x liefert. Die Funktion heißt Adress-Umgebung (Address
Environment).
95 / 184
Lesen von Variablen
Die Instruktion iload k lädt den Wert der Speicherzelle k, relativ zur
Spitze vom Stapel.
iload k
13
13
13
S[SP+1] = S[SP-k]; SP = SP+1;
Beispiel: Berechne x + 2 wobei = fx 7! 1g:
iload 1
iconst 2
iadd
96 / 184
Die Synthesephase
Kapitel 3:
Codeerzeugung für die Register-C-Maschine
97 / 184
Motivation für die Register C-Maschine
Ein moderner RISC-Prozessor besitzt eine fixe Anzahl von
Universalregistern.
98 / 184
Motivation für die Register C-Maschine
Ein moderner RISC-Prozessor besitzt eine fixe Anzahl von
Universalregistern.
arithmetische Operationen können oft nur auf diesen Registern
ausgeführt werden
Zugriffe auf den Speicher werden durch Adressen in Registern
vorgenommen
Register müssen gesichert werden, wenn Funktionen aufgerufen
werden
98 / 184
Motivation für die Register C-Maschine
Ein moderner RISC-Prozessor besitzt eine fixe Anzahl von
Universalregistern.
arithmetische Operationen können oft nur auf diesen Registern
ausgeführt werden
Zugriffe auf den Speicher werden durch Adressen in Registern
vorgenommen
Register müssen gesichert werden, wenn Funktionen aufgerufen
werden
Die Übersetzung von Code für einen solchen Prozessor muss:
98 / 184
Motivation für die Register C-Maschine
Ein moderner RISC-Prozessor besitzt eine fixe Anzahl von
Universalregistern.
arithmetische Operationen können oft nur auf diesen Registern
ausgeführt werden
Zugriffe auf den Speicher werden durch Adressen in Registern
vorgenommen
Register müssen gesichert werden, wenn Funktionen aufgerufen
werden
Die Übersetzung von Code für einen solchen Prozessor muss:
1
Variablen und Funktionsargumente in Registern speichern
2
während eines Funktionsaufrufes die entsprechenden Register
auf dem Keller sichern
3
beliebige Berechnungen mittels endlich vieler Register
ausdrücken
98 / 184
Motivation für die Register C-Maschine
Ein moderner RISC-Prozessor besitzt eine fixe Anzahl von
Universalregistern.
arithmetische Operationen können oft nur auf diesen Registern
ausgeführt werden
Zugriffe auf den Speicher werden durch Adressen in Registern
vorgenommen
Register müssen gesichert werden, wenn Funktionen aufgerufen
werden
Die Übersetzung von Code für einen solchen Prozessor muss:
1
Variablen und Funktionsargumente in Registern speichern
2
während eines Funktionsaufrufes die entsprechenden Register
auf dem Keller sichern
3
beliebige Berechnungen mittels endlich vieler Register
ausdrücken
; betrachte nur die ersten zwei Punkte (und behandle den letzten
Punkt als separates Problem)
98 / 184
Prinzip der Register C-Maschine
Die R-CMa besitzt einen Stack/Heap und ein Code-Segment, wie die
JVM, und zusätzlich zwei unbegrenzte Mengen an Registern.
lokale Register sind R1 ; R2 ; : : : Ri ; : : :
globale Register sind R0 ; R
1 ; : : : Rj ; : : :
C
0 1
PC
0
SP
S
Rloc
R1
R6
Rglob
R0
R
4
99 / 184
Die Registersätze der R-CMa
Die zwei Registersätze haben die folgenden Aufgaben:
1
die lokalen Register Ri
speichern temporäre Zwischenergebnisse
speichern lokale Variablen einer Funktion
können effizient auf den Stack gerettet werden
100 / 184
Die Registersätze der R-CMa
Die zwei Registersätze haben die folgenden Aufgaben:
1
die lokalen Register Ri
speichern temporäre Zwischenergebnisse
speichern lokale Variablen einer Funktion
können effizient auf den Stack gerettet werden
2
die globalen Register Ri
speichern Parameter einer Funktion
speichern den Rückgabewert einer Funktion
100 / 184
Die Registersätze der R-CMa
Die zwei Registersätze haben die folgenden Aufgaben:
1
die lokalen Register Ri
speichern temporäre Zwischenergebnisse
speichern lokale Variablen einer Funktion
können effizient auf den Stack gerettet werden
2
die globalen Register Ri
speichern Parameter einer Funktion
speichern den Rückgabewert einer Funktion
Achtung:
zunächst benutzten wir Register nur für Zwischenergebnisse
100 / 184
Die Registersätze der R-CMa
Die zwei Registersätze haben die folgenden Aufgaben:
1
die lokalen Register Ri
speichern temporäre Zwischenergebnisse
speichern lokale Variablen einer Funktion
können effizient auf den Stack gerettet werden
2
die globalen Register Ri
speichern Parameter einer Funktion
speichern den Rückgabewert einer Funktion
Achtung:
zunächst benutzten wir Register nur für Zwischenergebnisse
Idee für Übersetzung: benutze Registerzähler i:
Register Rj mit j < i sind in Benutzung
Register Rj mit j i stehen zur Verfügung
100 / 184
Übersetzen von einfachen Anweisungen
Behandlung von Variablen in Registern; laden von Konstanten:
Instruktion
loadc Ri c
move Ri Rj
Semantik Intuition
Ri = c
lade Konstante
Ri = Rj
kopiere Rj nach Ri
101 / 184
Übersetzen von einfachen Anweisungen
Behandlung von Variablen in Registern; laden von Konstanten:
Instruktion
loadc Ri c
move Ri Rj
Semantik Intuition
Ri = c
lade Konstante
Ri = Rj
kopiere Rj nach Ri
Wir definieren folgende Übersetzungsschemata (mit x = a):
codeiR c codeiR
codeiR x
x
=e
= loadc Ri c
= move Ri Ra
= codeiR e move Ra Ri
101 / 184
Übersetzen von einfachen Anweisungen
Behandlung von Variablen in Registern; laden von Konstanten:
Instruktion
loadc Ri c
move Ri Rj
Semantik Intuition
Ri = c
lade Konstante
Ri = Rj
kopiere Rj nach Ri
Wir definieren folgende Übersetzungsschemata (mit x = a):
codeiR c codeiR
codeiR x
x
=e
= loadc Ri c
= move Ri Ra
= codeiR e move Ra Ri
Beachte: alle Instruktionen benutzen die Intel-Konvention (im Ggs.
zur AT&T Konvention): op dst src1 : : : srcn .
101 / 184
Übersetzen von Ausdrücken
Sei op = fadd; sub; div; mul; mod; le; gr; eq; leq; geq; and; org. Die
R-CMa kennt für jeden Operator op eine Instruktion
op Ri Rj Rk
wobei Ri das Zielregister, Rj das erste und Rk das zweite Argument ist.
Entsprechend erzeugen wir den Code wie folgt:
codeiR e1 op e2 =
codeiR e1 codeRi+1 e2 op Ri Ri Ri+1
102 / 184
Übersetzen von Ausdrücken
Sei op = fadd; sub; div; mul; mod; le; gr; eq; leq; geq; and; org. Die
R-CMa kennt für jeden Operator op eine Instruktion
op Ri Rj Rk
wobei Ri das Zielregister, Rj das erste und Rk das zweite Argument ist.
Entsprechend erzeugen wir den Code wie folgt:
codeiR e1 op e2 =
codeiR e1 codeRi+1 e2 op Ri Ri Ri+1
Beispiel: Übersetze 3*4 mit i = 4:
code4R 3*4 =
code4R 3 code5R 4 mul R4 R4 R5
102 / 184
Übersetzen von Ausdrücken
Sei op = fadd; sub; div; mul; mod; le; gr; eq; leq; geq; and; org. Die
R-CMa kennt für jeden Operator op eine Instruktion
op Ri Rj Rk
wobei Ri das Zielregister, Rj das erste und Rk das zweite Argument ist.
Entsprechend erzeugen wir den Code wie folgt:
codeiR e1 op e2 =
codeiR e1 codeRi+1 e2 op Ri Ri Ri+1
Beispiel: Übersetze 3*4 mit i = 4:
code4R 3*4 =
loadc R4 3
loadc R5 4
mul R4 R4 R5
102 / 184
Verwaltung temporärer Register
Beachte, dass temporäre Register wiederverwendet werden:
Übersetze 3*4+3*4 mit t = 4:
code4R 3*4+3*4 =
code4R 3*4 code5R 3*4 add R4 R4 R5
mit
codeiR 3*4 =
loadc Ri 3
loadc Ri+1 4
mul Ri Ri Ri+1
ergibt sich
code4R 3*4+3*4 =
103 / 184
Verwaltung temporärer Register
Beachte, dass temporäre Register wiederverwendet werden:
Übersetze 3*4+3*4 mit t = 4:
code4R 3*4+3*4 =
code4R 3*4 code5R 3*4 add R4 R4 R5
mit
codeiR 3*4 =
loadc Ri 3
loadc Ri+1 4
mul Ri Ri Ri+1
ergibt sich
code4R 3*4+3*4 =
loadc R4 3
loadc R5 4
mul R4 R4 R5
loadc R5 3
loadc R6 4
mul R5 R5 R6
add R4 R4 R5
103 / 184
Semantik der Operatoren
Die Operatoren haben die folgende Semantik:
add Ri Rj Rk
sub Ri Rj Rk
div Ri Rj Rk
mul Ri Rj Rk
mod Ri Rj Rk
le Ri Rj Rk
gr Ri Rj Rk
eq Ri Rj Rk
leq Ri Rj Rk
geq Ri Rj Rk
and Ri Rj Rk
or Ri Rj Rk
Ri = Rj + Rk
Ri = Rj Rk
Ri = Rj =Rk
Ri = Rj Rk
Ri = sgn(Rk )k wobei
jRj j = njRk j + k ^ n 0; 0 k < jRk j
Ri = if Rj < Rk then 1 else 0
Ri = if Rj > Rk then 1 else 0
Ri = if Rj = Rk then 1 else 0
Ri = if Rj Rk then 1 else 0
Ri = if Rj Rk then 1 else 0
Ri = Rj & Rk
// bit-wise and
Ri = Rj j Rk
// bit-wise or
104 / 184
Semantik der Operatoren
Die Operatoren haben die folgende Semantik:
add Ri Rj Rk
sub Ri Rj Rk
div Ri Rj Rk
mul Ri Rj Rk
mod Ri Rj Rk
le Ri Rj Rk
gr Ri Rj Rk
eq Ri Rj Rk
leq Ri Rj Rk
geq Ri Rj Rk
and Ri Rj Rk
or Ri Rj Rk
Ri = Rj + Rk
Ri = Rj Rk
Ri = Rj =Rk
Ri = Rj Rk
Ri = sgn(Rk )k wobei
jRj j = njRk j + k ^ n 0; 0 k < jRk j
Ri = if Rj < Rk then 1 else 0
Ri = if Rj > Rk then 1 else 0
Ri = if Rj = Rk then 1 else 0
Ri = if Rj Rk then 1 else 0
Ri = if Rj Rk then 1 else 0
Ri = Rj & Rk
// bit-wise and
Ri = Rj j Rk
// bit-wise or
Beachte: alle Register und Speicherzellen enthalten Zahlen in Z
104 / 184
Übersetzung unärer Operatoren
Die unären Operatoren op = fneg; notg nehmen zwei Register:
codeiR op e =
codeiR e op Ri Ri
105 / 184
Übersetzung unärer Operatoren
Die unären Operatoren op = fneg; notg nehmen zwei Register:
codeiR op e =
codeiR e op Ri Ri
Beachte: Wir verwenden dasselbe Register.
105 / 184
Übersetzung unärer Operatoren
Die unären Operatoren op = fneg; notg nehmen zwei Register:
codeiR op e =
codeiR e op Ri Ri
Beachte: Wir verwenden dasselbe Register.
Beispiel: Übersetze -4 nach R5 :
code5R -4 =
code5R 4 neg R5 R5
105 / 184
Übersetzung unärer Operatoren
Die unären Operatoren op = fneg; notg nehmen zwei Register:
codeiR op e =
codeiR e op Ri Ri
Beachte: Wir verwenden dasselbe Register.
Beispiel: Übersetze -4 nach R5 :
code5R -4 =
loadc R5 4
neg R5 R5
105 / 184
Übersetzung unärer Operatoren
Die unären Operatoren op = fneg; notg nehmen zwei Register:
codeiR op e =
codeiR e op Ri Ri
Beachte: Wir verwenden dasselbe Register.
Beispiel: Übersetze -4 nach R5 :
code5R -4 =
loadc R5 4
neg R5 R5
Die Operatoren haben die folgende Semantik:
not Ri Rj
neg Ri Rj
Ri
Ri
if Rj
Rj
= 0 then 1 else 0
105 / 184
Beispiel: Übersetzungschema für Ausdrücke
void f(void) {
int x,y,z;
x = y+z*3;
}
Sei Adressumgebung = fx 7! 1; y 7! 2; z 7! 3g gegeben.
Sei R4 das erste freie Register, d.h. i = 4.
Sei folgende Funktion gegeben:
code4 x=y+z*3 = code4R y+z*3 move R1 R4
106 / 184
Beispiel: Übersetzungschema für Ausdrücke
void f(void) {
int x,y,z;
x = y+z*3;
}
Sei Adressumgebung = fx 7! 1; y 7! 2; z 7! 3g gegeben.
Sei R4 das erste freie Register, d.h. i = 4.
Sei folgende Funktion gegeben:
code4 x=y+z*3 = code4R y+z*3 move R1 R4
code4R
y+z*3 = move R4 R2
code5R z*3 add R4 R4 R5
106 / 184
Beispiel: Übersetzungschema für Ausdrücke
void f(void) {
int x,y,z;
x = y+z*3;
}
Sei Adressumgebung = fx 7! 1; y 7! 2; z 7! 3g gegeben.
Sei R4 das erste freie Register, d.h. i = 4.
Sei folgende Funktion gegeben:
code4 x=y+z*3 = code4R y+z*3 move R1 R4
code4R
y+z*3 code5R z*3 = move R4 R2
code5R z*3 add R4 R4 R5
= move R5 R3
code6R 3 mul R5 R5 R6
106 / 184
Beispiel: Übersetzungschema für Ausdrücke
void f(void) {
int x,y,z;
x = y+z*3;
}
Sei Adressumgebung = fx 7! 1; y 7! 2; z 7! 3g gegeben.
Sei R4 das erste freie Register, d.h. i = 4.
Sei folgende Funktion gegeben:
code4 x=y+z*3 = code4R y+z*3 move R1 R4
code4R
y+z*3 = move R4 R2
code5R z*3 add R4 R4 R5
code5R z*3 code6R 3 = move R5 R3
code6R 3 mul R5 R5 R6
= loadc R6 3
106 / 184
Beispiel: Übersetzungschema für Ausdrücke
void f(void) {
int x,y,z;
x = y+z*3;
}
Sei Adressumgebung = fx 7! 1; y 7! 2; z 7! 3g gegeben.
Sei R4 das erste freie Register, d.h. i = 4.
Sei folgende Funktion gegeben:
code4 x=y+z*3 = code4R y+z*3 move R1 R4
code4R
y+z*3 code5R z*3 add R4 R4 R5
code5R z*3 ;
= move R4 R2
code6R 3 = move R5 R3
code6R 3 mul R5 R5 R6
= loadc R6 3
die Zuweisung x=y+z*3 wird übersetzt als
move R4 R2 ; move R5 R3 ; loadc R6 3; mul R5 R5 R6 ; add R4 R4 R5 ; move R1 R4
106 / 184
Die Synthesephase
Kapitel 4:
Anweisungen und Kontrollstrukturen
107 / 184
Über Anweisungen und Ausdrücke
Allgemeine Übersetzungsidee:
codei s :
geniere Kode für Anweisung s
codeiR e :
generiere Kode für Ausdruck e in Ri
Es gilt: i; i + 1; : : : sind unbenutzte Register
108 / 184
Über Anweisungen und Ausdrücke
Allgemeine Übersetzungsidee:
codei s :
geniere Kode für Anweisung s
codeiR e :
generiere Kode für Ausdruck e in Ri
Es gilt: i; i + 1; : : : sind unbenutzte Register
Für den Ausdruck x = e mit x = a hatten wir definiert:
codeiR x = e = codeiR e move Ra Ri
Allerdings ist x = e auch eine Anweisung:
108 / 184
Über Anweisungen und Ausdrücke
Allgemeine Übersetzungsidee:
codei s :
geniere Kode für Anweisung s
codeiR e :
generiere Kode für Ausdruck e in Ri
Es gilt: i; i + 1; : : : sind unbenutzte Register
Für den Ausdruck x = e mit x = a hatten wir definiert:
codeiR x = e = codeiR e move Ra Ri
Allerdings ist x = e auch eine Anweisung:
Definiere:
codei e1
= e2 = codeiR e1 = e2 Das temporäre Register Ri wird dabei ignoriert. Allgemeiner:
codei e = codeiR e 108 / 184
Über Anweisungen und Ausdrücke
Allgemeine Übersetzungsidee:
codei s :
geniere Kode für Anweisung s
codeiR e :
generiere Kode für Ausdruck e in Ri
Es gilt: i; i + 1; : : : sind unbenutzte Register
Für den Ausdruck x = e mit x = a hatten wir definiert:
codeiR x = e = codeiR e move Ra Ri
Allerdings ist x = e auch eine Anweisung:
Definiere:
codei e1
= e2 = codeiR e1 = e2 Das temporäre Register Ri wird dabei ignoriert. Allgemeiner:
codei e = codeiR e Beobachtung: Die Zuweisung an e1 ist ein Seiteneffekt beim
Auswerten des Ausdrucks e1 = e2 .
108 / 184
Übersetzung von Anweisungsfolgen
Der Code für eine Anweisungsfolge ist die Konkatenation des Codes
for die einzelnen Anweisungen in der Folge:
codei (s ss) i
code " = codei s =
codei ss // leere Folge von Befehlen
Beachte: s ist eine Anweisung, ss ist eine Folge von Anweisungen
109 / 184
Sprünge
Um von linearer Ausführungsreihenfolge abzuweichen, benötigen wir
Sprünge:
jump A
A
PC
PC
PC = A;
110 / 184
Bedingte Sprünge
Ein bedingter Sprung verzweigt je nach Wert in Ri :
!0
Ri
jumpz Ri A
!0
Ri
PC
PC
0
Ri
0
Ri
jumpz Ri A
A
PC
PC
if (Ri == 0) PC = A;
111 / 184
Verwaltung von Kontrollfluss
Zur Übersetzung von Kontrollfluss Anweisungen müssen Sprünge
erzeugt werden.
bei der Übersetung eines if (c) Konstrukts ist nicht klar, an
welcher Adresse gesprungen werden muss, wenn c falsch ist
112 / 184
Verwaltung von Kontrollfluss
Zur Übersetzung von Kontrollfluss Anweisungen müssen Sprünge
erzeugt werden.
bei der Übersetung eines if (c) Konstrukts ist nicht klar, an
welcher Adresse gesprungen werden muss, wenn c falsch ist
Instruktionsfolgen können unterschiedlich arrangiert werden
minimiere dabei die Anzahl der unbedingten Sprünge
minimiere so dass weniger innerhalb Schleifen gesprungen wird
ersetze weite Sprünge (far jumps) durch nahe Sprünge (near
jumps)
112 / 184
Verwaltung von Kontrollfluss
Zur Übersetzung von Kontrollfluss Anweisungen müssen Sprünge
erzeugt werden.
bei der Übersetung eines if (c) Konstrukts ist nicht klar, an
welcher Adresse gesprungen werden muss, wenn c falsch ist
Instruktionsfolgen können unterschiedlich arrangiert werden
minimiere dabei die Anzahl der unbedingten Sprünge
minimiere so dass weniger innerhalb Schleifen gesprungen wird
ersetze weite Sprünge (far jumps) durch nahe Sprünge (near
jumps)
organisiere die Instruktionen in Blöcke ohne Sprünge
112 / 184
Verwaltung von Kontrollfluss
Zur Übersetzung von Kontrollfluss Anweisungen müssen Sprünge
erzeugt werden.
bei der Übersetung eines if (c) Konstrukts ist nicht klar, an
welcher Adresse gesprungen werden muss, wenn c falsch ist
Instruktionsfolgen können unterschiedlich arrangiert werden
minimiere dabei die Anzahl der unbedingten Sprünge
minimiere so dass weniger innerhalb Schleifen gesprungen wird
ersetze weite Sprünge (far jumps) durch nahe Sprünge (near
jumps)
organisiere die Instruktionen in Blöcke ohne Sprünge
Dazu definieren wir:
Definition
Ein Basisblock (basic block) besteht aus
einer Folge von Anweisungen ss, die keine Sprünge enthält
einer ausgehenden Menge von Kanten zu anderen Basisblöcken
wobei jede Kante mit einer Bedingung annotiert sein kann
112 / 184
Basisblöcke in der Register C-Maschine
Die R-CMa verfügt nur über einen bedingten Sprung, jumpz.
code ss
c
Die ausgehenden Kanten haben eine spezielle Form:
113 / 184
Basisblöcke in der Register C-Maschine
Die R-CMa verfügt nur über einen bedingten Sprung, jumpz.
code ss
c
Die ausgehenden Kanten haben eine spezielle Form:
1
eine Kante (unbedingter Sprung), mit jump übersetzt
113 / 184
Basisblöcke in der Register C-Maschine
Die R-CMa verfügt nur über einen bedingten Sprung, jumpz.
code ss
c
Die ausgehenden Kanten haben eine spezielle Form:
1
2
eine Kante (unbedingter Sprung), mit jump übersetzt
eine Kante mit Bedingung c = 0 (jumpz), eine Kante ohne
Bedinung (jump)
113 / 184
Basisblöcke in der Register C-Maschine
Die R-CMa verfügt nur über einen bedingten Sprung, jumpz.
code ss
c
Die ausgehenden Kanten haben eine spezielle Form:
1
2
3
eine Kante (unbedingter Sprung), mit jump übersetzt
eine Kante mit Bedingung c = 0 (jumpz), eine Kante ohne
Bedinung (jump)
eine Liste von Kanten (jumpi) und eine default Kante (jump); wird
verwendet für switch Anweisungen (kommt später)
113 / 184
Beschreibung von Kontrollfluss Übersetzungen
Der Einfachheit halber verwenden wir symbolische Sprungziele zur
Beschreibung der Übersetzung von Kontrollflussinstruktionen.
Ein zweiten Durchlauf ist nötig, um symbolische Adressen durch
absolute Code-Adressen zu ersetzen.
114 / 184
Beschreibung von Kontrollfluss Übersetzungen
Der Einfachheit halber verwenden wir symbolische Sprungziele zur
Beschreibung der Übersetzung von Kontrollflussinstruktionen.
Ein zweiten Durchlauf ist nötig, um symbolische Adressen durch
absolute Code-Adressen zu ersetzen.
Alternativ könnten direkt relative Sprünge eingeführt werden:
relative Sprünge haben Ziele relative zum aktuellen PC
; near jumps)
oft reichen kleinere Adressen aus (
der Code wird relokierbar, d. h. kann im Speicher unverändert
hin und her geschoben werden (position independent code, PIC)
Der erzeugte Code wäre im Prinzip direkt ausführbar.
114 / 184
Beschreibung von Kontrollfluss Übersetzungen
Der Einfachheit halber verwenden wir symbolische Sprungziele zur
Beschreibung der Übersetzung von Kontrollflussinstruktionen.
Ein zweiten Durchlauf ist nötig, um symbolische Adressen durch
absolute Code-Adressen zu ersetzen.
Alternativ könnten direkt relative Sprünge eingeführt werden:
relative Sprünge haben Ziele relative zum aktuellen PC
; near jumps)
oft reichen kleinere Adressen aus (
der Code wird relokierbar, d. h. kann im Speicher unverändert
hin und her geschoben werden (position independent code, PIC)
Der erzeugte Code wäre im Prinzip direkt ausführbar.
Basisböcke haben den Vorteil, dass sie später zur
Programmoptimierung weiterverwendet werden können.
(Z.B. Reduzierung der Anzahl an Sprüngen.)
114 / 184
Bedingte Anweisung, einseitig
Betrachten wir zuerst s if ( c ) ss.
Wir beschreiben die Code-Erzeugung zunächst ohne Basic-Blöcke.
Idee:
Lege den Code zur Auswertung von c und ss hintereinander in
den Code-Speicher;
Füge Sprung-Befehlen so ein, dass ein korrekter Kontroll-Fluss
gewährleistet ist
codei s = codeiR c jumpz Ri A
codei ss A:
:::
code R für c
jumpz
code für ss
115 / 184
Bedingte Anweisung, zweiseitig
code
c
code
code
tt
ee
Übersetzung von if ( c ) tt else ee.
codei if(c) tt else ee code R für c
=
codeiR c jumpz Ri A
codei tt jump B
A:
B:
jumpz
code für tt
jump
i
code ee code für ee
116 / 184
Beispiel für if-Anweisung
Sei = fx 7! 4; y 7! 7g und s die Anweisung:
if (x>y) {
x = x - y;
} else {
y = y - x;
}
/* (i) */
/* (ii) */
/* (iii) */
Dann liefert codei s :
117 / 184
Beispiel für if-Anweisung
Sei = fx 7! 4; y 7! 7g und s die Anweisung:
if (x>y) {
x = x - y;
} else {
y = y - x;
}
/* (i) */
/* (ii) */
/* (iii) */
Dann liefert codei s :
(i)
(ii)
move Ri R4
move Ri R4
move Ri+1 R7
move Ri+1 R7
gr Ri Ri Ri+1
jumpz Ri A
sub Ri Ri Ri+1
(iii)
A : move Ri R7
move Ri+1 R4
sub Ri Ri Ri+1
move R4 Ri
jump B
move R7 Ri
B:
117 / 184
Iterative Anweisungen
Betrachte schließlich die Schleife s while (e) s0 . Dafür erzeugen wir:
codei while(e) s = A : codeiR e jumpz Ri B
codei s jump A
B:
code R für e
jumpz
code für s’
jump
118 / 184
Beispiel: Übersetzung von Schleifen
Sei = fa 7! 7; b 7! 8; c 7! 9g und s die Anweisung:
while (a>0) {
c = c + 1;
a = a - b;
}
/* (i) */
/* (ii) */
/* (iii) */
Dann liefert codei s die Folge:
119 / 184
Beispiel: Übersetzung von Schleifen
Sei = fa 7! 7; b 7! 8; c 7! 9g und s die Anweisung:
while (a>0) {
c = c + 1;
a = a - b;
}
/* (i) */
/* (ii) */
/* (iii) */
Dann liefert codei s die Folge:
(i)
A:
(ii)
(iii)
move Ri R7
move Ri R9
move Ri R7
loadc Ri+1 0
loadc Ri+1 1
move Ri+1 R8
gr Ri Ri Ri+1
add Ri Ri Ri+1
jumpz Ri B
move R9 Ri
sub Ri Ri Ri+1
move R7 Ri
jump A
B:
119 / 184
for-Schleifen
Die for-Schleife s for (e1 ; e2 ; e3 ) s0 ist äquivalent zu der
Statementfolge e1 ; while (e2 ) fs0 e3 ; g – sofern s0 keine
continue-Anweisung enthält.
Darum übersetzen wir:
codei for(e1 ; e2 ; e3 ) s = codeiR e1 A : codeiR e2 jumpz Ri B
codei s codeiR e3 jump A
B:
120 / 184
Das switch-Statement
Idee:
Unterstütze Mehrfachverzweigung in konstanter Zeit
Benutze Sprungtabelle, die an der i-ten Stelle den Sprung an
den Anfang der i-tem Alternative enthält.
Eine Möglichkeit zur Realisierung besteht in der Einführung von
indizierten Sprüngen.
q
Ri
jumpi Ri A
q
Ri
B
A+q
PC
PC
PC = A + Ri ;
121 / 184
Aufeinanderfolgende Alternativen
Sei ein switch s mit k aufeinanderfolgenden case Fällen gegeben:
switch (e) {
case c0 : s0 ; break;
.
.
.
case ck 1 : sk 1 ; break;
default: s; break;
}
d.h. ci + 1 = ci+1 für i = [0; k 1].
122 / 184
Aufeinanderfolgende Alternativen
Sei ein switch s mit k aufeinanderfolgenden case Fällen gegeben:
switch (e) {
case c0 : s0 ; break;
.
.
.
case ck 1 : sk 1 ; break;
default: s; break;
}
d.h. ci + 1 = ci+1 für i = [0; k 1]. Definiere codei s wie folgt:
codei s =
codeiR e codei s0 B:
..
.
jump D
..
.
C:
checki c0 ck
A0
:
..
.
Ak
1
: codei sk
1
B
jump A0
..
.
jump Ak
1
1 jump D
122 / 184
Aufeinanderfolgende Alternativen
Sei ein switch s mit k aufeinanderfolgenden case Fällen gegeben:
switch (e) {
case c0 : s0 ; break;
.
.
.
case ck 1 : sk 1 ; break;
default: s; break;
}
d.h. ci + 1 = ci+1 für i = [0; k 1]. Definiere codei s wie folgt:
codei s =
codeiR e codei s0 B:
..
.
jump D
..
.
C:
checki c0 ck
A0
:
..
.
Ak
1
: codei sk
1
B
jump A0
..
.
jump Ak
1
1 jump D
checki l u B prüft ob l Ri < u gilt und springt entsprechend.
122 / 184
Übersetztung des checki Makros
Das Makro checki l u B prüft ob l Ri < u. Sei k = u
wenn l Ri < u, springt es nach B + Ri
wenn Ri < l oder Ri
u springt es nach C
B:
..
.
l.
l
jump A0
..
.
jump Ak
1
C:
123 / 184
Übersetztung des checki Makros
Das Makro checki l u B prüft ob l Ri < u. Sei k = u
wenn l Ri < u, springt es nach B + Ri
wenn Ri < l oder Ri
Wir definieren:
checki l u B
u springt es nach C
= loadc Ri+1 l
geq Ri+2 Ri Ri+1
jumpz Ri+2 E
sub Ri Ri Ri+1
loadc Ri+1 k
geq Ri+2 Ri Ri+1
jumpz Ri+2 D
E : loadc Ri k
D : jumpi Ri B
B:
..
.
l.
l
jump A0
..
.
jump Ak
1
C:
123 / 184
Übersetztung des checki Makros
Das Makro checki l u B prüft ob l Ri < u. Sei k = u
wenn l Ri < u, springt es nach B + Ri
wenn Ri < l oder Ri
Wir definieren:
checki l u B
u springt es nach C
= loadc Ri+1 l
geq Ri+2 Ri Ri+1
jumpz Ri+2 E
sub Ri Ri Ri+1
loadc Ri+1 k
geq Ri+2 Ri Ri+1
jumpz Ri+2 D
E : loadc Ri k
D : jumpi Ri B
Beachte: ein Sprung jumpi Ri B mit Ri
B:
..
.
l.
l
jump A0
..
.
jump Ak
1
C:
= k landet bei C.
123 / 184
Übersetzung der Sprungtabelle
Dies ist nur eine Übersetzung für spezielle switch-Anweisungen.
Beginnt die Tabelle mit 0 statt mit u, brauchen wir den R-Wert
von e nicht vermindern, bevor wir ihn als Index benutzen.
Sind sämtliche möglichen Werte von e sicher im Intervall [l; u],
können wir auf check verzichten.
Kann die Übersetzung von switch-Anweisungen als
L-Attributierung implementiert werden?
124 / 184
Übersetzung der Sprungtabelle
Dies ist nur eine Übersetzung für spezielle switch-Anweisungen.
Beginnt die Tabelle mit 0 statt mit u, brauchen wir den R-Wert
von e nicht vermindern, bevor wir ihn als Index benutzen.
Sind sämtliche möglichen Werte von e sicher im Intervall [l; u],
können wir auf check verzichten.
Kann die Übersetzung von switch-Anweisungen als
L-Attributierung implementiert werden?
schwierig, denn Marke B is unbekannt wenn checki übersetzt wird
flexiblere Anordnung wenn Kode-Sequenzen aneinandergehängt
werden können
im Extrem: Übersetzung mit Basisblöcken
124 / 184
Allgemeine Übersetzung der switch-Anweisung
Im Allgemeinen können die Werte der Alternativen weit auseinander
liegen.
generiere eine Folge von expliziten Tests, wie für die
if-Anweisung
125 / 184
Allgemeine Übersetzung der switch-Anweisung
Im Allgemeinen können die Werte der Alternativen weit auseinander
liegen.
generiere eine Folge von expliziten Tests, wie für die
if-Anweisung
bei n verschiedenen Tests kann binäre Suche angewendet
werden
O(log n) Tests
;
125 / 184
Allgemeine Übersetzung der switch-Anweisung
Im Allgemeinen können die Werte der Alternativen weit auseinander
liegen.
generiere eine Folge von expliziten Tests, wie für die
if-Anweisung
bei n verschiedenen Tests kann binäre Suche angewendet
werden
O(log n) Tests
;
hat die Menge der getesteten Werte kleine Lücken ( 3), so ist
Sprungtabelle effizienter und platzsparender
125 / 184
Allgemeine Übersetzung der switch-Anweisung
Im Allgemeinen können die Werte der Alternativen weit auseinander
liegen.
generiere eine Folge von expliziten Tests, wie für die
if-Anweisung
bei n verschiedenen Tests kann binäre Suche angewendet
werden
O(log n) Tests
;
hat die Menge der getesteten Werte kleine Lücken ( 3), so ist
Sprungtabelle effizienter und platzsparender
eventuell kann man mehrere Sprungtabellen für verschiedene,
zusammenhängende Mengen generieren
125 / 184
Allgemeine Übersetzung der switch-Anweisung
Im Allgemeinen können die Werte der Alternativen weit auseinander
liegen.
generiere eine Folge von expliziten Tests, wie für die
if-Anweisung
bei n verschiedenen Tests kann binäre Suche angewendet
werden
O(log n) Tests
;
hat die Menge der getesteten Werte kleine Lücken ( 3), so ist
Sprungtabelle effizienter und platzsparender
eventuell kann man mehrere Sprungtabellen für verschiedene,
zusammenhängende Mengen generieren
ein Suchbaum mit Tabellen kann durch profiling anders
angeordnet werden, so dass häufig genommene Pfade weniger
Tests erfordern
125 / 184
Übersetzung in Basic Blocks
Problem: Wie verknüpft man die verschiedenen Basic Blöcke?
Idee:
beim Übersetzen einer Funktion: erzeuge einen leeren Basic
Block und speichere den Zeiger im Konten der Funktion
126 / 184
Übersetzung in Basic Blocks
Problem: Wie verknüpft man die verschiedenen Basic Blöcke?
Idee:
beim Übersetzen einer Funktion: erzeuge einen leeren Basic
Block und speichere den Zeiger im Konten der Funktion
reiche diesen Basic Block zur Übersetzung der Anweisungen
runter
126 / 184
Übersetzung in Basic Blocks
Problem: Wie verknüpft man die verschiedenen Basic Blöcke?
Idee:
beim Übersetzen einer Funktion: erzeuge einen leeren Basic
Block und speichere den Zeiger im Konten der Funktion
reiche diesen Basic Block zur Übersetzung der Anweisungen
runter
eine Zuweisung wird am Ende des Basic Blocks angehängt
126 / 184
Übersetzung in Basic Blocks
Problem: Wie verknüpft man die verschiedenen Basic Blöcke?
Idee:
beim Übersetzen einer Funktion: erzeuge einen leeren Basic
Block und speichere den Zeiger im Konten der Funktion
reiche diesen Basic Block zur Übersetzung der Anweisungen
runter
eine Zuweisung wird am Ende des Basic Blocks angehängt
eine zweiseitige if Anweisung erzeugt drei neue, leere Blöcke:
1
2
3
einen für den then-Zweig, verbunden mit aktuellem Block durch
jumpz Kante
einen für den else-Zweig, durch jump Kante verbunden
einen für die nachfolgenden Anweisungen, verbunden mit thenund else-Zweig durch jump Kante
126 / 184
Übersetzung in Basic Blocks
Problem: Wie verknüpft man die verschiedenen Basic Blöcke?
Idee:
beim Übersetzen einer Funktion: erzeuge einen leeren Basic
Block und speichere den Zeiger im Konten der Funktion
reiche diesen Basic Block zur Übersetzung der Anweisungen
runter
eine Zuweisung wird am Ende des Basic Blocks angehängt
eine zweiseitige if Anweisung erzeugt drei neue, leere Blöcke:
1
2
3
einen für den then-Zweig, verbunden mit aktuellem Block durch
jumpz Kante
einen für den else-Zweig, durch jump Kante verbunden
einen für die nachfolgenden Anweisungen, verbunden mit thenund else-Zweig durch jump Kante
ähnlich für andere Konstrukte
126 / 184
Übersetzung in Basic Blocks
Problem: Wie verknüpft man die verschiedenen Basic Blöcke?
Idee:
beim Übersetzen einer Funktion: erzeuge einen leeren Basic
Block und speichere den Zeiger im Konten der Funktion
reiche diesen Basic Block zur Übersetzung der Anweisungen
runter
eine Zuweisung wird am Ende des Basic Blocks angehängt
eine zweiseitige if Anweisung erzeugt drei neue, leere Blöcke:
1
2
3
einen für den then-Zweig, verbunden mit aktuellem Block durch
jumpz Kante
einen für den else-Zweig, durch jump Kante verbunden
einen für die nachfolgenden Anweisungen, verbunden mit thenund else-Zweig durch jump Kante
ähnlich für andere Konstrukte
Zur besseren Navigation sollten auch Rückwärtskanten zwischen den
Blöcken eingefügt werden.
126 / 184
Die Synthesephase
Kapitel 5:
Funktionen
127 / 184
Aufbau einer Funktion
Die Definition einer Funktion besteht aus
einem Namen, mit dem sie aufgerufen werden kann;
einer Spezifikation der formalen Parameter;
evtl. einem Ergebnistyp;
einem Anweisungsteil.
In C gilt:
codeiR f =
loadc _f
mit _f Anfangsadresse des Codes für f
Beachte:
auch Funktions-Namen müssen eine Adresse zugewiesen
bekommen
da die Größe von Funktionen nicht vor der Übersetzung bekannt
ist, müssen die Adressen der Funktionen anschließend
eingetragen werden
128 / 184
Speicherverwaltung bei Funktionen
int fac(int x) {
if (x<=0) return 1;
else return x*fac(x-1);
}
int main(void) {
int n;
n = fac(2) + fac(1);
printf("%d", n);
}
Zu einem Ausführungszeitpunkt können mehrere Instanzen der
gleichen Funktion aktiv sein, d. h. begonnen, aber noch nicht beendet
sein.
Der Rekursionsbaum im Beispiel:
main
fac
fac
fac
fac
printf
fac
129 / 184
Speicherverwaltung von Funktionsvariablen
Die formalen Parameter und lokalen Variablen der verschiedenen
Aufrufe der selben Funktion (Instanzen) müssen auseinander
gehalten werden.
Idee zur Implementierung:
130 / 184
Speicherverwaltung von Funktionsvariablen
Die formalen Parameter und lokalen Variablen der verschiedenen
Aufrufe der selben Funktion (Instanzen) müssen auseinander
gehalten werden.
Idee zur Implementierung:
lege einen speziellen Speicherbereich für jeden Aufruf einer
Funktion an.
130 / 184
Speicherverwaltung von Funktionsvariablen
Die formalen Parameter und lokalen Variablen der verschiedenen
Aufrufe der selben Funktion (Instanzen) müssen auseinander
gehalten werden.
Idee zur Implementierung:
lege einen speziellen Speicherbereich für jeden Aufruf einer
Funktion an.
in sequentiellen Programmiersprachen können diese
Speicherbereiche auf dem Keller verwaltet werden
130 / 184
Speicherverwaltung von Funktionsvariablen
Die formalen Parameter und lokalen Variablen der verschiedenen
Aufrufe der selben Funktion (Instanzen) müssen auseinander
gehalten werden.
Idee zur Implementierung:
lege einen speziellen Speicherbereich für jeden Aufruf einer
Funktion an.
in sequentiellen Programmiersprachen können diese
Speicherbereiche auf dem Keller verwaltet werden
jede Instanz einer Funktion erhält dadurch einen Bereich auf
dem Stack
130 / 184
Speicherverwaltung von Funktionsvariablen
Die formalen Parameter und lokalen Variablen der verschiedenen
Aufrufe der selben Funktion (Instanzen) müssen auseinander
gehalten werden.
Idee zur Implementierung:
lege einen speziellen Speicherbereich für jeden Aufruf einer
Funktion an.
in sequentiellen Programmiersprachen können diese
Speicherbereiche auf dem Keller verwaltet werden
jede Instanz einer Funktion erhält dadurch einen Bereich auf
dem Stack
diese Bereiche heißen Keller-Rahmen (oder stack frame)
130 / 184
Kellerrahmen-Organisation
Stapel Präsentation: wächst nach oben, zu höheren Adressen
SP bestimmt die letzte, benutzte Stapeladresse
SP
lokaler Speicher
callee
FP
PCold
FPold
organisatorische
Zellen
EPold
lokaler Speicher
caller
131 / 184
Kellerrahmen-Organisation
Stapel Präsentation: wächst nach oben, zu höheren Adressen
SP bestimmt die letzte, benutzte Stapeladresse
SP
lokaler Speicher
callee
FP
PCold
FPold
organisatorische
Zellen
EPold
lokaler Speicher
caller
FP =
b Frame Pointer; zeigt auf die letzte organisatorische Zelle
wird zur Wiederherstellung des letzten Kellerrahmens benutzt
131 / 184
Kellerrahmen-Organisation
Stapel Präsentation: wächst nach oben, zu höheren Adressen
SP bestimmt die letzte, benutzte Stapeladresse
SP
lokaler Speicher
callee
FP
PCold
FPold
organisatorische
Zellen
EPold
lokaler Speicher
caller
FP =
b Frame Pointer; zeigt auf die letzte organisatorische Zelle
wird zur Wiederherstellung des letzten Kellerrahmens benutzt
EP hatten wir noch nicht; hat mit dem Heap zu tun
131 / 184
Arbeitsteilung beim Funktionsaufruf
Definition
Sei f die aktuelle Funktion, die die Funktion g aufruft.
f heißt caller
g heißt callee
Der Code für den Aufruf muss auf den Caller und den Callee verteilt
werden.
Die Aufteilung kann nur so erfolgen, dass der Teil, der von
Informationen des Callers abhängt, auch dort erzeugt wird und
analog für den Callee.
Beobachtung:
Den Platz für die aktuellen Parameter kennt nur der Caller:
Beispiel: printf
132 / 184
Prinzip vom Funktionsaufruf und Rücksprung
Aktionen beim Betreten von g:
1:
2:
3:
4:
5:
6:
7:
8:
Berechnung der Anfangsadresse von g
Berechnung der aktuellen Parameter
Retten aller caller-save Register
Retten von FP, EP
Setzen des neuen FP
Retten von PC und
Sprung an den Anfang von g
Setzen des neuen EP
Allokieren der lokalen Variablen
9
>
>
>
>
>
>
>
>
saveloc
=
stehen in f
mark
9
>
>
>
=
>
>
>
call
>
>
;
;
enter
stehen in g
alloc
Aktionen bei Verlassen von g:
1:
2:
3:
4:
5:
Berechnung des Rückgabewerts
Rücksetzen der Register FP, EP, SP
Rücksprung in den Code von f , d. h.
Restauration des PC
Wiederherstellen der caller-save Register
Aufräumen des Stack
9
=
9
>
>
=
return >
>
stehen in g
;
;
restoreloc stehen in f
pop k
133 / 184
Registerverwaltung bei Funktionsaufrufen
Die zwei Registersätze (global und lokal) werden wie folgt verwendet:
automatische Variablen leben in lokalen Registern Ri
Zwischenergebnisse leben auch in lokalen Registern Ri
Parameter leben in globalen Registern Ri (mit i 0)
globale Variablen:
134 / 184
Registerverwaltung bei Funktionsaufrufen
Die zwei Registersätze (global und lokal) werden wie folgt verwendet:
automatische Variablen leben in lokalen Registern Ri
Zwischenergebnisse leben auch in lokalen Registern Ri
Parameter leben in globalen Registern Ri (mit i 0)
globale Variablen: wie nehmen erstmal an, es gäbe keine
Konvention:
134 / 184
Registerverwaltung bei Funktionsaufrufen
Die zwei Registersätze (global und lokal) werden wie folgt verwendet:
automatische Variablen leben in lokalen Registern Ri
Zwischenergebnisse leben auch in lokalen Registern Ri
Parameter leben in globalen Registern Ri (mit i 0)
globale Variablen: wie nehmen erstmal an, es gäbe keine
Konvention:
die i te Argument einer Funktion wird in Register Ri übergeben
134 / 184
Registerverwaltung bei Funktionsaufrufen
Die zwei Registersätze (global und lokal) werden wie folgt verwendet:
automatische Variablen leben in lokalen Registern Ri
Zwischenergebnisse leben auch in lokalen Registern Ri
Parameter leben in globalen Registern Ri (mit i 0)
globale Variablen: wie nehmen erstmal an, es gäbe keine
Konvention:
die i te Argument einer Funktion wird in Register Ri übergeben
der Rückgabewert einer Funktion wird in R0 gespeichert
134 / 184
Registerverwaltung bei Funktionsaufrufen
Die zwei Registersätze (global und lokal) werden wie folgt verwendet:
automatische Variablen leben in lokalen Registern Ri
Zwischenergebnisse leben auch in lokalen Registern Ri
Parameter leben in globalen Registern Ri (mit i 0)
globale Variablen: wie nehmen erstmal an, es gäbe keine
Konvention:
die i te Argument einer Funktion wird in Register Ri übergeben
der Rückgabewert einer Funktion wird in R0 gespeichert
lokale Register werden von der aufrufenden Funktion
gespeichert
134 / 184
Registerverwaltung bei Funktionsaufrufen
Die zwei Registersätze (global und lokal) werden wie folgt verwendet:
automatische Variablen leben in lokalen Registern Ri
Zwischenergebnisse leben auch in lokalen Registern Ri
Parameter leben in globalen Registern Ri (mit i 0)
globale Variablen: wie nehmen erstmal an, es gäbe keine
Konvention:
die i te Argument einer Funktion wird in Register Ri übergeben
der Rückgabewert einer Funktion wird in R0 gespeichert
lokale Register werden von der aufrufenden Funktion
gespeichert
Definition
Sei f eine Funktion die g aufruft. Ein Register Ri heißt
caller-saved, wenn f Ri sichert und g es überschreiben darf
callee-saved, wenn f Ri nicht sichert und g es vor dem
Rücksprung wiederherstellen muss
134 / 184
Übersetzung von Funktionsaufrufen
Ein Funktionsaufruf g(e1 ; : : : en ) wird nun wie folgt übersetzt:
codeiR g(e1 ; : : : en ) = codeiR g codei+1 e1 R
..
.
codeiR+n en move R
..
.
move R
1
Ri+1
n
Ri+n
saveloc R1 Ri
1
mark
call Ri
restoreloc R1 Ri
1
move Ri R0
135 / 184
Übersetzung von Funktionsaufrufen
Ein Funktionsaufruf g(e1 ; : : : en ) wird nun wie folgt übersetzt:
codeiR g(e1 ; : : : en ) = codeiR g codei+1 e1 R
..
.
codeiR+n en move R
..
.
move R
1
Ri+1
n
Ri+n
saveloc R1 Ri
1
mark
call Ri
restoreloc R1 Ri
1
move Ri R0
Neue Instruktionen:
saveloc Ri Rj legt die Register Ri ; Ri+1 : : : Rj auf dem Stapel ab
mark rettet organisatorische Zellen
call Ri ruft Funktion auf die an Adresse Ri liegt
restoreloc Ri Rj nimmt Rj ; Rj 1 ; : : : Ri vom Stapel runter
135 / 184
Übersetzung von Funktionsaufrufen
Ein Funktionsaufruf g(e1 ; : : : en ) wird nun wie folgt übersetzt:
codeiR g(e1 ; : : : en ) =? codeiR e1 = codeiR g codei+1 e1 move R 1 Ri
..
.
codeiR en R
..
.
codeiR+n en move R
..
.
move R
1
n
Ri+1
move R
Ri+n
saveloc R1 Ri
n
Ri
codeiR g saveloc R1 Ri
1
mark
1
mark
call Ri
call Ri
restoreloc R1 Ri
restoreloc R1 Ri
1
1
move Ri R0
move Ri R0
Neue Instruktionen:
saveloc Ri Rj legt die Register Ri ; Ri+1 : : : Rj auf dem Stapel ab
mark rettet organisatorische Zellen
call Ri ruft Funktion auf die an Adresse Ri liegt
restoreloc Ri Rj nimmt Rj ; Rj 1 ; : : : Ri vom Stapel runter
135 / 184
Retten von EP und FP
Der Befehl mark legt Platz für Rückgabewert und organisatorische
Zellen an und rettet FP und EP.
FP
EP
FP
EP
e
e
e
mark
S[SP+1] = EP;
S[SP+2] = FP;
SP = SP + 2;
136 / 184
Aufrufen einer Funktion
Der Befehl call rettet den aktuellen Wert des PC als
Fortsetzungs-Adresse und setzt FP und PC.
FP
q
Ri
p
call Ri
q
Ri
PC
PC
p
q
S[SP] = PC;
SP = SP+1;
FP = SP;
PC = Ri;
137 / 184
Rückgabewerte einer Funktion
Die globalen Register werden auch benutzt um den Rückgabewert zu
übermitteln:
codei return e = codeiR e move R0 Ri
return
138 / 184
Rückgabewerte einer Funktion
Die globalen Register werden auch benutzt um den Rückgabewert zu
übermitteln:
codei return e = codeiR e move R0 Ri
return
Alternative ohne Rückgabewert:
codei return = return
138 / 184
Rückgabewerte einer Funktion
Die globalen Register werden auch benutzt um den Rückgabewert zu
übermitteln:
codei return e = codeiR e move R0 Ri
return
Alternative ohne Rückgabewert:
codei return = return
Globale Register werden ansonsten im Funktionsrumpf nicht benutzt:
Vorteil: an jeder Stelle im Rumpf können andere Funktionen
aufgerufen werden ohne globale Register retten zu müssen
Nachteil: beim Eintritt in eine Funktion müssen die globalen
Register gesichert werden
138 / 184
Rücksprung aus einer Funktion
Der Befehl return gibt den aktuellen Keller-Rahmen auf. D.h. er
restauriert die Register PC, EP und FP.
PC
FP
EP
p
return
PC
FP
EP
p
e
e
PC = S[FP]; EP = S[FP-2];
SP = FP-3; FP = S[SP+2];
139 / 184
Übersetzung ganzer Funktionen
Die Übersetzung einer Funktion ist damit wie folgt definiert:
code1 tr f(args)fdecls ssg = enter q
move Rl+1 R
..
.
1
move Rl+n R n
codel+n+1 ss 0
return
Randbedinungen:
140 / 184
Übersetzung ganzer Funktionen
Die Übersetzung einer Funktion ist damit wie folgt definiert:
code1 tr f(args)fdecls ssg = enter q
move Rl+1 R
..
.
1
move Rl+n R n
codel+n+1 ss 0
return
Randbedinungen:
die Funktion hat n Parameter
140 / 184
Übersetzung ganzer Funktionen
Die Übersetzung einer Funktion ist damit wie folgt definiert:
code1 tr f(args)fdecls ssg = enter q
move Rl+1 R
..
.
1
move Rl+n R n
codel+n+1 ss 0
return
Randbedinungen:
die Funktion hat n Parameter
die lokalen Variablen sind in den Registern R1 ; : : : Rl gespeichert
140 / 184
Übersetzung ganzer Funktionen
Die Übersetzung einer Funktion ist damit wie folgt definiert:
code1 tr f(args)fdecls ssg = enter q
move Rl+1 R
..
.
1
move Rl+n R n
codel+n+1 ss 0
return
Randbedinungen:
die Funktion hat n Parameter
die lokalen Variablen sind in den Registern R1 ; : : : Rl gespeichert
die Parameter der Funktion stehen in den Registern R 1 ; : : : R n
140 / 184
Übersetzung ganzer Funktionen
Die Übersetzung einer Funktion ist damit wie folgt definiert:
code1 tr f(args)fdecls ssg = enter q
move Rl+1 R
..
.
1
move Rl+n R n
codel+n+1 ss 0
return
Randbedinungen:
die Funktion hat n Parameter
die lokalen Variablen sind in den Registern R1 ; : : : Rl gespeichert
die Parameter der Funktion stehen in den Registern R 1 ; : : : R n
0
ist die um lokale Variablen decls und Funktions-Parameter args
erweiterte Umgebung 140 / 184
Übersetzung ganzer Funktionen
Die Übersetzung einer Funktion ist damit wie folgt definiert:
code1 tr f(args)fdecls ssg = enter q
move Rl+1 R
..
.
1
move Rl+n R n
codel+n+1 ss 0
return
Randbedinungen:
die Funktion hat n Parameter
die lokalen Variablen sind in den Registern R1 ; : : : Rl gespeichert
die Parameter der Funktion stehen in den Registern R 1 ; : : : R n
0
ist die um lokale Variablen decls und Funktions-Parameter args
erweiterte Umgebung return nicht immer nötig
140 / 184
Übersetzung ganzer Funktionen
Die Übersetzung einer Funktion ist damit wie folgt definiert:
code1 tr f(args)fdecls ssg = enter q
move Rl+1 R
..
.
1
move Rl+n R n
codel+n+1 ss 0
return
Randbedinungen:
die Funktion hat n Parameter
die lokalen Variablen sind in den Registern R1 ; : : : Rl gespeichert
die Parameter der Funktion stehen in den Registern R 1 ; : : : R n
0
ist die um lokale Variablen decls und Funktions-Parameter args
erweiterte Umgebung return nicht immer nötig
Sind die move Instruktionen immer nötig?
140 / 184
Übersetzen ganzer Programme
Ein Programm P = F1 ; : : : Fn muss eine main Funktion enthalten.
code1 P =
loadc R1 _main
mark
call R1
halt
code1 F1 f1
..
.
_f1
:
_fn
: code1 Fn fn
141 / 184
Übersetzen ganzer Programme
Ein Programm P = F1 ; : : : Fn muss eine main Funktion enthalten.
code1 P =
loadc R1 _main
mark
call R1
halt
code1 F1 f1
..
.
_f1
:
_fn
: code1 Fn fn
Diskussion:
= ; enthält die Adressen der globalen Variablen
fi
enthält die Adressen der lokalen Variablen
1
2 = x :
2 (x)
1 (x)
if x 2 dom(2 )
otherwise
141 / 184
Übersetzung der Fakultätsfunktion
Betrachte:
int fac(int x) {
if (x<=0) then
return 1;
else
return x*fac(x-1);
}
_fac:
i=2
enter 5
move R1 R 1
move R2 R1
loadc R3 0
leq R2 R2 R3
jumpz R2 _A
loadc R2 1
move R0 R2
return
jump _B
_A:
i=3
i=4
i=3
3 mark+call
save param.
if (x<=0)
to else
return 1
_B:
move R2 R1
move R3 R1
loadc R4 1
sub R3 R3 R4
move R 1 R3
loadc R3 _fac
saveloc R1 R2
mark
call R3
restoreloc R1 R2
move R3 R0
mul R2 R2 R3
move R0 R2
return
return
x*fac(x-1)
x-1
fac(x-1)
return x*...
code is dead
142 / 184
Themengebiet:
Variablen im Speicher
143 / 184
Register versus Speicher
Bisher:
alle Variablen sind in Registern gespeichert
alle Funktionsargumente und Rückgabewerte auch
144 / 184
Register versus Speicher
Bisher:
alle Variablen sind in Registern gespeichert
alle Funktionsargumente und Rückgabewerte auch
Beschränkungen:
in einer realen Maschine gibt es nur endlich viele Register
C erlaubt es, die Adresse von Variablen zu nehmen
Felder können nicht übersetzt werden, wegen der Indizierung
144 / 184
Register versus Speicher
Bisher:
alle Variablen sind in Registern gespeichert
alle Funktionsargumente und Rückgabewerte auch
Beschränkungen:
in einer realen Maschine gibt es nur endlich viele Register
C erlaubt es, die Adresse von Variablen zu nehmen
Felder können nicht übersetzt werden, wegen der Indizierung
Idee: speichere Variablen auch auf dem Keller
144 / 184
Variablen im Speicher
Kapitel 1:
Datenstrukturen im Speicher
145 / 184
Variablen im Speicher: L-Wert und R-Wert
Variablen können auf zwei Weisen verwendet werden.
Beispiel: a[x] = y + 1
Für y sind wir am Inhalt der Zelle, für a[x] an der Adresse
interessiert.
R-Wert von x
L-Wert von x
=
=
Inhalt von x
Adresse von x
Berechne R-Wert und L-Wert im Register Ri :
codeiR e codeiL e liefert den Code zur Berechnung des R-Werts von e
in der Adress-Umgebung analog für den L-Wert
Achtung:
Nicht jeder Ausdruck verfügt über einen L-Wert (z.B.: x + 1).
146 / 184
Adressumgebung
Eine Variable kann in einem von vier konzeptionell verschiedenen
Bereichen existieren.
1
Global: eine Variable ist global
2
Lokal: eine Variable liegt auf dem aktuellen Kellerrahmen
3
Register: eine Variable ist in lokalem Ri oder globalem Ri
147 / 184
Adressumgebung
Eine Variable kann in einem von vier konzeptionell verschiedenen
Bereichen existieren.
1
Global: eine Variable ist global
2
Lokal: eine Variable liegt auf dem aktuellen Kellerrahmen
3
Register: eine Variable ist in lokalem Ri oder globalem Ri
Entsprechend definieren wir : Var
! fG; L; Rg Z wie folgt:
x = hG; ai: Variable x ist an absoluter Adresse a gespeichert
x = hL; ai: Variable x ist an Adresse FP + a gespeichert
x = hR; ai: Variable x ist im Register Ra gespeichert
147 / 184
Adressumgebung
Eine Variable kann in einem von vier konzeptionell verschiedenen
Bereichen existieren.
1
Global: eine Variable ist global
2
Lokal: eine Variable liegt auf dem aktuellen Kellerrahmen
3
Register: eine Variable ist in lokalem Ri oder globalem Ri
Entsprechend definieren wir : Var
! fG; L; Rg Z wie folgt:
x = hG; ai: Variable x ist an absoluter Adresse a gespeichert
x = hL; ai: Variable x ist an Adresse FP + a gespeichert
x = hR; ai: Variable x ist im Register Ra gespeichert
Beachte: eine Variable x kann nur einen Eintrag in haben.
Allerdings:
147 / 184
Adressumgebung
Eine Variable kann in einem von vier konzeptionell verschiedenen
Bereichen existieren.
1
Global: eine Variable ist global
2
Lokal: eine Variable liegt auf dem aktuellen Kellerrahmen
3
Register: eine Variable ist in lokalem Ri oder globalem Ri
Entsprechend definieren wir : Var
! fG; L; Rg Z wie folgt:
x = hG; ai: Variable x ist an absoluter Adresse a gespeichert
x = hL; ai: Variable x ist an Adresse FP + a gespeichert
x = hR; ai: Variable x ist im Register Ra gespeichert
Beachte: eine Variable x kann nur einen Eintrag in haben.
Allerdings:
könnte unterschiedlich sein an verschiedenen
Programmpunkten
147 / 184
Adressumgebung
Eine Variable kann in einem von vier konzeptionell verschiedenen
Bereichen existieren.
1
Global: eine Variable ist global
2
Lokal: eine Variable liegt auf dem aktuellen Kellerrahmen
3
Register: eine Variable ist in lokalem Ri oder globalem Ri
Entsprechend definieren wir : Var
! fG; L; Rg Z wie folgt:
x = hG; ai: Variable x ist an absoluter Adresse a gespeichert
x = hL; ai: Variable x ist an Adresse FP + a gespeichert
x = hR; ai: Variable x ist im Register Ra gespeichert
Beachte: eine Variable x kann nur einen Eintrag in haben.
Allerdings:
könnte unterschiedlich sein an verschiedenen
Programmpunkten
d.h. x kann einem Register zugewiesen sein
und an einem anderen Punkt einer Speicherzelle
147 / 184
Notwendigkeit von Variablen im Speicher
Globale Variablen:
könnten programm-weit den Registern
R1 : : : Rn zugeordnet sein
z:
y:
x:
Weiterhin:
148 / 184
Notwendigkeit von Variablen im Speicher
Globale Variablen:
könnten programm-weit den Registern
R1 : : : Rn zugeordnet sein
z:
y:
x:
separate Übersetzung schwierig, da
Funktionscode von n abhängt
Weiterhin:
148 / 184
Notwendigkeit von Variablen im Speicher
Globale Variablen:
könnten programm-weit den Registern
R1 : : : Rn zugeordnet sein
z:
y:
x:
separate Übersetzung schwierig, da
Funktionscode von n abhängt
besser: speichere globale Variablen im
Speicher
Weiterhin:
148 / 184
Notwendigkeit von Variablen im Speicher
Globale Variablen:
könnten programm-weit den Registern
R1 : : : Rn zugeordnet sein
z:
y:
x:
separate Übersetzung schwierig, da
Funktionscode von n abhängt
besser: speichere globale Variablen im
Speicher
Weiterhin:
eine Variable x (int oder struct), deren Adresse genommen
wurde, muss im Speicher allokiert werden, d.h. x = hL; oi oder
x = hG; oi
148 / 184
Notwendigkeit von Variablen im Speicher
Globale Variablen:
könnten programm-weit den Registern
R1 : : : Rn zugeordnet sein
z:
y:
x:
separate Übersetzung schwierig, da
Funktionscode von n abhängt
besser: speichere globale Variablen im
Speicher
Weiterhin:
eine Variable x (int oder struct), deren Adresse genommen
wurde, muss im Speicher allokiert werden, d.h. x = hL; oi oder
x = hG; oi
auf ein Feld (array) wird immer mittels Zeiger zugegriffen, muss
also auch im Speicher allokiert werden
148 / 184
Notwendigkeit von Variablen im Speicher
Globale Variablen:
könnten programm-weit den Registern
R1 : : : Rn zugeordnet sein
z:
y:
x:
separate Übersetzung schwierig, da
Funktionscode von n abhängt
besser: speichere globale Variablen im
Speicher
Weiterhin:
eine Variable x (int oder struct), deren Adresse genommen
wurde, muss im Speicher allokiert werden, d.h. x = hL; oi oder
x = hG; oi
auf ein Feld (array) wird immer mittels Zeiger zugegriffen, muss
also auch im Speicher allokiert werden
Optimierung: Speichere Elemente eines struct in Registern,
während Zeigerzugriffe diese nicht verändern können
148 / 184
Übersetzung von Zuweisungen
Zuweisungen wie x=2*y wurden bisher übersetzt durch:
das Ermitteln das R-Wertes von 2*y in Register Ri ,
das Kopieren des Inhalts von Ri in das Register (x)
Formal: Sei (x) = hR; ji, dann gilt:
codeiR x = e2 = codeiR e2 move Rj Ri
149 / 184
Übersetzung von Zuweisungen
Zuweisungen wie x=2*y wurden bisher übersetzt durch:
das Ermitteln das R-Wertes von 2*y in Register Ri ,
das Kopieren des Inhalts von Ri in das Register (x)
Formal: Sei (x) = hR; ji, dann gilt:
codeiR x = e2 = codeiR e2 move Rj Ri
Aber: undefiniertes Resultat, falls x = hL; ai oder x = hG; ai.
149 / 184
Übersetzung von Zuweisungen
Zuweisungen wie x=2*y wurden bisher übersetzt durch:
das Ermitteln das R-Wertes von 2*y in Register Ri ,
das Kopieren des Inhalts von Ri in das Register (x)
Formal: Sei (x) = hR; ji, dann gilt:
codeiR x = e2 = codeiR e2 move Rj Ri
Aber: undefiniertes Resultat, falls x = hL; ai oder x = hG; ai.
Idee:
Berechne den R-Werte von e2 im Register Ri ,
berechne den L-Werte von e1 im Register Ri+1 und
schreibe e2 and Adresse e1 mit einem store Befehl.
149 / 184
Übersetzung von L-Werten
Neue Instruktion: store Ri Rj mit Semantik S[Ri ] = Rj
13 Rj
Ri
store Ri Rj
13
Definition für Anweisungen:
codei e = codeiR e Wie wird x = e (mit x = hG; ai) nun übersetzt?
150 / 184
Übersetzung von L-Werten
Neue Instruktion: store Ri Rj mit Semantik S[Ri ] = Rj
13 Rj
Ri
store Ri Rj
13
Definition für Anweisungen:
codei e = codeiR e Wie wird x = e (mit x = hG; ai) nun übersetzt?
Daher definiere für den Fall dass e1 = x und x = hR; ji nicht gilt:
codeiR e1
= e2 = codeiR e2 codei+1 e1 L
store Ri+1 Ri
150 / 184
Übersetzung von L-Werten
Neue Instruktion: store Ri Rj mit Semantik S[Ri ] = Rj
13 Rj
Ri
store Ri Rj
13
Definition für Anweisungen:
codei e = codeiR e Wie wird x = e (mit x = hG; ai) nun übersetzt?
Daher definiere für den Fall dass e1 = x und x = hR; ji nicht gilt:
codeiR e1
= e2 = codeiR e2 codei+1 e1 L
store Ri+1 Ri
Berechne den L-Wert einer Variable wie folgt:
codeiL x = loadc Ri a
150 / 184
Reservierung von Speicher für lokale Variablen
Gegeben: eine Funktion mit k lokalen int Variablen, deren Adresse
genommen wurden.
alloc k
k
pop k
alloc k
pop k
SP = SP + k;
SP = SP - k;
Der Befehl alloc k reserviert auf dem Keller Platz für die lokalen
Variablen, pop k gibt sie wieder frei.
151 / 184
Zugriff auf lokale Variablen
Zugriffe auf lokale Variablen erfolgt relativ zum aktuellen FP. Darum
modifizieren wir codeL für Variablen-Namen.
Für x = hL; ai definieren wir
codeiL x = loadrc Ri a if x = hL; ai
Der Befehl loadrc Ri k berechnet die Summe von FP und k.
f FP
f+k Ri
k
loadrc Ri k
Ri
= FP + k
152 / 184
Allgemeiner L-Wert von Variablen
Die Adresse einer Variablen wird wie folgt in Ri berechnet:
codeiL
x=
loadc Ri a
loadrc Ri a
if x = hG; ai
if x = hL; ai
153 / 184
Allgemeiner L-Wert von Variablen
Die Adresse einer Variablen wird wie folgt in Ri berechnet:
codeiL
x=
loadc Ri a
loadrc Ri a
if x = hG; ai
if x = hL; ai
Achtung: für x = hR; ji ist codeiL nicht definiert!
153 / 184
Allgemeiner L-Wert von Variablen
Die Adresse einer Variablen wird wie folgt in Ri berechnet:
codeiL
x=
loadc Ri a
loadrc Ri a
if x = hG; ai
if x = hL; ai
Achtung: für x = hR; ji ist codeiL nicht definiert!
Beobachtung:
153 / 184
Allgemeiner L-Wert von Variablen
Die Adresse einer Variablen wird wie folgt in Ri berechnet:
codeiL
x=
loadc Ri a
loadrc Ri a
if x = hG; ai
if x = hL; ai
Achtung: für x = hR; ji ist codeiL nicht definiert!
Beobachtung:
intuitiv: ein Register hat keine Adresse
153 / 184
Allgemeiner L-Wert von Variablen
Die Adresse einer Variablen wird wie folgt in Ri berechnet:
codeiL
x=
loadc Ri a
loadrc Ri a
if x = hG; ai
if x = hL; ai
Achtung: für x = hR; ji ist codeiL nicht definiert!
Beobachtung:
intuitiv: ein Register hat keine Adresse
ein Register darf während der Codegenerierung nie als L-Wert
auftreten
153 / 184
Allgemeiner L-Wert von Variablen
Die Adresse einer Variablen wird wie folgt in Ri berechnet:
codeiL
x=
loadc Ri a
loadrc Ri a
if x = hG; ai
if x = hL; ai
Achtung: für x = hR; ji ist codeiL nicht definiert!
Beobachtung:
intuitiv: ein Register hat keine Adresse
ein Register darf während der Codegenerierung nie als L-Wert
auftreten
dies erfordert eine Fallunterscheidung bei der Übersetzung von
Zuweisungen
153 / 184
Makro-Befehle zum Zugriff auf lokale Variablen
Definiere: Der Befehl load Ri Rj setzt Ri auf den Wert an Adresse Rj .
154 / 184
Makro-Befehle zum Zugriff auf lokale Variablen
Definiere: Der Befehl load Ri Rj setzt Ri auf den Wert an Adresse Rj .
Damit: loadrc Ri a; load Rj Ri : setze Rj auf x mit x = hL; ai.
154 / 184
Makro-Befehle zum Zugriff auf lokale Variablen
Definiere: Der Befehl load Ri Rj setzt Ri auf den Wert an Adresse Rj .
Damit: loadrc Ri a; load Rj Ri : setze Rj auf x mit x = hL; ai.
Allgemein: Lade Variable x in Register Ri :
codeiR x =
8
< loada Ri a
loadr R a
: move Ri R
i j
if x = hG; ai
if x = hL; ai
if x = hR; ii
154 / 184
Makro-Befehle zum Zugriff auf lokale Variablen
Definiere: Der Befehl load Ri Rj setzt Ri auf den Wert an Adresse Rj .
Damit: loadrc Ri a; load Rj Ri : setze Rj auf x mit x = hL; ai.
Allgemein: Lade Variable x in Register Ri :
codeiR x =
8
< loada Ri a
loadr R a
: move Ri R
i j
if x = hG; ai
if x = hL; ai
if x = hR; ii
Analog: Für Schreiboperationen definiere:
storer a Rj
storea a Rj
loadrc Ri a
store Ri Rj
loadc Ri a
store Ri Rj
D.h. storea a Rj ist Makro. Definiere Spezialfall (mit x = hG; ai):
codeiR x = e2 = codeiR e2 codei+1 x L
store Ri+1 Ri
154 / 184
Makro-Befehle zum Zugriff auf lokale Variablen
Definiere: Der Befehl load Ri Rj setzt Ri auf den Wert an Adresse Rj .
Damit: loadrc Ri a; load Rj Ri : setze Rj auf x mit x = hL; ai.
Allgemein: Lade Variable x in Register Ri :
codeiR x =
8
< loada Ri a
loadr R a
: move Ri R
i j
if x = hG; ai
if x = hL; ai
if x = hR; ii
Analog: Für Schreiboperationen definiere:
storer a Rj
storea a Rj
loadrc Ri a
store Ri Rj
loadc Ri a
store Ri Rj
D.h. storea a Rj ist Makro. Definiere Spezialfall (mit x = hG; ai):
codeiR x = e2 = codeiR e2 loadc Ri+1 a
store Ri+1 Ri
154 / 184
Makro-Befehle zum Zugriff auf lokale Variablen
Definiere: Der Befehl load Ri Rj setzt Ri auf den Wert an Adresse Rj .
Damit: loadrc Ri a; load Rj Ri : setze Rj auf x mit x = hL; ai.
Allgemein: Lade Variable x in Register Ri :
codeiR x =
8
< loada Ri a
loadr R a
: move Ri R
i j
if x = hG; ai
if x = hL; ai
if x = hR; ii
Analog: Für Schreiboperationen definiere:
storer a Rj
storea a Rj
loadrc Ri a
store Ri Rj
loadc Ri a
store Ri Rj
D.h. storea a Rj ist Makro. Definiere Spezialfall (mit x = hG; ai):
codeiR x = e2 = codeiR e2 storea a Ri
154 / 184
Datentransfer Instruktionen der R-CMa
Lesezugriffe und Schreibzugriffe der R-CMa :
Instruktion
load Ri Rj
loada Ri c
loadr Ri c
store Ri Rj
storea c Ri
storer c Ri
Semantik
Ri
S[Rj ]
Ri
S[c]
Ri
S[FP + c]
S[Ri ]
Rj
S[c]
Ri
S[FP + c]
Ri
Intuition
lade Wert von Adresse
lade globale Variable
lade lokale Variable
speichere Wert an Adresse
schreibe globale Variable
schreibe lokale Variable
Instruktionen zur Adressberechnung:
Instruktion
loadc Ri c
loadrc Ri c
Semantik
Intuition
Ri
c
lade Konstante
Ri
FP + c lade Konstante rel. zu FP
Instruktionen für den allgemeinen Datentransfer:
Instruktion
move Ri Rj
move Ri k Rj
Semantik
Ri
Rj
[S[SP + i] S[Rj + i]]ki=01
Ri
SP; SP
SP + k
Intuition
transferiere Wert zw. Registern
kopiere k Werte auf den Keller
155 / 184
Bestimmung der Adress-Umgebung
Variablen können verschiedenen Tag in Symboltabelle haben:
1
globale Variablen, die außerhalb von Funktionen definiert
werden;
2
lokale (automatische) Variablen, die innerhalb von Funktionen
definiert werden;
3
Register (automatische) Variablen, die innerhalb von Funktionen
definiert werden.
Beispiel:
int x, y;
void f(int v, int w) {
int a;
if (a>0) {
int b;
g(&b);
} else {
int c;
}
}
v
x
y
v
w
a
b
c
h
h
h
h
h
h
h
(v)
,
,
,
,
,
,
,
i
i
i
i
i
i
i
156 / 184
Bestimmung der Adress-Umgebung
Variablen können verschiedenen Tag in Symboltabelle haben:
1
globale Variablen, die außerhalb von Funktionen definiert
werden;
2
lokale (automatische) Variablen, die innerhalb von Funktionen
definiert werden;
3
Register (automatische) Variablen, die innerhalb von Funktionen
definiert werden.
Beispiel:
int x, y;
void f(int v, int w) {
int a;
if (a>0) {
int b;
g(&b);
} else {
int c;
}
}
v
x
y
v
w
a
b
c
(v)
hG,0i
hG,1i
h R , -1 i
h R , -2 i
hR,1i
hL ,0i
hR,2i
156 / 184
Funktionsargumente auf dem Keller
C erlaubt sogenannte variadic functions
unbekannte Anzahl an Parametern: R 1 ; R 2 ; : : :
Problem: callee kann auf Register nicht mit Index zugreifen
Beispiel:
int printf(const char * format, ...);
char *s =
"Hello %s!\nIt’s %i to %i!\n";
int main(void) {
printf(s ,"World", 5, 12);
return 0;
}
157 / 184
Funktionsargumente auf dem Keller
C erlaubt sogenannte variadic functions
unbekannte Anzahl an Parametern: R 1 ; R 2 ; : : :
Problem: callee kann auf Register nicht mit Index zugreifen
Beispiel:
int printf(const char * format, ...);
char *s =
"Hello %s!\nIt’s %i to %i!\n";
int main(void) {
printf(s ,"World", 5, 12);
return 0;
}
Idee:
schiebe variadic Parameter von rechts nach links auf Keller
Der erste Parameter liegt direkt unterhalb von PC, FP, EP
Für einen Prototypen f (1 x1 ; : : : ; k xk , ...) setzen wir:
x1
xk+1 wäre hL;
7! hR; 1i
2 jk+1 ji
7! hR; ki
xk+i wäre hL;
xk
2
jk+1 j
:::
jk+i ji
157 / 184
Funktionsargumente auf dem Keller
C erlaubt sogenannte variadic functions
unbekannte Anzahl an Parametern: R 1 ; R 2 ; : : :
Problem: callee kann auf Register nicht mit Index zugreifen
Beispiel:
int printf(const char * format, ...);
char *s =
value
"Hello %s!\nIt’s %i to %i!\n";
s
”World”
int main(void) {
5
printf(s ,"World", 5, 12);
12
return 0;
(pi )
hR;
hL;
hL;
hL;
1i
3i
4i
5i
}
Idee:
schiebe variadic Parameter von rechts nach links auf Keller
Der erste Parameter liegt direkt unterhalb von PC, FP, EP
Für einen Prototypen f (1 x1 ; : : : ; k xk , ...) setzen wir:
x1
xk+1 wäre hL;
7! hR; 1i
2 jk+1 ji
7! hR; ki
xk+i wäre hL;
xk
2
jk+1 j
:::
jk+i ji
157 / 184
Variablen im Speicher
Kapitel 2:
Felder und Zeiger
158 / 184
Felder
Beispiel: int[11] a;
Das Feld a enthält 11
Elemente und benötigt
darum 11 Zellen.
a[10]
a
ist die Adresse des
Elements a[0].
a[0]
Definiere Funktion j j um den Platzbedarf eines Typs zu berechnen:
jtj =
1
k jt 0 j
falls t ein Basistyp
falls t t0 [k]
Dann ergibt sich für die Deklaration d
= 1
xi = xi 1 + jti
x1
1
j
t1 x1 ; : : : tk xk ;
für i > 1
j j kann zur Übersetzungszeit berechnet werden, also auch .
Beachte: j j ist nötig zur Implementierung von C’s sizeof Operator
159 / 184
Übersetzung von Felderzugriffen
Erweitere codeL und codeR auf Ausdrücke mit indizierten Feldzugriffen.
Sei t[c] a; die Deklaration eines Feldes a.
160 / 184
Übersetzung von Felderzugriffen
Erweitere codeL und codeR auf Ausdrücke mit indizierten Feldzugriffen.
Sei t[c] a; die Deklaration eines Feldes a.
Um die Adresse des Elements a[i]zu bestimmen, müssen wir
a + jtj (R-Wert von i) ausrechnen. Folglich:
codeiL e2 [e1 ] = codeiR e1 codei+1 e2 R
loadc Ri+2 jtj
mul Ri+1 Ri+1 Ri+2
add Ri Ri Ri+1
160 / 184
Übersetzung von Felderzugriffen
Erweitere codeL und codeR auf Ausdrücke mit indizierten Feldzugriffen.
Sei t[c] a; die Deklaration eines Feldes a.
Um die Adresse des Elements a[i]zu bestimmen, müssen wir
a + jtj (R-Wert von i) ausrechnen. Folglich:
codeiL e2 [e1 ] = codeiR e1 codei+1 e2 R
loadc Ri+2 jtj
mul Ri+1 Ri+1 Ri+2
add Ri Ri Ri+1
Bemerkung:
In C ist ein Feld ein Zeiger. Ein deklariertes Feld a ist eine
Zeiger-Konstante, deren R-Wert die Anfangsadresse von a ist.
Formal setzen wir für ein Feld e codeiR e = codeiL e In C sind äquivalent (als L-Werte, nicht vom Typ):
2[a]
a[2]
a+2
160 / 184
Strukturen (Records)
Achtung: Komponenten-Namen von Strukturen dürfen sich
überlappen.
Hier: Komponenten-Umgebung st bezieht sich auf den gerade
übersetzten Struktur-Typ st.
Sei struct { int a; int b; } x; Teil einer Deklarationsliste.
x erhält die erste freie Zelle des Platzes für die Struktur als
Relativ-Adresse.
Für die Komponenten vergeben wir Adressen relativ zum Anfang
der Struktur, hier a 7! 0, b 7! 1.
161 / 184
Strukturen (Records)
Achtung: Komponenten-Namen von Strukturen dürfen sich
überlappen.
Hier: Komponenten-Umgebung st bezieht sich auf den gerade
übersetzten Struktur-Typ st.
Sei struct { int a; int b; } x; Teil einer Deklarationsliste.
x erhält die erste freie Zelle des Platzes für die Struktur als
Relativ-Adresse.
Für die Komponenten vergeben wir Adressen relativ zum Anfang
der Struktur, hier a 7! 0, b 7! 1.
Sei allgemein t struct { t1 v1 ;: : : ; tk vk }. Dann sei
jtj :=
k
X
=
jti j
st v1
:= 0
st vi
:= st vi 1 + jti
1
j
für i > 1
i 1
Damit erhalten wir:
codeiL (e:c) = codeiL e loadc Ri+1 (st c)
add Ri Ri Ri+1
161 / 184
Zeiger in C
Mit Zeiger (-Werten) rechnen, heißt in der Lage zu sein,
1
Zeiger zu erzeugen, d.h. Zeiger auf Speicherzellen zu setzen;
sowie
2
Zeiger zu dereferenzieren, d. h. durch Zeiger auf die Werte von
Speicherzellen zugreifen.
Erzeugen von Zeigern:
Die Anwendung des Adressoperators & liefert einen Zeiger auf
eine Variable, d. h. deren Adresse (=
b L-Wert). Deshalb:
codeiR
&e = codeiL e Beispiel:
Sei struct { int a; int b; } x; mit = fx 7! 13g und
st = fa 7! 0; b 7! 1g gegeben.
Dann ist
codeiL (x:b) = loadc Ri+1 13
loadc Ri 1
add Ri Ri Ri+1
162 / 184
Dereferenzieren von Zeigern
Die Anwendung des Operators * auf den Ausdruck e liefert den Inhalt
der Speicherzelle, deren Adresse der L-Wert von e ist:
codeiR
e =
codeiL e load Ri Ri
Beispiel: Betrachte für
struct t { int a[7]; struct t *b; };
int i,j;
struct t *pt;
den Ausdruck e ((pt -> b) -> a)[i+1]
Wegen e->a (*e).a gilt:
codeiL (e ! a) = codeiL e loadc Ri+1 ( a)
add Ri Ri Ri+1
163 / 184
Übersetzung von Dereferenzierungen (I)
Sei = fi
7! 1; j 7! 2; pt 7! 3; a 7! 0; b 7! 7 g.
b:
struct t { int a[7]; struct t *b; };
int i,j;
struct t *pt;
Übersetze e ((pt -> b) -> a)[i+1]
b:
pt:
j:
i:
Dann ist:
codeiL e =
codeiL ((pt ! b) ! a) codeRi+1 (i + 1) loadc Ri+2 1
mul Ri+1 Ri+1 Ri+2
add Ri Ri Ri+1
a:
a:
=
codeiL ((pt ! b) ! a) loada Ri+1 1
loadc Ri+2 1
add Ri+1 Ri+1 Ri+2
loadc Ri+2 1
mul Ri+1 Ri+1 Ri+2
add Ri Ri Ri+1
164 / 184
Übersetzung von Dereferenzierungen (II)
Für dereferenzierte Felder (*e).a ist der R-Wert gleich der
Dereferenzierung des L-Wert von e plus Offset von a. Deshalb
erhalten wir:
codeiL
((pt ! b) ! a) = codeiL (pt ! b) loadc Ri+1 0
add Ri Ri Ri+1
=
Damit ergibt sich insgesamt die Folge:
loada Ri 3
loadc Ri+1 7
add Ri Ri Ri+1
load Ri Ri
loadc Ri+1 0
add Ri Ri Ri+1
loada Ri+1 1
loadc Ri+2 1
add Ri+1 Ri+1 Ri+2
loada Ri 3
loadc Ri+1 7
add Ri Ri Ri+1
load Ri Ri
loadc Ri+1 0
add Ri Ri Ri+1
loadc Ri+2 1
mul Ri+1 Ri+1 Ri+2
add Ri Ri Ri+1
165 / 184
Berechnung der R-Werte von Funktionen
Ähnlich deklarierten Feldern, werden Funktions-Namen als
konstante Zeiger auf Funktionen aufgefasst. Dabei ist der R-Wert
dieses Zeigers gleich der Anfangs-Adresse der Funktion.
Achtung! Für eine Variable int (*)() g sind die beiden
Aufrufe
(*g)()
und
g()
äquivalent. Die Dereferenzierungen eines Funktions-Zeigers ist
unnötig und wird ignoriert.
Folglich:
codeR f codeR (e) = loadc ( f )
= codeR e f ein Funktions-Name
e ein Funktions-Zeiger
166 / 184
Übergeben von Zusammengesetzten Parametern
Betrachte folgenden Deklarationen:
typedef struct { int x, y; } point_t;
int distToOrigin(point_t);
; Wie übergibt man einen nicht-Basistypen als Parameter?
167 / 184
Übergeben von Zusammengesetzten Parametern
Betrachte folgenden Deklarationen:
typedef struct { int x, y; } point_t;
int distToOrigin(point_t);
; Wie übergibt man einen nicht-Basistypen als Parameter?
Idee: caller übergibt Zeiger auf Struktur
167 / 184
Übergeben von Zusammengesetzten Parametern
Betrachte folgenden Deklarationen:
typedef struct { int x, y; } point_t;
int distToOrigin(point_t);
; Wie übergibt man einen nicht-Basistypen als Parameter?
Idee: caller übergibt Zeiger auf Struktur
Problem: callee könnte übergebenen Parameter ändern
167 / 184
Übergeben von Zusammengesetzten Parametern
Betrachte folgenden Deklarationen:
typedef struct { int x, y; } point_t;
int distToOrigin(point_t);
; Wie übergibt man einen nicht-Basistypen als Parameter?
Idee: caller übergibt Zeiger auf Struktur
Problem: callee könnte übergebenen Parameter ändern
Lösung: caller legt eine Kopie der Struktur an
167 / 184
Übergeben von Zusammengesetzten Parametern
Betrachte folgenden Deklarationen:
typedef struct { int x, y; } point_t;
int distToOrigin(point_t);
; Wie übergibt man einen nicht-Basistypen als Parameter?
Idee: caller übergibt Zeiger auf Struktur
Problem: callee könnte übergebenen Parameter ändern
Lösung: caller legt eine Kopie der Struktur an
codeiR e = codeLi+1 e move Ri k Ri+1
e eine Struktur der Größe k
167 / 184
Übergeben von Zusammengesetzten Parametern
Betrachte folgenden Deklarationen:
typedef struct { int x, y; } point_t;
int distToOrigin(point_t);
; Wie übergibt man einen nicht-Basistypen als Parameter?
Idee: caller übergibt Zeiger auf Struktur
Problem: callee könnte übergebenen Parameter ändern
Lösung: caller legt eine Kopie der Struktur an
codeiR e = codeLi+1 e move Ri k Ri+1
e eine Struktur der Größe k
Neuer Befehl: move
167 / 184
Kopien von Speicherbereichen
Die move Instruktion kopiert k Elemente auf den Stack.
k
Rj
Ri
move Ri k Rj
for (i = k-1; i0; i--)
S[SP+i] = S[Rj +i];
Ri = SP;
SP = SP+k;
168 / 184
Variablen im Speicher
Kapitel 3:
Die Halde
169 / 184
Der Heap
Zeiger gestatten auch den Zugriff auf anonyme, dynamisch erzeugte
Datenelemente, deren Lebenszeit nicht dem LIFO-Prinzip
unterworfen ist.
Wir benötigen einen potentiell beliebig großen Speicherbereich H:
den Heap (Halde). Implementierung:
;
S
H
0
MAX
SP
NP
EP
EP
NP
=
b New Pointer; zeigt auf unterste belegte Haldenzelle.
=
b Extreme Pointer; zeigt auf die Zelle, auf die der SP maximal zeigen kann (innerhalb der aktuellen Funktion).
170 / 184
Invarianten des Heaps und des Stacks
Stack und Heap dürfen sich nicht überschneiden
171 / 184
Invarianten des Heaps und des Stacks
Stack und Heap dürfen sich nicht überschneiden
eine Überschneidung kann bei jeder Erhöhung von SP eintreten
(Stack Overflow)
oder bei einer Erniedrigung des NP eintreten (Out Of Memory)
171 / 184
Invarianten des Heaps und des Stacks
Stack und Heap dürfen sich nicht überschneiden
eine Überschneidung kann bei jeder Erhöhung von SP eintreten
(Stack Overflow)
oder bei einer Erniedrigung des NP eintreten (Out Of Memory)
im Gegensatz zum Stack Overflow kann ein Out Of Memory Fehler
vom Programmierer abgefangen werden
malloc liefert in diesem Fall NULL zurück, dass als (void*) 0
definiert ist
171 / 184
Invarianten des Heaps und des Stacks
Stack und Heap dürfen sich nicht überschneiden
eine Überschneidung kann bei jeder Erhöhung von SP eintreten
(Stack Overflow)
oder bei einer Erniedrigung des NP eintreten (Out Of Memory)
im Gegensatz zum Stack Overflow kann ein Out Of Memory Fehler
vom Programmierer abgefangen werden
malloc liefert in diesem Fall NULL zurück, dass als (void*) 0
definiert ist
EP reduziert die nötigen Überprüfungen auf Überschneidung auf
den Funktionseintritt
die Überprüfungen bei Heap-Allokationen bleiben erhalten
171 / 184
Reservierung von Speicher auf dem Stack
Der Befehl enter q setzt den EP auf den neuen Wert. Steht nicht
mehr genügend Platz zur Verfügung, wird die Programm-Ausführung
abgebrochen.
EP
q
enter q
EP = SP + q;
if (EP NP)
Error (“Stack Overflow”);
172 / 184
Dynamisch Allokierter Speicher
Es gibt eine weitere Arte, Zeiger zu erzeugen:
ein Aufruf von malloc liefert einen Zeiger auf eine Heap-Zelle:
codeiR malloc (e) = codeiR e new Ri
NP
NP
n
n
Ri
new Ri
Ri
if (NP - R[i] <= EP) R[i] = NULL; else {
NP = NP - R[i];
R[i] = NP;
}
173 / 184
Freigabe von Speicherplatz
Ein mit malloc allokierter Bereich muss irgendwann mit free
wieder freigegeben werden.
Probleme:
Der freigegebene Speicherbereich könnte noch von anderen
Zeigern referenziert (dangling references).
Nach einiger Freigabe könnte der Speicher etwa so aussehen
(fragmentation):
frei
174 / 184
Mögliche Implementierungen:
1
Nimm an, der Programmierer weiß, was er tut. Verwalte dann die
freien Abschnitte (etwa sortiert nach Größe) in einer speziellen
Datenstruktur;
malloc wird teuer
;
2
Tue nichts, d.h.:
codei free(e) = codeiR e ; einfach und effizient, aber nicht für reaktive Progaramme
3
Benutze eine automatische, evtl. “konservative”
Garbage-Collection, die gelegentlich sicher nicht mehr
benötigten Heap-Platz einsammelt und dann malloc zur
Verfügung stellt.
175 / 184
Variablen im Speicher
Kapitel 4:
Erweiterte Übersetzung von Funktionen
176 / 184
Übersetzung von Programmen
Vor der Programmausführung gilt:
SP =
1
FP = EP = 0
PC = 0
NP = MAX
Sei p V_defs F_def1 : : : F_defn , ein Programm, wobei F_defi eine
Funktion fi definiert, von denen eine main heißt.
Der Code für das Programm p enthält:
Code für die Funktions-Definitionen F_defi ;
Code zum Anlegen der globalen Variablen;
Code für den Aufruf von main()
die Instruktion halt.
177 / 184
Instruktionen für den Programmstart
Ein Programm P = F1 ; : : : Fn muss eine main Funktion enthalten.
code1 P =
enter (k + 3)
alloc k
loadc R1 _main
saveloc R1 R0
mark
call R1
restoreloc R1 R0
halt
codei F1 f1
..
.
_f1
:
_fn
: codei Fn fn
178 / 184
Instruktionen für den Programmstart
Ein Programm P = F1 ; : : : Fn muss eine main Funktion enthalten.
code1 P =
enter (k + 3)
alloc k
loadc R1 _main
saveloc R1 R0
mark
call R1
restoreloc R1 R0
halt
codei F1 f1
..
.
_f1
:
_fn
: codei Fn fn
Diskussion:
k sind die Anzahl der Stackplätze für globale Variablen
saveloc R1 R0 hat keinen Effekt (d.h. es rettet kein Register)
enthält die Adressen der Funktionen und globalen Variablen
178 / 184
Übersetzung von Funktionen
Die Übersetzung einer Funktion erweitern wir folgt:
code1 tr f(args)fdecls ssg = enter q
alloc k
move Rl+1 R
..
.
1
move Rl+n R n
codel+n+1 ss 0
return
Randbedinungen:
179 / 184
Übersetzung von Funktionen
Die Übersetzung einer Funktion erweitern wir folgt:
code1 tr f(args)fdecls ssg = enter q
alloc k
move Rl+1 R
..
.
1
move Rl+n R n
codel+n+1 ss 0
return
Randbedinungen:
enter stellt sicher, dass genügend Kellerspeicher zur Verfüngung
steht (q: Anzahl der maximal benötigten Kellerzellen)
179 / 184
Übersetzung von Funktionen
Die Übersetzung einer Funktion erweitern wir folgt:
code1 tr f(args)fdecls ssg = enter q
alloc k
move Rl+1 R
..
.
1
move Rl+n R n
codel+n+1 ss 0
return
Randbedinungen:
enter stellt sicher, dass genügend Kellerspeicher zur Verfüngung
steht (q: Anzahl der maximal benötigten Kellerzellen)
alloc reserviert Platz für Variablen auf dem Keller (k < q)
179 / 184
Übersetzung von Funktionen
Die Übersetzung einer Funktion erweitern wir folgt:
code1 tr f(args)fdecls ssg = enter q
alloc k
move Rl+1 R
..
.
1
move Rl+n R n
codel+n+1 ss 0
return
Randbedinungen:
enter stellt sicher, dass genügend Kellerspeicher zur Verfüngung
steht (q: Anzahl der maximal benötigten Kellerzellen)
alloc reserviert Platz für Variablen auf dem Keller (k < q)
Können lokale Felder in f deklariert werden?
179 / 184
Übersetzung von Funktionsaufrufen
Der Funktionsaufruf g(e1 ; : : : en ) wird nun wie folgt übersetzt:
codeiR g(e1 ; : : : en ) = codeiR g codeiR+1 e1 ..
.
codeiR+n en move R
..
.
move R
1
Ri+1
n
Ri+n
saveloc R1 Ri
1
mark
call Ri
restoreloc R1 Ri
1
pop k
move Ri R0
180 / 184
Übersetzung von Funktionsaufrufen
Der Funktionsaufruf g(e1 ; : : : en ) wird nun wie folgt übersetzt:
codeiR g(e1 ; : : : en ) = codeiR g codeiR+1 e1 ..
.
codeiR+n en move R
..
.
move R
1
Ri+1
n
Ri+n
saveloc R1 Ri
1
mark
call Ri
restoreloc R1 Ri
1
pop k
move Ri R0
Unterschied zu vorher:
wir gehen davon aus, dass g nur n Argumente hat, d.h., dass es
nicht variadic ist
neu: pop entfernt mögliche Kellerzellen, die durch codeiR+j ej erzeugt wurden
180 / 184
Peephole Optimization
Die Erzeugten Instruktionen enthalten viele redundante Sequenzen,
z.B.:
move R7 R7
pop 0
move R5 R7
mul R4 R4 R7
Peephole Optimierung sucht nach solchen Mustern und ersetzt sie
durch einfachere Instruktionen (bzw. gar keine).
181 / 184
Realistische Register Maschinen
Die R-CMa ist geeignet, auf einfache Weise Code für eine
Registermaschine zu erzeugen.
182 / 184
Realistische Register Maschinen
Die R-CMa ist geeignet, auf einfache Weise Code für eine
Registermaschine zu erzeugen.
reale Maschinen haben nur eine fixe Anzahl an Registern
182 / 184
Realistische Register Maschinen
Die R-CMa ist geeignet, auf einfache Weise Code für eine
Registermaschine zu erzeugen.
reale Maschinen haben nur eine fixe Anzahl an Registern
die unendliche Menge an virtuellen Registern muss auf eine
endliche Menge realer Register abgebildet werden
182 / 184
Realistische Register Maschinen
Die R-CMa ist geeignet, auf einfache Weise Code für eine
Registermaschine zu erzeugen.
reale Maschinen haben nur eine fixe Anzahl an Registern
die unendliche Menge an virtuellen Registern muss auf eine
endliche Menge realer Register abgebildet werden
Idee: Benutze ein Register Ri , das zur Zeit nicht genutzt wird, für
den Wert eines anderen Registers Rj
182 / 184
Realistische Register Maschinen
Die R-CMa ist geeignet, auf einfache Weise Code für eine
Registermaschine zu erzeugen.
reale Maschinen haben nur eine fixe Anzahl an Registern
die unendliche Menge an virtuellen Registern muss auf eine
endliche Menge realer Register abgebildet werden
Idee: Benutze ein Register Ri , das zur Zeit nicht genutzt wird, für
den Wert eines anderen Registers Rj
falls das Programm mehr als die verfügbaren Register braucht,
allokiere einige Variablen auf dem Stack und nicht mehr in
Registern
182 / 184
Realistische Register Maschinen
Die R-CMa ist geeignet, auf einfache Weise Code für eine
Registermaschine zu erzeugen.
reale Maschinen haben nur eine fixe Anzahl an Registern
die unendliche Menge an virtuellen Registern muss auf eine
endliche Menge realer Register abgebildet werden
Idee: Benutze ein Register Ri , das zur Zeit nicht genutzt wird, für
den Wert eines anderen Registers Rj
falls das Programm mehr als die verfügbaren Register braucht,
allokiere einige Variablen auf dem Stack und nicht mehr in
Registern
Wir benötigen eine Lösung für die folgenden Probleme:
182 / 184
Realistische Register Maschinen
Die R-CMa ist geeignet, auf einfache Weise Code für eine
Registermaschine zu erzeugen.
reale Maschinen haben nur eine fixe Anzahl an Registern
die unendliche Menge an virtuellen Registern muss auf eine
endliche Menge realer Register abgebildet werden
Idee: Benutze ein Register Ri , das zur Zeit nicht genutzt wird, für
den Wert eines anderen Registers Rj
falls das Programm mehr als die verfügbaren Register braucht,
allokiere einige Variablen auf dem Stack und nicht mehr in
Registern
Wir benötigen eine Lösung für die folgenden Probleme:
bestimme, wann ein Register nicht benutzt ist
182 / 184
Realistische Register Maschinen
Die R-CMa ist geeignet, auf einfache Weise Code für eine
Registermaschine zu erzeugen.
reale Maschinen haben nur eine fixe Anzahl an Registern
die unendliche Menge an virtuellen Registern muss auf eine
endliche Menge realer Register abgebildet werden
Idee: Benutze ein Register Ri , das zur Zeit nicht genutzt wird, für
den Wert eines anderen Registers Rj
falls das Programm mehr als die verfügbaren Register braucht,
allokiere einige Variablen auf dem Stack und nicht mehr in
Registern
Wir benötigen eine Lösung für die folgenden Probleme:
bestimme, wann ein Register nicht benutzt ist
weise mehreren virtuellen Registern das gleiche reale Register
zu, wenn sie nicht gleichzeitig benutzt werden
182 / 184
Realistische Register Maschinen
Die R-CMa ist geeignet, auf einfache Weise Code für eine
Registermaschine zu erzeugen.
reale Maschinen haben nur eine fixe Anzahl an Registern
die unendliche Menge an virtuellen Registern muss auf eine
endliche Menge realer Register abgebildet werden
Idee: Benutze ein Register Ri , das zur Zeit nicht genutzt wird, für
den Wert eines anderen Registers Rj
falls das Programm mehr als die verfügbaren Register braucht,
allokiere einige Variablen auf dem Stack und nicht mehr in
Registern
Wir benötigen eine Lösung für die folgenden Probleme:
bestimme, wann ein Register nicht benutzt ist
weise mehreren virtuellen Registern das gleiche reale Register
zu, wenn sie nicht gleichzeitig benutzt werden
Diese Probleme sind Thema in Programmoptimierung.
182 / 184
Registerfärbung in der Fakultätsfunktion
Betrachte: def-use
int fac(int x) {
if (x<=0) then
return 1;
else
return x*fac(x-1);
}
_A:
1 0 1 2 3 4
_fac:
enter 5
move R1 R 1
move R2 R1
loadc R3 0
leq R2 R2 R3
jumpz R2 _A
loadc R2 1
move R0 R2
return
jump _B
U
D
U D
U
D
D
U
U
D
U
D
U
_B:
move R2 R1
move R3 R1
loadc R4 1
sub R3 R3 R4
move R 1 R3
loadc R3 _fac
saveloc R1 R2
mark
call R3
restoreloc R1 R2
move R3 R0
mul R2 R2 R3
move R0 R2
return
return
1 0 1 2 3 4
U D
U
D
D
U
U
D
D
U
D
U U
U
D
U
D D
U
U
D
D
U
D
U
U
183 / 184
Registerfärbung in der Fakultätsfunktion
Betrachte: def-use liveness
int fac(int x) {
if (x<=0) then
return 1;
else
return x*fac(x-1);
}
1 0 1 2 3 4
_A:
1 0 1 2 3 4
_fac:
enter 5
move R1 R 1
move R2 R1
loadc R3 0
leq R2 R2 R3
jumpz R2 _A
loadc R2 1
move R0 R2
return
jump _B
>?
>|
|
|
|
|
|
|
|
>?
>|
?>| >?
?
>?
_B:
move R2 R1
move R3 R1
loadc R4 1
sub R3 R3 R4
move R 1 R3
loadc R3 _fac
saveloc R1 R2
mark
call R3
restoreloc R1 R2
move R3 R0
mul R2 R2 R3
move R0 R2
return
return
>|
|
|
|
|
|
? >
|
?|
>
?
|
|
|
|
|
|
|
|
|
|
|
|
>|
>|
?>| >?
?
? ? >||
|
|
|
|
|
|
|
|
|
|
?|
>|
?>| ?>
?
183 / 184
Registerfärbung in der Fakultätsfunktion
Betrachte: def-use liveness coloring
int fac(int x) {
if (x<=0) then
return 1;
else
return x*fac(x-1);
}
1 0 1 2 3 4
_A:
1 0 1 2 3 4
_fac:
enter 5
move R1 R 1
move R0 R1
loadc R 1 0
leq R0 R0 R 1
jumpz R2 _A
loadc R2 1
move R0 R2
return
jump _B
>?
>|
>
? ?>|
?
?>>
?
>|
|
|
|
|
|
|
|
_B:
move R2 R1
move R0 R1
loadc R 1 1
sub R0 R0 R 1
move R 1 R0
loadc R3 _fac
saveloc R1 R2
mark
call R3
restoreloc R1 R2
move R0 R0
mul R2 R2 R0
move R0 R2
return
return
>|
|
>|
|
|
>
? ?> |||
|
|
>| ?
|
| >
|
| | ? ?
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
?| ?>|
|
?>|
?
>
?
>|
?>|
?
183 / 184
Ausblick
Registerverteilung hat weitere Aufgaben:
unnötige move Instruktionen müssen vermieden werden
184 / 184
Ausblick
Registerverteilung hat weitere Aufgaben:
unnötige move Instruktionen müssen vermieden werden
Variablen müssen auf den Stack ausgelagert werden
; evtl. benötigt dies wiederum Register
184 / 184
Ausblick
Registerverteilung hat weitere Aufgaben:
unnötige move Instruktionen müssen vermieden werden
Variablen müssen auf den Stack ausgelagert werden
; evtl. benötigt dies wiederum Register
übersetze Funktionen in eine single static assignment Form
184 / 184
Ausblick
Registerverteilung hat weitere Aufgaben:
unnötige move Instruktionen müssen vermieden werden
Variablen müssen auf den Stack ausgelagert werden
; evtl. benötigt dies wiederum Register
übersetze Funktionen in eine single static assignment Form
optimale Färbung möglich (allerdings müssen evtl. Register
getauscht werden)
184 / 184
Ausblick
Registerverteilung hat weitere Aufgaben:
unnötige move Instruktionen müssen vermieden werden
Variablen müssen auf den Stack ausgelagert werden
; evtl. benötigt dies wiederum Register
übersetze Funktionen in eine single static assignment Form
optimale Färbung möglich (allerdings müssen evtl. Register
getauscht werden)
; Vorlesung Programmoptimierung
184 / 184
Ausblick
Registerverteilung hat weitere Aufgaben:
unnötige move Instruktionen müssen vermieden werden
Variablen müssen auf den Stack ausgelagert werden
; evtl. benötigt dies wiederum Register
übersetze Funktionen in eine single static assignment Form
optimale Färbung möglich (allerdings müssen evtl. Register
getauscht werden)
; Vorlesung Programmoptimierung
Schematisch präsentierte liveness-Analyse verbesserungsfähig:
nach x
y + 1 ist x nur lebendig wenn y lebendig ist
184 / 184
Ausblick
Registerverteilung hat weitere Aufgaben:
unnötige move Instruktionen müssen vermieden werden
Variablen müssen auf den Stack ausgelagert werden
; evtl. benötigt dies wiederum Register
übersetze Funktionen in eine single static assignment Form
optimale Färbung möglich (allerdings müssen evtl. Register
getauscht werden)
; Vorlesung Programmoptimierung
Schematisch präsentierte liveness-Analyse verbesserungsfähig:
nach x
y + 1 ist x nur lebendig wenn y lebendig ist
saveloc hält Register unnötig am Leben
; Zwischensprache
184 / 184
Ausblick
Registerverteilung hat weitere Aufgaben:
unnötige move Instruktionen müssen vermieden werden
Variablen müssen auf den Stack ausgelagert werden
; evtl. benötigt dies wiederum Register
übersetze Funktionen in eine single static assignment Form
optimale Färbung möglich (allerdings müssen evtl. Register
getauscht werden)
; Vorlesung Programmoptimierung
Schematisch präsentierte liveness-Analyse verbesserungsfähig:
nach x
y + 1 ist x nur lebendig wenn y lebendig ist
saveloc hält Register unnötig am Leben
; Zwischensprache
gibt es optimale Regeln für die liveness-Analyse?
184 / 184
Ausblick
Registerverteilung hat weitere Aufgaben:
unnötige move Instruktionen müssen vermieden werden
Variablen müssen auf den Stack ausgelagert werden
; evtl. benötigt dies wiederum Register
übersetze Funktionen in eine single static assignment Form
optimale Färbung möglich (allerdings müssen evtl. Register
getauscht werden)
; Vorlesung Programmoptimierung
Schematisch präsentierte liveness-Analyse verbesserungsfähig:
nach x
y + 1 ist x nur lebendig wenn y lebendig ist
saveloc hält Register unnötig am Leben
; Zwischensprache
gibt es optimale Regeln für die liveness-Analyse?
; Vorlesung Programmoptimierung
184 / 184
Ausblick
Registerverteilung hat weitere Aufgaben:
unnötige move Instruktionen müssen vermieden werden
Variablen müssen auf den Stack ausgelagert werden
; evtl. benötigt dies wiederum Register
übersetze Funktionen in eine single static assignment Form
optimale Färbung möglich (allerdings müssen evtl. Register
getauscht werden)
; Vorlesung Programmoptimierung
Schematisch präsentierte liveness-Analyse verbesserungsfähig:
nach x
y + 1 ist x nur lebendig wenn y lebendig ist
saveloc hält Register unnötig am Leben
; Zwischensprache
gibt es optimale Regeln für die liveness-Analyse?
; Vorlesung Programmoptimierung
Wie berechnet man die liveness Mengen, Registerverteilung zügig?
184 / 184
Ausblick
Registerverteilung hat weitere Aufgaben:
unnötige move Instruktionen müssen vermieden werden
Variablen müssen auf den Stack ausgelagert werden
; evtl. benötigt dies wiederum Register
übersetze Funktionen in eine single static assignment Form
optimale Färbung möglich (allerdings müssen evtl. Register
getauscht werden)
; Vorlesung Programmoptimierung
Schematisch präsentierte liveness-Analyse verbesserungsfähig:
nach x
y + 1 ist x nur lebendig wenn y lebendig ist
saveloc hält Register unnötig am Leben
; Zwischensprache
gibt es optimale Regeln für die liveness-Analyse?
; Vorlesung Programmoptimierung
Wie berechnet man die liveness Mengen, Registerverteilung zügig?
; Vorlesung Programmoptimierung
184 / 184
Herunterladen