Require Import PL.RTClosure PL.Imp PL.ImpExt PL.ImpExt2.
(** Before this lecture, we only discuss deterministic languages. The simple
imperative language is deterministic; the language with control flow is
deterministic; the language with function call is deterministic and the
language with integer division is also deterministic. However, realistic
programming languages like C are nondeterministic.
For example, the result of malloc is nondeterministic. When it succeeds,
it allocate a new piece of memory, arbitrarily. Also, when evaluating one
expression with two function calls involved, e.g.
[[
f(x) + f(y)
]]
the evaluation order is _unspecified_. A compiler can choose to evaluate
[f(x)] first; a compiler can choose to evaluate [f(y)] first; and a compiler
can even choose to pick a different order in different executions. We will
discuss programming languages with nondeterministic behavior today. *)
(* ################################################################# *)
(** * Determinism *)
(** In fact, determinism can be interpret in two different ways.
- A. A programming language is deterministic if every program only has one
possible outcome. An outcome can be (1) terminating on some program state
(2) not terminating and keep running forever or maybe (3) crushing, etc.
- B. A programming language is deterministic if every intermediate state of
execution has at most one possible ``next step''.
In short, we can interpret it by denotations or by small steps. *)
Definition Deterministic_D: Prop :=
forall c st1 st2 st2',
ceval c st1 st2 ->
ceval c st1 st2' ->
Func.equiv st2 st2'.
Definition Deterministic_S: Prop :=
forall c1 st1 c2 st2 c2' st2',
cstep (c1, st1) (c2, st2) ->
cstep (c1, st1) (c2', st2') ->
c2 = c2' /\ Func.equiv st2 st2'.
(** The simple imperative language and its extensions that we have discussed are
all deterministic under either interpretation. *)
Module ComCChoice.
(* ################################################################# *)
(** * A nondeterministic language *)
(** We consider a language with explicit nondeterminitic behavior. *)
Inductive com : Type :=
| CSkip
| CAss (X: var) (a : aexp)
| CSeq (c1 c2 : com)
| CIf (b : bexp) (c1 c2 : com)
| CWhile (b : bexp) (c : com)
| CChoice (c1 c2: com). (* <-- new *)
Notation "x '::=' a" :=
(CAss x a) (at level 80) : imp_scope.
Notation "'Skip'" :=
CSkip : imp_scope.
Notation "c1 ;; c2" :=
(CSeq c1 c2) (at level 80, right associativity) : imp_scope.
Notation "'While' b 'Do' c 'EndWhile'" :=
(CWhile b c) (at level 80, right associativity) : imp_scope.
Notation "'If' c1 'Then' c2 'Else' c3 'EndIf'" :=
(CIf c1 c2 c3) (at level 10, right associativity) : imp_scope.
Coercion AId: var >-> aexp.
Local Open Scope imp.
(** Here, we add one more constructor to the syntax trees of program commands.
To execute [CChoice c1 c2] is to execute either [c1] or [c2]. A compiler or
some external environment may decide which one to choose.
In this language, we can write a program with different outcomes. *)
Module sample_program.
Definition X: var := 1%nat.
Definition Y: var := 2%nat.
Definition prog_sample_1: com :=
CChoice (X ::= X + 1) (X ::= X - 1).
Definition prog_sample_2: com :=
CChoice (X ::= X + Y) (X ::= Y + X).
Definition prog_sample_3: com :=
CChoice (X ::= X + Y) (X ::= X - 1;; X ::= Y + X + 1).
Definition prog_sample_4: com :=
CChoice (X ::= 0) (While BTrue Do X ::= X - 1 EndWhile).
End sample_program.
(* ################################################################# *)
(** * Small step semantics *)
(** In order to describe behaviors of this nondeterministic language, it is
natural to leverage the expressiveness of relational definitions. For
example, [cstep] in small step semantics is a binary relation. A binary
relation is not necessary to be deterministic. We could write a new
definition such that both of the following are true.
[[
cstep (CChoice c1 c2, st) (c1, st),
cstep (CChoice c1 c2, st) (c2, st).
]]
Here is our definition.
*)
Inductive cstep : (com * state) -> (com * state) -> Prop :=
| CS_AssStep : forall st X a a',
astep st a a' ->
cstep (CAss X a, st) (CAss X a', st)
| CS_Ass : forall st1 st2 X n,
st2 X = n ->
(forall Y, X <> Y -> st1 Y = st2 Y) ->
cstep (CAss X (ANum n), st1) (Skip, st2)
| CS_SeqStep : forall st c1 c1' st' c2,
cstep (c1, st) (c1', st') ->
cstep (c1 ;; c2 , st) (c1' ;; c2, st')
| CS_Seq : forall st c2,
cstep (Skip ;; c2, st) (c2, st)
| CS_IfStep : forall st b b' c1 c2,
bstep st b b' ->
cstep
(If b Then c1 Else c2 EndIf, st)
(If b' Then c1 Else c2 EndIf, st)
| CS_IfTrue : forall st c1 c2,
cstep (If BTrue Then c1 Else c2 EndIf, st) (c1, st)
| CS_IfFalse : forall st c1 c2,
cstep (If BFalse Then c1 Else c2 EndIf, st) (c2, st)
| CS_While : forall st b c,
cstep
(While b Do c EndWhile, st)
(If b Then (c;; While b Do c EndWhile) Else Skip EndIf, st)
| CS_Choice_Left : forall st c1 c2, (* <-- new *)
cstep (CChoice c1 c2, st) (c1, st)
| CS_Choice_Right : forall st c1 c2, (* <-- new *)
cstep (CChoice c1 c2, st) (c2, st).
(* ################################################################# *)
(** * Denotational semantics *)
(** Comparing to this small step semantics, defining denotational semantics is
harder. The following is an intuitive attempt. *)
Module TheFirstTry.
Fixpoint ceval (c: com): state -> state -> Prop :=
match c with
| CSkip => BinRel.id
| CAss X E =>
fun st1 st2 =>
st2 X = aeval E st1 /\
forall Y, X <> Y -> st1 Y = st2 Y
| CSeq c1 c2 => BinRel.concat (ceval c1) (ceval c2)
| CIf b c1 c2 => if_sem b (ceval c1) (ceval c2)
| CWhile b c => loop_sem b (ceval c)
| CChoice c1 c2 => BinRel.union (ceval c1) (ceval c2)
end.
End TheFirstTry.
(** That is, we add one line to our original denotational semantics. We define
the denotation of [CChoice c1 c2] as the union of [c1] and [c2]'s
denotaitons. *)
(** Thus, we turn to consider the following domain for describing a denotational
semantics. *)
Record com_denote: Type := {
com_term: state -> state -> Prop; (* <-- ``term'' for terminate *)
com_div: state -> Prop (* <-- ``div'' for diverge *)
}.
(** That is, we have to explicitly describe all possible outcomes. This is not
necessary for a deterministic language. Using the simple imperative language
as an example, if executing [c] from [st1] does not terminate at any state
[st2], then it will not terminate. Also, if executing [c] from [st1] may
terminate at some state [st2], then non-termination is impossible. Thus,
describing all terminating cases is enough. Using our language with control
flow commands as another example, we only defines whether executing [c] from
[st1] will terminate normally at some state [st2], or terminate by break at
some state [st2], or terminate by continue at some [st2]. If none of the
above happens, this execution will not terminate. If some situation above
happens, then non-termination is impossible.
Now, in our new language with non-determinism, there can be different kinds
outcomes for a fixed beginning state [st1] and a program [c]. *)
Module TheSecondAttempt.
(** The semantics for skips, assignments, sequentail compositions and
if-branches is easy to define. *)
Definition skip_sem: com_denote :=
{| com_term := BinRel.id;
com_div := Sets.empty;
|}.
Definition asgn_sem (X: var) (a: aexp): com_denote :=
{| com_term :=
fun st1 st2 =>
st2 X = aeval a st1 /\
forall Y, X <> Y -> st1 Y = st2 Y;
com_div := Sets.empty
|}.
Definition seq_sem (DC1 DC2: com_denote): com_denote :=
{| com_term :=
BinRel.concat (com_term DC1) (com_term DC2);
com_div :=
Sets.union
(com_div DC1)
(BinRel.dia (com_term DC1) (com_div DC2))
|}.
Definition if_sem (b: bexp) (DC1 DC2: com_denote): com_denote :=
{| com_term :=
BinRel.union
(BinRel.concat (BinRel.test_rel (beval b)) (com_term DC1))
(BinRel.concat (BinRel.test_rel (beval (! b))) (com_term DC2));
com_div :=
Sets.union
(BinRel.dia (BinRel.test_rel (beval b)) (com_div DC1))
(BinRel.dia (BinRel.test_rel (beval (!b))) (com_div DC2))
|}.
(** The behavior of [CChoice] is defined as by unions. *)
Definition choice_sem (DC1 DC2: com_denote): com_denote :=
{| com_term :=
BinRel.union (com_term DC1) (com_term DC2);
com_div :=
Sets.union (com_div DC1) (com_div DC2)
|}.
(** Loops' semantics is tricky. We first define the cases of terminating
executions. *)
Fixpoint iter_loop_body (b: bexp)
(DC: com_denote)
(n: nat): state -> state -> Prop :=
match n with
| O => BinRel.test_rel (beval (! b))
| S n' =>
BinRel.concat
(BinRel.test_rel (beval b))
(BinRel.concat
(com_term DC)
(iter_loop_body b DC n'))
end.
(** We then start to consider nonterminating cases. *)
Fixpoint live_after_iter (b: bexp)
(DC: com_denote)
(n: nat): state -> Prop :=
match n with
| O => Sets.full
| S n' =>
BinRel.dia
(BinRel.test_rel (beval b))
(Sets.union
(com_div DC)
(BinRel.dia
(com_term DC)
(live_after_iter b DC n')))
end.
(** When discussing the execution of [While b Do c EndWhile], we use
[ live_after_iter b (ceval c) n ]
to represent the set of beginning states that:
- may fall into a diverging loop body in the first n-th iterations, or
- may complete at least [n] iterations.
In other words, the complement set of [ live_after_iter b (ceval c) n ]
contains those beginning states that guarantee termination within [n]
iterations.
For example, consider the following program.
[[
While (! (X == 0)) Do
If (X == 1)
Then (CChoice
(While BTrue Do Skip EndWhile)
(X ::= 0))
Else X ::= X - 2
EndWhile
]]
Then,
- [ live_after_iter _ _ 0 ] contains all states;
- [ live_after_iter _ _ 1 ] contains those states [st] such that [st (X)] is
nonzero;
- [ live_after_iter _ _ 2 ] contains those states [st] such that [st (X)] is
neither 0 nor 2.
In the end, we can define loops' semantics using [iter_loop_body] and
[live_after_iter]. *)
Definition loop_sem (b: bexp) (DC: com_denote): com_denote :=
{| com_term :=
BinRel.omega_union (iter_loop_body b DC);
com_div :=
Sets.omega_intersection (live_after_iter b DC)
|}.
Fixpoint ceval (c: com): com_denote :=
match c with
| CSkip => skip_sem
| CAss X E => asgn_sem X E
| CSeq c1 c2 => seq_sem (ceval c1) (ceval c2)
| CIf b c1 c2 => if_sem b (ceval c1) (ceval c2)
| CWhile b c => loop_sem b (ceval c)
| CChoice c1 c2 => choice_sem (ceval c1) (ceval c2)
end.
End TheSecondAttempt.
(** Is this definition correct? Yes. But the reason is nontrivial. We need to
explain that for any command [c] and program state [st], the following two
statements are equivalent:
- executing [c] from [st] must terminate;
- there exists a natural number [n] such that executing [c] from [st] must
terminate within [n] steps.
For loop, this equivalence means: a loop will terminate if and only if there
exists an natural number [n] such that this loop must terminate within [n]
iterations. We omit its proof here since the theory behind it is
complicated. In short, this claim above is true for this language with
[CChoice], and thus the denotational semantics above is a correct behavior
description. *)
End ComCChoice.
(* ################################################################# *)
(** * Another nondeterministic language *)
Module ComCAssAny.
(** Now we consider another nondeterministic language. *)
Inductive com : Type :=
| CSkip
| CAss (X: var) (a : aexp)
| CSeq (c1 c2 : com)
| CIf (b : bexp) (c1 c2 : com)
| CWhile (b : bexp) (c : com)
| CAssAny (X: var). (* <-- new *)
(** The new command [CAssAny X] will assign an arbitrary integer to the program
variable [X]. What is different between this language and our language with
[CChoice]? A [CAssAny] command has infinite possible outcomes. You may argue
that one of them is more realistic than the other. But we only focus on the
difference between their theories.
Consider the following program:
[[
X ::= AnyInteger;;
If (X <= 0) Then X ::= 0 - X Else Skip EndIf;;
While (! (X == 0)) Do
X ::= X - 1
EndWhile
]]
This program will always terminate, but there is no such bound [n] that we
can guarantee its termination in [n] steps. Moreover, we can make a slight
modification to it and turn it into one single loop.
[[
While (! (X == 0 && Y == 0)) Do
If (Y == 0)
Then X ::= X - 1
Else Y ::= 0;;
X ::= AnyInteger;;
If (X <= 0) Then X ::= 0 - X Else Skip EndIf
EndIf
EndWhile
]]
If [Y] is nonzero in the beginning state, then this loop will always
terminate but the number of iterations is unbounded, which fails our
semantic definition for loops above. *)
(* ################################################################# *)
(** * Better denotational semantics *)
(** We define its denotational semantics below. *)
Record com_denote: Type := {
com_term: state -> state -> Prop;
com_div: state -> Prop
}.
Definition skip_sem: com_denote :=
{| com_term := BinRel.id;
com_div := Sets.empty;
|}.
Definition asgn_sem (X: var) (a: aexp): com_denote :=
{| com_term :=
fun st1 st2 =>
st2 X = aeval a st1 /\
forall Y, X <> Y -> st1 Y = st2 Y;
com_div := Sets.empty
|}.
Definition seq_sem (DC1 DC2: com_denote): com_denote :=
{| com_term :=
BinRel.concat (com_term DC1) (com_term DC2);
com_div :=
Sets.union
(com_div DC1)
(BinRel.dia (com_term DC1) (com_div DC2))
|}.
Definition if_sem (b: bexp) (DC1 DC2: com_denote): com_denote :=
{| com_term :=
BinRel.union
(BinRel.concat (BinRel.test_rel (beval b)) (com_term DC1))
(BinRel.concat (BinRel.test_rel (beval (! b))) (com_term DC2));
com_div :=
Sets.union
(BinRel.dia (BinRel.test_rel (beval b)) (com_div DC1))
(BinRel.dia (BinRel.test_rel (beval (!b))) (com_div DC2))
|}.
Definition asgn_any_sem (X: var): com_denote :=
{| com_term :=
fun st1 st2 =>
forall Y, X <> Y -> st1 Y = st2 Y;
com_div := Sets.empty
|}.
Fixpoint iter_loop_body (b: bexp)
(DC: com_denote)
(n: nat): state -> state -> Prop :=
match n with
| O => BinRel.test_rel (beval (! b))
| S n' =>
BinRel.concat
(BinRel.test_rel (beval b))
(BinRel.concat
(com_term DC)
(iter_loop_body b DC n'))
end.
(** The most important part is still nonterminating cases of loops. *)
Definition diverge (b: bexp) (DC: com_denote) (X: state -> Prop): Prop :=
forall st,
X st ->
BinRel.dia
(BinRel.test_rel (beval b))
(Sets.union
(com_div DC)
(BinRel.dia (com_term DC) X)) st.
(** When discussing the execution of [While b Do c EndWhile], we use
[ diverge b (ceval c) X ]
to claim the following property and program state set [X]: for any state
[st] in [X],
- loop condition [b] must be true on [st], and
- there exists some execution of loop body [c] from [st] will directly
diverge, or there exists some execution of loop body [c] from [st] will
terminate in some state in [X].
We use [iter_loop_body] and [diverge] to define loops' behavior.
*)
Definition loop_sem (b: bexp) (DC: com_denote): com_denote :=
{| com_term :=
BinRel.omega_union (iter_loop_body b DC);
com_div :=
Sets.infinite_union (diverge b DC)
|}.
(** Here, [Sets.infinite_union (diverge b DC)] is the union of all sets [X]
such that [diverge b DC X] holds. *)
Print Sets.infinite_union.
(* Sets.infinite_union =
fun (A : Type) (X : (A -> Prop) -> Prop) (a : A) =>
exists S : A -> Prop, X S /\ S a *)
Fixpoint ceval (c: com): com_denote :=
match c with
| CSkip => skip_sem
| CAss X E => asgn_sem X E
| CSeq c1 c2 => seq_sem (ceval c1) (ceval c2)
| CIf b c1 c2 => if_sem b (ceval c1) (ceval c2)
| CWhile b c => loop_sem b (ceval c)
| CAssAny X => asgn_any_sem X
end.
End ComCAssAny.
(* 2021-05-17 00:32 *)