Analizzatori Lessicali con JLex Giuseppe Morelli Terminologia • Tre concetti sono necessari per comprendere la fase di analisi lessicale: – TOKEN: rappresenta un oggetto in grado di rappresentare una specifica classe di unità lessicali. In genere è rappresentato da un nome ed un attributo. – PATTERN o MODELLO: è una descrizione compatta delle possibili forme che una unità lessicale può assumere – LESSEMA: è una sequenza di caratteri del programma sorgente che corrisponde al pattern di un token (istanza specifica di un token) Compiti di un analizzatore lessicale • È la prima fase del processo di compilazione ed ha il compito principale di leggere il programma sorgente (sequenza di caratteri), raggrupparli in lessemi, e produrre in uscita una sequenza di token corrispondente ai lessemi trovati, interagendo con la tabella dei simboli. • Trattando direttamente il programma sorgente l’analizzatore lessicale è in grado di svolgere ulteriori operazioni e/o compiti • Elimina e/o ignora spazi vuoi e delimitatori in genere (spazi, tabulazioni, ritorni a capo) • Associa i messaggi di errore prodotti dal compilatore al programma sorgente ( es. conta il numero di CR e lo restituisce in presenza di un errore, o si occupa di riscrivere il programma sorgente con “iniettato” il messaggio di errore nella linea corrispondente) • Se nel programma sorgente sono presenti delle macro potrebbe occuparsi della relativa espansione Fasi analisi lessicale • A volte gli analizzatori lessicali sono visti come composizione di due distinti processi: • La scansione o scanning: eliminazione di separatori e commenti espansione di macro etc. …ovvero tutte le attività che non richiedono la tokenizzazione • L’analisi lessicale vera e propria: produzione della sequenza di TOKEN a partire dalla sequenza già “scansionata”. • Un analizzatore lessicale ha lo scopo di suddividere un “flusso” di caratteri in input in TOKEN. • Realizzare un analizzatore lessicale da zero, può risultare un lavoro alquanto complicato. • La miglior utility per la costruzione di un analizzatore lessicale è il programma Lex (generatore di analizzatori lessicali per Unix) che, dato un file di specifiche, genera il codice C di un analizzatore che soddisfa le specifiche JLex • JLex è una utility basata sul modello Lex, prende infatti delle specifiche simili a quelle accettate da Lex, quindi, crea un sorgente Java che implementa un analizzatore lessicale soddisfacente le specifiche. Download ed installazione • Il Link : http://www.cs.princeton.edu/~appel/modern/java/JLex/ • È possibile scaricare il sorgente del Main.java (naturalmente è scritto in Java) generatore: • Si mette in una directory JLex (il cui path sta nel PATH di ambiente) • Si compila • Si richiama su un file di specifica con : java JLex.Main source.lex • Nello stesso sito: manuale, readme, esempi. Struttura di un file di specifica Lex • Tre sezioni separate dal simbolo “%%”: 1. User Code: viene ricopiata nel file java finale “as is”; fornisce “spazio” per implementare classi di supporto etc.. 2. JLex directives: Vengono definite le Macro e vengono dichiarati i nomi degli stati 3. JLex rules: vengono definite le regole per l’analisi ognuna delle quali consiste di tre parti: lista degli stati (opzionale), espressione regolare, azione User code %% Jlex directives %% JLex rules User Code Section • Contiene le classi per: – Utilizzo del lexer (potrebbe non esserci se utilizzato in combinazione con un generatore di parser) – Funzioni, proprietà, variabili e/o costanti di supporto – La definizione degli oggetti TOKEN Note: JLex Directives • Yylex: è la classe generata dal lexer che implementa l’analizzatore lessicale • Per l’utilizzo del lexer nella sezione user code viene istanziato un oggetto e poi utilizzato come precedentemente mostrato • Vedremo in seguito che si può inserire codice in tale classe – Inserendo metodi e/o proprietà %{ code %} – Modificandone il costruttore (con o senza gestione delle eccezioni) %init{ code %init} – Inserendo codice eseguibile quando la fine del file viene raggiunta %eof{ code %eof} JLex Directives – Definizione di Macro • Una macro è di fatto la definizione di una espressione regolare; • Consiste di : <nome> = <definzione> dove nome è un identificatore, definizione è una espressione regolare; • Una definizione di macro può contenere l’espansione di altre macro JLex Directives - Dichiarazione di stato • Gli stati lessicali sono utilizzati per controllare il matching di alcune espressioni regolari. • La dichiarazione avviene: %state state[0], state[1], state[2] ….. dove state[0], state[1], state[2] … sono identificatori validi • YYINITIAL è lo stato implicito di ogni lexer generato con JLex • Tali stati compaiono opzionalmente all’inizio di ogni regola per la selezione della stessa. JLex Directives • %char permette di attivare il conteggio dei caratteri(yychar)in input (0 based) • %line permette di attivare il conteggio dei caratteri(yyline)in input (0 based) utile per la gestione e la segnalazione degli errori • La funzione Yylex.yylex() è la funzione da invocare per avere i TOKEN di tipo Yytoken Si possono modificare le cose con: – %class <name> cambio Yylex in name – %function <name> cambio yylex in name – %type <name> cambio Yytoken in name JLex Rules: Regolar Expression • Si tratta delle regole che consentono la suddivisione in TOKEN dell’input. • Si associano espressioni regolari, che rappresentano i lessemi del linguaggio, ad azioni (ovvero codice Java…. tokenizzazione) • Ogni regola ha tre parti: [<stati>] <espressione> {<azione>} • Se più di una regola è soddisfatta da una stringa di input viene eseguita la prima in ordine di apparizione (maggiore priorità) Stati ed espressioni • <stato0, stato1,…>: elenco di stati, opzionali, che permettono di attivare una regola Se la funzione yylex() è chiamata con il lexer che si trova nello stato X, il lexer potrà fare il matching con regole che hanno X nell’elenco degli stati • Nessuno stato -> la regola è selezionata in tutti gli stati • L’alfabeto per JLex è rappresentato dal set di caratteri Ascii (0 – 127): – – – – Metacaratteri ? * + | ( ) ^ $ . [ ] { } “ \ Escape \b \n \t \f \r \ddd \^C \c Concatenazione r1r2 concatenazione di r1 ed r2 Scelta r1 | r2 r1 oppure r2 Espressioni regolari Azione • L’azione associata ad una regola lessicale consiste in un blocco di codice Java { codice } • Tale codice dovrebbe prevedere un valore di ritorno.. Altrimenti il lexer va in loop alla ricerca di un altro lessema(coincide ad una chiamata ricorsiva a yylex()) • Attenzione alla ricorsione ed alla giusta gestione di yyline e yychar (non tail recursion vs tail recursion) Transizione degli stati • La transizione degli stati è fatta da una azione attraverso la chiamata alla funzione yybegin(state) • state deve essere uno stato valido dichiarato nella sezione JLex Directives • YYINITIAL è l’unico stato implicito ed è lo stato in cui il lexer rimane fino a che una non viene effettuata una transizione Il Lexer generato: note • Risiede in una classe Yylex • La funzione di accesso al lexer è Yylex.yylex() che restituisce token di tipo Yytoken • La classe Yytoken deve essere dichiarata nella USER CODE Section: – può ridefinire anche tipi primitivi – Si possono definire gerarchie di classi per token Esempi • Riconoscitore di Numeri ed identificatori • Riconoscitore di Numeri ed identificatori evoluto • Utilizzo degli stati per il riconoscimento di “stringhe”