%{ This article describes a Twelf formalization of
logic in terms of ''verifications'' and ''uses'' and a Twelf proof of global soundness and completeness
for this logic. For a full discussion of logics of verifications and uses, see
Frank Pfenning's [http://www.cs.cmu.edu/~fp/courses/15816-s10/lectures/01-judgments.pdf lecture notes]
from the spring 2010 course on Modal Logic.
Two critical properties of a logic are its ''global completeness'' - that in any situation where we can
use a proof of A we can also verify A -
and its ''global soundness'' - if we can verify the truth of proposition A, and then use an assumption
that A is true to verify the truth of B, then we can also verify the truth of
B without the assumption that A is true.
We can also think of the logic of verifications of uses, by way of the [[w:Curry-Howard correspondence|Curry-Howard correspondence]], as
a ("Church-style") [[intrinsic encoding]] of the [[canonical forms]] of the [[simply-typed lambda calculus]]. In this view, the
constructive content of the global completeness theorem is η-expansion and the constructive content of
global soundness is [[hereditary substitution]]. Therefore, this article is closely connected to the case
study on [[hereditary substitution for the STLC]]. The case study is structured quite differently than this
article, however. That article defines a terminating partial function called "hereditary substitution"
that operates on untyped ("Curry-style") lambda-calculus terms, and then works through the process of showing that,
given well-typed inputs, the function is actually total. That view is helpful when thinking about dependent type
systems, [[bidirectional type checking]], or the implementation of a logical framework, but
certain problems can be greatly simplified when we think of terms as intrinsically typed. }%
%{ == Propositions and rules == }%
%{ The following is a straightforward representation of a natural deduction
system with verifications and uses, with one exception: in most presentations,
use A and hyp A are conflated, which makes the
var rule unnecessary. However, it is somewhat convenient
to do things this way, and also foreshadows a number of other interesting
developments. }%
prop : type.
atom : type.
%block bl_atom : block {qp : atom}.
a : atom -> prop.
⊃ : prop -> prop -> prop. %infix right 9 ⊃.
∧ : prop -> prop -> prop. %infix right 8 ∧.
hyp : prop -> type.
verif : prop -> type.
use : prop -> type.
%block bl_hyp : some {A : prop} block {x : hyp A}.
var : hyp A -> use A.
atm : use (a Q) -> verif (a Q).
⊃I : (hyp A -> verif B) -> verif (A ⊃ B).
⊃E : use (A ⊃ B) -> verif A -> use B.
∧I : verif A -> verif B -> verif (A ∧ B).
∧E₁ : use (A ∧ B) -> use A.
∧E₂ : use (A ∧ B) -> use B.
%{ == Global completeness == }%
%{ The global completeness (or η-expansion) theorem for the logic of verifications and uses
is expressed by the theorem: if we can use a proof that use A under certain assumptions, then
under the same assumptions we can verify that A is true. It is therefore critical to note that
the output of the global completeness theorem is a function (use A -> verif A) and not a function
(hyp A -> verif A). Assumptions of hyp A alone are sufficient to describe the logic itself,
but in order to describe completeness we have to actually assume proofs of use A. }%
eta : {A : prop} (use A -> verif A) -> type.
%mode eta +A -N.
- : eta (a Q) ([r] atm r).
- : eta (A ⊃ B) ([r] ⊃I ([y] (N₂ (⊃E r (N₁ (var y))))))
<- eta A ([r₁] N₁ r₁)
<- eta B ([r₂] N₂ r₂).
- : eta (A ∧ B) ([r] ∧I (N₁ (∧E₁ r)) (N₂ (∧E₂ r)))
<- eta A ([r₁] N₁ r₁)
<- eta B ([r₂] N₂ r₂).
%worlds (bl_atom | bl_hyp) (eta _ _).
%total A (eta A _).
%{ == Detour == }%
%{ Before we proceed to global soundness, we
have to deal with the core annoyance of doing things
natural-deduction style as opposed to sequent-calculus style.
It is unavoidable that we must consider the case where we substitute
a verification into a use - if we associate the the metavariables
M and N with derivations of verif A and
the metavariable R with
derivations of use B, this looks like [N/x]R. In these
cases, we have to do something very different depending on whether
the "head variable" (the variable all the way on the inside of R)
is x (the variable we're substituting for) or
whether it is something else.
Specifically, if the variable is x we need to substitute
N in and then perform a series of reductions, but
if it is something else we can leave the structure of the term basically the
same.
The tutorial on [[hereditary substitution for the STLC]] deals with this
problem by defining hereditary substitution in such a way that Twelf cannot
immediately establish that it is total, and then showing totality after the
fact. We want to immediately establish totality, and to do this we will
define a judgment that asks "am I in the case where I need to have a series
of reductions performed (reduce_me), or are such
reductions unnecessary (just_fine)? }%
just_fine : (hyp A -> use B) -> type.
jfx : just_fine ([x] var Y).
jf⊃ : just_fine ([x] R x) -> just_fine ([x] ⊃E (R x) (M x)).
jf∧₁ : just_fine ([x] R x) -> just_fine ([x] ∧E₁ (R x)).
jf∧₂ : just_fine ([x] R x) -> just_fine ([x] ∧E₂ (R x)).
reduce_me : (hyp A -> use B) -> type.
rmx : reduce_me ([x] var x).
rm⊃ : reduce_me ([x] R x) -> reduce_me ([x] ⊃E (R x) (M x)).
rm∧₁ : reduce_me ([x] R x) -> reduce_me ([x] ∧E₁ (R x)).
rm∧₂ : reduce_me ([x] R x) -> reduce_me ([x] ∧E₂ (R x)).
jf_or_rm : (hyp A -> use B) -> type.
rm : reduce_me ([x] R x) -> jf_or_rm ([x] R x).
jf : just_fine ([x] R x) -> jf_or_rm ([x] R x).
%{ Now, we prove a metatheorem that ''every'' possible substitution instance
either needs to ask hereditary substitution to reduce_me
or else is just_fine. This has to use a bunch of
[[output factoring]] lemmas but is otherwise straightforward. }%
always_jf_or_rm : {R : hyp A -> use B} jf_or_rm ([x] R x) -> type.
%mode always_jf_or_rm +R -JFRM.
- : always_jf_or_rm ([x] var x) (rm rmx).
- : always_jf_or_rm ([x] var Y) (jf jfx).
lem : jf_or_rm ([x] R x) -> {N} jf_or_rm ([x] ⊃E (R x) (N x)) -> type.
- : lem (jf JF) _ (jf (jf⊃ JF)).
- : lem (rm RM) _ (rm (rm⊃ RM)).
%mode lem +JFRM +N -JFRM'.
%worlds (bl_atom | bl_hyp) (lem _ _ _).
%total {} (lem _ _ _).
- : always_jf_or_rm ([x] ⊃E (R x) (N x)) JFRM'
<- always_jf_or_rm ([x] R x) (JFRM : jf_or_rm ([x] R x))
<- lem JFRM ([x] N x) (JFRM' : jf_or_rm ([x] ⊃E (R x) (N x))).
lem : jf_or_rm ([x] R x) -> jf_or_rm ([x] ∧E₁ (R x)) -> type.
- : lem (jf JF) (jf (jf∧₁ JF)).
- : lem (rm RM) (rm (rm∧₁ RM)).
%mode lem +JFRM -JFRM'.
%worlds (bl_atom | bl_hyp) (lem _ _).
%total {} (lem _ _).
- : always_jf_or_rm ([x] ∧E₁ (R x)) JFRM'
<- always_jf_or_rm ([x] R x) (JFRM : jf_or_rm ([x] R x))
<- lem JFRM (JFRM' : jf_or_rm ([x] ∧E₁ (R x))).
lem : jf_or_rm ([x] R x) -> jf_or_rm ([x] ∧E₂ (R x)) -> type.
- : lem (jf JF) (jf (jf∧₂ JF)).
- : lem (rm RM) (rm (rm∧₂ RM)).
%mode lem +JFRM -JFRM'.
%worlds (bl_atom | bl_hyp) (lem _ _).
%total {} (lem _ _).
- : always_jf_or_rm ([x] ∧E₂ (R x)) JFRM'
<- always_jf_or_rm ([x] R x) (JFRM : jf_or_rm ([x] R x))
<- lem JFRM (JFRM' : jf_or_rm ([x] ∧E₂ (R x))).
%worlds (bl_atom | bl_hyp) (always_jf_or_rm _ _).
%total R (always_jf_or_rm R _).
%{ One way to avoid this ugly detour is to use [[spine form]], another way is considered in the page on [[verifications and uses with zippers]]. }%
%{ == Global soundness == }%
%{ Showing that every (hyp A -> use B) always either is just_fine
or needs hereditary substitution to reduce_me is the entirety of the
"ugly" part of the hereditary substitution/global completeness, theorem. The
theorem is made up of four mutually inductive theorems.
* hsubst_n represents substitutions [M0/x]M.
* hsubst_r represents substitutions [M0/x]R where R has atomic type. It is basically an [[output factoring]] lemma that dispatches to hsubst_rr and hsubst_rn.
* hsubst_rr represents substitutions [M0/x]R where the variable x is not the root of R.
* hsubst_rn represents substitutions [M0/x]R where the variable x '''is''' the root of R. This means that if we substitute M0 for x and then start reducing the use R, we will get back a verification N. Luckily, the type of N is known ahead of time, and so we can use this to know something about the shape of N. This case makes critical use of a [[%reduces]] declaration, and is also the only case where the type A is different in a recursive call. }%
hsubst_n : {A}{M₀ : verif A}{M : hyp A -> verif B} verif B -> type.
hsubst_r : {A}{M₀ : verif A}{R : hyp A -> use (a Q)} jf_or_rm R -> verif (a Q) -> type.
hsubst_rr: {A}{M₀ : verif A}{R : hyp A -> use B} just_fine R -> use B -> type.
hsubst_rn: {A}{B}{M₀ : verif A}{R : hyp A -> use B} reduce_me R -> verif B -> type.
%mode hsubst_n +A +M₀ +M -N.
%mode hsubst_r +A +M₀ +R +JFRM -N.
%mode hsubst_rr +A +M +R +JF -N.
%mode hsubst_rn +A +B +M +R +RM -N.
- : hsubst_n A M₀ ([x] ⊃I [y] M x y) (⊃I [y] N y)
<- {y : hyp B₁} hsubst_n A M₀ ([x] M x y) (N y : verif B₂).
- : hsubst_n A M₀ ([x] ∧I (M₁ x) (M₂ x)) (∧I N₁ N₂)
<- hsubst_n A M₀ ([x] M₁ x) (N₁ : verif B₁)
<- hsubst_n A M₀ ([x] M₂ x) (N₂ : verif B₂).
- : hsubst_n A M₀ ([x] atm (R x)) N
<- always_jf_or_rm ([x] R x) (JFRM : jf_or_rm ([x] R x))
<- hsubst_r A M₀ ([x] R x) JFRM N.
- : hsubst_r A M₀ ([x] R x) (jf JF) (atm R')
<- hsubst_rr A M₀ ([x] R x) JF R'.
- : hsubst_r A M₀ ([x] R x) (rm RM) N
<- hsubst_rn A _ M₀ ([x] R x) RM N.
- : hsubst_rr A M₀ ([x] ⊃E (R x) (M x)) (jf⊃ JF) (⊃E R' N)
<- hsubst_rr A M₀ ([x] R x) JF R'
<- hsubst_n A M₀ ([x] M x) N.
- : hsubst_rr A M₀ ([x] ∧E₁ (R x)) (jf∧₁ JF) (∧E₁ R')
<- hsubst_rr A M₀ ([x] R x) JF R'.
- : hsubst_rr A M₀ ([x] ∧E₂ (R x)) (jf∧₂ JF) (∧E₂ R')
<- hsubst_rr A M₀ ([x] R x) JF R'.
- : hsubst_rr A M₀ ([x] var Y) jfx (var Y).
- : hsubst_rn A B₂ M₀ ([x] ⊃E (R x) (M x)) (rm⊃ RM) N'
<- hsubst_rn A (B₁ ⊃ B₂) M₀ ([x] R x) RM ((⊃I [y] N y) : verif (B₁ ⊃ B₂))
<- hsubst_n A M₀ ([x] M x) (M' : verif B₁)
<- hsubst_n B₁ M' N (N': verif B₂).
- : hsubst_rn A B₁ M₀ ([x] ∧E₁ (R x)) (rm∧₁ RM) N₁
<- hsubst_rn A (B₁ ∧ B₂) M₀ ([x] R x) RM (∧I N₁ N₂ : verif (B₁ ∧ B₂)).
- : hsubst_rn A B₂ M₀ ([x] ∧E₂ (R x)) (rm∧₂ RM) N₂
<- hsubst_rn A (B₁ ∧ B₂) M₀ ([x] R x) RM (∧I N₁ N₂ : verif (B₁ ∧ B₂)).
- : hsubst_rn A A M₀ ([x] var x) rmx M₀.
%worlds (bl_atom | bl_hyp)
(hsubst_n _ _ _ _)
(hsubst_r _ _ _ _ _)
(hsubst_rr _ _ _ _ _)
(hsubst_rn _ _ _ _ _ _).
%{ A critical part of proving termination is showing that, during the hereditary substitution process, the second argument is no larger than the first - this is why
we are allowed to make the recursive call hsubst_n B1 M' N (N': verif B2) in the ⊃E case of hsubst_rn: we know A is no larger than B1 ⊃ B2, and therefore B2 is strictly smaller.
This is established by the following [[%reduces]] declaration. }%
%reduces B <= A (hsubst_rn A B _ _ _ _).
%{ The completeness of hereditary substitution is established first by lexicographic induction,
first on the type A and second on the term M
that we are substituting into.
The statement of hsubst_r has to come after hsubst_rr and
hsubst_rn in order for termination checking to work, because the
former theorem calls the latter two theorems with all the same arguments. }%
%total {(A B C D) (M R S T)}
(hsubst_n A _ M _)
(hsubst_rr C _ S _ _)
(hsubst_rn D _ _ T _ _)
(hsubst_r B _ R _ _).