Lecture notes 20210512 Nondeterminism and Nontermination

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 :=
  c st1 st2 st2',
    ceval c st1 st2 ->
    ceval c st1 st2' ->
    Func.equiv st2 st2'.

Definition Deterministic_S: Prop :=
  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 c2st) (c1st),
        cstep (CChoice c1 c2st) (c2st).
    
Here is our definition.
Inductive cstep : (com * state) -> (com * state) -> Prop :=
  | CS_AssStep : st X a a',
      astep st a a' ->
      cstep (CAss X a, st) (CAss X a', st)
  | CS_Ass : st1 st2 X n,
      st2 X = n ->
      (Y, XY -> st1 Y = st2 Y) ->
      cstep (CAss X (ANum n), st1) (Skip, st2)
  | CS_SeqStep : st c1 c1' st' c2,
      cstep (c1, st) (c1', st') ->
      cstep (c1 ;; c2 , st) (c1' ;; c2, st')
  | CS_Seq : st c2,
      cstep (Skip ;; c2, st) (c2, st)
  | CS_IfStep : 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 : st c1 c2,
      cstep (If BTrue Then c1 Else c2 EndIf, st) (c1, st)
  | CS_IfFalse : st c1 c2,
      cstep (If BFalse Then c1 Else c2 EndIf, st) (c2, st)
  | CS_While : 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 : st c1 c2, (* <-- new *)
      cstep (CChoice c1 c2, st) (c1, st)
  | CS_Choice_Right : 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
  | CSkipBinRel.id
  | CAss X E
      fun st1 st2
        st2 X = aeval E st1
        Y, XY -> st1 Y = st2 Y
  | CSeq c1 c2BinRel.concat (ceval c1) (ceval c2)
  | CIf b c1 c2if_sem b (ceval c1) (ceval c2)
  | CWhile b cloop_sem b (ceval c)
  | CChoice c1 c2BinRel.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
         Y, XY -> 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
  | OBinRel.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
  | OSets.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
  | CSkipskip_sem
  | CAss X Easgn_sem X E
  | CSeq c1 c2seq_sem (ceval c1) (ceval c2)
  | CIf b c1 c2if_sem b (ceval c1) (ceval c2)
  | CWhile b cloop_sem b (ceval c)
  | CChoice c1 c2choice_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
         Y, XY -> 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
         Y, XY -> 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
  | OBinRel.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 :=
  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
  | CSkipskip_sem
  | CAss X Easgn_sem X E
  | CSeq c1 c2seq_sem (ceval c1) (ceval c2)
  | CIf b c1 c2if_sem b (ceval c1) (ceval c2)
  | CWhile b cloop_sem b (ceval c)
  | CAssAny Xasgn_any_sem X
  end.

End ComCAssAny.

(* 2021-05-17 00:32 *)