Obiektowa implementacja parsera klasy LL(1)

Podobne dokumenty
Metody Kompilacji Wykład 8 Analiza Syntaktyczna cd. Włodzimierz Bielecki WI ZUT

Analiza leksykalna 1. Teoria kompilacji. Dr inż. Janusz Majewski Katedra Informatyki

Parsery LL(1) Teoria kompilacji. Dr inż. Janusz Majewski Katedra Informatyki

Metody Kompilacji Wykład 1 Wstęp

Języki formalne i automaty Ćwiczenia 4

Zadanie analizy leksykalnej

Języki formalne i automaty Ćwiczenia 3

10. Translacja sterowana składnią i YACC

Matematyczne Podstawy Informatyki

problem w określonym kontekście siły istotę jego rozwiązania

Metody Kompilacji Wykład 3

Wykład 5. Jan Pustelnik

Analiza leksykalna 1. Języki formalne i automaty. Dr inż. Janusz Majewski Katedra Informatyki

Metody Kompilacji Wykład 7 Analiza Syntaktyczna

Matematyczne Podstawy Informatyki

Wykład Ćwiczenia Laboratorium Projekt Seminarium

Programowanie Strukturalne i Obiektowe Słownik podstawowych pojęć 1 z 5 Opracował Jan T. Biernat

Języki formalne i automaty Ćwiczenia 1

Włączenie analizy leksykalnej do analizy składniowej jest nietrudne; po co więc jest wydzielona?

Generatory analizatorów

Wprowadzenie do kompilatorów

Języki formalne i automaty Ćwiczenia 2

Semantyka i Weryfikacja Programów - Laboratorium 6

0.1 Lewostronna rekurencja

Wprowadzenie do analizy składniowej. Bartosz Bogacki.

SYLABUS DOTYCZY CYKLU KSZTAŁCENIA Bieżący sylabus w semestrze zimowym roku 2016/17

Analizator syntaktyczny

Analiza i projektowanie obiektowe 2016/2017. Wykład 10: Tworzenie projektowego diagramu klas

2.2. Gramatyki, wyprowadzenia, hierarchia Chomsky'ego

Uproszczony schemat działania kompilatora

Podstawy programowania. Wykład Funkcje. Krzysztof Banaś Podstawy programowania 1

Historia modeli programowania

Dariusz Brzeziński. Politechnika Poznańska, Instytut Informatyki

PRZEWODNIK PO PRZEDMIOCIE

Języki formalne i gramatyki

Programowanie obiektowe - 1.

Wprowadzenie. Organizacja pracy i środowisko programistyczne. Mirosław Ochodek

Uproszczony schemat działania kompilatora

Programowanie w Javie nazwa przedmiotu SYLABUS A. Informacje ogólne

Plan wykładu. Kompilatory. Literatura. Translatory. Literatura Translatory. Paweł J. Matuszyk

Zaawansowane programowanie obiektowe - wykład 5

Programowanie obiektowe

Języki formalne i automaty Ćwiczenia 9

Efektywna analiza składniowa GBK

Programowanie obiektowe

Tworzenie języków specyfikacji dla zagadnień numerycznych

Gramatyki, wyprowadzenia, hierarchia Chomsky ego. Gramatyka

Gramatyki regularne i automaty skoczone

Programowanie w języku C++ Grażyna Koba

Podstawy programowania III WYKŁAD 4

Pierwsze kroki. Algorytmy, niektóre zasady programowania, kompilacja, pierwszy program i jego struktura

Języki formalne i automaty Ćwiczenia 8

Podstawy i języki programowania

Forum Client - Spring in Swing

Podstawy generatora YACC. Bartosz Bogacki.

Wzorce projektowe Michał Węgorek

Programowanie współbieżne i rozproszone

Program We Kompilator Wy Źródłowy

Konstruktory. Streszczenie Celem wykładu jest zaprezentowanie konstruktorów w Javie, syntaktyki oraz zalet ich stosowania. Czas wykładu 45 minut.

Programowanie obiektowe

Analiza i projektowanie oprogramowania. Analiza i projektowanie oprogramowania 1/32

Wstęp [2/2] Wbrew częstemu przekonaniu, nie są one gotowymi rozwiązaniami, to tylko półprodukty rozwiązania.

Technologie informacyjne - wykład 12 -

Egzamin / zaliczenie na ocenę*

Metody Kompilacji Wykład 13

