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.

Podobne dokumenty