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