ppad-random-0.1.0: lib/Crypto/PRNG.hs
{-# OPTIONS_HADDOCK prune #-}
{-# LANGUAGE BangPatterns #-}
-- |
-- Module: Crypto.PRNG
-- Copyright: (c) 2026 Jared Tobin
-- License: MIT
-- Maintainer: Jared Tobin <jared@ppad.tech>
--
-- A fast, non-cryptographic pseudorandom number generator:
-- [xoshiro256++](https://prng.di.unimi.it/), by David Blackman and
-- Sebastiano Vigna.
--
-- The generator is mutable, living in any 'PrimMonad' (e.g. 'IO' or
-- 'ST s'), and is not thread-safe. Use one generator per thread,
-- deriving non-overlapping streams via 'copy' and 'jump' or
-- 'long_jump'.
--
-- Its output is predictable from a small sample; use "Crypto.CSPRNG"
-- for anything that must be unpredictable. "Crypto.PRNG.Pure" offers
-- the same generator, with the same output, as an immutable value.
module Crypto.PRNG (
-- * Generator
Gen
, new
, new_system
, from_state
-- * Output
, word64
, word64_range
, int_range
, double
-- * Streams
, copy
, jump
, long_jump
) where
import Control.Monad.Primitive (PrimMonad, PrimState)
import Control.Monad.ST (ST)
import qualified Crypto.Range as Range
import Crypto.Xoshiro (S(..))
import qualified Crypto.Xoshiro as X
import Data.Bits ((.>>.))
import qualified Data.Primitive.PrimArray as PA
import Data.Word (Word64)
import GHC.Exts (RealWorld)
-- utils ----------------------------------------------------------------------
fi :: (Integral a, Num b) => a -> b
fi = fromIntegral
{-# INLINE fi #-}
-- generator ------------------------------------------------------------------
-- | A xoshiro256++ generator.
newtype Gen s = Gen (PA.MutablePrimArray s Word64)
read_s :: PrimMonad m => Gen (PrimState m) -> m S
read_s (Gen g) = do
!s0 <- PA.readPrimArray g 0
!s1 <- PA.readPrimArray g 1
!s2 <- PA.readPrimArray g 2
!s3 <- PA.readPrimArray g 3
pure $! S s0 s1 s2 s3
{-# INLINE read_s #-}
write_s :: PrimMonad m => Gen (PrimState m) -> S -> m ()
write_s (Gen g) (S s0 s1 s2 s3) = do
PA.writePrimArray g 0 s0
PA.writePrimArray g 1 s1
PA.writePrimArray g 2 s2
PA.writePrimArray g 3 s3
{-# INLINE write_s #-}
alloc :: PrimMonad m => S -> m (Gen (PrimState m))
alloc s = do
g <- fmap Gen (PA.newPrimArray 4)
write_s g s
pure g
{-# INLINE alloc #-}
-- | Create a generator from a 64-bit seed, expanded to the full
-- 256-bit state via splitmix64, per the xoshiro authors'
-- recommendation.
--
-- >>> gen <- new 42
-- >>> word64 gen
-- 15021278609987233951
new :: PrimMonad m => Word64 -> m (Gen (PrimState m))
new = alloc . X.seed
{-# INLINABLE new #-}
{-# SPECIALIZE new :: Word64 -> IO (Gen RealWorld) #-}
{-# SPECIALIZE new :: Word64 -> ST s (Gen s) #-}
-- | Create a generator from the given 256-bit state, returning
-- 'Nothing' if it is all zero.
--
-- >>> Just gen <- from_state 1 2 3 4
-- >>> word64 gen
-- 41943041
from_state
:: PrimMonad m
=> Word64
-> Word64
-> Word64
-> Word64
-> m (Maybe (Gen (PrimState m)))
from_state s0 s1 s2 s3 = traverse alloc (X.from_words s0 s1 s2 s3)
{-# INLINABLE from_state #-}
-- | Create a generator with a 256-bit state drawn from the operating
-- system's entropy source, returning 'Nothing' if that fails.
--
-- >>> Just gen <- new_system
new_system :: IO (Maybe (Gen RealWorld))
new_system = X.system >>= traverse alloc
-- output ---------------------------------------------------------------------
-- | Generate a uniformly-distributed 'Word64'.
--
-- >>> Just gen <- from_state 1 2 3 4
-- >>> word64 gen
-- 41943041
-- >>> word64 gen
-- 58720359
word64 :: PrimMonad m => Gen (PrimState m) -> m Word64
word64 gen = do
!s <- read_s gen
write_s gen (X.next s)
pure $! X.output s
{-# INLINE word64 #-}
-- | Generate a uniformly-distributed 'Word64' in the given inclusive
-- range, whose bounds may be given in either order.
--
-- The result is unbiased; see 'int_range'.
--
-- >>> gen <- new 42
-- >>> word64_range 1 6 gen
-- 5
word64_range
:: PrimMonad m => Word64 -> Word64 -> Gen (PrimState m) -> m Word64
word64_range a b gen = do
let !lo = min a b
!hi = max a b
!r <- Range.upto (word64 gen) (hi - lo)
pure $! lo + r
{-# INLINE word64_range #-}
-- | Generate a uniformly-distributed 'Int' in the given inclusive
-- range, whose bounds may be given in either order.
--
-- The result is unbiased, via Lemire's method: an occasional draw is
-- rejected and replaced, so the number of words consumed varies.
--
-- >>> gen <- new 42
-- >>> int_range (-10) 10 gen
-- 7
int_range :: PrimMonad m => Int -> Int -> Gen (PrimState m) -> m Int
int_range a b gen = do
let !lo = min a b
!hi = max a b
!r <- Range.upto (word64 gen) (fi hi - fi lo)
pure $! lo + fi r
{-# INLINE int_range #-}
-- | Generate a uniformly-distributed 'Double' in [0, 1), from the
-- upper 53 bits of a 'Word64'.
--
-- >>> Just gen <- from_state 1 2 3 4
-- >>> double gen
-- 2.2737367544323206e-12
double :: PrimMonad m => Gen (PrimState m) -> m Double
double gen = do
!w <- word64 gen
-- 2 ^ -53
pure $! fi (w .>>. 11) * 1.1102230246251565e-16
{-# INLINE double #-}
-- streams --------------------------------------------------------------------
-- | Copy a generator. The copy produces the same output as the
-- original, independently of it.
--
-- >>> gen <- new 42
-- >>> cop <- copy gen
-- >>> (==) <$> word64 gen <*> word64 cop
-- True
copy :: PrimMonad m => Gen (PrimState m) -> m (Gen (PrimState m))
copy gen = read_s gen >>= alloc
{-# INLINE copy #-}
-- | Advance a generator by 2 ^ 128 outputs.
--
-- Copying and then jumping repeatedly yields up to 2 ^ 128
-- non-overlapping streams of 2 ^ 128 outputs each, e.g. one per
-- thread.
--
-- >>> gen <- new 42
-- >>> one <- copy gen
-- >>> jump gen
-- >>> two <- copy gen
-- >>> jump gen
jump :: PrimMonad m => Gen (PrimState m) -> m ()
jump gen = do
!s <- read_s gen
write_s gen (X.jump s)
{-# INLINABLE jump #-}
-- | Advance a generator by 2 ^ 192 outputs.
--
-- Copying and long-jumping repeatedly yields up to 2 ^ 64 starting
-- points, from each of which 'jump' yields 2 ^ 64 non-overlapping
-- streams.
--
-- >>> gen <- new 42
-- >>> long_jump gen
long_jump :: PrimMonad m => Gen (PrimState m) -> m ()
long_jump gen = do
!s <- read_s gen
write_s gen (X.long_jump s)
{-# INLINABLE long_jump #-}