Implementation of the algorithm adm

This file is a companion to the paper How functorial are (deep) GADTs?. We implement here the algorithm presented in the paper designed to determine which functions can be mapped over a given term of a given GADT.

We would like to emphasize that this is merely proof-of-concept. There is room for improvement in both design choices and running time optimization. It is important to understand that Agda has been chosen for its ability to handle dependent types, which proved useful in the encoding of the GADT language, and not for verification purposes. In particular, the main function adm implementing the algorithm adm of the paper does not enforce the requirements listed in the beginning of Section 3 which make a call to adm legal: it will actually run on any correctly formed term, independently of it being an instance of the given specification or not. Of course, any non-legal call will yield results that are not meaningful. With this in mind, the reader should not be suprised by the presence of default cases in the function admissible and most of the helper functions that accompany it.

module map where

open import Relation.Nullary
open import Relation.Nullary.Decidable using (⌊_⌋)
open import Relation.Binary.Core
open import Data.Unit using () renaming (tt to )
open import Data.Empty using ()
open import Data.Product using (_×_ ; _,_ ; Σ ; proj₁ ; proj₂ )
open import Data.Sum using (_⊎_)
open import Data.Nat using ( ; zero ; suc ; _+_ ; _≟_ ; _≡ᵇ_ )
open import Data.Nat.Properties using (≡ᵇ⇒≡)
open import Agda.Builtin.String
open import Data.Bool using (Bool ; true ; false ; _∧_ ; _∨_ ; not)
open import Data.List
  using (
    List ;
    _++_ ;
    _∷_ ;
    [] ;
    map ;
    zip ;
    zipWith ;
    foldl
  )
  renaming (length to len)
open import Data.Vec
  renaming (
    Vec to tuple ;
    [] to [||] ; _∷_ to _¦_  ;
    map to tuple-map ;
    foldl to tuple-fold ;
    _++_ to _⊕⊕_ ;
    reverse to tuple-reverse ;
    last to _[-1] ;
    length to tuple-len ;
    [_] to [|_|] ;
    zip to tuple-zip ;
    zipWith to tuple-zip-with ;
    toList to [||]→[] ;
    fromList to []→[||]
  )
open import Agda.Builtin.Equality
open import Level renaming (zero to ℓ0 ; suc to ℓnext ; _⊔_  to ℓ∪)

Let us start with some standard proofs that will be useful later. These handle path algebras, properties of natural numbers, and some helper functions on tuples and booleans.

-- induced action of functions on paths
app :  {A B : Set} {a₀ a₁} (f : A  B)  a₀  a₁  (f a₀)  (f a₁)
app f refl = refl

-- composition of paths
_∙_ :  {A : Set} {a₀ a₁ a₂ : A}  a₁  a₂  a₀  a₁  a₀  a₂
q  refl = q

-- inverse operation on paths
_⁻¹ :  {A : Set} {a₀ a₁ : A}  a₀  a₁  a₁  a₀
refl ⁻¹ = refl

-- transport of an element in a dependent type family over a path
trp :  {A : Set} {a₀ a₁}  (a₀  a₁)   {B : A  Set}  B a₀  B a₁
trp refl b = b

-- transform a path in a sum type into a sum of paths 
Σ≡←≡Σ :
   {A : Set} {B : A  Set} {a₀ a₁ : A} {b₀ : B a₀} {b₁ : B a₁}
   (a₀ , b₀)  (a₁ , b₁)  Σ (a₀  a₁)  p  (trp p b₀)  b₁)
Σ≡←≡Σ refl = refl , refl

-- transform a sum of paths into as path in the sum type
≡Σ←Σ≡ :
   {A : Set} {B : A  Set} {a₀ a₁ : A} {b₀ : B a₀} {b₁ : B a₁}
   Σ (a₀  a₁)  p  (trp p b₀)  b₁)  (a₀ , b₀)  (a₁ , b₁) 
≡Σ←Σ≡ (refl , refl) = refl

-- neutrality of 0 for addition on the right
n≡n+0 :  {n}  n  n + 0
n≡n+0 {zero} = refl
n≡n+0 {suc n} = app suc n≡n+0

-- restricted case of commutativity of addition : n + 1 + m = n + m + 1
sn+m≡n+sm :  {n m}  suc n + m  n + suc m
sn+m≡n+sm {zero} = refl
sn+m≡n+sm {suc n} = app suc sn+m≡n+sm

-- commutativity of addition
n+m≡m+n :  {n m}  n + m  m + n
n+m≡m+n {zero} {m} = n≡n+0
n+m≡m+n {suc n} {m} = sn+m≡n+sm  (app suc (n+m≡m+n {n} {m}))

-- delete the last element of the tuple
cut-last :  {A : Set} {n : }  tuple A (suc n)  tuple A n
cut-last {n = zero} xs =  [||]
cut-last {n = suc n} (x ¦ xs) = x ¦ cut-last xs

-- extend a tuple with the given element
insert-last :  {A : Set} {n : }  A  tuple A n  tuple A (suc n)
insert-last a [||] = [| a |]
insert-last a (x ¦ xs) = x ¦ insert-last a xs

-- create a tuple of the given size with the given element in each cell
dummy-tuple :  {A : Set} n  A  tuple A n
dummy-tuple zero a = [||]
dummy-tuple (suc n) a = a ¦ dummy-tuple n a

-- the usual if-then-else
if_then_else_ :  {A : Set}  Bool  A  A  A
if false then _ else that = that
if true then this else _ = this

Now we will define the syntax of types in our language.

In our language, we are dealing with data type constructors, so we need a set of data type constructor variables. Each such variable is identified by an arity and a label (i.e., an element of ℕ). It is important that we use a set of identifiers with decidable equality so that we can look up a named data type constructor in a given list/tuple.

id-TC-var = 

data TC-var :   Set where
  TC : id-TC-var   k  TC-var k

