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ń == | ||
Linia 31: | Linia 33: | ||
e_1 + e_2 \,\,|\,\, | e_1 + e_2 \,\,|\,\, | ||
e_1 * e_2 \,\,|\,\, | e_1 * e_2 \,\,|\,\, | ||
e_1 - e_2 | e_1 - e_2 \,\,|\,\, \ldots | ||
</math> | </math> | ||
Linia 46: | Linia 48: | ||
b, s \longrightarrow l, | b, s \longrightarrow l, | ||
</math> | </math> | ||
gdzie | gdzie | ||
<math>s \in State</math>, | <math>s \in State</math>, <math>n</math> jest liczbą całkowitą, | ||
<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 e w stanie s wylicza się do | Tranzycja taka oznacza, że wyrażenie <math>e</math> w stanie <math>s<math> wylicza się do | ||
wartości n, oraz że wyrażenie logiczne b w stanie s wylicza się do l. | wartości <math>n</math>, oraz że wyrażenie logiczne <math>b</math> w | ||
stanie <math>s</math> wylicza się do <math>l</math>. | |||
Zauważmy, że zakładamy tu, iż obliczenie wyrażenia nie zmienia | Zauważmy, że zakładamy tu, iż obliczenie wyrażenia nie zmienia | ||
stanu (nie ma efektów ubocznych). | stanu (nie ma efektów ubocznych). | ||
Linia 73: | Linia 76: | ||
<math> | <math> | ||
\frac{b, s \longrightarrow true \quad \quad | \frac{b, s \longrightarrow true \quad \quad | ||
I; \ | I; \mathbf{while}\, b \mathbf{do}\, I, s \longrightarrow s'} | ||
{\mathbf{while}\, b\, \mathbf{do}\, I, s \longrightarrow s'} | {\mathbf{while}\, b\, \mathbf{do}\, I, s \longrightarrow s'} | ||
</math> | </math> | ||
<math> | <math> | ||
\frac{b, s \longrightarrow false}{while b do I, s \longrightarrow s} | \frac{b, s \longrightarrow false} | ||
{\mathbf{while}\, b\, \mathbf{do}\, I, s \longrightarrow s} | |||
</math> | </math> | ||
Linia 89: | Linia 93: | ||
</math> | </math> | ||
Zauważmy, iż celowo nie odróżniamy liczby n \in Int od | Zauważmy, iż celowo nie odróżniamy liczby <math>n \in Int</math> od | ||
stałej reprezentującej tę liczbę, która może pojawić się | stałej reprezentującej tę liczbę, która może pojawić się | ||
w wyrażeniach zgodnie z przyjętą przez nas składnią. | w wyrażeniach zgodnie z przyjętą przez nas składnią. | ||
Czyli zakładamy, że Int jest podzbiorem zbioru wyrażeń. | Czyli zakładamy, że Int jest podzbiorem zbioru wyrażeń. | ||
W powyższej tranzycji, n po lewej stronie to stała reprezentująca | W powyższej tranzycji, <math>n</math> po lewej stronie to stała reprezentująca | ||
liczbę, która widnieje po prawej stronie. | liczbę, która widnieje po prawej stronie. | ||
Analogiczne tranzycje dla stałych boolowskich to: | Analogiczne tranzycje dla stałych boolowskich to: | ||
<math> | <math> | ||
true, s \longrightarrow true | true, s \longrightarrow true | ||
\quad \quad \quad | |||
false, s \longrightarrow false | false, s \longrightarrow false | ||
</math> | </math> | ||
Analogicznie, czynimy tu założenie, że Bool jest podbiorem | |||
wyrażen boolowskich. | wyrażen boolowskich. | ||
Operatory arytmetyczne definiujemy następująco: | Operatory arytmetyczne definiujemy następująco: | ||
<math> | <math> | ||
\frac{e_1, s \longrightarrow n_1 | \frac{e_1, s \longrightarrow n_1 \quad \quad | ||
e_2, s \longrightarrow n_2 | e_2, s \longrightarrow n_2 \quad \quad | ||
n = n_1 + n_2 } | n = n_1 + n_2 } | ||
{e_1 + e_2,s \longrightarrow n} | {e_1 + e_2,s \longrightarrow n} | ||
</math> | </math> | ||
Czyli aby obliczyć sumę e_1 + e_2 w stanie s, trzeba | |||
najpierw obliczyć e_1 i e_2 w stanie s, | Czyli aby obliczyć sumę <math>e_1 + e_2</math> w stanie | ||
a następnie dodać obliczone wartości. | <math>s</math>, trzeba najpierw obliczyć <math>e_1</math> i | ||
<math>e_2</math> w stanie <math>s</math>, a następnie dodać obliczone wartości. | |||
Zauważmy, że nie specyfikujemy kolejności, w jakiej mają się | Zauważmy, że nie specyfikujemy kolejności, w jakiej mają się | ||
obliczać e_1 i e_2. | obliczać <math>e_1</math> i <math>e_2</math>. | ||
I choć tutaj nie ma to żadnego znaczenia, w przyszłości | I choć tutaj nie ma to żadnego znaczenia, w przyszłości | ||
będzie inaczej, gdy jezyk będzie | 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 | Podobne reguły można napisać dla pozostałych operacji | ||
Linia 124: | Linia 133: | ||
<math> | <math> | ||
\frac{b_1, s \longrightarrow l_1 | \frac{b_1, s \longrightarrow l_1 \quad \quad | ||
\longrightarrow l_2 | b_2, s \longrightarrow l_2 \quad \quad l = l_1 \land l_2} | ||
{b_1 \land b_2, s \longrightarrow l} | {b_1 \land b_2, s \longrightarrow l} | ||
</math> | </math> | ||
Oczywiście jeśli b_1 oblicza się do false, wartość | |||
Oczywiście jeśli <math>b_1</math> oblicza się do false, wartość | |||
całego wyrażenia jest false niezależnie od wartości wyrażenia | całego wyrażenia jest false niezależnie od wartości wyrażenia | ||
b_2. | <math>b_2</math>. | ||
Czyli jeśli zaczniemy od obliczenia <math>b_1</math> i wynikiem będzie | |||
Czyli jeśli zaczniemy od obliczenia b_1 i wynikiem będzie | false, to nie ma wogóle potrzeby obliczania <math>b_2</math>. | ||
false, to nie ma wogóle potrzeby obliczania b_2. | Oto odpowiednie reguły (nazwijmy je regułami lewo-stronnymi, ponieważ | ||
Oto odpowiednie reguły (lewo- | rozpoczynamy od ''lewego'' koniunktu): | ||
<math> | <math> | ||
\frac{b_1, s \longrightarrow false} | \frac{b_1, s \longrightarrow false} | ||
{b_1 \land b_2,s \longrightarrow false} | {b_1 \land b_2, s \longrightarrow false} | ||
\frac{b_1, s \longrightarrow true | \frac{b_1, s \longrightarrow true \quad \quad | ||
b_2, s \longrightarrow l} | b_2, s \longrightarrow l} | ||
{b_1 | {b_1 \land b_2,s \longrightarrow l} | ||
</math> | </math> | ||
Linia 151: | Linia 161: | ||
Rozważmy też następującą kombinację obydwu semantyk | Rozważmy też następującą kombinację obydwu semantyk | ||
(reguły równoległe): | (reguły ''równoległe''): | ||
<math> | <math> | ||
\frac{b_1, s \longrightarrow false} | \frac{b_1, s \longrightarrow false} | ||
{b_1 \land b_2,s \longrightarrow false} | {b_1 \land b_2,s \longrightarrow false} | ||
\quad \quad \quad | |||
\frac{b_2, s \longrightarrow false} | \frac{b_2, s \longrightarrow false} | ||
{b_1 \land b_2,s \longrightarrow false} | {b_1 \land b_2,s \longrightarrow false} | ||
</math> | </math> | ||
Czyli jeśli którekolwiek z podwyrażeń daje wynik false, | Czyli jeśli którekolwiek z podwyrażeń daje wynik false, | ||
to taki wynik zyskuje całe wyrażenie. | to taki wynik zyskuje całe wyrażenie. | ||
Dodatkowo potrzebujemy jeszcze reguły: | Dodatkowo potrzebujemy jeszcze reguły: | ||
<math> | <math> | ||
\frac{b_1, s \longrightarrow true | \frac{b_1, s \longrightarrow true \quad \quad | ||
b_2, s \longrightarrow true} | b_2, s \longrightarrow true} | ||
{b_1 \land b_2,s \longrightarrow true} | {b_1 \land b_2,s \longrightarrow true} | ||
</math> | </math> | ||
Zauważmy, że powyższych reguł nie da sie zaimplementować | Zauważmy, że powyższych reguł nie da sie zaimplementować | ||
sekwencyjnie: nie wiadomo czy najpierw obliczać | sekwencyjnie: nie wiadomo czy najpierw obliczać | ||
b_1 czy b_2. | <math>b_1</math> czy <math>b_2</math>. | ||
Reguły te odpowiadają raczej strategii ,,równoległej'': | Reguły te odpowiadają raczej strategii ,,równoległej'': | ||
obliczaj ,,jednocześnie'' b_1 i b_2 | 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. | z wynikiem true. | ||
W naszym prostym języku wszystkie | W naszym prostym języku wszystkie cztery warianty | ||
są równoważne. Różnice pomiędzy nimi zobaczymy jednak już w | są równoważne. Różnice pomiędzy nimi zobaczymy jednak już w | ||
następnym zadaniu, w którym pojawi się prosta | następnym zadaniu, w którym pojawi się prosta | ||
Linia 183: | Linia 197: | ||
Reguły dla pozostałych spójników logicznych oraz dla | Reguły dla pozostałych spójników logicznych oraz dla | ||
negacji pozostawiamy jako ćwiczenie. | negacji pozostawiamy jako ćwiczenie. | ||
A teraz małe kroki. | A teraz małe kroki. | ||
Linia 196: | Linia 209: | ||
b, s \longrightarrow b', s | b, s \longrightarrow b', s | ||
</math> | </math> | ||
gdzie s \in State. | gdzie <math>s \in State</math>. | ||
Przyjmijmy na razie takie same konfiguracje i konfiguracje końcowe jak dla semantyki | Przyjmijmy na razie takie same konfiguracje i konfiguracje końcowe | ||
naturalnej. | jak dla semantyki naturalnej. | ||
Zacznijmy od wyrażeń boolowskich. | Zacznijmy od wyrażeń boolowskich. | ||
Linia 204: | Linia 218: | ||
<math> | <math> | ||
true, s \Longrightarrow true | true, s \Longrightarrow true | ||
\quad \quad | |||
false, s \Longrightarrow false | false, s \Longrightarrow false | ||
</math> | </math> | ||
Przejdźmy do spójników logicznych, powiedzmy b_1 \land b_2. | Przejdźmy do spójników logicznych, powiedzmy <math>b_1 \land b_2</math>. | ||
Ponieważ opisujemy teraz pojedyncze (małe) kroki składające się na | Ponieważ opisujemy teraz pojedyncze (małe) kroki składające się na | ||
wykonanie programu, musimy podać w jakiej kolejności będą się | wykonanie programu, musimy podać w jakiej kolejności będą się | ||
obliczać <math>b_1</math> i <math>b_2</math>. Zacznijmy od strategii lewostronnej: | |||
<math> | <math> | ||
\frac{b_1, s \Longrightarrow b'_1, s} | \frac{b_1, s \Longrightarrow b'_1, s} | ||
{b_1 \land b_2, s \Longrightarrow b'_1 \land b_2, s} | {b_1 \land b_2, s \Longrightarrow b'_1 \land b_2, s} | ||
\quad | \quad \quad | ||
\frac{b_2, s \Longrightarrow b'_2, s} | \frac{b_2, s \Longrightarrow b'_2, s} | ||
{l_1 \land b_2, s \Longrightarrow l_1 \land b_2, s} | {l_1 \land b_2, s \Longrightarrow l_1 \land b_2, s} | ||
\quad | \quad \quad | ||
l_1 \land l_2 \Longrightarrow l, | l_1 \land l_2 \Longrightarrow l, | ||
\mbox{ o ile } l = l_1 \land l_2 | \mbox{ o ile } l = l_1 \land l_2 | ||
Linia 225: | Linia 239: | ||
Podobnie jak poprzednio, możemy zaniechać obliczania | Podobnie jak poprzednio, możemy zaniechać obliczania | ||
b_2 jeśli b_1 oblicza się do false. | <math>b_2</math> jeśli <math>b_1</math> oblicza się do false. | ||
Oto odpowiednio zmodyfikowane reguły: | |||
<math> | <math> | ||
\frac{b_1, s \Longrightarrow b'_1, s} | \frac{b_1, s \Longrightarrow b'_1, s} | ||
{b_1 \land b_2, s \Longrightarrow b'_1 \land b_2, s} | {b_1 \land b_2, s \Longrightarrow b'_1 \land b_2, s} | ||
\quad \quad | |||
false \land b_2, s \Longrightarrow false | false \land b_2, s \Longrightarrow false | ||
\quad \quad | |||
true \land b_2, s \Longrightarrow b_2,s | true \land b_2, s \Longrightarrow b_2, s | ||
</math> | </math> | ||
Linia 241: | Linia 256: | ||
\frac{b_2, s \Longrightarrow b'_2, s} | \frac{b_2, s \Longrightarrow b'_2, s} | ||
{b_1 \land b_2, s \Longrightarrow b_1 \land b'_2, s} | {b_1 \land b_2, s \Longrightarrow b_1 \land b'_2, s} | ||
\quad \quad | |||
b_1 \land false, s \Longrightarrow false | b_1 \land false, s \Longrightarrow false | ||
\quad \quad | |||
b_1 \land true, s \Longrightarrow b_1, s | b_1 \land true, s \Longrightarrow b_1, s | ||
</math> | </math> | ||
Reguły równoległe otrzymujemy jako sumę reguł lewo- i | Reguły ''równoległe'' otrzymujemy jako sumę reguł lewo- i | ||
prawostronnych (w sumie 6 reguł). | prawostronnych (w sumie 6 reguł). Zauważmy, że obliczanie | ||
wyrażeń <math>b_1</math> i <math>b_2</math> odbywa się teraz | |||
w twz. ''przeplocie'': Pojedynczy krok polega na wykonaniu | |||
jednego kroku obliczenia <math>b_1</math> albo jednego kroku | |||
obliczenia <math>b_2</math>. | |||
Zwróćmy też uwagę, że po raz pierwszy nasze tranzycje nie posiadają | |||
własności ''determinizmu'': wyrażenie <math>b_1 \land b_2</math> | |||
może wyewoluować w pojedyńczym kroku albo do | |||
<math>b'_1 \land b_2</math> albo do <math>b_1 \land b'_2</math>. | |||
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: | Oto reguła dla negacji: | ||
<math> | <math> | ||
\neg true, s \Longrightarrow false, s | \neg true, s \Longrightarrow false, s | ||
\quad \quad \quad | |||
\neg false, s \Longrightarrow true, s | \neg false, s \Longrightarrow true, s | ||
\quad \quad \quad | |||
\frac{b, s \Longrightarrow b', s} | |||
{\neg b, s \Longrightarrow \neg b', s} | |||
</math> | </math> | ||
Reguły dla e_1 \leq e_2 są następujące: | Reguły dla <math>e_1 \leq e_2</math> są następujące: | ||
<math> | <math> | ||
Linia 266: | Linia 295: | ||
{e_1 \leq e_2, s \Longrightarrow e_1 \leq e'_2, s} | {e_1 \leq e_2, s \Longrightarrow e_1 \leq e'_2, s} | ||
n_1 \leq n_2, s \Longrightarrow true, s | n_1 \leq n_2, s \Longrightarrow true, s \quad \mbox{ o ile } | ||
n_1 \leq n_2 | n_1 \leq n_2 | ||
n_1 \leq n_2, s \Longrightarrow false, s | n_1 \leq n_2, s \Longrightarrow false, s \quad \mbox{ o ile } | ||
n_1 > n_2 | n_1 > n_2 | ||
</math> | </math> | ||
Linia 282: | Linia 311: | ||
<math> | <math> | ||
\frac{b, s \Longrightarrow b', s} | \frac{b, s \Longrightarrow b', s} | ||
{if b then I_1 else I_2, s \Longrightarrow | {\mathbf{if}\, b\, \mathbf{then}\, I_1\, \mathbf{else}\, I_2, s \Longrightarrow | ||
if b' then I_1 else I_2, s} | \mathbf{if}\, b'\, \mathbf{then}\, I_1\, \mathbf{else}\, I_2, s} | ||
\quad \quad | |||
\frac{b, s \Longrightarrow b', s} | \frac{b, s \Longrightarrow b', s} | ||
{while b do I, s \Longrightarrow while b' do I, s} | {\mathbf{while}\, b\, \mathbf{do}\, I, s \Longrightarrow \mathbf{while}\, b'\, \mathbf{do}\, I, s} | ||
</math> | </math> | ||
Linia 293: | Linia 322: | ||
<math> | <math> | ||
if true then I_1 else I_2, s \Longrightarrow | \mathbf{if}\, true\, \mathbf{then}\, I_1\, \mathbf{else}\, I_2, s \Longrightarrow I_1, s | ||
I_1, s | \quad \quad \quad | ||
\mathbf{if}\, false\, \mathbf{then}\, I_1\, \mathbf{else}\, I_2, s \Longrightarrow I_2, s | |||
if false then I_1 else I_2, s \Longrightarrow | |||
I_2, s | |||
</math> | </math> | ||
Linia 303: | Linia 330: | ||
<math> | <math> | ||
while true do I, s \Longrightarrow I; while ? do I, s | \mathbf{while}\, true\, \mathbf{do}\, I, s \Longrightarrow | ||
I;\, \mathbf{while}\, ?\, \mathbf{do}\, I, s | |||
</math> | </math> | ||
ale nie wiemy już, jaki był dozór pętli (widzimy tylko wynik | ale nie wiemy już, jaki był dozór pętli (widzimy tylko wynik | ||
obliczenia tego dozoru w stanie s, true). | obliczenia tego dozoru w stanie s, true). | ||
Możemy odwołać się | Możemy odwołać się do tranzycji dużych kroków: | ||
<math> | <math> | ||
\frac{b, s \longrightarrow true} | \frac{b, s \longrightarrow true} | ||
{while b do I, s \Longrightarrow I; while b do I, s} | {\mathbf{while}\, b\, \mathbf{do}\' I, s \Longrightarrow | ||
I;\, \mathbf{while}\, b\, \mathbf{do}\, I, s} | |||
\quad \quad \quad | |||
\frac{b, s \longrightarrow false} | \frac{b, s \longrightarrow false} | ||
{while b do I, s \Longrightarrow s} | {\mathbf{while}\, b\, \mathbf{do}\, I, s \Longrightarrow s} | ||
</math> | </math> | ||
Takie rozwiązanie nie jest zatem | Takie rozwiązanie nie jest zatem ''czystą'' semantyką | ||
małych kroków. | małych kroków. | ||
Istnieją inne możliwe rozwiązania, w stylu małych kroków, | Istnieją inne możliwe rozwiązania, w stylu małych kroków, | ||
Linia 334: | Linia 363: | ||
{n + e_2, s \Longrightarrow n + e'_2, s} | {n + e_2, s \Longrightarrow n + e'_2, s} | ||
n_1 + n_2, s \Longrightarrow n, s | n_1 + n_2, s \Longrightarrow n, s \quad \mbox{ o ile } | ||
n = n_1 + n_2 | n = n_1 + n_2 | ||
</math> | </math> | ||
Niektóre konfiguracje nie były wogóle przez nas używane. Które? | |||
==== Zadanie: ==== | ==== Zadanie: ==== | ||
Linia 345: | Linia 375: | ||
e_1 / e_2 | e_1 / e_2 | ||
</math> | </math> | ||
i rozszerz semantyki z poprzedniego zadania | i rozszerz semantyki z poprzedniego zadania. | ||
Dzielenie przez zero jest błądem i kończy natychmiast wykonanie | |||
Oprócz stanu wynikiem programu powinna byc informacja | programu. | ||
o błędzie. | Oprócz stanu wynikiem programu powinna byc informacja o błędzie, | ||
jeśli błąd wystąpił. | |||
==== Rozwiązanie: ==== | ==== 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ą | |||
<math>Blad</math>. | |||
Reguła opisująca powstanie błędu może wyglądać np. tak | |||
w semantyce naturalnej: | |||
<math> | |||
\frac{e_2, s \longrightarrow 0} | |||
{e_1 / e_2, s \longrightarrow Blad} | |||
</math> | |||
a tak w semantyce małych kroków: | |||
<math> | |||
e_1 / 0, s \Longrightarrow Blad | |||
</math> | |||
Pomijamy tutaj reguły dla przypadku, gdy <math>e_2</math> | |||
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: | |||
<math> | |||
e_1 / 0, s \Longrightarrow Blad, s | |||
</math> | |||
I zamiast pojedyńczej konfiguracji końcowej <math>Blad</math>, potrzebowalibyśmy | |||
oczywiście całego zbioru <math>\{Blad\} \times State</math>. | |||
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: | |||
<math> | |||
\frac{e_1, s \dkrok Blad} | |||
{e_1 \leq e_2, s \dkrok Blad} | |||
\quad \quad \quad | |||
\frac{e_2, s \dkrok Blad} | |||
{e_1 \leq e_2, s \dkrok Blad} | |||
</math> | |||
Następnie, błąd w wyrażeniu powinien wstrzymać normalne wykonanie | |||
instrukcji: | |||
<math> | |||
\frac{b \dkrok Blad} | |||
{\mathbf{if}\, b\, \mathbf{then}\, I_1\, \mathbf{else}\, I_2, s \dkrok Blad} | |||
\quad \quad \quad | |||
\frac{b \dkrok Blad} | |||
{\mathbf{while}\, b\, \mathbf{do}\, I, s \dkrok Blad} | |||
</math> | |||
I wreszcie błąd powinien propagować się do kolejnych instrukcji: | |||
<math> | |||
\frac{I_1, s \dkrok Blad} | |||
{I_1;\, I_2, s \dkrok Blad} | |||
\quad \quad \quad | |||
\frac{I_1, s \dkrok Blad} | |||
{\mathbf{if}\, b\, \mathbf{then}\, I_1\, \mathbf{else}\, I_2, s \dkrok Blad} | |||
\quad \quad \quad | |||
\frac{I_2, s \dkrok Blad} | |||
{\mathbf{if}\, b\, \mathbf{then}\, I_1\, \mathbf{else}\, I_2, s \dkrok Blad} | |||
\quad \quad \quad | |||
\frac{I, s \dkrok Blad} | |||
{\mathbf{while}\, b\, \mathbf{do}\, I, s \dkrok Blad} | |||
</math> | |||
I tak dalej. | |||
Pytanie dla Czytelnika: jak napisać tę semantykę w podejściu mało-krokowym? | |||
. | (Powyżej traktowaliśmy tranzycję | ||
<math> I, s \dkrok Blad </math> 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'', | |||
<math> I, s \mkrok Blad </math>?) |
Wersja z 07:02, 24 lip 2006
Ćwiczenia 1: Semantyka operacyjna wyrażeń
Zadanie:
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 Parser nie mógł rozpoznać (błąd składni): {\displaystyle s<math> wylicza się do wartości <math>n} , 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:
Parser nie mógł rozpoznać (błąd składni): {\displaystyle \frac{b, s \longrightarrow true} {\mathbf{while}\, b\, \mathbf{do}\' I, s \Longrightarrow I;\, \mathbf{while}\, b\, \mathbf{do}\, I, s} \quad \quad \quad \frac{b, s \longrightarrow false} {\mathbf{while}\, b\, \mathbf{do}\, I, s \Longrightarrow s} }
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:
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:
Parser nie mógł rozpoznać (nieznana funkcja „\dkrok”): {\displaystyle \frac{e_1, s \dkrok Blad} {e_1 \leq e_2, s \dkrok Blad} \quad \quad \quad \frac{e_2, s \dkrok Blad} {e_1 \leq e_2, s \dkrok Blad} }
Następnie, błąd w wyrażeniu powinien wstrzymać normalne wykonanie instrukcji:
Parser nie mógł rozpoznać (nieznana funkcja „\dkrok”): {\displaystyle \frac{b \dkrok Blad} {\mathbf{if}\, b\, \mathbf{then}\, I_1\, \mathbf{else}\, I_2, s \dkrok Blad} \quad \quad \quad \frac{b \dkrok Blad} {\mathbf{while}\, b\, \mathbf{do}\, I, s \dkrok Blad} }
I wreszcie błąd powinien propagować się do kolejnych instrukcji:
Parser nie mógł rozpoznać (nieznana funkcja „\dkrok”): {\displaystyle \frac{I_1, s \dkrok Blad} {I_1;\, I_2, s \dkrok Blad} \quad \quad \quad \frac{I_1, s \dkrok Blad} {\mathbf{if}\, b\, \mathbf{then}\, I_1\, \mathbf{else}\, I_2, s \dkrok Blad} \quad \quad \quad \frac{I_2, s \dkrok Blad} {\mathbf{if}\, b\, \mathbf{then}\, I_1\, \mathbf{else}\, I_2, s \dkrok Blad} \quad \quad \quad \frac{I, s \dkrok Blad} {\mathbf{while}\, b\, \mathbf{do}\, I, s \dkrok Blad} }
I tak dalej.
Pytanie dla Czytelnika: jak napisać tę semantykę w podejściu mało-krokowym?
(Powyżej traktowaliśmy tranzycję Parser nie mógł rozpoznać (nieznana funkcja „\dkrok”): {\displaystyle I, s \dkrok Blad } 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, Parser nie mógł rozpoznać (nieznana funkcja „\mkrok”): {\displaystyle I, s \mkrok Blad } ?)