packages feed

creditmonad-1.1.0: src/Test/Credit.hs

{-# LANGUAGE DerivingStrategies, FunctionalDependencies, AllowAmbiguousTypes, TypeApplications, ScopedTypeVariables #-}

module Test.Credit
  (
  -- * Common Time-Complexity Functions
    Size, logstar, log2, linear
  -- * Execution Traces for Testing
  , Strategy(..), genExecutionTrace
  -- * Running Data Structures on Execution Traces
  , DataStructure(..), runTree, runTreeTrace
  -- * Testing Data Structures on Execution Traces
  , checkCredits, checkCreditsTrace
  ) where

import Data.Either
import Control.Monad.State
import Data.Tree
import Test.QuickCheck
import Prettyprinter
import Prettyprinter.Render.String

import Control.Monad.Credit.Base
import Control.Monad.Credit.CreditM

path :: Arbitrary a => Int -> Tree a -> Gen (Tree a)
path 0 end = pure end
path n end = Node <$> arbitrary <*> ((:[]) <$> path (n-1) end)

path' :: Arbitrary a => Int -> Gen (Tree a)
path' n = path n =<< Node <$> arbitrary <*> pure []

bloom :: Arbitrary a => Gen (Tree a)
bloom = sized $ \n -> do
  m <- chooseInt (0, n)
  k <- chooseInt (0, m `div` 2)
  ts <- mapM (\_ -> path' (m `div` k)) [1..k]
  path (n - m) =<< Node <$> arbitrary <*> pure ts

pennant :: Arbitrary a => Gen (Tree a)
pennant = sized go
  where
    go n | n <= 1 = path' 0
    go n = do
      k <- chooseInt (n `div` 3, 2 * (n `div` 3))
      ts <- mapM (\_ -> go ((n - k - 1) `div` 2)) [1..2]
      path k =<< Node <$> arbitrary <*> pure ts

newtype SeqTree a = SeqTree { fromSeqTree :: Tree a }
  deriving (Eq, Ord, Show)

instance Arbitrary a => Arbitrary (SeqTree a) where
  arbitrary = sized $ \n -> SeqTree <$> path' n

newtype BloomTree a = BloomTree { fromBloomTree :: Tree a }
  deriving (Eq, Ord, Show)

instance Arbitrary a => Arbitrary (BloomTree a) where
  arbitrary = BloomTree <$> bloom

newtype PennantTree a = PennantTree { fromPennantTree :: Tree a }
  deriving (Eq, Ord, Show)

instance Arbitrary a => Arbitrary (PennantTree a) where
  arbitrary = PennantTree <$> pennant

newtype PrsTree a = PrsTree { fromPrsTree :: Tree a }
  deriving (Eq, Ord, Show)

instance Arbitrary a => Arbitrary (PrsTree a) where
  arbitrary = PrsTree <$> arbitrary

data Strategy = Path | Bloom | Pennant | Random
  deriving (Eq, Ord, Show)

genExecutionTrace :: Arbitrary op => Strategy -> Gen (Tree op)
genExecutionTrace Path = fromSeqTree <$> arbitrary
genExecutionTrace Bloom = fromBloomTree <$> arbitrary
genExecutionTrace Pennant = fromPennantTree <$> arbitrary
genExecutionTrace Random = fromPrsTree <$> arbitrary

newtype Size = Size Int
  deriving (Eq, Ord, Show)
  deriving newtype (Num, Enum, Real, Integral, Pretty)

instance Monad m => MemoryCell m Size where
  prettyCell (Size i) = pure $ mkMCell (show i) []

logstar :: Size -> Credit
logstar (Size n) = fromInteger $ go n 0
  where
    go n acc | n < 2 = acc
    go n acc = go (log2 n 0) (acc + 1)

    log2 n acc | n < 2 = acc
    log2 n acc = log2 (n `div` 2) (acc + 1)

log2 :: Size -> Credit
log2 (Size n) = fromInteger $ go n 0
  where
    go n acc | n < 2 = acc
    go n acc = go (n `div` 2) (acc + 1)

linear :: Size -> Credit
linear (Size n) = fromInteger $ toInteger n

class (Arbitrary op, Show op) => DataStructure t op | t -> op where
  cost :: Size -> op -> Credit
  -- ^ Given a size and an operation, return the cost of the operation.
  -- This function can not inspect the internal state of the data structure.
  create :: MonadLazy m => m (t m)
  -- ^ create a new instance of the data structure.
  -- We allow the computation to be lazy, since lazy data structures
  -- often contain thunks even if they contain no elements.
  -- The create data structure is assumed to have size zero.
  perform :: MonadInherit m => Size -> t m -> op -> m (Size, t m)
  -- ^ Given a data structure, its size, and an operation,
  -- return the updated size and data structure.
  -- We allow the size to depend on the internal state of the data structure,
  -- since some operations, like insertions into a binary search tree, might
  -- return different sizes depending on whether a new element is already present.

-- | Evaluate an execution trace of operations on the given data structure
-- using the credit monad. Returns either an error or unit if the evaluation succeeded.
runTree :: forall t op. DataStructure t op => Tree op -> Either Error ()
runTree tree = runCreditM 0 (create @t >>= flip (go 0) tree)
  where
    go :: forall s t op. DataStructure t op => Size -> t (CreditM s) -> Tree op -> CreditM s ()
    go sz a (Node op ts) = do
      let cr = cost @t sz op
      resetCurrentThunk cr
      (sz, a) <- perform sz a op
      mapM_ (go sz a) ts

isPersistent :: Tree a -> Bool
isPersistent (Node _ ts) = length ts > 1 || any isPersistent ts

-- | Test the given data structure in the credit monad using the given strategy.
-- This property only reports if evaluation succeeded or not.
checkCredits :: forall t op. DataStructure t op => Strategy -> Property
checkCredits strat =
  forAllShrink (genExecutionTrace strat) shrink $ \t ->
    classify (isPersistent t) "persistent" $
      isRight $ runTree @t t

data RoseZipper a = Root | Branch a [Tree a] (RoseZipper a)
  deriving (Eq, Ord, Show)

up :: RoseZipper a -> RoseZipper a
up (Branch x ls (Branch y rs z)) = Branch y (Node x (reverse ls) : rs) z
up z = z

extend :: String -> RoseZipper String -> RoseZipper String
extend s (Branch x ls z) = Branch (x ++ s) ls z
extend _ Root = Root

extract :: RoseZipper a -> Tree a
extract (Branch x ls Root) = Node x (reverse ls)
extract z = extract (up z)

-- | If each node has only a single child, flatten the tree
-- by making all elements children of the root.
-- This improves the readability of the tree when printed.
-- Otherwise, return the original tree.
flattenTree :: Tree a -> Tree a
flattenTree t = case go t of
  Just (x:xs) -> Node x (map (\x -> Node x []) xs)
  _ -> t
  where
    go (Node x []) = Just [x]
    go (Node x [t]) = (x :) <$> go t
    go (Node _ _) = Nothing

showState :: (Either Error (), RoseZipper String) -> String
showState (Left e, t) = drawTree $ flattenTree $ extract $ extend (show $ pretty e) t
showState (Right (), t) = drawTree $ flattenTree $ extract t

type M s = CreditT s (State (RoseZipper String))

-- | Evaluate an execution trace of operations on the given data structure
-- using the credit monad. Returns a pretty-printed string of the execution trace
-- annotated with the internal state of the data structure at each step.
runTreeTrace :: forall t op. (MemoryStructure t, DataStructure t op) => Tree op -> String
runTreeTrace tree = showState $ runState (runCreditT 0 (create @t >>= flip (go 0) tree)) Root
  where
    go :: forall s t op. (MemoryStructure t, DataStructure t op) => Size -> t (M s) -> Tree op -> M s ()
    go sz a (Node op ts) = do
      let cr = cost @t sz op
      resetCurrentThunk cr
      lift $ modify' (Branch (show op ++ ": ") []) 
      (sz, a) <- perform sz a op
      mem <- prettyStructure a
      let s = renderString $ layoutSmart (defaultLayoutOptions { layoutPageWidth = Unbounded }) $ nest 2 $ pretty $ mem
      lift $ modify' (extend s)
      mapM_ (go sz a) ts
      lift $ modify' up

-- | Test the given data structure in the credit monad using the given strategy.
-- If evaluation fails, this property prints the execution trace
-- annotated with the internal state of the data structure at each step.
checkCreditsTrace :: forall t op. (MemoryStructure t, DataStructure t op) => Strategy -> Property
checkCreditsTrace strat =
  forAllShrinkShow (genExecutionTrace strat) shrink (\t -> runTreeTrace @t t) $ \t ->
    classify (isPersistent t) "persistent" $
      isRight $ runTree @t t