Wykład 5
Transkrypt
Wykład 5
Wykład 5 Jan Pustelnik Konstruowanie parsera ● ● Istnieje kilka podstawowych metod konstrukcji parsera bez nawracania Ze względów wydajnościowych parser bez nawracania jest jedynym sensownym rozwiązaniem (prawo Moore'a jest w stanie przyspieszyć znacząco wyłącznie algorytmy O(n)) Konstruowanie c.d. ● Podstawowymi metodami konstruowania parserów bez nawracania są: – top-down (metoda zejścia rekursywnego, oznaczana często LL, szczególnie dobra dla ręcznego budowania parsera dla prostych języków, zasadna zwłaszcza tam, gdzie nie mamy możliwości skorzystania z yacca/bisona, tak np. zbudowany jest freepascal, gdzie source jest w pascalu) Konstruowanie c.d. – bottom-up – metoda wstępująca; zwykle używana jest któryś z wariantów ogólnej metody zwanej LR (L – czytaj od lewej do prawej, R – wywódź prawostronnie – czyli od terminali do symbolu głównego): ● ● ● SLR – simple LR (LR(0)) LALR – lookahead LR (LR(k), k>0, zwykle LR(1)) LR(k) – to ogólny symbol, k oznacza tutaj liczbę znaków lookahead, które są rozważane w czasie analizowania jaką ścieżką podążyć; k=0 bierze pod uwagę wyłącznie bieżący symbol, k=1 patrzy jeden znak naprzód Konstruowanie c.d. ● Wszystkie wspomniane metody należą do rodziny L – czytających tekst od lewej strony, w związku z czym konieczne w ich wypadku jest wykonanie dwóch operacji: usunięcia lewostronnej rekursji i wykonanie lewostronnej faktoryzacji (wyjęcia największego wspólnego czynnika przed nawias) Konstruowanie c.d. ● W metodzie zejścia rekursywnego naturalną postacią parsera jest program w postaci ciągu rekurencyjnych procedur, które wywołują same siebie lub inne procedury; schemat wzajemnych zależności odpowiada gramatycje języka Konstruowanie c.d. ● W wypadku metod LR (wstępujących) mamy do czynienia z tablicami ACTION i GOTO oraz akcjami SHIFT i REDUCE – – – – akcja SHIFT wkłada symbol na stos akcja REDUCE implementuje dokonanie “zwinięcia” kilku symboli prostszych w jeden symbol bardziej ogólny zgodnie z którąś z reguł gramatyki tablica ACTION decyduje, czy mamy wykonać SHIFT czy REDUCE tablica GOTO mówi do którego stanu mamy się udać Konstruowanie c.d. – tablice ACTION i GOTO konstruowane są zgodnie z określonymi regułami; najbardziej zgrubną regułą jest: preferuj REDUCE względem SHIFT (reguła zdroworozsądkowa – nie odkładaj rzeczy na później) – niestety reguła ta zawodzi w szczególnych przypadkach, dlatego dla rzeczywistego parsera potrzebne są jeszcze trzy narzędzia: ● ● ● zbiory FIRST zbiory FOLLOW zbiory konfiguracji Konstruowanie c.d. ● Dlatego na wstępie powiemy sobie w ogólności o metodach eliminacji rekursji i faktoryzacji a także o zbiorach FIRST, FOLLOW i zbiorach konfiguracji tak, by rzeczy nudne, ale potrzebne zmieściły się w ramach niniejszego wykładu; potem będziemy już tylko mówić o konkretnych metodach analizy składniowej Rekurencja lewostronna ● ● ● Gramatyka jest lewostronnie rekurencyjna, jeśli ma terminal A taki, że istnieje wyprowadzenie A⇒A dla pewnego napisu . Powoduje to następujący problem: jeżeli analizujemy tekst od lewej strony, wówczas nigdy nie dotrzemy do , ponieważ “ugrzęźniemy” w A Ceterum censeo rekurencja lewostronna delendam esse i jej eliminacja Przeanalizujmy przypadek szczególny (nieterminal bezpośrednio rekurencyjny): Zastępujemy produkcję o postaci: AA| Następującymi produkcjami: AA' A'A'| Zwróćmy uwagę na pojawienie się epsilon-produkcji! ● eliminacji c.d. Przeanalizujmy teraz następujący przykład gramatyki: SAa|b AAc|Sd| Nieterminal S jest teraz lewostronnie rekurencyjny, ale nie jest bezpośrednio lewostronnie rekurencyjny. Zatem musimy zastosować pewien algorytm, polegający na sukcesywnym rozwinięciu wszystkich produkcji tak, by ● eliminacji c.d. dowolna produkcja zawierała po prawej stronie wyłącznie terminale, nieterminale w pozycjach prawostronnie rekurencyjnych lub nieterminal znajdujący się po lewej stronie w pozycji lewostronnie rekurencyjnej, a następnie wykonaniu eliminacji bezpośredniej lewostronnej rekurencji z wszystkich produkcji eliminacji c.d. ● ● Warunkiem powodzenia algorytmu eliminacji jest brak pętli oraz brak epsilon-produkcji w startowej gramatyce; efektem ubocznym jest stworzenie epsilon produkcji. Zwykle algorytm ten omijamy po prostu pisząc gramatykę w taki sposób, by nie było lewostronnej rekurencji Faktoryzacja lewostronna Weźmy następujące dwie produkcje: instr if wyr then instr else instr | if wyr then instr Jeśli na wejściu widzimy symbol if, wówczas nie wiemy, którą produkcję wybrać, należy wyciągnąć pierwszą część “przed nawias”, otrzymując następujące produkcje: instr if wyr then instr część-else część-else else instr | ● faktoryzacja c.d. ● ● Standardowo postępujemy pisząc gramatykę tak, by od razu uwzględnić lewostronną faktoryzację Należy zauważyć, że algorytm eliminacji lewostronnej faktoryzacji może zostać łatwo zintegrowany z algorytmem tworzenia analizatora składniowego typu LR Zbiory FIRST i FOLLOW ● ● Zbiory FIRST i FOLLOW budowane są w trakcie konstrukcji algorytmu analizatora składniowego metodą LR Rozumienie ich konstrukcji jest konieczne dla analizy błędów w gramatyce raportowanych przez generator analizatorów składniowych yacc/bison FIRST ● Zbiór FIRST dla dowolnego ciągu symboli z gramatyki X jest zbiorem terminali, od których zaczynają się ciągi wyprowadzalne z X; jeżeli z X można wyprowadzić epsilon, to epsilon jest także w FIRST(X); należy zauważyć, że wszystkie zbiory FIRST są konstruowane jednocześnie dla całej gramatyki, dzięki czemu czas obliczania ich jest proporcjonalny do długości najdłuższego wyprowadzenia FIRST c.d. FIRST – ALGORYTM: 1. Jeśli X jest terminalem, to FIRST(X) jest równe {X} 2. Jeśli mamy epsilon produkcję dla X, to dodajemy epsilon do FIRST(X) 3. Jeśli X jest nieterminalem, wówczas dla wszystkich produkcji postaci: X Y1Y2Y3... Należy wykonać następujący algorytm: a) dodaj do FIRST(X) zbiór FIRST(Y1) b) jeśli FIRST(Y1) zawiera epsilon, wówczas dodaj do FIRST(X) zbiór FIRST(Y2) c) jeśli FIRST(Y2) zawiera epsilon... itd. FIRST c.d. d) jeśli wszystkie FIRST(Yj) zawierają epsilon, wówczas FIRST(X) też będzie zawierał epsilon FOLLOW ● Zbiory FOLLOW obliczamy zgodnie z następującym algorytmem: 1) dla symbolu startowego S w FOLLOW(S) umieszczamy $ 2) dla każdej produkcji A B, gdy FIRST() nie zawiera , wszystkie symbole z FIRST() umieszczamy w FOLLOW(B) 3) jeżeli FIRST() zawiera , lub mamy do czynienia z produkcją A B, wówczas do FOLLOW(B) dodajemy FOLLOW(A) Prosty przykład analizatora LL ● Teraz podamy prosty przykład analizatora LL, żeby już dalej nie zajmować się tym tematem; dla nas ważne są analizatory LR, które są generowane automatycznie przez odpowiedni generator analizatorów składniowych Prosty przykład c.d. ● Analizatory LL zasadniczo nadają się wyłącznie do generowania ręcznego i jako takie sprawdzają się w wypadkach prostych języków (kalkulator) lub gdy do dyspozycji nie mamy odpowiednich narzędzi (Pascal), a dodatkowo zależy nam na małym kodzie wykonywalnym (tablice LR potrafią być duże, dla ośmiobitowców LL było jedynym wyjściem)