Skip to main content

zinc_poly/univariate/
binary_u64.rs

1use crate::{
2    ConstCoeffBitWidth, EvaluatablePolynomial, EvaluationError, Polynomial,
3    univariate::{dense::DensePolynomial, prepare_projection},
4};
5use core::mem::MaybeUninit;
6use crypto_primitives::{FieldConfig, Wrapper, boolean::Boolean};
7use derive_more::{AsRef, Display};
8use num_traits::{CheckedAdd, CheckedMul, One, Zero};
9use rand::{distr::StandardUniform, prelude::*};
10use std::{
11    array,
12    hash::Hash,
13    marker::PhantomData,
14    ops::{Add, Mul},
15};
16use zinc_transcript::{delegate_const_transcribable, traits::ConstTranscribable};
17use zinc_utils::{
18    from_ref::FromRef,
19    inner_product::{InnerProduct, InnerProductError},
20    mul_by_scalar::MulByScalar,
21    named::Named,
22    projectable_to_field::ProjectableToField,
23};
24
25#[derive(AsRef, Clone, Debug, Default, Display, Hash, PartialEq, Eq)]
26#[repr(transparent)]
27pub struct BinaryU64Poly<const DEGREE_PLUS_ONE: usize>(u64); // we can fit up to degree 64, which is ok for now
28
29impl<const DEGREE_PLUS_ONE: usize> BinaryU64Poly<DEGREE_PLUS_ONE> {
30    const _DEGREE_CHECK: () = {
31        // Note that associated constants on generic types are evaluated lazily, so we
32        // have to "access" this to trigger the assertion.
33        assert!(
34            DEGREE_PLUS_ONE <= 64,
35            "For BinaryU64Poly, degree cannot exceed 63"
36        );
37    };
38
39    #[inline(always)]
40    #[allow(clippy::let_unit_value)]
41    pub const fn inner(&self) -> &u64 {
42        let _ = Self::_DEGREE_CHECK; // Force lazy evaluation of constraint
43        &self.0
44    }
45}
46
47impl<const DEGREE_PLUS_ONE: usize> From<BinaryU64Poly<DEGREE_PLUS_ONE>> for u64 {
48    #[inline(always)]
49    fn from(binary_poly: BinaryU64Poly<DEGREE_PLUS_ONE>) -> Self {
50        binary_poly.0
51    }
52}
53
54impl<const DEGREE_PLUS_ONE: usize> From<BinaryU64Poly<DEGREE_PLUS_ONE>>
55    for DensePolynomial<Boolean, DEGREE_PLUS_ONE>
56{
57    #[inline(always)]
58    fn from(binary_poly: BinaryU64Poly<DEGREE_PLUS_ONE>) -> Self {
59        DensePolynomial {
60            coeffs: array::from_fn(|i| Boolean::new(!(binary_poly.0 & (1 << i)).is_zero())),
61        }
62    }
63}
64
65impl<const DEGREE_PLUS_ONE: usize> From<u32> for BinaryU64Poly<DEGREE_PLUS_ONE> {
66    #[inline(always)]
67    fn from(value: u32) -> Self {
68        // Keep masking enforced in a single place
69        Self::from(u64::from(value))
70    }
71}
72
73impl<const DEGREE_PLUS_ONE: usize> From<u64> for BinaryU64Poly<DEGREE_PLUS_ONE> {
74    #[inline(always)]
75    #[allow(clippy::arithmetic_side_effects, clippy::let_unit_value)]
76    fn from(value: u64) -> Self {
77        let _ = Self::_DEGREE_CHECK; // Force lazy evaluation of constraint
78
79        // Bit `i` of `value` becomes the coefficient of `X^i`.
80        if DEGREE_PLUS_ONE == 64 {
81            Self(value)
82        } else {
83            // Bits at positions `>= DEGREE_PLUS_ONE` are masked off so that no bits beyond
84            // the polynomial degree are set. This is important, as downstream
85            // code (such as `BinaryU64PolyInnerProduct::inner_product`) relies on it.
86            Self(value & ((1_u64 << DEGREE_PLUS_ONE) - 1))
87        }
88    }
89}
90
91impl<const DEGREE_PLUS_ONE: usize> BinaryU64Poly<DEGREE_PLUS_ONE> {
92    #[inline(always)]
93    pub fn new(coeffs: [Boolean; DEGREE_PLUS_ONE]) -> Self {
94        BinaryU64Poly::new_padded(coeffs)
95    }
96
97    /// Create a new polynomial with the given coefficients.
98    /// If the input has fewer than N+1 coefficients, the remaining slots will
99    /// be filled with zeros. If the input has more than N+1 coefficients,
100    /// it will panic.
101    #[inline(always)]
102    pub fn new_padded(coeffs: impl AsRef<[Boolean]>) -> Self {
103        let coeffs = coeffs.as_ref();
104        assert!(coeffs.len() <= DEGREE_PLUS_ONE);
105        let mut value: u64 = 0;
106        for (i, coeff) in coeffs.iter().enumerate() {
107            if *coeff.inner() {
108                value |= 1 << i;
109            }
110        }
111        Self(value)
112    }
113}
114
115impl<const DEGREE_PLUS_ONE: usize> Zero for BinaryU64Poly<DEGREE_PLUS_ONE> {
116    #[inline(always)]
117    fn zero() -> Self {
118        Self(0)
119    }
120
121    #[inline(always)]
122    fn is_zero(&self) -> bool {
123        self.0.is_zero()
124    }
125}
126
127impl<const DEGREE_PLUS_ONE: usize> One for BinaryU64Poly<DEGREE_PLUS_ONE> {
128    #[inline(always)]
129    fn one() -> Self {
130        Self(1)
131    }
132}
133
134impl<'a, const DEGREE_PLUS_ONE: usize> Add<&'a Self> for BinaryU64Poly<DEGREE_PLUS_ONE> {
135    type Output = Self;
136
137    #[allow(clippy::arithmetic_side_effects, clippy::suspicious_arithmetic_impl)]
138    #[inline(always)]
139    fn add(self, rhs: &'a Self) -> Self::Output {
140        // addition in GF(2) is XOR
141        Self(self.0 ^ rhs.0)
142    }
143}
144
145impl<const DEGREE_PLUS_ONE: usize> Add for BinaryU64Poly<DEGREE_PLUS_ONE> {
146    type Output = Self;
147
148    #[inline(always)]
149    fn add(self, _rhs: Self) -> Self::Output {
150        panic!("Binary polynomials addition is not defined")
151    }
152}
153
154impl<const DEGREE_PLUS_ONE: usize> Mul for BinaryU64Poly<DEGREE_PLUS_ONE> {
155    type Output = Self;
156
157    #[inline(always)]
158    fn mul(self, _rhs: Self) -> Self::Output {
159        panic!("Binary polynomials multiplication is not defined")
160    }
161}
162
163impl<const DEGREE_PLUS_ONE: usize> Distribution<BinaryU64Poly<DEGREE_PLUS_ONE>>
164    for StandardUniform
165{
166    #[inline(always)]
167    #[allow(clippy::arithmetic_side_effects)]
168    fn sample<Gen: Rng + ?Sized>(&self, rng: &mut Gen) -> BinaryU64Poly<DEGREE_PLUS_ONE> {
169        let mut value: u64 = rng.next_u64();
170        if DEGREE_PLUS_ONE < 64 {
171            value &= (1u64 << DEGREE_PLUS_ONE) - 1;
172        }
173        BinaryU64Poly::<DEGREE_PLUS_ONE>(value)
174    }
175}
176
177//
178// Zip-specific traits
179//
180impl<const DEGREE_PLUS_ONE: usize> Polynomial<Boolean> for BinaryU64Poly<DEGREE_PLUS_ONE> {
181    const DEGREE_BOUND: usize = DensePolynomial::<Boolean, DEGREE_PLUS_ONE>::DEGREE_BOUND;
182}
183
184#[allow(clippy::arithmetic_side_effects)]
185impl<R: Clone + Zero + One + CheckedAdd + CheckedMul, const DEGREE_PLUS_ONE: usize>
186    EvaluatablePolynomial<Boolean, R> for BinaryU64Poly<DEGREE_PLUS_ONE>
187{
188    type EvaluationPoint = R;
189
190    fn evaluate_at_point(&self, point: &R) -> Result<R, EvaluationError> {
191        if DEGREE_PLUS_ONE.is_one() {
192            return Ok(R::zero());
193        }
194
195        let mut result = R::zero();
196        let mut pow = R::one();
197        for i in 0..DEGREE_PLUS_ONE {
198            if (self.0 & (1 << i)) != 0 {
199                result = result.checked_add(&pow).ok_or(EvaluationError::Overflow)?;
200            }
201            if i + 1 < DEGREE_PLUS_ONE {
202                pow = pow.checked_mul(point).ok_or(EvaluationError::Overflow)?;
203            }
204        }
205
206        Ok(result)
207    }
208}
209
210impl<const DEGREE_PLUS_ONE: usize> ConstCoeffBitWidth for BinaryU64Poly<DEGREE_PLUS_ONE> {
211    const COEFF_BIT_WIDTH: usize = Boolean::NUM_BITS;
212}
213
214impl<const DEGREE_PLUS_ONE: usize> Named for BinaryU64Poly<DEGREE_PLUS_ONE> {
215    fn type_name() -> String {
216        format!("BPoly<{}>", Self::DEGREE_BOUND)
217    }
218}
219
220delegate_const_transcribable!(BinaryU64Poly<const DEGREE_PLUS_ONE: usize>(u64));
221
222impl<const DEGREE_PLUS_ONE: usize> FromRef<BinaryU64Poly<DEGREE_PLUS_ONE>>
223    for BinaryU64Poly<DEGREE_PLUS_ONE>
224{
225    #[inline(always)]
226    fn from_ref(poly: &BinaryU64Poly<DEGREE_PLUS_ONE>) -> Self {
227        poly.clone()
228    }
229}
230
231impl<const DEGREE_PLUS_ONE: usize> From<&BinaryU64Poly<DEGREE_PLUS_ONE>>
232    for BinaryU64Poly<DEGREE_PLUS_ONE>
233{
234    #[inline(always)]
235    fn from(value: &BinaryU64Poly<DEGREE_PLUS_ONE>) -> Self {
236        Self::from_ref(value)
237    }
238}
239
240pub struct BinaryU64PolyIter<'a, const DEGREE_PLUS_ONE: usize> {
241    i: usize,
242    poly: &'a BinaryU64Poly<DEGREE_PLUS_ONE>,
243}
244
245impl<'a, const DEGREE_PLUS_ONE: usize> BinaryU64PolyIter<'a, DEGREE_PLUS_ONE> {
246    pub fn new(poly: &'a BinaryU64Poly<DEGREE_PLUS_ONE>) -> Self {
247        Self { i: 0, poly }
248    }
249}
250
251impl<'a, const DEGREE_PLUS_ONE: usize> Iterator for BinaryU64PolyIter<'a, DEGREE_PLUS_ONE> {
252    type Item = Boolean;
253
254    #[allow(clippy::arithmetic_side_effects)]
255    fn next(&mut self) -> Option<Self::Item> {
256        if self.i < DEGREE_PLUS_ONE {
257            let i = self.i;
258
259            self.i += 1;
260
261            Some((!(self.poly.0 & (1 << i)).is_zero()).into())
262        } else {
263            None
264        }
265    }
266}
267
268impl<const DEGREE_PLUS_ONE: usize> BinaryU64Poly<DEGREE_PLUS_ONE> {
269    pub fn iter(&self) -> BinaryU64PolyIter<'_, DEGREE_PLUS_ONE> {
270        BinaryU64PolyIter::new(self)
271    }
272}
273
274#[derive(Clone, Debug)]
275pub struct BinaryU64PolyInnerProduct<R, const DEGREE_PLUS_ONE: usize>(PhantomData<R>);
276
277impl<C, Rhs, Out, const DEGREE_PLUS_ONE: usize>
278    InnerProduct<C, BinaryU64Poly<DEGREE_PLUS_ONE>, Rhs, Out>
279    for BinaryU64PolyInnerProduct<Rhs, DEGREE_PLUS_ONE>
280where
281    Rhs: Clone,
282    Out: FromRef<Rhs> + CheckedAdd,
283{
284    #[inline(always)]
285    #[allow(clippy::arithmetic_side_effects)] // By design
286    fn inner_product<const CHECK: bool>(
287        _cfg: &C,
288        lhs: &BinaryU64Poly<DEGREE_PLUS_ONE>,
289        rhs: &[Rhs],
290        zero: Out,
291    ) -> Result<Out, InnerProductError> {
292        if rhs.len() != DEGREE_PLUS_ONE {
293            return Err(InnerProductError::LengthMismatch {
294                lhs: DEGREE_PLUS_ONE,
295                rhs: rhs.len(),
296            });
297        }
298
299        let mut acc = zero;
300        let mut bits = lhs.0;
301        while bits != 0 {
302            let i = bits.trailing_zeros() as usize;
303            let rhs = Out::from_ref(&rhs[i]);
304            if CHECK {
305                acc = acc.checked_add(&rhs).ok_or(InnerProductError::Overflow)?;
306            } else {
307                acc = acc + rhs;
308            }
309            // changes the LSB 1 bit to 0
310            bits &= bits - 1;
311        }
312
313        Ok(acc)
314    }
315}
316
317impl<C, const DEGREE_PLUS_ONE: usize> ProjectableToField<C> for BinaryU64Poly<DEGREE_PLUS_ONE>
318where
319    C: FieldConfig + 'static,
320{
321    fn prepare_projection(cfg: &C, sampled_value: &C::Element) -> impl Fn(&Self) -> C::Element {
322        prepare_projection::<C, Self, _, DEGREE_PLUS_ONE>(cfg, sampled_value, |poly, i| {
323            (poly.0 & (1 << i)) != 0
324        })
325    }
326}
327
328impl<const DEGREE_PLUS_ONE: usize> MulByScalar<i64, DensePolynomial<i64, DEGREE_PLUS_ONE>>
329    for BinaryU64Poly<DEGREE_PLUS_ONE>
330{
331    fn mul_by_scalar<const CHECK: bool>(
332        self,
333        rhs: &i64,
334    ) -> Option<DensePolynomial<i64, DEGREE_PLUS_ONE>> {
335        Some(widen_simd::<DEGREE_PLUS_ONE>(&self, *rhs))
336    }
337}
338
339#[allow(unreachable_code, unused_variables)] // CI system does not support SIMD features
340#[inline(always)]
341pub fn widen_simd<const DEGREE_PLUS_ONE: usize>(
342    poly: &BinaryU64Poly<DEGREE_PLUS_ONE>,
343    scalar: i64,
344) -> DensePolynomial<i64, DEGREE_PLUS_ONE> {
345    let mut coeffs_uninit = MaybeUninit::<[i64; DEGREE_PLUS_ONE]>::uninit();
346    let out_ptr = coeffs_uninit.as_mut_ptr() as *mut i64;
347
348    #[cfg(target_arch = "aarch64")]
349    unsafe {
350        widen_fill_neon::<DEGREE_PLUS_ONE>(&poly.0, out_ptr, scalar);
351    }
352    #[cfg(all(target_arch = "x86_64", target_feature = "avx512f"))]
353    unsafe {
354        widen_fill_avx512::<DEGREE_PLUS_ONE>(&poly.0, out_ptr, scalar);
355    }
356    #[cfg(not(any(
357        target_arch = "aarch64",
358        all(target_arch = "x86_64", target_feature = "avx512f")
359    )))]
360    {
361        panic!("SIMD widening not supported on this architecture");
362    }
363
364    let coeffs = unsafe { coeffs_uninit.assume_init() };
365    DensePolynomial { coeffs }
366}
367
368#[allow(
369    clippy::arithmetic_side_effects,
370    clippy::cast_possible_truncation,
371    clippy::cast_lossless
372)]
373#[allow(unsafe_op_in_unsafe_fn)]
374#[cfg(target_arch = "aarch64")]
375#[inline(always)]
376// Converts a u64 bitmask into an array of i64 values using ARM NEON SIMD.
377// Processes 8 bits at a time with ~8-12× speedup vs scalar code.
378unsafe fn widen_fill_neon<const N: usize>(mask_ref: &u64, out_ptr: *mut i64, scalar: i64) {
379    use core::arch::aarch64::*;
380
381    let mask64: u64 = *mask_ref;
382
383    // Replicate scalar across SIMD lanes for branchless selection
384    let scalar_v: int64x2_t = vdupq_n_s64(scalar);
385
386    let mut i = 0usize;
387
388    // For N == 64, use lookup table (16KB). For smaller N, use SIMD widening
389    if N == 64 {
390        // Precomputed lookup table: maps each byte value (0-255) directly to 8 i64
391        // values Each bit in the byte produces -1 (if set) or 0 (if clear)
392        // Table size: 256 entries × 8 i64 × 8 bytes = 16KB
393        #[repr(align(64))]
394        struct LookupTable([[i64; 8]; 256]);
395
396        static LUT: LookupTable = {
397            let mut table = [[0i64; 8]; 256];
398            let mut i = 0;
399            while i < 256 {
400                let mut bit = 0;
401                while bit < 8 {
402                    table[i][bit] = if (i & (1 << bit)) != 0 { -1 } else { 0 };
403                    bit += 1;
404                }
405                i += 1;
406            }
407            LookupTable(table)
408        };
409
410        // Main loop: process 8 coefficients per iteration
411        while i + 8 <= N {
412            let shift = i as u32;
413            let byte: u8 = ((mask64 >> shift) & 0xFF) as u8;
414
415            // Direct table lookup: load 8 i64 values (4 int64x2_t vectors)
416            let masks_ptr = LUT.0[byte as usize].as_ptr();
417            let m0: int64x2_t = vld1q_s64(masks_ptr);
418            let m1: int64x2_t = vld1q_s64(masks_ptr.add(2));
419            let m2: int64x2_t = vld1q_s64(masks_ptr.add(4));
420            let m3: int64x2_t = vld1q_s64(masks_ptr.add(6));
421
422            // Branchless select: scalar & -1 = scalar, scalar & 0 = 0
423            vst1q_s64(out_ptr.add(i), vandq_s64(scalar_v, m0));
424            vst1q_s64(out_ptr.add(i + 2), vandq_s64(scalar_v, m1));
425            vst1q_s64(out_ptr.add(i + 4), vandq_s64(scalar_v, m2));
426            vst1q_s64(out_ptr.add(i + 6), vandq_s64(scalar_v, m3));
427
428            i += 8;
429        }
430    } else {
431        // Compile-time constant for bit masks [1,2,4,8,16,32,64,128]
432        const BIT_MASKS: [u8; 8] = [1, 2, 4, 8, 16, 32, 64, 128];
433        let bit_masks: uint8x8_t = vld1_u8(BIT_MASKS.as_ptr());
434        let zero_u8: uint8x8_t = vdup_n_u8(0);
435
436        // Main loop: process 8 coefficients per iteration
437        while i + 8 <= N {
438            let shift = i as u32;
439            let byte: u8 = ((mask64 >> shift) & 0xFF) as u8;
440
441            // Extract bits: replicate byte and AND with bit masks to isolate each bit
442            let vbyte: uint8x8_t = vdup_n_u8(byte);
443            let selected: uint8x8_t = vand_u8(vbyte, bit_masks);
444            let nz: uint8x8_t = vcgt_u8(selected, zero_u8); // 0xFF if bit set, 0x00 if not
445
446            // Sign-extend 0xFF → 0xFFFF...FFFF, 0x00 → 0x0000...0000 via signed widening
447            let nz_signed: int8x8_t = vreinterpret_s8_u8(nz); // Reinterpret: 0xFF = -1, 0x00 = 0
448            let s16: int16x8_t = vmovl_s8(nz_signed); // -1 → 0xFFFF, 0 → 0x0000
449            let lo32: int32x4_t = vmovl_s16(vget_low_s16(s16));
450            let hi32: int32x4_t = vmovl_s16(vget_high_s16(s16));
451
452            let m0: int64x2_t = vmovl_s32(vget_low_s32(lo32)); // -1 → 0xFFFF...FFFF
453            let m1: int64x2_t = vmovl_s32(vget_high_s32(lo32));
454            let m2: int64x2_t = vmovl_s32(vget_low_s32(hi32));
455            let m3: int64x2_t = vmovl_s32(vget_high_s32(hi32));
456
457            // Branchless select: scalar & 0xFFFF... = scalar, scalar & 0x0000... = 0
458            vst1q_s64(out_ptr.add(i), vandq_s64(scalar_v, m0));
459            vst1q_s64(out_ptr.add(i + 2), vandq_s64(scalar_v, m1));
460            vst1q_s64(out_ptr.add(i + 4), vandq_s64(scalar_v, m2));
461            vst1q_s64(out_ptr.add(i + 6), vandq_s64(scalar_v, m3));
462
463            i += 8;
464        }
465    }
466
467    // Tail: handle remaining coefficients one at a time
468    while i < N {
469        let bit = ((mask64 >> i) & 1) != 0;
470        let mask = -(bit as i64); // 0 or -1 (all bits set)
471        *out_ptr.add(i) = scalar & mask;
472        i += 1;
473    }
474}
475
476#[allow(
477    clippy::arithmetic_side_effects,
478    clippy::cast_possible_truncation,
479    clippy::cast_lossless
480)]
481#[allow(unsafe_op_in_unsafe_fn)]
482#[cfg(all(target_arch = "x86_64", target_feature = "avx512f"))]
483#[inline(always)]
484// Converts a u64 bitmask into an array of i64 values using AVX512 SIMD.
485// Processes 32 bits at a time with 4-way unrolling for instruction-level
486// parallelism. Significantly cleaner than NEON due to native mask register
487// support.
488unsafe fn widen_fill_avx512<const N: usize>(mask_ref: &u64, out_ptr: *mut i64, scalar: i64) {
489    #[cfg(target_arch = "x86_64")]
490    use core::arch::x86_64::*;
491
492    let mask64: u64 = *mask_ref;
493    let mut i = 0usize;
494
495    // Unrolled loop: process 32 coefficients (4 x 8) per iteration for ILP
496    // Interleave independent operations to keep multiple execution units busy
497    while i + 32 <= N {
498        let shift = i as u32;
499
500        // Extract all 4 bytes at once - no dependencies between extractions
501        let byte0: u8 = ((mask64 >> shift) & 0xFF) as u8;
502        let byte1: u8 = ((mask64 >> (shift + 8)) & 0xFF) as u8;
503        let byte2: u8 = ((mask64 >> (shift + 16)) & 0xFF) as u8;
504        let byte3: u8 = ((mask64 >> (shift + 24)) & 0xFF) as u8;
505
506        // Convert to mask registers - independent operations
507        let kmask0: __mmask8 = byte0;
508        let kmask1: __mmask8 = byte1;
509        let kmask2: __mmask8 = byte2;
510        let kmask3: __mmask8 = byte3;
511
512        // Predicated broadcasts - all can execute in parallel on different ports
513        let result0: __m512i = _mm512_maskz_set1_epi64(kmask0, scalar);
514        let result1: __m512i = _mm512_maskz_set1_epi64(kmask1, scalar);
515        let result2: __m512i = _mm512_maskz_set1_epi64(kmask2, scalar);
516        let result3: __m512i = _mm512_maskz_set1_epi64(kmask3, scalar);
517
518        // Stores - can pipeline as they have no dependencies on each other
519        _mm512_storeu_si512(out_ptr.add(i) as *mut __m512i, result0);
520        _mm512_storeu_si512(out_ptr.add(i + 8) as *mut __m512i, result1);
521        _mm512_storeu_si512(out_ptr.add(i + 16) as *mut __m512i, result2);
522        _mm512_storeu_si512(out_ptr.add(i + 24) as *mut __m512i, result3);
523
524        i += 32;
525    }
526
527    // Handle remaining full vectors (8 coefficients at a time)
528    while i + 8 <= N {
529        let shift = i as u32;
530        let byte: u8 = ((mask64 >> shift) & 0xFF) as u8;
531        let kmask: __mmask8 = byte;
532        let result: __m512i = _mm512_maskz_set1_epi64(kmask, scalar);
533        _mm512_storeu_si512(out_ptr.add(i) as *mut __m512i, result);
534        i += 8;
535    }
536
537    // Tail: handle remaining coefficients one at a time
538    while i < N {
539        let bit = ((mask64 >> i) & 1) != 0;
540        let mask = -(bit as i64); // 0 or -1 (all bits set)
541        *out_ptr.add(i) = scalar & mask;
542        i += 1;
543    }
544}
545
546#[cfg(test)]
547mod tests {
548    use crate::univariate::binary_ref::BinaryRefPoly;
549    use zinc_transcript::traits::{ConstTranscribable, GenTranscribable};
550    use zinc_utils::CHECKED;
551
552    use super::*;
553
554    #[test]
555    fn evaluate_is_correct() {
556        for i in 0..16 {
557            let poly = BinaryU64Poly::<4>::new([
558                (i & 0b0001 != 0).into(),
559                (i & 0b0010 != 0).into(),
560                (i & 0b0100 != 0).into(),
561                (i & 0b1000 != 0).into(),
562            ]);
563
564            let result = poly.evaluate_at_point(&2).unwrap();
565
566            assert_eq!(result, i);
567        }
568    }
569
570    #[test]
571    fn transcribable() {
572        let original =
573            BinaryU64Poly::<4>::new([true.into(), false.into(), true.into(), false.into()]);
574        let mut bytes = [0u8; BinaryU64Poly::<4>::NUM_BYTES];
575        assert_eq!(bytes.len(), u64::NUM_BYTES);
576
577        original.write_transcription_bytes_exact(&mut bytes);
578        let deserialized = BinaryU64Poly::<4>::read_transcription_bytes_exact(&bytes);
579        assert_eq!(original, deserialized);
580    }
581
582    fn widen_ref<const DEGREE_PLUS_ONE: usize>(
583        poly: &BinaryU64Poly<DEGREE_PLUS_ONE>,
584        scalar: i64,
585    ) -> DensePolynomial<i64, DEGREE_PLUS_ONE> {
586        let mut coeffs: [i64; DEGREE_PLUS_ONE] = [0; DEGREE_PLUS_ONE];
587        for (i, coeff) in coeffs.iter_mut().enumerate().take(DEGREE_PLUS_ONE) {
588            if (poly.0 & (1 << i)) != 0 {
589                *coeff = scalar;
590            }
591        }
592        DensePolynomial { coeffs }
593    }
594
595    #[ignore = "CI system does not support SIMD features"]
596    #[test]
597    fn widen_ref_and_widen_ref_simd_match() {
598        // Test with degree 4
599        for i in 0..16 {
600            let poly = BinaryU64Poly::<4>::new([
601                (i & 0b0001 != 0).into(),
602                (i & 0b0010 != 0).into(),
603                (i & 0b0100 != 0).into(),
604                (i & 0b1000 != 0).into(),
605            ]);
606
607            let poly_ref = BinaryRefPoly::<4>::new([
608                (i & 0b0001 != 0).into(),
609                (i & 0b0010 != 0).into(),
610                (i & 0b0100 != 0).into(),
611                (i & 0b1000 != 0).into(),
612            ]);
613
614            for scalar in [1, 42, -7, 100, -100, i64::MAX, i64::MIN] {
615                let result_simd_ref = widen_ref(&poly, scalar);
616                let result_simd = widen_simd(&poly, scalar);
617                let result_ref = poly_ref.clone().mul_by_scalar::<CHECKED>(&scalar).unwrap();
618
619                assert_eq!(
620                    result_simd_ref.coeffs, result_simd.coeffs,
621                    "Mismatch for pattern {} with scalar {}",
622                    i, scalar
623                );
624                assert_eq!(
625                    result_simd_ref.coeffs, result_ref.coeffs,
626                    "Mismatch for pattern {} with scalar {} between ref and MulByScalar",
627                    i, scalar
628                );
629            }
630        }
631
632        let coeffs: Vec<_> = [
633            1, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 0, 1, 1, 1,
634            0, 1, 0,
635        ]
636        .into_iter()
637        .map(|x| (x != 0).into())
638        .collect();
639
640        let poly32 = BinaryU64Poly::<32>::new_padded(coeffs.clone());
641        let poly32_ref = BinaryRefPoly::<32>::new_padded(coeffs);
642
643        for scalar in [1, 42, -7, 100, -100] {
644            let result_simd_ref = widen_ref(&poly32, scalar);
645            let result_simd = widen_simd(&poly32, scalar);
646            let result_ref = poly32_ref
647                .clone()
648                .mul_by_scalar::<CHECKED>(&scalar)
649                .unwrap();
650            assert_eq!(
651                result_simd_ref.coeffs, result_simd.coeffs,
652                "Mismatch for degree 32 with scalar {}",
653                scalar
654            );
655            assert_eq!(
656                result_simd_ref.coeffs, result_ref.coeffs,
657                "Mismatch for degree 32 with scalar {} between ref and MulByScalar",
658                scalar
659            );
660        }
661
662        // Test with all zeros
663        let poly_zeros = BinaryU64Poly::<16>::new([false.into(); 16]);
664        let result_ref_zeros = widen_ref(&poly_zeros, 42);
665        let result_simd_zeros = widen_simd(&poly_zeros, 42);
666        assert_eq!(result_ref_zeros.coeffs, result_simd_zeros.coeffs);
667
668        // Test with all ones
669        let poly_ones = BinaryU64Poly::<16>::new([true.into(); 16]);
670        let result_ref_ones = widen_ref(&poly_ones, 42);
671        let result_simd_ones = widen_simd(&poly_ones, 42);
672        assert_eq!(result_ref_ones.coeffs, result_simd_ones.coeffs);
673    }
674}