Gramatyki atrybutywne

KONSTRUKCJA KOMPILATORÓW

Wzorce projektowe i refaktoryzacja

Język JAVA podstawy. wykład 1, część 3. Jacek Rumiński. Politechnika Gdańska, Inżynieria Biomedyczna

PROLOG WSTĘP DO INFORMATYKI. Akademia Górniczo-Hutnicza. Wydział Elektrotechniki, Automatyki, Informatyki i Inżynierii Biomedycznej.

Mariusz Trzaska Modelowanie i implementacja systemów informatycznych

PLAN WYNIKOWY PROGRAMOWANIE APLIKACJI INTERNETOWYCH. KL III TI 4 godziny tygodniowo (4x30 tygodni =120 godzin ),

Programowanie komputerów

Wykład 10. Translacja sterowana składnią

Java EE produkcja oprogramowania

Języki programowania zasady ich tworzenia

Diagramy maszyn stanowych, wzorce projektowe Wykład 5 część 1

Wzorce projektowe ArrayList. Aplikacja i zdarzenia. Paweł Chodkiewicz

Programowanie niskopoziomowe. dr inż. Paweł Pełczyński

Dariusz Brzeziński. Politechnika Poznańska, Instytut Informatyki

Hierarchia Chomsky ego Maszyna Turinga

Programowanie obiektowe

METODY REPREZENTACJI INFORMACJI

Elżbieta Kula - wprowadzenie do Turbo Pascala i algorytmiki

Analiza i projektowanie obiektowe 2017/2018. Wykład 3: Model wiedzy dziedzinowej

Analiza metodą zstępującą. Bartosz Bogacki.

XQTav - reprezentacja diagramów przepływu prac w formacie SCUFL przy pomocy XQuery

Klasa 2 INFORMATYKA. dla szkół ponadgimnazjalnych zakres rozszerzony. Założone osiągnięcia ucznia wymagania edukacyjne na. poszczególne oceny

Matematyczne Podstawy Informatyki

Laboratorium 1 Temat: Przygotowanie środowiska programistycznego. Poznanie edytora. Kompilacja i uruchomienie prostych programów przykładowych.

Zofia Kruczkiewicz - Modelowanie i analiza systemów informatycznych 1

UML a kod w C++ i Javie. Przypadki użycia. Diagramy klas. Klasy użytkowników i wykorzystywane funkcje. Związki pomiędzy przypadkami.

Informatyka I. dr inż. Andrzej Czerepicki.

Dodatkowo planowane jest przeprowadzenie oceny algorytmów w praktycznym wykorzystaniu przez kilku niezależnych użytkowników ukończonej aplikacji.

Wykład 1 Inżynieria Oprogramowania

INFORMATYKA TECHNICZNA Badanie możliwości wykorzystania języka AutoLISP i środowiska VisualLISP w systemie CAx

INFORMATYKA, TECHNOLOGIA INFORMACYJNA ORAZ INFORMATYKA W LOGISTYCE

JIP. Analiza składni, gramatyki

Wzorce projektowe. dr inż. Marcin Pietroo

Zaawansowane programowanie w C++ (PCP)

Transkrypt:

Obiektowa implementacja parsera klasy LL(1) Piotr Jeruszka 1 1 Wydział Inżynierii Mechanicznej i Informatyki Kierunek Informatyka, II stopień Specjalność: Aplikacje biznesowe i bazy danych, Rok II Streszczenie Poniższa praca przedstawia projekt implementacji obiektowego parsera klasy LL(1), działającego na podstawie plików specyfikacji. Parsery są elementami kompilatorów, odpowiadającymi za analizę składniową (stąd inna nazwa analizatory składniowe) kodu źródłowego. W pracy zostały wyjaśnione teoretyczne podstawy działania parsera klasy LL(1), w szczególności proces tworzenia tablicy parsowania. Wyszczególniono problemy związane z gramatykami klasy LL(1), używanymi do specyfikowania analizatorów składniowych. W trakcie implementacji skupiono się na podejściu obiektowym do problemu budowy analizatora leksykalnego (generującego dane dla parsera) i analizatora składniowego. Każdy element zaprojektowano jako klasę, możliwą do implementacji w obiektowych językach programowania. Projekt i autorska implementacja mogą służyć jako zbiór informacji, pozwalających zaimplementować własny parser. Program pozwala prześledzić kolejne kroki działania parsera, co czyni go użytecznym narzędziem edukacyjnym. Nie jest wymagana znajomości niskopoziomowych mechanizmów sterujących, takich jak automaty skończone (do rozpoznawania elementów parsera). Zaimplementowany parser nie musi być częścią kompilatora. Można go wykorzystać wszędzie tam, gdzie do sterowania programem wykorzystywane są zewnętrzne pliki (strumienie), zapisane według określonych zasad składniowych, jak np. skrypty. 1 Wstęp W latach 50 i 60 ubiegłego wieku programowanie systemów informatycznych było procesem żmudnym i skomplikowanym [1]. Zamiana projektu na gotowy kod była trudna, ponieważ język maszynowy (składający się z operacji niskopoziomowych) nie odzwierciedlał abstrakcji problemu, którego rozwiązaniem był projektowany system. Pośrednim rozwiązaniem było wprowadzenie intepreterów. Był to program, w którym kod interpretowany był na język maszynowy w czasie uruchomienia tego kodu. Rozwiązanie takie jednak było od około 10 do 20 razy wolniejsze niż oprogramowanie pisane bezpośrednio w języku maszynowym [1]. 1

