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}