monad-effect-logging-0.3.0.0: src/Module/Logging/TraceId/XorShiftRNG.hs
-- | This module provides an implementation of a XorShift random number generator (RNG)
-- and a monad transformer for using it in computations.
--
-- It is lightweight and fast.
module Module.Logging.TraceId.XorShiftRNG
( -- * RNG
RNG, newRNG, uniformDoubleFromRNG, uniformWord64FromRNG
-- * RNGT
, newWord64IO
-- * MonadUniformValue
, Word64, nextWord64, word64ToDouble, splitSeeds, mix64
) where
import Foreign
import System.Clock
import Control.Monad.IO.Class
import GHC.Exts (RealWorld)
import qualified Data.Primitive.PrimArray as P
-- | The RNG state, which is a foreign pointer to a 64-bit unsigned integer.
newtype RNG = RNG (P.MutablePrimArray RealWorld Word64)
--(ForeignPtr Word64)
-- | Working with seeds
nextWord64 :: Word64 -> Word64
nextWord64 !x0 =
let !x1 = x0 `xor` (x0 `shiftL` 13)
!x2 = x1 `xor` (x1 `shiftR` 7)
in (x2 `xor`) $! (x2 `shiftL` 17)
{-# INLINE nextWord64 #-}
-- SplitMix64 "avalanche" ------------------------------------------------------
mix64 :: Word64 -> Word64
mix64 !z0 =
let !z1 = (z0 `xor` (z0 `shiftR` 30)) * 0xbf58476d1ce4e5b9
!z2 = (z1 `xor` (z1 `shiftR` 27)) * 0x94d049bb133111eb
in z2 `xor` (z2 `shiftR` 31)
{-# INLINE mix64 #-}
-- -----------------------------------------------------------------------------
-- | Turn one seed into @n@ independent seeds suitable for different threads.
--
-- * Deterministic: same parent → same list.
-- * Fast: O(n), two multiplies + a few shifts per child.
-- * Guarantees no child gets 0 (bad for XorShift).
--
splitSeeds :: [a] -> Word64 -> [(a, Word64)]
splitSeeds n parent =
[ (a, forceNonZero . mix64 $ parent + fromIntegral i * 0x9e3779b97f4a7c15)
| (i, a) <- zip [0::Int ..] n
]
where
-- XorShift fails when state == 0 or 0x8000000000000000.
forceNonZero 0 = 0x6a09e667f3bcc908 -- arbitrary non-zero constant
forceNonZero w = w
{-# INLINE splitSeeds #-}
newWord64IO :: MonadIO m => m Word64
newWord64IO = do
TimeSpec s n <- liftIO $ getTime Monotonic
pure $! fromIntegral s `xor` fromIntegral n
{-# INLINE newWord64IO #-}
word64ToDouble :: Word64 -> Double
word64ToDouble !x =
let !d = fromIntegral (x `shiftR` 11) * 1.1102230246251565e-16
-- 1/2^53 = 1.110…e-16
in d
{-# INLINE word64ToDouble #-}
-- | Create a new RNG instance seeded with the current time.
newRNG :: IO RNG
newRNG = do
marr <- P.newPrimArray 1 -- Create a mutable array of size 1
TimeSpec s n <- getTime Monotonic -- Get current time in seconds and nanoseconds
let !seed = fromIntegral s `xor` fromIntegral n -- Combine seconds and nanoseconds
P.writePrimArray marr 0 seed -- Write the seed into the array with 0 index
pure $ RNG marr
{-# INLINE newRNG #-}
-- | Generate a uniform random double in the range [0, 1) using the XorShift algorithm.
-- updates the RNG state.
uniformDoubleFromRNG :: RNG -> IO Double
uniformDoubleFromRNG rng = do
x3 <- uniformWord64FromRNG rng
-- top 53 bits → [0,1)
pure $! fromIntegral (x3 `shiftR` 11) * 1.1102230246251565e-16
{-# INLINE uniformDoubleFromRNG #-}
-- | Generate a uniform Word64 using the XorShift algorithm.
-- updates the RNG state.
uniformWord64FromRNG :: RNG -> IO Word64
uniformWord64FromRNG (RNG marr) = do
!x0 <- P.readPrimArray marr 0 -- Read the current state
let !x1 = x0 `xor` (x0 `shiftL` 13)
!x2 = x1 `xor` (x1 `shiftR` 7)
!x3 = x2 `xor` (x2 `shiftL` 17)
P.writePrimArray marr 0 x3 -- Update the state
pure $! x3
{-# INLINE uniformWord64FromRNG #-}