Progetto Laboratorio Programmazione Parte 1: Analizzatore

Transcript

Progetto Laboratorio Programmazione Parte 1: Analizzatore
Progetto Laboratorio Programmazione
Parte 1: Analizzatore lessicale di espressioni
Stefano Guerrini
A.A. 2000/01
Il primo passo di un compilatore/esecutore o di un qualsiasi programma che deve elaborare sequenze non banali
di dati è l’analisi lessicale.
Nell’esempio che tratteremo per il progetto del laboratorio, l’input è una sequenza di assegnazioni di espressioni
aritmetiche a delle variabili. Ad esempio, una sequenza come quella in Figura 1.
alfa
beta
x
y
=
=
=
=
32;
-41;
alfa * beta;
(alfa * beta + x) / x * alfa + beta.
Figura 1: Esempio
La definizione precisa di cosa si intende per sequenza corretta verrà fornita nei dettagli in seguito (per coloro che
conoscono la BNF, riportiamo in appendice la definizione formale di sequenza di assegnazioni, vedi Figura 2).
Per il momento, osserviamo che gli elementi atomici che compongono l’input sono dei caratteri, ma che è
possibile individuare delle sequenze di caratteri che, dal punto di vista semantico (ovvero, dal punto di vista
del significato di ciò che si sta analizzando) costituiscono unità distinte. Si prenda ad esempio la prima riga di
Figure 1, è abbastanza naturale decomporla nelle sequenti parti
alfa
=
32
;
ed interpretare la stringa alfa come il nome di una variabile, il carattere = come il simbolo speciale che
separa il nome della variabile dalla espressione che gli si vuole assegnare, la stringa 32 come il numero intero
corrispondente, il carattere ; come il simbolo che indica la fine della prima assegnazione (ovvero della prima
coppia variabile-espressione).
1
I token
Il compito dell’analizzatore lessicale è quello di suddividere la stringa di input in token, ovvero, di raggruppare i
caratteri dell’input in modo da formare le unità di significato atomiche in cui è conveniente decomporre l’input.
Chiaramente, i token in cui suddivedere l’input dipendono dal tipo di testo che si deve analizzare. Nel nostro
caso, gli elementi che possiamo individuare sono:
identificatori: corrispondenti ai nomi delle variabili. Per semplicità, supponiamo che gli identificatori possano
contenere solo caratteri alfanumerici, comincino per un carattere alfabetico e che maiuscolo/minuscolo sia
significativo (overro, alfa, aLFa, AlfA ed ALFA sono identificatori diversi).
costanti numeriche: si tratta di sequenze composte da cifre numeriche (da caratteri nel’intervallo 0..9). Di
cosequenza, i numeri con segno (ed in particolare i numeri negativi) sono decomposti in coppie formate
dal simbolo speciale corrispondente al segno e dalla parte numerica vera e propria.
1
simboli speciali: sono i simboli corrispondenti alle parentesi e agli operatori aritmetici che compaiono nelle
espressioni, il carattere ; che separa le assegnazioni, il carattere . che indica la fine della sequenza;
il carattere =. Si osservi che nel nostro caso tutti i simboli speciali sono formati da un solo carattere.
Riassumendo, i simboli speciali sono:
()+-*/=;.
In generale, uno o più spazi bianchi possono apparire tra una qualsiasi coppia di token. Gli spazi bianchi non
sono però obbligatori. Ad esempio, nell’ultima assegnazione della sequenza di Figura 1 non c’è nessuno spazio
bianco tra la coppia di token ( e alfa, come non c’è nessuno spazio bianco tra la coppia di token x e ). Inoltre,
tale assegnazione è equivalente a
y=(alfa*beta+x)/x*alfa+beta.
dove non ci sono spazi bianchi tra i token. Possiamo quindi dire che gli spazi bianchi tra token devono essere
ignorati, anche se, si osservi che le due stringhe alfa beta e alfabeta corrispondono a cose molto diverse, il
primo ad una sequenza di due idenficatori alfa e beta, il secondo ad un unico identificatore alfabeta.
Nota. Input contenenti sequenze del tipo alfa beta non sono possibili in sequenze di assegnazioni corrette.
Questo errore non è però legato alla costruzione di token errati, ma al modo in cui i token sono concatenati—
ovvero, si tratta di un errore sintattico. L’analizzatore lessicale non si deve curare di scoprire situazioni di
questo tipo, sarà a livello di analisi sintattica che l’errore verrà rilevato.
2
Come rappresentare i token
Supponiamo di utilizzare la seguente struttura dati Pascal per memorizzare i token.
type token = record
tipo: integer;
id: string;
val: integer;
end;
Nella quale i campi sono utilizzati nel seguente modo:
• Il campo tipo contiene un codice numerico che identifica univocamente il token, oppure il valore -1 in caso
di errore (ovvero, in caso di lettura di un simbolo speciale che non compare tra quelli ammessi, ad esempio,
!). Il fatto che ogni simbolo speciale sia formato da un solo carattere, ci permette di associare come codice
di ciascun simbolo speciale il numero d’ordine del corrispondente carattere. Per quanto riguarda i codici
di identificatori e numeri invece, basta scegliere due valori che non interferiscono con i precendenti; in
particolare, fissiamo 1 per gli identificatori e 2 per i numeri.
• Il campo id viene utilizzato nel caso in cui il token è un identificatore, per memorizzare la stringa
dell’identificatore.
• Il campo val viene utilizzato nel caso in cui il token è un numero, per memorizzare il valore del numero.
3
La procedura nexttoken
La procedura principale che si deve scrivere è la
procedure nexttoken(var tk: token);
che scandisce l’input alla ricerca del token successivo e ritorna i dati del token nella variabile tk passata per
riferimento.
La procedura deve anche gestire il caso in cui non c’è più alcun token da leggere dato che si è giunti alla fine
del file di input (oppure, ci sono solo spazi bianchi prima della fine del fine). Per gestire correttamente questa
2
situazione, si consiglia di assumere che il valor di tipo 0 indichi il token vuoto e che questo è il valore di ritorno
della procedura nexttoken nel caso in cui nessun token sia presente prima della fine del file di input. Un’altra
possibilità è quella di trasformare la nexttoken in una funzione
function nexttoken(var tk: token): boolean;
che ritorna true nel caso in cui venga effettivamente letto un token dal file di input e false nel caso in cui ci si
trovi alla fine del file e non ci siano altri token da leggere. Si sconsiglia di gestire la fine file mediante chiamate
di eof esterne alla nexttoken.
Ovviamente, la nexttoken può richiamare altre procedure. In particolare si consiglia di scrivere la
procedure getnum(var n: integer);
che si aspetta di trovare in input una sequenza di cifre (ovvero, un numero senza segno) e converte tale sequenza
nel corrispondente valore intero (da ritornare in n). Tale procedura termina la lettura dell’input non appena il
numero termina (ovvero appena incontra un carattere che non è una cifra).
E di scrivere la
procedure getid(var id: string);
che si aspetta di trovare un identificatore, lo legge e ne ritorna il nome in id.
Si consiglia inoltre di utilizzare funzioni come le getch e ungetch per la scansione dell’input, in modo da
garantire che:
1. al momento della chiamata di getnum e getid, quello che si deve cercare di leggere è un numero (il
prossimo carattere di input è una cifra numerica) o un identificatore (il prossimo carattere di input è un
carattere alfabetico).
2. la getnum e la getid consumino tutti e soli i caratteri dell’input del numero e dell’identificatore che devono
leggere. Ad esempio, se la getnum viene chiamata quando in input si trova una sequenza che comincia per
234+alfa*. . .
allora la getnum deve consumare solo 234 dalla sequenza di input e garantire che dopo il token 234 sia
correttamente riconosciuto il token +. Analogamente, la getid che verrà chiamata dopo aver acquisito +,
dovrà consumare solo alfa e garantire la successiva acquisizione di *.
Nota. La soluzione che si basa sulle funzioni getch e ungetch non è l’unica possibile, nè c’è un unico modo di
usare correttamente le suddette funzioni. L’importante è che la soluzione adottata gestisca correttamente i casi
particolari discussi precedentemente. Come ulteriore aiuto, si verifichi attentamente che getid non perda il
primo carattere dell’identificatore che deve leggere e che getnum non perda la prima cifra del numero da leggere.
4
Cosa si deve fare
Il programma da realizzare deve dividere in token il testo contenuto in un file di nome dati.txt che si trova
nella stessa directory del programma in esecuzione. Si fa notare che, il nome del file di input non può essere
cambiato e che il programma deve funzionare indipendentemente dalla directory in cui si trova.
In questa parte del progetto, non interessa verificare che i token siano organizzati in maniera sintatticamente
corretta: a livello lessicale si arriva ad un errore solo nel caso in cui si trova un token non valido.
Come risultato, il programma deve stampare su righe separate i token che compongono l’input, nell’ordine in
cui appaiono in input. In particolare,
• per i simboli speciali, si deve stampare il corrispondente carattere;
• per gli identificatori, si deve stampare il codice corrispondente (ovvero, 1) e la stringa dell’identificatore;
• per i numeri, si deve stampare il codice corrsipondente (ovvero, 2) ed il suo valore.
Dato che i programmi verranno verificati automaticamente, si richiama l’attenzione sul fatto che l’output deve corrispondere esattamente alla descrizione precedentemente fornita. Per maggiore chiarezza, si riporta in
appendice l’output che ci si attende dalla analisi lessicale dell’esempio riportato in Figura 1.
3
A
Definizione formale di sequenza di assegnazioni
In Figura 2 è riportata la BNF che definisce le sequenze di assegnazioni corrette.
hsequenzai
hassegnazionei
hespressionei
hterminei
hoperandoi
hidentificatorei
hnaturalei
→
→
→
→
→
→
→
hassegnazionei;hsequenzai | hassegnazionei.
hidentificatorei=hespressionei
htermineihoperandoihespressionei | hterminei
(hespressionei) | hidentificatorei | [- | +]hnaturalei
+|-|*|/
sequenza di caratteri alfabetici
un numero naturale
Figura 2: BNF per le sequenze di espressioni
B
1
=
2
;
1
=
2
;
1
=
1
*
1
;
1
=
(
1
*
1
+
1
)
/
1
*
1
+
1
.
Esempio di output
alfa
32
beta
41
x
alfa
beta
y
alfa
beta
x
x
alfa
beta
4