Algorithmic Equality for the Polymorphic Lambda-calculus (G-version)

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:

  • Context schemas with alternative assumptions
  • Induction on universally quantified objects
  • Stating and proving properties in a generalized context
  • Reasoning using context subsumption

Syntax

The polymorphic lambda-calculus is introduced with the following declarations:

LF tp : type =
| arr : tptptp
| all : (tptp) → tp;

--name tp T a.

LF term : type =
| app : termtermterm
| lam : (termterm) → term
| tlam : (tpterm) → term
| tapp : termtpterm;

--name term M x.

Judgements and Rules

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.

Algorithmic Equality for types

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 : tptptype =
| at_al : ({a : tp} atp a aatp (T a) (S a)) → atp (all T) (all S)
| at_arr : atp T1 T2atp S1 S2atp (arr T1 S1) (arr T2 S2);

--name atp Q u.

Algorithmic Equality for terms

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 : termtermtype =
| ae_a : aeq M1 N1aeq M2 N2aeq (app M1 M2) (app N1 N2)
| ae_l :
  ({x : term} aeq x xaeq (M x) (N x)) →
    aeq (lam (\x. M x)) (lam (\x. N x))
| ae_tl :
  ({a : tp} atp a aaeq (M a) (N a)) →
    aeq (tlam (\a. M a)) (tlam (\a. N a))
| ae_ta : aeq M Natp T Saeq (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.

Declarative Equality for types

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 : tptptype =
| dt_al : ({a : tp} dtp a adtp (T a) (S a)) → dtp (all T) (all S)
| dt_arr : dtp T1 T2dtp S1 S2dtp (arr T1 S1) (arr T2 S2)
| dt_r : dtp T T
| dt_t : dtp T Rdtp R Sdtp T S
| dt_s : dtp T Sdtp S T;

--name atp P u.

Declarative Equality for terms

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 : termtermtype =
| de_l :
  ({x : term} deq x xdeq (M x) (N x)) →
    deq (lam (\x. M x)) (lam (\x. N x))
| de_a : deq M1 N1deq M2 N2deq (app M1 M2) (app N1 N2)
| de_tl :
  ({a : tp} dtp a adeq (M a) (N a)) →
    deq (tlam (\a. M a)) (tlam (\a. N a))
| de_ta : deq M Ndtp T Sdeq (tapp M T) (tapp N S)
| de_r : deq M M
| de_t : deq M Ldeq L Ndeq M N
| de_s : deq T Sdeq S T;

Context declarations

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);

