Semantyka i weryfikacja programów/Ćwiczenia 1

Z Studia Informatyczne
Przejdź do nawigacjiPrzejdź do wyszukiwania


Ć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.