effectful-2.7.1.0: tests/NonDetTests.hs
module NonDetTests (nonDetTests) where
import Data.IORef.Strict qualified as S
import Data.Primitive.PrimArray
import Test.Tasty
import Test.Tasty.HUnit
import Effectful
import Effectful.Dispatch.Dynamic
import Effectful.Dispatch.Static
import Effectful.Dispatch.Static.Primitive
import Effectful.Internal.Env qualified as I
import Effectful.Internal.Utils
import Effectful.NonDet
import Effectful.Reader.Static
import Effectful.State.Dynamic
import Utils qualified as U
nonDetTests :: TestTree
nonDetTests = testGroup "NonDet"
[ testCaseSteps "empty (left)" $ test_empty leftEmpty
, testCaseSteps "empty (right)" $ test_empty rightEmpty
, testGroup "interaction"
[ testCaseSteps "local state" $ test_state evalStateLocal expectedLocalState
, testCaseSteps "shared state" $ test_state evalStateShared expectedSharedState
]
, testCaseSteps "local mutable state" $ test_mutState expectedLocalState
, testCaseSteps "different handlers are independent" test_independentHandlers
, testCase "stale reference is detected after rollback" test_staleReferenceAfterRollback
]
where
leftEmpty :: NonDet :> es => Eff es Bool
leftEmpty = runReader () (empty >> pure False) <|> runReader () (pure True)
rightEmpty :: NonDet :> es => Eff es Bool
rightEmpty = runReader () (pure True) <|> runReader () (empty >> pure False)
expectedLocalState :: OnEmptyPolicy -> Int
expectedLocalState = \case
OnEmptyKeep -> 7
OnEmptyRollback -> 0
expectedSharedState :: OnEmptyPolicy -> Int
expectedSharedState _ = 7
test_empty
:: Eff [NonDet, IOE] Bool
-> (String -> IO ())
-> Assertion
test_empty test step = runEff $ do
runNonDetBoth test $ \policy result -> do
liftIO . step $ show policy
U.assertEqual "result" (Just True) (dropLeft result)
test_state
:: (forall r. Int -> Eff [State Int, IOE] r -> Eff '[IOE] r)
-> (OnEmptyPolicy -> Int)
-> (String -> IO ())
-> IO ()
test_state evalState expectedState step = runEff $ do
evalState 0 . runNonDetBoth test $ \policy result -> do
liftIO . step $ show policy
U.assertEqual "result" Nothing (dropLeft result)
s <- state @Int $ \s -> (s, 0)
U.assertEqual "state" (expectedState policy) s
where
test :: (NonDet :> es, State Int :> es) => Eff es ()
test = do
modify @Int (+1)
_<- (modify @Int (+2) >> empty) <|> (modify @Int (+4) >> empty)
modify @Int (+8)
test_mutState
:: (OnEmptyPolicy -> Int)
-> (String -> IO ())
-> IO ()
test_mutState expectedState step = runEff $ do
runMutInt 0 . runNonDetBoth test $ \policy result -> do
liftIO . step $ show policy
s <- stateMutInt $ \s -> (s, 0)
U.assertEqual "result" Nothing (dropLeft result)
U.assertEqual "state" (expectedState policy) s
where
test :: (NonDet :> es, MutInt :> es) => Eff es ()
test = do
modifyMutInt (+1)
_<- (modifyMutInt (+2) >> empty) <|> (modifyMutInt (+4) >> empty)
modifyMutInt (+8)
test_independentHandlers :: (String -> IO ()) -> Assertion
test_independentHandlers step = runEff $ do
runNonDetBoth test $ \policy result -> liftIO $ do
step $ show policy
case result of
Left cs -> assertBool "stack trace points to the correct action" $
"outerEmpty" == fst (last $ getCallStack cs)
Right _ -> assertFailure "empty handled by the wrong (inner) handler"
where
test :: NonDet :> es => Eff es (Either CallStack Bool)
test = runOuterEmpty . runNonDet OnEmptyKeep $ outerEmpty <|> pure True
-- | A reference to an effect captured inside a rolled back branch must be
-- detected as out of date even if the storage was grown within the branch, in
-- which case the rollback must not shrink its capacity.
test_staleReferenceAfterRollback :: Assertion
test_staleReferenceAfterRollback = runEff $ do
ref <- liftIO $ S.newIORef Nothing
result <- runNonDet OnEmptyRollback $
(do stale <- captureStaleReference 16
capacity <- getStorageCapacity
liftIO $ S.writeIORef ref $ Just (capacity, stale)
emptyEff)
<|> pure True
U.assertEqual "result" (Just True) (dropLeft result)
liftIO (S.readIORef ref) >>= \case
Nothing -> U.assertFailure "stale reference not captured"
Just (capacity, stale) -> do
-- If the rollback shrunk the capacity of the storage, evaluation of the
-- stale reference below would read out of bounds.
U.assertEqual "capacity" capacity =<< getStorageCapacity
U.assertThrowsErrorCall "stale reference detected" $ liftIO stale
-- | Run a computation under the given number of effect handlers to grow the
-- storage, then capture an unlifted reference to the innermost one.
captureStaleReference :: IOE :> es => Int -> Eff es (IO Int)
captureStaleReference n = evalStateLocal @Int n $
if n <= 0
then withEffToIO SeqUnlift $ \unlift -> pure . unlift $ get @Int
else captureStaleReference (n - 1)
-- | Get the current capacity of the underlying storage of effects.
getStorageCapacity :: Eff es Int
getStorageCapacity = unsafeEff $ \es -> do
I.Storage _ storageData <- S.readIORef es.storage
getSizeofMutablePrimArray storageData.versions
----------------------------------------
-- Helpers
data MutInt :: Effect
type instance DispatchOf MutInt = Static NoSideEffects
newtype instance StaticRep MutInt = MutInt (S.IORef Int)
runMutInt :: Int -> Eff (MutInt : es) a -> Eff es a
runMutInt s action = unsafeEff $ \es -> do
ref <- S.newIORef s
inlineBracket
(consEnv (MutInt ref) relinkMutInt es)
unconsEnv
(unEff action)
where
relinkMutInt :: Relinker StaticRep MutInt
relinkMutInt = Relinker $ \_ (MutInt ref0) -> do
ref <- S.newIORef =<< S.readIORef ref0
pure $ MutInt ref
stateMutInt :: MutInt :> es => (Int -> (a, Int)) -> Eff es a
stateMutInt f = unsafeEff $ \es -> do
MutInt ref <- getEnv es
(r, s) <- f <$> S.readIORef ref
S.writeIORef ref s
pure r
modifyMutInt :: MutInt :> es => (Int -> Int) -> Eff es ()
modifyMutInt f = stateMutInt $ ((),) . f
----
data OuterEmpty :: Effect where
OuterEmpty :: OuterEmpty m a
type instance DispatchOf OuterEmpty = Dynamic
outerEmpty :: (HasCallStack, OuterEmpty :> es) => Eff es a
outerEmpty = send OuterEmpty
runOuterEmpty :: NonDet :> es => Eff (OuterEmpty : es) a -> Eff es a
runOuterEmpty = interpret_ $ \case
OuterEmpty -> emptyEff
----
dropLeft :: Either e a -> Maybe a
dropLeft = either (const Nothing) Just
runNonDetBoth
:: Eff (NonDet : es) a
-> (OnEmptyPolicy -> Either CallStack a -> Eff es ())
-> Eff es ()
runNonDetBoth m k = do
k OnEmptyKeep =<< runNonDet OnEmptyKeep m
k OnEmptyRollback =<< runNonDet OnEmptyRollback m