JACK Ein objektorientierter Compilergenerator auf Java-Basis für die Lehre Kirsten Berkenkötter, Roland Rössler Fachhochschule Bielefeld Fachbereich Elektrotechnik und Informationstechnik Einleitung: Compilergeneratoren oder Compiler-Compiler sind Softwarewerkzeuge, die es erlauben, über die formale Definition einer (Programmier-)Sprache automatisch einen dazugehörigen Compiler (oder zumindest Teile davon) zu erzeugen. Heutzutage greifen Compiler-Entwickler immer mehr zu solchen Tools, da diese einerseits den Entwicklungsprozess erheblich verkürzen und andererseits mittlerweile so ausgereift sind, dass bei den Endprodukten kaum mehr mit Effizienzeinbußen gegenüber einer Erstellung „von Hand“ zu rechnen ist. Die bekanntesten und am weitesten verbreiteten Werkzeuge sind wohl LEX und YACC (bzw. ihre „Klone“ FLEX und BISON unter der GNU public license). Darüber hinaus gibt es jedoch eine Vielzahl weiterer Produkte mit vergleichbarem Funktionsumfang für unterschiedliche Plattformen, wie z.B. ANTLR, Coco, CUP, LEMON, Llgen, WRG, JavaCC. Das Entwicklungssystem ELI sei hier auch noch erwähnt, das allerdings wesentlich universeller und leistungsfähiger als die zuvor genannten Produkte ist (siehe Abschnitt 5 – InternetQuellen). Die Entwicklung eines eigenen Compilergenerators („JACK“) im Labor für Technische Informatik an der FH Bielefeld orientiert sich hauptsächlich an den folgenden Kriterien: . Einfache Struktur und Handhabung für den praktischen Einsatz im Fach „Compilertechnik“ . Einheitlichkeit von Scanner- und Parser-Mechanismen . absolute Plattform-Unabhängigkeit (pure JAVA) . Objektorientiertheit Dagegen spielt die Effizienz (Schnelligkeit) des Generators und der erzeugten Compiler nur eine untergeordnete Rolle. Aufgebaut werden konnte auf einschlägigen positiven Erfahrungen mit einem ähnlichen Compiler-Generator („SPARGEL“), der unter Borland-Pascal entwickelt wurde und bereits zu einem früheren Zeitpunkt im Rahmen dieser Veranstaltungsreihe vorgestellt wurde. Der Name JACK steht für „JAVA Compiler Kit“. 1 Beschreibung der Grammatik einer höheren Sprache 1.1 Reguläre Ausdrücke und BNF – LEX und YACC Im Unix-Bereich – und insbesondere bei Verwendung von LEX und YACC – hat sich für die Beschreibung von höheren Sprachen ein zweigeteilter Weg durchgesetzt: „Reguläre Ausdrücke“ für die Definition von Token und „BNF“ für die Grammatikregeln. Dies führt zu einer weitgehenden logischen Trennung von Scanner und Parser. Als Beispiel soll ein einfacher „Tischrechner“ dienen, der arithmetische Ausdrücke analysieren kann. Die Aufgabe des Scanners kann mit regulären Ausdrücken etwa wie folgt formuliert werden: [-+*/()=] return yytext[0]; // Erkennung von // Einzelzeichen [0-9]+(\.[0-9]+)? { yylval=atof(yytext); return ZAHL; // Zahl erkennen } " " ; // Blank ignorieren Der dazugehörige Parser könnte – als Folge von BNF-Regeln – so aussehen: anfrage: ausdruck '=' ; ausdruck: ausdruck operator operand | operand ; operand: ZAHL | '(' ausdruck ')' ; operator: '+' | '-' | '*' | '/' ; Vorausgesetzt ist hierbei, dass die Analyse „bottom up“ erfolgt, da sonst die Linksrekursion in der Regel für ausdruck Schwierigkeiten macht (YACC und BISON arbeiten als LR(1)Parser mit Bottom-up-Analyse). 1.2 Erweiterte BNF (EBNF) - JACK Bei JACK wird statt BNF für die Beschreibung von Parser und Scanner eine erweiterte BNF eingesetzt, da diese nach Auffassung der Autoren für Anfänger besser verständlich ist. Als Konsequenz kann auf Linksrekursionen verzichtet werden, wodurch eine Topdown-Analyse möglich wird, die ebenfalls etwas leichter zu vermitteln ist. Das obige Beispiel würde mit JACK etwa wie folgt formuliert: scanner token ziffer : ziffer... ['.' ziffer...] ZAHL | ' ' _IGNORE ; : '0'-'9' ; parser 2 anfrage : ausdruck '=' ; ausdruck : operand [operator operand]... ; operand : ZAHL | '(' ausdruck ')' ; operator : '+' | '-' | '*' | '/' ; Die für den Scanner verwendete EBNF unterscheidet sich nur unwesentlich von der im Parser-Abschnitt benutzten Variante: Groß geschriebene Bezeichner legen in den ScannerRegeln die Token-Verschlüsselungen für „variable“ Token fest (wie im Beispiel: ZAHL, oder auch: NAME, usw.). _IGNORE definiert die entsprechende Symbolsequenz als zu ignorierendes Token. Zeichenmengen (wie '0'-'9') sind nur im Scanner erlaubt. Einzelzeichen erkennt der Scanner automatisch – die Formulierung entsprechender Regeln kann also unterbleiben. 1.3 Syntaxgraphen Noch anschaulicher als eine erweiterte BNF sind die in vielen Lehrbüchern verwendeten Syntaxgraphen. Bei diesen wird vielfach überhaupt nicht zwischen Scanner- und ParserMechanismen unterschieden: anfrage: ausdruck = ausdruck: ziffer: operand 0 operator 1 2 operand: zahl ( ausdruck operator: ) 9 zahl: ziffer + − . * ziffer / 3 JACK wird - im Endausbau – wahlweise die Formulierung von Grammatikregeln über EBNF oder Syntaxgraphen erlauben, wobei eine automatische Konvertierung beider Formen in die jeweils andere geplant ist. 2 Einbettung von semantischer Analyse und/oder Codeerzeugung 2.1 Behandlung von Attributen bei YACC bzw. BISON Bei den meisten Compilergeneratoren werden semantische Aktionen über „normale“ Anweisungen einer „Wirtssprache“ (meistens C) formuliert. Zum Teil werden diese Anweisungen unmittelbar in die Regeln eingefügt – wie dies z.B. weiter oben bei den regulären Ausdrücken bereits geschehen ist. Bei der BNF oder EBNF werden die Symbole einer Regel als „attributbehaftete“ Objekte interpretiert, wobei dann in den Anweisungen auf diese Attribute Bezug genommen wird. Das Beispiel in Abschnitt 1.1 könnte wie folgt erweitert werden, um den erzeugten „Compiler“ in die Lage zu versetzen, die analysierten Ausdrücke zu berechnen und auszugeben: anfrage: ausdruck '=' { printf("%g\n",$1); } ; ausdruck: ausdruck operator operand | operand ; { $$ = calc($1,$2,$3); } { $$ = $1; } operand: ZAHL | '(' ausdruck ')' ; operator: { $$ = $2; } '+' | '-' | '*' | '/' ; Der Compilergenerator YACC erlaubt den Zugriff auf die Attribute der Symbole eines Regelkörpers über die Notation „$n“, wobei n die Position des zugehörigen Symbols innerhalb der jeweiligen Regel(-Alternative) bezeichnet. Die Notation „$$“ identifiziert hingegen das (Ergebnis-)Attribut der Regel (d.h. das Attribut des jeweiligen nichtterminalen Symbols). Im obigen Beispiel wird also in der zweiten Zeile eine – noch zu formulierende CFunktion calc aufgerufen. Die Parameter des Aufrufs sind die Attribute von ausdruck, operator und operand der ersten Regelalternative. Wenn man voraussetzt, dass die Attribute von ausdruck und operand deren konkrete Zahlenwerte sind und das Attribut von operator das jeweilige Operationszeichen, dann ist klar, dass das Ergebnisattribut gleich dem berechneten (Teil-)Ausdruck sein muss. Die Formulierung der Funktion calc ist trivial und soll hier unterbleiben. Die Symbolattribute können bei YACC von jedem beliebigen Datentyp sein. Das bedeutet große Flexibilität (und Effizienz), jedoch leider auch große Fehleranfälligkeit, was noch dadurch verschärft wird, dass beim Ändern von Regeln auch immer an die weiterhin korrekte Nummerierung der Attribute geachtet werden muss. 4 2.2 Behandlung von Attributen bei JACK Bei JACK wird der Zugriff auf Attribute von Symbolen strengen Prüfungen unterworfen: Zuallererst müssen alle Attribute Instanzen von „echten“ JAVA-Klassen sein. In einer sog. „Attributsektion“ können die entsprechenden Zuordnungen vorgenommen werden. Darüber hinaus müssen die Symbolattribute explizit benannt werden, wenn sie in eingeschobenen JAVA-Anweisungen verwendet werden sollen. Dies soll wieder an einer geeigneten Erweiterung des Beispiels aus Abschnitt 1.2 demonstriert werden: attributes Double: ZAHL ausdruck operand ; String: operator ; scanner token ziffer : ziffer... ['.' ziffer...] ZAHL | ' ' _IGNORE ; : '0'-'9' ; parser anfrage : ausdruck<result> '=' # System.out.println(result); # ; ausdruck<result> : operand<result> [operator<optr> operand<opnd> ]... ; operand<result> operator # result=calc(result,optr,opnd); # : ZAHL<result> | '(' ausdruck<result> ')' ; : '+' | '-' | '*' | '/' ; Die in spitze Klammern eingeschlossenen Attribute stellen „normale“ Bezeichner für JAVAObjekte dar. Tritt ein Attributname sowohl auf der linken als auch auf der rechten Seite einer Regel auf, so erfolgt eine automatische Wertzuweisung des „rechten“ Attributes an das „linke“ (siehe Regel für operand). „Konstante“ Token – also Token wie 'if', '>=', '+', usw. erhalten automatisch ein Attribut vom Typ String, das identisch mit der Repräsentation des Tokens ist. Für numerische Token (also Token mit einem Attribut vom Typ Integer, Float, Double, ...) wird der jeweilige Attributwert automatisch vom Scanner berechnet – in unserem Beispiel der Attributwert von ZAHL. Dabei ist natürlich vorausgesetzt, dass die zugehörige Regelalternative der korrekten Schreibweise der betreffenden Zahl entspricht. 5 3 Handhabung von JACK JACK kann als eigenständiges Programm zum Ablauf gebracht werden, oder als „Plugin“ des komfortablen Editors „jEdit“ (siehe Abschnitt 5 Internet-Quellen). In beiden Fällen erfolgt die Bedienung über eine grafische Oberfläche. 3.1 JACK als eigenständiges Programm JACK ist mit einer eigenen grafischen Oberfläche ausgestattet: Die Oberfläche besteht aus einem einfache Editor und einer Werkzeugleiste mit diversen Buttons. Die wichtigste Schaltfläche ist der Rechtspfeil ( ), mit der die Generierung des Compilers gestartet wird. Der generierte Compiler kann schließlich mit einer identischen Oberfläche aktiviert werden, wobei als Quelle nun natürlich ein „Programm“ in der jeweiligen Sprache vorliegen muss: 6 3.2 Testhilfen Zum Testen der generierten Compiler gibt es zwei Möglichkeiten: . Das schrittweise Verfolgen der Token-Erkennung . Die Generierung eines Ableitungsbaumes Beide Möglichkeiten können auch kombiniert werden, was das folgende Bild zeigt: Es ist die Situation festgehalten, in der gerade das vorletzte Token (15.1) erkannt wurde. Sowohl im Ableitungsbaum als auch in der Statuszeile, die das jeweils aktuelle Token anzeigt, werden die Symbolattribute dargestellt. 3.3 JACK als Plugin für jEdit jEdit ist ein in JAVA geschriebener komfortabler Texteditor, dessen Leistungsfähigkeit nicht zuletzt durch eine Vielzahl von Plugins für unterschiedlichste Anwendungsbereiche bestimmt ist. Er ermöglicht unter anderem „Syntax Highlighting“, Editieren mit „Folds“, usw. Mit der Einbettung von JACK in diese Entwicklungsumgebung wird ein wesentlich höherer Bedienungskomfort erreicht als mit der „Standalone“-Version. Das folgende Bild zeigt die gleiche Situation wie in Abschnitt 3.2. 7 3.4 Ergebnis der Generierung Wenn JACK aus einer Regeldatei „nnnn.syn“ einen Compiler generiert, so ist das Ergebnis eine JAVA-Klasse mit dem Namen „nnnnParser“. Diese kann auf unterschiedliche Weise benutzt werden: . als Konsolen-Anwendung – unter der Angabe der zu übersetzenden Quelldatei als Parameter des Kommandos . als bedienbarer Compiler unter der grafischen Oberfläche von JACK (siehe Abschnitt 3.1). Übersetzt werden damit in der Regel Dateien mit der Endung „nnnn“. . als Plugin von „jEdit“ – der Parser wird automatisch aufgerufen, wenn der Menüpunkt „Generate/Compile with JACK“ ausgewählt wird und das aktuelle Textfenster eine Datei mit der Endung „nnnn“ enthält. . als Objekt in einer beliebigen anderen JAVA-Klasse. Dieses Szenario ist für die Analyse beliebiger Texte innerhalb von JAVA-Programmen nützlich. 8 4 Resümee und Ausblick Hinter der Entwicklung von JACK stand die Absicht, den Studierenden des Faches „Compilertechnik“ einen schnellen Einstieg in die Struktur und die Verfahren von „Compilergeneratoren“ zu ermöglichen. Ziel war ausdrücklich nicht die Implementierung eines leistungsfähigen Werkzeuges für den Praktiker, obwohl im Ergebnis ein Produkt entstanden ist, das in einer JAVA-Umgebung durchaus für die Lösung von Analyse- und Übersetzungsaufgaben im Bereich Textverarbeitung geeignet ist. JACK hat sich als Hilfsmittel für die Ausbildung bereits bewährt: In einem Blockkurs von 10 mal 5 Stunden (2 Wochen) wurde als Aufgabe die Implementierung eines Compilers für eine einfache Hochsprache vorgegeben (einfache ganzzahlige Konstanten und Variablen, Ausdrücke, Zuweisungen, Verzweigungen, Schleifen, Ein-/Ausgabe). Zielsprache war Assemblercode für den 80386. Alle Kursteilnehmer konnten die Aufgabe innerhalb der vorgesehenen Zeitspanne lösen. In früheren Fällen – unter Verwendung von LEX und YACC – war die Erfolgsquote deutlich geringer. Die folgenden Erweiterungen sind geplant bzw. bereits in Arbeit: . Alternative Darstellung der Grammatik mit Syntaxgraphen . Einbau von Semantikregeln zur Vereinfachung der semantischen Analyse . Einbau von Codeerzeugungs-Regeln 5 Internet-Quellen Compiler-Generatoren: ANTLR http://www.antlr.org/ Coco http://www.ssw.uni-linz.ac.at/Research/Projects/Compiler.html CUP http://www.cs.princeton.edu/~appel/modern/java/CUP/ ELI http://www.unipaderborn.de/fachbereich/AG/agkastens/eli/base/homeD.html FLEX/BISON z.B. http://www.tu-darmstadt.de/ftp/gnu/ JACK http://ti.fh-bielefeld.de/ti/vorlesung/cpt/begleitmaterial/ LEMON http://www.hwaci.com/sw/lemon/ Llgen http://www.cs.vu.nl/~ceriel/LLgen.html WRG http://www.informatik.unistuttgart.de/Dienst/UI/2.0/Describe/ncstrl.ustuttgart_fi/TR-1990-01 JavaCC http://www.suntest.com/JavaCC/ Editor jEdit: http://sourceforge.net/projects/jedit/ 9