Lecture notes 20210315 Denotational Semantics 2

Remark. Some material in this lecture is from << Software Foundation >> volume 1 and volume 2.
Require Import Coq.Classes.RelationClasses.
Require Import Coq.Classes.Morphisms.
Require Import PL.Imp.

Review: Program Expression's Denotational Semantics

We have learnt how to define integer expression's denotational semantics. We can define it by recursion. In this Coq description, state means the set of program states and every program state is a function from program variables to integer values, i.e. var -> Z.
Module AEval_first_try.

Section AEval.

Variable st: state.

Fixpoint aeval (a : aexp) : Z :=
  match a with
  | ANum nn
  | AId Xst X (* <----- the value of X on program state st *)
  | APlus a1 a2 ⇒ (aeval a1) + (aeval a2)
  | AMinus a1 a2 ⇒ (aeval a1) - (aeval a2)
  | AMult a1 a2 ⇒ (aeval a1) * (aeval a2)
  end.

End AEval.
Recall that fold_constants_aexp folds constant computation in integer expressions.
Fixpoint fold_constants_aexp (a : aexp) : aexp :=
  match a with
  | ANum nANum n
  | AId xAId x
  | APlus a1 a2
    match fold_constants_aexp a1, fold_constants_aexp a2 with
    | ANum n1, ANum n2
        ANum (n1 + n2)
    | _, _
        APlus (fold_constants_aexp a1) (fold_constants_aexp a2)
    end
  | AMinus a1 a2
    match fold_constants_aexp a1, fold_constants_aexp a2 with
    | ANum n1, ANum n2
        ANum (n1 - n2)
    | _, _
        AMinus (fold_constants_aexp a1) (fold_constants_aexp a2)
    end
  | AMult a1 a2
    match fold_constants_aexp a1, fold_constants_aexp a2 with
    | ANum n1, ANum n2
        ANum (n1 * n2)
    | _, _
        AMult (fold_constants_aexp a1) (fold_constants_aexp a2)
    end
  end.
Which statements are correct?
  • 1 + 2 + X will be turned into 3 + X;
  • 1 - 1 + X will be turned into X;
  • X + 1 - 1 will be turned into X + 0;
  • X + 1 * 1 will be turned into X + 1.
Answer:
  • Correct;
  • Incorrect;
  • Incorrect;
  • Correct.

Higher-Order Thinking

Let's take another look of our previous definition of aeval. What is its Coq type?
Check aeval.
(* aeval : state -> aexp -> Z *)
It is a binary function. It takes two arguments, a program state and an integer expression, then generates one integer value. At the same time, we can treate it as a special single-argument function, whose argument type is state and whose result type is aexp -> Z, i.e. a function from integer expressions to integer values.
End AEval_first_try.

Module AEval_second_try.
Now, let's take another definition.
Fixpoint aeval (a : aexp) (st : state) : Z :=
  match a with
  | ANum nn
  | AId Xst X
  | APlus a1 a2 ⇒ (aeval a1 st) + (aeval a2 st)
  | AMinus a1 a2 ⇒ (aeval a1 st) - (aeval a2 st)
  | AMult a1 a2 ⇒ (aeval a1 st) * (aeval a2 st)
  end.
This time, we swap the order of two arguments. As a result, aexp_eval can be interpreted as a two-argument function, or a one-argument function which maps integer expresions into functions from program states to integers.
Sometimes we say that the denotation of an integer expression a is a function from program states to integer values. To be more explicit, we can redefine it as follows via function operations.
End AEval_second_try.

Module Func1.
Module Func.

Definition add {A: Type} (f g: A -> Z): A -> Z :=
  fun af a + g a.

Definition sub {A: Type} (f g: A -> Z): A -> Z :=
  fun af a - g a.

Definition mul {A: Type} (f g: A -> Z): A -> Z :=
  fun af a * g a.

End Func.
End Func1.

Import Func1.

Declare Scope func_scop.
Delimit Scope func_scope with Func.

