{-

  Reflection-based tools for converting between iterated record types, particularly between
  record types and iterated Σ-types.

  The main functions are `declareRecordIsoΣ` and `declareRecordIsoΣ`. See end of file for
  example usage.

-}
{-# OPTIONS --no-exact-split --safe #-}
module Cubical.Reflection.RecordEquiv where

open import Cubical.Foundations.Prelude
open import Cubical.Foundations.Isomorphism
open import Cubical.Foundations.Equiv
open import Cubical.Data.List as List
open import Cubical.Data.Nat
open import Cubical.Data.Maybe as Maybe
open import Cubical.Data.Sigma

open import Agda.Builtin.String
import Agda.Builtin.Reflection as R
open import Cubical.Reflection.Base

-- Intended to represent a (possibly nested) field of a record type. For example, the list
-- "snd" ∷ fst" ∷ []` would represent the field `.fst .snd`.
Projections = List R.Name

-- Intended to represent a bijection between two record types by an association list
-- between (possibly nested) fields of the one and the other. The `Maybe`s are included to
-- allow dropping fields of Unit (or other definitionally unique) type.
--
-- For example, the correspondence
--
--   .fst .fst ↔ .snd
--   .fst .snd ↔ .fst
--   .snd ↔ ∅
--
-- between (A × B) × Unit and B × A would be represented by the list
--
--   [ (just ["fst"; "fst"] , just ["snd"])
--   ; (just ["snd"; "fst"] , just ["fst"])
--   ; (just ["snd"] , nothing)
--   ]
RecordAssoc = List (Maybe Projections × Maybe Projections)

-- Describes a correspondence between a non-nested record and an iterated Σ-type; more
-- convenient to work with than RecordAssoc in this special case, which is the most common
-- one. For example,
--
--   leaf "a" , (leaf "b" , leaf "c")
--
-- describes the following RecordAssoc:
--
--   .fst ↔ .a
--   .snd .fst ↔ .b
--   .snd .snd ↔ .c
--
-- The `unit` constructor is the correspondence between Unit ("nullary Σ") and the empty
-- record.
data ΣFormat : Type where
  leaf : R.Name  ΣFormat
  _,_ : ΣFormat  ΣFormat  ΣFormat
  unit : ΣFormat

infixr 4 _,_

-- Inverse of a correspondence between record types
flipRecordAssoc : RecordAssoc  RecordAssoc
flipRecordAssoc = List.map λ {p .fst  p .snd; p .snd  p .fst}

-- The identity correspondence on the domain of a given correspondence
fstIdRecordAssoc : RecordAssoc  RecordAssoc
fstIdRecordAssoc = List.map λ {p .fst  p .fst; p .snd  p .fst}

-- Constructs a ΣFormat from a list of fields meant to represent a right-associated Σ-type
List→ΣFormat : List R.Name  ΣFormat
List→ΣFormat [] = unit
List→ΣFormat (x  []) = leaf x
List→ΣFormat (x  y  xs) = leaf x , List→ΣFormat (y  xs)

-- Converts a ΣFormat to an association list as described above.
-- The domain of the RecordAssoc is the record type, the codomain is the Σ-type.
ΣFormat→RecordAssoc : ΣFormat  RecordAssoc
ΣFormat→RecordAssoc = go []
  where
  go : List R.Name  ΣFormat  RecordAssoc
  go prefix unit = [ nothing , just prefix ]
  go prefix (leaf fieldName) = [ just [ fieldName ] , just prefix ]
  go prefix (sig₁ , sig₂) =
    go (quote fst  prefix) sig₁ ++ go (quote snd  prefix) sig₂

-- Define a reflected type with the shape of the Σ-type described by a ΣFormat.
-- The type arguments to the Σ are filled in with unsolved metavariables.
ΣFormat→Ty : ΣFormat  R.Type
ΣFormat→Ty unit = R.def (quote Unit) []
ΣFormat→Ty (leaf _) = R.unknown
ΣFormat→Ty (sig₁ , sig₂) =
  R.def (quote Σ) (ΣFormat→Ty sig₁ v∷ R.lam R.visible (R.abs "_" (ΣFormat→Ty sig₂)) v∷ [])

-- Given the name of a record type and a ΣFormat describing an isomorphism between this
-- type and a Σ-type, constructs a reflected type of isomorphisms between the record and
-- Σ-type. If the record type takes parameters or indices, then the result is a similarly
-- parameterized family of isomorphisms. All parameters to the isomorphism are made
-- implicit.
recordName→isoTy : R.Name  ΣFormat  R.TC R.Term
recordName→isoTy name σ =
  R.inferType (R.def name []) >>= R.normalise >>= go []
  where
  σTy = ΣFormat→Ty σ

  -- Recurses on the type of the named record type
  go : List R.ArgInfo  R.Type  R.TC R.Term
  go acc (R.pi (R.arg i argTy) (R.abs s ty)) =
    -- If the record takes a parameter, the returned isomorphism is likewise parameterized
    liftTC  t  R.pi (R.arg i' argTy) (R.abs s t)) (go (i  acc) ty)
    where
    i' = R.arg-info R.hidden (R.modality R.relevant R.quantity-ω)
  go acc (R.agda-sort _) =
    -- Main case, constructs isomorphism type
    R.returnTC (R.def (quote Iso) (R.def name (makeArgs 0 [] acc) v∷ σTy v∷ []))
    where
    makeArgs :   List (R.Arg R.Term)  List R.ArgInfo  List (R.Arg R.Term)
    makeArgs n acc [] = acc
    makeArgs n acc (i  infos) = makeArgs (suc n) (R.arg i (v n)  acc) infos
  go _ _ = R.typeError (R.strErr "Not a record type name: "  R.nameErr name  [])

-- Given an association list `al` defining a correspondence between record types (say R
-- and S) and a `term` belonging to R, produces clauses defining an element of S, with
-- each field of S instantiated with a field of `term` according to `al`.  For example,
-- the correspondence
--
--   .fst ↔ .a
--   .snd .fst ↔ .b
--   .snd .snd ↔ ∅
--   ∅ ↔ .c
--
-- would produce the clauses
--
-- ... .a = term .fst
-- ... .b = term .snd
-- ... .c = _
--
-- Here the type of .c should have a definitionally unique element, so
-- we can safely fill it with an unsolved metavariable.
convertClauses : RecordAssoc  R.Term  List R.Clause
convertClauses al term = fixIfEmpty (List.filterMap makeClause al)
  where
  makeClause : Maybe Projections × Maybe Projections  Maybe R.Clause
  makeClause (projl , just projr) =
    just (R.clause [] (goPat [] projr) (Maybe.rec R.unknown goTm projl))
    where
    goPat : List (R.Arg R.Pattern)  List R.Name  List (R.Arg R.Pattern)
    goPat acc [] = acc
    goPat acc (π  projs) = goPat (varg (R.proj π)  acc) projs

    goTm : List R.Name  R.Term
    goTm [] = term
    goTm (π  projs) = R.def π [ varg (goTm projs) ]
  makeClause (_ , nothing) = nothing

  -- If there end up being zero clauses, then S should be a type with a definitionally
  -- unique element, so we return a single clause defined by an unsolved metavariable.
  fixIfEmpty : List R.Clause  List R.Clause
  fixIfEmpty [] = [ R.clause [] [] R.unknown ]
  fixIfEmpty (c  cs) = c  cs

-- Apply functions to the telescope and pattern parts of a clause
mapClause :
  (List (String × R.Arg R.Type)  List (String × R.Arg R.Type))
   (List (R.Arg R.Pattern)  List (R.Arg R.Pattern))
   (R.Clause  R.Clause)
mapClause f g (R.clause tel ps t) = R.clause (f tel) (g ps) t
mapClause f g (R.absurd-clause tel ps) = R.absurd-clause (f tel) (g ps)

-- Given a ΣFormat `σ` relating a record type R and Σ-type S, returns
-- a list of clauses defining an isomorphism between R and S.
recordIsoΣClauses : ΣFormat  List R.Clause
recordIsoΣClauses σ =
  funClauses (quote Iso.fun) R↔Σ ++
  funClauses (quote Iso.inv) Σ↔R ++
  -- Clauses for the forward and backward inverse conditions
  pathClauses (quote Iso.rightInv) Σ↔R ++
  pathClauses (quote Iso.leftInv) R↔Σ
  where
  R↔Σ = ΣFormat→RecordAssoc σ
  Σ↔R = flipRecordAssoc R↔Σ

  -- Given an association list `al` for a correspondence R ↔ S, produces the list of
  -- clauses defining a function from R to S.
  -- The `prefix` will be either `Iso.fun` or `Iso.inv`.
  funClauses : R.Name  RecordAssoc  List R.Clause
  funClauses prefix al =
    List.map
      -- Introduce a new variable of the input type
      (mapClause
        (("_" , varg R.unknown) ∷_)
         ps  R.proj prefix v∷ R.var 0 v∷ ps))
      -- Each clause is a projection applied to the input variable
      (convertClauses al (v 0))

  -- Given an association list `al` for a correspondence R ↔ S, produces the list of
  -- clauses defining the inverse path for the round-trip R → S → R.
  -- The `prefix` will be either `Iso.rightInv` or `Iso.leftInv`.
  pathClauses : R.Name  RecordAssoc  List R.Clause
  pathClauses prefix al =
    List.map
      -- Introduce a variable of the input type and an interval variable
      (mapClause
         vs  ("_" , varg R.unknown)  ("_" , varg (R.def (quote I) []))  vs)
         ps  R.proj prefix v∷ R.var 1 v∷ R.var 0 v∷ ps))
      -- The inverse condition at each clause holds by reflexivity
      (convertClauses (fstIdRecordAssoc al) (v 1))

