1 Laboratorio di Compilatori Contenuti Analisi lessicale Espressioni

Transcript

1 Laboratorio di Compilatori Contenuti Analisi lessicale Espressioni
Politecnico di Torino
1
3
✏
✏
✏
✏
✏
✏
✏
✏
✏
✏
✏
a.a. 1999/2000
Esercitazioni in aula
http://www.polito.it/Ulisse/CORSI/INF/N3070/materiale/
Marco Torchiano
Dipartimento di Automatica e Informatica
Tel. (011 564) 7081
E-mail: [email protected]
Contenuti
☛ Analisi lessicale
☛ Analisi sintattica
☛ Regole semantiche
☛ Controllo dei tipi
☛ Ambienti di esecuzione
☛ Codici intermedi
☛ Tecniche per il parsing
Analisi lessicale
☛ Espressioni regolari
☛ Lex
Laboratorio di Compilatori
2
Laboratorio di Compilatori
a.a 1999/2000
4
Espressioni regolari in Lex
Azioni associate alle espressioni regolari
La struttura di un programma sorgente Lex
Il codice generato da Lex
Risoluzione delle ambiguità
Dipendenza dal contesto
Condizioni iniziali
Eliminazione dei commenti
Il file prodotto
Inclusione di file
Meccanismi di chiamata e definizione di simboli
Espressioni regolari
☛ Costituiscono un metodo semplice ed efficace per
descrivere insiemi di stringhe di caratteri.
☛ Opportuni operatori consentono di indicare
✏ caratteri
✏ classi di caratteri
✏ opzionalità
✏ ripetizione (0 o più volte)
✏ ripetizione (1 o più volte)
✏ alternativa
✏ concatenazione
✏ ambito di operatori
Versione 1.2 © 2000 Marco Torchiano
‘c’ o anche c
[a,b,c] o [a-c]
exp ?
exp *
exp +
exp1 | exp2
exp1 exp2
( exp )
1
Politecnico di Torino
5
Esempi di espressioni regolari
7
✏ [0-9]+
☛ numero intero positivo senza 0 inziali
✏ [1-9][0-9]*
☛ numero intero positivo o negativo
[0-9]+
☛ numero in virgola mobile
✏ (‘+’|’-’)? ( ([0-9]+ ‘.’ [0-9]*)
([0-9]* ‘.’ [0-9]+)
|
)
Espressioni
Espressioni
regolari
regolari
gli apici consentono di distinguere un carattere in ingresso (‘+’) da un
operatore (+).
6
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.
Versione 1.2 © 2000 Marco Torchiano
lex - un generatore di analizzatori
lessicali
☛ 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.
☛ numero intero positivo
✏ (‘+’|’-’)?
Laboratorio di Compilatori
a.a 1999/2000
8
Lex
Lex
Programma
Programma
C
C
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.
2
Politecnico di Torino
9
Laboratorio di Compilatori
a.a 1999/2000
Espressioni regolari in Lex
11
Espressioni regolari in Lex
...continua...
...continua...
☛ Il carattere di fine riga viene descritto dal simbolo \n.
☛ I caratteri non alfabetici possono essere anche
descritti facendoli precedere dal carattere \
☛ Il carattere di tabulazione viene descritto dal simbolo
\t.
☛ L’operatore ? indica l’espressione precedente è
opzionale:
✏ l’espressione xyz\+\+ rappresenta la sequenza ‘x’ ‘y’ ‘z’
‘+’ ‘+’ nel testo di ingresso.
☛ Le classi di caratteri vengono descritte mediante gli
operatori []:
✏ ab?c indica sia la sequenza ac che abc.
✏ l’espressione [0123456789] rappresenta una cifra nel testo di
ingresso.
☛ L’operatore * indica l’espressione precedente può
essere ripetuta 0 o più volte:
☛ Nel descrivere classi di caratteri, il segno - indica
una gamma di caratteri:
✏ ab*c indica tutte le sequenze che iniziano per a, terminano per c
e hanno all’interno un numero qualsiasi di b.
✏ l’espressione [0-9] rappresenta una cifra nel testo di ingresso.
10
Espressioni regolari in Lex
12
Espressioni regolari in Lex
...continua...
☛ 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.
☛ L’insieme di tutti i caratteri eccetto il fine riga (new
line) viene descritto mediante il simbolo “.”.
Versione 1.2 © 2000 Marco Torchiano
...continua
☛ 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.
☛ L’operatore
espressioni:
|
indica
un’alternativa
tra
due
✏ 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.
3
Politecnico di Torino
13
Azioni associate alle espressioni
regolari
continua...
15
Azioni associate alle espressioni
regolari
...continua
☛ 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.
Versione 1.2 © 2000 Marco Torchiano
La struttura di un programma
sorgente 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.
☛ 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 ‘;’.
14
Laboratorio di Compilatori
a.a 1999/2000
16
Definizioni
continua...
☛ 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”);
4
Politecnico di Torino
17
18
Definizioni
Laboratorio di Compilatori
a.a 1999/2000
...continua
19
Risoluzione delle ambiguità lessicali
☛ 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 ‘%}’.
☛ Esistono due tipi di ambiguità lessicali:
☛ Tutte le righe presenti nella sezione delle procedure
(la terza) del programma sorgente sono ricopiate nel
file C generato da Lex.
☛ 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.
Esercizi
☛ Si scriva 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.
Versione 1.2 © 2000 Marco Torchiano
✏ 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.
20
Esempio
☛ Dato il file
%%
for
format
[a-z]+
{return FOR_CMD;}
{return FORMAT_CMD;}
{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
Politecnico di Torino
21
Risoluzione delle ambiguità lessicali
a.a 1999/2000
23
☛ 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;}
’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 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.
Versione 1.2 © 2000 Marco Torchiano
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.
cerca di riconoscere il secondo apice il più
lontano possibile: cosi, dato il seguente ingresso
22
Laboratorio di Compilatori
24
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.
6
Politecnico di Torino
25
Dipendenza dal contesto destro
27
☛ 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”.
Dipendenza dal contesto sinistro
☛ È 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.
Versione 1.2 © 2000 Marco Torchiano
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
☛ 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.
26
Laboratorio di Compilatori
a.a 1999/2000
✏ alla seconda miglior regola che riconosce lo stesso input, oppure
✏ ad una regola che riconosca un prefisso dello stesso input.
28
Esempio
☛ Il seguente programma gestisce pseudo-commenti
del tipo // $var+
int flag=0;
%%
“//”
{flag=1; /* begin of comment */}
\n
{flag=0; /* \n terminates comment */}
“ “
/* ignore blanks*/
\t
/* and tabs */
\$[a-zA-Z]+[-+] {
if(flag==1) process(yytext);
else REJECT;}
...
other rules
7
Politecnico di Torino
29
Uso di condizioni iniziali sulle
regole (stati inclusivi)
31
%s comment
%%
<comment>\$[a-zA-Z]+[-+] {process(yytext);}
“//”
{BEGIN(comment);}
\n
{BEGIN(INITIAL);}
“ “
/* ignore blanks*/
\t
/* and tabs */
...
other rules
sono attive solo quando l’analizzatore si trova
nello stato state.
☛ Gli stati possibili devono essere definiti nella sezione
delle dichiarazioni mediante la parola-chiave %s.
☛ Lo stato di default è lo stato 0 o INITIAL.
☛ Si entra in uno stato quando viene eseguita l’azione
BEGIN(state);
Uso di condizioni iniziali sulle
regole (stati inclusivi)
continua
☛ Quando uno stato viene attivato, le regole dello stato
si aggiungono (or-inclusivo) alle regole base
dell’analizzatore.
☛ Tale stato rimane attivo fino a che non se ne attiva
un altro. Per tornare alla situazione iniziale si deve
eseguire il comando
BEGIN(0); oppure BEGIN(INITIAL);
☛ Una regola può essere preceduta dal nome di più
stati, separati da virgola, per indicare che tale regola
è attiva in ciascuno di essi.
Versione 1.2 © 2000 Marco Torchiano
Esempio
☛ Il seguente programma gestisce pseudo-commenti
del tipo // $var+
☛ Le regole la cui espressione regolare inizia con
<state>
30
Laboratorio di Compilatori
a.a 1999/2000
32
Uso congiunto di più analizzatori
lessicali (stati esclusivi)
☛ È possibile raggruppare un insieme di regole
all’interno di uno stato esclusivo.
☛ Quando l’analizzatore si trova in uno stato esclusivo:
✏ le regole di default risultano disabilitate,
✏ sono attive solo le regole esplicitamente attivate nello stato.
☛ In tal modo è possibile specificare dei “minianalizzatori” che esaminano porzioni particolari del
flusso di caratteri in ingresso, quali ad esempio i
commenti o le stringhe.
☛ La parola chiave %x introduce uno stato esclusivo.
8
Politecnico di Torino
33
Eliminazione dei commenti
35
☛ Un possibile approccio all’eliminazione dei commenti del
linguaggio C:
%%
"/*"
34
Eliminazione dei commenti
☛ Questo è un analizzatore che riconosce e scarta
commenti del linguaggio C, mantenendo un
conteggio del numero della riga di ingresso.
int line_num = 1;
%x comment
%%
... altre regole
Versione 1.2 © 2000 Marco Torchiano
++line_num;
BEGIN(comment);
;
;
++line_num;
BEGIN(INITIAL);
Esercizi
☛ Si modifichi il programma precedente affinché
consenta l’eliminazione di commenti annidati.
☛ Si estenda il programma precedente in modo che
gestisca anche possibili pseudo-commenti della
forma $var[+-]
☛ Al programma precedente aggiungere l’eliminazione
di commenti brevi, introdotti dal simbolo // e terminati
dal carattere di fine riga.
{ int c;
for ( ; ; )
{
while ((c = input())!='*' &&
c != EOF )
;
/* eat up text of comment */
if ( c == '*') {
while ((c=input())=='*') ;
if ( c == '/' ) break;
/* found the end */
}
if ( c == EOF ) {
error( "EOF in comment" );
break;
}
}
}
\n
"/*"
<comment>[^*\n]*
<comment>"*"+[^*/\n]*
<comment>\n
<comment>"*"+"/"
Laboratorio di Compilatori
a.a 1999/2000
36
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.
9
Politecnico di Torino
Ingresso e uscita
37
Laboratorio di Compilatori
a.a 1999/2000
39
Il file prodotto
continua
☛ Negli analizzatori costruiti tramite il programma flex,
per motivi di efficienza, vengono letti blocchi di
caratteri dal file di ingresso invece che singoli valori.
Questo comportamento può essere modificato
alterando la definizione della macro YY_INPUT.
☛ Tale macro è definita come segue:
☛ Per default, i file yyin e yyout sono inizializzati
rispettivamente a stdin e stdout.
☛ Il programmatore può alterare questa situazione reinizializzando tali variabili globali.
YY_INPUT(buf,result,max_size)
☛ Essa pone nel vettore di caratteri buf un blocco la
cui lunghezza è compresa tra 1 e max_size.
☛ La variabile intera result contiene il numero di
caratteri letti oppure la costante YY_NULL per
indicare la fine del file di ingresso.
Il file generato
38
yylex()
trovato
EOF
scansione del
file di ingresso
azione
esegue
return
1
inizializzazione
del file yyin
(default=stdin)
40
2
elaborazione
del simbolo
Versione 1.2 © 2000 Marco Torchiano
no
si
reinizializzazione
del file yyin
3
continua
☛ Per far avvenire la lettura un carattere alla volta si
può utilizzare il codice seguente:
yywrap()
altri
file?
Il file prodotto
fine dell’analisi
%{
#undef YY_INPUT
#define YY_INPUT(buf,result,max_size) \
{\
int c = getchar(); \
result = (c == EOF) ? YY_NULL : (buf[0] = c, 1); \
}
%}
☛ Questo non si deve fare per gli analizzatori scritti con
Lex, in cui la routine input() richiama direttamente
la routine getchar().
10
Politecnico di Torino
41
Il file prodotto
continua
43
☛ Quando l’analizzatore incontra la fine del file di
ingresso, chiama la funzione yywrap().
☛ Se essa ritorna il valore 0, Lex assume che il file
yyin sia stato re-inizializzato e procede con l’analisi.
☛ Altrimenti, l’analizzatore assume che non ci siano
altri file da scandire e ritorna al chiamante
restituendo il valore 0.
☛ La funzione yywrap() può essere usata per
stampare tabelle e statistiche circa l’analisi appena
terminata. Su molti sistemi, per default, yywrap()
restituisce sempre il valore 1.
42
Regole di fine file
☛ La regola speciale <<EOF>> introduce le azioni da
intraprendere alla fine del file quando yywrap
restituisce non-zero.
☛ Con tale azione è possibile passare ad un altro file o
terminare l’esecuzione.
☛ Questa regola può essere utile unitamente alle start
condition per intercettare simboli con delimitatori non
bilanciati:
\”
...
<quote><<EOF>>
Laboratorio di Compilatori
a.a 1999/2000
{ BEGIN(quote); }
{ error(“EOF in string”); }
Versione 1.2 © 2000 Marco Torchiano
Inclusione di file
☛ Negli analizzatori generati tramite il programma
flex, dove il testo in ingresso è fortemente
bufferizzato,
è
necessario
adottare
alcuni
accorgimenti per sospendere l’elaborazione del file di
ingresso al fine di scandire il contenuto di un
secondo file che si intende includere nel primo.
☛ Per gestire questo tipo di situazioni, viene fornito un
meccanismo per creare ed attivare nuovi buffer di
ingresso.
44
Inclusione di file
continua
☛ La funzione
YY_BUFFER_STATE
yy_create_buffer(FILE *file, int size)
crea un nuovo buffer, di dimensione size, relativo al
file file.
☛ La funzione
void yy_switch_to_buffer(YY_BUFFER_STATE
new_buf)
sostituisce il buffer attuale di ingresso con il
buffer new_buf.
☛ Si cancella un buffer con la chiamata
void yy_delete_buffer( YY_BUFFER_STATE buf)
11
Politecnico di Torino
45
Inclusione di file
continua
47
☛ Esempio di analizzatore che elabora file inclusi con
annidamento.
Inclusione di file
<incl>[ \t]*
continua
/* eat the whitespace */
<incl>[^ \t\n]+ {/* process filename */
if (include_stack_ptr>=MAX_INCLUDE_DEPTH) {
fprintf( stderr, "Nesting too deep" );
exit( 1 );
}
include_stack[include_stack_ptr++]=
YY_CURRENT_BUFFER;
yyin = fopen( yytext, "r" );
if ( ! yyin ) error( ... );
yy_switch_to_buffer(
yy_create_buffer(yyin,YY_BUF_SIZE));
BEGIN(INITIAL);
}
Versione 1.2 © 2000 Marco Torchiano
Inclusione di file
continua
%%
int yywrap()
{
if ( --include_stack_ptr < 0 )
return(1);
else {
yy_switch_to_buffer(
include_stack[include_stack_ptr]);
return(0);
}
}
%x incl
%{
#define MAX_INCLUDE_DEPTH 10
YY_BUFFER_STATE include_stack[MAX_INCLUDE_DEPTH];
int include_stack_ptr = 0;
%}
%%
include
BEGIN(incl);
[a-z]+
ECHO;
[^a-z\n]*\n?
ECHO;
46
Laboratorio di Compilatori
a.a 1999/2000
48
Uso della funzione yylex()
☛ Per un normale uso all’interno di un compilatore, è
opportuno che la funzione yylex() restituisca il
controllo ogni volta che incontra un identificatore.
☛ A tale scopo è necessario assegnare a ciascuna
classe di simboli che si intende riconoscere un
opportuno valore.
☛ Poiché tale valore deve essere conosciuto anche dal
riconoscitore sintattico, è pratica comune creare un
file di definizioni dei simboli del linguaggio ed
includere tale file nella sezione delle dichiarazioni.
☛ I valori assegnati sono interi, in genere > 256.
12
Politecnico di Torino
49
Lex: parametri ed opzioni
51
☛ Lex viene invocato con la seguente sintassi:
☛ Si ottiene come risultato il file lex.yy.c.
☛ Tra le opzioni, le più comuni sono:
50
-t copia il programma generato su stdout,
-v stampa una statistica circa il programma compilato,
-p stampa informazioni sulle performance dello scanner,
-ooutput genera il file output invece di lex.yy.c ,
-B(-I) genera uno scanner Batch (Interattivo),
-f ottimizza le prestazioni dello scanner,
-i genera uno scanner case insensitive,
-d attiva i messaggi di debug nello scanner.
Linguaggi CF e Riconoscitori
☛ Grammatiche CF
✏ Alberi di derivazione
✏ Ambiguità
☛ Parser bottom-up
☛ Introduzione a YACC
✏
✏
✏
✏
Definizione dei simboli
Codifica della grammatica
Integrazione con l’analizzatore lessicale
Formato del programma prodotto da YACC
☛ Ambiguità e conflitti
✏ Conflitti shift-reduce e reduce-reduce
✏ Definizione di operatori e gestione delle priorità in YACC
✏ Gestione degli errori sintattici
Versione 1.2 © 2000 Marco Torchiano
Grammatiche context free
continua...
☛ Il concetto di ricorsione è fondamentale nella
scrittura di grammatiche context-free.
☛ È lo strumento per descrivere delle sequenze lunghe
a piacere.
☛ Lista (grammatica ricorsiva sinistra) :
lex [opzioni] file.lex
✏
✏
✏
✏
✏
✏
✏
✏
Laboratorio di Compilatori
a.a 1999/2000
✏ List → List ‘,’ Element
✏ List →=Element
☛ Lista (grammatica ricorsiva destra) :
✏ List → Element Tail
✏ Tail →==’,’
== Element Tail
✏ Tail →==ε
==ε
52
Alberi di derivazione
☛ Data una sequenza di simboli che appartiene al
linguaggio definito da una data grammatica, è
possibile
definire
l’albero
di
derivazione
dell’espressione nel seguente modo:
✏ la radice dell’albero è il simbolo iniziale della grammatica
✏ ciascun nodo dell’albero è etichettato con un simbolo della
grammatica
✏ le foglie dell’albero sono etichettate con simboli terminali
✏ la sequenza delle foglie, lette da sinistra a destra, corrisponde alla
sequenza di ingresso
✏ un nodo n domina una sequenza ordinata di nodi n1, ..., nm, se e
solo se la grammatica contiene la regola
n → n1 ... nm
13
Politecnico di Torino
53
Alberi di derivazione
55
List → el Tail
Tail →==’,’
== el Tail
Tail →==ε
==ε
List → List ‘,’ el
List →=el
if (i=1) then if (j=2) then a:=0 else a:=1
data la grammatica:
Tail
List
✏
✏
✏
✏
✏
✏
Tail
List
Tail
54
‘,’
el
‘,’
el
el
‘,’
el
‘,’
el
ε
Costrutti Pascal-like
☛ Un frammento di programma di un linguaggio simile
al Pascal può essere definito dalle seguenti regole:
✏
✏
✏
✏
✏
✏
✏
✏
✏
✏
P → ‘begin’ Is ‘end’ ‘.’
Is → Is I | ε
I → ‘if’ C ‘then’ I ‘else I ‘;’
I → ‘while’ C ‘do’ I’;’
I → ‘repeat’ Is ‘until’ C ‘;’
I →==var
‘:=‘ E ‘;’
==
I →==‘begin’
==
Is ‘end’
C → E Op E
Op → ‘=‘ | ‘>‘ | ‘<‘ | ‘>=‘ | ‘<=‘ | ‘~=‘
E → algebric expression
Versione 1.2 © 2000 Marco Torchiano
Ambiguità
☛ Una grammatica si dice ambigua se esiste almeno
una sequenza di simboli del linguaggio per cui
esistono due o più alberi di derivazione distinti.
☛ Esercizio: trovare gli alberi di derivazione per
List
List
el
Laboratorio di Compilatori
a.a 1999/2000
56
S→I
I → ‘if’ C ‘then’ I
I → ‘if’ C ‘then’ I ‘else I
I →=var ‘:=‘ E
C → ‘(‘ E ‘=‘ E ‘)’
E → algebric expression
Espressioni algebriche
☛ La grammatica che descrive le espressioni
algebriche è context-free ed usa la ricorsione:
✏
✏
✏
✏
✏
✏
✏
✏
✏
S→E
E → E ‘+’ T
E → E ‘-’ T
E→T
T → T ‘*’ F
T → T ‘/’ F
T→F
F → ‘(‘ E ‘)’
F → number
14
Politecnico di Torino
57
Esempio
59
58
✏
✏
✏
✏
I →=I1
I1 → ‘if’ C ‘then’ I1
I1 → ‘if’ C ‘then’ I2 ‘else’ I1
I1 →=I3
I2 → ‘if’ C ‘then’ I2 ‘else’ I2
I2 →=I3
I3 →=var ‘:=‘ E
Esercizi
List id_list
List ε
id_list id_list ‘;’ id
id_list id
List List ‘;’ id
List id
List ε
☛ Una lista , eventualmente vuota, di identificatori
terminati da “;”
✏ List List id ‘;’
✏ List ε
60
☛ Scrivere due grammatiche di liste di 0 o più
identificatori aventi “;” l’una come terminatore, l’altra
come separatore.
☛ Scrivere una grammatica non ambigua che descriva
le espressioni logiche e che assegni l’opportuna
priorità agli operatori ‘and’, ‘or’ e ‘not’.
☛ Costruire la grammatica per i test
con un
delimitatore alla fine del costrutto e verificare che
non è ambigua sull’esempio:
Separatori e terminatori
☛ Una lista, eventualmente vuota, di identificatori
separati da “;”
☛ I simboli T e F della grammatica algebrica servono a
togliere l’ambiguità sulla priorità degli operatori ‘+’ e
‘-’ rispetto agli operatori ‘*’ e ‘/’.
☛ La grammatica per i costrutti del tipo if-then-else può
essere resa non ambigua come segue:
✏
✏
✏
✏
✏
✏
✏
Laboratorio di Compilatori
a.a 1999/2000
Riconoscitori e analizzatori
sintattici
☛ Data una grammatica non ambigua ed una sequenza
di simboli in ingresso, un riconoscitore è un
programma che verifica se la sequenza appartiene o
no al linguaggio definito dalla grammatica.
☛ Un analizzatore sintattico (parser) è un programma
che è in grado di associare alla sequenza di simboli
in ingresso il relativo albero di derivazione.
☛ Gli algoritmi di analisi possono essere
✏ top-down (dalla radice alle foglie)
✏ bottom-up (dalle foglie alla radice).
if C then if C then A end else A end
Versione 1.2 © 2000 Marco Torchiano
15
Politecnico di Torino
61
Riconoscitori Bottom-Up
63
La tecnica shift-reduce
continua…
☛ Si usa uno stack, inizialmente vuoto, per
memorizzare i simboli già riconosciuti.
☛ La sequenza dei simboli sullo stack, seguita dai
simboli in ingresso non ancora trattati costituisce
sempre una forma sentenziale destra (se l’input è
corretto).
☛ I token vengono immessi sullo stack (azione di shift),
fino a che la cima dello stack non contiene un
handle: quando ciò avviene, l’handle viene ridotto
(reduce) con il non-terminale relativo.
☛ Si ha successo se esaminati tutti i simboli in
ingresso, lo stack contiene solo il simbolo distintivo
della grammatica.
Versione 1.2 © 2000 Marco Torchiano
La tecnica shift-reduce
…continua
☛ Per un corretto funzionamento bisogna:
☛ Il problema principale di un analizzatore top-down è
decidere quale produzione deve essere usata per
espandere un dato simbolo non terminale.
☛ Nei riconoscitori bottom-up il problema è capire
quando si sono incontrati tutti i simboli del lato
sinistro di una produzione così da poterli sostituire
con il relativo non terminale.
☛ Il problema diventa più complesso se la grammatica
non è ε-free.
☛ Se la grammatica è LR(k) si può usare un
analizzatore shift-reduce.
62
Laboratorio di Compilatori
a.a 1999/2000
✏ comprendere quando si è raggiunta la fine di un handle;
✏ determinare la lunghezza dell’handle;
✏ decidere quale non terminale sostituire all’handle nel caso in cui
ci siano più produzioni con lo stesso lato destro.
☛ Si risolve il problema definendo un insieme di stati
del parser e calcolando due tabelle
✏ Action Table: dice quale azione deve eseguire il parser (shift,
reduce, terminate, error) in funzione dello stato corrente.
✏ Goto Table: descrive in quale stato deve portarsi il parser.
☛ Il modo con cui vengono calcolati stati e tabelle
porta a soluzioni diverse, sia in termini di complessità
che di completezza.
64
LALR(1)
2 exp
S exp .
exp exp . ‘+’ T
‘+’
exp exp ‘+’ . T
4
5
T
3
exp T .
NUM
NUM
T
exp exp ‘+’ T .
0
S . exp
exp . exp ‘+’ T
exp . T
exp . NUM
T
NUM .
1
SS
:: exp
exp
exp
exp
:: exp
exp '+'
'+'TT
|| TT
TT
:: NUM
NUM
16
Politecnico di Torino
65
LALR(1)
T
0
3
+
NUM
NUM
4
1
T
2
Laboratorio di Compilatori
a.a 1999/2000
67
exp
NUM + NUM
eof
0
shift, go to state 1
5
LALR(1)
T
0
3
+
NUM
NUM
4
1
T
2
exp
NUM + NUM
3
0
reduce
reduce (( exp
exp
TT ))
5
0
NUM + NUM
1
0
T
LALR(1)
T
0
3
+
NUM
NUM
4
1
T
2
68
exp
NUM + NUM
1
0
eof
reduce
reduce (( TT
NUM
NUM))
5
0
Versione 1.2 © 2000 Marco Torchiano
NUM + NUM
2
0
eof
NUM + NUM
eof
shift, go to state 4
T
3
0
eof
exp
exp
NUM
NUM + NUM
LALR(1)
T
0
3
+
NUM
NUM
4
1
T
2
2
0
5
T
exp
exp
eof
NUM
66
eof
T
4
2
0
NUM + NUM
eof
NUM
17
Politecnico di Torino
69
2
+
LALR(1)
exp
T
0
3
NUM
NUM
4
1
Laboratorio di Compilatori
a.a 1999/2000
71
4
2
0
NUM + NUM
eof
T
2
+
LALR(1)
exp
T
0
3
NUM
NUM
4
1
T
shift, go to state 1
5
5
4
2
0
1
4
2
0
T
NUM + NUM
T
NUM +
LALR(1)
T
0
3
+
NUM
NUM
4
1
T
2
exp
72
1
4
2
0
NUM + NUM
eof
reduce
reduce (( TT
NUM
NUM))
4
T
2
0
5
exp
T
T
NUM
NUM
5
4
2
0
Versione 1.2 © 2000 Marco Torchiano
NUM + NUM
eof
0
exp
exp
eof
NUM
70
eof
reduce
reduce (( exp
exp
exp
exp ‘+’
‘+’TT ))
5
exp
exp
NUM + NUM
T
NUM
2
0
NUM + NUM
eof
Introduzione a Yacc
☛ Yacc è un generatore di analizzatori sintattici che
trasforma la descrizione di una grammatica contextfree LALR(1) in un programma C che riconosce ed
analizza la grammatica stessa.
☛ Oltre alle regole sintattiche, è possibile specificare
quali
azioni
devono
essere
eseguite
in
corrispondenza del riconoscimento dei vari simboli
della grammatica.
☛ È necessario integrare il parser così generato con un
analizzatore lessicale: alcune convenzioni ne
semplificano sensibilmente l’integrazione con lo
scanner generato da Lex.
18
Politecnico di Torino
73
Il formato del file di ingresso
75
✏ le dichiarazioni,
✏ le regole,
✏ le procedure
☛ Le sezioni sono separate dal simbolo ‘%%’
☛ La prima e l’ultima sezione possono essere vuote.
☛ Se l’ultima sezione è vuota, il secondo separatore
può essere omesso.
☛ Possono essere introdotti commenti racchiusi dai
simboli ‘/*’ e ‘*/’.
Dichiarazioni
☛ Un file Yacc inizia con la sezione delle dichiarazioni.
☛ In essa vengono definiti
✏
✏
✏
✏
✏
i simboli terminali,
il simbolo distintivo della grammatica,
le regole di associatività e precedenza tra gli operatori,
alcune informazioni semantiche,
codice C racchiuso tra i simboli ‘%{’ e ‘%}’.
☛ I simboli non terminali della grammatica sono nomi:
✏ un nome è formato da lettere, ‘_’, ‘.’ e cifre (non iniziali).
Dichiarazioni
continua
☛ La parola chiave ‘%token’ definisce una lista di nomi
di terminali, separati tra loro da uno o più spazi.
☛ Tale parola chiave può comparire più volte in questa
sezione.
☛ I letterali non sono dichiarati.
☛ La parola chiave ‘%start’ definisce il simbolo
distintivo della grammatica. È lecita una sola
occorrenza di questa parola chiave.
☛ Non è necessario dichiarare i simboli non terminali tranne quando si vuole associare loro un valore
semantico.
☛ Il file di ingresso su cui opera Yacc è formato da tre
sezioni:
74
Laboratorio di Compilatori
a.a 1999/2000
76
Codifica della grammatica
☛ La sezione delle regole è costituita da una o più
regole del tipo:
NonTerminale : CorpoDellaRegola ;
☛ dove NonTerminale è un nome, e CorpoDellaRegola
è una sequenza di 0 o più nomi o letterali.
☛ Se per un dato non terminale esistono più
produzioni, queste possono essere raggruppate tra
loro e separate dal carattere ‘|’.
☛ I simboli terminali sono nomi o letterali:
✏ un letterale è un singolo carattere racchiuso tra apici.
Versione 1.2 © 2000 Marco Torchiano
19
Politecnico di Torino
77
Esempio
79
Laboratorio di Compilatori
Formato del programma generato
%token integer
%start Expression
☛ Il parser generato fa capo alla funzione
int yyparse()
%%
☛ Tale funzione ritorna 1 se è stato incontrato un errore
nel testo in ingresso, altrimenti ritorna il valore 0.
☛ Bisogna definire il corpo per la funzione
void yyerror(char *)
Expression
Term
Factor
78
a.a 1999/2000
:
|
;
:
|
;
:
|
;
Expression ‘+’ Term
Term
Term ‘*’ Factor
Factor
che viene invocata quando si incontra un errore.
☛ Inoltre, il programma generato non contiene il
main(),
che
deve
essere
definito
dal
programmatore.
integer
‘(’ Expression ‘)’
La sezione delle procedure
☛ Tutto ciò che segue il secondo delimitatore ‘%%’
forma la sezione delle procedure.
☛ Questa porzione di file viene ricopiata tale e quale in
uscita.
☛ All’interno di tale sezione vengono comunemente
posti:
✏ le procedure semantiche usate nel corso dell’analisi,
✏ l’analizzatore lessicale,
✏ il corpo principale del programma.
Versione 1.2 © 2000 Marco Torchiano
80
Integrazione con l’analizzatore
lessicale
☛ Il parser generato presuppone l’esistenza di una
funzione che realizzi l’analisi lessicale. Tale funzione
deve restituire un numero intero positivo che
definisce il token letto oppure 0, se è stata raggiunta
la fine del file di ingresso.
☛ La funzione di analisi lessicale è così definita:
int yylex();
☛ Il valore semantico, eventualmente associato al
simbolo terminale, deve essere memorizzato nella
variabile yylval. Tale variabile è allocata all’interno
di Yacc.
20
Politecnico di Torino
81
Integrazione con l’analizzatore
lessicale
continua
83
☛ Parser e scanner devono accordarsi sui valori
associati ai token. Tali valori possono essere scelti
da Yacc o dal programmatore.
☛ I terminali introdotti come letterali sono associati al
codice ASCII del carattere.
☛ Quando si introduce un terminale per mezzo della
parola chiave %token, yacc associa a tale simbolo
un valore intero maggiore di 256, mediante il
costrutto ‘#define’ del pre-processore C.
☛ Se il nome del token è seguito da un numero intero,
esso viene interpretato come valore da associare al
token stesso.
82
Integrazione con l’analizzatore
lessicale
continua
☛ Si possono integrare i programmi generati da Lex e
da Yacc in due modi:
✏ in fase di compilazione
✏ in fase di link.
☛ Nel primo caso, il file C prodotto da Lex (il file
‘lex.yy.c’) viene incluso nel programma generato da
Yacc mediante la direttiva ‘#include’ posta nella
sezione delle procedure.
☛ Ciò rende le definizione dei token visibili direttamente
in fase di compilazione.
Versione 1.2 © 2000 Marco Torchiano
Laboratorio di Compilatori
a.a 1999/2000
Esempio: inclusione del sorgente
language.y
language.y
Yacc
Yacc
#include “lex.yy.c”
language.l
language.l
Lex
Lex
lex.yy.c
lex.yy.c
y.tab.c
y.tab.c
#include “lex.yy.c”
include
include
cc
cc
parser.obj
parser.obj
84
Integrazione con l’analizzatore
lessicale
continua
☛ Nel secondo caso, si chiede a Yacc (mediante
l’opzione ‘-d’) di generare un file (‘y.tab.h’) che
contiene le definizioni dei token.
☛ Tale file deve essere incluso dallo scanner in modo
che siano definiti i valori dei simboli.
☛ Di conseguenza, il file lex.yy.c, prodotto da Lex,
può essere compilato dopo aver generato y.tab.c e
y.tab.h tramite Yacc.
☛ Scanner e parser vengono quindi compilati
separatamente ed integrati in fase di link.
21
Politecnico di Torino
85
Esempio: compilazione separata
language.y
language.y
Yacc
Yacc
y.tab.c
y.tab.c
cc
cc
Conflitti shift-reduce
87
parser.obj
parser.obj
S
scanner.obj
scanner.obj
y.tab.h
y.tab.h
include
include
Il prossimo simbolo in ingresso è ‘else’.
if
Possono succedere due cose:
86
Lex
Lex
E
#include “y.tab.h”
lex.yy.c
lex.yy.c
Ambiguità e conflitti in Yacc
☛ Se la grammatica è ambigua, o se non è LALR(1),
possono verificarsi dei conflitti.
☛ Ciò significa che l’analizzatore deve scegliere tra più
azioni alternative plausibili.
☛ Il problema viene, di solito, risolto modificando la
grammatica per renderla non ambigua oppure
fornendo indicazioni a Yacc su come comportarsi in
caso di ambiguità.
☛ La seconda ipotesi richiede una comprensione
adeguata dell’algoritmo di analisi, per evitare di
generare comportamenti scorretti.
Versione 1.2 © 2000 Marco Torchiano
✏ ridurre le prime quattro voci dello stack, secondo la
produzione 1;
✏ introdurre ‘else’ nello stack secondo quanto previsto
dalla produzione 2.
then
cc
cc
1) S →==if E then S
2) S →==if E then S else S
3) S →==var = E
E
if
language.l
language.l
Top of
Stack
then
-d
-d
#include “y.tab.h”
Laboratorio di Compilatori
a.a 1999/2000
☛ In queste situazioni, Yacc decide, in mancanza
di altri suggerimenti, di eseguire lo shift.
Conflitti reduce-reduce
88
b
a
Top of
Stack
1) S →==a B
2) S →==B
3) B →==a b
4) B →== b
Il prossimo simbolo in ingresso è ‘$’.
Possono succedere due cose:
✏ ridurre le prime due voci dello stack, secondo la
produzione 3;
✏ ridurre la prima voce secondo quanto previsto dalla
produzione 4.
☛ In queste situazioni, Yacc decide, in mancanza
di altri suggerimenti, di ridurre la regola che è
stata definita per prima (la n°3).
22
Politecnico di Torino
89
Definizione degli operatori e
gestione delle priorità
91
E →==E
== ‘+’ E
E →==E
== ‘*’ E
E →==’(‘
== E ‘)’
E →==integer
==
☛ Questa grammatica è fortemente ambigua.
90
Operatori
☛ La regola 1 (così come la 2) è ambigua in quanto
non specifica l’associatività dell’operatore ‘+’ (‘*’).
☛ Inoltre le regole 1 e 2 non specificano la precedenza
tra gli operatori ‘+’ e ‘*’.
☛ E’ possibile suggerire a Yacc come comportarsi
aggiungendo due informazioni nella sezione delle
dichiarazioni.
☛ La parola chiave ‘%left’ introduce un operatore
associativo a sinistra, ‘%right’ introduce un
operatore associativo a destra, ‘%nonassoc’
introduce un operatore non associativo.
☛ L’ordine con cui gli operatori sono dichiarati è inverso
alla loro priorità.
Versione 1.2 © 2000 Marco Torchiano
Regole di risoluzione dell’ambiguità
☛ Ad ogni regola che contiene almeno un terminale
definito come operatore, Yacc associa la precedenza
ed l’associatività dell’operatore più a destra.
☛ Se la regola è seguita dalla parola chiave ‘%prec’, la
precedenza
e
l’associatività
sono
quelle
dell’operatore specificato.
☛ In caso di conflitto shift-reduce, viene favorita
l’azione adatta alla regola con la precedenza
maggiore.
☛ Se la precedenza è la stessa, si usa l’associatività:
sinistra implica reduce, destra shift.
☛ In certi casi la grammatica può essere resa
volutamente ambigua al fine di limitare il numero
delle regole.
☛ È necessario però fornire indicazioni sulla risoluzione
delle ambiguità.
☛ Un caso tipico è dato dalle espressioni algebriche:
1)
2)
3)
4)
Laboratorio di Compilatori
a.a 1999/2000
92
Esempio
%token integer
%left ‘+’ ‘-’
%left ‘*’ ‘/’
%left uminus
%%
E : E ‘+’ E
| E ‘-’ E
| E ‘*’ E
| E ‘/’ E
| ‘-’ E
| ‘(’ E ‘)’
| integer
;
/* lowest priority */
/* highest priority */
%prec uminus
23
Politecnico di Torino
93
Gestione degli errori sintattici
a.a 1999/2000
continua...
95
☛ In genere quando un parser incontra un errore non
dovrebbe terminare brutalmente l’esecuzione
☛ Per default, un parser generato da YACC, quando
incontra un errore:
☛ Se il precedente simbolo di look-ahead è
accettabile procede nell’analisi.
☛ Altrimenti il parser continua a leggere e scartare
simboli finché non ne trova uno accettabile
✏ segnala, tramite yyerror(), un “parse error”
✏ restituisce il valore 1
☛ Il comportamento di default può essere accettabile
per un semplice parser interattivo.
Gestione degli errori sintattici ...continua...
☛ Il simbolo predefinito ‘error’ indica una
condizione di errore. Esso può essere usato
all’interno della grammatica per consentire al
parser di riprendere l’esecuzione dopo un
eventuale errore.
stmts : /* empty statement */
| stmts ‘\n’
| stmts exp ‘\n’
| stmts error ‘\n’
;
Versione 1.2 © 2000 Marco Torchiano
Gestione degli errori sintattici ...continua...
☛ Quando il parser generato da Yacc incontra un
errore, comincia a svuotare lo stack fino a che
incontra uno stato in cui il simbolo ‘error’ è
lecito.
☛ Fa lo shift del simbolo error.
✏ Un compilatore in genere cerca di provvedere alla situazione per
poter analizzare il resto del file, in modo da segnalare il maggior
numero possibile di errori
94
Laboratorio di Compilatori
96
Gestione degli errori sintattici ...continua...
☛ Una semplice strategia per la gestione degli errori è
quella di saltare lo statement corrente:
stmt : error ‘;’
☛ A volte può essere utile recuperare un delimitatore di
chiusura corrispondente ad uno di apertura:
primary : ‘(‘ expr ‘)’
| ‘(‘ error ‘)’
24
Politecnico di Torino
97
Gestione degli errori sintattici
...continua
99
☛ Le strategie di recupero degli errori si basano su
scommesse; quando si perde la scommessa il
rischio è che un errore sintattico ne produca altri
spuri.
☛ Per limitare la proliferazione di errori spuri, dopo il
riconoscimento di un errore, la segnalazione è
sospesa finché non vengono shiftati almeno tre
simboli consecutivi.
☛È
possibile
riattivare
immediatamente
la
segnalazione degli errori utilizzando la macro
‘yyerrok’.
98
Regole semantiche
☛ Traduzione guidata dalla sintassi
☛ La semantica in YACC
✏
✏
✏
✏
✏
✏
Definizione di valori semantici
Calcolo di attributi sintetizzati
Sintesi di attributi in Lex
Calcolo di attributi ereditati
Trasformazione delle grammatiche
Gestione degli errori semantici
☛ Abstract Syntax Tree
Versione 1.2 © 2000 Marco Torchiano
Laboratorio di Compilatori
a.a 1999/2000
Definizioni Guidate dalla Sintassi
☛ Sono una generalizzazione delle grammatiche
context-free.
☛ Ad ogni simbolo viene associato un insieme di
attributi:
✏ sintetizzati: calcolati in base al valore degli attributi dei figli di un
nodo nell’albero di derivazione,
✏ ereditati: calcolati in base al valore degli attributi dei nodi fratelli
e del nodo padre nell’albero di derivazione,
☛ Ad ogni produzione viene associato un insieme di
regole semantiche che specificano come vengono
calcolati gli attributi.
100
Attributi sintetizzati
☛ Se una definizione guidata dalla sintassi usa solo attributi
sintetizzati è detta definizione ad attributi S.
☛ È possibile calcolare i valori degli attributi di una
definizione ad attributi S bottom-up, dalle foglie alla
radice dell’albero di derivazione.
E
E1 ‘+’ T
E.value = E1.value + T.value
E
T
E.value = T.value
T
number
T.value = number.value
25
Politecnico di Torino
101
a.a 1999/2000
Attributi ereditati
103
102
L ‘:’ T ‘;’
L.type = T.type
L
L1 ‘,’ id
L1.type = L.type
new_var(id.name,L.type)
L
id
new_var(id.name,L.type)
T
‘integer’
T.type = type_int
Attributi L
☛ L’ordine di valutazione degli attributi dipende
dall’ordine con cui vengono creati o visitati i nodi
dell’albero di derivazione.
☛ Comunemente i parser seguono l’ordine di una visita
in profondità dell’albero.
☛ Una grammatica è detta ad attributi L se è possibile il
calcolo dei valori degli attributi con una visita in
profondità dell’albero di derivazione.
☛ In tali grammatiche si ha una propagazione delle
informazioni da sinistra a destra (dell’albero di
derivazione).
Versione 1.2 © 2000 Marco Torchiano
La semantica in Yacc
continua...
☛ Accanto allo stack che contiene i simboli sintattici,
c’è un secondo stack che contiene i valori semantici
ad essi associati.
☛ Ad ogni regola può essere associata un’azione da
eseguirsi ogni qualvolta la regola è applicata nel
processo di analisi.
☛ Tale azione provvede ad aggiornare i valori
semantici associati a ciascun simbolo.
☛ Un azione è composta da una o più istruzioni di
codice C racchiuse tra i simboli ‘{‘ e ‘}’, ed è posta
(di norma) al termine della regola.
☛ Sono utili per esprimere la dipendenza di costrutti di un
linguaggio dal contesto in cui si trovano.
D
Laboratorio di Compilatori
104
La semantica in Yacc
...continua
☛ All’interno di ogni azione, si fa riferimento ai valori
semantici associati ai successivi simboli del lato
destro della regola mediante i nomi ‘$1’, ‘$2’, ‘$3’, ...
☛ Il valore semantico associato al simbolo del lato
sinistro della regola è indicato dal nome ‘$$’.
☛ Il traduttore generato da Yacc associa ad ogni regola
che non contiene un’azione esplicita la seguente
azione di default:
{ $$ = $1; }
26
Politecnico di Torino
105
Esempio
107
%{
☛ Per default, ad ogni simbolo viene associato un solo attributo di
tipo intero.
☛ Ciò può essere cambiato ridefinendo (mediante typedef) il
simbolo YYSTYPE, oppure servendosi, nella sezione delle
dichiarazioni, dei costrutti ‘%union’ e ‘%type’ .
☛ Nel primo caso, tutti i simboli sono legati ad un valore
semantico il cui tipo è quello attribuito a YYSTYPE.
☛ In alternativa, ‘%union’ consente di definire un insieme di
interpretazioni alternative dei valori semantici contenuti nello
stack.
☛ ‘%type’ associa una data interpretazione ad uno o più simboli
non terminali.
Versione 1.2 © 2000 Marco Torchiano
typedef struct _function_desc {
int
arg_number;
char *name;
} function_desc;
%}
%union {
function_desc
function;
float
expression;
char *
id;
int
other; }
%type <expression> E T F
%type <function> Func
%type <other> Args
%%
/* ... rules here... */
☛ Un’azione può contenere istruzioni qualsiasi, anche
facenti riferimento a variabili globali:
A : B C D { printf(“%d\n”,i); } ;
Tipi dei valori semantici
Esempio
☛ ‘Func’ ha due attributi: un intero ed un puntatore a
carattere. I simboli ‘E’, ‘T’ ed ‘F’ hanno un solo attributo
di tipo reale, ‘Args’ un solo attributo di tipo intero.
☛ Data la grammatica delle espressioni algebriche, la
regola seguente associa al simbolo ‘E’ la somma o la
sottrazione dei valori degli addendi/sottraendi:
E : E ‘+’ T { $$ = $1 + $3; }
| E ‘-’ T { $$ = $1 - $3; }
;
106
Laboratorio di Compilatori
a.a 1999/2000
108
Calcolo di attributi sintetizzati
☛ Un attributo sintetizzato viene calcolato assegnando, nell’azione
associata ad una data regola, un valore alla pseudo-variabile ‘$$’.
☛ Tale valore è funzione, in genere, di uno o più degli attributi dei
simboli del lato destro della regola stessa.
☛ Se ad un simbolo sono stati associati più attributi sotto forma di
‘struct’, è possibile far riferimento a ciascuno di essi con la sintassi
$n.attr.
Func: identifier ‘(‘ Args ‘)’
{ $$.name=$1;
$$.arg_number=$3; }
;
27
Politecnico di Torino
109
Sintesi di attributi in Lex
111
☛ In un riconoscitore bottom-up, non viene riservato
spazio sullo stack semantico fino a che il
corrispondente simbolo sintattico non è riconosciuto.
☛ Ciò rende problematica la gestione degli attributi
ereditati.
☛ Se la grammatica è ad attributi L, e le regole
semantiche per il calcolo degli attributi ereditati sono
copy-rules è possibile “aggirare l’ostacolo” in
maniera semplice.
☛ Se le regole sono più complesse è possibile inserire
dei marker, cioè dei non-terminali che si espandono
in ε.
%token <expression> number
☛ L’analizzatore lessicale deve provvedere a depositare, all’atto
del riconoscimento del simbolo, il suo valore semantico nella
variabile globale esterna ‘yylval’.
☛ Se si è usato il costrutto %union, nello scanner bisogna citare
esplicitamente il campo della unione a cui ci si riferisce:
[0-9]+”.”[0-9]* {
yylval.field = atof(yytext);
return(number);}
Esercizi
Calcolo di attributi ereditati
continua...
☛ Se ad un simbolo terminale sono associati degli attributi,
bisogna definirne il tipo nella sezione delle dichiarazioni con la
sintassi seguente:
110
Laboratorio di Compilatori
a.a 1999/2000
112
Calcolo di attributi ereditati
…continua...
☛ Si scriva una grammatica per riconoscere delle
espressioni regolari tipo Lex e costruire l’albero di
derivazione relativo all’espressione riconosciuta. Si
utilizzino le primitive:
✏ treenode* create_node(char* name)
✏ void add_son(treenode* father, treenode* son)
☛ Si scriva una grammatica che riconosca sequenze di
espressioni matematiche separate dal simbolo ‘=‘ e
che stampi, in conseguenza del riconoscimento di
tale simbolo il valore dell’espressione riconosciuta.
Versione 1.2 © 2000 Marco Torchiano
☛ Una produzione che usa una copy-rule:
(1)
(2)
Decl
D1
Situazione dello
stack prima di
ridurre D1
T D1
id
id.n
D1.i = T.s
var(D1.i,id.n)
$1
contesto della regola (2)
T.s
...
28
Politecnico di Torino
Calcolo di attributi ereditati
113
Calcolo di attributi ereditati
115
...continua...
Decl
T
Decl1
type:
type:int
int
type:
type:int
int
id
name:
name:j j
114
Laboratorio di Compilatori
a.a 1999/2000
☛ Il simbolo ‘Decl1’ ha l’attributo
ereditato ‘type’.
☛ Il valore di tale attributo è
presente sullo stack semantico
(nella posizione associata a ‘T’)
prima che venga creato il
simbolo ‘Decl1’.
☛ Tuttavia esso è al di fuori del
contesto semantico della regola
relativa a ‘Decl1’.
Calcolo di attributi ereditati
...continua...
☛ È possibile accedere
ai valori semantici
precedentemente memorizzati nello stack purché se
ne conosca la posizione relativa alla regola corrente.
☛ la pseudo-variabile ‘$0’ indica il valore associato al
simbolo che precede immediatamente (nel parsing
left-to-right rightmost-derive) la regola, ‘$-1’ quello
ancora precedente e così via.
☛ Assumendo che il simbolo ‘Decl1’ sia sempre
preceduto da un identificatore di tipo:
Decl1: id { $$.type = $0.type;
add_id($1.name, $$.type); }
;
Versione 1.2 © 2000 Marco Torchiano
...continua...
Decl
T
type:
type:int
int
id
name:
name:i i
Decl1
type:
type:int
int
‘,’ Empty
type:
type:int
int
Decl1
type:
type:int
int
id
name:
name:j j
116
☛ Aggiungendo la regola
Decl1: id ‘,’ Decl1 ;
non è più vero che ‘Decl1’ è
sempre preceduto da un
identificatore di tipo.
☛ Si può aggiungere un nonterminale che porta questa
informazione:
Decl1: id ‘,’ Empty Decl1
{ $$.type= $0.type;
add_id($1.name,
$$.type); } ;
Empty: {$$=$-2;};
Calcolo di attributi ereditati
...continua
☛ Si può evitare di introdurre esplicitamente un non
terminale riscritto come stringa nulla, utilizzando,
nella parte destra delle regole, le azioni intermedie.
☛ Esse vengono automaticamente sostituite da YACC
con un simbolo non terminale, a sua volta riscritto
come ε.
☛ Nelle azioni intermedie, ‘$$’ indica il valore associato
al non-terminale implicito.
☛ Per associare a tale valore un’interpretazione tra
quelle introdotte con il costrutto %union, si usa la
sintassi ‘$<union_field>$’.
29
Politecnico di Torino
Esempio
117
%{ typedef struct {
int type_name;
char* id_name;
} YYSTYPE;
%}
%token integer char id
%start Decls
%%
Decls : /* empty */
| Decls Decl
;
Decl : TypeName Decl1 ‘;’
;
TypeName
: integer {
$$.type_name=1; }
| char {
$$.type_name=2; };
119
Decl1 : id {
$$.id_name =
$1.id_name;
$$.type_name =
$0.type_name
}
| id ‘,’ {$$ = $0;}
Decl1 {
$$.id_name =
$1.id_name;
$$.type_name =
$0.type_name;
}
;
%%
Trasformazione della grammatica
118
☛ È possibile evitare l’uso degli attributi ereditati
trasformando la grammatica.
D
L ‘:’ T
D
id L
T
integer | real
L
‘,’ id L | ‘:’ T
L
L ‘,’ id | id
T
integer | real
DD
LL
LL
DD
LL
TT
i , j : integer
Versione 1.2 © 2000 Marco Torchiano
Laboratorio di Compilatori
a.a 1999/2000
LL
TT
Aggiunta di marker
☛ L’uso di marker, ovvero di simboli non terminali che
si espandono in ε è molto utile in varie occasioni;
tuttavia può generare alcune complicazioni.
☛ La seguente trasformazioni genera una grammatica
che non è LR(1):
L
120
Lb|a
L
M
MLb|a
ε
Gestione degli errori semantici
☛ All’interno delle azioni possono essere eseguiti i vari
controlli che verificano la correttezza semantica dei
costrutti sintattici (ad esempio, verificano la giusta
corrispondenza tra tipi di operandi).
☛ Si possono utilizzare le seguenti macro:
✏ YYABORT: termina l’esecuzione del parser e restituisce 1;
✏ YYACCEPT: termina l’esecuzione del parser e restituisce 0;
✏ YYERROR: genera un errore sintattico (non chiama yyerror).
☛ Alternativamente si può cercare di ricuperare almeno
parzialmente l’errore e proseguire nella analisi
sintattica.
i , j : integer
30
Politecnico di Torino
121
Abstract Syntax Tree
123
☛ Gli AST sono una forma condensata degli alberi di
derivazione.
☛ Gli operatori e le parole chiave non compaiono come
foglie dell’albero.
☛ Esse sono associate ai nodi intermedi che sarebbero
stati i genitori delle foglie.
S
S1
✏ sistemi di tipi,
✏ espressioni di tipi,
✏ costruttori di tipo.
☛ Costruzione di type-expression
☛ Symbol tables
☛ Implementazione di un type-checker
✏ strutture dati
✏ grammatica
✏ semantica
S2
Esercizi
☛ Si modifichi la grammatica delle dichiarazioni C in
modo che usi solo attributi ereditati.
☛ Si scriva una grammatica per riconoscere delle
espressioni regolari tipo Lex e costruire l’AST relativo
all’espressione riconosciuta. Si utilizzino le primitive:
✏ ast_node* create_leaf(char ch)
✏ ast_node* create_node(char optr,
ast_node* opnd1,
ast_node* opnd2)
✏ la seconda primitiva può accettare 1 o 2 operandi, ovvero il
secondo operando può essere NULL;
✏ gli operatori sono rappresentati dai caratteri ‘+’ ‘*’ ‘?’ ‘|’ ‘&’, dove
l’ultimo rappresenta la concatenazione.
Versione 1.2 © 2000 Marco Torchiano
Controllo dei tipi
☛ Type expressions
if-then-else
if B then S1 else S2
B
122
Laboratorio di Compilatori
a.a 1999/2000
124
Sistemi di tipi
☛ Il controllo dei tipi è basato su:
✏ i costrutti sintattici del linguaggio,
✏ il concetto di tipo,
✏ le regole per assegnare i tipi ai costrutti.
☛ In generale, i tipi possono essere:
✏ primitivi (int, float, char)
✏ costruiti (struct, union)
☛ Ad ogni costrutto del linguaggio deve essere
possibile associare un tipo, descritto per mezzo di
una type-expression.
31
Politecnico di Torino
125
Laboratorio di Compilatori
a.a 1999/2000
Type-expressions
127
☛ Una type-expression è formata da un tipo primitivo,
oppure è un costruttore di tipo applicato ad una typeexpression.
☛ I tipi primitivi sono tutti quelli necessari al linguaggio,
più i due tipi speciali:
Costruttori di tipo
☛ Una funzione mappa un elemento del proprio
dominio in un elemento del range.
☛ Funzioni:
T1 T2
✏ T1 : tipo del dominio,
✏ T2 : tipo del range.
✏ void : denota l’assenza di un tipo,
✏ type_error : indica un errore rilevato durante il controllo dei tipi.
☛ La funzione int* f(char a, char b) viene
rappresentata dalla seguente type expression:
☛ È possibile assegnare un nome ad un tipo o ad un
suo campo.
☛ Un nome è una type-expression.
(char x char)
☛ Teoricamente una funzione può avere come dominio
e codominio tipi qualsiasi:
((int
126
Costruttori di tipo
☛ Array:
128
pointer( T )
T 1 X T2
struct( T )
char v[10]
struct {
int i;
char s[5];
}
int) x (int
int))
(int
int)
Grafo dei tipi
array( I , T )
✏ I : dimensione dell’array
✏ T : type expression
☛ Puntatori:
☛ Prodotto:
☛ Strutture:
pointer(int)
☛ Un modo efficace di rappresentare
expression è l’uso di grafi (alberi o DAG).
(char x char)
le
type
pointer(int)
array(10,char)
X
pointer
char
integer
struct((i x int )x( s x array(5,char)))
Versione 1.2 © 2000 Marco Torchiano
32
Politecnico di Torino
129
Costruzione di type-expression
131
☛ Il calcolo dei tipi per il C potrebbe essere fatto
tramite la seguente definizione guidata dalla sintassi:
Vl.type=T.type
D
T Vl ‘;’
Vl
V
V.type=Vl.type
Vl
Vl1 ‘,’ V
V.type=Vl.type
V
‘*’ V1
V1.type=pointer(V.type)
V
Va
Va.type=V.type
Va
Va1 ‘[‘ num ‘]’ Va1.type=array(num.val,Va.type)
Va
id
add_var(id.name,Va.type);
130
Laboratorio di Compilatori
a.a 1999/2000
Costruzione di type-expression
132
Costruzione di type-expression
D
Vl
Vl
V
T Vl ‘;’
V
Vl1 ‘,’ V
P id A
P
P
ε
P1 ‘*’
A
A
ε
A1 ‘[‘ num ‘]’
Vl.type=T.type
V.type=Vl.type
V.type=Vl.type
P.base=V.type
A.base=P.type
add_var(id.name,A.type)
P.type=P.base
P.type=pointer(P1.type)
P1.base=P.base
A.type=A.base
A.type=array(num.val,A1.type)
A1.base=A.base
Costruzione di type-expression
D
pointer(char)
Vl
D
char
pointer(char)
Vl
V
V
char
Non è ad
attributi L
V
array(10,pointer(char))
Va
char *
id
argv [
Versione 1.2 © 2000 Marco Torchiano
A
P
Va
T
P
T ε
num
10
char *
]
array(10,pointer(char))
id
ε
argv [
A
num
10
]
;
;
33
Politecnico di Torino
133
Costruzione di type-expression
☛ Occorre costruire type
espressioni:
E.type
E
literal
E
num
E.type
E
id
E.type
E
‘&’ E1
E.type
E
‘*’ E1
E.type
E
134
E1 [ E2 ]
Laboratorio di Compilatori
a.a 1999/2000
135
expression anche per le
☛ La possibilità di assegnare dei nomi ai tipi complica
la verifica dell’equivalenza.
☛ Esempio:
= char
= integer
typedef cell* link;
link p;
cell* q;
= lookup(id.name)
= pointer(E1.type)
E.type = if E1.type=array(s,T)
and E2.type=integer
then T else type_error
☛ Il modo più naturale di definire l’equivalenza tra due
type-expression è l’equivalenza strutturale.
☛ Due espressioni sono uguali se:
✏ sono lo stesso tipo primitivo, oppure
✏ sono l’applicazione del costruttore di tipo a tipi equivalenti.
136
Riferimenti circolari
☛ Molte strutture dati basilari, come le liste concatenate
o gli alberi, sono definite ricorsivamente.
☛ Tale configurazione introduce, potenzialmente, dei
cicli nel grafo di tipi.
struct cell
☛ Utilizzando la naturale rappresentazione delle type
expression come alberi, è possibile utilizzare un
algoritmo a discesa ricorsiva per verificare
l’equivalenza.
X
X
info
Versione 1.2 © 2000 Marco Torchiano
type link = ^cell;
var p : link;
q : ^cell;
☛ Le variabili p e q sono uguali? La risposta varia se
viene utilizzata l’equivalenza di nomi o l’equivalenza
strutturale.
☛ In C viene utilizzata l’equivalenza strutturale, mentre
il Pascal adotta l’equivalenza dei nomi.
= if E1.type=pointer(T)
then T else type_error
Equivalenza
Nomi dei tipi
1
X
integer
next
pointer
2
struct cell
34
Politecnico di Torino
137
Riferimenti circolari
Laboratorio di Compilatori
a.a 1999/2000
139
☛ Un semplice algoritmo per la verifica dell’equivalenza
strutturale di tipi può non funzionare in presenza di
riferimenti circolari.
☛ In C è sempre necessario dichiarare un nome prima
dell’uso, con l’unica eccezione di puntatori a struct
non ancora dichiarate.
☛ Se il nome del record è parte del costruttore di
struttura; il test per l’equivalenza si ferma quando
raggiunge due strutture:
Type checker
☛ Un type-checker è formato da un insieme di moduli
interoperanti:
✏
✏
✏
✏
scanner: riconosce il lessico,
parser: verifica la sintassi ed aggiunge la semantica,
gestore di type-expresssion,
gestore di symbol table.
Symbol table
✏ se hanno lo stesso nome, sono equivalenti;
✏ altrimenti sono diversi.
138
Esercizi
☛ Si scriva la definizione guidata dalla sintassi per la
costruzione della type-expression relativa ad una
struct C.
☛ Date le seguenti dichiarazioni C:
typedef struct {
int a,b;
} CELL, *PCELL;
CELL foo[100];
PCELL bar(int x, CELL y);
si scrivano le type-expression corrispondenti.
Versione 1.2 © 2000 Marco Torchiano
Parser
Type
expression
Scanner
140
Symbol table
☛ Le tabelle dei simboli associano valori a nomi per
rendere accessibili informazioni semantiche, legate
ad un identificatore, al di fuori del contesto in cui
esso è stato dichiarato.
☛ Le informazioni associate a ciascun nome vengono
utilizzate per verificare il corretto uso semantico degli
identificatori di un programma.
☛ Tali informazioni possono essere aggiornate
dinamicamente via via che nuove caratteristiche del
nome sono dichiarate nel programma o dedotte dal
compilatore.
35
Politecnico di Torino
141
Symbol table
143
☛ Le informazioni che vengono memorizzate all’interno
di una tabella dei simboli vengono dette entry.
☛ Ogni operazione di inserimento definisce una chiave
(solitamente una stringa) tramite la quale poter,
successivamente, reperire le informazioni.
☛ In generale, le informazioni memorizzate in una
symbol table non sono omogenee, perciò conviene
memorizzare dei puntatori invece delle informazioni
stesse.
☛ Un traduttore utilizza diverse tabelle dei simboli per
memorizzare informazioni diverse o appartenenti a
contesti diversi.
142
Symbol table: interfaccia
☛ Una porzione di codice che implementa una tabella
dei simboli dovrebbe consentire le seguenti
operazioni astratte:
✏ Create()
✏ Destroy()
✏ Enter(K,I)
✏ Lookup(K)
Laboratorio di Compilatori
a.a 1999/2000
costruttore
distruttore
aggiunge alla tabella le informazioni I,
associandole alla chiave K.
cerca nella tabella le informazioni associate alla
chiave K.
☛ Nell’ottica del riutilizzo, è conveniente associare ad
ogni tabella un handle, che identifica l’istanza della
tabella su cui si desidera operare.
Versione 1.2 © 2000 Marco Torchiano
Symbol table: implementazione
☛ Da un punto di vista implementativo una tabella può
essere realizzata con una delle seguenti tecniche
✏
✏
✏
✏
✏
Liste disordinate
Liste ordinate
Alberi binari
Tabelle hash
BTree …
☛ La scelta dipende dal numero di simboli da
memorizzare, dalle prestazioni che si intendono
ottenere, dalla complessità del codice che si intende
produrre.
144
Symbol table: implementazione
☛ L’interfaccia di un modulo per la gestione di symbol
table basate su hash-table potrebbe essere:
ht_handle ht_create(int size);
void ht_destroy(ht_handle);
int ht_insert(ht_handle, char* key, void* info);
void* ht_lookup(ht_handle, char* key);
void* ht_delete(ht_handle, char* key);
36
Politecnico di Torino
145
a.a 1999/2000
Type expression
147
☛ La rappresentazione naturale delle type expression
tramite grafi di tipi può essere trasformata in una
rappresentazione interna.
☛ La gestione delle type expression richiede
te_node* te_make_base(int code);
te_node* te_make_name(char* name);
te_node* te_make_product(te_node* l, te_node* r);
te_node* te_make_struct(te_node* flds, char* n);
te_node* te_make_fwdstruct(char* name);
te_node* te_make_function(te_node* d, te_node* r);
te_node* te_make_pointer(te_node* base);
te_node* te_make_array(int size, te_node* base);
void te_cons_struct(te_node* str, te_node* flds);
☛ I nodi devono essere in grado di rappresentare i
diversi costruttori di tipo ed i tipi di base.
☛ Le
primitive
servono
per
nascondere
la
rappresentazione interna dei nodi e consentire
all’utilizzatore di scrivere del codice il più semplice
possibile.
Type expression: implementazione
Type expression: implementazione
☛ Il modulo di gestione delle TE deve offrire le seguenti
primitive:
✏ la definizione della struttura dati dei nodi del grafo,
✏ la definizione delle primitive per operare sui nodi.
146
Laboratorio di Compilatori
148
Type checker: semantica
☛ Ogni nodo di un grafo dei tipi contiene:
✏ un tag, che rappresenta il tipo di nodo;
✏ un campo fisso, che rappresenta il primo operando;
✏ una union di vari campi
typedef struct te_node_tag {
int tag;
struct te_node_tag* left;
union {
struct te_node_tag* right;
int size;
int code;
char* name;
} r;
} te_node;
Versione 1.2 © 2000 Marco Torchiano
☛ Oltre alle funzioni offerte dal modulo di gestione delle
type expression, è opportuno fornire alcune primitive
per l’accesso alle tabelle dei simboli.
te_node* type_lookup(char* );
int add_type(char* name,te_node* type);
int add_var(char* name,te_node* type);
☛ Oltre a semplificare la scrittura delle azioni
semantiche, permettono di nascondere i dettagli
implementativi delle tabelle.
37
Politecnico di Torino
149
Laboratorio di Compilatori
a.a 1999/2000
Type checker: semantica
Type checker: semantica
151
YACC
☛ L’organizzazione delle tabelle dei simboli può
seguire due strade.
☛ Due tabelle separate:
✏ per i tipi, definiti con typedef o struct tag,
✏ per le variabili.
✏ il “genere” del simbolo,
✏ le informazioni.
%type
%type
%type
%type
%type
typedef struct {
int symbol_kind;
char* name;
te_node* type;
} table_entry;
Type checker: semantica
Decl
: T Vlist
;
{ }
T
: TYPE
;
{ $$=$1; }
Vlist
V
:V
| Vlist ','
{ $$=$<type>0 } V
;
: Ptr ID {$$=$1;} Ary { add_var($2,$4); }
;
Ptr
: /* empty */
| Ptr '*'
;
{ $$=$<type>0; }
{ $$=te_make_pointer($1); }
Ary
: /* empty */
| Ary '[' NUM ']'
;
{ $$=$<type>0; }
{ $$=te_make_array($3,$1); }
Versione 1.2 © 2000 Marco Torchiano
ID
[A-Za-z_][0-9A-Za-z_]*
%%
{ID}
{
te_node* pte;
pte = type_lookup(yytext);
if(pte!=NULL){
yylval.type=pte;
return TYPE;
}
yylval.name=strdup(yytext);
return ID;
%token <ival> NUM
%token <name> ID
%token <type> TYPE
☛ Un’unica tabella in cui vengono memorizzati:
150
LEX
%union {
te_node* type;
char* name;
int ival;
}
<type>
<type>
<type>
<type>
<type>
Vlist
V
T
Ptr
Ary
}
Type checker: grammatica
152
S : /* empty */
| S Decl ';'
;
Decl
T :
|
|
|
;
SFL
: T Vlist
| TYPEDEF T Vlist
;
TYPE
STRUCT ID '{' SFL '}'
STRUCT '{' SFL '}'
STRUCT ID
: Field
| SFL Field
;
Field : T Vlist
;
Vlist : V
| Vlist ',' V
;
V : Ptr ID Ary
;
Ptr
: /* empty */
| Ptr '*'
;
Ary
: /* empty */
| Ary '[' NUM ']'
;
38
Politecnico di Torino
153
Type checker: semantica
155
✏ una variabile,
✏ un campo di una struttura,
✏ un nome di un nuovo tipo definito dall’utente.
☛ Volendo utilizzare la grammatica precedente,
occorre inserire, nella semantica di V, tre alternative
per trattare i diversi casi.
☛ Per distinguere i casi occorre impostare una variabile
di stato in corrispondenza:
✏ del riconoscimento della keyword typedef, oppure
✏ della keyword struct seguita da graffe.
Ambienti di esecuzione
☛ Procedure e parametri
✏ Parametri formali ed attuali
✏ Record di attivazione
☛ Tabelle a blocchi
☛ Allocazione della memoria
☛ Passaggio di parametri
✏ Stack frame
✏ Tail recursion
Versione 1.2 © 2000 Marco Torchiano
Procedure ed attivazioni
☛ Un programma può essere visto come un insieme di
procedure o funzioni; essenzialmente, una procedura
assegna un identificatore (nome) ad un insieme di
statement, (corpo).
☛ È bene distinguere il codice sorgente di una
procedura dalla sua attivazione all’interno di un
processo.
☛ Si ha una chiamata ad una procedura quando il suo
nome compare in uno statement eseguibile.
☛ Quando viene eseguita la chiamata, la procedura
viene attivata cioè vengono eseguiti gli statement del
suo corpo.
☛ Il non terminale V può corrispondere a:
154
Laboratorio di Compilatori
a.a 1999/2000
156
Parametri
☛ Nella definizione di una procedura possono
comparire dei parametri formali.
☛ All’atto della chiamata, vengono specificati degli
argomenti o parametri attuali, corrispondenti ai
parametri formali.
☛ Ad ogni attivazione di una procedura vengono forniti
dei parametri formali.
☛ Una procedura è ricorsiva se una sua attivazione
può iniziare prima che ne finisca una precedente.
☛ È possibile la ricorsione solo se ad ogni attivazione i
parametri attuali vengono memorizzati in locazioni
diverse.
39
Politecnico di Torino
157
Stack di controllo
a.a 1999/2000
159
☛ Ad ogni attivazione di una procedura occorre
associare un record di attivazione che contiene le
informazioni utilizzate dal codice nell’attivazione
corrente.
☛ Risulta naturale memorizzare i record di attivazione
in uno stack di controllo.
☛ È possibile associare l’ambito di visibilità (scope)
legato ad una procedura al suo record di attivazione.
Come conseguenza, ogni attivazione ha le proprie
variabili locali.
☛ Il concetto di scope ha un’applicazione più ampia del
semplice contesto di una procedura.
158
Tabelle strutturate a blocchi
☛ Nei linguaggi strutturati è possibile introdurre più
identificatori con lo stesso nome, in contesti separati
e/o nidificati.
☛ Gli identificatori sono racchiusi in unità (moduli,
procedure, blocchi, ecc.) che definiscono un
contesto (scope).
☛ Data un’istruzione, l’unità più interna che la
racchiude viene detta contesto corrente.
☛ Tutte le unità che racchiudono lo scope corrente
sono dette contesti aperti.
☛ Le restanti unità del programma formano i contesti
chiusi.
Versione 1.2 © 2000 Marco Torchiano
Laboratorio di Compilatori
Regole di visibilità
☛ Per ciascun punto di un programma, solo i nomi
dichiarati nel contesto corrente o nei contesti aperti
sono accessibili.
☛ Se un nome è dichiarato in più di un contesto, la
dichiarazione più interna (più vicina al contesto
corrente)
viene
utilizzata
per
interpretare
un’occorrenza del nome.
☛ Possono essere fatte nuove dichiarazioni solo nel
contesto corrente.
160
Tabelle a blocchi: realizzazione
☛ Le regole di visibilità suggeriscono la realizzazione di
un’insieme di symbol table organizzate a stack.
☛ La tabella in cima allo stack corrisponde al contesto
corrente;
quella
successiva
al
contesto
immediatamente più esterno, e cosi via.
☛ Dato un simbolo, esso viene cercato dapprima in
cima allo stack e, se non trovato, via via nelle tabelle
successive.
☛ Ogni volta che si apre un nuovo contesto, una tabella
vuota viene inserita in cima allo stack.
☛ Alla chiusura del contesto, tale tabella viene rimossa
dallo stack.
40
Politecnico di Torino
161
Tabelle a blocchi: interfaccia
a.a 1999/2000
163
☛ Si può realizzare un modulo che incapsula il
comportamento a stack delle tabelle:
inserisce una tabella in cima allo stack, essa
diventa la tabella del contesto corrente,
✏ Pop()
rimuove la tabella del contesto corrente,
✏ CurrentScope() restituisce la tabella corrente,
✏ Lookup(Id)
cerca nello stack l’elemento Id ritorna un
riferimento all’oggetto.
☛ All’atto della creazione di una nuova tabella, è
necessario memorizzarne un riferimento, per poter
accedere in seguito alle informazioni in essa
contenute.
Estensioni per i record
☛ Insieme alla definizione di un record (struttura,
unione, classe) è necessario memorizzare una
tabella dei simboli riportante le informazioni relative
ai singoli campi.
☛ Quando il compilatore incontra un riferimento ad una
variabile di tipo record, ottiene dalla tabella dei
simboli dei tipi un puntatore alla tabella dei campi del
record.
☛ Mediante tale puntatore determina le caratteristiche
del campo selezionato.
Versione 1.2 © 2000 Marco Torchiano
Dichiarazioni implicite
☛ In alcuni linguaggi non è necessaria una
dichiarazione esplicita di un simbolo prima del suo
uso.
☛ In questi casi, quando il simbolo viene incontrato il
compilatore deve dedurre dal contesto il maggior
numero di informazioni possibili ed inserirle nella
tabella dei simboli.
☛ Se il linguaggio ammette anche contesti annidati,
bisogna determinare se il simbolo appartiene ad un
contesto esistente o costituisce una nuova
definizione nel contesto corrente.
✏ Push(Table)
162
Laboratorio di Compilatori
164
Riferimenti in avanti
☛ Un caso particolare di dichiarazione implicita è dato
dall’uso dei nomi delle etichette di salto (label) in
alcuni linguaggi.
☛ Quando il compilatore incontra un riferimento
all’etichetta, essa può non essere ancora stata
definita.
☛ Non è possibile, in tal caso, creare dei compilatori a
passo singolo: i riferimenti a entità sconosciute sono
registrati temporaneamente e, al termine dell’analisi,
sostituiti con i relativi indirizzi. Solo dopo può essere
generato il codice.
41
Politecnico di Torino
165
Laboratorio di Compilatori
a.a 1999/2000
Integrazione con il parser
167
☛ Nello scrivere la grammatica diventa necessario
introdurre azioni semantiche che interagiscano con
la tabella dei simboli per la corretta interpretazione
degli identificatori.
☛ Tali azioni possono essere intermedie rispetto a
qualche regola (in particolare con i record). In tal
caso la grammatica può diventare non LALR(1) e,
come tale, non essere utilizzabile con generatori
automatici come YACC.
☛ La seguente dichiarazione può essere una variabile
oppure una funzione:
Allocazione della memoria
☛ All’interno di un programma possono essere
utilizzate, anche contemporaneamente, diverse
strategie di allocazione della memoria.
☛ Il principale aspetto distintivo è costituito dal momento
in cui viene assegnata una locazione di memoria ad
una variabile.
☛ In C si definiscono tre grandi classi di allocazione:
✏ statica: a tempo di compilazione
✏ automatica: all’attivazione di una procedura.
✏ dinamica: in qualsiasi momento durante l’esecuzione.
pippo (x);
166
Dipendenza dal contesto
☛ La semantica associata ad un costrutto sintattico può
variare in funzione del contesto in cui esso si trova.
☛ Per utilizzare generatori di parser context-free
occorre introdurre, nel parser, uno stato (semantico)
che modifichi l’interpretazione dei costrutti sintattici.
☛ Questo tipo di soluzione può creare problemi se si
utilizzano regole per il recupero degli errori.
☛ Occorre ripristinare lo stato precedente al
riconoscimento del costrutto sintattico in cui si
intercetta l’errore.
Versione 1.2 © 2000 Marco Torchiano
168
Variabili statiche
char*
char* s1="pippo";
s1="pippo";
char
char s2[]="pluto";
s2[]="pluto"; .globl s1
char*
.section
.rodata
char* s3(){
s3(){
return
return "minni";
"minni"; .LC0:
}}
.string "pippo"
.data
s1:
.long .LC0
.globl s2
s2:
.string "pluto"
.section
.rodata
.LC1:
.string "minni"
*s1=‘a’;
*s3()=‘a’;
*s2=‘a’;
42
Politecnico di Torino
169
Stack frame
171
Responsabilità
del chiamato
170
return value
parametri
attuali
CPU status
control link
Variabili
locali e
temporanee
frame link
172
☛ Codice sorgente e stack frame:
0
1
2
3
BP+16
return value
BP+12
*p
BP+8
i
BP
BP+4
return address
SP
BP
BP
BPold
BP-4
j
situazione
a regime
Versione 1.2 © 2000 Marco Torchiano
leal -8(%ebp),%eax
pushl %eax
secondo parametro: *p
movl -4(%ebp),%eax
pushl %eax
primo parametro: i
call _set
Esempio
int set
(int i, int* p)
{
int j;
j=*p;
*p = i;
return j;
}
Esempio
☛ Il chiamante pone sullo stack i parametri.
☛ Il valore di ritorno viene passato attraverso un
registro della CPU per motivi di efficienza.
☛ Per convenzione i parametri vengono depositati in
ordine inverso a quello con cui compaiono nella
definizione della procedura.
☛ Il record di attivazione viene memorizzato nello stack
di esecuzione.
☛ Ad esso vengono affiancate le informazioni di stato
del processore.
Responsabilità
del chiamante
Laboratorio di Compilatori
a.a 1999/2000
situazione
all’ingresso
esecuzione della chiamata
Esempio
pushl %ebp
movl %esp,%ebp
subl $4,%esp
finisce di costruire
lo stack frame
movl 12(%ebp),%eax
movl (%eax),%edx
movl %edx,-4(%ebp)
j=*p;
movl 12(%ebp),%eax
movl 8(%ebp),%edx
movl %edx,(%eax)
*p=i;
movl -4(%ebp),%eax
return j;
leave
ret
distrugge il frame
e restituisce il controllo
43
Politecnico di Torino
a.a 1999/2000
Tail recursion
173
175
☛ Si dice che una procedura è tail-recursive se il suo
ultimo statement è la una chiamata ricorsiva alla
procedura stessa.
☛ Le funzioni tail-recursive possono essere oggetto di
ottimizzazione.
1
int exp(int n)
int rexp(int n,int r)
{
{
if(n==1) return 1;
if(n==1) return r;
return n*exp(n-1);
return rexp(n-1,r*n);
}
L2:
movl $1,%eax
L3:
movl -4(%ebp),%ebx
leave
ret
Versione 1.2 © 2000 Marco Torchiano
5*N+8
if(n==1) return r
pushl %ebp
movl %esp,%ebp
pushl %ebx
movl 8(%ebp),%ebx
cmpl $1,%ebx
je L2
leal -1(%ebx),%eax
pushl %eax
call _exp
imull %ebx,%eax
jmp L3
☛ L’uso dello stack sia per il passaggio di parametri
che per memorizzare gli indirizzi di ritorno delle call,
consente manipolazioni particolari.
☛ È possibile modificare l’indirizzo di ritorno di una
funzione saltando ad altro codice.
☛ È possibile passare dei parametri senza utilizzare
l’istruzione di push:
✏ si utilizza un valore di ritorno memorizzato da una call per
indirizzare un’area di memoria.
176
return rexp(n-1,r*n)
return n*exp(n-1)
if(n==1) return 1
14*N+10
Manipolazioni dello stack
}
Tail recursion
174
Laboratorio di Compilatori
pushl %ebp
movl %esp,%ebp
movl 8(%ebp),%edx
movl 12(%ebp),%eax
L6:
cmpl $1,%edx
je L5
imull %edx,%eax
decl %edx
jmp L6
L5:
leave
ret
invece di:
pushl %eax
pushl %edx
call _rexp
Manipolazione dello stack
Ret Addr
BP
void nascosta(){
new_bp
printf("Nascosta\n");
}
void visibile(){
int new_bp;
new_bp = *(&new_bp + 1);
*(&new_bp+1)
= (int)(&nascosta);
BP
__asm__(" addl $-4,%ebp");
}
BPold
Ret Addr
&nascosta
BPold
44
Politecnico di Torino
177
Laboratorio di Compilatori
a.a 1999/2000
Manipolazioni dello stack
179
Codici intermedi
☛ Tipi di codici intermedi
✏ AST, notazione post-fissa, three-address code, RTL
void main(){ printf("Eccoci...\n");
__asm__(" jmp my_end
my_bg:
call _printf
add $0x4,%esp
jmp my_out
my_end:
call my_bg
.string \"Ciao\\n\"
my_out: ");
printf("!!!!!!!!\n");}
178
SP
Manipolazione dello stack
BP
ret
✏
✏
✏
✏
Tipologie di quadruple
Implementazione
Espressioni matematiche e logiche
Puntatori, strutture ed array
☛ Trasformazione delle strutture di controllo
✏ if / while / for
✏ flussi di controllo per espressioni logiche
☛ Accesso ad elementi di tipi composti
✏ array
✏ strutture
180
Rappresentazione intermedia
☛ Più semplice da generare rispetto al codice finale.
☛ Più facilmente ottimizzabile.
BP
prima della call
SP
☛ Rappresentazione a quadruple
0x40107f:call
0x401075 <my_bg>
0x401084:”Ciao\n”
0x40108a:add
$0xfffffff4,%esp
dopo la call
Scanner
Parser
Rappres.
Intermedia
Ottimizzatore
Symbol
Table
Generatore
Codice
Assembler
Versione 1.2 © 2000 Marco Torchiano
45
Politecnico di Torino
181
Tipi di rappresentazioni
☛ Esistono varie rappresentazioni
esaminiamo quattro:
✏
✏
✏
✏
Laboratorio di Compilatori
a.a 1999/2000
183
intermedie,
Three-address code
☛ È una sequenza di istruzioni del tipo x = y op z
dove x , y e z sono:
ne
✏ variabili,
✏ costanti,
✏ varibili temporanee generate dal compilatore.
syntax tree
notazione post-fissa
three-address code
RTL
t1 = - c
t2 = b * t1
t3 = - c
t4 = b * t3
t5 = t2 + t4
a = t5
☛ Per confrontarle consideriamo le traduzioni della
seguente espressione:
a = b * (- c) + b * (- c);
182
Notazione post-fissa e syntax tree
184
☛ La notazione post-fissa rappresenta così:
a b c uminus * b c uminus * + assign
☛ Un sintax tree può essere completo o in forma di dag
se vengono identificate le espressioni comuni:
assign
a
b
a
*
*
uminus b uminus
c
Versione 1.2 © 2000 Marco Torchiano
c
☛ Assegnamento:
✏
✏
✏
✏
con operatori binari:
con operatori unari:
di copia:
indicizzato
x = y op z
x = op y
x = y
x = y[i]
x[i] = y
☛ Salto:
assign
+
Tipologie di 3-address code
✏ non condizionale:
✏ condizionale:
+
*
b uminus
goto L
if x relop y goto L
if x goto L
☛ Puntatori ed indirizzi:
✏ puntatore:
✏ indirizzo:
x = *y
x = &y
c
46
Politecnico di Torino
185
RTL
187
☛ È una rappresentazione più vicina alla macchina.
☛ Utilizza degli pseudo registri e fa riferimento ad una
disposizione in memoria delle variabili (GCC).
set
set
set
set
set
set
reg_22 mem(reg_18 - 8)
reg_23 mem(reg_18 - 8)
reg_21 reg_22 + reg_23
reg_24 - mem(reg_18 - 12)
reg_25 reg_21 * reg_24
mem(reg_18 - 4) reg_25
a
186
Laboratorio di Compilatori
a.a 1999/2000
☛ Aritmetiche:
✏ su interi:
ADDI opnd1, opnd2, res
SUBI opnd1, opnd2, res
MULI opnd1, opnd2, res
DIVI opnd1, opnd2, res
NEGI opnd, - , res
ITOF opnd, - , res
✏ su floating point: ADDF, SUBF, MULF, DIVF, NEGF, FTOI
b
c
☛ Logiche:
✏ booleane: AND, OR, NOT, XOR
✏ relazionali: EQ, NE, GT, LT, GE, LE
a = ( b + b )*( -c )
Implementazione di 3-address code
☛ Quadruple: rappresentano gli operandi, il tipo di
operazione ed il risultato in una struttura a quattro
campi:
1 UMINUS c
t1
2
Tipi di quadruple
ADD
b
t1
t2
☛ Triple: non rappresentano esplicitamente il risultato
ma identificano esse stesse un risultato:
1
UMINUS
c
2
ADD
b
Versione 1.2 © 2000 Marco Torchiano
188
Tipi di quadruple
☛ Flusso di controllo:
✏ label: LABEL n, -, ✏ salti:
JUMP -, -, n
JUMP_IF_TRUE opnd, - , n
JUMP_IF_FALSE opnd, - , n
☛ Assegnamento:
✏ ASSIGN opnd, size, res
✏ ASSIGNP opnd, size, ptr(res)
✏ ASSIGN[] opnd, offset, res
☛ Su variabili:
(1)
✏ ADDRESS opnd, - , res
✏ DEREF opnd, size, res
47
Politecnico di Torino
189
Struttura dati
191
☛ Le quadruple possono essere realizzate tramite liste
linkate, descritte da una struct C:
typedef union {
t_label label;
t_var var;
t_offset add;
double fval;
long ival;
} t_op_data;
190
Programma di esempio
int i,j,k,l,m;
{
i = 3 + m*2 - l;
typedef struct op_tag {
t_code tag;
t_op_data data;
} t_opnd, * t_op;
☛ Si possono definire un insieme di primitive per operare
sulle quadruple:
*mkquad(t_code,t_op,t_op,t_op);
*append(quad*,quad*);
mklabel(void);
mk_op(int);
mk_var(st_entry*);
new_add(t_offset);
new_ival(long);
new_fval(double);
☛ Occorre definire delle costanti che corrispondano ai vari
tipi di quadruple.
Versione 1.2 © 2000 Marco Torchiano
if(i==1) m=1;
else m=2;
while(i<5){
i = i+1;
}
for(i=0; i<10; i=i+1){ m = k+i; }
typedef struct q_tag {
t_code code;
t_op op1,op2,op3;
struct q_tag *next;
} quad;
Primitive
quad
quad
t_op
t_op
t_op
t_op
t_op
t_op
Laboratorio di Compilatori
a.a 1999/2000
if( i==j && k==l && m==1 || j>3) { i=1; }
}
192
Attributi
☛ Code: contiene le quadruple associate al simbolo
non-terminale.
☛ Place: un riferimento alla variabile che contiene il
risultato del non-terminale; è utilizzato se si tratta di
espressioni.
☛ True, False: nomi delle label a cui salta
un’espressione logica.
☛ Nella valutazioni di espressioni vengono generate
nuove variabili; esse vengono inserite normalmente
nella symbol table del contesto corrente.
48
Politecnico di Torino
Espressioni aritmetiche
193
ADD
VAR
195
VAR %1
quad
Expr
place
t_opnd
ID
IVAL
t_opnd
place
Expr
Expr
194
1
place
code
code
ID
‘+’
ii
++
NUM
11
Espressioni aritmetiche
Expr : Expr '+' Expr
{
$$.type=compose($1.type,$3.type);
$$.place=new_temp($$.type);
$1.code
$$.code=append(
$3.code
append($1.code,$3.code) ,
ADD
mkquad(qADDI,$1.place,$3.place,$$.place));
}
| Expr '-' Expr { … }
…
| primary
{ $$=$1; }
;
Versione 1.2 © 2000 Marco Torchiano
Espressioni aritmetiche
primary:
'(' Expr ')'
{ $$=$2; }
| NUM
{ $$.type=st_lookup("int")->type;
$$.code=NULL;
$$.place=new_ival($1);}
| ID
{ st_entry* ste;
ste=st_lookup($1);
$$.code=NULL;
$$.type=ste->type;
$$.place=mk_var(ste); }
t_opnd
code
Laboratorio di Compilatori
a.a 1999/2000
196
Esempio di espressione aritmetica
☛ Ogni nodo genera una quadrupla che usa i risultati
dei suoi sotto-nodi.
1
2
3
4
MULI
ADDI
SUBI
ASSIGNV
m
3
%1
%2
2
%0
l
%0
%1
%2
i
4
3
2
1
ii == 33 ++ mm ** 22 -- ll ;;
49
Politecnico di Torino
197
Costrutti di controllo
If-then
$3.code
: $3.true
$5
: $3.false
if_stmt : IF '(' bool_exp ')' Stmt {
$$=append(
append($3.code,
mkquad(qLABEL,NULL,NULL,$3.true)
),
append($5,
mkquad(qLABEL,NULL,NULL,$3.false)
)
);
}
Versione 1.2 © 2000 Marco Torchiano
If-then-else
199
☛ I blocchi di istruzioni che rappresentano costrutti di
controllo ed espressioni booleane richiedono due
ulteriori attributi:
☛ True: etichetta a cui il costrutto salta quando la
condizione è vera.
☛ False: etichetta a cui il costrutto salta quando la
condizione è falsa.
☛ Le etichette vengono generate durante la traduzione
delle espressioni booleane o di confronto e vengono
utilizzate per guidare i costrutti di controllo.
198
Laboratorio di Compilatori
a.a 1999/2000
$3.code
:$3.true
$5 true
jump next
:$3.false
$7
false
:next
200
| IF '(' bool_exp ')' Stmt ELSE Stmt {
t_op next;
next=new_label();
$$=append(append($3.code,
mkquad(qLABEL,NULL,NULL,$3.true)),
append(append($5,append(
mkquad(qJUMP,NULL,NULL,next),
mkquad(qLABEL,NULL,NULL,$3.false))),
append($7,
mkquad(qLABEL,NULL,NULL,next)))
);}
;
Esempio di if-then-else
☛ I due rami dell’if vengono racchiusi tra salti e label.
JEQ
JUMP
LABEL
ASSIGNV
JUMP
LABEL
ASSIGNV
LABEL
i
1
2
1
L0
L1
L0
m
L2
L1
m
L2
if(i==1)
if(i==1)
mm == 1;
1;
else
else
mm == 2;
2;
50
Politecnico di Torino
201
F
Espressioni logiche - confronto
rel_exp : Expr REL_OP Expr
{
if(log_lab_flag){
$$.true=$<bexp>0.true;
$1.code
$$.false=$<bexp>0.false;
}else{
$3.code
$$.true=new_label();
J($2) $$.true
$$.false=new_label();
}
jump $$.false
$$.code=append(append($1.code,$3.code),
append(
T
mkquad($2,$1.place,$3.place,$$.true),
mkquad(qJUMP,NULL,NULL,$$.false)));
}
202
Espressioni logiche - op. booleani
bool_exp: bool_exp AND { $<bexp>$.true=new_label();
$<bexp>$.false=$1.false;
log_lab_flag=1; }
bool_exp {
$$.code=append($1.code, append(
mkquad(qLABEL,NULL,NULL,$1.true),
$4.code));
$$.true=$4.true;
T $$.false=$1.false;
log_lab_flag=0; }
| NOT {$<bexp>$=$<bexp>0;} bool_exp
{
$$.code=$3.code;
$$.true=$3.false;
$$.false=$3.true; }
$1.code
$1.true
$4.code
F
$4.code
F
T
Versione 1.2 © 2000 Marco Torchiano
Laboratorio di Compilatori
a.a 1999/2000
203
Espressioni logiche - op. booleani
| bool_exp OR
{ $<bexp>$.false=new_label();
$<bexp>$.true=$1.true;
log_lab_flag=1; }
bool_exp {
$$.code=append($1.code,append(
mkquad(qLABEL,NULL,NULL,$1.false),
$4.code));
$$.true=$1.true;
$$.false=$4.false;
log_lab_flag=0;
$1.code
$1.false
$4.code
F
T
}
| rel_exp
;
204
{ $$=$1; }
Esempio di espressioni logiche
JEQ
JUMP
LABEL
JEQ
JUMP
LABEL
JEQ
JUMP
LABEL
JGT
JUMP
LABEL
ASSIGNV
LABEL
i
j
k
l
m
1
j
3
1
L9
L10
L9
L11
L10
L11
L12
L10
L10
L12
L13
L12
i
L13
if(
if( i==j
i==j &&
&& k==l
k==l
&&
&& m==1
m==1 ||
|| j>3)
j>3)
{{ i=1;
i=1; }}
51
Politecnico di Torino
205
While
:begin
$3.code
:$3.true
$5
jump begin
:$3.false
Laboratorio di Compilatori
a.a 1999/2000
207
while_stmt: WHILE '(' bool_exp ')' Stmt {
t_op lab;
bgin=new_label();
$$=append(append(
mkquad(qLABEL,NULL,NULL, bgin),append(
$3.code,
mkquad(qLABEL,NULL,NULL,$3.true))),
append(
$5,append(
mkquad(qJUMP,NULL,NULL,bgin),
mkquad(qLABEL,NULL,NULL,$3.false)))
);}
For
$3.code
:begin
$5.code
:$5.true
$9
$7.code
jump begin
:$5.false
206
Esempio di while
208
☛ Una label iniziale consente di fare un ciclo che ripete
il test ed esegue il corpo del while.
LABEL
JLT
JUMP
LABEL
ADDI
ASSIGNV
JUMP
LABEL
i
i
%3
5
1
Versione 1.2 © 2000 Marco Torchiano
L5
L3
L4
L3
%3
i
L5
L4
while(i<5)
while(i<5)
{{
ii == i+1;
i+1;
}}
for_stmt: FOR '('Expr';'bool_exp';'Expr')' Stmt
{ t_op lab;
bgin=new_label();
$$=append(append(append(
$3.code,
mkquad(qLABEL,NULL,NULL,bgin)),append(
$5.code,
mkquad(qLABEL,NULL,NULL,$5.true))),
append(append(
$9,
$7.code),append(
mkquad(qJUMP,NULL,NULL,bgin),
mkquad(qLABEL,NULL,NULL,$5.false))));
};
Esempio di for
☛ Come nel while, salvo che prima viene messo il
codice di inizializzazione.
ASSIGNV
LABEL
JLT
JUMP
LABEL
ADDI
ASSIGNV
ADDI
ASSIGNV
JUMP
LABEL
0
i
10
k
%5
i
%4
i
1
i
L8
L6
L7
L6
%5
m
%4
i
L8
L7
for(i=0;
for(i=0; i<10;
i<10; i=i+1)
i=i+1)
{{
m=k+i;
m=k+i;
}}
52
Politecnico di Torino
209
Accesso con spiazzamento
Laboratorio di Compilatori
a.a 1999/2000
211
☛ Per accedere agli elementi di un array o ai campi di
una variabile strutturata è necessario associare un
nuovo attributo:
☛ Offset: rappresenta lo spiazzamento dell’elemento a
cui si vuole accedere, rispetto alla variabile di
riferimento, memorizzata nell’attributo place.
☛ È necessario sommare gli offset man mano che
vengono calcolati.
Accesso ad elementi di array
int
int
int
int
……
ii ==
☛ Costruzione dell’offset:
place=v
offset=j*8+4*k
place=v
offset=j*8
place=v
offset=null
Expr
v[j][k];
v[j][k];
array, 5
size=40
array, 2
Expr
Expr Expr
i,j,k;
i,j,k;
v[5][2];
v[5][2];
size=8
Expr
int
size=4
vv [[ jj ][
][ kk ];
];
210
Accesso ad elementi di array
☛ La disposizione in memoria degli array può seguire due strategie:
✏ row major: riga per riga, utilizzata da C e Pascal
✏ column major: colonna per colonna, adottata in Fortran.
☛ Il tipo di un array deve essere costruito da destra verso sinistra;
può servire la ricorsione destra:
Decl
Vdecl
Ary
:
:
:
|
T Vdecl | Decl ‘,’ Vdecl ;
ID {$$=$0;} Ary
{ ... } ;
/* empty */ { $$=$<type>0; }
'[' NUM ']'
{ $$=$<type>0; }
Ary
{ $$=te_make_array($2,$5); }
;
Versione 1.2 © 2000 Marco Torchiano
212
Accesso ad elementi di array
| Expr '[' Expr ']' {
t_op ofs;
$$.type=$1.type->left;
$$.code=append($1.code,$3.code);
ofs=new_ival($1.type->left->size);
$$.offset=new_temp(st_lookup("int")->type);
$$.code=append($$.code,
mkquad(qMULI,$3.place,ofs,$$.offset));
if($1.offset!=NULL){
ofs=new_temp(st_lookup("int")->type);
append($$.code,
mkquad(qADDI,$1.offset,$$.offset,ofs));
$$.offset=ofs;
}
}
53
Politecnico di Torino
213
Accesso ad elementi di array
215
☛ Istruzioni di costruzioni dell’offset seguite da un
assegnamento con dereferenziazione ed offset.
MULI
MULI
ADDI
DRF_OFS
j
k
%0
v
20
4
%1
%2
%0
%1
%2
i
int
int
int
int
……
ii ==
☛ La quadrupla:
DRF_OFS base, offset, dest
equivale a:
dest = base [ offset ]
214
v[j][k];
v[j][k];
216
☛ Rappresentazione di un tipo struttura:
struct
size=12
struct
size=8
i = s.cf.b;
i
offset=0
cf
offset=4
a
offset=0
b
offset=4
Accesso ad elementi di strutture
☛ Costruzione dell’offset ed assegnamento con
dereferenziazione ed offset, come per gli array.
☛ Invece degli indici utilizza gli
struct
struct tag
tag {{
offset dei campi all’interno
int
delle strutture.
int i;
i;
ADDI
4
DRF_OFS s
int
size=4
Versione 1.2 © 2000 Marco Torchiano
Accesso ad elementi di strutture
| Expr '.' ID
{ st_entry* stp; t_op ofs;
stp=(st_entry*)ht_lookup($1.type->r.table,$3);
$$.type=stp->type;
$$.code=$1.code;
if($1.offset==NULL){
$$.offset=new_ival(stp->offset);
}else{
t_op ofs;
ofs=new_ival(stp->offset);
$$.offset=new_temp(st_lookup("int")->type);
$$.code=append($1.code,
mkquad(qADDI,$1.offset,ofs,$$.offset));
}
}
i,j,k;
i,j,k;
v[5][2];
v[5][2];
Accesso ad elementi di strutture
struct tag {
int i;
struct x {
int a;
int b;
} cf;
} s;
Laboratorio di Compilatori
a.a 1999/2000
4
%0
%0
i
struct
struct xx {{
int
int a;
a;
int
int b;
b;
}} cf;
cf;
}} s;
s;
...
...
ii == s.cf.b;
s.cf.b;
54
Politecnico di Torino
217
Laboratorio di Compilatori
a.a 1999/2000
Uso avanzato di Bison
☛ Gestione degli errori sintattici
✏ errori negli statement
✏ gestione del contesto
✏ interazione con lo scanner
☛ Debugging dei parser
219
Statement ed espressioni
stmt : …
| error ';' { error("syntax error in statement"); }
compound : …
| '{' stmts error '}'{ error("missing ; before '}'");}
exp: …
|'(' error ')'{ error("syntax error in expression"); }
✏ conflitti shift-reduce
✏ conflitti reduce-reduce
yyerror(char* s){ }
void error(char* s){
printf("%s:%d: %s\n",filename,yylineno,s);
}
218
file
Grammatica
:
;
funcs :
|
;
func :
funcs
/* empty */
funcs func
ID '(' ')'
compound
;
stmts : /* empty */
| stmts stmt
;
Versione 1.2 © 2000 Marco Torchiano
220
stmt
: exp ';'
| compound
;
compound: '{' stmts '}'
;
exp
: NUM
| exp '+' exp
| exp '-' exp
| exp '*' exp
| exp '/' exp
| '-' exp %prec NEG
;
Errori negli statement
NUM
stmts -> stmts . stmt
10
compound -> '{' stmts . '}'
compound -> '{' stmts . error '}'
error
exp -> NUM .
12
exp
stmt -> exp . ';'
18
exp -> exp . '+' exp
11
stmt -> error . ';'
compound -> '{' stmts error . '}'
...
‘+’
+
25 (exp)
10 (stmts)
3
+
;
exp -> exp '+' . exp 25
55
Politecnico di Torino
221
Contesto
func
: ID '(' ')'
compound
223
{ new_context($1); }
{ out_context(); }
Interazione con lo scanner
224
funcs : …
| funcs error
{ if(yychar<256){
char buf[255];
sprintf(buf,"syntax error before '%c'",yychar);
error(buf);
yyclearin;
}else
error("syntax error");
}
exp: …
| '(' error ';'
{
error("missing ')' before ';'");
pushback(';'); }
void pushback(char ch){
Versione 1.2 © 2000 Marco Torchiano
Esempio
1
2
3
4
5
6
7
8
9
10
11
12
13
14
if(!context_printed){
if(context)
printf("%s: In function %s:\n",
filename,context);
else
printf("%s: At top level:\n",filename);
context_printed = 1;
}
222
Laboratorio di Compilatori
a.a 1999/2000
f(){
1 + 1 ;
1 + ( 6 + ) ;
1 + (5 + ;
3 + ;
{ 3 + 2 ; }
}
?
g(){
{ 3 + 3 }
{ 3 + (3-) }
3+1
}
prova.c:
prova.c:In
Infunction
functionf:f:
prova.c:3:
prova.c:3:syntax
syntaxerror
errorin
inexpression
expression
prova.c:4:
missing
')'
before
prova.c:4: missing ')' before';'';'
prova.c:5:
prova.c:5:syntax
syntaxerror
errorin
instatement
statement
prova.c:
prova.c:At
Attop
toplevel:
level:
prova.c:8:
prova.c:8:syntax
syntaxerror
errorbefore
before'?'
'?'
prova.c:
In
function
g:
prova.c: In function g:
prova.c:10:
prova.c:10:missing
missing;;before
before'}'
'}'
prova.c:11:
prova.c:11:syntax
syntaxerror
errorin
inexpression
expression
prova.c:11:
prova.c:11:missing
missing;;before
before'}'
'}'
prova.c:14:
prova.c:14:missing
missing;;before
before'}'
'}'
Debugging del parser
☛ L’opzione -v di Bison genera un file, con estensione
out, in cui è descritto il parser generato.
☛ La struttura del file è la seguente:
✏
✏
✏
✏
✏
elenco delle anomalie e dei conflitti (eventualmente risolti)
regole della grammatica senza alternative
lista dei terminali con i loro usi
lista dei non terminali, con definizioni e usi
elenco degli stati del parser
nello
nelloscanner
scanner
perchè
perchèunput
unput
èèuna
unamacro!
macro!
unput(ch); }
56
Politecnico di Torino
225
Liste
☛ rule 1
☛ rule 2
227
S -> '{' L '}' …
L ->
/* empty */
S : ‘{‘ L ‘}’ ‘{‘ M ‘}’ …
M : ID '(' ')' { /* */ } '{' '}'
| ID '(' ')' '{' '?' '}'
☛ rule 6
Liste
L
: /* empty */
|E
|LE
;
Riscrittura
L
L
ε
E
L
Versione 1.2 © 2000 Marco Torchiano
L
: /* empty */
| Lst
;
Lst : E
| Lst E
;
@1 ->
/* empty */
state 12
M -> ID '(' ')' . @1 '{' '}' (rule 7)
M -> ID '(' ')' . '{' '?' '}' (rule 8)
'{' shift, and go to state 14
'{' [reduce using rule 6 (@1)]
$default
reduce using rule 6 (@1)
@1 go to state 15
228
èè ambigua
ambigua
Azioni intermedie
S : ‘{‘ L ‘}’ …
L : /* epsilon */
| E
| L E ;
E : ID ;
state 1
S -> '{' . L '}' … (rule 1)
ID shift, and go to state 2
ID [reduce using rule 2 (L)]
$default
reduce using rule 2 (L)
L go to state 3
E go to state 4
226
Laboratorio di Compilatori
a.a 1999/2000
Reduce-reduce e contesto
☛ S : … ‘{‘ A ‘}’
☛A:B
| A ',' B
☛ rule 20 C -> ID
☛ rule 21 D -> ID
BB
CC
DD
state 27
C -> ID . (rule 20)
D -> ID . (rule 21)
'}' reduce using rule 20 (C)
'}' [reduce using rule 21 (D)]
',' reduce using rule 20 (C)
',' [reduce using rule 21 (D)]
$default
reduce using rule 20 (C)
::
||
;;
::
;;
::
||
;;
CC
DD
ID
ID
ID
ID
DD ','
',' ID
ID
57
Politecnico di Torino
229
a.a 1999/2000
Laboratorio di Compilatori
Reduce-reduce ed epsilon
state 16
S -> … '{' N . '}' (rule 1)
N -> N . O (rule 10)
N -> N . Q (rule 11)
'}' shift, and go to state 19
ID reduce using rule 12 (O)
ID [reduce using rule 14 (Q)]
'}' [reduce using rule 12 (O)]
'}' [reduce using rule 14 (Q)]
'!'
reduce using rule 12 (O)
'!'
[reduce using rule 14 (Q)]
$default
reduce using rule 12 (O)
…
Versione 1.2 © 2000 Marco Torchiano
SS :: …… ‘{‘
‘{‘ NN ‘}’
‘}’ ……
NN :: /*
/* empty
empty */
*/ 9
|| NN OO
10
|| NN QQ
11
;;
OO :: /*
/* empty
empty */
*/ 12
|| OO ID
13
ID
;;
QQ :: /*
/* empty
empty */
*/ 14
|| QQ '!’
15
'!’
;;
58