• Nie Znaleziono Wyników

Analiza leksykalna

N/A
N/A
Protected

Academic year: 2021

Share "Analiza leksykalna"

Copied!
8
0
0

Pełen tekst

(1)

Analiza leksykalna

Marcin Orchel

1 Wstęp

1.1 Kompilatory

Kompilator to program komputerowy, który przekształca kod jednego języka kompute- rowego do drugiego. Kompilator najczęściej przekształca język do programu, który moż- na wykonać. Kompilator najczęściej przekształca program z języka wyższego poziomu do niższego poziomu. Self-hosting compiler - kompilator, który został napisany w języ- ku, który kompiluje. Problem kompilacji takiego kompilatora nazywa się bootstrapping.

Kompilator może być skompilowany kompilatorem napisanym w innym języku lub może być skompilowany kompilatorem powstałym dla innej architektury za pomocą kompilacji skrośnej (cross-compilation) lub może być skompilowany ręcznie. Kompilator natywny - programy będą uruchamiane na tej samej maszynie, co kompilator. Kompilator składa się z:

• front end: syntaktyczne i semantyczne przetwarzanie z przekształceniem do repre- zentacji przejściowej

• middle end: optymalizacja reprezentacji przejściowej

• back end: optymalizacja pod konkretną maszynę, generacja kodu maszynowego Front end. Zadania front end to

• analiza leksykalna, czyli podział kodu źródłowego na tokeny

• preprocessing - włączanie makr w C

• analiza syntaktyczna, czyli identyfikacja struktury programu, na wyjściu drzewo tokenów

• analiza semantyczna, czyli dodanie informacji semantycznych do drzewa tokenów, sprawdzanie typów, sprawdzanie powiązania zmiennych i referencji do funkcji z ich definicjami, sprawdzanie czy wszystkie możliwe zmienne zostały zainicjalizowane Analiza leksykalna. Dla danego źródła analizator leksykalny zwraca listę tokenów.

Podział na tokeny może być zapisany za pomocą wyrażeń regularnych. Wyrażenia re-

gularne mogą być przekształcone do automatów skończonych, co pozwala na wygodne

rozpoznawanie tokenów. Token składa się z typu i wartości. Wartość jest nazywana lek-

semem. Przykład tokenów w wyrażeniu sum = 3 + 2

(2)

• sum IDENT

• = SAVE_OP

• 3 NUMBER

• + ADD_OP

• 2 NUMBER

Następujące wyrażenia regularne mogą być użyte do rozpoznania tych tokenów:

• = SAVE_OP

• + ADD_OP

• [0 − 9]

NUMBER

• [a − z]

IDENT

Rozpoznawanie tokenów we flexie. Tokeny są zdefiniowane za pomocą wyrażeń re- gularnych. Symbole czytane są z wejścia. Wybierany jest najdłuższy leksem, do którego pasuje co najmniej jedno wyrażenie regularne. Jeśli pasuje kilka wyrażeń, to wybierane jest to, które zostało zdefiniowane wcześniej w skrypcie. Jeśli np. mamy dwa wyrażenia regularne

1. begin - SLOWO_KLUCZOWE 2. [a − z]

+

- IDENTYFIKATOR

i mamy do przeczytania słowo beginner, to najdłuższy leksem to beginner, i pasuje do drugiego wyrażenia regularnego, a więc jest to IDENTYFIKATOR.

Analiza syntaktyczna. Struktura programu może być zapisana za pomocą gramatyki, w której terminalami są tokeny, słowami tej gramatyki są ciągi tokenów czyli popraw- nie zdefiniowane programy. Parsowanie polega na rozpoznaniu czy dany program należy do języka zdefiniowanego za pomocą danej gramatyki, a więc czy jest poprawnie zdefi- niowanym programem. Na wyjściu parsowania zwracane jest również drzewo rozbioru.

Podczas interpretacji programu przechodzimy przez drzewo rozbioru dla podanego pro- gramu i stosujemy operacje, które zostały zdefiniowane dla poszczególnych produkcji.

W programie Bison używany jest parser typu LR. Gramatyka dla podanego przykła- du z sumowaniem może wyglądać następująco:

1. S → IDEN T SAV E_OP T

2. T → N U M BER ADD_OP N U M BER

Podczas parsowania LR najpierw stosujemy drugą produkcję, a następnie pierwszą. Do każdej produkcji przypisane są odpowiednie operacje.

Back end. Zadania back end, m.in. optymalizacja kodu taka jak eliminacja martwego kodu, propagacja stałych, automatyczne zrównoleglanie.