Rozwiązaniem, które jest szeroko stosowane do dzisiaj, było zastosowanie kompilatora. Sam termin został wymyślony przez Grace Hooper na potrzeby języka programowania FLOC-MATIC, używanego do obliczeń finansowych [2]. Kompilator tłumaczy cały kod źródłowy języka wysokiego poziomu na język maszynowy. Pozwoliło to na odejście od rozkazów maszynowych do instrukcji bliższych rozwiązywanemu problemowi. Oprogramowanie było łatwiejsze do zrealizowania i utrzymywania. Sam kompilator utożsamia się z translatorem. Translator to program, tłumaczący kod napisany w języku programowania do równoważnego kodu w innym języku (w przypadku kompilatora do kodu maszynowego). Kompilacja składa się z pięciu zasadniczych faz: analizy leksykalnej, analizy składniowej, analizy semantycznej, optymalizacji i generowania kodu. Celem pracy jest implementacja dwóch pierwszych faz kompilacji: analizy leksykalnej i analizy składniowej (parsowania). Analizator leksykalny (lekser) generuje na podstawie kodu źródłowego ciąg leksemów (ciąg wejściowy), który następnie jest przekazywany do parsera klasy LL(1). Nazwa LL(1) oznacza, że ciąg wejściowy leksemów czytany jest od lewej do prawej strony (ang. left-right scan) oraz stosowane są lewostronne wyprowadzenia (ang. leftmost derivation). Wyprowadzenia te są wybierane na podstawie kolejnego (pojedynczego) elementu z ciągu wejściowego stąd liczba 1 w nazwie metody. Implementacji dokonano w języku programowania Java. Podane tutaj informacje powinny być jednak na tyle uniwersalne, że pozwolą na implementację analizatorów w dowolnym obiektowym języku programowania. Należy podkreślić, że w odróżnieniu od narzędzi yacc czy bison autorski program nie służy do generowania parserów. Jego głównym zadaniem jest sam rozbiór gramatyczny kodu źródłowego oraz wizualizacja tego procesu. 2 Implementacja analizatora leksykalnego Kompilacja rozpoczyna się od fazy analizy leksykalnej. Zadaniem tej analizy jest wygenerowanie ciągu leksemów na podstawie specyfikacji tokenów. Token to klasa elementów języka programowania, takich jak identyfikator, operator, literał liczbowy, słowo kluczowe czy średnik. Leksem to fragment konkretnego kodu źródłowego (atrybut), który zawiera informację o towarzyszącym mu tokenie. Na rysunku 1 przedstawiono przykład analizy leksykalnej. Pierwszy wiersz przedstawia fragment kodu źródłowego. Drugi i trzeci wiersz przedstawiają odpowiednio podział kodu na tokeny i atrybuty kolejnych leksemów. kod źródłowy: int a = 1; tokeny: typ prosty ' ' identyfikator operator przypisania ' ' literał liczbowy średnik atrybuty: int a = 1 ; Rys. 1: Przykład analizy leksykalnej fragmentu kodu źródłowego. Analizator leksykalny potrafi wykryć elementy, które nie pasują do żadnego tokenu. Wtedy powinien zostać zgłoszony błąd leksykalny. Błąd taki może być spowodowany literówką bądź użyciem słowa, które nie pasuje do żadnego wyrażenia regularnego. Lekser działa w oparciu o wyrażenia regularne. Zwykle mechanizm wyrażeń regularnych implementuje się za pomocą deterministycznych automatów skończonych (ang. 2

Deterministic Finite Automata, DFA) [1]. W oparciu o kolejne znaki kodu źródłowego automat przechodzi od stanu do stanu, akceptując kolejne słowa bądź stwierdzając brak dopasowania. W autorskim oprogramowaniu wykorzystano gotową bibliotekę wyrażeń regularnych, która dostępna jest w bibliotece standardowej Javy. Dwie użyte klasy: Pattern i Matcher znajdują się w pakiecie java.util.regex. LexemeRecord Token LexemesTable TokenList SimplyFileLTB SimplyFileTokenListBuilder <<interface>> LexemesTableBuilder <<interface>> TokenListBuilder LexemesTableBuilderFactory TokenListBuilderFactory getltb(buildername : String) : LexemesTableBuilder gettlb(name : String) : TokenListBuilder Rys. 2: Diagram klas, przedstawiający główne klasy analizatora leksykalnego. Rysunek 2 przedstawia diagram głównych klas, użytych w implementacji leksera. Należy zwrócić uwagę na klasę LexemesTable. Reprezentuje ona ciąg wykrytych leksemów, które zostaną przekazane do parsera. Każdy leksem reprezentowany jest przez obiekt klasy LexemeRecord. Obiekt ten przechowuje informacje o atrybucie, użytym tokenie (reprezentowanym przez obiekt klasy Token), jak również o pozycji w kodzie źródłowym. Tokeny przechowują informacje o wyrażeniach regularnych, używanych do rozpoznawania leksemów i korzystają z wymienionych klas Pattern i Matcher. Za budowę ciągu leksemów odpowiedzialny jest interfejs LexemesTableBuilder, podobnie jest z budową listy tokenów (interfejs TokenListBuilder). Wykorzystano tutaj parę wzorców projektowych: Fabryka abstrakcyjna i Metoda wytwórcza. Takie połączenie udostępnia interfejs do budowania powyższych elementów i umożliwia wykorzystanie w przyszłości różnych algorytmów budujących [3]. 3 Gramatyka i parser klasy LL(1) Analizator leksykalny ułatwia dalszą analizę kodu rozpoznaje poszczególne elementy kodu. W kolejnych fazach kompilacji odpada konieczność rozpoznawania klasy, do której należy dany podciąg znaków. Jak wspomniano lekser pracuje w oparciu o wyrażenia regularne. Wyrażenia te wchodzą w skład elementów gramatyki regularnej (według hierachii Chomsky ego [4]). Gramatyki regularne nie wystarczają jednak do sprawdzania kolejności leksemów ani reguł pomiędzy nimi zachodzących. Należy skorzystać z bardziej ogólnej klasy gramatyk gramatyk bezkontekstowych. Jest to czwórka uporządkowana G = {N, T, R, S}, gdzie: N to zbiór symboli nieterminalnych; T to zbiór symboli terminalnych, można je utożsamiać z tokenami; 3

