Semantyka i weryfikacja programów/Ćwiczenia 6

From Studia Informatyczne

Zawartość

Ostatnie zajęcia poświęcone semantyce naturalnej. Rozszerzymy język TINY o deklaracje zmiennych i procedur z jednym parametrem. Rozważymy statyczną i dynamiczną widoczność (wiązanie) identyfikatorów oraz różne mechanizmy przekazywania parametrów. Pracujemy ze środowiskami i stanami.


Semantyka naturalna procedur

Ćwiczenie 1 (procedury)

Rozszerzmy język TINY o deklaracje i wywołania procedur:

I \, ::= \,\,       \ldots  \,\,|\,\,      \mathbf{begin}\, d;\, I \,\mathbf{end} \,\,|\,\,      \mathbf{call}\, x_1(x_2)

d \, ::= \,\,      \mathbf{var}\, x = e \,\,|\,\,      \mathbf{proc}\, x_1(x_2);\, I \,\,|\,\,      d_1; \, d_2

Konstrukcja \mathbf{proc}\, x_1(x_2) deklaruje procedurę o nazwie x_1, której parametrem formalnym jest x_2. Zakładamy statyczne wiązanie identyfikatorów i przekazywanie parametrów przez zmienną (to znaczy, że w momencie wywołania procedury powinna zostać przekazana lokacja odpowiadająca parametrowi aktualnemu). Instrukcja \mathbf{call}\, x_1(x_2) wywołuje procedurę x_1 z parametrem aktualnym x_2.


Rozwiązanie

Użyjemy środowisk E \in \mathbf{Env} i stanów s \in \mathbf{Store}:

\mathbf{Store} = \mathbf{Loc} \to_{\mathrm{fin}} \mathbf{Num}  \quad \quad \quad \mathbf{Env}= \mathbf{Var} \to_{\mathrm{fin}} (\mathbf{Loc} \cup \mathbf{Proc}).

Nowym elementem jest zbiór \mathbf{Proc}, potrzebny nam do przechowywania informacji o procedurach.

Informacja na temat procedury, którą musimy przechowywać, to:

  • nazwa parametru formalnego,
  • ciało procedury (instrukcja I \in \mathbf{Stmt}) oraz
  • środowisko, w którym procedura została zadeklarowana.

Zauważmy, że nie musimy (i nie powinniśmy) pamiętać stanu z momentu deklaracji, czyli konkretnych wartości zmiennych wówczas zadeklarowanych. Pamiętamy tylko środowisko, które wyznacza nam wiązanie identyfikatorów (nazw zmiennych i procedur) występujących w ciele procedury z lokacjami (lub opisami procedur z \mathbf{Proc}), a więc z konkretnymi zmiennymi (lub procedurami). Widać tutaj jasno, jak elegancki i wygodny jest podział na środowisko i stan.

Zatem niech \mathbf{Proc} = \mathbf{Var} \times \mathbf{Stmt} \times \mathbf{Env}.

Czyli znów zbiór \mathbf{Env} zadany jest przez równanie.

Nasze tranzycje będą następujących postaci:

E \,\vdash\, e, s \,\longrightarrow\, n \quad \quad E \,\vdash\, b, s \,\longrightarrow\, l

E \,\vdash\, I, s \,\longrightarrow\, s' \quad \quad E \,\vdash\, d, s \,\longrightarrow\, (E', s')

Na uwagę zasługuje ostatnia postać tranzycji: deklaracja, w odróżnieniu od instrukcji, może zmieniać środowisko.

Umówmy się, że środowisko E' będzie rozszerzało E o informację o nowych identyfikatorach, tzn. tych zadeklarowanych w d.

Zajmijmy się najpierw deklaracjami zmiennych:

\frac{E \,\vdash\, e, s \,\longrightarrow\, n}      {E \,\vdash\, \mathbf{var}\, x=e, s \,\longrightarrow\, (E', s')} \quad \mbox{ gdzie } E' = E[x \mapsto l],\,                      s' = s[l \mapsto n],\,                        l = \mathtt{newloc}(s)