Notation "f + g" := (Func.add f g): func_scope.
Notation "f - g" := (Func.sub f g): func_scope.
Notation "f * g" := (Func.mul f g): func_scope.
Here, we define Func.add to be the sum of two functions. In Coq, we use fun a ... to represent a function which takes st as its argument. The right hand side expression of fun a represents the function value given this specific argument a. In summary, add is a function which takes two arguments. These two arguments and the function value are themselves functions. The function value is define using fun a ... in Coq. Functions that manipulate other functions are often called higher-order functions (高阶函数). Thus, Func.add is a higher-order function.
In hand written math, we sometimes use f(x) to represent a function and sometimes use f(x) to represent a specific value: the result of applying function f to a specific value x. Moreover, we write f(x) + g(x) to represent the sum of two functions, or two values, which is usually unambiguous from context. In comparison, f+g is not used very often. In Coq, f x + g x is the sum of two numbers while fun x f x + g x is the sum of two functions.
In these definitions, we sometimes use braces "{}" instead of parentheses "()". When braces are used, those arguments are called implicit arguments, i.e. you do not need to write those arguments when you use a function. For using Func.add we do not need to feed that argument A explicitly. Coq can infer that from the context. We also define the subtraction and multiplication of two functions in a similar way.
Based on that, the denotation of an aexp can be defined as:
Definition constant_func {A: Type} (c: Z): A -> Z := fun _c.
Definition query_var (X: var): state -> Z := fun stst X.

Fixpoint aeval (a : aexp) : state -> Z :=
  match a with
  | ANum nconstant_func n
  | AId Xquery_var X
  | APlus a1 a2 ⇒ (aeval a1 + aeval a2)%Func
  | AMinus a1 a2 ⇒ (aeval a1 - aeval a2)%Func
  | AMult a1 a2 ⇒ (aeval a1 * aeval a2)%Func
  end.
Higher-order thinking is critical here. Remember, the denotation of an aexp is a function. Func.add computes the sum of two functions, which is used to define the meaning of APlus, the "+" symbol in expressions.

More Higher-order Objects

Do-it-three-times


Definition doit3times {X:Type} (f:X->X) (n:X) : X :=
  f (f (f n)).
The argument f here is itself a function (from X to X); the body of doit3times applies f three times to some value n.
Check @doit3times.
(* ===> doit3times : forall X : Type, (X -> X) -> X -> X *)

