packages feed

ppad-random-0.1.0: internal/Crypto/Range.hs

{-# OPTIONS_HADDOCK hide #-}
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE MagicHash #-}
{-# LANGUAGE UnboxedTuples #-}

-- |
-- Module: Crypto.Range
-- Copyright: (c) 2026 Jared Tobin
-- License: MIT
-- Maintainer: Jared Tobin <jared@ppad.tech>
--
-- Unbiased integers in a range, via Lemire's nearly-divisionless
-- method ("Fast Random Integer Generation in an Interval", 2019).

module Crypto.Range (
    upto
  , upto_pure
  , W2(..)
  , mul_wide
  , _mul_wide_limbs
  ) where

import Data.Bits ((.&.), (.>>.))
import qualified Data.Bits as B
import Data.Word (Word64)
import GHC.Exts (Word(..), timesWord2#)

-- | A 128-bit product, as its high and low words.
data W2 = W2 {-# UNPACK #-} !Word64 {-# UNPACK #-} !Word64
  deriving (Eq, Show)

-- | The full 128-bit product of two words.
mul_wide :: Word64 -> Word64 -> W2
mul_wide a b
  | B.finiteBitSize (0 :: Word) == 64 =
      let !(W# x) = fromIntegral a
          !(W# y) = fromIntegral b
          !(# h, l #) = timesWord2# x y
      in  W2 (fromIntegral (W# h)) (fromIntegral (W# l))
  | otherwise = _mul_wide_limbs a b
{-# INLINE mul_wide #-}

-- | The full 128-bit product of two words, via 32-bit limbs, for
--   platforms with 32-bit machine words.
_mul_wide_limbs :: Word64 -> Word64 -> W2
_mul_wide_limbs a b =
  let !a0  = a .&. 0xffffffff
      !a1  = a .>>. 32
      !b0  = b .&. 0xffffffff
      !b1  = b .>>. 32
      !p00 = a0 * b0
      !p01 = a0 * b1
      !p10 = a1 * b0
      !p11 = a1 * b1
      !mid = (p00 .>>. 32) + (p01 .&. 0xffffffff) + (p10 .&. 0xffffffff)
      !hi  = p11 + (p01 .>>. 32) + (p10 .>>. 32) + (mid .>>. 32)
  in  W2 hi (a * b)
{-# INLINE _mul_wide_limbs #-}

-- | A uniformly-distributed word in [0, n], given an action producing
--   uniformly-distributed words.
--
--   Each draw x is mapped to the high word of x * (n + 1); draws whose
--   low word falls below 2 ^ 64 mod (n + 1) are rejected, so that
--   every output is equally likely. Rejection is rare for small n, and
--   the remainder is only computed when it might occur.
upto :: Monad m => m Word64 -> Word64 -> m Word64
upto gen n
  | n == maxBound = gen
  | otherwise = do
      !x <- gen
      let !(W2 h l) = mul_wide x s
      if   l >= s
      then pure h
      else reject (negate s `rem` s) h l
  where
    !s = n + 1
    reject !t = loop where
      loop !h !l
        | l >= t    = pure h
        | otherwise = do
            !x <- gen
            let !(W2 nh nl) = mul_wide x s
            loop nh nl
{-# INLINE upto #-}

-- | 'upto' for a pure generator, given its step function.
upto_pure :: (g -> (Word64, g)) -> Word64 -> g -> (Word64, g)
upto_pure step n g0
  | n == maxBound = step g0
  | otherwise =
      let !(x, g1) = step g0
          !(W2 h l) = mul_wide x s
      in  if   l >= s
          then (h, g1)
          else reject (negate s `rem` s) h l g1
  where
    !s = n + 1
    reject !t = loop where
      loop !h !l !g
        | l >= t    = (h, g)
        | otherwise =
            let !(x, ng) = step g
                !(W2 nh nl) = mul_wide x s
            in  loop nh nl ng
{-# INLINE upto_pure #-}