Analiza metodą zstępującą. Bartosz Bogacki.

Podobne dokumenty
Wprowadzenie do analizy składniowej. Bartosz Bogacki.

0.1 Lewostronna rekurencja

Metody Kompilacji Wykład 7 Analiza Syntaktyczna

Wykład 5. Jan Pustelnik

Metody Kompilacji Wykład 8 Analiza Syntaktyczna cd. Włodzimierz Bielecki WI ZUT

Parsery LL(1) Teoria kompilacji. Dr inż. Janusz Majewski Katedra Informatyki

Matematyczne Podstawy Informatyki

Podstawy generatora YACC. Bartosz Bogacki.

Języki formalne i automaty Ćwiczenia 3

2.2. Gramatyki, wyprowadzenia, hierarchia Chomsky'ego

10. Translacja sterowana składnią i YACC

Efektywna analiza składniowa GBK

Języki formalne i automaty Ćwiczenia 4

Metody Kompilacji Wykład 3

JAO - Wprowadzenie do Gramatyk bezkontekstowych

Metody Kompilacji Wykład 13

Gramatyki rekursywne

L E X. Generator analizatorów leksykalnych

Języki formalne i automaty Ćwiczenia 2

Konstruktory. Streszczenie Celem wykładu jest zaprezentowanie konstruktorów w Javie, syntaktyki oraz zalet ich stosowania. Czas wykładu 45 minut.

JIP. Analiza składni, gramatyki

Podstawy Kompilatorów

Podstawy programowania. Wykład Funkcje. Krzysztof Banaś Podstawy programowania 1

Języki formalne i automaty Ćwiczenia 1

3.4. Przekształcenia gramatyk bezkontekstowych

znajdowały się różne instrukcje) to tak naprawdę definicja funkcji main.

Gramatyki, wyprowadzenia, hierarchia Chomsky ego. Gramatyka

Metody Kompilacji Wykład 1 Wstęp

Gramatyki regularne i automaty skoczone

Matematyczne Podstawy Informatyki

Języki formalne i automaty Ćwiczenia 9

Matematyczne Podstawy Informatyki

Analiza leksykalna 1. Teoria kompilacji. Dr inż. Janusz Majewski Katedra Informatyki

Włączenie analizy leksykalnej do analizy składniowej jest nietrudne; po co więc jest wydzielona?

Notacja RPN. 28 kwietnia wyliczanie i transformacja wyrażeń. Opis został przygotowany przez: Bogdana Kreczmera.

Uproszczony schemat działania kompilatora

Wykład 10. Translacja sterowana składnią

Języki formalne i automaty Ćwiczenia 8

Uproszczony schemat działania kompilatora

Automat ze stosem. Języki formalne i automaty. Dr inż. Janusz Majewski Katedra Informatyki

REKURENCJA W JĘZYKU HASKELL. Autor: Walczak Michał

Wyrażenie nawiasowe. Wyrażenie puste jest poprawnym wyrażeniem nawiasowym.

KONSTRUKCJA KOMPILATORÓW

Klasy abstrakcyjne i interfejsy

Translacja sterowana składnią w metodzie zstępującej

Języki programowania zasady ich tworzenia

GRAMATYKI BEZKONTEKSTOWE

Podstawą w systemie dwójkowym jest liczba 2 a w systemie dziesiętnym liczba 10.

Programowanie strukturalne i obiektowe. Funkcje

Języki formalne i gramatyki

ALGORYTMY I STRUKTURY DANYCH

Zakład Podstaw Cybernetyki i Robotyki Instytut Informatyki, Automatyki i Robotyki Politechnika Wrocławska

Gramatyka operatorowa

W przeciwnym wypadku wykonaj instrukcję z bloku drugiego. Ćwiczenie 1 utworzyć program dzielący przez siebie dwie liczby

Szablony funkcji i klas (templates)

Definiowanie języka przez wyrażenie regularne(wr)

Analizator syntaktyczny

Rekurencja (rekursja)

3. Instrukcje warunkowe

Plan wykładu. Kompilatory. Literatura. Translatory. Literatura Translatory. Paweł J. Matuszyk

Zadanie analizy leksykalnej

Wprowadzenie do kompilatorów

Podstawy Kompilatorów

Zadanie nr 3: Sprawdzanie testu z arytmetyki

4. Funkcje. Przykłady

Teoretyczne podstawy informatyki. Wykład 12: Gramatyki. E. Richter-Was 1

12. Rekurencja. UWAGA Trzeba bardzo dokładnie ustalić <warunek>, żeby mieć pewność, że ciąg wywołań się zakończy.

Podstawy Kompilatorów

Generator LLgen. Wojciech Complak Generator LLgen - charakterystyka. Generator LLgen -składnia specyfikacji

Informatyka I. Klasy i obiekty. Podstawy programowania obiektowego. dr inż. Andrzej Czerepicki. Politechnika Warszawska Wydział Transportu 2018

Przegląd metod error recovery (dla parsingu top-down, przykłady)

Zadanie 2: Arytmetyka symboli

Poprawność semantyczna

0 + 0 = 0, = 1, = 1, = 0.

Dziedziczenie. Streszczenie Celem wykładu jest omówienie tematyki dziedziczenia klas. Czas wykładu 45 minut.

Układy VLSI Bramki 1.0

Odwrotna Notacja Polska

Złożoność obliczeniowa zadania, zestaw 2

Pytania sprawdzające wiedzę z programowania C++

Lingwistyka Matematyczna Języki formalne i gramatyki Analiza zdań

