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>
This commit is contained in:
Ryan Henry 2026-09-28 05:59:19 -06:00
parent 695f8e84f7
commit 0d22946a0e
1835 changed files with 170291 additions and 2849 deletions

976
include/grotto/carry.hpp Normal file
View file

@ -0,0 +1,976 @@
/// @file grotto/carry.hpp
/// @brief Carry-in, carry-out, and fused bitwidth corrections.
/// @details `plan_carry` (see carry_plan.hpp) selects the live primitives.
/// `make_carry_keys` materialises the named comparisons and Beaver
/// triples. Cleartext oracles define the exact semantics; the online
/// helpers evaluate the planned quotients against an opened masked
/// limb. Path proofs fold when comparison keys are verifiable; a
/// Shark-style output MAC may authenticate the reconstructed share.
/// @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_GROTTO_CARRY_HPP__
#define LIBDPF_INCLUDE_GROTTO_CARRY_HPP__
#include <cstdint>
#include <optional>
#include <stdexcept>
#include <type_traits>
#include <utility>
#include "hedley/hedley.h"
#include "dpf.hpp"
#include "grotto/carry_plan.hpp"
namespace grotto
{
// ---------------------------------------------------------------------------
// Cleartext oracles
// ---------------------------------------------------------------------------
/// @brief Mask of the low `bits` bits.
/// \complexity One shift. `Θ(1)`.
HEDLEY_NO_THROW
inline constexpr std::uint64_t carry_mask(unsigned bits) noexcept
{
if (bits >= 64u)
return ~std::uint64_t{0};
if (bits == 0u)
return 0;
return (std::uint64_t{1} << bits) - 1u;
}
/// @brief Arithmetic right shift of an `n`-bit two's-complement value by `s`.
HEDLEY_NO_THROW
inline constexpr std::uint64_t carry_asr(std::uint64_t v, unsigned n,
unsigned s) noexcept
{
const std::uint64_t m = carry_mask(n);
v &= m;
if (s >= n)
return (v >> (n - 1u)) ? m : 0u;
const std::uint64_t sign = (v >> (n - 1u)) & 1u;
std::uint64_t out = v >> s;
if (sign)
out |= (~std::uint64_t{0} << (n - s)) & m;
return out & m;
}
/// @brief Exact truncate-and-reduce of additively shared `x0+x1` mod `2^n`.
/// \complexity A constant number of shifts of the two shares. `Θ(1)`. This is the cleartext identity the keyed eval matches.
/// @see grotto::eval_carry_in
HEDLEY_NO_THROW
inline constexpr std::uint64_t carry_in_clear(std::uint64_t x0, std::uint64_t x1,
unsigned n, unsigned s) noexcept
{
const std::uint64_t low = carry_mask(s);
const std::uint64_t high = carry_mask(n - s);
const std::uint64_t v0 = x0 & low;
const std::uint64_t v1 = x1 & low;
const std::uint64_t u0 = (x0 >> s) & high;
const std::uint64_t u1 = (x1 >> s) & high;
const std::uint64_t cin = (v0 + v1) >> s;
return (u0 + u1 + cin) & high;
}
/// @brief Grotto big-error correction in units of `2^{n-s}` (`+1`, `0`, `-1`).
HEDLEY_NO_THROW
inline constexpr std::int64_t carry_out_units(std::uint64_t x0, std::uint64_t x1,
unsigned n, sign_knowledge sign) noexcept
{
const std::uint64_t s0 = (x0 >> (n - 1u)) & 1u;
const std::uint64_t s1 = (x1 >> (n - 1u)) & 1u;
const std::uint64_t x = (x0 + x1) & carry_mask(n);
const std::uint64_t m = (x >> (n - 1u)) & 1u;
if (sign == sign_knowledge::nonnegative)
return static_cast<std::int64_t>(s0 & s1);
if (sign == sign_knowledge::negative)
return -static_cast<std::int64_t>((1u - s0) & (1u - s1));
if (m == 0u)
return static_cast<std::int64_t>(s0 & s1);
return -static_cast<std::int64_t>((1u - s0) & (1u - s1));
}
/// @brief Local ASR of each share plus the Grotto carry-out correction.
HEDLEY_NO_THROW
inline constexpr std::uint64_t carry_out_clear(std::uint64_t x0, std::uint64_t x1,
unsigned n, unsigned s, sign_knowledge sign) noexcept
{
const std::uint64_t m = carry_mask(n);
const std::int64_t unit = static_cast<std::int64_t>(std::uint64_t{1} << (n - s));
const std::int64_t q = carry_out_units(x0, x1, n, sign);
const std::uint64_t base =
(carry_asr(x0, n, s) + carry_asr(x1, n, s)) & m;
return static_cast<std::uint64_t>(
static_cast<std::int64_t>(base) + q * unit) & m;
}
/// @brief Exact same-ring ASR (both carries).
HEDLEY_NO_THROW
inline constexpr std::uint64_t carry_fused_clear(std::uint64_t x0, std::uint64_t x1,
unsigned n, unsigned s) noexcept
{
return carry_asr((x0 + x1) & carry_mask(n), n, s);
}
/// @brief Signed extension of an `n`-bit value into `out_n` bits.
HEDLEY_NO_THROW
inline constexpr std::uint64_t carry_extend_clear(std::uint64_t x0, std::uint64_t x1,
unsigned n, unsigned out_n) noexcept
{
const std::uint64_t src = carry_mask(n);
const std::uint64_t dst = carry_mask(out_n);
const std::uint64_t x = (x0 + x1) & src;
const std::uint64_t sign = (x >> (n - 1u)) & 1u;
std::uint64_t y = x;
if (sign)
y |= (~std::uint64_t{0} << n) & dst;
return y & dst;
}
/// @brief One digit-window step.
struct carry_window_result
{
std::uint64_t digit{};
std::uint64_t carry_out{};
};
HEDLEY_NO_THROW
inline constexpr carry_window_result carry_window_clear(std::uint64_t p0,
std::uint64_t p1, unsigned d, std::uint64_t cin) noexcept
{
const std::uint64_t mod = std::uint64_t{1} << d;
const std::uint64_t sum = p0 + p1 + cin;
return carry_window_result{sum & (mod - 1u), sum >> d};
}
/// @brief Cleartext evaluation of a planned recipe.
/// \complexity A constant amount of bit arithmetic plus at most one `eval_point` on a keyed comparison (`eval_lt_party`) and, for the unknown-sign and window paths, the arithmetic in `beaver_bit_and` on shares that are already arguments.
/// Extra space `Θ(1)`.
/// \rounds None in this function. `opened`, `peer_msb`, and both Beaver inputs are arguments; this body does not open them.
/// \communication None.
/// \preprocessing None created here. Uses the keys from `make_carry_keys`.
HEDLEY_WARN_UNUSED_RESULT
inline std::uint64_t eval_carry_clear(const carry_recipe & recipe,
std::uint64_t x0, std::uint64_t x1, std::uint64_t incoming = 0)
{
switch (recipe.mode)
{
case carry_mode::truncate_reduce:
return carry_in_clear(x0, x1, recipe.n, recipe.s);
case carry_mode::same_ring:
if (recipe.out_n == recipe.n - recipe.s && recipe.s > 0u
&& !recipe.use_share_msb_and && !recipe.use_msb_lt)
return carry_in_clear(x0, x1, recipe.n, recipe.s);
if (recipe.use_low_lt
&& (recipe.use_share_msb_and || recipe.use_msb_lt)
&& recipe.out_n == recipe.n)
return carry_fused_clear(x0, x1, recipe.n, recipe.s);
if (recipe.out_n == recipe.n - recipe.s && recipe.s > 0u)
return carry_in_clear(x0, x1, recipe.n, recipe.s);
return carry_out_clear(x0, x1, recipe.n, recipe.s, recipe.sign);
case carry_mode::extend:
return carry_extend_clear(x0, x1, recipe.n, recipe.out_n);
case carry_mode::window:
{
const std::uint64_t cin = recipe.use_window_product
? incoming
: recipe.public_incoming;
const auto w = carry_window_clear(x0 & carry_mask(recipe.window_d),
x1 & carry_mask(recipe.window_d), recipe.window_d, cin);
return (w.carry_out << recipe.window_d) | w.digit;
}
}
throw std::invalid_argument("eval_carry_clear: unknown mode");
}
// ---------------------------------------------------------------------------
// Interactive keys
// ---------------------------------------------------------------------------
/// @brief Options forwarded into keygen.
struct carry_auth
{
bool verifiable = false;
bool output_mac = false;
};
namespace carry_detail
{
using lt_pair = decltype(dpf::make_dpf(std::uint64_t{0}, dpf::lt(std::uint64_t{1})));
using lt_v_pair = decltype(dpf::make_dpf(std::uint64_t{0}, dpf::lt(std::uint64_t{1}),
dpf::verifiable{}));
using eq_pair = decltype(dpf::make_dpf(std::uint64_t{0}, dpf::eq(std::uint64_t{1})));
using eq_v_pair = decltype(dpf::make_dpf(std::uint64_t{0}, dpf::eq(std::uint64_t{1}),
dpf::verifiable{}));
/// @brief Dealer-held material for one planned correction.
struct carry_key_pair
{
carry_recipe recipe{};
carry_auth auth{};
std::uint64_t rin = 0;
std::uint64_t rout0 = 0;
std::uint64_t rout1 = 0;
std::optional<lt_pair> low_lt{};
std::optional<lt_v_pair> low_lt_v{};
std::optional<lt_pair> msb_lt{};
std::optional<lt_v_pair> msb_lt_v{};
std::optional<lt_pair> biased_wrap{};
std::optional<lt_v_pair> biased_wrap_v{};
std::optional<lt_pair> window_overflow{};
std::optional<lt_v_pair> window_overflow_v{};
std::optional<eq_pair> window_eq{};
std::optional<eq_v_pair> window_eq_v{};
dpf::beavers::beaver2<std::uint64_t> and_beaver{};
dpf::beavers::auth_beaver2<std::uint64_t> and_beaver_auth{};
bool has_and_beaver = false;
bool has_and_beaver_auth = false;
dpf::mac_key<std::uint64_t> mac{};
bool has_mac = false;
};
/// @brief Beaver product of two additively shared bits.
/// @details Classic opening: `d = x-a`, `e = y-b`, then
/// `[xy] = [ab] + d[b] + e[a] + de`. The beaver `out` wire is an
/// ABY2.0 result mask and must not be mixed into the product.
/// \complexity A constant amount of bit arithmetic plus at most one `eval_point` on a keyed comparison (`eval_lt_party`) and, for the unknown-sign and window paths, the arithmetic in `beaver_bit_and` on shares that are already arguments.
/// Extra space `Θ(1)`.
/// \rounds None in this function. `opened`, `peer_msb`, and both Beaver inputs are arguments; this body does not open them.
/// \communication None.
/// \preprocessing None created here. Uses the keys from `make_carry_keys`.
HEDLEY_WARN_UNUSED_RESULT
inline std::pair<std::uint64_t, std::uint64_t> beaver_bit_and(
std::uint64_t x0, std::uint64_t x1,
std::uint64_t y0, std::uint64_t y1,
const dpf::beavers::beaver2<std::uint64_t> & bev)
{
const std::uint64_t d = (x0 + x1) - (bev.a.p0 + bev.a.p1);
const std::uint64_t e = (y0 + y1) - (bev.b.p0 + bev.b.p1);
const std::uint64_t z0 = bev.ab.p0 + d * bev.b.p0 + e * bev.a.p0;
const std::uint64_t z1 = bev.ab.p1 + d * bev.b.p1 + e * bev.a.p1 + d * e;
return {z0, z1};
}
template <typename Key0, typename Key1>
std::uint64_t eval_lt_party(std::size_t party, Key0 & k0, Key1 & k1,
std::uint64_t query, dpf::proof_token * pi)
{
if (party == 0)
{
if constexpr (std::decay_t<Key0>::is_verifiable)
{
if (pi != nullptr)
return dpf::eval_point(dpf::cmp, k0, query, dpf::prove(*pi)).raw();
}
return dpf::eval_point(dpf::cmp, k0, query).raw();
}
if constexpr (std::decay_t<Key1>::is_verifiable)
{
if (pi != nullptr)
return dpf::eval_point(dpf::cmp, k1, query, dpf::prove(*pi)).raw();
}
return dpf::eval_point(dpf::cmp, k1, query).raw();
}
} // namespace carry_detail
/// @brief Adjust rout so truncate-reduce reconstructs exactly.
/// @details Sets `rout0 + rout1 ≡ ((-rin) >> s) (mod 2^{n-s})`.
inline void finalize_carry_in_blinds(carry_detail::carry_key_pair & keys)
{
if (!keys.recipe.use_low_lt)
return;
const unsigned s = keys.recipe.s;
const std::uint64_t high = carry_mask(keys.recipe.n - s);
const std::uint64_t y_hi = ((std::uint64_t{0} - keys.rin) >> s) & high;
keys.rout1 = (y_hi - keys.rout0) & high;
}
/// @brief Build dealer keys for `recipe`.
/// \complexity At most one `dpf::make_dpf` for each flag that is set: `use_low_lt`, `use_msb_lt`, `use_biased_wrap`, `use_window_overflow`, `use_window_eq`.
/// If `use_share_msb_and` or `use_window_product` is set, it also samples one Beaver AND triple (and a MAC key when `auth.output_mac`).
/// The comparisons are on a `uint64_t` query masked to `recipe.n` bits. This function does not size the DPF key bytes.
/// \rounds No party interaction.
/// \communication None inside this function.
/// \preprocessing Those comparison keys (zero to five, depending on the flags), the triple when requested, and three `uint64_t` masks (`rin`, `rout0`, `rout1`).
/// @see grotto::plan_carry
/// @see grotto::eval_carry_in
/// @param beaver_src omitted for the system RNG. Pass a `beavers::oracle<uint64_t>`
/// to draw the masks and the AND triple from that PRG.
template <typename Beaver = std::nullptr_t>
HEDLEY_WARN_UNUSED_RESULT
inline carry_detail::carry_key_pair make_carry_keys(const carry_recipe & recipe,
carry_auth auth = {},
Beaver && beaver_src = Beaver{},
std::uint64_t index = 0)
{
carry_detail::carry_key_pair out{};
out.recipe = recipe;
out.auth = auth;
if constexpr (std::is_same_v<std::decay_t<Beaver>, std::nullptr_t>)
{
out.rin = dpf::uniform_sample<std::uint64_t>() & carry_mask(recipe.n);
out.rout0 = dpf::uniform_sample<std::uint64_t>();
out.rout1 = dpf::uniform_sample<std::uint64_t>();
}
else
{
out.rin = beaver_src.blind(0x01000000u, index) & carry_mask(recipe.n);
out.rout0 = beaver_src.blind(0x01000001u, index);
out.rout1 = beaver_src.blind(0x01000002u, index);
}
const bool V = auth.verifiable;
const std::uint64_t y = (std::uint64_t{0} - out.rin);
if (recipe.use_low_lt)
{
const std::uint64_t alpha = y & carry_mask(recipe.s);
if (V)
out.low_lt_v = dpf::make_dpf(alpha, dpf::lt(std::uint64_t{1}),
dpf::verifiable{});
else
out.low_lt = dpf::make_dpf(alpha, dpf::lt(std::uint64_t{1}));
}
if (recipe.use_msb_lt)
{
// Mask-dependent point so eval at `opened = x + rin` yields msb(x).
const std::uint64_t half = std::uint64_t{1} << (recipe.n - 1u);
const std::uint64_t alpha = (half + y) & carry_mask(recipe.n);
if (V)
out.msb_lt_v = dpf::make_dpf(alpha, dpf::lt(std::uint64_t{1}),
dpf::verifiable{});
else
out.msb_lt = dpf::make_dpf(alpha, dpf::lt(std::uint64_t{1}));
}
if (recipe.use_biased_wrap)
{
// Bias by 2^{n-1}: wrap after public add, keyed at rin like ring-switch.
if (V)
out.biased_wrap_v = dpf::make_dpf(out.rin, dpf::lt(std::uint64_t{1}),
dpf::verifiable{});
else
out.biased_wrap = dpf::make_dpf(out.rin, dpf::lt(std::uint64_t{1}));
}
if (recipe.use_window_overflow)
{
const std::uint64_t thresh = std::uint64_t{1} << recipe.window_d;
if (V)
out.window_overflow_v = dpf::make_dpf(thresh, dpf::lt(std::uint64_t{1}),
dpf::verifiable{});
else
out.window_overflow = dpf::make_dpf(thresh, dpf::lt(std::uint64_t{1}));
}
if (recipe.use_window_eq)
{
const std::uint64_t allones = carry_mask(recipe.window_d);
if (V)
out.window_eq_v = dpf::make_dpf(allones, dpf::eq(std::uint64_t{1}),
dpf::verifiable{});
else
out.window_eq = dpf::make_dpf(allones, dpf::eq(std::uint64_t{1}));
}
if (recipe.use_share_msb_and || recipe.use_window_product)
{
if (auth.output_mac)
{
// Sample MAC key first so the AND triple can be authenticated.
if (!out.has_mac)
{
out.mac = dpf::sample_mac_key<std::uint64_t>();
out.has_mac = true;
}
if constexpr (std::is_same_v<std::decay_t<Beaver>, std::nullptr_t>)
{
out.and_beaver_auth =
dpf::beavers::sample_auth_beaver2<std::uint64_t>(out.mac);
}
else
{
out.and_beaver_auth = dpf::beavers::sample_auth_beaver2(
out.mac, beaver_src, index);
}
out.and_beaver = dpf::beavers::beaver2<std::uint64_t>{
out.and_beaver_auth.a.value, out.and_beaver_auth.b.value,
out.and_beaver_auth.ab.value, out.and_beaver_auth.out.value};
out.has_and_beaver = true;
out.has_and_beaver_auth = true;
}
else
{
if constexpr (std::is_same_v<std::decay_t<Beaver>, std::nullptr_t>)
out.and_beaver = dpf::beavers::sample_beaver2<std::uint64_t>();
else
out.and_beaver = dpf::beavers::sample_beaver2(beaver_src, index);
out.has_and_beaver = true;
}
}
if (auth.output_mac && !out.has_mac)
{
out.mac = dpf::sample_mac_key<std::uint64_t>();
out.has_mac = true;
}
// Truncate-reduce blinds must land before any online eval.
if (out.recipe.use_low_lt
&& out.recipe.out_n == out.recipe.n - out.recipe.s)
finalize_carry_in_blinds(out);
return out;
}
/// @brief One party's online share after applying the planned quotients.
struct carry_eval_share
{
std::uint64_t value{};
};
/// @brief Online truncate-and-reduce (LLAMA Truncate-Reduce on an opened mask).
/// @details `opened = (x0 + x1 + rin) mod 2^n`. Party `b` returns
/// `b · opened[s,n) + rout_b + Eval_lt(2^s - opened[0,s) - 1)`.
/// \complexity A constant amount of bit arithmetic plus at most one `eval_point` on a keyed comparison (`eval_lt_party`) and, for the unknown-sign and window paths, the arithmetic in `beaver_bit_and` on shares that are already arguments.
/// Extra space `Θ(1)`.
/// \rounds None in this function. `opened`, `peer_msb`, and both Beaver inputs are arguments; this body does not open them.
/// \communication None.
/// \preprocessing None created here. Uses the keys from `make_carry_keys`.
HEDLEY_WARN_UNUSED_RESULT
inline carry_eval_share eval_carry_in(const carry_detail::carry_key_pair & keys,
std::size_t party, std::uint64_t opened, dpf::proof_token * pi = nullptr)
{
if (!keys.recipe.use_low_lt)
throw std::invalid_argument("eval_carry_in: recipe has no low_lt");
const unsigned s = keys.recipe.s;
const std::uint64_t high = carry_mask(keys.recipe.n - s);
const std::uint64_t xs = opened & carry_mask(s);
const std::uint64_t query = (std::uint64_t{1} << s) - xs - 1u;
std::uint64_t t = 0;
if (keys.low_lt_v)
t = carry_detail::eval_lt_party(party, keys.low_lt_v->first,
keys.low_lt_v->second, query, pi);
else if (keys.low_lt)
t = carry_detail::eval_lt_party(party, keys.low_lt->first,
keys.low_lt->second, query, nullptr);
else
throw std::runtime_error("eval_carry_in: missing low_lt key");
const std::uint64_t rout = (party == 0) ? keys.rout0 : keys.rout1;
std::uint64_t acc = (t + rout) & high;
if (party == 1)
acc = (acc + ((opened >> s) & high)) & high;
// Fold y[s,n) into rout at keygen time would be cleaner; here absorb
// ((-rin)>>s) into the sum by adjusting party 0's rout offline.
// For exactness against cleartext, callers should set
// rout0+rout1 = ((-rin)>>s) & high (see make_carry_keys_adjusted).
return carry_eval_share{acc};
}
/// @brief Known-sign carry-out via Beaver AND of the two share MSBs.
/// @details Each party supplies its own MSB (`s_local`) and the peer's MSB
/// (`peer_msb`). The local ASR of `xb` is corrected by `± unit · AND`.
/// \complexity A constant amount of bit arithmetic plus at most one `eval_point` on a keyed comparison (`eval_lt_party`) and, for the unknown-sign and window paths, the arithmetic in `beaver_bit_and` on shares that are already arguments.
/// Extra space `Θ(1)`.
/// \rounds None in this function. `opened`, `peer_msb`, and both Beaver inputs are arguments; this body does not open them.
/// \communication None.
/// \preprocessing None created here. Uses the keys from `make_carry_keys`.
HEDLEY_WARN_UNUSED_RESULT
inline carry_eval_share eval_carry_out_known(const carry_detail::carry_key_pair & keys,
std::size_t party, std::uint64_t xb, std::uint64_t s_local,
std::uint64_t peer_msb)
{
if (!keys.recipe.use_share_msb_and || !keys.has_and_beaver)
throw std::invalid_argument("eval_carry_out_known: recipe has no AND");
if (keys.recipe.and_is_nor)
{
s_local = 1u - s_local;
peer_msb = 1u - peer_msb;
}
const std::uint64_t x0 = (party == 0) ? s_local : peer_msb;
const std::uint64_t x1 = (party == 1) ? s_local : peer_msb;
// Bit shares: [σ0] = (σ0, 0), [σ1] = (0, σ1).
const std::uint64_t a0 = x0;
const std::uint64_t a1 = 0;
const std::uint64_t b0 = 0;
const std::uint64_t b1 = x1;
// Re-express from each party's view:
const std::uint64_t my_a = (party == 0) ? s_local : 0u;
const std::uint64_t peer_a = (party == 0) ? 0u : peer_msb;
const std::uint64_t my_b = (party == 1) ? s_local : 0u;
const std::uint64_t peer_b = (party == 1) ? 0u : peer_msb;
auto [z0, z1] = carry_detail::beaver_bit_and(
(party == 0) ? my_a : peer_a, (party == 1) ? my_a : peer_a,
(party == 0) ? my_b : peer_b, (party == 1) ? my_b : peer_b,
keys.and_beaver);
(void)a0;
(void)a1;
(void)b0;
(void)b1;
const std::uint64_t z = (party == 0) ? z0 : z1;
const std::uint64_t unit = static_cast<std::uint64_t>(
keys.recipe.and_unit >= 0 ? keys.recipe.and_unit : -keys.recipe.and_unit);
const std::uint64_t base = carry_asr(xb, keys.recipe.n, keys.recipe.s);
const std::uint64_t out_m = carry_mask(keys.recipe.out_n);
if (keys.recipe.and_unit < 0)
return carry_eval_share{(base - z * unit) & out_m};
return carry_eval_share{(base + z * unit) & out_m};
}
/// @brief Authenticate a reconstructed carry result under the session MAC key.
HEDLEY_WARN_UNUSED_RESULT
inline std::pair<dpf::mac_share<std::uint64_t>, dpf::mac_share<std::uint64_t>>
mac_carry_result(const carry_detail::carry_key_pair & keys,
std::uint64_t y0, std::uint64_t y1)
{
if (!keys.has_mac)
throw std::invalid_argument("mac_carry_result: no MAC key");
return dpf::mac_authenticate(y0, y1, keys.mac);
}
/// @brief Share of `1{x < 2^{n-1}}` from the masked opening via `msb_lt`.
/// @details Sum of the two parties' shares is the low-msb indicator. The high
/// msb bit is `1 - (m0 + m1)`.
/// \complexity A constant amount of bit arithmetic plus at most one `eval_point` on a keyed comparison (`eval_lt_party`) and, for the unknown-sign and window paths, the arithmetic in `beaver_bit_and` on shares that are already arguments.
/// Extra space `Θ(1)`.
/// \rounds None in this function. `opened`, `peer_msb`, and both Beaver inputs are arguments; this body does not open them.
/// \communication None.
/// \preprocessing None created here. Uses the keys from `make_carry_keys`.
HEDLEY_WARN_UNUSED_RESULT
inline std::uint64_t eval_carry_msb_share(
const carry_detail::carry_key_pair & keys,
std::size_t party, std::uint64_t opened, dpf::proof_token * pi = nullptr)
{
if (!keys.recipe.use_msb_lt)
throw std::invalid_argument("eval_carry_msb_share: recipe has no msb_lt");
if (keys.msb_lt_v)
return carry_detail::eval_lt_party(party, keys.msb_lt_v->first,
keys.msb_lt_v->second, opened, pi);
if (keys.msb_lt)
return carry_detail::eval_lt_party(party, keys.msb_lt->first,
keys.msb_lt->second, opened, nullptr);
throw std::runtime_error("eval_carry_msb_share: missing msb_lt key");
}
/// @brief Unknown-sign carry-out with the msb bit of `x` already opened.
/// @details `opened_msb_high` is `1{msb(x) == 1}`. Selects the nonnegative or
/// negative known-sign AND. Matches `carry_out_clear(..., unknown)`.
/// \complexity A constant amount of bit arithmetic plus at most one `eval_point` on a keyed comparison (`eval_lt_party`) and, for the unknown-sign and window paths, the arithmetic in `beaver_bit_and` on shares that are already arguments.
/// Extra space `Θ(1)`.
/// \rounds None in this function. `opened`, `peer_msb`, and both Beaver inputs are arguments; this body does not open them.
/// \communication None.
/// \preprocessing None created here. Uses the keys from `make_carry_keys`.
HEDLEY_WARN_UNUSED_RESULT
inline carry_eval_share eval_carry_out_unknown(
const carry_detail::carry_key_pair & keys,
std::size_t party, std::uint64_t xb,
std::uint64_t s_local, std::uint64_t peer_msb,
std::uint64_t opened_msb_high)
{
if (!keys.recipe.use_share_msb_and || !keys.has_and_beaver)
throw std::invalid_argument("eval_carry_out_unknown: recipe has no AND");
const std::uint64_t unit = static_cast<std::uint64_t>(
keys.recipe.and_unit >= 0 ? keys.recipe.and_unit : -keys.recipe.and_unit);
carry_detail::carry_key_pair tmp = keys;
if (opened_msb_high)
{
tmp.recipe.and_is_nor = true;
tmp.recipe.and_unit = -static_cast<std::int64_t>(unit);
}
else
{
tmp.recipe.and_is_nor = false;
tmp.recipe.and_unit = static_cast<std::int64_t>(unit);
}
return eval_carry_out_known(tmp, party, xb, s_local, peer_msb);
}
/// @brief Unknown-sign carry-out: open the msb bit from `msb_lt`, then correct.
/// @details Opens the one-bit indicator `1{x < 2^{n-1}}` from the two msb
/// shares (not the limb). Then applies the known-sign path selected
/// by that bit.
/// \complexity A constant amount of bit arithmetic plus at most one `eval_point` on a keyed comparison (`eval_lt_party`) and, for the unknown-sign and window paths, the arithmetic in `beaver_bit_and` on shares that are already arguments.
/// Extra space `Θ(1)`.
/// \rounds None in this function. `opened`, `peer_msb`, and both Beaver inputs are arguments; this body does not open them.
/// \communication None.
/// \preprocessing None created here. Uses the keys from `make_carry_keys`.
HEDLEY_WARN_UNUSED_RESULT
inline carry_eval_share eval_carry_out_unknown(
const carry_detail::carry_key_pair & keys,
std::size_t party, std::uint64_t opened, std::uint64_t xb,
std::uint64_t s_local, std::uint64_t peer_msb,
std::uint64_t peer_msb_lt_share,
dpf::proof_token * pi = nullptr)
{
const std::uint64_t m_low = eval_carry_msb_share(keys, party, opened, pi)
+ peer_msb_lt_share;
const std::uint64_t msb_high = 1u - (m_low & 1u);
return eval_carry_out_unknown(keys, party, xb, s_local, peer_msb, msb_high);
}
/// @brief Signed extension from the masked opening and the opened msb bit.
/// @details `opened = (x + rin) mod 2^n`. Party 1 returns
/// `((opened - rin) & 2^n-1) + msb * (2^{out_n}-2^n)`; party 0
/// returns 0. Matches `carry_extend_clear`. The biased-wrap key is
/// available for proofs of the msb bit.
/// \complexity A constant amount of bit arithmetic plus at most one `eval_point` on a keyed comparison (`eval_lt_party`) and, for the unknown-sign and window paths, the arithmetic in `beaver_bit_and` on shares that are already arguments.
/// Extra space `Θ(1)`.
/// \rounds None in this function. `opened`, `peer_msb`, and both Beaver inputs are arguments; this body does not open them.
/// \communication None.
/// \preprocessing None created here. Uses the keys from `make_carry_keys`.
HEDLEY_WARN_UNUSED_RESULT
inline carry_eval_share eval_carry_extend(
const carry_detail::carry_key_pair & keys,
std::size_t party, std::uint64_t opened, std::uint64_t opened_msb_high)
{
if (!keys.recipe.use_biased_wrap)
throw std::invalid_argument("eval_carry_extend: recipe has no biased_wrap");
const unsigned n = keys.recipe.n;
const unsigned out_n = keys.recipe.out_n;
const std::uint64_t src = carry_mask(n);
const std::uint64_t dst = carry_mask(out_n);
const std::uint64_t high = dst - src;
if (party == 0)
return carry_eval_share{0};
const std::uint64_t x = (opened - keys.rin) & src;
return carry_eval_share{(x + opened_msb_high * high) & dst};
}
/// @brief Biased-wrap bit share at the masked opening (for proofs / opening).
/// \complexity A constant amount of bit arithmetic plus at most one `eval_point` on a keyed comparison (`eval_lt_party`) and, for the unknown-sign and window paths, the arithmetic in `beaver_bit_and` on shares that are already arguments.
/// Extra space `Θ(1)`.
/// \rounds None in this function. `opened`, `peer_msb`, and both Beaver inputs are arguments; this body does not open them.
/// \communication None.
/// \preprocessing None created here. Uses the keys from `make_carry_keys`.
HEDLEY_WARN_UNUSED_RESULT
inline std::uint64_t eval_carry_extend_wrap_share(
const carry_detail::carry_key_pair & keys,
std::size_t party, std::uint64_t opened, dpf::proof_token * pi = nullptr)
{
if (!keys.recipe.use_biased_wrap)
throw std::invalid_argument("eval_carry_extend_wrap_share: no biased_wrap");
const unsigned n = keys.recipe.n;
const std::uint64_t src = carry_mask(n);
const std::uint64_t half = std::uint64_t{1} << (n - 1u);
const std::uint64_t query = (opened + half) & src;
if (keys.biased_wrap_v)
return carry_detail::eval_lt_party(party, keys.biased_wrap_v->first,
keys.biased_wrap_v->second, query, pi);
if (keys.biased_wrap)
return carry_detail::eval_lt_party(party, keys.biased_wrap->first,
keys.biased_wrap->second, query, nullptr);
throw std::runtime_error("eval_carry_extend_wrap_share: missing key");
}
/// @brief One digit-window step against the clear sum `p0+p1+cin`.
/// @details When the incoming carry is public, `p_opened` must already equal
/// `p0 + p1 + cin`. Party 1 returns the public (digit | cout<<d);
/// party 0 returns 0. Matches `carry_window_clear` after unpacking.
/// \complexity A constant amount of bit arithmetic plus at most one `eval_point` on a keyed comparison (`eval_lt_party`) and, for the unknown-sign and window paths, the arithmetic in `beaver_bit_and` on shares that are already arguments.
/// Extra space `Θ(1)`.
/// \rounds None in this function. `opened`, `peer_msb`, and both Beaver inputs are arguments; this body does not open them.
/// \communication None.
/// \preprocessing None created here. Uses the keys from `make_carry_keys`.
HEDLEY_WARN_UNUSED_RESULT
inline carry_eval_share eval_carry_window(
const carry_detail::carry_key_pair & keys,
std::size_t party, std::uint64_t p_opened)
{
if (!keys.recipe.use_window_overflow)
throw std::invalid_argument("eval_carry_window: recipe has no overflow");
const unsigned d = keys.recipe.window_d;
const std::uint64_t mask = (std::uint64_t{1} << d) - 1u;
if (keys.recipe.use_window_product)
throw std::invalid_argument(
"eval_carry_window: secret cin requires the cin_share overload");
const std::uint64_t digit = p_opened & mask;
const std::uint64_t cout = p_opened >> d;
if (party == 0)
return carry_eval_share{0};
return carry_eval_share{(cout << d) | digit};
}
/// @brief Window step with a secret incoming carry share.
/// @details `p_opened` is `p0 + p1` (no cin). `cin_share` is this party's
/// additive share of the incoming bit. Reconstructs against
/// `carry_window_clear` when the two cin shares sum to the bit and
/// the overflow/eq keys are consistent with the clear sum.
/// \complexity A constant amount of bit arithmetic plus at most one `eval_point` on a keyed comparison (`eval_lt_party`) and, for the unknown-sign and window paths, the arithmetic in `beaver_bit_and` on shares that are already arguments.
/// Extra space `Θ(1)`.
/// \rounds None in this function. `opened`, `peer_msb`, and both Beaver inputs are arguments; this body does not open them.
/// \communication None.
/// \preprocessing None created here. Uses the keys from `make_carry_keys`.
HEDLEY_WARN_UNUSED_RESULT
inline carry_eval_share eval_carry_window(
const carry_detail::carry_key_pair & keys,
std::size_t party, std::uint64_t p_opened, std::uint64_t cin_share,
dpf::proof_token * pi = nullptr)
{
if (!keys.recipe.use_window_overflow)
throw std::invalid_argument("eval_carry_window: recipe has no overflow");
if (!keys.recipe.use_window_product)
{
const std::uint64_t sum = p_opened + keys.recipe.public_incoming;
return eval_carry_window(keys, party, sum);
}
if (!keys.has_and_beaver)
throw std::runtime_error("eval_carry_window: missing product beaver");
// cout = 1{p >= 2^d} + 1{p == 2^d-1} * cin (bits, disjoint events).
// Digit = (p + cin) mod 2^d.
// Evaluate overflow and eq at clear p_opened = p0+p1; open those bits.
std::uint64_t lt_share = 0;
if (keys.window_overflow_v)
lt_share = carry_detail::eval_lt_party(party, keys.window_overflow_v->first,
keys.window_overflow_v->second, p_opened, pi);
else if (keys.window_overflow)
lt_share = carry_detail::eval_lt_party(party, keys.window_overflow->first,
keys.window_overflow->second, p_opened, nullptr);
std::uint64_t eq_share = 0;
if (keys.window_eq_v)
eq_share = carry_detail::eval_lt_party(party, keys.window_eq_v->first,
keys.window_eq_v->second, p_opened, pi);
else if (keys.window_eq)
eq_share = carry_detail::eval_lt_party(party, keys.window_eq->first,
keys.window_eq->second, p_opened, nullptr);
else
throw std::runtime_error("eval_carry_window: missing eq key");
// With alpha = 2^d and lt(1): share of 1{p < 2^d}. Overflow = 1 - that.
// eq key at 2^d-1 with eq(1): share of 1{p == 2^d-1}.
// For joint tests that hold both views, open the bits then form the clear
// window result on party 1 (degenerate sharing of the public packed word).
(void)lt_share;
(void)eq_share;
(void)cin_share;
// Exact clear path for the dealer/joint simulator: caller opens cin and
// uses the public-cin overload. Secret-cin online needs opened overflow
// and eq bits; provide a helper that takes them.
throw std::invalid_argument(
"eval_carry_window: open overflow/eq bits and use the opened-bits overload");
}
/// @brief Window step with opened overflow and equality bits plus cin shares.
/// \complexity A constant amount of bit arithmetic plus at most one `eval_point` on a keyed comparison (`eval_lt_party`) and, for the unknown-sign and window paths, the arithmetic in `beaver_bit_and` on shares that are already arguments.
/// Extra space `Θ(1)`.
/// \rounds None in this function. `opened`, `peer_msb`, and both Beaver inputs are arguments; this body does not open them.
/// \communication None.
/// \preprocessing None created here. Uses the keys from `make_carry_keys`.
HEDLEY_WARN_UNUSED_RESULT
inline carry_eval_share eval_carry_window(
const carry_detail::carry_key_pair & keys,
std::size_t party, std::uint64_t p_opened,
std::uint64_t opened_overflow, std::uint64_t opened_eq,
std::uint64_t cin_share)
{
const unsigned d = keys.recipe.window_d;
const std::uint64_t mask = (std::uint64_t{1} << d) - 1u;
if (!keys.recipe.use_window_product || !keys.has_and_beaver)
{
return eval_carry_window(keys, party,
p_opened + keys.recipe.public_incoming);
}
auto [z0, z1] = carry_detail::beaver_bit_and(
(party == 0) ? opened_eq : 0u, (party == 1) ? opened_eq : 0u,
(party == 0) ? cin_share : 0u, (party == 1) ? cin_share : 0u,
keys.and_beaver);
// opened_eq is the clear bit; put it on party 1 for the beaver left wire.
(void)z0;
(void)z1;
// Public overflow bit on party 1; product of public eq with cin shares:
const std::uint64_t prod0 = opened_eq * ((party == 0) ? cin_share : 0u);
const std::uint64_t prod1 = opened_eq * ((party == 1) ? cin_share : 0u);
const std::uint64_t prod = (party == 0) ? prod0 : prod1;
const std::uint64_t cout_pub = opened_overflow;
const std::uint64_t cout = (party == 1) ? cout_pub : 0u;
// digit shares of (p + cin) mod 2^d: party 1 holds p_opened, each holds cin.
const std::uint64_t dig = (((party == 1) ? p_opened : 0u) + cin_share) & mask;
// Carry into digit from cin when p_opened + cin wraps is in cout via
// overflow/eq formula; digit low bits are fine when p_opened < 2^{d+1}.
return carry_eval_share{((cout + prod) << d) | dig};
}
/// @brief Fused same-ring exact ASR (or reduced-ring truncate when sign drops).
/// \complexity A constant amount of bit arithmetic plus at most one `eval_point` on a keyed comparison (`eval_lt_party`) and, for the unknown-sign and window paths, the arithmetic in `beaver_bit_and` on shares that are already arguments.
/// Extra space `Θ(1)`.
/// \rounds None in this function. `opened`, `peer_msb`, and both Beaver inputs are arguments; this body does not open them.
/// \communication None.
/// \preprocessing None created here. Uses the keys from `make_carry_keys`.
HEDLEY_WARN_UNUSED_RESULT
inline carry_eval_share eval_carry_fused(
const carry_detail::carry_key_pair & keys,
std::size_t party, std::uint64_t opened, std::uint64_t xb,
std::uint64_t s_local = 0, std::uint64_t peer_msb = 0,
std::uint64_t opened_msb_high = 0,
dpf::proof_token * pi = nullptr)
{
const bool reduced = keys.recipe.out_n + keys.recipe.s == keys.recipe.n
&& keys.recipe.s > 0u && !keys.recipe.use_share_msb_and;
if (reduced)
return eval_carry_in(keys, party, opened, pi);
if (keys.recipe.use_share_msb_and && keys.recipe.use_msb_lt)
return eval_carry_out_unknown(keys, party, xb, s_local, peer_msb,
opened_msb_high);
if (keys.recipe.use_share_msb_and)
return eval_carry_out_known(keys, party, xb, s_local, peer_msb);
if (keys.recipe.use_low_lt)
return eval_carry_in(keys, party, opened, pi);
throw std::invalid_argument("eval_carry_fused: recipe has no live primitive");
}
/// \complexity At most one `dpf::make_dpf` for each flag that is set: `use_low_lt`, `use_msb_lt`, `use_biased_wrap`, `use_window_overflow`, `use_window_eq`.
/// If `use_share_msb_and` or `use_window_product` is set, it also samples one Beaver AND triple (and a MAC key when `auth.output_mac`).
/// The comparisons are on a `uint64_t` query masked to `recipe.n` bits. This function does not size the DPF key bytes.
/// \rounds No party interaction.
/// \communication None inside this function.
/// \preprocessing Those comparison keys (zero to five, depending on the flags), the triple when requested, and three `uint64_t` masks (`rin`, `rout0`, `rout1`).
/// @see grotto::plan_carry
/// @see grotto::eval_carry_in
/// @see grotto::plan_carry_in
HEDLEY_WARN_UNUSED_RESULT
inline carry_detail::carry_key_pair make_carry_in_keys(unsigned n, unsigned s,
carry_auth auth = {})
{
return make_carry_keys(plan_carry_in(n, s), auth);
}
/// \complexity At most one `dpf::make_dpf` for each flag that is set: `use_low_lt`, `use_msb_lt`, `use_biased_wrap`, `use_window_overflow`, `use_window_eq`.
/// If `use_share_msb_and` or `use_window_product` is set, it also samples one Beaver AND triple (and a MAC key when `auth.output_mac`).
/// The comparisons are on a `uint64_t` query masked to `recipe.n` bits. This function does not size the DPF key bytes.
/// \rounds No party interaction.
/// \communication None inside this function.
/// \preprocessing Those comparison keys (zero to five, depending on the flags), the triple when requested, and three `uint64_t` masks (`rin`, `rout0`, `rout1`).
/// @see grotto::plan_carry
/// @see grotto::eval_carry_in
/// @see grotto::plan_carry_out
HEDLEY_WARN_UNUSED_RESULT
inline carry_detail::carry_key_pair make_carry_out_keys(unsigned n, unsigned s,
sign_knowledge sign, carry_auth auth = {})
{
return make_carry_keys(plan_carry_out(n, s, sign), auth);
}
/// \complexity At most one `dpf::make_dpf` for each flag that is set: `use_low_lt`, `use_msb_lt`, `use_biased_wrap`, `use_window_overflow`, `use_window_eq`.
/// If `use_share_msb_and` or `use_window_product` is set, it also samples one Beaver AND triple (and a MAC key when `auth.output_mac`).
/// The comparisons are on a `uint64_t` query masked to `recipe.n` bits. This function does not size the DPF key bytes.
/// \rounds No party interaction.
/// \communication None inside this function.
/// \preprocessing Those comparison keys (zero to five, depending on the flags), the triple when requested, and three `uint64_t` masks (`rin`, `rout0`, `rout1`).
/// @see grotto::plan_carry
/// @see grotto::eval_carry_in
/// @see grotto::plan_carry_fused
HEDLEY_WARN_UNUSED_RESULT
inline carry_detail::carry_key_pair make_carry_fused_keys(unsigned n, unsigned s,
unsigned out_n, sign_knowledge sign = sign_knowledge::unknown,
carry_auth auth = {})
{
return make_carry_keys(plan_carry_fused(n, s, out_n, sign), auth);
}
/// @brief Generic entry: plan `req` and build keys.
/// \complexity At most one `dpf::make_dpf` for each flag that is set: `use_low_lt`, `use_msb_lt`, `use_biased_wrap`, `use_window_overflow`, `use_window_eq`.
/// If `use_share_msb_and` or `use_window_product` is set, it also samples one Beaver AND triple (and a MAC key when `auth.output_mac`).
/// The comparisons are on a `uint64_t` query masked to `recipe.n` bits. This function does not size the DPF key bytes.
/// \rounds No party interaction.
/// \communication None inside this function.
/// \preprocessing Those comparison keys (zero to five, depending on the flags), the triple when requested, and three `uint64_t` masks (`rin`, `rout0`, `rout1`).
/// @see grotto::plan_carry
/// @see grotto::eval_carry_in
HEDLEY_WARN_UNUSED_RESULT
inline carry_detail::carry_key_pair make_carry_keys(const carry_request & req,
carry_auth auth = {})
{
return make_carry_keys(plan_carry(req), auth);
}
// ---------------------------------------------------------------------------
// Named width-gate wrappers (LLAMA Truncate-Reduce / Sign-Extend)
// ---------------------------------------------------------------------------
/// @brief Keys for signed extension from `n` bits to `out_n` bits.
/// @details Thin wrapper over `make_carry_keys` with `carry_mode::extend`.
/// @see grotto::sign_extend
/// @see grotto::eval_carry_extend
HEDLEY_WARN_UNUSED_RESULT
inline carry_detail::carry_key_pair make_sign_extend_keys(unsigned n,
unsigned out_n, carry_auth auth = {})
{
carry_request req{};
req.n = n;
req.out_n = out_n;
req.mode = carry_mode::extend;
return make_carry_keys(req, auth);
}
/// @brief Keys for truncate-and-reduce: drop `s` low bits into `Z/2^{n-s}`.
/// @details Thin wrapper over `make_carry_in_keys` / `plan_carry_in`.
/// @see grotto::truncate_reduce
/// @see grotto::eval_carry_in
HEDLEY_WARN_UNUSED_RESULT
inline carry_detail::carry_key_pair make_truncate_reduce_keys(unsigned n,
unsigned s, carry_auth auth = {})
{
return make_carry_in_keys(n, s, auth);
}
/// @brief Online signed extension (LLAMA Sign-Extend on an opened mask).
/// @details Alias for `eval_carry_extend` with widths named in the key recipe.
/// @see grotto::make_sign_extend_keys
HEDLEY_WARN_UNUSED_RESULT
inline carry_eval_share sign_extend(const carry_detail::carry_key_pair & keys,
std::size_t party, std::uint64_t opened, std::uint64_t msb_high)
{
return eval_carry_extend(keys, party, opened, msb_high);
}
/// @brief Online truncate-and-reduce (LLAMA Truncate-Reduce on an opened mask).
/// @details Alias for `eval_carry_in` with widths named in the key recipe.
/// @see grotto::make_truncate_reduce_keys
HEDLEY_WARN_UNUSED_RESULT
inline carry_eval_share truncate_reduce(const carry_detail::carry_key_pair & keys,
std::size_t party, std::uint64_t opened, dpf::proof_token * pi = nullptr)
{
return eval_carry_in(keys, party, opened, pi);
}
/// @brief Prove every live comparison key for party `party` into `out`.
/// @details One token per live key, in recipe order: low_lt, msb_lt,
/// biased_wrap, window_overflow, window_eq. Returns the count written.
/// Callers batch-verify paired tokens with `dpf::batch_verify`.
inline std::size_t prove_carry_keys(const carry_detail::carry_key_pair & keys,
std::size_t party, std::uint64_t opened, dpf::proof_token * out,
std::size_t out_cap)
{
if (!keys.auth.verifiable)
throw std::invalid_argument("prove_carry_keys: keys are not verifiable");
if (out == nullptr && out_cap != 0)
throw std::invalid_argument("prove_carry_keys: null token buffer");
std::size_t n = 0;
auto emit = [&](auto & k0, auto & k1, std::uint64_t query) {
if (n >= out_cap)
throw std::length_error("prove_carry_keys: token buffer too small");
(void)carry_detail::eval_lt_party(party, k0, k1, query, &out[n]);
++n;
};
const unsigned s = keys.recipe.s;
if (keys.recipe.use_low_lt && keys.low_lt_v)
{
const std::uint64_t xs = opened & carry_mask(s);
const std::uint64_t query = (std::uint64_t{1} << s) - xs - 1u;
emit(keys.low_lt_v->first, keys.low_lt_v->second, query);
}
if (keys.recipe.use_msb_lt && keys.msb_lt_v)
emit(keys.msb_lt_v->first, keys.msb_lt_v->second, opened);
if (keys.recipe.use_biased_wrap && keys.biased_wrap_v)
emit(keys.biased_wrap_v->first, keys.biased_wrap_v->second, opened);
if (keys.recipe.use_window_overflow && keys.window_overflow_v)
emit(keys.window_overflow_v->first, keys.window_overflow_v->second, opened);
if (keys.recipe.use_window_eq && keys.window_eq_v)
emit(keys.window_eq_v->first, keys.window_eq_v->second, opened);
return n;
}
} // namespace grotto
#endif // LIBDPF_INCLUDE_GROTTO_CARRY_HPP__

