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

Z Studia Informatyczne
Przejdź do nawigacjiPrzejdź do wyszukiwania
Sl (dyskusja | edycje)
Nie podano opisu zmian
Sl (dyskusja | edycje)
Nie podano opisu zmian
Linia 1: Linia 1:


== Ćwiczenia 1: semantyka operacyjna wyrażeń (małe kroki) ==
=== Zadania z rozwiązaniami ===
==== Zadanie 1 (przygotowawcze) ====
Rozważmy bardzo prosty język wyrażeń, którego składnia opisana jest
następującą gramatyką:
<math>
n \, ::= \,\, 0 \,\,|\,\, 1 \,\,|\,\, \ldots
</math>
<math>
x \, ::= \,\, \ldots \, (identyfikatory) \, \ldots
</math>
<math>
e \,  ::=  \,\, 
        n  \,\,|\,\,
        x  \,\,|\,\,
        e_1 + e_2  \,\,|\,\,
        \mathbf{if}\, e_1 \,\mathbf{then}\, e_2 \,\mathbf{else}\, e_3
</math>
Wynikiem wyrażenienia warunkowego <math> \mathbf{if}\, e_1 \,\mathbf{then}\, e_2 \,\mathbf{else}\, e_3 </math>
jest wartość wyrażenia <math> e_2 </math>, o ile wyrażenie
<math> e_1 </math> oblicza się do wartości różnej od zera; w przeciwnym
przypadku wynikiem jest wartość wyrażenia <math> e_3 </math>.
Zaproponuj semantykę operacyjną (małe kroki) dla tego języka.
==== Rozwiązanie ====
Zacznijmy od ustalenia notacji i dziedzin syntaktycznych.
Niech <math> \mathbf{Num} </math> oznacza zbiór stałych liczbowych,
<math> n \in \mathbf{Num} = \{ 0, 1, \ldots \} </math>.
Podobnie, niech <math> \mathbf{Var} </math> oznacza zbiór identyfikatorów, które
mogą być nazwami zmiennych; <math> x \in \mathbf{Var} </math>.
Wreszcie, niech <math> \mathbf{Exp} </math> oznacza zbiór wyrażeń;
<math> e \in \mathbf{Exp} </math>.
Dla ułatwienia zapisywania reguł zakładamy, ze stałe
liczbowe sa wyrażeniami, czyli <math> \mathbf{Num} \subseteq \mathbf{Exp} </math>.
Będziemy potrzebować zbioru ''stanów'', opisujących wartości
przypisane zmiennym.
Najprostszym rozwiązaniem jest przyjąc, że stan to funkcja
z <math> \mathbf{Var} </math> do <math> \mathbf{Num} </math>.
Oznaczmy przez <math> \mathbf{State} </math> zbiór wszystkich takich funkcji;
stany oznaczac będziemy przez <math> s, s_1, s', \ldots \in \mathbf{State} </math>.
W naszej semantyce będziemy potrzebowac tranzycji dwóch postaci.
Po pierwsze, tranzycja
<math>
e, s \,\Longrightarrow\, e', s
</math>
oznaczająca mały krok w trakcie obliczania wyrażenia <math> e </math>
w stanie <math> s </math>, w wyniku którego <math> e </math> wyewoluowało do
<math> e' </math>. Stan nie ulega zmiania podczas obliczania wyrażenia,
więc to samo <math> s </math> figuruje po lewej i prawej stronie strzałki.
Po drugie, tranzycja
<math>
e, s \,\Longrightarrow\, n 
</math>
będzie oznaczaczać, że wyrażenie <math> e </math> jest już policzone,
a jego wartością jest <math> n </math>.
Zatem przyjmijmy, że zbiór konfiguracji to
<math>
( \mathbf{Exp} \times \mathbf{State} ) \, \cup \, \mathbf{Num}
</math>
a konfiguracje końcowe to <math> \mathbf{Num} </math>.
'''Uwaga:'''
Tranzycje pierwszej postaci mogłyby również wyglądać
następująco:
<math>
e, s \,\Longrightarrow\, e'.
</math>
Wtedy zbiorem konfiguracji byłby zbiór
<math>
( \mathbf{Exp} \times \mathbf{State} ) \, \cup \, \mathbf{Exp}
</math>
a konfiguracje końcowe pozostałyby bez zmian.
'''(koniec uwagi)'''
Najprostsze są tranzycje prowadzące do konfiguracji końcowej:
<math>
n, s \,\Longrightarrow\, n
</math>
Zmienna oblicza się do swojej wartości w bieżącym stanie: 
<math>
x, s \,\Longrightarrow\, n, s \quad \mbox{ o ile } s(x) = n.
</math>
Teraz zajmiemy się dodawaniem <math> e_1 + e_2 </math>. Ponieważ semantyka jest w stylu małych
kroków, musimy zdecydować się czy najpierw obliczyć pierwszy (lewy) składnik
<math> e_1 </math> czy drugi?
Jeśli wybierzemy lewy (strategia ''lewostronna''), otrzymamy regułę:
<math>
\frac{e_1, s \,\Longrightarrow\, e'_1, s}
    {e_1 + e_2, s \,\Longrightarrow\, e'_1 + e_2, s}.
</math>
Czyli mały krok w <math> e_1 </math> stanowi też mały krok w <math> e_1 + e_2 </math>.
Po zakończeniu obliczania <math> e_1 </math> przechodzimy do <math> e_2 </math>:
<math>
\frac{e_2, s \,\Longrightarrow\, e'_2, s}
    {n + e_2, s \,\Longrightarrow\, n + e'_2, s}.
</math>
A na końcu dodajemy:
<math>
n_1 + n_2, s \,\Longrightarrow\, n, s \quad \mbox{ o ile } n = n_1 + n_2.
</math>
Zauważmy tutaj pewną subtelność, dotyczącą dwóch wystąpień
symbolu ''+'': pierwsze wystąpienie oznacza jedną z konstrukcji składniowych
języka, a drugie oznacza operację dodawania w zbiorze <math> \mathbf{Num} </math>.
Pozwalamy sobie na taką kolizję oznaczeń, gdyż nie powinna ona
prowadzić do niejednoznaczności. Pamiętajmy, że składnia języka jest
składnią abstrajkcyjną, więc zamiast <math> e_1 + e_2 </math> moglibyśmy równie
dobrze pisać np. <math> {\mathrm{add}}(e_1, e_2) </math>.
Inna możliwą strategią obliczania <math> e_1 + e_2 </math> jest strategia
''prawostronna'', którą otrzymujemy zastępując pierwsze dwie z trzech
powyższych reguł przez:
<math>
\frac{e_2, s \,\Longrightarrow\, e'_2, s}
    {e_1 + e_2, s \,\Longrightarrow\, e_1 + e'_2}
\quad \quad \quad
\frac{e_1, s \,\Longrightarrow\, e'_1, s}
    {e_1 + n, s \,\Longrightarrow\, e'_1 + n, s}.
</math>
Ponadto, jeśli przyjmiemy regułę pierwszą (dla <math> e_1 </math>), trzecią
i czwartą (dla <math> e_2 </math>), otrzymamy strategię
''równoległą'', polegającą na obliczaniu jednocześnie <math> e_1 </math> i
<math> e_2 </math>:
<math>
\frac{e_1, s \,\Longrightarrow\, e'_1, s}
    {e_1 + e_2, s \,\Longrightarrow\, e'_1 + e_2, s}
\quad \quad \quad
\frac{e_2, s \,\Longrightarrow\, e'_2, s}
    {e_1 + e_2, s \,\Longrightarrow\, e_1 + e'_2}
\quad \quad \quad
n_1 + n_2, s \,\Longrightarrow\, n, s \quad \mbox{ o ile } n = n_1 + n_2.
</math>
Bardziej precyzyjnie mówiąc, małe kroki obliczające
obydwa podwyrażenia przeplatają się, i to w dowolny sposób.
Ta dowolność prowadzi do ''niedeterminizmu'', czyli do sytuacji, gdy
kolejna (następna) konfiguracja nie jest wyznaczona jednoznacznie.
Jest tak, gdyż możemy mieć do wyboru dwie różne tranzycje
<math>
e_1 + e_2, s \,\Longrightarrow\, e'_1 + e_2, s
\quad \quad \quad
e_1 + e_2, s \,\Longrightarrow\, e_1 + e'_2, s.
</math>
Zauważmy natomiast, że kolejność przeplatania się małych kroków obliczających
<math> e_1 </math> i <math> e_2 </math> nie wpływa w tym przypadku na końcową wartość
całego wyrażenia.
Na koniec reguły dla wyrażenia warunkowego.
<math>
\frac{e_1, s \,\Longrightarrow\, e'_1, s}
    {\mathbf{if}\, e_1 \,\mathbf{then}\, e_2 \,\mathbf{else}\, e_3, s \,\Longrightarrow\, \mathbf{if}\, e'_1 \,\mathbf{then}\, e_2 \,\mathbf{else}\, e_3, s}
</math>
<math>
\mathbf{if}\, n \,\mathbf{then}\, e_2 \,\mathbf{else}\, e_3, s \,\Longrightarrow\, e_2, s \quad \mbox{ o ile } n \neq 0
</math>
<math>
\mathbf{if}\, n \,\mathbf{then}\, e_2 \,\mathbf{else}\, e_3, s \,\Longrightarrow\, e_3, s \quad \mbox{ o ile } n = 0
</math>
==== Zadanie 2 ====
Rozszerzmy język wyrażeń z poprzedniego zadania o jedną konstrukcję
<math>
e \,  ::=  \,\, 
        \ldots  \,\,|\,\,
        \mathbf{let}\, x = e_1 \,\mathbf{in}\, e_2
</math>
Wyrażenie <math> \mathbf{let}\, x = e_1 \,\mathbf{in}\, e_2 </math> zawiera w sobie deklarację
<math> x = e_1 </math>, która stanowi mechanizm wiązania
identyfikatorów w naszym języku.
Deklaracja <math> x = e_1 </math> wprowadza nową zmienną <math> x </math>
oraz przypisuje jej wartość.
Wartość wyrażenia <math> \mathbf{let}\, x = e_1 \,\mathbf{in}\, e_2 </math> obliczamy następująco:
najpierw oblicza się wartość <math> e_1 </math>, podstawia ją na zmienna
<math> x </math>, a następnie oblicza wyrażenie <math> e_2 </math>.
Zakresem zmiennej <math> x </math> jest wyrażenie <math> e_2 </math>, czyli
wewnątrz <math> e_2 </math> można odwoływać się (wielokrotnie) do zmiennej <math> x </math>;
Ogólniej, odwołania do zmiennej w wyrażeniu odnoszą się do ''najbliższej''
(najbardziej zagnieżdzonej) deklaracji tej zmiennej.
Taki mechanizm wiązania identyfikatorów nazywamy ''wiązaniem
statycznym''.
Przyjmujemy zwykłe reguły przesłaniania zmiennych.
Np., jeśli w <math> e_2 </math> występuje podwyrażenie <math> \mathbf{let}\, x = \ldots \,\mathbf{in}\, e </math> to
odwołania do <math> x </math> wewnątrz <math> e </math> odnoszą się do ''najbliższej''
deklaracji zmiennej <math> x </math>.
Zakładamy, że na początku wartości wszystkich zmiennych są
''nieokreślone'', czyli zmienne są niezainicjowane, a odwołanie do
niezainicjowanej zmiennej jest uważane za niepoprawne.
==== Przykłady ====
<math>
\mathbf{let}\, x = z+z \,\mathbf{in}\, \mathbf{let}\, y = 7 \,\mathbf{in}\, \mathbf{let}\, x = y+3 \,\mathbf{in}\, x+x+y
\quad \quad \mapsto \quad \quad \mbox{wynik} = 24
</math>
<math>
\mathbf{let}\, y = 5 \,\mathbf{in}\, \mathbf{let}\, x = (\, \mathbf{let}\, y = 3 \,\mathbf{in}\, y+y \,) \,\mathbf{in}\, x+y
\quad \quad \mapsto \quad \quad \mbox{wynik} = 11
</math>
<math>
\mathbf{let}\, z = 5 \,\mathbf{in}\, x+z
\quad \quad \mapsto \quad \quad \mbox{brak wyniku; odwołanie do niezainicjowanej
zmiennej}\, x
</math>
<math>
\mathbf{let}\, x = 1 \,\mathbf{in}\, \mathbf{let}\, x = x+x \,\mathbf{in}\, x+x
\quad \quad \mapsto \quad \quad \mbox{wynik} = 4
</math>
==== Rozwiązanie ====
Podobnie jak poprzednio,
stan powinien opisywać wartości przypisane zmiennym, ale powinniśmy też
uwzględnić niezainicjowane zmienne, czyli zmienne bez żadnej wartości.
Przyjmijmy zatem, że stan to skończona funkcja częściowa z <math> \mathbf{Var} </math> do <math> \mathbf{Num} </math>.
Oznaczmy symbolem <math> \mathbf{State} </math> zbiór wszystkich takich funkcji:
<math>
\mathbf{State} = \mathbf{Var} \to_{\mathrm{fin}} \mathbf{Num}
</math>.
Naturalnym stanem początkowym jest stan ''pusty'', tzn.
pusta funkcja częściowa, który będziemy oznaczać <math> \emptyset </math>.
A wartość wyrażenia <math> e </math> w stanie początkowym wynosi <math> n </math>
o ile zachodzi:
<math>
e, \emptyset \,\Longrightarrow^{*}\, n.
</math>
Będziemy potrzebowac tranzycji dwóch postaci, podobnie jak poprzednio,
ale pierwsza postać będzie trochę ogólniejsza:
<math>
e, s \,\Longrightarrow\, e', s'.
</math>
Tranzycja ta oznacza mały krok w trakcie obliczania wyrażenia <math> e </math>
w stanie <math> s </math>, w wyniku którego <math> e </math> wyewoluowało do
<math> e' </math> a nowym stanem jest <math> s' </math>.
Stan może się teraz zmienić na skutek deklaracji zmiennych.
Spróbujmy rozszerzyc semantykę z poprzedniego zadania.
Ponieważ stan jest funkcją częściową, musimy zmienić niektóre reguły, np.
<math>
x, s \,\Longrightarrow\, n, s \quad \mbox{ o ile } s(x) \mbox{ jest określone i } s(x) = n
</math>
Następnie dodajemy reguły dla wyrażenia
<math> \mathbf{let}\, x = e_1 \,\mathbf{in}\, e_2 </math>.
Gdy <math> e_1 </math> jest już obliczne, wyatarczy reguła:
<math>
\mathbf{let}\, x = n \,\mathbf{in}\, e_2, s \,\Longrightarrow\, e_2, s[x \mapsto n].
</math>
Notacja <math> s[x \mapsto n] </math> oznacza stan <math> s </math>, który zmodyfikowano
przypisując zmiennej <math> x </math> wartość <math> n </math>,
niezależnie od tego, czy <math> s(x) </math> było określone, czy nie,
i pozostawiając niezmienione wartości dla pozostałych zmiennych.
Formanie
<math>
s[x \mapsto n](y) =
\begin{cases}
n    & y = x \\
s(y) & y \neq x
\end{cases}
</math>
Dla <math> y \neq x </math> zachodzi <math> s[x \mapsto n](y) = s(y) </math>
(w szczególności, <math> s[x \mapsto n](y) </math> jest określone wtedy i tylko
wtedy, gdy <math> s(y) </math> jest określone).
Natomiast aby obliczyc <math> e_1 </math> potrzebujemy reguły:
<math>
\frac{e_1, s \,\Longrightarrow\, e'_1, s'}
{\mathbf{let}\, x = e_1 \,\mathbf{in}\, e_2, s \,\Longrightarrow\, \mathbf{let}\, x = e'_1 \,\mathbf{in}\, e_2, s'}
</math>
Zwróćmy uwagę, że stan <math> s' </math> może być różny od <math> s </math>,
np. dlatego, że wewnątrz <math> e_1 </math> znajduje się podwyrażenie
<math> \mathbf{let}\, y = \ldots </math>.
'''Pytanie:''' czy taka semantyka jest poprawna?
Niestety nie, gdyż nie uwzględniamy ograniczonego zasięgu zmiennej.
Rzućmy okiem na przykład:
<math>
\mathbf{let}\, x = (\mathbf{let}\, z = 4 \,\mathbf{in}\, z+z+z) in z
</math>
Według naszych intencji to wyrażenie nie ma wartości, gdyż ostatnie
odwołanie do <math> z </math> jest błędne.
Natomiast według powyższych reguł mamy
<math>
\mathbf{let}\, x = (\mathbf{let}\, z = 4 in z+z+z) \,\mathbf{in}\, z, \emptyset \,\Longrightarrow\,
\mathbf{let}\, x = z+z+z \,\mathbf{in}\, z, \emptyset[z \mapsto 4] \,\Longrightarrow\, \quad \ldots \quad \,\Longrightarrow\,
\mathbf{let}\, x = 12 \,\mathbf{in}\, z, \emptyset[z \mapsto 4] \,\Longrightarrow\,
12, \emptyset[z \mapsto 4] \,\Longrightarrow\,
12 !
</math>
Nasz błąd polega na tym, że po zakończeniu obliczania podwyrażenia
<math> \mathbf{let}\, z = 4 \,\mathbf{in}\, z+z+z </math> ''zapominamy'' przywrócić zmiennej <math> z </math>
poprzednią wartość (a właściwie brak wartości w przykładzie powyżej).
Wygodne i eleganckie rozwiązanie tego problemu jest możliwe, jeśli
rozszerzymy składnię naszego języka.
Intuicyjnie, reguła
<math>
\mathbf{let}\, x = n \,\mathbf{in}\, e_2, s \,\Longrightarrow\, e_2, s[x \mapsto n].
</math>
powinna zostać zastąpiona przez
<math>
\mathbf{let}\, x = n \,\mathbf{in}\, e_2, s \,\Longrightarrow\, e_2 \,\mathbf{then}\, \mbox{,,przywróć wartość zmiennej x''}, s[x \mapsto n].
</math>
czyli potrzebujemy konstrukcji składniowej, która polega na obliczeniu
wyrażenia
<math> e_2 </math> a następnie na przypisaniu zmiennej <math> x </math> danej wartości.
Rozszerzmy zatem składnię następujaco:
<math>
e \,  ::=  \,\, 
        \ldots  \,\,|\,\,
        e \,\mathbf{then}\, x := n.
</math>
Zauważmy, że wyrażenie <math> e \,\mathbf{then}\, x:= n </math> jest w pewnym sensie dualne
do <math> \mathbf{let}\, x = n \,\mathbf{in}\, e </math>, gdyż jedyna (choc niewątpliwie istotna) różnica
między nimi to kolejność obliczenia <math> e </math> i przypisania wartości
na zmienną <math> x </math>.
Oto nowa reguła
<math>
\mathbf{let}\, x = n \,\mathbf{in}\, e_2, s \,\Longrightarrow\, e_2 \,\mathbf{then}\, x := n', s[x \mapsto n] \quad
\mbox{ o ile } s(x) = n'.
</math>
Pewna trudność pojawiają się w sytuacji, gdy <math> s(x) </math> jest
nieokreślone, czyli gdy zmienna <math> x </math> jest niezainicjowana -- reguła
powyższa nie obejmuje wogóle takiej sytuacji.
Najprostszym sposobem rozwiązania tej trudności jest rozszerzenie
konstrukcji <math> e \,\mathbf{then}\, x := n </math>:
<math>
e \,  ::=  \,\, 
        \ldots  \,\,|\,\,
        e \,\mathbf{then}\, x := n  \,\,|\,\,
        e \,\mathbf{then}\, x := \bot
</math>
gdzie symbol <math> \bot </math> oznacza brak wartości.
Dodajemy również regułę:
<math>
\mathbf{let}\, x = n \,\mathbf{in}\, e_2, s \,\Longrightarrow\, e_2 \,\mathbf{then}\, x := \bot, s[x \mapsto n] \quad
\mbox{ o ile } s(x) \, \mbox{ jest nieokreślone}.
</math>
'''Uwaga:'''
Inny sposób rozwiązania omawianej trudności polega na rozszerzeniu
zbioru <math> \mathbf{Num} </math> o dodatkowy element <math> \bot </math>:
<math>
n \, ::= \,\, \bot \,\,|\,\, 0 \,\,|\,\, 1 \,\,|\,\, \ldots
</math>
Wtedy nie musimy dodawać osobnego wariantu ostatniej reguły, ale
za to <math> n = \bot </math> może sie pojawić w wyrażeniach.
Dodatkowo, w tym rozwiązaniu warto poczynić umowę, że
<math> s(x) = \bot </math> reprezentuje brak wartości zmiennej <math> x </math>.
Wtedy stany są funkcjami całkowitymi z <math> \mathbf{Var} </math> w <math> \mathbf{Num} </math>
przyjmującymi wartość różną od <math> \bot </math> tylko dla skończenie
wielu elementów.
'''(koniec uwagi)'''
Na zakończenie ''scalamy'' semantykę dla <math> e \,\mathbf{then}\, x := n </math>
z semantyką pozostałych wyrażeń:
<math>
\frac{e, s \,\Longrightarrow\, e', s'}
{e \,\mathbf{then}\, x := n, s \,\Longrightarrow\, e' \,\mathbf{then}\, x:= n, s'}
</math>
<math>
n' \,\mathbf{then}\, x := n, s \,\Longrightarrow\, n', s[x \mapsto n]
</math>
==== Zadanie 3 ====
Zmodyfikuj semantykę z poprzedniego zadania tak, aby uzyskać
''leniwą'' ewaluację wyrażeń, zgodnie z dyrektywą: nie obliczaj
wyrażenia o ile jego wynik nie jest potrzebny
(albo: obliczaj wartość wyrażenia dopiero wtedy, gdy jego wynik jest
naprawdę potrzebny). Spójrzmy na przykład:
<math>
\mathbf{let}\, x = 7 \,\mathbf{in}\, \mathbf{let}\, y = y+y \,\mathbf{in}\, x+x
</math>
Według semantyki z poprzedniego zadania wyrażnie to nie ma wartości,
bo w deklaracji <math> y = y+y </math> jest odwołanie do niezainicjowanej
zmiennej.
Natomiast w semantyce leniwej wyrażenie to obliczy się do wartości
<math> 14 </math>, gdyż wyrażenie <math> y+y </math> nie będzie wogóle obliczane.
Będzie tak dlatego, że w wyrażeniu <math> x+x </math> nie ma odwołań do
zmiennej <math> y </math>.
==== Rozwiązanie ====
Semantyka będzie bardzo podobna do tej z poprzedniego zadania.
Zasadnicza różnica dotyczy informacji przechowywanej w stanie.
Dotychczas <math> s(x) \in \mathbf{Num} </math>, gdyż podwyrażenie <math> e </math> w
<math> \mathbf{let}\, x = e \,\mathbf{in}\, \ldots </math> obliczało sie natychmiast.
Jeśli chcemy opóżnic obliczenie tego podwyrażenia, to w
<math> s(x) </math> powinniśmy zapamiętać całe (nieobliczone) wyrażenie <math> e </math>
wraz ze stanem bieżącym.
Czyli
<math>
\mathbf{State} \, = \, \mathbf{Var} \to \mathbf{Exp} \times \mathbf{State}.
</math>
Odpowiednia reguła dla wyrażenia <math> \mathbf{let}\, </math> to
<math>
\mathbf{let}\, x = e_1 \,\mathbf{in}\, e_2, s \,\Longrightarrow\, e_2, s[x \mapsto (e_1, s)].
</math>
Uważnego czytelnika zapewne zaniepokoił fakt, że <math> \mathbf{State} </math>
stoi zarówno po lewej jak i po prawej stronie równania
<math>
\mathbf{State} \, = \, \mathbf{Var} \to \mathbf{Exp} \times \mathbf{State}.
</math>
Również zapis <math> s[x \mapsto (e_1, s)] </math> może wzbudzić niepokój,
gdyż sugeruje on, iż <math> s(x) </math> zawiera, jako jeden z elementów pary,
obiekt ''tego samego typu'' co <math> s </math>.
Formalnego rozwiązania tego typu dylematów dostarcza teoria dziedzin.
Natomiast na użytek semantyki operacyjnej wystarczy, jeśli
uznamy, iż równanie
<math>
\mathbf{State} \, = \, \mathbf{Var} \to \mathbf{Exp} \times \mathbf{State}
</math>
stanowi skrótowy zapis następującej definicji.
Zdefiniujmy <math> \mathbf{State}_0, \mathbf{State}_1, \ldots </math> następująco:
<math>
\mathbf{State}_{0} = \{ \emptyset \}
</math>
<math>
\mathbf{State}_{n+1} = \mathbf{Var} \to_{\mathrm{fin}} \mathbf{Exp} \times \mathbf{State}_{n}
</math>
....
=== Zadania domowe ===
==== Zadanie 4 ====
Dotychczas wystąpienie błędu podczas obliczania wyrażenia,
np. odwołanie do niezainicjowanej zmiennej, powodowało, że
wyrażenie nie posiadało wartości (nie było ciągu tranzycji
prowadzących do konfiguracji końcowej). Zmodyfikuj którąś z semantyk
z poprzednich zadań tak, aby błąd był komunikowany
jako jedna z konfiguracji końcowych. To znaczy: jeśli obliczenie
wyrażenia <math> e </math> w stanie <math> s </math> jest niemożliwe bo wystąpił
błąd, to
<math>
e, s \,\Longrightarrow^{*}\, Blad
</math>
==== Zadanie 5 ====
Rozważ rozszerzenie języka z zadania 2 o wyrażenia boolowskie:
<math>
n \, ::= \,\, 0 \,\,|\,\, 1 \,\,|\,\, \ldots
</math>
<math>
x \, ::= \,\, \ldots \, (identyfikatory) \, \ldots
</math>
<math>
b \, ::= \,\,
        \mathbf{true}  \,\,|\,\,
        \mathbf{true}  \,\,|\,\,
        e_1 \leq e_2  \,\,|\,\,
        \neg b  \,\,|\,\,
        b_1  \land  b_2
</math>
<math>
e \,  ::=  \,\, 
        n  \,\,|\,\,
        x  \,\,|\,\,
        e_1 + e_2  \,\,|\,\,
        \mathbf{if}\, b \,\mathbf{then}\, e_2 \,\mathbf{else}\, e_3  \,\,|\,\,
        \mathbf{let}\, x = e_1 \,\mathbf{in}\, e_2
</math>
Zaproponuj semantykę małych kroków dla tego języka.

Wersja z 17:17, 26 lip 2006


Ćwiczenia 1: semantyka operacyjna wyrażeń (małe kroki)

Zadania z rozwiązaniami

Zadanie 1 (przygotowawcze)

Rozważmy bardzo prosty język wyrażeń, którego składnia opisana jest następującą gramatyką:

n::=0|1|

x::=(identyfikatory)

e::=n|x|e1+e2|𝐢𝐟e1𝐭𝐡𝐞𝐧e2𝐞𝐥𝐬𝐞e3

Wynikiem wyrażenienia warunkowego 𝐢𝐟e1𝐭𝐡𝐞𝐧e2𝐞𝐥𝐬𝐞e3 jest wartość wyrażenia e2, o ile wyrażenie e1 oblicza się do wartości różnej od zera; w przeciwnym przypadku wynikiem jest wartość wyrażenia e3.

Zaproponuj semantykę operacyjną (małe kroki) dla tego języka.


Rozwiązanie

Zacznijmy od ustalenia notacji i dziedzin syntaktycznych. Niech 𝐍𝐮𝐦 oznacza zbiór stałych liczbowych, n𝐍𝐮𝐦={0,1,}. Podobnie, niech 𝐕𝐚𝐫 oznacza zbiór identyfikatorów, które mogą być nazwami zmiennych; x𝐕𝐚𝐫. Wreszcie, niech 𝐄𝐱𝐩 oznacza zbiór wyrażeń; e𝐄𝐱𝐩. Dla ułatwienia zapisywania reguł zakładamy, ze stałe liczbowe sa wyrażeniami, czyli 𝐍𝐮𝐦𝐄𝐱𝐩.

Będziemy potrzebować zbioru stanów, opisujących wartości przypisane zmiennym. Najprostszym rozwiązaniem jest przyjąc, że stan to funkcja z 𝐕𝐚𝐫 do 𝐍𝐮𝐦. Oznaczmy przez 𝐒𝐭𝐚𝐭𝐞 zbiór wszystkich takich funkcji; stany oznaczac będziemy przez s,s1,s,𝐒𝐭𝐚𝐭𝐞.

W naszej semantyce będziemy potrzebowac tranzycji dwóch postaci. Po pierwsze, tranzycja

e,se,s

oznaczająca mały krok w trakcie obliczania wyrażenia e w stanie s, w wyniku którego e wyewoluowało do e. Stan nie ulega zmiania podczas obliczania wyrażenia, więc to samo s figuruje po lewej i prawej stronie strzałki.

Po drugie, tranzycja e,sn

będzie oznaczaczać, że wyrażenie e jest już policzone, a jego wartością jest n.

Zatem przyjmijmy, że zbiór konfiguracji to

(𝐄𝐱𝐩×𝐒𝐭𝐚𝐭𝐞)𝐍𝐮𝐦

a konfiguracje końcowe to 𝐍𝐮𝐦.

Uwaga: Tranzycje pierwszej postaci mogłyby również wyglądać następująco: e,se. Wtedy zbiorem konfiguracji byłby zbiór (𝐄𝐱𝐩×𝐒𝐭𝐚𝐭𝐞)𝐄𝐱𝐩 a konfiguracje końcowe pozostałyby bez zmian. (koniec uwagi)


Najprostsze są tranzycje prowadzące do konfiguracji końcowej:

n,sn

Zmienna oblicza się do swojej wartości w bieżącym stanie:

x,sn,s o ile s(x)=n.

Teraz zajmiemy się dodawaniem e1+e2. Ponieważ semantyka jest w stylu małych kroków, musimy zdecydować się czy najpierw obliczyć pierwszy (lewy) składnik e1 czy drugi? Jeśli wybierzemy lewy (strategia lewostronna), otrzymamy regułę:

e1,se'1,se1+e2,se'1+e2,s.

Czyli mały krok w e1 stanowi też mały krok w e1+e2. Po zakończeniu obliczania e1 przechodzimy do e2:

e2,se'2,sn+e2,sn+e'2,s.

A na końcu dodajemy:

n1+n2,sn,s o ile n=n1+n2.

Zauważmy tutaj pewną subtelność, dotyczącą dwóch wystąpień symbolu +: pierwsze wystąpienie oznacza jedną z konstrukcji składniowych języka, a drugie oznacza operację dodawania w zbiorze 𝐍𝐮𝐦. Pozwalamy sobie na taką kolizję oznaczeń, gdyż nie powinna ona prowadzić do niejednoznaczności. Pamiętajmy, że składnia języka jest składnią abstrajkcyjną, więc zamiast e1+e2 moglibyśmy równie dobrze pisać np. add(e1,e2).

Inna możliwą strategią obliczania e1+e2 jest strategia prawostronna, którą otrzymujemy zastępując pierwsze dwie z trzech powyższych reguł przez:

e2,se'2,se1+e2,se1+e'2e1,se'1,se1+n,se'1+n,s.

Ponadto, jeśli przyjmiemy regułę pierwszą (dla e1), trzecią i czwartą (dla e2), otrzymamy strategię równoległą, polegającą na obliczaniu jednocześnie e1 i e2:

e1,se'1,se1+e2,se'1+e2,se2,se'2,se1+e2,se1+e'2n1+n2,sn,s o ile n=n1+n2.

Bardziej precyzyjnie mówiąc, małe kroki obliczające obydwa podwyrażenia przeplatają się, i to w dowolny sposób. Ta dowolność prowadzi do niedeterminizmu, czyli do sytuacji, gdy kolejna (następna) konfiguracja nie jest wyznaczona jednoznacznie. Jest tak, gdyż możemy mieć do wyboru dwie różne tranzycje

e1+e2,se'1+e2,se1+e2,se1+e'2,s.

Zauważmy natomiast, że kolejność przeplatania się małych kroków obliczających e1 i e2 nie wpływa w tym przypadku na końcową wartość całego wyrażenia.

Na koniec reguły dla wyrażenia warunkowego.

e1,se'1,s𝐢𝐟e1𝐭𝐡𝐞𝐧e2𝐞𝐥𝐬𝐞e3,s𝐢𝐟e'1𝐭𝐡𝐞𝐧e2𝐞𝐥𝐬𝐞e3,s

𝐢𝐟n𝐭𝐡𝐞𝐧e2𝐞𝐥𝐬𝐞e3,se2,s o ile n0

𝐢𝐟n𝐭𝐡𝐞𝐧e2𝐞𝐥𝐬𝐞e3,se3,s o ile n=0


Zadanie 2

Rozszerzmy język wyrażeń z poprzedniego zadania o jedną konstrukcję

e::=|𝐥𝐞𝐭x=e1𝐢𝐧e2

Wyrażenie 𝐥𝐞𝐭x=e1𝐢𝐧e2 zawiera w sobie deklarację x=e1, która stanowi mechanizm wiązania identyfikatorów w naszym języku. Deklaracja x=e1 wprowadza nową zmienną x oraz przypisuje jej wartość. Wartość wyrażenia 𝐥𝐞𝐭x=e1𝐢𝐧e2 obliczamy następująco: najpierw oblicza się wartość e1, podstawia ją na zmienna x, a następnie oblicza wyrażenie e2. Zakresem zmiennej x jest wyrażenie e2, czyli wewnątrz e2 można odwoływać się (wielokrotnie) do zmiennej x; Ogólniej, odwołania do zmiennej w wyrażeniu odnoszą się do najbliższej (najbardziej zagnieżdzonej) deklaracji tej zmiennej. Taki mechanizm wiązania identyfikatorów nazywamy wiązaniem statycznym. Przyjmujemy zwykłe reguły przesłaniania zmiennych. Np., jeśli w e2 występuje podwyrażenie 𝐥𝐞𝐭x=𝐢𝐧e to odwołania do x wewnątrz e odnoszą się do najbliższej deklaracji zmiennej x.

Zakładamy, że na początku wartości wszystkich zmiennych są nieokreślone, czyli zmienne są niezainicjowane, a odwołanie do niezainicjowanej zmiennej jest uważane za niepoprawne.


Przykłady

𝐥𝐞𝐭x=z+z𝐢𝐧𝐥𝐞𝐭y=7𝐢𝐧𝐥𝐞𝐭x=y+3𝐢𝐧x+x+ywynik=24 𝐥𝐞𝐭y=5𝐢𝐧𝐥𝐞𝐭x=(𝐥𝐞𝐭y=3𝐢𝐧y+y)𝐢𝐧x+ywynik=11 Parser nie mógł rozpoznać (błąd składni): {\displaystyle \mathbf{let}\, z = 5 \,\mathbf{in}\, x+z \quad \quad \mapsto \quad \quad \mbox{brak wyniku; odwołanie do niezainicjowanej zmiennej}\, x } 𝐥𝐞𝐭x=1𝐢𝐧𝐥𝐞𝐭x=x+x𝐢𝐧x+xwynik=4


Rozwiązanie

Podobnie jak poprzednio, stan powinien opisywać wartości przypisane zmiennym, ale powinniśmy też uwzględnić niezainicjowane zmienne, czyli zmienne bez żadnej wartości. Przyjmijmy zatem, że stan to skończona funkcja częściowa z 𝐕𝐚𝐫 do 𝐍𝐮𝐦. Oznaczmy symbolem 𝐒𝐭𝐚𝐭𝐞 zbiór wszystkich takich funkcji: 𝐒𝐭𝐚𝐭𝐞=𝐕𝐚𝐫fin𝐍𝐮𝐦. Naturalnym stanem początkowym jest stan pusty, tzn. pusta funkcja częściowa, który będziemy oznaczać . A wartość wyrażenia e w stanie początkowym wynosi n o ile zachodzi:

e,*n.

Będziemy potrzebowac tranzycji dwóch postaci, podobnie jak poprzednio, ale pierwsza postać będzie trochę ogólniejsza:

e,se,s.

Tranzycja ta oznacza mały krok w trakcie obliczania wyrażenia e w stanie s, w wyniku którego e wyewoluowało do e a nowym stanem jest s. Stan może się teraz zmienić na skutek deklaracji zmiennych.

Spróbujmy rozszerzyc semantykę z poprzedniego zadania. Ponieważ stan jest funkcją częściową, musimy zmienić niektóre reguły, np.

x,sn,s o ile s(x) jest określone i s(x)=n

Następnie dodajemy reguły dla wyrażenia 𝐥𝐞𝐭x=e1𝐢𝐧e2. Gdy e1 jest już obliczne, wyatarczy reguła:

𝐥𝐞𝐭x=n𝐢𝐧e2,se2,s[xn].

Notacja s[xn] oznacza stan s, który zmodyfikowano przypisując zmiennej x wartość n, niezależnie od tego, czy s(x) było określone, czy nie, i pozostawiając niezmienione wartości dla pozostałych zmiennych. Formanie

s[xn](y)={ny=xs(y)yx


Dla yx zachodzi s[xn](y)=s(y) (w szczególności, s[xn](y) jest określone wtedy i tylko wtedy, gdy s(y) jest określone).

Natomiast aby obliczyc e1 potrzebujemy reguły:

e1,se'1,s𝐥𝐞𝐭x=e1𝐢𝐧e2,s𝐥𝐞𝐭x=e'1𝐢𝐧e2,s

Zwróćmy uwagę, że stan s może być różny od s, np. dlatego, że wewnątrz e1 znajduje się podwyrażenie 𝐥𝐞𝐭y=.

Pytanie: czy taka semantyka jest poprawna?

Niestety nie, gdyż nie uwzględniamy ograniczonego zasięgu zmiennej. Rzućmy okiem na przykład:

𝐥𝐞𝐭x=(𝐥𝐞𝐭z=4𝐢𝐧z+z+z)inz

Według naszych intencji to wyrażenie nie ma wartości, gdyż ostatnie odwołanie do z jest błędne. Natomiast według powyższych reguł mamy

𝐥𝐞𝐭x=(𝐥𝐞𝐭z=4inz+z+z)𝐢𝐧z,𝐥𝐞𝐭x=z+z+z𝐢𝐧z,[z4]𝐥𝐞𝐭x=12𝐢𝐧z,[z4]12,[z4]12!

Nasz błąd polega na tym, że po zakończeniu obliczania podwyrażenia 𝐥𝐞𝐭z=4𝐢𝐧z+z+z zapominamy przywrócić zmiennej z poprzednią wartość (a właściwie brak wartości w przykładzie powyżej).

Wygodne i eleganckie rozwiązanie tego problemu jest możliwe, jeśli rozszerzymy składnię naszego języka. Intuicyjnie, reguła

𝐥𝐞𝐭x=n𝐢𝐧e2,se2,s[xn].

powinna zostać zastąpiona przez

𝐥𝐞𝐭x=n𝐢𝐧e2,se2𝐭𝐡𝐞𝐧,,przywróć wartość zmiennej x'',s[xn].

czyli potrzebujemy konstrukcji składniowej, która polega na obliczeniu wyrażenia e2 a następnie na przypisaniu zmiennej x danej wartości. Rozszerzmy zatem składnię następujaco:

e::=|e𝐭𝐡𝐞𝐧x:=n.

Zauważmy, że wyrażenie e𝐭𝐡𝐞𝐧x:=n jest w pewnym sensie dualne do 𝐥𝐞𝐭x=n𝐢𝐧e, gdyż jedyna (choc niewątpliwie istotna) różnica między nimi to kolejność obliczenia e i przypisania wartości na zmienną x. Oto nowa reguła

𝐥𝐞𝐭x=n𝐢𝐧e2,se2𝐭𝐡𝐞𝐧x:=n,s[xn] o ile s(x)=n.

Pewna trudność pojawiają się w sytuacji, gdy s(x) jest nieokreślone, czyli gdy zmienna x jest niezainicjowana -- reguła powyższa nie obejmuje wogóle takiej sytuacji. Najprostszym sposobem rozwiązania tej trudności jest rozszerzenie konstrukcji e𝐭𝐡𝐞𝐧x:=n:

e::=|e𝐭𝐡𝐞𝐧x:=n|e𝐭𝐡𝐞𝐧x:=

gdzie symbol oznacza brak wartości. Dodajemy również regułę:

𝐥𝐞𝐭x=n𝐢𝐧e2,se2𝐭𝐡𝐞𝐧x:=,s[xn] o ile s(x) jest nieokreślone.

Uwaga: Inny sposób rozwiązania omawianej trudności polega na rozszerzeniu zbioru 𝐍𝐮𝐦 o dodatkowy element :

n::=|0|1|

Wtedy nie musimy dodawać osobnego wariantu ostatniej reguły, ale za to n= może sie pojawić w wyrażeniach. Dodatkowo, w tym rozwiązaniu warto poczynić umowę, że s(x)= reprezentuje brak wartości zmiennej x. Wtedy stany są funkcjami całkowitymi z 𝐕𝐚𝐫 w 𝐍𝐮𝐦 przyjmującymi wartość różną od tylko dla skończenie wielu elementów. (koniec uwagi)

Na zakończenie scalamy semantykę dla e𝐭𝐡𝐞𝐧x:=n z semantyką pozostałych wyrażeń:

e,se,se𝐭𝐡𝐞𝐧x:=n,se𝐭𝐡𝐞𝐧x:=n,s

n𝐭𝐡𝐞𝐧x:=n,sn,s[xn]


Zadanie 3

Zmodyfikuj semantykę z poprzedniego zadania tak, aby uzyskać leniwą ewaluację wyrażeń, zgodnie z dyrektywą: nie obliczaj wyrażenia o ile jego wynik nie jest potrzebny (albo: obliczaj wartość wyrażenia dopiero wtedy, gdy jego wynik jest naprawdę potrzebny). Spójrzmy na przykład:

𝐥𝐞𝐭x=7𝐢𝐧𝐥𝐞𝐭y=y+y𝐢𝐧x+x

Według semantyki z poprzedniego zadania wyrażnie to nie ma wartości, bo w deklaracji y=y+y jest odwołanie do niezainicjowanej zmiennej. Natomiast w semantyce leniwej wyrażenie to obliczy się do wartości 14, gdyż wyrażenie y+y nie będzie wogóle obliczane. Będzie tak dlatego, że w wyrażeniu x+x nie ma odwołań do zmiennej y.


Rozwiązanie

Semantyka będzie bardzo podobna do tej z poprzedniego zadania. Zasadnicza różnica dotyczy informacji przechowywanej w stanie. Dotychczas s(x)𝐍𝐮𝐦, gdyż podwyrażenie e w 𝐥𝐞𝐭x=e𝐢𝐧 obliczało sie natychmiast. Jeśli chcemy opóżnic obliczenie tego podwyrażenia, to w s(x) powinniśmy zapamiętać całe (nieobliczone) wyrażenie e wraz ze stanem bieżącym. Czyli

𝐒𝐭𝐚𝐭𝐞=𝐕𝐚𝐫𝐄𝐱𝐩×𝐒𝐭𝐚𝐭𝐞.

Odpowiednia reguła dla wyrażenia 𝐥𝐞𝐭 to

𝐥𝐞𝐭x=e1𝐢𝐧e2,se2,s[x(e1,s)].

Uważnego czytelnika zapewne zaniepokoił fakt, że 𝐒𝐭𝐚𝐭𝐞 stoi zarówno po lewej jak i po prawej stronie równania 𝐒𝐭𝐚𝐭𝐞=𝐕𝐚𝐫𝐄𝐱𝐩×𝐒𝐭𝐚𝐭𝐞. Również zapis s[x(e1,s)] może wzbudzić niepokój, gdyż sugeruje on, iż s(x) zawiera, jako jeden z elementów pary, obiekt tego samego typu co s. Formalnego rozwiązania tego typu dylematów dostarcza teoria dziedzin. Natomiast na użytek semantyki operacyjnej wystarczy, jeśli uznamy, iż równanie 𝐒𝐭𝐚𝐭𝐞=𝐕𝐚𝐫𝐄𝐱𝐩×𝐒𝐭𝐚𝐭𝐞 stanowi skrótowy zapis następującej definicji. Zdefiniujmy 𝐒𝐭𝐚𝐭𝐞0,𝐒𝐭𝐚𝐭𝐞1, następująco:

𝐒𝐭𝐚𝐭𝐞0={}

𝐒𝐭𝐚𝐭𝐞n+1=𝐕𝐚𝐫fin𝐄𝐱𝐩×𝐒𝐭𝐚𝐭𝐞n

....


Zadania domowe

Zadanie 4

Dotychczas wystąpienie błędu podczas obliczania wyrażenia, np. odwołanie do niezainicjowanej zmiennej, powodowało, że wyrażenie nie posiadało wartości (nie było ciągu tranzycji prowadzących do konfiguracji końcowej). Zmodyfikuj którąś z semantyk z poprzednich zadań tak, aby błąd był komunikowany jako jedna z konfiguracji końcowych. To znaczy: jeśli obliczenie wyrażenia e w stanie s jest niemożliwe bo wystąpił błąd, to

e,s*Blad


Zadanie 5

Rozważ rozszerzenie języka z zadania 2 o wyrażenia boolowskie:

n::=0|1|

x::=(identyfikatory)

b::=𝐭𝐫𝐮𝐞|𝐭𝐫𝐮𝐞|e1e2|¬b|b1b2

e::=n|x|e1+e2|𝐢𝐟b𝐭𝐡𝐞𝐧e2𝐞𝐥𝐬𝐞e3|𝐥𝐞𝐭x=e1𝐢𝐧e2

Zaproponuj semantykę małych kroków dla tego języka.