1. LFC
Il front-end di un
compilatore
Architettura del front-end
Symbol table
Traduzione guidata dalla
sintassi
Generazione del
Linguaggi formali e compilazione three-address code
Corso di Laurea in Informatica
A.A. 2008/2009
2. LFC
Linguaggi formali e compilazione
Il front-end di un
compilatore
Architettura del front-end
Symbol table
Traduzione guidata dalla
sintassi
Generazione del
three-address code
Il front-end di un compilatore
Architettura del front-end
Symbol table
Traduzione guidata dalla sintassi
Generazione del three-address code
3. LFC
Linguaggi formali e compilazione
Il front-end di un
compilatore
Architettura del front-end
Symbol table
Traduzione guidata dalla
sintassi
Generazione del
three-address code
Il front-end di un compilatore
Architettura del front-end
Symbol table
Traduzione guidata dalla sintassi
Generazione del three-address code
4. LFC
Componenti del front-end
Il front-end di un
compilatore
Architettura del front-end
Symbol table
Traduzione guidata dalla
Il front-end di un compilatore (cioè la parte che
◮ sintassi
Generazione del
termina con la creazione di una rappresentazione three-address code
intermedia del programma) è costituita dai seguenti
moduli:
analizzatore lessicale;
◮
parser;
◮
symbol table;
◮
generatore di codice intermedio.
◮
Per ragioni di tempo non ci potremo occupare, in
◮
questo corso, della gestione degli errori.
5. LFC
Il modello di front-end
Il front-end di un
compilatore
Architettura del front-end
Symbol table
Traduzione guidata dalla
La seguente figura illustra un tipico schema di
◮ sintassi
Generazione del
organizzazione del front-end di un compilatore (tratto three-address code
da Aho, Lam, Sethi, Ullman (2007)).
Richiesta
token
three-addreess
Programma Albero
code
sorgente Generatore di
Analizzatore sintattico
Parser
codice intermedio
lessicale
Token
Gestore della
symbol table
6. LFC
Analizzatore lessicale
Il front-end di un
L’analizzatore lessicale ha il fondamentale ruolo di
◮ compilatore
Architettura del front-end
leggere la sequenza di caratteri che forma l’input file Symbol table
Traduzione guidata dalla
e inviare al parser una sequenza di token, oggetti sintassi
Generazione del
astratti che coincidono essenzialmente con i simboli three-address code
terminali del linguaggio (ma vedremo subito dopo
una definizione precisa).
Ci sono però altri compiti che deve svolgere
◮
l’analizzatore lessicale:
riconoscere e “filtrare” commenti, spazi e altri
◮
caratteri di separazione;
associare gli eventuali errori trovati da altri moduli del
◮
compilatore (in particolare dal parser) alle posizioni
(righe di codice) dove tali errori si sono verificati allo
scopo di emettere appropriati messaggi diagnostici;
procedere all’eventuale espansione delle macro (se il
◮
compilatore le prevede).
7. LFC
Interazione parser-lexical analyzer
Il front-end di un
L’analizzatore lessicale è normalmente programmato compilatore
◮
Architettura del front-end
come “routine” del parser. Symbol table
Traduzione guidata dalla
sintassi
Quando il parser deve leggere il prossimo simbolo di
◮ Generazione del
three-address code
input (si ricordino gli esempi di parser, top down o
bottom up indifferentemente, visti nel gruppo di lucidi
relativi al parsing di linguaggi liberi) esegue in realtà
una chiamata all’analizzatore lessicale;
quest’ultimo si attiva leggendo la “prossima”
◮
porzione di input fino a quando non riconosce un
simbolo terminale della grammatica, che viene
restituito al parser;
ciò che viene propriamente restituito è un token
◮
name, una delle due componenti che costituiscono il
token vero e proprio.
8. LFC
Token
Il front-end di un
compilatore
Un token è un oggetto astratto caratterizzato da due Architettura del front-end
◮
Symbol table
attributi, un token name (obbligatorio) e un valore Traduzione guidata dalla
sintassi
opzionale. Generazione del
three-address code
Ecco alcuni esempi di sequenze di caratteri
◮
riconosciuti come token da un compilatore C++:
“0.314E+1”, cui corrisponde la coppia (token) in cui
◮
il nome è number e il valore è il numero razionale
3.14, opportunamente rappresentato;
“x1”, cui corrisponde un token in cui il nome è id e il
◮
cui valore è, a sua volta, un insieme di informazioni
elementari (la stringa di caratteri che forma
l’identificatore, il suo tipo, il punto del programma
dove è stato definito);
“else”, cui corrisponde il solo token name else.
◮
9. LFC
Token name
Il front-end di un
Il nome del token serve principalmente al parser, in compilatore
◮
Architettura del front-end
quanto sono propriamente i token name che Symbol table
Traduzione guidata dalla
coincidono con i simboli terminali della grammatica. sintassi
Generazione del
three-address code
Il valore dei token è invece (in prima istanza) non
◮
rilevante dal punto di vista della correttezza sintattica
del programma.
Ad esempio, le frasi “x<10” e “pippo!=35” sono del
◮
tutto equivalenti e riconducibili alla frase generica in
cui il valore di un identificatore è confrontato con un
numero.
Una tale frase può quindi essere astrattamente
◮
descritta come “id comparison number”, dove
abbiamo utilizzato il nome comparison per il token
che descrive gli operatori relazionali.
10. LFC
Token value
Il front-end di un
compilatore
Architettura del front-end
Symbol table
Il valore di un token gioca il ruolo principale nella
◮ Traduzione guidata dalla
sintassi
traduzione del codice. Generazione del
three-address code
Con riferimento agli esempi del lucido precedente:
◮
di un identificatore è importante sapere (tra gli altri) il
◮
tipo, perché da questo dipende (in particolare) la
quantità di memoria allocare;
di un operatore di confronto è necessario sapere
◮
esattamente di quale confronto si tratta, per poter
predisporre il test e l’eventuale istruzione di salto
opportuna;
di una costante numerica è necessario sapere il
◮
valore per effettuare l’inizializzazione corretta.
11. LFC
Token (continua)
Il front-end di un
compilatore
Architettura del front-end
Il progettista del linguaggio stabilisce quali debbano
◮ Symbol table
Traduzione guidata dalla
essere considerati i simboli terminali del linguaggi, e sintassi
Generazione del
quindi i token. three-address code
Naturalmente, a tali simboli potranno corrispondere
◮
(in generale) molte stringhe alfanumeriche nel codice
sorgente (si pensi ai token che descrivono numeri o
identificatori).
Il progettista deve quindi stabilire esattamente anche
◮
la mappatura fra stringhe e token name/terminali del
linguaggio.
Tale mappatura viene tipicamente descritta, come
◮
già sappiamo, mediante espressioni regolari.
12. LFC
Pattern, lessemi e terminali
Il front-end di un
compilatore
Architettura del front-end
Symbol table
Traduzione guidata dalla
Una descrizione ben formalizzata di quanto appena sintassi
◮
Generazione del
three-address code
ricordato può essere fatta utilizzando i concetti di
lessema e pattern.
Un pattern è una descrizione (formale o informale,
◮
ma tipicamente esprimibile mediante espressioni
regolari) delle stringhe che devono essere
riconosciute (dall’analizzatore lessicale) come
istanze di un determinato simbolo terminale.
Le stringhe stesse sono dette lessemi.
◮
13. LFC
Pattern, lessemi e terminali (continua)
Il front-end di un
compilatore
Architettura del front-end
Symbol table
Traduzione guidata dalla
La seguente tabella illustra, mediante alcuni esempi,
◮ sintassi
Generazione del
i concetti che stiamo analizzando. three-address code
Token name Pattern Esempio di lessema
id letter(letter | digit)∗ pippo1
number [+ | -]nzd digit∗ [.]digit∗ -3.14
comparison <, >, <=, >=, =, != <
literal “[:alpha:]” “Pi greco”
if if if
while while while
Si noti che, per ragioni di spazio, il pattern che descrive il
token number non prevede la notazione scientifica.
14. LFC
Linguaggi formali e compilazione
Il front-end di un
compilatore
Architettura del front-end
Symbol table
Traduzione guidata dalla
sintassi
Generazione del
three-address code
Il front-end di un compilatore
Architettura del front-end
Symbol table
Traduzione guidata dalla sintassi
Generazione del three-address code
15. LFC
Symbol table
Come evidenziato dalla struttura del front-end,
◮
Il front-end di un
l’analizzatore lessicale presenta al parser una compilatore
Architettura del front-end
funzione, get-next-token(), che il parser invoca Symbol table
Traduzione guidata dalla
quando deve leggere il prossimo simbolo terminale sintassi
Generazione del
della grammatica. three-address code
Il risultato esplicito della get-next-token() è il
◮
nome del prossimo token.
Nei casi in cui è significativo anche il valore (fanno
◮
eccezione le keyword, simboli di separazione come il
punto e virgola e gli operatori), l’analizzatore
lessicale restituisce anche (tipicamente) un
puntatore alla symbol table dove è stato
memorizzato il valore stesso.
In pratica, la funzione get-next-token() può
◮
restituire una coppia nome, valore , oppure (come
nel caso di Lex/Yacc) il valore viene restituito
attraverso una variabile globale.
16. LFC
Esempio
Il front-end di un
Nel caso del seguente frammento di programma C++
◮ compilatore
Architettura del front-end
while (i<10) { Symbol table
Traduzione guidata dalla
sintassi
i=i+1; Generazione del
three-address code
}
l’analizzatore lessicale restituisce al parser la
seguente sequenza di token:
while id, pointer
leftpar assign_op
id, pointer id, pointer
comparison, pointer plus_op
number, pointer number, pointer
rightpar semicolon
leftbrace rightbrace
17. LFC
Symbol table (continua)
Il front-end di un
Come già detto, le informazioni raccolte nella symbol compilatore
◮
Architettura del front-end
table, relative ai token ai quali è associato un valore, Symbol table
Traduzione guidata dalla
sono fondamentali nella generazione del codice, ma sintassi
Generazione del
three-address code
non solo.
Mediante tali informazioni il front-end può anche
◮
completare l’analisi sintattica del programma e,
dicendo questo, intendiamo procedere a chiarire un
aspetto che potrebbe essere rimasto oscuro,
In precedenza abbiamo affermato che, dal punto di
◮
vista sintattico, frasi come i < 10 o j < 10 sono del
tutto equivalenti,
Questo non è del tutto vero e non solo perché esse
◮
(in termini di codice generato) significano cose
diverse.
18. LFC
Symbol table (continua)
Infatti, in molti linguaggio di programmazione
◮
Il front-end di un
moderni, una variabile deve essere dichiarata prima compilatore
Architettura del front-end
di poter essere utilizzata. Symbol table
Traduzione guidata dalla
sintassi
Se, con riferimento all’esempio appena fatto,
◮ Generazione del
three-address code
l’identificatore i fosse stato dichiarato mentre j no,
allora la presenza della seconda frase genererebbe
un errore.
Usare una variabile non definita è un errore
◮
sintattico? Si e no.
Teoricamente sarebbe possibile inserire nella
◮
grammatica il vincolo che ogni variabile deve essere
dichiarata prima dell’uso.
Questo però richiederebbe l’uso di produzioni (e
◮
quindi di grammatiche) non context-free, per le quali
non è neppure garantita l’esistenza di algoritmi di
parsing.
19. LFC
Symbol table (continua)
La soluzione adottata è quindi di non inserire tali
◮
Il front-end di un
regole nella grammatica ma di controllare il compilatore
Architettura del front-end
soddisfacimento dei vincoli in altro modo, cioè Symbol table
Traduzione guidata dalla
proprio mediante l’uso della symbol-table. sintassi
Generazione del
three-address code
Più precisamente, quando il parser incontra un
◮
identificatore nel contesto, poniamo, di
un’espressione, va a verificare nella symbol table se
tale identificatore è già presente (ed eventualmente
con quale tipo è stato dichiarato).
Se non è presente (ovvero se il tipo non è quello
◮
“giusto”) segnala una condizione di errore, che
considereremo di natura semantica.
Un altro esempio di vincolo non facilmente
◮
descrivibile con produzioni libere è quello della
concordanza fra numero di parametri formali di una
procedura e argomenti ad essa passati nelle varie
chiamate.
20. LFC
Symbol table (continua)
Il front-end di un
compilatore
Precisiamo bene come tali controlli vengono svolti
◮ Architettura del front-end
Symbol table
dal front-end usando la symbol table, che può essere Traduzione guidata dalla
sintassi
strutturata in modo tale da soddisfare la più ampia Generazione del
three-address code
esigenza rappresentata dalla implementazione delle
regole di ambiente o di visibilità dei simboli (in
inglese scoping rule).
Ricordiamo che lo scope di una variabile (e della
◮
dichiarazione che tale variabile introduce) è la
porzione di programma in cui tale variabile è visibile
(e dunque utilizzabile).
Lo stesso identificare (un caso comune è i) può
◮
essere definito più volte in un programma, ma ad
esso saranno associati ambienti diversi.
21. LFC
Symbol table (continua)
Nei moderni linguaggi di programmazione l’ambiente
◮ Il front-end di un
compilatore
di una variabile è “quasi sempre” determinabile in Architettura del front-end
modo statico, cioè guardando il testo del programma Symbol table
Traduzione guidata dalla
sintassi
(se escludiamo alcuni casi ricercati). Generazione del
three-address code
In particolare, le regole di visibilità degli identificatori
◮
riflettono la struttura di annidamento dei blocchi di
programma (incluse le procedure e le classi, se
previste).
Come detto, in quaesto caso si parla di static (o
◮
lexical) scoping.
Dal punto di vista del compilatore, la nozione di
◮
ambiente può essere realizzata definendo una nuova
symbol table all’ingresso di ogni blocco di
programma e “concatenando” le simbol table in una
lista, in cui la testa è sempre l’ultima symbol table
definita.
22. LFC
Esempio
Il front-end di un
compilatore
Architettura del front-end
Symbol table
Si consideri, come esempio, il seguente semplice
◮ Traduzione guidata dalla
sintassi
frammento di programma C++: Generazione del
three-address code
1. { int i,j;
2. cin » i » j;
{ int i; float x;
3.
4. i=1;
5. x=2.0*j;
6. cout « i « “: ” « x;
}
7.
8. cout « i « “: ” « j;
9. }
23. LFC
Esempio (continua)
Chiameremo ambiente la lista di symbol table.
◮
Il front-end di un
compilatore
All’ingresso del blocco più esterno viene creata una
◮ Architettura del front-end
symbol table S1 (essenzialmente un dizionario) che Symbol table
Traduzione guidata dalla
sintassi
diviene la testa di ambiente. Generazione del
three-address code
Ogni volta che viene incontrata una definizione,
◮
l’identificatore viene inserito nella symbol table alla
testa di ambiente.
Ne consegue che gli identificatori i e j di riga 1
◮
vengono inseriti in S1 .
All’ingresso del blocco interno (riga 3) viene creata
◮
(e inserita in testa ad ambiente) una seconda symbol
table, S2 , nella quale vengono poi inseriti gli
identificatori i e x di riga 3 (col loro tipo).
All’uscita di ogni blocco, il puntatore alla testa di
◮
ambiente viene fatto avanzare, con l’effetto di
rendere inaccessibile la symbol table di testa.
24. LFC
Esempio (continua)
Il front-end di un
compilatore
Architettura del front-end
Quando nel programma si incontra un riferimento ad
◮ Symbol table
Traduzione guidata dalla
una variabile, l’identificatore relativo viene cercato sintassi
Generazione del
nella lista ambiente, seguendo il naturale ordine. three-address code
Ad esempio, il riferimento alla variabile i di riga 4
◮
viene risolto con un lookup alla symbol table di testa
(S2 ), che contiene un’entry con chiave i.
Si noti quindi che, nel blocco interno, la variabile i
◮
definita alla riga 1 diviene inaccessibile.
Il riferimento alla variabile j di riga 5 richiede invece
◮
il lookup di due symbol table, nell’ordine in cui sono
presenti nella lista ambiente: S2 (search miss) ed S1
(search hit).
25. LFC
Symbol table (continua)
Il front-end di un
compilatore
Architettura del front-end
Come già sottolineato, oltre alla implementazione Symbol table
◮ Traduzione guidata dalla
sintassi
dello scope (e alla verifica del soddisfacimento di Generazione del
three-address code
vincoli non facilmente esprimibili a livello
grammaticale), le symbol table mantengono
informazioni fondamentali per la generazione del
codice
Ad esempio, la entry per un identificatore contiene:
◮
la sequenza di caratteri che ne costituisce il lessema
◮
(che può essere usata come chiave per l’accesso
alla tabella);
il tipo della variabile;
◮
l’indirizzo di memoria della variabile.
◮
26. LFC
Linguaggi formali e compilazione
Il front-end di un
compilatore
Architettura del front-end
Symbol table
Traduzione guidata dalla
sintassi
Generazione del
three-address code
Il front-end di un compilatore
Architettura del front-end
Symbol table
Traduzione guidata dalla sintassi
Generazione del three-address code
27. LFC
Le due parti del compilatore
Il passaggio dal codice sorgente ad un codice Il front-end di un
◮
compilatore
macchina efficiente viene tipicamente spezzato in Architettura del front-end
Symbol table
due parti. Traduzione guidata dalla
sintassi
Generazione del
La prima parte, gestita dal front-end del compilatore,
◮ three-address code
termina con la generazione di un opportuno codice
intermedio ed è chiaramente dominata dalle
caratteristiche del linguaggio sorgente.
La seconda parte, gestita dal back-end, è invece
◮
specializzata ad ottenere un codice macchina
efficiente in funzione della particolare architettura.
La suddivisione netta del progetto di un compilatore
◮
in front-end e back-end (la prima indipendente
dall’architettura e la seconda indipendente dal
linguaggio) ha, fra le altre proprietà, un notevole
impatto in termini di economicità di progetto.
28. LFC
Rappresentazioni intermedie
Il front-end di un
Le rappresentazioni intermedie più importanti sono i compilatore
◮
Architettura del front-end
syntax tree e il cosiddetto three-address code. Symbol table
Traduzione guidata dalla
sintassi
Per ora ci concentriamo sui syntax tree, posponendo
◮ Generazione del
three-address code
alla sezione sulla generazione del codice intermedio
la definizione di three-address code.
Un abstract syntax tree per un linguaggio L è un
◮
albero in cui:
i nodi interni rappresentano costrutti di L;
◮
i figli di un nodo che rappresenta un costrutto C
◮
rappresentano a loro volta le componenti significative
di C;
le foglie sono “costrutti elementari” (non
◮
ulteriormente decomponibili) caratterizzati da un
valore lessicale (tipicamente un numero o un
puntatore alla symbol table).
29. LFC
Esempio
Il front-end di un
Un abstract syntax tree per la frase id + id × id è
◮ compilatore
Architettura del front-end
+ Symbol table
Traduzione guidata dalla
sintassi
Generazione del
×
id, ptr three-address code
id, ptr id, ptr
Abstract syntax tree e parse tree sono oggetti diversi.
◮
E
E + T
×
T T F
F F id
id id
30. LFC
Rappresentazioni intermedie
Per alcuni semplici linguaggi interpretati, una Il front-end di un
◮
compilatore
rappresentazione finale basata su una struttura ad Architettura del front-end
Symbol table
albero (da dare poi in input all’interprete) può essere Traduzione guidata dalla
sintassi
sufficiente. Generazione del
three-address code
Questo è il caso, ad esempio, della realizzazione di
◮
un semplice “desk calculator” (interattivo) che
accetta in input formule che seguono la sintassi
classica (con operatori infissi).
In questo caso, infatti, è semplice scrivere un
◮
interprete per la formula rappresentata come
abstract syntax tree.
In generale, tuttavia, i syntax tree costituiscono solo
◮
un passaggio intermedio verso al generazione di
codice intermedio (tipicamente il three-address
code).
31. LFC
I vari passaggi in dettaglio
Per chiarezza didattica, possiamo “in prima battuta” Il front-end di un
◮
compilatore
immaginare che tutte le strutture intermedie che Architettura del front-end
Symbol table
portano alla generazione del three-address code Traduzione guidata dalla
sintassi
vengano effettivamente costruite. Generazione del
three-address code
A partire dalla sequenza di token restituita
◮
dall’analizzatore sintattico, possiamo quindi
immaginare (per il momento) che il parser costruisca
esplicitamente tanto il parse tree quanto l’abstract
syntax tree delle stringhe “corrette”.
In un compilatore reale, tuttavia, il parser emette il
◮
three-address code senza costruire “esplicitamente”
il syntax tree ne’ il parse tree.
In realtà, anche nell’ambito della nostra trattazione
◮
semplificata vedremo come almeno la generazione
del parse tree sia (quasi sempre) evitabile.
32. LFC
Traduzione guidata dalla sintassi
La generazione dell’abstract syntax tree, il nostro
◮
Il front-end di un
obiettivo attuale, può essere eseguita mediante compilatore
l’applicazione di una tecnica nota come Traduzione Architettura del front-end
Symbol table
guidata dalla sintassi (in inglese Syntax-Directed Traduzione guidata dalla
sintassi
Generazione del
Translation). three-address code
In altre parole, l’output desiderato (un abstract syntax
◮
tree) viene generato mediante un processo che è
guidato dalla stessa grammatica e, in particolare,
dalle sue produzioni.
Tipicamente, quando viene applicata una produzione
◮
(nel parsing top-down) oppure viene effettuata una
riduzione (nel parsing bottom-up) viene anche
effettuata una qualche azione di costruzione
dell’abstract syntax tree.
La modalità specifica che vedremo per la
◮
realizzazione del processo di traduzione è quella
degli schemi di traduzione (syntax-directed
translation scheme).
33. LFC
Syntax-directed translation scheme
Uno schema di traduzione prevede l’inserimento di
◮
Il front-end di un
frammenti di codice nel corpo (parte destra) delle compilatore
Architettura del front-end
produzioni. Symbol table
Traduzione guidata dalla
sintassi
Per ogni produzione, il codice viene eseguito dal
◮ Generazione del
three-address code
parser in un preciso momento che può essere prima,
durante o dopo l’applicazione della produzione
stessa.
Negli esempi di costruzione del syntax tree che
◮
svilupperemo le azioni saranno essenzialmente
operazioni di costruzione di nodi e manipolazione di
puntatori.
L’aspetto importante che dovremo chiarire è relativo
◮
al preciso ordine di esecuzione dei vari frammenti di
codice.
Dobbiamo però preliminarmente descrivere quali dati
◮
vengono manipolati dagli schemi di traduzione.
34. LFC
Attributi
Il front-end di un
Un qualunque processo di calcolo richiede la
◮ compilatore
Architettura del front-end
manipolazione di informazione, che deve essere Symbol table
Traduzione guidata dalla
memorizzata “da qalche parte”. sintassi
Generazione del
three-address code
Nel caso degli schemi di traduzione, l’informazione
◮
manipolata è costituita da attributi associati ai simboli
della grammatica.
Naturalmente abbiamo bisogno di una notazione per
◮
poter fare riferimento a tale informazione.
Se X è un simbolo della grammatica e a è il nome di
◮
un suo attributo, useremo dunque la scrittura X .a per
indicare il valore dell’attributo a di X .
Il valore di un attributo può essere di qualunque
◮
natura: stringa, numero, albero, tabella, tipo di dato,
ecc.
35. LFC
Attributi (continua)
Il front-end di un
compilatore
Si noti tuttavia che nel processo di derivazione di una
◮ Architettura del front-end
Symbol table
stringa, (e quindi nel corrispondente parse tree) un Traduzione guidata dalla
sintassi
simbolo X della grammatica può apparire più volte. Generazione del
three-address code
Dobbiamo quindi precisare che la scrittura X .a si
◮
applica non tanto al “generico” simbolo X bensì ad
una sua occorrenza nel parse tree.
In altri termini, se la scrittura X .a nel frammento di
◮
codice associato (poniamo) alla produzione
A → XYZ si dovrà intendere come riferita ad ogni
specifica applicazione della regola (che introduce il
simbolo X ), bisognerà comunque tenere conto del
fatto che, in applicazioni diverse della regola, il valore
di X .a potrà essere (e in generale sarà) diverso.
36. LFC
Attributi (continua)
Il front-end di un
compilatore
Architettura del front-end
Symbol table
Nel nostro caso, i valori degli attributi saranno
◮ Traduzione guidata dalla
sintassi
puntatori al “costruendo” syntax tree. Generazione del
three-address code
Per questa ragione, associato ad ogni non terminale
◮
della grammatica si troverà sempre un attributo di
nome node.
In alcuni casi ci potranno essere anche ulteriori
◮
attributi
Siamo ora in grado di presentare il nostro primo
◮
esempio di schema di traduzione, relativo alla “solita”
grammatica delle espressioni aritmetiche.
37. LFC
Esempio
Il front-end di un
{E .node ← new node(′ +′ , E1 .node,
→ E1 + T
E compilatore
Architettura del front-end
T .node)} Symbol table
Traduzione guidata dalla
→T {E .node ← T .node}
E sintassi
Generazione del
{T .node ← new node(′ ×′ , T1 .node,
→ T1 × F
T three-address code
F .node)}
→F {T .node ← F .node}
T
→ (E ) {F .node ← E .node}
F
→ id {F .node ← new leaf(id, id.lexval)}
F
Si noti che:
◮
ai non terminali è associato il solo attributo node;
◮
ai terminali è associato l’attributo lexval (tipicamente,
◮
un puntatore alla entry della symbol table).
l’aggiunta dell’indice ai non terminali E e T , nella
◮
parte destra delle produzioni E → E + T e
T → T × F , serve unicamente a disambiguare i
riferimenti nelle corrispondenti azioni.
38. LFC
Calcolabilità degli schemi
Il front-end di un
Come si passa dalla descrizione “statica” delle azioni
◮
compilatore
associate alle singole produzioni ad un processo di Architettura del front-end
Symbol table
calcolo precisamente definito? Traduzione guidata dalla
sintassi
Generazione del
Se immaginiamo (come più volte affermato) che il three-address code
◮
parser costruisca effettivamente il parse tree,
possiamo pensare che le azioni descritte dallo
schema vengano eseguite nell’ambito di un
“opportuno” attraversamento del parse tree stesso.
In questo modo, specificando il tipo di
◮
attraversamento, otteniamo la descrizione cercata.
Infatti, una volta stabilito l’attraversamento (ad
◮
esempio, in ordine posticipato) rimane univocamente
determinato l’ordine di visita ai nodi del parse tree
con conseguente esecuzione delle azioni specificate
dallo schema.
39. LFC
Calcolabilità degli schemi (continua)
Si noti che, in generale, il valore di un attributo Il front-end di un
◮
compilatore
presente in un nodo del parse tree dipende dal Architettura del front-end
Symbol table
valore degli attributi presenti in altri nodi. Traduzione guidata dalla
sintassi
Generazione del
L’ordine di attraversamento dell’albero deve quindi
◮ three-address code
garantire il soddisfacimento della fondamentale
proprietà che il valore di ogni attributo presente nello
schema sia calcolato prima di essere utilizzato.
Ad esempio, con riferimento allo schema introdotto, i
◮
valori degli attributi T1 .node e F .node dovranno
essere disponibili nel momento in cui viene eseguito
l’assegnamento
T .node ← new node(‘×′ , T1 .node, F .node).
Si tratta di un punto molto delicato, al quale
◮
dobbiamo dedicare attenzione specifica utilizzando
anche la terminologia appropriata.
40. LFC
Il ruolo del parse tree
Prima di procedere, vogliamo però soffemarci sul Il front-end di un
◮
compilatore
ruolo del parse tree nei processi di calcolo dei quali Architettura del front-end
Symbol table
stiamo discutendo. Traduzione guidata dalla
sintassi
Generazione del
Tale ruolo è essenzialmente di “raccordo”, in quanto
◮ three-address code
il processo di parsing consente di generare in modo
naturale prorpio il parse tree e non (direttamente) il
syntax tree.
Il parse tree offre, da un lato, un “posto” per
◮
memorizzare gli attributi (cioè l’informazione
manipolata dagli schemi), dall’altro (come abbiamo
appena visto) un modo per descrivere l’ordine di
esecuzione delle azioni dello schema.
Va comunque da se che, qualora si trovino modi più
◮
efficienti per risolvere questi due problemi, la
costruzione esplicita del parse tree diviene superflua.
41. LFC
Tipi di attributi
Il front-end di un
compilatore
Tornando al nostro problema (garantire cioè che il
◮ Architettura del front-end
Symbol table
calcolo del valore di un attributo venga eseguito solo Traduzione guidata dalla
sintassi
quando tutti gli attributi ai quali esso fa riferimento Generazione del
three-address code
siano stati a loro volta calcolati), definiamo le due
diverse tipologie di attributi utilizzate nelle
applicazioni reali: gli attributi sintetizzati e gli attributi
ereditati.
Con riferimento ad una generica produzione
◮
A → X1 . . . Xi−1 Xi . . . Xk
un attributo per A si dice sintetizzato se il suo valore
dipende solo dal valore degli attributi dei simboli Xi
nella parte destra della produzione.
42. LFC
Tipi di attributi (continua)
Il front-end di un
compilatore
Architettura del front-end
Più precisamente, un attributo che etichetta un nodo
◮ Symbol table
Traduzione guidata dalla
del parse tree è sintetizzato se il suo valore dipende sintassi
Generazione del
solo dal valore degli attributi nei nodi figli. three-address code
Ci si può facilmente rendere conto che questa
◮
condizione vale per tutti gli attributi dell’unico schema
di traduzione finora visto come esempio.
Uno schema in cui tutti gli attributi sono sintetizzati è
◮
detto S-attributed.
Se associamo ad uno schema S-attributed un ordine
◮
di attraversamento posticipato del parse tree, la
proprietà fondamentale viene automaticamente
soddisfatta.
43. LFC
Esempio
Attraversando in ordine anticipato il parse tree Il front-end di un
◮
compilatore
relativo alla stringa di input id + id × id si ottiene un Architettura del front-end
Symbol table
processo che costruisce correttamente il syntax tree. Traduzione guidata dalla
sintassi
Generazione del
Il risultato completo del processo (incluso il valore
◮ three-address code
associato a tutti gli attributi utilizzati nello schema)
può essere descritto mediante il cosiddetto
annotated parse tree, che mostriamo di seguito.
E E.node <,>
E E.node + T T.node <,>
T T.node T T.node F F.node
F F.node F F.node id <id,id.lexval>
id <id,id.lexval> id <id,id.lexval>
44. LFC
Esempio (continua)
Il front-end di un
compilatore
Architettura del front-end
Nell’annotated parse tree sono evidenziati gli attributi
◮ Symbol table
Traduzione guidata dalla
presenti ai vari nodi nodi e il loro valore, che (in sintassi
Generazione del
questo caso) è un puntatore ai nodi dell’abstract three-address code
syntax tree.
Per semplificare l’interpretazione della figura,
◮
abbiamo usato frecce diverse per rappresentare
derivazioni (e dunque puntatori ai nodi del parse
tree) e valori degli attributi (puntatori ai nodi
dell’abstract syntax tree).
Comprendere il modo in cui si arriva alla situazione
◮
illustrata eseguendo le azioni è, in questo caso,
molto semplice.
45. LFC
Esempio (continua)
Il front-end di un
compilatore
Architettura del front-end
Seguendo l’ordine di attraversamento posticipato ad
◮ Symbol table
Traduzione guidata dalla
ogni nodo A si esegue l’azione prevista dallo schema sintassi
Generazione del
in corrispondenza della produzione A → XYZ , dove three-address code
X , Y e Z sono i figli del nodo A.
Si noti che, in realtà, non è necessario neppure
◮
seguire strettamente l’ordine di attraversamento
posticipato; è infatti sufficiente procedere in modo
bottom up, dalle foglie verso la radice, con notevoli
gradi di libertà sull’ordine preciso di esecuzione delle
azioni descritte dallo schema.
Si noti infine che il syntax tree completo è accessibile
◮
attraverso l’attributo E .node nella radice parse tree.
46. LFC
Tipi di attributi (riprende)
Ancora con riferimento alla generica produzione
◮
Il front-end di un
compilatore
A → X1 . . . Xi−1 Xi . . . Xk Architettura del front-end
Symbol table
Traduzione guidata dalla
un attributo per Xi si dice ereditato se il suo valore sintassi
Generazione del
three-address code
dipende (eventualmente) dal valore di attributi
ereditati di A e di attributi (ereditati o sintetizzati) di
X1 , . . . , Xi−1 .
Con riferimento al parse tree, un attributo a un nodo
◮
è ereditato se il suo valore dipende dal valore di
attributi ereditati al nodi genitore o attributi (di
qualunque tipo) ai nodi che sono “fratelli maggiori”.
Questa definizione non è la più generale ma, di fatto,
◮
è quella operativa.
In particolare, schemi di traduzione in cui vi sia (oltre
◮
ad eventuali attributi sintetizzati) almeno un attributo
ereditato secondo questa definizione vengono detti
L-attributed.
47. LFC
Esempio
Il seguente schema di traduzione, relativo alla
◮ Il front-end di un
compilatore
grammatica per le espressioni modificata (per Architettura del front-end
renderla adatta al parsing a discesa ricorsiva), è Symbol table
Traduzione guidata dalla
sintassi
L-attributed. Generazione del
three-address code
Si noti che alcuni nodi hanno due attributi,
◮
sintetizzato (node) ed ereditato (inh).
E → TE ′ {E ′ .inh ← T .node; E .node ← E ′ .node}
E ′ → +TE1
′ {E1 .inh ← new node(′ +′ , E ′ .inh, T .node);
′
′ .node ← E ′ .node}
E 1
E′ → ǫ {E ′ .node ← E ′ .inh}
T → FT ′ {T ′ .inh ← F .node; T .node ← T ′ .node}
T ′ → ×FT1
′ {T1 .inh ← new node(′ ×′ , T ′ .inh, F .node);
′
T ′ .node ← T1 .node}
′
T′ → ǫ ′ .node ← T ′ .inh}
{T
F → (E ) {F .node ← E .node}
F → id {F .node ← new leaf(id, id.lexval)}
48. LFC
Calcolabilità degli schemi (riprende)
Il front-end di un
compilatore
Architettura del front-end
Vedremo un poco più avanti la logica che sta dietro
◮ Symbol table
Traduzione guidata dalla
lo schema L-attributed dell’esempio appena visto. sintassi
Generazione del
three-address code
Qui vogliamo riconsiderare per un istante il problema
◮
generale della calcolabilità degli schemi.
Esiste infatti un rischio insito nella definizione di
◮
attributi con dipendenze “arbitrarie”, e cioè che non
esista nessun ordine di esecuzione delle azioni in
grado si soddisfare la proprietà fondamentale.
Comunque, sempre nel caso generale, la
◮
determinazione dell’esistenza di un tale ordine di
esecuzione costituisce problema algoritmico molto
difficile.
49. LFC
Calcolabilità degli schemi (continua)
Il front-end di un
compilatore
Architettura del front-end
Symbol table
Traduzione guidata dalla
sintassi
È alla luce di ciò che dobbiamo “apprezzare” la bontà Generazione del
◮ three-address code
degli schemi S-attributed e L-attributed.
Infatti, l’esistenza (e la immediata determinazione) di
◮
un ordine di esecuzione delle azioni previste dagli
schemi è garantita proprio nei seguenti due casi:
la grammatica ammette parsing di tipo LR e lo
◮
schema è S-attributed, oppure
la grammatica ammette parsing di tipo LL e lo
◮
schema è L-attributed.
50. LFC
Parsing LR
La prima combinazione, di grammatica analizzabile
◮ Il front-end di un
compilatore
con parser LR unitamente a schema S-attributed, è Architettura del front-end
più semplice da comprendere e, di fatto, l’abbiamo Symbol table
Traduzione guidata dalla
sintassi
già vista. Generazione del
three-address code
Infatti, in un parser LR, il parse tree viene costruito
◮
(anche solo implicitamente) dal basso verso l’alto.
Questo implica che, nel momento in cui viene
◮
eseguita una riduzione A → α, è possibile calcolare il
valore degli attributi (sintetizzati) definiti nel nodo
corrispondente ad A.
Tali valori dipendono infatti solo dal valore di attributi
◮
definiti ai nodi corrispondenti ai simboli di α, che
sono stati a loro volta già calcolati.
In altri termini, la situazione che abbiamo visto
◮
nell’esempio è del tutto generale per grammatiche
LR e schemi S-attributed.
51. LFC
Esempio
Il front-end di un
compilatore
Riconsideriamo in dettaglio l’esempio relativo al
◮ Architettura del front-end
Symbol table
parsing della stringa id + id × id, secondo lo schema Traduzione guidata dalla
sintassi
(S-attributed) introdotto. Generazione del
three-address code
Usando il parser SLR(1) che abbiamo discusso a
◮
suo tempo, l’albero di parsing viene costruito dal
basso verso l’alto.
Se le azioni indicate dallo schema vengono eseguite
◮
nel momento in cui si eseguono le corrispondenti
riduzioni, i valori degli attributi sono tutti calcolabili
(cioè le dipendenze sono tutte soddisfatte) come
illustrato nelle due seguenti trasparenze (nelle quali,
per semplicità, non sono state disegnate foglie
isolate).
52. LFC
Esempio (continua)
E E.node
Il front-end di un
T T.node T T.node compilatore
Architettura del front-end
Symbol table
F F.node F F.node F F.node Traduzione guidata dalla
sintassi
Generazione del
id <id,id.lexval> id <id,id.lexval> id <id,id.lexval> three-address code
Dopo la riduzione F−>id Dopo T−>F Dopo E−>T
E E.node
T T.node
F F.node F F.node
id <id,id.lexval> id <id,id.lexval>
Dopo F−>id
E E.node
T T.node T T.node
F F.node F F.node
id <id,id.lexval> id <id,id.lexval>
Dopo T−>F
53. LFC
Esempio (continua)
E E.node
Il front-end di un
compilatore
T T.node T T.node F F.node
Architettura del front-end
Symbol table
F F.node F F.node id <id,id.lexval> Traduzione guidata dalla
sintassi
Generazione del
id <id,id.lexval> id <id,id.lexval> three-address code
Dopo F−>id
E E.node T T.node <,>
T T.node T T.node F F.node
F F.node F F.node id <id,id.lexval>
id <id,id.lexval> id <id,id.lexval>
Dopo T−>T*F
E E.node <,>
E E.node + T T.node <,>
T T.node T T.node F F.node
F F.node F F.node id <id,id.lexval>
id <id,id.lexval> id <id,id.lexval>
Dopo E−>E+T
54. LFC
Aspetti implementativi
Nel caso di parsing LR e schema S-attributed è
◮
Il front-end di un
relativamente semplice mostrare come la compilatore
costruzione esplicita del parse tree non sia richiesta. Architettura del front-end
Symbol table
Se infatti è vero che gli attributi sono logicamente Traduzione guidata dalla
◮ sintassi
Generazione del
definiti ai nodi del parse tree, è però immediato three-address code
convincersi del fatto che, non appena un nodo N è
stato collegato al padre P (a seguito di una
riduzione), gli attributi definiti in N non servono più.
Nella pratica è sufficiente una modifica allo stack
◮
utilizzato dal parser LR, in modo che esso non
memorizzi soltanto stati ma anche gli attributi
associati al simbolo che corrisponde a quello stato.
A quest’ultimo riguardo, possiamo notare come ogni
◮
stato s dell’automa possa essere associato ad un
simbolo della grammatica (si riconsideri, ad esempio,
l’automa LR(0) relativo alla nostra grammatica per le
espressioni, che riportiamo per comodità nella
successiva trasparenza).
55. LFC
Aspetti implementativi (continua)
accept Il front-end di un
compilatore
$ Architettura del front-end
+ Symbol table
T
I6
I1 Traduzione guidata dalla
sintassi
Generazione del
three-address code
F
+
E
I3 I9
id
F
(
)
id
I0 I8
I5 I11
F id
×
( id
T F
I4
T (
(
F I10
I2 I7
×
56. LFC
Schema di traduzione modificato
Il front-end di un
compilatore
Architettura del front-end
Presentiamo di seguito lo schema di traduzione per
◮ Symbol table
Traduzione guidata dalla
la grammatica delle espressioni, modificato in modo sintassi
Generazione del
da utilizzare lo stack per memorizzare gli attributi. three-address code
Più precisamente, lo stack memorizza ora coppie
◮
s, p , dove s è uno stato (un numero compreso fra 0
e 11), mentre p è il valore della proprietà node (in
pratica, un puntatore ad un nodo dell’abstract syntax
tree in costruzione) del simbolo associato ad s.
Si noti che non è più necessario distinguere differenti
◮
occorrenze dello stesso simbolo (in quanto non
vengono più indicati esplicitamente gli attributi nelle
azioni dello schema).
57. LFC
Schema di traduzione modificato
Il front-end di un
compilatore
→ E +T {p2 ← pop().p; pop(); p1 ← pop().p;
E Architettura del front-end
Symbol table
s ← GOTO(top().s, E ); Traduzione guidata dalla
sintassi
Generazione del
push( s, new node(′ +′ , p1 , p2 ) )} three-address code
→T {p ← pop().p; s ← GOTO(top().s, E );
E
push( s, p )}
→ T ×F {p2 ← pop().p; pop(); p1 ← pop().p;
T
s ← GOTO(top().s, T );
push( s, new node(′ ×′ , p1 , p2 ) )}
→F {p ← pop().p; s ← GOTO(top().s, T );
T
push( s, p )}
→ (E ) {pop(); p ← pop().p; pop();
F
s ← GOTO(top().s, F ); push( s, p )}
→ id {p ← pop().p; s ← GOTO(top().s, F );
F
push( s, new leaf(id, id.lexval) }
58. LFC
Parsing LL
Passiamo ora ad unalizzare il secondo caso di Il front-end di un
◮
compilatore
calcolabiltà efficiente degli schemi di traduzione, e Architettura del front-end
Symbol table
precisamente quello relativo a schemi L-attributed in Traduzione guidata dalla
sintassi
unione con grammatiche di tipo LL. Generazione del
three-address code
Anche in questo caso, l’esempio che ci guiderà sarà
◮
quello relativo alla grammatica per le espressioni
modificata con l’eliminazione della ricorsione a
sinistra.
Abbiamo già visto come lo schema sia L-attributed.
◮
Ciò che ci resta da stabilire è dunque come debbano
◮
essere sequenzializzati i frammenti di codice
associati alle varie produzioni in modo da ottenere
un processo di calcolo che produca effettivamente
(ed efficientemente) l’output desiderato (l’abstract
syntax tree).
59. LFC
Un semplice esempio
Per capire il processo generale ci aiutiamo con il
◮
Il front-end di un
semplice esempio di parsing della frase id1 + id2 : compilatore
Architettura del front-end
E Symbol table
Traduzione guidata dalla
sintassi
Generazione del
three-address code
E′
T
T′
F + T E’
T′
ǫ ǫ
id1 F
ǫ
id2
Come già per gli schemi S-attributed, le dipendenze
◮
funzionali fra gli attributi (e dunque il flusso di dati
necessario affinché il calcolo proceda in modo
corretto) possono essere evidenziati utilizzando un
annotated parse tree.
60. LFC
Esempio (continua)
In questo caso, nel disegno dell’annotated parse tree
◮
Il front-end di un
abbiamo utilizzato una notazione semplificativa. compilatore
Architettura del front-end
Symbol table
Ciò è dovuto al fatto che la maggior parte dei nodi ha
◮ Traduzione guidata dalla
sintassi
due attributi i quali, se scritti esplicitamente, Generazione del
three-address code
avrebbero reso la figura difficile da “leggere”.
Abbiamo quindi usato la seguente convenzione:
◮
una freccia indica un arco del parse tree
◮
(queste frecce sono sempre orientate verso il basso);
una freccia (orientata verso l’alto) indica che
◮
l’attributo sintetizzato al nodo di partenza è usato per
calcolare l’attributo sintetizzato al nodo di arrivo;
una freccia (orientata verso il basso) indica
◮
che l’attributo ereditato al nodo di partenza è usato
per calcolare l’attributo ereditato al nodo di arrivo;
infine, una freccia (orientata verso destra)
◮
indica che l’attributo sintetizzato al nodo di partenza
è usato per calcolare l’attributo ereditato al nodo di
arrivo.
61. LFC
Esempio (continua)
L’annotated parse tree: Il front-end di un
◮
compilatore
Architettura del front-end
Symbol table
E Traduzione guidata dalla
sintassi
Generazione del
three-address code
T E’
+
F T’ T E’
ε ε
id 1 F T’
ε
id 2
62. LFC
Organizzazione concreta del calcolo
Nel caso di implementazione del parser LL mediante
◮ Il front-end di un
compilatore
procedure ricorsive, un’implementazione dello Architettura del front-end
schema di traduzione che rispetti le dipendenze Symbol table
Traduzione guidata dalla
sintassi
funzionali è alquanto semplice. Generazione del
three-address code
Consideriamo infatti l’utilizzo di una generica
◮
produzione A → X1 . . . Xi−1 Xi . . . Xk .
Nel momento in cui la procedura per Xi viene
◮
chiamata (dalla procedura per A), le procedure per
X1 , . . . , Xi−1 sono già state chiamate e dunque
eventuali valori da esse produtti possono essere
passati in input a Xi .
Questo consente proprio di implementare il flusso
◮
dati richiesto dalla definizione di attributo ereditato,
nel senso che, qualora Xi abbia attributi ereditati,
questi dipendono da valori associati a (e producibili
dalle procedure per) A e/o X1 , . . . , Xi−1 .
63. LFC
Organizzazione concreta del calcolo
(continua)
Il front-end di un
compilatore
In realtà, è proprio la struttura delle procedure
◮ Architettura del front-end
Symbol table
ricorsive di un parser LL ad aver suggerito la nostra Traduzione guidata dalla
sintassi
definizione di attributo ereditato. Generazione del
three-address code
Tornando alla nostra grammatica per le espressioni,
◮
consideriamo, ad esempio, la procedura per E e le
corrispondenti azioni dello schema:
E ′ .inh ← T .node e E .node ← E ′ .node.
La procedura invocherà quindi dapprima la routine T
◮
(che non necessita di “informazione ereditata”).
Il valore restituito da quest’ultima (T .node) sarà
◮
passato in input a E ′ come valore ereditato.
Il valore restituito dalla chiamata ad E ′ sarà poi
◮
anche il valore restituito da E .
64. LFC
Organizzazione concreta del calcolo
(continua)
Il front-end di un
compilatore
Architettura del front-end
Symbol table
Traduzione guidata dalla
sintassi
Generazione del
three-address code
Si noti quindi che le procedure corrispondenti ai non
◮
terminali sono più utilmente realizzate mediante
funzioni.
La funzione per E può quindi essere sinteticamente
◮
espressa nel modo seguente:
Tnode ← T ()
1:
return E ′ (Tnode )
2:
65. LFC
Organizzazione concreta del calcolo
(continua)
Il front-end di un
compilatore
Consideriamo, come altro esempio, la routine per E ′ .
◮ Architettura del front-end
Symbol table
Abbiamo appena visto che essa riceve in input un
◮ Traduzione guidata dalla
sintassi
argomento (attributo ereditato) che supporremo Generazione del
three-address code
venga assegnato al parametro formale inh.
Il corpo della funzione può essere espresso nel
◮
modo seguente (si ricordi la tabella di parsing per la
produzione E ′ → +TE ′ ):
1: if t ← get_next_token() =′ +′ then
Tnode ← T ()
2:
inh1 ← new node(′ +′ , inh, Tnode )
3:
return E ′ (inh1 )
4:
′
5: else if t =′ ) or t =′ $′ then
return inh
6:
7: else
error()
8:
66. LFC
Organizzazione concreta del calcolo
(continua)
Il front-end di un
Nel caso di implementazione ricorsiva del parser, compilatore
◮
Architettura del front-end
non è dunque necessario che venga Symbol table
Traduzione guidata dalla
preliminarmente costruito il parse tree. sintassi
Generazione del
three-address code
Nel caso il parse tree sia disponibile non è
◮
comunque difficile rendersi conto di come le
dipendenze funzionali presenti negli schemi siano
soddisfattibili mediante un attraversamento in ordine
anticipato (preordine) del parse tree medesimo.
Infatti, l’attaversamento in ordine anticipato consente
◮
(sempre con riferimento alla generica produzione
A → X1 . . . Xi−1 Xi . . . Xk ) di “raccogliere”
l’informazione (attributi) relativa ad A e X1 , . . . , Xi−1
prima di usarla (come informazione ereditata) nel
calcolo degli attributi per Xi
Si veda al riguardo l’annotated parse tree relativo
◮
alla frase id1 + id2 .
67. LFC
Linguaggi formali e compilazione
Il front-end di un
compilatore
Architettura del front-end
Symbol table
Traduzione guidata dalla
sintassi
Generazione del
three-address code
Il front-end di un compilatore
Architettura del front-end
Symbol table
Traduzione guidata dalla sintassi
Generazione del three-address code
68. LFC
Il completamenento del front-end
Obiettivo di quest’ultima parte del corso è la
◮
Il front-end di un
presentazione di alcuni semplici esempi di compilatore
Architettura del front-end
generazione di codice intermedio. Symbol table
Traduzione guidata dalla
Non intendiamo presentare una descrizione sintassi
◮
Generazione del
completa, sia per ragioni di tempo sia perché (a three-address code
nostro avviso) questa attività è molto meno
interessante dal punto di vista delle idee sottostanti.
Gli esempi hanno dunque il solo scopo di illustrare
◮
l’approccio base per la generazione di codice
intermedio, ignorando quegli aspetti che
richiederebbero un ragionamento più approfondito.
Faremo inoltre tre ipotesi molto forti, e precisamente:
◮
(1) di disporre dell’abstract syntax tree delle stringhe
da tradurre; (2) che il risultato debba essere
presentato sotto forma di file alfanumerico, (3) che
esista un unico tipo di dato nei programmi sorgenti
(ad esempio, numeri interi).
69. LFC
Il modello di computer intermedio
Il codice intermedio nel quale dovrà essere tradotto il
◮
Il front-end di un
programma sorgente sarà il cosiddetto three-address compilatore
code. Architettura del front-end
Symbol table
Naturalmente, prima di descrivere gli esempi di Traduzione guidata dalla
◮ sintassi
Generazione del
traduzione, dobbiamo precisare che cosa si intenda three-address code
con codice a tre indirizzi, cioè a quale modello
astratto di calcolatore tale codice faccia riferimento.
Ipotizziamo dunque che la macchina astratta sia in
◮
grado di eseguire semplici istruzioni caratterizzate da
un codice operativo e da al più tre indirizzi per gli
operandi.
Fra le istruzioni disponibili troviamo le operazioni
◮
logico/aritmetiche binarie e unarie, le istruzioni di
salto (condizionato o incondizionato), il semplice
assegnamento, la chiamata di procedura e la
gestione di array lineari.
Ogni istruzione può essere preceduta da una o più
◮
etichette (stringhe letterali)
70. LFC
Istruzioni del three-address code
Il front-end di un
compilatore
Più precisamente, allo scopo di presentare gli Architettura del front-end
◮ Symbol table
esempi, noi considereremo solo le seguenti Traduzione guidata dalla
sintassi
istruzioni, il cui significato dovrebbe risultare chiaro: Generazione del
three-address code
A ← B op C, dove op ∈ {+, −, ×, /} e A, B e C sono
◮
identificatori definiti dall’utente nel programma
sorgente oppure temporanee generate dal parser;
A ← B, dove A e B sono definiti come al punto
◮
precedente;
goto L, dove L è un’etichetta generata dal parser;
◮
if A goto L, dove A è un identificatore definito
◮
dall’utente oppure una temporanea generata dal
parser ed L è un’etichetta generata dal parser;
ifFalse A goto L, dove A ed L sono definiti come
◮
al punto precedente.
71. LFC
Ulteriori assunzioni
Ipotizzeremo che il parser possa invocare una
◮
Il front-end di un
funzione per generare identificatori univoci, come compilatore
pure una funzione per generare etichette univoche. Architettura del front-end
Symbol table
Assumeremo inoltre la disponibilità di una funzione, Traduzione guidata dalla
◮ sintassi
Generazione del
che chiameremo emit(), che stampa la stringa three-address code
passata come parametro (che rappresenta una
porzione del programma in three-address code) su
un opportuno supporto di output.
Assumeremo infine che il generico nodo dell’abstract
◮
syntax tree abbia la seguente struttura:
un campo label che caratterizza il tipo di nodo
◮
(assegnamento, operatore, comando if, ...);
se significativo (ad esempio nel caso di identificatore
◮
o operatore), un puntatore alla symbol table per il
corrispondente valore lessicale, accessibie mediante
la funzione lexval;
uno o più puntatori ai nodi che rappresentano i
◮
componenti del costrutto, accessibili mediante i
campi c1, c2, ...
72. LFC
La struttura generale del traduttore
Con le ipotesi fatte, il traduttore (da syntax tree a
◮ Il front-end di un
compilatore
three-address code) può essere organizzato come Architettura del front-end
procedura ricorsiva il cui corpo è costituito Symbol table
Traduzione guidata dalla
sintassi
essenzialmente da una “grossa” istruzione case (o, Generazione del
three-address code
se si preferisce, switch), che analizza il nodo p
passato come parametro e, a seconda del tipo,
esegue operazioni diverse.
Data la struttura del parse tree, la generazione del
◮
codice per un dato nodo implicherà poi una o più
chiamate ricorsive per la generazione del codice
associato ai nodi figli.
La procedura, che chiameremo gencode, riceve un
◮
ulteriore parametro (RES) che è una stringa
(eventualmente vuota) alla quale (vista come nome
di variabile) deve essere assegnato il risultato
calcolato dal codice generato per il nodo p.
73. LFC
Procedure 1 gencode(string RES, AST ∗ p) Il front-end di un
compilatore
tag ← (p → label) Architettura del front-end
Symbol table
case tag of Traduzione guidata dalla
sintassi
Generazione del
id : . . . three-address code
assignment : . . .
comparison : . . .
binaryop : . . .
seq : . . .
if : . . .
ifElse : . . .
while : . . .
...
default : error ()
end