Analizator leksykalny i składniowy
Transkrypt
Analizator leksykalny i składniowy
Semantyka i Weryfikacja Programów - Laboratorium 6 Analizator leksykalny i składniowy - kalkulator programowalny Cel. Przedstawienie zasad budowy i działania narzędzi do tworzenia kompilatorów języków wysokiego poziomu na przykładzie analizatora wyrażeń arytmetycznych – kalkulatora programowalnego.. Wprowadzenie Na definicję języka programowania składają się następujące elementy: • struktura leksykalna – syntaktyka (stałe arytmetyczne, słowa kluczowe, identyfikatory, operatory), opisywana przez wyrażenia regularne (regular expression), • gramatyka – struktura wyrażeń, deklaracji, definicji, programu ..., opisywana przez gramatykę bezkontekstową (context-free grammar), • sposób obliczania wyrażeń i wykonywania instrukcji.. Program najpierw jest sprawdzany pod względem leksykalnym i składniowym, a następnie tworzy się kod wykonywalny lub pośredni zależny od typu procesora i środowiska operacyjnego. Tym ostatnim zagadnieniem zajmuje się semantyka operacyjna. Przykład - kalkulator. Jako przykład posłuży dalej program kalkulatora arytmetycznego wykonującego dodawanie, odejmowanie, mnożenie i dzielenie w dziedzinie liczb całkowitych. Dopuszcza się też stosowanie nawiasów. Wyrażenie arytmetyczne podaje się w postaci łańcucha, np.: 2+3*4 Podobne zagadnienie było prezentowane na dwóch poprzednich laboratoriach. Narzędziem realizacyjnym był język ML. Wyrażenie podawane było w postaci symbolicznej, co stanowiło pewną niedogodność. Teraz zostanie opracowany w pełni funkcjonalny program translatora. Przekształcenia wyrażenia podanego w postaci tekstowej przebiegają w dwóch etapach: • lekser (zwany skanerem) wyodrębnia jednostki leksykalne zwane tokenami: tu są to stałe całkowite, nawiasy i operatory , • parser (analizator składniowy) buduje z pobieranych tokenów drzewo syntaktyczne pokazane na rys. 1. + * 3 2 4 Rys. 1. Drzewo syntaktyczne wyrażenia 2+3*4 Na podstawie drzewa syntaktycznego kompilator generuje kod wykonywalny np. w pliku *.exe. Możliwa jest także praca w trybie interpretera, jak to pokazano na rys. 2. Odbywa się to podobnie jak przy pracy z interpreterem MosML. Przykład c.d. W podanym wyżej przykładzie na podstawie drzewa syntaktycznego pokazanego na rys. 1 tworzona jest reprezentacja wyrażenia zapisana w odwrotnej notacji polskiej 3,4,*,2,+ wykonywana za pomocą prostego programu interpretera. wejście program analizator leksykalny LEXER jednostki leksykalne analizator składniowy PARSER drzewo syntaktyczne interpreter Rys. 2. Zasada tłumaczenia i wykonywania programu wyjście Analizatory leksykalne i składniowe (Lexer i Parser) są budowane dla wielu języków programowania. Powstały programy narzędziowe do ich tworzenia dostępne w językach C, JAVA, ML i innych. Rysunek 3 przedstawia zasadę przetwarzania plików za pomocą narzędzi dostarczanych w pakiecie MosML. definicje jednostek leksykalnych *.lex mosmllex analizator leksykalny *.sml treść programu, dane interpreter definicja składni *.grm mosmlyac analizator składniowy *.sml wyniki Rys. 3. Zasada tworzenia interpretera języka programowania w pakiecie MosML pracującego w trybie interaktywnym Użytkownik przygotowuje pliki źródłowe z definicjami jednostek leksykalnych i składni (zwykle są identyczne dla różnych implementacji języka i zgodne z odpowiednią normą ANSI). Po ich przetworzeniu przez programy mosmllex.exe i mosmyac.exe otrzymuje się analizatory w plikach z rozszerzeniem *.sml. Pliki te są można dołączyć w trybie interaktywnym (interpretera) pakietu MosML wywołując zawarte w nich funkcje i wykonując zależne od implementacji akcje semantyczne.. Szczegóły takiego postępowania podano niżej. Wyrażenia regularne (regular expressions) Jednostki leksykalne wygodnie jest definiować jako wyrażenia regularne. Typowe elementy w językach wysokiego poziomu można nieformalnie zdefiniować jako: • liczba całkowita – ciąg cyfr o dowolnej długości, może być poprzedzona znakiem ‘-‘ • identyfikator – ciąg liter i cyfr rozpoczynający się od litery • słowo kluczowe – podobnie jak identyfikator, podawane w postaci zbioru skończonego Do budowania wyrażeń regularnych stosuje się specjalnej notacji z symbolami i operatorami podanymi w tablicy 1. Tabl. 1. Symbole stosowane w definiowaniu jednostek leksykalnych symbol opis przykład 'a' znak ‘a’, ‘(‘, ‘:’ ”łańcuch” konkretny łańcuch ”while”, ”let”, ”::”, ”>=” [‘a’ ‘b’ ‘c’] jedna z wymienionych liter [‘a’-‘z’ ‘0’ – ‘9’ ] litera lub cyfra z zakresu + symbol występuje co najmniej raz [0-9]+ ? opcjonalne wystąpienie [-+]?[0-9]+ liczba ze znakiem (opcja) * występuje wielokrotnie lub wcale eof, eol koniec pliku, linii liczba całkowita bez znaku Dla każdego wyrażenia regularnego istnieje automat skończenie stanowy, co pozwala zautomatyzować proces tworzenia skanera na podstawie definicji. Przykład – kalkulator c.d. Definicje dla generatora jednostek leksykalnych są następujące (symbole reprezentujące jednostki leksykalne – PLUS, MINUS, TIMES, itd. zostały zdefiniowane w przedstawionym dalej analizatorze składniowym): { val intOfString = valOf o Int.fromString; } rule Token = parse [` ` `\t`] | [`\n` ] | eof | [`0`-`9`]+ | `+` | `-` | `*` | `(` | `)` | `~` | _ ; { { { { { { { { { { { Token lexbuf } (* pomin odstępy *) EOL } (* koniec linii *) EOF } (* koniec pliku *) INT(intOfString (getLexeme lexbuf)) } PLUS } (* zwroc symbolicznie PLUS MINUS } TIMES } LPAREN } (* lewostronny nawias *) RPAREN } UMINUS } raise Fail "illegal symbol" } Z lewej strony podano zbiór definicji wyrażeń regularnych: odstępy, znak nowej linii i końca pliku, liczbę całkowitą i operatory. W nawiasach klamrowych podano wartość symboliczną zwracaną przez skaner po rozpoznaniu wyrażenia. Należy tu wyjaśnić ważniejsze elementy związane z językiem ML. Token: fn: lexbuf -> token - nazwa funkcji głównej utworzonej na podstawie podanych definicji, to ona zwraca kolejne rozpoznane w tekście jednostki leksykalne, czyli tokeny. Specjalne typy lexbuf i token zdefiniowano w pliku Lexing.sig w katologu lib pakietu MosML. W pierwszym wariancie funkcja Token napotykając znak odstępu przechodzi do dalszej analizy nic nie zwracając. W drugim i trzecim rozpoznawane są znaki końca linii i pliku. W czwartym (liczba całkowita) dana typu lexbuf przekształcana jest najpierw do typu string przez funkcję biblioteczną getLexeme, a następnie do typu int przez intOfString zdefiniowaną następująco: val intOfString = valOf o Int.fromString; Składnia wyrażeń Składają się na nią: • symbole terminalne: identyfikatory, stałe, czyli tokeny generowane przez lexer, • symbole nieterminalne, służące do denotacji klas, • symbol startowy, który jest nieterminalny • reguły lub inaczej produkcje do definicji symboli. Zilustruje to przykład definicji gramatyki kalkulatora: Main ::= Expr EOL Expr::= INT | LPAREN Expr RPAREN | Expr PLUS Expr | Expr MINUS Expr | Expr TIMES Expr | UMINUS Expr Symbolem startowym i zarazem terminalnym jest tu Main, elementy terminalne to wcześniej zdefiniowane jednostki leksykalne (liczby, operatory nawiasy). Symbolem nieterminalnym opisującym budowę wyrażenia jest Expr. Przyjęto, że „program” zawarty jest w łańcuchu lub wprowadzany z klawiatury. Składa się on z wyrażenia Expr i znaku końca linii. Definicja wyrażenia Expr jest rekurencyjna. Może to być liczba całkowita, wyrażenie w nawiasach, operacja dodawania, odejmowania, mnożenia lub dzielenia albo zmiany znaku. Przy definiowaniu gramatyki istotne jest podanie łączności i priorytetu operatorów. Realizacja w pakiecie MosML. Na podstawie powyższych definicji zapisanych odpowiednio w pliku źródłowym zwykle z rozszerzeniem *.grm analizator składniowy generowany jest automatycznie przez program mosmlyac.exe. Format pliku źródłowego podzielonego na sekcje znakami %{, }% i %% jest następujący: %{ }% (* nagłówek – funkcje w języku ML*) deklaracje tokenów i symboli %% reguły produkcji dla symboli %% Deklaracje podawane są po jednej w linii. Zaczynają się od znaku %. Mogą one mieć postać: • %token symbol...symbol – deklaracja tokenów (symboli terminalnych) bezargumentowych. • %token <type> symbol...symbol deklaracja symboli jw., ale o podanym typie, przykładem jest deklaracja: %token <int> INT, w której symbol INT, użyty w deklaracji jednostek leksykalnych „zwraca” liczbę typu int, • %start symbol – symbol (może ich być kilka) startowy gramatyki, • • • %left symbol – symbol (zwykle operator) lewostronnie łączny, %right symbol – prawostronnie łączny, %nonassoc – niełączny (zwykle jednoargumentowy), Kolejność definicji symboli decyduje o ich priorytecie. Reguły mają postać podobną do notacji BNF: terminal_symbol: symbol ...symbol { (* akcja semantyczna *)} | symbol ...symbol { (* akcja semantyczna *)} ... ; albo: nonterminal_symbol: symbol ...symbol { (* akcja semantyczna *) | symbol ...symbol { (* akcja semantyczna *)} ... ; Akcja semantyczna to wyrażenie w języku ML. Stosuje się w nim umowne oznaczenie symboli występujących w deklaracji reguły, co zostanie wyjaśnione na przykładzie poniżej. Definicja składni kalkulatora jest zatem następująca: %token %token %token %token <int> INT PLUS MINUS TIMES UMINUS LPAREN RPAREN EOL EOF %left PLUS MINUS %left TIMES %nonassoc UMINUS /* symbol z podanym typem */ /* operatory */ /* nawiasy */ /* koniec linii, pliku */ /* najnizszy priorytet */ /* średni priorytet */ /* najwyzszy priorytet */ %type <int> Expr /* symbol Expr jest typu int */ %start Main %type <int> Main /* punkt startowy */ /* typ symbolu */ %% Main: Expr EOL ; Expr: INT | LPAREN Expr RPAREN | Expr PLUS Expr | Expr MINUS Expr | Expr TIMES Expr | UMINUS Expr ; { $1 } { { { { { { $1 } $2 } $1 + $1 $1 * ~ $2 $3 } $3 } $3 } } W pierwszej części pliku podano deklaracje tokenów, które są zwracane przez analizator leksykalny. Prawie wszystkie są bez podanego typu. Dla tokenu INT podano typ int. Analizator leksykalny wyodrębniwszy liczbę całkowitą zwróci symbolicznie INT oraz wartość arytmetyczną liczby. W języku ML tokeny są to konstruktory bezargumentowe lub z argumentem (tutaj int). W drugiej części podano typy symboli oraz symbol terminalny (startowy) gramatyki. Po znakach %% znajduje się definicja gramatyki z podanymi akcjami semantycznymi jako wyrażenia w języku ML. Użyto tu specjalnego symbolu $ z podanym numerem. Np $1 reprezentuje wartość pierwszego symbolu w danej produkcji (linii definicji). W definicji INT { $1 } wartością wyrażenia jest liczba typu int (dla symbolu INT zdefiniowano wcześniej typ). W kolejnej definicji | Expr PLUS Expr { $1 + $3 } sumy wyrażeń symbole Expr (oznaczone dalej $1 i $3, bo są pierwszym i trzecim elementem definicji) są typu int i w akcji semantycznej kalkulatora należy je dodać. Praca w trybie interaktywnym. Programy pakietu MosML powstały do pracy w trybie tekstowym. Dla wygody pracy w systemie Windows przygotowano katalog o nazwie calc z następującą zawartością: • mosmllex.exe, mosmlyac.exe – programy narzędziowe przekopiowane z katalogu /bin pakietu MosML • Lexer.lex, Parser.grm – definicje jednostek leksykalnych i składniowych • calc.sml – plik interpretera (opisany dalej) • yacc.bat, lex.bat – pliki wsadowe systemu DOS do kompilacji źródeł. Kompilacja. Celem uruchomienia interpretera należy: • Utworzyć plik Parser.sml za pomocą programu mosmlyac.exe uruchamiając plik wsadowy yacc.bat. • Utworzyć plik Lexer.sml kompilując plik Lexer.lex za pomocą lex.bat (wywołującej program mosmllex.exe). • Uruchomić VisualML. • Otworzyć plik calc.sml. Do wykorzystania powstałych analizatorów napisano następujący plik główny interpretera calc.sml. load "BasicIO"; load "Nonstdio"; load "Lexing"; load "Parsing"; load "Int"; use "Parser.sml"; use "Lexer.sml"; (* dołączenie bibliotek *) (* plik analizatora składniowego *) (* plik analizatora leksykalnego *) (*definicja wyrażenia ze znakiem końca linii *) val wyraz = " (2*3+4)-(7*2)\n"; (* utworzenie danej typu lexbuf z łańcucha *) val lexbuf = Lexing.createLexerString wyraz; (* string -> lexbuf *) (* wywołując wielokrotnie funkcję Token otrzymujemy kolejne rozpoznane jednostki leksykalne *) Token lexbuf; Token lexbuf; Token lexbuf; (* itd. *) (* ponowne utworzenie danej lexbuf *) val lexbuf = Lexing.createLexerString wyraz; (* string -> lexbuf *) (* automatyczne obliczenie wyrażenia *) val result:int = Main Token lexbuf; Wyżej podano komentarze. Na uwagę zasługuje szczególnie ostatnia linia. Pojawia się tu funkcja Main zawarta w pliku Parser.sml utworzona na podstawie definicji symbolu startowego w pliku Parser.sml. Funkcja Token z pliku Lexer.sml powstała na podstawie definicji rule Token = ... z pliku Lexer.lex. Przebieg ćwiczenia 1. Pobierz ze strony przedmiotu katalog calc z zawartymi w nim plikami. 2. Wykonaj kompilację plików źródłowych za pomocą „plików wsadowych” yacc.bat i lex.bat. 3. Uruchom środowisko VisualML. 4. Wczytaj plik calc.sml. 5. Wywołując wielokrotnie funkcję Token sprawdź, czy jednostki leksykalne pobierane są prawidłowo. 6. Policz inne wyrażenie arytmetyczne. Zadania do samodzielnego wykonania 1. Wprowadź do programu kalkulatora operator dzielenia. Wskazówka: najpierw zdefiniuj token DIV w pliku parser.grm, następnie zapisz dla niego akcję semantyczną oraz zdefiniuj w pliku lexer.lex operator dzielenia. 2. Zmodyfikuj zabezpiecz funkcję dzielenia przed możliwością wystąpienia błędu stosując konstrukcję handle. 3. Zmodyfikuj program kalkulatora tak, by wykonywał obliczenia na liczbach rzeczywistych. Tworzenie pliku wykonywalnego Wyrażenia można przetwarzać nie tylko w trybie interaktywnym, ale także za pomocą programu wykonywalnego otrzymanego za pomocą kompilatora języka ML – mosmlc. W tym przypadku należy przygotować pliki źródłowe zastępując instrukcje load i use przez open, czyli dołączając moduły wstępnie skompilowane. Plik z definicjami jednostek leksykalnych lexer.lex jest następujący: { open Parser; open Nonstdio; val intOfString = valOf o Int.fromString; } rule Token = parse [` ` `\t`] { Token lexbuf } (* skip blanks *) ... dotychczasowe deklaracje ; W pliku definicji składni – parser.grm żadne zmiany nie są konieczne. Główny plik calc.sml po modyfikacji ma postać: open open open open BasicIO; Lexing; Int; Lexer; open Nonstdio; open Parsing; open Parser; val wyraz = " (2*3+4)-(7*2)\n"; val lexbuf = Lexing.createLexerString wyraz; (* string -> lexbuf *) val result:int = Main Lexer.Token lexbuf; val _ = print (Int.toString result); Wywołując funkcje, należy podawać podobnie jak dla modułów bibliotecznych w którym module występują. Program wyprowadza wynik obliczeń na monitor. Przebieg kompilacji. Polecenia wykonywane w trybie MSDOS, np. w środowisku programu Norton Commander w sposób podany w tabeli. polecenie mosmlyac parser.grm rezultat parser.sig parser.sml mosmllex lexer.lex lexer.sml mosmlc –c parser.sig parser.ui mosmlc –c parser.sml parser.uo mosmlc –c lexer.sml lexer.ui lexer.uo mosmlc calc.sml mosmlout.exe Plik mosmlout.exe (nazwę można zmienić) jest plikiem wykonywalnym – programem nie wymagającym interpretera MosML. Polecenia do samodzielnego wykonania 1. Zdefiniować jednostki leksykalne: adresy stron www, adresy kont email, rejestracje samochodowe. 2. 3. Przekształć plik calc.sml tak, by wyrażenia były pobierane z pliku – wykorzystanie funkcji bibliotecznych 4. Utwórz kalkulator logiczny ze stałymi t i f i operatorami jak w języku C . 5. Wprowadź instrukcje umożliwiające diagnostykę błędów syntaktycznych w podawanym wyrażeniu.