Read-only mirrors. Development, issues and pull requests are on GitHub.

summaryrefslogtreecommitdiffhomepage
path: root/src/Progmasoft/Catch3/Random.hpp
blob: 0a95cb84c5513ce6e6457480b0f539523eab4c9f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
// Copyright (c) 2026 Progmasoft.
// SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
#pragma once

#include <bit>
#include <concepts>
#include <cstdint>
#include <limits>
#include <stdexcept>
#include <type_traits>

namespace Progmasoft::Catch3 {

    /// Small, deterministic random source for repeatable property-test cases.
    ///
    /// The SplitMix64 sequence is specified here instead of delegating to a
    /// standard-library engine, whose output sequence is not portable between
    /// library implementations. A Random object is intentionally not
    /// thread-safe; property trials receive independent instances derived from
    /// the root seed.
    class Random final {
    public:
        explicit constexpr Random( std::uint64_t seed ) noexcept:
            state_( seed ) {}

        /// Returns the next value in the stable SplitMix64 sequence.
        [[nodiscard]] constexpr std::uint64_t Next() noexcept {
            state_ += kGamma;
            std::uint64_t value = state_;
            value = ( value ^ ( value >> 30U ) ) * kMultiplierOne;
            value = ( value ^ ( value >> 27U ) ) * kMultiplierTwo;
            return value ^ ( value >> 31U );
        }

        /// Returns a value in [0, bound); bound == 0 requests the full uint64
        /// range.
        [[nodiscard]] constexpr std::uint64_t
        Below( std::uint64_t bound ) noexcept {
            if ( bound == 0 ) { return Next(); }

            // Rejection sampling avoids the modulo bias of a single `% bound`.
            const std::uint64_t threshold =
                ( std::uint64_t{ 0 } - bound ) % bound;
            while ( true ) {
                const std::uint64_t value = Next();
                if ( value >= threshold ) { return value % bound; }
            }
        }

        /// Returns an inclusive integral value without overflowing at type
        /// limits.
        template <std::integral IntegerType>
        requires( !std::same_as<IntegerType, bool> ) [[nodiscard]] IntegerType
            Between( IntegerType minimum, IntegerType maximum ) {
            if ( minimum > maximum ) {
                throw std::invalid_argument(
                    "Random::Between requires minimum <= maximum" );
            }

            using UnsignedType = std::make_unsigned_t<IntegerType>;
            static_assert( std::numeric_limits<UnsignedType>::digits <= 64 );

            constexpr unsigned kBitCount = static_cast<unsigned>(
                std::numeric_limits<UnsignedType>::digits );
            constexpr UnsignedType kSignBit = UnsignedType{ 1 }
                                              << ( kBitCount - 1U );

            const auto encode =
                []( IntegerType value ) constexpr -> UnsignedType {
                if constexpr ( std::is_signed_v<IntegerType> ) {
                    return std::bit_cast<UnsignedType>( value ) ^ kSignBit;
                } else {
                    return value;
                }
            };
            const auto decode =
                []( UnsignedType value ) constexpr -> IntegerType {
                if constexpr ( std::is_signed_v<IntegerType> ) {
                    return std::bit_cast<IntegerType>( value ^ kSignBit );
                } else {
                    return value;
                }
            };

            const UnsignedType lower = encode( minimum );
            const UnsignedType upper = encode( maximum );
            // Unsigned wrap to zero represents the complete value domain.
            const UnsignedType width =
                static_cast<UnsignedType>( upper - lower + UnsignedType{ 1 } );
            const UnsignedType offset =
                width == 0 ? static_cast<UnsignedType>( Next() )
                           : static_cast<UnsignedType>(
                                 Below( static_cast<std::uint64_t>( width ) ) );
            return decode( static_cast<UnsignedType>( lower + offset ) );
        }

        /// Derives an independent, reproducible seed for one trial.
        [[nodiscard]] static constexpr std::uint64_t
        DeriveSeed( std::uint64_t rootSeed,
                    std::uint64_t trialIndex ) noexcept {
            Random trialRandom( rootSeed + kGamma * ( trialIndex + 1U ) );
            return trialRandom.Next();
        }

    private:
        static constexpr std::uint64_t kGamma = 0x9E3779B97F4A7C15ULL;
        static constexpr std::uint64_t kMultiplierOne = 0xBF58476D1CE4E5B9ULL;
        static constexpr std::uint64_t kMultiplierTwo = 0x94D049BB133111EBULL;

        std::uint64_t state_;
    };

} // namespace Progmasoft::Catch3