7 Einführung in den Compilerbau

Werbung
7 Einführung in den Compilerbau
7. 1
7. 2
Allgemeines
Übersetzung
7.1 Allgemeines
7.1.1 Definitionen
Wichtige Begriffe bei Programmiersprachen:
(1)
Menge der erlaubten Zeichen
Groß-, Kleinbuchstaben, Sonderzeichen (von der Programmiersprache abhängig)
FORTRAN: nur Großbuchstaben
Pascal, C: Sonderzeichen
(2)
Menge der gültigen Programme
Ist definiert durch grammatikalische Regeln
Lexikalische Struktur: reservierte Worte (Symbole), Regeln für Bezeichner, etc.
Syntaktische Struktur: Aufbau eines Programms
(3)
Bedeutung der gültigen Programme
Möglichkeiten:
− Bedeutung = Ausgabe des Compilers nach Anwendung auf ein Programm
− Abstrakte Maschine definieren;
Bedeutung = Wirkung des Programms auf der Abstrakten Maschine
− Bedeutung ist definiert durch die Bedeutung eines äquivalenten Programms im Funktionenkalkül
aquivalent ... beide Programm definieren dieselbe Funktion
Grundbegriffe bei Programmiersprachen und deren Übersetzung
Quellenalphabet Σ
wird bei der Beschreibung des Programms benutzt
Quellenprogramm: ist ein Wort über Σ, w ∈ Σ*
Zielalphabet ∆
Zeichenvorrat der Zielsprache
druckbare, nicht druckbare Zeichen
Beispiel Drei-Adress-Code: druckbare Zeichen
Beispiel Assembler: druckbare Zeichen
Beispiel Maschinensprache: Folgen von Bytes
Zielprogramm: Wort y ∈ ∆*
Übersetzung: Abbildung von Σ* nach ∆*
w → y bzw. (w , y) heißt Übersetzung
7.1.2 Überblick über die Übersetzung
Übersetzungen Σ* → ∆* werden meist als Komposition einer Reihe einfacherer Abbildungen realisiert (Phasen
des Compilers)
Syntaktische Abbildung umfaßt zwei Teile:
o Lexikalische Analyse
Eingabe: Programm (w ∈ Σ*)
Ausgabe: Folge von Token
... dient als Eingabe für die syntaktische Analyse
o Syntaktische Analyse
Eingabe: Tokenfolge
überprüft die formale Korrektheit der Tokenfolge
Z.B. korrekter Aufbau eines arithmetischen Ausdrucks
Ausgabe: Syntaxbaum
Dient als Eingabe für die semantische Abbildung
© 2003 K. Ecker
Angewandte und praktische Informatik, Kap. 7: Einführung in den Compilerbau
7- 1
Semantische Abbildung umfasst i.a. folgende Teile:
o Semantische Analyse
überprüft die inhaltliche Korrektheit des Programms
korrekte Typ-Verwendung; Ausgabe: sog. Operatorbaum
o Code- oder Zwischencodeerzeugung
Erzeugt aus dem Operatorbaum einen Zwischencode (3-Adress-Code)
Grund: Unabhängigkeit von der Zielmaschine
o Code-Optimierung
Entfernen überflüssiger Anweisungen wie unnötige Zugriffe auf den Arbeitsspeicher
o Objektcode-Erzeugung
Übersetzung des Zwischencodes in die Sprache der Zielmaschine (ausführbarer Code)
Gründe für die Phaseneinteilung:
− Unabhängigkeit zwischen "sinnvoll definierbaren" Einheiten;
− Maschinen- und Betriebssystemunabhängigkeit so weit wie möglich
7.1.3 Begleitendes Beispiel
Im folgenden wird ein Überblick in die Compilerphasen gegeben. Als Beispiel wird folgender arithmetischer
Ausdruck betrachtet:
kapital := kapital * (100 - zinssatz) / 100;
Dieses Beispiel dient bei der Erklärung des Übersetzungsprozesses zur Demonstration.
7.2 Übersetzung
7.2.1 Lexikalische Analyse
Eingabe: Folge von Zeichen aus einem Alphabet Σ
z.B. PL/I:
Σ* = {A B ... Z $ @ # 0 ... 9 _ <blank> 0 + - * / ( ) , . ; : ' & | Ø > < ? %}
(60 Zeichen)
Lexikalische Einheiten (Lexeme): sind gewisse Kombinationen von Zeichen
z. B.:
−
−
−
−
−
Folge von mehreren Blanks ist gleichbedeutend mit 1 (oder 0) blank
Schlüsselworte (BEGIN, END, DO, GOTO, INTEGER, ...)
numerische Konstante
Namen für Variable, Funktionen, Prozeduren, Marken, etc.
Kommentare (werden überlesen)
Aufgabe der lexikalischen Analyse ist zusätzlich die Bestimmung der Lexeme
Mit jeder lexikalischen Einheit wird ein Token assoziiert
Struktur des Tokens:
tokentype (synaktische Information)
data (semantische Information)
tokentype: klassifiziert in "const", "identifier", "operation", "key word", etc.
data: Zeiger in die Symboltabelle
Symboltabelle: enthält für jedes Token weitere Information ("Attribute"), z.B. Variablennamen, Konstantenwerte
Erstellung meist in einem eigenen Schritt "Buchhaltung"
© 2003 K. Ecker
Angewandte und praktische Informatik, Kap. 7: Einführung in den Compilerbau
7- 2
Beispiel
kapital := kapital * (100 - zinssatz) / 100;
Symboltabelle
Lexem
(tokentype, data)
token_ptr
information
:=
(ass_op, nil)
1
1
−
(
(brack_op, nil)
2
4
−
-
(sub_op, nil)
3
6
−
*
(mul_op, nil)
4
8
−
/
(div_op, nil)
5
10
−
)
(brack_cl, nil)
6
12
−
kapital
(id, 7)
7
14
Real-Variable
100
(const, 8)
8
22
(id, 9)
9
26
…
…
zinssatz
Real Konstante (oder Integer Konstante)
Real-Variable
Lexikalische Einheiten können eine sehr lange oder sehr kurze Zeichenfolgen sein; daher: in einem linearen
Zeichenarray ablegen:
1
4
:=
(
6
8
-
*
10
12
/
)
Wortsymbole (vordefiniert)
14
k a p i t a l
22
26
1 0 0
35
z i n s s a t z
...
Benutzerdefinierte Lexeme ...
(
Trennzeichen,
∉ Σ)
Symboltabelle selbst wird häufig mit Hasching gespeichert.
Zum eingegebenen Programm ermittelte Tokenfolge
(id, 7) (ass_op, nil) (id, 7) (mul_op, nil) (brack_op, nil) (const, 8) (sub_op, nil)
(id, 9) (brack_cl, nil) (div_op, nil) (const, 8)
Bestimmung der Lexeme
Alphabet Σ;
−
endlicher Automat akzeptiert nur gültige Lexeme aus Σ*
−
reguläre Ausdrücke
Definition regulärer Ausdruck
∅
ist regulärer Ausdruck
ε
ist regulärer Ausdruck
a∈Σ
ist regulärer Ausdruck
Sind R, S regulärer Ausdrücke, dann auch (R), R+S, R⋅S, R*
Interpretation regulärer Ausdrücke: als Sprachen über Σ
Dazu wird eine Interpretations-Abbildung λ definiert:
λ(∅)
leere Wortmenge
λ(ε) := {ε}
Menge die nur das leere Wort enthält
λ(a) := {a}
λ((R)) := λ(R)
λ(R+S ) := λ(R) ∪ λ(S )
Vere inigung der Wortmengen
λ(R⋅S) := λ(R) λ(S)
Konkatenation der Wortmengen
λ(R*) := λ(∅) ∪ λ(R) ∪ λ(R)λ(R) ∪ λ(R)λ(R)λ(R) ∪ ...
© 2003 K. Ecker
abgeschlossene Hülle
Angewandte und praktische Informatik, Kap. 7: Einführung in den Compilerbau
7- 3
Lexemstrukturen werden mittels regulärer Ausdrücke definiert
Beispiel:
Z = reg. Ausdruck für {0,1,... ,9}
B = reg. Ausdruck für {A,B,... ,Z,a,b,... ,z}
VZ = reg. Ausdruck für {+, −}
<dez_pkt> ... reg. Ausdruck für das Zeichen "Dezimalpunkt"
Bezeichner:
B⋅(B+Z)*
Real-Konstante:
(VZ+ε)⋅Z*⋅(<dez_pkt>⋅Z+ε)⋅Z*
Gängigere Darstellungen sind Syntaxdiagramme und (äquivalent) die Baccus-Naur Form.
Realisierung der lexikalischen Analyse erfolgt z.B. mit endlichem Automaten
Beispiel Endlicher Automat zur Erkennung einer Real-Konstante (als Übung):
Eingabe:
Textdatei (File of Character), der ein Pascal-Programm enthält
Ausgabe:
Folge aller Tokens, die zu Bezeichnern, Integerkonstanten und Realkonstanten
gehören
Probleme bei der lexikalischen Analyse
Look ahead Problem:
Bedeutung eines Lexems (d.h. dessen Tokentype) wird oft erst im späteren Verlauf der lexikalischen Analyse
klar.
Beipiele
PASCAL:
FORTRAN:
7.2.2
Aufzählungen in Variablendeklarationen
DO 10 I = 1,15
DO 10 I = 1.15
Syntaxanalyse
Chomsky Grammatik vom Typ-2: G = (N, V, R, S)
mit: N = endl. Menge von Nichtterminalzeichen
T = endl. Menge von Terminalzeichen
R ⊂ N × (N + T)*
endl. Menge von Regeln
Schreibweise für Regeln: A → w ( A ∈ N, w ∈ (N + T)* )
S∈ N
Ableitung:
Startsymbol
Folge S (= a 0) ⇒ a 1 ⇒ a 2 ⇒ .... ⇒ a k
mit a 1, a 2, ..., a k−1 ∈ (N + T)* und a k ∈ T*
Ableitungsschritt a i → a i+1:
a i enthält ein Nichtterminalzeichen A, es gibt eine Regel (A → wk), und a i+1 wird aus a i durch Ersetzen von
A durch wk erhalten
Ableitungsbaum zu einer Ableitung S (= a 0) ⇒ a 1 ⇒ a 2 ⇒ .... ⇒ a k:
−
Wurzel ist mit S markiert
−
Wenn a 1 = X 1...X r:
Kante von S nach r Knoten, die (von links nach rechts) mit X 1, ..., X r markiert sind
−
Wenn a i = αAβ ⇒ a i+1 = αY 1...Y pβ (d.h. die Regel A → Y 1...Y p wurde angewandt) ...
je eine Kante vom Knoten A nach p Knoten, die (von links nach rechts) mit Y 1, ..., Y p markiert sind
Zweck der Syntaxanalyse
Bestimmung einer Ableitung zu einem gegebenen Programm
Dient zur Prüfung der formalen Korrektheit eines Programms (grammatikalische Regeln)
Syntaktische Abbildung: zu einer gegebenen Tokenfolge (Ausgabe des Lexikalischen Analyse) wird eine
Ableitung bzw. ein Ableitungsbaum erstellt.
Die Ableitbarkeit der Tokenfo lge signalisiert die syntaktische Korrektheit des Programmes
© 2003 K. Ecker
Angewandte und praktische Informatik, Kap. 7: Einführung in den Compilerbau
7- 4
Beispiel: Grammatik G = (N, T, R, E) mit
N = {E, E', T, T', F },
T = {+, *, /, -, :=, (, ), ⟨id⟩, ⟨const⟩}
( := wird als ein Zeichen aufgefaßt)
R = {E → TE', E' → +TE' | -TE' | :=TE' | ε,
T → FT', T' → *FT' | /FT' | ε, F → (E) | ⟨id⟩ | ⟨const⟩}
Hilfsmittel zu Bestimmung einer Ableitung: Parsingtabelle ($ = end of text symbol))
ass_op
E⇒
id|const
add_op
sub_op
mul_op
div_op brack_op brack_cl
TE'
E' ⇒
:=TE'
T⇒
TE'
+TE'
−TE'
FT'
T' ⇒
ε
F⇒
$
ε
ε
ε
ε
FT'
ε
ε
*FT'
/FT'
⟨id⟩ | ⟨const⟩
(E)
Beispiel kapital := kapital * (100 - zinssatz) / 100;
mit der Tokenfolge
(id, 7) (ass_op, nil) (id, 7) (mul_op, nil) (brack_op, nil) (const, 8) (sub_op, nil)
(id, 9) (brack_cl, nil) (div_op, nil) (const, 8)
Vereinfachte Darstellung (nur Zeichen aus Teminalalphabet T): id := id * (const − id) / const
Ableitung:
Ableitungsbaum:
E ⇒ TE' ⇒ FT'E' ⇒ ⟨id⟩T'E' ⇒ ⟨id⟩E' ⇒ ⟨id⟩:=TE'
⇒ ⟨id⟩:=FT'E' ⇒ <id>:=<id>T'E' ⇒ <id>:=<id>*FT'E'
⇒ ⟨id⟩:=<id>*(E)T'E' ⇒ <id>:=<id>*(TE')T'E'
⇒ ⟨id⟩:=<id>*(FT'E')T'E'
⇒ ⟨id⟩:=<id>*(⟨const⟩T'E')T'E'
⇒ ⟨id⟩:=<id>*(⟨const⟩E')T'E'
⇒ ⟨id⟩:=<id>*(⟨const⟩-TE')T'E'
⇒ ⟨id⟩:=<id>*(⟨const⟩-TE')T'E'
⇒ ⟨id⟩:=<id>*(⟨const⟩-FT'E')T'E'
⇒ ⟨id⟩:=⟨id⟩*(⟨const⟩-⟨id⟩T'E')T'E'
⇒ ⟨id⟩:=⟨id⟩*(⟨const⟩-⟨id⟩E')T'E'
⇒ ⟨id⟩:=⟨id⟩*(⟨const⟩-⟨id⟩)T'E'
⇒ ⟨id⟩:=⟨id⟩*(⟨const⟩-⟨id⟩)/FT'E'
⇒ ⟨id⟩:=⟨id⟩*(⟨const⟩-⟨id⟩)/⟨const⟩T'E'
⇒ ⟨id⟩:=⟨id⟩*(⟨const⟩-⟨id⟩)/⟨const⟩E'
⇒ ⟨id⟩:=⟨id⟩*(⟨const⟩-⟨id⟩)/⟨const⟩
7.2.3
E
T
E'
F
T'
:=
T
E'
id
ε
F
T'
ε
id
*
F
T'
(
E
)
/
E'
T
F
T'
const
ε
-
T
F
T'
const
ε
E'
F
T'
id
ε
ε
Semantische Analyse
Aufgaben der semantischen Analyse:
−
Typverträglichkeit überprüfen
Jeder innere Knoten bekommt einen Typ zugewiesen
−
Zwischencode Generierung
Nach Erstellung des Ableitungsbaumes: Vereinfachung zum Operatorbaum:
Klammern werden weggelassen; Sequenzen von Nichtterminalzeichen werden durch einen einzelnen Knoten
ersetzt.
© 2003 K. Ecker
Angewandte und praktische Informatik, Kap. 7: Einführung in den Compilerbau
7- 5
Beispiel
kapital := kapital * (100 - zinssatz) / 100;
Zuerst: Ableitungsbaum wird
vereinfacht:
E
•
alle Blätter mit Marke ε , ( , )
werden weggelassen
•
alle inneren Knoten mit nur einem
Nachfolger werden weggelassen
id
E'
T
:=
id
T'
*
E
T'
const
E'
/
id
-
⇒ Operatorbaum
const
n4
innere Knoten sind durchnumeriert
id
:=
n3
(n 1, n 2, ...)
id
const
7.2.4
*
n2
n1
/
-
const
id
Code Generierung (Zwischencode)
Annahmen über eine "Zwischencode-Maschine":
Ein Register: accu
Inhalt: ⟨accu⟩
Speicherzellen 1, ..., n
Inhalt von Speicherzelle m: ⟨m⟩
Operation
Wirkung
LOAD m
accu ← ⟨m⟩
LOAD =m
accu ← m
STORE m
m ← ⟨accu⟩
ADD m
accu ← ⟨accu⟩ + ⟨m⟩
SUB m
accu ← ⟨accu⟩ − ⟨m⟩
MUL m
accu ← ⟨accu⟩ * ⟨m⟩
DIV m
accu ← ⟨accu⟩ / ⟨m⟩
ADD =m
accu ← ⟨accu⟩ + m
SUB =m
accu ← ⟨accu⟩ − m
MUL =m
accu ← ⟨accu⟩ * m
DIV =m
accu ← ⟨accu⟩ / m
Hier: "Syntaxorientierte" Übersetzung; jedem Knoten n des Operatorbaumes wird (bottom-up) ein Stück Code
C(n) zugewiesen.
C(n) wird durch Zusammenfügen aus den Codestücken der unmittelbaren Nachfolgerknoten erhalten.
Zwei Arten von inneren Knoten im Operatorbaum:
© 2003 K. Ecker
Angewandte und praktische Informatik, Kap. 7: Einführung in den Compilerbau
7- 6
w
(a)
nl
:=
n
(b)
nl
nr
rechter Teilbaum
linker Teilbaum
Knotenart := ... := steht für das Token (ass_op, nil)
nr
op
rechter Teilbaum
Knotenart op:
op ∈ {add_op, sub_op, mul_op, div_op}
steht für ein Token der Form (op, nil)
Code C(n) im Falle (a): Berechnet zuerst den Wert für den rechten Nachfolger; dieser wird dann in der für den
linken Nachfolger reservierten Speicherzelle abgelegt.
Code C(n) im Falle (b): Berechnet zuerst den Wert für den rechten Nachfolger; dieser wird in einer Hilfszelle $
abgelegt. Dann wird der Wert für den linken Nachfolger berechnet (er steht im accu); schließlich wird der accuWert mit dem Wert in $ verknüpft. Das Resultat steht dann im accu.
Der folgende Algorithmus bestimmt zu einem Operatorbaum den Code.
Jeder Knoten des Operatorbaumes bekommt eine Level-Zahl ∈ IN0 zugewiesen:
Level(Blatt) := 0;
Level (n) := max{ Level(m) | m ist unmittelbarer Nachfolger von n }
Sei h der größte im Baum auftretende Level (h = Level(w ), wobei w die Wurzel ist)
Jedem Blatt ist außerdem ein Token (Tokentype, ...) zugewiesen.
Algorithmus
zur syntaxorientierten Übersetzung einer Zuweisungsanweisung,
die nur arithmetische Operationen (+, −, *, /) enthält.
Eingabe: Operatorbaum
Ausgabe: Assemblerprogramm C(n) zur Wurzel w; C(n) ist ein String von Zeichen
Methode:
begin
for all Knoten n mit Level 0 do
-- Blätter
case Tokentype(n) of
-- Type des zu n gehörenden Tokens
id: C (n) := name der zugehörigen Variablen;
-- Nummer der Speicherzelle,
-- in der der Wert der Variablen steht
const: C(n) := '=k';
-- falls n die Konstante k darstellt
ass_op, add_op, sub_op, mul_op, div_op: C(n) := ' '
-- leerer String
end_case;
for i := 1 to h do
for all Knoten mit Level i do
-- Unmittelbare Nachfolger von n
-- (von links nach rechts) sind n 1, n 2, n 3
case Knotenart von n of
':=' : C (n) := 'LOAD 'C(n r)';STORE 'C (n l) ;
-- Zuweisungsfall
op : C (n) := C(n r)';STORE $'i';LOAD ' C(n l)';'C (op)' $'i ;
-- C (op) steht für: 'ADD', falls op = add_op; 'SUB'
(falls op = sub_op)
-'MUL', falls op = mul_op; 'DIV'
(falls op = div_op)
end_case;
end;
-- syntaxorientierte Übersetzung
© 2003 K. Ecker
Angewandte und praktische Informatik, Kap. 7: Einführung in den Compilerbau
7- 7
Beispiel: kapital := kapital * (100 - zinssatz) / 100;
Code für die Knoten n 1, n 2, n 3, n 4 (aus Übersichtlichkeitsgründen wurde nach jeden ';' ein Zeilenumbruch
eingefügt)
n 1:
ZINSSATZ;
STORE $1;
LOAD =100;
SUB $1
n 2:
=100;
STORE $2;
LOAD ZINSSATZ;
STORE $1;
LOAD = 100;
SUB $1;
DIV $2
n 3:
=100;
STORE $2;
LOAD ZINSSATZ;
STORE $1;
LOAD = 100;
SUB $1;
DIV $2;
STORE $3;
LOAD KAPITAL;
MUL $3
n 4:
LOAD =100;
STORE $2;
LOAD ZINSSATZ;
STORE $1;
LOAD = 100;
SUB $1;
DIV $2;
STORE $3;
LOAD KAPITAL;
MUL $3;
STORE KAPITAL
Behauptung zur Anzahl Hilfszellen $i : Sei h die Länge des längsten Weges von der Wurzel w zu den Blättern
(Anzahl Knoten auf dem längsten Weg). Dann gilt: es werden höchstens h − 2 Hilfszellen zur Speicherung von
Zwischenwerten benötigt.
Beweis: Induktion über die Höhe des Operatorbaumes.
w
h = 2:
Code für w :
LOAD n r; STORE n l
:=
nl
nr
h = k > 2: Induktionsannahme: die Behauptung sei für alle j < k erfüllt.
Wenn op ≠ :=
n
Code für n :
⇒
C(n) = C(n r); STORE $1; C(n l); C(op) $1
op
nl
nr
Eine Hilfszelle wird zur Speicherung des Wertes des Knotens n r benötigt.
Wenn op = :=
⇒
keine zusätzliche Hilfszelle benötigt, da der Wert von n r aus dem accu direkt nach n l
gebracht wird.
Zur Berechnung der Werte von n l und n r werden maximal k − 3, also für n insgesamt maximal k − 2 Hilfszellen
benötigt.
?
7.2.5
Code Optimierung
(a)
STORE $i; LOAD $i ;
kann entfernt werden, sofern $i später nicht mehr gebraucht wird
(b)
LOAD m; MUL $i;
kann durch LOAD $i; MUL m ersetzt werden (kommutatives Gesetz
anwenden)
analog: ADD m
(c)
LOAD ZINSSATZ; STORE $i; ...; SUB $i;
kann ersetzt werden durch ... SUB m
sofern $i sonst nicht gebraucht wird
© 2003 K. Ecker
Angewandte und praktische Informatik, Kap. 7: Einführung in den Compilerbau
7- 8
Beispiel: kapital := kapital * (100 - zinssatz) / 100;
n 4:
7.2.6
Original:
... Verbesserungen:
LOAD =100;
STORE $2;
LOAD ZINSSATZ;
STORE $1;
LOAD = 100;
SUB $1;
DIV $2;
STORE $3;
LOAD KAPITAL;
MUL $3;
STORE KAPITAL
LOAD =100;
STORE $2;
LOAD 100;
SUB ZINSSATZ;
DIV $2;
STORE $3;
LOAD $3
MUL KAPITAL
STORE KAPITAL
endgültiger Zwischencode:
LOAD 100;
SUB ZINSSATZ;
DIV 100;
MUL KAPITAL
STORE KAPITAL
Objektcode-Erzeugung
Das Programm im Zwischencode wird schließlich in die Ziel-Assemblersprache übersetzt.
--- Ende Kapitel 7 ---
© 2003 K. Ecker
Angewandte und praktische Informatik, Kap. 7: Einführung in den Compilerbau
7- 9
Herunterladen