Skip to main content

binius_field/
util.rs

1// Copyright 2024-2025 Irreducible Inc.
2
3use std::iter;
4
5use crate::{Field, PackedField, UnderlierType, field::FieldOps};
6
7/// An arithmetic function over field elements, generic in the field it evaluates in.
8///
9/// A closure `FnOnce(&[F]) -> F` is monomorphic: it runs in one fixed field.
10/// Carrying the genericity on the method instead lets one value run in many fields:
11/// - natively, in the verifier's own base field `F`;
12/// - over any larger field `E` whose scalar is `F`.
13pub trait FieldFn<F: Field> {
14	/// Evaluates the function on `inputs` in the field `E`, returning one element.
15	///
16	/// The scalar of `E` is the base field `F`.
17	/// The `From<F>` bound lets the function embed base-field constants into `E`.
18	fn call<E: FieldOps<Scalar = F> + From<F>>(&self, inputs: &[E]) -> E;
19
20	/// Evaluates the function on `inputs` natively in the base field `F`.
21	///
22	/// The default is `self.call::<F>(inputs)`; implementors may override with a base-field
23	/// specialized fast path (e.g. deferred `WideMul` reduction) that the generic
24	/// [`call`](Self::call) — which cannot assume `E: WideMul` — can't express. Callers evaluating
25	/// in `F` should prefer this.
26	fn call_native(&self, inputs: &[F]) -> F {
27		self.call::<F>(inputs)
28	}
29}
30
31/// Expands an array of field elements into all possible subset sums.
32///
33/// For an input array `[a, b, c]`, this computes all possible sums of subsets:
34/// `[0, a, b, a+b, c, a+c, b+c, a+b+c]`
35///
36/// This is used to create lookup tables for the Method of Four Russians optimization,
37/// where we precompute all possible combinations of a small set of values to avoid
38/// doing the additions at runtime.
39///
40/// ## Type Parameters
41///
42/// * `F` - The field element type
43/// * `N` - Size of the input array
44/// * `N_EXP2` - Size of the output array, must be 2^N
45///
46/// ## Arguments
47///
48/// * `elems` - Input array of N field elements
49///
50/// ## Returns
51///
52/// An array of size N_EXP2 containing all possible subset sums of the input elements
53///
54/// ## Preconditions
55///
56/// * N_EXP2 must equal 2^N
57///
58/// ## Example
59///
60/// ```ignore
61/// let input = [F::ONE, F::from(2)];
62/// let sums = expand_subset_sums_array(input);
63/// // sums = [F::ZERO, F::ONE, F::from(2), F::from(3)]
64/// ```
65pub fn expand_subset_sums_array<P: PackedField, const N: usize, const N_EXP2: usize>(
66	elems: [P; N],
67) -> [P; N_EXP2] {
68	const {
69		assert!(N_EXP2 == 1 << N, "N_EXP2 must equal 2^N");
70	}
71
72	let mut expanded = [P::zero(); N_EXP2];
73	for (i, elem_i) in elems.into_iter().enumerate() {
74		let span = &mut expanded[..1 << (i + 1)];
75		let (lo_half, hi_half) = span.split_at_mut(1 << i);
76		for (lo_half_i, hi_half_i) in iter::zip(lo_half, hi_half) {
77			*hi_half_i = *lo_half_i + elem_i;
78		}
79	}
80	expanded
81}
82
83/// Expands `elems` into all `2^elems.len()` subset sums, indexed by subset bitmask.
84///
85/// The dynamically sized counterpart of [`expand_subset_sums_array`], for callers whose element
86/// count is only known at run time. Entry `mask` holds the sum of `elems[i]` over every bit `i` set
87/// in `mask`, so entry `0` is zero and entry `2^i` is `elems[i]`.
88///
89/// Each entry costs one addition, where summing a subset directly would cost one per set bit.
90///
91/// ## Preconditions
92///
93/// * `elems.len()` must be less than `usize::BITS`
94pub fn expand_subset_sums<P: PackedField>(elems: &[P]) -> Vec<P> {
95	assert!(elems.len() < usize::BITS as usize); // precondition
96
97	let mut expanded = vec![P::zero(); 1 << elems.len()];
98	for (i, &elem_i) in elems.iter().enumerate() {
99		let (lo_half, hi_half) = expanded[..1 << (i + 1)].split_at_mut(1 << i);
100		for (lo_half_i, hi_half_i) in iter::zip(lo_half, hi_half) {
101			*hi_half_i = *lo_half_i + elem_i;
102		}
103	}
104	expanded
105}
106
107/// Expands `elems` into all `2^elems.len()` subset products, indexed by subset bitmask.
108///
109/// The multiplicative counterpart of [`expand_subset_sums`].
110/// Entry `mask` holds the product of `elems[i]` over every bit `i` set in `mask`.
111/// So entry `0` is one and entry `2^i` is `elems[i]`.
112///
113/// This is the tensor expansion `(1, elems[0]) x ... x (1, elems[k-1])`.
114/// A caller holding `k` factors of a product basis recovers all `2^k` basis elements from them.
115///
116/// Each entry costs one multiplication.
117/// Multiplying a subset directly would cost one per set bit.
118///
119/// ## Preconditions
120///
121/// * `elems.len()` must be less than `usize::BITS`
122pub fn expand_subset_products<P: PackedField>(elems: &[P]) -> Vec<P> {
123	assert!(elems.len() < usize::BITS as usize); // precondition
124
125	let mut expanded = vec![P::one(); 1 << elems.len()];
126	for (i, &elem_i) in elems.iter().enumerate() {
127		let (lo_half, hi_half) = expanded[..1 << (i + 1)].split_at_mut(1 << i);
128		for (lo_half_i, hi_half_i) in iter::zip(lo_half, hi_half) {
129			*hi_half_i = *lo_half_i * elem_i;
130		}
131	}
132	expanded
133}
134
135/// Expands `elems` into all `2^N` subset XOR combinations, indexed by subset bitmask.
136///
137/// Entry `mask` holds the XOR of `elems[i]` over every bit `i` set in `mask`. This is the
138/// bitwise-XOR analogue of [`expand_subset_sums_array`] over raw underliers, used to build Method
139/// of Four Russians lookup tables.
140///
141/// ## Preconditions
142///
143/// * `N_EXP2` must equal `2^N`
144pub fn expand_subset_xors<U: UnderlierType, const N: usize, const N_EXP2: usize>(
145	elems: [U; N],
146) -> [U; N_EXP2] {
147	const {
148		assert!(N_EXP2 == 1 << N, "N_EXP2 must equal 2^N");
149	}
150
151	let mut expanded = [U::ZERO; N_EXP2];
152	for (i, elem_i) in elems.into_iter().enumerate() {
153		let span = &mut expanded[..1 << (i + 1)];
154		let (lo_half, hi_half) = span.split_at_mut(1 << i);
155		for (lo_half_i, hi_half_i) in iter::zip(lo_half, hi_half) {
156			*hi_half_i = *lo_half_i ^ elem_i;
157		}
158	}
159	expanded
160}
161
162#[cfg(test)]
163mod tests {
164	use std::array;
165
166	use proptest::prelude::*;
167	use rand::{SeedableRng, rngs::StdRng};
168
169	use super::*;
170	use crate::{Ghash128b, Random};
171
172	type F = Ghash128b;
173
174	/// Expands `N` random elements and asserts that entry `index` of the resulting `2^N`-sized
175	/// lookup table equals the subset sum selected by the set bits of `index`.
176	fn check_subset_sums<const N: usize, const N_EXP2: usize>(seed: u64, index: usize) {
177		let mut rng = StdRng::seed_from_u64(seed);
178		let elems: [F; N] = array::from_fn(|_| F::random(&mut rng));
179
180		let result = expand_subset_sums_array::<_, N, N_EXP2>(elems);
181		assert_eq!(result.len(), N_EXP2);
182
183		// Compute expected sum based on the binary representation of index.
184		let index = index % N_EXP2;
185		let mut expected = F::ZERO;
186		for (bit_pos, &elem) in elems.iter().enumerate() {
187			if (index >> bit_pos) & 1 == 1 {
188				expected += elem;
189			}
190		}
191
192		assert_eq!(
193			result[index], expected,
194			"index {index} should hold the subset sum for its binary representation"
195		);
196	}
197
198	proptest! {
199		#[test]
200		fn test_expand_subset_sums_array_correctness(
201			n in 0usize..=8,  // Input length (small to avoid exponential blowup)
202			index in 0usize..256,  // Index to check
203		) {
204			// Dispatch to the const-generic helper: `expand_subset_sums_array` needs the output
205			// length `2^n` at compile time.
206			match n {
207				0 => check_subset_sums::<0, 1>(n as u64, index),
208				1 => check_subset_sums::<1, 2>(n as u64, index),
209				2 => check_subset_sums::<2, 4>(n as u64, index),
210				3 => check_subset_sums::<3, 8>(n as u64, index),
211				4 => check_subset_sums::<4, 16>(n as u64, index),
212				5 => check_subset_sums::<5, 32>(n as u64, index),
213				6 => check_subset_sums::<6, 64>(n as u64, index),
214				7 => check_subset_sums::<7, 128>(n as u64, index),
215				8 => check_subset_sums::<8, 256>(n as u64, index),
216				_ => unreachable!("n is constrained to 0..=8"),
217			}
218		}
219	}
220	proptest! {
221		#[test]
222		fn expand_subset_products_selects_the_product_over_set_bits(seed: u64, n in 0usize..=8) {
223			let mut rng = StdRng::seed_from_u64(seed);
224			let elems = (0..n)
225				.map(|_| F::random(&mut rng))
226				.collect::<Vec<_>>();
227
228			let expanded = expand_subset_products(&elems);
229			prop_assert_eq!(expanded.len(), 1 << n);
230
231			// Entry `mask` multiplies exactly the elements whose bit is set in `mask`.
232			for (mask, &entry) in expanded.iter().enumerate() {
233				let expected = (0..n)
234					.filter(|i| (mask >> i) & 1 == 1)
235					.fold(F::ONE, |acc, i| acc * elems[i]);
236				prop_assert_eq!(entry, expected);
237			}
238		}
239	}
240}