Analisi sintattica Syntax Analysis albero programma scanner tokens parser sintattico rrori sintattici Un parser deve riconoscere la struttura di una stringa di ingresso, la cui struttura è fornita in termini di regole di produzione di una CFG, BNF, o diagrammi sintattici. Un parser è una macchina astratta che raggruppa input in accordo con regole grammaticali.
Sintassi La sintassi è costituita da un insieme di regole che definiscono le frasi formalmente corrette e allo stesso tempo permettono di assegnare ad esse una struttura (albero sintattico) che ne indica la decomposizione nei costituenti immediati. Ad es. la struttura di una frase (ovvero di un programma) di un linguaggio programmativo ha come costituenti le parti dichiarative e quelle esecutive. Le parti dichiarative definiscono i dati usati dal programma. Le parti esecutive si articolano nelle istruzioni, che possono essere di vari tipi: assegnamenti, istruzioni condizionali, frasi di lettura, ecc. I costituenti del livello più basso sono gli elementi lessicali già considerati, che dalla sintassi sono visti come atomi indecomponibili. Infatti la loro definizione spetta al livello lessicale. Sintassi La teoria formale dei linguaggi offre diversi modelli, ma nella quasi totalità dei casi il tipo di sintassi adottata è quello noto come sintassi libera o non-contestuale (context-free) che corrisponde al tipo 2 della gerarchia di Chomsky. I metodi sintattici per il trattamento del linguaggio sono semplici ed efficienti: la definizione del linguaggio attraverso le regole delle sintassi libere è diretta ed intuitiva; gli algoritmi deterministici di riconoscimento delle frasi sono veloci (hanno complessità lineare) e facili da realizzare partendo dalla sintassi. Tutti questi vantaggi hanno imposto le sintassi libere come l unico metodo pratico per definire formalmente la struttura di un linguaggio.
Sintassi Ma quello che si può ottenere con una sintassi libera è limitato e spesso insufficiente; la sintassi non basta a definire le frasi corrette del linguaggio di programmazione, perché una stringa sintatticamente corretta non è detto che lo sia in assoluto; infatti essa potrebbe violare altre condizioni non esprimibile nel modello noncontestuale. Ad esempio è noto che la sintassi non può esprimere le seguente condizioni: in un programma ogni identificatore di variabile deve comparire in una dichiarazione; il tipo del parametro attuale di un sottoprogramma deve essere compatibile con quello del parametro formale corrispondente. Sintassi Un altro limite del modello sintattico libero tocca le esigenze di calcolo della traduzione. Se si guardano le traduzioni definibili con i soli metodi sintattici, esse sono decisamente povere inadeguate ai problemi reali (compilazione di un linguaggio programmativo nel linguaggio macchina) Da queste critiche sarebbe sbagliato concludere che i metodi sintattici sono inutili: al contrario essi sono indispensabili come supporto (concettuale e progettuale) su cui poggiare i più completi strumenti della semantica.
Parser Il parser lavora con stringhe di tokens source program Lexical Analyze r token get next token Parser parse tree sempio: Consideriamo l analisi della frase 1 + 2 * 3 secondo la seguente grammatica Parser <expr> ::= <expr> + <expr> <expr> * <expr> L analisi sintattica può essere vista come un number processo per costruire gli alberi sintattici (parse tree). The syntax of a programming is described by a context-free grammar (CFG). We will use BNF (Backus-Naur Form) notation in the description of CFGs. Il parser verifica se un programma sorgente soddisfa le regole implicate da una grammatica context-free o no. Se le soddisfa, il parser crea l albero sintattico (del programma Altrimenti genera un messaggio di errore. Una grammatica non contestuale Da una specifica rigorosa della sintassi dei linguaggi di programmazione Il progetto della grammatica e la fase iniziale del progetto del compilatore sistono strumenti automatici per costruire automaticamente il compilatore dalla grammatica
Parser Due differenti metodi di analisi sintattica top-down: l albero sintattico è creato dalla radice alle foglie bottom-up: l albero sintattico è creato dalle foglie alla radice Il parser può lavorare in una varietà di modi ma esso tipicamente processa l input da sinistra a destra. Parser sia top-down che bottom-up possono essere implmentati in modo efficente solo per alcune sottoclassi di gramatiche context-free: LL per top-down LR per bottom-up Una left-most (right-most) derivation è una derivazione nella quale durante ogni passo solo il non-terminale più a sinistra 8destra) è sostituito. Grammatiche di tipo Context-Free Le strutture ricorsive presenti nei linguaggi di programmazione sono definite da una grammatica context-free. In una grammatica context-free grammar, noi abbiamo: Un insieme finito di terminali T (l insieme dei tokens) Un insieme finito di no terminali V(variabili sintattiche) Un insieme finito di produzioni nella forma A α dove A V e α (V T)* Un start symbol (uno dei non-terminal) sempio T = { (, ), id, +, *, /, -} V = {} + * / - ( ) id
Derivazioni + + deriva da Possiamo sostituire con by + Perr poter fare questo deve esistere una produzione + nella grammatica. + id+ id+id Una sequenza di sostituzioni di non-terminali e chiamata una derivazione di id+id da. In generale un passo di derivazione è αaβ αγβ se A γ in G α (V T)* e β (V T)* α 1 α 2... α n (α n deriva da α 1 o α 1 èderivatodaα n ) one step * zero o più steps + 1 o più steps Le produzioni devono avere le seguenti proprietà: no produzioni inutili o ridondanti (i.e., A A), no non-terminals senza produzioni (e.g., A Ba dove B non è definito), no cicli infiniti (e.g., A Aa senza altre produzioni per A), no ambiguita : una grammatica con più alberi sintattici per la stessa espressione è ambigua descrivere correttamente il linguaggio.
CFG - Terminologia L(G) è il linguaggio generato da G. Una frase di L(G) è una stringa di simboli terminali di G. Se S è l assioma o + start symbol di G allora ω è una frase di L(G) iff S ω dove ω T*. Se G è una grammatica context-free, L(G) è un linguaggio context-free. Due grammatiche sono equivalenti se producono lo stesso * linguaggio. S α Se α contiene non-terminali, è chiamata forma di frase di G. Se α non contiene no-terminali è chiamata frase di G. Left-Most and Right-Most Derivations Left-Most Derivation lm lm lm lm lm - -() -(+) -(id+) -(id+id) Right-Most Derivation rm rm rm rm rm - -() -(+) -(+id) -(id+id) top-down parser cerca una left-most derivation del programma sorgente bottom-up parser cerca una right-most derivation del programma sorgente
Parse Tree Inner nodes dell albero sintattico sono simboli non-terminal, le foglie simboli terminali. - - -() - -(+) - ( ) ( ) -(id+) - ( ) -(id+id) - ( ) + + + id id id Un albero sintattico è una rappresentazione grafica di una derivazione. Ambiguita Una grammatica è ambigua se esistono più alberi sintattici per la stessa frase. + id+ id+* id+id* id+id*id + id id * id * +* id+* id+id* id+id*id * + id id id
Per poter costruire un parser la grammatica non deve essere ambigua Grammatica non ambigua unica selezione nell albero sintattico per una frase Le ambiguità nella grammatica devono essere eliminate durante il progetto di un compilatore Ambiguità stmt if expr then stmt if expr then stmt else stmt otherstmts if 1 then if 2 then S 1 else S 2 stmt if expr then stmt else stmt 1 if expr then stmt S 2 stmt if expr then stmt 1 if expr then stmt else st 2 S 1 1 2 2 S 1 S
Ambiguità Noi preferiamo il secondo albero sintattico (else corrisponde al if più vicino). Dobbiamo eliminare l ambiguità con tale obbiettivo La grammatica non-ambigua sarà: stmt matchedstmt unmatchedstmt matchedstmt if expr then matchedstmt else matchedstmt otherstmts unmatchedstmt if expr then stmt if expr then matchedstmt else unmatchedstmt Ambiguita Precedenza degli operatori Grammatiche ambigue possono essere rese non-ambigue in accordo alle precedenze degli operatori e alle regole di associativià degli operatori. + * ^ id () +T T T T*F F F G^F G G id () precedenze: ^ (right to left) * (left to right) + (left to right)
Ricorsione sinistra (Left Recursion) Una grammatica è left recursive se ha un non + terminale A tale che A Aα per qualche stringa α Le tecniche di parser Top-down non possono gestire grammatiche left-recursive. Una grammatica left-recursive deve esssere convertita in una non left-recursive. La ricorsione sinistra può comparire in un singolo passo della derivazione (immediate left-recursion), o può comparire in più che un passo. Immediate Left-Recursion A A α β dove β Aγ e γ (V T)* A βa A αa ε grammatica equivalente In generale A A α 1... A α m β 1... β n dove β i Aγ e γ (V T)* A β 1 A... β n A A α 1 A... α m A ε grammatica equivalente
sempio +T T T T*F F F id () T +T ε T F T T *F T ε F id () eliminate immediate left recursion Left-Recursion Problema!! Una grammatica che non e direttamente left-recursive, ma lo è in modo indirettp. Anche in questo caso va eliminata la ricorsione sinistra sempio S Aa b A Sc d S Aa Sca A Sc Aac
Algoritmo per l eliminazione della ricorsione sinistra - Ordinare I non-terminali A 1... A n -for i from 1 to n do { - for j from 1 to i-1 do { sostituire ogni produzione A i A j γ con A i α 1 γ... α k γ } dove A j α 1... α k } - eliminare la ricorsione sinistra nelle produzioni di A i sempio S Aa b A Ac Sd f - Ordinare I non-terminali: S, A per S: - non c e una ricorsione sinistra diretta. for A: - sostituiamo A Sd con A Aad bd Cosi avremoa Ac Aad bd f - liminiamo la ricorsione sinistra in A A bda fa A ca ada ε Avremo la grammatica non ricorsiva equivalente: S Aa b A bda fa A ca ada ε
sempio S Aa b A Ac Sd f - Ordinare I non-terminali : A, S per A: liminamo la ricorsione sinistra in A A SdA fa A ca ε per S: - Sostituiamo S Aa with S SdA a fa a Così avremo S SdA a fa a b - liminamo la ricorsione sinistra in S S fa as bs S da as ε Avremo la grammatica non ricorsiva equivalente: S fa as bs S da as ε A SdA fa A ca ε Left-Factoring (fattorizzazione sinistra) Un parser predittivo richiede una grammatica leftfactored. grammatica grammatica equivalente stmt if expr then stmt else stmt if expr then stmt
Left-Factoring (cont.) In generale, A αβ 1 αβ 2 dove α (V T)* - {ε}, β 1 (V T)* - {ε}, β 2 (V T)* - {ε} e β 1. Noi possiamo riscrivere la grammatica come segue A αa A β 1 β 2 Algoritmo Per ogni non-terminale A con due o più alternative (produzioni) con una parte non vuota comune A αβ 1... αβ n γ 1... γ m diventa A αa γ 1... γ m A β 1... β n
sempio A abb ab cdg cdeb cdfb A aa cdg cdeb cdfb A bb B A aa cda A bb B A g eb fb sempio A ad a ab abc b A aa b A d ε b bc A aa b A d ε ba A ε c
Le frasi possono essere analizzate da sinistra a destra (L parser), possono essere costruite con derivazioni left-most (LL(k) parser) o right-most (LR(k) parser) utilizzando k symboli di lookahead! LL è più conosciuta come top-down parser. LR è più conosciuta come bottom-up parser. Per ragioni pratiche k deve essere piccolo. Per un compilatore è auspicabile l uso di grammatiche che possono essere analizzate in modo deterministico con al più k symboli di lookahead. L assenza di ambiguità è condizione necessaria per un analisi determinstica Consideriamo un bottom up parse per abbcde generato dalla seguente grammatica con assioma S, eseguento un approccio left-most matches First. S aacbe A Ab b B d
abbcde aabcbe aacbe S applicando B d applicando A b applicando A Ab applicando S aacbe A 2 S 4 A 1 B 3 a b b c d e Definzione di First(α) Data una grammatica non contestuale G(V,T,P,S) si definisce First(α) il seguente sottoinsieme di T: se α = aβ con a T allora a First(α) se α = Aβ con A T allora se una produzione A γallora First(γ) First(α) se una produzione A εallora First(β) First(α) First(α) è l insieme di tutti i terminali con cui può iniziare una stringa derivata da α. Se α *ε allora ε First(α). First(α) = {a T α * a β} if a *ε then {ε} else
sempio T = {a, b, c, d, e}, V = {S, B, C} P = { S ase B, B bbe C, C cce d } S = S First(aSe) = {a} First(B) = First(bBe) First(C) = {b} First(cCe) First(d) = {b} {c} {d} = {b, c, d} First(bBe) = {b} First(C) = First(cCe) First(d) = {c} {d} = {c, d} First(cCe) = {c} First(d) = {d} sempio T = {a, b, c, d, e} V = {A, B, C, D} P = { A B Ce a B bc, C Dc D d ε} S = A First(B) = First(b) ={b} First(C e) = First (Dc) = First(d) First(ε) = {d} {ε} = {d, ε } First(a) = {a} First(bC )= {b} First(Dc) = First(d) First(ε) = {d} {ε} = {d, ε } First(d) = {d} First(ε )= {ε }
sempio T = {(, +, ), v, f} V = {, Prefix, Tail} P = { Prefix() v Tail Prefix f ε Tail + ε } S = First(Prefix() ) = {f} {ε} First(v Tail) = {v} First(f) = {f} First(ε )= {ε} First(+) = {+} sempio T = {a, (, ), +, ","} V = {, T, L, P} P = { + T T T a () a(l) L P ε, P P "," } S = First() = {a, (} First(T) = {a, (} First(L) = {ε, a, (} First(P) = {a, (} T = {a, b, c, d, e} V = {A, B, C, D} P = { A B C Ce a B bc C Dc D D d ε } S = A First(A) = {a, b, d, c, e, ε } First(B) = {b} First(C) = {d, c, ε } First(D) = {d, ε }
Definizione di Follow(A) Data una grammatica non contestuale G(V,T,P,S) si definisce Follow(A) l insieme dei terminali che seguono in una qualsiasi frase. sso è definito come segue: Follow(A) = {a T S + Aa.} (if S * A then {ε} else } T = {a, b, c, d, e}, V = {S, B, C} P = { S ase B, B bbe C, C cce d } S = S Follow(S) = {e, $} Follow(B) = {e, $} Follow(C) = {e, $} T = {a, (, ), +, ","} V = {, T, L, P} P = { + T T T a () a(l) L P ε, P P "," } S = Follow() = {+, ),,, $} Follow(T) = {+, ),,, $} Follow(L) = { ) } Follow(P) = {),, }
T = {(, +, ), v, f} V = {, Prefix, Tail} P = { Prefix() v Tail Prefix f ε Tail + ε } S = Follow(Prefix ) = { ( } Follow() = {$} Follow(Tail) = { $ } T = {a, b, c, d, e} V = {A, B, C, D} P = { A B Ce a B bc, C Dc D d ε} S = A Follow(A) = {$} Follow(B) = {$} Follow(C) = {$, e} Follow(D) = {c} T = {a, b, c, d, e} V = {A, B, C, D} P = { A B C Ce a B bc C Dc D D d ε } S = A Follow(A) = {$} Follow(B) = Follow(A) = {$} Follow(C) = Follow(A) Follow(B) {e} = {e, $} Follow(D) = {c } Follow(C) = {c, e, $}