p3-binary-field 0.9.0-rc.1

Characteristic-2 tower fields GF(2) ⊂ GF(4) ⊂ … ⊂ GF(2^128) for Plonky3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
//! The packing of the polynomial-basis `GF(2^128)` over the wide carryless multiply.
//!
//! `VPCLMULQDQ` applies the carryless multiply to every 128-bit lane of a wide register.
//!
//! One field element is exactly one lane, so the scalar kernel becomes the packed one.
//!
//! Every other operation is exclusive or, a lane-local shift, or a permutation of lanes.
//!
use core::iter::{Product, Sum};
use core::mem::transmute;
use core::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign};

use p3_field::op_assign_macros::{
    impl_add_assign, impl_add_base_field, impl_div_methods, impl_mul_base_field, impl_mul_methods,
    impl_packed_field_div, impl_packed_value, impl_rng, impl_sub_assign, impl_sub_base_field,
    impl_sum_prod_base_field, ring_sum,
};
use p3_field::{
    Algebra, Field, PackedField, PackedFieldPow2, PackedValue, PrimeCharacteristicRing,
};
use rand::distr::{Distribution, StandardUniform};
use rand::{Rng, RngExt};

use super::lanes::{self, WIDTH};
use crate::gf2::characteristic_two_methods;
use crate::packed::split::{HIGH_BY_HIGH, LOW_BY_LOW, fold_shifted};
use crate::{BinaryField128, Gf2, Ghash128};

/// Several elements of the polynomial-basis `GF(2^128)`, one per 128-bit lane of a register.
///
/// Two under `AVX2`, four under `AVX-512`.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
// Needed to make the transmutes below sound.
#[repr(transparent)]
#[must_use]
pub struct PackedGhash128([Ghash128; WIDTH]);

impl PackedGhash128 {
    /// The register holding these elements.
    #[inline]
    #[must_use]
    fn to_vector(self) -> lanes::Reg {
        // SAFETY: the scalar is `repr(transparent)` over `u128`.
        //
        // So the array is `WIDTH` contiguous `u128` values, the register's own layout.
        //
        // This type is `repr(transparent)` over that array.
        unsafe { transmute(self) }
    }

    /// The elements held in a register.
    #[inline]
    fn from_vector(vector: lanes::Reg) -> Self {
        // SAFETY: the inverse of the transmute above.
        //
        // Every bit pattern is a valid element, so no value can be out of range.
        unsafe { transmute(vector) }
    }

    /// The same element in every lane.
    #[inline]
    const fn broadcast(value: Ghash128) -> Self {
        Self([value; WIDTH])
    }
}

impl From<Ghash128> for PackedGhash128 {
    #[inline]
    fn from(value: Ghash128) -> Self {
        Self::broadcast(value)
    }
}

impl Add for PackedGhash128 {
    type Output = Self;

    #[inline]
    #[allow(clippy::suspicious_arithmetic_impl)]
    fn add(self, rhs: Self) -> Self {
        // Addition in characteristic 2 is `XOR`, lane by lane.
        Self::from_vector(lanes::xor(self.to_vector(), rhs.to_vector()))
    }
}

impl Sub for PackedGhash128 {
    type Output = Self;

    #[inline]
    #[allow(clippy::suspicious_arithmetic_impl)]
    fn sub(self, rhs: Self) -> Self {
        // Subtraction coincides with addition in characteristic 2.
        self + rhs
    }
}

impl Neg for PackedGhash128 {
    type Output = Self;

    #[inline]
    fn neg(self) -> Self {
        // `-x = x` in characteristic 2.
        self
    }
}

impl Mul for PackedGhash128 {
    type Output = Self;

    #[inline]
    fn mul(self, rhs: Self) -> Self {
        let (x, y) = (self.to_vector(), rhs.to_vector());

        // The two diagonal half products.
        let low = lanes::clmul::<LOW_BY_LOW>(x, y);
        let high = lanes::clmul::<HIGH_BY_HIGH>(x, y);

        // Karatsuba reaches the middle coefficient with one product instead of two.
        //
        //     middle = (a0 + a1)(b0 + b1) + a0 b0 + a1 b1
        //
        // The scalar kernel takes the schoolbook form instead.
        //
        // A wide carryless multiply has half the throughput of the 128-bit one on Zen 4 and 5.
        //
        // So trading a product for two shuffles and three exclusive ors pays only here.
        //
        // Measured on Zen 5, four lanes: 0.47 ns per element against 0.55 for schoolbook.
        let mixed_x = lanes::xor(x, lanes::swap_halves(x));
        let mixed_y = lanes::xor(y, lanes::swap_halves(y));
        let middle = lanes::xor(
            lanes::xor(low, high),
            lanes::clmul::<LOW_BY_LOW>(mixed_x, mixed_y),
        );

        // Inner fold brings the top limb down, outer fold finishes the reduction.
        Self::from_vector(fold_shifted(low, fold_shifted(middle, high)))
    }
}

