ePrometeusCorsoLinuxLinux
testi articoli
Testi Articoli  Download
Home | OpenSource | PhpNuke | Programming | SysAdm | 
CorsoJava è ora Video! Free for all!
Clicca Qui!
UN COMPILATORE CON LEX E YACC
Usare Lex e Yacc per un traduttore da BASIC a C
Grammatiche ed alberi di sintassi concreta e astratta
Lex e Yacc
Un traduttore elementare da BASIC a C
Il processo della compilazione
Analisi lessicale con Lex
Analisi sintattica con Yacc
Azioni semantiche di traduzione
Conclusioni
Riferimenti
Download
L'Autore


<<< Analisi lessicale con Lex >>>

I linguaggi umani europei sono codificati utilizzando un ristretto insieme di simboli, i caratteri dell'alfabeto; tuttavia sono troppo pochi per descrivere un insieme di concetti sufficiente per l’uso: non si considerano infatti i singoli caratteri ma i loro raggruppamenti in parole.

Analogamente, nei linguaggi formali, i programmi sono codificati utilizzando un ristretto insieme di simboli, di solito l'ASCII, e anche in questo caso si considerano unità non tanto i singoli caratteri ma dei raggruppamenti significativi, i token appunto. Un compilatore tuttavia riceve in input una sequenza di caratteri ASCII ed il suo primo compito è quello di “spezzettare” l'input in token.

L'operazione di tokenizzazione può essere svolta in vari modi. Si può scrivere direttamente un programma in C, o si può utilizzare il formalismo delle grammatiche anche per riconoscere i token. Tuttavia il problema della tokenizzazione è sufficientemente noto da poter essere automatizzato e abbastanza complesso da essere scomodo da implementare a mano. Il meccanismo delle grammatiche è molto potente, ma applicarlo alla tokenizzazione si rivela inefficiente.

Tradizionalmente si divide l'analisi sintattica in due fasi, la prima detta analisi lessicale (che corrisponde alla tokenizzazione dell'input) e la seconda di analisi sintattica vera e propria. Per la generazione di analizzatori lessicale si utilizza spesso il tool Lex, che è progettato specificamente per essere utilizzato insieme allo Yacc. Infatti molte assunzioni del codice generato da Lex si sposano bene con quelle dello Yacc. Per esempio l'analizzatore lessicale prodotto da Lex è una funzione C chiamata yylex(), che è esattamente quella che si aspetta Yacc per l'analizzatore lessicale.

Lex utilizza per la specifica dell'analizzatore lessicale le espressioni regolari, che sono un formalismo più efficiente delle grammatiche ma meno potente. La distinzione tra grammatiche e espressioni regolari sta nel fatto che le espressioni regolari non sono in grado di riconoscere strutture sintattiche ricorsive, mentre questo non è un problema per le grammatiche. Una struttura sintattica come le parentesi bilanciate, che richiede che le parentesi aperte siano nello stesso numero di quelle chiuse non può essere riconosciuta da un analizzatore lessicale, e per questo scopo si ricorre all’analizzatore sintattico. Invece costanti numeriche, identificatori o keyword vengono normalmente riconosciute dall'analizzatore lessicale.

Nel seguente listato si può vedere il sorgente Lex dell'analizzatore lessicale di b2c.

%{

extern int line_nr;
char* new_string(char* s, int n);
int yywrap(void);

%}