Definition minustwo (x: Z): Z := x - 2.
What is the following computations' result?
   doit3times minustwo 9
   doit3times minustwo (doit3times minustwo 9)
   doit3times (doit3times minustwo) 9
   (doit3times doit3timesminustwo 9
   doit3times (fun n ⇒ n * n) 2
   doit3times (Func.add minustwo) (fun x ⇒ x * x) 4
   doit3times ((fun x y ⇒ y * y - x * y + x * x) 1) 1

Computing sets from functions


Module Func2.
Module Func.
In Coq, we can use A -> Prop to represent subsets of A. Here, a mapping from A to Prop means a criterion judging whether every element of A is in this set.
Definition test_eq {A: Type} (f g: A -> Z): A -> Prop :=
  fun af a = g a.

Definition test_le {A: Type} (f g: A -> Z): A -> Prop :=
  fun af ag a.

End Func.
End Func2.

Import Func2.

Higher-order Properties


Module Func3.
Module Func.

Definition equiv {A: Type} (f g: A -> Z): Prop :=
  a, f a = g a.

Definition le {A: Type} (f g: A -> Z): Prop :=
  a, f ag a.

End Func.
End Func3.

Import Func3.

Module Sets1.
Module Sets.

Definition equiv {A: Type} (X Y: A -> Prop): Prop :=
  a, X aY a.

End Sets.
End Sets1.

Import Sets1.
Here is a lemma about function equivalence.
Lemma Func_add_comm: {A} (f g: A -> Z),
  Func.equiv (f + g)%Func (g + f)%Func.
Proof.
(* WORKED IN CLASS *)
  intros.
  unfold Func.equiv, Func.add.
  intros.
  lia.
Qed.
Obviously, Func.equiv and Sets.equiv are equivalent relations, i.e. they are both reflexive (自反的), symmetric (对称的) and transitive (传递的).
Lemma Func_equiv_refl: A, Reflexive (@Func.equiv A).
Proof.
  intros.
  unfold Reflexive.
Reflexive is defined in Coq standard library. We can unfold its definitions.
  unfold Func.equiv.
  intros.
  reflexivity.
Qed.

Lemma Func_equiv_sym: A, Symmetric (@Func.equiv A).
Proof.
  intros.
  unfold Symmetric.
  unfold Func.equiv.
  intros.
  rewrite H.
  reflexivity.
Qed.

Lemma Func_equiv_trans: A, Transitive (@Func.equiv A).
Proof.
  intros.
  unfold Transitive.
  unfold Func.equiv.
  intros.
  rewrite H, H0.
  reflexivity.
Qed.

Lemma Sets_equiv_refl: A, Reflexive (@Sets.equiv A).
Proof.
  intros.
  unfold Reflexive.
  unfold Sets.equiv.
  intros.
  tauto.
Qed.

Lemma Sets_equiv_sym: A, Symmetric (@Sets.equiv A).
Proof.
  intros.
  unfold Symmetric.
  unfold Sets.equiv.
  intros.
  rewrite H.
  reflexivity.
Qed.

Lemma Sets_equiv_trans: A, Transitive (@Sets.equiv A).
Proof.
  intros.
  unfold Transitive.
  unfold Sets.equiv.
  intros.
  rewrite H, H0.
  reflexivity.
Qed.
Moreover, Func.equiv is preserved by Func.add, Func.sub and Func.mul. You may want to state a lemma as follows:
Lemma Func_add_equiv_naive: A (f1 f2 g1 g2: A -> Z),
  Func.equiv f1 f2 ->
  Func.equiv g1 g2 ->
  Func.equiv (f1 + g1)%Func (f2 + g2)%Func.
Proof.
Abort.
Coq suggests you use Proper to describe such preservation theorems.
Lemma Func_add_equiv: A,
  Proper (@Func.equiv A ==> @Func.equiv A ==> @Func.equiv A) Func.add.
Proof.
  intros.
The following line exposes Proper's meaning.
  unfold Proper, respectful.
  intros f1 f2 ? g1 g2 ?.
  unfold Func.equiv in H.
  unfold Func.equiv in H0.
  unfold Func.equiv.
  intros.
  unfold Func.add.
  rewrite H, H0.
  reflexivity.
Qed.

Lemma Func_sub_equiv: A,
  Proper (@Func.equiv A ==> @Func.equiv A ==> @Func.equiv A) Func.sub.
Proof.
  intros.
  unfold Proper, respectful.
  intros f1 f2 ? g1 g2 ?.
  unfold Func.equiv in H.
  unfold Func.equiv in H0.
  unfold Func.equiv.
  intros.
  unfold Func.sub.
  rewrite H, H0.
  reflexivity.
Qed.

Lemma Func_mul_equiv: A,
  Proper (@Func.equiv A ==> @Func.equiv A ==> @Func.equiv A) Func.mul.
Proof.
  intros.
  unfold Proper, respectful.
  intros f1 f2 ? g1 g2 ?.
  unfold Func.equiv in H.
  unfold Func.equiv in H0.
  unfold Func.equiv.
  intros.
  unfold Func.mul.
  rewrite H, H0.
  reflexivity.
Qed.
Why using Proper is better than Func_add_equiv_naive? Coq supports rewriting via Proper!
Existing Instances Func_equiv_refl
                   Func_equiv_sym
                   Func_equiv_trans
                   Func_add_equiv
                   Func_sub_equiv
                   Func_mul_equiv.

Fact domo_of_rewrite: f g h j l: Z -> Z,
  Func.equiv f (j + l)%Func ->
  Func.equiv g h ->
  Func.equiv (g + f)%Func (j + l + h)%Func.
Proof.
  intros.
  rewrite (Func_add_comm g f).
  rewrite H.
  rewrite H0.
  reflexivity.
Qed.
The first rewrite is sound because Func.equiv is transitive. The second and third rewrite are sound because Func.equiv is preserved by Func.add and Func.equiv is reflexive. Coq reasons about that automatically according to the registrations of Existing Instances.
Again, do not drown in Coq's details! But try to understand higher-order functions and their higher-order properties. This example shows how higher order objects help describing nontrivial theories in an abstract and intuitive way.

Evaluating Boolean Expressions

We will use higher-order objects to define boolean expressions' denotations. Before that, we need more definitions about sets.
Module Sets2.
Module Sets.

Definition full {A: Type}: A -> Prop := fun _True.

Definition empty {A: Type}: A -> Prop := fun _False.

Definition intersect {A: Type} (X Y: A -> Prop) := fun aX aY a.

Definition complement {A: Type} (X: A -> Prop) := fun a ⇒ ¬X a.

End Sets.
End Sets2.

Import Sets2.
Recall that our syntax trees of boolean expressions are: b ::= true | false | a == a | a <= a | ! b | b && b
Fixpoint beval (b : bexp) : state -> Prop :=
  match b with
  | BTrueSets.full
  | BFalseSets.empty
  | BEq a1 a2Func.test_eq (aeval a1) (aeval a2)
  | BLe a1 a2Func.test_le (aeval a1) (aeval a2)
  | BNot b1Sets.complement (beval b1)
  | BAnd b1 b2Sets.intersect (beval b1) (beval b2)
  end.

Evaluating Command

Next we need to define what it means to evaluate a command. One idea is to define such evaluation as a function from beginning state and command to ending state. But the fact that WHILE loops don't necessarily terminate means that such evaluating function cannot be a total function; it must be a partial function. Although such definition is no problem in theory, computer scientists choose not to do this since it is less extensible. Also, if you try do write it in Coq, it is nontrivial.
Usually, computer scientists use a set of state pairs S to represent a program c's denotation. Specifically, if a program state pair (st1, st2) is an element of S, then executing c from state st1 may terminate with state st2. In other words, the denotation of a program has type state -> state -> Prop in Coq. Remark: this is different from Hoare triples. Hoare triples are about assertion pairs but a program's denotation is about program state pairs.
A set of program state pairs is also called a binary relation between program states. In Coq, we can use state -> state -> Prop to present such type. It is like using A -> Prop to describe subsets of A. As a preparation, we first define some basic concepts about relations.
Module BinRel.

Definition id {A: Type}: A -> A -> Prop := fun a ba = b.

Definition empty {A B: Type}: A -> B -> Prop := fun a bFalse.

Definition concat {A B C: Type} (r1: A -> B -> Prop) (r2: B -> C -> Prop): A -> C -> Prop :=
  fun a cb, r1 a br2 b c.

Definition union {A B: Type} (r1 r2: A -> B -> Prop): A -> B -> Prop :=
  fun a br1 a br2 a b.

Definition intersection {A B: Type} (r1 r2: A -> B -> Prop): A -> B -> Prop :=
  fun a br1 a br2 a b.

Definition test_rel {A: Type} (X: A -> Prop): A -> A -> Prop :=
  fun x yx = yX x.

End BinRel.
Using these basic definitions about relation, we can easily define the denotation of empty commands, assignment commands, sequential composition and if-then-else commands.
Module CEval_first_try.

Definition if_sem
  (b: bexp)
  (then_branch else_branch: state -> state -> Prop)
  : state -> state -> Prop
:=
  BinRel.union
    (BinRel.concat (BinRel.test_rel (beval b)) then_branch)
    (BinRel.concat (BinRel.test_rel (beval (BNot b))) else_branch).

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 _ _BinRel.empty
  end.

End CEval_first_try.

(* 2021-03-15 23:15 *)