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

Podobne dokumenty