-- boolean test for equality, checking the decidable equality of both the arities and the identifiers
⌊_≟ᴳ_⌋ :  {k k'}  TC-var k  TC-var k'  Bool
 (TC G k) ≟ᴳ (TC G' k')  =  G  G'    k  k' 

Next we will have identifiers for the data constructors that make elements of the data types. We choose ℕ as the set of identifiers again in order to have decidable equality.

id-data-constructor = 

We are now ready to describe the syntax of raw type expressions. We express the syntax over a set var of variables that will be instantiated later. We define the syntax via three mutually recursive data types: type-expr, data-constructor and user-defined. To guide the reader, in this setting the usual inductive definition of lists would be represented by give an element List of user-defined 1, where the usual data constructors nil and cons would each be elements of data-constructor 1, and an expression like List (ℕ × α) would then be an element of type-expr.

module type-syntax {var : Set} where

  interleaved mutual
    data type-expr : Set  -- set of type expressions
    data data-constructor :   Set  -- data constructors of given type-arity
    data user-defined :   Set  -- data-type definitions of given arity

A type expression is either the basic type 0, the basic type 1, a type variable, or a composed expression which can take one of three forms:

However, the last form needs to be refine into two subcases. Either:

For the former, we choose to write the whole definition of the type constructor as the first argument of the application. For the latter, we write only the name of the currently defined data type. (Note that it is impossible to write the whole definition, since the data type is not yet fully defined.)

Because Agda already has types constructions named 0, 1, ×, + and data_where_, we use the superscript ᵀ to differentiate our constructors in what follows.

    data type-expr where
      0ᵀ 1ᵀ : type-expr 
      varᵀ : var  type-expr
      _×ᵀ_ _+ᵀ_ : type-expr  type-expr  type-expr

      -- appᵀ G [ φs ] is the application of G to type expressions φs during the process of defining G
      appᵀ_[_] :  {k}  (TC-var k)  tuple type-expr k  type-expr

      -- data-typeᵀ G [ φs ] is the application of a previously defined data type constructor G to type expressions φs
      data-typeᵀ_[_] :  {k}  user-defined k  tuple type-expr k  type-expr

A user-defined data type constructor is given by a type constructor variable of a certain arity, together with a list of data constructors (for which we recall this type-arity).

    data user-defined where
      dataᵀ_whereᵀ_ :  {k}  (TC-var k)  List (data-constructor k)  user-defined k

A data constructor of a data type constructor G of type-arity k is given by an identifier together with a list of type expressions (the types of the arguments of the constructor), and a k-tuple of type-expressions expressing the instance of G for which the data constructor is constructing an element.

    data data-constructor where
      _∷_⇒ᵀ_ :  {k}  id-data-constructor  List type-expr  tuple type-expr k  data-constructor k

We define as well some helper functions dealing type expressions.

    -- return the arity of the outermost constructor in a type expression
    outmost-arity : type-expr  
    outmost-arity 0ᵀ = 0
    outmost-arity 1ᵀ = 0
    outmost-arity (varᵀ x) = 1
    outmost-arity (e ×ᵀ e₁) = 2
    outmost-arity (e +ᵀ e₁) = 2
    outmost-arity (appᵀ_[_] {k} _ _) = k
    outmost-arity (data-typeᵀ_[_] {k} _ _)  = k
    
    -- return the number of arguments that a data constructor is expecting
    data-constructor-term-arity :  {k}  data-constructor k  
    data-constructor-term-arity (_  Fs ⇒ᵀ _) = len Fs

    -- return the identifier of the type constructor variable used in the definition of a given data type
    user-defined-name :  {k}  user-defined k  TC-var k
    user-defined-name (dataᵀ G whereᵀ _) = G

    -- return the list of data constructors in the definition of a given data type
    user-defined-cs :  {k}  user-defined k  List (data-constructor k)
    user-defined-cs (dataᵀ _ whereᵀ cs) = cs

open type-syntax public

Of course, this raw syntax is very permissive. To structure our type calculus and make sure that it captures the informal idea of GADTs explained in the paper, we will coerce the raw syntax through inference rules. In order to do so, we first introduce contexts:

module context-handling {var : Set} where

  -- a context is either empty or Γ,α with Γ a context and α a variable 
  data context : Set where 
     : context
    _,_ : context  var  context


  -- length of a context
  ⌈_⌉ : context  
     = 0
   Γ , _  = suc  Γ 


  --concatenation of contexts
  _,,_ : context  context  context
  Γ ,,  = Γ
  Γ ,, (Γ' , α) = (Γ ,, Γ') , α


  -- make a context from a tuple of variables
  ⟦_⟧ :  {k}  tuple var k  context
   [||]  = 
   α ¦ αs  = ( , α) ,,  αs 

  
  -- the length of the concatenation of two contexts is the sum of the lengths of those contexts
  ⌈,,⌉ :  Γ Γ'   Γ ,, Γ'    Γ  +  Γ' 
  ⌈,,⌉ Γ  = n≡n+0
  ⌈,,⌉ Γ (Γ' , x) = sn+m≡n+sm  (app suc (⌈,,⌉ Γ Γ'))

  -- the length of a singleton context is 1
  ⌈∅,α⌉≡1 :  {α}    , α   1
  ⌈∅,α⌉≡1 = refl

  -- the length of a context made from a k-tuple of variables is k
  ⌈⟦k-tuple⟧⌉≡k :  {k} (αs : tuple var k)    αs    k
  ⌈⟦k-tuple⟧⌉≡k [||] = refl
  ⌈⟦k-tuple⟧⌉≡k (α ¦ αs) = (app suc (⌈⟦k-tuple⟧⌉≡k αs))  (⌈,,⌉ ( , α)  αs )

  -- make a tuple of variables from a context
  tuple-from-context :  Γ  tuple var  Γ 
  tuple-from-context  = [||]
  tuple-from-context (Γ , α) = insert-last α (tuple-from-context Γ)

In order to perform substitution in a correctly formed type expression, we need to keep track of the variables appearing in the expression. We do so by keeping track of where the variable is located in the context. In other words, we use an indexing system à la de Bruijn.

  data _∋_ : context  var  Set where
    ∋-last :  {α : var} {Γ : context}  ((Γ , α)  α)  -- either a variable is the last one is the context 
    ∋-rec :  {α β : var} {Γ : context}  (Γ  α)  ((Γ , β)  α)  -- or it is in the first part of the context

The way we handle membership of variables in a context implies that there is no repetition of variables, or more precisely that a repeated variable in a context will be treated independently from its previous occurrence. For example, given the type expression α × α in the context α , α, if the first occurrence of α is justified by ∋-last while the second is justified by ∋-rec ∋-last, then the type expression is in effect the same as β × α in the context α , β. For this reason, and to avoid confusion, we always use contexts whose elements are pairwise distinct. Notice that we don’t enforce this rule through Agda, but instead trust the user to follow this guideline.