i procedur:

E \,\vdash\, \mathbf{proc}\, x_1(x_2);\, I, s \,\longrightarrow\, (E', s) \quad \mbox{ gdzie } E' = E[x_1 \mapsto \langle x_2, I, E \rangle].

Zauważmy, że deklaracja procedury nie zmienia stanu, tylko środowisko, w odróżnieniu od deklaracji zmiennej.

W środowisku pamiętamy trójkę \langle x_2, I, E \rangle zawierającą nazwę parametru formalnego, ciało procedury oraz środowisko deklaracji procedury. To ostatnie determinuje statyczne wiązanie identyfikatorów: wiązanie identyfikatorów występujących w ciele procedury jest jakby "zamrożone" (ustalone) w momencie deklaracji procedury. Aby dokończyć semantykę deklaracji potrzebujemy jeszcze jednej reguły:

\frac{E \,\vdash\, d_1, s \,\longrightarrow\, (E', s')       \quad \quad       E' \,\vdash\, d_2, s' \,\longrightarrow\, (E'', s'')}      {E \,\vdash\, d_1;\, d_2,s \,\longrightarrow\, (E'', s'')}

która opisuje sekwencyjną "kumulację" modyfikacji dokonanych w deklaracjach.

Pytanie: czy kolejność poszczególnych deklaracji ma znaczenie?

Teraz określimy semantykę bloku:

\frac{E \,\vdash\, d, s \,\longrightarrow\, (E', s')       \quad \quad      E' \,\vdash\, I, s' \,\longrightarrow\, s''}      {E \,\vdash\, \mathbf{begin}\, d;\, I \,\mathbf{end}, s \,\longrightarrow\, s''}

Czyli instrukcja wewnętrzna wykonywana jest w środowisku (i stanie) wytworzonym (rozszerzonym) przez deklarację d.

Stan końcowy po wykonaniu bloku to po prostu stan końcowy s'' po wykonaniu instrukcji wewnętrznej. Może się to wydawać niepokojące, gdyż oznacza, że nowe lokacje, powołane podczas deklaracji d, zachowują przechowywaną wartość również po wyjściu z bloku. Na szczęście wykonanie bloku nie ma wpływu na środowisko, a to ono decyduje, które identyfikatory są widoczne (zadeklarowane) i ogólniej, które lokacje są przypisane identyfikatorom. A więc deklaracja d nie ma wpływu na środowisko, w którym zostaną wykonane kolejne instrukcje. Widać to jasno w regule dla złożenia sekwencyjnego:

\frac{E \,\vdash\, I_1, s \,\longrightarrow\, s'        \quad \quad        E, \,\vdash\, I_2, s' \,\longrightarrow\, s''}      {E \,\vdash\, I_1;\, I_2, s \,\longrightarrow\, s''}


Pozostało nam jeszcze określenie semantyki wywołania procedury:

\frac{E'[x \mapsto l] \,\vdash\, I, s \,\longrightarrow\, s'}      {E \,\vdash\, \mathbf{call}\, x_1(x_2), s \,\longrightarrow\, s'} \quad \mbox{ o ile } E(x_1) = \langle x, I, E' \rangle \in \mathbf{Proc} \mbox{ i } E(x_2) = l \in \mathbf{Loc}.

Czyli uruchamiane jest ciało I procedury, w środowisku E' z miejsca deklaracji tej procedury zmodyfikowanym o przypisanie x \mapsto l lokacji l do parametru formalnego x procedury.

Stan, w którym uruchomione jest I, to po prostu stan bieżący z miejsca wołania procedury; o prawidłowe wiązanie identyfikatorów "troszczy" się wyłącznie środowisko E'.

Pytanie: czy powyższa reguła dopuszcza rekurencyjne wywołania procedury?

Okazuje się, że niestety nie, gdyż w środowisku E'[x \mapsto l] nie ma informacji o procedurze x_1, którą właśnie wołamy.

Może się zdarzyć, że E'[x \mapsto l] \in \mathbf{Proc}, ale oznacza to tylko, że w miejscu deklaracji procedury x_1 była już wcześniej (statycznie) zadeklarowana inna procedura o tej samej nazwie. Aby umożliwić rekurencję, powinniśmy zadbać sami o to, aby środowisko, w którym wykonujemy I zawierało informację o naszej procedurze:

\frac{E'[x \mapsto l][x_1 \mapsto \langle x, I, E' \rangle] \,\vdash\, I, s \,\longrightarrow\, s'}      {E \,\vdash\, \mathbf{call}\, x_1(x_2), s \,\longrightarrow\, s'} \quad \mbox{ o ile } E(x_1) = \langle x, I, E' \rangle \in \mathbf{Proc} \mbox{ i } E(x_2) = l \in \mathbf{Loc}.

Zauważmy, że przypisanie x_1 \mapsto \langle x, I, E' \rangle dodane do E' wystarcza na tylko jedno wywołanie rekurencyjne (licząc w głąb).

Ale każde kolejne wywołanie dodaje tę informację, czyli każde wywołanie procedury przygotowuje środowisko dla następnego. I to jest wystarczające.

Pytanie: czy możliwa jest rekurencja wzajemna?

Pozostaje jeszcze jedna kwestia: dodajemy do środowiska E' dwa przypisania:

x \mapsto l oraz x_1 \mapsto \langle x, I, E' \rangle, ale dlaczego w takiej kolejności?

Pytanie: co się stanie, gdy zamienimy tę kolejność:

\frac{E'[x_1 \mapsto \langle x, I, E' \rangle][x \mapsto l] \,\vdash\, I, s \,\longrightarrow\, s'}      {E \,\vdash\, \mathbf{call}\, x_1(x_2), s \,\longrightarrow\, s'} \quad \mbox{ o ile } E(x_1) = \langle x, I, E' \rangle \in \mathbf{Proc} \mbox{ i } E(x_2) = l \in \mathbf{Loc}.