impl PrimeCharacteristicRing for PackedGhash128 {
    type PrimeSubfield = Gf2;

    const ZERO: Self = Self::broadcast(Ghash128::ZERO);
    const ONE: Self = Self::broadcast(Ghash128::ONE);
    // The characteristic is 2, so `TWO = ONE + ONE = ZERO`.
    const TWO: Self = Self::broadcast(Ghash128::ZERO);
    // The characteristic is 2, so `NEG_ONE = ONE`.
    const NEG_ONE: Self = Self::broadcast(Ghash128::ONE);

    #[inline]
    fn from_prime_subfield(f: Self::PrimeSubfield) -> Self {
        Self::broadcast(Ghash128::from_prime_subfield(f))
    }

    characteristic_two_methods!();

    #[inline]
    fn square(&self) -> Self {
        let x = self.to_vector();

        // The cross term of `(p0 + p1 x^64)^2` doubles to zero.
        //
        // So the square has no middle coefficient, and the inner fold has nothing to add to.
        let low = lanes::clmul::<LOW_BY_LOW>(x, x);
        let high = lanes::clmul::<HIGH_BY_HIGH>(x, x);

        Self::from_vector(fold_shifted(low, fold_shifted(lanes::zero(), high)))
    }

    /// `x (x - 1) = x^2 - x = x^2 + x` in characteristic 2.
    ///
    /// A square skips the cross-term carryless multiplies a general product pays for.
    #[inline]
    fn bool_check(&self) -> Self {
        self.square() + *self
    }

    #[inline]
    fn dot_product<const N: usize>(u: &[Self; N], v: &[Self; N]) -> Self {
        let (mut low, mut high, mut middle) = (lanes::zero(), lanes::zero(), lanes::zero());

        for (a, b) in u.iter().zip(v) {
            let (x, y) = (a.to_vector(), b.to_vector());

            // Accumulate the three polynomial coefficients without reducing.
            low = lanes::xor(low, lanes::clmul::<LOW_BY_LOW>(x, y));
            high = lanes::xor(high, lanes::clmul::<HIGH_BY_HIGH>(x, y));

            // Karatsuba's third product, from the sum of each operand's halves.
            let mixed_x = lanes::xor(x, lanes::swap_halves(x));
            let mixed_y = lanes::xor(y, lanes::swap_halves(y));
            middle = lanes::xor(middle, lanes::clmul::<LOW_BY_LOW>(mixed_x, mixed_y));
        }

        // Remove the diagonal contributions from the accumulated third product.
        middle = lanes::xor(middle, lanes::xor(low, high));

        // Reduction is linear, so the whole sum folds the modulus once.
        Self::from_vector(fold_shifted(low, fold_shifted(middle, high)))
    }
}

impl_add_assign!(PackedGhash128);
impl_sub_assign!(PackedGhash128);
impl_mul_methods!(PackedGhash128);
ring_sum!(PackedGhash128);
impl_rng!(PackedGhash128);

impl_add_base_field!(PackedGhash128, Ghash128);
impl_sub_base_field!(PackedGhash128, Ghash128);
impl_mul_base_field!(PackedGhash128, Ghash128);
impl_div_methods!(PackedGhash128, Ghash128);
impl_packed_field_div!(PackedGhash128);
impl_sum_prod_base_field!(PackedGhash128, Ghash128);

impl Algebra<Ghash128> for PackedGhash128 {}

impl From<Gf2> for PackedGhash128 {
    /// `GF(2)` is the prime subfield, embedded as `{ZERO, ONE}` in every lane.
    #[inline]
    fn from(x: Gf2) -> Self {
        Self::from_prime_subfield(x)
    }
}

impl_add_base_field!(PackedGhash128, Gf2);
impl_sub_base_field!(PackedGhash128, Gf2);
impl_mul_base_field!(PackedGhash128, Gf2);

impl Algebra<Gf2> for PackedGhash128 {}

impl From<BinaryField128> for PackedGhash128 {
    /// The same field element, seen in the polynomial basis, in every lane.
    ///
    /// The change of basis is a field isomorphism.
    ///
    /// So this packing is an algebra over the tower, exactly as one lane is.
    #[inline]
    fn from(x: BinaryField128) -> Self {
        Self::broadcast(Ghash128::from(x))
    }
}

