Lecture notes 20210407 Denotations vs Triples 1

Require Import PL.Imp.

Review: Hoare Logic


Module HoareLogic.
In the first couple of lectures of this course, we learnt using Hoare logic to prove program correctness. Here is the set of Hoare logic proof rules.
Import Concrete_Pretty_Printing.

Axiom hoare_seq : (P Q R: Assertion) (c1 c2: com),
   {{P}c1  {{Q}}  ->
   {{Q}c2  {{R}}  ->
   {{P}c1;;c2  {{R}} .

Axiom hoare_skip : P,
   {{P}Skip  {{P}} .

Axiom hoare_if : P Q b c1 c2,
   {{ P AND [[b]] }c1  {{ Q }}  ->
   {{ P AND NOT [[b]] }c2  {{ Q }}  ->
   {{ P }If b Then c1 Else c2 EndIf  {{ Q }} .

Axiom hoare_while : P b c,
   {{ P AND [[b]] }c  {{P}}  ->
   {{P}While b Do c EndWhile  {{ P AND NOT [[b]] }} .

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

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

Axiom hoare_consequence : (P P' Q Q' : Assertion) c,
  P  P' ->
   {{P'}c  {{Q'}}  ->
  Q'  Q ->
   {{P}c  {{Q}} .

End HoareLogic.
This time, we are going to study the meta properties of the logic ranther than only use it. For example, we wonder whether the program behavior defined by Hoare logic is the same as the one defined by denotational semantics. Here is a formalization of syntactic definitions related to Hoare logic. You may compare this version with the one in the Imp library; they are only a little bit different.
Import Abstract_Pretty_Printing.

Assertion Language

Logical variables are used together with universal quantifiers and existential quantifiers. In order to formalize the assertions language, we use logical variables' identifiers to distinguish them, i.e. we just call then the 0th logical variable, 1st logical variable, 2nd logical variable etc.
Definition logical_var: Type := nat.
Logical variables may appear in program expressions to represent a special constant. For example, in the forward assignment rule, the postcondition is
    EXISTS xP [X ⟼ xAND [[X]] = [E [X ⟼ x]].
In this assertion X x describes the action of replacing X with constant expression x. One possible definition of variable expressions' (可变表达式) syntax tree is as follows.
Module Variable_Expression_Attempt.

Inductive aexp' : Type :=
  | ANum' (n : Z)
  | AId' (X : var)
  | ALid (x: logical_var)
  | APlus' (a1 a2 : aexp')
  | AMinus' (a1 a2 : aexp')
  | AMult' (a1 a2 : aexp').

End Variable_Expression_Attempt.
In reality, we need more expressiveness. For example, in order to prove
    [[Y]] = [[X]] * 2 + 1  ⊢ EXISTS z[[(Y + 1) - 2 * z]] = 0
we would like to prove result of instantiating the existentially quantified variable z with [[X]] + 1 . Specifically, the derivation above immediately follows the statement below:
    [[Y]] = [[X]] * 2 + 1  ⊢ [[(Y + 1) - 2 * ([[X]] + 1) ]] = 0.
In this statement, (Y + 1) - 2 * ([[X]] + 1) represents the following syntax tree:
       -
     /   \
    /     \
   +       *
  / \     / \
 /   \   /   \
Y    1   2 [[X]] + 1
in which the right most leaf is a constant whose value is [[X]] + 1. We use Coq's mutually inductive type to define such syntax trees.
Inductive aexp' : Type :=
  | ANum' (t : term)
  | AId' (X: var)
  | APlus' (a1 a2 : aexp')
  | AMinus' (a1 a2 : aexp')
  | AMult' (a1 a2 : aexp')
with term : Type :=
  | TNum (n : Z)
  | TId (x: logical_var)
  | TDenote (a : aexp')
  | TPlus (t1 t2 : term)
  | TMinus (t1 t2 : term)
  | TMult (t1 t2 : term).
Here, an integer term in assertions can be a constant, a program expression's value, the sum of two subterms, the subtraction of two subterms or the multiplication of two terms. Also, we define variable bool expressions based on variable integer expressions.
Inductive bexp' : Type :=
  | BTrue'
  | BFalse'
  | BEq' (a1 a2 : aexp')
  | BLe' (a1 a2 : aexp')
  | BNot' (b : bexp')
  | BAnd' (b1 b2 : bexp').
The following are some notations for pretty printing. It is worth noticing those Coercion statements. A coercion from type A to type B allows Coq to treat a value of type A as a value of type B when necessary. For example, Coercion ANum' means ANum' t can be written as t for convenience.
Coercion ANum' : term >-> aexp'.
Coercion AId' : var >-> aexp'.
Bind Scope vimp_scope with aexp'.
Bind Scope vimp_scope with bexp'.
Delimit Scope vimp_scope with vimp.

Notation "x + y" := (APlus' x y) (at level 50, left associativity) : vimp_scope.
Notation "x - y" := (AMinus' x y) (at level 50, left associativity) : vimp_scope.
Notation "x * y" := (AMult' x y) (at level 40, left associativity) : vimp_scope.
Notation "x ≤ y" := (BLe' x y) (at level 70, no associativity) : vimp_scope.
Notation "x == y" := (BEq' x y) (at level 70, no associativity) : vimp_scope.
Notation "x && y" := (BAnd' x y) (at level 40, left associativity) : vimp_scope.
Notation "'!' b" := (BNot' b) (at level 39, right associativity) : vimp_scope.

Coercion TNum : Z >-> term.
Coercion TId: logical_var >-> term.
Bind Scope term_scope with term.
Delimit Scope term_scope with term.

Notation "x + y" := (TPlus x y) (at level 50, left associativity) : term_scope.
Notation "x - y" := (TMinus x y) (at level 50, left associativity) : term_scope.
Notation "x * y" := (TMult x y) (at level 40, left associativity) : term_scope.
Notation "[[ a ]]" := (TDenote ((a)%vimp)) (at level 30, no associativity) : term_scope.
Of course, every normal expression is a variable expression.
Fixpoint ainj (a: aexp): aexp' :=
  match a with
  | ANum nANum' (TNum n)
  | AId XAId' X
  | APlus a1 a2APlus' (ainj a1) (ainj a2)
  | AMinus a1 a2AMinus' (ainj a1) (ainj a2)
  | AMult a1 a2AMult' (ainj a1) (ainj a2)
  end.

Fixpoint binj (b : bexp): bexp' :=
  match b with
  | BTrueBTrue'
  | BFalseBFalse'
  | BEq a1 a2BEq' (ainj a1) (ainj a2)
  | BLe a1 a2BLe' (ainj a1) (ainj a2)
  | BNot b1BNot' (binj b1)
  | BAnd b1 b2BAnd' (binj b1) (binj b2)
  end.
The following two lines of Coercion definition say that Coq will treat a as ainj b and treat b a s binj b automatically when a variable expression is needed.
Coercion ainj: aexp >-> aexp'.
Coercion binj: bexp >-> bexp'.

Module example.

Example coercion_ex: ainj (APlus (ANum 0) (ANum 1)) = APlus' (ANum' 0) (ANum' 1).
Proof.
The left hand side is actually not a normal expression, but a variable expression too. The Coercion definition tells Coq to hide that ainj when printing it out,
  simpl.
  reflexivity.
Qed.

End example.
Next, we define the syntax tree of assertions.
Inductive Assertion : Type :=
  | AssnLe (t1 t2 : term)
  | AssnLt (t1 t2 : term)
  | AssnEq (t1 t2 : term)
  | AssnDenote (b: bexp')
  | AssnOr (P1 P2 : Assertion)
  | AssnAnd (P1 P2 : Assertion)
  | AssnImpl (P1 P2 : Assertion)
  | AssnNot (P: Assertion)
  | AssnExists (x: logical_var) (P: Assertion)
  | AssnForall (x: logical_var) (P: Assertion).

Bind Scope assert_scope with Assertion.
Delimit Scope assert_scope with assert.

Notation "x ≤ y" := (AssnLe ((x)%term) ((y)%term)) (at level 70, no associativity) : assert_scope.
Notation "x '<' y" := (AssnLt ((x)%term) ((y)%term)) (at level 70, no associativity) : assert_scope.
Notation "x = y" := (AssnEq ((x)%term) ((y)%term)) (at level 70, no associativity) : assert_scope.
Notation "[[ b ]]" := (AssnDenote ((b)%vimp)) (at level 30, no associativity) : assert_scope.
Notation "P1 'OR' P2" := (AssnOr P1 P2) (at level 76, left associativity) : assert_scope.
Notation "P1 'AND' P2" := (AssnAnd P1 P2) (at level 74, left associativity) : assert_scope.
Notation "P1 'IMPLY' P2" := (AssnImpl P1 P2) (at level 74, left associativity) : assert_scope.
Notation "'NOT' P" := (AssnNot P) (at level 73, right associativity) : assert_scope.
Notation "'EXISTS' x ',' P " := (AssnExists x ((P)%assert)) (at level 77, right associativity) : assert_scope.
Notation "'FORALL' x ',' P " := (AssnForall x ((P)%assert)) (at level 77, right associativity) : assert_scope.
Based on these definitions, we are already able to write assertions and triples.
Inductive hoare_triple: Type :=
| Build_hoare_triple (P: Assertion) (c: com) (Q: Assertion).

Notation " {{ P }}  c  {{ Q }} " :=
  (Build_hoare_triple P c%imp Q) (at level 90, c at next level).

Module Assertion_Triple_Example.

Definition X: var := 0%nat.
Definition Y: var := 1%nat.
Definition TEMP: var := 99%nat.
Definition n: logical_var := 0%nat.
Definition m: logical_var := 1%nat.
Definition k: logical_var := 2%nat.
Definition q: logical_var := 3%nat.

Definition assertion_ex1: Assertion :=
  [[X]] = n AND [[Y]] = m.

Definition assertion_ex2: Assertion :=
  EXISTS q, [[X]] * k + q = m AND 0 ≤ q AND q < k.

Definition triple_ex: hoare_triple :=
   {{ [[X]] = n AND [[Y]] = m }
  TEMP ::= X;;
  X ::= Y;;
  Y ::= TEMP
   {{ [[X]] = m AND [[Y]] = n }} .

End Assertion_Triple_Example.

Syntactic Substitution

In order to formulate Hoare logic proof rules, we need to formalize syntactic substition of program variables. Here we define the result of replacing X with E. Since integer variable expressions and integer terms are defined as mutually inductive types, this substitution should be defined as mutual recursion.
Fixpoint aexp_sub (X: var) (E: aexp') (a: aexp'): aexp' :=
    match a with
    | ANum' tANum' (term_sub X E t)
    | AId' X'
         if Nat.eq_dec X X'
         then E
         else AId' X'
    | APlus' a1 a2APlus' (aexp_sub X E a1) (aexp_sub X E a2)
    | AMinus' a1 a2AMinus' (aexp_sub X E a1) (aexp_sub X E a2)
    | AMult' a1 a2AMult' (aexp_sub X E a1) (aexp_sub X E a2)
    end
with term_sub (X: var) (E: aexp') (t: term) :=
    match t with
    | TNum nTNum n
    | TId xTId x
    | TDenote aTDenote (aexp_sub X E a)
    | TPlus t1 t2TPlus (term_sub X E t1) (term_sub X E t2)
    | TMinus t1 t2TMinus (term_sub X E t1) (term_sub X E t2)
    | TMult t1 t2TMult (term_sub X E t1) (term_sub X E t2)
    end.

Fixpoint bexp_sub (X: var) (E: aexp') (b: bexp'): bexp' :=
    match b with
    | BTrue'BTrue'
    | BFalse'BFalse'
    | BEq' a1 a2BEq' (aexp_sub X E a1) (aexp_sub X E a2)
    | BLe' a1 a2BLe' (aexp_sub X E a1) (aexp_sub X E a2)
    | BNot' bBNot' (bexp_sub X E b)
    | BAnd' b1 b2BAnd' (bexp_sub X E b1) (bexp_sub X E b2)
    end.
The definition till now is trivial. But we must be very careful when defining substitution in assertions. A naive attempt will not work.
Module Assertion_Sub_Attempt.

Fixpoint assn_sub (X: var) (E: aexp') (d: Assertion): Assertion :=
    match d with
    | AssnLe t1 t2AssnLe (term_sub X E t1) (term_sub X E t2)
    | AssnLt t1 t2AssnLt (term_sub X E t1) (term_sub X E t2)
    | AssnEq t1 t2AssnEq (term_sub X E t1) (term_sub X E t2)
    | AssnDenote bAssnDenote (bexp_sub X E b)
    | AssnOr P1 P2AssnOr (assn_sub X E P1) (assn_sub X E P2)
    | AssnAnd P1 P2AssnAnd (assn_sub X E P1) (assn_sub X E P2)
    | AssnImpl P1 P2AssnImpl (assn_sub X E P1) (assn_sub X E P2)
    | AssnNot PAssnNot (assn_sub X E P)
    | AssnExists x PAssnExists x (assn_sub X E P)
    | AssnForall x PAssnForall x (assn_sub X E P)
    end.

End Assertion_Sub_Attempt.
What's wrong? Consider the following substitution,
    (Exists x[[X]] = x + 1) [X ⟼ x]
Theoretically, the correct substition result should be:
    (Exists x[[X]] = x + 1) [X ⟼ x]    ===>
    (Exists y[[X]] = y + 1) [X ⟼ x]    ===>
    Exists yx = y + 1.
But the definition above says:
    (Exists x[[X]] = x + 1) [X ⟼ x]    ===>
    Exists xx = x + 1
which does not make sense. The lesson that we learnt from this failure is that we need to define logical variable's renaming first.
Fixpoint aexp_rename (x y: logical_var) (a: aexp'): aexp' :=
    match a with
    | ANum' tANum' (term_rename x y t)
    | AId' XAId' X
    | APlus' a1 a2APlus' (aexp_rename x y a1) (aexp_rename x y a2)
    | AMinus' a1 a2AMinus' (aexp_rename x y a1) (aexp_rename x y a2)
    | AMult' a1 a2AMult' (aexp_rename x y a1) (aexp_rename x y a2)
    end
with term_rename (x y: logical_var) (t: term) :=
    match t with
    | TNum nTNum n
    | TId x'
        if Nat.eq_dec x x'
        then TId y
        else TId x'
    | TDenote aTDenote (aexp_rename x y a)
    | TPlus t1 t2TPlus (term_rename x y t1) (term_rename x y t2)
    | TMinus t1 t2TMinus (term_rename x y t1) (term_rename x y t2)
    | TMult t1 t2TMult (term_rename x y t1) (term_rename x y t2)
    end.

Fixpoint bexp_rename (x y: logical_var) (b: bexp'): bexp' :=
    match b with
    | BTrue'BTrue'
    | BFalse'BFalse'
    | BEq' a1 a2BEq' (aexp_rename x y a1) (aexp_rename x y a2)
    | BLe' a1 a2BLe' (aexp_rename x y a1) (aexp_rename x y a2)
    | BNot' bBNot' (bexp_rename x y b)
    | BAnd' b1 b2BAnd' (bexp_rename x y b1) (bexp_rename x y b2)
    end.

Fixpoint assn_rename (x y: logical_var) (d: Assertion): Assertion :=
    match d with
    | AssnLe t1 t2AssnLe (term_rename x y t1) (term_rename x y t2)
    | AssnLt t1 t2AssnLt (term_rename x y t1) (term_rename x y t2)
    | AssnEq t1 t2AssnEq (term_rename x y t1) (term_rename x y t2)
    | AssnDenote bAssnDenote (bexp_rename x y b)
    | AssnOr P1 P2AssnOr (assn_rename x y P1) (assn_rename x y P2)
    | AssnAnd P1 P2AssnAnd (assn_rename x y P1) (assn_rename x y P2)
    | AssnImpl P1 P2AssnImpl (assn_rename x y P1) (assn_rename x y P2)
    | AssnNot PAssnNot (assn_rename x y P)
    | AssnExists x' Pif Nat.eq_dec x x'
                         then AssnExists x' P
                         else AssnExists x' (assn_rename x y P)
    | AssnForall x' Pif Nat.eq_dec x x'
                         then AssnForall x' P
                         else AssnForall x' (assn_rename x y P)
    end.
Also, we need to find a logical variable which is not yet used. This is easy — we just choose the largest used variable index's successor.
Fixpoint aexp_max_var (a: aexp'): logical_var :=
    match a with
    | ANum' tterm_max_var t
    | AId' XO
    | APlus' a1 a2max (aexp_max_var a1) (aexp_max_var a2)
    | AMinus' a1 a2max (aexp_max_var a1) (aexp_max_var a2)
    | AMult' a1 a2max (aexp_max_var a1) (aexp_max_var a2)
    end
with term_max_var (t: term): logical_var :=
    match t with
    | TNum nO
    | TId xx
    | TDenote aaexp_max_var a
    | TPlus t1 t2max (term_max_var t1) (term_max_var t2)
    | TMinus t1 t2max (term_max_var t1) (term_max_var t2)
    | TMult t1 t2max (term_max_var t1) (term_max_var t2)
    end.

Fixpoint bexp_max_var (b: bexp'): logical_var :=
    match b with
    | BTrue'O
    | BFalse'O
    | BEq' a1 a2max (aexp_max_var a1) (aexp_max_var a2)
    | BLe' a1 a2max (aexp_max_var a1) (aexp_max_var a2)
    | BNot' bbexp_max_var b
    | BAnd' b1 b2max (bexp_max_var b1) (bexp_max_var b2)
    end.

Fixpoint assn_max_var (d: Assertion): logical_var :=
    match d with
    | AssnLe t1 t2max (term_max_var t1) (term_max_var t2)
    | AssnLt t1 t2max (term_max_var t1) (term_max_var t2)
    | AssnEq t1 t2max (term_max_var t1) (term_max_var t2)
    | AssnDenote bbexp_max_var b
    | AssnOr P1 P2max (assn_max_var P1) (assn_max_var P2)
    | AssnAnd P1 P2max (assn_max_var P1) (assn_max_var P2)
    | AssnImpl P1 P2max (assn_max_var P1) (assn_max_var P2)
    | AssnNot Passn_max_var P
    | AssnExists x' Pmax x' (assn_max_var P)
    | AssnForall x' Pmax x' (assn_max_var P)
    end.

Definition new_var (P: Assertion) (E: aexp'): logical_var :=
  S (max (assn_max_var P) (aexp_max_var E)).
Now we need to determine whether a renaming is necessary in substition. Consider a substition of the following form (where logical variable x may appear in P):
    (EXISTS xP) [X ⟼ E].
Do we need a renaming from x to some unused variable y? It depends on whether x occurs in E. The following function computes the number of x's occurrence in E.
Fixpoint aexp_occur (x: logical_var) (a: aexp'): nat :=
    match a with
    | ANum' tterm_occur x t
    | AId' XO
    | APlus' a1 a2 ⇒ (aexp_occur x a1) + (aexp_occur x a2)
    | AMinus' a1 a2 ⇒ (aexp_occur x a1) + (aexp_occur x a2)
    | AMult' a1 a2 ⇒ (aexp_occur x a1) + (aexp_occur x a2)
    end
with term_occur (x: logical_var) (t: term): nat :=
    match t with
    | TNum nO
    | TId x'if Nat.eq_dec x x' then S O else O
    | TDenote aaexp_occur x a
    | TPlus t1 t2 ⇒ (term_occur x t1) + (term_occur x t2)
    | TMinus t1 t2 ⇒ (term_occur x t1) + (term_occur x t2)
    | TMult t1 t2 ⇒ (term_occur x t1) + (term_occur x t2)
    end.
Eventually, we can define syntactic substition in assertions.
Fixpoint rename_all (E: aexp') (d: Assertion): Assertion :=
    match d with
    | AssnLe t1 t2AssnLe t1 t2
    | AssnLt t1 t2AssnLt t1 t2
    | AssnEq t1 t2AssnEq t1 t2
    | AssnDenote bAssnDenote b
    | AssnOr P1 P2AssnOr (rename_all E P1) (rename_all E P2)
    | AssnAnd P1 P2AssnAnd (rename_all E P1) (rename_all E P2)
    | AssnImpl P1 P2AssnImpl (rename_all E P1) (rename_all E P2)
    | AssnNot PAssnNot (rename_all E P)
    | AssnExists x Pmatch aexp_occur x E with
                        | OAssnExists x (rename_all E P)
                        | _AssnExists
                                 (new_var (rename_all E P) E)
                                 (assn_rename x
                                   (new_var (rename_all E P) E)
                                   (rename_all E P))
                        end
    | AssnForall x Pmatch aexp_occur x E with
                        | OAssnForall x (rename_all E P)
                        | _AssnForall
                                 (new_var (rename_all E P) E)
                                 (assn_rename x
                                   (new_var (rename_all E P) E)
                                   (rename_all E P))
                        end
    end.

Fixpoint naive_sub (X: var) (E: aexp') (d: Assertion): Assertion :=
    match d with
    | AssnLe t1 t2AssnLe (term_sub X E t1) (term_sub X E t2)
    | AssnLt t1 t2AssnLt (term_sub X E t1) (term_sub X E t2)
    | AssnEq t1 t2AssnEq (term_sub X E t1) (term_sub X E t2)
    | AssnDenote bAssnDenote (bexp_sub X E b)
    | AssnOr P1 P2AssnOr (naive_sub X E P1) (naive_sub X E P2)
    | AssnAnd P1 P2AssnAnd (naive_sub X E P1) (naive_sub X E P2)
    | AssnImpl P1 P2AssnImpl (naive_sub X E P1) (naive_sub X E P2)
    | AssnNot PAssnNot (naive_sub X E P)
    | AssnExists x PAssnExists x (naive_sub X E P)
    | AssnForall x PAssnForall x (naive_sub X E P)
    end.

Definition assn_sub (X: var) (E: aexp') (P: Assertion): Assertion :=
  naive_sub X E (rename_all E P).

Notation "P [ X ⟼ E ]" := (assn_sub X E ((P)%assert)) (at level 10, X at next level) : assert_scope.
Notation "a [ X ⟼ E ]" := (aexp_sub X E ((a)%vimp)) (at level 10, X at next level) : vimp_scope.
In logic text books, substitution in a quantifed assertion is defined as renaming first followed by recursive substitution. In formally,
    (EXISTS xP) [X ⟼ E]    ===>
    EXISTS yP [x ⟼ y] [X ⟼ E]
in which y is an unused logical variable. Our definition of assn_sub is not that natural comparing this traditional approach. We write this definition like only in order to fit Coq's requirement of structure recursion.

Hoare logic's Proof System

In logic studies, a set of proof rules, which can be used compositionally in reasoning, is called a proof system (推理系统), or a logic. A Hoare triple is called provable (可证) if we can prove it in the Hoare logic within finite steps. Thus, "provable" can be defined in Coq as an inductive predicate.
Module Attempt1.

Inductive provable: hoare_triple -> Prop :=
  | hoare_seq : (P Q R: Assertion) (c1 c2: com),
      provable ( {{P}c1  {{Q}} ) ->
      provable ( {{Q}c2  {{R}} ) ->
      provable ( {{P}c1;;c2  {{R}} )
  | hoare_skip : P,
      provable ( {{P}Skip  {{P}} )
  | hoare_if : P Q (b: bexp) c1 c2,
      provable ( {{ P AND [[b]] }c1  {{ Q }} ) ->
      provable ( {{ P AND NOT [[b]] }c2  {{ Q }} ) ->
      provable ( {{ P }If b Then c1 Else c2 EndIf  {{ Q }} )
  | hoare_while : P (b: bexp) c,
      provable ( {{ P AND [[b]] }c  {{P}} ) ->
      provable ( {{P}While b Do c EndWhile  {{ P AND NOT [[b]] }} )
  | hoare_asgn_fwd : P (X: var) E (x: logical_var),
      provable (
         {{ P }
        X ::= E
         {{ EXISTS x, P [Xx] AND
                     [[X]] = [[ E [Xx] ]] }} )
  | hoare_asgn_bwd : P (X: var) (E: aexp),
      provable ( {{ P [ XE] }X ::= E  {{ P }} ).
(*
  | hoare_consequence : forall (P P' Q Q' : Assertion) c,
      P  ⊢ P' ->
      provable ( {{P'}}  c  {{Q'}} ) ->
      Q'  ⊢ Q ->
      provable ( {{P}}  c  {{Q}} ).
*)


End Attempt1.
The formalization attempt above does not work very well because we have not defined assertion derivation yet. In fact, different logics for assertion derivation correspond to different Hoare logics even if all proof rules other than the consequence rule remain the same. Coq enable us to express this idea by defining a parameterized Hoare logic, the parameter D below represents the derivation relation between two assertions.
Module Attempt2.

Inductive provable (D: Assertion -> Assertion -> Prop): hoare_triple -> Prop :=
  | hoare_seq : (P Q R: Assertion) (c1 c2: com),
      provable D ( {{P}c1  {{Q}} ) ->
      provable D ( {{Q}c2  {{R}} ) ->
      provable D ( {{P}c1;;c2  {{R}} )
  | hoare_skip : P,
      provable D ( {{P}Skip  {{P}} )
  | hoare_if : P Q (b: bexp) c1 c2,
      provable D ( {{ P AND [[b]] }c1  {{ Q }} ) ->
      provable D ( {{ P AND NOT [[b]] }c2  {{ Q }} ) ->
      provable D ( {{ P }If b Then c1 Else c2 EndIf  {{ Q }} )
  | hoare_while : P (b: bexp) c,
      provable D ( {{ P AND [[b]] }c  {{P}} ) ->
      provable D ( {{P}While b Do c EndWhile  {{ P AND NOT [[b]] }} )
  | hoare_asgn_fwd : P (X: var) E (x: logical_var),
      provable D (
         {{ P }
        X ::= E
         {{ EXISTS x, P [Xx] AND
                     [[X]] = [[ E [Xx] ]] }} )
  | hoare_asgn_bwd : P (X: var) (E: aexp),
      provable D ( {{ P [ XE] }X ::= E  {{ P }} )
  | hoare_consequence : (P P' Q Q' : Assertion) c,
      D P P' -> (* P  ⊢ P' *)
      provable D ( {{P'}c  {{Q'}} ) ->
      D Q' Q -> (* Q'  ⊢ Q *)
      provable D ( {{P}c  {{Q}} ).

End Attempt2.
We use Coq's type class to turn on notations for assertion derivation.
Class FirstOrderLogic: Type := {
  FOL_provable: Assertion -> Prop
}.

Definition derives {T: FirstOrderLogic} (P Q: Assertion): Prop :=
  FOL_provable (P IMPLY Q).

Notation "P ' ⊢' Q" :=
  (derives ((P)%assert) ((Q)%assert)) (at level 90, no associativity).

Inductive provable {T: FirstOrderLogic}: hoare_triple -> Prop :=
  | hoare_seq : (P Q R: Assertion) (c1 c2: com),
      provable ( {{P}c1  {{Q}} ) ->
      provable ( {{Q}c2  {{R}} ) ->
      provable ( {{P}c1;;c2  {{R}} )
  | hoare_skip : P,
      provable ( {{P}Skip  {{P}} )
  | hoare_if : P Q (b: bexp) c1 c2,
      provable ( {{ P AND [[b]] }c1  {{ Q }} ) ->
      provable ( {{ P AND NOT [[b]] }c2  {{ Q }} ) ->
      provable ( {{ P }If b Then c1 Else c2 EndIf  {{ Q }} )
  | hoare_while : P (b: bexp) c,
      provable ( {{ P AND [[b]] }c  {{P}} ) ->
      provable ( {{P}While b Do c EndWhile  {{ P AND NOT [[b]] }} )
  | hoare_asgn_bwd : P (X: var) (E: aexp),
      provable ( {{ P [ XE] }X ::= E  {{ P }} )
  | hoare_consequence : (P P' Q Q' : Assertion) c,
      P  P' ->
      provable ( {{P'}c  {{Q'}} ) ->
      Q'  Q ->
      provable ( {{P}c  {{Q}} ).
Also, we choose to pick the backward assignment rule but not to include the forward assignment rule here. We will show that it is not a harmful design choice — these two assignment rules can be derived from each other.
Notation " ⊢ tr" := (provable tr) (at level 91, no associativity).

Hoare Triples' Semantic Meaning Via Denotations

In Hoare logic, we use a Hoare triple
     {{P}}  c  {{Q}
to represent: if the beginning state satisfies P and c's execution terminates, the ending state will always satisfy Q. This property can also be described using the denotational semantics.
Remember, logical variables may occur in assertions. Thus, when we define the satisfaction relation, it is a relation between program states and assertions under a specific assignment (指派) of logical variables.
Definition Lassn: Type := logical_var -> Z.

Definition Lassn_update (La: Lassn) (x: logical_var) (v: Z): Lassn :=
  fun yif (Nat.eq_dec x y) then v else La y.
In summary, an interpretation (解释) of program variables and logical variables is a pair of program state and logical variable assignment.
Definition Interp: Type := state * Lassn.

Definition Interp_Lupdate (J: Interp) (x: logical_var) (v: Z): Interp :=
  (fst J, Lassn_update (snd J) x v).
We first define the meaning (or the denotations) of variable expressions.
Fixpoint aexp'_denote (J: Interp) (a: aexp'): Z :=
    match a with
    | ANum' tterm_denote J t
    | AId' X ⇒ (fst J) X
    | APlus' a1 a2aexp'_denote J a1 + aexp'_denote J a2
    | AMinus' a1 a2aexp'_denote J a1 - aexp'_denote J a2
    | AMult' a1 a2aexp'_denote J a1 * aexp'_denote J a2
    end
with term_denote (J: Interp) (t: term): Z :=
    match t with
    | TNum nn
    | TId x ⇒ (snd J) x
    | TDenote aaexp'_denote J a
    | TPlus t1 t2term_denote J t1 + term_denote J t2
    | TMinus t1 t2term_denote J t1 - term_denote J t2
    | TMult t1 t2term_denote J t1 * term_denote J t2
    end.

Fixpoint bexp'_denote (J: Interp) (b: bexp'): Prop :=
    match b with
    | BTrue'True
    | BFalse'False
    | BEq' a1 a2aexp'_denote J a1 = aexp'_denote J a2
    | BLe' a1 a2 ⇒ (aexp'_denote J a1aexp'_denote J a2)%Z
    | BNot' b ⇒ ¬bexp'_denote J b
    | BAnd' b1 b2bexp'_denote J b1bexp'_denote J b2
    end.

Fixpoint satisfies (J: Interp) (d: Assertion): Prop :=
    match d with
    | AssnLe t1 t2 ⇒ (term_denote J t1term_denote J t2)%Z
    | AssnLt t1 t2 ⇒ (term_denote J t1 < term_denote J t2)%Z
    | AssnEq t1 t2 ⇒ (term_denote J t1 = term_denote J t2)%Z
    | AssnDenote bbexp'_denote J b
    | AssnOr P1 P2 ⇒ (satisfies J P1) ∨ (satisfies J P2)
    | AssnAnd P1 P2 ⇒ (satisfies J P1) ∧ (satisfies J P2)
    | AssnImpl P1 P2 ⇒ ¬(satisfies J P1) ∨ (satisfies J P2)
    | AssnNot P ⇒ ¬(satisfies J P)
    | AssnExists x Pv, satisfies (Interp_Lupdate J x v) P
    | AssnForall x Pv, satisfies (Interp_Lupdate J x v) P
    end.
We can prove that these two definitions coincide with aeval and beval on normal expressions.
Lemma aeval_aexp'_denote: st La a,
  aeval a st = aexp'_denote (st, La) (ainj a).
Proof.
  intros.
  induction a; simpl.
  + reflexivity.
  + reflexivity.
  + unfold Func.add.
    rewrite IHa1, IHa2.
    reflexivity.
  + unfold Func.sub.
    rewrite IHa1, IHa2.
    reflexivity.
  + unfold Func.mul.
    rewrite IHa1, IHa2.
    reflexivity.
Qed.

Lemma beval_bexp'_denote: st La b,
  beval b stbexp'_denote (st, La) (binj b).
Proof.
  intros.
  induction b; simpl.
  + tauto.
  + tauto.
  + rewrite <- aeval_aexp'_denote.
    rewrite <- aeval_aexp'_denote.
    tauto.
  + rewrite <- aeval_aexp'_denote.
    rewrite <- aeval_aexp'_denote.
    tauto.
  + unfold Sets.complement.
    tauto.
  + unfold Sets.intersect.
    tauto.
Qed.
Based on these definitions, we can state the semantic meaning of Hoare triples.
Notation "J ⊨ x" := (satisfies J x) (at level 90, no associativity).

Definition valid (Tr: hoare_triple): Prop :=
  match Tr with
  | Build_hoare_triple P c Q
      La st1 st2,
        (st1, La) ⊨ P -> ceval c st1 st2 -> (st2, La) ⊨ Q
  end.

Notation "⊨ Tr" := (valid Tr) (at level 91, no associativity).
Intuitively, a Hoare triple is valid (有效) if it is true on all possible assignment.
Traditionally, a single turnstile "|—" is used to represent a proof-theory related concept, like "provable" and "derivable". But a double turnstile "⊨" is used to represent a semantic concept like "satisfaction relation", "valid" and "consequence relation".

Hoare Logic Versus Denotational Semantics

Comparing with the denotational semantics, Hoare logic is a more coarse grained description of program behavior. It is natural to ask whether they are indeed equivalent. This equivalence property is called the soundness and completeness of Hoare logic.
Definition hoare_sound (T: FirstOrderLogic): Prop :=
  P c Q,
      {{ P }c  {{ Q }}  ->
    ⊨  {{ P }c  {{ Q }} .

Definition hoare_complete (T: FirstOrderLogic): Prop :=
  P c Q,
    ⊨  {{ P }c  {{ Q }}  ->
      {{ P }c  {{ Q }} .
Remember, we are not talking about one Hoare logic but a series of Hoare logics. Every different first order logic for assertion derivation defines a different Hoare logic. Thus, we would ask more specifically: for what kind of assertion derivation logics, their corresponding Hoare logics are sound and complete? A short answer is:
  • if the assertion derivation logic is sound, the corresponding Hoare logic is sound;
  • if the assertion language is expressive enough and the assertion derivation logic is complete, the corresponding Hoare logic is complete.
(* 2021-04-06 13:41 *)