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}