R to zbiór reguł (produkcji); S to symbol startowy (będący nieterminalem). Analizator składniowy, korzystając z gramatyk bezkontekstowych, potrafi stwierdzić czy dany łańcuch symboli leksykalnych należy do języka, generowanego przez tą gramatykę. Innymi słowy, czy możliwe są takie wyprowadzenia (definiowane za pomocą reguł), które pozwalają na dopasowanie do ciągu leksemów. Istnieją dwie główne strategie analizy składniowej: zstępujące bądź wstępujące [5]. Metody zstępujące wyprowadzają symbol startowy do ciągu, który jest równoważny ciągowi wejściowemu. Metody wstępujące redukują ciąg wejściowy do symbolu startowego. Parser LL(1) należy do klasy analizatorów realizujących strategię zstępującą. Wybieranie kolejnej produkcji następuje w oparciu o tablicę parsowania. By utworzyć tę tablicę wymagane jest utworzenie zbiorów First i Follow dla każdego symbolu nieterminalnego. Intuicyjnie, zbiór First oznacza zbiór tych symboli terminalnych, które można wyprowadzić z danego symbolu nieterminalnego i które znajdują się na początku wyprowadzanego ciągu. Zbiór Follow zawiera te symbole terminalne, które w wyprowadzeniu znajdują się po prawej stronie danego nieterminalu. Dodatkowo uwzględnia się symbol terminalny $, oznaczający koniec ciągu. Analizowana metoda korzysta z gramatyk klasy LL(1), które są specjalnym przypadkiem gramatyki bezkontekstowej. Gramatyka klasy LL(1) posiada dwa główne ograniczenia: zbiór First nie może zawierać symbolu terminalnego, który jest możliwy do wyprowadzenia przez dwie (lub więcej) produkcji z danego symbolu nieterminalnego; zbiory First i Follow muszą być rozłączne, tj. ten sam symbol terminalny nie może wystąpić zarówno w jednym, jak i w drugim zbiorze danego nieterminala. ( * ) $ E (E) * - - Tab. 1: Przykładowa tablica parsowania. Tabela 1 przedstawia przykładową tablicę parsowania. Wiersze reprezentują symbole nieterminalne, występujące na początku ciągu wyprowadzeń. Kolumny to wszystkie możliwe do wystąpienia w ciągu leksemów symbole terminalne. Wybór kolejnej produkcji polega na wyborze komórki, leżącej na przecięciu wiersza symbolu nieterminalnego (ze szczytu ciągu wyprowadzeń) i symbolu terminalnego (ze szczytu ciągu wejściowego). Brak produkcji w danej komórce oznacza błąd składniowy analizowanego ciągu leksemów. Błąd składniowy nastąpi też wtedy, gdy terminal będący na szczycie ciągu wyprowadzeń i leksem na szczycie ciągu wejściowego reprezentują różne jednostki leksykalne. 4 Projekt i implementacja parsera klasy LL(1) Parser jest kluczowym elementem opisywanego programu. Dlatego poświęcono mu najwięcej uwagi, zarówno w kwestii projektowej, jak i implementacyjnej. Przedstawione zostaną podstawowe elementy parsera oraz klasy, odpowiadające za wyprowadzanie kolejnych symboli. 4

