Języki formalne i automaty Ćwiczenia 4 Autor: Marcin Orchel Spis treści Spis treści... 1 Wstęp teoretyczny... 2 Sposób tworzenia deterministycznego automatu skończonego... 4 Intuicyjne rozumienie konstrukcji automatu.... 4 Tworzenie tabeli algorytmu SLR(1)... 4 Sprawdzanie czy dane słowo należy do języka gramatyki SLR(1)... 5 Przykład... 5 Zadania... 9 Zadania na 3.0... 9 Zadania na 4.0... 9 Zadania na 5.0... 9
Wstęp teoretyczny Algorytm SLR(1) służy do sprawdzenia czy dane słowo należy do języka generowanego przez gramatykę SLR(1). Algorytm SLR(1) jest typu bottom-up. Będziemy rozpatrywać wyłącznie gramatyki SLR(1) będące gramatykami bezkontekstowymi. Gramatyka SLR(1) jest szczególnym typem gramatyki LR(1). Gramatyka LR(1) jest podobna do gramatyki LL(1). Jedyną różnicą jest to, że stosujemy wyprowadzenie prawostronne, a więc za każdym razem zastępujemy pierwszy od prawej nieterminal. Gramatyka SLR(1) jest to taka gramatyka LR(1) dla której istnieje algorytm SLR(1) rozpoznający słowa tego języka. Porównanie wyprowadzenia lewostronnego i prawostronnego: Przykład: S aabcb A a B b C c Wyprowadzamy słowo: aabcb. 1. S aabcb aabcb aabcb aabcb 2. S aabcb aabcb aabcb aabcb Pierwsze wyprowadzenie jest lewostronne, drugie prawostronne. Algorytm LL jest typu top-down, algorytm SLR jest typu bottom-up. Algorytm LL działa w ten sposób, że zaczynamy od symbolu startowego S i stosujemy kolejne produkcje tak aby uzyskiwać zgodność na kolejnych symbolach tego słowa począwszy od najbardziej lewego. Algorytm SLR stosuje wyprowadzenie prawostronne, a więc nie może być typu top-down ponieważ wtenczas nie moglibyśmy walidować kolejnych symboli słowa począwszy od najbardziej lewego. W algorytmie SLR wyprowadzenie jest typu bottom-up, a więc wygląda następująco: aabcb aabcb aabcb aabcb S. (1) Jest to algorytm typu bottom-up, a więc w każdym kroku stosujemy tzw. redukcje, to znaczy zastępujemy prawe części odpowiednich produkcji. W związku z tym, że jest to wyprowadzenie prawostronne (jeśli rozpatrujemy je od symbolu startowego S), to jeśli w danym łańcuchu występuje więcej niż jeden nieterminal to musiały być one wyprowadzone w algorytmie SLR w kolejności od lewej do prawej. W wyprowadzeniu (1) dla łańcucha aabcb najpierw wyprowadzone było A, a później B. Rozpatrzmy bliżej to wyprowadzenie: Jeśli wprowadzimy pojęcie stosu to wyprowadzenie powyższe można zamodelować następująco: Bufor wejściowy: aabcb$. Stos: pusty
Bufor wejściowy: abcb$. Stos: a Bufor wejściowy: bcb$. Stos: aa Bufor wejściowy: bcb$. Stos: aa Bufor wejściowy: cb$. Stos: aab Bufor wejściowy: cb$. Stos: aab Bufor wejściowy: b$. Stos: aabc Bufor wejściowy: b$. Stos: aabc Bufor wejściowy: $. Stos: aabcb Bufor wejściowy: $. Stos: S W każdym kroku aktualny łańcuch wyprowadzenia to stos + bufor wejściowy. Podział na stos i bufor wejściowy jest po to, aby ułatwić wyprowadzenie prawostronne. Powyższy proces można przedstawić również następująco: ^aabcb a^abcb aa^bcb aa^bcb aab^cb aab^cb aabc^b aabc^b aabcb^ S. Jak widzimy w wyprowadzeniu są dwie możliwe operacje: przemieszczenie terminala z bufora wejściowego na stos, redukcja na stosie. Po usunięciu z powyższego wyprowadzenia operacji przemieszczania terminala otrzymujemy właściwe wyprowadzenie. Algorytm SLR(1) pomaga stwierdzić jakie operacje należy zastosować w poszczególnych krokach wyprowadzenia. Schemat algorytmu SLR(1) wygląda następująco: 1. Dodajemy produkcje S S do zbioru produkcji, symbol startowy jest S. 2. Konstruujemy zbiory FIRST i FOLLOW podobnie jak w algorytmie LL(1).
3. Tworzymy deterministyczny skończony automat dla podanej gramatyki. 4. Tworzymy na jego podstawie tabelę algorytmu SLR(1). 5. Sprawdzamy czy podane słowo należy do języka. Sposób tworzenia deterministycznego automatu skończonego Automat skończony to pewien model zachowania składający się ze skończonej liczby stanów, tranzycji pomiędzy stanami i z akcji. Deterministyczny automat skończony to taki automat skończony, że dla każdej pary (stan, symbol wejściowy) jest dokładnie jedna tranzycja do następnego stanu. Oznaczona produkcja to taka, w której pojawia się kropka po prawej stronie produkcji. Kropka ta oznacza, że wszystkie symbole po jej lewej zostały umieszczone na stosie, a po prawej jeszcze nie. W każdym zbiorze oznaczonych produkcji jest jedna wyróżniona główna produkcja oznaczona. Konstrukcja stanów. Zaczynamy od stanu q0, dla którego główną produkcją oznaczoną jest S.S. Od każdego stanu przechodzimy do kolejnego stanu lub tego samego wybierając jedną z produkcji oznaczonych, ale taką, że kropka nie jest na końcu produkcji. Tranzycji odpowiada symbol występujący tuż po kropce. W nowo powstałym stanie główną produkcją oznaczoną będzie ta, z której powstał ten stan, ale z przesuniętą kropką o jeden symbol w prawo. Dla każdego stanu tuż po dodaniu głównej produkcji oznaczonej dodajemy pozostałe produkcje oznaczone postaci w.x. Są to wszystkie produkcje z w po lewej stronie wraz z dodaną kropką na początku, gdzie w to pierwszy po kropce nieterminal występujący w głównej produkcji oznaczonej. Stany, w których występuje co najmniej jedna oznaczona produkcja, taka, że kropka jest na końcu, są stanami końcowymi i są oznaczane podwójnym okręgiem. Intuicyjne rozumienie konstrukcji automatu. Konstrukcja automatu to przejście przez możliwe wyprowadzenia, ale z drugiej strony, a więc zaczynając od ostatniej produkcji jaką będziemy musieli zastosować dla naszego słowa w wyprowadzeniu SLR(1). Po prawej stronie kropki produkcji oznaczonej znajduje się łańcuch, który chcemy wyprowadzić z aktualnego bufora wejściowego, a po lewej stronie znajduje się stos, w którym jest zapisany łańcuch, dla którego chcemy zastosować redukcje. Tranzycja z symbolem terminalnym oznacza umieszczenie na stosie symbolu terminalnego w celu zastosowania redukcji, ale równocześnie symbol terminalny jest zdejmowany z bufora wejściowego. Tranzycja z symbolem nieterminalnym odpowiada przetworzeniu sybolu nieterminalnego, który pojawił się wcześniej na stosie po redukcji. Zaczynamy konstrukcję od stanu q0, dodajemy do niego główną produkcję oznaczoną: S.S. Tworzenie tabeli algorytmu SLR(1) Na podstawie stworzonego automatu konstruujemy tabelę algorytmu SLR(1). Tabela ma wiersze, które odpowiadają wszystkim stanom automatu, a kolumny odpowiadają wszystkim terminalom, nieterminalom oraz znakowi $. Tabelę wypełniamy począwszy od wiersza 1. Dla każdej tranzycji konstruujemy odpowiedni wpis w tabeli. Jeśli w tranzycji jest nieterminal to wpisujemy: (w, N) := i, gdzie i to stan do którego dochodzimy za pomocą tej tranzycji.
Jeśli w tranzycji jest terminal to wpisujemy: (w, t) := si, gdzie s oznacza operację shift, a i to stan, do którego dochodzimy za pomocą tej tranzycji. Jeśli analizując tranzycje trzeba będzie wypełnić komórkę (i, S) to wpisujemy do niej acc, co oznacza accepted. Jeśli analizowany stan jest stanem końcowym to wpisujemy do komórek (i, a) := rj, gdzie i to analizowany stan, a to każdy nieterminal ze zbioru FOLLOW(A), a j to numer produkcji. Jest to operacja redukcji. Sprawdzanie czy dane słowo należy do języka gramatyki SLR(1) Sprawdzanie czy dane słowo należy do języka wygląda następująco: W każdym kroku mamy bufor wejściowy w którym na początku znajduje się analizowane słowo zakończone $, oraz mamy stos na którym na początku znajduje się stan 0. Wybieramy produkcję z tabeli algorytmu SLR(1) z komórki (i, a), gdzie i to aktualny stan na stosie, a a to terminal na początku buforu wejściowego. Terminal ten jest usuwany z bufora wejściowego, a na stos jest dopisywany ten terminal wraz z nowym stanem. Gdy pojawi się w komórce redukcja, wtedy usuwamy ze stosu odpowiednią część zgodnie z redukcją i sprawdzamy następnie komórkę (i, A), gdzie i to aktualny stan a A to lewa strona produkcji związanej z redukcją. Jeśli otrzymamy na samym końcu accepted oraz w buforze wejściowym tylko $ to dane słowo należy do rozpatrywanego języka. Przykład 0. S aabb 1. A aac 2. A ε 3. B bb 4. B c Na początku dodajemy produkcje S S, od tej pory symbolem startowym jest S. 0. S S 1. S aabb 2. A aac 3. A ε 4. B bb 5. B c Podobnie jak dla parsera LL(1) wyznaczamy tabelę zbiorów FIRST i FOLLOW: FIRST FOLLOW A {ε, a} {b, c} B {b, c} {b} S {a} {$} Następnie budujemy deterministyczny automat skończony, który ma postać końcową:
Przykładowe kroki w konstrukcji automatu: Zaczynamy od stanu q0. Należy do niego główna produkcja oznaczona S.S, oraz wszystkie produkcje oznaczone, które po lewej stronie mają symbol występujący po kropce czyli S i mają na początku kropkę. Jest taka jedna produkcja oznaczona: S aabb. Rozważając produkcję oznaczoną S.S konstruujemy tranzycję dla S, przechodzimy do nowego stanu q1, do którego dołączamy tą produkcję tylko z przesuniętą kropką o jeden symbol S S.. Kropka jest na końcu więc jest to stan końcowy. Pójdźmy następnie inną drogą wybierzmy w stanie q0 terminal a. Tworzymy stan q2, w którym zapisujemy produkcję oznaczoną S a.abb, oraz wszystkie produkcje oznaczone z A po lewej stronie i kropce na początku prawej strony: A aac i A.. Jest to stan końcowy ponieważ znajduje się w nim produkcja oznaczona z kropką na końcu. W stanie q2 na stosie znajduje się symbol a. Następnie możemy wybrać terminal a z produkcji oznaczonej A.aAc, wtenczas otrzymamy nowy stan q4 z produkcjami A a.ac oraz A.aAc, A., itd. Intuicyjnie zaczynamy od stanu q0. i głównej produkcji oznaczonej S.S. Oznacza ona, że aktualne słowo z bufora wejściowego chcemy wyprowadzić z terminala S, stos jest pusty. W związku z tym umieszczamy S na stosie i przechodzimy do stanu q1, w którym główną produkcją jest S S.. W tym stanie mamy na stosie S, i nie ma potrzeby porównania z buforem wejściowym. Jeśli w tym stanie bufor wejściowy jest pusty to otrzymaliśmy stan accepted, który kończy wyprowadzenie. Zamiast do stanu q1 możemy również przejść do stanu q2, ponieważ S może zostać zastąpione produkcją 1. Wtedy umieszczamy na stosie terminal a, równocześnie zdejmując go z bufora wejściowego, itd. Następnie konstruujemy tabelę algorytmu SLR(1), która wygląda następująco: a b c $ A B S 0 s2 1
1 acc 2 s4 r3 r3 3 3 s6 s7 5 4 s4 r3 r3 8 5 s9 6 s6 s7 10 7 r5 8 s11 9 r1 10 r4 11 r2 r2 Przykładowe wyprowadzenie. Zaczynamy od stanu q0. Gdy zastosujemy tranzycję S idziemy do stanu q1, a więc wpisujemy w [0, S] = 1. Gdy wybierzemy tranzycję a, idziemy do stanu q2, a więc wpisujemy s2. q1 to stan końcowy do którego prowadzi tranzycja S, a więc wpisujemy w nim acc. Następnie analizujemy stan q2. Jeśli w stanie q2 wybierzemy a, to idziemy do stanu q4, a więc [2, a] = s4. Jeśli natomiast wybierzemy A to idziemy do stanu 3, [2, A] = 3. Jest to stan końcowy ze względu na produkcję A., a zatem sprawdzamy jaki jest zbiór FOLLOW(A), i wpisujemy: [2, b] = r3, [2, c]=r4, itd. Sprawdźmy słowo aacbbcb: Góra stosu znajduje się na początku. Bufor wejściowy aacbbcb$. Stos: 0. Sprawdzamy komórkę tabeli [0, a]. Jest w niej s2, a zatem, s oznacza, że a zdejmujemy z bufora wejściowego, kładziemy na stos, i kładziemy 2: Bufor wejściowy acbbcb$. Stos: 2a0. Sprawdzamy komórkę: [2, a]. Jest w niej s4 a zatem: Bufor wejściowy cbbcb$. Stos: 4a2a0. Sprawdzamy komórkę [4, c]. Jest w niej r3, a zatem stosujemy produkcję nr. 3 dla stosu począwszy od lewej, w produkcji mamy produkcję pustą, a więc: Bufor wejściowy cbbcb$. Stos: A4a2a0. Następnie sprawdzamy komórkę: [4, A] w której jest 8. A więc kładziemy 8 na stos: Bufor wejściowy cbbcb$. Stos: 8A4a2a0. I dalej analogicznie sprawdzamy komórkę [8, c] w której jest s11. A więc: Bufor wejściowy bbcb$. Stos: 11c8A4a2a0. Dalej sprawdzamy komórkę [11, b] w której jest r2, a więc redukujemy stos za pomocą produkcji nr. 2: Bufor wejściowy bbcb$. Stos: A2a0. Następnie sprawdzamy komórkę [2, A], w której jest 3, a więc kładziemy 3 na stos: Bufor wejściowy bbcb$.
Stos: 3A2a0, itd. Odtworzenie stosowanych produkcji: Zdjęcie ze stosu terminala oznacza przesunięcie w łańcuchu aktualnego wskaźnika. Wyprowadzenie pośrednie wygląda następująco: ^aacbbcb a^acbbcb aa^cbbcb aaa^cbbcb aaac^bbcb a^bbcb Po lewej stronie wskaźnika jest stos, a po prawej stronie wskaźnika jest bufor wejściowy. Końcowe wyprowadzenie otrzymamy usuwając z niego wyprowadzenia w których przesuwamy tylko wskaźnik: aacbbcb aaacbbcb abbcb.
Zadania Zadania na 3.0 Skontruować algorytm SLR(1) dla następującej gramatyki: S SaT S T T TbF T F F cfd F e Czy podana gramatyka jest typu SLR(1)? Wykonać parsowanie dla słowa: eaebe. Na podstawie parsowania zapisać wyprowadzenie dla tego słowa. Zadania na 4.0 Skontruować algorytm SLR(1) dla następującej gramatyki: S SaS S SbS S csd S e Czy podana gramatyka jest typu SLR(1)? Wykonać parsowanie dla słowa: eaebe. Na podstawie parsowania zapisać wyprowadzenie dla tego słowa. Zadania na 5.0 Implementacja algorytmu SLR(1) w Javie. Porównanie wyników z programem JFLAP dla gramatyk z zadań na 3.0 i 4.0.