Analisi semantica e traduzione - Dipartimento di Ingegneria
Transcript
Analisi semantica e traduzione - Dipartimento di Ingegneria
Analisi semantica e traduzione Grammatiche ad attributi Semantica La semantica, come la sintassi, è costituita da un insieme di regole che associano un significato ai costituenti lessicali e sintattici del linguaggio. Il significato dell’intera frase viene quindi ottenuto con un processo compositivo, unendo il significato dei suoi costituenti. Le regole semantiche possono essere fornite seguendo diversi modelli: Semantica operazionale Semantica denotazionale Il particolare tipo di regole semantiche che costituiscono l’argomento del corso sono note sotto il nome di grammatica ad attributi, ed è senza dubbio il più diffuso nel campo dei linguaggi di programmazione. 1 Un metodo di definizione del linguaggio si dice semantico se soddisfa uno o più dei seguenti criteri: Il metodo è diverso da quello delle grammatiche formali di Chomsky, ed in particolare dalle sintassi libere. Il metodo è basato sulla composizione di funzioni aventi domini e codomini arbitrari, in contrasto con i metodi sintattici che trattano unicamente le stringhe per mezzo delle operazioni della teoria dei linguaggi formali (concatenamento, ripetizione, sostituzione di stringhe). Primo approccio Sono classificati semantici i fondati su regole di riscrittura più potenti di quelle libere. Nella classificazione di Chomsky delle sintassi, le regole del tipo 0 permettono di esprimere ogni linguaggio computabile, in quanto è noto che tali sintassi definiscono la classe degli insiemi ricorsivamente numerabili. È concettualmente possibile descrivere il significato di qualsiasi linguaggio artificiale mediante una sintassi le cui regole hanno una forma più generale di quelle libere. In pratica tale sistema sarebbe difficilissimo da progettare, illeggibile e praticamente inutilizzabile. Allo stato attuale delle conoscenze si può dire che il rapporto costo-benefici del formalismo sintattico raggiunge l’ottimo con le sintassi libere, ma peggiora rapidamente quando si tenta di estenderlo verso modelli più complessi. 2 Secondo approccio Nella semantica si sfruttano meccanismi definitori più generali di quelli basati sulla manipolazione delle stringhe La semantica raggiunge il suo potere espressivo grazie all’impiego dell’aritmetica e al ricorso a strutture-dati più ricche delle stringhe: le tabelle,liste, insiemi, relazioni, ecc. comunemente impiegati dall’ingegneria del software. Esempio: la tabella dei nomi delle variabili usate in un programma con le loro proprietà. Per costruire e consultare tali strutture, che costituiscono il dominio semantico del linguaggio, si usano delle funzioni (dette semantiche), scritte in un opportuno linguaggio algoritmico, che spesso viene scelto tra i linguaggi programmativi più diffusi. Le funzioni semantiche sono raggruppate in gruppi di regole che riflettono strettamente la struttura della sintassi Il potere espressivo ottenuto grazie al libero uso delle strutture-dati e delle funzioni semantiche offusca il carattere rigorosamente formale del modello semantico, che va considerato un buon compromesso tra il rigore formale della sintassi e la tecnica della programmazione. Traduzione Sarebbe difficile progettare in modo globale un algoritmo di traduzione di un linguaggio complesso senza prima analizzare la frase sorgente nei suoi costituenti definiti dalla sintassi. L’analisi sintattica riduce la complessità del problema del calcolo della traduzione, decomponendolo in sottoproblemi più semplici. Per ogni costituente della frase sorgente il traduttore esegue le azioni appropriate per preparare la traduzione, azioni che consistono nel raccogliere certe informazioni (ad es. le tabelle dei simboli), nel fare dei controlli semantici, o nell’emettere certe parti della frase pozzo. In altre parole il lavoro del traduttore è modularizzato secondo la struttura descritta dall’albero sintattico della frase. Perciò questo tipo di traduttori è detto guidato dalla sintassi. 3 Organizzazione del traduttore Per il traduttore sia efficace, è necessario che la sintassi sia strutturalmente adeguata, nel senso che ogni sottoalbero deve permettere il calcolo di una parte significativa della traduzione. Le grammatiche ad attributi sono la tecnica per eccellenza per sfruttare la struttura sintattica nel calcolare la traduzione o il significato di una frase. Le regole semantiche della grammatica sono definite in corrispondenza delle produzioni sintattiche e gli attributi semantici sono associati ai simboli nonterminali e terminali della sintassi. Sintassi e semantica hanno allora esattamente la stessa modularità. Vi sono altri approcci per formalizzare la semantica che pure sfruttano la struttura sintattica. metodo denotazionale: esprime il significato della frase come composizione di funzioni matematiche. Traduzioni puramente sintattiche Un caso molto particolare di traduzione compositiva è quello dei cosiddetti schemi sintattici di traduzione, un formalismo puramente sintattico per definire una funzione dal linguaggio sorgente al pozzo. Anche se gli schemi sintattici non dispongono della capacità algoritmica necessaria per i casi reali, essi sono interessanti come modello semplice ed elegante della traduzione. Uno schema di traduzione è un sistema formale che accoppia due sintassi libere, la sintassi-sorgente e la sintassi-pozzo, ottenendo così una corrispondenza i sottoalberi da esse generati, e quindi anche tra le frasi del linguaggio-sorgente L e quelle del linguaggio pozzo L'. 4 Traduzione puramente sintattica Siano Σ e Δ gli gli alfabeti sorgente e pozzo, e sia x ∈ L ⊆ Σ* una una frase sorgente. Allora la traduzione di x è una stringa y che viene detta immagine nella traduzione. L'insieme di tali coppie (x, y) costituisce una relazione τ ⊆ (Σ* x Δ*) Detta relazione di traduzione. Il linguaggio pozzo L' è l'insieme delle stringhe y ⊆ Δ*) tali che per qualche stringa x ∈ L sia (x, y) ∈ τ Se poi ogni frase sorgente ammette una sola immagine, la traduzione diventa una funzione (ad un solo valore) τ : L → L’ Si può scrivere con notazione funzionale y= τ(x). Definizione Uno schema di traduzione puramente sintattico è costituito dalla sintassi-sorgente G= (V,Σ, P, S) e dalla sintassi pozzo G'= (V,Δ, P', S), aventi lo stesso alfabeto nonterminale V, e da una corrispondenza biunivoca tra le produzioni P e P', con le seguenti proprietà: Siano (A→α) ∈P e (A→β) ∈ P’ le produzioni corrispondenti. Allora I nonterminali presenti in α e β devono essere gli stessi comparire lo stesso numero di volte comparire nello stesso ordine. 5 Esempio Sorgente A → aCD C→D C→b D → DaCD D→c Pozzo A → bCD C → bDe C → ca D →DCD D→f A → aCD → aDD → aDaCDD → acaCDD → acabDD → acabcD → acabcc A → bCD → bbDeD →bbCDDeD → bbCfDeD →bbcafDeD → bbcaffeD → bbcaffef Esempio La sintassi-sorgente espressioni aritmetiche, linguaggio pozzo espressioni nella forma polacca postfissa, nella quale è noto che gli operatori seguono i loro argomenti. Un esempio di traduzione è: v * v *(v+v) ⇔ v v mult v v add mult dove gli alfabeti sorgente e pozzo sono Σ = { v, ' (' , ' )' , +,*} e Δ = {v, add, mult} Lo schema di traduzione è definito dalle seguenti coppie di produzioni: 1. E → E + T E → E T add 2. E → T E→T 3. T → T * F T → T F mult 4. T → F T→F 5. F →( E ) F →E 6. F → v F→v 6 Limiti sono insiti nel fatto che, per costruire la stringa pozzo, si possono soltanto applicare delle semplici operazioni di spostamento dei terminali-sorgente e di cancellazione o sostituzione di essi con i terminali dell'alfabeto pozzo. In tale modo ad esempio sarebbe impossibile definire la traduzione che converte un numero dalla rappresentazione in base due a quella in base dieci. Per realizzare si possono utilizzare le grammatiche ad attributi Traduzione syntax direct Ambito: traduzione di linguaggi guidata dalle grammatiche context- free Traduzione: è una mappatura (mapping ) dell'input nell'output Definizione syntax-directed: è un formalismo per specificare come si traduce un costrutto di un ling. di progr. in termini di attributi associati ai suoi componenti sintattici Schema di traduzione è una notazione più procedurale (implementativa) per specificare come si traduce un costrutto di un ling. di progr. 7 Traduzione syntax direct approccio classico (due passate): 1. si sintetizza l'albero sintattico (struttura dati) nella fase di analisi sintattica 2. si utilizza l'albero sintattico per la sintesi dell'output approccio semplificato (una passata): 1. si sintetizza l'output direttamente durante l'analisi sintattica, senza costruire l'albero sintattico `l'approccio semplificato ad una passata funziona solo nei casi “semplici" di traduzione e, più in generale, di elaborazione dell'input invece, costruendo l'albero sintattico e visitandolo successivamente si può implementare un insieme molto più grande di funzioni di traduzione e di elaborazione Come opera una definizione syntaxdirected: traduzione senza albero sintattico = analisi sintattica dell'input e sintesi dell'output in una sola passata pertanto, la traduzione deve avvenire durante l'analisi sintattica occorre modificare l'analizzatore sintattico, aggiungendo istruzioni che permettano la sintesi dell'output in parallelo all'analisi dell'input NOTA BENE: gli analizzatori sintattici LL e LR visti in precedenza NON generano l'albero sintattico 8 si parla di traduzione guidata dalla sintassi nei casi in cui è possibile definire il processo di sintesi dell'output (traduzione) sulla base della struttura sintattica dell'input in altre parole, in questi casi l'operazione di traduzione è fortemente legata alla sintassi dell'input, pertanto la traduzione puµo essere definita in modo ”parallelo" alla definizione della sintassi dell'input in moltissime applicazioni informatiche la funzione di traduzione (o di elaborazione) è guidata dalla sintassi nel seguito trattiamo solo funzioni di traduzione guidate dalla sintassi anche per traduzioni guidate dalla sintassi, si può avere la necessità di costruire l'albero sintattico (due passate distinte per analisi sintattica e sintesi dell'output) oppure no (una sola passata per analisi sintattica e sintesi dell'output) per la traduzione guidata dalla sintassi viene definita una estensione del formalismo delle grammatiche non contestuali: le grammatiche ad attributi le grammatiche ad attributi estendono le grammatiche non contestuali introducendo le nozioni di: attributi associati ai simboli (terminali e non terminali) della grammatica azioni semantiche e regole semantiche, che affiancano le regole sintattiche della grammatica 9 nelle grammatiche ad attributi: ogni simbolo terminale o non terminale può avere uno o più attributi attributo = proprietà associata al simbolo, che può essere letta o assegnata dalle azioni semantiche ( = variabile di un linguaggio di programmazione) ad ogni regola di produzione (regola sintattica) può essere associata una regola semantica regola semantica = sequenza di azioni semantiche azione semantica = azione in grado di assegnare valori agli attributi e di avere altri effetti ( = istruzione di un linguaggio di programmazione) gli attributi sono usati per rappresentare un “significato" che viene associato ai simboli sintattici signicato = valore (numero, stringa, struttura dati, ecc.) che assume l'attributo le azioni semantiche sono il meccanismo che effettua il calcolo di tali valori, e che quindi calcola tale “significato" possiamo effettuare la traduzione utilizzando attributi e azioni semantiche, se assegnamo come “significato" di un simbolo la traduzione della parte di stringa di input che tale simbolo rappresenta nell'albero sintattico 10 esempio: in una grammatica per espressioni aritmetiche sui numeri interi, possiamo associare un attributo di tipo intero ad ogni simbolo non terminale della grammatica possiamo poi definire delle azioni semantiche in modo tale che il valoredi ogni attributo corrisponda al valore della sottoespressione associata al corrispondente simbolo non terminale pertanto: valore dell'attributo associato alla radice dell'albero sintattico = valore dell'intera espressione aritmetica Grammatiche ad attributi Una grammatica ad attributi H è costituita dalle seguenti entità. 1. Una sintassi libera G = (V,Σ, P, S), dove V è il vocabolario nonterminale, Σ, quello terminale, P è l’insieme delle produzioni e S è l’assioma. Spesso conviene imporre che l’assioma non figuri in alcuna parte destra di produzione. 2. Un insieme di simboli, gli attributi (semantici), associati ai simboli nonterminali ed eventualmente ai terminali. – L’attributo α associato al (non)terminale D si indica con α of D. L’insieme degli attributi dei nonterminali è spartito in due insiemi tra loro disgiunti detti attributi sintetizzati e attributi ereditati. – Gli attributi dei terminali sono detti lessicali. 3. Per ogni attributo α è specificato un dominio, l’insieme dei valori che esso può assumere. 4. Un insieme reg(H) di regole (semantiche). Ogni regola è associata ad una produzione, ed in generale una produzione p ha più regole indicate come reg(p). 11 Cosa sono gli attributi Ogni attributo può rappresentare qualunque cosa vogliamo: stringhe, numeri, tipi, locazioni di memoria, etc. Il valore di ogni attributo ad ogni nodo è determinato da una regola semantica associata alla produzione che si usa nel nodo annotated (decorated) parse tree: è un parse tree che mostra i valori degli attributi (dei simboli) per ciascun nodo Definizioni guidate dalla sintassi gli attributi si distinguono in: attributi sintetizzati: sono gli attributi il cui valore dipende solo dai valori degli attributi presenti nel sottoalbero del nodo dell'albero sintattico a cui sono associati attributi ereditati: sono gli attributi il cui valore dipende solo dai valori degli attributi presenti nei nodi predecessori e nei nodi fratelli del nodo dell'albero sintattico a cui sono associati gli attributi sintetizzati realizzano un flusso informativo ascendente nell'albero sintattico (dalle foglie verso la radice) gli attributi ereditati realizzano un flusso informativo discendente (dalla radice verso le foglie) e laterale (da sinistra verso destra e viceversa) nell'albero sintattico 12 Forma di una definizione In un syntax-directed definition, ogni produzione A→α è associata con un insieme di regole semantiche di forma b=f(c1,c2,…,cn) dove f è una funzione, e b è b è un attributo sintetizzato di A e c1,c2,…,cn sono attributi dei simboli della grammatiche presenti nella produzione A→α. oppure b è un attributo ereditato del simbolo sella grammatica in α (nella parte destra della produzione), e c1,c2,…,cn sono attributi dei simboli della grammatiche nella produzione A→α. Regole semantiche l'esecuzione di una regola semantica avviene in parallelo all'applicazione della corrispondente regola sintattica nell'analisi sintattica pertanto, l'ordine di applicazione delle regole semantiche è conseguenza dell'ordine di applicazione delle regole di produzione nell'analisi sintattica in particolare, dato un certo ordine di applicazione, le istruzioni che nelle regole semantiche effettuano la valutazione degli attributi potrebbero non essere eseguibili in modo corretto ad esempio, può accadere che in una azione semantica si tenti di leggere il valore di un attributo non ancora assegnato questo problema, noto come problema della valutazione degli attributi, µe un problema fondamentale per il trattamento automatico delle grammatiche ad attributi 13 Regole semantiche La valutazione può avere anche side- effects (effetti collaterali) come la stampa di valori o l’aggiornamento di una veriabile globale In una definizione guidata dalla sintassi si assume che i simboli terminali abbiano solo attributi sintetizzati I valori per questi attributi sono in genere forniti dall’analizzatore lessicale Il simbolo iniziale, se non diversamente specificato, non ha attributi ereditati Esempio ll calcolo del valore decimale di un numero frazionario in base due (D. Knuth 1968) L’esempio mostra il ruolo degli attributi ereditati, e la loro sostituibilità con gli attributi sintetizzati. Il linguaggio sintatticamente è definito dall’espressione regolare L= { 0,1}*. {0, 1}* da interpretare come un numero binario con il punto che separa la parte intera da quella frazionaria. Ad es. il significato di 1101.01 è il numero 13,25 Sintassi 1. num → str . str 2. str → str bit 3. str → bit 4. bit → 0 5. bit → 1 num è l’assioma, str definisce la stringa binaria della parte intera e frazionaria, che è composta di bit. 14 Grammatica ad attributi (1) Grammatica ad attributi (1) 1. num → str1 . str2 2. str0 → str1 bit 3. str → bit 4. 5. bit → 0 bit → 1 f of str1 := 0 f of str2 := - l of str2 v of num= v of str1 +v of str2 f of str1 := f of str0 +1 f of bit := f of str l of str0 := l of str1 + 1 v of str0 := v of str1 + v of bit f of bit:= f of str l of str := 1 v of str := v of bit v of bit := 0 v of bit := 2 f of bit 15 Grammatica ad attributi (2) num → str1 . str2 v of num= v of str1 +v of str2 * 2 -l of str2 2. str0 → str1 bit 1. v of str0 := 2* v of str1 +v of bit l of str0 := l of str1 + 1 3. str → bit v of str := v of bit l of str := 1 4. 5. bit → 0 bit → 1 v of bit := 0 v of bit := 1 Annotated Parse Tree Albero sintattico annotato o Albero sintattico decorato: albero sintattico contenente I valori degli attributi. Annotazione o decorazione dell’albero sintattico: processo di calcolo degli attributi 16 Esempio PRODUZIONE REGOLE SEMANTICHE L→En print(E.val) E → E1 + T E.val := E1.val ⊕ T.val E→T E.val := T.val T → T1 * F T.val := T1.val ⊗ F.val T→F T.val := F.val F→(E) F.val := E.val F → digit F.val := digit.lexval Esempio: spiegazioni La grammatica genera le espressioni aritmetiche tra cifre seguite dal carattere n di newline Ogni simbolo non terminale ha un attributo sintetizzato val Il simbolo terminale digit ha un attributo sintetizzato il cui valore si assume essere fornito dall’analizzatore lessicale La regola associata al simbolo iniziale L è una procedura che stampa un valore intero (side-effect) mentre tutte le altre regole servono per il calcolo del valore degli attributi 17 Annotated Parse Tree -- Example L Input: 5+3*4 E.val=17 E.val=5 + return T.val=12 T.val=5 T.val=3 * F.val=5 F.val=3 digit.lexval=5 digit.lexval=3 F.val=4 digit.lexval=4 I simboli E, T, e F sono associati con gli attibuti sintetizzati loc e code. Il token id ha un attributo sintetizzato name (si assume che l’analizzatore lessicale si occupi di tale valore) Si assume che || e l’operatore di concatenazione di stringhe. Syntax-Directed Definition – Esempio2 Production Semantic Rules E → E1 + T E→T T → T1 * F T→F F→(E) F → id E.loc=newtemp(), E.code = E1.code || T.code || add E1.loc,T.loc,E.loc E.loc = T.loc, E.code=T.code T.loc=newtemp(), T.code = T1.code || F.code || mult T1.loc,F.loc,T.loc T.loc = F.loc, T.code=F.code F.loc = E.loc, F.code=E.code F.loc = id.name, F.code=“” 18 Syntax-Directed Definition – Inherited Attributes Production Semantic Rules D→TL T → int T → real L → L1 id L → id L.in = T.type T.type = integer T.type = real L1.in = L.in, addtype(id.entry,L.in) addtype(id.entry,L.in) Il simbolo T è associato con un attributo sintetizzato type. IL simbolo L è associato con un attributo ereditato in. Calcolo degli attributi L’ordine di calcolo degli attributi dipende dal grafo delle dipendenze creato sulla base delle regole semantiche 19 Dipendenze funzionali e circolarità L’attributo definito da una regola dipende dagli argomenti della funzione semantica, dando luogo ad una relazione di dipendenza funzionale tra l’attributo definito ed ognuno dei suoi argomenti. Data la produzione E la regola Si definisce dipendenza funzionale Grafo delle dipendenze (Dependency Graph) Si definisce relazione di dipendenza funzionale della produzione p l’unione relazioni dipp,α,k per tutte le regole reg(p) dipp,= ∪∀reg(p) dipp,α,k una relazione binaria definita su un insieme può essere visualizzata come un grafo i nodi sono gli elementi dell’insieme un arco n1 →n2 indica la relazione tra n1 ed n2. Grafo dipp delle dipendenze funzionali della produzione p e il grafo che ha come nodi gli attributi attr(p) e presenta l’arco β of Dj → α of Dk se α of Dj è argomento di una funzione semantica associata a p che calcola β of Dk . 20 Grafo delle dipendenze (Dependency Graph) E' un grafo orientato aciclico che illustra le interdipendenze tra gli attributi dei nodi del parse-tree Per costruire il grafo delle dipendenze per un parse- tree occorre prima mettere tutte le regole semantiche nella forma b := f( c1 ,c2 ,... ck ) anche quelle che consistono di una chiamata di procedura (introducendo un attributo fittizio) Il grafo ha un nodo per ogni attributo. Se l'attributo b dipende dall'attributo c b = f ( c .........) ci sarà un arco da c a b b ← c Se un attributo b in un nodo dipende da un attributo c allora la regola semantica per b deve essere valutata dopo la regola semantica che definisce c Esempio Grammatica 1 21 Circolarità delle definizioni semantiche Una frase di un linguaggio deve avere un ben preciso significato, quindi è essenziale che la grammatica determini, per ogni albero, una e una sola soluzione per gli attributi, una volta fissati i valori iniziali degli attributi lessicali. Il grafo delle dipendenze funzionali dipp di una produzione è aciclico se non contiene alcun circuito (orientato). l’albero semantico t è aciclico se il grafo delle dipendenze funzionali dip(t) è privo di circuiti. Una grammatica ad attributi è aciclica se per ogni albero sintattico il corrispondente albero semantico è aciclico. Verifica di aciclicita Il grafo di una produzione contiene un numero ridotto di nodi e di archi, è semplice controllare ad occhio che sia aciclico. Se in qualcuno dei grafi dipp delle produzioni vi fosse una circolarità certamente esisterebbe in T(G) un albero t il cui grafo dip(t) presenterebbe la stessa circolarità. Non è vero il viceversa 22 Proprietà 1 Se per un albero t il grafo delle dipendenze funzionali dip(t) è aciclico, il sistema delle equazioni semantiche dell’albero ammette una e una sola soluzione. Proprietà 1: Dimostrazione. Le incognite sono gli attributi dei nodi di t. Per ogni incognita esiste una e una sola equazione che le assegna un valore. Se dip(t) non ha cicli i nodi del grafo, ossia le incognite, possono essere ordinati topologicamente ottenendo un ordinamento totale: a1 := f1 a2 := f2 ....... am := fm dove fj è la regola che assegna il valore all’attributo aj . La sequenza di istruzioni è tale che tutti gli argomenti di una regola sono definiti da regole precedenti, quindi sono di valore noto. Poiché il programma non contiene istruzioni di salto, ogni riga viene eseguita una sola volta, quindi ogni incognita riceve un solo valore. 23 Algoritmo 1.Ordina topologicamente gli attributi di t rispetto al grafo dip(t) e scrivi nell’ordine le regole che li calcolano. 2.Esegui nell’ordine le regole semantiche. Algoritmo Inconventi dell’algoritmo sono la necessità di calcolare l’ordinamento topologico di t, operazione che richiede una complessità di calcolo O(m) dove m è il numero dei nodi di dip(t), Ogni nodo sintattico dell’albero viene attraversato tante volte quanti sono i suoi attributi, e in generale tali attraversamenti non sono contigui nell’ordinamento topologico, con un conseguente lavoro di spostamento sull’albero. Come miglioramento si introdurranno più avanti certe sottoclassi delle grammatiche in cui le dipendenze funzionali consentono la valutazione degli attributi con un numero più ridotto di attraversamenti dei nodi sintattici. 24 Equivalenza forte e debole Due grammatiche ad attributi H e H’ aventi la stessa sintassi G sono equivalenti in senso forte se esse definiscono la stessa traduzione, ossia Tf (H) = Tf(H') Due grammatiche ad attributi H e H' aventi sintassi G e G' sono equivalenti in senso debole seesse definiscono la stessa traduzione, ossia Td(H) = Td(H') Due classi di grammatiche hanno lo stesso potere espressivo (in senso forte o debole) se per ogni grammatica di una classe esiste nell’altra classe una grammatica equivalente (in senso forte o debole). Proprietà Proprietà 2 La classe delle grammatiche elementari ha lo stesso potere espressivo della classe delle grammatiche con attributi ereditati e sintetizzati. 25 Metodi di valutazione degli attributi semantici Il problema della valutazione semantica consiste nel calcolare i valori degli attributi in tutti i nodi di un albero sintattico dato. Tali valori sono la soluzione del sistema delle equazioni determinate dalle regole semantiche, riportate su ogni nodo dell’albero, soluzione che esiste ed è unica se la grammatica è aciclica. Caratteristiche del metodo di calcolo: generalità del metodo: è la capacità di trattare grammatiche ad attributi di tipo più generale; efficienza di calcolo del valutatore: tempo di calcolo (o memoria); possibilità di accorpare l’analisi sintattica e valutazione degli attributi; incrementalità nel calcolo degli attributi di fronte a modifiche della frase (o dell’albero) da analizzare: una modifica non richiede di rifare da zero la valutazione; complessità della costruzione del valutatore. Ordine di valutazione I numeri dell’esempio sulle dichiarazioni sono un ordinamento topologico Se scriviamo in ordine le regole semantiche otteniamo il seguente programma: a4 := real; a5:= a4; addtype(id3.entry, a5); • La valutazione degli attributi a7:= a5; sintetizzati dei simboli terminali addtype(id2.entry, a7); non viene considerata a9:= a7; • Infatti questi valori sono già addtype(id1.entry, a9); disponibili dall’analisi lessicale 26 Ordinamento topologico Si ordinano topologicamente (rispetto alla relazione di dipendenza funzionale) le comparse degli attributi nell' albero Si applicano in tale ordine le funzioni semantiche, si è così sicuri di rispettare la condizione fondamentale che il calcolo di un attributo sia preceduto dalla valutazione di tutti i suoi argomenti. Tra i tanti ordinamenti topologici possibili, sono da preferire quelli che meglio rispondono alle esigenze applicative Schedulazione La schedulazione e può avvenire in diversi momenti: Statica: al momento della costruzione del valutatore. e dipende da H ma non da t, in quanto la scelta del prossimo attributo da calcolare è codificata nel programma del valutatore. Un caso particolarmente semplice è quello in cui l’ordine è lo stesso per ogni produzione della sintassi (schedulazione fissa). Dinamica: l’ordinamento topologico viene calcolato di volta in volta per l’albero t. esempio utilizzando un algoritmo parallelo seguente che associa un processo ad ogni comparsa di un attributo 27 Definizione degli attributi Syntax-directed definitions sono utilizzate per specificare traduzioni syntax-directed. La creazione di un traduttore per un definizione syntaxdirected può essere difficile. Per valutare le regole semantiche durante il parsing (compilatori a singolo passo) si possono utilizzare due sottoclassi di syntax-directed definitions: S-Attributed Definitions: solo attributi sintetizzati L-Attributed Definitions: attributi ereditati e attributi sintetizzti dipendenti dalla parte sinistra. L-Attributed Definitions Una definzione syntax-directed è di tipo L (L- attributed) se ogni attributo di Xj, con 1≤j≤n, nella RHS di A → X1X2...Xn dipende solo da: 1. 2. Gli attributi dei simboli X1,...,Xj-1 della parte sinistra di Xj nella produzione e Gli attributi ereditati di A Una grammatica del tipo L risulta necessariamente aciclica. Infatti è immediato vedere che il grafo delle dipendenze di ogni produzione è aciclico. 28 Analizzatori a singolo passo Nelle traduzioni più semplici si riesce a calcolare il significato di una frase con un solo attraversamento dell’albero, ottenendo così la massima efficienza possibile. Si presentano qui le condizioni che assicurano la calcolabilità degli attributi mediante una passata discendente da sinistra a destra o destrorsa (ordine anche detto anticipato o preordine). Il metodo qui esposto rientra come caso particolare in quelli ad una scansione (one-sweep) più avanti descritti, che permettono ordini di visita anche non rivolti da sinistra a destra. Le grammatiche che permettono la valutazione con una visita destrorsa in profondità sono ora caratterizzate mediante una condizione nota come L (che sta per "left to right"). Una definizione che non è di tipo L produzioni A→LM A→QR regole semantiche L.in=l(A.i), M.in=m(L.s), A.s=f(M.s) R.in=r(A.in), Q.in=q(R.s), A.s=f(Q.s) La grammatica non è di tipo L perchè la regola Q.in=q(R.s) viola le restrizioni di tipo L Q.in deve essere valutato prima che noi entriamo in Q poichè esso è un attributo ereditato Ma il valore di Q.in dipende da R.s che sarà disponibile alla fine di R. Cosi, non sarà possibile valutare Q.in prima di entrare in Q. 29 Grammatiche di tipo L Data una grammatica della classe L è facile esprimere l’algoritmo di calcolo sotto forma di procedure a discesa ricorsiva. Sia p: D0→ D1 D2 ... Dr la produzione nel nodo n0 . Come primo schema realizzativo, si definisce per ogni produzione p, una procedura Discendente_p con il compito di calcolare gli attributi sint(n0); essa ha come parametri d’ingresso il sottoalbero radicato in n0 e gli attributi ered(n0). Grammatiche di tipo L Void Discendente_p (in n0 : nodo, ered(n0) : out sint(n0) ) { for k:=1 to r { if (Dk è nonterminale) { <calcola ered(nk) eseguendo le funzioni semantiche corrispondenti> -- sia qk la produzione del nodo nk ; Discendente_ qk (nk, ered(nk); sint(nk)) -- chiamata che ricorsivamente calcola sint(nk) }} calcola sint(n0) eseguendo le funzioni semantiche corrispondenti> } 30 Top-Down Evaluation (of S-Attributed Definitions) Productions Semantic Rules A→B print(B.n0), print(B.n1) B → 0 B1 B.n0=B1.n0+1, B.n1=B1.n1 B.n0=B1.n0, B.n1=B1.n1+1 B → 1 B1 B→ε B.n0=0, B.n1=0 Dove B ha due attributi sintetizzati (n0 e n1). Valutazione Top-Down (di attributi S) In un parser ricorsivo discendente ogni non terminale corrisponde ad una. void A() { B();} void B() { if (currtoken=0) { nexttoken(); B(); } else if (currtoken=1) { nexttoken; B(); } else if (currtoken=$) {} // $ fine stringa else error(“unexpected token”); } A→B B→0B B→1B B→ε 31 Valutazione Top-Down (di attributi S) void A() { int n0,n1; B(out n0,out n1); print(n0); print(n1); } void B(out int n0, out int n1) { if (currtoken=0) {int a,b; nexttoken(); B(out a, out b); n0=a+1; n1=b;} else if (currtoken=1) { int a,b; nexttoken(); B(out a, ref b); n0=a; n1=b+1; } else if (currtoken=$) {n0=0; n1=0; } // $ fine stringa else error(“unexpected token”); } Analizzatori a singolo passo BottomUp Uso delle grammatiche S Mettere il valore dell’attributo sintetizzato del simbolo della grammtica in una pila in parallelo. Quando un entry della pila del parser è un simbolo X (terminale o non-terminale), il correspondente entry dello stack parallelo conterra l’attribuyo sintetizzato del simbolo X. Il valore dell’attributo viene valutato durante la riduzione. A → XYZ A.a=f(X.x,Y.y,Z.z) dove tutti gli attributi sono sintetizzati. stack parallel-stack top → Z Z.z Y Y.y X X.x . . Î top → A . A.a . 32 Bottom-Up Eval. of S-Attributed Definitions Production Semantic Rules L → E return E → E1 + T E→T T → T1 * F T→F F→(E) F → digit print(val[top-1]) val[ntop] = val[top-2] + val[top] val[ntop] = val[top-2] * val[top] val[ntop] = val[top-1] Ad ogni spostamento di digit, il valore digit.lexval sarà inserito in val-stack (stack parallelo). Per gli altri spostamenti, inserire un simbolo qualsiasi in val-stack poichè tutti gli altri terminali non hanno attributi (lo stack pointer di val-stack deve essere incrementato). Canonical LR(0) Collection for The Grammar .. .. .. .. I0: L’→ L→ E→ E→ T→ T→ F→ F→ . .. .. . .. . .. .. . L I: L 1 L’→L Er E I: E+T 2 L →E r E →E +T T T*F T I: F 3 E →T (E) T →T *F d F I: 4 T →F ( d I5: F → E→ E→ T→ T→ F→ F→ I6: F →d + * ( E) E+T E T T*F F T 3 (E) F d 4 ( d . .. . .. I7: L →Er r I8: E →E+ T T → T*F T→ F F → (E) F→ d .. . .. I9: T →T* F F → (E) F→ d .. I11: E →E+T T →T *F T F 4 ( * 9 5 d 6 . F I12: T →T*F ( 5 d 6 ) I10:F →(E ) E →E +T + I13: F →(E) . 8 5 6 33 Bottom-Up Evaluation -- Esempio 5+3*4r stack val-stack 0 input action semantic rule 5+3*4r s6 d.lexval(5) into val-stack F.val=d.lexval – do nothing 0d6 5 +3*4r F→d 0F4 5 +3*4r T→F T.val=F.val – do nothing 0T3 5 +3*4r E→T E.val=T.val – do nothing 0E2 5 +3*4r s8 push empty slot into val-stack 0E2+8 5- 3*4r s6 d.lexval(3) into val-stack F.val=d.lexval – do nothing 0E2+8d6 5-3 *4r F→d 0E2+8F4 5-3 *4r T→F T.val=F.val – do nothing 0E2+8T11 5-3 *4r s9 push empty slot into val-stack 0E2+8T11*9 5-3- s6 d.lexval(4) into val-stack 0E2+8T11*9d6 5-3-4 r F→d F.val=d.lexval – do nothing 0E2+8T11*9F12 5-3-4 r T→T*F T.val=T1.val*F.val 0E2+8T11 5-12 r E→E+T E.val=E1.val*T.val 0E2 17 r s7 push empty slot into val-stack 17- $ L→Er print(17), pop empty slot from val- 17 $ acc 0E2r7 4r stack 0L1 Schema di traduzione In una syntax-directed definition, noi non diciamo niente circa la l’ordine di valutazione delle regole semantiche Un translation scheme è una grammatica di tipo 2 nella quale: Gli attributi sono associati con i simboli della grammartica e Le azioni semantiche sono incluse fra parentesi {} e insirite nella parte destra delle produzioni. Ex: A → { ... } X { ... } Y { ... } Semantic Actions 34 Schema di traduzione Quando si progetta uno schema di traduzione, alcune restrizioni dovrebbero essere osservate per assicurare che il valore si un attributo sia disponibile quando una azione samantica si riferisce a tale attributo Queste restrizioni (grammatiche L) assicurano che una azione semantica non si riferisca ad un attributo non ancora computato. In uno schema di traduzione useremo il termine semantic action piuttosto che semantic rule utilizzato nella syntax-directed definitions. Schema di traduzione per attributi S Se la syntax-directed definition è di tipo S il corrispondente schema di traduzione si ricava semplicemente. Ogni regola semantica associata ad una syntax-directed definition con attributi S sara inserita come azione semantica alla fine della parte destra della prodeuzione associata Production Semantic Rule E → E1 + T E.val = E1.val + T.val Î syntax directed definition ⇓ E → E1 + T { E.val = E1.val + T.val } Î corresponding translation scheme 35 Esempio di schema di traduzione Un semplice schema di traduzione che converte espressioni infisse nella corrispondente espressione postfissa E→TR R → + T { print(“+”) } R1 R→ε T → id { print(id.name) } Î ab+c+ a+b+c Espressione infissa Espressione postfissa Schema Edi traduzione R T id {print(“a”) } + id T R {print(“+”)} {print(“b”)} + T id {print(“+”)} {print(“c”)} R ε L’attraversamento depth first dell’albero sintattico (eseguendo le azioni semantiche in ordine) produrra la rappresentazione postfissa della espressione infissa 36 Attributi ereditati in uno schema di traduzione Se in uno schema di traduzione sono presenti sia attributi ereditati che sintetizzati si devono seguire le seguenti regolo:: 1. 2. 3. Un attributo ereditato di un simbolo nel RHs di una produzione deve essere computato in una azione semantica prima del simbolo. Una azione semantica non si deve riferire ad un attributo sintetizzato di un simbolo della RHS di tale azione semantica Un attributo sintetizzato per un non terminale nella LHS può solo essere computato dopo che tutti gli attributi a cui si riferisce sono stati computati (noi normalmente porremo tale azione semantica alla fine della produzione) Con una grammatica L (L-attributed syntax-directed definition) è posssible sempre construire il corrispondente schema di traduzione che soddisfa queste tre regole. Un schema di traduzione con attributi ereditati D → T id { addtype(id.entry,T.type), L.in = T.type } L T → int { T.type = integer } T → real { T.type = real } L → id { addtype(id.entry,L.in), L1.in = L.in } L1 L→ε Questo e il translation scheme per una L-attributed definitions. 37 Predictive Parsing (di attributi ereditati) void D() { int Ttype,Lin,identry; T(&Ttype); nextoken(id,&identry); addtype(identry,Ttype); Lin=Ttype; call L(Lin); a synthesized attribute (an output parameter) } Void T(int *Ttype) { if (currtoken is int) {nextoken(int); *Ttype=TYPEINT; } else if (currtoken is real) { consume(real); *Ttype=TYPEREAL; } else { error(“unexpected type”); } } an inherited attribute (an input parameter) Void L(int Lin) { if (currtoken is id) { int L1in,identry; nextoken(id,&identry); addtype(identry,Lin); L1in=Lin; call L(L1in); } else if (currtoken is endmarker) { } else { error(“unexpected token”); } } Eliminazione delle ricorsioni sinistre dallo schema di traduzione Schema di traduzione con una grammatica left. E → E1 + T { val of E = E1.val + T .val } E → E1 - T { E.val = E1.val –T .val } E→T { E.val = T.val } T → T1 * F { T.val = T1 .val* F.val } T→F { T.val = F.val } F→(E) { F.val = E.val } F → digit { F.val = digit.lexval } Quando eliminiamo la ricorsione sinistra dalla grammatica dobbiamo eseguire delle modifiche nelle regole semantiche 38 Eliminazione ricorsione sinistra E → T { A.in=T.val } A { E.val=A.syn } A → + T { A1.in=A.in+T.val } A1 { A.syn = A1.syn} A → - T { A1.in=A.in-T.val } A1 { A.syn = A1.syn} A→ε { A.syn = A.in } T → F { B.in=F.val } B { T.val=B.syn } B → * F { B1.in=B.in*F.val } B1 { B.syn = B1.syn} B→ε { B.syn = B.in } F → ( E ) { F.val = E.val } F → digit { F.val = digit.lexval } Eliminazione ricorsione sinistra A → A1 Y { A.a = g(A1.a,Y.y) } sinistra A → X { A.a=f(X.x) } (a,y,x). grammatica ricorsiva con attributi sintetizzati ⇓ eliminazione ricorsione sinistra attributo ereditato del nuovo non terminale attributo sintetizzato del nuovo non terminale A → X { R.in=f(X.x) } R { A.a=R.syn } R → Y { R1.in=g(R.in,Y.y) } R1 { R.syn = R1.syn} R → ε { R.syn = R.in } 39 Valutazione degli attributi A A A Y A.a=g(f(X.x),Y.y) X R.in=f(X. R R.syn=g(f(X.x),Y.y) x) X X.x=f(X.x) Y R1.in=g(f(X.x),Y.y)R R.syn=g(f(X.x),Y.y 1 ε R1.syn=g(f(X.x),Y y) Schema di traduzione – Generazione di codice intermedio E → T { A.in=T.loc } A { E.loc=A.loc } A → + T { A1.in=newtemp(); emit(add,A.in,T.loc,A1.in) } A1 { A.loc = A1.loc} A → ε { A.loc = A.in } T → F { B.in=F.loc } B { T.loc=B.loc } B → * F { B1.in=newtemp(); emit(mult,B.in,F.loc,B1.in) } B1 { B.loc = B1.loc} B → ε { B.loc = B.in } F → ( E ) { F.loc = E.loc } F → id { F.loc = id.name } 40 Predictive Parsing – Intermediate Code Generation void E(char **Eloc) { char *Ain, *Tloc, *Aloc; T(&Tloc); Ain=Tloc; A(Ain,&Aloc); *Eloc=Aloc; } Void A(char *Ain, char **Aloc) { if (currtok is +) { char *A1in, *Tloc, *A1loc; nexttoken(+); T(&Tloc); A1in=newtemp(); emit(“add”,Ain,Tloc,A1in); A(A1in,&A1loc); *Aloc=A1loc; } else { *Aloc = Ain } } Predictive Parsing (cont.) T(char **Tloc) { char *Bin, *Floc, *Bloc; F(&Floc); Bin=Floc; B(Bin,&Bloc); *Tloc=Bloc;} B(char *Bin, char **Bloc) { if (currtok is *) { char *B1in, *Floc, *B1loc; nextoken(+); call F(&Floc); B1in=newtemp(); emit(“mult”,Bin,Floc,B1in); B(B1in,&B1loc); Bloc=B1loc;} else { *Bloc = Bin }} F(char **Floc) { if (currtok is “(“) { char *Eloc; nexttolken(“(“); call E(&Eloc); consume(“)”); *Floc=Eloc } else { char *idname; nexttoken(id,&idname); *Floc=idname } } 41 Valutazione Bottom-Up di attributi ereditati Utilizzando uno schema di traduzione top-down si può implementare qualsiasi L-attributed definition basata su grammatiche LL(1). Utilizzando uno schema di traduzione bottom-up translation scheme, si può anche implementare qualsiasi L-attributed definition basata su grammatiche LL(1) (qualsiasi grammatica LL(1) è anche LR(1)). Oltre alle L-attributed definitions basate su grammatiche LL(1), noi possiamo impe,emtare alcune L-attributed definitions basate su grammatiche LR(1) (non tutte) utilzzando uno schema di traduzione bottom-up. Removing Embedding Semantic Actions Nello schema di valutazione bottom-up, le azioni semantiche sono valutate durante la riduzione. Durinte la valutazione bottom-up degli attributi S noi abbiamo uno stack parallelo che contiene gli attributi sintetizzati. Problema: dove memorizzare gli attributi ereditati? Soluzione: Convertie la nostra grammatica in una equivalente che garantisca: Tutte le azioni semantiche embedded sono rimosse Tutti gli attributi ereditati sono copiati negli attributi sintetizzati. Quindi valujtare tutte le azioni semantiche durante le riduzioni e troveremo un posto per gli attributi ereditati 42 Rimozione di Azione Semantiche Embedded 1. 2. 3. 4. Per trasformare uno schema di traduzione in uno equivalente Rimuovere un azione semantica embedded Si, ponendo un nuovo non terminale Mi al posto dell’azione semantica. Porre l’azione semantica Si alla fine della nuova produzione Mi→ε per il non terminaleMi. Questa azione semantica Si sara valutata quando la nuova produzione sarà ridotta. L’ordine di valutazione delle regole semantiche rimane invariato da questa trasformazione Esempio di rimozione delle azioni semantiche embedded A→ {S1} X1 {S2} X2 ... {Sn} Xn ⇓ A→ M1 X1 M2 X2 ... Mn Xn M1→ε {S1} M2→ε {S2} . . Mn→ε {Sn} 43 Rimozione azioni semantiche embedded E→TR R → + T { print(“+”) } R1 R→ε T → id { print(id.name) } ⇓ E→TR R → + T M R1 R→ε T → id { print(id.name) } M → ε { print(“+”) } Traduzione con attributi ereditati Supponiamo che ogni non-terminale A abbia un attributo ereditato A.i e ogni simbolo C abbia un attributo sintetizzato nella nostra grammatica Per ogni produzione A→ X1 X2 ... Xn, (1) Introdurre dei nuovi non terminali M1,M2,...,Mn e Sostituire (1) con A→ M1 X1 M2 X2 ... Mn Xn Gli attributi sintetizzati di Xi non verranno modificati Gli attributi ereditati di Xi saranno copiati in un attributo sintetizzato di Mi nella nuova regola semantica aggiunta alla fine della produzioneMi→ε. A → {B.i=f1(...)} B {C.i=f2(...)} C {A.s= f3(...)} Ora, glia attributi ereditati di Xi possono essere trovati in un ⇓ attributo sintetizzato di Mi (che è disponibile nello stack). A → {M1.i=f1(...)} M1 {B.i=M1.s} B {M2.i=f2(...)} M2 {C.i=M2.s} C {A.s= f3(...)} M1→ε {M1.s=M1.i} M2→ε {M2.s=M2.i} 44 Traduzione con attributi ereditati S → {A.i=1} A {S.s=k(A.i,A.s)} A → {B.i=f(A.i)} B {C.i=g(A.i,B.i,B.s)} C {A.s= h(A.i,B.i,B.s,C.i,C.s)} B → b {B.s=m(B.i,b.s)} C → c {C.s=n(C.i,c.s)} S → {M1.i=1} M1 {A.i=M1.s} A {S.s=k(M1.s,A.s)} A → {M2.i=f(A.i)} M2 {B.i=M2.s} B {M3.i=g(A.i,M2.s,B.s)} M3 {C.i=M3.s} C {A.s= h(A.i, M2.s,B.s, M3.s,C.s)} B → b {B.s=m(B.i,b.s)} C → c {C.s=n(C.i,c.s)} M1→ε {M1.s=M1.i} M2→ε {M2.s=M2.i} M3→ε {M3.s=M3.i} Schema di traduzione reale S → {M1.i=1} M1 {A.i=M1.s} A {S.s=k(M1.s,A.s)} A → {M2.i=f(A.i)} M2 {B.i=M2.s} B {M3.i=g(A.i,M2.s,B.s)} M3 {C.i=M3.s} C {A.s= h(A.i, M2.s,B.s, M3.s,C.s)} B → b {B.s=m(B.i,b.s)} C → c {C.s=n(C.i,c.s)} M1→ε {M1.s= M1.i} M2→ε {M2.s=M2.i} M3→ε {M3.s=M3.i} 45 Schema di traduzione reale S → M1 A M1→ ε A → M2 B M3 C M2→ ε M3→ ε B→b C→c { s[ntop]=k(s[top-1],s[top]) } { s[ntop]=1 } { s[ntop]=h(s[top-4],s[top-3],s[top-2],s[top-1],s[top]) } { s[ntop]=f(s[top]) } { s[ntop]=g(s[top-2],s[top-1],s[top])} { s[ntop]=m(s[top-1],s[top]) } { s[ntop]=n(s[top-1],s[top]) } Valutazione degli attributi S S.s=k(1,h(..)) A.i=1 A A.s=h(1,f(1),m(..),g(..),n(..)) B.i=f(1) B C.i=g(1,f(1),m(..)) C B.s=m(f(1),b.s) b C.s=n(g(..),c.s) c 46 Evaluation of Attributes stack M1 M1 M2 M1 M2 b M1 M2 B M1 M2 B M3 M1 M2 B M3 c M1 M2 B M3 C n(g(..),c.s) M1 A S input bc$ bc$ bc$ c$ c$ c$ $ $ s-attribute stack 1 1 1 1 1 1 1 $ $ 1 h(f(1),m(..),g(..),n(..)) k(1,h(..)) f(1) f(1) f(1) f(1) f(1) f(1) b.s m(f(1),b.s) m(f(1),b.s) g(1,f(1),m(f(1),b.s)) m(f(1),b.s) g(1,f(1),m(f(1),b.s)) c.s m(f(1),b.s) g(1,f(1),m(f(1),b.s)) Problemi Tutti gli L-attributed definitions basati su grammatiche LR non possono essere valutati durante il parsing bottom-up. S → { L.i=0 } L essere L → { L1.i=L.i+1 } L1 1 up L → ε { print(L.i) } Î questo schema di traduzione non può implementato durante il parsing the bottom- S → M1 L Î ma dato che L → ε sara ridotto prima L → M2 L1 1 dal’analizzatore L → ε { print(s[top]) } botton-up il traduttore non puo conoschere il numero si. M1 → ε { s[ntop]=0 } 1 M2 → ε { s[ntop]=s[top]+1 } 47 Problemi La grammatica modificata non può essere LR. L→Lb L→a Î L→MLb L→a M→ε NOT LR-grammar . L → . M L b, $ L → . a, $ M → .,a Î shift/reduce conflict S’ → L, $ 48