covenant-1.3.0: src/Covenant/Internal/KindCheck.hs
-- | This module define a "kind-checking" pass. This requires some explanation, since we don't have
-- an *explicit* notion of kinds in Covenant:
--
-- With the addition of type constructors for datatypes into ValT comes a new set of things that can
-- "go wrong". In particular:
-- - Someone may try to use a type constructor which is not defined anywhere
-- - A type may be applied to an incorrect number of arguments
-- - The "count" - the number of bound tyvars in the `ValT.Datatype` representation - may be incorrect (i.e. inconsistent with the count in the declaration)
--
-- The checks to detect these errors are entirely independent from the checks performed during typechecking or renaming, so we do them in a separate pass.
module Covenant.Internal.KindCheck
( checkDataDecls,
checkValT,
KindCheckError (..),
EncodingArgErr (..),
cycleCheck,
checkEncodingArgs,
)
where
import Control.Monad (unless)
import Control.Monad.Except (ExceptT, MonadError (throwError), runExceptT)
import Control.Monad.Reader
( MonadReader (local),
ReaderT (ReaderT),
asks,
runReaderT,
)
import Covenant.Data (everythingOf)
import Covenant.Index (Count, intCount)
import Covenant.Internal.Strategy
( DataEncoding (SOP),
)
import Covenant.Internal.Type
( AbstractTy,
CompT (CompT),
CompTBody (CompTBody),
Constructor (Constructor),
DataDeclaration (DataDeclaration, OpaqueData),
TyName,
ValT (Abstraction, BuiltinFlat, Datatype, ThunkT),
checkStrategy,
datatype,
)
import Data.Foldable (traverse_)
import Data.Functor.Identity (Identity, runIdentity)
import Data.Kind (Type)
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as M
import Data.Maybe (mapMaybe)
import Data.Set (Set)
import Data.Set qualified as Set
import Data.Vector (Vector)
import Data.Vector qualified as V
import Optics.Core (A_Lens, LabelOptic (labelOptic), folded, lens, preview, review, to, toListOf, view, (%))
{- TODO: Explicitly separate the kind checker into two check functions:
- One which kind checks `ValT`s to ensure:
1. All TyCons in the ValT exist
2. All TyCons in the ValT have the correct arity
- One which checks *datatype declarations* to ensure:
1. Everything satisfies the above ValT checks
2. No thunk arguments to ctors!
3. No mutual recursion (cycles)
-}
data KindCheckError
= UnknownType TyName
| IncorrectNumArgs TyName (Count "tyvar") (Vector (ValT AbstractTy)) -- first is expected (from the decl), second is actual
| ThunkConstructorArg (CompT AbstractTy) -- no polymorphic function args to ctors
| MutualRecursionDetected (Set TyName)
| InvalidStrategy TyName
| EncodingMismatch (EncodingArgErr AbstractTy)
| OpaqueWithNoConstructors TyName
deriving stock (Show, Eq)
newtype KindCheckContext a = KindCheckContext (Map TyName (DataDeclaration a))
instance
(k ~ A_Lens, a ~ Map TyName (DataDeclaration c), b ~ Map TyName (DataDeclaration c)) =>
LabelOptic "kindCheckContext" k (KindCheckContext c) (KindCheckContext c) a b
where
{-# INLINEABLE labelOptic #-}
labelOptic =
lens
(\(KindCheckContext x) -> x)
(\(KindCheckContext _) x' -> KindCheckContext x')
newtype KindCheckM t a = KindCheckM (ReaderT (KindCheckContext t) (ExceptT KindCheckError Identity) a)
deriving
(Functor, Applicative, Monad, MonadReader (KindCheckContext t), MonadError KindCheckError)
via (ReaderT (KindCheckContext t) (ExceptT KindCheckError Identity))
runKindCheckM :: forall (t :: Type) (a :: Type). Map TyName (DataDeclaration t) -> KindCheckM t a -> Either KindCheckError a
runKindCheckM dtypes (KindCheckM act) = runIdentity . runExceptT $ runReaderT act (KindCheckContext dtypes)
lookupDeclaration :: forall (t :: Type). TyName -> KindCheckM t (DataDeclaration t)
lookupDeclaration tn = do
types <- asks (view #kindCheckContext)
case M.lookup tn types of
Nothing -> throwError $ UnknownType tn
Just decl -> pure decl
{- This sanity checks datatype declarations using the criteria enumerated above.
-}
-- | Checks that all the data declarations in the argument \'make sense\'.
-- Specifically:
--
-- * The strategy declared for the datatype is valid for it
-- * There are no mutually recursive datatype declarations
-- * Constructor arguments are not thunks
-- * The number of type variables in any constructor isn't greater than we
-- expect
--
-- @since 1.1.0
checkDataDecls :: Map TyName (DataDeclaration AbstractTy) -> Either KindCheckError ()
checkDataDecls decls = runKindCheckM decls $ traverse_ checkDataDecl (M.elems decls)
checkDataDecl :: DataDeclaration AbstractTy -> KindCheckM AbstractTy ()
checkDataDecl (OpaqueData tn ctors)
| null ctors = throwError $ OpaqueWithNoConstructors tn
| otherwise = pure ()
checkDataDecl decl@(DataDeclaration tn _ ctors _) = do
unless (checkStrategy decl) $ throwError (InvalidStrategy tn)
cycleCheck' mempty decl
let allCtorArgs = view #constructorArgs =<< ctors
traverse_ (checkKinds CheckDataDecl) allCtorArgs
checkEncodingArgsInDataDecl decl
data KindCheckMode = CheckDataDecl | CheckValT
deriving stock (Show, Eq, Ord)
-- This isn't really a "kind checker" in the normal sense and just checks that none of the three failure conditions above obtain
checkKinds :: KindCheckMode -> ValT AbstractTy -> KindCheckM AbstractTy ()
checkKinds mode = \case
Abstraction _ -> pure ()
ThunkT compT@(CompT _ (CompTBody nev))
| mode == CheckDataDecl -> throwError $ ThunkConstructorArg compT
| otherwise -> traverse_ (checkKinds mode) nev
BuiltinFlat {} -> pure ()
Datatype tn args ->
lookupDeclaration tn >>= \case
OpaqueData {} -> pure ()
DataDeclaration _ numVars _ _ -> do
let numArgsActual = V.length args
numArgsExpected = review intCount numVars
unless (numArgsActual == numArgsExpected) $ throwError (IncorrectNumArgs tn numVars args)
traverse_ (checkKinds mode) args
-- | This is for checking type annotations in the ASG, *not* datatypes
checkValT :: Map TyName (DataDeclaration AbstractTy) -> ValT AbstractTy -> Maybe KindCheckError
checkValT dtypes = either Just (const Nothing) . runKindCheckM dtypes . checkKinds CheckValT
-- | Verifies that no types in the argument are mutually recursive.
--
-- @since 1.1.0
cycleCheck :: forall (a :: Type). (Ord a) => Map TyName (DataDeclaration a) -> Maybe KindCheckError
cycleCheck decls = either Just (const Nothing) $ runKindCheckM decls go
where
go =
local (\_ -> KindCheckContext decls) $
traverse_ (cycleCheck' mempty) =<< asks (view (#kindCheckContext % to M.elems))
{- This is a bit odd b/c we don't want to fail for auto-recursive types, so we need to be careful
*not* to mark the current decl being examined as "visited" until we've "descended" into the dependencies
(I think?)
-}
cycleCheck' :: forall (a :: Type). (Ord a) => Set TyName -> DataDeclaration a -> KindCheckM a ()
cycleCheck' _ OpaqueData {} = pure ()
cycleCheck' visited (DataDeclaration tn _ ctors _) = traverse_ (checkCtor visited tn) ctors
where
checkCtor :: Set TyName -> TyName -> Constructor a -> KindCheckM a ()
checkCtor vs tn' (Constructor _ args) = do
let allComponents = Set.toList . Set.unions $ everythingOf <$> V.toList args
-- every type constructor in any part of a constructor arg, *except* the tycon of the decl
-- we're examining, since autorecursion is fine/necessary
allTyCons = Set.filter (/= tn') . Set.fromList . mapMaybe (fmap fst . preview datatype) $ allComponents
alreadyVisitedArgTys = Set.intersection allTyCons vs
unless (null alreadyVisitedArgTys) $ throwError (MutualRecursionDetected alreadyVisitedArgTys)
let newVisited = Set.insert tn' vs
nextRound <- traverse lookupDeclaration (Set.toList allTyCons)
traverse_ (cycleCheck' newVisited) nextRound
{- Arguably the closest thing to a real kind checker in the module.
Checks whether the arguments to type constructors (ValT 'Datatype's) conform with their encoding.
-}
-- | Indicates that we tried to instantiate a polymorphic data type using a type
-- whose encoding is incompatible. The most common case of this is when we have
-- a \'polymorphic\' data type that uses some kind of @Data@ encoding and we try
-- to instantiate it with a type which has a SOP encoding.
--
-- @since 1.2.0
data EncodingArgErr a
= -- | First field is a constructor name for the type we tried to instantiate,
-- second field is the bad instantiator.
--
-- @since 1.2.0
EncodingArgMismatch TyName (ValT a)
deriving stock (Show, Eq)
-- | Verifies that a datatype (third argument) is valid according to its stated
-- encoding, as provided by the first two arguments (projection and metadata).
--
-- = Note
--
-- If the datatype being validated refers to other datatypes, we assume that
-- they exist in the metadata 'Map'. Thus, we must ensure this holds or the
-- check will fail.
--
-- @since 1.1.0
checkEncodingArgs ::
forall (a :: Type) (info :: Type).
(info -> DataEncoding) -> -- this lets us not care about whether we're doing this w/ a DataDeclaration or DatatypeInfo
Map TyName info ->
ValT a ->
Either (EncodingArgErr a) ()
checkEncodingArgs getEncoding tyDict = \case
Abstraction {} -> pure ()
BuiltinFlat {} -> pure ()
ThunkT (CompT _ (CompTBody args)) -> traverse_ go args
Datatype tn args -> do
let encoding = getEncoding $ tyDict M.! tn
case encoding of
-- Might as well check all the way down
SOP -> do
{- NOTE Sean 7/2/25: We are *temporarily* disallowing thunk arguments to SOPs to speed up development and
create consistency. We disallow Thunk arguments to constructors in datatype declarations,
and while we could very well allow them outside of those declarations, it creates a strange situation
where the same function might be safe/unsafe depending on whether it is used on a ValT inside of a data
declaration vs (e.g.) a type annotation in the ASG.
To remove this restriction, delete the `traverse_ isValidSOPArg args` line below
-}
traverse_ go args
traverse_ (isValidSOPArg tn) args
-- Both explicit data encodings and builtins should be "morally data encoded"
_ -> do
traverse_ go args
traverse_ (isValidDataArg tn) args
where
go :: ValT a -> Either (EncodingArgErr a) ()
go = checkEncodingArgs getEncoding tyDict
isValidDataArg :: TyName -> ValT a -> Either (EncodingArgErr a) ()
isValidDataArg tn = \case
Abstraction {} -> pure ()
BuiltinFlat {} -> pure ()
thunk@ThunkT {} -> throwError $ EncodingArgMismatch tn thunk
dt@(Datatype tn' args') -> do
let encoding = getEncoding $ tyDict M.! tn'
case encoding of
SOP -> throwError $ EncodingArgMismatch tn dt
_ -> traverse_ go args'
isValidSOPArg :: TyName -> ValT a -> Either (EncodingArgErr a) ()
isValidSOPArg tn = \case
Abstraction {} -> pure ()
BuiltinFlat {} -> pure ()
thunk@ThunkT {} -> throwError $ EncodingArgMismatch tn thunk
Datatype tn' args' -> traverse_ (isValidSOPArg tn') args'
checkEncodingArgsInDataDecl :: DataDeclaration AbstractTy -> KindCheckM AbstractTy ()
checkEncodingArgsInDataDecl decl =
asks (view #kindCheckContext) >>= \tyDict ->
case traverse (checkEncodingArgs (view #datatypeEncoding) tyDict) allConstructorArgs of
Left encErr -> throwError $ EncodingMismatch encErr
Right _ -> pure ()
where
allConstructorArgs :: Vector (ValT AbstractTy)
allConstructorArgs = V.concat $ toListOf (#datatypeConstructors % folded % #constructorArgs) decl