{-# Language CPP, TemplateHaskell #-}
{-|
Module : Harness
Description : Comparison functions for data type info used in tests
Copyright : Eric Mertens 2017
License : ISC
Maintainer : emertens@gmail.com
This module provides comparison functions that are able to check
that the computed 'DatatypeInfo' values match the expected ones
up to alpha renaming.
-}
module Harness
( validateDI
, validateCI
, equateCxt
-- * Utilities
, varKCompat
) where
import Control.Monad
import qualified Data.Map as Map
import Data.Map (Map)
import Data.Maybe
import Language.Haskell.TH
import Language.Haskell.TH.Datatype
import Language.Haskell.TH.Lib (starK)
validateDI :: DatatypeInfo -> DatatypeInfo -> ExpQ
validateDI = validate equateDI
validateCI :: ConstructorInfo -> ConstructorInfo -> ExpQ
validateCI = validate equateCI
validate :: (a -> a -> Either String ()) -> a -> a -> ExpQ
validate equate x y = either fail (\_ -> [| return () |]) (equate x y)
-- | If the arguments are equal up to renaming return @'Right' ()@,
-- otherwise return a string exlaining the mismatch.
equateDI :: DatatypeInfo -> DatatypeInfo -> Either String ()
equateDI dat1 dat2 =
do check "datatypeName" (nameBase . datatypeName) dat1 dat2
check "datatypeVars len" (length . datatypeVars) dat1 dat2
check "datatypeVariant" datatypeVariant dat1 dat2
check "datatypeCons len" (length . datatypeCons) dat1 dat2
let sub = Map.fromList (zip (freeVariables (datatypeVars dat2))
(map VarT (freeVariables (datatypeVars dat1))))
zipWithM_ (equateCxt "datatypeContext")
(datatypeContext dat1)
(applySubstitution sub (datatypeContext dat2))
check "datatypeVars" id
(datatypeVars dat1)
(applySubstitution sub (datatypeVars dat2))
zipWithM_ equateCI
(datatypeCons dat1)
(datatypeCons dat2) -- Don't bother applying the substitution here, as
-- equateCI takes care of this for us
equateCxt :: String -> Pred -> Pred -> Either String ()
equateCxt lbl pred1 pred2 =
do check (lbl ++ " class") asClassPred pred1 pred2
check (lbl ++ " equality") asEqualPred pred1 pred2
-- | If the arguments are equal up to renaming return @'Right' ()@,
-- otherwise return a string exlaining the mismatch.
equateCI :: ConstructorInfo -> ConstructorInfo -> Either String ()
equateCI con1 con2 =
do check "constructorName" (nameBase . constructorName) con1 con2
check "constructorVariant" constructorVariantBase con1 con2
let sub1 = Map.fromList (zip (map tvName (constructorVars con2))
(map VarT (map tvName (constructorVars con1))))
sub2 = Map.fromList (zip (freeVariables (map tvKind (constructorVars con2)))
(map VarT (freeVariables
(map tvKind (constructorVars con1)))))
sub3 = Map.fromList (zip (freeVariables con2)
(map VarT (freeVariables con1)))
sub = Map.unions [sub1, sub2, sub3]
zipWithM_ (equateCxt "constructorContext")
(constructorContext con1)
(applySubstitution sub (constructorContext con2))
check "constructorVars" id
(constructorVars con1)
(substIntoTyVarBndrs sub (constructorVars con2))
check "constructorFields" id
(constructorFields con1)
(applySubstitution sub (constructorFields con2))
zipWithM_ equateStrictness
(constructorStrictness con1)
(constructorStrictness con2)
where
constructorVariantBase :: ConstructorInfo -> ConstructorVariant
constructorVariantBase con =
case constructorVariant con of
NormalConstructor -> NormalConstructor
i@InfixConstructor{} -> i
RecordConstructor fields -> RecordConstructor $ map (mkName . nameBase) fields
-- Substitutes both type variable names and kinds.
substIntoTyVarBndrs :: Map Name Type -> [TyVarBndr] -> [TyVarBndr]
substIntoTyVarBndrs subst = map go
where
go (PlainTV n) = PlainTV $ substName subst n
go (KindedTV n k) = KindedTV (substName subst n)
(applySubstitution subst k)
substName :: Map Name Type -> Name -> Name
substName subst n = fromMaybe n $ do
nty <- Map.lookup n subst
case nty of
VarT n' -> Just n'
_ -> Nothing
equateStrictness :: FieldStrictness -> FieldStrictness -> Either String ()
equateStrictness fs1 fs2 =
check "constructorStrictness" oldGhcHack fs1 fs2
where
#if MIN_VERSION_template_haskell(2,7,0)
oldGhcHack = id
#else
-- GHC 7.0 and 7.2 didn't have an Unpacked TH constructor, so as a
-- simple workaround, we will treat unpackedAnnot as isStrictAnnot
-- (the closest equivalent).
oldGhcHack fs
| fs == unpackedAnnot = isStrictAnnot
| otherwise = fs
#endif
check :: (Show b, Eq b) => String -> (a -> b) -> a -> a -> Either String ()
check lbl f x y
| f x == f y = Right ()
| otherwise = Left (lbl ++ ":\n\n" ++ show (f x) ++ "\n\n" ++ show (f y))
-- If on a recent-enough version of Template Haskell, construct a kind variable.
-- Otherwise, default to starK.
varKCompat :: Name -> Kind
#if MIN_VERSION_template_haskell(2,8,0)
varKCompat = VarT
#else
varKCompat _ = starK
#endif