packages feed

rzk-0.11.1: src/Rzk/TypeCheck/Decl/Data.hs

{-# OPTIONS_GHC -fno-warn-name-shadowing #-}
{-# LANGUAGE LambdaCase        #-}
{-# LANGUAGE OverloadedStrings #-}

-- | The surface-syntax scaffolding of @#data@ declarations, kept separate from
-- the monadic checker in "Rzk.TypeCheck.Decl".
--
-- Two things live here, both pure. First, small builders over the surface
-- syntax (@surfaceApps@, @surfacePi@, …) and the preprocessed forms of a
-- declaration (the 'SortIndex' telescope and the 'DataConSurface'
-- constructors). Second, the construction of the generated eliminators: given
-- an 'ElimSpec' — the declaration's data together with the fresh binders of the
-- generated types and the validated endpoint images — 'elimTerms' produces the
-- surface types of @ind-D@ and @rec-D@ and the path computation rules. The
-- checker then pushes those surface terms through the ordinary elaborator, so
-- nothing here constructs core terms or touches the type-checking monad.
module Rzk.TypeCheck.Decl.Data where

import           Data.Data          (Data, cast, gmapQ)
import qualified Data.Text          as T

import qualified Language.Rzk.Syntax as Rzk
import           Rzk.TypeCheck.Display (panicImpossible)

-- * Surface-syntax builders and preprocessed forms

addParamDecls :: [Rzk.ParamDecl] -> Rzk.Term -> Rzk.Term
addParamDecls [] = id
addParamDecls (paramDecl : paramDecls)
  = Rzk.TypeFun Nothing paramDecl . addParamDecls paramDecls

-- | One index of a family, as declared in the sort: its binder (when the
-- sort names it, as in @(n : nat) → U@) and its type.
data SortIndex = SortIndex
  { sortIndexVar  :: Maybe Rzk.VarIdent
  , sortIndexType :: Rzk.Term
  }

-- | The sort of a constructor: a point constructor returns the declared
-- type; a path constructor returns an identity type over it (spelled
-- @l =_{D …} r@), declaring an identification between the two endpoint
-- terms. Path β-rules are propositional (the generated @compute-@ lemmas),
-- following the HoTT-book treatment of higher inductive types.
data DataConSort
  = DataConPoint
  | DataConPath Rzk.Term Rzk.Term
    -- ^ the endpoints of the declared identification, as written

-- | A constructor, preprocessed at the surface level.
data DataConSurface = DataConSurface
  { dataConName       :: Rzk.VarIdent
  , dataConFields     :: [Rzk.ParamDecl]
    -- ^ the field telescope, one entry per bound variable
  , dataConFieldPats  :: [Rzk.Term]
    -- ^ the field patterns as terms, in the same order
  , dataConRecursive  :: [(Int, [Rzk.Term])]
    -- ^ the directly recursive fields (their type is the declared type
    -- applied to its parameters and some index terms): 0-based position
    -- and the index terms; each contributes an induction hypothesis to
    -- the eliminator's method
  , dataConRetIndices :: [Rzk.Term]
    -- ^ the index terms of the constructor's return type
  , dataConType       :: Rzk.Term
    -- ^ the constructor's full surface type: params → fields → D params
  , dataConProbe      :: Rzk.Term
    -- ^ the non-recursive fields → Unit, for the positivity and largeness
    -- checks (a directly recursive field mentions the type legitimately)
  , dataConNonRec     :: [Rzk.Term]
    -- ^ the non-recursive field types, for the per-field error message
  , dataConLocalNames :: [Rzk.VarIdentToken]
    -- ^ every binder token of the constructor, for freshening
  , dataConSort       :: DataConSort
  }

surfacePatternVars :: Rzk.Pattern -> [Rzk.VarIdent]
surfacePatternVars = \case
  Rzk.PatternVar _ v        -> [v]
  Rzk.PatternPair _ a b     -> surfacePatternVars a <> surfacePatternVars b
  Rzk.PatternTuple _ a b cs -> concatMap surfacePatternVars (a : b : cs)
  Rzk.PatternUnit _         -> []

identTokenOf :: Rzk.VarIdent -> Rzk.VarIdentToken
identTokenOf (Rzk.VarIdent _ tok) = tok

surfaceVar :: Rzk.VarIdent -> Rzk.Term
surfaceVar = Rzk.Var Nothing

-- | A λ over plain (untyped) variable patterns; only ever used in checking
-- position, where the domains come from the expected type.
surfaceLambda :: [Rzk.VarIdent] -> Rzk.Term -> Rzk.Term
surfaceLambda vs = Rzk.Lambda Nothing
  [ Rzk.ParamPattern Nothing (Rzk.PatternVar Nothing v) | v <- vs ]

-- | The wildcard binder, for a generated λ that ignores an argument.
underscoreIdent :: Rzk.VarIdent
underscoreIdent = Rzk.VarIdent Nothing (Rzk.VarIdentToken "_")

-- | Every variable-occurrence token in a surface term, collected
-- generically. Used for the endpoint well-formedness check of path
-- constructors, which is deliberately syntactic.
surfaceVarTokens :: Data a => a -> [Rzk.VarIdentToken]
surfaceVarTokens x = concat
  [ maybe [] varTok (cast x)
  , concat (gmapQ surfaceVarTokens x)
  ]
  where
    varTok :: Rzk.Term -> [Rzk.VarIdentToken]
    varTok = \case
      Rzk.Var _ v -> [identTokenOf v]
      _           -> []

surfaceApps :: Rzk.Term -> [Rzk.Term] -> Rzk.Term
surfaceApps f []       = f
surfaceApps f (x : xs) = surfaceApps (Rzk.App Nothing f x) xs

surfaceAppSpine :: Rzk.Term -> (Rzk.Term, [Rzk.Term])
surfaceAppSpine = go []
  where
    go acc (Rzk.App _ f x) = go (x : acc) f
    go acc t               = (t, acc)

surfaceArrow :: Rzk.Term -> Rzk.Term -> Rzk.Term
surfaceArrow a = Rzk.TypeFun Nothing (Rzk.ParamType Nothing a)

-- | The domains and codomain of a surface Π-chain (domain types only; for
-- the shaped and modal parameter forms the carrier is what matters here).
surfacePiSpine :: Rzk.Term -> ([Rzk.Term], Rzk.Term)
surfacePiSpine = go []
  where
    go acc (Rzk.TypeFun _ param ret)       = go (domainsOf param <> acc) ret
    go acc (Rzk.ASCII_TypeFun _ param ret) = go (domainsOf param <> acc) ret
    go acc t                               = (reverse acc, t)
    domainsOf = \case
      Rzk.ParamType _ t                 -> [t]
      Rzk.ParamTermType _ _ t           -> [t]
      Rzk.ParamTermShape _ _ cube _     -> [cube]
      Rzk.ParamTermModalType _ _ _ t    -> [t]
      Rzk.ParamTermModalShape _ _ _ c _ -> [c]

-- | Is the term the given name applied to exactly the given parameter
-- variables (in order, the uniformity requirement) and then exactly
-- @arity@ index terms? Returns those index terms. A syntactic check: this
-- is how constructor return types and directly recursive fields are
-- recognised.
dataAppliedIndices :: Rzk.VarIdent -> [Rzk.VarIdent] -> Int -> Rzk.Term -> Maybe [Rzk.Term]
dataAppliedIndices dataName paramVars arity t = case surfaceAppSpine t of
  (Rzk.Var _ h, args)
    | identTokenOf h == identTokenOf dataName
    , (paramArgs, indexArgs) <- splitAt (length paramVars) args
    , map (Just . identTokenOf) paramVars == map varTokenOf paramArgs
    , length indexArgs == arity
    -> Just indexArgs
  _ -> Nothing
  where
    varTokenOf (Rzk.Var _ v) = Just (identTokenOf v)
    varTokenOf _             = Nothing

matchesDataApplied :: Rzk.VarIdent -> [Rzk.VarIdent] -> Int -> Rzk.Term -> Bool
matchesDataApplied dataName paramVars arity =
  maybe False (const True) . dataAppliedIndices dataName paramVars arity

surfacePi :: Rzk.VarIdent -> Rzk.Term -> Rzk.Term -> Rzk.Term
surfacePi v ty = Rzk.TypeFun Nothing (Rzk.ParamTermType Nothing (surfaceVar v) ty)

prefixedIdent :: T.Text -> Rzk.VarIdent -> Rzk.VarIdent
prefixedIdent p (Rzk.VarIdent pos (Rzk.VarIdentToken t)) =
  Rzk.VarIdent pos (Rzk.VarIdentToken (p <> t))

-- | Index into a list that is long enough by construction, panicking with a
-- label instead of the opaque @Prelude.!!@ message if that invariant is ever
-- broken. Used where an index is derived from the same data as the list (the
-- induction-hypothesis binders, a constructor's field patterns).
nthByConstruction :: String -> [a] -> Int -> a
nthByConstruction what xs i = case drop i xs of
  x : _ -> x
  []    -> panicImpossible (what <> ": index " <> show i <> " out of range")

-- | The 0-based positions of a constructor's directly recursive fields.
recPositionsOf :: DataConSurface -> [Int]
recPositionsOf = map fst . dataConRecursive

-- * Eliminator construction

-- | Everything the pure construction of the eliminators closes over: the
-- declaration's name, its parameters and index telescope, the fresh binders of
-- the generated types (the motive, the scrutinee, the induction hypotheses,
-- the path-method names, and the binders of the inlined transport), the
-- preprocessed constructors, and — for each path constructor — the images of
-- its endpoints under the section being defined. Assembled by
-- "Rzk.TypeCheck.Decl" once the fresh names are allocated and the endpoints
-- validated.
data ElimSpec = ElimSpec
  { esName       :: Rzk.VarIdent
  , esParamVars  :: [Rzk.VarIdent]
  , esParamDecls :: [Rzk.ParamDecl]
  , esIndexVars  :: [Rzk.VarIdent]
  , esIndexDecls :: [Rzk.ParamDecl]
  , esMotiveV    :: Rzk.VarIdent
  , esScrutV     :: Rzk.VarIdent
  , esIhNames    :: [Rzk.VarIdent]
  , esMethodVars :: Maybe [Rzk.VarIdent]
  , esEndpointV  :: Rzk.VarIdent
  , esPathV      :: Rzk.VarIdent
  , esTransportV :: Rzk.VarIdent
  , esConsData   :: [DataConSurface]
  , esPathData   :: [Maybe ((Rzk.Term, Rzk.Term), (Rzk.Term, Rzk.Term))]
  }

-- | The generated surface types of a declaration's eliminators and the path
-- computation rules, all as surface terms for the ordinary elaborator.
data ElimTerms = ElimTerms
  { indTypeTerm  :: Rzk.Term
    -- ^ the type of @ind-D@ (the dependent eliminator)
  , recTypeTerm  :: Rzk.Term
    -- ^ the type of @rec-D@ (the non-dependent eliminator)
  , computeRules :: [(Rzk.VarIdent, Rzk.Term)]
    -- ^ one @compute-ind-@/@compute-rec-@ lemma per path constructor
  }

elimTerms :: ElimSpec -> ElimTerms
elimTerms spec = ElimTerms
    { indTypeTerm  = elimTy True
    , recTypeTerm  = elimTy False
    , computeRules = computes
    }
  where
    ElimSpec
      { esName = name, esParamVars = paramVars, esParamDecls = paramDecls
      , esIndexVars = indexVars, esIndexDecls = indexDecls, esMotiveV = motiveV
      , esScrutV = scrutV, esIhNames = ihNames, esMethodVars = methodVars
      , esEndpointV = endpointV, esPathV = pathV, esTransportV = transportV
      , esConsData = consData, esPathData = pathData } = spec
    dApplied = surfaceApps (surfaceVar name) (map surfaceVar paramVars)
    dAppliedIx = surfaceApps dApplied (map surfaceVar indexVars)
    motive = surfaceVar motiveV
    -- The motive abstracts over the indices (and, dependently, the
    -- scrutinee); a method's hypotheses and codomain instantiate it
    -- at the relevant index terms.
    motiveSort dependent = addParamDecls indexDecls $
      if dependent
        then surfaceArrow dAppliedIx (Rzk.Universe Nothing)
        else Rzk.Universe Nothing
    -- The constructor applied to the parameters and its own fields
    -- (for a path constructor, this is the declared identification).
    conApplied con = surfaceApps (surfaceVar (dataConName con))
      (map surfaceVar paramVars <> dataConFieldPats con)
    -- @transport@ in the motive along a path @p : l = rEnd@, spelled
    -- through idJ (rzk has no primitive transport):
    -- @idJ (D …, l, \y' _ → C l → C y', \y' → y', rEnd, p) u@.
    transportAlong l rEnd p u =
      Rzk.App Nothing
        (Rzk.IdJ Nothing dApplied l
          (surfaceLambda [transportV, underscoreIdent]
            (surfaceArrow (Rzk.App Nothing motive l)
              (Rzk.App Nothing motive (surfaceVar transportV))))
          (surfaceLambda [transportV] (surfaceVar transportV))
          rEnd p)
        u
    -- One method per constructor: its fields with an induction
    -- hypothesis interleaved after each recursive field (HoTT-book
    -- style). A point method then ends in the motive at the
    -- constructor's return indices (and, for @ind-D@, at the
    -- constructor applied to parameters and fields); a path method
    -- ends in an equation between the images of the endpoints, over
    -- the path for @ind-D@ (β on a path constructor stays
    -- propositional: the equation is the type of the generated
    -- @compute-@ lemma, not a rule the checker computes with).
    methodTy dependent (con, mpath) = wrapFields (0 :: Int)
      (zip [0 :: Int ..] (zip (dataConFields con) (dataConFieldPats con)))
      where
        wrapFields _ [] = case mpath of
          Nothing -> surfaceApps motive $
            dataConRetIndices con <> [ conApplied con | dependent ]
          Just ((l, r), (iL, iR))
            | dependent -> Rzk.TypeIdSimple Nothing
                (transportAlong l r (conApplied con) iL) iR
            | otherwise -> Rzk.TypeIdSimple Nothing iL iR
        wrapFields nRec ((j, (fieldDecl, fpat)) : more)
          | Just fieldIxs <- lookup j (dataConRecursive con) =
              Rzk.TypeFun Nothing fieldDecl $
                surfacePi (nthByConstruction "induction hypotheses" ihNames nRec)
                  (surfaceApps motive (fieldIxs <> [ fpat | dependent ]))
                  (wrapFields (nRec + 1) more)
          | otherwise =
              Rzk.TypeFun Nothing fieldDecl (wrapFields nRec more)
    elimTail dependent = addParamDecls indexDecls $
      if dependent
        then surfacePi scrutV dAppliedIx $
          surfaceApps motive (map surfaceVar indexVars <> [surfaceVar scrutV])
        else surfaceArrow dAppliedIx $
          surfaceApps motive (map surfaceVar indexVars)
    conWithPath = zip consData pathData
    methodsPis dependent inner = case methodVars of
      Nothing -> foldr (surfaceArrow . methodTy dependent) inner conWithPath
      Just ms -> foldr
        (\(m, cp) rest -> surfacePi m (methodTy dependent cp) rest)
        inner (zip ms conWithPath)
    elimTy dependent = addParamDecls paramDecls $
      surfacePi motiveV (motiveSort dependent) $
        methodsPis dependent (elimTail dependent)
    indName = prefixedIdent "ind-" name
    recName = prefixedIdent "rec-" name
    -- The propositional β-lemma per path constructor and eliminator:
    -- the section's action on the constructor's path (ap/apd, spelled
    -- through idJ) equals the path method at the fields, with the
    -- section applied to each recursive field as its hypothesis.
    -- Generated as opaque entries, like the eliminators themselves.
    sectionOf elimIdent = surfaceApps (surfaceVar elimIdent)
      (map surfaceVar paramVars <> [motive]
        <> maybe [] (map surfaceVar) methodVars)
    methodArgs f con = concat
      [ fpat : [ Rzk.App Nothing f fpat | j `elem` recPositionsOf con ]
      | (j, fpat) <- zip [0 :: Int ..] (dataConFieldPats con) ]
    computeTy dependent elimIdent m con l r =
      addParamDecls paramDecls $
        surfacePi motiveV (motiveSort dependent) $
          methodsPis dependent $
            addParamDecls (dataConFields con) $
              let f = sectionOf elimIdent
                  fAt = Rzk.App Nothing f
                  -- apd (dependent) or ap: the motive of the outer
                  -- idJ states what the section does to a path
                  motiveBody
                    | dependent = Rzk.TypeIdSimple Nothing
                        (transportAlong l (surfaceVar endpointV)
                          (surfaceVar pathV) (fAt l))
                        (fAt (surfaceVar endpointV))
                    | otherwise = Rzk.TypeIdSimple Nothing
                        (fAt l) (fAt (surfaceVar endpointV))
                  lhs = Rzk.IdJ Nothing dApplied l
                    (surfaceLambda [endpointV, pathV] motiveBody)
                    (Rzk.Refl Nothing) r (conApplied con)
                  rhs = surfaceApps (surfaceVar m) (methodArgs f con)
               in Rzk.TypeIdSimple Nothing lhs rhs
    computeNameFor pfx con =
      let Rzk.VarIdent pos (Rzk.VarIdentToken d) = name
          Rzk.VarIdentToken c = identTokenOf (dataConName con)
       in Rzk.VarIdent pos (Rzk.VarIdentToken (pfx <> d <> "-" <> c))
    computes = case methodVars of
      Nothing -> []
      Just ms ->
        [ entry
        | (m, (con, Just ((l, r), _))) <- zip ms conWithPath
        , entry <-
            [ ( computeNameFor "compute-ind-" con
              , computeTy True indName m con l r )
            , ( computeNameFor "compute-rec-" con
              , computeTy False recName m con l r ) ] ]