Semantyka i weryfikacja programów/Ćwiczenia 1
Ć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: Parser nie mógł rozpoznać (nieznana funkcja „\dkrok”): {\displaystyle e, s \dkrok n b, s \dkrok l } gdzie s \in State, n jest liczbą całkowitą, n \in Int, a l \in Bool = \{ true, false \}. Tranzycja taka oznacza, że wyrażenie e w stanie s wylicza się do wartości n (wyrażenie logiczne b w stanie s wylicza się do l). 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. Stan = Var \to Int. Konfiguracje końcowe pozostają bez zmian (Stan). 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:
Parser nie mógł rozpoznać (nieznana funkcja „\regula”): {\displaystyle \regula{b, s \dkrok true I; while b do I, s \dkrok s'} {while b do I, s \dkrok s'} }
Parser nie mógł rozpoznać (nieznana funkcja „\regula”): {\displaystyle \regula{b, s \dkrok false}{while b do I, s \dkrok s} }
Podobnie dla instrukcji warunkowej. Teraz zajmiemy się tranzycjami dla wyrażeń. Zacznijmy od stalych arytmetycznych: Parser nie mógł rozpoznać (nieznana funkcja „\dkrok”): {\displaystyle n, s \dkrok n, dla n \in Int } Zauważmy, iż celowo nie odróżniamy liczby n \in Int 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, n po lewej stronie to stała reprezentująca liczbę, która widnieje po prawej stronie.
Analogiczne tranzycje dla stałych boolowskich to: Parser nie mógł rozpoznać (nieznana funkcja „\dkrok”): {\displaystyle true, s \dkrok true false, s \dkrok false } Czynimy tu analogiczne założenie, że Bool jest podbiorem wyrażen boolowskich.
Operatory arytmetyczne definiujemy następująco: Parser nie mógł rozpoznać (nieznana funkcja „\regula”): {\displaystyle \regula{e<sub>1</sub>, s \dkrok n<sub>1</sub> e<sub>2</sub>, s \dkrok n<sub>2</sub> n = n<sub>1</sub> + n<sub>2</sub> } {e<sub>1</sub> + e<sub>2</sub>,s \dkrok n} } Czyli aby obliczyć sumę e1 + e2 w stanie s, trzeba najpierw obliczyć e1 i e2 w stanie s, a następnie dodać obliczone wartości. Zauważmy, że nie specyfikujemy kolejności, w jakiej mają się obliczać e1 i e2. I choć tutaj nie ma to żadnego znaczenia, w przyszłości będzie inaczej, gdy jezyk będzie dopuszczał efekty uboczne wyrażeń.
Podobne reguły można napisać dla pozostałych operacji arytmnetycznych, oraz dla spójników logicznych:
Parser nie mógł rozpoznać (nieznana funkcja „\regula”): {\displaystyle \regula{b<sub>1</sub>, s \dkrok l<sub>1</sub> b<sub>2</sub>, s \dkrok l<sub>2</sub> l = l<sub>1</sub> \land l<sub>2</sub>} {b<sub>1</sub> \land b<sub>2</sub>, s \dkrok l} } Oczywiście jeśli b1 oblicza się do false, wartość całego wyrażenia jest false niezależnie od wartości wyrażenia b2.
Czyli jeśli zaczniemy od obliczenia b1 i wynikiem będzie false, to nie ma wogóle potrzeby obliczania b2. Oto odpowiednie reguły (lewo-stronne):
Parser nie mógł rozpoznać (nieznana funkcja „\regula”): {\displaystyle \regula{b<sub>1</sub>, s \dkrok false} {b<sub>1</sub> \land b<sub>2</sub>,s \dkrok false} \regula{b<sub>1</sub>, s \dkrok true b<sub>2</sub>, s \dkrok l} {b<sub>1</sub> + b<sub>2</sub>,s \dkrok l} }
Wybraliśmy następującą kolejność obliczania wyrażeń: najpierw b1, potem b2. 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): Parser nie mógł rozpoznać (nieznana funkcja „\regula”): {\displaystyle \regula{b<sub>1</sub>, s \dkrok false} {b<sub>1</sub> \land b<sub>2</sub>,s \dkrok false} \regula{b<sub>2</sub>, s \dkrok false} {b<sub>1</sub> \land b<sub>2</sub>,s \dkrok false} } Czyli jeśli którekolwiek z podwyrażeń daje wynik false, to taki wynik zyskuje całe wyrażenie. Dodatkowo potrzebujemy jeszcze reguły: Parser nie mógł rozpoznać (nieznana funkcja „\regula”): {\displaystyle \regula{b<sub>1</sub>, s \dkrok true b<sub>2</sub>, s \dkrok true} {b<sub>1</sub> \land b<sub>2</sub>,s \dkrok true} } Zauważmy, że powyższych reguł nie da sie zaimplementować sekwencyjnie: nie wiadomo czy najpierw obliczać b1 czy b2. Reguły te odpowiadają raczej strategii ,,równoległej: obliczaj ,,jednocześnie b1 i b2 albo do pierwszego false, albo aż obydwa się zakończą z wynikiem true.
W naszym prostym języku wszystkie czterech 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: Parser nie mógł rozpoznać (nieznana funkcja „\dkro”): {\displaystyle e, s \dkro e', s } i podobnie dla wyrażeń boolowskich: Parser nie mógł rozpoznać (nieznana funkcja „\dkrok”): {\displaystyle b, s \dkrok b', s } gdzie s \in State. Przyjmijmy na razie takie same konfiguracje i konfiguracje końcowe jak dla semantyki naturalnej.
Zacznijmy od wyrażeń boolowskich.
Parser nie mógł rozpoznać (nieznana funkcja „\mkrok”): {\displaystyle true, s \mkrok true false, s \mkrok false }
Przejdźmy do spójników logicznych, powiedzmy b1 \land b2. Ponieważ opisujemy teraz pojedyncze (małe) kroki składające się na wykonanie programu, musimy podać w jakiej kolejności będą się wykonywać. Zacznijmy od strategii lewostronnej:
Parser nie mógł rozpoznać (nieznana funkcja „\regu”): {\displaystyle \reguła{b<sub>1</sub>, s \mkrok b'<sub>1</sub>, s} {b<sub>1</sub> \land b<sub>2</sub>, s \mkrok b'<sub>1</sub> \land b<sub>2</sub>, s} \reguła{b<sub>2</sub>, s \mkrok b'<sub>2</sub>, s} {l<sub>1</sub> \land b<sub>2</sub>, s \mkrok l<sub>1</sub> \land b<sub>2</sub>, s} l<sub>1</sub> \land l<sub>2</sub> \mkrok l, jeśli l = l<sub>1</sub> \land l<sub>2</sub> }
Podobnie jak poprzednio, możemy zaniechać obliczania b2 jeśli b1 oblicza się do false.
Parser nie mógł rozpoznać (nieznana funkcja „\regu”): {\displaystyle \reguła{b<sub>1</sub>, s \mkrok b'<sub>1</sub>, s} {b<sub>1</sub> \land b<sub>2</sub>, s \mkrok b'<sub>1</sub> \land b<sub>2</sub>, s} false \land b<sub>2</sub>, s \mkrok false true \land b<sub>2</sub>, s \mkrok b<sub>2</sub>,s }
Analogicznie reguły prawostronne to:
Parser nie mógł rozpoznać (nieznana funkcja „\regu”): {\displaystyle \reguła{b<sub>2</sub>, s \mkrok b'<sub>2</sub>, s} {b<sub>1</sub> \land b<sub>2</sub>, s \mkrok b<sub>1</sub> \land b'<sub>2</sub>, s} b<sub>1</sub> \land false, s \mkrok false b<sub>1</sub> \land true, s \mkrok b<sub>1</sub>, s }
Reguły równoległe otrzymujemy jako sumę reguł lewo- i prawostronnych (w sumie 6 reguł).
Oto reguła dla negacji: Parser nie mógł rozpoznać (nieznana funkcja „\mkrok”): {\displaystyle \neg true, s \mkrok false, s \neg false, s \mkrok true, s }
Reguły dla e1 \leq e2 są następujące:
Parser nie mógł rozpoznać (nieznana funkcja „\regula”): {\displaystyle \regula{e<sub>1</sub>, s \mkrok e'<sub>1</sub>, s} {e<sub>1</sub> \leq e<sub>2</sub>, s \mkrok e'<sub>1</sub> \leq e<sub>2</sub>, s} \regula{e<sub>2</sub>, s \mkrok e'<sub>2</sub>, s} {e<sub>1</sub> \leq e<sub>2</sub>, s \mkrok e<sub>1</sub> \leq e'<sub>2</sub>, s} n<sub>1</sub> \leq n<sub>2</sub>, s \mkrok true, s o ile n<sub>1</sub> \leq n<sub>2</sub> n<sub>1</sub> \leq n<sub>2</sub>, s \mkrok false, s o ile n<sub>1</sub> > n<sub>2</sub> }
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 e1 i e2.
Rozważmy teraz instrukcję warunkową i instrukcję pętli. Najpierw obliczamy wartość dozoru:
Parser nie mógł rozpoznać (nieznana funkcja „\regula”): {\displaystyle \regula{b, s \mkrok b', s} {if b then I<sub>1</sub> else I<sub>2</sub>, s \mkrok if b' then I<sub>1</sub> else I<sub>2</sub>, s} \regula{b, s \mkrok b', s} {while b do I, s \mkrok while b' do I, s} <math> a gdy dozór jest już obliczony, podejmujemy decyzję. W przypadku instrukcji warunkowej reguły są oczywiste: <math> if true then I<sub>1</sub> else I<sub>2</sub>, s \mkrok I<sub>1</sub>, s if false then I<sub>1</sub> else I<sub>2</sub>, s \mkrok I<sub>2</sub>, s }
Gorzej jest w przypadku instukcji pętli. Reguła mogłaby wyglądać tak:
Parser nie mógł rozpoznać (nieznana funkcja „\mkrok”): {\displaystyle while true do I, s \mkrok I; while ? do I, s }
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ę więc do tranzycji dużych kroków:
Parser nie mógł rozpoznać (nieznana funkcja „\regula”): {\displaystyle \regula{b, s \dkrok true} {while b do I, s \mkrok I; while b do I, s} \regula{b, s \dkrok false} {while b do I, s \mkrok 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ą:
Parser nie mógł rozpoznać (nieznana funkcja „\regula”): {\displaystyle \regula{e<sub>1</sub>, s \mkrok e'<sub>1</sub>, s} {e<sub>1</sub> + e<sub>2</sub>, s \mkrok e'<sub>1</sub> + e<sub>2</sub>, s} \regula{e<sub>2</sub>, s \mkrok e'<sub>2</sub>, s} {n + e<sub>2</sub>, s \mkrok n + e'<sub>2</sub>, s} n<sub>1</sub> + n<sub>2</sub>, s \mkrok n, s o ile n = n<sub>1</sub> + n<sub>2</sub> }
Zadanie:
Rozważ dodatkowo operację dzielenia: i rozszerz semantyki z poprzedniego zadania tak, by dzielenie przez zero kończyło program. Oprócz stanu wynikiem programu powinna byc informacja o błędzie.
Rozwiązanie:
....