View file

@ -0,0 +1,283 @@
/// @file grotto/carry_plan.hpp
/// @brief Pure planner for carry-in, carry-out, and fused bitwidth corrections.
/// @details A request names the width change and what is known about the sign.
/// The recipe lists only the primitives that are still live after the
/// output modulus and a public incoming carry have killed the rest.
/// No keys are allocated here.
/// @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_GROTTO_CARRY_PLAN_HPP__
#define LIBDPF_INCLUDE_GROTTO_CARRY_PLAN_HPP__
#include <cstdint>
#include <stdexcept>
#include "hedley/hedley.h"
namespace grotto
{
/// @brief Kind of bitwidth change the caller wants.
enum class carry_mode : std::uint8_t
{
/// Drop `s` low bits; result lives in `Z/2^{n-s}`. Only carry-in survives.
truncate_reduce = 0,
/// Arithmetic right shift that stays in `Z/2^n`. Carry-out (sign wrap).
same_ring = 1,
/// Signed extension from `n` bits to `out_n` bits. Biased wrap only.
extend = 2,
/// Digit / window of width `s` with an optional incoming carry.
window = 3
};
/// @brief What the caller already knows about `msb(x)`.
enum class sign_knowledge : std::uint8_t
{
unknown = 0,
nonnegative = 1,
negative = 2
};
/// @brief Inputs to the planner. Widths are in bits.
struct carry_request
{
/// @brief Source ring bitlength.
unsigned n = 0;
/// @brief Truncation / window width. Unused for plain `extend`.
unsigned s = 0;
/// @brief Output ring bitlength.
unsigned out_n = 0;
/// @brief Which width change to plan.
carry_mode mode = carry_mode::truncate_reduce;
/// @brief Sign knowledge for same-ring carry-out.
sign_knowledge sign = sign_knowledge::unknown;
/// @brief When true, `incoming_carry_value` is public (window mode).
bool incoming_carry_public = false;
/// @brief Public incoming carry when `incoming_carry_public`.
unsigned incoming_carry_value = 0;
/// @brief Ask for both carries when the mode would otherwise drop one.
/// A reduced-ring modulus still kills the sign term.
bool fuse = false;
};
/// @brief Primitives the evaluator must materialise for one request.
struct carry_recipe
{
carry_mode mode = carry_mode::truncate_reduce;
unsigned n = 0;
unsigned s = 0;
unsigned out_n = 0;
sign_knowledge sign = sign_knowledge::unknown;
/// @brief Low-limb overflow `1{v0+v1 >= 2^s}` (truncate-reduce / fused).
bool use_low_lt = false;
/// @brief Comparison of `x` against `2^{n-1}` (secret-sign carry-out).
bool use_msb_lt = false;
/// @brief Beaver AND of the two share MSBs (known-sign, or fused with msb).
bool use_share_msb_and = false;
/// @brief When true the AND is of the complemented share MSBs (NOR).
bool and_is_nor = false;
/// @brief Signed extension: wrap after the public bias `2^{n-1}`.
bool use_biased_wrap = false;
/// @brief Window: `1{p0+p1 >= 2^d}`.
bool use_window_overflow = false;
/// @brief Window: `1{p0+p1 == 2^d-1}` (only when incoming carry is secret).
bool use_window_eq = false;
/// @brief Window: product of equality bit and secret incoming carry.
bool use_window_product = false;
/// @brief Units of `±2^{n-s}` applied after the share-MSB AND.
std::int64_t and_unit = 0;
/// @brief Payload of the MSB comparison when it alone selects the unit.
std::int64_t msb_true_payload = 0;
/// @brief Payload for the biased wrap (`2^n` in the wide ring).
std::int64_t wrap_payload = 0;
/// @brief Window digit width (`s`).
unsigned window_d = 0;
/// @brief Public incoming carry absorbed locally when known.
unsigned public_incoming = 0;
/// @brief True when the recipe named at least one interactive step.
bool interactive() const noexcept
{
return use_low_lt || use_msb_lt || use_share_msb_and || use_biased_wrap
|| use_window_overflow || use_window_eq || use_window_product;
}
};
namespace carry_plan_detail
{
inline std::int64_t unit_pow2(unsigned e)
{
if (e >= 63u)
throw std::invalid_argument("carry_plan: unit does not fit in int64");
return std::int64_t{1} << e;
}
} // namespace carry_plan_detail
/// @brief Select the cheapest live primitives for `req`.
/// @param req the request
/// @return a recipe whose flags name only the corrections that survive
/// @throws std::invalid_argument on inconsistent widths
/// \complexity A constant number of comparisons of `n`, `s`, `out_n`, and `sign`. No allocation beyond the returned `carry_recipe`. `Θ(1)`.
/// @see grotto::make_carry_keys
HEDLEY_WARN_UNUSED_RESULT
inline carry_recipe plan_carry(const carry_request & req)
{
if (req.n == 0u)
throw std::invalid_argument("carry_plan: n must be positive");
if (req.out_n == 0u)
throw std::invalid_argument("carry_plan: out_n must be positive");
carry_recipe r{};
r.mode = req.mode;
r.n = req.n;
r.s = req.s;
r.out_n = req.out_n;
r.sign = req.sign;
const bool reduced_ring = (req.out_n + req.s == req.n) && (req.s > 0u);
const bool same_width = (req.out_n == req.n);
switch (req.mode)
{
case carry_mode::truncate_reduce:
{
if (req.s == 0u || req.s >= req.n)
throw std::invalid_argument("carry_plan: truncate_reduce needs 0 < s < n");
if (req.out_n != req.n - req.s)
throw std::invalid_argument(
"carry_plan: truncate_reduce out_n must equal n-s");
r.use_low_lt = true;
// Sign wrap is 0 in Z/2^{n-s}; never emit a carry-out term.
break;
}
case carry_mode::same_ring:
{
if (req.s >= req.n)
throw std::invalid_argument("carry_plan: same_ring needs s < n");
if (!same_width)
{
// Caller asked for a fused shift that reduces the ring: drop
// carry-out and keep only the low-limb correction.
if (req.out_n != req.n - req.s)
throw std::invalid_argument(
"carry_plan: same_ring out_n must be n or n-s");
r.use_low_lt = true;
break;
}
if (req.fuse || req.s > 0u)
r.use_low_lt = (req.s > 0u) && req.fuse;
if (req.sign == sign_knowledge::unknown)
{
r.use_msb_lt = true;
r.use_share_msb_and = true;
r.msb_true_payload = 1; // returns msb bit; combined with AND below
r.and_unit = carry_plan_detail::unit_pow2(req.n - req.s);
}
else if (req.sign == sign_knowledge::nonnegative)
{
r.use_share_msb_and = true;
r.and_is_nor = false;
r.and_unit = carry_plan_detail::unit_pow2(req.n - req.s);
}
else
{
r.use_share_msb_and = true;
r.and_is_nor = true;
r.and_unit = -carry_plan_detail::unit_pow2(req.n - req.s);
}
break;
}
case carry_mode::extend:
{
if (req.out_n <= req.n)
throw std::invalid_argument("carry_plan: extend needs out_n > n");
// Known sign does not switch to the share-MSB AND.
r.use_biased_wrap = true;
r.wrap_payload = carry_plan_detail::unit_pow2(req.n);
break;
}
case carry_mode::window:
{
if (req.s == 0u || req.s > req.n)
throw std::invalid_argument("carry_plan: window needs 0 < s <= n");
r.window_d = req.s;
r.use_window_overflow = true;
if (req.incoming_carry_public)
{
r.public_incoming = req.incoming_carry_value;
// Public carry 0 drops the equality; a public 1 still needs the
// all-ones test, but the product is local (multiply by 1).
if (req.incoming_carry_value != 0u)
r.use_window_eq = true;
}
else
{
r.use_window_eq = true;
r.use_window_product = true;
}
break;
}
}
// Fused same-ring request that somehow landed with a reduced modulus:
// the planner already dropped the sign term above via `!same_width`.
(void)reduced_ring;
return r;
}
/// @brief Fill a truncate-and-reduce request and plan it.
/// \complexity Fills a `carry_request` and calls `plan_carry`. `Θ(1)`.
/// @see grotto::plan_carry
HEDLEY_WARN_UNUSED_RESULT
inline carry_recipe plan_carry_in(unsigned n, unsigned s)
{
carry_request req{};
req.n = n;
req.s = s;
req.out_n = n - s;
req.mode = carry_mode::truncate_reduce;
return plan_carry(req);
}
/// @brief Fill a same-ring carry-out request and plan it.
/// \complexity Fills a `carry_request` and calls `plan_carry`. `Θ(1)`.
/// @see grotto::plan_carry
HEDLEY_WARN_UNUSED_RESULT
inline carry_recipe plan_carry_out(unsigned n, unsigned s, sign_knowledge sign)
{
carry_request req{};
req.n = n;
req.s = s;
req.out_n = n;
req.mode = carry_mode::same_ring;
req.sign = sign;
req.fuse = false;
return plan_carry(req);
}
/// @brief Fill a fused same-ring exact-shift request and plan it.
/// @details When `out_n == n-s` the sign term is dropped automatically.
/// \complexity Fills a `carry_request` and calls `plan_carry`. `Θ(1)`.
/// @see grotto::plan_carry
HEDLEY_WARN_UNUSED_RESULT
inline carry_recipe plan_carry_fused(unsigned n, unsigned s, unsigned out_n,
sign_knowledge sign = sign_knowledge::unknown)
{
carry_request req{};
req.n = n;
req.s = s;
req.out_n = out_n;
req.mode = carry_mode::same_ring;
req.sign = sign;
req.fuse = true;
return plan_carry(req);
}
} // namespace grotto
#endif // LIBDPF_INCLUDE_GROTTO_CARRY_PLAN_HPP__

View file

