LFC




                                     Il front-end di un
                                     compilatore
         ...
LFC
Linguaggi formali e compilazione
                                          Il front-end di un
                        ...
LFC
Linguaggi formali e compilazione
                                          Il front-end di un
                        ...
LFC
Componenti del front-end
                                                            Il front-end di un
              ...
LFC
Il modello di front-end
                                                                                              ...
LFC
Analizzatore lessicale
                                                                         Il front-end di un
   ...
LFC
Interazione parser-lexical analyzer
                                                                  Il front-end di ...
LFC
Token
                                                                      Il front-end di un
                       ...
LFC
Token name
                                                                 Il front-end di un
     Il nome del token ...
LFC
Token value
                                                                         Il front-end di un
              ...
LFC
Token (continua)
                                                                Il front-end di un
                  ...
LFC
Pattern, lessemi e terminali
                                                             Il front-end di un
         ...
LFC
Pattern, lessemi e terminali (continua)
                                                                      Il front...
LFC
Linguaggi formali e compilazione
                                          Il front-end di un
                        ...
LFC
Symbol table
      Come evidenziato dalla struttura del front-end,
  ◮
                                               ...
LFC
Esempio
                                                         Il front-end di un
     Nel caso del seguente frammen...
LFC
Symbol table (continua)
                                                                 Il front-end di un
      Come...
LFC
Symbol table (continua)
      Infatti, in molti linguaggio di programmazione
  ◮
                                     ...
LFC
Symbol table (continua)
      La soluzione adottata è quindi di non inserire tali
  ◮
                                ...
LFC
Symbol table (continua)
                                                               Il front-end di un
            ...
LFC
Symbol table (continua)
      Nei moderni linguaggi di programmazione l’ambiente
  ◮                                  ...
LFC
Esempio
                                                        Il front-end di un
                                   ...
LFC
Esempio (continua)
      Chiameremo ambiente la lista di symbol table.
  ◮
                                           ...
LFC
Esempio (continua)
                                                                  Il front-end di un
              ...
LFC
Symbol table (continua)
                                                                     Il front-end di un
      ...
LFC
Linguaggi formali e compilazione
                                          Il front-end di un
                        ...
LFC
Le due parti del compilatore
      Il passaggio dal codice sorgente ad un codice            Il front-end di un
  ◮
   ...
LFC
Rappresentazioni intermedie
                                                                    Il front-end di un
   ...
LFC
Esempio
                                                                 Il front-end di un
     Un abstract syntax tr...
LFC
Rappresentazioni intermedie
      Per alcuni semplici linguaggi interpretati, una           Il front-end di un
  ◮
   ...
LFC
I vari passaggi in dettaglio
      Per chiarezza didattica, possiamo “in prima battuta”     Il front-end di un
  ◮
   ...
LFC
Traduzione guidata dalla sintassi
      La generazione dell’abstract syntax tree, il nostro
  ◮
                      ...
LFC
Syntax-directed translation scheme
      Uno schema di traduzione prevede l’inserimento di
  ◮
                       ...
LFC
Attributi
                                                                  Il front-end di un
      Un qualunque proc...
LFC
Attributi (continua)
                                                                   Il front-end di un
           ...
LFC
Attributi (continua)
                                                                Il front-end di un
              ...
LFC
Esempio
                                                                       Il front-end di un
                    ...
LFC
Calcolabilità degli schemi
                                                               Il front-end di un
      Com...
LFC
Calcolabilità degli schemi (continua)
      Si noti che, in generale, il valore di un attributo        Il front-end di...
LFC
Il ruolo del parse tree
      Prima di procedere, vogliamo però soffemarci sul         Il front-end di un
  ◮
        ...
LFC
Tipi di attributi
                                                                       Il front-end di un
          ...
LFC
Tipi di attributi (continua)
                                                                  Il front-end di un
    ...
LFC
Esempio
     Attraversando in ordine anticipato il parse tree                     Il front-end di un
 ◮
              ...
LFC
Esempio (continua)
                                                                 Il front-end di un
               ...
LFC
Esempio (continua)
                                                                 Il front-end di un
               ...
