polysemy-mocks-0.3.1.0: src/Test/Polysemy/Mock/TH.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE TemplateHaskell #-}
module Test.Polysemy.Mock.TH (genMock) where
import Data.Bifunctor (first)
import Data.List (foldl')
import GHC.Stack (HasCallStack)
import Language.Haskell.TH hiding (Strict)
import Polysemy (Embed, Members, Sem, interpret, pureT, reinterpretH)
import Polysemy.Internal (embed, send)
import Polysemy.Internal.TH.Common
import Polysemy.State (get, put)
import Test.Polysemy.Mock
-- | Generate mock using template-haskell.
-- Example usage:
--
-- @
-- genMock ''Teletype
-- @
genMock :: Name -> Q [Dec]
genMock effName = do
constructors <- getEffectMetadata effName
-- MockImpl
let mockImplEffectType = ConT ''MockImpl `AppT` ConT effName `AppT` returnsEffect
let mockImplReturnType = mockImplEffectType `AppT` VarT (mkName "m")
let mockImplDataType = mockImplReturnType `AppT` VarT (mkName "a")
let mockImplConstructors =
map (mkMockConstructor mockImplReturnType) constructors
<> map (mkMockReturns mockImplReturnType) constructors
<> map (mkMockCalls mockImplReturnType) constructors
let mockImplD = DataInstD [] Nothing mockImplDataType Nothing mockImplConstructors []
-- MockState
mockStateConName <- newName (nameBase ''MockState <> nameBase effName)
let mockStateRec =
map mkMockStateCallsField constructors
<> map mkMockStateReturnsField constructors
let mockStateConstructor = RecC mockStateConName mockStateRec
let mockStateType = ConT ''MockState `AppT` ConT effName `AppT` returnsEffect
let mockStateD = DataInstD [] Nothing mockStateType Nothing [mockStateConstructor] []
-- initialMockState
let initialStateExps =
map mkInitialCalls constructors
<> map mkInitialReturns constructors
let initialStateBody = NormalB (RecConE mockStateConName initialStateExps)
let initialStateD = FunD 'initialMockState [Clause [] initialStateBody []]
-- mock
let mockMatches = map (mkMockMatch mockImplEffectType) constructors
let mockBody = NormalB (AppE (VarE 'interpret) (LamCaseE mockMatches))
let mockD = FunD 'mock [Clause [] mockBody []]
-- mockToState
let mockToStateMatches =
map (mkMockToStateMatch mockStateType) constructors
<> map (mkReturnsToStateMatch mockStateType) constructors
<> map (mkCallsToStateMatch mockStateType) constructors
let mockToStateBody = NormalB (AppE (VarE 'reinterpretH) (LamCaseE mockToStateMatches))
let mockToStateD = FunD 'mockToState [Clause [] mockToStateBody []]
-- instance
let mockInstanceD =
InstanceD
Nothing
[ConT ''Applicative `AppT` returnsEffect]
(ConT ''Mock `AppT` ConT effName `AppT` returnsEffect)
[ mockImplD,
mockStateD,
initialStateD,
mockD,
mockToStateD
]
-- makeSem
let semD =
concatMap (mkReturnsSem mockImplEffectType) constructors
<> concatMap (mkCallsSem mockImplEffectType) constructors
-- Bring it together
pure $ mockInstanceD : semD
mkMockConstructor :: Type -> ConLiftInfo -> Con
mkMockConstructor t c =
let args = (map (first (const defaultBang)) $ cliFunArgs c)
in GadtC [mockConName c] args (AppT t $ cliEffRes c)
mkMockReturns :: Type -> ConLiftInfo -> Con
mkMockReturns t c =
GadtC [returnsConName c] [(defaultBang, returnsFunctionType c)] (AppT t $ TupleT 0)
mkMockCalls :: Type -> ConLiftInfo -> Con
mkMockCalls t c =
GadtC [callsConName c] [] (AppT t (functionCallType c))
mkMockStateCallsField :: ConLiftInfo -> (Name, Bang, Type)
mkMockStateCallsField c =
(callsFieldName c, defaultBang, functionCallType c)
mkMockStateReturnsField :: ConLiftInfo -> (Name, Bang, Type)
mkMockStateReturnsField c =
(returnsFieldName c, defaultBang, returnsFunctionType c)
mkInitialCalls :: ConLiftInfo -> (Name, Exp)
mkInitialCalls c =
(callsFieldName c, ListE [])
mkInitialReturns :: ConLiftInfo -> (Name, Exp)
mkInitialReturns c =
let returnsFn =
case cliEffRes c of
(TupleT 0) -> LamE (map (const WildP) $ cliFunArgs c) $ AppE (VarE 'pure) (TupE [])
_ -> AppE (VarE 'error) (LitE (StringL $ "Unexpected mock invocation: " <> nameBase (cliFunName c)))
in (returnsFieldName c, returnsFn)
mkMockMatch :: Type -> ConLiftInfo -> Match
mkMockMatch t c =
#if MIN_VERSION_template_haskell(2,18,0)
let pat = ConP (cliConName c) [] (map (VarP . fst) (cliFunArgs c))
#else
let pat = ConP (cliConName c) (map (VarP . fst) (cliFunArgs c))
#endif
sendFn = VarE 'send
args = map (VarE . fst) (cliFunArgs c)
theMock = foldl' AppE (ConE $ mockConName c) args
body = NormalB (AppE (AppTypeE sendFn t) theMock)
in Match pat body []
#if MIN_VERSION_template_haskell(2,17,0)
#define UNQUALIFIED_DO Nothing
#else
#define UNQUALIFIED_DO
#endif
{- ORMOLU_DISABLE -}
mkMockToStateMatch :: Type -> ConLiftInfo -> Match
mkMockToStateMatch t c =
#if MIN_VERSION_template_haskell(2,18,0)
let pat = ConP (mockConName c) [] (map VarP vars)
#else
let pat = ConP (mockConName c) (map VarP vars)
#endif
--
vars = map fst (cliFunArgs c)
newArgs = if length (cliFunArgs c) == 1
then ListE [ VarE . fst . head . cliFunArgs $ c]
else
#if MIN_VERSION_template_haskell(2,16,0)
ListE [TupE (map (Just . VarE . fst) $ cliFunArgs c)]
#else
ListE [TupE (map (VarE . fst) $ cliFunArgs c)]
#endif
oldArgs = AppE (VarE (callsFieldName c)) (VarE stateName)
allArgs = InfixE (Just oldArgs) (VarE '(++)) (Just newArgs)
newState = RecUpdE (VarE stateName) [(callsFieldName c, allArgs)]
--
applyReturnsFn = foldl' AppE (VarE (returnsFieldName c)) (VarE stateName : map VarE vars)
embedReturnsFn = AppE (VarE 'embed) applyReturnsFn
returnAsPureT = NoBindS $ InfixE (Just (VarE 'pureT)) (VarE '(=<<)) (Just embedReturnsFn)
body =
NormalB
( DoE
UNQUALIFIED_DO
[ getState t,
putState newState,
returnAsPureT
]
)
in Match pat body []
{- ORMOLU_ENABLE -}
mkReturnsToStateMatch :: Type -> ConLiftInfo -> Match
mkReturnsToStateMatch t c =
let f = mkName "f"
#if MIN_VERSION_template_haskell(2,18,0)
pat = ConP (returnsConName c) [] [VarP f]
#else
pat = ConP (returnsConName c) [VarP f]
#endif
newState = RecUpdE (VarE stateName) [(returnsFieldName c, VarE f)]
returnNothing = NoBindS $ AppE (VarE 'pureT) (TupE [])
body =
NormalB
( DoE
UNQUALIFIED_DO
[ getState t,
putState newState,
returnNothing
]
)
in Match pat body []
mkCallsToStateMatch :: Type -> ConLiftInfo -> Match
mkCallsToStateMatch t c =
#if MIN_VERSION_template_haskell(2,18,0)
let pat = ConP (callsConName c) [] []
#else
let pat = ConP (callsConName c) []
#endif
returnCalls = NoBindS $ AppE (VarE 'pureT) (AppE (VarE (callsFieldName c)) (VarE stateName))
body =
NormalB
( DoE
UNQUALIFIED_DO
[ getState t,
returnCalls
]
)
in Match pat body []
mkReturnsSem ::
-- | Should look like: @MockImpl Teletype n@
-- n is assumed to be 'stateEffectName', maybe this is problematic, but it works for now
Type ->
ConLiftInfo ->
[Dec]
mkReturnsSem mockImplEffType c =
let funcName = mkName ("mock" <> nameBase (cliConName c) <> "Returns")
f = mkName "f"
body = NormalB $ VarE 'send `AppE` (ConE (returnsConName c) `AppE` VarE f)
appArrowT = AppT . AppT ArrowT
r = VarT $ mkName "r"
semr t = ConT ''Sem `AppT` r `AppT` t
typ = ForallT [] [membersEffListType mockImplEffType r] (returnsFunctionType c `appArrowT` semr (TupleT 0))
in [ SigD funcName typ,
FunD funcName [Clause [VarP f] body []]
]
#if MIN_VERSION_template_haskell(2,17,0)
#define TY_VAR_SPECIFICTY SpecifiedSpec
#else
#define TY_VAR_SPECIFICTY
#endif
mkCallsSem ::
-- | Should look like: @MockImpl Teletype n@
-- n is assumed to be 'stateEffectName', maybe this is problematic, but it works for now
Type ->
ConLiftInfo ->
[Dec]
mkCallsSem mockImplEffType c =
let funcName = mkName ("mock" <> nameBase (cliConName c) <> "Calls")
typeAppliedSend = VarE 'send `AppTypeE` mockImplEffType
body = NormalB $ typeAppliedSend `AppE` ConE (callsConName c)
r = VarT $ mkName "r"
semr t = ConT ''Sem `AppT` r `AppT` t
typ =
ForallT
[PlainTV returnsEffectName TY_VAR_SPECIFICTY, PlainTV (mkName "r") TY_VAR_SPECIFICTY]
[membersEffListType mockImplEffType r]
(semr $ functionCallType c)
in [ SigD funcName typ,
FunD funcName [Clause [] body []]
]
membersEffListType :: Type -> Type -> Type
membersEffListType mockImplEffType r =
let embededStateEffect = ConT ''Embed `AppT` VarT returnsEffectName
appConsT = AppT . AppT PromotedConsT
effList = foldr appConsT PromotedNilT [mockImplEffType, embededStateEffect]
in ConT ''Members `AppT` effList `AppT` r
getState :: Type -> Stmt
getState t = BindS (VarP stateName) (VarE 'get `AppTypeE` t)
putState :: Exp -> Stmt
putState newState = NoBindS (AppE (VarE 'put) newState)
stateName :: Name
stateName = mkName "state"
callsConName :: ConLiftInfo -> Name
callsConName c = mkName ("Mock" <> nameBase (cliConName c) <> "Calls")
returnsConName :: ConLiftInfo -> Name
returnsConName c = mkName ("Mock" <> nameBase (cliConName c) <> "Returns")
mockConName :: ConLiftInfo -> Name
mockConName c = mkName ("Mock" <> nameBase (cliConName c))
callsFieldName :: ConLiftInfo -> Name
callsFieldName c = mkName (nameBase (cliFunName c) <> "Calls")
returnsFieldName :: ConLiftInfo -> Name
returnsFieldName c = mkName (nameBase (cliFunName c) <> "Returns")
defaultBang :: Bang
defaultBang = Bang NoSourceUnpackedness NoSourceStrictness
functionCallType :: ConLiftInfo -> Type
functionCallType c =
let arity = length $ cliFunArgs c
in if arity == 1
then AppT ListT $ snd $ head $ cliFunArgs c
else AppT ListT $ foldl' AppT (TupleT arity) (map snd $ cliFunArgs c)
returnsFunctionType :: ConLiftInfo -> Type
returnsFunctionType c =
let argTypes = (map snd $ cliFunArgs c)
returnType = (AppT returnsEffect $ cliEffRes c)
in ForallT [] [ConT ''HasCallStack] $ foldr (AppT . AppT ArrowT) returnType argTypes
returnsEffect :: Type
returnsEffect = VarT returnsEffectName
returnsEffectName :: Name
returnsEffectName = mkName "n"