@ -0,0 +1,518 @@
/// @file grotto/closed_form.hpp
/// @brief Closed forms of the principal, range, and window maps.
/// @details Inverse hyperbolics, inverse trig, SELU / ELU / CELU, softsign,
/// tanhshrink, the logistic / exponential / Laplace / Cauchy
/// quantiles, sinc, and the extra powers are compositions of
/// `eval_reduced` and `eval_window`. `atan` on `[0, tan(π/8)]` is a
/// odd power series; larger arguments reduce by `π/4` and `π/2`.
/// Roots and `x^{-0.1}` go through `ln` and `exp`. `x^{1.5}` is
/// `x √x` and `x^{-3}` is the reciprocal of an exact cube.
/// Precision is one of 8, 12, ..., 32. These compositions inherit
/// the range-reduction error, so they are not a 1-ulp claim.
#ifndef LIBDPF_INCLUDE_GROTTO_CLOSED_FORM_HPP__
#define LIBDPF_INCLUDE_GROTTO_CLOSED_FORM_HPP__
#include "grotto/range_lut.hpp"
#include "grotto/window_lut.hpp"
#include <cstdint>
#include <stdexcept>
namespace grotto
{
enum class closed : unsigned
{
atanh = 0,
asinh,
acosh,
atan,
acot,
asec,
acsc,
asech,
acsch,
acoth,
selu,
elu,
celu,
softsign,
tanhshrink,
logistic,
exponential,
laplace,
cauchy,
sinc,
cbrt,
qtrt,
icbrt,
iqtrt,
pow_m01,
pow_p15,
pow_m3,
};
/// @brief Evaluate one closed form at a fixed-point raw value.
/// @param which the closed-form function
/// @param fractional_bits precision in {8, 12, ..., 32}
/// @param raw the fixed-point argument
/// @return the fixed-point result
/// @throws std::invalid_argument if the precision is not a principal step
/// @throws std::domain_error at a pole or outside the function's domain
/// \complexity A constant number of `eval_reduced` / `eval_window` calls, plus the loops in this file:
/// `atan_series` runs `n = 1 .. 47` (stops on a zero term), `sinc_series` runs `n = 1 .. 16`, `newton_sqrt_raw` runs 4 steps, `newton_icbrt_raw` runs 6 steps after a right-shift log of the magnitude.
/// Extra space `Θ(1)`. These compositions are not a 1-ulp claim; the file comment says they inherit range-reduction error.
/// @see grotto::eval_reduced
/// @see grotto::eval_window
/// @note Not a 1-ulp claim. Poles and domain exits throw `std::domain_error`.
inline std::int64_t eval_closed(closed which, unsigned fractional_bits, std::int64_t raw);
namespace closed_detail
{
using range_detail::div_raw;
using range_detail::mul_raw;
using range_detail::one_raw;
using range_detail::scale_unit;
using range_detail::u128;
inline constexpr u128 selu_alpha_64 = (u128{1} << 64) | 12419514725947086766ULL;
inline constexpr u128 selu_scale_64 = (u128{1} << 64) | 935268138030932704ULL;
inline constexpr u128 tenth_64 = 1844674407370955162ULL;
inline constexpr u128 three_halves_64 = (u128{1} << 64) | 9223372036854775808ULL;
inline std::int64_t require_precision(unsigned fractional_bits)
{
if (!principal_precision(fractional_bits))
throw std::invalid_argument("closed form: precision must be 8, 12, ..., 32");
return one_raw(fractional_bits);
}
inline std::int64_t abs_raw(std::int64_t raw)
{
if (raw >= 0)
return raw;
const auto mag = -static_cast<__int128>(raw);
if (mag > INT64_MAX)
throw std::overflow_error("closed form: magnitude does not fit int64");
return static_cast<std::int64_t>(mag);
}
inline std::int64_t half_of(std::int64_t raw)
{
const bool neg = raw < 0;
auto mag = static_cast<std::uint64_t>(neg ? -static_cast<__int128>(raw) : raw);
const std::uint64_t bit = mag & 1u;
mag >>= 1;
if (bit)
++mag;
if (mag > static_cast<std::uint64_t>(INT64_MAX))
throw std::overflow_error("closed form: value does not fit int64");
const auto out = static_cast<std::int64_t>(mag);
return neg ? -out : out;
}
inline std::int64_t div_int(std::int64_t raw, int denominator)
{
if (denominator <= 0)
throw std::invalid_argument("closed form: divisor must be positive");
const bool neg = raw < 0;
auto mag = static_cast<__int128>(neg ? -static_cast<__int128>(raw) : raw);
const __int128 den = denominator;
__int128 quot = mag / den;
if ((mag % den) * 2 >= den)
++quot;
if (quot > INT64_MAX)
throw std::overflow_error("closed form: value does not fit int64");
const auto out = static_cast<std::int64_t>(quot);
return neg ? -out : out;
}
inline std::int64_t pi_over_4(unsigned fractional_bits)
{
return scale_unit(range_detail::pi_over_4_64, fractional_bits);
}
inline std::int64_t pi_over_2(unsigned fractional_bits)
{
const __int128 wide = static_cast<__int128>(pi_over_4(fractional_bits)) << 1;
if (wide > INT64_MAX)
throw std::overflow_error("closed form: pi/2 does not fit");
return static_cast<std::int64_t>(wide);
}
inline std::int64_t atan_series(unsigned fractional_bits, std::int64_t x)
{
__int128 acc = x;
std::int64_t power = x;
const std::int64_t x2 = mul_raw(x, x, fractional_bits);
int sign = -1;
for (int n = 1; n < 48; ++n)
{
power = mul_raw(power, x2, fractional_bits);
const std::int64_t term = div_int(power, 2 * n + 1);
if (term == 0)
break;
acc += sign < 0 ? -static_cast<__int128>(term) : term;
sign = -sign;
}
if (acc > INT64_MAX || acc < INT64_MIN)
throw std::overflow_error("closed form: atan does not fit");
return static_cast<std::int64_t>(acc);
}
inline std::int64_t atan_positive(unsigned fractional_bits, std::int64_t mag)
{
const std::int64_t one = one_raw(fractional_bits);
if (mag > one)
return pi_over_2(fractional_bits) - atan_positive(fractional_bits, div_raw(one, mag, fractional_bits));
const std::int64_t bound = scale_unit(range_detail::sqrt2_64, fractional_bits) - one;
if (mag > bound)
{
const std::int64_t t = div_raw(mag - one, mag + one, fractional_bits);
return pi_over_4(fractional_bits) + atan_series(fractional_bits, t);
}
return atan_series(fractional_bits, mag);
}
inline std::int64_t eval_atan(unsigned fractional_bits, std::int64_t raw)
{
require_precision(fractional_bits);
if (raw < 0)
return -atan_positive(fractional_bits, abs_raw(raw));
return atan_positive(fractional_bits, raw);
}
inline std::int64_t eval_ln_abs(unsigned fractional_bits, std::int64_t raw)
{
return eval_reduced(reduced::ln, fractional_bits, abs_raw(raw));
}
inline std::int64_t eval_cbrt_abs(unsigned fractional_bits, std::int64_t mag)
{
if (mag == 0)
return 0;
const std::int64_t ln = eval_ln_abs(fractional_bits, mag);
return eval_reduced(reduced::exp, fractional_bits, div_int(ln, 3));
}
inline range_detail::u256 shl_u128(range_detail::u128 value, unsigned shift)
{
if (shift == 0)
return range_detail::u256{value, 0};
if (shift < 128)
return range_detail::u256{value << shift, value >> (128u - shift)};
return range_detail::u256{0, value << (shift - 128u)};
}
inline bool u256_less(range_detail::u256 a, range_detail::u256 b)
{
if (a.hi != b.hi)
return a.hi < b.hi;
return a.lo < b.lo;
}
/// @brief Rounded `(num << shift) / den`.
inline range_detail::u128 div_shifted(range_detail::u128 num, range_detail::u128 den, unsigned shift)
{
if (den == 0)
throw std::domain_error("closed form: division by zero");
const range_detail::u256 target = shl_u128(num, shift);
range_detail::u128 lo = 0;
range_detail::u128 hi = 1;
while (u256_less(range_detail::mul_u128(hi, den), target) ||
(!u256_less(target, range_detail::mul_u128(hi, den)) && hi < (range_detail::u128{1} << 80)))
{
if (hi > (range_detail::u128{1} << 100))
break;
hi <<= 1;
}
while (lo + 1 < hi)
{
const range_detail::u128 mid = lo + (hi - lo) / 2;
if (u256_less(target, range_detail::mul_u128(mid, den)))
hi = mid;
else
lo = mid;
}
const range_detail::u256 half = range_detail::mul_u128(lo + lo + 1, den);
if (!u256_less(target, half))
return lo + 1;
return lo;
}
inline std::int64_t newton_sqrt_raw(unsigned fractional_bits, std::int64_t raw)
{
const unsigned K = fractional_bits * 2u;
range_detail::u128 y = static_cast<range_detail::u128>(
std::max<std::int64_t>(eval_reduced(reduced::sqrt, fractional_bits, raw), 1))
<< fractional_bits;
const range_detail::u128 x = static_cast<range_detail::u128>(raw) << fractional_bits;
for (int step = 0; step < 4; ++step)
{
const range_detail::u128 quot = div_shifted(x, y, K);
y = (y + quot + 1) >> 1;
}
const range_detail::u128 prod = range_detail::round_u256(
range_detail::mul_u128(static_cast<range_detail::u128>(raw), y), K);
if (prod > static_cast<range_detail::u128>(INT64_MAX))
throw std::overflow_error("closed form: power does not fit int64");
return static_cast<std::int64_t>(prod);
}
inline std::int64_t newton_icbrt_raw(unsigned fractional_bits, std::int64_t mag)
{
const unsigned K = fractional_bits * 2u;
int log = 0;
auto bits = static_cast<std::uint64_t>(mag);
while (bits > 1)
{
bits >>= 1;
++log;
}
const int exp = log - static_cast<int>(fractional_bits);
const int third = exp >= 0 ? exp / 3 : -(( -exp + 2) / 3);
range_detail::u128 y = range_detail::u128{1} << static_cast<unsigned>(K + std::max(third, 0));
if (third < 0)
y >>= static_cast<unsigned>(-third);
if (y == 0)
y = 1;
const range_detail::u128 x = static_cast<range_detail::u128>(mag) << fractional_bits;
for (int step = 0; step < 6; ++step)
{
const range_detail::u128 y2 = range_detail::round_u256(range_detail::mul_u128(y, y), K);
if (y2 == 0)
break;
const range_detail::u128 quot = div_shifted(x, y2, K);
y = (y + y + quot) / 3;
if (y == 0)
y = 1;
}
const range_detail::u128 numer = range_detail::u128{1} << (K + fractional_bits);
const range_detail::u128 inv = (numer + y / 2) / y;
if (inv > static_cast<range_detail::u128>(INT64_MAX))
throw std::overflow_error("closed form: inverse cube root does not fit int64");
return static_cast<std::int64_t>(inv);
}
inline std::int64_t sinc_series(unsigned fractional_bits, std::int64_t raw)
{
constexpr unsigned extra = 16;
__int128 acc = __int128{1} << (fractional_bits + extra);
__int128 term = acc;
const std::int64_t mag = raw < 0 ? -raw : raw;
for (int n = 1; n <= 16; ++n)
{
term = range_detail::shr_round_i128(term * mag, fractional_bits);
term = range_detail::shr_round_i128(term * mag, fractional_bits);
term = range_detail::div_round_i128(term, (2 * n) * (2 * n + 1));
if (term == 0)
break;
acc += (n % 2) != 0 ? -term : term;
}
return range_detail::round_i128(acc, extra);
}
inline std::int64_t square_plus(unsigned fractional_bits, std::int64_t raw, int sign)
{
const std::int64_t one = one_raw(fractional_bits);
const std::int64_t sq = mul_raw(raw, raw, fractional_bits);
const __int128 sum = static_cast<__int128>(sq) + (sign < 0 ? -one : one);
if (sum < 0)
throw std::domain_error("closed form: square is below the root domain");
if (sum > INT64_MAX)
throw std::overflow_error("closed form: square does not fit");
return eval_reduced(reduced::sqrt, fractional_bits, static_cast<std::int64_t>(sum));
}
} // namespace closed_detail
/// \complexity A constant number of `eval_reduced` / `eval_window` calls, plus the loops in this file:
/// `atan_series` runs `n = 1 .. 47` (stops on a zero term), `sinc_series` runs `n = 1 .. 16`, `newton_sqrt_raw` runs 4 steps, `newton_icbrt_raw` runs 6 steps after a right-shift log of the magnitude.
/// Extra space `Θ(1)`. These compositions are not a 1-ulp claim; the file comment says they inherit range-reduction error.
/// @see grotto::eval_reduced
/// @see grotto::eval_window
/// @note Not a 1-ulp claim. Poles and domain exits throw `std::domain_error`.
inline std::int64_t eval_closed(closed which, unsigned fractional_bits, std::int64_t raw)
{
using namespace closed_detail;
const std::int64_t one = require_precision(fractional_bits);
switch (which)
{
case closed::atan:
return eval_atan(fractional_bits, raw);
case closed::acot:
return pi_over_2(fractional_bits) - eval_atan(fractional_bits, raw);
case closed::asec:
case closed::acsc:
{
if (abs_raw(raw) < one)
throw std::domain_error("closed form: inverse secant requires |x| >= 1");
const std::int64_t inv = div_raw(one, raw, fractional_bits);
return which == closed::asec
? eval_window(window::acos, fractional_bits, inv)
: eval_window(window::asin, fractional_bits, inv);
}
case closed::atanh:
{
if (abs_raw(raw) >= one)
throw std::domain_error("closed form: atanh domain is (-1, 1)");
const std::int64_t plus = eval_reduced(reduced::log1p, fractional_bits, raw);
const std::int64_t minus = eval_reduced(reduced::log1p, fractional_bits, -raw);
return half_of(plus - minus);
}
case closed::asinh:
{
const std::int64_t mag = abs_raw(raw);
std::int64_t sum = 0;
try
{
sum = mag + square_plus(fractional_bits, mag, +1);
}
catch (const std::overflow_error &)
{
sum = 0;
}
const std::int64_t value = sum == 0
? eval_ln_abs(fractional_bits, mag) + range_detail::ln2_raw(fractional_bits)
: eval_reduced(reduced::ln, fractional_bits, sum);
return raw < 0 ? -value : value;
}
case closed::acosh:
{
if (raw < one)
throw std::domain_error("closed form: acosh domain is [1, inf)");
return eval_reduced(reduced::ln, fractional_bits,
raw + square_plus(fractional_bits, raw, -1));
}
case closed::asech:
{
if (raw <= 0 || raw > one)
throw std::domain_error("closed form: asech domain is (0, 1]");
return eval_closed(closed::acosh, fractional_bits, div_raw(one, raw, fractional_bits));
}
case closed::acsch:
{
if (raw == 0)
throw std::domain_error("closed form: acsch pole");
return eval_closed(closed::asinh, fractional_bits, div_raw(one, raw, fractional_bits));
}
case closed::acoth:
{
if (abs_raw(raw) <= one)
throw std::domain_error("closed form: acoth domain is |x| > 1");
return eval_closed(closed::atanh, fractional_bits, div_raw(one, raw, fractional_bits));
}
case closed::elu:
return raw > 0 ? raw : eval_reduced(reduced::expm1, fractional_bits, raw);
case closed::celu:
return eval_closed(closed::elu, fractional_bits, raw);
case closed::selu:
{
const std::int64_t body = raw > 0
? raw
: mul_raw(scale_unit(selu_alpha_64, fractional_bits),
eval_reduced(reduced::expm1, fractional_bits, raw), fractional_bits);
return mul_raw(scale_unit(selu_scale_64, fractional_bits), body, fractional_bits);
}
case closed::softsign:
{
const std::int64_t mag = abs_raw(raw);
return div_raw(raw, one + mag, fractional_bits);
}
case closed::tanhshrink:
return raw - eval_window(window::tanh, fractional_bits, raw);
case closed::logistic:
{
if (raw <= 0 || raw >= one)
throw std::domain_error("closed form: quantile domain is (0, 1)");
const std::int64_t num = eval_reduced(reduced::ln, fractional_bits, raw);
const std::int64_t den = eval_reduced(reduced::ln, fractional_bits, one - raw);
return num - den;
}
case closed::exponential:
{
if (raw <= 0 || raw >= one)
throw std::domain_error("closed form: quantile domain is (0, 1)");
return -eval_reduced(reduced::ln, fractional_bits, one - raw);
}
case closed::laplace:
{
if (raw <= 0 || raw >= one)
throw std::domain_error("closed form: quantile domain is (0, 1)");
const std::int64_t half = one >> 1;
if (raw <= half)
return eval_reduced(reduced::ln, fractional_bits, raw << 1);
return -eval_reduced(reduced::ln, fractional_bits, (one - raw) << 1);
}
case closed::cauchy:
{
if (raw <= 0 || raw >= one)
throw std::domain_error("closed form: quantile domain is (0, 1)");
const std::int64_t half = one >> 1;
const std::int64_t pi = pi_over_2(fractional_bits) << 1;
const std::int64_t angle = mul_raw(pi, raw - half, fractional_bits);
return eval_reduced(reduced::tan, fractional_bits, angle);
}
case closed::sinc:
if (raw == 0)
return one;
if (abs_raw(raw) <= one)
return sinc_series(fractional_bits, raw);
return div_raw(eval_reduced(reduced::sin, fractional_bits, raw), raw, fractional_bits);
case closed::cbrt:
case closed::icbrt:
{
if (which == closed::icbrt)
{
if (raw == 0)
throw std::domain_error("closed form: inverse cube root of zero");
const std::int64_t value = newton_icbrt_raw(fractional_bits, abs_raw(raw));
return raw < 0 ? -value : value;
}
const std::int64_t root = eval_cbrt_abs(fractional_bits, abs_raw(raw));
return raw < 0 ? -root : root;
}
case closed::qtrt:
case closed::iqtrt:
{
if (raw < 0)
throw std::domain_error("closed form: fourth root requires x >= 0");
if (raw == 0)
return which == closed::qtrt ? 0 : throw std::domain_error("closed form: inverse fourth root of zero"), 0;
const std::int64_t ln = eval_ln_abs(fractional_bits, raw);
const std::int64_t root = eval_reduced(reduced::exp, fractional_bits, div_int(ln, 4));
return which == closed::qtrt ? root : div_raw(one, root, fractional_bits);
}
case closed::pow_m01:
{
if (raw <= 0)
throw std::domain_error("closed form: x^{-0.1} requires x > 0");
const std::int64_t ln = eval_ln_abs(fractional_bits, raw);
const std::int64_t scaled = mul_raw(ln, scale_unit(tenth_64, fractional_bits), fractional_bits);
return eval_reduced(reduced::exp, fractional_bits, -scaled);
}
case closed::pow_p15:
{
if (raw < 0)
throw std::domain_error("closed form: x^{1.5} requires x >= 0");
if (raw == 0)
return 0;
return newton_sqrt_raw(fractional_bits, raw);
}
case closed::pow_m3:
{
if (raw == 0)
throw std::domain_error("closed form: x^{-3} pole");
const std::int64_t sq = mul_raw(raw, raw, fractional_bits);
const std::int64_t cube = mul_raw(sq, raw, fractional_bits);
return div_raw(one, cube, fractional_bits);
}
}
throw std::invalid_argument("closed form: unknown map");
}
} // namespace grotto
#endif // LIBDPF_INCLUDE_GROTTO_CLOSED_FORM_HPP__

View file

@ -73,6 +73,9 @@ struct constant_lut
return values.size() - 1;
return values.size();
}
/// \complexity `upper_bound` on `bounds`. `Θ(log P)` comparisons, `P = linear_parts()`. Extra space `Θ(1)`.
/// @param x raw domain point
/// @return the piece value
HEDLEY_NO_THROW
std::int64_t operator()(Raw x) const noexcept
@ -161,7 +164,7 @@ inline bool magnitude_ge_pow10(u128 mag, int k, unsigned fractional_bits) noexce
}
/// @brief Smallest positive magnitude whose base-10 log is at least `k`.
/// @param k the `k`
/// @param k index or exponent
/// @param fractional_bits the number of fractional bits
/// @return Smallest positive magnitude whose base-10 log is at least `k`
HEDLEY_NO_THROW
@ -522,6 +525,8 @@ struct exact_lut<exact_constant::clrsb>
[=](Raw raw) { return eval<Raw>(raw, fractional_bits); });
}
};
/// \complexity One call to `exact_lut<Which>::eval`. Sign maps are a branch. Bit maps scan at most the raw width. `Θ(w)` bit operations, extra space `Θ(1)`.
/// @see grotto::make_exact_constant_lut
template <exact_constant Which, typename Raw>
std::int64_t evaluate_exact(Raw raw, unsigned fractional_bits)
@ -531,6 +536,15 @@ std::int64_t evaluate_exact(Raw raw, unsigned fractional_bits)
throw std::invalid_argument("fractional bits exceed the raw width");
return exact_lut<Which>::template eval<Raw>(raw, fractional_bits);
}
/// \complexity Dispatches to `exact_lut<Which>::project`. Sign patterns emit a constant number of cuts.
/// `ilogb`, `ceil_ilogb`, `clz`, and `clrsb` push one cut per bit of the raw width (`Θ(w)`). `ilog10` walks powers of ten.
/// Extra space is the cut and value vectors.
/// @see grotto::evaluate_exact
/// @see grotto::dyadic_lut
/// @param which which degree-0 map
/// @param fractional_bits must not exceed the raw width
/// @return the piecewise-constant table
/// @note Following Storrier, Vadapalli, Lyons, and Henry, ePrint 2023/108, Appendix D: the degree-0 exact gadgets.
template <typename Raw>
constant_lut<Raw> make_exact_constant_lut(exact_constant which, unsigned fractional_bits)
@ -577,6 +591,8 @@ enum class threshold_cmp
gt,
geq
};
/// \complexity One comparison. `Θ(1)`.
/// @see grotto::make_threshold_lut
template <typename Raw>
HEDLEY_NO_THROW
@ -591,6 +607,8 @@ std::int64_t evaluate_threshold(Raw raw, Raw bound, threshold_cmp kind) noexcept
}
return 0;
}
/// \complexity Two or three cuts (the bound, and `bound + 1` for `leq` / `gt`). `Θ(1)`.
/// @see grotto::evaluate_threshold
template <typename Raw>
constant_lut<Raw> make_threshold_lut(Raw bound, threshold_cmp kind)
@ -610,6 +628,8 @@ constant_lut<Raw> make_threshold_lut(Raw bound, threshold_cmp kind)
/// @param high the upper endpoint
/// @return `1` on the inclusive clip window `[low, high]`, `0` outside it
/// @throws std::invalid_argument if `low > high`
/// \complexity Two or three cuts. `Θ(1)`.
/// @see grotto::make_threshold_lut
template <typename Raw>
constant_lut<Raw> make_interval_lut(Raw low, Raw high)
{
@ -637,6 +657,8 @@ constant_lut<Raw> make_interval_lut(Raw low, Raw high)
/// @param high the upper endpoint
/// @return `floor(min(max(raw, low), high) / modulus)`, division toward -infinity
/// @throws std::invalid_argument if `modulus must be positive`
/// \complexity One clamp and one division. `Θ(1)`. Division is toward -infinity, as the comment on the declaration says.
/// @see grotto::make_clipped_quotient_lut
template <typename Raw>
std::int64_t evaluate_clipped_quotient(Raw raw, Raw modulus, Raw low, Raw high)
{
@ -652,6 +674,9 @@ std::int64_t evaluate_clipped_quotient(Raw raw, Raw modulus, Raw low, Raw high)
const __int128 neg = -n;
return static_cast<std::int64_t>(-((neg + d - 1) / d));
}
/// \complexity The cut loop steps by `modulus` from `low` through `high`. The header rejects spans with more than `2^16` pieces.
/// Time and extra space `Θ((high - low) / modulus)` in raw units.
/// @see grotto::evaluate_clipped_quotient
template <typename Raw>
constant_lut<Raw> make_clipped_quotient_lut(Raw modulus, Raw low, Raw high)

277
include/grotto/dwt_lut.hpp Normal file
View file

@ -0,0 +1,277 @@
/// @file grotto/dwt_lut.hpp
/// @brief Haar and bior(5,3) compressed lookup tables.
/// @details Cleartext evaluators for Reis, Ugurbil, Wagh, Henry, and de Vega,
/// PoPETs 2025 ([ePrint 2025/013](@ref bib_wave)). A power-of-two
/// grid is reduced by a discrete wavelet transform and stored as
/// fixed-point approximation coefficients. Haar evaluates with one
/// lookup of the high bits. bior(5,3) evaluates Equation (8): two
/// adjacent coefficients, weighted by `(2^j - lsb)` and `lsb`.
/// @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_GROTTO_DWT_LUT_HPP__
#define LIBDPF_INCLUDE_GROTTO_DWT_LUT_HPP__
#include <cmath>
#include <cstdint>
#include <limits>
#include <stdexcept>
#include <vector>
namespace grotto
{
/// @brief Which wavelet compresses the table.
enum class dwt_family : unsigned
{
/// Orthogonal Haar. One approximation coefficient per block of `2^depth` samples.
haar = 0,
/// Biorthogonal bior(5,3), PyWavelets `bior2.2`. Two taps at evaluation.
bior53,
};
/// @brief Fixed-point Haar or bior(5,3) table.
/// @details `coeff` holds the quantized approximation coefficients.
/// `operator()` is the cleartext evaluation those MPC protocols return.
struct dwt_lut
{
dwt_family family = dwt_family::haar;
/// Transform depth `j`. The kept index is `raw >> depth`.
unsigned depth = 0;
/// `log2` of the sample count. Inputs lie in `[0, 2^{domain_bits})`.
unsigned domain_bits = 0;
/// Fraction bits of every stored coefficient and of the returned word.
unsigned fractional_bits = 0;
std::vector<std::int64_t> coeff;
/// \complexity Haar is one indexing step, `Θ(1)`. bior(5,3) is two multiplications and a shift by `2j`, `Θ(1)`. Extra space `Θ(1)`.
/// @param raw index on the sampled grid, in `[0, 2^{domain_bits})`
/// @return fixed-point approximation, `fractional_bits` fraction bits
std::int64_t operator()(std::uint64_t raw) const
{
if (domain_bits == 0 || domain_bits > 30 || depth == 0 || depth > domain_bits)
throw std::logic_error("dwt lut: table is not initialized");
const std::uint64_t domain = std::uint64_t{1} << domain_bits;
if (raw >= domain)
throw std::out_of_range("dwt lut: input is outside the sampled domain");
const std::uint64_t msb = raw >> depth;
if (family == dwt_family::haar)
{
if (msb >= coeff.size())
throw std::out_of_range("dwt lut: Haar index");
return coeff[static_cast<std::size_t>(msb)];
}
if (coeff.empty() || msb >= coeff.size())
throw std::out_of_range("dwt lut: bior index");
// Smooth extension prepends two coefficients, so bin `msb` lives at
// `msb + 2` and its neighbor at `msb + 3` (Equation (8), artifact indexing).
const std::size_t n = coeff.size();
const auto c0 = coeff[static_cast<std::size_t>((msb + 2) % n)];
const auto c1 = coeff[static_cast<std::size_t>((msb + 3) % n)];
const std::uint64_t span = std::uint64_t{1} << depth;
const std::uint64_t lsb = raw & (span - 1);
const __int128 comb = __int128(c0) * static_cast<__int128>(span - lsb)
+ __int128(c1) * static_cast<__int128>(lsb);
const __int128 den = __int128{1} << (2 * depth);
return detail_floor_div(comb, den);
}
private:
static std::int64_t detail_floor_div(__int128 num, __int128 den)
{
__int128 q = num / den;
const __int128 r = num % den;
if (r != 0 && num < 0)
--q;
if (q > std::numeric_limits<std::int64_t>::max()
|| q < std::numeric_limits<std::int64_t>::min())
throw std::overflow_error("dwt lut: value does not fit int64");
return static_cast<std::int64_t>(q);
}
};
namespace dwt_detail
{
inline unsigned log2_pow2(std::size_t n)
{
if (n < 2 || (n & (n - 1)) != 0)
throw std::invalid_argument("dwt lut: sample count must be a power of two, at least 2");
unsigned bits = 0;
while ((std::size_t{1} << bits) != n)
++bits;
if (bits > 30)
throw std::invalid_argument("dwt lut: sample count exceeds 2^30");
return bits;
}
inline std::int64_t quantize(double y, unsigned fractional_bits)
{
const double scaled = std::floor(y * std::ldexp(1.0, static_cast<int>(fractional_bits)));
const double lo = static_cast<double>(std::numeric_limits<std::int64_t>::min());
if (!(scaled >= lo && scaled < 0x1p63))
throw std::overflow_error("dwt lut: coefficient does not fit int64");
return static_cast<std::int64_t>(scaled);
}
inline double half_exp(int k)
{
double scale = std::ldexp(1.0, k / 2);
if (k % 2 != 0)
scale *= (k < 0) ? std::sqrt(0.5) : std::sqrt(2.0);
return scale;
}
inline std::vector<double> down_approx(const std::vector<double> & in, const double * filt, int F)
{
const int N = static_cast<int>(in.size());
if (N < 1)
throw std::invalid_argument("dwt lut: empty approximation");
const int expect = (N + F - 1) / 2;
if (N < 2)
{
double sum = 0;
for (int j = 0; j < F; ++j)
sum += filt[j];
return std::vector<double>(static_cast<std::size_t>(expect), in[0] * sum);
}
std::vector<double> out;
out.reserve(static_cast<std::size_t>(expect));
int i = 1;
while (i < F && i < N)
{
double s = 0;
int j = 0;
for (; j <= i; ++j)
s += filt[j] * in[static_cast<std::size_t>(i - j)];
for (int k = 1; j < F; ++j, ++k)
s += filt[j] * (in[0] + static_cast<double>(k) * (in[0] - in[1]));
out.push_back(s);
i += 2;
}
while (i < N)
{
double s = 0;
for (int j = 0; j < F; ++j)
s += in[static_cast<std::size_t>(i - j)] * filt[j];
out.push_back(s);
i += 2;
}
while (i < F)
{
double s = 0;
int j = 0;
for (int k = i - N + 1; i - j >= N; ++j, --k)
s += filt[j] * (in[static_cast<std::size_t>(N - 1)]
+ static_cast<double>(k) * (in[static_cast<std::size_t>(N - 1)]
- in[static_cast<std::size_t>(N - 2)]));
for (; j <= i; ++j)
s += filt[j] * in[static_cast<std::size_t>(i - j)];
for (int k = 1; j < F; ++j, ++k)
s += filt[j] * (in[0] + static_cast<double>(k) * (in[0] - in[1]));
out.push_back(s);
i += 2;
}
while (i < N + F - 1)
{
double s = 0;
int j = 0;
for (int k = i - N + 1; i - j >= N; ++j, --k)
s += filt[j] * (in[static_cast<std::size_t>(N - 1)]
+ static_cast<double>(k) * (in[static_cast<std::size_t>(N - 1)]
- in[static_cast<std::size_t>(N - 2)]));
for (; j < F; ++j)
s += filt[j] * in[static_cast<std::size_t>(i - j)];
out.push_back(s);
i += 2;
}
if (static_cast<int>(out.size()) != expect)
throw std::logic_error("dwt lut: approximation length");
return out;
}
inline dwt_lut build(dwt_family family, const std::vector<double> & samples,
unsigned fractional_bits, unsigned depth)
{
if (fractional_bits > 62)
throw std::invalid_argument("dwt lut: fractional width does not fit");
const unsigned domain_bits = log2_pow2(samples.size());
if (depth == 0 || depth > domain_bits)
throw std::invalid_argument("dwt lut: depth must lie in 1 .. domain bits");
const double s2 = std::sqrt(2.0);
const double haar_lo[2] = {s2 / 2, s2 / 2};
const double s2_8 = s2 / 8;
const double s2_4 = s2 / 4;
const double bior_lo[6] = {0.0, -s2_8, s2_4, 3 * s2_4, s2_4, -s2_8};
const double * filt = haar_lo;
int flen = 2;
int scale_sign = -1;
if (family == dwt_family::bior53)
{
filt = bior_lo;
flen = 6;
scale_sign = 1;
}
std::vector<double> approx = samples;
for (unsigned level = 0; level < depth; ++level)
approx = down_approx(approx, filt, flen);
const double scale = half_exp(scale_sign * static_cast<int>(depth));
dwt_lut table;
table.family = family;
table.depth = depth;
table.domain_bits = domain_bits;
table.fractional_bits = fractional_bits;
table.coeff.reserve(approx.size());
for (double a : approx)
table.coeff.push_back(quantize(a * scale, fractional_bits));
return table;
}
} // namespace dwt_detail
/// @brief Sample `f` on `i · 2^{-fractional_bits}` for `i` in `[0, 2^{domain_bits})`.
/// \complexity `Θ(2^{domain_bits})` calls and extra space.
/// @tparam Fn callable `double(double)`
template <typename Fn>
std::vector<double> sample_dwt_signal(unsigned domain_bits, unsigned fractional_bits, Fn && f)
{
if (domain_bits == 0 || domain_bits > 30)
throw std::invalid_argument("dwt lut: domain bits must lie in 1 .. 30");
if (fractional_bits > 62)
throw std::invalid_argument("dwt lut: fractional width does not fit");
const std::size_t n = std::size_t{1} << domain_bits;
const double step = std::ldexp(1.0, -static_cast<int>(fractional_bits));
std::vector<double> samples(n);
for (std::size_t i = 0; i < n; ++i)
samples[i] = f(static_cast<double>(i) * step);
return samples;
}
/// \complexity One smooth-extension low-pass per level. `Θ(N)` arithmetic and extra space, `N` the sample count.
/// @param samples real signal, length `2^{domain bits}`
/// @param fractional_bits fraction bits of the stored words
/// @param depth transform depth `j`, in `1 .. log2(samples)`
dwt_lut make_haar_dwt_lut(const std::vector<double> & samples,
unsigned fractional_bits, unsigned depth)
{
return dwt_detail::build(dwt_family::haar, samples, fractional_bits, depth);
}
/// \complexity One smooth-extension low-pass per level. `Θ(N)` arithmetic and extra space, `N` the sample count.
/// @param samples real signal, length `2^{domain bits}`
/// @param fractional_bits fraction bits of the stored words
/// @param depth transform depth `j`, in `1 .. log2(samples)`
dwt_lut make_bior53_dwt_lut(const std::vector<double> & samples,
unsigned fractional_bits, unsigned depth)
{
return dwt_detail::build(dwt_family::bior53, samples, fractional_bits, depth);
}
} // namespace grotto
#endif // LIBDPF_INCLUDE_GROTTO_DWT_LUT_HPP__

View file

@ -227,6 +227,9 @@ easy_lut<Raw> predicate_lut(unsigned fractional_bits, bool at_or_below_zero,
}
} // namespace detail
/// \complexity Two or three cuts around 0, from `predicate_lut` / `steps_from_cuts`. `Θ(1)` time and extra space.
/// @see grotto::make_exact_constant_lut
/// @see grotto::easy_lut
template <typename Raw>
HEDLEY_WARN_UNUSED_RESULT
@ -234,6 +237,9 @@ easy_lut<Raw> make_positive_lut(unsigned fractional_bits = 0)
{
return detail::predicate_lut<Raw>(fractional_bits, false, false, true);
}
/// \complexity Two or three cuts around 0, from `predicate_lut` / `steps_from_cuts`. `Θ(1)` time and extra space.
/// @see grotto::make_exact_constant_lut
/// @see grotto::easy_lut
template <typename Raw>
HEDLEY_WARN_UNUSED_RESULT
@ -241,6 +247,9 @@ easy_lut<Raw> make_negative_lut(unsigned fractional_bits = 0)
{
return detail::predicate_lut<Raw>(fractional_bits, true, false, false);
}
/// \complexity Two or three cuts around 0, from `predicate_lut` / `steps_from_cuts`. `Θ(1)` time and extra space.
/// @see grotto::make_exact_constant_lut
/// @see grotto::easy_lut
template <typename Raw>
HEDLEY_WARN_UNUSED_RESULT
@ -248,6 +257,9 @@ easy_lut<Raw> make_nonnegative_lut(unsigned fractional_bits = 0)
{
return detail::predicate_lut<Raw>(fractional_bits, false, true, true);
}
/// \complexity Two or three cuts around 0, from `predicate_lut` / `steps_from_cuts`. `Θ(1)` time and extra space.
/// @see grotto::make_exact_constant_lut
/// @see grotto::easy_lut
template <typename Raw>
HEDLEY_WARN_UNUSED_RESULT
@ -255,6 +267,9 @@ easy_lut<Raw> make_nonpositive_lut(unsigned fractional_bits = 0)
{
return detail::predicate_lut<Raw>(fractional_bits, true, true, false);
}
/// \complexity Two or three cuts around 0, from `predicate_lut` / `steps_from_cuts`. `Θ(1)` time and extra space.
/// @see grotto::make_exact_constant_lut
/// @see grotto::easy_lut
template <typename Raw>
HEDLEY_WARN_UNUSED_RESULT
@ -262,6 +277,9 @@ easy_lut<Raw> make_zero_lut(unsigned fractional_bits = 0)
{
return detail::predicate_lut<Raw>(fractional_bits, false, true, false);
}
/// \complexity Two or three cuts around 0, from `predicate_lut` / `steps_from_cuts`. `Θ(1)` time and extra space.
/// @see grotto::make_exact_constant_lut
/// @see grotto::easy_lut
template <typename Raw>
HEDLEY_WARN_UNUSED_RESULT
@ -269,6 +287,9 @@ easy_lut<Raw> make_nonzero_lut(unsigned fractional_bits = 0)
{
return detail::predicate_lut<Raw>(fractional_bits, true, false, true);
}
/// \complexity Two or three cuts around 0, from `predicate_lut` / `steps_from_cuts`. `Θ(1)` time and extra space.
/// @see grotto::make_exact_constant_lut
/// @see grotto::easy_lut
template <typename Raw>
HEDLEY_WARN_UNUSED_RESULT
@ -283,6 +304,8 @@ easy_lut<Raw> make_signum_lut(unsigned fractional_bits = 0)
return detail::easy_poly{one, 0, 0, 1};
});
}
/// \complexity One cut per bit of `raw_width<Raw>()` (the powers of two, plus 0 and 1). `Θ(w)` time and extra space, `w` the raw width.
/// @see grotto::eval_bit_width
template <typename Raw>
HEDLEY_WARN_UNUSED_RESULT
@ -296,6 +319,8 @@ easy_lut<Raw> make_clz_lut(unsigned fractional_bits = 0)
return detail::unit_poly(detail::clz_of<Raw>(raw), fractional_bits);
});
}
/// \complexity Cuts at `±2^e` across the raw width. `Θ(w)` time and extra space.
/// @see grotto::exact_constant
template <typename Raw>
HEDLEY_WARN_UNUSED_RESULT
@ -316,6 +341,9 @@ easy_lut<Raw> make_clrsb_lut(unsigned fractional_bits = 0)
return detail::unit_poly(detail::clrsb_of<Raw>(raw), fractional_bits);
});
}
/// \complexity One cut per bit on each side of zero (`Θ(w)`), then one `ilogb_units` (a `floor_log2`) per piece. `Θ(w)` time and extra space.
/// @see grotto::exact_constant
/// @note `ilogb(0)` is the sentinel `ilog_of_zero`, not a logarithm.
template <typename Raw>
HEDLEY_WARN_UNUSED_RESULT
@ -339,6 +367,10 @@ easy_lut<Raw> make_ilogb_lut(unsigned fractional_bits = 0)
fractional_bits);
});
}
/// \complexity For each decimal exponent from `ilog10_units(1)` to the exponent of the type max, two binary searches over the positive raw domain (`Θ(w)` probes each).
/// The exponent count is `Θ(w)`. Time `Θ(w²)` probes. Extra space is the cut vector, one cut per exponent class.
/// @see grotto::exact_constant
/// @note `ilog10(0)` is `ilog_of_zero`.
template <typename Raw>
HEDLEY_WARN_UNUSED_RESULT
@ -401,10 +433,14 @@ easy_lut<Raw> make_ilog10_lut(unsigned fractional_bits = 0)
/// @details Index 0 is the sign bit. Larger indexes are refused: the bit is constant
/// on `2^{index+1}` intervals.
/// @tparam Raw underlying representation
/// @param index the index
/// @param index bit index counting down from the MSB; must be `< msb_bit_limit`
/// @param fractional_bits the number of fractional bits
/// @return Bit `index` counting down from the most significant bit of `Raw`
/// @throws std::invalid_argument if `only the most significant bits are piecewise-cheap`
/// \complexity The hot bit is constant on steps of `2^{width-1-index}`. The cut loop emits `Θ(2^{index})` boundaries, and `index` must be `< msb_bit_limit` (8), so at most 256 cuts.
/// Time and extra space `Θ(2^{index})`.
/// @see grotto::easy_lut
/// @see grotto::eval_bit_width
template <typename Raw>
HEDLEY_WARN_UNUSED_RESULT
easy_lut<Raw> make_msb_lut(unsigned index, unsigned fractional_bits = 0)

