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 EE+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
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
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   (VT)* - {}, 1 (VT)* 2 (VT)*
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
Dd|  }
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
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 , $}
25