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