Plan wykładu Generator YACC: gramatyki niejednoznaczne Wojciech Complak Wojciech.Complak@cs.put.poznan.pl 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 1.03 Generator YACC: gramatyki niejednoznaczne (2/39) Gramatyki jednoznaczne i niejednoznaczne 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 opisuje 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) rozważmy niejednoznaczną gramatykę dla ciągu liczb rozdzielonych znakami odejmowania: - sprawdźmy teraz: jak będzie wyglądać drzewo składniowe dla zdania jaki będzie wynik (i kolejność) obliczeń analizatora wygenerowanego przez YACCa Generator YACC: gramatyki niejednoznaczne (3/39) Generator YACC: gramatyki niejednoznaczne (4/39) Drzewa wywodu zdania - skaner dla zdania - - istnieją dwa drzewa składniowe lewostronna łączność operatora odejmowania prawostronna łączność operatora odejmowania Generator YACC: gramatyki niejednoznaczne (5/39) 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/39)
z nieterminalem wiążemy akcje semantyczne: dla produkcji rezultatem będzie różnica argumentów operatora odejmowania dla produkcji NUM rezultatem będzie wartość liczby S : { printf("%d",$1) } : { $$ = $1 - $3 } NUM { $$ = $1 } Generator YACC: gramatyki niejednoznaczne (7/39) w trakcie generowania analizatora przez YACCa (MKS) otrzymujemy informację o pojedynczym konflikcie przesuń/redukuj: State 5 :. (2) :. [ $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/39) po skompilowaniu analizatora testujemy jego działanie dla wejścia 1-2 - 3, zamiast odpowiedzi -4 otrzymujemy jednak 2 analiza wywodu: [2] [1]-[-1] [1]-[2]-[3] [1]-[2]-[3] [1]-[2]-[3] [1]-[2]-[3] wskazuje, że problem leży w kolejności redukcji symboli formy zdaniowej - - 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 - - sposób rozstrzygnięcia tego konfliktu decyduje o łączności operatora (przesunięcie łączność prawostronna, redukcja łączność lewostronna) Generator YACC: gramatyki niejednoznaczne (9/39) Generator YACC: gramatyki niejednoznaczne (10/39) 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/39) 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/39)
Porównanie złożoności gramatyk Porównanie złożoności gramatyk gramatyka jednoznaczna S : {printf("%d",$1)} : '+' T {$$=$1+$3} T {$$=$1-$3} T {$$=$1} T : T '*' F {$$=$1*$3} T '/' F {$$=$1/$3} F {$$=$1} F : '(' ')' {$$=$2} NUM {$$=$1} '+' F {$$=$2} F {$$=-$2} gramatyka niejednoznaczna %left '+' %left '*' '/' %left UMINUS L : {printf("%d",$1)} : '+' {$$=$1+$3} '+' %prec UMINUS {$$=$2} {$$=$1-$3} %prec UMINUS {$$=-$2 } '*' {$$=$1*$3} '/' {$$=$1/$3} '(' ')' {$$=$2} NUM {$$=$1} Generator YACC: gramatyki niejednoznaczne (13/39) gramatyka jednoznaczna /* 1 */ : '+' T /* 2 */ T /* 3 */ T /* 4 */ T : T '*' F /* 5 */ T '/' F /* 6 */ F /* 7 */ F : '(' ')' /* 8 */ NUM /* 9 */ '+' F /* 10 */ F gramatyka niejednoznaczna /* 1 */ : '+' /* 2 */ '+' /* 3 */ /* 4 */ /* 5 */ '*' /* 6 */ '/' /* 7 */ '(' ')' /* 8 */ NUM Generator YACC: gramatyki niejednoznaczne (14/39) gramatyka jednoznaczna S : {printf("%d",$1)} : '+' T {$$=$1+$3 printf("r1([%d]->[%d]+t[%d]) ",$$,$1,$3)} T {$$=$1-$3 printf("r2([%d]->[%d]-t[%d]) ",$$,$1,$3)} T {$$=$1 printf("r3([%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 : '(' ')' {$$=$2 printf("r7(f[%d]->([%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/39) gramatyka niejednoznaczna %left '+' %left '*' '/' %left UMINUS L : { printf("%d",$1) } : '+' {$$=$1+$3 printf("r1([%d]->[%d]+[%d]) ",$$,$1,$3)} '+' %prec UMINUS {$$=$2 printf("r2([%d]->+[%d]) ",$$,$2)} {$$=$1-$3 printf("r3([%d]->[%d]-[%d]) ",$$,$1,$3)} %prec UMINUS {$$=-$2 printf("r4([%d]->-[%d]) ",$$,$2)} '*' {$$=$1*$3 printf("r5([%d]->[%d]*[%d]) ",$$,$1,$3)} '/' {$$=$1/$3 printf("r6([%d]->[%d]/[%d]) ",$$,$1,$3)} '(' ')' {$$=$2 printf("r7([%d]->([%d])) ",$$,$2)} NUM {$$=$1 printf("r8([%d]->num[%d]) ",$$,$1)} Generator YACC: gramatyki niejednoznaczne (16/39) 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([2]->t[2]) gramatyka niejednoznaczna 1 redukcja: r8([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/39) 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([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([14]->[2]+t[12]), r8(f[1]->num[1]), r6(t[1]->f[1]), r2([13]->[14]-t[1]) niejednoznaczna 7 redukcji: r8([2]->num[2]), r8([3]->num[3]), r8([4]->num[4]), r5([12]->[3]*[4]), r1([14]- >[2]+[12]), r8([1]->num[1]), r3([13]->[14]- [1]) Generator YACC: gramatyki niejednoznaczne (18/39)
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 S : { printf("%d",$1) } : { $$ = $1 - $3 } NUM { $$ = $1 } Generator YACC: gramatyki niejednoznaczne (19/39) priorytet instrukcji wynika z priorytetu najbardziej prawego tokenu niewielka (ale nieprzemyślana) modyfikacja gramatyki powoduje powstanie konfliktów %left S : { printf("%d",$1) } : ' ' ' ' { $$ = $1 - $5 } NUM { $$ = $1 } Generator YACC: gramatyki niejednoznaczne (20/39) operator dodawania 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/39) dodajmy teraz do gramatyki operator dodawania ( + ) o takim samym priorytecie i wiązaniu jak operator odejmowania ( - ) %left '+' S : { printf("%d",$1) } : '+' { $$ = $1 + $3 } { $$ = $1 - $3 } NUM { $$ = $1 } Generator YACC: gramatyki niejednoznaczne (22/39) operatory multiplikatywne operator potęgowania następnie uzupełnijmy gramatykę o operatory multiplikatywne (mnożenia i dzielenia) i definiujemy ich priorytet i łączność %left '+' %left '*' '/' : '+' { $$ = $1 + $3 } { $$ = $1 - $3 } '*' { $$ = $1 * $3 } '/' { $$ = $1 / $3 } NUM { $$ = $1 } Generator YACC: gramatyki niejednoznaczne (23/39) 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/39)
operator potęgowania operatory bez wiązania 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ę: : '^' z akcją: {$$=(int)pow($1,$3)} Generator YACC: gramatyki niejednoznaczne (25/39) 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ę : '=' (z odpowiednimi akcjami) Generator YACC: gramatyki niejednoznaczne (26/39) unarny minus unarny minus poprawne rozbudowanie gramatyki o unarny minus wymaga specjalnych zabiegów wydaje się, że wystarczy dodanie produkcji : - 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) błąd wynika z faktu, że produkcja - ma taki priorytet jak operator -, niższy od priorytetu operatora / (i produkcji / ) problem powstaje w pozycji - /, 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 (27/39) Generator YACC: gramatyki niejednoznaczne (28/39) unarny minus unarny plus należy więc przypisać produkcji - 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: : %prec UMINUS {$$=-$2} Generator YACC: gramatyki niejednoznaczne (29/39) 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 zamiast poprawnej: 1 Generator YACC: gramatyki niejednoznaczne (30/39)
wiszące else 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/39) 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/39) wiszące else 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 THN S IF THN S LS S nieterminale S i, to odpowiednio instrukcja i wyrażenie terminale IF, THN, LS reprezentują słowa kluczowe języka Pascal Generator YACC: gramatyki niejednoznaczne (33/39) w typowej gramatyce instrukcje nie mają priorytetów w wyniku czego powstaje konflikt przesunięcie/redukcja w pozycji: IF THN S LS 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/39) wiszące else przypadki specjalne rozwiązanie tego konfliktu wymaga: przypisania terminalowi LS prawostronnego wiązania oraz przypisania części if-then takiego samego priorytetu jaki ma terminal LS: %right LS S : IF THN S IF THN S LS S %prec LS Generator YACC: gramatyki niejednoznaczne (35/39) fragment gramatyki preprocesora QN do składania równań (Kernighan & Cherry): %token sub sup : sub sup /* */ sub /* */ sup /* */... konflikty przesunięcie/redukcja można usunąć definiując łączność terminali sub i sup: %right sub sup Generator YACC: gramatyki niejednoznaczne (36/39)
przypadki specjalne przypadki specjalne konflikt redukcja/redukcja jest związany z produkcjami: sub sup sup w pozycji: sub sup sup 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/39) obsłużenie przypadku specjalnego wymaga wybrania redukcji zgodnie z produkcją: sub sup 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/39) 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 LX & 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)