4.1 Podstawowe klasy parsera klasy LL(1) SymbolSet getnonterminalsymbols() : Set getterminalsymbols() : Set getsymbol(name : String) : Symbol ProductionRules rules ContextFreeGrammar Rule SymbolSequence 0..* 1..* symbols Symbol startsymbol EpsylonSymbol TerminalSymbol NonterminalSymbol Rys. 3: Podstawowe klasy parsera. Na rysunku 3 przedstawiono podstawowe klasy w projekcie parsera. Należy podkreślić znaczenie dwóch klas: Symbol i ContextFreeGrammar. Klasa Symbol reprezentuje symbol leksykalny. ContextFreeGrammar przechowuje informacje na temat produkcji (klasa agregująca ProductionRules), zbioru symboli (klasa agregująca SymbolSet) oraz symbolu startowego (referencja do obiektu klasy NonterminalSymbol). Często wykorzystywana jest klasa SymbolSequence. Jest to klasa agregująca referencje do obiektów klasy Symbol, zawierająca metody przydatne do operacji na listach. Metody te, jak na przykład gethead (zwraca pierwszy symbol ciągu) czy gettail (zwraca ogon listy) nie są zaimplementowane w kontenerach biblioteki standardowej Javy. Pojedyncza produkcja jest reprezentowana w obiekcie klasy Rule (obiekty tej klasy agregowane są w klasie ProductionRules). Zawiera ona referencję do symbolu nieterminalnego, stojącego po lewej stronie produkcji oraz sekwencji symboli (SymbolSequence), stojących po prawej stronie produkcji. 4.2 Budowa zbiorów First i Follow Algorytm, zaproponowany w [1], służący do budowy zbiorów First i Follow jest algorytmem o charakterze rekurencyjnym, pracującym na listach. Do takiej klasy algorytmów można zastosować funkcyjne języki programowania [6]. Języki te wspierają niektóre operacje, jak pobieranie ogona czy głowy listy, a które musiały zostać zaimplementowane (metody gettail i gethead klasy SymbolSequence). Co więcej, elementy zbioru First wymagają dodatkowej informacji o produkcji, która wyprowadza dany element. Celem rozwiązania powyższych problemów utworzono klasę SymbolRuleSet, która przechowuje instancje klasy SymbolRulePair. Ta ostatnia jest po prostu parą referencji do obiektów Symbol oraz Rule. Klasa SymbolRuleSet reprezentuje zbiór First danego symbolu nieterminalnego wtedy przechowuje pary symbol terminalny, produkcja. Zbiór Follow również jest odwzorowany w klasie SymbolRuleSet, przy czym nie jest wymagana informacja o produkcji. Na rysunku 4 przedstawiono diagram opisywanych w tym podrozdziale klas. Dodatkowo zaznaczono klasę FirstFollowSets, agregującą zbiory First i Follow każdego 5

