Semantyka i weryfikacja programów/Ćwiczenia 1
Zawartość
Tematem tych zajęć jest semantyka operacyjna wyrażeń (małe kroki).
Zadania z rozwiązaniami
Ćwiczenie 1
Rozważmy bardzo prosty język wyrażeń, którego składnia opisana jest następującą gramatyką:
Wynikiem wyrażenienia warunkowego jest wartość wyrażenia , o ile wyrażenie oblicza się do wartości różnej od zera; w przeciwnym przypadku wynikiem jest wartość wyrażenia .
Zaproponuj semantykę operacyjną (małe kroki) dla tego języka.
{{rozwiazanie||roz1|
Zacznijmy od ustalenia notacji i dziedzin syntaktycznych.
Niech oznacza zbiór stałych liczbowych,
.
Podobnie, niech oznacza zbiór identyfikatorów, które
mogą być nazwami zmiennych; .
Wreszcie, niech oznacza zbiór wyrażeń;
.
Dla ułatwienia zapisywania reguł zakładamy, ze stałe
liczbowe są wyrażeniami, czyli .
Będziemy potrzebować zbioru stanów, opisujących wartości przypisane zmiennym. Najprostszym rozwiązaniem jest przyjąc, że stan to funkcja z do . Oznaczmy przez zbiór wszystkich takich funkcji; stany oznaczac będziemy przez .
W naszej semantyce będziemy potrzebowac tranzycji dwóch postaci. Po pierwsze, tranzycje postaci
oznaczające mały krok w trakcie obliczania wyrażenia w stanie , w wyniku którego wyewoluowało do . Stan nie ulega zmiania podczas obliczania wyrażenia (nie ma tzw. efektów ubocznych), więc to samo figuruje po lewej i prawej stronie strzałki.
Po drugie, tranzycje postaci
będą oznaczaczać, że wyrażenie jest już policzone, a jego wartością jest .
Zatem przyjmijmy, że zbiór konfiguracji to
a konfiguracje końcowe to .
Uwaga: Tak naprawdę to druga postać tranzycji nie jest niezbędna, gdyż moglibyśmy umówić się, że konfiguracje końcowe to . (koniec uwagi)
Najprostsze są tranzycje prowadzące do konfiguracji końcowej:
Zauważmy, że po prawej stronie to wyrażenie składające się ze stałej, podczas gdy po prawej stronie reprezentuje liczbę będącą wartością wyrażenia.
Zmienna oblicza się do swojej wartości w bieżącym stanie:
Teraz zajmiemy się dodawaniem . Ponieważ semantyka jest w stylu małych kroków, musimy zdecydować się czy najpierw obliczyć pierwszy (lewy) składnik czy drugi? Jeśli wybierzemy lewy (strategia lewostronna), otrzymamy regułę:
Reguły tej postaci będziem zapisywac tak:
[[ \frac{e_1, s \,\Longrightarrow\, e'_1, s} {e_1 + e_2, s \,\Longrightarrow\, e'_1 + e_2, s}. </math>
Czyli mały krok w stanowi też mały krok w . Po zakończeniu obliczania przechodzimy do :
A na końcu dodajemy:
Zauważmy tutaj pewną subtelność, dotyczącą dwóch wystąpień symbolu +: pierwsze wystąpienie oznacza jedną z konstrukcji składniowych języka, a drugie oznacza operację dodawania w zbiorze . Pozwalamy sobie na taką kolizję oznaczeń, gdyż nie powinna ona prowadzić do niejednoznaczności. Pamiętajmy, że składnia języka jest składnią abstrajkcyjną, więc zamiast moglibyśmy równie dobrze pisać np. .
Inna możliwą strategią obliczania jest strategia prawostronna, którą otrzymujemy zastępując pierwsze dwie z trzech powyższych reguł przez:
Ponadto, jeśli przyjmiemy regułę pierwszą (dla ), trzecią i czwartą (dla ), otrzymamy strategię równoległą, polegającą na obliczaniu jednocześnie i :
Bardziej precyzyjnie mówiąc, małe kroki obliczające obydwa podwyrażenia przeplatają się, i to w dowolny sposób. Ta dowolność prowadzi do niedeterminizmu, czyli do sytuacji, gdy kolejna (następna) konfiguracja nie jest wyznaczona jednoznacznie. Jest tak, gdyż możemy mieć do wyboru dwie różne tranzycje
Zauważmy natomiast, że kolejność przeplatania się małych kroków obliczających i nie wpływa w tym przypadku na końcową wartość całego wyrażenia.
Na koniec reguły dla wyrażenia warunkowego.
}}
Ćwiczenie 2
Rozszerzmy język wyrażeń z poprzedniego zadania o jedną konstrukcję
Wyrażenie zawiera w sobie deklarację , która stanowi mechanizm wiązania identyfikatorów w naszym języku. Deklaracja wprowadza nową zmienną oraz przypisuje jej wartość. Wartość wyrażenia obliczamy następująco: najpierw oblicza się wartość , podstawia ją na zmienna , a następnie oblicza wyrażenie . Zakresem zmiennej jest wyrażenie , czyli wewnątrz można odwoływać się (wielokrotnie) do zmiennej ; Ogólniej, odwołania do zmiennej w wyrażeniu odnoszą się do najbliższej (najbardziej zagnieżdzonej) deklaracji tej zmiennej. Taki mechanizm wiązania identyfikatorów nazywamy wiązaniem statycznym. Przyjmujemy zwykłe reguły przesłaniania zmiennych. Np., jeśli w występuje podwyrażenie to odwołania do wewnątrz odnoszą się do najbliższej deklaracji zmiennej .
Zakładamy, że na początku wartości wszystkich zmiennych są nieokreślone, czyli zmienne są niezainicjowane, a odwołanie do niezainicjowanej zmiennej jest uważane za niepoprawne.
Przykłady
Parser nie mógł rozpoznać (błąd składni): {\displaystyle \mathbf{let}\, z = 5 \,\mathbf{in}\, x+z \quad \quad \mapsto \quad \quad \mbox{brak wyniku; odwołanie do niezainicjowanej zmiennej}\, x }
Rozwiązanie
Podobnie jak poprzednio,
stan powinien opisywać wartości przypisane zmiennym.
Tym razem jednak
uwzględnimy niezainicjowane zmienne, czyli zmienne bez żadnej wartości.
Przyjmijmy zatem, że stan to skończona funkcja częściowa z do .
Oznaczmy symbolem zbiór wszystkich takich funkcji:
.
Naturalnym stanem początkowym jest stan pusty, tzn.
pusta funkcja częściowa, który będziemy oznaczać symbolem .
Wartość wyrażenia w stanie początkowym wynosi
o ile zachodzi:
Będziemy potrzebowac tranzycji dwóch postaci, podobnie jak poprzednio, ale pierwsza postać będzie trochę ogólniejsza:
Tranzycja ta oznacza mały krok w trakcie obliczania wyrażenia w stanie , w wyniku którego wyewoluowało do a nowym stanem jest . Stan może się teraz zmienić na skutek deklaracji zmiennych.
Spróbujmy rozszerzyc semantykę z poprzedniego zadania. Ponieważ stan jest funkcją częściową, musimy zmienić niektóre reguły, np.
Następnie dodajemy reguły dla wyrażenia . Gdy jest już obliczne, wyatarczy reguła:
Notacja oznacza stan , który zmodyfikowano przypisując zmiennej wartość , niezależnie od tego, czy było określone, czy nie, i pozostawiając niezmienione wartości dla pozostałych zmiennych. Formanie
W szczególności, dla , jest określone wtedy i tylko wtedy, gdy jest określone.
Natomiast aby obliczyc potrzebujemy reguły:
Zwróćmy uwagę, że stan może być różny od , np. dlatego, że wewnątrz znajduje się podwyrażenie .
Pytanie: czy taka semantyka jest poprawna?
Niestety nie, gdyż nie uwzględniamy ograniczonego zasięgu zmiennej. Rzućmy okiem na przykład:
Według naszych intencji to wyrażenie nie ma wartości, gdyż ostatnie odwołanie do jest błędne. Natomiast według powyższych reguł mamy
Nasz błąd polega na tym, że po zakończeniu obliczania podwyrażenia zapominamy przywrócić zmiennej poprzednią wartość (a właściwie brak wartości w przykładzie powyżej). Przedyskutujmy kilka wariantów.
Wariant 1
Wygodne i eleganckie rozwiązanie tego problemu jest możliwe, jeśli rozszerzymy składnię naszego języka. Intuicyjnie, reguła
powinna zostać zastąpiona przez
czyli potrzebujemy konstrukcji składniowej, która polega na obliczeniu wyrażenia a następnie na przypisaniu zmiennej danej wartości. Rozszerzmy zatem składnię następujaco:
Zauważmy, że wyrażenie jest w pewnym sensie dualne do , gdyż jedyna (choc niewątpliwie istotna) różnica między nimi to kolejność obliczenia i przypisania wartości na zmienną . Oto nowa reguła
Pewna trudność pojawia się w sytuacji, gdy jest nieokreślone, czyli gdy zmienna jest niezainicjowana -- reguła powyższa nie obejmuje wogóle takiej sytuacji. Najprostszym sposobem rozwiązania tej trudności jest rozszerzenie konstrukcji :
gdzie symbol oznacza brak wartości. Dodajemy również regułę:
Rozwiązanie to jest odrobinę nieeleganckie, gdyż prawie identyczne reguły musimy napisać dwukrotnie. Widać to np. w poniższych regułach, scalających semantykę dla z semantyką pozostałych wyrażeń:
Wariant 2
Zanim przejdziemy do kolejnego wariantu, zastanówmy się czy istnieje inny sposób rozwiązania trudności związanej z , który pozwalałby uniknąć wprowadzania dodatkowej konstrukcji . Pomysł może polegać na rozszerzeniu zbioru o dodatkowy element :
Wtedy nie musimy pisać dwóch bardzo podobnych wariantów reguł. Dodatkowo, w tym rozwiązaniu warto poczynić umowę, że reprezentuje brak wartości zmiennej . Wtedy stany są funkcjami całkowitymi z w przyjmującymi wartość różną od tylko dla skończenie wielu elementów. Pewnym mankamentem jest to, że teraz może pojawiać sie w wyrażeniach podobnie jak stałe. Tym niemniej nie musimy adaptować reguł dla stałych tak, aby radziły one sobie z , ponieważ wyrażenia zawierające możemy również uważać za roszerzenie składni.
Jeśli jednak dopuścimy symbol w wyrażeniach, to możemy elegancko wybrnąć z sytuacji, rozszerzając operacje arytmetyczne na zbiór tak, aby zachowywały one nieokreśloność:
Trzeba jednak w takim razie zadbać o to, aby wyrażenie obliczało się normalnie tylko wtedy, gdy wartość wyrażenia jest różna od .
Wariant 3
Zrewidujmy teraz podstawowe założenia, które dotychczas poczyniliśmy. Jednym z nich było przyjęcie ogólnej postaci tranzycji:
pozwalającej na zmianę stanu podczas obliczania wyrażenia. Czy faktycznie był to dobry pomysł? Czy moglibyśmy poradzić sobie przy pomocy tranzycji postaci
Spróbujmy! Oto nowa wersja jednej z reguł dla dotycząca kroku wewnątrz :
Dotychczas nie ma problemu: podwyrażenie jest prawidłowo obliczane w stanie . Trudność pojawi się, gdy zakończymy obliczanie i przejdziemy do . Oto możliwa reguła:
Okazuje się, że wszystko jest w porządku. Wyrażenie obliczamy w prawidłowym stanie, tzn. z wartością przypisaną zmiennej . Mały krok w daje przyczynek do małego kroku w całym wyrażeniu, a przy tym stan pozostaje niezmieniony. Przy tym wogóle nie potrzebujemy przywracać poprzedniej wartości zmiennej , ponieważ zyskuje nową wartość tylko na potrzeby obliczania podwyrażenia ! Można na to również spojrzeć inaczej: informacja o nowej wartości dla zmiennej nie jest jawnie dodawana do stanu , ale jest przechowywana w składni wyrażenia jako deklaracja . Na końcu musimy oczywiście pozbyć się tej deklaracji za pomocą następującej tranzycji:
Podsumujmy. Okazuje się, że rozwiązanie nie było wcale łatwe, nawet dla tak prościutkiego języka. W przyszłości przekonamy się, że łatwiej jest poradzić sobie z zagadnieniem wiązania identyfikatorów w semantyce naturalnej (duże kroki). W wariancie 1 i 2 wprowadziliśmy do języka dodatkowe elementy, tak, by łatwiej było pisać reguły. W przyszłości będziemy czasem stosować takie podejście. Niekiedy jednak rozszerzanie języka będzie zabronione.
Zadania domowe
Zadanie 1
Zapisz wariant 2 semantyki z poprzedniego zadania.
Zadanie 2
Dotychczas wystąpienie błędu podczas obliczania wyrażenia, np. odwołanie do niezainicjowanej zmiennej, powodowało, że wyrażenie nie posiadało wartości (nie było ciągu tranzycji prowadzących do konfiguracji końcowej). Zmodyfikuj którąś z semantyk z poprzednich zadań tak, aby błąd był komunikowany jako jedna z konfiguracji końcowych. To znaczy: jeśli obliczenie wyrażenia w stanie jest niemożliwe bo wystąpił błąd, to
Zadanie 3
Rozważ rozszerzenie języka wyrażeń o wyrażenia boolowskie:
Zaproponuj semantykę małych kroków dla tego języka. Rozważ różne strategie obliczania wyrażeń boolowskich, oraz podejście leniwe. Na przykład w strategii lewostronnej dla , gdy zostało obliczone do , nie ma wogóle potrzeby obliczania .