Skip to main content

binius_field/
util.rs

1// Copyright 2024-2025 Irreducible Inc.
2
3use std::iter;
4
5use crate::{Field, PackedField, Underlier, 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/// Iterate the powers of a given value, beginning with 1 (the 0'th power).
32pub fn powers<F: FieldOps>(val: F) -> impl Iterator<Item = F> {
33	iter::successors(Some(F::one()), move |power| Some(power.clone() * val.clone()))
34}
35
36/// Expands an array of field elements into all possible subset sums.
37///
38/// For an input array `[a, b, c]`, this computes all possible sums of subsets:
39/// `[0, a, b, a+b, c, a+c, b+c, a+b+c]`
40///
41/// This is used to create lookup tables for the Method of Four Russians optimization,
42/// where we precompute all possible combinations of a small set of values to avoid
43/// doing the additions at runtime.
44///
45/// ## Type Parameters
46///
47/// * `F` - The field element type
48/// * `N` - Size of the input array
49/// * `N_EXP2` - Size of the output array, must be 2^N
50///
51/// ## Arguments
52///
53/// * `elems` - Input array of N field elements
54///
55/// ## Returns
56///
57/// An array of size N_EXP2 containing all possible subset sums of the input elements
58///
59/// ## Preconditions
60///
61/// * N_EXP2 must equal 2^N
62///
63/// ## Example
64///
65/// ```ignore
66/// let input = [F::ONE, F::from(2)];
67/// let sums = expand_subset_sums_array(input);
68/// // sums = [F::ZERO, F::ONE, F::from(2), F::from(3)]
69/// ```
70pub fn expand_subset_sums_array<P: PackedField, const N: usize, const N_EXP2: usize>(
71	elems: [P; N],
72) -> [P; N_EXP2] {
73	const {
74		assert!(N_EXP2 == 1 << N, "N_EXP2 must equal 2^N");
75	}
76
77	let mut expanded = [P::zero(); N_EXP2];
78	for (i, elem_i) in elems.into_iter().enumerate() {
79		let span = &mut expanded[..1 << (i + 1)];
80		let (lo_half, hi_half) = span.split_at_mut(1 << i);
81		for (lo_half_i, hi_half_i) in iter::zip(lo_half, hi_half) {
82			*hi_half_i = *lo_half_i + elem_i;
83		}
84	}
85	expanded
86}
87
88/// Expands `elems` into all `2^elems.len()` subset sums, indexed by subset bitmask.
89///
90/// The dynamically sized counterpart of [`expand_subset_sums_array`], for callers whose element
91/// count is only known at run time. Entry `mask` holds the sum of `elems[i]` over every bit `i` set
92/// in `mask`, so entry `0` is zero and entry `2^i` is `elems[i]`.
93///
94/// Each entry costs one addition, where summing a subset directly would cost one per set bit.
95///
96/// ## Preconditions
97///
98/// * `elems.len()` must be less than `usize::BITS`
99pub fn expand_subset_sums<P: PackedField>(elems: &[P]) -> Vec<P> {
100	assert!(elems.len() < usize::BITS as usize); // precondition
101
102	let mut expanded = vec![P::zero(); 1 << elems.len()];
103	for (i, &elem_i) in elems.iter().enumerate() {
104		let (lo_half, hi_half) = expanded[..1 << (i + 1)].split_at_mut(1 << i);
105		for (lo_half_i, hi_half_i) in iter::zip(lo_half, hi_half) {
106			*hi_half_i = *lo_half_i + elem_i;
107		}
108	}
109	expanded
110}
111
112/// Expands `elems` into all `2^elems.len()` subset products, indexed by subset bitmask.
113///
114/// The multiplicative counterpart of [`expand_subset_sums`].
115/// Entry `mask` holds the product of `elems[i]` over every bit `i` set in `mask`.
116/// So entry `0` is one and entry `2^i` is `elems[i]`.
117///
118/// This is the tensor expansion `(1, elems[0]) x ... x (1, elems[k-1])`.
119/// A caller holding `k` factors of a product basis recovers all `2^k` basis elements from them.
120///
121/// Each entry costs one multiplication.
122/// Multiplying a subset directly would cost one per set bit.
123///
124/// ## Preconditions
125///
126/// * `elems.len()` must be less than `usize::BITS`
127pub fn expand_subset_products<P: PackedField>(elems: &[P]) -> Vec<P> {
128	assert!(elems.len() < usize::BITS as usize); // precondition
129
130	let mut expanded = vec![P::one(); 1 << elems.len()];
131	for (i, &elem_i) in elems.iter().enumerate() {
132		let (lo_half, hi_half) = expanded[..1 << (i + 1)].split_at_mut(1 << i);
133		for (lo_half_i, hi_half_i) in iter::zip(lo_half, hi_half) {
134			*hi_half_i = *lo_half_i * elem_i;
135		}
136	}
137	expanded
138}
139
140/// Expands `elems` into all `2^N` subset XOR combinations, indexed by subset bitmask.
141///
142/// Entry `mask` holds the XOR of `elems[i]` over every bit `i` set in `mask`. This is the
143/// bitwise-XOR analogue of [`expand_subset_sums_array`] over raw underliers, used to build Method
144/// of Four Russians lookup tables.
145///
146/// ## Preconditions
147///
148/// * `N_EXP2` must equal `2^N`
149pub fn expand_subset_xors<U: Underlier, const N: usize, const N_EXP2: usize>(
150	elems: [U; N],
151) -> [U; N_EXP2] {
152	const {
153		assert!(N_EXP2 == 1 << N, "N_EXP2 must equal 2^N");
154	}
155
156	let mut expanded = [U::ZERO; N_EXP2];
157	for (i, elem_i) in elems.into_iter().enumerate() {
158		let span = &mut expanded[..1 << (i + 1)];
159		let (lo_half, hi_half) = span.split_at_mut(1 << i);
160		for (lo_half_i, hi_half_i) in iter::zip(lo_half, hi_half) {
161			*hi_half_i = *lo_half_i ^ elem_i;
162		}
163	}
164	expanded
165}
166
167#[cfg(test)]
168mod tests {
169	use std::array;
170
171	use proptest::prelude::*;
172	use rand::{SeedableRng, rngs::StdRng};
173
174	use super::*;
175	use crate::{Ghash128b, Random};
176
177	#[test]
178	fn test_powers_against_pow() {
179		// The iterator starts at the 0'th power, so entry i must equal the base raised to i.
180		let generator = Ghash128b::MULTIPLICATIVE_GENERATOR;
181		let power_values: Vec<_> = powers(generator).take(10).collect();
182
183		for (i, power) in power_values.iter().enumerate() {
184			assert_eq!(*power, generator.pow(i as u64));
185		}
186	}
187
188	type F = Ghash128b;
189
190	/// Expands `N` random elements and asserts that entry `index` of the resulting `2^N`-sized
191	/// lookup table equals the subset sum selected by the set bits of `index`.
192	fn check_subset_sums<const N: usize, const N_EXP2: usize>(seed: u64, index: usize) {
193		let mut rng = StdRng::seed_from_u64(seed);
194		let elems: [F; N] = array::from_fn(|_| F::random(&mut rng));
195
196		let result = expand_subset_sums_array::<_, N, N_EXP2>(elems);
197		assert_eq!(result.len(), N_EXP2);
198
199		// Compute expected sum based on the binary representation of index.
200		let index = index % N_EXP2;
201		let mut expected = F::ZERO;
202		for (bit_pos, &elem) in elems.iter().enumerate() {
203			if (index >> bit_pos) & 1 == 1 {
204				expected += elem;
205			}
206		}
207
208		assert_eq!(
209			result[index], expected,
210			"index {index} should hold the subset sum for its binary representation"
211		);
212	}
213
214	proptest! {
215		#[test]
216		fn test_expand_subset_sums_array_correctness(
217			n in 0usize..=8,  // Input length (small to avoid exponential blowup)
218			index in 0usize..256,  // Index to check
219		) {
220			// Dispatch to the const-generic helper: `expand_subset_sums_array` needs the output
221			// length `2^n` at compile time.
222			match n {
223				0 => check_subset_sums::<0, 1>(n as u64, index),
224				1 => check_subset_sums::<1, 2>(n as u64, index),
225				2 => check_subset_sums::<2, 4>(n as u64, index),
226				3 => check_subset_sums::<3, 8>(n as u64, index),
227				4 => check_subset_sums::<4, 16>(n as u64, index),
228				5 => check_subset_sums::<5, 32>(n as u64, index),
229				6 => check_subset_sums::<6, 64>(n as u64, index),
230				7 => check_subset_sums::<7, 128>(n as u64, index),
231				8 => check_subset_sums::<8, 256>(n as u64, index),
232				_ => unreachable!("n is constrained to 0..=8"),
233			}
234		}
235	}
236	proptest! {
237		#[test]
238		fn expand_subset_products_selects_the_product_over_set_bits(seed: u64, n in 0usize..=8) {
239			let mut rng = StdRng::seed_from_u64(seed);
240			let elems = (0..n)
241				.map(|_| F::random(&mut rng))
242				.collect::<Vec<_>>();
243
244			let expanded = expand_subset_products(&elems);
245			prop_assert_eq!(expanded.len(), 1 << n);
246
247			// Entry `mask` multiplies exactly the elements whose bit is set in `mask`.
248			for (mask, &entry) in expanded.iter().enumerate() {
249				let expected = (0..n)
250					.filter(|i| (mask >> i) & 1 == 1)
251					.fold(F::ONE, |acc, i| acc * elems[i]);
252				prop_assert_eq!(entry, expected);
253			}
254		}
255	}
256}