SymbolRulePair ParsingTable rule : Rule symbol : Symbol FirstFollowSets SymbolParsingSet firstsets : Map followsets : Map buildfirstsets() : boolean buildfollowsets() : boolean Rys. 4: Diagram klas, będących kontenerami na elementy zbiorów First i Follow. symbolu nieterminalnego. Wykorzystano strukturę mapy, w której kluczem jest symbol nieterminalny, a wartością odpowiedni zbiór. 4.3 Implementacja produkcji lewostronnego wyprowadzenia NodeStack 1 DerivationWorker getusednodes() : List 1 DerivationJournal 1 0..* SymbolNode usednodes getparent() : SymbolNode getchildren() : List 1..* DerivationManager 0..* DerivationJournalEntry Rys. 5: Diagram klas, odpowiadających za konstrukcję lewostronnego wyprowadzenia. Na rysunku 5 przedstawiono uproszczony diagram klas, używanych przy tworzeniu lewostronnego wyprowadzenia. Zaprojektowano i zaimplementowano kontener do obsługi ciągu wyprowadzeń klasę NodeStack z wykorzystaniem listy wiązanej (klasy java.util.linkedlist). Reprezentuje ona ciąg wyprowadzeń, którego elementami są obiekty klasy SymbolNode. Jest to po prostu symbol z dodatkowymi informacjami: z którego symbolu został wyprowadzony (metoda getparent) i jakie symbole zostały z niego wyprowadzone (metoda getchildren). O ile w samym ciągu wyprowadzeń te informacje nie są zbyt istotne, to stają się ważne po zdjęciu symbolu ze stosu i dodawaniu nowych elementów do stosu. Informacje na temat rodzica i dzieci są cały czas aktualizowane. Klasa DerivationWorker zawiera informację o wszystkich zdjętych elementach ze stosu wyprowadzeń (metoda getusednodes zwraca listę obiektów typu SymbolNode). Zajmuje się ona również logiką tworzenia wyprowadzenia. Klasę DerivationManager utworzono głównie dla wygody programisty i dla lepszej współpracy z GUI. Jeżeli parsowanie zakończy się sukcesem, to na liście usednodes powinny być wszystkie wyprowadzane symbole. Wiedząc, że pierwszym symbolem na liście jest symbol startowy (jest on pierwszy zdejmowany ze stosu wyprowadzeń) oraz mając informację o symbolach (dzieciach), które wyprowadził możliwe jest utworzenie struktury drzewiastej, zwanej drzewem rozbioru gramatyki. 5 Wizualizacja działania programu Z punktu widzenia użytkownika ważna jest interakcja pomiędzy widokiem (oknem aplikacji) a logiką parsera, tak aby zapewnić spójny graficzny interfejs użytkownika (GUI). 6

