Skip to main content

binius_field/packed_fields/
mod.rs

1// Copyright 2023-2025 Irreducible Inc.
2// Copyright 2026 The Binius Developers
3
4//! Packed field implementations, and the packings of the tower's base field.
5
6pub mod ghash;
7pub mod ghash_sq;
8pub mod primitive;
9pub mod rijndael;
10pub mod sliced;
11
12use std::ops::Mul;
13
14pub use ghash::*;
15pub use ghash_sq::*;
16pub use rijndael::*;
17
18use crate::{
19	BinaryField1b,
20	arch::{M128, M256, M512},
21	arithmetic_traits::{InvertOrZero, Square, WideMul},
22	packed_fields::primitive::PackedPrimitiveType,
23	underlier::{U1, U2, U4, Underlier},
24};
25
26// Type aliases for the `BinaryField1b` packings. The underlier determines the width; `M128`/`M256`/
27// `M512` resolve to the architecture-appropriate type (SIMD where available, scaled otherwise).
28pub type PackedBinaryField1x1b = PackedPrimitiveType<U1, BinaryField1b>;
29pub type PackedBinaryField2x1b = PackedPrimitiveType<U2, BinaryField1b>;
30pub type PackedBinaryField4x1b = PackedPrimitiveType<U4, BinaryField1b>;
31pub type PackedBinaryField8x1b = PackedPrimitiveType<u8, BinaryField1b>;
32pub type PackedBinaryField16x1b = PackedPrimitiveType<u16, BinaryField1b>;
33pub type PackedBinaryField32x1b = PackedPrimitiveType<u32, BinaryField1b>;
34pub type PackedBinaryField64x1b = PackedPrimitiveType<u64, BinaryField1b>;
35pub type PackedBinaryField128x1b = PackedPrimitiveType<M128, BinaryField1b>;
36pub type PackedBinaryField256x1b = PackedPrimitiveType<M256, BinaryField1b>;
37pub type PackedBinaryField512x1b = PackedPrimitiveType<M512, BinaryField1b>;
38
39// Every `BinaryField1b` packing shares the same arithmetic, which is available for any underlier:
40// addition is bitwise XOR (provided generically for all `PackedPrimitiveType` in `packed.rs`) and
41// multiplication is bitwise AND. Squaring and inversion are the identity, since `0` and `1` are
42// each their own square and inverse. A single blanket impl over `U` therefore replaces the
43// per-type definitions that the `define_packed_binary_field` macro used to generate.
44impl<U: Underlier> Mul for PackedPrimitiveType<U, BinaryField1b> {
45	type Output = Self;
46
47	#[inline]
48	#[allow(clippy::suspicious_arithmetic_impl)]
49	fn mul(self, rhs: Self) -> Self {
50		(self.0 & rhs.0).into()
51	}
52}
53
54impl<U: Underlier> Square for PackedPrimitiveType<U, BinaryField1b> {
55	#[inline]
56	fn square(self) -> Self {
57		self
58	}
59}
60
61impl<U: Underlier> InvertOrZero for PackedPrimitiveType<U, BinaryField1b> {
62	#[inline]
63	fn invert_or_zero(self) -> Self {
64		self
65	}
66}
67
68impl<U: Underlier> WideMul for PackedPrimitiveType<U, BinaryField1b> {
69	type Output = Self;
70
71	#[inline]
72	fn wide_mul(a: Self, b: Self) -> Self {
73		a * b
74	}
75
76	#[inline]
77	fn reduce(wide: Self) -> Self {
78		wide
79	}
80}
81
82/// Common code to test different multiply, square and invert implementations
83#[cfg(test)]
84pub mod test_utils {
85	use proptest::{
86		arbitrary::{Arbitrary, any},
87		strategy::{BoxedStrategy, Strategy},
88	};
89
90	use crate::{
91		Field, PackedField,
92		arch::{M128, M256, M512},
93		underlier::UnderlierView,
94	};
95
96	// Proptest generates primitive underliers itself; a SIMD underlier borrows the strategy of the
97	// `u128` array it converts from, so `any::<P::Underlier>()` resolves at every packing width.
98	impl Arbitrary for M128 {
99		type Parameters = ();
100		type Strategy = BoxedStrategy<Self>;
101
102		fn arbitrary_with((): Self::Parameters) -> Self::Strategy {
103			any::<u128>().prop_map(Self::from).boxed()
104		}
105	}
106
107	impl Arbitrary for M256 {
108		type Parameters = ();
109		type Strategy = BoxedStrategy<Self>;
110
111		fn arbitrary_with((): Self::Parameters) -> Self::Strategy {
112			any::<[u128; 2]>().prop_map(Self::from).boxed()
113		}
114	}
115
116	impl Arbitrary for M512 {
117		type Parameters = ();
118		type Strategy = BoxedStrategy<Self>;
119
120		fn arbitrary_with((): Self::Parameters) -> Self::Strategy {
121			any::<[u128; 4]>().prop_map(Self::from).boxed()
122		}
123	}
124
125	/// Every lane of the product is the product of the operands' lanes.
126	pub fn check_mul<P: PackedField + UnderlierView>(a: P::Underlier, b: P::Underlier) {
127		let (a, b) = (P::from_underlier(a), P::from_underlier(b));
128
129		let c = a * b;
130		for i in 0..P::WIDTH {
131			assert_eq!(c.get(i), a.get(i) * b.get(i));
132		}
133	}
134
135	/// Every lane of the square is its own lane multiplied by itself.
136	pub fn check_square<P: PackedField + UnderlierView>(a: P::Underlier) {
137		let a = P::from_underlier(a);
138
139		let c = a.square();
140		for i in 0..P::WIDTH {
141			assert_eq!(c.get(i), a.get(i) * a.get(i));
142		}
143	}
144
145	/// A non-zero lane inverts to its multiplicative inverse, and a zero lane inverts to zero.
146	pub fn check_invert_or_zero<P: PackedField + UnderlierView>(a: P::Underlier) {
147		let a = P::from_underlier(a);
148
149		let c = a.invert_or_zero();
150		for i in 0..P::WIDTH {
151			if a.get(i).is_zero() {
152				assert!(c.get(i).is_zero());
153			} else {
154				assert_eq!(a.get(i) * c.get(i), P::Scalar::ONE);
155			}
156		}
157	}
158
159	/// One deferred product, reduced immediately, equals the plain multiply.
160	pub fn check_wide_mul<P: PackedField + UnderlierView>(a: P::Underlier, b: P::Underlier) {
161		let (a, b) = (P::from_underlier(a), P::from_underlier(b));
162
163		assert_eq!(P::reduce(P::wide_mul(a, b)), a * b);
164	}
165
166	/// Two deferred products summed and reduced once equal the sum of the plain multiplies.
167	pub fn check_wide_mul_linearity<P: PackedField + UnderlierView>(
168		a1: P::Underlier,
169		b1: P::Underlier,
170		a2: P::Underlier,
171		b2: P::Underlier,
172	) {
173		let (a1, b1) = (P::from_underlier(a1), P::from_underlier(b1));
174		let (a2, b2) = (P::from_underlier(a2), P::from_underlier(b2));
175
176		// The sum reaches wide values no single product produces, so this exercises the reduction
177		// over its full accumulated domain.
178		let sum = P::wide_mul(a1, b1) + P::wide_mul(a2, b2);
179		assert_eq!(P::reduce(sum), a1 * b1 + a2 * b2);
180	}
181
182	/// Check the packed arithmetic of `$ty` lane-by-lane against its own scalar field.
183	macro_rules! packed_field_tests {
184		($mod:ident, $ty:ty) => {
185			mod $mod {
186				use proptest::{prelude::any, proptest};
187				use $crate::packed_fields::test_utils::{
188					check_invert_or_zero, check_mul, check_square, check_wide_mul,
189					check_wide_mul_linearity,
190				};
191
192				use super::*;
193
194				// The underlier is the packing's raw bit pattern, so one strategy fits every width.
195				type U = <$ty as $crate::underlier::UnderlierView>::Underlier;
196
197				proptest! {
198					#[test]
199					fn mul(a in any::<U>(), b in any::<U>()) {
200						check_mul::<$ty>(a, b);
201					}
202
203					#[test]
204					fn square(a in any::<U>()) {
205						check_square::<$ty>(a);
206					}
207
208					#[test]
209					fn invert_or_zero(a in any::<U>()) {
210						check_invert_or_zero::<$ty>(a);
211					}
212
213					#[test]
214					fn wide_mul(a in any::<U>(), b in any::<U>()) {
215						check_wide_mul::<$ty>(a, b);
216					}
217
218					#[test]
219					fn wide_mul_linearity(
220						a1 in any::<U>(), b1 in any::<U>(),
221						a2 in any::<U>(), b2 in any::<U>(),
222					) {
223						check_wide_mul_linearity::<$ty>(a1, b1, a2, b2);
224					}
225				}
226			}
227		};
228	}
229
230	pub(crate) use packed_field_tests;
231
232	pub fn check_interleave<P: PackedField + UnderlierView>(
233		lhs: P::Underlier,
234		rhs: P::Underlier,
235		log_block_len: usize,
236	) {
237		let lhs = P::from_underlier(lhs);
238		let rhs = P::from_underlier(rhs);
239		let (a, b) = lhs.interleave(rhs, log_block_len);
240		let block_len = 1 << log_block_len;
241		for i in (0..P::WIDTH).step_by(block_len * 2) {
242			for j in 0..block_len {
243				assert_eq!(a.get(i + j), lhs.get(i + j));
244				assert_eq!(a.get(i + j + block_len), rhs.get(i + j));
245
246				assert_eq!(b.get(i + j), lhs.get(i + j + block_len));
247				assert_eq!(b.get(i + j + block_len), rhs.get(i + j + block_len));
248			}
249		}
250	}
251
252	pub fn check_interleave_all_heights<P: PackedField + UnderlierView>(
253		lhs: P::Underlier,
254		rhs: P::Underlier,
255	) {
256		for log_block_len in 0..P::LOG_WIDTH {
257			check_interleave::<P>(lhs, rhs, log_block_len);
258		}
259	}
260
261	pub fn check_unzip<P: PackedField + UnderlierView>(
262		lhs: P::Underlier,
263		rhs: P::Underlier,
264		log_block_len: usize,
265	) {
266		let lhs = P::from_underlier(lhs);
267		let rhs = P::from_underlier(rhs);
268		let block_len = 1 << log_block_len;
269		let (a, b) = lhs.unzip(rhs, log_block_len);
270		for i in (0..P::WIDTH / 2).step_by(block_len) {
271			for j in 0..block_len {
272				assert_eq!(
273					a.get(i + j),
274					lhs.get(2 * i + j),
275					"i: {}, j: {}, log_block_len: {}, P: {:?}",
276					i,
277					j,
278					log_block_len,
279					P::zero()
280				);
281				assert_eq!(
282					b.get(i + j),
283					lhs.get(2 * i + j + block_len),
284					"i: {}, j: {}, log_block_len: {}, P: {:?}",
285					i,
286					j,
287					log_block_len,
288					P::zero()
289				);
290			}
291		}
292
293		for i in (0..P::WIDTH / 2).step_by(block_len) {
294			for j in 0..block_len {
295				assert_eq!(
296					a.get(i + j + P::WIDTH / 2),
297					rhs.get(2 * i + j),
298					"i: {}, j: {}, log_block_len: {}, P: {:?}",
299					i,
300					j,
301					log_block_len,
302					P::zero()
303				);
304				assert_eq!(b.get(i + j + P::WIDTH / 2), rhs.get(2 * i + j + block_len));
305			}
306		}
307	}
308
309	pub fn check_transpose_all_heights<P: PackedField + UnderlierView>(
310		lhs: P::Underlier,
311		rhs: P::Underlier,
312	) {
313		for log_block_len in 0..P::LOG_WIDTH {
314			check_unzip::<P>(lhs, rhs, log_block_len);
315		}
316	}
317}
318
319#[cfg(test)]
320mod tests {
321	use std::{fmt::Debug, iter::repeat_with};
322
323	use binius_utils::{
324		DeserializeBytes, FixedSizeSerializeBytes, SerializeBytes, bytes::BytesMut,
325	};
326	use proptest::prelude::*;
327	use rand::prelude::*;
328	use test_utils::check_interleave_all_heights;
329
330	use super::{test_utils::packed_field_tests, *};
331	use crate::{
332		Divisible, PackedField, PackedGhash1x128b, PackedGhash2x128b, PackedGhash4x128b,
333		PackedRijndael1x8b, PackedRijndael16x8b, PackedRijndael32x8b, PackedRijndael64x8b, Random,
334		test_utils::check_transpose_all_heights,
335		underlier::{U2, U4},
336	};
337
338	fn test_add_packed<P: PackedField + From<u128>>(a_val: u128, b_val: u128) {
339		let a = P::from(a_val);
340		let b = P::from(b_val);
341		let c = a + b;
342		for i in 0..P::WIDTH {
343			assert_eq!(c.get(i), a.get(i) + b.get(i));
344		}
345	}
346
347	fn test_mul_packed<P: PackedField>(a: P, b: P) {
348		let c = a * b;
349		for i in 0..P::WIDTH {
350			assert_eq!(c.get(i), a.get(i) * b.get(i));
351		}
352	}
353
354	fn test_mul_packed_random<P: PackedField>() {
355		let mut rng = StdRng::seed_from_u64(0);
356		test_mul_packed(P::random(&mut rng), P::random(&mut rng));
357	}
358
359	fn test_set_then_get<P: PackedField>() {
360		let mut rng = StdRng::seed_from_u64(0);
361		let mut elem = P::random(&mut rng);
362
363		let scalars = repeat_with(|| P::Scalar::random(&mut rng))
364			.take(P::WIDTH)
365			.collect::<Vec<_>>();
366
367		for (i, val) in scalars.iter().enumerate() {
368			elem.set(i, *val);
369		}
370		for (i, val) in scalars.iter().enumerate() {
371			assert_eq!(elem.get(i), *val);
372		}
373	}
374
375	fn test_serialize_then_deserialize<P: PackedField + DeserializeBytes + SerializeBytes>() {
376		let mut buffer = BytesMut::new();
377		let mut rng = StdRng::seed_from_u64(0);
378		let packed = P::random(&mut rng);
379		packed.serialize(&mut buffer).unwrap();
380
381		let mut read_buffer = buffer.freeze();
382
383		assert_eq!(P::deserialize(&mut read_buffer).unwrap(), packed);
384	}
385
386	#[test]
387	fn test_set_then_get_128b() {
388		test_set_then_get::<PackedGhash1x128b>();
389		test_set_then_get::<PackedGhash2x128b>();
390		test_set_then_get::<PackedGhash4x128b>();
391	}
392
393	#[test]
394	fn test_serialize_then_deserialize_128b() {
395		test_serialize_then_deserialize::<PackedGhash1x128b>();
396		test_serialize_then_deserialize::<PackedGhash2x128b>();
397		test_serialize_then_deserialize::<PackedGhash4x128b>();
398	}
399
400	#[test]
401	fn test_serialize_deserialize_different_packing_width() {
402		let mut rng = StdRng::seed_from_u64(0);
403
404		let packed0 = PackedGhash1x128b::random(&mut rng);
405		let packed1 = PackedGhash1x128b::random(&mut rng);
406
407		let mut buffer = BytesMut::new();
408		packed0.serialize(&mut buffer).unwrap();
409		packed1.serialize(&mut buffer).unwrap();
410
411		let mut read_buffer = buffer.freeze();
412		let packed01 = PackedGhash2x128b::deserialize(&mut read_buffer).unwrap();
413
414		assert!(
415			packed01
416				.iter()
417				.zip([packed0, packed1])
418				.all(|(x, y)| x == y.get(0))
419		);
420	}
421
422	// TODO: Generate lots more proptests using macros
423	proptest! {
424		#[test]
425		fn test_add_packed_128x1b(a_val in any::<u128>(), b_val in any::<u128>()) {
426			test_add_packed::<PackedBinaryField128x1b>(a_val, b_val);
427		}
428
429		#[test]
430		fn test_add_packed_16x8b(a_val in any::<u128>(), b_val in any::<u128>()) {
431			test_add_packed::<PackedRijndael16x8b>(a_val, b_val);
432		}
433
434		#[test]
435		fn test_add_packed_1x128b(a_val in any::<u128>(), b_val in any::<u128>()) {
436			test_add_packed::<PackedGhash1x128b>(a_val, b_val);
437		}
438	}
439
440	#[test]
441	fn test_mul_packed_256x1b() {
442		test_mul_packed_random::<PackedBinaryField256x1b>();
443	}
444
445	#[test]
446	fn test_mul_packed_32x8b() {
447		test_mul_packed_random::<PackedRijndael32x8b>();
448	}
449
450	#[test]
451	fn test_mul_packed_2x128b() {
452		test_mul_packed_random::<PackedGhash2x128b>();
453	}
454
455	packed_field_tests!(packed_8x1b, PackedBinaryField8x1b);
456	packed_field_tests!(packed_16x1b, PackedBinaryField16x1b);
457	packed_field_tests!(packed_32x1b, PackedBinaryField32x1b);
458	packed_field_tests!(packed_64x1b, PackedBinaryField64x1b);
459	packed_field_tests!(packed_128x1b, PackedBinaryField128x1b);
460	packed_field_tests!(packed_256x1b, PackedBinaryField256x1b);
461	packed_field_tests!(packed_512x1b, PackedBinaryField512x1b);
462
463	proptest! {
464		#[test]
465		fn test_interleave_2b(a_val in 0u8..3, b_val in 0u8..3) {
466			check_interleave_all_heights::<PackedBinaryField2x1b>(U2::new(a_val), U2::new(b_val));
467		}
468
469		#[test]
470		fn test_interleave_4b(a_val in 0u8..16, b_val in 0u8..16) {
471			check_interleave_all_heights::<PackedBinaryField4x1b>(U4::new(a_val), U4::new(b_val));
472		}
473
474		#[test]
475		fn test_interleave_8b(a_val in 0u8.., b_val in 0u8..) {
476			check_interleave_all_heights::<PackedBinaryField8x1b>(a_val, b_val);
477			check_interleave_all_heights::<PackedRijndael1x8b>(a_val, b_val);
478		}
479
480		#[test]
481		fn test_interleave_16b(a_val in 0u16.., b_val in 0u16..) {
482			check_interleave_all_heights::<PackedBinaryField16x1b>(a_val, b_val);
483		}
484
485		#[test]
486		fn test_interleave_32b(a_val in 0u32.., b_val in 0u32..) {
487			check_interleave_all_heights::<PackedBinaryField32x1b>(a_val, b_val);
488		}
489
490		#[test]
491		fn test_interleave_64b(a_val in 0u64.., b_val in 0u64..) {
492			check_interleave_all_heights::<PackedBinaryField64x1b>(a_val, b_val);
493		}
494
495		#[test]
496		#[allow(clippy::useless_conversion)] // this warning depends on the target platform
497		fn test_interleave_128b(a_val in 0u128.., b_val in 0u128..) {
498			check_interleave_all_heights::<PackedBinaryField128x1b>(a_val.into(), b_val.into());
499			check_interleave_all_heights::<PackedRijndael16x8b>(a_val.into(), b_val.into());
500			check_interleave_all_heights::<PackedGhash1x128b>(a_val.into(), b_val.into());
501		}
502
503		#[test]
504		fn test_interleave_256b(a_val in any::<[u128; 2]>(), b_val in any::<[u128; 2]>()) {
505			check_interleave_all_heights::<PackedBinaryField256x1b>(a_val.into(), b_val.into());
506			check_interleave_all_heights::<PackedRijndael32x8b>(a_val.into(), b_val.into());
507			check_interleave_all_heights::<PackedGhash2x128b>(a_val.into(), b_val.into());
508		}
509
510		#[test]
511		fn test_interleave_512b(a_val in any::<[u128; 4]>(), b_val in any::<[u128; 4]>()) {
512			check_interleave_all_heights::<PackedBinaryField512x1b>(a_val.into(), b_val.into());
513			check_interleave_all_heights::<PackedRijndael64x8b>(a_val.into(), b_val.into());
514			check_interleave_all_heights::<PackedGhash4x128b>(a_val.into(), b_val.into());
515		}
516
517		#[test]
518		fn check_transpose_2b(a_val in 0u8..3, b_val in 0u8..3) {
519			check_transpose_all_heights::<PackedBinaryField2x1b>(U2::new(a_val), U2::new(b_val));
520		}
521
522		#[test]
523		fn check_transpose_4b(a_val in 0u8..16, b_val in 0u8..16) {
524			check_transpose_all_heights::<PackedBinaryField4x1b>(U4::new(a_val), U4::new(b_val));
525		}
526
527		#[test]
528		fn check_transpose_8b(a_val in 0u8.., b_val in 0u8..) {
529			check_transpose_all_heights::<PackedBinaryField8x1b>(a_val, b_val);
530			check_transpose_all_heights::<PackedRijndael1x8b>(a_val, b_val);
531		}
532
533		#[test]
534		fn check_transpose_16b(a_val in 0u16.., b_val in 0u16..) {
535			check_transpose_all_heights::<PackedBinaryField16x1b>(a_val, b_val);
536		}
537
538		#[test]
539		fn check_transpose_32b(a_val in 0u32.., b_val in 0u32..) {
540			check_transpose_all_heights::<PackedBinaryField32x1b>(a_val, b_val);
541		}
542
543		#[test]
544		fn check_transpose_64b(a_val in 0u64.., b_val in 0u64..) {
545			check_transpose_all_heights::<PackedBinaryField64x1b>(a_val, b_val);
546		}
547
548		#[test]
549		#[allow(clippy::useless_conversion)] // this warning depends on the target platform
550		fn check_transpose_128b(a_val in 0u128.., b_val in 0u128..) {
551			check_transpose_all_heights::<PackedBinaryField128x1b>(a_val.into(), b_val.into());
552			check_transpose_all_heights::<PackedRijndael16x8b>(a_val.into(), b_val.into());
553			check_transpose_all_heights::<PackedGhash1x128b>(a_val.into(), b_val.into());
554		}
555
556		#[test]
557		fn check_transpose_256b(a_val in any::<[u128; 2]>(), b_val in any::<[u128; 2]>()) {
558			check_transpose_all_heights::<PackedBinaryField256x1b>(a_val.into(), b_val.into());
559			check_transpose_all_heights::<PackedRijndael32x8b>(a_val.into(), b_val.into());
560			check_transpose_all_heights::<PackedGhash2x128b>(a_val.into(), b_val.into());
561		}
562
563		#[test]
564		fn check_transpose_512b(a_val in any::<[u128; 4]>(), b_val in any::<[u128; 4]>()) {
565			check_transpose_all_heights::<PackedBinaryField512x1b>(a_val.into(), b_val.into());
566			check_transpose_all_heights::<PackedRijndael64x8b>(a_val.into(), b_val.into());
567			check_transpose_all_heights::<PackedGhash4x128b>(a_val.into(), b_val.into());
568		}
569	}
570
571	// The generic `SerializeBytes`/`DeserializeBytes` impls on `PackedPrimitiveType` round-trip
572	// across both integer underliers and (where applicable) SIMD underliers.
573	#[test]
574	fn test_serialize_roundtrip() {
575		fn check_roundtrip<
576			P: PackedField + SerializeBytes + DeserializeBytes + PartialEq + Debug,
577		>(
578			rng: &mut StdRng,
579		) {
580			let value = P::random(rng);
581			let mut buf = BytesMut::new();
582			value.serialize(&mut buf).unwrap();
583			let deserialized = P::deserialize(buf.freeze()).unwrap();
584			assert_eq!(value, deserialized);
585		}
586
587		let mut rng = StdRng::seed_from_u64(0);
588		check_roundtrip::<PackedBinaryField8x1b>(&mut rng);
589		check_roundtrip::<PackedBinaryField64x1b>(&mut rng);
590		check_roundtrip::<PackedBinaryField128x1b>(&mut rng);
591		check_roundtrip::<PackedGhash1x128b>(&mut rng);
592	}
593
594	// `FixedSizeSerializeBytes` propagates from the underlier. Integer-backed packed fields (here,
595	// `u8`- and `u64`-backed on every arch) report the underlier's byte size.
596	#[test]
597	fn test_fixed_size_byte_size() {
598		assert_eq!(<PackedBinaryField8x1b as FixedSizeSerializeBytes>::BYTE_SIZE, 1);
599		assert_eq!(<PackedBinaryField64x1b as FixedSizeSerializeBytes>::BYTE_SIZE, 8);
600	}
601}