Generator YACC: gramatyki niejednoznaczne Wojciech Complak Wojciech.Complak@cs.put.poznan.pl 1
Plan wykładu gramatyki jednoznaczne i niejednoznaczne zalety gramatyk niejednoznacznych opisywanie łączności i priorytetów w generatorze YACC problem tzw. wiszącego else używanie przypadków specjalnych w gramatykach niejednoznacznych Generator YACC: gramatyki niejednoznaczne (2) W ramach wykładu zostaną przedstawione następujące zagadnienia: definicja gramatyk jednoznacznych i niejednoznacznych zalety gramatyk niejednoznacznych opisywanie łączności i priorytetów w generatorze YACC problem tzw. wiszącego else używanie przypadków specjalnych w gramatykach niejednoznacznych 2
Gramatyki jednoznaczne i niejednoznaczne gramatyka jest jednoznaczna wtedy, gdy dla każdego zdania należącego do języka opisanego gramatyką istnieje tylko jedno drzewo składniowe drzewo składniowe opisuję składnię, a nie znaczenie (semantykę), nie odzwierciedla więc kolejności zastępowania symboli w formach zdaniowych każdemu drzewu składniowemu odpowiada jeden unikalny wywód prawostronny (YACC) Generator YACC: gramatyki niejednoznaczne (3) W ramach dotychczasowych wykładów rozważana była tylko implementacja gramatyk jednoznacznych w YACCu. Gramatyka jest jednoznaczna wtedy i tylko wtedy, gdy dla każdego zdania języka wejściowego istnieje tylko jedno drzewo składniowe (wywodu). W przeciwnym przypadku gramatyka jest niejednoznaczna. Słowo niejednoznaczna sugeruje, że jest to gorsza gramatyka niż jednoznaczna. Jak zobaczymy, w praktyce zastosowanie gramatyki niejednoznacznej może okazać się lepszym rozwiązaniem niż równoważnej jednoznacznej. Trzeba także pamiętać, że drzewo składniowe (jak sama nazwa zresztą wskazuje) opisuje składnię a nie znaczenie (semantykę) zdania. Nie odzwierciedla więc kolejności zastępowania symboli w formach zdaniowych (porządku wykonywania akcji). Każdemu drzewu wywodu odpowiada jedno unikalne wyprowadzenie prawostronne (jedno lewostronne zresztą też ale w ramach wykładu rozważamy LR-parsery wygenerowane przez YACCa, które konstruują odwrotność prawostronnego wywodu). 3
Gramatyki jednoznaczne i niejednoznaczne rozważmy niejednoznaczną gramatykę dla ciągu liczb rozdzielonych znakami odejmowania: E E - E E num sprawdźmy teraz: jak będzie wyglądać drzewo składniowe dla zdania num num num jaki będzie wynik (i kolejność) obliczeń analizatora wygenerowanego przez YACCa Generator YACC: gramatyki niejednoznaczne (4) Rozważmy przedstawioną w przykładzie niejednoznaczną gramatykę opisującą ciąg liczb rozdzielonych znakami odejmowania. Wyrażenie (E) składa się z wyrażeń (E) rozdzielonych znakiem odejmowania ( - ) albo pojedynczej liczby. Prześledźmy działanie gramatyki dla wejścia num - num - num. W tym celu skonstruujmy drzewo (albo drzewa jeśli będzie ich więcej) składniowe dla tego wyrażenia i sprawdźmy działanie analizatora wygenerowanego przez YACCa w oparciu o tę gramatykę. 4
Drzewa wywodu zdania num num - num dla zdania num - num - num istnieją dwa drzewa składniowe lewostronna łączność operatora odejmowania E prawostronna łączność operatora odejmowania E E '-' E E '-' E E '-' E num num E '-' E num num num num Generator YACC: gramatyki niejednoznaczne (5) Widać teraz, że gramatyka jest niejednoznaczna dla tego samego wyrażenia (zdania) zbudowaliśmy dwa różne drzewa składniowe. Drzewa odzwierciedlają różne interpretacje semantyki operatora odejmowania. Zgodnie z lewym drzewem operator odejmowania ma lewostronną (tradycyjną) łączność, zgodnie z prawym prawostronną (zazwyczaj błędną). 5
Gramatyka niejednoznaczna skaner analizator leksykalny ma za zadanie: rozpoznać i zwrócić znak odejmowania ( - ) rozpoznać liczbę całkowitą, dokonać jej konwersji i zwrócić jako atrybut symbolu NUM %{ #include <stdlib.h> #include "y.tab.h" %} %% \- { return '-' } [0-9]+ { yylval = atoi(yytext) return NUM } Generator YACC: gramatyki niejednoznaczne (6) Sprawdźmy teraz jak będzie działał analizator zaimplementowany z wykorzystaniem generatorów LEX i YACC. Analizator leksykalny zawiera dwie reguły: rozpoznaje i zwraca znak odejmowania rozpoznaje liczbę całkowitą, konwertuje ją do postaci binarnej i ustawia wartość atrybutu zwracanego symbolu NUM 6
Gramatyka niejednoznaczna z nieterminalem E wiążemy akcje semantyczne: dla produkcji E E E rezultatem będzie różnica argumentów operatora odejmowania dla produkcji E num rezultatem będzie wartość liczby %token NUM %% S : E { printf("%d",$1) } E : E '-' E { $$ = $1 - $3 } NUM { $$ = $1 } Generator YACC: gramatyki niejednoznaczne (7) Analizator składniowy jest również relatywnie prosty. Z produkcjami dla nieterminala E wiążemy odpowiednie akcje: dla produkcji E > E E rezultatem będzie różnica argumentów operatora odejmowania dla produkcji E > num rezultatem będzie wartość liczby. Z przyczyn technicznych dodajemy jeszcze produkcję jednostkową S > E, która przyda się do wypisania końcowego rezultatu obliczeń. 7
Gramatyka niejednoznaczna w trakcie generowania analizatora przez YACCa otrzymujemy informację o pojedynczym konflikcie przesuń/redukuj: State 5 E: E.'-' E (2) E: E '-' E. [ $end '-' ] Shift/reduce conflict (4,2) on '-' '-' shift 4. reduce (2) Conflicts: State Token Action 5 '-' shift 4 5 '-' reduce (2) Generator YACC: gramatyki niejednoznaczne (8) W trakcie generowania analizatora składniowego przez YACCa otrzymujemy informację o pojedynczym konflikcie przesuń/redukuj. Jeżeli użyjemy opcji v w wywołaniu generatora otrzymamy plik Y.OUT, który zawiera szczegółowe informacje o stanach i akcjach parsera, a w szczególności o tym, jakie konflikty występują w gramatyce. Dla przykładowej gramatyki konflikt wystąpił w stanie 5. Gdy głowica czytająca znajduje się za symbolem E, a na wejściu widoczny jest znak odejmowania, parser może: wykonać przesunięcie znaku odejmowania i przejść do stanu 4. albo dokonać redukcji zgodnie z produkcją 2. (E -> E - E) Dokładniej oznacza to, że na stosie znajduje się już ciąg symboli E-E i parser musi podjąć decyzję o tym, czy zredukować trzy symbole z wierzchołka stosu (i przy okazji wykonać ewentualną akcję skojarzoną z 2. produkcją), czy też kontynuować przesuwając symbol terminalny - na stos. Konflikt z powodu braku jakichkolwiek innych wskazówek zostaje rozstrzygnięty w oparciu o domyślne reguły na korzyść przesunięcia. 8
Gramatyka niejednoznaczna po skompilowaniu analizatora testujemy jego działanie dla wejścia 1-2 - 3, zamiast odpowiedzi -4 otrzymujemy jednak 0 analiza wywodu: E[0] E[1]-E[-1] E[1]-E[2]-E[3] E[1]-E[2]-num[3] E[1]-num[2]-num[3] num[1]-num[2]-num[3] wskazuje, że problem leży w kolejności redukcji symboli formy zdaniowej E - E - E Generator YACC: gramatyki niejednoznaczne (9) Analizator kompilujemy i testujemy jego działanie podając jako przykładowe wejście wyrażenie 1 2-3. Operator odejmowania jest lewostronnie łączny, więc wyrażenie powinno być zinterpretowane jako (1-2) - 3, a zatem powinniśmy otrzymać odpowiedź -4. Analizator podaje jednak odpowiedź 0. Analiza wywodu testowanego wejścia wskazuje, że wygenerowany przez YACCa analizator składniowy wybrał nie to drzewo składniowe, o które nam chodziło. Wyrażenie E -E-Ezostało zredukowane w złej kolejności tak jakby operator odejmowania miał prawostronne wiązanie. Składnia jest więc opisana poprawnie, a zły rezultat obliczeń wynika z niewłaściwego porządku wykonania akcji (złej kolejności zastępowania symboli w formach zdaniowych). 9
Gramatyka niejednoznaczna operator odejmowania jest lewostronnie łączny, powinniśmy więc wybrać interpretację (1-2) - 3 a została wybrana (zgodnie z domyślnymi regułami YACCa): 1 - (2-3) w trakcie generowania analizatora otrzymujemy komunikat o konflikcie przesunięcie/redukcja w pozycji E E - E - E sposób rozstrzygnięcia tego konfliktu decyduje o łączności operatora (przesunięcie łączność prawostronna, redukcja łączność lewostronna) Generator YACC: gramatyki niejednoznaczne (10) Skoro operator odejmowania jest lewostronnie łączny powinniśmy wybrać inną interpretację niż przyjęta domyślnie przez YACCa. Jak już widzieliśmy - w trakcie generowania analizatora YACC wyświetla informację o konflikcie przesunięcie/redukcja w pozycji: E > E - E. - E i z braku jakichkolwiek wskazówek automatycznie rozstrzyga konflikt w oparciu o domyślne reguły. Właśnie sposób rozstrzygnięcia tego konfliktu decyduje o interpretacji łączności operatora odejmowania. Jeśli zostanie wybrane przesunięcie operator będzie traktowany jak prawostronnie łączny, jeżeli redukcja jak lewostronnie łączny. 10
Gramatyka niejednoznaczna domyślne reguły rozstrzygania konfliktów przez generator YACCa: konflikt przesunięcie/redukcja jest rozstrzygany na korzyść przesunięcia konflikt redukcja/redukcja - na korzyść redukcji zgodnie z wcześniejszą tekstową produkcją w specyfikacji w rozpatrywanym przykładzie domyślna reguła rozstrzygania konfliktów nie dała oczekiwanej łączności operatora odejmowania Generator YACC: gramatyki niejednoznaczne (11) Zgodnie z domyślnymi regułami generator YACC konflikt przesunięcie/redukcja rozstrzyga na korzyść przesunięcia, a konflikt redukcja/redukcja na korzyść redukcji zgodnie z produkcją, która wystąpiła tekstowo wcześniej w pliku specyfikacji. Jak widać w rozpatrywanym przykładzie domyślne reguły niekoniecznie rozstrzygają konflikty zgodnie z oczekiwaniami. Dlatego więc w YACCu przewidziano odpowiednie mechanizmy pozwalające na jawne wskazanie sposobu rozwiązywania konfliktów. 11
Gramatyka niejednoznaczna czy jest w takim razie sens stosować gramatyki niejednoznaczne? gramatyki niejednoznaczne mają istotne zalety: są proste, łatwiej je modyfikować i rozbudowywać analizatory są efektywniejsze łatwo jest uwzględnić przypadki specjalne konieczne jest jedynie odpowiednie wskazanie sposobu rozstrzygnięcia konfliktów za pomocą słów kluczowych YACCa Generator YACC: gramatyki niejednoznaczne (12) Skoro gramatyki niejednoznaczne wymagają specjalnych zabiegów by wygenerowane na ich podstawie analizatory działały zgodnie z oczekiwaniami, to czy nie lepiej używać gramatyk jednoznacznych? Nie, gramatyki niejednoznaczne mają istotne zalety: opis języka z ich wykorzystaniem jest relatywnie prosty i zrozumiały, a jednocześnie łatwiejszy do modyfikacji i rozbudowy, analizatory skonstruowane z ich wykorzystaniem są szybsze, łatwiej jest uwzględniać przypadki specjalne. Korzystanie z zalet gramatyk niejednoznacznych w YACCu wymaga jedynie wskazania odpowiedniego sposobu rozstrzygnięcia konfliktów za pomocą przeznaczonych do tego celu słów kluczowych generatora. 12
Porównanie złożoności gramatyk gramatyka jednoznaczna gramatyka niejednoznaczna %token NUM %% S : E {printf("%d",$1)} E : E '+' T {$$=$1+$3} E '-' T {$$=$1-$3} T {$$=$1} T : T '*' F {$$=$1*$3} T '/' F {$$=$1/$3} F {$$=$1} F : '(' E ')' {$$=$2} NUM {$$=$1} '+' F {$$=$2} '-' F {$$=-$2} %token NUM %left '+' '-' %left '*' '/' %left UMINUS %% L : E {printf("%d",$1)} E : E '+' E {$$=$1+$3} '+' E %prec UMINUS {$$=$2} E '-' E {$$=$1-$3} '-' E %prec UMINUS {$$=-$2 } E '*' E {$$=$1*$3} E '/' E {$$=$1/$3} '(' E ')' {$$=$2} NUM {$$=$1} Generator YACC: gramatyki niejednoznaczne (13) Spróbujmy teraz wykazać w praktyce, że wymienione zalety gramatyk niejednoznacznych mają istotne znaczenie praktyczne. Wykorzystajmy do tego celu gramatykę kalkulatora uwzględniającego operacje dodawania, odejmowania, mnożenia, dzielenia, nawiasy oraz unarny minus i plus. W przykładzie przedstawiono dwie wersje gramatyki dla takiego kalkulatora: -po lewej gramatykę jednoznaczną -po prawej niejednoznaczną. 13
Porównanie złożoności gramatyk gramatyka jednoznaczna /* 1 */ E : E '+' T /* 2 */ E '-' T /* 3 */ T /* 4 */ T : T '*' F /* 5 */ T '/' F /* 6 */ F /* 7 */ F : '(' E ')' /* 8 */ NUM /* 9 */ '+' F /* 10 */ '-' F gramatyka niejednoznaczna /* 1 */ E : E '+' E /* 2 */ '+' E /* 3 */ E '-' E /* 4 */ '-' E /* 5 */ E '*' E /* 6 */ E '/' E /* 7 */ '(' E ')' /* 8 */ NUM Generator YACC: gramatyki niejednoznaczne (14) Sprawdźmy teraz, czy rzeczywiście gramatyka niejednoznaczna jest prostsza. W inżynierii oprogramowania jako miarę złożoności gramatyk przyjmuje się liczbę nieterminali i liczbę produkcji (im liczby te są większe, tym złożoność jest większa). Liczba terminali nie zależy od gramatyki tylko od języka wejściowego. W porównaniu nie weźmiemy pod uwagę produkcji S > E, ponieważ została dodana tylko ze względów technicznych do drukowania wyniku. Wyniki porównania wskazują, że gramatyka niejednoznaczna rzeczywiście jest prostsza: w gramatyce jednoznacznej są 3 nieterminale (E, T, F), w niejednoznacznej 1 (E) w gramatyce jednoznacznej mamy 10 produkcji, w niejednoznacznej - 8. Jako porównanie trudności rozbudowywania obu typów gramatyk warto spróbować rozbudować przedstawioną gramatykę jednoznaczną o: niewiążący operator porównania = prawostronnie łączny operator potęgowania '^ a następnie porównać efekt z przedstawionymi dalej rozwiązaniami dla gramatyki niejednoznacznej. 14
Porównanie wydajność gramatyk gramatyka jednoznaczna %token NUM %% S : E {printf("%d",$1)} E : E '+' T {$$=$1+$3 printf("r1(e[%d]->e[%d]+t[%d]) ",$$,$1,$3)} E '-' T {$$=$1-$3 printf("r2(e[%d]->e[%d]-t[%d]) ",$$,$1,$3)} T {$$=$1 printf("r3(e[%d]->t[%d]) ",$$,$1)} T : T '*' F {$$=$1*$3 printf("r4(t[%d]->t[%d]*f[%d]) ",$$,$1,$3)} T '/' F {$$=$1/$3 printf("r5(t[%d]->t[%d]/f[%d]) ",$$,$1,$3)} F {$$=$1 printf("r6(t[%d]->f[%d]) ",$$,$1)} F : '(' E ')' {$$=$2 printf("r7(f[%d]->(e[%d])) ",$$,$2)} NUM {$$=$1 printf("r8(f[%d]->num[%d]) ",$$,$1)} '+' F {$$=$2 printf("r9(f[%d]->+f[%d]) ",$$,$2)} '-' F {$$=-$2 printf("r10(f[%d]->-f[%d]) ",$$,$2)} Generator YACC: gramatyki niejednoznaczne (15) Porównajmy teraz efektywność czasową obu rozwiązań. Jako miarę złożoności czasowej przyjmuje się liczbę wykonywanych redukcji (akcje są związane z redukcjami, a liczba przesunięć nie zależy od gramatyki tylko od rozmiaru wejścia). Zmodyfikujemy do tego celu analizatory tak, aby w trakcie przetwarzania wypisywały na ekran informacje o stosowanych produkcjach. Przy okazji analizy gramatyki jednoznacznej warto zwrócić uwagę, że: produkcje jednostkowe E > T i T > F służą wyłącznie do zróżnicowania priorytetów operatorów addytywnych ( +, - ) i multiplikatywnych ( *, / ) lewo/prawostronna rekurencja służy do nadawania łączności operatorom. O ile typ rekurencji będzie tak samo wpływać na wydajność gramatyki jednoznacznej i niejednoznacznej, o tyle konieczność użycia produkcji jednostkowych ewidentnie będzie miała niekorzystny wpływ na wydajność gramatyki jednoznacznej. 15
Porównanie wydajność gramatyk gramatyka niejednoznaczna %token NUM %left '+' '-' %left '*' '/' %left UMINUS %% L : E { printf("%d",$1) } E : E '+' E {$$=$1+$3 printf("r1(e[%d]->e[%d]+e[%d]) ",$$,$1,$3)} '+' E %prec UMINUS {$$=$2 printf("r2(e[%d]->+e[%d]) ",$$,$2)} E '-' E {$$=$1-$3 printf("r3(e[%d]->e[%d]-e[%d]) ",$$,$1,$3)} '-' E %prec UMINUS {$$=-$2 printf("r4(e[%d]->-e[%d]) ",$$,$2)} E '*' E {$$=$1*$3 printf("r5(e[%d]->e[%d]*e[%d]) ",$$,$1,$3)} E '/' E {$$=$1/$3 printf("r6(e[%d]->e[%d]/e[%d]) ",$$,$1,$3)} '(' E ')' {$$=$2 printf("r7(e[%d]->(e[%d])) ",$$,$2)} NUM {$$=$1 printf("r8(e[%d]->num[%d]) ",$$,$1)} Generator YACC: gramatyki niejednoznaczne (16) W YACCu do rozstrzygania konfliktów w gramatyce niejednoznacznej będziemy używać specjalnych słów kluczowych (m. in. %left i %prec), których użycie będzie omówione w ramach bieżącego wykładu. 16
Porównanie wydajność gramatyk złożoność czasowa dla najprostszego poprawnego wejścia (pojedynczej liczby), np.: 2 gramatyka jednoznaczna 3 redukcje: r8(f[2]->num[2]),r6(t[2]->f[2]),r3(e[2]->t[2]) gramatyka niejednoznaczna 1 redukcja: r8(e[2]->num[2]) w tym prostym przypadku różnica wydajności jest bardzo duża (3-krotna) jak będzie wyglądać sytuacja dla większego rozmiaru wejścia? Generator YACC: gramatyki niejednoznaczne (17) Porównanie wydajności czasowej obu analizatorów rozpocznijmy od najprostszego poprawnego wyrażenia pojedynczej liczby np. 2. W gramatyce jednoznacznej potrzebne są 3 redukcje, w niejednoznacznej tylko 1. Widać tu już wyraźnie, że strata wydajności w przypadku gramatyki jednoznacznej rzeczywiście wynika z konieczności wykonywania redukcji zgodnie z jednostkowymi produkcjami E > T i T > F, które nie wykonują żadnych istotnych akcji semantycznych (tylko kopiowanie wartości atrybutów). Jak w takim razie będzie wyglądało porównanie wydajności obu gramatyk dla większych rozmiarów wejścia? 17
Porównanie wydajność gramatyk złożoność czasowa dla wyrażenia: 2+3*4-1 jednoznaczna 11 redukcji: r8(f[2]->num[2]), r6(t[2]->f[2]), r3(e[2]->t[2]), r8(f[3]->num[3]), r6(t[3]->f[3]), r8(f[4]->num[4]), r4(t[12]->t[3]*f[4]), r1(e[14]->e[2]+t[12]), r8(f[1]->num[1]), r6(t[1]->f[1]), r2(e[13]->e[14]-t[1]) niejednoznaczna 7 redukcji: r8(e[2]->num[2]), r8(e[3]->num[3]), r8(e[4]->num[4]), r5(e[12]->e[3]*e[4]), r1(e[14]->e[2]+e[12]), r8(e[1]->num[1]), r3(e[13]->e[14]-e[1]) Generator YACC: gramatyki niejednoznaczne (18) Dla większego rozmiaru wejścia różnica w wydajności wydaje się nie być już aż tak miażdżąca. Np. dla wyrażenia 2+3*4-1 w przypadku gramatyki jednoznacznej parser wykona 11 redukcji, w przypadku gramatyki niejednoznacznej 7. Różnica rzędu 30% może jednak znacząco wpływać na komfort pracy z kompilatorem. Trzeba jednak zauważyć, że dodanie nowych operatorów o odmiennym priorytecie niż już zdefiniowane (np. operatora potęgowania) spowoduje dalszy spadek efektywności analizatora wygenerowanego na podstawie gramatyki jednoznacznej. 18
Gramatyka niejednoznaczna jak opisać łączność i priorytet operatorów w YACCu? większość operatorów arytmetycznych ma lewostronne wiązanie definiujemy ich wiązanie za pomocą słowa kluczowego %left %left '-' %token NUM %% S : E { printf("%d",$1) } E : E '-' E { $$ = $1 - $3 } NUM { $$ = $1 } Generator YACC: gramatyki niejednoznaczne (19) Skoro wykazaliśmy już, że użycie gramatyk niejednoznacznych daje wymierne (a nie tylko teoretyczne) korzyści zobaczmy w jaki sposób rozstrzygać konflikty z użyciem słów kluczowych YACCa. Powróćmy w tym celu do niejednoznacznej gramatyki dla wyrażeń (liczb) rozdzielonych znakami odejmowania. Jeżeli chcemy określić, że jakiś operator (token) ma mieć lewostronne wiązanie należy mu je przypisać za pomocą słowa kluczowego %left. Przy okazji nadajemy operatorowi również relatywny priorytet kolejne tekstowo deklaracje wiązań będą przypisywały operatorom coraz wyższe priorytety. W jednej deklaracji można także przypisać to samo wiązanie wielu operatorom wymieniając je po %left i rozdzielając białymi spacjami wszystkie wymienione na liście operatory będą miały wtedy ten sam priorytet. Na podstawie deklaracji: %left - YACC rozstrzygnie konflikt w sytuacji E > E E. - E zgodnie z naszymi oczekiwaniami na korzyść redukcji (czyli lewostronnego wiązania). 19
Gramatyka niejednoznaczna priorytet instrukcji wynika z priorytetu najbardziej prawego tokenu niewielka (ale nieprzemyślana) modyfikacja gramatyki powoduje powstanie konfliktów %left '-' %token NUM %% S : E { printf("%d",$1) } E : E ' ' '-' ' ' E { $$ = $1 - $5 } NUM { $$ = $1 } Generator YACC: gramatyki niejednoznaczne (20) Warto jednak dokładniej przyjrzeć się mechanizmowi nadawania priorytetów i łączności, ponieważ jedna nieprzemyślana (i wydawałoby się mało istotna) modyfikacja gramatyki może spowodować problemy. Powróćmy do poprzedniego przykładu. Dokładniejsze przyjrzenie się rozstrzygnięciu konfliktu w sytuacji E > E E. - E na korzyść redukcji wywołuje jednak pewne wątpliwości. Na szczycie stosu leży przecież nieterminal, który nie ma (i nie może mieć) ani łączności, ani priorytetu! Rozstrzygnięcie tego konfliktu następuje na podstawie priorytetu produkcji, który jest jej w YACCu nadawany na podstawie priorytetu najbardziej prawego tokenu. Skoro więc priorytet produkcji leżącej na stosie i operatora widzianego na wejściu jest identyczny, to konflikt jest rozstrzygany zgodnie z zadeklarowanym (w tym przypadku lewym) wiązaniem. Zmodyfikujmy gramatykę zakładając, że operator odejmowania musi być otoczony spacjami. Wydawałoby się, że taka modyfikacja nie powinna mieć wpływu na działanie parsera ale... jednak ma, ponownie pojawia się konflikt przesunięcie/redukcja i wszystkie problemy z niego wynikające. Źródłem problemu jest oczywiście dodanie do produkcji E > E E spacji wokół operatora odejmowania. Spacje nie mają ani wiązania, ani priorytetu (swoją drogą jakie miałyby mieć?) a więc gramatyka ponownie stała się niejednoznaczna i YACC wybrał domyślną metodę rozstrzygania konfliktu na korzyść przesunięcia. 20
Gramatyka niejednoznaczna operatory mogą mieć trzy typy wiązań dla każdego typu przewidziano w YACCu odpowiednie słowo kluczowe: %left wiązanie lewostronne %right wiązanie prawostronne %nonassoc brak wiązania Generator YACC: gramatyki niejednoznaczne (21) W ogólności operatory mogą mieć trzy typy wiązań i dla każdego z nich przewidziano odpowiednie słowo kluczowe w YACCu i tak: %left określa wiązanie lewostronne, %right wiązanie prawostronne, %nonassoc brak wiązania. 21
Gramatyka niejednoznaczna operator dodawania dodajmy teraz do gramatyki operator dodawania ( + ) o takim samym priorytecie i wiązaniu jak operator odejmowania ( - ) %left '-' '+' %token NUM %% S : E { printf("%d",$1) } E : E '+' E { $$ = $1 + $3 } E '-' E { $$ = $1 - $3 } NUM { $$ = $1 } Generator YACC: gramatyki niejednoznaczne (22) Uzupełnijmy teraz gramatykę o operator dodawania ( + ). W klasycznej matematyce nie definiuje się łączności operatora dodawania zakładając, że jest ona nieistotna i nie ma wpływu na wynik (co niekoniecznie jest prawdą w obliczeniach przeprowadzonych z użyciem komputerów). W językach programowania zazwyczaj zakłada się, że jeśli chodzi o łączność i priorytet dodawanie należy traktować tak samo jak odejmowanie (są one w pewnym sensie równoważne). Przyjmijmy taką interpretację również w naszym kalkulatorze, ponieważ nie nadanie łączności operatorowi dodawania znowu uczyniłoby gramatykę niejednoznaczną. Aktualizujemy więc deklarację: %token '-' '+' określając w ten sposób, że oba operatory mają mieć identyczne (lewe) wiązanie i takie same priorytety. 22
Gramatyka niejednoznaczna operatory multiplikatywne następnie uzupełnijmy gramatykę o operatory multiplikatywne (mnożenia i dzielenia) i definiujemy ich priorytet i łączność %token NUM %left '+' '-' %left '*' '/' E : E '+' E { $$ = $1 + $3 } E '-' E { $$ = $1 - $3 } E '*' E { $$ = $1 * $3 } E '/' E { $$ = $1 / $3 } NUM { $$ = $1 } Generator YACC: gramatyki niejednoznaczne (23) Gramatykę należy uzupełnić również o operatory multiplikatywne (mnożenia * i dzielenia / ) o wyższym priorytecie niż operatory addytywne ( + i - ). Jeśli chodzi o łączność operatorów można zaobserwować analogię mnożenia do dodawania i odejmowania do dzielenia. I dodawanie i mnożenie w klasycznej matematyce nie muszą mieć określonej łączności w przeciwieństwie do odejmowania i dzielenia. Natomiast w gramatyce oba operatory multiplikatywne muszą mieć lewostronne wiązanie i wyższy priorytet niż addytywne a zatem deklarację: %left '*' '/' umieszczamy po deklaracji: %left '+' '-' 23
Gramatyka niejednoznaczna operator potęgowania w celu przypisania operatorowi prawostronnego wiązania należy użyć słowa kluczowego %right w językach programowania stosunkowo niewiele operatorów ma prawostronne wiązanie, np.: potęgowanie (operator ** albo ^) przypisanie (=) w języku C prawostronne wiązanie operatora potęgowania (np. ^) oznacza, że wyrażenie 2 ^ 3 ^ 2 jest interpretowane jako 2 ^ (3 ^ 2) Generator YACC: gramatyki niejednoznaczne (24) W typowych językach programowania większość operatorów ma lewostronne wiązanie. Do wyjątków należy, dostępny w niektórych językach, operator potęgowania (zapisywany jako ** albo ^), który zwykle ma prawostronne wiązanie. Innym przykładem operatora o prawostronnym wiązaniu jest operator przypisania (=) w języku C (ale np. w Pascalu operator przypisania wcale nie ma wiązania). W celu przypisania jakiemuś operatorowi prawostronnego wiązania należy użyć słowa kluczowego %right. Po użyciu deklaracji: %right ^ wyrażenie: 2 ^ 3 ^ 2 jest interpretowane jako 2 ^ (3 ^ 2) i rezultatem jest 512. Gdyby operator potęgowania był lewostronnie łączny, to samo wyrażenie byłoby interpretowane jako (2 ^ 3) ^ 2 i wynikiem byłoby 81. 24
Gramatyka niejednoznaczna operator potęgowania dodanie do kalkulatora operatora potęgowania np. w postaci takiej jak w języku AWK (prawostronnie wiążący ^ ), wymaga: rozbudowania skanera o regułę: \^ { return '^' } oraz parsera o: deklarację: %right '^ produkcję: E : E '^' E z akcją: {$$=(int)pow($1,$3)} Generator YACC: gramatyki niejednoznaczne (25) W celu rozbudowania kalkulatora o obsługę operatora potęgowania np. w takiej postaci w jakiej występuje w języku AWK (prawostronnie łączny ^ ) należy: rozbudować analizator leksykalny o rozpoznawanie i przekazywanie do analizatora składniowego terminala ^ : \^ { return '^' } rozbudować analizator składniowy o: deklarację: %right ^ z odpowiednim priorytetem potęgowanie zwykle ma dość wysoki priorytet, wyższy od binarnych operatorów arytmetycznych (takich jak +, -, *, /) produkcję: E : E '^' E i związać z nią akcję np. o postaci: { $$ = (int)pow($1,$3) }. pow to funkcja obliczania potęgi dostępna w standardowej bibliotece języka C. 25
Gramatyka niejednoznaczna operatory bez wiązania w celu określenia, że operator nie ma wiązania używamy słowa kluczowego %nonassoc przykładem takiego operatora jest porównanie (=) w Pascalu, w którym poprawne jest wyrażenie: a = b ale nie: a = b = c które należy zapisać np. jako: (a = b) = c modyfikujemy gramatykę dodając: deklarację %nonassoc '=' produkcję E : E '=' E (z odpowiednimi akcjami) Generator YACC: gramatyki niejednoznaczne (26) W celu określenia, że jakiś operator nie ma wiązania używamy słowa kluczowego %nonassoc. Wiązania nie ma np. operator porównania (=) w języku Pascal (podobnie jest np. w Fortranie z.eq.). Brak wiązania oznacza, że można konstruować wyrażenie o postaci: a = b ale nie: a = b = c które należy zapisać np. jako (a = b) = c. W celu wprowadzenia do gramatyki operatora porównania, który nie ma mieć wiązania używamy deklaracji: %nonassoc = i zazwyczaj nadajemy mu niski priorytet po to, aby w wyrażeniach typu: (a + b) = (c + d) można było opuścić nawiasy. Dodajemy również produkcję E : E = E i kojarzymy z nią odpowiednie akcje semantyczne. Konieczne jest oczywiście również odpowiednie rozszerzenie analizatora leksykalnego. W niektórych językach operator porównania jest łączny. Np. język C dopuszcza konstrukcję: 0 == 3 == 0 (natomiast bardzo specyficzna jest jej semantyka ten warunek jest prawdziwy!). Ciekawy jest fakt, że z technicznego punktu widzenia %nonassoc nie rozstrzyga konfliktu przesuń/redukuj. Skoro operator = nie ma mieć łączności, to sytuacja E > E '=' E. '=', to po prostu błąd składniowy. 26
Gramatyka niejednoznaczna unarny minus poprawne rozbudowanie gramatyki o unarny minus wymaga specjalnych zabiegów wydaje się, że wystarczy dodanie produkcji E : - E z akcją semantyczną { $$ = -$2 } czy jest to jednak dobre podejście? generacja analizatora brak konfliktów ( ) testy: - 2-3 => -5 ( ) - 4 * - 2 => 8 ( ) - 8 / - 4 / - 2 => -4 (, powinno być -1) Generator YACC: gramatyki niejednoznaczne (27) Większość języków programowania przewiduje również unarny (jednoargumentowy) minus służący do zmiany znaku wyrażenia, przed którym jest umieszczony. Poprawne obsłużenie go w gramatyce wymaga jednak specjalnych zabiegów. Nie wystarczy dodanie produkcji: E : '-' E { $$ = -$2 } Co prawda, w wyniku dodania tej produkcji nie powstaną konflikty, ale zostaną zaburzone wzajemne relacje priorytetów operatorów (unarny minus musi mieć inny priorytet niż binarny minus). Może się wydawać, że kalkulator działa poprawnie, ale można znaleźć wyrażenia, dla których daje złą odpowiedź, np.: - 8 / - 4 / - 2 powinno dać (- 8 / - 4) / - 2, czyli 1, a otrzymujemy rezultat -4. 27
Gramatyka niejednoznaczna unarny minus błąd wynika z faktu, że produkcja E - E ma taki priorytet jak operator -, niższy od priorytetu operatora / (i produkcji E E / E) problem powstaje w pozycji E - E /, kiedy to powinna nastąpić redukcja, ale skoro operator / ma wyższy priorytet od - zostanie wykonane przesunięcie wyrażenie zostaje źle zinterpretowane jako: - 8 / ( - 4 / - 2) Generator YACC: gramatyki niejednoznaczne (28) Błąd w obliczeniach wynika z faktu, że produkcja E > - E uzyskuje taki sam priorytet jak operator -, niższy niż priorytet operatora /. Efekt jest taki, że choć w pozycji E > - E. / powinna (zgodnie ze standardową semantyką unarnego minusa) nastąpić redukcja, to na skutek niewłaściwej relacji priorytetów następuje przesunięcie. Wyrażenie zostaje więc zinterpretowane jako 8 / ( - 4 / - 2) i wynik jest błędny. 28
Gramatyka niejednoznaczna unarny minus należy więc przypisać produkcji E - E wyższy priorytet niż operatorom * i / modyfikowanie priorytetu operatora - jest bezcelowe (minus unarny i binarny można rozróżnić dopiero na poziomie produkcji) problem można rozwiązać definiując pomocniczy token, np.: %token UMINUS a następnie za pomocą słowa kluczowego %prec przypisując jego priorytet produkcji: E : '-' E %prec UMINUS {$$=-$2} Generator YACC: gramatyki niejednoznaczne (29) Oczywiste jest więc, że należy odpowiednio podnieść priorytet instrukcji E > - E. Nie można tego efektu osiągnąć podnosząc priorytet operatora -, bo przy okazji priorytet produkcji E > E - E stałby się wyższy niż operatorów multiplikatywnych. W YACCu przewidziano rozwiązanie dla tego typu problemów można arbitralnie sterować priorytetem produkcji za pomocą słowa kluczowego %prec. Aby naprawić działanie kalkulatora musimy więc: zdefiniować pomocniczy token, np.: %token UMINUS o odpowiednio wysokim priorytecie przypisać priorytet tokenu UMINUS do produkcji E > - E: E : - E %prec UMINUS {$$=-$2} 29
Gramatyka niejednoznaczna unarny plus w niektórych językach dostępny jest również unarny plus obsługa unarnego plusa wymaga takich samych zabiegów, jak w przypadku unarnego minusa (można wykorzystać ten sam pomocniczy token) jeżeli zignorujemy ten problem, to kalkulator potraktuje np. + 8 / + 4 / + 2 jak + 8 / (+ 4 / + 2) i poda błędną odpowiedź 4 Generator YACC: gramatyki niejednoznaczne (30) Na marginesie rozważań na temat unarnego minusa warto podkreślić, że podobny problem mamy z unarnym plusem, ale ten problem jest zdecydowanie łatwiej przeoczyć konstruując gramatykę. Unarny plus został np. wprowadzony w ANSI C (w pierwszej edycji C go nie było). Sens stosowania unarnego plusa może być dyskusyjny, ale skoro norma języka go przewiduje należy umieć go poprawnie obsłużyć. Jeżeli zdefiniowaliśmy już pomocniczy token dla unarnego minusa możemy go wykorzystać do przypisania odpowiedniego priorytetu produkcji E > + E: E : + E %prec UMINUS { $$ = $2 } 30
Gramatyka niejednoznaczna wiszące 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 i jednocześnie nie ma słowa (słów) kluczowego kończącego instrukcję warunkową zamknięcie instrukcji warunkowej przewidziano m. in. w Algolu 68 (fi), języku powłoki systemu UNIX (fi), Adzie (end if) i Moduli-2 (end) Generator YACC: gramatyki niejednoznaczne (31) Z problemem tzw. wiszącego else mamy do czynienia wtedy, gdy: w języku występuje instrukcja warunkowa z opcjonalną częścią ( else ), wykonywaną wtedy, gdy testowany warunek nie jest spełniony i jednocześnie w języku nie przewidziano specjalnego słowa (słów) kluczowych zamykających instrukcję warunkową. Problem wiszącego else dotyczy więc wielu popularnych języków programowania takich, jak C, C++ czy Pascal. W innych językach, w konstrukcji instrukcji warunkowej, przewidziano zakończenie jej za pomocą odpowiedniego słowa (słów) kluczowych jest tak już np. w Algolu 68 (fi) czy w języku powłoki systemu UNIX (również fi) oraz nowszych językach programowania takich, jak Ada (end if) czy Modula-2 (end). 31
Gramatyka niejednoznaczna wiszące else problem wiszącego else (na przykładzie składni języka Pascal) polega na tym, że instrukcja: if a then if b then writeln('b') else writeln('c') może być interpretowana na dwa sposoby: if a then begin if b then writeln('b') else writeln('c') end { a } if a then begin if b then writeln('b') end else writeln('c') { b } Generator YACC: gramatyki niejednoznaczne (32) Załóżmy, że rozpatrywanym językiem jest Pascal. Składnia instrukcji warunkowej różni się co prawda nieco od np. języka C (w C nie ma słowa kluczowego then, ale jest przed słowem kluczowym else), ale nie zmienia to istoty problemu ani metody jego rozwiązania. Do zademonstrowania problemu wystarczy użyć zagnieżdżonej instrukcji warunkowej przedstawionej w przykładzie. Taka instrukcja może być interpretowana na dwa sposoby zestawione w tabeli (instrukcje bloku beginend wstawiono, aby podkreślić sposób interpretacji). Tylko jedna z tych interpretacji jest zgodna z definicją języka. W pierwszej interpretacji testujemy warunek, a następnie traktujemy kolejną część if oraz następującą po niej część else jako jedną całość. Druga interpretacja traktuje drugą gałąź if jako oddzielną instrukcję, a część else wiąże z pierwszą instrukcję if. 32
Gramatyka niejednoznaczna wiszące else poprawna jest oczywiście interpretacja {a} wiążąca część else z bezpośrednio poprzedzającą ją instrukcją if-then zapiszmy instrukcję warunkową w YACCu: S : IF E THEN S IF E THEN S ELSE S nieterminale S i E, to odpowiednio instrukcja i wyrażenie terminale IF, THEN, ELSE reprezentują słowa kluczowe języka Pascal Generator YACC: gramatyki niejednoznaczne (33) W językach programowania przyjmuje się, że poprawna jest interpretacja {a}, zgodnie z którą część else jest kontynuacją bezpośrednio poprzedzającej ją instrukcji if-then. Aby zilustrować problem i jego rozwiązanie zapiszmy teraz składnię instrukcji warunkowej języka Pascal w YACCu. Nieterminale S i E będą reprezentować odpowiednio instrukcję i wyrażenie, terminale IF, THEN, ELSE odpowiednie słowa kluczowe języka Pascal. 33
Gramatyka niejednoznaczna wiszące else w typowej gramatyce instrukcje nie mają priorytetów w wyniku czego powstaje konflikt przesunięcie/redukcja w pozycji: IF E THEN S ELSE S czy po przeczytaniu części if-then, widząc na wejściu else należy wykonać redukcję czy przesunięcie? konflikt zostanie rozstrzygnięty zgodnie z domyślnymi regułami na korzyść przesunięcia (i właściwej interpretacji instrukcji if-then-else) Generator YACC: gramatyki niejednoznaczne (34) W typowej gramatyce instrukcje nie mają przypisanych priorytetów, a zatem powstaje konflikt przesunięcie/redukcja. Jeżeli głowica czytająca znajdzie się w pozycji: S -> IF E THEN S. ELSE S parser nie wie, czy ma zredukować symbole na stosie do S, czy przesunąć else. Konflikt zostanie rozstrzygnięty zgodnie z domyślnymi zasadami na korzyść akcji przesunięcia i jest to rozstrzygnięcie zgodne z pożądaną interpretacją instrukcji ifthen-else. 34
Gramatyka niejednoznaczna wiszące else rozwiązanie tego konfliktu wymaga: przypisania terminalowi ELSE prawostronnego wiązania oraz przypisania części if-then takiego samego priorytetu jaki ma terminal ELSE: %right ELSE S : IF E THEN S IF E THEN S ELSE S %prec ELSE Generator YACC: gramatyki niejednoznaczne (35) Widać więc, że w tym przypadku można zignorować komunikat o konflikcie wyświetlany przez YACCa w trakcie generacji parsera. Usunięcie niejednoznaczności z gramatyki jest jednak bardzo proste, wystarczy: przypisać terminalowi ELSE prawostronne wiązanie oraz przypisać produkcji odpowiadającej części if-then taki sam priorytet jaki ma terminal ELSE 35
Gramatyka niejednoznaczna przypadki specjalne fragment gramatyki preprocesora EQN do składania równań (Kernighan & Cherry): %token sub sup E : E sub E sup E E sub E E sup E... konflikty przesunięcie/redukcja można usunąć definiując łączność terminali sub i sup: %right sub sup Generator YACC: gramatyki niejednoznaczne (36) Gramatyki niejednoznaczne pozwalają również na relatywnie łatwe dołączenie nowej produkcji opisującej specjalny przypadek konstrukcji opisanych pozostałymi produkcjami. Uzyskanie tego samego efektu przy użyciu gramatyki jednoznacznej jest niezwykle trudne. Uwzględnienie przypadku specjalnego w gramatyce niejednoznacznej można pokazać na przykładzie preprocesora EQN służącego do składania wzorów matematycznych. Preprocesor EQN (autorstwa Kernighana i Cherry ego) pozwala m. in. na stosowanie indeksów dolnych (sub) i górnych (sup). Konieczne jest wyróżnienie szczególnego przypadku, gdy wzorzec ma zarówno indeks dolny jak i górny (produkcja E > E sub E sup E), aby nadać mu odpowiednią postać graficzną. Fragment gramatyki języka preprocesora przedstawiony w przykładzie jest niejednoznaczny z kilku powodów. Terminale (operatory) sup i sub nie mają zdefiniowanej ani łączności, ani priorytetów. Konsekwencją tego są podobne konflikty przesunięcie/redukcja jak w gramatyce dla wyrażeń arytmetycznych. Aby gramatyka odzwierciedlała semantykę języka należy nadać terminalom sup i sub prawostronną łączność i ten sam priorytet (przy okazji pozbywamy się konfliktów przesunięcie/redukcja). 36
Gramatyka niejednoznaczna przypadki specjalne konflikt redukcja/redukcja jest związany z produkcjami: E E sub E sup E E E sup E w pozycji: E E sub E sup E E E sup E YACC wykrywa konflikt redukcja/redukcja zgodnie z którą produkcją ma wykonać redukcję i jak wskazać generatorowi właściwy wybór? Generator YACC: gramatyki niejednoznaczne (37) Nadanie priorytetów i łączności tokenom sup i sub nie rozwiązuje jednak konfliktu redukuj/redukuj związanego z produkcjami: E > E sub E sup E E > E sup E Konflikt powstanie w sytuacji, gdy analizator osiągnie pozycję: E > E sub E sup E. E > E sup E. Trzeba teraz znaleźć odpowiedzi na dwa pytania: zgodnie z którą produkcją analizator powinien wykonać redukcję? jak wskazać generatorowi YACC właściwą redukcję? 37
Gramatyka niejednoznaczna przypadki specjalne obsłużenie przypadku specjalnego wymaga wybrania redukcji zgodnie z produkcją: E E sub E sup E a więc należy nadać jej wyższy priorytet modyfikowanie priorytetu tokenu sup jest bezcelowe (priorytet obu produkcji pochodzi właśnie od niego) wystarczy użyć domyślnych reguł produkcja, która ma mieć wyższy priorytet musi być tekstowo pierwsza w specyfikacji Generator YACC: gramatyki niejednoznaczne (38) Jak już wcześniej wskazano celem wstawienia dodatkowej produkcji było obsłużenie przypadku specjalnego. Konflikt musi być więc rozstrzygnięty na korzyść produkcji: E > E sub E sup E Należy więc nadać jej wyższy priorytet niż produkcji: E > E sup E Nie ma oczywiście sensu manipulowanie priorytetem tokena sup obie produkcje otrzymują priorytet właśnie od niego (jako najbardziej prawego w obu produkcjach). Problem można rozwiązać na dwa sposoby: 1) najprostsze jest skorzystanie z domyślnych reguł rozstrzygania konfliktów, wystarczy zadbać, aby produkcja: E > E sub E sup E poprzedzała tekstowo produkcję: E > E sup E 2) zdefiniować pomocniczy token, nadać mu priorytet wyższy niż sup (i sub) i za pomocą słowa kluczowego %prec nadać ten priorytet produkcji: E > E sub E sup E 38
Dalsza lektura podręczniki YACCa: Bison (klon YACCa): http://www.gnu.org/software/bison/manual/ Johnson S. C., YACC: Yet Another Compiler- Compiler, Unix Programmer's Manual Vol 2b, 1979 Levine J., Mason T., Brown D., lex & yacc, 2nd edition, O'Reilly, 1992 MKS LEX & YACC, Reference Manual, MKS Software Inc., 2004 gramatyki niejednoznaczne Aho A. V., Sethi R., Ullman J. D., Compilers: Principles, Techniques, and Tools, Addison-Wesley, 1986 Generator YACC: gramatyki niejednoznaczne (39) 39