Skip to main content

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