Analisi Sintattica Nicola Fanizzi Corso di Linguaggi di Programmazione Dipartimento di Informatica Università degli Studi di Bari 5 maggio 2014 N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 1 / 93 1 2 3 4 Introduzione Definizione Funzionalità del Parser Modalità di Parsing Gestione Errori Analisi Sintattica Discendente Parsing deterministico e Lookahead Insiemi FIRST e FOLLOW Forma Normale LL(k) Trasformazioni utili Analisi Discendente Guidata da Tabella Algoritmo di Parsing Grammatiche LL(1) Gestione Errori Analisi Top-down in Discesa Ricorsiva Introduzione Costruzione Parser senza gestione errori Costruzione Parser con gestione errori N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 2 / 93 Introduzione 1 2 3 4 Introduzione Definizione Funzionalità del Parser Modalità di Parsing Gestione Errori Analisi Sintattica Discendente Parsing deterministico e Lookahead Insiemi FIRST e FOLLOW Forma Normale LL(k) Trasformazioni utili Analisi Discendente Guidata da Tabella Algoritmo di Parsing Grammatiche LL(1) Gestione Errori Analisi Top-down in Discesa Ricorsiva Introduzione Costruzione Parser senza gestione errori Costruzione Parser con gestione errori N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 3 / 93 Introduzione Definizione Introduzione Analisi Sintattica riconoscimento della struttura del programma sorgente in termini della grammatica del LdP, con costruzione dell’albero sintattico corrispondente in base ai simboli (token) forniti dallo scanner chiamata (controllo) sorgente analizzatore txt lessicale caratteri token analizzatore sintattico resto del front-end albero di derivazione tabella dei simboli La componente del compilatore che si occupa di questa attività è l’analizzatore sintattico (o parser) N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 4 / 93 Introduzione Funzionalità del Parser Funzionalità del Parser Analisi costruzione dell’albero di derivazione alg. universali: alg. CYK o alg. di Earley alg. discendenti (o top-down): dalla radice alle foglie termina se G è in GNF alg. ascendenti (o bottom-up): dalle foglie alla radice termina se G non ha regole di contrazione e concatenazione Segnalazione errori sintattici segnalazione precisa della posizione degli errori recupero in modo da portare a termine l’analisi se possibile efficienza del processo N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 5 / 93 Introduzione Modalità di Parsing Attraversamento di un albero Processo ricorsivo: partenza dalla radice; per ogni nodo N possibile un’azione post-ordine pre-ordine 5 1 3 2 4 1 5 4 2 3 pre-ordine visita N e quindi attraversa tutti i suoi sotto-alberi da sinistra a destra post-ordine attraversa tutti i sotto-alberi di N (da sinistra a destra) ed, al ritorno, visita N in-ordine ... N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 6 / 93 Introduzione Modalità di Parsing Top-down Parsing 1● 2● 3● ○ ○ ○t 4 ○t 5 ○ 4● 5● ○ ○ ●t 1 ○ t2 ○t 3 ... ○t n Si costruisce l’albero in pre-ordine: Si parte con la costruzione della radice etichettata con S ogni nodo di un non-terminale viene espanso seguendo una regola che faccia corrispondere i terminali ai token in input N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 7 / 93 Introduzione Modalità di Parsing Bottom-up Parsing 3● 1● ●t 1 ●t 2 2● ●t 3 ●t 4 ●t 5 ●t6 ... ○tn Si costruisce l’albero in post-ordine: Si parte dai token in input e si costruiscono sotto-alberi fino a quello con radice S si costruiscono sotto-alberi crescenti partendo dai sotto-alberi minimi rappresentati dai singoli token non appena sia stato costruito un sotto-albero completo (es. procedendo da sinistra verso destra) si può costruire il loro padre (parte sinistra della regola) N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 8 / 93 Introduzione Gestione Errori Gestione Errori I Panic-mode. Alla scoperta di un errore, il parser scarta simboli in ingresso fino a ri-sincronizzarsi con un token ammesso (in genere delimitatori) vantaggi metodo universale di semplice implementazione; va bene in caso di pochi errori attesi in una stessa istruzione; non porta a loop infiniti svantaggi può far scartare troppi token, sorvolando su altri errori presenti. N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 9 / 93 Introduzione Gestione Errori Gestione Errori II Recupero a livello di frase. alla scoperta di un errore il parser può operare una correzione locale sull’input restante rimpiazzando il prefisso con una stringa che consente di continuare (es. ";" invece di ",") Spesso usato con parser discendenti vantaggi molto usato; può correggere ogni tipo di stringa svantaggi non si sa far fronte a situazioni dove l’errore reale sia avvenuto prima del punto determinato; può portare a loop infiniti N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 10 / 93 Introduzione Gestione Errori Gestione Errori III Produzioni d’errore. Se si ha un’idea degli errori comuni, si può estendere la grammatica con produzioni che generino costrutti erronei tipici con tali produzioni, si rileva e notifica l’errore corrispondente e lo si può gestire meglio Correzione globale. Idealmente bisognerebbe modificare il meno possibile sulla stringa di input da correggere: esistono algoritmi che scelgono una sequenza minima di cambi per ottenere una correzione dal minimo costo globale svantaggi metodo prettamente teorico e comunque costoso in termini di spazio e tempo N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 11 / 93 Analisi Sintattica Discendente 1 2 3 4 Introduzione Definizione Funzionalità del Parser Modalità di Parsing Gestione Errori Analisi Sintattica Discendente Parsing deterministico e Lookahead Insiemi FIRST e FOLLOW Forma Normale LL(k) Trasformazioni utili Analisi Discendente Guidata da Tabella Algoritmo di Parsing Grammatiche LL(1) Gestione Errori Analisi Top-down in Discesa Ricorsiva Introduzione Costruzione Parser senza gestione errori Costruzione Parser con gestione errori N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 12 / 93 Analisi Sintattica Discendente Introduzione Analisi Sintattica Discendente I Riconoscimento della struttura della frase in ingresso (sorgente) costruendo l’albero sintattico dalla radice alle foglie seguendo la derivazione canonica sinistra ad ogni passo si espande il non-terminale più a sinistra nella forma di frase generata dal parser fino a quel momento Se la forma di frase è uAω con u ∈ Σ∗ A ∈ V simbolo non-terminale corrente da espandere ω ∈ (Σ ∪ V )∗ N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 13 / 93 Analisi Sintattica Discendente Introduzione Analisi Sintattica Discendente II Algoritmi di costruzione/ricerca su albero implicito 1 depth-first 2 breadth-first Osservazioni difficoltà principale: scelta della parte destra espandere nelle produzioni per il non-terminale corrente 1 necessità di fare backtracking (fonte di inefficienza) 2 spazio di ricerca troppo ampio N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 14 / 93 Analisi Sintattica Discendente Introduzione Algoritmo Depth-First con backtracking input G = (Σ, V , S, P): grammatica; z ∈ Σ∗ : stringa output b: boolean albero t.new(S); pila s.new() token ← z.nextToken() // token corrente sull’input simbolo ← t.root // simbolo corrente (in Σ ∪ V) done ← false // fine lavoro do if simbolo ∈ Σ and simbolo = token then simbolo ← prossimo simbolo/nodo sulla frontiera token ← z.nextToken() else if simbolo ∈ Σ and simbolo 6= token then if not p.empty() then backtrack(p, simbolo, token, t) else if simbolo = A ∈ V then // considera la prima A-regola A −→ w0 ∈ P for each x ∈ w0 do t.creaFiglio(A, x) for each A −→ wi ∈ P, i 6= 0 do p.push(wi ) if ∃B ∈ V : w0 = uBy, u ∈ Σ∗ then simbolo ← B else simbolo ← ] if simbolo = ] and token = EOF then done ← true else if simbolo = ] and token 6= EOF then if not p.empty() then backtrack(p, simbolo, token, t) while not (done or p.empty()) return done N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 15 / 93 Analisi Sintattica Discendente Introduzione Algoritmo Depth-First con stack implicito DFParse(σ, φ): Boolean σ ∈ (Σ ∪ V ) // simbolo corrente φ ∈ (Σ ∪ V)∗ // suffisso frontiera corrente da anlizzare τ ← scanner.nextToken() // token corrente if σ = ] then if τ = EOF then return true else return false if σ ∈ Σ and σ 6= τ then return false else if σ ∈ Σ and σ = τ then Sia φ = πφ0 con π ∈ (Σ ∪ V) e φ0 ∈ (Σ ∪ V)∗ σ ← π; φ ← φ0 τ ← z.nextToken() else if σ = A ∈ V then sottoalberoCompletato ← false while ∃A −→ ω ∈ P and not sottoalberoCompletato do Sia ω = σ 0 ω 0 sottoalberoCompletato ← DFParse(σ 0 , ω 0 φ) return sottoalberoCompletato Da invocare con: if DFParse(S], ) then accetta else errore N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 16 / 93 Analisi Sintattica Discendente Introduzione Algoritmo Depth-First – Esempio (1) Data una grammatica G con le seguenti produzioni: 1o passo S =⇒ aAZ (2) 2o passo aAZ =⇒ abWZ (4) (1) (2) (3) (4) (5) (6) S A A W B Z −→ −→ −→ −→ −→ −→ aAZ bW bB c d e consideriamo la frase in ingresso abde N. Fanizzi Linguaggi di prog.+Lab 3o passo abWZ =⇒ abcZ la stringa abc non è un prefisso della stringa d’ingresso quindi bisogna fare backtracking (3) 2o passo aAZ =⇒ abBZ (5) 3o passo abBZ =⇒ abdZ (6) 4o passo abdZ =⇒ abde Analisi Sintattica 5 maggio 2014 17 / 93 Analisi Sintattica Discendente Introduzione Algoritmo Breadth-First input: G = (Σ, V , S, P): grammatica; z ∈ Σ∗ : stringa output: Boolean albero t.new(S); coda q.new(S) do nodo ← q.get() con nodo = uAσ e A ∈ V, u ∈ Σ∗ , σ ∈ V ∗ i ← 0 // indice dell’ultima produzione usata espansioneCompleta ← false do k if 6 ∃(A −→ w) ∈ P con k > i then espansioneCompleta ← true if not espansioneCompleta then p p0 let A −→ ω ∈ P tale che ∀A −→ ω 0 ∈ P : p0 > p > i if uwσ 6∈ Σ∗ and ∃v ∈ Σ∗ : v ∈ prefissi(z) ∧ v ∈ prefissi(uωσ) then q.insert(uωσ) t.creaNodoFiglio(uAσ, uωσ) i←p while not (espansioneCompleta or z = uωσ) while not (q.empty() or z = uωσ) return z = uωσ N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 18 / 93 Analisi Sintattica Discendente Introduzione Algoritmo Breadth-First - Esempio I Sia G+ con produzioni S −→ A A −→ T|A+T T −→ b|(A) Per riconoscere (b+b) si può costruire l’albero N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 19 / 93 Analisi Sintattica Discendente Introduzione Algoritmo Breadth-First - Esempio II b (b) T (T) ((A)) (A) (A+T+T) (A+T) (T+T) S (A+T+T+T) (T+T+T) ((A)+T) (b+T) (b+b) A (A+T)+T (A)+T (T)+T T+T b+T A+T (A)+T+T (A+T+T)+T (T+T)+T ((A))+T (b)+T (A+T)+T+T (T)+T+T T+T+T b+T+T A+T+T (A)+T+T+T T+T+T+T b+T+T+T A+T+T+T A+T+T+T+T+T A+T+T+T+T T+T+T+T+T N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 20 / 93 Analisi Sintattica Discendente Introduzione Analisi Discendente Deterministica Problemi dell’Analisi Discendente inefficienza causata dal non-determinismo 1 2 spazio: ampiezza in alg. BF tempo: backtracking in alg. DF ritrattazione azioni già intraprese (per l’analisi semantica) Rimedi: 1 trasformare la grammatica ai fini dell’analisi discendente 2 utilizzare l’informazione fornita dai simboli successivi (lookahead) Forme Normali per grammatiche libere N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 21 / 93 Analisi Sintattica Discendente Parsing deterministico e Lookahead Parsing deterministico predittivo Sia z la stringa di terminali (token) da riconoscere e si supponga già lavorato sul suo prefisso u ∈ Σ∗ ∗ S =⇒ uAω con A ∈ V, ω ∈ (Σ ∪ V)∗ Per scegliere la regola per A, il lookahead sulla stringa di input aiuta a ridurre il numero di opzioni: una sola regola nel caso deterministico Se z = uaω allora a ∈ Σ è il simbolo di lookahead per scegliere la regola per A, scartando le regole che non portano a tale prefisso N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 22 / 93 Analisi Sintattica Discendente Parsing deterministico e Lookahead Lookahead I Esempio Sia G con produzioni: S −→ aS|cA A −→ bA|cB| B −→ cB|a| per derivare acbb: prefisso lookahead produzione derivazione a S −→ aS S =⇒ aS a c S −→ cA =⇒ acA ac b A −→ bA =⇒ acbA acb b A −→ bA =⇒ acbbA acbb A −→ =⇒ acbb N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 23 / 93 Analisi Sintattica Discendente Parsing deterministico e Lookahead Lookahead II Generalizzando al caso di stringhe di lookahead: Definizione (insiemi di lookahead) Data una grammatica libera G = (Σ, V, S, P) e A ∈ V: l’insieme di lookahead di A è definito: n o ∗ ∗ LA(A) = x ∈ Σ∗ | S =⇒ uAω =⇒ ux ∈ Σ∗ l’insieme di lookahead di A −→ ω è definito: n o ∗ ∗ LA(A −→ ω) = x ∈ Σ∗ | S =⇒ uAβ ∧ ωβ =⇒ x N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 24 / 93 Analisi Sintattica Discendente Parsing deterministico e Lookahead Lookahead III Per un dato simbolo non-terminale A ∈ V, siano A −→ ω1 | · · · | ωn le relative regole di produzione Per una scelta univoca della regola da applicare guidata dalle stringhe di lookahead devono valere: Proprietà degli insiemi di lookahead 1 LA(A) = n [ LA(A −→ ωi ) i=1 2 N. Fanizzi LA(A −→ ωi ) ∩ LA(A −→ ωj ) = ∅ Linguaggi di prog.+Lab Analisi Sintattica ∀i, j : 1 ≤ i 6= j ≤ n 5 maggio 2014 25 / 93 Analisi Sintattica Discendente Parsing deterministico e Lookahead Lookahead IV Esempio Data la grammatica con produzioni: S −→ Aabd|cAbcd A −→ a|b| Insiemi di lookahead per S e le sue produzioni: LA(S) = {aabd, babd, abd, cabcd, cbbcd, cbcd} LA(S −→ Aabd) = {aabd, babd, abd} LA(S −→ cAbcd) = {cabcd, cbbcd, cbcd} N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 26 / 93 Analisi Sintattica Discendente Parsing deterministico e Lookahead Lookahead V Insiemi di lookahead per le produzioni di A: LA(A −→ a) = {aabd, abcd} LA(A −→ b) = {babd, bbcd} LA(A −→ ) = {abd, bcd} Quanti simboli servono a scegliere tra le produzioni ? Notare che: il prefisso ab può essere ottenuto sia applicando A −→ a alla FDF Abcd, sia applicando A −→ alla FDF Abd Quindi un lookahead di due simboli non è sufficiente per decidere tra le due regole Conviene basarsi su stringhe che risultino le più brevi possibili N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 27 / 93 Analisi Sintattica Discendente Parsing deterministico e Lookahead Lookahead limitato I Definizione (insiemi di lookahead limitati) Data una grammatica libera G = (Σ, V, S, P), A ∈ V e k ∈ N: l’insieme di lookahead di A di lunghezza k è definito: LAk (A) = trunck (LA(A)) l’insieme di lookahead di A −→ ω di lunghezza k è definito: LAk (A −→ ω) = trunck (LA(A −→ ω)) con trunck (T) = {u ∈ T | |u| ≤ k ∨ (uw ∈ T ∧ |u| = k)} N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 28 / 93 Analisi Sintattica Discendente Parsing deterministico e Lookahead Lookahead limitato II Per k = 3, per la grammatica precedente risulta: LAk (S −→ Aabd) LAk (S −→ cAbcd) LAk (A −→ a) LAk (A −→ b) LAk (A −→ ) = = = = = {aab, bab, abd} {cab, cbb, cbc} {aab, abc} {bab, bbc} {abd, bcd} Si osservi che k = 3 è sufficiente per decidere la regola giusta per la grammatica in oggetto N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 29 / 93 Analisi Sintattica Discendente Parsing deterministico e Lookahead Lookahead limitato III Esempio Dato il linguaggio L = {ai abci | i > 0} generato dalle grammatiche seguenti, studiamo i diversi insiemi di lookahead G1 : S −→ aSc S −→ aabc G2 : S −→ aA A −→ Sc A −→ abc G3 : S −→ aaAc A −→ aAc A −→ b N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica {aaa} {aab} {aa} {ab} {a} {b} 5 maggio 2014 30 / 93 Analisi Sintattica Discendente Insiemi FIRST e FOLLOW Insiemi FIRST e FOLLOW I Per incorporare l’informazione del lookahead nel parser occorre generare gli insiemi (limitati) a partire dalla grammatica Per ogni A ∈ V, LAk (A) contiene i prefissi (di lung. max k) di stringhe generabili a partire da A se A deriva stringhe di lunghezza inferiore a k, il resto del lookahead viene da derivazioni che seguono A in una FDF insiemi FIRSTk (A) e FOLLOWk (A) per costruire LAk (A): FIRSTk (A) prefissi delle stringhe di token derivabili da A FOLLOWk (A) prefissi delle stringhe di token che seguono le stringhe derivabili da A N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 31 / 93 Analisi Sintattica Discendente Insiemi FIRST e FOLLOW Insiemi FIRST e FOLLOW II Definizione (Insiemi FIRST limitati) Data una grammatica libera G = (Σ, V, S, P) e k ∈ N, per ogni FDF ω ∈ (V ∪ Σ)∗ : ∗ FIRSTk (ω) = trunck {x ∈ Σ∗ | ω =⇒ x} Esempio Data la grammatica G con produzioni P = {S −→ ABCabcd, A −→ a|, B −→ b|, C −→ c|}: FIRST1 (ABC) = {a, b, c, } FIRST2 (ABC) = {ab, ac, bc, a, b, c, } FIRST3 (S) = {abc, aca, aba, aab, bca, bab, cab} N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 32 / 93 Analisi Sintattica Discendente Insiemi FIRST e FOLLOW Insiemi FIRST e FOLLOW III Lemma (proprietà dei FIRST) Data una grammatica libera G = (Σ, V, S, P), per ogni k ∈ N: 1 FIRSTk () = {} 2 FIRSTk (a) = {a}, 3 FIRSTk (aβ) = {az | z ∈ FIRSTk−1 (β)}, 4 FIRSTk (αβ) = trunck (FIRSTk (α) · FIRSTk (β)) 5 se (A −→ ω) ∈ P: FIRSTk (ω) ⊆ FIRSTk (A) N. Fanizzi Linguaggi di prog.+Lab ∀a ∈ Σ Analisi Sintattica ∀a ∈ Σ 5 maggio 2014 33 / 93 Analisi Sintattica Discendente Insiemi FIRST e FOLLOW Insiemi FIRST e FOLLOW IV Definizione (Insiemi FOLLOW limitati) Data una grammatica libera G = (Σ, V, S, P) e k ∈ N, per ogni A ∈ V : ∗ FOLLOWk (A) = trunck {x ∈ Σ∗ | S =⇒ uAσ ∧ x ∈ FIRSTk (σ)} Esempio Data la grammatica G con P = {S −→ ABCabcd, A −→ a|, B −→ b|, FOLLOW1 (S) = {} FOLLOW1 (A) = {a, b, c} FOLLOW1 (B) = {a, c} FOLLOW1 (C) = {a} N. Fanizzi Linguaggi di prog.+Lab C −→ c|} : FOLLOW2 (S) = {} FOLLOW2 (A) = {ab, bc, ba, ca} FOLLOW2 (B) = {ca, ab} FOLLOW2 (C) = {ab} Analisi Sintattica 5 maggio 2014 34 / 93 Analisi Sintattica Discendente Insiemi FIRST e FOLLOW Insiemi FIRST e FOLLOW V Osservazioni FOLLOWk (B) si ottiene dalle regole che hanno B nella parte destra: A −→ αBβ Se A −→ αB ∈ P allora ogni stringa che segua A può seguire anche B e le stringhe di token che seguono B appartengono a quelle generabili da α concatenate a quelle derivate da A N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 35 / 93 Analisi Sintattica Discendente Insiemi FIRST e FOLLOW Insiemi FIRST e FOLLOW VI Lemma (proprietà dei FOLLOW) Data una grammatica libera G = (Σ, V, S, P), per ogni k ∈ N: 1 ∈ FOLLOWk (S) 2 Se A −→ αB ∈ P allora FOLLOWk (A) ⊆ FOLLOWk (B) 3 Se A −→ αBβ ∈ P allora trunck (FIRSTk (β) · FOLLOWk (A)) ⊆ FOLLOWk (B) N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 36 / 93 Analisi Sintattica Discendente Insiemi FIRST e FOLLOW Da FIRST e FOLLOW a LA - I Dalle definizioni dell’insieme di lookahead limitato a k token (e della funzione di troncamento): Teorema Data una grammatica libera G = (Σ, V, S, P) e k ∈ N, per ogni A ∈ V e A −→ ω ∈ P con ω = s1 s2 · · · sn : LAk (A) = trunck (FIRSTk (A) · FOLLOWk (A)) LAk (A −→ ω) = trunck (FIRST(ω) · FOLLOWk (A)) = trunck (FIRST(s1 ) · · · FIRST(sn ) · FOLLOWk (A)) N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 37 / 93 Analisi Sintattica Discendente Insiemi FIRST e FOLLOW Da FIRST e FOLLOW a LA - II Esempio Data G con P = {S −→ Aabd|cAbcd, A −→ a|b|} FIRST3 (S) FIRST3 (A) FIRST3 (a) FIRST3 (b) FIRST3 (c) FIRST3 (d) FOLLOW3 (S) FOLLOW3 (A) = = = = = = = = {aab, bab, abd, cab, cbb, cbc} {a, b, } {a} {b} {c} {d} {} {abd, bcd} LA3 (S −→ Aabd) = = trunck (FIRSTk (A)FIRSTk (a)FIRSTk (b)FIRSTk (d)FOLLOW3 (S))) = trunck ({a, b, } · {a} · {b} · {d} · {}) = trunck ({aabd, babd, abd}) = {aab, bab, abd} N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 38 / 93 Analisi Sintattica Discendente Forma Normale LL(k) Forma Normale LL(k) - I Classe di grammatiche (libere) in cui L la stringa in ingresso va esaminata da sinistra a destra (LeftToRight) L va costruita la derivazione canonica sinistra (Leftmost derivation) k numero di simboli di lookahead per la scelta della produzione N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 39 / 93 Analisi Sintattica Discendente Forma Normale LL(k) Forma Normale LL(k) - II Può essere utile considerare token aggiuntivi finali come marcatori di fine stringa ]k , in modo che i LAk contengano stringhe di k token Se S non è ricorsivo basta aggiungere ]k a destra di ogni sua produzione, altrimenti si considera un nuovo S0 −→ S]k Definizione (grammatiche strong LL(k)) Una grammatica libera G = (Σ, V , S, P) con marcatore ]k è in forma strong LL(k) se, per ogni coppia di derivazioni canoniche sinistre ∗ ∗ S =⇒ u1 Aσ1 =⇒ u1 ω1 σ1 =⇒ u1 zw1 ∗ ∗ S =⇒ u2 Aσ2 =⇒ u2 ω2 σ2 =⇒ u2 zw2 (con ui , wi , z ∈ Σ∗ , σi ∈ (V ∪ Σ)∗ , i = 1, 2 e |z| = k) risulta necessariamente: ω1 = ω2 N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 40 / 93 Analisi Sintattica Discendente Forma Normale LL(k) Forma Normale LL(k) - III Teorema (caratterizzazione) Una grammatica libera G = (Σ, V , S, P) è strong LL(k) sse, LAk (A) risulta partizionabile dai LAk (A −→ ω) per ogni A ∈ V Caratterizzazione negative: Teorema (ricorsione sinistra) Una grammatica libera G che abbia una variabile ricorsiva a sinistra non può essere strong LL(k), per ogni k ∈ N, Teorema (ambiguità) Una grammatica libera G che sia strong LL(k), per un certo k ∈ N, non può essere ambigua N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 41 / 93 Analisi Sintattica Discendente Trasformazioni utili Trasformazioni utili I S −→ A] A −→ T S −→ A] A −→ T A −→ TZ G1 : A −→ A+T elim. ricorsione sin. G2 : Z −→ +T Z −→ +TZ T −→ b T −→ b T −→ (A) T −→ (A) S −→ A] A −→ TB B −→ Z B −→ fattorizzazione G3 : Z −→ +TY strong LL(1) ! Y −→ Z Y −→ T −→ b T −→ (A) N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 42 / 93 Analisi Sintattica Discendente Trasformazioni utili Trasformazioni utili II Problemi: 1 ricorsione sinistra nelle produzioni della grammatica 2 presenza di prefissi comuni in parti destre per lo stesso NT N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 43 / 93 Analisi Sintattica Discendente Trasformazioni utili Trasformazioni utili III Rimedi: 1 eliminazione della ricorsione sinistra diretta1 A −→ Aρ1 | · · · |Aρr |α1 | · · · |αn diventa A −→ α1 B| · · · |αn B B −→ ρ1 B| · · · |ρr B| 2 N. Fanizzi fattorizzazione sinistra: A −→ αβ|αγ con α, β ∈ (Σ ∪ V)+ , γ ∈ (Σ ∪ V)∗ uno dei suffissi può essere e possono non avere prefissi comuni A −→ αA0 A −→ αβ|αγ diventa A0 −→ β|γ Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 44 / 93 Analisi Sintattica Discendente Trasformazioni utili Trasformazioni utili IV Esempio Data la grammatica: E ::= T | -T | E + T | E - T T ::= F | T * F F ::= i | (E ) eliminando le ricorsioni sinistre: E ::= TE 0 | -TE 0 E 0 ::= +TE 0 | -TE 0 | T ::= FT 0 T 0 ::= * FT 0 | F ::= i | (E ) 1 Per le forme indirette si impone un ordine dei NT e si utilizza la sostituzione e l’eliminazione della ricorsione diretta per trasformare le prod. che non rispettano l’ordine N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 45 / 93 Analisi Sintattica Discendente Trasformazioni utili Grammatiche LL(k) I Le grammatiche strong LL(k) danno un metodo globale di scelta della regola giusta Si può far dipendere la scelta della regola dalla particolare FDF dove occorre A da espandere (info contestuale) Definizione (grammatiche LL(k)) Una grammatica libera G = (Σ, V , S, P) con marcatore ]k è in forma LL(k) se, per ogni coppia di derivazioni canoniche sinistre: ∗ ∗ S =⇒ uAv =⇒ uω1 v =⇒ uzw1 ∗ ∗ S =⇒ uAv =⇒ uω2 v =⇒ uzw2 (con u, wi , z ∈ Σ∗ e |z| = k) risulta necessariamente: ω1 = ω2 N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 46 / 93 Analisi Sintattica Discendente Trasformazioni utili Grammatiche LL(k) II Osservazioni La famiglia delle grammatiche strong LL(k) costituisce un sottoinsieme delle LL(k) Tuttavia non tutti i ling. deterministici sono generabili da grammatiche LL(k) Per le LL(k), si definiscono insiemi di lookahead per FDF: LAk (uAσ) = FIRSTk (Aσ) per FDF, data una regola: LAk (uAσ | A −→ ω) = FIRSTk (ωσ) N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 47 / 93 Analisi Sintattica Discendente Trasformazioni utili Grammatiche LL(k) III Algoritmo deterministico parse(G ∈ LL(k), z ∈ Σ∗ ): boolean begin φ←S do Si considera φ = uAv per una certa u ∈ Σ∗ e A ∈ V Si considera z ← uyp con |y| = k for each (A −→ ωi ) ∈ P si costruisce LAk (uAv | A −→ ωi ) if ∃(A −→ ωi ) ∈ P : y ∈ LAk (uAv | A −→ ωi ) then φ ← uωi v while (φ 6= z and ∃(A −→ ωi ) ∈ P : y ∈ LAk (uAv | A −→ ωi )) if φ = z then return true else return false end N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 48 / 93 Analisi Discendente Guidata da Tabella 1 2 3 4 Introduzione Definizione Funzionalità del Parser Modalità di Parsing Gestione Errori Analisi Sintattica Discendente Parsing deterministico e Lookahead Insiemi FIRST e FOLLOW Forma Normale LL(k) Trasformazioni utili Analisi Discendente Guidata da Tabella Algoritmo di Parsing Grammatiche LL(1) Gestione Errori Analisi Top-down in Discesa Ricorsiva Introduzione Costruzione Parser senza gestione errori Costruzione Parser con gestione errori N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 49 / 93 Analisi Discendente Guidata da Tabella Analisi discendente guidata da tabella - I Si consideri una grammatica LL(1) descritta da una tabella in cui righe non-terminali colonne terminali caselle parti destre delle produzioni / info errori Tabella guida V /Σ E E0 T T0 F N. Fanizzi i TE 0 + +TE 0 -TE 0 -TE 0 * FT 0 Linguaggi di prog.+Lab ) # FT 0 i ( TE 0 0 *FT Analisi Sintattica (E) 5 maggio 2014 50 / 93 Analisi Discendente Guidata da Tabella Analisi discendente guidata da tabella - II Osservazioni Derivazione canonica sinistra =⇒ occorre mantenere solo la parte destra dell’albero sintattico (ancora da espandere) in uno stack Il parser chiede il prossimo simbolo allo scanner o usa un indice per la stringa in ingresso che viene preventivamente bufferizzata l’algoritmo risultante simula un PDA N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 51 / 93 Analisi Discendente Guidata da Tabella Algoritmo di Parsing Algoritmo di Parsing I Inizialmente sullo stack c’è S e l’indice punta al primo terminale sulla stringa in ingresso Il parser scorre la tabella in base al top dello stack e all’indice 1 2 se al top c’e’ un simbolo terminale esso deve coincidere con quello puntato dall’indice, in tal caso il top viene cancellato e l’indice viene incrementato; se al top c’e’ un non simbolo terminale si sceglie una sua produzione in base al terminale puntato dall’indice, considerando la casella corrispondente in tabella si fa avanzare l’indice; altrimenti si ricade in condizione di errore (codificabile nella tabella stessa) N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 52 / 93 Analisi Discendente Guidata da Tabella Algoritmo di Parsing Algoritmo di Parsing II Esempio N. Fanizzi Riconoscimento di - i + i * stack frase #E - i + i * i #E 0 T#E 0 T - i + i * i #E 0 T 0 F #E 0 T 0 i #E 0 T 0 - i + i * i #E 0 #E 0 T+ #E 0 T - i + i * i #E 0 T 0 F #E 0 T 0 i #E 0 T 0 - i + i * i #E 0 T 0 F * #E 0 T 0 F - i + i * i #E 0 T 0 i #E 0 T 0 - i + i * i #E 0 # Linguaggi di prog.+Lab i # # casella tab[E][-] # tab[T][i] tab[F][i] # tab[T 0 ][+] tab[E 0 ][+] # tab[T][i] tab[F][i] # tab[T 0 ][*] # tab[F][i] # tab[T 0 ][#] tab[E 0 ][#] Analisi Sintattica 5 maggio 2014 53 / 93 Analisi Discendente Guidata da Tabella Algoritmo di Parsing Implementazione I nel seguito: n = |V|, m=|Σ| 1 void parse(StringaToken w) { 2 3 4 5 6 PilaSimboli stack; // pila int ip; // indice prox simbolo ParteDestra tab[n][m]; simbolo x; 7 8 9 10 init(stack); push(stack,#); push(stack,S); 11 12 13 ip = 1; w = w+#; 14 15 16 N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 54 / 93 Analisi Discendente Guidata da Tabella Algoritmo di Parsing Implementazione II do ss = pop(stack); if (terminale(ss)) if (ss == w[ip]) // token atteso trovato ip++; // passa al prossimo else error(1); else { // NT fdf = tab[ss][w[ip]]; // p.destra in tab. if (fdf == { }) // casella vuota error(2); // nessuna azione prevista else // casella con parte destra for (i=|fdf|; i>0 : i--) push(stack,fdf[i]); } // else NT while (!empty(stack)); 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 } N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 55 / 93 Analisi Discendente Guidata da Tabella Grammatiche LL(1) Grammatiche LL(1): insiemi FIRST Data una grammatica LL(1) e ω ∈ (Σ ∪ V )+ FIRST(ω) ins. dei prefissi di stringhe terminali derivabili da ω FIRST(t) = {t} ∀t ∈ Σ ∀A ∈ V tale che A −→ ω1 |ω2 | . . . |ωn ∈ P: n [ FIRST(A) = FIRST(ωi ) = i=1 n o + = t ∈ Σ | A =⇒ tσ, σ ∈ (Σ ∪ V)∗ ∀ω = σ1 σ2 · · · σn , σi ∈ (V ∪ Σ) + se σ1 =⇒ 6 allora FIRST(ω) = FIRST(σ1 ) + + se σi =⇒ , ∀i = 1, . . . , k e σk+1 =⇒ 6 k+1 [ allora FIRST(ω) = FIRST(σi ) i=1 + se σi =⇒ , ∀i = 1, . . . , n allora FIRST(ω) = n [ FIRST(σi ) i=1 N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 56 / 93 Analisi Discendente Guidata da Tabella Grammatiche LL(1) Grammatiche LL(1): insiemi FOLLOW FOLLOW insieme dei terminali che in una derivazione possono seguire immediatamente A ∈ (Σ ∪ V) Formalmente: n o + FOLLOW(A) = t ∈ Σ | S =⇒ uAtσ con u ∈ Σ∗ , σ ∈ (Σ ∪ V )∗ Se Y −→ uAv ∈ P con A ∈ V , u ∈ Σ∗ , σ ∈ (Σ ∪ V)∗ i due insiemi sono legati dalla relazione + FOLLOW(A) ⊇ FIRST(σ) se σ =⇒ 6 ovvero FOLLOW(A) ⊇ FIRST(σ) ∪ FOLLOW(Y) altrimenti Tali insiemi possono essere calcolati da appositi algoritmi (vedi testo) N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 57 / 93 Analisi Discendente Guidata da Tabella Grammatiche LL(1) Condizioni LL(1) Una grammatica è LL(1) sse, per ogni NT A con produzioni A −→ ω1 | ω2 | . . . | ωn , sono soddisfatte le seguenti condizioni: 1 FIRST(ωi ) ∩ FIRST(ωj ) = ∅ n [ FIRST(A) = FIRST(ωi ) ∀i 6= j i=1 2 N. Fanizzi + esiste al più un solo ωj tale che ωj =⇒ e, nel caso: FIRST(A) ∩ FOLLOW(A) = ∅ Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 58 / 93 Analisi Discendente Guidata da Tabella Grammatiche LL(1) Costruzione Tabella per il Parser Per ogni A ∈ V con le produzioni A −→ ω1 | ω2 | . . . | ωn si pone tab[A][t] = ωi ∀t ∈ FIRST(ωi ) ∗ Per ogni ωj tale che ωj =⇒ si pone: tab[A][b] = ∀b ∈ FOLLOW(A) N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 59 / 93 Analisi Discendente Guidata da Tabella Gestione Errori Gestione Errori (analisi guidata da tabella) Tipi di errore 1 mancata corrispondenza tra terminale corrente e quello al top dello stack 2 accesso ad un elemento della tabella che risulta vuoto Trattamento 1 nel primo caso si hanno due alternative: 1 2 2 scartare un certo numero dei prossimi simbolo in ingresso finchè si trovino simboli per far riprendere l’analisi inserire (virtualmente) il simbolo mancante in modo da riprendere l’analisi (senza causare altri errori) nel secondo caso non ci si può basare solo sullo stato corrente: coppia (top stack, token terminale) corrente ma occorre tener conto dell’analisi già effettuata (se ne trova traccia sullo stack) usando criteri euristici N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 60 / 93 Analisi Top-down in Discesa Ricorsiva 1 2 3 4 Introduzione Definizione Funzionalità del Parser Modalità di Parsing Gestione Errori Analisi Sintattica Discendente Parsing deterministico e Lookahead Insiemi FIRST e FOLLOW Forma Normale LL(k) Trasformazioni utili Analisi Discendente Guidata da Tabella Algoritmo di Parsing Grammatiche LL(1) Gestione Errori Analisi Top-down in Discesa Ricorsiva Introduzione Costruzione Parser senza gestione errori Costruzione Parser con gestione errori N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 61 / 93 Analisi Top-down in Discesa Ricorsiva Introduzione Parsing di grammatiche LL(1) Il parser top-down di una grammatica LL(1) è in grado di scegliere univocamente la parte destra in base al prossimo simbolo a della stringa in ingresso: 1 se a ∈ FIRST(ωi ) allora si espande A con la parte destra ωi 2 se a 6∈ FIRST(ωi ) ∀i ∈ {1, . . . , n} ∗ ma ∃|j ∈ {1, . . . , n} : ωj =⇒ (e per nessun’altra k 6= j) e a ∈ FOLLOW(A) allora si espande A con 3 altrimenti si segnala la situazione di ERRORE N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 62 / 93 Analisi Top-down in Discesa Ricorsiva Introduzione Analisi Top-down in Discesa Ricorsiva Tecnica rapida di scrittura di procedure ricorsive di riconoscimento in base alle produzioni della grammatica LL(1) Lo stack viene realizzato implicitamente dal meccanismo di gestione delle chiamate delle procedure associate ad ogni non terminale agenda nel seguito: passaggio da EBNF alle procedure senza gestione errori passaggio da EBNF alle procedure con gestione errori N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 63 / 93 Analisi Top-down in Discesa Ricorsiva Introduzione Richiamo sulla (E)BNF Nelle produzioni (libere) si separa la parte sinistra da quella destra usando ::= invece di −→ uso dei metasimboli (, ), [, ], {, }: gruppo A ::= (x | y)z equivale a A −→ xz | yz opzionalità A ::= [x]yz equivale a A −→ xyz | yz chiusura A ::= {x}y equivale a A −→ y | xy | xxy | . . . chiusura lim. {x}nm come sopra ma per un numero di ripetizioni tra m e n Quando si vogliono usare questi simboli letteralmente essi vanno racchiusi tra apici N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 64 / 93 Analisi Top-down in Discesa Ricorsiva Costruzione Parser senza gestione errori Costruzione Parser da BNF I Implementazione senza gestione errori. Per ogni non-terminale, si costruisce una procedura corrispondente Siano A −→ ω1 |ω2 | · · · |ωn ; + se A =⇒ 6 allora la procedura A() da scrivere è: void A() { if (FIRST[A,1].contains(token.code)) 3 /* codice relativo alla fdf ω1 */ 4 else if (FIRST[A,2].contains(token.code)) 5 /* codice relativo alla fdf ω2 */ 6 ... 7 else if (FIRST[A,n].contains(token.code)) 8 /* codice relativo alla fdf ωn */ 9 else error(); 10 } 1 2 N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 65 / 93 Analisi Top-down in Discesa Ricorsiva Costruzione Parser senza gestione errori Costruzione Parser da BNF II + Se A =⇒ allora la procedura A() da scrivere sarà: void A() { if (FIRST[A,1].contains(token.code)) 3 /* codice relativo alla fdf ω1 */ 4 else if (FIRST[A,2].contains(token.code)) 5 /* codice relativo alla fdf ω2 */ 6 ... 7 else if (FIRST[A,n].contains(token.code)) 8 /* codice relativo alla fdf ωn */ 9 else if (!FOLLOW[A].contains(token.code)) 10 error(); 11 } 1 2 N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 66 / 93 Analisi Top-down in Discesa Ricorsiva Costruzione Parser senza gestione errori Costruzione Parser da BNF III Osservazioni token: variabile (globale) utilizzata dallo scanner per passare il prossimo simbolo per quanto riguarda il codice relativo ad ogni parte destra xi un’istruzione per simbolo in xi nell’ordine dato: per ogni NT: chiamata della relativa procedura per ogni token T: istruzione: 1 2 if (token.code == T) scan(token); else error(n); si chiama scan(token) leggere il prossimo token nel main del parser: 1 2 initScan(token); S(); inizializza lo scanner e fa partire il riconoscimento del simbolo iniziale della grammatica N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 67 / 93 Analisi Top-down in Discesa Ricorsiva Costruzione Parser senza gestione errori Esempio Data la grammatica: E ::= T | E + T T ::= F | T * F F ::= i | (E) trasformando le ricorsioni sinistre: E ::= T EE EE ::= + T EE | T ::= F TT TT ::= * F TT | F ::= i | (E) N. Fanizzi Linguaggi di prog.+Lab Si calcolano gli insiemi FIRST e FOLLOW: FIRST(T EE) = [ID, PARSIN] FIRST(+T EE) = [PIU] FIRST(F TT) = [ID, PARSIN] FIRST(*F TT) = [PER] FIRST((E)) = [PARSIN] FIRST(i) = [ID] FOLLOW(EE) = [PARDES, EOF] FOLLOW(TT) = [PIU, PARDES, EOF] Analisi Sintattica 5 maggio 2014 68 / 93 Analisi Top-down in Discesa Ricorsiva Costruzione Parser senza gestione errori parser() { initScan(token); 3 E(); 4 if (token.code != EOF) 5 error(1); 6} 1 2 7 E() { if (FIRST[T EE].contains(token.code)) { 10 T(); EE(); 11 } else error(1); 12 } 8 9 13 EE() { if (FIRST[+T EE].contains(token.code)) { 16 scan(token); T(); EE(); 17 } else if (!FOLLOW[EE].contains(token.code)) 18 error(3); 19 } 14 15 N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 69 / 93 Analisi Top-down in Discesa Ricorsiva Costruzione Parser senza gestione errori 20 21 22 T() { if (FIRST[F TT].contains(token.code)) { 25 F(); TT(); 26 } else error(1); 27 } 23 24 28 29 TT() { if (FIRST[*F TT].contains(token.code)) { 32 scan(token); F(); TT(); 33 } else if (!FOLLOW[TT].contains(token.code)) 34 error(3); 35 } 30 31 36 37 38 N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 70 / 93 Analisi Top-down in Discesa Ricorsiva Costruzione Parser senza gestione errori F() { if (FIRST[(E)].contains(token.code)) { 41 scan(token); E(); 42 if (token.code == PARDES) 43 scan(token); 44 else error(4); 39 40 45 else if (FIRST[i].contains(token.code)) scan(token); else error(1); 46 47 48 49 50 } 51 52 // PARSER N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 71 / 93 Analisi Top-down in Discesa Ricorsiva Costruzione Parser con gestione errori Costruzione Parser con gestione errori I La gestione degli errori all’analisi ricorsiva discendente si può effettuare con l’aggiunta, a ciascuna procedura, di un’istruzione di prologo un’istruzione di epilogo Insiemi di token per la Sincronizzazione Si saltano i token successivi se non sono contenuti nell’insieme di token validi fornito: s ins. token di sincronizzazione in ingresso contiene elementi di FIRST(A) ma risulta più preciso tiene conto della particolare derivazione in locale z ins. token di sincronizzazione in uscita si garantisce l’appartenenza del prossimo token ad un insieme di token susseguenti s⊆z N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 72 / 93 Analisi Top-down in Discesa Ricorsiva Costruzione Parser con gestione errori Implementazione I NT che non derivano la stringa vuota A(TokenSet s,z) { skipTo(s+FIRST(A))); // prologo 3 // salta ad un simbolo di FIRST(A) o s 4 if (FIRST(A).contains(token.code)) { 5 // codice relativo al corpo di A() 6 } else error(...); 7 skipTo(z); // epilogo 8} 1 2 N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 73 / 93 Analisi Top-down in Discesa Ricorsiva Costruzione Parser con gestione errori Implementazione II NT che può derivare la stringa vuota Y(TokenSet s,z) { skipTo(s+FIRST(Y)+z)); 3 // salta ad un simbolo di FIRST(Y) o s o z */ 4 if (FIRST(Y).contains(token.code)) { 5 // codice relativo al corpo di Y 6 } else if (!FOLLOW(Y).contains(token.code)) 7 error(...) 8 } 9 skipTo(z); // epilogo 10 } 1 2 N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 74 / 93 Analisi Top-down in Discesa Ricorsiva Costruzione Parser con gestione errori Implementazione III Funzione di salto token: skipTo(TokenSet s) { if (!s.contains(token.code)) { 3 beginSkipMsg(); // messaggio inizio skip 4 do 5 scan(token); 6 while (!s.contains(token.code)); 7 endSkipMsg(); // messaggio uscita skip 8 } 9} 1 2 N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 75 / 93 Analisi Top-down in Discesa Ricorsiva Costruzione Parser con gestione errori Implementazione IV Esempio Data la grammatica EBNF: E ::= T { + T } T ::= F { * F } F ::= i | ( E ) parser3() { FIRST[E] = FIRST[T] = FIRST[F] = [ID, PARSIN]; 3 initScan(token); 4 E([EOF],[EOF]); 5} 1 2 N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 76 / 93 Analisi Top-down in Discesa Ricorsiva Costruzione Parser con gestione errori Implementazione V E(TokenSet s,z) { skipTo(s+FIRST[E]); 3 if (FIRST[E].contains(token.code)) { 4 T(s,[PIU]+z); 5 while (token.code == PIU) { 6 scan(token); 7 T(s,[PIU]+z); 8 } 9 } else error(1); 10 skipTo(z); 11 } 1 2 N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 77 / 93 Analisi Top-down in Discesa Ricorsiva Costruzione Parser con gestione errori Implementazione VI T(TokenSet s,z) { skipTo(s+FIRST[T]); 3 if (FIRST[T].contains(token.code)) { 4 F(s,[PER]+FIRST[F]+z); 5 while (([PER]+FIRST[F]).contains(token.code)) { 6 if (token.code == PER) scan(token); 7 else error(2); 8 F(s,[PER]+FIRST[F]+z); 9 } 10 } else error(1); 11 skipTo(z); 12 } 1 2 N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 78 / 93 Analisi Top-down in Discesa Ricorsiva Costruzione Parser con gestione errori Implementazione VII F(TokenSet s,z) { skipTo(s+FIRST[T]); 3 if (FIRST[F].contains(token.code)) 4 if (token.code == PARSIN) { 5 scan(token); 6 E(s+[PARDES],[PARDES]+z); 7 if (token.code == PARDES) scan(token); 8 else error(4); 9 } else scan(token); 10 else error(1); 11 skipTo(z); 12 } 1 2 ottimizzabile eliminando le istruzioni di sincronizzazione ridondanti N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 79 / 93 Appendice - Analisi Sintattica Ascendente Appendice 5 Appendice - Analisi Sintattica Ascendente Grammatiche LR(k) Riduzione Algoritmo LR N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 80 / 93 Appendice - Analisi Sintattica Ascendente Introduzione Analisi Sintattica Ascendente I L’analisi sintattica ascendente (o bottom-up) consente di trattare una classe di grammatiche libere più ampia di quella gestita tramite tecniche top-down e consente una più sofisticata gestione degli errori In questo caso, si intende costruire l’albero di derivazione a partire dalle foglie (token) risalendo fino al simbolo distintivo L’albero viene costruito mediante riduzioni successive: Definizione (riduzione) la riduzione è la relazione inversa rispetto alla derivazione Esempio Data la grammatica G con prod. S −→ aABe A −→ Abc | b B −→ d N. Fanizzi Linguaggi di prog.+Lab riduzioni per abbcde Analisi Sintattica abbcde aAbcde aAde aABe S b←A Abc ← A d←B aABe ← S 5 maggio 2014 81 / 93 Appendice - Analisi Sintattica Ascendente Grammatiche LR(k) Grammatiche LR(k) L’Analisi Sintattica Ascendente si avvale in genere della classe di grammatiche LR(k) (spesso per k = 1) L: la stringa di ingresso viene esaminata da sinistra (Left) verso destra R: viene effettuata la riduzione destra (Right) processo inverso della derivazione canonica destra k: numero di simboli (di lookahead) successivi alla parte già riconosciuta della stringa in ingresso utili alla decisione da prendere N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 82 / 93 Appendice - Analisi Sintattica Ascendente Riduzione Riduzione I Il parser bottom-up effettua l’analisi riducendo la frase in ingresso al simbolo iniziale della grammatica forma di frase corrente fi si individua una sottostringa che coincide con la parte destra di una produzione della grammatica si sostituisce questa sottostringa in fi con la parte sinistra ottenendo fi+1 (forma di frase ridotta di fi ) N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 83 / 93 Appendice - Analisi Sintattica Ascendente Riduzione Riduzione II La sequenza di forme di frase costituisce una riduzione destra della stringa in ingresso Una forma di frase può contenere varie parti destre di produzioni: parte destra riducibile di fi (detta handle): la sottostringa che ridotta produce fi+1 prefisso LR riducibile di fi : un prefisso che contiene la parte destra riducibile come suffisso (ossia non ha altri simboli più a destra) prefisso LR: un qualunque prefisso di un prefisso riducibile; prefisso LR candidato alla riduzione: un prefisso LR che ha come suffisso la parte destra di una produzione Osservazione N. Fanizzi prefissi riducibili ⊆ prefissi LR candidati Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 84 / 93 Appendice - Analisi Sintattica Ascendente Riduzione Proprietà I Una volta effettuata la riduzione di fi = axw dove ax è il prefisso riducibile (a, x ∈ (Σ ∪ V)∗ e w ∈ Σ∗ ) nella forma di frase fi+1 = aAw mediante la regola A −→ x la stringa aA è ancora un prefisso LR di fi+1 Quindi: si può utilizzare una pila per memorizzare il prefisso riducibile corrente N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 85 / 93 Appendice - Analisi Sintattica Ascendente Riduzione Proprietà II Data la grammatica: S −→ E E −→ E + T | T T −→ T ∗ F | F F −→ i | (E) N. Fanizzi Linguaggi di prog.+Lab Sequenza di riduzioni: i+i*i F+i*i T+i*i E+i*i E+F*i E+T*i E+T*F E+T E S Analisi Sintattica 5 maggio 2014 86 / 93 Appendice - Analisi Sintattica Ascendente Riduzione Proprietà III Derivazione canonica destra: S =⇒ E =⇒ E + T =⇒ E + T ∗ F =⇒ =⇒ E + T ∗ i =⇒ E + F ∗ i =⇒ E + i ∗ i =⇒ =⇒ T + i ∗ i =⇒ F + i ∗ i =⇒ i + i ∗ i N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 87 / 93 Appendice - Analisi Sintattica Ascendente Algoritmo LR Algoritmo Algoritmi shift-reduce (es. alg. LR canonico [Knuth, 65]) il parser funziona come un PDA analisi guidata da tabella (di difficile costruzione) osservazione: a dispetto della lunghezza dell’input, della forma di frase corrente e della profondità dello stack corrente il numero di situazioni possibili è ridotto: una per ogni simbolo della grammatica N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 88 / 93 Appendice - Analisi Sintattica Ascendente Algoritmo LR Tabelle-Guida LR(k) Si suppone di avere sullo stack il prefisso LR corrente, le azioni possibili in tabella sono sposta quando il prefisso LR presente sullo stack non è riducibile, si legge il prossimo simbolo in ingresso ponendolo sullo stack riduci quando lo stack compare un prefisso riducibile, si sostituisce la parte destra riducibile con il rispettivo non-terminale della parte destra accetta lo stack contiene il simbolo iniziale; la stringa in input viene accettata errore viene richiamata un’apposita procedura di gestione degli errori N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 89 / 93 Appendice - Analisi Sintattica Ascendente Algoritmo LR Osservazioni Se la parte destra è riducibile allora si trova certamente nella parte alta dello stack (suffisso del prefisso) Per decidere se il prefisso candidato sullo stack sia proprio quello riducibile, il parser LR(1) usa il prossimo simbolo nella stringa di ingresso (lookahead) N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 90 / 93 Appendice - Analisi Sintattica Ascendente Algoritmo LR −−−−→ Esempio (cont.) i + i ∗ i grammatica: S −→ E E −→ E+T | T T −→ T *F | F F −→ i | (E) i + + i F T E E E 1 2 3 4 5 6 N. Fanizzi F + E 7 Linguaggi di prog.+Lab T + E 8 * T + E 9 i * T + E 10 F * T + E 11 T + E E S 12 13 14 Analisi Sintattica 1 sposta i 2 riduci F −→ i 3 riduci T −→ F 4 riduci E −→ T 5 sposta + 6 sposta i 7 riduci F −→ i 8 riduci T −→ F 9 sposta * 10 sposta i 11 riduci F −→ i 12 riduci T −→ T * F 13 riduci E −→ E + T 14 riduci S −→ E 5 maggio 2014 91 / 93 Appendice - Analisi Sintattica Ascendente Algoritmo LR Una grammatica è adatta all’analisi bottom-up LR(k) se il parser, rilevando un prefisso candidato in cima allo stack, decide univocamente l’azione da intraprendere in base ai prossimi k simboli in ingresso Tipologie di parsing bottom-up LR(k) metodo potente ma oneroso nella costruzione della tabella SLR(k) metodo più debole ma di facile implementazione tabella compatta LALR(k) metodo quasi al pari di LR(k) ma con tabella compatta come nel caso precedente Es. Yacc genera simili tabelle per l’analizzatore sintattico N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 92 / 93 Appendice - Analisi Sintattica Ascendente Algoritmo LR Fonti Aho, Lam, Sethi, Ullman: Compilatori - Principi, tecniche e strumenti. 2a ed., Pearson Dos Reis: Compiler Construction Using Java, JavaCC, and Yacc. Wiley-IEEE Sudkamp: Languages and Machines, 3rd ed., Addison Wesley Grune, Bal, Jacobs, Langendoen: Modern Compiler Design, Wiley Galles: Modern Compiler Design, Scott/Jones Publishing N. Fanizzi Linguaggi di prog.+Lab Analisi Sintattica 5 maggio 2014 93 / 93