Ausdruck

Werbung
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)
Herunterladen