We define now functions to weaken contexts.

  -- weaken context with one new variable on the right
  weakenr-context :  {Γ α β}  Γ  α  (Γ , β)  α
  weakenr-context = ∋-rec

  -- weaken context with a whole new context on the right
  weakenr-context-with-context :  {Γ Γ' α}  Γ  α  (Γ ,, Γ')  α
  weakenr-context-with-context {Γ' = } Γ∋α = Γ∋α
  weakenr-context-with-context {Γ' = Γ' , x} Γ∋α = weakenr-context (weakenr-context-with-context {Γ' = Γ'} Γ∋α)

  -- weaken context with one new variable on the left
  weakenl-context :
     {Γ} {α β}
     Γ  α  (( , β) ,, Γ)  α
  weakenl-context ∋-last = ∋-last
  weakenl-context (∋-rec Γ∋α) = ∋-rec (weakenl-context Γ∋α)

  -- weaken context with a whole new context on the left
  weakenl-context-with-context :
     {Γ Γ'} {α}
     Γ  α  (Γ' ,, Γ)  α
  weakenl-context-with-context ∋-last = ∋-last
  weakenl-context-with-context (∋-rec Γ∋α) = ∋-rec (weakenl-context-with-context Γ∋α)

  -- spot the first variable
  ∋-first :
     {α Γ} →(( , α) ,, Γ)  α
  ∋-first {Γ = } = ∋-last
  ∋-first {Γ = Γ , x} = ∋-rec ∋-first
  
open context-handling public

Before we can define our type calculus properly, we need to define general constructions on families of dependent types. Given a list [x₁,…,xₙ] of elements of type A and a type family B over A, the type foreach B [x₁,…,xₙ] comprises elements that are dependent sequences (p₁,…,pₙ) of elements pᵢ in B(xᵢ). The construction foreach-tuple performs the analogous operation for tuples instead of lists.

foreach :  {A : Set} (B : A  Set)  List A  Set
foreach B [] = 
foreach B (x  xs) = (B x) × (foreach B xs)

foreach-tuple :  {k} {A : Set} (B : A  Set)  tuple A k  Set
foreach-tuple B [||] = 
foreach-tuple B (x ¦ xs) = (B x) × (foreach-tuple B xs)

These new types come together with their own associated map functions.

map-foreach :
   {A A' : Set} {B : A  Set} {B' : A'  Set} {xs : List A} {g : A  A'}
   (∀ {x}  B x  B' (g x))  foreach B xs  
   (List A') × foreach B' (map g xs)
map-foreach {xs = []} _ _ = [] , 
map-foreach {xs = x  xs} {g = g} f (fst , rest) =
  let gxs , frest =  map-foreach f rest in (g x  gxs) , f fst , frest

map-foreach-tuple :
   {k} {A A' : Set} {B : A  Set} {B' : A'  Set} {xs : tuple A k} {g : A  A'}
   (∀ {x}  B x  B' (g x))  foreach-tuple B xs  
   (tuple A' k) × foreach-tuple B' (tuple-map g xs)
map-foreach-tuple {xs = [||]} _ _ = [||] , 
map-foreach-tuple {xs = x ¦ xs} {g = g} f (fst , rest) =
  let gxs , frest =  map-foreach-tuple f rest in (g x ¦ gxs) , f fst , frest

-- the version of map-foreach-tuple for which the underlying function g is id
-- needed since Agda cannot normalize tuple-map id xs to xs 
map-foreach-tuple-over-id :
   {k} {A : Set} {B B' : A  Set} {xs : tuple A k}
   (∀ {x}  B x  B' x)  foreach-tuple B xs  
   foreach-tuple B' xs
map-foreach-tuple-over-id {xs = [||]} _ _ = 
map-foreach-tuple-over-id {xs = x ¦ xs} f (fst , rest) = (f fst) , map-foreach-tuple-over-id f rest

We will also need these helper functions in the rest of the file:

-- fold with for foreach-tuples
foreach-tuple-zip-fold-with :
  {k} {A A' : Set} {B : A  Set} {B' : A'  Set} {C : Set} {xs : tuple A k} {xs' : tuple A' k}
   (∀ {x x'}  B x  B' x'  C  C)  C  foreach-tuple B xs  foreach-tuple B' xs'
   C
foreach-tuple-zip-fold-with {xs = [||]} {xs' = [||]} f acc   = acc
foreach-tuple-zip-fold-with {xs = x ¦ xs} {xs' = x' ¦ xs'} f acc (fst , rest) (fst' , rest') = 
  foreach-tuple-zip-fold-with f (f fst fst' acc) rest rest'

-- remove the last element of a dependent sequence
cut-last-foreach-tuple :
   {k} {A : Set} {B : A  Set} {xs : tuple A (suc k)}
    foreach-tuple B xs  foreach-tuple B (cut-last xs)
cut-last-foreach-tuple {xs = _ ¦ [||]} _ =  
cut-last-foreach-tuple {xs = _ ¦ _ ¦ _} (fst , rest) = fst , cut-last-foreach-tuple rest

-- extend a dependent sequence by the given element
insert-last-foreach-tuple :
   {k} {A : Set} {B : A  Set} {xs : tuple A k} {x : A} (b : B x) 
    foreach-tuple B xs  foreach-tuple B (insert-last x xs)
insert-last-foreach-tuple {xs = [||]} b _ = b ,  
insert-last-foreach-tuple {xs = _ ¦ _} b (fst , rest) = fst , insert-last-foreach-tuple b rest

-- create the constant dependent sequence with given value over a constant tuple 
dummy-foreach-tuple :
   k {A : Set} {B : A  Set} (x : A) (b : B x)
   foreach-tuple B (dummy-tuple k x)
dummy-foreach-tuple 0 x b = 
dummy-foreach-tuple (suc k) x b = b , dummy-foreach-tuple k x b

We need to be able to pass back and forth between a foreach-tuple (p₁,…,pₙ) over [x₁,…,xₙ] and its tuple of elements, by which we mean the tuple whose iᵗʰ cell is the dependent pair (xᵢ,pᵢ).

-- return the tuple of elements of a foreach-tuple
elt-foreach-tuple :
    {k} {A : Set} {B : A  Set} {xs : tuple A k}
    foreach-tuple B xs  tuple (Σ A B) k
elt-foreach-tuple {xs = [||]}  = [||]
elt-foreach-tuple {xs = x ¦ _} (fst , rest) = (x , fst) ¦ elt-foreach-tuple rest

-- return the foreach-tuple of a tuple of dependent pairs
foreach-tuple-from-tuple :
    {k} {A : Set} {B : A  Set} 
     (xs : tuple (Σ A B) k)  foreach-tuple B (tuple-map proj₁ xs)  
foreach-tuple-from-tuple [||] = 
foreach-tuple-from-tuple ((_ , b) ¦ xs) = b , foreach-tuple-from-tuple xs

-- return the list of dependent pairs (xᵢ,yᵢ) in Σ A B such that (y₁,…,yₙ) is 
-- the element of foreach B [x₁,…,xₙ] given as input 
elt-foreach :
    {A : Set} {B : A  Set} {xs : List A}
    foreach B xs  List (Σ A B)
elt-foreach {xs = []}  = []
elt-foreach {xs = x  _} (fst , rest) = (x , fst)  elt-foreach rest

-- map function for foreach-tuples when the function to be mapped is a dependent function
dmap-foreach-tuple :
   {k} {A A' : Set} {B : A  Set} {B' : A'  Set} {xs : tuple A k} {g :  {x}  B x  A'}
   (∀ {x} (b : B x)  B' (g {x} b))   (bs : foreach-tuple B xs)  
   foreach-tuple B' (tuple-map  where (x , b)  g {x} b) (elt-foreach-tuple bs))
dmap-foreach-tuple {xs = [||]} _ _ = 
dmap-foreach-tuple {xs = x ¦ xs} f (fst , rest) = (f fst) , dmap-foreach-tuple f rest

Now we can define the rules of the calculus defining the correctly formed types. It comprises two mutually recursive types written in judgment-style. The turnstile takes two contexts on its left, one of type variables and the other of type constructors variables, and allows a type expression on the right. The turnstile ⊢ᶜ takes the same kinds of contexts of the left, but admits a data constructor definition on the right. To guide the reader, we suggest reading Γ ∥ χ ⊢ φ as “the type of justifications of φ in type context Γ and TC-context χ”.

We adopt the following convention: type expressions will be denoted by variables by small (Greek) letters, while their justifications will be prepended by the symbol . For example, for a type expression φ, the elements of the type Γ ∥ χ ⊢ φ will typically be denoted ⊢φ. When the type context Γ of the justification of φ is of importance, we will use Γ⊢φ instead of ⊢φ to signify that. A typical use-case will be when weakening the context: if we have a type expression φ with justification in type context Γ, then we will use Γ⊢φ to refer to it. We will similarly use Γ,α⊢φ to refer to the induced justification in the extended context. We will reserve the letter Γ for type contexts, and the letter χ for TC-contexts. Small Greek letters from the beginning of the alphabet (α , β , γ , …) will typically denote type variables, whereas φ , ψ , σ, ζ will typically denote type expressions.

module type-calculus {var : Set} where
  type-context = context {var}
  TC-context = context {Σ  TC-var}

  interleaved mutual
    data _∥_⊢_ : type-context  TC-context  type-expr  Set 
    data _∥_⊢ᶜ_ :  {k}  type-context  TC-context  data-constructor k  Set

    data _∥_⊢_ where
      0-intro :  {Γ χ}  Γ ∥ χ ⊢ 0ᵀ  -- justification of 0
      1-intro :  {Γ χ}  Γ ∥ χ ⊢ 1ᵀ  -- justification of 1
      var-intro :  {Γ χ} {α}  (Γ ∋ α)  Γ ∥ χ ⊢ (varᵀ α) -- justification of a projection to a variable
      ×-intro :  {Γ χ G H}  (Γ ∥ χ ⊢ G)  (Γ ∥ χ ⊢ H)  Γ ∥ χ ⊢ (G ×ᵀ H) -- justification of a product
      +-intro :  {Γ χ G H}  (Γ ∥ χ ⊢ G)  (Γ ∥ χ ⊢ H)  Γ ∥ χ ⊢ (G +ᵀ H) -- justification of a sum
      app-intro : -- justification of an application of a currently defined data type
         {Γ χ} {k} {G : TC-var k}
          {φs}  foreach-tuple  φ  Γ ∥ χ ⊢ φ) φs
         (χ ∋ (k , G))  Γ ∥ χ ⊢ (appᵀ G [ φs ])
      data-intro : ---- justification of an application of a previously defined data type
         {Γ χ} {k} {G : TC-var k}
          {Γcs χcs cs}  foreach  (c , Γc , χc)  Γc ∥ χc ⊢ᶜ c) (zip cs (zip Γcs χcs))
          {φs}  foreach-tuple  φ  Γ ∥ χ ⊢ φ) φs
         Γ ∥ χ ⊢ data-typeᵀ (dataᵀ G whereᵀ cs) [ φs ]

    data _∥_⊢ᶜ_ where
      dcᵀ-intro : -- justification of the definition of a data constructor
         {Γ χ c k G}
          {Fs}  foreach  F  Γ ∥ (χ , (k , G)) ⊢ F) Fs
          {Ks : tuple _ k}  foreach-tuple  K  Γ ∥ ∅ ⊢ K) Ks
         Γ ∥ χ ⊢ᶜ (c ∷ Fs ⇒ᵀ Ks)

The type Γ ∥ χ ⊢ᶜ (c ∷ Fs ⇒ᵀ Ks) should be understood as the type of justifications of the constructor c in the given contexts. Notice how it comprises justifications of the domain of its arguments (the Fs) in the TC context extended by G, and justifications of the types at which the instance of G is defined (the Ks) in the empty TC context. This is important: we don’t want to allow any non-previously defined data types among the Ks. In particular, G itself should not appear in the Ks.

We use the following helper functions to recover the implicit arguments of an element of Γ ∥ χ ⊢ φ.

  extract-context :   {Γ χ φ}  Γ  χ  φ  context
  extract-context {Γ} _ = Γ
  
  extract-TC-context :   {Γ χ φ}  Γ  χ  φ  TC-context
  extract-TC-context {χ = χ} _ = χ

  extract-type-expr :   {Γ χ φ}  Γ  χ  φ  type-expr
  extract-type-expr {φ = φ} _ = φ

  both :  {Γ χ φ}  Γ  χ  φ  Σ type-expr  ψ  Γ  χ  ψ)
  both ⊢φ = extract-type-expr ⊢φ , ⊢φ

We can now define substitution on types Γ ∥ χ ⊢ φ. It will be crucial in the main function adm to be able to instantiate a type expression at other given type expressions.

  -- return the substitution φ[xs] in the type expression φ of the
  -- variables in its context Γ by the (justified) type expressions xs
  {-# NON_TERMINATING #-}
  _[_] :
     {Γ χ} {Γ'} {φ} {xs : tuple type-expr  Γ }
     Γ  χ  φ  foreach-tuple  x  Γ'  χ  x) xs
     Σ type-expr  φ[xs]  Γ'  χ  φ[xs])

  -- constant types do not contain variables, so substitution is neutral
  0-intro [ _ ] = 0ᵀ , 0-intro 
  1-intro [ _ ] = 1ᵀ , 1-intro 
  
  -- α₁,…,αₙ ⊢ αᵢ instantiated at x₁,…,xₙ is simply xᵢ
  var-intro ∋-last [ ⊢xs ] = (elt-foreach-tuple ⊢xs) [-1]  -- the notation [-1] returns the last element of a tuple
  var-intro {χ = χ} (∋-rec Γ∋α) [ ⊢xs ] =  (var-intro {χ = χ} Γ∋α) [ cut-last-foreach-tuple ⊢xs ]

  -- substitution in a product (resp., sum) is the product (resp., sum) of the substitutions
  (×-intro ⊢φ₁ ⊢φ₂) [ ⊢xs ] =
    let φ₁[xs] ,  ⊢φ₁[xs] =  ⊢φ₁ [ ⊢xs ] in
    let φ₂[xs] ,  ⊢φ₂[xs] =  ⊢φ₂ [ ⊢xs ] in
    (φ₁[xs] ×ᵀ φ₂[xs] , ×-intro ⊢φ₁[xs] ⊢φ₂[xs])
  (+-intro ⊢φ₁ ⊢φ₂) [ ⊢xs ] =
    let φ₁[xs] ,  ⊢φ₁[xs] = ⊢φ₁ [ ⊢xs ] in
    let φ₂[xs] ,  ⊢φ₂[xs] = ⊢φ₂ [ ⊢xs ] in
    (φ₁[xs] +ᵀ φ₂[xs] , +-intro ⊢φ₁[xs] ⊢φ₂[xs])

  -- substitution in (either) application of a data type G is the application to the substitutions
  app-intro {G = G} ⊢φs χ∋G [ ⊢xs ] =
    both (app-intro (dmap-foreach-tuple  ⊢φ  let φ[xs] , ⊢φ[xs] = ⊢φ [ ⊢xs ] in ⊢φ[xs]) ⊢φs) χ∋G)
  data-intro {G = G} ⊢cs ⊢φs [ ⊢xs ] =
    both (data-intro {G = G} ⊢cs (dmap-foreach-tuple  ⊢φ  let φ[xs] , ⊢φ[xs] = ⊢φ [ ⊢xs ] in ⊢φ[xs]) ⊢φs)) 

We now define the justification of a type expression in a weakened context.

  -- weakening by a single variable on the right
  {-# NON_TERMINATING #-}  
  weakenr :  {Γ χ} {φ} {β : var}  Γ  χ  φ  (Γ , β)  χ  φ
  weakenr 0-intro = 0-intro
  weakenr 1-intro = 1-intro
  weakenr (var-intro Γ∋α) = var-intro (weakenr-context Γ∋α)
  weakenr (×-intro ⊢φ₁ ⊢φ₂) = ×-intro (weakenr ⊢φ₁) (weakenr ⊢φ₂)
  weakenr (+-intro ⊢φ₁ ⊢φ₂) = +-intro (weakenr ⊢φ₁) (weakenr ⊢φ₂)
  weakenr (app-intro ⊢φs χ∋G) = app-intro (map-foreach-tuple-over-id weakenr ⊢φs) χ∋G 
  weakenr (data-intro ⊢cs ⊢φs) = data-intro ⊢cs (map-foreach-tuple-over-id weakenr ⊢φs)

  -- weakening by a whole context on the right
  {-# NON_TERMINATING #-}
  weakenr-with-context :
     {Γ Γ' χ} {φ : type-expr {var}}  (Γ  χ  φ)  (Γ ,, Γ')  χ  φ
  weakenr-with-context 0-intro = 0-intro
  weakenr-with-context 1-intro = 1-intro
  weakenr-with-context (var-intro Γ∋α) =  var-intro (weakenr-context-with-context Γ∋α)
  weakenr-with-context (×-intro ⊢φ₁ ⊢φ₂) = ×-intro (weakenr-with-context ⊢φ₁) (weakenr-with-context ⊢φ₂)
  weakenr-with-context (+-intro ⊢φ₁ ⊢φ₂) = +-intro (weakenr-with-context ⊢φ₁) (weakenr-with-context ⊢φ₂)
  weakenr-with-context (app-intro ⊢φs χ∋G) = app-intro (map-foreach-tuple-over-id weakenr-with-context ⊢φs) χ∋G 
  weakenr-with-context (data-intro ⊢cs ⊢φs) = data-intro ⊢cs (map-foreach-tuple-over-id weakenr-with-context ⊢φs)

  -- weakening by a single variable on the left of a variable introduction
  weakenl-var :
     {Γ} {χ} {α β : var}
     Γ  χ  varᵀ α
     (( , β) ,, Γ)  χ  varᵀ α
  weakenl-var (var-intro Γ∋α) = var-intro (weakenl-context Γ∋α)

  -- weakening by a whole context on the left
  {-# NON_TERMINATING #-}
  weakenl-with-context :
     {Γ Γ' χ} {φ}  Γ  χ  φ  (Γ' ,, Γ)  χ  φ
  weakenl-with-context 0-intro = 0-intro
  weakenl-with-context 1-intro = 1-intro
  weakenl-with-context (var-intro Γ∋α) =  var-intro (weakenl-context-with-context Γ∋α)
  weakenl-with-context (×-intro ⊢φ₁ ⊢φ₂) = ×-intro (weakenl-with-context ⊢φ₁) (weakenl-with-context ⊢φ₂)
  weakenl-with-context (+-intro ⊢φ₁ ⊢φ₂) = +-intro (weakenl-with-context ⊢φ₁) (weakenl-with-context ⊢φ₂)
  weakenl-with-context (app-intro ⊢φs χ∋G) = app-intro (map-foreach-tuple-over-id weakenl-with-context ⊢φs) χ∋G 
  weakenl-with-context (data-intro ⊢cs ⊢φs) = data-intro ⊢cs (map-foreach-tuple-over-id weakenl-with-context ⊢φs)

  -- provide justifications of each of the variables in the context made out the given tuple of variables
  vars-in-context :
     {k} {χ}
      (αs : tuple var k)
     (foreach-tuple  α   αs   χ  varᵀ α) αs)
  vars-in-context [||] = 
  vars-in-context {χ = χ} (α ¦ αs) =
    var-intro ∋-first , map-foreach-tuple-over-id weakenl-var (vars-in-context αs)

  -- weakening the TC context on the right by a single variable
  {-# NON_TERMINATING #-}
  weakenr-TC-var :
     {Γ χ G} {φ}  Γ  χ  φ  Γ  (χ , G)  φ
  weakenr-TC-var 0-intro = 0-intro
  weakenr-TC-var 1-intro = 1-intro
  weakenr-TC-var (var-intro x) = var-intro x
  weakenr-TC-var (×-intro ⊢φ₁ ⊢φ₂) = ×-intro (weakenr-TC-var ⊢φ₁) (weakenr-TC-var ⊢φ₂)
  weakenr-TC-var (+-intro ⊢φ₁ ⊢φ₂) = +-intro (weakenr-TC-var ⊢φ₁) (weakenr-TC-var ⊢φ₂)
  weakenr-TC-var (app-intro ⊢φs χ∋G) = app-intro (map-foreach-tuple-over-id weakenr-TC-var ⊢φs) (∋-rec χ∋G)
  weakenr-TC-var (data-intro ⊢cs ⊢φs) = data-intro ⊢cs (map-foreach-tuple-over-id weakenr-TC-var ⊢φs)

  -- weakening the TC context on the right by a whole TC context
  {-# NON_TERMINATING #-}
  weakenr-TC-context :
     {Γ χ χ'} {φ}  Γ  χ  φ  Γ  (χ ,, χ')  φ
  weakenr-TC-context {χ' = } ⊢φ = ⊢φ
  weakenr-TC-context {χ' = χ' , G} ⊢φ = weakenr-TC-var {G = G} (weakenr-TC-context {χ' = χ'} ⊢φ)

  -- weakening the TC context on the left by a whole TC context
  {-# NON_TERMINATING #-}
  weakenl-TC-context :
     {Γ χ χ'} {φ}  Γ  χ  φ  Γ  (χ' ,, χ)  φ
  weakenl-TC-context 0-intro = 0-intro
  weakenl-TC-context 1-intro = 1-intro
  weakenl-TC-context (var-intro x) = var-intro x
  weakenl-TC-context (×-intro ⊢φ₁ ⊢φ₂) = ×-intro (weakenl-TC-context ⊢φ₁) (weakenl-TC-context ⊢φ₂)
  weakenl-TC-context (+-intro ⊢φ₁ ⊢φ₂) = +-intro (weakenl-TC-context ⊢φ₁) (weakenl-TC-context ⊢φ₂)
  weakenl-TC-context (app-intro ⊢φs χ∋G) = app-intro (map-foreach-tuple-over-id weakenl-TC-context ⊢φs) (weakenl-context-with-context χ∋G)
  weakenl-TC-context (data-intro ⊢cs ⊢φs) = data-intro ⊢cs (map-foreach-tuple-over-id weakenl-TC-context ⊢φs)
  
open type-calculus public

Up to now, our constructions have been done over a set var of variables, that can be instantiated to different kind of variables. We now define such instances.

We need first a set of type variables, each of them identified by a label (here an element of ℕ). We also define functions whose purpose is to make variables which are fresh relative to a given tuple.

id-type-var = 
type-var = id-type-var -- interface to the identifiers

-- create a fresh natural number out of a tuple by returning the successor of the maximum 
fresh-ℕ :  {k}  tuple  k  
fresh-ℕ xs =
  suc (find-max xs 0)
  where
    max :     
    max zero m = m
    max (suc n) zero = suc n
    max (suc n) (suc m) = suc (max n m)
    
    find-max :  {k}  tuple  k    
    find-max [||] n = n
    find-max (x ¦ xs) zero = find-max xs x
    find-max (zero ¦ xs) n+1@(suc n) = find-max xs n+1
    find-max (suc x ¦ xs) (suc n) = find-max xs (suc (max x n))

-- interface for type-var
fresh-type-var :  {k}  tuple type-var k  type-var
fresh-type-var = fresh-ℕ 

-- make a whole fresh tuple of variables
make-distinct-tuple :  {var : Set} {m}  tuple var m  (∀ {k}  tuple var k  var)   n  tuple var n
make-distinct-tuple xs f zero = [||]
make-distinct-tuple xs f (suc n) = let new = f xs in new ¦ make-distinct-tuple (new ¦ xs) f n 

Next we need to define a set of function variables. We want to be just a bit more cautious with function variables: since they will be used to express the set of constraints that adm returns, we want to carefully track a state specifying which function variables have already been used. By tracking this state in the various recursive calls to adm, we can ensure that fresh function variables are produced.

id-fun-var =   -- identifiers for function variables
fun-var = id-fun-var  -- interface to function variables

fresh-id-fun-var : id-fun-var  id-fun-var  -- function creating the next fresh identifier
fresh-id-fun-var = suc

record state (id : Set) (fresh : id  id) : Set where  -- generic type of states
  field
    curr : id
fun-var-state = state id-fun-var fresh-id-fun-var  -- interface to the state tracking function variables


-- return a tuple with pairwise distinct elements, all of which are fresh relative to the argument tuple, by keeping track of the function variable state
make-fresh-tuple :  {id fresh} (k : )  state id fresh  tuple id k × state id fresh 
make-fresh-tuple zero s = [||] , s
make-fresh-tuple {fresh = fresh} (suc n) s =
  let (ids , s') = make-fresh-tuple n (record { curr = fresh (state.curr s) }) in
  state.curr s ¦ ids , s'

We now craft a type of variables that can be either a type variable or a function variable. This is done in order to be able to represent a substitution of the form Σ[φs] when Σ is a type expression with variable of type type-var and the φs are type expressions with variables of type fun-var. For a way to avoid this intermediate set of variables mix-var, see the list of improving ideas at the end of the file.

data mix-var : Set where
  fvar : fun-var  mix-var
  tvar : type-var  mix-var

-- version of fresh-type-var within mix-var
fresh-tvar-mix-var :  {k}  tuple mix-var k  mix-var
fresh-tvar-mix-var tvars = tvar (fresh-type-var (tuple-map  {(tvar id)  id ; _  0}) tvars))

-- boolean equality for elements of mix-var. The decidability of equality in the type of identifiers (ℕ here) is crucial here.
mix-var-bool-= : mix-var  mix-var  Bool
mix-var-bool-= (fvar i) (fvar j) =  i  j 
mix-var-bool-= (tvar i) (tvar j) =  i  j 
mix-var-bool-= _ _ = false

The algorithm adm acts on terms of a given type in a certain specification. So we need to encode the syntax of our terms next. We only deal with closed terms, since the presence of open terms is not relevant to the algorithm adm. A term is either in the type 1ᵀ, or an inl or an inr in a sum type, or a pair in a product, or a term constructed by a data constructor of one of the user-defined data types of our language.

module term-calculus where

  data term-expr : Set where
    ∗ᵗ : term-expr
    inl inr : term-expr  term-expr
    _,_ : term-expr  term-expr  term-expr
    dc :  {k} (c : data-constructor {mix-var} k)  tuple term-expr (data-constructor-term-arity c)   term-expr
    
open term-calculus public

We now give definitions relative to the constraint and assignments presented in the algorithm.

data constraint (var : Set) : Set where
  ⟨_,_⟩ : type-expr {var}  type-expr {var}  constraint var

data assignment (var : Set) {Γ Γ' : context {var}} {χ χ' : TC-context {var}} : Set where
  _≣_ :  {φ φ'}  Γ  χ  φ  Γ'  χ'  φ'  assignment var


-- return the list of assignments obtained after top-unifying a given assignment
-- The *should not happen!*-cases should indeed not happen since we will only solve top-unifiable assignments.
{-# NON_TERMINATING #-}
solve-matching :  {Γ Γ' χ χ'}  assignment mix-var {Γ} {Γ'} {χ} {χ'}  List (assignment mix-var {Γ} {Γ'} {χ} {χ'})

-- base cases: a variable is assigned to a type expression
solve-matching a@(var-intro _  _) = a  []
solve-matching a@(_  var-intro _) = a  []

-- peeling off the outermost constructor whenever possible
solve-matching (×-intro ⊢φ₁ ⊢φ₂  ×-intro ⊢φ₁' ⊢φ₂') = 
  solve-matching (⊢φ₁  ⊢φ₁') ++ solve-matching (⊢φ₂  ⊢φ₂')
solve-matching (+-intro ⊢φ₁ ⊢φ₂  +-intro ⊢φ₁' ⊢φ₂') =
  solve-matching (⊢φ₁  ⊢φ₁') ++ solve-matching (⊢φ₂  ⊢φ₂') 
solve-matching (data-intro {G = TC k G} ⊢cs ⊢φs  data-intro {G = TC k' G'} ⊢cs' ⊢φs') with  G  G'    k  k' 
... | true = foldl  acc a  acc ++ solve-matching a) [] (zipWith  (_ , ⊢φ) (_ , ⊢φ')  ⊢φ  ⊢φ')
        ([||]→[] (elt-foreach-tuple ⊢φs))
        ([||]→[] (elt-foreach-tuple ⊢φs')))
... | false = []  -- *should not happen!* 
solve-matching (app-intro {G = TC k G} ⊢φs χ∋G  app-intro {G = TC k' G'} ⊢φs' χ∋G') with  G  G'    k  k' 
... | true = foldl  acc a  acc ++ solve-matching a) [] (zipWith  (_ , ⊢φ) (_ , ⊢φ')  ⊢φ  ⊢φ')
        ([||]→[] (elt-foreach-tuple ⊢φs))
        ([||]→[] (elt-foreach-tuple ⊢φs')))
... | false = []  -- *should not happen!*
solve-matching _ = (0-intro  1-intro)  []  -- *should not happen!*

We define a type that encloses the data needed for the full definition of a user-defined data type. Elements of that type will be carried over the different calls to adm to recall the user-defined data type already encountered during the process. In that way, when we encounter the name of a data type, we can look up this list of already-encountered data types to recover the associated constructors.

record recorded-data-type : Set where
  constructor arity_def_ctx_∥_der_
  field
   arity : 
   def : user-defined {mix-var} arity
   type-ctxs : List (type-context {mix-var})
   TC-ctxs : List (TC-context {mix-var})
   der : foreach  (c , Γc , χc)  _∥_⊢ᶜ_ {k = arity} Γc χc c) (zip (user-defined-cs def) (zip type-ctxs TC-ctxs))
-- return the list of constaints [⟨σ₁,σⱼ⟩ : j ∈ {2,…,n}] when the input list is [σ₁,σ₂,…,σₙ].  
constraint-with-fst :
   {Γ χ}
   List (Σ (type-expr {mix-var})  σ  Γ  χ  σ))
   List (constraint mix-var)
constraint-with-fst [] = []
constraint-with-fst (_  []) = []
constraint-with-fst ((σ₁ , ⊢σ₁)  (σ₂ , _)  σs) =  σ₂ , σ₁   (constraint-with-fst ((σ₁ , ⊢σ₁)  σs))

We are now ready to implement the algorithm adm of the paper. For reasons specific to the implementation, adm takes more arguments than in the paper. We keep the same name as in the paper for the arguments that are common. We will also keep, as much as possible, the names of the various local variables in adm, with the exception that the tuples named with an overline in the paper are here marked with an ending ‘s’. For example, the tuple f̅ is written as fs in the algorithm. We will also indicate in comments the correspondence between a code excerpt and the steps of the algorithm in the paper. However, the correspondence is not perfect and sometimes a step of the paper is divided in the following code. The comments indicating the steps are thus to be taken more as an aid to reading the code than an exact match with the algorithm described in the paper.

Here is a high-level description of the explicit arguments (in order) of adm:

  1. t is the term we want to map over.
  2. ⊢Φ is the justification of the specification Φ in which we are considering t.
  3. fs is a tuple of functions for which adm will return constraints to be mappable over t
  4. s is the state of function variables with which to start the algorithm in order to be sure to have fresh function variables when needed.
  5. Gs is a list of already-encountered data types in the recorded-data-type format. The function adm return a list of constraints together with the new state of function variables.

The function adm is mutually recursively defined with handle which is basically a glorified switch case that performs the recursive calls to adm described in the paper. Described at a high level, the call handle t ⊢𝐷(ζ₁,…,ζₙ) ⊢gs s Gs, where 𝐷 ∈ {+,×} ∪ 𝒢, performs first the substitution of the gs in each type expression ζⱼ and then calls adm t ⊢𝐷(ζ₁,…,ζₙ) (ζ₁[gs],…,ζₙ[gs]) s Gs.

Another difference with the algorithm in the paper lies in the handling of the lengths of the various tuples involved. In the paper, some of these tuples have obviously the same length. Unfortunately, this fact can not always be derived by Agda, in particular when the lengths involved are not definitionally equal (that is, when Agda can’t normalize them to the same term). In these cases we have to guide Agda. In particular, when we use a transport over a path p : k ≡ k' of an element b in a type family B : ∀ {n : ℕ} → tuple A n → Set (that is, B is dependent over tuples of given type), the resulting element will typically be denoted b-corr (standing for corrected version of b).

{-# NON_TERMINATING #-}
adm :
  term-expr -- term we want to map over
    {Γ χ} {Φ : type-expr {mix-var}}  Γ  χ  Φ -- type expression the term should be regarded as an element of
   tuple (type-expr {mix-var}) (outmost-arity Φ) -- function expressions to map over the term
   fun-var-state -- current state for fresh function variables
   List recorded-data-type
  -- return a list of constraints on the function expressions and the new current state
   List (constraint mix-var) × fun-var-state 

-- Mutually recursive function to handle the recursive calls of adm
handle :
  term-expr -- term to call adm recursively on
    {Γ Γ'} {χ} {Φ}  (Γ'  χ  Φ) -- type expression the term should be regarded as an element of
    {gs}  foreach-tuple {k =  Γ' }  v  Γ  χ  v) gs -- function expressions to feed the recursive calls after substitution
   fun-var-state -- current state for fresh function variables
   List recorded-data-type -- list of user-defined data types already encountered
  -- return a list of constraints on the function expressions and the new current state
   List (constraint mix-var) × fun-var-state

handle _ (var-intro x) _ s Gs = [] , s
handle t (×-intro {G = ζ₁} {H = ζ₂} ⊢ζ₁ ⊢ζ₂) ⊢gs s Gs =
  let ζ₁[gs] , _ = ⊢ζ₁ [ ⊢gs ] in
  let ζ₂[gs] , _ = ⊢ζ₂ [ ⊢gs ] in
  adm t (×-intro ⊢ζ₁ ⊢ζ₂) ( ζ₁[gs] ¦ ζ₂[gs] ¦ [||]) s Gs
handle t (+-intro {G = ζ₁} {H = ζ₂} ⊢ζ₁ ⊢ζ₂) ⊢gs s Gs =
  let ζ₁[gs] , _ = ⊢ζ₁ [ ⊢gs ] in
  let ζ₂[gs] , _ = ⊢ζ₂ [ ⊢gs ] in
  adm t (+-intro ⊢ζ₁ ⊢ζ₂) (ζ₁[gs] ¦ ζ₂[gs] ¦ [||]) s Gs
handle t D@(data-intro {k = k} {G = TC _ k} _ ⊢ζs) ⊢gs s Gs =
  adm t D (tuple-map  (_ , ⊢ζ)  proj₁ (⊢ζ [ ⊢gs ])) (elt-foreach-tuple ⊢ζs)) s Gs
handle t D@(app-intro ⊢ζs χ∋G) ⊢gs s Gs =
  adm t D (tuple-map  (_ , ⊢ζ)  proj₁ (⊢ζ [ ⊢gs ])) (elt-foreach-tuple ⊢ζs)) s Gs
handle _ 0-intro _ s _ = [] , s
handle _ 1-intro _ s _ = [] , s

-- handle built-in pairs: case A in the paper
adm (t₁ , t₂) {Γ} {χ} {Σ₁ ×ᵀ Σ₂} (×-intro {Γ = Γ} ⊢Σ₁ ⊢Σ₂) (f₁ ¦ f₂ ¦ [||] ) s Gs = 
  let (ids-gs , s') = make-fresh-tuple  Γ  s in  -- step A.(i)
  let gs-vars = vars-in-context (tuple-map fvar ids-gs) in 
  let _ , gs = map-foreach-tuple  ⊢α  ⊢α) gs-vars in
  let handle-t₁ , s₁ = (handle t₁ ⊢Σ₁ gs s' Gs) in  -- step A.(ii) for j=1
  let handle-t₂ , s₂ = (handle t₂ ⊢Σ₂ gs s₁ Gs) in  -- step A.(ii) for j=2
  let Σ₁[gs] , _ = ⊢Σ₁ [ gs ] in
  let Σ₂[gs] , _ = ⊢Σ₂ [ gs ] in
   Σ₁[gs] , f₁    Σ₂[gs] , f₂    handle-t₁ ++ handle-t₂ , s₂  -- step A.(iii)

-- handle built-in injections
-- inl: case B in the paper
adm (inl t) {Γ} {χ} {Σ₁ +ᵀ Σ₂} (+-intro {Γ = Γ} ⊢Σ₁ ⊢Σ₂) (f₁ ¦ f₂ ¦ [||] ) s Gs = 
  let (ids-gs , s') = make-fresh-tuple  Γ  s in  -- step B.(i)
  let gs-vars = vars-in-context (tuple-map fvar ids-gs) in
  let _ , gs = map-foreach-tuple  ⊢α  ⊢α) gs-vars in
  let handle-t , s'' = (handle t ⊢Σ₁ gs s' Gs) in -- step B.(ii)
  let Σ₁[gs] , _ = ⊢Σ₁ [ gs ] in
  let Σ₂[gs] , _ = ⊢Σ₂ [ gs ] in
   Σ₁[gs] , f₁    Σ₂[gs] , f₂    handle-t , s''  -- step B.(iii)

-- inr: case C of the paper
adm (inr t) {Γ} {χ} {Σ₁ +ᵀ Σ₂} (+-intro {Γ = Γ} ⊢Σ₁ ⊢Σ₂) (f₁ ¦ f₂ ¦ [||] ) s Gs = 
  let (ids-gs , s') = make-fresh-tuple  Γ  s in  -- step C.(i) 
  let gs-vars = vars-in-context (tuple-map fvar ids-gs) in
  let _ , gs = map-foreach-tuple  ⊢α  ⊢α) gs-vars in
  let handle-t , s'' = (handle t ⊢Σ₂ gs s' Gs) in  -- step C.(ii)
  let Σ₁[gs] , _ = ⊢Σ₁ [ gs ] in
  let Σ₂[gs] , _ = ⊢Σ₂ [ gs ] in
   Σ₁[gs] , f₁    Σ₂[gs] , f₂    handle-t , s''  -- step C.(iii)

We are now tackling case D of the paper. This is where the interesting constraints on morphisms can appear. The informal version in the paper works under the implicit assumption that we can recover the constructors of a data type merely from its name. This is a fair assumption when working in a high-level fashion. In contrast, the implementation of our language presented here needs extra care for calls to admissible where the specification Φ has the form 𝐷(ζ₁,…,ζₙ): the specification is indeed fairly different depending on whether the application of 𝐷 is extracted from the definition of a data constructor (that is, the application is a appᵀ name-of-𝐷 [ζ₁,…,ζₙ]) or the application of 𝐷 is the application of a previously completely defined data type (that is, the application is a data-typeᵀ full-definition-of-𝐷 [ζ₁,…,ζₙ]). These two cases should be both handled by case D of the paper, hence we just translate the former into the latter and treat only the latter according to case D.

-- handle expression of the form appᵀ G [ Σs ]. It searches for G in
-- the list Gs of already-encountered user-defined data types. G MUST
-- be in the list. Then it simply calls admissible on data-typeᵀ (dataᵀ
-- G whereᵀ cs) [ Σs ], where the cs is the list of data constructors
-- associated to G found in Gs.
adm
  t -- the term doesn't matter here, but it should be of the form `dc …`
  {Γ} {χ} {appᵀ G@(TC _ k) [ Σs ]}
  (app-intro {Γ = Γ} {χ} ⊢Σs χ∋G)
  fs s Gs =

  let arity k' def Gdef ctx _  _ der ⊢cs , k≡k' = lookup-data-type G Gs in  -- find G in Gs

  -- the arity k' of G' is equal to k, but we have to guide Agda to
  -- get the version of ⊢Σs over the correct size k'
  let _ , ⊢Σs-corr = trp k≡k' {B = λ n  Σ (tuple _ n)  xs  foreach-tuple _ xs)} (Σs , ⊢Σs) in
  -- same for fs
  let fs-corr = trp k≡k' {B = tuple type-expr} fs in
  adm t (data-intro {G = user-defined-name Gdef} ⊢cs ⊢Σs-corr) fs-corr s Gs

    where

    -- return the element in the input list whose field definition is `dataᵀ
    -- G' whereᵀ …` with G' equal to G, together with a proof of
    -- equality of the respective arities of G' and G.
    lookup-data-type :
       {k}  TC-var k  List recorded-data-type
       Σ recorded-data-type  (arity k' def _ ctx _  _ der _)  k  k') 
    lookup-data-type {k} G (head@(arity k' def (dataᵀ G' whereᵀ _) ctx _  _ der _)  Gs) with k  k'
    ... | false because _ = lookup-data-type G Gs
    ... | true because (ofʸ k≡k') = if  G ≟ᴳ G'  then (head , k≡k') else (lookup-data-type G Gs)
    -- should not happen
    lookup-data-type {k} G [] = arity k def (dataᵀ G whereᵀ []) ctx []  [] der  , refl

-- handle user-defined data types: case D in the paper
adm
  (dc (c  Fs ⇒ᵀ Ks) ts)
  {Γ} {χ} {data-typeᵀ (dataᵀ G@(TC _ k) whereᵀ cs) [ Σs ]}
  (data-intro {Γ = Γ} {χ = χ} {Γcs = Γcs} {χcs = χcs} ⊢cs ⊢Σs)
  fs s Gs = 

  -- we just encountered a (possibly) new data type, so we push it on the stack of the already-encountered ones. 
  let Gs-updated = (arity k def (dataᵀ G whereᵀ cs) ctx Γcs  χcs der ⊢cs)  Gs in -- 

  -- create the variables in βs in context
  let βs = tuple-from-context Γ in
  let Γβs =  βs  in let βs⊢βs-var = vars-in-context {χ = χ} βs in
  let βs-size-⌈Γβs⌉ = correct-size βs in 
  let _ , βs⊢βs = map-foreach-tuple {B' = λ Φ  Γβs  χ  Φ}  ⊢α  ⊢α) βs⊢βs-var in

  -- create the fresh function variables in gs in context (step D.(i))
  let (ids-gs , s-after-gs) = make-fresh-tuple  Γβs  s in
  let Γgs =  tuple-map fvar ids-gs  in let gs-vars = vars-in-context {χ = χ} (tuple-map fvar ids-gs) in
  let gs , gs⊢gs = map-foreach-tuple {B' = λ Φ  Γgs  χ  Φ} {g = varᵀ}  ⊢α  ⊢α) gs-vars in
  let gs⊢gs-corr = foreach-tuple-from-tuple (trp (⌈⟦k-tuple⟧⌉≡k βs) {B = tuple (Σ type-expr  Φ  Γgs  χ  Φ))} (elt-foreach-tuple gs⊢gs)) in

  -- add new constraints to C (step D.(i))
  let C = [||]→[] (tuple-map  ((_ , ⊢Σ) , f)   proj₁ (⊢Σ [ gs⊢gs-corr ]) , f ) (tuple-zip (elt-foreach-tuple ⊢Σs) fs)) in


  -- look for the data constructor c in the list of data constructors associated to G
  let ( Gc , Γc , χc ) , (Fsc , ⊢Fsc) , (Ksc , ∅⊢Ksc) = lookup-dc {G = G} c ⊢cs in
  -- set the element of the tuple Ks in the correct context
  let ⊢Ksc = map-foreach-tuple-over-id  ⊢K  weakenl-TC-context {χ' = χ} ⊢K) ∅⊢Ksc in

  -- create the variables in γs in context 
  let γs = make-distinct-tuple βs fresh-tvar-mix-var  Γc  in
  let Γγs =  γs  in let γs⊢γs-var = vars-in-context {χ = } (γs) in
  let γs-size-⌈Γγs⌉ = correct-size γs in
  let _ , γs⊢γs = map-foreach-tuple {B' = λ Φ  Γγs  χ  Φ}  ⊢α  weakenl-TC-context ⊢α) γs⊢γs-var in

  -- consider each Σₗ in variables βs and each Kₗ in variables γs 
  let ⊢Σs[βs] = dmap-foreach-tuple {B' = λ Φ  Γβs  χ  Φ} {g = λ {Σ} (⊢Σ : Γ  χ  Σ)  proj₁ (⊢Σ [ βs⊢βs ])}  ⊢Σ  proj₂ (⊢Σ [ βs⊢βs ])) ⊢Σs in
  let ⊢Ksc[γs] = dmap-foreach-tuple {B' = λ Φ  Γγs  χ  Φ} {g = λ {K} (⊢K : Γc  χ  K)  proj₁ (⊢K [ γs⊢γs ])}  ⊢K  proj₂ (⊢K [ γs⊢γs ])) ⊢Ksc in

  -- create the system of matching problems of step D.(ii)
  let system = foreach-tuple-zip-fold-with  ⊢Σ[βs] ⊢K[γs] acc  ( ⊢Σ[βs]   ⊢K[γs])  acc) [] ⊢Σs[βs] ⊢Ksc[γs] in
  -- solve the system to get the assignments in the form described in the last paragraph of step D.(ii)
  let assignments = foldl  acc a  acc ++ solve-matching {Γ = Γβs} {Γ' = Γγs} a) [] system in 

  -- make a pass on the list of assignments to sort them into the tuples called ψs and σs in case D (these are tuples of lists of type expressions)
  let ψs , σs = sort-assignments {Γ = Γβs} {Γ' = Γγs} assignments βs-size-⌈Γβs⌉ γs-size-⌈Γγs⌉ (dummy-tuple   βs   []) (dummy-tuple   γs   []) in

  -- helping functions
  let βs→βs,γs = λ Φ (⊢Φ : Γβs  χ  Φ)  weakenr-with-context {Γ = Γβs} {Γ' = Γγs} {χ = χ} {φ = Φ} ⊢Φ in  -- weakening on the right by γs
  let γs→βs,γs = λ Φ (⊢Φ : Γγs  χ  Φ)  weakenl-with-context {Γ = Γγs} {Γ' = Γβs} {χ = χ} {φ = Φ} ⊢Φ in  -- weakening on the left by βs

  -- put the type expressions in each of the σᵢ in the context βs,γs
  let βs,γs⊢σs = tuple-map {B = List (Σ type-expr  Φ  (Γβs ,, Γγs)  χ  Φ))} (map  (σ , ⊢σ)  σ , βs→βs,γs σ ⊢σ)) σs in
  -- put each variable γᵢ in context βs,γs (with corrected size when needed)
  let βs,γs⊢γs-off = tuple-map  (γ , ⊢γ)  γ , γs→βs,γs γ ⊢γ) (elt-foreach-tuple γs⊢γs) in
  let βs,γs⊢γs = dcorrect-size {B = λ {n} γs  tuple (Σ type-expr  Φ  (Γβs ,, Γγs)  χ  Φ)) n} {αs = γs} βs,γs⊢γs-off in

  -- create the elements of τs of step D.(iii)
  let τs-off = tuple-zip-with  {_ (σ  σi)  σ ; γ []  γ}) βs,γs⊢γs βs,γs⊢σs in
  let τs-tuple = trp (⌈⟦k-tuple⟧⌉≡k γs) {B = tuple (Σ type-expr  Φ  _  χ  Φ))} τs-off in
  let ⊢τs = foreach-tuple-from-tuple τs-tuple in

  -- create the fresh function variables in hs (step D.(iv))
  let ids-hs , s-after-hs = make-fresh-tuple  Γγs  s-after-gs in
  let Γhs =  tuple-map fvar ids-hs  in let hs-vars = vars-in-context {χ = χ} (tuple-map fvar ids-hs) in
  let hs , hs⊢hs = map-foreach-tuple {B' = λ Φ  Γhs  χ  Φ}  ⊢α  ⊢α) hs-vars in

  -- add the constraints of step D.(v) 
  let hs⊢ψs[hs] = tuple-map (map  (_ , ⊢ψ)  ⊢ψ [ hs⊢hs ] )) ψs in
  let Cψs = tuple-zip-with  ψi g  map  (ψ , ⊢ψ)   ψ , g ) ψi) hs⊢ψs[hs] gs in

  -- add the constraints of step D.(vi)
  let gs⊢σs[gs] = tuple-map (map  (_ , ⊢σ)  _[_] {Γ' = Γgs} ⊢σ gs⊢gs )) σs in
  let Cσs = tuple-map constraint-with-fst gs⊢σs[gs] in
  
  -- create the proof ⊢g and ⊢h for each g and h in the context gs,hs (with corrected size when needed)
  let gs,hs⊢gs = map-foreach-tuple-over-id (weakenr-with-context {Γ' = Γhs}) gs⊢gs in
  let gs,hs⊢hs = map-foreach-tuple-over-id (weakenl-with-context {Γ' = Γgs}) hs⊢hs in
  let gs,hs⊢gs,hs-off = (elt-foreach-tuple gs,hs⊢gs ⊕⊕ elt-foreach-tuple gs,hs⊢hs) in
  let gs,hs⊢gs,hs = foreach-tuple-from-tuple (trp ((⌈,,⌉ Γβs Γγs) ⁻¹) {B = tuple (Σ type-expr  Φ  (Γgs ,, Γhs)  χ  Φ))} gs,hs⊢gs,hs-off) in
  
  -- compute each Rⱼ in step D.(vii)
  let ⊢Rs = map  (_ , ⊢F)  (weakenl-TC-context {χ' = χ} ⊢F) [ map-foreach-tuple-over-id (weakenr-TC-context {χ' = (χc , (k , Gc))}) ⊢τs ]) (elt-foreach ⊢Fsc) in

  -- make each of the recursive calls of step D.(vii) through handle
  let handle-ts-s'' = foldl  (acc , curr) (t , (R , ⊢R))  let Cₜ , next = handle t ⊢R (map-foreach-tuple-over-id weakenr-TC-context gs,hs⊢gs,hs) curr Gs-updated in (acc ++ Cₜ) , next) (C , s-after-hs) (zip ([||]→[] ts) ⊢Rs) in

  -- collect all of the constraints obtained in step D.(vii) and add them to C before returning it (step D.(viii)) 
  (foldl  acc elt  acc ++ elt) [] (([||]→[] Cψs) ++ ([||]→[] Cσs))) ++ proj₁ handle-ts-s'' , proj₂ handle-ts-s'' 
    where

    -- return the exact same tuple as the input but considered with modified length
    correct-size :  {k} (αs : tuple mix-var k)  tuple mix-var   αs  
    correct-size αs = trp ((⌈⟦k-tuple⟧⌉≡k αs) ⁻¹) {B = tuple mix-var} αs

    -- same but for elements of a family dependent on tuples of type mix-var
    dcorrect-size :
       {k} {B :  {n}  tuple mix-var n  Set} {αs : tuple mix-var k}
       B αs
       B (correct-size αs)
    dcorrect-size {B = B} {αs = αs} b = trp (≡Σ←Σ≡ (((⌈⟦k-tuple⟧⌉≡k αs) ⁻¹) , refl)) {B = λ (n , αs)  B {n} αs} b

    -- look for the input identifier in a given list of data
    -- constructors and return its associated data, namely, the
    -- identifier itself, the type expressions in its domain, and the 
    -- type expressions in its codomain. The identifier MUST be present.
    lookup-dc :
       {k} {G : TC-var k}  id-data-constructor
        {Γcs χcs} {cs :  List (data-constructor {mix-var} k)}  foreach  (c , Γc , χc)  Γc  χc ⊢ᶜ c) (zip cs (zip Γcs χcs))
      
        Σ (TC-var k × type-context {mix-var} × TC-context {mix-var})
         (G' , Γc , χc)  
          Σ (List type-expr)  Fs  foreach  F  Γc  (χc , (k , G'))  F) Fs)
          ×
          Σ (tuple type-expr k)  Ks  foreach-tuple  K  Γc    K) Ks)
        )
    lookup-dc {k} {G} c {Γc'  _} {χc'  _} {(c'  _ ⇒ᵀ _)  _} ((dcᵀ-intro {G = G'} {Fs = Fsc'} ⊢Fsc' {Ks = Ksc'} ⊢Ksc') , ⊢cs) =
      if  c  c'    G ≟ᴳ G'  then (G' , Γc' , χc') , (Fsc' , ⊢Fsc') , (Ksc' , ⊢Ksc')
      else lookup-dc {G = G} c ⊢cs
    lookup-dc {k} _ _ =
      (TC 0 k ,  , ) , ([] , ) , (dummy-tuple k 0ᵀ , dummy-foreach-tuple k 0ᵀ 0-intro)  -- default, should not happen

    -- return the tuple given as first argument where the position
    -- corresponding to the first occurrence of the third argument in
    -- the tuple given as second argument has been updated according
    -- to the function in fourth argument; the boolean equality to find
    -- the first occurrence is determined by the function given in
    -- fifth argument
    lookup&update :  {A B : Set} {k}  tuple A k  tuple B k  B  (A  A)  (B  B  Bool)  tuple A k
    lookup&update [||] [||] _ _ _ = [||]
    lookup&update (x ¦ datas) (i ¦ indexs) i' update _?=_ with i ?= i'
    ... | true = (update x) ¦ datas
    ... | false = x ¦ (lookup&update datas indexs i' update _?=_)

    -- sort the assignments (solutions to the matching problem of step D.(ii)) 
    sort-assignments :
       {Γ Γ'} {χ χ'} {k k'}
       List (assignment mix-var {Γ} {Γ'} {χ} {χ'})
       tuple (mix-var) k  tuple (mix-var) k'
       tuple (List (Σ type-expr  Φ  Γ'  χ'  Φ))) k
       tuple (List (Σ type-expr  Φ'  Γ  χ  Φ'))) k'
       (
        tuple (List (Σ type-expr  Φ  Γ'  χ'  Φ))) k
        ×
        tuple (List (Σ type-expr  Φ'  Γ  χ  Φ'))) k'
      )
    sort-assignments [] _ _ ψs σs = ψs , σs
    sort-assignments ((σ  var-intro {α = γ} _)  as) βs γs ψs σs =
      sort-assignments as βs γs ψs (lookup&update σs γs γ  σi  (both σ)  σi) (mix-var-bool-=)) 
    sort-assignments ((var-intro {α = β} _  ψ)  as) βs γs ψs σs =
      sort-assignments as βs γs (lookup&update ψs βs β  ψi  (both ψ)  ψi) (mix-var-bool-=)) σs
    -- default case, should not happen
    sort-assignments _ βs γs _ _ = (tuple-map  _  []) βs) , (tuple-map  _  []) γs) 
    
-- default case, should not happen; we use the 'warning code' 2000 to
-- indicate that the user has called the function with an illegal set
-- of inputs (e.g., in the case where the term cannot be seen in the
-- given specification)
adm _ _ _ _ _ = ( varᵀ (fvar 2000) , varᵀ (fvar 2000)   []) , record { curr = 2000 }

We emphasize again that this implementation is offered solely as proof-of-concept. There is lots of room for improvement, and we list some ideas below.

It would be nice to have a unified signature for variable sets, perhaps something along the lines of:

module variable-set where
  
  data Var : Set₁ where
    make-var-set :
       (name : String)  -- label
        (id : Set)  -- identifiers
        (fresh :  {k}  tuple id k  id)  -- fresh function
       Var

open variable-set public

Although we don’t use it in the algorithm as it is, we could envision having a term calculus over the type calculus described above:

data ⊨_∷_ : term-expr  type-expr {mix-var}  Set where
  ∗-intro : (    1ᵀ {var = mix-var})   ∗ᵗ  1ᵀ
  -- … 

In that way, we could actually force the inputs to adm to be legal in the sense described in the paper (beginning of section 3).

We could also improve space and time efficiency, by only pushing a newly encountered data type on the stack Gs if not already there (first operation done in the last case of adm above). In the examples that we handle here, it does not matter, since the recursive calls would not blow up the size of the stack (notice that the stack’s length never exceeds the depth of the tree of calls). But we could imagine running out of memory for this reason when dealing with complex examples.