Czy obydwie semantyki są sobie równoważne?

Okazuje się, że tak nie jest, oto dwa kontrprzykłady:


\mathbf{begin}\,
  \mathbf{var}\, y = 7;
  \mathbf{proc}\, x(x); x := x+1;

  \mathbf{call}\, x(y);
\,\mathbf{end}


\mathbf{begin}\,
  \mathbf{var}\, y = 7;
  \mathbf{proc}\, x(x);
    \mathbf{if}\, y > 0 \,\mathbf{then}\, y := y-1; \mathbf{call}\, x(y) \,\mathbf{else}\, \mathbf{skip};
  \mathbf{call}\, x(y);
\,\mathbf{end}


Każdy z tych programów uruchomi się poprawnie w dokładnie jednym z wariantów semantyki.

Pytanie: Który w którym?

Semantyka pozostałych instrukcji i wyrażeń języka TINY pozostaje bez zmian.

Rozważmy na koniec problem dealokacji.


Wariant (dealokacja)

Chcemy, aby "posprzątano" po zakończeniu wykonania bloku, tzn. aby zwolniono te lokacje, które były używane tylko w tym bloku i w związku z tym nie będą już potrzebne.

Oznacza to, że powinniśmy przywrócić nieokreśloność stanu dla wszystkich tych lokacji, które były użyte podczas deklaracji d.

Jest wiele możliwości wyrażenia tego dodatkowego warunku, np. możemy dodać do powyższej reguły

\frac{E \,\vdash\, d, s \,\longrightarrow\, (E', s')       \quad \quad      E' \,\vdash\, I, s' \,\longrightarrow\, s''}      {E \,\vdash\, \mathbf{begin}\, d;\, I \,\mathbf{end}, s \,\longrightarrow\, \bar{s}}

dodatkowy warunek postaci: \mbox{ gdzie } \bar{s} = \ldots.

Proponujemy tu rozwiązanie ciut bardziej eleganckie. Zacznijmy od tranzycji dokonujących dealokacji, które mogą być np. postaci s'', L \,\longrightarrow\, \bar{s}. Zakładamy tutaj, że znamy zbiór nowo zaalokowanych lokacji L \subseteq \mathbf{Loc}.