Temat: Algorytm kompresji plików metodą Huffmana

Gramatyki atrybutywne

Jak zawsze wyjdziemy od terminologii. While oznacza dopóki, podczas gdy. Pętla while jest

Imię, nazwisko, nr indeksu

11 Probabilistic Context Free Grammars

Wykład 6. Reguły inferencyjne systemu aksjomatycznego Klasycznego Rachunku Zdań

Warunek wielokrotnego wyboru switch... case

ZASADY PROGRAMOWANIA KOMPUTERÓW ZAP zima 2014/2015. Drzewa BST c.d., równoważenie drzew, kopce.

Ćwiczenie: JavaScript Cookies (3x45 minut)

Słowem wstępu. Część rodziny języków XSL. Standard: W3C XSLT razem XPath 1.0 XSLT Trwają prace nad XSLT 3.0

Rozpoznawanie obrazu. Teraz opiszemy jak działa robot.

Wykład 2. Drzewa zbalansowane AVL i 2-3-4

lekcja 8a Gry komputerowe MasterMind

5. OKREŚLANIE WARTOŚCI LOGICZNEJ ZDAŃ ZŁOŻONYCH

Języki, automaty i obliczenia

Języki formalne i automaty Ćwiczenia 7

Wprowadzenie. Teoria automatów i języków formalnych. Literatura (1)

Generatory analizatorów

Translacja sterowana składnią w generatorze YACC

Wyszukiwanie binarne

Transkrypt:

Analiza metodą zstępującą Bartosz Bogacki Bartosz.Bogacki@cs.put.poznan.pl Witam Państwa. Wykład, który za chwilę Państwo wysłuchają dotyczy analizy metodą zstępującą. Zapraszam serdecznie do wysłuchania. 1

Wprowadzenie Dana jest forma zdaniowa α (T N) Czy α L(G)? Analiza składniowa (2) Po wprowadzeniu podstawowych pojęć z zakresu analizy składniowej takich jak: -Gramatyka, -Zdanie, -Forma zdaniowa, -Wyprowadzenie, -Drzewo wyprowadzenia nadszedł czas aby przyjrzeć się sposobom realizacji rozbioru składniowego. Załóżmy, że dana jest forma zdaniowa alfa oraz gramatyka G. Pytanie brzmi, czy forma ta należy do języka generowanego przez gramatykę G? 2

Metoda zstępująca Poszukiwanie lewostronnego wyprowadzenia dla ciągu wejściowego Budowa drzewa wyprowadzenia dla wejścia zaczynając od korzenia i tworząc wierzchołki zgodnie z porządkiem preorder * S α Analiza składniowa (3) Rozpoczniemy od metody zstępującej, która jest de facto poszukiwaniem lewostronnego wyprowadzenia dla zdania będącego ciągiem wejściowym. Metodę tę można również omawiać jako budowę drzewa wyprowadzenia dla wejścia. Rozpoczynając od korzenia, wierzchołki tworzy się zgodnie z porządkiem preorder. To co odróżnia tę metodę od metody wstępującej, która omawiana będzie na następnym wykładzie to fakt, iż przetwarzanie rozpoczynamy od symbolu startowego a następnie stosujemy wyprowadzenie tak długo aż osiągniemy zdanie wejściowe, lub odpowiedź, iż nie da się wygenerować takiego zdania. 3

Metoda zstępująca 1. E ( E ) 2. E + α = (((+))) Analiza składniowa (4) Przyjrzyjmy się przykładowi. Dana jest gramatyka generująca język, do którego należą zdania: +, (+), ((+)), (((+))), itd., czyli albo pojedynczy plus, albo plus ujęty w nawiasy. Liczba nawiasów otwierających musi być równa liczbie nawiasów zamykających. Pytanie brzmi, czy zdanie: (((+))) należy do języka generowanego przez tą gramatykę? Oczywiście już na pierwszy rzut oka jesteśmy w stanie odpowiedzieć, że tak, ale przyjrzyjmy się mechanizmowi postępowania zanim przejdziemy do trudniejszych przypadków. 4

Metoda zstępująca 1. E ( E ) 2. E + E E ( E ) α = (((+))) Analiza składniowa (5) Przetwarzanie rozpoczynamy od symbolu startowego (czyli E). Wykorzystując pierwszą produkcję stosujemy lewostronne bezpośrednie wyprowadzenie. 5

Metoda zstępująca 1. E ( E ) 2. E + E E ( E ) (E) E ( E ) ls α = (((+))) Analiza składniowa (6) Ponieważ ciąg wejściowy różni się od formy zdaniowej, którą uzyskaliśmy, kontynuujemy procedurę. Ponownie stosujemy lewostronne bezpośrednie wyprowadzenie i ponownie wykorzystujemy pierwszą produkcję. 6

Metoda zstępująca E E ( E ) (E) E ( E ) ls ((E)) E ( E ) ls 1. E ( E ) 2. E + α = (((+))) Analiza składniowa (7) Ciąg wejściowy nadal różni się od uzyskanej formy zdaniowej. Ponownie stosujemy lewostronne bezpośrednie wyprowadzenie i ponownie wykorzystujemy pierwszą produkcję. 7

Metoda zstępująca E E ( E ) (E) E ( E ) ls ((E)) E ( E ) ls (((E))) E + ls 1. E ( E ) 2. E + α = (((+))) Analiza składniowa (8) Ciąg wejściowy nadal różni się od uzyskanego wyprowadzenia. Widzimy natomiast, że wyprowadzenie zawiera już 3 nawiasy otwierające i 3 nawiasy zamykające. W bezpośrednim wyprowadzeniu wykorzystujemy więc drugą produkcję zastępując nieterminal E plusem. 8

