WDP Gramatyki – definiowanie składni i semantyki wyrażeń: Różnice pomiędzy wersjami
Nie podano opisu zmian |
Nie podano opisu zmian |
||
Linia 5: | Linia 5: | ||
Komunikacja z komputerem jest o tyle trudniejsza, że nie pozostawia miejsca na domysły, czy interpretacje. Musimy niezwykle ściśle wyartykułować nasze wobec komputera żądania, aby ten bez wahania wykonał to, o co nam rzeczywiście chodziło. Ponadto chodzi o to, żeby nasze programy były uniwersalne, przynajmniej w części dotyczącej algorytmiki, a ich wykonanie na każdym komputerze wyglądało tak samo, albo różniło się co najwyżej nieznaczącymi szczegółami, wynikającymi ze specyfiki architektury komputera, a nie z istoty zapisu algorytmu. | Komunikacja z komputerem jest o tyle trudniejsza, że nie pozostawia miejsca na domysły, czy interpretacje. Musimy niezwykle ściśle wyartykułować nasze wobec komputera żądania, aby ten bez wahania wykonał to, o co nam rzeczywiście chodziło. Ponadto chodzi o to, żeby nasze programy były uniwersalne, przynajmniej w części dotyczącej algorytmiki, a ich wykonanie na każdym komputerze wyglądało tak samo, albo różniło się co najwyżej nieznaczącymi szczegółami, wynikającymi ze specyfiki architektury komputera, a nie z istoty zapisu algorytmu. | ||
Różnica między językiem programowania, a językiem naturalnym polega na tym, że język naturalny jest zastany, podczas gdy język programowania możemy stworzyć od zera według naszego pomysłu. Użyjemy do tego metody gramatyk formalnych, które zostały wynalezione przez znanego językoznawcę Noama Chomsky'ego. Chomsky, \Rysunek{Chomsky} jeden z największych językoznawców – postać niezwykle barwna i kontrowersyjna – przez większość swojego życia próbował usystematyzować gramatykę języka angielskiego. Mechanizm gramatyk zaproponowany przez niego co prawda nie wystarczył do tego, żeby oddać bogactwo języka naturalnego, ale okazał się niezastąpiony przy definiowaniu sztucznych języków programowania. Oto pokrótce część jego teorii dotycząca tzw. {\em gramatyk bezkontekstowych | Różnica między językiem programowania, a językiem naturalnym polega na tym, że język naturalny jest zastany, podczas gdy język programowania możemy stworzyć od zera według naszego pomysłu. Użyjemy do tego metody gramatyk formalnych, które zostały wynalezione przez znanego językoznawcę Noama Chomsky'ego. Chomsky, \Rysunek{Chomsky} jeden z największych językoznawców – postać niezwykle barwna i kontrowersyjna – przez większość swojego życia próbował usystematyzować gramatykę języka angielskiego. Mechanizm gramatyk zaproponowany przez niego co prawda nie wystarczył do tego, żeby oddać bogactwo języka naturalnego, ale okazał się niezastąpiony przy definiowaniu sztucznych języków programowania. Oto pokrótce część jego teorii dotycząca tzw. {\em gramatyk bezkontekstowych}, z których będziemy korzystali. | ||
{\em Alfabetem | {\em Alfabetem} nazywamy dowolny skończony i niepusty zbiór symboli <math>A={a_1,...,a_n\}</math>. {\em Słowami} nad alfabetem <math>A</math> nazwiemy wszystkie skończone ciągi złożone z elementów alfabetu. W szczególności dopuścimy puste słowo – słowo nie mające żadnej litery, które oznaczamy wspólnym symbolem <math>\varepsilon</math>, niezależnie od alfabetu nad którym jest utworzone. Zbiór wszystkich słów nad alfabetem <math>A</math> oznaczamy przez <math>A^*</math>. Przykładowo jeśli <math>A={a}</math>, to <math>A^*={\varepsilon,a,aa,aaa,\ldots}</math>. Jeśli <math>A==\{a,b\}</math>, to <math>A^*=\{\varepsilon,a,b,aa,ab,ba,bb,aaa,aab,\ldots\}</math>. {\em Długość słowa}, to liczba jego liter. {\em Język} nad alfabetem <math>A</math>, to dowolny podzbiór zbioru <math>A^*</math>. | ||
{\em Gramatyką | {\em Gramatyką} (bezkontekstową) nazwiemy uporządkowaną czwórkę | ||
<span id=""/> <math> | <span id=""/> <math> | ||
Linia 15: | Linia 15: | ||
\langle N,T,P,S \rangle, | \langle N,T,P,S \rangle, | ||
</math>gdzie < | </math>gdzie <math>N</math> oraz <math>T</math>, to skończone i rozłączne zbiory odpowiednio symboli {\em pomocniczych} (zwanych czasem nieterminalnymi lub nieterminalami) i {\końcowych} (zwanych czasem terminalnymi lub terminalami), <math>P</math> jest zbiorem produkcji, a <math>S\in N</math> jest wyróżnionym symbolem pomocniczym zwanym {\em aksjomatem} gramatyki. Produkcje wchodzące w skład zbioru <math>P</math> są postaci <math>X ::= w</math>, gdzie <math>X\in N</math>, a <math>w\in (N\cup T)^*</math>. Pojedynczą taką produkcję interpretujemy, jako możliwość zastąpienia symbolu pomocniczego <math>X</math> przez słowo, w skład którego mogą wchodzić zarówno symbole pomocnicze, jak i końcowe. Proces wyprowadzania słów języka zdefiniowanego przez gramatykę odbywa się w taki sposób, że zaczynamy od symbolu <math>S</math> i do poszczególnych symboli pomocniczych stosujemy którąś z produkcji ze zbioru <math>P</math> tak długo, aż wszystkie symbole pomocnicze znikną. To, co zostanie, czyli słowo złożone z samych symboli końcowych nazywamy słowem wyprowadzalnym z gramatyki. Zbiór wszystkich słów wyprowadzalnych z danej gramatyki nazywa się {\em językiem generowanym przez gramatykę}. | ||
Zwróćmy uwagę na to, że jeśli w trakcie wywodu słowa pojawia się kilka identycznych symboli pomocniczych, to każdy z nich może samodzielnie decydować o tym, co z siebie wyprodukować. | Zwróćmy uwagę na to, że jeśli w trakcie wywodu słowa pojawia się kilka identycznych symboli pomocniczych, to każdy z nich może samodzielnie decydować o tym, co z siebie wyprodukować. | ||
Linia 24: | Linia 24: | ||
G_1=\langle \{S\},\{a\},\{S::=\varepsilon, S::=aaS\},S \rangle | G_1=\langle \{S\},\{a\},\{S::=\varepsilon, S::=aaS\},S \rangle | ||
</math>Gramatyka < | </math>Gramatyka <math>G_1</math> generuje wszystkie parzyste słowa złożone z samych liter <math>a</math> (uwaga słowo puste jest słowem o parzystej długości!). | ||
\Przykład[2] | \Przykład[2] | ||
Linia 32: | Linia 32: | ||
G_2=\langle \{S\},\{a\},\{S::=\varepsilon, S::=aSa, S::=bSb\},S \rangle | G_2=\langle \{S\},\{a\},\{S::=\varepsilon, S::=aSa, S::=bSb\},S \rangle | ||
</math>Gramatyka < | </math>Gramatyka <math>G_2</math> generuje wszystkie parzyste palindromy nad dwuelementowym alfabetem <math>\{a,b\}</math>. | ||
\begin{ukryj}[Jeśli nie wiesz co to są palindromy, to kliknij tutaj] | \begin{ukryj}[Jeśli nie wiesz co to są palindromy, to kliknij tutaj] | ||
Palindromem nazwiemy każde słowo, które czytane wprost jest identyczne, jak czytane wspak, czyli takie słowo < | Palindromem nazwiemy każde słowo, które czytane wprost jest identyczne, jak czytane wspak, czyli takie słowo <math>a_1,\ldots,a_n</math>, że dla każdego <math>k\in\{1,...,n\}</math> zachodzi <math>a_k=a_{n-k+1}</math>. Parzysty palindrom, to palindrom o parzystej długości. | ||
\end{ukryj} | \end{ukryj} | ||
Przyjmijmy konwencję notacyjną, która pozwoli nam uprościć wypisywanie produkcji wywodzących się z tego samego symbolu. Jeśli jest kilka wariantów produkcji z tego samego symbolu, to piszemy je w jednym ciągu oddzielając pionowymi kreskami. W szczególności produkcje gramatyki < | Przyjmijmy konwencję notacyjną, która pozwoli nam uprościć wypisywanie produkcji wywodzących się z tego samego symbolu. Jeśli jest kilka wariantów produkcji z tego samego symbolu, to piszemy je w jednym ciągu oddzielając pionowymi kreskami. W szczególności produkcje gramatyki <math>G_2</math> wyglądają w tej notacji następująco: <math>\{S::=\varepsilon | aSa | bSb\}</math>. | ||
Historię wyprowadzeń słowa w danej gramatyce możemy zapisać za pomocą {\em drzewa wywodu}. Korzeniem w tym drzewie jest aksjomat gramatyki, a jeśli została do symbolu pomocniczego < | Historię wyprowadzeń słowa w danej gramatyce możemy zapisać za pomocą {\em drzewa wywodu}. Korzeniem w tym drzewie jest aksjomat gramatyki, a jeśli została do symbolu pomocniczego <math>X</math>, który jest węzłem w drzewie zastosowana produkcja <math>X::=x_1,\ldots,x_n</math>, to synami węzła <math>X</math> stają się elementy <math>x_1,\ldots,x_n</math>. Oto przykład wyprowadzenia słowa abba w gramatyce <math>G_2</math>. | ||
\begin{Rysunek}[rys1-abba] | \begin{Rysunek}[rys1-abba] | ||
\end{Rysunek} | \end{Rysunek} | ||
Moglibyśmy to wyprowadzenie przedstawić w wersji liniowej < | Moglibyśmy to wyprowadzenie przedstawić w wersji liniowej <math>S \rightarrow aSa \rightarrow abSba \rightarrow abba</math>, jednak drzewo wywodu daje nam dokładnie taką informację, o jaką nam chodzi, zamazując niepotrzebne szczegóły. Przyjrzyjmy się bliżej różnicom w reprezentacji historii wywodu. Rozważmy gramatykę <math>G=\langle \{S\},\{a,b\},\{S::= SaS | b\},S \rangle</math>. Za pomocą tej gramatyki możemy wygenerować dowolną naprzemienną sekwencję liter <math>b</math> oraz <math>a</math> zaczynającą się i kończącą na literę <math>b</math>. Na ile sposobów można otrzymać słowo <math>babab</math>? W notacji liniowej tych sposobów jest sporo, przykładowo <math> S \rightarrow SaS \rightarrow SaSaS \rightarrow SaSab \rightarrow baSab \rightarrow babab</math>. Albo inaczej <math>S \rightarrow SaS \rightarrow Sab \rightarrow SaSab \rightarrow Sabab \rightarrow babab</math>. | ||
\Pytanie Na ile sposobów można liniowo zapisać wywód słowa < | \Pytanie Na ile sposobów można liniowo zapisać wywód słowa <math>babab</math>? | ||
\Odpowiedź 12 | \Odpowiedź 12 | ||
Linia 56: | Linia 56: | ||
\end{Rysunek} | \end{Rysunek} | ||
Można gramatykę tak skonstruować, żeby słowo < | Można gramatykę tak skonstruować, żeby słowo <math>babab</math>, podobnie jak i każde inne generowane przez powyższą gramatykę, miało tylko jedno drzewo wywodu. Oto ta gramatyka: | ||
<span id=""/> <math> | <span id=""/> <math> | ||
Linia 65: | Linia 65: | ||
</math> | </math> | ||
Taka gramatyka ma już tylko jedno drzewo wywodu dla słowa < | Taka gramatyka ma już tylko jedno drzewo wywodu dla słowa <math>babab</math>. Oto ono: | ||
\begin{Rysunek}[rys3-babab-jedno] | \begin{Rysunek}[rys3-babab-jedno] | ||
Linia 71: | Linia 71: | ||
Podobnie, każde inne słowo wyprowadzalne z tej gramatyki ma jedno drzewo wywodu. | Podobnie, każde inne słowo wyprowadzalne z tej gramatyki ma jedno drzewo wywodu. | ||
Powiemy, że gramatyka < | Powiemy, że gramatyka <math>G</math> jest {\em jednoznaczna}, jeśli każde słowo wyprowadzalne z <math>G</math> ma tylko jedno drzewo wywodu. Własność jednoznaczności jest niezwykle ważna – będziemy starali się tak zdefiniować język programowania, aby drzewo wywodu każdego programu było jednoznaczne. To pozwoli nam określić semantykę języka programowania jako funkcję obliczaną na podstawie drzewa wywodu. | ||
Powiemy, że gramatyka < | Powiemy, że gramatyka <math>G</math> jest {\em zgodna} z językiem <math>L\in T^*</math>, jeśli każde słowo wyprowadzalne z <math>G</math> należy do <math>L</math>. Powiemy, że gramatyka <math>G</math> jest {\em pełna} względem języka <math>L</math>, jeśli wszystkie słowa należące do <math>L</math> są wyprowadzalne z <math>G</math>. Rzecz jasna każda gramatyka G jest zarówno zgodna, jak i pełna względem swojego języka <math>L(G)</math>. | ||
Zacznijmy od zdefiniowania liczb całkowitych. Po pierwsze określmy, co będziemy chcieli nazywać liczbami całkowitymi. Przyjmijmy, że poprawnymi liczbami są wszystkie skończone ciągi cyfr dziesiętnych poprzedzone pojedynczym znakiem < | Zacznijmy od zdefiniowania liczb całkowitych. Po pierwsze określmy, co będziemy chcieli nazywać liczbami całkowitymi. Przyjmijmy, że poprawnymi liczbami są wszystkie skończone ciągi cyfr dziesiętnych poprzedzone pojedynczym znakiem <math>+</math> lub <math>-</math>. Dopuścimy zatem jako poprawne napisy <math>123,-22,00,+013</math>, ale jako niepoprawne uznamy np. napisy <math>++12,-+0,2+3,XVI,+,\varepsilon</math>. Oto propozycja gramatyki: | ||
<span id=""/> <math> | <span id=""/> <math> | ||
Linia 81: | Linia 81: | ||
G=\langle \{S,Z,C,L\},\{-,+,0,1,2,3,4,5,6,7,8,9\},\{C::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9, S::=L | ZL, Z::=+ | -, L::= C | LC, \},S \rangle | G=\langle \{S,Z,C,L\},\{-,+,0,1,2,3,4,5,6,7,8,9\},\{C::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9, S::=L | ZL, Z::=+ | -, L::= C | LC, \},S \rangle | ||
</math>W ten sposób napisana gramatyka daje jednoznaczne wyprowadzenie każdej liczby całkowitej, zgodnej z naszymi założeniami. Zauważmy, że na samym początku wyprowadzania decydujemy, czy liczba ma być ze znakiem, czy bez – o tym decyduje wybór < | </math>W ten sposób napisana gramatyka daje jednoznaczne wyprowadzenie każdej liczby całkowitej, zgodnej z naszymi założeniami. Zauważmy, że na samym początku wyprowadzania decydujemy, czy liczba ma być ze znakiem, czy bez – o tym decyduje wybór <math>L</math> lub <math>ZL</math> spośród dwóch wariantów, które daje nam symbol <math>S</math>. Z symbolu pomocniczego <math>L</math> możemy wygenerować dowolny niepusty ciąg cyfr, a z symbolu <math>Z</math> znak. Przykładowe wyprowadzenie liczby -120 wygląda następująco: | ||
\begin{Rysunek}[rys3-120] | \begin{Rysunek}[rys3-120] | ||
\end{Rysunek} | \end{Rysunek} | ||
Podamy teraz reguły semantyczne związane z produkcjami, które pozwolą na określenie wartości liczby na podstawie jej drzewa wywodu. Reguły te będą związane z produkcjami gramatyki. Musimy dokonać tu pewnego rozróżnienia, często niezauważonego. Gdy piszemy na przykład < | Podamy teraz reguły semantyczne związane z produkcjami, które pozwolą na określenie wartości liczby na podstawie jej drzewa wywodu. Reguły te będą związane z produkcjami gramatyki. Musimy dokonać tu pewnego rozróżnienia, często niezauważonego. Gdy piszemy na przykład <math>11</math>, to myślimy o tym napisie, jako o liczbie „jedenaście”. Jednak na dobrą sprawę są to po prostu dwie jedynki napisane na klawiaturze, które można zinterpretować też na przykład jako trójkę w systemie dwójkowym. Gramatyka służy do tego, aby produkować napisy. Zbiór napisów wyprowadzalnych z gramatyki jest językiem. Nas jednak przede wszystkim będzie interesowało znaczenie tych napisów w pewnej dziedzinie. Umówmy się więc, że aby zrobić rozróżnienie między napisem <math>11</math>, a liczbą <math>11</math>, tę drugą będziemy oznaczali kolorem zielonym. Zatem <math>11</math> oznacza napis złożony z dwóch znaków, a \zielone[<math>11</math>] będzie oznaczało liczbę jedenaście. | ||
Teraz możemy już ustalić znaczenie poszczególnych produkcji gramatyki. Będziemy wiązali z każdą produkcją regułę semantyczną. Węzły drzewa będą interpretowane w wybranej przez nas dziedzinie. Określenie wartości węzła będzie zależało od wartości podwieszonych pod nim węzłów, czyli jego synów. Reguła semantyczna będzie określała, jak wartość węzła zależy od wartości synów. Oznaczmy wartość węzła < | Teraz możemy już ustalić znaczenie poszczególnych produkcji gramatyki. Będziemy wiązali z każdą produkcją regułę semantyczną. Węzły drzewa będą interpretowane w wybranej przez nas dziedzinie. Określenie wartości węzła będzie zależało od wartości podwieszonych pod nim węzłów, czyli jego synów. Reguła semantyczna będzie określała, jak wartość węzła zależy od wartości synów. Oznaczmy wartość węzła <math>v</math> w drzewie przez <math>I(v)</math>. Zatem <math>I:V\rightarrow Z</math> jest funkcją, która każdemu węzłowi przypisze liczbę całkowitą. | ||
Zacznijmy od tego, że określimy znaczenie węzłów końcowych – liści drzewa. Są one cyframi - symbolami końcowymi. Ustalamy zatem, że < | Zacznijmy od tego, że określimy znaczenie węzłów końcowych – liści drzewa. Są one cyframi - symbolami końcowymi. Ustalamy zatem, że <math>I(0)=\zielone[0], I(1)=\zielone[1],\ldots,I(9)=\zielone[9],I(-)=\zielone[-1],I(+)=\zielone[1]</math>. Dalej | ||
*dla produkcji < | *dla produkcji <math>C::=x</math> określamy <math>I{C)=I(x)</math> gdzie <math>x\in\{0,1,\ldtots,9\}</math> | ||
*dla produkcji < | *dla produkcji <math>Z::=z</math> określamy <math>I{Z)=I(z)</math> gdzie <math>z\in\{+,-\}</math> | ||
*dla produkcji < | *dla produkcji <math>L::=C</math> określamy <math>I(L)=I(C)</math>, | ||
przy czym stosujemy tu konwencję, że jeśli zastosowaliśmy produkcję < | przy czym stosujemy tu konwencję, że jeśli zastosowaliśmy produkcję <math>L::=C</math>, to ojciec w tej regule jest oznaczany przez <math>L</math>, a syn przez <math>C</math>. Dla produkcji <math>L::=LC</math> musimy się umówić, jak rozróżnimy <math>L</math> z lewej i z prawej strony produkcji. Zatem umawiamy się, że jeśli symbol powtarza się z lewej strony i prawej, to ten z prawej wystąpi we wzorze z primem. Kolejne reguły zatem, to | ||
*Dla produkcji < | *Dla produkcji <math>(L::=LC)</math> określamy <math>I(L)=10I(L')+I(C)</math>, | ||
*Dla produkcji < | *Dla produkcji <math>(S::=L)</math> określamy <math>I(S)=I(L)</math>, | ||
*Dla produkcji < | *Dla produkcji <math>(S::=ZL)</math> określamy <math>I(S)=I(Z)I(L)</math> | ||
Oto drzewo semantyczne dla napisu < | Oto drzewo semantyczne dla napisu <math>-124</math>. | ||
\begin{Rysunek}[semantyka-124] | \begin{Rysunek}[semantyka-124] | ||
\end{Rysunek} | \end{Rysunek} | ||
Jak widać, wartość w korzeniu to < | Jak widać, wartość w korzeniu to <math>\zielone[-124]</math> i jest to faktycznie liczba, którą chcemy oznaczać za pomocą napisu <math>-124</math>. | ||
\begin{ćwiczenie} | \begin{ćwiczenie} | ||
Linia 121: | Linia 121: | ||
'''Reguły semantyczne dla stałych całkowitych w systemie dwójkowym''' | '''Reguły semantyczne dla stałych całkowitych w systemie dwójkowym''' | ||
<span id="Semantyka stałych dwójkowych" \> | <span id="Semantyka stałych dwójkowych" \> | ||
*Dla symboli końcowych określamy < | *Dla symboli końcowych określamy <math>I(0)=\zielone[0], I(1)=\zielone[1],I(-)=\zielone[-1],I(+)=\zielone[1]</math> | ||
*dla produkcji < | *dla produkcji <math>C::=x</math> określamy <math>I{C)=I(x)</math> gdzie <math>x\in\{0,1\}</math> | ||
*dla produkcji < | *dla produkcji <math>Z::=z</math> określamy <math>I{Z)=I(z)</math> gdzie <math>z\in\{+,-\}</math> | ||
*dla produkcji < | *dla produkcji <math>L::=C</math> określamy <math>I(L)=I(C)</math>, | ||
*Dla produkcji < | *Dla produkcji <math>(L::=LC)</math> określamy <math>I(L)=2I(L')+I(C)</math>, | ||
*Dla produkcji < | *Dla produkcji <math>(S::=L)</math> określamy <math>I(S)=I(L)</math>, | ||
*Dla produkcji < | *Dla produkcji <math>(S::=ZL)</math> określamy <math>I(S)=I(Z)I(L)</math> | ||
\end{Odpowiedź} | \end{Odpowiedź} | ||
Linia 136: | Linia 136: | ||
G=\langle \{Id,L,C,Ch\},\{0,1,\ldots,9,a,b,\ldots,z,\_\},\{Id::= LCh, Ch ::= \varepsilon | ChL |ChC\},Id \rangle | G=\langle \{Id,L,C,Ch\},\{0,1,\ldots,9,a,b,\ldots,z,\_\},\{Id::= LCh, Ch ::= \varepsilon | ChL |ChC\},Id \rangle | ||
</math>W gramatyce tej < | </math>W gramatyce tej <math>L</math> oznacza pojedynczą literę, <math>C</math> oznacza pojedynczą cyfrę, <math>Ch</math> oznacza ciąg złożony z liter lub cyfr. Zgodnie z tradycją programistyczną, poza literami alfebetu łacińskiego dopuszczamy jeszcze jako literę symbol podkreślenia <math>\_</math>. | ||
Zbliżyliśmy się do momentu, kiedy może nam braknąć oznaczeń na symbole pomocnicze. W rzeczywistości w językach programowania gramatyki mają sto kilkadziesiąt symboli pomocniczych. Żeby się nam to wszystko nie pomyliło przyjmijmy konwencję, w myśl której symbole pomocnicze piszemy w nawiasach rombowych, a oznaczamy je znaczącym tekstem, np. {\tt <cyfra>, <litera>, <ciąg liter lub cyfr>} itp. | Zbliżyliśmy się do momentu, kiedy może nam braknąć oznaczeń na symbole pomocnicze. W rzeczywistości w językach programowania gramatyki mają sto kilkadziesiąt symboli pomocniczych. Żeby się nam to wszystko nie pomyliło przyjmijmy konwencję, w myśl której symbole pomocnicze piszemy w nawiasach rombowych, a oznaczamy je znaczącym tekstem, np. {\tt <cyfra>, <litera>, <ciąg liter lub cyfr>} itp. | ||
Jak teraz w analogiczny sposób określić gramatykę wyrażeń całkowitoliczbowych? Zauważmy, że w wyrażeniach są zmienne, stałe, operatory < | Jak teraz w analogiczny sposób określić gramatykę wyrażeń całkowitoliczbowych? Zauważmy, że w wyrażeniach są zmienne, stałe, operatory <math>+,-,\div,\bmod</math> oraz nawiasy. Ogólnie rzecz biorąc można powiedzieć, że wyrażeniem jest stała, zmienna oraz że jeśli <math>U,V</math> są wyrażeniami, to wyrażeniem też jest <math>-U,U+V,U-V,U*V,U\div V, U \bmod V, (U)</math>. Daje to niezwykle prostą gramatykę: | ||
<span id=""/> <math> | <span id=""/> <math> | ||
Linia 148: | Linia 148: | ||
</math>gdzie {\tt identyfikator}to Id z poprzedniej gramatyki, a {\tt liczba całkowita}, to S z naszej gramatyki definiującej liczby całkowite. | </math>gdzie {\tt identyfikator}to Id z poprzedniej gramatyki, a {\tt liczba całkowita}, to S z naszej gramatyki definiującej liczby całkowite. | ||
Niestety ta gramatyka ma kilka wad. Po pierwsze nie jest jednoznaczna. Po drugie nie można określić dla niej semantyki – ta wada jest konsekwencją niejednoznaczności. Po trzecie dopuszcza wytworzenie dziwolągów typu < | Niestety ta gramatyka ma kilka wad. Po pierwsze nie jest jednoznaczna. Po drugie nie można określić dla niej semantyki – ta wada jest konsekwencją niejednoznaczności. Po trzecie dopuszcza wytworzenie dziwolągów typu <math>---0</math> czy <math>0 - - 1</math> i w żaden sposób nie oddaje właściwego związku z tradycją matematyczną. | ||
O co nam zatem chodzi? Oto lista naszych wymagań. | O co nam zatem chodzi? Oto lista naszych wymagań. | ||
Linia 154: | Linia 154: | ||
\begin{itermize} | \begin{itermize} | ||
\item gramatyka wyrażeń powinna być jednoznaczna | \item gramatyka wyrażeń powinna być jednoznaczna | ||
\item wartości wyrażeń powinny być zgodne z tradycją matematyczną, w szczególności należy zachować priorytety operatorów oraz wykluczyć napisy typu < | \item wartości wyrażeń powinny być zgodne z tradycją matematyczną, w szczególności należy zachować priorytety operatorów oraz wykluczyć napisy typu <math>2 - - 3</math> | ||
\item nawiasy powinny wymusić dowolną kolejność, na której nam zależy. | \item nawiasy powinny wymusić dowolną kolejność, na której nam zależy. | ||
\item operacje niełączne, takie jak odejmowanie i dzielenie powinny być lewostronnie łączne, czyli ich wykonywanie musi być od lewej do prawej (w szczególności np. < | \item operacje niełączne, takie jak odejmowanie i dzielenie powinny być lewostronnie łączne, czyli ich wykonywanie musi być od lewej do prawej (w szczególności np. <math>5-3-1</math> powinno dać wynik <math>1</math>, a nie <math>3</math>). | ||
\end{ramka} | \end{ramka} | ||
Rozwiązanie jest zaskakująco proste, jak na wyrafinowanie naszych wymagań. Oto szukana gramatyka wyrażeń całkowitoliczbowych. | Rozwiązanie jest zaskakująco proste, jak na wyrafinowanie naszych wymagań. Oto szukana gramatyka wyrażeń całkowitoliczbowych. | ||
Linia 166: | Linia 166: | ||
G_Z=\langle \{<wyrażenie Z>, <składnik>, <czynnik>, <stała>, <zmienna>\}, \{\_,a,b,\ldots,z,0,1,\ldots,9,-,+,\div,\bmod,(,) \}, \{<zmienna> ::= <identyfikator>, <stała> ::= <liczba całkowita bez znaku>,P,<wyrażenie Z> | G_Z=\langle \{<wyrażenie Z>, <składnik>, <czynnik>, <stała>, <zmienna>\}, \{\_,a,b,\ldots,z,0,1,\ldots,9,-,+,\div,\bmod,(,) \}, \{<zmienna> ::= <identyfikator>, <stała> ::= <liczba całkowita bez znaku>,P,<wyrażenie Z> | ||
</math>gdzie < | </math>gdzie <math>P</math> jest zbiorem następujących produkcji: | ||
<wyrażenie Z> ::= <składnik> | -<składnik> | <wyrażenie Z> + <składnik> | <wyrażenie Z> - <składnik> | <wyrażenie Z> ::= <składnik> | -<składnik> | <wyrażenie Z> + <składnik> | <wyrażenie Z> - <składnik> | ||
<składnik> ::= <czynnik> | <składnik> * <czynnik> | <składnik> '''div''' <czynnik> | <składnik> '''mod''' <czynnik> | <składnik> ::= <czynnik> | <składnik> * <czynnik> | <składnik> '''div''' <czynnik> | <składnik> '''mod''' <czynnik> | ||
Linia 191: | Linia 191: | ||
*Dla symboli końcowych określamy < | *Dla symboli końcowych określamy <math>I(0)=\zielone[0], I(1)=\zielone[1], \ldots, I(9)=\zielone[9]</math> | ||
*Dla produkcji < | *Dla produkcji <math>C::=x</math> określamy <math>I{C)=I(x)</math> gdzie <math>x=0,1,\ldots,9</math> | ||
*Dla produkcji (S::=SC) określamy < | *Dla produkcji (S::=SC) określamy <math>I(S)=10I(S')+I(C)</math>, | ||
*Dla produkcji (S::=C) określamy < | *Dla produkcji (S::=C) określamy <math>I(S)=I(C)</math>, | ||
\end{Odpowiedź} | \end{Odpowiedź} | ||
Zbadajmy teraz, jak wygląda drzewo wywodu wyrażenia < | Zbadajmy teraz, jak wygląda drzewo wywodu wyrażenia <math>a+b*4</math>. Łatwo się można przekonać, że pierwszą produkcją musi być {\tt <wyrażenie Z> ::= <wyrażenie Z> + <składnik>}. Żadna inna produkcja nie wchodzi w grę: dwie z nich wprowadzają znak minus, którego byśmy się już nie pozbyli, a trzecia {\tt <wyrażenie Z> ::= <składnik>} przenosi nas do składnika, z którego nie daje się już uzyskać plusa bez wprowadzenia nawiasów. | ||
Skoro tak, to dalej dostajemy już w jednoznaczny sposób następujące drzewo wywodu. | Skoro tak, to dalej dostajemy już w jednoznaczny sposób następujące drzewo wywodu. | ||
\begin{rysunek}[Drzewo wywodu dla wyrażenia < | \begin{rysunek}[Drzewo wywodu dla wyrażenia <math>a+b*4</math>] | ||
\end{rysunek} | \end{rysunek} | ||
Jakie reguły semantyczne należy dobrać do naszych produkcji? Dość naturalne. Dla stałych nie ma problemu: już je mamy. Ze zmiennymi jest jednak mały kłopot: jaką wartość powinny mieć zmienne? Musimy tu zrobić dygresję na temat implementacji. Wszystkie zmienne w programie w momencie, w którym się z nich korzysta, mają swoje miejsce w pamięci. W czasie kompilacji i konsolidacji programu ustala się adres, pod którym można je znaleźć i kiedy odwołujemy się do nich, ich wartość uzyskujemy zaglądając do tego miejsca w pamięci. Przyjmijmy, że pamięć w naszym ujęciu modeluje funkcja < | Jakie reguły semantyczne należy dobrać do naszych produkcji? Dość naturalne. Dla stałych nie ma problemu: już je mamy. Ze zmiennymi jest jednak mały kłopot: jaką wartość powinny mieć zmienne? Musimy tu zrobić dygresję na temat implementacji. Wszystkie zmienne w programie w momencie, w którym się z nich korzysta, mają swoje miejsce w pamięci. W czasie kompilacji i konsolidacji programu ustala się adres, pod którym można je znaleźć i kiedy odwołujemy się do nich, ich wartość uzyskujemy zaglądając do tego miejsca w pamięci. Przyjmijmy, że pamięć w naszym ujęciu modeluje funkcja <math>V:ID\rightarrow Z?</math>, która zbiór wszystkich identyfikatorów <math>ID</math> przeprowadza w zbiór nieco rozszerzony liczb całkowitych <math>Z_?=Z\cup \{?\}</math>,gdzie symbol <math>?</math> oznacza wartość {\em nieokreśloną}. Ta wartość jest nam potrzebna, gdyż wszystkie zmienne w chwili gdy zaczynamy wykonywać program, mają wartości nieokreślone i dopiero w efekcie wykonywania instrukcji konkretne wartości będą im sukcesywnie nadawane (i zmieniane). Przy okazji mała dygresja. W odróżnieniu od całej matematyki, zmienne naprawdę zmieniają swoje wartości. Gdy w matematyce zaczynamy wywód mówiąc np. ,,Niech <math>k</math> będzie liczbą przekątnych w wielokącie'', wówczas do końca wywodu <math>k</math> nie może oznaczać niczego innego. W informatyce, jeśli napiszemy niech <math>k</math> stanie się równe <math>5</math>, to za chwilę wartość zmiennej <math>k</math> może ulec zmianie, jeśli sobie tego zażyczymy. | ||
Zatem przez < | Zatem przez <math>V(z)</math> będziemy rozumieli wartość zmiennej <math>z</math>, a wartość wyrażenia, w którym zmienne występują, będzie zależała od tego wartościowania. Interpretację wyrażenia musimy zatem uzależnić od wartościowania <math>V</math>. Podamy teraz reguły wartościowania wyrażeń. Tym razem funkcję interpretacji oznaczymy przez <math>I_V</math>, żeby podkreślić zależność interpretacji wyrażenia od wartościowania zmiennych. Musimy jeszcze zauważyć, że jeśli w wyrażeniu występuje dzielenie (niezależnie od tego czy div czy mod), to wynik dzielenia będzie błędny, jeśli dzielnik będzie zerem. Komputer zmuszony do wykonania takiego dzielenia odmawia współpracy i generuje błąd. Wygodnie jest określając semantykę tę sytuację uwzględnić w dziedzinie semantycznej dodając wartość error do zestawu wartości całkowitych. Zatem ostatecznie nasza dziedzina liczb całkowitych rozrasta się nam o dwie wartości: error i ?. Rozszerzmy jeszcze działania na te wartości. Przyjmujemy więc, że dla każdej operacji dwuargumentowej <math>\oplus</math> | ||
*dla każdego < | *dla każdego <math>x\neq error: x\oplus ? = ?\oplus x = ?</math> | ||
*dla każdego < | *dla każdego <math>x: x \oplus error = error \oplus x = error</math> | ||
i analogicznie dla operacji jednoargumentowych. | i analogicznie dla operacji jednoargumentowych. | ||
Reguły semantyczne | Reguły semantyczne | ||
*Dla symboli końcowych określamy < | *Dla symboli końcowych określamy <math>I_V(<stała>)=I(<stala bez znaku>) (jak w ćwiczeniu | ||
[[#Stałe bez znaku]]) | [[#Stałe bez znaku]]) | ||
*dla produkcji <math><zmienna>::=<identyfikator>< | *dla produkcji </math><zmienna>::=<identyfikator><math> określamy </math>I_V(<zmienna>)=V(<identyfikator>)<math> | ||
*dla produkcji <math><czynnik>::=<stała>< | *dla produkcji </math><czynnik>::=<stała><math> określamy </math>I_V(<czynnik>)=I_V(<stała>)<math> | ||
*dla produkcji <math><czynnik>::=<zmienna>< | *dla produkcji </math><czynnik>::=<zmienna><math> określamy </math>I_V(<czynnik>)=I_V(<zmienna>)<math> | ||
*dla produkcji <math><czynnik>::=(<wyrażenie Z>)< | *dla produkcji </math><czynnik>::=(<wyrażenie Z>)<math> określamy </math>I_V(<czynnik>)=I_V(<wyrażenie Z>)<math> | ||
*dla produkcji <math><składnik>::=<czynnik>< | *dla produkcji </math><składnik>::=<czynnik><math> określamy </math>I_V(<składnik>)=I_V(<czynnik>)<math> | ||
*dla produkcji <math><składnik>::=<składnik>*<czynnik>< | *dla produkcji </math><składnik>::=<składnik>*<czynnik><math> określamy </math>I_V(<składnik>)=I_V(<składnik>)I_V(<czynnik>)<math> | ||
*dla produkcji <math><składnik>::=<składnik> div <czynnik>< | *dla produkcji </math><składnik>::=<składnik> div <czynnik><math> określamy | ||
<span id=""/> <math> | <span id=""/> <math> | ||
Linia 226: | Linia 226: | ||
I_V(<składnik>)=\left\{ \begin{array}{ll} | I_V(<składnik>)=\left\{ \begin{array}{ll} | ||
I_V(<składnik'>\zielony[\div] I_V(<czynnik>)& \mbox{jeśli <math>I_V(<czynnik>\neq0< | I_V(<składnik'>\zielony[\div] I_V(<czynnik>)& \mbox{jeśli </math>I_V(<czynnik>\neq0<math>} \\ | ||
error& \mbox{jeśli <math>I_V(<czynnik>=0< | error& \mbox{jeśli </math>I_V(<czynnik>=0<math>} | ||
\end{array} | \end{array} | ||
\right. | \right. | ||
</math> | </math> | ||
*dla produkcji <math><składnik>::=<składnik> mod <czynnik>< | *dla produkcji </math><składnik>::=<składnik> mod <czynnik><math> określamy | ||
<span id=""/> <math> | <span id=""/> <math> | ||
Linia 237: | Linia 237: | ||
I_V(<składnik>)=\left\{ \begin{array}{ll} | I_V(<składnik>)=\left\{ \begin{array}{ll} | ||
I_V(<składnik'>\zielony[\bmod] I_V(<czynnik>)& \mbox{jeśli <math>I_V(<czynnik>\neq0< | I_V(<składnik'>\zielony[\bmod] I_V(<czynnik>)& \mbox{jeśli </math>I_V(<czynnik>\neq0<math>} \\ | ||
error& \mbox{jeśli <math>I_V(<czynnik>=0< | error& \mbox{jeśli </math>I_V(<czynnik>=0<math>} | ||
\end{array} | \end{array} | ||
\right. | \right. | ||
</math> | </math> | ||
*dla produkcji <math><wyrażenie Z>::=<składnik>< | *dla produkcji </math><wyrażenie Z>::=<składnik><math> określamy </math>I_V(<wyrażenie Z>)=I_V(<składnik>)<math> | ||
*dla produkcji <math><wyrażenie Z>::=-<składnik>< | *dla produkcji </math><wyrażenie Z>::=-<składnik><math> określamy </math>I_V(<wyrażenie Z>)=\zielony[-]I_V(<składnik>)<math> | ||
*dla produkcji <math><wyrażenie Z>::=<wyrażenie Z>+<składnik>< | *dla produkcji </math><wyrażenie Z>::=<wyrażenie Z>+<składnik><math> określamy </math>I_V(<wyrażenie Z>)=I_V(<wyrażenie Z>)\zielony[+] I_V(<składnik>)<math> | ||
*dla produkcji <math><wyrażenie Z>::=<wyrażenie Z>-<składnik>< | *dla produkcji </math><wyrażenie Z>::=<wyrażenie Z>-<składnik><math> określamy </math>I_V(<wyrażenie Z>)=I_V(<wyrażenie Z>)\zielony{-}I_V(<składnik>)<math> | ||
Uff! Wiemy już więc, jak interpretować wyrażenia arytmetyczne. A teraz parę pytań sprawdzających. | Uff! Wiemy już więc, jak interpretować wyrażenia arytmetyczne. A teraz parę pytań sprawdzających. | ||
\Ćwiczenie | \Ćwiczenie | ||
Ile to jest dla <math>a=2, b=0, c=6, d=?< | Ile to jest dla </math>a=2, b=0, c=6, d=?<math>? | ||
*<math>c+a*b< | *</math>c+a*b<math> | ||
\Odpowiedź <math>6< | \Odpowiedź </math>6<math> | ||
*<math>c/a*b< | *</math>c/a*b<math> | ||
\Odpowiedź <math>0< | \Odpowiedź </math>0<math> | ||
*<math>c/a/b< | *</math>c/a/b<math> | ||
\Odpowiedź <math>error< | \Odpowiedź </math>error<math> | ||
*<math>(c+a) div (b+d)< | *</math>(c+a) div (b+d)<math> | ||
\Odpowiedź <math>?</math> | \Odpowiedź </math>?<math></math> |
Wersja z 08:01, 26 lip 2006
Wykład 3. Gramatyki – definiowanie składni i semantyki wyrażeń
Język naturalny jest siłą rzeczy niejednoznaczny. Wiele pozostawia interpretacji, domysłom – często słyszymy, że dwie osoby rozmawiając nie mogą się nawzajem zrozumieć. Czasem sami poniewczasie dostrzegamy, że inaczej coś zrozumieliśmy, niż miał to na myśli nasz rozmówca.
Komunikacja z komputerem jest o tyle trudniejsza, że nie pozostawia miejsca na domysły, czy interpretacje. Musimy niezwykle ściśle wyartykułować nasze wobec komputera żądania, aby ten bez wahania wykonał to, o co nam rzeczywiście chodziło. Ponadto chodzi o to, żeby nasze programy były uniwersalne, przynajmniej w części dotyczącej algorytmiki, a ich wykonanie na każdym komputerze wyglądało tak samo, albo różniło się co najwyżej nieznaczącymi szczegółami, wynikającymi ze specyfiki architektury komputera, a nie z istoty zapisu algorytmu.
Różnica między językiem programowania, a językiem naturalnym polega na tym, że język naturalny jest zastany, podczas gdy język programowania możemy stworzyć od zera według naszego pomysłu. Użyjemy do tego metody gramatyk formalnych, które zostały wynalezione przez znanego językoznawcę Noama Chomsky'ego. Chomsky, \Rysunek{Chomsky} jeden z największych językoznawców – postać niezwykle barwna i kontrowersyjna – przez większość swojego życia próbował usystematyzować gramatykę języka angielskiego. Mechanizm gramatyk zaproponowany przez niego co prawda nie wystarczył do tego, żeby oddać bogactwo języka naturalnego, ale okazał się niezastąpiony przy definiowaniu sztucznych języków programowania. Oto pokrótce część jego teorii dotycząca tzw. {\em gramatyk bezkontekstowych}, z których będziemy korzystali.
{\em Alfabetem} nazywamy dowolny skończony i niepusty zbiór symboli Parser nie mógł rozpoznać (błąd składni): {\displaystyle A={a_1,...,a_n\}} . {\em Słowami} nad alfabetem nazwiemy wszystkie skończone ciągi złożone z elementów alfabetu. W szczególności dopuścimy puste słowo – słowo nie mające żadnej litery, które oznaczamy wspólnym symbolem , niezależnie od alfabetu nad którym jest utworzone. Zbiór wszystkich słów nad alfabetem oznaczamy przez . Przykładowo jeśli , to . Jeśli , to . {\em Długość słowa}, to liczba jego liter. {\em Język} nad alfabetem , to dowolny podzbiór zbioru .
{\em Gramatyką} (bezkontekstową) nazwiemy uporządkowaną czwórkę gdzie oraz , to skończone i rozłączne zbiory odpowiednio symboli {\em pomocniczych} (zwanych czasem nieterminalnymi lub nieterminalami) i {\końcowych} (zwanych czasem terminalnymi lub terminalami), jest zbiorem produkcji, a jest wyróżnionym symbolem pomocniczym zwanym {\em aksjomatem} gramatyki. Produkcje wchodzące w skład zbioru są postaci , gdzie , a . Pojedynczą taką produkcję interpretujemy, jako możliwość zastąpienia symbolu pomocniczego przez słowo, w skład którego mogą wchodzić zarówno symbole pomocnicze, jak i końcowe. Proces wyprowadzania słów języka zdefiniowanego przez gramatykę odbywa się w taki sposób, że zaczynamy od symbolu i do poszczególnych symboli pomocniczych stosujemy którąś z produkcji ze zbioru tak długo, aż wszystkie symbole pomocnicze znikną. To, co zostanie, czyli słowo złożone z samych symboli końcowych nazywamy słowem wyprowadzalnym z gramatyki. Zbiór wszystkich słów wyprowadzalnych z danej gramatyki nazywa się {\em językiem generowanym przez gramatykę}. Zwróćmy uwagę na to, że jeśli w trakcie wywodu słowa pojawia się kilka identycznych symboli pomocniczych, to każdy z nich może samodzielnie decydować o tym, co z siebie wyprodukować.
\Przykład[1] Gramatyka generuje wszystkie parzyste słowa złożone z samych liter (uwaga słowo puste jest słowem o parzystej długości!).
\Przykład[2] Gramatyka generuje wszystkie parzyste palindromy nad dwuelementowym alfabetem .
\begin{ukryj}[Jeśli nie wiesz co to są palindromy, to kliknij tutaj] Palindromem nazwiemy każde słowo, które czytane wprost jest identyczne, jak czytane wspak, czyli takie słowo , że dla każdego zachodzi . Parzysty palindrom, to palindrom o parzystej długości. \end{ukryj}
Przyjmijmy konwencję notacyjną, która pozwoli nam uprościć wypisywanie produkcji wywodzących się z tego samego symbolu. Jeśli jest kilka wariantów produkcji z tego samego symbolu, to piszemy je w jednym ciągu oddzielając pionowymi kreskami. W szczególności produkcje gramatyki wyglądają w tej notacji następująco: .
Historię wyprowadzeń słowa w danej gramatyce możemy zapisać za pomocą {\em drzewa wywodu}. Korzeniem w tym drzewie jest aksjomat gramatyki, a jeśli została do symbolu pomocniczego , który jest węzłem w drzewie zastosowana produkcja , to synami węzła stają się elementy . Oto przykład wyprowadzenia słowa abba w gramatyce .
\begin{Rysunek}[rys1-abba] \end{Rysunek}
Moglibyśmy to wyprowadzenie przedstawić w wersji liniowej , jednak drzewo wywodu daje nam dokładnie taką informację, o jaką nam chodzi, zamazując niepotrzebne szczegóły. Przyjrzyjmy się bliżej różnicom w reprezentacji historii wywodu. Rozważmy gramatykę . Za pomocą tej gramatyki możemy wygenerować dowolną naprzemienną sekwencję liter oraz zaczynającą się i kończącą na literę . Na ile sposobów można otrzymać słowo ? W notacji liniowej tych sposobów jest sporo, przykładowo . Albo inaczej .
\Pytanie Na ile sposobów można liniowo zapisać wywód słowa ?
\Odpowiedź 12
Ile jest drzew wywodu tego słowa? Okazuje się, że tylko dwa. Oto one
\begin{Rysunek}[rys2-babab] \end{Rysunek}
Można gramatykę tak skonstruować, żeby słowo , podobnie jak i każde inne generowane przez powyższą gramatykę, miało tylko jedno drzewo wywodu. Oto ta gramatyka:
Taka gramatyka ma już tylko jedno drzewo wywodu dla słowa . Oto ono:
\begin{Rysunek}[rys3-babab-jedno] \end{Rysunek}
Podobnie, każde inne słowo wyprowadzalne z tej gramatyki ma jedno drzewo wywodu. Powiemy, że gramatyka jest {\em jednoznaczna}, jeśli każde słowo wyprowadzalne z ma tylko jedno drzewo wywodu. Własność jednoznaczności jest niezwykle ważna – będziemy starali się tak zdefiniować język programowania, aby drzewo wywodu każdego programu było jednoznaczne. To pozwoli nam określić semantykę języka programowania jako funkcję obliczaną na podstawie drzewa wywodu.
Powiemy, że gramatyka jest {\em zgodna} z językiem , jeśli każde słowo wyprowadzalne z należy do . Powiemy, że gramatyka jest {\em pełna} względem języka , jeśli wszystkie słowa należące do są wyprowadzalne z . Rzecz jasna każda gramatyka G jest zarówno zgodna, jak i pełna względem swojego języka .
Zacznijmy od zdefiniowania liczb całkowitych. Po pierwsze określmy, co będziemy chcieli nazywać liczbami całkowitymi. Przyjmijmy, że poprawnymi liczbami są wszystkie skończone ciągi cyfr dziesiętnych poprzedzone pojedynczym znakiem lub . Dopuścimy zatem jako poprawne napisy , ale jako niepoprawne uznamy np. napisy . Oto propozycja gramatyki: W ten sposób napisana gramatyka daje jednoznaczne wyprowadzenie każdej liczby całkowitej, zgodnej z naszymi założeniami. Zauważmy, że na samym początku wyprowadzania decydujemy, czy liczba ma być ze znakiem, czy bez – o tym decyduje wybór lub spośród dwóch wariantów, które daje nam symbol . Z symbolu pomocniczego możemy wygenerować dowolny niepusty ciąg cyfr, a z symbolu znak. Przykładowe wyprowadzenie liczby -120 wygląda następująco:
\begin{Rysunek}[rys3-120] \end{Rysunek}
Podamy teraz reguły semantyczne związane z produkcjami, które pozwolą na określenie wartości liczby na podstawie jej drzewa wywodu. Reguły te będą związane z produkcjami gramatyki. Musimy dokonać tu pewnego rozróżnienia, często niezauważonego. Gdy piszemy na przykład , to myślimy o tym napisie, jako o liczbie „jedenaście”. Jednak na dobrą sprawę są to po prostu dwie jedynki napisane na klawiaturze, które można zinterpretować też na przykład jako trójkę w systemie dwójkowym. Gramatyka służy do tego, aby produkować napisy. Zbiór napisów wyprowadzalnych z gramatyki jest językiem. Nas jednak przede wszystkim będzie interesowało znaczenie tych napisów w pewnej dziedzinie. Umówmy się więc, że aby zrobić rozróżnienie między napisem , a liczbą , tę drugą będziemy oznaczali kolorem zielonym. Zatem oznacza napis złożony z dwóch znaków, a \zielone[] będzie oznaczało liczbę jedenaście.
Teraz możemy już ustalić znaczenie poszczególnych produkcji gramatyki. Będziemy wiązali z każdą produkcją regułę semantyczną. Węzły drzewa będą interpretowane w wybranej przez nas dziedzinie. Określenie wartości węzła będzie zależało od wartości podwieszonych pod nim węzłów, czyli jego synów. Reguła semantyczna będzie określała, jak wartość węzła zależy od wartości synów. Oznaczmy wartość węzła w drzewie przez . Zatem jest funkcją, która każdemu węzłowi przypisze liczbę całkowitą.
Zacznijmy od tego, że określimy znaczenie węzłów końcowych – liści drzewa. Są one cyframi - symbolami końcowymi. Ustalamy zatem, że Parser nie mógł rozpoznać (nieznana funkcja „\zielone”): {\displaystyle I(0)=\zielone[0], I(1)=\zielone[1],\ldots,I(9)=\zielone[9],I(-)=\zielone[-1],I(+)=\zielone[1]} . Dalej
- dla produkcji określamy Parser nie mógł rozpoznać (błąd składni): {\displaystyle I{C)=I(x)} gdzie Parser nie mógł rozpoznać (nieznana funkcja „\ldtots”): {\displaystyle x\in\{0,1,\ldtots,9\}}
- dla produkcji określamy Parser nie mógł rozpoznać (błąd składni): {\displaystyle I{Z)=I(z)} gdzie
- dla produkcji określamy ,
przy czym stosujemy tu konwencję, że jeśli zastosowaliśmy produkcję , to ojciec w tej regule jest oznaczany przez , a syn przez . Dla produkcji musimy się umówić, jak rozróżnimy z lewej i z prawej strony produkcji. Zatem umawiamy się, że jeśli symbol powtarza się z lewej strony i prawej, to ten z prawej wystąpi we wzorze z primem. Kolejne reguły zatem, to
- Dla produkcji określamy ,
- Dla produkcji określamy ,
- Dla produkcji określamy
Oto drzewo semantyczne dla napisu .
\begin{Rysunek}[semantyka-124] \end{Rysunek}
Jak widać, wartość w korzeniu to Parser nie mógł rozpoznać (nieznana funkcja „\zielone”): {\displaystyle \zielone[-124]} i jest to faktycznie liczba, którą chcemy oznaczać za pomocą napisu .
\begin{ćwiczenie} Podaj gramatykę generującą liczby w układzie dwójkowym i określ dla niej reguły semantyczne \end{ćwiczenie}. \Podpowiedź1: odrzuć parę symboli końcowych i lekko zmodyfikuj reguły semantyczne. \begin{Odpowiedź}
Reguły semantyczne dla stałych całkowitych w systemie dwójkowym
- Dla symboli końcowych określamy Parser nie mógł rozpoznać (nieznana funkcja „\zielone”): {\displaystyle I(0)=\zielone[0], I(1)=\zielone[1],I(-)=\zielone[-1],I(+)=\zielone[1]}
- dla produkcji określamy Parser nie mógł rozpoznać (błąd składni): {\displaystyle I{C)=I(x)} gdzie
- dla produkcji określamy Parser nie mógł rozpoznać (błąd składni): {\displaystyle I{Z)=I(z)} gdzie
- dla produkcji określamy ,
- Dla produkcji określamy ,
- Dla produkcji określamy ,
- Dla produkcji określamy
\end{Odpowiedź}
Zmienne, których będziemy używali w wyrażeniach będą oznaczane za pomocą identyfikatorów. Identyfikatory będą ciągami liter lub cyfr, zaczynającymi się od litery. Zatem gramatyka dla identyfikatora, to W gramatyce tej oznacza pojedynczą literę, oznacza pojedynczą cyfrę, oznacza ciąg złożony z liter lub cyfr. Zgodnie z tradycją programistyczną, poza literami alfebetu łacińskiego dopuszczamy jeszcze jako literę symbol podkreślenia .
Zbliżyliśmy się do momentu, kiedy może nam braknąć oznaczeń na symbole pomocnicze. W rzeczywistości w językach programowania gramatyki mają sto kilkadziesiąt symboli pomocniczych. Żeby się nam to wszystko nie pomyliło przyjmijmy konwencję, w myśl której symbole pomocnicze piszemy w nawiasach rombowych, a oznaczamy je znaczącym tekstem, np. {\tt <cyfra>, <litera>, <ciąg liter lub cyfr>} itp.
Jak teraz w analogiczny sposób określić gramatykę wyrażeń całkowitoliczbowych? Zauważmy, że w wyrażeniach są zmienne, stałe, operatory Parser nie mógł rozpoznać (błąd składni): {\displaystyle +,-,\div,\bmod} oraz nawiasy. Ogólnie rzecz biorąc można powiedzieć, że wyrażeniem jest stała, zmienna oraz że jeśli są wyrażeniami, to wyrażeniem też jest . Daje to niezwykle prostą gramatykę: Parser nie mógł rozpoznać (błąd składni): {\displaystyle G=\langle \{W,<zmienna>,<stała>\},\{\_,a,b,\ldots,z,0,1,\ldots,9,-,+,\div,\bmod,(,)\},\{<zmienna> ::= <identyfikator>, <stała> ::= <liczba całkowita>, W::= <zmienna> | <stała> | -W | W+W | W-W | W*W | W\div W | (W) \},W \rangle, } gdzie {\tt identyfikator}to Id z poprzedniej gramatyki, a {\tt liczba całkowita}, to S z naszej gramatyki definiującej liczby całkowite.
Niestety ta gramatyka ma kilka wad. Po pierwsze nie jest jednoznaczna. Po drugie nie można określić dla niej semantyki – ta wada jest konsekwencją niejednoznaczności. Po trzecie dopuszcza wytworzenie dziwolągów typu czy i w żaden sposób nie oddaje właściwego związku z tradycją matematyczną.
O co nam zatem chodzi? Oto lista naszych wymagań. \begin{ramka} \begin{itermize} \item gramatyka wyrażeń powinna być jednoznaczna \item wartości wyrażeń powinny być zgodne z tradycją matematyczną, w szczególności należy zachować priorytety operatorów oraz wykluczyć napisy typu \item nawiasy powinny wymusić dowolną kolejność, na której nam zależy. \item operacje niełączne, takie jak odejmowanie i dzielenie powinny być lewostronnie łączne, czyli ich wykonywanie musi być od lewej do prawej (w szczególności np. powinno dać wynik , a nie ). \end{ramka} Rozwiązanie jest zaskakująco proste, jak na wyrafinowanie naszych wymagań. Oto szukana gramatyka wyrażeń całkowitoliczbowych. \begin{ramka}[Wyrażenia całkowitoliczbowe]
Parser nie mógł rozpoznać (błąd składni): {\displaystyle G_Z=\langle \{<wyrażenie Z>, <składnik>, <czynnik>, <stała>, <zmienna>\}, \{\_,a,b,\ldots,z,0,1,\ldots,9,-,+,\div,\bmod,(,) \}, \{<zmienna> ::= <identyfikator>, <stała> ::= <liczba całkowita bez znaku>,P,<wyrażenie Z> } gdzie jest zbiorem następujących produkcji:
<wyrażenie Z> ::= <składnik> | -<składnik> | <wyrażenie Z> + <składnik> | <wyrażenie Z> - <składnik> <składnik> ::= <czynnik> | <składnik> * <czynnik> | <składnik> div <czynnik> | <składnik> mod <czynnik> <czynnik< ::= <stała> | <zmienna> | (<wyrażenie Z>) <stała>::= <liczba całkowita bez znaku> <zmienna> ::= <identyfikator>
\end{ramka} Symbol pomocniczy <liczba całkowita bez znaku> jest prostym uproszczeniem gramatyki liczb polegającym na usunięciu znaku.
\begin{Ćwiczenie}
Podaj gramatykę dla liczb całkowitych bez znaku
\Podpowiedź Usuń znak i związane z nim produkcje z gramatyki dla stałych całkowitoliczbowych
\Odpowiedź i reguły semantyczne:
- Dla symboli końcowych określamy Parser nie mógł rozpoznać (nieznana funkcja „\zielone”): {\displaystyle I(0)=\zielone[0], I(1)=\zielone[1], \ldots, I(9)=\zielone[9]}
- Dla produkcji określamy Parser nie mógł rozpoznać (błąd składni): {\displaystyle I{C)=I(x)} gdzie
- Dla produkcji (S::=SC) określamy ,
- Dla produkcji (S::=C) określamy ,
\end{Odpowiedź}
Zbadajmy teraz, jak wygląda drzewo wywodu wyrażenia . Łatwo się można przekonać, że pierwszą produkcją musi być {\tt <wyrażenie Z> ::= <wyrażenie Z> + <składnik>}. Żadna inna produkcja nie wchodzi w grę: dwie z nich wprowadzają znak minus, którego byśmy się już nie pozbyli, a trzecia {\tt <wyrażenie Z> ::= <składnik>} przenosi nas do składnika, z którego nie daje się już uzyskać plusa bez wprowadzenia nawiasów.
Skoro tak, to dalej dostajemy już w jednoznaczny sposób następujące drzewo wywodu.
\begin{rysunek}[Drzewo wywodu dla wyrażenia ] \end{rysunek}
Jakie reguły semantyczne należy dobrać do naszych produkcji? Dość naturalne. Dla stałych nie ma problemu: już je mamy. Ze zmiennymi jest jednak mały kłopot: jaką wartość powinny mieć zmienne? Musimy tu zrobić dygresję na temat implementacji. Wszystkie zmienne w programie w momencie, w którym się z nich korzysta, mają swoje miejsce w pamięci. W czasie kompilacji i konsolidacji programu ustala się adres, pod którym można je znaleźć i kiedy odwołujemy się do nich, ich wartość uzyskujemy zaglądając do tego miejsca w pamięci. Przyjmijmy, że pamięć w naszym ujęciu modeluje funkcja , która zbiór wszystkich identyfikatorów przeprowadza w zbiór nieco rozszerzony liczb całkowitych ,gdzie symbol oznacza wartość {\em nieokreśloną}. Ta wartość jest nam potrzebna, gdyż wszystkie zmienne w chwili gdy zaczynamy wykonywać program, mają wartości nieokreślone i dopiero w efekcie wykonywania instrukcji konkretne wartości będą im sukcesywnie nadawane (i zmieniane). Przy okazji mała dygresja. W odróżnieniu od całej matematyki, zmienne naprawdę zmieniają swoje wartości. Gdy w matematyce zaczynamy wywód mówiąc np. ,,Niech będzie liczbą przekątnych w wielokącie, wówczas do końca wywodu nie może oznaczać niczego innego. W informatyce, jeśli napiszemy niech stanie się równe , to za chwilę wartość zmiennej może ulec zmianie, jeśli sobie tego zażyczymy.
Zatem przez będziemy rozumieli wartość zmiennej , a wartość wyrażenia, w którym zmienne występują, będzie zależała od tego wartościowania. Interpretację wyrażenia musimy zatem uzależnić od wartościowania . Podamy teraz reguły wartościowania wyrażeń. Tym razem funkcję interpretacji oznaczymy przez , żeby podkreślić zależność interpretacji wyrażenia od wartościowania zmiennych. Musimy jeszcze zauważyć, że jeśli w wyrażeniu występuje dzielenie (niezależnie od tego czy div czy mod), to wynik dzielenia będzie błędny, jeśli dzielnik będzie zerem. Komputer zmuszony do wykonania takiego dzielenia odmawia współpracy i generuje błąd. Wygodnie jest określając semantykę tę sytuację uwzględnić w dziedzinie semantycznej dodając wartość error do zestawu wartości całkowitych. Zatem ostatecznie nasza dziedzina liczb całkowitych rozrasta się nam o dwie wartości: error i ?. Rozszerzmy jeszcze działania na te wartości. Przyjmujemy więc, że dla każdej operacji dwuargumentowej
- dla każdego
- dla każdego
i analogicznie dla operacji jednoargumentowych.
Reguły semantyczne
- Dla symboli końcowych określamy Parser nie mógł rozpoznać (błąd składni): {\displaystyle I_V(<stała>)=I(<stala bez znaku>) (jak w ćwiczeniu [[#Stałe bez znaku]]) *dla produkcji } <zmienna>::=<identyfikator>Parser nie mógł rozpoznać (błąd składni): {\displaystyle określamy } I_V(<zmienna>)=V(<identyfikator>)<czynnik>::=<stała>Parser nie mógł rozpoznać (błąd składni): {\displaystyle określamy } I_V(<czynnik>)=I_V(<stała>)<czynnik>::=<zmienna>Parser nie mógł rozpoznać (błąd składni): {\displaystyle określamy } I_V(<czynnik>)=I_V(<zmienna>)<czynnik>::=(<wyrażenie Z>)Parser nie mógł rozpoznać (błąd składni): {\displaystyle określamy } I_V(<czynnik>)=I_V(<wyrażenie Z>)<składnik>::=<czynnik>Parser nie mógł rozpoznać (błąd składni): {\displaystyle określamy } I_V(<składnik>)=I_V(<czynnik>)<składnik>::=<składnik>*<czynnik>Parser nie mógł rozpoznać (błąd składni): {\displaystyle określamy } I_V(<składnik>)=I_V(<składnik>)I_V(<czynnik>)<składnik>::=<składnik> div <czynnik>Parser nie mógł rozpoznać (błąd składni): {\displaystyle określamy <span id=""/> <math> I_V(<składnik>)=\left\{ \begin{array}{ll} I_V(<składnik'>\zielony[\div] I_V(<czynnik>)& \mbox{jeśli } I_V(<czynnik>\neq0Parser nie mógł rozpoznać (błąd składni): {\displaystyle } \\ error& \mbox{jeśli } I_V(<czynnik>=0Parser nie mógł rozpoznać (błąd składni): {\displaystyle } \end{array} \right. }
- dla produkcji </math><składnik>::=<składnik> mod <czynnik>Parser nie mógł rozpoznać (błąd składni): {\displaystyle określamy <span id=""/> <math> I_V(<składnik>)=\left\{ \begin{array}{ll} I_V(<składnik'>\zielony[\bmod] I_V(<czynnik>)& \mbox{jeśli } I_V(<czynnik>\neq0Parser nie mógł rozpoznać (błąd składni): {\displaystyle } \\ error& \mbox{jeśli } I_V(<czynnik>=0Parser nie mógł rozpoznać (błąd składni): {\displaystyle } \end{array} \right. }
- dla produkcji </math><wyrażenie Z>::=<składnik>Parser nie mógł rozpoznać (błąd składni): {\displaystyle określamy } I_V(<wyrażenie Z>)=I_V(<składnik>)<wyrażenie Z>::=-<składnik>Parser nie mógł rozpoznać (błąd składni): {\displaystyle określamy } I_V(<wyrażenie Z>)=\zielony[-]I_V(<składnik>)<wyrażenie Z>::=<wyrażenie Z>+<składnik>Parser nie mógł rozpoznać (błąd składni): {\displaystyle określamy } I_V(<wyrażenie Z>)=I_V(<wyrażenie Z>)\zielony[+] I_V(<składnik>)<wyrażenie Z>::=<wyrażenie Z>-<składnik>Parser nie mógł rozpoznać (błąd składni): {\displaystyle określamy } I_V(<wyrażenie Z>)=I_V(<wyrażenie Z>)\zielony{-}I_V(<składnik>)Parser nie mógł rozpoznać (błąd składni): {\displaystyle Uff! Wiemy już więc, jak interpretować wyrażenia arytmetyczne. A teraz parę pytań sprawdzających. \Ćwiczenie Ile to jest dla } a=2, b=0, c=6, d=?c+a*bParser nie mógł rozpoznać (nieznana funkcja „\Odpowied”): {\displaystyle \Odpowiedź } 6c/a*bParser nie mógł rozpoznać (nieznana funkcja „\Odpowied”): {\displaystyle \Odpowiedź } 0c/a/bParser nie mógł rozpoznać (nieznana funkcja „\Odpowied”): {\displaystyle \Odpowiedź } error(c+a) div (b+d)Parser nie mógł rozpoznać (nieznana funkcja „\Odpowied”): {\displaystyle \Odpowiedź } ?