LFC
Tipi di attributi (riprende)
      Ancora con riferimento alla generica produzione
  ◮
                               ...
LFC
Esempio
     Il seguente schema di traduzione, relativo alla
 ◮                                                       ...
LFC
Calcolabilità degli schemi (riprende)
                                                              Il front-end di un...
LFC
Calcolabilità degli schemi (continua)
                                                                Il front-end di ...
LFC
Parsing LR
      La prima combinazione, di grammatica analizzabile
  ◮                                                ...
LFC
Esempio
                                                                  Il front-end di un
                         ...
LFC
Esempio (continua)
                                                            E E.node
                              ...
LFC
Esempio (continua)
        E E.node
                                                                             Il fr...
LFC
Aspetti implementativi
      Nel caso di parsing LR e schema S-attributed è
  ◮
                                      ...
LFC
Aspetti implementativi (continua)
          accept                                                                    ...
LFC
Schema di traduzione modificato
                                                                Il front-end di un
    ...
LFC
Schema di traduzione modificato
                                                        Il front-end di un
            ...
LFC
Parsing LL
      Passiamo ora ad unalizzare il secondo caso di           Il front-end di un
  ◮
                      ...
LFC
Un semplice esempio
      Per capire il processo generale ci aiutiamo con il
  ◮
                                     ...
LFC
Esempio (continua)
      In questo caso, nel disegno dell’annotated parse tree
  ◮
                                   ...
LFC
Esempio (continua)
      L’annotated parse tree:                                 Il front-end di un
  ◮
              ...
LFC
Organizzazione concreta del calcolo
      Nel caso di implementazione del parser LL mediante
  ◮                      ...
LFC
Organizzazione concreta del calcolo
(continua)
                                                              Il front-...
LFC
Organizzazione concreta del calcolo
(continua)
                                                              Il front-...
LFC
Organizzazione concreta del calcolo
(continua)
                                                               Il front...
LFC
Organizzazione concreta del calcolo
(continua)
                                                                     Il...
LFC
Linguaggi formali e compilazione
                                          Il front-end di un
                        ...
LFC
Il completamenento del front-end
      Obiettivo di quest’ultima parte del corso è la
  ◮
                            ...
LFC
Il modello di computer intermedio
      Il codice intermedio nel quale dovrà essere tradotto il
  ◮
                  ...
LFC
Istruzioni del three-address code
                                                                 Il front-end di un
...
LFC
Ulteriori assunzioni
      Ipotizzeremo che il parser possa invocare una
  ◮
                                         ...
LFC
La struttura generale del traduttore
      Con le ipotesi fatte, il traduttore (da syntax tree a
  ◮                  ...
LFC




Procedure 1 gencode(string RES, AST ∗ p)   Il front-end di un
                                           compilato...
Linguaggi Formali e Compilazione: Frontend
Linguaggi Formali e Compilazione: Frontend
Linguaggi Formali e Compilazione: Frontend
Linguaggi Formali e Compilazione: Frontend
Linguaggi Formali e Compilazione: Frontend
Linguaggi Formali e Compilazione: Frontend
Linguaggi Formali e Compilazione: Frontend
Linguaggi Formali e Compilazione: Frontend
Linguaggi Formali e Compilazione: Frontend
Linguaggi Formali e Compilazione: Frontend
Linguaggi Formali e Compilazione: Frontend
Linguaggi Formali e Compilazione: Frontend
Linguaggi Formali e Compilazione: Frontend
Linguaggi Formali e Compilazione: Frontend
Upcoming SlideShare
Loading in …5
×

Linguaggi Formali e Compilazione: Frontend

1,520 views
1,461 views

Published on

Published in: Technology
0 Comments
0 Likes
Statistics
Notes
  • Be the first to comment

  • Be the first to like this

No Downloads
Views
Total views
1,520
On SlideShare
0
From Embeds
0
Number of Embeds
0
Actions
Shares
0
Downloads
39
Comments
0
Likes
0
Embeds 0
No embeds

No notes for slide

Linguaggi Formali e Compilazione: Frontend

  1. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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

×