Metoda zstępująca E E ( E ) (E) E ( E ) ls ((E)) E ( E ) ls (((E))) E + ls (((+))) ls 1. E ( E ) 2. E + α = (((+))) Analiza składniowa (9) W ten oto sposób uzyskaliśmy ciąg wejściowy. Analiza zakończyła się sukcesem. 9

Metoda zstępująca 1. zdanie podmiot orzeczenie 2. podmiot przymiotnik rzeczownik 3. orzeczenie czasownik dopełnienie 4. dopełnienie rzeczownik 5. przymiotnik małe 6. czasownik wypiło 7. rzeczownik dziecko 8. rzeczownik mleko α = małe dziecko wypiło mleko Analiza składniowa (10) Kolejny przykład rozważymy wykorzystując do tego celu dobrze znaną z poprzedniego wykładu gramatykę. Sprawdzimy czy zdanie: małe dziecko wypiło mleko należy do języka generowanego przez tą gramatykę. Tym razem spojrzymy na metodę zstępującą jak na drzewo wyprowadzenia, które budujemy rozpoczynając od korzenia. 10

Metoda zstępująca 1. zdanie podmiot orzeczenie 2. podmiot przymiotnik rzeczownik 3. orzeczenie czasownik dopełnienie 4. dopełnienie rzeczownik 5. przymiotnik małe 6. czasownik wypiło 7. rzeczownik dziecko 8. rzeczownik mleko zdanie α = małe dziecko wypiło mleko Analiza składniowa (11) Budowę drzewa rozpoczynamy od korzenia, czyli od symbolu startowego. Symbolem startowym w rozważanej gramatyce jest symbol zdanie. 11

Metoda zstępująca 1. zdanie podmiot orzeczenie 2. podmiot przymiotnik rzeczownik 3. orzeczenie czasownik dopełnienie 4. dopełnienie rzeczownik 5. przymiotnik małe 6. czasownik wypiło 7. rzeczownik dziecko 8. rzeczownik mleko zdanie podmiot orzeczenie α = małe dziecko wypiło mleko Analiza składniowa (12) Zgodnie z pierwszą produkcją możemy dokonać wyprowadzenia dwóch symboli: podmiot oraz orzeczenie. 12

Metoda zstępująca 1. zdanie podmiot orzeczenie 2. podmiot przymiotnik rzeczownik 3. orzeczenie czasownik dopełnienie 4. dopełnienie rzeczownik 5. przymiotnik małe 6. czasownik wypiło 7. rzeczownik dziecko 8. rzeczownik mleko zdanie podmiot orzeczenie przymiotnik rzeczownik α = małe dziecko wypiło mleko Analiza składniowa (13) Pamiętając, że budujemy drzewo tworząc wierzchołki zgodnie z porządkiem preorder, przechodzimy do bezpośredniego wyprowadzenia dla symbolu podmiot. Wykorzystując drugą produkcję tworzymy dwa nowe węzły drzewa przymiotnik oraz rzeczownik. 13

Metoda zstępująca 1. zdanie podmiot orzeczenie 2. podmiot przymiotnik rzeczownik 3. orzeczenie czasownik dopełnienie 4. dopełnienie rzeczownik 5. przymiotnik małe 6. czasownik wypiło 7. rzeczownik dziecko 8. rzeczownik mleko zdanie podmiot orzeczenie przymiotnik rzeczownik Małe α = małe dziecko wypiło mleko Analiza składniowa (14) Dokonujemy wyprowadzenia symbolu małe z symbolu przymiotnik zgodnie z produkcją piątą. 14

Metoda zstępująca 1. zdanie podmiot orzeczenie 2. podmiot przymiotnik rzeczownik 3. orzeczenie czasownik dopełnienie 4. dopełnienie rzeczownik 5. przymiotnik małe 6. czasownik wypiło 7. rzeczownik dziecko 8. rzeczownik mleko zdanie podmiot orzeczenie przymiotnik rzeczownik Małe α = małe dziecko wypiło mleko dziecko Analiza składniowa (15) Dokonujemy wyprowadzenia symbolu dziecko z symbolu rzeczownik zgodnie z produkcją siódmą. 15

Metoda zstępująca 1. zdanie podmiot orzeczenie 2. podmiot przymiotnik rzeczownik 3. orzeczenie czasownik dopełnienie 4. dopełnienie rzeczownik 5. przymiotnik małe 6. czasownik wypiło 7. rzeczownik dziecko 8. rzeczownik mleko zdanie podmiot orzeczenie przymiotnik rzeczownik czasownik dopełnienie Małe dziecko α = małe dziecko wypiło mleko Analiza składniowa (16) Dokonujemy wyprowadzenia symboli czasownik oraz dopełnienie z symbolu orzeczenie zgodnie z produkcją trzecią. 16

Metoda zstępująca 1. zdanie podmiot orzeczenie 2. podmiot przymiotnik rzeczownik 3. orzeczenie czasownik dopełnienie 4. dopełnienie rzeczownik 5. przymiotnik małe 6. czasownik wypiło 7. rzeczownik dziecko 8. rzeczownik mleko zdanie podmiot orzeczenie przymiotnik rzeczownik czasownik dopełnienie α = małe dziecko wypiło mleko Małe dziecko wypiło Analiza składniowa (17) Dokonujemy wyprowadzenia symbolu wypiło z symbolu czasownik zgodnie z produkcją szóstą. 17

