Semantyka i weryfikacja programów/Ćwiczenia 2

Z Studia Informatyczne
Przejdź do nawigacjiPrzejdź do wyszukiwania


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 semantyki języka Tiny

Zadanie 1

Semantyka języka Tiny z wykładu używała funkcji semantycznych B,E:StateState dla określenia 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:

b::=l|e1e2|¬b|b1b2|

l::=𝐭𝐫𝐮𝐞|𝐭𝐫𝐮𝐞

e::=n|x|e1+e2|

n::=0|1|

x::=(identyfikatory)

Niech 𝐁𝐄𝐱𝐩 oznacza zbiór wyrażeń boolowskich, b𝐁𝐄𝐱𝐩. Chcemy, aby tranzycje dla wyrażeń były postaci:

e,se,s

i podobnie dla wyrażeń boolowskich:

b,sb,s

gdzie s𝐒𝐭𝐚𝐭𝐞. Dodatkowo będziemy potrzebować również tranzycji postaci:

e,sn oraz b,sl

gdzie n jest liczbą całkowitą, n𝐍𝐮𝐦, a l𝐁𝐨𝐨𝐥={𝐭𝐫𝐮𝐞,𝐭𝐫𝐮𝐞}. Formalnie, zbiór konfiguracji dla semantyki całego języka Tiny to

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

a konfiguracje końcowe to 𝐒𝐭𝐚𝐭𝐞; aczkolwiek konfiguracje ze zbioru 𝐍𝐮𝐦𝐁𝐨𝐨𝐥 pełnią podobną rolę dla wyrażeń arytmetycznych i boolowskich jako konfiguracje końcowe dla instrukcji. Przypomnijmy, że 𝐒𝐭𝐦𝐭 oznacza zbiór instrukcji, I𝐒𝐭𝐦𝐭.

Zacznijmy od (chyba najprostszych) tranzycji dla stałych boolowskich i liczbowych:

l,sln,sn.

Podobnie jak poprzednio, zakładamy tutaj dla wygody, że 𝐍𝐮𝐦subseteq𝐄𝐱𝐩 oraz 𝐁𝐨𝐨𝐥𝐁𝐄𝐱𝐩. Pozwala nam to nie odróżniać stałych występujących w wyrażeniach a zatem pojawiających się po lewej stonie tranzycji mkrok od wartości im odpowiadających pojawiających się po prawej stronie.

Przejdźmy do spójników logicznych, powiedzmy b1b2. Ponieważ opisujemy teraz pojedyncze (małe) kroki składające się na wykonanie programu, musimy podać w jakiej kolejności będą się obliczać b1 i b2. Zacznijmy od strategii lewostronnej:

b1,sb'1,sb1b2,sb'1b2,sb2,sb'2,sl1b2,sl1b2,sl1l2,sl,s o ile l=l1l2

Możemy zaniechać obliczania b2 jeśli b1 oblicza się do false. Oto odpowiednio zmodyfikowane reguły:

b1,sb'1,sb1b2,sb'1b2,s𝐭𝐫𝐮𝐞b2,s𝐭𝐫𝐮𝐞,s𝐭𝐫𝐮𝐞b2,sb2,s

Analogicznie reguły prawostronne to:

b2,sb'2,sb1b2,sb1b'2,sb1𝐭𝐫𝐮𝐞,s𝐭𝐫𝐮𝐞,sb1𝐭𝐫𝐮𝐞,sb1,s

Reguły równoległe otrzymujemy jako sumę reguł lewo- i prawostronnych (w sumie 6 reguł). Zauważmy, że obliczanie wyrażeń b1 i b2 odbywa się teraz w twz. przeplocie: Pojedynczy krok polega na wykonaniu jednego kroku obliczenia b1 albo jednego kroku obliczenia b2. Zwróćmy też uwagę, że nasze tranzycje nie posiadają teraz własności determinizmu: wyrażenie b1b2 może wyewoluować w pojedyńczym kroku albo do b'1b2 albo do b1b'2. Na szczęście, końcowy wynik, do jakiego oblicza się wyrażenie jest zawsze taki sam, niezależnie od przeplotu.

