1 scanner tokens programma parser albero sintattico Errori sintattici
Transcript
1 scanner tokens programma parser albero sintattico Errori sintattici
programma scanner tokens parser albero sintattico Errori 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. 1 La sintassi è costituita da un insieme di regole che definiscono le frasi formalmente corrette 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 di programmazione 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. 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 efficienti la definizione del linguaggio attraverso le regole delle sintassi libere dal contesto è 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 grammatiche libere dal contesto come l’unico metodo pratico per definire formalmente la struttura di un linguaggio. 2 Non tutte le regole di un linguaggio di programmazione possono essere espresse in termini nel modello noncontestuale. Ad esempio non puo espremere le seguenti regole: 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 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. • Il parser lavora con stringhe di tokens Programa sorgente Scanner token get next token Parser Albero sintattico 3 Le strutture ricorsive presenti nei linguaggi di programmazione sono definite da una grammatica contextfree. Una grammatica context-free (CFG) è definita come Un insieme finito di terminali S (sono i token prodotti dallo scanner) Un insieme di non terminali V. Un assioma o simbolo iniziale (uno dei non terminali. Un insieme di produzioni (dette anche “rewriting rules” con la seguente forma A → X1 ... Xm dove X1 to Xm possono essere terminali o non terminali. Se m =0 si ha A → che è una produzione valida Le strutture ricorsive presenti nei linguaggi di programmazione sono definite da una grammatica contextfree. Una grammatica context-free (CFG) è definita come Un insieme finito diEsempio terminali V (sono i token prodotti dallo scanner) T = { (, ), id , vsl, + , * , / , -} V = {E}V. Un insieme di non terminali E E+E Un assioma o simbolo iniziale (uno dei non terminali. E E–E Un insieme di produzioni E (dette E * Eanche “rewriting rules” con E E/E la seguente forma E-E A → X1 ... Xm E ( E ) id terminali o non terminali. dove X1 to Xm possono Eessere E val Se m =0 si ha A → che è una produzione valida 4 E E+E E+E deriva da E Possiamo sostituire E con con E+E Perr poter fare questo deve esistere una produzione EE+E nella grammatica. E E+E id+E id+id Una sequenza di sostituzioni di non-terminali e chiamata una derivazione di id+id da E. In generale un passo di derivazione è A 1 2 ... n * + se A in G (V T)* e (V T)* (n deriva da 1 o 1 è derivato da n ) one step zero o più steps 1 o più steps V = { Prog, Seq, Istr, Expr} T = { {, ; , id , + ,} } Assioma = Prog Prog → { Seq } Seq → Istr; Seq Seq → Istr → { Seq } Istr →id = Expr Expr →id Expr →Expr + id V = { Prog, Seq, Istr, Expr} T = { {, ; , id , + , } } Assioma = Prog Prog → { Seq } Seq →Seq ; Istr | Istr → {Seq } | id = Expr Expr →id | Expr + id 5 Per esempio partendo da Prog possiamo generare, applicando ripetutamente le produzione le seguenti stringhe: Prog { Seq } { Istr ; Seq} {Istr; Istr ; Seq} { id = Expr ; Istr, } { id = id ;Istr ; } { id = id ; id = Expr } { id = id ; id = Expr + id} { id = id ; id = id + id} Le produzioni devono avere le seguenti proprietà: no produzioni inutili o ridondanti (i.e., A A), no non-terminali nella RHS di una produzione senza produzioni con LHS uguale al non terminale ▪ (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 6 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. Derivazione canonica sinistra (Left-Most Derivation) E -E -(E) -(E+E) -(id+E) -(id+id) Derivazione canonica destra (Right-Most Derivation) E -E -(E) -(E+E) -(E+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 7 I nodi non foglie dell’albero sintattico sono simboli non-terminal, le foglie simboli terminali. E -E E -(E) - E E - E ( E ) E - -(id+E) E - E ( E ) E + E E -(E+E) -(id+id) id E ( E ) E + E E ( E ) E + E id id Un albero sintattico è una rappresentazione grafica di una derivazione. Esempio: Consideriamo l’analisi della frase 1+2*3 secondo la seguente grammatica <expr> ::= <expr> + <expr> | <expr> * <expr> | number L’analisi sintattica può essere vista come un processo per costruire gli alberi sintattici (parse tree). La sintassi di un programma è descritto da un agrammatica libera dal contesto (CFG). La notazione BNF (Backus-Naur Form) è una notazione per la descrzione di una CFG. 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 Esistono strumenti automatici per costruire automaticamente il compilatore dalla grammatica 8 • Una grammatica è ambigua se esistono più alberi sintattici per la E stessa frase. E E+E id+E id+E*E id+id*E id+id*id E id + E E id E E*E E+E*E id+E*E id+id*E id+id*id E id E E + * E id E id * E id T = { (, ), id , + , * , / , -} V = {E} E E+E E E–E E E*E E E/E E-E E (E) E id Per poter costruire un parser la grammatica non deve essere ambigua Grammatica non ambigua un unico albero sintattico per ogni frase del linguaggio Le ambiguità nella grammatica devono essere eliminate durante il progetto del compilatore 9 istr if expr then istr | if expr then istr else istr | otheristrs if E1 then if E2 then S1 else S2 istr istr if expr then E1 istr else if expr then E2 istr istr S2 S1 1 if expr then istr E1 if expr then istr else istr E2 S1 S 2 • Noi preferiamo il secondo albero sintattico (else corrisponde al if più vicino). • Dobbiamo eliminare l’ambiguità con tale obiettivo • La grammatica non-ambigua sarà: istr matchedistr | unmatchedistr matchedistr if expr then matchedistr else matchedistr | otheristrs unmatchedistr if expr then istr | if expr then matchedistr else unmatchedistr 10 Grammatiche ambigue possono essere rese non-ambigue in accordo alle precedenze degli operatori e alle regole di associativià degli operatori. E E+E | E*E | id | (E) precedenze: E E+T | T T T*F | F F id | (E) Una grammatica è “left recursive” se ha un non terminale A + tale che A A * (left to right) + (left to right) 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. 11 AA | 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 E E+T E T T T*F T F eliminate immediate left recursion F id F (E) E T E’ E’ +T E’ E’ T F T’ T’ *F T’ T’ F id F (E) 12 • Una grammatica che non e direttamente left-recursive, ma lo è in modo indiretto • Anche in questo caso va eliminata la ricorsione sinistra Esempio S Aa | b A Sc | d S Aa Sca A Sc Aac - Ordinare I non-terminali A1 ... An - for i from 1 to n do { - for j from 1 to i-1 do { sostituire ogni produzione Ai Aj con Ai 1 | ... | k dove Aj 1 | ... | k } - eliminare la ricorsione sinistra nelle produzioni di Ai } 13 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 - Eliminiamo 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’ | S Aa | b A Ac | Sd | f - Ordinare I non-terminali : A, S per A: Eliminamo 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 - Eliminamo 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’ | 14 Un parser top down deterministico richiede una grammatica left-factored. grammatica grammatica equivalente istr if (expr ) istr else istr | if (expr) istr In generale, A 1 | 2 dove (VT)* - {}, 1 (VT)* 2 (VT)* e 1≠ 2 . Noi possiamo riscrivere la grammatica come segue A A’ A’ 1 | 2 15 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 A abB | aB | cdg | cdeB | cdfB A aA’ | cdg | cdeB | cdfB A’ bB | B A aA’ | cdA’’ A’ bB | B A’’ g | eB | fB 16 A ad | a | ab | abc | b A aA’ | b A’ d | | b | bc A aA’ | b A’ d | | bA’’ A’’ | c istr if (expr ) istr else istr | if (expr) istr istr if (expr ) istr X X else istr X 17 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 look-ahead! 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 18 abbcde applicando B d aAbcBe applicando A b aAcBe applicando A Ab S applicando S aAcBe S4 A2 B3 A1 a b b c d e 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 contextfree: LL per top-down LR per bottom-up Una “left-most (right-most) derivation” è una derivazione nella quale durante ogni passo viene sostituito solo il non-terminale più a sinistra (destra). 19 Data una grammatica non contestuale G(V,T,P,S) si definisce First() = {a in T | * a} Quindi First() è il seguente sottoinsieme di T: • se = a con a T allora a First() • se = A con A V 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 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} 20 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(bC) ={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( )= { } T = {(, +, ), v, f} V = {E, Prefix, Tail} P = { E Prefix(E) | v Tail Prefix f | Tail + E | } S=E First(Prefix(E) ) = {f} {} First(v Tail) = {v} First(f) = {f} First( )= {} First(+E) = {+} 21 T = {a, (, ), +, ","} V = {E, T, L, P} P={ E E+T|T T a | (E) | a(L) L P|, P E | P "," E} S=E First(E) = {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 Dd| } S=A First(A) = {a, b, d, c, e, } First(B) = {b} First(C) = {d, c, } First(D) = {d, } 22 Data una grammatica non contestuale G(V,T,P,S) si definisce Follow(A) l’insieme dei terminali che seguono in una qualsiasi frase Esso è 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, $} 23 T = {a, (, ), +, ","} V = {E, T, L, P} P={ E E+T|T T a | (E) | a(L) L P|, P E | P "," E} S=E Follow(E) = {+, ) , “,“ , $} Follow(T) = {+, ) , “,“ , $} Follow(L) = { ) } Follow(P) = {) , “,”} T = {(, +, ), v, f} V = {E, Prefix, Tail} P = { E Prefix(E) | v Tail Prefix f | Tail + E | } S=E Follow(Prefix ) = { ( } Follow(E) = { ), $} Follow(Tail) = {), $ } 24 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 Dd| } S=A Follow(A) = {$} Follow(B) = Follow(A) = {$} Follow(C) = Follow(A) Follow(B) {e} = {e , $} Follow(D) = {c } Follow(C) = {c, e , $} 25