Vous êtes sur la page 1sur 14

JavaCC: Eine Einführung

in den Parser Generator

Marco Maniscalco
Hochschule für Technik Stuttgart

20. Februar 2007


Inhaltsverzeichnis

1 Einleitung ............................................................................... 3

2 Lexikalische Analyse ................................................................ 4

2.1 Token Typ .................................................................................... 5


2.2 Leerzeichen .................................................................................. 5
2.3 Reguläre Ausdrücke ..................................................................... 6
2.4 Token Strom ................................................................................ 7

3 Syntaktische Analyse............................................................... 8

3.1 EBNF ............................................................................................ 8


3.2 Eigenschaften der Funktionen....................................................... 9
3.3 Rekursiver Abstieg........................................................................ 9

4 Semantische Aktionen .......................................................... 11

5 Fazit ..................................................................................... 13
1 Einleitung

Übersetzer und Interpreter trifft man heutzutage an vielen Stellen der Informatik an.
Im Gegensatz zu den frühen siebziger Jahren, als Übersetzer fast ausschließlich zum
Übersetzen von Programmiersprachen verwendet wurden, wird heute eine große
Anzahl von Problemen mit moderner Übersetzer Technologie angesprochen. Das
Erkennen einer Datenstruktur und Transformieren, beziehungsweise Übersetzen, in
eine andere Form steht hierbei im Fordergrund. Anwendungen sind hierfür Hoch-
sprachen, Skriptsprachen, Datenstrukturen mit flexiblem Format und jegliche Daten,
die mit Hilfe von grammatikalischen Regeln beschrieben werden können.
Längst entwickelt man die einzelnen Bestandteile eines Übersetzers oder Interpreters
nicht mehr von Hand. Vielmehr kommen hier leistungsfähige Werkzeuge zum Ein-
satz, die wiederkehrende oder modellierbare Tätigkeiten automatisieren können und
somit wertvolle Arbeitszeit sparen. JavaCC1 (Java Compiler Compiler) ist ein solches
Werkzeug, das die wichtigsten Komponenten eines Übersetzers automatisch erzeu-
gen kann. Hierzu gehören zunächst die lexikalische Analyseeinheit und der Parser.
Beide Komponenten werden benötigt um eine Struktur innerhalb von beliebigen
Daten zu erkennen. Die lexikalische Analyseeinheit erkennt lexikalische Fragmente,
so genannte Tokens, dies sind Textfragmente denen bestimmte Eigenschaften zu-
geordnet werden. Der Parser erkennt anhand dieser Tokens syntaktische Strukturen
indem er grammatikalische Regeln überprüft. Des Weiteren ist er in der Lage Syntax-
fehler zu erkennen, beziehungsweise zu lokalisieren.
JavaCC bietet die Möglichkeit, die eben genannten Komponenten automatisch zu
erzeugen, so dass bei der Lösung eines Problems der Fokus auf dem eigentlichen
Problem liegen kann. Als Eingabe fordert JavaCC hierbei eine lexikalische Spezifika-
tion der zu erkennenden Eingabedaten und eine syntaktische Spezifikation der Ein-
gabesprache. Des Weiteren können semantische Aktionen definiert werden, die
entweder während, oder nach dem Erkennen der Baumstruktur ausgeführt werden.
Der folgende Artikel befasst sich sowohl mit der lexikalischen und syntaktischen
Spezifikationen, als auch mit den semantischen Aktionen und deren Verwendung.
Die Grundlagen und Eigenschaften von JavaCC werden vor dem Hintergrund prakti-
scher Beispiele erläutert und ein Überblick über die wichtigsten Merkmale gegeben.

1
https://javacc.dev.java.net
2 Lexikalische Analyse

options {
JDK_VERSION = "1.5";
}
PARSER_BEGIN(EinParser)

public class EinParser {


}

PARSER_END(EinParser)

SKIP :
{
" "|"\r"|"\t"|"\n"
}
TOKEN : /* OPERATORS */
{
< PLUS: "+" >
| < MINUS: "-" >
| < MULTIPLY: "*" >
| < DIVIDE: "/" >
}
TOKEN :
{
< CONSTANT: ( <DIGIT> )+ >
| < #DIGIT: ["0" - "9"] >
}
/* Ab hier: Grammatikregeln */

Abbildung 1 (Teil einer JavaCC Datei)

