Plan wykładu Wojciech Complak Wojciech.Complak@cs.put.poznan.pl charakterystyka generatora LLgen składnia specyfikacji analizatora składniowego dołączanie analizatora leksykalnego rozszerzenia składni gramatyk bezkontekstowych środowisko generatora przykładowa gramatyka i jej implementacja w generatorze LLgen mechanizmy statycznego i dynamicznego rozstrzygania konfliktów 2.02 Generator LLGen (2/34) - charakterystyka -składnia specyfikacji LLgen jest generatorem parserów, dystrybuowanym w ramach pakietu ACK (The Amsterdam Compiler Kit) jak i jako samodzielny produkt (obecnie rozwijany i dystrybuowany w oparciu o licencję BSD przez SourceForge.net z jego pomocą zaimplementowano translatory dla wielu języków programowania (m. in. Basic, C, Pascal, Modula-2, occam) LLgen generuje parsery działające metodą zejść rekurencyjnych bez nawrotów w specyfikacji analizatora można korzystać z rozszerzonych gramatyk LL(1) oraz gramatyk niejednoznacznych (dostępne są mechanizmy statycznego i dynamicznego rozstrzygania konfliktów) na podstawie specyfikacji generowana jest implementacja analizatora składniowego w języku C specyfikację parsera należy przygotować w pliku tekstowym specyfikacja zawiera produkcje, dyrektywy LLgena oraz kod i deklaracje w języku C każda produkcja składa się z nieterminala, znaku :, prawej strony produkcji i jest zakończona znakiem po prawej stronie produkcji mogą znajdować się symbole terminalne, nieterminalne i akcje semantyczne alternatywne prawe strony produkcji rozdziela się znakiem białe spacje są ignorowane, ale nie mogą występować w obrębie nazw komentarze można umieszczać wszędzie tam, gdzie dozwolona jest nazwa komentarze mają postać taką jak w języku ANSI C (/* */) i nie mogą być zagnieżdżane Generator LLGen (3/34) Generator LLGen (4/34) -składnia specyfikacji -składnia specyfikacji nazwy symboli terminalnych i nieterminalnych mogą być dowolnej długości nazwy symboli mają składnię taką jak identyfikatory języka C: mogą składać się z liter, znaku podkreślenia ('_') i cyfr dziesiętnych (nie mogą rozpoczynać się od cyfry) wielkość liter jest rozróżniana nazwy symboli nie mogą kolidować ze słowami kluczowymi języka C nazwy symboli mogą być dowolnej długości, ale: w samym LLgenie znaczących jest 50 pierwszych znaków nazwy należy uwzględnić ograniczenia liczby znaczących znaków nakładane przez docelowy kompilator języka C i konsolidator nazwy wszystkich symboli wykorzystywanych i generowanych przez LLgena zaczynają się od liter LL Generator LLGen (5/34) terminale, które nie są literałami (są nazwane) muszą być wcześniej zadeklarowane za pomocą słowa kluczowego %token, np.: %token num każda deklaracja %token musi być zakończona średnikiem w deklaracji można wymienić wiele nazw rozdzielonych przecinkami, np.: %token NUM, LITERAL terminale, które są literałami muszą być ujęte w apostrofy rozpoznawane są następujące literały specjalne: '\n' nowa linia '\r' powrót karetki '\'' apostrof '\\' odwrotny ukośnik '\t' tabulator '\b' wycofanie znaku '\xxx' liczba oktalna Generator LLGen (6/34)
-składnia specyfikacji - analizator leksykalny każda napotkana nazwa, która nie została wcześniej zadeklarowana jako token jest uważana za nieterminal LLgen pozwala generować analizatory z alternatywnymi nieterminalami startowymi aksjomat musi być wskazany za pomocą słowa kluczowego %start, np.: %start parse, S określa, że aksjomatem gramatyki jest nieterminal S, a jego implementacja ma znaleźć się w funkcji o nazwie parse akcje semantyczne można wstawiać w dowolnym miejscu prawej strony produkcji akcje to pojedyncza instrukcja albo instrukcje języka C ujęte w nawiasy klamrowe każdy nieterminal jest implementowany jako funkcja języka C, zmienne lokalne funkcji można zadeklarować po lewej stronie produkcji za symbolem nieterminalnym w nawiasach klamrowych, np.: S int zm : '(' num suma = 0 R Generator LLGen (7/34) domyślnie LLgen korzysta z zewnętrznego analizatora leksykalnego generowanego przez LEXa (wywoływana jest funkcja yylex()) w celu skorzystania z innego analizatora niż generowany przez LEXa należy: umieścić implementację w jednym z dwu miejsc: o bezpośrednio w specyfikacji gramatyki (w bloku ) o w zewnętrznym pliku wskazać (po słowie kluczowym %lexical) nazwę funkcji, która ma być wywoływana przez LLgena do skanowania wejścia, np.: %lexical my_yylex Generator LLGen (8/34) - analizator leksykalny - analizator leksykalny w trakcie generacji parsera LLgen tworzy plik interfejsu Lpars.h, który zawiera definicje (#define) przypisujące stałe liczbowe nazwom zadeklarowanych tokenów jeżeli zostały zadeklarowane jakieś tokeny plik Lpars.h należy włączyć dyrektywą #include "Lpars.h" do analizatora leksykalnego szkielet skanera: schemat współpracy analizatora leksykalnego i składniowego a 1 a i a n $ % int yywrap(void) int yylex(void) #include "Lpars.h" % /* reguły */ int yywrap(void) return 1 skaner parser token zwracany przez yylex() atrybut ustawiany przez skaner w zmiennej LLlval Generator LLGen (9/34) Generator LLGen (10/34) : generacja parsera : opcja v, funkcja main() na wejście generatora LLgen podajemy specyfikację parsera (plik gram.g) na wyjściu generatora LLgen otrzymujemy: implementację parsera w języku C w plikach: gram.c (implementacja nieterminali, funkcje main() i LLmessage()) Lpars.c (tablica sterująca i szkielet parsera, funkcja parse()) definicję interfejsu parser/skaner: plik Lpars.h specyfikacja analizatora składniowego (gram.g) LLgen gram.g gram.c Lpars.c Lpars.h analizator leksykalny (domyślnie funkcję yylex()) można: napisać bezpośrednio w języku C utworzyć za pomocą generatora analizatorów leksykalnych (LEX) Generator LLGen (11/34) na etapie uruchamiania i testowania parsera może być użyteczna opcja -v, powodująca wygenerowanie pliku LL.output zawierającego informacje o nierozwiązanych konfliktach w gramatyce opcji -v można używać do trzech razy w jednym wywołaniu, każde kolejne użycie podnosi poziom szczegółowości komunikatów, trzykrotne daje kompletny opis gramatyki użytkownik musi także dostarczyć implementację funkcji main() jeśli symbol startowy został zadeklarowany jako: %start parse, S to w najprostszym przypadku ciało funkcji main wywołuje funkcję parse: int main() parse() return 0 Generator LLGen (12/34)
Kompilacja kompletnego analizatora: LEX + LLgen Rozszerzenia składni gramatyk bezkontekstowych scan.l lex lex.yy.c gram.g lex.yy.c gram.c Lpars.c Lpars.h LLgen cc gram.c Lpars.c Lpars.h a.out Generator LLGen (13/34) w specyfikacji analizatora można korzystać z rozszerzeń składni gramatyk bezkontekstowych, do których należą: domknięcie zwrotne * z opcjonalnym ograniczeniem liczby powtórzeń *n język a*b S : 'a'* 'b' domknięcie dodatnie + z opcjonalnym ograniczeniem liczby powtórzeń +n S : 'a'+ 'b' język a+b operator opcjonalności? (równoważne *1) możliwość grupowania symboli [ ] (i kodu wewnątrz grupy) S : 'a' R 'b' R S : [ 'a' 'b' ] R Generator LLGen (14/34) przykład: napisać akceptor dla języka bezkontekstowego a n b n dla n>0 1. z wykorzystaniem gramatyki LL(1) 2. z wykorzystaniem rozszerzeń LLgena w akceptorze zostaną wykorzystane dwie globalne ( ) zmienne typu całkowitego int l_a i l_b do zliczania liczby wystąpień poszczególnych liter w aksjomacie gramatyki (S) po przetworzeniu ciągu liter a (nieterminal A) i ciągu liter b (nieterminal B) porównane zostaną wartości zmiennych i wydrukowany odpowiedni komunikat int l_a, l_b %start parse, S S : l_a = l_b = 0 A B if(l_a == l_b)puts("ok") else puts("error") powtarzanie liter a w ciągu możemy opisać za pomocą lewostronnej rekurencji: A : 'a' l_a = 1 A 'a' l_a++ B : 'b' l_b = 1 B 'b' l_b++ taka gramatyka nie jest jednak LL(1)! Generator LLGen (15/34) Generator LLGen (16/34) jeśli opiszemy powtarzanie liter a za pomocą prawostronnej rekurencji: A : 'a' l_a = 1 'a' A l_a++ B : 'b' l_b = 1 'b' B l_b++ mamy z kolei problem z konfliktem alternatyw trzeba przeprowadzić lewostronną faktoryzację przeprowadzenie lewostronnej faktoryzacji wymaga wprowadzenia dodatkowych nieterminali i odpowiedniej modyfikacji akcji A : 'a' RA l_a=1 RA : 'a' RA l_a++ B : 'b' RB l_b=1 RB : 'b' RB l_b++ Generator LLGen (17/34) Generator LLGen (18/34)
: obsługa błędów dzięki wykorzystaniu rozszerzeń LLgena (grupowanie i operator domknięcia dodatniego) możemy uniknąć modyfikowania gramatyki (ewentualnie usunąć nieterminale A i B): S : l_a = l_b = 0 A B if(l_a==l_b)puts("ok") else puts("error") A : [ 'a' l_a++ ] + B : [ 'b' l_b++ ] + S : l_a = l_b = 0 [ 'a' l_a++ ] + [ 'b' l_b++ ] + if(l_a==l_b)puts("ok") else puts("error") do obsługi błędów składniowych w LLgenie przewidziana jest funkcja LLmessage, której implementację dostarcza użytkownik funkcja LLmessage jest automatycznie wywoływana przez parser wygenerowany przez LLgena w przypadku wystąpienia błędu składniowego implementacja funkcji LLmessage może podjąć różne działania: wyświetlić komunikat o błędzie (analizator pracujący w trybie wsadowym) albo zachętę do ponownego wprowadzenia danych (tryb interaktywny) kontynuować albo przerwać analizę danych wejściowych w przypadku kontynuacji podjąć próbę podniesienia z błędu (naprawienia stanu) Generator LLGen (19/34) Generator LLGen (20/34) : obsługa błędów przy implementacji funkcji LLmessage może być przydatna zmienna LLsymb (typu int) przechowująca ostatnio wczytany (podglądany) token funkcja LLmessage ma jeden parametr typu całkowitego tk zawierający informację o typie błędu i nie zwraca żadnej wartości: void LLmessage(int tk) tk > 0 : oczekiwany był token tk a napotkano token LLsymb (aby podnieść się z błędu należy wstawić token tk przed bieżącym symbolem znajdującym się w LLsymb analizator leksykalny musi pozwalać na wycofanie jednej jednostki leksykalnej!) 0 : nieoczekiwany token (LLsymb) został napotkany i usunięty, -1 : parser nie napotkał oczekiwanego końca pliku (napotkany token jest w LLsymb), reszta wejścia zostanie pominięta Generator LLGen (21/34) przykład: na wejściu znajduje się ciąg nieujemnych liczb całkowitych ujęty w nawiasy okrągłe, np. (1,2,3) ciąg zawiera co najmniej jedną liczbę, jeśli jest ich więcej są rozdzielone przecinkami. Napisać analizator składniowy, który sprawdzi poprawność wprowadzonych danych. ( 1 ( (, (1 ) 3 LLmessage: unexpected token '(' deleted in line 3 LLmessage: expected token <num> not found, unexpected token ',' encountered in line 2 LLmessage: expected EOF not encountered, unexpected token <num> found in line 3, skipping extra input Generator LLGen (22/34) analizator leksykalny implementujemy w LEXie % #include <stdlib.h> int yywrap(void) int yylex(void) #include "Lpars.h" % \( return '(' \) return ')' \, return ',' [0-9]+ return num [ \t\n]. printf("unexpected character %c in line %d\n", yytext[0], yylineno) exit(exit_failure) int yywrap(void) return 1 Generator LLGen (23/34) analizator składniowy definicje, gramatyka #include <stdlib.h> extern int yylineno %token num %start parse, S S : '(' num R R : ')' ',' num R Generator LLGen (24/34)
analizator składniowy definicje, gramatyka z wykorzystaniem rozszerzeń LLgena #include <stdlib.h> extern int yylineno %token num %start parse, S S : '(' num [ ',' num ]* ')' analizator składniowy funkcja print_token() void print_token(int t) switch(t) case num : printf("<num>") break case EOFILE : printf("<eof>") break default : if(isprint(t)) printf("'%c'",t) else printf("[%d]",t) Generator LLGen (25/34) Generator LLGen (26/34) Rozwiązywanie konfliktów analizator składniowy funkcja LLmessage() void LLmessage(int tk) printf("llmessage: ") switch(tk) case -1 : printf("expected EOF not encountered, unexpected token ") print_token(llsymb) printf(" found in line %d, skipping extra input\n", yylineno) exit(exit_failure) case 0 : printf("unexpected token ") print_token(llsymb) printf(" deleted in line %d\n", yylineno) exit(exit_failure) default : printf("expected token ") print_token(tk) printf(" not found, unexpected token ") print_token(llsymb) printf(" encountered in line %d\n", yylineno) exit(exit_failure) Generator LLGen (27/34) generując parser działający metodą rekurencyjnych zejść bez nawrotów możemy mieć do czynienia z dwoma typami konfliktów: konfliktem alternatyw nie wiadomo, którą z prawych stron produkcji wybrać konfliktem powtórzeń aktualnie rozwijany nieterminal zawiera domknięcie i nie wiadomo czy wejście jest jego dalszym ciągiem czy też rozpoczyna inną produkcję do rozwiązywania konfliktu alternatyw służą odpowiednie słowa kluczowe: do dynamicznego rozstrzygania: %if(warunek) do statycznego rozstrzygania: %prefer (równoważne %if(1)) %avoid (równoważne %if(0)) Generator LLGen (28/34) Rozwiązywanie konfliktów przykład użycia %if Rozwiązywanie konfliktów przykład użycia %prefer przykład: badanie parzystości liczby binarnej zapisanej począwszy od najstarszej cyfry (odpowiedź będzie znana po wczytaniu ostatniej cyfry) analizator leksykalny ma następującą postać: [01] return yytext[0] int parity %start parse, S S : '0' parity = 0 R '1' parity = 1 R R : %if(parity == 0) puts("even") puts("odd") S Generator LLGen (29/34) przykładem użycia mechanizmu statycznego rozwiązywania konfliktu alternatyw za pomocą słowa kluczowego %prefer jest instrukcja warunkowa z opcjonalną częścią else problem tzw. wiszącego else występuje w tych językach (np.: C, C++, Pascal), w których: instrukcja warunkowa ma opcjonalną część else a jednocześnie nie ma słowa (słów) kluczowego kończącego instrukcję warunkową w produkcji opisującej składnię instrukcji warunkowej wskazujemy, że część else ma być powiązana z bezpośrednio poprzedzającą ją częścią ifthen: stmnt : IF exp THEN stmnt [ %prefer ELSE stmnt ] gdzie stmnt to instrukcja, a IF, THEN i ELSE to słowa kluczowe języka źródłowego Generator LLGen (30/34)
Rozwiązywanie konfliktów powtórzeń Rozwiązywanie konfliktów powtórzeń do rozwiązywania konfliktu powtórzeń służy słowo kluczowe %while: %while(warunek) wejście będzie traktowane jako należące do rozwinięcia bieżącego symbolu dopóki warunek będzie prawdziwy. zapis warunku często wymagania obliczenia zbioru First LLgen pozwala zmniejszyć pracochłonność obliczania warunku dzięki użyciu makra języka C generowanego na podstawie słowa kluczowego %first, np. zapis: %first fmac, nonterm generuje jednoargumentowe makro fmac(nonterm) testujące czy token podany jako argument należy do zbioru First(nonterm) Generator LLGen (31/34) jako przykład użycia słowa kluczowego %while do rozstrzygnięcia konfliktu powtórzeń rozważmy następujący język: %start parse, S S : N * 'x' "gram.g", line 8: Repetition conflict N : 'x' 'y' 'z' problem: analizator widząc znak x nie wie czy rozpoczyna on produkcję N x y czy kończy S N* x xyx$?? Generator LLGen (32/34) Rozwiązywanie konfliktów Dalsza lektura rozwiązanie konfliktu powtórzeń wymaga: 1. modyfikacji gramatyki (pojawia się jednak konflikt alternatyw obie alternatywy mogą być napisami pustymi) 2. rozbudowania skanera o podgląd: jednego symbolu z wejścia pozwoli rozwiązać konflikt alternatyw dwóch symboli z wejścia pozwoli rozwiązać konflikt powtórzeń 3. wykorzystania podglądów w celu rozwiązania konfliktów w gramatyce %start parse, S S : XY 'x' XY : %if(lookahead1() == 'z') 'z' * [ %while(lookahead2() == 'y') 'x' 'y' ] * Generator LLGen (33/34) gramatyki LL: Aho A. V., Sethi R., Ullman J. D., Compilers: Principles, Techniques, and Tools, Addison-Wesley, 1986 Bauer F. L., Eickel J., Compiler Construction: An Advanced Course, Springer-Verlag, 1976 generator LLgen: Grune D., Jacobs C. J. H., A Programmer-friendly LL(1) Parser Generator, Software - Practice and Experience, vol. 18, no. 1, pp. 29-38, 1/1988 Jacobs C. J. H., LLgen, an extended LL(1) parser generator, tack.sourceforge.net/olddocs/llgen.pdf Grune D., Bal H., Jacobs C., Langendoen K., Modern Compiler Design, John Wiley & Sons, 2000 Generator LLGen (34/34)