Entwicklung einer neuen Codegenerierungstufe im Java Compiler

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