Lecture notes 20210426 Control Flow

Require Import Coq.Lists.List.
Require Import PL.Imp.

The language

Now, let's consider a programming language with break and continue commands.
The majority of our definition remains the same with the language in Imp. We only add two kinds of commands to the definition of com:
Inductive com : Type :=
  | CSkip
  | CAss (X: var) (a : aexp)
  | CSeq (c1 c2 : com)
  | CIf (b : bexp) (c1 c2 : com)
  | CWhile (b : bexp) (c : com)
  | CBreak (* <--- new *)
  | CCont (* <--- new *)
  .
In this lecture, we discover how to defines its Hoare logic, denotational semantics and small step semantics.
Obviously, the functionalities of Break and Skip are different. But they look the same if only considering their modification on program states: neither of them modify program states. The difference between them is about determining which command will be executed next. This is called control flow.

Denotational Semantics

We start from denotational semantics.
Module Denotation_With_ControlFlow.
Program's denotation is defined as three binary relations: the NormalExit part, the BreakExit part and the ContExit part. Specifically,
  • st1, st2 belongs to the NormalExit part of program c's denotation if and only if executing c from st1 may terminate at state st2 normally;
  • st1, st2 belongs to the BreakExit part of program c's denotation if and only if executing c from st1 may terminate at state st2 by break;
  • st1, st2 belongs to the ConExit part of program c's denotation if and only if executing c from st1 may terminate at state st2 by continue.
Record is a special inductive type. For example, the following type can be defined as:

       Inductive denoteType :=
       | Build_denote (x y zstate -> state -> Prop):  denote.
Record denote: Type := {
  NormalExit: state -> state -> Prop;
  BreakExit: state -> state -> Prop;
  ContExit: state -> state -> Prop;
}.
Obviously, skip commands can only terminate normally.
Definition skip_sem: denote := {|
  NormalExit := BinRel.id;
  BreakExit := BinRel.empty;
  ContExit := BinRel.empty
|}.
In contrast, CBreak and CCont will never terminate will normal exit.
Definition break_sem: denote := {|
  NormalExit := BinRel.empty;
  BreakExit := BinRel.id;
  ContExit := BinRel.empty
|}.

Definition cont_sem: denote := {|
  NormalExit := BinRel.empty;
  BreakExit := BinRel.empty;
  ContExit := BinRel.id
|}.
Assignment commands only terminate normally.
Definition asgn_sem (X: var) (E: aexp): denote := {|
  NormalExit :=
    fun st1 st2
      st2 X = aeval E st1
      (Y, XY -> st1 Y = st2 Y);
  BreakExit := BinRel.empty;
  ContExit := BinRel.empty
|}.
For sequential composition, the second command will be executed if and only if the first one terminates normally.
Definition seq_sem (d1 d2: denote): denote := {|
  NormalExit := BinRel.concat (NormalExit d1) (NormalExit d2);
  BreakExit := BinRel.union
                 (BreakExit d1)
                 (BinRel.concat (NormalExit d1) (BreakExit d2));
  ContExit := BinRel.union
                (ContExit d1)
                (BinRel.concat (NormalExit d1) (ContExit d2));
|}.
The semantics of branches and loops are similar to our original definitions. But remember, a loop itself will never terminate by break or continue although a loop body may break and terminates whole loop's execution.
Definition if_sem (b: bexp) (d1 d2: denote): denote := {|
  NormalExit := BinRel.union
                  (BinRel.concat
                     (BinRel.test_rel (beval b))
                     (NormalExit d1))
                  (BinRel.concat
                     (BinRel.test_rel (beval (BNot b)))
                     (NormalExit d2));
  BreakExit := BinRel.union
                 (BinRel.concat
                    (BinRel.test_rel (beval b))
                    (BreakExit d1))
                 (BinRel.concat
                    (BinRel.test_rel (beval (BNot b)))
                    (BreakExit d2));
  ContExit := BinRel.union
                (BinRel.concat
                   (BinRel.test_rel (beval b))
                   (ContExit d1))
                (BinRel.concat
                   (BinRel.test_rel (beval (BNot b)))
                   (ContExit d2))
|}.

