packages feed

covenant-1.2.0: src/Covenant/Data.hs

{-# LANGUAGE ViewPatterns #-}

-- |
-- Module: Covenant.Data
-- Copyright: (C) MLabs 2025
-- License: Apache 2.0
-- Maintainer: koz@mlabs.city, sean@mlabs.city
--
-- Information about datatype definitions, and various ways to interact with
-- them. Most of the useful functionality is in 'DatatypeInfo' and its optics.
--
-- = Note
--
-- Some of the low-level functions in the module make use of @ScopeBoundary@.
-- This is mostly an artifact of needing this for tests; if you ever need their
-- functionality, assume that the only sensible value is @0@, which will work
-- via its overloaded number syntax.
--
-- @since 1.1.0
module Covenant.Data
  ( -- * Types
    BBFError (..),
    DatatypeInfo (..),

    -- * Functions

    -- ** Datatype-related
    mkDatatypeInfo,
    allComponentTypes,
    mkBBF,
    noPhantomTyVars,

    -- ** Lower-level
    mkBaseFunctor,
    isRecursiveChildOf,
    hasRecursive,
    everythingOf,
    mapValT,
  )
where

import Control.Monad.Except (MonadError (throwError))
import Control.Monad.Reader (MonadReader (ask, local), MonadTrans (lift), Reader, runReader)
import Control.Monad.Trans.Except (ExceptT, runExceptT)
import Covenant.DeBruijn (DeBruijn (S, Z), asInt)
import Covenant.Index (Count, Index, count0, intCount, intIndex)
import Covenant.Internal.PrettyPrint (ScopeBoundary (ScopeBoundary))
import Covenant.Internal.Type
  ( AbstractTy (BoundAt),
    CompT (CompT),
    CompTBody (CompTBody),
    Constructor (Constructor),
    ConstructorName (ConstructorName),
    DataDeclaration (DataDeclaration, OpaqueData),
    TyName (TyName),
    ValT (Abstraction, BuiltinFlat, Datatype, ThunkT),
  )
import Data.Bitraversable (bisequence)
import Data.Kind (Type)
import Data.Maybe (fromJust)
import Data.Set (Set)
import Data.Set qualified as Set
import Data.Vector qualified as V
import Data.Vector.NonEmpty qualified as NEV
import Optics.Core (A_Lens, LabelOptic (labelOptic), folded, lens, preview, review, toListOf, view, (%), _2)
import Optics.Indexed.Core (A_Fold)

-- | All possible errors that could arise when constructing a Boehm-Berrarducci
-- form.
--
-- @since 1.1.0
data BBFError
  = -- | The type is recursive in a prohibited way. Typically, this means
    -- contravariant recursion. This gives the type name and the invalid
    -- recursive constructor argument.
    --
    -- @since 1.1.0
    InvalidRecursion TyName (ValT AbstractTy)
  deriving stock
    ( -- | @since 1.1.0
      Show,
      -- | @since 1.1.0
      Eq
    )

-- | Contains essential information about datatype definitions. Most of the
-- time, you want to use this type via its optics, rather than directly.
--
-- In pretty much any case imaginable, the @var@ type variable will be one of
-- 'AbstractTy' or 'Renamed'.
--
-- @since 1.1.0
data DatatypeInfo (var :: Type)
  = DatatypeInfo
  { _originalDecl :: DataDeclaration var,
    _baseFunctorStuff :: Maybe (DataDeclaration var, ValT var),
    -- NOTE: The ONLY type that won't have a BB form is `Void` (or something isomorphic to it)
    _bbForm :: Maybe (ValT var)
  }
  deriving stock
    ( -- | @since 1.1.0
      Eq,
      -- | @since 1.1.0
      Show
    )

-- | The original declaration of the data type.
--
-- @since 1.1.0
instance
  (k ~ A_Lens, a ~ DataDeclaration var, b ~ DataDeclaration var) =>
  LabelOptic "originalDecl" k (DatatypeInfo var) (DatatypeInfo var) a b
  where
  {-# INLINEABLE labelOptic #-}
  labelOptic =
    lens
      (\(DatatypeInfo ogDecl _ _) -> ogDecl)
      (\(DatatypeInfo _ b c) ogDecl -> DatatypeInfo ogDecl b c)

-- | The base functor for this data type, if it exists. Types which are not
-- self-recursive lack base functors.
--
-- @since 1.1.0
instance
  (k ~ A_Lens, a ~ Maybe (DataDeclaration var, ValT var), b ~ Maybe (DataDeclaration var, ValT var)) =>
  LabelOptic "baseFunctor" k (DatatypeInfo var) (DatatypeInfo var) a b
  where
  {-# INLINEABLE labelOptic #-}
  labelOptic =
    lens
      (\(DatatypeInfo _ baseF _) -> baseF)
      (\(DatatypeInfo a _ c) baseF -> DatatypeInfo a baseF c)

-- | The Boehm-Berrarducci form of this type, if it exists. Types with no
-- constructors (that is, types without inhabitants) lack Boehm-Berrarducci
-- forms.
--
-- @since 1.1.0
instance
  (k ~ A_Lens, a ~ Maybe (ValT var), b ~ Maybe (ValT var)) =>
  LabelOptic "bbForm" k (DatatypeInfo var) (DatatypeInfo var) a b
  where
  {-# INLINEABLE labelOptic #-}
  labelOptic =
    lens
      (\(DatatypeInfo _ _ bb) -> bb)
      (\(DatatypeInfo a b _) bb -> DatatypeInfo a b bb)

-- | The base functor Boehm-Berrarducci form of this type, if it exists. A type
-- must have both a base functor and a Boehm-Berrarducci form to have a base
-- functor Boehm-Berrarducci form. In other words, they must have at least one
-- constructor and be self-recursive.
--
-- @since 1.1.0
instance
  (k ~ A_Fold, a ~ ValT var, b ~ ValT var) =>
  LabelOptic "bbBaseF" k (DatatypeInfo var) (DatatypeInfo var) a b
  where
  {-# INLINEABLE labelOptic #-}
  labelOptic = #baseFunctor % folded % _2

-- | Given a declaration of a datatype, either produce its datatype info, or
-- fail.
--
-- @since 1.1.0
mkDatatypeInfo :: DataDeclaration AbstractTy -> Either BBFError (DatatypeInfo AbstractTy)
mkDatatypeInfo decl = DatatypeInfo decl <$> baseFStuff <*> mkBBF decl
  where
    baseFStuff :: Either BBFError (Maybe (DataDeclaration AbstractTy, ValT AbstractTy))
    baseFStuff =
      let baseFDecl = runReader (mkBaseFunctor decl) 0
          baseBBF = case baseFDecl of
            Nothing -> Right Nothing
            Just d -> mkBBF d
       in (bisequence . (baseFDecl,) <$> baseBBF)

-- | Returns all datatype constructors used as any argument to the datatype
-- defined by the first argument.
--
-- @since 1.1.0
allComponentTypes :: DataDeclaration AbstractTy -> [ValT AbstractTy]
allComponentTypes = toListOf (#datatypeConstructors % folded % #constructorArgs % folded)

-- | Constructs a base functor from a suitable data declaration, returning
-- 'Nothing' if the input is not a recursive type.
--
-- @since 1.1.0
mkBaseFunctor :: DataDeclaration AbstractTy -> Reader ScopeBoundary (Maybe (DataDeclaration AbstractTy))
mkBaseFunctor OpaqueData {} = pure Nothing
mkBaseFunctor (DataDeclaration tn numVars ctors strat) = do
  anyRecComponents <- or <$> traverse (hasRecursive tn) allCtorArgs
  if null ctors || not anyRecComponents
    then pure Nothing
    else do
      baseCtors <- traverse mkBaseCtor ctors
      pure . Just $ DataDeclaration baseFName baseFNumVars baseCtors strat
  where
    baseFName :: TyName
    baseFName = case tn of
      TyName tyNameInner -> TyName (tyNameInner <> "_F")
    baseFNumVars :: Count "tyvar"
    baseFNumVars = fromJust . preview intCount $ review intCount numVars + 1
    -- The argument position of the new type variable parameter (typically `r`).
    -- A count represents the number of variables, but indices for those variables start at 0,
    -- so an additional tyvar will always have an index == the old count
    rIndex :: Index "tyvar"
    rIndex = fromJust . preview intIndex $ review intCount numVars
    -- Replace recursive children with a DeBruijn index & position index that points at the top-level binding context
    -- (technically the top level binding context is the ONLY admissable binding context if we forbid higher-rank types,
    -- but we still have to regard a computation type that binds 0 variables as having a scope boundary)
    replaceWithR :: ValT AbstractTy -> Reader ScopeBoundary (ValT AbstractTy)
    replaceWithR vt =
      isRecursive vt >>= \case
        True -> do
          ScopeBoundary here <- ask -- this should be the distance from the initial binding context (which is what we want)
          let db = fromJust $ preview asInt here
          pure $ Abstraction (BoundAt db rIndex)
        False -> pure vt
    -- TODO: This should be refactored with `mapMValT`, which I will do after I write it :P
    replaceAllRecursive :: ValT AbstractTy -> Reader ScopeBoundary (ValT AbstractTy)
    replaceAllRecursive = \case
      abst@Abstraction {} -> pure abst
      bif@BuiltinFlat {} -> pure bif
      ThunkT (CompT cnt (CompTBody compTargs)) ->
        local (+ 1) $ ThunkT . CompT cnt . CompTBody <$> traverse replaceAllRecursive compTargs
      Datatype tx args -> (replaceWithR . Datatype tx =<< traverse replaceAllRecursive args)
    mkBaseCtor :: Constructor AbstractTy -> Reader ScopeBoundary (Constructor AbstractTy)
    mkBaseCtor (Constructor ctorNm ctorArgs) = Constructor (baseFCtorName ctorNm) <$> traverse replaceAllRecursive ctorArgs
      where
        baseFCtorName :: ConstructorName -> ConstructorName
        baseFCtorName (ConstructorName nm) = ConstructorName (nm <> "_F")
    allCtorArgs :: [ValT AbstractTy]
    allCtorArgs = concatMap (V.toList . view #constructorArgs) ctors
    -- This tells us whether the ValT *is* a recursive child of the parent type
    isRecursive :: ValT AbstractTy -> Reader ScopeBoundary Bool
    isRecursive = isRecursiveChildOf tn

-- | Returns 'True' if the second argument is a recursive child of the datatype
-- named by the first argument.
--
-- @since 1.1.0
isRecursiveChildOf :: TyName -> ValT AbstractTy -> Reader ScopeBoundary Bool
isRecursiveChildOf tn = \case
  Datatype tn' args
    | tn' == tn -> V.ifoldM checkArgsIsRec' True args
    | otherwise -> pure False
  _ -> pure False
  where
    checkArgsIsRec' :: Bool -> Int -> ValT AbstractTy -> Reader ScopeBoundary Bool
    checkArgsIsRec' acc n = \case
      Abstraction (BoundAt db varIx) -> do
        ScopeBoundary here <- ask
        let dbInt = review asInt db
        -- Explanation: A component ValT is only a recursive instance of the parent type if
        --              the DeBruijn index of its type variables points to Z (and the other conditions obtain)
        if dbInt - here == 0 && review intIndex varIx == n
          then pure acc
          else pure False
      _ -> pure False

-- | Determines whether the type represented by the second argument and named by
-- the first requires a base functor.
--
-- @since 1.1.0
hasRecursive :: TyName -> ValT AbstractTy -> Reader ScopeBoundary Bool
hasRecursive tn = \case
  Abstraction {} -> pure False
  BuiltinFlat {} -> pure False
  -- NOTE: This assumes that we've forbidden higher rank arguments to constructors (i.e. we can ignore the scope here)
  ThunkT (CompT _ (CompTBody (NEV.toList -> compTArgs))) -> local (+ 1) $ do
    or <$> traverse (hasRecursive tn) compTArgs
  dt@(Datatype _ args) -> do
    thisTypeIsRecursive <- isRecursiveChildOf tn dt
    aComponentIsRecursive <- or <$> traverse (hasRecursive tn) args
    pure $ thisTypeIsRecursive || aComponentIsRecursive

-- | Constructs a base functor Boehm-Berrarducci form for the given datatype.
-- Returns 'Nothing' if the type is not self-recursive.
--
-- @since 1.1.0
mkBBF :: DataDeclaration AbstractTy -> Either BBFError (Maybe (ValT AbstractTy))
mkBBF decl = sequence . runExceptT $ mkBBF' decl

-- | Verifies that all type variables declared by the given datatype have a
-- corresponding value in some \'arm\'.
--
-- @since 1.1.0
noPhantomTyVars :: DataDeclaration AbstractTy -> Bool
noPhantomTyVars OpaqueData {} = True
noPhantomTyVars decl@(DataDeclaration _ numVars _ _) =
  let allChildren = allComponentTypes decl
      allResolved = Set.unions $ runReader (traverse allResolvedTyVars' allChildren) 0
      indices :: [Index "tyvar"]
      indices = fromJust . preview intIndex <$> [0 .. (review intCount numVars - 1)]
      declaredTyVars = BoundAt Z <$> indices
   in all (`Set.member` allResolved) declaredTyVars

-- | Collect all (other) value types a given value type refers to.
--
-- @since 1.1.0
everythingOf :: forall (a :: Type). (Ord a) => ValT a -> Set (ValT a)
everythingOf = foldValT (flip Set.insert) Set.empty

-- Helpers

{- NOTE: For the purposes of base functor transformation, we follow the pattern established by Edward Kmett's
         'recursion-schemes' library. That is, we regard a datatype as "recursive" if and only if at least one
         argument to a constructor contains "the exact same thing as we find to the left of the =". Dunno how to
         describe it more precisely, but the general idea is that things like these ARE recursive for us:

           data Foo = End Int | More Foo Int -- contains 'Foo' as a ctor arg

           data Bar a = Beep | Boom a (Bar a) -- contains 'Bar a'

         but things like this are NOT recursive by our reckoning (even though in some sense they might be considered as such):

           data FunL a b = Done b | Go (FunL b a) a -- `FunL b a` isn't `FunL a b` so it's not literally recursive

         Obviously we're working with DeBruijn indices so the letters are more-or-less fictitious, but hopefully
         these examples nonetheless get the point across.
-}

-- TODO: Rewrite this as `mapMValT`. The change to a `Reader` below makes this unusable, but we can
--       write the non-monadic version as a special case of the monadic version and it is *highly* likely
--       we will need both going forward.
mapValT :: forall (a :: Type). (ValT a -> ValT a) -> ValT a -> ValT a
mapValT f = \case
  -- for terminal nodes we just apply the function
  absr@(Abstraction {}) -> f absr
  bif@BuiltinFlat {} -> f bif
  -- For CompT and Datatype we apply the function to the components and then to the top level
  ThunkT (CompT cnt (CompTBody compTargs)) -> f (ThunkT $ CompT cnt (CompTBody (mapValT f <$> compTargs)))
  Datatype tn args -> f $ Datatype tn (mapValT f <$> args)

-- Did in fact need it
foldValT :: forall (a :: Type) (b :: Type). (b -> ValT a -> b) -> b -> ValT a -> b
foldValT f e = \case
  absr@(Abstraction {}) -> f e absr
  bif@(BuiltinFlat {}) -> f e bif
  thk@(ThunkT (CompT _ (CompTBody compTArgs))) ->
    let e' = NEV.foldl' f e compTArgs
     in f e' thk
  dt@(Datatype _ args) ->
    let e' = V.foldl' f e args
     in f e' dt

allResolvedTyVars' :: ValT AbstractTy -> Reader Int (Set AbstractTy)
allResolvedTyVars' = \case
  Abstraction (BoundAt db argpos) -> do
    here <- ask
    let db' = fromJust . preview asInt $ review asInt db - here
    pure . Set.singleton $ BoundAt db' argpos
  ThunkT (CompT _ (CompTBody nev)) -> local (+ 1) $ do
    Set.unions <$> traverse allResolvedTyVars' nev
  BuiltinFlat {} -> pure Set.empty
  Datatype _ args -> Set.unions <$> traverse allResolvedTyVars' args

incAbstractionDB :: ValT AbstractTy -> ValT AbstractTy
incAbstractionDB = mapValT $ \case
  Abstraction (BoundAt db indx) ->
    let db' = fromJust . preview asInt $ review asInt db + 1
     in Abstraction (BoundAt db' indx)
  other -> other

-- Only returns `Nothing` if there are no Constructors or the type is Opaque
mkBBF' :: DataDeclaration AbstractTy -> ExceptT BBFError Maybe (ValT AbstractTy)
mkBBF' OpaqueData {} = lift Nothing
mkBBF' (DataDeclaration tn numVars ctors _)
  | V.null ctors = lift Nothing
  | otherwise = do
      ctors' <- traverse mkBBCtor ctors
      lift $ ThunkT . CompT bbfCount . CompTBody . flip NEV.snoc topLevelOut <$> NEV.fromVector ctors'
  where
    topLevelOut = Abstraction $ BoundAt Z outIx

    outIx :: Index "tyvar"
    outIx = fromJust . preview intIndex $ review intCount numVars

    bbfCount = fromJust . preview intCount $ review intCount numVars + 1

    mkBBCtor :: Constructor AbstractTy -> ExceptT BBFError Maybe (ValT AbstractTy)
    mkBBCtor (Constructor _ args)
      | V.null args = pure topLevelOut
      | otherwise = do
          elimArgs <- fmap incAbstractionDB <$> traverse fixArg args
          elimArgs' <- lift . NEV.fromVector $ elimArgs
          let out = Abstraction $ BoundAt (S Z) outIx
          pure . ThunkT . CompT count0 . CompTBody . flip NEV.snoc out $ elimArgs'

    fixArg :: ValT AbstractTy -> ExceptT BBFError Maybe (ValT AbstractTy)
    fixArg arg = do
      let isDirectRecursiveTy = runReader (isRecursiveChildOf tn arg) 0
      if isDirectRecursiveTy
        then pure $ Abstraction (BoundAt Z outIx)
        else case arg of
          Datatype tn' dtArgs
            | tn == tn' -> throwError $ InvalidRecursion tn arg
            | otherwise -> do
                dtArgs' <- traverse fixArg dtArgs
                pure . Datatype tn' $ dtArgs'
          _ -> pure arg

{- Note (Sean, 14/05/25): Re  DeBruijn indices:

     - None of the existing variable DeBruijn or position indices change at all b/c the binding context of the
       `forall` we're introducing replaces the binding context of the datatype declaration and only extends it.

     - The only special thing we have to keep track of is the (DeBruijn) index of the `out` variable, but this doesn't require
       any fancy scope tracking: It will always be Z for the top-level result and `S Z` wherever it occurs in a
       transformed constructor. It won't ever occur any "deeper" than that (because we don't nest these, and a constructor gets exactly one
       `out`)

     - Actually this is slightly false, we need to "bump" all of the indices inside constructor arms by one (because
       they now occur within a Thunk), but after that bump everything is stable as indicated above.
-}

{- Here for lack of a better place to put it (has to be available to Unification and ASG)
-}