Professional Documents
Culture Documents
JavaCC
ver. 0.736
0.736 del 10-05-1210-05-12-16.30
Nicola Fanizzi
Indice
[1] Introduzione..................................................................................5
1.1 JavaCC.....................................................................................5
1.2 Linea di Comando........................................................................7
1.3 File Prodotti da JavaCC..................................................................9
1.4 Struttura di una Specifica JavaCC....................................................10
1.4.1 Opzioni.............................................................................12
1.4.2 Produzioni..........................................................................15
[2] Analisi Lessicale............................................................................16
2.1 L'Analizzatore Lessicale................................................................16
2.2 Funzionamento del Token Manager...................................................17
2.3 Dichiarazioni.............................................................................18
2.4 Token: Regole per Espressioni Regolari..............................................18
2.4.1 Stati Lessicali......................................................................19
2.4.2 Tipi di Espressioni Regolari......................................................19
2.4.3 Specifica di Espressioni Regolari................................................19
2.5 Prefissi Comuni..........................................................................22
2.6 La Classe dei Token.....................................................................23
2.7 Azioni Lessicali..........................................................................24
2.7.1 Variabili............................................................................24
2.7.2 Esempi..............................................................................26
2.7.3 Token Speciali e Parser..........................................................27
2.8 Evoluzione della Classe dei Token....................................................27
2.9 Analisi Lessicale Efficiente............................................................28
[3] Analisi Sintattica............................................................................30
3.1 Produzioni Javacode....................................................................30
3.2 Produzioni BNF..........................................................................32
3.2.1 Espansioni BNF....................................................................32
3.2.2 Lookahead Locale.................................................................33
[4] Lookahead...................................................................................36
INTRODUZIONE [1]
[1] INTRODUZIONE
1.1 JavaCC
Questo manualetto si propone di illustrare l'utilizzo di JavaCC ("Java Compiler
Compiler"), uno strumento open-source per la costruzione automatica di analizzatori
lessicali e sintattici in Java. A partire da una specifica formale della grammatica del
linguaggio, esso produce classi in puro codice Java atte a leggere ed analizzare frasi in
tale linguaggio.
JavaCC risulta particolarmente utile allorch i linguaggi da analizzare presentino
strutture tanto complesse da sconsigliare un approccio non automatizzato. Questa
tecnologia nata per rendere pi semplice l'implementazione dei linguaggi di
programmazione - da qui il termine "compilatore di compilatori" - ma trova
applicazione anche per linguaggi pi semplici e di tipo diverso (scripting, markup,
ecc.). JavaCC non automatizza anche la costruzione esplicita degli alberi sintattici ma
1.1 JAVACC
esistono (almeno) due altri strumenti a corredo di JavaCC che possono essere utilizzati
a tale scopo: JJTree e JTB. Risulta facile, inoltre, estendere le specifiche per
automatizzare la costruzione delle tabelle dei simboli.
Sia JavaCC sia i parser generati con questo strumento girano su numerose piattaforme.
Molti linguaggi hanno gi una specifica per JavaCC come quella dello stesso Java,
JavaScript, RTF, Visual Basic, PHP, Python, C, C++, ma anche XML, HTML, SQL, e
molti altri ancora.
Le caratteristiche principali di JavaCC sono le seguenti:
INTRODUZIONE [1]
TOKEN SPECIALI: I token definiti come speciali nella specifica lessicale sono
ignorati in fase di analisi sintattica, ma possono essere elaborati da altri
strumenti. Un'applicazione utile potrebbe essere quella relativa
all'elaborazione dei commenti ai fini della documentazione.
(e.g., -STATIC=false)
(e.g., -STATIC:false)
(equivalent to -optionname=true.
e.g.,
Option settings are not case-sensitive, so one can say "nOsTaTiC" instead
of "-NOSTATIC". Option values must be appropriate for the
corresponding
option, and must be either an integer, a boolean, or a string
value.
The integer valued options are:
LOOKAHEAD
(default 1)
CHOICE_AMBIGUITY_CHECK (default 2)
OTHER_AMBIGUITY_CHECK (default 1)
The boolean valued options are:
STATIC
(default true)
SUPPORT_CLASS_VISIBILITY_PUBLIC (default true)
DEBUG_PARSER
(default false)
DEBUG_LOOKAHEAD
(default false)
DEBUG_TOKEN_MANAGER
(default false)
ERROR_REPORTING
(default true)
JAVA_UNICODE_ESCAPE
(default false)
UNICODE_INPUT
(default false)
IGNORE_CASE
(default false)
COMMON_TOKEN_ACTION
(default false)
USER_TOKEN_MANAGER
(default false)
USER_CHAR_STREAM
(default false)
BUILD_PARSER
(default true)
BUILD_TOKEN_MANAGER
(default true)
TOKEN_MANAGER_USES_PARSER (default false)
INTRODUZIONE [1]
SANITY_CHECK
FORCE_LA_CHECK
CACHE_TOKENS
KEEP_LINE_COLUMN
(default
(default
(default
(default
true)
false)
false)
true)
EXAMPLE:
javacc -STATIC=false -LOOKAHEAD:2 -debug_parser
mygrammar.jj
%
Ogni opzione pu essere impostata sulla linea di comando come mostrato in questo
messaggio oppure nel file di specifica della grammatica (come si vedr in seguito).
Se la stessa opzione viene impostata sia su linea di comando sia nella grammatica,
impostazione della linea di comando ha la precedenza.
pu
essere
lanciato
file customizzati (nel seguito il prefisso XXX indica un generico prefisso scelto
dall'utente):
IGNORE_CASE
JAVACODE
LOOKAHEAD
MORE
PARSER_BEGIN
PARSER_END
SKIP
SPECIAL_TOKEN
TOKEN
TOKEN_MGR_DECLS
10
INTRODUZIONE [1]
Il nome usato nelle direttive PARSER_BEGIN e PARSER_END deve essere identico: esso
corrisponder al nome dato al parser generato (ovvero alla sua classe). Ad esempio, se
si usa Prova come nome, allora i file generati saranno:
Prova.java: la classe del parser generato;
ProvaTokenManager.java: Il token manager generato;
ProvaConstants.java: contiene costanti d'utilit.
Un generico file "Prova.jj" ha il seguente formato:
options{
/* codice per settare i vari flag delle opzioni */
/*
(questa sezione opzionale)
*/
}
PARSER_BEGIN(Prova)
public
/* Qui
/* Pu
/* Pu
}
class Prova {
definizione metodi aggiuntivi per il parser */
anche essere lasciato vuoto */
essere inserito qui il metodo main() */
PARSER_END(Prova)
TOKEN_MGR_DECLS :
{
/* Dichiarazioni ad uso dello scanner */
/* (opzionale, serve per usi speciali) */
}
/* Sezione per le regole dei token ed azioni */
Tra i costrutti PARSER_BEGIN e PARSER_END si pu inserire un'unit di compilazione
Java (ossia l'intero contenuto di un file Java). Essa dovrebbe contenere la dichiarazione
di una classe con lo stesso nome utilizzato nelle direttive ("Prova" nell'esempio).
PARSER_BEGIN(NOME)
...
class NOME {
...
}
...
PARSER_END(NOME)
JavaCC non compie controlli particolari su tale unit, per cui possibile che un file di
specifica venga elaborato da JavaCC senza problemi e, tuttavia, vengano riconosciuti
errori in fase di compilazione dei file Java generati.
Se tale unit contiene dichiarazioni di package, queste vengono incluse in tutti i file
generati. Se, invece, comprende dichiarazioni import, queste sono incluse nei file
generati per il parser ed il token manager .
Il file generato per il parser contiene tutto quello che nell'unit di compilazione e, in
aggiunta, il codice per il parser incluso alla fine della classe relativa.
11
1.4.1 OPZIONI
Se presente, la sezione delle opzioni comincia con la parola riservata di JavaCC
options, seguita da una lista di una o pi impostazioni (option-binding) racchiusa tra
parentesi graffe.
sezione_opzioni ::= [ "options" "{" ( impostazione )* "}" ]
Ogni importazione specifica il settaggio di una opzione. Una stessa opzione non pu
essere impostata pi volte. Come detto in precedenza, le opzioni possono anche essere
impostate sulla riga di comando (queste ultime vengono considerate come
impostazioni a maggiore precedenza sulle altre modalit)
Le impostazioni si scrivono secondo la regola:
impostazione :: nomeOpzione "=" costante_java ";"
dove le possibili opzioni sono:
12
INTRODUZIONE [1]
13
TOKEN_FACTORY: Opzione che richiede una stringa (default ""). Essa indica
che i Token andranno creati chiamando Token.newToken(). Va settata per
indicare una classe Token-factory che contiene un metodo public static
Token newToken(int ofKind, String image).
14
INTRODUZIONE [1]
rilevati per via dell'opzione posta a false) pu causare un comportamento
inatteso da parte del parser.
1.4.2 PRODUZIONI
Le produzioni del file
regola:
produzione ::=
|
|
|
15
17
2.3 Dichiarazioni
Riprendendo il discorso fatto per le produzioni (cfr. 1.4.2) la regola per le
dichiarazioni del token manager risulta la seguente:
dic_token_manager ::=
"TOKEN_MGR_DECLS" ":" blocco
La sezione delle dichiarazioni del token manager inizia con la parola riservata
TOKEN_MGR_DECLS seguita da ":" e quindi da un blocco di dichiarazioni e istruzioni in
Java (blocco). Tali dichiarazioni ed istruzioni verranno trascritte nel token manager e
sono accessibili attraverso le azioni lessicali (descritte nel seguito). In ogni file di
specifica ci pu essere una sola sezione delle dichiarazioni.
18
regola
vengono
19
20
21
"?": la corrispondenza avviene con la stringa vuota o con una delle espressioni
tra parentesi.
Si noti che diversamente dalle espansioni BNF, l'espressione regolare "[...]" non
equivale a "(...)?". Ci perch si usa il costrutto [...] per descrivere liste di caratteri
nelle espressioni regolari.
Una lista di caratteri descrive un set di caratteri, ed definita attraverso la seguente
meta-regola:
lista_caratteri ::=
[ "~" ] "["
[ descrittore_caratteri ( ","
descrittore_caratteri )* ] "]"
Essa consiste in una lista di descrittori di caratteri separati da virgole ed includa tra
quadre. Ogni descrittore descrive un singolo carattere od un intervallo Se viene
anteposto il prefisso "~", l'insieme da intendersi complementato (rispetto all'insieme
dei caratteri UNICODE).
Formalmente il descrittore segue la meta-regola:
descrittore_caratteri ::= stringa [ "-" stringa ]
Un descrittore pu essere dato da una stringa di un solo carattere, nel qual caso
descrive l'insieme composto da quel solo carattere, oppure da due caratteri separati da
un "-", nel qual caso descrive un intervallo che comprende tutti i caratteri, estremi
compresi.
22
..."
La regola 1 taglia fuori la seconda produzione. La regola 2 dice di preferire la terza alla
prima. L'ordine delle produzioni non ha effetto in questo esempio.
Esempio: Soffermandoci su Java, (o C, o C++), si supponga di avere le produzioni di
espressioni regolari
TOKEN : { < KWINT : "int" > }
TOKEN : { < IDENT : ["a"-"z","A"-"Z", "_"]
(["a"-"z","A"-"Z","0"-"9","_"])* > }
Si supponga anche che l'input stream cominci con "integer i; ", allora la seconda
produzione sarebbe preferita per la regola 2. Ma se la parte restante fosse "int
i; ...", allora tale regola non sarebbe d'aiuto, dato che entrambe le produzioni
avrebbero in comune un prefisso di lunghezza 3. In tal caso la produzione KWINT viene
preferita (regola 3) perch viene prima nella specifica.
I token speciali sono altri token che non hanno rilevanza sintattica ma non
vengono scartati, come si farebbe ad esempio con i commenti.
23
String image rappresenta l'immagine del token cos come appare nel flusso
d'ingresso.
24
25
2.7.2 ESEMPI
Esempio 1: Commenti
SKIP :
{
"/*" : InCommento
}
<InCommento> SKIP :
{
"*/" : DEFAULT
}
<InCommento> MORE :
{
<~[]>
}
26
27
28
29
30
31
32
Un'unit pu essere data da una specifica locale di lookahead (cfr. 3.2.2). Questo
spiega al parser generato come effettuare le scelte nei punti idonei.
Un'unit potrebbe essere anche data da un insieme di dichiarazioni Java e codice tra
graffe (un blocco). Questa forma si dice anche azione del parser. Essa viene generata
nel metodo del non-terminale in punti specifici. Il blocco viene eseguito tutte le volte
che il processo di analisi superi tale punto con successo. Quando JavaCC elabora il
blocco, non opera alcun controllo sintattico o semantico. Pertanto possibile che il
compilatore Java trovi errori nelle azioni dopo che queste siano state elaborate da
JavaCC. Si noti che le azioni non vengono eseguite durante la valutazione del
lookahead.
Una unit potrebbe essere costituita da un insieme posto tra parentesi di una o pi
scelte d'espansione. In tal caso, perch l'unit sia stata correttamente analizzata basta
che lo sia stata una delle sue scelte. L'insieme di scelte pu essere seguito dai seguenti
suffissi (opzionali):
Un'unit pu essere anche costituita da una espressione regolare. In tal caso ogni token
che corrisponda all'espressione va bene per il parsing. Quando la corrispondenza viene
trovata, si crea un oggetto di tipo Token accessibile se assegnato ad una variabile
anteponendo all'espressione regolare "id_variabile =". In generale, si pu avere
qualunque parte sinistra di assegnazione valida prime di "=". Tale assegnazione non
viene attuata durante la valutazione del lookahead.
Un'unit, infine, potrebbe essere costituita da un non-terminale. In tal caso, prende la
forma di chiamata del metodo relativo a quel non-terminale. Se l'analisi del nonterminale va a buon fine lavorando sui i parametri della chiamata viene restituito un
valore (a meno che il tipo restituito dal metodo non sia "void"). Il valore restituito pu
opzionalmente esser assegnato ad una variabile nel modo visto in precedenza. Anche in
tal caso, Tale assegnazione non viene attuata durante la valutazione del lookahead. I
non-terminali non possono essere usati in un espansione in modo da introdurre
ricorsione-sinistra. JavaCC effettua questo controllo per l'utente.
33
34
Se il vincolo sintattico non viene fornito, ha per default la scelta per la quale si
applica la specifica locale di lookahead. Se tale specifica non presso un punto
di scelta, allora il lookahead sintattico viene ignorato - per cui il valore di
default non rilevante.
35
[4] LOOKAHEAD
[4] LOOKAHEAD
Questo capitolo si basa sugli esempi della distribuzione ufficiale di JavaCC contenti
nella sotto-directory examples/Lookahead della directory degli esempi.
36
LOOKAHEAD [4]
La maniera generale per trovare questa corrispondenza quella di muoversi attraverso
la grammatica basandosi sulle stringhe, come segue. Nel seguito si user "abc" come
stringa di input:
Passo 1. C' una sola scelta il primo carattere in input dev'essere una 'a' e
dato che proprio il nostro caso, si va avanti.
Passo 2. Si procede col non-terminale BC. Qui, di nuovo, c' una sola
possibilit per il prossimo carattere in input dev'essere una 'b'. L'input
corrisponde e si va avanti.
Passo 3. Si ora ad un punto di scelta" nella grammatica. Si pu entrare nel
costrutto [...] e trovare la corrispondenza, oppure ignorarlo del tutto.
Decidiamo di provare ad entrare. Per cui il prossimo carattere dev'essere una
'c'. Si va avanti.
Passo 4. Si completato il lavoro con il non-terminale BC e si torna al nonterminale Input. Ora la grammatica dice che il prossimo carattere dev'essere
un altro 'c'. Ma non ci sono pi caratteri in input. Pertanto sorge un problema.
Passo 5. Con un simile problema nel caso generale, si conclude che potrebbe
essere stata operata una scelta sbagliata in passato. In questo caso, si
sbagliato al Passo 3. Per cui si torna indietro al Passo 3 e si prova a fare una
scelta diversa. Tale processo si chiama "backtracking".
Passo 6. Si torna quindi indietro per operare l'altra scelta al Passo 3 - cio,
ignorare il contenuto di [...]. S' completato cos il lavoro su BC e si torna al
non-terminale Input. Ora la grammatica dice che il prossimo carattere
dev'essere ancora un altra 'c'. Il prossimo carattere in input proprio una 'c',
per cui si va avanti.
Passo 7. Si constata ora di aver raggiunto con successo la fine dell'analisi
sintattica (fine del non-terminale Input). Ci significa che si riconosciuta
correttamente la stringa "abc" in base alla grammatica.
Come indica questo esempio, la soluzione del problema dell'analisi sintattica in
generale potrebbe richiedere molto backtracking e tornare su vecchie scelte risulta
un'attivit dispendiosa. Il tempo richiesto dipende molto anche dal modo in cui
definita la grammatica: per lo stesso linguaggio possono essere proposte molte
grammatiche equivalenti.
Ad esempio, la grammatica seguente accelera il parsing dello stesso linguaggio rispetto
alla grammatica data precedentemente:
void Input() :
{}
{
"a" "b" "c" [ "c" ]
}
invece la grammatica che segue peggiorerebbe le prestazioni ancora di pi dato che
costringe il parser a tornare indietro e ricominciare tutto daccapo:
37
38
LOOKAHEAD [4]
// Scelta 1
// Scelta 2
// Scelta 3
39
// Scelta 1
// Scelta 2
// Scelta 3
// Scelta 4
40
LOOKAHEAD [4]
else
stampa messaggio d'errore
}
Si noti che nell'esempio riportato l'algoritmo per la scelta non guarda oltre il costrutto
(...)* per prendere la sua decisione. Si supponga che ci sia un altra produzione nella
grammatica (Example5.jj):
void funny_list() :
{}
{
identifier_list() "," <INT>
}
Quando l'algoritmo di default sta prendendo la scelta al punto ( "," <ID> )*,
entrer sempre in (...)* se il prossimo token un ",". Lo far anche quando
identifier_list fosse chiamato da funny_list e il token dopo "," fosse un
<INT>. Intuitivamente, la cosa giusta da fare in tale situazione di saltare il costrutto
(...)* e tornare a funny_list. Torneremo su questo pi tardi.
Come esempio concreto, si supponga che l'input sia "id1, id2, 5"; il parser
lamenter di aver incontrato un 5 mentre s'aspettava un <ID>. Si noti che alla
generazione del parser si ottiene un avvertimento come quello che segue:
Warning: Choice conflict in (...)* construct at line 25, column
8.
Expansion nested within construct and expansion following
construct have common prefixes, one of which is: ","
Consider using a lookahead of 2 or more for nested expansion.
In buona sostanza, JavaCC ci dice di aver trovato una situazione nella grammatica che
potrebbe indurre l'algoritmo di lookahead di default a comportarsi in modo strano. Il
parser funzionerebbe ma non farebbe ancora quello che ci aspetta che faccia.
Sono stati mostrati esempi di due tipi di punti di scelta - "exp1 | exp2 | ...", e
"(exp)*". Gli altri tipi di punti di scelta - "(exp)+" e "(exp)?" - si comportano allo
stesso modo di (exp)* per cui non verranno forniti ulteriori esempi d'uso.
41
42
LOOKAHEAD [4]
void basic_expr() :
{}
{
{ initMethodTables(); } <ID> "(" expr() ")"
|
"(" expr() ")"
"new" <ID>
|
{ initObjectTables(); } <ID> "." <ID>
}
Dato che le azioni sono diverse, la fattorizzazione non pu essere fatta.
43
// Scelta 1
// Scelta 2
// Scelta 3
// Scelta 4
Solo la prima scelta (la prima condizione nella traduzione di seguito riportata) viene
interessata dalla specifica di lookahead. Tutte le altre continuano ad usare un singolo
token anticipato:
if (i prossimi 2 token sono <ID> e "(" ) {
scegli Scelta 1
} else if (prossimo token "(") {
scegli Scelta 2
} else if ( prossimo token "new") {
scegli Scelta 3
} else if ( prossimo token <ID>) {
scegli Scelta 4
} else {
stampa messaggio d'errore
}
Analogamente, Example5.jj pu essere modificato come riportato (Example9.jj):
void identifier_list() :
{}
{
<ID> ( LOOKAHEAD(2) "," <ID> )*
}
Si noti che la specifica LOOKAHEAD deve comparire all'interno di (...)* che
rappresenta la scelta che si sta facendo. La traduzione di questo costrutto viene di
seguito mostrata (dopo il primo <ID> stato consumato):
while (i prossimi 2 token sono "," e <ID>) {
scegli l'espansione interna
(ossia, entra nel costrutto (...)* )
consuma il token ","
consuma il token <ID>
}
Si scoraggia fortemente dalla modifica del lookahead globale di default. La maggior
parte delle grammatiche quasi del tutto LL(1), per cui, convertendo il parser
risultante dalla grammatica in uno utile alla forma LL(k) per facilitare semplicemente
alcune porzioni della grammatica che non sono LL(1), si abbasserebbero notevolmente
le prestazioni senza un reale bisogno. Questo non comporterebbe eccessivi problemi
solo se la grammatica ed i file di input sono corti.
44
LOOKAHEAD [4]
Si dovrebbe anche considerare che i messaggi d'avvertimento che JavaCC stampa a
causa di ambiguit nei punti di scelta (come i due messaggi mostrati in precedenza)
dicono semplicemente che nei punti specificati la grammatica non pu essere LL(1).
JavaCC non verifica la correttezza della specifica locale di lookahead - assume che si
sappia quello che si fa, ma di fatto, non pu davvero verificare tale correttezza come
illustrato nel seguente esempio riguardante le istruzioni if (Example10.jj):
void IfStm() :
{}
{
"if" C() S() [ "else" S() ]
}
void S() :
{}
{
...
|
IfStm()
}
Questo il famoso esempio del problema dell'else (dangling else problem). Avendo in
input un programma scritto come segue:
"if C1 if C2 S1 else S2"
Il costrutto "else S2" pu essere legato ad entrambi i comandi if. L'interpretazione
standard che dovrebbe essere legato al comando pi interno (quello ad esso pi
vicino). Capita che l'algoritmo di default per determinare la faccia la cosa giusta, ma
stampa comunque il seguente messaggio d'avvertimento:
Warning: Choice conflict in [...] construct at line 25, column
15.
Expansion nested within construct and expansion following
construct have common prefixes, one of which is: "else"
Consider using a lookahead of 2 or more for nested expansion.
Per evitare il messaggio, si potrebbe semplicemente dire a JavaCC di essere consci di
quello che si fa in questo modo:
void IfStm() :
{}
{
"if" C() S() [ LOOKAHEAD(1) "else" S() ]
}
Per forzare il controllo di ambiguit del lookahead in tali casi, si imposta a true
l'opzione FORCE_LA_CHECK.
45
46
LOOKAHEAD [4]
if (i prossimi token corrispondono a ClassDeclaration) {
choose ClassDeclaration()
} else if (il prossimo token corrisponde a quelli per
InterfaceDeclaration) {
choose InterfaceDeclaration()
} else {
stampa un messaggio d'errore
}
Il problema con questa specifica di lookahead sintattico che il calcolo del lookahead
richiede molto tempo e compie numerosi controlli non necessari. In tal caso, il calcolo
pu fermarsi non appena si incontri il token "class", ma la specifica forza il calcolo a
continuare fino a quando si raggiunga la fine della dichiarazione della classe il che
molto dispendioso. Questo problema pu essere risolto mettendo una espansione pi
corta da tentare nella specifica di lookahead sintattico come nell'esempio che segue:
void TypeDeclaration() :
{}
{
LOOKAHEAD( ( "abstract" | "final" | "public" )* "class" )
ClassDeclaration()
|
InterfaceDeclaration()
}
In sintesi, i passi da fare diventano:
if (i prossimi token sono una sequenza di "abstract", "final",
o "public" seguita da "class") {
choose ClassDeclaration()
} else if (prossimo token corrisponde a
InterfaceDeclaration) {
scegli InterfaceDeclaration()
} else {
stampa un messaggio d'errore
}
Facendo ci, si fa fermare l'algoritmo di scelta non appena si incontri il token " class"
cio si prende la decisione il pi presto possibile.
Si pu altres mettere un limite al numero di token da consumare nel lookahead
sintattico:
void TypeDeclaration() :
{}
{
LOOKAHEAD(10, ( "abstract" | "final" | "public" )* "class" )
ClassDeclaration()
|
InterfaceDeclaration()
}
In questo caso, non si permette all'algoritmo di andare oltre i 10 token. Se si raggiunge
tale limite e si stanno trovando corrispondenze con ( "abstract" | "final" |
"public" )* "class", allora si seleziona ClassDeclaration.
47
48
LOOKAHEAD [4]
L'esempio pu essere riscritto combinando lookahead sintattico e semantico come
segue (riconosce il primo "c" con il sintattico e l'assenza del secondo "c" con il
semantico):
void BC() :
{}
{
"b"
[ LOOKAHEAD( "c", { getToken(2).kind != C } )
<C:"c">
]
}
49
Questa sezione dedicata alle caratteristiche di gestione e recupero degli errori con
JavaCC.
Ci sono due nuove eccezioni:
. ParseException
. TokenMgrError
Quando il token manager incontra un problema, esso lancia l'eccezione
TokenMgrError. (con le precedenti versioni veniva stampato il messaggio Lexical
Error ..." dopo il quale veniva lanciato comunque una eccezione ParseError).
Quando il parser incontra un problema, esso lancia l'eccezione ParseException.
(precedentemente veniva stampato il messaggio Encountered ... Was
expecting one of ..." dopo il quale veniva lanciata l'eccezione ParseError).
Nelle nuove versioni i messaggi d'errore non sono mai stampati esplicitamente,
l'informazione viene conservata nell'oggetto eccezione da lanciare. Per maggiori
dettagli si vedano le classi ParseException.java e TokenMgrError.java
generate da JavaCC insieme al parser.
50
eccezioni
51
52
53
54
ESEMPI [5]
[5] ESEMPI
5.1 Calcolatrice
La specifica sintattica seguente riguarda una semplice calcolatrice che accetta dall'input
standard espressioni additive e moltiplicative su numeri interi:
options {
LOOKAHEAD=1;
}
PARSER_BEGIN(Calcolatrice)
public class Calcolatrice {
public static void main(String args[])
throws ParseException {
Calc1 parser = new Calc1(System.in);
while (true) {
System.out.print("Espressione <- : ");
System.out.flush();
try {
switch (parser.espressione()) {
case -1: System.exit(0);
default: break;
}
}
catch (ParseException e) {
System.out.println("Terminazione");
throw e;
55
5.1 CALCOLATRICE
}
} // while
} // main
} // class
PARSER_END(Calcolatrice)
/* sezione lessicale */
SKIP :
{
" "
| "\r"
| "\t"
}
TOKEN :
{
< EOL: "\n" >
}
TOKEN :
{
<
|
|
|
}
/* operatori */
PIU: "+" >
< MENO: "-" >
< PER: "*" >
< DIV: "/" >
TOKEN :
{
< COSTANTE: ( <CIFRA> )+ >
| < #CIFRA: ["0"-"9"] >
}
/* sezione sintattica */
int espressione() :
{}
{
somma() <EOL> { return 1; }
| <EOL> { return 0; }
| <EOF> { return -1; }
}
void somma() :
{ }
56
ESEMPI [5]
{
termine() (( <PIU> | <MENO> ) termine())*
}
void termine() :
{ }
{
fattore() (( <PER> | <DIV> ) fattore())*
}
void fattore() :
{ }
{
<MENO> elemento() | elemento()
}
void elemento() :
{}
{
<COSTANTE> | "(" somma() ")"
}
5.2 Bowdlerizer
Si devono sostituire certi pattern in ingresso con un testo diverso. Supponiamo che il
pattern da cercare quello delle parole di quattro lettere. Prenderemo la specifica alla
lettera e si sostituisce ogni parola di quattro lettere a prescindere dal significato.
options {
STATIC = false ;
}
PARSER_BEGIN(PQL)
import java.io.Reader ;
import java.io.StringReader ;
class PQL {
static String sostituzione(String inStringa) {
Reader sr = new StringReader(inStringa);
PQL parser = new PQL(sr);
StringBuffer buffer = new StringBuffer();
try { parser.inizio(buffer); }
catch(TokenMgrError e)
{throw new IllegalStateException();}
catch(ParseException e)
{throw new IllegalStateException();}
return buffer.toString();
} // sostituzione
} // classe
PARSER END(PQL)
TOKEN : {
57
5.2 BOWDLERIZER
< PAROLA_DI_4_LETTERE : (<LETTERA>){4} >
< PAROLA_DI_PIU_DI_4_LETTERE :
(<LETTERA>){5} (<LETTERA>)* >
< #LETTERA : ["a"-"z","A"-"Z"] >
< ALTRO : [] >
}
58
ESEMPI [5]
Si sarebbe potuto potuto specificare le cifre elencando tutte le cifre singolarmente
["0"-"9"] oppure si scrive anche ["0", "1", "2", "3", "4", "5", "6",
"7", "8", "9"]. In generale si elencano caratteri individuali o intervalli. Ad
esempio ["0"-"9", "a"-"z", "A"-"Z", "", "-"] corrisponde ad ogni
carattere che una cifra, lettera, apostrofo o trattino.
Per trovare i token che non fanno parte delle parole consentite di 4 lettere o pi si
user.
TOKEN : { < ALTRO : [] > }
5.2.3 PARSING
La specifica del parser immediata. I token dei tre tipi possono apparire in qualsiasi
numero ed ordine. Per token PAROLA_DI_4_LETTERE si scrivono quattro asterischi
sull'output, mentre per gli altri tipi si stampa semplicemente l'immagine del token.
void inizio(StringBuffer buffer) :
{ Token t; }
{
(
<PAROLA_DI_4_LETTERE> { buffer.append("****"); }
|
( t=<PAROLA_DI_PIU_DI_4_LETTERE>
| t=<OTHER>
) { buffer.append(t.image); }
)*
<EOF>
}
59
5.2 BOWDLERIZER
Dato che il parser accetta qualunque sequenza di token in ingresso, il parser non potr
mai lanciare una ParseException. Sia il token manager che il parser sono totali":
essi accettano qualsiasi stringa in ingresso.
5.3 CSV2HTML
Si illustrer l'idea della traduzione con l'esempio che traduce un file CSV (Comma
Separated Values) in una tabella HTML.
Una CSV un semplice tipo di struttura dati: tabella di variabili separate da virgole e
ritorni a capo. Un esempio riportato di seguito:
STUDENT ID
NAME
DATE OF BIRTH
129384
Davy
328649
Clare Manstead
30/11/81
237090
Richard Stoppit
22/06/82
523343
Brian Hardwick
15/11/81
908423
Sally Brush
06/06/81
328453
Elisa Strudel
12/09/82
883632
Peter Smith
03/05/83
542033
Ryan Alcot
04/12/80
Jones
03/04/81
60
ESEMPI [5]
TOKEN :
{
<VIRGOLA: ",">
| <RECORD: (~[",","\r","\n"])+ >
| <FINELINEA: "\r\n" | "\r" | "\n" >
}
Il main() deve preoccuparsi dei file in ingresso ed uscita (segnalando errori) e
dell'inizio e fine standard di una pagina HTML da costruire:
public class CSV2HTML {
public static void main(String args[]) {
String tabella ="<tabella>\n";
if(args.length!=2) { errore(); }
FileInputStream myIn = null;
DataOutputStream myOut = null;
try {
myIn = new FileInputStream(args[0]);
myOut = new DataOutputStream(new
FileOutputStream(args[1]));
} catch(Exception e) { errore2(); }
try {
DataInputStream input = new DataInputStream(myIn);
CSV2HTML parser = new CSV2HTML(myIn);
String p ="";
p="<html>\n <head>\n";
p+="<title>Tabella generata da CSV2HTML </title>\n";
p+=" </head>\n <body>\n";
p+= parser.file();
p+=" </body>\n </html>";
myOut.writeBytes(p); myOut.close();
} catch(Exception e) {
System.err.println(e.getMessage());
}
} // main
private static void errore() {
System.out.println("USO:");
System.out.println("java CSV2HTML inFile outFile");
System.exit(0);
} // errore
private static void errore2() {
System.out.println("inputfile non valido.");
System.exit(0);
} // errore2
} // classe
PARSER_END(CSV2HTML)
61
5.3 CSV2HTML
Le regole per il parser vanno arricchite con azioni che creino le stringhe utili a riempire
la parte centrale del file di output:
String file() :
{ String tabella="<table border='1'>", lineaDati; }
{
lineaDati=linea()
{ tabella+=lineaDati; }
(<FINELINEA> lineaDati=linea()
{ tabella+=lineaDati; } )*
(<FINELINEA>)? <EOF>
{ return tabella+="</table>"; }
}
String linea():
{ String recordDati, lineaDati=" <tr>\n"; }
{
(recordDati=record() { lineaDati+=recordDati; })+
{ return lineaDati+=" </tr>\n"; }
}
String record():
{ String recordDati="
<td>"; Token dati; }
{
dati=<RECORD> (<VIRGOLA>)?
{return recordDati+=dati +"</td>\n";}
}
62
[6] COSTRUZIONE DI
ALBERI SINTATTICI
CON JJTREE
6.1 Introduzione
JJTree un preprocessore per JavaCC che inserisce azioni per la costruzione degli
alberi di parsing in vari punti del sorgente per JavaCC. L'output di JJTree viene
sottoposto a JavaCC per creare il parser. Questo documento descrive il modo per
utilizzare JJTree, e come interfacciare il proprio parser.
Per default JJTree genera codice per costruire nodi di alberi sintattici per ogni nonterminal nel linguaggio. Questo comportamento pu essere modificato affinch alcuni
non-terminali non comportino la generazione di nodi, oppure affinch si generi un
nodo per una parte dell'espansione di una produzione.
JJTree definisce un interfaccia Java detta Node implementata da tutti i nodi degli
alberi sintattici. L'interfaccia fornisce metodi per operazioni come impostare il genitore
del nodo, per aggiungere figli e recuperarli.
63
6.1 INTRODUZIONE
JJTree opera in una delle due modalit semplice o multipla (in mancanza di termini
migliori). In modalit semplice ogni nodo del tipo concreto SimpleNode; in modalit
multipla il tipo del nodo deriva dal nome del nodo. Se non si forniscono
implementazioni per le classi dei nodi, JJTree generer implementazioni-campione
basate su SimpleNode. Si possono quindi modificare a piacimento tali
implementazioni.
Sebbene JavaCC produca parser top-down, JJTree costruisce l'albero nel verso bottomup. Per far questo utilizza uno stack dove inserisce (push) i nodi dopo la loro creazione.
Quando si trova il loro genitore, si possono estrarre (pop) i figli dallo stack ed
aggiungere tale genitore all'albero inserendolo anche sullo stack. Lo stack libero,
intendendo con questo che possibile accedervi da parte delle azioni grammaticali: si
pu inserire, estrarre o effettuare altre operazioni sul contenuto della pila. Si veda 6.2
per altre informazioni.
JJTree fornisce la possibilit di aggiungere decorazioni per due variet di base dei nodi
e abbreviazioni per facilitarne l'utilizzo.
1. Un nodo definito viene ad essere costruito con un numero di figli specificato.
Altrettanti nodi sono estratti dallo stack e resi figli del nuovo nodo, il quale
viene a sua volta inserir sullo stack. La notazione per un nodo siffatto la
seguente:
#AdefiniteNode(ESPRESSIONE INTERA)
L'espressione descrittiva di un nodo definito pu essere data da qualunque
espressione a valori interi, sebbene molto comunemente si trovino utilizzate
direttamente costanti intere.
2. Un nodo condizionale si costruisce con tutti i figli inseriti sullo stack all'interno
del suo scope di nodo (vedi 6.2) se e solo se la sua condizione risulta vera.
Qualora risultasse falsa, il nodo non sar costruito e tutti i figli rimanono sullo
stack dei nodi. La notazione per i nodi condizionali la seguente:
#ConditionalNode(BOOLEAN EXPRESSION)
L'espressione descrittiva di un nodo condizionale pu essere fatta da
qualunque espressione a valori booleani. Ci sono due abbreviazioni comuni per
tali nodi:
1. nodi indefiniti
#IndefiniteNode
sta per
#IndefiniteNode(true)
2. nodi maggiore-di (greater-than)
#GTNode(>1)
sta per
#GTNode(jjtree.arity() > 1)
64
65
66
67
68
6.8 Opzioni
JJTree supporta le seguenti opzioni per la linea di comando o per il costrutto options
di JavaCC:
NODE_FACTORY (default: "") Specifica una classe contenente un metodofactory utile a costruire nodi, con la seguente intestazione:
public static Node jjtCreate(int id)
Per compatibilit con precedenti versioni, si pu anche specificare il valore
false, che fa si che SimpleNode venga usato come classe-factory.
NODE_PACKAGE (default: "") Il package nel quale vengono generate le classi dei
nodi. Il default il package del parser.
69
6.8 OPZIONI
STATIC (default: true) Genera codice per un parser static. Tale opzione va
usata coerentemente con le opzioni equivalenti di JavaCC. Il valore di
quest'opzione viene emesso nel sorgente per JavaCC.
JJTREE_OUTPUT_DIRECTORY
(default:
si
usa
il
valore
di
OUTPUT_DIRECTORY) Per default, JJTree generata il suo output nella
directory specificata dal valore dell'opzione globale OUTPUT_DIRECTORY.
Impostando esplicitamente questa opzione permette agli utenti di separare i
file per il parser da quelli per la costruzione degli alberi.
70
6.10
I nodi dell'AST devono implementare l'interfaccia Node. Essa fornisce la base per la
costruzione delle relazioni padre-figlio tra i nodi. L'interfaccia ha la forma seguente:
public interface Node {
/** chiamato dopo che il nodo diventato quello corrente.
Indica che si possono aggiungere ora nodi-figlio . */
public void jjtOpen();
/** Chiamato dopo l'aggiunta di figli. */
public void jjtClose();
/** Metodi per informare il nodo sul suo padre. */
public void jjtSetParent(Node n);
public Node jjtGetParent();
/** Dice al nodo di aggiungere i suoi argomenti alla lista
dei figli. */
public void jjtAddChild(Node n, int i);
/** Restituisce un nodo figlio. I filgi sono numerati a
partire da 0, e da sinistra a destra. */
public Node jjtGetChild(int i);
/** Restituisce il numero di figli del nodo. */
int jjtGetNumChildren();
}
La classe SimpleNode implementa l'interfaccia Node e viene generata
automaticamente da JJTree qualora non esista ancora. Si pu usare tale classe come
template o superclasse per le proprie implementazioni dei nodi, oppure la si pu
modificare a piacimento. SimpleNode fornisce in pi un meccanismo rudimentale di
dumping ricorsivo del nodo e dei suoi figli. Si pu utilizzare questa caratteristica in
azioni come:
{
((SimpleNode)jjtree.rootNode()).dump(">");
}
Il parametro di tipo String in dump() utilizzato per indicare la gerarchia
dell'albero.
Un altro metodo d'utilit viene generato se stata impostata l'opzione VISITOR:
{
public void childrenAccept(MyParserVisitor visitor);
71
6.11
Esempi
JJTree viene distribuito con una serie di esempi che contengono una grammatica per il
parsing delle espressioni aritmetiche. Per ulteriori dettagli si veda il file
examples/JJTreeExamples/README.
Tra gli esempi c' anche un interprete per un semplice linguaggio che usa JJTree per
costruire la rappresentazione del programma. Per ulteriori informazioni, si veda
examples/Interpreter/README.
Per informazioni su un esempio che usa il supporto alla visita si veda invece
examples/VTransformer/README.
72
Bibliografia
Sito ufficiale: http://javacc.dev.java.net/
JavaCC FAQ: http://www.engr.mun.ca/theo/JavaCC-FAQ
Usenet group: comp.compilers.tools.javacc
Tom Copeland: Generating Parsers with JavaCC. Centennial Books, 2007.
Viswanathan Kodaganallur: Incorporating Language Processing into Java
Applications: A JavaCC Tutorial. IEEE Software, 21(4): 70-77, 2004.
73