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