Proof of Reflexivity for Types

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} {T : (Γtp)} (Γatp T T) =
mlam Γmlam Tcase [ΓT] of
  | [Γ#p.1] ⇒ [Γ#p.2]
  | [Γall (\x. T)] ⇒
    let [Γ, b : block (a : tp, _t : atp a a) ⊢ D[…, b.1, b.2]] =
      reftp [Γ, b : block (a : tp, _t : atp a a)] [Γ, bT[…, b.1]] in
    [Γat_al (\x. \w. D)]
  | [Γarr T S] ⇒ let [ΓD1] = reftp [Γ] [ΓT] in
    let [ΓD2] = reftp [Γ] [ΓS] in [Γat_arr D1 D2];

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:

  • Variable case. If 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;.
  • Existential case. If 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.
  • Arrow case. If T is an arrow type, we appeal twice to the induction hypothesis and build a proof for [Γ |- atp (arr T S) (arr T S)].

Proof of Reflexivity for Terms

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} {M : (Γterm)} (Γaeq M M) =
mlam Γmlam Mcase [ΓM] of
  | [Γ#p.1] ⇒ [Γ#p.2]
  | [Γlam (\x. M)] ⇒
    let [Γ, b : block (y : term, _t : aeq y y) ⊢ D[…, b.1, b.2]] =
      ref [Γ, b : block (y : term, _t : aeq y y)] [Γ, bM[…, b.1]] in
    [Γae_l (\x. \w. D)]
  | [Γapp M1 M2] ⇒ let [ΓD1] = ref [Γ] [ΓM1] in
    let [ΓD2] = ref [Γ] [ΓM2] in [Γae_a D1 D2]
  | [Γtlam (\a. M)] ⇒
    let [Γ, b : block (a : tp, _t : atp a a) ⊢ D[…, b.1, b.2]] =
      ref [Γ, b : block (a : tp, _t : atp a a)] [Γ, bM[…, b.1]] in
    [Γae_tl (\x. \w. D)]
  | [Γtapp M T] ⇒ let [ΓD1] = ref [Γ] [ΓM] in
    let [ΓD2] = reftp [Γ] [ΓT] in [Γae_ta D1 D2];

This time, there are five possible cases for our meta-variable M:

  • Variable case. If 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.
  • Lambda-abstraction case. If 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.
  • Term application case. If M is an application, we appeal twice to the induction hypothesis and build a proof for [Γ |- aeq (app M1 M2) (app M1 M2)].
  • Type abstraction case. If 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.
  • Type application case. If M is a type application, we appeal twice to the induction hypothesis and build a proof for [Γ |- aeqCtx (tapp M T) (tapp M T)].

Proof of Transitivity for Types

rec transtp : (Γ : atpCtx) (Γatp T R) → (Γatp R S) → (Γatp T S) =
fn ae1fn ae2case ae1 of
  | [Γ#p.2] ⇒ ae2
  | [Γat_al (\a. \u. D1[…, a, u])] ⇒
    let [Γat_al (\a. \u. D2[…, a, u])] = ae2 in
    let [Γ, b : block (a : tp, _t : atp a a) ⊢ D[…, b.1, b.2]] =
      transtp [Γ, b : block (a : tp, _t : atp a a) ⊢ D1[…, b.1, b.2]]
        [Γ, bD2[…, b.1, b.2]] in [Γat_al (\a. \u. D[…, a, u])]
  | [Γat_arr D1 D2] ⇒ let [Γat_arr D3 D4] = ae2 in
    let [ΓD] = transtp [ΓD1] [ΓD3] in
    let [ΓD'] = transtp [ΓD2] [ΓD4] in [Γat_arr D D'];

Proof of Transitivity for Terms

rec trans : (Γ : aeqCtx) (Γaeq T R) → (Γaeq R S) → (Γaeq T S) =
fn ae1fn ae2case ae1 of
  | [Γ#p.2] ⇒ ae2
  | [Γae_l (\x. \u. D1)] ⇒ let [Γae_l (\x. \u. D2)] = ae2 in
    let [Γ, b : block (x : term, _t : aeq x x) ⊢ D[…, b.1, b.2]] =
      trans [Γ, b : block (x' : term, _t : aeq x' x') ⊢ D1[…, b.1, b.2]]
        [Γ, bD2[…, b.1, b.2]] in [Γae_l (\x. \u. D)]
  | [Γae_a D1 D2] ⇒ let [Γae_a D3 D4] = ae2 in
    let [ΓD] = trans [ΓD1] [ΓD3] in
    let [ΓD'] = trans [ΓD2] [ΓD4] in [Γae_a D D']
  | [Γae_tl (\a. \u. D1[…, a, u])] ⇒
    let [Γae_tl (\a. \u. D2[…, a, u])] = ae2 in
    let [Γ, b : block (a : tp, _t : atp a a) ⊢ D[…, b.1, b.2]] =
      trans [Γ, b : block (a : tp, _t : atp a a) ⊢ D1[…, b.1, b.2]]
        [Γ, bD2[…, b.1, b.2]] in [Γae_tl (\x. \u. D)]
  | [Γae_ta D1 Q1] ⇒ let [Γae_ta D2 Q2] = ae2 in
    let [ΓD] = trans [ΓD1] [ΓD2] in
    let [ΓQ] = transtp [ΓQ1] [ΓQ2] in [Γae_ta D Q];

Proof of Symmetry for Types

rec symtp : (Γ : atpCtx) (Γatp T R) → (Γatp R T) =
fn aecase ae of
  | [Γ#p.2] ⇒ ae
  | [Γat_al (\x. \u. D)] ⇒
    let [Γ, b : block (a : tp, _t : atp a a) ⊢ D'[…, b.1, b.2]] =
      symtp [Γ, b : block (a : tp, _t : atp a a) ⊢ D[…, b.1, b.2]] in
    [Γat_al (\x. \u. D')]
  | [Γat_arr D1 D2] ⇒ let [ΓD1'] = symtp [ΓD1] in
    let [ΓD2'] = symtp [ΓD2] in [Γat_arr D1' D2'];

Proof of Symmetry for Terms

rec sym : (Γ : aeqCtx) (Γaeq T R) → (Γaeq R T) =
fn aecase ae of
  | [Γ#p.2] ⇒ ae
  | [Γae_l (\x. \u. D)] ⇒
    let [Γ, b : block (x : term, _t : aeq x x) ⊢ D'[…, b.1, b.2]] =
      sym [Γ, b : block (x : term, _t : aeq x x) ⊢ D[…, b.1, b.2]] in
    [Γae_l (\x. \u. D')]
  | [Γae_a D1 D2] ⇒ let [ΓD1'] = sym [ΓD1] in
    let [ΓD2'] = sym [ΓD2] in [Γae_a D1' D2']
  | [Γae_tl (\x. \u. D)] ⇒
    let [Γ, b : block (a : tp, _t : atp a a) ⊢ D'[…, b.1, b.2]] =
      sym [Γ, b : block (a : tp, _t : atp a a) ⊢ D[…, b.1, b.2]] in
    [Γae_tl (\x. \u. D')]
  | [Γae_ta D Q] ⇒ let [ΓD'] = sym [ΓD] in
    let [ΓQ'] = symtp [ΓQ] in [Γae_ta D' Q'];

Proof of Completeness for Types

rec ctp : (Γ : dtpCtx) (Γdtp T S) → (Γatp T S) =
fn ecase e of
  | [Γ#p.3] ⇒ [Γ#p.2]
  | [Γdt_r] ⇒ reftp [Γ] [Γ ⊢ _]
  | [Γdt_arr F1 F2] ⇒ let [ΓD1] = ctp [ΓF1] in
    let [ΓD2] = ctp [ΓF2] in [Γat_arr D1 D2]
  | [Γdt_al (\x. \u. F)] ⇒
    let [Γ, b : block (a : tp, u : atp a a, _t : dtp a a) ⊢ D[…, b.1, b.2]] =
      ctp [Γ, b : block (a : tp, u : atp a a, _t : dtp a a) ⊢ F[…, b.1, b.3]]
    in [Γat_al (\x. \v. D)]
  | [Γdt_t F1 F2] ⇒ let [ΓD2] = ctp [ΓF2] in
    let [ΓD1] = ctp [ΓF1] in transtp [ΓD1] [ΓD2]
  | [Γdt_s F] ⇒ let [ΓD] = ctp [ΓF] in symtp [ΓD];

Proof of Completeness for Terms

rec ceq : (Γ : deqCtx) (Γdeq T S) → (Γaeq T S) =
fn ecase e of
  | [Γ#p.3] ⇒ [Γ#p.2]
  | [Γde_r] ⇒ ref [Γ] [Γ ⊢ _]
  | [Γde_a F1 F2] ⇒ let [ΓD1] = ceq [ΓF1] in
    let [ΓD2] = ceq [ΓF2] in [Γae_a D1 D2]
  | [Γde_l (\x. \u. F)] ⇒
    let
      [Γ, b : block (x : term, u : aeq x x, _t : deq x x) ⊢ D[…, b.1, b.2]] =
      ceq
        [Γ, b : block (x : term, u : aeq x x, _t : deq x x) ⊢ F[…, b.1, b.3]]
    in [Γae_l (\x. \v. D)]
  | [Γde_t F1 F2] ⇒ let [ΓD2] = ceq [ΓF2] in
    let [ΓD1] = ceq [ΓF1] in trans [ΓD1] [ΓD2]
  | [Γde_s F] ⇒ let [ΓD] = ceq [ΓF] in sym [ΓD]
  | [Γde_tl (\a. \u. F[…, a, u])] ⇒
    let [Γ, b : block (a : tp, u : atp a a, _t : dtp a a) ⊢ D[…, b.1, b.2]] =
      ceq [Γ, b : block (a : tp, u : atp a a, _t : dtp a a) ⊢ F[…, b.1, b.3]]
    in [Γae_tl (\x. \v. D)]
  | [Γde_ta F1 P2] ⇒ let [ΓQ2] = ctp [ΓP2] in
    let [ΓD1] = ceq [ΓF1] in [Γae_ta D1 Q2];
Download the code

© Computation and Logic Group McGill University Logo
Department of Computer Science, McGill University
3480 University Street, Montreal, Quebec, Canada, H3A 0E9