fix(basics): allow zero-bit groups in random word extraction

This commit is contained in:
Nicholas Dudfield
2026-09-23 16:12:23 +07:00
parent d7e6be2f59
commit c527e0a721
2 changed files with 84 additions and 2 deletions

View File

@@ -58,11 +58,14 @@ std::uint64_t
randomU64(Engine& engine)
{
static_assert(std::is_unsigned_v<typename Engine::result_type>);
static_assert(
std::numeric_limits<typename Engine::result_type>::digits <= 64);
static_assert(Engine::min() < Engine::max());
auto const draw = [&engine]() -> std::uint64_t {
return static_cast<std::uint64_t>(engine() - Engine::min());
};
// 0 means the span is 2^64. That is one full-range draw.
// A wrapped cardinality of 0 means 2^64 values: one full-range draw.
constexpr auto span =
static_cast<std::uint64_t>(Engine::max() - Engine::min());
constexpr std::uint64_t range = span + 1u;
@@ -101,7 +104,9 @@ randomU64(Engine& engine)
constexpr auto y0 = (std::uint64_t{1} << w0) * (range >> w0);
constexpr auto y1 =
(std::uint64_t{1} << (w0 + 1)) * (range >> (w0 + 1));
static_assert(w0 > 0 && w0 < 63);
// R == 3 gives n == 65 and w0 == 0. The first group still
// consumes its draw even though it contributes no output bits.
static_assert(w0 >= 0 && w0 < 63);
std::uint64_t word = 0;
for (int k = 0; k != n0; ++k)

View File

@@ -23,6 +23,7 @@
#include <array>
#include <cstdint>
#include <limits>
#include <random>
namespace ripple {
@@ -95,6 +96,33 @@ struct NonzeroMin
}
};
// A valid three-value engine exercises the zero-bit first group in
// independent_bits_engine. Cycling values make draw consumption explicit.
template <class Result, Result Minimum = 0>
struct ThreeValues
{
using result_type = Result;
std::size_t calls = 0;
static constexpr result_type
min()
{
return Minimum;
}
static constexpr result_type
max()
{
return Minimum + 2;
}
result_type
operator()()
{
return static_cast<result_type>(Minimum + (calls++ % 3));
}
};
} // namespace
class random_test : public beast::unit_test::suite
@@ -185,6 +213,54 @@ public:
BEAST_EXPECT(detail::randomU64(uneven) == ibits());
}
void
testZeroBitGroup()
{
testcase("three-value engines retain the zero-bit group draw");
auto const check = [&](auto engine) {
using Engine = decltype(engine);
std::independent_bits_engine<Engine, 64, std::uint64_t> reference{
engine};
// R=3: n=65, w0=0, n0=1, y0=3, y1=2. Discard the
// first draw, then take 64 bits, rejecting normalized value 2.
// For this cycle the accepted bits are 1010...10 (97 draws).
auto const first = detail::randomU64(engine);
BEAST_EXPECT(first == 0xaaaaaaaaaaaaaaaaULL);
BEAST_EXPECT(engine.calls == 97);
BEAST_EXPECT(first == reference());
BEAST_EXPECT(engine.calls == reference.base().calls);
for (int i = 0; i < 16; ++i)
{
BEAST_EXPECT(detail::randomU64(engine) == reference());
BEAST_EXPECT(engine.calls == reference.base().calls);
}
// Also exercise the public closed-range mapper with the same
// normalized stream, including the runtime fault-hook range.
for (auto const count : {10u, 10'000u})
{
auto const n = static_cast<std::uint64_t>(count);
auto const slack = static_cast<std::uint64_t>(-n) % n;
std::uint64_t expected;
do
{
expected = reference();
} while (expected < slack);
BEAST_EXPECT(rand_int(engine, 0u, count - 1) == expected % n);
BEAST_EXPECT(engine.calls == reference.base().calls);
}
};
check(ThreeValues<std::uint32_t>{});
check(ThreeValues<std::uint64_t>{});
check(ThreeValues<std::uint32_t, 5>{});
check(ThreeValues<
std::uint64_t,
std::numeric_limits<std::uint64_t>::max() - 2>{});
}
void
testBounds()
{
@@ -212,6 +288,7 @@ public:
{
testReducedMapping();
testEngineVectors();
testZeroBitGroup();
testBounds();
}
};