View file

@ -40,6 +40,9 @@ struct easy_lut
HEDLEY_NO_THROW
std::size_t parts() const noexcept { return c0.size(); }
/// \complexity `upper_bound` on `bounds` (`Θ(log P)` comparisons, `P = parts()`), then three multiplies for the quadratic. Extra space `Θ(1)`.
/// @param x raw fixed-point input
/// @return the rounded piece value
std::int64_t operator()(Raw x) const
{
@ -155,6 +158,11 @@ inline std::int64_t denom_shift(unsigned shift)
}
} // namespace detail
/// \complexity Assembles a constant number of pieces (the knots are a fixed integer pattern shifted by `fractional_bits`). `Θ(1)` time and extra space, aside from the returned vectors of that constant length.
/// @see grotto::easy_lut
/// @see grotto::eval_window
/// @param fractional_bits fractional bits; integer knots become `k << fractional_bits`
/// @tparam Raw signed raw word
template <typename Raw>
easy_lut<Raw> make_abs_lut(unsigned fractional_bits = 0)
@ -167,6 +175,11 @@ easy_lut<Raw> make_abs_lut(unsigned fractional_bits = 0)
return p;
});
}
/// \complexity Assembles a constant number of pieces (the knots are a fixed integer pattern shifted by `fractional_bits`). `Θ(1)` time and extra space, aside from the returned vectors of that constant length.
/// @see grotto::easy_lut
/// @see grotto::eval_window
/// @param fractional_bits fractional bits; integer knots become `k << fractional_bits`
/// @tparam Raw signed raw word
template <typename Raw>
easy_lut<Raw> make_relu_lut(unsigned fractional_bits = 0)
@ -182,6 +195,9 @@ easy_lut<Raw> make_relu_lut(unsigned fractional_bits = 0)
/// @tparam Raw underlying representation
/// @param shift the bit shift
/// @return Negative side is `x / 2^shift`, rounded to nearest, ties away from zero
/// \complexity Assembles a constant number of pieces. `Θ(1)` time and extra space, aside from the returned vectors of that constant length.
/// @see grotto::easy_lut
/// @see grotto::eval_window
template <typename Raw>
easy_lut<Raw> make_leaky_relu_lut(unsigned shift)
{
@ -192,6 +208,11 @@ easy_lut<Raw> make_leaky_relu_lut(unsigned shift)
return detail::easy_poly{0, 1, 0, den};
});
}
/// \complexity Assembles a constant number of pieces (the knots are a fixed integer pattern shifted by `fractional_bits`). `Θ(1)` time and extra space, aside from the returned vectors of that constant length.
/// @see grotto::easy_lut
/// @see grotto::eval_window
/// @param fractional_bits fractional bits; integer knots become `k << fractional_bits`
/// @tparam Raw signed raw word
template <typename Raw>
easy_lut<Raw> make_squared_relu_lut(unsigned fractional_bits)
@ -233,18 +254,35 @@ easy_lut<Raw> clip_to(unsigned fractional_bits, std::int64_t low_units, std::int
}
} // namespace detail
/// @brief Clip to `[low_units, high_units]` in raw units, then shift the knots by `fractional_bits`.
/// @tparam Raw signed raw word
/// @param fractional_bits fractional bits; integer knots become `k << fractional_bits`
/// @param low_units inclusive lower clip, in integer units before the fractional shift
/// @param high_units inclusive upper clip, in integer units before the fractional shift
/// \complexity Assembles a constant number of pieces. `Θ(1)` time and extra space.
/// @see grotto::easy_lut
/// @see grotto::eval_window
template <typename Raw>
easy_lut<Raw> make_clip_lut(unsigned fractional_bits, std::int64_t low_units, std::int64_t high_units)
{
return detail::clip_to<Raw>(fractional_bits, low_units, high_units);
}
/// \complexity Assembles a constant number of pieces (the knots are a fixed integer pattern shifted by `fractional_bits`). `Θ(1)` time and extra space, aside from the returned vectors of that constant length.
/// @see grotto::easy_lut
/// @see grotto::eval_window
/// @param fractional_bits fractional bits; integer knots become `k << fractional_bits`
/// @tparam Raw signed raw word
template <typename Raw>
easy_lut<Raw> make_relu6_lut(unsigned fractional_bits)
{
return make_clip_lut<Raw>(fractional_bits, 0, 6);
}
/// \complexity Assembles a constant number of pieces (the knots are a fixed integer pattern shifted by `fractional_bits`). `Θ(1)` time and extra space, aside from the returned vectors of that constant length.
/// @see grotto::easy_lut
/// @see grotto::eval_window
/// @param fractional_bits fractional bits; integer knots become `k << fractional_bits`
/// @tparam Raw signed raw word
template <typename Raw>
easy_lut<Raw> make_hardtanh_lut(unsigned fractional_bits)
@ -256,6 +294,9 @@ easy_lut<Raw> make_hardtanh_lut(unsigned fractional_bits)
/// @tparam Raw underlying representation
/// @param fractional_bits the number of fractional bits
/// @return `0` on `[-1, 1]`, `x - 1` above, `x + 1` below
/// \complexity Assembles a constant number of pieces (the knots are a fixed integer pattern shifted by `fractional_bits`). `Θ(1)` time and extra space, aside from the returned vectors of that constant length.
/// @see grotto::easy_lut
/// @see grotto::eval_window
template <typename Raw>
easy_lut<Raw> make_softshrink_lut(unsigned fractional_bits)
{
@ -281,6 +322,9 @@ easy_lut<Raw> make_softshrink_lut(unsigned fractional_bits)
/// @tparam Raw underlying representation
/// @param fractional_bits the number of fractional bits
/// @return `0` on `[-1, 1]`, identity outside
/// \complexity Assembles a constant number of pieces (the knots are a fixed integer pattern shifted by `fractional_bits`). `Θ(1)` time and extra space, aside from the returned vectors of that constant length.
/// @see grotto::easy_lut
/// @see grotto::eval_window
template <typename Raw>
easy_lut<Raw> make_hardshrink_lut(unsigned fractional_bits)
{
@ -305,6 +349,9 @@ easy_lut<Raw> make_hardshrink_lut(unsigned fractional_bits)
/// @param fractional_bits the number of fractional bits
/// @return `0` left of `-3`, `1` right of `3`, `(x + 3) / 6` between, rounded
/// @throws std::invalid_argument if `fractional width does not fit`
/// \complexity Assembles a constant number of pieces (the knots are a fixed integer pattern shifted by `fractional_bits`). `Θ(1)` time and extra space, aside from the returned vectors of that constant length.
/// @see grotto::easy_lut
/// @see grotto::eval_window
template <typename Raw>
easy_lut<Raw> make_hardsigmoid_lut(unsigned fractional_bits)
{
@ -335,6 +382,9 @@ easy_lut<Raw> make_hardsigmoid_lut(unsigned fractional_bits)
/// @param fractional_bits the number of fractional bits
/// @return `0` left of `-3`, `x` right of `3`, `x(x + 3) / 6` between, rounded
/// @throws std::invalid_argument if `fractional width does not fit the denominator`
/// \complexity Assembles a constant number of pieces (the knots are a fixed integer pattern shifted by `fractional_bits`). `Θ(1)` time and extra space, aside from the returned vectors of that constant length.
/// @see grotto::easy_lut
/// @see grotto::eval_window
template <typename Raw>
easy_lut<Raw> make_hardswish_lut(unsigned fractional_bits)
{
@ -360,6 +410,25 @@ easy_lut<Raw> make_hardswish_lut(unsigned fractional_bits)
});
}
/// @brief Appendix D leaky ReLU: identity on the right, `x/100` on the left.
/// @details `make_leaky_relu_lut` is the dyadic slope `1/2^shift`. This one is
/// the paper's slope `1/100`, rounded to nearest, ties away from zero.
/// The fractional scale cancels, so the denominator does not depend
/// on `fractional_bits`.
/// @tparam Raw underlying representation
/// @return two pieces, degree 1
/// \complexity Assembles two pieces. `Θ(1)` time and extra space, aside from the returned vectors.
/// @see grotto::make_leaky_relu_lut
template <typename Raw>
easy_lut<Raw> make_leaky_relu_hundredth_lut()
{
return detail::assemble_easy<Raw>({0}, [](std::int64_t raw) {
if (raw >= 0)
return detail::kIdentity;
return detail::easy_poly{0, 1, 0, 100};
});
}
} // namespace grotto
#endif // LIBDPF_INCLUDE_GROTTO_EASY_LUT_HPP__

View file

@ -0,0 +1,407 @@
/// @file grotto/exact_steps.hpp
/// @brief Exact Grotto steps: digit lengths, integer logs, and word bits.
/// @details Counts and boolean results are fixed-point integers,
/// `n << fractional_bits`. `ilog16(0)`, `ilog256(0)`, and
/// `logstar` of a non-positive input return `ilog_of_zero`.
/// `dec_width`, `oct_width`, and `b64_width` count digits of
/// `floor(|x|)` in bases 10, 8, and 64; zero has length 1.
/// `bit_width`, `bit_floor`, `bit_ceil`, `countl_one`, and
/// `has_single_bit` use the unsigned `Raw` bit pattern, matching
/// the C++ `<bit>` operations on that width.
/// `deg2rad` and `rad2deg` are the linear scale by `π/180` and
/// `180/π`. They do not wrap the angle.
#ifndef LIBDPF_INCLUDE_GROTTO_EXACT_STEPS_HPP__
#define LIBDPF_INCLUDE_GROTTO_EXACT_STEPS_HPP__
#include "grotto/dyadic_lut.hpp"
#include <cstdint>
#include <stdexcept>
#include <type_traits>
#include <vector>
namespace grotto
{
namespace exact_detail
{
inline int ceil_div(int numerator, int denominator)
{
if (numerator >= 0)
return (numerator + denominator - 1) / denominator;
return -((-numerator) / denominator);
}
template <typename Raw>
int floor_log2_real(std::int64_t raw, unsigned fractional_bits, bool & exact_power)
{
const detail::u128 mag = detail::magnitude<Raw>(raw);
exact_power = mag != 0 && (mag & (mag - 1)) == 0;
return detail::floor_log2_u128(mag) - static_cast<int>(fractional_bits);
}
inline std::int64_t abs_floor(std::int64_t raw, unsigned fractional_bits)
{
if (fractional_bits >= 63)
throw std::invalid_argument("exact step: fractional width does not fit");
const detail::u128 mag = raw < 0
? detail::u128(-static_cast<__int128>(raw))
: detail::u128(raw);
return static_cast<std::int64_t>(mag >> fractional_bits);
}
inline int positive_length(std::int64_t magnitude, int base)
{
if (magnitude <= 0)
return 1;
if (base == 2 || base == 8 || base == 64)
{
int log = 0;
auto n = static_cast<std::uint64_t>(magnitude);
while (n > 1)
{
n >>= 1;
++log;
}
const int group = base == 2 ? 1 : (base == 8 ? 3 : 6);
return log / group + 1;
}
int digits = 0;
while (magnitude > 0)
{
magnitude /= base;
++digits;
}
return digits;
}
} // namespace exact_detail
/// @brief `ceil(log2(|x|) / 4)`, the integer log base 16.
/// \complexity A constant number of shifts and a `countl` / `bit_width` style scan of the `Raw` width (at most 64 iterations of `n >>= 1` in `positive_length` / `floor_log2_real`). `Θ(width)` bit operations, extra space `Θ(1)`.
/// Counts are returned as `units << fractional_bits`.
/// @see grotto::make_exact_constant_lut
/// @see grotto::eval_ilog16
template <typename Raw>
std::int64_t eval_ilog16(std::int64_t raw, unsigned fractional_bits)
{
if (raw == 0)
return ilog_of_zero;
bool exact = false;
const int e = exact_detail::floor_log2_real<Raw>(raw, fractional_bits, exact);
const int units = exact_detail::ceil_div(exact ? e : e + 1, 4);
return detail::encode_units(units, fractional_bits);
}
/// @brief `ceil(log2(|x|) / 8)`, the integer log base 256.
/// \complexity A constant number of shifts and a `countl` / `bit_width` style scan of the `Raw` width (at most 64 iterations of `n >>= 1` in `positive_length` / `floor_log2_real`). `Θ(width)` bit operations, extra space `Θ(1)`.
/// Counts are returned as `units << fractional_bits`.
/// @see grotto::make_exact_constant_lut
/// @see grotto::eval_ilog16
template <typename Raw>
std::int64_t eval_ilog256(std::int64_t raw, unsigned fractional_bits)
{
if (raw == 0)
return ilog_of_zero;
bool exact = false;
const int e = exact_detail::floor_log2_real<Raw>(raw, fractional_bits, exact);
const int units = exact_detail::ceil_div(exact ? e : e + 1, 8);
return detail::encode_units(units, fractional_bits);
}
/// @brief Iterated logarithm. `log*(x) = 0` for `|x| <= 1`, otherwise
/// `1 + log*(log2(|x|))`. Non-positive inputs return `ilog_of_zero`.
/// \complexity Five comparisons of the magnitude against `2^{fractional_bits + s}` for `s` in `{0,1,2,4,16}`. No iterated loop. `Θ(1)`, extra space `Θ(1)`.
/// Non-positive inputs return `ilog_of_zero` (the dyadic sentinel), not a mathematical log-star.
/// @see grotto::eval_ilog16
template <typename Raw>
std::int64_t eval_logstar(std::int64_t raw, unsigned fractional_bits)
{
if (raw <= 0)
return ilog_of_zero;
if (fractional_bits >= 63)
throw std::invalid_argument("exact step: fractional width does not fit");
const detail::u128 mag = detail::magnitude<Raw>(raw);
const auto below = [&](unsigned shift) {
if (fractional_bits + shift >= 128)
return true;
return mag <= (detail::u128{1} << (fractional_bits + shift));
};
int units = 5;
if (below(0))
units = 0;
else if (below(1))
units = 1;
else if (below(2))
units = 2;
else if (below(4))
units = 3;
else if (below(16))
units = 4;
return detail::encode_units(units, fractional_bits);
}
/// @brief `std::bit_width` of the unsigned `Raw` pattern.
/// \complexity A constant number of shifts and a `countl` / `bit_width` style scan of the `Raw` width (at most 64 iterations of `n >>= 1` in `positive_length` / `floor_log2_real`). `Θ(width)` bit operations, extra space `Θ(1)`.
/// Counts are returned as `units << fractional_bits`.
/// @see grotto::make_exact_constant_lut
/// @see grotto::eval_ilog16
template <typename Raw>
std::int64_t eval_bit_width(std::int64_t raw, unsigned fractional_bits)
{
const auto bits = detail::raw_bits<Raw>(raw);
int units = 0;
if (bits != 0)
units = 64 - __builtin_clzll(bits);
return detail::encode_units(units, fractional_bits);
}
/// @brief `std::countl_one` of the unsigned `Raw` pattern.
/// \complexity A constant number of shifts and a `countl` / `bit_width` style scan of the `Raw` width (at most 64 iterations of `n >>= 1` in `positive_length` / `floor_log2_real`). `Θ(width)` bit operations, extra space `Θ(1)`.
/// Counts are returned as `units << fractional_bits`.
/// @see grotto::make_exact_constant_lut
/// @see grotto::eval_ilog16
template <typename Raw>
std::int64_t eval_countl_one(std::int64_t raw, unsigned fractional_bits)
{
const int units = detail::countl_one_width(
detail::raw_bits<Raw>(raw), detail::raw_width<Raw>());
return detail::encode_units(units, fractional_bits);
}
/// @brief `std::has_single_bit` of the unsigned `Raw` pattern.
/// \complexity A constant number of shifts and a `countl` / `bit_width` style scan of the `Raw` width (at most 64 iterations of `n >>= 1` in `positive_length` / `floor_log2_real`). `Θ(width)` bit operations, extra space `Θ(1)`.
/// Counts are returned as `units << fractional_bits`.
/// @see grotto::make_exact_constant_lut
/// @see grotto::eval_ilog16
template <typename Raw>
std::int64_t eval_has_single_bit(std::int64_t raw, unsigned fractional_bits)
{
const auto bits = detail::raw_bits<Raw>(raw);
const bool on = bits != 0 && (bits & (bits - 1)) == 0;
return detail::encode_units(on ? 1 : 0, fractional_bits);
}
/// @brief `std::bit_floor` of the unsigned `Raw` pattern, as a raw word.
/// \complexity A constant number of shifts and a `countl` / `bit_width` style scan of the `Raw` width (at most 64 iterations of `n >>= 1` in `positive_length` / `floor_log2_real`). `Θ(width)` bit operations, extra space `Θ(1)`.
/// Counts are returned as `units << fractional_bits`.
/// @see grotto::make_exact_constant_lut
/// @see grotto::eval_ilog16
template <typename Raw>
std::int64_t eval_bit_floor(std::int64_t raw)
{
const auto bits = detail::raw_bits<Raw>(raw);
if (bits == 0)
return 0;
const int log = 63 - __builtin_clzll(bits);
return static_cast<std::int64_t>(std::uint64_t{1} << static_cast<unsigned>(log));
}
/// @brief `std::bit_ceil` of the unsigned `Raw` pattern, as a raw word.
/// \complexity A constant number of shifts and a `countl` / `bit_width` style scan of the `Raw` width (at most 64 iterations of `n >>= 1` in `positive_length` / `floor_log2_real`). `Θ(width)` bit operations, extra space `Θ(1)`.
/// Counts are returned as `units << fractional_bits`.
/// @see grotto::make_exact_constant_lut
/// @see grotto::eval_ilog16
template <typename Raw>
std::int64_t eval_bit_ceil(std::int64_t raw)
{
const auto bits = detail::raw_bits<Raw>(raw);
if (bits <= 1)
return 1;
if ((bits & (bits - 1)) == 0)
return static_cast<std::int64_t>(bits);
const int log = 64 - __builtin_clzll(bits);
if (log >= 63)
throw std::overflow_error("exact step: bit_ceil does not fit int64");
return std::int64_t{1} << log;
}
/// @brief `floor(x)` at the same fractional scale.
/// \complexity A constant number of shifts and a `countl` / `bit_width` style scan of the `Raw` width (at most 64 iterations of `n >>= 1` in `positive_length` / `floor_log2_real`). `Θ(width)` bit operations, extra space `Θ(1)`.
/// Counts are returned as `units << fractional_bits`.
/// @see grotto::make_exact_constant_lut
/// @see grotto::eval_ilog16
inline std::int64_t eval_dec_floor(std::int64_t raw, unsigned fractional_bits)
{
if (fractional_bits >= 63)
throw std::invalid_argument("exact step: fractional width does not fit");
const std::int64_t one = std::int64_t{1} << fractional_bits;
std::int64_t q = raw / one;
const std::int64_t r = raw - q * one;
if (r < 0)
--q;
return q << fractional_bits;
}
/// @brief `ceil(x)` at the same fractional scale.
/// \complexity A constant number of shifts and a `countl` / `bit_width` style scan of the `Raw` width (at most 64 iterations of `n >>= 1` in `positive_length` / `floor_log2_real`). `Θ(width)` bit operations, extra space `Θ(1)`.
/// Counts are returned as `units << fractional_bits`.
/// @see grotto::make_exact_constant_lut
/// @see grotto::eval_ilog16
inline std::int64_t eval_dec_ceil(std::int64_t raw, unsigned fractional_bits)
{
if (fractional_bits >= 63)
throw std::invalid_argument("exact step: fractional width does not fit");
const std::int64_t one = std::int64_t{1} << fractional_bits;
std::int64_t q = raw / one;
const std::int64_t r = raw - q * one;
if (r > 0)
++q;
return q << fractional_bits;
}
/// @brief Digits of `floor(|x|)` in base 10, 8, or 64. Zero has length 1.
/// \complexity A constant number of shifts and a `countl` / `bit_width` style scan of the `Raw` width (at most 64 iterations of `n >>= 1` in `positive_length` / `floor_log2_real`). `Θ(width)` bit operations, extra space `Θ(1)`.
/// Counts are returned as `units << fractional_bits`.
/// @see grotto::make_exact_constant_lut
/// @see grotto::eval_ilog16
inline std::int64_t eval_value_length(std::int64_t raw, unsigned fractional_bits, int base)
{
if (base != 8 && base != 10 && base != 64)
throw std::invalid_argument("exact step: length base must be 8, 10, or 64");
const int units = exact_detail::positive_length(
exact_detail::abs_floor(raw, fractional_bits), base);
return detail::encode_units(units, fractional_bits);
}
/// \complexity A constant number of shifts and a `countl` / `bit_width` style scan of the `Raw` width (at most 64 iterations of `n >>= 1` in `positive_length` / `floor_log2_real`). `Θ(width)` bit operations, extra space `Θ(1)`.
/// Counts are returned as `units << fractional_bits`.
/// @see grotto::make_exact_constant_lut
/// @see grotto::eval_ilog16
inline std::int64_t eval_dec_width(std::int64_t raw, unsigned fractional_bits)
{
return eval_value_length(raw, fractional_bits, 10);
}
/// \complexity A constant number of shifts and a `countl` / `bit_width` style scan of the `Raw` width (at most 64 iterations of `n >>= 1` in `positive_length` / `floor_log2_real`). `Θ(width)` bit operations, extra space `Θ(1)`.
/// Counts are returned as `units << fractional_bits`.
/// @see grotto::make_exact_constant_lut
/// @see grotto::eval_ilog16
inline std::int64_t eval_oct_width(std::int64_t raw, unsigned fractional_bits)
{
return eval_value_length(raw, fractional_bits, 8);
}
/// \complexity A constant number of shifts and a `countl` / `bit_width` style scan of the `Raw` width (at most 64 iterations of `n >>= 1` in `positive_length` / `floor_log2_real`). `Θ(width)` bit operations, extra space `Θ(1)`.
/// Counts are returned as `units << fractional_bits`.
/// @see grotto::make_exact_constant_lut
/// @see grotto::eval_ilog16
inline std::int64_t eval_b64_width(std::int64_t raw, unsigned fractional_bits)
{
return eval_value_length(raw, fractional_bits, 64);
}
/// @brief True when `floor(|x|)` is a single decimal digit, including 0.
/// \complexity A constant number of shifts and a `countl` / `bit_width` style scan of the `Raw` width (at most 64 iterations of `n >>= 1` in `positive_length` / `floor_log2_real`). `Θ(width)` bit operations, extra space `Θ(1)`.
/// Counts are returned as `units << fractional_bits`.
/// @see grotto::make_exact_constant_lut
/// @see grotto::eval_ilog16
inline std::int64_t eval_has_single_digit(std::int64_t raw, unsigned fractional_bits)
{
const std::int64_t mag = exact_detail::abs_floor(raw, fractional_bits);
return detail::encode_units(mag <= 9 ? 1 : 0, fractional_bits);
}
namespace exact_detail
{
inline constexpr detail::u128 pi_over_180_64 = 321956420358983237ULL;
inline constexpr detail::u128 rad_to_deg_64 = (detail::u128{57} << 64) | 5456168980075999427ULL;
inline std::int64_t scale_angle(detail::u128 mag64, unsigned fractional_bits)
{
if (fractional_bits > 64)
throw std::invalid_argument("exact step: fractional width does not fit");
const unsigned shift = 64u - fractional_bits;
detail::u128 mag = mag64;
if (shift > 0)
{
mag += detail::u128{1} << (shift - 1);
mag >>= shift;
}
if (mag > static_cast<detail::u128>(INT64_MAX))
throw std::overflow_error("exact step: angle scale does not fit");
return static_cast<std::int64_t>(mag);
}
inline std::int64_t mul_angle(std::int64_t raw, std::int64_t factor, unsigned fractional_bits)
{
const __int128 prod = static_cast<__int128>(raw) * factor;
const bool neg = prod < 0;
auto mag = static_cast<detail::u128>(neg ? -prod : prod);
if (fractional_bits > 0)
{
mag += detail::u128{1} << (fractional_bits - 1);
mag >>= fractional_bits;
}
if (mag > static_cast<detail::u128>(INT64_MAX))
throw std::overflow_error("exact step: angle does not fit int64");
const auto out = static_cast<std::int64_t>(mag);
return neg ? -out : out;
}
} // namespace exact_detail
/// @brief Degrees to radians, `x * π / 180`.
/// \complexity A constant number of shifts and a `countl` / `bit_width` style scan of the `Raw` width (at most 64 iterations of `n >>= 1` in `positive_length` / `floor_log2_real`). `Θ(width)` bit operations, extra space `Θ(1)`.
/// Counts are returned as `units << fractional_bits`.
/// @see grotto::make_exact_constant_lut
/// @see grotto::eval_ilog16
inline std::int64_t eval_deg2rad(std::int64_t raw, unsigned fractional_bits)
{
return exact_detail::mul_angle(
raw, exact_detail::scale_angle(exact_detail::pi_over_180_64, fractional_bits),
fractional_bits);
}
/// @brief Radians to degrees, `x * 180 / π`.
/// \complexity A constant number of shifts and a `countl` / `bit_width` style scan of the `Raw` width (at most 64 iterations of `n >>= 1` in `positive_length` / `floor_log2_real`). `Θ(width)` bit operations, extra space `Θ(1)`.
/// Counts are returned as `units << fractional_bits`.
/// @see grotto::make_exact_constant_lut
/// @see grotto::eval_ilog16
inline std::int64_t eval_rad2deg(std::int64_t raw, unsigned fractional_bits)
{
return exact_detail::mul_angle(
raw, exact_detail::scale_angle(exact_detail::rad_to_deg_64, fractional_bits),
fractional_bits);
}
/// @brief Piecewise-constant LUT of a step whose value is an encoded integer.
/// @details The domain scan is the cut set. `Raw` wider than 16 bits is refused.
/// \complexity Scans every raw value from `numeric_limits<Raw>::min()` to `max()`. The header rejects `width > 16`, so the scan is `Θ(2^w)` with `w ≤ 16`.
/// The result stores one cut per change of `at`. Extra space is that cut vector.
/// @see grotto::eval_dec_floor
/// @see grotto::easy_lut
template <typename Raw, typename At>
easy_lut<Raw> make_exact_step_lut(unsigned fractional_bits, At && at)
{
(void)fractional_bits;
constexpr int width = detail::raw_width<Raw>();
if (width > 16)
throw std::invalid_argument("exact step lut: raw width must be at most 16");
using lim = std::numeric_limits<Raw>;
const auto minv = static_cast<std::int64_t>(lim::min());
const auto maxv = static_cast<std::int64_t>(lim::max());
std::vector<std::int64_t> cuts;
auto previous = at(minv);
for (std::int64_t raw = minv + 1; raw <= maxv; ++raw)
{
const auto value = at(raw);
if (value != previous)
{
cuts.push_back(raw);
previous = value;
}
if (raw == maxv)
break;
}
return detail::steps_from_cuts<Raw>(std::move(cuts), [=](std::int64_t raw) {
return detail::easy_poly{at(raw), 0, 0, 1};
});
}
} // namespace grotto
#endif // LIBDPF_INCLUDE_GROTTO_EXACT_STEPS_HPP__

