2026-09-24 14:08:32 -06:00
|
|
|
#include <gtest/gtest.h>
|
2026-09-28 05:59:19 -06:00
|
|
|
#include <tuple>
|
2026-09-24 14:08:32 -06:00
|
|
|
|
|
|
|
|
#include "dpf.hpp"
|
|
|
|
|
#include "grotto/constant_lut.hpp"
|
|
|
|
|
#include "grotto/prefix_parity.hpp"
|
|
|
|
|
|
|
|
|
|
#include <array>
|
|
|
|
|
#include <cmath>
|
|
|
|
|
#include <cstdint>
|
|
|
|
|
#include <limits>
|
|
|
|
|
#include <random>
|
|
|
|
|
#include <string>
|
|
|
|
|
#include <vector>
|
|
|
|
|
|
|
|
|
|
namespace
|
|
|
|
|
{
|
|
|
|
|
|
|
|
|
|
using grotto::exact_constant;
|
|
|
|
|
using u128 = unsigned __int128;
|
|
|
|
|
|
|
|
|
|
const exact_constant kAll[] = {
|
|
|
|
|
exact_constant::signum,
|
|
|
|
|
exact_constant::positive,
|
|
|
|
|
exact_constant::negative,
|
|
|
|
|
exact_constant::nonneg,
|
|
|
|
|
exact_constant::nonpos,
|
|
|
|
|
exact_constant::zero,
|
|
|
|
|
exact_constant::nonzero,
|
|
|
|
|
exact_constant::ilogb,
|
|
|
|
|
exact_constant::ceil_ilogb,
|
|
|
|
|
exact_constant::ilog10,
|
|
|
|
|
exact_constant::clz,
|
|
|
|
|
exact_constant::clrsb,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const char * name_of(exact_constant which)
|
|
|
|
|
{
|
|
|
|
|
switch (which)
|
|
|
|
|
{
|
|
|
|
|
case exact_constant::signum: return "signum";
|
|
|
|
|
case exact_constant::positive: return "positive";
|
|
|
|
|
case exact_constant::negative: return "negative";
|
|
|
|
|
case exact_constant::nonneg: return "nonneg";
|
|
|
|
|
case exact_constant::nonpos: return "nonpos";
|
|
|
|
|
case exact_constant::zero: return "zero";
|
|
|
|
|
case exact_constant::nonzero: return "nonzero";
|
|
|
|
|
case exact_constant::ilogb: return "ilogb";
|
|
|
|
|
case exact_constant::ceil_ilogb: return "ceil_ilogb";
|
|
|
|
|
case exact_constant::ilog10: return "ilog10";
|
|
|
|
|
case exact_constant::clz: return "clz";
|
|
|
|
|
case exact_constant::clrsb: return "clrsb";
|
|
|
|
|
}
|
|
|
|
|
return "?";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
unsigned __int128 magnitude(std::int64_t raw)
|
|
|
|
|
{
|
|
|
|
|
if (raw >= 0)
|
|
|
|
|
return static_cast<unsigned __int128>(raw);
|
|
|
|
|
if (raw == std::numeric_limits<std::int64_t>::min())
|
|
|
|
|
return u128{1} << 63;
|
|
|
|
|
return static_cast<unsigned __int128>(-raw);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int shift_log2(unsigned __int128 mag)
|
|
|
|
|
{
|
|
|
|
|
int lg = 0;
|
|
|
|
|
while (mag > 1)
|
|
|
|
|
{
|
|
|
|
|
mag >>= 1;
|
|
|
|
|
++lg;
|
|
|
|
|
}
|
|
|
|
|
return lg;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int shift_clz64(std::uint64_t t)
|
|
|
|
|
{
|
|
|
|
|
int n = 0;
|
|
|
|
|
if (t == 0)
|
|
|
|
|
return 64;
|
|
|
|
|
while ((t & (std::uint64_t{1} << 63)) == 0)
|
|
|
|
|
{
|
|
|
|
|
t <<= 1;
|
|
|
|
|
++n;
|
|
|
|
|
}
|
|
|
|
|
return n;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool ge_pow10(unsigned __int128 mag, int k, unsigned fractional_bits)
|
|
|
|
|
{
|
|
|
|
|
if (k >= 0)
|
|
|
|
|
{
|
|
|
|
|
unsigned __int128 thresh = 1;
|
|
|
|
|
for (int i = 0; i < k; ++i)
|
|
|
|
|
{
|
|
|
|
|
if (thresh > (~u128{0}) / 10)
|
|
|
|
|
return false;
|
|
|
|
|
thresh *= 10;
|
|
|
|
|
}
|
|
|
|
|
if (fractional_bits >= 128
|
|
|
|
|
|| thresh > (~u128{0} >> fractional_bits))
|
|
|
|
|
return false;
|
|
|
|
|
return mag >= (thresh << fractional_bits);
|
|
|
|
|
}
|
|
|
|
|
unsigned __int128 scaled = mag;
|
|
|
|
|
for (int i = 0; i < -k; ++i)
|
|
|
|
|
{
|
|
|
|
|
if (scaled > (~u128{0}) / 10)
|
|
|
|
|
return true;
|
|
|
|
|
scaled *= 10;
|
|
|
|
|
}
|
|
|
|
|
return fractional_bits < 128 && scaled >= (u128{1} << fractional_bits);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::int64_t toward_zero(std::int64_t raw, unsigned fractional_bits)
|
|
|
|
|
{
|
|
|
|
|
const unsigned __int128 mag = magnitude(raw);
|
|
|
|
|
const unsigned __int128 q = mag >> fractional_bits;
|
|
|
|
|
if (raw >= 0)
|
|
|
|
|
return static_cast<std::int64_t>(q);
|
|
|
|
|
if (q > static_cast<unsigned __int128>(std::numeric_limits<std::int64_t>::max()))
|
|
|
|
|
return std::numeric_limits<std::int64_t>::min();
|
|
|
|
|
return -static_cast<std::int64_t>(q);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Independent of grotto::detail: shift loops and the xor form of clrsb,
|
|
|
|
|
// not the builtins the LUT builder calls.
|
|
|
|
|
std::int64_t reference(exact_constant which, std::int64_t raw, unsigned fractional_bits)
|
|
|
|
|
{
|
|
|
|
|
const unsigned __int128 mag = magnitude(raw);
|
|
|
|
|
switch (which)
|
|
|
|
|
{
|
|
|
|
|
case exact_constant::signum:
|
|
|
|
|
return raw < 0 ? -1 : (raw > 0 ? 1 : 0);
|
|
|
|
|
case exact_constant::positive:
|
|
|
|
|
return raw > 0;
|
|
|
|
|
case exact_constant::negative:
|
|
|
|
|
return raw < 0;
|
|
|
|
|
case exact_constant::nonneg:
|
|
|
|
|
return raw >= 0;
|
|
|
|
|
case exact_constant::nonpos:
|
|
|
|
|
return raw <= 0;
|
|
|
|
|
case exact_constant::zero:
|
|
|
|
|
return raw == 0;
|
|
|
|
|
case exact_constant::nonzero:
|
|
|
|
|
return raw != 0;
|
|
|
|
|
case exact_constant::ilogb:
|
|
|
|
|
if (mag == 0)
|
|
|
|
|
return -64;
|
|
|
|
|
if (fractional_bits >= 64
|
|
|
|
|
&& mag <= (u128{1} << (fractional_bits - 64)))
|
|
|
|
|
return -64;
|
|
|
|
|
return shift_log2(mag) - static_cast<std::int64_t>(fractional_bits);
|
|
|
|
|
case exact_constant::ceil_ilogb:
|
|
|
|
|
if (mag == 0)
|
|
|
|
|
return -64;
|
|
|
|
|
if (fractional_bits >= 64
|
|
|
|
|
&& mag <= (u128{1} << (fractional_bits - 64)))
|
|
|
|
|
return -64;
|
|
|
|
|
{
|
|
|
|
|
const auto floor_exp = shift_log2(mag) - static_cast<std::int64_t>(fractional_bits);
|
|
|
|
|
const bool power = (mag & (mag - 1)) == 0;
|
|
|
|
|
return power ? floor_exp : floor_exp + 1;
|
|
|
|
|
}
|
|
|
|
|
case exact_constant::ilog10:
|
|
|
|
|
if (mag == 0 || !ge_pow10(mag, -19, fractional_bits))
|
|
|
|
|
return -19;
|
|
|
|
|
{
|
|
|
|
|
int k = -19;
|
|
|
|
|
while (k < 40 && ge_pow10(mag, k + 1, fractional_bits))
|
|
|
|
|
++k;
|
|
|
|
|
return k;
|
|
|
|
|
}
|
|
|
|
|
case exact_constant::clz:
|
|
|
|
|
if (raw < 0)
|
|
|
|
|
return 0;
|
|
|
|
|
if (fractional_bits >= 128 || mag < (u128{1} << fractional_bits))
|
|
|
|
|
return 64;
|
|
|
|
|
return shift_clz64(static_cast<std::uint64_t>(mag >> fractional_bits));
|
|
|
|
|
case exact_constant::clrsb:
|
|
|
|
|
{
|
|
|
|
|
const std::int64_t trunc = toward_zero(raw, fractional_bits);
|
|
|
|
|
const std::uint64_t u = static_cast<std::uint64_t>(trunc);
|
|
|
|
|
const std::uint64_t y = u ^ static_cast<std::uint64_t>(trunc >> 1);
|
|
|
|
|
if (y == 0)
|
|
|
|
|
return 63;
|
|
|
|
|
return shift_clz64(y) - 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
template <typename Raw>
|
|
|
|
|
void expect_partition(const grotto::constant_lut<Raw> & lut)
|
|
|
|
|
{
|
|
|
|
|
using lim = std::numeric_limits<Raw>;
|
|
|
|
|
ASSERT_FALSE(lut.bounds.empty());
|
|
|
|
|
ASSERT_EQ(lut.bounds.size(), lut.values.size());
|
|
|
|
|
EXPECT_EQ(lut.bounds.front(), lim::min());
|
|
|
|
|
for (std::size_t i = 1; i < lut.bounds.size(); ++i)
|
|
|
|
|
{
|
|
|
|
|
EXPECT_LT(lut.bounds[i - 1], lut.bounds[i]);
|
|
|
|
|
EXPECT_NE(lut.values[i - 1], lut.values[i]);
|
|
|
|
|
EXPECT_EQ(lut(static_cast<Raw>(lut.bounds[i] - 1)), lut.values[i - 1]);
|
|
|
|
|
}
|
|
|
|
|
EXPECT_EQ(lut(lim::max()), lut.values.back());
|
|
|
|
|
for (std::size_t i = 0; i < lut.bounds.size(); ++i)
|
|
|
|
|
EXPECT_EQ(lut(lut.bounds[i]), lut.values[i]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
template <typename Raw, std::size_t N>
|
|
|
|
|
bool segment_product_matches(const grotto::constant_lut<Raw> & lut, Raw alpha)
|
|
|
|
|
{
|
|
|
|
|
std::array<Raw, N> ends{};
|
|
|
|
|
for (std::size_t i = 0; i < N; ++i)
|
|
|
|
|
ends[i] = lut.bounds[i];
|
|
|
|
|
auto [k0, k1] = dpf::make_dpf(alpha, dpf::bit::one);
|
|
|
|
|
const auto s0 = grotto::segment_parities(k0, ends);
|
|
|
|
|
const auto s1 = grotto::segment_parities(k1, ends);
|
|
|
|
|
__int128 a0 = 0;
|
|
|
|
|
__int128 a1 = 0;
|
|
|
|
|
__int128 u0 = 0;
|
|
|
|
|
__int128 u1 = 0;
|
|
|
|
|
int hot = 0;
|
|
|
|
|
int hot_at = -1;
|
|
|
|
|
for (std::size_t i = 0; i < N; ++i)
|
|
|
|
|
{
|
|
|
|
|
const int b0 = s0[i] ? 1 : 0;
|
|
|
|
|
const int b1 = s1[i] ? 1 : 0;
|
|
|
|
|
a0 += __int128(lut.values[i]) * b0;
|
|
|
|
|
a1 += __int128(lut.values[i]) * b1;
|
|
|
|
|
u0 += b0;
|
|
|
|
|
u1 += b1;
|
|
|
|
|
if ((b0 ^ b1) != 0)
|
|
|
|
|
{
|
|
|
|
|
++hot;
|
|
|
|
|
hot_at = static_cast<int>(i);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (hot != 1)
|
|
|
|
|
return false;
|
|
|
|
|
const std::int64_t opened = static_cast<std::int64_t>((u0 - u1) * (a0 - a1));
|
|
|
|
|
return opened == lut.values[static_cast<std::size_t>(hot_at)]
|
|
|
|
|
&& opened == lut(alpha);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
template <std::size_t N>
|
|
|
|
|
bool dispatch_segments(const grotto::constant_lut<std::int8_t> & lut, std::int8_t alpha)
|
|
|
|
|
{
|
|
|
|
|
if (lut.bounds.size() == N)
|
|
|
|
|
return segment_product_matches<std::int8_t, N>(lut, alpha);
|
|
|
|
|
if constexpr (N > 1)
|
|
|
|
|
return dispatch_segments<N - 1>(lut, alpha);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} // namespace
|
|
|
|
|
|
|
|
|
|
TEST(ConstantLut, RejectsFractionalBitsPastTheRawWidth)
|
|
|
|
|
{
|
2026-09-28 05:59:19 -06:00
|
|
|
EXPECT_THROW(grotto::make_exact_constant_lut<std::int8_t>(exact_constant::signum, 9), std::invalid_argument);
|
|
|
|
|
EXPECT_THROW(grotto::make_exact_constant_lut<std::int64_t>(exact_constant::clz, 65), std::invalid_argument);
|
2026-09-24 14:08:32 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST(ConstantLut, PaperPartCountsOnInt64With16FractionalBits)
|
|
|
|
|
{
|
|
|
|
|
// Appendix D, 64-bit fixed point, 16 fractional bits.
|
|
|
|
|
// `zero`, `nonzero`, and `ilog10` are counted after the two end pieces
|
|
|
|
|
// join across the wrap (they hold the same constant). `clrsb` is the
|
|
|
|
|
// linear count the generator emits; joining its equal ends would make 94.
|
|
|
|
|
struct row
|
|
|
|
|
{
|
|
|
|
|
exact_constant which;
|
|
|
|
|
std::size_t linear;
|
|
|
|
|
std::size_t wrapped;
|
|
|
|
|
};
|
|
|
|
|
const row rows[] = {
|
|
|
|
|
{exact_constant::signum, 3, 3},
|
|
|
|
|
{exact_constant::positive, 2, 2},
|
|
|
|
|
{exact_constant::negative, 2, 2},
|
|
|
|
|
{exact_constant::nonneg, 2, 2},
|
|
|
|
|
{exact_constant::nonpos, 2, 2},
|
|
|
|
|
{exact_constant::zero, 3, 2},
|
|
|
|
|
{exact_constant::nonzero, 3, 2},
|
|
|
|
|
{exact_constant::ilogb, 128, 128},
|
|
|
|
|
{exact_constant::ilog10, 41, 40},
|
|
|
|
|
{exact_constant::clz, 49, 49},
|
|
|
|
|
{exact_constant::clrsb, 95, 94},
|
|
|
|
|
};
|
|
|
|
|
for (const row & r : rows)
|
|
|
|
|
{
|
|
|
|
|
const auto lut = grotto::make_exact_constant_lut<std::int64_t>(r.which, 16);
|
|
|
|
|
expect_partition(lut);
|
|
|
|
|
EXPECT_EQ(lut.linear_parts(), r.linear) << name_of(r.which);
|
|
|
|
|
EXPECT_EQ(lut.wrapped_parts(), r.wrapped) << name_of(r.which);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST(ConstantLut, AnchorsMatchTheDefinitions)
|
|
|
|
|
{
|
|
|
|
|
const auto sgn = grotto::make_exact_constant_lut<std::int64_t>(exact_constant::signum, 16);
|
|
|
|
|
EXPECT_EQ(sgn(0), 0);
|
|
|
|
|
EXPECT_EQ(sgn(-1), -1);
|
|
|
|
|
EXPECT_EQ(sgn(1), 1);
|
|
|
|
|
EXPECT_EQ(sgn(std::numeric_limits<std::int64_t>::min()), -1);
|
|
|
|
|
EXPECT_EQ(sgn(std::numeric_limits<std::int64_t>::max()), 1);
|
|
|
|
|
|
|
|
|
|
const auto pos = grotto::make_exact_constant_lut<std::int64_t>(exact_constant::positive, 0);
|
|
|
|
|
const auto neg = grotto::make_exact_constant_lut<std::int64_t>(exact_constant::negative, 0);
|
|
|
|
|
const auto nn = grotto::make_exact_constant_lut<std::int64_t>(exact_constant::nonneg, 0);
|
|
|
|
|
const auto np = grotto::make_exact_constant_lut<std::int64_t>(exact_constant::nonpos, 0);
|
|
|
|
|
const auto z = grotto::make_exact_constant_lut<std::int64_t>(exact_constant::zero, 0);
|
|
|
|
|
const auto nz = grotto::make_exact_constant_lut<std::int64_t>(exact_constant::nonzero, 0);
|
|
|
|
|
EXPECT_EQ(pos(0), 0);
|
|
|
|
|
EXPECT_EQ(pos(1), 1);
|
|
|
|
|
EXPECT_EQ(pos(-1), 0);
|
|
|
|
|
EXPECT_EQ(neg(0), 0);
|
|
|
|
|
EXPECT_EQ(neg(-1), 1);
|
|
|
|
|
EXPECT_EQ(nn(0), 1);
|
|
|
|
|
EXPECT_EQ(nn(-1), 0);
|
|
|
|
|
EXPECT_EQ(np(0), 1);
|
|
|
|
|
EXPECT_EQ(np(1), 0);
|
|
|
|
|
EXPECT_EQ(z(0), 1);
|
|
|
|
|
EXPECT_EQ(z(1), 0);
|
|
|
|
|
EXPECT_EQ(z(-1), 0);
|
|
|
|
|
EXPECT_EQ(nz(0), 0);
|
|
|
|
|
EXPECT_EQ(nz(1), 1);
|
|
|
|
|
|
|
|
|
|
const auto ilogb = grotto::make_exact_constant_lut<std::int64_t>(exact_constant::ilogb, 16);
|
|
|
|
|
EXPECT_EQ(ilogb(0), -64);
|
|
|
|
|
EXPECT_EQ(ilogb(1), -16);
|
|
|
|
|
EXPECT_EQ(ilogb(-1), -16);
|
|
|
|
|
EXPECT_EQ(ilogb(65536), 0);
|
|
|
|
|
EXPECT_EQ(ilogb(-65536), 0);
|
|
|
|
|
EXPECT_EQ(ilogb(std::numeric_limits<std::int64_t>::min()), 47);
|
|
|
|
|
|
|
|
|
|
const auto ilog10 = grotto::make_exact_constant_lut<std::int64_t>(exact_constant::ilog10, 16);
|
|
|
|
|
EXPECT_EQ(ilog10(0), -19);
|
|
|
|
|
EXPECT_EQ(ilog10(1), -5);
|
|
|
|
|
EXPECT_EQ(ilog10(-1), -5);
|
|
|
|
|
EXPECT_EQ(ilog10(65536), 0);
|
|
|
|
|
EXPECT_EQ(ilog10(655360), 1);
|
|
|
|
|
|
|
|
|
|
const auto clz = grotto::make_exact_constant_lut<std::int64_t>(exact_constant::clz, 16);
|
|
|
|
|
EXPECT_EQ(clz(0), 64);
|
|
|
|
|
EXPECT_EQ(clz(1), 64);
|
|
|
|
|
EXPECT_EQ(clz(65535), 64);
|
|
|
|
|
EXPECT_EQ(clz(65536), 63);
|
|
|
|
|
EXPECT_EQ(clz(131072), 62);
|
|
|
|
|
EXPECT_EQ(clz(-1), 0);
|
|
|
|
|
EXPECT_EQ(clz(std::int64_t{1} << 62), 17);
|
|
|
|
|
|
|
|
|
|
const auto clrsb = grotto::make_exact_constant_lut<std::int64_t>(exact_constant::clrsb, 16);
|
|
|
|
|
EXPECT_EQ(clrsb(0), 63);
|
|
|
|
|
EXPECT_EQ(clrsb(65536), 62);
|
|
|
|
|
EXPECT_EQ(clrsb(-65536), 63);
|
|
|
|
|
EXPECT_EQ(clrsb(std::numeric_limits<std::int64_t>::min()), 16);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST(ConstantLut, EveryInt8AndInt16Point)
|
|
|
|
|
{
|
|
|
|
|
const unsigned widths_f_8[] = {0, 1, 4, 7, 8};
|
|
|
|
|
for (unsigned fractional_bits : widths_f_8)
|
|
|
|
|
{
|
|
|
|
|
for (exact_constant which : kAll)
|
|
|
|
|
{
|
|
|
|
|
const auto lut = grotto::make_exact_constant_lut<std::int8_t>(which, fractional_bits);
|
|
|
|
|
expect_partition(lut);
|
|
|
|
|
for (int raw = -128; raw <= 127; ++raw)
|
|
|
|
|
{
|
|
|
|
|
const auto r = static_cast<std::int8_t>(raw);
|
|
|
|
|
EXPECT_EQ(lut(r), reference(which, r, fractional_bits))
|
|
|
|
|
<< name_of(which) << " F=" << fractional_bits << " raw=" << raw;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const unsigned widths_f_16[] = {0, 1, 4, 8, 12, 15, 16};
|
|
|
|
|
for (unsigned fractional_bits : widths_f_16)
|
|
|
|
|
{
|
|
|
|
|
for (exact_constant which : kAll)
|
|
|
|
|
{
|
|
|
|
|
const auto lut = grotto::make_exact_constant_lut<std::int16_t>(which, fractional_bits);
|
|
|
|
|
expect_partition(lut);
|
|
|
|
|
for (int raw = -32768; raw <= 32767; ++raw)
|
|
|
|
|
{
|
|
|
|
|
const auto r = static_cast<std::int16_t>(raw);
|
|
|
|
|
EXPECT_EQ(lut(r), reference(which, r, fractional_bits))
|
|
|
|
|
<< name_of(which) << " F=" << fractional_bits << " raw=" << raw;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST(ConstantLut, Int32AndInt64BoundariesAndSamples)
|
|
|
|
|
{
|
|
|
|
|
const unsigned widths_f_32[] = {0, 1, 8, 16, 31, 32};
|
|
|
|
|
const unsigned widths_f_64[] = {0, 1, 4, 8, 12, 16, 20, 32, 48, 63, 64};
|
|
|
|
|
std::mt19937 rng(0x108u);
|
|
|
|
|
for (exact_constant which : kAll)
|
|
|
|
|
{
|
|
|
|
|
for (unsigned fractional_bits : widths_f_32)
|
|
|
|
|
{
|
|
|
|
|
const auto lut = grotto::make_exact_constant_lut<std::int32_t>(which, fractional_bits);
|
|
|
|
|
expect_partition(lut);
|
|
|
|
|
for (std::int32_t raw : lut.bounds)
|
|
|
|
|
EXPECT_EQ(lut(raw), reference(which, raw, fractional_bits));
|
|
|
|
|
EXPECT_EQ(lut(0), reference(which, 0, fractional_bits));
|
|
|
|
|
EXPECT_EQ(lut(-1), reference(which, -1, fractional_bits));
|
|
|
|
|
EXPECT_EQ(lut(1), reference(which, 1, fractional_bits));
|
|
|
|
|
EXPECT_EQ(lut(std::numeric_limits<std::int32_t>::min()),
|
|
|
|
|
reference(which, std::numeric_limits<std::int32_t>::min(), fractional_bits));
|
|
|
|
|
EXPECT_EQ(lut(std::numeric_limits<std::int32_t>::max()),
|
|
|
|
|
reference(which, std::numeric_limits<std::int32_t>::max(), fractional_bits));
|
|
|
|
|
std::uniform_int_distribution<std::int32_t> dist;
|
|
|
|
|
for (int n = 0; n < 256; ++n)
|
|
|
|
|
{
|
|
|
|
|
const std::int32_t raw = dist(rng);
|
|
|
|
|
EXPECT_EQ(lut(raw), reference(which, raw, fractional_bits))
|
|
|
|
|
<< name_of(which) << " F=" << fractional_bits;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for (unsigned fractional_bits : widths_f_64)
|
|
|
|
|
{
|
|
|
|
|
const auto lut = grotto::make_exact_constant_lut<std::int64_t>(which, fractional_bits);
|
|
|
|
|
expect_partition(lut);
|
|
|
|
|
for (std::size_t i = 0; i < lut.bounds.size(); ++i)
|
|
|
|
|
{
|
|
|
|
|
const std::int64_t raw = lut.bounds[i];
|
|
|
|
|
EXPECT_EQ(lut(raw), reference(which, raw, fractional_bits))
|
|
|
|
|
<< name_of(which) << " F=" << fractional_bits << " bound " << i;
|
|
|
|
|
if (i + 1 < lut.bounds.size())
|
|
|
|
|
{
|
|
|
|
|
const std::int64_t before = static_cast<std::int64_t>(lut.bounds[i + 1] - 1);
|
|
|
|
|
EXPECT_EQ(lut(before), reference(which, before, fractional_bits));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
std::uniform_int_distribution<std::int64_t> dist;
|
|
|
|
|
for (int n = 0; n < 128; ++n)
|
|
|
|
|
{
|
|
|
|
|
const std::int64_t raw = dist(rng);
|
|
|
|
|
EXPECT_EQ(lut(raw), reference(which, raw, fractional_bits))
|
|
|
|
|
<< name_of(which) << " F=" << fractional_bits << " raw=" << raw;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST(ConstantLut, LibmAgreesOnPowersOfTwo)
|
|
|
|
|
{
|
|
|
|
|
for (unsigned fractional_bits : {0u, 4u, 8u, 12u, 16u, 20u})
|
|
|
|
|
{
|
|
|
|
|
const auto ilogb = grotto::make_exact_constant_lut<std::int64_t>(
|
|
|
|
|
exact_constant::ilogb, fractional_bits);
|
|
|
|
|
const auto ilog10 = grotto::make_exact_constant_lut<std::int64_t>(
|
|
|
|
|
exact_constant::ilog10, fractional_bits);
|
|
|
|
|
for (int k = 0; k <= 62; ++k)
|
|
|
|
|
{
|
|
|
|
|
const std::int64_t raw = std::int64_t{1} << k;
|
|
|
|
|
const double x = std::ldexp(static_cast<double>(raw), -static_cast<int>(fractional_bits));
|
|
|
|
|
if (x == 0.0 || !std::isfinite(x))
|
|
|
|
|
continue;
|
|
|
|
|
EXPECT_EQ(ilogb(raw), std::ilogb(x)) << "F=" << fractional_bits << " k=" << k;
|
|
|
|
|
EXPECT_EQ(ilogb(-raw), std::ilogb(-x)) << "F=" << fractional_bits << " k=" << k;
|
|
|
|
|
const int from_log10 = static_cast<int>(std::floor(std::log10(std::fabs(x))));
|
|
|
|
|
EXPECT_EQ(ilog10(raw), from_log10) << "F=" << fractional_bits << " k=" << k;
|
|
|
|
|
EXPECT_EQ(ilog10(-raw), from_log10) << "F=" << fractional_bits << " k=" << k;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST(ConstantLut, PartCountsScaleWithTheDomain)
|
|
|
|
|
{
|
|
|
|
|
// clz pieces: one negative piece, the [0, 1) piece, then one piece per
|
|
|
|
|
// power of two that still fits. For a 64-bit word and F fractional bits
|
|
|
|
|
// with F <= 62 that is 63 - F + 2.
|
|
|
|
|
for (unsigned fractional_bits : {0u, 4u, 8u, 16u, 20u, 32u})
|
|
|
|
|
{
|
|
|
|
|
const auto clz = grotto::make_exact_constant_lut<std::int64_t>(
|
|
|
|
|
exact_constant::clz, fractional_bits);
|
|
|
|
|
EXPECT_EQ(clz.linear_parts(), 63u - fractional_bits + 2u) << fractional_bits;
|
|
|
|
|
}
|
|
|
|
|
// No positive power of two fits once every value is strictly inside (-1, 1).
|
|
|
|
|
EXPECT_EQ(grotto::make_exact_constant_lut<std::int64_t>(exact_constant::clz, 63).linear_parts(), 2u);
|
|
|
|
|
EXPECT_EQ(grotto::make_exact_constant_lut<std::int64_t>(exact_constant::clz, 64).linear_parts(), 2u);
|
|
|
|
|
|
|
|
|
|
// ilogb splits at every raw power of two, on both sides, plus zero: 128
|
|
|
|
|
// pieces for every fractional width that still sees the whole int64 word.
|
|
|
|
|
for (unsigned fractional_bits : {0u, 4u, 8u, 12u, 16u, 20u, 32u, 48u, 63u})
|
|
|
|
|
{
|
|
|
|
|
EXPECT_EQ(grotto::make_exact_constant_lut<std::int64_t>(
|
|
|
|
|
exact_constant::ilogb, fractional_bits).linear_parts(), 128u)
|
|
|
|
|
<< fractional_bits;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Sign tests do not depend on the fractional width.
|
|
|
|
|
for (unsigned fractional_bits : {0u, 7u, 15u, 16u})
|
|
|
|
|
{
|
|
|
|
|
EXPECT_EQ(grotto::make_exact_constant_lut<std::int16_t>(
|
|
|
|
|
exact_constant::signum, fractional_bits).linear_parts(), 3u);
|
|
|
|
|
EXPECT_EQ(grotto::make_exact_constant_lut<std::int16_t>(
|
|
|
|
|
exact_constant::zero, fractional_bits).wrapped_parts(), 2u);
|
|
|
|
|
}
|
|
|
|
|
for (unsigned fractional_bits : {0u, 16u, 31u, 32u})
|
|
|
|
|
{
|
|
|
|
|
EXPECT_EQ(grotto::make_exact_constant_lut<std::int32_t>(
|
|
|
|
|
exact_constant::positive, fractional_bits).linear_parts(), 2u);
|
|
|
|
|
EXPECT_EQ(grotto::make_exact_constant_lut<std::int64_t>(
|
|
|
|
|
exact_constant::signum, fractional_bits).linear_parts(), 3u);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST(ConstantLut, OneProgramCoversEveryWidthAndPrecision)
|
|
|
|
|
{
|
|
|
|
|
// Sign programs are three constants. Projecting them onto any word yields
|
|
|
|
|
// the same piece values, and the direct evaluator matches the table.
|
|
|
|
|
for (unsigned fractional_bits : {0u, 4u, 8u})
|
|
|
|
|
{
|
|
|
|
|
const auto narrow = grotto::make_exact_constant_lut<std::int8_t>(
|
|
|
|
|
exact_constant::signum, fractional_bits);
|
|
|
|
|
const auto wide = grotto::make_exact_constant_lut<std::int64_t>(
|
|
|
|
|
exact_constant::signum, fractional_bits);
|
|
|
|
|
ASSERT_EQ(narrow.values, wide.values);
|
|
|
|
|
EXPECT_EQ(narrow.values, (std::vector<std::int64_t>{-1, 0, 1}));
|
|
|
|
|
EXPECT_EQ(grotto::evaluate_exact<exact_constant::signum>(std::int8_t{-5}, fractional_bits),
|
|
|
|
|
narrow(-5));
|
|
|
|
|
EXPECT_EQ(grotto::evaluate_exact<exact_constant::signum>(std::int64_t{5}, fractional_bits),
|
|
|
|
|
wide(5));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ilogb is floor(log2(|raw|)) - F. Changing F only shifts that one exponent.
|
|
|
|
|
EXPECT_EQ(grotto::evaluate_exact<exact_constant::ilogb>(std::int64_t{1} << 20, 4u),
|
|
|
|
|
grotto::evaluate_exact<exact_constant::ilogb>(std::int64_t{1} << 20, 8u) + 4);
|
|
|
|
|
|
|
|
|
|
// ilog10's positive unit boundary is 10^0 from the shared pow10 table,
|
|
|
|
|
// placed at raw = 2^F.
|
|
|
|
|
for (unsigned fractional_bits : {0u, 4u, 8u, 16u})
|
|
|
|
|
{
|
|
|
|
|
const auto lut = grotto::make_exact_constant_lut<std::int64_t>(
|
|
|
|
|
exact_constant::ilog10, fractional_bits);
|
|
|
|
|
const std::int64_t unit = std::int64_t{1} << fractional_bits;
|
|
|
|
|
EXPECT_EQ(lut(unit), 0) << fractional_bits;
|
|
|
|
|
if (unit > 1)
|
|
|
|
|
EXPECT_LT(lut(static_cast<std::int64_t>(unit - 1)), 0) << fractional_bits;
|
|
|
|
|
EXPECT_EQ(grotto::evaluate_exact<exact_constant::ilog10>(unit, fractional_bits), lut(unit));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// clz reads the integer part. Shifting the raw word and the fractional
|
|
|
|
|
// width by the same amount does not change the count.
|
|
|
|
|
EXPECT_EQ(grotto::evaluate_exact<exact_constant::clz>(std::int64_t{1} << 8, 0u),
|
|
|
|
|
grotto::evaluate_exact<exact_constant::clz>(std::int64_t{1} << 12, 4u));
|
|
|
|
|
EXPECT_EQ(grotto::evaluate_exact<exact_constant::clrsb>(std::int64_t{1} << 8, 0u),
|
|
|
|
|
grotto::evaluate_exact<exact_constant::clrsb>(std::int64_t{1} << 12, 4u));
|
|
|
|
|
|
|
|
|
|
const unsigned fractional_bits = 4;
|
|
|
|
|
for (exact_constant which : kAll)
|
|
|
|
|
{
|
|
|
|
|
const auto lut = grotto::make_exact_constant_lut<std::int16_t>(which, fractional_bits);
|
|
|
|
|
for (int raw = -32768; raw <= 32767; raw += 17)
|
|
|
|
|
{
|
|
|
|
|
const auto r = static_cast<std::int16_t>(raw);
|
|
|
|
|
std::int64_t fast = 0;
|
|
|
|
|
switch (which)
|
|
|
|
|
{
|
|
|
|
|
case exact_constant::signum:
|
|
|
|
|
fast = grotto::evaluate_exact<exact_constant::signum>(r, fractional_bits); break;
|
|
|
|
|
case exact_constant::positive:
|
|
|
|
|
fast = grotto::evaluate_exact<exact_constant::positive>(r, fractional_bits); break;
|
|
|
|
|
case exact_constant::negative:
|
|
|
|
|
fast = grotto::evaluate_exact<exact_constant::negative>(r, fractional_bits); break;
|
|
|
|
|
case exact_constant::nonneg:
|
|
|
|
|
fast = grotto::evaluate_exact<exact_constant::nonneg>(r, fractional_bits); break;
|
|
|
|
|
case exact_constant::nonpos:
|
|
|
|
|
fast = grotto::evaluate_exact<exact_constant::nonpos>(r, fractional_bits); break;
|
|
|
|
|
case exact_constant::zero:
|
|
|
|
|
fast = grotto::evaluate_exact<exact_constant::zero>(r, fractional_bits); break;
|
|
|
|
|
case exact_constant::nonzero:
|
|
|
|
|
fast = grotto::evaluate_exact<exact_constant::nonzero>(r, fractional_bits); break;
|
|
|
|
|
case exact_constant::ilogb:
|
|
|
|
|
fast = grotto::evaluate_exact<exact_constant::ilogb>(r, fractional_bits); break;
|
|
|
|
|
case exact_constant::ceil_ilogb:
|
|
|
|
|
fast = grotto::evaluate_exact<exact_constant::ceil_ilogb>(r, fractional_bits); break;
|
|
|
|
|
case exact_constant::ilog10:
|
|
|
|
|
fast = grotto::evaluate_exact<exact_constant::ilog10>(r, fractional_bits); break;
|
|
|
|
|
case exact_constant::clz:
|
|
|
|
|
fast = grotto::evaluate_exact<exact_constant::clz>(r, fractional_bits); break;
|
|
|
|
|
case exact_constant::clrsb:
|
|
|
|
|
fast = grotto::evaluate_exact<exact_constant::clrsb>(r, fractional_bits); break;
|
|
|
|
|
}
|
|
|
|
|
EXPECT_EQ(fast, lut(r)) << name_of(which) << " raw=" << raw;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST(ConstantLut, SegmentParitiesRecoverEveryInt16Sign)
|
|
|
|
|
{
|
|
|
|
|
const exact_constant signs[] = {
|
|
|
|
|
exact_constant::signum,
|
|
|
|
|
exact_constant::positive,
|
|
|
|
|
exact_constant::negative,
|
|
|
|
|
exact_constant::nonneg,
|
|
|
|
|
exact_constant::nonpos,
|
|
|
|
|
exact_constant::zero,
|
|
|
|
|
exact_constant::nonzero,
|
|
|
|
|
};
|
|
|
|
|
for (unsigned fractional_bits : {0u, 8u, 15u})
|
|
|
|
|
{
|
|
|
|
|
for (exact_constant which : signs)
|
|
|
|
|
{
|
|
|
|
|
const auto lut = grotto::make_exact_constant_lut<std::int16_t>(which, fractional_bits);
|
|
|
|
|
ASSERT_LE(lut.bounds.size(), 3u);
|
|
|
|
|
for (int raw = -32768; raw <= 32767; ++raw)
|
|
|
|
|
{
|
|
|
|
|
const auto alpha = static_cast<std::int16_t>(raw);
|
|
|
|
|
const bool ok = lut.bounds.size() == 2
|
|
|
|
|
? segment_product_matches<std::int16_t, 2>(lut, alpha)
|
|
|
|
|
: segment_product_matches<std::int16_t, 3>(lut, alpha);
|
|
|
|
|
EXPECT_TRUE(ok) << name_of(which) << " F=" << fractional_bits
|
|
|
|
|
<< " alpha=" << raw;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST(ConstantLut, SegmentParitiesRecoverEveryInt8Point)
|
|
|
|
|
{
|
|
|
|
|
const unsigned widths[] = {0u, 1u, 4u, 7u};
|
|
|
|
|
for (unsigned fractional_bits : widths)
|
|
|
|
|
{
|
|
|
|
|
for (exact_constant which : kAll)
|
|
|
|
|
{
|
|
|
|
|
const auto lut = grotto::make_exact_constant_lut<std::int8_t>(which, fractional_bits);
|
|
|
|
|
ASSERT_LE(lut.bounds.size(), 40u) << name_of(which);
|
|
|
|
|
for (int raw = -128; raw <= 127; ++raw)
|
|
|
|
|
{
|
|
|
|
|
const auto alpha = static_cast<std::int8_t>(raw);
|
|
|
|
|
EXPECT_TRUE(dispatch_segments<40>(lut, alpha))
|
|
|
|
|
<< name_of(which) << " F=" << fractional_bits << " alpha=" << raw
|
|
|
|
|
<< " parts=" << lut.bounds.size();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::int64_t ref_floor_div(std::int64_t n, std::int64_t d)
|
|
|
|
|
{
|
|
|
|
|
if (n >= 0)
|
|
|
|
|
return n / d;
|
|
|
|
|
const __int128 neg = -static_cast<__int128>(n);
|
|
|
|
|
return static_cast<std::int64_t>(-((neg + d - 1) / d));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::int64_t ref_quot(std::int64_t raw, std::int64_t modulus, std::int64_t low, std::int64_t high)
|
|
|
|
|
{
|
|
|
|
|
const std::int64_t clipped = raw < low ? low : (raw > high ? high : raw);
|
|
|
|
|
return ref_floor_div(clipped, modulus);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST(ConstantLut, CeilLogStepsUpOffPowersOfTwo)
|
|
|
|
|
{
|
|
|
|
|
const auto lut = grotto::make_exact_constant_lut<std::int64_t>(exact_constant::ceil_ilogb, 0);
|
|
|
|
|
expect_partition(lut);
|
|
|
|
|
EXPECT_EQ(lut(0), -64);
|
|
|
|
|
EXPECT_EQ(lut(1), 0);
|
|
|
|
|
EXPECT_EQ(lut(2), 1);
|
|
|
|
|
EXPECT_EQ(lut(3), 2);
|
|
|
|
|
EXPECT_EQ(lut(4), 2);
|
|
|
|
|
EXPECT_EQ(lut(-3), 2);
|
|
|
|
|
EXPECT_EQ(lut(-4), 2);
|
|
|
|
|
// The same real value, written with 4 more fractional bits, has the same ceil log.
|
|
|
|
|
EXPECT_EQ(grotto::evaluate_exact<exact_constant::ceil_ilogb>(std::int64_t{3}, 0u),
|
|
|
|
|
grotto::evaluate_exact<exact_constant::ceil_ilogb>(std::int64_t{3} << 4, 4u));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST(ConstantLut, ClippedQuotientIntervalAndThreshold)
|
|
|
|
|
{
|
|
|
|
|
// quot(3, -4, 10) on the raw grid, including the clip tails.
|
|
|
|
|
const auto quot = grotto::make_clipped_quotient_lut<std::int16_t>(3, -4, 10);
|
|
|
|
|
expect_partition(quot);
|
|
|
|
|
for (int raw = -32768; raw <= 32767; ++raw)
|
|
|
|
|
{
|
|
|
|
|
const auto r = static_cast<std::int16_t>(raw);
|
|
|
|
|
EXPECT_EQ(quot(r), ref_quot(r, 3, -4, 10)) << raw;
|
|
|
|
|
}
|
|
|
|
|
EXPECT_EQ(quot(-5), ref_floor_div(-4, 3));
|
|
|
|
|
EXPECT_EQ(quot(11), ref_floor_div(10, 3));
|
|
|
|
|
EXPECT_EQ(quot(0), 0);
|
|
|
|
|
EXPECT_EQ(quot(3), 1);
|
|
|
|
|
EXPECT_EQ(quot(-3), -1);
|
|
|
|
|
|
|
|
|
|
// A mathematical step of 0.5 with 2 fractional bits is raw modulus 2.
|
|
|
|
|
const auto half = grotto::make_clipped_quotient_lut<std::int8_t>(2, -6, 6);
|
|
|
|
|
for (int raw = -128; raw <= 127; ++raw)
|
|
|
|
|
EXPECT_EQ(half(static_cast<std::int8_t>(raw)), ref_quot(raw, 2, -6, 6)) << raw;
|
|
|
|
|
|
|
|
|
|
const auto window = grotto::make_interval_lut<std::int8_t>(-2, 5);
|
|
|
|
|
expect_partition(window);
|
|
|
|
|
for (int raw = -128; raw <= 127; ++raw)
|
|
|
|
|
{
|
|
|
|
|
const auto r = static_cast<std::int8_t>(raw);
|
|
|
|
|
EXPECT_EQ(window(r), (r >= -2 && r <= 5) ? 1 : 0) << raw;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const auto ge = grotto::make_threshold_lut<std::int8_t>(-3, grotto::threshold_cmp::geq);
|
|
|
|
|
const auto lt = grotto::make_threshold_lut<std::int8_t>(-3, grotto::threshold_cmp::lt);
|
|
|
|
|
for (int raw = -128; raw <= 127; ++raw)
|
|
|
|
|
{
|
|
|
|
|
const auto r = static_cast<std::int8_t>(raw);
|
|
|
|
|
EXPECT_EQ(ge(r), r >= -3);
|
|
|
|
|
EXPECT_EQ(lt(r), r < -3);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
EXPECT_THROW(grotto::make_clipped_quotient_lut<std::int16_t>(0, -1, 1), std::invalid_argument);
|
|
|
|
|
EXPECT_THROW(grotto::make_clipped_quotient_lut<std::int16_t>(1, 4, -4), std::invalid_argument);
|
2026-09-28 05:59:19 -06:00
|
|
|
EXPECT_THROW(grotto::make_clipped_quotient_lut<std::int32_t>(1, -100000, 100000), std::invalid_argument);
|
2026-09-24 14:08:32 -06:00
|
|
|
EXPECT_THROW(grotto::make_interval_lut<std::int8_t>(2, -2), std::invalid_argument);
|
|
|
|
|
}
|