packages feed

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)