Semantyka i weryfikacja programów/Ćwiczenia 2
Zawartość
Ćwiczymy dalej semantykę małych kroków. Uzupełnimy semantykę języka Tiny o semantykę operacyjną wyrażeń boolowskich i arytmetycznych. Następnie rozszerzymy nasz język o róznorodne konstrukcje iteracji. Na koniec zdefiniujemy operacje arytmetyczne liczb binarnych.
Rozszerzenia języka Tiny
Zadanie 1
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 małych kroków.
Rozwiązanie
Przypomnijmy składnię wyrażeń boolowskich i arytmetycznych:
Chcemy, aby tranzycje dla wyrażeń były postaci: i podobnie dla wyrażeń boolowskich: gdzie .
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:
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 nasze tranzycje nie posiadają teraz 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, czyli ). Możemy odwołać się do tranzytywnego domknięcia relacji (czyli w zadadzie do semantyki dużych kroków):
Takie rozwiązanie nie jest zatem czystą semantyką małych kroków. Istnieją inne możliwe rozwiązania w stylu małych kroków. Jedno z nich oparte jest na pomyśle, aby rozwinąc pętlę jeden raz:
Dzięki temu obliczany warunek logiczny jest zawsze jednorazowy. Znalezienie innych rozwiązań, np. opartych na rozszerzeniu składni, pozostawiamy dociekliwemu czytelnikowi.
Reguły dla operacji arytmetycznych pozostawiamy do napisania Czytelnikowi.
Zadanie 2
Rozszerzmy język Tiny o następujące dobrze znane konstrukcje iteracji:
Napisz semantykę małych kroków dla powyższych konstrukcji.
Rozwiązanie
Zacznijmy od pętli . Przyjrzyjmy się dwóm podejściom, które zastosowaliśmy dla pętli w poprzednim zadaniu. Po pierwsze rozwinięcie:
Po drugie, spróbujmy odwołać się do dla dozoru pętli :
Okazuje się, że jest jeszcze gorzej niż w przypadku pętli : nie pamiętamy już, jaka była instrukcja wewnętrzna naszej pętli!
.....
Semantykę dla pozostawiamy Czytelnikowi jako ćwiczenie. Oczywiście minimalistyczne rozwiązanie to
Kalkulator binarny
Zadanie 1
Rozważmy następujący język wyrażeń (liczby binarne z dodawaniem):
oznacza słowo puste, czyli np. oznacza binarną liczbę 101. Napisz semantykę operacyjną obliczającą wartość wyrażeń.
Rozwiązanie
Składnia wyrażeń pozwala na wygodny dostęp do najmniej znaczącego bitu liczby. Spróbujmy zatem zastosować metodę dodawania pisemnego:
Ale co zrobić z przeniesieniem?
Podstawowy pomysł polega na potraktowaniu przeniesienia jak dodatkowego składnika:
Zauważmy, że w składni dopuszcza się dowolne przeplatanie operatora dodawania i bitów . Tę dowolność wykorzystaliśmy właśnie w regułach powyżej. Gdyby nasz język ograniczyć tylko do składni
(nazwijmy ją składnią ograniczoną) to powyższe reguły byłyby niepoprawne.
Zanim dopiszemy pozostałe reguły, określmy zbiór konfiguracji jako zbiór wyrażeń. Konfiguracje końcowe to wyrażenia bez operatora dodawania (liczby binarne). Nasz pomysł jest taki, że tranzycje stopniowo przesuwają operator dodawania w lewo, aż się go ostatecznie pozbędą.
Gdy jeden ze składników ma mniej cyfr niż drugi, potrzebujemy reguł:
oraz ich odpowiedników:
Niestety, nie możemy użyć reguły przemienności:
gdyż spowodowałaby ona możliwość pętlenia się, a zatem braku wyniku obliczenia.
Na koniec dodajemy typowe reguły opisujące jak krok podwyrażenia indukuje krok całego wyrażenia:
Zadanie 2
Rozszerzmy składnię o jeden symbol oznaczający przepełnienie: Na przykład oznacza tę samą liczbę co , ale z dodatkową informacją, że podczas jej obliczania nastąpiło przepełnienie. Rozumiemy przez to sytuację, gdy wynik ma więcej cyfr niż każdy z argumentów. Cyfry zero z lewej strony (najbardziej znaczące) również uważamy za pełnoprawne cyfry, nie należy ich usuwać ani dodawać nowych.
Napisz semantykę operacyjną obliczającą wyrażenie wraz z informacja o ewentualnym przepełnieniu. Wynik powinien byc poprawny przynajmniej dla wyrażeń w składni ograniczonej.
Rozwiązanie
Zadanie dotyczy w zasadzie składni ograniczonej, ale jako konfiguracji pośrednich będziemy zapewne potrzebować wyrażeń wykraczających poza tę składnię, np. , podobnie jak w poprzednim zadaniu. Zatem mamy tu do czynienia z typowym zabiegiem rozszerzania składni na użytek semantyki operacyjnej (z tym, że rozszerzenie jest dane z góry i nie musimy go wymyślać :)
Przyjmijmy, że konfiguracjami są dowolne wyrażenia, a konfiguracjami końcowymi wyrażenia bez operatora dodawania (ale teraz mogą to być np. wyrażenia postaci ). Spróbujmy pozostawić wszystkie reguły z poprzedniego zadania. Dodamy tylko kilka nowych reguł, odpowiedzialnych za przepełnienie.
Zacznijmy od najprostszej sytuacji, gdy jeden ze składników ma mniej cyfr, i to ten właśnie składnik odnotował przepełnienie:
W takiej sytuacji oczywiście informacja o przepełnieniu zostaje wymazana. Jeśli przepełnienie zostało odnotowane w składniku o większej liczbie cyfr, to reguły
z poprzedniego zadania są wystarczające.
Rozważmy teraz przypadek, gdy obydwa składniki mają tę samą liczbę cyfr. Jeśli obydwa odnotowały przepełnienie, to oczywiście informacja ta powinna zostać zachowana:
Ale co należy zrobić, gdy tylko jeden ze składników odnotował przepełnienie? Oczywiście, w tej sytuacji żadnego przepełnienia nie ma, ponieważ drugi składnik ma wystarczająco dużo cyfr by je przesłonić. Oto odpowiednie reguły:
Na koniec zostało najważniejsze: kiedy powinniśmy generować sygnał o przepełnieniu? Przypomnijmy sobie reguły dla dadawania pisemnego w poprzednim zadaniu. Jeśli nie ma przeniesienia, to przepełnienie nie może powstać. Natomiast jeśli jest przeniesienie, to stanowi ono potencjalne przepełnienie. Odpowiednia reguła to
Nowy sztuczny składnik zawiera jakby na wszelki wypadek informacje o potencjalnym przepełnieniu. Jeśli którykolwiek z pozostałych składników i ma przynajmniej jedną cyfrę, to zostanie usunięte. W przeciwnym wypadku symbol i przetrwa i będzie poprawnie informował o sytuacji przepełnienia.
Zadania domowe
Zadanie 1
Podaj przykład wyrażenia, które nie policzy się ani przy użyciu strategii lewo- ani prawostronnej, a policzy się przy strategii równoległej.
Zadanie 2
Dodaj do wyrażeń binarnych operację odejmowania.