ppad-random-0.1.0: lib/Crypto/PRNG/Pure.hs
{-# OPTIONS_HADDOCK prune #-}
{-# LANGUAGE BangPatterns #-}
-- |
-- Module: Crypto.PRNG.Pure
-- 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/), as an immutable value.
--
-- This is the generator of "Crypto.PRNG", with the same output for the
-- same seed, but threaded through code explicitly: each function
-- returns its result along with the next generator. Generators are
-- values, so using one twice yields the same output twice; derive
-- non-overlapping streams via 'jump' or 'long_jump'.
--
-- When these functions are inlined into a strict loop, GHC keeps the
-- generator's state in registers and allocates nothing, which is
-- several times faster than the mutable generator. Bind results with
-- @case@ or bang patterns rather than lazy @let@s. Called where GHC
-- can't inline or specialise them, e.g. via a lazy fold or an
-- unspecialised class method, each draw allocates, and the mutable
-- "Crypto.PRNG" may be faster.
--
-- Its output is predictable from a small sample; use "Crypto.CSPRNG"
-- for anything that must be unpredictable.
module Crypto.PRNG.Pure (
-- * Generator
Gen
, Draw(..)
, new
, new_system
, from_state
-- * Output
, word64
, word64_range
, int_range
, double
-- * Streams
, jump
, long_jump
) where
import qualified Crypto.Range as Range
import qualified Crypto.Xoshiro as X
import Data.Bits ((.>>.))
import Data.Word (Word64)
-- utils ----------------------------------------------------------------------
fi :: (Integral a, Num b) => a -> b
fi = fromIntegral
{-# INLINE fi #-}
-- generator ------------------------------------------------------------------
-- | A xoshiro256++ generator.
newtype Gen = Gen X.S
-- | A generated value, along with the next generator.
data Draw a = Draw !a {-# UNPACK #-} !Gen
-- | Create a generator from a 64-bit seed, expanded to the full
-- 256-bit state via splitmix64, per the xoshiro authors'
-- recommendation.
--
-- >>> let Draw w _ = word64 (new 42)
-- >>> w
-- 15021278609987233951
new :: Word64 -> Gen
new = Gen . X.seed
{-# INLINE new #-}
-- | Create a generator from the given 256-bit state, returning
-- 'Nothing' if it is all zero.
--
-- >>> let Just gen = from_state 1 2 3 4
-- >>> let Draw w _ = word64 gen
-- >>> w
-- 41943041
from_state :: Word64 -> Word64 -> Word64 -> Word64 -> Maybe Gen
from_state s0 s1 s2 s3 = fmap Gen (X.from_words s0 s1 s2 s3)
{-# INLINE 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)
new_system = fmap (fmap Gen) X.system
-- output ---------------------------------------------------------------------
-- | Generate a uniformly-distributed 'Word64'.
--
-- >>> let Just gen = from_state 1 2 3 4
-- >>> let Draw a g1 = word64 gen
-- >>> let Draw b _ = word64 g1
-- >>> (a, b)
-- (41943041,58720359)
word64 :: Gen -> Draw Word64
word64 (Gen s) = Draw (X.output s) (Gen (X.next 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'.
--
-- >>> let Draw w _ = word64_range 1 6 (new 42)
-- >>> w
-- 5
word64_range :: Word64 -> Word64 -> Gen -> Draw Word64
word64_range a b (Gen s) =
let !lo = min a b
!hi = max a b
!(r, ns) = Range.upto_pure step (hi - lo) s
in Draw (lo + r) (Gen ns)
{-# 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.
--
-- >>> let Draw x _ = int_range (-10) 10 (new 42)
-- >>> x
-- 7
int_range :: Int -> Int -> Gen -> Draw Int
int_range a b (Gen s) =
let !lo = min a b
!hi = max a b
!(r, ns) = Range.upto_pure step (fi hi - fi lo) s
in Draw (lo + fi r) (Gen ns)
{-# INLINE int_range #-}
-- | Generate a uniformly-distributed 'Double' in [0, 1), from the
-- upper 53 bits of a 'Word64'.
--
-- >>> let Just gen = from_state 1 2 3 4
-- >>> let Draw d _ = double gen
-- >>> d
-- 2.2737367544323206e-12
double :: Gen -> Draw Double
double gen = case word64 gen of
-- 2 ^ -53
Draw w ng -> Draw (fi (w .>>. 11) * 1.1102230246251565e-16) ng
{-# INLINE double #-}
-- one step of the generator, for 'Range.upto_pure'
step :: X.S -> (Word64, X.S)
step s = (X.output s, X.next s)
{-# INLINE step #-}
-- streams --------------------------------------------------------------------
-- | Advance a generator by 2 ^ 128 outputs.
--
-- Repeated jumps yield up to 2 ^ 128 non-overlapping streams of
-- 2 ^ 128 outputs each, e.g. one per thread.
--
-- >>> let gens = take 4 (iterate jump (new 42))
jump :: Gen -> Gen
jump (Gen s) = Gen (X.jump s)
-- | Advance a generator by 2 ^ 192 outputs.
--
-- Repeated long jumps yield up to 2 ^ 64 starting points, from each
-- of which 'jump' yields 2 ^ 64 non-overlapping streams.
--
-- >>> let starts = take 4 (iterate long_jump (new 42))
long_jump :: Gen -> Gen
long_jump (Gen s) = Gen (X.long_jump s)