use crate::clmul::TAIL_128;
pub(crate) const LOW_BY_LOW: i32 = 0x00;
pub(crate) const HIGH_BY_HIGH: i32 = 0x11;
pub(crate) const HIGH_BY_LOW: i32 = 0x01;
pub(crate) const LOW_BY_HIGH: i32 = 0x10;
pub(crate) trait Lanes: Copy {
fn zero() -> Self;
fn broadcast(value: u128) -> Self;
#[inline(always)]
fn tail() -> Self {
Self::broadcast(TAIL_128)
}
fn xor(self, other: Self) -> Self;
fn unpack_low_64(self, other: Self) -> Self;
fn clmul<const IMM: i32>(self, other: Self) -> Self;
}
#[inline(always)]
pub(crate) fn fold_shifted<L: Lanes>(t0: L, t1: L) -> L {
let raised = L::zero().unpack_low_64(t1);
let folded = t1.clmul::<HIGH_BY_LOW>(L::tail());
t0.xor(raised.xor(folded))
}
#[derive(Clone, Copy)]
pub(crate) struct SplitScalar<L> {
t: L,
t_x64: L,
}
impl<L: Lanes> SplitScalar<L> {
#[inline(always)]
pub(crate) fn new(scalar: u128) -> Self {
let t = L::broadcast(scalar);
let t_x64 = fold_shifted(L::zero(), t);
Self { t, t_x64 }
}
#[cfg(any(
test,
all(
target_arch = "x86_64",
target_feature = "vpclmulqdq",
target_feature = "avx512f"
)
))]
#[inline(always)]
pub(crate) const fn from_parts(t: L, t_x64: L) -> Self {
Self { t, t_x64 }
}
#[inline(always)]
pub(crate) fn apply(self, v: L) -> L {
let low = v
.clmul::<LOW_BY_LOW>(self.t)
.xor(v.clmul::<HIGH_BY_LOW>(self.t_x64));
let high = v
.clmul::<LOW_BY_HIGH>(self.t)
.xor(v.clmul::<HIGH_BY_HIGH>(self.t_x64));
fold_shifted(low, high)
}
}
#[cfg(test)]
pub(crate) mod model {
use super::Lanes;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct Model<const LANES: usize>(pub(crate) [u128; LANES]);
fn carryless_64(x: u64, y: u64) -> u128 {
let mut acc = 0u128;
for i in 0..64 {
if (y >> i) & 1 == 1 {
acc ^= (x as u128) << i;
}
}
acc
}
impl<const LANES: usize> Lanes for Model<LANES> {
fn zero() -> Self {
Self([0; LANES])
}
fn broadcast(value: u128) -> Self {
Self([value; LANES])
}
fn xor(self, other: Self) -> Self {
Self(core::array::from_fn(|i| self.0[i] ^ other.0[i]))
}
fn unpack_low_64(self, other: Self) -> Self {
Self(core::array::from_fn(|i| {
(self.0[i] & u64::MAX as u128) | (other.0[i] << 64)
}))
}
fn clmul<const IMM: i32>(self, other: Self) -> Self {
Self(core::array::from_fn(|i| {
let x = if IMM & 0x01 == 0 {
self.0[i] as u64
} else {
(self.0[i] >> 64) as u64
};
let y = if IMM & 0x10 == 0 {
other.0[i] as u64
} else {
(other.0[i] >> 64) as u64
};
carryless_64(x, y)
}))
}
}
}
#[cfg(test)]
mod tests {
use proptest::prelude::*;
use super::model::Model;
use super::{Lanes, SplitScalar, fold_shifted};
use crate::clmul;
const LANES: usize = 2;
const CORNERS: [u128; 7] = [0, 1, u128::MAX, 1 << 127, 1 << 64, (1u128 << 64) - 1, 0x87];
fn split_agrees(scalar: u128, values: [u128; LANES]) -> Result<(), TestCaseError> {
let got = SplitScalar::new(scalar).apply(Model::<LANES>(values));
let want = core::array::from_fn(|i| clmul::poly_mul_128(scalar, values[i]));
prop_assert_eq!(got, Model(want));
Ok(())
}
#[test]
fn the_split_product_agrees_with_the_scalar_backend_at_the_corners() {
for scalar in CORNERS {
for value in CORNERS {
for other in CORNERS {
split_agrees(scalar, [value, other])
.unwrap_or_else(|e| panic!("scalar {scalar:#x}: {e}"));
}
}
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(512))]
#[test]
fn the_split_product_agrees_with_the_scalar_backend(
scalar in any::<u128>(),
values in any::<[u128; LANES]>(),
) {
split_agrees(scalar, values)?;
}
#[test]
fn the_companion_is_the_shifted_multiplier(scalar in any::<u128>()) {
let companion = fold_shifted(Model::<LANES>::zero(), Model::broadcast(scalar));
let want = clmul::poly_mul_128(scalar, 1 << 64);
prop_assert_eq!(companion, Model::broadcast(want));
}
#[test]
fn a_multiplier_from_its_parts_scales_each_lane_by_its_own(
scalars in any::<[u128; LANES]>(),
values in any::<[u128; LANES]>(),
) {
let t = Model(scalars);
let got = SplitScalar::from_parts(t, fold_shifted(Model::zero(), t)).apply(Model(values));
let want = core::array::from_fn(|i| clmul::poly_mul_128(scalars[i], values[i]));
prop_assert_eq!(got, Model(want));
}
}
}