Lex FLEX – struktura generowanego analizatora leksykalnego
Transkrypt
Lex FLEX – struktura generowanego analizatora leksykalnego
Lex Instrukcja laboratoryjna Niniejsza instrukcja w przeważającej części dotyczy implementacji GNU FLEX [2] popularnego narzędzia lex [1]. FLEX – struktura generowanego analizatora leksykalnego FLEX generuje kod źródłowy (typowo w C) analizatora leksykalnego. Generowany plik źródłowy domyślnie posiada nazwę pliku z definicją analizatora oraz dodatkowym rozszerzeniem yy.c. Struktura generowanego pliku pokazana jest na poniższym rysunku. Partie kodu opisującego implementację i działanie analizatora generuje FLEX na podstawie wbudowanego pliku szkieletowego. Dodatkowo w odpowiednich miejscach wstawiane są partie kodu użytkownika kopiowane z pliku z definicją analizatora. Kod wykorzystuje funkcje biblioteki stdio, strumienie wejściowy yyin i wyjściowy yyout oparte są na typie FILE*. 1 Istnieje również możliwość generacji analizatora jako klasy C++. W tym celu należy podać opcję -+ w linii wywołania FLEX'a lub użyć opcji %option c++ (specyficzna dla FLEX'a) w sekcji definicji opisu analizatora. Generowany plik źródłowy posiada nazwę pliku z definicją analizatora oraz dodatkowym rozszerzeniem yy.cc. Struktura generowanego pliku pokazana jest na poniższym rysunku. Tym razem kod wykorzystuje biblioteke iostream, strumienie wejściowy yyin i wyjściowy yyout oparte są na typach istream oraz ostream. Definicja klasy jest osiągana z dodatkowego pliku nagłówkowego FlexLexer.h. Celem generacji analizatora w formie klasy C++ jest umożliwienie użycia wiecej niż jednego takiego analizatora w kodzie danej aplikacji. W przypadku FLEX'a opcja ta wydaje się być jeszcze na etapie konstrukcji i być może bez dodatkowych zabiegów użycie wielu analizatorów będzie niemożliwe. Działanie ciągłe i krokowe Przy pomocy LEX'a można generować dwa rodzaje analizatorów leksykalnych: • o działaniu ciągłym, • o działaniu krokowym. 2 Analizator o działaniu ciągłym wykonuje swoja pracę o wywołania funkcji yylex() aż do momentu wyczerpania strumienia wejściowego (lub zaistnienia innych warunków określonych przez użytkownika). Dopiero wtedy zwracane jest sterowanie z wywołania funkcji yylex(). Analizator może generować wyniki działania poprzez strumień wyjściowy yyout lub inne formy komunikacji z otoczeniem dostępne w C. Analizator o działaniu krokowym rozpoznaje pojedynczą jednostkę leksykalną w odpowiedzi na dane wywołanie funkcji yylex() i zwraca jej kod jako wynik tejże funkcji. Wyczerpanie strumienia wejściowego sygnalizowane jest kodem 0. Wartości kodów dla jednostek leksykalnych pozostają w gestii użytkownika. Oprócz kodu rozpoznanej jednostki analizator może również dostarczać dodatkowych danych dotyczących tej jednostki, aktualnego stanu strumienia wejściowego lub samego analizatora w dowolny sposób dostępny w C. Analizator krokowy jest najczęściej wykorzystywany w parze z analizatorem składniowym – najczęściej generowanym przez narzędzie yacc. W takim przypadku forma wymiany dodatkowych informacji oraz wartości kodów jednostek są dyktowane przez specyfikację analizatora składniowego. Rozmiar/szybkość analizatora LEX generuje implementację analizatora leksykalnego w postaci maszyny stanowej sterowanej ciągiem znaków napływających z wejścia (YY_INPUT). Graf maszyny (tranzycje pomiędzy stanami oraz podejmowane akcje) zapisany jest w postaci tablicy, która może przybierać spore rozmiary w przypadku rozbudowanych definicji analizatorów leksykalnych (tzw. lekserów). Główną siłą analizatora generowanego przez lex’a jest jego szybkość wynikająca z prekompilowania wszystkich decyzji do postaci tablicy. Analizator w czasie swojej pracy nie podejmuje żadnych kosztownych czasowo decyzji – tylko wykonuje program maszyny stanowej zapisany w tablicy. Istnieje możliwość wyboru (kompromisu) pomiędzy wielkością implementacji analizatora a jego szybkością. Chcąc zredukować rozmiar generowanej tablicy (co ma przekład na wielkość statycznego kodu aplikacji) należy ograniczyć liczbę reguł w definicji analizatora przerzucając część analizy do kodu użytkownika. Typowym przykładem takiego podejścia jest implementacja analizy słów kluczowych danego języka. Jako przykład rozważmy poniższą implementację analizatora leksykalnego języka C: %% "auto" "break" "case" ... "void" "volatile" return _AUTO; return _BREAK; return _CASE; return _VOID; return _VOLATILE; ({LETTER}|"_")({LETTER}|{DIGIT}|"_")* %% 3 return _ID; Wszystkie słowa kluczowe zostały zapisane jawnie jako osobne reguły. Wygenerowany analizator będzie szybki (słowo kluczowe zostanie rozpoznane w momencie dopasowania reguły, czyli w momencie kiedy zostanie całe przeczytane), ale tablica analizatora będzie duża. Jeśli zależy nam na zmniejszeniu rozmiarów tablicy możemy wykorzystać inną implementację: %{ char* keywords[] = { "auto", "break", "case", ... "void", "volatile" }; int match_keyword(char* text) { // binary search thru 'keywords' table ... if (found) return table_index; else return –1; } %} %% ({LETTER}|"_")({LETTER}|{DIGIT}|"_")* { int keyword = match_keyword(yytext); if (keyword >= 0) return keyword; else return _ID; } %% Tym razem słowa kluczowe zostały zapisane w tablicy w kodzie użytkownika. Ich rozpoznanie zostało przerzucone do akcji reguły dla identyfikatorów. Po rozpoznaniu identyfikatora przez analizator jest on poddawany testowi (wyszukiwanie binarne), czy jest on słowem kluczowym. Zależnie od wyniku testu zwracany jest odpowiedni kod. Jak widać, oprócz samego dopasowania reguły konieczne jest dodatkowe poszukiwanie w tablicy. Dla prawdziwych identyfikatorów osiągnie ono maksymalny czas odpowiedzi, ponieważ takiego słowa nie ma w tablicy. Tablica analizatora będzie – oczywiście – znacznie mniejsza. 4 Ograniczenia implementacyjne analizatorów leksykalnych Generowany kod implementacji analizatora posiada często pewne ograniczenia, których należy być świadomym przygotowując opis analizatora. Jednym z dosyć często występujących ograniczeń jest stałej długości bufor yytext oraz wewnętrzny bufor linii. W buforze yytext kompletowany jest tekst fragmentu wejścia dopasowywanego do wzorca reguły (wyrażenia regularnego). Dopasowana treść jest dostępna (właśnie poprzez zmienną yytext) dla kodu użytkownika wykonywanego w ramach dopasowania reguły. Bufory są typowo implementowane jako stałej długości tablice typu char*. Ich pojemność ogranicza maksymalną długość fragmentu wejścia, jaki może być dopasowywany do reguły. Rozważmy implementację analizatora leksykalnego dla języka C++, a w szczególności reguły filtrujące komentarze. %% "//".*\n "/*"(.|\n)*"*/" %% /* single line comment */ ; /* multi line comment */ ; Pierwsza z reguł rozpoznaje treść komentarza jednowierszowego, druga treść komentarza wielowierszowego. Chociaż obie reguły są poprawnie skonstruowane, druga z nich może nie działać w praktyce: zbyt długie komentarze wielowierszowe mogą przepełniać bufor yytext. Dokumentacja FLEX'a zaleca następujący sposób implementacji: %% "//".*\n "/*" { /* single line comment */ ; /* multi line comment */ register int c; for ( ; ; ) { while ( (c = input()) != '*' && c != EOF ) ; /* eat up text of comment */ if ( c == '*' ) { while ( (c = input()) == '*' ) ; if ( c == '/' ) break; /* found the end */ } if ( c == EOF ) { error( "EOF in comment" ); break; } } } 5 Stany warunkowe Stany warunkowe pozwalają na grupowanie reguł oraz włączanie / wyłączanie poszczególnych grup w trakcie pracy analizatora – stosownie do napotykanych kontekstów w strumieniu wejściowym. Podstawowe informacje dotyczące składni oraz sposobu działania stanów warunkowych można znaleźć w dokumentacji FLEX'a. Tutaj skupimy się jedynie na przykładach ich praktycznego wykorzystania. Oto inny przykład implementacji problemu filtracji komentarzy: %s CODE COMMENT %% <CODE>"auto" <CODE>"break" <CODE>"case" ... <CODE>"void" <CODE>"volatile" return _AUTO; return _BREAK; return _CASE; return _VOID; return _VOLATILE; <CODE>"//".*\n /* single line comment */ ; <CODE>"/*" /* multi line comment begin*/ BEGIN(COMMENT); <COMMENT>.|\n <COMMENT>"*/" /* each comment */ ; / multi line comment end */ BEGIN(CODE); %% int main() { BEGIN(CODE); ... yylex(); } Reguły dotyczące kodu C zostały przypisane do stanu CODE. W momencie napotkania początku komentarza wielowierszowego następuje przełączenie aktywnego zestawu reguł ze stanu CODE na COMMENT. W tym stanie aktywne reguły powodują jedynie pochłanianie znaków z wejścia aż do napotkania końca komentarza wielowierszowego, gdzie nastąpi ponowne przełączenie do stanu CODE. Stany analizatora mogą ułatwić konstrukcję analizatorów przeznaczonych dla więcej niż jednego języka, jeśli ich treści występują we wspólnym strumieniu. Przykładami takich notacji mogą być: • język HTML z osadzonymi fragmentami w języku JavaScript, • kod Javy z osadzonymi fragmentami dokumentacji w formacie JavaDoc. 6 Implementacja funkcji preprocesora Wiele języków zawiera polecenia preprocesora, które aktywnie wpływają na postać strumienia wejściowego podczas jego analizy. Najprostszym przykładem funkcji preprocesora jest filtracja komentarzy. W poprzednich przykładach pokazane zostały możliwe implementacje tej funkcji w postaci jawnej w ramach specyfikacji analizatora. Oprócz tego pozostają dwie istotne (i popularne) funkcje preprocesora, których implementacja nie jest już tak trywialna: • makropodstawienie (#define), • dołączanie plików (#include). Istnieje kilka metod praktycznych implementacji tych funkcji. Implementacja zewnętrzna. Ze względu na spore problemy przy implementacji wyżej wymienionych funkcji w ramach analizatora, dosyć często wykorzystywaną metodą jest implementacja preprocesora jako zewnętrznej aplikacji. Aplikacja preprocesora oraz analizatora leksykalnego uruchamiane są następnie w systemie wielozadaniowym w potoku. Technika ta pozwala na bardzo przejrzystą i efektywną implementację. Jest stosowana między innymi w kompilatorach LCC oraz GCC. Inną metodą współpracy preprocesora i analizatora może być przedefiniowanie makra YY_INPUT, którego zadaniem jest odczyt kolejnych linii z wejścia. Funkcje preprocesora były wtedy ukryte w części operacyjnej makra. Implementacja wewnętrzna makropodstawienia. Taka forma implementacji wymaga aktywnej analizy oraz modyfikacji wewnętrznego bufora linii analizatora. Do tego konieczna jest znajomość szczegółów implementacji maszyny analizatora generowanej przez konkretne narzędzie oraz bardzo często wymaga przebudowania tej implementacji. Standardowo narzędzia lex nie wspomagają tego typu manipulacji. Implementacja wewnętrzna dołączania plików. Nieco lepiej jest w przypadku funkcji dołączania plików, której implementacja jest wspomagana częściowo przez standardowe mechanizmy lex'a, a częściowo przez niestandardowe rozszerzenie konkretnych implementacji. Do mechanizmów standardowych należy funkcja yywrap(), której treść użytkownik może samodzielnie zdefiniować. Funkcja ta jest wywoływana w momencie osiągnięcia fizycznego końca strumienia. Nie musi to wcale oznaczać logicznego końca strumienia. Ostateczna decyzja należy do yywrap(). Domyślnie funkcja zwraca wartość 0 (fałsz), co faktycznie oznacza koniec pracy. Może jednak zwrócić wartość niezerową (prawda), co spowoduje kontynuację pracy analizatora. Wcześniej jednak funkcja musi przełączyć wejście yyin analizatora do nowego fizycznego źródła (najczęściej pliku). Funkcja yywrap() jest z powodzeniem wykorzystywana do przełączania strumienia w momencie napotkania końca dołączanego pliku. W takim momencie strumień logiczny wcale się nie kończy – należy wznowić analizę pliku nadrzędnego od momentu, w którym została ona przerwana. Samo przełączanie strumieni wiąże się z operacjami niskopoziomowymi silnie zależnymi od konkretnej implementacji maszyny analizatora. Ze względu na możliwe wewnętrzne buforowanie strumienia, wykorzystanie w tym celu makra YY_INPUT może nie wystarczyć. 7 Implementacja FLEX'a dostarcza specjalnego zestawu funkcji oraz makr dedykowanych problemowi dołączania plików. Są nimi: YY_BUFFER_STATE yy_create_buffer( FILE *file, int size ); void yy_switch_to_buffer( YY_BUFFER_STATE new_buffer ); void yy_delete_buffer( YY_BUFFER_STATE buffer ); void yy_flush_buffer( YY_BUFFER_STATE buffer ); YY_CURRENT_BUFFER Szczegóły dotyczące sposobu ich wykorzystania można znaleźć w dokumentacji FLEX'a. Literatrura [1]. M. E. Lesk and E. Schmidt "Lex - A Lexical Analyzer Generator" [2]. V. Paxson "Flex, version 2.5 A fast scanner generator. Edition 2.5" 8