/// @file dpf/cmp_group.hpp /// @brief Comparison-payload group for types that do not fit in a masked `uint64_t`. /// @details Payloads of at most 64 bits that already convert to `uint64_t` /// stay on that path. Everything else — wider integers, `modint`, /// `fixedpoint`, `xor_wrapper`, `bitstring`, and `dpf::vec` — is a /// little-endian limb vector. Lanes of a `vec` add (or XOR) apart, /// with no carry from one lane into the next. A PRG stretch fills a /// group element from one GGM node, so the element is uniform even /// when it is wider than 64 bits. /// @copyright Copyright (c) 2019-2026 Ryan Henry and [others](@ref authors) /// @license Released under a GNU General Public v2.0 (GPLv2) license. #ifndef LIBDPF_INCLUDE_DPF_CMP_GROUP_HPP__ #define LIBDPF_INCLUDE_DPF_CMP_GROUP_HPP__ #include #include #include #include #include #include "hedley/hedley.h" #include "dpf/twiddle.hpp" #include "dpf/utils.hpp" #include "dpf/wildcard.hpp" namespace dpf { namespace detail { struct group_elem { static constexpr std::size_t cap = 4; uint64_t limb[cap]{}; std::uint16_t lane_bits = 64; std::uint16_t lanes = 1; bool xor_group = false; }; template struct is_modint_tag : std::false_type {}; template struct is_modint_tag> : std::bool_constant {}; template struct is_bitstring_tag : std::false_type {}; template struct is_bitstring_tag> : std::bool_constant {}; template struct is_vec_tag : std::false_type {}; template struct is_vec_tag> : std::bool_constant {}; template struct has_from_seed : std::false_type {}; template struct has_from_seed(nullptr), std::size_t{0}))>> : std::true_type {}; template struct has_integral_representation : std::false_type {}; template struct has_integral_representation().integral_representation())>> : std::true_type {}; template struct cmp_lane_of { using type = T; }; template struct cmp_lane_of { using type = typename T::lane_type; }; template struct cmp_group_info { using type = concrete_type_t>; static constexpr bool is_vec = is_vec_tag::value; using lane = typename cmp_lane_of::type; static constexpr bool lane_xor = utils::is_xor_wrapper_v || is_bitstring_tag::value; static constexpr std::size_t lanes = []() constexpr { if constexpr (is_vec) return type::lane_count; else return std::size_t{1}; }(); static constexpr std::size_t lane_bits = utils::bitlength_of_v; static constexpr std::size_t total_bits = lanes * lane_bits; /// @brief `uint64_t` ring, including a fixed-point value whose raw word fits. static constexpr bool narrow_ring = !is_vec && !lane_xor && lane_bits <= 64; static constexpr bool custom = !narrow_ring; static_assert(!custom || total_bits <= 256, "comparison payload exceeds 256 bits"); static_assert(!custom || total_bits > 0, "comparison payload has no bits"); }; template HEDLEY_ALWAYS_INLINE group_elem group_layout() { using info = cmp_group_info; group_elem g; g.lane_bits = static_cast(info::lane_bits); g.lanes = static_cast(info::lanes); g.xor_group = info::lane_xor; return g; } HEDLEY_ALWAYS_INLINE group_elem group_zero(const group_elem & layout) { group_elem g; g.lane_bits = layout.lane_bits; g.lanes = layout.lanes; g.xor_group = layout.xor_group; return g; } inline bool bit_at(const uint64_t limb[4], std::size_t bit) noexcept { return ((limb[bit / 64u] >> (bit % 64u)) & 1ull) != 0; } inline void set_bit(uint64_t limb[4], std::size_t bit, bool on) noexcept { const std::size_t i = bit / 64u; const uint64_t m = 1ull << (bit % 64u); if (on) limb[i] |= m; else limb[i] &= ~m; } inline void mask_limbs(uint64_t limb[4], std::size_t bits) noexcept { if (bits >= 256) return; for (std::size_t b = bits; b < 256; ++b) set_bit(limb, b, false); } inline void read_lane(const group_elem & g, std::size_t lane, uint64_t out[4]) noexcept { std::memset(out, 0, 4 * sizeof(uint64_t)); const std::size_t base = lane * g.lane_bits; for (std::size_t b = 0; b < g.lane_bits; ++b) set_bit(out, b, bit_at(g.limb, base + b)); } inline void write_lane(group_elem & g, std::size_t lane, const uint64_t in[4]) noexcept { const std::size_t base = lane * g.lane_bits; for (std::size_t b = 0; b < g.lane_bits; ++b) set_bit(g.limb, base + b, bit_at(in, b)); } inline void add_lane(uint64_t a[4], const uint64_t b[4], std::size_t bits) noexcept { unsigned carry = 0; const std::size_t n = (bits + 63u) / 64u; for (std::size_t i = 0; i < n; ++i) { const unsigned __int128 sum = static_cast(a[i]) + b[i] + carry; a[i] = static_cast(sum); carry = static_cast(sum >> 64); } mask_limbs(a, bits); } inline void neg_lane(uint64_t a[4], std::size_t bits) noexcept { uint64_t one[4] = {1, 0, 0, 0}; for (std::size_t i = 0; i < 4; ++i) a[i] = ~a[i]; mask_limbs(a, bits); add_lane(a, one, bits); } /// @brief Bit-serial product. Used when a wildcard coefficient scales δ, and when /// interval containment multiplies δ by a small public integer. /// @param a the `a` /// @param b the `b` /// @param bits the packed bits /// @param out the output buffer inline void mul_lane(const uint64_t a[4], const uint64_t b[4], std::size_t bits, uint64_t out[4]) noexcept { std::memset(out, 0, 4 * sizeof(uint64_t)); for (std::size_t bit = 0; bit < bits; ++bit) { if (!bit_at(b, bit)) continue; uint64_t shifted[4]{}; for (std::size_t s = 0; s < bits; ++s) { if (s + bit < bits && bit_at(a, s)) set_bit(shifted, s + bit, true); } add_lane(out, shifted, bits); } } inline group_elem group_apply(const group_elem & a, const group_elem & b, void (*lane_op)(uint64_t *, const uint64_t *, std::size_t)) { group_elem out = group_zero(a); for (std::size_t i = 0; i < a.lanes; ++i) { uint64_t la[4]{}, lb[4]{}; read_lane(a, i, la); read_lane(b, i, lb); if (a.xor_group) { for (std::size_t k = 0; k < 4; ++k) la[k] ^= lb[k]; mask_limbs(la, a.lane_bits); } else { lane_op(la, lb, a.lane_bits); } write_lane(out, i, la); } return out; } inline group_elem group_add(const group_elem & a, const group_elem & b) { return group_apply(a, b, add_lane); } inline group_elem group_neg(const group_elem & a) { group_elem out = group_zero(a); if (a.xor_group) return a; for (std::size_t i = 0; i < a.lanes; ++i) { uint64_t la[4]{}; read_lane(a, i, la); neg_lane(la, a.lane_bits); write_lane(out, i, la); } return out; } inline group_elem group_sub(const group_elem & a, const group_elem & b) { if (a.xor_group) return group_add(a, b); return group_add(a, group_neg(b)); } inline group_elem group_mul(const group_elem & a, const group_elem & b) { group_elem out = group_zero(a); for (std::size_t i = 0; i < a.lanes; ++i) { uint64_t la[4]{}, lb[4]{}, lc[4]{}; read_lane(a, i, la); read_lane(b, i, lb); if (a.xor_group) { for (std::size_t k = 0; k < 4; ++k) lc[k] = la[k] & lb[k]; mask_limbs(lc, a.lane_bits); } else { mul_lane(la, lb, a.lane_bits, lc); } write_lane(out, i, lc); } return out; } inline group_elem group_sgn(bool t, const group_elem & a) { return t ? group_neg(a) : a; } /// @brief Multiplicative identity: `1` in each additive lane, all-ones in an XOR lane. /// @param layout the `layout` /// @return Multiplicative identity: `1` in each additive lane, all-ones in an XOR lane inline group_elem group_one(const group_elem & layout) { group_elem g = group_zero(layout); for (std::size_t i = 0; i < g.lanes; ++i) { uint64_t lane[4]{}; if (g.xor_group) { for (std::size_t b = 0; b < g.lane_bits; ++b) set_bit(lane, b, true); } else { lane[0] = 1; } write_lane(g, i, lane); } return g; } /// @brief Integer `s` in every lane. Negative `s` is the group negation of `|s|`. /// @param s the `s` /// @param layout the `layout` /// @return Integer `s` in every lane inline group_elem group_scalar(int s, const group_elem & layout) { if (layout.xor_group) { if ((s & 1) == 0) return group_zero(layout); return group_one(layout); } const bool neg = s < 0; const auto mag = static_cast(neg ? -s : s); group_elem g = group_zero(layout); for (std::size_t i = 0; i < g.lanes; ++i) { uint64_t lane[4] = {mag, 0, 0, 0}; mask_limbs(lane, g.lane_bits); if (neg) neg_lane(lane, g.lane_bits); write_lane(g, i, lane); } return g; } inline group_elem group_from_bytes(const unsigned char * bytes, std::size_t nbytes, const group_elem & layout) { group_elem g = group_zero(layout); const std::size_t need = (static_cast(layout.lanes) * layout.lane_bits + 7u) / 8u; if (nbytes < need) throw std::invalid_argument("comparison group stretch was short"); std::size_t bit = 0; for (std::size_t lane = 0; lane < layout.lanes; ++lane) { uint64_t raw[4]{}; for (std::size_t b = 0; b < layout.lane_bits; ++b, ++bit) { const unsigned char byte = bytes[bit / 8u]; const bool on = ((byte >> (bit % 8u)) & 1u) != 0; set_bit(raw, b, on); } write_lane(g, lane, raw); } return g; } template group_elem group_from_node(Node node, const group_elem & layout) { auto seed = dpf::unset_lo_2bits(node); auto kids = PRG::eval01(seed); unsigned char bytes[64]{}; constexpr std::size_t nb = sizeof(kids[0]); static_assert(nb <= 32, "comparison stretch expects a 128- or 256-bit block"); std::memcpy(bytes, &kids[0], nb); std::memcpy(bytes + nb, &kids[1], nb); return group_from_bytes(bytes, nb * 2, layout); } template Word group_to_word(const group_elem & g) { Word w{}; static_assert(sizeof(Word) <= sizeof(g.limb), "comparison word is wider than 256 bits"); std::memcpy(&w, g.limb, sizeof(Word)); return w; } template group_elem group_from_word(const Word & w, const group_elem & layout) { group_elem g = group_zero(layout); std::memcpy(g.limb, &w, sizeof(Word) < sizeof(g.limb) ? sizeof(Word) : sizeof(g.limb)); mask_limbs(g.limb, static_cast(layout.lanes) * layout.lane_bits); return g; } template void store_raw_integer(group_elem & g, std::size_t lane, const T & value) { uint64_t tmp[4]{}; if constexpr (has_integral_representation::value) { auto raw = value.integral_representation(); std::memcpy(tmp, &raw, sizeof(raw) < sizeof(tmp) ? sizeof(raw) : sizeof(tmp)); } else if constexpr (is_modint_tag::value) { auto raw = static_cast(value); std::memcpy(tmp, &raw, sizeof(raw) < sizeof(tmp) ? sizeof(raw) : sizeof(tmp)); } else if constexpr (is_bitstring_tag::value) { auto raw = utils::to_integral_type{}(value); std::memcpy(tmp, &raw, sizeof(raw) < sizeof(tmp) ? sizeof(raw) : sizeof(tmp)); } else if constexpr (utils::is_xor_wrapper_v) { auto raw = value.data(); std::memcpy(tmp, &raw, sizeof(raw) < sizeof(tmp) ? sizeof(raw) : sizeof(tmp)); } else { static_assert(std::is_trivially_copyable_v, "comparison payload must be trivially copyable"); static_assert(sizeof(T) <= sizeof(tmp), "comparison payload exceeds 256 bits"); std::memcpy(tmp, &value, sizeof(T)); } mask_limbs(tmp, g.lane_bits); write_lane(g, lane, tmp); } template group_elem group_from_beta(const Beta & value) { using C = std::decay_t; group_elem g = group_layout(); if constexpr (is_vec_tag::value) { for (std::size_t i = 0; i < C::lane_count; ++i) { auto lane = group_from_beta(value.lanes[i]); uint64_t raw[4]{}; read_lane(lane, 0, raw); write_lane(g, i, raw); } } else { store_raw_integer(g, 0, value); } return g; } template Beta group_to_beta(const group_elem & g) { using C = std::decay_t; if constexpr (is_vec_tag::value) { C out{}; for (std::size_t i = 0; i < C::lane_count; ++i) { group_elem lane = group_zero(g); lane.lanes = 1; lane.lane_bits = g.lane_bits; lane.xor_group = g.xor_group; uint64_t raw[4]{}; read_lane(g, i, raw); write_lane(lane, 0, raw); out.lanes[i] = group_to_beta(lane); } return out; } else { uint64_t tmp[4]{}; read_lane(g, 0, tmp); if constexpr (has_integral_representation::value) { using integral = typename C::integral_type; integral raw{}; std::memcpy(&raw, tmp, sizeof(raw) < sizeof(tmp) ? sizeof(raw) : sizeof(tmp)); return C::from_raw(raw); } else if constexpr (is_modint_tag::value) { using integral = typename C::integral_type; integral raw{}; std::memcpy(&raw, tmp, sizeof(raw) < sizeof(tmp) ? sizeof(raw) : sizeof(tmp)); return C{raw}; } else if constexpr (is_bitstring_tag::value) { using integral = typename utils::make_from_integral_value::integral_type; integral raw{}; std::memcpy(&raw, tmp, sizeof(raw) < sizeof(tmp) ? sizeof(raw) : sizeof(tmp)); return utils::make_from_integral_value{}(raw); } else if constexpr (utils::is_xor_wrapper_v) { using raw_type = typename C::value_type; raw_type raw{}; std::memcpy(&raw, tmp, sizeof(raw) < sizeof(tmp) ? sizeof(raw) : sizeof(tmp)); return C{raw}; } else { C raw{}; std::memcpy(&raw, tmp, sizeof(C) < sizeof(tmp) ? sizeof(C) : sizeof(tmp)); return raw; } } } template group_elem group_value_cw(const typename PRG::block_type & c0L, const typename PRG::block_type & c0R, const typename PRG::block_type & c1L, const typename PRG::block_type & c1R, uint8_t t0, uint8_t t1, int ai, group_elem & Va, const group_elem & beta) { (void)t0; const group_elem v0L = group_from_node(c0L, Va); const group_elem v0R = group_from_node(c0R, Va); const group_elem v1L = group_from_node(c1L, Va); const group_elem v1R = group_from_node(c1R, Va); const group_elem & v0K = ai == 0 ? v0L : v0R; const group_elem & v1K = ai == 0 ? v1L : v1R; const group_elem & v0Lo = ai == 0 ? v0R : v0L; const group_elem & v1Lo = ai == 0 ? v1R : v1L; group_elem vcw = group_sgn(t1 != 0, group_add(group_add(v1Lo, group_neg(v0Lo)), group_neg(Va))); // Lose-left plants β. A planted recipe passes the plant in `beta` for both directions. if (ai == 1) vcw = group_add(vcw, group_sgn(t1 != 0, beta)); Va = group_add(group_add(group_add(Va, group_neg(v1K)), v0K), group_sgn(t1 != 0, vcw)); return vcw; } template group_elem group_final_cw(const typename PRG::block_type & s0, const typename PRG::block_type & s1, uint8_t t1, const group_elem & Va, const group_elem & on_path) { const group_elem c0 = group_from_node(s0, Va); const group_elem c1 = group_from_node(s1, Va); return group_sgn(t1 != 0, group_add(group_add(group_add(c1, group_neg(c0)), group_neg(Va)), on_path)); } /// @brief Type-erased group for a comparison payload that supplies `from_seed`, /// `operator+`, and unary `operator-`. The element is at most 256 bytes. struct payload_ops { static constexpr std::size_t cap = 256; std::size_t size = 0; void (*add)(unsigned char *, const unsigned char *, const unsigned char *) = nullptr; void (*neg)(unsigned char *, const unsigned char *) = nullptr; void (*from_node)(unsigned char *, const void *, std::size_t) = nullptr; void (*scale)(unsigned char *, const unsigned char *, std::int64_t) = nullptr; }; template void payload_add(unsigned char * dst, const unsigned char * a, const unsigned char * b) { T x{}, y{}; std::memcpy(&x, a, sizeof(T)); std::memcpy(&y, b, sizeof(T)); const T z = x + y; std::memset(dst, 0, payload_ops::cap); std::memcpy(dst, &z, sizeof(T)); } template void payload_neg(unsigned char * dst, const unsigned char * a) { T x{}; std::memcpy(&x, a, sizeof(T)); const T z = -x; std::memset(dst, 0, payload_ops::cap); std::memcpy(dst, &z, sizeof(T)); } template void payload_from_node(unsigned char * dst, const void * node, std::size_t n) { const T z = T::from_seed(node, n); std::memset(dst, 0, payload_ops::cap); std::memcpy(dst, &z, sizeof(T)); } template void payload_scale(unsigned char * dst, const unsigned char * a, std::int64_t k) { T g{}; std::memcpy(&g, a, sizeof(T)); T r{}; if (k < 0) { g = -g; k = -k; } auto m = static_cast(k); while (m != 0) { if ((m & 1u) != 0) r = r + g; m >>= 1; if (m != 0) g = g + g; } std::memset(dst, 0, payload_ops::cap); std::memcpy(dst, &r, sizeof(T)); } template payload_ops make_payload_ops() { static_assert(sizeof(T) <= payload_ops::cap, "comparison payload exceeds 256 bytes"); static_assert(has_from_seed::value, "comparison payload needs from_seed"); payload_ops ops; ops.size = sizeof(T); ops.add = &payload_add; ops.neg = &payload_neg; ops.from_node = &payload_from_node; ops.scale = &payload_scale; return ops; } inline void payload_copy(unsigned char * dst, const unsigned char * src, std::size_t n) { std::memset(dst, 0, payload_ops::cap); if (n != 0) std::memcpy(dst, src, n); } inline void payload_sgn(const payload_ops & ops, unsigned char * dst, const unsigned char * src, bool neg) { if (!neg) payload_copy(dst, src, ops.size); else ops.neg(dst, src); } /// @brief One comparison-level correction in `ops`'s group. /// @details Mirrors `group_value_cw`. When `beta` is null the correction is split /// into a group element independent of δ and an integer coefficient of δ. inline void payload_value_cw(const payload_ops & ops, const void * n0l, const void * n0r, const void * n1l, const void * n1r, std::size_t node_len, std::uint8_t t1, int ai, unsigned char * va, std::int64_t & va_c, const unsigned char * beta, unsigned char * vcw, std::int64_t * coeff) { unsigned char v0l[payload_ops::cap]{}, v0r[payload_ops::cap]{}; unsigned char v1l[payload_ops::cap]{}, v1r[payload_ops::cap]{}; ops.from_node(v0l, n0l, node_len); ops.from_node(v0r, n0r, node_len); ops.from_node(v1l, n1l, node_len); ops.from_node(v1r, n1r, node_len); const unsigned char * v0k = ai == 0 ? v0l : v0r; const unsigned char * v1k = ai == 0 ? v1l : v1r; const unsigned char * v0lo = ai == 0 ? v0r : v0l; const unsigned char * v1lo = ai == 0 ? v1r : v1l; unsigned char neg_v0[payload_ops::cap]{}, neg_va[payload_ops::cap]{}; ops.neg(neg_v0, v0lo); ops.neg(neg_va, va); unsigned char sum[payload_ops::cap]{}, inner[payload_ops::cap]{}; ops.add(sum, v1lo, neg_v0); ops.add(inner, sum, neg_va); std::int64_t inner_c = -va_c; if (ai == 1) { if (beta != nullptr) ops.add(inner, inner, beta); else inner_c += 1; } payload_sgn(ops, vcw, inner, t1 != 0); const std::int64_t vcw_c = (t1 != 0) ? -inner_c : inner_c; unsigned char sgn_vcw[payload_ops::cap]{}; payload_sgn(ops, sgn_vcw, vcw, t1 != 0); unsigned char neg_v1k[payload_ops::cap]{}, acc[payload_ops::cap]{}; ops.neg(neg_v1k, v1k); ops.add(acc, va, neg_v1k); ops.add(acc, acc, v0k); ops.add(va, acc, sgn_vcw); va_c += (t1 != 0) ? -vcw_c : vcw_c; if (coeff != nullptr) *coeff = vcw_c; if (beta != nullptr && vcw_c != 0) { unsigned char extra[payload_ops::cap]{}; ops.scale(extra, beta, vcw_c); ops.add(vcw, vcw, extra); } } inline void payload_final_cw(const payload_ops & ops, const void * s0, const void * s1, std::size_t node_len, std::uint8_t t1, const unsigned char * va, std::int64_t va_c, const unsigned char * on_path, int on_c, unsigned char * out, std::int64_t * coeff) { unsigned char c0[payload_ops::cap]{}, c1[payload_ops::cap]{}; ops.from_node(c0, s0, node_len); ops.from_node(c1, s1, node_len); unsigned char neg_c0[payload_ops::cap]{}, neg_va[payload_ops::cap]{}; ops.neg(neg_c0, c0); ops.neg(neg_va, va); unsigned char sum[payload_ops::cap]{}, inner[payload_ops::cap]{}; ops.add(sum, c1, neg_c0); ops.add(inner, sum, neg_va); if (on_path != nullptr) ops.add(inner, inner, on_path); std::int64_t inner_c = -va_c + on_c; payload_sgn(ops, out, inner, t1 != 0); const std::int64_t out_c = (t1 != 0) ? -inner_c : inner_c; if (coeff != nullptr) *coeff = out_c; } template Word payload_to_word(const unsigned char * bytes, std::size_t n) { Word w{}; std::memcpy(&w, bytes, n < sizeof(Word) ? n : sizeof(Word)); return w; } template struct payload_has_canonicalize : std::false_type {}; template struct payload_has_canonicalize> : std::true_type {}; template T payload_from_word(const Word & w) { T t{}; utils::raw_memcpy(&t, &w, sizeof(T) < sizeof(Word) ? sizeof(T) : sizeof(Word)); if constexpr (payload_has_canonicalize::value) return T::canonicalize(t); return t; } template std::int64_t payload_coeff_of(const Word & w) { std::int64_t k = 0; std::memcpy(&k, &w, sizeof(k) < sizeof(Word) ? sizeof(k) : sizeof(Word)); return k; } template Word payload_coeff_word(std::int64_t k) { Word w{}; std::memcpy(&w, &k, sizeof(k) < sizeof(Word) ? sizeof(k) : sizeof(Word)); return w; } } // namespace detail } // namespace dpf #endif // LIBDPF_INCLUDE_DPF_CMP_GROUP_HPP__