LEX Espressioni regolari in Lex

Transcript

LEX Espressioni regolari in Lex
LEX
Poiché la trasformazione di espressioni regolari in
automi a stati finiti deterministici e la implementazione
di questi ultimi sono processi meccanici (e noiosi),
spesso si utilizza un generatore automatico di
analizzatori lessicali.
Lex è un generatore che accetta in ingresso un insieme
di espressioni regolari e di azioni associate a ciascuna
espressione e produce in uscita un programma che
riconosce tali espressioni.
Espressioni regolari in Lex
Le espressioni regolari descrivono sequenze di caratteri ASCII ed
utilizzano un certo numero di operatori:
“\[]^-?.*+|()$/{}%<>
Lettere e numeri del testo di ingresso sono descritti mediante se
stessi:
l’espressione regolare val1 rappresenta la sequenza ‘v’ ‘a’ ‘l’ ‘1’ nel
testo di ingresso
I caratteri non alfabetici vengono rappresentati in Lex
racchiudendoli tra doppi apici, per evitare ambiguità con gli
operatori:
l’espressione xyz“++” rappresenta la sequenza ‘x’ ‘y’ ‘z’ ‘+’ ‘+’ nel
testo di ingresso
I caratteri non alfabetici possono essere anche descritti facendoli
precedere dal carattere \
l’espressione xyz\+\+ rappresenta la sequenza ‘x’ ‘y’ ‘z’ ‘+’ ‘+’ nel
testo di ingresso.
1
Espressioni regolari in Lex
Le classi di caratteri vengono descritte mediante gli operatori []
l’espressione [0123456789] rappresenta una cifra nel testo di
ingresso.
Nel descrivere classi di caratteri, il segno - indica una gamma di
caratteri:
l’espressione [0-9] rappresenta una cifra nel testo di ingresso
Per includere il carattere - in una classe di caratteri, questo deve
essere specificato come primo o ultimo della serie:
l’espressione [-+0-9] rappresenta una cifra o un segno nel testo di
ingresso.
Nelle descrizioni di classi di caratteri, il segno ^ posto all’inizio
indica una gamma di caratteri da escludere:
l’espressione [^0-9] rappresenta un qualunque carattere che non sia
una cifra nel testo di ingresso
Espressioni regolari in Lex
L’insieme di tutti i caratteri eccetto il fine riga (new line) viene
descritto mediante il simbolo “.”
Il carattere di fine riga viene descritto dal simbolo \n
Il carattere di tabulazione viene descritto dal simbolo \t
L’operatore ? indica l’espressione precedente è opzionale:
ab?c indica sia la sequenza ac che abc
L’operatore * indica l’espressione precedente può essere ripetuta 0
o più volte:
ab*c indica tutte le sequenze che iniziano per a, terminano per c e
hanno all’interno un numero qualsiasi di b
L’operatore + indica l’espressione precedente può essere ripetuta
1 o più volte:
ab+c indica tutte le sequenze che iniziano per a, terminano per c e
hanno all’interno almeno un b.
2
Espressioni regolari in Lex
L’operatore | indica un’alternativa tra due espressioni:
ab|cd indica sia la sequenza ab che la sequenza cd
Le parentesi tonde consentono di esprimere la priorità tra operatori:
(ab|cd+)?ef indica sequenze tipo ef, abef, cdddef.
Azioni associate alle
espressioni
Ad ogni espressione regolare è associata in Lex un’azione che viene
eseguita all’atto del riconoscimento.
Le azioni sono espresse sotto forma di codice C: se tale codice comprende
più di una istruzione o occupa più di una linea deve essere racchiuso tra
parentesi graffe.
L’azione più semplice consiste nell’ignorare il testo riconosciuto: si esprime
un’azione nulla con il carattere ‘;’.
Il testo riconosciuto viene accumulato nella variabile yytext, definita come
puntatore a caratteri.
Operando su tale variabile, si possono definire azioni
più complesse.
Il numero di caratteri riconosciuti viene memorizzato nella variabile yyleng,
definita come intero.
Esiste un’azione di default che viene eseguita in corrispondenza del testo
non descritto da nessuna espressione regolare: il testo non riconosciuto
viene ricopiato in uscita, carattere per carattere.
3
Struttura di un file lex
Un file sorgente per lex è composto di tre sezioni distinte separate dal simbolo
‘%%’.
La prima sezione contiene le definizioni e può essere vuota.
La seconda sezione contiene le regole sotto forma di coppie
espressione_regolare azione.
Le azioni devono iniziare sulla stessa riga in cui termina l’espressione regolare
e ne sono separate tramite spazi o tabulazioni.
La terza sezione contiene le procedure di cui il programmatore intende
servirsi: se è vuota, il separatore ‘%%’ viene omesso.
Per semplificare la gestione di espressioni regolari complesse o ripetitive, è
possibile definire identificatori che designano sotto-espressioni regolari.
Ogni riga della prima sezione il cui primo carattere non sia di spaziatura è una
definizione: numero [+-]?[0-9]+
La sotto-espressione così definita può essere utilizzata racchiudendone il
nome tra parentesi graffe: {numero} printf(“trovato numero\n”);
Definizioni
Le righe che iniziano con spazio o tabulazione sono ricopiate
identiche nel file di codice C generato dall’esecuzione di Lex.
Lo stesso avviene per tutti i caratteri compresi tra i delimitatori ‘%{‘
e ‘%}’.
Tutte le righe presenti nella sezione delle procedure (la terza) del
programma sorgente sono ricopiate nel file C generato da Lex.
Esempio
Scrivere un programma LEX che dato in ingresso un programma
C ne produca in uscita uno equivalente ma privo dei commenti.
Si modifichi il programma dell’esercizio precedente in modo che
riconosca le direttive #include e, quando le incontra, segnali un
errore e termini l’analisi. Si tenga conto che: possono comparire
degli spazi o tabulazioni, non interessa verificare la correttezza
del path: è un controllo che dovrebbe essere effettuato ad un
livello superiore.
4
Ambiguità lessicali
Esistono due tipi di ambiguità lessicali:
la parte iniziale di una sequenza di caratteri
riconosciuta da un’espressione regolare è riconosciuta
anche da una seconda espressione regolare.
La stessa sequenza di caratteri è riconosciuta da due
espressioni regolari distinte.
Nel primo caso verrà eseguita l’azione associata
all’espressione regolare che ha riconosciuto la sequenza
più lunga.
Nel secondo caso sarà eseguita l’azione associata
all’espressione regolare dichiarata per prima nel file
sorgente di lex.
ESEMPIO
Dato il file
%%
for {return FOR_CMD;}
format {return FORMAT_CMD;}
[a-z]+ {return GENERIC_ID;}
e la stringa di ingresso “format”, la procedura yylex
ritorna il valore FORMAT_CMD, preferendo la seconda
regola alla prima - perché descrive una sequenza più
lunga, e la seconda regola alla terza -perché definita
prima nel file sorgente.
5
Risoluzione delle ambiguità
lessicali
Date le regole di risoluzione dell’ambiguità, è necessario
definire prima le regole per le parole chiave e poi quelle
per gli identificatori.
Il principio di preferenza per le corrispondenze più
lunghe può essere pericoloso:
’.*’ {return QUOTED_STRING;}
cerca di riconoscere il secondo apice il più lontano
possibile: cosi, dato il seguente ingresso first’ quoted
string here, ’second’ here riconoscerà 36 caratteri invece
di 7
Una regola migliore è la seguente:
’[^’\n]+’ {return QUOTED_STRING;}
Esercizi
Scrivere l’espressione regolare che descrive un
identificatore nel linguaggio C.
Scrivere l’espressione regolare di un numero in virgola
mobile con eventuale parte esponenziale
Come sopra, eliminando zeri iniziali e finali non
significativi.
Scrivere l’espressione regolare di un commento nel
linguaggio C.
Il commento può contenere i caratteri ‘*’ e ‘/’ purché
non adiacenti.
6
ESERCIZI
Scrivere l’espressione regolare di una stringa che possa
contenere al proprio interno anche apici purché
preceduti dal carattere ‘\’.
Modificare la regola precedente affinché ritorni una
copia
della
stringa
riconosciuta,
dopo
aver
opportunamente rimosso eventuali ‘\’ di troppo.
Scrivere un programma lex che rimuova i tag da un file
in formato HTML sostituendo i tag <P> e <BR> con un
a-capo.
Dipendenza dal contesto
Può essere necessario limitare la validità di un’espressione regolare
all’interno di un determinato contesto.
Esistono meccanismi diversi per specificare la dipendenza dal contesto
destro (cioè ciò che segue la sequenza di caratteri che si sta
riconoscendo) rispetto alla dipendenza dal contesto sinistro (ciò che la
precede).
Fa eccezione la gestione del contesto di inizio e Fine riga.
Contesto di inizio e fine riga
Il carattere ‘^’ all’inizio di un’espressione regolare indica che la sequenza
descritta deve essere posta all’inizio di riga.
Ciò significa che o si è posizionati all’inizio del file di ingresso o che
l’ultimo carattere letto è stato un carattere di fine riga.
Il carattere ‘$’ al termine di un’espressione regolare indica che la
sequenza descritta deve essere seguita da un carattere di fine riga.
Tale carattere non viene incluso nella sequenza riconosciuta, deve
essere riconosciuto da un’altra regola.
7
Dipendenza dal contesto destro
e sinistro
L’operatore binario ‘/’ separa un’espressione regolare dal suo
contesto destro.
Pertanto, l’espressione ab/cd indica la stringa “ab”, ma solo se
seguita da “cd”.
I caratteri che formano il contesto destro vengono letti dal file di
ingresso ma non introdotti nel testo riconosciuto. A tale scopo viene
utilizzato un apposito buffer fornito da Lex.
L’espressione ab$ è equivalente a ab/\n.
È utile poter avere diversi insiemi di regole lessicali da applicare in
porzioni diverse del file di ingresso, in genere in funzione di ciò che
precede, ovvero del contesto sinistro.
Esistono tre approcci distinti per affrontare il problema:
uso di variabili flag.
uso di condizioni iniziali sulle regole o stati.
uso congiunto di più analizzatori lessicali.
Uso di variabili flag
Per esprimere la dipendenza dal contesto sinistro è
possibile utilizzare variabili flag nelle azioni delle regole.
Tali variabili devono essere state precedentemente
dichiarate come variabili globali.
La funzione REJECT può essere utilizzata per evitare
corrispondenze non volute: redirige lo scanner alla
seconda miglior regola che riconosce lo stesso input,
oppure ad una regola che riconosca un prefisso dello
stesso input.
8
La struttura del programma
generato da Lex
Lex produce un programma C, privo di main() il cui punto di
accesso è dato dalla funzione int yylex().
Tale funzione legge dal file yyin e ricopia sul file yyout il testo non
riconosciuto.
Se non specificato diversamente nelle azioni (tramite l’istruzione
return), tale funzione termina solo quando l’intero file di ingresso è
stato analizzato.
Al termine di ogni azione l’automa si ricolloca sullo stato iniziale
pronto a riconoscere nuovi simboli.
Per default, i file yyin e yyout sono inizializzati rispettivamente a
stdin e stdout.
Il programmatore può alterare questa situazione re-inizializzando
tali variabili globali.
9
Jlex: a scanner generator
jlex specification
xxx.jlex
JLex.Main
(java)
xxx.jlex.java
javac
input program
test.sim
P2.main
(java)
generated scanner
xxx.jlex.java
Yylex.class
Output of P2.main
Yylex.class
Come creare e chiamare lo
scanner
public class P2 {
public static void main(String[] args) {
FileReader inFile = new FileReader(args[0]);
Yylex scanner = new Yylex(inFile);
}
Symbol token = scanner.next_token();
while (token.sym != sym.EOF) {
switch (token.sym) {
case sym.INTLITERAL:
System.out.println("INTLITERAL ("
+ ((IntLitTokenVal)token.value).intVal \
+ ")");
break;
…
}
token = scanner.next_token();
}
10
JLEX
Per produrre lo scanner, è stato fatto uso del generatore
d’analizzatori lessicali conosciuto come JLex. Questo software,
scritto interamente in java, produce in uscita delle classi java che
mplementano i metodi per effettuare l’analisi lessicale di una stringa
in ingresso.
La classe principale che è prodotta è Yylex, la quale contiene il
metodo yylex() che preleva e analizza il successivo token in arrivo.
Un altro metodo contenuto nella classe è yytext(), esso ritorna il
testo riconosciuto da yylex().
Il JLex per funzionare, ha bisogno in ingresso un file di specifica,
contenente tutti i dettagli relativi all’analisi lessicale che si vuole
realizzare.
JLEX
user code
%%
Jlex directives
%%
regular expression rules
Usercode: codice java scritto dall’utente, che implementa le
classi da questi definite, eventualmente richiamate nelle azioni
compiute a seguito del riconoscimento dei tokens.
Ad esempio si possono scrivere classi che gestiscono la
visualizzazione dei messaggi d’errore ecc.
11
JLEX
user code
%%
Jlex directives
%%
regular expression rules
Jlex directives: contiene tutta una serie di direttive previste nel
manuale, impostabili dall’utente e danno la possibilità di
personalizzare l’analizzatore secondo le esigenze attuali.
Consente di definire delle macro
JLEX
user code
%%
Jlex directives
%%
regular expression rules
regular expression rules: contiene tutte le espressioni regolari
definite in precedenza nel lessico. A ciascuna può essere associata
un’azione da compiere, ogni volta che il token corrente è
riconosciuto. Ogni azione deve essere scritta di seguito
all’espressione regolare e racchiusa tra parentesi graffe.
12
Regular expression rules
regular-expression
{
action
}
pattern che deve essere
riconosciuto
codice che deve essere eseguito
quando è riconosciuto il pattern
Quando il metodo next_token() viene chiamato, le seguenti azioni
vengono eseguite:
Trova la sequenza di caratteri più lunga nella stringa di ingresso
(iniziando dal carattere corrente) che matches il pattern.
Esegue le azioni associate
Regole di Matching
Se diversi patterns possono corrispondere alla stessa sequenza di
caratteri viene scelta la strategia longest pattern, cioè viene scelta
la stringa più lunga fra quelle possibili.
Se diversi patterns possono corrispondere alla stessa sequenza più
lunga viene seguite la strategia first such pattern, cioè viene scelto
il primo pattern che è stato definito.
Pertanto l’ordine di definizione dei patterns può essere
importante!
Se un carattere di input non matched alcun pattern viene lanciata
una exception
13
Regular expressions
Hanno la struttura di una espressione regolare.
⌧abc
⌧==
⌧while
Stringhe di caratteri fra apici, incluso I caratteri
speciali corrispondono alla stringa stessa “a|b”
matches a|b not a or b
⌧“a\”\”\tb”
matches a””\tb
not
a””<TAB>b
Operatori
Per costruire le espressioni regolari si usano I seguenti
operatori
|
*
+
?
()
unione
Stella di Kleen
Significa una o piu istanze
Significa zero o una o una istanza
14
Altri operatori
^ inizio linea
^main significa che la stringa “main” deve comparire
all’inizio della linea.
$ fine linea
main$ significa che la stringa “main” deve comparire
alla fine della linea.
Classi di caratteri
[abc]
Significa a | b | c)
[a-z]
Significa qualsiasi carattere fra a e z, inclusi
[^abc]
Qualsiasi carattere esclusi a, b, e c.
^ ha il significato di 1a posizione in […]
[\t\\]
Signitica tab o \
[a bc] è equivalente a a|" "|b|c
15
JLex directives
Costituisce il secondo blocco di xxx.jlex.
Può anche specificare
⌧Il valore di ritorno alla fine del file
⌧Che lo scanere sarà utilizzato con il parser generator java
cup.
Le direttive includono le definizioni di macro:
name = regular-expression
⌧Qualsiasi identificatore Java valido
DIGIT= [0-9]
LETTER= [a-zA-Z]
WHITESPACE= [ \t\n]
Per usare una macro, utilizzaremo il suo nome fra parentesi graffe
{LETTER}({LETTER}|{DIGIT})*
Esempio
%%
DIGIT=
[0-9]
LETTER=
[a-zA-Z]
WHITESPACE= [ \t\n] // spazio,tab, newline
// per renderlo compatibile con java CUP
%implements java_cup.runtime.Scanner
%function next_token
%type java_cup.runtime.Symbol
…
16
Esempio
…
%%
{LETTER}({LETTER}|{DIGIT}*)
{System.out.println(yyline+1
+ ": ID " + yytext());}
{DIGIT}+ {System.out.println(yyline+1 +
"="
{System.out.println(yyline+1 + ":
"=="
{System.out.println(yyline+1 + ":
{WHITESPACE}* { }
.
{System.out.println(yyline+1 + ":
": INT");}
ASSIGN");}
EQUALS");}
bad char");}
17