#include #include #include #include #include "dpf.hpp" #include "dpf/json.hpp" static_assert(NLOHMANN_JSON_VERSION_MAJOR == 3 && NLOHMANN_JSON_VERSION_MINOR == 12 && NLOHMANN_JSON_VERSION_PATCH == 0, "JSON tests build against nlohmann 3.12.0"); namespace { // Round-trip a comparison-only multi-level key through JSON and confirm the // (public) comparison channel still reconstructs after deserialization. TEST(IncrementalJsonTest, CmpKeyRoundTrips) { const uint32_t alpha = 0x00abcdefu; const uint64_t yt = 42u; auto [k0, k1] = dpf::make_dpf(alpha, dpf::lt(yt)); using KT0 = std::decay_t; using KT1 = std::decay_t; static_assert(KT0::is_multilevel, "cmp key must be multi-level"); ASSERT_EQ(KT0::num_outputs, 0u); const std::string s0 = dpf::json::to_json(k0); const std::string s1 = dpf::json::to_json(k1); auto r0 = dpf::json::from_json(s0); auto r1 = dpf::json::from_json(s1); const uint64_t mask = k0.cmp().mask; EXPECT_EQ(r0.cmp().nbits, k0.cmp().nbits); EXPECT_EQ(r0.cmp().mask, k0.cmp().mask); EXPECT_EQ(r0.cw_last(), k0.cw_last()); EXPECT_EQ(r0.cmp_addend(), k0.cmp_addend()); auto recon_cmp = [&](uint32_t q) { return dpf::reconstruct(dpf::eval_point(dpf::cmp, r0, q), dpf::eval_point(dpf::cmp, r1, q)) & mask; }; EXPECT_EQ(recon_cmp(alpha - 1u), yt); EXPECT_EQ(recon_cmp(0u), yt); EXPECT_EQ(recon_cmp(alpha), 0u); EXPECT_EQ(recon_cmp(alpha + 1u), 0u); } // A geq comparison (inverted path-sum) must also survive the round-trip. TEST(IncrementalJsonTest, CmpGeqRoundTrips) { const uint32_t alpha = 100u; const uint64_t yt = 5u, yf = 9u; auto [k0, k1] = dpf::make_dpf(alpha, dpf::geq(yt, yf)); using KT0 = std::decay_t; using KT1 = std::decay_t; auto r0 = dpf::json::from_json(dpf::json::to_json(k0)); auto r1 = dpf::json::from_json(dpf::json::to_json(k1)); const uint64_t mask = k0.cmp().mask; auto r = [&](uint32_t q) { return dpf::reconstruct(dpf::eval_point(dpf::cmp, r0, q), dpf::eval_point(dpf::cmp, r1, q)) & mask; }; EXPECT_EQ(r(99u), yf); EXPECT_EQ(r(100u), yt); EXPECT_EQ(r(101u), yt); } // A single-output classic key, including its leaf share. TEST(IncrementalJsonTest, ClassicPointRoundTrips) { const uint32_t alpha = 0x00abcdefu; const uint32_t y = 99u; auto [k0, k1] = dpf::make_dpf(alpha, y); using P0 = std::decay_t; using P1 = std::decay_t; auto r0 = dpf::json::from_json(dpf::json::to_json(k0)); auto r1 = dpf::json::from_json(dpf::json::to_json(k1)); EXPECT_EQ(static_cast(dpf::reconstruct( *dpf::eval_point(r0, alpha), *dpf::eval_point(r1, alpha))), y); EXPECT_EQ(static_cast(dpf::reconstruct( *dpf::eval_point(r0, alpha ^ 1u), *dpf::eval_point(r1, alpha ^ 1u))), 0u); } // Multi-lane outputs are stored as leaf blocks, not truncated integers. TEST(IncrementalJsonTest, VecOutputRoundTrips) { using out_t = dpf::vec; const std::uint16_t alpha = 0x1234; out_t y; y[0] = 1; y[1] = 0xffffffffu; y[2] = 7; y[3] = 100; auto [k0, k1] = dpf::make_dpf(alpha, y); using P0 = std::decay_t; using P1 = std::decay_t; auto r0 = dpf::json::from_json(dpf::json::to_json(k0)); auto r1 = dpf::json::from_json(dpf::json::to_json(k1)); auto at = [&](std::uint16_t x) { return dpf::reconstruct(*dpf::eval_point(r0, x), *dpf::eval_point(r1, x)); }; EXPECT_EQ(at(alpha), y); EXPECT_EQ(at(0), out_t{}); EXPECT_EQ(at(static_cast(alpha + 1)), out_t{}); } // `at<>` keys carry leaf outputs the comparison-only path used to drop. TEST(IncrementalJsonTest, AtOutputRoundTrips) { const uint32_t x = 0x00abcdefu; auto [k0, k1] = dpf::make_dpf(x, dpf::at<10>(dpf::bit::one)); using P0 = std::decay_t; using P1 = std::decay_t; static_assert(P0::is_multilevel, "at<> key must be multi-level"); ASSERT_GT(P0::num_outputs, 0u); auto r0 = dpf::json::from_json(dpf::json::to_json(k0)); auto r1 = dpf::json::from_json(dpf::json::to_json(k1)); EXPECT_TRUE(static_cast(dpf::reconstruct( *dpf::eval_point(dpf::out<0, 10>, r0, x), *dpf::eval_point(dpf::out<0, 10>, r1, x)))); const uint32_t neighbor = x ^ (1u << (32 - 10)); EXPECT_FALSE(static_cast(dpf::reconstruct( *dpf::eval_point(dpf::out<0, 10>, r0, neighbor), *dpf::eval_point(dpf::out<0, 10>, r1, neighbor)))); } // Point output and comparison channel on one key. TEST(IncrementalJsonTest, OutputAndCmpRoundTrips) { const uint32_t alpha = 0x00abcdefu; const uint64_t yt = 42u; auto [k0, k1] = dpf::make_dpf(alpha, uint32_t{7}, dpf::lt(yt)); using P0 = std::decay_t; using P1 = std::decay_t; auto r0 = dpf::json::from_json(dpf::json::to_json(k0)); auto r1 = dpf::json::from_json(dpf::json::to_json(k1)); EXPECT_EQ(static_cast(dpf::reconstruct( *dpf::eval_point(r0, alpha), *dpf::eval_point(r1, alpha))), 7u); EXPECT_EQ(static_cast(dpf::reconstruct( *dpf::eval_point(r0, alpha ^ 1u), *dpf::eval_point(r1, alpha ^ 1u))), 0u); const uint64_t mask = k0.cmp().mask; auto recon_cmp = [&](uint32_t q) { return dpf::reconstruct(dpf::eval_point(dpf::cmp, r0, q), dpf::eval_point(dpf::cmp, r1, q)) & mask; }; EXPECT_EQ(recon_cmp(alpha - 1u), yt); EXPECT_EQ(recon_cmp(alpha), 0u); EXPECT_EQ(recon_cmp(alpha + 1u), 0u); } // Comparison payloads wider than 64 bits stay in the correction words. TEST(IncrementalJsonTest, Uint128CmpRoundTrips) { using beta = simde_uint128; const std::uint8_t alpha = 0x20; const beta hi = (beta{1} << 80) + 9; const beta lo = beta{3}; auto [k0, k1] = dpf::make_dpf(alpha, dpf::gt(hi, lo)); using P0 = std::decay_t; using P1 = std::decay_t; const std::string s0 = dpf::json::to_json(k0); EXPECT_NE(s0.find("\"cw_last\":["), std::string::npos); auto r0 = dpf::json::from_json(s0); auto r1 = dpf::json::from_json(dpf::json::to_json(k1)); EXPECT_EQ(r0.cw_last_word(), k0.cw_last_word()); EXPECT_EQ(r0.cmp_addend_word(), k0.cmp_addend_word()); EXPECT_EQ(r0.value_cw(), k0.value_cw()); for (int x = 0; x < 256; ++x) { const auto q = static_cast(x); const beta got = dpf::reconstruct( dpf::eval_point(dpf::cmp, r0, q), dpf::eval_point(dpf::cmp, r1, q)); const beta want = q > alpha ? hi : lo; EXPECT_EQ(got, want) << int(q); } } // iDCF prefix corrections are part of the same wide word. TEST(IncrementalJsonTest, Uint128IdcfRoundTrips) { using beta = simde_uint128; const std::uint8_t alpha = 0x6e; const beta y = (beta{1} << 100) + 13; auto [k0, k1] = dpf::make_dpf(alpha, dpf::idcf(dpf::gt(y))); using P0 = std::decay_t; using P1 = std::decay_t; auto r0 = dpf::json::from_json(dpf::json::to_json(k0)); auto r1 = dpf::json::from_json(dpf::json::to_json(k1)); EXPECT_EQ(r0.prefix_cws(), k0.prefix_cws()); for (std::uint8_t q : {std::uint8_t{0}, std::uint8_t{0x60}, alpha, std::uint8_t{0x70}}) { const beta got = dpf::reconstruct( dpf::eval_point(dpf::cmp, r0, q), dpf::eval_point(dpf::cmp, r1, q)); const beta want = dpf::reconstruct( dpf::eval_point(dpf::cmp, k0, q), dpf::eval_point(dpf::cmp, k1, q)); EXPECT_EQ(got, want) << int(q); } } // Wildcard coefficients survive, so assign_cmp still works after a round-trip. TEST(IncrementalJsonTest, WildcardCmpAssignAfterRoundTrip) { const uint32_t alpha = 0x00abcdefu; const uint64_t yt = 42u; auto [k0, k1] = dpf::make_dpf(alpha, dpf::lt(dpf::wildcard_value{})); using P0 = std::decay_t; using P1 = std::decay_t; static_assert(P0::cmp_is_wildcard, "expected a wildcard comparison key"); auto r0 = dpf::json::from_json(dpf::json::to_json(k0)); auto r1 = dpf::json::from_json(dpf::json::to_json(k1)); EXPECT_FALSE(r0.cmp_assigned()); EXPECT_EQ(r0.value_cw_coeff(), k0.value_cw_coeff()); EXPECT_EQ(r0.cw_last_coeff_word(), k0.cw_last_coeff_word()); dpf::assign_cmp(r0, r1, yt); EXPECT_TRUE(r0.cmp_assigned()); const uint64_t mask = k0.cmp().mask; auto r = [&](uint32_t q) { return dpf::reconstruct(dpf::eval_point(dpf::cmp, r0, q), dpf::eval_point(dpf::cmp, r1, q)) & mask; }; EXPECT_EQ(r(alpha - 1u), yt); EXPECT_EQ(r(0u), yt); EXPECT_EQ(r(alpha), 0u); EXPECT_EQ(r(alpha + 1u), 0u); auto a0 = dpf::json::from_json(dpf::json::to_json(r0)); auto a1 = dpf::json::from_json(dpf::json::to_json(r1)); EXPECT_TRUE(a0.cmp_assigned()); auto again = [&](uint32_t q) { return dpf::reconstruct(dpf::eval_point(dpf::cmp, a0, q), dpf::eval_point(dpf::cmp, a1, q)) & mask; }; EXPECT_EQ(again(alpha - 1u), yt); EXPECT_EQ(again(alpha), 0u); } } // namespace