s, \emptyset \,\longrightarrow\, s \quad \quad \frac{s \setminus \{ (l, s(l)) \} , L \setminus \{ l \} \,\longrightarrow\, s'} {s, L \,\longrightarrow\, s'} \quad \mbox{ o ile } l \in L \mbox{ i } s(l) \mbox{ jest określone}

Teraz albo napiszemy w dodatkowym warunku, że L = \mathrm{dom}(s'') \setminus \mathrm{dom}(s):

\frac{E \,\vdash\, d, s \,\longrightarrow\, (E', s')       \quad \quad       E' \,\vdash\, I, s' \,\longrightarrow\, s''       s'', L \,\longrightarrow\, \bar{s}}      {E \,\vdash\, \mathbf{begin}\, d;\, I \,\mathbf{end}, s \,\longrightarrow\, \bar{s}} \quad \mbox{ gdzie } L = \mathrm{dom}(s'') \setminus \mathrm{dom}(s)

albo możemy poprosić o pomoc w posprzątaniu tego, kto "nabałaganił", czyli deklarację d.

Niech deklaracja zwraca, oprócz pary (E', s'), dodatkowo zbiór L. Oto poprawione reguły dla deklaracji:


\frac{E \,\vdash\, e, s \,\longrightarrow\, n}      {E \,\vdash\, \mathbf{var}\, x=e, s \,\longrightarrow\, (E', s', \{ l \})        \quad \mbox{ gdzie } E' = E[x \mapsto l],\,                            s' = s[l \mapsto n],\,                              l = \mathtt{newloc}(s)}

