We discuss completeness of algorithmic equality for typed lambda-terms with respect to declarative equality of lambda-terms. This case-study is part of ORBI, Open challenge problem Repository for systems reasoning with Binders. For a detailed description of the proof and a discussion regarding other systems see (Felty et al, 2014).
The mechanization highlights several aspects:
The polymorphic lambda-calculus is introduced with the following declarations:
LF tp : type =
| arr : tp → tp → tp
| all : (tp → tp) → tp;
--name tp T a.
LF term : type =
| app : term → term → term
| lam : (term → term) → term
| tlam : (tp → term) → term
| tapp : term → tp → term;
--name term M x.
We describe algorithmic and declarative equality for the polymorphic lambda-calculus as judgements using axioms and inference rules. The Beluga code is a straightforward HOAS encoding of the associated rules.
We add the judgement for type equality atp of type
tm -> tm -> type along with inference rules for universal quantifiers
at_al and arrow types at_arr.
LF atp : tp → tp → type =
| at_al : ({a : tp} atp a a → atp (T a) (S a)) → atp (all T) (all S)
| at_arr : atp T1 T2 → atp S1 S2 → atp (arr T1 S1) (arr T2 S2);
--name atp Q u.
We extend the term equality judgement given for the untyped lambda-calculus with rules for type
abstraction ae_tl and type application ae_ta.
LF aeq : term → term → type =
| ae_a : aeq M1 N1 → aeq M2 N2 → aeq (app M1 M2) (app N1 N2)
| ae_l :
({x : term} aeq x x → aeq (M x) (N x)) →
aeq (lam (\x. M x)) (lam (\x. N x))
| ae_tl :
({a : tp} atp a a → aeq (M a) (N a)) →
aeq (tlam (\a. M a)) (tlam (\a. N a))
| ae_ta : aeq M N → atp T S → aeq (tapp M T) (tapp N S);
--name aeq D u.
Note that type equality atp A B can be defined independently of term equality
aeq M N. In other words, aeq M N depends on atp A B, but
not vice-versa.
We define declarative equality for types in order to establish its equivalence with algorithmic equality and prove completeness. Rules for reflexivity, transitivity, and symmetry are explicitly derived.
LF dtp : tp → tp → type =
| dt_al : ({a : tp} dtp a a → dtp (T a) (S a)) → dtp (all T) (all S)
| dt_arr : dtp T1 T2 → dtp S1 S2 → dtp (arr T1 S1) (arr T2 S2)
| dt_r : dtp T T
| dt_t : dtp T R → dtp R S → dtp T S
| dt_s : dtp T S → dtp S T;
--name atp P u.
Declarative equality for terms is encoded similarly to its counterpart. Again, we are extending
the Untyped Equality case study to account for polymorphism with constructors for type
abstraction de_tl and type application de_ta
LF deq : term → term → type =
| de_l :
({x : term} deq x x → deq (M x) (N x)) →
deq (lam (\x. M x)) (lam (\x. N x))
| de_a : deq M1 N1 → deq M2 N2 → deq (app M1 M2) (app N1 N2)
| de_tl :
({a : tp} dtp a a → deq (M a) (N a)) →
deq (tlam (\a. M a)) (tlam (\a. N a))
| de_ta : deq M N → dtp T S → deq (tapp M T) (tapp N S)
| de_r : deq M M
| de_t : deq M L → deq L N → deq M N
| de_s : deq T S → deq S T;
Just as types classify expressions, contexts are classified by context schemas.
schema atpCtx = block (a : tp, _t : atp a a);
Since the case for lambda-abstraction lam deals with term assumptions while the
type abstraction tlam introduces type assumptions, we need to specify
alternating assumptions. This alternation of blocks is described by using
+ in Beluga's concrete syntax.
schema aeqCtx = block (x : term, _u : aeq x x)
+ block (a : tp, _t : atp a a);
schema dtpCtx = block (a : tp, u : atp a a, _t : dtp a a);
schema deqCtx = block (x : term, u : aeq x x, _t : deq x x)
+ block (a : tp, u : atp a a, _t : dtp a a);
The reflexivity for types is implemented as a recursive function called
reftp of type: {Γ:atpCtx}{T:[Γ |- tp]}[Γ |- atp T T]. This can be read
as: for all contexts g that have schema atpCtx, for all types
T, we have a proof that [g |- atp T T]. Quantification over contexts
and contextual objects in computation-level types is denoted between braces {}; the
corresponding abstraction on the level of expressions is written as
mlam g => mlam T1 => e.
rec reftp : { : atpCtx} { : (Γ ⊢ tp)} (Γ ⊢ atp ) =
mlam ⇒ mlam ⇒
case [Γ ⊢ ] of
| [Γ ⊢ #p.1] ⇒ [Γ ⊢ #p.2]
| [Γ ⊢ all (\x. )] ⇒
let [Γ, b : block (a : tp, _t : atp a a) ⊢ […, b.1, b.2]] =
reftp [Γ, b : block (a : tp, _t : atp a a)] [Γ, b ⊢ […, b.1]] in
[Γ ⊢ at_al (\x. \w. )]
| [Γ ⊢ arr ] ⇒ let [Γ ⊢ ] = reftp [Γ] [Γ ⊢ ] in
let [Γ ⊢ ] = reftp [Γ] [Γ ⊢ ] in [Γ ⊢ at_arr ];
In the proof for refltp we begin by introducing and T followed by a
case analysis on [Γ |- T] using pattern matching. There are three possible cases
for T:
T is a variable from g, we write
[Γ |- #p.1] where #p denotes a parameter variable declared in the
context g. Operationally, #p can be instantiated with any bound
variable from the context g. Since the context g has schema
atpCtx, it contains blocks a:tp , _t:atp a a;. The first projection
allows us to extract the type component, while the second projection denotes the proof of
_t:atp a a;.
T is an existential quantification, then we
extend the context and appeal to the induction hypothesis by making a recursive call. Beluga
supports declaration weakening which allows us to use T that has type
[Γ, a:tp |- tp] in the extended context
[Γ, b:block a:tp , _t: atp a a]. We simply construct a weakening substitution
.. b.1 with domain g, a:tp and range
g, b:block a:tp , _t: atp a a that essentially renames a to
b.1 in T. The recursive call returns
[Γ, b:block a:tp , _t:atp a a |- D[.., b.1 , b.2]]. Using it together with rule
at_la we build the final derivation.
T is an arrow type, we appeal twice to the
induction hypothesis and build a proof for [Γ |- atp (arr T S) (arr T S)].
The recursive function ref encodes the proof reflexivity for terms. The type
signature reads: for all contexts g that have schema aeqCtx, for all
terms M, we have a proof that [g |- aeq M M].
rec ref : { : aeqCtx} { : (Γ ⊢ term)} (Γ ⊢ aeq ) =
mlam ⇒ mlam ⇒
case [Γ ⊢ ] of
| [Γ ⊢ #p.1] ⇒ [Γ ⊢ #p.2]
| [Γ ⊢ lam (\x. )] ⇒
let [Γ, b : block (y : term, _t : aeq y y) ⊢ […, b.1, b.2]] =
ref [Γ, b : block (y : term, _t : aeq y y)] [Γ, b ⊢ […, b.1]] in
[Γ ⊢ ae_l (\x. \w. )]
| [Γ ⊢ app ] ⇒ let [Γ ⊢ ] = ref [Γ] [Γ ⊢ ] in
let [Γ ⊢ ] = ref [Γ] [Γ ⊢ ] in [Γ ⊢ ae_a ]
| [Γ ⊢ tlam (\a. )] ⇒
let [Γ, b : block (a : tp, _t : atp a a) ⊢ […, b.1, b.2]] =
ref [Γ, b : block (a : tp, _t : atp a a)] [Γ, b ⊢ […, b.1]] in
[Γ ⊢ ae_tl (\x. \w. )]
| [Γ ⊢ tapp ] ⇒ let [Γ ⊢ ] = ref [Γ] [Γ ⊢ ] in
let [Γ ⊢ ] = reftp [Γ] [Γ ⊢ ] in [Γ ⊢ ae_ta ];
This time, there are five possible cases for our meta-variable
M:
M is a variable from g, we write
[Γ |- #p.1] where #p denotes a parameter variable declared in the
context g. Operationally, #p can be instantiated with any bound
variable from the context g. Since the context g has schema
aeqCtx, it contains blocks x:tm , ae_v:aeq x x. The first projection
allows us to extract the term component, while the second projection denotes the proof of
aeq x x.
M is a lambda-term, then we extend
the context and appeal to the induction hypothesis by making a recursive call. Automatic
context subsumption comes into play again, allowing us to use M that has type
[Γ, x:tm |- tm] in the extended context
[Γ, b:block y:tm , ae_v: aeq y y]. We simply construct a weakening substitution
.. b.1 with domain g, y:tm and range
g, b:block y:tm , ae_v:aeq y y. that essentially renames y to
b.1 in M. The recursive call returns
[Γ, b:block y:tm , ae_v:aeq y y |- D[.., b.1 b.2]]. Using it together with rule
ae_l we build the final derivation.
M is an application, we appeal twice
to the induction hypothesis and build a proof for
[Γ |- aeq (app M1 M2) (app M1 M2)].
M is a type abstraction, then we
extend the context and appeal to the induction hypothesis by making a recursive call. We use
M that has type [Γ, a:tp |- tp] in the extended context
[Γ, b:block a:tp , _t: atp a a] and construct a weakening substitution
.. b.1 with domain g, a:tp and range
g, b:block a:tp , _t: atp a a that essentially renames a to
b.1 in T. The recursive call returns
[Γ, b:block a:tp , _t:atp a a |- D[.., b.1, b.2]]. Using it together with rule
at_la we build the final derivation.
M is a type application, we appeal
twice to the induction hypothesis and build a proof for
[Γ |- aeqCtx (tapp M T) (tapp M T)].
rec transtp : ( : atpCtx) (Γ ⊢ atp ) → (Γ ⊢ atp ) → (Γ ⊢ atp ) =
fn ae1 ⇒ fn ae2 ⇒
case ae1 of
| [Γ ⊢ #p.2] ⇒ ae2
| [Γ ⊢ at_al (\a. \u. […, a, u])] ⇒
let [Γ ⊢ at_al (\a. \u. […, a, u])] = ae2 in
let [Γ, b : block (a : tp, _t : atp a a) ⊢ […, b.1, b.2]] =
transtp [Γ, b : block (a : tp, _t : atp a a) ⊢ […, b.1, b.2]]
[Γ, b ⊢ […, b.1, b.2]] in [Γ ⊢ at_al (\a. \u. […, a, u])]
| [Γ ⊢ at_arr ] ⇒ let [Γ ⊢ at_arr ] = ae2 in
let [Γ ⊢ ] = transtp [Γ ⊢ ] [Γ ⊢ ] in
let [Γ ⊢ ] = transtp [Γ ⊢ ] [Γ ⊢ ] in [Γ ⊢ at_arr ];
rec trans : ( : aeqCtx) (Γ ⊢ aeq ) → (Γ ⊢ aeq ) → (Γ ⊢ aeq ) =
fn ae1 ⇒ fn ae2 ⇒
case ae1 of
| [Γ ⊢ #p.2] ⇒ ae2
| [Γ ⊢ ae_l (\x. \u. )] ⇒ let [Γ ⊢ ae_l (\x. \u. )] = ae2 in
let [Γ, b : block (x : term, _t : aeq x x) ⊢ […, b.1, b.2]] =
trans [Γ, b : block (x' : term, _t : aeq x' x') ⊢ […, b.1, b.2]]
[Γ, b ⊢ […, b.1, b.2]] in [Γ ⊢ ae_l (\x. \u. )]
| [Γ ⊢ ae_a ] ⇒ let [Γ ⊢ ae_a ] = ae2 in
let [Γ ⊢ ] = trans [Γ ⊢ ] [Γ ⊢ ] in
let [Γ ⊢ ] = trans [Γ ⊢ ] [Γ ⊢ ] in [Γ ⊢ ae_a ]
| [Γ ⊢ ae_tl (\a. \u. […, a, u])] ⇒
let [Γ ⊢ ae_tl (\a. \u. […, a, u])] = ae2 in
let [Γ, b : block (a : tp, _t : atp a a) ⊢ […, b.1, b.2]] =
trans [Γ, b : block (a : tp, _t : atp a a) ⊢ […, b.1, b.2]]
[Γ, b ⊢ […, b.1, b.2]] in [Γ ⊢ ae_tl (\x. \u. )]
| [Γ ⊢ ae_ta ] ⇒ let [Γ ⊢ ae_ta ] = ae2 in
let [Γ ⊢ ] = trans [Γ ⊢ ] [Γ ⊢ ] in
let [Γ ⊢ ] = transtp [Γ ⊢ ] [Γ ⊢ ] in [Γ ⊢ ae_ta ];
rec symtp : ( : atpCtx) (Γ ⊢ atp ) → (Γ ⊢ atp ) =
fn ae ⇒
case ae of
| [Γ ⊢ #p.2] ⇒ ae
| [Γ ⊢ at_al (\x. \u. )] ⇒
let [Γ, b : block (a : tp, _t : atp a a) ⊢ […, b.1, b.2]] =
symtp [Γ, b : block (a : tp, _t : atp a a) ⊢ […, b.1, b.2]] in
[Γ ⊢ at_al (\x. \u. )]
| [Γ ⊢ at_arr ] ⇒ let [Γ ⊢ ] = symtp [Γ ⊢ ] in
let [Γ ⊢ ] = symtp [Γ ⊢ ] in [Γ ⊢ at_arr ];
rec sym : ( : aeqCtx) (Γ ⊢ aeq ) → (Γ ⊢ aeq ) =
fn ae ⇒
case ae of
| [Γ ⊢ #p.2] ⇒ ae
| [Γ ⊢ ae_l (\x. \u. )] ⇒
let [Γ, b : block (x : term, _t : aeq x x) ⊢ […, b.1, b.2]] =
sym [Γ, b : block (x : term, _t : aeq x x) ⊢ […, b.1, b.2]] in
[Γ ⊢ ae_l (\x. \u. )]
| [Γ ⊢ ae_a ] ⇒ let [Γ ⊢ ] = sym [Γ ⊢ ] in
let [Γ ⊢ ] = sym [Γ ⊢ ] in [Γ ⊢ ae_a ]
| [Γ ⊢ ae_tl (\x. \u. )] ⇒
let [Γ, b : block (a : tp, _t : atp a a) ⊢ […, b.1, b.2]] =
sym [Γ, b : block (a : tp, _t : atp a a) ⊢ […, b.1, b.2]] in
[Γ ⊢ ae_tl (\x. \u. )]
| [Γ ⊢ ae_ta ] ⇒ let [Γ ⊢ ] = sym [Γ ⊢ ] in
let [Γ ⊢ ] = symtp [Γ ⊢ ] in [Γ ⊢ ae_ta ];
rec ctp : ( : dtpCtx) (Γ ⊢ dtp ) → (Γ ⊢ atp ) =
fn e ⇒
case e of
| [Γ ⊢ #p.3] ⇒ [Γ ⊢ #p.2]
| [Γ ⊢ dt_r] ⇒ reftp [Γ] [Γ ⊢ _]
| [Γ ⊢ dt_arr ] ⇒ let [Γ ⊢ ] = ctp [Γ ⊢ ] in
let [Γ ⊢ ] = ctp [Γ ⊢ ] in [Γ ⊢ at_arr ]
| [Γ ⊢ dt_al (\x. \u. )] ⇒
let [Γ, b : block (a : tp, u : atp a a, _t : dtp a a) ⊢ […, b.1, b.2]] =
ctp [Γ, b : block (a : tp, u : atp a a, _t : dtp a a) ⊢ […, b.1, b.3]]
in [Γ ⊢ at_al (\x. \v. )]
| [Γ ⊢ dt_t ] ⇒ let [Γ ⊢ ] = ctp [Γ ⊢ ] in
let [Γ ⊢ ] = ctp [Γ ⊢ ] in transtp [Γ ⊢ ] [Γ ⊢ ]
| [Γ ⊢ dt_s ] ⇒ let [Γ ⊢ ] = ctp [Γ ⊢ ] in symtp [Γ ⊢ ];
rec ceq : ( : deqCtx) (Γ ⊢ deq ) → (Γ ⊢ aeq ) =
fn e ⇒
case e of
| [Γ ⊢ #p.3] ⇒ [Γ ⊢ #p.2]
| [Γ ⊢ de_r] ⇒ ref [Γ] [Γ ⊢ _]
| [Γ ⊢ de_a ] ⇒ let [Γ ⊢ ] = ceq [Γ ⊢ ] in
let [Γ ⊢ ] = ceq [Γ ⊢ ] in [Γ ⊢ ae_a ]
| [Γ ⊢ de_l (\x. \u. )] ⇒
let
[Γ, b : block (x : term, u : aeq x x, _t : deq x x) ⊢ […, b.1, b.2]] =
ceq
[Γ, b : block (x : term, u : aeq x x, _t : deq x x) ⊢ […, b.1, b.3]]
in [Γ ⊢ ae_l (\x. \v. )]
| [Γ ⊢ de_t ] ⇒ let [Γ ⊢ ] = ceq [Γ ⊢ ] in
let [Γ ⊢ ] = ceq [Γ ⊢ ] in trans [Γ ⊢ ] [Γ ⊢ ]
| [Γ ⊢ de_s ] ⇒ let [Γ ⊢ ] = ceq [Γ ⊢ ] in sym [Γ ⊢ ]
| [Γ ⊢ de_tl (\a. \u. […, a, u])] ⇒
let [Γ, b : block (a : tp, u : atp a a, _t : dtp a a) ⊢ […, b.1, b.2]] =
ceq [Γ, b : block (a : tp, u : atp a a, _t : dtp a a) ⊢ […, b.1, b.3]]
in [Γ ⊢ ae_tl (\x. \v. )]
| [Γ ⊢ de_ta ] ⇒ let [Γ ⊢ ] = ctp [Γ ⊢ ] in
let [Γ ⊢ ] = ceq [Γ ⊢ ] in [Γ ⊢ ae_ta ];
Download the code © Computation and Logic Group
Department of Computer Science, McGill University
3480 University Street, Montreal, Quebec, Canada, H3A 0E9