libdpf/include/dpf/eval_sequence.hpp
Ryan Henry 0d22946a0e Checkpoint the party/runtime stack before share-program and malicious-mode work.
Ship the TLS mesh, composer, Beaver/Yao/leaf MPC, prep/online paths, apps, and docs so the tree is pushable before elevating share_expr, security_mode, and prep resume.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-28 05:59:19 -06:00

1002 lines
42 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/// @file dpf/eval_sequence.hpp
/// @brief Evaluate a sorted list of DPF inputs.
/// @details The range is nondecreasing; an unsorted range throws
/// `std::runtime_error`. `return_output_only_tag_` stores one share
/// per point. `return_entire_node_tag_` stores whole leaves and is
/// the default. A `sequence_recipe` repeats the list, and a sequence
/// memoizer bound to that recipe object resumes the traversal.
/// @snippet evaluation/eval_sequence.cpp eval-sequence
/// @author Ryan Henry <ryan.henry@ucalgary.ca>
/// @author Christopher Jiang <christopher.jiang@ucalgary.ca>
/// @copyright Copyright (c) 2019-2024 Ryan Henry and [others](@ref authors)
/// @license Released under a GNU General Public v2.0 (GPLv2) license;
/// see [LICENSE.md](@ref license) for details.
#ifndef LIBDPF_INCLUDE_DPF_EVAL_SEQUENCE_HPP__
#define LIBDPF_INCLUDE_DPF_EVAL_SEQUENCE_HPP__
#include <portable-snippets/builtin/builtin.h>
#include "hedley/hedley.h"
#include <cstddef>
#include <cstring>
#include <type_traits>
#include <utility>
#include <tuple>
#include <algorithm>
#include <array>
#include <iterator>
#include <stdexcept>
#include <limits>
#include <vector>
#include "dpf/dpf_key.hpp"
#include "dpf/eval_common.hpp"
#include "dpf/eval_target.hpp"
#include "dpf/eval_point.hpp"
#include "dpf/eval_interval.hpp"
#include "dpf/path_memoizer.hpp"
#include "dpf/sequence_memoizer.hpp"
#include "dpf/sequence_utils.hpp"
#include "dpf/subsequence_iterable.hpp"
#include "dpf/subinterval_iterable.hpp"
#include "dpf/verifiable.hpp"
namespace dpf
{
template <typename KeyT, typename ForwardIterator>
void prove_sequence(const KeyT & key, ForwardIterator begin, ForwardIterator end,
prove_ref pr);
// Contiguous runs at least this long use the interval tree (one expand per
// node). Shorter runs stay on the path memoizer. Isolated points at least
// this many use the breadth-first block walk, which beats a fresh path per
// point once the list is wide.
inline constexpr std::size_t sequence_interval_run = 24;
// Breadth-first shares prefixes on sparse lists, but its per-level block-split
// bookkeeping loses to a path memoizer once single-child expands cut the
// path PRG cost in half. Keep the explicit `eval_sequence_breadth_first`
// entry point; do not auto-select it from `eval_sequence`.
inline constexpr std::size_t sequence_breadth_points = std::numeric_limits<std::size_t>::max();
template <std::size_t I,
typename DpfKey,
typename ForwardIterator,
typename OutputBuffer>
inline auto eval_sequence_breadth_first(const DpfKey & dpf, ForwardIterator begin,
ForwardIterator end, OutputBuffer && outbuf);
template <bool Entire,
std::size_t ...Is,
typename DpfKey,
typename InputT,
typename OutputBuffers,
std::size_t ...IIs>
void scatter_interval_run(const DpfKey & dpf, InputT from, InputT to,
OutputBuffers & outbufs, std::size_t index0, std::index_sequence<IIs...>)
{
// std::make_tuple, not utils::make_tuple: one output must stay a tuple
// so each selected buffer is addressable by index.
auto ibufs = std::make_tuple(
make_output_buffer_for_interval<DpfKey, Is>(from, to)...);
(void)eval_interval<Is...>(dpf, from, to, ibufs);
constexpr auto opl = DpfKey::outputs_per_leaf;
constexpr auto lg = DpfKey::lg_outputs_per_leaf;
constexpr auto mod_pow_2 = utils::mod_pow_2<InputT>{};
const std::size_t preclip = mod_pow_2(dpf.offset_x(from), lg);
constexpr auto to_int = utils::to_integral_type<InputT>{};
auto span = to_int(to) - to_int(from);
constexpr auto bits = utils::bitlength_of_v<InputT>;
if constexpr (bits < utils::bitlength_of_v<decltype(span)>)
span &= (decltype(span){1} << bits) - 1;
const std::size_t n = static_cast<std::size_t>(span) + 1;
constexpr std::size_t leaf_mask = (std::size_t{1} << lg) - 1;
auto one = [&](auto which)
{
constexpr std::size_t k = decltype(which)::value;
auto & src = utils::get<k>(ibufs);
auto & dst = utils::get<k>(outbufs);
auto put = [](auto & slot, const auto & val)
{
assign_share_slot(slot, val);
};
if constexpr (Entire)
{
for (std::size_t j = 0; j < n; ++j)
{
const std::size_t elem_i = preclip + j;
const std::size_t leaf = elem_i - (elem_i & leaf_mask);
for (std::size_t lane = 0; lane < opl; ++lane)
put(dst[(index0 + j) * opl + lane], src[leaf + lane]);
}
}
else
{
for (std::size_t j = 0; j < n; ++j)
put(dst[index0 + j], src[preclip + j]);
}
};
(one(std::integral_constant<std::size_t, IIs>{}), ...);
}
namespace internal
{
/// @brief Leaf interior nodes for a list, eight paths at a time.
/// @details One scalar spine up to the shallowest divergence, then
/// `tree::traverse8` across the whole suffix of each level. `walk[i]`
/// is the offset input with the signed MSB already flipped, matching
/// `eval_point`.
template <typename DpfKey>
std::vector<typename DpfKey::interior_node> sequence_wide_leaves(
const DpfKey & dpf, const typename DpfKey::input_type * walk, std::size_t n)
{
using node = typename DpfKey::interior_node;
std::vector<node> leaves(n);
if (n == 0)
return leaves;
constexpr auto clz = utils::countl_zero_symmetric_difference<
typename DpfKey::input_type>{};
std::size_t common = dpf.depth + 1;
for (std::size_t i = 1; i < n; ++i)
common = std::min(common, clz(walk[0], walk[i]) + 1);
std::array<node, DpfKey::depth + 1> path{};
path[0] = dpf.root();
auto mask = dpf.msb_mask;
for (std::size_t level = 1; level < common && level <= dpf.depth;
++level, mask >>= 1)
{
const bool bit = (mask & walk[0]) != 0;
path[level] = DpfKey::traverse_interior(path[level - 1],
dpf.correction_word(level - 1, bit), bit,
DpfKey::tree::is_last_level(level - 1, dpf.depth));
}
if (common > dpf.depth)
{
std::fill(leaves.begin(), leaves.end(), path[dpf.depth]);
return leaves;
}
std::vector<node> cur(n, path[common - 1]);
std::vector<node> next(n);
mask = dpf.msb_mask >> (common - 1);
for (std::size_t level = common; level <= dpf.depth; ++level, mask >>= 1)
{
const node cw0 = dpf.correction_word(level - 1, false);
const node cw1 = dpf.correction_word(level - 1, true);
const bool last = DpfKey::tree::is_last_level(level - 1, dpf.depth);
std::size_t i = 0;
for (; i + 8 <= n; i += 8)
{
node parents[8];
node cws[8];
bool dirs[8];
for (int k = 0; k < 8; ++k)
{
dirs[k] = (mask & walk[i + static_cast<std::size_t>(k)]) != 0;
cws[k] = dirs[k] ? cw1 : cw0;
parents[k] = cur[i + static_cast<std::size_t>(k)];
}
node outs[8];
DpfKey::tree::traverse8(parents, cws, dirs, outs);
for (int k = 0; k < 8; ++k)
next[i + static_cast<std::size_t>(k)] = outs[k];
}
for (; i < n; ++i)
{
const bool bit = (mask & walk[i]) != 0;
next[i] = DpfKey::traverse_interior(cur[i], bit ? cw1 : cw0, bit, last);
}
cur.swap(next);
}
return cur;
}
template <std::size_t ...Is,
typename DpfKey,
typename ForwardIterator,
typename OutputBuffers,
std::size_t ...IIs>
auto eval_sequence_entire_node(const DpfKey & dpf, ForwardIterator begin, ForwardIterator end,
OutputBuffers && outbufs, std::index_sequence<IIs...>)
{
static constexpr std::size_t outputs_per_leaf = DpfKey::outputs_per_leaf;
using input_type = typename DpfKey::input_type;
constexpr bool any_packed =
(utils::is_packed_subbyte_v<typename DpfKey::concrete_output_type<Is>> || ...);
constexpr bool any_bit =
(std::is_same_v<typename DpfKey::concrete_output_type<Is>, dpf::bit> || ...);
constexpr bool integral_in = std::is_integral_v<input_type>;
// Bit and packed leaves are proxy buffers. The interval scatter writes
// ordinary word slots; those outputs stay on the path memoizer.
constexpr bool interval_ok = integral_in && !any_packed && !any_bit;
auto write_point = [&](auto & path, std::size_t i, const input_type & x)
{
if constexpr (any_packed)
{
auto nodes = std::make_tuple(dpf::eval_point<Is>(dpf, x, path).node...);
(store_leaf_bytes(utils::get<IIs>(outbufs), i, std::get<IIs>(nodes)), ...);
}
else
{
auto temp = std::make_tuple(dpf::eval_point<Is>(dpf, x, path).node...);
(utils::raw_memcpy(&utils::get<IIs>(outbufs)[i * outputs_per_leaf],
&utils::get<IIs>(temp),
sizeof(typename DpfKey::concrete_output_type<Is>) * outputs_per_leaf), ...);
}
};
if constexpr (interval_ok)
{
using iter_cat = typename std::iterator_traits<ForwardIterator>::iterator_category;
bool classify = true;
if constexpr (std::is_base_of_v<std::random_access_iterator_tag, iter_cat>)
{
// Below this length the interval-run cover does not apply, so skip
// the classification pass.
classify = static_cast<std::size_t>(std::distance(begin, end))
>= sequence_interval_run;
}
if (classify)
{
bool sorted = true;
std::size_t n = 0;
std::size_t longest = 0;
std::size_t cur = 0;
bool dup = false;
input_type prev{};
for (auto it = begin; it != end; ++it, ++n)
{
if (n == 0)
{
prev = *it;
longest = 1;
cur = 1;
continue;
}
const input_type x = *it;
if (x < prev)
sorted = false;
else if (x == prev)
dup = true;
if (x == static_cast<input_type>(prev + input_type{1}))
{
++cur;
if (cur > longest)
longest = cur;
}
else
cur = 1;
prev = x;
}
if (sorted && longest >= sequence_interval_run)
{
std::size_t i = 0;
for (auto it = begin; it != end; )
{
const input_type run_from = *it;
input_type run_to = run_from;
auto run_end = it;
++run_end;
while (run_end != end
&& *run_end == static_cast<input_type>(run_to + input_type{1}))
{
run_to = *run_end;
++run_end;
}
const auto span_n = static_cast<std::size_t>(std::distance(it, run_end));
if (span_n >= sequence_interval_run)
{
scatter_interval_run<true, Is...>(dpf, run_from, run_to, outbufs, i,
std::index_sequence<IIs...>{});
i += span_n;
}
else
{
auto path = make_basic_path_memoizer(dpf);
for (auto p = it; p != run_end; ++p, ++i)
write_point(path, i, *p);
}
it = run_end;
}
return utils::make_tuple(
dpf::subsequence_iterable<DpfKey, decltype(std::begin(utils::get<IIs>(outbufs))), ForwardIterator>(std::begin(utils::get<IIs>(outbufs)), begin, end)...);
}
// Breadth-first pays for a block split at every level. On a short
// tree (8-bit inputs) that overhead loses to the path memoizer.
// The one-output overload is the only one; keep it out of the
// multi-output instantiation.
if constexpr (sizeof...(Is) == 1)
{
if (sorted && !dup && n >= sequence_breadth_points
&& DpfKey::depth >= 16)
{
eval_sequence_breadth_first<Is...>(dpf, begin, end, utils::get<0>(outbufs));
return utils::make_tuple(
dpf::subsequence_iterable<DpfKey, decltype(std::begin(utils::get<IIs>(outbufs))), ForwardIterator>(std::begin(utils::get<IIs>(outbufs)), begin, end)...);
}
}
}
}
const std::size_t nseq = static_cast<std::size_t>(std::distance(begin, end));
if constexpr (sizeof...(Is) == 1 && !DpfKey::is_verifiable
&& prg_has_indep4<typename DpfKey::interior_prg>::value)
{
if (nseq >= 16)
{
using input_type = typename DpfKey::input_type;
std::vector<input_type> walk(nseq);
std::vector<input_type> lane(nseq);
std::size_t i = 0;
for (auto it = begin; it != end; ++it, ++i)
{
lane[i] = dpf.offset_x(*it);
walk[i] = lane[i];
utils::flip_msb_if_signed_integral(walk[i]);
}
const auto leaves = sequence_wide_leaves(dpf, walk.data(), nseq);
constexpr std::size_t ids[] = {Is...};
constexpr std::size_t I0 = ids[0];
using output_type = typename DpfKey::concrete_output_type<I0>;
auto & dst = utils::get<0>(outbufs);
for (std::size_t p = 0; p < nseq; ++p)
{
auto wrapped = make_eval_dpf_output<DpfKey, output_type>(
dpf.template traverse_exterior<I0>(leaves[p]), lane[p]);
utils::raw_memcpy(&dst[p * outputs_per_leaf], &wrapped.node,
sizeof(output_type) * outputs_per_leaf);
}
return utils::make_tuple(
dpf::subsequence_iterable<DpfKey, decltype(std::begin(utils::get<IIs>(outbufs))), ForwardIterator>(std::begin(utils::get<IIs>(outbufs)), begin, end)...);
}
}
auto path = make_basic_path_memoizer(dpf);
std::size_t i = 0;
for (auto it = begin; it != end; ++it, ++i)
write_point(path, i, *it);
return utils::make_tuple(
dpf::subsequence_iterable<DpfKey, decltype(std::begin(utils::get<IIs>(outbufs))), ForwardIterator>(std::begin(utils::get<IIs>(outbufs)), begin, end)...);
}
template <typename Slot, typename Val>
HEDLEY_ALWAYS_INLINE
void assign_eval_slot(Slot && slot, Val && val)
{
assign_share_slot(slot, std::forward<Val>(val));
}
template <std::size_t ...Is,
typename DpfKey,
typename ForwardIterator,
typename OutputBuffers,
std::size_t ...IIs>
auto eval_sequence_output_only(const DpfKey & dpf, ForwardIterator begin, ForwardIterator end, OutputBuffers && outbufs,
std::index_sequence<IIs...>)
{
using input_type = typename DpfKey::input_type;
constexpr bool any_packed =
(utils::is_packed_subbyte_v<typename DpfKey::concrete_output_type<Is>> || ...);
constexpr bool any_bit =
(std::is_same_v<typename DpfKey::concrete_output_type<Is>, dpf::bit> || ...);
constexpr bool integral_in = std::is_integral_v<input_type>;
auto write_point = [&](auto & path, std::size_t i, const input_type & x)
{
(assign_eval_slot(utils::get<IIs>(outbufs)[i],
*dpf::eval_point<Is>(dpf, x, path)), ...);
};
auto finish = [&](std::size_t i)
{
if (i == 0)
{
return utils::make_tuple(subinterval_iterable(std::begin(utils::get<IIs>(outbufs)), utils::size(utils::get<IIs>(outbufs)), 0, 0, 0, 0, false)...);
}
return utils::make_tuple(subinterval_iterable(std::begin(utils::get<IIs>(outbufs)), utils::size(utils::get<IIs>(outbufs)), 0, i - 1, 0, 0)...);
};
if constexpr (integral_in && !any_packed && !any_bit)
{
bool sorted = true;
std::size_t longest = 0;
std::size_t cur = 0;
std::size_t n = 0;
input_type prev{};
for (auto it = begin; it != end; ++it, ++n)
{
if (n == 0)
{
prev = *it;
longest = 1;
cur = 1;
continue;
}
const input_type x = *it;
if (x < prev)
sorted = false;
if (x == static_cast<input_type>(prev + input_type{1}))
{
++cur;
if (cur > longest)
longest = cur;
}
else
cur = 1;
prev = x;
}
if (sorted && longest >= sequence_interval_run)
{
auto path = make_basic_path_memoizer(dpf);
std::size_t i = 0;
for (auto it = begin; it != end; )
{
const input_type run_from = *it;
input_type run_to = run_from;
auto run_end = it;
++run_end;
while (run_end != end
&& *run_end == static_cast<input_type>(run_to + input_type{1}))
{
run_to = *run_end;
++run_end;
}
const auto span_n = static_cast<std::size_t>(std::distance(it, run_end));
if (span_n >= sequence_interval_run)
{
scatter_interval_run<false, Is...>(dpf, run_from, run_to, outbufs, i,
std::index_sequence<IIs...>{});
i += span_n;
}
else
{
for (auto p = it; p != run_end; ++p, ++i)
write_point(path, i, *p);
}
it = run_end;
}
return finish(i);
}
}
auto path = make_basic_path_memoizer(dpf);
std::size_t i = 0;
for (auto it = begin; it != end; ++it, ++i)
write_point(path, i, *it);
return finish(i);
}
} // namespace internal
/// \complexity O(n k) interior traversals in the worst case and O(k) node workspace. k is the number of listed points and n is `depth`. The breadth-first buffer is 2k nodes, so each level traverses at most one node per point. Shared prefixes do fewer traversals. A recipe memoizer instead stores O(recipe leaf nodes) (see that memoizer).
template <std::size_t I = 0,
std::size_t ...Is,
typename DpfKey,
typename ForwardIterator,
typename OutputBuffers,
typename ReturnType = return_entire_node_tag_,
std::enable_if_t<looks_like_dpf_key_v<DpfKey> && !is_multilevel_key_v<DpfKey>, bool> = true,
std::enable_if_t<std::is_base_of_v<return_type_tag_, ReturnType>, bool> = true,
std::enable_if_t<!std::is_base_of_v<return_type_tag_, OutputBuffers>, bool> = true>
inline auto eval_sequence(const DpfKey & dpf, ForwardIterator begin, ForwardIterator end,
OutputBuffers && outbufs, ReturnType return_type = ReturnType{})
{
(void)return_type;
static_assert(std::is_same_v<ReturnType, return_entire_node_tag_> ||
std::is_same_v<ReturnType, return_output_only_tag_>);
if constexpr(std::is_same_v<ReturnType, return_entire_node_tag_>)
{
return internal::eval_sequence_entire_node<I, Is...>(dpf, begin, end, outbufs, std::make_index_sequence<1+sizeof...(Is)>{});
}
else
{
return internal::eval_sequence_output_only<I, Is...>(dpf, begin, end, outbufs, std::make_index_sequence<1+sizeof...(Is)>{});
}
}
/// @brief Evaluate a sorted sequence and fold the same VDPF proof as `prove_sequence`.
/// @details Uses interval-run covers (not a path-memo double walk). Values are
/// written by a single subsequent `eval_sequence` without proof.
/// \complexity O(n k) interior traversals in the worst case and O(k) node workspace. k is the number of listed points and n is `depth`. The breadth-first buffer is 2k nodes, so each level traverses at most one node per point. Shared prefixes do fewer traversals. A recipe memoizer instead stores O(recipe leaf nodes) (see that memoizer).
template <std::size_t I = 0,
std::size_t ...Is,
typename DpfKey,
typename ForwardIterator,
typename OutputBuffers,
typename ReturnType = return_entire_node_tag_,
std::enable_if_t<looks_like_dpf_key_v<DpfKey> && !is_multilevel_key_v<DpfKey>, bool> = true,
std::enable_if_t<std::is_base_of_v<return_type_tag_, ReturnType>, bool> = true,
std::enable_if_t<!std::is_base_of_v<return_type_tag_, OutputBuffers>, bool> = true>
inline auto eval_sequence(const DpfKey & dpf, ForwardIterator begin, ForwardIterator end,
OutputBuffers && outbufs, prove_ref pr, ReturnType return_type = ReturnType{})
{
static_assert(DpfKey::is_verifiable,
"eval_sequence(..., prove(π)): key must carry dpf::verifiable");
prove_sequence(dpf, begin, end, pr);
return eval_sequence<I, Is...>(dpf, begin, end,
std::forward<OutputBuffers>(outbufs), return_type);
}
/// @brief Evaluate a sorted sequence and fold each written output into a sketch.
/// \complexity O(n k) interior traversals in the worst case and O(k) node workspace. k is the number of listed points and n is `depth`. The breadth-first buffer is 2k nodes, so each level traverses at most one node per point. Shared prefixes do fewer traversals. A recipe memoizer instead stores O(recipe leaf nodes) (see that memoizer).
template <std::size_t I = 0,
std::size_t ...Is,
typename DpfKey,
typename ForwardIterator,
typename OutputBuffers,
typename ReturnType = return_entire_node_tag_,
std::enable_if_t<looks_like_dpf_key_v<DpfKey> && !is_multilevel_key_v<DpfKey>, bool> = true,
std::enable_if_t<std::is_base_of_v<return_type_tag_, ReturnType>, bool> = true,
std::enable_if_t<!std::is_base_of_v<return_type_tag_, OutputBuffers>, bool> = true>
inline auto eval_sequence(const DpfKey & dpf, ForwardIterator begin, ForwardIterator end,
OutputBuffers && outbufs, sketch_ref & sk, ReturnType return_type = ReturnType{})
{
static_assert(DpfKey::is_extractable,
"eval_sequence(..., sketch(σ)): key must carry dpf::extractable");
auto ret = eval_sequence<I, Is...>(dpf, begin, end, outbufs, return_type);
if constexpr (sizeof...(Is) == 0)
{
for (std::size_t k = 0; k < utils::size(outbufs); ++k)
sk.absorb(outbufs[k]);
}
return ret;
}
/// @brief Evaluate the sorted range `[begin, end)`, allocating a buffer.
/// @tparam I output index
/// @tparam Is is
/// @tparam DpfKey DPF key type
/// @tparam ForwardIterator forward iterator type
/// @tparam ReturnType return type
/// @tparam DpfKey DPF key type
/// @tparam ReturnType return type
/// @param return_type `return_entire_node_tag_{}` or `return_output_only_tag_{}`.
/// @param dpf the DPF key
/// @param begin the iterator to the first query
/// @param end the iterator past the last query
/// @return Pair of buffer (or tuple of buffers) and an iterable in list order.
/// \complexity O(n k) interior traversals in the worst case and O(k) node workspace. k is the number of listed points and n is `depth`. The breadth-first buffer is 2k nodes, so each level traverses at most one node per point. Shared prefixes do fewer traversals. A recipe memoizer instead stores O(recipe leaf nodes) (see that memoizer).
template <std::size_t I = 0,
std::size_t ...Is,
typename DpfKey,
typename ForwardIterator,
typename ReturnType = return_entire_node_tag_,
std::enable_if_t<looks_like_dpf_key_v<DpfKey> && !is_multilevel_key_v<DpfKey>, bool> = true,
std::enable_if_t<std::is_base_of_v<return_type_tag_, ReturnType>, bool> = true>
auto eval_sequence(const DpfKey & dpf, ForwardIterator begin, ForwardIterator end,
ReturnType return_type = ReturnType{})
{
auto outbufs = utils::make_tuple(
make_output_buffer_for_subsequence<I>(dpf, begin, end, return_type),
make_output_buffer_for_subsequence<Is>(dpf, begin, end, return_type)...);
// moving `outbufs` is allowed as the `outbufs` are `std::vectors`
// the underlying data remains on the heap
// and thus the data the iterable refers to is still valid
auto iterable = eval_sequence<I, Is...>(dpf, begin, end, outbufs, return_type);
return std::make_pair(std::move(outbufs), std::move(iterable));
}
/// \complexity O(n k) interior traversals in the worst case and O(k) node workspace. k is the number of listed points and n is `depth`. The breadth-first buffer is 2k nodes, so each level traverses at most one node per point. Shared prefixes do fewer traversals. A recipe memoizer instead stores O(recipe leaf nodes) (see that memoizer).
template <std::size_t I = 0,
typename DpfKey,
typename ForwardIterator,
typename OutputBuffer>
inline auto eval_sequence_breadth_first(const DpfKey & dpf, ForwardIterator begin, ForwardIterator end, OutputBuffer && outbuf)
{
assert_not_wildcard_output<I>(dpf);
using dpf_type = DpfKey;
using input_type = typename DpfKey::input_type;
using node_type = typename DpfKey::interior_node;
using output_type = typename DpfKey::concrete_output_type<I>;
HEDLEY_PRAGMA(GCC diagnostic push)
HEDLEY_PRAGMA(GCC diagnostic ignored "-Wignored-attributes")
using allocator = aligned_allocator<typename DpfKey::interior_node>;
HEDLEY_PRAGMA(GCC diagnostic pop)
using unique_ptr = typename allocator::unique_ptr;
allocator alloc = allocator{};
if (HEDLEY_UNLIKELY(!std::is_sorted(begin, end)))
{
throw std::runtime_error("list must be sorted");
}
if (begin == end)
{
return subsequence_iterable<DpfKey, decltype(std::begin(outbuf)), ForwardIterator>(
std::begin(outbuf), begin, end);
}
auto mask = dpf_type::msb_mask;
std::size_t nodes_in_sequence = std::distance(begin, end);
unique_ptr memo{alloc.allocate_unique_ptr(nodes_in_sequence*2)};
bool curhalf = (dpf_type::depth ^ 1) & 1;
memo[!curhalf*nodes_in_sequence + 0] = dpf.root();
std::vector<ForwardIterator> splits;
splits.reserve(nodes_in_sequence + 1);
splits.push_back(begin);
splits.push_back(end);
std::size_t level_index = 1;
auto func = [&](const bool flip = false)
{
std::size_t i = 0, j = 0;
const node_type cw[2] = {
dpf.correction_word(level_index-1, 0),
dpf.correction_word(level_index-1, 1)
};
const bool is_last = dpf_type::tree::is_last_level(level_index - 1,
dpf_type::depth);
// `lower` and `upper` are always adjacent elements of `splits` with `lower` < `upper`
// [lower, upper) = "block"
for (std::size_t s = 0; s + 1 < splits.size(); ++s)
{
auto lower = splits[s];
auto upper = splits[s + 1];
// `upper_bound()` returns iterator to first element where the relevant bit (based on `mask`) is set
auto it = std::upper_bound(lower, upper, mask,
[&flip](auto a, auto b){ return static_cast<bool>(a&b) ^ flip; });
if (it == lower) // right only since first element in "block" requires right traversal
{
memo[curhalf*nodes_in_sequence + i++] = dpf_type::traverse_interior(memo[!curhalf*nodes_in_sequence + j++], cw[1], 1, is_last);
}
else if (it == upper) // left only since no element in "block" requires right traversal
{
memo[curhalf*nodes_in_sequence + i++] = dpf_type::traverse_interior(memo[!curhalf*nodes_in_sequence + j++], cw[0], 0, is_last);
}
else // both ways since some (non-lower) element within "block" requires right traversal
{
auto cur_node = memo[!curhalf*nodes_in_sequence + j++];
auto kids = dpf_type::traverse_interior01(cur_node, cw[0], cw[1], is_last);
memo[curhalf*nodes_in_sequence + i++] = kids[0];
memo[curhalf*nodes_in_sequence + i++] = kids[1];
splits.insert(splits.begin() + static_cast<std::ptrdiff_t>(s + 1), it);
++s; // skip the newly inserted right-block start
}
}
};
if (dpf_type::depth >= level_index)
{
func(utils::uses_signed_msb_v<input_type>);
++level_index;
mask >>= 1;
curhalf =! curhalf;
}
for (; level_index <= dpf_type::depth; ++level_index, mask>>=1, curhalf=!curhalf)
{
func();
}
HEDLEY_PRAGMA(GCC diagnostic push)
HEDLEY_PRAGMA(GCC diagnostic ignored "-Wignored-attributes")
auto cw = dpf.template leaf<I>();
HEDLEY_PRAGMA(GCC diagnostic pop)
auto buf = memo.get();
constexpr auto clz = utils::countl_zero_symmetric_difference<input_type>{};
auto curr = begin, prev = curr;
for (std::size_t i = 0, j = 0; i < nodes_in_sequence; ++i)
{
j += (clz(*prev, *curr)) < dpf_type::depth;
auto leaf = dpf_type::template traverse_exterior<I>(buf[j],
get_if_lo_bit(cw, buf[j]));
if constexpr (utils::is_packed_subbyte_v<output_type>)
{
store_leaf_bytes(outbuf, i, leaf);
}
else
{
utils::raw_memcpy(&outbuf[i*dpf_type::outputs_per_leaf], &leaf, sizeof(output_type)*dpf_type::outputs_per_leaf);
}
prev = curr++;
}
return subsequence_iterable<DpfKey, decltype(std::begin(outbuf)), ForwardIterator>(std::begin(outbuf), begin, end);
}
/// \complexity O(n k) interior traversals in the worst case and O(k) node workspace. k is the number of listed points and n is `depth`. The breadth-first buffer is 2k nodes, so each level traverses at most one node per point. Shared prefixes do fewer traversals. A recipe memoizer instead stores O(recipe leaf nodes) (see that memoizer).
template <std::size_t I = 0,
typename DpfKey,
typename ForwardIterator>
auto eval_sequence_breadth_first(const DpfKey & dpf, ForwardIterator begin, ForwardIterator end)
{
auto outbuf = make_output_buffer_for_subsequence<I>(dpf, begin, end);
// moving `outbuf` is allowed as `outbuf` is a `std::vectors`
// the underlying data remains on the heap
// and thus the data the iterable refers to is still valid
auto iterable = eval_sequence_breadth_first<I>(dpf, begin, end, outbuf);
return std::make_pair(std::move(outbuf), std::move(iterable));
}
namespace internal
{
template <typename DpfKey,
typename SequenceMemoizer>
inline auto eval_sequence_interior(const DpfKey & dpf, const sequence_recipe & recipe,
SequenceMemoizer && memoizer, std::size_t to_level = DpfKey::depth)
{
using dpf_type = DpfKey;
using node_type = typename DpfKey::interior_node;
// level_index represents the current level being built
// level_index = 0 => root
// level_index = depth => last layer of interior nodes
if (recipe.num_leaf_nodes() == 0)
return;
std::size_t level_index = memoizer.assign_dpf(dpf, recipe);
std::size_t recipe_index = recipe.level_endpoints()[level_index-1];
std::size_t nodes_at_level = memoizer.get_nodes_at_level(level_index-1);
for (; level_index <= to_level; level_index = memoizer.advance_level(), nodes_at_level = memoizer.get_nodes_at_level(level_index-1))
{
const node_type cw[2] = {
dpf.correction_word(level_index-1, 0),
dpf.correction_word(level_index-1, 1)
};
const bool is_last = dpf_type::tree::is_last_level(level_index - 1,
dpf_type::depth);
auto prevbuf = memoizer[level_index-1];
auto currbuf = memoizer[level_index];
DPF_UNROLL_LOOP
for (std::size_t input_index = 0, output_index = 0; input_index < nodes_at_level; ++input_index, ++recipe_index)
{
if (memoizer.traverse_first(recipe_index) == true)
{
bool dir = memoizer.get_direction(0);
currbuf[output_index++] = dpf_type::traverse_interior(prevbuf[input_index], cw[dir], dir, is_last);
}
if (memoizer.traverse_second(recipe_index) == true)
{
bool dir = memoizer.get_direction(1);
currbuf[output_index++] = dpf_type::traverse_interior(prevbuf[input_index], cw[dir], dir, is_last);
}
}
}
}
template <std::size_t I,
typename DpfKey,
typename OutputBuffer,
typename SequenceMemoizer>
inline auto eval_sequence_exterior_entire_node(const DpfKey & dpf, const sequence_recipe & recipe,
OutputBuffer && outbuf, SequenceMemoizer && memoizer)
{
assert_not_wildcard_output<I>(dpf);
using dpf_type = DpfKey;
using output_type = typename DpfKey::concrete_output_type<I>;
auto nodes_in_interval = recipe.num_leaf_nodes();
HEDLEY_PRAGMA(GCC diagnostic push)
HEDLEY_PRAGMA(GCC diagnostic ignored "-Wignored-attributes")
auto buf = memoizer[dpf.depth];
HEDLEY_PRAGMA(GCC diagnostic pop)
DPF_UNROLL_LOOP
for (std::size_t j = 0; j < nodes_in_interval; ++j)
{
auto leaf = dpf.template traverse_exterior<I>(buf[j]);
if constexpr (utils::is_packed_subbyte_v<output_type>)
{
store_leaf_bytes(outbuf, j, leaf);
}
else
{
utils::raw_memcpy(&outbuf[j*dpf_type::outputs_per_leaf], &leaf, sizeof(output_type)*dpf_type::outputs_per_leaf);
}
}
}
template <std::size_t I,
typename DpfKey,
typename OutputBuffer,
typename SequenceMemoizer>
inline auto eval_sequence_exterior_output_only(const DpfKey & dpf, const sequence_recipe & recipe,
OutputBuffer && outbuf, SequenceMemoizer && memoizer)
{
assert_not_wildcard_output<I>(dpf);
using dpf_type = DpfKey;
using output_type = typename DpfKey::concrete_output_type<I>;
HEDLEY_PRAGMA(GCC diagnostic push)
HEDLEY_PRAGMA(GCC diagnostic ignored "-Wignored-attributes")
auto cw = dpf.template leaf<I>();
HEDLEY_PRAGMA(GCC diagnostic pop)
using node_type = typename DpfKey::exterior_node;
using leaf_node_type = std::tuple_element_t<I, typename DpfKey::leaf_tuple>;
auto buf = memoizer[dpf.depth];
leaf_node_type node;
// DPF_UNROLL_LOOP
for (std::size_t i = 0, j = -1, prev = -1, curr;
i < recipe.output_indices().size();
prev = curr, ++i)
{
curr = recipe.output_indices()[i]/dpf_type::outputs_per_leaf;
if (prev != curr)
{
++j;
node = dpf_type::template traverse_exterior<I>(buf[j], get_if_lo_bit(cw, buf[j]));
}
auto v = extract_leaf<node_type, output_type>(node,
recipe.output_indices()[i] % dpf_type::outputs_per_leaf);
assign_share_slot(outbuf[i], v);
}
}
template <std::size_t ...Is,
typename DpfKey,
typename OutputBuffers,
typename SequenceMemoizer,
typename ReturnType = return_entire_node_tag_,
std::enable_if_t<std::is_base_of_v<return_type_tag_, ReturnType>, bool> = true,
std::size_t ...IIs>
auto eval_sequence(const DpfKey & dpf, const sequence_recipe & recipe,
OutputBuffers && outbufs, SequenceMemoizer && memoizer, ReturnType return_type, std::index_sequence<IIs...>)
{
(void)return_type;
internal::eval_sequence_interior(dpf, recipe, memoizer);
static_assert(std::is_same_v<ReturnType, return_entire_node_tag_> ||
std::is_same_v<ReturnType, return_output_only_tag_>);
if constexpr (std::is_same_v<ReturnType, return_entire_node_tag_>)
{
(internal::eval_sequence_exterior_entire_node<Is>(dpf, recipe, utils::get<IIs>(outbufs), memoizer), ...);
return utils::make_tuple(
recipe_subsequence_iterable(std::begin(utils::get<IIs>(outbufs)), recipe.output_indices())...);
}
else
{
(internal::eval_sequence_exterior_output_only<Is>(dpf, recipe, utils::get<IIs>(outbufs), memoizer), ...);
const auto nout = recipe.output_indices().size();
if (nout == 0)
{
return utils::make_tuple(subinterval_iterable(std::begin(utils::get<IIs>(outbufs)), utils::size(utils::get<IIs>(outbufs)), 0, 0, 0, 0, false)...);
}
return utils::make_tuple(subinterval_iterable(std::begin(utils::get<IIs>(outbufs)), utils::size(utils::get<IIs>(outbufs)), 0, nout-1, 0, 0)...);
}
}
} // namespace internal
/// @brief Evaluate `recipe` into a named buffer, reusing `memoizer`.
/// @tparam I output index
/// @tparam Is is
/// @tparam DpfKey DPF key type
/// @tparam OutputBuffers tuple of output buffers
/// @tparam SequenceMemoizer sequence memoizer
/// @tparam ReturnType return type
/// @tparam SequenceMemoizer sequence memoizer
/// @tparam ReturnType return type
/// @param recipe The same object `memoizer` was constructed from.
/// @param outbufs Named buffer. The returned iterable refers into it.
/// @param dpf the DPF key
/// @param memoizer the memoizer built for this key
/// @param return_type `return_entire_node_tag_{}` or `return_output_only_tag_{}`
/// @return the evaluation result
/// \complexity O(n k) interior traversals in the worst case and O(k) node workspace. k is the number of listed points and n is `depth`. The breadth-first buffer is 2k nodes, so each level traverses at most one node per point. Shared prefixes do fewer traversals. A recipe memoizer instead stores O(recipe leaf nodes) (see that memoizer).
template <std::size_t I = 0,
std::size_t ...Is,
typename DpfKey,
typename OutputBuffers,
typename SequenceMemoizer,
typename ReturnType = return_entire_node_tag_,
std::enable_if_t<!std::is_base_of_v<return_type_tag_, SequenceMemoizer>, bool> = true,
std::enable_if_t<std::is_base_of_v<return_type_tag_, ReturnType>, bool> = true>
HEDLEY_ALWAYS_INLINE
auto eval_sequence(const DpfKey & dpf, const sequence_recipe & recipe,
OutputBuffers & outbufs, SequenceMemoizer && memoizer, // NOLINT(runtime/references)
ReturnType return_type = ReturnType{})
{
assert_not_wildcard_output<I, Is...>(dpf);
assert_not_wildcard_input(dpf);
return internal::eval_sequence<I, Is...>(dpf, recipe, outbufs, memoizer, return_type, std::make_index_sequence<1+sizeof...(Is)>());
}
/// \complexity O(n k) interior traversals in the worst case and O(k) node workspace. k is the number of listed points and n is `depth`. The breadth-first buffer is 2k nodes, so each level traverses at most one node per point. Shared prefixes do fewer traversals. A recipe memoizer instead stores O(recipe leaf nodes) (see that memoizer).
template <std::size_t I = 0,
std::size_t ...Is,
typename DpfKey,
typename OutputBuffers,
typename ReturnType = return_entire_node_tag_,
std::enable_if_t<!std::is_base_of_v<sequence_memoizer_tag_,
std::decay_t<OutputBuffers>>, bool> = true,
std::enable_if_t<std::is_base_of_v<return_type_tag_, ReturnType>, bool> = true>
HEDLEY_ALWAYS_INLINE
auto eval_sequence(const DpfKey & dpf, const sequence_recipe & recipe,
OutputBuffers & outbufs, ReturnType return_type = ReturnType{}) // NOLINT(runtime/references)
{
return eval_sequence<I, Is...>(dpf, recipe, outbufs,
dpf::make_double_space_sequence_memoizer<DpfKey>(recipe), return_type);
}
/// \complexity O(n k) interior traversals in the worst case and O(k) node workspace. k is the number of listed points and n is `depth`. The breadth-first buffer is 2k nodes, so each level traverses at most one node per point. Shared prefixes do fewer traversals. A recipe memoizer instead stores O(recipe leaf nodes) (see that memoizer).
template <std::size_t I = 0,
std::size_t ...Is,
typename DpfKey,
typename SequenceMemoizer,
typename ReturnType = return_entire_node_tag_,
std::enable_if_t<std::is_base_of_v<sequence_memoizer_tag_,
std::decay_t<SequenceMemoizer>>, bool> = true,
std::enable_if_t<std::is_base_of_v<return_type_tag_, ReturnType>, bool> = true>
HEDLEY_ALWAYS_INLINE
auto eval_sequence(const DpfKey & dpf, const sequence_recipe & recipe,
SequenceMemoizer && memoizer, ReturnType return_type = ReturnType{})
{
auto outbufs = utils::make_tuple(
make_output_buffer_for_recipe_subsequence<I>(dpf, recipe, return_type),
make_output_buffer_for_recipe_subsequence<Is>(dpf, recipe, return_type)...);
// moving `outbufs` is allowed as the `outbufs` are `std::vectors`
// the underlying data remains on the heap
// and thus the data the iterable refers to is still valid
auto iterable = eval_sequence<I, Is...>(dpf, recipe, outbufs, memoizer, return_type);
return std::make_pair(std::move(outbufs), std::move(iterable));
}
/// \complexity O(n k) interior traversals in the worst case and O(k) node workspace. k is the number of listed points and n is `depth`. The breadth-first buffer is 2k nodes, so each level traverses at most one node per point. Shared prefixes do fewer traversals. A recipe memoizer instead stores O(recipe leaf nodes) (see that memoizer).
template <std::size_t I = 0,
std::size_t ...Is,
typename DpfKey,
typename ReturnType = return_entire_node_tag_,
std::enable_if_t<std::is_base_of_v<return_type_tag_, ReturnType>, bool> = true>
HEDLEY_ALWAYS_INLINE
auto eval_sequence(const DpfKey & dpf, const sequence_recipe & recipe,
ReturnType return_type = ReturnType{})
{
return eval_sequence<I, Is...>(dpf, recipe,
dpf::make_double_space_sequence_memoizer<DpfKey>(recipe), return_type);
}
/// @brief Fold a sorted sequence into `pr` via interval-run covers.
/// @details Maximal contiguous runs use once-per-BFS-node `prove_fold_interval`.
/// Isolated points are length-1 runs. Both parties must see the same
/// sorted list. Prefer this over path-memo when the list is dense.
template <typename KeyT, typename ForwardIterator>
void prove_sequence(const KeyT & key, ForwardIterator begin, ForwardIterator end,
prove_ref pr)
{
static_assert(KeyT::is_verifiable,
"prove_sequence: key must carry dpf::verifiable");
if (HEDLEY_UNLIKELY(begin != end && !std::is_sorted(begin, end)))
throw std::runtime_error("list must be sorted");
detail::vdpf::init_proof(pr.token, key);
using input_type = typename KeyT::input_type;
for (auto it = begin; it != end; )
{
const auto run_from = static_cast<input_type>(*it);
auto run_to = run_from;
++it;
while (it != end)
{
const auto next = static_cast<input_type>(*it);
if (next != static_cast<input_type>(run_to + input_type{1}))
break;
run_to = next;
++it;
}
prove_fold_interval(key, run_from, run_to, pr.token);
}
detail::vdpf::fold_output_binding(pr.token, key);
}
} // namespace dpf
#endif // LIBDPF_INCLUDE_DPF_EVAL_SEQUENCE_HPP__