Let's build a compiler Let's build a compiler a.k.a. Compilerbauwerkzeuge Yannik Hampe (a.k.a yankee) Kapitel 1 Einführung Syntaxbäume, Instruktionen, Umformungen Werkzeuge eines Programms ● ● Speicher – Heap – Stack Instruktionen – Instruktionen machen nur genau eine Sache: ● ● “addiere” ist eine Instruktion “1+2+3” können (je nach Architektur) eine ganze Reihe Instruktionen sein Ein einfacher Instruktionssatz Instruktion Bedeutung Erklärung ldc Load constant Lege Konstante auf den Stack iadd Integer addition Entferne die obersten beiden Zahlen vom Stack, addiere sie und lege das Ergebnis auf den Stack imul Integer multiplication Entferne die obersten beiden Zahlen vom Stack, multipliziere sie und lege das Ergebnis auf den Stack Baum zu Code ● ● ● Wie sieht der Baum links als mathematische Formel aus? Wie lässt sich die Berechnung mit den vorher benannten Instruktionen ausführen? Was ist die Knotenreihenfolge in Post-Order? Lösung: Baum zu Code ● Formel: 1+2 ● ldc 1 ldc 2 iadd ● 1, 2, + Baum zu Code 2 ● Nochmal die gleiche Aufgaben etwas interessanter. ● Formel? ● Instruktionen? ● Post-Order Knotenreihenfolge? Lösung: Baum zu Code 2 ● Formel: 1 + 2 * 3 ● lcd 1 lcd 2 lcd 3 imul iadd ● 1, 2, 3, *, + Code zu Baum int i = 10; while(i != 0) { --i; } ● Stelle den Code links als Baum dar. Lösung: Code zu Baum Baum zu Code: Einfache Schleife Um den Baum in Instruktionen zu übersetzen brauchen wir weitere Instruktionen Instruktion Bedeutung Beschreibung istore <num> Integer store Nimm die oberste Zahl vom stack und speichere sie als Variable Nummer <num> iload <num> Integer load Kopiere die Integervariable mit Nummer <num> und lege sie auf den Stack isub Integer substract Subtrahiere die oberste Zahl vom stack von der zweitobersten Zahl auf dem Stack. Entferne die beiden Zahlen vom Stack und lege das Ergebnis auf den Stack. ifeq <target> Branch if equal Wenn die oberste Zahl auf dem Stack 0 ist, dann springe zu Position <target> goto <target> branch Springe zu Position <target> Baum zu Instruktionen ● ● Die while-Schleife ist jetzt eine Besonderheit, weil sie sich aus zwei Codefragmenten zusammensetzt, die mehrmals ausgeführt werden: – Bedingung – Code block Wann wird in welcher Reihenfolge welcher Block wie häufig ausgeführt? Einfache Schleife: Instruktionscodefragmente Variablendeklaration Schleifencode declare variable i 10 set i get i 1 isub set i Schleifenbedingung get i 0 not equal Einfache Schleife: Instruktionscodefragmente Variablendeklaration Schleifencode Symboltabelle: Variable 0 heißt i und ist vom Typ int. lcd 10 istore 0 iload 0 lcd 1 isub istore 0 goto schleifenbedingung Schleifenbedingung iload 0 lcd 0 isub ifeq schleifenende Instruktionsfragmente zusammengefügt ldc 10 istore 0 schleifenbedingung: iload 0 ldc 0 isub ifeq schleifenende iload 0 ldc 1 isub istore 0 goto schleifenbedingung schleifenende: Kapitel 2 Jasmin Instruktionen für die Java Virtual Machine Jasmin – Java assembler ● OpenSource Projekt ● http://jasmin.sourceforge.net/ ● ● Kompiliert eine Textbeschreibung einer Java .class Datei in eine .class Datei Eine praktische Vereinfachung für uns damit wir die Ergebnisse unseres Compilers leichter lesen können als im Binärformat. Jasmin skeleton .class public MyClass .super java/lang/Object .method public static main([Ljava/lang/String;)V .limit stack <your maximum stack size> <your instructions> return .end method Compile & run ● Compile: – ● java -jar jasmin.jar <your file> Run: – java MyClass Tipp: Nach stdout schreiben ● Den PrintWriter für stdout oben auf den stack legen: – ● Einen auszugebenden Wert auf den stack legen: – ● getstatic java/lang/System/out Ljava/io/PrintStream; ldc 42 Println aufrufen – invokevirtual java/io/PrintStream/println(I)V (entfernt sowohl die 42, als auch den PrintWriter vom stack) Kapitel 3 Sprachbeschreibung Rekursive Sprachbeschreibungen aus denen ein Parser generiert werden kann Wildcards ● Textbeschreibung der Sprache: Eine ganze Zahl, dann ein Minuszeichen und dann eine weitere ganze Zahl. ● Wenn wir Platzhalter haben können wir die Sprache so beschreiben: Ausdruck: ZAHL '-' ZAHL ● Wenn wir Kettenausdrücke erlauben könnte der Ausdruck iterativ so aussehen: Ausdruck: ZAHL ( '-' ZAHL )+ Rekursion ● ● ● Wir haben gelernt, dass wir die Form einer Sprache mit Platzhaltern beschreiben können. Was jedoch, wenn wir Klammerausdrücke erlauben wollen? Der Ausdruck in der Klammer folgt den gleichen Regeln wie wieder ein kompletter Ausdruck. Wenn wir das nur rekursiv definieren könnten! Beschreibungen auf Basis von regulären Ausdrücken erlauben dies nicht. Rekursion (2) Ausdruck: ZAHL Ausdruck: ZAHL '-' Ausdruck Ausdruck: '(' Ausdruck ')' … In ANTLR4-Syntax ... grammar Demo; start: ausdruck ; ausdruck: ausdruck '-' ausdruck | '(' ausdruck ')' | ZAHL ; ZAHL: [0-9]+ ; WHITE: [\r\n\t ]+ -> skip ; … In ANTLR4-Syntax ... Name der Grammatik (Emphelung: Upper camel case) grammar Demo; start: ausdruck ; Parserregel – beginnt mit Kleinbuchstaben ausdruck: ausdruck '-' ausdruck | '(' ausdruck ')' oder | ZAHL ; ZAHL: [0-9]+ ; WHITE: [\r\n\t ]+ -> skip ; Lexerbefehl Lexerregel (Terminal) – beginnt mit Großbuchstaben … mit Labels grammar Demo; start: ausdruck ; ausdruck: links=ausdruck '-' rechts=ausdruck #Minus | '(' inKlammer=ausdruck ')' #Klammer | ZAHL #Zahl ; ZAHL: [0-9]+ ; WHITE: [\r\n\t ]+ -> skip ; … mit Labels grammar Demo; start: ausdruck ; Labels ausdruck: links=ausdruck '-' rechts=ausdruck #Minus | '(' inKlammer=ausdruck ')' #Klammer | ZAHL #Zahl ; ZAHL: [0-9]+ ; WHITE: [\r\n\t ]+ -> skip ; 1-2 als verschachtelte Menge Terminal 1 - 2 Ausdruck 1-2 als Baum 1-2 1 2 1-2-3 ● ● Wie sieht die verschachtelte Menge/der Baum für “1-2-3” aus? (Es gibt zwei Möglichkeiten!) Dass es zwei mögliche Ergebnisse gibt liegt an unser Linksrekursion: ausdruck: ausdruck '-' ausdruck Das erste Symbol in der Regel “ausdruck” geht wieder auf die Regel “ausdruck”. → Dies nennt sich direkte Linksrekursion Bonusfrage: Was ist indirekte Linksrekursion? Auflösung der Linksrekursion ● Per Standard behandelt ANTLR die Regeln “links nach rechts”. Dies bedeuted: ausdruck '-' ausdruck wird mit dem Input 1-2-3 als (1-2)-3 behandelt. ● 3 4 Was jedoch bei 2 ? Wenn wir diese Rechnung aufschreiben als “2^3^4”, dann meinen wir 2^(3^4) und nicht (2^3)^4. Dann müssen wir dies in unser Grammatik angeben: ausdruck '^'<assoc=right> ausdruck Operratorrangfolge (a.k.a. Punkt vor Strich) ● Beispiel: ausdruck: ausdruck '*' ausdruck #Mal | ausdruck '-' ausdruck #Minus ● ● Auch in diesem Beispiel gibt es wieder zwei Interpretationsmöglichkeiten ANTLR wird in diesem Fall immer der oberen Regel Priorität geben. Kapitel 4 Den Syntaxbaum verarbeiten Mit ANTLR einen Syntaxbaum generieren und mit diesem tolle Dinge machen Parser erzeugen & Syntaxbäume erstellen ● Parser erzeugen: – ● antlr4 Demo.g4 -no-listener -visitor Parser kompilieren: – ● Name der Grammatikdatei javac *.java Einen Syntaxbaum generieren und anzeigen lassen: – echo “1-2-3” | grun Demo start -gui Name der Startregel Name der Grammatik Code aus dem der Syntaxbaum gebaut werden soll Mit einem Visitor durch den Baum Diese Regel: ausdruck: links=ausdruck operator='*' rechts=ausdruck #Mult Generiert folgende Javaklasse: public static class MultContext extends AusdruckContext { public AusdruckContext links; public Token operator; public AusdruckContext rechts; ... } Der generierte Visitor ● ● Aus unseren Regeln: start: ausdruck ; ausdruck: links=ausdruck '-' rechts=ausdruck #Minus | '(' inKlammer=ausdruck ')' #Klammer | ZAHL #Zahl ; Entsteht folgender Visitor: public interface DemoVisitor<T> extends ParseTreeVisitor<T> { T visitStart(DemoParser.StartContext ctx); T visitKlammer(DemoParser.KlammerContext ctx); T visitMinus(DemoParser.MinusContext ctx); T visitZahl(DemoParser.ZahlContext ctx); } Unsere Implementierung public class MyDemoVisitor extends DemoBaseVisitor<String> { public String visitZahl(DemoParser.ZahlContext ctx) { System.out.println("Zahl: " + ctx.getText()); return null; } public String visitMinus(DemoParser.MinusContext ctx) { visit(ctx.links); visit(ctx.rechts); System.out.println("Sub"); return null; } public String visitKlammer(DemoParser.KlammerContext ctx) { visit(ctx.inKlammer); return null; } } Implementierung von Schleifen int whileCounter = 0; visitWhileLoop(... context) { int whileNum = whileCounter++; print(“whileBedingung_” + whileNum + “:”); visit(context.bedingung); Bedingung print(“ifeq whileEnd_” + whileNum); visit(context.code); print(“goto whileBedingung_” + whileNum); print(“whileEnd_” + whileNum + “:”); } Code Kapitel 5 Die Symboltabelle Mehrere Variablen, Variablentypen und andere “Identifier” Mehrere Variablen ● Unser Instruktionscode versteht für die iload/istore Befehle nur Zahlen. Wir wohlen jedoch Bezeichner im code. Hier ist die einfachste Lösung einer “Symboltabelle”: Map<String, Integer> variables; visitVariablendefinition(... context) { if (variables.contains(context.variablenname)) ERROR! variables.put(context.variablenname, variables.size()); } visitIdentifier(... context) { print(“iload ” + variables.get(context.identifierName)); } Scopes: Warum wir sie brauchen ● ● In C++: Was gibt die Methode rechts aus? Was würde bei der eben vorgestellten Strategie passieren? int i = 1337; void doStuff() { printf("%d\n", i); { int i = 43; printf("%d\n", i); } printf("%d\n", i); } Eine Scopeklasse class Scope { private Scope parent; private Map<String, Integer> variables; public int getVariableNumber(String varName) { Integer result = variables.get(varName); if (result == null) { return parent.getVariableNumber(varName); } return result; } Delegiere an übergeordneten } Scope falls nötig! Was ist eigentlich wenn parent null ist? Verwendung der Scopeklasse Class MyVisitor extends … { private Scope currentScope; public String visitWhile(... ctx) { In neuen Scope eintreten currentScope = new Scope(currentScope); // process while loop currentScope = currentScope.getParent(); } } Scope verlassen Dies gilt natürlich nicht nur für visitWhile - sondern für alle die visit* methoden die einen neuen Scope erstellen … und noch? ● ● Herzlichen Glückwunsch! Wir haben jetzt eine sehr einfache Symboltabelle mit Unterstützung für “scopes” implementiert. Momentan enthält die Tabelle nur Variablennamen. Auch noch sinnvoll sind: – Variablentypen der Variablen – Funktionen – Konstanten Kapitel 6 Stackverwaltung Wie wir sicherstellen dass immer das Richtige auf dem Stack liegt. Worauf wir achten müssen ● Unterschiedliche Typen haben unterschiedliche Größen und brauchen unterschiedlich viel Platz auf dem Stack – ● integer (4 byte), double/long (8 byte),... Der Stack muss am Ende jeder Methode genauso groß sein wie am Anfang. – Alles was wir auf den Stack drauflegen müssen wir auch wieder runternehmen. – Wir dürfen nicht zu viel runternehmen Der Stack in Java ● ● ● Die Adressierung läuft in 4-Byte-Einheiten – Die Typen “double” und “long” brauchen 8 Byte – Alle anderen Typen belegen 4 Byte (selbst “kleinere” Typen (byte, char, short) belegen 4 Byte auf dem Stack) Beim Laden einer Klasse wird der JVM bytecode verifier ausgeführt. Er garantiert dass unsere Typen korrekt sind und wirft eine Exception “VerifyError” falls nicht. Nach Durchlauf des “verifiers“ werden aus Performancegründen keine Überprüfungen mehr durchgeführt. Effekte unterschiedlicher Typen ● Angenommen wir könnten den Bytecode verifier umgehen. Was würde folgender Code ausgeben? getstatic java/lang/System/out Ljava/io/PrintStream; ldc 1 ldc 2 invokevirtual java/io/PrintStream/println(J)V ldc läd eine 4-Byte-Konstante Stack 64... 40 36 Typ: long 32... 8 4 0 ... 0000 0001 … 0000 0010 1 33 2 +2 =8.589.934 .594 Verwaiste und fehlende Werte ● Angenommen wir sagen, dass der Rückgabewert einer Funktion oben auf dem Stack abgelegt wird: eineFunktion(); ● verwaister Wert int a = eineFunktion(); OK int a, b; a = b = eineFunktion(); fehlender Wert … und dann könnte es sich auch noch um eine “void”-Funktion handeln Ein Lösungsansatz für die Datentypen ● ● Um passende Instruktionen wählen zu können müssen wir den Datentyp auf dem Stack wissen. Dies können wir ähnlich wie unsere Variablen im Visitor verfolgen Lösungsidee Typen auf dem Stack Class MyVisitor extends … { private Deque<DataType> typesOnStack; public String visitZahl(... ctx) { System.out.println(“ldc ” + ctx.getText()); typesOnStack.addLast(DataType.INTEGER); } public String visitMinus(... ctx) { Wert auf den Stack laden Werte auf dem Stack verifizieren und dann entfernen System.out.println(“isub”); popType(DataType.INTEGER); popType(DataType.INTEGER); typesOnStack.addLast(DataType.INTEGER); } } Vom Stack kopieren und löschen ● Zum Löschen und Kopieren stehen uns folgende Instruktionen zur Verfügung: – pop (Lösche 4 Byte vom Stack) – pop2 (Lösche 8 Byte vom Stack) – dup (Dupliziere 4 Byte) – dup2 (Dupliziere 8 Byte) Tipp Return statt print Um mehr mit den generierten Instruktionen zu machen Return statt print ● Instruktionen können wir auch zurückgeben statt diese direkt auszugeben: String visitSub(DemoParser.SubContext ctx) { String links = visit(ctx.links); String rechts = visit(ctx.rechts); return links + “\n” + rechts + “\n” + “isub”; } ● ● Dies hat den Vorteil, dass wir die Strings später nochmal bearbeiten können. Wir können statt Strings sogar andere Datentypen verwenden. Kapitel 7 Funktionen in Jasmin Deklaration, Parameter und Rückgabetypen Angabe von Typen ● ● Die Tabelle zeigt alle verfügbaren Typen Arrays bekommen ein “[” vorangestellt. (Beispiel: “[I” für array of int) Kürzel Bedeutung Z boolean B byte (8 bit signed) S short (16 bit signed) C char (16 bit unsigned) I int (32 bit signed) J long (64 bit signed) F float (32 bit) D double (64 bit) L<Class Name>; Object (Beispiel: “Ljava/lang/String;” für Strings) V void Funktionsdefinition ● .method public static main([Ljava/lang/String;)V Die Methode hat einen Parameter welcher ein array mit Objekten vom Typ String ist. Die Methode hat keinen Rückgabetyp. ● Die Parameter stehen als lokale Variablen zur Verfügung und können mit den *load-Instruktionen auf den Stack geholt werden. Methodenaufruf ● ● invokestatic MyClass/myMethod(I)V Ruft die Methode myMethod(int) (kein Rückgabewert) in der Klasse MyClass auf. Problem: Keine “main()” ● ● ● PHP ist ein Beispiel für eine Sprache ohne explizite “main”-Funktion. Im Beispiel rechts ist ein Beispiel PHP-Skript mit der Problematik. Beim Kompilieren müssen wir “main” und nicht-”main” Abschnitte trennen. echo “Hello “; world(); function world() { echo “world”; } echo “\n”; Main Nicht-main Main Lösungsansatz: Keine “main()” ● ● ● Wir können den Visitor zunächst über alle Funktionsdefinitionen laufen lassen und so alle Methoden außer der Main generieren – Mit getChild(int) auf unseren aktuellen Knoten können wir einen beliebigen Subknoten wählen – Mit instanceof können wir den Typ bestimmen Im zweiten Schritt laufen wir über alle übrigen Baumelemente und generieren dabei die Main-Methode. Beispielimplementierung: http://cipher-code.de/letsBuildACompiler/examples/noMainExample Globale Variablen ● Java kennt keine globalen Variablen ● “static”-Eigenschaften einer Klasse verhalten sich aber genauso. ● static-Eigenschaften können in Jasmin definiert werden mit: – ● Felder können auf den Stack kopiert werden: – ● .field static <Name> <Typ> getstatic <Klassenname> <Typ> Felder können vom Stack weggespeichert werden: – putstatic <Klassenname> <Typ> Beispiel globale Variablen C int myInt; void main() { print(myInt); } Java class MyClass { public static int myInt; public ...main(...) { System.out.println(myInt); } } Jasmin .class MyClass .super java/lang/Object .field static myInt I .method … main(...)V getstatic .../System/out getstatic MyClass/myInt I invokevirtual …/println(I)V .end method Kapitel 8 Typkonvertierungen integer, float, double, string, etc. Notwendigkeit von Typkonvertierung ● ● ● ● ● Alle unsere Instruktionen erwarten einen bestimmten Typ. “iadd” erwartet zwei Integers und hinterlässt einen Integer “aload” erwartet ein Objekt an spezifizierter Stelle in der Variablentabelle Manchmal haben wir jedoch unpassende Datentypen. Typkonvertierungen können explizit sein ((int)1.4) oder implizit sein (“text” + 5); Beispiel implizite Typkonvertierung int i = 2; double d = 1.4; String txt = “Text”; String result = txt + (i + d)); txt + Double.toString((double)i + d); Typkonvertierungsinstruktionen von nach Instruktion int long i2l int double i2d int float i2f int String invokestatic java/lang/Integer/toString(I)Ljava/lang/String; String int invokestatic java/lang/Integer/parseInt(Ljava/lang/String;)I ● ● Die Tabelle oben zeigt eine Auswahl der zur Konvertierung zur Verfügung stehenden Instruktionen Nicht für jede Typkombination steht eine Instruktion zur Verfügung. Beispiel: char → byte muss den Umweg über einen int gehen. Implizite Konvertierung generieren ensureString(VarType originalType) { if (originalType == STRING) { return ""; Nichts zu tun } Ist bereits ein String Ist ein Integer if (originalType == INTEGER) { return "invokestatic java/lang/Integer/toString(...)..."; } if ... } Konvertiere zu String Modifiziertes visitAdd visitAdd(... ctx) { String links = visit(ctx.links); VarType linksTyp = typesOnStack.getLast(); String rechts = visit(ctx.rechts); VarType rechtsTyp = typesOnStack.getLast(); if (linksTyp == STRING || rechtsTyp == STRING) { links += ensureString(linksTyp); rechts += ensureString(rechtsTyp); return links + "\n" + rechts + "\n" + "invokevirtual .../concat(...) ..."; } else if ... } Zwei Strings addieren ● Um das vorherige Beispiel lösen zu können müssen wir wissen wir wir Strings zusammenfügen: ldc “Hallo” ldc “ welt!” invokevirtual java/lang/String/concat(Ljava/lang/String;)Ljava/lang/String; ● In Java wäre dies: “Hallo”.concat(“ welt!”) “Text” + (2 + 1.4d) ldc “Text” ldc 2 i2d ldc2_w 1.4 dadd invokestatic java/lang/Double/toString(D)Ljava/lang/String; invokevirtual java/lang/String/concat(Ljava/lang/String;)Ljava/lang/String; Kapitel 9 Debuginformationen ...und dann Zeilenweise durch Quelltext steppen Motivation ● ● ● In unserem Instruktionscode geht die Information wie Variablen heißen, wann diese gültig sind und wie der Ursprungscode aussah verloren. Zum Debuggen kann es jedoch sehr nützlich sein ein Programm an beliebiger Stelle anzuhalten und... – Werte aller Variablen zu sehen – Zu sehen wo im Ursprungsquelltext sich das Programm befindet. Java erlaubt es diese Informationen in Klassendateien einzubetten. Link zur Position im Quelltext ● ● Damit der Debugger im Orginalquelltext anzeigen kann wo er ist, muss er wissen in welcher Datei in welcher Zeile der Orginalquelltext steht. Die Orginaldatei können wir in Jasmin spezifizieren mit: .source MeinQuelltext.txt ● Die Zeilennumern können wir mit “.line” angeben: .line 42 ldc 1 istore 0 MeinQuelltext.txt Zeile 42: int i = 1; Variablentabelle beschreiben ● Einen Eintrag in der Variablentabelle können wir so beschreiben: .var 0 is myInteger I from scope_begin to scope_end Name Index in Var-Tabelle Labelname Gültigkeitsbeginn Typ Labelname Gültigkeitsende Run eclipse Debugger! ● ● ● Auf dem classpath eines Eclipseprojekts liegt eine von uns kompilierte .class Datei. Im Build path is ein “source attachment” konfiguriert das den Pfad unser Quelltextdatei angibt. In den “debug configurations” ist eine Konfiguration angelegt die die von uns generierte Klasse ausführt und der Haken “stop in main” ist gesetzt. → Set, ready, debug! (Livedemo)