packages feed

th-deepstrict-0.1.0.0: src/Language/Haskell/TH/DeepStrict.hs

{-# LANGUAGE DeriveFunctor              #-}
{-# LANGUAGE DeriveLift                 #-}
{-# LANGUAGE DerivingVia                #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase                 #-}
{-# LANGUAGE MagicHash                  #-}
{-# LANGUAGE TemplateHaskellQuotes      #-}
{-# LANGUAGE TupleSections              #-}

-- | Check that a datatype is deeply strict, ie, it recursively only has strict fields.
module Language.Haskell.TH.DeepStrict
  (
  -- * DeepStrict
    DeepStrict(..)
  , DeepStrictReason(..)
  , DeepStrictWithReason
  -- * Checking data types
  , isDeepStrict
  , isDeepStrictWith
  , assertDeepStrict
  , assertDeepStrictWith
  -- * Context
  , Context(..)
  , Strictness(..)
  , emptyContext
  , FieldKey
  ) where

import Data.Maybe                    (mapMaybe)
import Control.Monad                 (when)
import Control.Monad.IO.Class        (MonadIO)
import Control.Monad.Reader          (MonadReader (ask, local), ReaderT (..), asks)
import Control.Monad.Trans           (lift)
import Data.Bifunctor                (first)
import Data.IORef                    (IORef, modifyIORef', newIORef, readIORef)
import Data.Traversable              (for)
import GHC.Stack                     (HasCallStack)
import Language.Haskell.TH           (Q)
import Language.Haskell.TH.Instances ()

import qualified Data.Map                     as ML
import qualified Data.Set                     as S
import qualified Data.Map.Strict              as M
import qualified Language.Haskell.TH          as TH
import qualified Language.Haskell.TH.Datatype as TH
import qualified Language.Haskell.TH.Ppr      as Ppr
import qualified Language.Haskell.TH.PprLib   as Ppr
import qualified Language.Haskell.TH.Syntax   as TH

newtype DeepStrictM a = DeepStrictM { runDeepStrictM :: ReaderT Context Q a }
  deriving newtype (Functor, Applicative, Monad, MonadIO, MonadFail, MonadReader Context)
  deriving (TH.Quote, TH.Quasi) via (ReaderT Context Q)

-- | Allow overriding various setting that determine what types we consider deep strict.
data Context = Context
  { contextSpine          :: !(S.Set TH.Type) -- ^ The types we are recursively checking. By the inductive hypothesis, we assume they are DeepStrict.
  , contextCache          :: !(IORef (M.Map TH.Type DeepStrictWithReason))
  , contextOverride       :: !(M.Map TH.Name (Maybe [Strictness])) -- ^ Maps names of types to whether they can be deep strict and if they can which arguments need to be strict
  , contextRecursionDepth :: !Int -- ^ A recursion depth to avoid infinite loops.
  }

-- | The default t'Context'.
emptyContext :: Q Context
emptyContext = do
  emptyCache <- TH.runIO $ newIORef M.empty
  pure $
    Context
      { contextSpine = S.empty
      , contextCache = emptyCache
      , contextOverride = M.empty
      , contextRecursionDepth = 1000
      }

-- | A type is deep strict if and only if for each constructor:
--
--   - All of its fields are strict, ie, they have a @!@ if possible.
--   - The type of of each field is deep strict.
--
-- The Monoid instance allows us to gather up reasons why a type fails to be deep strict.
--
-- === Examples
--
-- @()@ is deep strict because its single constructor doesn't have any fields so it is vacuously deep strict.
--
-- 'Int', 'Char', etc are all deep strict because they are wrappers around unlifted types that cannot be lazy.
--
-- @Maybe Int@ is not deep strict.
-- It has a 'Nothing' constructor, which is fine.
-- But, the 'Just' constructor has a lazy field, which means it's not deep strict.
data DeepStrict reason =
    DeepStrict
  | NotDeepStrict !reason
  deriving (Eq, Ord, Show, TH.Lift, Functor)

type DeepStrictWithReason = DeepStrict [DeepStrictReason]

instance Semigroup reason => Semigroup (DeepStrict reason) where
  DeepStrict <> DeepStrict                       = DeepStrict
  NotDeepStrict reason <> DeepStrict             = NotDeepStrict reason
  DeepStrict <> NotDeepStrict reason             = NotDeepStrict reason
  NotDeepStrict reason1 <> NotDeepStrict reason2 = NotDeepStrict $ reason1 <> reason2

instance Semigroup reason => Monoid (DeepStrict reason) where
  mempty = DeepStrict

-- | Reasons why a type fails to be deep strict.
data DeepStrictReason =
    LazyType !TH.Type ![DeepStrictReason]
  -- ^ The type is lazy.
  | LazyConstructor !TH.Name ![DeepStrictReason]
  -- ^ The type has a lazy constructor.
  | FieldReason !FieldKey ![DeepStrictReason]
  -- ^ One of the fields of the constructor fails to be deep strict.
  | LazyField !FieldKey
  -- ^ One of the fields of the constructor is lazy, ie, doesn't have a @!@.
  | LazyOther !String
  deriving (Eq, Ord, Show, TH.Lift)

instance Ppr.Ppr reason => Ppr.Ppr (DeepStrict reason) where
  ppr DeepStrict             = Ppr.text "DeepStrict" Ppr.$+$ Ppr.text ""
  ppr (NotDeepStrict reason) = Ppr.text "NotDeepStrict" Ppr.$$ Ppr.ppr reason Ppr.$+$ Ppr.text ""

instance Ppr.Ppr DeepStrictReason where
  ppr (LazyType typ rest) = Ppr.hang (Ppr.ppr typ) 2 (Ppr.vcat (map Ppr.ppr rest))
  ppr (LazyConstructor name rest) = Ppr.hang (Ppr.text "con" Ppr.<+> Ppr.ppr name) 2 $ Ppr.vcat $ map Ppr.ppr rest
  ppr (FieldReason (Left ix) rest) = Ppr.hang (Ppr.text "field" Ppr.<+> Ppr.int ix) 2  $ Ppr.vcat $ map Ppr.ppr rest
  ppr (FieldReason (Right name) rest) = Ppr.hang (Ppr.ppr name) 2  $ Ppr.vcat $ map Ppr.ppr rest
  ppr (LazyField (Left ix)) = Ppr.text "field" Ppr.<+> Ppr.int ix Ppr.<+> Ppr.text "is lazy"
  ppr (LazyField (Right name)) = Ppr.text "field" Ppr.<+>  Ppr.ppr name Ppr.<+> Ppr.text "is lazy"
  ppr (LazyOther txt) = Ppr.text txt

giveReasonContext :: ([DeepStrictReason] -> DeepStrictReason) -> DeepStrictWithReason  -> DeepStrictWithReason
giveReasonContext f =  fmap (pure . f)

prettyPanic :: (HasCallStack, Ppr.Ppr x, Show x) => String -> x -> a
prettyPanic context x = error $ context <> ": " <> Ppr.pprint x

data Levity = Lifted | Unlifted
  deriving (Eq, Ord, Show)

-- | Whether a type is used strictly by a data type.
-- We use these to annotate types with deep strictness overrides.
-- Types that have fields labelled as 'Language.Haskell.TH.DeepStrict.Strict' require those types to be deep strict.
-- Types that have fields labelled as 'Language.Haskell.TH.DeepStrict.Lazy' will never be deep strict, but this can be helpful for nicer messages.
data Strictness = Strict | Lazy
  deriving (Eq, Ord, Show)

-- | A function/constructor is weak strict either iff it is strict and the argument isn't unlifted
-- So, it is like strictness but functions/constructors with unlifted/newtype args are WeakLazy
-- See: https://gitlab.haskell.org/ghc/ghc/-/issues/21380
data WeakStrictness = WeakStrict | WeakLazy
  deriving (Eq, Ord, Show)

data HasBang = HasBang | NoBang
  deriving (Eq, Ord, Show)

type FieldKey = Either Int TH.Name

data FieldInfo =
  FieldInfo
  { fieldInfoName :: FieldKey -- ^ either the index of the field or the name
  , fieldInfoBang :: WeakStrictness
  , fieldInfoType :: TH.Type -- ^ May contain variables bound by datatype args
  } deriving (Eq, Ord, Show)

type Env = ML.Map TH.Name TH.Type

prepareDatatypeInfoEnv :: HasCallStack => [TH.Type] -> [TH.Name] -> (Env, [TH.Type])
prepareDatatypeInfoEnv args argNames = first makeEnv $ splitAt (length argNames) args
  where
    makeEnv = ML.fromList . zip argNames

substituteDatatypeInfoEnv :: HasCallStack => [TH.Type] -> TH.DatatypeInfo -> (TH.DatatypeInfo, [TH.Type])
substituteDatatypeInfoEnv typeArgs datatypeInfo =
  (datatypeInfo { TH.datatypeCons = TH.applySubstitution env (TH.datatypeCons datatypeInfo)
                }
  , typeArgs')
  where
    getVariable :: TH.Type -> Maybe TH.Name
    getVariable (TH.SigT t _k) = getVariable t
    getVariable (TH.VarT v) = Just v
    getVariable _ = Nothing
    freeVars = mapMaybe getVariable $ TH.datatypeInstTypes datatypeInfo
    (env, typeArgs') = prepareDatatypeInfoEnv typeArgs freeVars

decodeDecidedStrictness :: TH.DecidedStrictness -> WeakStrictness
decodeDecidedStrictness TH.DecidedStrict = WeakStrict
decodeDecidedStrictness TH.DecidedUnpack = WeakStrict
decodeDecidedStrictness TH.DecidedLazy   = WeakLazy

reifyLevityType :: HasCallStack => TH.Type -> DeepStrictM Levity
reifyLevityType (TH.ConT name) = reifyLevityName name
reifyLevityType (TH.AppT x _)  = reifyLevityType x
reifyLevityType (TH.ListT{})   = pure Lifted
reifyLevityType (TH.TupleT{})  = pure Lifted
reifyLevityType (TH.ArrowT{})  = pure Lifted
reifyLevityType (TH.UnboxedTupleT{})  = pure Unlifted
reifyLevityType (TH.UnboxedSumT{})  = pure Unlifted
reifyLevityType typ            = prettyPanic "unexpected type" typ

-- | precondtion: name is a type
reifyLevityName :: HasCallStack => TH.Name -> DeepStrictM Levity
reifyLevityName name = do
  kind <- TH.qReifyType name
  pure $ classifyKindLevity kind

-- | Figure out the levity of a type from its kind.
--   If it has type arguments the kind will have arrows, we want to know the final return type.
--   Eg, for (x -> (y -> z)), we care about z
classifyKindLevity :: TH.Kind -> Levity
classifyKindLevity (TH.AppT _ x) = classifyKindLevity x
classifyKindLevity TH.StarT      = Lifted
classifyKindLevity _             = Unlifted


isDatatypeDeepStrict :: HasCallStack => TH.DatatypeInfo -> [TH.Type] -> DeepStrictM DeepStrictWithReason
isDatatypeDeepStrict dt args = isDatatypeDeepStrict' dt' args'
  where
    (dt', args') = substituteDatatypeInfoEnv args dt

isDatatypeDeepStrict' :: HasCallStack => TH.DatatypeInfo -> [TH.Type] -> DeepStrictM DeepStrictWithReason
isDatatypeDeepStrict' datatypeInfo args = do
  consDeepStrict <- traverse (\c -> isConDeepStrict c (TH.datatypeVariant datatypeInfo) args) $ TH.datatypeCons datatypeInfo
  pure $ mconcat consDeepStrict

-- | Figure out the field names for a constructor.
-- We have names for records, we use indices for everything else.
extractFieldNames :: TH.ConstructorVariant -> [FieldKey]
extractFieldNames (TH.RecordConstructor fieldNames) = map Right fieldNames
extractFieldNames _                                 = map Left [0..]

isConDeepStrict :: HasCallStack => TH.ConstructorInfo -> TH.DatatypeVariant -> [TH.Type] -> DeepStrictM DeepStrictWithReason
isConDeepStrict conInfo@(TH.ConstructorInfo { TH.constructorName = conName, TH.constructorFields = fieldTypes }) variant args = do
  fieldBangs <-
    if isNewtype variant
    then pure $ repeat WeakStrict -- newtypes are strict
    else map decodeDecidedStrictness <$> TH.qReifyConStrictness conName
  let fieldNames = extractFieldNames $ TH.constructorVariant conInfo
  let conFields = zipWith3 FieldInfo fieldNames fieldBangs fieldTypes
  fieldDeepStrict <- traverse (`isFieldDeepStrict` args) conFields
  pure $ giveReasonContext (LazyConstructor conName) $ mconcat fieldDeepStrict

isNewtype :: TH.DatatypeVariant -> Bool
isNewtype TH.Newtype         = True
isNewtype TH.NewtypeInstance = True
isNewtype _                  = False

isFieldDeepStrict :: HasCallStack => FieldInfo -> [TH.Type] -> DeepStrictM DeepStrictWithReason
isFieldDeepStrict (FieldInfo fieldName fieldWeakStrictness fieldType) args = do
  fieldTypeRecStrict <- isTypeDeepStrict fieldType args
  fieldLevity <- reifyLevityType fieldType
  case (fieldWeakStrictness, fieldTypeRecStrict, fieldLevity) of
    (WeakStrict, DeepStrict, _) -> pure DeepStrict
    (WeakLazy, DeepStrict, Unlifted) -> pure DeepStrict
    (WeakLazy, strictness, Lifted) -> pure $ NotDeepStrict [LazyField fieldName] <> inField strictness
    (_, strictness, _) -> pure $ inField strictness
  where
    inField = giveReasonContext (FieldReason fieldName)

getCachedDeepStrict :: HasCallStack => TH.Type -> DeepStrictM (Maybe DeepStrictWithReason)
getCachedDeepStrict typ = do
  cacheRef <- asks contextCache
  cache <- TH.qRunIO $ readIORef cacheRef
  pure $ M.lookup typ cache

putCachedDeepStrict :: HasCallStack => TH.Type -> DeepStrictWithReason  -> DeepStrictM ()
putCachedDeepStrict typ val = do
  cacheRef <- asks contextCache
  TH.qRunIO . modifyIORef' cacheRef $ M.insert typ (const [LazyOther $ Ppr.pprint typ <> " is lazy see above"] <$> val)

isTypeDeepStrict :: HasCallStack => TH.Type -> [TH.Type] -> DeepStrictM DeepStrictWithReason
isTypeDeepStrict typ args = do
  ctxt <- ask
  cachedVal <- getCachedDeepStrict typ
  when (contextRecursionDepth ctxt <= 0) . fail $ "Recursion depth reached. Try adding an override for this type: " <> take 1000 (show typ)
  case (cachedVal, S.member typ $ contextSpine ctxt) of
    (Just val, _) -> pure val
    (_, True) -> pure DeepStrict -- by inductive hypothesis
    _ ->
      local (\_ctxt ->
        ctxt {contextSpine = S.insert typ (contextSpine ctxt), contextRecursionDepth = contextRecursionDepth ctxt - 1}) $ do
          ret <- inType <$> isTypeDeepStrict' typ args
          putCachedDeepStrict typ ret
          pure ret
  where
    inType = giveReasonContext (LazyType typ)

isTypeDeepStrict' :: HasCallStack => TH.Type -> [TH.Type] -> DeepStrictM DeepStrictWithReason
isTypeDeepStrict' (TH.ConT typeName) args = isNameDeepStrict typeName args
isTypeDeepStrict' (TH.AppT func arg) args = isTypeDeepStrict' func (arg:args)
isTypeDeepStrict' (TH.TupleT 0) _         = pure DeepStrict -- () is DeepStrict
isTypeDeepStrict' (TH.TupleT n) args      = isNameDeepStrict (TH.tupleTypeName n) args
isTypeDeepStrict' (TH.ArrowT{}) _         = pure $ NotDeepStrict [LazyOther "Functions are lazy"]
isTypeDeepStrict' (TH.ListT{}) args       = isNameDeepStrict ''[] args
isTypeDeepStrict' (TH.UnboxedTupleT arity) args = isNameDeepStrict (TH.unboxedTupleTypeName arity) args
isTypeDeepStrict' (TH.UnboxedSumT arity) args  = isNameDeepStrict (TH.unboxedSumTypeName arity) args
isTypeDeepStrict' typ _                   = prettyPanic "Unexpected type" typ

-- | Is this type constructor applied to these arguments deep strict
isNameDeepStrict :: HasCallStack => TH.Name -> [TH.Type] -> DeepStrictM DeepStrictWithReason
isNameDeepStrict typeName args = do
  ctxt <- ask
  case M.lookup typeName $ contextOverride ctxt of
    Nothing -> do
      info <- DeepStrictM $ lift $ TH.reify typeName
      case info of
        -- th-abstraction can't handle type synonyms.
        -- let's treat a type synonym as just the RHS
        TH.TyConI (TH.TySynD _name tyvarbndrs rhs) -> do
          let (env, args') = prepareDatatypeInfoEnv args (map TH.tvName tyvarbndrs)
          isTypeDeepStrict (TH.applySubstitution env rhs) args'
        -- th-abstraction doesn't handle type/data families
        TH.FamilyI{} -> do
          instances <- DeepStrictM $ lift $ TH.reifyInstances typeName args
          case instances of
            -- a type synonym instance is handled like a type synonym:
            -- just treat it as the RHS.
            (TH.TySynInstD (TH.TySynEqn _ lhs rhs)):_ -> do
              let (env, args') = prepareDatatypeInfoEnv args (TH.freeVariables lhs)
              isTypeDeepStrict (TH.applySubstitution env rhs) args'
            _ -> error "The majority of data/type families are currently not supported"
        _ -> do
          datatypeInfo <- DeepStrictM $ lift $ TH.normalizeInfo info
          isDatatypeDeepStrict datatypeInfo args
    Just Nothing -> pure $ NotDeepStrict [LazyOther "This type is marked as lazy"]
    Just (Just strictnessReqs) ->
      fmap mconcat . for (zip strictnessReqs args) $ \case
        (Lazy, _)     -> pure DeepStrict
        (Strict, typ) -> isTypeDeepStrict typ []

-- | Determine if a type is deep strict
-- Invariant: The type doesn't contain any free variables, eg, @Maybe a@ will fail.
isDeepStrict :: TH.Type -> Q DeepStrictWithReason
isDeepStrict typ = do
  emptyC <- emptyContext
  isDeepStrictWith emptyC typ

isDeepStrictWith :: Context -> TH.Type -> Q DeepStrictWithReason
isDeepStrictWith context typ = do
  typRes <- TH.resolveTypeSynonyms typ
  runReaderT (runDeepStrictM $ isTypeDeepStrict typRes []) context


-- | Assert that a type is deep strict.
-- If the type isn't deep strict then this will produce an error with the reasons why.
assertDeepStrict :: TH.Type -> Q [TH.Dec]
assertDeepStrict typ = do
  emptyC <- emptyContext
  assertDeepStrictWith emptyC typ

data DeepStrictAssertionFailed = DeepStrictAssertionFailed TH.Type [DeepStrictReason]

instance Ppr.Ppr DeepStrictAssertionFailed where
  ppr (DeepStrictAssertionFailed typ reason) =
   Ppr.ppr typ Ppr.$+$ Ppr.text "is not Deep Strict, because: "
   Ppr.$$ Ppr.ppr reason

assertDeepStrictWith :: Context -> TH.Type -> Q [TH.Dec]
assertDeepStrictWith context typ = do
  result <- isDeepStrictWith context typ
  case result of
    DeepStrict -> pure []
    NotDeepStrict reason ->
      fail $ Ppr.pprint $ DeepStrictAssertionFailed typ reason