Fixpoint iter_loop_body (b: bexp) (d: denote) (n: nat): state -> state -> Prop :=
  match n with
  | OBinRel.union
              (BinRel.test_rel (beval (! b)))
              (BinRel.concat (BinRel.test_rel (beval b)) (BreakExit d))
  | S n'BinRel.concat
              (BinRel.test_rel (beval b))
              (BinRel.concat
                 (BinRel.union (NormalExit d) (ContExit d))
                 (iter_loop_body b d n'))
  end.

Definition loop_sem (b: bexp) (d: denote): denote := {|
  NormalExit := BinRel.omega_union (iter_loop_body b d);
  BreakExit := BinRel.empty;
  ContExit := BinRel.empty
|}.

Fixpoint ceval (c: 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)
  | CBreakbreak_sem
  | CContcont_sem
  end.

End Denotation_With_ControlFlow.

Hoare Logic

A Hoare logic for Imp with break and continue has multiple postconditions.
Module Hoare_logic.

Import Assertion_S.

Notation "d [ X ⟼ a ]" := (assn_sub X a ((d)%assert)) (at level 10, X at next level) : assert_scope.
Notation "a0 [ X ⟼ a ]" := (aexp_sub X a ((a0)%imp)) (at level 10, X at next level) : imp_scope.

Parameter hoare_triple: Assertion ->
                         com ->
                         Assertion * (* Normal Postcondition *)
                         Assertion * (* Break  Postcondition *)
                         Assertion -> (* Continue Condition *)
                         Prop.

Notation " {{ P }}  c  {{ Q } {{ QB } {{ QC }} " :=
  (hoare_triple
     P
     c
     (Q%assert: Assertion, QB%assert: Assertion, QC%assert: Assertion))
  (at level 90, c at next level).
This Hoare triple says: if command c is started in a state satisfying assertion P, and if c eventually terminates normally / by break / by continue in some final state, then this final state will satisfy assertion Q / QB / QC.
All proof rules need to be slightly modified:
Axiom hoare_seq : (P Q R RB RC: Assertion) (c1 c2: com),
   {{P}c1  {{Q} {{RB} {{RC}}  ->
   {{Q}c2  {{R} {{RB} {{RC}}  ->
   {{P}CSeq c1 c2  {{R} {{RB} {{RC}} .

Axiom hoare_skip : P,
   {{P}CSkip  {{P} {{False} {{False}} .

Axiom hoare_if : P Q QB QC b c1 c2,
   {{ P AND [[b]] }c1  {{Q} {{QB} {{QC}}  ->
   {{ P AND NOT [[b]] }c2  {{Q} {{QB} {{QC}}  ->
   {{ P }CIf b c1 c2  {{Q} {{QB} {{QC}} .

Axiom hoare_asgn_fwd : P (X: var) E,
   {{ P }
  CAss X E
   {{ EXISTS x, P [Xx] AND
               [[AId X]] = [[ E [Xx] ]] }
   {{False}
   {{False}} .

Axiom hoare_asgn_bwd : P (X: var) E,
   {{ P [ XE] }CAss X E  {{ P } {{False} {{False}} .

Axiom hoare_consequence : (P P' Q Q' QB QB' QC QC' : Assertion) c,
  P  P' ->
   {{P'}c  {{Q'} {{QB'} {{QC'}} ->
  Q'  Q ->
  QB'  QB ->
  QC'  QC ->
   {{P}c  {{Q} {{QB} {{QC}} .
The proof rules for break / continue / while is most interesting. Specifically, the assumption of hoare_while says: if the loop invariant I and the loop condition b is satisfied initially, then executing the loop body c may have these different results:
  • Terminating normally at some state satisfying the invariant again;
  • Terminating by break at some state satisfying the loop's postcondition P;
  • Terminating by continue at some state satisfying the invariant again;
  • Not terminating.
The conclusion of hoare_while says: if the loop invariant I is satisfied initially, then the whole loop either does not terminate or terminates normally at some state satisfying I AND NOT [[ b ]] or P.
Axiom hoare_break : P,
   {{P}CBreak  {{False} {{P} {{False}} .

Axiom hoare_cont : P,
   {{P}CCont  {{False} {{False} {{P}} .

Axiom hoare_while : I P b c,
   {{ I AND [[b]] }c  {{I} {{P} {{I}}  ->
   {{ I }CWhile b c  {{ P OR (I AND NOT [[b]]) } {{False} {{False}} .

End Hoare_logic.

Small Step Semantics With Virtual Loop Commands

We demonstrate two versions of small step semantics, one based on virtual loops and one based on control stack.
Module Small_Step_Semantics_Virtual_Loop.
The main idea of virtual loops is to introduce a different loop command CWhile' which takes three argument intead of two. CWhile' c1 b c means: during executing the loop body of While b Do c Endwhile, c1 is the leftover part of current iteration. CWhile' is not something appearing in real programs, but is used here for the purpose of describing program semantics.
Inductive com' : Type :=
  | CSkip'
  | CAss' (X: var) (a : aexp)
  | CSeq' (c1 c2 : com')
  | CIf' (b : bexp) (c1 c2 : com')
  | CWhile' (c1: com') (b : bexp) (c : com') (* <--- the real change *)
  | CBreak'
  | CCont'
  .
Of course we can easily define an injection from real programs' syntax trees to these auxiliary syntax trees.
Fixpoint com_inj (c: com): com' :=
  match c with
  | CSkipCSkip'
  | CAss X aCAss' X a
  | CSeq c1 c2CSeq' (com_inj c1) (com_inj c2)
  | CIf b c1 c2CIf' b (com_inj c1) (com_inj c2)
  | CWhile b cCWhile' CSkip' b (com_inj c)
  | CBreakCBreak'
  | CContCCont'
  end.
Then, in our small step semantics, loops' behavior can be described by rules like the followings:
CS_While : st1 st2 c b c1 c2, cstep (c1, st1) (c2, st2) -> cstep (CWhile' c1 b c, st1) (CWhile' c2 b c, st2) CS_WhileNormal : st b c, cstep (CWhile' CSkip' b c, st) (CWhile' (CIf' b c CBreak') b c, st).
Obviously, when a loop body's execution arrive at a break, the whole loop should terminate. You may think of the following rule:
b c st, cstep (CWhile' CBreak' b c, st) (CSkip', st)
However, this rule above is not good enough because "arriving at a break" does not mean that loop body's leftover part is exactly a break. It can be anything that start with a break. Thus, we need the following definition.
Inductive start_with_break: com'-> Prop :=
| SWB_Break: start_with_break CBreak'
| SWB_Seq: c1 c2,
             start_with_break c1 ->
             start_with_break (CSeq' c1 c2).
Similarly, we use the following definitions for commands starting with a continue.
Inductive start_with_cont: com' -> Prop :=
| SWC_Cont: start_with_cont CCont'
| SWC_Seq: c1 c2,
             start_with_cont c1 ->
             start_with_cont (CSeq' c1 c2).
After all, the small step semantics for control-flow-involved programs can be described as follows.
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)
        (CSkip', st2)
  | CS_SeqStep : st c1 c1' st' c2,
      cstep
        (c1, st)
        (c1', st') ->
      cstep
        (CSeq' c1 c2, st)
        (CSeq' c1' c2, st')
  | CS_Seq : st c2,
      cstep
        (CSeq' CSkip' c2, st)
        (c2, st)
  | CS_IfStep : st b b' c1 c2,
      bstep st b b' ->
      cstep
        (CIf' b c1 c2, st)
        (CIf' b' c1 c2, st)
  | CS_IfTrue : st c1 c2,
      cstep
        (CIf' BTrue c1 c2, st)
        (c1, st)
  | CS_IfFalse : st c1 c2,
      cstep
        (CIf' BFalse c1 c2, st)
        (c2, st)
  | CS_While : st1 st2 c b c1 c2, (* <-- new *)
      cstep
        (c1, st1)
        (c2, st2) ->
      cstep
        (CWhile' c1 b c, st1)
        (CWhile' c2 b c, st2)
  | CS_WhileNormal : st b c, (* <-- new *)
      cstep
        (CWhile' CSkip' b c, st)
        (CWhile' (CIf' b c CBreak') b c, st)
  | CS_WhileBreak : st b c_break c, (* <-- new *)
      start_with_break c_break ->
      cstep
        (CWhile' c_break b c, st)
        (CSkip', st)
  | CS_WhileCont : st b c_cont c, (* <-- new *)
      start_with_cont c_cont ->
      cstep
        (CWhile' c_cont b c, st)
        (CWhile' (CIf' b c CBreak') b c, st)
.

End Small_Step_Semantics_Virtual_Loop.

Module Small_Step_Semantics_Control_Stack.
Now we turn to a control-stack-based definition. Here, every element in a control stack describe a loop (loop condition and loop body) and an after-loop command.
Definition cstack: Type := list (bexp * com * com).
Similar to our small step semantics via virtual loops, we need some auxiliary definitions:
  • a command c starts with break: start_with_break c;
  • a command c starts with continue: start_with_break c;
  • a command c is equivalent with a sequential composition of loop CWhile b c1 and another command c2: start_with_loop c b c1 c2.
Inductive start_with_break: com -> Prop :=
| SWB_Break: start_with_break CBreak
| SWB_Seq: c1 c2,
             start_with_break c1 ->
             start_with_break (CSeq c1 c2).

Inductive start_with_cont: com -> Prop :=
| SWC_Cont: start_with_cont CCont
| SWC_Seq: c1 c2,
             start_with_cont c1 ->
             start_with_cont (CSeq c1 c2).

Inductive start_with_loop: com -> bexp -> com -> com -> Prop :=
| SWL_While: b c, start_with_loop (CWhile b c) b c CSkip
| SWL_Seq: c1 b c11 c12 c2,
             start_with_loop c1 b c11 c12 ->
             start_with_loop (CSeq c1 c2) b c11 (CSeq c12 c2).
Now, we are ready to define a small step semantics with control stack. In the following definition, the most important rules are CS_While, CS_Skip, CS_Break and CS_Cont. CS_While says: one more program context will be pushed into the stack. CS_Skip and CS_Cont talk about the scenarios when a loop body ends normally or ends by a continue. CS_Break says: when a loop body ends by a break, a program context will be popped our from the control stack.
Inductive cstep : (com * cstack * state) -> (com * cstack * state) -> Prop :=
  | CS_AssStep : st s X a a',
      astep st a a' ->
      cstep
        (CAss X a, s, st)
        (CAss X a', s, st)
  | CS_Ass : st1 st2 s X n,
      st2 X = n ->
      (Y, XY -> st1 Y = st2 Y) ->
      cstep
        (CAss X (ANum n), s, st1)
        (CSkip, s, st2)
  | CS_SeqStep : st s c1 c1' st' c2,
      cstep
        (c1, s, st)
        (c1', s, st') ->
      cstep
        (CSeq c1 c2, s, st)
        (CSeq c1' c2, s, st')
  | CS_Seq : st s c2,
      cstep
        (CSeq CSkip c2, s, st)
        (c2, s, st)
  | CS_IfStep : st s b b' c1 c2,
      bstep st b b' ->
      cstep
        (CIf b c1 c2, s, st)
        (CIf b' c1 c2, s, st)
  | CS_IfTrue : st s c1 c2,
      cstep
        (CIf BTrue c1 c2, s, st)
        (c1, s, st)
  | CS_IfFalse : st s c1 c2,
      cstep
        (CIf BFalse c1 c2, s, st)
        (c2, s, st)
  | CS_While : st s c b c1 c2, (* <-- new *)
      start_with_loop c b c1 c2 ->
      cstep
        (c, s, st)
        (CIf b c1 CBreak, (b, c1, c2) :: s, st)
  | CS_Skip : st s b c1 c2, (* <-- new *)
      cstep
        (CSkip, (b, c1, c2) :: s, st)
        (CIf b c1 CBreak, (b, c1, c2) :: s, st)
  | CS_Break : st s b c1 c2 c, (* <-- new *)
      start_with_break c ->
      cstep
        (c, (b, c1, c2) :: s, st)
        (c2, s, st)
  | CS_Cont : st s b c1 c2 c, (* <-- new *)
      start_with_cont c ->
      cstep
        (c, (b, c1, c2) :: s, st)
        (CIf b c1 CBreak, (b, c1, c2) :: s, st)
.

End Small_Step_Semantics_Control_Stack.

(* 2021-04-27 01:16 *)