FAKULTÄT FÜR INFORMATIK Vereinfachung der

Werbung
FAKULTÄT FÜR INFORMATIK
DER TECHNISCHEN UNIVERSITÄT MÜNCHEN
Bachelorarbeit in Informatik
Vereinfachung der Spezifikation von
Analysen im Programmanalyseframework
VoTUM
Matthias Putz
FAKULTÄT FÜR INFORMATIK
DER TECHNISCHEN UNIVERSITÄT MÜNCHEN
Bachelorarbeit in Informatik
Vereinfachung der Spezifikation von Analysen im
Programmanalyseframework VoTUM
Improvement of Analysis Specification in the Program
Analysis Framework VoTUM
Bearbeiter:
Aufgabensteller:
Betreuer:
Abgabedatum:
Matthias Putz
Prof. Dr. Helmut Seidl
Andrea Flexeder
15. Februar 2010
Ich versichere, dass ich diese Bachelorarbeit selbständig verfasst und nur die angegebenen
Quellen und Hilfsmittel verwendet habe.
München, den 2. Februar 2010
Matthias Putz
Zusammenfassung
Das Ziel dieser Arbeit ist die Spezifikation von Analysen im Programmanalyse-Werkzeug
VoTUM zu vereinfachen, indem Java für diese Aufgabe durch eine andere Programmiersprache abgelöst wird. Analysiert werden die Sprachen Clojure, Groovy und Scala. Anschließend wird die Migration mit der für die Analyse-Definition am besten geeigneten
Sprache, Scala, erläutert.
vii
viii
Inhaltsverzeichnis
Zusammenfassung
vii
1
Motivation
1
2
Anforderungen und Ziele
2.1 Allgemein . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.2 Nachteile durch Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3
3
3
3
Evaluation verschiedener Sprachen
3.1 Kriterien . . . . . . . . . . . . .
3.2 Clojure . . . . . . . . . . . . . .
3.3 Groovy . . . . . . . . . . . . . .
3.4 Scala . . . . . . . . . . . . . . .
3.5 Evaluation . . . . . . . . . . . .
4
5
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
7
7
8
8
9
10
Die Sprache Scala
4.1 Struktur eines Scala-Programms
4.2 Collections und Implicit . . . . .
4.3 Pattern Matching . . . . . . . . .
4.4 Ein- und Auspacken . . . . . . .
4.5 Typisierung und Closures . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
11
11
11
12
13
14
Realisierung
5.1 Allgemeines Vorgehen . . . . . . . . . . . .
5.2 Einbindung und Analysen . . . . . . . . . .
5.3 Analyse-Passes . . . . . . . . . . . . . . . .
5.4 Lattice-Definition . . . . . . . . . . . . . . .
5.5 Erweitern von Lattice und Lattice-Element
5.6 Pattern Matching mit Objekten . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
17
17
17
20
21
25
30
6
Vergleich mit PAG
33
7
Zusammenfassung
35
Literaturverzeichnis
37
ix
Inhaltsverzeichnis
x
1 Motivation
Programmanalysen nehmen in heutigen Softwareprojekten eine wichtige Rolle ein: Sie dienen zur Verifikation und zur Inferenz von bestimmten Eigenschaften in einem Programm.
Diese können z.B. unterstützend bei der Fehlererkennung und Zertifizierung von Code
eingesetzt werden. AbsInts Programmanalyse-Tool WCET [1] zum Beispiel analysiert sicherheitskritische Programme, wie z.B. Flugsteuerungssoftware, indem es obere Grenzen
für die maximale Ausführungszeiten einzelner Tasks inferiert.
Das Programmanalyse-Tool VoTUM dagegen ist zur schnellen und einfachen Konzeption von Analysen, besonders im Rahmen der Lehre und für Prototypen von eigenen Analyseideen, gedacht. Die bisherige VoTUM Version verlangt eine Analyse-Definition in Java,
die nicht nur sehr umständlich, sondern auch schwer zu überblicken und zu verstehen ist.
Ursache hierfür ist der große, von Java verlangte, Implementierungsoverhead.
Was das konkret bedeutet, wird im nächsten Kapitel betrachtet (Kapitel 2). Es folgt eine
Evaluierung verschiedener Alternativsprachen, bei denen sich die Programmiersprache
Scala als beste Lösung, vor Groovy und Clojure, herausstellt (Kapitel 3). Nach einer Einführung in Scala (Kapitel 4) wird die konkrete Umsetzung mit Scala erläutert (Kapitel 5).
Den Abschluss macht ein allgemeiner Vergleich mit dem Analysetool PAG von AbsInt, um
die gewonnenen Vorteile zu verdeutlichen (Kapitel 6).
1
1 Motivation
2
2 Anforderungen und Ziele
2.1 Allgemein
Das angestrebte Ziel ist die Vereinfachung der Analyse-Definition, d.h. der Spezifikation
von Analysen in VoTUM.
Eine Analyse arbeitet stets auf einem Programm, das in Form eines CFGs, eines Control
Flow Graphs (Kontrollflussgraph), vorliegt. Das heißt, jeder Programmanweisung ist eine
Kante im Graphen zugeordnet. Die Knoten bezeichnen Mengen von Programmzuständen.
Abbildung 2.1 zeigt den CFG, der Listing 2.1 repräsentiert. Die Programmvariablen wurden dabei mit eindeutigen Namen versehen, so wird Variable a auf R1 abgebildet.
Listing 2.1: Beispielprogramm in C
1
2
3
4
5
6
7
8
9
10
int main(void) {
int a = 13;
int b = 6;
int c = b;
int d = c / 2;
int e = 14;
if (a == 4)
c = a - 5;
else
b = a + 5;
11
e = e + b;
12
13
return e;
14
15
}
Für eine konkrete Analyse wird zum einen ein vollständiger Verband definiert, der an
jedem CFG-Knoten eine Menge von Programmzuständen beschreibt. Zum anderen legt
man mit den Transferfunktionen die Übergänge zwischen den Knoten fest. Das bedeutet,
dass man eine Abstraktion über die Effekte einzelner Programmanweisungen vornimmt,
d.h. wie sich der Programmzustand nach Ausführen einer Programmanweisung ändert.
2.2 Nachteile durch Java
Eine Analyse iteriert über den CFG. Dabei propagiert und transformiert man Datenflußwerte entlang der Kanten des CFG. Java bringt in zweierlei Hinsicht Schwierigkeiten für
diese Aufgabe:
1. In Java besteht das Problem, wie die einzelnen Kantentypen bzw. die damit verbundenen Programmanweisungen unterschieden werden können. Die sinnvollste Lösung in Java ist es hierfür das Visitor-Pattern umzusetzen. Das verlangt aber eine
3
2 Anforderungen und Ziele
Abbildung 2.1: CFG Darstellung des Programms in Listing 2.1
4
2.2 Nachteile durch Java
Unmenge an Schreibarbeit, da für jeden Kantentyp eine eigene Methode ausimplementiert werden muss.
Listing 2.2 zeigt zwei Methoden aus der Visitor-Pattern Umsetzung: ConditionalStmt
und AssignmentStmt sind Kantenbeschriftungen, Set<RegisterExpr> spezifiziert ein Element des Verbands.
Listing 2.2: Visitor-Pattern in Java
1
2
3
4
// JAVA-CODE
public Void visit(ConditionalStmt ct, Set<RegisterExpr> vars) {
// ...
}
5
6
7
8
public Void visit(AssignmentStmt at, Set<RegisterExpr> vars) {
// ...
}
2. Als weiteres Problem kommt hinzu, dass man oft nicht das gesamte, sondern nur
Teile des Lattice-Element verändert. Um jedoch an diese Werte zu gelangen, ist ein
Aus- und Einpacken der Daten notwendig. Dies bedeutet in Java, dass man sich mit
etlichen get-Methoden-Aufrufen zu den benötigten Objekten durchhangeln muss.
Listing 2.3 zeigt diese Problematik, die vor allem bei kombinierten Verbänden, wie
dem hier gezeigten Tuple<Map<Integer,String>,Interval>, auftritt.
Listing 2.3: Visitor-Pattern in Java
1
2
3
4
5
6
7
8
// JAVA-CODE
Tuple<Map<Integer,String>,Interval> replaceMap(Tuple<Map<Integer,String>,Interval>
x) {
Map<Integer,String> m = x.getFirst();
Interval i = x.getSecond();
m.transform();
i.transform();
return new Tuple<Map<Integer,String>,Interval>(m,i);
}
Eleganter wäre hier das aus funktionalen Sprachen bekannte Konstrukt Pattern Matching, um beide Probleme ohne allzu viel Schreiboverhead umzusetzen. Wie jedoch in [11]
erörtert ist, sind solche Konstrukte in objektorientierten Sprachen nicht nur ungewöhnlich,
sondern auch eher schwierig zu realisieren.
Die entscheidende Frage ist nun, in wie weit man sich von der objektorientierten Sprache Java entfernen möchte, um Konstrukte wie Pattern Matching verwenden zu können.
Deshalb sind die in Betracht gezogenen Sprachen auf speziell diese Anforderungen zu
überprüfen.
5
2 Anforderungen und Ziele
6
3 Evaluation verschiedener Sprachen
Da VoTUM in Java geschrieben ist und somit auf der JVM läuft, fallen alle Sprachen
weg, die nicht auf der JVM lauffähig sind. Übrig bleiben drei Sprachen, die jeweils einem
Sprachtyp zuzuordnen sind. Die erste Sprache ist Clojure [2], eine hauptsächlich funktionale Sprache. Als zweite Sprache wird Groovy [4] betrachtet, welche eine imperative,
objektorientierte Sprache ist, die mit ein paar funktionalen Elementen ausgestattet ist. Die
letzte untersuchte Sprache ist Scala [16], die als Mix aus objektorientierten und funktionalen Paradigmen bezeichnet werden kann.
Diese 3 Sprachen werden im folgenden bezüglich ihres praktischen Einsatzes für Analaysespezifikationen in VoTUM evaluiert.
3.1 Kriterien
Am besten eignet sich eine Sprache für unsere Zwecke, wenn sie die folgenden Kriterien
erfüllt:
Interoperabilität mit Java: Zuerst soll die Verbindung zu Java untersucht werden. Dies
ist nicht nur wegen der Integration in das bestehende Java-Projekt VoTUM interessant, sondern auch wegen der Vielfalt an bestehenden Java-APIs, auf die zugegriffen werden kann.
Einbindung in VoTUM: Es soll möglichst einfach sein, die Sprache in das aktuelle VoTUMProjekt einzubinden. Einfach bedeutet hierbei, ohne viele Codeänderungen vornehmen zu müssen und ohne extensiv Libraries oder andere Programme zu benötigen.
Datenstrukturen: Analysen verwenden Datenstrukturen zur Repräsentation der Programmzustände. Eine einfache Verwendung erleichtert somit die Analysespezifikation selbst.
Funktionale Fähigkeiten: Für die Programmanalyse geeignete Konzepte aus funktionalen Sprachen, insbesonders das Pattern Matching, sind gewünscht.
Entwicklungsprogress: Entscheidend ist außerdem eine gesicherte Unterstützung und
Weiterentwicklung der Sprache.
Besonderheiten: Hervorheben von besonderen Fähigkeiten der Sprache, die sich bei der
Analysespezifikation als nützlich erweisen können.
Neben den aufgeführten Kritikpunkten werden im folgenden die einzelnen Sprachen
auch auf ihren Ursprung, Hintergrund und Intention betrachtet, um Schlüsse über die
weitere Entwicklung, Unterstützung und die Einsatzfähigkeit in VoTUM zu ziehen.
7
3 Evaluation verschiedener Sprachen
3.2 Clojure
Vorteile: Clojure, als Lisp-Derivat, ist den funktionalen Sprachen zu zuordnen. Dennoch
ist es möglich, Java-Objekte zu erstellen und zu verwenden, wodurch die Interoperabilität
gegeben ist und eine einfache Einbindung möglich ist.
Nachteile: Die beiden großen Nachteile und die Gründe für das Vernachlässigen dieser Sprache in der restlichen Arbeit sind zum einen die noch massiven Umbauten an der
Sprache selbst und zum anderen die noch fehlende Community. Ersteres ist neben der gerade einmal zweijährigen Entwicklungszeit auch durch den aktuellen Versionsstand 1.0.0
(Nov 2009) verständlich. Durch die kurze Lebenszeit fehlt Clojure zudem die tatkräftige
Community im Hintergrund, die das Projekt bzw. die Sprache vorantreibt.
Da für VoTUM eine robuste und etablierte Sprache gewünscht ist, die auch in einigen
Jahren noch weiterentwickelt wird, verfolgen wir den Ansatz Clojure nicht weiter.
3.3 Groovy
Im Gegensatz zu Clojure ist Groovy bereits eine standardisierte Sprache.
Vorteile: Groovy bietet eine nahtlose Integration von Java. Das liegt daran, dass JavaCode auch Groovy-Code ist. Zudem ist es einfach, aus Java heraus auf Groovy-Klassen
und Objekte zu zugreifen (weil grundsätzlich dieselben Konstrukte verwendet werden).
Für die Einbindung in VoTUM benötigt man lediglich ein JAR-Archiv, um die GroovyKlassen aus Java heraus verwenden zu können. Wichtig ist hierbei, dass man die *.groovy
Dateien vorher mit dem Compiler groovyc vorkompiliert (Bytecode erstellt). Alternativ
kann Groovy zur Laufzeit interpretiert werden, d.h. es kann aus Java heraus eine Datei
oder ein String mit Groovy-Code direkt ausgeführt werden.
Collections wie Listen und Maps werden in Groovy bereits auf Sprachebene unterstützt.
Zudem bestehen Konstrukte zur einfachen Verwendung von Collections, wie z.B. das
Operator-Überladen oder Filter-Methoden mit Closures als Parameter.
Groovy ist mit der letzten Version 1.6 einen entscheidenden Schritt in Richtung Stabilität gegangen. Das Motiv der Groovy-Programmierer ist nicht die Entwicklung einer
eigenständigen und in sich geschlossenen Sprache, sondern viel mehr, die Java-Welt zugänglicher zu machen. Am besten beschreibt man Groovy daher, indem man sie als (Java-)
Skriptsprache bezeichnet. Diese Bezeichnung gründet auf der prägnanten Syntax und der
hohen Dynamik.
Die hohe Dynamik ist eine der großen Stärken von Groovy. So kann zur Laufzeit die
Struktur einer Klasse verändert werden und z.B. neue Methoden hinzugefügt werden.
Oder es lassen sich Methoden dynamisch aufrufen, indem man deren Signatur aus Strings
erstellt. Aus diesen beiden Konzepten könnte man sich Konstrukte zum Dispatchen der
Kanten überlegen, die das Visitor-Pattern mit weniger Overhead umsetzen.
Nachteile: Groovys funktionale Eigenschaften beschränken sich im Wesentlichen auf die
Umsetzung von Closures. Das heißt, man kann Code-Objekte erstellen, die man dann als
Variablen weiterreichen und an einer beliebigen Stelle ausführen kann. Allerdings sind
8
3.4 Scala
hiermit die funktionalen Aspekte bereits abgehandelt. Leider bietet Groovy keine Möglichkeit einer Typisierung der Closures, also der Festlegung der benötigten Parametertypen und des Rückgabetyps. Weitere übliche funktionale Konzepte wie Currying sind nur
bedingt und umständlich möglich, das Pattern Matching fehlt in Groovy komplett. Auch
eine besondere Unterstützung der Rekursion ist in Groovy nicht gegeben und so ist es
leicht möglich, selbst bei endrekursiven Methoden in einen Stackoverflow zu laufen.
3.4 Scala
Verfolgt Clojure den radikalen Ansatz und möchte eine von Java völlig fremden Sprachtyp
auf der JVM realisieren, so verfolgt Groovy den milden Ansatz, indem es die komplette
Java-Welt weiterhin bereitstellt, aber diese mit mächtigen Konzepten wie den Closures
anreichert. Eine Art Mittelweg beschreitet Scala.
Vorteile: Die Einbindung der Sprache in VoTUM ist genau wie die Groovy-Anbindung:
Ein Jar-Archiv genügt, es gibt einen Compiler scalac, um Bytecode zu erstellen, und es
existieren Klassen zur Interpretation von Scala-Code aus Java heraus.
Scala bietet die üblichen Collection-Klassen wie Listen und Maps. Durch Operator-Überladen und Closures wird die Verwendung stark vereinfacht. Ein kleiner Nachteil ist, dass
die Collections nicht bereits auf Sprachebene unterstützt werden. Dies macht die Erstellung um wenige Zeichen länger, als es bei Groovy der Fall ist. Dagegen unterstützt Scala
den Datentyp Tupel auf Sprachebene, das heißt man kann z.B. einen Typ (Int, List, Long)
erstellen, der drei verschiedene Datentypen zusammenfasst.
Scala ist einer funktionalen Sprache sehr ähnlich. Zum Beispiel können Code-Objekte
streng typisiert werden. Damit kann man auch bei Funktionen Code-Objekte als Parameter übergeben und angeben, welchen Typen die Parameter und der Rückgabewert haben.
Zudem werden endrekursive Methodenaufrufe automatisch vom Compiler erkannt und
optimiert.
Insgesamt ist Scala eine sehr elegante und durchdachte Sprache; dies wird durch die
starke theoretische Basis unterstrichen, die von der hinter Scala stehenden Community
in zahlreichen Research Papers [17] dokumentiert ist. Zum Beispiel geht Scala aktuelle
Probleme wie die Parallelisierung von Programmen direkt an.
Wegen der neuen Konzepten in Scala ist eine Inkompatibilität zu bestehendem JavaCode unvermeidbar. Allerdings steht einem nur dadurch das vor allem in dieser Arbeit
sehr wichtige Konzept des objektorientierten Pattern Matching [11] zur Verfügung. Für Objekte ist das Pattern Matching an sich nicht trivial, allein schon die Abwesenheit dieser
Möglichkeit in gängigen OOP-Sprachen (inklusive Java) untermauert dies.
Allein diese Fähigkeit verschafft Scala im Hinblick auf die Analyse-Sprache einen riesigen Vorsprung vor Groovy und Clojure und ist nicht zuletzt der Ausschlag dafür, dass
Scala für die Realisierung verwendet wird.
Nachteile: Die Interoperabilität ist in Scala nicht so nahtlos wie in Groovy: Zum Beispiel
gibt es keine statischen Methoden, sondern lediglich die Möglichkeit, singleton-Objekte
zu erstellen. Beim Zugriff aus Java heraus muss das beachtet werden. Ferner implementiert Scala Listen, Maps, etc. auf eine eigene Art und Weise; somit muss eine Konvertie-
9
3 Evaluation verschiedener Sprachen
rung durchgeführt werden. Dies kann jedoch durch ein weiteres Sprachkonstrukt (implicit) automatisiert werden. Der Grund für die notwendige Konvertierung ist, dass Scala
strikt zwischen mutable und immutable bei Datenstrukturen unterscheidet. Im Hinblick
auf konkurrierenden Zugriff ist das sicherlich hilfreich.
Von Nachteil könnte auch sein, dass bei Scala die Sprachdefinition noch nicht vollständig abgeschlossen ist. Zwar steht die Sprache und die API in ihren Grundzügen, jedoch
wurde für die kommende Version 2.8 z.B. das elementare Collection-Package komplett restrukturiert, bestehender Code ist inkompatibel. Weitere Sprachänderungen sind auch in
der Zukunft nicht ausgeschlossen. Für dieses Projekt ist das jedoch eher unbedeutend, da
zum einen mit der kurz vor dem Release stehenden Scala Version 2.8 gearbeitet wird und
andererseits keine besonderen Sprachfeatures verwendet werden, die evtl. verändert oder
gelöscht werden und ein Umschreiben des kompletten Projekts erfordern würden.
3.5 Evaluation
Die Entscheidung fällt für Scala (Version 2.8), weil es sich am geeignetsten für unsere Zwecke herausgestellt hat. Vor allem das Pattern Matching ist überzeugend.
10
4 Die Sprache Scala
Nun wird Scala etwas genauer betrachtet und ihre Besonderheiten, aber auch ihre Eigenheiten herausgestellt. Es folgt eine kleine Auswahl der vermutlich am häufigsten benötigten Sprachkonstrukte, sowie die größten Stolpersteine im Bezug auf das Schreiben von
Analysen.
4.1 Struktur eines Scala-Programms
Der Aufbau eines Scala-Programms ist identisch mit dem eines Java-Programms: In der
Regel beginnt die Datei mit einer Package-Deklaration, gefolgt von diversen Import-Anweisungen. Danach kommt die Klassendefinition. In Scala ist es möglich, den Dateinamen
und den Klassennamen unterschiedlich zu wählen. Zudem erlaubt Scala mehrere Klassen
in einer Datei zu deklarieren.
4.2 Collections und Implicit
Einer der wichtigsten Bestandteile beim Programmieren sind die Datenstrukturen. Funktionale Sprachen verwenden oft immutable, unveränderliche, Datenstrukturen. Das hat
den Vorteil, dass man die Programme gut und einfach parallelisieren kann, ohne dabei in
schwer auffindbare Concurrency-Bugs zu gelangen. Solchen Anwendungen spricht man
eine gute Skalierbarkeit zu. Da Scala die Thread-Programmierung erleichtern möchte, sind
alle Collection-Datentypen per default immutable.
Möchte man dennoch veränderliche Datenstrukturen, so genügt der Import der entsprechenden Klasse aus dem Package scala.collection.mutable.
Schwieriger wird es, wenn man zwischen Java und Scala Collection-Objekten konvertieren möchte, weil diese standardmäßig nicht einander entsprechen (im Gegensatz zu
Groovy funktioniert das leider nicht vollkommen automatisch). Jedoch genügt der Import
aus Listing 4.1 für eine transparente Konvertierung.
Listing 4.1: Verwenden von Java-Collections in Scala
1
import collection.JavaConversions
Das funktioniert dank der sogenannten impliziten Aufrufe. Dieses Feature wird vor allem
anhand von Wrapper Klassen deutlich (siehe REPL-Session in Listing 4.2): Angenommen
wir haben eine Wrapper Klasse MyStr, die eine Methode dummyDo und ein String Objekt als
Parameter hat.
Nun definieren wir eine implizierte Umwandlung von String in MyStr mit Hilfe der Methode str2mystr. Von nun an kann die Methode dummyDo auf String Objekten ausgeführt
werden, als wäre die Methode in der String Klasse definiert. Ohne die implizite Methode
11
4 Die Sprache Scala
würde ein Fehler geworfen werden. Damit die Konvertierung überhaupt vom
Compiler erkannt bzw. versucht wird, muss die Methode mit implicit ausgewiesen werden.
str2mystr
Listing 4.2: Implizite Typumwandlung von String
1
2
scala> class MyStr(s : String) { def dummyDo = 42 }
defined class MyStr
3
4
5
scala> implicit def str2mystr(s : String) = new MyStr(s)
str2mystr: (s: String)MyStr
6
7
8
scala> val str = "abc"
str: java.lang.String = abc
9
10
11
scala> val answer = str.dummyDo
answer: Int = 42
In Abschnitt 5.5 wird mit Hilfe des implicit-Features eine elegante Möglichkeit zur einfachen Definition von Analysen gezeigt.
Zu den Collection-Klassen ist noch anzumerken, dass es neben allen erwarteten StandardMethoden dank einer Art Operator-Überladens noch zahlreiche prägnante Funktionen
wie z.B. ++ zum Konkatenieren von zwei Listen gibt.
4.3 Pattern Matching
Dieser Abschnitt soll einen Eindruck von der Syntax und den wichtigsten Fähigkeiten,
aber auch den Grenzen, des Pattern Matching zeigen.
Listing 4.3: Synatx von Pattern Matching
1
2
3
4
5
6
edge match {
case ct @ IF(_, true_case, false_case) => // ...
case at @ ASSIGN(loc : RegisterExpr, expr) if isVariableAlive(loc, getSourceNode(at))
=> // ...
case ASSIGN(loc : MemAccessExpr, expr) => // ...
case _ => // ...
}
Listing 4.3 zeigt das Scala Pattern Matching, angewendet in einer Analyse für den CFG
in Abbildung 2.1. Die angewendeten Features sind zeilenweise die folgenden:
Zeile 1: Variable edge soll mit Hilfe von Pattern Matching untersucht werden. Es folgen
verschiedene Fälle, Patterns, als case-Statements.
Zeile 2: Überprüfung, ob die Kante eine If-Anweisung ist. Falls ja, wird sie in der neuen
Variable ct gespeichert (ist also bereits von dem Typ der zum Pattern IF gehört!) und
zudem werden noch die Variablen true_case und false_case ausgepackt und belegt.
Um einen beliebigen Wert zu erlauben, verwendet man die Wildcard _.
Die Namenskonvention lautet hier: Variablennamen sind in Pattern klein geschrieben, Klassennamen bzw. Extraktornamen, die das eigentliche Pattern darstellen, sind
groß geschrieben (Extraktoren werden in 5.6 erklärt).
12
4.4 Ein- und Auspacken
Zeile 3: Selbiges wie in Zeile 2, nur wird hier verlangt, dass die Variable loc vom Typ
RegisterExpr ist. Zudem wird mit einer if Anweisung am Ende des case-Falls, einem so genannten Guard, noch eine bestimmte Eigenschaft der Variablen gesichert.
Das bedeutet, dass das Pattern trotz eines Matchs fehlschlagen kann, nämlich genau
dann, wenn der Guard false ist.
Zeile 4: Selbiges wie oben, nur soll loc nun vom Typ MemAccessExpr sein.
Zeile 5: Der default-Fall, wenn kein anderes Pattern zutrifft. Alternativ könnte man hier
auch eine Variable angeben, weil dieses Pattern immer zutrifft. Dann entspricht die
Variable dem Match-Objekt edge. Dieser default-Fall muss immer angegeben werden,
weil Scala sonst einen Fehler wirft!
Neben der noch unklaren Semantik der Ausdrücke IF und ASSIGN bleibt die Frage offen,
wie man eigene Pattern definieren kann. Diese Punkte werden in Kapitel 5.6 behandelt.
4.4 Ein- und Auspacken
Ferner ist das zweite in Abschnitt 2.2 angesprochene Problem mit Java, das Ein- und Auspacken von Objekten, in Scala mit Hilfe des Pattern Matching knapper und prägnanter
lösbar. Man kann wie in Listing 4.4 mit Hilfe einer Variablen ein Pattern Matching durchführen, oder man verwendet die partiellen Funktionen aus Scala, siehe Listing 4.5. Letzteres erstellt eine Funktion, die nur an bestimmten Stellen definiert ist und somit indirekt
wieder ein Matching darstellt.
Listing 4.4: Ein- und Auspacken mit eigener Variable
1
2
3
4
def replaceMapping(x : Tuple[Map[Int,String],Interval]) = {
val (l,r) = x
new Tuple(l.transform, r.transform)
}
Listing 4.5: Ein- und Auspacken mit partiellen Funktionen
1
2
3
def replaceMap :
PartialFunction[Tuple[Map[Int,String],Interval],Tuple[Map[Int,String],Interval]] = {
case (l,r) => new Tuple(l.transform, r.transform)
}
Anmerkung: Wünschenswert wäre es, wenn man bereits bei der Angabe der Funktionsparameter ein Pattern Matching anwenden kann, wie Lisitng 4.6 zeigt. Allerdings ist dieser Ansatz in Scala 2.8 noch nicht umgesetzt, wird aber vermutlich in späteren Versionen
eingeführt werden.
Listing 4.6: Ein- und Auspacken bei den Parametern
1
2
3
4
// SCALA-PSEUDO-CODE
def replaceMap (x @ (l,r) : Tuple[Map[Int,String],Interval]) {
new Tuple(l.transform, r.transform)
}
13
4 Die Sprache Scala
Abbildung 4.1: Klassenhierarchie in Scala [13]
4.5 Typisierung und Closures
Klassen-Hierarchie: Scala verlangt eine strenge Typisierung. Um sich an die etwas veränderten Standard-Objekte zu gewöhnen, hilft die in Abbildung 4.1 gezeigte Hierarchie
von Scala-Objekten. Hervorzuheben ist folgendes:
•
Any
in Scala entspricht der Oberklasse Object aus Java.
• Dieser Obertyp teilt sich in zwei Unterbäume auf: Die ursprünglichen primitiven
Datentypen als Unterklassen von AnyVal sowie den Referenz-Objekten mit AnyRef als
Obertyp.
• Neben dem aus Java bekannten Null gibt es noch Nothing am Ende der Hierarchie.
Von diesem Typ gibt es keine konkrete Instanz, dennoch erweist sich dieser Typ z.B.
bei generischen Klassen als nützlich (siehe Abschnitt 5.4).
Closures: Ferner existiert in Scala ein Datentyp für Closures. Dieser Typ wird durch eine
kommaseparierte Aufzählung der Parametertypen, gefolgt von einem => und dem Angeben des Rückgabewerts, definiert. Möchte man eine so typisierte Variable mit einem
Wert belegen, muss man die Eingabeparameter mit selbst gewählten Namen benennen
und dann nach einem => den Funktionscode schreiben. In der Regel ist Scala im Stande,
14
4.5 Typisierung und Closures
die Parametertypen zu inferieren; somit müssen die Parameter nicht nochmal explizit typisiert werden. Ein Beispiel für eine Typdeklaration (Zeile 1) sowie die Erstellung eines
Funktionsobjekts (Zeile 2) ist in Listing 4.7 zu sehen.
Listing 4.7: Extraktor-Objekt
1
2
3
var c : (Int,Int) => Boolean = {
(a,b) => if(a > b) true else false
}
Zu diesem Beispiel ist folgendes anzumerken: Scala kann den Typ der Variable c aus
dem Funktionsobjekt in Zeile 2 inferieren. Alternativ könnte man c völlig untypisiert lassen, muss dafür jedoch dann die Parameter a und b typisieren.
Um tiefer in die Sprache einsteigen zu können, bietet sich die Einführung A Tour of Scala
[18] an. Für ausführlichere Erklärungen zu bestimmten Features und Spracheigenschaften
sind die beiden Referenzen [22] [12] sehr gut geeignet.
15
4 Die Sprache Scala
16
5 Realisierung
Die beschriebenen Scala-Klassen werden nun im Hinblick auf die in Kapitel 2 aufgestellten
Anforderungen in das bereits existierenden VoTUM-Projekt eingebunden.
5.1 Allgemeines Vorgehen
Bei der Umsetzung wird versucht, den bestehenden VoTUM-Codes möglichst unangetastet zu lassen. Das heißt konkret, dass die Erweiterung durch Erben von bestehenden
VoTUM-Klassen realisiert wird.
Damit bestehende VoTUM-Klassen von den neuen Scala-Klassen unterschieden werden
können, aber dennoch der Bezug zwischen den Klassenpaaren besteht, haben die ScalaKlassen eine Namens-Konvention: Sie beginnen mit dem Prefix Scala.
Eine weitere Konvention ist, dass dieselbe Package-Struktur für das Scala Projekt wie
für die Java Klassen verwendet wird. Alle Scala-Klassen findet man somit im Package de.
tum.in.wwwseidl.votumscala, das die Struktur von de.tum.in.wwwseidl.votum übernimmt.
Um für die Scala-Klassen eine gemeinsame Basis bereit zu stellen, werden für die in
VoTUM bereits abstrakten Klassen wie AbstractAnalysisSequence oder AbstractAnalysisPass
auch abstrakte Scala-Klassen definiert. Diese erben von der jeweiligen VoTUM-Klasse.
Somit wird also eine Zwischenschicht, eine Schnittstelle, konstruiert, so dass alle ScalaKlassen bereits existierende VoTUM-Klassen als Grundlage haben. Abbildung 5.1 verbildlicht den Zusammenhang.
Diese Designentscheidung für eine Java-Scala-Schnittstelle ermöglicht es Konzepte aus
Scala in VoTUM einzubringen und zu verwenden.
5.2 Einbindung und Analysen
Im folgenden ist mit einer Analyse-Sequenz eine Kombination von mehreren einzelnen Analysen gemeint. Die einzelne Analyse wird als (Analyse-) Pass bezeichnet.
Die folgenden Abschnitte gehen schrittweise die Überlegungen zur Definition einer
Analyse-Sequenz mit ihren Analyse-Passes durch. Dabei werden besonders die auftretenden Probleme und deren Lösung diskutiert.
Gemeinsame Methoden und Variablen: Häufig verwenden mehrere Passes einer AnalyseSequenz dieselben Methoden oder haben gemeinsame Variablen. Dann macht es Sinn, diese statisch in der Analyse-Sequenz-Klasse zu definieren.
Jedoch ist diese Variante in Scala nicht möglich: Scala kennt keine statischen Methoden oder Variablen. Die Lösung sind singleton-Objekte. Somit ist die Designentscheidung
für (fast alle) konkrete Analyse-Sequenzen bereits festgelegt: Um zu erreichen, dass die
17
5 Realisierung
Abbildung 5.1: Scala Schnittstelle zwischen Java-VoTUM-Klassen und den Scala-Analysen
einzelnen Passes eine gemeinsame Basis haben, d.h. auf evtl. für alle Passes allgemein definierte Methoden zugreifen zu können, werden Analyse-Sequenzen als Unterklassen von
ScalaAnalysisSequence, mit dem Schlüsselwort object angelegt (um sie als singleton-Instanz
zu markieren).
ScalaAnalysisSequence selbst ist in der Schnittstellenschicht von Java zu Scala (siehe Bild
5.1) zu finden. Diese Klasse erweitert die Funktionalität von AbstractAnalysisSequence, ist
aber immer noch abstrakt und muss von den konkreten Scala-Analyse-Sequenzen implementiert werden.
Analyse-Sequenz-Instanzen Da die Analyse-Sequenz als singleton-Objekt definiert ist,
existiert immer nur eine einzige Instanz dieser Analyse-Sequenz. Das ist jedoch unerwünscht, weil man beim Laden einer Analyse-Sequenz in VoTUM ein neues Objekt haben möchte und nicht für jede Analyse-Sequenz-Ausführung dasselbe Objekt. Da VoTUM
lediglich eine Instanz von AnalysisSequence bzw. AbstractAnalysisSequence erwartet, können
wir das ausnutzen.
Der Code in Listing 5.1 zeigt einen Teil der Umsetung der Scala-Analyse-Sequenz.
Listing 5.1: Erweiterung der Package-Deklaration
1
abstract class ScalaAnalysisSequence extends AbstractAnalysisSequence {
2
/**
* Creates a new object of type AbstractAnalysisSequence and returns the instance.
* Within the anonym object the expected methods getVersion, getName and
* getDescription are defined with the values of ScalaAnalysisSequence.
3
4
5
6
18
5.2 Einbindung und Analysen
* Additionally the method newPasses of ScalaAnalysisSequence is called to
* receive a new set of pass-instances. These passes are added to the
* AbstractAnalysisSequence over the addPass-Method.
*/
def apply() = {
new AbstractAnalysisSequence() {
// add new set of passes to this AbstractAnalysisSequence instance
newPasses.foreach(addPass(_))
7
8
9
10
11
12
13
14
15
// defines methods by setting values of ScalaAnalysisSequence
def getVersion = version
def getName = name
def getDescription() = description
16
17
18
19
}
20
}
21
22
23
// have to define general infos about analysis
val version : String
val name : String
val description : String
24
25
26
27
28
// generates and returns a new set of analysis passes
def newPasses : List[AnalysisPass]
29
30
31
// ...
32
33
}
Anmerkungen zu Listing 5.1:
1. Die Methode apply erstellt ein anonymes Objekt vom Typ AbstractAnalysisSequence.
Dabei werden die notwendigen Methoden mit den Werten aus ScalaAnalysisSequence
belegt. Somit liefert apply das Objekt mit der eine VoTUM-Analyse durchgeführt
wird.
2. In ScalaAnalysisSequence gibt es die Methode newPasses, welche neue Instanzen der
Analyse-Passes erstellt und in einer Liste zurückgibt.
3. Innerhalb der anonymen Klasse wird die Methode newPasses aufgerufen und jedem
Pass wird als Parameter an die Methode addPass(.) von AbstractAnalysisSequence übergeben. Somit hat die anonyme Klasse nun seine eigenen Instanzen der AnalysePasses.
Einbindung in VoTUM: Damit muss jetzt lediglich noch dafür gesorgt werden, dass in
der Methode loadAnalysisPlugin() im AnalysisPluginController nicht nur versucht wird mit
Reflections eine neue Instanz zu erzeugen, sondern auch versucht wird die apply-Methode
aufzurufen. Dieser Methodenaufruf kann (wie newInstance) ebenfalls über Reflections realisiert werden. Sobald der AnalysisPluginController die Instanz von apply() bekommt, ist
die Analyse nicht mehr von den Java Analysen zu unterscheiden.
Anmerkung: In Scala kann man abkürzend für MyAnalyse.apply() einfach MyAnalyse() schreiben (ohne new).
19
5 Realisierung
5.3 Analyse-Passes
Eine Analyse-Sequenz besteht aus einer oder mehreren Passes, die durchlaufen werden.
Die einzelnen Passes wiederum iterieren abhängig von der Wahl der Iterationsstrategie
über den CFG des Programms und annotieren dabei die Knoten. Der zeitliche Verlauf
der Annotations-Änderungen an den Knoten nennt man auch den Datenfluss der AnalysePasses. Dieser wird dann beim Aktualisieren einer Annotation verändert.
Ein Pass wird vom Interface AnalysisPass beschrieben. Auch hier existiert eine abstrakte
Klasse AbstractAnalysisPass, die das Interface bereits implementiert. Beispiele für IterationsAlgorithmen, die von AbstractAnalysisPass erben, sind NaiveIterativeAlgorithm und
TransformationsPass.
Der Analyse-Schreiber leitet in der Regel nicht von AbstractAnalysisPass, sondern von
einem bestimmten Algorithmus ab. Je nach Algorithmus müssen bestimmte Methoden
implementiert werden.
Die Schnittstelle zu Scala ist hier zwischen den diversen Algorithmen-Klassen und der
zugehörigen Scala-Variante, wie es Abbildung 5.1 zeigt. Das heißt, dass die Logik aus den
bereits in Java implementierten Algorithmen übernommen wird.
Allgemeines zu den Algorithmen: Alle Algorithmen haben einen Namen name und eine Beschreibung description. Der Zugriff mit Getter- und Setter-Methoden von der JavaOberklasse ist in Scala vereinfacht, man kann das Prefix get bzw. set weglassen. Zudem definiert jeder Pass noch die Richtung (Vorwärts oder Rückwärts) sowie die Kontroll-FlussStrategie. Allerdings sind diese beiden Variablen direction und strategy von der Oberklasse übernommen, inklusive ihrer Festlegung, was bereits im Konstruktor erfolgt.
Beispielhaft sei nun der NaiveIterativeAlgorithm herausgegriffen.
NaiveIterativeAlgorithm: Dieser Iterations-Algorithmus arbeitet auf einem Lattice-Element, das gewisse Methoden zur Initialisierung und Verarbeitung benötigt (der Ausdruck
LE steht für die Datenstruktur des Lattice-Elements):
Abstrakte Methoden von ScalaNaiveIterativeAlgorithm:
•
initStart(CfgNode)
•
evalEdge(CfgEdge, LE)
•
initEdge(CfgEdge)
•
initCarrier()
Bereitgestellte Methoden/Variablen von ScalaNaiveIterativeAlgorithm
20
•
carrier:
•
bot
•
all:
Gibt Lattice zurück (erst nach der Initialisierung sinnvoll)
und top: Top- und Bottom-Element des Lattice
für getEdges()
5.4 Lattice-Definition
Zusätzlich zu Name und Beschreibung benötigt dieser Algorithmus noch die Festlegung eines Schlüssels für den Datenfluss dataFlowValueKey, der die Kanten-Annotationen
eindeutig dieser Analyse zuordnet.
5.4 Lattice-Definition
Besonders wichtig für die Analyse ist die Definition des Complete Lattice, des vollständigen
Verbands. Charakterisiert wird ein Lattice durch folgende Eigenschaften:
• Definition eines Lattice-Elements, welches implementierungstechnisch eine Datenstruktur (wie z.B. Map, Set, Tupel, Interval, ...) ist.
• Vom Lattice-Element gibt es ein ausgezeichnetes > und ein ⊥ Element.
• Zudem werden über einem Lattice-Element folgende drei Methoden von einem Complete Lattice bereitgestellt:
–
merge(...)
zum Vereinen zweier Lattice-Elemente,
–
widen(...)
zum Widening zweier Lattice-Elemente und
– einer Ordnungsrelation lessOrEqual(...) auf den Verbandselementen.
Es gibt in VoTUM bereits eine Vielzahl von konkreten Lattice Implementierungen, die
über Sets, Listen und Intervallen einen Verband definieren. Diese sind im Package de.tum.
in.wwwseidl.votum.analyses.carriers zu finden.
Das Ziel bei der Umsetzung eines Lattice ist auf relativ einfache Weise vor allem kombinierte Verbände zu definieren und dann auch einen effektiven und einfachen Zugriff auf
die Lattice-Elemente zu ermöglichen.
Da man besonders hier von den Fähigkeiten von Scala profitieren kann, ist es nicht wie
bei den Analysen möglich, eine generelle Regel für die Migration zu Scala festzulegen:
Jede Lattice-Definition, die im Java Bereich auf Abbildung 5.2 zu sehen ist, muss gesondert
betrachtet und dann bestmöglich in Scala umgesetzt werden.
Eine Fähigkeit von Scala, die hier ausgenutzt werden soll, ist die besondere Unterstützung von Maps, Sets und Tupeln. Darauf wird im Kapitel 5.5 zur Erweiterung der Lattice
und Lattice-Elemente näher eingegangen, zuerst ein paar allgemeine Erweiterungen und
Definitionen.
Erweiterungen
Funktionsnamen verkürzt: Eine erste Vereinfachung bzw. zumindest Verkürzung ist das
Umbenennen der bestehenden Methoden merge, widen und lessOrEqual (bzw. die im konkreten Lattice implementierten Methoden mergeImpl, widenImpl und lessOrEqualImpl) in \/, /\
und <=. Dadurch, daß Scala diese Sonderzeichen als Methodennamen erlaubt, sind diese
wesentlich prägnanteren Bezeichner möglich.
21
5 Realisierung
Abbildung 5.2: Schnittstelle Java-Scala für die Lattice-Klassen
22
5.4 Lattice-Definition
Abbildung 5.3: Hierarchie zur Repräsentation der Lattice-Elemente
Lattice-Element Klassen: Lattice-Elemente werden immer über eine bestehende Datenstruktur gebildet. Diese wird um zwei ausgezeichnete Elemente erweitert, die als top und
bot Elementen bezeichnet werden. Dabei kann top und bot ein konkretes Element der Datenstruktur sein (wie z.B. die leere Menge in einem Set Lattice-Element) oder es sind spezielle Elemente, die außerhalb der Datenstruktur definiert sind. Die letzte Eigenschaft verlangt
ein Einpacken der Datenstruktur.
Eingepackte Lattice-Elemente: Ähnlich dem in funktionalen Sprachen üblichen OptionDatentyp, wird ein neuer Datentyp LatticeElement zum Einpacken definiert. Dieser besteht
aus drei Konstruktortypen: Elem, Top und Bot (siehe Abbildung 5.3). Dabei ist es sinnvoll,
diese Struktur als Traits zu implementieren. So kann man später die Funktionalität in einen
bestehenden Datentyp mixen und muss nicht die Vererbungshierarchie ändern. In Scala
können Klassen, wie in Java auch, nur von einer Klasse erben. Dagegen ist es erlaubt,
mehrere Traits in einen Klasse zu mixen. Alternativ kann man die Elemente aber auch
einfach mit konkreten Wrapper-Klassen einpacken.
Alle Traits sind covariant zum generischen Typ A definiert. A ist der Datenstruktur-Typ,
über den das Lattice-Element definiert ist. Abbildung 5.3 zeigt die Beziehung der abstrakten Traits zu den Wrapper-Klassen (x steht dabei für eine Instanz der Datenstruktur). Zur
Bedeutung:
•
Element(x):
•
TopE(x)
•
TopN
Ein konkretes Objekt des (generischen) Typs A.
und BotE(x): Top und Bot Elemente, die ein konkretes Objekt vom Typ A darstellen (also ein Objekt der Datenstruktur).
und BotN: Top und Bot Elemente, die kein konkretes Objekt repräsentieren.
Anmerkung: Hier wird bei der Umsetzung der in Abbildung 4.1 gezeigte Typ Nothing
als Typ A angegeben. Aufgrund der Covarianz darf dann überall wo ein LatticeElement[A]
erwartet wird, auch eine Instanz von TopN verwendet werden.
23
5 Realisierung
⊤
...
­2
­1
0
1
2
...
⊥
Abbildung 5.4: Funktionsweise der flat-Funktion
Flat und Lift
Die nun folgenden Methoden sind in der Klasse AnalysisFunctions im Package de.tum.in.
untergebracht. Diese Klasse soll eine Sammlung nützlicher
Funktionen darstellen, zu denen die Methoden flat und lift gehören.
Eine Form eines Lattice ist das Flat-Lattice. Dieses wurde in VoTUM als konkrete Klasse
definiert (s. Abbildung 5.2). Das Flat-Lattice definiert sich über einen konkreten Lattice
und ist flach. Das bedeutet, dass alle Elemente innerhalb des Lattice unvergleichbar sind
(siehe Abbildung 5.4). Die Definition von flat lautet wie folgt:
wwwseidl.scalavotum.analyses
flat Diese Funktion ist auf jeder beliebigen Datenstruktur definiert. Zurückgegeben wird
ein vollständiger Verband, der folgende Eigenschaften besitzt:
• Erstellt top und bot als eigenständige Objekte im Verband.
• Definiert eine Funktion zum Vergleichen von Objekten innerhalb des Verbands. Dabei sind alle Elemente unvergleichbar, es gilt aber ∀x.⊥ < x und ∀x.> > x.
•
/\
ist identisch mit \/.
•
\/
berechnet lediglich if(<=(x,
y))y else top.
• Verwendung: Definition eines Lattice über eine Datenstruktur, ohne dabei bestimmte Elemente der Datenstruktur als bot und top ausweisen zu müssen.
Die Methode lift wird auf einen bestehenden Verbund angewandt und ersetzt bei diesem das top und bot Element durch neue Elemente. Abbildung 5.5 zeigt dies. Die Definition von lift ist:
lift Als Parameter benötigt diese Funktion einen vollständigen Verband, der bereits eine
Ordnungsrelation definiert. Das Ergebnis ist wiederum ein Lattice, mit folgenden Eigenschaften:
• Ersetzt die alten top und bot Elemente mit neuen, eigenständige Objekte vom Typ
TopN bzw. BotN.
• Verwendet /\ und \/ aus dem übergebenen Lattice.
24
5.5 Erweitern von Lattice und Lattice-Element
⊤
⊤
­1
0
⊥
⊤
1
­1
0
1
⊥
⊥
Abbildung 5.5: Funktionsweise der lift-Funktion
• Ändert <= wie in flat ab.
• Verwendung: Wenn man auf einem bereits existierenden Lattice die top und bot
Elemente in der Analyse verwenden möchte, ohne dass sie eben als besondere Elemente gekennzeichnet sind. Es gibt z.B. Set-Lattice-Definitionen, die als bot Element
die leere Menge haben.
5.5 Erweitern von Lattice und Lattice-Element
Dieser Abschnitt widmet sich der konkreten Erstellung, Verwendung und Erweiterung
eines Lattice samt Lattice-Element. Vorweg sind folgende Problemstellungen für Lattice
und seine Elemente genannt:
Erweiterung von Lattice: Ein Verband soll sich erweitern lassen, um so neue Funktionen
hinzuzufügen, die dann in der Analyse verwendet werden können.
Erweiterung von Lattice-Elementen: Auch die Lattice-Elemente sollen erweitert werden können, um so neue Funktionen bereit zu stellen oder evtl. Funktionen zu überschreiben. Die toString()-Methode ist ein Beispiel für eine Methode, die man beim
Erweitern eines Elements überschreiben möchte.
Selftype von bestimmten Funktionen (in Lattice und Lattice-Element): Man möchte bei
manchen Operationen den Typ des Lattice-Elements zurückgeliefert bekommen. Das
bedeutet, wenn man ein Lattice-Element mit class MySet extends Set definiert, so möchte man bei den für Sets üblichen Operationen + und - nicht eine Instanz von Set zurück geliefert haben, sondern von MySet. Das ist problematisch, weil die genannten
Operationen eigentlich bereits in Set definiert sind.
Für ein Lattice betrifft dieses Problem die Operationen \/, /\ und <=.
25
5 Realisierung
Einfache (funktionale) Erweiterung: Oft möchte man nicht kompliziert die grundlegende Funktionalität eines Lattice-Element ändern, sondern lediglich Funktionen hinzufügen, die den Zugriff auf das Lattice-Element vereinfachen. Wiederum ist die
toString()-Rückgabe ein gutes Beispiel.
Da Scala mit implicit eine einfache (automatische) Konvertierung zwischen Datentypen ermöglicht, sollte dieses Scala-Konzept hier eine vereinfachte Definition des
Lattice-Elements ermöglichen.
Prinzipien
Über die Probleme bei Erweiterungen und dem Problem der selftypes wurden die ScalaCollections von Version 2.7.7 auf 2.8 komplett überarbeitet. Das neue Collection-Package
vermeidet Code-Duplikate und bietet dennoch die Möglichkeit der selftypes bei Methoden. In [14] wird zuerst aus Benutzersicht auf die neuen Collection-Hierarchie eingegangen. Anschließend wird aus Collection-Entwicklersicht aufgezeigt, wie eigene Datenstrukturen umgesetzt werden können, um den maximalen Vorteil aus der Umstrukturierung zu
erhalten.
Das generelle Prinzip ist: Man definiert die Methoden so allgemein wie möglich und
verlangt dann nur wenige konkrete Implementierungen, um alle anderen Methoden zur
Verfügung zu stellen. Da man hier beim Erstellen einer neuen Datenstruktur dem Problem
begegnet, dass man bei selftypes neue Instanzen dieser neuen Datenstruktur benötigt (in
VoTUM sind hierfür Factory-Klassen notwendig), werden sogenannte Builder einführt.
Die Traits, die sich noch nicht auf den selftype festlegen, erhalten das Suffix Like. Zum
Beispiel gibt es für Maps das Trait MapLike. Zu beachten ist, dass noch in allgemein gültigen Methoden-Definitionen und den für mutable und immutable Datenstrukturen unterschiedlichen Methoden-Definitionen aufgeteilt wird. Somit gibt es ein MapLike in Package scala.collection und jeweils eines in scala.collection.immutable und scala.collection.
mutable.
Alle Like-Traits haben gemeinsam, dass sie als letzten generischen Typ einen selftype
This verlangen. Dieser soll dem konkreten Typ entsprechen. Also dem Wert, den man von
den Methoden + und - als Rückgabe erwartet.
In Anlehnung an die Umstrukturierungen von Scala 2.8 werden alle konkreten Lattices als Traits definiert. Möchte man konkrete Implementierungen, mixt man sich das gewünschte Trait in die neue Klasse. Genauso funktioniert das mit Lattice-Elementen. Allgemeine Lattice-Element-Definitionen sind in Traits definiert. Das konkrete Lattice-Element
erhält man durch Zusammenmixen der gewünschten Traits.
Das Resultat ist, Lattice-Elemente definieren zu können, die bereits eine riesige Funktionsvielfalt haben.
Erweiterung von Lattice
Erweitert man ein Lattice, so möchte man weitere Funktionen hinzufügen. Da es im Lattice
selbst keine Funktionen gibt, die das selftype-Probleme haben, muss beim Erweitern nichts
beachtet werden.
Einzig allein beim Erstellen eines eigenen Lattice-Traits, das als Basis für konkrete LatticeKlassen dienen soll, und zudem noch neue Funktionen hat, die den korrekten Lattice-
26
5.5 Erweitern von Lattice und Lattice-Element
Element-Typ zurückgeben soll, gibt es einige Hinweise zu beachten (ein Beispiel ist
MapLatticeLike).
Hat man eine Methode, die als Eingabe ein Lattice-Element erwartet, auf diesem arbeitet
und es zurück gibt, treten keine Probleme auf. Diese Situation ist in Listing 5.2 gezeigt.
Listing 5.2: Einfache Funktionserweiterung
1
2
3
4
5
// ...
def removeFirstElement(x : LE) : LE = {
x -= x.first
return x
}
Problematischer ist es, wenn man eine neue Instanz des Lattice-Elements benötigt. Da
man kein konkretes Objekt erzeugen kann, weil sonst der Rückgabetyp auf den Typ dieses Objekts festgelegt wird, muss man einen Builder definieren, über den man die neue
Instanz abstrakt erstellt. Listing 5.3 zeigt die Signatur eines solchen Builders (aus Trait
MapLatticeLike). Listing 5.4 zeigt, wie das genannte Problem gelöst wird. Den Builder, den
man mit newBuilder erhält, muss man für das konkrete Lattice definieren.
Dies geschieht in der Regel jedoch beim zugehörigen Lattice-Element und man leitet
den Methodenaufruf aus dem Lattice lediglich an das Lattice-Element weiter.
Listing 5.3: Signatur eines Map-Builder
1
2
// builder for map; meaning: map entries of type tuple (K,V), instance of map with type E
def newBuilder : Builder[(K,V), E]
Listing 5.4: Verwendung eines Builders
1
2
3
4
5
6
7
8
// ...
def removeFirstElement(x : LE) : LE = {
val b = newBuilder
// add any element to collection by adding it to the builder
b += createElementOfLE()
// compute the resulting LE
return b.result
}
Erweiterung von Lattice-Element
Wie bereits mehrfach erwähnt, ist ein großes Problem bei Collection-Klassen, dass sie sich
selbst bei manchen Operationen zurück geben. Das ist bei mutable Objekten weniger das
Problem, bei immutable Objekten führt das jedoch zu Schwierigkeiten. Da Scala schon immer versucht die immutable Datenstrukturen in einer objektorientierten Welt zu unterstützen, ist der Schritt in Version 2.8, der Restrukturierung des Collection-Package, notwendig
gewesen.
Diese gewonnenen Vorteile möchte man auch bei den Lattice-Elementen haben. Also,
so wenig wie möglich selbst definieren zu müssen, und dennoch von der riesigen, bereits
existierenden, Funktionsvielfalt profitieren.
Nach der bereits erläuterten Einführung der Like-Klassen ist es demnach ratsam, diese
auch zu verwenden.
27
5 Realisierung
Als Beispiel wollen wir ein eigenes Lattice-Element aus einer immutable Map erstellen.
In der Scala-Doc (siehe [15]) findet man die Signatur aus Listing 5.5. Die generischen Werte
A und B stehen für Schlüssel- und Wert-Typen. This dagegen definiert den selftype. Ferner
steht in der Scala-Doc, dass noch die in Listing 5.6 aufgeführten Methoden zu implementieren sind.
Listing 5.5: Signatur des immutable.MapLike Traits
1
trait MapLike[A, +B, +This <: MapLike[A, B, This] with Map[A, B]] extends MapLike[A, B,
This]}
Listing 5.6: Verwendung eines Builders
1
2
3
4
def
def
def
def
get(key: A): Option[B]
iterator: Iterator[(A, B)]
+ [B1 >: B](kv: (A, B)): Map[A, B1]
- (key: A): This
5
6
7
// so that methods like, take and drop return objects of type This, you have to implement
empty
def empty: This
Macht man dies, hat man bereits alle Anforderungen erfüllt, d.h. man hat eine Klasse,
die Methoden wie find, foreach, ... kennt und darüber hinaus sogar für Methoden wie take,
slice, ... den eigenen Typ zurückgeben.
In Abbildung 5.6 sind alle Traits, aus denen sich das MapLE zusammensetzt, graphisch
dargestellt, Listing 5.7 zeigt das Ganze dann als Quelltext.
Auffallen dürfte der implizite Parameter lattice vom Typ ScalaLattice[_]. Dieser ist wegen des Traits AbstractLE notwendig. Dieses Trait sorgt dafür, dass das Lattice-Element
selbst weiß, ob es top oder bot ist, indem es das zugehörige Lattice fragt. Der implizite
Parameter taucht dann auch wieder in den (vorgeschriebenen) Methoden im Companion
Objekt auf.
Die Frage ist, woher bekommt man diesen Parameter? Da in der Regel ein LatticeElement in seinem Lattice erstellt wird - also in MapLattice - und dieses intern einen implizit
verwendbaren Parameter von sich selbst hat, wird MapLE mit der Instanz des MapLattice instanziiert, in der es auch erstellt wird.
Listing 5.7: Verwendung eines Builders
1
2
3
4
5
class MapLE[K,+V](val map : Map[K,V] = HashMap[K,V]())(implicit val lattice :
ScalaLattice[_])
extends Map[K,V] with MapLike[K,V,MapLE[K,V]]
with MapLELike[K,V,MapLE[K,V]]
with MapLEHashMap[K,V,MapLE[K,V]]
with AbstractLE {
6
override def empty = MapLE.empty[K,V]
7
8
def +[V1 >: V](kv: (K, V1)) = new MapLE(map + kv)
def -(key : K) : MapLE[K,V] = {
if(!map.contains(key)) this
else new MapLE(map - key)
}
9
10
11
12
13
14
override def toString = "MapLE | " + super.toString
15
16
}
28
5.5 Erweitern von Lattice und Lattice-Element
Abbildung 5.6: Traits von MapLE
17
18
19
20
21
object MapLE {
def newBuilder[A,B](implicit lattice : ScalaLattice[_]) = new
MapBuilder[A,B,MapLE[A,B]](empty)
def empty[A, B](implicit lattice : ScalaLattice[_]) : MapLE[A,B] = new MapLE[A,B]
}
Einfache (funktionale) Erweiterung
Wie bereits in der Einführung erwähnt möchte man, statt ein Lattice zu verändern, oft nur
weitere Funktionen hinzufügen, um so vor allem eine transparentere Verwendung des
Lattice für den Analyse-Schreiber anzubieten.
Scala bietet mit seinen implicit-Konzept ein mächtiges Werkzeug, das hier zum Einsatz
kommt.
Der relevante Trait ist hierbei ExtLattice, der im selben Package wie ScalaLattice liegt.
ExtLattice erbt von ScalaLattice und hat selbst den generischen Typ A. Dieser Datentyp
repräsentiert unsere erweiterte Datenstruktur, enthält somit neue/überschriebene Funktionen. Zusätzlich zu diesem generischen Wert gibt es noch den Typ B in ExtLattice. Zu diesem
generischen Wert B verlangt das ExtLattice-Trait eine ScalaLattice-Variable mit dem Namen
backLattice. Eine weitere Anforderungen ist die Definition zweier implicit-Methoden für
die Konvertierung von Typ A zu Typ B und umgekehrt.
Der Trick ist nun, dass das konkrete Lattice ExtLattice einmixt und nach außen hin auf
dem Lattice-Element A arbeitet. Intern jedoch werden über das sogenannte backLattice und
den implicit-Konvertierungen alle Operationen auf dem Datentyp B ausgeführt.
Die folgenden Anmerkungen zu dieser Umsetzung mit dem ExtLattice sind bei der Verwendung zu beachten:
• Typ A und B müssen bis auf die Möglichkeit der Hin- und Rückkonvertierung keinerlei
29
5 Realisierung
Beziehung zueinander haben. Allerdings ist es oft sinnvoll, in Typ A eine Variable zu
haben, die bei der Konvertierung von B nach A das Objekt vom Typ B speichert, um
es dann bei der Rückkonvertierung einfach zurückzugeben.
• Zuerst werden Methoden von Typ A aufgerufen. Falls sie nicht existieren, wird eine
Methode in Typ B gesucht. So ist es möglich Methoden aus B zu überschreiben, wie z.B.
toString(). Würde man nicht auf die gezeigte Art und Weise erzwingen, dass zuerst
die Methode in A gesucht wird, wäre das Überschreiben nicht möglich.
Anmerkung: Man könnte sich z.B. überlegen erst im konkreten Algorithmus die implicitKonvertierung von B nach A zu definieren. In diesem Fall würde aber jede Methode, die in A und B existiert, immer auf B aufgerufen. Es besteht kein Grund für die
implicit-Konvertierung, da die Methode in B existiert.
• Von Nachteil ist, dass man in A lediglich funktionale Erweiterungen machen darf. Das
bedeutet, man sollte keine Variablen in A definieren, deren Gültigkeit außerhalb einer
Funktion liegen. Der Grund hierfür ist, dass bei der intern auftretenden implicitKonvertierung von A nach B und zurück das ursprüngliche Objekt verloren geht und
bei der Rückkonvertierung eine neue Instanz vom Typ A erstellt wird.
Anmerkung: In diesem Fall müsste man gemäß der Ausführungen in 5.5 ein eigenes
vollständiges Lattice-Element erstellen.
• Im konkreten Lattice können ohne Probleme Methoden hinzugefügt werden, die
z.B. den Zugriff auf das Lattice-Element vereinfachen. Für Operationen auf Objekten
vom Typ A und B können in diesem Fall die zugehörigen Konstruktoren verwendet
werden, da die beiden Typen ineinander überführt werden können.
Möchte man jedoch das Trait als Basis weiterer Lattice-Definitionen verwenden, sollte man das wiederum über das Trait ExtLattice tun und nicht vom Basis-Lattice erben.
5.6 Pattern Matching mit Objekten
Prinzipiell bietet Scala zwei Möglichkeiten an, um Pattern Matching mit Objekten zu verwirklichen [11]: Entweder können speziell markierte Klassen, sogenannte Case Classes, verwendet werden, oder man erstellt sogenannte Extraktor-Objekte.
Case Classes bieten sich an, wenn man eine Hierarchie oder Baumstruktur komplett neu
entwirft; jedoch ist das für die Knoten des CFG in VoTUM nicht der Fall, da bereits eine
Klassenhierarchie besteht.
Somit fällt die Wahl auf die Extraktor-Objekte. Mit Objekte sind die singleton-objects aus
Scala gemeint. Ein vollständiges Extraktor-Objekt definiert lediglich eine Methode unapply.
Mit Hilfe dieser unapply-Methoden kann ein Transformieren des Graphen in eine andere
Darstellung vermieden werden, d.h. man arbeitet weiterhin direkt auf den bereits existierenden Java-Klassen.
Neben den Knoten des Kontrollflussgraphen soll auch die Funktionalität der Klasse
ExprsChooser mit Objekt-Pattern-Matching umgesetzt werden. Diese Klasse filtert die Knoten des CFGs nach bestimmten Kriterien. Bisher basiert ExprsChooser auf regulären Ausdrücken, das heißt Objekte werden in Strings umgewandelt und darauf wird ein Pattern
30
5.6 Pattern Matching mit Objekten
Matching mit einem regulären Ausdruck als String durchgeführt. Das ist offensichtlich
nicht nur sehr fehleranfällig bei der Verwendung, sondern verlangt zudem in Java eine
extrem aufwändige Implementierung und einen hohen Verwaltungsaufwand.
Damit verwenden wir also in beiden Anwendungsfällen die Variante mit unapply. Konkret bedeutet der erste Anwendungsfall, dass Subtypen von CmacStmt mit Pattern Matching
funktionieren sollen und in Anwendungsfall zwei sind es Subtypen von CmacExpr. Dabei ist
CmacStmt eine Unterklasse von CfgEdge, welche die Oberklasse aller Kanten eines CFGs ist.
Da sich diese Arbeit dem Cmac-Bereich von VoTUM widmet, reichen die Subklassen von
CmacStmt aus.
Extraktor Objekte
Listing 5.8: Extraktor-Objekt
1
2
3
object IF {
def unapply(s : ConditionalStmt) = Some(s.getRelop(), s.getLeft(), s.getRight())
}
Extraktor-Objekte sind singleton-Klassen mit der Gestalt von Listing 5.8. Das Beispiel
zeigt einen Subtyp von CfgEdge, das ConditionalStmt. Es wird als Parameter an die unapplyMethode übergeben. Als Rückgabewert wird ein Option-Tupel mit den Bestandteilen - also
den internen Variablen - definiert. Der Option-Datentyp bietet dabei die Möglichkeit eines
Some(..., ...)-Ausdrucks mit beliebig vielen Argumenten, oder einfach nur den Wert None,
was gleichbedeutend damit ist, dass die unapply-Methode nicht angewendet werden kann
und somit kein Matching vorliegt. Betrachtet man Listing 5.8 genauer, so identifiziert man
die Bestandteile des von IF.unapply zurückgegebenen Tupels: Eine Operation relop sowie
den linken und rechten Operanden. Diese werden bei einem Pattern Match ggf. automatisch weiter mit unapply ausgepackt.
Anzumerken ist, dass im Beispiel allein durch die Festlegung des Parametertyps als
ConditionalStmt ein Matching nur dann zutrifft, wenn der Ausdruck von eben diesem Typ
ist. In 4.3 ist bereits ein Anwendungsfall für den IF-Extraktor betrachtet worden.
Im Sonderfall, bei dem ein Datentyp aus keinen weiteren inneren Elementen besteht,
wird ein Boolean zurückgegeben. Dieser sagt aus, ob das Pattern für das übergebene Objekt
gematcht werden kann.
Anmerkung: Im Rahmen des zu dieser Arbeiten gehörenden Projekts entstand auch ein
in Groovy geschriebenes Skript, das dazu dient, die gerade erläuterten Extraktor-Objekte
auch für die PPC-Statements zu erstellen. Ein Skript ist deswegen sinnvoll, weil es über
25 solcher Statements gibt. Das Resultat des Skripts ist die Klasse PPCInstrsExtractors im
Package de.tum.in.wwwseidl.scalavotum.compilers.ppc.instrs.
Anwendung für CfgEdge und CmacExpr
Aus der Struktur der Unterklassen von CfgEdge und CmacExpr, das heißt insbesondere aus
den Parametern des Konstruktors, kann man ablesen, aus welchen Bestandteilen sich die
Klasse zusammensetzt. Dieses Wissen muss nun in die Extraktor-Objekte gepackt werden,
so dass ein einfaches Auspacken der inneren Elemente möglich ist.
31
5 Realisierung
Zum Beispiel besteht das AssignmentStmt stets aus einer DataLocationExpr und einem weiteren CmacExpr, was aus dem Konstruktor der zugehörigen Java-Klassen gelesen werden
kann (siehe Konstruktor in Listing 5.9).
Listing 5.9: Konstruktor von AssignmentStmt
1
2
3
4
5
6
7
// JAVA-CODE
public class AssignmentStmt extends CmacStmt {
// ...
public AssignmentStmt(DataLocationExpr location, CmacExpr expr) {
// ...
}
}
Die konkreten Umsetzungen sind in
CfgEdgeExtractors
im Package
de.tum.in.wwwseidl.
scalavotum.graph für CfgEdge und in CmacExprExtractors im Package de.tum.in.wwwseidl.scalavotum.
compilers.cmac.instrs
für CmacExpr zu sehen.
Anwendung in ExprsChooser
Bereits am Anfang dieses Unterkapitels wurde kurz auf den ExprsChooser eingegangen (im
Package de.tum.in.wwwseidl.scalavotum.compilers.cmac.analysistools). Er stellt einen Filter
für die Knoten des CFGs bereit und enthält zudem Methoden zum nützliche Abfragen
von bestimmten Knotentypen (z.B. alle Variablen oder Konstanten).
Für die Optimierung dieser Klasse kann man neben dem Pattern Matching auch auf
die Möglichkeit der Closures zurückgreifen, indem man Filter-Closures definiert. Innerhalb
eines Filter-Closure definiert man in der Regel ein Pattern Matching auf einem CmacExpr
Objekt und liefert einen Wahrheitswert zurück.
Alle weiteren Funktionen der Klasse ExprsChooser sind entweder wie in der Java-Klasse
aus VoTUM definiert oder Kombinationen der genannten Möglichkeiten.
Listing 5.10 zeigt eine mögliche Verwendung des ExprsChooser. Dabei wird in der newPassesMethode eine Instanz mit dem Pattern aus usefulExprs erzeugt. Das heißt, es wird die Methode usefulExprs als Funktionsparameter an ExprsChooser übergeben.
Listing 5.10: Verwendung von ExprsChooser
1
2
3
4
5
6
def newPasses = {
// ...
var pattern = new ExprsChooser(usefulExprs)
// ...
}
7
8
9
10
11
12
13
def usefulExprs(expr : CmacExpr) = expr match {
case BIN(_, REG(_), REG(_)) => true
case BIN(_, REG(_), CONST(_)) => true
case BIN(_, CONST(_), REG(_)) => true
case _ => false
}
32
6 Vergleich mit PAG
Abschließend gilt es noch, die Ergebnisse in dieser Arbeit zu bewerten und die entstandenen Vorteile von Scala in VoTUM gegenüber bestehenden Projekten zu evaluieren. Wieder
dient die PAG Software von AbsInt mit ihrer eigenen Analyse-Sprache als Referenz.
AbsInt verwendet die selbstentwickelten proprietären Sprachen FULA/DATLA zur Definition von Analysen und Verbänden. Aufgrund der großen Vorteile von funktionalen
Sprachen im Bereich der Programmanalyse sind FULA und DATLA von diesem Sprachtyp. Mit Scala bietet VoTUM nun ebenfalls eine funktionale Sprache für das Schreiben der
Analysen an.
Für die Weiterentwicklung und für die Fehlerbehebung der Sprachen FULA und DATLA ist allein AbsInt verantwortlich. Anders ist es bei Scala, sie wird von einer großen
Community vorangetrieben und die wachsende Popularität lässt auf eine langfristige Unterstützung schließen.
Für eine prorietäre Sprache spricht die mögliche Spezialisierung auf das gegebene Problem, dem Schreiben von Programmanalysen. Allerdings ist der dadurch gewonnene Vorteil im Vergleich zum hohen Aufwand für die Pflege und Weiterentwicklung einer Sprache
gering.
Zudem ist Scala eine vollständige Sprache; zwar war das mit Java in VoTUM schon vorher der Fall, doch musste man die Analysen sehr umständlich schreiben. Die Sprache aus
PAG ist eher eine kleine, speziell auf Analysen-Definition ausgelegte Sprache. Das merkt
man besonders, wenn z.B. komplexere Verbände für die Analyse notwendig sind. Diese
sind nicht direkt mit der Sprache FULA erstellbar, sondern müssen in C programmiert
und dann eingebunden werden.
Somit ist die in VoTUM realisierte Analyse-Definition mit Scala wesentlich mächtiger,
als dies mit AbsInts Sprachen FULA/DATLA der Fall ist.
33
6 Vergleich mit PAG
34
7 Zusammenfassung
Zusammenfassend ist festzustellen, dass VoTUM sich mit der Entscheidung, für Scala und
der hier gezeigten Umsetzung im Hinblick auf die Einfachheit Analysen zu schreiben, definitiv verbessert hat. Die Analysen sind in Scala nicht nur kürzer und prägnanter, sondern
lassen sich auch intuitiver definieren als es in Java möglich ist.
Allerdings gibt es noch weitere Konzepte in Scala, die in VoTUM für eine Verbesserung sorgen können. Herausgegriffen sei die elegante Unterstützung von XML durch Scala, welche man für die Ausgabe der Analyse-Ergebnisse verwenden kann und so die zur
Zeit verwendeten, unleserlichen HTML-String-Konkatenationen überflüssig macht. Ferner gibt es das Konzepte der Varianz von generischen Werten, welche in dieser Form in
Java gar nicht unterstützt wird. Mit dieser Fähigkeit kann man erlauben, dass eine generische Klasse K[T] Untertypen der Klasse K[S] ist, wenn T ein Untertyp von S ist.
Von Vorteil könnte Scala auch sein, wenn man die Performanz durch ein paralleles Ausführen der Analysen steigern möchte, weil mächtige Konzepte zur Parallelisierung in Scala
zur Verfügung stehen.
35
Literaturverzeichnis
[1] Absint programm: Pag. http://www.absint.com/pag. [Online; letzter Zugriff
07.11.2009].
[2] Clojure language. http://clojure.org. [Online; letzter Zugriff 18.11.2009].
[3] Funktionale programmierung und skripting mit der jvm, groovy. http://www2.
in.tum.de/hp/file?fid=127. [Online; letzter Zugriff 07.10.2009].
[4] Groovy language. http://www.groovy.org. [Online; letzter Zugriff 18.11.2009].
[5] Groovy multiple assignment.
http://groovy.codehaus.org/Multiple+
Assignment+Proposal. [Online; letzter Zugriff 07.10.2009].
[6] Interop java - scala.
http://www.codecommit.com/blog/java/
interop-between-java-and-scala. [Online; letzter Zugriff 05.10.2009].
[7] Interop
java
scala
2.
http://www.eishay.com/2009/05/
scala-java-interoperability-statics.html.
[Online; letzter Zugriff
15.11.2009].
[8] Lecture: Absint zu programmanalyse. http://rw4.cs.uni-sb.de/teaching/
esd07/.
[9] Lecture: Compiler 2 uni karlsruhe.
lehre/SS2009/compiler2/.
http://pp.info.uni-karlsruhe.de/
[10] Lecture: Program optimization ws07.
vorlesungen/WS07/optimierung.
http://www2.in.tum.de/lehre/
[11] Matching objects with patterns.
http://infoscience.epfl.ch/record/
98468/files/MatchingObjectsWithPatterns-TR.pdf?version=4. [Online; letzter Zugriff 07.10.2009].
[12] Programming scala (online version).
http://programming-scala.labs.
oreilly.com/index.html. [Online; letzter Zugriff 01.12.2009].
[13] Scala class hierarchie. http://www.scala-lang.org/sites/default/files/
images/classhierarchy.png. [Online; letzter Zugriff 16.12.2009].
[14] Scala collection redesign in 2.8. http://www.scala-lang.org/sid/3. [Online;
letzter Zugriff 23.12.2009].
[15] Scala doc, version 2.8. http://www.scala-lang.org/archives/downloads/
distrib/files/nightly/docs/library/index.html. [Online; letzter Zugriff
01.01.2010].
37
Literaturverzeichnis
[16] Scala language.
01.11.2009].
http://www.scala-lang.org.
[Online; letzter Zugriff
[17] Scala language research. http://www.scala-lang.org/node/143. [Online;
letzter Zugriff 18.11.2009].
[18] A tour of scala. http://www.scala-lang.org/node/104. [Online; letzter Zugriff 01.12.2009].
[19] Votum. http://www2.in.tum.de:8080/votum.
[20] Dierk Koenig. Groovy im Einsatz. Carl Hanser Verlag, 2007.
[21] Flemming Nielson. Principles of Program Analysis. Springer, 1999.
[22] Martin Odersky. Programming in Scala. Artima, 2008.
38
Herunterladen