Semantyka i weryfikacja programów/Ćwiczenia 5: Różnice pomiędzy wersjami

Z Studia Informatyczne
Przejdź do nawigacjiPrzejdź do wyszukiwania
Mengel (dyskusja | edycje)
Nie podano opisu zmian
Sl (dyskusja | edycje)
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:

I::=|𝐟𝐨𝐫x=e1𝐭𝐨e2𝐭𝐫𝐲I1𝐞𝐥𝐬𝐞I2|𝐟𝐚𝐢𝐥

Znaczenie instrukcji 𝐟𝐨𝐫x=e1𝐭𝐨e2𝐭𝐫𝐲I1𝐞𝐥𝐬𝐞I2 jest następujące. Obliczamy wartości wyrażeń e1 i e2. Jeśli pierwsza z nich jest mniejsza lub równa od drugiej, to podstawiamy pierwszą wartość (wartość wyrażenia e1) na zmienną x i uruchamiamy I1. Jeśli w I1 nie zostanie napotkana instrukcja 𝐟𝐚𝐢𝐥, kończymy instrukcję 𝐟𝐨𝐫 i przyracamy wartość zmiennej x sprzed tej instrukcji. Natomiast jeśli w I1 zostanie napotkana instrukcja 𝐟𝐚𝐢𝐥, podstawiamy na x kolejną, o jeden większą wartość, przywracamy wartości wszystkich pozostałych zmiennych sprzed instrukcji 𝐟𝐨𝐫, i ponownie wykonujemy I1. Powtarzamy w ten sposób aż I1 zakończy się nie napotkawszy 𝐟𝐚𝐢𝐥, albo wartość zmiennej x przekroczy wartość wyrażenia e2 obliczoną na początku. W pierwszym przypadku kończymy instrukcję 𝐟𝐨𝐫 i przyracamy wartość zmiennej x sprzed tej instrukcji. W drugim przywracamy wartości wszystkich zmiennych sprzed instrukcji 𝐟𝐨𝐫 i uruchamiamy I2.


Przykład

x:=0;y:=1;𝐟𝐨𝐫x:=1𝐭𝐨5𝐭𝐫𝐲y:=y+1;𝐢𝐟x4𝐭𝐡𝐞𝐧𝐟𝐚𝐢𝐥𝐞𝐥𝐬𝐞z:=x𝐞𝐥𝐬𝐞𝐬𝐤𝐢𝐩

Wartości zmiennych po zakończeniu programu to: x=0,y=2,z=5.


Rozwiązanie

Będziemy posługiwać się jak zwykle tranzycjami postaci:

I,sse,sn.

Zacznijmy od najprostszego przypadku, gdy nie ma wogóle potrzeby uruchamiania instrukcji wewnętrznej I1, ponieważ wartość wyrażenia e1 jest większa od e2:

e1,sn1e2,sn2I2,ss𝐟𝐨𝐫x=e1𝐭𝐨e2𝐭𝐫𝐲I1𝐞𝐥𝐬𝐞I2,ss o ile n1>n2.

Po prostu uruchamiamy instrukcję I2 w stanie s, ignorując I1.

Kolejny nietrudny przypadek to sytuacja, w której n1n2 a wykonanie instrukcji wewnętrznej I1 kończy się sukcesem, tzn. nie następuje wywołanie instrukcji 𝐟𝐚𝐢𝐥. Oto reguła dla tego przypadku:

e1,sn1e2,sn2I1,s[xn1]s𝐟𝐨𝐫x=e1𝐭𝐨e2𝐭𝐫𝐲I1𝐞𝐥𝐬𝐞I2,ss[xs(x)] o ile n1n2.

Tym razem uruchamiamy instrukcję I1, podstawiając pod zmienną x wartość n1. Zwróćmy uwagę, że po zakończeniu I1 przywracamy wartość zmiennej x sprzed jej wykonania.

Pozostał nam trzeci przypadek, gdy n1n2 a wykonanie instrukcji I1 zakończyło się wykonaniem instrukcji 𝐟𝐚𝐢𝐥 gdzieś wewnątrz I1. Aby poprawnie opisać takie zdarzenie, potrzebujemy dodatkowych tranzycji postaci:

I,sbyło-𝐟𝐚𝐢𝐥.

Mówiąc formalnie, poszerzamy zbiór konfiguracji o jeden element było-𝐟𝐚𝐢𝐥. Zauważmy, że po prawej stronie tranzycji dkrok nie ma wogóle stanu. Nie potrzebujemy go dla opisania semantyki instrukcji 𝐟𝐨𝐫: jeśli wystąpi 𝐟𝐚𝐢𝐥, powtarzamy I1 dla większej o 1 wartości zmiennej x, ale pozostałym zmiennym przywracamy wartość sprzed I1. A więc załóżmy, że

I1,s[xn1]było-𝐟𝐚𝐢𝐥.