Metoda zstępująca 1. zdanie podmiot orzeczenie 2. podmiot przymiotnik rzeczownik 3. orzeczenie czasownik dopełnienie 4. dopełnienie rzeczownik 5. przymiotnik małe 6. czasownik wypiło 7. rzeczownik dziecko 8. rzeczownik mleko zdanie podmiot orzeczenie przymiotnik rzeczownik czasownik dopełnienie rzeczownik α = małe dziecko wypiło mleko Małe dziecko wypiło Analiza składniowa (18) Dokonujemy wyprowadzenia symbolu rzeczownik z symbolu dopełnienie zgodnie z produkcją czwartą. 18

Metoda zstępująca 1. zdanie podmiot orzeczenie 2. podmiot przymiotnik rzeczownik 3. orzeczenie czasownik dopełnienie 4. dopełnienie rzeczownik 5. przymiotnik małe 6. czasownik wypiło 7. rzeczownik dziecko 8. rzeczownik mleko zdanie podmiot orzeczenie przymiotnik rzeczownik czasownik dopełnienie rzeczownik α = małe dziecko wypiło mleko Małe dziecko wypiło mleko Analiza składniowa (19) Wyprowadzając symbol mleko z symbolu rzeczownik tworzymy ostatni liść w drzewie wyprowadzenia. Liście drzewa czytane w porządku preorder tworzą zdanie wejściowe. 19

Metoda zstępująca (nawroty) 1. S a A d 2. S a B 3. A b 4. A c 5. B c c d 6. B d d c α = accd Analiza składniowa (20) Ostatni rozpatrywany przykład będzie nieco bardziej skomplikowany. Na slajdzie przedstawiona została gramatyka oraz zdanie stanowiące ciąg wejściowy. Pytanie brzmi czy zdanie to należy do języka generowanego przez tą gramatykę? Sprawdźmy! 20

Metoda zstępująca (nawroty) 1. S a A d 2. S a B 3. A b 4. A c 5. B c c d 6. B d d c S α = accd Analiza składniowa (21) Rozpoczynamy budowę drzewa wyprowadzenia od symbolu startowego gramatyki. W rozpatrywanej gramatyce jest to symbol S. 21

Metoda zstępująca (nawroty) 1. S a A d 2. S a B 3. A b 4. A c 5. B c c d 6. B d d c a S A d α = accd Analiza składniowa (22) Kolejne węzły drzewa tworzymy stosując bezpośrednie wyprowadzenie. Wykorzystujemy produkcję pierwszą. Pierwszy symbol z wejścia jest identyczny, więc przechodzimy do rozwinięcia węzła, w którym znajduje się nieterminal A. 22

Metoda zstępująca (nawroty) 1. S a A d 2. S a B 3. A b 4. A c 5. B c c d 6. B d d c a S A b d α = accd Analiza składniowa (23) Wykorzystując produkcję, dla A tworzymy nowy liść w drzewie wyprowadzenia, którym jest terminal b. Ponieważ znaleźliśmy się w sytuacji, w której liście drzewa nie są równe ciągowi wejściowemu, więc musimy dokonać nawrotu i spróbować dokonać innego wyprowadzenia. 23

Metoda zstępująca (nawroty) 1. S a A d 2. S a B 3. A b 4. A c 5. B c c d 6. B d d c a S A c d α = accd Analiza składniowa (24) Usuwamy liść zawierający terminal b. W jego miejsce próbujemy wstawić inne rozwinięcie nietermianala A. Tym razem skorzystamy z produkcji czwartej. W wyniku bezpośredniego wyprowadzenia powstaje liść z terminalem c. Liście drzewa to acd. Ciąg wejściowy to accd. Ponieważ ciągi te są różne, musimy dokonać kolejnego nawrotu i spróbować innych wyprowadzeń. 24

Metoda zstępująca (nawroty) 1. S a A d 2. S a B 3. A b 4. A c 5. B c c d 6. B d d c S a B α = accd Analiza składniowa (25) Cofamy się więc aż do symbolu startowego (S) i wykorzystujemy drugą produkcję do utworzenia dwóch węzłów. 25

Metoda zstępująca (nawroty) 1. S a A d 2. S a B 3. A b 4. A c 5. B c c d 6. B d d c S a B c c d α = accd Analiza składniowa (26) Po rozwinięciu nieterminala B uzyskujemy oczekiwany rezultat. Liście drzewa: accd są identyczne z ciągiem wejściowym. 26

Proste gramatyki LL(1) Prostą gramatyką LL(1) jest gramatyka bezkontekstowa, która: nie zawiera ε-produkcji (pustych produkcji) A N, prawe strony produkcji rozpoczynają się od różnych symboli terminalnych Analiza składniowa (27) Wprowadzimy teraz definicję prostej gramatyki LL(1). Proste gramatyki LL(1) to klasa gramatyk, które mogą być automatycznie przetwarzane poprzez analizatory bazujące na metodzie zstępującej. Prostą gramatyką LL(1) jest gramatyka bezkontekstowa, która: -nie zawiera pustych produkcji -dla każdego nieterminala A prawe strony produkcji A rozpoczynają się od różnych symboli terminalnych 27

