Semantyka i weryfikacja programów/Ćwiczenia 3: Różnice pomiędzy wersjami
Nie podano opisu zmian |
Nie podano opisu zmian |
||
Linia 1: | Linia 1: | ||
== Zawartość == | |||
Kończymy semantykę małych kroków i rozpoczynamy semantykę naturalną (duże kroki). | |||
Uzupełnimy semantykę naturalną języka Tiny o semantykę naturalną dla | |||
wyrażeń boolowskich i arytmetycznych i semantykę dla | |||
błędów wykonania. | |||
Wreszcie dodamy instrukcję pętli <math> \mathbf{loop}\, </math> i instrukcje | |||
niestrukturalnego zakończenia pętli (instrukcje <math> \mathbf{exit}</math> i <math> \mathbf{continue} </math>). | |||
== Rozszerzenia semantyki języka Tiny == | |||
==== Zadanie 1 ==== | |||
Zdefiniuj znaczenie wyrażeń boolowskich i arytmetycznych | |||
w języku Tiny, w stylu dużych kroków (semantyka naturalna). | |||
==== Rozwiązanie ==== | |||
Przypomnijmy składnię wyrażeń boolowskich i arytmetycznych | |||
jezyka Tiny: | |||
<math> | |||
b \, ::= \,\, | |||
l \,\,|\,\, | |||
e_1 \leq e_2 \,\,|\,\, | |||
\neg b \,\,|\,\, | |||
b_1 \land b_2 \,\,|\,\, \ldots | |||
</math> | |||
<math> | |||
l \, ::= \,\, | |||
\mathbf{true} \,\,|\,\, | |||
\mathbf{true} | |||
</math> | |||
<math> | |||
e \, ::= \,\, | |||
n \,\,|\,\, | |||
x \,\,|\,\, | |||
e_1 + e_2 \,\,|\,\, \ldots | |||
</math> | |||
<math> | |||
n \, ::= \,\, 0 \,\,|\,\, 1 \,\,|\,\, \ldots | |||
</math> | |||
<math> | |||
x \, ::= \,\, \ldots \, (identyfikatory) \, \ldots | |||
</math> | |||
Chcemy, aby tranzycje wyrażen wyglądały następująco: | |||
<math> | |||
e, s \longrightarrow n | |||
\quad \quad \quad | |||
b, s \longrightarrow l, | |||
</math> | |||
gdzie <math> s \in \mathbf{State} </math>, <math> n \in </math> jest liczbą całkowitą, | |||
<math> n \in \mathbf{Num} </math>, a <math> l \in \mathbf{Bool} = \{ \mathbf{true}, \mathbf{true} \} </math>. | |||
Tranzycja taka oznacza, że wyrażenie <math> e </math> w stanie <math> s </math> wylicza się do | |||
wartości <math> n </math> oraz analogicznie, wyrażenie logiczne <math> b </math> w | |||
stanie <math> s </math> wylicza się do <math> l </math>. | |||
Zatem zbiór konfiguracji dla semantyki całego języka Tiny to znów | |||
<math> | |||
( (\mathbf{Exp} \, \cup \, \mathbf{BExp} \, \cup \, \mathbf{Stmt}) \times \mathbf{State} ) \, \bigcup \, | |||
\mathbf{Num} \, \cup \, \mathbf{Bool} \, \cup \, \mathbf{State} | |||
</math> | |||
a konfiguracje końcowe to <math> \mathbf{State} </math> tak jak poprzednio. | |||
Tranzycje dla instrukcji pozostają zasadniczo bez zmian, z tym, | |||
że odwołania do funkcji semantycznych dla wyrażen zastępujemy | |||
przez odpowiednie tranzycje. | |||
Np. dla instrukcji pętli będziemy mieć następujące reguły: | |||
<math> | |||
\frac{b, s \,\longrightarrow\, \mathbf{true} \quad \quad I; \mathbf{while}\, b \,\mathbf{do}\, I, s \,\longrightarrow\, s'} | |||
{\mathbf{while}\, b \,\mathbf{do}\, I, s \,\longrightarrow\, s'} | |||
</math> | |||
<math> | |||
\frac{b, s \,\longrightarrow\, \mathbf{true}} | |||
{\mathbf{while}\, b \,\mathbf{do}\, I, s \,\longrightarrow\, s} | |||
</math> | |||
Podobnie dla instrukcji warunkowej. | |||
Teraz zajmiemy się tranzycjami dla wyrażeń. | |||
Zacznijmy od stalych arytmetycznych i boolowskich: | |||
<math> | |||
n, s \,\longrightarrow\, n | |||
\quad \quad | |||
l, s \,\longrightarrow\, l. | |||
</math> | |||
Operatory arytmetyczne definiujemy następująco: | |||
<math> | |||
\frac{e_1, s \,\longrightarrow\, n_1 \quad \quad | |||
e_2, s \,\longrightarrow\, n_2 \quad \quad | |||
n = n_1 + n_2 } | |||
{e_1 + e_2,s \,\longrightarrow\, n} | |||
</math> | |||
Czyli aby obliczyć sumę <math> e_1 + e_2 </math> w stanie | |||
<math> s </math>, trzeba najpierw obliczyć <math> e_1 </math> i | |||
<math> e_2 </math> w tym stanie, a następnie dodać obliczone wartości. | |||
Zauważmy, że nie specyfikujemy kolejności, w jakiej mają się | |||
obliczać obydwa podwyrażenia. | |||
I choć tutaj nie ma to żadnego znaczenia, w przyszłości | |||
będzie inaczej, gdy jezyk będzie umożliwiał efekty uboczne | |||
podczas obliczania wyrażeń. | |||
Podobne reguły można napisać dla pozostałych operacji | |||
arytmnetycznych, oraz dla spójników logicznych: | |||
<math> | |||
\frac{b_1, s \,\longrightarrow\, l_1 \quad \quad | |||
b_2, s \,\longrightarrow\, l_2 \quad \quad | |||
l = l_1 \land l_2} | |||
{b_1 \land b_2, s \,\longrightarrow\, l} | |||
</math> | |||
Oto inny wariant, reguły lewo-stronne (ponieważ rozpoczynamy od ''lewego'' koniunktu): | |||
<math> | |||
\frac{b_1, s \,\longrightarrow\, \mathbf{true}} | |||
{b_1 \land b_2, s \,\longrightarrow\, \mathbf{true}} | |||
\frac{b_1, s \,\longrightarrow\, \mathbf{true} \quad \quad | |||
b_2, s \,\longrightarrow\, l} | |||
{b_1 \land b_2, s \,\longrightarrow\, l} | |||
</math> | |||
Inny wariant to wariant prawo-stronny ( | |||
najpierw <math> b_2 </math>, potem <math> b_1 </math>). | |||
Wreszcie rozważmy kombinację obydwu semantyk (reguły ''równoległe''): | |||
<math> | |||
\frac{b_1, s \,\longrightarrow\, \mathbf{true}} | |||
{b_1 \land b_2, s \,\longrightarrow\, \mathbf{true}} | |||
\quad \quad \quad | |||
\frac{b_2, s \,\longrightarrow\, \mathbf{true}} | |||
{b_1 \land b_2, s \,\longrightarrow\, \mathbf{true}} | |||
</math> | |||
Czyli jeśli którekolwiek z podwyrażeń daje wynik <math> \mathbf{true} </math>, | |||
to taki wynik zyskuje całe wyrażenie. | |||
Dodatkowo potrzebujemy jeszcze reguły: | |||
<math> | |||
\frac{b_1, s \,\longrightarrow\, \mathbf{true} \quad \quad | |||
b_2, s \,\longrightarrow\, \mathbf{true}} | |||
{b_1 \land b_2,s \,\longrightarrow\, \mathbf{true}} | |||
</math> | |||
W naszym prostym języku wszystkie cztery warianty | |||
są równoważne. | |||
Reguły dla pozostałych spójników logicznych, dla | |||
negacji oraz dla instrukcji przypisania pozostawiamy jako proste ćwiczenie. | |||
==== Zadanie 2 ==== | |||
Rozważ dodatkowo operację dzielenia: | |||
<math> | |||
e \, ::= \,\, \ldots \,\,|\,\, | |||
e_1 / e_2 | |||
</math> | |||
i rozszerz semantykę 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. | |||
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> \mathtt{Blad} </math>. | |||
Reguła opisująca powstanie błędu może wyglądać np. tak: | |||
<math> | |||
\frac{e_2, s \longrightarrow 0} | |||
{e_1 / e_2, 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> \mathtt{Blad} </math> potrzebowalibyśmy | |||
oczywiście całego zbioru <math> \{ \mathtt{Blad} \} \times \mathbf{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. | |||
<math> | |||
\frac{e_1, s \,\longrightarrow\, \mathtt{Blad}} | |||
{e_1 \leq e_2, s \,\longrightarrow\, \mathtt{Blad}} | |||
\quad \quad \quad | |||
\frac{e_2, s \,\longrightarrow\, \mathtt{Blad}} | |||
{e_1 \leq e_2, s \,\longrightarrow\, \mathtt{Blad}} | |||
</math> | |||
Następnie, błąd w wyrażeniu powinien wstrzymać normalne wykonanie instrukcji: | |||
<math> | |||
\frac{b \,\longrightarrow\, \mathtt{Blad}} | |||
{\mathbf{if}\, b \,\mathbf{then}\, I_1 \,\mathbf{else}\, I_2, s \,\longrightarrow\, \mathtt{Blad}} | |||
</math> | |||
<math> | |||
\frac{b \,\longrightarrow\, \mathtt{Blad}} | |||
{\mathbf{while}\, b \,\mathbf{do}\, I, s \,\longrightarrow\, \mathtt{Blad}} | |||
</math> | |||
<math> | |||
\frac{e, s \,\longrightarrow\, \mathtt{Blad}} | |||
{x := e, s \,\longrightarrow\, \mathtt{Blad}} | |||
</math> | |||
I wreszcie błąd powinien propagować się do kolejnych instrukcji: | |||
<math> | |||
\frac{I_1, s \,\longrightarrow\, \mathtt{Blad}} | |||
{I_1;\, I_2, s \,\longrightarrow\, \mathtt{Blad}} | |||
</math> | |||
<math> | |||
\frac{b, s \,\longrightarrow\, \mathbf{true} \quad \quad I_1, s \,\longrightarrow\, \mathtt{Blad}} | |||
{\mathbf{if}\, b \,\mathbf{then}\, I_1 \,\mathbf{else}\, I_2, s \,\longrightarrow\, \mathtt{Blad}} | |||
\quad \quad \quad | |||
\frac{b, s \,\longrightarrow\, \mathbf{true} \quad \quad I_2, s \,\longrightarrow\, \mathtt{Blad}} | |||
{\mathbf{if}\, b \,\mathbf{then}\, I_1 \,\mathbf{else}\, I_2, s \,\longrightarrow\, \mathtt{Blad}} | |||
</math> | |||
<math> | |||
\frac{b, s \,\longrightarrow\, \mathbf{true} \quad \quad I, s \,\longrightarrow\, \mathtt{Blad}} | |||
{\mathbf{while}\, b \,\mathbf{do}\, I, s \,\longrightarrow\, \mathtt{Blad}} | |||
\quad \quad | |||
\frac{b, s \,\longrightarrow\, \mathbf{true} \quad \quad I, s \,\longrightarrow\, s' \quad \quad | |||
\mathbf{while}\, b \,\mathbf{do}\, I, s' \,\longrightarrow\, \mathtt{Blad}} | |||
{\mathbf{while}\, b \,\mathbf{do}\, I, s \,\longrightarrow\, \mathtt{Blad}} | |||
</math> | |||
==== Zadanie 3 ==== | |||
Rozszerz język Tiny o następującą instrukcję pętli: | |||
<math> | |||
I \, ::= \,\, | |||
\mathbf{loop}\, I \,\,|\,\, | |||
\mathbf{exit} \,\,|\,\, | |||
\mathbf{continue} | |||
</math> | |||
<math> \mathbf{loop}\, I </math> to instrukcja pętli, <math> I </math> stanowi instrukcję wewnętrzną. | |||
Instrukcja <math> \mathbf{exit} </math> wychodzi z nabliższej pętli <math> \mathbf{loop}\, </math> | |||
i kontynuuje wykonanie programu od pierwszej instrukcji za tą pętlą. | |||
Instrukcje <math> \mathbf{continue} </math> powraca na początek instrukcji wewnętrznej | |||
najbliższej pętli. | |||
Pozważ zarówno semantykę naturalną jak i semantykę małych kroków. | |||
==== Rozwiązanie 1 (semantyka naturalna) ==== | |||
Dodamy do reguł semantyki naturalnej dla języka Tiny | |||
kilka nowych reguł opisujących nową konstrukcję języka | |||
i jej ''interakcję'' z pozostałymi konstrukcjami. | |||
Pomysł polega na dodaniu specjalnych konfiguracji zawierających | |||
informację o tym, że została wykonana instrukcja <math> \mathbf{exit} </math> | |||
lub <math> \mathbf{continue} </math>. Oto odpowiednie reguły: | |||
<math> | |||
\mathbf{exit}, s \,\longrightarrow\, s, \mbox{było-}\mathbf{exit} | |||
\quad \quad | |||
\mathbf{continue}, s \,\longrightarrow\, s, \mbox{było-}\mathbf{continue}. | |||
</math> | |||
Czyli instrukcje <math> \mathbf{exit} </math> i <math> \mathbf{continue} </math> nie modyfikują | |||
stanu <math> s </math>, ale zostawiają po sobie \''ślad". | |||
Zauważmy, że taki ślad zostaje pozostawiony tylko wtedy, jesli | |||
nie było dotychczas innego śladu, to znaczy jeśli <math> \mathbf{exit} </math> | |||
(lub <math> \mathbf{continue} </math>) zostało wykonane w zwykłym stanie <math> s </math>. | |||
Oczywiście poszerzamy odpowiednio zbiór konfiguracji o: | |||
</math>[ | |||
\mathbf{State} \times \{ \mbox{było-}\mathbf{exit}, \mbox{było-}\mathbf{continue} \} | |||
</math> | |||
, | |||
'''Pytanie''': jakie powinno być zachowanie <math> \mathbf{exit} </math> i | |||
<math> \mathbf{continue} </math> w konfiguracji <math> s, \mbox{było-}\mathbf{exit} </math> | |||
lub <math> s, \mbox{było-}\mathbf{continue} </math>? | |||
Czy instrukcje <math> \mathbf{exit} </math> i <math> \mathbf{continue} </math> | |||
będą faktycznie wykonywane w takich konfiguracjach? | |||
Zapiszmy teraz jak inne instrukcje korzystają z dodatkowej informacji | |||
(śladu) zawartej w konfiguracjach. | |||
Oczywiście beneficjentem tej informacji jest instrukcji <math> \mathbf{loop}\, </math>: | |||
<math> | |||
\frac{I, s \,\longrightarrow\, s', \mbox{było-}\mathbf{exit}} | |||
{\mathbf{loop}\, I, s \,\longrightarrow\, s'} | |||
\quad \quad | |||
\frac{I, s \,\longrightarrow\, s', \mbox{było-}\mathbf{continue} \quad \quad \mathbf{loop}\, I, s' \,\longrightarrow\, s''} | |||
{\mathbf{loop}\, I, s \,\longrightarrow\, s''} | |||
</math> | |||
Czyli w zależności od tego, jaki ślad został zarejestrowany | |||
podczas wykonania <math> I </math>, albo kończymy wykonanie pętli <math> \mathbf{loop}\, </math>, | |||
albo rozpoczynamy kolejną iterację. | |||
Zauważmy, że stan <math> s' </math> może być różny od <math> s </math>, ponieważ | |||
zanim wykonała się ktoraś z instrukcji <math> \mathbf{exit} </math> lub | |||
<math> \mathbf{continue} </math>, mogły zostać zmienione wartości niektórych zmiennych. | |||
Oczywiście, jeśli instrukcja wewnętrzna <math> I </math> zakończyła się | |||
''normalnie'', kontynuujemy wykonanie pętli podobnie jak w przypadku | |||
wywołania <math> \mathbf{continue} </math>: | |||
<math> | |||
\frac{I, s \,\longrightarrow\, s' \quad \quad \mathbf{loop}\, I, s' \,\longrightarrow\, s''} | |||
{\mathbf{loop}\, I, s \,\longrightarrow\, s''} | |||
</math> | |||
'''Pytanie:''' czy potrzebujemy dodatkowo reguł postaci: | |||
<math> | |||
\frac{I, s \,\longrightarrow\, s' \quad \quad \mathbf{loop}\, I, s' \,\longrightarrow\, s'', \mbox{było-}\mathbf{exit}} | |||
{\mathbf{loop}\, I, s \,\longrightarrow\, s'', \mbox{było-}\mathbf{exit}} | |||
\quad \quad | |||
\frac{I, s \,\longrightarrow\, s', \mbox{było-}\mathbf{continue} \quad \quad \mathbf{loop}\, I, s' \,\longrightarrow\, s'', \mbox{było-}\mathbf{exit}} | |||
{\mathbf{loop}\, I, s \,\longrightarrow\, s'', \mbox{było-}\mathbf{exit}} | |||
</math> | |||
Okazuje się że nie, ponieważ ślad powinien zostać zawsze | |||
''wymazany'' przez pętlę <math> \mathbf{loop}\, </math>. | |||
Poza tym reguły te wykraczałyby poza klasyczna podejście semantyki | |||
naturalnej, w którym zwykle bezpośrednio definiujemy | |||
tranzycje postaci <math> I, s \,\longrightarrow\, s' </math>. | |||
Teraz musimy określić zachowanie wszystkich instrukcji w sytuacji, | |||
gdy bieżąca konfiguracja zawiera już ślad. Zasadniczo, powinniśmy | |||
zaniechać wykonania instrukcji (w przypadku pętli, powinniśmy | |||
zaniechać dalszego iterowania tej pętli). | |||
Oto odpowiednie reguły dla <math> \mbox{było-}\mathbf{exit} </math>: | |||
<math> | |||
\frac{I_1, s \,\longrightarrow\, s', \mbox{było-}\mathbf{exit}} | |||
{I_1;\, I_2, s \,\longrightarrow\, s', \mbox{było-}\mathbf{exit}} | |||
\quad \quad | |||
\frac{I_1, s \,\longrightarrow\, s' \quad \quad I_2, s' \,\longrightarrow\, s'', \mbox{było-}\mathbf{exit}} | |||
{I_1;\, I_2, s \,\longrightarrow\, s'', \mbox{było-}\mathbf{exit}} | |||
</math> | |||
<math> | |||
\frac{b, s \,\longrightarrow\, \mathbf{true} \quad \quad I_1, s \,\longrightarrow\, s', \mbox{było-}\mathbf{exit}} | |||
{\mathbf{if}\, b \,\mathbf{then}\, I_1 \,\mathbf{else}\, I_2, s \,\longrightarrow\, s', \mbox{było-}\mathbf{exit}} | |||
\quad \quad | |||
\frac{b, s \,\longrightarrow\, \mathbf{true} \quad \quad I_2, s \,\longrightarrow\, s', \mbox{było-}\mathbf{exit}} | |||
{\mathbf{if}\, b \,\mathbf{then}\, I_1 \,\mathbf{else}\, I_2, s \,\longrightarrow\, s', \mbox{było-}\mathbf{exit}} | |||
</math> | |||
<math> | |||
\frac{b, s \,\longrightarrow\, \mathbf{true} \quad \quad I, s \,\longrightarrow\, s', \mbox{było-}\mathbf{exit}} | |||
{\mathbf{while}\, b \,\mathbf{do}\, I, s \,\longrightarrow\, s', \mbox{było-}\mathbf{exit}} | |||
\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{exit}} | |||
{\mathbf{while}\, b \,\mathbf{do}\, I, s \,\longrightarrow\, s', \mbox{było-}\mathbf{exit}} | |||
</math> | |||
Pominęliśmy zupełnie analogiczne reguły dla konfiguracji | |||
<math> s', \mbox{było-}\mathbf{continue} </math>. | |||
Zwróćmy szczególnie uwagę na ostatnią regułę dla pętli <math> \mathbf{while}\, </math>: | |||
wyraża ona przypadek, gdy ślad <math> \mathbf{exit} </math> został wygenerowany | |||
nie w pierwszym obiegu pętli, ale w którymś z kolejnych. | |||
Musieliśmy rozważyc również ten przypadek, ponieważ wybraliśmy | |||
podejście dużych kroków; w podejściu małych kroków nie byłoby | |||
to zapewne konieczne. | |||
Zauważmy też, że dla pętli <math> \mathbf{while}\, </math> nie rozpatrujemy przypadku, | |||
gdy dozór <math> b </math> oblicza się do <math> \mathbf{true} </math>, gdyż w tym przypadku | |||
nie ma możliwości wygenerowania śladu. | |||
Zauważmy, że nasze reguły nie pozwalają na dodanie drugiego | |||
(kolejnego) śladu! | |||
==== Rozwiązanie 2 (małe kroki) ==== | |||
W semantyce naturalnej musieliśmy napisać wiele reguł aby | |||
zapewnić pomijanie instrukcji w sytuacji, gdy został juz | |||
zarejestrowany jakiś ślad. Było to dość uciążliwe. | |||
Okazuje się, że podejście mało-krokowe oferuje możliwość | |||
bardziej eleganckiego rozwiązania. | |||
Punktem startowym sę teraz reguły mało-krokowe dla języka Tiny. | |||
Podobnie jak poprzednio rozszerzymy zbiór konfiguracji i podobnie | |||
opiszemy, jak powstaje ślad: | |||
<math> | |||
\mathbf{exit}, s \,\Longrightarrow\, s, \mbox{było-}\mathbf{exit} | |||
\quad \quad | |||
\mathbf{continue}, s \,\Longrightarrow\, s, \mbox{było-}\mathbf{continue}. | |||
</math> | |||
Ponadto musimy określić sposób, w jaki mały krok | |||
mniejszej instrukcji zostaje zamieniony na mały krok | |||
większej. Np. | |||
<math> | |||
\frac{I_1, s \,\Longrightarrow\, s', \mbox{było-}\mathbf{exit}} | |||
{I_1;\, I_2, s \,\Longrightarrow\, s', \mbox{było-}\mathbf{exit}} | |||
\quad \quad | |||
\mbox{ i analogicznie dla } \mbox{było-}\mathbf{continue}. | |||
</math> | |||
Zauważmy, że nie musimy zajmowac się przypadkiem, gdy ślad | |||
powstaje w <math> I_2 </math>, bo wybraliśmy podejście mało-krokowe. | |||
Ponadto, nie musimy opisywać instrukcji warunkowej i pętli <math> \mathbf{while}\, </math>, | |||
ponieważ ślad nie może powstać podczas oblcizania dozoru! | |||
Wreszcie zobaczmy jak zachowuje się pętla <math> \mathbf{loop}\, </math>: | |||
<math> | |||
\frac{I, s \,\Longrightarrow\, s', \mbox{było-}\mathbf{exit}} | |||
{\mathbf{loop}\, I, s \,\Longrightarrow\, s'} | |||
\quad \quad | |||
\frac{I, s \,\Longrightarrow\, s', \mbox{było-}\mathbf{continue}} | |||
{\mathbf{loop}\, I, s \,\Longrightarrow\, \mathbf{loop}\, I, s'} | |||
</math> | |||
Reguły te są prawie identyczne do reguł semantyki naturalnej dla tej | |||
sytuacji! | |||
Natomiast korzystamy z tego, że w podejściu mało-krokowym zmiana konfiguracji | |||
na <math> s', \mbox{było-}\mathbf{exit} </math> czy <math> s', \mbox{było-}\mathbf{continue} </math> jest | |||
''natychmiast'' widoczna w instrukcji <math> \mathbf{loop}\, I </math>, nawet jeśli | |||
<math> \mathbf{exit} </math> czy <math> CONT\,\mathbf{in}\,IE </math> zostało wywołane głęboko wewnątrz | |||
<math> I </math>! | |||
Niestety, powyzsze reguły '''nie są poprawne'''! | |||
Dlaczego? Jak zwykle w semantyce małych kroków, wykonując instrukcję | |||
wewnętrzną ''zapominamy'' stopniowo jaka była ona na początku. | |||
I w związku z tym nie potrafimy poprawnie powrócić do wykonywania | |||
pętli <math> \mathbf{loop}\, </math> po wywołaniu <math> \mathbf{continue} </math>. | |||
Prostym sposobem poprawienia naszego blędu jest rozszerzenie składni | |||
tak, aby możliwe było jednorazowe rozwinięcie pętli <math> \mathbf{loop}\, </math>: | |||
<math> | |||
\mathbf{loop}\, I, s \,\Longrightarrow\, I \,\mathbf{then}\, \mathbf{loop}\, I, s. | |||
</math> | |||
Teraz możemy zapisać dwie powyższe reguły dla <math> \mathbf{loop}\, </math> w poprawnej | |||
wersji, pamiętając o tym, że <math> \mbox{było-}\mathbf{exit} </math> i | |||
<math> \mbox{było-}\mathbf{continue} </math> pojawią się nie w instrukcji wewnętrznej, | |||
ale w jej ''kopii'' umieszczonej przed <math> \,\mathbf{then}\, </math>: | |||
<math> | |||
\frac{I', s \,\Longrightarrow\, s', \mbox{było-}\mathbf{exit}} | |||
{I' \,\mathbf{then}\, \mathbf{loop}\, I, s \,\Longrightarrow\, s'} | |||
\quad \quad | |||
\frac{I', s \,\Longrightarrow\, s', \mbox{było-}\mathbf{continue}} | |||
{I; \,\mathbf{then}\, \mathbf{loop}\, I, s \,\Longrightarrow\, \mathbf{loop}\, I, s'} | |||
</math> | |||
A ponieważ wewnątrz kopii mogą być zagnieżdzone instrukcje | |||
<math> \mathbf{loop}\, </math> potrzebujemy dodatkowej reguły: | |||
<math> | |||
\frac{I', s \,\Longrightarrow\, I'', s'} | |||
{I' \,\mathbf{then}\, \mathbf{loop}\, I, s \,\Longrightarrow\, I'' \,\mathbf{then}\, \mathbf{loop}\, I, s'} | |||
</math> | |||
Na koniec zauważmy, że stan <math> s' </math> w | |||
pierwszych dwóch z powyższych trzech reguł nigdy nie jest różny | |||
od <math> s </math>. Zatem równoważnie moglibyśmy zamienić <math> s' </math> na <math> s </math> | |||
w powyższych dwóch regułach. Ale wtedy okazuje się , <math> s </math> | |||
w parze z <math> \mbox{było-}\mathbf{exit} </math> albo <math> \mbox{było-}\mathbf{continue} </math> jest nadmiarowe | |||
i może zostać wyeliminowane. | |||
Zatem ostatecznie nasze reguły mogą wyglądać tak: | |||
<math> | |||
\mathbf{exit}, s \,\Longrightarrow\, \mbox{było-}\mathbf{exit} | |||
\quad \quad | |||
\mathbf{continue}, s \,\Longrightarrow\, \mbox{było-}\mathbf{continue} | |||
</math> | |||
<math> | |||
\frac{I_1, s \,\Longrightarrow\, \mbox{było-}\mathbf{exit}} | |||
{I_1;\, I_2, s \,\Longrightarrow\, \mbox{było-}\mathbf{exit}} | |||
\quad \quad | |||
\frac{I_1, s \,\Longrightarrow\, \mbox{było-}\mathbf{continue}} | |||
{I_1;\, I_2, s \,\Longrightarrow\, \mbox{było-}\mathbf{continue}} | |||
</math> | |||
<math> | |||
\mathbf{loop}\, I, s \,\Longrightarrow\, I \,\mathbf{then}\, \mathbf{loop}\, I, s. | |||
</math> | |||
<math> | |||
\frac{I', s \,\Longrightarrow\, \mbox{było-}\mathbf{exit}} | |||
{I' \,\mathbf{then}\, \mathbf{loop}\, I, s \,\Longrightarrow\, s} | |||
\quad \quad | |||
\frac{I', s \,\Longrightarrow\, \mbox{było-}\mathbf{continue}} | |||
{I; \,\mathbf{then}\, \mathbf{loop}\, I, s \,\Longrightarrow\, \mathbf{loop}\, I, s} | |||
</math> | |||
<math> | |||
\frac{I', s \,\Longrightarrow\, I'', s'} | |||
{I' \,\mathbf{then}\, \mathbf{loop}\, I, s \,\Longrightarrow\, I'' \,\mathbf{then}\, \mathbf{loop}\, I, s'} | |||
</math> | |||
a zbiór konfiguracji poszerzamy tylko o | |||
<math> | |||
\{ \mbox{było-}\mathbf{exit}, \mbox{było-}\mathbf{continue} \}. | |||
</math> | |||
== Zadania domowe == | |||
==== Zadanie 1 ==== | |||
Zaproponuj semantykę mało-krokową języka z zdania 2. | |||
Czy różni się ona istotnie od semantyki naturalnej? | |||
==== Zadanie 2 ==== | |||
Napisz semantyję naturalną dla nieznacznie rozszerzonej wersji instrukcji <math> \mathbf{loop}\, </math>: | |||
<math> | |||
I \, ::= \,\, | |||
x: \mathbf{loop}\, I \,\,|\,\, | |||
\mathbf{exit} x \,\,|\,\, | |||
\mathbf{continue} x | |||
</math> | |||
Identyfikator <math> x </math> pełni tutaj rolę etykiety przypisanej | |||
instrukcji <math> \mathbf{loop}\, </math>, jest też parametrem dwóch pozostałych | |||
instrukcji. <math> \mathbf{exit} x </math> kończy teraz najbliższą otaczającą pętlę <math> \mathbf{loop}\, </math> | |||
o etykiecie <math> x </math>. Podobnie <math> \mathbf{continue} x </math> wznawia najbliższą | |||
pętlę o etykiecie <math> x </math>. Np. program | |||
<math> | |||
x: \mathbf{loop}\,\\ | |||
\quad \quad a := 1; | |||
\quad \quad y: \mathbf{loop}\, | |||
\quad \quad \quad \quad \mathbf{exit} x; | |||
\quad \quad \quad \quad a := a-10; | |||
\quad \quad a := a+1; | |||
a := a+2; | |||
</math> | |||
kończy działanie z wartością zmiennej <math> a = 3 </math>. | |||
Zauważmy, że musieliśmy jakoś określić, do wnętrza której pętli | |||
<math> \mathbf{loop}\, </math> należą trzy ostatnie instrukcje. Użyliśmy to tego celu | |||
wcięć a niejednoznaczność bierze się oczywiście stąd, że | |||
pracujemy ze składnią abstrakcyjną. Składnia konkretna zawierałaby | |||
prawdopodobnie jakąś konstukcję ''zamykającą'' pętlę <math> \mathbf{loop}\, </math>, | |||
np. <math> \mathbf{loop}\, I \,\mathbf{end}\mathbf{loop}\, </math>. | |||
==== Zadanie 3 ==== | |||
Napisz semantykę naturalną i mało-krokową dla rozszerzenia | |||
języka Tiny o wyrażenia z efektami ubocznymi: | |||
<math> | |||
e \, ::= \,\, | |||
\ldots \,\,|\,\, | |||
\,\mathbf{do}\, I \,\mathbf{then}\, e \,\,|\,\, | |||
x := e \,\,|\,\, | |||
x++ \,\,|\,\, \ldots | |||
</math> | |||
Obliczenie wyrażenia <math> \,\mathbf{do}\, I \,\mathbf{then}\, e </math> polega na wykonaniu <math> I </math> | |||
a potem na obliczeniu <math> e </math>. Wartość wyrażenia <math> x:= e </math> jest taka | |||
jak wartość wyrażenia <math> e </math> a efektem ubocznym jest | |||
podstawienie tej wartości na zmienną <math> x </math>. |
Wersja z 08:21, 7 sie 2006
Zawartość
Kończymy semantykę małych kroków i rozpoczynamy semantykę naturalną (duże kroki). Uzupełnimy semantykę naturalną języka Tiny o semantykę naturalną dla wyrażeń boolowskich i arytmetycznych i semantykę dla błędów wykonania. Wreszcie dodamy instrukcję pętli i instrukcje niestrukturalnego zakończenia pętli (instrukcje i ).
Rozszerzenia semantyki języka Tiny
Zadanie 1
Zdefiniuj znaczenie wyrażeń boolowskich i arytmetycznych w języku Tiny, w stylu dużych kroków (semantyka naturalna).
Rozwiązanie
Przypomnijmy składnię wyrażeń boolowskich i arytmetycznych jezyka Tiny:
Chcemy, aby tranzycje wyrażen wyglądały następująco:
gdzie , jest liczbą całkowitą, , a . Tranzycja taka oznacza, że wyrażenie w stanie wylicza się do wartości oraz analogicznie, wyrażenie logiczne w stanie wylicza się do . Zatem zbiór konfiguracji dla semantyki całego języka Tiny to znów
a konfiguracje końcowe to tak jak poprzednio. Tranzycje dla instrukcji pozostają zasadniczo bez zmian, z tym, że odwołania do funkcji semantycznych dla wyraż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 i boolowskich:
Operatory arytmetyczne definiujemy następująco:
Czyli aby obliczyć sumę w stanie , trzeba najpierw obliczyć i w tym stanie, a następnie dodać obliczone wartości. Zauważmy, że nie specyfikujemy kolejności, w jakiej mają się obliczać obydwa podwyrażenia. I choć tutaj nie ma to żadnego znaczenia, w przyszłości będzie inaczej, gdy jezyk będzie umożliwiał efekty uboczne podczas obliczania wyrażeń.
Podobne reguły można napisać dla pozostałych operacji arytmnetycznych, oraz dla spójników logicznych:
Oto inny wariant, reguły lewo-stronne (ponieważ rozpoczynamy od lewego koniunktu):
Inny wariant to wariant prawo-stronny ( najpierw , potem ). Wreszcie rozważmy kombinację obydwu semantyk (reguły równoległe):
Czyli jeśli którekolwiek z podwyrażeń daje wynik , to taki wynik zyskuje całe wyrażenie. Dodatkowo potrzebujemy jeszcze reguły:
W naszym prostym języku wszystkie cztery warianty są równoważne. Reguły dla pozostałych spójników logicznych, dla negacji oraz dla instrukcji przypisania pozostawiamy jako proste ćwiczenie.
Zadanie 2
Rozważ dodatkowo operację dzielenia:
i rozszerz semantykę 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. 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:
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.
Następnie, błąd w wyrażeniu powinien wstrzymać normalne wykonanie instrukcji:
I wreszcie błąd powinien propagować się do kolejnych instrukcji:
Zadanie 3
Rozszerz język Tiny o następującą instrukcję pętli:
to instrukcja pętli, stanowi instrukcję wewnętrzną. Instrukcja wychodzi z nabliższej pętli i kontynuuje wykonanie programu od pierwszej instrukcji za tą pętlą. Instrukcje powraca na początek instrukcji wewnętrznej najbliższej pętli.
Pozważ zarówno semantykę naturalną jak i semantykę małych kroków.
Rozwiązanie 1 (semantyka naturalna)
Dodamy do reguł semantyki naturalnej dla języka Tiny kilka nowych reguł opisujących nową konstrukcję języka i jej interakcję z pozostałymi konstrukcjami.
Pomysł polega na dodaniu specjalnych konfiguracji zawierających informację o tym, że została wykonana instrukcja lub . Oto odpowiednie reguły:
Czyli instrukcje i nie modyfikują stanu , ale zostawiają po sobie \ślad". Zauważmy, że taki ślad zostaje pozostawiony tylko wtedy, jesli nie było dotychczas innego śladu, to znaczy jeśli (lub ) zostało wykonane w zwykłym stanie . Oczywiście poszerzamy odpowiednio zbiór konfiguracji o:
</math>[ \mathbf{State} \times \{ \mbox{było-}\mathbf{exit}, \mbox{było-}\mathbf{continue} \} </math> , Pytanie: jakie powinno być zachowanie i w konfiguracji lub ? Czy instrukcje i będą faktycznie wykonywane w takich konfiguracjach?
Zapiszmy teraz jak inne instrukcje korzystają z dodatkowej informacji (śladu) zawartej w konfiguracjach. Oczywiście beneficjentem tej informacji jest instrukcji :
Czyli w zależności od tego, jaki ślad został zarejestrowany podczas wykonania , albo kończymy wykonanie pętli , albo rozpoczynamy kolejną iterację. Zauważmy, że stan może być różny od , ponieważ zanim wykonała się ktoraś z instrukcji lub , mogły zostać zmienione wartości niektórych zmiennych.
Oczywiście, jeśli instrukcja wewnętrzna zakończyła się normalnie, kontynuujemy wykonanie pętli podobnie jak w przypadku wywołania :
Pytanie: czy potrzebujemy dodatkowo reguł postaci:
Okazuje się że nie, ponieważ ślad powinien zostać zawsze wymazany przez pętlę . Poza tym reguły te wykraczałyby poza klasyczna podejście semantyki naturalnej, w którym zwykle bezpośrednio definiujemy tranzycje postaci .
Teraz musimy określić zachowanie wszystkich instrukcji w sytuacji, gdy bieżąca konfiguracja zawiera już ślad. Zasadniczo, powinniśmy zaniechać wykonania instrukcji (w przypadku pętli, powinniśmy zaniechać dalszego iterowania tej pętli). Oto odpowiednie reguły dla :
Pominęliśmy zupełnie analogiczne reguły dla konfiguracji . Zwróćmy szczególnie uwagę na ostatnią regułę dla pętli : wyraża ona przypadek, gdy ślad został wygenerowany nie w pierwszym obiegu pętli, ale w którymś z kolejnych. Musieliśmy rozważyc również ten przypadek, ponieważ wybraliśmy podejście dużych kroków; w podejściu małych kroków nie byłoby to zapewne konieczne. Zauważmy też, że dla pętli nie rozpatrujemy przypadku, gdy dozór oblicza się do , gdyż w tym przypadku nie ma możliwości wygenerowania śladu.
Zauważmy, że nasze reguły nie pozwalają na dodanie drugiego (kolejnego) śladu!
Rozwiązanie 2 (małe kroki)
W semantyce naturalnej musieliśmy napisać wiele reguł aby zapewnić pomijanie instrukcji w sytuacji, gdy został juz zarejestrowany jakiś ślad. Było to dość uciążliwe. Okazuje się, że podejście mało-krokowe oferuje możliwość bardziej eleganckiego rozwiązania.
Punktem startowym sę teraz reguły mało-krokowe dla języka Tiny.
Podobnie jak poprzednio rozszerzymy zbiór konfiguracji i podobnie opiszemy, jak powstaje ślad:
Ponadto musimy określić sposób, w jaki mały krok mniejszej instrukcji zostaje zamieniony na mały krok większej. Np.
Zauważmy, że nie musimy zajmowac się przypadkiem, gdy ślad powstaje w , bo wybraliśmy podejście mało-krokowe. Ponadto, nie musimy opisywać instrukcji warunkowej i pętli , ponieważ ślad nie może powstać podczas oblcizania dozoru!
Wreszcie zobaczmy jak zachowuje się pętla :
Reguły te są prawie identyczne do reguł semantyki naturalnej dla tej sytuacji! Natomiast korzystamy z tego, że w podejściu mało-krokowym zmiana konfiguracji na czy jest natychmiast widoczna w instrukcji , nawet jeśli czy zostało wywołane głęboko wewnątrz !
Niestety, powyzsze reguły nie są poprawne! Dlaczego? Jak zwykle w semantyce małych kroków, wykonując instrukcję wewnętrzną zapominamy stopniowo jaka była ona na początku. I w związku z tym nie potrafimy poprawnie powrócić do wykonywania pętli po wywołaniu .
Prostym sposobem poprawienia naszego blędu jest rozszerzenie składni tak, aby możliwe było jednorazowe rozwinięcie pętli :
Teraz możemy zapisać dwie powyższe reguły dla w poprawnej wersji, pamiętając o tym, że i pojawią się nie w instrukcji wewnętrznej, ale w jej kopii umieszczonej przed :
A ponieważ wewnątrz kopii mogą być zagnieżdzone instrukcje potrzebujemy dodatkowej reguły:
Na koniec zauważmy, że stan w pierwszych dwóch z powyższych trzech reguł nigdy nie jest różny od . Zatem równoważnie moglibyśmy zamienić na w powyższych dwóch regułach. Ale wtedy okazuje się , w parze z albo jest nadmiarowe i może zostać wyeliminowane.
Zatem ostatecznie nasze reguły mogą wyglądać tak:
a zbiór konfiguracji poszerzamy tylko o
Zadania domowe
Zadanie 1
Zaproponuj semantykę mało-krokową języka z zdania 2. Czy różni się ona istotnie od semantyki naturalnej?
Zadanie 2
Napisz semantyję naturalną dla nieznacznie rozszerzonej wersji instrukcji :
Identyfikator pełni tutaj rolę etykiety przypisanej instrukcji , jest też parametrem dwóch pozostałych instrukcji. kończy teraz najbliższą otaczającą pętlę o etykiecie . Podobnie wznawia najbliższą pętlę o etykiecie . Np. program
Parser nie mógł rozpoznać (błąd składni): {\displaystyle x: \mathbf{loop}\,\\ \quad \quad a := 1; \quad \quad y: \mathbf{loop}\, \quad \quad \quad \quad \mathbf{exit} x; \quad \quad \quad \quad a := a-10; \quad \quad a := a+1; a := a+2; }
kończy działanie z wartością zmiennej . Zauważmy, że musieliśmy jakoś określić, do wnętrza której pętli należą trzy ostatnie instrukcje. Użyliśmy to tego celu wcięć a niejednoznaczność bierze się oczywiście stąd, że pracujemy ze składnią abstrakcyjną. Składnia konkretna zawierałaby prawdopodobnie jakąś konstukcję zamykającą pętlę , np. .
Zadanie 3
Napisz semantykę naturalną i mało-krokową dla rozszerzenia języka Tiny o wyrażenia z efektami ubocznymi:
Obliczenie wyrażenia polega na wykonaniu a potem na obliczeniu . Wartość wyrażenia jest taka jak wartość wyrażenia a efektem ubocznym jest podstawienie tej wartości na zmienną .