Skip to main content

p3_binary_field/
extension.rs

1//! Extension-field structure between the byte-aligned levels of the Wiedemann tower.
2//!
3//! Each level `GF(2^b)` of the tower contains every level below it, so for byte-aligned levels
4//! `Lower = GF(2^l)` and `Upper = GF(2^u)` with `l < u`, `Upper` is a degree-`u/l` extension of
5//! `Lower`. The basis is the tower basis: writing an element of `Upper` as an unsigned integer,
6//! basis coefficient `i` is the `l`-bit chunk at bit `i * l`, and basis element `i` is `1 << (i * l)`.
7//!
8//! On a little-endian target that chunking is exactly the in-memory byte layout, so the basis
9//! coefficients of an element can be borrowed in place rather than copied out. The levels below
10//! `GF(2^8)` are excluded because they do not fill their backing integer, and so their chunks
11//! would not all be canonical representatives.
12
13use alloc::vec::Vec;
14use core::ops::{Add, Mul, Sub};
15use core::{ptr, slice};
16
17use p3_field::extension::HasFrobenius;
18use p3_field::op_assign_macros::{impl_add_base_field, impl_sub_base_field};
19use p3_field::{
20    Algebra, BasedVectorSpace, ExtensionField, Field, PackedFieldExtension, Powers,
21    PrimeCharacteristicRing,
22};
23
24use crate::tower::TowerLevel;
25use crate::{BinaryField8, BinaryField16, BinaryField32, BinaryField64, BinaryField128};
26
27/// Make one byte-aligned tower level an extension of a byte-aligned level below it.
28///
29/// The parameters are the upper level with its backing integer, then the lower level with its
30/// backing integer.
31macro_rules! binary_tower_extension {
32    ($upper:ty, $upper_repr:ty, $lower:ty, $lower_repr:ty) => {
33        impl From<$lower> for $upper {
34            /// The lower level occupies the low `BITS` bits; the other coefficients vanish.
35            #[inline]
36            fn from(x: $lower) -> Self {
37                Self::from_repr(<$upper_repr>::from(x.to_repr()))
38            }
39        }
40
41        impl_add_base_field!($upper, $lower);
42        impl_sub_base_field!($upper, $lower);
43        impl Mul<$lower> for $upper {
44            type Output = Self;
45
46            #[inline]
47            fn mul(self, rhs: $lower) -> Self {
48                // Coordinate expansion costs one narrow product per coordinate.
49                // A carryless multiply makes the wide product cheaper than that from here up.
50                //
51                // The byte level stays on the expansion: its products are table lookups.
52                // The half-width level stays on it too: two products still beat the wide one.
53                if crate::clmul::HAS_HARDWARE_CLMUL
54                    && matches!(<$upper>::BITS, 64 | 128)
55                    && matches!(<$lower>::BITS, 16 | 32)
56                {
57                    return self * Self::from(rhs);
58                }
59                <Self as BasedVectorSpace<$lower>>::from_basis_coefficients_fn(|i| {
60                    <$lower>::from_repr((self.to_repr() >> (i * <$lower>::BITS)) as $lower_repr) * rhs
61                })
62            }
63        }
64
65        impl Mul<$upper> for $lower {
66            type Output = $upper;
67
68            #[inline]
69            fn mul(self, rhs: $upper) -> $upper {
70                rhs * self
71            }
72        }
73
74        impl Algebra<$lower> for $upper {}
75
76        impl p3_field::AlgebraIdentity<$lower> for $upper {
77            fn algebra_id() -> Vec<u8> {
78                alloc::format!("p3-Wiedemann-tower-v1:chunk-basis:{}:{}", <$lower>::BITS, <$upper>::BITS).into_bytes()
79            }
80        }
81
82        impl BasedVectorSpace<$lower> for $upper {
83            const DIMENSION: usize = <$upper>::BITS / <$lower>::BITS;
84
85            #[inline]
86            fn as_basis_coefficients_slice(&self) -> &[$lower] {
87                const {
88                    assert!(
89                        cfg!(target_endian = "little"),
90                        "the tower basis coincides with the memory layout only on little-endian targets"
91                    );
92                    // The lower level fills its backing integer, so every chunk of `self` is
93                    // already a canonical representative.
94                    assert!(<$lower>::BITS == <$lower_repr>::BITS as usize);
95                    assert!(
96                        size_of::<$upper>()
97                            == <$upper as BasedVectorSpace<$lower>>::DIMENSION
98                                * size_of::<$lower>()
99                    );
100                    assert!(align_of::<$lower>() <= align_of::<$upper>());
101                }
102
103                // SAFETY: both levels are `repr(transparent)` over unsigned integers, so `self`
104                // is `DIMENSION` contiguous, padding-free `$lower` values, aligned at least as
105                // strictly as `$lower` requires; the assertions above check the size, alignment
106                // and endianness this relies on, and that every chunk is canonical. The returned
107                // slice borrows `self`, so it cannot outlive it.
108                unsafe {
109                    slice::from_raw_parts(
110                        ptr::from_ref(self).cast::<$lower>(),
111                        <$upper as BasedVectorSpace<$lower>>::DIMENSION,
112                    )
113                }
114            }
115
116            #[inline]
117            fn from_basis_coefficients_fn<Fn: FnMut(usize) -> $lower>(mut f: Fn) -> Self {
118                let mut repr: $upper_repr = 0;
119                for i in 0..<$upper as BasedVectorSpace<$lower>>::DIMENSION {
120                    repr |= <$upper_repr>::from(f(i).to_repr()) << (i * <$lower>::BITS);
121                }
122                Self::from_repr(repr)
123            }
124
125            /// A whole vector of elements is one contiguous run of coefficients, for the same
126            /// reason a single element is, so the flattening is a single copy of the buffer.
127            ///
128            /// The inverse direction is not symmetric: a `Vec<$lower>` is allocated at `$lower`'s
129            /// alignment, which is weaker than `$upper`'s, so freeing it under a `Layout` whose
130            /// alignment differs from the one it was made with would be undefined behaviour.
131            /// `reconstitute_from_base` below instead copies into a freshly allocated buffer at
132            /// `$upper`'s own alignment.
133            #[inline]
134            fn flatten_to_base(vec: Vec<Self>) -> Vec<$lower> {
135                const {
136                    assert!(
137                        cfg!(target_endian = "little"),
138                        "the tower basis coincides with the memory layout only on little-endian targets"
139                    );
140                    assert!(<$lower>::BITS == <$lower_repr>::BITS as usize);
141                    assert!(
142                        size_of::<$upper>()
143                            == <$upper as BasedVectorSpace<$lower>>::DIMENSION
144                                * size_of::<$lower>()
145                    );
146                    assert!(align_of::<$lower>() <= align_of::<$upper>());
147                }
148
149                // SAFETY: the argument of `as_basis_coefficients_slice`, over the whole buffer
150                // rather than one element: both levels are `repr(transparent)` over unsigned
151                // integers and a `Vec`'s elements are contiguous and padding-free, so the
152                // allocation is `len * DIMENSION` canonical `$lower` values, aligned at least as
153                // strictly as `$lower` requires; the assertions above check the size, alignment
154                // and endianness that relies on. An empty `Vec` yields a dangling but aligned
155                // pointer, which is what a zero-length slice needs. The slice borrows `vec`, and
156                // is copied out below while `vec` is still live, so it never dangles.
157                let coefficients: &[$lower] = unsafe {
158                    slice::from_raw_parts(
159                        vec.as_ptr().cast::<$lower>(),
160                        vec.len() * <$upper as BasedVectorSpace<$lower>>::DIMENSION,
161                    )
162                };
163                coefficients.to_vec()
164            }
165
166            #[inline]
167            fn from_basis_coefficients_iter<I: ExactSizeIterator<Item = $lower>>(
168                iter: I,
169            ) -> Option<Self> {
170                let dimension = <$upper as BasedVectorSpace<$lower>>::DIMENSION;
171                (iter.len() == dimension).then(|| {
172                    let mut repr: $upper_repr = 0;
173                    // Zipping against the dimension bounds the shift even if `len` lied.
174                    for (i, c) in (0..dimension).zip(iter) {
175                        repr |= <$upper_repr>::from(c.to_repr()) << (i * <$lower>::BITS);
176                    }
177                    Self::from_repr(repr)
178                })
179            }
180
181            #[cfg(target_endian = "little")]
182            #[inline]
183            fn reconstitute_from_base(vec: Vec<$lower>) -> Vec<Self> {
184                let dimension = <Self as BasedVectorSpace<$lower>>::DIMENSION;
185                assert_eq!(vec.len() % dimension, 0);
186                let len = vec.len() / dimension;
187                let mut result = Vec::<Self>::with_capacity(len);
188                // SAFETY: both transparent integer representations have no padding and every
189                // bit pattern is canonical. Copying bytes preserves the little-endian tower
190                // coefficients without changing the allocation's original alignment.
191                unsafe {
192                    ptr::copy_nonoverlapping(
193                        vec.as_ptr().cast::<u8>(),
194                        result.as_mut_ptr().cast::<u8>(),
195                        vec.len() * size_of::<$lower>(),
196                    );
197                    result.set_len(len);
198                }
199                result
200            }
201        }
202
203        impl ExtensionField<$lower> for $upper {
204            type ExtensionPacking = Self;
205
206            #[inline]
207            fn is_in_basefield(&self) -> bool {
208                // The base field is exactly the elements whose coefficients above the
209                // constant one vanish.
210                (self.to_repr() >> <$lower>::BITS) == 0
211            }
212
213            #[inline]
214            fn as_base(&self) -> Option<$lower> {
215                <Self as ExtensionField<$lower>>::is_in_basefield(self)
216                    .then(|| <$lower>::from_repr(self.to_repr() as $lower_repr))
217            }
218        }
219
220        /// A tower level packs one element per vector, so every lane index is `0`.
221        impl PackedFieldExtension<$lower, $upper> for $upper {
222            #[inline]
223            fn from_ext_fn(f: impl Fn(usize) -> Self) -> Self {
224                f(0)
225            }
226
227            #[inline]
228            fn from_ext_slice(slice: &[Self]) -> Self {
229                assert_eq!(slice.len(), 1);
230                slice[0]
231            }
232
233            #[inline]
234            fn extract(&self, lane: usize) -> Self {
235                assert_eq!(lane, 0, "lane index out of range");
236                *self
237            }
238
239            #[inline]
240            fn add_assign_lane(&mut self, lane: usize, value: Self) {
241                assert_eq!(lane, 0, "lane index out of range");
242                *self += value;
243            }
244
245            #[inline]
246            fn packed_ext_powers(base: Self) -> Powers<Self> {
247                base.powers()
248            }
249        }
250
251        impl HasFrobenius<$lower> for $upper {
252            /// `x ↦ x^n` for `n = 2^BITS` the order of the base field.
253            #[inline]
254            fn frobenius(&self) -> Self {
255                // Every level of the tower is twice as wide as the one below, so a dimension of
256                // `2` holds exactly when the base field is the level immediately below, and only
257                // then is `$upper` the quadratic extension `X² + αX + 1` whose `split`, `join`
258                // and `mul_alpha` are phrased in terms of. `DIMENSION` is a constant, so the
259                // branch is settled at compile time.
260                if <$upper as BasedVectorSpace<$lower>>::DIMENSION == 2 {
261                    // The two roots of `X² + αX + 1` sum to `α`, so the automorphism fixing the
262                    // level below sends `X` to `X + α`, and `a0 + a1·X` to `(a0 + α·a1) + a1·X`.
263                    let (a0, a1) = self.split();
264                    Self::join(a0 + a1.mul_alpha(), a1)
265                } else {
266                    self.exp_power_of_2(<$lower>::BITS)
267                }
268            }
269
270            #[inline]
271            fn repeated_frobenius(&self, count: usize) -> Self {
272                // The Galois group is cyclic of order `DIMENSION`, so `count` reduces modulo it.
273                let count = count % <$upper as BasedVectorSpace<$lower>>::DIMENSION;
274                if <$upper as BasedVectorSpace<$lower>>::DIMENSION == 2 {
275                    if count == 0 {
276                        *self
277                    } else {
278                        <Self as HasFrobenius<$lower>>::frobenius(self)
279                    }
280                } else {
281                    self.exp_power_of_2(<$lower>::BITS * count)
282                }
283            }
284
285            #[inline]
286            fn pseudo_inv(&self) -> Self {
287                // Inversion through the norm to the level below is cheaper than the
288                // `n^D - 2` exponentiation the contract is phrased in terms of.
289                self.try_inverse().unwrap_or(Self::ZERO)
290            }
291        }
292    };
293}
294
295binary_tower_extension!(BinaryField16, u16, BinaryField8, u8);
296binary_tower_extension!(BinaryField32, u32, BinaryField8, u8);
297binary_tower_extension!(BinaryField64, u64, BinaryField8, u8);
298binary_tower_extension!(BinaryField128, u128, BinaryField8, u8);
299binary_tower_extension!(BinaryField32, u32, BinaryField16, u16);
300binary_tower_extension!(BinaryField64, u64, BinaryField16, u16);
301binary_tower_extension!(BinaryField128, u128, BinaryField16, u16);
302binary_tower_extension!(BinaryField64, u64, BinaryField32, u32);
303binary_tower_extension!(BinaryField128, u128, BinaryField32, u32);
304binary_tower_extension!(BinaryField128, u128, BinaryField64, u64);
305
306#[cfg(test)]
307mod tests {
308    use alloc::vec::Vec;
309
310    use p3_field::extension::HasFrobenius;
311    use p3_field::{BasedVectorSpace, ExtensionField, PrimeCharacteristicRing};
312    use proptest::prelude::*;
313
314    use crate::tower::TowerLevel;
315    use crate::{BinaryField8, BinaryField16, BinaryField32, BinaryField64, BinaryField128};
316
317    /// A fixed nonzero element of `GF(2^128)` used across the tests.
318    const A128: u128 = 0x1234_5678_9abc_def0_0fed_cba9_8765_4321;
319
320    /// Check that the basis coefficients over `$lower` really are coordinates in the basis
321    /// returned by `ith_basis_element`, and that both reassembly routes recover the element.
322    macro_rules! assert_basis_round_trip {
323        ($upper:ty, $lower:ty, $val:expr) => {{
324            let a = <$upper>::from_repr($val);
325            let dim = <$upper as BasedVectorSpace<$lower>>::DIMENSION;
326            let coeffs = BasedVectorSpace::<$lower>::as_basis_coefficients_slice(&a);
327            assert_eq!(coeffs.len(), dim);
328            for len in [0, 1, 19] {
329                let values = alloc::vec![a; len];
330                let flat = <$upper as BasedVectorSpace<$lower>>::flatten_to_base(values.clone());
331                assert_eq!(<$upper as BasedVectorSpace<$lower>>::reconstitute_from_base(flat), values);
332            }
333
334            // `a = Σ coeffs[i] · basis[i]` is the defining property of a coordinate map.
335            let recomposed = (0..dim)
336                .map(|i| {
337                    <$upper as BasedVectorSpace<$lower>>::ith_basis_element(i).unwrap() * coeffs[i]
338                })
339                .sum::<$upper>();
340            assert_eq!(recomposed, a);
341
342            assert_eq!(
343                <$upper as BasedVectorSpace<$lower>>::from_basis_coefficients_slice(coeffs),
344                Some(a)
345            );
346            assert_eq!(
347                <$upper as BasedVectorSpace<$lower>>::from_basis_coefficients_fn(|i| coeffs[i]),
348                a
349            );
350        }};
351    }
352
353    /// Check that the mixed-type operators agree with embedding the scalar first.
354    macro_rules! assert_mixed_ops_match_embedding {
355        ($upper:ty, $lower:ty, $val:expr, $scalar:expr) => {{
356            let a = <$upper>::from_repr($val);
357            let s = <$lower>::from_repr($scalar);
358            let s_up = <$upper>::from(s);
359
360            assert_eq!(a + s, a + s_up);
361            assert_eq!(a - s, a - s_up);
362            assert_eq!(a * s, a * s_up);
363            assert_eq!(s * a, a * s_up);
364
365            let mut acc = a;
366            acc += s;
367            assert_eq!(acc, a + s_up);
368            let mut acc = a;
369            acc -= s;
370            assert_eq!(acc, a - s_up);
371            let mut acc = a;
372            acc *= s;
373            assert_eq!(acc, a * s_up);
374        }};
375    }
376
377    /// Check that Frobenius over `$lower` fixes `$lower` pointwise, has order `DIMENSION`, and
378    /// that `repeated_frobenius` iterates it.
379    macro_rules! assert_frobenius_over {
380        ($upper:ty, $lower:ty, $val:expr) => {{
381            let dim = <$upper as BasedVectorSpace<$lower>>::DIMENSION;
382
383            // Frobenius fixes the base field: `x ↦ x^|lower|` is the identity on `lower`.
384            for i in 0..=u8::MAX {
385                let embedded = <$upper>::from(<$lower>::from_repr(i.into()));
386                assert_eq!(
387                    HasFrobenius::<$lower>::frobenius(&embedded),
388                    embedded,
389                    "Frobenius must fix the embedded base field"
390                );
391            }
392
393            // It is not the identity away from the base field: the generator of `$upper` over
394            // `$lower` is the first basis element beyond the base field.
395            let x = <$upper as BasedVectorSpace<$lower>>::ith_basis_element(1).unwrap();
396            assert_ne!(
397                HasFrobenius::<$lower>::frobenius(&x),
398                x,
399                "Frobenius must move elements outside the base field"
400            );
401
402            // `repeated_frobenius(count)` iterates `frobenius` and has order `DIMENSION`.
403            let a = <$upper>::from_repr($val);
404            let mut acc = a;
405            for count in 0..(2 * dim + 3) {
406                assert_eq!(HasFrobenius::<$lower>::repeated_frobenius(&a, count), acc);
407                acc = HasFrobenius::<$lower>::frobenius(&acc);
408            }
409
410            // `pseudo_inv` is inversion, extended by `0 ↦ 0`.
411            assert_eq!(
412                HasFrobenius::<$lower>::pseudo_inv(&<$upper>::ZERO),
413                <$upper>::ZERO
414            );
415            assert_eq!(a * HasFrobenius::<$lower>::pseudo_inv(&a), <$upper>::ONE);
416        }};
417    }
418
419    #[test]
420    fn gf2_128_over_gf2_8_basis_coefficients_are_bytes() {
421        let a = BinaryField128::from_repr(0x0f0e_0d0c_0b0a_0908_0706_0504_0302_0100);
422        let coeffs = BasedVectorSpace::<BinaryField8>::as_basis_coefficients_slice(&a);
423        assert_eq!(coeffs.len(), 16);
424        for (i, c) in coeffs.iter().enumerate() {
425            assert_eq!(*c, BinaryField8::from_repr(i as u8));
426        }
427        assert_eq!(
428            <BinaryField128 as BasedVectorSpace<BinaryField8>>::from_basis_coefficients_slice(
429                coeffs
430            ),
431            Some(a)
432        );
433    }
434
435    #[test]
436    fn mixed_mul_matches_embedding() {
437        let a = BinaryField128::from_repr(A128);
438        let s = BinaryField8::from_repr(0xa7);
439        assert_eq!(a * s, a * BinaryField128::from(s));
440    }
441
442    #[test]
443    fn dimension_is_the_ratio_of_bit_widths() {
444        assert_eq!(
445            <BinaryField16 as BasedVectorSpace<BinaryField8>>::DIMENSION,
446            2
447        );
448        assert_eq!(
449            <BinaryField32 as BasedVectorSpace<BinaryField8>>::DIMENSION,
450            4
451        );
452        assert_eq!(
453            <BinaryField64 as BasedVectorSpace<BinaryField8>>::DIMENSION,
454            8
455        );
456        assert_eq!(
457            <BinaryField128 as BasedVectorSpace<BinaryField8>>::DIMENSION,
458            16
459        );
460        assert_eq!(
461            <BinaryField32 as BasedVectorSpace<BinaryField16>>::DIMENSION,
462            2
463        );
464        assert_eq!(
465            <BinaryField64 as BasedVectorSpace<BinaryField16>>::DIMENSION,
466            4
467        );
468        assert_eq!(
469            <BinaryField128 as BasedVectorSpace<BinaryField16>>::DIMENSION,
470            8
471        );
472        assert_eq!(
473            <BinaryField64 as BasedVectorSpace<BinaryField32>>::DIMENSION,
474            2
475        );
476        assert_eq!(
477            <BinaryField128 as BasedVectorSpace<BinaryField32>>::DIMENSION,
478            4
479        );
480        assert_eq!(
481            <BinaryField128 as BasedVectorSpace<BinaryField64>>::DIMENSION,
482            2
483        );
484    }
485
486    #[test]
487    fn basis_coefficients_round_trip_on_every_pair() {
488        assert_basis_round_trip!(BinaryField16, BinaryField8, 0xfedc);
489        assert_basis_round_trip!(BinaryField32, BinaryField8, 0xfedc_ba98);
490        assert_basis_round_trip!(BinaryField64, BinaryField8, 0xfedc_ba98_7654_3210);
491        assert_basis_round_trip!(BinaryField128, BinaryField8, A128);
492        assert_basis_round_trip!(BinaryField32, BinaryField16, 0xfedc_ba98);
493        assert_basis_round_trip!(BinaryField64, BinaryField16, 0xfedc_ba98_7654_3210);
494        assert_basis_round_trip!(BinaryField128, BinaryField16, A128);
495        assert_basis_round_trip!(BinaryField64, BinaryField32, 0xfedc_ba98_7654_3210);
496        assert_basis_round_trip!(BinaryField128, BinaryField32, A128);
497        assert_basis_round_trip!(BinaryField128, BinaryField64, A128);
498    }
499
500    #[test]
501    fn mixed_ops_match_embedding_on_every_pair() {
502        assert_mixed_ops_match_embedding!(BinaryField16, BinaryField8, 0xfedc, 0xa7);
503        assert_mixed_ops_match_embedding!(BinaryField32, BinaryField8, 0xfedc_ba98, 0xa7);
504        assert_mixed_ops_match_embedding!(BinaryField64, BinaryField8, 0xfedc_ba98_7654_3210, 0xa7);
505        assert_mixed_ops_match_embedding!(BinaryField128, BinaryField8, A128, 0xa7);
506        assert_mixed_ops_match_embedding!(BinaryField32, BinaryField16, 0xfedc_ba98, 0xa73b);
507        assert_mixed_ops_match_embedding!(
508            BinaryField64,
509            BinaryField16,
510            0xfedc_ba98_7654_3210,
511            0xa73b
512        );
513        assert_mixed_ops_match_embedding!(BinaryField128, BinaryField16, A128, 0xa73b);
514        assert_mixed_ops_match_embedding!(
515            BinaryField64,
516            BinaryField32,
517            0xfedc_ba98_7654_3210,
518            0xa73b_c91d
519        );
520        assert_mixed_ops_match_embedding!(BinaryField128, BinaryField32, A128, 0xa73b_c91d);
521        assert_mixed_ops_match_embedding!(
522            BinaryField128,
523            BinaryField64,
524            A128,
525            0xa73b_c91d_5566_7788
526        );
527    }
528
529    #[test]
530    fn frobenius_fixes_exactly_the_lower_level_on_every_pair() {
531        assert_frobenius_over!(BinaryField16, BinaryField8, 0xfedc);
532        assert_frobenius_over!(BinaryField32, BinaryField8, 0xfedc_ba98);
533        assert_frobenius_over!(BinaryField64, BinaryField8, 0xfedc_ba98_7654_3210);
534        assert_frobenius_over!(BinaryField128, BinaryField8, A128);
535        assert_frobenius_over!(BinaryField32, BinaryField16, 0xfedc_ba98);
536        assert_frobenius_over!(BinaryField64, BinaryField16, 0xfedc_ba98_7654_3210);
537        assert_frobenius_over!(BinaryField128, BinaryField16, A128);
538        assert_frobenius_over!(BinaryField64, BinaryField32, 0xfedc_ba98_7654_3210);
539        assert_frobenius_over!(BinaryField128, BinaryField32, A128);
540        assert_frobenius_over!(BinaryField128, BinaryField64, A128);
541    }
542
543    /// The order of the base field, not its bit width, is the Frobenius exponent: over
544    /// `GF(2^8)` the map is `x ↦ x^256`, which fixes all 256 elements of `GF(2^8)`.
545    #[test]
546    fn frobenius_exponent_is_the_order_of_the_base_field() {
547        let a = BinaryField128::from_repr(A128);
548        assert_eq!(
549            HasFrobenius::<BinaryField8>::frobenius(&a),
550            a.exp_power_of_2(8)
551        );
552        assert_eq!(
553            HasFrobenius::<BinaryField64>::frobenius(&a),
554            a.exp_power_of_2(64)
555        );
556    }
557
558    #[test]
559    fn is_in_basefield_detects_the_embedded_subfield() {
560        let low = BinaryField128::from(BinaryField8::from_repr(0xa7));
561        assert!(ExtensionField::<BinaryField8>::is_in_basefield(&low));
562        assert_eq!(
563            ExtensionField::<BinaryField8>::as_base(&low),
564            Some(BinaryField8::from_repr(0xa7))
565        );
566
567        // Sixteen bits wide: inside `GF(2^16)`, outside `GF(2^8)`.
568        let mid = BinaryField128::from_repr(0xa73b);
569        assert!(!ExtensionField::<BinaryField8>::is_in_basefield(&mid));
570        assert_eq!(ExtensionField::<BinaryField8>::as_base(&mid), None);
571        assert!(ExtensionField::<BinaryField16>::is_in_basefield(&mid));
572        assert_eq!(
573            ExtensionField::<BinaryField16>::as_base(&mid),
574            Some(BinaryField16::from_repr(0xa73b))
575        );
576    }
577
578    #[test]
579    fn from_basis_coefficients_iter_checks_the_length() {
580        let coeffs = [BinaryField64::ONE, BinaryField64::ZERO];
581        assert_eq!(
582            <BinaryField128 as BasedVectorSpace<BinaryField64>>::from_basis_coefficients_iter(
583                coeffs.into_iter()
584            ),
585            Some(BinaryField128::ONE)
586        );
587        assert_eq!(
588            <BinaryField128 as BasedVectorSpace<BinaryField64>>::from_basis_coefficients_iter(
589                coeffs[..1].iter().copied()
590            ),
591            None
592        );
593        assert_eq!(
594            <BinaryField128 as BasedVectorSpace<BinaryField64>>::from_basis_coefficients_slice(&[]),
595            None
596        );
597    }
598
599    /// Flattening concatenates each element's coefficient bytes in memory order, and
600    /// reconstituting inverts it.
601    #[test]
602    fn flatten_and_reconstitute_round_trip() {
603        let elems: Vec<BinaryField128> = (0..64u128)
604            .map(|i| {
605                BinaryField128::from_repr(i.wrapping_mul(0x9e37_79b9_7f4a_7c15_f39c_c060_5ced_c834))
606            })
607            .collect();
608
609        // Every coefficient position must take a nonzero value somewhere in the fixture,
610        // or a shift that lands in the wrong position could OR in zeros and still round-trip.
611        for pos in 0..16 {
612            assert!(
613                elems.iter().any(|e| {
614                    BasedVectorSpace::<BinaryField8>::as_basis_coefficients_slice(e)[pos]
615                        != BinaryField8::ZERO
616                }),
617                "coefficient position {pos} is never nonzero"
618            );
619        }
620
621        let flat =
622            <BinaryField128 as BasedVectorSpace<BinaryField8>>::flatten_to_base(elems.clone());
623        assert_eq!(flat.len(), elems.len() * 16);
624        for (i, e) in elems.iter().enumerate() {
625            assert_eq!(&flat[i * 16..(i + 1) * 16], e.as_basis_coefficients_slice());
626        }
627
628        let back = <BinaryField128 as BasedVectorSpace<BinaryField8>>::reconstitute_from_base(flat);
629        assert_eq!(back, elems);
630    }
631
632    #[test]
633    #[should_panic]
634    fn reconstitute_rejects_an_incomplete_element() {
635        let _ = <BinaryField128 as BasedVectorSpace<BinaryField64>>::reconstitute_from_base(
636            alloc::vec![BinaryField64::ONE],
637        );
638    }
639
640    /// Every byte of every `GF(2^8)` value survives a pack and an unpack.
641    #[test]
642    fn reconstitute_is_exhaustive_over_the_byte_level() {
643        let all: Vec<BinaryField8> = (0..=u8::MAX).map(BinaryField8::from_repr).collect();
644        let packed =
645            <BinaryField16 as BasedVectorSpace<BinaryField8>>::reconstitute_from_base(all.clone());
646        assert_eq!(packed.len(), 128);
647        let flat = <BinaryField16 as BasedVectorSpace<BinaryField8>>::flatten_to_base(packed);
648        assert_eq!(flat, all);
649    }
650
651    /// Check the conjugation `frobenius` takes over the level immediately below against the
652    /// repeated squaring that defines it, for one element.
653    macro_rules! assert_frobenius_is_repeated_squaring {
654        ($upper:ty, $lower:ty, $val:expr) => {{
655            let a = <$upper>::from_repr($val);
656            let dim = <$upper as BasedVectorSpace<$lower>>::DIMENSION;
657            assert_eq!(dim, 2, "the specialisation only covers adjacent levels");
658
659            prop_assert_eq!(
660                HasFrobenius::<$lower>::frobenius(&a),
661                a.exp_power_of_2(<$lower>::BITS)
662            );
663            for count in 0..(2 * dim + 3) {
664                prop_assert_eq!(
665                    HasFrobenius::<$lower>::repeated_frobenius(&a, count),
666                    a.exp_power_of_2(<$lower>::BITS * (count % dim))
667                );
668            }
669        }};
670    }
671
672    proptest! {
673        #![proptest_config(ProptestConfig::with_cases(2000))]
674
675        /// Frobenius over the level immediately below is conjugation rather than a chain of
676        /// squarings; the two must agree at each of the four levels that take that route.
677        #[test]
678        fn frobenius_over_the_adjacent_level_is_repeated_squaring(
679            a in any::<u128>(),
680        ) {
681            assert_frobenius_is_repeated_squaring!(BinaryField16, BinaryField8, a as u16);
682            assert_frobenius_is_repeated_squaring!(BinaryField32, BinaryField16, a as u32);
683            assert_frobenius_is_repeated_squaring!(BinaryField64, BinaryField32, a as u64);
684            assert_frobenius_is_repeated_squaring!(BinaryField128, BinaryField64, a);
685        }
686
687        /// The levels whose base field is further down keep the repeated squaring, so the
688        /// specialisation must not have leaked into them.
689        #[test]
690        fn frobenius_over_a_deeper_level_is_repeated_squaring(a in any::<u128>()) {
691            macro_rules! assert_unspecialised {
692                ($upper:ty, $lower:ty, $val:expr) => {{
693                    let x = <$upper>::from_repr($val);
694                    prop_assert_eq!(
695                        HasFrobenius::<$lower>::frobenius(&x),
696                        x.exp_power_of_2(<$lower>::BITS)
697                    );
698                }};
699            }
700
701            assert_unspecialised!(BinaryField32, BinaryField8, a as u32);
702            assert_unspecialised!(BinaryField64, BinaryField8, a as u64);
703            assert_unspecialised!(BinaryField128, BinaryField8, a);
704            assert_unspecialised!(BinaryField64, BinaryField16, a as u64);
705            assert_unspecialised!(BinaryField128, BinaryField16, a);
706            assert_unspecialised!(BinaryField128, BinaryField32, a);
707        }
708    }
709
710    /// Check `flatten_to_base` against concatenating each element's coefficients in turn, which
711    /// is what [`BasedVectorSpace`] defines it to be, and against reassembly.
712    macro_rules! assert_flatten_matches_concatenation {
713        ($upper:ty, $lower:ty, $vals:expr) => {{
714            let elements: Vec<$upper> = $vals.iter().map(|&v| <$upper>::from_repr(v)).collect();
715
716            let expected: Vec<$lower> = elements
717                .iter()
718                .flat_map(|x| {
719                    <$upper as BasedVectorSpace<$lower>>::as_basis_coefficients_slice(x).to_vec()
720                })
721                .collect();
722
723            let flat = <$upper as BasedVectorSpace<$lower>>::flatten_to_base(elements.clone());
724            assert_eq!(flat, expected);
725            assert_eq!(
726                flat.len(),
727                elements.len() * <$upper as BasedVectorSpace<$lower>>::DIMENSION
728            );
729            assert_eq!(
730                <$upper as BasedVectorSpace<$lower>>::reconstitute_from_base(flat),
731                elements
732            );
733        }};
734    }
735
736    #[test]
737    fn flatten_to_base_concatenates_the_coefficients_on_every_pair() {
738        // The empty case has to reach the same answer as the non-empty one.
739        assert_flatten_matches_concatenation!(BinaryField128, BinaryField8, [0u128; 0]);
740
741        let vals16 = [0u16, 1, 0xfedc, 0xffff];
742        let vals32 = [0u32, 1, 0xfedc_ba98, 0xffff_ffff];
743        let vals64 = [0u64, 1, 0xfedc_ba98_7654_3210, u64::MAX];
744        let vals128 = [0u128, 1, A128, u128::MAX];
745
746        assert_flatten_matches_concatenation!(BinaryField16, BinaryField8, vals16);
747        assert_flatten_matches_concatenation!(BinaryField32, BinaryField8, vals32);
748        assert_flatten_matches_concatenation!(BinaryField64, BinaryField8, vals64);
749        assert_flatten_matches_concatenation!(BinaryField128, BinaryField8, vals128);
750        assert_flatten_matches_concatenation!(BinaryField32, BinaryField16, vals32);
751        assert_flatten_matches_concatenation!(BinaryField64, BinaryField16, vals64);
752        assert_flatten_matches_concatenation!(BinaryField128, BinaryField16, vals128);
753        assert_flatten_matches_concatenation!(BinaryField64, BinaryField32, vals64);
754        assert_flatten_matches_concatenation!(BinaryField128, BinaryField32, vals128);
755        assert_flatten_matches_concatenation!(BinaryField128, BinaryField64, vals128);
756    }
757
758    /// The embedding is multiplicative and additive, i.e. a ring homomorphism.
759    #[test]
760    fn embedding_is_a_ring_homomorphism() {
761        for a in 0..=u8::MAX {
762            for b in 0..=u8::MAX {
763                let (x, y) = (BinaryField8::from_repr(a), BinaryField8::from_repr(b));
764                assert_eq!(
765                    BinaryField128::from(x * y),
766                    BinaryField128::from(x) * BinaryField128::from(y)
767                );
768                assert_eq!(
769                    BinaryField128::from(x + y),
770                    BinaryField128::from(x) + BinaryField128::from(y)
771                );
772            }
773        }
774        assert_eq!(BinaryField128::from(BinaryField8::ONE), BinaryField128::ONE);
775    }
776}