Metoda zejść rekurencyjnych Osobna funkcja dla każdego nieterminala Na podstawie symbolu znajdującego się na wejściu podejmowana jest decyzja o wyborze produkcji. Dla nieterminala następuje wywołanie funkcji związanej z tym nieterminalem Dla terminala następuje sprawdzenie jego zgodności z symbolami, których funkcja oczekuje na wejściu Analiza składniowa (28) Przejdźmy teraz do omówienia popularnej implementacji metody zstępującej, którą jest metoda zejść rekurencyjnych. Do prezentacji metody wykorzystamy język C. Oto podstawowe zasady zgodnie, z którymi piszemy analizator: -Dla każdego nieterminala tworzymy osobną funkcję, -Decyzję o wyborze produkcji analizator podejmuje na podstawie symbolu znajdującego się na wejściu -Dla nieterminala następuje wywołanie funkcji związanej z tym nieterminalem -Dla terminala następuje sprawdzenie jego zgodności z symbolami, których funkcja oczekuje na wejściu. 28

Metoda zejść rekurencyjnych char biezacy; void wczytaj(char); void sygnalizuj_blad(); char nastepny_symbol(); void wczytaj(char symbol) { if (biezacy == symbol) { biezacy = nastepny_symbol(); else { sygnalizuj_blad(); void sygnalizuj_blad() { puts("syntax Error!"); exit(-1); char nastepny_symbol() { return getchar(); Analiza składniowa (29) Zacznijmy od zdefiniowania kilku funkcji pomocniczych, które będziemy wykorzystywali w dalszej części wykładu. Na początek funkcja wczytaj. Funkcja ta jest odpowiedzialna za sprawdzenie czy na wejściu znajduje się oczekiwany symbol leksykalny. Jeśli tak, to wczytywany jest następny symbol z wejścia. W przeciwnym razie wywoływana jest funkcja sygnalizuj błąd, która wypisuje informację o błędzie i kończy pracę analizatora. W celu pobrania kolejnego symbolu leksykalnego wywoływana jest funkcja następny_symbol. W tym miejscu powinien być wpięty analizator leksykalny, który po odpowiednim przetworzeniu strumienia danych wejściowych zwróci gotowe symbole do analizatora składniowego. W rozważanym analizatorze zastosowano uproszczony mechanizm polegający na bezpośrednim przekazywaniu znaków znajdujących się w strumieniu danych wejściowych do analizatora składniowego. 29

Metoda zejść rekurencyjnych E ( E ) E + void E() { if (biezacy == '(') { wczytaj('('); E(); wczytaj(')'); else if (biezacy == '+') { wczytaj('+'); else { sygnalizuj_blad(); Analiza składniowa (30) Przygotujmy analizator składniowy dla prostej gramatyki, którą przedstawiliśmy na początku wykładu. Przypomnijmy, że gramatyka ta ma za zadanie zweryfikować czy dane wejściowe zawierają symbol plus ujęty w zero lub więcej nawiasów okrągłych. 30

Metoda zejść rekurencyjnych E ( E ) E + void E() { if (biezacy == '(') { wczytaj('('); E(); wczytaj(')'); else if (biezacy == '+') { wczytaj('+'); else { sygnalizuj_blad(); Analiza składniowa (31) Przyjrzyjmy się na slajdzie w jaki sposób skonstruowana jest funkcja dla nieterminala E. Pierwsza produkcja tworzy nam pierwszy blok warunkowy w ciele funkcji. Jeśli bieżący symbol jest nawiasem otwierającym, to wykonują się: -Wywołanie funkcji wczytaj() dla nawiasu otwierającego, -Rekurencyjne wywołanie funkcji E(), -oraz wywołanie funkcji wczytaj() dla nawiasu zamykającego. W przeciwnym razie sprawdzane jest czy bieżący symbol jest plusem. Jeśli tak, to wywoływana jest funkcja wczytaj() dla plusa. Jeśli bieżący symbol nie jest ani nawiasem otwierającym ani plusem, to wywoływana jest funkcja informująca o błędzie i kończone jest działanie analizatora. 31

Metoda zejść rekurencyjnych E ( E ) E + char biezacy; int main() { biezacy = nastepny_symbol(); E(); puts("dane OK"); return 0; Analiza składniowa (32) Oto pozostały fragment kodu służący do uruchomienia całości analizatora. Przed wywołaniem funkcji, która jest implementacją symbolu startowego należy zainicjalizować zmienną bieżący odpowiednią wartością. Jeśli przetwarzanie zakończy się pomyślnie to wypisany zostaje komunikat o sukcesie. Proszę zwrócić uwagę, że komunikat ten zostanie wypisany również w momencie gdy na wejściu pozostaną jeszcze nie odczytane symbole. Przykładowo jeśli strumień wejściowy składałby się z następujących znaków: (+)+, to analizator poinformuje nas o sukcesie. Aby zapobiec takiej sytuacji, warto rozszerzyć analizator o sprawdzenie czy na wejściu nie pozostały zbędne symbole. 32

Metoda zejść rekurencyjnych E ( E ) E + char biezacy; int main() { biezacy = nastepny_symbol(); E(); if (biezacy!= EOF) { sygnalizuj_blad(); else { puts("dane OK"); return 0; Analiza składniowa (33) Oto poprawiona funkcja main(), która zawiera już takie sprawdzenie. 33

Metoda zejść rekurencyjnych A B a B b B B c void B() { if (biezacy == 'b') { wczytaj('b'); B(); else if (biezacy == 'c'){ wczytaj('c'); else { sygnalizuj_blad(); Analiza składniowa (34) Rozważmy teraz kolejną gramatykę oraz przykładową implementację w języku C. Zacznijmy od nieterminala B. Implementacja jest analogiczna do tej z poprzedniego przykładu. Najpierw sprawdzamy bieżący symbol a następnie na jego podstawie decydujemy którą produkcję wybrać. Jeśli w produkcji jest terminal, to odczytujemy go z wejścia. Jeśli jest nieterminal, to wywołujemy funkcję związaną z tym nieterminalem. 34

Metoda zejść rekurencyjnych A B a B b B B c void A() { if (biezacy ==???... Analiza składniowa (35) Zastanówmy się teraz nad implementacją funkcji odpowiadającej nieterminalowi A. W pierwszym kroku powinniśmy sprawdzić czy symbol bieżący jest odpowiednim terminalem. Niestety produkcja rozpoczyna się od nieterminala... 35

Zbiór FIRST Zbiór FIRST(X), gdzie X jest dowolną sekwencją symboli tworzy się zgodnie z poniższymi regułami: Jeśli X T, to FIRST(X)={X Jeśli X ε to ε FIRST(X) Jeśli X N i X Y 1 Y 2...Y n, to w FIRST(X) jeśli istnieje takie i, że w FIRST(Y i ) a ε jest we wszystkich FIRST(Y 1 )...FIRST(Y i-1 ) Jeśli ε FIRST(Y i ) dla wszystkich i, to ε FIRST(X) Analiza składniowa (36) Aby poradzić sobie z tym problemem wprowadzimy definicję zbioru FIRST. Nieformalnie można powiedzieć, że zbiór FIRST jest zbiorem zawierającym terminale, które mogą rozpoczynać daną sekwencję. Przyjrzyjmy się teraz formalnej definicji. Zbiór FIRST(X), gdzie X jest dowolną sekwencją symboli tworzy się zgodnie z poniższymi regułami: -Jeśli X jest terminalem, to X należy do zbioru FIRST(X) -Jeśli X jest symbolem pustym to epsilon należy do zbioru FIRST(X) -Jeśli X jest nieterminalem i X->Y 1 Y 2...Y n, to w należy do FIRST(X) jeśli istnieje takie i, że w należy do FIRST(Y i ) a epsilon jest we wszystkich zbiorach FIRST(Y 1 )...FIRST(Y i-1 ) -Jeśli epsilon należy do FIRST(Y i ) dla wszystkich i, to epsilon należy do FIRST(X) 36

Zbiór FIRST 1. zdanie podmiot orzeczenie 2. podmiot przymiotnik rzeczownik 3. orzeczenie czasownik dopełnienie 4. dopełnienie rzeczownik 5. przymiotnik małe 6. czasownik wypiło 7. rzeczownik dziecko 8. rzeczownik mleko FIRST(mleko)={mleko FIRST(zdanie)= FIRST(podmiot orzeczenie)= FIRST(przymiotnik rzeczownik orzeczenie)= FIRST(małe rzeczownik orzeczenie)= {małe Analiza składniowa (37) Przyjrzyjmy się jak wygląda obliczanie zbioru FIRST w praktyce. Zaczniemy od obliczenia zbioru FIRST(mleko) dla gramatyki przedstawionej na slajdzie. Ponieważ symbol mleko jest terminalem, więc zbiór FIRST(mleko) jest jednoelementowy i zawiera symbol mleko. Obliczmy teraz zbiór FIRST(zdanie). Tu sytuacja jest bardziej skomplikowana, gdyż zdanie jest nieterminalem. Zgodnie z zasadami podanymi na poprzednim slajdzie. Obliczamy więc zbiór FIRST rozważając prawą stronę produkcji nieterminala zdanie. W wyniku naszych obliczeń otrzymujemy jednoelementowy zbiór zawierający symbol małe. 37

Zbiór FIRST E ( E ) E ε FIRST(E) = FIRST (( E )) FIRST(ε) = { (, ε Analiza składniowa (38) Na slajdzie przedstawiono wyliczenie zbioru FIRST(E) dla gramatyki znajdującej się w ramce. Ponieważ istnieją dwie produkcje dla symbolu nieterminalnego E, więc rozpatrujemy je osobno. Z pierwszej produkcji do zbioru FIRST trafi (. Z drugiej produkcji natomiast do zbioru FIRST(E) trafi symbol epsilon. Zbiór FIRST(E) składa się więc z dwóch symboli: ( oraz epsilon. 38

Zbiór FIRST E E + T E T T T * F T F F ( E ) F id FIRST(T) = FIRST(T * F) FIRST(F) = FIRST(( E )) FIRST(id) = { (, id Analiza składniowa (39) Oto kolejny przykład wyliczenia zbioru FIRST. Na slajdzie przedstawiono zbiór FIRST(T) dla gramatyki znajdującej się w ramce. W gramatyce istnieją dwie produkcje dla nieterminala T, więc obie musimy rozważyć tworząc zbiór FIRST. Pierwsza produkcja jest lewostronnie rekurencyjna, dlatego musimy najpierw rozważyć drugą produkcję, aby zobaczyć czy do zbioru FIRST(T) nie trafi przypadkiem epsilon. Jeśli tak by się stało, to musielibyśmy wrócić do pierwszej produkcji i rozważać kolejne znaki w sekwencji. Po przeanalizowaniu drugiej produkcji widzimy jednak, że do zbioru FIRST(T) trafi ( oraz id. Ostatecznie zbiór FIRST(T) zawiera symbol nawiasu otwierającego oraz symbol id. 39

Metoda zejść rekurencyjnych Osobna funkcja dla każdego nieterminala Na podstawie symbolu znajdującego się na wejściu podejmowana jest decyzja o wyborze produkcji. Produkcja X α jest wybierana jeśli symbol na wejściu należy do FIRST(α) Dla nieterminala następuje wywołanie funkcji związanej z tym nieterminalem Dla terminala następuje sprawdzenie jego zgodności z symbolami, których funkcja oczekuje na wejściu Analiza składniowa (40) A oto uaktualniony opis zasad zgodnie, z którymi piszemy analizator w oparciu o metodę zejść rekurencyjnych. -Dla każdego nieterminala tworzymy osobną funkcję, -Decyzję o wyborze produkcji analizator podejmuje na podstawie symbolu znajdującego się na wejściu. Aby wybrać konkretną produkcję, symbol na wejściu musi należeć do zbioru FIRST od prawej strony tej produkcji. -Dla nieterminala następuje wywołanie funkcji związanej z tym nieterminalem -Dla terminala następuje sprawdzenie jego zgodności z symbolami, których funkcja oczekuje na wejściu. 40

Metoda zejść rekurencyjnych A B a B b B B c FIRST(B a) = { b, c Teraz OK void A() { if ((biezacy == 'b') (biezacy == 'c')) { B(); wczytaj('a'); else { sygnalizuj_blad(); Analiza składniowa (41) Wróćmy teraz do pozostawionej implementacji. Po obliczeniu zbioru FIRST(B a) uzyskujemy brakującą informację i możemy kontynuować pisanie funkcji dla nieterminala A. W pierwszym bloku warunkowym sprawdzamy czy symbol bieżący jest jednym z elementów należących do zbioru FIRST(B a). Jeśli tak, to wykonujemy prawą stronę produkcji, czyli wywołanie funkcji związanej z nieterminalem B oraz funkcji wczytaj dla terminala a. 41

ε-produkcje A B a B b B B ε FIRST(B a) = { b, a void A() { if ((biezacy == 'b') (biezacy == 'a')) { B(); wczytaj('a'); else { sygnalizuj_blad(); Analiza składniowa (42) Zmieńmy teraz trochę gramatykę wprowadzając do niej pustą produkcję i spójrzmy jaki wpływ będzie miała taka zmiana na implementację. Ostatnia produkcja jest produkcją pustą. Zacznijmy od funkcji związanej z nieterminalem A. Ponieważ zmiana powoduje zmianę zbioru FIRST(B a), więc wprowadzamy niewielką korektę do kodu. 42

ε-produkcje A B a B b B B ε void B() { if (biezacy == 'b') { wczytaj('b'); B(); else { /* epsilon */ Analiza składniowa (43) Przyjrzyjmy się teraz implementacji funkcji dla nieterminala B. Pierwsza część związana z produkcją B -> b B pozostaje bez zmian. Druga część jest teraz odpowiedzialna za wygenerowanie symbolu pustego. Będzie ona wykonana dla każdego symbolu różnego od b. Łatwo dostrzec, że pomimo, iż gramatyka nie jest już gramatyką LL(1) to przy założeniu, że epsilon produkcja wybrana zostanie tylko w przypadku gdy żadna inna produkcja nie będzie pasowała, można przygotować implementację za pomocą metody zejść rekurencyjnych. 43

Lewostronna rekurencja A a B a B ε B B b FIRST(B b) = { ε, b??? void B() { if (biezacy == 'b') { B(); wczytaj('b'); else { /* epsilon */ Analiza składniowa (44) Zajmijmy się teraz ograniczeniami metody zejść rekurencyjnych. Na początek przyjrzyjmy się gramatyce. Stosując znany już wzorzec wyliczamy zbiór FIRST dla pierwszej i trzeciej produkcji, czyli dla FIRST(a B a) oraz FIRST(B b). Implementujemy funkcję dla nieterminala B. Przykładowa implementacja wygenerowana na bazie gramatyki znajduje się na slajdzie. Proszę zwrócić uwagę, że już dla ciągu symboli leksykalnych: aba nasz analizator nie będzie działał poprawnie. Po wczytaniu symbolu a przejdziemy do rozważanej funkcji B(). Tu nasz program zapętli się wykonując w nieskończoność (a w zasadzie do momentu przepełnienia stosu) rekurencyjne wywołanie funkcji B(). Przyczyną takiego zachowania jest wykorzystanie w gramatyce lewostronnej rekurencji. 44

Lewostronna rekurencja Gramatyka lewostronnie rekursywna * A A α A N; α (N T) Analiza składniowa (45) Przypomnijmy sobie czym jest lewostronna rekurencja. Gramatykę nazywamy rekurencyjną jeśli w wyprowadzeniu dla danego symbolu nieterminalnego występuje ten sam symbol. Jeśli symbol ten występuje na skrajnie lewej pozycji, to mamy do czynienia z lewostronną rekurencją. 45

Eliminacja lewostronnej rekurencji Jeśli produkcje zawierają konstrukcje lewostronnie rekurencyjne, należy przepisać gramatykę dokonując eliminacji lewostronnej rekurencji. A Aα A β A βa A αa A ε Analiza składniowa (46) W metodzie zejść rekurencyjnych jeśli produkcje zawierają konstrukcje lewostronnie rekurencyjne, należy przepisać gramatykę dokonując eliminacji lewostronnej rekurencji. Na slajdzie przedstawiono szablon zgodnie z którym można wyeliminować bezpośrednią lewostronną rekurencję. 46

Eliminacja lewostronnej rekurencji A a B a B ε B B b A a B a B B B b B B ε A Aα A β A βa A αa A ε Analiza składniowa (47) Przyjrzyjmy się naszej gramatyce. Na niebieskim tle pokazano gramatykę zawierającą lewostronną rekurencję. Na białym tle znajduje się gramatyka z wyeliminowaną lewostronną rekurencją poprzez zastosowanie przedstawionego szablonu. Oto zastosowane podstawienia: A = B Alfa = b Beta = epsilon Oczywiście epsilon to symbol pusty i nie ma sensu pisanie w produkcji drugiej epsilon B. Dlatego też powstała produkcja zawierająca po prawej stronie jedynie symbol B. 47

Eliminacja lewostronnej rekurencji A a B a B ε B B b A a B a B B B b B B ε A a B a B b B B ε A Aα A β A βa A αa A ε Analiza składniowa (48) Produkcję B -> B możemy wyeliminować z naszej gramatyki, gdyż nie niesie ona żadnej istotnej informacji. Ostatecznie otrzymujemy uproszczoną gramatykę. 48

Eliminacja lewostronnej rekurencji A a B a B b B B ε Teraz OK void B() { if (biezacy == 'b') { wczytaj('b'); B(); else { /* epsilon */ Analiza składniowa (49) Wróćmy do implementacji. Po zmodyfikowaniu gramatyki nie ma problemu ze stworzeniem poprawnego kodu. Ostateczną wersję gramatyki oraz implementację funkcji dla symbolu nieterminalnego B przedstawiono na slajdzie. 49

Nierozróżnialność produkcji A a B a B b B b B??? void B() { if (biezacy == 'b') { wczytaj('b'); else if (biezacy == 'b') { wczytaj('b'); B(); else { sygnalizuj_blad(); Analiza składniowa (50) A oto kolejny problem, który może pojawić się podczas implementacji analizatora składniowego w oparciu o metodę zejść rekurencyjnych. Na slajdzie przedstawiono kolejną gramatykę oraz implementację symbolu nieterminalnego B. Problem, który tu się pojawił, to nierozróżnialność produkcji. Należy pamiętać, że analizator, który tworzymy należy do rodziny analizatorów przewidujących. Wybiera on odpowiednią ścieżkę (czyli produkcję) podejmując decyzję w oparciu o symbol znajdujący się na wejściu. Nie jest wykonywane nawracanie. Aby móc spełnić te wymagania każda z produkcji danego symbolu nieterminalnego musi rozpoczynać się od innego symbolu terminalnego. W rozważanej gramatyce tak nie jest, gdyż FIRST(b) = FIRST(b B) = {b. Powoduje to, że wygenerowany kod nie jest poprawny. 50

Lewostronna faktoryzacja Jeśli nie można rozróżnić dwóch produkcji, gdyż rozpoczynają się od tego samego terminala, należy przepisać gramatykę dokonując lewostronnej faktoryzacji. A α A A α β 1 A β A α β 1 2 A β 2 Analiza składniowa (51) W metodzie zejść rekurencyjnych jeśli produkcje tego samego nieterminala rozpoczynają się od tego samego terminala, to należy przepisać gramatykę dokonując lewostronnej faktoryzacji. Na slajdzie przedstawiono szablon zgodnie z którym można tego dokonać. 51

Lewostronna faktoryzacja A a B a B b B b B A a B a B b B2 B2 ε B2 B A α A A α β 1 A β A α β 1 2 A β 2 Analiza składniowa (52) Przyjrzyjmy się naszej gramatyce. Na niebieskim tle pokazano gramatykę zawierającą 2 produkcję nieterminala B, które rozpoczynają się od symbolu b. Na białym tle znajduje się gramatyka po zastosowaniu lewostronnej faktoryzacji zgodnie z przedstawionym szablonem. Oto zastosowane podstawienia: A = B Alfa = b Beta1 = epsilon Beta2 = B 52

Lewostronna faktoryzacja A a B a B b B2 B2 ε B2 B Teraz OK void B() { if (biezacy == 'b') { wczytaj('b'); B2(); else { sygnalizuj_blad(); void B2() { if (biezacy == 'b') { B(); else { /* epsilon */ Analiza składniowa (53) A oto poprawiona gramatyka i działająca implementacja. Ponieważ symbol nieterminalny B został rozbity na dwa symbole B i B2, więc na slajdzie przedstawiono kod dla obu tych symboli. 53

Podsumowanie Proste gramatyki LL(1) Zbiór FIRST Analiza zstępująca Metoda zejść rekurencyjnych Eliminacja lewostronnej rekurencji Lewostronna faktoryzacja Analiza składniowa (54) Przejdźmy do podsumowania wykładu. W wykładzie podano definicję prostych gramatyk LL(1). Przedstawiono sposób obliczania zbioru FIRST. Następnie zaprezentowano sposób implementacji analizatora zstępującego oparty na metodzie zejść rekurencyjnych. Przedstawiono popularne problemy, na które może natrafić osoba wykorzystująca tę metodę oraz sposoby ich rozwiązywania. Pokazano eliminację lewostronnej rekurencji oraz lewostronną faktoryzację. 54

Literatura Aho A. V., Sethi R., Ullman J. D., Kompilatory Reguły, metody i narzędzia, WNT 2002. Tremblay J. P., Sorenson P. G., The Theory and Practice of Compiler Writing, McGraw-Hill, 1985 Wilhelm R., Maurer D., Compiler Design, Addison-Wesley 1995 Analiza składniowa (55) Więcej informacji na temat omówionych w wykładzie zagadnień można znaleźć w literaturze przedstawionej na slajdzie. 55