Semantyka i weryfikacja programów/Ćwiczenia 5: Różnice pomiędzy wersjami
Nie podano opisu zmian |
Nie podano opisu zmian |
||
Linia 1: | Linia 1: | ||
== Zawartość == | |||
Kontynuujemy ćwiczenie semantyki naturalnej, | |||
dodając pewne konstrukcje do języka Tiny. | |||
W szczególności rozszerzymy go o deklaracje zmiennych (bloki). | |||
Po raz pierwszy roszdzielimy informację przechowywaną w konfiguracji | |||
na ''środowisko'' określające wiązanie identyfikatorów i ''stan'' | |||
przechowujący wartości zmiennych. | |||
Będzie to przygotowanie do kolejnych zajęć. | |||
== Zadania z rozwiązaniami == | |||
==== Zadanie 1 ==== | |||
Rozszerzamy język Tiny następująco: | |||
<math> | |||
I \, ::= \,\, | |||
\ldots \,\,|\,\, | |||
\mathbf{for}\, x = e_1 \,\mathbf{to}\, e_2 \,\mathbf{try}\, I_1 \,\mathbf{else}\, I_2 \,\,|\,\, | |||
\mathbf{fail} | |||
</math> | |||
Znaczenie instrukcji <math> \mathbf{for}\, x = e_1 \,\mathbf{to}\, e_2 \,\mathbf{try}\, I_1 \,\mathbf{else}\, I_2 </math> | |||
jest następujące. | |||
Obliczamy wartości wyrażeń <math> e_1 </math> i <math> e_2 </math>. Jeśli pierwsza | |||
z nich jest mniejsza lub równa od drugiej, to podstawiamy pierwszą | |||
wartość (wartość wyrażenia <math> e_1 </math>) na zmienną <math> x </math> i uruchamiamy <math> I_1 </math>. | |||
Jeśli w <math> I_1 </math> nie zostanie napotkana instrukcja <math> \mathbf{fail} </math>, | |||
kończymy instrukcję <math> \mathbf{for}\, </math> i przyracamy wartość zmiennej <math> x </math> | |||
sprzed tej instrukcji. | |||
Natomiast jeśli w <math> I_1 </math> zostanie napotkana instrukcja <math> \mathbf{fail} </math>, | |||
podstawiamy na <math> x </math> kolejną, o jeden większą wartość, | |||
przywracamy wartości wszystkich | |||
pozostałych zmiennych sprzed instrukcji <math> \mathbf{for}\, </math>, i ponownie | |||
wykonujemy <math> I_1 </math>. | |||
Powtarzamy w ten sposób aż <math> I_1 </math> zakończy się nie napotkawszy <math> \mathbf{fail} </math>, | |||
albo wartość zmiennej <math> x </math> przekroczy wartość wyrażenia <math> e_2 </math> | |||
obliczoną na początku. | |||
W pierwszym przypadku | |||
kończymy instrukcję <math> \mathbf{for}\, </math> i przyracamy wartość zmiennej <math> x </math> | |||
sprzed tej instrukcji. | |||
W drugim przywracamy wartości wszystkich zmiennych sprzed instrukcji | |||
<math> \mathbf{for}\, </math> i uruchamiamy <math> I_2 </math>. | |||
===== Przykład ===== | |||
<math> | |||
x := 0; y := 1; | |||
\mathbf{for}\, x := 1 \,\mathbf{to}\, 5 \,\mathbf{try}\, | |||
\quad y := y+1; | |||
\quad \mathbf{if}\, x \leq 4 \,\mathbf{then}\, \mathbf{fail} \,\mathbf{else}\, z:= x | |||
\,\mathbf{else}\, \mathbf{skip} | |||
</math> | |||
Wartości zmiennych po zakończeniu programu to: | |||
<math> x = 0, y = 2, z = 5 </math>. | |||
==== Rozwiązanie ==== | |||
Będziemy posługiwać się jak zwykle tranzycjami postaci: | |||
<math> | |||
I, s \,\longrightarrow\, s' \quad \quad \quad e, s \,\longrightarrow\, n. | |||
</math> | |||
Zacznijmy od najprostszego przypadku, gdy nie ma wogóle potrzeby | |||
uruchamiania instrukcji wewnętrznej <math> I_1 </math>, ponieważ | |||
wartość wyrażenia <math> e_1 </math> jest większa od <math> e_2 </math>: | |||
<math> | |||
\frac{e_1, s \,\longrightarrow\, n_1 | |||
\quad \quad | |||
e_2, s \,\longrightarrow\, n_2 | |||
\quad \quad | |||
I_2, s \,\longrightarrow\, s'} | |||
{\mathbf{for}\, x = e_1 \,\mathbf{to}\, e_2 \,\mathbf{try}\, I_1 \,\mathbf{else}\, I_2, s \,\longrightarrow\, s'} | |||
\quad \mbox{ o ile } n_1 > n_2. | |||
</math> | |||
Po prostu uruchamiamy instrukcję <math> I_2 </math> w stanie <math> s </math>, ignorując | |||
<math> I_1 </math>. | |||
Kolejny nietrudny przypadek to sytuacja, w której | |||
<math> n_1 \leq n_2 </math> a wykonanie instrukcji wewnętrznej <math> I_1 </math> | |||
kończy się sukcesem, tzn. nie następuje wywołanie instrukcji | |||
<math> \mathbf{fail} </math>. Oto reguła dla tego przypadku: | |||
<math> | |||
\frac{e_1, s \,\longrightarrow\, n_1 | |||
\quad \quad | |||
e_2, s \,\longrightarrow\, n_2 | |||
\quad \quad | |||
I_1, s[x \mapsto n_1] \,\longrightarrow\, s'} | |||
{\mathbf{for}\, x = e_1 \,\mathbf{to}\, e_2 \,\mathbf{try}\, I_1 \,\mathbf{else}\, I_2, s \,\longrightarrow\, s'[x \mapsto s(x)]} | |||
\quad \mbox{ o ile } n_1 \leq n_2. | |||
</math> | |||
Tym razem uruchamiamy instrukcję <math> I_1 </math>, podstawiając | |||
pod zmienną <math> x </math> wartość <math> n_1 </math>. | |||
Zwróćmy uwagę, że po zakończeniu <math> I_1 </math> przywracamy | |||
wartość zmiennej <math> x </math> sprzed jej wykonania. | |||
Pozostał nam trzeci przypadek, gdy <math> n_1 \leq n_2 </math> | |||
a wykonanie instrukcji <math> I_1 </math> zakończyło się | |||
wykonaniem instrukcji <math> \mathbf{fail} </math> gdzieś wewnątrz <math> I_1 </math>. | |||
Aby poprawnie opisać takie zdarzenie, potrzebujemy dodatkowych | |||
tranzycji postaci: | |||
<math> | |||
I, s \,\longrightarrow\, \mbox{było-}\mathbf{fail}. | |||
</math> | |||
Mówiąc formalnie, poszerzamy zbiór konfiguracji o jeden element | |||
<math> \mbox{było-}\mathbf{fail} </math>. | |||
Zauważmy, że po prawej stronie tranzycji <math> dkrok </math> nie ma wogóle | |||
stanu. | |||
Nie potrzebujemy go dla opisania semantyki instrukcji <math> \mathbf{for}\, </math>: | |||
jeśli wystąpi <math> \mathbf{fail} </math>, powtarzamy <math> I_1 </math> dla większej o <math> 1 </math> wartości | |||
zmiennej <math> x </math>, ale pozostałym zmiennym przywracamy wartość ''sprzed'' | |||
<math> I_1 </math>. | |||
A więc załóżmy, że | |||
<math> I_1, s[x \mapsto n_1] \,\longrightarrow\, \mbox{było-}\mathbf{fail}. </math> | |||
W takiej sytuacji powinniśmy przypisać <math> n_1 + 1 </math> | |||
na zmienną <math> x </math> i spróbować ponownie wykonać | |||
<math> I_1 </math> przy wartościach wszystkich pozostałych | |||
zmiennych takich, jak na początku instrukcji <math> \mathbf{for}\, </math>. | |||
I powtarzać ten schemat aż do skutku, tzn. aż do mementu, | |||
gdy zaistnieje któraś z poprzednich dwóch (prostych) sytuacji. | |||
Oto odpowiednia reguła: | |||
<math> | |||
\frac{e_1, s \,\longrightarrow\, n_1 | |||
\quad \quad | |||
e_2, s \,\longrightarrow\, n_2 | |||
\quad \quad | |||
I_1, s[x \mapsto n_1] \,\longrightarrow\, \mbox{było-}\mathbf{fail} | |||
\quad \quad | |||
\mathbf{for}\, x = e_1 + 1 \,\mathbf{to}\, e_2 \,\mathbf{try}\, I_1 \,\mathbf{else}\, I_2, s \drok s'} | |||
{\mathbf{for}\, x = e_1 \,\mathbf{to}\, e_2 \,\mathbf{try}\, I_1 \,\mathbf{else}\, I_2, s \,\longrightarrow\, s'} | |||
\quad \mbox{ o ile } n_1 \leq n_2. | |||
</math> | |||
Zwróćmy uwagę na pewnien niezwykle istotny szczegół: po zakończeniu | |||
wykonania instrukcji <math> \mathbf{for}\, x = e_1 + 1 \,\mathbf{to}\, e_2 \,\mathbf{try}\, I_1 \,\mathbf{else}\, I_2 </math> | |||
otrzymujemy stan <math> s' </math>, w którym '''nie przywracamy''' wartości | |||
zmiennej <math> x </math>. Gdybyśmy tak zrobili, tzn. gdybyśmy zastąpili <math> s' | |||
</math> przez <math> s'[x \mapsto s(x)] </math>, nasze semantyka byłaby | |||
niepoprawna. Dlaczego? Dlatego, że nie wiemy tak naprawdę, | |||
czy powinniśmy przywracać wartość zmiennej <math> x </math> czy nie. | |||
Jeśli ostatecznie nasza instrukcja <math> \mathbf{for}\, </math> zakończyla | |||
się przez bezbłędne zakończenie instrukcji <math> I_1 </math> | |||
(przypadek drugi), to powinniśmy to zrobić; | |||
ale jeśli zakończyła się wykonaniem instrukcji <math> I_2 </math> | |||
(przypadek pierwszy), | |||
to powinniśmy pozostawić wartość zmiennej <math> x </math> taką, | |||
jaka jest ona po zakończeniu <math> I_2 </math>. | |||
A zatem w powyższej regule dla przypadku trzeciego | |||
nie przywracamy wartości zmiennej <math> x </math>; jeśli było | |||
to konieczne, to zostało już wykonane ,,głębiej", | |||
dzięki regule dla przypadku drugiego oraz dzięki temu, | |||
że instrukcja <math> \mathbf{for}\, x = e_1 + 1 \,\mathbf{to}\, e_2 \,\mathbf{try}\, I_1 \,\mathbf{else}\, I_2 </math> | |||
wykonywana jest w orginalnym stanie <math> s </math>, | |||
a nie w stanie, w którym kończy się <math> I_1 </math> | |||
(tego ostatniego stanu nawet nie znamy). | |||
Na zakończenie prezentujemy reguły niebędne do tego, | |||
aby wygenerować <math> \mbox{było-}\mathbf{fail} </math>: | |||
<math> | |||
\mathbf{fail}, s \,\longrightarrow\, \mbox{było-}\mathbf{fail} | |||
</math> | |||
oraz aby wystąpienie <math> \mathbf{fail} </math> umiejscowione gdzieś ,,głęboko" | |||
zostało rozpropagowane do najbliższej otaczającej | |||
instrukcji <math> \mathbf{for}\, </math>. | |||
Jeśli pojawił się <math> \mathbf{fail} </math>, powinniśmy zaniechać dalszego | |||
wykonania instrukcji a w przypadku pętli, powinniśmy | |||
zaniechać dalszego iterowania tej pętli. | |||
Oto odpowiednie reguły: | |||
<math> | |||
\frac{I_1, s \,\longrightarrow\, s', \mbox{było-}\mathbf{fail}} | |||
{I_1;\, I_2, s \,\longrightarrow\, s', \mbox{było-}\mathbf{fail}} | |||
\quad \quad | |||
\frac{I_1, s \,\longrightarrow\, s' \quad \quad I_2, s' \,\longrightarrow\, s'', \mbox{było-}\mathbf{fail}} | |||
{I_1;\, I_2, s \,\longrightarrow\, s'', \mbox{było-}\mathbf{fail}} | |||
</math> | |||
<math> | |||
\frac{b, s \,\longrightarrow\, \mathbf{true} \quad \quad I_1, s \,\longrightarrow\, s', \mbox{było-}\mathbf{fail}} | |||
{\mathbf{if}\, b \,\mathbf{then}\, I_1 \,\mathbf{else}\, I_2, s \,\longrightarrow\, s', \mbox{było-}\mathbf{fail}} | |||
\quad \quad | |||
\mbox{ i analogicznie gdy } b, s \,\longrightarrow\, \mathbf{true} | |||
</math> | |||
<math> | |||
\frac{b, s \,\longrightarrow\, \mathbf{true} \quad \quad I, s \,\longrightarrow\, s', \mbox{było-}\mathbf{fail}} | |||
{\mathbf{while}\, b \,\mathbf{do}\, I, s \,\longrightarrow\, s', \mbox{było-}\mathbf{fail}} | |||
\quad \quad | |||
\frac{b, s \,\longrightarrow\, \mathbf{true} \quad \quad I, s \,\longrightarrow\, s' \quad \quad | |||
\mathbf{while}\, b \,\mathbf{do}\, I, s' \,\longrightarrow\, s'', \mbox{było-}\mathbf{fail}} | |||
{\mathbf{while}\, b \,\mathbf{do}\, I, s \,\longrightarrow\, s', \mbox{było-}\mathbf{fail}} | |||
</math> | |||
Widać podobieństwo do analogicznych reguł dla | |||
pętli <math> \mathbf{loop}\, I </math>, którą zajmowaliśmy się na wcześniejszych | |||
zajęciach. | |||
Zauważmy, że jesli <math> \mathbf{fail} </math> zostało wykonane poza jakąkolwiek | |||
pętlą <math> \mathbf{for}\, </math>, to program ,,zakończy się" w konfiguracji | |||
<math> \mbox{było-}\mathbf{fail} </math>. Możemy zatem tę właśnie konfigurację | |||
uznać za konfigurację końcową, informującą o porażce | |||
wykonania programu. | |||
==== Zadanie 2 (bloki i deklaracje zmiennych) ==== | |||
Rozszerz semantykę języka Tiny o deklarację zmiennej: | |||
<math> | |||
I \, ::= \,\, | |||
\ldots \,\,|\,\, | |||
\mathbf{begin}\, \mathbf{var}\, x = e;\, I \,\mathbf{end} | |||
</math> | |||
Zasięgiem zmiennej <math> x </math> jest instrukcja <math> I </math> | |||
czyli wnętrze bloku, w którym jest zadeklarowana. | |||
Zakładamy zwykłe (statyczne) reguły widoczności, przesłaniania, itp. | |||
==== Rozwiązanie 1 (tylko stan) ==== | |||
Oczywiście powinniśmy odróżniać zmienne zadeklarowane (i zarazem | |||
zainicjowane) od tych niezadeklarowanych. | |||
Zatem przyjmijmy, że | |||
<math> \mathbf{State} = \mathbf{Var} \to_{\mathrm{fin}} \mathbf{Num} </math>. | |||
Zachowuujemy wszystkie reguły semantyczne dla języka Tiny | |||
i dodajemy jedną nową: | |||
<math> | |||
\frac{e, s \,\longrightarrow\, n \quad \quad I, s[x \mapsto n] \,\longrightarrow\, s'} | |||
{\mathbf{begin}\, \mathbf{var}\, x = e;\, I \,\mathbf{end}, s \,\longrightarrow\, s'} | |||
</math> | |||
mówiącą, że instrukcja wewnętrzna <math> I </math> ewaluowana jest | |||
w stanie, w którym dodatkow zaznaczono wartość zmiennej <math> x </math> | |||
równą wartości, do której oblicza się <math> e </math>. | |||
Oczywiście takie rozwiązanie jest niepoprawne, gdyż wartość | |||
zmiennej <math> x </math> pozostaje w stanie nawet po wyjściu z bloku | |||
(,,wycieka" z bloku). | |||
Musimy dodać ,,dealokację" zmiennej <math> x </math>: | |||
<math> | |||
\frac{e, s \,\longrightarrow\, n \quad \quad I, s[x \mapsto n] \,\longrightarrow\, s'} | |||
{\mathbf{begin}\, \mathbf{var}\, x = e;\, I \,\mathbf{end}, s \,\longrightarrow\, s''} | |||
\quad \mbox{ o ile } s'' = s'[x \mapsto s(x)] | |||
</math> | |||
I znów napotykamy podobnie trudności jak na wcześniejszych zajęciach: | |||
powyższa reguła nie obejmuje przypadku, gdy <math> s(x) </math> jest | |||
nieokreślone. | |||
I choć moglibyśmy naprawić ten mankament podobnie jak kiedyś | |||
(co pozostawiamy Czytelnikowi), istnieje inne, bardzo eleganckie | |||
i elastyczne rozwiązanie tego problemu, które teraz omówimy. | |||
==== Rozwiązanie 2 (stan i środowisko) ==== | |||
Podstawowy pomysł polega na rozdzielenie informacji przechowywanej | |||
dotychczas w stanie na dwa ,,etapy". | |||
Pierwszy z nich, odwzorowuje identyfikatory zmiennych na ''lokacje'', a drugi | |||
lokacje na wartości. | |||
Mamy więc ''środowiska'' <math> E \in \mathbf{Env}], będące funkcjami częściowymi z <math> \mathbf{Var} </math> | |||
do <math> \mathbf{Loc} </math>, zbioru lokacji: | |||
<math> | |||
\mathbf{Env} = \mathbf{Var} \to_{\mathrm{fin}} \mathbf{Loc} | |||
</math> | |||
oraz stany, będące teraz funkcjami częściowymi z <math> \mathbf{Loc} </math> do <math> \mathbf{Num} </math>: | |||
<math> | |||
\mathbf{State} = \mathbf{Loc} \to_{\mathrm{fin}} \mathbf{Num}. | |||
</math> | |||
Intuicyjnie można myśleć o lokacjach jak o lokacjach w pamięci | |||
operacyjnej maszyny. Środowisko zawiera informację o lokacji, w której | |||
przechowywana jest wartość danej zmiennej. | |||
A stan opisuje właśnie zawartość używanych lokacji. | |||
Zakładamy przy tym, że mamy do dyspozycji nieskończony zbiór | |||
lokacji <math> \mathbf{Loc} = \{ l_0, l_1, \ldots \} </math> i że | |||
w każdym momencie tylko skończenie wiele spośród nich jest | |||
wykorzystywane. | |||
Formalnie mowiąc, dziedzina funkcji częściowej <math> s </math> jest zawsze skończony. | |||
Daje nam to pewność, że zawsze jest jakaś nieużywana lokacja. | |||
Środowisko początkowe, w którym uruchamiany | |||
będzie program, będzie z reguły puste. | |||
Ponadto obraz funkcji częściowej <math> E </math> | |||
będzie zawsze zawarty w zbiorze aktualnie używanych lokacji, | |||
czyli zawarty w dziedzinie funkcji częściowej <math> s </math>, oznaczanej | |||
<math> \dom(s) </math>. | |||
Pożytek z tego podziału na dwa etapy będzie taki, że | |||
będziemy umieli łatwo i elastycznie opisywać deklaracje zmiennych, | |||
przesłanianie identyfikatorów, itp. | |||
Tranzycje będą teraz postaci: | |||
<math> | |||
I, s, E \,\longrightarrow\, s' | |||
</math> | |||
czyli instrukcja <math> I </math> będzie modyfikować stan ale nie będzie | |||
zmieniać środowiska <math> E </math>. Dla czytelności będziemy zapisywać | |||
nasze reguły w następujący sposób: | |||
<math> | |||
E \,\vdash\, I, s \,\longrightarrow\, s' | |||
</math> | |||
podkreślając w ten sposób, że środowisko nie ulega zmianie. | |||
Ale należy pamiętać, że konfiguracja, w której ,,uruchamiamy" | |||
instrukcję <math> I </math> składa się naprawdę z pary <math> (E, s) </math>. | |||
Deklarując nową zmienną <math> x </math> dodamy po prostu do <math> E </math> parę | |||
<math> (x, l) </math>, gdzie <math> l </math> jest nową, nieużywaną dotychczas lokacją. | |||
Dla wygody zapisu załóżmy, że mamy do dyspozycji funkcję | |||
funkcję | |||
<math> | |||
\mathtt{new} : \mathbf{State} \to \mathbf{Loc} | |||
</math> | |||
która zwraca jakąś nową, nieużywaną lokację. Formalnie wymagamy, by | |||
<math> | |||
\mathtt{new}(s) \notin \dom(s). | |||
</math> | |||
Dla ilustracji popatrzmy na przykładową regułę dla deklaracji. | |||
<math> | |||
\frac{E \,\vdash\, e, s \,\longrightarrow\, n \quad \quad | |||
E[x \mapsto l] \,\vdash\, I, s[l \mapsto n] \,\longrightarrow\, s'} | |||
{E \,\vdash\, \mathbf{begin}\, \mathbf{var}\, x=e;\, I \,\mathbf{end}, s \,\longrightarrow\, s'} | |||
\quad \mbox{ gdzie } l = \mathtt{new}(s) | |||
</math> | |||
Zauważmy, że stan <math> s' </math> po zakończeniu instrukcji | |||
<math> \mathbf{begin}\, \mathbf{var}\, x=e;\, I \,\mathbf{end} </math> zawiera informację o zmiennej | |||
lokalnej <math> x </math>, tzn. <math> s(l) </math> jest określone. | |||
Ale lokacja <math> l </math> jest ,,nieosiągalna" w środowisku | |||
<math> E </math>, gdyż para <math> x \mapsto l </math> została dodana tylko do | |||
środowiska, w którym ewaluuje się wnętrze bloku, a środowisko | |||
<math> E </math> całego bloku nie jest modyfikowane. | |||
Poniżej przedstawiamy reguły dla pozostałych instrukcji. | |||
Przede wszystkim złożenie sekwencyjne: | |||
<math> | |||
\frac{E \,\vdash\, I_1, s \,\longrightarrow\, s' | |||
\quad \quad | |||
E, \,\vdash\, I_2, s' \,\longrightarrow\, s''} | |||
{E \,\vdash\, I_1;\, I_2, s \,\longrightarrow\, s''} | |||
</math> | |||
Reguła ta uzmysławia nam różnicę pomiędzy środowiskiem a stanem: | |||
środowisko pozostaje to samo, gdy przechodzimy od jednej instrukcji do | |||
następn\ej, a stan oczywiście ewoluuje wraz ze zmieniającymi się | |||
wartościami zmiennych. | |||
Reguły dla przypisania, instrukcji warunkowej i pętli | |||
nie przedstawiają żadnych nowych trudności. | |||
Musimy tylko najpierw ustalić postać reguł dla wyrażeń: | |||
<math> | |||
E \,\vdash\, e, s \,\longrightarrow\, n | |||
</math> | |||
która jest zupełnie naturalna w naszym podejściu opartym o środowiska | |||
i stany. | |||
Reguły mogą wyglądać np. tak: | |||
<math> | |||
\frac{E \,\vdash\, e, s \,\longrightarrow\, n} | |||
{E \,\vdash\, x := e, s \,\longrightarrow\, s[x \mapsto n]} | |||
\quad \quad | |||
E \,\vdash\, \mathbf{skip}, s \,\longrightarrow\, s | |||
</math> | |||
<math> | |||
\frac{E \,\vdash\, b, s \,\longrightarrow\, \mathbf{true} | |||
\quad \quad | |||
E \,\vdash\, I_1, s \,\longrightarrow\, s'} | |||
{E \,\vdash\, \mathbf{if}\, b \,\mathbf{then}\, I_1 \,\mathbf{else}\, I_2, s \,\longrightarrow\, s'} | |||
\quad \quad | |||
\frac{E \,\vdash\, b, s \,\longrightarrow\, \mathbf{true} | |||
\quad \quad | |||
E \,\vdash\, I_2, s \,\longrightarrow\, s'} | |||
{E \,\vdash\, \mathbf{if}\, b \,\mathbf{then}\, I_1 \,\mathbf{else}\, I_2, s \,\longrightarrow\, s'} | |||
</math> | |||
<math> | |||
{E \,\vdash\, \mathbf{if}\, b \,\mathbf{then}\, (\mathbf{while}\, b \,\mathbf{do}\, I) \,\mathbf{else}\, \mathbf{skip}, s \,\longrightarrow\, s'} | |||
{E \,\vdash\, \mathbf{while}\, b \,\mathbf{do}\, I, s \,\longrightarrow\, s'} | |||
</math> | |||
Reguły dla wyrażeń są oczywiste: | |||
<math> | |||
E \,\vdash\, n, s \,\longrightarrow\, n | |||
\quad \quad | |||
E \,\vdash\, x, s \,\longrightarrow\, n | |||
\quad \mbox{ o ile } E(x)=l \in \mathbf{Loc} \mbox{ i } s(l) = n | |||
</math> | |||
i tak dalej -- pomijamy pozostałe reguły. | |||
===== Wariant (dealokacja) ===== | |||
Przypomnijmy sobie semantykę bloku | |||
<math> | |||
\frac{E \,\vdash\, e, s \,\longrightarrow\, n \quad \quad | |||
E[x \mapsto l] \,\vdash\, I, s[l \mapsto n] \,\longrightarrow\, s'} | |||
{E \,\vdash\, \mathbf{begin}\, \mathbf{var}\, x=e;\, I \,\mathbf{end}, s \,\longrightarrow\, s'} | |||
\quad \mbox{ gdzie } l = \mathtt{new}(s) | |||
</math> | |||
i zastanówmy się, jak ,,posprzątać" po zakończeniu wykonania bloku, | |||
tzn. zwolnić lokację <math> l </math>, która była używana tylko w tym bloku | |||
i w związku z tym nie będzie już potrzebne. | |||
Oznaczałoby to przywrócenie lokacji <math> l </math> do puli wolnych | |||
(nieużywanych) lokacji. | |||
Zmodyfikowana reguła dla instrukcji bloku powinna wyglądać mniej więcej tak: | |||
<math> | |||
\frac{E \,\vdash\, d, s \,\longrightarrow\, (E', s') | |||
\quad \quad | |||
E' \,\vdash\, I, s' \,\longrightarrow\, s''} | |||
{E \,\vdash\, \mathbf{begin}\, d;\, I \,\mathbf{end}, s \,\longrightarrow\, \bar{s}} | |||
</math> | |||
gdzie <math> \bar{s} </math> to stan <math> s'' </math> ,,okrojony" do dziedziny stanu <math> s </math>. | |||
Zatem powinniśmy po prostu przywrócić nieokreśloność stanu dla lokacji | |||
<math> l </math>. | |||
Natomiast oczywiście nie ma potrzeby dealokownia środowiska! | |||
Oto rozwiązanie: | |||
<math> | |||
\frac{E \,\vdash\, e, s \,\longrightarrow\, n \quad \quad | |||
E[x \mapsto l] \,\vdash\, I, s[l \mapsto n] \,\longrightarrow\, s'} | |||
{E \,\vdash\, \mathbf{begin}\, \mathbf{var}\, x=e;\, I \,\mathbf{end}, s \,\longrightarrow\, s' \setminus \{ (l, s'(l)) \}} | |||
\quad \mbox{ gdzie } l = \mathtt{new}(s) | |||
</math> | |||
== Zadania domowe == | |||
==== Zadanie 1 ==== | |||
Napisz semantykę naturalną dla następującego | |||
rozszerzenia języka Tiny: | |||
<math> | |||
I \, ::= \,\, | |||
\ldotes \,\,|\,\, | |||
\mathbf{throw}\, x \,\,|\,\, | |||
\,\mathbf{try}\, I_1 \,\mathbf{catch}\, exc\, I_2 | |||
</math> | |||
<math> | |||
exc \, ::= \,\, | |||
x \,\,|\,\, | |||
\mathbf{any} | |||
</math> | |||
Instrukcja <math> \mathbf{throw}\, x </math> oznacza podniesienie wyjątku o nazwie <math> x </math>. | |||
Instrukcja <math> I = \,\mathbf{try}\, I_1 \,\mathbf{catch}\, exc\, I_2 </math> wykonuje <math> I_1 </math>. | |||
Jeśli podczas wykonania <math> I_1 </math> zostanie podniesiony wyjątej <math> x | |||
</math>, i <math> exc = x </math> albo <math> exc = \mathbf{any} </math>, to następuje przerwanie <math> | |||
I_1 </math> i sterowanie zostaje przeniesione do <math> I_2 </math> | |||
(następuje ''obsługa wyjątku''). | |||
Jeśli zaś podczas wykonania <math> I_1 </math> zostanie podniesiony wyjątej <math> x | |||
</math>, i <math> exc \neq x </math> i <math> exc \neq \mathbf{any} </math>, to obsługa wyjątku | |||
przekazana jest do najbliższej instrukcji <math> \,\mathbf{try}\, </math> otaczającej <math> I | |||
</math>. | |||
Umawiamy się, że <math> \,\mathbf{try}\, I_1 \,\mathbf{catch}\, exc\, I_2 </math> ''otacza'' <math> I_1 </math> | |||
i wszystkie instrukcje wewnątrz <math> I_1 </math>, | |||
ale ''nie'' otacza <math> I_2 </math>. | |||
==== Zadanie 2 ==== | |||
Zaproponuj modyfikację semantyki języka Small, w której deklaracja | |||
jest wykonywana ,,równolegle\'', analogicznie do przypisania | |||
równoległego. Przy takiej semantyce kolejność poszczególnych | |||
deklaracji nie powinna wpływać na wynik programu. |
Wersja z 08:51, 7 sie 2006
Zawartość
Kontynuujemy ćwiczenie semantyki naturalnej, dodając pewne konstrukcje do języka Tiny. W szczególności rozszerzymy go o deklaracje zmiennych (bloki). Po raz pierwszy roszdzielimy informację przechowywaną w konfiguracji na środowisko określające wiązanie identyfikatorów i stan przechowujący wartości zmiennych. Będzie to przygotowanie do kolejnych zajęć.
Zadania z rozwiązaniami
Zadanie 1
Rozszerzamy język Tiny następująco:
Znaczenie instrukcji jest następujące. Obliczamy wartości wyrażeń i . Jeśli pierwsza z nich jest mniejsza lub równa od drugiej, to podstawiamy pierwszą wartość (wartość wyrażenia ) na zmienną i uruchamiamy . Jeśli w nie zostanie napotkana instrukcja , kończymy instrukcję i przyracamy wartość zmiennej sprzed tej instrukcji. Natomiast jeśli w zostanie napotkana instrukcja , podstawiamy na kolejną, o jeden większą wartość, przywracamy wartości wszystkich pozostałych zmiennych sprzed instrukcji , i ponownie wykonujemy . Powtarzamy w ten sposób aż zakończy się nie napotkawszy , albo wartość zmiennej przekroczy wartość wyrażenia obliczoną na początku. W pierwszym przypadku kończymy instrukcję i przyracamy wartość zmiennej sprzed tej instrukcji. W drugim przywracamy wartości wszystkich zmiennych sprzed instrukcji i uruchamiamy .
Przykład
Wartości zmiennych po zakończeniu programu to: .
Rozwiązanie
Będziemy posługiwać się jak zwykle tranzycjami postaci:
Zacznijmy od najprostszego przypadku, gdy nie ma wogóle potrzeby uruchamiania instrukcji wewnętrznej , ponieważ wartość wyrażenia jest większa od :
Po prostu uruchamiamy instrukcję w stanie , ignorując .
Kolejny nietrudny przypadek to sytuacja, w której a wykonanie instrukcji wewnętrznej kończy się sukcesem, tzn. nie następuje wywołanie instrukcji . Oto reguła dla tego przypadku:
Tym razem uruchamiamy instrukcję , podstawiając pod zmienną wartość . Zwróćmy uwagę, że po zakończeniu przywracamy wartość zmiennej sprzed jej wykonania.
Pozostał nam trzeci przypadek, gdy a wykonanie instrukcji zakończyło się wykonaniem instrukcji gdzieś wewnątrz . Aby poprawnie opisać takie zdarzenie, potrzebujemy dodatkowych tranzycji postaci:
Mówiąc formalnie, poszerzamy zbiór konfiguracji o jeden element . Zauważmy, że po prawej stronie tranzycji nie ma wogóle stanu. Nie potrzebujemy go dla opisania semantyki instrukcji : jeśli wystąpi , powtarzamy dla większej o wartości zmiennej , ale pozostałym zmiennym przywracamy wartość sprzed . A więc załóżmy, że
W takiej sytuacji powinniśmy przypisać na zmienną i spróbować ponownie wykonać przy wartościach wszystkich pozostałych zmiennych takich, jak na początku instrukcji . I powtarzać ten schemat aż do skutku, tzn. aż do mementu, gdy zaistnieje któraś z poprzednich dwóch (prostych) sytuacji. Oto odpowiednia reguła:
Parser nie mógł rozpoznać (nieznana funkcja „\drok”): {\displaystyle \frac{e_1, s \,\longrightarrow\, n_1 \quad \quad e_2, s \,\longrightarrow\, n_2 \quad \quad I_1, s[x \mapsto n_1] \,\longrightarrow\, \mbox{było-}\mathbf{fail} \quad \quad \mathbf{for}\, x = e_1 + 1 \,\mathbf{to}\, e_2 \,\mathbf{try}\, I_1 \,\mathbf{else}\, I_2, s \drok s'} {\mathbf{for}\, x = e_1 \,\mathbf{to}\, e_2 \,\mathbf{try}\, I_1 \,\mathbf{else}\, I_2, s \,\longrightarrow\, s'} \quad \mbox{ o ile } n_1 \leq n_2. }
Zwróćmy uwagę na pewnien niezwykle istotny szczegół: po zakończeniu wykonania instrukcji otrzymujemy stan , w którym nie przywracamy wartości zmiennej . Gdybyśmy tak zrobili, tzn. gdybyśmy zastąpili przez , nasze semantyka byłaby niepoprawna. Dlaczego? Dlatego, że nie wiemy tak naprawdę, czy powinniśmy przywracać wartość zmiennej czy nie. Jeśli ostatecznie nasza instrukcja zakończyla się przez bezbłędne zakończenie instrukcji (przypadek drugi), to powinniśmy to zrobić; ale jeśli zakończyła się wykonaniem instrukcji (przypadek pierwszy), to powinniśmy pozostawić wartość zmiennej taką, jaka jest ona po zakończeniu . A zatem w powyższej regule dla przypadku trzeciego nie przywracamy wartości zmiennej ; jeśli było to konieczne, to zostało już wykonane ,,głębiej", dzięki regule dla przypadku drugiego oraz dzięki temu, że instrukcja wykonywana jest w orginalnym stanie , a nie w stanie, w którym kończy się (tego ostatniego stanu nawet nie znamy).
Na zakończenie prezentujemy reguły niebędne do tego, aby wygenerować :
oraz aby wystąpienie umiejscowione gdzieś ,,głęboko" zostało rozpropagowane do najbliższej otaczającej instrukcji . Jeśli pojawił się , powinniśmy zaniechać dalszego wykonania instrukcji a w przypadku pętli, powinniśmy zaniechać dalszego iterowania tej pętli. Oto odpowiednie reguły:
Widać podobieństwo do analogicznych reguł dla pętli , którą zajmowaliśmy się na wcześniejszych zajęciach.
Zauważmy, że jesli zostało wykonane poza jakąkolwiek pętlą , to program ,,zakończy się" w konfiguracji . Możemy zatem tę właśnie konfigurację uznać za konfigurację końcową, informującą o porażce wykonania programu.
Zadanie 2 (bloki i deklaracje zmiennych)
Rozszerz semantykę języka Tiny o deklarację zmiennej:
Zasięgiem zmiennej jest instrukcja czyli wnętrze bloku, w którym jest zadeklarowana. Zakładamy zwykłe (statyczne) reguły widoczności, przesłaniania, itp.
Rozwiązanie 1 (tylko stan)
Oczywiście powinniśmy odróżniać zmienne zadeklarowane (i zarazem zainicjowane) od tych niezadeklarowanych. Zatem przyjmijmy, że
.
Zachowuujemy wszystkie reguły semantyczne dla języka Tiny i dodajemy jedną nową:
mówiącą, że instrukcja wewnętrzna ewaluowana jest w stanie, w którym dodatkow zaznaczono wartość zmiennej równą wartości, do której oblicza się . Oczywiście takie rozwiązanie jest niepoprawne, gdyż wartość zmiennej pozostaje w stanie nawet po wyjściu z bloku (,,wycieka" z bloku). Musimy dodać ,,dealokację" zmiennej :
I znów napotykamy podobnie trudności jak na wcześniejszych zajęciach: powyższa reguła nie obejmuje przypadku, gdy jest nieokreślone. I choć moglibyśmy naprawić ten mankament podobnie jak kiedyś (co pozostawiamy Czytelnikowi), istnieje inne, bardzo eleganckie i elastyczne rozwiązanie tego problemu, które teraz omówimy.
Rozwiązanie 2 (stan i środowisko)
Podstawowy pomysł polega na rozdzielenie informacji przechowywanej dotychczas w stanie na dwa ,,etapy". Pierwszy z nich, odwzorowuje identyfikatory zmiennych na lokacje, a drugi lokacje na wartości. Mamy więc środowiska Parser nie mógł rozpoznać (błąd składni): {\displaystyle E \in \mathbf{Env}], będące funkcjami częściowymi z <math> \mathbf{Var} } do , zbioru lokacji:
oraz stany, będące teraz funkcjami częściowymi z do :
Intuicyjnie można myśleć o lokacjach jak o lokacjach w pamięci operacyjnej maszyny. Środowisko zawiera informację o lokacji, w której przechowywana jest wartość danej zmiennej. A stan opisuje właśnie zawartość używanych lokacji.
Zakładamy przy tym, że mamy do dyspozycji nieskończony zbiór lokacji i że w każdym momencie tylko skończenie wiele spośród nich jest wykorzystywane. Formalnie mowiąc, dziedzina funkcji częściowej jest zawsze skończony. Daje nam to pewność, że zawsze jest jakaś nieużywana lokacja.
Środowisko początkowe, w którym uruchamiany będzie program, będzie z reguły puste. Ponadto obraz funkcji częściowej będzie zawsze zawarty w zbiorze aktualnie używanych lokacji, czyli zawarty w dziedzinie funkcji częściowej , oznaczanej Parser nie mógł rozpoznać (nieznana funkcja „\dom”): {\displaystyle \dom(s) } .
Pożytek z tego podziału na dwa etapy będzie taki, że będziemy umieli łatwo i elastycznie opisywać deklaracje zmiennych, przesłanianie identyfikatorów, itp. Tranzycje będą teraz postaci:
czyli instrukcja będzie modyfikować stan ale nie będzie zmieniać środowiska . Dla czytelności będziemy zapisywać nasze reguły w następujący sposób:
podkreślając w ten sposób, że środowisko nie ulega zmianie. Ale należy pamiętać, że konfiguracja, w której ,,uruchamiamy" instrukcję składa się naprawdę z pary .
Deklarując nową zmienną dodamy po prostu do parę , gdzie jest nową, nieużywaną dotychczas lokacją. Dla wygody zapisu załóżmy, że mamy do dyspozycji funkcję funkcję
która zwraca jakąś nową, nieużywaną lokację. Formalnie wymagamy, by
Parser nie mógł rozpoznać (nieznana funkcja „\dom”): {\displaystyle \mathtt{new}(s) \notin \dom(s). }
Dla ilustracji popatrzmy na przykładową regułę dla deklaracji.
Zauważmy, że stan po zakończeniu instrukcji zawiera informację o zmiennej lokalnej , tzn. jest określone. Ale lokacja jest ,,nieosiągalna" w środowisku , gdyż para została dodana tylko do środowiska, w którym ewaluuje się wnętrze bloku, a środowisko całego bloku nie jest modyfikowane.
Poniżej przedstawiamy reguły dla pozostałych instrukcji.
Przede wszystkim złożenie sekwencyjne:
Reguła ta uzmysławia nam różnicę pomiędzy środowiskiem a stanem: środowisko pozostaje to samo, gdy przechodzimy od jednej instrukcji do następn\ej, a stan oczywiście ewoluuje wraz ze zmieniającymi się wartościami zmiennych.
Reguły dla przypisania, instrukcji warunkowej i pętli nie przedstawiają żadnych nowych trudności. Musimy tylko najpierw ustalić postać reguł dla wyrażeń:
która jest zupełnie naturalna w naszym podejściu opartym o środowiska i stany. Reguły mogą wyglądać np. tak:
Reguły dla wyrażeń są oczywiste:
i tak dalej -- pomijamy pozostałe reguły.
Wariant (dealokacja)
Przypomnijmy sobie semantykę bloku
i zastanówmy się, jak ,,posprzątać" po zakończeniu wykonania bloku, tzn. zwolnić lokację , która była używana tylko w tym bloku i w związku z tym nie będzie już potrzebne. Oznaczałoby to przywrócenie lokacji do puli wolnych (nieużywanych) lokacji.
Zmodyfikowana reguła dla instrukcji bloku powinna wyglądać mniej więcej tak:
gdzie to stan ,,okrojony" do dziedziny stanu . Zatem powinniśmy po prostu przywrócić nieokreśloność stanu dla lokacji . Natomiast oczywiście nie ma potrzeby dealokownia środowiska! Oto rozwiązanie:
Zadania domowe
Zadanie 1
Napisz semantykę naturalną dla następującego rozszerzenia języka Tiny:
Parser nie mógł rozpoznać (nieznana funkcja „\ldotes”): {\displaystyle I \, ::= \,\, \ldotes \,\,|\,\, \mathbf{throw}\, x \,\,|\,\, \,\mathbf{try}\, I_1 \,\mathbf{catch}\, exc\, I_2 }
Instrukcja oznacza podniesienie wyjątku o nazwie . Instrukcja wykonuje . Jeśli podczas wykonania zostanie podniesiony wyjątej , i albo , to następuje przerwanie i sterowanie zostaje przeniesione do (następuje obsługa wyjątku). Jeśli zaś podczas wykonania zostanie podniesiony wyjątej , i i , to obsługa wyjątku przekazana jest do najbliższej instrukcji otaczającej . Umawiamy się, że otacza i wszystkie instrukcje wewnątrz , ale nie otacza .
Zadanie 2
Zaproponuj modyfikację semantyki języka Small, w której deklaracja jest wykonywana ,,równolegle\, analogicznie do przypisania równoległego. Przy takiej semantyce kolejność poszczególnych deklaracji nie powinna wpływać na wynik programu.