JavaCC benötigt als Eingabe eine Spezifikation der lexikalischen Merkmale des zu
erkennenden Quelltextes. Die Spezifikation wird in Form einer Datei erstellt. Hier
sind die Tokens definiert, als auch grammatikalische Regeln zum Erkennen der Ein-
gabesprache. Im folgenden Beispiel erkennt man die Tokens PLUS, MINUS, MUL-
TIPLY, DIVIDE, CONSTANT und DIGIT sowie die Syntax, mit der diese angegeben
werden müssen.
Die lexikalische Analyse erkennt in einem Eingabestrom, der meist aus beliebigen
Bytes besteht, die Tokens. Diese Tokens stellen unter anderem das Vokabular einer
Sprache bereit. Wie bereits bekannt ist, müssen diesen Tokens gewisse Eigenschaf-
ten zugeordnet werden. Eine dieser Eigenschaften ist ihr Typ, in JavaCC ist dies eine
Konstante mit einem symbolischen Namen. Des Weiteren ist das Abbild (Image)
eines solchen Tokens wichtig, das im Wesentlichen den ursprünglichen Klartext des
Tokens enthält. Ein Token vom Typ PLUS würde das Abbild „+“ enthalten. Zusam-
menfassend kann ein Token in Java Quelltext also wie folgt beschrieben werden.
/**
* Describes a token.
*/
public class Token {

public int typ;


public String image;

Abbildung 2 (Von JavaCC Erzeugt)

2.1 Token Typ

Wie aus dieser Klassendefinition hervorgeht, handelt es sich bei dem Typ um eine
Ganzzahl. Diesen Indizes werden symbolische Namen zugeordnet, um sie später
wieder zu identifizieren. JavaCC erzeugt diese Konstanten automatisch. Es muss, wie
in Abbildung 1 ersichtlich ist, lediglich das Token in der lexikalischen Spezifikation
definiert werden. Die Benutzung erfolgt innerhalb der Spezifikation stets mit dem
symbolischen Namen. Im Gegensatz dazu arbeitet die lexikalische Analyse intern mit
diesen Ganzzahl Typen, was die Bearbeitungsgeschwindigkeit natürlich deutlich
erhöht, da Ganzzahloperationen weniger Rechenzyklen als komplexe Zeichenketten
benötigen.

public interface eg1Constants {

int EOF = 0;
int PLUS = 5;
int MINUS = 6;
int MULTIPLY = 7;
int DIVIDE = 8;
int CONSTANT = 9;
int DIGIT = 10;
}

Abbildung 3 (Von JavaCC erzeugt)

2.2 Leerzeichen

Neben den Tokens existieren die Leerzeichen, ihnen wird jedoch keine größere Be-
deutung beigemessen und beim Erkennungsvorgang schlicht überlesen. In Abbil-
dung 1 sind diese mit SKIP markiert. Sie sind nur vom praktischen Gesichtspunkt aus
betrachtet relevant, da sie als Abgrenzungen oder Strukturelemente fungieren. Zei-
lenumbrüche (\r\n) sind ein Beispiel hierfür. Sie werden benötigt, um den Quelltext
zu formatieren oder zu strukturieren, haben jedoch keine Bedeutung für die Syntax
des Quelltextes. Vielmehr dienen sie dem menschlichen Auge zum Erstellen des
Quelltextes, da zum Beispiel die Lesbarkeit eines C-Programms in einer Zeile be-
kanntlich ungünstig ist.

2.3 Reguläre Ausdrücke

Um Tokens angeben zu können, bietet JavaCC die Möglichkeit, reguläre Ausdrücke


zu verwenden. Das Token CONSTANT setzt sich in Abbildung 1 aus (<DIGIT>)+ zu-
sammen wobei DIGIT sich wiederum aus dem regulären Ausdruck ["0" - "9"] zu-
sammensetzt. Es können also reguläre Ausdrücke verwendet werden, um Tokens zu
charakterisieren. Die lexikalische Analyse versucht dementsprechend die Tokens zu
erkennen [1] (Kapitel 2.2). Beim Erkennungsvorgang geht JavaCC hierbei linear vor
und führt die Regeln der Reihe nach aus. Sobald eine Regel einen Teil der Quellda-
ten erkennen konnte, wird ein Token Objekt erzeugt. Diesem Objekt wird das jewei-
lige Abbild und der Typ zugewiesen sowie im Token Strom angefügt. Es liegt nahe,
dass reguläre Ausdrücke existieren, die niemals oder zumindest falsch erkannt wer-
den. Die zwei wichtigsten Fehlerquellen werden im Folgenden erläutert.

• Im praktischen Einsatz müssen häufig Variablenbezeichner erkannt werden


[1] (Kapitel 2.1). Hier ist dann die Reihenfolge der regulären Ausdrücke wich-
tig. Ist der reguläre Ausdruck, der den Bezeichner erkennen soll, der erste
Ausdruck der lexikalischen Spezifikation, so wird die lexikalische Analyse
stets einen Bezeichner zuerst erkennen. Ein ähnlicher Ausdruck, der zum
Beispiel ein Schlüsselwort erkennen soll, wird somit niemals erkannt, da ihm
die Regel des Bezeichners „zuvor kommt“. Es liegt jedoch nahe, dass das
Schlüsselwort „void“ in C per Definition kein Bezeichner sein darf. Ergo ist
die Reihenfolge der Token Definitionen relevant für das korrekte Erkennen
und Zuweisen des Typs.
• Auch können Regeln existieren, die Quelldaten unter gewissen Umständen
nicht erkennen. Solche undefinierten Zustände sind nicht erwünscht, jedoch
in der Praxis relativ schwer auszuspüren, da große Token Ströme schwer zu
überblicken sind. Es bietet sich an, als letzten regulären Ausdruck ein speziel-
les Token (zum Beispiel: UNDEFINED) zu definieren und alles erkennen zu
lassen (regulärer Ausdruck: ~[]). Somit wird sichergestellt, dass alle Regeln
zumindest probiert wurden, bevor ein undefiniertes Token auftritt [6]. Es
muss nach der lexikalischen Analyse lediglich der Token Strom befragt wer-
den ob ein Token vom Typ UNDEFINED existiert. Im Falle des Auftretens ei-
nes UNDEFINED Tokens kann zum Beispiel ein Fehler erzeugt werden.

2.4 Token Strom

Wie bereits angedeutet, erstellt JavaCC mit der lexikalischen Analyse einen Strom
von Token Objekten. Leerzeichen (siehe Abbildung 1: SKIP) werden hierbei überle-
sen und gelangen nicht in den Token Strom. Man kann das Aufbauen des Stroms
als Transformation des rohen Quelltext Eingabestroms in einen objektorientierten
Token Strom betrachten. Es ist leicht ersichtlich, dass hierbei schon Informationen
verloren gehen, wenn auch weniger relevante Formatinformation. Wie bereits bei
den Leerzeichen erwähnt wurde, werden Formatinformationen schlicht überlesen.
Vor dem Hintergrund der lexikalischen Spezifikation, bei der die Leerzeichen als un-
wichtig definiert wurden, geht hierbei aus lexikalischer und syntaktischer Sicht keine
Information verloren. Ein Quelltext wie zum Beispiel "1+2*4;" wäre nach der lexika-
lischen Analyse ein Strom von Token Objekten der folgenden Form:

Type Image

9 CONSTANT 1

5 PLUS +

9 CONSTANT 2

7 MULTIPLY *

9 CONSTANT 4

Abbildung 4
3 Syntaktische Analyse

Im Anschluss an die lexikalische Analyse folgt die syntaktische Analyse. Hierbei wird
das Ergebnis der lexikalischen Analyse, der Token Strom, als wesentliche Grundlage
vorausgesetzt. Der Parser, der die syntaktische Analyse durchführt kann per se von
einem korrekten Token Strom ausgehen, da dieser im Falle eines lexikalischen Feh-
lers im Vorfeld per Definition einen lexikalischen Fehler erzeugt hätte.
JavaCC generiert Parser mit rekursivem Abstieg [1] (Kapitel 4.1). Das bedeutet, dass
alle Nicht-Terminale Symbole der Grammatik als rekursive Funktionen existieren [1]
(Kapitel 3.1). Die implementierten Funktionen in der syntaktischen Spezifikation
stellen genau diese Nicht-Terminale dar. Da diese Implementierungen eine Art Me-
tacode darstellen, jedoch in nativen Java Quelltext übersetzt werden, wird hier Ja-
vaCC Syntax und Java Syntax gemischt verwendet. Im Folgenden ist eine syntakti-
sche Spezifikation zu sehen, die in Abbildung 1 im Anschluss folgt.

/* Ab hier: Grammatikregeln */
void einstiegspunkt() : {}
{
sum() ";"
}
void sum() : {}
{
term() (( <PLUS> | <MINUS> ) term())*
}
void term() : {}
{
unary() (( <MULTIPLY> | <DIVIDE> ) unary())*
}
void unary() : {}
{
<MINUS> element()
| element()
}
void element() : {}
{
<CONSTANT>
| "(" sum() ")"
}

Abbildung 5 (Teil einer JavaCC Datei)

3.1 EBNF

Ähnlich der lexikalischen Spezifikation sind auch in der syntaktischen Spezifikation


reguläre Ausdrücke möglich [1] (Kapitel 3.2). Entgegen älteren Generatoren wie Lex
[4] und yacc [3] oder Flex [5] und bison [2] erlaubt JavaCC lexikalische und syntakti-
sche Spezifikation in einer gemeinsamen Datei. Somit bietet JavaCC die Möglichkeit,
die zuvor definierten Tokens in den regulären Ausdrücken direkt weiter zu verwen-
den. Formal kann hierbei von einer EBNF (Extended Backus Naur Form) Grammatik
gesprochen werden, da mit Hilfe der regulären Ausdrücke und Erweiterungen der
BNF (Backus Naur Form), komplexe Sachverhalte ohne flache Grammatik möglich
sind. Entgegen der BNF wird hier eine bessere Lesbarkeit und größere Prägnanz der
Definitionen erreicht. Hier liegt somit eine der Stärken von JavaCC, da mit Hilfe der
grundlegenden Operatoren (Ausdruck)+ und (Ausdruck)* zum Beispiel kompaktere
Grammatiken als ohne diese Befehle möglich sind.

3.2 Eigenschaften der Funktionen

Wie in Abbildung 5 zu sehen ist, besteht die Funktion aus einem Java Funktionskopf
und regulären Java Funktionsaufrufen. Dazwischen werden die Tokens platziert.
JavaCC bietet an dieser Stelle eine weitere Funktionalität, die Grammatiken lesbarer
machen kann. Konstante beziehungsweise triviale Tokens können direkt in der syn-
taktischen Spezifikation eingebunden werden. Hiermit können Präfixe gezielt in eine
Grammatik eingebaut werden, ohne erneut die lexikalische Spezifikation ändern zu
müssen. Regeln wie in Abbildung 5 in der Funktion element() sind somit besser les-
bar. Es scheint logisch, dass die Klammern „(“ und „)“ von essentieller Wichtigkeit
für die Syntax einer Sprache sind. Auf das Token Objekt von Strukturelementen
muss in realitätsnahen Anwendungen jedoch kaum zugegriffen werden. Dies rührt
daher, da meist eine Struktur durch den erkannten Baum implizit gegeben ist und
Klammern hier redundant wären.

3.3 Rekursiver Abstieg

Es ist ersichtlich, dass mit dem Aufruf der nicht-terminalen Funktion einstiegspunkt()
und einem zuvor befüllten Token Strom, ein impliziter Baum aufgebaut wird. Es
wird also rekursiv so tief hinab gestiegen, bis letztendlich alle Tokens konsumiert
wurden und der Token Strom leer ist, oder ein syntaktischer Fehler auftritt. Konsu-
mieren eines Tokens bedeutet das erfolgreiche Entfernen eines Tokens aus dem
Strom. Somit wird der Token Strom aus Abbildung 4 beim Erkennen wie folgt gele-
sen:
einstiegspukt sum

term

unary
1

element

<CONSTANT

<PLUS>

term

unary

2
element

<CONSTANT

<MULTIPLY>

element
4
<CONSTANT

Abbildung 6
4 Semantische Aktionen

Wie in Kapitel 2 und 3 erläutert wurde, erlauben die lexikalische und syntaktische
Analyse das Erstellen eines Baumes anhand eines Quelltextes. Somit hat ein Parser,
der mit JavaCC generiert wurde, bereits die Fähigkeit ein Wort aus seiner Sprache zu
erkennen. Damit ein Parser im letzten Schritt jedoch zum Übersetzer wird, werden
semantische Aktionen benötigt. Dies sind Aktionen, die während des Erkennens
eines Quelltextes ausgeführt werden und die Möglichkeit besitzen, den Syntaxbaum
aufzubauen, beziehungsweise zu manipulieren.
Semantische Aktionen bestehen formal aus derselben Sprache wie der Parser selbst,
in diesem Fall Java. In JavaCC werden diese Aktionen direkt in der syntaktischen
Spezifikation angegeben. Die Funktionsdefinitionen der syntaktischen Spezifikation
repräsentieren bekanntlich eine grammatikalische Regel der zu erkennenden Spra-
che. Der Rückgabewert beziehungsweise Parameterliste der Funktionsdefinitionen
können aus beliebigen Java Klassen bestehen. Des Weiteren erlaubt JavaCC das An-
reichern einer Grammatik um semantischen Quelltext an beliebigen Stellen inner-
halb des Funktionsrumpfes, solange dieser in geschweiften Klammern eingeschlos-
sen ist. Um eine Verbindung zur syntaktischen Analyse zu erreichen, kann hierbei
auf alle konsumierte Terminal Symbole mit der JavaCC Klasse „Token“ zugegriffen
werden. Dies wird mit einer einfachen Zuweisung direkt in der syntaktischen Spezi-
fikation erreicht. Im Folgenden sind sämtliche semantischen Stellen mit Umrandun-
gen markiert.

Integer element() throws NumberFormatException :

{
Token t = null;
}
{
t = <CONSTANT>
{return new Integer.valueOf(t.image);}
}

Abbildung 7 (Teil einer JavaCC Datei)

JavaCC erzeugt beim Generieren im Parser eine Funktion, die nahezu identisch mit
der Notation aus Abbildung 7 ist. Die Variablen Deklarationen, hier „Token t = null;“,
wandern in den Funktionsrumpf an die oberste Stelle und der komplette semanti-
sche Quelltext innerhalb des Funktionsrumpfes wird ohne die umschließenden ge-
schweiften Klammern eingesetzt. Auch liegt auf der Hand, dass der Ausdruck „t =
<CONSTANT>“ im Parser in ein Konstrukt übersetzt wird, das das aktuell nächste
Token im Strom betrachtet, und bei übereinstimmendem Typen das Token im Strom
konsumiert. Demnach beschreibt der folgende Pseudocode den Sachverhalt.
WENN(Typ_des_nächsten_Token_im_Strom = CONSTANT) Strom.entferneNächstesToken()
5 Fazit

JavaCC ist ein Parser Generator, der lexikalische-, syntaktische- und semantische
Spezifikation in einer Datei vereint und Java Parser erzeugen kann. Der Generator
bietet somit die Möglichkeit, die Bestandteile der lexikalischen Analyse, die Tokens,
direkt in der syntaktischen Analyse weiter zu verarbeiten und wiederum semantische
Aktionen direkt in syntaktische Grammatik Regeln einzubetten. So entsteht eine
Einheit, die aufgrund ihrer Kompaktheit eine gute Übersicht bietet, wobei gleichzei-
tig leistungsfähige Funktionalitäten bereitgestellt werden. Eine sehr alltagstaugliche
Eigenschaft von JavaCC ist die Möglichkeit, reguläre Ausdrücke nicht nur in der lexi-
kalischen Spezifikation, sondern vielmehr auch in der syntaktischen Spezifikation zu
verwenden. JavaCC bietet somit die Möglichkeit EBNF anstatt nur BNF Grammatiken
zu erzeugen, was den Vorteil der besseren Lesbarkeit mit sich bringt.
Auch bietet der Generator auf Grund des rekursiven Abstiegs seiner generierten
Parser ein gutes Verhalten im Schritt Modus bei der Fehlersuche, da sämtliche nicht-
terminale als eigene Funktion implementiert sind. Es ist demnach leicht ersichtlich,
dass der semantische Quelltext zur Laufzeit bequem untersucht werden kann.
Dies sind einige Gründe, die für die Benutzung von JavaCC sprechen, und nicht zu-
letzt durch die Einfachheit der Benutzung, können Alltagsprobleme auf einem ele-
ganten Weg gelöst werden. Ein einfacher Parser in JavaCC besteht in seiner Spezifi-
kation zum Beispiel aus 200 Zeilen, sein generierter Code jedoch aus mehreren tau-
send Zeilen. Der generierte Code von JavaCC kann natürlich auch manuell imple-
mentiert werden. Tiefgehende Änderungen sind hierbei nur durch hohen Zeitauf-
wand möglich, da eine große Codebasis angefasst werden muss.
Diese Arbeit übernimmt JavaCC und stellt somit die Möglichkeit bereit, sich auf das
eigentliche Problem zu konzentrieren.
Referenzen

[1] Andrew W. Appel, Jens P.: Modern Compiler Implementation in Java,


Second Edition. Cambridge University Press, 2002

[2] Donnelly, Charles ; Stallman, Richard: Bison: the Yacc-compatible


parser generator. 1990

[3] Johnson, S. C.: Yacc: Yet another compiler compiler. In: Computer
Science Technical Report #32, Bell Laboratories (1975)

[4] Lesk, M. E. ; Schmidt, E.: Lex — A lexical analyzer generator. In:


Computing Science Technical Report No. 39, Bell Laboratories (1975)

[5] Nicol, G. T.: Flex: The Lexical Scanner Generator. 1993

[6] Sun Microsystems, Tips for writing a good JavaCC lexical specification. Web:
https://javacc.dev.java.net/doc/lexertips.html, Abgerufen: 10. Januar 2007

Vous aimerez peut-être aussi