E \,\vdash\, \mathbf{proc}\, x_1(x_2);\, I, s \,\longrightarrow\, (E', s, \emptyset) \quad \mbox{ gdzie } E' = E[x_1 \mapsto \langle x_2, I, E \rangle].

\frac{E \,\vdash\, d_1, s \,\longrightarrow\, (E', s', L')       \quad \quad       E' \,\vdash\, d_2, s' \,\longrightarrow\, (E'', s'', L'')}      {E \,\vdash\, d_1;\, d_2,s \,\longrightarrow\, (E'', s'', L' \cup L'')}

Wtedy ostatecznie reguła dla bloku wygląda następująco:

\frac{E \,\vdash\, d, s \,\longrightarrow\, (E', s', L)       \quad \quad       E' \,\vdash\, I, s' \,\longrightarrow\, s''       \quad \quad       s'', L \,\longrightarrow\, \bar{s}}      {E \,\vdash\, \mathbf{begin}\, d;\, I \,\mathbf{end}, s \,\longrightarrow\, \bar{s}}

Ćwiczenie 2 (wiązanie dynamiczne)

Zastanówmy się, jakich modyfikacji wymaga nasza semantyka, aby wiązanie identyfikatorów było dynamiczne zamiast statycznego. Rozumiemy przez to, że w przypadku odwołania do zmiennej wewnątrz procedury, odwołujemy się do środowiska w miejscu wywołania tej procedury, zamiast do środowiska z miejsca deklaracji jak dotychczas. Tak samo powinno być w przypadku wywołania innej procedury wewnątrz procedury, czyli wiązanie dynamiczne dotyczy i zmiennych i procedur.


Przykład

Spójrzmy na kilka prostych programów.

\mathbf{begin}\,
  \mathbf{var}\, x = 1;
  \mathbf{proc}\, p(y); y := y+x;

  \mathbf{begin}\,
    \mathbf{var}\, x = 10; \mathbf{var}\, z = 0;

    \mathbf{call}\, p(z);
  \,\mathbf{end}
\,\mathbf{end}


Wartość końcowa zmiennej z = 10. Gdyby wiązanie zmiennej x było statyczne, to wartość ta wynosiłaby 1. Podobnie dla procedur:


\mathbf{begin}\,
  \mathbf{proc}\, q(x); x := x+1;
  \mathbf{proc}\, p(y); \mathbf{call}\, q(y); \mathbf{call}\, q(y);

  \mathbf{begin}\,
    \mathbf{proc}\, q(x); x := x+x;
    \mathbf{var}\, z = 2;

    \mathbf{call}\, p(z);
  \,\mathbf{end}
\,\mathbf{end}


Wartość końcowa zmiennej z = 8. Gdyby widoczność procedury q była statyczna, to wartość ta wynosiłaby 4. A oto przykład programu, który nie wykonałby się poprawnie przy wiązaniu statycznych, a wykonuje się poprawnie przy dynamicznym:


\mathbf{begin}\,
  \mathbf{proc}\, q(x); \mathbf{call}\, r(x);
  \mathbf{proc}\, p(y);
    \mathbf{begin}\,
      \mathbf{proc}\, r(x); x := x+x;

      \mathbf{call}\, q(x);
    \,\mathbf{end}
  \mathbf{var}\, z = 7;

  \mathbf{call}\, p(z);
\,\mathbf{end}


Wartość końcowa zmiennej z wynosi 14. Procedura p wywołuje procedurę q "przekazując" jej również środowisko z miejsca wołania, zawierające informację o procedurze r. Ta ostatnia przy widoczności statycznej byłaby lokalna, a teraz nie jest.


Rozwiązanie

Spójrzmy na regułę dla wywołania procedury:

\frac{E'[x \mapsto l] \,\vdash\, I, s \,\longrightarrow\, s'}      {E \,\vdash\, \mathbf{call}\, x_1(x_2), s \,\longrightarrow\, s'} \quad \mbox{ o ile } E(x_1) = \langle x, I, E' \rangle \in \mathbf{Proc} \mbox{ i } E(x_2) = l \in \mathbf{Loc}.

O statyczności decyduje to, że wnętrze procedury I wykonywane jest w środowisku zmiejsca deklaracji E'.

Wystarczy więc, jeśli zignorujemy to środowisko, a zamiast niego użyjemy bieżącego środowiska E (czyli środowiska z miejsca wywołania):

\frac{E[x \mapsto l] \,\vdash\, I, s \,\longrightarrow\, s'}      {E \,\vdash\, \mathbf{call}\, x_1(x_2), s \,\longrightarrow\, s'} \quad \mbox{ o ile } E(x_1) = \langle x, I, E' \rangle \in \mathbf{Proc} \mbox{ i } E(x_2) = l \in \mathbf{Loc}.

Oczywiście w takim przypadku w środowiskach wystarczy przechowywać dla procedur pary \langle x, I \rangle.

Zastanówmy się teraz, jak uwzględnić rekurencję?

Moglibyśmy postąpić tak jak poprzednio, np.

\frac{E[x \mapsto l][x_1 \mapsto \langle x, I \rangle] \,\vdash\, I, s \,\longrightarrow\, s'}      {E \,\vdash\, \mathbf{call}\, x_1(x_2), s \,\longrightarrow\, s'} \quad \mbox{ o ile } E(x_1) = \langle x, I \rangle \in \mathbf{Proc} \mbox{ i } E(x_2) = l \in \mathbf{Loc}

ale jeśli uważnie przyjrzymy się tej regule, to okazuje się, że "przelewamy z pustego w próżne".

Sciślej, dodajemy do E parę x_1 \mapsto \langle x, I \rangle, która już tam tak naprawdę jest! Okazuje się, że dzięki dynamicznemu wiązaniu identyfikatorów "za darmo" otrzymujemy rekurencje! Tak jest na przykład w programie:


\mathbf{begin}\,
  \mathbf{proc}\, p(x); \mathbf{if}\, x < 100 \,\mathbf{then}\, x := x+x; \mathbf{call}\, p(x) \,\mathbf{else}\, \mathbf{skip};
  \mathbf{var}\, z = 8;

  \mathbf{call}\, p(z);
\,\mathbf{end}


ponieważ w momencie wywołania rekurencyjnego \mathbf{call}\, p(x), w środowisku jest już informacja nt. procedury p!


Ćwiczenie 3 (różne mechanizmy przekazywana parametru)

Rozważ inne mechanizmy przekazywania parametru (wiązanie statyczne):

  • przez wartość
  • in-out

Należy wyjaśnić ostatni mechanizm (in-out). Parametrem aktualnym jest zmienna, której wartość jest przypisywana parametrowi formalnemu (tak jak przy przekazywaniu przez wartość, gdy parametrem aktualnym jest zmienna). Ale po zakończeniu procedury, wartość parametru formalnego, która mogła zmienić się w czasie dzialania procedury, jest z powrotem przypisywana na zmienną będącą parametrem aktualnym (a więc podobnie do przekazywania przez zmienną). Parametr formalny jest traktowany jak zmienna lokalna procedury.


Rozwiązanie (przekazywanie parametru przez wartość)

Omówimy tylko instrukcję wywołania procedury, której składnia jest teraz szersza niż poprzednio:

I \, ::= \,\,         \ldots \,\,|\,\,        \mathbf{call}\, x(e)

Przed wywołaniem procedury należy zaalokować nową lokację, którą przypiszemy parametrowi formalnemu; następnie obliczamy wartość parametru aktualnego i umieszczamy ją w tej lokacji:

\frac{E \,\vdash\, e, s \,\longrightarrow\, n       \quad       E'[x' \mapsto l] \,\vdash\, I, s[l \mapsto n] \,\longrightarrow\, s'}      {E \,\vdash\, \mathbf{call}\, x(e), s \,\longrightarrow\, s'} \quad \mbox{ o ile } E(x) = \langle x', I, E' \rangle \in \mathbf{Proc} \mbox{ i } l = \mathtt{newloc}(s)

Rozwiązanie (przekazywanie parametru in-out)

Początkowo postępujemy tak jak w przypadku przekazywania przez wartość: alokujemy nową lokację l, którą przypiszemy parametrowi formalnemu i umieszczamy w niej wartość zmiennej (x_2 w regule poniżej) będącej parametrem aktualnym.

Zasadnicza różnica widoczna jest po zakończeniu procedury: wartość z lokacji l powinna zostać spowrotem przypisana zmiennej x_2. Oto reguła:

\frac{E'[x \mapsto l] \,\vdash\, I, s[l \mapsto s(l_2)] \,\longrightarrow\, s'}      {E \,\vdash\, \mathbf{call}\, x_1(x_2), s \,\longrightarrow\, s'[l_2 \mapsto s'(l)]} \quad \mbox{ o ile } E(x_2) = l_2 \in \mathbf{Loc} \mbox{ i } s(l_2) \mbox{ jest określone i }