W takiej sytuacji powinniśmy przypisać n1+1 na zmienną x i spróbować ponownie wykonać I1 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 𝐟𝐨𝐫x=e1+1𝐭𝐨e2𝐭𝐫𝐲I1𝐞𝐥𝐬𝐞I2 otrzymujemy stan s, w którym nie przywracamy wartości zmiennej x. Gdybyśmy tak zrobili, tzn. gdybyśmy zastąpili s przez s[xs(x)], nasze semantyka byłaby niepoprawna. Dlaczego? Dlatego, że nie wiemy tak naprawdę, czy powinniśmy przywracać wartość zmiennej x czy nie. Jeśli ostatecznie nasza instrukcja 𝐟𝐨𝐫 zakończyla się przez bezbłędne zakończenie instrukcji I1 (przypadek drugi), to powinniśmy to zrobić; ale jeśli zakończyła się wykonaniem instrukcji I2 (przypadek pierwszy), to powinniśmy pozostawić wartość zmiennej x taką, jaka jest ona po zakończeniu I2. A zatem w powyższej regule dla przypadku trzeciego nie przywracamy wartości zmiennej x; 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 𝐟𝐨𝐫x=e1+1𝐭𝐨e2𝐭𝐫𝐲I1𝐞𝐥𝐬𝐞I2 wykonywana jest w orginalnym stanie s, a nie w stanie, w którym kończy się I1 (tego ostatniego stanu nawet nie znamy).

Na zakończenie prezentujemy reguły niebędne do tego, aby wygenerować było-𝐟𝐚𝐢𝐥:

𝐟𝐚𝐢𝐥,sbyło-𝐟𝐚𝐢𝐥

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:

I1,ss,było-𝐟𝐚𝐢𝐥I1;I2,ss,było-𝐟𝐚𝐢𝐥I1,ssI2,ss,było-𝐟𝐚𝐢𝐥I1;I2,ss,było-𝐟𝐚𝐢𝐥

b,s𝐭𝐫𝐮𝐞I1,ss,było-𝐟𝐚𝐢𝐥𝐢𝐟b𝐭𝐡𝐞𝐧I1𝐞𝐥𝐬𝐞I2,ss,było-𝐟𝐚𝐢𝐥 i analogicznie gdy b,s𝐭𝐫𝐮𝐞

b,s𝐭𝐫𝐮𝐞I,ss,było-𝐟𝐚𝐢𝐥𝐰𝐡𝐢𝐥𝐞b𝐝𝐨I,ss,było-𝐟𝐚𝐢𝐥b,s𝐭𝐫𝐮𝐞I,ss𝐰𝐡𝐢𝐥𝐞b𝐝𝐨I,ss,było-𝐟𝐚𝐢𝐥𝐰𝐡𝐢𝐥𝐞b𝐝𝐨I,ss,było-𝐟𝐚𝐢𝐥

Widać podobieństwo do analogicznych reguł dla pętli 𝐥𝐨𝐨𝐩I, 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 było-𝐟𝐚𝐢𝐥. 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:

I::=|𝐛𝐞𝐠𝐢𝐧𝐯𝐚𝐫x=e;I𝐞𝐧𝐝

Zasięgiem zmiennej x jest instrukcja I 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

𝐒𝐭𝐚𝐭𝐞=𝐕𝐚𝐫fin𝐍𝐮𝐦.

Zachowuujemy wszystkie reguły semantyczne dla języka Tiny i dodajemy jedną nową:

e,snI,s[xn]s𝐛𝐞𝐠𝐢𝐧𝐯𝐚𝐫x=e;I𝐞𝐧𝐝,ss