Oto reguły dla negacji:

¬𝐭𝐫𝐮𝐞,s𝐭𝐫𝐮𝐞,s¬𝐭𝐫𝐮𝐞,s𝐭𝐫𝐮𝐞,sb,sb,s¬b,s¬b,s

Reguły dla e1e2 są następujące:

e1,se'1,se1e2,se'1e2,se2,se'2,se1e2,se1e'2,sn1n2,s𝐭𝐫𝐮𝐞,s o ile n1n2n1n2,s𝐭𝐫𝐮𝐞,s o ile n1>n2

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.

Jako pierwszą z instrukcji rozważmy przypisanie. Najpierw obliczamy wyrażenie po prawej stronie przypisania, a gdy wyrażenie to wyewoluuje do stałej (obliczy się), modyfikujemy stan:

e,se,sx:=e,sx:=e,sx:=n,ss[xn]

Rozważmy teraz instrukcję warunkową i instrukcję pętli. Najpierw obliczamy wartość dozoru:

b,sb,s𝐢𝐟b𝐭𝐡𝐞𝐧I1𝐞𝐥𝐬𝐞I2,s𝐢𝐟b𝐭𝐡𝐞𝐧I1𝐞𝐥𝐬𝐞I2,sb,sb,s𝐰𝐡𝐢𝐥𝐞b𝐝𝐨I,s𝐰𝐡𝐢𝐥𝐞b𝐝𝐨I,s

a gdy dozór jest już obliczony, podejmujemy decyzję. W przypadku instrukcji warunkowej reguły są oczywiste:

𝐢𝐟𝐭𝐫𝐮𝐞𝐭𝐡𝐞𝐧I1𝐞𝐥𝐬𝐞I2,sI1,s𝐢𝐟𝐭𝐫𝐮𝐞𝐭𝐡𝐞𝐧I1𝐞𝐥𝐬𝐞I2,sI2,s

Gorzej jest w przypadku instrukcji pętli. Reguła mogłaby wyglądać tak:

𝐰𝐡𝐢𝐥𝐞𝐭𝐫𝐮𝐞𝐝𝐨I,sI;𝐰𝐡𝐢𝐥𝐞?𝐝𝐨I,s

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):

b,s*𝐭𝐫𝐮𝐞𝐰𝐡𝐢𝐥𝐞b𝐝𝐨I,sI;𝐰𝐡𝐢𝐥𝐞b𝐝𝐨I,sb,s*𝐭𝐫𝐮𝐞𝐰𝐡𝐢𝐥𝐞b𝐝𝐨I,ss

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ę 𝐰𝐡𝐢𝐥𝐞, zanim obliczymy wartość dozoru b. Jedyną reguła dla pętli 𝐰𝐡𝐢𝐥𝐞 byłaby wtedy reguła:

𝐰𝐡𝐢𝐥𝐞b𝐝𝐨I,s𝐢𝐟b𝐭𝐡𝐞𝐧(I;𝐰𝐡𝐢𝐥𝐞b𝐝𝐨I)𝐞𝐥𝐬𝐞𝐬𝐤𝐢𝐩,s.

Dzięki temu obliczany warunek logiczny b jest zawsze jednorazowy. Znalezienie innych rozwiązań, np. opartych na rozszerzeniu składni, pozostawiamy dociekliwemu Czytelnikowi.

Reguły dla operacji arytmetycznych również pozostawiamy do napisania Czytelnikowi.


Zadanie 2

Rozszerzmy język Tiny o następujące dobrze znane konstrukcje iteracji:

I::=𝐫𝐞𝐩𝐞𝐚𝐭I𝐮𝐧𝐭𝐢𝐥b|𝐟𝐨𝐫x:=e1𝐭𝐨e2𝐝𝐨I|𝐝𝐨e𝐭𝐢𝐦𝐞𝐬I|𝐝𝐨I𝐰𝐡𝐢𝐥𝐞b

Napisz semantykę małych kroków dla powyższych konstrukcji.


Rozwiązanie