E(x_1) = \langle x, I, E' \rangle \in \mathbf{Proc} \mbox{ i } l = \mathtt{newloc}(s).


Zadania domowe

Ćwiczenie 1

Zaadaptuj semantykę "deklaracji równoległych" dla rekurencji wzajemnej wewnątrz jednej deklaracji równoległej. To znaczy, że procedury zadeklarowane w jednej deklaracji równoległej mogą się wzajemniej wywoływać bez żadnych ograniczeń.


Ćwiczenie 2

Zamiast procedur, rozważ funkcje z pojedynczym parametrem przekazywanym przez zmienną. Wywołanie funkcji może teraz pojawić się wewnątrz wyrażenia:

e \, ::= \,\,           \ldots \,\,|\,\,           \mathbf{call}\, x_1(x_2).

Wewnątrz funkcji jej nazwą można posługiwać się jak zmienną lokalną. Wynik zwracany przez funkcję to wartość tej zmiennej w momencie wyjścia z funkcji.

Uwaga na efekty uboczne!


Ćwiczenie 3

Rozważ następujące warianty:

  • wiązanie zmiennych jest statyczne, a procedur dynamiczne
  • wiązanie zmiennych jest dynamiczne, a procedur statyczne.


Ćwiczenie 4

Rozszerz semantykę procedur tak, aby parametrem aktualnym procedury mogła być procedura.