http://www.angelfire.com/dragon/letstry/tutorials/compiler/index.html

(3)

1.2 Lekser

Analizator leksykalny jest wywoływany przez parser za pomocą polecenia getN extT oken.

Analizator odczytuje kolejne znaki z wejścia aż zidentyfikuje leksem i utworzy token.

Token jest parą uporządkowaną złożoną z nazwy tokenu i opcjonalnej wartości atry- butu. Wzorzec jest opisem formy, którą mogą przyjmować leksemy tokenu. Leksem jest sekwencją znaków w programie źródłowym, która pasuje do wzorca tokenu i jest iden- tyfikowana przez analizator leksykalny jako wystąpienie tego tokenu. Przykładowo dla tokenu number mamy leksemy 1,2,3, 3.14. W programach mamy następujące tokeny

• tokeny dla słów kluczowych

• tokeny dla poszczególnych operatorów lub klas operatorów (np. comparison)

• jeden token dla wszystkich identyfikatorów

• tokeny dla stałych

• tokeny dla znaków interpunkcyjnych jak nawiasy

Dla tokeny id wartością atrybutu jest wskaźnik do wpisu tablicy symboli.

1.3 Lex

Narzędzie Lex pozwala na specyfikację wzorców tokenów jako wyrażeń regularnych. Plik lex.1 jest przekształcany w plik lex.yy.c przez kompilator Lex. Program w Lex ma na- stępującą postać:

deklaracje

%%

reguły translacji

%%

funkcje pomocnicze. Sekcja deklaracji zawiera deklaracje zmiennych, identyfikatorów oznaczających stałe, czyli nazwy tokenów i definicji regularnych. Reguły translacji mają postać Wzorzec {Akcja}. Każdy wzorzec jest wyrażeniem regularnym, które może uży- wać definicji regularnych. Akcje są fragmentami kodu w C. Trzecia sekcja przechowuje dodatkowe funkcje, które są używane w akcjach.

Lex wykonuje wczytywanie pozostałego wejścia po jednym znaku, dopóki nie znaj- dzie w wejściu najdłuższego prefiksu pasującego do jednego ze wzorców P

i

. Następnie wykonuje powiązaną akcję A

i

. Akcja A

i

zazwyczaj zwraca znaleziony token. Lex zwraca do parsera nazwę tokenu, a dodatkowe informacje o leksemie może zwracać w zmiennej globalnej yylval.

W deklaracjach mogą pojawić się nawiasy %{. . . %}. Kod ten jest kopiowany bez- pośrednio do pliku C. Mogą tutaj znajdować się definicje stałych manifestu z instruk- cją #def ine. Dla definicji regularnych używamy rozszerzonego zapisu wyrażeń regu- larnych. Definicje używane w późniejszych wzorcach są ujęte w nawiasy klamrowe, np.

delim [ \t\n], ws {delim}+.

(4)

Przetestować użycie symboli +, ∗, ? w ich własnym znaczeniu.

Cała część sekcji funkcji pomocniczych jest kopiowana do pliku lex.yy.c.

Identyfikatorowi może być przyporządkowana pusta akcja. {ws}{}. Dla białych zna- ków nie zwracamy nic do parsera, tylko dalej szukamy innego leksemu.

Mamy regułę if {return(IF ); }. Zwracana jest liczba całkowita odpowiadająca stałej manifestu IF .

Lex wybiera ten wzorzec, który jest wymieniony wcześniej w przypadkach, gdy naj- dłuższy pasujący prefiks pasuje do dwóch lub więcej wzorców.

Dla reguły {id}{yylval = (int)installID(); return(ID); } funkcja installID umiesz- cza znaleziony leksem w tablicy symboli i zwraca wskaźnik do tablicy symboli, który jest umieszczany w globalnej zmiennej yylval. Funkcja ta korzysta z dwóch wskaźników yytext do pierwszego znaku leksemu i yyleng długości leksemu.

Dla reguły {number}{yylval = (int)installN um(); return(N U M BER); } mamy oddzielną tablicę ze stałymi numerycznymi.

Inny przykład " < " {yylval = LT ; return(RELOP ); }.

1.3.1 Rozwiązywanie konfliktów w Lex

Gdy wiele wzorców pasuje do danego prefiksu stosowane są reguły 1. Zawsze preferowany jest dłuższy pasujący prefiks.

2. Jeśli najdłuższy możliwy prefiks pasuje do dwóch lub więcej wzorców, preferowany jest ten wzorzec, który jest wymieniony wcześniej w programie Lex.

1.3.2 Operator prawego kontekstu

