Fachhochschule Aachen Fachbereich Elektrotechnik und Informationstechnik Entwicklung eines MiniJavaOO-Compilers für JVM-Bytecode BACHELORARBEIT Zur Erlangung des akademischen Grades Bachelor of Science in Informatik Aachen, den 14. Juli 2009 Name: Matrikelnummer: Erstgutachter: Zweitgutachter: Michael Pasdziernik 317677 Prof. Dr. rer. nat. Heinrich Faßbender Prof. Dr.-Ing. Martin Oßmann Erklärung Hiermit versichere ich, daß ich die vorliegende Arbeit selbständig verfaßt und keine anderen als die angegebenen Quellen und Hilfsmittel benutzt habe, daß alle Stellen der Arbeit, die wörtlich oder sinngemäß aus anderen Quellen übernommen wurden, als solche kenntlich gemacht sind und daß die Arbeit in gleicher oder ähnlicher Form noch keiner Prüfungsbehörde vorgelegt wurde. Aachen, den 14. Juli 2009 Inhaltsverzeichnis 1 Einleitung 1.1 Motivation und Ziele . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.2 Überblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.3 Beispielprogramm . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 11 12 14 2 Hintergrund 2.1 Die Programmiersprache MiniJavaOO . . . . 2.1.1 Sprachelemente von MiniJavaFunProc 2.1.2 Sprachelemente von MiniJavaOO . . . 2.1.3 Ergänzungen zur Sprachspezifikation . 2.2 Die Java Virtual Machine . . . . . . . . . . . 2.2.1 Architektur . . . . . . . . . . . . . . . 2.2.2 Erzeugen von Bytecode . . . . . . . . 2.3 Auswahl eines Compilergenerators . . . . . . 2.3.1 Anforderungen und Alternativen . . . 2.3.2 ANTLR . . . . . . . . . . . . . . . . . 2.4 Agiles Projektmanagement . . . . . . . . . . . . . . . . . . . . . 18 18 19 20 21 22 22 24 25 25 26 30 . . . . . . . . . . . . . . . . 34 34 37 37 38 40 45 47 47 49 49 54 55 55 58 65 66 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 Der Compiler 3.1 Aufbau und Phasen . . . . . . . . . . . . . . . . . . . 3.2 Parser und abstrakte Syntax . . . . . . . . . . . . . . 3.2.1 Spracherweiterung über semantische Prädikate 3.2.2 Grenzen des LL(∗)-Parsergenerators . . . . . . 3.2.3 Abstrakter Syntaxbaum . . . . . . . . . . . . . 3.2.4 Fehlerbehandlung . . . . . . . . . . . . . . . . . 3.3 Symboltabellen und Typsystem . . . . . . . . . . . . . 3.3.1 Design . . . . . . . . . . . . . . . . . . . . . . . 3.3.2 Traversieren des AST . . . . . . . . . . . . . . 3.3.3 Aufbau der Symboltabellen . . . . . . . . . . . 3.3.4 Fehlerbehandlung . . . . . . . . . . . . . . . . . 3.4 Semantische Analyse . . . . . . . . . . . . . . . . . . . 3.4.1 Semantische Prüfungen . . . . . . . . . . . . . 3.4.2 Namensauflösung . . . . . . . . . . . . . . . . . 3.5 Generieren von Jasmin-Assemblercode . . . . . . . . . 3.5.1 Struktur des Assemblercodes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 Inhaltsverzeichnis 3.5.2 Funktionsweise des Code-Emitters . . . . . . . . . . . . . . . . . . . 3.5.3 Ermitteln der Größe von Methoden-Stacks . . . . . . . . . . . . . . . Generieren und Verifizieren von JVM-Bytecode . . . . . . . . . . . . . . . . 68 72 74 4 Testgetriebene Compiler-Entwicklung 4.1 Tests mit gUnit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.2 Tests mit jUnit und StringTemplate . . . . . . . . . . . . . . . . . . . . . . 77 77 82 5 Zusammenfassung und Ausblick 87 Literaturverzeichnis 90 3.6 A Syntax und statische Semantik von MiniJavaOO A.1 Programmstruktur . . . . . . . . . . . . . . . . A.2 Klassen . . . . . . . . . . . . . . . . . . . . . . A.3 Typen, Literale, Bezeichner und Namen . . . . A.4 Referenzieren von Variablen und Methoden . . A.5 Variablen . . . . . . . . . . . . . . . . . . . . . A.6 Methoden . . . . . . . . . . . . . . . . . . . . . A.7 Ausdrücke . . . . . . . . . . . . . . . . . . . . . A.8 Blöcke und Statements . . . . . . . . . . . . . . A.9 Syntaxdiagramme . . . . . . . . . . . . . . . . . . . . . . . . . 91 92 93 96 97 98 99 101 102 105 B Grammatiken B.1 Lexer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . B.2 Parser . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . B.3 AST-Parser . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112 112 114 117 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . C Übersetzungsbeispiele 119 C.1 Binärbaum-Programm . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119 C.2 Ackermann-Programm . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126 4 Abbildungsverzeichnis 1.1 1.2 1.3 1.4 Die MiniJavaOO-Compiler . . . . . . . . . . Der MiniJavaOO-Compiler für die JVM . . Entarteter Binärbaum . . . . . . . . . . . . Binärbaum-Programm: Klasse Nil im AST . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 12 14 16 2.1 2.2 2.3 2.4 2.5 2.6 DEA zur Regelentscheidung von unendlichem k Debugging mit ANTLRWorks . . . . . . . . . . ANTLR IDE in Eclipse . . . . . . . . . . . . . Sprint Backlog in Agilo for Scrum . . . . . . . Sprint Burndown in Agilo for Scrum . . . . . . Timeline in Agilo for Scrum . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27 28 29 31 32 32 3.1 3.2 3.3 3.4 3.5 3.6 3.7 3.8 3.9 3.10 3.11 3.12 3.13 3.14 3.15 3.16 3.17 3.18 3.19 3.20 3.21 3.22 3.23 Analyse-Phasen des Compilers . . . . . . . . . . . . . . . . . . . . . . . . Synthese-Phasen des Compilers . . . . . . . . . . . . . . . . . . . . . . . . Klassen zur Steuerung des Compilers . . . . . . . . . . . . . . . . . . . . . Parser- und Lexer-Klassen . . . . . . . . . . . . . . . . . . . . . . . . . . . DEA zur Regelentscheidung mit semantischen Prädikaten . . . . . . . . . Klasse AstNode zur Symbol-Annotation . . . . . . . . . . . . . . . . . . . Binärbaum-Programm: Globale Prozedur und main-Methode im AST . . Binärbaum-Programm: Deklaration von Instanzvariablen im AST . . . . . Ackermann-Programm: Ausdrücke im AST . . . . . . . . . . . . . . . . . Exception-Klassen zur Fehlerbehandlung . . . . . . . . . . . . . . . . . . . Klassen für Symbole und Symboltabellen . . . . . . . . . . . . . . . . . . Klassen für das Typsystem . . . . . . . . . . . . . . . . . . . . . . . . . . Klassen der AST-Parser zum Aufbau der Symboltabellen . . . . . . . . . Binärbaum-Programm: Symboltabellen . . . . . . . . . . . . . . . . . . . . Binärbaum-Programm: Annotation der Deklarationen im AST . . . . . . Klassen des AST-Parsers für die semantische Analyse . . . . . . . . . . . Klasse SemantikCheckWalkerBase . . . . . . . . . . . . . . . . . . . . Binärbaum-Programm: Annotationen im AST nach der Namensauflösung Symbolsuche . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Klassen für den Zugriffsschutz . . . . . . . . . . . . . . . . . . . . . . . . . Klassen des AST-Parsers für den Code-Emitter . . . . . . . . . . . . . . . Java-API des Jasmin-Assemblers . . . . . . . . . . . . . . . . . . . . . . . Klassen des Bytecode-Verifiers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34 35 36 37 38 41 42 44 45 46 48 48 50 53 54 55 56 62 63 65 68 75 75 5 Abbildungsverzeichnis 6 4.1 Klassen für die jUnit-Tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . C.1 C.2 C.3 C.4 C.5 Binärbaum-Programm: Globale Sprachelemente Binärbaum-Programm: Klasse Baum im AST . Binärbaum-Programm: Klasse Nil im AST . . Ackermann-Programm im AST 1 . . . . . . . . Ackermann-Programm im AST 2 . . . . . . . . im AST . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83 119 120 120 126 127 Listings 1.1 1.2 Beispielprogramm: Binärbaum in MiniJavaOO . . . . . . . . . . . . . . . . Beispielprogramm: Klasse Nil in Assemblercode . . . . . . . . . . . . . . . 14 17 2.1 Beispielprogramm: Ackermann-Funktion in MiniJavaFunProc . . . . . . . . 19 3.1 3.2 3.3 3.4 3.5 3.6 3.7 3.8 3.9 3.10 3.11 3.12 3.13 3.14 3.15 3.16 3.17 3.18 3.19 3.20 3.21 3.22 3.23 3.24 3.25 3.26 3.27 3.28 Binärbaum-Programm: Globale Prozedur und main-Methode . . . . . . . Parser-Regel: Übersetzen von Variablendeklarationen in abstrakte Syntax Binärbaum-Programm: Deklaration von Instanzvariablen . . . . . . . . . Parser-Regel: Übersetzen von Summen-Ausdrücken in abstrakte Syntax . Ackermann-Programm: Ausdrücke . . . . . . . . . . . . . . . . . . . . . . Lexer-Regel für das ANY-Token zur Fehlerbehandlung . . . . . . . . . . . AST-Parser-Regel zum Aufbau der Klassentabelle . . . . . . . . . . . . . AST-Parser-Regel zum Aufbau der Symboltabelle: Block . . . . . . . . . . AST-Parser-Regel zum Aufbau der Symboltabelle: Instanzvariable . . . . AST-Parser-Regel zur Prüfung der Zuweisung . . . . . . . . . . . . . . . . AST-Parser-Regel zur Namensauflösung . . . . . . . . . . . . . . . . . . . Methode zum Auflösen qualifizierter Namen . . . . . . . . . . . . . . . . . Methoden zur Suche in den Symboltabellen . . . . . . . . . . . . . . . . . Methode getSymbolRecursive . . . . . . . . . . . . . . . . . . . . . . Methode checkAccess zur Zugriffsschutz-Prüfung . . . . . . . . . . . . Methode accessOk zur Zugriffsschutz-Prüfung . . . . . . . . . . . . . . . Binärbaum-Programm: Klassendefinition und Instanzvariablen . . . . . . Binärbaum-Programm: Methode setValue in Assemblercode . . . . . . Binärbaum-Programm: Methode inorder in Assemblercode . . . . . . . Binärbaum-Programm: Konstruktor in Assemblercode . . . . . . . . . . . Binärbaum-Programm: if-Statement . . . . . . . . . . . . . . . . . . . . Binärbaum-Programm: if-Statement in Assemblercode . . . . . . . . . . AST-Parser-Regel: if-Statement im Code-Emitter . . . . . . . . . . . . . Templates: if-Statement im Code-Emitter . . . . . . . . . . . . . . . . . Ein Stack unbekannter Größe in Assemblercode . . . . . . . . . . . . . . . Template: print-Statement im Code-Emitter . . . . . . . . . . . . . . . . AST-Parser-Regel: print-Statement im Code-Emitter . . . . . . . . . . . AST-Parser-Regel: Methodenaufruf im Code-Emitter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42 43 43 44 45 47 50 51 52 57 59 61 63 64 64 65 66 66 67 68 69 69 70 71 72 73 73 74 4.1 gUnit-Test: Parser . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78 7 Listings 8 4.2 4.3 4.4 4.5 4.6 4.7 4.8 4.9 4.10 gUnit-Test: if und while im Parser . . . . . . . gUnit-Test: Lokale Konstanten im Parser . . . . gUnit-Test: Klasse im Parser . . . . . . . . . . . Anpassen von gUnit an die Klasse AstNode . . . gUnit-Test: AST-Parser für die Symboltabellen . jUnit-Test: Semantik der Methodenüberlagerung Templates: Test der Methodenüberlagerung . . . jUnit-Test: Methodenaufruf im Code-Emitter . . Templates: Test eines Methodenaufrufs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79 79 79 80 81 84 84 85 85 A.1 A.2 A.3 A.4 Beispielprogramm . . . . . . . . . . . . . main-Methode zum Programmstart . . . Einzelnes Statement zum Programmstart Minimalbeispiel einer Klasse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91 93 93 95 C.1 C.2 C.3 C.4 Binärbaum-Programm: Globale Sprachelemente in Assemblercode . Binärbaum-Programm: Klasse Baum in Assemblercode . . . . . . . Binärbaum-Programm: Klasse Nil in Assemblercode . . . . . . . . Ackermann-Programm in Assemblercode . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 121 123 124 127 . . . . . . . . . . . . . . . . Zusammenfassung In dieser Bachelorarbeit wird die Entwicklung eines Mehrphasen-Compilers mit Hilfe des compilergenerierenden Frameworks ANTLR beschrieben. Quellsprache ist die objektorientierte Sprache MiniJavaOO. Zielsprache ist Bytecode für die Java Virtual Machine. Der Compiler realisiert die typischen Phasen, aufgeteilt in ein Frontend und ein Backend, und nutzt zur internen Verarbeitung des Quellprogramms eine abstrakte Syntax. Neben der Implementierung des Compilers werden die testgetriebene Entwicklung sowie die Planung und Steuerung der Programmierung mit agilen Methoden betrachtet. 1 Einleitung Die vorliegende Arbeit befaßt sich mit der Entwicklung eines Mehrphasen-Compilers zur Übersetzung der objektorientierten Programmiersprache MiniJavaOO in Bytecode für die Java Virtual Machine (JVM). MiniJavaOO ist eine im Rahmen des Praxisprojekts zusammen mit Manuel Rieke entworfene Erweiterung von MiniJavaFunProc 1 . Die Programmiersprache MiniJavaFunProc ist prozedural und in Syntax bzw. Semantik an Java angelehnt. Sie dient dazu, während des Compilerbau-Praktikums an der FH Aachen einen Einphasen-Compiler auf der Basis einer LL(1)-Grammatik zu realisieren. Die Erweiterungen sind in einer gemeinsam erstellten Sprachspezifikation (Anhang A) festgehalten, auf deren Grundlage zwei Compiler (Abbildung 1.1) entwickelt werden, die mit unterschiedlichem Fokus verschiedene Aspekte des Compilerbaus beleuchten. ! "! #$ %&' %&' ( )* + + + 0/22 -)3 ! ,-,./01 ., ., /01 ., !!! ! !!! ! !!!! !! !!! ! !!! ! !!!! !! !!! ! !!! ! !!!! !! !!! ! !!! ! !!!! !! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !! ! ! ! !! ! ! ! !! ! ! ! !! ! ! ! ! ! ! ! ! ! ! ! !! ! ! ! !! ! ! ! !! ! ! ! !! ! ! ! ! ! ! ! ! ! ! Abbildung 1.1: Die MiniJavaOO-Compiler Manuel Riekes Arbeit [10] erweitert den im Praktikum entwickelten MiniJavaFunProcCompiler zur Übersetzung von MiniJavaOO in Assemblercode für die M32-CPU-Simulationsumgebung 2 . Der Fokus der Arbeit liegt auf der Umsetzung objektorientierter Sprachkonzepte mit dem Befehlssatz eines registerbasierten Prozessors. 1 2 Siehe http://www.fassbender.fh-aachen.de/Download/Compilerbau/Folien/ Siehe http://www.ossmann.fh-aachen.de/Down load/m32.zip. Die M32-CPU-Simulationsumgebung dient der Vermittlung von Grundlagen zur System- und Assemblerprogrammierung im Praktikum zur Vorlesung Architektur von Rechnersystemen und Betriebssystemkonzepte. 10 1.1 Motivation und Ziele Der in der vorliegenden Arbeit beschriebene Compiler ist eine Neuentwicklung mit Hilfe des compilererzeugenden Frameworks ANTLR 3 . Ziel ist die Entwicklung eines typischen Mehrphasen-Compilers, der intern eine abstrakte Syntax in Form einer Baumstruktur nutzt. Möglichst alle Phasen sollen mit Hilfsmitteln von ANTLR realisiert werden. Der Compiler übersetzt MiniJavaFunProc und MiniJavaOO auf die für objektorientierte Sprachen spezialisierte virtuelle Stackmaschine JVM. Die Programme sind somit direkt auf Rechnern mit installierter JVM ausführbar. 1.1 Motivation und Ziele Neben der Entwicklung von Compilern und Interpretern für allgemeine Programmiersprachen sind die Techniken des Compilerbaus in vielen Bereichen der Softwareentwicklung von großem Nutzen. Komplexen Problemen kann oft mit den Mitteln der metalinguistischen Abstraktion und durch den Einsatz domänenspezifischer Sprachen (Domain Specific Languages, kurz DSL) begegnet werden (vgl. [1], Kapitel 4, S. 375 ff.), weshalb neuere Programmiersprachen entsprechende Techniken in die Sprache integrieren (z. B. interne DSLs in Ruby 4 oder Parser-Kombinatoren in Scala 5 ). Durch die intensive Auseinandersetzung mit der Entwicklung eines Compilers im Rahmen der Bachelorarbeit können Kompetenzen erworben werden, die für die Tätigkeit des Softwareentwicklers nützlich sind. Konkret werden mit der Implementierung des MiniJavaOO-Compilers nachstehende Ziele verfolgt: Weiterentwicklung einer Programmiersprache Durch die Erweiterung der Programmiersprache MiniJavaFunProc um objektorientierte und weitere Sprachelemente, wie z. B. die Möglichkeit zur Deklaration lokaler Variablen an beliebiger Stelle innerhalb von Blöcken, wird die Spezifikation der Syntax wie auch der semantischen Einschränkungen einer formalen Sprache geübt und das Verständnis für die Konzepte der Objektorientierung vertieft. Realisierung eines Mehrphasen-Compilers Aufbauend auf den im Praktikum gemachten Erfahrungen in der Compilerprogrammierung, wird ein Mehrphasen-Compiler entwickelt, der den Anforderungen an die Übersetzung einer typischen Programmiersprache gerecht wird. Es werden verschiedene Techniken des Compilerbaus praktisch umgesetzt, wozu unterstützend auf ein compilererzeugendes Framework zurückgegriffen wird und Methoden der ingenieursmäßigen Softwareentwicklung, wie agiles Projektmanagement und testgetriebene Entwicklung, zur Anwendung kommen. 3 Siehe http://antlr.org/ Siehe http://www.artima.com/rubycs/articles/ruby as dsl.html 5 Siehe http://debasishg.blogspot.com/2008/04/external-dsls-made-easy-with-scala.html 4 11 1 Einleitung Wissensvertiefung: Java Plattform Java ist von besonderer Relevanz in der professionellen Softwareentwicklung. Im TIOBE-Index6 zur Verbreitung von Programmiersprachen rangiert Java seit Jahren auf den oberen Plätzen. Auch abstrakte Stackmaschinen wie die JVM oder auch die Common Language Runtime (CLR) des Microsoft .NET Frameworks liegen im Trend und werden immer häufiger als Zielplattform für Programmiersprachen gewählt. Durch die zur Umsetzung des Compilers erforderliche Auseinandersetzung mit den entsprechenden Sprach- und Maschinenspezifikationen ([3], [4] und [11]) wird das im Laufe des Studiums erlangte Wissen in diesem Bereich vertieft. 1.2 Überblick Der MiniJavaOO-Compiler (Abbildung 1.2) folgt der typischen Aufteilung in ein Frontend und ein Backend, die den Übersetzungsprozeß in mehreren sequenziell ausgeführten Phasen organisiert (vgl. [14], S. 146 ff.). Dabei wird die Eingabe mehrfach durchlaufen. Um dies effizient zu gestalten, übersetzt die erste Phase das MiniJavaOO-Programm in eine abstrakte Syntax mit der Struktur eines Baums. Die Aufteilung der Übersetzung auf mehrere Phasen ist insbesondere zur Prüfung der statischen Semantik nötig, z. B. kann mit einem Einphasen-Compiler eine Typangabe nicht überprüft werden, falls sie sich auf eine erst danach deklarierte Klasse bezieht. ! "! #$ $ $%&% $' $' $( !!! ! !!! ! !!!! !! !!! ! !!! ! !!!! !! !!! ! !!! ! !!!! !! !!! ! !!! ! !!!! !! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !! ! ! ! !! ! ! ! !! ! ! ! !! ! ! ! ! ! ! ! ! ! ! ! !! ! ! ! !! ! ! ! !! ! ! ! !! ! ! ! ! ! ! ! ! ! ! Abbildung 1.2: Der MiniJavaOO-Compiler für die JVM Zur Übersetzung durchläuft ein MiniJavaOO-Programm den Compiler wie folgt: • Parser und Lexer prüfen die lexikalische und syntaktische Korrektheit des Quellprogramms und übersetzen es in die zur Weiterverarbeitung notwendige abstrakte Syntax. Abschnitt 1.3 zeigt ein Beispiel für ein MiniJavaOO-Programm und die Übersetzung in abstrakte Syntax. 6 Der TIOBE-Index bewertet die Verbreitung einer Programmiersprache anhand der Anzahl auf sie spezialisierter Software-Ingenieure, Kurs-Angebote und Anbieter von Zusatzprodukten. Auf http://www.tiobe.com/index.php/content/paperinfo/tpci/index.html ist der monatlich aktualisierte Index zu finden, auf http://www.tiobe.com/index.php/content/paperinfo/tpci/tpci definition.htm findet sich eine Erläuterung der Bewertungskriterien. 12 1.2 Überblick • Es folgen zwei Durchläufe zum Aufbau der für die semantische Analyse nötigen Symboltabellen. Zunächst werden Symbole für die Klassendeklarationen angelegt, damit die verfügbaren Typen bekannt sind, anschließend für die übrigen Deklarationen im Programm. • Dann erfolgt die Phase der semantischen Analyse, die z. B. die Gültigkeit von Bezeichnern und Typangaben prüft oder Typkompatibilität und Zugriffsschutz sicherstellt. Ergebnis der Phasen bis hierher sind ein abstrakter Syntaxbaum7 (Abstract Syntax Tree, kurz AST), welcher die Anweisungsstruktur des Quellprogramms widerspiegelt, sowie Symboltabellen, welche die Deklarationsstruktur des Quellprogramms widerspiegeln. Durch Symbol-Annotationen im AST sind beide Strukturen zur Weiterverarbeitung miteinander verknüpft. Es folgt die Erzeugung von Bytecode in drei Phasen: • Zunächst wird aus den Informationen im AST und in den Symboltabellen Assemblercode erzeugt. In Abschnitt 1.3 findet sich auch ein Beispiel für JVM-Assemblercode. • Der Assemblercode wird in Bytecode übersetzt. • Der Bytecode durchläuft einen Verifier, der die Einhaltung aller strukturellen und semantischen Bedingungen der JVM überprüft. Zur Vorstellung der Programmiersprache MiniJavaOO, der Implementierung der einzelnen Compiler-Phasen und des Entwicklungsprozesses ist die Arbeit in folgende Kapitel unterteilt: In Kapitel 2 werden die Quellsprache MiniJavaOO und die Zielmaschine JVM des Compilers vorgestellt. Es werden auch die für den hier beschriebenen Compiler spezifischen MiniJavaOO-Erweiterungen erläutert, die nicht Teil der im Anhang angegebenen Spezifikation sind. Weiterhin werden die Auswahl eines compilererzeugenden Frameworks wie auch mögliche Hilfen zur Erstellung von JVM-Bytecode diskutiert und die Planung bzw. Steuerung der Entwicklung mit Hilfe agiler Methoden dargelegt. Kapitel 3 stellt die einzelnen Phasen des Compilers und ihre Implementierung vor und geht näher auf einzelne, besonders interessante Aspekte anhand von Beispielen ein. In Kapitel 4 werden die mit der testgetriebenen Compiler-Entwicklung gemachten Erfahrungen erläutert. Kapitel 5 bietet schließlich eine Zusammenfassung der Arbeit und einen Ausblick auf mögliche Weiterentwicklungen bzw. Verbesserungen des Compilers. In den Anhängen finden sich die Spezifikation der Syntax und der statischen Semantik von MiniJavaOO, die als Anforderungskatalog für die Analysephase des Compilers dient, die 7 Der abstrakte Syntaxbaum dient der Weiterverarbeitung des Quellprogramms im Compiler und stellt dessen logische Struktur dar. Er ist zu unterscheiden vom Syntax- oder Parsebaum, der die Ableitung des Quellprogramms darstellt. 13 1 Einleitung Lexer- und Parser-Grammatiken des Compilers in EBNF-Schreibweise (Extended BackusNaur Form 8 ) sowie die abstrakten Syntaxbäume und der Assemblercode zu den in diesem und im nächsten Kapitel vorgestellten MiniJavaOO-Programmen. 1.3 Beispielprogramm Um einen ersten Eindruck von MiniJavaOO, der abstrakten Syntax und dem Assemblercode für die JVM zu vermitteln, zeigt Listing 1.1 ein MiniJavaOO-Programm zur Realisierung des in Abbildung 1.3 gezeigten entarteten Binärbaums nach dem Composite-Pattern 9 . Abbildung 1.3: Entarteter Binärbaum Ein Baum wird durch miteinander verknüpfte Objekte der Klasse Baum (Zeilen 40 bis 59) dargestellt. Der Wert eines Knotens ist eine Integer-Zahl zwischen 0 und 9. Ein Blatt ist ein Knoten, dessen rechter und linker Teilbaum Objekte der Klasse Nil (Zeilen 62 bis 75) sind. Die Nil-Knoten werden in der Abbildung als Rechtecke dargestellt. Ein Nil-Knoten hat den Wert 10 und überlagert die Methoden setvalue und inorder der Superklasse Baum. In der globalen Funktion erzeugeBaum (Zeilen 5 bis 24) wird in einer while-Schleife ein entarteter Baum mit fünf Knoten erzeugt und zurückgegeben. Die globale Prozedur printBaum (Zeilen 27 bis 32) nimmt einen Baum als Parameter entgegen und gibt diesen zunächst ganz und daraufhin nur teilweise in Inorder aus. Die main-Prozedur (Zeilen 35 bis 38) ruft die Funktion erzeugeBaum auf und weist den zurückgelieferten Baum der in Zeile 2 deklarierten globalen Variablen zu. Danach wird der Baum durch Aufruf der Prozedur printBaum auf dem Bildschirm ausgegeben. Listing 1.1: (Beispielprogramm: Binärbaum in MiniJavaOO) 1 2 // globale Variable Baum baum; 3 8 9 Siehe http://de.wikipedia.org/wiki/Erweiterte Backus-Naur-Form Siehe http://sourcemaking.com/design patterns/composite 14 1.3 Beispielprogramm 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 // globale Funktion Baum erzeugeBaum() { int i = 0; Baum b = new Baum(); Baum aktuell = b; // erzeuge 5 Knoten while i < 4 { aktuell.setValue(i); // rechts immer ein Nil-Knoten aktuell.r = new Nil(); // links immer der naechste Knoten aktuell.l = new Baum(); aktuell = aktuell.l; i++; } // Abschluss des Baums mit Nil-Knoten aktuell.setValue(i); aktuell.l = new Nil(); aktuell.r = new Nil(); return b; } 25 26 27 28 29 30 31 32 // globale Prozedur void printBaum(Baum b) { // gibt den gesamten Baum in Inorder aus b.inorder(); // gibt nur die Knoten 4 und 3 aus b.l.l.l.inorder(); } 33 34 35 36 37 38 // Hauptprogramm main { baum = erzeugeBaum(); printBaum(baum); } 39 40 41 42 43 44 45 46 47 48 49 50 class Baum extends Object { // rechter und linker Teilbaum public Baum r, l; // Wert des Knotens protected int v; // einziger, obligatorischer Konstruktor Baum() {} public void setValue(int v) { if v <= 10 { if v >= 0 { this.v = v; 15 1 Einleitung } 51 } 52 } public void inorder() { this.l.inorder(); print(this.v); this.r.inorder(); } 53 54 55 56 57 58 59 } 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 // Unterklasse von Baum class Nil extends Baum { Nil() { this.v = 10; } // ueberlagerte Methode // Wert muss 10 bleiben! public void setValue(int v) { } // ueberlagerte Methode // Rekursion beenden, keine Ausgabe // sonst Null-Pointer-Exception public void inorder() { } } In Abbildung 1.4 wird der abstrakte Syntaxbaum der Klasse Nil, so wie er vom Parser für die Weiterverarbeitung des Quellprogramms im Compiler erzeugt wird, dargestellt. Unterhalb des class-Knotens finden sich, von links nach rechts betrachtet, zunächst die Angabe des Klassennamens und der Superklasse, daraufhin der Konstruktor, die Instanzvariablen und schließlich die Instanzmethoden. Der Teilbaum FIELDS hat keine weiteren Knoten, da in der Klasse keine Instanzvariablen deklariert wurden. class Nil extends Nil Baum this v METHODS BLOCK = QUALIFIEDNAME FIELDS METHOD MODIFIER 10 public void setValue METHOD PARAMS PARAM int BLOCK MODIFIER void inorder public v Abbildung 1.4: Binärbaum-Programm: Klasse Nil im AST 16 PARAMS BLOCK 1.3 Beispielprogramm Listing 1.2 zeigt den vom Compiler erzeugten JVM-Assemblercode. In den Zeilen 2 und 3 sind der Klassenname und die Superklasse definiert. Die Zeilen 5 bis 15 zeigen die beiden Methoden setValue und inorder, deren Methodenrümpfe jeweils leer sind. In den Zeilen 17 bis 26 wird schließlich der Konstruktor der Klasse dargestellt. Der Konstruktor der Superklasse Baum wird in den Zeilen 20 und 21 aufgerufen, daraufhin folgt die Übersetzung von this.v = 10; in den Zeilen 22 bis 24. Listing 1.2: (Beispielprogramm: Klasse Nil in Assemblercode) 1 2 3 .class public Nil .super Baum 4 5 6 7 8 9 .method public setValue(I)V .limit stack 0 .limit locals 2 return .end method 10 11 12 13 14 15 .method public inorder()V .limit stack 0 .limit locals 1 return .end method 16 17 18 19 20 21 22 23 24 25 26 .method public <init>()V .limit stack 2 .limit locals 1 aload_0 invokespecial Baum/<init>()V aload_0 ldc 10 putfield Baum/v I return .end method Die Darstellungen und Listings für die anderen Klassen finden sich in Anhang C.1. Im Laufe der Arbeit werden Ausschnitte dieses Beispielprogramms aufgegriffen, um verschiedene Aspekte des Übersetzungsprozesses zu erläutern. 17 2 Hintergrund Bevor auf die Implementierung des MiniJavaOO-Compilers eingegangen wird, liefern die Abschnitte dieses Kapitels das nötige Hintergrundwissen zur Programmiersprache MiniJavaOO bzw. zur Zielmaschine JVM. Zusätzlich werden die Möglichkeiten zur Generierung von Bytecode für die JVM vorgestellt, die Auswahl des zur Programmierung eingesetzten Compilergenerators ANTLR diskutiert sowie die Planung und Steuerung des Entwicklungsprozesses mit agilen Methoden beschrieben. 2.1 Die Programmiersprache MiniJavaOO Wie bereits in der Einleitung beschrieben, ist die Programmiersprache MiniJavaOO die Quellsprache für zwei während des Praxisprojekts entwickelte Compiler. Daraus ergeben sich folgende Rahmenbedingungen: • Die Sprache ist kompatibel zu MiniJavaFunProc. Jedes MiniJavaFunProc-Programm ist auch ein MiniJavaOO-Programm. • Die Sprache läßt sich mit einer kontextfreien LL(1)-Grammatik darstellen. Dies ist wegen des von Manuel Rieke eingesetzten Parsergenerators JavaCC 1 (siehe Abschnitt 2.3.2) nötig. • Die Sprache enthält die wichtigsten Konstrukte zur Objektorientierung. • Die Spracherweiterungen orientieren sich an der Java-Syntax. Es folgt ein Überblick über die von MiniJavaFunProc geerbten Sprachelemente sowie die spezifischen MiniJavaOO-Erweiterungen. Die formale Spezifikation der Sprache, bestehend aus den Syntaxregeln in EBNF-Notation und den Bedingungen der statischen Semantik, ist in Anhang A enthalten. Dort ist auch die Syntax in Form von Diagrammen zu finden. 1 Siehe https://javacc.dev.java.net/ 18 2.1 Die Programmiersprache MiniJavaOO 2.1.1 Sprachelemente von MiniJavaFunProc MiniJavaFunProc ist prozedural und in der Syntax an Java angelehnt. Die Sprache ist darauf ausgelegt, die Grundlagen des Compilerbaus zu vermitteln. Der Sprachumfang ist entsprechend klein, es gibt folgende Sprachelemente: • Integer als Datentyp • Globale Variablen und Konstanten • Globale Funktionen und Prozeduren mit der Möglichkeit zum rekursiven Aufruf • Lokale Variablen und Konstanten, deren Deklaration im Methodenrumpf vor den Statements stattfinden muß. • Ein globales Statement, mit dem die Ausführung des Programms beginnt. • Die Kontrollstrukturen if und while mit einfachen logischen Ausdrücken • Verschachtelte arithmetische Ausdrücke • Strukturierung von Statements in Blöcken • Ein print-Statement In Listing 2.1 wird ein MiniJavaFunProc-Programm angegeben, welches Beispiele für alle Elemente der Sprache enthält. Die globale Prozedur (Zeilen 7 bis 20) führt die Berechnung zweier verschachtelter arithmetischer Ausdrücke durch und gibt das Ergebnis auf dem Bildschim aus. Die globale Funktion in den Zeilen 23 bis 36 ist rekursiv aufgebaut und berechnet mit Hilfe einer lokalen Variablen und verschachtelten if-Kontrollstrukturen die Ackermann-Funktion2 für die Aufruf-Parameter. Das globale Statement ist ein Block (Zeilen 39 bis 48), der in einer while-Schleife die Ackermann-Funktion und anschließend die globale Prozedur aufruft. Die Übersetzung des Programms in abstrakte Syntax und in Assemblercode für die JVM finden sich in Anhang C.2. Listing 2.1: (Beispielprogramm: Ackermann-Funktion in MiniJavaFunProc) 1 2 3 4 // globale Konstante final int b = 5; // globale Variable int i; 5 6 7 8 9 // globale Prozedur void berechneAusdruck() { // lokale Variable final int x = 9; 2 http://de.wikipedia.org/wiki/Ackermannfunktion 19 2 Hintergrund //lokale Konstante int ergebnis; // Block als Statement der Prozedur { // verschachtelte arithmetische Ausdruecke ergebnis = x * x + ((56 + 9) / 3 - 5); // Bildschirmausgaben print(ergebnis); print(1 + 2 + 3 + 4 + 5 * 6); } 10 11 12 13 14 15 16 17 18 19 20 } 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 // globale, rekursive Funktion mit Parametern func ackermann(int a, int b) { // lokale Variable int ret = 0; // if als Statement der Funktion if b == 0 { ret = 1; } else if a == 0 { if b == 1 ret = 2; else ret = b + 2; } else { ret = ackermann(a-1, ackermann(a,b-1)); } return ret; } 37 38 39 40 41 42 43 44 45 46 47 48 // Block als globales Statement zum Programmstart { i = 0; while i + 2 < b { // Funktionsaufruf als Ausdruck im print-Statement print(ackermann(i, i + 2)); i = i + 1; } // Prozeduraufruf berechneAusdruck(); } 2.1.2 Sprachelemente von MiniJavaOO MiniJavaOO fügt der Sprache MiniJavaFunProc einen Teil der Objektorientierung von Java hinzu und beseitigt syntaktische und semantische Beschränkungen. Folgende Sprachelemente sind neu: 20 2.1 Die Programmiersprache MiniJavaOO • Objektreferenzen als weiterer primitiver Datentyp • Klassen mit Einfachvererbung sowie Object als Basis-Klasse • Instanzvariablen und Instanzmethoden • Zugriffsschutz für Instanzvariablen und Instanzmethoden • Polymorphie, Methodenüberlagerung und dynamisches Binden • Die Möglichkeit, lokale Variablen an beliebiger Stelle innerhalb von Blöcken zu deklarieren • Die Operatoren ++, -- und % Das MiniJavaOO-Programm aus Kapitel 1 zeigt wesentliche Merkmale der objektorientierten Erweiterungen. 2.1.3 Ergänzungen zur Sprachspezifikation Um einige erweiterte Möglichkeiten von ANTLR auszutesten, ergeben sich für den hier vorgestellten Compiler Änderungen gegenüber der MiniJavaOO-Sprachspezifikation aus Anhang A. Sie schränken die Syntax und die Semantik nicht ein, so daß ein der Spezifikation entsprechendes Programm korrekt übersetzt wird. Ein Programm, welches sich die hier angegebenen Erweiterungen zunutze macht, ist jedoch nicht mit Manuel Riekes Compiler übersetzbar. Initialisierung von Variablen Anders als in Java werden lokale Variablen in MiniJavaOO automatisch mit 0 bzw. null initialisiert. Um die Kompatibilität zu Java zu erhöhen, kann dieses Verhalten durch eine Option beim Aufruf des Compilers abgeschaltet werden. Hieraus ergibt sich eine ergänzende semantische Bedingung: Statische Semantik 2.1 (Zugriff auf lokale Variablen) (1) Vor dem Zugriff auf lokale Variablen müssen diese initialisiert werden (vgl. [11], S. 35). Zudem können Variablen bei der Deklaration mit dem Wert einer anderen Variablen initialisiert werden. Es ergibt sich folgende Änderung für die Regel SingleVariable aus Syntax A.13: Syntax 2.2 (Initialisierung von Variablen bei der Deklaration) SingleVariable → SimpleName ( = ( Name | NUMBER | ConstructorCall ) )? Zusätzlich werden die semantischen Bedingungen in A.14 erweitert: 21 2 Hintergrund Statische Semantik 2.3 (Initialisierung von Variablen bei der Deklaration) (1) Name muß eine Variable referenzieren. Syntaxvereinfachungen Durch die Möglichkeiten des ANTLR-Parsergenerators, die in Abschnitt 2.3.2 noch beschrieben werden, wird die Syntax von MiniJavaOO an zwei Stellen vereinfacht: • Das Schlüsselwort func ist bei der Definition einer globalen Methode optional. Damit entspricht die Syntax derjenigen von Instanzmethoden. Regel A.9 (10) fällt somit weg. • Increment- und Decrement-Statements sind auch in Präfix-Schreibweise zulässig. Hierdurch wird Regel A.11 (3) hinfällig. 2.2 Die Java Virtual Machine Nachdem im vorherigen Abschnitt ein Überblick über die Quellsprache des Compilers gegeben wurde, soll nun die Zielmaschine JVM näher betrachtet werden. Die Java Virtual Machine ist eine abstrakte Stackmaschine, deren Struktur und Befehlsvorrat auf die Übersetzung statisch typisierter, objektorientierter Programmiersprachen, insbesondere Java, ausgelegt sind. Die JVM ist keine konkret implementierte virtuelle Maschine, sondern die Spezifikation einer Schnittstelle zur Ausführung von in .class-Dateien organisiertem Bytecode. Relevant für ihre Implementierung ist die Java Virtual Machine Specification 2nd Edition [4], die auch online eingesehen werden kann3 . In ihr werden die Syntax und die Semantik von Bytecode definiert und das Verhalten der Maschine beim Laden und Linken von Klassen erläutert. Neben der Definition der Schnittstelle werden außerdem Hinweise zur CompilerEntwicklung und zur Implementierung der Maschine gegeben. 2.2.1 Architektur Die JVM hat viele Parallelen zu Java. Sie kennt Klassen und Interfaces und implementiert die gleichen primitiven Typen und Zugriffsrechte. Klassen können statische bzw. dynamische Felder und Methoden sowie Konstruktoren enthalten. Weiter gibt es eine direkte Unterstützung für Exceptions und nebenläufige Programmierung über Monitore. .class-Dateien Jede .class-Datei enthält eine Klasse oder ein Interface. Neben den Methodenimplementierungen besteht diese aus Verwaltungsinformationen in einem sogenannten Constant Pool. 3 Siehe http://java.sun.com/docs/books/jvms/ 22 2.2 Die Java Virtual Machine Der Constant Pool dient zur Laufzeit als eine Art Symboltabelle für die Klasse und beschreibt z. B. die Instanzvariablen und Zugriffsrechte. (Vgl. [5], S. 50 f. und [4], S. 93 ff.) Das Laden einer Klasse ist ein umfangreicher Prozeß, der sich rekursiv über alle Superklassen erstreckt. So wird u. a. die globale Symboltabelle der JVM um die Informationen des Constant Pool der Klassen ergänzt, werden Klassenvariablen initialisiert und Klasseninitialisierer ausgeführt. Auch werden durch einen Verifier die strukturellen und semantischen Bedingungen des Bytecodes sowie die Typsicherheit überprüft. Als sichere virtuelle Maschine vertraut die JVM also nicht den Compilern. (Vgl. [4], S. 140 ff. und S. 155 ff.) Datentypen und Befehlsvorrat Wie in Java sind die primitiven Typen boolean, byte, short, int, long, char, float und double sowie der reference-Typ implementiert. Zusätzlich existiert für den Methodenrücksprung der Typ returnAddress. Die Floatingpoint-Arithmetik entspricht dem üblichen Standard IEEE 754. (Vgl. [4], S. 61 ff.) Die JVM-Befehle operieren immer auf einem festgelegten Datentyp. Der Additionsbefehl iadd beispielsweise erwartet zwei int-Werte auf dem Stack, während fadd float-Werte erwartet. Ansonsten werden von der JVM Exceptions geworfen. Durch die Begrenzung auf 255 verschiedene Befehle werden nicht alle Datentypen gleichberechtigt unterstützt, so daß gegebenenfalls Typkonversionen vorgenommen werden müssen. Der Befehlsvorrat beinhaltet folgende Arten von Operationen (vgl. [4], S. 80 ff.): • Transfer von Werten zwischen lokalen Variablen, Instanzvariablen und dem Stack • Vielfältige arithmetische Operationen sowie direkte Stackmanipulationen • Typkonversionen, Erzeugen und Casten von Arrays und Objekten • Vielfältige bedingte und unbedingte Sprungbefehle und eine switch-ähnliche Operation • Aufruf von Instanz- und Klassenmethoden und Konstruktoren sowie Rücksprungbefehle • Befehle zum Exception-Handling und zur Synchronisation von Threads Datenbereiche zur Laufzeit Jeder Thread hat einen eigenen Program Counter (Befehlsregister) sowie einen eigenen Stack. Auf dem Stack wird für jede laufende Methode ein Frame abgelegt. Ein Frame enthält die Methodenargumente, lokale Variablen, Informationen für den Rücksprung sowie einen Operand Stack zur Berechnung von Ausdrücken. Die Größe des Operand Stack wird pro Methode fest angegeben und muß daher vom Compiler berechnet werden. 23 2 Hintergrund Der Heap nimmt alle instanzspezifischen Informationen von Objekten und Arrays, wie z. B. Werte von Variablen, auf und ist allen Threads gemein. In der Methode Area und dem Runtime Constant Pool, die auch von allen Threads geteilt werden, sind alle statischen Informationen zu Klassen und Interfaces enthalten. Dies sind u. a. die Namen von Feldern und Methoden, Zugriffsrechte und die Implementierungen der Methoden. (Vgl. [4], S. 67 ff.) 2.2.2 Erzeugen von Bytecode Es gibt drei verschiedene Vorgehensweisen, um mit einem Compiler Bytecode für die JVM zu erzeugen. Man kann, entsprechend der Spezifikation in [4], direkt binären Bytecode erzeugen. Dies ist jedoch sehr aufwendig und fehleranfällig. Die zweite Möglichkeit ist die Nutzung von Bibliotheken, die von den Details des Bytecodes abstrahieren. Der dritte, hier eingeschlagene Weg nutzt eine Assemblersprache, um nahe an der Spezifikation zu entwickeln, ohne aber die genaue binäre Darstellung der .class-Dateien kennen zu müssen. Bibliotheken für Java Bekannte Java-Biblioteheken sind die Byte Code Engineering Library 4 , kurz BCEL, die Teil des Apache Jacarta-Projekts ist, sowie die ASM Library 5 der ObjectWeb Middleware. Beide Bibliotheken stellen Bytecode als Baumstruktur ähnlich dem DOM6 (Document Object Model ) dar und erlauben so die Erzeugung, Manipulation, Analyse und Verifikation von .class-Dateien. Noch ein Abstraktionsniveau höher ist die Bibliothek Cojen 7 anzusiedeln. Mittels einer Builder-Klasse werden von der JVM auszuführende Aktionen spezifiziert, aus denen Cojen optimierte Befehlsketten generiert. Cojen wurde von Wolfram Fischer im Compilerbau-Praktikum genutzt, um MiniJavaFunProc auf die JVM zu übersetzen. Der Jasmin-Assembler Von Sun wird kein Assembler für JVM-Bytecode zur Verfügung gestellt. Es gibt einen Disassembler, an dessen Syntax sich der für den MiniJavaOOCompiler genutzte Jasmin-Assembler 8 orientiert. Ursprünglich als Beigabe des Buchs Java Virtual Machine [5] entwickelt, wird er seitdem stetig an Veränderungen der JVM angepaßt. Die Assembleranweisungen abstrahieren von der binären Darstellung der Datenstrukturen zur Definition der Klassen, Felder und Methoden in .classDateien, während die eigentlichen Maschinenbefehle innerhalb der Methodenrümpfe entsprechend den in der Spezifikation angegebenen Mnemonics 9 notiert werden. 4 Siehe http://jakarta.apache.org/bcel/index.html Siehe http://asm.ow2.org/ 6 Baumdarstellung von HMTL-Seiten und JavaScript-Code innerhalb eines Webbrowsers. 7 Siehe http://cojen.sourceforge.net/ 8 Siehe http://jasmin.sourceforge.net/ 9 Ein Mnemonic ist die textuelle Notation einer binären Maschineninstruktion. 5 24 2.3 Auswahl eines Compilergenerators 2.3 Auswahl eines Compilergenerators Zur Entwicklung des MiniJavaOO-Compilers soll ein Compilergenerator eingesetzt werden, der neben der Lexer- und Parsergeneration weitere Aspekte der Entwicklung eines Mehrphasen-Compilers unterstützt. Im Vorfeld des Praxisprojekts wurde ANTLR unter verschiedenen Alternativen ausgewählt. 2.3.1 Anforderungen und Alternativen Basis für die Suche nach einem geeigneten Framework waren die folgenden Anforderungen: • Der Compiler soll mit Java entwickelt werden. Dies ermöglicht es, Teile des im Praktikum entwickelten Compilers wiederzuverwenden. • Der Parsergenerator soll auf der Grundlage einer LL(k)-Grammatik in EBNF-Notation einen Top-Down-Parser erzeugen. Somit kann auf den theoretischen Grundlagen der Vorlesung Compilerbau und der im Praktikum entwickelten LL(1)-Grammatik aufgebaut werden. • Gute und ausführliche Dokumention sowie Support durch eine lebendige Anwendergemeinschaft sollen eine zügige und effiziente Entwicklung unterstützen. Das Framework soll ausgereift sein und aktiv weiterentwickelt werden. Als Anhaltspunkte dienen die Versionsgeschichte und die Mailingliste des Projekts. • Unterstützung für die Erzeugung und Traversierung eines abstrakten Syntaxbaums und ein Plugin für Eclipse 10 wären wünschenswert. Neben ANTLR wurden folgende Compiler-Frameworks näher betrachtet: JavaCC Bekanntester Parsergenerator in der Java-Welt ist JavaCC, welcher auf LL(1)Grammatiken beruht. Bei Bedarf können einzelne Regeln auf LL(k) mit festem k umgestellt werden. Das Erzeugen eines AST wird durch JJTree unterstützt, zur Traversierung muß ein Java-Interface nach dem Visitor-Pattern 11 implementiert werden. Die Dokumentation beschränkt sich auf eine Referenz für die Grammatik-Notation und die Java-API sowie einige Tutorials. Das Nachrichtenaufkommen auf der Mailingliste ist sehr hoch, seit mindestens 2004 erscheinen regelmäßig neue Versionen. Der Parsergenerator JavaCC wird von Manuel Rieke zur Entwicklung des MiniJavaOOCompilers für M32-Assemblercode eingesetzt. 10 11 Siehe http://www.eclipse.org/ Siehe http://sourcemaking.com/design patterns/visitor 25 2 Hintergrund Coco/R Coco/R 12 ist ein an der Johannes Kepler Universität Linz entwickelter LL(1)Parsergenerator für Java, C# wie auch viele weitere Sprachen. Auch hier können einzelne Regeln auf LL(k) mit festem k umgestellt werden. Zusätzlich können Alternativen über semantische Prädikate entschieden werden. Die Dokumentation ist sehr umfangreich, u. a. wird Coco/R in [12] ausführlich behandelt. Es stehen Plugins für Eclipse, NetBeans 13 und weitere Entwicklungsumgebungen zur Verfügung. Coco/R wird seit mindestens 2003 entwickelt, es erscheinen regelmäßig neue Versionen. Eine Mailingliste für Anwender existiert nicht. Grammatica Grammatica 14 ist ein LL(k)-Parsergenerator für beliebige k. Von Interesse ist die strikte Trennung von Grammatik und Java-Sourcecode: Der Sourcecode für die Regel-Aktionen wird in einer von der Parserklasse abgeleiteten Klasse notiert. Es existiert nahezu keine Dokumentation. Die Mailingsliste wird wenig genutzt. Die erste stabile Version stammt aus dem Jahr 2003, zwischen August 2003 und März 2009 sind keine neuen Versionen erschienen. 2.3.2 ANTLR ANTLR wird seit 1989 von Terence Parr an der University of San Francisco entwickelt und unterstützt neben Java viele weitere Sprachen. Die Software ist ausgereift und sehr gut dokumentiert. Neben einem Wiki und einer API-Referenz ist vor allem das Handbuch [8] hervorzuheben. Das Projekt hat die im Vergleich zu den anderen Parsergeneratoren aktivste Mailingliste. Besondere Features sind außerdem: LL(*)-Parsergenerator Bei LL(k)-Parsergeneratoren muß in der Grammatik global oder für jede Regel ein fester Wert für k angegeben werden, auf dessen Grundlage die LookAhead-Entscheidung für die Regelalternativen der kontextfreien Grammatik getroffen wird. LL(∗) ermöglicht auch unendliches k. Hierzu erzeugt ANTLR Parser, deren Look-Ahead-Entscheidung auf zyklischen deterministischen endlichen Automaten (DEA) basiert. Beispiel 2.1 verdeutlicht den Sachverhalt: Die Regel für Prozedurdefinitionen soll zwischen konkreten und abstrakten Prozeduren unterscheiden (vgl. [8], S. 255). Beispiel 2.1 (Look-Ahead-Entscheidung mit unendlichem k) procedure → void IDENT ( params? ) ; | void IDENT ( params? ) block params → int IDENT 12 ( , int IDENT )∗ Siehe http://www.ssw.uni-linz.ac.at/Research/Projects/Coco/ Siehe http://www.netbeans.org/ 14 Siehe http://grammatica.percederberg.net/index.html 13 26 2.3 Auswahl eines Compilergenerators Durch den Kleene-Stern in der Regel params hat die Regel procedure einen unendlichen Look-Ahead. In der Praxis haben Methoden jedoch eine begrenzte Anzahl Parameter, so daß der von ANTLR generierte zyklische DEA (Abbildung 2.1) effizient zwischen den beiden Regelalternativen von procedure entscheiden kann. s10 ’int’ s9 IDENT ’,’ ’,’ s11 ’}’ ’;’ ’int’ s0 ’void’ s1 IDENT s2 ’{’ s4 IDENT ’}’ s6 s3 ’}’ s5 s7=>1 ’{’ s8=>2 Abbildung 2.1: DEA zur Regelentscheidung von unendlichem k In [8], Kapitel 11 und [7] wird das LL(∗)-Parsen ausführlich beschrieben. Es erlaubt die Formulierung weitaus natürlicherer Grammatiken, als dies mit LL(k)-Parsergeneratoren möglich ist. Trotz der Möglichkeiten bleiben auch viele Einschränkungen des Top-Down-Parsens erhalten, so sind z. B. linksrekursive Regeln nicht umsetzbar (vgl. [8], S. 264 ff.). Effizient werden die von ANTLR generierten Parser durch Linear Approximate Lookahead (vgl. [7], Folie 7), wodurch die Komplexität von LookAhead-Entscheidungen für LL(k) mit k > 1 von exponentiellem auf linearen Aufwand reduziert wird. Für Sprachkonstrukte, die auch mit LL(∗) nicht umsetzbar sind, bietet ANTLR semantische und syntaktische Prädikate. Laut Aussage von Terence Parr kann ANTLR auf diese Weise mit nahezu jeder kontextfreien Grammatik und sogar mit vielen kontextsensitiven Sprachkonstrukten umgehen (vgl. S. [8], 283 ff.). Unterstützung für abstrakte Syntaxbäume ANTLR-Grammatiken bieten direkte Unterstützung für die Übersetzung eines Quellprogramms in eine abstrakte Syntax und die Ausgabe eines entsprechenden, mit den Token des Lexers annotierten abstrakten Syntaxbaums. Der AST kann dann mit einem Tree-Parser traversiert werden. Dieser kann von ANTLR auf der Basis einer Parser-Grammatik für die abstrakte Syntax erzeugt werden. Template-Engine für den Code-Emitter Weiterhin unterstützen ANTLR-Grammatiken die Ausgabe von Text über die Template-Engine StringTemplate 15 . Dies vereinfacht die Ausgabe vom Zielcode, da StringTemplate entsprechend der Struktur einer kontextfreien Grammatik einzelne Textfragmente rekursiv und ineinander verschachtelt ausgeben kann. 15 Siehe http://www.stringtemplate.org/ 27 2 Hintergrund Entwicklungsumgebungen Mit ANTLRWorks 16 gibt es eine spezielle IDE für die Grammatik-Entwicklung. Besonders hilfreich sind der integrierte Debugger mit Einzelschrittmodus und verschiedene Visualisierungsmöglichkeiten. Es gibt Darstellungen für die EBNF-Regeln als Syntaxdiagramm und die DEAs der Look-Ahead-Entscheidungen. Außerdem können sowohl Parsebäume als auch abstrakte Syntaxbäume visualisiert werden. Abbildung 2.2 zeigt den Debugger in ANTLRWorks. Im EditorBereich ist ein Breakpoint gesetzt, an dem sich der Debugger-Lauf gerade befindet. Darunter sind das Quellprogramm, der aktuell aufgebaute Parsebaum sowie eine Auflistung der durchlaufenen Regeln zu sehen. Abbildung 2.2: Debugging mit ANTLRWorks Zur Integration in Eclipse kann zwischen drei verschiedenen Plugins gewählt werden, zur Entwicklung des MiniJavaOO-Compilers wurde ANTLR IDE 17 eingesetzt. Abbildung 2.3 zeigt die ANTLR IDE innerhalb von Eclipse. Im Editor ist eine Grammatik geöffnet, deren Regeln in der Outline aufgelistet sind. Unter dem Editor ist die 16 17 Siehe http://www.antlr.org/works/index.html Siehe http://antlrv3ide.sourceforge.net/ 28 2.3 Auswahl eines Compilergenerators ANTLR-Console zu sehen. Abbildung 2.3: ANTLR IDE in Eclipse Tests mit gUnit In ANTLR integriert ist das Testframework gUnit 18 , welches es erlaubt, Parser- und Baumparser-Grammatiken zu testen. Die Tests werden als EingabeAusgabe-Paare formuliert, die gUnit mittels eines Parser-Laufs verifiziert. gUnit bietet einen Interpreter, der die Testfälle mit Hilfe der Java-Reflection-API überprüft, wie auch einen Generator für jUnit 19 . 18 19 Siehe http://www.antlr.org/wiki/display/ANTLR3/gUnit+-+Grammar+Unit+Testing Siehe http://www.junit.org/ 29 2 Hintergrund 2.4 Agiles Projektmanagement Zur Planung und Steuerung der Entwicklung des Compilers werden Praktiken und Artefakte der agilen Methode Scrum (siehe [2], S.149 ff.) genutzt. Da Scrum auf ein Entwicklerteam und unterschiedliche Rollen ausgelegt ist, ist eine ganzheitliche Umsetzung der Methode während des Praxisprojekts nicht möglich. Wie im folgenden erläutert wird, können dennoch Teile der Methode erfolgreich durch einen einzelnen Entwickler angewandt werden. Zur Planung und Steuerung des Prozesses kommt die webbasierte Software Agilo for Scrum 20 zum Einsatz. Der Entwicklungsprozeß ist in drei Phasen, jeweils durch einen Meilenstein repräsentiert, aufgeteilt. Zum Erreichen der Meilensteine wird in Sprints zu je einer Woche entwickelt. Ein Sprint ist eine abgeschlossene Iteration, in der zu Beginn des Sprints geplante Features umgesetzt werden. Ein Feature ist fertiggestellt, wenn es programmiert und ausführlich getestet wurde. Montags wird zunächst die Sprintplanung durchgeführt, in der, mit Blick auf das bisher Erreichte, die Ziele für die laufende Woche festgelegt werden. Während des Sprints kann der Fortschritt stetig durch einen Soll-Ist-Vergleich überwacht werden. Meilensteine Zu Beginn des Praxisprojekts werden folgende Meilensteine festgelegt: 1. Fertigstellung des MiniJavaOO-Parsers: Der Parser erzeugt einen abstrakten Syntaxbaum. Die MiniJavaOO-Erweiterung kann deaktiviert werden, um einen reinen MiniJavaFunProc-Parser zu erhalten. Zur Fertigstellung werden zwei Sprints benötigt. 2. Fertigstellung des MiniJavaFunProc-Compilers: Alle Compiler-Phasen bis zur Erstellung von JVM-Bytecode sind für die Sprache MiniJavaFunProc fertiggestellt. So wird die Gesamtarchitektur des Compilers zunächst an einer kleineren Menge Sprachelemente erprobt, deren praktische Umsetzung bereits aus dem Praktikum bekannt ist. Es entsteht eine Art vertikaler Prototyp. Der Meilenstein wird nach weiteren drei Sprints erreicht. 3. Fertigstellung des MiniJavaOO-Compilers: Alle Compiler-Phasen bis zur Erstellung von JVM-Bytecode sind für die Spracherweiterung MiniJavaOO fertiggestellt. Das Gesamtbild ist durch den vorherigen Meilenstein bekannt, die einzelnen Compiler-Phasen werden nun um die MiniJavaOO-Sprachelemente erweitert. Nach weiteren fünf Sprints wird der letzte Meilenstein erreicht und der Compiler somit fertiggestellt. 20 Siehe http://www.agile42.com/cms/pages/agilo/ 30 2.4 Agiles Projektmanagement Sprintplanung Die im Sprint zu realisierenden Features werden als User Stories formuliert, in Story Points bewertet und im sogenannten Sprint Backlog festgehalten. Ein Story Point entspricht etwa einem halben Entwicklungstag. Jede User Story wird in einzelne Tasks aufgegliedert, die dann jeweils in Stunden geschätzt werden. Zeigt sich nach der Aufgliederung, daß einzelne User Stories über- oder unterschätzt wurden, werden die Story Points entsprechend angepaßt. Die Planung ist beendet, wenn User Stories und die ihnen zugeordneten Tasks mit einem Gesamtaufwand von etwa zehn Story Points bzw. 25 bis 30 Stunden im Sprint Backlog liegen. Der Planungsprozeß dauert etwa einen halben Tag. Abbildung 2.4 zeigt das Sprint Backlog des neunten Sprints. Es sind drei User Stories mit den jeweiligen untergeordneten Tasks zu sehen. Das Backlog zeigt den Zustand am Ende des Sprints, daher werden die User Stories durchgestrichen angezeigt und so als fertiggestellt markiert. Die verbleibende Zeit wird jeweils mit null Stunden angegeben. Abbildung 2.4: Sprint Backlog in Agilo for Scrum Sprintdurchführung Während der Woche wird der Entwicklungsfortschritt stetig im Sprint Backlog festgehalten. Hierzu werden die Schätzungen des zur Fertigstellung der Tasks verbliebenen Aufwands regelmäßig nach oben oder unter korrigiert. Im Burndown Chart werden die Veränderungen der Schätzungen im Verhältnis zum Zeitverlauf visualisiert. Eine Trendlinie gibt über den vermutlichen Erfolg bzw. Mißerfolg des Sprints Auskunft. In Abbildung 2.5 ist das Burndown Chart nach Ende des neunten Sprints abgebildet. 31 2 Hintergrund Abbildung 2.5: Sprint Burndown in Agilo for Scrum Integration der Versionsverwaltung Weiterhin hilfreich ist die Integration der Versionsverwaltung Subversion 21 in Agilo. In den Commits kann Bezug auf die Tasks des Sprints genommen werden, so daß Sprints und zugehörige Versionen der entwickelten Software in der Weboberfläche miteinander verknüpft sind. In der Timeline-Ansicht (Abbildung 2.6) werden Ereignisse im Repository und Veränderungen in der Projektplanung im zeitlichen Bezug zueinander angezeigt. Abbildung 2.6: Timeline in Agilo for Scrum 21 Siehe http://subversion.tigris.org/ 32 2.4 Agiles Projektmanagement Im Beispiel wurde Task 79 am 30. Mai 2009 um 10:58 Uhr durch einen Commit ins Subversion-Repository abgeschlossen. Die Quellcodeveränderungen im entsprechenden Changeset 41 können direkt über die Weboberfläche von Agilo for Scrum eingesehen werden. Durch die beschriebenen agilen Praktiken kann der Compiler flexibel und dennoch planvoll entwickelt werden. Die Meilensteine bieten eine Grobplanung für das Gesamtprojekt, während jede Woche die Erfahrungen der Vorwoche in die aktuelle Sprintplanung einfließen können. Ein Projektmanagement auf der Grundlage der Netzplantechnik oder von Gantt-Charts ist hier aufgrund der geringen Erfahrung mit der Softwareentwicklung im allgemeinen wie auch mit der Compiler-Entwicklung im speziellen unrealistisch. 33 3 Der Compiler Nachdem im vorherigen Kapitel die Quell- und Zielsprache des Compilers sowie die zur Entwicklung eingesetzten Hilfsmittel vorgestellt wurden, folgt nun eine Beschreibung der Architektur und Implementierung des Compilers. Zunächst werden der Aufbau und die Aufgaben der einzelnen Phasen erläutert, dann wird jede Phase in einem eigenen Abschnitt detailiert betrachtet. 3.1 Aufbau und Phasen Der MiniJavaOO-Compiler ist ein Mehrphasen-Compiler, aufgeteilt in ein Frontend und ein Backend. Das Frontend dient der Analyse des Quellprogramms und durchläuft drei Phasen (Abbildung 3.1). ! "! #$ Abbildung 3.1: Analyse-Phasen des Compilers Parser und Lexer prüfen die Syntax des Quellprogramms und transformieren es in abstrakte Syntax. Diese gibt die logische Struktur des Programms in verdichteter Form 34 3.1 Aufbau und Phasen in einer Baumstruktur, dem AST, wieder. Der AST ist mit den Token aus der Lexerphase annotiert und kann von den nachgelagerten Compiler-Phasen effizient traversiert werden. (Abschnitt 3.2) Aufbau der Symboltabellen und der Klassentabelle Diese Phase wird mittels zweier AST-Parser realisiert. In einem ersten Durchlauf wird eine Klassentabelle aufgebaut, und die Knoten der Klassendefinitionen werden mit den Klassensymbolen annotiert. Im zweiten Durchlauf entstehen die Symboltabellen für Klassen, Methoden und Blöcke, die entsprechend der Struktur des Programms miteinander verknüpft sind. Auch in dieser Phase werden die Knoten der Defintionen mit den jeweiligen Symbolen annotiert. (Abschnitt 3.3) Semantische Analyse Die semantische Analyse überprüft die Bedingungen der statischen Semantik (siehe Anhang A). Es werden im Programm vorkommende einfache und qualifizierte Namen für Klassen, Methoden und Variablen mit Hilfe der Symboltabellen aufgelöst und die entsprechenden Knoten mit den Symbolen annotiert. Weiterhin finden Typ-, Scope- und Zugriffsprüfungen statt. Auch diese Phase wird mit einem AST-Parser realisiert. (Abschnitt 3.4) Der annotierte AST dient als Eingabe für das Backend des Compilers. Das Backend erzeugt den JVM-Bytecode und durchläuft drei Phasen (Abbildung 3.2). Abbildung 3.2: Synthese-Phasen des Compilers Code-Emitter Ein weiterer AST-Parser erzeugt aus den Informationen des annotierten AST und mit Hilfe der StringTemplate-Engine Quellcode für den Jasmin-Assembler. (Abschnitt 3.5) 35 3 Der Compiler Jasmin-Assembler Daraufhin wird dieser in Bytecode assembliert und in je einer .classDatei pro Klasse gespeichert. (Abschnitt 3.6) Bytecode-Verifier Der generierte Bytecode wird schließlich einer Analyse unterzogen, um sicherzustellen, daß alle strukturellen und semantischen Bedingungen der JVM erfüllt sind. (Abschnitt 3.6) Neben dem compilierten Bytecode speichert der Compiler auch den Assemblercode in .jasmin-Dateien ab und gibt eine Visualisierung des AST im Graphviz -Format1 in der Datei AST.dot aus. In Abbildung 3.3 wird die zentrale Klasse CompilerPhases zur Steuerung des Compilers dargestellt. Sie dient als Schnittstelle sowohl für das Kommandozeilen-Interface in der Klasse Main als auch für die Klassen zur Durchführung automatisierter Tests während der Entwicklung des Compilers (Details hierzu in Kapitel 4). CompilerPhases +lexerPrepare() +parserPhase() +symbolTablePhase() +semanticCheckPhase() +codeEmitterPhase() +jasminAssemblerPhase() +verifierPhase() +readSource() +readSource(String:file) +saveJasminCode(outdir:String) +saveByteCode(outdir:String) +saveAstDotFile(outdir:String) +getAst(): AstNode +getSymbolTable(): ClassTable +getJasminCode(): HashTable<String, String> +setLvarAutoInit(autoInit:boolean) +setFunProcMode(funProcMode:boolean) +setDebugMode(debugMode:boolean) Main +main(args:String[]) CompilerTestBase CompilerTestTool CompilerTestTool2 Abbildung 3.3: Klassen zur Steuerung des Compilers Zur Steuerung des Compilers stellt CompilerPhases folgende Funktionalität zur Verfügung: • Einlesen des Quellprogramms und Abspeichern der Ausgaben des Compilers • Aufruf der einzelnen Compiler-Phasen • Zugriff auf die Zwischenergebnisse: AST, Symboltabellen und Jasmin-Assemblercode • Einschalten des Debug-Modus, der erweiterte Fehlermeldungen ermöglicht. • Umschalten zwischen MiniJavaFunProc- und MiniJavaOO-Modus • Ein- und Ausschalten der automatischen Initialisierung lokaler Variablen 1 Siehe http://www.graphviz.org/ 36 3.2 Parser und abstrakte Syntax 3.2 Parser und abstrakte Syntax Parser und Lexer entstehen aus der kombinierten Grammatik MiniJavaOO.g. Wie Abbildung 3.4 zeigt, werden die beiden Klassen MiniJavaOOLexer und MiniJavaOOParser sowie die Datei MiniJavaOO.token von ANTLR erzeugt. Letztere enthält die Definition der Lexemklassen und wird neben dem Lexer und dem Parser auch von allen nachgelagerten AST-Parsern genutzt. org.antlr.runtime.Parser org.antlr.runtime.Lexer ParserBase MiniJavaOOLexer MiniJavaOOParser Erzeugt Erzeugt Erzeugt MiniJavaOO.g MiniJavaOO.token Abbildung 3.4: Parser- und Lexer-Klassen Zusätzlich zu den Produktionen in EBNF-Notation enthalten ANTLR-Grammatiken auch eingebetteten Javacode. Dieser sollte aus Gründen der Übersicht2 kurz gehalten sein. Hierzu sind, soweit möglich, Regelaktionen und anderer Code in die Methoden einer Superklasse ausgelagert. Bei einer kombinierten Grammatik ist dies derzeit nur für den Parser möglich: die Klasse MiniJavaOOParser beerbt die Klasse ParserBase. Die EBNF-Produktionen des Lexers werden in Anhang B.1, die des Parsers in Anhang B.2 angegeben. In den folgenden Abschnitten wird näher auf einige Aspekte des MiniJavaOOParsers eingegangen und die Transformation von MiniJavaOO-Quellcode in einen AST erläutert. 3.2.1 Spracherweiterung über semantische Prädikate Die im Vergleich zu MiniJavaFunProc neuen Sprachfeatures von MiniJavaOO können im Parser über Gated Semantic Predicates (vgl. [8], S. 317 ff.) ein- und ausgeschaltet werden. Beim Aufruf des Compilers kann so über eine Option MiniJavaFunProc oder MiniJavaOO als Quellsprache ausgewählt werden. Die Erweiterungen sind in drei Gruppen gegliedert, die jeweils durch ein Prädikat repräsentiert werden: • declEverywhere erlaubt es, lokale Variablen beliebig innerhalb von Blöcken zu deklarieren, 2 Zudem bietet Eclipse innerhalb einer ANTLR-Grammatik nicht die übliche Hilfestellung bei der JavaProgrammierung. 37 3 Der Compiler • enableOO schaltet die objektorientierten Features ein und • enableExtensions steuert Erweiterungen wie die neuen Operatoren oder die Möglichkeit zur Definition einer main-Methode. Die EBNF-Regel in Syntax 3.1 zeigt den Einsatz zweier semantischer Prädikate. Eine Funktion beginnt in MiniJavaFunProc mit dem Schlüsselwort func. Ist das semantische Prädikat enableExtensions eingeschaltet, ist func optional, und es muß der Typ der Funktion angegeben werden. Dieser ist entweder int oder, falls auch das semantische Prädikat enableOO eingeschaltet ist, ein Klassenname. Beispiel 3.1 (Parser-Regel mit semantischen Prädikaten) funcStart → FUNC | {enableExtensions}?=> FUNC? ( INT | {enableOO}?=> CLASSIDENT ) In Abbildung 3.5 wird der von ANTLR zur Entscheidung der Regelalternativen erzeugte DEA dargestellt. FUNC CLASSIDENT&&{enableExtensions}? s5=>2 IDENT s4=>1 s1 INT&&{enableExtensions}? INT&&{enableExtensions}? s0 s2=>2 CLASSIDENT&&{(enableExtensions&&enableOO)}? s3=>2 Abbildung 3.5: DEA zur Regelentscheidung mit semantischen Prädikaten 3.2.2 Grenzen des LL(∗)-Parsergenerators Wie in Abschnitt 2.3.2 beschrieben, können ANTLR-Grammatiken aufgrund des LL(∗)Parsergenerators sehr natürlich formuliert werden. Hierbei kann jedoch das Problem auftreten, daß der generierte Code zu groß wird. Die DEAs zur Regelentscheidung können so komplex werden, daß einzelne Java-Methoden die Größenbegrenzung von 64 kB überschreiten, insbesondere wenn eine Grammatik mit Debugging-Support für ANTLRWorks generiert wird. Während der Entwicklung des MiniJavaOO-Parsers wurde diese Grenze mehrfach erreicht, so daß die Grammatik überarbeitet werden mußte. Das folgende Beispiel verdeutlicht die Problematik: 38 3.2 Parser und abstrakte Syntax Im ersten Ansatz wurde für jede Regel der MiniJavaFunProc-Grammatik aus dem Compilerbau-Praktikum eine gleichlautende, aber mit dem Suffix OO“ versehene zweite Regel mit ” den spezifischen Erweiterungen geschrieben und von der ursprünglichen auf diese verwiesen. Da Ausdrücke Teil vieler Regeln sind, haben sich besonders die Regeln für atomare Ausdrücke (siehe Syntax 3.1) auf die Komplexität der DEAs ausgewirkt. Ein auf der Grundlage dieser Version der Grammatik erzeugter Parser mit Debugging-Support beinhaltet Methoden, die die Größenbegrenzung von 64 kB nicht einhalten. Syntax 3.1 (Parser-Regel für atomare Ausdrücke: Erster Ansatz) AtomExpr → | | | | | | IDENT NUMBER RLB expr RRB FunProcCall {enableExtensions}?=> IDENT ( INC | DEC ) {enableExtensions}?=> ( INC | DEC ) IDENT {enableOO}?=> AtomExprOO AtomExprOO → | | | MemberIdent MethodCall {enableExtensions}?=> MemberIdent ( INC | DEC ) {enableExtensions}?=> ( INC | DEC ) MemberIdent In der überarbeiteten Version (siehe Syntax 3.2) sind die beiden Regeln zusammengeführt. Außerdem faßt die Regel Identifier jetzt den einfachen Namen (vorher IDENT) mit dem qualifizierten Namen (vorher MemberIdent) zusammen, und in der ersten Alternative sind alle Möglichkeiten vereint, die mit einem Namen beginnen. Syntax 3.2 (Parser-Regel für atomare Ausdrücke: Optimierte Fassung) AtomExpr → | | | Identifier ( FunProcCall | {enableExtensions}?=> PostfixIncDec {enableExtensions}?=> PraefixIncDec Identifier NUMBER RLB Expr RRB )? Zum Vergleich der generierten DEAs: • Der ursprüngliche DEA für AtomExpr ist zyklisch und hat 27 Zustände. • Der ursprüngliche DEA für AtomExprOO ist zyklisch und hat 21 Zustände. • Der DEA der optimierten Version ist azyklisch und hat sechs Zustände. 39 3 Der Compiler Da die Regel für Statement ähnlich komplex ist und fast jede andere Regel entweder Statements oder Ausdrücke beinhaltet, ist fast jeder DEA der ersten Version der Grammatik azyklisch und hat mehr als 20 Zustände. Nachdem alle Regelpaare, wie im Beispiel gezeigt, zusammengelegt und weitere Optimierungen in den Regeln Statement und Identifier durchgeführt wurden, gibt es in der gesamten Grammatik keinen zyklischen DEA mehr, und nur drei DEAs haben mehr als zehn Zustände. Man sollte also trotz der Möglichkeiten von ANTLR stets um einen kleinen Look-Ahead bemüht sein. Die Mächtigkeit von LL(∗) kann dann ausgeschöpft werden, wenn ein Sprachelement nicht oder nur schwer anders auszudrücken ist. Für MiniJavaOO ist dies an keiner Stelle nötig, da die Sprache so definiert ist, daß sie auch mit einem herkömmlichen LL(k)Parser umgesetzt werden kann. 3.2.3 Abstrakter Syntaxbaum Die abstrakte Syntax des MiniJavaOO-Compilers orientiert sich an der Struktur von Java und der JVM. Wie in Anhang B.3 dargestellt, ist sie wesentlich kompakter als die Parsersyntax (Anhang B.2). Dies ist möglich, da überflüssige syntaktische Elemente wegfallen und viele syntaktische Bedingungen, die der Parser bereits geprüft hat, im AST vereinfacht dargestellt werden können. Beispielsweise wird im Parser zwischen einem Funktionsblock, der als letzte Anweisung ein return enthalten muß, und einem Prozedurblock unterschieden. Im AST sind beide einfach Blöcke, und return ist ein Teil der Statements. Der Block der Funktion enthält, sichergestellt durch den Parser, die return-Anweisung. Im wesentlichen finden im Parser folgende Transformationen statt: • Prozeduren und Funktionen werden als Methodenknoten dargestellt. • Variablen und Konstanten werden als Variablenknoten dargestellt. • Die spezielle Klasse FunProcMainClass dient als Container für die globalen Sprachelemente, da diese in der JVM nicht direkt abgebildet werden können. – Globale Funktionen und Prozeduren werden zu Instanzmethoden von FunProcMainClass. – Globale Variablen und Konstanten werden zu Instanzvariablen von FunProcMainClass. Der Name der Klasse kann durch einen Schalter beim Aufruf des Compilers geändert werden. Die semantische Analyse stellt später sicher, daß die Klasse von den anderen Klassen nicht aggregiert, instanziert oder erweitert wird. 40 3.2 Parser und abstrakte Syntax • Ein Großteil der Token des Quellprogramms wird durch die Baumstruktur überflüssig und daher nicht in den AST übernommen. Dies betrifft neben Klammerungen auch Schlüsselwörter wie else. Im Ergebnis ist die abstrakte Syntax für den Programmierer schwerer zu lesen als MiniJavaOO-Quellcode, kann aber um so besser maschinell verarbeitet werden. Sie ist nicht nur kompakter, sondern erfordert auch einfachere Look-Ahead-DEAs zur Entscheidung von Alternativen. Darstellung in ANTLR Ein AST wird in ANTLR durch Knoten-Objekte der Klasse CommonTree dargestellt (Abbildung 3.6). Ein Knoten besteht aus einem Token des Quellprogramms und einem Verweis auf einen Elternknoten und die Kindknoten. Um den AST mit weiteren Informationen anzureichern, wird eine Klasse AstNode abgeleitet, die die Annotation von Symbolen ermöglicht. Eine entsprechende Klasse AstNodeAdaptor wird innerhalb der Parser als Factory-Klasse und zur Steuerung der Traversierung genutzt. (Vgl. [8], S. 155 ff.) org.antlr.runtime.tree.CommonTree org.antlr.tree.CommonTreeAdaptor +getParent(): CommonTree +getChild(i:int): CommonTree +getToken(): Token AstNode AstNodeAdaptor +setSymbol(s:Symbol) +getSymbol(): Symbol Abbildung 3.6: Klasse AstNode zur Symbol-Annotation In einer ANTLR-Grammtik wird ein AST durch die Syntax 3.3 notiert, die sowohl zur Konstruktion (siehe [8], Kapitel 7) als auch zur Traversierung mittels einer AST-Grammatik (vgl. [8], Kapitel 8) genutzt wird. Die verschachtelte Struktur wird durch die Klammerung ausgedrückt, ^ markiert den Start eines neuen Teilbaums, wobei das erste Token nach der öffnenden Klammer der Elternknoten ist. Syntax 3.3 (Darstellung abstrakter Syntaxbäume in ANTLR-Grammatiken) ast → ^( TOKEN ast∗ ) | TOKEN | nil Es folgen drei Beispiele für die Übersetzung von MiniJavaOO in abstrakte Syntax. Anhang C zeigt außerdem die ASTs zu den Beispiel-Programmen der Kapitel 1 und 2. 41 3 Der Compiler Beispiel: Globale Sprachelemente Der Ausschnitt aus dem Binärbaum-Programm in Listing 3.1 zeigt die Definitionen der globalen Prozedur printBaum und der main-Methode. Listing 3.1: (Binärbaum-Programm: Globale Prozedur und main-Methode) 27 28 29 30 31 32 void printBaum(Baum b) { // gibt den gesamten Baum in Inorder aus b.inorder(); // gibt nur die Knoten 4 und 3 aus b.l.l.l.inorder(); } 33 34 35 36 37 38 // Hauptprogramm main { baum = erzeugeBaum(); printBaum(baum); } Bei der Transformation in abstrakte Syntax (Abbildung 3.7) werden sowohl main als auch printBaum zu Methoden der Klasse FunProcMainClass. Der Code ist in diesem speziellen Fall umfangreicher und nicht kompakter geworden. Das Beispiel verdeutlicht auch den Aufbau der Klassenknoten in der abstrakten Syntax. Erster Kindknoten ist der Klassenname, daraufhin folgen die Superklasse, der Konstruktor, die Instanzvariablen und schließlich die Methoden. CLASS FunProcMainClass EXTENDS FunProcMainClass Object BLOCK MODIFIER void public FIELDS ... METHOD main PARAMS METHODS METHOD BLOCK MODIFIER ... public void ... printBaum PARAMS BLOCK PARAM ... Baum b Abbildung 3.7: Binärbaum-Programm: Globale Prozedur und main-Methode im AST Beispiel: Variablendeklaration Der Quellcode in Listing 3.2 zeigt einen Ausschnitt aus der ANTLR-Parser-Grammatik. Der Regel zur Variablendeklaration varDecl wird als Parameter ein AST übergeben, der bei einer Instanzvariablen den Zugriffsmodifizierer und ansonsten einen leeren Baum enthält (siehe (AstNode)adaptor.nil() in Regel localVarDecl). Da in einer Anweisung mehrere Variablen deklariert werden können, wird der Variablentyp, zurückgegeben von 42 3.2 Parser und abstrakte Syntax der Regel varDeclTyp, jedem Aufruf von varAssign mitgegeben. Nach dem Operator -> erfolgt die Produktion des AST. ANTLR erkennt, daß innerhalb der AST-Produktion nur varAssign eine Liste von Bäumen ist, und erzeugt wegen des Operators + einen VAR-Teilbaum je varAssign. Listing 3.2: (Übersetzen von Variablendeklarationen in abstrakte Syntax) 1 2 3 4 varDecl[AstNode modifier] : typ=varDeclTyp varAssign[typ.tree] (COMMA varAssign[typ.tree])* ENDSTMNT -> ^(VAR ^(MODIFIER {$modifier}) varAssign)+ ; 5 6 7 8 9 varDeclTyp : INT -> INT |{enableOO}?=> CLASSIDENT -> SIMPLENAME[$CLASSIDENT] ; 10 11 12 13 14 varAssign[AstNode typ] : IDENT (ASSIGN (identifier | NUMBER | {enableOO}?=>constructorCall))? -> {$typ} SIMPLENAME[$IDENT] identifier? NUMBER? constructorCall? ; 15 16 17 18 19 localVarDecl : varDecl[(AstNode)adaptor.nil()] -> varDecl ; Die Parser-Grammatik verdeutlicht zwei Besonderheiten der AST-Konstruktion mit ANTLR: • In varDecl werden die imaginären Token VAR und MODIFIER erzeugt. Sie sind nicht Ergebnis des Lexers-Laufs und haben somit keine direkte Entsprechung im Quellprogramm. Sie dienen der besseren Strukturierung des AST. • In den Regeln varDeclTyp und varAssign findet jeweils eine Token-Transformation statt. Sowohl CLASSIDENT als auch IDENT werden zu einem imaginären Token SIMPLENAME transformiert, in dem dieses mit den Werten des realen Token initialisiert wird. Dies vereinfacht die nachfolgenden AST-Parser und bedeutet außerdem eine Anpassung an die Begriffswelt der JVM. Es folgt ein Übersetzungsbeispiel. In Listing 3.3 werden in der Klasse Baum drei Instanzvariablen deklariert. Listing 3.3: (Binärbaum-Programm: Deklaration von Instanzvariablen) 40 41 class Baum extends Object { // rechter und linker Teilbaum 43 3 Der Compiler public Baum r, l; // Wert des Knotens protected int v; 42 43 44 Abbildung 3.8 zeigt einen Ausschnitt aus dem zugehörigen AST mit je einem Teilbaum pro Variable unterhalb des Knotens FIELDS. Unterhalb des VAR-Knotens folgen die Modifier, der Typ und schließlich der Name der Variablen. Entsprechend der Konstruktion in varDecl verweisen die ersten beiden Deklarationen auf dieselben Blätter mit den Token public und Baum. class Baum extends Baum Object BLOCK MODIFIER FIELDS Baum public VAR r ... VAR MODIFIER VAR l MODIFIER int v protected Abbildung 3.8: Binärbaum-Programm: Deklaration von Instanzvariablen im AST Beispiel: Ausdrücke Das letzte Beispiel in Listing 3.4 zeigt ein weiteres, ausdrucksstarkes Feature von ANTLRGrammatiken. Teil einer üblichen LL(k)-Grammatik für Ausdrücke mit Operator-Vorrang ist ein Summen-Ausdruck, dessen Teilausdrücke Multiplikations-Ausdrücke sind. Listing 3.4: (Übersetzen von Summen-Ausdrücken in abstrakte Syntax) 1 2 3 sumExpr : (multExpr -> multExpr) ((op=ADD|op=SUB) e=multExpr -> ^($op $sumExpr $e))* ; Im einfachsten Fall ist der Summen-Ausdruck ein Multiplikations-Ausdruck, und der erste ->-Operator gibt diesen zurück. Besteht der Summen-Ausdruck jedoch aus mehreren verknüpften Multiplikations-Ausdrücken, so wird die zweite AST-Produktion ^($op $sumExpr $e) für jeden weiteren Teilausdruck einmal ausgeführt. Die Besonderheit ist die Variable $sumExpr, die jeweils den bisher produzierten AST enthält. Hierzu ein Beispiel: Listing 3.5 zeigt den Rumpf der Methode berechneAusdruck aus dem Ackermann-Programm. Er enthält zwei Ausdrücke. Der zweite in Zeile 18 besteht, abgesehen von der Multiplikation am Ende der Rechnung, nur aus Summen. 44 3.2 Parser und abstrakte Syntax Listing 3.5: (Ackermann-Programm: Ausdrücke) 13 { // verschachtelte arithmetische Ausdruecke ergebnis = x * x + ((56 + 9) / 3 - 5); // Bildschirmausgaben print(ergebnis); print(1 + 2 + 3 + 4 + 5 * 6); 14 15 16 17 18 19 } In Abbildung 3.9 wird der zugehörige AST dargestellt. Ganz rechts, unter dem printKnoten, ist der Summen-Ausdruck zu erkennen. Wegen der Struktur der Regel sumExpr wird dieser wie folgt von unten nach oben aufgebaut: • $sumExpr enthält den Teilbaum 1, dieser wird zum linken Ast von 1+2. • $sumExpr enthält den Teilbaum 1+2, dieser wird zum linken Ast von 1+2+3. • $sumExpr enthält den Teilbaum 1+2+3, dieser wird zum linken Ast von 1+2+3+4. BLOCK = print print ergebnis + ergebnis + * - x / x 5 + 3 56 9 + + 1 + * 4 5 6 3 2 Abbildung 3.9: Ackermann-Programm: Ausdrücke im AST Die Übersetzung des mit Klammern verschachtelten Ausdrucks aus Zeile 15 zeigt, daß die Klammerung im AST wegfällt. Sie wird, ebenso wie die Operator-Vorrang-Regeln, durch die Anordnung der Knoten wiedergegeben. 3.2.4 Fehlerbehandlung Die Behandlung von Syntaxfehlern in ANTLR-Parsern basiert auf einer Recovery-Strategie (siehe [8], S. 246 ff.). Der Parser meldet Fehler über eine Ausgabe auf System.err und versucht, durch das Überlesen oder Einfügen von Token wieder in einen korrekten Zustand zu gelangen. Dieses Verhalten ist für viele Anwendungen angemessen, z. B. für den Syntax-Check innerhalb einer Entwicklungsumgebung oder für eine DSL innerhalb eines Softwaresystems. 45 3 Der Compiler Innerhalb eines Compilers sollte der Parser jedoch beim ersten Fehler eine Exception werfen und abbrechen. Wie dies umgesetzt wird, beschreibt [8], S. 241 ff.: In der Klasse ParserBase werden Methoden von org.antlr.runtime.Parser überlagert. In einigen Fehlerkonstellationen führte dies allerdings nicht zum gewünschten Ergebnis, erst eine Recherche auf der Mailingliste von ANTLR zeigte, daß zusätzlich noch die Methode recoverFromMismatchedToken überlagert werden muß. Exception-Hierarchie Obwohl es die Syntax von ANTLR-Grammatiken erlaubt, eigene Exception-Klassen anzugeben, übernimmt ANTLR diese z. Zt. nicht in die throws-Anweisungen der generierten Parser-Methoden. Daher müssen eigene Exception-Klassen von RecognitionException abgeleitet werden. Abbildung 3.10 zeigt die sich so ergebende Exception-Hierarchie des MiniJavaOO-Compilers. org.antlr.runtime.RecognitionException CodePosition CompilerException -desc: String ErrorCode -code: int -desc: String +get(n:int) SymbolTableException SemanticException Abbildung 3.10: Exception-Klassen zur Fehlerbehandlung Die Klasse CompilerException realisiert die spezifische Fehlerbehandlung des Compilers. Während RecognitionException keine Möglichkeit zum Zugriff auf die ausgegebene Fehlermeldung bietet, kann einer CompilerException ein beschreibender Text mitgegeben werden. Das aggregierte CodePosition-Objekt zeigt auf die Fehlerstelle im Quellprogramm, und durch Objekte der Klasse ErrorCode können Exceptions der gleichen Klasse weiter durch einen eindeutigen Fehlercode mit zugeordneter Fehlermeldung differenziert werden. Jeder spezifische Fehler kann so durch seinen Code in den an späterer Stelle gezeigten (Kapitel 4) automatisierten Tests identifiziert werden. Spezialfall Lexer Auch der Lexer hat eine automatische Recovery-Funktion. Diese gibt jedoch keine Fehlermeldungen aus und kann auch nicht durch Methodenüberlagerung ausgeschaltet werden. Hier sorgt ebenfalls eine Suche auf der Mailingliste für Abhilfe durch die in Listing 3.6 gezeigte Regel für die Lexemklasse ANY. Durch die Platzierung als letzte Regel innerhalb des Lexers in Kombination mit dem Greedy-Wildcard-Operator ’.’ werden alle lexikalischen Fehler in einem ANY-Token aufgefangen, das an den Parser weitergereicht wird. Der Parser 46 3.3 Symboltabellen und Typsystem beendet die Verarbeitung, sobald er auf das ANY-Token stößt, da es in keiner Parser-Regel vorkommt. Die Exception des Lexers wird in this.e gespeichert und führt zur Ausgabe der Fehlermeldung syntax error: invalid character, der Meldung, die in Error-Code 101 hinterlegt ist. Listing 3.6: (Lexer-Regel für das ANY-Token zur Fehlerbehandlung) 1 2 3 ANY : . { CodePosition pos = new CodePosition($line, $pos); if($line >= 0) { this.e = new CompilerException(ErrorCode.get(101), "lexer", pos); System.err.println(e.getMessage()); } 4 5 6 7 8 } 9 10 ; 3.3 Symboltabellen und Typsystem Neben dem AST werden für die semantische Analyse Symboltabellen benötigt, in denen alle im Quellprogramm vorkommenden Klassen-, Methoden- und Variablendefinitionen verzeichnet sind. Die Symboltabellen sind entsprechend der Struktur des Quellprogramms in einem Baum angeordnet, dessen Wurzel die Klassentabelle ist. Die Klassentabelle ist das Verzeichnis der im Programm gültigen Typen. 3.3.1 Design Das UML-Diagramm in Abbildung 3.11 zeigt die implementierte Klassen-Hierarchie für die Symboltabellen. Jede Definition wird durch ein Objekt einer Unterklasse von Symbol dargestellt. Ein Symbol zeichnet sich durch seinen Namen und die Position der Definition im Quelltext aus. Eine Symboltabelle ist ein Symbol, welches das Interface SymbolTable implementiert und weitere Symbole in einer Hash-Tabelle aggregiert: • Ein Klassensymbol aggregiert Symbole für jede seiner Instanzvariablen und Methoden. • Ein Methodensymbol aggregiert Symbole für die Parametervariablen sowie ein Blocksymbol für den Rumpf. Da Parametervariablen semantisch den lokalen Variablen entsprechen, werden sie durch die Klasse LocalVariableSymbol abgebildet. 47 3 Der Compiler • Blocksymbole aggregieren Symbole für lokale Variablen und Blöcke. Sie haben einen künstlichen Namen und sind für Scope-Prüfungen von lokalen Variablen von Bedeutung. <<interface>> <<abstract>> SymbolTable Symbol <<abstract>> SymbolWithTable <<abstract>> VariableSymbol ClassTable ClassSymbol FieldSymbol MethodSymbol LocalVariableSymbol BlockSymbol Abbildung 3.11: Klassen für Symbole und Symboltabellen Die Klassensymbole werden von einem Objekt der Klasse ClassTable aggregiert und erweitern das in Abbildung 3.12 gezeigte Typsystem. Die Basisklasse Object wird durch ein künstliches Symbol der Singleton-Klasse3 ObjectClassSymbol abgebildet und in die Klassentabelle eingefügt. Die primitiven Typen werden ebenfalls als Singletons realisiert. <<interface>> Type ClassTable ClassSymbol <<abstract>> PrimitiveType ObjectClassSymbol VoidType Abbildung 3.12: Klassen für das Typsystem 3 Siehe http://sourcemaking.com/design patterns/singleton 48 IntType 3.3 Symboltabellen und Typsystem Auf die Vorteile des vorgestellten Designs für Namensauflösungen, Zugriffs- und Typprüfungen wie auch auf einzelne Details der verschiedenen Klassen wird während der Beschreibung der semantischen Analyse in Abschnitt 3.4 eingegangen. 3.3.2 Traversieren des AST Zum Aufbau der Symboltabellen, aber auch in den späteren Compiler-Phasen muß der AST traversiert werden. In der ANTLR-Anwendergemeinde werden hierzu drei Möglichkeiten diskutiert (vgl. [9] und [13]): 1. Tiefensuche mit dem Visitor Pattern 2. Manuell erstellte Tree Walker 3. Von ANTLR generierte Tree Walker Das Visitor Pattern wird als die ungeeignetste Lösung angesehen, da es die Knoten isoliert behandelt. Nicht betrachtet werden die Struktur des Baums und der Kontext eines Knotens. Der Austausch von Informationen zwischen den Knoten ist umständlich. Die Diskussion für oder wider einen automatisch generierten Tree Walker ähnelt in vielerlei Hinsicht der Diskussion um automatisch generierte Parser, und tatsächlich wird in [9] dargelegt, weshalb das Traversieren eines AST im Grunde ein Parserproblem ist. Eine entgegengesetzte Argumentation findet sich in [13]. Zur Entwicklung des MiniJavaOO-Compilers werden automatisch generierte Tree Walker eingesetzt. Hierzu wird zunächst eine EBNF-Grammatik entwickelt, die die Struktur des AST zum Ausdruck bringt. In [8], Kapitel 8 werden die Syntax und die Möglichkeiten einer entsprechenden Tree-Grammatik beschrieben. Die Grammatik wird für jede einzelne Compiler-Phase kopiert und mit Actioncode versehen. Durch die Art der Positionierung des Actioncodes innerhalb der Regeln kann nicht nur für jeden Knotentyp, sondern auch abhängig vom Kontext des Knotens entschieden werden, ob die Traversierung in Preorder, Postorder oder Inorder erfolgt. 3.3.3 Aufbau der Symboltabellen In Abbildung 3.13 sind die beiden Parser zum Aufbau der Tabellen abgebildet. Wie alle AST-Parser des MiniJavaOO-Compilers beerben sie die Klasse AstWalkerBase, die Zugriff auf die Klassentabelle und die Symboltabellen sowie den Namen der speziellen Klasse FunProcMainClass gewährt sowie Methoden zur Fehlerbehandlung implementiert. Klassen Bevor die Symboltabellen aufgebaut werden können, müssen alle Klassennamen bekannt sein, da die Klassensymbole innerhalb der Symboltabellen als Typangabe von Variablen und 49 3 Der Compiler org.antlr.runtime.tree.TreeParser AstWalkerBase SymbolTableConstructionWalkerBase SymbolTableConstructionWalker Erzeugt SymbolTableConstructionWalker.g ClassTableConstructionWalker Erzeugt ClassTableConstructionWalker.g Abbildung 3.13: Klassen der AST-Parser zum Aufbau der Symboltabellen Methoden gespeichert werden. Hierzu ruft der Parser aus SymbolTableConstructionWalker zunächst den Parser aus ClassTableConstructionWalker auf, bevor er selbst den AST traversiert. In der Klasse SymbolTableConstructionWalkerBase werden hierzu und für die Symbol-Annotation Methoden definiert. Listing 3.7 zeigt die Regel zum Aufbau der Klassentabelle. Hinter dem Token SIMPLENAME ist ein Block Javacode eingefügt, der eine Instanz von ClassSymbol erzeugt (Zeile 5) und der versucht, diese in die Klassentabelle einzufügen. Wird eine SymbolTableException geworfen, so wird diese, angereichert mit Informationen des Parsers, als Exception vom Typ SemanticException an den Compiler weitergereicht (Zeile 11). Die Syntax zur Analyse des AST entspricht der in Abschnitt 3.2.3 vorgestellten Syntax zur Synthese des AST. Listing 3.7: (AST-Parser-Regel zum Aufbau der Klassentabelle) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 classDef : ^( CLASS SIMPLENAME { CodePosition pos = new CodePosition($SIMPLENAME.line, $SIMPLENAME.pos); ClassSymbol s = new ClassSymbol($SIMPLENAME.text, pos); try { this.getClassTable().addClass(s); } catch(SymbolTableException e) { throw new SemanticException(e.getErrorCode(), $SIMPLENAME.text, pos); } } ^(EXTENDS SIMPLENAME) constructor 50 3.3 Symboltabellen und Typsystem ^(FIELDS varDecl*) ^(METHODS method*) 16 17 ) 18 19 ; Methoden, Blöcke und Variablen Im Parser für den Aufbau der Symboltabellen wird für jede Variable, jeden Parameter, jeden Block und jede Methode ein Symbol erzeugt. Der entsprechende Knoten im AST wird mit dem Symbol annotiert und das Symbol in die aktuelle Symboltabelle eingefügt. Listing 3.8 verdeutlicht die baumartige Verknüpfung der Symboltabellen. Vor dem Betreten eines Blocks ist this.currentSymbolTable eine Referenz entweder auf ein Methodensymbol oder ein Blocksymbol. Nachdem das Symbol für den gerade betretenen Block der aktuellen Symboltabelle hinzugefügt wurde (Zeile 7), wird es selbst zur aktuellen Symboltabelle für die folgenden Blockelemente (Zeile 8). Beim Verlassen des Blocks wiederum wird dann die ursprüngliche Symboltabelle zur aktuellen (Zeile 18), deren Referenz dem Konstruktor für das neue Blocksymbol in Zeile 6 übergeben wurde. In den Regeln für Methoden und Klassen wird analog vorgegangen. Listing 3.8: (AST-Parser-Regel zum Aufbau der Symboltabelle: Block) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 block : ^( BLOCK { CodePosition pos = new CodePosition($BLOCK.line, $BLOCK.pos); try { BlockSymbol s = new BlockSymbol(pos, this.currentSymbolTable); this.currentSymbolTable.addSymbol(s); this.currentSymbolTable = s; $BLOCK.setSymbol(s); } catch(SymbolTableException e) { throw new SemanticException(e.getErrorCode(), "BLOCK", pos); } } blockElement* ) { this.currentSymbolTable = this.currentSymbolTable.getParentTable(); } ; 51 3 Der Compiler In Listing 3.9 wird die Regel für die Definition einer Instanzvariablen gezeigt, die sich Informationen aus dem Kontext des Knotens zunutze macht. Die Regel declTyp (Zeile 2) liefert ein Type-Object an fieldDecl zurück, welches in Zeile 5 dem Konstruktor als Argument übergeben werden kann. In Zeile 10 wird abgefragt, ob die optionale Regel varDeclValue (Zeile 2) ausgeführt wurde. Listing 3.9: (AST-Parser-Regel zum Aufbau der Symboltabelle: Instanzvariable) 1 2 3 4 5 6 fieldDecl : ^(VAR ^(MODIFIER FINAL? accessModifier) declTyp SIMPLENAME varDeclValue?) { CodePosition pos = new CodePosition($SIMPLENAME.line, $SIMPLENAME.pos); FieldSymbol s = new FieldSymbol($SIMPLENAME.text, $declTyp.type, $classDef::symbol, pos); this.addSymbol($SIMPLENAME, s); 7 $SIMPLENAME.setSymbol(s); s.setAccess($accessModifier.access); if($varDeclValue.text != null) { s.setValue($varDeclValue.text); } if($FINAL != null) { s.setFinal(); if($varDeclValue.text != null) { s.setInitialized(); } } 8 9 10 11 12 13 14 15 16 17 18 } 19 ; 20 Die weiteren Regeln zum Aufbau der Symboltabellen folgen dem gleichen Prinzip. Beispiel: Binärbaum-Programm Abbildung 3.14 stellt die Klassentabelle und die Symboltabellen des Binärbaums aus Listing 1.1 dar. Die Symboltabellen werden durch weiße Rechtecke, die Symbole durch graue Rechtecke repräsentiert. Symbole, die ihrerseits Symboltabellen enthalten, sind mit diesen über die durchgezogenen Linien verbunden. Die gestrichelten Linien symbolisieren die Verknüpfung der Klassensymbole innerhalb der Klassentabelle, wodurch die Vererbungshierarchie abgebildet wird. Bei der Suche in den Symboltabellen kann sich entsprechend den Pfeilen bewegt werden. In der Abbildung ist zu erkennen, daß Blocksymbole einen künstlichen Namen erhalten, der 52 3.3 Symboltabellen und Typsystem ** ** + $#* $#* %*%$ "$& %*%$ "$& #!#) #!#) )&( )&( )!"% $)#$ )!"% $)#$ )(%& )(%& )'%( )'%( &"' &"' !"#$ !"#$ !%#$ &("% !%#$ &("% Abbildung 3.14: Binärbaum-Programm: Symboltabellen 53 3 Der Compiler die Zeile und die Spalte ihrer öffnenden geschweiften Klammer beinhaltet. An der Methode setValue (siehe Mitte rechts) in der Klasse Baum kann die Darstellung verschachtelter Blöcke in den Symboltabellen nachvollzogen werden. Durch die Transformation der globalen Sprachelemente in die Klasse FunProcMainClass ist im AST auch ein Konstruktor entstanden, der sich nun als Methodensymbol in den Symboltabellen wiederfindet (siehe oben rechts). Da der zugehörige Block keine Repräsentation im Quellprogramm hat, trägt er den Namen $l0c0 für Zeile 0 bzw. Spalte 0. Ein weiteres Zwischenergebnis des Compilers nach dem Aufbau der Symboltabellen sind die Symbol-Annotationen im AST. Abbildung 3.15 zeigt den AST der Klasse Baum. Knoten im AST, die mit einem Symbol annotiert sind, sind grau hervorgehoben. Neben den Blöcken sind nun alle Knoten und Blätter, die einen Namen deklariert haben, annotiert. Sie dienen als Einstiegspunkte in die Symboltabellen. Der Übersicht halber sind die Teilbäume unter den Blöcken nur angedeutet, sie enthalten keine weiteren Annotationen. class Baum extends Baum Object BLOCK MODIFIER FIELDS Baum VAR r ... VAR VAR MODIFIER public l MODIFIER int v protected ... METHODS METHOD MODIFIER void setValue public METHOD PARAMS BLOCK MODIFIER PARAM ... public int void inorder PARAMS BLOCK ... v Abbildung 3.15: Binärbaum-Programm: Annotation der Deklarationen im AST 3.3.4 Fehlerbehandlung Auch für die von ANTLR generierten AST-Parser müssen entsprechend den Aussagen aus Abschnitt 3.2.4 einige Methoden aus org.antlr.runtime.tree.TreeParser überschrieben werden. Dies erfolgt für alle AST-Parser des Compilers zentral in der Klasse AstWalkerBase. Neben den vom Parser geworfenen Exceptions der Klasse RecognitionExcepion werden vom Actioncode auch eigene Exceptions vom Typ SemanticException geworfen. Wie aus den Listings des vorherigen Abschnitts ersichtlich ist, basieren sie meist 54 3.4 Semantische Analyse auf Exceptions, die von den Symboltabellen geworfen werden. Im AST-Parser werden diese mit Informationen zur Position des Fehlers im Quelltext angereichert. 3.4 Semantische Analyse Nachdem nun die Symbol- und Klassentabellen ausgebaut wurden und der AST über die Symbole mit den Tabellen verknüpft ist, kann die semantische Analyse durchgeführt werden. Hierzu bedient sich der Compiler wieder eines AST-Parsers (Abbildung 3.16). Außer der Prüfung der semantischen Bedingungen finden auch Namensauflösungen und die damit verbundenen Symbol-Annotationen im AST statt. org.antlr.runtime.tree.TreeParser AstWalkerBase SemantikCheckWalkerBase SemanticCheckWalker Erzeugt SemanticCheckWalker.g Abbildung 3.16: Klassen des AST-Parsers für die semantische Analyse Die Methoden zur semantischen Analyse sind in der Klasse SemantikCheckWalkerBase (Abbildung 3.17) implementiert. Neben dem AST sowie den darin annotierten Token und Symbolen haben die Methoden zur semantischen Analyse Zugriff auf die aktuelle Symboltabelle (getSymbolTable) und das aktuelle Klassen-Symbol (getClassSymbol). Wie dies jeweils beim Betreten bzw. Verlassen einer Block-, Methoden- oder Klassenregel realisiert wird, wurde anhand von Listing 3.8 in Abschnitt 3.3.3 erläutert. Die Methoden isLvarAutoInit und setLvarAutoInit steuern das Verhalten der Analyse bezüglich der automatischen Initialisierung von lokalen Variablen. Diese kann beim Aufruf des Compilers ein- oder ausgeschaltet werden. 3.4.1 Semantische Prüfungen Methoden, deren Name mit check beginnt, führen eine semantische Prüfung durch. Sie werden, den semantischen Bedingungen der Sprachspezifikation folgend (Anhang A), an den entsprechenden Stellen im AST-Parser aufgerufen. Der für die Prüfung relevante Teilbaum wird als Parameter übergeben. Bei erfolgreicher Prüfung geben die Methoden kein Ergebnis 55 3 Der Compiler SemantikCheckWalkerBase +getSymbolTable(): SymbolTable +setSymbolTable(st:SymbolTable) +getClassSymbol(): ClassSymbol +setClassSymbol(c:ClassSymbol) +isLvarAutoInit(): boolean +setLvarAutoInit(autoInit:boolean) +checkVariable(name:AstNode) +checkLocalVariable(name:AstNode) +checkInScope(name:AstNode) +checkHidesLocalVariableOrParameter(name:AstNode) +checkNotThis(name:AstNode) +thisNotAllowed(name:AstNode) +checkAssignToFinal(name:AstNode) +checkInitialized(name:AstNode) +setInitialized(name:AstNode) +checkMethod(name:AstNode) +checkNumberOfArguments(name:AstNode,actual:int) +checkTypeOfArgument(name:AstNode,actual:Type, index:int,method:MethodSymbol) +checkOverride(name:AstNode) +checkConstructorName(name:AstNode,class:String) +checkExtendsChain(name:AstNode) +checkType(name:AstNode,actual:Type,expected:Type) +checkintType(name:AstNode) +checkIntType(name:AstNode,actualType:Type) +resolveClassName(name:AstNode): AstNode +resolveSimpleNameInMethodAndClass(name:AstNode): AstNode +resolveNameInMethod(name:AstNode): AstNode +resolveQualifiedName(qname:List<AstNode>, inSpecialClass:boolean): AstNode Abbildung 3.17: Klasse SemantikCheckWalkerBase zurück. Im anderen Falle wird der Compiler zur Ausgabe einer Fehlermeldung mit Angabe der relevanten Stelle im Quellcode veranlaßt. Hierzu werden von den Methoden Exceptions vom Typ SemanticException geworfen. Die Fehlermeldungen orientieren sich an den Formulierungen der Fehlermeldungen von Eclipse. Listing 3.10 zeigt die AST-Parser-Regel zur Prüfung der Zuweisung. Die in Zeile 2 aufgerufene AST-Parser-Regel name führt die Namensauflösung der linken Seite der Zuweisung durch (Details hierzu beschreibt Abschnitt 3.4.2.) und liefert die letzte Komponente des Namens an assignment zurück. Diese wird nun in den Zeilen 4 bis 6 semantischen Prüfungen unterzogen: • Ist der Name das Schlüsselwort this, so bricht der Compiler mit Error-Code 411 und der Meldung this not allowed here ab. • Referenziert der Name keine Variable, so lautet der Fehler field or local variable name expected, repräsentiert durch Error-Code 403. • Fehler 406 mit der Meldung final variable cannot be assigned tritt auf, falls die Variable als final deklariert wurde. In Zeile 7 wird die Variable als initialisiert markiert. Daraufhin erfolgt die Prüfung der Typverträglichkeit der Zuweisung: In den Zeilen 12 bis 18 wird der Typ des R-Value festgestellt 56 3.4 Semantische Analyse und in den Zeilen 19 und 20 mit dem Typ des L-Value verglichen. Tritt eine Typverletzung auf, so wird je nach Art des Fehlers entweder type mismatch: cannot convert to int oder type mismatch: cannot convert to void oder type mismatch: cannot convert type ausgegeben. Listing 3.10: (AST-Parser-Regel zur Prüfung der Zuweisung) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 assignment : ^( ASSIGN name { this.checkNotThis($name.lastName); this.checkVariable($name.lastName); this.checkAssignToFinal($name.lastName); this.setInitialized($name.lastName); } expr? constructorCall? { Type actual= null; if($expr.start != null) { actual = $expr.type; } else if($constructorCall.start != null) { actual = $constructorCall.type; } this.checkType($ASSIGN, actual, ((VariableSymbol)$name.lastName.getSymbol()).getType()); } ) ; Es folgt ein Überblick über die verschiedenen Prüfmethoden. Weitere Beispiele zum Einsatz der Methoden werden im nächsten Abschnitt genannt. Obwohl den Methoden ein AstNode-Object übergeben wird, findet die eigentliche Prüfung in der Regel an annotierten Symbolen statt. Das AstNode-Object enthält allerdings nützliche Informationen zur Ausgabe der Fehlermeldung, wie etwa Zeile und Spalte des betroffenen Token im Quellprogramm. Prüfen von Variablen • checkVariable prüft, ob ein Symbol eine Variable ist. • checkLocalVariable prüft, ob ein Symbol eine lokale Variable ist. • checkInScope prüft, ob eine Variable im Scope ihrer Deklaration ist. • checkNotThis bricht ab, falls das Symbol this ist. 57 3 Der Compiler • checkAssignToFinal verhindert die Zuweisung an als final deklarierte Variablen. • checkInitialized prüft, ob eine Variable bereits initialisiert wurde. • checkHidesLocalVariableOrParameter prüft, ob die Deklaration einer lokalen Variablen eine bereits deklarierte lokale Variable oder einen Methoden-Parameter überdeckt. Prüfen von Methoden • checkMethod prüft, ob ein Symbol eine Methode ist. • checkNumberOfArguments stellt sicher, daß eine Methode mit der richtigen Anzahl Argumente aufgerufen wurde. • checkTypeOfArgument prüft für jedes Argument die Typverträglichkeit mit der Signatur. Prüfungen zur Objektorientierung • checkConstructorName stellt sicher, daß der Name eines Konstruktors dem Namen der Klasse entspricht. • checkExtendsChain überprüft die Zyklenfreiheit des Ableitungsbaums. • checkOverride prüft die semantischen Bedingungen der Methodenüberlagerung. Prüfungen zur Typverträglichkeit • checkType stellt sicher, daß ein Symbol kompatibel zu einem Typ ist. • checkIntType stellt sicher, daß ein Symbol vom Typ Integer ist. 3.4.2 Namensauflösung Die Methoden, deren Namen mit resolve beginnen, dienen der Auflösung von Namen. Sie nehmen einen Teilbaum des AST entgegen, der den aufzulösenden Namen repräsentiert, suchen dessen Bestandteile in den Symboltabellen und geben bei erfolgreicher Prüfung die letzte Teilkomponente des Namens, annotiert mit dem relevanten Symbol, zurück. Tritt eine Verletzung der semantischen Bedingungen auf oder kann ein Name nicht aufgelöst werden, so wird auch hier eine SemanticException geworfen, die den Compiler zum Abbruch und zur Ausgabe einer Fehlermeldung veranlaßt. 58 3.4 Semantische Analyse In Listing 3.11 ist die relevante Parser-Regel abgebildet, die drei Fälle unterscheidet (vgl. Anhang A.4): Der Name ist ein einfacher Name Traversiert der AST-Parser gerade die spezielle Klasse FunProcMainClass, so muß der Name eine globale Methode, eine globale Variable oder eine lokale Variable sein. Hierzu sucht die Methode resolveSimpleNameInMethodAndClass (Zeile 5) das zugehörige Symbol in den Symboltabellen. Die Suche beginnt in der Tabelle des aktuellen Blocks und endet in der Symboltabelle der aktuellen Klasse. In einer anderen Klasse muß der Name eine lokale Variable sein. Dementsprechend wird in der Symboltabelle des aktuellen Blocks und der darüber liegenden Blöcke bis zur Symboltabelle der aktuellen Methode gesucht (Zeile 8). Nach der Auflösung des Namens folgt die Prüfung des Scopes (Zeile 10). Der Name ist this Befindet sich der Parser gerade in der speziellen Klasse FunProcMainClass, also innerhalb globaler Methoden, so ist this nicht erlaubt, und der Compiler bricht ab (Zeile 15). Im anderen Falle wird der Name als lokale Variable der aktuellen Methode aufgelöst (Zeile 18). Jede Instanzmethode enthält ein Variablensymbol für this. Der Name ist ein qualifizierter Name Ein aus mehreren Komponenten bestehender qualifizierter Name wird durch die Methode resolveQualifiedName aufgelöst und geprüft. Neben der Symbol-Annotation findet hier auch die Prüfung des Zugriffsschutzes für Instanzvariablen und Methoden statt. Die Implementierung wird im folgenden erläutert. Listing 3.11: (AST-Parser-Regel zur Namensauflösung) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 name returns [AstNode lastName] : SIMPLENAME { if($classDef::inSpecialMainClass) { $lastName = this.resolveSimpleNameInMethodAndClass($SIMPLENAME); } else { $lastName = this.resolveSimpleNameInMethod($SIMPLENAME); } this.checkInScope($SIMPLENAME); } | THIS { if($classDef::inSpecialMainClass) { this.thisNotAllowed($THIS); } else { 59 3 Der Compiler $lastName = this.resolveSimpleNameInMethod($THIS); 18 } 19 } | ^(QUALIFIEDNAME qualifiedName+=THIS? qualifiedName+=SIMPLENAME+) { $lastName = this.resolveQualifiedName($qualifiedName, $classDef::inSpecialMainClass); } ; 20 21 22 23 24 25 Der Methode resolveQualifiedName (Listing 3.12) wird der qualifizierte Name als Liste von AstNode-Objekten übergeben, über die in einer Schleife iteriert wird (Zeile 8). Jedes AstNode-Objekt repräsentiert einen einfachen Namen. Innerhalb der Schleife werden zwei Fälle unterschieden: 1. Komponente Befindet sich der Parser in der speziellen Klasse FunProcMainClass, so wird der Name in den Symboltabellen der Methode und der Klasse gesucht (Zeile 16). Wird er gefunden, so muß er eine Variable repräsentieren (Zeile 22) und darf nicht this sein (Zeile 20). Methoden sind nur als letzte Komponente zulässig. Befindet sich der Parser in einer anderen Klasse, so wird nur in der Methode gesucht (Zeile 26). Der Name muß eine lokale Variable sein (Zeile 28). In beiden Fällen wird der Scope (Zeile 30) überprüft, und es wird sichergestellt, daß die Variable vom Referenztyp ist (Zeile 31). Der Klassenname der Referenz wird gespeichert, um bei der nächsten Komponente prüfen zu können, ob sie ein Member der Klasse ist. Schließlich wird der AST der Komponente mit dem gefundenen Symbol annotiert (Zeile 32). n. Komponente Es wird in der Symboltabelle der Klasse der vorherigen Komponente nach dem Namen gesucht (Zeile 36). Ist die aktuelle Komponente nicht die letzte Komponente des Namens, so muß sie eine Instanzvariable (Zeile 39) vom Referenztyp (Zeile 40) repräsentieren. Die letzte Komponente darf Instanzvariable oder Instanzmethode beliebigen Typs sein. In jedem Fall muß der Zugriffsschutz geprüft werden (Zeile 42): Hat die Klasse, in der sich der Parser gerade befindet, Zugriff auf die Instanzvariable oder Instanzmethode, die die aktuelle Komponente referenziert? Ist der Zugriff erlaubt, so kann schließlich das gefundene Symbol im AST annotiert werden (Zeile 43). Die letzte Namenskomponente wird als Ergebnis der Methode zurückgegeben (Zeile 49). Die Parser-Regel Name gibt diese an die sie aufrufende Regel zum Zweck der Typprüfung weiter. Wie aus Zeile 2 ersichtlich wird, wirft die Methode eine SemanticException. Diese ist entweder eine weitergeleitete Exceptions der verschiedenen check-Methoden, oder es werden Exceptions der Symboltabellen abgefangen (Zeile 46), die von den search-Methoden geworfen werden. Letztere werden als semantische Exceptions, angereichert um Angaben zur Position des Fehlers im Quellprogramm, an den Compiler weitergereicht (Zeile 47). 60 3.4 Semantische Analyse Listing 3.12: (Methode zum Auflösen qualifizierter Namen) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 public AstNode resolveQualifiedName(List<AstNode> qualifiedName, boolean inSpecialMainClass) throws SemanticException { AstNode currentName = null; ClassSymbol currentClass = null; CodePosition p = null; Symbol s = null; try { for(int i = 0; i < qualifiedName.size(); i++) { currentName = qualifiedName.get(i); p = new CodePosition(currentName.getLine(), currentName.getCharPositionInLine()); // 1. Komponente if(i == 0) { if(inSpecialMainClass) { // Feld oder lokale Variable in FunProc s = this.searchLocalVariableOrClassMember( currentName.getText(), (BlockSymbol) this.symbolTable); if(s.getName() == "this") { this.thisNotAllowed(currentName); } this.checkVariable(currentName); } else { // lokale Variable sonst s = this.searchLocalVariable(currentName.getText(), (BlockSymbol) this.symbolTable); this.checkLocalVariable(currentName); } this.checkInScope(currentName); currentClass = this.getTypeOfReferenceVariable(currentName); currentName.setSymbol(s); } // n. Komponente else { s = this.searchClassMember(currentName.getText(), currentClass); // Nicht letzte Komponente muss Referenztyp sein if(i < qualifiedName.size()-1) { this.checkField(currentName); currentClass = this.getTypeOfReferenceVariable(currentName); } checkAccess(s, p); currentName.setSymbol(s); } 61 3 Der Compiler } } catch (SymbolTableException e) { throw new SemanticException(e.getErrorCode(), currentName.getText(), p); } return currentName; 45 46 47 48 49 50 } Beispiel: Binärbaum-Programm Das Ergebnis der Namensauflösung in den Methodenrümpfen der Klasse Baum wird in Abbildung 3.18 dargestellt. Waren nach dem Aufbau der Symboltabellen lediglich die Knoten der Deklarationen und Blöcke annotiert, sind es nun alle im Programm vorkommenden Namen. Durch diese vollständige Annotation des AST ist in den nachfolgenden CompilerPhasen keine erneute Suche in den Symboltabellen nötig. METHODS METHOD MODIFIER void setValue public int METHOD PARAMS BLOCK MODIFIER PARAM if public v <= v BLOCK 10 v this l inorder PARAMS BLOCK INVOKE print INVOKE ARGLIST QUALIFIEDNAME QUALIFIEDNAME this v this r ARGLIST inorder BLOCK 0 = QUALIFIEDNAME this inorder QUALIFIEDNAME if >= void v v Abbildung 3.18: Binärbaum-Programm: Annotationen im AST nach der Namensauflösung Suche in den Symboltabellen Die in resolveQualifiedName benutzten search-Methoden machen sich die polymorphen Eigenschaften der Symbol-Klassen (vgl. Abschnitt 3.3.3, Abbildung 3.11) und die Verknüpfung der Tabellen miteinander zunutze. Abbildung 3.19 verdeutlicht die Hierarchie der Symboltabellen sowie die Einstiegs- und Ausstiegspunkte der Methoden aus Listing 3.13. Es wird jeweils die Methode getSymbolRecursive aufgerufen, der als Parameter ein 62 3.4 Semantische Analyse class-Objekt übergeben wird, welches definiert, wann der rekursive Aufstieg in den Symboltabellen stoppen soll. Abbildung 3.19: Symbolsuche Listing 3.13: (Methoden zur Suche in den Symboltabellen) 1 2 3 4 private Symbol searchLocalVariable(String name, SymbolTable st) throws SymbolTableException { return st.getSymbolRecursive(name, MethodSymbol.class); } 5 6 7 8 9 private Symbol searchLocalVariableOrClassMember(String name, SymbolTable st) throws SymbolTableException { return st.getSymbolRecursive(name, ObjectClassSymbol.class); } 10 11 12 13 14 private Symbol searchClassMember(String name, ClassSymbol st) throws SymbolTableException { return st.getSymbolRecursive(name, ObjectClassSymbol.class); } In Listing 3.14 ist die Implementierung von getSymbolRecursive in der Klasse SymbolWithTable notiert. Wird das Symbol in der aktuellen Symboltabelle nicht gefunden (Zeile 2), so wird die Suche durch Rekursion in der darüber liegenden Symboltabelle fortgesetzt (Zeile 4). Die Rekursion stoppt, falls die aktuelle Symboltabelle eine Instanz der Klasse 63 3 Der Compiler stopClass ist (Zeile 3) oder das gesuchte Symbol gefunden wurde (Zeile 9). Listing 3.14: (Methode getSymbolRecursive) 1 2 3 4 5 6 7 8 9 10 11 public Symbol getSymbolRecursive(String name, Class stopClass) throws SymbolTableException { if(! this.table.containsKey(name)) { if(! stopClass.isInstance(this)) { return this.parentTable.getSymbolRecursive(name, stopClass); } throw new SymbolTableException(ErrorCode.get(301)); } else { return this.table.get(name); } } Es stellt sich heraus, daß das recht komplexe Klassendesign der Symbole und Symboltabellen eine kompakte Implementierung der Suche ermöglicht. Zugriffsschutz Zur Prüfung des Zugriffs ruft resolveQualifiedName die Methode checkAccess auf (Listing 3.15). Diese delegiert die eigentliche Prüfung an die Methode accessOk (Zeile 2). Listing 3.15: (Methode checkAccess zur Zugriffsschutz-Prüfung) 1 2 3 4 5 6 7 8 9 10 private void checkAccess(Symbol s, CodePosition p) throws SemanticException { if(! accessOk((AccessProtected) s)) { if(s instanceof FieldSymbol) { throw new SemanticException(ErrorCode.get(416), s.getName(), p); } else if(s instanceof MethodSymbol) { throw new SemanticException(ErrorCode.get(417), s.getName(), p); } } } Die Methode accessOk (Listing 3.16) muß sowohl Instanzvariablen als auch Instanzmethoden prüfen, daher implementieren die Klassen FieldSymbol und MethodSymbol das gemeinsame Interface AccessProtected und aggregieren ein Objekt vom Typ AccessModifier (Abbildung 3.20). Der klassenbasierte Zugriffsschutz unterscheidet drei Fälle (vgl. Anhang A.2): 64 3.5 Generieren von Jasmin-Assemblercode <<interface>> AccessProtection +setAccess(a:AccessModifier) +getAccess(): AccessModifier +getParentClass(): ClassSymbol MethodSymbol FieldSymbol AccessModifier +includes(a:AccessModifier): boolean Abbildung 3.20: Klassen für den Zugriffsschutz 1. Entspricht die Klasse des zugriffsgeschützten Symbols der aktuellen Klasse (Zeile 2), so ist der Zugriff in jedem Fall gewährt. 2. Ist die aktuelle Klasse eine Unterklasse der Klasse des zugriffsgeschützten Symbols (Zeile 6), so wird der Zugriffsmodifizierer protected bzw. public erwartet. 3. Im anderen Falle wird der Zugriffsmodifizierer public erwartet (Zeile 12). Listing 3.16: (Methode accessOk zur Zugriffsschutz-Prüfung) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 private boolean accessOk(AccessProtected s) { if(this.classSymbol.equals(s.getParentClass())) { // Selbe Klasse braucht private, protected oder public return true; } if(this.classSymbol.isSubClassOf(s.getParentClass())) { // Unterklasse braucht protected oder public return s.getAccess().includes(AccessModifier.getProtected()); } else { // Sonst public benoetigt return s.getAccess().includes(AccessModifier.getPublic()); } } 3.5 Generieren von Jasmin-Assemblercode Um aus den Informationen im AST und den Symboltabellen Bytecode für die JVM zu generieren, wird zunächst mit einem Code-Emitter Assemblercode erzeugt, der in der nächsten 65 3 Der Compiler Phase mit Jasmin assembliert wird. Weitere Aufgabe dieser Phase ist die Berechnung des Platzbedarfs von Operanden-Stacks. 3.5.1 Struktur des Assemblercodes Bevor die Implementierung des Code-Emitters beschrieben wird, behandelt dieser folgende Abschnitt die Struktur des Assemblercodes am Beispiel der Klasse Baum. Es muß zwischen den Anweisungen des Assemblers, die mit einem Punkt beginnen, und den Maschinenbefehlen der JVM unterschieden werden. Die Assembleranweisungen (siehe [6]) sind spezifisch für Jasmin und codieren die Struktur der Klassen. Die Maschinenbefehle sind in der Spezifikation der JVM (siehe [4], Kapitel 6, S. 171 ff.) definiert und werden für den eigentlichen Code innerhalb der Methoden benötigt. In den Zeilen 1 bis 6 (Listing 3.17) sind der Klassenname, die Superklasse und die Instanzvariablen der Klasse Baum definiert. Die Zugriffsmodifizierer werden wie in Java angegeben. Mögliche Typangaben sind I für Integer, V für Void oder auch die Angabe eines Klassennamens, dem ein L (für Load) vorangestellt ist und der mit einem Semikolon abgeschlossen wird. Listing 3.17: (Binärbaum-Programm: Klassendefinition und Instanzvariablen) 1 2 .class public Baum .super java/lang/Object 3 4 5 6 .field public r LBaum; .field public l LBaum; .field protected v I Die Zeilen 8 bis 10 (Listing 3.18) leiten die Methode setvalue ein. In den runden Klammern sind die Typen der Argumente notiert, hinter den Klammern steht der Rückgabetyp. Es folgt die Angabe der Größe des Operanden-Stacks und der Anzahl der lokalen Variablen, die innerhalb der Methode definiert sind. Hierzu zählen auch die Parametervariablen und this. Abgeschlossen wird die Methode in Zeile 27. Innerhalb der Methode ist der Code für zwei verschachtelte if-Statements zu sehen (vgl. Listing 1.1, Zeilen 45 bis 51), die durch für Assemblercode typische Sprünge realisiert werden. Am Beispiel des inneren if-Statements wird im nächsten Abschnitt die Funktionsweise des Code-Emitters erläutert. Listing 3.18: (Binärbaum-Programm: Methode setValue in Assemblercode) 8 9 10 11 .method public setValue(I)V .limit stack 2 .limit locals 2 iload 1 66 3.5 Generieren von Jasmin-Assemblercode 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 ldc 10 if_icmpgt c0 iload 1 ldc 0 if_icmplt c1 aload_0 iload 1 putfield Baum/v I goto e1 c1: e1: goto e0 c0: e0: return .end method Die Methode inorder in Listing 3.19 zeigt die Übersetzung qualifizierter Namen, des Methodenaufrufs und des print-Statements. Der Methodenaufruf this.l.inorder() wird durch die Zeilen 32 bis 34 abgebildet. Zunächst wird die Objektreferenz this auf den Stack geladen. Der Befehl getfield nimmt die Referenz vom Stack und legt den Wert der Instanzvariablen l auf den Stack. Der nächste Befehl nimmt wiederum diese Objektreferenz vom Stack und führt die Methode inorder auf ihr aus. Entsprechend ist in den Zeilen 39 bis 41 die Übersetzung von this.r.inorder() angegeben. Die Zeilen 35 bis 38 zeigen den Assemblercode des Befehls print(this.v), die Übersetzung wird in Abschnitt 3.5.3 im Zusammenhang mit der Berechnung der von der Methode benötigten Stackgröße erläutert. Listing 3.19: (Binärbaum-Programm: Methode inorder in Assemblercode) 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 .method public inorder()V .limit stack 2 .limit locals 1 aload_0 getfield Baum/l LBaum; invokevirtual Baum/inorder()V getstatic java/lang/System/out Ljava/io/PrintStream; aload_0 getfield Baum/v I invokevirtual java/io/PrintStream/println(I)V aload_0 getfield Baum/r LBaum; invokevirtual Baum/inorder()V return .end method 67 3 Der Compiler Listing 3.20 zeigt schließlich den Konstruktor der Klasse, der in der JVM durch den speziellen Namen <init> repräsentiert wird. In den Zeilen 48 und 49 wird der Konstruktor der Superklasse Object aufgerufen. Listing 3.20: (Binärbaum-Programm: Konstruktor in Assemblercode ) 45 46 47 48 49 50 51 .method public <init>()V .limit stack 1 .limit locals 1 aload_0 invokespecial java/lang/Object/<init>()V return .end method 3.5.2 Funktionsweise des Code-Emitters Implementiert wird der Code-Emitter durch einen AST-Parser (Abbildung 3.21), der sich der Template-Engine StringTemplate bedient, um strukturierten Text auszugeben. StringTemplate ist eine in ANTLR integrierte Bibliothek für Java, mit deren Hilfe Text aus einzelnen Fragmenten flexibel zusammengesetzt werden kann. Hierzu werden Templates mit Platzhaltern definiert, in die andere Templates, Strings oder Java-Objekte eingefügt werden können. Die Templates können beliebig verschachtelt werden und befinden sich in der Datei JasminCodeEmitterTemplates.stg. org.antlr.runtime.tree.TreeParser AstWalkerBase JasminCodeEmitterWalkerBase JasminCodeEmitterWalker Erzeugt JasminCodeEmitterWalker.g org.antlr.strintemplate.StringTemplate Laden der Templates JasminCodeEmitterTemplates.stg Abbildung 3.21: Klassen des AST-Parsers für den Code-Emitter Nachstehend wird die Funktionsweise des Code-Emitters anhand der Übersetzung des ifStatements verdeutlicht. Listing 3.21 zeigt ein Beispiel aus der Methode setValue der Klasse Baum. 68 3.5 Generieren von Jasmin-Assemblercode Listing 3.21: (Binärbaum-Programm: if-Statement) 49 50 51 if v >= 0 { this.v = v; } Die Übersetzung in Assemblercode wird in Listing 3.22 dargestellt. Zunächst wird der Wert der lokalen Variablen v (Zeile 14), dann die Konstante 0 (Zeile 15) auf den Stack geladen. Die beiden Werte auf dem Stack werden in Zeile 16 verglichen, und falls v < 0 ist, die inverse Logik also, wird zu Marke c1 (Zeile 21) hinter den if-Zweig gesprungen. Anderenfalls wird zunächst die Objektreferenz this (Zeile 17) und danach der Wert der lokalen Variable v auf den Stack geladen. Der Befehl putfield in Zeile 19 nimmt den obersten Wert vom Stack und speichert ihn in der Instanzvariablen Baum/v der nun oben auf dem Stack liegenden Objektreferenz (this). Schließlich wird in Zeile 20 an das Ende des if-Statements gesprungen. Listing 3.22: (Jasmin-Assemblercode: if-Statement) 14 15 16 17 18 19 20 21 22 iload 1 ldc 0 if_icmplt c1 aload_0 iload 1 putfield Baum/v I goto e1 c1: e1: Das Beispiel verdeutlicht, daß der Code-Emitter nicht zwischen if-Statements mit oder ohne else-Zweig unterscheidet und daher hier überflüssigen Code erzeugt. Das Beispiel enthält keinen else-Zweig, weshalb eine Sprungmarke genügt hätte und der goto-Befehl unnötig ist. Die Assembler-Befehle eines else-Zweigs lägen zwischen den Sprungmarken. Zudem zeigt das Beispiel, daß lokale Variablen in der JVM nicht über ihren Namen, sondern durch einen Index angesprochen werden. this ist die erste lokale Variable mit Index 0, v die zweite mit Index 1. Ein weiteres Merkmal ist, daß die JVM-Befehle typsicher sind, d. h. iload lädt eine lokale Variable vom Typ Integer, aload eine lokale Variable vom Typ Objektreferenz. Auch der Befehl putfield ist typsicher, wegen des I am Ende des Befehls erwartet er einen Integer-Wert auf dem Stack. Listing 3.23 zeigt die AST-Parser-Regel zur Übersetzung des if-Statements. Die Ausgabe von StringTemplates innerhalb einer ANTLR-Grammatik wird analog zur Ausgabe eines AST mit dem Operator -> notiert (vgl. [8], Kapitel 9, S. 195 ff.). Ergebnis der Regel ist das in Zeile 7 aufgerufene Template ifBlock, welches vier Parameter hat. Mit den Parametern werden die im Template angegebenen Platzhalter gefüllt. 69 3 Der Compiler Im ersten Argument wird das von der in Zeile 3 aufgerufenen Regel condition (Zeilen 13 bis 22) zurückgegebene Template übergeben, im zweiten und dritten Argument sind es die von den beiden statement-Regeln (Zeilen 4 und 5) zurückgelieferten Templates. Die zweite statement-Regel repräsentiert den optionalen else-Zweig und wird ggf. nicht aufgerufen. In diesem Fall beinhaltet die Variable $s2.st in Zeile 9 eine null-Referenz, wodurch der Platzhalter elsestatement im ifBlock-Template mit einem leeren String gefüllt wird. Listing 3.23: (AST-Parser-Regel: if-Statement im Code-Emitter) 1 2 3 4 5 6 7 8 9 10 11 ifBlock : ^( IF {$statement::labelId=$method::labelId++;} condition s1=statement s2=statement? ) -> ifBlock(condition={$condition.st}, ifstatement={$s1.st}, elsestatement={$s2.st}, id={$statement::labelId}) ; 12 13 14 15 16 17 18 19 20 21 condition : ^(compOp expr1=expr expr2=expr) { this.getMethod().decStackSize(2); } -> condition(compop={$compOp.st}, expr1={$expr1.st}, expr2={$expr2.st}) ; 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 compOp : NE -> | EQ -> | LE -> | GE -> | LT -> | GT -> ; 70 compOpNe(id={$statement::labelId}) compOpEq(id={$statement::labelId}) compOpLe(id={$statement::labelId}) compOpGe(id={$statement::labelId}) compOpLt(id={$statement::labelId}) compOpGt(id={$statement::labelId}) 3.5 Generieren von Jasmin-Assemblercode Als viertes Argument wird eine für dieses if-Statement eindeutige Nummer zur Benamung der im Assemblercode benötigten Sprungmarken übergeben. Die Nummer wird in Zeile 2 erzeugt, in $statement::labelId gespeichert und ist innerhalb der aktuellen Methode eindeutig. In der JVM sind Sprünge über Methodengrenzen hinaus unzulässig, daher hat jede Methode ihren eigenen Namensraum für Sprungmarken. Die Variable $statement::labelId ist eine in der Regel statement definierte Scope-Variable, die dem Informationsaustausch von Regeln untereinander dient (vgl. [8], S. 135 ff.). Dies bedeutet, daß alle von der statement-Regel aufgerufenen Regeln Zugriff auf die Variable haben. Die Regel compOp in den Zeilen 23 bis 36 macht davon Gebrauch, um zu den Sprungmarken passende bedingte Sprünge zu erzeugen. Wird allerdings unterhalb von statement wieder statement aufgerufen, so hat die Variable für diesen Teilbaum einen anderen Wert, und es werden für ineinander verschachtelte if-Statements jeweils eindeutige Sprungmarken erzeugt (vgl. Listing 3.18). In der condition-Regel in den Zeilen 13 bis 22 werden in gleicher Weise wie in der ifBlock-Regel die Templates der untergeordneten Regeln compOp und expr in die Platzhalter des Templates condition eingefügt. Stack-Berechnungen wie die in Zeile 16 werden im nächsten Abschnitt erläutert. In Listing 3.24 werden die Templates zu den Regeln dargestellt. Im Template ifBlock wird zunächst die Condition erzeugt (Zeile 2), die die beiden Teilausdrücke auf dem Stack berechnet (Zeilen 11 und 12), bevor, abhängig vom logischen Operator, einer der bedingten Sprünge (Zeilen 16 bis 38) ausgeführt wird. Ziel des Sprungs ist der else-Zweig (Zeile 6). Hinter der Condition wird der if-Zweig eingefügt (Zeile 3) und dann mit goto hinter den else-Zweig gesprungen. Listing 3.24: (Template: if-Statement im Code-Emitter) 1 2 3 4 5 6 7 8 ifBlock(condition, ifstatement, elsestatement, id) ::= << <condition> <ifstatement> goto e<id> c<id>: <elsestatement> e<id>: >> 9 10 11 12 13 14 condition(compop, expr1, expr2) ::= << <expr1> <expr2> <compop> >> 15 16 17 18 compOpNe(id) ::= << if_icmpeq c<id> >> 71 3 Der Compiler 19 20 21 22 compOpEq(id) ::= << if_icmpne c<id> >> 23 24 25 26 compOpLt(id) ::= << if_icmpge c<id> >> 27 28 29 30 compOpGt(id) ::= << if_icmple c<id> >> 31 32 33 34 compOpLe(id) ::= << if_icmpgt c<id> >> 35 36 37 38 compOpGe(id) ::= << if_icmplt c<id> >> Im nächsten Abschnitt folgt ein weiteres Beispiel zur Arbeit des Code-Emitters, das auch die Einbeziehung von im AST annotierten Symbolen zeigt. 3.5.3 Ermitteln der Größe von Methoden-Stacks Die JVM legt zur Laufzeit für jeden Methodenaufruf einen Frame auf den Stack des aktuellen Threads. Bestandteil eines solchen Frames ist der Operanden-Stack für die Berechnungen innerhalb des Methodenrumpfs. Da im Bytecode Informationen zum Platzbedarf der Frames enthalten sein müssen, wird die Größe des Operanden-Stack zur Compilezeit berechnet (vgl. Abschnitt 2.2.1). Es ließe sich vermuten, daß die Berechnung der Stackgröße zur Compilezeit nicht möglich ist. Dies trifft zu, falls ein Assemblerprogramm wie das in Listing 3.25 geschrieben wird. Das Beispiel zeigt eine Schleife, die in jedem Durchlauf die Konstante 1 auf den Stack legt, bis die lokale Variable mit Index 1 gleich der lokalen Variablen mit Index 2 ist. In den Zeilen 2 bis 4 findet der Vergleich mit bedingtem Sprung statt. Zeile 5 legt eine weitere 1 auf den Stack. In den Zeilen 6 bis 8 wird die lokale Variable mit Index 1 inkrementiert, bevor in Zeile 9 wieder zum Anfang der Schleife gesprungen wird. Der benötigte Stackplatz ist also von den lokalen Variablen abhängig und daher erst zur Laufzeit bekannt. Listing 3.25: (Ein Stack unbekannter Größe in Assemblercode) loop: iload 1 1 2 72 3.5 Generieren von Jasmin-Assemblercode 3 4 5 6 7 8 9 10 iload 2 if_icmpeq end ldc 1 ldc 1 iload 1 istore 1 goto loop end: Die JVM ist jedoch zur Übersetzung höherer Programmiersprachen, nicht zur manuellen Assembler-Programmierung gedacht. Tatsächlich zeigt sich bei der Entwicklung des Compilers, daß der benötigte Stackbedarf für alle Sprachelemente von MiniJavaOO berechenbar ist. Zur Berechnung der benötigten Stackgröße werden die Methoden incStackSize und decstackSize des Symbols der Methode, die der AST-Parser gerade traversiert, aufgerufen. Entsprechend der Struktur der erzeugten Assemblerbefehle wird so die Stackgröße in den Parser-Regeln inkrementiert und dekrementiert sowie das Maximum bestimmt. Bei der Erzeugung des Methodenkopfs kann dann auf den Wert zurückgegriffen werden, um einen Platzhalter im Methoden-Template zu füllen. Listing 3.26 zeigt das Template für das print-Statement. Zunächst wird die Referenz für System.out auf den Stack gelegt, dann die auszugebende Expression, die einen Wert auf dem Stack zurückläßt, berechnet und schließlich mit invokevirtual die statische Methode println aufgerufen. println nimmt die Referenz für System.out und den Wert von Expression vom Stack und führt die Ausgabe durch. Listing 3.26: (Template: print-Statement im Code-Emitter) 1 2 3 4 5 print(expr) ::= << getstatic java/lang/System/out Ljava/io/PrintStream; <expr> invokevirtual java/io/PrintStream/println(I)V >> Die zugehörige Parser-Regel in Listing 3.27 erhöht die Stackgröße vor der Ausführung der Regel expr um 1 (Zeile 1). In der Regel expr wird die Stackgröße entsprechend der Struktur des Ausdrucks weiter inkrementiert und dekrementiert, in der Summe wird der Wert um 1 erhöht. Letztlich muß wegen des println-Befehls in Zeile 3 die Stackgröße um 2 dekrementiert werden. Listing 3.27: (AST-Parser-Regel: print-Statement im Code-Emitter) 1 2 3 print : ^(PRINT {this.getMethod().incStackSize(1);} expr) { 73 3 Der Compiler this.getMethod().decStackSize(2); } -> print(expr={$expr.st}) 4 5 6 ; 7 Die AST-Parser-Regel in Listing 3.28 zeigt ein Beispiel, das sich auch die im AST annotierten Symbole zunutze macht. Vor dem Aufruf einer Methode werden deren Argumente auf dem Stack berechnet (Zeile 4). Entsprechend viele Werte liegen auf dem Stack, die durch den Methodenaufruf wieder entfernt werden. In den Zeilen 7 und 8 wird auf das Methodensymbol zugegriffen und die Stackgröße um die Anzahl der Parameter dekrementiert. Listing 3.28: (AST-Parser-Regel: Methodenaufruf im Code-Emitter) 1 2 3 4 5 6 7 8 9 10 11 12 13 invocation : ^( INVOKE name[CodeEmitterNameMode.INVOKE] ^(ARGLIST args+=expr*) ) { MethodSymbol m = (MethodSymbol) $name.lastname.getSymbol(); this.getMethod().decStackSize(m.getNumberOfParams()); } -> invocation(ref={$name.reference}, args={$args}, name={$name.st}) ; Dem Prinzip der beiden gezeigten Beispiele folgend, wird innerhalb jeder AST-ParserRegel des Code-Emitters die Stackgröße inkrementiert und dekrementiert. Die Methode incStackSize speichert jeweils das Maximum, so daß nach der Traversierung des Methoden-Blocks der benötigte Stackbedarf feststeht. 3.6 Generieren und Verifizieren von JVM-Bytecode Nachdem nun sowohl für jede im Quellprogramm definierte Klasse als auch für die globalen Sprachelemente in der speziellen Klasse FunProcMainClass Assemblercode erzeugt wurde, muß dieser noch in auf der JVM lauffähigen Bytecode transferiert werden. Der Jasmin-Assembler bietet hierzu, neben einem Kommandozeilen-Interface, ein JavaAPI (Abbildung 3.22). Der Assemblercode der einzelnen Klassen wird in je ein Objekt vom Typ jasmin.ClassFile eingelesen (Methode readJasmin) und, falls kein Fehler beim Parsen erkannt wird (Methode errorCount), mit der Methode write in ein Byte-Array geschrieben. 74 3.6 Generieren und Verifizieren von JVM-Bytecode jasmin.ClassFile +readJasmin(input:Reader,name:String,numberLines:boolean) +errorCount() +getClassName(): String +write(outp:OutputStream) Abbildung 3.22: Java-API des Jasmin-Assemblers Um sicherzustellen, daß der generierte Bytecode die strukturellen und semantischen Bedingungen der JVM erfüllt, ist es sinnvoll, ihn durch einen Verifier zu überprüfen. Außerdem werden auf diese Weise noch einmal die Abhängigkeiten der Klassen untereinander überprüft. Der Jasmin-Assembler übersetzt jede Klasse für sich. Als Verifier kommt die in Abschnitt 2.2.2 genannte Bytecode Engineering Library zum Einsatz. In Abbildung 3.23 werden die benötigten Klassen und Methoden dargestellt. org.apache.bcel.classfile.ClassParser +ClassParser(input:ByteArrayInputStream) +parse(): JavaClass org.apache.bcel.verifier.VerifierFactory +getVerifier(className:String) org.apache.bcel.Repository +addClass(clazz:JavaClass) org.apache.bcel.verifier.Verifier +doPass1() +doPass2() +doPass3a() +doPass3b() Abbildung 3.23: Klassen des Bytecode-Verifiers Zunächst wird der Bytecode mit Hilfe der Klasse ClassParser in ein Repository eingelesen. Dann wird für jede Klasse ein Verifier-Object erzeugt, das die drei in der JVM-Spezifikation (vgl. [4], Abschnitt 4.9, S. 140 ff.) genannten Phasen durchläuft: Pass 1 Der erste Durchlauf überprüft Formatierung, Struktur und Syntax des Bytecodes. Pass 2 In der zweiten Phase werden die semantischen Bedingungen verifiziert, die ohne Untersuchung der Methodenrümpfe überprüft werden können. Dies beinhaltet z. B. die Prüfung von Namen, Typangaben von Instanzvariablen und Methoden oder Angaben zur Superklasse. Pass 3 Während der dritten Phase werden die Rümpfe der Methoden mit einer Datenflußanalyse durchlaufen. Es werden z. B. die Größe der Operanden-Stacks, die Argumente der Methodenaufrufe oder die Typkompatibilität von Zuweisungen verifiziert. 75 3 Der Compiler Tritt während der Assembler- oder Verifier-Phase ein Fehler auf, so bricht der Compiler wie üblich mit einer CompilerException und der Ausgabe der von Jasmin oder der BCEL produzierten Fehlermeldung ab. Werden die beiden Phasen ohne Fehler durchlaufen, so speichert der Compiler schließlich den Bytecode, den Assemblercode und die visuelle Darstellung des AST im GraphvizFormat in Dateien ab. 76 4 Testgetriebene Compiler-Entwicklung Nachdem im vorigen Kapitel die Implementierung des MiniJavaOO-Compilers behandelt wurde, soll nun auf die testgetriebene Entwicklung des Compilers eingegangen werden. Zu Beginn des zweiten Sprints, während des Studiums der Dokumentation zu gUnit, kommt die Idee auf, den Compiler testgetrieben zu entwickeln. Der Grundgedanke ist, nicht die im Test Driven Development (TDD)1 üblichen Unit-Tests durchzuführen, sondern die Tests auf die Ein- und Ausgabe der einzelnen Compiler-Phasen zu beziehen. Voraussetzung für die Entwicklung von Phase n ist, daß Phase n − 1 bereits getestet wurde. So kann als Test-Eingabe stets ein MiniJavaOO-Programm genutzt werden. Ein Beispiel zur Verdeutlichung: Bei der Entwicklung der Compiler-Phase zum Aufbau der Symboltabellen werden für die verschiedenen Symbole und Symboltabellen jeweils kleine MiniJavaOO-Programme geschrieben. Die Test-Eingaben durchlaufen den bereits fertiggestellten und getesteten Parser. Der Parser erzeugt den AST, der als Eingabe für den AST-Parser zum Aufbau der Symboltabellen dient. Der AST-Parser wird nun von Testfall zu Testfall weiterentwickelt. Die durch die Tests überprüfte Ausgabe ist eine eindeutige textuelle Darstellung der Symboltabellen. Die ersten Compiler-Phasen werden mit gUnit getestet. Ab der semantischen Analyse ist dies nicht mehr möglich, da gUnit maximal zwei Parser hintereinander schalten kann. Die AST-Parser der semantischen Analyse und des Code-Emitters werden daher mit einer Kombination aus jUnit und StringTemplate getestet. Der AST-Parser zum Aufbau der Klassentabelle wird, nach außen unsichtbar, durch den AST-Parser zum Aufbau der Symboltabellen aufgerufen. So kann auch diese Phase noch mit gUnit getestet werden. 4.1 Tests mit gUnit Mit gUnit können zwei verschiedene Testarten geschrieben werden. Ein Test kann prüfen, ob das Eingabeprogramm im Sinne der Syntax korrekt ist. Weiterhin kann überprüft werden, ob die vom Parser erzeugte Ausgabe, z. B. ein AST, den Erwartungen entspricht. 1 Siehe http://www.frankwestphal.de/TestgetriebeneEntwicklung.html 77 4 Testgetriebene Compiler-Entwicklung Parser und AST-Konstruktion Listing 4.1 zeigt den Anfang einer gUnit-Test-Datei. In der ersten Zeile wird definiert, welche Grammatik zu testen ist. Ab Zeile 4 erfolgen die eigentlichen Tests, jeweils bezogen auf eine Regel der Grammatik. Zunächst werden vollständige Programme, anschließend lokale Konstantendeklarationen getestet. Es können sowohl innerhalb der Datei angegebene Programmteile (etwa die Variablendeklaration in Zeile 9) als auch umfangreichere, in externen Dateien gespeicherte Programme (wie in Zeile 5) getestet werden. Hinter dem Test-Code wird jeweils durch OK oder FAIL notiert, ob der Parser die Syntax als korrekt oder fehlerhaft erkennen soll. In den Konstantendeklarationen aus den Zeilen 11 und 12 fehlt jeweils die Wertzuweisung, so daß hier ein FAIL erwartet wird. Listing 4.1: (gUnit-Test: Parser) 1 2 gunit MiniJavaOO; @header{package net.pasdziernik.minijavaoo.compiler.generated;} 3 4 5 6 ooProgram: MiniJavaOO_test_1.mjava OK MiniJavaOO_test_mrieke_1.mjava OK 7 8 9 10 11 12 13 14 15 localConstDecl: "final int a=4, b=56, d=2;" OK "final int a = 6;" OK "final int a=1,b;" FAIL "final int b;" FAIL "final int a = b;" OK "final int a = b.c;" OK "final int a = b.c.d;" OK Um die Anzahl der Testfälle zu begrenzen, sind die Tests so aufgebaut, daß für jede Regel nur die spezifischen Aspekte des entsprechenden Sprachkonstrukts getestet werden müssen. In den EBNF-Regeln für if und while (Syntax 4.1) sind sowohl Expression als auch Statement als auch Condition enthalten. Die Tests in Listing 4.2 setzen voraus, daß mögliche Ausprägungen von Expression und Statement bereits getestet wurden, weshalb Statement hier z. B. stets durch einen leeren Block repräsentiert wird. Die Regel Condition wird zusammen mit der Regel WhileStatement getestet, so daß dies in den Tests zu if nicht nochmals erfolgt. Syntax 4.1 (Kontrollstrukturen und Conditions) IfStatement → if Condition Statement ( else Statement WhileStatement → while Condition Statement Condition → Expression CondOperator Expression CondOperator → == | != | < | > | <= | >= 78 )? 4.1 Tests mit gUnit Listing 4.2: (gUnit-Test: if und while im Parser) 1 2 3 4 5 6 7 8 9 whileBlock: "while 0 < c { }" OK "while 0 <= c.d { }" OK "while 0 > c.d { }" OK "while 0 >= c.d { }" OK "while 0 == c.d { }" OK "while 0 != c.d { }" OK "while 0 { }" FAIL "while 0 < c.d < f { }" FAIL 10 11 12 13 ifBlock: "if a<b { }" OK "if a<b { } else { }" OK Die Erzeugung der abstrakten Syntaxbäume für die Konstantendeklarationen aus Listing 4.1 wird in Listing 4.3 getestet. Statt OK oder FAIL wird nun nach -> der erwartete AST in der üblichen ANTLR-Syntax angegeben. Listing 4.3: (gUnit-Test: Lokale Konstanten im Parser) 1 2 3 4 5 6 7 8 9 10 11 localConstDecl: "final int a=4, b=56, d=2;" -> (VAR (MODIFIER final) int (VAR (MODIFIER final) int "final int a = 6;" -> (VAR (MODIFIER final) int "final int a = b;" -> (VAR (MODIFIER final) int "final int a = b.c;" -> (VAR (MODIFIER final) int "final int a = b.c.d;" -> (VAR (MODIFIER final) int a 4) (VAR (MODIFIER final) int b 56) d 2) a 6) a b) a (QUALIFIEDNAME b c)) a (QUALIFIEDNAME b c d)) Einen Testfall für eine Klassendeklaration zeigt Listing 4.4. Ein mehrzeiliges Eingabeprogramm wird durch doppelte eckige Klammern begrenzt. Listing 4.4: (gUnit-Test: Klasse im Parser) 1 2 3 4 5 6 classDef: << class A extends Object { A(){} public void f() {} 79 4 Testgetriebene Compiler-Entwicklung 7 8 9 10 } >> -> "(class A (extends Object) (A BLOCK) FIELDS (METHODS (METHOD (MODIFIER public) void f PARAMS BLOCK)))" Durch die beiden verschiedenen Testarten können Lexer und Parser zunächst ohne Ausgabe entwickelt werden, um danach die AST-Konstruktion in die Regeln einzufügen. Symboltabellen Wie in Abschnitt 3.2.3 beschrieben, muß der Parser AST-Knoten der Klasse AstNode erzeugen, damit Symbol-Annotationen möglich sind. gUnit kann allerdings von CommonTree abgeleitete Klassen nicht testen. Daher ist es nötig, den Quellcode von gUnit entsprechend anzupassen. Dies betrifft den gUnit-Interpreter, da der gUnit-Generator für jUnit-Tests während der Entwicklung des MiniJavaOO-Compilers keine Anwendung findet. Das Design von gUnit gestattet es nicht, die Anpassung durch Klassen-Ableitungen zu realisieren, vielmehr muß die Klasse gUnitExecutor direkt geändert werden. Listing 4.5 verdeutlicht das Prinzip: In der Klasse wird an drei Stellen ein neues Parser-Objekt erzeugt (Zeile 1). Unmittelbar danach wird, falls es sich um einen MiniJavaOO-Parser handelt (Zeile 3), ein zu AstNode passendes Adapter-Objekt erzeugt und mit dem Parser-Objekt verknüpft (Zeile 5). Listing 4.5: (Anpassen von gUnit an die Klasse AstNode) 1 Object parObj = parConstructor.newInstance(parArgs); 2 3 4 5 6 if(parObj instanceof MiniJavaOOParser) { MiniJavaOOParser mp = (MiniJavaOOParser) parObj; mp.setTreeAdaptor(new AstNodeAdaptor()); } Weitere Voraussetzung für den Eingabe-Ausgabe-Test ist eine formale Darstellung der verschachtelten Symboltabellen als String. Die toString-Methode der Symboltabellen erzeugt hierzu eine den EBNF-Regeln in Syntax 4.2 entsprechende Ausgabe. Eine Symboltabelle besteht aus einer Auflistung von Symbolen innerhalb spitzer Klammern. Ein Symbol ist durch runde Klammern begrenzt und hat einen Namen sowie eine Angabe zur Art des Symbols. Je nach Art des Symbols kommt noch eine Typangabe, z. B. bei Variablensymbolen, oder eine Symboltabelle, z. B. bei Methodensymbolen, hinzu. Der Name eines Blocks beginnt mit $ und beinhaltet die Zeilen- und Spaltenangabe der öffnenden geschweiften Klammer des Blocks. Syntax 4.2 (String-Darstellung von Symboltabellen) SymbolTabelle → < Symbol ∗ > Symbol → ( SymbolName : SymbolArt 80 ( : Typ ) ? ( : SymbolTabelle ) ? ) 4.1 Tests mit gUnit SymbolName → ClassName | SimpleName | BlockName BlockName → $l [0−9]+ c [0−9]+ SymbolArt → CLASS | METH | BLOCK | FIELD | FFIELD Typ → V | I | ClassName | LVAR | FLVAR Da die textuelle Darstellung der Symboltabellen schnell lang wird, werden in Listing 4.6 zwei kürzere Beispiele für Testfälle angegeben. Bereits die Symboltabelle des aus einem leeren Block bestehenden minimalen MiniJavaOO-Programms ist wegen der Darstellung der globalen Sprachelemente als spezielle Klasse FunProcMainClass recht umfangreich. Es wird deutlich, daß die Symboltabelle zahlreiche Symbole enthält, die keine direkte Entsprechung im Quellprogramm haben, z. B. das Symbol für den Konstruktor von FunProcMainClass oder das Schlüsselwort this, welches in jeder Methode als Symbol einer lokalen Variablen auftritt. Die Beispiele zeigen, daß es nicht einfach ist, entsprechende Testfälle zu formulieren. Listing 4.6: (gUnit-Test: AST-Parser für die Symboltabellen) 1 program walks ooProgram: 2 3 4 5 << {} >> -> 6 7 8 9 10 "<(Object:CLASS::<>)(FunProcMainClass:CLASS:Object:< (FunProcMainClass:METH:V:<(this:LVAR:FunProcMainClass) ($l0c0:BLOCK:<>)>)(main:METH:V:<(this:LVAR:FunProcMainClass) ($l2c2:BLOCK:<>)>)>)>" 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 << {} class A extends Object { A() {} } class B extends A { B() {} public void f() { A a; B b = new B(); } } >> -> 27 28 29 "<(Object:CLASS::<>)(FunProcMainClass:CLASS:Object:< (FunProcMainClass:METH:V:<(this:LVAR:FunProcMainClass) 81 4 Testgetriebene Compiler-Entwicklung 30 31 32 33 34 ($l0c0:BLOCK:<>)>)(main:METH:V:<(this:LVAR:FunProcMainClass) ($l2c2:BLOCK:<($l2c2:BLOCK:<>)>)>)>) (A:CLASS:Object:<(A:METH:V:<(this:LVAR:A)($l6c8:BLOCK:<>)>)>) (B:CLASS:A:<(B:METH:V:<(this:LVAR:B)($l11c8:BLOCK:<>)>) (f:METH:V:<(this:LVAR:B)($l12c20:BLOCK:<(a:LVAR:A)(b:LVAR:B)>)>)>)>" Der Parser zum Aufbau der Symboltabellen bricht ab, falls von den Symboltabellen Exceptions geworfen werden. Auch dies wird testgetrieben entwickelt, das Prinzip entspricht den im nächsten Abschnitt beschriebenen Tests für die semantische Analyse. 4.2 Tests mit jUnit und StringTemplate Zum Testen von mehr als zwei hintereinander geschalteten Parsern wie auch zum Testen der Fehlerbehandlung des Compilers kann gUnit nicht eingesetzt werden. Eine Kombination aus StringTemplate, jUnit und einiger Hilfsklassen (siehe Abbildung 4.1) schafft hier Abhilfe. Die Klasse CompilerTestBase bietet vier Methoden, die jeweils ein MiniJavaOO-Programm entgegennehmen und die Ausgabe der jeweiligen Compiler-Phase zurückliefern: • testParser liefert den AST, dargestellt als String. • testSymbolTableConstruction gibt die Klassen- und Symboltabellen als String zurück. • testSemanticCheck liefert keine String-Ausgabe. Hier erfolgen Tests über den Error-Code von geworfenen Exceptions. • testCodeEmitter gibt eine Hash-Tabelle zurück, die die Klassennamen des Quellprogramms als Schlüssel und den Assemblercode der Klassen als Wert enthält. Die Methoden dienen zur Steuerung der Klasse CompilerPhases (siehe Abschnitt 3.1). Zur Formulierung der Testfälle wird zunächst die Klasse CompilerTestTool entwickelt. Dem Konstruktor werden Name und Pfad einer StringTemplate-Datei übergeben, die den MiniJavaOO-Quellcode und die erwarteten Ausgaben der Tests enthält. Mit den verschiedenen set-Methoden können die Eingabeprogramme und die erwarteten Ausgaben der Testfälle aus ineinander verschachtelten Quelltexten mit Platzhaltern zusammengefügt werden. So können mehrere Testfälle, die sich nur in wenigen Codezeilen unterscheiden, effizient dargestellt werden. Die eigentlichen Tests werden mit den TestOk- und TestFail-Methoden durchgeführt. Bei semantischen Tests wird nur eine Eingabe erzeugt, und die TestOk-Methode erwartet, daß keine SemanticException geworfen wird. Die TestFail-Methode erwartet eine Exception mit dem als Parameter angegebenen ErrorCode. Die Test-Methode für den Code-Emitter erwartet, daß die Ausgabe des Tests dem Ausgabe-Template entspricht. 82 4.2 Tests mit jUnit und StringTemplate CompilerTestBase +testParser(input:String): String +testSymbolTableConstruction(input:String): String +testSemanticCheck(input:String) +testCodeEmitter(input:String): HashTable<String, String> CompilerTestTool +CompilerTestTool(path:String,file:String) +setInBaseTemplate(template:String) +setInTemplate(name:String,template:String) +setOutBaseTemplate(template:String) +setOutTemplate(name:String,template:String) +setOutFieldInitsTemplate(template:String) +reset() +sematicTestOk(template:String) +semanticTestOk(name:String,template:String) +semanticTestFail(template:String,code:int) +semanticTestFail(name:String,template:String, code:int) +emitterTestOk(template:String) CompilerPhases CompilerTestTool2 +CompilerTestTool2(path:String,file:String) +reset() +symbolTableConstructionTestOk(source:String) +symbolTableConstructionTestFail(source:String, code:int) +testEmitterOk(source:String,expected:HashTable<String, String>) SourceTemplate +SourceTemplate(path:String,file:String) +reset() +load(template:String) +add(template:String) +add(name:String,template:String) +addCode(name:String,code:String) +toString(): String Abbildung 4.1: Klassen für die jUnit-Tests Bei der Formulierung von Tests für den Code-Emitter zeigt sich, daß Templates für Tests mit mehreren Klassen mit den Methoden der Klasse CompilerTestTool nicht formuliert werden können. Daher werden die Klassen CompilerTestTool2 und SourceTemplate programmiert, die flexibler in der Schachtelung von Templates sind. Die Tests für die Fehlerbehandlung der Symboltabellen-Phase wurden im nachhinein von CompilerTestTool auf CompilerTestTool2 migriert. Die Tests der semantischen Analyse sowie ein Teil der Code-Emitter-Tests wurden aus Zeitmangel nicht migriert. Die folgenden Beispiele verdeutlichen die Anwendung der Test-Klassen in der semantischen Analyse und im Code-Emitter. Semantische Analyse Der Ausschnitt aus einer jUnit-Methode in Listing 4.7 zeigt den Test der semantischen Bedingungen der Methodenüberlagerung. Zunächst wird ein Basis-Template geladen (Zeile 3). Das Template enthält ein MiniJavaOO-Programm mit einer Basisklasse, die verschiedene Methoden definiert, und eine abgeleitete Klasse mit einem Platzhalter <inner> (Listing 4.8, Zeile 1 bis 13). Es folgen vier Tests. Der erste in Zeile 4 lädt den Inhalt des Templates testMethodOverrideSignatureOk in das Basis-Template und läßt das erhaltene Programm durch die Compiler-Phasen bis zur semantischen Analyse laufen. Der Test ist erfolgreich, wenn keine Exception geworfen wird. Die Methodenüberlagerungen in den drei folgenden Tests haben falsche Signaturen, daher wird vom Compiler erwartet, jeweils eine SemanticException mit dem Error-Code 423 zu werfen. 83 4 Testgetriebene Compiler-Entwicklung Listing 4.7: (jUnit-Test: Semantik der Methodenüberlagerung) 1 2 3 4 5 6 7 @Test public void testMethodOverride() throws RecognitionException { ctt.setInBaseTemplate("testMethodOverrideBase"); ctt.semanticTestOk("testMethodOverrideSignatureOk"); ctt.semanticTestFail("testMethodOverrideSignatureFail1", 423); ctt.semanticTestFail("testMethodOverrideSignatureFail2", 423); ctt.semanticTestFail("testMethodOverrideSignatureFail3", 423); Listing 4.8: (Templates zum Test der Methodenüberlagerung) 1 2 3 4 5 6 7 8 9 10 11 12 13 testMethodOverrideBase(inner) ::= << class A extends Object { A() {} public int mpub() { return 0;} protected void mpro(int i, int j) {} private A mpri(int i, B b, Object o) { A a = new A(); return a;} } class B extends A { B() {} <inner> } {} >> 14 15 16 17 18 19 testMethodOverrideSignatureOk() ::= << public int mpub() { return 0;} protected void mpro(int i, int j) {} private A mpri(int i, B b, Object o) { A a = new A(); return a;} >> 20 21 22 23 testMethodOverrideSignatureFail1() ::= << public int mpub(int i) { return 0;} >> 24 25 26 27 testMethodOverrideSignatureFail2() ::= << protected void mpro() {} >> 28 29 30 31 testMethodOverrideSignatureFail3() ::= << protected void mpro(int i) {} >> Nach diesem Schema werden alle Bedingungen der statischen Semantik testgetrieben entwickelt. Ein Teil wird bereits in der Phase zum Aufbau der Symboltabellen abgeprüft. 84 4.2 Tests mit jUnit und StringTemplate Code-Emitter Die testgetriebene Entwicklung der Phase zur Erzeugung des JVM-Assemblercodes soll am Beispiel einer sich rekursiv aufrufenden Methode demonstriert werden. In Listing 4.9 ist der Testfall notiert. Das Eingabe-Programm besteht in diesem Fall aus einem einfachen Template ohne Platzhalter (Listing 4.10, Zeile 1 bis 9) und wird in Zeile 3 geladen. Die erwartete Ausgabe wird in den Zeilen 5 bis 9 zusammengesetzt. Zunächst werden in das allgemeine Klassen-Template (Listing 4.10, Zeile 21 bis 31) die Platzhalter für den Klassennamen und die Basisklasse gefüllt, dann wird das Template für die rekursive Methode (Listing 4.10, Zeile 11 bis 19) in den Platzhalter <inner> eingefügt. Schließlich wird in Zeile 8 die gerade zusammengesetzte Klasse der erwarteten Ausgabe hinzugefügt, ebenso in Zeile 9 das Template für eine leere main-Methode. Zeile 11 ruft dann das CompilerTestTool2-Object zur Durchführung des Tests auf. Listing 4.9: (Test eines Methodenaufrufs) 1 2 3 @Test public void testQualifiedNameMethod() throws RecognitionException { source.load("testQualifiedNameMethod_in"); 4 classBase.addCode("name", "A"); classBase.addCode("base", "java/lang/Object"); classBase.add("testQualifiedNameMethod_out"); expected.put("A", classBase.toString()); expected.put("FunProcMainClass", funProcBase.toString()); 5 6 7 8 9 10 ctt.testEmitterOk(source.toString(), expected); 11 12 } Listing 4.10: (Templates: Test eines Methodenaufrufs) 1 2 3 4 5 6 7 8 9 testQualifiedNameMethod_in() ::= << {} class A extends Object { A() {} public void m() { this.m(); } } >> 10 11 12 13 14 15 16 testQualifiedNameMethod_out() ::= << .method public m()V .limit stack 1 .limit locals 1 aload_0 invokevirtual A/m()V 85 4 Testgetriebene Compiler-Entwicklung 17 18 19 return .end method >> 20 21 classBase(name, base, inner, constructor) ::= << 22 23 24 .class public <name> .super <base> 25 26 <inner> 27 28 <if(constructor)><constructor><else><emptyconstructor(base=base)><endif> 29 30 31 >> 32 33 34 35 36 37 38 39 40 41 emptyconstructor(base) ::= << .method public \<init>()V .limit stack 1 .limit locals 1 aload_0 invokespecial <base>/\<init>()V return .end method >> Nach diesem Muster werden die verschiedenen Teile des Code-Emitters testgetrieben entwickelt. Es wird deutlich, daß die Formulierung größerer Testfälle komplex werden kann. Das Design der Hilfsklassen für die jUnit-Tests hat sich mit den Anforderungen des MiniJavaOO-Compilers entwickelt. Es kann die Grundlage für ein generisches Framework zur Durchführung von Ein-Ausgabe-Tests auf der Basis von jUnit und StingTemplate liefern. 86 5 Zusammenfassung und Ausblick Im folgenden wird zunächst ein Rückblick auf die Inhalte der einzelnen Kapitel dieser Arbeit gegeben, um daraufhin die während der Compiler-Programmierung gemachten Erfahrungen im Hinblick auf die Zielsetzung des Praxisprojekts zu erörtern. Die Arbeit schließt mit einem Ausblick auf mögliche Ansatzpunkte zur Verbesserung und Weiterentwicklung des Compilers. Rückblick In der Einleitung wurden die Motivation zur näheren Beschäftigung mit dem Compilerbau und die mit der Entwicklung des Compilers verfolgten Ziele definiert. Zudem wurden die einzelnen Compiler-Phasen kurz vorgestellt sowie ein Beispielprogramm und dessen Übersetzung gezeigt. Das zweite Kapitel stellte die Quellsprache MiniJavaOO bzw. die Zielmaschine JVM vor und erörterte die Auswahl von Hilfsmitteln zur Programmierung. Es folgte ein Überblick über die Organisation des Entwicklungsprozesses mit Praktiken der agilen Methode Scrum. Design und Implementierung des Compilers waren Thema des dritten Kapitels. Es wurden Ausschnitte aus dem Quellcode des Compilers besprochen und die Zwischenergebnisse der einzelnen Compiler-Phasen anhand der MiniJavaOO-Beispiele Binärbaum-Programm und Ackermann-Programm beschrieben. Im letzten Kapitel der Arbeit wurde schließlich die testgetriebene Entwicklung des Compilers betrachtet, die zunächst mit Hilfe der Grammatik-Testsuite gUnit und dann mit einer Kombination aus StringTemplate und jUnit erfolgte. Ziele und Erfahrungen In der Einleitung wurden drei Ziele formuliert, die nun im Hinblick auf die während der Entwicklung des Compilers gemachten Erfahrungen wieder aufgegriffen werden: Weiterentwicklung einer Programmiersprache Die prozedurale Sprache MiniJavaFunProc wurde zur objektorientierten Programmiersprache MiniJavaOO weiterentwickelt. Im Fokus stand dabei die Kompatibilität zu Java, so daß die entsprechenden Sprachspezifikationen in [3] und [11] die Grundlage für die Spezifikation aus Anhang A legten. 87 5 Zusammenfassung und Ausblick Rückblickend hätte eine weniger vorsichtige Erweiterung der Sprache sowohl die Sprachspezifikation als auch die Implementierung des Compilers an vielen Stellen vereinfacht. Zwei Beispiele zur Verdeutlichung: Die Syntax beschränkt Konstanten auf den Typ Integer. Zur Initialisierung ist ein Integer-Literal oder der Wert eines einfachen Namens erlaubt. Variablen dürfen alternativ eine Objektreferenz aufnehmen und erlauben bei der Initialisierung ein IntegerLiteral, den Wert eines einfachen Namens oder den Aufruf eines Konstruktors. Wären Konstanten nicht im Typ beschränkt und, so wie bei der Zuweisung, beliebige Ausdrücke bei der Initialisierung zugelassen, und wäre der Aufruf des Konstruktors auch ein Ausdruck, so wären sowohl die Syntax-Regeln als auch die Regeln der statischen Semantik wesentlich kompakter. Weiterhin sollte in der Syntax nicht zwischen Funktionen und Prozeduren unterschieden werden. Eine einfache semantische Bedingung, die sicherstellt, daß eine nichtvoid Methode als letzte Anweisung ein Return-Statement enthält, vereinfachte die Grammatik von MiniJavaOO zusätzlich. Realisierung eines Mehrphasen-Compilers Die Entscheidung, den Compiler mit Hilfe von ANTLR zu programmieren, hat sich als richtig erwiesen. ANTLR bietet ein durchgängiges Framework zur Entwicklung eines typischen Mehrphasen-Compilers: • Generatoren für Parser und Lexer auf der Grundlage von EBNF-Grammatiken • Spezielle Syntax zur Transformation eines Quellprogramms in einen AST • Generatoren für Parser zum Traversieren des AST • Spezielle Syntax zum Erzeugen der Ausgabe des Code-Emitters • Unterstützung für automatisierte Grammatik-Tests durch gUnit Die Unterstützung durch die Entwicklungswerkzeuge Eclipse und ANTLRWorks ist hilfreich. Dennoch besteht Verbesserungsbedarf. Es ist umständlich, die Fehlerbehandlung von ANTLR auf die eigenen Bedürfnisse auszurichten, und gUnit bietet keine Möglichkeit, um mehr als zwei hintereinander geschaltete Parser zu testen. Durch die Anwendung der agilen Methode Scrum, wenn diese auch an die Bedürfnisse eines einzelnen Entwicklers angepaßt wurde, konnte der Entwicklungsprozeß geplant und gesteuert werden. Die testgetriebene Entwicklung gab während der Implementierung einer neuen Phase die Sicherheit, daß die Implementierung der vorherigen Phase erfolgreich war. Allerdings hätte die Methode noch konsequenter Anwendung finden können. Es wurden Tests für die Ein- und Ausgaben der einzelnen Phasen, nicht aber für Methoden der Hilfsklassen (Symboltabellen, Exceptions usw.) geschrieben. Desweiteren wurde manchmal im Eifer der Programmierung vergessen, die Tests vor der Implementierung zu schreiben. 88 Wissensvertiefung: Java-Plattform Die Implementierung des Code-Emitters hat bestätigt, daß Java und die JVM einander sehr nahe sind. Im Vergleich zum M32-Assembler ist die JVM auf einem sehr hohen Abstraktionsniveau angesiedelt, welches die Übersetzung statisch typisierter, objektorientierter Programmiersprachen einfach gestaltet. Der Schwerpunkt des MiniJavaOO-Compilers liegt daher auf der semantischen Analyse. Die Spezialisierung der JVM auf statische, objektorientierte Sprachen bringt jedoch nicht nur Vorteile. So ist es derzeit umständlich, dynamische Sprachen wie Ruby1 oder Groovy 2 auf der JVM umzusetzen, weshalb entsprechende Erweiterungen der JVM in Planung sind3 . Mögliche Verbesserungen und Erweiterungen Vor der Erweiterung des Compilers um neue Sprachfeatures sollten zunächst die Sprachspezifikation vereinfacht und der Compiler entsprechend angepaßt werden. Weiterhin könnte sich die Parser-Grammatik zum besseren Verständnis des Compiler-Quellcodes näher an den Bezeichnungen in der Sprachspezifikation orientieren. Eine weitere Vereinfachung des Compilers kann durch eine Überarbeitung der abstrakten Syntax hin zu einer noch kompakteren Darstellung erreicht werden. Nach der Vereinfachung des Compilers kann die Sprache um eine import-Anweisung erweitert werden, um so insbesondere die Benutzung der String- und Collection-Klassen der JVM zu ermöglichen. Voraussetzung sind die Ausweitung der Zugriffsschutzprüfung auf Klassen und die Ergänzung der Symboltabellen um Informationen zu den importierten Klassen. Letzteres kann mit Hilfe der Reflection-API umgesetzt werden. Es ist zusätzlich zu prüfen, ob die Einführung von Interfaces eine weitere nötige Voraussetzung ist. Ebenso von Interesse sind die Weiterentwicklung der Klassen CompilerTestTool2 und SourceTemplate zu einem allgemeinen Compiler-Testframework auf der Basis von jUnit und StringTemplate wie auch die Implementierung des MiniJavaOO-Compilers mit einer Programmiersprache wie Scala, die einen LL(∗)-Parser direkt in die Syntax der Sprache integriert. Hierdurch wird der Sprachmix aus Java- und ANTLR-Code vermieden, und die Parser-Generator-Läufe werden überflüssig. 1 Siehe http://jruby.codehaus.org/ Siehe http://groovy.codehaus.org/ 3 Siehe http://www.jcp.org/en/jsr/detail?id=292 2 89 Literaturverzeichnis [1] H. Abelson, G. J. Sussman, J. Sussman: Struktur und Interpretation von Computerprogrammen: Eine Informatik-Einführung. Springer, 4. Auflage, 2001. [2] W.-G. Bleek, H. Wolf: Agile Softwareentwicklung. dpunkt.verlag, 2008. [3] J. Gosling, B. Joy, G. L. Steele: The Java Language Specification. Addison-Wesley Longman, 3., aktualisierte Auflage, 2005. [4] T. Lindholm, F. Yellin: The Java Virtual Machine Specification. Addison-Wesley Longman, 2., aktualisierte Auflage, 1999. [5] J. Meyer, T. Downing: JAVA Virtual Machine. O’Reilly, 1997. [6] J. Meyer: JASMIN USER GUIDE. Handbuch auf der Jasmin-Homepage, 1996. Online: http://jasmin.sourceforge.net/guide.html, abgerufen: 1.2.2009. [7] T. Parr: LL(*)-Parsing. Vortrag während der ANTLR-Workshops 2005. Online: http://www.antlr.org/workshop/ANTLR2005/presentations/LL-Star.ppt, abgerufen: 1.2.2009. [8] T. Parr: The Definitive ANTLR Reference. The Pragmatic Programmers, 2007. [9] T. Parr: Translators Should Use Tree Grammars. Artikel auf der ANTLR-Homepage, 2004. Online: http://antlr.org/article/1100569809276/use.tree.grammars.tml, abgerufen: 10.2.2009. [10] M. Rieke: Entwicklung eines OO-Compilers für M32-Assemblercode und Erweiterung des M32-Simulators. FH Aachen, SS 2009, Bachelorarbeit. [11] R. Stärk, J. Schmid, E. Börger: Java and the Java Virtual Maschine. Springer-Verlag, 2001. [12] P. Terry: Compiling with C# and Java. Addison-Wesley, 2004. [13] A. Tripp: Manual Tree Walking Is Better Than Tree Grammars. Artikel auf der ANTLR-Homepage. Online: http://www.antlr.org/article/1170602723163/treewalkers.html, abgerufen: 10.2.2009. [14] N. Wirth: Grundlagen und Techniken des Compilerbaus. Oldenbourg, 2., bearbeitete Auflage, 2008. 90 A Syntax und statische Semantik von MiniJavaOO Verfasser: Michael Pasdziernik und Manuel Rieke Die folgenden Erläuterungen stellen die Syntax von MiniJavaOO sowie die von einem Compiler zu prüfenden Bedingungen der statischen Semantik vor. Die angegebenen EBNFProduktionen sollen die Syntax der Sprache möglichst leicht verständlich machen und entsprechen nicht den Parser-Grammatiken der entwickelten Compiler. Zusätzlich wird in Anhang A.9 die komplette Syntax der Sprache als Diagramm dargestellt. Als Grundlage für die Beschreibung der Sprache soll das Beispiel in Listing A.1 dienen. Dieses Beispiel deckt nicht alle, jedoch die wichtigsten Einzelheiten von MiniJavaOO ab. Listing A.1: Beispielprogramm 1 2 3 int iGlobal = 23; int iGlobal2; BspClass myGlobalClass; 4 5 func globalFunction(int iParameter){ 6 return 10 + iParameter * 3; 7 8 } 9 10 11 12 void globalProcedure(int iValue){ iGlobal2 = iValue; } 13 14 15 class BspClass extends Object{ protected int a; 16 17 18 19 20 21 22 23 24 BspClass(){ this.a = 12; } public void test(){ this.test2(); } private void test2(int iVar){ print(iVar); 91 A Syntax und statische Semantik von MiniJavaOO } 25 26 } 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 main{ int iLocal; while iLocal < iGlobal iLocal++; if iGlobal >= 23 print(3); else{ print(4); } /* Dies ist ein mehrzeiliger Kommentar */ BspClass myClass = new BspClass(); // Dies ein einzeiliger Kommentar myClass.test(); myClass = new Class2(); myClass.test(); myClass.test2(iLocal); 46 47 } 48 49 50 51 52 class Class2 extends BspClass{ Class2{ this.a = 1; 53 } public void test(){ print(2) } 54 55 56 57 58 } A.1 Programmstruktur Ein MiniJavaOO-Programm besteht aus der Definition globaler Variablen (Abschnitt A.5) und globaler Methoden (Abschnitt A.6) sowie einer main-Methode, mit der die Ausführung des Programms beginnt. Um die Kompatibilität zu MiniJavaFunProc zu erhalten, kann statt der main-Methode auch ein einzelnes Statement angegeben werden (Abschnitt A.8). Selbstdefinierte Typen in Form von Klassen (Abschnitt A.2) können vor oder nach der main-Methode definiert werden. 92 A.2 Klassen Syntax A.1 (MiniJavaOO-Programm) Program → GlobalVariables Class∗ GlobalMethods GlobalVariables → ( FinalVariable | Variable )∗ GlobalMethods → Method ∗ ProgramStart → ( main Block ) | Statement ProgramStart Class∗ Statische Semantik A.2 (MiniJavaOO-Programm) (1) Die main-Methode kann nicht explizit aufgerufen werden. (2) Klassen haben keinen Zugriff auf globale Variablen und globale Methoden. Kommentare können wie in Java als Zeilen- oder Blockkommentar notiert werden. Beispiel Das Beispiel aus Listing A.2 ist äquivalent zu dem Beispiel aus Listing A.3. Listing A.2: main-Methode zum Programmstart 1 int a = 3; 2 3 4 5 main{ a = 4; } Listing A.3: Einzelnes Statement zum Programmstart 1 int a = 3; 2 3 a = 4; Am Beispiel aus Listing A.1 kann man sehr gut die sonstige Programmstruktur von MiniJavaOO erkennen. Kommentare werden wie in Java geschrieben, ein Beispiel wird in Listing A.1 gegeben. A.2 Klassen Bei der Definition einer Klasse werden nach dem Klassennamen (Abschnitt A.3) und der Unterklasse zunächst die Instanzvariablen (Abschnitt A.5), dann der Konstruktor und schließlich die Instanzmethoden (Abschnitt A.6) angegeben. Instanzvariablen und Instanzmethoden sind nach dem Grad des Zugriffsschutzes sortiert. MiniJavaOO kennt keine statischen Methoden oder Variablen innerhalb von Klassen. 93 A Syntax und statische Semantik von MiniJavaOO Syntax A.3 (Klassen) Class → class ClassName extends ClassName ClassDefinition ClassDefinition → { InstanceVariables Constructor InstanceMethods InstanceVariables → ( public ( FinalVariable | Variable ) )∗ ( protected ( FinalVariable | Variable ) )∗ ( private ( FinalVariable | Variable ) )∗ InstanceMethods → ( public Method )∗ ( protected Method )∗ ( private Method )∗ } Statische Semantik A.4 (Klassen) (1) Zyklenfreiheit der Ableitung: Eine Klasse darf sich nicht direkt oder indirekt selbst erweitern (vgl. [11], S. 48). Zugriffsschutz Die Sichtbarkeit von Instanzmethoden und Instanzvariablen wird durch die Zugriffsmodifizierer eingeschränkt. Im Vergleich zu Java (vgl. [3], S. 138 ff.) ist der Zugriffsschutz in MiniJavaOO aufgrund des Fehlens von Paketen einfacher definiert. Es gibt keinen DefaultZugriff. Statische Semantik A.5 (Zugriffsschutz) (1) Eine als public deklarierte Instanzvariable oder Instanzmethode ist in globalen Methoden, der main-Methode und allen Klassen sichtbar. (2) Eine als protected deklarierte Instanzvariable oder Instanzmethode ist in der sie definierenden Klasse und in von dieser abgeleiteten Klassen sichtbar. (3) Eine als private deklarierte Instanzvariable oder Instanzmethode ist in der sie definierenden Klasse sichtbar. Konstruktor und Konstruktoraufruf Eine MiniJavaOO-Klasse hat einen parameterlosen Konstruktor. Beim Aufruf des Konstruktors wird zunächst der Konstruktor der Basisklasse aufgerufen und daraufhin der Konstruktorrumpf (Abschnitt A.8) abgearbeitet. Syntax A.6 (Konstruktordefinition und Konstruktoraufruf ) Constructor → ClassName () Block ConstructorCall → new ClassName () 94 A.2 Klassen Statische Semantik A.7 (Konstruktordefinition und Konstruktoraufruf ) (1) In der Konstruktordefinition muß der Name des Konstruktors dem Klassennamen entsprechen (vgl. [11], S. 73). (2) Beim Konstruktoraufruf muß ClassName eine im Programm definierte Klasse oder Object referenzieren. Beispiel Listing A.4 zeigt ein Minimalbeispiel einer Klasse. Diese Beispielklasse enthält in diesem Fall nur die von Object geerbten Attribute und Routinen, besitzt jedoch keine eigenen. Listing A.4: Minimalbeispiel einer Klasse 1 2 3 class SimpleClass extends Object { SimpleClass(){ 4 } 5 6 7 } Das globale Beispiel aus Listing A.1 enthält zwei weitere Beispiele zur Deklarierung einer Klasse. Das erste Beispiel befindet sich in den Zeilen 14 bis 26: 14 15 class BspClass extends Object{ protected int a; 16 BspClass(){ this.a = 12; } public void test(){ this.test2(); } private void test2(int iVar){ print(iVar); } 17 18 19 20 21 22 23 24 25 26 } Das zweite Beispiel ist in den Zeilen 50 bis 61 zu sehen: 50 51 52 class Class2 extends BspClass{ Class2{ this.a = 1; 53 54 } 95 A Syntax und statische Semantik von MiniJavaOO public void test(){ print(2) } 55 56 57 58 } A.3 Typen, Literale, Bezeichner und Namen MiniJavaOO kennt die grundlegenden Typen Integer mit 32-Bit Breite und Objektreferenz. Integer-Literale (NUMBER) werden in positiver Dezimalschreibweise angegeben. Objektreferenzen können auf Objekte der Klasse Object und von Object abgeleitete und im Programm definierte Klassen zeigen. Die Basisklasse Object enthält keine Methoden oder Attribute. Syntax A.8 (Typangaben und Literale) Type → int | ClassName NUMBER → [1−9] [0−9]* Statische Semantik A.9 (Typangabe) (1) Bei einer Typangabe muß ClassName eine im Programm definierte Klasse oder Object referenzieren (vgl. [11], S. 47 und S. 51). Klassennamen bestehen aus einem Bezeichner, beginnend mit einem Großbuchstaben (UPPERIDENT ). Namen (SimpleName) für Variablen und Methoden beginnen mit einem Kleinbuchstaben (LowerIdent). Syntax A.10 (Bezeichner und Namen) ClassName → UPPERIDENT UPPERIDENT → [A−Z] ( [0−9] | [a−z] | [A−Z] )∗ SimpleName → LOWERIDENT LOWERIDENT → [a−z] ( [0−9] | [a−z] | [A−Z] )∗ Beispiel Im globalen Beispiel aus Listing A.1 beginnen Attribute sowie lokale und globale Variablen alle mit kleinem Buchstaben. Zeilen 1 bis 3: 1 2 3 int iGlobal = 23; int iGlobal2; BspClass myGlobalClass; 96 A.4 Referenzieren von Variablen und Methoden Zeile 15: 15 protected int a; A.4 Referenzieren von Variablen und Methoden Der Zugriff auf Variablen und Methoden erfolgt über ihren Namen. Bei globalen und lokalen Variablen sowie globalen Methoden ist dies der einfache Name (Abschnitt A.3). Bei Instanzvariablen und Instanzmethoden wird der qualifizierte Name angegeben. Er besteht aus einer Sequenz einfacher Namen, die durch den Punktoperator getrennt werden. (Vgl. [3], S. 113 ff.) Syntax A.11 (Namen) Name → this | SimpleName | QualifiedName QualifiedName → ( this | SimpleName ) ( . SimpleName )+ Statische Semantik A.12 (Namen) (1) this ist nur innerhalb einer Klasse ein gültiger Name. (2) Ein einfacher Name innerhalb einer Klasse muß eine lokale Variable referenzieren. (3) Ein einfacher Name innerhalb einer globalen Methode muß eine lokale oder globale Variable oder eine globale Methode referenzieren. (4) Die erste Komponente eines qualifizierten Namens innerhalb einer globalen Methode muß eine lokale oder globale Variable referenzieren. (5) Die erste Komponente eines qualifizierten Namens innerhalb einer Klasse muß this oder eine lokale Variable referenzieren. (6) Eine mittlere Komponente eines qualifizierten Namens muß eine Instanzvariable referenzieren, die in der Klasse oder einer Oberklasse der vorherigen Komponente deklariert und im Kontext des Zugriffs entsprechend den Zugriffsregeln (siehe Abschnitt A.2) sichtbar ist. (7) Die Instanzvariable einer mittleren Komponente muß ein Referenztyp sein. (8) Die letzte Komponente eines qualifizierten Namens muß eine Instanzvariable oder Instanzmethode referenzieren, die in der Klasse oder einer Oberklasse der vorherigen Komponente deklariert und im Kontext des Zugriffs entsprechend den Zugriffsregeln (siehe Abschnitt A.2) sichtbar ist. (9) Eine lokale Variable als Bestandteil eines Namens muß im Scope ihrer Deklaration sein (siehe Abschnitt A.5, vgl. [11], S. 35). 97 A Syntax und statische Semantik von MiniJavaOO Beispiel Das globale Beispiel aus Listing A.1 zeigt verschiedene Variablenreferenzierungen, so etwa in Zeile 31: iLocal++; 31 In dieser Zeile wird iLocal referenziert und der Wert dieser Variablen um 1 erhöht. Eine Methode wird beispielsweise in der Main-Methode referenziert. Zeilen 42 bis 45: myClass.test(); myClass = new Class2(); myClass.test(); myClass.test2(iLocal); 42 43 44 45 A.5 Variablen Ein Deklarations-Statement kann mehrere Einzeldeklarationen enthalten. Als final deklarierte Variablen sind auf den Typ Integer beschränkt und müssen bei der Deklaration mit dem Wert einer anderen Variablen (Abschnitt A.4) oder einem Integer-Literal (Abschnitt A.3) initialisiert werden. Nicht als final deklarierte Variablen können außerdem durch einen Konstruktoraufruf (Abschnitt A.2) initialisiert werden. Syntax A.13 (Deklaration von Variablen) FinalVariable → final int SingleFinalVariable ( , SingleFinalVariable SingleFinalVariable → SimpleName = NUMBER Variable → Type SingleVariable ( , SingleVariable )∗ ; SingleVariable → SimpleName ( = ( NUMBER | ConstructorCall ) )? )∗ ; Variablen werden automatisch mit 0 oder einer null-Referenz initialisiert. Statische Semantik A.14 (Deklaration von Variablen) (1) Die Deklaration einer globalen Variablen darf eine bereits deklarierte globale Variable nicht überdecken. (2) Die Deklaration einer lokalen Variablen darf eine bereits deklarierte lokale Variable1 nicht überdecken (vgl. [11], S. 35 und S. 52). (3) Die Deklaration einer Instanzvariablen darf eine bereits deklarierte Instanzvariable oder Instanzmethode der gleichen Klasse nicht überdecken (vgl. [11], S. 53). 1 Der Begriff lokale Variable beinhaltet auch Methoden-Parameter. 98 A.6 Methoden (4) Die Deklaration einer Instanzvariablen darf eine Instanzvariable einer Superklasse überdecken (vgl. [11], S. 53). (5) Ist die deklarierte Variable vom Typ Integer, so muß der Initialisierungswert vom Typ Integer sein. (6) Ist die deklarierte Variable ein Referenztyp, so muß der Initialisierungswert eine Referenz auf ein Objekt der gleichen Klasse oder einer Unterklasse sein. Statische Semantik A.15 (Scope von Variablen) (1) Eine globale Variable ist in allen globalen Methoden sowie der main-Methode sichtbar. (2) Der Scope einer lokalen Variablen reicht von der Deklaration bis zum Ende des Blocks, in dem sie deklariert wurde. Dies schließt auch weitere, in den Block verschachtelte Blöcke ein. (3) Lokale Parametervariablen sind im gesamten Methodenblock sichtbar. Beispiel 2 int iGlobal2; In Zeile 2 des globalen Beispiels (Listing A.1) wird eine globale Variable definiert und automatisch mit dem Wert 0 initialisiert. Ein Beispiel für eine finale Variable, auch Konstante genannt, ist folgender Programmcode: 1 final int iFinal = 4; Diese Konstante wird innerhalb ihres Scopes genauso referenziert wie eine normale Variable. A.6 Methoden MiniJavaOO unterscheidet die Methodentypen Funktion und Prozedur. Prozedurdefinitionen werden mit dem Schlüsselwort void eingeleitet, Funktionsdefinitionen mit dem Typ des Rückgabewerts. Aus Kompatibilitätsgründen zu MiniJavaFunProc kann eine Funktionsdefinition auch mit dem Schlüsselwort func beginnen, folgt daraufhin keine Typangabe, so ist die Funktion vom Typ Integer. Funktionen liefern als letztes Statement ihres Blocks (Abschnitt A.8) einen Integer-Wert oder eine Objektreferenz zurück. Die Aufrufparameter zählen zu den lokalen Variablen einer Methode. 99 A Syntax und statische Semantik von MiniJavaOO Syntax A.16 (Definition und Aufruf von Methoden) Method → Procedure | Function Procedure → void SimpleName ( Parameters? ) Block Parameters → Type SimpleName ( , Type SimpleName )∗ Function → FunctionStart SimpleName ( Parameters? ) FunctionBlock FunctionStart → func | ( func? Type ) FunctionBlock → { BlockElement∗ return Expression ; } MethodInvocation → Name ( Arguments? ) Arguments → Expression ( , Expression )∗ Während Funktionsaufrufe immer Teil eines Ausdrucks sind (Abschnitt A.7), treten Prozeduraufrufe als Statement (Abschnitt A.8) auf. MiniJavaOO erlaubt das Überlagern von Instanzmethoden durch die Neudefinition in einer abgeleiteten Klasse. Die Überladung von Methoden ist nicht möglich. Statische Semantik A.17 (Methodendefinition) (1) Die Definition einer globalen Methode darf nicht eine bereits definierte globale Variable oder globale Methode überdecken. (2) Die Definition einer Instanzmethode darf nicht eine in dieser oder in einer Superklasse definierte Instanzvariable überdecken. (3) Die Definition einer Instanzmethode darf eine Instanzmethode einer Superklasse mit gleichem Namen und gleicher Signatur überlagern (vgl. [11], S. 55). (4) Die überlagernde Methode darf im Vergleich zur überlagerten Methode die Sichtbarkeit nicht weiter einschränken (vgl. [11], S. 55). (5) Ist die Methode vom Typ Integer, so muß der Ausdruck des return-Statements einen Integer-Wert liefern. (6) Ist die Methode vom Referenztyp, so muß der Ausdruck des return-Statements eine Objektreferenz derselben oder einer abgeleiteten Klasse liefern. (7) Die Namen der Parametervariablen sind paarweise verschieden (vgl. [11], S. 51). (8) Innerhalb eines Methodenaufrufs muß Name eine Methode referenzieren. (9) Beim Methodenaufruf müssen Typ und Anzahl der Argumente mit der Signatur der Methode übereinstimmen. Ist der Typ eines Parameters eine Klasse, so muß das entsprechende Argument Objektreferenzen der gleichen oder einer abgeleiteten Klasse referenzieren. (10) Das Schlüsselwort func darf bei globalen Methodendeklarationen nicht weggelassen werden. 100 A.7 Ausdrücke Beispiel Das Globalbeispiel (Listing A.1) beinhaltet eine globale Funktion und eine globale Prozedur (Zeilen 5 bis 12): 5 func globalFunction(int iParameter){ 6 return 10 + iParameter * 3; 7 8 } 9 10 11 12 void globalProcedure(int iValue){ iGlobal2 = iValue; } Wie man erkennen kann, werden Routinenparameter äquivalent zu Java angegeben. A.7 Ausdrücke Ein Ausdruck kann eine Objektreferenz oder einen Integer-Wert zurückliefern. Atomare Bestandteile können Methodenaufrufe (Abschnitt A.6), Variablenwerte (Abschnitt A.4) und Integer-Literale (Abschnitt A.3) sein. Zusammengesetzte Ausdrücke sind immer arithmetische Berechnungen. Neben der Addition, Subtraktion, Multiplikation und Division ist die Restwertberechnung möglich. Die Inkrement- und Dekrement-Operation kann auch als einzelnes Statement (Abschnitt A.8) ausgeführt werden. Syntax A.18 (Ausdrücke) Expression → | | | | | ArithOperator Increment → Decrement → Name MethodInvocation NUMBER ( Expression ) Increment | Decrement Expression ArithOperator Expression → + | - | * | / | % ( ++ Name ) | ( Name ++ ) ( -- Name ) | ( Name -- ) Statische Semantik A.19 (Ausdrücke) (1) Jeder Teilausdruck eines arithmetischen Ausdrucks muß vom Typ Integer sein (vgl. [11], S. 37). (2) Eine innerhalb eines Ausdrucks aufgerufene Methode muß eine Funktion sein. 101 A Syntax und statische Semantik von MiniJavaOO (3) Name muß eine Variable referenzieren. (4) Eine zu inkrementierende oder dekrementierende Variable muß vom Typ Integer sein (vgl. [11], S. 37). (5) Eine zu inkrementierende oder dekrementierende Variable darf nicht final sein. Beispiel In Zeile 7 des Beispiels aus Listing A.1 wird eine Rechnung durchgeführt: return 10 + iParameter * 3; 7 Dabei wird die Punkt-vor-Strich-Regel eingehalten. Der Restwert-Operator (%) wird dabei zu den Multiplikationsoperatoren, zu denen auch die Multiplikation (*) und die Division (/) gehören, gezählt. A.8 Blöcke und Statements Blöcke bestehen aus Variablendeklarationen (Abschnitt A.5) sowie Statements und können ineinander verschachtelt werden. Statements sind die Bildschirmausgabe über print, Kontrollstrukturen, Zuweisungen, Inkrement- und Dekrement-Anweisungen (Abschnitt A.7) sowie der Aufruf von Methoden (Abschnitt A.6). Syntax A.20 (Blöcke und Statements) Block → { BlockElement∗ } BlockElement → ( Variable | FinalVariable Statement → Block | Print | ControlStructure | Assignment | Increment | Decrement ; | MethodInvocation ; Print → print ( Expression ) ; | Statement ) Das print-Statement gibt einen Wert in die Standardausgabe aus. Statische Semantik A.21 (Blöcke und Statements) (1) Ein mit print auszugebender Ausdruck muß vom Typ Integer sein. (2) Beim Methodenaufruf muß die Methode eine Prozedur sein. 102 A.8 Blöcke und Statements (3) Inkrement- und Dekrement-Anweisungen sind nur in Postfix-Schreibweise zulässig. Kontrollstrukturen Als Kontrollstrukturen stehen die bedingte Ausführung über if mit optionalem elseZweig und die while-Schleife zur Verfügung. Die Angabe der Bedingungen erfolgt über einfache logische Ausdrücke. Syntax A.22 (Kontrollstrukturen und logische Ausdrücke) ControlStructure → IfStatement | WhileStatement IfStatement → if Condition Statement ( else Statement WhileStatement → while Condition Statement Condition → Expression CondOperator Expression CondOperator → == | != | < | > | <= | >= )? Statische Semantik A.23 (Conditions) (1) Die beiden Teilausdrücke müssen vom Typ Integer sein (vgl. [11], S. 37). Zuweisung Mit der Zuweisung kann Variablen der Wert eines Ausdrucks (Abschnitt A.7) oder die Referenz auf ein durch einen Konstruktoraufruf erzeugtes Objekt (Abschnitt A.2) zugewiesen werden. Syntax A.24 (Zuweisung) Assignment → Name = ( Expression | ConstructorCall ) ; Statische Semantik A.25 (Zuweisung) (1) Die Zuweisung an this ist nicht möglich. (2) Name muß eine Variable referenzieren. (3) Die Variable darf nicht final sein. (4) Ist die Variable Name vom Typ Integer, so muß der Wert der Zuweisung vom Typ Integer sein (vgl. [11], S. 37). (5) Ist die Variable Name ein Referenztyp, so muß der Wert der Zuweisung eine Referenz der gleichen Klasse oder einer Unterklasse sein (vgl. [11], S. 37). 103 A Syntax und statische Semantik von MiniJavaOO Beispiel Zeilen 30 bis 36 aus dem Globalbeispiel (Listing A.1) veranschaulichen den Umgang mit Statements und Blöcken. while iLocal < iGlobal iLocal++; if iGlobal >= 23 print(3); else{ print(4); } 30 31 32 33 34 35 36 Im Vergleich zu Java ist es wichtig zu erkennen, daß die logischen Ausdrücke in MiniJavaOO nicht in Klammern stehen dürfen. Auch existieren keine Bedingungsblöcke wie in Java. 104 A.9 Syntaxdiagramme A.9 Syntaxdiagramme Program ² GlobalMethods ¯ GlobalVariables ¯ ¯° ±² Class ± ° ² ° ± ProgramStart ² ¯ ¯° ±² Class ± ° GlobalVariables ² ¯ ±²¯ FinalVariable ²¯° ± Variable ° ± ° GlobalMethods ² ¯ ±² Method ± ¯° ° ProgramStart ² ¯ ¯ main Block ± ° ± Statement ² ° Class ² ¯ class °ClassName ± ² ¯ extends °ClassName ± ClassDefinition ClassDefinition ²¯ { InstanceVariables ±° Constructor InstanceMethods ²¯ } ±° 105 A Syntax und statische Semantik von MiniJavaOO InstanceVariables ¯ ² ¯ ±² public ¯ FinalVariable ± ° ± Variable ²¯ ²¯° ° ± ° ² ° ²¯ ±¯ ² ¯ ±² protected ¯ FinalVariable ± ° ± Variable ²¯° ° ± ° ² ° ² ±¯ ² ¯ ±² private ¯ FinalVariable ± ° ± Variable ± 106 ²¯° ° ° A.9 Syntaxdiagramme InstanceMethods ¯ ²¯ ² ¯ ±² public Method ± ° ¯° ± ° ² ° ±¯ ²¯ ² ¯ ±² protected Method ± ° ¯° ± ° ² ° ±¯ ² ¯ ±² private Method ± ° ± ² ¯° ° Constructor ClassName ²¯ ²¯ ( Block ) ±° ±° ConstructorCall ² ¯ new ClassName ± ° ²¯ ²¯ ( ) ±° ±° Name ² ¯ ¯ this ± ° ² ± SimpleName ° ± QualifiedName ° QualifiedName ² ¯ ¯ this ± ° ± SimpleName ²¯ ²² . SimpleName ±° ¯ °± ° FinalVariable ² ¯ ² ¯ final ° int SingleFinalVariable ± ± ° ²¯ ²; ±° ¯ ²¯ ±² , SingleFinalVariable ±° ± ¯° ° 107 A Syntax und statische Semantik von MiniJavaOO SingleFinalVariable SimpleName ²¯ NUMBER = ±° Variable Type ²¯ ²; ±° ¯ SingleVariable ²¯ ±² , SingleVariable ±° ± ¯° ° SingleVariable SimpleName ²¯ ¯ = ±° ² ±¯ NUMBER ²° ± ConstructorCall ° Method ¯ Procedure ² ± Function ° Procedure ² ¯ void °SimpleName ± ²¯ ¯ ( ±° ± Parameters ²¯ ²) Block ±° ° Parameters Type SimpleName ¯ ²¯ ±² , Type ±° ² SimpleName ± ¯° ° Function FunctionStart SimpleName ²¯ ¯ ( ±° ± Parameters FunctionStart ² ¯ ¯ func ± ° ±¯ ± func 108 ² ² Type ° ° ²¯ ²) FunctionBlock ±° ° A.9 Syntaxdiagramme FunctionBlock ²¯ ¯ { ±° ±² BlockElement ± ² ¯ ² return Expression ± ° ¯° ²¯ ²¯ ; } ±° ±° ° MethodInvocation Name ²¯ ¯ ( ±° ± Arguments ²¯ ²) ±° ° Arguments Expression ¯ ²¯ ±² , Expression ±° ± ² ¯° ° Expression ¯ Name ² ± MethodInvocation ° ± NUMBER ° ²¯ ±( Expression ±° ± Increment ²¯ ° ) ±° ° ± Decrement ± Expression ° ArithOperator Expression ° ArithOperator ²¯ ¯+ ² ±° ²¯ ±° ±° ²¯ ±* ° ±° ²¯ ±/ ° ±° ²¯ ±% ° ±° 109 A Syntax und statische Semantik von MiniJavaOO Increment ² ¯ ¯ ++ ² Name ± ° ² ¯ ± Name ° ++ ± ° Decrement ² ¯ ¯- ² Name ± ° ² ¯ ± Name ° - ± ° Block ²¯ ¯ { ±° ±² BlockElement ± ²¯ ²} ±° ¯° ° BlockElement ¯ Variable ² ± FinalVariable ° ± Statement ° Statement ¯ Block ² ± Print ° ± ControlStructure ° ± Assignment ° ± Increment ± Decrement ²¯ ° ; ±° ²¯ ° ; ±° ± MethodInvocation ²¯ ° ; ±° Print print ²¯ ( Expression ±° ControlStructure 110 ¯ IfStatement ² ± WhileStatement ° ²¯ ²¯ ) ; ±° ±° A.9 Syntaxdiagramme IfStatement ² ¯ if Condition ± ° Statement ² ¯ ± else Statement ± ° WhileStatement ² ¯ while °Condition ± Condition Expression ¯ ² ° Statement CondOperator Expression CondOperator ² ¯ ¯ == ² ± ° ² ¯ ± != ° ± ° ²¯ ±< ° ±° ²¯ ±> ° ±° ² ¯ ± <= ° ± ° ² ¯ ± >= ° ± ° Assignment Name ²¯ ¯ Expression = ±° ± ConstructorCall ²¯ ²; ±° ° 111 B Grammatiken B.1 Lexer Schlüsselwörter: FINAL → final INT → int VOID → void FUNC → func RETURN → return IF → if ELSE → else WHILE → while PRINT → print CLASS → class EXTENDS → extends PUBLIC → public PROTECTED → protected PRIVATE → private NEW → new MAIN → main THIS → this Operatoren: PO → . NE → != EQ → == LE → <= GE → >= LT → < GT → > ADD → + SUB → - 112 B.1 Lexer MULT → * DIV → / MOD → % INC → ++ DEC → -ASSIGN → = Struktur: RLB → ( RRB → ) CLB → { CRB → } COMMA → , ENDSTMNT → ; Literale und Bezeichner: NUMBER → 0 | ( [1−9] [0−9]* ) UPPERIDENT → [A−Z] ( [0−9] | [a−z] | [A−Z] )∗ LOWERIDENT → [a−z] ( [0−9] | [a−z] | [A−Z] )∗ CLASSIDENT → UPPERIDENT IDENT → LOWERIDENT 113 B Grammatiken B.2 Parser Program → ConstDecl ∗ VarDecl ∗ {enableOO}?=> ClassDef ∗ Procedure∗ Function∗ MainFunction {enableOO}?=> ClassDef ∗ MainFunction → {enableExtensions}?=> MAIN Block | Statement ConstDecl → FINAL ConstDeclTyp ConstAssign ( COMMA ConstAssign ConstDeclTyp → INT ConstAssign → IDENT ASSIGN ( Identifier | NUMBER ) )∗ ENDSTMNT VarDecl → VarDeclTyp VarAssign ( COMMA VarAssign )∗ ENDSTMNT VarDeclTyp → INT | {enableOO}?=> CLASSIDENT VarAssign → IDENT ( ASSIGN ( IDENT | NUMBER | {enableOO}?=> ConstructorCall ) )? LocalVarDecl → VarDecl LocalConstDecl → ConstDecl Procedure → VOID IDENT RLB ParamDecl ? RRB ProcBlock ProcBlock → {!declEverywhere}?=> CLB LocalConstDecl ? LocalVarDecl ? Statement | {declEverywhere}?=> Block CRB Function → FuncStart IDENT RLB ParamDecl ? RRB FuncBlock FuncStart → FUNC | {enableExtensions}?=> FUNC? ( INT | {enableOO}?=> CLASSIDENT ) FuncBlock → {!declEverywhere}?=> CLB LocalConstDecl ? LocalVarDecl ? Statement RETURN Expr ENDSTMNT CRB | {declEverywhere}?=> CLB BlockElement∗ RETURN Expr ENDSTMNT CRB ParamDecl → SingleParamDecl ( COMMA SingleParamDecl SingleParamDecl → SingleParamDeclTyp IDENT SingleParamDeclTyp → INT | {enableOO}?=> CLASSIDENT Block → CLB BlockElement∗ CRB BlockElement → Statement | {declEverywhere}?=> LocalConstDecl 114 )∗ B.2 Parser | {declEverywhere}?=> LocalVarDecl Expr → SumExpr SumExpr → MultExpr ( ( ADD | SUB ) MultExpr )∗ MultExpr → AtomExpr ( MultOp AtomExpr )∗ MultOp → MULT | DIV | {enableExtensions}?=> MOD AtomExpr → Identifier ( FunProcCall | {enableExtensions}?=> PostfixIncDec | {enableExtensions}?=> PraefixIncDec Identifier | NUMBER | RLB Expr RRB )? PostfixIncDec → INC | DEC PraefixIncDec → INC | DEC Identifier → {!enableOO}?=> IDENT | {enableOO}?=> ( IDENT | THIS ) ( IdentifierPart+ )? IdentifierPart → PO IDENT Statement → IdentifierStatement ENDSTMNT | enableExtensions?=> PraefixIncDec Identifier ENDSTMNT | Print ENDSTMNT | Block | IfBlock | WhileBlock IdentifierStatement → Identifier ( Assignment | FunProcCall | PostfixIncDec ) Assignment → ASSIGN ( Expr | {enableOO}?=> ConstructorCall ) FunProcCall → RLB ( Expr ( COMMA Expr )∗ )? RRB Print → PRINT RLB Expr RRB IfBlock → IF Condition Statement ( ELSE Statement )? WhileBlock → WHILE Condition Statement Condition → Expr CompOp Expr CompOp → NE | EQ | LE | GE | LT | GT ClassDef → CLASS CLASSIDENT EXTENDS CLB ( PUBLIC FieldDecl )∗ ( PROTECTED FieldDecl )∗ ( PRIVATE FieldDecl )∗ PUBLIC? Constructor ( PUBLIC Method )∗ ( PROTECTED Method )∗ CLASSIDENT 115 B Grammatiken ( PRIVATE Method )∗ CRB FieldDecl → VarDecl | ConstDecl Method → Function | Procedure Constructor → CLASSIDENT RLB RRB Block ConstructorCall → NEW CLASSIDENT RLB RRB 116 B.3 AST-Parser B.3 AST-Parser Program → ClassDef + FieldDecl → ^( VAR ^( MODIFIER FINAL? AccessModifier ) DeclTyp SIMPLENAME VarDeclValue? ) VarDecl → ^( VAR ^( MODIFIER FINAL? ) DeclTyp SIMPLENAME VarDeclValue? ) DeclTyp → INT | SIMPLENAME VarDeclValue → Name | NUMBER | ConstructorCall AccessModifier → PUBLIC | PROTECTED | PRIVATE Method → ^( METHOD ^( MODIFIER AccessModifier ) MethodTyp SIMPLENAME ^( PARAMS ParamDecl ∗ Block ) MethodTyp → INT | SIMPLENAME | VOID ParamDecl → ^( PARAM ParamDeclTyp SIMPLENAME ) ParamDeclTyp → INT | SIMPLENAME Block → ^( BLOCK blockElement∗ ) BlockElement → Statement | VarDecl Expr → | | | | | | | | | ^(ADD Expr ^(SUB Expr ^(MULT Expr ^(DIV Expr ^(MOD Expr NUMBER Name Invocation PostfixIncDec PraefixIncDec Expr Expr Expr Expr Expr ) ) ) ) ) PostfixIncDec → ^( POSTFIX IncDec Name ) PraefixIncDec → ^( PRAEFIX IncDec Name ) IncDec → INC | DEC Name → SIMPLENAME | THIS | ^( QUALIFIEDNAME THIS? SIMPLENAME+ ) Statement → Assignment 117 B Grammatiken | | | | | | | | Invocation PostfixIncDec PraefixIncDec Print MethodReturn Block IfBlock WhileBlock Assignment → ^( ASSIGN Name Expr ? ConstructorCall ? ) Invocation → ^( INVOKE Name ^( ARGLIST Expr ∗ ) ) Print → ^( PRINT Expr ) MethodReturn → ^( RETURN Expr ) IfBlock → ^( IF Condition Statement Statement? ) WhileBlock → ^( WHILE Condition Statement ) Condition → ^( CompOp Expr Expr ) CompOp → NE | EQ | LE | GE | LT | GT ClassDef → ^( CLASS SIMPLENAME ^(EXTENDS SIMPLENAME) Constructor ^( FIELDS FieldDecl ∗ ) ^(METHODS Method ∗ ) ) Constructor → ^( CONSTRUCTOR block ) ConstructorCall → ^( NEW SIMPLENAME) 118 C Übersetzungsbeispiele C.1 Binärbaum-Programm Nachstehend werden die Übersetzungen des Binärbaum-Programms aus Listing 1.1 in abstrakte Syntax sowie JVM-Assemblercode angegeben. Abstrakte Syntax CLASS FunProcMainClass EXTENDS FunProcMainClass Object BLOCK FIELDS METHODS VAR MODIFIER Baum METHOD baum MODIFIER public void ... main ... PARAMS public baum BLOCK = INVOKE INVOKE printBaum erzeugeBaum ARGLIST ARGLIST baum ... METHOD MODIFIER public MODIFIER int Baum VAR i erzeugeBaum PARAMS VAR 0 MODIFIER Baum b VAR new MODIFIER Baum QUALIFIEDNAME aktuell BLOCK i ARGLIST setValue Baum 4 i aktuell aktuell INVOKE QUALIFIEDNAME r while b < BLOCK = new INVOKE = Nil aktuell QUALIFIEDNAME = QUALIFIEDNAME POSTFIX new l = aktuell Baum aktuell ARGLIST setValue QUALIFIEDNAME aktuell = QUALIFIEDNAME i ++ aktuell new l Nil QUALIFIEDNAME aktuell i l ... METHOD MODIFIER void printBaum public PARAMS PARAM Baum b BLOCK INVOKE QUALIFIEDNAME b inorder INVOKE ARGLIST b QUALIFIEDNAME ARGLIST r inorder r r return Abbildung C.1: Binärbaum-Programm: Globale Sprachelemente im AST 119 r new Nil b C Übersetzungsbeispiele class Baum extends Baum Object FIELDS BLOCK MODIFIER VAR Baum METHODS VAR r l VAR MODIFIER METHOD MODIFIER public int v MODIFIER protected void ... setValue public PARAMS BLOCK PARAM if int v <= v BLOCK 10 if >= v BLOCK 0 = QUALIFIEDNAME this v ... METHOD MODIFIER void inorder public QUALIFIEDNAME this l PARAMS BLOCK INVOKE print INVOKE ARGLIST QUALIFIEDNAME QUALIFIEDNAME inorder this v this r ARGLIST inorder Abbildung C.2: Binärbaum-Programm: Klasse Baum im AST class Nil extends Nil Baum this v METHODS BLOCK = QUALIFIEDNAME FIELDS 10 METHOD MODIFIER public void setValue METHOD PARAMS PARAM int BLOCK MODIFIER void inorder PARAMS public v Abbildung C.3: Binärbaum-Programm: Klasse Nil im AST 120 BLOCK v C.1 Binärbaum-Programm JVM-Assemblercode Listing C.1: (Binärbaum-Programm: Globale Sprachelemente in Assemblercode) 1 2 3 .class public FunProcMainClass .super java/lang/Object 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 .field public baum LBaum; .method public main()V .limit stack 2 .limit locals 1 aload_0 aload_0 invokevirtual FunProcMainClass/erzeugeBaum()LBaum; putfield FunProcMainClass/baum LBaum; aload_0 aload_0 getfield FunProcMainClass/baum LBaum; invokevirtual FunProcMainClass/printBaum(LBaum;)V return .end method 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 .method public erzeugeBaum()LBaum; .limit stack 3 .limit locals 4 ldc 0 istore 1 new Baum dup invokespecial Baum/<init>()V astore 2 aload 2 astore 3 w0: iload 1 ldc 4 if_icmpge c0 aload 3 iload 1 invokevirtual Baum/setValue(I)V aload 3 new Nil dup invokespecial Nil/<init>()V putfield Baum/r LBaum; aload 3 121 C Übersetzungsbeispiele 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 new Baum dup invokespecial Baum/<init>()V putfield Baum/l LBaum; aload 3 getfield Baum/l LBaum; astore 3 iload 1 ldc 1 iadd istore 1 goto w0 c0: aload 3 iload 1 invokevirtual Baum/setValue(I)V aload 3 new Nil dup invokespecial Nil/<init>()V putfield Baum/l LBaum; aload 3 new Nil dup invokespecial Nil/<init>()V putfield Baum/r LBaum; aload 2 areturn .end method 73 74 75 76 77 78 79 80 81 82 83 84 85 .method public printBaum(LBaum;)V .limit stack 1 .limit locals 2 aload 1 invokevirtual Baum/inorder()V aload 1 getfield Baum/l LBaum; getfield Baum/l LBaum; getfield Baum/l LBaum; invokevirtual Baum/inorder()V return .end method 86 87 88 89 90 .method public <init>()V .limit stack 1 .limit locals 1 aload_0 122 C.1 Binärbaum-Programm 91 92 93 invokespecial java/lang/Object/<init>()V return .end method 94 95 96 97 98 99 100 101 102 103 .method static public main([Ljava/lang/String;)V .limit stack 2 .limit locals 1 new FunProcMainClass dup invokespecial FunProcMainClass/<init>()V invokevirtual FunProcMainClass/main()V return .end method Listing C.2: (Binärbaum-Programm: Klasse Baum in Assemblercode) 1 2 .class public Baum .super java/lang/Object 3 4 5 6 .field public r LBaum; .field public l LBaum; .field protected v I 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 .method public setValue(I)V .limit stack 2 .limit locals 2 iload 1 ldc 10 if_icmpgt c0 iload 1 ldc 0 if_icmplt c1 aload_0 iload 1 putfield Baum/v I goto e1 c1: e1: goto e0 c0: e0: return .end method 28 29 30 31 .method public inorder()V .limit stack 2 .limit locals 1 123 C Übersetzungsbeispiele 32 33 34 35 36 37 38 39 40 41 42 43 aload_0 getfield Baum/l LBaum; invokevirtual Baum/inorder()V getstatic java/lang/System/out Ljava/io/PrintStream; aload_0 getfield Baum/v I invokevirtual java/io/PrintStream/println(I)V aload_0 getfield Baum/r LBaum; invokevirtual Baum/inorder()V return .end method 44 45 46 47 48 49 50 51 .method public <init>()V .limit stack 1 .limit locals 1 aload_0 invokespecial java/lang/Object/<init>()V return .end method Listing C.3: (Binärbaum-Programm: Klasse Nil in Assemblercode) 1 2 3 .class public Nil .super Baum 4 5 6 7 8 9 .method public setValue(I)V .limit stack 0 .limit locals 2 return .end method 10 11 12 13 14 15 .method public inorder()V .limit stack 0 .limit locals 1 return .end method 16 17 18 19 20 21 22 23 24 .method public <init>()V .limit stack 2 .limit locals 1 aload_0 invokespecial Baum/<init>()V aload_0 ldc 10 putfield Baum/v I 124 C.1 Binärbaum-Programm 25 26 return .end method 125 C Übersetzungsbeispiele C.2 Ackermann-Programm Im folgenden werden die Übersetzungen des Ackermann-Programms aus Listing 2.1 in abstrakte Syntax sowie JVM-Assemblercode angegeben. Abstrakte Syntax CLASS FunProcMainClass EXTENDS FunProcMainClass Object BLOCK MODIFIER final int FIELDS METHODS VAR VAR 5 MODIFIER b public METHOD int i MODIFIER public void ... ... main PARAMS BLOCK public BLOCK = i while INVOKE 0 < BLOCK + b print i 2 berechneAusdruck = INVOKE ackermann i ARGLIST i i + i 2 ... METHOD MODIFIER void berechneAusdruck public PARAMS VAR MODIFIER final int x BLOCK VAR 9 MODIFIER int BLOCK ergebnis = ergebnis + * - x / x print print ergebnis 5 + 3 56 9 + + + 1 + * 4 5 3 2 Abbildung C.4: Ackermann-Programm im AST 1 126 + 6 1 ARGLIST C.2 Ackermann-Programm ... METHOD MODIFIER int ackermann public PARAMS PARAM int a BLOCK PARAM int b VAR MODIFIER int ret if 0 == b return BLOCK 0 = ret 1 if ret == a BLOCK 0 == b 1 ret BLOCK if = = = ret INVOKE 2 ret + ackermann b 2 a ARGLIST - INVOKE 1 ackermann ARGLIST a - b Abbildung C.5: Ackermann-Programm im AST 2 JVM-Assemblercode Listing C.4: (Ackermann-Programm in Assemblercode) 1 2 3 .class public FunProcMainClass .super java/lang/Object 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 .field final public b I .field public i I .method public main()V .limit stack 5 .limit locals 1 aload_0 ldc 0 putfield FunProcMainClass/i I w0: aload_0 getfield FunProcMainClass/i I ldc 2 iadd aload_0 getfield FunProcMainClass/b I if_icmpge c0 getstatic java/lang/System/out Ljava/io/PrintStream; 127 1 C Übersetzungsbeispiele 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 aload_0 aload_0 getfield FunProcMainClass/i I aload_0 getfield FunProcMainClass/i I ldc 2 iadd invokevirtual FunProcMainClass/ackermann(II)I invokevirtual java/io/PrintStream/println(I)V aload_0 aload_0 getfield FunProcMainClass/i I ldc 1 iadd putfield FunProcMainClass/i I goto w0 c0: aload_0 invokevirtual FunProcMainClass/berechneAusdruck()V return .end method 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 .method public berechneAusdruck()V .limit stack 4 .limit locals 3 ldc 9 istore 1 iconst_0 istore 2 iload 1 iload 1 imul ldc 56 ldc 9 iadd ldc 3 idiv ldc 5 isub iadd istore 2 getstatic java/lang/System/out Ljava/io/PrintStream; iload 2 invokevirtual java/io/PrintStream/println(I)V getstatic java/lang/System/out Ljava/io/PrintStream; ldc 1 ldc 2 128 C.2 Ackermann-Programm 69 70 71 72 73 74 75 76 77 78 79 80 iadd ldc 3 iadd ldc 4 iadd ldc 5 ldc 6 imul iadd invokevirtual java/io/PrintStream/println(I)V return .end method 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 .method public ackermann(II)I .limit stack 6 .limit locals 4 ldc 0 istore 3 iload 2 ldc 0 if_icmpne c0 ldc 1 istore 3 goto e0 c0: iload 1 ldc 0 if_icmpne c1 iload 2 ldc 1 if_icmpne c2 ldc 2 istore 3 goto e2 c2: iload 2 ldc 2 iadd istore 3 e2: goto e1 c1: aload_0 iload 1 ldc 1 isub aload_0 129 C Übersetzungsbeispiele 116 117 118 119 120 121 122 123 124 125 126 127 iload 1 iload 2 ldc 1 isub invokevirtual FunProcMainClass/ackermann(II)I invokevirtual FunProcMainClass/ackermann(II)I istore 3 e1: e0: iload 3 ireturn .end method 128 129 130 131 132 133 134 135 136 137 138 .method public <init>()V .limit stack 2 .limit locals 1 aload_0 invokespecial java/lang/Object/<init>()V aload_0 ldc 5 putfield FunProcMainClass/b I return .end method 139 140 141 142 143 144 145 146 147 148 .method static public main([Ljava/lang/String;)V .limit stack 2 .limit locals 1 new FunProcMainClass dup invokespecial FunProcMainClass/<init>()V invokevirtual FunProcMainClass/main()V return .end method 130