packages feed

antigen-0.4.0.0: src/Test/AntiGen/Internal.hs

{-# LANGUAGE CPP #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE UndecidableInstances #-}

module Test.AntiGen.Internal (
  AntiGen,
  ZapResult (..),
  prettyZapResult,
  (|!),
  (#!),
  zapAntiGen,
  zapAntiGenResult,
  runAntiGen,
  evalToPartial,
  evalPartial,
  countDecisionPoints,
  zapAt,
  withAnnotation,
  scaleWeight,
  reweigh,
) where

import Control.Monad ((<=<))
import Control.Monad.Free.Church (F (..), MonadFree (..))
import Data.Foldable (toList)
import Data.List.NonEmpty (NonEmpty)
import qualified Data.List.NonEmpty as NE
import Data.Sequence (Seq (..))
import qualified Data.Sequence as Seq
import Data.Text (Text)
import qualified Data.Text as T
#if MIN_VERSION_QuickCheck(2,18,0)
import System.Random (SplitGen (..))
#else
import System.Random (RandomGen (split))
#endif
import Test.QuickCheck (getSize)
import Test.QuickCheck.Gen (Gen (..))
import Test.QuickCheck.GenT (MonadGen (..))
import Test.QuickCheck.Random (QCGen)

#if !MIN_VERSION_QuickCheck(2,18,0)
splitGen :: RandomGen g => g -> (g, g)
splitGen = split
#endif

data BiGen next where
  BiGen :: Gen t -> Maybe (Gen t) -> Float -> (t -> next) -> BiGen next
  Annotate :: Text -> AntiGen t -> (t -> next) -> BiGen next
  Reweigh :: (Float -> Float) -> AntiGen t -> (t -> next) -> BiGen next

instance Functor BiGen where
  fmap f (BiGen p n w c) = BiGen p n w $ f . c
  fmap f (Annotate ann inner c) = Annotate ann inner $ f . c
  fmap f (Reweigh g inner c) = Reweigh g inner $ f . c

newtype AntiGen a = AntiGen (F BiGen a)
  deriving (Functor, Applicative, Monad, MonadFree BiGen)

mapGen :: (forall x. Gen x -> Gen x) -> AntiGen a -> AntiGen a
mapGen f (AntiGen (F m)) = m pure $ \case
  BiGen pos neg w c -> wrap $ BiGen (f pos) (f <$> neg) w c
  Annotate ann inner c -> wrap $ Annotate ann (mapGen f inner) c
  Reweigh g inner c -> wrap $ Reweigh g (mapGen f inner) c

instance MonadGen AntiGen where
  liftGen g = AntiGen $ F $ \p b -> b $ BiGen g Nothing 1 p
  variant n = mapGen (variant n)
  sized f = wrap $ BiGen (f <$> getSize) Nothing 1 id
  resize n m = mapGen (resize n) m
  choose = liftGen . choose

mkAntiGen :: Gen a -> Gen a -> AntiGen a
mkAntiGen active alt =
  AntiGen $ F $ \p b -> b $ BiGen (p <$> active) (Just $ p <$> alt) 1 id

-- | Create a negatable generator by providing a positive and a negative
-- generator
(|!) :: Gen a -> Gen a -> AntiGen a
(|!) = mkAntiGen

infixl 6 |!

-- | Postfix annotation operator. Annotates an 'AntiGen' with a label that
-- will be included in 'ZapResult' when this generator is zapped.
--
-- @
-- myGen = positive |! negative #! "sign"
-- @
(#!) :: AntiGen a -> Text -> AntiGen a
(#!) = flip withAnnotation

infixl 5 #!

-- | Wrap an AntiGen with an annotation
withAnnotation :: Text -> AntiGen a -> AntiGen a
withAnnotation ann inner = wrap $ Annotate ann inner pure

scaleWeight :: (Float -> Float) -> AntiGen a -> AntiGen a
scaleWeight rw inner = wrap $ Reweigh rw inner pure

reweigh :: Float -> AntiGen a -> AntiGen a
reweigh w = scaleWeight $ const w

data DecisionPoint next where
  DecisionPoint ::
    { dpValue :: t
    , dpActiveGen :: Gen t
    , dpAlternativeGen :: Maybe (Gen t)
    , dpAnnotation :: Seq Text
    , dpWeight :: Float
    , dpContinuation :: t -> next
    } ->
    DecisionPoint next

instance Functor DecisionPoint where
  fmap f (DecisionPoint v p n a w c) = DecisionPoint v p n a w $ f . c

continue :: DecisionPoint next -> next
continue DecisionPoint {..} = dpContinuation dpValue

newtype PartialGen a = PartialGen (F DecisionPoint a)
  deriving (Functor, Applicative, Monad, MonadFree DecisionPoint)

evalToPartial :: AntiGen a -> Gen (PartialGen a)
evalToPartial (AntiGen (F m)) = MkGen $ \qcGen sz ->
  m kp kf Seq.empty id qcGen sz
  where
    kp :: a -> Seq Text -> (Float -> Float) -> QCGen -> Int -> PartialGen a
    kp x _ _ _ _ = pure x

    kf ::
      BiGen (Seq Text -> (Float -> Float) -> QCGen -> Int -> PartialGen a) ->
      Seq Text ->
      (Float -> Float) ->
      QCGen ->
      Int ->
      PartialGen a
    kf (BiGen activeGen altGen w cont) path rw qcGen sz =
      let (qcGenValue, qcGenCont) = splitGen qcGen
          value = unGen activeGen qcGenValue sz
       in wrap $
            DecisionPoint
              { dpValue = value
              , dpActiveGen = activeGen
              , dpAlternativeGen = altGen
              , dpAnnotation = path
              , dpWeight = rw w
              , dpContinuation = \v -> cont v path rw qcGenCont sz
              }
    kf (Annotate ann (AntiGen (F inner)) cont) path rw qcGen sz = do
      let (qcGenInner, qcGenCont) = splitGen qcGen
      t <- inner kp kf (path :|> ann) rw qcGenInner sz
      cont t path rw qcGenCont sz
    kf (Reweigh g (AntiGen (F inner)) cont) path rw qcGen sz = do
      let (qcGenInner, qcGenCont) = splitGen qcGen
      t <- inner kp kf path (rw . g) qcGenInner sz
      cont t path rw qcGenCont sz

countDecisionPoints :: PartialGen a -> Int
countDecisionPoints (PartialGen (F m)) = m (const 0) $ \dp@DecisionPoint {..} ->
  case dpAlternativeGen of
    Just _ -> succ $ continue dp
    Nothing -> continue dp

totalWeight :: PartialGen a -> Float
totalWeight (PartialGen (F m)) = m (const 0) $ \dp@DecisionPoint {..} ->
  case dpAlternativeGen of
    Just _ -> continue dp + dpWeight
    Nothing -> continue dp

data ZapResult a = ZapResult
  { zrValue :: a
  , zrAnnotation :: [NonEmpty Text]
  , zrZapped :: Int
  }
  deriving (Functor)

instance Semigroup (ZapResult a) where
  ZapResult v a1 z1 <> ZapResult _ a2 z2 =
    ZapResult v (a1 <> a2) (z1 + z2)

-- | Pretty print the annotation paths from a ZapResult
prettyZapResult :: ZapResult a -> Text
prettyZapResult ZapResult {..} =
  T.unlines $
    [ "Zapped " <> T.pack (show zrZapped) <> " decision points"
    ]
      <> case zrAnnotation of
        [] -> []
        anns -> "Annotations:" : map prettyPath anns
  where
    prettyPath :: NonEmpty Text -> Text
    prettyPath path = "  - " <> T.intercalate "." (NE.toList path)

zapAt :: Float -> PartialGen a -> Gen (ZapResult (PartialGen a))
zapAt cutoffWeight (PartialGen (F m)) = MkGen $ \qcGen sz ->
  m kp (kf qcGen sz) $ Just cutoffWeight
  where
    kp :: a -> Maybe Float -> ZapResult (PartialGen a)
    kp x _ = ZapResult (pure x) mempty 0

    kf ::
      QCGen ->
      Int ->
      DecisionPoint (Maybe Float -> ZapResult (PartialGen a)) ->
      Maybe Float ->
      ZapResult (PartialGen a)
    kf qcGen sz DecisionPoint {..} mn =
      case dpAlternativeGen of
        Just altGen
          | Just n <- mn
          , n < dpWeight ->
              ZapResult
                { zrValue =
                    let newValue = unGen altGen qcGen sz
                     in wrap $
                          DecisionPoint
                            { dpValue = newValue
                            , dpActiveGen = altGen
                            , dpAlternativeGen = Nothing
                            , dpContinuation = \v -> zrValue (dpContinuation v Nothing)
                            , ..
                            }
                , zrAnnotation = toList (NE.nonEmpty (toList dpAnnotation))
                , zrZapped = 1
                }
        _ ->
          -- Preserve tree structure
          let n' = case dpAlternativeGen of
                Just _ -> (\x -> x - dpWeight) <$> mn
                Nothing -> mn
              restResult = dpContinuation dpValue n'
           in ZapResult
                { zrValue =
                    wrap $
                      DecisionPoint
                        { dpContinuation = \v -> zrValue (dpContinuation v n')
                        , ..
                        }
                , zrAnnotation = zrAnnotation restResult
                , zrZapped = zrZapped restResult
                }

zap :: PartialGen a -> Gen (ZapResult (PartialGen a))
zap p =
  let w = totalWeight p
   in if w == 0
        then pure $ ZapResult p [] 0
        else (`zapAt` p) =<< choose (0, w)

zapNTimes :: Int -> PartialGen a -> Gen (ZapResult a)
zapNTimes n x
  | n <= 0 = pure $ ZapResult (evalPartial x) [] 0
  | otherwise = do
      zapResult <- zap x
      rest <- zapNTimes (pred n) $ zrValue zapResult
      pure $ rest <> fmap evalPartial zapResult

evalPartial :: PartialGen a -> a
evalPartial (PartialGen (F m)) = m id continue

-- | Create a negative generator from an `AntiGen` by introducing at most
-- `n` mistakes. If there are no negatable generators in the `AntiGen`, it will
-- return a positive generator. Also if the number of negatable generators in
-- the `AntiGen` is lower than `n`, then the number of negations will be less
-- than `n`.
zapAntiGen :: Int -> AntiGen a -> Gen a
zapAntiGen n = fmap zrValue . zapAntiGenResult n

-- | Create a negative generator from an `AntiGen` by introducing at most
-- `n` mistakes.
zapAntiGenResult :: Int -> AntiGen a -> Gen (ZapResult a)
zapAntiGenResult n = zapNTimes n <=< evalToPartial

-- | Create a positive generator from the provided `AntiGen`.
runAntiGen :: AntiGen a -> Gen a
runAntiGen ag = evalPartial <$> evalToPartial ag