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