490 likes | 690 Views
Technika kompilacji. Analiza leksykalna. Adam Piotrowski. Plan wykładu. Analizator leksykalny Wyrażenia regularne Przykład analizatora leksykalnego implementowanego w języku C Generator analizatorów leksykalnych – flex. Elementy kompilatora. Zadania:
E N D
Technika kompilacji Analiza leksykalna Adam Piotrowski
Plan wykładu Analizator leksykalny Wyrażenia regularne Przykład analizatora leksykalnego implementowanego w języku C Generator analizatorów leksykalnych – flex
Elementy kompilatora • Zadania: • czytanie znaków z wejścia i produkcja sekwencji symboli leksykalnych dla analizatora składniowego • eliminacja komentarzy i białych znaków • informowanie o aktualnym numerze analizowanego wiersza • rozwijanie makr preprocesora • uzupełnianie tablicy symboli Program źródłowy Analizator leksykalny Analizator składniowy Analizator semantyczny Obsługa błędów Tablica symboli Gen. kodu pośredniego Optymalizator kodu Generator kodu Program wynikowy
Analiza leksykalna • Powody rozdzielenia analizy leksykalnej i składniowej • prostota projektowania • poprawienie wydajności • zwiększenie przenośności kompilatora • Opisywane za pomocą prostych gramatyk lub wyrażeń regularnych (automaty stanowe)
Analiza leksykalna • Większość symboli leksykalnych należy do jednej z grup: • nazwy (identyfikatory) • słowa zarezerwowane (ustalony podzbiór zbioru nazw) • liczbycałkowite • liczbyrzeczywiste • łańcuchyznakowe • operatory: • addytywne (+, -), • unarne (+, -), • multiplikatywne (*, /) • relacyjne (<, >, =, <=, >=, <>) • logiczne (and, or, not) • przypisania (:=) • ogranicznikijednoznakowe: ; , [ ] ( ) . • ogranicznikidwuznakowe: (* , *), +=, ++ itd.
Analizator leksykalny Leksemy: Symbole leksykalne: identyfikator op_przypisania op_add op_mul identyfikator liczba identyfikator • identyfikator: • zbiór zaczynający się od litery lub podkreślenia, po którym następuje dowolnie długi ciąg liter, cyfr, podkreśleń • op_add: • operator dodawania lub odejmowania • op_mul: • operatory mnożenia i dzielenia • op_przypisania: • operator składający się ze znaku „:” po którym następuje znak „=„ pozycja := poczatek + tempo * 60
Symbole leksykalne, wzorce i leksemy Analizator leksykalny czyta kolejne znaki programu źródłowego i wydziela jednostki leksykalne, zwane symbolami leksykalnymi (tokenami). Leksem to podstawowa jednostka leksykalna na jaką jest dzielony tekst kodu źródłowego. Wzorzec to reguła opisująca postać leksemu który można zaliczyć do danego symbolu leksykalnego.
Przykłady intfun() { int x, y, z; x = y + z - 1.5; return x * 2; }
Atrybuty symboli leksykalnych ? identyfikator int z intfun() { int x, y, z; x = y + z - 1.5; return x * 2; } return y fun x Jeżeli znaki z wejścia pasują do więcej niż jednego wzorca, analizator leksykalny musi mieć dodatkową informację dla dalszych faz kompilacji o konkretnym dopasowanym leksemie. W praktyce symbole leksykalne mają jeden atrybut – wskaźnik pozycji w tablicy symboli.
Analizator leksykalny i jego rola w kompilatorze Symbol leksykalny Analizator leksykalny Analizator składniowy Program źródłowy Analizator składniowy Daj następny symbol leksykalny Tablica symboli
Błędy leksykalne • Błędy wykrywane na etapie analizy leksykalnej: • Niepoprawny ciąg symboli użyty jako identyfikator np.: 123moja_zienna w C/C++ • Użycie nie dozwolonego znaku np.: moja@zmienna w C/C++ • Strategie zachowania po napotkaniu błędu: • Tryb paniki – kasowanie kolejnych znaków, do napotkania pierwszego poprawnego • Wstawienie brakującego znaku • Wymiana złego znaku na poprawny • Zamiana miejscami dwóch sąsiednich znaków
Specyfikacja symboli leksykalnych identyfikator [a-zA-Z_][a-zA-Z0-9]* Pierwszy znak analizowanego ciągu musi być dużą lub małą literą po której następuje zero lub więcej liter lub cyfr • Wzorce zapisujemy jako wyrażenia regularne
Definicje regularne litera A|B|C|…|Z|a|b|c|…|z cyfra 0|1|2|…|9 id litera (litera | cyfra)* Wyrażeniom regularnym można nadać nazwy i używać tych nazw tak jak symboli:
Skróty notacyjne Co najmniej jedno wystąpienie (+) – jednoargumentowy operator +, oznaczający co najmniej jedno wystąpienie poprzedzającego znaku lub wyrażenia regularnego Co najwyżej jedno wystąpienie (?) – jednoargumentowy operator oznaczający zero lub jedno wystąpienie poprzedzającego znaku lub wyrażenia regularnego Dowolna ilość wystąpień (*) – jednoargumentowy operator oznaczający zero lub więcej wystąpień poprzedzającego znaku lub wyrażenia regularnego Klasy znaków – notacja [abc] oznacza skrótowy zapis wyrażenia regularnego a|b|c
Zbiory nieregularne Nie wszystkie języki dają się opisać wyrażeniami regularnymi, np.: wyrażenia regularne nie mogą być użyte do opisu zrównoważonych nawiasów. Wyrażenia regularne mogą jedynie opisać ustaloną liczbę powtórzeń albo dowolną liczbę powtórzeń danej konstrukcji.
Implementacja analizatora leksykalnego Napisanie analizatora w wybranym języku programowania Wykorzystanie jednego z istniejących generatorów analizatorów leksykalnych
Przykład 2+3*5 2 3 5 * + (2+3)*5 2 3 + 5 * ((2+7)/3+(14-3)*4)/2 2 7 + 3 / 14 3 - 4 * + 2 / • Omawiając poszczególne etapy translacji będziemy analizowali kompilator tłumaczący wyrażenia w notacji infiksowej na postfiksową (Odwrotna Notacja Polska), w której operatory występują po swoich argumentach np.: • Zapis ten pozwala na całkowitą rezygnację z użycia nawiasów w wyrażeniach, jako że jednoznacznie określa kolejność wykonywanych działań.
Przykład • Zadania analizatora leksykalnego • Usuwanie znaków odstępu i komentarzy • Analiza stałych • Analizator leksykalny zamienia ciąg symboli w sekwencję par <symbol leksykalny, atrybut leksykalny> np.: 21 + 28 * 29 <liczba, 21> <+, > <liczba, 28> <*, > <liczba, 29> • Rozpoznawanie identyfikatorów i słów kluczowych • Dla identyfikatorów i słów kluczowych atrybut leksykalny oznacza indeks w tablicy symboli licznik = licznik + przyrost <id, 1> <=, > <id, 1> <+, > <id, 2>
Interfejs analizatora leksykalnego Czytaj znak Przekaż symbol leksykalny i jego atrybuty wejście Analizator leksykalny Analizator składniowy Zwróć znak Zwraca symbol leksykalny wywołującemu Używa getchar() do wczytania znaku lexan() analizator leksykalny Zwraca znak c przy użyciu ungetc(c, stdin) Ustawia wartość atrybutu w zmiennej globalnej tokenval
Analizator leksykalny w C intlineno = 1; inttokenval = NONE; intlexan () { int t; while (1) { t = getchar (); if (t == ' ' || t == '\t'); else if (t == '\n') lineno++; else if (isdigit (t)) { tokenval = t – '0'; t = getchar(); while (isdigit(t)) { tokenval = tokenval*10 + t – '0'; t = getchar(); } ungetc (t, stdin); return NUM; } else { tokenval = NONE; return t; } } }
Analizator leksykalny w C intlineno = 1; inttokenval = NONE; intlexan () { int t; while (1) { t = getchar (); if (t == ' ' || t == '\t'); else if (t == '\n') lineno++; else if (isdigit (t)) { tokenval = t – '0'; t = getchar(); while (isdigit(t)) { tokenval = tokenval*10 + t – '0'; t = getchar(); } ungetc (t, stdin); return NUM; } else { tokenval = NONE; return t; } } } Numer aktualnie analizowanego wiersza
Analizator leksykalny w C intlineno = 1; inttokenval = NONE; intlexan () { int t; while (1) { t = getchar (); if (t == ' ' || t == '\t'); else if (t == '\n') lineno++; else if (isdigit (t)) { tokenval = t – '0'; t = getchar(); while (isdigit(t)) { tokenval = tokenval*10 + t – '0'; t = getchar(); } ungetc (t, stdin); return NUM; } else { tokenval = NONE; return t; } } } Atrybut symbolu leksykalnego
Analizator leksykalny w C intlineno = 1; inttokenval = NONE; intlexan () { int t; while (1) { t = getchar (); if (t == ' ' || t == '\t'); else if (t == '\n') lineno++; else if (isdigit (t)) { tokenval = t – '0'; t = getchar(); while (isdigit(t)) { tokenval = tokenval*10 + t – '0'; t = getchar(); } ungetc (t, stdin); return NUM; } else { tokenval = NONE; return t; } } } Pobierz jeden znak
Analizator leksykalny w C intlineno = 1; inttokenval = NONE; intlexan () { int t; while (1) { t = getchar (); if (t == ' ' || t == '\t'); else if (t == '\n') lineno++; else if (isdigit (t)) { tokenval = t – '0'; t = getchar(); while (isdigit(t)) { tokenval = tokenval*10 + t – '0'; t = getchar(); } ungetc (t, stdin); return NUM; } else { tokenval = NONE; return t; } } } Jeżeli t jest białym znakiem, pomiń znak
Analizator leksykalny w C intlineno = 1; inttokenval = NONE; intlexan () { int t; while (1) { t = getchar (); if (t == ' ' || t == '\t'); else if (t == '\n') lineno++; else if (isdigit (t)) { tokenval = t – '0'; t = getchar(); while (isdigit(t)) { tokenval = tokenval*10 + t – '0'; t = getchar(); } ungetc (t, stdin); return NUM; } else { tokenval = NONE; return t; } } } Jeżeli t jest znakiem nowej linii, zwiększ numer aktualnie analizowanego wiersza
Analizator leksykalny w C intlineno = 1; inttokenval = NONE; intlexan () { int t; while (1) { t = getchar (); if (t == ' ' || t == '\t'); else if (t == '\n') lineno++; else if (isdigit (t)) { tokenval = t – '0'; t = getchar(); while (isdigit(t)) { tokenval = tokenval*10 + t – '0'; t = getchar(); } ungetc (t, stdin); return NUM; } else { tokenval = NONE; return t; } } } Jeżeli t jest liczbą, zamień napis na wartość typu całkowitego
Analizator leksykalny w C intlineno = 1; inttokenval = NONE; intlexan () { int t; while (1) { t = getchar (); if (t == ' ' || t == '\t'); else if (t == '\n') lineno++; else if (isdigit (t)) { tokenval = t – '0'; t = getchar(); while (isdigit(t)) { tokenval = tokenval*10 + t – '0'; t = getchar(); } ungetc (t, stdin); return NUM; } else { tokenval = NONE; return t; } } } Wczytany znak jest nieznanym symbolem
Tablica symboli int insert(const char* s, int t); /* zwraca indeks w tablicy symboli dla nowego leksemu s i tokenu t */ int lookup(const char* s); /* zwraca indeks wpisu dla leksemu s lub 0 gdy nie znaleziono */ insert("div", DIV); insert("mod", MOD); Tablica symboli jest wykorzystywana do przechowywania zmiennych oraz słów kluczowych Jest inicjalizowana poprzez wstawienie do niej słów kluczowych:
Analizator leksykalny z tablicą symboli intlexan () { int t; while (1) { t = getchar (); if (t == ' ' || t == '\t'); elseif (t == '\n') lineno++; elseif (isdigit (t)) { ungetc (t, stdin); scanf ("%d", &tokenval); return NUM; } elseif (isalpha (t)) { int p, b = 0; while (isalnum (t)) { lexbuf[b] = t; t = getchar (); b++; if (b >= BSIZE) error ("compilererror"); } lexbuf[b] = EOS; if (t != EOF) ungetc (t, stdin); p = lookup (lexbuf); if (p == 0) p = insert (lexbuf, ID); tokenval = p; return symtable[p].token; } else if (t == EOF) return DONE; else { tokenval = NONE; return t; } } }
Analizator leksykalny z tablicą symboli intlexan () { int t; while (1) { t = getchar (); if (t == ' ' || t == '\t'); elseif (t == '\n') lineno++; elseif (isdigit (t)) { ungetc (t, stdin); scanf ("%d", &tokenval); return NUM; } elseif (isalpha (t)) { int p, b = 0; while (isalnum (t)) { lexbuf[b] = t; t = getchar (); b++; if (b >= BSIZE) error ("compilererror"); } lexbuf[b] = EOS; if (t != EOF) ungetc (t, stdin); p = lookup (lexbuf); if (p == 0) p = insert (lexbuf, ID); tokenval = p; return symtable[p].token; } else if (t == EOF) return DONE; else { tokenval = NONE; return t; } } } Jeżeli t jest identyfikatorem
Analizator leksykalny z tablicą symboli intlexan () { int t; while (1) { t = getchar (); if (t == ' ' || t == '\t'); elseif (t == '\n') lineno++; elseif (isdigit (t)) { ungetc (t, stdin); scanf ("%d", &tokenval); return NUM; } elseif (isalpha (t)) { int p, b = 0; while (isalnum (t)) { lexbuf[b] = t; t = getchar (); b++; if (b >= BSIZE) error ("compilererror"); } lexbuf[b] = EOS; if (t != EOF) ungetc (t, stdin); p = lookup (lexbuf); if (p == 0) p = insert (lexbuf, ID); tokenval = p; return symtable[p].token; } else if (t == EOF) return DONE; else { tokenval = NONE; return t; } } } Jeżeli t jest znakiem końca pliku
Generator analizatorów leksykalnych - flex Program źródłowy w języku flex - lex.l Kompilator flexa lex.yy.c Kompilator C a.out lex.yy.c Sekwencja symboli leksykalnych Strumień wejściowy a.out Generuje kod analizatora na podstawie zadanej specyfikacji Domyślnie analizator jest napisany w języku C Wygenerowany kod źródłowy kompilujemy jako samodzielny program lub moduł programu. yylex() – funkcja wygenerowana przez LEX-a odpowiedzialna za działanie leksera (można ją wykorzystać w innej aplikacji).
Podstawy działania flexa Niedopasowane znaki są przepisywane na wyjście Operacja pusta = reguła składająca się tylko ze wzorca Wzorce zawierające spacje ujęte muszą być w cudzysłów Znaki specjalne poprzedzone muszą być znakiem \ (backslash) Komentarze oznaczamy jak w języku c ( /* */ )
Format pliku konfiguracyjnego Sekcja definicji %% Sekcja reguł przetwarzania, gdzie reguła składa się z dwóch części: wyrażenia regularnego oraz kodu w C %% Sekcja kodu dodatkowego
Specyfikacja dla flexa - przykłady Wzorce %{ int num_lines = 0; num_chars = 0; int_nr = 0; %} %% \n {++num_lines; ++num_chars;} int {++int_nr;} . {++num_chars;} %% intmain() { yylex(); printf( "# of lines = %d, # of chars = %d\n",num_lines, num_chars ); } Operacje
Specyfikacja dla flexa - przykłady sekcja kodu użytkownika - kod bezpośrednio przepisany na początek pliku wyjściowego %{ int num_lines = 0; num_chars = 0; int_nr = 0; %} %% \n {++num_lines; ++num_chars;} int {++int_nr;} . {++num_chars;} %% intmain() { yylex(); printf( "# of lines = %d, # of chars = %d\n",num_lines, num_chars ); } kod bezpośrednio przepisany na koniec pliku wyjściowego
Specyfikacja dla flexa - przykłady #include <stdio.h> signed int foo1(int); int foo2(void); int i; unsigned char c; %{ #include <stdio.h> intyylex(); %} %% ”signedint” {printf(”int”);} ”unsigned char” {printf(”char”);} %% intmain() { return yylex(); } #include <stdio.h> int foo1(int); int foo2(void); int i; char c;
Specyfikacja dla flexa - przykłady %{ #include <math.h> %} DIGIT [0-9] ID [a-z][a-z0-9]* %% {DIGIT}+ { printf( "An integer: %s (%d)\n", yytext,atoi( yytext ) ); } {DIGIT}+"."{DIGIT}* { printf( "A float: %s (%g)\n", yytext, atof( yytext ) ); } if|then|begin|end|procedure|function { printf( "A keyword: %s\n", yytext ); } {ID} { printf( "An identifier: %s\n", yytext ); } "+"|"-"|"*"|"/" {printf( "An operator: %s\n", yytext );} "{"[\^{}}\n]*"}“ [ \t\n]+ . {printf( "Unrecognized character: %s\n", yytext );} %% intmain(intargc, char* argv[] ) { ++argv, --argc; if ( argc > 0 ) yyin = fopen( argv[0], "r" ); else yyin = stdin; yylex(); }
Wyrażenia regularne flexa Następujące operatory jezyka flex mają specjalne znaczenie: \” . ^ $ [ ] * + ? { } | / Aby przywrócić im znaczenie znaku (a nie operatora), w wyrażeniach regularnych muszą być poprzedzone znakiem \ (backslash).
Wyrażenia regularne, przykłady • Założenia dla wzorca: • - nazwa użytkownika składa się z małych i wielkich liter, cyfr, kropki, podkreślenia i znaku minus, • - nazwa hosta składa się z dwóch do czterech części oddzielonych kropką, • - pierwsza część nazwy hosta składa się z małych i wielkich liter, cyfr, podkreślenia i znaku minus, • pozostałe część nazwy hosta składają się z małych i wielkich liter. • [A-Za-z0-9._-]+@[A-Za-z0-9_-]+(\.[A-Za-z]+){1,3}
Definicje regularne %{ #include <stdio.h> intyylex(); voidon_ID_found(); voidon_NUM_INT_found(); voidon_NUM_FLOAT_found(); %} ID [a-zA-Z_][a-zA-Z0-9_]* NUM_INT [0-9]+ NUM_FLOAT {NUM_INT}”.”{NUM_INT}* %% {ID} {on_ID_found();} {NUM_INT} {on_NUM_INT_found();} {NUM_FLOAT} {on_NUM_FLOAT_found();} %% voidon_ID_found() { /* */ } voidon_NUM_INT_found() { /* */ } voidon_NUM_FLOAT_found() { /* */ }
Zmienne globalne flexa ID [a-zA-Z_][a-zA-Z0-9_]* %% {ID} {printf(”Identyfikator: %s o dlugosci %d znakow”, yytext, yyleng);} . {printf(”Nieznany leksem: %s o dlugosci %d znakow”, yytext, yyleng);} %% int main(int argc, char * argv[]) { if (argc > 1) yyin = fopen( argv[1], "r" ); else yyin = stdin; return yylex(); }
Dopasowywanie ciągów |aaaaaa| > |aaa| Zasada najdłuższego dopasowania. aaaaaa aaa %{ #include <stdio.h> int yywrap(); int yylex(); %} %% aaa {printf(”F”);} a* {printf(”T”);} %% int main() { return yylex(); } T F
Dopasowywanie ciągów |aaa| = |aaa| Zasada wcześniejszego dopasowania. aaa aaa %{ #include <stdio.h> intyywrap(); intyylex(); %} %% aaa {printf(”F”);} a* {printf(”T”);} %% intmain() { return yylex(); } F F
Zwracanie wartości z yylex() %{ #include <stdio.h> intyylex(); #defineKW_begin 300 #defineKW_end 301 #define ID 302 %} %% [Bb][Ee][Gg][Ii][Nn] return KW_begin; [Ee][Nn][Dd] return KW_end; ”(” return '('; ”)” return ')'; ”.” return '.'; [a-zA-Z_]+[a-zA-Z0-9_]* return ID; . %% intmain() { intsymb_leks; while(symb_leks = yylex()) printf(”Znaleziono symbol leksykalny o identyfikatorze %d\n”, symb_leks); return 1; }