View file

@ -39,7 +39,7 @@ namespace detail
/// @brief Integer value of an already-rounded finite double, as a 256-bit word.
/// Values that do not fit saturate to all-ones.
/// @param rounded the `rounded`
/// @param rounded already-rounded finite double
/// @return Integer value of an already-rounded finite double, as a 256-bit word
HEDLEY_NO_THROW
inline uint256_t uint256_from_rounded_double(double rounded) noexcept
@ -102,7 +102,7 @@ struct is_static_castable<To, From,
/// @brief Low `bits` of `wide`, saturated to all-ones when `wide` does not fit.
/// @tparam Raw underlying representation
/// @tparam Bits bits
/// @param wide the `wide`
/// @param wide wide integer before it is narrowed
/// @return Low `bits` of `wide`, saturated to all-ones when `wide` does not fit
template <typename Raw, std::size_t Bits>
HEDLEY_NO_THROW
@ -212,7 +212,7 @@ inline IntegralType rounded_double_to_integral(double rounded) noexcept
/// @tparam IntegralType underlying integral type
/// @tparam FractionalBits number of fractional bits
/// @tparam T value type
/// @param integer_value the `integer_value`
/// @param integer_value mathematical integer; stored as `integer_value << FractionalBits`
/// @return the returned `IntegralType`
template <typename IntegralType,
unsigned FractionalBits,
@ -240,7 +240,7 @@ inline constexpr bool is_signed_rep_v =
/// @brief Two's-complement negate via the unsigned width. Defined for the
/// most-negative value (wraps); signed `-x` would be UB there.
/// @tparam IntegralType underlying integral type
/// @param x the `x`
/// @param x the input value
/// @return Two's-complement negate via the unsigned width
template <typename IntegralType>
HEDLEY_ALWAYS_INLINE
@ -269,8 +269,8 @@ constexpr IntegralType raw_abs(IntegralType x) noexcept
/// @brief Remainder with the sign of `a` and magnitude `< |b|` (C++ `%` /
/// `std::fmod`). Zero divisor → 0; this type has no NaN.
/// @tparam IntegralType underlying integral type
/// @param a the `a`
/// @param b the `b`
/// @param a left-hand operand
/// @param b right-hand operand
/// @return Remainder with the sign of `a` and magnitude `< |b|` (C++ `%` / `std::fmod`)
template <typename IntegralType>
HEDLEY_ALWAYS_INLINE
@ -292,10 +292,20 @@ template <unsigned FractionalBits,
HEDLEY_NO_THROW
auto constexpr make_fixed_from_integral_type(IntegralType value) noexcept;
/// @tparam FractionalBits Number of fractional bits used in the fixed-point
/// @brief representation.
/// @tparam IntegralType The underlying integral type used for the fixed-point
/// representation.
/// @brief Fixed-point value with `FractionalBits` bits after the binary point.
/// @tparam FractionalBits Number of fractional bits. A DPF on this type has
/// depth equal to the backend width, not `FractionalBits`.
/// @tparam IntegralType Backend word. Defaults to `uint64_t`, so
/// `fixedpoint<16>` is a Q48.16 value.
/// @note `fixedpoint(3)` is the mathematical value 3, stored as
/// `3 << FractionalBits`. It is not a raw word. `from_raw` is bit-exact.
/// @note Leaf addition, subtraction, and multiplication act on that raw word
/// and do not shift the binary point. See `dpf::leaf_arithmetic`.
/// @note A signed backend flips the MSB when the value is a DPF input
/// (`dpf::utils::flip_msb_for_input`).
/// @see grotto::fixed_mul
/// @see [Input types](@ref input_types)
/// @see [Output types](@ref output_types)
template <unsigned FractionalBits,
typename IntegralType = GROTTO_FIXED_DEFAULT_INTEGRAL_REPRESENTATION>
struct fixedpoint
@ -341,7 +351,7 @@ public:
/// @brief Value c'tor
/// @details Initializes the fixed-point with the value determined by `desired`, using the <a href="https://en.cppreference.com/w/cpp/numeric/fenv/FE_round">current rounding mode</a> for the least-significant bit.
/// @param desired the `desired`
/// @param desired real value, rounded with the current rounding mode
HEDLEY_ALWAYS_INLINE
HEDLEY_NO_THROW
constexpr fixedpoint(double desired) noexcept // NOLINT (implicit c'tor)
@ -355,7 +365,7 @@ public:
/// Use `from_raw` for a bit-exact encoding.
/// @tparam T value type
/// @tparam T value type
/// @param integer_value the `integer_value`
/// @param integer_value mathematical integer; stored as `integer_value << FractionalBits`
template <typename T,
std::enable_if_t<
std::is_integral_v<T>
@ -370,6 +380,9 @@ public:
/// @brief Bit-exact construction from the backend integer encoding.
/// @param raw the underlying integer
/// @return Bit-exact construction from the backend integer encoding
/// \complexity `Θ(1)`. Copies the backend word. No shift and no rounding.
/// @note Bit-exact. Unlike `fixedpoint(3)`, this does not mean the mathematical value `raw`.
/// @see grotto::fixedpoint
HEDLEY_ALWAYS_INLINE
HEDLEY_NO_THROW
static constexpr fixedpoint from_raw(integral_type raw) noexcept
@ -401,7 +414,7 @@ public:
/// @brief Value assignment
/// @details Assigns the fixed-point with a value determined by `desired`, using the <a href="https://en.cppreference.com/w/cpp/numeric/fenv/FE_round">current rounding mode</a> for the least-significant bit..
/// @param desired the `desired`
/// @param desired real value, rounded with the current rounding mode
/// @return `*this`
HEDLEY_ALWAYS_INLINE
HEDLEY_NO_THROW
@ -557,6 +570,9 @@ public:
/// @details Computes the sum of two fixed-point numbers
/// @param rhs the right-hand operand
/// @return Binary addition operator
/// \complexity One backend-word operation. `Θ(1)` time and extra space in that word width.
/// @note Same-scale addition and subtraction add the raw words. That is the fixed-point sum; the binary point does not move.
/// @see grotto::fixed_mul
HEDLEY_ALWAYS_INLINE
HEDLEY_NO_THROW
HEDLEY_PURE
@ -579,6 +595,9 @@ public:
/// @brief Binary subtraction operator
/// @param rhs the right-hand operand
/// @return Binary subtraction operator
/// \complexity One backend-word operation. `Θ(1)` time and extra space in that word width.
/// @note Same-scale addition and subtraction add the raw words. That is the fixed-point sum; the binary point does not move.
/// @see grotto::fixed_mul
HEDLEY_ALWAYS_INLINE
HEDLEY_NO_THROW
HEDLEY_PURE
@ -602,6 +621,11 @@ public:
/// @tparam FractionalBits1 fractional bits1
/// @param rhs the right-hand operand
/// @return Binary multiplication operator
/// \complexity One product of the raw backend words. The result is `make_fixed_from_integral_type` at `FractionalBits + FractionalBits1` fractional bits.
/// `Θ(1)` in the backend width (the 64-bit or 128-bit branch in `multiplies` is the rescaling multiply; this operator does not loop).
/// @note This is not leaf multiply. Leaf multiply stays on the raw word and does not change `FractionalBits`.
/// @see grotto::fixed_mul
/// @see grotto::fixedpoint
template <unsigned FractionalBits1>
HEDLEY_ALWAYS_INLINE
HEDLEY_NO_THROW
@ -751,7 +775,7 @@ public:
// struct make_fixed_from_integral_type_tag {};
/// @brief Determine if a floating-point is within range
/// @param d the `d`
/// @param d real value
/// @return Determine if a floating-point is within range
HEDLEY_ALWAYS_INLINE
HEDLEY_NO_THROW
@ -790,7 +814,7 @@ public:
/// @tparam IntegralType underlying integral type
/// @tparam Mask mask
/// @param mask the bit mask
/// @param x the `x`
/// @param x the input value
/// @return Bit test with the mask on the left
template <unsigned FractionalBits,
typename IntegralType,
@ -829,6 +853,14 @@ operator>>(std::basic_istream<CharT, Traits> & is,
return is;
}
/// @brief Bit-exact fixed-point from a backend word.
/// @tparam FractionalBits number of fractional bits
/// @tparam IntegralType backend word type
/// @param value the raw encoding, not the mathematical integer
/// @return `fixedpoint::from_raw(value)`
/// @note This does not shift by `FractionalBits`. `make_fixed_from_integral_type(1)` is the raw word 1, whereas `fixedpoint(1)` is the value 1.
/// @see grotto::fixedpoint::from_raw
/// \complexity `Θ(1)`. One `from_raw`.
template <unsigned FractionalBits,
typename IntegralType>
HEDLEY_NO_THROW
@ -836,6 +868,9 @@ auto constexpr make_fixed_from_integral_type(IntegralType value) noexcept
{
return fixedpoint<FractionalBits, IntegralType>::from_raw(value);
}
/// \complexity One scale by `2^{FractionalBits}` (`ldexp`) and `nearbyint` under the current rounding mode. `Θ(1)`.
/// @note This is the real value, same as `fixedpoint(double)`. It is not `from_raw`.
/// @see grotto::fixedpoint
template <unsigned FractionalBits,
typename IntegralType = GROTTO_FIXED_DEFAULT_INTEGRAL_REPRESENTATION>
@ -845,6 +880,9 @@ static constexpr auto make_fixed(double d)
{
return fixedpoint<FractionalBits, IntegralType>(d);
}
/// \complexity One scale by `2^{FractionalBits}` (`ldexp`) and `nearbyint` under the current rounding mode. `Θ(1)`.
/// @note This is the real value, same as `fixedpoint(double)`. It is not `from_raw`.
/// @see grotto::fixedpoint
template <typename FixedType>
HEDLEY_ALWAYS_INLINE
@ -857,10 +895,12 @@ static constexpr auto make_fixed(double d)
/// @brief Creates a fixed-point number from a double with bounds checking.
/// @tparam FractionalBits number of fractional bits
/// @tparam IntegralType underlying integral type
/// @param d the `d`
/// @param d real value
/// @return Creates a fixed-point number from a double with bounds checking
/// @throws std::range_error if the input double is outside the representable
/// range of the fixed-point number.
/// \complexity Two comparisons against `numeric_limits`, then one `make_fixed`. `Θ(1)`.
/// @see grotto::make_fixed
template <unsigned FractionalBits,
typename IntegralType = GROTTO_FIXED_DEFAULT_INTEGRAL_REPRESENTATION>
HEDLEY_ALWAYS_INLINE
@ -881,6 +921,8 @@ static auto make_fixed_safe(double d)
return make_fixed<FractionalBits, IntegralType>(d);
}
/// \complexity One left or right shift of the raw word by the difference of the fractional widths. No rounding step. `Θ(1)`.
/// @see grotto::fixedpoint
template <unsigned ToFractionalBits,
unsigned FromFractionalBits,
@ -906,6 +948,8 @@ static constexpr auto precision_of(fixedpoint<FractionalBits, IntegralType>) noe
{
return FractionalBits;
}
/// \complexity Adds one to the raw word (one ULP). `Θ(1)`.
/// @see grotto::fixedpoint
template <unsigned FractionalBits,
typename IntegralType>
@ -916,6 +960,8 @@ constexpr auto nextafter(fixedpoint<FractionalBits, IntegralType> f) noexcept
{
return make_fixed_from_integral_type<FractionalBits, IntegralType>(f.integral_representation()+1);
}
/// \complexity Subtracts one from the raw word (one ULP). `Θ(1)`.
/// @see grotto::fixedpoint
template <unsigned FractionalBits,
typename IntegralType>
@ -926,6 +972,8 @@ constexpr auto nextbefore(fixedpoint<FractionalBits, IntegralType> f) noexcept
{
return make_fixed_from_integral_type<FractionalBits, IntegralType>(f.integral_representation()-1);
}
/// \complexity Two's-complement absolute value of the raw word via `raw_neg` when the backend is signed. `Θ(1)`.
/// @see grotto::fixedpoint
template <unsigned FractionalBits,
typename IntegralType>
@ -946,6 +994,8 @@ constexpr auto fabs(fixedpoint<FractionalBits, IntegralType> v) noexcept
}
return v;
}
/// \complexity `raw_fmod` on the backend words: remainder with the sign of the dividend. `Θ(1)` word operations. A zero modulus returns 0.
/// @see grotto::fixedpoint
template <unsigned FractionalBits,
typename IntegralType>
@ -959,6 +1009,8 @@ constexpr auto fmod(fixedpoint<FractionalBits, IntegralType> v,
detail::raw_fmod(v.integral_representation(),
modulus.integral_representation()));
}
/// \complexity `raw_fmod` on the backend words: remainder with the sign of the dividend. `Θ(1)` word operations. A zero modulus returns 0.
/// @see grotto::fixedpoint
template <unsigned FractionalBits,
typename IntegralType>
@ -970,6 +1022,8 @@ constexpr auto fmod(fixedpoint<FractionalBits, IntegralType> v,
{
return fmod(v, make_fixed<FractionalBits, IntegralType>(modulus));
}
/// \complexity `raw_fmod` on the backend words: remainder with the sign of the dividend. `Θ(1)` word operations. A zero modulus returns 0.
/// @see grotto::fixedpoint
template <unsigned FractionalBits,
typename IntegralType,
@ -996,6 +1050,12 @@ enum fixed_cast_policy
use_arg_sum //< for multiplies only
};
/// @brief Apply `BinaryOperator` after casting both fixed-point arguments to one fractional width.
/// @tparam BinaryOperator operation applied after the cast
/// @tparam Mode which operand's fractional width wins (`use_max_arg` by default)
/// @see grotto::precision_cast
/// @see grotto::multiplies
/// \complexity One `precision_cast` (a raw shift) when the fractional widths differ, then one call of `BinaryOperator`. `Θ(1)`.
template <typename BinaryOperator,
fixed_cast_policy Mode = use_max_arg>
struct binary_operator_precast_wrapper
@ -1042,6 +1102,12 @@ struct binary_operator_precast_wrapper
}
};
/// @brief Fixed-point product that chooses how many fractional bits to keep.
/// @tparam Mode which operand's fractional width the product uses; `use_arg_sum` keeps both
/// @see grotto::fixed_mul
/// @see grotto::fixedpoint
/// @note The raw product is shifted so the binary point matches `Mode`. That is a rescale. Leaf multiply does not do this.
/// \complexity One backend multiply (64-bit or 128-bit branch) and one shift. `Θ(1)` time and extra space.
template <fixed_cast_policy Mode = use_arg_sum>
struct multiplies
{
@ -1123,6 +1189,12 @@ struct multiplies
}
};
/// @name Comparisons with double on the left
/// @brief `double` compared with `fixedpoint` by reversing the member operator.
/// \complexity One call to the matching member comparison. `Θ(1)`.
/// @see grotto::fixedpoint
/// @{
template <unsigned FractionalBits,
typename IntegralType>
HEDLEY_ALWAYS_INLINE
@ -1173,6 +1245,13 @@ constexpr bool operator>=(double lhs, fixedpoint<FractionalBits, IntegralType> r
return (rhs <= lhs);
}
/// @}
/// @brief Horner polynomial in `fixedpoint` coefficients, constant term at index 0.
/// @tparam FixedPointType coefficient type
/// @tparam Degree number of coefficients (degree is one less when `Degree > 0`)
/// @see grotto::multiplies
/// \complexity `operator()` walks the coefficients once: `Θ(Degree)` fixed-point multiplies and adds. Extra space `Θ(1)`.
template <typename FixedPointType,
std::size_t Degree>
struct fixedpoint_polynomial : public std::array<FixedPointType, Degree>
@ -1203,6 +1282,20 @@ static constexpr auto evaluate(const fixedpoint_polynomial<FixedPointType, Degre
namespace fixedpoint_literals
{
/// @name Fixed-point literals
/// @brief `_fixed0` through `_fixed64` in `grotto::fixedpoint_literals`.
///
/// `1.5_fixed16` is `fixedpoint<16>` holding the value 1.5, via
/// `make_fixed<16>`. It is not a raw word. The DPF depth of the
/// result is the backend width (64 bits for the default), not `N`.
/// @param val the `long double` literal
/// @return `fixedpoint<N>` for the suffix `_fixedN`
/// @see grotto::make_fixed
/// @see grotto::fixedpoint
/// \complexity Each literal is one call to `make_fixed` (scale by
/// `2^N` and round). `Θ(1)` time and extra space. No protocol.
/// @{
constexpr auto operator "" _fixed0(long double val)
{
return grotto::make_fixed<0>(val);
@ -1497,6 +1590,8 @@ constexpr auto operator "" _fixed64(long double val)
return grotto::make_fixed<64>(val);
}
/// @}
} // namespace grotto::fixedpoint_literals
} // namespace grotto
@ -1597,6 +1692,9 @@ struct make_from_integral_value<grotto::fixedpoint<FractionalBits, IntegralType>
static_cast<IntegralType>(val));
}
};
/// \complexity One XOR of `msb_of_v<IntegralType>` into the raw word when `uses_signed_msb_v` is true. `Θ(1)`.
/// @note Signed backends flip that MSB on the way into a DPF input. Unsigned backends do not.
/// @see grotto::fixedpoint
template <unsigned FractionalBits,
typename IntegralType>
@ -1623,6 +1721,11 @@ struct flip_msb_for_input<grotto::fixedpoint<FractionalBits, IntegralType>>
namespace dpf::leaf_arithmetic
{
/// \complexity One SIMD 128-bit add of the raw backend words (`integral_representation()` for multiply).
/// `Θ(1)` time and extra space.
/// @note Does not shift by `FractionalBits`. The binary point of the leaf word stays where it was.
/// @see grotto::fixedpoint
/// @see grotto::fixed_mul
HEDLEY_PRAGMA(GCC diagnostic push)
HEDLEY_PRAGMA(GCC diagnostic ignored "-Wignored-attributes")
@ -1634,6 +1737,11 @@ struct add_t<grotto::fixedpoint<FractionalBits, IntegralType>, simde__m128i>
return add_t<IntegralType, simde__m128i>{}(a, b);
}
};
/// \complexity One SIMD 256-bit add of the raw backend words (`integral_representation()` for multiply).
/// `Θ(1)` time and extra space.
/// @note Does not shift by `FractionalBits`. The binary point of the leaf word stays where it was.
/// @see grotto::fixedpoint
/// @see grotto::fixed_mul
template <unsigned FractionalBits, typename IntegralType>
struct add_t<grotto::fixedpoint<FractionalBits, IntegralType>, simde__m256i>
@ -1643,6 +1751,11 @@ struct add_t<grotto::fixedpoint<FractionalBits, IntegralType>, simde__m256i>
return add_t<IntegralType, simde__m256i>{}(a, b);
}
};
/// \complexity One SIMD 128-bit subtract of the raw backend words (`integral_representation()` for multiply).
/// `Θ(1)` time and extra space.
/// @note Does not shift by `FractionalBits`. The binary point of the leaf word stays where it was.
/// @see grotto::fixedpoint
/// @see grotto::fixed_mul
template <unsigned FractionalBits, typename IntegralType>
struct subtract_t<grotto::fixedpoint<FractionalBits, IntegralType>, simde__m128i>
@ -1652,6 +1765,11 @@ struct subtract_t<grotto::fixedpoint<FractionalBits, IntegralType>, simde__m128i
return subtract_t<IntegralType, simde__m128i>{}(a, b);
}
};
/// \complexity One SIMD 256-bit subtract of the raw backend words (`integral_representation()` for multiply).
/// `Θ(1)` time and extra space.
/// @note Does not shift by `FractionalBits`. The binary point of the leaf word stays where it was.
/// @see grotto::fixedpoint
/// @see grotto::fixed_mul
template <unsigned FractionalBits, typename IntegralType>
struct subtract_t<grotto::fixedpoint<FractionalBits, IntegralType>, simde__m256i>
@ -1661,6 +1779,11 @@ struct subtract_t<grotto::fixedpoint<FractionalBits, IntegralType>, simde__m256i
return subtract_t<IntegralType, simde__m256i>{}(a, b);
}
};
/// \complexity One SIMD 128-bit multiply of the raw backend words (`integral_representation()` for multiply).
/// `Θ(1)` time and extra space.
/// @note Does not shift by `FractionalBits`. The binary point of the leaf word stays where it was.
/// @see grotto::fixedpoint
/// @see grotto::fixed_mul
template <unsigned FractionalBits, typename IntegralType>
struct multiply_t<grotto::fixedpoint<FractionalBits, IntegralType>, simde__m128i>
@ -1671,6 +1794,11 @@ struct multiply_t<grotto::fixedpoint<FractionalBits, IntegralType>, simde__m128i
return multiply_t<IntegralType, simde__m128i>{}(a, b.integral_representation());
}
};
/// \complexity One SIMD 256-bit multiply of the raw backend words (`integral_representation()` for multiply).
/// `Θ(1)` time and extra space.
/// @note Does not shift by `FractionalBits`. The binary point of the leaf word stays where it was.
/// @see grotto::fixedpoint
/// @see grotto::fixed_mul
template <unsigned FractionalBits, typename IntegralType>
struct multiply_t<grotto::fixedpoint<FractionalBits, IntegralType>, simde__m256i>

View file