W tym celu wykorzystano gotowe biblioteki graficzne (Swing oraz AWT). Należy też podkreślić wykorzystanie uproszczonego wzorca projektowego Obserwator [3]. 5.1 Wykorzystanie obiektu Słuchacza Uproszczenie wzorca Obserwator polega na pominięciu interfejsu Obserowany oraz utworzeniu nowego interfejsu (Słuchacza, ang. Listener) w zależności od zadania. Przykładowo, przedstawiana wcześniej klasa DerivationWorker posiada referencję do obiektu, realizującego interfejs DerivationWorkerListener. W trakcie pracy nad ciągiem wyprowadzeń wywoływane są metody, zadeklarowane wcześniej w interfejsie DerivationWorkerListener, takie jak informowanie o wyprowadzeniu symbolu nieterminalnego (metoda derivatenonterminal) bądź napotkaniu błędu (metody z prefiksem notmatching). ParserPanel::LinkedWithPanelDerivationListener DerivationWorker derivateterminal(worker : DerivationWorker,node : SymbolNode) : void derivatenonterminal(worker : DerivationWorker,node : SymbolNode,rule : Rule) : void notmatchingterminals(worker : DerivationWorker,invalidNode : SymbolNode,invalidLexeme : LexemeRecord) : void notmatchingrule(worker : DerivationWorker,invalidNode : SymbolNode,invalidSymbol : TerminalSymbol) : void changedstate(worker : DerivationWorker,state : ParsingResult) : void listener <<realize>> <<interface>> DerivationWorkerListener derivateterminal(worker : DerivationWorker,node : SymbolNode) : void derivatenonterminal(worker : DerivationWorker,node : SymbolNode,rule : Rule) : void notmatchingterminals(worker : DerivationWorker,invalidNode : SymbolNode,invalidLexeme : LexemeRecord) : void notmatchingrule(worker : DerivationWorker,invalidNode : SymbolNode,invalidSymbol : TerminalSymbol) : void changedstate(worker : DerivationWorker,state : ParsingResult) : void ParserPanel Rys. 6: Diagram klas z uwzględnieniem interfejsu DerivationWorkerListener. Interfejs DerivationWorkerListener może być implementowany przez jakikolwiek komponent graficzny, co umożliwia prezentację aktualnego stanu parsera. Na rysunku 6 przedstawiono uproszczony diagram klas z wykorzystaniem klasy słuchacza, wykorzystywanej w klasie ParserPanel (klasa, odpowiadająca za rysowanie widoku parsera). 5.2 Przykładowy wyglad parsera Rysunek 7 przedstawia zrzut ekranu z autorskiego programu LLVisualizer (graficzny interfejs użytkownika do zaimplementowanego parsera). Zbiór leksemów (uzyskany wcześniej z analizy leksykalnej, zakładka Lekser) został poddany analizie składniowej. Program generuje drzewo rozbioru gramatycznego, widoczne po lewej stronie obecnie zaimplementowane za pomocą komponentu JTree z biblioteki Swing. Możliwe jest przejrzenie wykorzystanych produkcji (tabela w centralnej części okna). 6 Podsumowanie Do autorskiej implementacji parsera klasy LL(1) wymagane było poznanie teorii kompilacji, zwłaszcza analizy leksykalnej i analizy składniowej. Głównym celem pracy było zaznajomienie Czytelnika z problemem budowy obiektowego parsera klasy LL(1). 7

Rys. 7: Okno aplikacji LLVisualizer po dokonaniu analizy składniowej. Budowa aplikacji wymagała zaprojektowania odpowiednich klas, zgodnych z paradygmatem programowania obiektowego. W wielu sytuacjach mogło to rodzić problemy, ze względu na rekurencyjny charakter algorytmów, dostosowanych do języków funkcyjnych. Należało wyodrębnić odpowiednie elementy programu, co zostało przedstawione. Implementacja, prócz ugruntowania wiedzy teoretycznej, pozwoliła Autorowi na poszerzenie swojej wiedzy na temat programowania aplikacji w języku Java. Poprawiono umiejętności wykorzystania wyrażeń regularnych oraz programowania graficznego interfejsu użytkownika. Zaprezentowana aplikacja jest nadal rozwijana. Warstwa logiki może być łatwo wykorzystana przy budowie własnego kompilatora. Warstwa prezentacji przedstawia wynik działania parsera w atrakcyjny dla użytkownika sposób. Dlatego poniższa praca, wraz z programem, dedykowana jest celom edukacyjnym w kursach, które swoim materiałem obejmują teorię kompilacji. Literatura [1] A. Aiken, Compilers Course, Stanford University, http://class2go.stanford.edu/cs143/spring2013, 2013. [2] R. Norman, Biographies of Women Mathematicians Grace Hooper, Agnes Scott College, http://www.agnesscott.edu/lriddle/women/hopper.htm, 2000. [3] E. Gamma, R. Helm, R. Johnson, J. Vlissides, Wzorce projektowe. Elementy oprogramowania obiektowego wielokrotnego użytku, Helion, 2010. [4] N. Chomsky, Three models for the description od language, Massachusetts Institute od Technology, 1956. [5] W. Complak, B. Bogacki, Podstawy kompilatorów, Politechnika Poznańska, http://wazniak.mimuw.edu.pl/index.php?title=podstawy_kompilatorów, 2009. [6] M. Kubica, Programowanie funkcyjne, http://wazniak.mimuw.edu.pl/index.php?title=programowanie_funkcyjne, 2009. 8