You are on page 1of 64

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 = NT
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  abba 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

• Dvosmislenost - nejasnoća (Ambiguity): za


jednu rečenicu više od jednog stabla
parsiranja
• Vidi primjer na sljedećem slajdu

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
SiEtS|iEtSeS|a
Eb

Ovdje je i if, t then, e else, E i S


označavaju izraz i naredbu

SiEtSS’ |a
S’eS|
Eb
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
• SAa|b
• AAc|Sd|
• Eliminisanje indirektne rekurzije SAa|
b AAc|Aad|bd|
• Eliminacijom direktne lijeve rekurzije
• AbdA’|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
ET+E|T
Ulazni niz:
Primjer: TF*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.

• Za svaki neterminalni simbol W, piše se


funkcija za W

• Ako je G rekurzivna, funkcije mogu biti


rekurzivne

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 tFirst(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

Parsirati “IF true THEN temp++ 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

FIRST(A) = { "a" , "(" , "[", "."}


FIRST(B) = { "a" , "(" , "[", }
FIRST(D) = { "+" , }
FIRST(C) = FIRST(B) U FIRST(D)

FOLLOW(D) = FOLLOW(C) = { ")" }


FOLLOW(B) = { "." , "]" , "+" , ")" }
FOLLOW(A) = {$}

43
Primjer 2 - nastavak

#include <stdio.h>
#include <stdlib.h>
char sym; // Source token
void getsym(void) {
sym = getchar();
}

44
Primjer 2 - nastavak

void accept(char expectedterminal, char


*errormessage) {
if (sym != expectedterminal) {
puts(errormessage);
exit(1);
}
getsym();
}

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,

– Odrediti First i Follow skupove


– Dodati printf u kod tako da se štampa koje je
pravilo primjenjeno
– Razmislite kako da obradite eventualne greške

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

• Eliminišemo lijevu rekurziju

56
Primjer 2

Gramatika za aritmetičke izraze


E -> TA
A -> +TA | 
T -> FB
B -> *FB | 
F -> (E) | id

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)

Tabela nerekurzivnog prediktivnog parsera

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

You might also like