------------------------------------------------------------------------------------------
-- Main functions
------------------------------------------------------------------------------------------

-- Given a ΣFormat describing a correspondence between a record and nested Σ-type,
-- constructs an isomorphism between them as a term.
--
-- Because this term is a large pattern-matching lambda, it is better to use the
-- declare* functions below, which declare a named function.
recordIsoΣTerm : ΣFormat  R.Term
recordIsoΣTerm σ = R.pat-lam (recordIsoΣClauses σ) []

-- Given a name `idName`, a ΣFormat `σ` describing a correspondence between a record and
-- nested Σ-type, and the name `recordName` of the record type, declares a function with
-- name `idName` defining an isomorphism between the two types (with implicit parameters
-- corresponding to the parameters and indices of the record type).
declareRecordIsoΣ' : R.Name  ΣFormat  R.Name  R.TC Unit
declareRecordIsoΣ' idName σ recordName =
  recordName→isoTy recordName σ >>= λ isoTy 
  R.declareDef (varg idName) isoTy >>
  R.defineFun idName (recordIsoΣClauses σ)

-- Given a name `idName` and the name `recordName` of a record type, declares a function
-- with name `idName` defining an isomorphism from the record type to the right-associated
-- Σ-type corresponding to its list of fields (with implicit parameters corresponding to
-- the parameters and indices of the record type).
declareRecordIsoΣ : R.Name  R.Name  R.TC Unit
declareRecordIsoΣ idName recordName =
  R.getDefinition recordName >>= λ where
  (R.record-type _ fs) 
    let σ = List→ΣFormat (List.map  {(R.arg _ n)  n}) fs) in
    declareRecordIsoΣ' idName σ recordName
  _ 
    R.typeError (R.strErr "Not a record type name:"  R.nameErr recordName  [])


------------------------------------------------------------------------------------------
-- Examples
------------------------------------------------------------------------------------------

private
  module Example where
    variable
       ℓ' : Level
      A : Type 
      B : A  Type ℓ'

    record Example0 {A : Type } (B : A  Type ℓ') : Type (ℓ-max  ℓ') where
      no-eta-equality -- works with or without eta equality
      field
        cool : A
        fun : A
        wow : B cool

    -- Declares a function `Example0IsoΣ` that gives an isomorphism between the record type and a
    -- right-associated nested Σ-type (with the parameters to Example0 as implict arguments).
    unquoteDecl Example0IsoΣ = declareRecordIsoΣ Example0IsoΣ (quote Example0)

    -- `Example0IsoΣ` has the type we expect
    test0 : Iso (Example0 B) (Σ[ a  A ] (Σ[ _  A ] B a))
    test0 = Example0IsoΣ

    -- A record with no fields is isomorphic to Unit

    record Example1 : Type where

    unquoteDecl Example1IsoΣ = declareRecordIsoΣ Example1IsoΣ (quote Example1)

    test1 : Iso Example1 Unit
    test1 = Example1IsoΣ