Skip to main content

binius_field/
sliced_packed_field.rs

1// Copyright 2026 The Binius Developers
2
3//! A packed extension field in a *sliced* (struct-of-arrays) memory layout.
4//!
5//! An extension field element `x = c_0·β_0 + … + c_{N-1}·β_{N-1}` over a subfield `FSub` is a
6//! vector of `N = DEGREE` subfield coordinates in the basis `{β_j}`. [`SlicedPackedField`] packs
7//! `WIDTH` such extension scalars by storing each *coordinate* of every element in its own packed
8//! subfield register:
9//!
10//! ```text
11//! coords[0] = [ c_0(x_0), c_0(x_1), …, c_0(x_{WIDTH-1}) ]   // β_0 coordinate of every lane
12//! coords[1] = [ c_1(x_0), c_1(x_1), …, c_1(x_{WIDTH-1}) ]   // β_1 coordinate of every lane
13//! …
14//! ```
15//!
16//! The coordinates of a single extension element are *not* adjacent in memory — hence "sliced".
17//! This is the layout that lets a batch multiply run as a handful of packed subfield multiplies
18//! over the whole batch (Karatsuba over the extension), instead of a schoolbook product per lane.
19//!
20//! # What is generic and what is not
21//!
22//! Everything that does not depend on the extension's multiplication rule is provided here,
23//! generically, for any `PSub: PackedField` and any scalar `F: ExtensionField<PSub::Scalar>`:
24//! scalar access, broadcast, iteration, addition, masking, interleave/unzip/spread, and
25//! `square_transpose`. The layout makes these uniform: a lane permutation (interleave, spread) or a
26//! bitwise op (add, mask) applies to each coordinate register identically, and scalar access reads
27//! or writes the `N` coordinate registers at one lane through the [`ExtensionField`] basis.
28//!
29//! The field arithmetic is written per concrete extension: a type supplies a custom [`WideMul`]
30//! (the widening multiply, with a deferred reduction), plus [`Square`] and [`InvertOrZero`]. `Mul`
31//! is then blanket-implemented as `reduce(wide_mul(a, b))`, mirroring how the scalar fields are
32//! defined. A concrete extension whose coordinate `PSub` is a [`PackedPrimitiveType`] can reach
33//! into its underlier for optimizations a generic packed field cannot express. See
34//! `packed_ghash_sq` for the GHASH² instantiation.
35//!
36//! # The `F` type parameter
37//!
38//! The scalar `F` is carried as a phantom parameter rather than derived from `(PSub, N)`. A degree
39//! and a subfield do not name a unique extension, and Rust requires a type parameter used only as
40//! `type Scalar = F` to appear in the self type. This mirrors [`PackedPrimitiveType<U, Scalar>`],
41//! which likewise carries its scalar. The invariant `N == <F as ExtensionField<PSub::Scalar>>::
42//! DEGREE` is upheld by the concrete type aliases.
43//!
44//! [`PackedPrimitiveType`]: crate::arch::PackedPrimitiveType
45//! [`PackedPrimitiveType<U, Scalar>`]: crate::arch::PackedPrimitiveType
46
47use std::{
48	array,
49	fmt::Debug,
50	iter::{Product, Sum},
51	marker::PhantomData,
52	ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign},
53};
54
55use bytemuck::Zeroable;
56use rand::distr::{Distribution, StandardUniform};
57
58use crate::{
59	Divisible, ExtensionField, Field, Maskable, PackedField, WideMul,
60	arithmetic_traits::{InvertOrZero, Square},
61	field::FieldOps,
62};
63
64/// A packed extension field stored as `N` packed subfield coordinate registers.
65///
66/// `F` is the extension scalar, `PSub` the packed subfield holding one coordinate of every lane,
67/// and `N = <F as ExtensionField<PSub::Scalar>>::DEGREE` the extension degree. See the module
68/// documentation for the layout and for which operations are generic here versus supplied per
69/// concrete extension.
70///
71/// Concrete packings are named through type aliases; see `packed_ghash_sq` for GHASH².
72///
73/// ```
74/// use binius_field::{Divisible, Field, GhashSq256b, PackedField, SlicedGhashSq2x256b};
75///
76/// let scalars = [GhashSq256b::ONE, GhashSq256b::MULTIPLICATIVE_GENERATOR];
77/// let a = SlicedGhashSq2x256b::from_scalars(scalars);
78/// let squared = a * a;
79/// for i in 0..SlicedGhashSq2x256b::WIDTH {
80///     assert_eq!(squared.get(i), scalars[i] * scalars[i]);
81/// }
82/// ```
83#[repr(transparent)]
84pub struct SlicedPackedField<F, PSub, const N: usize>([PSub; N], PhantomData<F>);
85
86// `Clone`/`Copy`/`Eq` are implemented by hand rather than derived: the `PhantomData<F>` field is
87// always `Copy`, so these should bound only on `PSub` and not drag an `F: Copy` obligation into
88// every generic impl.
89impl<F, PSub: Copy, const N: usize> Clone for SlicedPackedField<F, PSub, N> {
90	#[inline]
91	fn clone(&self) -> Self {
92		*self
93	}
94}
95
96impl<F, PSub: Copy, const N: usize> Copy for SlicedPackedField<F, PSub, N> {}
97
98impl<F, PSub: PartialEq, const N: usize> PartialEq for SlicedPackedField<F, PSub, N> {
99	#[inline]
100	fn eq(&self, other: &Self) -> bool {
101		self.0 == other.0
102	}
103}
104
105impl<F, PSub: Eq, const N: usize> Eq for SlicedPackedField<F, PSub, N> {}
106
107impl<F, PSub: PackedField, const N: usize> SlicedPackedField<F, PSub, N> {
108	/// Wraps `N` coordinate registers, where `coords[j]` holds the `β_j` coordinate of every lane.
109	#[inline]
110	pub const fn from_coords(coords: [PSub; N]) -> Self {
111		Self(coords, PhantomData)
112	}
113
114	/// Unwraps the `N` coordinate registers.
115	#[inline]
116	pub const fn to_coords(self) -> [PSub; N] {
117		self.0
118	}
119}
120
121impl<F, PSub: PackedField, const N: usize> Default for SlicedPackedField<F, PSub, N> {
122	#[inline]
123	fn default() -> Self {
124		Self::from_coords([PSub::default(); N])
125	}
126}
127
128impl<F, PSub: PackedField, const N: usize> Debug for SlicedPackedField<F, PSub, N> {
129	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
130		write!(f, "SlicedPacked<{N}>({:?})", self.0)
131	}
132}
133
134// SAFETY: the struct is `[PSub; N]` plus a zero-sized `PhantomData`; an all-zero bit pattern is a
135// valid `[PSub; N]` whenever `PSub: Zeroable`.
136unsafe impl<F, PSub: Zeroable, const N: usize> Zeroable for SlicedPackedField<F, PSub, N> {}
137
138// --- Additive group: coordinate-wise (a binary field is characteristic two, so neg is identity).
139
140impl<F, PSub: PackedField, const N: usize> Neg for SlicedPackedField<F, PSub, N> {
141	type Output = Self;
142
143	#[inline]
144	fn neg(self) -> Self {
145		self
146	}
147}
148
149impl<F, PSub: PackedField, const N: usize> Add for SlicedPackedField<F, PSub, N> {
150	type Output = Self;
151
152	#[inline]
153	fn add(self, rhs: Self) -> Self {
154		Self::from_coords(array::from_fn(|j| self.0[j] + rhs.0[j]))
155	}
156}
157
158impl<F, PSub: PackedField, const N: usize> Sub for SlicedPackedField<F, PSub, N> {
159	type Output = Self;
160
161	#[inline]
162	fn sub(self, rhs: Self) -> Self {
163		Self::from_coords(array::from_fn(|j| self.0[j] - rhs.0[j]))
164	}
165}
166
167impl<F, PSub: PackedField, const N: usize> Add<&Self> for SlicedPackedField<F, PSub, N> {
168	type Output = Self;
169
170	#[inline]
171	fn add(self, rhs: &Self) -> Self {
172		self + *rhs
173	}
174}
175
176impl<F, PSub: PackedField, const N: usize> Sub<&Self> for SlicedPackedField<F, PSub, N> {
177	type Output = Self;
178
179	#[inline]
180	fn sub(self, rhs: &Self) -> Self {
181		self - *rhs
182	}
183}
184
185impl<F, PSub: PackedField, const N: usize> AddAssign for SlicedPackedField<F, PSub, N> {
186	#[inline]
187	fn add_assign(&mut self, rhs: Self) {
188		*self = *self + rhs;
189	}
190}
191
192impl<F, PSub: PackedField, const N: usize> SubAssign for SlicedPackedField<F, PSub, N> {
193	#[inline]
194	fn sub_assign(&mut self, rhs: Self) {
195		*self = *self - rhs;
196	}
197}
198
199impl<F, PSub: PackedField, const N: usize> AddAssign<&Self> for SlicedPackedField<F, PSub, N> {
200	#[inline]
201	fn add_assign(&mut self, rhs: &Self) {
202		*self = *self + *rhs;
203	}
204}
205
206impl<F, PSub: PackedField, const N: usize> SubAssign<&Self> for SlicedPackedField<F, PSub, N> {
207	#[inline]
208	fn sub_assign(&mut self, rhs: &Self) {
209		*self = *self - *rhs;
210	}
211}
212
213impl<F, PSub: PackedField, const N: usize> Sum for SlicedPackedField<F, PSub, N> {
214	#[inline]
215	fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
216		iter.fold(Self::default(), |acc, x| acc + x)
217	}
218}
219
220impl<'a, F, PSub: PackedField, const N: usize> Sum<&'a Self> for SlicedPackedField<F, PSub, N> {
221	#[inline]
222	fn sum<I: Iterator<Item = &'a Self>>(iter: I) -> Self {
223		iter.fold(Self::default(), |acc, x| acc + *x)
224	}
225}
226
227// --- Multiply: blanket-implemented in terms of the per-extension `WideMul`.
228
229impl<F, PSub: PackedField, const N: usize> Mul for SlicedPackedField<F, PSub, N>
230where
231	Self: WideMul,
232{
233	type Output = Self;
234
235	#[inline]
236	fn mul(self, rhs: Self) -> Self {
237		Self::reduce(Self::wide_mul(self, rhs))
238	}
239}
240
241impl<F, PSub: PackedField, const N: usize> Mul<&Self> for SlicedPackedField<F, PSub, N>
242where
243	Self: Mul<Output = Self>,
244{
245	type Output = Self;
246
247	#[inline]
248	fn mul(self, rhs: &Self) -> Self {
249		self * *rhs
250	}
251}
252
253impl<F, PSub: PackedField, const N: usize> MulAssign for SlicedPackedField<F, PSub, N>
254where
255	Self: Mul<Output = Self>,
256{
257	#[inline]
258	fn mul_assign(&mut self, rhs: Self) {
259		*self = *self * rhs;
260	}
261}
262
263impl<F, PSub: PackedField, const N: usize> MulAssign<&Self> for SlicedPackedField<F, PSub, N>
264where
265	Self: Mul<Output = Self>,
266{
267	#[inline]
268	fn mul_assign(&mut self, rhs: &Self) {
269		*self = *self * *rhs;
270	}
271}
272
273impl<F, PSub: PackedField, const N: usize> Product for SlicedPackedField<F, PSub, N>
274where
275	F: ExtensionField<PSub::Scalar>,
276	Self: Mul<Output = Self>,
277{
278	#[inline]
279	fn product<I: Iterator<Item = Self>>(iter: I) -> Self {
280		iter.fold(<Self as Divisible<F>>::broadcast(F::ONE), |acc, x| acc * x)
281	}
282}
283
284impl<'a, F, PSub: PackedField, const N: usize> Product<&'a Self> for SlicedPackedField<F, PSub, N>
285where
286	F: ExtensionField<PSub::Scalar>,
287	Self: Mul<Output = Self>,
288{
289	#[inline]
290	fn product<I: Iterator<Item = &'a Self>>(iter: I) -> Self {
291		iter.fold(<Self as Divisible<F>>::broadcast(F::ONE), |acc, x| acc * *x)
292	}
293}
294
295// --- Scalar (extension-element) operations broadcast the scalar across every lane.
296
297impl<F, PSub, const N: usize> Add<F> for SlicedPackedField<F, PSub, N>
298where
299	F: ExtensionField<PSub::Scalar>,
300	PSub: PackedField,
301{
302	type Output = Self;
303
304	#[inline]
305	fn add(self, rhs: F) -> Self {
306		self + <Self as Divisible<F>>::broadcast(rhs)
307	}
308}
309
310impl<F, PSub, const N: usize> Sub<F> for SlicedPackedField<F, PSub, N>
311where
312	F: ExtensionField<PSub::Scalar>,
313	PSub: PackedField,
314{
315	type Output = Self;
316
317	#[inline]
318	fn sub(self, rhs: F) -> Self {
319		self - <Self as Divisible<F>>::broadcast(rhs)
320	}
321}
322
323impl<F, PSub, const N: usize> Mul<F> for SlicedPackedField<F, PSub, N>
324where
325	F: ExtensionField<PSub::Scalar>,
326	PSub: PackedField,
327	Self: Mul<Output = Self>,
328{
329	type Output = Self;
330
331	#[inline]
332	fn mul(self, rhs: F) -> Self {
333		self * <Self as Divisible<F>>::broadcast(rhs)
334	}
335}
336
337impl<F, PSub, const N: usize> AddAssign<F> for SlicedPackedField<F, PSub, N>
338where
339	F: ExtensionField<PSub::Scalar>,
340	PSub: PackedField,
341{
342	#[inline]
343	fn add_assign(&mut self, rhs: F) {
344		*self = *self + rhs;
345	}
346}
347
348impl<F, PSub, const N: usize> SubAssign<F> for SlicedPackedField<F, PSub, N>
349where
350	F: ExtensionField<PSub::Scalar>,
351	PSub: PackedField,
352{
353	#[inline]
354	fn sub_assign(&mut self, rhs: F) {
355		*self = *self - rhs;
356	}
357}
358
359impl<F, PSub, const N: usize> MulAssign<F> for SlicedPackedField<F, PSub, N>
360where
361	F: ExtensionField<PSub::Scalar>,
362	PSub: PackedField,
363	Self: Mul<Output = Self>,
364{
365	#[inline]
366	fn mul_assign(&mut self, rhs: F) {
367		*self = *self * <Self as Divisible<F>>::broadcast(rhs);
368	}
369}
370
371// --- Scalar access: a lane's extension scalar is read from / written to the `N` coordinates.
372
373impl<F, PSub, const N: usize> Divisible<F> for SlicedPackedField<F, PSub, N>
374where
375	F: ExtensionField<PSub::Scalar>,
376	PSub: PackedField,
377{
378	// One extension scalar per subfield lane: the packing width is `PSub::WIDTH`.
379	const LOG_N: usize = PSub::LOG_WIDTH;
380
381	#[inline]
382	fn value_iter(value: Self) -> impl ExactSizeIterator<Item = F> + Send + Clone {
383		(0..Self::N).map(move |i| unsafe { value.get_unchecked(i) })
384	}
385
386	#[inline]
387	fn ref_iter(value: &Self) -> impl ExactSizeIterator<Item = F> + Send + Clone + '_ {
388		let value = *value;
389		(0..Self::N).map(move |i| unsafe { value.get_unchecked(i) })
390	}
391
392	#[inline]
393	fn slice_iter(slice: &[Self]) -> impl ExactSizeIterator<Item = F> + Send + Clone + '_ {
394		(0..slice.len() * Self::N).map(move |global| {
395			let (elem, lane) = (global / Self::N, global % Self::N);
396			// SAFETY: `lane < Self::N` by construction.
397			unsafe { slice[elem].get_unchecked(lane) }
398		})
399	}
400
401	#[inline]
402	unsafe fn get_unchecked(&self, index: usize) -> F {
403		// SAFETY: `index < Self::N == PSub::WIDTH` by the caller's contract, so each coordinate
404		// access is in bounds.
405		F::from_bases((0..N).map(|j| unsafe { self.0[j].get_unchecked(index) }))
406	}
407
408	#[inline]
409	unsafe fn set_unchecked(&mut self, index: usize, val: F) {
410		for (j, coord) in self.0.iter_mut().enumerate() {
411			// SAFETY: `index < Self::N == PSub::WIDTH`; `j < N == DEGREE` so `get_base(j)` is in
412			// range.
413			unsafe {
414				coord.set_unchecked(index, val.get_base_unchecked(j));
415			}
416		}
417	}
418
419	#[inline]
420	fn broadcast(val: F) -> Self {
421		Self::from_coords(array::from_fn(|j| PSub::broadcast(val.get_base(j))))
422	}
423
424	#[inline]
425	fn from_iter(mut iter: impl Iterator<Item = F>) -> Self {
426		let mut result = Self::default();
427		for i in 0..Self::N {
428			match iter.next() {
429				Some(val) => result.set(i, val),
430				None => break,
431			}
432		}
433		result
434	}
435}
436
437// --- Lane masking applies the same per-lane mask to every coordinate.
438
439impl<F, PSub, const N: usize> Maskable<F> for SlicedPackedField<F, PSub, N>
440where
441	F: ExtensionField<PSub::Scalar>,
442	PSub: PackedField,
443{
444	type Mask = PSub::Mask;
445
446	#[inline]
447	fn make_mask(selectors: impl Iterator<Item = bool>) -> Self::Mask {
448		PSub::make_mask(selectors)
449	}
450
451	#[inline]
452	fn select(&self, mask: &Self::Mask) -> Self {
453		Self::from_coords(array::from_fn(|j| self.0[j].select(mask)))
454	}
455}
456
457impl<F, PSub, const N: usize> FieldOps for SlicedPackedField<F, PSub, N>
458where
459	F: ExtensionField<PSub::Scalar>,
460	PSub: PackedField,
461	Self: Square + InvertOrZero + Mul<Output = Self>,
462{
463	type Scalar = F;
464
465	#[inline]
466	fn zero() -> Self {
467		Self::default()
468	}
469
470	#[inline]
471	fn one() -> Self {
472		<Self as Divisible<F>>::broadcast(F::ONE)
473	}
474
475	fn square_transpose<FSub: Field>(elems: &mut [Self])
476	where
477		F: ExtensionField<FSub>,
478	{
479		let degree = <F as ExtensionField<FSub>>::DEGREE;
480		assert_eq!(elems.len(), degree);
481
482		// Transpose the `degree × degree` matrix of `FSub` coordinates independently in each lane.
483		// Reading a whole column before writing keeps the in-place update free of read-after-write
484		// hazards within the lane.
485		for lane in 0..PSub::WIDTH {
486			let column = (0..degree).map(|j| elems[j].get(lane)).collect::<Vec<F>>();
487			for (i, elem) in elems.iter_mut().enumerate() {
488				let transposed = <F as ExtensionField<FSub>>::from_bases(
489					(0..degree).map(|j| <F as ExtensionField<FSub>>::get_base(&column[j], i)),
490				);
491				elem.set(lane, transposed);
492			}
493		}
494	}
495}
496
497impl<F, PSub, const N: usize> PackedField for SlicedPackedField<F, PSub, N>
498where
499	F: ExtensionField<PSub::Scalar>,
500	PSub: PackedField,
501	Self:
502		Square + InvertOrZero + Mul<Output = Self> + WideMul<Output: Debug + Send + Sync + 'static>,
503{
504	// LOG_WIDTH defaults to `<Self as Divisible<F>>::LOG_N == PSub::LOG_WIDTH`; scalar access is
505	// provided by the `Divisible<F>` impl above.
506
507	#[inline]
508	fn interleave(self, other: Self, log_block_len: usize) -> (Self, Self) {
509		assert!(log_block_len < Self::LOG_WIDTH);
510		// The lane permutation is data-independent, so interleaving every coordinate register with
511		// the same block length keeps each lane's coordinates together.
512		let pairs: [(PSub, PSub); N] =
513			array::from_fn(|j| self.0[j].interleave(other.0[j], log_block_len));
514		(Self::from_coords(pairs.map(|(c, _)| c)), Self::from_coords(pairs.map(|(_, d)| d)))
515	}
516
517	#[inline]
518	fn unzip(self, other: Self, log_block_len: usize) -> (Self, Self) {
519		assert!(log_block_len < Self::LOG_WIDTH);
520		let pairs: [(PSub, PSub); N] =
521			array::from_fn(|j| self.0[j].unzip(other.0[j], log_block_len));
522		(Self::from_coords(pairs.map(|(c, _)| c)), Self::from_coords(pairs.map(|(_, d)| d)))
523	}
524
525	#[inline]
526	fn from_fn(mut f: impl FnMut(usize) -> Self::Scalar) -> Self {
527		let mut result = Self::default();
528		for i in 0..Self::WIDTH {
529			result.set(i, f(i));
530		}
531		result
532	}
533
534	#[inline]
535	unsafe fn spread_unchecked(self, log_block_len: usize, block_idx: usize) -> Self {
536		// Spread repeats a block of lanes; the same lane pattern applies to each coordinate.
537		Self::from_coords(array::from_fn(|j| unsafe {
538			self.0[j].spread_unchecked(log_block_len, block_idx)
539		}))
540	}
541}
542
543impl<F, PSub: PackedField, const N: usize> Distribution<SlicedPackedField<F, PSub, N>>
544	for StandardUniform
545{
546	#[inline]
547	fn sample<R: rand::Rng + ?Sized>(&self, rng: &mut R) -> SlicedPackedField<F, PSub, N> {
548		SlicedPackedField::from_coords(array::from_fn(|_| PSub::random(&mut *rng)))
549	}
550}