Lex czyta znak po znaku, jeśli po wczytaniu znaku rozpozna leksem kończący się na wcześniejszym znaku, to cofa się o jedną pozycję i zwraca leksem.

Dodać do analizatora operator prawego kontekstu. Gdy chcemy dopasować wzorzec tylko wtedy, gdy są po nim inne znaki, używamy ukośnika. Wszystko po ukośniku to jest dodatkowy wzorzec, ale nie jest on zwracany w leksemie. Przykładowo IF / \(. ∗

\){letter}.

1.3.3 PLY

Implementacja lexa w Pythonie. Dokumentacja https://www.dabeaz.com/ply/ply.

html. Lexer jest zaimplementowany w pliku lex.py. Każda reguła rozpoczyna się od prefixu t_, t od token, przykładowo t_PLUS=r’\+’. Reguła tokenowa może być także funkcją, przykładowo zamieniamy string na liczbę dodatkowo

def t_NUMBER(t):

r’\d+’

t.value = int(t.value)

return t

(5)

Argumentem jest instancja typu LexToken. Atrybuty LexToken to type, value, lineno, lexpos. type jest ustawiany na nazwę po t_.

Wszystkie tokeny definiowane za pomocą funkcji są sprawdzane w kolejności z pliku.

Tokeny zdefiniowane za pomocą napisów są sortowane od najdłuższego.

Dla zarezerwowanych słów lepiej zagregować słowa reserved = {

’if’ : ’IF’,

’then’ : ’THEN’,

’else’ : ’ELSE’,

’while’ : ’WHILE’, ...

}

tokens = [’LPAREN’,’RPAREN’,...,’ID’] + list(reserved.values()) def t_ID(t):

r’[a-zA-Z_][a-zA-Z_0-9]*’

t.type = reserved.get(t.value,’ID’) # Check for reserved words

return t

Wartość dla tokenu może zawierać dodatkowe dane, przykładowo def t_ID(t):

# Look up symbol table information and return a tuple t.value = (t.value, symbol_lookup(t.value))

...

return t

W celu zignorowania tokenu możemy użyć def t_COMMENT(t):

r’\#.*’

pass

# No return value. Token discarded

albo możemy użyć prefixu ignore t_ignore_COMMENT = r’\#.*’.

W celu zaktualizowania numeru linii mamy token

# Define a rule so we can track line numbers def t_newline(t):

r’\n+’

t.lexer.lineno += len(t.value)

Pojedyncze znaki są definiowane w literals = [ ’+’,’-’,’*’,’/’ ]. Pojedyczne

znaki są sprawdzane na samym końcu. Można zdefiniować także te tokeny za pomocą

funkcji

(6)

literals = [ ’{’, ’}’ ] def t_lbrace(t):

r’\{’

t.type = ’{’ # Set token type to the expected literal return t

def t_rbrace(t):

r’\}’

t.type = ’}’ # Set token type to the expected literal return t

Uruchomienie lexera, zbudowanie lexera lexer=lex.lex(), podanie wejścia lexer.input(data), zwrócenie kolejnego tokenu lexer.token().

2 Zadania

2.1 Zadania na laboratorium

1. Zadanie wprowadzające. Zaimplementować wypisywanie tokenów z lexera.

2. Dla przykładu z kalkulatorem, zaimplementować obsługę liczb rzeczywistych, w szczególności liczb rozpoczynających się lub kończących się kropką. Sprawdzić po- prawność dla wejść .1, 1., .. Rezultat działania powinien być taki jak po wpisaniu do konsoli python tych liczb. .

3. Dodać tokeny dla funkcji specjalnych, takich jak sin, cos, exp, sqrt, log, itp, np.

sin(3). Przetestować przypadek, gdy nazwy identyfikatorów rozpoczynają się tak samo. Sprawdzić poprawność tokenów dla wejść sin23, sinsin12, sin12, sin23term, sin23sin23, sin(x), sinx, asin. Tokeny powinny być zgodne z kalkulatorem gnome- calculator.

4. Dodać do lexera kalkulatora token dla potęgowania (**). Działanie powinno być zgodne z kalkulatorem gnome-calculator. Dodatkowo należy zamienić wartość **

naˆ. Sprawdzić poprawność dla 2 ∗ ∗3 ∗ 4.

5. Napisać lexer, który będzie automatycznie poprawiał wybrane błędy w kodzie ję- zyka, np. literówki w słowach kluczowych, ignorowanie znaku równa się równości (=) na końcu wyrażenia.

2.2 Wskazówki