Instrukcja 𝐫𝐞𝐩𝐞𝐚𝐭

Zacznijmy od pętli 𝐫𝐞𝐩𝐞𝐚𝐭I𝐮𝐧𝐭𝐢𝐥b. Przyjrzyjmy się dwóm podejściom, które zastosowaliśmy dla pętli 𝐰𝐡𝐢𝐥𝐞 w poprzednim zadaniu. Po pierwsze rozwinięcie:

𝐫𝐞𝐩𝐞𝐚𝐭I𝐮𝐧𝐭𝐢𝐥b,sI;𝐢𝐟b𝐭𝐡𝐞𝐧(𝐫𝐞𝐩𝐞𝐚𝐭I𝐮𝐧𝐭𝐢𝐥b)𝐞𝐥𝐬𝐞𝐬𝐤𝐢𝐩,s.

Po drugie, spróbujmy odwołać się do * dla dozoru pętli b:

I,sI,s𝐫𝐞𝐩𝐞𝐚𝐭I𝐮𝐧𝐭𝐢𝐥b,s𝐫𝐞𝐩𝐞𝐚𝐭I𝐮𝐧𝐭𝐢𝐥b,s
I,ssb,s*𝐭𝐫𝐮𝐞𝐫𝐞𝐩𝐞𝐚𝐭I𝐮𝐧𝐭𝐢𝐥b,ssI,ssb,s*𝐭𝐫𝐮𝐞𝐫𝐞𝐩𝐞𝐚𝐭I𝐮𝐧𝐭𝐢𝐥b,s𝐫𝐞𝐩𝐞𝐚𝐭?𝐮𝐧𝐭𝐢𝐥b,s

Okazuje się, że jest jeszcze gorzej niż w przypadku pętli 𝐰𝐡𝐢𝐥𝐞: nie pamiętamy już, jaka była instrukcja wewnętrzna naszej pętli! Czyli takie podejście jest teraz nieskuteczne.


Instrukcja 𝐝𝐨e𝐭𝐢𝐦𝐞𝐬I

Pętla 𝐝𝐨e𝐭𝐢𝐦𝐞𝐬I, w stanie s, oznacza wykonianie instrukcji I n razy, gdzie n to wartość, do której oblicza się e w stanie s. Czyli najpierw obliczmy e przy pomocy reguły:

e,se,s𝐝𝐨e𝐭𝐢𝐦𝐞𝐬I,sDe𝐭𝐢𝐦𝐞𝐬I,s

która doprowadza nas do konfiguracji:

𝐝𝐨n𝐭𝐢𝐦𝐞𝐬I,s.

Teraz jest juz łatwo:

𝐝𝐨n𝐭𝐢𝐦𝐞𝐬I,sI;𝐝𝐨n1𝐭𝐢𝐦𝐞𝐬I,s o ile n>0

𝐝𝐨n𝐭𝐢𝐦𝐞𝐬I,ss o ile n=0.


Instrukcja 𝐟𝐨𝐫

W przypadku pętli 𝐟𝐨𝐫 przyjmijmy, że wartości wyrażeń e1 i e2 obliczane są przed pierwszą iteracją pętli. Dodatkowo ustalmy, że e1 będzie obliczone jako pierwsze. Czyli:

e1,se'1,s𝐟𝐨𝐫x=e1𝐭𝐨e2𝐝𝐨I,s𝐟𝐨𝐫x=e'1𝐭𝐨e2𝐝𝐨I,s
e2,se'2,s𝐟𝐨𝐫x=n𝐭𝐨e2𝐝𝐨I,s𝐟𝐨𝐫x=n𝐭𝐨e'2𝐝𝐨I,s

Zatem zakres zmiennej x mamy już obliczony, tzn. jesteśmy w konfiguracji

𝐟𝐨𝐫x=n1𝐭𝐨n2𝐝𝐨I,s.

Dalsze reguły mogą być podobne do reguł dla pętli 𝐝𝐨n𝐭𝐢𝐦𝐞𝐬I:

