Semantyka i weryfikacja programów/Ćwiczenia 1

Z Studia Informatyczne
Przejdź do nawigacjiPrzejdź do wyszukiwania

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

Zadanie 1 (przygotowawcze)

Rozważmy 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 Parser nie mógł rozpoznać (błąd składni): {\displaystyle e_1 ] czy drugi? Jeśli wybierzemy lewy, otrzymamy regułę: <math> \frac{e_1, s \,\Longrightarrow\, e'_1, s} {e_1 + e_2, s \,\Longrightarrow\, e'_1 + e_2}. }

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 wszystkie pięc reguł, otrzymamy strategię równoległą, polegającą na obliczaniu jednocześnie e1 i e2. 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ż jednocześnie możemy mieć dwie 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 jedyny mechannizm wiązania identyfikatorów w naszym języku. 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, ale jeśli w e2 występuje podwyrażenie 𝐥𝐞𝐭x=𝐢𝐧e to odwołania do x wewnątrz e odnoszą się do najbliższej (najbardziej zagnieżdzonej) deklaracji zmiennej x. Taki mechanizm wiązania identyfikatorów nazywamy wiązaniem statycznym.

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 \mbox{brak wyniku; odwołanie do niezainicjowanej zmiennej}\, x } 𝐥𝐞𝐭x=1𝐢𝐧𝐥𝐞𝐭x=x+x𝐢𝐧xwynik=2


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 przez 𝐒𝐭𝐚𝐭𝐞 zbiór wszystkich takich funkcji. 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 trankcie 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.

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


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). Spojrzmy 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, ponieważ 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

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