packages feed

effectful-0.0.0.0: src/Effectful/Internal/Env.hs

{-# OPTIONS_HADDOCK not-home #-}
-- | The enviroment for 'Effective.Internal.Monad.Eff'.
--
-- This module is intended for internal use only, and may change without warning
-- in subsequent releases.
module Effectful.Internal.Env
  ( Env

  -- * Safe operations
  , emptyEnv
  , cloneEnv
  , sizeEnv
  , takeLastEnv
  , getEnv
  , checkSizeEnv

  -- * Extending and shrinking
  , unsafeReplaceEnv
  , unsafeConsEnv
  , unsafeAppendEnv
  , unsafeTrimEnv

  -- * Data retrieval and update
  , unsafePutEnv
  , unsafeModifyEnv
  , unsafeStateEnv
  ) where

import Control.Monad
import Control.Monad.Primitive
import Data.IORef
import Data.Primitive.SmallArray
import GHC.Exts (Any)
import GHC.Stack
import Unsafe.Coerce

import Effectful.Internal.Has

type role Env nominal

-- | A mutable, extensible record indexed by effect data types.
newtype Env (es :: [Effect]) = Env (IORef EnvRef)

data EnvRef = EnvRef Int (SmallMutableArray RealWorld Any)

-- | Create an empty environment.
emptyEnv :: IO (Env '[])
emptyEnv = fmap Env . newIORef . EnvRef 0
  =<< newSmallArray 0 (error "undefined field")

-- | Clone the environment.
cloneEnv :: Env es -> IO (Env es)
cloneEnv (Env ref) = do
  EnvRef n es <- readIORef ref
  fmap Env . newIORef . EnvRef n
    =<< cloneSmallMutableArray es 0 (sizeofSmallMutableArray es)

-- | Get the current size of the environment.
sizeEnv :: Env es -> IO Int
sizeEnv (Env ref) = do
  EnvRef n _ <- readIORef ref
  pure n

-- | Take last @k@ values from the top of the environment.
takeLastEnv :: HasCallStack => Int -> Env es0 -> IO (Env es)
takeLastEnv k (Env ref) = do
  EnvRef n es <- readIORef ref
  if k > n
    then error $ "k (" ++ show k ++ ") > n (" ++ show n ++ ")"
    else fmap Env . newIORef . EnvRef k =<< cloneSmallMutableArray es (n - k) k

-- | Extract a specific data type from the environment.
getEnv :: forall e es. (HasCallStack, e :> es) => Env es -> IO e
getEnv (Env ref) = do
  EnvRef n es <- readIORef ref
  fromAny <$> readSmallArray es (ixEnv @e @es n)

-- | Check that the size of the environment is the same as the expected value.
checkSizeEnv :: HasCallStack => Int -> Env es -> IO ()
checkSizeEnv k (Env ref) = do
  EnvRef n _ <- readIORef ref
  when (k /= n) . error $ "k (" ++ show k ++ ") /= n (" ++ show n ++ ")"

----------------------------------------
-- Extending and shrinking

-- | Replace the first argument with the second one in place.
unsafeReplaceEnv :: HasCallStack => Env es -> Env es -> IO ()
unsafeReplaceEnv (Env ref0) (Env ref1) = writeIORef ref0 =<< readIORef ref1

-- | Extend the environment with a new data type in place.
unsafeConsEnv :: HasCallStack => e -> Env es -> IO (Env (e : es))
unsafeConsEnv e (Env ref) = do
  EnvRef n es0 <- readIORef ref
  let len0 = sizeofSmallMutableArray es0
  case n `compare` len0 of
    GT -> error $ "n (" ++ show n ++ ") > len0 (" ++ show len0 ++ ")"
    LT -> do
      e `seq` writeSmallArray es0 n (toAny e)
      writeIORef ref $! EnvRef (n + 1) es0
    EQ -> do
      let len = doubleCapacity len0
      es <- newSmallArray len (error "undefined field")
      copySmallMutableArray es 0 es0 0 len0
      e `seq` writeSmallArray es n (toAny e)
      writeIORef ref $! EnvRef (n + 1) es
  pure $ Env ref
{-# NOINLINE unsafeConsEnv #-}

-- | Extend the first environment with the second one in place.
unsafeAppendEnv :: HasCallStack => Env es0 -> Env es1 -> IO (Env es)
unsafeAppendEnv (Env ref0) (Env ref1) = do
  EnvRef n0 es0 <- readIORef ref0
  EnvRef n1 es1 <- readIORef ref1
  let n = n0 + n1
  if n <= sizeofSmallMutableArray es0
    then do
      copySmallMutableArray es0 n0 es1 0 n1
      writeIORef ref0 $! EnvRef n es0
      pure $ Env ref0
    else do
      es <- newSmallArray n (error "undefined field")
      copySmallMutableArray es 0  es0 0 n0
      copySmallMutableArray es n0 es1 0 n1
      writeIORef ref0 $! EnvRef n es
      pure $ Env ref0
{-# NOINLINE unsafeAppendEnv #-}

-- | Trim the environment to the given size in place.
unsafeTrimEnv :: HasCallStack => Int -> Env es -> IO (Env es0)
unsafeTrimEnv k (Env ref) = do
  EnvRef n es <- readIORef ref
  if k > n
    then error $ "k (" ++ show k ++ ") > n (" ++ show n ++ ")"
    else do
      overwrite es k (n - k)
      writeIORef ref $! EnvRef k es
      pure $ Env ref
  where
    overwrite es base = \case
      0 -> pure ()
      i -> do
        writeSmallArray es (base + i - 1) (error "undefined field")
        overwrite es base (i - 1)
{-# NOINLINE unsafeTrimEnv #-}

----------------------------------------
-- Data retrieval and update

-- | Replace the data type in the environment with a new value in place.
unsafePutEnv
  :: forall e es. (HasCallStack, e :> es)
  => e
  -> Env es
  -> IO ()
unsafePutEnv e (Env ref) = do
  EnvRef n es <- readIORef ref
  e `seq` writeSmallArray es (ixEnv @e @es n) (toAny e)

-- | Modify the data type in the environment in place.
unsafeModifyEnv
  :: forall e es. (HasCallStack, e :> es)
  => (e -> e)
  -> Env es
  -> IO ()
unsafeModifyEnv f (Env ref) = do
  EnvRef n es <- readIORef ref
  let i = ixEnv @e @es n
  e <- f . fromAny <$> readSmallArray es i
  e `seq` writeSmallArray es i (toAny e)

-- | Modify the data type in the enviroment in place and return a value.
unsafeStateEnv
  :: forall e es a. (HasCallStack, e :> es)
  => (e -> (a, e))
  -> Env es -> IO a
unsafeStateEnv f (Env ref) = do
  EnvRef n es <- readIORef ref
  let i = ixEnv @e @es n
  (a, e) <- f . fromAny <$> readSmallArray es i
  e `seq` writeSmallArray es i (toAny e)
  pure a

----------------------------------------
-- Internal helpers

doubleCapacity :: Int -> Int
doubleCapacity n = max 1 n * 2

toAny :: a -> Any
toAny = unsafeCoerce

fromAny :: Any -> a
fromAny = unsafeCoerce