Traduzione guidata dalla sintassi Attributi e definizioni guidate dalla sintassi Dipartimento di Matematica e Informatica mariarita.diberardini@unicam.it
Analisi Semantica Analisi sintattica - output: il flusso di token (fase di analisi lessicale) viene raggruppato in frasi grammaticali rappresentate tramite alberi di derivazione di una CFG Questo tipo di rappresentazione intermedia consente di identificare operatori ed operandi delle espressioni e statements Costituisce l input della fase di analisi semantica: verifica dell esistenza di eventuali errori semantici acquisizione di informazioni sui tipi utilizzate nelle fasi successive del processo di compilazione type checking Problema: come possiamo definire la semantica ai costrutti dei linguaggi di programmazione?
Un semplice esempio Consideriamo la grammatica per le espressioni sui naturali: E E + T E T T T T F T /F F F (E) number Definire la semantica di questo linguaggio significa definire il valore di ogni espressione costruita applicando le produzioni della grammatica
Un semplice esempio Consideriamo la grammatica per le espressioni sui naturali: E E + T E T T T T F T /F F F (E) number Definire la semantica di questo linguaggio significa definire il valore di ogni espressione costruita applicando le produzioni della grammatica Associamo ad ogni non terminale della grammatica (e quindi ad ogni nodo del albero di derivazione per una data stringa in input) un attributo val che rappresenta appunto il suo valore: E.val, T.val, F.val A questo punto non ci resta che associare a ciascuna produzione una regola, detta regola semantica, che dice come calcolare il valore di una espressione a partire da quello delle sue sottoespressioni
Un semplice esempio PRODUZIONI E E 1 + T E E 1 T E T T T 1 F T T 1 /F T F F (E) F num REGOLE SEMANTICHE E.val = E 1.val + T.val E.val = E 1.val T.val E.val = T.val T.val = T 1.val F.val T.val = T 1.val/F.val T.val = F.val F.val = E.val F.val = num.lexval
Albero di derivazione della stringa 3 5 + 1 E E + T E * T F T F num F num num
Albero di derivazione (annotato) della stringa 3 5 + 1 E.val =16 E.val =15 + T.val =1 E.val =3 * T.val =5 F.val =1 T.val =3 F.val =3 num F.val =5 num num
Valutazione delle regole Fornire le regole semantiche non è però sufficiente Bisogna anche fornire un ordine di valutazione: dobbiamo specificare quali regole applicare (e, soprattutto, in quale ordine) per calcolare il valore desiderato L ordine di valutazione può essere ricostruito a partire dall albero di derivazione per la stringa input, non è altro che un qualche schema di visita dell albero di derivazione
Obiettivi Vediamo, in breve, una tecnica che permette di effettuare analisi semantica e traduzione usando la struttura sintattica data dalla grammatica di un linguaggio L idea di base è quella di associare ad ogni costrutto del nostro linguaggio delle informazioni utili allo scopo Queste informazioni utili vengono rappresentate mediante degli attributi associati a simboli (terminali e non) della grammatica Il valore di ciascun attributo è calcolato tramite delle regole semantiche associate alle produzioni della grammatica È anche indispensabile determinare il giusto ordine di valutazione (grafo delle dipendenze e ordinamento topologico)
(SDD) Sono generalizzazioni delle grammatiche context-free in cui ad ogni simbolo della grammatica è associato un insieme di attributi Se pensiamo ad ogni nodo del parse tree come ad una struttura (un record, un oggetto), allora gli attributi per un simbolo grammaticale X sono i campi della struttura che rappresenta il nodo X Scriveremo X.b per indicare il fatto che b è un attributo di X Un attributo può rappresentare qualsiasi cosa: stringhe (anche frammenti di codice), numeri, tipi, locazioni di memoria, entry di tabelle, etc. Il valore di ogni attributo ad ogni nodo è calcolato applicando la regola semantica associata alla produzione che si usa nel nodo
Attributi Sintetizzati ed Ereditati Attributi Sintetizzati: il loro valore in un dato nodo N può essere calcolato a partire dai valori degli attributi dei nodi figli di N Se A XYZ è una produzione di una SDD e b è un attributo sintetizzato per il non terminale A, il valore di b può dipendere solo dal valore di attributi dei simboli X, Y e Z Esempio: PRODUZIONI E E 1 + T E E 1 T E T T T 1 F REGOLE SEMANTICHE E.val = E 1.val + T.val E.val = E 1.val T.val E.val = T.val T.val = T 1.val F.val
Attributi Sintetizzati ed Ereditati Attributi Ereditati: il loro valore in un dato nodo N può essere calcolato a partire dai valori degli attributi dei nodi fratelli e del nodo padre di N Se A XYZ è una produzione di una SDD e b è un attributo ereditato del simbolo Y, il valore di b dipende dal valore di attributi dei simboli A (padre), X e Z (fratelli) Un terminale può avere solo attributi ereditati tipicamente restituiti dall analizzatore lessicale (valori lessicali) Esempio: PRODUZIONI F num REGOLE SEMANTICHE F.val = num.lexval
Formalmente... Una definizione guidata dalla sintassi è una grammatica libera da contesto in cui associamo ad ogni produzione A α un insieme di regole semantiche della forma b ::= f (c 1, c 2,..., c k ) dove f è una funzione arbitraria (di solito espressa con delle espressioni) e b, c 1, c 2,..., c k sono degli attributi Se b è un attributo sintetizzato di A allora c 1, c 2,..., c k sono attributi dei simboli in α (figli di A) Se b è un attributo ereditato di un di simbolo di α allora c 1, c 2,..., c k sono attributi di simboli di α oppure di A (fratelli e padre di A) In ogni caso l attributo b dipende dagli attributi c 1, c 2,..., c k
Una SDD per un semplice desk calculator PRODUZIONI L En E E 1 + T E T T T 1 F T F F (E) F digit REGOLE SEMANTICHE print(e.val) E.val = E 1.val T.val E.val = T.val T.val = T 1.val F.val T.val = F.val F.val = E.val F.val = digit.lexval Ha solo attributi sintetizzati, è una definizione S-attributed
Una SDD per un semplice desk calculator La grammatica genera le espressioni aritmetiche tra cifre seguite dal newline (n) Ogni non terminale ha un attributo sintetizzato val Il terminale num ha un attributo sintetizzato lexval il cui valore è fornito dall analizzatore lessicale (è il valore della particolare sequenza di caratteri che corrisponde al token num) La regola associata al simbolo iniziale L è una chiamata di procedura che stampa un valore intero (side-effect) mentre tutte le altre regole servono per il calcolo del valore degli attributi Assunzioni e convenzioni: L uso di non terminali con pedici (es, E 1 e T 1) serve per distinguere due diverse occorrenze dello stesso non terminale in una data produzione
Una SDD per delle semplici dichiarazioni di tipo PRODUZIONI D T L T int T real L L 1, id L id REGOLE SEMANTICHE L.inh := T.type T.type = int T.type = real L 1.inh = L.inh addtype(id.entry, L.inh) addtype(id.entry, L.inh)
Una SDD per delle semplici dichiarazioni di tipo Gli attributi ereditati vengono usati per distribuire informazioni sul tipo degli identificatori in una dichiarazione Una dichiarazione è costituita (in molti linguaggi di programmazione come C, Java,... ) da un identificare di tipo T seguito da una lista L di identificatori type è un attributo sintetizzato di T, mentre inh è un attributo ereditato per L Inizialmente il valore dell attributo type viene passato all attributo inh di L (mediante la regola L.type := T.type) Durante la costruzione della lista, ogni elemento passa al successivo il valore di inh (mediante la regola L 1.inh = L.inh) La procedura addtype, prende in input l entrata nella tabella dei simboli per un qualche identificare (id.entry) ed un tipo (L.inh) ed aggiunge al record dell identificare informazioni riguardanti il tipo
Le regole semantiche inducono delle dipendenze tra il valore degli attributi rappresentate mediante i cosidetti grafi delle dipendenze La valutazione, nel giusto ordine, delle regole semantiche determina il valore di tutti gli attributi dei nodi del parse tree di una stringa data La valutazione può avere anche side-effects (effetti collaterali) come la stampa di valori o l aggiornamento di una variabile globale Un parse tree che mostri i valori degli attributi ad ogni nodo è detto parse tree annotato Il processo di calcolo di questi valori prende il nome di annotazione o decorazione del parse tree
Grafo delle dipendenze Input: parse tree per una data stringa Ha un nodo per ogni attributo di ogni nodo n del parse tree Gli archi tengono conto delle dipendenze tra gli attributi indotte dalle regole semantiche Es: Sia A X Y una produzione con regola semantica associata A.a := f (X.x, Y.y) A a X x y Y
Grafo delle dipendenze Input: parse tree per una data stringa Ha un nodo per ogni attributo di ogni nodo n del parse tree Gli archi tengono conto delle dipendenze tra gli attributi indotte dalle regole semantiche Es: Sia A X Y una produzione con regola semantica associata X.x := g(a.a, Y.y) A a X x y Y
Grafo delle dipendenze: un esempio D T inh type L 1 inh L int 2 entry id 1 inh L 3 id entry 2 id 3 entry
Un algoritmo per la costruzione del grafo delle dipendenze for each nodo n nel parse tree do for each attributo a del nodo n do costruisci un nodo per a nel grafo; for each nodo n nel parse tree do for each regola semantica b := f (c 1, c 2,..., c k ) associata con una produzione usata in n do for i := 1 to k do aggiungi un arco dal nodo per c i al nodo per b Attenzione: per le chiamate di procedure (Es: addtype(id.entry, L.type)) aggiungiamo un nodo (e, quindi, un attributo) fittizio
D T 4 inh type 5 L 1 int inh 7 inh 6 L 3 2 id entry 1 L 2 3 id 8 entry 2 9 10 id 3 1 entry
Un ordinamento topologico di un grafo diretto aciclico è un qualsiasi ordinamento dei nodi n 1 n 2... n k tale che se esiste un arco nel grafo dal nodo n i al nodo n j (se la coppia (n i, n j ) A) allora n i precedete n j nell ordinamento (n i n j ) ogni coppia i, j Un qualsiasi ordinamento topologico del grafo delle dipendenze dà un ordine valido in cui le regole semantiche possono essere valutate Nell ordinamento topologico i valori degli attributi c 1, c 2,..., c k di una regola b := f (c 1, c 2,..., c k ) sono disponibili sempre prima che f sia valutata
La traduzione specificata da una qualsiasi definizione guidata dalla sintassi può essere sempre e comunque implementata nel seguente modo: 1 si costruisce il parse tree 2 si costruisce il grafo delle dipendenze 3 si trova un ordinamento topologico del grafo 4 si valutano le regole semantiche dei nodi secondo l ordinamento topologico
Consideriamo, di nuovo, la valutazione di int d 1, d 2, d 3. Abbiamo: (1) costruito il parse tree dalla stringa, (2) costruito il grafo delle dipendenze e (3) identificato un ordinamento topologico del grafo. Ora valutiamo le regole in base all ordinamento, ottenendo il seguente frammento di codice: T.type := int; L 1.inh := T.type; L 2.inh := L 1.inh; L 3.inh := L 2.inh; addtype(id 3.entry, L 3.inh); addtype(id 2.entry, L 2.inh); addtype(id 1.entry, L 1.inh);
Ordinamenti topologici e cicli La presenza di un ciclo in in grafo delle dipendenze rende impossibile definire un ordinamento topologico e, di conseguenza, uno schema di valutazione per le regole semantiche. Ad esempio, il seguente ciclo sta ad indicare che per calcolare il valore dell attributo a abbiamo bisogno del valore dell attributo b, che a sua volta dipende dal valore di a... a b
Una SDD che usa solo attributi sintetizzati è detta S-attributed Un parse tree di una defizione S-attributed può sempre essere annotato valutando le regole semantiche per gli attributi in maniera bottom-up (dalle foglie alla radice) In genere, basta una semplice visita in postordine del parse tree secondo il seguente schema (dove N è la radice del parse tree): postorder (N) for each (figlio C di N, da sinistra a destra) postorder(c) valuta gli attributi di N Possono quindi essere implementate facilmente durante il parsing LR Generatori automatici di LR-parser possono essere modificati per implementare una definizione S-attributed basata su una grammatica LR
Valutazione degli attributi in una definizione S-attributed Grafo delle dipendenze per 3 4n L 8 E 7 val n T 6 val T 3 val * F 5 val 2 4 F lexval digit lexval digit 1 lexval
Valutazione degli attributi in una definizione S-attributed Albero annotato per 3 4n L E.val=12 n T.val=12 T.val=3 * F.val=4 F.val=3 digit.lexval=4 digit.lexval=3
Definizioni L-Attributed È una sottoclasse di SDD per la quale riusciamo ad identificare un ordine di valutazione; impone dei vincoli sugli attributi In particolare ogni attributo deve essere: 1 sintettizzato oppure 2 ereditato, ma... se X i.a è un attributo ereditato il cui valore è calcolato tramite una regola associata alla produzione A X 1 X 2... X n, tale regola può usare (a) attributi ereditati di A (b) attributi (ereditati e sintetizzati) dei simboli X 1, X 2,..., X i 1 (c) attributi (ereditati e sintetizzati) di X i solo se in questo modo non si formano cicli nel grafo delle dipendenze (d) se X i è un terminale, la regola può anche dipendere da valori lessicali per X i
Una Definizione L-Attributed PRODUZIONI D T L T int T real L L 1, id L id REGOLE SEMANTICHE L.inh := T.type T.type = int T.type = real L 1.inh = L.inh addtype(id.entry, L.inh) addtype(id.entry, L.inh)
Una definizione non L-attributed e non S-attributed PRODUZIONI A B C REGOLE SEMANTICHE A.s = B.a B.i = f (C.b, A.s) s è un attributo sintetizzato di A (dipende da un attributo a di un nodi figlio) i è un attributo ereditato di B, ma tale attributo dipende da : un attributo sintetizzato (e non ereditato) di A un attributo di C, fratello destro (e non sinistro) di B
Syntax Tree Vediamo ora come sia possibile usare le definizioni guidate dalla sintassi per specificare (implementare) la costruzione di syntax tree per espressioni Abbiamo parlato di syntax tree all inizio del corso: un albero sintattico (astratto) è una forma condensata di un parse tree che è utile per rappresentare i costrutti dei linguaggi Operatori e le parole chiave non appaiono come foglie, ma sono associati a nodi interni; sulle foglie troviamo invece gli operandi Un altra semplificazione è che le catene di applicazione di una singola produzione vengono collassate
Costruzione di un Syntax Tree Problema: definire delle regole semantiche per le produzioni della seguente grammatica che producano in output il syntax-tree per la stringa in input E E 1 + T E E 1 T E T T (E) T num T id
Vediamo ora in dettaglio come costruire gli alberi sintattici per le espressioni aritmetiche; Costruiamo, inanzitutto, i sottoalberi per le sottoespressioni creando un nodo per ogni operatore ed operando I figli di un nodo operatore altro non sono che le radici dei sottoalberi che rappresentano le sottoespressioni che definiscono lespressione principale Ogni nodo di un syntree può essere implementato mediante un record con diversi campi
In un nodo operatore un campo identifica l operatore stesso e i campi rimanenti son i puntatori ai nodi operandi; l operatore è spesso detto etichetta del nodo I nodi in un syntree possono avere campi addizionali per gli attributi che sono stati definiti In un nodo operando un campo identifica l operando che può essere un identificatore o un numero I campi rimanenti possono rappresentare un entrata alla symbol table (nel caso in cui l operando sia un identificatore) o un valore (nel caso in cui l operando sia un numero)
Usiamo le seguenti funzioni per costruire i nodi dei syntree per espressioni con operatori binari: 1 mknode(op, left, right) crea un nodo operatore con etichetta op e due campi puntatore all operando destro e sinistro 2 mkleaf (id, entry) crea un nodo identificatore con etichetta id ed un puntatore entry alla tabella dei simboli 3 mkleaf (num, val) crea un nodo numero con etichetta num e un campo val contentente il valore
Il seguente frammento di programma crea (in maniera bottom-up) un syntax tree per lespressione a 4 + c 1. p 1 = mkleaf (id, entry a ); 2. p 2 = mkleaf (num, 4); 3. p 3 = mknode(, p 1, p 2 ); 4. p 4 = mkleaf (id, entry c ); 5. p 3 = mknode( +, p 3, p 5 );
SDD per i Syntax Trees Diamo una definizione guidata dalla sintassi S-attributed per la costruzione dell albero sintattico di una espressione contenente gli operatori + e - Introduciamo un attributo nptr per ogni simbolo non terminale per tener traccia dei puntatori ritornati dalle funzioni di creazione dei nodi Produzioni E E 1 + T E E 1 T E T T (E) T id T num Regole Semantiche E.nptr := mknode( +, E 1.nptr, T.nptr) E.nptr := mknode(, E 1.nptr, T.nptr) E.nptr := T.nptr T.nptr := E.nptr E.nptr := mkleaf(id, id.entry) E.nptr := mkleaf(num, num.val)
Albero annotato