Compilerbau Teil 6: Abstrakter...
Transcript of Compilerbau Teil 6: Abstrakter...
23.07.20 1Compilerbau – SS 2020 – Teil 6/AST
Compilerbau
Teil 6: Abstrakter Syntaxbaum
Version 2
2Compilerbau – SS 2020 – Teil 6/AST
Überblick
� Syntaxbäume
� Serialisieren und Deserialisieren eines Syntaxbaums
� Vererbungshierarchie
� Realisierung in Strukturen bzw. Klassen
3Compilerbau – SS 2020 – Teil 6/AST
Wo sind wir?
Makro-Prozessor
Scanner Parser Semantik
Optimierung
Code-Generierung
Optimierung
Symbol-Tabelle
4Compilerbau – SS 2020 – Teil 6/AST
Compiler
� Scanner = Teil eines Compilers, der die lexikalische Analyse durchführt
� Parser = Teil des Compilers, der eine Sequenz von Token auf syntaktische Korrektheit prüft und bei Korrektheit in einen abstrakten Syntaxbaum transformiert
Scanner
Parser
Erster Teileines Compilers
Tokenstrom
Abstrakter Syntaxbaum
Datei(en)
5Compilerbau – SS 2020 – Teil 6/AST
Definition und weitere Schnittstelle
� Abstrakter Syntax Baum = Abstract Syntax tree = AST = auf die Semantik der Sprachkonstrukte reduzierter Ableitungsbaum
� Im folgenden wird statt AST nur noch einfach vom Syntaxbaum gesprochen.
� Das Generieren des AST ist die letzte Aktion des Parsers.� Damit ist der Parser von den danach folgenden Phasen
entkoppelt.� Es besteht die Möglichkeit, dass der AST serialisiert
abgespeichert wird, so dass die folgenden Phasen lediglich dies einzulesen brauchen (wird aber selten gemacht).
� Bei einige Compilern werden die folgenden Phasen mit dem Parser gemischt, z.B. bei 1-Pass-Compilern (Pascal-Dialekte).
� In diesem Kurs werden die Phasen klar getrennt.
6Compilerbau – SS 2020 – Teil 6/AST
Beispiel I - Zuweisung
� Es handelt sich um einen n-ären Baum.
� Die Semantik eines Knotens k ist die Semantik der Unterknoten verknüpft durch die Operation des betreffenden Knotens k.
� Häufig werden diese Bäume mit depth-first (Tiefensuche) in Postorder-Reihenfolge traversiert.
a:= 3*(b+a);:=
a
3 +
b a
*
7Compilerbau – SS 2020 – Teil 6/AST
Beispiel II – Reihenfolge des Traversierens
� Preorder = Knoten, linker Unterbaum, rechter UnterbaumIm Beispiel: :=, a, *, 3, +, b, a Dies wird polnische Notation genannt.
� Postorder = linker Unterbaum, rechter Unterbaum, KnotenIm Beispiel: a, 3, b, a, +, *, := Dies wird umgekehrte Polnische Notation genannt.
� Inorder = linker Unterbaum, Knoten, rechter UnterbaumIm Beispiel: a, :=, 3, *, b, +, a Dies ist die übliche Infix-Schreibweise für arithmetische Ausdrücke. Hier sind Klammern zur Darstellung der Bindungsstärken erforderlich.
� https://de.wikipedia.org/wiki/Polnische_Notation
� https://de.wikipedia.org/wiki/Umgekehrte_polnische_Notation
� https://www.fmi.uni-jena.de/fmimedia/Fakultaet/Institute+und+Abteilungen/Abteilung+f%C3%BCr+Didaktik/Ressourcen/GE_If/InfixPostfixPrefix.pdf
Siehe
8Compilerbau – SS 2020 – Teil 6/AST
Traversieren von binären Bäumen
proc Preorder(Node root) { if root<>nil { ...root.data... Preorder(root.left); Preorder(root.right); }}
proc Postorder(Node root) { if root<>nil { Postorder(root.left); Postorder(root.right); ...root.data... }}
proc Inorder(Node root) { if root<>nil { Inorder(root.left); ...root.data... Inorder(root.right); }}
Das Traversieren in der Postorder- Reihenfolge ist die Basis der Code- generierung für Stack-Maschinen.
9Compilerbau – SS 2020 – Teil 6/AST
Beispiel III – Reihenfolge des Traversierens
proc Postorder(Node root) { if root<>nil { Postorder(root.left); Postorder(root.right); if terminal(root.data) { emit("push "+data); else emit(data); } }}
push apush 3push bpush aaddmulassign
Generierte Sequenz mitsymbolischen Operationen
10Compilerbau – SS 2020 – Teil 6/AST
Aufbau eines Syntaxbaumes (Beispiel) I
class Node { enum type; ref Node left; ref Node right; Node(type,left,right) { Konstruktor... }}
Klasse für binäre Bäume
+
b a
p1:= new Node(ID,nil,nil); // bp2:= new Node(ID,nil,nil); // aroot:= new Node(plus,p1,p2);
� Aufbau erfolgt Bottom-Up: erst die Blätter, dann die Unterbäume, dann der Baum
� Verknüpft wird das Ganze im übergeordneten Knoten.
� In dem Beispiel werden die Namen der beiden IDs ("a" und "b") nicht abgespeichert; dazu ist eine Symboltabelle nötig.
11Compilerbau – SS 2020 – Teil 6/AST
Aufbau eines Syntaxbaumes (Beispiel) II
� Damit lässt sich explizit programmiert ein Baum aufbauen.
� Aber wie soll dies Daten-gesteuert gehen?
� Dazu wird ein kleine Aufbausprache entworfen, die durch die Postordner-Traversierung generiert wird.
� Diese Sprache wird dann zum späteren Aufbau des Baums interpretiert.
� Beispiel: push apush 3push bpush aaddmulassign
12Compilerbau – SS 2020 – Teil 6/AST
Aufbau eines Syntaxbaumes (Beispiel) III
� Das ist hier nun das Skelett eines Interpreters.
� Am Ende liegt auf dem Stack die Adresse der Wurzel des Baums.
push apush 3push bpush aaddmulassign
(01) while not eof {(02) cmd:= read(); // Scanner vereinfacht(03) switch(cmd) {(04) case push: push(new Node(Operand));(05) case add: op1:= pop();(06) op2:= pop();(07) push(new Node(add,op1,op2));(08) case mul: op1:= pop(); (09) op2:= pop();(10) push(new Node(mul,op1,op2));(11) case assign: op1:= pop(); (12) op2:= pop();(13) push(new Node(assign,op1,op2));(14) }(15) }
13Compilerbau – SS 2020 – Teil 6/AST
AST und rekursiver Abstieg I
expr
expr
id
term
factor
id id+ *
term
factor
term
factor
+
id
*id
id
Abstrakter SyntaxbaumParsebaum mit allen beteiligten Regeln
� Jeder Knoten entspricht (fast immer) einer Regel.
� Ausnahmen: Regeln haben keine terminalen Kinder, die den Operationen entsprechen.
14Compilerbau – SS 2020 – Teil 6/AST
Eine Entscheidung
� Es wird statt der Adresse des Unterbaums ein NULL/NIL geliefert.
� Der Unterbaum wird verworfen; stattdessen wird ein Error-Knoten mit Diagnose-Information geliefert.
Wie werden syntaktische Fehler behandelt?
� Error sei eine Klasse, deren Objekte jeweils einen Fehler beschreiben.
� Diese Objekte können anstelle jeden Knotens geliefert werden, d.h. sie werden gestreut in den Syntaxbaum eingebaut.
� Über eine globale Variable gesteuert wird später der Baum verworfen.
15Compilerbau – SS 2020 – Teil 6/AST
AST und rekursiver Abstieg II
func ref node rule1() { if(token in FIRST(rule1) { ref Node left:= rule2(); if token="+" { eat("+"); else return Error("+ missing"); } ref Node right:= rule3(); return new node("+",left,right); else return Error("FIRST(rule1) missing"); }}
rule1= rule2, "+", rule3;
Es wird in der eigenenRegel auf die korrektenFirst-Token geprüft.
Auch wird dort, wo derFehler festgestellt wird,die Fehlerbehandlungdurchgeführt.
Die Adressen der Unter-bäume werden hier nichtals Parameter sondernals Funktionsresultategeliefert.
16Compilerbau – SS 2020 – Teil 6/AST
AST und rekursiver Abstieg III - Verkürzungen
� Die Fehlerbehandlung error() gibt hier lediglich eine Meldung aus. Diese wird noch mit lexikalischer Information (Dateiname, Zeile, Spalte) ergänzt.
� Besser ist es, wenn eine "Stop-Menge" von Token als Parameter mit gegeben werden. Im Fehlerfall werden dann alle folgenden Token überlesen bis eines aus dieser Menge gefunden wird.
func bool eat(Token tk) { if token=tk { getNextToken(); return true; else return false; }}
Class Error(String s) ... { ... process error ... print(s); return this;}
proc error(String s) { ... process error ... print(tk+" missing"); return nil;}
17Compilerbau – SS 2020 – Teil 6/AST
AST und rekursiver Abstieg IV - Verkürzungen
rule1= rule2, "+", rule3;
Schon etwaskürzer undüberschaubarer.
func ref node rule1() { if(token in FIRST(rule1) { ref Node left:= rule2(); if not eat("+") { return Error("+ missing"); } ref Node right:= rule3(); return new node("+",left,right); else return Error("FIRST(rule1) missing"); }}
18Compilerbau – SS 2020 – Teil 6/AST
Behandlung von Fehlern I
� Im Fall1 sollte alles bis zum "then" überlesen werden; sollte dies auch fehlen, dann bis zum "fi".Die Stop-Menge ist hier: ["then","fi"], wobei die vorderen Elemente Priorität gegenüber den hinteren haben.Das Überlesen erfolgt in der If-Regel so, dass halbwegs sinnvoll weiter gemacht werden kann.
� Im Fall2 sollte bis zum ";" überlesen werden. Hier wird die Assignment-Regel beendet, so dass sinnvoll vom Aufrufer her weiter gemacht werden sollte, z.B. mit einer anderen Alternative.Die Stop-Menge ist hier: [";","if","while","od","fi","end"],
... if a+<10 then a:= b+c;fi
... a:= (b+c)*c);
Fall1 Fall2
19Compilerbau – SS 2020 – Teil 6/AST
Behandlung von Fehlern II
� Die Bestimmung der Stop-Menge hängt stark von den Regeln und deren verschachtelte Aufrufen ab.
� Häufig ist auch noch eine Skip-Menge erforderlich: Solange ein Element aus dieser Menge gefunden wird, dann wird weiter überlesen.
� Bitte beachten, dass die vorhandene Aufrufschachtelung nach dem Überlesen abgebaut sein muss. Das ist nicht einfach zu lösen, da dann ein „schneller return-Durchlauf“ bis zu einem Wiederaufsetzpunkt realisiert werden muss.
� Exceptions sollten nur dann benutzt werden, wenn der Parser interne Fehler hat, z.B. zu wenig Speicher.
� Auf der anderen Seite werden durch Exceptions viele If-Abfragen gespart.
20Compilerbau – SS 2020 – Teil 6/AST
AST und rekursiver Abstieg V - Hinweise
� Immer wenn ein Token verarbeitet ist, wird um 1 Token weiter geschaltet (eat()).
� In der Regel-Funktion wird die Rechtmäßigkeit des Aufrufes geprüft.– Weniger, einfacher Code
– Schlechte Fehlerbehandlung
� Aber:Es sollten nie Funktionen mit Seiteneffekten programmiert werden – hier geht das nicht anders.
21Compilerbau – SS 2020 – Teil 6/AST
AST und rekursiver Abstieg VI - Hinweise
� Es besteht auch die Möglichkeit Daten von oben nach unten zu transportieren: per Parameter.
� Da komplexere Konstrukte, wie z.B. Prozeduren oder Klassen, über mehrere syntaktische Regeln aufgeteilt definiert werden, bedeutet dies aber, dass nicht zu jeder dieser "Hilfsregeln" ein eigener Knoten erzeugt wird.
� Es gibt dann die Möglichkeit, dass ein Knoten von der umfassenden Regelfunktion, z.B. Funktion, erzeugt und unvollständig als Parameter an anderen Routinen gegeben wird, die die fehlenden Teile ergänzen.
� Der Parameter ist dann der Kontext.
ParameterReferenz aufUnterbaumoder Error
22Compilerbau – SS 2020 – Teil 6/AST
Implementierungshinweise (primär für Java)
� Die Tokentypen werden durch eine enum-Klasse definiert.
� Bei den Mengenabfragen ("token in FIRST(T)") werden konstante Set-Objekte vom Typ EnumSet benutzt (Singletons).
� Aus Gründen der Verständlichkeit sollten alle Mengenabfragen in Methoden gekapselt werden:boolean isRuleXYZ().
� Das gilt auch für die Stop-Mengen.
� Wenn die Methoden genauso wie die Regeln heißen würden, kann es Konflikte mit Bibliotheksroutinen geben. Daher wird "rule" vor den Namen der Regel gesetzt: z.B. ruleStatement.
Wenn Generatoren wie Coco/R benutzt werden, sind das allesProbleme der Implementierer dieser Generatoren.
23Compilerbau – SS 2020 – Teil 6/AST
Knoten des Syntaxbaumes I
� Es besteht die Möglichkeit, n-äre Knoten als Listen von binären darzustellen, z.B. für Ketten von Statements.
� Dies hat den Vorteil, dass die Liste innerhalb des Knotens stückweise aufgebaut werden kann, anstatt zum Beginn ein Array mit einer geschätzten Größe anzulegen.
k
a dcb
k1 k
2k3
k4
a dcb
24Compilerbau – SS 2020 – Teil 6/AST
Knoten des Syntaxbaumes II
class PlusOp { type t= plusOp; ref Node left; ref Node right;}
class IfStatm { type t= IfStatm; ref Node expression; ref Node thenPart; ref Node elsePart;}
Zwei Beispielefür Knotentypen
struct node { type t; union { struct { struct *node left, struct *node right } PlusOp; struct { struct *node expression, struct *node thenPart struct *node elsePart } IfStatm; };};
Eine möglicheRealisierung in C/C++
Dies ist nichttypsicher.
25Compilerbau – SS 2020 – Teil 6/AST
Knoten des Syntaxbaumes III
type node = record t : typ; case nt : typ of PlusOp: ( left : node; right : node; ) IfStatm: ( expression : node; thenPart : node; elsePart : node; ) end;
Eine möglicheRealisierung in Pascal
� Dies ist auch nicht typsicher, aber es gibt immerhin einen Deskriptor, der zur Laufzeit bei Zugriffen geprüft werden könnte: nt.
� Wird dies immer getan, wie z.B. in Ada, dann sind diese Konstruktionen typsicher.
26Compilerbau – SS 2020 – Teil 6/AST
AST für Plong I - Vorgehen
� Anhand der Grammatik wird der AST entworfen.
� Durch Spezialisierung wird ein Klassenmodell entwickelt; dies gilt für Programmiersprachen mit Vererbung.
� Für Sprachen ohne Vererbung werden Strukturen mit Unions benutzt.
27Compilerbau – SS 2020 – Teil 6/AST
AST für Plong II - Deklarationen
AbstrakteMutterklasse Klasse für
alle Deklarationen
Aufzählung derbeiden Typenint und StringVererbungshierarchie für Deklarationen.
28Compilerbau – SS 2020 – Teil 6/AST
Bemerkungen I
� Eine Kette von Deklarationen wird durch eine lineare Liste dargestellt. In jedem Element wird eine Variable deklariert.
� DeclType wird der Datentyp – hier in Plong: int und String – dargestellt. Dies wird durch einen Aufzählungstyp erreicht.
� DeclID repräsentiert den Namen als terminales Symbol.
int a, b, c;
DeclVar
DeclType ID
int a
DeclVar
DeclType ID
int b
DeclVar
DeclType ID
int c
Beispiel
29Compilerbau – SS 2020 – Teil 6/AST
Bemerkungen II – Deklaration von Prozeduren
� Beachten Sie, dass es lokale Prozeduren oder Funktionen gibt.
� Die drei Punkte deuten eine Liste wie bei den Parametern an.
DeclProc
ID
Name
DeclParam
DeclType ID
Typ Parameter
DeclParam
DeclType ID
ParameterTyp
...
DeclVar DeclProc DeclFunc... ...
proc Name(Param, Param) … begin … end
Stmt ...
Prozeduren undFunktionen werdengemischt in derDeklarationsreihen-folge dargestellt.
30Compilerbau – SS 2020 – Teil 6/AST
Bemerkungen III – Deklaration von Funktionen
� Eine Funktion sieht genauso aus wie eine Prozedur, nur das ein Typ für den Rückgabewert angegeben wird.
DeclFunc
ID
Name
DeclParam
DeclVar DeclProc DeclFunc... ...
DeclType
Typ
Stmt ...
func typ Name(Param, Param) … begin … end
DeclType ID
ParameterTyp
DeclParam
DeclType ID
ParameterTyp
...
31Compilerbau – SS 2020 – Teil 6/AST
Bemerkungen IV - Program
� Das gesamte Programm sieht wie eine parameterlose Prozedur aus.
� Es ist erkennbar, wie sich die Struktur der Grammatikregeln sich in der Struktur des Ableitungsbaums niederschlägt.
ID
Name
DeclVar DeclProc DeclFunc... ...
program Name() … begin … end
Stmt ...
DeclProgram
32Compilerbau – SS 2020 – Teil 6/AST
AST für Plong III - Statements
Vererbungshierarchie für Statements.
33Compilerbau – SS 2020 – Teil 6/AST
Bemerkungen I
StmtAssign
DeclID
Variable
ID Expr
Variable := Expression;
Expression
StmtReturn
Expr
return Expression;
Expression
34Compilerbau – SS 2020 – Teil 6/AST
Bemerkungen II
� Ein Block von Statements wird durch eine Liste von Stmt-Knoten definiert.
� Analog bildet eine Liste von Expressions die Parameterliste einer aufgerufenen Routine.
call Name(Expr, Expr, ...);
StmtCall
DeclID
Name
ID ExprParam
Expression
...
if Expr then Statem1 else Statem2 fi
Expr
Expression
...Stmt
StmtIf
...Stmt
Statem1 Statem2
35Compilerbau – SS 2020 – Teil 6/AST
Bemerkungen III
while Expr do Statements od
Expr
Expression
...Stmt
StmtWhile
Statements
36Compilerbau – SS 2020 – Teil 6/AST
AST für Plong IV - Ausdrücke
37Compilerbau – SS 2020 – Teil 6/AST
Bemerkungen I - Blätter
� Es gibt keinen Unterschied zwischen ExprID und DeclID, außer dass es einen anderen Kontext gibt.
� Aber: es kann sein, dass eine benutzte Variable (ExprID) nicht deklariert ist (DeclID).
� ExprInt beinhaltet den Integerwert selbst, genauso wie ExprString den String selbst beinhaltet.
Variable
Variable
ID
Integer String
Value
ExprInt
Value
ExprString
38Compilerbau – SS 2020 – Teil 6/AST
Bemerkungen II
� Die Typanpassungsfunktionen (Coercions) werden wie vorher deklarierte Funktionsaufrufe behandelt.
� Auf dieselbe Weise werden auch die anderen vordefinierten Routinen behandelt: writeln(), readln()
Name(Expr, Expr, ...);
ExprFunc
DeclID
Name
ID ExprParam
Expression
...
int string2int(String);String int2string(int);
ExprFunc
DeclID
string2intint2string
ID ExprParam
Expression
39Compilerbau – SS 2020 – Teil 6/AST
Typanpassungen allgemein I
� Coercion = (Hier:) Funktion, die einen Typ in einen anderen durch Berechnung zur Laufzeit konvertiert.
� Cast = (Hier:) Anweisung an den Compiler ein bestehendes Bitmuster (Variablenwert) als einen bestimmten Typ anzusehen, d.h. keine "aktiven" Umrechnungen
� In anderen Programmiersprachen wird dies anders definiert.
� Siehe– https://en.wikipedia.org/wiki/Type_conversion
– https://de.wikipedia.org/wiki/Typumwandlung
40Compilerbau – SS 2020 – Teil 6/AST
Typanpassungen allgemein II
� Typerweiterungen = type promotion = Wert eines Typs als Wert eines umfassenden Typs ansehen bzw. dorthin konvertieren
� Typeinschränkung = type demotion = Wert eines Typs als Wert eines verkleinerten Typs ansehen bzw. dorthin konvertieren
Typeinschränkung
int i = 42;byte b = (byte) i;
float f = 3.0;int j = f;
Typerweiterung
byte b = 42;int i = (int) b;
int j = 42;float f = j;
Normalerweise kein Genauigkeitsverlust
Eventuell ein Genauigkeitsverlust
Was wäre, wennhier 3.141stehen würde?
41Compilerbau – SS 2020 – Teil 6/AST
Bibliotheksroutinen - Übersicht
Signatur Erläuterung
int string2int(String); String int2string(int);
Konvertierungsroutinen
String stringConcat(String,String) Realisierung der String-Addition
proc write(String s)proc writeln(String s)
Ausgaberoutinen
int readInt()String readString()proc readln()
Einleseroutinen
bool notEOF() Anzeige des EOF/EOI-Status
42Compilerbau – SS 2020 – Teil 6/AST
Bemerkungen III - Arithmetik
Expr, "+", Expr ;
ExprDyadic
DeclID
Left
Expr Expr
Right
ExprMonadic
Expr
Expression
"+", Expr ;
enum MonadicOp { plus, // + minus, // -}
enum DyadicOp { plus, // + minus, // - mul, // * div, // /
eq, // = ne, // <> ge, // >= gt, // > le, // <= lt, // <}
43Compilerbau – SS 2020 – Teil 6/AST
Grundsätzliche Ergänzung I
� Damit der AST später nicht rekursiv traversiert werden muss, erhalten alle Knoten in der abstrakten Klasse AST einen Zeiger zum Elternknoten, also hin zur Wurzel.
� Bei der Wurzel ist dieser Zeiger immer null.
� Hinweis: Wenn ein Zeiger zurück zum Elternknoten existiert, reicht eine einfache Schleife zum Traversieren des Baums.
44Compilerbau – SS 2020 – Teil 6/AST
Grundsätzliche Ergänzung II - Beispiel
program = "program", id, "(", ")", body ;procDecl = "proc", id, "(", [paramDecls], ")", body ;funcDecl = "func", ( "int" | "String" ), id, "(", ..., ")", body ;body = { varDecl }, { subDecl }, "begin", { ... }, "end" ;
DeclID
DeclVar DeclProc DeclFunc... ... ...
Stmt ...
DeclProgram
body
Diesen Knoten gibt es nicht im Syntaxbaum.
45Compilerbau – SS 2020 – Teil 6/AST
Grundsätzliche Ergänzung III - Beispiel
� Wenn also mehrere Aufrufe von verschiedenen Routinen erst den gesamten Knoten erzeugen, dann werden die teilweise gesetzten Knoten per Parameter zur Weiterverarbeitung den anderen Routinen übergeben.
� Dann entsteht die richtige Struktur:
DeclID DeclVar DeclProc DeclFunc... ...
program Name() … begin … end
Stmt ...
DeclProgram
46Compilerbau – SS 2020 – Teil 6/AST
Nach dieser Anstrengung etwas Entspannung...