Annotate noexcept and constexpr with HEDLEY, and add interval containment, ChaCha, and the dyadic range tables.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Ryan Henry 2026-09-24 20:44:07 -06:00
parent 875f09fec1
commit 0d8a5a8131
97 changed files with 9212 additions and 1159 deletions

View file

@ -1,34 +1,126 @@
<!-- # Evaluating DPFs {#evalaution} -->
Once a `DPF` generated, the *eval_* * functions are used to evaluate differents inputs.
The appropriate function depends on your specific needs. If you only need the DPF's output
for a single input value, use `eval_point`. For evaluating a continuous range of inputs,
`eval_interval` is suitable. To analyze the DPF's behavior across its entire domain, use `eval_full`.
The code likely offers different implementations of memoization and output buffers, allowing you to
optimize for memory usage or execution speed depending on your needs.\n
Using a PRG while making a `DPF` allows the user to check how much it cost to manipulate the `DPF`s.
<!-- # Evaluating DPFs {#evaluation} -->
# Memoizers
The `memoizers` remembers the most used path while the DPF is being created. These are usefull functions
to improve the speed and the cost of execution.
`make_dpf(x, y)` returns one key per party. Evaluation of a key yields that
party's share. Leaf outputs are subtractive shares: open them with
`dpf::reconstruct`, which computes `share0 - share1`. Comparison outputs are
additive shares: `reconstruct` computes `share0 + share1`. A single-output
`eval_point` returns a small handle; `*handle` is the share. An unassigned
`dpf::wildcard` output throws `std::runtime_error`.
`[from, to]` is inclusive. The points passed to `eval_sequence` are a
nondecreasing range; an unsorted range throws `std::runtime_error`.
Memoizers hold interior nodes between calls. Output buffers hold the shares
a multi-point evaluation writes. Pass both as mutable named objects when a
later call should reuse them. The factories
`make_basic_path_memoizer`, `make_basic_interval_memoizer`, and
`make_*_sequence_memoizer` unwrap `party_key`, so a workspace built from
either party's type accepts both parties. Name that type with
`dpf::unwrap_party_key_t<std::decay_t<decltype(key)>>`.
# Memoizers {#memoizers}
## Path memoizers {#path_memoizers}
`eval_point` walks one root-to-leaf path. `make_basic_path_memoizer<Key>()`
keeps every node of the previous point, and the next point recomputes only
the suffix after the common prefix. `make_nonmemoizing_path_memoizer<Key>()`
keeps one node and starts from the root on every call. A one-off
`eval_point(key, x)` uses the nonmemoizing memoizer.
Pass the memoizer as a mutable lvalue. The default argument is a new
temporary, so it has no previous point to resume from. Keep a separate
memoizer for each key you are in the middle of evaluating. A different root
restarts the path.
**Code samples**\n
For instance, in the code below the utilization of `dpf::make_basic_path_memoizer` reduced by 10 the time of execution
compare to the code that is commented that doesn't use the `memoizers`.
<div class="tabbed">
- <b class="tab-title">memoizers.cpp</b> \include{cpp} evaluation/memoizers.cpp
</div>
## Interval memoizers {#interval_memoizers}
# dpf::eval_point
This function evaluate a single input of a DPF. The XOR result of the `eval_point` for both shares will only be equal to 1 if it represents the correct input in both evaluations. As input arguments it uses the `share` and the input to evaluate (note: it can't be a wildcard, otherwise it will throw an error).\n
`eval_interval` and `eval_full` expand every leaf in a range.
`make_basic_interval_memoizer<Key>(from, to)` stores two levels of that
range. That is the workspace the convenience overloads allocate.
`make_full_tree_interval_memoizer<Key>(from, to)` keeps every level.
`make_basic_full_memoizer<Key>()` and `make_full_tree_full_memoizer<Key>()`
are the same workspaces sized for the whole domain.
**See also**\n
PIR
Size the memoizer for the widest interval you will pass to it. A wider
interval throws `std::length_error`. The same key and the same endpoints
leave the final interior level in place. A different key or a different
interval rebuilds into the same allocation.
**Pro tip**\n
Use the `dpf::pathmemoizer` for a faster execution.\n
Passing only the memoizer still allocates a fresh output buffer and returns
`std::pair(buffer, iterable)`.
## Sequence memoizers {#sequence_memoizers}
`make_sequence_recipe<Key>(begin, end)` compiles a sorted point list into a
traversal. The recipe depends on the input type, and one recipe serves every
key of that type.
A sequence memoizer stores a reference to the recipe object it was built
from and checks later calls by address. Pass that same object, and keep the
recipe alive for as long as the memoizer is used. A copy of the recipe
throws `std::logic_error`.
`make_double_space_sequence_memoizer<Key>(recipe)` keeps two levels. It is
what `eval_sequence(key, recipe, buffer)` allocates when you omit the
memoizer. `make_inplace_reversing_sequence_memoizer<Key>(recipe)` keeps one
level and reverses direction as it descends.
`make_full_tree_sequence_memoizer<Key>(recipe)` retains every level. A key
whose depth differs from the recipe throws `std::logic_error`.
# Output buffers {#output_buffers}
`output_buffer<T>` is move-only storage with `size`, iterators, `data`, and
`operator[]`. Build it with the factory that matches the evaluation:
- `make_output_buffer_for_interval(key, from, to)`
- `make_output_buffer_for_full(key)`
- `make_output_buffer_for_subsequence(key, begin, end, tag)`
- `make_output_buffer_for_recipe_subsequence(key, recipe, tag)`
On a `party_key`, leaf slots are `subtractive_share`s and comparison slots
are `additive_share`s. `dpf::bit`, `dpf::twobit`, and `dpf::nyble` slots are
packed. Trivially default-constructible slot types are left uninitialized;
the evaluation overwrites every slot it is responsible for.
`eval_interval` and recipe `eval_sequence` take the buffer as a non-const
reference, so the argument is a named object. The returned iterable refers
into that buffer. Read it while the buffer is alive, and only over the
points the iterable covers. The next evaluation overwrites those slots.
For one output, the convenience overload returns the buffer itself as the
first element of the pair. For several output indices it returns a tuple of
buffers.
`make_output_buffer(dpf::out<I>, key, from, to)` and
`make_output_buffer(dpf::cmp, key, n)` size a buffer for one channel of a
multi-output or comparison key. The slot types follow the same party-share
rule.
**Code samples**\n
<div class="tabbed">
- <b class="tab-title">output_buffers.cpp</b> \include{cpp} evaluation/output_buffers.cpp
</div>
# dpf::eval_point {#eval_point}
`eval_point(key, x)` evaluates output 0 at one input.
`eval_point<I>(key, x)` selects another output. Two or more indices,
`eval_point<0, 1>(key, x)`, return a tuple of shares rather than handles.
`eval_point(key, x, path)` continues a path memoizer.
`eval_point(dpf::out<I>, key, x, path)` and `eval_point(dpf::cmp, key, x, path)`
are the same walk with an explicit channel. Comparison results are additive
shares.
**Code samples**\n
<div class="tabbed">
@ -37,12 +129,13 @@ Use the `dpf::pathmemoizer` for a faster execution.\n
</div>
# dpf::eval_interval
This function evaluates a contiguous range of inputs. As input arguments it uses the `share` generated by `make_dpf`,
`from` and `to` for the range of inputs to evaluate.\n
# dpf::eval_interval {#eval_interval}
**See also**\n
PIR
`eval_interval(key, from, to)` evaluates every input from `from` through
`to`. The iterable yields one share per input, in that order. Optional
arguments are an output buffer and then an interval memoizer. An output
index pack, `eval_interval<0, 1>(key, from, to, buffers, memo)`, writes each
selected output.
**Code samples**\n
<div class="tabbed">
@ -51,8 +144,13 @@ PIR
</div>
# dpf::eval_full
This function evaluate all the passible inputs it only uses as argument the `share` of the `DPF` to evaluate.
# dpf::eval_full {#eval_full}
`eval_full(key)` is the closed interval from
`std::numeric_limits<Input>::min()` through `max()`. The buffer and
full-domain memoizer overloads match `eval_interval`.
`make_output_buffer_for_full(key)` and `make_basic_full_memoizer<Key>()`
size both for that domain.
**Code samples**\n
<div class="tabbed">
@ -61,14 +159,16 @@ This function evaluate all the passible inputs it only uses as argument the `sha
</div>
# dpf::eval_sequence {#eval_sequence}
# dpf::eval_sequence
This function evaluate a subset of inputs that is not contiguous (useful for a `DPF` made with `keyword`),
it uses as arguments the `share` generated by `make_dpf`, `from` and `to` for the subset of inputs to evaluate.\n
For a better utilization, you can use the `make_sequence_recipe` it takes as input a sorted list and returns a `recipe`.
The cost of creating is a little bit worse than just calling `eval_sequence`, however once the `recipe` created
`eval_sequence` is faster and has a better cost.
`eval_sequence(key, begin, end, tag)` evaluates a sorted list.
`dpf::return_output_only_tag_` stores one share per listed point.
`dpf::return_entire_node_tag_` stores whole leaves; it is the default.
The iterable still yields one share per listed point, in list order.
`eval_sequence(key, recipe, buffer, memo, tag)` repeats that list.
`memo` is a sequence memoizer bound to `recipe`. Omit `memo` to allocate a
`double_space` workspace for that call.
**Code samples**\n
<div class="tabbed">
@ -77,4 +177,22 @@ The cost of creating is a little bit worse than just calling `eval_sequence`, ho
</div>
# Output buffers
# Buffered PRG {#buffered_prg}
`dpf::randomness::buffered_prg<PRG, Ts...>` (alias
`dpf::randomness::aes_buffered_prg<Ts...>`) is a forward cursor with one
PRG stream per value type. `get<I>()` and `fill<I>(out, n)` consume the
cursor. `at<I>(index)` reads an absolute index and leaves the cursor where
it is. `sampled<I>()` is how far `get` and `fill` have advanced.
`per_stream_buffer_elems` is at least 1.
`dpf::randomness::lane_table<T>` is the seekable form for a runtime set of
roles. `value_at(role, index)` and `mask_at(role, index)` are independent
streams, and a repeated index returns the same element.
**Code samples**\n
<div class="tabbed">
- <b class="tab-title">buffered_prg.cpp</b> \include{cpp} evaluation/buffered_prg.cpp
</div>