Języki formalne i automaty Ćwiczenia 3 Autor: Marcin Orchel Spis treści Spis treści... 1 Wstęp teoretyczny... 2 Algorytm LL(1)... 2 Definicja zbiorów FIRST1 i FOLLOW1... 3 Konstrukcja tabeli parsowania algorytmu LL(1)... 4 Algorytm sprawdzania konkretnych słów.... 5 Zadania... 8 Zadania na 3.0... 8 Zadania na 4.0... 8 Zadania na 5.0... 8
Wstęp teoretyczny Wyprowadzenie lewostronne to takie wyprowadzenie, w którym za każdym razem zastępujemy pierwszy od lewej nieterminal. Przykład: Gramatyka S F S asbfc F d Wyprowadzenie lewostronne słowa: adbdc. S asbfc afbfc adbfc adbdc Algorytm LL(1) Algorytm LL(1) służy do sprawdzenia czy dane słowo należy do języka generowanego przez gramatykę LL(1). Algorytm działa w czasie liniowym. Gramatyka LL to taka, w której czytamy słowo podczas sprawdzania od lewej do prawej, oraz taka, że możemy podjąć podczas wyprowadzenia lewostronnego jednoznaczną decyzję, którą produkcje wybrać. Gramatyka LL(1) to gramatyka LL taka, że podczas podejmowania decyzji, którą produkcję wybrać analizujemy dokładnie jeden symbol słowa. Algorytm LL(1) będzie brany pod uwagę tylko dla gramatyk bezkontekstowych, można zauważyć, że z tego względu, jeśli istnieje dowolne wyprowadzenie danego słowa, to zawsze istnieje wyprowadzenie lewostronne tego słowa. Przykład gramatyki, która nie jest typu LL(1). S blc a L LdS S Przykładowe wyprowadzenie dla słowa baadac. S blc Po pierwszym kroku mamy zgodność symbolu pierwszego słowa, następnie mamy znaleźć taki ciąg produkcji aby otrzymać zgodność dla drugiego symbolu słowa. Tutaj są dwie możliwości: S blc bsc bac oraz S blc bldsc bsdsc basdsc Widzimy, że w obu wyprowadzeniach otrzymaliśmy zgodność drugiego symbolu słowa (a). Dlatego, że istnieje więcej niż jedno wyprowadzenie dla drugiego symbolu słowa gramatyka ta nie jest typu LL(1). Zauważmy ponadto, że słowo bac i tak byłoby odrzucone ponieważ trzeci symbol jest inny, ale gramatyka jest typu 1, a więc nie możemy korzystać w wyprowadzeniu z informacji o kolejnych symbolach słowa, a jedynie o aktualnie rozpatrywanym.
Przykład 2 S blc a L am S M ε Przykładowe wyprowadzenia dla słowa bac: S blc bamc oraz S blc bsc bac Gramatyka nie jest typu LL(1) ponieważ otrzymaliśmy zgodność drugiego symbolu słowa dwoma różnymi lewostronnymi wyprowadzeniami. Definicja zbiorów FIRST 1 i FOLLOW 1 Zbiory te będą zdefiniowane dla gramatyk bezkontekstowych. α jest pojedynczym symbolem terminalnym lub nieterminalnym. Zbiór FIRST 1 (α) jest zbiorem wszystkich terminalnych przedrostków długości 1 łańcuchów, które mogą być wyprowadzone z α oraz słowa pustego, jeśli może być wyprowadzone z α. Zbiór FOLLOW 1 (α) jest zbiorem wszystkich terminalnych łańcuchów długości 1, które występują tuż za symbolem α w dowolnych wyprowadzeniach. Algorytmy wyznaczenia zbiorów FIRST i FOLLOW. Wyznaczenie zbioru FIRST 1 (α): 1. Jeśli α jest terminalem to FIRST 1 (α) = {α}. 2. Jeśli istnieje produkcja postaci: α ε to FIRST 1 (α) = FIRST 1 (α) {ε} 3. Jeśli α jest nieterminalem to dla każdej produkcji postaci α y 1 y 2 y k : FIRST 1 (α) = FIRST 1 (α) FIRST 1 (y 1 ) Od i = 2 do k Jeśli ε FIRST 1 (y 1 ) FIRST 1 (y i-1 ) to FIRST 1 (α) = FIRST 1 (α) FIRST 1 (y i ) Jeśli ε FIRST 1 (y 1 ) ε FIRST 1 (y k ) to FIRST 1 (α) = FIRST 1 (α) {ε} Wyznaczenie FIRST 1 (x 1 x 2 x n )gdzie x i są terminalami lub nieterminalami przebiega następująco: 1. FIRST 1 (x 1 x 2 x n ) = FIRST 1 (x 1 ) \ {ε} 2. Od i = 2 do n Jeśli ε FIRST 1 (x 1 ) FIRST 1 (x i-1 ) to FIRST 1 (x 1 x 2 x n ) = FIRST 1 (x 1 x 2 x n ) FIRST 1 (x i ) \ {ε}
Jeśli ε FIRST 1 (x 1 ) ε FIRST 1 (x n ) to FIRST 1 (x 1 x 2 x n ) = FIRST 1 (x 1 x 2 x n ) {ε} Wyznaczenie zbioru FOLLOW 1 (B), gdzie B jest nieterminalem: 1. Jeśli B = S to FOLLOW 1 (B) = FOLLOW 1 (B) {$} 3. Dla każdej produkcji postaci A αbβ: FOLLOW 1 (B) = FOLLOW 1 (B) FIRST 1 (β) \ {ε} 4. Dla każdej produkcji postaci A αb lub (A αbβ ε FIRST 1 (β)): FOLLOW 1 (B) = FOLLOW 1 (B) FOLLOW 1 (A) Zauważmy, że zbiór FOLLOW nie zawiera symbolu ε. Przykład: 1. S aabb 2. A aac 3. A ε 4. B bb 5. B c Wyznaczmy przykładowo: FIRST 1 (A) - w związku z produkcją 3 FIRST 1 (A) = {ε}, w związku z produkcją 2 FIRST 1 (A) = {a, ε}. FIRST 1 (aac) w związku z regułą 1 FIRST 1 (aac) = {a} FIRST 1 (B) w związku z produkcją 4 FIRST 1 (B) = {b}, w związku z produkcją 5 FIRST 1 (B) = {b, c} FOLLOW 1 (A) w związku z produkcją 1 FOLLOW 1 (A) = {b, c}, produkcja 2 nie wnosi nic nowego. Dla potrzeb algorytmu tworzymy tabelkę ze zbiorami FIRST i FOLLOW dla każdego nieterminala. FIRST FOLLOW A {ε, a} {b, c} B {b, c} {b} S {a} {$} Konstrukcja tabeli parsowania algorytmu LL(1) Algorytm rozpoznawania czy dane słowo należy do języka i do wyznaczenia wyprowadzenia wykorzystuje następującą tabelę: wiersze odpowiadają nieterminalom, kolumny odpowiadają terminalom i symbolowi $, w komórkach tabeli znajdują się prawe strony odpowiednich produkcji. Algorytm wypełniania tabeli: 1. Dla każdej produkcji wyznacz FIRST(p), gdzie p to prawa strona produkcji. Zapisz prawą stronę
produkcji w komórkach (A, a) dla każdego a, gdzie A to lewa strona produkcji, a to terminal znajdujący się w zbiorze FIRST(P). 2. Dla każdej produkcji, jeśli ε FIRST(p) to zapisz prawą stronę produkcji w komórkach (A, a) dla każdego a, gdzie A to lewa strona produkcji, a to terminal lub symbol $ znajdujący się w zbiorze FOLLOW(A). Jeśli w danej komórce należy wpisać prawą stronę więcej niż jednej produkcji, to gramatyka nie jest typu LL(1). Przykład dla gramatyki z poprzedniego zadania: a b c $ A aac ε ε B bb c S aabb Dla produkcji 1: FIRST(aABb) wynosi {a}, a więc do komórki (S, a) wpisujemy aabb. Dla produkcji 2: FIRST(aAc) wynosi {a}, a więc do komórki (A, a) wpisujemy aac. Dla produkcji 3: FIRST(ε) wynosi {ε}. Dla produkcji 4: FIRST(bB) wynosi {b}, a więc do komórki (B, b) wpisujemy bb. Dla produkcji 5: FIRST(c) wynosi {c}, a więc do komórki (B, c) wpisujemy c. Dla produkcji 3 w FIRST(ε) znajduje się ε. A więc zapisujemy do komórek (A, b) i (A, c) ε. Algorytm sprawdzania konkretnych słów. Mamy pewne słowo do sprawdzenia czy należy do gramatyki. W każdym krok aktualizujemy bufor wejściowy w którym znajduje się słowo oraz znak $ na końcu, oraz aktualizujemy stos, na którym na początku znajduje się symbol S. Algorytm sprawdzenia polega na zdejmowaniu ze stosu kolejnych symboli, jeśli jest to symbol terminalny to sprawdzamy czy jest identyczny z pierwszym symbolem z buforu wejściowego, jeśli jest to usuwamy go również z buforu wejściowego. Jeśli jest to symbol nieterminalny to zastępujemy go odpowiednią produkcją i umieszczamy prawą stronę produkcji na stosie. Jeśli w buforze wejściowym pozostał tylko symbol $, wtedy możemy zastosować również produkcje z kolumny $. Algorytm zatrzymuje się w następujących stanach: w buforze wejściowym pozostał $, a stos jest pusty, wtedy i tylko wtedy słowo należy do danego języka. terminal na stosie oraz w buforze wejściowym nie są zgodne, lub nie można już zastosować żadnych produkcji, wtedy i tylko wtedy słowo nie należy do danego języka Przykładowo dla podanej wcześniej gramatyki sprawdźmy słowo: aacbbcb 1. Bufor wejściowy: aacbbcb$ Stos: S
2. Zdejmujemy S, zastępujemy łańcuchem znajdującym się w komórce (S, a) Bufor wejściowy: aacbbcb$ Stos: aabb 3. Zdejmujemy a, w słowie wejściowym również jest a, a więc: Bufor wejściowy: acbbcb$ Stos: ABb 4. Zdejmujemy A, zastępujemy komórką (A, a) Bufor wejściowy: acbbcb$ Stos: aacbb 5. Zdejmujemy a, w słowie wejściowym również jest a, a więc: Bufor wejściowy: cbbcb$ Stos: AcBb 6. Zdejmujemy A, zastępujemy komórką (A, c) Bufor wejściowy: cbbcb$ Stos: cbb 7. Zdejmujemy c Bufor wejściowy: bbcb$ Stos: Bb 8. Zdejmujemy B i zastępujemy komórką (B, b) Bufor wejściowy: bbcb$ Stos: bbb 9. Zdejmujemy b Bufor wejściowy: bcb$ Stos: Bb 10. Zdejmujemy B i zastępujemy (B, b) Bufor wejściowy: bcb$ Stos: bbb 11. Zdejmujemy b Bufor wejściowy: cb$ Stos: Bb 12. Zdejmujemy B i zastępujemy (B, c) Bufor wejściowy: cb$ Stos: cb
13. Zdejmujemy c. Bufor wejściowy: b$ Stos: b 14. Zdejmujemy b. Bufor wejściowy: $ Stos: Jako, że stos jest pusty a wejście równe $, słowo należy do języka. Wyprowadzenie tego słowa wygląda następująco: S aabb aaacbb aacbb aacbbb aacbbbb aacbbcb Inne słowo: cbcbcb 1. Bufor wejściowy: cbcbcb $ Stos: S 2. Zdejmujemy S, zastępujemy łańcuchem znajdującym się w komórce (S, a) Bufor wejściowy: cbcbcb$ Stos: aabb Zdejmujemy ze stosu symbol a, pierwszy symbol w wejściu jest inny, a mianowicie c, a więc dane słowo nie należy do tego języka. Inne słowo: aab 1. Bufor wejściowy: aba $ Stos: S 2. Zdejmujemy S, zastępujemy łańcuchem znajdującym się w komórce (S, a) Bufor wejściowy: aba$ Stos: aabb 3. Zdejmujemy a Bufor wejściowy: ba$ Stos: ABb 4. Zdejmujemy A i zastępujemy b Bufor wejściowy: ba$ Stos: bbb 5. Zdejmujemy b Bufor wejściowy: a$ Stos: Bb 6. Zdejmujemy B, niestety nie jest możliwe zastąpienie B, ponieważ komórka (B, a) jest pusta, a więc słowo nie należy do tego języka
Zadania Zadania na 3.0 Wykonać algorytm LL(1) dla następujących gramatyk: S ads S b D a D bsd Sprawdzić słowa: aaab, oraz aab. oraz S ab A bc B A B ε C c C d Sprawdzić słowo abc. Czy powyższe gramatyki są typu LL(1)? Zadania na 4.0 Sprawdzić na podstawie tabeli parsowania czy poniższa gramatyka jest typu LL(1). S DaS S b D a D bsd Zadania na 5.0 Implementacja algorytmu LL(1) w Javie. Porównanie wyników z programem JFLAP dla gramatyk z zadań na 3.0 i 4.0.