ppad-random-0.1.0: lib/Crypto/Xoshiro.hs
{-# OPTIONS_HADDOCK hide #-}
{-# LANGUAGE BangPatterns #-}
-- |
-- Module: Crypto.Xoshiro
-- Copyright: (c) 2026 Jared Tobin
-- License: MIT
-- Maintainer: Jared Tobin <jared@ppad.tech>
--
-- The xoshiro256++ state, step function, scrambler, jumps, and
-- seeding, shared by the mutable and pure generators.
module Crypto.Xoshiro (
S(..)
, output
, next
, jump
, long_jump
, seed
, from_words
, system
) where
import qualified Crypto.Entropy as Entropy
import Data.Bits ((.|.), (.^.), (.<<.), (.>>.))
import qualified Data.Bits as B
import Data.Word (Word64)
import Foreign.Marshal.Alloc (allocaBytes)
import Foreign.Ptr (Ptr, castPtr)
import Foreign.Storable (peekElemOff)
rotl :: Word64 -> Int -> Word64
rotl x k = (x .<<. k) .|. (x .>>. (64 - k))
{-# INLINE rotl #-}
-- | A xoshiro256++ state, which must not be all zero.
data S = S
{-# UNPACK #-} !Word64
{-# UNPACK #-} !Word64
{-# UNPACK #-} !Word64
{-# UNPACK #-} !Word64
-- | The ++ scrambler: the output for a state.
output :: S -> Word64
output (S s0 _ _ s3) = rotl (s0 + s3) 23 + s0
{-# INLINE output #-}
-- | The linear engine: the next state.
next :: S -> S
next (S s0 s1 s2 s3) =
let !t = s1 .<<. 17
!a2 = s2 .^. s0
!a3 = s3 .^. s1
!b1 = s1 .^. a2
!b0 = s0 .^. a3
!b2 = a2 .^. t
!b3 = rotl a3 45
in S b0 b1 b2 b3
{-# INLINE next #-}
xor_s :: S -> S -> S
xor_s (S a0 a1 a2 a3) (S b0 b1 b2 b3) =
S (a0 .^. b0) (a1 .^. b1) (a2 .^. b2) (a3 .^. b3)
{-# INLINE xor_s #-}
data P = P !S !S
-- advance the state by the jump polynomial with the given coefficients
leap :: Word64 -> Word64 -> Word64 -> Word64 -> S -> S
leap j0 j1 j2 j3 s0 =
let !(P acc _) =
coef j3 (coef j2 (coef j1 (coef j0 (P (S 0 0 0 0) s0))))
in acc
where
coef :: Word64 -> P -> P
coef j = loop 0 where
loop :: Int -> P -> P
loop !b p@(P acc s)
| b == 64 = p
| otherwise =
let !nacc | B.testBit j b = xor_s acc s
| otherwise = acc
in loop (b + 1) (P nacc (next s))
-- | Advance a state by 2 ^ 128 steps.
jump :: S -> S
jump = leap 0x180ec6d33cfd0aba 0xd5a61266f0c9392c
0xa9582618e03fc9aa 0x39abdc4529b1661c
-- | Advance a state by 2 ^ 192 steps.
long_jump :: S -> S
long_jump = leap 0x76e15d3efefdcbbf 0xc5004e441c522fb3
0x77710069854ee241 0x39109bb02acbe635
data SM = SM {-# UNPACK #-} !Word64 {-# UNPACK #-} !Word64
splitmix64 :: Word64 -> SM
splitmix64 x =
let !nx = x + 0x9e3779b97f4a7c15
!z0 = (nx .^. (nx .>>. 30)) * 0xbf58476d1ce4e5b9
!z1 = (z0 .^. (z0 .>>. 27)) * 0x94d049bb133111eb
in SM nx (z1 .^. (z1 .>>. 31))
{-# INLINE splitmix64 #-}
-- | A state expanded from a 64-bit seed via splitmix64, per the
-- xoshiro authors' recommendation. It is never all zero.
seed :: Word64 -> S
seed x =
let !(SM x0 s0) = splitmix64 x
!(SM x1 s1) = splitmix64 x0
!(SM x2 s2) = splitmix64 x1
!(SM _ s3) = splitmix64 x2
in S s0 s1 s2 s3
-- | A state from the given words, unless they are all zero.
from_words :: Word64 -> Word64 -> Word64 -> Word64 -> Maybe S
from_words s0 s1 s2 s3
| s0 .|. s1 .|. s2 .|. s3 == 0 = Nothing
| otherwise = Just (S s0 s1 s2 s3)
{-# INLINE from_words #-}
-- | A state drawn from the operating system's entropy source, unless
-- that fails.
system :: IO (Maybe S)
system = allocaBytes 32 $ \buf -> do
ok <- Entropy.fill buf 32
if not ok
then pure Nothing
else do
let p = castPtr buf :: Ptr Word64
s0 <- peekElemOff p 0
s1 <- peekElemOff p 1
s2 <- peekElemOff p 2
s3 <- peekElemOff p 3
pure $! from_words s0 s1 s2 s3