KONWERTER WYRAŻENIA REGULARNE AUTOMAT GRAMATYKA Teoria kompilacji i kompilatory Tomasz Raus Jacek Boruch
SPIS TREŚCI I Wprowadzenie do tematu... 3 Wstęp... 3 Założenia projektu... 3 II Wstęp teoretyczny... 6 Konwersja Z Wyrażeń Regularnych na Automat... 6 Konwersja Z Automatu na Wyrażenia Regularne... 8 Konwersja Automatu na Gramatykę Regularną... 10 Konwersja Gramatyki Regularnej na Automat... 10 Konwersja Niedeterministycznego na Deterministyczny Automat Skończony... 10 Minimalizacja Deterministycznego Automatu Skończonego... 13 III Implementacja... 15 Wykorzystane technologie... 15 Przykładowe fragmenty kodu z implementacji algorytmów... 17 Klient... 20 IV Działanie aplikacji... 22 Obsługa i wprowadzanie danych... 22 Wyniki... 26 V Podsumowanie... 32 Ogólne wnioski... 32 Napotkane problemy... 32 Możliwe przyszłe prace... 32 VI Literatura... 33 2
I WPROWADZENIE DO TEMATU WSTĘP Języki regularne, podobnie jak zbiory regularne, mają ogromne znaczenie praktyczne. Są one bowiem wykorzystywane do definiowania języków programowania, do formalizacji pojęcia analizy syntaktycznej, do upraszczania translacji języków programowania, oraz przy innych zastosowania z dziedziny przetwarzania łańcuchów. I tak na przykład gramatyki bezkontekstowe są używane przy opisywaniu wyrażeń arytmetycznych z dowolnie zagnieżdżonymi zrównoważonymi nawiasami oraz struktur blokowych w językach programowania (wyznaczonych przez słowa kluczowe begin i end, dopasowane jak nawiasy). Języki regularne mogą być przedstawione w różnej postaci, na przykład za pomocą gramatyki regularnej, automatu skończonego lub wyrażenia regularnego. Wszystkie tego postacie są sobie równoważne. Często zachodzi potrzeba przekształcenia jednej z nich na inną. W takim przypadku istnieją różne algorytmy, które opisują w jaki sposób tego dokonać. Warto jednak jest mieć w posiadaniu narzędzie, które wykona to automatycznie. Jego realizację podjęliśmy w tym projekcie. ZAŁOŻENIA PROJEKTU Naszym celem jest zbudowanie konwertera w oparciu o istniejące już algorytmy, który pozwoli na szybką i prostą konwersję z różnych postaci przedstawienia języka formalnego, takich jak: wyrażenia regularne, gramatyka regularna, automat skończony. Na początku trzeba się przyjrzeć dostępnym algorytmom i zaprojektować w jaki sposób nasz konwerter miałby działać. 3
Na powyższym schemacie przedstawiony jest proces konwersji. Wynika z niego, że trzeba by było wykorzystać 6 różnych algorytmów. Zamiast tego można ograniczyć ich liczbę przez konwersję na postać pośrednią. Na przykład przy konwersji z wyrażeń regularnych do gramatyki regularnej można wykorzystać postać automatu skończonego, na który najpierw zamienić z wyrażeń regularnych a następnie na gramatykę regularną. Przestawia to poniższy schemat: 4
Za pomocą takiego rozwiązania ograniczyliśmy liczbę algorytmów, którą wykorzystamy w naszym projekcie do 4. Kolejnym założeniem jest stworzenie przyjaznego dla użytkownika klienta, który umożliwi obsługę naszego narzędzia bez potrzeby wdrożenia w różne sposoby formatowania i przedstawiania automatów i gramatyk (np. json, xml). Użytkownik powinien móc utworzyć automat i gramatykę korzystając z odpowiednich narzędzi interfejsu graficznego. 5
II WSTĘP TEORETYCZNY KONWERSJA Z WYRAŻEŃ REGULARNYCH NA AUTOMAT Języki akceptowane przez automaty skończone są dokładnie językami opisanymi przez wyrażenia regularne. Na mocy tego twierdzenia na podstawie wyrażenia regularnego można skonstruować automat skończony. Poniżej przedstawione są reguły konstrukcji niedeterministycznego automatu skończonego na podstawie poszczególnych przypadków występujących w wyrażeniach regularnych: Automat dla pustego języka, nie zawierającego żadnego słowa; L( ) = Automat dla języka akceptującego tylko słowo puste; L(ε) = {ε} Automat dla języka akceptującego tylko jeden symbol; L(a) = {a} 6
Automat dla konkatenacji dwóch wyrażeń regularnych: α β Automat dla sumy dwóch wyrażeń regularnych: α + β Automat dla domknięcia Kleene'ego wyrażenia regularnego: α 7
KONWERSJA Z AUTOMATU NA WYRAŻENIA REGULARNE W przypadku konwersji automatu na wyrażenia regularne dostępnych jest kilka metod, które pozwalają to osiągnąć. Metoda usuwania stanów W tej metodzie rozpatrujemy krawędzie automatu jako wyrażenia regularne i usuwamy pośrednie stany utrzymując wartości na krawędziach zgodne. Główny wzorzec tej metody można zaobserwować z poniższego przykładu. Pierwszy graf posiada stany p, q, r, pomiędzy którymi występują wyrażenia regularne e, f, g, h, i. Chcemy usunąć stan q. Po jego usunięciu trzeba złożyć e, f, g, h, i razem: Niestety ta metoda nie za bardzo nadaje się do implementacji jako algorytm ze względu na dużą ilość przypadków. Wykorzystuje się ją raczej do ręcznego rozwiązywania tego typu zadań. 8
Metoda domknięcia przechodniego Metody tej zwykle nie używa się do ręcznego przekształcania gdyż jest za bardzo semantyczna, jednak dobrze nadaje się do implementacji w formie algorytmu ze względu jej względną prostotę, dlatego właśnie ją wybraliśmy. Niech R k i,j reprezentuje wyrażenie regularne dla łańcuchów przebiegających od q i do q j używając stanów {q j,, q k }. Niech n będzie liczbą stanów automatu. Zakłada się że znane jest już wyrażenie regularne R i,j od q i do q j bez stanu pośredniego q k dla każdego i, j. Wtedy można przewidzieć w jaki sposób dodanie innego stanu wpłynie na nowe wyrażenie regularne R i,j : zmienia się on tylko wtedy gdy istnieje bezpośrednie przejście do stanu q k i może być wyrażone w poniższy sposób: R i,j = R i,j + R i,k. R k,k. R k,j Algorytm w pseudokodzie: Inicjalizacja: Domknięcie przechodnie: Ostateczne wyrażenie (zakładając, że qs jest stanem początkowym): 9
KONWERSJA AUTOMATU NA GRAMATYKĘ REGULARNĄ 1. Zmień nazwy stanów na wielkie litery. 2. Symbol startowy jest stanem startowym w NAS. 3. Dla każdego przejścia I -> J oznaczone przez a, utwórz produkcję I -> aj. 4. Dla każdego przejścia I -> J oznaczone przez ε, utwórz produkcję I -> J. 5. Dla każdego końcowego stanu K, utwórz produkcję K -> ε. KONWERSJA GRAMATYKI REGULARNEJ NA AUTOMAT 1. Jeżeli jest to konieczne przekształć gramatykę tak, aby wszystkie produkcje miały postać A -> x lub A -> xb, gdzie x to pojedyncza litera lub ε. 2. Stan startowy NAS jest symbolem startowym gramatyki. 3. Dla każdej produkcji I -> aj, utwórz przejście stanów I -> J oznaczone przez a. 4. Dla każdej produkcji I -> J, utwórz przejście stanów I -> J oznaczone przez ε. 5. Jeżeli istnieją produkcję postaci I -> a, dla jakiejś litery a, wtedy utwórz nowy symbol stanu F. Dla każdej produkcji I -> a, utwórz przejście stanów I -> F oznaczone przez a. 6. Końcowe stany NAS to wszystkie F razem z tymi I, dla których istnieje produkcja I -> ε. KONWERSJA NIEDETERMINISTYCZNEGO NA DETERMINISTYCZNY AUTOMAT SKOŃCZONY Niedeterministyczny automat skończony różni się od deterministycznego automatu skończonego tym, że przeczytanie tego samego symbolu w danym stanie może powodować przejście do jednego z kilku różnych stanów. 10
Każdemu niedeterministycznemu automatowi skończonemu odpowiada deterministyczny automat skończony akceptujący dokładnie te same słowa. Możemy go uzyskać dokonując determinizacji automatu skończonego. Aby tego dokonać postępujemy w sposób podany poniżej. ε-domknięcie danego stanu S jest to zbiór wszystkich stanów, włączając w to S, do których można dotrzeć poprzez ε-przejścia. Konstrukcja DFA: Krok 1: Niech stan początkowy DFA będzie utworzony z ε-domknięcia stanu początkowego NFA. Kolejne kroki: Jeżeli S jest jakimkolwiek stanem, który został poprzednio skonstruowany dla DFA i jest utworzony ze stanów t1,, tr NFA, wtedy dla każdego symbolu x dla którego choć jeden ze stanów t1,, tr posiada x-następcę, x-następna stanu S jest ε-domknięciem x-następcy stanów t1,, tr. Każdy stan DFA, który jest utworzony z akceptującego stanu NFA (pośród innych) staje się akceptującym stanem. Po przekształceniu następującego NFA: 11
Otrzymujemy DFA, które nie posiada ε-przejść i posiada pojedynczy stan akceptujący: Przykładowy pseudokod, który wykorzystaliśmy do implementacji w naszym narzędziu: 12
MINIMALIZACJA DETERMINISTYCZNEGO AUTOMATU SKOŃCZONEGO Minimalizacja to proces przekształcania deterministycznych automatów skończonych do automatów o najmniejszej możliwej liczbie stanów. Niektóre stany w automacie deterministycznym mogą być sobie,,równoważne'' i możemy je skleić. Natomiast badając, czy stany są równoważne trzeba zwrócić uwagę na to jakie słowa z danych stanów prowadzą do stanów akceptujących. Jeśli są tu jakieś różnice, to stanów nie możemy sklejać. Algorytm minimalizacji składa się z dwóch faz: 1. usunięcia stanów nieosiągalnych, 2. wyznaczenia relacji równoważności i sklejenia ze sobą stanów równoważnych. Znalezienie stanów nieosiągalnych jest prostsze. Trudniej natomiast znaleźć stany równoważne. Przypomnijmy, że dwa stany p i q są sobie równoważne, gdy: Pokażemy algorytm, który znajduje wszystkie pary nierównoważnych sobie stanów. Siłą rzeczy, pozostałe pary stanów są sobie równoważne i można je skleić ze sobą. Jeśli stany p i q nie są sobie równoważne, to istnieje rozróżniające je słowo x, takie że: Początkowo zakładamy, że wszystkie stany można skleić ze sobą. Następnie sukcesywnie wyznaczamy pary stanów które nie są sobie równoważne -- w kolejności wg. rosnącej długości najkrótszych rozróżniających je słów. 13
Algorytm: 1. Tworzymy tablicę wartości logicznych T {p,q} indeksowaną nieuporządkowanymi parami {p, q} stanów p, q Q. Początkowo T {p,q} = true dla wszystkich p, q Q. 2. Dla wszystkich takich par stanów {p, q}, że p F i q F zaznaczamy T {p,q} = false. Jeśli p jest akceptujący, a q nie, to stany te rozróżnia słowo puste ε. 3. Dla wszystkich par stanów {p, q}i znaków a Σ takich, że T {δ(p,a),δ(q,a)} = false, zaznaczamy również T {p,q} = false. 4. Jeżeli w kroku 3 zmieniliśmy choć jedną komórkę tablicy T z true na false, to powtarzamy krok 3 - tak długo, aż nie będzie on powodował żadnych zmian w tablicy T. 5. Na koniec mamy: p q T {p,q}. 14
III IMPLEMENTACJA WYKORZYSTANE TECHNOLOGIE Do stworzenia naszego konwertera wybraliśmy technologie, w których posiadamy największe doświadczenie, żeby móc skupić się na wprowadzaniu samych funkcji zamiast uczyć nowego języka. Wybór padł na język programowania Java, który posiada potężne możliwości w tworzeniu i organizacji kodu, struktur danych jak i samych algorytmów. Z drugiej strony chcieliśmy także stworzyć wygodne środowisko dla użytkownika, który mógłby bez problemu wprowadzać dane za pomocą graficznych kontrolek. Dlatego zdecydowaliśmy się stworzyć aplikację webową, której część serwerowa oparta jest na Javie, a część kliencka na HTML, CSS, JavaScript. Aby w pełni móc zaprezentować wizualizację automatów wykorzystaliśmy w naszym projekcie bibliotekę języka JavaScript o nazwie Viz. Jej wykorzystanie zostanie szerzej omówione w następnym paragrafie. Lista wykorzystanych technologii: Back-end: o JAVA o tomcat Front-end: o HTML + CSS + JavaScript o jquery o Bootstrap o Viz.js 15
Ważniejsze elementy aplikacji od strony back-end: W pakiecie algorithm znajdują się implementacje algorytmów, http servlet kieruje request do controllera, który mapuje request do opowiedniej metody w oparciu o konwersję, którą klient zażądał. Pakiet model reprezentuje wszystkie klasy odpowiadające za model aplikacji. 16
PRZYKŁADOWE FRAGMENTY KODU Z IMPLEMENTACJI ALGORYTMÓW Minimalizacja DAS Stanem startowym jest stan startowy DFA. Metoda filterunnecessarystates eliminuje stany nieosiągalne oraz takie, które nie są końcowe i nie ma z nich wyjścia. Następnie tworzona jest tablica logiczna reprezentująca relacje pomiędzy stanami, false oznacza, że stany nie są równe, na początku wszystkie ustawiamy na true, funkcja removefinalstatescombinations oznacza stany jako nierówne dla tych, w których jeden tylko jeden jest końcowym. W kolejnym kroku metoda removedifferentstates usuwa nierówne stany zgodnie z algorytmem przedstawionym w sekcji algorytmów. Na końcu ustawiane są stany końcowe. 17
NFA -> DFA 18
REGEX -> NAS 19
KLIENT Część kliencka, jak już wcześniej zostało wspomniane, opiera się o technologie HTML, CSS i JS. Elementy interfejsu zostały umieszczone w dokumencie HTML. Za ich wygląd w głównej mierze odpowiada biblioteka Bootstrap. Dostarcza ona arkusz stylów CSS oraz skrypty JS, na podstawie których zbudowane są poszczególne kontrolki. Za działanie kontrolek odpowiadają skrypty JS. Także w tym języku zaprogramowana jest cała komunikacja z częścią serwerową. Przebiega ona asynchronicznie za pomocą AJAX-a w postaci zapytania GET lub POST. Dane zapisane są w formacie JSON według przyjętej własnej konwencji. Przykładowy kod JSON przedstawiający dane automatu DAS: Jak zostało wspomniane w poprzednim paragrafie rysowanie automatu odbywa się przy pomocy biblioteki Viz.js, która służy do rysowania różnego typu diagramów. Do rysowania służy funkcja Viz(), która jako pierwszy argument przyjmuje łańcuch z odpowiednio sformatowanymi danymi, a drugi opcje, np. format. 20
Poniżej znajduje się kod, który odpowiada za rysowanie diagramów automatów: 21
IV DZIAŁANIE APLIKACJI OBSŁUGA I WPROWADZANIE DANYCH Tak jak już zostało wspomniane to wcześniej celem naszego narzędzia było stworzenie przyjaznego interfejsu użytkownika, dlatego wprowadziliśmy pewnie udogodnienia przy tworzeniu automatów oraz gramatyk. Całość interfejsu, zgodnie z przyjętymi założeniami, została podzielona na trzy części: Wyrażenia Regularne, Gramatyka oraz Automat. Każda część odpowiada za osobną postać opisywanego języka. Wyrażenia Regularne W tym miejscu znajduje się zwyczajne pole tekstowe, które umożliwia wpisanie łańcucha znaków, z którego składa się dane wyrażenie regularne w odpowiednim, przyjętym przez nas formacie. Format wyrażeń regularnych jest następujący:. oznacza konkatenację oznacza sumę * oznacza domknięcie Kleene'ego 22
Gramatyka W przypadku gramatyki wprowadzanie jest trochę bardziej skomplikowane, jednak z punktu widzenia użytkownika bardziej przejrzyste. Znajdują się tutaj trzy pola tekstowe odpowiadające za symbole: startowy, nieterminalne oraz terminalne. Każdy z nich wpisujemy oddzielając przecinkiem od poprzedniego. Poniżej znajduje się lista produkcji. Kolejne produkcje dodajemy wpisując odpowiednie wartości w pola Z i DO i klikając w zielony przycisk z +. Po jego naciśnięciu produkcja pojawia się powyżej. Aby ją usunąć należy nacisnąć czerwony przycisk x. Całość prezentuje się następująco: 23
Automat W przypadku automatu wprowadzanie danych odbywa się podobnie jak w przypadku gramatyki. Również znajduje się lista tym razem stanów, które dodajemy za pomocą odpowiedniego formularza. Aby zobaczyć jak wygląda wprowadzony automat można użyć przycisku Generuj, który odpowiada za generowanie graficznej reprezentacji w formie diagramu. Za pomocą odpowiednich przycisków możemy przełączać się również pomiędzy niedeterministyczną a deterministyczną oraz minimalną postacią automatu. 24
Po konwersji na DAS i jego minimalizacji nazwy stanów są złączeniem nazw stanów równoważnych, z których utworzono nowe. Aby je skrócić można nacisnąć zielony przycisk: Pozostałe Każda sekcja (postać) posiada przycisk Konwertuj. Po jego wciśnięciu wprowadzona postać zostaje przekonwertowana do pozostałych dwóch i wyświetlona w odpowiednich miejscach. Na górze znajduje się również przycisk Resetuj, który odpowiada za wyczyszczenie wszystkich wprowadzonych przez użytkownika danych. 25
WYNIKI Test 1 (minimalizacja automatu) Dla podanego poniżej wyrażenia regularnego generowany jest automat skończony, a następnie zostaje on wyświetlony w postaci minimalnej. Po minimalizacji: 26
Przed minimalizacją: 27
Test 2 Dla podanego poniżej automatu generowane jest odpowiednie wyrażenie regularne i gramatyka. 28
29
Test 3 Dla podanej poniżej gramatyki generowany jest automat i odpowiednie wyrażenie regularne. 30
31
V PODSUMOWANIE OGÓLNE WNIOSKI Podsumowując, wszystkie założenia naszego projektu zostały spełnione. Przyjęta struktura i konwencja konwersji została w odpowiedni sposób zaimplementowana i przetestowana z pozytywnym skutkiem. Interfejs narzędzia jest przyjazny dla użytkownika i umożliwia łatwe i szybkie wprowadzanie odpowiednich danych. NAPOTKANE PROBLEMY Jedynym lecz nie małym problemem na jaki się napotkaliśmy było przełożenie znalezionych przez nas algorytmów na język programowania. Większość opisana była w sposób przystępny dla człowieka, jako sposób na rozwiązanie pewnego problemu od strony matematycznej. Naszym zadaniem było natomiast zaimplementowanie ich w języku Java i zapewnienie poprawnego działania dla różnych danych. MOŻLIWE PRZYSZŁE PRACE Przydatną funkcją naszego narzędzia mogłaby być możliwość jeszcze bardziej przyjaznego sposobu wprowadzania danych do automatów poprzez bezpośrednie tworzenie ich graficznej reprezentacji przez użytkownika za pomocą przygotowanych elementów (pól, strzałek). Wykracza to jednak daleko poza cel tego projektu i pozostaje jedynie koncepcją, która mogłaby być wdrożona w nieokreślonej przyszłości. 32
VI LITERATURA John E. Hopcroft, Jeffrey D. Ullman Wprowadzenie do teorii automatów, języków i obliczeń, Wydawnictwo Naukowe PWN, Warszawa 2003 Harry H. Porter III Lexical Analysis, Portland State University James L. Hein, Discrete Structures, Logic, and Computability, Jones and Bartlett Publishers, 2010, Chapter 11 Inne: Viz.js Github Webpage https://github.com/mdaines/viz.js/ Bootstrap Webpage http://getbootstrap.com/ 33