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.
Require Import Coq.Classes.Morphisms.
Require Import PL.Imp.
Review: Program Expression's Denotational Semantics
Module AEval_first_try.
Section AEval.
Variable st: state.
Fixpoint aeval (a : aexp) : Z :=
match a with
| ANum n ⇒ n
| AId X ⇒ st 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.
Section AEval.
Variable st: state.
Fixpoint aeval (a : aexp) : Z :=
match a with
| ANum n ⇒ n
| AId X ⇒ st 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 n ⇒ ANum n
| AId x ⇒ AId 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.
match a with
| ANum n ⇒ ANum n
| AId x ⇒ AId 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?
Answer:
Let's take another look of our previous definition of aeval. What is its
Coq type?
- 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.
- Correct;
- Incorrect;
- Incorrect;
- Correct.
Higher-Order Thinking
Check aeval.
(* aeval : state -> aexp -> Z *)
(* 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.
Module AEval_second_try.
Now, let's take another definition.
Fixpoint aeval (a : aexp) (st : state) : Z :=
match a with
| ANum n ⇒ n
| AId X ⇒ st 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.
match a with
| ANum n ⇒ n
| AId X ⇒ st 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 a ⇒ f a + g a.
Definition sub {A: Type} (f g: A -> Z): A -> Z :=
fun a ⇒ f a - g a.
Definition mul {A: Type} (f g: A -> Z): A -> Z :=
fun a ⇒ f 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.
Module Func1.
Module Func.
Definition add {A: Type} (f g: A -> Z): A -> Z :=
fun a ⇒ f a + g a.
Definition sub {A: Type} (f g: A -> Z): A -> Z :=
fun a ⇒ f a - g a.
Definition mul {A: Type} (f g: A -> Z): A -> Z :=
fun a ⇒ f 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 st ⇒ st X.
Fixpoint aeval (a : aexp) : state -> Z :=
match a with
| ANum n ⇒ constant_func n
| AId X ⇒ query_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.
Definition query_var (X: var): state -> Z := fun st ⇒ st X.
Fixpoint aeval (a : aexp) : state -> Z :=
match a with
| ANum n ⇒ constant_func n
| AId X ⇒ query_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.
(* ===> 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 doit3times) minustwo 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 a ⇒ f a = g a.
Definition test_le {A: Type} (f g: A -> Z): A -> Prop :=
fun a ⇒ f a ≤ g a.
End Func.
End Func2.
Import Func2.
fun a ⇒ f a = g a.
Definition test_le {A: Type} (f g: A -> Z): A -> Prop :=
fun a ⇒ f a ≤ g a.
End Func.
End Func2.
Import Func2.
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 a ≤ g a.
End Func.
End Func3.
Import Func3.
Module Sets1.
Module Sets.
Definition equiv {A: Type} (X Y: A -> Prop): Prop :=
∀a, X a ↔ Y 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.
Func.equiv (f + g)%Func (g + f)%Func.
Proof.
(* WORKED IN CLASS *)
intros.
unfold Func.equiv, Func.add.
intros.
lia.
Qed.
(* 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.
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).
Lemma Func_equiv_trans: ∀A, Transitive (@Func.equiv A).
Lemma Sets_equiv_refl: ∀A, Reflexive (@Sets.equiv A).
Lemma Sets_equiv_sym: ∀A, Symmetric (@Sets.equiv A).
Lemma Sets_equiv_trans: ∀A, Transitive (@Sets.equiv A).
intros.
reflexivity.
Qed.
Lemma Func_equiv_sym: ∀A, Symmetric (@Func.equiv A).
Proof.
intros.
unfold Symmetric.
unfold Func.equiv.
intros.
rewrite H.
reflexivity.
Qed.
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.
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.
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.
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.
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.
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.
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.
Lemma Func_mul_equiv: ∀A,
Proper (@Func.equiv A ==> @Func.equiv A ==> @Func.equiv A) Func.mul.
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.
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.
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.
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.
We will use higher-order objects to define boolean expressions' denotations.
Before that, we need more definitions about sets.
Evaluating Boolean Expressions
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 a ⇒ X a ∧ Y a.
Definition complement {A: Type} (X: A -> Prop) := fun a ⇒ ¬X a.
End Sets.
End Sets2.
Import 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 a ⇒ X a ∧ Y 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
| BTrue ⇒ Sets.full
| BFalse ⇒ Sets.empty
| BEq a1 a2 ⇒ Func.test_eq (aeval a1) (aeval a2)
| BLe a1 a2 ⇒ Func.test_le (aeval a1) (aeval a2)
| BNot b1 ⇒ Sets.complement (beval b1)
| BAnd b1 b2 ⇒ Sets.intersect (beval b1) (beval b2)
end.
match b with
| BTrue ⇒ Sets.full
| BFalse ⇒ Sets.empty
| BEq a1 a2 ⇒ Func.test_eq (aeval a1) (aeval a2)
| BLe a1 a2 ⇒ Func.test_le (aeval a1) (aeval a2)
| BNot b1 ⇒ Sets.complement (beval b1)
| BAnd b1 b2 ⇒ Sets.intersect (beval b1) (beval b2)
end.
Evaluating Command
Module BinRel.
Definition id {A: Type}: A -> A -> Prop := fun a b ⇒ a = b.
Definition empty {A B: Type}: A -> B -> Prop := fun a b ⇒ False.
Definition concat {A B C: Type} (r1: A -> B -> Prop) (r2: B -> C -> Prop): A -> C -> Prop :=
fun a c ⇒ ∃b, r1 a b ∧ r2 b c.
Definition union {A B: Type} (r1 r2: A -> B -> Prop): A -> B -> Prop :=
fun a b ⇒ r1 a b ∨ r2 a b.
Definition intersection {A B: Type} (r1 r2: A -> B -> Prop): A -> B -> Prop :=
fun a b ⇒ r1 a b ∧ r2 a b.
Definition test_rel {A: Type} (X: A -> Prop): A -> A -> Prop :=
fun x y ⇒ x = y ∧ X x.
End BinRel.
Definition id {A: Type}: A -> A -> Prop := fun a b ⇒ a = b.
Definition empty {A B: Type}: A -> B -> Prop := fun a b ⇒ False.
Definition concat {A B C: Type} (r1: A -> B -> Prop) (r2: B -> C -> Prop): A -> C -> Prop :=
fun a c ⇒ ∃b, r1 a b ∧ r2 b c.
Definition union {A B: Type} (r1 r2: A -> B -> Prop): A -> B -> Prop :=
fun a b ⇒ r1 a b ∨ r2 a b.
Definition intersection {A B: Type} (r1 r2: A -> B -> Prop): A -> B -> Prop :=
fun a b ⇒ r1 a b ∧ r2 a b.
Definition test_rel {A: Type} (X: A -> Prop): A -> A -> Prop :=
fun x y ⇒ x = y ∧ X 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
| CSkip ⇒ BinRel.id
| CAss X E ⇒
fun st1 st2 ⇒
st2 X = aeval E st1 ∧
∀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 _ _ ⇒ BinRel.empty
end.
End CEval_first_try.
(* 2021-03-15 23:15 *)
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
| CSkip ⇒ BinRel.id
| CAss X E ⇒
fun st1 st2 ⇒
st2 X = aeval E st1 ∧
∀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 _ _ ⇒ BinRel.empty
end.
End CEval_first_try.
(* 2021-03-15 23:15 *)