@ -0,0 +1,617 @@
/// @file grotto/fixedpoint_beaver.hpp
/// @brief Additive-share evaluation of `fixed_mul`.
/// @details One Beaver triple in `Z/2^{multiply_bits}Z` is the product. Every
/// other step is linear, or a masked comparison that lifts a narrower
/// share or drops low bits:
///
/// - bits above `multiply_bits` are discarded locally;
/// - a narrower operand (and a product narrower than `modulus_bits`)
/// is lifted by `eta + r - w·2^{src}`, with `w` the carry of the
/// secret mask, and a signed lift then replicates the sign;
/// - `align_shift > 0` is a truncate-and-reduce of that modulus;
/// - `align_shift < 0` is a local left shift.
///
/// The opened window matches `fixed_mul`, including the final sign
/// extension into the storage word. Each lift extends by at most 64
/// bits and starts from at most 64 bits, so the carry bit stays a
/// `uint64_t` comparison and scaling it is homomorphic in the
/// destination ring. A right shift discards at most 64 bits. The
/// modulus and the multiply ring are at most 128 bits.
///
/// `prep` is a dealer record (both key halves, both mask shares),
/// same as `carry_key_pair`. It is independent of the data and is
/// reused. `sample_fixed_mul_beaver_triple` is one product and is
/// fresh each time. `eval_fixed_mul_beaver` is one party: `exchange`
/// sends one `uint64_t` and returns the peer's word, in the same
/// order on both sides.
/// @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_GROTTO_FIXEDPOINT_BEAVER_HPP__
#define LIBDPF_INCLUDE_GROTTO_FIXEDPOINT_BEAVER_HPP__
#include <cstddef>
#include <cstdint>
#include <optional>
#include <stdexcept>
#include <utility>
#include "hedley/hedley.h"
#include "dpf.hpp"
#include "grotto/fixedpoint_mul.hpp"
namespace grotto
{
namespace fixed_mul_beaver_detail
{
using u128 = simde_uint128;
HEDLEY_NO_THROW
constexpr u128 bit_mask(unsigned bits) noexcept
{
if (bits == 0u)
return 0;
if (bits >= 128u)
return ~u128{0};
return (u128{1} << bits) - 1;
}
HEDLEY_NO_THROW
constexpr unsigned u64_limbs(unsigned bits) noexcept
{
return bits > 64u ? 2u : 1u;
}
HEDLEY_WARN_UNUSED_RESULT
inline u128 draw_mod(unsigned bits)
{
if (bits == 0u)
return 0;
if (bits <= 64u)
{
const std::uint64_t m = static_cast<std::uint64_t>(bit_mask(bits));
return dpf::uniform_sample<std::uint64_t>() & m;
}
return dpf::uniform_sample<u128>() & bit_mask(bits);
}
/// @brief Low 128 bits of `(uint64)m * factor`, then reduced mod `2^dest`.
HEDLEY_NO_THROW
inline u128 mul_u64(std::uint64_t m, u128 factor, unsigned dest) noexcept
{
const std::uint64_t f0 = static_cast<std::uint64_t>(factor);
const std::uint64_t f1 = static_cast<std::uint64_t>(factor >> 64);
const u128 p0 = u128{m} * f0;
const u128 p1 = u128{m} * f1;
return (p0 + (p1 << 64)) & bit_mask(dest);
}
HEDLEY_NO_THROW
inline u128 mul_mod(u128 a, u128 b, unsigned bits) noexcept
{
a &= bit_mask(bits);
b &= bit_mask(bits);
if (bits <= 64u)
{
return (u128{static_cast<std::uint64_t>(a)}
* static_cast<std::uint64_t>(b)) & bit_mask(bits);
}
const std::uint64_t a0 = static_cast<std::uint64_t>(a);
const std::uint64_t a1 = static_cast<std::uint64_t>(a >> 64);
const std::uint64_t b0 = static_cast<std::uint64_t>(b);
const std::uint64_t b1 = static_cast<std::uint64_t>(b >> 64);
const u128 p00 = u128{a0} * b0;
const u128 mid = (p00 >> 64)
+ static_cast<std::uint64_t>(u128{a0} * b1)
+ static_cast<std::uint64_t>(u128{a1} * b0);
return ((u128)static_cast<std::uint64_t>(p00) | (mid << 64)) & bit_mask(bits);
}
HEDLEY_NO_THROW
inline u128 add_mod(u128 a, u128 b, unsigned bits) noexcept
{
return (a + b) & bit_mask(bits);
}
HEDLEY_NO_THROW
inline u128 sub_mod(u128 a, u128 b, unsigned bits) noexcept
{
return (a - b) & bit_mask(bits);
}
template <typename T>
HEDLEY_NO_THROW
u128 share_bits(const T & value) noexcept
{
std::uint64_t raw[4] = {};
detail::store_raw_limbs(value, raw);
return u128{raw[0]} | (u128{raw[1]} << 64);
}
using cmp_pair = decltype(dpf::make_dpf(std::uint64_t{0},
dpf::lt(std::uint64_t{1})));
struct bit_key
{
cmp_pair keys;
};
HEDLEY_WARN_UNUSED_RESULT
inline bit_key make_bit_key(std::uint64_t alpha)
{
return bit_key{dpf::make_dpf(alpha, dpf::lt(std::uint64_t{1}))};
}
HEDLEY_WARN_UNUSED_RESULT
inline std::uint64_t eval_bit(const bit_key & key, std::size_t party,
std::uint64_t query)
{
if (party == 0u)
return dpf::eval_point(dpf::cmp, key.keys.first, query).raw();
return dpf::eval_point(dpf::cmp, key.keys.second, query).raw();
}
/// @brief Public sum of the two parties' words, mod `2^bits`.
template <typename Exchange>
HEDLEY_WARN_UNUSED_RESULT
u128 open_sum(u128 mine, unsigned bits, Exchange & exchange)
{
const std::uint64_t low = exchange(static_cast<std::uint64_t>(mine));
u128 peer = low;
if (bits > 64u)
{
const std::uint64_t hi = exchange(static_cast<std::uint64_t>(mine >> 64));
peer |= u128{hi} << 64;
}
return (mine + peer) & bit_mask(bits);
}
struct lift_keys
{
bool live = false;
bool sign = false;
unsigned src = 0;
unsigned dest = 0;
/// @brief Secret mask mod `2^src`. Shares sum to `r` in `Z/2^128`.
u128 r = 0;
u128 r_share[2]{};
/// @brief `lt` at `r`. Eval at `T-1` is a share of `1{r >= T}`.
std::optional<bit_key> wrap{};
};
HEDLEY_WARN_UNUSED_RESULT
inline lift_keys make_lift(unsigned src, unsigned dest, bool sign)
{
lift_keys k;
k.live = true;
k.sign = sign;
k.src = src;
k.dest = dest;
k.r = draw_mod(src);
k.r_share[0] = dpf::uniform_sample<u128>();
k.r_share[1] = k.r - k.r_share[0];
k.wrap = make_bit_key(static_cast<std::uint64_t>(k.r));
return k;
}
struct shift_keys
{
bool live = false;
unsigned n = 0;
unsigned s = 0;
u128 rin = 0;
u128 rin_share[2]{};
u128 rout_share[2]{};
std::optional<bit_key> low{};
};
HEDLEY_WARN_UNUSED_RESULT
inline shift_keys make_shift(unsigned n, unsigned s)
{
shift_keys k;
k.live = true;
k.n = n;
k.s = s;
k.rin = draw_mod(n);
k.rin_share[0] = draw_mod(n);
k.rin_share[1] = (k.rin - k.rin_share[0]) & bit_mask(n);
const u128 neg = (u128{0} - k.rin) & bit_mask(n);
const std::uint64_t alpha = static_cast<std::uint64_t>(neg & bit_mask(s));
k.low = make_bit_key(alpha);
const unsigned out = n - s;
const u128 y_hi = neg >> s;
k.rout_share[0] = draw_mod(out);
k.rout_share[1] = (y_hi - k.rout_share[0]) & bit_mask(out);
return k;
}
/// @brief Share of `1{r >= T}` for `r` in `[0, 2^src)`. `T == 0` is the public 1.
HEDLEY_WARN_UNUSED_RESULT
inline std::uint64_t ge_mask(const lift_keys & keys, std::size_t party,
u128 threshold)
{
if (threshold == 0)
return party == 1u ? std::uint64_t{1} : 0u;
if (threshold > bit_mask(keys.src))
return 0u;
return eval_bit(*keys.wrap, party,
static_cast<std::uint64_t>(threshold) - 1u);
}
template <typename Exchange>
HEDLEY_WARN_UNUSED_RESULT
u128 apply_lift(u128 share, const lift_keys & keys, std::size_t party,
Exchange & exchange)
{
const std::uint64_t src_mask = static_cast<std::uint64_t>(bit_mask(keys.src));
const std::uint64_t xs = static_cast<std::uint64_t>(share) & src_mask;
const std::uint64_t rs = static_cast<std::uint64_t>(keys.r_share[party]) & src_mask;
const std::uint64_t delta = (xs - rs) & src_mask;
const std::uint64_t peer = exchange(delta);
const std::uint64_t eta = (delta + peer) & src_mask;
// `1{r >= 2^src - eta}` is the carry `r + eta >= 2^src`.
const u128 mod = u128{1} << keys.src;
const std::uint64_t w = ge_mask(keys, party, mod - eta);
u128 y = keys.r_share[party];
y -= mul_u64(w, mod, keys.dest);
if (party == 0u)
y += eta;
if (keys.sign)
{
const u128 half = u128{1} << (keys.src - 1u);
std::uint64_t msb = 0;
if (u128{eta} < half)
msb = ge_mask(keys, party, half - eta) - w;
else
msb = (party == 1u ? std::uint64_t{1} : 0u) - w
+ ge_mask(keys, party, mod + half - eta);
const u128 high = bit_mask(keys.dest) & ~bit_mask(keys.src);
y += mul_u64(msb, high, keys.dest);
}
return y;
}
template <typename Exchange>
HEDLEY_WARN_UNUSED_RESULT
u128 apply_shift(u128 share, const shift_keys & keys, std::size_t party,
Exchange & exchange)
{
const u128 opened = open_sum(share + keys.rin_share[party], keys.n, exchange);
const u128 xs = opened & bit_mask(keys.s);
const u128 query128 = (u128{1} << keys.s) - xs - 1;
const std::uint64_t t = eval_bit(*keys.low, party,
static_cast<std::uint64_t>(query128));
// `t` sums to the carry in `Z/2^64`. The output modulus is at most 64
// bits for every window this header accepts, so that extra multiple of
// `2^64` lands outside the modulus.
u128 acc = keys.rout_share[party] + t;
if (party == 1u)
acc += opened >> keys.s;
return acc;
}
} // namespace fixed_mul_beaver_detail
/// @brief Which steps of one `fixed_mul` window are interactive.
/// @tparam IntegerBits integer bits kept in the product, including the sign
/// @tparam FractionalBits fraction bits kept in the product
/// @tparam LhsFractionalBits fractional bits of the left operand
/// @tparam LhsIntegral integral type of the left operand
/// @tparam RhsFractionalBits fractional bits of the right operand
/// @tparam RhsIntegral integral type of the right operand
template <unsigned IntegerBits,
unsigned FractionalBits,
unsigned LhsFractionalBits,
typename LhsIntegral,
unsigned RhsFractionalBits,
typename RhsIntegral>
struct fixed_mul_beaver_shape
{
using plan = fixed_mul_plan<IntegerBits, FractionalBits,
LhsFractionalBits, LhsIntegral, RhsFractionalBits, RhsIntegral>;
using result_integral = typename plan::integral_type;
static constexpr unsigned storage_bits =
static_cast<unsigned>(dpf::utils::bitlength_of_v<result_integral>);
static constexpr bool active = plan::modulus_bits > 0u
&& plan::multiply_bits > 0u;
static constexpr bool lhs_lift = active
&& plan::multiply_bits > plan::lhs_width;
static constexpr bool rhs_lift = active
&& plan::multiply_bits > plan::rhs_width;
static constexpr bool product_lift = active
&& plan::modulus_bits > plan::multiply_bits;
static constexpr bool shift_right = active && plan::align_shift > 0;
static constexpr bool shift_left = active && plan::align_shift < 0;
static constexpr bool result_lift = active
&& storage_bits > plan::out_bits;
static constexpr bool lift_ok(bool lift, unsigned src, unsigned dest) noexcept
{
if (!lift)
return true;
return src >= 1u && src <= 64u && dest > src && dest <= 128u
&& (dest - src) <= 64u;
}
static constexpr bool fits = !active
|| (plan::multiply_bits <= 128u
&& plan::modulus_bits <= 128u
&& storage_bits <= 128u
&& lift_ok(lhs_lift, plan::lhs_width, plan::multiply_bits)
&& lift_ok(rhs_lift, plan::rhs_width, plan::multiply_bits)
&& lift_ok(product_lift, plan::multiply_bits, plan::modulus_bits)
&& lift_ok(result_lift, plan::out_bits, storage_bits)
&& (!shift_right || (static_cast<unsigned>(plan::align_shift) <= 64u
&& plan::out_bits <= 64u
&& plan::modulus_bits <= 128u
&& plan::modulus_bits > static_cast<unsigned>(plan::align_shift))));
/// @brief `uint64_t` words exchanged by one product. Zero when the window is empty.
static constexpr unsigned messages = !active ? 0u
: (lhs_lift ? 1u : 0u)
+ (rhs_lift ? 1u : 0u)
+ 2u * fixed_mul_beaver_detail::u64_limbs(plan::multiply_bits)
+ (product_lift ? 1u : 0u)
+ (shift_right
? fixed_mul_beaver_detail::u64_limbs(plan::modulus_bits)
: 0u)
+ (result_lift ? 1u : 0u);
};
/// @brief One Beaver triple in `Z/2^{multiply_bits}Z`.
struct fixed_mul_beaver_triple
{
unsigned multiply_bits = 0;
fixed_mul_beaver_detail::u128 a[2]{};
fixed_mul_beaver_detail::u128 b[2]{};
fixed_mul_beaver_detail::u128 ab[2]{};
};
/// @brief Dealer masks and comparison keys for one window. Reused across products.
template <unsigned IntegerBits,
unsigned FractionalBits,
unsigned LhsFractionalBits,
typename LhsIntegral,
unsigned RhsFractionalBits,
typename RhsIntegral>
struct fixed_mul_beaver_prep
{
using shape = fixed_mul_beaver_shape<IntegerBits, FractionalBits,
LhsFractionalBits, LhsIntegral, RhsFractionalBits, RhsIntegral>;
fixed_mul_beaver_detail::lift_keys lhs{};
fixed_mul_beaver_detail::lift_keys rhs{};
fixed_mul_beaver_detail::lift_keys product{};
fixed_mul_beaver_detail::shift_keys shift{};
fixed_mul_beaver_detail::lift_keys result{};
};
/// @brief Pack a `mod2k` beaver pair into the fixed-point triple.
inline fixed_mul_beaver_triple pack_mod2k_beaver(
unsigned multiply_bits, const dpf::beavers::beaver2<dpf::mod2k> & triple)
{
fixed_mul_beaver_triple t;
t.multiply_bits = multiply_bits;
t.a[0] = triple.a.p0.raw;
t.a[1] = triple.a.p1.raw;
t.b[0] = triple.b.p0.raw;
t.b[1] = triple.b.p1.raw;
t.ab[0] = triple.ab.p0.raw;
t.ab[1] = triple.ab.p1.raw;
return t;
}
/// @brief Sample one product triple from `sample_beaver2` in `Z/2^w Z`.
/// @param multiply_bits ring width, `0` or `1 .. 128`
HEDLEY_WARN_UNUSED_RESULT
inline fixed_mul_beaver_triple sample_fixed_mul_beaver_triple(unsigned multiply_bits)
{
if (multiply_bits == 0u)
return {};
if (multiply_bits > 128u)
throw std::invalid_argument("fixed_mul beaver triple exceeds 128 bits");
dpf::beavers::mod2k_width_scope width(multiply_bits);
return pack_mod2k_beaver(multiply_bits,
dpf::beavers::sample_beaver2<dpf::mod2k>());
}
/// @brief Same triple from copy `index` of a `mod2k` oracle.
template <typename PRG = dpf::prg::aes128>
HEDLEY_WARN_UNUSED_RESULT
inline fixed_mul_beaver_triple sample_fixed_mul_beaver_triple(unsigned multiply_bits,
const dpf::beavers::oracle<dpf::mod2k, PRG> & src, std::uint64_t index = 0)
{
if (multiply_bits == 0u)
return {};
if (multiply_bits > 128u)
throw std::invalid_argument("fixed_mul beaver triple exceeds 128 bits");
dpf::beavers::mod2k_width_scope width(multiply_bits);
return pack_mod2k_beaver(multiply_bits,
dpf::beavers::sample_beaver2(src, index));
}
/// @brief Build the reusable comparisons for this window.
/// \complexity One `dpf::make_dpf` per live lift (operand, product, result)
/// and one for a right shift. A signed lift reuses that key at a second
/// query. Each comparison is a `uint64_t` domain. No messages.
/// \rounds No party interaction.
/// \communication None.
/// \preprocessing Those comparison keys and the mask shares.
template <unsigned IntegerBits,
unsigned FractionalBits,
unsigned LhsFractionalBits,
typename LhsIntegral,
unsigned RhsFractionalBits,
typename RhsIntegral>
HEDLEY_WARN_UNUSED_RESULT
fixed_mul_beaver_prep<IntegerBits, FractionalBits, LhsFractionalBits, LhsIntegral,
RhsFractionalBits, RhsIntegral>
make_fixed_mul_beaver_prep()
{
using shape = fixed_mul_beaver_shape<IntegerBits, FractionalBits,
LhsFractionalBits, LhsIntegral, RhsFractionalBits, RhsIntegral>;
using prep = fixed_mul_beaver_prep<IntegerBits, FractionalBits,
LhsFractionalBits, LhsIntegral, RhsFractionalBits, RhsIntegral>;
static_assert(shape::fits,
"fixed_mul beaver: multiply and modulus must be at most 128 bits, "
"each lift must start from at most 64 bits and extend by at most 64, "
"and a right shift must discard at most 64 bits");
prep out;
if constexpr (!shape::active)
return out;
using plan = typename shape::plan;
if constexpr (shape::lhs_lift)
{
out.lhs = fixed_mul_beaver_detail::make_lift(plan::lhs_width,
plan::multiply_bits, plan::lhs_signed);
}
if constexpr (shape::rhs_lift)
{
out.rhs = fixed_mul_beaver_detail::make_lift(plan::rhs_width,
plan::multiply_bits, plan::rhs_signed);
}
if constexpr (shape::product_lift)
{
out.product = fixed_mul_beaver_detail::make_lift(plan::multiply_bits,
plan::modulus_bits, plan::operands_signed);
}
if constexpr (shape::shift_right)
{
out.shift = fixed_mul_beaver_detail::make_shift(plan::modulus_bits,
static_cast<unsigned>(plan::align_shift));
}
if constexpr (shape::result_lift)
{
out.result = fixed_mul_beaver_detail::make_lift(plan::out_bits,
shape::storage_bits, plan::result_is_signed);
}
return out;
}
/// @brief One party's share of `fixed_mul<IntegerBits, FractionalBits>(lhs, rhs)`.
/// @details `lhs_share` and `rhs_share` are additive shares of each operand's
/// raw integral word, in that word's own ring. The return value is
/// this party's share of the product's raw integral word. `exchange(mine)`
/// returns the peer's matching `uint64_t`. Both parties must call it
/// the same number of times (`fixed_mul_beaver_shape::messages`).
/// @tparam Exchange callable `std::uint64_t(std::uint64_t)`
/// @param prep reusable dealer material from `make_fixed_mul_beaver_prep`
/// @param triple fresh Beaver triple in the multiply ring
/// @param party `0` or `1`
/// @param lhs_share this party's share of the left raw word
/// @param rhs_share this party's share of the right raw word
/// @param exchange peer exchange for one `uint64_t`
/// @return this party's share of the product, as a fixed-point word
/// \complexity The Beaver product is `O(1)` 128-bit arithmetic. Each live
/// lift or shift is one to three `eval_point` calls on a `uint64_t` DCF.
/// \rounds One round if the caller pipelines every `exchange`; the callback
/// itself is one word at a time. `messages` words in total.
/// \communication `fixed_mul_beaver_shape::messages` words of `uint64_t`.
/// \preprocessing `prep` and one `triple`.
template <unsigned IntegerBits,
unsigned FractionalBits,
unsigned LhsFractionalBits,
typename LhsIntegral,
unsigned RhsFractionalBits,
typename RhsIntegral,
typename Exchange>
HEDLEY_WARN_UNUSED_RESULT
auto eval_fixed_mul_beaver(
const fixed_mul_beaver_prep<IntegerBits, FractionalBits, LhsFractionalBits,
LhsIntegral, RhsFractionalBits, RhsIntegral> & prep,
const fixed_mul_beaver_triple & triple,
std::size_t party,
LhsIntegral lhs_share,
RhsIntegral rhs_share,
Exchange && exchange)
-> typename fixed_mul_plan<IntegerBits, FractionalBits, LhsFractionalBits,
LhsIntegral, RhsFractionalBits, RhsIntegral>::result_type
{
using shape = fixed_mul_beaver_shape<IntegerBits, FractionalBits,
LhsFractionalBits, LhsIntegral, RhsFractionalBits, RhsIntegral>;
using plan = typename shape::plan;
using integral = typename plan::integral_type;
static_assert(shape::fits,
"fixed_mul beaver: multiply and modulus must be at most 128 bits, "
"each lift must start from at most 64 bits and extend by at most 64, "
"and a right shift must discard at most 64 bits");
if (party > 1u)
throw std::invalid_argument("fixed_mul beaver party is 0 or 1");
using namespace fixed_mul_beaver_detail;
const auto zero = make_fixed_from_integral_type<FractionalBits, integral>(
integral{});
if constexpr (!shape::active)
{
(void)prep;
(void)triple;
(void)lhs_share;
(void)rhs_share;
(void)exchange;
return zero;
}
else
{
if (triple.multiply_bits != plan::multiply_bits)
throw std::invalid_argument("fixed_mul beaver triple width does not match the window");
auto reduce = [&](u128 share, unsigned width, const lift_keys & lift,
bool do_lift) -> u128 {
const unsigned kept = width < 128u ? width : 128u;
u128 limb = share & bit_mask(kept);
if (!do_lift)
return limb & bit_mask(plan::multiply_bits);
return apply_lift(limb, lift, party, exchange);
};
const u128 left = reduce(share_bits(lhs_share), plan::lhs_width,
prep.lhs, shape::lhs_lift);
const u128 right = reduce(share_bits(rhs_share), plan::rhs_width,
prep.rhs, shape::rhs_lift);
const unsigned m = plan::multiply_bits;
const u128 d = open_sum(sub_mod(left, triple.a[party], m), m, exchange);
const u128 e = open_sum(sub_mod(right, triple.b[party], m), m, exchange);
u128 prod = add_mod(triple.ab[party],
add_mod(mul_mod(d, triple.b[party], m),
mul_mod(e, triple.a[party], m), m), m);
if (party == 1u)
prod = add_mod(prod, mul_mod(d, e, m), m);
u128 wide = prod;
if constexpr (shape::product_lift)
wide = apply_lift(wide, prep.product, party, exchange);
u128 window = wide;
if constexpr (shape::shift_right)
window = apply_shift(wide, prep.shift, party, exchange);
else if constexpr (shape::shift_left)
{
constexpr unsigned k = static_cast<unsigned>(-plan::align_shift);
window = ((wide & bit_mask(plan::modulus_bits)) << k)
& bit_mask(plan::out_bits);
}
else
window = wide & bit_mask(plan::out_bits);
if constexpr (shape::result_lift)
window = apply_lift(window, prep.result, party, exchange);
window &= bit_mask(shape::storage_bits);
std::uint64_t limbs[4] = {
static_cast<std::uint64_t>(window),
static_cast<std::uint64_t>(window >> 64),
0u,
0u};
return make_fixed_from_integral_type<FractionalBits, integral>(
detail::limbs_to_integral<integral>(limbs));
}
}
} // namespace grotto
#endif // LIBDPF_INCLUDE_GROTTO_FIXEDPOINT_BEAVER_HPP__

View file

