Semantyka i weryfikacja programów/Ćwiczenia 1: Różnice pomiędzy wersjami
Nie podano opisu zmian |
Nie podano opisu zmian |
||
Linia 1: | Linia 1: | ||
== Ćwiczenia 1: Semantyka operacyjna wyrażeń == | == Ćwiczenia 1: Semantyka operacyjna wyrażeń == | ||
==== Zadanie | ==== Zadanie 1 ==== | ||
Semantyka języka Tiny z wykładu używała funkcji semantycznych | Semantyka języka Tiny z wykładu używała funkcji semantycznych | ||
<math> | <math> | ||
Linia 13: | Linia 12: | ||
w stylu dużych kroków (semantyka naturalna) i małych kroków. | w stylu dużych kroków (semantyka naturalna) i małych kroków. | ||
==== Rozwiązanie | ==== Rozwiązanie ==== | ||
Przypomnijmy składnię wyrażeń boolowskich i arytmetycznych: | Przypomnijmy składnię wyrażeń boolowskich i arytmetycznych: | ||
Linia 52: | Linia 51: | ||
<math>s \in State</math>, <math>n</math> jest liczbą całkowitą, | <math>s \in State</math>, <math>n</math> jest liczbą całkowitą, | ||
<math>n \in Int</math>, a <math>l \in Bool = \{ true, false \}</math>. | <math>n \in Int</math>, a <math>l \in Bool = \{ true, false \}</math>. | ||
Tranzycja taka oznacza, że wyrażenie <math>e</math> w stanie <math>s<math> wylicza się do | Tranzycja taka oznacza, że wyrażenie <math>e</math> w stanie <math>s</math> wylicza się do | ||
wartości <math>n</math>, oraz że wyrażenie logiczne <math>b</math> w | wartości <math>n</math>, oraz że wyrażenie logiczne <math>b</math> w | ||
stanie <math>s</math> wylicza się do <math>l</math>. | stanie <math>s</math> wylicza się do <math>l</math>. | ||
Linia 340: | Linia 339: | ||
<math> | <math> | ||
\frac{b, s \longrightarrow true} | \frac{b, s \longrightarrow true} | ||
{\mathbf{while}\, b\, \mathbf{do}\ | {\mathbf{while}\, b\, \mathbf{do}\, I, s \Longrightarrow | ||
I;\, \mathbf{while}\, b\, \mathbf{do}\, I, s} | I;\, \mathbf{while}\, b\, \mathbf{do}\, I, s} | ||
\quad \quad \quad | \quad \quad \quad | ||
Linia 369: | Linia 368: | ||
Niektóre konfiguracje nie były wogóle przez nas używane. Które? | Niektóre konfiguracje nie były wogóle przez nas używane. Które? | ||
==== Zadanie | ==== Zadanie 2 ==== | ||
Rozważ dodatkowo operację dzielenia: | Rozważ dodatkowo operację dzielenia: | ||
<math> | <math> | ||
Linia 381: | Linia 380: | ||
jeśli błąd wystąpił. | jeśli błąd wystąpił. | ||
==== Rozwiązanie (szkic) | ==== Rozwiązanie (szkic) ==== | ||
Dopóki nie wystąpi błąd dzielenia przez zero, semantyka programu | Dopóki nie wystąpi błąd dzielenia przez zero, semantyka programu | ||
Linia 431: | Linia 430: | ||
<math> | <math> | ||
\frac{e_1, s \ | \frac{e_1, s \longrightarrow Blad} | ||
{e_1 \leq e_2, s \ | {e_1 \leq e_2, s \longrightarrow Blad} | ||
\quad \quad \quad | \quad \quad \quad | ||
\frac{e_2, s \ | \frac{e_2, s \longrightarrow Blad} | ||
{e_1 \leq e_2, s \ | {e_1 \leq e_2, s \longrightarrow Blad} | ||
</math> | </math> | ||
Linia 442: | Linia 441: | ||
<math> | <math> | ||
\frac{b \ | \frac{b \longrightarrow Blad} | ||
{\mathbf{if}\, b\, \mathbf{then}\, I_1\, \mathbf{else}\, I_2, s \ | {\mathbf{if}\, b\, \mathbf{then}\, I_1\, \mathbf{else}\, I_2, s \longrightarrow Blad} | ||
\quad \quad \quad | \quad \quad \quad | ||
\frac{b \ | \frac{b \longrightarrow Blad} | ||
{\mathbf{while}\, b\, \mathbf{do}\, I, s \ | {\mathbf{while}\, b\, \mathbf{do}\, I, s \longrightarrow Blad} | ||
</math> | </math> | ||
Linia 452: | Linia 451: | ||
<math> | <math> | ||
\frac{I_1, s \ | \frac{I_1, s \longrightarrow Blad} | ||
{I_1;\, I_2, s \ | {I_1;\, I_2, s \longrightarrow Blad} | ||
\quad \quad \quad | \quad \quad \quad | ||
\frac{I_1, s \ | \frac{I_1, s \longrightarrow Blad} | ||
{\mathbf{if}\, b\, \mathbf{then}\, I_1\, \mathbf{else}\, I_2, s \ | {\mathbf{if}\, b\, \mathbf{then}\, I_1\, \mathbf{else}\, I_2, s \longrightarrow Blad} | ||
\quad \quad \quad | \quad \quad \quad | ||
\frac{I_2, s \ | \frac{I_2, s \longrightarrow Blad} | ||
{\mathbf{if}\, b\, \mathbf{then}\, I_1\, \mathbf{else}\, I_2, s \ | {\mathbf{if}\, b\, \mathbf{then}\, I_1\, \mathbf{else}\, I_2, s \longrightarrow Blad} | ||
\quad \quad \quad | \quad \quad \quad | ||
\frac{I, s \ | \frac{I, s \longrightarrow Blad} | ||
{\mathbf{while}\, b\, \mathbf{do}\, I, s \ | {\mathbf{while}\, b\, \mathbf{do}\, I, s \longrightarrow Blad} | ||
</math> | </math> | ||
Linia 470: | Linia 469: | ||
(Powyżej traktowaliśmy tranzycję | (Powyżej traktowaliśmy tranzycję | ||
<math> I, s \ | <math> I, s \longrightarrow Blad </math> jak ''duży krok''. | ||
Ale tak naprawdę pojawienie się błędu powinno spowodować | Ale tak naprawdę pojawienie się błędu powinno spowodować | ||
''natychmiastowe'' zatrzymanie programu, więc może tranzycję taką | ''natychmiastowe'' zatrzymanie programu, więc może tranzycję taką | ||
można również traktować jak ''mały krok'', | można również traktować jak ''mały krok'', | ||
<math> I, s \ | <math> I, s \Longrightarrow Blad </math>?) |
Wersja z 07:10, 24 lip 2006
Ćwiczenia 1: Semantyka operacyjna wyrażeń
Zadanie 1
Semantyka języka Tiny z wykładu używała funkcji semantycznych dla określenie znaczenia wyrażeń boolowskich i arytmetycznych. Zdefiniuj znaczenie wyrażeń za pomocą semantyki operacyjnej, w stylu dużych kroków (semantyka naturalna) i małych kroków.
Rozwiązanie
Przypomnijmy składnię wyrażeń boolowskich i arytmetycznych:
Zacznijmy od dużych kroków.
Semantyka naturalna
Chcemy, aby tranzycje wyrażen wyglądały następująco:
gdzie , jest liczbą całkowitą, , a . Tranzycja taka oznacza, że wyrażenie w stanie wylicza się do wartości , oraz że wyrażenie logiczne w stanie wylicza się do . Zauważmy, że zakładamy tu, iż obliczenie wyrażenia nie zmienia stanu (nie ma efektów ubocznych).
W tym celu rozszerzamy zbiór konfiguracji następująco:
gdzie Instr oznacza zbiór instrukcji (jedna z kategorii syntaktycznych języka Tiny), Expr zbiór wyrażeń arytmetycznych a BExpr zbiór wyrażeń boolowskich. . Konfiguracje końcowe pozostają bez zmian (State). Tranzycje dla instrukcji pozostają zasadniczo bez zmian, z tym, że odwołania do funkcji semantycznych dla wyrazżen zastępujemy przez odpowiednie tranzycje. Np. dla instrukcji pętli będziemy mieć następujące reguły:
Podobnie dla instrukcji warunkowej. Teraz zajmiemy się tranzycjami dla wyrażeń. Zacznijmy od stalych arytmetycznych:
Zauważmy, iż celowo nie odróżniamy liczby od stałej reprezentującej tę liczbę, która może pojawić się w wyrażeniach zgodnie z przyjętą przez nas składnią. Czyli zakładamy, że Int jest podzbiorem zbioru wyrażeń. W powyższej tranzycji, po lewej stronie to stała reprezentująca liczbę, która widnieje po prawej stronie.
Analogiczne tranzycje dla stałych boolowskich to:
Analogicznie, czynimy tu założenie, że Bool jest podbiorem wyrażen boolowskich.
Operatory arytmetyczne definiujemy następująco:
Czyli aby obliczyć sumę w stanie , trzeba najpierw obliczyć i w stanie , a następnie dodać obliczone wartości. Zauważmy, że nie specyfikujemy kolejności, w jakiej mają się obliczać i . I choć tutaj nie ma to żadnego znaczenia, w przyszłości będzie inaczej, gdy jezyk będzie umożliwiał efekty uboczne podzas obliczania wyrażeń.
Podobne reguły można napisać dla pozostałych operacji arytmnetycznych, oraz dla spójników logicznych:
Oczywiście jeśli oblicza się do false, wartość całego wyrażenia jest false niezależnie od wartości wyrażenia . Czyli jeśli zaczniemy od obliczenia i wynikiem będzie false, to nie ma wogóle potrzeby obliczania . Oto odpowiednie reguły (nazwijmy je regułami lewo-stronnymi, ponieważ rozpoczynamy od lewego koniunktu):
Wybraliśmy następującą kolejność obliczania wyrażeń: najpierw b_1, potem b_2. Pozostawiamy Czytelnikowi napisanie analogicznych reguł dla kolejności odwrotnej (reguły prawo-stronne).
Rozważmy też następującą kombinację obydwu semantyk (reguły równoległe):
Czyli jeśli którekolwiek z podwyrażeń daje wynik false, to taki wynik zyskuje całe wyrażenie. Dodatkowo potrzebujemy jeszcze reguły:
Zauważmy, że powyższych reguł nie da sie zaimplementować sekwencyjnie: nie wiadomo czy najpierw obliczać czy . Reguły te odpowiadają raczej strategii ,,równoległej: obliczaj ,,jednocześnie b_1 i b_2 do momentu, gdy jedno z nich obliczy się do wynikiem false, albo aż obydwa się zakończą z wynikiem true.
W naszym prostym języku wszystkie cztery warianty są równoważne. Różnice pomiędzy nimi zobaczymy jednak już w następnym zadaniu, w którym pojawi się prosta odmiana efektów ubocznych (błąd wykonania).
Reguły dla pozostałych spójników logicznych oraz dla
negacji pozostawiamy jako ćwiczenie.
A teraz małe kroki.
Strukturalna semantyka operacyjna (małe kroki)
Chcemy, aby tranzycje dla wyrażeń były postaci: i podobnie dla wyrażeń boolowskich: gdzie . Przyjmijmy na razie takie same konfiguracje i konfiguracje końcowe jak dla semantyki naturalnej.
Zacznijmy od wyrażeń boolowskich.
Przejdźmy do spójników logicznych, powiedzmy . Ponieważ opisujemy teraz pojedyncze (małe) kroki składające się na wykonanie programu, musimy podać w jakiej kolejności będą się obliczać i . Zacznijmy od strategii lewostronnej:
Podobnie jak poprzednio, możemy zaniechać obliczania jeśli oblicza się do false. Oto odpowiednio zmodyfikowane reguły:
Analogicznie reguły prawostronne to:
Reguły równoległe otrzymujemy jako sumę reguł lewo- i prawostronnych (w sumie 6 reguł). Zauważmy, że obliczanie wyrażeń i odbywa się teraz w twz. przeplocie: Pojedynczy krok polega na wykonaniu jednego kroku obliczenia albo jednego kroku obliczenia . Zwróćmy też uwagę, że po raz pierwszy nasze tranzycje nie posiadają własności determinizmu: wyrażenie może wyewoluować w pojedyńczym kroku albo do albo do . Na szczęście, końcowy wynik, do jakiego oblicza się wyrażenie jest zawsze taki sam, niezależnie od przeplotu.
Oto reguła dla negacji:
Reguły dla są następujące:
Reguły powyższe zależą od semantyki wyrażen arytmetycznych. Zauważmy, że ponownie pozostawiliśmy dowolność jeśli chodzi o kolejność obliczania wyrażeń arytmetycznych e_1 i e_2.
Rozważmy teraz instrukcję warunkową i instrukcję pętli. Najpierw obliczamy wartość dozoru:
a gdy dozór jest już obliczony, podejmujemy decyzję. W przypadku instrukcji warunkowej reguły są oczywiste:
Gorzej jest w przypadku instukcji pętli. Reguła mogłaby wyglądać tak:
ale nie wiemy już, jaki był dozór pętli (widzimy tylko wynik obliczenia tego dozoru w stanie s, true). Możemy odwołać się do tranzycji dużych kroków:
Takie rozwiązanie nie jest zatem czystą semantyką małych kroków. Istnieją inne możliwe rozwiązania, w stylu małych kroków, których znalezienie pozostawiamy dociekliwemu czytelnikowi.
Na koniec podajemy reguły dla operacji arytmetycznych, na przykładzie dodawania. Przyjmijmy, dla przykładu, strategię lewostronną:
Niektóre konfiguracje nie były wogóle przez nas używane. Które?
Zadanie 2
Rozważ dodatkowo operację dzielenia: i rozszerz semantyki z poprzedniego zadania. Dzielenie przez zero jest błądem i kończy natychmiast wykonanie programu. Oprócz stanu wynikiem programu powinna byc informacja o błędzie, jeśli błąd wystąpił.
Rozwiązanie (szkic)
Dopóki nie wystąpi błąd dzielenia przez zero, semantyka programu powinna być identyczna jak w poprzednim zadaniu. Zatem pozostawiamy wszystkie reguły z poprzedniego zadania, tak w semantyce naturalnej jak i w semantyce małych kroków. Dodatkowo potrzebujemy reguł, które opiszą
- kiedy powstaje błąd oraz
- jak zachowuje się program po wystąpieniu błędu
Zaczynamy od pierwszego punktu. W tym celu dodajemy do konfiguracji jedną konfigurację końcową . Reguła opisująca powstanie błędu może wyglądać np. tak w semantyce naturalnej:
a tak w semantyce małych kroków:
Pomijamy tutaj reguły dla przypadku, gdy oblicza się do wartości różnej od zera. Ponadto dla czytelności przyjęliśmy, że wynikiem tranzycji jest wyłącznie informacja o błędzie, a stan jest zapominany. Bez trudu możnaby wszystkie reguły (zarówno te powyżej jak i te poniżej) zmodyfikować tak, by wraz z informacją o błędzie zwracany był też stan, w którym błąd się pojawił. Np. ostatnia reguła wyglądałaby następująco:
I zamiast pojedyńczej konfiguracji końcowej , potrzebowalibyśmy oczywiście całego zbioru .
Przejdźmy teraz do drugiego punktu. Potrzebujemy dodatkowych reguł, które zagwarantują, że błąd, raz pojawiwszy się, propaguje się przez wszystkie konstrukcje składniowe, a normalne obliczenie wyrażenia jest wstrzymame. Zacznijmy od sementyki naturalnej:
Następnie, błąd w wyrażeniu powinien wstrzymać normalne wykonanie instrukcji:
I wreszcie błąd powinien propagować się do kolejnych instrukcji:
I tak dalej.
Pytanie dla Czytelnika: jak napisać tę semantykę w podejściu mało-krokowym?
(Powyżej traktowaliśmy tranzycję jak duży krok. Ale tak naprawdę pojawienie się błędu powinno spowodować natychmiastowe zatrzymanie programu, więc może tranzycję taką można również traktować jak mały krok, ?)