impl_add_base_field!(PackedGhash128, BinaryField128);
impl_sub_base_field!(PackedGhash128, BinaryField128);
impl_mul_base_field!(PackedGhash128, BinaryField128);

impl Algebra<BinaryField128> for PackedGhash128 {}

impl_packed_value!(PackedGhash128, Ghash128, WIDTH);

// SAFETY: the transparent array satisfies the packed layout contract.
//
// Arithmetic acts independently on each 128-bit field element.
unsafe impl PackedField for PackedGhash128 {
    type Scalar = Ghash128;
}

// SAFETY: the width is two or four, both powers of two.
unsafe impl PackedFieldPow2 for PackedGhash128 {
    /// # Panics
    /// Panics if the block length does not divide the width.
    #[inline]
    fn interleave(&self, other: Self, block_len: usize) -> (Self, Self) {
        let (a, b) = lanes::interleave(self.to_vector(), other.to_vector(), block_len);
        (Self::from_vector(a), Self::from_vector(b))
    }
}

#[cfg(test)]
mod tests {
    use p3_field::{PackedValue, PrimeCharacteristicRing};
    use p3_field_testing::test_packed_binary_field;
    use proptest::prelude::*;

    use super::lanes::{self, WIDTH};
    use crate::packed::split::model::Model;
    use crate::packed::split::{
        HIGH_BY_HIGH, HIGH_BY_LOW, LOW_BY_HIGH, LOW_BY_LOW, Lanes, SplitScalar, fold_shifted,
    };
    use crate::{Ghash128, PackedGhash128};

    /// The bit patterns a random search is unlikely to reach.
    ///
    /// Each one drives the fold of the modulus to an extreme:
    ///
    /// ```text
    ///     0            nothing to reduce
    ///     all ones     every coefficient of the product is live
    ///     x^127        the highest degree, so the fold spills furthest
    ///     0x87         the modulus tail itself
    /// ```
    const SPECIAL: [u128; 4] = [0, u128::MAX, 1 << 127, 0x87];

    /// One extreme bit pattern per lane.
    fn specials() -> PackedGhash128 {
        PackedValue::from_fn(|i| Ghash128::from_le_bytes(SPECIAL[i].to_le_bytes()))
    }

    /// The elements a register holds, read back one per lane.
    fn lanes_of(register: lanes::Reg) -> [u128; WIDTH] {
        let mut out = [0u128; WIDTH];

        // SAFETY: the destination is exactly one register of contiguous 128-bit integers.
        //
        // The store is the unaligned form, so the array's own alignment is irrelevant.
        unsafe { lanes::store(out.as_mut_ptr(), register) };

        out
    }

    /// A register holding the given elements, one per lane.
    fn register_of(values: [u128; WIDTH]) -> lanes::Reg {
        // SAFETY: the source is exactly one register of contiguous 128-bit integers.
        unsafe { lanes::load(values.as_ptr()) }
    }

    /// The two 64-bit halves of every lane, exchanged.
    fn swapped_halves(values: [u128; WIDTH]) -> [u128; WIDTH] {
        // Rotating a 128-bit value by half its width is exactly the exchange.
        core::array::from_fn(|i| values[i].rotate_left(64))
    }

    /// Each lane-local operation on the register, against the same operation on the model.
    ///
    /// One assertion per operation, so a mismatch names the intrinsic that disagreed.
    ///
    /// The whole-element interleave is the one left out, since it crosses lanes by design.
    fn lanes_conform(
        a: [u128; WIDTH],
        b: [u128; WIDTH],
        scalar: u128,
    ) -> Result<(), TestCaseError> {
        let (x, y) = (register_of(a), register_of(b));
        let (mx, my) = (Model(a), Model(b));

        prop_assert_eq!(lanes_of(lanes::Reg::zero()), Model::<WIDTH>::zero().0);
        prop_assert_eq!(
            lanes_of(lanes::Reg::broadcast(scalar)),
            Model::<WIDTH>::broadcast(scalar).0
        );
        prop_assert_eq!(lanes_of(lanes::Reg::tail()), Model::<WIDTH>::tail().0);
        prop_assert_eq!(lanes_of(x.xor(y)), mx.xor(my).0);
        prop_assert_eq!(lanes_of(x.unpack_low_64(y)), mx.unpack_low_64(my).0);
        prop_assert_eq!(
            lanes_of(x.clmul::<LOW_BY_LOW>(y)),
            mx.clmul::<LOW_BY_LOW>(my).0
        );
        prop_assert_eq!(
            lanes_of(x.clmul::<HIGH_BY_LOW>(y)),
            mx.clmul::<HIGH_BY_LOW>(my).0
        );
        prop_assert_eq!(
            lanes_of(x.clmul::<LOW_BY_HIGH>(y)),
            mx.clmul::<LOW_BY_HIGH>(my).0
        );
        prop_assert_eq!(
            lanes_of(x.clmul::<HIGH_BY_HIGH>(y)),
            mx.clmul::<HIGH_BY_HIGH>(my).0
        );

        // Not a trait method, so the model cannot supply the expectation.
        //
        // It carries the only hand-written shuffle immediate here, which is why it is pinned.
        prop_assert_eq!(lanes_of(lanes::swap_halves(x)), swapped_halves(a));

        // The two composites built from those operations.
        //
        // A lane-crossing slip therefore shows up here as well as in the primitives above.
        prop_assert_eq!(lanes_of(fold_shifted(x, y)), fold_shifted(mx, my).0);
        prop_assert_eq!(
            lanes_of(SplitScalar::new(scalar).apply(x)),
            SplitScalar::new(scalar).apply(mx).0
        );

        Ok(())
    }

