Bachelorarbeit Entwicklung einer neuen Codegenerierungstufe im Java Compiler von Sun von Paul Salomon im Studiengang Informatik im Auftrag des Instituts für praktische Informatik im Fachgebiet Programmiersprachen und Übersetzer an der Leibniz Universität Hannover 30.10.2009 1 Inhaltsverzeichnis 1 Einleitung..................................................................................................................................4 1.1 Die Aufgabe und Motivation.............................................................................................4 2 Grundlagen................................................................................................................................5 2.1 Der Java Compiler.............................................................................................................5 2.1.1 Der Java Zwischencode.............................................................................................6 2.1.2 Das Visitor Pattern im Java Compiler......................................................................10 2.1.3 Weitere wichtige Klassen des Java Compilers.........................................................10 2.2 Die JBCG Bibliothek.......................................................................................................11 2.3 Die cojen Bibliothek........................................................................................................12 3 Die Übersetzung......................................................................................................................13 3.1 Grundaufbau der Übersetzung.........................................................................................13 3.2 Übersetzung der Klassenstruktur.....................................................................................18 3.2.1 Übersetzung der Klasse............................................................................................18 3.2.2 Übersetzung der Funktionen und Konstruktoren.....................................................19 3.2.3 Übersetzung von globalen Variablen.......................................................................20 3.2.4 Übersetzung von statischen Blöcken.......................................................................20 3.3 Übersetzung der Blockstruktur........................................................................................20 3.3.1 Definitionen von lokalen Variablen.........................................................................21 3.3.2 Übersetzung von Blöcken und Hinzufügen von Anweisungen...............................22 3.3.3 Verzweigungen im Programmfluss..........................................................................23 3.3.4 If Strukturen.............................................................................................................24 3.3.5 While Strukturen......................................................................................................25 3.3.6 Do While Strukturen................................................................................................26 3.3.7 For Strukturen..........................................................................................................27 3.3.8 Switch Struktur........................................................................................................28 3.3.9 Break und continue Anweisungen............................................................................29 3.3.10 Labeled Blocks.......................................................................................................31 3.3.11 Try und catch Strukturen........................................................................................32 3.3.12 return Anweisungen...............................................................................................32 3.3.13 Weitere wichtige Funktionen des BlockScanners..................................................33 3.4 Übersetzung der Anweisungen........................................................................................34 3.4.1 Variablen und konstante Werte.................................................................................34 3.4.2 Zuweisungen und Zuweisungsoperatoren................................................................37 3.4.3 Operationen..............................................................................................................39 3.4.4 Typumwandlungen...................................................................................................43 3.4.5 Funktionsaufrufe......................................................................................................45 3.4.6 Konstruktoraufrufe...................................................................................................47 3.4.7 Konditionale Werte und booleschen Operationen....................................................47 3.5 Erweiterungen der JBCG Bibliothek...............................................................................49 3.5.1 Neue Klassen............................................................................................................49 3.5.2 Erweiterungen der vorhandenen Klassen.................................................................50 3.6 Bewältigung der Komplexität..........................................................................................52 3.7 Mögliche Erweiterungen.................................................................................................53 3.8 Veränderungen am Java Compiler...................................................................................54 3.9 Graphische Darstellung...................................................................................................54 3.9.1 Darstellung des Java Zwischencode........................................................................54 2 3.9.2 Darstellung des JBCG Zwischencode......................................................................55 4 Ergebnisse................................................................................................................................59 5 Fazit und Ausblick...................................................................................................................64 5.1 Fazit.................................................................................................................................64 5.2 Ausblick...........................................................................................................................65 6 Anhang.....................................................................................................................................66 6.1 Literatur- und Quellenverzeichnis...................................................................................66 3 1 Einleitung 1.1 Die Aufgabe und Motivation Die Aufgabenstellung dieser Bachelorarbeit ist es, den Java Zwischencode des Java Compilers von SUN in die Klassen der JBCG (Java-Bytecode Generator) Bibliothek zu übersetzen. Dazu soll eine neue Übersetzungsebene nach der Semantischen Analyse eingebaut werden. Ziel hierbei ist es, den Java Zwischencode möglichst komplett in die JBCG Bibliothek zu übertragen. Da die JBCG Bibliothek noch nicht die gesamte Sprache Java abdeckt, soll die Bibliothek entsprechend erweitert werden. Die Motivation der Arbeit ist es, eine Möglichkeit zur besseren Codeoptimierung zu schaffen. Der Java Compiler von SUN führt kaum Optimierungen am Zwischencode durch, da die strenge baumartige Struktur des Zwischencodes dies erschwert. Der Zwischencode der JBCG Bibliothek dagegen hat eher eine flussartige Struktur. An ihr kann der Programmfluss leichter nachvollzogen werden, was Optimierungen erleichtert. Ziel dieser Arbeit ist nicht die Optimierung des Zwischencodes, sondern die Erzeugung einer Struktur, welche eine Optimierung erlaubt. Die folgende Grafik zeigt die strukturellen Unterschiede der Zwischencode an Hand des Programmflusses eines If. Code Java Zwischencode public int max(int a, int b) { int max; if(a > b) { max = a; } else { max = b; } return max; } 4 JBCG Zwischencode 2 Grundlagen 2.1 Der Java Compiler Der Java Compiler übersetzt Java Code in Java Maschinen Code, welcher von der Java Virtual Maschine ausgeführt werden kann. Dabei durchläuft der Compiler verschiedene Phasen: • Lexikalische Analyse Die Codedatei wird Zeichen für Zeichen eingelesen und in Blöcke oder Token eingeteilt. • Syntaktische Analyse Die Anordnung der Token wird überprüft. Es werden Syntaxfehler erkannt und es wird ein Syntaxbaum erstellt. • Semantische Analyse Der Syntaxbaum wird auf semantische Fehler untersucht. Besonders wichtig hierbei sind Typprüfungen. Automatische Typumwandelungen werden hinzugefügt. Aus dem Syntaxbaum wird der Zwischencode. • Code Generierung Der Zwischencode wird in Maschinen Sprache übersetzt. Dabei wird der Zwischencode-Baum nach dem Scanner Pattern durchlaufen. Übersetzte Elemente werden aus dem Baum gelöscht, sodass ein leerer Baum zurückbleibt. Die verschiedenen Prozesse der Kompilierung werden von der Klasse com.sun.tools.javac.main.JavaCompiler gesteuert. In ihr ist für jede Phase eine Funktion definiert. Die wichtigste Funktion für diese Arbeite ist generate(...). Sie startet die Codegenerierungsphase. Der Java Zwischencode ist zu diesen Zeitpunkt schon vollständig aufgebaut. Die Funktion übersetzt jede Klasse einzeln, sodass leicht die einzelnen Klassen abgegriffen und in die JBCG Bibliothek übersetzt werden können. 5 2.1.1 Der Java Zwischencode Der Java Zwischencode hat eine Baumstruktur. Jedes Element kann theoretisch unendlich viele Unterknoten enthalten, hat aber immer nur einen Elternknoten. Alle Elemente des Zwischencodes sind von der Klasse JCTree abgeleitet. Der Rootknoten besteht aus einem JCCompilationUnit Objekt. Für jede Java Code Datei wird eine JCCompilationUnit erzeugt. Jedes Objekt im Zwischencode-Baum stellt ein Element der Sprache Java dar. Die Element besitzen verschiedene Attribute, welche wieder auf Unterknoten verweisen können. Folgende Tabelle gibt eine Übersicht über einige verschiedene Typen der Baumelemente und ihre wichtigsten Attribute: Objekt / Attribute Beschreibung JCCompilationUnit Rootknoten des Zwischencodes. Für jede Code Datei wird ein JCCompilationUnit erzeugt. JCStatement Überklasse für alle Kontrollstrukturen. JCExpression Überklasse für alle Ausdrücke. JCClassDecl Eine Definition einer Klasse Attribut Typ Beschreibung sym ClassSymbol Das Symbol für diese Klasse. extending JCTree Die Superklasse. implementing List<JCExpression> Eine Liste der implementierten Interfaces defs List<JCTree> Liste von globalen Variablen, Funktionen und statischen Blöcken. mods JCModifiers Die Modifizierer der Klasse. JCMethodDecl Definition einer Funktion oder eines Konstruktors Attribut Typ Beschreibung sym MethodSymbol Das Symbol der Methode params List<JCVariableDecl> Liste der Parameter mods JCModifiers Modifizierer der Funktion / des Konstruktors body JCBlock Der Funkions Body JCVariableDecl Definition einer Variablen Attribut Typ Beschreibung sym VarSymbol Das Symbol der Variablen mods JCModifiers Modifizierer der Variablen init JCExpression Die Implementierung vartype JCExpression Der Typ der Variablen JCSkip Ein leerer Ausdruck 6 JCBlock Ein Block Attribut Typ Beschreibung stats List<JCStatement> Eine Liste aller Anweisungen in diesem Block JCDoWhileLoop Eine Do While Schleife Attribut Typ Beschreibung cond JCExperssion Die Bedingung der Schleife body JCStatement Der Body der Schleife JCWhileLoop Eine While Schleife Attribut Typ Beschreibung cond JCExperssion Die Bedingung der Schleife body JCStatement Der Body der Schleife JCForLoop Eine For Schleife Attribut Typ Beschreibung cond JCExperssion Die Bedingung der Schleife body JCStatement Der Body der Schleife init List<JCStatement> Der initalisierungs Teil der For Schleife. step List<JCExpressionStatement Definition des Schrittes der nach jeden Schleifen > Durchgang ausgeführt wird. JCLabeledStatement Ein Statement mit einem Label Attribut Typ Beschreibung body JCStatement Das Statement, meistens ein Block. label Name Der Name des Lables. JCSwitch Eine Switch Struktur Attribut Typ Beschreibung selector JCExpression Der Ausdruck, nach dem geschaltet wird cases List<JCCase> Eine Liste mit allen Fällen JCCase Ein Fall eines Switch Attribut Typ Beschreibung pat JCExpression Der Wert für diesen Fall stats List<JCStatement> Eine Liste mit allen Anweisungen für diesen Case Fall JCTry Ein Try Catch Struktur Attribut Typ Beschreibung body JCBlock Der Body des Try Struktur catchers List<JCCatch> Eine Liste mit allen catch Blöcken. finalizer JCBlock Der finilizer Block JCConditional Eine bedingte Zuweisung Attribut Typ Beschreibung cond JCExperssion Die Bedingung truepart JCExperssion Der Wert falls die Bedingung wahr ist 7 falsepart JCExperssion JCIf Der Wert falls die Bedingung falsch ist. Eine If Struktur Attribut Typ Beschreibung cond JCExpression Die Bedingung thenpart JCStatement Der then Block des If elsepart JCStatement Der else Block des If JCExpressionStatement Enthält eine Anweisung Attribut Typ Beschreibung expr JCExpression Die Anweisung JCBreak Eine break Anweisung Attribut Typ Beschreibung label Name Der Name eines Blocks mit label. Ist in normalen Blocks null JCContinue Eine continue Anweisung Attribut Typ Beschreibung label Name Der Name eines Blocks mit label. Ist in normalen Blocks null JCReturn Eine return Anweisung Attribut Typ Beschreibung expr JCExpression Der Rückgabewert des return. Ist null wenn void zurückgegeben wird. JCMethodInvocation Der Aufruf einer Methode oder eines Konstruktors. args List<JCExpression> Die Parameter meth JCExpression Die Methode oder der Konstruktor. JCNewClass Die Erzeugung einer neuen Klasse Attribut Typ Beschreibung args List<JCExpression> Eine Liste von Parameter für den Konstruktor constructor Symbol Das Symbol für den aufzurufenden Konstruktor JCNewArray Ein neues Array Attribut Typ Beschreibung elemtype JCExpression Der Typ des Array dims List<JCExpression> Die Größe aller Dimensionen elems List<JCExpression> Liste aller Elemente des neuen Arrays. Die Liste kann null sein wenn nur die Dimensionen angegeben werden. JCParens Ein Ausdruck in Klammern Attribut Typ Beschreibung expr JCExpression Der Ausdruck JCAssign Eine Zuweisung Attribut Typ Beschreibung lhs JCExpression Die linke Seite der Zuweisung 8 rhs JCExpression JCAssignOp Die rechte Seite der Zuweisung Ein Zuweisungsoperator Attribut Typ Beschreibung lhs JCExpression Die linke Seite der Zuweisung rhs JCExpression Die rechte Seite der Zuweisung operator Symbol Das Symbol des Operators JCUnary Eine unäre Operation Attribut Typ Beschreibung arg JCExpression Der Parameter des Operators operator Symbol Das Symbol des Operators JCBinary Eine binäre Operation Attribut Typ Beschreibung lhs JCExpression Der linke Operand rhs JCExpression Der rechte Operand operator Symbol Das Symbol des Operators JCTypeCast Eine Typumwandlung Attribut Typ Beschreibung clazz JCTree Der Typ in den umgewandelt werden soll expr JCExpression Der Ausdruck der umgewandelt werden soll JCInstanceOf Eine Überprüfung des Typen durch instanceof Attribut Typ Beschreibung clazz JCTree Der Typ auf den getestet werden soll expr JCExperssion Der Ausdruck der getestet wird JCArrayAccess Der Zugriff auf ein Array Attribut Typ Beschreibung indexed JCExpression Das Array auf das zugegriffen wird index JCExpression Der index des Zugriffes JCFieldAccess Der Zugriff auf ein Feld Attribut Typ Beschreibung sym Symbol Das Symbol des Feldes selected JCExpression Das Element aus dem das Feld ausgewählt wird. Kann ein anderes Feld oder eine Klasse sein JCIdent Ein Element Attribut Typ Beschreibung sym Symbol Das Symbol des Elementes JCLiteral Ein statischer Wert Attribut Typ Beschreibung typetag int Index des Typs value Object Der Wert 9 JCModifiers Alle Modifizierer eines Elementes Attribut Typ Beschreibung getFlags() Set<Modifier> Alle Modifizierer in einem Set Die Bachelorarbeit „Untersuchung des Compilers zum Java 6 Release“ von Carsten Grenz und Marc Lindenberg beschreibt den Java Compiler und seinen Zwischencode sehr ausführlich. 2.1.2 Das Visitor Pattern im Java Compiler Der Java Zwischencode wird vom Compiler durch das so genannte Visitor Pattern ausgewertet. Das Visitor Pattern ist ein Verfahren, um Bäume zu durchlaufen und zu bearbeiten. Der Visitor besitzt Funktionen, um jede Art von Objekten zu besuchen. Jedes Objekt hat eine Funktion, die es ihm ermöglicht, von einem Visitor besucht zu werden. Im Java Compiler ist dieses Verfahren mit der Klasse com.sun.tools.javac.tree.TreeScanner implementiert. Der TreeScanner besitzt für jedes Objekt im Java Zwischencode eine visit Funktion. Jedes Objekt im Zwischencode besitzt eine Funktion accept(TreeScanner), welche einfach die zum Objekt gehörige visit Funktion im TreeScanner aufruft und sich selbst übergibt. Ein JCModifiers Objekt ruft z. B. in ihrer accept Funktion visitModifiers(JCModifiers) auf. In der visit Funktion übergibt der TreeScanner mögliche Unterknoten der Funktion scan(JCTree). Die Funktion scan ruft wiederum die accept Funktion der Unterknoten auf. So wird der gesamte Baum durchlaufen. Der Vorteil dieses Verfahren ist, dass die Bearbeitung des Baumes in nur einer Klasse gekapselt werden kann. Die Klassen des Baumes benötigen nur die Funktion accept(...). Alle Klassen, die vom TreeScanner abgeleitet sind, können so den Zwischencode durchlaufen und bearbeiten, ohne dass an den JCTree Klassen etwas geändert werden muss. Da dies eine sehr praktische Art ist, den Java Zwischencode zu bearbeiten, wird dieses Verfahren auch zum Übersetzen des Zwischencodes in die JBCG Bibliothek genutzt. Dazu wurden mehrere Klassen vom TreeScanner abgeleitet, die den Zwischencode nach dem Visitor Pattern durchlaufen. 2.1.3 Weitere wichtige Klassen des Java Compilers Neben der JCTree Klasse und ihren Unterklassen gibt es noch weitere Klassen des Java Compilers, die für diese Bachelorarbeit wichtig sind. Die folgenden Klassen werden oft bei der Übersetzung verwendet: 10 • com.sun.tools.javac.code.Symbol Diese Klasse stellt den Namen eines Elementes und deren Eigenschaften dar. Alle Elemente des Java Zwischencodes, die einen Namen besitzen, haben ein Attribut vom Typ Symbol. Das Symbol stellt Klassennamen, Variablennamen, Methodennamen oder auch den Namen eines Typs oder einer Operation (z.B. + oder -) dar. Die Symbol Klasse verfügt über eine ganze Menge an nützlichen Attributen und Funktionen. Über das Attribut owner kann das übergeordnete Symbol abgefragt werden. Bei einer Funktion z. B. würde owner das Symbol der Klasse sein, in der die Funktion definiert ist. Auf diese Weise kann leicht heraus gefunden werden, ob es sich bei einer Variable um eine globale oder lokale Variable handelt. Globale Variablen haben als owner eine Klasse, während lokale eine Funktion besitzen. Auch der Typ eines Objektes kann aus der Klasse Symbol gelesen werden. Dazu besitzt Symbol das Attribut type, welches ein Objekt der com.sun.tools.javac.code.Type Klasse ist. (siehe unten) Die Klasse Symbol hat einige Unterklassen, die vom Art des Symbols abhängen. Eine Funktion besitzt z.B. ein MethodSymbol Objekt. Die für diese Arbeit wichtigen Unterklassen sind ClassSymbol, MethodSymbol, OperatorSymbol, TypeSymbol und VarSymbol. • com.sun.tools.javac.code.Type Die Klasse Type repräsentiert den Typ einer Klasse, Variable, Operation oder Funktion. Mit Typ ist aber nicht unbedingt der Typ des Rückgabewertes gemeint. Eine Funktion z. B. hat den Typ method. Um den Rückgabewert einer Funktion oder Operation zu erhalten, kann die Funktion getReturnType() aufgerufen werden. Sie gibt den Typ des Rückgabewertes zurück. 2.2 Die JBCG Bibliothek Ziel der JBCG Bibliothek ist es, einen kompletten Java Zwischencode abzubilden, zu optimieren und in Java Maschinencode zu übersetzen. Die Motivation dahinter ist einmal, den Java Zwischencode zu optimieren, was im Zwischencode des Java Compiler von Sun nur schwer möglich ist. Desweiteren könnten Programme in Java Maschinencode übersetzt werden, die nicht in Java geschrieben sind. Das Portieren solcher Programme auf andere Systeme wäre so erheblich leichter. Die JBCG Bibliothek liegt im Package de.unihannover.psue.jbcgen und besteht aus vier Teilen. Das Unterpackage de.unihannover.psue.jbcgen.classfile stellt die Klassen Struktur dar. Sie ähnelt sehr der Klassen Struktur des Java Zwischencodes. Das Package beinhaltet Klassen die 11 Java -Klassen, -Funktionen und -Variablen darstellen. Klassen zur Beschreibung des Programmflusses innerhalb einer Funktion sind im Package de.unihannover.psue.jbcgen.basicblock definiert. Der Programmfluss wird anders als im Java Zwischencode in einer flussartigen Struktur dargestellt. Grob gesagt kann der Programmfluss als einfache verkettete Liste dargestellt werden, die aber sowohl Verzweigungen, als auch Schleifen zulässt. Der dritte Teil von JBCG ist das Package de.unihannover.psue.jbcgen.expression. Es enthält Klassen zur Definition von Ausdrücken. Dazu zählen Operationen, Zuweisungen, Methodenaufrufe usw. Dieser Teil ist, ähnlich wie der Java Zwischencode, baumartig aufgebaut. Der letzte Teil ist das Package de.unihannover.psue.jbcgen.optimizer. Es enthält eine Klasse TreeWalker, die den JBCG Zwischencode nach dem Scanner Pattern durchläuft. Die Klasse wird in dieser Arbeit dazu genutzt, den Zwischencode grafisch darzustellen. Ansonsten sind noch weitere Klassen zur Optimierung des Zwischencodes und zum Erzeugen von Maschinencode in diesem Package enthalten. Diese Klassen sind für diese Arbeit nicht relevant. Der Unterschied zwischen dem Java Zwischencode und dem JBCG Zwischencode besteht darin, dass der JBCG Zwischencode sehr viel mehr auf den Java Maschinencode ausgerichtet ist. Das macht sich besonders in der Darstellung des Programmflusses bemerkbar. If Anweisungen und Schleifen werden in Programmfluss übersetzt. Es gibt keine Unterschiede mehr zwischen einer Schleife und einem If. Beide werden nur durch Verzweigungen im Programmfluss dargestellt. Operationen werden konkretisiert oder in Funktionsaufrufe übersetzt (z. B. String + String). Durch diese Nähe zum Maschinencode lässt sich der Programmfluss leichter nachvollziehen und er kann besser optimiert werden. Leider kann die JBCG Bibliothek noch nicht den gesamten Sprachumfang von Java darstellen. In dieser Arbeit wurde die Bibliothek zwar erweitert, trotzdem sind noch weitere Erweiterungen nötig, um Java komplett abzubilden. Die JBCG Bibliothek wird in der Bachelorarbeit „Entwicklung eines Zwischencode-Modells für die Java-Bytecode Generierung“ von Adreas Prante genauer erklärt. 2.3 Die cojen Bibliothek Cojen ist eine Open Source Bibliothek zur Java Maschinen Code Erzeugung. Sie stellt dabei eine Vielzahl an Klassen bereit, die den Maschinencode beschreiben. Da die JBCG Bibliothek die cojen Bibliothek zur Erzeugung des Maschinencodes verwendet, spielt diese auch bei der Übersetzung des Zwischencodes eine Rolle. Es werden allerdings nur zwei Klassen der cojen Bibliothek zum Übersetzen benötigt. Die Klasse Modifier stellt die Modifizierer von Klassen, Funktionen und globalen Variablen dar. Die Klasse TypeDesc beschreibt einen Typ. Dabei können primitive Typen sowie Objekttypen dargestellt werden. 12 3 Die Übersetzung 3.1 Grundaufbau der Übersetzung Die Übersetzung des Java Zwischencodes ist in drei Teile aufgeteilt, welche die Klassenstruktur, den Progammflusses und die Anweisung übersetzen. Diese Teilung leitet sich auch aus der JBCG Bibliothek ab. Auch sie ist in die Teile Classes, BasicBlock und Expression aufgeteilt. Gesteuert wird die Übersetzung von der Klasse JBCGClassTranslator. Sie ist die Hauptklasse der Übersetzung. Die Aufgabe der Klasse ist es, alle Klassendefinitionen des Java Zwischencodes zu sammeln und die Übersetzung zu starten. Klassendefinitionen werden im Zwischencode als ein JCClassDef Objekt abgebildet. Mit der Methode addClass() werden alle JCClassDef Objekte in einer Liste gespeichert. Der Aufruf der addClass() Methode erfolgt in der Klasse com.sun.tools.javac.main.JavaCompiler in der generate() Funktion. Sind alle Klassen hinzugefügt, wird die Übersetzung gestartet. Dies macht die Methode translate(), welche auch vom JavaCompiler aufgerufen wird. Wichtig für die Übersetzung der Klasse ist das Feld defs der JCClassDef Objekte. Das Feld ist eine Liste aus JCTree Objekten. Diese können folgende Objekte sein: • JCMethodDecl: Diese Objekte repräsentieren entweder einen Konstruktor oder eine Funktion. Ob es sich bei dem Objekt um eine Funktion oder einen Konstruktor handelt, kann durch die Funktion isConstructor() des Symbols festgestellt werden. • JCVariableDecl: Hierbei handelt es sich um die Definition einer globalen Variablen. Das Problem hierbei ist, dass diese Definitionen möglicherweise auch eine Implementierung enthalten kann. Bei nicht statischen Variablen werden diese Implementierungen schon vom Java Compiler in jeden Konstruktor kopiert. Bei statischen passiert dies allerdings nicht. Darum sammelt der JBCGClassTranslator alle statischen Implementierungen und fügt sie später dem statischen Block der Klasse hinzu. (Mehr dazu Kap. 3.2.4) 13 • JCBlock: Dies ist die Definition eines statischen Blocks. Jede Klasse kann davon beliebig viele besitzen. In jeder JBCGClass gibt es aber immer nur genau einen statischen Block pro Klasse. Deswegen werden alle statischen Blocks des JCClassDef Objekts zu einem statischen Block in der JBCGClass zusammengefügt (Mehr dazu Kap. 3.2.4). Falls kein JCBlock in der Klasse definiert ist, wird ein leerer erzeugt. Eine weitere wichtige Klasse zur Übersetzung ist die GlobalClassTable. Sie ist eine Datenbank, in der alle JBCGClass Objekte gespeichert sind. Auch alle Funktionen, Konstruktoren und globalen Variablen sind in der GlobalClassTable enthalten und können durch ihr Symbol abgerufen werden. Die GlabalClassTable wird von allen Scannern genutzt, um Felder und Funktionen zu laden. Sie wird schon vor Beginn der eigentlichen Übersetzung in einer Vorübersetzung erstellt. Alle Klassen, Funktionen und globalen Variablen sind also schon vor der eigentlichen Übersetzung im JBCG Zwischencode vorhanden. Zuständig für die Vorübersetzung ist der PreClassScanner. Zum Übersetzen des Java Zwischencodes wurden mehrere Scanner Klassen definiert, die von der TreeScanner des Java Compilers abgeleitet sind. Die Scanner überschreiben die visit Funktionen der TreeScanner Klasse und arbeiten den Java Zwischencode nach dem Scanner Pattern ab. Die Scanner sind dabei hierarchisch aufgebaut. Der PreClassScanner, FunctionScanner und der ConstructorScanner sind für die Klassenstruktur zuständig. Sie rufen den BlockScanner auf. Dieser übersetzt zusammen mit dem ConditionScanner, der Verzweigungen erzeugt, den Programmfluss. Anweisungen werden vom ExpressionScanner übersetzt. Folgende Grafik verdeutlicht den Grundaufbau der Übersetzung. 14 Abbildung 1: Grundaufbau der Übersetzung Jeder Scanner überschreibt nur die für ihn relevanten visit Funktionen des TreeScanner. Die folgende Tabelle zeigt, welche visit Funktionen von welchem Scanner überschrieben werden: Funktion Parameter Bedeutung Beispiel FunctionScanner visitMethodDef JCMethodDecl Definition einer Funktionen visitModifiers JCModifiers Modifiziere der Funktion visitVarDef JCVariableDecl Definition eines Parameters visitBlock JCBlock Der Block der Funktion private / public / static ConstructorScanner visitMethodDef JCMethodDecl Definition eines Konstruktors visitModifiers JCModifiers Modifiziere des Konstruktors visitVarDef JCVariableDecl Definition eines Parameters visitBlock JCBlock Der Block des Konstruktors 15 private / public BlockScanner visitBlock JCBlock Der Block einer Funktion oder es Konstruktors visitVarDef JCVariableDecl Eine lokale Variable visitIf JCIf Eine if Struktur visitWhileLoop JCWhileLoop Eine While Schleife visitDoLoop JVDoWhileLoop Eine do While Schleife visitForLoop JCForLoop Eine For Schleife visitSwitch JCSwitch Eine Switch Struktur visitSkip JCSkip Ein leeres Statement visitBreak JCBreak Eine break Anweisung visitContinue JCContinue Eine continue Anweisung continue; visitReturn JCReturn Eine return Anweisung return; visitExec JCExpressionStatemen Eine Anweisung t visitTry JCTry Eine try catch Struktur visitLabeled JCLabeledStatement Ein Statement mit einem Label if(a); i = 4 + a; / println(), label: i = 5 + 7; ExpressionScanner visitAssign JCAssign Eine Zuweisung visitAssignop JCAssignOp Eine Zuweisung mit Operator i += 7; visitBinary JCBinary Eine binäre Operation 5-7 visitIdent JCIdent Der Name einer Variablen, einer Funktion oder einer Klasse i / println(String) / java.lang.Object visitLiteral JCLiteral Ein Wert, wie eine Zahl oder ein Text. 5 / 5.7f / „Test“ / true visitExec JCExpressionStatemen Eine Anweisung t visitUnary JCUnary Eine Operation mit nur einem -i / +i / i++ Operanten. visitSelect JCFieldAccess Der Zugriff auf ein Feld oder eine andere Klasse. o.toString() / System.out.println() visitIndexed JCArrayAccess Der Zugriff auf ein Array a[i] visitApply JCMethodInvocation Das Ausführen einer Funktion print() visitNewClass JCNewClass Das Anlegen eines neuen Objektes new Point(); visitNewArray JCNewArray Das Anlegen eines neuen Arrays new int[9] / {1 , 5 , 3} visitTypeCast JCTypeCast Das Casten von Typen (int) / (String) visitParens JCParens Ein umklammerter Ausdruck (5 + 7) 16 i = 5; / p = new Point(); visitConditional JCConditional Eine bedingte Zuweisung. i=(a>b) ? a:b; visitTypeTest JCInstanceOf Ein Type-Test mit instanceof o instanceof Point Funktionen, die von keinem Scanner überschrieben werden visitForeachLoop JCForeachLoop Eine Foreach Schleife for(Object o: ObjectList) visitCase JCCase Das case eines Switch case 0: visitSynchronized JCSynchronized Ein synchronisierter Block synchronized(this) { i=0; } visitCatch JCCatch Der catch Block einer try und catch(Exception e) { } catch Struktur visitThrow JCThrow Eine throw Anweisung visitAssert JCAssert Eine assert Anweisung assert i == 0; visitTypeIdent JCPrimitiveTypeTree Ein primitiver Typ int / byte / double visitTypeArray JCArrayTypeTree Ein array Typ int[] / byte[] / double[] visitTypeApply JCTypeApply visitTypeParameter JCTypeParameter visitWildcard JCWildcard Eine Annotation @Override visitTypeBoundKind TypeBoundKind visitAnnotation JCAnnotation visitErroneous JCErroneous visitLetExpr LetExpr Wie zu sehen ist, werden einige Funktionen von keinem Scanner überschrieben. Dafür gibt es drei unterschiedliche Gründe: • Die Elemente kommen nicht mehr im Zwischencode vor Einige Elemente werden nur bei der Erstellung des Zwischencodes gebraucht und wurden vom Java Compiler bereits umgewandelt, so dass sie bei der Übersetzung nicht mehr vorkommen. Dazu zählen die Elemente, die generischen Code darstellen, sowie die Foreach Schleife und Annotationen. • Die Elemente werden in anderen Funktionen übersetzt Einige Elemente werden gleich bei der Übersetzung ihrer Elternknoten mit übersetzt. Ihre visit Funktionen werden also nie aufgerufen und müssen deshalb nicht überschrieben werden. Dazu gehören vor allen die Typ Elemente und das JCCatch. • Die Elemente können noch nicht übersetzt werden Einige Elemente können noch nicht übersetzt werden. Dazu gehören die Elemente der Ausnahme-Behandlung und assert Anweisungen. 17 Eine weitere Klasse ist wichtig für die Übersetzung. Die Klasse Translator besitzt eine Vielzahl an statischen Hilfsfunktionen. Die Klasse wird von allen Scannern an vielen Stellen verwendet. Sie stellt Hilfsfunktionen bereit, die kleine Teile des Java Zwischencodes übersetzen. 3.2 Übersetzung der Klassenstruktur Die Übersetzung der Klassenstruktur ist der einfachste Teil der Übersetzung. Die Klassen Struktur des Java Zwischencodes und die des JBCG sind sehr ähnlich. Jedes JCClassDecl Objekt wird in ein JBCGClass Objekt übersetzt. Jedes JCMethodDecl Objekt wird entweder in ein InternalFunction Objekt oder in ein Constructor Objekt übersetzt. Globale Variablen werden in Field Objekte übersetzt. Einzig die Übersetzung von statischen Blöcken ist etwas schwieriger, da die JBCG Bibliothek keine alleine stehenden, statischen Blöcke darstellen kann. 3.2.1 Übersetzung der Klasse Da bei der Übersetzung von Funktionsaufrufen oder den Zugriff auf globale Variablen festgestellt werden muss, ob diese Funktion bzw. das Feld innerhalb der zu übersetzenden Klassen definiert ist oder nicht, muss schon zur Übersetzung bekannt sein, welche Klasse welche Funkionen und globalen Variablen besitzt. Zu diesem Zweck wurde die Klasse GlobalClassTable definiert. Die GlobalClassTable hat zwei Aufgaben: Einmal ist sie eine Datenbank, die alle Klassen mit ihren Funktionen und ihren Feldern speichert. Andererseits scannt sie JCClassDef Objekt, um diese Datenbanken anzulegen. Die dafür zuständige Methode ist scanClass(JCClassDef). Sie wird vom JBCGClassTranslator jedes Mal dann aufgerufen, wenn eine neues JCClassDecl Objekt hinzugefügt wird. Für jedes eingescannte JCClassDef Objekt wird ein neues JBCGClass Objekt erzeugt. Die Klasseneigenschaften Name, Modifizierer und Superklasse werden übersetzt und der JBCGClass zugewiesen. Außerdem erhält jede JBCG Klasse einen Typ, der aus einem TypeDesc Objekt besteht. Auch alle globalen Felder und Funktion werden eingelesen. Dazu hat die GlobalClassTable eine innere Hilfeklasse, den PreClassScanner. Der PreClassScanner ist eine Unterklasse von TreeScanner und überschreibt die Funktionen visitClassDef(JCClassDef), visitVarDef(JCVariableDecl) und visitMethodDef(JCMethodDecl). Für jede globale Variablen wird ein Field Objekt angelegt und dem JBCGClass Objekt hinzugefügt. Jede Methodendefinition muss getestet werden, ob sie ein Konstruktor oder eine Funktion ist. Konstruktoren werden in ein Constructor Objekt übersetzt und Funktionen in ein InternalFunction Objekt. Die Implementierung der Funktionen werden an dieser Stelle nicht übersetzt. Dies passiert später bei der eigentlichen Übersetzung. 18 3.2.2 Übersetzung der Funktionen und Konstruktoren Wurden alle Klassen in die GlobalClassTable aufgenommen, startet die eigentliche Übersetzung. Alle InternalFunction und Constructor Objekt sind zwar bereits vorhanden, allerdings nur rudimentär. Attribute wie Modifizierer und Rückgabewerte fehlen noch. Sie werden in den Klassen FunctionScanner und ConstructorScanner übersetzt. Die beiden Klassen FunctionScanner und ConstructorScanner sind fast identisch. Wie der Name schon sagt ist der FunctionScanner für Klassen-Funktionen zuständig, der ConstructorScanner für Konstruktoren. Leider konnte für Konstruktoren und Funktionen keine gemeinsame Scanner Klasse gefunden werden, da die get und set Funktionen für Parameter und Modifizierer nicht in der gemeinsamen Superklasse Function definiert sind. Die Scanner besitzen die statische Funktion scanFunction(InternalFunction, GlobalClassTable, JCTree) bzw. scanConstructor(Constructor, GlobalClassTable, JCTree), die den Scanvorgang starten. Die Scanner übersetzen die folgenden Bestandteile: • Die Modifizierer Aus einem JCModifiers Objekt wird ein Modifier Objekt der cojen Bibliothek erzeugt. Diese Objekte beschreiben alle Modifizierer der Funktion bzw. des Konstruktors. • Die Parameter Das JCMethodDecl besitzt im Attribut params eine Liste der Parameter. Diese bestehen aus JCVariableDecl Objekten. Die Scanner erzeugen für jedes JCVariableDecl Objekt ein Identifier Objekt und fügt es der Funktion bzw. dem Konstruktor als Parameter hinzu. Die Namen der Parameter werden bei der Übersetzung geändert. Der Grund dafür ist im Kapitel 3.4.1 beschrieben. • Den Rückgabewert Der Rückgabewert ist im Attribut restType definiert. Er wird in ein TypeDesc Objekt übersetzt. • Den Block Wenn es sich nicht um eine abstrakte Funktion handelt, hat jede Funktion und jeder Konstruktor einen Block, der alle Anweisungen und Ausdrücke enthält. Dieser Block besteht aus einem JCBlock Objekt. Das Objekt wird an den BlockScanner weitergegeben, welcher es übersetzt. 19 3.2.3 Übersetzung von globalen Variablen Alle globalen Variablen Definitionen wurden bereits vom PreClassScanner übersetzt. Für jede globale Variable existiert also schon ein Field Objekt in dem jeweiligen JBCGClass Objekt. Allerdings können globale Variablen in Java schon bei der Definition implementiert werden. Diese Implementierungen werden vom PreClassScanner ignoriert. Im Fall von nicht statischen Variablen ist es auch nicht nötig, die Implementierungen zu übersetzen, da der Java Compiler automatisch alle Implementierungen in die Konstruktoren kopiert. Statische Implementierungen werden allerdings nicht in den Konstruktor kopiert, da sie logischerweise nicht von einer Instanziierung der Klasse abhängig sind. Da sie aber trotzdem irgendwo implementiert werde müssen, sammelt der JBCGClassTranslator alle Implementierungen statischer globaler Variablen, übersetzt sie später bei der Übersetzung des statischen Blocks mit und fügt sie dort ein (siehe Kap. 3.2.4). 3.2.4 Übersetzung von statischen Blöcken Bei der Übersetzung von statischen Blöcken trat das Problem auf, dass die Klasse JBCGClass keine Möglichkeit bot, einen statischen Block aufzunehmen. Deswegen wurde die Klasse um die Funktion getStaticBlockFunction() erweitert. Die Funktion gibt ein InternalFunction Objekt zurück, welches den statischen Block enthält. Obwohl dieser statische Block keine wirkliche Funktion ist, wird er durch ein InternalFunction dargestellt. Grund dafür ist, dass auch in statischen Blöcken lokale Variablen definiert werden können. Diese Variablen müssen einer Funktion hinzugefügt werden, also wird der statische Block auch als Funktion dargestellt. In Java kann jede Klasse beliebig viele statische Blöcke enthalten. Alle statischen Blöcke werden bei der Übersetzung zu einem zusammengefasst. Alle statischen Implementierungen von globalen Variablen werden in diesen statischen Block eingefügt. Sie werden vom BlockScanner in der Funktion visitVarDef(...) bearbeitet. 3.3 Übersetzung der Blockstruktur Die Blockstruktur beschreibt den Programmfluss innerhalb einer Funktion oder eines Konstruktors. Übersetzt wird die Blockstruktur vom BlockScanner und vom ConditionScanner. Der BlockScanner ist vom TreeScanner abgeleitet und scannt Blöcke nach dem Scanner Pattern. Er erzeugt verschiedene BasicBlock Objekte und verkettet diese. Ihm wird in der Funktion scanTree(...) ein JCBlock Objekt oder eine Liste aus JCTree Objekten übergeben. 20 Wenn in einem Block weitere Blöcke enthalten sind, oder sich der Programmfluss verzweigt, ruft der BlockScanner sich rekursiv selbst auf. Stößt der BlockScanner auf Anweisungen, werden diese mit Hilfe des ExpressionScanner übersetzt und dem Block hinzugefügt. Da Anweisungen nur in einen SequenceBlock eingefügt werden können, gibt es immer einen aktuellen SequenceBlock. In diesen Block werden alle auftretenden Anweisungen eingefügt. Bei jeden Aufruf von scanTree(...) muss ein nextBlock vom Typ BasicBlock übergeben werden. Dieser folgt dem aktuell zu übersetzenden Block. Sind alle Anweisungen des aktuellen Blocks übersetzt, wird der aktuelle Block mit den nextBlock verkettet. Da sich aber innerhalb des aktuelle Blocks Verzweigungen befinden können, muss der aktuelle Block nicht der gleiche sein wie zum Anfang der Übersetzung. Im Java Zwischencode ist die Blockstruktur hierarchisch aufgebaut. Kommt z. B. in einem Block ein If vor, ist das If im stats Attribut enthalten und dem Block untergeordnet. Die trueund else-Blöcke sind wiederum dem If untergeordnet. In der JBCG Bibliothek beinhaltet ein Block keinen anderen Block, sondern verweist auf den nächsten. Das If würde also auf den Block folgen so wie der true oder else Block auf dem If Block folgen würde. Aufgabe der Block Übersetzung ist es also vor allem, die hierarchische Struktur in eine Flussstruktur zu überführen. Der ConditionScanner erzeugt Verzweigungen. Er hat ebenfalls eine statische Funktion scanTree(...). Der Scanner gibt ein ConditionBlock Objekt zurück, welches die Verzweigung darstellt. Hier bei gibt es keinen Unterschied zwischen einer If Verzweigung und einer Schleife. 3.3.1 Definitionen von lokalen Variablen Wie globale Variablen sind auch lokale Variablen als JCVariableDecl Objekt im Java Zwischencode definiert. Definitionen von lokalen Variablen können überall innerhalb von Blöcken auftreten. Übersetzt werden Definitionen von lokalen Variablen vom BlockScanner in der Funktion visitVarDef(...). Die Variablen werden in Identifier Objekte übersetzt und der Funktion hinzugefügt. Neue Identifier Objekte werden mit Hilfe der Translator Klasse in der Funktion translateLocalVariable(...) erzeugt. Beim späteren Zugriff auf diese Variable wird genau dieses Identifier Objekt benutzt, es wird kein neues erzeugt. Wird die Variable bei der Definition gleich mit implementiert, übersetzt der ExpressionScanner die Implementierung und es wird eine StoreOperation in den aktuellen Block eingefügt. Im Java Code sind lokale Variablen nur innerhalb des Blockes sichtbar, in dem sie definiert sind. Im Java Maschinen Code ist dies nicht so. Dort sind alle lokalen Variablen in der gesamten Funktion sichtbar. Auch in der JBCG Bibliothek werden lokale Variablen den Funktionen zugeordnet und nicht den Blöcken. Problematisch ist dies, wenn zwei lokale Variablen innerhalb einer Funktion den selben Namen haben, wie z. B. bei folgendem Code: 21 void f() { for(int i = 0; i < 10; i++) { ... } for(double i = 1.5; i < 80.5; i = i * i) { ... } } Sowohl die Variable i der ersten Schleife wie auch die Variable i der zweiten Schleife sind in der gesamten Funktion f() sichtbar. Wenn in der zweiten Schleife auf i zugegriffen wird, kann nicht mehr festgestellt werden, welche Variable gemeint ist. Haben beide Variable den selben Typ, ist dies unproblematisch, da in diesem Fall einfach die Variablen der ersten Schleife weiter benutzt werden kann. Haben die Variablen aber, wie im Beispiel, verschiedene Typen, würde dies zu Fehlern führen. Darum wird der Name der lokalen Variablen beim Übersetzen abgeändert. Hinter den normalen Namen wird, durch ein „_“ getrennt, der Typ angehängt. Im Beispiel würde aus i also i_int und i_double werden. So haben lokale Variablen mit unterschiedlichen Typ auch immer unterschiedliche Namen und es kommt zu keinem Fehler. 3.3.2 Übersetzung von Blöcken und Hinzufügen von Anweisungen Der BlockScanner übersetzt alle Blöcke in der Funktion visitBlock(...). Blöcke werden im Java Zwischencode als JCBlock Objekte dargestellt. Diese enthalten das Attribut stats, welches aus einer Liste von JCStatement Objekten besteht und alle Anweisungen und Kontrollstrukturen des Blocks enthält. Der BlockScanner arbeitet diese Liste von oben nach unten ab und übersetzt die einzelnen Statements. Wird die Funktion visitExec(...) aufgerufen, muss eine Anweisung übersetzt werden. Die Anweisung wird dem ExpressionScanner übergeben, der sie übersetzt und ein Expression Objekt zurück gibt. Das Expression Objekt wird in den aktuellen SequenceBlock eingefügt. Allerdings können nicht alle Anweisungen des Java Zwischencodes in eine einzige Anweisung der JBCG Bibliothek übersetzt werden. Zum Beispiel der Ausdruck println(++i) muss in zwei Anweisungen aufgeteilt werden. Einmal die Erhöhung der Variablen i ums eins und einmal der Aufruf der print(...) Funktion. Da der ExpressionScanner nur ein einziges Expression Objekt zurück liefert, gibt es im BlockScanner die Möglichkeit pre und post Expression einzufügen. Dazu werden die Funktionen addPreExpression(...) und addPostExpression(...) benutzt (siehe Kap. 3.3.13). Wenn eine Anweisung dem aktuellen Block hinzugefügt werden soll, werden erst alle pre Expression (im Beispiel i = i +1), dann die normale Anweisung (print(i)) und zum Schluss alle post Expression eingefügt (im Beispiel keine). 22 3.3.3 Verzweigungen im Programmfluss Verzweigungen im Programmfluss werden in der JBCG Bibliothek durch ConditionBlock Objekte dargestellt. Ein ConditionBlock hat eine Bedingung sowie einen BasicBlock für den true Pfad und einen für den false Pfad. Jede Schleife und auch Ifs werden in ConditionBlock Objekte umgewandelt. Erzeugt werden ConditionBlock Objekte vom ConditionScanner mit der Funktion scanTree(...). Der Funktion wird eine Bedingung, in Form eines JCExpression Objektes, übergeben sowie je ein BasicBlock für true- und false Teil. Mit Hilfe des ExpressionScanners werden die Operanden der Bedingungen übersetzt. Die Bedingung selber wird vom ConditionScanner übersetzt. Der ConditionBlock kennt die einfachen boolesche Bedingungen gleich (==), ungleich (!=), größer als (>), kleiner als (<), größer oder gleich als (>=) und kleiner oder gleich als (<=). Alle booleschen Operationen müssen auf diese Bedingungen zurückgeführt werden. Ist nur ein einfacher boolescher Wert die Bedingung, so wird dieser auf Gleichheit mit true überprüft. Wird der Wert in der negierter Form geprüft, wird er auf Gleichheit mit false getestet. Kompliziert ist die Übersetzung von verundeten und veroderten Bedingungen. Verundungen und Verorderungen können nicht im ConditionBlock dargestellt werden, sondern müssen durch die Hintereinanderschaltung von ConditionBlöcken erzeugt werden. Findet der ConditionScanner eine Verundung zweier Bedingungen, erstellt er zunächst einen ConditionBlock mit der ersten Bedingung. Danach ruft er sich rekursiv selbst auf und übergibt die zweite Bedingung. Der zurückgegebene ConditionBlock wird als true-Block in den übergeordneten ConditionBlock gesetzt. Sind zwei Bedingungen verodert, geht der ConditionScanner genauso vor, nur dass der zweite ConditionBlock als false-Block gesetzt wird. So ist auch sichergestellt, dass wenn bei einer Verundung die erste Bedingung falsch ist, die zweite gar nicht mehr getestet wird. Gleiches gilt bei einer wahren Bedingung in einer Veroderung. Folgende Grafik gibt ein Beispiel für eine veroderte und verundete Bedingung. Im Kapitel 3.9.2 wird die grafische Darstellung des JBCG Zwischencodes erklärt. 23 Java Code JBCG Zwischencode (Funktionsansicht) public void f() { if(bool1 && (bool2 || bool3)) { } } 3.3.4 If Strukturen If Strukturen werden im Java Zwischencode als JCIf Objekte abgebildet. Die Funktion visitIf(...) des BlockScanners bearbeitet dieses Objekt. Die wichtigsten Attribute des JCIfs sind: • cond Das Attribut cond ist ein JCExpression und stellt die Bedingung dar. • thenStatement Dieses Attribut ist der true Block. Er ist ein JCStatement und kann entweder aus einem ganz Block bestehen oder auch nur aus einer einzigen Anweisung. • elseStatement Der else Block kann wieder aus einem ganzen Block oder auch nur aus einer Anweisung bestehen. Wenn das If keine else Block besitzt ist das Attribut elseStatement nicht definiert. Das If wird in ein ConditionBlock und drei SequenceBlocks übersetzt. Der ConditionBlock wird mit dem ConditionScanner erstellt. Ihm wird das cont Attribut als Bedingung und zwei 24 neue SequenceBlock Objekte als true- und false-Block übergeben. Um die Anweisungen des thenStatement und des elseStatement Attributs zu übersetzen, ruft sich der BlockScanners rekursiv selbst auf. Als letztes wird ein SequenceBlock erzeugt, der das Ende des Ifs darstellt. Der true- und der false-Block verweisen auf diesen Block. Das Ende des Ifs ist der neue aktuelle Block, in den folgende Anweisungen eingefügt werden. Folgende Grafik zeigt ein If als ProgrammflussGraphen. Java Code JBCG Programmfluss (Funktionsansicht) public void f() { if(a > 5) { a = 5; } } 3.3.5 While Strukturen While Schleifen werden im Java Zwischencode als JCWhileLoop dargestellt. Die wichtigsten Attribut des JCWhileLoop sind cond und body. Das cond Attribut ist die Bedingung unter der die Schleife fortgesetzt wird. body ist der Block (kann auch eine einzelne Anweisung sein), der in der Schleife ausgeführt wird. Übersetzt wird die While Schleife ähnlich wie ein If (siehe Kap. 3.3.4) mit zwei Unterschieden: Der nextBlock des body Blocks ist wieder mit dem ConditionBlock verbunden und der false-Block des ConditionBlocks ist das Ende der While Schleife. So entsteht eine Schleife, die erst abbricht, wenn die Bedingung falsch wird. Zum Übersetzen des body Blocks wird ein Loop Objekt erzeugt. Das Loop gibt an, welcher 25 Block beim Auftreten eines break oder eines continue als nächstes ausgeführt wird (mehr dazu Kap. 3.3.9). Im diesem Fall ist der break Block das Schleifenende und der continue Block ist der ConditionBlock. Beispiel Code: JBCG Programmfluss (Funktionsansicht) public void f() { while(a > 0) { a--; } } 3.3.6 Do While Strukturen JCDoWhileLoop ist das Objekt welches im Java Zwischencode do while Schleifen repräsentiert. Das JCDoWhileLoop Objekt hat die gleichen Attribut wie das JCWhileLoop der normalen while Schleife (siehe Kap. 3.3.5). Der Aufbau der do-while Schleife gleicht dem der while Schleife, bis auf die Tatsache, dass der ConditionBlock und der body Block vertauscht sind. Das heißt, dass der body Block vor dem ConditionBlock durchlaufen wird. Ist die Bedingung erfüllt, wird der body Block noch einmal durchlaufen, wenn nicht, ist die Schleife beendet. 26 Java Code JBCG Programmfluss (Funktionsansicht) public void f() { do { a--; } while(a > 0); } 3.3.7 For Strukturen For Schleifen werden in der Funktion visitForLoop(...) übersetzt. Ihr wird ein JCForLoop Objekt übergeben, welches die beiden Attribute cond und body enthält. Die beiden Attribute haben dieselbe Bedeutung wie bei der while Schleife (siehe Kap. 3.3.5). Außerdem besitzt das Objekt noch die Attribute init und step. init beinhaltet den Initialisierungsteil der for Schleife. Das step Attribut stellt die Anweisung dar, die nach jedem Schritt durchgeführt wird. Für das step und das init werden jeweils eigene SequenceBlock Objekte erstellt. Der init Block wird zuerst durchlaufen, dann folgt der ConditionBlock. Ist die Bedingung wahr, wird der body Block und anschließende der step Block durchlaufen. Der step Block verweist wieder auf den ConditionBlock. Ist die Bedingung falsch, ist die Schleife beendet. Im Fall einer continue Anweisung wird zum step Block gesprungen und nicht zum ConditionBlock wie in anderen Schleifen. Eine break Anweisung lässt den Programmfluss zum Schleifenende springen. 27 Java Code JBCG Programmfluss (Funktionsansicht) public void f() { for(int i = 0; i < a; i++) { System.out.println(i); } } 3.3.8 Switch Struktur Switch Strukturen werden als JCSwitch dargestellt und in der Funktion visitSwitch(...) übersetzt. Das JCSwitch Objekt hat zwei wichtige Attribute: • selector Das ist der Wert, den das Switch auswertet. • cases Dies ist eine Liste von JCCase Objekten. Sie beinhaltet alle case Fälle und den default Fall. Das JCCase hat wiederum zwei wichtige Attribute. stats ist eine Liste von JCStatement Objekten. Diese sind alle Anweisungen, die ab dem case bis zum nächsten case oder zum Ende des Switches, ausgeführt werden. Das Attribut pat ist das Label des case. Beim default ist pat nicht definiert. Die JBCG Bibliothek besaß keine Klasse zur Darstellung einer Switch Struktur. Darum wurde eine neue Klasse SwitchBlock hinzugefügt (Siehe Kap. 3.5.1). Das Attribut selector wird vom ExpressionScanner übersetzt und an den SwitchBlock übergeben. Alle Cases werden von einen neuen Aufruf des Blockscanners übersetzt. Als nextBlock bekommt jeder Aufruf des Blockscanners den Block des nächsten case übergeben. Da innerhalb eines Switch auch break 28 Anweisungen vorkommen können, muss auch ein Loop Objekt übergeben werden (mehr dazu Kap. 3.3.9). In Falle einer break Anweisung wird das Switch beendet und der jeweilige case Block bekommt als nextBlock das Ende des Switch gesetzt. Java Code JBCG Programmfluss (Funktionsansicht) public void f() { switch(a) { case 0: case 1: b = 8; break; case 2: b = 9; default: } } 3.3.9 Break und continue Anweisungen Sowohl break als auch continue Anweisungen tauchen in der JBCG Bibliothek nicht mehr auf und werden in Programmfluss übersetzt. Das JCBreak Objekt stellt eine break Anweisung im Java Zwischencode dar. Continue Anweisungen werden durch das JCContinue Objekt dargestellt. Beide Objekte haben das Attribut name, welches den Namen eines Labels enthalten kann. Um break und continue Anweisungen zu übersetzen, wurde die Klasse Loop eingeführt. Sie speichert, welcher Block der nächste ist, falls ein break oder continue auftritt. Dabei wird unterschieden, ob das name Attribut einen Labelnamen besitzt oder nicht. Break und continue ohne Label beziehen sich immer auf die nächste höhere Schleife. If Blöcke werden dabei ignoriert. Bei jeder Schleife wird ein neues Loop Objekt erzeugt, welches bei der Übersetzung des Schleifenbodys dem BlockScanner übergeben wird. Dem Loop Objekt wird beim Konstruktor Aufruf übergeben, welcher Block bei einem break und bei einem continue folgt. Tritt eine solche Anweisung auf, kann der folgende Block aus dem Loop ausgelesen werden. Anders sieht es bei break und continue mit einem Label aus. Diese treten nur innerhalb eines 29 labeled Block auf. Dort können sie auch in mehreren Schleifen verschachtelt vorkommen. Die Funktion addLabel(...) speichert ein Label mit dem dazugehörigen break Block. Dieser ist immer der Block, der das Ende des labeled Block darstellt (siehe Kap. 3.3.10). Komplizierter ist die Übersetzung von continue. Der beim continue folgende Block kann erst bei der Übersetzung der dazugehörigen Schleife gesetzt werden. Zuständig für das setzen des continue Block sind die statischen Funktionen setCurrentLabel(String) und addContinueLabel(BasicBlock). Das aktuelle Label wird mit setCurrentLabel(...) beim Übersetzen des labeled Block gesetzt. Wenn der labeled Block zu einer Schleife gehört, wird bei der Übersetzung der Schleife mit addContinueLabel(...) der continue Block gesetzt. Das folgende Beispiel zeigt ein break und ein continue in einer Schleife ohne Label. Java Code JBCG Programmfluss (Funktionsansicht) public void f() { while(i < 5) { if(i == 4) continue; if(i == -5) break; } } 30 3.3.10 Labeled Blocks Labeled Blocks werden im Java Zwischencode als JCLabeledStatement Objekt dargestellt. Das Objekt hat das Attribut body, welches den Inhalt des labeled Block darstellt. Dieser Inhalt muss nicht immer ein Block sein, sondern kann ein beliebiges Objekt vom Typ JCStatement sein. Im JBCG Programmfluss wird ein labeled Block durch zwei SequenceBlocks dargestellt. Der erste ist der labeled Block selbst, der zweite stellt den nachfolgenden Block dar. Labeled Block lassen sich in zwei Arten unterscheiden: Erstens einzelne Blöcke und zweitens in Schleifen integrierte Blöcke. Alleinstehende Blocks können break Anweisungen enthalten, die den Block beenden. Labeled Blocks in Schleifen können sowohl break als auch continue Anweisungen enthalten. Kompliziert ist die Auswertung von continue Anweisungen innerhalb einer labeled Block-Schleife, da der zu verweisende Block von der Art der Schleife anhängt. In einer For Schleife wird auf den step Block verwiesen, bei allen anderen auf den continue Block. Aus diesen Grund wird der continue Block erst bei der Auswertung des Schleife gesetzt (siehe auch Kap. 3.3.9). Folgende Grafik zeigt einen labeled Block ohne Schleife. Java Code JBCG Programmfluss (Funktionsansicht) public void f() { label: { i++; if(i < 5) break label; i++; } } 31 3.3.11 Try und catch Strukturen Es gibt keine Möglichkeit, try und catch Strukturen im JBCG Zwischencode darzustellen. Die Bibliothek wurde in diesen Punkt auch nicht erweitert, so dass try und catch Anweisungen auch nicht korrekt übersetzt werden. Beim Auftreten einer try und catch Struktur wird der try Block und der finalizer Block in normale SequenceBlock Objekte übersetzt. Der catch Block wird ignoriert. Auch throw Anweisungen werden ignoriert. 3.3.12 return Anweisungen Tritt eine return Anweisung auf, ist die Funktion an dieser Stelle beendet. Es wird ein neuer ReturnBlock erzeugt und der aktuelle Block verweist auf ihn. Return Anweisungen werden im Java Zwischencode als JCReturn dargestellt. Das Attribut expr stellt einen möglichen Rückgabewert dar. Ist ein Rückgabewert vorhanden, wird er vom ExpressionScanner übersetzt. Der Wert kann allerdings nicht direkt zurückgegeben werden. Das Problem hierbei sind Postfix Anweisungen, die möglicherweise in der Rückgabe Funktion enthalten sein können. Im Fall einer Postfix Anweisung müsste eine Anweisung nach dem Ende der Funktion ausgeführt werden, was nicht möglich ist. Um das Problem zu umgehen, wird bei jedem Auftreten von return Anweisungen, mit einem Rückgabewert, eine temporäre lokale Variable angelegt und in ihr der Rückgabewert gespeichert. Zurückgegeben wird im return dann eine LoadOperation auf die temporäre Variable. Java Code JBCG Programmfluss (volle Blockansicht) public int f() { return i++; } 32 3.3.13 Weitere wichtige Funktionen des BlockScanners Der BlockScanner hat noch einige wichtige Funktionen, die für die Übersetzung besonders wichtig sind: addPreExpression(...) und addPostExpression(...) Soll eine Anweisung übersetzt werden, ruft der BlockScanner die Funktion scanTree(...) des ExpressionScanners auf. Diese gibt ein Expression Objekt zurück. In machen Fällen kann aber eine Anweisung nicht in ein einziges Expression Objekt übersetzt werden. Wenn das der Fall ist, werden mit den Funktionen addPreExpression(...) und addPostExpression(...) weitere Anweisungen in den Block eingefügt. Sie können dabei entweder vor oder nach der eigentlichen Anweisung eingefügt werden. createTmpIdentifer(...) In einigen Fällen ist es nötig, temporäre, lokale Variablen, die nicht im Java Code vorhanden sind, zu einer Funktion hinzuzufügen. Dies geschieht mit der Funktion createTmpIdentifer(...). Ihr wird ein TypeDesc Objekt übergeben, das den Typen der temporären Variable angibt. Mit temporär ist allerdings nicht gemeint, das die Variable nur in einem Teil der Funktion besteht. Sie ist wie alle lokale Variablen in der gesamten Funktion gültig. Sie wird allerdings nur innerhalb eines kleines Bereiches benutzt und hat daher einen temporären Charakter. Die Variable bekommt einem Namen, der sich aus „$$__TmpVar$“ und einem Index zusammensetzt. Zurückgeben wird ein Identifier Objekt, welches die Variable darstellt. addIf(...) Mit dieser Funktion können neue Ifs in den Programmfluss eingefügt werden. Die Funktion wird nicht dazu genutzt, um die im Java Code beschriebenen Ifs hinzuzufügen. Sie wird bei der Übersetzung von booleschen Operationen und konditionalen Werte gebraucht. Übergeben wird der Funktion eine Bedingung sowie zwei Expressions. Mit Hilfe der Funktion createTmpIdentifer(...) wird eine neue Variable erzeugt. Der ConditionScanner erzeugt aus der Bedingung einen ConditionBlock. Ist die Bedingung wahr, wird der Variablen im true Block des ConditionBlocks die erste Expression zugewiesen. Ist die Bedingung falsch, wird der Variablen die zweite Expression zugewiesen. Die erzeugte Struktur lässt sich durch folgenden Code beschreiben: <Typ> $$__TmpVar$0; if(<Bedingung>) { $$__TmpVar$0 = <Expression1>; } else { $$__TmpVar$0 = <Expression2>; } 33 Zurückgegeben wird das Identifier Objekt, welches die „$$__TmpVar$“ Variable darstellt. Das Identifier kann dann als normale lokale Variable in Anweisungen verwendet werden. 3.4 Übersetzung der Anweisungen Das Übersetzen von Anweisungen erfolgt im ExpressionScanner. Er ist vom TreeScanner des Java Compiler abgeleitet und durchläuft ein JCTree nach dem Scanner Pattern. Der ExpressionScanner hat die Funktion scanTree(...), welche ein JCTree Objekt übersetzt und ein Expression Objekt zurück liefert. 3.4.1 Variablen und konstante Werte Werte können entweder eine Konstante oder eine Variablen sein. Konstante haben einen festen Wert wie eine Zahl oder ein String. In der JBCG Bibliothek gibt es die Klassen IntergerConstant, LongConstant, FloatConstant, DoubleConstant und StringConstant, welche konstante Werte symbolisieren. Auffällig ist, dass es keine Klassen zur Darstellung von konstanten byte, short und char Werten gibt. Gründ hierfür ist die Nähe der JBCG Bibliothek zur Java Maschinensprache. Die Java Maschinensprache speichert alle numerischen Typen, die kürzer als Integer sind, als Integer Wert ab. Bei der Übersetzung des Java Zwischencode werden deshalb alle konstanten byte, short und char Werte in IntergerConstant Objekte übersetzt. Auch boolesche Werte werden in IntergerConstant Objekte übersetzt. Der Wert true erhält den Integer Wert „1“ und ein false eine „0“. Übersetzt werden alle Konstanten Werte in der Funktion visitLiteral(...) des ExpressionScanner. Die Funktion bekommt ein JCLiteral Objekt übergeben welches die Attribute typetag und value besitzt. typetag ist ein int, der den Typ des Wertes darstellt. Der Wert ist in value gespeichert. Der Typ von value ist Object. Um Werte auszulesen, muss value in das Objekt des primitiven Typs gecastet werden (z.B. in ein Integer Objekt für ein int). Lokale Variablen werden in der JBCG Bibliothek als Identifier Objekte dargestellt. Sie sind entweder als Parameter oder innerhalb der Funktion deklariert worden. Im Java Zwischencode sind lokale Variablen als JCIdent dargestellt. Anhand des Symbols kann das Identifier Objekt aus der Funktion gesucht werden. Ist das Identifier Objekt gefunden, wird ein neues LoadOperation Objekt erzeugt, dem das Identifier übergeben wird. Ein JCIdent muss allerdings nicht immer eine lokale Variable sein. Es steht für alle möglichen Elemente, die einen Namen haben. Dazu gehören Klassen, Methoden, Konstruktoren usw. Über 34 das Symbol kann herausgefunden, werden um welches Element es sich handelt und ob es lokal oder global ist. Globale Variablen werden in der JBCG Bibliothek als Field Objekte dargestellt. Im Java Zwischencode sind sie entweder als JCFieldAccess oder JCIdent vorhanden. Ein JCFieldAccess beschreibt einen Zugriff auf ein Attribut einer Variablen. Die Variable, die das Attribut enthält, wird durch selected dargestellt. Dabei kann selected wiederum ein JCFieldAccess oder ein JCIdent sein. Auch Aufrufe über this werden als JCFieldAccess repräsentiert. So entsteht eine Zugriffsbaum, an dessen Blättern immer ein JCIdent steht. Das JCFieldAccess und das JCIdent haben das Attribut sym, welches das Symbol Objekt des aktuellen Zugriffs darstellt. Anhand dieses Symbols kann eine globale Variable identifiziert werden. Zum Zeitpunkt der Übersetzung sind schon alle globalen Variablen in der GlobalClassTable Datenbank gespeichert. Das dazugehörige Field Objekt wird anhand des Symbol Objektes in der Datenbank gesucht. Wird kein Field Objekt gefunden bedeutet dies, dass die Variable zu einer Klasse gehört, die nicht mit kompiliert wird. In diesen Fall wird ein neues Field Objekt erzeugt. Um den Zugriff auf die Variable im JBCG Zwischencode darzustellen, wird ein LoadField erzeugt. Das LoadField wird vom ExpressionScanner zurückgegeben. Ähnlich wie der Zugriff auf globale Variablen erfolgt der Zugriff auf Arrays. Im Java Zwischencode werden Array Zugriffe als JCArrayAccess dargestellt. Das JBCG Gegenstück dazu ist das LoadFromArray Objekt. Das JCArrayAccess hat die Attribute index und indexed, welche den Index des Zugriffes und das Array, auf das zugegriffen wird, darstellen. Beide Attribute werden durch einen neuen Aufruf des ExpressionScanners übersetzt und dem LoadFromArray übergeben. Folgende Grafik gibt ein Beispiel für eine Verkettung von Zugriffen. Die Variable stadt ist dabei eine lokale Variable. 35 Java Code int i = stadt.haus[5].grundstueck.flaeche; JBCG Zwischencode (Expressionansicht) 36 3.4.2 Zuweisungen und Zuweisungsoperatoren Im Java Zwischencode gibt es zwei Arten von Zuweisungen: normale Zuweisungen und Zuweisungsoperatoren. Normale Zuweisungen werden durch JCAssign dargestellt und in der Funktion visitAssign(...) übersetzt. Das JCAssign Objekt hat zwei Attribute. Das Attribut lhs steht für die linke Seite der Zuweisung und rhs für die rechte Seite. Bei einer normalen Zuweisung können drei Fälle auftreten: • Es wird einer lokalen Variable etwas zugewiesen Die Zuweisung wird in ein StoreOperation Objekt umgewandelt. Das lhs Attribut ist vom Typ JCIdent und repräsentiert eine lokale Variable. Anhand des Symbols des JCIdent kann das Identifier Objekt aus der aktuellen Funktion geladen werden. Das Identifier wird als LocalVar in der StoreOperation gesetzt. • Es wird einen Array etwas zugewiesen In diesen Fall wird die Zuweisung in ein StoreToArray Objekt übersetzt. Das lhs Attribut ist vom Typ JCArrayAccess, welches die Attribute index und indexed hat. Beide werden durch einen neuen Aufruf des ExpressionScanners übersetzt. indexed stellt dabei den Zugriff auf ein Array dar und index den Index, mit dem der Zugriff geschieht. • Es wird einer globalen Variable etwas zugewiesen Das lhs Attribut ist vom Typ JCIdent oder JCFieldAccess. Die Zuweisung wird in ein StoreField Objekt übersetzt. Das dazugehörige Field Objekt wird, mit Hilfe des Symbols und der GlobalClassTable, aus der jeweiligen Klasse herausgesucht. Wird diese Klasse, in der die Variable definiert ist, nicht mit übersetzt, kann kein Field Objekt gefunden werden und es wird ein Neues erzeugt. Die rechte Seite der Zuweisung bildet den Wert, der zugewiesen wird. Der kann aus allen möglichen Operationen, Funktionsaufrufen, Variablen oder konstanten Werten bestehen. Ein neuer Aufruf des ExpressionScanners übersetzt den Wert. Er wird als value Attribut den Store Objekten zugewiesen. Zuweisungsoperatoren werden im Java Zwischencode mit JCAssignOp Objekten dargestellt. Sie haben die gleichen Attribute wie eine normale Zuweisung und das zusätzliche Attribut operator, welches einen binären Operator definiert. Die linke Seite wird genauso übersetzt wie bei einer normalen Zuweisung. Die rechte allerdings besteht aus der vom operator definierten binären Operation und zwei Operanten. Der eine Operand ist ein Load auf die Variable der linken Seite, der andere die Expression, die die Übersetzung des rhs Attributs zurück liefert. Es 37 können alle in Java definierten Zuweisungsoperatoren übersetzt werden, diese sind: Zuweisungsoperator Definiert für += byte, char, short, int, long, float, double, String -= byte, char, short, int, long, float, double %= byte, char, short, int, long, float, double /= byte, char, short, int, long, float, double *= byte, char, short, int, long, float, double >>>= byte, char, short, int, long <<= byte, char, short, int, long >>= byte, char, short, int. long |= byte, char, short, int, long &= byte, char, short, int, long ^= byte, char, short, int, long Die Übersetzung der Operation erfolgt auf die gleiche Weise wie die Übersetzung bei einer binären Operation(siehe Kap. 3.4.3). Folgendes Beispiel zeigt den Zuweisungsoperator „+=“: Java Code JBCG Zwischencode (Expressionansicht) public void f() { a += 5; } 38 Problematisch ist es, wenn Zuweisungen innerhalb einer anderen Anweisung definiert oder verschachtelt sind. Zum Beispiel bei folgenden Anweisungen: a = 5 + (b = 7); f(a=9); a = b = c = 9; Diese Zuweisungen müssen getrennt von der Anweisung dem aktuellen Block hinzugefügt werden. Zuständig hierfür ist die statische Funktion checkValue(...) aus der Klasse Translator. Sie prüft, ob ein Expression Objekt vom Typ StoreField, StoreOperation oder StoreToArray ist. Ist dies der Fall, wird die Store Expression als Preexpression dem aktuellen Block hinzugefügt und ihr value Attribut weiterverarbeitet. Hat die Expression keinen dieser Typen, wird die Expression selbst weiter verwendet. Bei jeder Zuweisung, jeder Operation und jedem Funktionsaufruf oder ähnlichem werden die Werte mit der checkValue(...) Funktion getestet und mögliche enthaltene Zuweisungen ausgelagert. 3.4.3 Operationen Grundlegend kann man drei Arten von Operationen unterscheiden. Diese sind binäre, unäre und boolesche Operationen. Zwar können boolesche Operationen auch sowohl binär und unär sein, allerdings werden sie komplett in Programmfluss übersetzt, daher gibt es im JBCG Zwischencode keine boolesche Operationen mehr. Die Übersetzung von boolesche Operatoren wird im Kapitel 3.4.7 erklärt. Binäre Operationen werden in der Funktion visitBinary(...) übersetzt. Ihr wird ein JCBinary Objekt übergeben. Das Objekt hat die Attribute lhs und rhs, welche den linken und den rechten Operanden darstellen. Das Attribut operator ist das Symbol des Operators. Zur Übersetzung wird ein neues BinaryOperation Objekt erzeugt. Der linke und der rechte Operand werden durch einen neuen Aufruf des ExpressionScanners übersetzt und dem BinaryOperation als linker und rechter Wert gesetzt. Um die Operation festzulegen, besitzt die Klasse BinaryOperation das enum Operation, welches alle möglichen binären Operationen enthält. Die richtige Operation wird anhand des Operationssymbols (z. B. „+“ oder „*“) und des Operationstypen identifiziert. Da nicht alle in Java definierten binären Operationen im enum Operation enthalten waren, wurde während der Arbeit das enum erweitert, so dass nun alle Operationen abgedeckt sind (mehr dazu Kap. 3.5.2). Folgende Operationen sind im enum enthalten: 39 Operation Definiert für die Typen + int, long, float, double - int, long, float, double * int, long, float, double / int, long, float, double % int, long, float, double & int, long ^ int, long | int, long << int, long >> int, long >>> int, long Für die Typen byte, short und char sind keine Operationen definiert. Alle diese Typen werden vom Java Maschinencode als int Werte angesehen. Für diese Typen werden deshalb int Operationen verwendet. Die booleschen Operationen werden in Programmfluss übersetzt. Für sie sind daher keine Operatoren definiert. Ein Problem bei der Übersetzung von binären Operation ist die Verkettung von Strings. Im Java Zwischencode wird die Verkettung zwei String durch den „+“ Operator als binäre Operation dargestellt und durch ein JCBinary repräsentiert. Der Maschinencode kennt aber keine Operation, die zwei Strings verkettet. Daher wird eine solche Operation in einen Aufruf der concat(...) Funktion übersetzt. Dabei wird die concat(...) Funktion des linken Operanten aufgerufen und der rechte Operand wird übergeben. Nun ist es aber in Java auch möglich, einen String mit einer Zahl oder mit einem Objekt, welches kein String ist, zu verketten. Da die concat(...) Funktion nur Stringobjekte als Parameter zulässt und sie auch nur von Stringobjekten aus aufgerufen werden kann, muss vor dem Aufruf überprüft werden, ob es sich bei den Operanden wirklich um Strings handelt. Ist dies nicht der Fall, müssen sie in Stringobjekte umgewandelt werden. Dies geschieht in der Funktion expressionToString(...) der Translator Klasse. Sie überprüft Expression Objekte auf ihre Typen. Ist der Typ ein numerischer primitiver Typ, wird die statische toString(...) Funktion von der zugehörigen Objektklasse aufgerufen. Handelt es sich beispielsweise um ein int, wird die toString(int) Funktion der Klasse Integer aufgerufen. Ist der Typ nicht primitiv, sondern ein Objekt, wird die toString() Funktion des Objektes aufgerufen. Folgende Grafik gibt ein Beispiel zu einer Verkettung von Strings: 40 Java Code String s = "Hallo" + 7.8; JBCG Zwischencode (Expressionansicht) Fehler entstehen hierbei bei der Verkettung von einem String und einem konstanten booleschen Wert. Zum Beispiel der Ausdruck „Hallo“ + true . Das true wird in ein IntergerConstant Objekt mit den Wert „1“ übersetzt. Daher würde bei der Verkettung der String „Hallo1“ entstehen. Bei einem Load auf eine boolesche Variable kommt dieser Fehler allerdings nicht vor, da die Variable weiß, dass sie den Typ boolean hat. Unäre Operationen werden im Java Zwischencode als JCUnary Objekt abgebildet. Übersetzt werden sie in der Funktion visitUnary(...) im ExpressionScanner. Das Objekt hat die Attribute arg und operator. Das operator Attribut ist das Symbol Objekt des Operators und arg ist der Operand. Folgende Tabelle zeigt alle unären Operatoren, die im Java Zwischencode definiert sind: Operationen Definiert für ++ int, long, float, double -- int, long, float, double + int, long, float, double - int, long, float, double ~ int, long ! boolean 41 In der JBCG Bibliothek ist allerdings nur die unäre Operation „-“ definiert. Alle anderen Operationen werden in anderer Form übersetzt. Als einzige unäre Operation wird die „-“ Operation in ein UnaryOperation Objekt übersetzt. Wie auch bei dem BinaryOperation Objekt hat auch UnaryOperation ein Operation enum. Das enum besitzt zwar nur einen Operator, dieser ist aber für die vier Typen int, long, float und double definiert. Der passende Operator wird anhand des Typen identifiziert. Das arg Attribut wird durch den ExpressionScanner übersetzt und als expression Attribut der UnaryOperation zugewiesen. Die beiden Operationen „++“ und „--“ sind Pre- oder Postfix Operationen. Diese Operatoren werden in zwei Teilen übersetzt. Der eine Teil besteht aus einem Store Objekt (StoreField, StoreOperation oder StoreToArray), das als value ein BinaryOperation bekommt. Das BinaryOperation Objekt hat als linken Operanten einen Load auf die Variable und als rechten eine Konstante mit den Wert „1“. Als Operation wird entweder „+“ oder „-“ des jeweiligen Typen gesetzt. Dieser Teil stellt die Erhöhung bzw. die Verringerung der Variable um eins dar. Handelt es sich um eine Präfix Operation, wird dieser Teil als Preexpression dem BlockScanner hinzugefügt. Ist es ein Postfix, wird er als Postexpression hinzugefügt. Der zweite Teil ist ein Load auf die Variable. Das Load wird vom ExpressionScanner zurückgegeben und kann auf höhere Ebene als Bestandteil einer anderen Expression verwendet werden. Die folgende Grafik zeigt den Aufbau einer Präfix Operation innerhalb einer Anweisung. Java Code a = b + (++c); JBCG Zwischencode (Expressionansicht) Der unäre Operator „+“ wird bei der Übersetzung ignoriert, da er den Wert nicht verändert. Der Operator „~“ wird in die binäre Operation „^“ übersetzt. Dabei wird der zweite Operand auf eine Konstante mit dem Wert „-1“ gesetzt. 42 Die „!“ Operation stellt eine boolesche Operation dar welche in Programmfluss übersetzt wird. Boolesche Operationen werden im Kapitel 3.4.7 beschrieben. 3.4.4 Typumwandlungen Im Java Zwischencode sind Typumwandlungen als JCTypeCast enthalten. Dabei wird nicht zwischen primitiven und Objekt Typen unterschieden. Das JCTypeCast Objekt hat die beiden Attribute clazz und expr. Das clazz Attribut stellt den Typ dar, in den ein Wert umgewandelt werden soll und expr ist der Wert, der gewandelt werden soll. In der JBCG Bibliothek wird zwischen dem Cast von primitiven Typen und von Objekt Typen unterschieden. Primitive Typumwandlungen werden durch die Klasse Converter abgebildet. Für die Umwandlung von Objekt Typen gab es keine Klasse in der Bibliothek. Darum wurde sie um die Klasse ConvertObject erweitert (siehe Kap. 3.5.1). Im Fall einer primitiven Typumwandlung wird das Attribut expr durch einen neuen Aufruf des ExpressionScanners übersetzt und als expression Attribut dem Converter Objekt übergeben. Dem Converter Objekt muss noch mitgeteilt werden, um welche Umwandlungsoperation es sich bei diesem Cast handelt. Im enum Converter.Operation sind alle primitiven Umwandlungen definiert. Welche davon die richtige ist, kann anhand des Typen des expr Attributs und des Typen, der im clazz Attribut definiert ist, erkannt werden. Auch das Converter.Operation enum wurde um einige Operationen erweitert (mehr dazu Kap. 3.5.2). Nun können alle primitiven Umwandlungen durch das Converter Objekte abgebildet werden. Folgende Operationen sind definiert: Name von... nach... I2B int byte I2S int short I2C int char I2L int long I2F int float I2D int double L2I long int L2F long float L2D long double F2I float int F2L float long F2D float double 43 D2I double int D2L double long D2F double float Verlustfreie Typumwandlungen müssen im Java Code nicht angegeben werden (z.B int zu long oder float zu double). Auch im Java Zwischencode fehlen solche Umwandlungen. Im Maschinen Code müssen diese Werte allerdings umgewandelt werden. Der Compiler scheint die Umwandlungen in der Codegenerierungsphase automatisch hinzuzufügen. Um aus dem JBCG Zwischencode korrekten Maschinen Code zu erzeugen, müssen auch bei der Übersetzung solche Typumwandlungen hinzugefügt werden. Dies erledigt die Funktion checkType(...) vom Translator. Bei jeder Zuweisung, jeder Operation und jeden Aufruf von Funktionen oder Konstruktoren werden primitive Werte daraufhin getestet, ob eine verlustfreie Typumwandlung erforderlich ist. Ist dies der Fall, wird ein neues Converter Objekt eingefügt. Java Code JBCG Zwischencode (Expressionansicht) int a = 0; long b = 1; public void f() { b = a; } Die Typumwandlung von Objekttypen konnten in der JBCG Bibliothek bisher nicht dargestellt werden. Darum wurde sie auch hier erweitert und die Klasse ConvertObject hinzugefügt (siehe Kap. 3.5.1). Die Klasse benötigt als Attribut den Wert, der umgewandelt werden soll sowie den Typ, in den umgewandelt werden soll, in Form eines TypeDesc Objektes. Der Wert wird aus dem expr Attribut des JCTypeCast übersetzt und der Typ aus dem clazz Attribut. 44 3.4.5 Funktionsaufrufe In der JBCG Bibliothek werden Funktionsaufrufe mit einem Call Objekt abgebildet. Im Java Zwischencode sind sie als JCMethodInvocation definiert. Allerdings werden auch Konstruktoraufrufe als JCMethodInvocation dargestellt (siehe Kap. 3.4.6). Daher muss jedes dieser Elemente darauf untersucht werden, ob es sich um einen Konstruktor- oder um einen Funktionsaufruf handelt. Das Attribut meth des JCMethodInvocation stellt die Funktion dar, die aufgerufen wird. Bei dem Attribut kann es sich entweder um ein JCIdent Objekt handeln oder um ein JCFieldAccess. Wird die Funktion direkt aufgerufen ohne einen Selector, ist es ein JCIdent (z.B. f()). Bei einem direkten Aufruf ist klar, dass es sich um eine Funktion der eigenen Klasse handeln muss. Ist meth von Typ JCFieldAccess, wird entweder eine statische Funktion einer anderen Klasse aufgerufen (z. B. Math.random()) oder eine Objektmethode (z. B. win.repaint()). Wird die Methode über ein this aufgerufen (z. B. this.repaint()), wird das auch als Aufruf einer Objekt Methode dargestellt. Für die Übersetzung ist das nur für das object Attribut des Call Objektes interessant. Dieses Attribut entscheidet wo, d. h. in welchem Objekt, die Funktion aufgerufen wird. Das object Attribut kann entweder ein Load auf eine Objekt Variable oder ein This Objekt sein. Aufrufe mittels super werden auch durch ein This Objekt dargestellt, nur wird bei super Aufrufen dem This den Typ der Superklasse übergeben. Ist die Funktion statisch, bleibt object undefiniert. Das args Attribut des JCMethodInvocation Objektes ist eine Liste aller Parameter, die der Funktion übergeben werden. Sie werden alle durch einen neuen Aufruf des ExpressionScanners übersetzt und dem Call Objekt als Parameter hinzugefügt. Nun muss nur noch die eigentliche Funktion dem Call übergeben werden. Dies kann entweder ein InternalFunction oder ExternalFunction Objekt sein. Um zu entscheiden, ob die Funktion intern oder extern ist, wird in der GlobalClassTable die richtige InternalFunction gesucht. Wird die Funktion gefunden, wird sie dem Call als function Attribut gesetzt. Wird sie nicht gefunden, wird ein neues ExternalFunction Objekt erzeugt. Genau aus diesen Grund wird die GlobalClassTable Datenbank schon vor der eigentlichen Übersetzung erstellt. Ohne diese Datenbank wäre es nicht möglich zu entscheiden, ob es sich bei einem Aufruf um eine interne oder eine externe Funktion handelt. Hier ein Beispiel eines verschachtelten Aufrufs. 45 Java Code win.setLocation(getX(), this.getY()); JBCG Zwischencode (Expressionansicht) Ein Problem bei der Entscheidung, ob eine Aufruf intern oder extern ist zeigt dieses Beispiel: class Test2 { public Test2(boolean b) { Object a; if(b) a = new Object(); else a = new B(); a.toString(); } } class B { public String toString() { return "B"; } } Ob der Aufruf der toString Funktion intern oder extern ist, kann bei der Übersetzung nicht entschieden werden. Zur Zeit wird eine ExternalFunction Objekt erzeugt, welches die toString(...) von Object darstellt. Wenn aber die Variable a vom Typ B ist, müsste eine InternalFunction Objekt der toString(...) Funktion von B geladen werden. 46 3.4.6 Konstruktoraufrufe Konstruktoraufrufe werden im Java Zwischencode, genau wie Funktionsaufrufe, als JCMethodInvocation Objekte dargestellt. Über die Funktion isConstructor() des sym Attribut kann abgefragt werden, ob es sich um einen Konstruktor handelt. In der JBCG Bibliothek werden Konstruktoraufrufe als ConstructorCall Objekte dargestellt. Das ConstructorCall Objekt braucht ein Expression Objekt, welches das Objekt darstellt, in dem der Konstruktor aufgerufen wird. Konstruktoraufrufe unterscheiden sich in zwei Arten: Handelt es sich beim Konstruktoraufruf um einen Aufruf mittels this oder super, wird dem ConstructorCall ein neues This Objekt zugewiesen. In Fall eines super Aufrufs bekommt das This den Typ der Superklasse. Wird der Konstruktor aufgerufen, weil ein neues Objekt erzeugt werden soll (z. B. new Point()), muss ein NewObject erzeugt und dem ConstructorCall zugewiesen werden. Die übergebenen Parameter sind im Attribut args des JCMethodInvocation Objektes gespeichert und werden durch einen neuen Aufruf des ExpressionScanners übersetzt. Die übersetzten Expressions werden als Parameter dem ConstructorCall Objekt hinzugefügt. Wird ein neues Objekt erzeugt, wird dieses Objekt meistens einer Variable zugewiesen, oder einer Funktion übergeben. Ein Problem hierbei ist, dass der ConstructorCall keinen Wert zurückliefert. Darum wird der Konstruktoraufruf in zwei Teile übersetzt. Der erste Teil ist das ConstructorCall. Es wird als Preexpression dem aktuellen Block hinzugefügt. Der zweite Teil ist das NewObject, welches als Expression in einer Zuweisung, einem Funktionsaufruf oder ähnlichem verwendet werden kann. Java Code JBCG Zwischencode (volle Blockansicht) Point p = new Point(); 3.4.7 Konditionale Werte und booleschen Operationen Im Java Maschinen Code gibt es keine booleschen Operatoren. Darum werden konditionale Werte und boolesche Operationen in Programmfluss übersetzt. Ihre Übersetzungen ähneln sich sehr. Es wird eine temporäre lokale Variable und ein neues If erzeugt. Die temporäre Variable 47 wird mit der Funktion createTmpIdentifer(...) des BlockScanners erzeugt (siehe Kap. 3.3.13). Das If erzeugt die Funktion addIf(...) (siehe Kap. 3.3.13). In Abhängigkeit der Bedingung, wird der Variable im If ein Wert zugewiesen. Ziel diese Vorgehens ist es, aus z. B. solchem Code: boolean b = a > c; folgenden zu generieren: public void f() { boolean $$__TmpVar$0; if(a > c) $$__TmpVar$0 = true; else $$__TmpVar$0 = false; boolean b = $$__TmpVar$0; } Eine konditionaler Wert wird im Java Zwischencode als JCConditional Objekt dargestellt. Das Objekt hat drei Attribute. Das Attribut cond ist eine Bedingung. Die Attribute truePart und falsePart sind die Werte, die zurückgegeben werden, falls die Bedingung wahr bzw. falsch ist. Der Typ der temporären Variable wird auf den Typ der konditionalen Werte gesetzt. Das neue If bekommt das cond Attribut als Bedingung. Ist die Bedingung wahr, wird die temporäre Variable auf den truePart Wert gesetzt. Ist sie falsch, wird die Variable auf den falsePart gesetzt. Der ExpressionScanner gibt nur noch eine LoadOperation auf die temporäre Variable zurück. Boolesche Operationen werden im Java Zwischencode als normale Operationen dargestellt. Es gibt mehrere binäre und einen unären booleschen Operator. Sie werden daher als JCBinary und JCUnary Objekt dargestellt und rufen auch die selben visit Funktionen auf wie numerische Operationen (siehe 3.4.3). Jede Operation muss daher auf ihren Rückgabewert geprüft werden. Ist dieser boolean, wird die Operation in Programmfluss übersetzt, ansonsten nicht. Folgende booleschen Operatoren sind in Java definiert: Operator Bedeutung Binär oder unär == gleich binär != ungleich binär < kleiner als binär > größer als binär <= kleiner oder gleich als binär >= größer oder gleich als binär %% Verundung zweier Bedingungen binär || Veroderung zweier Bedingungen binär ! Negation unär Im Fall einer binären Operation bekommt die temporäre Variable immer den Typ boolean. Das 48 If bekommt als Bedingung die boolesche Operation. Ist die Bedingung wahr, wird die temporäre Variabel auf true gesetzt, ansonsten auf false. Der ExpressionScanner gibt eine LoadOperation auf die temporäre Variable zurück. 3.5 Erweiterungen der JBCG Bibliothek Da die JBCG Bibliothek anfangs nur für die Sprache Slic, die eine Teilsprache von Java ist, entworfen wurde, konnten nicht alle Sprachelemente von Java im JBCG Zwischencode dargestellt werden. Während dieser Arbeit wurde die Bibliothek an vielen Stellen erweitert, um mehr Sprachelemente abzudecken. Die zwei folgenden Kapitel beschreiben alle Änderungen der JBCG Bibliothek. 3.5.1 Neue Klassen Während der Arbeit wurden der JBCG Bibliothek einige neue Klassen hinzugefügt, diese sind: de.unihannover.psue.jbcgen.basicblock.SwitchBlock Die Klasse SwitchBlock erweitert die Klasse BasicBlock. Sie stellt eine switch Struktur dar. Über die Methode setSelector(Expression) kann ein Expression Objekt übergeben werden, welches den Selector darstellt. Neue case Fälle können mit der Funktion addCase(...) hinzugefügt werden. Übergeben werden muss dazu das case Label in Form eines Expression und ein BasicBlock, der die Anweisungen dieses Falles enthält. Der default Fall wird mit der Funktion setDefault(...) gesetzt. Hier muss ein BasicBlock übergeben werden. de.unihannover.psue.jbcgen.expression.CovertObject Ein ConvertObject Objekt repräsentiert eine Typumwandlung eines Objektes. Primitive Typumwandlungen werden weiterhin durch die Klasse Converter dargestellt. ConvertObject ist von der Expression Klasse abgeleitet. Ihr muss mit setExpression(...) eine Expression gesetzt werden, die den umzuwandelnden Wert enthält. Ein TypeDesc Objekt beschreibt, in welchem Typ die Expression umgewandelt werden soll. 49 de.unihannover.psue.jbcgen.expression.TypeCheck Die TypeCheck Klasse ist ebenfalls von Expression abgeleitet. Sie stellt eine Typprüfung mittels instanceof dar. Ihr muss eine Expression (setValue(...)) und ein TypeDesc (setType(...)) übergeben werden. Die Expression ist der zu überprüfende Wert und das TypeDesc der Typ, auf den geprüft wird. de.unihannover.psue.jbcgen.expression.NullConstant Da es in der JBCG Bibliothek keine Möglichkeit gab, die Konstante null darzustellen, wurde die Klasse NullConstant hinzugefügt. Sie ist eine Unterklasse von Expression. Ihr können keine Werte übergeben werden. 3.5.2 Erweiterungen der vorhandenen Klassen Auch schon bestehende Klassen der JBCG Bibliothek wurden erweitert oder geändert. Folgende Klassen haben sich verändert: de.unihannover.psue.jbcgen.basicblock.BasicBlock Der Klasse BasicBlock wurde das String Attribut text hinzugefügt. Das Attribut dient nur zu Präsentationszwecken. Bei der graphischen Darstellung der Blöcke wird der Text innerhalb der Blöcke angezeigt. So ist leichter ersichtlich, welcher Block aus welcher Struktur entstanden ist. de.unihannover.psue.jbcgen.classfile.JBCGClass Die JBCGClass Klasse wurde an zwei Stellen erweitert: Erstens wurde ihr das neue Attribut interfaceList hinzugefügt. Dieses Attribut ist eine Liste von TypeDesc Objekten, welche alle Interfaces enthält, die von der Klasse implementiert werden. Außerdem kam das Attribut staticBlockFunction hinzu. Das Attribut ist ein InternalFunction Objekt und es enthält den statischen Block einer Klasse (siehe Kap. 3.2.4). Das Attribut kann nicht gesetzt werden. Es wird schon im Konstruktor implementiert und kann über die Funktion getStaticBlockFunction() abgefragt werden. Der InternalFunction kann dann ein BasicBlock zugewiesen werden, der den statischen Block repräsentiert. de.unihannover.psue.jbcgen.expression.BinaryOperation Dem enum BinaryOperation.Operation wurden einige Operationen hinzugefügt. Diese sind: 50 Name Operation Typ Name Operation Typ IREM % int IOR | int LREM % long LOR | long FREM % float ISHL << int DREM % double LSHL << long IAND & int ISHR >> int LAND & long LSHR >> long IXOR ^ int IUSHR >>> int LXOR ^ long LUSHR >>> long Damit sind nun alle im Java Maschinencode definierten binären Operationen abgedeckt. de.unihannover.psue.jbcgen.expression.Converter Auch das enum Converter.Operation wurde um fehlende Typumwandlungen erweitert. Alle im Java Maschinencode enthaltenen primitiven Umwandlungsoperationen sind im enum enthalten. Die neuen Operationen sind: Name Von Typ Nach Typ Name Von Typ Nach Typ I2L int long L2D long double I2B int byte L2F long float I2C int char L2I long int I2S int short de.unihannover.psue.jbcgen.expression.NewObject In der NewObject Klasse wurde das Attribut dimSize verändert. Es ist nun kein Array vom Typ int, sondern vom Typ Expression. Damit ist es jetzt möglich, ein Array zu erzeugen, dessen Größe erst zur Laufzeit bestimmt wird. de.unihannover.psue.jbcgen.optimizer.TreeWalker Alle neuen Klassen haben eine walk Funktion in der TreeWalker Klasse bekommen. Einige Klassen im Package de.unihannover.psue.jbcgen.optimizer leiten sich von der TreeWalker Klasse ab und müssen nun ebenfalls erweitert werden. Sie können nicht mehr kompiliert werden, bis sie alle neuen walk Funktionen implementieren. 51 3.6 Bewältigung der Komplexität Das größte Problem bei dieser Arbeit ist die hohe Komplexität der Sprache Java. Die vielen verschiedenen Varianten, Anweisungen zu verschachteln und zu kombinieren, machen es zu einer großen Aufgabe, Java komplett zu übersetzen. Es wurde zwar versucht, möglichst alle Varianten zu beachten, aber es wird sicherlich noch viele weitere geben, bei denen noch Fehler auftreten. Es wurden einige Testklassen erstellt, um mögliche Fehler aufzudecken. Die folgende Tabelle enthält alle Testklassen: Klasse Testbereich ClassTest Klassendefinitionen, Vererbung, innere Klassen ConditionStatemanetTest konditionale Werte, boolesche Operationen ConstructiorTest Verschiedene Konstruktoraufrufe DoTest Do While Schleifen ForeachTest Foreach Schleifen ForTest For Schleifen GlobalVarTest Globale Variablen, statische Variablen IfTest If Strukturen InterfaceTest Interfaces LabledBlockTest Blocks mit label LocalVarTest Lokale Variablen MethodTest Verschiedene Funktionen, Funktionsaufrufe OperatorTest Binäre und unäre Operationen, Zuweisungsoperationen WhileTest While Schleifen Durch die Testklassen konnten einige Fehler gefunden und korrigiert werden. Eine andere Maßnahme zur Fehlererkennung ist die Klasse JBCGTreeScanner. Die abstrakte Klasse ist vom TreeScanner abgeleitet und überschreibt alle visit Funktionen. Alle Scanner Klassen sind nicht direkt von TreeScanner abgeleitet, sondern von JBCGTreeScanner. Aufgabe der Klasse ist es, Fehlermeldungen auszugeben, falls in einer Unterklasse eine visit Funktion ausgeführt wird, die nicht von dieser Unterklasse überschrieben wird. Würde z. B. im BlockScanner die Funktion visitAnnotation(JCAnnotation) aufgerufen, würde diese Fehlermeldung ausgegeben werden: Fehler: Folgende Funktion ist nicht überschrieben worden! de.unihannover.psue.jbcgtrans.scanner.BlockScanner.visitAnnotation(Unknown Source) 52 Auf diese Weise kann leicht festgestellt werden, welche Elemente des Java Zwischencode noch nicht bearbeitet werden können. Des weiteren werden während der Übersetzung beim Auftreten von unerwarteten Elementen Fehlermeldungen ausgegeben. Dabei wurde darauf geachtet, dass die Meldung die genaue Funktion und Klasse angibt, wo der Fehler auftrat. Das Programm wird bei einem Fehler nicht beendet, sondern es versucht, die Übersetzung fortzusetzen. Es kann allerdings auf Grund des Fehlers zu Abstürzen kommen. 3.7 Mögliche Erweiterungen Die Übersetzung des Java Zwischencodes ist noch nicht vollständig. Die wichtigste Erweiterung der Übersetzung wäre, die Behandlung von Ausnahmen korrekt zu übersetzen. Dazu muss die JBCG Bibliothek die Möglichkeit bekommen, try Blöcke darzustellen. Funktionen müssen die Möglichkeit haben, throws Werte zu speichern und throw Anweisungen müssen darstellbar sein. Auch die Übersetzung von assert Anweisungen ist noch nicht möglich. Auch hier müsste die JBCG Bibliothek möglicherweise erweitert werden. Die JBCG Bibliothek wurde um einige Klassen erweitert, um mehr Elemente der Java Sprache darzustellen. Die Klassen können zwar die Java Elemente darstellen, besitzen aber keine Funktionalität, um in Maschinen Code umgewandelt zu werden. Da auch die TreeWalker Klasse um einige walk Funktionen erweitert wurde, werden alle älteren Klassen, die vom TreeWalker abgeleitet sind, nicht mehr funktionieren. Auch sie müssen um die neuen walk Funktionen erweitert werden. Ein Ziel der Übersetzung ist es, Optimierungen zu erleichtern. Hierzu müssen Algorithmen entwickelt werden, die den JBCG Zwischencode bearbeiten. Nützlich kann sich hier die GUI erweisen (siehe Kap. 3.9). Mit ihr könnte der optimierte Zwischencode sofort angezeigt und überprüft werden. Als Beispiel wurde ein Algorithmus entwickelt, der leere SequenceBlock Objekte entfernt. Der Algorithmus kann über das Menü der GUI aufgerufen werden. Es wird allerdings keine Garantie auf Korrektheit gegeben. Der Algorithmus soll nur ein Beispiel sein. Des weiteren könnte auch ein Speichern und Laden des Zwischencodes in eine Datei nützlich sein. So wäre es auch möglich, die GUI komplett von der Übersetzung zu entkoppeln. Die Übersetzung würde nur eine Datei erstellen, die den JBCG Zwischencode enthält. Mit der GUI könnte die Datei geöffnet, verändert und wieder gespeichert werden. 53 3.8 Veränderungen am Java Compiler Es wurde versucht, am Java Compiler möglichst wenig zu ändern. Zum einen, weil der Java Compiler sehr komplex und schwer verständlich ist, und zum anderen, um die Übersetzung leichter in eine neue Versionen des Compilers einbauen zu können. Nur an zwei Klassen wurden Änderungen vorgenommen. In der Klasse com.sun.tools.javac.main.JavaCompiler wurde eine globale Instanz der JBCGClassTranslator Klasse angelegt. Im Konstruktor des JavaCompiler wird sie instanziiert. Der Java Zwischencode wird in der Funktion generate(...) abgegriffen und vom JBCGClassTranslator kopiert. Ist der gesamte Zwischencode kopiert, wird in der Funktion compile(...) die translate(...) Funktion des JBCGClassTranslator aufgerufen. Die zweite geänderte Klasse ist com.sun.tools.javac.Main (nicht zu verwechseln mit der Klasse com.sun.tools.javac.main.Main). Hier wurde am Ende der main(...) Funktion der Befehl System.exit(0) auskommentiert. Ein Beenden des Programmes an dieser Stelle, würde auch sofort die GUI schließen. Das Programm wird nun erst beendet, wenn die GUI geschlossen wird. Der Java Code wird noch ganz normal vom Compiler verarbeitet und erzeugt class Dateien. Theoretisch wäre es einfach, die Übersetzung in eine neue Version des Java Compilers einzubauen, wenn sich nicht viel am Java Zwischencode geändert hat. 3.9 Graphische Darstellung Um den JBCG Zwischencode graphisch darzustellen und um die Ergebnisse der Übersetzung zu kontrollieren, wurde eine GUI programmiert. Die GUI besteht aus zwei Ansichten. Eine zeigt den Java Zwischencode mit dem dazugehörigen Programmcode. Die andere Sicht zeigt den JBCG Zwischencode. Die Klassen der GUI liegen im Package de.unihannover.psue.jbcgtrans.gui. Die TreeWindow Klasse ist die Hauptklasse der GUI und von javax.swing.JFrame abgeleitet. Das Fenster wird angezeigt, sobald die Übersetzung abgeschlossen ist. 3.9.1 Darstellung des Java Zwischencode Die Ansicht des Java Zwischencodes besteht aus zwei Teilen. Der linke Teil zeigt den gesamten Java Zwischencode als Baum. Bei einem Klick auf ein Element wird im rechten Teil der Quellcode zu diesen Element und allen Unterelementen angezeigt. So ist leicht ersichtlich, aus 54 welchem Quellcode welche Elemente im Java Zwischencode entstanden sind. Allerdings ist zu beachten, dass der angezeigte Quellcode schon vom Compiler bearbeitet wurde und nicht mit dem Ur-Quellcode übereinstimmt. Abbildung 2: Screenshot der GUI. Ansicht des Java Zwischencodes 3.9.2 Darstellung des JBCG Zwischencode Auch die JBCG Zwischencode Ansicht besteht aus zwei Teilen. Der rechte Teil zeigt den JBCG Programmfluss als Graph in vier Ebenen an. Die vier Ebenen sind: Klassenansicht Alle Klassen werden mit ihren Funktionen und Konstruktoren angezeigt. Konstruktoren werden als Kreis und Funktionen als Rechteck dargestellt. Beim Klick auf eine Funktion oder einen Konstruktor gelangt man zur Funktionsansicht. 55 Eine Klasse mit einen Konstruktor, einer Funktion und dem statischen Block Funktionsansicht Der Programmfluss innerhalb einer Funktion wird angezeigt. Dabei werden alle Blöcke dargestellt. Return- und SequenceBlöcke werden als Rechteck und ConditionBlöcke als Karos angezeigt. An der Beschriftung der Blöcke ist ersichtlich, aus welcher Struktur im Java Zwischencode sie entstanden sind. Die Zahl nach dem '#' Zeichen im SequencBlock gibt an, wieviele Anweisungen in diesem Block enthalten sind. Mit einem Klick auf ein Block gelangt man in die Blockansicht. Funktionsansicht von max() Blockansicht Der Inhalt eines Blockes wird dargestellt. Dabei werden die Root Knoten der Expression Bäume angezeigt. Beim Klick auf ein Expression kommt man in die Expressionansicht. Auch im Programmfluss folgende Blöcke werden dargestellt. Wenn man auf sie klickt, werden diese in der Blockansicht dargestellt. 56 Ein SequenceBlock mit fünf Anweisungen Expressionansicht Der gesamte Baum einer Expression wird angezeigt. Greifen zwei Expressions auf dasselbe Element zu, wird dieses Element nur einmal dargestellt. Dies ist besonders hilfreich bei Optimierungen, da ersichtlich ist, ob ein Ausdruck mehrmals im Zwischencode vorkommt oder nicht. Eine Ausnahme hiervon sind Identifier, Field und InternalFunction Objekte, da diese sowieso nur einmal existieren. Ein Klick auf ein Expression zeigt den Unterbaum dieser Expression an. Expressionansicht einer StoreField Anweisung Um schnell und einfach durch den Zwischencode zu navigieren, wurden Tastatur Befehle eingebaut. Durch das Drücken von Backslash kommt man zurück zur letzten Ansicht. Mit 'c' gelangt man zur Klassenansicht, die 'f' Taste schaltet die volle Sicht an. In der vollen Ansicht wird der gesamte Graph ab den gewählten Knoten angezeigt. Dies kann selbst bei kleinen 57 Programmen schnell unübersichtlich werden. Allerdings ist es nur so möglich, immer zu sehen, welche Expression Objekte mehrfach verwendet werden. Drückt man nochmals auf 'f', kommt man wieder zur normalen Ansicht. Um die Übersichtlichkeit zu erhalten, wurden einige Kanten farbig eingefärbt. Programmflusskanten sind in der Funktionsansicht dunkel grün. Die linke Seite der JBCG Ansicht besteht aus einem Baum. Es werden alle Informationen über die Klassen, Funktionen und Konstruktoren angezeigt. Blöcke und Programmfluss werden hier nicht dargestellt. An dieser Ansicht kann man leicht sehen, ob Variablen richtig und vollständig übersetzt wurden und ob die Rückgabewerte und Modifizierer richtig zugeordnet wurden usw.. Folgende Grafik zeigt den JBCG Zwischencode in der Baum Ansicht: Abbildung 3: Screenshot der GUI Der Graph wird mit Hilfe der Grappa Bibliothek erstellt. Die Bibliothek erstellt den Graph und lässt ihn durch das Programm Graphviz ordnen. Dafür benötigt Grappa die dot.exe von Graphviz. In der Klasse de.unihannover.psue.jbcgtrans.gui.GrappaPrinter ist der Pfad angegeben in dem die dot.exe liegt. 58 4 Ergebnisse Um das Ergebnis dieser Arbeit zu präsentieren, wird der Fibonacci Code aus der Arbeit „Entwicklung eines Zwischencode-Modells für die Java-Bytecode Generierung“ von Andreas Prante übersetzt und analysiert. Der Code beschreibt einen Algorithmus zur Berechnung der nten Fibonaccizahl. public class FiboTest { public static void main(String[] args) { System.out.println(fibo(5)); } } public static int fibo(int n) { int f0 = 0; int f1 = 1; int z; int i = 1; while (i <= n) { z = f1; f1 = f0 + f1; f0 = z; i++; } return f1; } Die Klassenansicht des Codes sieht folgendermaßen aus: Im Codebeispiel ist kein Konstruktor definiert. Wie man sieht, hat der Java Compiler einen Standard Konstruktor hinzugefügt. Die beiden Funktionen main(...) und fibo(...) wurden zu InternalFunction Objekten übersetzt. Außerdem wurde ein statischer Block hinzugefügt. Da keine statischen Variablen definiert wurden und die Klasse FiboTest keinen statischen Block besitzt, sollte auch der statische Block im JBCG Modell keine Anweisungen enthalten. Nun sehen wir uns den Konstruktor genauer an: 59 Dies ist der Konstruktor in der vollen Ansicht ('f' Taste). Er enthält nur einen SequenceBlock und einen ReturnBlock ohne Rückgabewert. Der SequenceBlock enthält einen ConstructorCall, der den Konstruktor der Klasse java.lang.Object ohne Parameter aufruft. Dies ist die Anweisung super(), welche vom Java Compiler automatisch eingefügt wurde. Die nächste Grafik zeigt die Funktion main(...) in der vollen Funktionsansicht. Auch die main(...) Funktion besteht nur aus einem SequenceBlock und einem ReturnBlock. Die einzige Expression des SequenceBlocks ist ein Call von println(int) der Klasse java.io.PrintStream. Die Funktion wird in einem statischen Objekt mit dem Namen out aufgerufen. Das Feld out ist vom Typ java.io.PrintStream und ist in der Klasse java.lang.System definiert. Die println(int) Funktion wird durch ein ExternalFunction Objekt dargestellt. Das ist logisch, da die Klasse java.io.PrintStream nicht mit kompiliert wird und daher extern ist. Dieser Call stellt die Anweisung System.out.println(...) dar. Als Parameter wird der Funktion ein weiteres Call gegeben. Diesmal wird die Funktion fibo(...) der Klasse FiboTest aufgerufen. Die Funktion wird durch ein InternalFunction Objekt dargestellt, da sie mit kompiliert wird. Sie gibt ein int Wert zurück und braucht auch ein int als Parameter. 60 Der Parameter besteht aus dem konstanten int Wert '5'. Da fibo(...) eine statische Funktion ist, wird sie auf keinem Objekt ausgeführt. Das object Attribut des Call ist daher nicht definiert. Insgesamt wird die Anweisung System.out.println(fibo(5)); dargestellt. Die fibo(...) Funktion sieht in der Funktionsansicht folgendermaßen aus. Da die volle Ansicht hier viel zu unübersichtlich wäre, wird sie in der normalen Ansicht angezeigt. Die Funktion beginnt mit einem SequenceBlock mit drei Anweisungen. Dies sollten die Anweisungen sein, die vor der while Schleife stehen. Im Code stehen aber vier Anweisungen: int int int int f0 = 0; f1 = 1; z; i = 1; Der Grund warum es nur drei von ihnen in den SequneceBlock geschafft haben ist, dass int z; nur eine Deklaration einer Variablen ist und keine Anweisung. Die Variable z wird der fibo(...) Funktion als lokale Variable hinzugefügt, aber es wird keine Expression erzeugt. Der SequenceBlock verweist auf ein ConditionBlock. Dieser sieht in der Blockansicht so aus: 61 Der ConditionBlock überprüft, ob die lokale Variable i_int kleiner oder gleich der lokalen Variable n_int ist. Dies stellt die Bedingung der while Schleife dar. Die lokalen Variablen i und n sind vom Typ int und wurden bei der Übersetzung umbenannt (mehr dazu Kap. 3.4.1). Ist die Bedingung wahr, wird der true Block durchlaufen. Dieser ist der while Body. In der Blockansicht sieht dieser so aus: Wie schon in der Funktionsansicht zu sehen ist, enthält der while Body vier Anweisungen. Alle sind StoreOperation Objekte. Es werden also lokalen Variablen Werte zugewiesen. Es handelt sich dabei um die Anweisungen innerhalb der while Schleife: z = f1; f1 = f0 + f1; f0 = z; i++; Ist der Block durchlaufen, verweist er wieder auf den ConditionBlock. Sehen wir uns die letzte Anweisung i++; einmal in der Expressionansicht an: Wie zusehen ist, wird der lokalen Variable i_int das Ergebnis aus einer binären Operation zugewiesen. Die binäre Operation wird durch ein BinaryOperation Objekt dargestellt. Es hat die Operation + und als linken Operanden einen LoadOperation auf die Variable i_int. Der rechte Operand ist eine Integer Konstante mit den Wert '1'. Die Anweisung i = i + 1; wird 62 erzeugt. Die unäre Anweisung i++ wurde also in eine binäre umgewandelt (mehr dazu Kap. 3.4.1). Auffällig an der Funktionsansicht von fibo(...) ist, dass der SequenceBlock nach der while Schleife auch eine Anweisung enthält, obwohl im Code nach dem while die Funktion beendet wird. In der vollen Blockansicht sieht dieser SequenceBlock folgendermaßen aus: Der Block enthält eine StoreOperation. Der Inhalt der lokalen Variablen f1_int wird der Variablen $$_TmpVar$0 zugewiesen. Die Variable $$_TmpVar$0 ist nicht im Quellcode vorhanden. Sie wurde erst bei der Übersetzung erzeugt, um das Problem durch Postfix Anweisungen, die innerhalb des Rückgabewertes (siehe Kap. 3.3.12) definiert sind, zu umgehen. Als Lösung wird bei jeden return mit Rückgabewert eine neue lokale Variable erzeugt, die den Rückgabewert erhält. Das return bekommt nur eine LoadOperation auf diese Variable. Im Code wird die Variable f1 zurück gegeben. Die Variable $$_TmpVar$0 erhält den Wert von f1. Der folgende ReturnBlock enthält eine LoadOperation auf die Variable $ $_TmpVar$0. 63 5 Fazit und Ausblick 5.1 Fazit In dieser Arbeit konnte eine automatische, fast vollständige Übersetzung des Java Zwischencode in den Zwischencode der JBCG Bibliothek realisiert werden. Der Java Compiler wurde dazu um eine Phase erweitert. Vor der Generierung des Maschinen Codes wird der Zwischencode kopiert und in JBCG Zwischencode übersetzt. Dabei wird der gesamte Zwischencodebaum mit Hilfe des Scanner Pattern durchlaufen. Zunächst wird eine Datenbank aller Klassen, ihrer Funktionen und globalen Variablen erstellt. Danach wird in drei Phasen der JBCG Zwischencode erzeugt. In der ersten Phase wird die Klassenstruktur übersetzt. Merkmale wie Modifizierer, Rückgabewerte und Parameter werden den Funktionen, Konstruktoren und Klassen hinzugefügt. Dies stellt die trivialste Phase dar. Die zweite Phase ist die Übersetzung der Blockstruktur. Hier geht es vor allen um den Programmfluss und lokale Variablen. If, Schleifen und Switch Strukturen werden in die Blockstrukturen des JBCG Zwischencodes übersetzt. Hierbei gibt es die größten Unterschiede zwischen Java und dem JBCG Zwischencode. Phase drei ist die Übersetzung der einzelnen Anweisungen. Die Darstellung von Anweisungen ist zwar in beiden Zwischencodes relativ ähnlich, trotzdem gibt es zum Teil gravierende Unterschiede. Besonders die Übersetzung von boolesche Operationen war problematisch, da boolesche Operatoren in Programmfluss übersetzt werden müssen. Das heißt, es musste praktisch ein Schritt zurück gegangen und die Blockstruktur verändert werden. Es konnte fast die gesamte Sprache Java übersetzt werden. Nur Ausnahme-Behandlung und assert Anweisungen können nicht übersetzt werden. Die Struktur des JBCG Zwischencodes macht Optimierungen erheblich leichter oder sogar erst möglich. Um den JBCG Zwischencode darzustellen, wurde eine GUI entwickelt, die den Zwischencode als Graph dargestellt. Zum Vergleich wird auch der Java Zwischencode und der Quelltext dargestellt. So ist leicht ersichtlich, wie der Quelltext erst in den Java Zwischencode und dann in den JBCG Zwischencode übersetzt wird. Wird der JBCG Zwischencode durch Optimierungen oder anderen Algorithmen verändert, können diese Veränderungen sofort angezeigt werden. Der JBCG Zwischencode ist von der Struktur her viel näher am Java Maschinen Code als der Java Zwischencode. Dadurch ist der eigentliche Programmfluss viel deutlicher zu erkennen und der Code besser zu optimieren. Ein effizienterer Maschinencode wäre eine Folge solcher Optimierungen. Dieser Arbeit könnte ein Schritt in Richtung schnellerer oder speicheroptimierterer Java Programme sein. 64 5.2 Ausblick Wie schon beschrieben können in Zukunft Optimierungsalgorithmen implementiert werden, die den JBCG Zwischencode und damit auch den Java Maschinencode hinsichtlich auf Laufzeit oder Speicherverbrauch verbessern. Auch die GUI bietet vielseitige Möglichkeiten. Nützlich wäre eine Funktion, die den JBCG Zwischencode in einer Datei speichern und laden kann. So könnte die Übersetzung von der GUI entkoppelt werden. Die GUI könnten den Zwischencode laden und durch Algorithmen oder sogar manuell verändern. Einen andere Möglichkeit könnte ein Plugin für Entwicklungsumgebnungen wie z. B. Eclipse sein. Der Java Code könnte während des Programmierens in JBCG Zwischencode übersetzt und angezeigt werden. So wäre schnell ersichtlich, welcher Quellcode welchen Zwischencode erzeugt. Optimierungen direkt am Quellcode wären so erleichtert. 65 6 Anhang 6.1 Literatur- und Quellenverzeichnis Bachelorarbeit „Untersuchung des Compilers zum Java 6 Release“ von Carsten Grenz und Marc Lindenberg Bacherlorarbeit „Entwicklung eines Zwischencode-Modells für die Java-Bytecode Generierung“ von Andreas Prante Masterarbeit „Optimierung der Java-Bytecode-Erzeugung für gerichtete azyklische Graphen“ von Michael Mainik API Dokumentation der JBCG Bibliothek API Dokumentation der cojen 2.1 Bibliothek Programmierhandbuch und Referenz für die JavaTM 2 Plattform, Standard Edition 3. Auflage 2002 (http://www.dpunkt.de/java//index.html) The Java Language Specification, Third Edition (http://java.sun.com/docs/books/jls/third_edition/html/j3TOC.html) Graphviz Documention (http://www.graphviz.org/Documentation.php) Quellcode des Java Compilers 66 Erklärung Hiermit erkläre ich, dass ich diese Arbeit und die Implementierung selbständig angefertigt habe. Ich habe nur die im Quellenverzeichnis angegebenen Hilfsmittel benutzt. Paul Salomon 67