mówiącą, że instrukcja wewnętrzna I ewaluowana jest w stanie, w którym dodatkow zaznaczono wartość zmiennej x równą wartości, do której oblicza się e. Oczywiście takie rozwiązanie jest niepoprawne, gdyż wartość zmiennej x pozostaje w stanie nawet po wyjściu z bloku (,,wycieka" z bloku). Musimy dodać ,,dealokację" zmiennej x:

e,snI,s[xn]s𝐛𝐞𝐠𝐢𝐧𝐯𝐚𝐫x=e;I𝐞𝐧𝐝,ss o ile s=s[xs(x)]

I znów napotykamy podobnie trudności jak na wcześniejszych zajęciach: powyższa reguła nie obejmuje przypadku, gdy s(x) 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:

𝐄𝐧𝐯=𝐕𝐚𝐫fin𝐋𝐨𝐜

oraz stany, będące teraz funkcjami częściowymi z 𝐋𝐨𝐜 do 𝐍𝐮𝐦:

𝐒𝐭𝐚𝐭𝐞=𝐋𝐨𝐜fin𝐍𝐮𝐦.

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 𝐋𝐨𝐜={l0,l1,} i że w każdym momencie tylko skończenie wiele spośród nich jest wykorzystywane. Formalnie mowiąc, dziedzina funkcji częściowej s 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 E będzie zawsze zawarty w zbiorze aktualnie używanych lokacji, czyli zawarty w dziedzinie funkcji częściowej s, 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:

I,s,Es

czyli instrukcja I będzie modyfikować stan ale nie będzie zmieniać środowiska E. Dla czytelności będziemy zapisywać nasze reguły w następujący sposób:

EI,ss

podkreślając w ten sposób, że środowisko nie ulega zmianie. Ale należy pamiętać, że konfiguracja, w której ,,uruchamiamy" instrukcję I składa się naprawdę z pary (E,s).

Deklarując nową zmienną x dodamy po prostu do E parę (x,l), gdzie l jest nową, nieużywaną dotychczas lokacją. Dla wygody zapisu załóżmy, że mamy do dyspozycji funkcję funkcję

new:𝐒𝐭𝐚𝐭𝐞𝐋𝐨𝐜

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.

Ee,snE[xl]I,s[ln]sE𝐛𝐞𝐠𝐢𝐧𝐯𝐚𝐫x=e;I𝐞𝐧𝐝,ss gdzie l=new(s)

Zauważmy, że stan s po zakończeniu instrukcji 𝐛𝐞𝐠𝐢𝐧𝐯𝐚𝐫x=e;I𝐞𝐧𝐝 zawiera informację o zmiennej lokalnej x, tzn. s(l) jest określone. Ale lokacja l jest ,,nieosiągalna" w środowisku E, gdyż para xl została dodana tylko do środowiska, w którym ewaluuje się wnętrze bloku, a środowisko E całego bloku nie jest modyfikowane.


Poniżej przedstawiamy reguły dla pozostałych instrukcji. Przede wszystkim złożenie sekwencyjne:

EI1,ssE,I2,ssEI1;I2,ss

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ń:

Ee,sn

która jest zupełnie naturalna w naszym podejściu opartym o środowiska i stany. Reguły mogą wyglądać np. tak:

Ee,snEx:=e,ss[xn]E𝐬𝐤𝐢𝐩,ss

Eb,s𝐭𝐫𝐮𝐞EI1,ssE𝐢𝐟b𝐭𝐡𝐞𝐧I1𝐞𝐥𝐬𝐞I2,ssEb,s𝐭𝐫𝐮𝐞EI2,ssE𝐢𝐟b𝐭𝐡𝐞𝐧I1𝐞𝐥𝐬𝐞I2,ss

E𝐢𝐟b𝐭𝐡𝐞𝐧(𝐰𝐡𝐢𝐥𝐞b𝐝𝐨I)𝐞𝐥𝐬𝐞𝐬𝐤𝐢𝐩,ssE𝐰𝐡𝐢𝐥𝐞b𝐝𝐨I,ss

Reguły dla wyrażeń są oczywiste:

En,snEx,sn o ile E(x)=l𝐋𝐨𝐜 i s(l)=n

i tak dalej -- pomijamy pozostałe reguły.


Wariant (dealokacja)

Przypomnijmy sobie semantykę bloku

Ee,snE[xl]I,s[ln]sE𝐛𝐞𝐠𝐢𝐧𝐯𝐚𝐫x=e;I𝐞𝐧𝐝,ss gdzie l=new(s)

i zastanówmy się, jak ,,posprzątać" po zakończeniu wykonania bloku, tzn. zwolnić lokację l, 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 l do puli wolnych (nieużywanych) lokacji.

Zmodyfikowana reguła dla instrukcji bloku powinna wyglądać mniej więcej tak:

Ed,s(E,s)EI,ssE𝐛𝐞𝐠𝐢𝐧d;I𝐞𝐧𝐝,ss¯

gdzie s¯ to stan s ,,okrojony" do dziedziny stanu s. Zatem powinniśmy po prostu przywrócić nieokreśloność stanu dla lokacji l. Natomiast oczywiście nie ma potrzeby dealokownia środowiska! Oto rozwiązanie:

Ee,snE[xl]I,s[ln]sE𝐛𝐞𝐠𝐢𝐧𝐯𝐚𝐫x=e;I𝐞𝐧𝐝,ss{(l,s(l))} gdzie l=new(s)


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 }

exc::=x|𝐚𝐧𝐲

Instrukcja 𝐭𝐡𝐫𝐨𝐰x oznacza podniesienie wyjątku o nazwie x. Instrukcja I=𝐭𝐫𝐲I1𝐜𝐚𝐭𝐜𝐡excI2 wykonuje I1. Jeśli podczas wykonania I1 zostanie podniesiony wyjątej x, i exc=x albo exc=𝐚𝐧𝐲, to następuje przerwanie I1 i sterowanie zostaje przeniesione do I2 (następuje obsługa wyjątku). Jeśli zaś podczas wykonania I1 zostanie podniesiony wyjątej x, i excx i exc𝐚𝐧𝐲, to obsługa wyjątku przekazana jest do najbliższej instrukcji 𝐭𝐫𝐲 otaczającej I. Umawiamy się, że 𝐭𝐫𝐲I1𝐜𝐚𝐭𝐜𝐡excI2 otacza I1 i wszystkie instrukcje wewnątrz I1, ale nie otacza I2.


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.