JACK

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