hls-tactics-plugin-0.5.1.0: src/Ide/Plugin/Tactic/KnownStrategies/QuickCheck.hs
{-# LANGUAGE ViewPatterns #-}
{-# LANGUAGE LambdaCase #-}
module Ide.Plugin.Tactic.KnownStrategies.QuickCheck where
import Control.Monad.Except (MonadError(throwError))
import Data.Bool (bool)
import Data.List (partition)
import DataCon ( DataCon, dataConName )
import Development.IDE.GHC.Compat (HsExpr, GhcPs, noLoc)
import GHC.Exts ( IsString(fromString) )
import GHC.List ( foldl' )
import GHC.SourceGen (int)
import GHC.SourceGen.Binds ( match, valBind )
import GHC.SourceGen.Expr ( case', lambda, let' )
import GHC.SourceGen.Overloaded ( App((@@)), HasList(list) )
import GHC.SourceGen.Pat ( conP )
import Ide.Plugin.Tactic.CodeGen
import Ide.Plugin.Tactic.Judgements (jGoal)
import Ide.Plugin.Tactic.Machinery (tracePrim)
import Ide.Plugin.Tactic.Types
import OccName (occNameString, mkVarOcc, HasOccName(occName) )
import Refinery.Tactic (goal, rule )
import TyCon (tyConName, TyCon, tyConDataCons )
import Type ( splitTyConApp_maybe )
import Data.Generics (mkQ, everything)
------------------------------------------------------------------------------
-- | Known tactic for deriving @arbitrary :: Gen a@. This tactic splits the
-- type's data cons into terminal and inductive cases, and generates code that
-- produces a terminal if the QuickCheck size parameter is <=1, or any data con
-- otherwise. It correctly scales recursive parameters, ensuring termination.
deriveArbitrary :: TacticsM ()
deriveArbitrary = do
ty <- jGoal <$> goal
case splitTyConApp_maybe $ unCType ty of
Just (gen_tc, [splitTyConApp_maybe -> Just (tc, apps)])
| occNameString (occName $ tyConName gen_tc) == "Gen" -> do
rule $ \_ -> do
let dcs = tyConDataCons tc
(terminal, big) = partition ((== 0) . genRecursiveCount)
$ fmap (mkGenerator tc apps) dcs
terminal_expr = mkVal "terminal"
oneof_expr = mkVal "oneof"
pure
( tracePrim "deriveArbitrary"
, noLoc $
let' [valBind (fromString "terminal") $ list $ fmap genExpr terminal] $
appDollar (mkFunc "sized") $ lambda [bvar' (mkVarOcc "n")] $
case' (infixCall "<=" (mkVal "n") (int 1))
[ match [conP (fromString "True") []] $
oneof_expr @@ terminal_expr
, match [conP (fromString "False") []] $
appDollar oneof_expr $
infixCall "<>"
(list $ fmap genExpr big)
terminal_expr
]
)
_ -> throwError $ GoalMismatch "deriveArbitrary" ty
------------------------------------------------------------------------------
-- | Helper data type for the generator of a specific data con.
data Generator = Generator
{ genRecursiveCount :: Integer
, genExpr :: HsExpr GhcPs
}
------------------------------------------------------------------------------
-- | Make a 'Generator' for a given tycon instantiated with the given @[Type]@.
mkGenerator :: TyCon -> [Type] -> DataCon -> Generator
mkGenerator tc apps dc = do
let dc_expr = var' $ occName $ dataConName dc
args = dataConInstOrigArgTys' dc apps
num_recursive_calls = sum $ fmap (bool 0 1 . doesTypeContain tc) args
mkArbitrary = mkArbitraryCall tc num_recursive_calls
Generator num_recursive_calls $ case args of
[] -> mkFunc "pure" @@ dc_expr
(a : as) ->
foldl'
(infixCall "<*>")
(infixCall "<$>" dc_expr $ mkArbitrary a)
(fmap mkArbitrary as)
------------------------------------------------------------------------------
-- | Check if the given 'TyCon' exists anywhere in the 'Type'.
doesTypeContain :: TyCon -> Type -> Bool
doesTypeContain recursive_tc =
everything (||) $ mkQ False (== recursive_tc)
------------------------------------------------------------------------------
-- | Generate the correct sort of call to @arbitrary@. For recursive calls, we
-- need to scale down the size parameter, either by a constant factor of 1 if
-- it's the only recursive parameter, or by @`div` n@ where n is the number of
-- recursive parameters. For all other types, just call @arbitrary@ directly.
mkArbitraryCall :: TyCon -> Integer -> Type -> HsExpr GhcPs
mkArbitraryCall recursive_tc n ty =
let arbitrary = mkFunc "arbitrary"
in case doesTypeContain recursive_tc ty of
True ->
mkFunc "scale"
@@ bool (mkFunc "flip" @@ mkFunc "div" @@ int n)
(mkFunc "subtract" @@ int 1)
(n == 1)
@@ arbitrary
False -> arbitrary