#include #include #include #include #include #include #include #include #include #include "dpf.hpp" namespace { template auto recon(const A & a, const B & b) { if constexpr (dpf::is_secret_share_v> && dpf::is_secret_share_v>) return dpf::reconstruct(a, b); else { using T = std::common_type_t, std::decay_t>; if constexpr (std::is_integral_v && std::is_unsigned_v) return static_cast(a - b); else return a - b; } } template void assign_input_local(Key0 & k0, Key1 & k1, InputT alpha) { const InputT a0 = static_cast(0x12); const InputT a1 = static_cast(alpha - a0); const auto sh0 = k0.offset_x.compute_and_get_share(a0); const auto sh1 = k1.offset_x.compute_and_get_share(a1); k0.offset_x.reconstruct(sh1); k1.offset_x.reconstruct(sh0); } template std::size_t inclusive_span(InputT from, InputT to) { constexpr auto bits = dpf::utils::bitlength_of_v; constexpr auto to_int = dpf::utils::to_integral_type{}; auto span = to_int(to) - to_int(from); if constexpr (bits < dpf::utils::bitlength_of_v) span &= (decltype(span){1} << bits) - 1; return static_cast(span) + 1; } template void for_inclusive_wrap(InputT from, InputT to, Fn && fn) { constexpr auto to_int = dpf::utils::to_integral_type{}; const auto n = inclusive_span(from, to); InputT x = from; for (std::size_t i = 0; i < n; ++i) { fn(x); x = static_cast(to_int(x) + 1); } } } // namespace // --------------------------------------------------------------------------- // utils: interval_wraps / split_leaf_nodes / get_leafnodes // --------------------------------------------------------------------------- TEST(WrapUtils, IntervalWrapsUnsigned) { EXPECT_FALSE(dpf::utils::interval_wraps(std::uint8_t{10}, std::uint8_t{20}, 8)); EXPECT_TRUE(dpf::utils::interval_wraps(std::uint8_t{200}, std::uint8_t{10}, 8)); EXPECT_FALSE(dpf::utils::interval_wraps(std::uint8_t{0}, std::uint8_t{255}, 8)); EXPECT_TRUE(dpf::utils::interval_wraps(std::uint8_t{255}, std::uint8_t{0}, 8)); EXPECT_FALSE(dpf::utils::interval_wraps(0, 0, 0)); } TEST(WrapUtils, IntervalWrapsMaskedLowBits) { // Only low 4 bits participate; 0x1F vs 0x02 wraps in nibble space. EXPECT_TRUE(dpf::utils::interval_wraps(0x1Fu, 0x02u, 4)); EXPECT_FALSE(dpf::utils::interval_wraps(0x12u, 0x1Eu, 4)); } TEST(WrapUtils, SplitNonWrapSingleSegment) { auto segs = dpf::utils::split_leaf_nodes(std::uint8_t{3}, std::uint8_t{10}, 8, false); ASSERT_EQ(segs.n, 1u); EXPECT_EQ(segs.seg[0].from_node, 3); EXPECT_EQ(segs.seg[0].to_node, 10); EXPECT_EQ(segs.seg[0].count, 7u); EXPECT_EQ(segs.total, 7u); } TEST(WrapUtils, SplitWrapTwoSegments) { // depth < bitwidth so domain_end is representable. auto segs = dpf::utils::split_leaf_nodes( std::uint16_t{250}, std::uint16_t{4}, 8, true); ASSERT_EQ(segs.n, 2u); EXPECT_EQ(segs.seg[0].from_node, 250); EXPECT_EQ(segs.seg[0].to_node, 256); EXPECT_EQ(segs.seg[0].count, 6u); EXPECT_EQ(segs.seg[1].from_node, 0); EXPECT_EQ(segs.seg[1].to_node, 4); EXPECT_EQ(segs.seg[1].count, 4u); EXPECT_EQ(segs.total, 10u); } TEST(WrapUtils, SplitWrapShallowDepth) { // depth 6 => domain_end 64 auto segs = dpf::utils::split_leaf_nodes(std::uint8_t{60}, std::uint8_t{3}, 6, true); ASSERT_EQ(segs.n, 2u); EXPECT_EQ(segs.seg[0].from_node, 60); EXPECT_EQ(segs.seg[0].to_node, 64); EXPECT_EQ(segs.seg[0].count, 4u); EXPECT_EQ(segs.seg[1].from_node, 0); EXPECT_EQ(segs.seg[1].to_node, 3); EXPECT_EQ(segs.seg[1].count, 3u); EXPECT_EQ(segs.total, 7u); } TEST(WrapUtils, InputWrapWithFromNodeLeToNodeStillSplits) { // Packing can make from_node <= to_node while the input still wraps. // Collapsing would omit the duplicated shared leaf. auto segs = dpf::utils::split_leaf_nodes(std::uint8_t{2}, std::uint8_t{5}, 6, true); ASSERT_EQ(segs.n, 2u); EXPECT_EQ(segs.seg[0].from_node, 2); EXPECT_EQ(segs.seg[0].to_node, 64); EXPECT_EQ(segs.seg[1].from_node, 0); EXPECT_EQ(segs.seg[1].to_node, 5); EXPECT_GT(segs.total, 5u - 2u); } TEST(WrapUtils, LeafCountAgreesWithSplitTotal) { using In = std::uint8_t; auto [k0, k1] = dpf::make_dpf(In{1}, std::uint32_t{1}); using key_t = std::decay_t; auto check = [](In from, In to) { In ff = from, ft = to; dpf::utils::flip_msb_if_signed_integral(ff); dpf::utils::flip_msb_if_signed_integral(ft); constexpr auto to_int = dpf::utils::to_integral_type{}; using integral = typename key_t::integral_type; const auto from_i = static_cast(to_int(ff)); const auto to_i = static_cast(to_int(ft)); const bool wraps = dpf::utils::interval_wraps(from_i, to_i, 8); const auto segs = dpf::utils::split_leaf_nodes( dpf::utils::get_from_node(ff), dpf::utils::get_to_node(ft), static_cast(key_t::depth), wraps); EXPECT_EQ((dpf::utils::get_leafnodes_in_output_interval(from, to)), segs.total); }; check(In{10}, In{20}); check(In{200}, In{10}); check(In{10}, In{9}); check(In{0}, In{255}); (void)k1; } TEST(WrapUtils, LeafCountVariesWithAlignmentWhenPacked) { using In = std::int8_t; auto [k0, k1] = dpf::make_dpf(In{0}, std::uint32_t{7}); using key_t = std::decay_t; ASSERT_GT(key_t::outputs_per_leaf, 1u); const In from{-40}, to{10}; const auto n0 = dpf::utils::get_leafnodes_in_output_interval(from, to); bool saw_larger = false; for (int d = 0; d < 64; ++d) { const In tfrom = static_cast(from + d); const In tto = static_cast(to + d); const auto n = dpf::utils::get_leafnodes_in_output_interval(tfrom, tto); if (n > n0) saw_larger = true; } EXPECT_TRUE(saw_larger); (void)k1; } // --------------------------------------------------------------------------- // rotation_iterable: indexing past the wrap (the deferred-view bug class) // --------------------------------------------------------------------------- TEST(RotationIterable, OperatorBracketMatchesIteratorOrder) { std::vector values{10, 20, 30, 40, 50}; dpf::rotation_iterable rot(values.begin(), values.end(), 3); std::vector by_it; for (auto it = rot.begin(); it != rot.end(); ++it) by_it.push_back(*it); EXPECT_EQ(by_it, (std::vector{40, 50, 10, 20, 30})); std::vector by_idx; for (std::ptrdiff_t i = 0; i < static_cast(values.size()); ++i) by_idx.push_back(rot[i]); EXPECT_EQ(by_idx, by_it); } TEST(RotationIterable, NegativeAndFullCycleDistanceNormalize) { std::vector values{1, 2, 3, 4}; dpf::rotation_iterable neg(values.begin(), values.end(), -1); EXPECT_EQ(neg.distance(), 3); EXPECT_EQ(neg[0], 4); EXPECT_EQ(neg[1], 1); dpf::rotation_iterable full(values.begin(), values.end(), 8); EXPECT_EQ(full.distance(), 0); EXPECT_EQ(full[0], 1); EXPECT_EQ(full[3], 4); } TEST(RotationIterable, WrappingSubrangeViaIndexDoesNotWalkPastEnd) { // Contiguous std::next from rot.begin() past the physical end is wrong // for a wrapping logical slice; operator[] with modulo is the safe path. std::vector values{0, 1, 2, 3, 4, 5, 6, 7}; dpf::rotation_iterable rot(values.begin(), values.end(), 5); // Logical slice starting at index 6 of the unrotated domain, length 4: // rotated indices (6+5)%8 ... => values 3,4,5,6 in rot order from start 6. const std::size_t start = 6; const std::size_t count = 4; const std::size_t n = values.size(); std::vector got; for (std::size_t i = 0; i < count; ++i) got.push_back(rot[static_cast((start + i) % n)]); EXPECT_EQ(got, (std::vector{3, 4, 5, 6})); } TEST(RotationIterable, BidirectionalRoundTrip) { std::vector values{1, 2, 3, 4, 5}; dpf::rotation_iterable rot(values.begin(), values.end(), 2); auto it = rot.begin(); ++it; ++it; EXPECT_EQ(*it, 5); --it; EXPECT_EQ(*it, 4); --it; EXPECT_EQ(it, rot.begin()); EXPECT_EQ(*it, 3); } // --------------------------------------------------------------------------- // Eager eval_interval wrap vs pointwise // --------------------------------------------------------------------------- TEST(EagerWrap, Uint8MatchesPointwise) { using In = std::uint8_t; using Out = std::uint32_t; constexpr In from{200}, to{10}, alpha{250}; constexpr Out beta{0xABCDEF01u}; auto [k0, k1] = dpf::make_dpf(alpha, beta); auto [buf0, it0] = dpf::eval_interval(k0, from, to); auto [buf1, it1] = dpf::eval_interval(k1, from, to); auto a = std::begin(it0); auto b = std::begin(it1); std::size_t n = 0; for_inclusive_wrap(from, to, [&](In x) { ASSERT_NE(a, std::end(it0)); ASSERT_NE(b, std::end(it1)); EXPECT_EQ(recon(*a, *b), recon(*dpf::eval_point(k0, x), *dpf::eval_point(k1, x))) << "x=" << +x; ++a; ++b; ++n; }); EXPECT_EQ(a, std::end(it0)); EXPECT_EQ(b, std::end(it1)); EXPECT_EQ(n, inclusive_span(from, to)); (void)buf0; (void)buf1; } TEST(EagerWrap, SameLeafWrapMatchesPointwise) { using In = std::uint8_t; using Out = std::uint32_t; // from=10, to=9 wraps almost the full domain; opl may share a leaf. auto [k0, k1] = dpf::make_dpf(In{40}, Out{0x11111111u}); auto [buf0, it0] = dpf::eval_interval(k0, In{10}, In{9}); auto [buf1, it1] = dpf::eval_interval(k1, In{10}, In{9}); auto a = std::begin(it0); auto b = std::begin(it1); for_inclusive_wrap(In{10}, In{9}, [&](In x) { ASSERT_NE(a, std::end(it0)); ASSERT_NE(b, std::end(it1)); EXPECT_EQ(recon(*a, *b), recon(*dpf::eval_point(k0, x), *dpf::eval_point(k1, x))); ++a; ++b; }); EXPECT_EQ(a, std::end(it0)); EXPECT_EQ(b, std::end(it1)); (void)buf0; (void)buf1; } // --------------------------------------------------------------------------- // Wildcard offset: memoizer / buffer must size on tree coordinates // --------------------------------------------------------------------------- TEST(OffsetSizing, LogicalMemoizerCanUndersizeAfterAssign) { using In = std::int8_t; using Out = std::uint32_t; constexpr In from{-40}, to{10}; using key_t = std::decay_t(dpf::make_dpf(dpf::wildcard_value{}, Out{7})))>; ASSERT_GT(key_t::outputs_per_leaf, 1u); const auto n_logical = dpf::utils::get_leafnodes_in_output_interval(from, to); bool found = false; for (int trial = 0; trial < 256; ++trial) { auto [k0, k1] = dpf::make_dpf(dpf::wildcard_value{}, Out{7}); const In alpha = static_cast(trial - 128); assign_input_local(k0, k1, alpha); const auto tfrom = k0.offset_x(from); const auto tto = k0.offset_x(to); const auto n_tree = dpf::utils::get_leafnodes_in_output_interval(tfrom, tto); In ff = tfrom, ft = tto; dpf::utils::flip_msb_if_signed_integral(ff); dpf::utils::flip_msb_if_signed_integral(ft); constexpr auto to_int = dpf::utils::to_integral_type{}; using integral = typename key_t::integral_type; const bool wraps = dpf::utils::interval_wraps( static_cast(to_int(ff)), static_cast(to_int(ft)), 8); // Single-segment undersize: each wrap half may still fit in n_logical. if (wraps || n_tree <= n_logical) continue; found = true; auto small = dpf::make_basic_interval_memoizer(from, to); auto buf = dpf::make_output_buffer_for_interval(k0, from, to); EXPECT_THROW( (void)dpf::eval_interval(k0, from, to, buf, small), std::exception); auto memo = dpf::make_basic_interval_memoizer(k0, from, to); auto buf2 = dpf::make_output_buffer_for_interval(k0, from, to); auto it = dpf::eval_interval(k0, from, to, buf2, memo); auto it1_buf = dpf::make_output_buffer_for_interval(k1, from, to); auto memo1 = dpf::make_basic_interval_memoizer(k1, from, to); auto it1 = dpf::eval_interval(k1, from, to, it1_buf, memo1); auto a = std::begin(it); auto b = std::begin(it1); for_inclusive_wrap(from, to, [&](In x) { ASSERT_NE(a, std::end(it)); ASSERT_NE(b, std::end(it1)); EXPECT_EQ(recon(*a, *b), recon(*dpf::eval_point(k0, x), *dpf::eval_point(k1, x))); ++a; ++b; }); break; } ASSERT_TRUE(found) << "no non-wrapping misaligned offset found"; } TEST(OffsetSizing, ConvenienceEvalIntervalSurvivesSignedWildcardSweep) { using In = std::int8_t; using Out = std::uint32_t; constexpr In from{-40}, to{10}; constexpr Out beta{9}; int ok = 0; for (int trial = 0; trial < 64; ++trial) { auto [d0, d1] = dpf::make_dpf(dpf::wildcard_value{}, beta); assign_input_local(d0, d1, static_cast(trial * 3 - 96)); auto buf0 = dpf::make_output_buffer_for_interval(d0, from, to); auto buf1 = dpf::make_output_buffer_for_interval(d1, from, to); // Default memoizer path must not throw after the sizing fix. auto it0 = dpf::eval_interval(d0, from, to, buf0); auto it1 = dpf::eval_interval(d1, from, to, buf1); auto a = std::begin(it0); auto b = std::begin(it1); for_inclusive_wrap(from, to, [&](In x) { ASSERT_NE(a, std::end(it0)); ASSERT_NE(b, std::end(it1)); EXPECT_EQ(recon(*a, *b), recon(*dpf::eval_point(d0, x), *dpf::eval_point(d1, x))) << "x=" << +x << " trial=" << trial; ++a; ++b; }); ++ok; } EXPECT_EQ(ok, 64); } // --------------------------------------------------------------------------- // Deferred wrap: index-based view vs eager (regression for contiguous-next bug) // --------------------------------------------------------------------------- TEST(DeferWrap, WrappingUint8MatchesEagerAndPointwise) { using In = std::uint8_t; using Out = std::uint32_t; constexpr In from{200}, to{10}; constexpr Out beta{42}; for (unsigned alpha_i = 0; alpha_i < 256; alpha_i += 37) { const In alpha = static_cast(alpha_i); auto [d0, d1] = dpf::make_dpf(dpf::wildcard_value{}, beta); auto buf0 = dpf::make_output_buffer_for_full(d0); auto buf1 = dpf::make_output_buffer_for_full(d1); auto def0 = dpf::defer_eval_interval(d0, from, to, buf0); auto def1 = dpf::defer_eval_interval(d1, from, to, buf1); assign_input_local(d0, d1, alpha); auto eager_buf0 = dpf::make_output_buffer_for_interval(d0, from, to); auto eager_buf1 = dpf::make_output_buffer_for_interval(d1, from, to); auto eager0 = dpf::eval_interval(d0, from, to, eager_buf0); auto eager1 = dpf::eval_interval(d1, from, to, eager_buf1); auto v0 = def0.get(); auto v1 = def1.get(); auto it_a = std::begin(v0); auto it_b = std::begin(v1); auto it_c = std::begin(eager0); auto it_d = std::begin(eager1); for_inclusive_wrap(from, to, [&](In x) { ASSERT_NE(it_a, std::end(v0)); ASSERT_NE(it_b, std::end(v1)); ASSERT_NE(it_c, std::end(eager0)); ASSERT_NE(it_d, std::end(eager1)); const auto y_def = recon(*it_a, *it_b); const auto y_eag = recon(*it_c, *it_d); const auto y_pt = recon(*dpf::eval_point(d0, x), *dpf::eval_point(d1, x)); EXPECT_EQ(y_def, y_eag) << "x=" << +x << " alpha=" << +alpha; EXPECT_EQ(y_def, y_pt) << "x=" << +x << " alpha=" << +alpha; ++it_a; ++it_b; ++it_c; ++it_d; }); EXPECT_EQ(it_a, std::end(v0)); EXPECT_EQ(it_b, std::end(v1)); } } TEST(DeferWrap, SignedSpanAcrossZeroMatchesEager) { using In = std::int8_t; using Out = std::uint32_t; constexpr In from{-5}, to{5}; constexpr Out beta{3}; for (int trial = 0; trial < 32; ++trial) { auto [d0, d1] = dpf::make_dpf(dpf::wildcard_value{}, beta); auto buf0 = dpf::make_output_buffer_for_full(d0); auto buf1 = dpf::make_output_buffer_for_full(d1); auto def0 = dpf::defer_eval_interval(d0, from, to, buf0); auto def1 = dpf::defer_eval_interval(d1, from, to, buf1); assign_input_local(d0, d1, static_cast(trial * 7 - 112)); auto eager_buf0 = dpf::make_output_buffer_for_interval(d0, from, to); auto eager_buf1 = dpf::make_output_buffer_for_interval(d1, from, to); auto eager0 = dpf::eval_interval(d0, from, to, eager_buf0); auto eager1 = dpf::eval_interval(d1, from, to, eager_buf1); auto v0 = def0.get(); auto v1 = def1.get(); auto a = std::begin(v0); auto b = std::begin(v1); auto c = std::begin(eager0); auto d = std::begin(eager1); while (a != std::end(v0)) { ASSERT_NE(b, std::end(v1)); ASSERT_NE(c, std::end(eager0)); ASSERT_NE(d, std::end(eager1)); EXPECT_EQ(recon(*a, *b), recon(*c, *d)); ++a; ++b; ++c; ++d; } EXPECT_EQ(b, std::end(v1)); EXPECT_EQ(c, std::end(eager0)); EXPECT_EQ(d, std::end(eager1)); } } TEST(DeferWrap, MultiOplUnalignedWrappingMatchesEager) { using In = std::uint8_t; using Out = std::uint64_t; constexpr In from{0xF1}, to{0x0E}; constexpr Out beta{0x55}; auto [d0, d1] = dpf::make_dpf(dpf::wildcard_value{}, beta); ASSERT_GT(decltype(d0)::outputs_per_leaf, std::size_t{1}); auto buf0 = dpf::make_output_buffer_for_full(d0); auto buf1 = dpf::make_output_buffer_for_full(d1); auto def0 = dpf::defer_eval_interval(d0, from, to, buf0); auto def1 = dpf::defer_eval_interval(d1, from, to, buf1); assign_input_local(d0, d1, In{0xA3}); auto eager_buf0 = dpf::make_output_buffer_for_interval(d0, from, to); auto eager_buf1 = dpf::make_output_buffer_for_interval(d1, from, to); auto eager0 = dpf::eval_interval(d0, from, to, eager_buf0); auto eager1 = dpf::eval_interval(d1, from, to, eager_buf1); auto v0 = def0.get(); auto v1 = def1.get(); auto a = std::begin(v0); auto b = std::begin(v1); auto c = std::begin(eager0); auto d = std::begin(eager1); for_inclusive_wrap(from, to, [&](In x) { ASSERT_NE(a, std::end(v0)); ASSERT_NE(b, std::end(v1)); ASSERT_NE(c, std::end(eager0)); ASSERT_NE(d, std::end(eager1)); EXPECT_EQ(recon(*a, *b), recon(*c, *d)) << "x=" << +x; EXPECT_EQ(recon(*a, *b), recon(*dpf::eval_point(d0, x), *dpf::eval_point(d1, x))); ++a; ++b; ++c; ++d; }); } // --------------------------------------------------------------------------- // Inner-product wrap already covered; keep a wildcard-offset variant // --------------------------------------------------------------------------- TEST(InnerProductWrap, WildcardOffsetWrappingMatchesPoints) { using In = std::uint8_t; constexpr In from{250}, to{4}; auto [k0, k1] = dpf::make_dpf(dpf::wildcard_value{}, std::uint32_t{9}); assign_input_local(k0, k1, In{1}); std::vector w; std::uint64_t expect = 0; for_inclusive_wrap(from, to, [&](In x) { w.push_back(static_cast(w.size() + 1)); expect += static_cast( recon(*dpf::eval_point(k0, x), *dpf::eval_point(k1, x))) * w.back(); }); EXPECT_EQ(recon( dpf::eval_inner_product(dpf::paired, k0, from, to, w), dpf::eval_inner_product(dpf::paired, k1, from, to, w)), expect); }