    #[test]
    fn the_register_matches_the_scalar_model_at_the_corners() {
        // Invariant: each operation is lane-local, so distinct lanes must stay distinct.
        //
        // Lane 0 carries the pair under test, so all sixteen combinations are reached.
        //
        // That includes the squaring-shaped case where both operands are the same value.
        //
        // The other lanes rotate through the corners.
        //
        // A value that crosses a lane boundary then lands on a different extreme and mismatches.
        for (i, &x) in SPECIAL.iter().enumerate() {
            for (j, &y) in SPECIAL.iter().enumerate() {
                let a = core::array::from_fn(|lane| match lane {
                    0 => x,
                    _ => SPECIAL[(i + lane) % SPECIAL.len()],
                });
                let b = core::array::from_fn(|lane| match lane {
                    0 => y,
                    _ => SPECIAL[(j + lane) % SPECIAL.len()],
                });

                // The multiplier walks the corners too, so the split runs from each extreme.
                for scalar in SPECIAL {
                    lanes_conform(a, b, scalar)
                        .unwrap_or_else(|e| panic!("a {x:#x}, b {y:#x}, scalar {scalar:#x}: {e}"));
                }
            }
        }
    }

    proptest! {
        #![proptest_config(ProptestConfig::with_cases(256))]

        #[test]
        fn the_register_matches_the_scalar_model(
            a in prop::array::uniform(any::<u128>()),
            b in prop::array::uniform(any::<u128>()),
            scalar in any::<u128>(),
        ) {
            // The seam the scalar model alone cannot reach.
            //
            // Each intrinsic must be the one the algebra was written against, lane by lane.
            lanes_conform(a, b, scalar)?;
        }
    }

    #[test]
    fn bool_check_matches_the_general_product_in_every_lane() {
        // Invariant: the squaring shortcut answers what the general product answers.
        //
        // Every booleanity constraint an AIR states takes this shortcut.
        //
        // The corners cover the two roots the check accepts and the extreme reductions.
        for (index, &value) in SPECIAL.iter().enumerate() {
            // Lane 0 carries the value under test.
            //
            // The rest rotate through the corners, so a lane-crossing result lands elsewhere.
            let lanes: [Ghash128; WIDTH] = core::array::from_fn(|lane| {
                let pattern = if lane == 0 {
                    value
                } else {
                    SPECIAL[(index + lane) % SPECIAL.len()]
                };
                Ghash128::from_le_bytes(pattern.to_le_bytes())
            });
            let packed: PackedGhash128 = PackedValue::from_fn(|lane| lanes[lane]);

            for (lane, &scalar) in lanes.iter().enumerate() {
                assert_eq!(
                    packed.bool_check().as_slice()[lane],
                    scalar * (scalar - Ghash128::ONE),
                    "lane {lane} of corner {value:#x}"
                );
                assert_eq!(packed.bool_check().as_slice()[lane], scalar.bool_check());
            }
        }

        // Zero and one are the roots the check accepts, so both must vanish in every lane.
        for root in [Ghash128::ZERO, Ghash128::ONE] {
            let packed: PackedGhash128 = PackedValue::from_fn(|_| root);
            assert!(
                packed
                    .bool_check()
                    .as_slice()
                    .iter()
                    .all(|&value| value == Ghash128::ZERO)
            );
        }
    }

    test_packed_binary_field!(
        crate::PackedGhash128,
        &[crate::PackedGhash128::ZERO],
        &[crate::PackedGhash128::ONE],
        super::specials()
    );
}