Skip to main content

p3_binary_pcs/
fold.rs

1//! Folding a codeword over the Cantor additive domain.
2//!
3//! A codeword symbol at domain point `x` and its partner at `x + v_0 = x + 1` determine the
4//! two halves of the novel-basis decomposition
5//!
6//! ```text
7//!     f(x) = f_0(W_1(x)) + x * f_1(W_1(x))
8//! ```
9//!
10//! so `f_1 = f(x) + f(x + 1)` and `f_0 = f(x) + x * f_1`.
11//! The fold at `beta` is the evaluation-basis combination `(1 - beta) * f_0 + beta * f_1`.
12//!
13//! Over characteristic 2 that combination is `f_0 + beta * (f_0 + f_1)`.
14//! It is chosen because binding the multilinear's lowest variable computes the same thing.
15//!
16//! That is what lets the sumcheck and the codeword move in lockstep.
17//!
18//! Because `W_1(x) = x^2 + x` is `F_2`-linear with `W_1(v_0) = 0` and `W_1(v_i) = v_{i-1}`, and
19//! `domain_point` is `F_2`-linear too, `W_1(domain_point(i)) = domain_point(i >> 1)`: the folded
20//! domain is the same function at a halved index, so no per-round domain state is carried. The
21//! folding partners `domain_point(2j)` and `domain_point(2j + 1)` differ by `v_0 = 1`, so they
22//! are adjacent rows in memory.
23//!
24//! # Two fields
25//!
26//! Codeword symbols come from a committed alphabet, challenges from a wider field:
27//!
28//! ```text
29//!     base word       one symbol per committed element,  the narrow alphabet
30//!     folded words    one symbol per challenge element,  the wide field
31//! ```
32//!
33//! Only the first fold of a batch reads the narrow alphabet, and every later one stays wide.
34//! A narrow alphabet halves the leaves of the largest Merkle tree in the proof.
35//!
36//! The Cantor basis of a level is the leading part of every wider level's own.
37//! So a domain point means the same thing at both widths.
38//!
39//! A narrow level also sits in the leading bits of a wider one's representation.
40//! So widening a symbol is a zero-extension, which costs nothing and moves no bit.
41//!
42//! # Two routes
43//!
44//! Each output symbol costs two field multiplications.
45//!
46//! A tower-basis multiplication wraps a polynomial-basis one in three changes of basis.
47//! Each change of basis is sixteen dependent byte-table lookups, so the wrapper dominates.
48//!
49//! One route therefore runs in the polynomial basis.
50//! It multiplies with the widest carryless-multiply register the target offers.
51//!
52//! Symbols cross into that basis a block at a time and back a task at a time, so a target that
53//! changes the basis of many elements in one pass does so for every symbol the fold loads or
54//! produces.
55//!
56//! Input and output stay in the tower basis, so the folded codeword is unchanged bit for bit.
57//!
58//! A narrow alphabet widens on the way into that block, so its first fold takes the same route.
59//! No lifted copy of the codeword is ever materialised.
60//!
61//! The other route multiplies in the tower basis, one symbol at a time.
62//! It carries the widths the packed route has no register for.
63
64use alloc::vec;
65use alloc::vec::Vec;
66
67use p3_binary_dft::{EncodableLevel, domain_point, domain_point_steps};
68use p3_binary_field::{
69    BinaryField8, BinaryField16, BinaryField32, BinaryField64, BinaryField128, Ghash128, Poly64,
70    TowerLevel, poly_basis,
71};
72use p3_field::{Algebra, ExtensionField, Field, PackedValue, PrimeCharacteristicRing};
73use p3_maybe_rayon::prelude::*;
74use p3_sumcheck::strategy::IntoTranscriptField;
75
76/// The polynomial-basis type the fold multiplies with.
77///
78/// This is the widest carryless-multiply register the target offers.
79/// Where the target has none it is a single element, and every loop below still holds.
80type Packed = <Ghash128 as Field>::Packing;
81
82/// How many output symbols one packed multiplication covers.
83const WIDTH: usize = Packed::WIDTH;
84
85/// Output symbols one parallel task owns.
86///
87/// Large enough that dispatching a task is amortised over many folds.
88///
89/// Small enough to leave real parallelism at every codeword length this crate exercises.
90const FOLD_GRAIN: usize = 1 << 10;
91
92/// Input symbols one block of cosets holds, up to the arity where one packed group of cosets
93/// needs more.
94///
95/// A block this size crosses into the polynomial basis in one pass and then runs every round of
96/// the batch while it is still in cache.
97///
98/// A block never holds fewer cosets than a packed group of width `W`, so at arity `a` it holds
99/// `max(BLOCK_SYMBOLS, W << a)` symbols, which grows with the arity once `2^a` passes
100/// `BLOCK_SYMBOLS / W`.
101///
102/// One grain of pairs, so a single-round fold takes a whole task as one block.
103const BLOCK_SYMBOLS: usize = 2 * FOLD_GRAIN;
104
105// A task's first output index is a multiple of the grain.
106//
107// The lane-offset identity below needs that index to be a multiple of the packing width.
108//
109// So the grain must cover whole packed groups.
110const _: () = assert!(
111    FOLD_GRAIN.is_multiple_of(WIDTH),
112    "the fold grain must be a whole number of packed groups"
113);
114
115/// Fold one pair of the codeword.
116///
117/// `lo` is the symbol at `domain_point(2 * index)`, `hi` the one at `domain_point(2 * index + 1)`.
118///
119/// The two symbols come from the codeword's own alphabet, the challenge from the wider field.
120/// The novel-basis halves are formed in the narrow alphabet, and only the last step widens.
121#[inline]
122pub fn fold_pair<F, EF>(index: usize, beta: EF, lo: F, hi: F) -> EF
123where
124    F: TowerLevel,
125    EF: ExtensionField<F>,
126{
127    let x: F = domain_point(index << 1);
128    let f1 = lo + hi;
129    let f0 = lo + x * f1;
130    beta * (f0 + f1) + f0
131}
132
133/// Fold one round in the tower basis, widening the codeword into the challenge's field.
134///
135/// Each task owns a contiguous run of output symbols and the pairs feeding them.
136/// No two tasks therefore touch the same symbol on either side.
137///
138/// A task evaluates the domain once and then walks it by one addition per symbol.
139fn fold_round<F, EF>(codeword: &[F], beta: EF) -> Vec<EF>
140where
141    F: TowerLevel + Sync,
142    EF: ExtensionField<F> + Send + Sync,
143{
144    // A trailing unpaired symbol produces no output, so it is never read.
145    let num_pairs = codeword.len() / 2;
146
147    // Output symbol `j` sits at `domain_point(2 * j)`.
148    // The walk over those points is the one the transform already owns.
149    // Symbol indices run below the pair count.
150    // So one entry per bit of that count covers every step.
151    let steps = domain_point_steps::<F>(num_pairs.next_power_of_two().trailing_zeros() as usize);
152
153    let mut folded = EF::zero_vec(num_pairs);
154    folded
155        .par_chunks_mut(FOLD_GRAIN)
156        .zip(codeword.par_chunks(2 * FOLD_GRAIN))
157        .enumerate()
158        .for_each(|(task, (slots, symbols))| {
159            // Task `t` opens at output symbol `t * GRAIN`, the walk's only domain evaluation.
160            let start = task * FOLD_GRAIN;
161            let mut x: F = domain_point(start << 1);
162
163            // A trailing unpaired symbol feeds no slot, so it never enters the loop.
164            let (pairs, _) = symbols.as_chunks::<2>();
165
166            for (offset, (slot, pair)) in slots.iter_mut().zip(pairs).enumerate() {
167                // Advance the walk, which the task's first symbol has already reached.
168                if offset != 0 {
169                    x += steps[(start + offset).trailing_zeros() as usize];
170                }
171
172                // The novel-basis halves, both still in the narrow alphabet.
173                let f1 = pair[0] + pair[1];
174                let f0 = pair[0] + x * f1;
175
176                // The evaluation-basis combination, which is where the widening happens.
177                *slot = beta * (f0 + f1) + f0;
178            }
179        });
180    folded
181}
182
183/// Fold one round per challenge in the tower basis, materialising each round.
184fn fold_rounds_scalar<EF>(codeword: &[EF], challenges: &[EF]) -> Vec<EF>
185where
186    EF: TowerLevel + Send + Sync,
187{
188    let mut folded = fold_round(codeword, challenges[0]);
189    for &beta in &challenges[1..] {
190        folded = fold_round(&folded, beta);
191    }
192    folded
193}
194
195/// Lift the codeword with the batch's first challenge, then hand the rest to the wide field.
196///
197/// Only the first fold of a batch reads the narrow alphabet.
198/// Every later fold runs whichever route the wide field itself offers.
199fn fold_rounds_lifting<F, EF>(codeword: &[F], challenges: &[EF]) -> Vec<EF>
200where
201    F: TowerLevel + Sync,
202    EF: ExtensionField<F> + FoldAlphabet<EF> + Send + Sync,
203{
204    let lifted = fold_round(codeword, challenges[0]);
205    match challenges.len() {
206        1 => lifted,
207        _ => EF::fold_rounds(&lifted, &challenges[1..]),
208    }
209}
210
211/// A codeword alphabet, together with the field its folds are challenged from.
212///
213/// # Overview
214///
215/// One implementation names one route, and the set of them is this crate's route table:
216///
217/// ```text
218///     challenge field has a packed route   ->  that route, symbols widened on the way in
219///     challenge field has none             ->  one widening fold, then that field's route
220/// ```
221///
222/// Adding a challenge level is one implementation per alphabet it admits.
223/// Nothing else in this crate names a field.
224///
225/// # Panics
226///
227/// Every implementation panics unless there is at least one challenge.
228/// A fold with no round would leave every output slot at zero rather than fail.
229pub trait FoldAlphabet<EF: TowerLevel>: TowerLevel {
230    /// Fold one round per challenge, halving the codeword each time.
231    ///
232    /// `challenges` stays in the order the sumcheck drew it.
233    fn fold_rounds(codeword: &[Self], challenges: &[EF]) -> Vec<EF>;
234}
235
236impl FoldAlphabet<Self> for BinaryField128 {
237    fn fold_rounds(codeword: &[Self], challenges: &[Self]) -> Vec<Self> {
238        fold_rounds_packed(codeword, challenges)
239    }
240}
241
242impl FoldAlphabet<Self> for BinaryField64 {
243    fn fold_rounds(codeword: &[Self], challenges: &[Self]) -> Vec<Self> {
244        fold_rounds_scalar(codeword, challenges)
245    }
246}
247
248impl FoldAlphabet<BinaryField128> for BinaryField64 {
249    fn fold_rounds(codeword: &[Self], challenges: &[BinaryField128]) -> Vec<BinaryField128> {
250        fold_rounds_packed(codeword, challenges)
251    }
252}
253
254impl FoldAlphabet<BinaryField128> for BinaryField32 {
255    fn fold_rounds(codeword: &[Self], challenges: &[BinaryField128]) -> Vec<BinaryField128> {
256        fold_rounds_packed(codeword, challenges)
257    }
258}
259
260impl FoldAlphabet<BinaryField64> for BinaryField32 {
261    fn fold_rounds(codeword: &[Self], challenges: &[BinaryField64]) -> Vec<BinaryField64> {
262        fold_rounds_lifting(codeword, challenges)
263    }
264}
265
266impl FoldAlphabet<BinaryField128> for BinaryField16 {
267    fn fold_rounds(codeword: &[Self], challenges: &[BinaryField128]) -> Vec<BinaryField128> {
268        fold_rounds_packed(codeword, challenges)
269    }
270}
271
272impl FoldAlphabet<BinaryField64> for BinaryField16 {
273    fn fold_rounds(codeword: &[Self], challenges: &[BinaryField64]) -> Vec<BinaryField64> {
274        fold_rounds_lifting(codeword, challenges)
275    }
276}
277
278impl FoldAlphabet<BinaryField128> for BinaryField8 {
279    fn fold_rounds(codeword: &[Self], challenges: &[BinaryField128]) -> Vec<BinaryField128> {
280        fold_rounds_packed(codeword, challenges)
281    }
282}
283
284impl FoldAlphabet<BinaryField64> for BinaryField8 {
285    fn fold_rounds(codeword: &[Self], challenges: &[BinaryField64]) -> Vec<BinaryField64> {
286        fold_rounds_lifting(codeword, challenges)
287    }
288}
289
290/// Elements one task moves out of the sumcheck representation in a single block conversion.
291const REPR_CONVERSION_BLOCK: usize = 1 << 10;
292
293/// A challenge field and the arithmetic representation used by its residual sumcheck.
294///
295/// The representation depends on the challenge field alone.
296/// A 128-bit challenge uses the carryless-multiply representation.
297/// A 64-bit challenge uses its polynomial-basis carryless-multiply representation.
298///
299/// A folded codeword over this field is an encoding under its own level's encoder.
300pub trait ChallengeField<F: Field>: EncodableLevel {
301    /// Isomorphic field used for residual sumcheck arithmetic.
302    type SumcheckRepr: IntoTranscriptField<Self> + Algebra<F>;
303
304    /// Writes every value of `values`, held in the sumcheck representation, into `out`.
305    ///
306    /// The default maps one value at a time.
307    ///
308    /// # Panics
309    ///
310    /// The two slices differ in length.
311    fn from_sumcheck_repr(values: &[Self::SumcheckRepr], out: &mut [Self]) {
312        assert_eq!(values.len(), out.len(), "one output per value");
313        out.par_iter_mut()
314            .zip(values.par_iter())
315            .for_each(|(slot, &value)| *slot = value.into_transcript());
316    }
317}
318
319impl<F: Field> ChallengeField<F> for BinaryField128
320where
321    Ghash128: Algebra<F>,
322{
323    type SumcheckRepr = Ghash128;
324
325    /// Crosses a block of polynomial coordinates back into the tower basis in one pass.
326    fn from_sumcheck_repr(values: &[Ghash128], out: &mut [Self]) {
327        assert_eq!(values.len(), out.len(), "one output per value");
328        out.par_chunks_mut(REPR_CONVERSION_BLOCK)
329            .zip(values.par_chunks(REPR_CONVERSION_BLOCK))
330            .for_each_init(Vec::new, |words, (out, values)| {
331                words.clear();
332                words.extend(values.iter().map(|value| value.to_repr()));
333                poly_basis::to_tower_slice(words);
334                for (slot, &word) in out.iter_mut().zip(words.iter()) {
335                    *slot = Self::from_repr(word);
336                }
337            });
338    }
339}
340
341impl<F: Field> ChallengeField<F> for BinaryField64
342where
343    Poly64: Algebra<F>,
344{
345    type SumcheckRepr = Poly64;
346}
347
348/// The domain point each lane adds on top of the one evaluated at its group's first index.
349///
350/// # Algorithm
351///
352/// Write `v_r` for the `r`-th Cantor basis vector, so that
353///
354/// ```text
355///     domain_point(n) = sum_r bit_r(n) * v_r
356/// ```
357///
358/// That map is `F_2`-linear in the *bits* of its index, hence additive over exclusive-or:
359///
360/// ```text
361///     domain_point(m XOR n) = domain_point(m) + domain_point(n)
362/// ```
363///
364/// It is not additive over integer addition, because a carry moves a bit to a place the sum of
365/// the two basis vectors never reaches.
366///
367/// Carries are absent exactly when the two bit patterns are disjoint, and then addition and
368/// exclusive-or agree:
369///
370/// ```text
371///     m AND n == 0   =>   m + n == m XOR n
372/// ```
373///
374/// A packed group's first output index `g` is a multiple of the width, which is a power of two,
375/// so every bit of `g` below `log2(width)` is clear.
376///
377/// A lane index `k` is below the width, so every bit it sets is below `log2(width)`.
378///
379/// The two patterns are therefore disjoint, and doubling shifts both up one place without
380/// disturbing that:
381///
382/// ```text
383///     g AND k == 0   =>   2 * (g + k) == (2 * g) XOR (2 * k)
384/// ```
385///
386/// Combining the two displays gives the identity the fold runs on:
387///
388/// ```text
389///     domain_point(2 * (g + k)) = domain_point(2 * g) + domain_point(2 * k)
390/// ```
391///
392/// The second term depends on the lane alone, so the whole fold needs one group base plus this
393/// vector, built once.
394///
395/// The packing is a type parameter rather than `Packed` itself.
396///
397/// That lets the lane arithmetic be exercised at widths this target compiles no register for.
398fn lane_offsets<P: PackedValue<Value = Ghash128>>() -> P {
399    // Lane `k` carries `domain_point(2 * k)`, the offset from its group's first domain point.
400    P::from_fn(|lane| domain_point(lane << 1))
401}
402
403/// The exclusive-or step from one packed group's first domain point to the next.
404///
405/// # Algorithm
406///
407/// Group `G` opens at output index `G * W`, so it opens at doubled index `G << L`:
408///
409/// ```text
410///     L = log2(2 * W)
411///     base(G) = domain_point(G << L) = sum_r bit_r(G) * v_{L + r}
412/// ```
413///
414/// That map is `F_2`-linear in the bits of `G`, hence additive over exclusive-or.
415///
416/// Stepping from `G - 1` to `G` flips exactly bits `0 ..= k`, for `k = G.trailing_zeros()`:
417///
418/// ```text
419///     (G - 1) XOR G = 2^(k + 1) - 1
420/// ```
421///
422/// Consecutive group bases therefore differ by the basis vectors those bits select:
423///
424/// ```text
425///     base(G) = base(G - 1) + sum_{r <= k} v_{L + r}
426/// ```
427///
428/// Entry `k` is that sum, so a group past the first costs one exclusive-or, not a `domain_point`.
429fn group_steps<const W: usize>(num_pairs: usize) -> Vec<Ghash128> {
430    // A group strides `W` output indices, hence `2 * W` domain indices.
431    let log_stride = (2 * W).trailing_zeros() as usize;
432
433    // Group indices run below `num_pairs / W`, so one entry per bit of that count is enough.
434    let levels = (num_pairs / W).next_power_of_two().trailing_zeros() as usize;
435
436    let mut step = Ghash128::ZERO;
437    (0..levels)
438        .map(|level| {
439            step += Ghash128::cantor_basis(log_stride + level);
440            step
441        })
442        .collect()
443}
444
445/// Fold the pairs of `src` into `dst`, both held in polynomial coordinates.
446///
447/// `src[2 * j]` and `src[2 * j + 1]` fold into `dst[j]`, the symbol at output index `start + j`.
448///
449/// `start` is a multiple of the packing width.
450///
451/// That alignment is what lets a group share one domain point across its lanes.
452///
453/// `group_steps` walks the group bases, so the call evaluates the domain exactly once.
454///
455/// # Panics
456///
457/// Panics unless `src` holds exactly two symbols per slot of `dst`.
458///
459/// Panics in debug builds unless `start` is a multiple of the packing width.
460fn fold_pairs(
461    src: &[u128],
462    dst: &mut [u128],
463    start: usize,
464    beta: Ghash128,
465    lane_offsets: Packed,
466    group_steps: &[Ghash128],
467) {
468    // Every output slot consumes one pair, so the two slices are locked together.
469    assert_eq!(
470        src.len(),
471        2 * dst.len(),
472        "a fold must read one pair per output symbol"
473    );
474
475    // Invariant: `start` is a multiple of the packing width.
476    //
477    // The lane-offset identity rests on `g AND k == 0` for a group start `g` and a lane `k`.
478    //
479    // A lane index only sets bits below `log2(WIDTH)`, so `g` must carry none of them.
480    //
481    // Group starts are `start + group * WIDTH`, so they inherit that from `start` alone.
482    debug_assert!(
483        start.is_multiple_of(WIDTH),
484        "a fold must start on a packed group boundary"
485    );
486
487    // The challenge is the same in every lane, so it broadcasts once for the whole call.
488    let beta_packed = Packed::broadcast(beta);
489
490    // Split both sides into whole packed groups plus a shorter remainder.
491    // The two remainders match because one output slot always consumes two input symbols.
492    let (slot_groups, tail) = dst.as_chunks_mut::<WIDTH>();
493    let (symbol_blocks, tail_pairs) = src.as_chunks::<{ 2 * WIDTH }>();
494
495    // Output index the remainder starts at, relative to `start`.
496    let tail_offset = slot_groups.len() * WIDTH;
497
498    // Where the first group sits among all the round's groups.
499    let first_group = start / WIDTH;
500
501    // The call's only domain evaluation, opening the walk over its groups.
502    let mut base: Ghash128 = domain_point(start << 1);
503
504    // Phase 1: whole packed groups.
505    //
506    //     block  : [ lo_0 hi_0 | lo_1 hi_1 | ... | lo_{W-1} hi_{W-1} ]   2 * WIDTH symbols
507    //     slots  : [ out_0     | out_1     | ... | out_{W-1}         ]       WIDTH symbols
508    for (group, (slots, block)) in slot_groups.iter_mut().zip(symbol_blocks).enumerate() {
509        // Advance the walk to this group's first domain point.
510        if group != 0 {
511            base += group_steps[(first_group + group).trailing_zeros() as usize];
512        }
513
514        // That base is lane 0's domain point, and the offsets carry the rest of the lanes.
515        let x = Packed::broadcast(base) + lane_offsets;
516
517        // Gather the lanes.
518        let lo = Packed::from_fn(|lane| Ghash128::from_repr(block[2 * lane]));
519        let hi = Packed::from_fn(|lane| Ghash128::from_repr(block[2 * lane + 1]));
520
521        // The novel-basis halves of each pair.
522        let f1 = lo + hi;
523        let f0 = lo + x * f1;
524
525        // The evaluation-basis combination at the challenge.
526        let folded = f0 + beta_packed * (f0 + f1);
527
528        for (slot, &value) in slots.iter_mut().zip(folded.as_slice()) {
529            *slot = value.to_repr();
530        }
531    }
532
533    // Phase 2: fewer output slots left than one packed group holds.
534    //
535    // A call's slot count is either a whole block's share of the round or the round's whole
536    // pair count.
537    //
538    // A block covers whole groups, and a power-of-two pair count is a multiple of the width
539    // unless it is below it, so this runs only for a round with fewer pairs than the width.
540    //
541    // Each leftover evaluates its own domain point, so it rests on no group alignment at all.
542    //
543    // Twice as many symbols are left as slots, so the leftovers pair up exactly and the
544    // remainder discarded here is empty.
545    let (tail_blocks, _) = tail_pairs.as_chunks::<2>();
546    for (offset, (slot, pair)) in tail.iter_mut().zip(tail_blocks).enumerate() {
547        let x: Ghash128 = domain_point((start + tail_offset + offset) << 1);
548        let lo = Ghash128::from_repr(pair[0]);
549        let hi = Ghash128::from_repr(pair[1]);
550        let f1 = lo + hi;
551        let f0 = lo + x * f1;
552        *slot = (f0 + beta * (f0 + f1)).to_repr();
553    }
554}
555
556/// Fold a whole codeword, halving its length.
557///
558/// Each block of symbols evaluates the domain once, then advances it by one exclusive-or per
559/// packed group.
560///
561/// Evaluating per symbol instead is the right shape for a single query.
562///
563/// Over a whole codeword it would recompute that value once per output symbol.
564///
565/// # Panics
566///
567/// Panics unless the codeword is empty or has a power-of-two length.
568/// That is the shape every round schedule produces.
569///
570/// The loops themselves cover any even length, so this is a deliberate narrowing of the
571/// contract rather than a limit of the method.
572#[must_use]
573pub fn fold_codeword<F, EF>(codeword: &[F], beta: EF) -> Vec<EF>
574where
575    F: FoldAlphabet<EF>,
576    EF: TowerLevel,
577{
578    assert!(
579        codeword.is_empty() || codeword.len().is_power_of_two(),
580        "codeword length must be a power of two"
581    );
582    F::fold_rounds(codeword, &[beta])
583}
584
585/// Fold one aligned coset, including its global offset in every virtual layer.
586///
587/// `scratch` is working space the caller reuses across cosets, and its contents are replaced.
588///
589/// # Panics
590///
591/// Panics unless the coset holds one symbol per leaf of the challenge tree.
592pub(crate) fn fold_coset<F, EF>(
593    coset_index: usize,
594    values: &[F],
595    challenges: &[EF],
596    scratch: &mut Vec<EF>,
597) -> EF
598where
599    F: TowerLevel,
600    EF: ExtensionField<F> + TowerLevel,
601{
602    assert_eq!(values.len(), 1usize << challenges.len());
603
604    // The first round reads the coset's own alphabet, so it is the one that widens.
605    let mut len = values.len() / 2;
606    scratch.clear();
607    scratch.extend((0..len).map(|i| {
608        fold_pair(
609            coset_index * len + i,
610            challenges[0],
611            values[2 * i],
612            values[2 * i + 1],
613        )
614    }));
615
616    // Later rounds fold in place, halving the live prefix each time.
617    for &beta in &challenges[1..] {
618        len /= 2;
619        for i in 0..len {
620            scratch[i] = fold_pair::<EF, EF>(
621                coset_index * len + i,
622                beta,
623                scratch[2 * i],
624                scratch[2 * i + 1],
625            );
626        }
627    }
628    scratch[0]
629}
630
631/// Fold every challenge of one batch, materializing only the last round's codeword.
632///
633/// `challenges` stays in sumcheck order.
634pub(crate) fn fold_codeword_batch<F, EF>(codeword: &[F], challenges: &[EF]) -> Vec<EF>
635where
636    F: FoldAlphabet<EF>,
637    EF: TowerLevel,
638{
639    assert!(!challenges.is_empty());
640    assert!(codeword.len().is_power_of_two());
641    assert!(challenges.len() <= codeword.len().ilog2() as usize);
642    F::fold_rounds(codeword, challenges)
643}
644
645/// [`fold_rounds`] over this target's packing.
646fn fold_rounds_packed<F>(codeword: &[F], challenges: &[BinaryField128]) -> Vec<BinaryField128>
647where
648    F: TowerLevel + Sync,
649    F::Repr: Into<u128>,
650{
651    let offsets = lane_offsets::<Packed>();
652    fold_rounds::<WIDTH, F, _>(codeword, challenges, &|src, dst, start, beta, steps| {
653        fold_pairs(src, dst, start, beta, offsets, steps);
654    })
655}
656
657/// Fold one round per challenge, materializing only the last round's codeword.
658///
659/// `kernel` has the contract of [`fold_pairs`] at packing width `W`.
660///
661/// # Algorithm
662///
663/// A batch of `a` rounds maps each coset of `2^a` adjacent symbols to one output symbol.
664///
665/// Round `r` pairs symbols exactly as a single fold of the round-`r` codeword does, so the `n`
666/// adjacent cosets starting at coset `first` stay a run of adjacent symbols in every round:
667///
668/// ```text
669///     before round 0    2^a * n symbols          starting at index first * 2^a
670///     after round r     2^(a-1-r) * n symbols    starting at index first * 2^(a-1-r)
671/// ```
672///
673/// A block of cosets therefore runs each round as one kernel call with no seam at a coset
674/// boundary, and the last rounds keep whole packed groups.
675///
676/// A block holds `BLOCK_SYMBOLS / 2^a` cosets, but never fewer than a packed group's worth.
677///
678/// So every call starts on a group boundary, and even a block's last round fills whole groups
679/// unless the whole round is shorter than one.
680///
681/// Each worker's scratch holds one block in, plus half a block for the rounds before the last:
682///
683/// ```text
684///     symbols in    max(BLOCK_SYMBOLS, W << a)
685///     W = 8, a <= 8      2048 symbols     32 KiB   + 16 KiB
686///     W = 8, a = 12     32768 symbols    512 KiB   + 256 KiB
687/// ```
688///
689/// Each parallel task owns a contiguous run of output symbols and the cosets feeding them, so no
690/// two tasks touch the same symbol on either side.
691///
692/// A task copies each block into scratch and changes its basis in one pass, alternates the
693/// rounds between two scratch buffers, and writes the last round straight into its output, whose
694/// basis it changes back in one pass once every block has run.
695///
696/// # Panics
697///
698/// Panics if there are no challenges.
699fn fold_rounds<const W: usize, F, K>(
700    codeword: &[F],
701    challenges: &[BinaryField128],
702    kernel: &K,
703) -> Vec<BinaryField128>
704where
705    F: TowerLevel + Sync,
706    F::Repr: Into<u128>,
707    K: Fn(&[u128], &mut [u128], usize, Ghash128, &[Ghash128]) + Sync,
708{
709    // With no round to run, every output slot would keep its zero and the fold would return an
710    // all-zero codeword instead of failing.
711    assert!(!challenges.is_empty(), "a fold runs at least one round");
712
713    let arity = challenges.len();
714    let size = 1usize << arity;
715    let num_cosets = codeword.len() >> arity;
716
717    // A trailing partial coset produces no output symbol, so it is never read.
718    let codeword = &codeword[..num_cosets << arity];
719
720    // The challenges cross into the polynomial basis once for the whole fold.
721    let betas: Vec<Ghash128> = challenges.iter().copied().map(Ghash128::from).collect();
722
723    // Round `r` produces `codeword.len() >> (r + 1)` symbols, which sizes its group walk.
724    let steps: Vec<Vec<Ghash128>> = (0..arity)
725        .map(|round| group_steps::<W>(codeword.len() >> (round + 1)))
726        .collect();
727
728    // Cosets per block, a power of two no larger than the grain, so every task starts a block.
729    let block = (BLOCK_SYMBOLS >> arity).max(W);
730    debug_assert!(
731        FOLD_GRAIN.is_multiple_of(block),
732        "a task must hold whole blocks"
733    );
734
735    let mut folded = vec![0u128; num_cosets];
736
737    // Task `t` owns outputs `[t * GRAIN, (t + 1) * GRAIN)` and inputs `[t * GRAIN * 2^a, ...)`.
738    // Output `j` reads only its own coset, inside its own task's input chunk, so the two chunk
739    // streams stay index-aligned and every task is disjoint from every other.
740    folded
741        .par_chunks_mut(FOLD_GRAIN)
742        .zip(codeword.par_chunks(FOLD_GRAIN.saturating_mul(size)))
743        .enumerate()
744        .for_each_init(
745            || (Vec::new(), Vec::new()),
746            |(even, odd), (task, (out, input))| {
747                for (index, (slots, symbols)) in out
748                    .chunks_mut(block)
749                    .zip(input.chunks(block * size))
750                    .enumerate()
751                {
752                    let first = task * FOLD_GRAIN + index * block;
753
754                    // A narrower level occupies the leading bits of the wider one.
755                    // So a symbol widens by zero-extending its representation.
756                    even.clear();
757                    even.extend(symbols.iter().map(|&symbol| symbol.to_repr().into()));
758                    poly_basis::from_tower_slice(even);
759
760                    // Only a round before the last writes here.
761                    odd.resize(if arity > 1 { symbols.len() / 2 } else { 0 }, 0);
762
763                    // Round `r` reads the buffer round `r - 1` wrote and writes the other one.
764                    for (round, (&beta, steps)) in betas.iter().zip(&steps).enumerate() {
765                        let len = symbols.len() >> round;
766                        let (src, spare) = if round % 2 == 0 {
767                            (&even[..len], &mut odd[..])
768                        } else {
769                            (&odd[..len], &mut even[..])
770                        };
771                        let dst = if round + 1 == arity {
772                            &mut *slots
773                        } else {
774                            &mut spare[..len / 2]
775                        };
776                        kernel(src, dst, first * (size >> (round + 1)), beta, steps);
777                    }
778                }
779                poly_basis::to_tower_slice(out);
780            },
781        );
782
783    folded.into_iter().map(BinaryField128::from_repr).collect()
784}
785
786#[cfg(test)]
787mod tests {
788    use alloc::vec::Vec;
789    use core::array;
790
791    use p3_binary_dft::{AdditiveNtt, NaiveAdditiveNtt, domain_point};
792    use p3_binary_field::{
793        BinaryField8, BinaryField16, BinaryField32, BinaryField64, BinaryField128, Ghash128,
794        TowerLevel,
795    };
796    use p3_field::PrimeCharacteristicRing;
797    use p3_matrix::dense::RowMajorMatrix;
798    use p3_multilinear_util::poly::Poly;
799    use proptest::prelude::*;
800    use rand::distr::{Distribution, StandardUniform};
801    use rand::rngs::SmallRng;
802    use rand::{RngExt, SeedableRng};
803
804    use super::{
805        FOLD_GRAIN, FoldAlphabet, WIDTH, fold_codeword, fold_pair, fold_rounds, fold_rounds_scalar,
806        lane_offsets,
807    };
808
809    /// Batch arities that reach every clamp of the per-block coset count.
810    ///
811    /// A block holds `2 * FOLD_GRAIN / 2^arity` cosets but never fewer than the packing width.
812    ///
813    ///     1 .. 5      many cosets per block, blocks of whole packed groups
814    ///     9 .. 12     the width floor takes over at widths 8, 4, 2 and 1 in turn
815    const ARITIES: [usize; 9] = [1, 2, 3, 4, 5, 9, 10, 11, 12];
816
817    #[test]
818    fn batched_folds_match_sequential_folds_at_every_coset_offset() {
819        let mut rng = SmallRng::seed_from_u64(0xBA7C);
820        for log_len in [4, 7, 12, 14] {
821            let word: Vec<BinaryField128> = (0..1 << log_len).map(|_| rng.random()).collect();
822            for arity in ARITIES.into_iter().filter(|&arity| arity <= log_len) {
823                let challenges: Vec<BinaryField128> = (0..arity).map(|_| rng.random()).collect();
824                let mut expected = word.clone();
825                for &beta in &challenges {
826                    expected = fold_codeword(&expected, beta);
827                }
828                assert_eq!(super::fold_codeword_batch(&word, &challenges), expected);
829                for (index, coset) in word.chunks(1 << arity).enumerate() {
830                    assert_eq!(
831                        super::fold_coset(index, coset, &challenges, &mut Vec::new()),
832                        expected[index]
833                    );
834                }
835            }
836        }
837    }
838
839    /// Encode novel-basis coefficients over the additive domain, through the oracle.
840    ///
841    /// `NaiveAdditiveNtt` evaluates the definition directly and depends on no identity the
842    /// fold also relies on, so agreement between the two is real evidence rather than a
843    /// restatement.
844    fn encode(coeffs: &[BinaryField128]) -> Vec<BinaryField128> {
845        NaiveAdditiveNtt::<BinaryField128>::default()
846            .ntt_batch(RowMajorMatrix::new(coeffs.to_vec(), 1))
847            .values
848    }
849
850    /// The XOR step from `domain_point(2 * j)` to `domain_point(2 * (j + 1))`, indexed by
851    /// `(j + 1).trailing_zeros()`.
852    ///
853    /// The domain map is `F_2`-linear, and `j XOR (j + 1)` sets exactly the trailing ones of
854    /// `j` together with the first zero bit above them.
855    ///
856    /// Consecutive doubled domain points therefore differ by the sum of Cantor basis vectors
857    /// `1` through `level + 1`.
858    fn gray_chain_steps(num_pairs: usize) -> Vec<BinaryField128> {
859        let levels = num_pairs.next_power_of_two().trailing_zeros() as usize;
860        let mut steps = Vec::with_capacity(levels);
861        let mut acc = BinaryField128::ZERO;
862        for level in 0..levels {
863            acc += BinaryField128::cantor_basis(level + 1);
864            steps.push(acc);
865        }
866        steps
867    }
868
869    /// The tower-basis fold, kept here as the bit-for-bit reference the packed form must match.
870    ///
871    /// It walks the domain by a chained `F_2`-linear step and multiplies in the tower basis, so
872    /// it shares neither the loop shape nor the arithmetic representation of the code under
873    /// test.
874    fn fold_codeword_tower_reference(
875        codeword: &[BinaryField128],
876        beta: BinaryField128,
877    ) -> Vec<BinaryField128> {
878        let num_pairs = codeword.len() / 2;
879        let steps = gray_chain_steps(num_pairs);
880
881        codeword
882            .chunks(2 * FOLD_GRAIN)
883            .enumerate()
884            .flat_map(|(block, chunk)| {
885                let start = block * FOLD_GRAIN;
886                let mut x: BinaryField128 = domain_point(start << 1);
887                let steps = &steps;
888                chunk
889                    .chunks(2)
890                    .enumerate()
891                    .map(move |(offset, pair)| {
892                        if offset != 0 {
893                            x += steps[(start + offset).trailing_zeros() as usize];
894                        }
895                        let (lo, hi) = (pair[0], pair[1]);
896                        let f1 = lo + hi;
897                        let f0 = lo + x * f1;
898                        f0 + beta * (f0 + f1)
899                    })
900                    .collect::<Vec<_>>()
901            })
902            .collect()
903    }
904
905    /// The additive domain of a level is the leading part of every wider level's own.
906    ///
907    /// Both sides of a lifting fold evaluate the domain, at a different width each.
908    /// Every identity in this module rests on the two agreeing.
909    #[test]
910    fn a_narrow_domain_point_embeds_into_the_wide_one() {
911        // Fixture state: every index a 64-bit level spans a domain for, plus a wider sweep.
912        for i in 0..4096usize {
913            let narrow: BinaryField64 = domain_point(i);
914            let wide: BinaryField128 = domain_point(i);
915            assert_eq!(BinaryField128::from(narrow), wide, "i={i}");
916        }
917
918        // The smallest level, to its own limit: 8 basis vectors span 256 points.
919        for i in 0..256usize {
920            let narrow: BinaryField8 = domain_point(i);
921            assert_eq!(
922                BinaryField128::from(narrow),
923                domain_point::<BinaryField128>(i),
924                "i={i}"
925            );
926        }
927    }
928
929    /// A pair at output index zero folds to a value with no domain term in it.
930    ///
931    /// Invariant: `x = domain_point(0) = 0`, so the halves collapse.
932    ///
933    /// ```text
934    ///     f_1  = lo + hi
935    ///     f_0  = lo + 0 * f_1 = lo
936    ///     fold = f_0 + beta * (f_0 + f_1) = lo + beta * hi
937    /// ```
938    ///
939    /// The value is hand-computed, and it discriminates.
940    /// The novel-basis convention would give `lo + beta * (lo + hi)` at the same index.
941    ///
942    /// The two differ by `beta * lo`.
943    #[test]
944    fn the_first_pair_folds_to_a_hand_computed_value() {
945        let lo = BinaryField64::from_repr(0x0123_4567_89AB_CDEF);
946        let hi = BinaryField64::from_repr(0xFEDC_BA98_7654_3210);
947        let beta = BinaryField128::from_repr(0x3141_5926_5358_9793_2384_6264_3383_2795);
948
949        // The domain point at index zero is the empty subset of the basis.
950        assert_eq!(domain_point::<BinaryField64>(0), BinaryField64::ZERO);
951
952        let expected = BinaryField128::from(lo) + beta * hi;
953        assert_eq!(fold_pair(0, beta, lo, hi), expected);
954
955        // The wrong convention lands elsewhere, so the anchor is not shape-blind.
956        let novel_basis = BinaryField128::from(lo) + beta * (lo + hi);
957        assert_ne!(novel_basis, expected);
958    }
959
960    /// The tower-basis route and the packed polynomial-basis route agree bit for bit.
961    ///
962    /// The packed route is what a 128-bit codeword takes.
963    /// The tower route is what every other width and every lifting fold takes.
964    ///
965    /// A codeword committed through one and queried through the other would split the two.
966    /// The routes are therefore pinned equal over every batch shape.
967    #[test]
968    fn the_tower_route_and_the_packed_route_agree() {
969        let mut rng = SmallRng::seed_from_u64(0x70A7_E12E);
970
971        // Shapes chosen so both routes cross their own boundaries:
972        //
973        //     - 2^1 and 2^3     fewer pairs than one packed group holds
974        //     - 2^11            several blocks inside one task
975        //     - 2^12 and 2^13   more than one parallel task
976        for log_len in [1usize, 3, 11, 12, 13] {
977            let codeword: Vec<BinaryField128> = (0..1 << log_len).map(|_| rng.random()).collect();
978            for arity in ARITIES.into_iter().filter(|&arity| arity <= log_len) {
979                let challenges: Vec<BinaryField128> = (0..arity).map(|_| rng.random()).collect();
980
981                assert_eq!(
982                    fold_rounds_scalar(&codeword, &challenges),
983                    super::fold_rounds_packed(&codeword, &challenges),
984                    "log_len={log_len} arity={arity}"
985                );
986            }
987        }
988    }
989
990    /// Fold a narrow codeword and its own embedding, and require the two to agree.
991    ///
992    /// The reference embeds first and then runs the independent tower reference.
993    /// It shares neither the loop shape nor the arithmetic width of the route under test.
994    ///
995    /// Lengths are capped at the alphabet's own domain, which is what a schedule allows.
996    fn check_folds_like_its_embedding<F>(seed: u64)
997    where
998        F: TowerLevel + Sync + FoldAlphabet<BinaryField128>,
999        BinaryField128: p3_field::ExtensionField<F>,
1000        StandardUniform: Distribution<F>,
1001    {
1002        let mut rng = SmallRng::seed_from_u64(seed);
1003
1004        for log_len in [1usize, 4, 8, 11, 12]
1005            .into_iter()
1006            .filter(|&log_len| log_len <= F::bits())
1007        {
1008            let narrow: Vec<F> = (0..1 << log_len).map(|_| rng.random::<F>()).collect();
1009            let embedded: Vec<BinaryField128> =
1010                narrow.iter().copied().map(BinaryField128::from).collect();
1011
1012            for arity in ARITIES.into_iter().filter(|&arity| arity <= log_len) {
1013                let challenges: Vec<BinaryField128> =
1014                    (0..arity).map(|_| rng.random::<BinaryField128>()).collect();
1015
1016                let expected = challenges.iter().fold(embedded.clone(), |word, &beta| {
1017                    fold_codeword_tower_reference(&word, beta)
1018                });
1019                assert_eq!(
1020                    super::fold_codeword_batch(&narrow, &challenges),
1021                    expected,
1022                    "bits={} log_len={log_len} arity={arity}",
1023                    F::bits()
1024                );
1025            }
1026        }
1027    }
1028
1029    /// Every alphabet the wide challenge field admits takes the packed route.
1030    ///
1031    /// Widening a symbol is a zero-extension of its representation.
1032    /// The packed driver does that on the way into its block, not over a lifted copy.
1033    ///
1034    /// A level whose leading bits were not the narrower one's would fold elsewhere.
1035    /// That is the property each case below pins.
1036    #[test]
1037    fn every_narrow_alphabet_folds_like_its_embedding() {
1038        check_folds_like_its_embedding::<BinaryField8>(0x11F7_1408);
1039        check_folds_like_its_embedding::<BinaryField16>(0x11F7_1416);
1040        check_folds_like_its_embedding::<BinaryField32>(0x11F7_1432);
1041        check_folds_like_its_embedding::<BinaryField64>(0x11F7_1467);
1042    }
1043
1044    /// One coset's fold agrees with the whole codeword's, at every coset offset.
1045    ///
1046    /// The verifier folds one coset per query.
1047    /// A global-offset mistake would otherwise show only on the positions a proof samples.
1048    #[test]
1049    fn a_narrow_coset_folds_like_its_slice_of_the_whole_codeword() {
1050        let mut rng = SmallRng::seed_from_u64(0xC05E_7000);
1051        let narrow: Vec<BinaryField64> = (0..1 << 10).map(|_| rng.random()).collect();
1052
1053        for arity in [1usize, 2, 3, 5] {
1054            let challenges: Vec<BinaryField128> = (0..arity).map(|_| rng.random()).collect();
1055            let whole = super::fold_codeword_batch(&narrow, &challenges);
1056
1057            let mut scratch = Vec::new();
1058            for (index, coset) in narrow.chunks(1 << arity).enumerate() {
1059                assert_eq!(
1060                    super::fold_coset(index, coset, &challenges, &mut scratch),
1061                    whole[index],
1062                    "arity={arity} index={index}"
1063                );
1064            }
1065        }
1066    }
1067
1068    proptest! {
1069        // Each case folds up to 2^12 symbols through the tower reference, the slow side.
1070        // A few dozen cases therefore keep the unoptimized run short.
1071        #![proptest_config(ProptestConfig::with_cases(32))]
1072
1073        /// Every batch shape, over a narrow alphabet, against the embedded wide reference.
1074        #[test]
1075        fn every_narrow_shape_matches_the_embedded_wide_fold(
1076            (log_len, arity) in (1usize..=12)
1077                .prop_flat_map(|log_len| (Just(log_len), 1..=log_len)),
1078            seed: u64,
1079        ) {
1080            let mut rng = SmallRng::seed_from_u64(seed);
1081            let narrow: Vec<BinaryField64> = (0..1 << log_len).map(|_| rng.random()).collect();
1082            let challenges: Vec<BinaryField128> = (0..arity).map(|_| rng.random()).collect();
1083
1084            let embedded: Vec<BinaryField128> =
1085                narrow.iter().copied().map(BinaryField128::from).collect();
1086            let expected = challenges.iter().fold(embedded, |word, &beta| {
1087                fold_codeword_tower_reference(&word, beta)
1088            });
1089
1090            prop_assert_eq!(super::fold_codeword_batch(&narrow, &challenges), expected);
1091        }
1092    }
1093
1094    /// A narrow challenge field reaches the tower route on its own codewords.
1095    ///
1096    /// The wide field's packed route is not on this path at all.
1097    /// The reference is a direct per-pair computation, not either production route.
1098    #[test]
1099    fn a_narrow_challenge_field_folds_pair_by_pair() {
1100        let mut rng = SmallRng::seed_from_u64(0x6A11_0064);
1101        let codeword: Vec<BinaryField64> = (0..1 << 11).map(|_| rng.random()).collect();
1102        let beta: BinaryField64 = rng.random();
1103
1104        let folded = fold_codeword(&codeword, beta);
1105        for (j, &value) in folded.iter().enumerate() {
1106            assert_eq!(
1107                value,
1108                fold_pair(j, beta, codeword[2 * j], codeword[2 * j + 1]),
1109                "j={j}"
1110            );
1111        }
1112    }
1113
1114    #[test]
1115    fn the_folded_domain_is_domain_point_at_halved_index() {
1116        // W_1(x) = x^2 + x, and W_1(domain_point(i)) == domain_point(i >> 1).
1117        for i in 0..512usize {
1118            let x: BinaryField128 = domain_point(i);
1119            assert_eq!(
1120                x.square() + x,
1121                domain_point::<BinaryField128>(i >> 1),
1122                "i={i}"
1123            );
1124        }
1125    }
1126
1127    #[test]
1128    fn folding_partners_are_adjacent_and_differ_by_one() {
1129        for j in 0..256usize {
1130            let lo: BinaryField128 = domain_point(2 * j);
1131            let hi: BinaryField128 = domain_point(2 * j + 1);
1132            assert_eq!(lo + BinaryField128::ONE, hi, "j={j}");
1133        }
1134    }
1135
1136    #[test]
1137    fn a_lane_offset_shifts_a_group_start_to_its_own_domain_point() {
1138        // Invariant: a group's first index is a multiple of the width, so its low bits are
1139        // clear and adding a lane index is a disjoint exclusive-or.
1140        //
1141        //     group start 8, width 4:   1000
1142        //     lane 3:                   0011
1143        //     8 + 3 == 8 | 3 == 11:     1011   no carry crosses between the two
1144        //
1145        // The domain map is additive over exclusive-or, so the group's point plus the lane's
1146        // point is the point of the sum.
1147        for group in 0..64usize {
1148            let first = group * WIDTH;
1149            for lane in 0..WIDTH {
1150                // Disjointness is what the identity rests on, so pin it directly.
1151                assert_eq!(first & lane, 0, "group={group} lane={lane}");
1152                assert_eq!(
1153                    domain_point::<BinaryField128>((first + lane) << 1),
1154                    domain_point::<BinaryField128>(first << 1)
1155                        + domain_point::<BinaryField128>(lane << 1),
1156                    "group={group} lane={lane}"
1157                );
1158            }
1159        }
1160    }
1161
1162    /// The pair kernel with `[Ghash128; W]` standing in for the packing.
1163    ///
1164    /// Every lane index, gather offset and step lookup is the one `fold_pairs` uses.
1165    ///
1166    /// A build compiles exactly one packing width, so this is what reaches the others.
1167    fn fold_pairs_model<const W: usize>(
1168        src: &[u128],
1169        dst: &mut [u128],
1170        start: usize,
1171        beta: Ghash128,
1172        lane_offsets: [Ghash128; W],
1173        group_steps: &[Ghash128],
1174    ) {
1175        debug_assert!(
1176            start.is_multiple_of(W),
1177            "a fold must start on a packed group boundary"
1178        );
1179
1180        // Whole groups, then the leftover slots, split where the packed form splits them.
1181        let (slot_groups, tail) = dst.as_chunks_mut::<W>();
1182        let tail_offset = slot_groups.len() * W;
1183
1184        // A block of `2 * W` symbols per group, so the remainder starts at twice that offset.
1185        let (symbol_blocks, tail_pairs) = src.split_at(2 * tail_offset);
1186
1187        let first_group = start / W;
1188        let mut base: Ghash128 = domain_point(start << 1);
1189
1190        for (group, slots) in slot_groups.iter_mut().enumerate() {
1191            if group != 0 {
1192                base += group_steps[(first_group + group).trailing_zeros() as usize];
1193            }
1194
1195            // The `2 * W` symbols this group consumes.
1196            let block = &symbol_blocks[2 * W * group..][..2 * W];
1197
1198            // Broadcasting the base and adding the offset vector is this, lane by lane.
1199            let x: [Ghash128; W] = array::from_fn(|lane| base + lane_offsets[lane]);
1200
1201            // The gather a packed block performs: even symbols low, odd symbols high.
1202            let lo: [Ghash128; W] = array::from_fn(|lane| Ghash128::from_repr(block[2 * lane]));
1203            let hi: [Ghash128; W] = array::from_fn(|lane| Ghash128::from_repr(block[2 * lane + 1]));
1204
1205            for (lane, slot) in slots.iter_mut().enumerate() {
1206                let f1 = lo[lane] + hi[lane];
1207                let f0 = lo[lane] + x[lane] * f1;
1208                *slot = (f0 + beta * (f0 + f1)).to_repr();
1209            }
1210        }
1211
1212        let (tail_blocks, _) = tail_pairs.as_chunks::<2>();
1213        for (offset, (slot, pair)) in tail.iter_mut().zip(tail_blocks).enumerate() {
1214            let x: Ghash128 = domain_point((start + tail_offset + offset) << 1);
1215            let lo = Ghash128::from_repr(pair[0]);
1216            let hi = Ghash128::from_repr(pair[1]);
1217            let f1 = lo + hi;
1218            let f0 = lo + x * f1;
1219            *slot = (f0 + beta * (f0 + f1)).to_repr();
1220        }
1221    }
1222
1223    /// The production round driver, running the width-generic kernel.
1224    fn fold_rounds_model<const W: usize>(
1225        codeword: &[BinaryField128],
1226        challenges: &[BinaryField128],
1227    ) -> Vec<BinaryField128> {
1228        let offsets = lane_offsets::<[Ghash128; W]>();
1229        fold_rounds::<W, BinaryField128, _>(
1230            codeword,
1231            challenges,
1232            &|src, dst, start, beta, steps| {
1233                fold_pairs_model::<W>(src, dst, start, beta, offsets, steps);
1234            },
1235        )
1236    }
1237
1238    /// Fold every shape at one packing width and compare against the tower reference.
1239    fn check_width<const W: usize>() {
1240        let mut rng = SmallRng::seed_from_u64(0x1A5E_0FF5_E750_0000);
1241
1242        // Shapes chosen so both phases run at all four widths:
1243        //
1244        //     0             no pairs at all
1245        //     2, 4          fewer pairs than a group holds at the wider widths
1246        //     8, 16, 1024   whole groups at every width, inside one task
1247        //     2048          pairs exactly one grain
1248        //     4096, 8192    several tasks, so more than one group base is evaluated
1249        for len in [0usize, 2, 4, 8, 16, 1024, 2048, 4096, 8192] {
1250            let codeword: Vec<BinaryField128> = (0..len).map(|_| rng.random()).collect();
1251            let beta: BinaryField128 = rng.random();
1252
1253            assert_eq!(
1254                fold_rounds_model::<W>(&codeword, &[beta]),
1255                fold_codeword_tower_reference(&codeword, beta),
1256                "W={W} len={len}"
1257            );
1258        }
1259
1260        // Batches, where every round after the first runs on symbols the previous round wrote:
1261        //
1262        //     2^1, 2^3, 2^6   fewer cosets than a packed group holds at the wider widths
1263        //     2^12            two tasks at arity 1, several blocks at arity 5, one coset at 12
1264        //     2^14, arity 2   several tasks of several blocks each
1265        let shapes = [1, 3, 6, 12]
1266            .into_iter()
1267            .flat_map(|log_len| {
1268                ARITIES
1269                    .into_iter()
1270                    .filter(move |&arity| arity <= log_len)
1271                    .map(move |arity| (log_len, arity))
1272            })
1273            .chain([(14, 2)]);
1274        for (log_len, arity) in shapes {
1275            let codeword: Vec<BinaryField128> = (0..1 << log_len).map(|_| rng.random()).collect();
1276            let challenges: Vec<BinaryField128> = (0..arity).map(|_| rng.random()).collect();
1277
1278            let expected = challenges.iter().fold(codeword.clone(), |word, &beta| {
1279                fold_codeword_tower_reference(&word, beta)
1280            });
1281            assert_eq!(
1282                fold_rounds_model::<W>(&codeword, &challenges),
1283                expected,
1284                "W={W} log_len={log_len} arity={arity}"
1285            );
1286        }
1287    }
1288
1289    #[test]
1290    fn the_lane_arithmetic_holds_at_every_packing_width() {
1291        // A target compiles one packing, so `the_packed_fold_matches_the_tower_reference` below
1292        // only ever exercises that one width.
1293        //
1294        // Widths swept here, none of them requiring the register that would carry them:
1295        //
1296        //     1   no packing
1297        //     2   the 256-bit carryless multiply
1298        //     4   the 512-bit carryless multiply
1299        //     8   past every packing this workspace defines
1300        //
1301        // The verifier folds pair by pair in the tower basis, so a lane-gather or step-table
1302        // mistake at a width this host cannot compile would split the prover from the verifier
1303        // on hosts that can.
1304        check_width::<1>();
1305        check_width::<2>();
1306        check_width::<4>();
1307        check_width::<8>();
1308    }
1309
1310    proptest! {
1311        // Each case folds up to 2^14 symbols through the tower reference, the slow side here,
1312        // so a few dozen cases keep the unoptimized run short.
1313        #![proptest_config(ProptestConfig::with_cases(32))]
1314
1315        /// Every batch shape up to 2^14 symbols, at every packing width.
1316        ///
1317        /// The coset count crosses the grain at a different arity for each length, so this
1318        /// reaches shapes the enumerated sweep steps over, such as exactly one full task.
1319        #[test]
1320        fn every_batch_shape_matches_the_tower_reference(
1321            (log_len, arity) in (1usize..=14)
1322                .prop_flat_map(|log_len| (Just(log_len), 1..=log_len)),
1323            seed: u64,
1324        ) {
1325            let mut rng = SmallRng::seed_from_u64(seed);
1326            let codeword: Vec<BinaryField128> = (0..1 << log_len).map(|_| rng.random()).collect();
1327            let challenges: Vec<BinaryField128> = (0..arity).map(|_| rng.random()).collect();
1328
1329            let expected = challenges.iter().fold(codeword.clone(), |word, &beta| {
1330                fold_codeword_tower_reference(&word, beta)
1331            });
1332            prop_assert_eq!(&fold_rounds_model::<1>(&codeword, &challenges), &expected);
1333            prop_assert_eq!(&fold_rounds_model::<2>(&codeword, &challenges), &expected);
1334            prop_assert_eq!(&fold_rounds_model::<4>(&codeword, &challenges), &expected);
1335            prop_assert_eq!(&fold_rounds_model::<8>(&codeword, &challenges), &expected);
1336        }
1337    }
1338
1339    #[test]
1340    fn the_packed_fold_matches_the_tower_reference() {
1341        let mut rng = SmallRng::seed_from_u64(0xF01D_0BA5_15C0_DE00);
1342
1343        // Shapes worth separating, all of them power-of-two lengths the assertion admits:
1344        //
1345        //     0                  no pairs at all
1346        //     2                  one pair, fewer than one packed group
1347        //     4, 8               a partial group on a four-lane build, a whole one on two lanes
1348        //     16 .. 1024         several groups, still inside one parallel task
1349        //     2048               pairs exactly one grain, so exactly one task
1350        //     4096, 8192, 65536  several tasks, the last of them full
1351        for len in [0usize, 2, 4, 8, 16, 64, 1024, 2048, 4096, 8192, 65536] {
1352            let codeword: Vec<BinaryField128> = (0..len).map(|_| rng.random()).collect();
1353            let beta: BinaryField128 = rng.random();
1354
1355            // Bit-for-bit, not merely equal up to the folded polynomial: the committed bytes
1356            // of every round after this one depend on the exact output.
1357            assert_eq!(
1358                fold_codeword(&codeword, beta),
1359                fold_codeword_tower_reference(&codeword, beta),
1360                "len={len}"
1361            );
1362        }
1363    }
1364
1365    #[test]
1366    fn the_whole_fold_chain_matches_the_tower_reference() {
1367        let mut rng = SmallRng::seed_from_u64(0x0C0F_FEE0_DEAD_BEEF);
1368
1369        // 8192 symbols is 13 rounds down to a single symbol, and 4096 pairs is four grains, so
1370        // the chain starts multi-task and shrinks through the single-task and sub-group shapes.
1371        let start: Vec<BinaryField128> = (0..8192).map(|_| rng.random()).collect();
1372
1373        // One independent challenge per round, as the prover draws them.
1374        let betas: Vec<BinaryField128> = (0..13).map(|_| rng.random()).collect();
1375
1376        let mut packed = start.clone();
1377        let mut reference = start;
1378
1379        for (round, &beta) in betas.iter().enumerate() {
1380            packed = fold_codeword(&packed, beta);
1381            reference = fold_codeword_tower_reference(&reference, beta);
1382
1383            // Diverging at any round would silently change every later commitment, so compare
1384            // the whole codeword each time rather than only the final symbol.
1385            assert_eq!(packed, reference, "round={round}");
1386        }
1387
1388        // Thirteen halvings of 8192 leave one symbol, the value the proof carries in the clear.
1389        assert_eq!(packed.len(), 1);
1390        assert_eq!(reference.len(), 1);
1391    }
1392
1393    proptest! {
1394        // Each case runs the naive oracle twice over a codeword of up to 512 symbols, so the
1395        // per-case cost is far above a typical property test's; 64 cases still explore every
1396        // (rate, length) pair many times over.
1397        #![proptest_config(ProptestConfig::with_cases(64))]
1398
1399        /// The crate's central identity: folding the codeword equals binding the message's
1400        /// lowest variable in the evaluation basis (`Poly::fix_suffix_var`).
1401        ///
1402        /// The oracle is a direct evaluation of the additive-NTT definition, which shares no
1403        /// identity with the fold, so agreement is evidence rather than restatement.
1404        /// `log_n` and `log_inv_rate` are generated rather than swept because the identity is
1405        /// claimed for every message length and every blowup a prover can configure, not for a
1406        /// chosen list; every real prover fold runs on a blown-up codeword, so rate 0 is the
1407        /// degenerate end of the range rather than the case of interest.
1408        #[test]
1409        fn folding_the_codeword_equals_binding_the_lowest_variable(
1410            log_inv_rate in 0usize..=2,
1411            log_n in 1usize..=7,
1412            raw in prop::collection::vec(any::<u128>(), 128),
1413            beta_raw: u128,
1414        ) {
1415            let n = 1usize << log_n;
1416            let coeffs: Vec<BinaryField128> = raw[..n]
1417                .iter()
1418                .copied()
1419                .map(BinaryField128::from_repr)
1420                .collect();
1421            let beta = BinaryField128::from_repr(beta_raw);
1422
1423            let mut message = coeffs.clone();
1424            message.resize(n << log_inv_rate, BinaryField128::ZERO);
1425
1426            let bound = Poly::new(coeffs).fix_suffix_var(beta);
1427            let mut bound_message = bound.into_evals();
1428            bound_message.resize(bound_message.len() << log_inv_rate, BinaryField128::ZERO);
1429
1430            prop_assert_eq!(
1431                fold_codeword(&encode(&message), beta),
1432                encode(&bound_message)
1433            );
1434        }
1435    }
1436
1437    #[test]
1438    fn the_pair_form_agrees_with_the_vector_form() {
1439        // Invariant: the whole-codeword form agrees with the independent per-pair computation
1440        // at every position, including across a task boundary.
1441        //
1442        // Fixture state: 4096 symbols is 2048 pairs, past the 1024-symbol grain.
1443        //
1444        // So this spans two full parallel tasks and exercises the per-task index base that a
1445        // single-task codeword leaves silent.
1446        let mut rng = SmallRng::seed_from_u64(0x9E37_79B9_7F4A_7C15);
1447        let codeword: Vec<BinaryField128> = (0..4096).map(|_| rng.random()).collect();
1448        let beta: BinaryField128 = rng.random();
1449        let folded = fold_codeword(&codeword, beta);
1450        for (j, &value) in folded.iter().enumerate() {
1451            assert_eq!(
1452                value,
1453                fold_pair(j, beta, codeword[2 * j], codeword[2 * j + 1]),
1454                "j={j}"
1455            );
1456        }
1457    }
1458
1459    #[test]
1460    #[should_panic(expected = "codeword length must be a power of two")]
1461    fn an_odd_length_codeword_is_rejected() {
1462        let odd = [BinaryField128::ONE; 3];
1463        let _ = fold_codeword(&odd, BinaryField128::ONE);
1464    }
1465
1466    #[test]
1467    #[should_panic(expected = "codeword length must be a power of two")]
1468    fn an_even_non_power_of_two_codeword_is_rejected() {
1469        // Even is not enough: the contract is narrowed to the shape a round schedule produces.
1470        // Six symbols is three pairs, which no round schedule produces.
1471        let six = [BinaryField128::ONE; 6];
1472        let _ = fold_codeword(&six, BinaryField128::ONE);
1473    }
1474
1475    #[test]
1476    fn an_empty_codeword_folds_to_an_empty_one() {
1477        // Zero pairs to fold, so the empty input is not a panic.
1478        assert!(fold_codeword::<BinaryField128, _>(&[], BinaryField128::ONE).is_empty());
1479    }
1480
1481    #[test]
1482    fn a_single_symbol_codeword_folds_to_an_empty_one() {
1483        // One symbol is half a pair, so there is nothing to fold and the output is empty.
1484        // A length of one is a power of two, so the length assertion admits it.
1485        assert!(fold_codeword(&[BinaryField128::ONE], BinaryField128::ONE).is_empty());
1486    }
1487
1488    #[test]
1489    #[should_panic(expected = "a fold runs at least one round")]
1490    fn a_fold_with_no_challenges_is_rejected() {
1491        // No round would write an output slot, so the driver refuses rather than return zeros.
1492        let codeword = [BinaryField128::ONE; 4];
1493        let _ = fold_rounds_model::<1>(&codeword, &[]);
1494    }
1495
1496    #[test]
1497    fn the_novel_basis_fold_is_a_different_operation() {
1498        // `f_0 + beta * f_1`, the novel-basis combination, is a different operation from the
1499        // evaluation-basis one this module folds with: the two differ by exactly `beta * f_0`,
1500        // so they agree only when that term vanishes.
1501        let mut rng = SmallRng::seed_from_u64(0x51DE);
1502        let j = 5usize;
1503        let x: BinaryField128 = domain_point(2 * j);
1504        let lo: BinaryField128 = rng.random();
1505        let hi: BinaryField128 = rng.random();
1506        let beta: BinaryField128 = rng.random();
1507
1508        let f1 = lo + hi;
1509        let f0 = lo + x * f1;
1510        let novel_basis_fold = lo + (x + beta) * (lo + hi);
1511
1512        assert_eq!(
1513            fold_pair(j, beta, lo, hi) + novel_basis_fold,
1514            beta * f0,
1515            "the evaluation-basis and novel-basis folds differ by beta * f_0"
1516        );
1517    }
1518
1519    proptest! {
1520        #[test]
1521        fn folding_is_affine_in_the_challenge(
1522            lo_raw: u128, hi_raw: u128, b0_raw: u128, b1_raw: u128, j in 0usize..64,
1523        ) {
1524            let lo = BinaryField128::from_repr(lo_raw);
1525            let hi = BinaryField128::from_repr(hi_raw);
1526            let b0 = BinaryField128::from_repr(b0_raw);
1527            let b1 = BinaryField128::from_repr(b1_raw);
1528            let x: BinaryField128 = domain_point(2 * j);
1529            let f1 = lo + hi;
1530            let f0 = lo + x * f1;
1531
1532            // fold(beta) = f0 + beta * (f0 + f1), so fold(b0) + fold(b1) == (b0 + b1) * (f0 + f1).
1533            prop_assert_eq!(
1534                fold_pair(j, b0, lo, hi) + fold_pair(j, b1, lo, hi),
1535                (b0 + b1) * (f0 + f1)
1536            );
1537
1538            // At beta = 0 the fold is f0, pinning the internal domain index against one
1539            // computed independently here.
1540            prop_assert_eq!(fold_pair(j, BinaryField128::ZERO, lo, hi), f0);
1541        }
1542    }
1543}