Professional Documents
Culture Documents
03 - Top-Down Parsiranje
03 - Top-Down Parsiranje
1
Gramatike - podsjećanje
• KSG kao vodič za izgradnju parsera
• Potrebno je modifikovati gramatike da bi se
mogao primijeniti odgovarajući algoritam
• Pretpostavimo da imamo gramatiku G za dati
jezik L
• Kako konstruišemo parser za dati jezik na
osnovu gramatike?
2
Definisanje KSG
KSG je struktura {S, P, N, T}
S = startno stanje
P = produkcije (pravila)
N = neterminali
T = terminali
V = NT
p P ima oblik X -> W1 W2 …Wn
X N
Wi V, i=1,...,n
3
KSG primjer 1
Palindromi
npr. “abba”
T = {a, b} N={E } S=E
P = { E -> aEa | bEb | } E
Derivacija Pravilo / | \
1. E aEa E -> aEa a E a
/ | \
2. E abEba E -> bEb
b E b
3. E abba E-> |
4. E abba
4
KSG primjer 1
T = {a, b} N = { E } S=E
P = { E -> aEa | bEb | }
E
Prikaz rečenice – obilazak stabla / | \
a E a
/ | \
b E b
|
5
KSG primjer 2
T = {a, b} N = { S }
S = S, S = { S -> | aSbS | bSaS }
Stringovi sa jednakim brojem slova a i b.
S S
a S b S a S b S
b S a S a S b S
6
Transformacija gramatika
• Uklanjanje nejasnoća
• Uklanjanje zajedničkih prefiksa (lijevo
faktorisanje)
• Eleminisanje lijeve rekurzije
7
Nejasne gramatike
8
Nejasne gramatike
E –> E op E | ( E ) | int
op –> + | - | * | /
Input: 10 –2 * 5
E E
E OP E
E OP E
E OP E * int E OP E
int -
int - int 5 10 int * int
10 2 2 5
Koje je stablo ispravno?
9
Otklanjanje nejasnoća
• Prilikom pisanja gramatike treba voditi računa
da se eleminišu nejasnoće
E –> E Add_op T | T
Add_op–> + | -
T –> T Mult_op F | F
Mult_op –> * | /
F –> (E) | int
10
Zajednički prefiksi
Neki parseri koriste sledeći terminal (token) za
izbor pravila gramatike.
Ako dva pravila imaju isti prefiks, nemoguće je
odrediti koje od njih treba primjeniti.
X -> aBcD | aB
Lijevo faktorisanje eliminiše zajedničke prefikse.
X -> aB End End -> | cD
Pojednostavljuje generisanje parsera.
11
Lijevo faktorisanje - primjer
SiEtS|iEtSeS|a
Eb
SiEtSS’ |a
S’eS|
Eb
12
Rekurzija u pravilima gramatike
U teoriji uobičajen način predstavljanja, problem
nastaje kod implementacije
(Rekurzivna pravila mogu dovesti do bekonačne petlje)
• A -> Aw lijeva rekurzija
• A -> wA desna rekurzija
Primjer: <var_list> -> <var_list> <variable> |
<variable>
• Gramatika se može transformisati tako da se
deterministički eliminiše lijeva ili desna rekurzija.
13
Rekurzija u pravilima gramatike
Transformacija G (sa lijevom rekurzijom) u G’ (sa
desnom rekurzijom):
G: X –> Xa | Xb | AB | C | DEF
Dodati neterminal X’ na kraj svakog nerekurzivnog
pravila za X:
X –> ABX' | CX' | DEFX'
X' –> aX' | bX' |
Isti jezik, ali je rekurzija sada na desnoj strani.
(Ovo je vezano za pojedine algoritme za kreiranje
parsera.)
14
Indirektna rekurzija ili prefiksi
• Rekurzija ili zajednički prefiksi ne
moraju biti odmah vidljivi:
Rekurzija Zajednički prefiks
X -> YW X -> aW | VZ
Y -> XZ V -> aY
• Poslije primjene dva ili tri pravila:
X -> YW X -> aW | VZ
Y -> XZ V -> aY
X + XZW X aW, X + aYZ
15
Lijeva rekurzija - primjer
• SAa|b
• AAc|Sd|
• Eliminisanje indirektne rekurzije SAa|
b AAc|Aad|bd|
• Eliminacijom direktne lijeve rekurzije
• AbdA’|A’
• A’cA’|adA’|
16
Načini parsiranja
• Top down
– kreće se od početnog neterminalnog
simbola gramatike
– primjenjuju se pravila (produkcije) dok se
ne dobije tražena sentenca
– izgradnja drveta parsiranja od korijena ka
listovima, (preorder)
• Bottom up
– počinjemo od sentence
– redukcija na startni simbol
– (proces obrnut desnoj derivaciji)
17
Top down parsiranje - ideja
18
Bottom up parsiranje - ideja
if x == y then z = 1; else z = 2;
19
ET+E|T
Ulazni niz:
Primjer: TF*T|F
2*3
F Int
Top-down Bottom up
E 2*3
T Int * 3
F*T F*3
Int * T F * Int
2*T F*F
2*F F*T
2 * Int T
2*3 E
20
Top-down
• Lako se koriste sa malim gramatikama i
za ad hoc parsere
• Parser posmatra 1 token unaprijed, ne
čitav string
– Kako izabrati produkciju na osnovu 1
tokena?
– Na primjer: B -> b | bB
– Problem: zajednički prefiks!
21
Mogući pristup: backtracking
• Primjena pravila, provjeriti da li pravilo
prolazi, ako se zaglavimo, vraćamo se
nazad
• Može se primijeniti više pravila do
zaglavljivanja
• Moguć je veliki broj povrataka i
višestruko primjenjivanje pravila
22
Gramatika:
Backtracking primjer S -> p + Int
Sent. form Ulaz Derivacija S -> p * Int
Int -> 5 | 6 | 7
S p * 5 pokušaj p + Int
p + Int p * 5 poklapanje p
+ Int *5 oops, “+” “*”, povratak,
pokušaj sledeće
pravilo
S p * 5 pokušaj p * Int
p * Int p * 5 poklapanje p
* Int *5 poklapanje *
Int 5 pokušaj Int -> 5
5 5 uspjeh!
23
Osobine backtracking-a
• Prednosti:
– može se primijeniti na svaku KSG
– eventualno daje poklapanje
• Nedostaci:
– Kombinatorna eksplozija (Combinatorial
explosion)
– Bočni efekti:
• dodati simboli u tabelu simbola
• mora se uraditi undo bočnih efekata
24
Top-down bez backtracking-a
• Kretanje naprijed, bez backtracking-a
• Gramatika mora poštovati određena pravila
• Pravilo se bira na osnovu sledećeg tokena
• Nije potreban backtracking ako je G LL(1)
– L: skeniranje ulaza slijeva udesno
– L: lijeva derivacija
– 1: posmatra se 1 ulazni token (lookahead)
25
Osobine LL(1) gramatika
• Eliminisati nejasnoće
• Problem zajedničkog prefiksa: lijevo
faktorisanje
– LL(1) parser bira pravilo na osnovu sledećeg
tokena
• Problem lijeve rekurzije
– Desna rekurzija je OK
• Prevesti G u LL(1)
• Ovo nije dovoljno; G i dalje ne mora biti
LL(1). Formalna definicija biće data kasnije,
ovo koristite kao intuiciju
26
Implementacija top-down parsera
Data je LL(1) gramatika
• Rekurzivni spust (Recursive descent)
• Table-driven prediktivni parser
– (Kao rekurzivni spust, ali može se
implementirati u nerekurzivnim jezicima)
27
Parsiranje rekurzivnim spustom
• Parsiranje sentence primjenom pravila na
startni simbol.
28
Primjer 1
function –> FUNC identifier ( parameter_list ) statements
void ParseFunction() {
Assert(lookahead == T_FUNC)
lookahead = GetSym(); // saw “FUNC”; get next token
ParseIdentifier();
Assert(lookahead == T_LPAREN)
lookahead = GetSym(); // saw “(“, get next token
ParseParameterList(); // go parse list of parms
Assert(lookahead == T_RPAREN) ;
lookahead = GetSym(); // saw “)“, get next token
ParseStatements(); // go parse body of function
}
29
Izbor pravila
Primjer:
stmt -> if_stmt | null_stmt | return_stmt
| assg_stmt | while_stmt
| block_of_stmts
• Može se vidjeti samo sledeći token
– Koja desna strana može izvesti odgovarajuću
sentencu?
• Ne želimo backtracking!
• Rješenje problema: First skupovi
30
Primjer First skupa
• Slično zajedničkim prefiksima, samo uz
tranzitivno zatvorenje
Primjer: X -> YZW | VQR
• First (YZW) sadrži sve tokene kojim mogu
počinjati sentence izvedene iz YZW
• Ako token tFirst(YZW) tada ako
– želimo da reduciramo X po nekom pravilu i t je
tekući ulazni simbol, tada je X -> YZW
moguće pravilo koje možemo primjeniti
• Ako token t ne pripada First(VQR), tada je
X -> YZW jedino pravilo koje možemo
primjeniti
31
First skup
• First skup niza simbola u, u oznaci First(u), je
skup terminalnih simbola kojim može početi niz
sentenci koje se mogu izvesti iz u.
• Formalno, razmatramo sve stringove koje
možemo izvesti iz u:
Ako u =>* v, gdje v počinje terminalnim
simbolom t, tada t pripada skupu First(u).
Ako u =>* , tada pripada skupu First(u).
32
Upotreba First skupova
G ima produkcije X -> u1 | u2 | u3 | ... | un
Tekući ulazni token je t
Koje od pravila X -> ui izabarati?
– Ako je t u tačno jednom od skupova First (ui),
možemo predvidjeti (predict).
– Ako je t u First(uj) i First(uk), nije moguće
predvidjeti
• Za izgradnju prediktivnog parsera nad G:
– Za svaki neterminalni simbol v i sva pravila v ->
u1 | u2 | ... un, skupovi First(ui) moraju biti
disjunktni
33
First skupovi - nastavak
• Nulabilni (nullable) neterminalni simboli.
X -> ABC | DEF A ->
• Tekući simbol t
• Nije dovoljno naći First(A) and First(D)
– ako A zamijenimo sa , šta ako B može izvesti string
koji počinje sa t?
• Rješenje: Follow skupovi
– Follow(A) uključuje sve tokene koji se mogu pojaviti
neposredno iza A u nekoj sentencijalnoj formi.
• Izračunavanjem First i Follow skupova, može se
izgraditi parser sa rekurzivnim spustom
34
Izračunavanje First skupa
First skup za svaki neterminalni simbol u sa lijeve strane
pravila u X1 X2 ... Xn, je:
1. Ako je X1 terminalni simbol, dodaj X1 skupu First(u). Kraj!
2. Ako je X1 neterminalni simbol, dodaj (First(X1) - skupu
First(u).
a. Ako je X1 nulabilan (X1 =>* , dodaj (First(X2) – )
skupu First(u).
Ako X2 =>* dodaj (First(X3)– , itd. do Xn, dok ne
naiđemo na prvi nenulabilni neterminali simbol.
b. Ako X1 X2... Xn =>* , dodaj skupu First(u).
35
Izračunavanje Follow skupa
Za svaki neterminal A gramatike:
1. Dodati EOF u Follow(S), gdje je S startni simbol
gramatike, EOF je $.
2. Za svako pravilo A –> uBv , gdje su u i v stringovi
simbola gramatike i B je neterminalni simbol,
dodaj sve elemente skupa First( v ) (osim )
skupu Follow(B).
3. Za svako pravilo A –> uB, ili pravilo A –> uBv gdje
First(v) sadrži (t.j. v je nulabilan), dodati sve
elemente skupa Follow(A) skupu Follow(B).
36
Recursive-descent parser: kod
void ParseA() {
switch (lookahead) {
case First(u1): /* code to recognize u1 */
return;
case First(u2): /* code to recognize u2 */
return;
...
case Follow(A): // predict production A-> if A nullable
/* usually do nothing here */
default:
printf("syntax error \n"); /* no rule matches */
exit(0);
}}
37
Recursive-descent primjer
Gramatika za IF naredbu u nekom PJ
if_stmt –> IF expr THEN stmt ENDIF
| IF expr THEN stmt ELSE stmt ENDIF
Lijeva faktorizacija:
if_stmt –> IF expr THEN stmt close_if
close_if –> ENDIF | ELSE stmt ENDIF
38
Recursive-descent primjer koda
void ParseIfStmt() {
MatchToken(T_IF);
ParseExpr ();
MatchToken(T_THEN);
ParseStmt();
ParseCloseIf(); }
39
void ParseCloseIf() {
if (lookahead == T_ENDIF)
lookahead = GetSym();
else {
MatchToken(T_ELSE);
ParseStmt();
MatchToken(T_ENDIF);
lookahead = GetSym();
}
}
40
Primjer 2
Gramatika G
G={N,T,S,P}
Neterminalni simboli:
N={A,B,C,D}
Terminalni simboli:
T = { "(" , ")" , "+" , "a" , "[" , "]" , "." }
Startni simbol:
S=A
41
Primjer 2 - nastavak
A –> B "." (1)
B –> "a" (2)
B –> "(" C ")" (3)
B –> "[" B "]" (4)
B –>(5)
C –> B D (6)
D –> "+" B D (7)
D –> (8)
42
Primjer 2 - nastavak
43
Primjer 2 - nastavak
#include <stdio.h>
#include <stdlib.h>
char sym; // Source token
void getsym(void) {
sym = getchar();
}
44
Primjer 2 - nastavak
45
Primjer 2 - nastavak
void A(void) {
B();
accept('.', " Error '.' expected");
}
void C(void) {
B();
D();
}
46
Primjer 2 - nastavak
void B(void){
switch (sym) {
case 'a':
getsym();
break;
case '(':
getsym();
C();
accept(')', " Error ')'
expected"); break;
47
Primjer 2 - nastavak
case '[':
getsym();
B();
accept(']', " Error ']' expected");
break;
case ')':
case ']':
case '+':
case '.':
break; //no action for followers of B
48
Primjer 2 - nastavak
default:
printf("Unknown symbol\n");
exit(1);
}
}
49
Primjer 2 - nastavak
void D(void) {
…
}
50
Primjer 2 - nastavak
void main() {
sym = getchar();
A();
printf("Successful\n");
}
51
Domaći zadatak!
Napisati rekurzivni prediktivni parser za aritmetičke
izraze, sa 4 osnovne računske operacije.
Promjenljive mogu biti samo slova engleske
abecede, brojne konstante kao u C-u,
52
Nerekurzivni LL(1) parser
• Top-down, kao i rekurzivni
• Kod rekurzivnog spusta:
– Funkcije obrađuju akciju za neterminale
– Call stack evidentira napredak u parsiranju
– Može doći do rekurzivnih poziva, npr.
exp -> term -> factor -> ( exp )
• Nerekurzivni (prediktivni) parser sam čuva
informacije:
– Eksplicitni zapis na steku za svako pravilo
– Eksplicitna tabela čuva informaciju o akcijama
53
Izgradnja tabele parsera
• Izgradnja tabele - ogroman rad ali jasno
definisan
• Za datu gramatiku G, tabela treba da
implementira preslikavanje koje par
<neterminal na steku, ulazni simbol>
preslikava u <pravilo gramatike>
• Preslikavanje mora biti f-ija (t.j. moramo
naći First i Follow skupove).
Preslikavanje je f-ija <==> G je LL(1)
54
Formalna definicija LL(1)
• Bez nejasnoća, bez lijeve rekurzije
• G je LL(1) ako za svake dvije produkcije
A –> u | v važi:
– First(u) First(v) =
– Najviše iz jednog od u i v može se izvesti
prazan string
- ako v =>* tada se iz u ne može izvesti
string koji počinje terminalom iz Follow(A)
(t.j. First i Follow moraju biti disjunktni ako je
A nulabilan)
55
Primjer 2
• Gramtika za aritmetičke izraze
E -> E+T | T
T -> T*F | F
F -> (E) | id
56
Primjer 2
57
Primjer 2 - nastavak
FIRST(E) = {id, ( }, FOLLOW(E) = {$, )}
FIRST(T) = {id, ( }
FOLLOW(T) = {+, $, )}
FIRST(F) = {id, ( }
FOLLOW(F) = { *, $, ),+}
FIRST(A) = {+, } , FOLLOW(A) = {$, )}
FIRST(B) = {*, }, FOLLOW(B) = {+, $ , )}
58
Kreiranje tabele parsera
• Za svaki neterminalni simbol N i
terminalni simbol x iz skupa First(N), u
polju table[x,N] upisujemo odgovarajuću
produkciju
• Ako pripada skupu First(N), tada za
svaki element y iz skupa Follow(N) u
polje table[y,N] upisujemo odgovarajuću
produkciju
59
Primjer 2 - nastavak
id + * ( ) $
E E->TA E->TA
A A->+TA A-> A->
T T->FB T->FB
B B-> B->*FB B-> B->
F F->id F->(E)
60
Primjer 2 - nastavak
Stek ulazni niz akcija
E$ id+id*id$ E->TA
TA$ id+id*id$ T->FB
FBA$ id+id*id$ F->id
idBA$ id+id*id$ match
BA$ +id*id$ B->
61
Primjer 2 - nastavak
A$ +id*id$ A->+TA
+TA$ +id*id$ match
TA$ id*id$ T->FB
FBA$ id*id$ F->id
idBA$ id*id$ match
BA$ *id$ B->*FB
62
Primjer 2 - nastavak
Stek ulazni niz akcija
*FBA$ *id$ match
FBA$ id$ F->id
idBA$ id$ match
BA$ $ B->
A$ $ A->
$ $ accept
63
Obrada grešaka pri parsiranju
Parsiranje zasnovano na gramatikama je odlično za
prepoznavanje ispravnih ulaznih nizova.
Ako je ulazni niz neispravan, korisnik želi da vidi:
• Greške po redosljedu pojavljivanja
• Lokaciju greške u izvornom kodu
• Poruke izražene terminima source jezika
– Npr. “simbol T na steku a token je x” je primjer
besmislene poruke
• Prijavljivanje svih grešaka, ali ne i svih koje
proisitiču zbog jedne nepravilnosti (na primjer,
ako promjenljiva x nije deklarisana, ne
prijavljivati grešku za svaku upotrebu x)
64