identifier [A-Za-z][A-Za-z0-9]*
integer [0-9]+
string ["][^"]*["] 

%%

[ \t]+ { /* skip */ }
"\n" { ++line_nr; 
return(SEP); }
":" { return(SEP); } 
">=" { return(GE); }
"<=" { return(LE); }
"<>" { return(NE); }
[Aa][Nn][Dd] { return(AND); }
[Oo][Rr] { return(OR); }
[Nn][Oo][Tt] { return(NOT); }
[Ll][Ee][Tt] { return(LET); }
[Ff][Oo][Rr] { return(FOR); }
[Tt][Oo] { return(TO); }
[Nn][Ee][Xx][Tt] { return(NEXT); }
[Ww][Hh][Ii][Ll][Ee] { return(WHILE); }
[Ww][Ee][Nn][Dd] { return(WEND); }
[Ii][Ff] { return(IF); }
[Tt][Hh][Ee][Nn] { return(THEN); }
[Ee][Ll][Ss][Ee] { return(ELSE); }
[Ee][Nn][Dd][Ii][Ff] { return(ENDIF); }
[Pp][Rr][Ii][Nn][Tt] { return(PRINT); }
[Ii][Nn][Pp][Uu][Tt] { return(INPUT); }
{identifier} { yylval.str = new_string(yytext, yyleng); 
return(VARIABLE); }
{integer} { yylval.str = new_string(yytext, yyleng); 
return(INTEGER); }
{string} { yylval.str = new_string(yytext, yyleng); 
return(STRING); } 
. { return yytext[0]; }

%%

/* wrap function */
int yywrap(void) { 
return 1; 
}

char* new_string(char* s, int n) {
char* t = malloc(n+1);
strncpy(t, s, n);
t[n]='\0';
return t;
}

La seguente tabella riassume le principali espressioni regolari supportate da Lex.

x il carattere 'x'
. ogni carattere eccetto '\n'
[xyz] una classe di caratteri;
in questo caso, 'x', 'y' o 'z'
[a-z] una classe con un range;
ogni carattere compreso tra 'a' e 'z'
[^A-Z] una classe negata:
ogni carattere NON nella classe
r* zero o più r,
dove r è un'altra espressione regolare
r+ una o più r
r? zero o una r
r{2,5} tra due a cinque r
r{2,} due o più r
r{4} esattamente 4 r
{nome} l'espansione della definizione di nome
(r) r, parentesizzata per raggruppare
rs concatenazione: r seguita da s
r|s alternativa: r oppure s
r/s restrizione: r ma solo se seguita da s
^r r ma solo a inizio linea
r$ r ma solo a fine linea

L'input di Lex viene diviso in tre parti, separate da %%. La prima parte comprende delle definizioni, che sono delle sottoespressioni regolari, da utilizzare più avanti; nel listato possiamo vedere definizioni per identifier, integer, string. In generale le espressioni regolari possono essere anche molto complesse, quindi queste definizioni sono indispensabili per semplificare le espressioni regolari vere e proprie. La seconda parte comprende le espressioni regolari che vengono riconosciute dall'analizzatore lessicale; si può notare che alcune utilizzano definizioni dalla prima parte. Associata ad ogni espressione regolare c'è un pezzo di codice C che viene eseguito ogni volta che una espressione regolare viene riconosciuta. La terza parte comprende solo codice di supporto in C. Parti di codice possono essere inserite sia nella prima parte (inserendolo tra %{ e %}) che nella seconda, inserendolo tra { } immediatamente dopo ogni espressione regolare che si vuole riconoscere.

Lex genera una funzione, yylex(), che legge lo standard input e viene ripetutamente invocata dal parser ogni volta che è richiesto un nuovo token. Per essere esatti Lex genera un file, lex.yy.c, che viene normalmente incluso (con una #include) nel sorgente generato da Yacc. Infatti molte dichiarazioni, come i token (SEP, GE, AND,...) e le strutture dati per comunicare con il parser vengono dichiarate nel sorgente generato da Yacc, y.tab.c. La funzione yylex() viene richiamata ripetutamente dalla funzione principale del parser , yyparse().

Torniamo al Lex: una volta che un token viene riconosciuto, abbiamo varie possibilità: possiamo ignorarlo (come facciamo per gli spazi bianchi) e passare al token successivo; oppure possiamo ritornare il codice del token riconosciuto. Quando viene ritornato un token, la funzione yylex() termina ma sarà richiamata dal parser quando avrà bisogno di un altro token.

Notiamo che può essere necessario fare qualche azione supplementare oltre a ritornare un token o ignorarlo. Possiamo notare che quando incontriamo un newline, incrementiamo il contatore delle linee in input. Molto più importante è il fatto che di certi token occorre conoscere qualcosa di più oltre al loro tipo. Per esempio, di una variabile non basta sapere che è una variabile: occorre sapere quale è. Lex mantiene in un buffer il testo letto dall'input e riconosciuto, buffer accessibile all'utente tramite le variabili char* yytext e int yyleng.

Yacc fornisce le variabili per passare informazioni alla yyparse(). Ad ogni token, come pure ad ogni simbolo grammaticale, è possibile associare un valore. I valori associabili ad un token sono definiti da una union, dichiarata nel sorgente Yacc. Nel nostro caso dichiareremo anche un campo str di tipo char*. La variabile yylval, che è una union, è utilizzata per passare al parser il valore associato al token corrente; per variabili, costanti e stringhe provvederemo a passare informazioni supplementari, usando i campi di yylval.

Il testo riconosciuto da una espressione regolare si trova momentaneamente nel buffer yytext, ma dobbiamo provvedere a copiarlo da qualche parte per passarlo al parser; questa operazione è effettuata dalla funzione new_string. Notiamo infine la funzione yywrap(), che viene invocata da yylex() quando incontra la fine dell'input per decidere il da farsi. Se ritorna 1, vuol dire che l'input è proprio finito. Altrimenti la yywrap() può prendere provvedimenti, come aprire un altro file e collegarlo allo standard input, ritornando 0 per dire a yylex() di continuare a leggere.

ePrometeus s.r.l. - Web Software House & Open Source System Integrator
MILANO - SAN BENEDETTO DEL TRONTO(AP)
Contatti: info@eprometeus.com