𝐟𝐨𝐫x=n1𝐭𝐨n2𝐝𝐨I,sx:=n1;I;𝐟𝐨𝐫x=n1+1𝐭𝐨n2𝐝𝐨I,s o ile n1n2
𝐟𝐨𝐫x=n1𝐭𝐨n2𝐝𝐨I,s𝐬𝐤𝐢𝐩,s o ile n1>n2


Zauważmy, wartość zmiennej x po zakończeniu pętli wynosi n2. Ponieważ nie zostało wyspecyfikowane jaka powinna być wartość tej zmiennej, taką semantykę uważamy za poprawną.


Semantykę dla 𝐝𝐨I𝐰𝐡𝐢𝐥𝐞b pozostawiamy Czytelnikowi jako proste ćwiczenie. Oczywiście minimalistyczne rozwiązanie to

𝐝𝐨I𝐰𝐡𝐢𝐥𝐞b,s𝐫𝐞𝐩𝐞𝐚𝐭I𝐮𝐧𝐭𝐢𝐥¬b,s


Kalkulator binarny

Zadanie 1

Rozważmy następujący język wyrażeń (liczby binarne z dodawaniem):

e::=ϵ|e0|e1|e1+e2

ϵ oznacza słowo puste, czyli np. ϵ1011 oznacza binarną liczbę 1011. 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:

e10+e20(e1+e2)0
e10+e21(e1+e2)1
e11+e20(e1+e2)1

Ale co zrobić z przeniesieniem?

e11+e21?

Podstawowy pomysł polega na potraktowaniu przeniesienia jak dodatkowego składnika:

e11+e21((e1+e2)+ϵ1)0

Zauważmy, że w składni dopuszcza się dowolne przeplatanie operatora dodawania i bitów 0,1. Tę dowolność wykorzystaliśmy właśnie w regułach powyżej. Gdyby nasz język ograniczyć tylko do składni

e::=b|e1+e2

b::=ϵ|b0|b1

(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 obydwa składniki mają tyle samo cyfr, do zakończenia dodawania potrzebujemy reguły:

ϵ+ϵϵ.

Gdy jeden ze składników ma mniej cyfr niż drugi, potrzebujemy reguł:

ϵ+e0e0ϵ+e1e1

oraz ich odpowiedników:

e0+ϵe0e1+ϵe1.

Niestety, nie możemy użyć reguły przemienności:

e1+e2e2+e1

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:

e1e'1e1+e2e'1+e2e2e'2e1+e2e1+e'2eee0e0eee1e1


Zadanie 2

Rozszerzmy składnię o jeden symbol p oznaczający przepełnienie:

e::=ϵ|p|e0|e1|e1+e2.

Na przykład p101 oznacza tę samą liczbę co ϵ101, 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. (e1+e2)0, 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 p100). 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:

p+e0e0p+e1e1e0+pe0e1+pe1.

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

ϵ+e0e0ϵ+e1e1e0+ϵe0e1+ϵe1.

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:

p+pp.

Ale co należy zrobić, gdy tylko jeden ze składników odnotował przepełnienie? p+ϵ? 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:

p+ϵϵϵ+pϵ.

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

e11+e21((e1+e2)+p1)0.

Nowy sztuczny składnik p1 zawiera jakby na wszelki wypadek informacje o potencjalnym przepełnieniu. Jeśli którykolwiek z pozostałych składników e1 i e2 ma przynajmniej jedną cyfrę, to p zostanie usunięte. W przeciwnym wypadku symbol p 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

Rozważ inną semantykę pętli 𝐟𝐨𝐫x=e1𝐭𝐨e2doI, w której wyrażenie e2 jest obliczane na nowo przed każdą iteracją pętli. Napisz reguły semantyczne dla tej instrukcji, nie odwołując się do innych istrukcji pętli.

Zadanie 3

Dodaj do wyrażeń binarnych operację odejmowania.


Zadanie 4

Zaproponuj semantykę przypisania równoległego

x1:=e1||||xn:=en

polegającego na obliczeniu najpierw wartości wyrażeń e1,,en a następnie na podstawieniu nowych wartości na zmienne x1,,xn.