Skip to main content

p3_sumcheck/layout/
witness.rs

1//! Physical placement of source tables inside the stacked committed polynomial.
2
3use alloc::vec::Vec;
4
5use itertools::Itertools;
6use p3_field::Field;
7use p3_multilinear_util::point::Point;
8use p3_multilinear_util::poly::Poly;
9use p3_util::reverse_bits_len;
10
11use crate::layout::plan::{LayoutShape, plan_layout};
12use crate::table::TableShape;
13
14/// Identifies one slot inside the stacked polynomial.
15#[derive(Debug, Clone, Copy)]
16pub struct Selector {
17    /// Bit-width of the slot address, carved from the stacked variables.
18    num_variables: usize,
19    /// Slot index, interpreted as an integer in `0..2^num_variables`.
20    index: usize,
21}
22
23impl Selector {
24    /// Builds a selector over `num_variables` bits pointing at slot `index`.
25    ///
26    /// # Panics
27    ///
28    /// - Slot index must fit in `num_variables` bits.
29    pub const fn new(num_variables: usize, index: usize) -> Self {
30        // Bounds check: slot index must address a valid hypercube point.
31        assert!(index < (1 << num_variables));
32        Self {
33            num_variables,
34            index,
35        }
36    }
37
38    /// Returns the hypercube point that addresses this slot.
39    #[inline(always)]
40    pub fn point<F: Field>(&self) -> Point<F> {
41        Point::hypercube(self.index, self.num_variables)
42    }
43
44    /// Returns the number of selector bits.
45    #[inline(always)]
46    pub const fn num_variables(&self) -> usize {
47        self.num_variables
48    }
49
50    /// Returns the slot index.
51    #[inline(always)]
52    pub const fn index(&self) -> usize {
53        self.index
54    }
55
56    /// Returns this selector with its bitstring reversed.
57    #[inline(always)]
58    pub const fn reverse(&mut self) {
59        self.index = reverse_bits_len(self.index, self.num_variables);
60    }
61
62    /// Prefixes `other` with the selector bits.
63    #[inline(always)]
64    pub fn lift_prefix<Ext: Field>(&self, other: &Point<Ext>) -> Point<Ext> {
65        // Expand the slot index as selector bits; single allocation.
66        let mut out: Point<Ext> = self.point();
67        // Append the local coordinates to finish the stacked-space point.
68        out.extend(other);
69        out
70    }
71
72    /// Appends the selector bits after the local coordinates.
73    #[inline(always)]
74    pub fn lift_suffix<Ext: Field>(&self, other: &Point<Ext>) -> Point<Ext> {
75        let mut out = other.clone();
76        out.extend(&self.point());
77        out
78    }
79}
80
81/// A column-major table of multilinear polynomials sharing a common arity.
82///
83/// # Invariants
84///
85/// - At least one column.
86/// - Every column has the same number of variables.
87#[derive(Debug, Clone)]
88pub struct Table<F: Field>(Vec<Poly<F>>);
89
90impl<F: Field> Table<F> {
91    /// Creates a table from one or more polynomials.
92    ///
93    /// # Panics
94    ///
95    /// - Column list must not be empty.
96    /// - Every column must share the same arity.
97    pub fn new(polys: Vec<Poly<F>>) -> Self {
98        // Structural precondition: at least one column.
99        assert!(!polys.is_empty());
100        // Consistency precondition: every column shares the same arity.
101        assert!(
102            polys.iter().map(Poly::num_variables).all_equal(),
103            "every column of a table must share the same arity",
104        );
105        Self(polys)
106    }
107
108    /// Returns the polynomial at column `id`.
109    pub fn poly(&self, id: usize) -> &Poly<F> {
110        &self.0[id]
111    }
112
113    /// Returns the number of columns.
114    pub const fn num_polys(&self) -> usize {
115        self.0.len()
116    }
117
118    /// Returns the shared number of variables.
119    pub fn num_variables(&self) -> usize {
120        // Invariant (set by the constructor): every column shares this value.
121        self.0[0].num_variables()
122    }
123
124    /// Returns the verifier shape of this table.
125    pub fn shape(&self) -> TableShape {
126        TableShape::new(self.num_variables(), self.num_polys())
127    }
128
129    /// Pads every column with zeros until the table has at least `num_variables`.
130    fn pad_zeros(&mut self, num_variables: usize) {
131        let current_num_variables = self.num_variables();
132        if current_num_variables < num_variables {
133            self.0
134                .iter_mut()
135                .for_each(|poly| poly.pad_zeros(num_variables));
136        }
137    }
138}
139
140/// Placement metadata for one table inside the stacked polynomial.
141#[derive(Debug, Clone)]
142pub struct TablePlacement {
143    /// Source table index this placement refers back to.
144    pub(super) idx: usize,
145    /// One selector per column, addressing the column's slot.
146    pub(super) selectors: Vec<Selector>,
147}
148
149impl TablePlacement {
150    /// Creates placement metadata for table index `idx` with the given selectors.
151    pub const fn new(idx: usize, selectors: Vec<Selector>) -> Self {
152        Self { idx, selectors }
153    }
154
155    /// Reverses every selector bitstring in this placement.
156    pub fn reverse_selectors(&mut self) {
157        self.selectors.iter_mut().for_each(Selector::reverse);
158    }
159
160    /// Returns the number of columns placed for this table.
161    pub const fn num_polys(&self) -> usize {
162        self.selectors.len()
163    }
164
165    /// Returns the source table index.
166    pub const fn idx(&self) -> usize {
167        self.idx
168    }
169
170    /// Returns the selector assigned to each column.
171    pub fn selectors(&self) -> &[Selector] {
172        &self.selectors
173    }
174}
175
176/// Owns the source tables together with the stacked committed polynomial.
177#[derive(Debug, Clone)]
178pub struct Witness<F: Field> {
179    /// Source tables behind the stacked polynomial.
180    pub(super) tables: Vec<Table<F>>,
181    /// Per-table placement metadata inside the stacked polynomial.
182    pub(super) placements: Vec<TablePlacement>,
183    /// Number of variables of the stacked polynomial.
184    pub(super) num_variables: usize,
185    /// Preprocessing depth (number of rounds the protocol folds upfront).
186    pub(super) folding: usize,
187    /// Stacked committed polynomial.
188    pub(super) poly: Poly<F>,
189}
190
191impl<F: Field> Witness<F> {
192    /// Stacks the given tables into a single committed polynomial.
193    ///
194    /// # Algorithm
195    ///
196    /// - Sort tables by arity ascending; reverse-iterate to place largest first.
197    /// - Each column occupies one slot of size `2^arity`.
198    /// - Selector bit-width equals the stacked arity minus the table arity.
199    /// - Total stacked size is rounded up to the next power of two.
200    /// - Unused tail entries stay zero.
201    ///
202    /// # Panics
203    ///
204    /// - Table list must be non-empty.
205    /// - Tables below the preprocessing depth are zero-padded to that depth.
206    #[tracing::instrument(skip_all)]
207    pub fn new(mut tables: Vec<Table<F>>, folding: usize) -> Self {
208        // Precondition: need at least one source table to stack.
209        assert!(
210            !tables.is_empty(),
211            "Witness requires at least one source table"
212        );
213        // Normalize small tables to the committed arity used by the protocol.
214        tables.iter_mut().for_each(|table| table.pad_zeros(folding));
215
216        // Delegate slot assignment to the shared planner (same routine as the verifier).
217        let shapes: Vec<LayoutShape> = tables
218            .iter()
219            .map(|t| LayoutShape {
220                arity: t.num_variables(),
221                width: t.num_polys(),
222            })
223            .collect();
224        let (num_variables, placements) = plan_layout(&shapes);
225
226        // Stacked buffer starts zero; unused tail entries stay zero.
227        let mut stacked = Poly::<F>::zero(num_variables);
228
229        // Copy each source column into its planner-assigned slot.
230        for placement in &placements {
231            let table = &tables[placement.idx()];
232            for (poly_idx, selector) in placement.selectors().iter().enumerate() {
233                let poly = table.poly(poly_idx);
234                let dst = selector.index << poly.num_variables();
235                stacked.as_mut_slice()[dst..dst + poly.num_evals()]
236                    .copy_from_slice(poly.as_slice());
237            }
238        }
239
240        Self {
241            tables,
242            placements,
243            num_variables,
244            folding,
245            poly: stacked,
246        }
247    }
248
249    /// Stacks the given tables with local variables before selector variables.
250    ///
251    /// # Layout
252    ///
253    /// Current `new` stores each column contiguously as:
254    ///
255    /// ```text
256    ///     P(selector_bits, local_bits)
257    /// ```
258    ///
259    /// This constructor stores each column strided by selector bits as:
260    ///
261    /// ```text
262    ///     P(local_bits, selector_bits)
263    /// ```
264    ///
265    /// Local table evaluation order is preserved. Only selector bitstrings are
266    /// reversed from the prefix-oriented planner so mixed-arity selector codes
267    /// remain suffix-disjoint.
268    ///
269    /// # Panics
270    ///
271    /// - Table list must be non-empty.
272    /// - Tables below the preprocessing depth are zero-padded to that depth.
273    #[tracing::instrument(skip_all)]
274    pub fn new_interleaved(mut tables: Vec<Table<F>>, folding: usize) -> Self {
275        assert!(
276            !tables.is_empty(),
277            "Witness requires at least one source table"
278        );
279        tables.iter_mut().for_each(|table| table.pad_zeros(folding));
280
281        let shapes: Vec<LayoutShape> = tables
282            .iter()
283            .map(|t| LayoutShape {
284                arity: t.num_variables(),
285                width: t.num_polys(),
286            })
287            .collect();
288        let (num_variables, mut placements) = plan_layout(&shapes);
289        placements
290            .iter_mut()
291            .for_each(TablePlacement::reverse_selectors);
292
293        let mut stacked = Poly::<F>::zero(num_variables);
294
295        for placement in &placements {
296            let table = &tables[placement.idx()];
297            for (poly_idx, selector) in placement.selectors().iter().enumerate() {
298                let poly = table.poly(poly_idx);
299
300                for (local_idx, &value) in poly.as_slice().iter().enumerate() {
301                    let dst = (local_idx << selector.num_variables) | selector.index;
302                    stacked.as_mut_slice()[dst] = value;
303                }
304            }
305        }
306
307        Self {
308            tables,
309            placements,
310            num_variables,
311            folding,
312            poly: stacked,
313        }
314    }
315
316    /// Returns the number of variables of the stacked polynomial.
317    pub const fn num_variables(&self) -> usize {
318        self.num_variables
319    }
320
321    /// Returns verifier table shapes after witness normalization/padding.
322    pub fn table_shapes(&self) -> Vec<TableShape> {
323        self.tables.iter().map(Table::shape).collect()
324    }
325
326    /// Returns the stacked committed polynomial.
327    pub const fn poly(&self) -> &Poly<F> {
328        &self.poly
329    }
330
331    /// Splits the witness into its owned components for downstream use.
332    pub(super) fn into_parts(self) -> WitnessParts<F> {
333        // Hand each field to the caller verbatim; no normalisation is needed.
334        WitnessParts {
335            tables: self.tables,
336            placements: self.placements,
337            num_variables: self.num_variables,
338            folding: self.folding,
339            poly: self.poly,
340        }
341    }
342}
343
344/// Owned components of a stacked-layout commitment.
345#[derive(Debug, Clone)]
346pub(super) struct WitnessParts<F: Field> {
347    /// Source tables stacked into the committed polynomial.
348    pub(super) tables: Vec<Table<F>>,
349    /// Per-table placement metadata inside the stacked polynomial.
350    pub(super) placements: Vec<TablePlacement>,
351    /// Number of variables of the stacked polynomial.
352    pub(super) num_variables: usize,
353    /// Number of preprocessing rounds folded upfront.
354    pub(super) folding: usize,
355    /// Stacked committed polynomial.
356    pub(super) poly: Poly<F>,
357}
358
359#[cfg(test)]
360mod tests {
361    use alloc::vec;
362
363    use p3_baby_bear::BabyBear;
364    use p3_field::PrimeCharacteristicRing;
365    use p3_field::extension::BinomialExtensionField;
366    use p3_util::log2_ceil_usize;
367    use proptest::prelude::*;
368    use rand::SeedableRng;
369    use rand::rngs::SmallRng;
370
371    use super::*;
372    use crate::layout::{Layout, PrefixProver, SuffixProver};
373
374    type F = BabyBear;
375    type EF = BinomialExtensionField<F, 4>;
376
377    #[test]
378    fn selector_new_stores_num_variables_and_index() {
379        // Invariant:
380        //     Constructor stores both fields verbatim.
381        //
382        // Fixture state:
383        //     num_variables = 3, index = 5
384        let sel = Selector::new(3, 5);
385
386        // Check: getters return what was passed in.
387        assert_eq!(sel.num_variables(), 3);
388        assert_eq!(sel.index(), 5);
389    }
390
391    #[test]
392    #[should_panic]
393    fn selector_new_panics_on_index_out_of_range() {
394        // Invariant:
395        //     A slot index that does not fit in num_variables bits is rejected.
396        //
397        // Fixture state:
398        //     num_variables = 2 → legal indices are 0..=3; use 4 to trigger panic.
399        let _ = Selector::new(2, 4);
400    }
401
402    #[test]
403    fn selector_point_returns_boolean_vector_of_right_length() {
404        // Invariant:
405        //     point() returns a num_variables-long vector of 0/1 field elements.
406        //
407        // Fixture state:
408        //     num_variables = 3, index = 5 = 0b101 → point bits are {1, 0, 1}.
409        let sel = Selector::new(3, 5);
410        let point: Point<F> = sel.point();
411
412        // Check: length matches the bit-width.
413        assert_eq!(point.num_variables(), 3);
414
415        // Check: every coordinate is either 0 or 1.
416        for &bit in point.as_slice() {
417            assert!(bit == F::ZERO || bit == F::ONE);
418        }
419    }
420
421    #[test]
422    fn selector_lift_prefixes_selector_bits_onto_other() {
423        // Invariant:
424        //     lift(other) returns a point with the selector bits prepended,
425        //     total length = selector.num_variables() + other.num_variables().
426        //
427        // Fixture state:
428        //     selector: 3-bit, index 5 → 3 prefix coordinates.
429        //     other:    2-variable point over EF.
430        let sel = Selector::new(3, 5);
431        let other: Point<EF> = Point::new(vec![EF::from_u64(7), EF::from_u64(11)]);
432        let lifted = sel.lift_prefix(&other);
433
434        // Check: total length is the concatenation length.
435        assert_eq!(lifted.num_variables(), 5);
436
437        // Check: the suffix matches the original point element-wise.
438        for i in 0..2 {
439            assert_eq!(lifted.as_slice()[3 + i], other.as_slice()[i]);
440        }
441    }
442
443    #[test]
444    fn selector_lift_suffix_appends_selector_bits_after_other() {
445        // Invariant:
446        //     lift_suffix(other) returns a point with the local coordinates
447        //     first and the selector bits appended, total length =
448        //     other.num_variables() + selector.num_variables().
449        //
450        // Fixture state:
451        //     selector: 3-bit, index 5 = 0b101 → bits {1, 0, 1}.
452        //     other:    2-variable point over EF.
453        let sel = Selector::new(3, 5);
454        let other: Point<EF> = Point::new(vec![EF::from_u64(7), EF::from_u64(11)]);
455        let lifted = sel.lift_suffix(&other);
456
457        // Check: total length is the concatenation length.
458        assert_eq!(lifted.num_variables(), 5);
459
460        // Check: the prefix matches the original point element-wise.
461        assert_eq!(lifted.as_slice()[0], other.as_slice()[0]);
462        assert_eq!(lifted.as_slice()[1], other.as_slice()[1]);
463
464        // Check: the suffix is the boolean expansion of the slot index.
465        let selector_bits: Point<EF> = sel.point();
466        assert_eq!(lifted.as_slice()[2], selector_bits.as_slice()[0]);
467        assert_eq!(lifted.as_slice()[3], selector_bits.as_slice()[1]);
468        assert_eq!(lifted.as_slice()[4], selector_bits.as_slice()[2]);
469    }
470
471    #[test]
472    fn selector_reverse_swaps_msb_and_lsb_within_width() {
473        // Invariant:
474        //     reverse() flips the slot index bitstring within num_variables bits,
475        //     leaving num_variables itself untouched.
476        //
477        // Fixture state:
478        //     num_variables = 4, index = 0b0010 = 2 → reversed: 0b0100 = 4.
479        let mut sel = Selector::new(4, 0b0010);
480        sel.reverse();
481
482        // Check: bit-width is preserved.
483        assert_eq!(sel.num_variables(), 4);
484        // Check: index has been bit-reversed within those 4 bits.
485        assert_eq!(sel.index(), 0b0100);
486    }
487
488    #[test]
489    fn selector_reverse_is_idempotent_under_double_application() {
490        // Invariant:
491        //     Reversing a selector twice restores the original index.
492        //
493        // Fixture state:
494        //     num_variables = 5, index = 13 → reverse → reverse → 13.
495        let mut sel = Selector::new(5, 13);
496        let original_index = sel.index();
497
498        sel.reverse();
499        sel.reverse();
500
501        assert_eq!(sel.num_variables(), 5);
502        assert_eq!(sel.index(), original_index);
503    }
504
505    #[test]
506    #[should_panic]
507    fn table_new_panics_on_empty_polys() {
508        // Invariant:
509        //     A table with no columns is rejected.
510        let _: Table<F> = Table::new(vec![]);
511    }
512
513    #[test]
514    #[should_panic(expected = "must share the same arity")]
515    fn table_new_panics_on_mixed_arities() {
516        // Invariant:
517        //     Mixing columns of different arities is rejected.
518        //
519        // Fixture state:
520        //     poly_a: 3 variables, poly_b: 4 variables → must panic.
521        let poly_a = Poly::<F>::zero(3);
522        let poly_b = Poly::<F>::zero(4);
523        let _ = Table::new(vec![poly_a, poly_b]);
524    }
525
526    #[test]
527    fn table_accessors_report_shape() {
528        // Invariant:
529        //     num_polys, num_variables, size, and poly(id) all agree with the input.
530        //
531        // Fixture state:
532        //     two columns of arity 3 → num_polys = 2, num_variables = 3, size = 2^3 * 2 = 16.
533        let table = Table::new(vec![Poly::<F>::zero(3), Poly::<F>::zero(3)]);
534
535        // Check: all shape queries match the fixture.
536        assert_eq!(table.num_polys(), 2);
537        assert_eq!(table.num_variables(), 3);
538        // Check: column lookup returns a ref to the i-th poly with matching arity.
539        assert_eq!(table.poly(0).num_variables(), 3);
540    }
541
542    #[test]
543    fn table_placement_accessors_return_stored_values() {
544        // Invariant:
545        //     TablePlacement forwards the table index and the selector slice.
546        //
547        // Fixture state:
548        //     idx = 7, two selectors with num_variables = 2.
549        let selectors = vec![Selector::new(2, 0), Selector::new(2, 1)];
550        let placement = TablePlacement::new(7, selectors);
551
552        // Check: idx() forwards the constructor argument.
553        assert_eq!(placement.idx(), 7);
554        // Check: num_polys() matches the selector count.
555        assert_eq!(placement.num_polys(), 2);
556        // Check: selectors() exposes the underlying slice.
557        assert_eq!(placement.selectors().len(), 2);
558    }
559
560    #[test]
561    fn table_placement_reverse_selectors_flips_each_in_place() {
562        // Invariant:
563        //     reverse_selectors() bit-reverses every selector index in place,
564        //     leaving each selector's width and the placement's table index
565        //     untouched.
566        //
567        // Fixture state:
568        //     idx = 4, three selectors of width 3 with indices {0b001, 0b010, 0b110}.
569        //     Reversed indices within 3 bits: {0b100, 0b010, 0b011} = {4, 2, 3}.
570        let selectors = vec![
571            Selector::new(3, 0b001),
572            Selector::new(3, 0b010),
573            Selector::new(3, 0b110),
574        ];
575        let mut placement = TablePlacement::new(4, selectors);
576
577        placement.reverse_selectors();
578
579        // Check: the table index is preserved.
580        assert_eq!(placement.idx(), 4);
581        // Check: every selector's width is preserved.
582        for selector in placement.selectors() {
583            assert_eq!(selector.num_variables(), 3);
584        }
585        // Check: each selector index has been bit-reversed within 3 bits.
586        let indices: Vec<usize> = placement.selectors().iter().map(Selector::index).collect();
587        assert_eq!(indices, vec![0b100, 0b010, 0b011]);
588    }
589
590    #[test]
591    fn table_shape_reports_arity_and_width() {
592        // Invariant:
593        //     shape() returns the (num_variables, width) pair derived from the
594        //     table's columns.
595        //
596        // Fixture state:
597        //     three columns of arity 4 → expected shape: (4, 3).
598        let table = Table::new(vec![
599            Poly::<F>::zero(4),
600            Poly::<F>::zero(4),
601            Poly::<F>::zero(4),
602        ]);
603
604        let shape = table.shape();
605
606        assert_eq!(shape.num_variables(), 4);
607        assert_eq!(shape.width(), 3);
608    }
609
610    // Builds a deterministic witness with two tables of arities (4, 3) and
611    // two columns each. Used by several tests to avoid repeating the setup.
612    fn fixture_witness() -> Witness<F> {
613        let mut rng = SmallRng::seed_from_u64(1);
614        // Table 0: arity 3, two columns.
615        let t0 = Table::new(vec![
616            Poly::<F>::rand(&mut rng, 3),
617            Poly::<F>::rand(&mut rng, 3),
618        ]);
619        // Table 1: arity 4, two columns.
620        let t1 = Table::new(vec![
621            Poly::<F>::rand(&mut rng, 4),
622            Poly::<F>::rand(&mut rng, 4),
623        ]);
624        Witness::new(vec![t0, t1], 1)
625    }
626
627    #[test]
628    fn witness_new_places_largest_table_first() {
629        // Invariant:
630        //     Placement order is tables sorted by arity ascending, reversed.
631        //
632        // Fixture state:
633        //     table 0: arity 3, table 1: arity 4.
634        //     expected placement order: [table 1 (arity 4), table 0 (arity 3)]
635        let w = fixture_witness();
636
637        // Check: first placement targets the larger table.
638        assert_eq!(w.placements[0].idx(), 1);
639        // Check: second placement targets the smaller table.
640        assert_eq!(w.placements[1].idx(), 0);
641    }
642
643    #[test]
644    fn witness_num_variables_is_stacked_arity_rounded_up() {
645        // Invariant:
646        //     num_variables equals log2_ceil of total stacked size.
647        //
648        // Fixture state:
649        //     total size = 2^3 * 2 + 2^4 * 2 = 16 + 32 = 48 → ceil(log2) = 6.
650        let w = fixture_witness();
651        assert_eq!(w.num_variables(), 6);
652    }
653
654    #[test]
655    fn witness_new_copies_column_evals_into_slots() {
656        // Invariant:
657        //     Every column's evaluations appear in the stacked polynomial
658        //     at the offset computed by its selector.
659        //
660        // Fixture state:
661        //     two columns per table, two tables; each column occupies its
662        //     own slot; destination = selector.index << arity.
663        let w = fixture_witness();
664
665        // Walk every placement; for each column, compare the slot slice to the source column.
666        for placement in &w.placements {
667            let table = &w.tables[placement.idx()];
668            for (poly_idx, selector) in placement.selectors().iter().enumerate() {
669                let col = table.poly(poly_idx);
670                let dst = selector.index() << col.num_variables();
671                let slot = &w.poly.as_slice()[dst..dst + col.num_evals()];
672                // Check: slot contents match the source column evaluations.
673                assert_eq!(slot, col.as_slice());
674            }
675        }
676    }
677
678    #[test]
679    fn witness_new_interleaves_by_suffix_selectors() {
680        // Invariant:
681        //     Local-first stacking stores P(local_bits, selector_bits), preserving
682        //     each table's local evaluation order.
683        //
684        // Fixture state:
685        //     A has arity 2: [a0, a1, a2, a3]
686        //     B has arity 1: [b0, b1]
687        //     Expected storage: [a0, b0, a1, 0, a2, b1, a3, 0].
688        let a0 = F::from_u64(10);
689        let a1 = F::from_u64(11);
690        let a2 = F::from_u64(12);
691        let a3 = F::from_u64(13);
692        let b0 = F::from_u64(20);
693        let b1 = F::from_u64(21);
694
695        let table_a = Table::new(vec![Poly::new(vec![a0, a1, a2, a3])]);
696        let table_b = Table::new(vec![Poly::new(vec![b0, b1])]);
697        let witness = Witness::new_interleaved(vec![table_a, table_b], 0);
698
699        assert_eq!(witness.num_variables(), 3);
700        assert_eq!(
701            witness.poly.as_slice(),
702            &[a0, b0, a1, F::ZERO, a2, b1, a3, F::ZERO],
703        );
704    }
705
706    #[test]
707    fn witness_new_pads_tables_below_folding() {
708        // Invariant:
709        //     A table smaller than the preprocessing depth is committed as the
710        //     zero-padded polynomial with arity equal to folding.
711        let a0 = F::from_u64(10);
712        let a1 = F::from_u64(11);
713        let table = Table::new(vec![Poly::new(vec![a0, a1])]);
714
715        let witness = Witness::new(vec![table], 3);
716
717        assert_eq!(witness.tables[0].num_variables(), 3);
718        assert_eq!(witness.num_variables(), 3);
719        assert_eq!(
720            witness.tables[0].poly(0).as_slice(),
721            &[a0, a1, F::ZERO, F::ZERO, F::ZERO, F::ZERO, F::ZERO, F::ZERO],
722        );
723        assert_eq!(
724            witness.poly.as_slice(),
725            &[a0, a1, F::ZERO, F::ZERO, F::ZERO, F::ZERO, F::ZERO, F::ZERO],
726        );
727    }
728
729    #[test]
730    fn witness_table_shapes_returns_shapes_in_source_order() {
731        // Invariant:
732        //     table_shapes() returns one shape per source table, in the order
733        //     the witness was constructed (independent of placement order).
734        //
735        // Fixture state:
736        //     table 0: arity 3, two columns → shape (3, 2).
737        //     table 1: arity 4, two columns → shape (4, 2).
738        //     Source order: [table 0, table 1].
739        let w = fixture_witness();
740
741        let shapes = w.table_shapes();
742
743        assert_eq!(shapes, vec![TableShape::new(3, 2), TableShape::new(4, 2)],);
744    }
745
746    #[test]
747    fn witness_poly_returns_the_stacked_polynomial() {
748        // Invariant:
749        //     poly() returns a borrow of the stacked committed polynomial.
750        //     Its arity equals num_variables() and its contents match the
751        //     internal field used by every consumer.
752        //
753        // Fixture state:
754        //     two-table fixture; expected stacked arity = 6.
755        let w = fixture_witness();
756
757        let stacked = w.poly();
758
759        assert_eq!(stacked.num_variables(), w.num_variables());
760        assert_eq!(stacked.as_slice(), w.poly.as_slice());
761    }
762
763    #[test]
764    fn witness_into_parts_preserves_every_field() {
765        // Invariant:
766        //     into_parts() destructures the witness while preserving every
767        //     field byte-for-byte: source tables (in source order), placement
768        //     metadata, stacked arity, folding depth, and stacked polynomial.
769        //
770        // Fixture state:
771        //     two-table fixture; folding = 1.
772        let w = fixture_witness();
773        // Snapshot every field before consumption.
774        let expected_num_variables = w.num_variables();
775        let expected_folding = w.folding;
776        let expected_table_shapes = w.table_shapes();
777        let expected_placement_idx: Vec<usize> =
778            w.placements.iter().map(TablePlacement::idx).collect();
779        let expected_poly = w.poly.clone();
780
781        let parts = w.into_parts();
782
783        // Check: scalar fields survive the move.
784        assert_eq!(parts.num_variables, expected_num_variables);
785        assert_eq!(parts.folding, expected_folding);
786        // Check: source tables are carried over in source order.
787        let actual_table_shapes: Vec<TableShape> = parts.tables.iter().map(Table::shape).collect();
788        assert_eq!(actual_table_shapes, expected_table_shapes);
789        // Check: placement order and table indices survive verbatim.
790        let actual_placement_idx: Vec<usize> =
791            parts.placements.iter().map(TablePlacement::idx).collect();
792        assert_eq!(actual_placement_idx, expected_placement_idx);
793        // Check: the stacked polynomial is bit-for-bit identical.
794        assert_eq!(parts.poly.as_slice(), expected_poly.as_slice());
795    }
796
797    #[test]
798    fn prefix_prover_from_witness_carries_stacked_state() {
799        // Invariant:
800        //     Handing the witness to the prefix prover preserves the
801        //     stacked polynomial and the per-table shapes.
802        let w = fixture_witness();
803        let stacked_copy = w.poly.clone();
804        let num_variables = w.num_variables();
805
806        // Build a prefix-mode prover from the witness.
807        let prover = PrefixProver::<F, EF>::from_witness(w);
808
809        // Check: arity matches the original stacked polynomial.
810        assert_eq!(prover.num_variables(), num_variables);
811        // Check: the committed polynomial is bit-for-bit identical.
812        assert_eq!(prover.poly.as_slice(), stacked_copy.as_slice());
813    }
814
815    #[test]
816    fn suffix_prover_from_witness_carries_stacked_state() {
817        // Invariant:
818        //     Handing the witness to the suffix prover preserves the
819        //     stacked arity and the per-table data layout.
820        //
821        //     The suffix prover does not retain the stacked polynomial —
822        //     it walks per-table evaluations on demand — so this test only
823        //     checks the structural fields that survive the move.
824        let w = fixture_witness();
825        let num_variables = w.num_variables();
826        let table_shapes = w.table_shapes();
827
828        // Build a suffix-mode prover from the witness.
829        let prover = SuffixProver::<F, EF>::from_witness(w);
830
831        // Check: stacked arity matches.
832        assert_eq!(prover.num_variables(), num_variables);
833        // Check: every source table is carried over with its original shape.
834        let prover_shapes: Vec<TableShape> = prover.tables.iter().map(Table::shape).collect();
835        assert_eq!(prover_shapes, table_shapes);
836    }
837
838    // Proptest strategy: random table shapes within safe bounds.
839    //
840    //     1..=3 tables, each with arity in 2..=5 and 1..=3 columns.
841    //     folding = 1 fits since every arity is at least 2.
842    fn arb_table_shapes() -> impl Strategy<Value = Vec<(usize, usize)>> {
843        prop::collection::vec((2usize..=5, 1usize..=3), 1..=3)
844    }
845
846    proptest! {
847        #![proptest_config(ProptestConfig { cases: 32, ..ProptestConfig::default() })]
848
849        // Invariant:
850        //     For any valid set of table shapes:
851        //     - every column's evaluations live at selector.index << arity
852        //     - num_variables equals log2_ceil of total size
853        //     - trailing slots past the concatenation stay zero
854        #[test]
855        fn witness_stacks_columns_and_zeros_the_tail(shapes in arb_table_shapes()) {
856            let mut rng = SmallRng::seed_from_u64(123);
857
858            // Build one table per (arity, width) entry with random evaluations.
859            let tables: Vec<Table<F>> = shapes
860                .iter()
861                .map(|&(arity, width)| {
862                    let polys = (0..width).map(|_| Poly::<F>::rand(&mut rng, arity)).collect();
863                    Table::new(polys)
864                })
865                .collect();
866
867            // Total stacked size (before power-of-two rounding).
868            let total_used: usize = tables
869                .iter()
870                .map(|t| (1 << t.num_variables()) * t.num_polys())
871                .sum();
872
873            // Folding = 1 is always safe since the strategy guarantees arity >= 2.
874            let witness = Witness::new(tables, 1);
875
876            // Check: stacked arity equals log2_ceil of total occupied size.
877            assert_eq!(witness.num_variables(), log2_ceil_usize(total_used));
878
879            // Check: each column's evaluations land at the predicted slot.
880            let mut used = 0usize;
881            for placement in &witness.placements {
882                let table = &witness.tables[placement.idx()];
883                for (poly_idx, selector) in placement.selectors().iter().enumerate() {
884                    let col = table.poly(poly_idx);
885                    let dst = selector.index() << col.num_variables();
886                    let slot = &witness.poly.as_slice()[dst..dst + col.num_evals()];
887                    assert_eq!(slot, col.as_slice());
888                    used += col.num_evals();
889                }
890            }
891
892            // Check: the counted occupied region matches total_used.
893            assert_eq!(used, total_used);
894
895            // Check: every entry past the concatenation is the zero element.
896            let stacked_len = 1usize << witness.num_variables();
897            for &v in &witness.poly.as_slice()[used..stacked_len] {
898                // The specific region of "used" is contiguous here only because
899                // placements are emitted largest-first and slot offsets are the
900                // cursor value. That matches the "unused tail stays zero" rule.
901                assert_eq!(v, F::ZERO);
902            }
903        }
904    }
905}