• Instalacja PLY. Dla Pythona 2.7 należy ściągnąć wersję 3.11 dostępną pod adre-

sem https://www.dabeaz.com/ply/ i skopiować katalog ply z zipa do katalogu

projektu. Dla Pythona 3.6 dostępna jest wersja 4.0 na github https://github.

(7)

com/dabeaz/ply, należy tym razem przekopiować dwa pliki do katalogu projektu lex.py oraz yacc.py. W linuksie opensuse dostępny jest pakiet o nazwie python3-ply.

• Dla przykładu z kalkulatorem usunąć linijki kodu począwszy od linii # Parsing rules.

• Tokeny mogą być wypisywane zgodnie ze sposobem podanym w dokumentacji https://www.dabeaz.com/ply/ply.html.

• Należy zdefiniować oddzielne tokeny dla liczb całkowitych i rzeczywistych.

• Możemy zdefiniować token o nazwie FUNCTION.

• Moduł re Pythona dla wyrażeń regularnych. https://docs.python.org/3/library/

re.html.

• Dokumentacja do PLY https://www.dabeaz.com/ply/ply.html.

• Lexer dla ansi c dla PLY w wersji 3.11 znajduje się w zipie w katalogu example, dla PLY w wersji 4.0 znajduje się na stronie https://github.com/dabeaz/ply/

blob/master/example/ansic/clex.py.

• Lexer dla kalkulatora dla PLY w wersji 3.11 znajduje się w zipie w katalogu exam- ple, dla PLY w wersji 4.0 znajduje się na stronie https://github.com/dabeaz/

ply/blob/master/example/calc/calc.py.

• Lexer dla języka Python https://github.com/woshifyz/zlang/blob/master/

zlexer.py

2.3 Zadania dodatkowe

1. Zadanie wprowadzające. Dodać możliwość rozpoznawania wzorów w tekście po- między znakami $. Wykorzystać stany do implementacji.

2. Dodać do lexera języka Markdown możliwość definiowania wzorów. Można wyko- rzystać lexer http://github.com/StevenChangZH/Markdown_python_compiler/

blob/master/codes/plyLex.py. Zdefiniować osobny stan dla wzorów. Można wy- korzystać notację Latex z dolarem do definiowania wyrażeń.

3. Napisać lexer w PLY, które dla podanego pliku html zwraca tokeny języka mark- down. Tokeny muszą mieć te same nazwy co w tokenizerze http://github.com/

StevenChangZH/Markdown_python_compiler/blob/master/codes/plyLex.py, np.

BOLD, CONTENTS. Można rozpocząć pracę od implementacji pogrubienia, lub nagłówków.

4. Napisać lexer w PLY dla wybranych fragmentów plików html.

(8)

2.4 Wskazówki

• https://en.wikipedia.org/wiki/Markdown

• Dla dowolnego tekstu zdefiniować token TEXT.

• Tokenizer dla markdown http://github.com/StevenChangZH/Markdown_python_

compiler/blob/master/codes/plyLex.py

• Konwerter online języka markdown do HTML https://daringfireball.net/

projects/markdown/dingus

Literatura

Cytaty

Powiązane dokumenty

Następnie wszystkie zabawki, które uczniowie przynieśli na lekcję zostają na ławce i każdy z uczniów zabiera jedną bądź dwie zabawki... i podchodzi do kolegi, zadając

Wciskamy lewy klawisz Ctrl + klawisz V lub na pasku u góry Wklej i drugiej linijce powinien pojawić się nasz napis.. Przechodzimy Enterem do następnej linijki i powtarzamy już

• Wzorce projektowe Elementy oprogramowania obiektowego wielokrotnego użytku, Gamma E., Helm E., Johnson R., Vlissades J., WNT

Jeśli natomiast wielkość przeszkody staje się porównywalna z długością fali (dźwięk), opis przy użyciu promieni załamuje się, ponieważ fala ulega silnemu ugięciu

W przypadku uboju zwierząt ochro- na prawna i ścisłe regulacje dotyczące ludzkich działań w tym zakresie są ko- nieczne. Nie można bowiem tutaj liczyć na wrażliwość

(b) w pierścieniu ideałów głównych każdy ideał pierwszy

Skaner powinien zostać zaimplementowany jako funkcja, która za każdym wywo- łaniem zwraca albo liczbę naturalną oznaczającą, jakiego rodzaju token został znaleziony w

W odniesieniu do klas opisanych na stronie Baldwina, Wasze główne zadanie będzie polegało na stworzeniu implemen- tacji klasy MinimLTokenSeq.. Jeżeli Wasz analizator leksykalny