In questo articolo esporrò le tecniche usate per poter creare degli analizzatori lessicali e parser in Java, usando degli strumenti gratuiti ( JFlex e byacc/J). In questo modo è facile creare un nuovo linguaggio di programmazione o più semplicemente creare un programma che estrae le parti significative da un input testuale, quale può essere un file in lettura o una stringa inserita da linea di comando, e le elabora a seconda della sintassi predefinita dal programma. Si pensi ad esempio ad un programma che prenda un’espressione numerica in input con variabili e costanti numeriche e ne calcoli il risultato stando attento a come usare le parentesi tramite una gerarchia prestabilità ( prima le parentesi tonde, poi le quadre ed in ultimo le graffe). Il problema è più complesso di quanto sembri, ad esempio si immagini un file sorgente C con la stringa:
E’ evidente come alla frase “int” si debba dare un significato e prima ancora occorre un modo per poter estrarre la frase “int” dal file. Questo causa molti problemi, se non trattati adeguatamente, in casi del genere:
Siccome un analizzatore lessicale legge uno stream di caratteri partendo dal primo byte, la domanda che dobbiamo porci è come fa nelle precedente seconda riga a sapere se si deve fermare ai primi tre caratteri perchè ha trovato una parola chiave oppure deve continuare a leggere per prendere l’identificatore “intero” ? Molti testi sono stati scritti riguardanti la teoria dei compilatori e su gli algoritmi che vengono usati per poter risolvere il problema precedente. In questo articolo non ci sofferemeremo su gli aspetti teorici come l’algortimo di Thompson, ma cercherò di mostrare come in poche righe di codice è possibile definire un’analizzatore lessicale abbastanza complesso e un parser che riconosce una grammatica predefinta da noi.
// File .flex in ingresso a JFlex.
%%
%byaccj
%{
private Parser yyparser;
public Yylex(java.io.Reader r, Parser yyparser) {
this(r);
this.yyparser = yyparser;
}
%}
ID = [a-zA-Z]([a-zA-Z0-9_])*
NUM = [0-9]+
FLOATNUM = {NUM}"."{NUM}
%%
{ID} { /*Salva il valore dell'ID in yyparser.yylval
e lo passa in input al parser*/
yyparser.yylval=new ParserVal(yytext());
return Parser.ID;
}
"+" {return (int) yycharat(0);}
"-" {return (int) yycharat(0);}
"/" {return (int) yycharat(0);}
"*" {return (int) yycharat(0);}
"(" {return (int) yycharat(0);}
")" {return (int) yycharat(0);}
\n {return (int) yycharat(0);}
{FLOATNUM} {yyparser.yylval= new ParserVal(yytext());
return Parser.FLOATNUM;}
{NUM} {yyparser.yylval= new ParserVal(yytext());
return Parser.NUM;}
[ \t\b\r] { // Skip }
%%
Ora, salvando il codice precedente in un file con estensione .flex e aprendolo con JFlex, se tutto è andato a buon fine quest’ultimo dovrebbe fornirci un file chiamato Yylex.java pronto per essere incluso nel nostro progetto. In pratica questa classe non ci è ancora molto utile perchè stiamo usando JFlex in coppia con Byacc/J e non singolarmente. Se omettessimo la direttiva:
%byaccj
potremmo usare la classe precendente per scandagliare un file in input contente espressioni aritmetiche ed estrarre tutte le sue componenti, ad esempio :
(1+2) – 3
prenderebbe il carattere “(” poi 1 come token NUM poi “+” ecc. tralasciando eventuali spazi di tabulazione intermedi.Da notare come il codice racchiuso tra parentesi graffe viene eseguito ogni volta che viene incontrato un token o lessema valido, ossia che rispecchia un’espressione regolare. Quindi, invece di passare l’argomento al parser un programma JFlex può benissimo eseguire altre operazioni.
Per poter dare un significato a tale espressione è necessario fornire una sintassi e una semantica. La semantica ovviamente è banale per quest’esempio come pure la sintassi, ma entrambi possono essere costruite in modo agevole con un genereatore di parser come byaccj.
%{
%}
%token ID
%token NUM
%token FLOATNUM
%start input // produzione di inizio
%left '+' '-' '*' '/' // Associatività degli operatori
%%
input:
|
input '\n' {/* Salta una riga vuota per
far avanzare il parser */
}
| input exp { System.out.println($2.sval);}
;
exp : exp '+' exp {$$.sval = $1.sval + $3.sval;}
| '(' exp ')' {$$.sval = $2.sval;}
| exp '-' exp {$$.sval = $1.sval - $3.sval;}
| exp '*' exp {$$.sval = $1.sval * $3.sval;}
| exp '/' exp {$$.sval = $1.sval / $3.sval;}
| NUM {$$.sval = $1.sval;}
| FLOATNUM {$$.sval = $1.sval;}
| ID { /*Recupera il valore dell'identificatore e memorizzalo in $$.sval,
non implementato*/}
;
%%
private Yylex lexer;
private int yylex () {
int yyl_return = -1;
try {
yylval = new ParserVal(0);
yyl_return = lexer.yylex();
}
catch (IOException e) {
System.err.println("IO error :"+e);
}
return yyl_return;
}
public void yyerror (String error) {
System.err.println ("Syntatic Error: " + error);
}
public Parser(Reader r) {
lexer = new Yylex(r, this);
}
public static void main(String args[]) throws IOException {
Parser yyparser;
String input = "(1+2)-2";
if ( args.length > 0 ) {
yyparser = new Parser( new java.io.StringReader(input));
yyparser.yyparse();
}else{
System.exit(0);
}
Salviamo il codice precendente in un file con estensione .y , ad esempio Parser.y (attenzione il nome del file è significativo per la classe in output, vedere la doc di byacc/j).Pre creare il parser è necessario richiamare byaccj con i seguenti argomenti:
yacc.exe -J Parser.y
L’esempio precedente non fornisce propriamente una calcolatrice perchè è stata omessa la parte relativa alla gestione dei tipi dei token i quali sono considerati tutti di tipo String. Leggendo accuratamente la documentazione di byacc/j ed i riferimenti seguenti non è difficile superare tale limitazione.
JFlex fornisce una comoda interfaccia grafica per poter creare il nostro analizzatore lessicale senza bisogno di usare la shell o il prompt.
Riferimenti
Aho, Sethi, Ullman, Compilers: Principles, Techniques, and Tools. Per maggiori dettagli vedere qui.
byaccj
JFlex