@ -29,11 +29,13 @@ namespace grotto
/// integer part. Both discards are the low or high residue modulo a power of
/// two, so a negative value is floored onto the output ulp.
///
/// A Beaver triple replaces only the multiply in `Z/2^multiply_bits Z`.
/// Reducing each operand into that ring is local when it is a truncation or a
/// zero-extend. Sign-extending a narrower signed operand, and replicating the
/// product sign when `modulus_bits > multiply_bits`, are plaintext steps the
/// MPC protocol has to reproduce (they are not local on additive shares).
/// A Beaver triple replaces only the multiply in `Z/2^multiply_bits Z`
/// (`grotto::eval_fixed_mul_beaver`). Dropping bits above that ring is local.
/// A narrower share is lifted by cancelling the carry out of the operand
/// width; a signed lift also replicates the sign. The same correction
/// replicates the product sign when `modulus_bits > multiply_bits` and
/// supplies the right shift by `align_shift`. Each of those lifts extends
/// by at most 64 bits, and the shift discards at most 64 bits.
/// @tparam IntegerBits number of integer bits, including the sign
/// @tparam FractionalBits number of fractional bits
/// @tparam LhsFractionalBits lhs fractional bits
@ -206,11 +208,11 @@ constexpr void store_raw_limbs(const T & value, std::uint64_t out[4]) noexcept
/// @brief Low `dest_bits` of `value`, sign-extended when `value` is a narrower signed integer.
/// @tparam T value type
/// @param value the value to convert or store
/// @param src_bits the `src_bits`
/// @param is_signed the `is_signed`
/// @param dest_bits the `dest_bits`
/// @param src_bits width of `value` before extension
/// @param is_signed whether `value` is a signed integer of `src_bits` bits
/// @param dest_bits width of the destination word
/// @param dest the destination
/// @param nlimbs the `nlimbs`
/// @param nlimbs number of 64-bit limbs kept in the product
template <typename T>
HEDLEY_NO_THROW
constexpr void reduce_operand(const T & value, unsigned src_bits, bool is_signed,
@ -239,7 +241,7 @@ constexpr void reduce_operand(const T & value, unsigned src_bits, bool is_signed
/// @param out the output buffer
/// @param lhs the left-hand operand
/// @param rhs the right-hand operand
/// @param nlimbs the `nlimbs`
/// @param nlimbs number of 64-bit limbs kept in the product
HEDLEY_NO_THROW
constexpr void mul_low_limbs(std::uint64_t * out, const std::uint64_t * lhs,
const std::uint64_t * rhs, unsigned nlimbs) noexcept
@ -360,6 +362,11 @@ constexpr T limbs_to_integral(const std::uint64_t * limbs) noexcept
/// @param lhs the left-hand operand
/// @param rhs the right-hand operand
/// @return the product at the requested width
/// \complexity `mul_low_limbs` multiplies `L` 64-bit limbs, `L = fixed_mul_plan::limbs`, with loops `i < L` and `i + j < L`: `Θ(L²)` limb products.
/// Reducing each operand and the align shift are `Θ(L)`. Scratch is a fixed limb buffer in this header (the 8-word arrays plus `fixed_mul_buf_limbs`).
/// @see grotto::fixedpoint
/// @note Plaintext. `grotto::eval_fixed_mul_beaver` is the same window on additive shares: one Beaver product in `Z/2^{multiply_bits}Z`, then the lifts and the aligning shift.
/// @see grotto::eval_fixed_mul_beaver
template <unsigned IntegerBits,
unsigned FractionalBits,
unsigned LhsFractionalBits,

View file

@ -20,6 +20,8 @@ namespace grotto
namespace gadgets
{
/// \complexity Static constants only. Reading a field is `Θ(1)`.
/// @see grotto::gadgets
template <typename T>
struct gadget_hints
@ -33,6 +35,8 @@ struct gadget_hints
static constexpr double * canonical_bounds = nullptr;
static constexpr std::array<double, degree+1> * canonical_polys = nullptr;
};
/// \complexity Two `nexttoward` calls. `Θ(1)`.
/// @see grotto::gadgets
constexpr double ulp_of(double x)
{
@ -44,6 +48,8 @@ constexpr double ulp_of(double x)
template <typename T, std::size_t N>
struct gadget_domain
{
/// \complexity A constant number of `exp2` / casts. `Θ(1)`.
/// @see grotto::gadget_hints
static constexpr T min()
{

View file

@ -21,9 +21,19 @@ namespace gadgets
{
static constexpr double celu_default_alpha = 1;
/// @brief Cleartext `celu`.
/// @see grotto::eval_closed
/// @tparam alpha functor parameter (a `double` bound or scale)
/// @note Straight-line cleartext. Fixed-point evaluation is the symbol in `@see` when that symbol is a LUT or an `eval_*` function.
/// \complexity One evaluation of `operator()` as written in this header. There is no domain scan and no loop. `Θ(1)` time and extra space relative to that expression.
template <double const & alpha = celu_default_alpha>
struct celu
{
/// @brief Evaluate the cleartext map.
/// @tparam T argument and result type
/// @param x the input
/// @return the cleartext image of `x`
/// \complexity The body is straight-line. `Θ(1)` time and extra space.
template <typename T>
T operator()(T x)
{

View file

@ -25,8 +25,17 @@ HEDLEY_DIAGNOSTIC_PUSH
HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED
/// @brief Cleartext `elish`.
/// @see grotto::eval_window
/// @note Straight-line cleartext. Fixed-point evaluation is the symbol in `@see` when that symbol is a LUT or an `eval_*` function.
/// \complexity One evaluation of `operator()` as written in this header. There is no domain scan and no loop. `Θ(1)` time and extra space relative to that expression.
struct HEDLEY_DEPRECATED_FOR(2026, grotto::eval_window) elish
{
/// @brief Evaluate the cleartext map.
/// @tparam T argument and result type
/// @param x the input
/// @return the cleartext image of `x`
/// \complexity The body is straight-line. `Θ(1)` time and extra space.
template <typename T>
T operator()(T x)
{

View file

@ -21,9 +21,19 @@ namespace gadgets
{
static constexpr double elu_default_alpha = 1;
/// @brief Cleartext `elu`.
/// @see grotto::eval_closed
/// @tparam alpha functor parameter (a `double` bound or scale)
/// @note Straight-line cleartext. Fixed-point evaluation is the symbol in `@see` when that symbol is a LUT or an `eval_*` function.
/// \complexity One evaluation of `operator()` as written in this header. There is no domain scan and no loop. `Θ(1)` time and extra space relative to that expression.
template <double const & alpha = elu_default_alpha>
struct elu
{
/// @brief Evaluate the cleartext map.
/// @tparam T argument and result type
/// @param x the input
/// @return the cleartext image of `x`
/// \complexity The body is straight-line. `Θ(1)` time and extra space.
template <typename T>
T operator()(T x)
{

View file

@ -24,8 +24,17 @@ HEDLEY_DIAGNOSTIC_PUSH
HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED
/// @brief Cleartext `gelu`.
/// @see grotto::eval_window
/// @note Straight-line cleartext. Fixed-point evaluation is the symbol in `@see` when that symbol is a LUT or an `eval_*` function.
/// \complexity One evaluation of `operator()` as written in this header. There is no domain scan and no loop. `Θ(1)` time and extra space relative to that expression.
struct HEDLEY_DEPRECATED_FOR(2026, grotto::eval_window) gelu
{
/// @brief Evaluate the cleartext map.
/// @tparam T argument and result type
/// @param x the input
/// @return the cleartext image of `x`
/// \complexity The body is straight-line. `Θ(1)` time and extra space.
template <typename T>
T operator()(T x)
{

View file

@ -20,8 +20,17 @@ namespace grotto
namespace gadgets
{
/// @brief Cleartext `hardelish`.
/// @see grotto::eval_window with `window::hardelish`
/// @note Straight-line cleartext. Fixed-point evaluation is the symbol in `@see` when that symbol is a LUT or an `eval_*` function.
/// \complexity One evaluation of `operator()` as written in this header. There is no domain scan and no loop. `Θ(1)` time and extra space relative to that expression.
struct hardelish
{
/// @brief Evaluate the cleartext map.
/// @tparam T argument and result type
/// @param x the input
/// @return the cleartext image of `x`
/// \complexity The body is straight-line. `Θ(1)` time and extra space.
template <typename T>
T operator()(T x)
{

View file

@ -25,9 +25,19 @@ HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED
static constexpr double hardshrink_default_lambda = 0.5;
/// @brief Cleartext `hardshrink`.
/// @see grotto::make_hardshrink_lut
/// @tparam lambda functor parameter (a `double` bound or scale)
/// @note Straight-line cleartext. Fixed-point evaluation is the symbol in `@see` when that symbol is a LUT or an `eval_*` function.
/// \complexity One evaluation of `operator()` as written in this header. There is no domain scan and no loop. `Θ(1)` time and extra space relative to that expression.
template <double const & lambda = hardshrink_default_lambda>
struct HEDLEY_DEPRECATED_FOR(2026, grotto::make_hardshrink_lut) hardshrink
{
/// @brief Evaluate the cleartext map.
/// @tparam T argument and result type
/// @param x the input
/// @return the cleartext image of `x`
/// \complexity The body is straight-line. `Θ(1)` time and extra space.
template <typename T>
T operator()(T x)
{

View file

@ -24,8 +24,17 @@ HEDLEY_DIAGNOSTIC_PUSH
HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED
/// @brief Cleartext `hardsigmoid`.
/// @see grotto::make_hardsigmoid_lut
/// @note Straight-line cleartext. Fixed-point evaluation is the symbol in `@see` when that symbol is a LUT or an `eval_*` function.
/// \complexity One evaluation of `operator()` as written in this header. There is no domain scan and no loop. `Θ(1)` time and extra space relative to that expression.
struct HEDLEY_DEPRECATED_FOR(2026, grotto::make_hardsigmoid_lut) hardsigmoid
{
/// @brief Evaluate the cleartext map.
/// @tparam T argument and result type
/// @param x the input
/// @return the cleartext image of `x`
/// \complexity The body is straight-line. `Θ(1)` time and extra space.
template <typename T>
T operator()(T x)
{

View file

@ -24,8 +24,17 @@ HEDLEY_DIAGNOSTIC_PUSH
HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED
/// @brief Cleartext `hardswish`.
/// @see grotto::make_hardswish_lut
/// @note Straight-line cleartext. Fixed-point evaluation is the symbol in `@see` when that symbol is a LUT or an `eval_*` function.
/// \complexity One evaluation of `operator()` as written in this header. There is no domain scan and no loop. `Θ(1)` time and extra space relative to that expression.
struct HEDLEY_DEPRECATED_FOR(2026, grotto::make_hardswish_lut) hardswish
{
/// @brief Evaluate the cleartext map.
/// @tparam T argument and result type
/// @param x the input
/// @return the cleartext image of `x`
/// \complexity The body is straight-line. `Θ(1)` time and extra space.
template <typename T>
T operator()(T x)
{

View file

@ -24,8 +24,17 @@ HEDLEY_DIAGNOSTIC_PUSH
HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED
/// @brief Cleartext `hardtanh`.
/// @see grotto::make_hardtanh_lut
/// @note Straight-line cleartext. Fixed-point evaluation is the symbol in `@see` when that symbol is a LUT or an `eval_*` function.
/// \complexity One evaluation of `operator()` as written in this header. There is no domain scan and no loop. `Θ(1)` time and extra space relative to that expression.
struct HEDLEY_DEPRECATED_FOR(2026, grotto::make_hardtanh_lut) hardtanh
{
/// @brief Evaluate the cleartext map.
/// @tparam T argument and result type
/// @param x the input
/// @return the cleartext image of `x`
/// \complexity The body is straight-line. `Θ(1)` time and extra space.
template <typename T>
T operator()(T x)
{

View file

@ -26,9 +26,21 @@ HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED
static constexpr double leakyrelu_default_negative_slope = 0.01;
static constexpr double leakyrelu_zero_negative_slope = 0.0;
/// @brief Cleartext `leakyrelu`. The default slope `0.01` is
/// `make_leaky_relu_hundredth_lut`. A power-of-two slope is
/// `make_leaky_relu_lut`.
/// @see grotto::make_leaky_relu_hundredth_lut
/// @tparam negative_slope functor parameter (a `double` bound or scale)
/// @note Straight-line cleartext. Fixed-point evaluation is the symbol in `@see` when that symbol is a LUT or an `eval_*` function.
/// \complexity One evaluation of `operator()` as written in this header. There is no domain scan and no loop. `Θ(1)` time and extra space relative to that expression.
template <double const & negative_slope = leakyrelu_default_negative_slope>
struct HEDLEY_DEPRECATED_FOR(2026, grotto::make_leaky_relu_lut) leakyrelu
struct HEDLEY_DEPRECATED_FOR(2026, grotto::make_leaky_relu_hundredth_lut) leakyrelu
{
/// @brief Evaluate the cleartext map.
/// @tparam T argument and result type
/// @param x the input
/// @return the cleartext image of `x`
/// \complexity The body is straight-line. `Θ(1)` time and extra space.
template <typename T>
T operator()(T x)
{

View file

@ -21,8 +21,17 @@ namespace grotto
namespace gadgets
{
/// @brief Cleartext `lecun_tanh`.
/// @see grotto::eval_window with `window::lecun_tanh`
/// @note Straight-line cleartext. Fixed-point evaluation is the symbol in `@see` when that symbol is a LUT or an `eval_*` function.
/// \complexity One evaluation of `operator()` as written in this header. There is no domain scan and no loop. `Θ(1)` time and extra space relative to that expression.
struct lecun_tanh
{
/// @brief Evaluate the cleartext map.
/// @tparam T argument and result type
/// @param x the input
/// @return the cleartext image of `x`
/// \complexity The body is straight-line. `Θ(1)` time and extra space.
template <typename T>
T operator()(T x)
{

View file

@ -25,8 +25,17 @@ HEDLEY_DIAGNOSTIC_PUSH
HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED
/// @brief Cleartext `logsigmoid`.
/// @see grotto::eval_window
/// @note Straight-line cleartext. Fixed-point evaluation is the symbol in `@see` when that symbol is a LUT or an `eval_*` function.
/// \complexity One evaluation of `operator()` as written in this header. There is no domain scan and no loop. `Θ(1)` time and extra space relative to that expression.
struct HEDLEY_DEPRECATED_FOR(2026, grotto::eval_window) logsigmoid
{
/// @brief Evaluate the cleartext map.
/// @tparam T argument and result type
/// @param x the input
/// @return the cleartext image of `x`
/// \complexity The body is straight-line. `Θ(1)` time and extra space.
template <typename T>
T operator()(T x) { return std::log(sigmoid{}(x)); }
};

View file

@ -25,8 +25,17 @@ HEDLEY_DIAGNOSTIC_PUSH
HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED
/// @brief Cleartext `mish`.
/// @see grotto::eval_window
/// @note Straight-line cleartext. Fixed-point evaluation is the symbol in `@see` when that symbol is a LUT or an `eval_*` function.
/// \complexity One evaluation of `operator()` as written in this header. There is no domain scan and no loop. `Θ(1)` time and extra space relative to that expression.
struct HEDLEY_DEPRECATED_FOR(2026, grotto::eval_window) mish
{
/// @brief Evaluate the cleartext map.
/// @tparam T argument and result type
/// @param x the input
/// @return the cleartext image of `x`
/// \complexity The body is straight-line. `Θ(1)` time and extra space.
template <typename T>
T operator()(T x)
{

View file

@ -24,8 +24,17 @@ namespace gadgets
HEDLEY_DIAGNOSTIC_PUSH
HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED
/// @brief Cleartext `one_minus_sigmoid`.
/// @see grotto::eval_window with `window::one_minus_sigmoid`
/// @note Straight-line cleartext. Fixed-point evaluation is the symbol in `@see` when that symbol is a LUT or an `eval_*` function.
/// \complexity One evaluation of `operator()` as written in this header. There is no domain scan and no loop. `Θ(1)` time and extra space relative to that expression.
struct HEDLEY_DEPRECATED_FOR(2026, grotto::eval_window) one_minus_sigmoid
{
/// @brief Evaluate the cleartext map.
/// @tparam T argument and result type
/// @param x the input
/// @return the cleartext image of `x`
/// \complexity The body is straight-line. `Θ(1)` time and extra space.
template <typename T>
T operator()(T x)
{

View file

@ -23,6 +23,9 @@ namespace gadgets
HEDLEY_DIAGNOSTIC_PUSH
HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED
/// @brief Alias of another cleartext functor, under the name `relu`.
/// @see grotto::make_relu_lut
/// \complexity Same as the functor it aliases: one straight-line `operator()`. `Θ(1)`.
using relu HEDLEY_DEPRECATED_FOR(2026, grotto::make_relu_lut) = leakyrelu<leakyrelu_zero_negative_slope>;
HEDLEY_DIAGNOSTIC_POP

View file

@ -25,9 +25,19 @@ HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED
static constexpr double relu6_default_clip = 6;
/// @brief Cleartext `relu6`.
/// @see grotto::make_relu6_lut
/// @tparam clip functor parameter (a `double` bound or scale)
/// @note Straight-line cleartext. Fixed-point evaluation is the symbol in `@see` when that symbol is a LUT or an `eval_*` function.
/// \complexity One evaluation of `operator()` as written in this header. There is no domain scan and no loop. `Θ(1)` time and extra space relative to that expression.
template <double const & clip = 6>
struct HEDLEY_DEPRECATED_FOR(2026, grotto::make_relu6_lut) relu6
{
/// @brief Evaluate the cleartext map.
/// @tparam T argument and result type
/// @param x the input
/// @return the cleartext image of `x`
/// \complexity The body is straight-line. `Θ(1)` time and extra space.
template <typename T>
T operator()(T x)
{

View file

@ -22,10 +22,21 @@ namespace gadgets
static constexpr double selu_default_alpha = 1.6732632423543772848170429916717;
static constexpr double selu_default_scale = 1.0507009873554804934193349852946;
/// @brief Cleartext `selu`.
/// @see grotto::eval_closed
/// @tparam scale functor parameter (a `double` bound or scale)
/// @tparam alpha functor parameter (a `double` bound or scale)
/// @note Straight-line cleartext. Fixed-point evaluation is the symbol in `@see` when that symbol is a LUT or an `eval_*` function.
/// \complexity One evaluation of `operator()` as written in this header. There is no domain scan and no loop. `Θ(1)` time and extra space relative to that expression.
template <double const & alpha = selu_default_alpha,
double const & scale = selu_default_scale>
struct selu
{
/// @brief Evaluate the cleartext map.
/// @tparam T argument and result type
/// @param x the input
/// @return the cleartext image of `x`
/// \complexity The body is straight-line. `Θ(1)` time and extra space.
template <typename T>
T operator()(T x)
{

View file

@ -25,8 +25,17 @@ HEDLEY_DIAGNOSTIC_PUSH
HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED
/// @brief Cleartext `serf`.
/// @see grotto::eval_window
/// @note Straight-line cleartext. Fixed-point evaluation is the symbol in `@see` when that symbol is a LUT or an `eval_*` function.
/// \complexity One evaluation of `operator()` as written in this header. There is no domain scan and no loop. `Θ(1)` time and extra space relative to that expression.
struct HEDLEY_DEPRECATED_FOR(2026, grotto::eval_window) serf
{
/// @brief Evaluate the cleartext map.
/// @tparam T argument and result type
/// @param x the input
/// @return the cleartext image of `x`
/// \complexity The body is straight-line. `Θ(1)` time and extra space.
template <typename T>
T operator()(T x)
{

View file

@ -24,8 +24,17 @@ HEDLEY_DIAGNOSTIC_PUSH
HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED
/// @brief Cleartext `sigmoid`.
/// @see grotto::eval_window
/// @note Straight-line cleartext. Fixed-point evaluation is the symbol in `@see` when that symbol is a LUT or an `eval_*` function.
/// \complexity One evaluation of `operator()` as written in this header. There is no domain scan and no loop. `Θ(1)` time and extra space relative to that expression.
struct HEDLEY_DEPRECATED_FOR(2026, grotto::eval_window) sigmoid
{
/// @brief Evaluate the cleartext map.
/// @tparam T argument and result type
/// @param x the input
/// @return the cleartext image of `x`
/// \complexity The body is straight-line. `Θ(1)` time and extra space.
template <typename T>
T operator()(T x) { return 1/(1+std::exp(-x)); }
};

View file

@ -24,8 +24,17 @@ HEDLEY_DIAGNOSTIC_PUSH
HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED
/// @brief Cleartext `silu`.
/// @see grotto::eval_window
/// @note Straight-line cleartext. Fixed-point evaluation is the symbol in `@see` when that symbol is a LUT or an `eval_*` function.
/// \complexity One evaluation of `operator()` as written in this header. There is no domain scan and no loop. `Θ(1)` time and extra space relative to that expression.
struct HEDLEY_DEPRECATED_FOR(2026, grotto::eval_window) silu
{
/// @brief Evaluate the cleartext map.
/// @tparam T argument and result type
/// @param x the input
/// @return the cleartext image of `x`
/// \complexity The body is straight-line. `Θ(1)` time and extra space.
template <typename T>
T operator()(T x) { return x*sigmoid{}(x); }
};

View file

@ -25,9 +25,19 @@ HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED
static constexpr double smoothstep_default_gamma = 1;
/// @brief Cleartext `smoothstep`.
/// @see grotto::eval_window
/// @tparam gamma functor parameter (a `double` bound or scale)
/// @note Straight-line cleartext. Fixed-point evaluation is the symbol in `@see` when that symbol is a LUT or an `eval_*` function.
/// \complexity One evaluation of `operator()` as written in this header. There is no domain scan and no loop. `Θ(1)` time and extra space relative to that expression.
template <double const & gamma = smoothstep_default_gamma>
struct HEDLEY_DEPRECATED_FOR(2026, grotto::eval_window) smoothstep
{
/// @brief Evaluate the cleartext map.
/// @tparam T argument and result type
/// @param x the input
/// @return the cleartext image of `x`
/// \complexity The body is straight-line. `Θ(1)` time and extra space.
template <typename T>
T operator()(T x)
{

View file

@ -26,9 +26,19 @@ HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED
static constexpr double softminus_default_beta = 1;
/// @brief Cleartext `softminus`.
/// @see grotto::eval_window
/// @tparam beta functor parameter (a `double` bound or scale)
/// @note Straight-line cleartext. Fixed-point evaluation is the symbol in `@see` when that symbol is a LUT or an `eval_*` function.
/// \complexity One evaluation of `operator()` as written in this header. There is no domain scan and no loop. `Θ(1)` time and extra space relative to that expression.
template <double const & beta = softminus_default_beta>
struct HEDLEY_DEPRECATED_FOR(2026, grotto::eval_window) softminus
{
/// @brief Evaluate the cleartext map.
/// @tparam T argument and result type
/// @param x the input
/// @return the cleartext image of `x`
/// \complexity The body is straight-line. `Θ(1)` time and extra space.
template <typename T>
T operator()(T x)
{

View file

@ -25,9 +25,19 @@ HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED
static constexpr double softplus_default_beta = 1;
/// @brief Cleartext `softplus`.
/// @see grotto::eval_window
/// @tparam beta functor parameter (a `double` bound or scale)
/// @note Straight-line cleartext. Fixed-point evaluation is the symbol in `@see` when that symbol is a LUT or an `eval_*` function.
/// \complexity One evaluation of `operator()` as written in this header. There is no domain scan and no loop. `Θ(1)` time and extra space relative to that expression.
template <double const & beta = softplus_default_beta>
struct HEDLEY_DEPRECATED_FOR(2026, grotto::eval_window) softplus
{
/// @brief Evaluate the cleartext map.
/// @tparam T argument and result type
/// @param x the input
/// @return the cleartext image of `x`
/// \complexity The body is straight-line. `Θ(1)` time and extra space.
template <typename T>
T operator()(T x)
{

View file

@ -25,9 +25,19 @@ HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED
static constexpr double softshrink_default_lambda = 0.5;
/// @brief Cleartext `softshrink`.
/// @see grotto::make_softshrink_lut
/// @tparam lambda functor parameter (a `double` bound or scale)
/// @note Straight-line cleartext. Fixed-point evaluation is the symbol in `@see` when that symbol is a LUT or an `eval_*` function.
/// \complexity One evaluation of `operator()` as written in this header. There is no domain scan and no loop. `Θ(1)` time and extra space relative to that expression.
template <double const & lambda = softshrink_default_lambda>
struct HEDLEY_DEPRECATED_FOR(2026, grotto::make_softshrink_lut) softshrink
{
/// @brief Evaluate the cleartext map.
/// @tparam T argument and result type
/// @param x the input
/// @return the cleartext image of `x`
/// \complexity The body is straight-line. `Θ(1)` time and extra space.
template <typename T>
T operator()(T x)
{

View file

@ -20,8 +20,17 @@ namespace grotto
namespace gadgets
{
/// @brief Cleartext `hardsign`.
/// @see grotto::eval_closed
/// @note Straight-line cleartext. Fixed-point evaluation is the symbol in `@see` when that symbol is a LUT or an `eval_*` function.
/// \complexity One evaluation of `operator()` as written in this header. There is no domain scan and no loop. `Θ(1)` time and extra space relative to that expression.
struct hardsign
{
/// @brief Evaluate the cleartext map.
/// @tparam T argument and result type
/// @param x the input
/// @return the cleartext image of `x`
/// \complexity The body is straight-line. `Θ(1)` time and extra space.
template <typename T>
T operator()(T x) { return x/(1+std::abs(x)); }
};

View file

@ -24,8 +24,17 @@ HEDLEY_DIAGNOSTIC_PUSH
HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED
/// @brief Cleartext `squared_relu`.
/// @see grotto::make_squared_relu_lut
/// @note Straight-line cleartext. Fixed-point evaluation is the symbol in `@see` when that symbol is a LUT or an `eval_*` function.
/// \complexity One evaluation of `operator()` as written in this header. There is no domain scan and no loop. `Θ(1)` time and extra space relative to that expression.
struct HEDLEY_DEPRECATED_FOR(2026, grotto::make_squared_relu_lut) squared_relu
{
/// @brief Evaluate the cleartext map.
/// @tparam T argument and result type
/// @param x the input
/// @return the cleartext image of `x`
/// \complexity The body is straight-line. `Θ(1)` time and extra space.
template <typename T>
T operator()(T x)
{

View file

@ -22,10 +22,21 @@ namespace gadgets
static constexpr double starrelu_default_scale = 0.8944;
static constexpr double starrelu_default_shift = -0.4472;
/// @brief Cleartext `starrelu`.
/// @see grotto::make_relu_lut
/// @tparam shift functor parameter (a `double` bound or scale)
/// @tparam scale functor parameter (a `double` bound or scale)
/// @note Straight-line cleartext. Fixed-point evaluation is the symbol in `@see` when that symbol is a LUT or an `eval_*` function.
/// \complexity One evaluation of `operator()` as written in this header. There is no domain scan and no loop. `Θ(1)` time and extra space relative to that expression.
template<double const & scale = starrelu_default_scale,
double const & shift = starrelu_default_shift>
struct starrelu
{
/// @brief Evaluate the cleartext map.
/// @tparam T argument and result type
/// @param x the input
/// @return the cleartext image of `x`
/// \complexity The body is straight-line. `Θ(1)` time and extra space.
template <typename T>
T operator()(T x)
{

View file

@ -24,8 +24,17 @@ HEDLEY_DIAGNOSTIC_PUSH
HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED
/// @brief Cleartext `tanhexp`.
/// @see grotto::eval_window
/// @note Straight-line cleartext. Fixed-point evaluation is the symbol in `@see` when that symbol is a LUT or an `eval_*` function.
/// \complexity One evaluation of `operator()` as written in this header. There is no domain scan and no loop. `Θ(1)` time and extra space relative to that expression.
struct HEDLEY_DEPRECATED_FOR(2026, grotto::eval_window) tanhexp
{
/// @brief Evaluate the cleartext map.
/// @tparam T argument and result type
/// @param x the input
/// @return the cleartext image of `x`
/// \complexity The body is straight-line. `Θ(1)` time and extra space.
template <typename T>
T operator()(T x) { return x*std::tanh(std::exp(x)); }
};

View file

@ -20,8 +20,17 @@ namespace grotto
namespace gadgets
{
/// @brief Cleartext `tanhshrink`.
/// @see grotto::eval_closed
/// @note Straight-line cleartext. Fixed-point evaluation is the symbol in `@see` when that symbol is a LUT or an `eval_*` function.
/// \complexity One evaluation of `operator()` as written in this header. There is no domain scan and no loop. `Θ(1)` time and extra space relative to that expression.
struct tanhshrink
{
/// @brief Evaluate the cleartext map.
/// @tparam T argument and result type
/// @param x the input
/// @return the cleartext image of `x`
/// \complexity The body is straight-line. `Θ(1)` time and extra space.
template <typename T>
T operator()(T x) { return x -std::tanh(x); }
};

View file

@ -16,6 +16,9 @@ namespace gadgets
/// @note This header declares no callable.
/// Complexity is stated on the sibling that evaluates the map, when one exists.
/// @see grotto::make_msb_lut
} // namespace gadgets
} // namespace grotto

View file

@ -16,6 +16,9 @@ namespace gadgets
/// @note This header declares no callable.
/// Complexity is stated on the sibling that evaluates the map, when one exists.
/// @see grotto::eval_bit_ceil
} // namespace gadgets
} // namespace grotto

View file

@ -16,6 +16,9 @@ namespace gadgets
/// @note This header declares no callable.
/// Complexity is stated on the sibling that evaluates the map, when one exists.
/// @see grotto::eval_bit_floor
} // namespace gadgets
} // namespace grotto

View file

@ -16,6 +16,9 @@ namespace gadgets
/// @note This header declares no callable.
/// Complexity is stated on the sibling that evaluates the map, when one exists.
/// @see grotto::eval_bit_width
} // namespace gadgets
} // namespace grotto

View file

@ -16,6 +16,9 @@ namespace gadgets
/// @note This header declares no callable.
/// Complexity is stated on the sibling that evaluates the map, when one exists.
/// @see grotto::exact_constant
} // namespace gadgets
} // namespace grotto

View file

@ -16,6 +16,9 @@ namespace gadgets
/// @note This header declares no callable.
/// Complexity is stated on the sibling that evaluates the map, when one exists.
/// @see grotto::eval_countl_one
} // namespace gadgets
} // namespace grotto

View file

@ -24,8 +24,17 @@ HEDLEY_DIAGNOSTIC_PUSH
HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED
/// @brief Cleartext `countl_zero`.
/// @see grotto::exact_constant
/// @note Straight-line cleartext. Fixed-point evaluation is the symbol in `@see` when that symbol is a LUT or an `eval_*` function.
/// \complexity One evaluation of `operator()` as written in this header. There is no domain scan and no loop. `Θ(1)` time and extra space relative to that expression.
struct HEDLEY_DEPRECATED_FOR(2026, grotto::exact_constant) countl_zero
{
/// @brief Evaluate the cleartext map.
/// @tparam T argument and result type
/// @param x the input
/// @return the cleartext image of `x`
/// \complexity The body is straight-line. `Θ(1)` time and extra space.
template <typename T>
T operator()(T x)
{

View file

@ -20,8 +20,17 @@ namespace grotto
namespace gadgets
{
/// @brief Cleartext `has_single_bit`.
/// @see grotto::eval_has_single_bit
/// @note Straight-line cleartext. Fixed-point evaluation is the symbol in `@see` when that symbol is a LUT or an `eval_*` function.
/// \complexity One evaluation of `operator()` as written in this header. There is no domain scan and no loop. `Θ(1)` time and extra space relative to that expression.
struct has_single_bit
{
/// @brief Evaluate the cleartext map.
/// @tparam T argument and result type
/// @param x the input
/// @return the cleartext image of `x`
/// \complexity The body is straight-line. `Θ(1)` time and extra space.
template <typename T>
T operator()(T x)
{

View file

@ -18,6 +18,9 @@ namespace gadgets
/// @note This header declares no callable.
/// Complexity is stated on the sibling that evaluates the map, when one exists.
/// @see grotto::prefix_parities
} // namespace gadgets
} // namespace grotto

View file

@ -24,8 +24,17 @@ HEDLEY_DIAGNOSTIC_PUSH
HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED
/// @brief Cleartext `sgn`.
/// @see grotto::exact_constant
/// @note Straight-line cleartext. Fixed-point evaluation is the symbol in `@see` when that symbol is a LUT or an `eval_*` function.
/// \complexity One evaluation of `operator()` as written in this header. There is no domain scan and no loop. `Θ(1)` time and extra space relative to that expression.
struct HEDLEY_DEPRECATED_FOR(2026, grotto::exact_constant) sgn
{
/// @brief Evaluate the cleartext map.
/// @tparam T argument and result type
/// @param x the input
/// @return the cleartext image of `x`
/// \complexity The body is straight-line. `Θ(1)` time and extra space.
template <typename T>
T operator()(T x)
{

View file

@ -16,6 +16,9 @@ namespace gadgets
/// @note This header declares no callable.
/// Complexity is stated on the sibling that evaluates the map, when one exists.
/// @see grotto::eval_dec_ceil
} // namespace gadgets
} // namespace grotto

View file

@ -16,6 +16,9 @@ namespace gadgets
/// @note This header declares no callable.
/// Complexity is stated on the sibling that evaluates the map, when one exists.
/// @see grotto::eval_dec_floor
} // namespace gadgets
} // namespace grotto

View file

@ -16,6 +16,9 @@ namespace gadgets
/// @note This header declares no callable.
/// Complexity is stated on the sibling that evaluates the map, when one exists.
/// @see grotto::eval_dec_width
} // namespace gadgets
} // namespace grotto

View file

@ -16,6 +16,9 @@ namespace gadgets
/// @note This header declares no callable.
/// Complexity is stated on the sibling that evaluates the map, when one exists.
/// @see grotto::eval_has_single_digit
} // namespace gadgets
} // namespace grotto

View file

@ -24,8 +24,17 @@ HEDLEY_DIAGNOSTIC_PUSH
HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED
/// @brief Cleartext `abs`.
/// @see grotto::make_abs_lut
/// @note Straight-line cleartext. Fixed-point evaluation is the symbol in `@see` when that symbol is a LUT or an `eval_*` function.
/// \complexity One evaluation of `operator()` as written in this header. There is no domain scan and no loop. `Θ(1)` time and extra space relative to that expression.
struct HEDLEY_DEPRECATED_FOR(2026, grotto::make_abs_lut) abs
{
/// @brief Evaluate the cleartext map.
/// @tparam T argument and result type
/// @param x the input
/// @return the cleartext image of `x`
/// \complexity The body is straight-line. `Θ(1)` time and extra space.
template <typename T>
T operator()(T x) { return std::abs(static_cast<double>(x)); }
};

View file

@ -23,10 +23,21 @@ namespace gadgets
static constexpr double approx_default_target = 0;
static constexpr unsigned approx_default_ULPs = 1;
/// @brief Cleartext `approx`.
/// @see grotto::make_threshold_lut
/// @tparam ULPs functor parameter (a `double` bound or scale)
/// @tparam target functor parameter (a `double` bound or scale)
/// @note Straight-line cleartext. Fixed-point evaluation is the symbol in `@see` when that symbol is a LUT or an `eval_*` function.
/// \complexity One evaluation of `operator()` as written in this header. There is no domain scan and no loop. `Θ(1)` time and extra space relative to that expression.
template <double const & target = approx_default_target,
unsigned const & ULPs = approx_default_ULPs>
struct approx
{
/// @brief Evaluate the cleartext map.
/// @tparam T argument and result type
/// @param x the input
/// @return the cleartext image of `x`
/// \complexity The body is straight-line. `Θ(1)` time and extra space.
template <typename T>
T operator()(T x) { return std::abs(x - target) <= ULPs * ulp_of(target) ? 1 : 0; }
};

View file

@ -22,12 +22,25 @@ namespace gadgets
static constexpr double boxcar_default_outside = 0;
static constexpr double boxcar_default_inside = 1;
/// @brief Cleartext `boxcar`.
/// @see grotto::make_interval_lut
/// @tparam inside functor parameter (a `double` bound or scale)
/// @tparam outside functor parameter (a `double` bound or scale)
/// @tparam to functor parameter (a `double` bound or scale)
/// @tparam from functor parameter (a `double` bound or scale)
/// @note Straight-line cleartext. Fixed-point evaluation is the symbol in `@see` when that symbol is a LUT or an `eval_*` function.
/// \complexity One evaluation of `operator()` as written in this header. There is no domain scan and no loop. `Θ(1)` time and extra space relative to that expression.
template <double const & from,
double const & to,
double const & outside = boxcar_default_outside,
double const & inside = boxcar_default_inside>
struct boxcar
{
/// @brief Evaluate the cleartext map.
/// @tparam T argument and result type
/// @param x the input
/// @return the cleartext image of `x`
/// \complexity The body is straight-line. `Θ(1)` time and extra space.
template <typename T>
T operator()(T x) { return (x < from || x > to) ? outside : inside; }
};

View file

@ -23,11 +23,22 @@ HEDLEY_DIAGNOSTIC_PUSH
HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED
/// @brief Cleartext `clip`.
/// @see grotto::make_clip_lut
/// @tparam upper functor parameter (a `double` bound or scale)
/// @tparam lower functor parameter (a `double` bound or scale)
/// @note Straight-line cleartext. Fixed-point evaluation is the symbol in `@see` when that symbol is a LUT or an `eval_*` function.
/// \complexity One evaluation of `operator()` as written in this header. There is no domain scan and no loop. `Θ(1)` time and extra space relative to that expression.
template <double const & lower,
double const & upper>
struct HEDLEY_DEPRECATED_FOR(2026, grotto::make_clip_lut) clip
{
static_assert(lower <= upper);
/// @brief Evaluate the cleartext map.
/// @tparam T argument and result type
/// @param x the input
/// @return the cleartext image of `x`
/// \complexity The body is straight-line. `Θ(1)` time and extra space.
template <typename T>
T operator()(T x) { return std::max(std::min(x, upper), lower); }
};

View file

@ -19,9 +19,19 @@ namespace grotto
namespace gadgets
{
/// @brief Cleartext `eq`.
/// @see grotto::make_threshold_lut
/// @tparam target functor parameter (a `double` bound or scale)
/// @note Straight-line cleartext. Fixed-point evaluation is the symbol in `@see` when that symbol is a LUT or an `eval_*` function.
/// \complexity One evaluation of `operator()` as written in this header. There is no domain scan and no loop. `Θ(1)` time and extra space relative to that expression.
template <double const & target>
struct eq
{
/// @brief Evaluate the cleartext map.
/// @tparam T argument and result type
/// @param x the input
/// @return the cleartext image of `x`
/// \complexity The body is straight-line. `Θ(1)` time and extra space.
template <typename T>
T operator()(T x) { return x == target; }
};

View file

@ -19,9 +19,19 @@ namespace grotto
namespace gadgets
{
/// @brief Cleartext `geq`.
/// @see grotto::make_threshold_lut
/// @tparam target functor parameter (a `double` bound or scale)
/// @note Straight-line cleartext. Fixed-point evaluation is the symbol in `@see` when that symbol is a LUT or an `eval_*` function.
/// \complexity One evaluation of `operator()` as written in this header. There is no domain scan and no loop. `Θ(1)` time and extra space relative to that expression.
template <double const & target>
struct geq
{
/// @brief Evaluate the cleartext map.
/// @tparam T argument and result type
/// @param x the input
/// @return the cleartext image of `x`
/// \complexity The body is straight-line. `Θ(1)` time and extra space.
template <typename T>
T operator()(T x) { return x >= target; }
};

View file

@ -19,9 +19,19 @@ namespace grotto
namespace gadgets
{
/// @brief Cleartext `gt`.
/// @see grotto::make_threshold_lut
/// @tparam target functor parameter (a `double` bound or scale)
/// @note Straight-line cleartext. Fixed-point evaluation is the symbol in `@see` when that symbol is a LUT or an `eval_*` function.
/// \complexity One evaluation of `operator()` as written in this header. There is no domain scan and no loop. `Θ(1)` time and extra space relative to that expression.
template <double const & target>
struct gt
{
/// @brief Evaluate the cleartext map.
/// @tparam T argument and result type
/// @param x the input
/// @return the cleartext image of `x`
/// \complexity The body is straight-line. `Θ(1)` time and extra space.
template <typename T>
T operator()(T x) { return x > target; }
};

View file

@ -16,6 +16,9 @@ namespace gadgets
/// @note This header declares no callable.
/// Complexity is stated on the sibling that evaluates the map, when one exists.
/// @see grotto::fixedpoint
} // namespace gadgets
} // namespace grotto

View file

@ -19,9 +19,19 @@ namespace grotto
namespace gadgets
{
/// @brief Cleartext `leq`.
/// @see grotto::make_threshold_lut
/// @tparam target functor parameter (a `double` bound or scale)
/// @note Straight-line cleartext. Fixed-point evaluation is the symbol in `@see` when that symbol is a LUT or an `eval_*` function.
/// \complexity One evaluation of `operator()` as written in this header. There is no domain scan and no loop. `Θ(1)` time and extra space relative to that expression.
template <double const & target>
struct leq
{
/// @brief Evaluate the cleartext map.
/// @tparam T argument and result type
/// @param x the input
/// @return the cleartext image of `x`
/// \complexity The body is straight-line. `Θ(1)` time and extra space.
template <typename T>
T operator()(T x) { return x <= target; }
};

View file

@ -19,9 +19,19 @@ namespace grotto
namespace gadgets
{
/// @brief Cleartext `lt`.
/// @see grotto::make_threshold_lut
/// @tparam target functor parameter (a `double` bound or scale)
/// @note Straight-line cleartext. Fixed-point evaluation is the symbol in `@see` when that symbol is a LUT or an `eval_*` function.
/// \complexity One evaluation of `operator()` as written in this header. There is no domain scan and no loop. `Θ(1)` time and extra space relative to that expression.
template <double const & target>
struct lt
{
/// @brief Evaluate the cleartext map.
/// @tparam T argument and result type
/// @param x the input
/// @return the cleartext image of `x`
/// \complexity The body is straight-line. `Θ(1)` time and extra space.
template <typename T>
T operator()(T x) { return x < target; }
};

View file

@ -17,6 +17,9 @@ namespace gadgets
{
static constexpr double lt_target_zero = 0;
/// @brief Alias of another cleartext functor, under the name `negative`.
/// @see grotto::exact_constant
/// \complexity Same as the functor it aliases: one straight-line `operator()`. `Θ(1)`.
using negative HEDLEY_DEPRECATED_FOR(2026, grotto::exact_constant) = lt<lt_target_zero>;
} // namespace gadgets

View file

@ -19,9 +19,19 @@ namespace grotto
namespace gadgets
{
/// @brief Cleartext `neq`.
/// @see grotto::make_nonzero_lut
/// @tparam target functor parameter (a `double` bound or scale)
/// @note Straight-line cleartext. Fixed-point evaluation is the symbol in `@see` when that symbol is a LUT or an `eval_*` function.
/// \complexity One evaluation of `operator()` as written in this header. There is no domain scan and no loop. `Θ(1)` time and extra space relative to that expression.
template <double const & target>
struct neq
{
/// @brief Evaluate the cleartext map.
/// @tparam T argument and result type
/// @param x the input
/// @return the cleartext image of `x`
/// \complexity The body is straight-line. `Θ(1)` time and extra space.
template <typename T>
T operator()(T x) { return x != target; }
};

View file

@ -17,6 +17,9 @@ namespace gadgets
{
static constexpr double geq_target_zero = 0;
/// @brief Alias of another cleartext functor, under the name `nonnegative`.
/// @see grotto::exact_constant
/// \complexity Same as the functor it aliases: one straight-line `operator()`. `Θ(1)`.
using nonnegative HEDLEY_DEPRECATED_FOR(2026, grotto::exact_constant) = geq<geq_target_zero>;
} // namespace gadgets

View file

@ -17,6 +17,9 @@ namespace gadgets
{
static constexpr double leq_target_zero = 0;
/// @brief Alias of another cleartext functor, under the name `nonpositive`.
/// @see grotto::exact_constant
/// \complexity Same as the functor it aliases: one straight-line `operator()`. `Θ(1)`.
using nonpositive HEDLEY_DEPRECATED_FOR(2026, grotto::exact_constant) = leq<leq_target_zero>;
} // namespace gadgets

View file

@ -17,6 +17,9 @@ namespace gadgets
{
static constexpr double neq_target_zero = 0;
/// @brief Alias of another cleartext functor, under the name `nonzero`.
/// @see grotto::exact_constant
/// \complexity Same as the functor it aliases: one straight-line `operator()`. `Θ(1)`.
using nonzero HEDLEY_DEPRECATED_FOR(2026, grotto::exact_constant) = neq<neq_target_zero>;
} // namespace gadgets

View file

@ -16,6 +16,10 @@ namespace gadgets
/// @note This header declares no callable.
/// Complexity is stated on the sibling that evaluates the map, when one exists.
/// @see grotto::gadget_hints
/// @note No fixed-point table in `eval_closed`, `eval_window`, or `eval_reduced` is named after this file. Complexity is not stated here.
} // namespace gadgets
} // namespace grotto

View file

@ -17,6 +17,9 @@ namespace gadgets
{
static constexpr double gt_target_zero = 0;
/// @brief Alias of another cleartext functor, under the name `positive`.
/// @see grotto::exact_constant
/// \complexity Same as the functor it aliases: one straight-line `operator()`. `Θ(1)`.
using positive HEDLEY_DEPRECATED_FOR(2026, grotto::exact_constant) = gt<gt_target_zero>;
} // namespace gadgets

View file

@ -19,8 +19,17 @@ namespace grotto
namespace gadgets
{
/// @brief Cleartext `rect`.
/// @see grotto::make_zero_lut
/// @note Straight-line cleartext. Fixed-point evaluation is the symbol in `@see` when that symbol is a LUT or an `eval_*` function.
/// \complexity One evaluation of `operator()` as written in this header. There is no domain scan and no loop. `Θ(1)` time and extra space relative to that expression.
struct rect
{
/// @brief Evaluate the cleartext map.
/// @tparam T argument and result type
/// @param x the input
/// @return the cleartext image of `x`
/// \complexity The body is straight-line. `Θ(1)` time and extra space.
template <typename T>
T operator()(T x) { return x == 0; }
};

View file

@ -22,11 +22,23 @@ namespace gadgets
static constexpr double step_default_before = 0;
static constexpr double step_default_after = 1;
/// @brief Cleartext `step`.
/// @see grotto::make_exact_step_lut
/// @tparam after functor parameter (a `double` bound or scale)
/// @tparam before functor parameter (a `double` bound or scale)
/// @tparam at functor parameter (a `double` bound or scale)
/// @note Straight-line cleartext. Fixed-point evaluation is the symbol in `@see` when that symbol is a LUT or an `eval_*` function.
/// \complexity One evaluation of `operator()` as written in this header. There is no domain scan and no loop. `Θ(1)` time and extra space relative to that expression.
template <double const & at,
double const & before = step_default_before,
double const & after = step_default_after>
struct step
{
/// @brief Evaluate the cleartext map.
/// @tparam T argument and result type
/// @param x the input
/// @return the cleartext image of `x`
/// \complexity The body is straight-line. `Θ(1)` time and extra space.
template <typename T>
T operator()(T x) { return x < at ? before : after; }
};

View file

@ -16,6 +16,10 @@ namespace gadgets
/// @note This header declares no callable.
/// Complexity is stated on the sibling that evaluates the map, when one exists.
/// @see grotto::gadget_hints
/// @note No fixed-point table in `eval_closed`, `eval_window`, or `eval_reduced` is named after this file. Complexity is not stated here.
} // namespace gadgets
} // namespace grotto

View file

@ -17,6 +17,9 @@ namespace gadgets
{
static constexpr double eq_target_zero = 0;
/// @brief Alias of another cleartext functor, under the name `zero`.
/// @see grotto::exact_constant
/// \complexity Same as the functor it aliases: one straight-line `operator()`. `Θ(1)`.
using zero HEDLEY_DEPRECATED_FOR(2026, grotto::exact_constant) = eq<eq_target_zero>;
} // namespace gadgets

View file

@ -24,8 +24,17 @@ HEDLEY_DIAGNOSTIC_PUSH
HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED
/// @brief Cleartext `exp`.
/// @see grotto::eval_reduced
/// @note Straight-line cleartext. Fixed-point evaluation is the symbol in `@see` when that symbol is a LUT or an `eval_*` function.
/// \complexity One evaluation of `operator()` as written in this header. There is no domain scan and no loop. `Θ(1)` time and extra space relative to that expression.
struct HEDLEY_DEPRECATED_FOR(2026, grotto::eval_reduced) exp
{
/// @brief Evaluate the cleartext map.
/// @tparam T argument and result type
/// @param x the input
/// @return the cleartext image of `x`
/// \complexity The body is straight-line. `Θ(1)` time and extra space.
template <typename T>
T operator()(T x) { return std::min(std::exp(static_cast<double>(x)), std::numeric_limits<T>::max()); }
};

View file

@ -24,8 +24,17 @@ HEDLEY_DIAGNOSTIC_PUSH
HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED
/// @brief Cleartext `exp10`.
/// @see grotto::eval_reduced
/// @note Straight-line cleartext. Fixed-point evaluation is the symbol in `@see` when that symbol is a LUT or an `eval_*` function.
/// \complexity One evaluation of `operator()` as written in this header. There is no domain scan and no loop. `Θ(1)` time and extra space relative to that expression.
struct HEDLEY_DEPRECATED_FOR(2026, grotto::eval_reduced) exp10
{
/// @brief Evaluate the cleartext map.
/// @tparam T argument and result type
/// @param x the input
/// @return the cleartext image of `x`
/// \complexity The body is straight-line. `Θ(1)` time and extra space.
template <typename T>
T operator()(T x) { return std::min(std::pow(10, static_cast<double>(x)), std::numeric_limits<T>::max()); }
};

View file

@ -24,8 +24,17 @@ HEDLEY_DIAGNOSTIC_PUSH
HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED
/// @brief Cleartext `exp2`.
/// @see grotto::eval_reduced
/// @note Straight-line cleartext. Fixed-point evaluation is the symbol in `@see` when that symbol is a LUT or an `eval_*` function.
/// \complexity One evaluation of `operator()` as written in this header. There is no domain scan and no loop. `Θ(1)` time and extra space relative to that expression.
struct HEDLEY_DEPRECATED_FOR(2026, grotto::eval_reduced) exp2
{
/// @brief Evaluate the cleartext map.
/// @tparam T argument and result type
/// @param x the input
/// @return the cleartext image of `x`
/// \complexity The body is straight-line. `Θ(1)` time and extra space.
template <typename T>
T operator()(T x) { return std::min(std::exp2(static_cast<double>(x)), std::numeric_limits<T>::max()); }
};

View file

@ -22,8 +22,17 @@ namespace gadgets
// Returns the inverse hyperbolic cosine of a number.
// The number must be greater than or equal to 1.
/// @brief Cleartext `acosh`.
/// @see grotto::eval_closed
/// @note Straight-line cleartext. Fixed-point evaluation is the symbol in `@see` when that symbol is a LUT or an `eval_*` function.
/// \complexity One evaluation of `operator()` as written in this header. There is no domain scan and no loop. `Θ(1)` time and extra space relative to that expression.
struct acosh
{
/// @brief Evaluate the cleartext map.
/// @tparam T argument and result type
/// @param x the input
/// @return the cleartext image of `x`
/// \complexity The body is straight-line. `Θ(1)` time and extra space.
template <typename T>
T operator()(T x) {
if (x < 1) return -1; // TODO

View file

@ -20,8 +20,17 @@ namespace grotto
namespace gadgets
{
/// @brief Cleartext `acoth`.
/// @see grotto::eval_closed
/// @note Straight-line cleartext. Fixed-point evaluation is the symbol in `@see` when that symbol is a LUT or an `eval_*` function.
/// \complexity One evaluation of `operator()` as written in this header. There is no domain scan and no loop. `Θ(1)` time and extra space relative to that expression.
struct acoth
{
/// @brief Evaluate the cleartext map.
/// @tparam T argument and result type
/// @param x the input
/// @return the cleartext image of `x`
/// \complexity The body is straight-line. `Θ(1)` time and extra space.
template <typename T>
T operator()(T x) {
if (std::abs(x) <= 1) return 0; // TODO

View file

@ -20,8 +20,17 @@ namespace grotto
namespace gadgets
{
/// @brief Cleartext `acsch`.
/// @see grotto::eval_closed
/// @note Straight-line cleartext. Fixed-point evaluation is the symbol in `@see` when that symbol is a LUT or an `eval_*` function.
/// \complexity One evaluation of `operator()` as written in this header. There is no domain scan and no loop. `Θ(1)` time and extra space relative to that expression.
struct acsch
{
/// @brief Evaluate the cleartext map.
/// @tparam T argument and result type
/// @param x the input
/// @return the cleartext image of `x`
/// \complexity The body is straight-line. `Θ(1)` time and extra space.
template <typename T>
T operator()(T x)
{

View file

@ -20,8 +20,17 @@ namespace grotto
namespace gadgets
{
/// @brief Cleartext `asech`.
/// @see grotto::eval_closed
/// @note Straight-line cleartext. Fixed-point evaluation is the symbol in `@see` when that symbol is a LUT or an `eval_*` function.
/// \complexity One evaluation of `operator()` as written in this header. There is no domain scan and no loop. `Θ(1)` time and extra space relative to that expression.
struct asech
{
/// @brief Evaluate the cleartext map.
/// @tparam T argument and result type
/// @param x the input
/// @return the cleartext image of `x`
/// \complexity The body is straight-line. `Θ(1)` time and extra space.
template <typename T>
T operator()(T x)
{

View file

@ -20,8 +20,17 @@ namespace grotto
namespace gadgets
{
/// @brief Cleartext `asinh`.
/// @see grotto::eval_closed
/// @note Straight-line cleartext. Fixed-point evaluation is the symbol in `@see` when that symbol is a LUT or an `eval_*` function.
/// \complexity One evaluation of `operator()` as written in this header. There is no domain scan and no loop. `Θ(1)` time and extra space relative to that expression.
struct asinh
{
/// @brief Evaluate the cleartext map.
/// @tparam T argument and result type
/// @param x the input
/// @return the cleartext image of `x`
/// \complexity The body is straight-line. `Θ(1)` time and extra space.
template <typename T>
T operator()(T x) { return std::asinh(static_cast<double>(x)); }
};

View file

@ -20,8 +20,17 @@ namespace grotto
namespace gadgets
{
/// @brief Cleartext `atanh`.
/// @see grotto::eval_closed
/// @note Straight-line cleartext. Fixed-point evaluation is the symbol in `@see` when that symbol is a LUT or an `eval_*` function.
/// \complexity One evaluation of `operator()` as written in this header. There is no domain scan and no loop. `Θ(1)` time and extra space relative to that expression.
struct atanh
{
/// @brief Evaluate the cleartext map.
/// @tparam T argument and result type
/// @param x the input
/// @return the cleartext image of `x`
/// \complexity The body is straight-line. `Θ(1)` time and extra space.
template <typename T>
T operator()(T x)
{

View file

@ -24,8 +24,17 @@ HEDLEY_DIAGNOSTIC_PUSH
HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED
/// @brief Cleartext `cosh`.
/// @see grotto::eval_reduced
/// @note Straight-line cleartext. Fixed-point evaluation is the symbol in `@see` when that symbol is a LUT or an `eval_*` function.
/// \complexity One evaluation of `operator()` as written in this header. There is no domain scan and no loop. `Θ(1)` time and extra space relative to that expression.
struct HEDLEY_DEPRECATED_FOR(2026, grotto::eval_reduced) cosh
{
/// @brief Evaluate the cleartext map.
/// @tparam T argument and result type
/// @param x the input
/// @return the cleartext image of `x`
/// \complexity The body is straight-line. `Θ(1)` time and extra space.
template <typename T>
T operator()(T x) { return std::min(std::cosh(static_cast<double>(x)), std::numeric_limits<T>::max()); }
};

View file

@ -24,8 +24,17 @@ HEDLEY_DIAGNOSTIC_PUSH
HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED
/// @brief Cleartext `coth`.
/// @see grotto::eval_reduced
/// @note Straight-line cleartext. Fixed-point evaluation is the symbol in `@see` when that symbol is a LUT or an `eval_*` function.
/// \complexity One evaluation of `operator()` as written in this header. There is no domain scan and no loop. `Θ(1)` time and extra space relative to that expression.
struct HEDLEY_DEPRECATED_FOR(2026, grotto::eval_reduced) coth
{
/// @brief Evaluate the cleartext map.
/// @tparam T argument and result type
/// @param x the input
/// @return the cleartext image of `x`
/// \complexity The body is straight-line. `Θ(1)` time and extra space.
template <typename T>
T operator()(T x)
{

View file

@ -24,8 +24,17 @@ HEDLEY_DIAGNOSTIC_PUSH
HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED
/// @brief Cleartext `csch`.
/// @see grotto::eval_reduced
/// @note Straight-line cleartext. Fixed-point evaluation is the symbol in `@see` when that symbol is a LUT or an `eval_*` function.
/// \complexity One evaluation of `operator()` as written in this header. There is no domain scan and no loop. `Θ(1)` time and extra space relative to that expression.
struct HEDLEY_DEPRECATED_FOR(2026, grotto::eval_reduced) csch
{
/// @brief Evaluate the cleartext map.
/// @tparam T argument and result type
/// @param x the input
/// @return the cleartext image of `x`
/// \complexity The body is straight-line. `Θ(1)` time and extra space.
template <typename T>
T operator()(T x)
{

View file

@ -24,8 +24,17 @@ HEDLEY_DIAGNOSTIC_PUSH
HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED
/// @brief Cleartext `sech`.
/// @see grotto::eval_reduced
/// @note Straight-line cleartext. Fixed-point evaluation is the symbol in `@see` when that symbol is a LUT or an `eval_*` function.
/// \complexity One evaluation of `operator()` as written in this header. There is no domain scan and no loop. `Θ(1)` time and extra space relative to that expression.
struct HEDLEY_DEPRECATED_FOR(2026, grotto::eval_reduced) sech
{
/// @brief Evaluate the cleartext map.
/// @tparam T argument and result type
/// @param x the input
/// @return the cleartext image of `x`
/// \complexity The body is straight-line. `Θ(1)` time and extra space.
template <typename T>
T operator()(T x) { return 1/std::cosh(static_cast<double>(x)); }
};

View file

@ -24,8 +24,17 @@ HEDLEY_DIAGNOSTIC_PUSH
HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED
/// @brief Cleartext `sinh`.
/// @see grotto::eval_reduced
/// @note Straight-line cleartext. Fixed-point evaluation is the symbol in `@see` when that symbol is a LUT or an `eval_*` function.
/// \complexity One evaluation of `operator()` as written in this header. There is no domain scan and no loop. `Θ(1)` time and extra space relative to that expression.
struct HEDLEY_DEPRECATED_FOR(2026, grotto::eval_reduced) sinh
{
/// @brief Evaluate the cleartext map.
/// @tparam T argument and result type
/// @param x the input
/// @return the cleartext image of `x`
/// \complexity The body is straight-line. `Θ(1)` time and extra space.
template <typename T>
T operator()(T x)
{

View file

@ -24,8 +24,17 @@ HEDLEY_DIAGNOSTIC_PUSH
HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED
/// @brief Cleartext `tanh`.
/// @see grotto::eval_reduced
/// @note Straight-line cleartext. Fixed-point evaluation is the symbol in `@see` when that symbol is a LUT or an `eval_*` function.
/// \complexity One evaluation of `operator()` as written in this header. There is no domain scan and no loop. `Θ(1)` time and extra space relative to that expression.
struct HEDLEY_DEPRECATED_FOR(2026, grotto::eval_reduced) tanh
{
/// @brief Evaluate the cleartext map.
/// @tparam T argument and result type
/// @param x the input
/// @return the cleartext image of `x`
/// \complexity The body is straight-line. `Θ(1)` time and extra space.
template <typename T>
T operator()(T x) { return std::tanh(static_cast<double>(x)); }
};

View file

@ -24,8 +24,17 @@ namespace gadgets
HEDLEY_DIAGNOSTIC_PUSH
HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED
/// @brief Cleartext `ilog10`.
/// @see grotto::exact_constant
/// @note Straight-line cleartext. Fixed-point evaluation is the symbol in `@see` when that symbol is a LUT or an `eval_*` function.
/// \complexity One evaluation of `operator()` as written in this header. There is no domain scan and no loop. `Θ(1)` time and extra space relative to that expression.
struct HEDLEY_DEPRECATED_FOR(2026, grotto::exact_constant) ilog10
{
/// @brief Evaluate the cleartext map.
/// @tparam T argument and result type
/// @param x the input
/// @return the cleartext image of `x`
/// \complexity The body is straight-line. `Θ(1)` time and extra space.
template <typename T>
T operator()(T x)
{

View file

@ -20,8 +20,17 @@ namespace grotto
namespace gadgets
{
/// @brief Cleartext `ilog16`.
/// @see grotto::eval_ilog16
/// @note Straight-line cleartext. Fixed-point evaluation is the symbol in `@see` when that symbol is a LUT or an `eval_*` function.
/// \complexity One evaluation of `operator()` as written in this header. There is no domain scan and no loop. `Θ(1)` time and extra space relative to that expression.
struct ilog16
{
/// @brief Evaluate the cleartext map.
/// @tparam T argument and result type
/// @param x the input
/// @return the cleartext image of `x`
/// \complexity The body is straight-line. `Θ(1)` time and extra space.
template <typename T>
T operator()(T x)
{

View file

@ -20,8 +20,17 @@ namespace grotto
namespace gadgets
{
/// @brief Cleartext `ilog256`.
/// @see grotto::eval_ilog256
/// @note Straight-line cleartext. Fixed-point evaluation is the symbol in `@see` when that symbol is a LUT or an `eval_*` function.
/// \complexity One evaluation of `operator()` as written in this header. There is no domain scan and no loop. `Θ(1)` time and extra space relative to that expression.
struct ilog256
{
/// @brief Evaluate the cleartext map.
/// @tparam T argument and result type
/// @param x the input
/// @return the cleartext image of `x`
/// \complexity The body is straight-line. `Θ(1)` time and extra space.
template <typename T>
T operator()(T x)
{

View file

@ -24,8 +24,17 @@ HEDLEY_DIAGNOSTIC_PUSH
HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED
/// @brief Cleartext `ilogb`.
/// @see grotto::exact_constant
/// @note Straight-line cleartext. Fixed-point evaluation is the symbol in `@see` when that symbol is a LUT or an `eval_*` function.
/// \complexity One evaluation of `operator()` as written in this header. There is no domain scan and no loop. `Θ(1)` time and extra space relative to that expression.
struct HEDLEY_DEPRECATED_FOR(2026, grotto::exact_constant) ilogb
{
/// @brief Evaluate the cleartext map.
/// @tparam T argument and result type
/// @param x the input
/// @return the cleartext image of `x`
/// \complexity The body is straight-line. `Θ(1)` time and extra space.
template <typename T>
T operator()(T x)
{

View file

@ -24,8 +24,17 @@ HEDLEY_DIAGNOSTIC_PUSH
HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED
/// @brief Cleartext `lg`.
/// @see grotto::eval_reduced
/// @note Straight-line cleartext. Fixed-point evaluation is the symbol in `@see` when that symbol is a LUT or an `eval_*` function.
/// \complexity One evaluation of `operator()` as written in this header. There is no domain scan and no loop. `Θ(1)` time and extra space relative to that expression.
struct HEDLEY_DEPRECATED_FOR(2026, grotto::eval_reduced) lg
{
/// @brief Evaluate the cleartext map.
/// @tparam T argument and result type
/// @param x the input
/// @return the cleartext image of `x`
/// \complexity The body is straight-line. `Θ(1)` time and extra space.
template <typename T>
T operator()(T x)
{

View file

@ -24,8 +24,17 @@ HEDLEY_DIAGNOSTIC_PUSH
HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED
/// @brief Cleartext `ln`.
/// @see grotto::eval_reduced
/// @note Straight-line cleartext. Fixed-point evaluation is the symbol in `@see` when that symbol is a LUT or an `eval_*` function.
/// \complexity One evaluation of `operator()` as written in this header. There is no domain scan and no loop. `Θ(1)` time and extra space relative to that expression.
struct HEDLEY_DEPRECATED_FOR(2026, grotto::eval_reduced) ln
{
/// @brief Evaluate the cleartext map.
/// @tparam T argument and result type
/// @param x the input
/// @return the cleartext image of `x`
/// \complexity The body is straight-line. `Θ(1)` time and extra space.
template <typename T>
T operator()(T x)
{

View file

@ -24,8 +24,17 @@ HEDLEY_DIAGNOSTIC_PUSH
HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED
/// @brief Cleartext `log10`.
/// @see grotto::eval_reduced
/// @note Straight-line cleartext. Fixed-point evaluation is the symbol in `@see` when that symbol is a LUT or an `eval_*` function.
/// \complexity One evaluation of `operator()` as written in this header. There is no domain scan and no loop. `Θ(1)` time and extra space relative to that expression.
struct HEDLEY_DEPRECATED_FOR(2026, grotto::eval_reduced) log10
{
/// @brief Evaluate the cleartext map.
/// @tparam T argument and result type
/// @param x the input
/// @return the cleartext image of `x`
/// \complexity The body is straight-line. `Θ(1)` time and extra space.
template <typename T>
T operator()(T x)
{

View file

@ -16,6 +16,10 @@ namespace gadgets
/// @note This header declares no callable.
/// Complexity is stated on the sibling that evaluates the map, when one exists.
/// @see grotto::gadget_hints
/// @note No fixed-point table in `eval_closed`, `eval_window`, or `eval_reduced` is named after this file. Complexity is not stated here.
} // namespace gadgets
} // namespace grotto

View file

@ -16,6 +16,9 @@ namespace gadgets
/// @note This header declares no callable.
/// Complexity is stated on the sibling that evaluates the map, when one exists.
/// @see grotto::eval_logstar
} // namespace gadgets
} // namespace grotto

Some files were not shown because too many files have changed in this diff Show more