4.3. Automaty skończone (skończenie stanowe) automatem skończonym niedeterministycznym (ang. nondeterministic finite automaton) M N nazywamy system: M N = < Q, Σ, δ, q 0, F > Q Σ skończony zbiór stanów sterowania, skończony alfabet wejściowy, δ : Q Σ 2 Q funkcja przejść, q 0 Q wyróŝniony stan początkowy, F Q zbiór stanów końcowych.
przykład (graf funkcji przejść automatu M N1 akceptującego język (a b)*abb) automat M N moŝe mieć dodatkowo ε-przejścia (δ : Q Σ {ε} 2 Q ), automatem skończonym deterministycznym (ang. deterministic finite automaton) M D nazywamy system: Q skończony zbiór stanów sterowania, Σ skończony alfabet wejściowy, δ : Q Σ Q funkcja przejść, M D = < Q, Σ, δ, q 0, F > 2
q 0 Q wyróŝniony stan początkowy, F Q zbiór stanów końcowych. przykład (graf funkcji przejść automatu M D1 akceptującego język (a b)*abb) 3
powiązania pomiędzy wyraŝeniami regularnymi, niedeterministycznymi i deterministycznymi automatami skończonymi: dla wyraŝenia regularnego r konstruuje się M N z ε-przejściami akceptujący L(r), z automatu z ε-przejściami moŝna te przejścia usunąć, z automatu M N konstruuje się równowaŝny automat deterministyczny M D, moŝna zminimalizować liczbę stanów M D ; zasady konstruowania M N na podstawie wyraŝenia regularnego r: 1) dla wyraŝenia ε, M N przyjmuje postać: 4
2) dla dowolnego a Σ, M N przyjmuje postać: 3) niech M N1 oraz M N2 oznaczają automaty uzyskane dla wyraŝeń regularnych r 1 i r 2 ; wtedy dla wyraŝenia regularnego r 1 r 2 automat przyjmuje postać: 4) dla wyraŝenia regularnego r 1 r 2 automat przyjmuje postać: 5
5) niech M N oznacza automat dla wyraŝenia r; wtedy dla wyraŝenia regularnego r* automat przyjmuje postać: przykład (graf funkcji przejść automatu M N2 z akceptującego język (a b)*abb) 6
zasada tworzenia M D =< Q, Σ, δ, q 0, F > na podstawie M N = < Q, Σ, δ, q 0, F>; niech Q = 2 Q, 1) q 0 = {q 0 }, 2) F zawiera wszystkie S Q takie, Ŝe S F, 3) dla wszystkich S Q określamy δ (S, a) = S, gdzie S = {p p δ(q, a) q S}; przykład (tworzenie M D1 na podstawie M N1 ) δ ({0}, a) = {0,1} δ ({0}, b) = {0} δ ({0,1}, a) = {0,1} δ ({0,1}, b) = {0,2} δ ({0,2}, a) = {0,1} δ ({0,2}, b) = {0,3} δ ({0,3}, a) = {0,1} δ ({0,3}, b) = {0} b a {0} {0,1} b b {0,2} a b a {0,3} a 7
automaty deterministyczne z wyjściem: 1) automat deterministyczny Moore a: M Moore = <Q, Σ,, δ, λ, q 0 >, alfabet wyjściowy, λ : Q, funkcja wyjść, przyporządkowująca stanom wyjścia. 2) automat deterministyczny Mealy ego: M Mealy = <Q, Σ,, δ, λ, q 0 > alfabet wyjściowy, λ : Q Σ, funkcja wyjść, przyporządkowująca parom: stan, wejście wartość wyjścia. Tw. (lemat o pompowaniu dla języków regularnych) Dla dowolnego języka regularnego L(G R ) istnieje liczba naturalna p taka, Ŝe jeśli słowo z L(G R ) i z p, to z = uvw oraz: 1) v 1, 2) uv p, 3) kaŝde słowo postaci uv i w L(G r ), i 0. 8
4.4. Język Lex programy w języku Lex opisują działanie przetworników tekstu, konkretny system Lex (np. flex) jest generatorem przetworników zdefiniowanych w języku Lex: program w języku Lex Generator Lex (np. flex) program w C (przetwornik p.c) tekst wejściowy Przetwornik (np. a.out) Kompilator języka C (np. cc, gcc, ) tekst wyjściowy 9
struktura programu w języku Lex: definicje pomocnicze %% opcjonalne (dyrektywy, nazwy wyraŝeń, deklaracje, ) wzorzec 1 akcja1 obligatoryjne (reguły przetwarzania) wzorzec 2 akcja2 wzorzec n akcja n %% podprogramy pomocnicze opcjonalne 10
4.4.1 Wzorce w programach mają postać (poszerzonych o nowe operatory) definicji regularnych (w cudzysłowie lub bez), definicje regularne są zbudowane nad pewnym alfabetem oraz zbiorem nazw (wyraŝeń regularnych), np. wyraŝenie regularne: [a-za-z][a-za-z0-9]*, nad alfabetem A={a,b,,z,A,B,,Z,0,1,,9} moŝna zastąpić definicją regularną postaci: litera [a-za-z] cyfra [0-9] {litera}({litera} {cyfra})* nad alfabetem A = {a,b,,z,a,b,,z,0,1,,9} {litera,cyfra} znaki specjalne:. % \ " * ( )? + ^ $ [ ] - < > { } 11
priorytety operatorów tworzenia wyraŝeń regularnych w porządku malejącym: a) domknięcie zwrotne *, domknięcie dodatnie +, opcjonalny wybór?, zbiór znaków [], dopełnienie zbioru znaków [^], konkatenacja wielokrotna {}(nowy operator), b) konkatenacja, c) alternatywa, d) prawy kontekst /(nowy operator), początek wiersza ^, koniec wiersza $, e) lewy kontekst <> (nowy operator), f) koniec pliku <<EOF>> (nowy operator). przykłady programów z róŝnymi typami wzorców: %% %% nie ; "nie" ; Kto nie pracuje, ten nie ma. 12
%% ^[a-za-z_][a-za-z_0-9]*$ {printf("nazwa: %s", yytext);} ^[0-9]+(\.[0-9]+)?$ {printf("liczba: %s", yytext);} ^[^a-za-z0-9]+$ {printf("inne: %s", yytext);}. {;} zastosowanie konkatenacji wielokrotnej: %% [a-za-z0-9]{1,8}(\.[a-za-z0-9]{1,3})? ECHO;. {;} wykorzystanie znaków specjalnych: %% \ \t \n {printf("biala spacja");}. {;} 13
zastosowanie operatora lewego kontekstu: %start nazwa stanu w części definicji pomocniczych (moŝe być wiele) <nazwa stanu> odwołanie do stanu we wzorcach reguł przetwarzania np.: %start string definicja stanu S (pomijanie znaków łańcucha) %% <string>\" {BEGIN 0;} <string>. ; \" {BEGIN string;} albo <0>\" {BEGIN string;} " 0 S " w stanie 0 następuje przepisywanie wejścia na wyjście, w stanie string (S) pomijanie znaków. 14
przykład zastosowania definicji regularnych i operatora prawego kontekstu: wyraŝenie regularne/podgląd wyodrębnienie części pasującej do wyraŝenie regularne, o ile występuje po niej fragment pasujący do wyraŝenia regularnego podgląd np.: numb [0-9]+ nazwa wyraŝenia %% {numb} {printf("card ");} {numb}"."{numb}?/[^\.] {printf("real ");} ".." {printf("range ");} "." {printf("dot ");} " " ; dla wejścia: 1..5 37.5 otrzymamy na wyjściu: Card Range Card Real 15
4.4.2 Zasady działania przetwornika (reguły dopasowywania wzorców) cykliczna analiza pliku wejściowego, którego przedrostki są dopasowywane do wzorców zawartych w regułach przetwarzania; wykonywanie akcji związanych z wzorcami, reguła najdłuŝszego dopasowania (gdy przedrostek pasuje do kilku wzorców; bierze się pod uwagę takŝe długość ciągu podglądanego, który jest z powrotem przesyłany na wejście), reguła pierwszego dopasowania (wybór wzorca występującego wcześniej, gdy ta sama długość dopasowania), ustawienie wartości zmiennych globalnych: yytext (dopasowany ciąg znaków, przedrostek pliku wejściowego, łącznie z częścią podgląd w sytuacji zastosowania prawego kontekstu), yyleng (długość przedrostka bez części dopasowanej do podgląd) i yy_end (długość całości dopasowanej, łącznie z częścią dopasowaną do podgląd), wykonanie akcji związanej z wzorcem dopasowania i usunięcie dopasowanego przedrostka z pliku wejściowego (część dopasowana do podgląd jest zwrotnie przekazywana na wejście). reguła zastępcza (przepisanie znaku wejściowego na wyjście), w wypadku braku dopasowania. 16
4.4.3 Akcje w regułach akcja ma postać instrukcji języka C (moŝe teŝ być pusta ; oznaczająca pominięcie dopasowanego ciągu znaków oraz przyjąć postać kreski sugerującej powtórzenie akcji z następnej reguły przetwarzania skrócenie zapisu), w akcjach moŝna wykorzystywać zmienne standardowe, funkcje standardowe i makrowywołania; zestawienie makrowywołań: Makrowywołanie Opis ECHO BEGIN stan REJECT ECHO polecenie skopiowania yyleng początkowych znaków zmiennej yytext do pliku wyjściowego; BEGIN stan polecenie zmiany bieŝącego stanu początkowego przetwornika tekstu na stan początkowy stan, wymieniony bezpośrednio po nazwie polecenia; REJECT polecenie przekazania sterowania do kolejnej spośród aktualnie pasujących reguł przetwarzania; yymore() yymore() polecenie modyfikuje standardowy algorytm wyznaczania wartości yytext; w kolejnym (i tylko tym) kroku działania wartość zmiennej yytext ma być uzyskana przez sklejenie przedrostka ciągu wejściowego dopasowanego do ustalonego w tym kroku wzorca z bieŝącą wartością zmiennej yytext; yyterminate() odpowiada instrukcji return 0; yyless(n) yyless(n) polecenie przesłania zwrotnie na początek pliku wejściowego wszystkich, z 17
wyjątkiem n początkowych, znaków dopasowanych do aktualnego wzorca. zestawienie funkcji standardowych: input() 18 Funkcja Opis input() usuwa jeden, początkowy znak z nieprzetworzonej części pliku wejściowego, unput(c) unput(c) dołącza wskazany znak c na początek nieprzetworzonej części pliku wejściowego, yywrap() yywrap() wysyła do przetwornika tekstu informację o: a) zakończeniu działania, w sytuacji napotkania końca aktualnie przetwarzanego pliku i braku dalszych plików do przetwarzania; b) kontynuacji działania w odniesieniu do nowego pliku wejściowego, w sytuacji napotkania końca aktualnie przetwarzanego pliku wejściowego i wystąpienia Ŝądania przetwarzania kolejnego pliku; yyerror(msg, ) yyerror(msg, ) wysyła do standardowego pliku z błędami komunikat o treści msg, sygnalizujący niepoprawną budowę pliku wejściowego; po wysłaniu tego komunikatu przetwornik tekstu kontynuuje swoje działanie w odniesieniu do pozostałej części pliku wejściowego. yyerror(msg, ) jest równocześnie funkcją standardową systemu YACC.
zestawienie zmiennych standardowych: Zmienna yytext yy_end yyleng yylastc yyin yyout Opis char yytext[yylmax] (ewentualnie char *yytext) przechowuje przedrostek dopasowany do wzorca w bieŝącej regule przetwarzania; jeśli wzorzec jest postaci wregularne/podglad, to w yytext znajdzie się sklejenie ciągów znaków dopasowanych do rwregularne i podglad; standardowa pojemność YYLMAX tablicy yytext jest ustalana za pomocą makrodefinicji; int yy_end pamięta długość ciągu znaków aktualnie przechowywanego w yytext; jeśli yytext przechowuje przedrostek dopasowany do wregularne/podglad, to yy_end pamięta łączną długość tego przedrostka; int yyleng pamięta: a) długość całego ciągu znaków przechowywanego w yytext, jeśli jest nim przedrostek dopasowany do wyraŝenia bez operatora prawego kontekstu /; b) długość tego fragmentu ciągu znaków przechowywanego w yytext, który pasuje do wregularne, jeśli zmienna yytext przechowuje przedrostek dopasowany do wzorca z operatorem prawego kontekstu /, o postaci: rwregularne/podglad ; char yylastc przechowuje ostatni znak przedrostka dopasowanego do wzorca poprzedniej aktywnej reguły przetwarzania; jeśli wzorzec ten był postaci wregularne/podglad, to jest to ostatni znak dopasowanego do wregularne; FILE *yyin wskazuje na aktualnie przetwarzany plik wejściowy; domyślnie jest nim stdin; redefinicja przyjmuje postać yyin = plikwe, gdzie plikwe to wskazanie na nowy plik wejściowy; FILE *yyout wskazuje na plik wyjściowy, domyślnie na stdout; redefinicja za pomocą 19
instrukcji yyout = plikwy, gdzie plikwy oznacza wskazanie na nowy plik wyjściowy; yylineno int yylineno przechowuje numer porządkowy aktualnie przetwarzanego wiersza pliku wejściowego; yystart int yystart przechowuje numer tego stanu automatu skończonego, w którym rozpoczyna się realizacja bieŝącej reguły przetwarzania (automat skończony opisujący działanie przetwornika, jest budowany równolegle z opisem w języku C, standardowo umieszczanym w pliku lex.yy.c) 4.4.4 Sekcja definicji pomocniczych deklaracje extern zmiennych zewnętrznych programu, definicje zmiennych statycznych, widocznych w pewnym fragmencie lub całym programie (definicje zmiennych automatycznych, których zakres widoczności ma być ograniczony do pojedynczej akcji programu naleŝy umieścić w treści tej akcji); definicje stanów początkowych wykorzystywanych we wzorcach z operatorem lewego kontekstu; definicje te mają postać: %start stan, gdzie stan oznacza nazwę stanu początkowego uŝywanego w programie, dyrektywy #include włączania innych plików do programu; definicje nazw wyraŝeń regularnych, postaci: nazwa znaczenie 20
gdzie nazwa oznacza kod mnemoniczny wyraŝenia regularnego, a znaczenie definiuje jego treść. 4.4.5 Sekcja podprogramów pomocniczych zawierają opisy działań w postaci funkcji języka C, mogą redefiniować funkcje standardowe lub definiować specyficzne działania niestandardowe. przykłady programów: %{ sekcja definicji pomocniczych int num_count = 0; %} %% sekcja reguł przetwarzania [0-9]+ {printf("+"); REJECT;} [\+\-]?[0-9]+ {ECHO; ++num_count;} [\ \t\n] {ECHO;} %% sekcja podprogramów (redefinicja funkcji standardowej) int yywrap(void) { printf("file CONSISTS OF %d NUMBERS", num_count); return 1;} 21
%{ int Top=0, Stos[100]; %} %% [0-9]+ {push1(atoi(yytext));} "+" {push1(pop()+pop());} "*" {push1(pop()*pop());} [\ \t] ; \n {printf("%d\n",pop());}. {yyterminate();} %% void push1(int x) {Stos[++Top]=x; return;} pop(void) {return Stos[Top--];} dane (wyraŝenia arytmetyczne w ONP) i wyniki (wartości wyraŝeń): 2 2 3 5 + 8 2 8 * 16 2 3 5 + * 16 22
2 3 5 * + 17 %{ #include <stdlib.h> #include <string.h> #include "y.tab.h" typedef union {char *txt; float fval; int ival;} u_type; u_type attrib; %} %% \+ {printf("+");} \* {printf("*");} \( {printf("(");} \) {printf(")");} [0-9]*\.[0-9]+ {attrib.fval = atof(yytext);echo;} [0-9]+\. {attrib.fval = atof(yytext);echo;} [0-9]+ {attrib.ival = atoi(yytext);echo;} [A-Za-z][A-Za-z0-9_]* {attrib.txt = strdup(yytext);echo;} [\ \t\n] {;}. {yyerror("unexpected symbol");} 23
4.4.6 Uwagi o uruchamianiu generatora Lex i przetwornika tekstu jeśli plik z programem w języku Lex, opisującym funkcjonowanie przetwornika tekstu nazywa się przetw.l, to polecenie: lex -o przetw.c przetw.l spowoduje utworzenie przetwornika (automatu) w języku C i umieszczenie go w pliku o standardowej nazwie przetw.c, co kończy proces generacji przetwornika tekstu, przed wykorzystaniem przetwornika naleŝy go skompilować, korzystając z dostępnego kompilatora języka C (np. cc lub gcc) i wskazując plik biblioteczny systemu Lex o nazwie l (opcja ma postać -ll): gcc przetw.c -ll powstanie plik wykonywalny o standardowej nazwie a.out, który moŝna następnie stosować do przetwarzania tekstów, np.: a.out < test.in > test.out a.out < test.in a.out > test.out a.out 24