Skip to main content

p3_binary_pcs/
packing.rs

1//! Columns over a narrow alphabet, read as the packed elements a commitment holds.
2//!
3//! ```text
4//!     1-bit column, one element per bit   ->  127 of 128 coordinates wasted
5//!     1-bit column, packed                ->    0 of 128 coordinates wasted
6//!     32-bit column, packed               ->    0 of 128 coordinates wasted
7//! ```
8//!
9//! # Why packing moves no bits
10//!
11//! A tower level holds its elements in the basis its bytes already define.
12//! Coordinate `j` of a value is bit `j` of its little-endian byte string.
13//!
14//! One run of narrow cells and the wide elements holding it are the same bytes, in order.
15//! Packing is therefore one copy, and unpacking is that copy back.
16//!
17//! # What the packing means
18//!
19//! Cell `c` owns coordinates `c * a .. (c + 1) * a`, and `d` of them form one element.
20//!
21//! ```text
22//!     a = 1,  d = 128   ->  element w holds cells 128w .. 128w + 128
23//!     a = 32, d = 128   ->  element w holds cells   4w ..   4w + 4
24//! ```
25//!
26//! Which ring switch opens such a commitment depends on the alphabet, not on anything here.
27//!
28//! # Orientation
29//!
30//! A bit-sliced trace often arrives one block per row, with a lane per column.
31//! A commitment wants the transpose of that, every bit of one column contiguous.
32//!
33//! The square bit transpose turns one into the other in `log2(d)` word passes.
34//! Reading one bit per source lane would cost `d^2` bit extractions instead.
35
36use alloc::vec::Vec;
37use core::marker::PhantomData;
38use core::{ptr, slice};
39
40use p3_binary_field::{
41    BinaryField8, BinaryField16, BinaryField32, BinaryField64, BinaryField128, Gf2, PackedGf2,
42    TowerLevel, Underlier,
43};
44use p3_field::PrimeCharacteristicRing;
45use p3_matrix::dense::RowMajorMatrix;
46use p3_maybe_rayon::prelude::*;
47use p3_multilinear_util::poly::Poly;
48use p3_sumcheck::layout::Table;
49use p3_util::log2_strict_usize;
50use thiserror::Error;
51
52/// A value whose bytes are a run of `F_2` coordinates, lowest coordinate first.
53///
54/// # Safety
55///
56/// An implementor is a transparent wrapper over an unsigned integer, or over a block of them.
57/// Every bit pattern of its bytes is a value, so a run has no padding and no invalid state.
58///
59/// The coordinate count is the size in bits, with nothing left over:
60///
61/// ```text
62///     COORDINATES == 8 * size_of::<Self>()
63/// ```
64///
65/// A value carrying more bytes than that would read past the source of a copy.
66/// On a little-endian target coordinate `j` of a run is bit `j % 8` of its byte `j / 8`.
67pub unsafe trait Coordinates: Copy + Send + Sync + 'static {
68    /// Coordinates one value of this type holds.
69    const COORDINATES: usize;
70}
71
72/// Pin the part of one implementor's contract a constant can state: its two counts agree.
73const fn check_coordinates<A: Coordinates>() {
74    assert!(
75        A::COORDINATES == 8 * size_of::<A>(),
76        "a coordinate run must be exactly the bits of its bytes"
77    );
78}
79
80// SAFETY: each level below wraps an integer it fills, whose bits are its own coordinates.
81//
82//     at or above a byte  ->  the integer is full, so a run has no padding
83//     below a byte        ->  the integer is partly unused, so no run qualifies
84//
85// A one-bit alphabet therefore arrives bit-sliced rather than one element per byte.
86unsafe impl Coordinates for BinaryField8 {
87    const COORDINATES: usize = 8;
88}
89
90unsafe impl Coordinates for BinaryField16 {
91    const COORDINATES: usize = 16;
92}
93
94unsafe impl Coordinates for BinaryField32 {
95    const COORDINATES: usize = 32;
96}
97
98unsafe impl Coordinates for BinaryField64 {
99    const COORDINATES: usize = 64;
100}
101
102unsafe impl Coordinates for BinaryField128 {
103    const COORDINATES: usize = 128;
104}
105
106// SAFETY: a packing is exactly its backing block, whose contract pins size and alignment.
107// Lane `j` is bit `j` of the byte view, which is the convention fixed above.
108unsafe impl<U: Underlier> Coordinates for PackedGf2<U> {
109    const COORDINATES: usize = U::BITS;
110}
111
112/// Bytes one task of a packing copy moves.
113///
114/// Enough that each task outweighs the fork-join overhead, so a short run copies in one piece.
115const PACK_CHUNK_BYTES: usize = 1 << 20;
116
117/// Read a run of cells as the wide elements holding the same coordinates.
118///
119/// One copy at the wide alignment, and no arithmetic. Reusing the source buffer is unsound.
120///
121/// # Panics
122///
123/// Panics unless the run's coordinates fill a whole number of wide elements.
124#[must_use]
125pub fn pack<A, EF>(cells: &[A]) -> Vec<EF>
126where
127    A: Coordinates,
128    EF: Coordinates,
129{
130    const {
131        check_coordinates::<A>();
132        check_coordinates::<EF>();
133        assert!(
134            cfg!(target_endian = "little"),
135            "packing a coordinate run needs a little-endian target"
136        );
137    }
138    let coordinates = cells.len() * A::COORDINATES;
139    assert_eq!(
140        coordinates % EF::COORDINATES,
141        0,
142        "a packed run must fill whole elements"
143    );
144    let len = coordinates / EF::COORDINATES;
145    let mut packed = Vec::<EF>::with_capacity(len);
146
147    // The copy runs in chunks of whole destination elements, each chunk on its own task.
148    //
149    // The source is viewed as bytes, so a chunk boundary need not fall on a whole cell.
150    // Both sides hold the same byte count, so the chunks pair up one to one.
151    let elements_per_chunk = (PACK_CHUNK_BYTES / size_of::<EF>()).max(1);
152    packed.spare_capacity_mut()[..len]
153        .par_chunks_mut(elements_per_chunk)
154        .zip(coordinate_bytes(cells).par_chunks(elements_per_chunk * size_of::<EF>()))
155        .for_each(|(destination, source)| {
156            debug_assert_eq!(source.len(), size_of_val(destination));
157            // SAFETY: each side is a padding-free run of its own coordinates, by both contracts.
158            // The constant block above pins each one's count at exactly the bits of its bytes.
159            //
160            // The two cover the same coordinate count, hence the same byte count, and every
161            // pair of chunks sits at the same byte offset into each, with the same length.
162            //
163            // Every bit pattern of the destination is a value, and the chunks are disjoint
164            // parts of reserved capacity nothing else aliases.
165            unsafe {
166                ptr::copy_nonoverlapping(
167                    source.as_ptr(),
168                    destination.as_mut_ptr().cast::<u8>(),
169                    source.len(),
170                );
171            }
172        });
173    // SAFETY: the chunks above cover all `len` reserved elements, so each one is written.
174    unsafe {
175        packed.set_len(len);
176    }
177    packed
178}
179
180/// Read a run of wide elements back as the cells whose coordinates they hold.
181///
182/// The inverse of packing, so a round trip is the identity, and it panics on a partial cell.
183#[must_use]
184pub fn unpack<A, EF>(elements: &[EF]) -> Vec<A>
185where
186    A: Coordinates,
187    EF: Coordinates,
188{
189    const {
190        check_coordinates::<A>();
191        check_coordinates::<EF>();
192        assert!(
193            cfg!(target_endian = "little"),
194            "unpacking a coordinate run needs a little-endian target"
195        );
196    }
197    let coordinates = elements.len() * EF::COORDINATES;
198    assert_eq!(
199        coordinates % A::COORDINATES,
200        0,
201        "an unpacked run must fill whole cells"
202    );
203    let len = coordinates / A::COORDINATES;
204    let mut cells = Vec::<A>::with_capacity(len);
205
206    // SAFETY: the argument above, with the two roles exchanged.
207    unsafe {
208        ptr::copy_nonoverlapping(
209            elements.as_ptr().cast::<u8>(),
210            cells.as_mut_ptr().cast::<u8>(),
211            len * size_of::<A>(),
212        );
213        cells.set_len(len);
214    }
215    cells
216}
217
218/// The byte view of a run of cells, which is the same bytes the packing holds.
219#[must_use]
220pub const fn coordinate_bytes<A: Coordinates>(cells: &[A]) -> &[u8] {
221    const {
222        check_coordinates::<A>();
223        assert!(
224            cfg!(target_endian = "little"),
225            "viewing coordinate bytes needs a little-endian target"
226        );
227    }
228
229    // SAFETY: by the trait contract the run is exactly this many initialised bytes.
230    // It has no padding and no invalid pattern, and every bit pattern of a byte is valid.
231    //
232    // The byte slice borrows the same region for the same lifetime.
233    unsafe { slice::from_raw_parts(cells.as_ptr().cast::<u8>(), size_of_val(cells)) }
234}
235
236/// Equal-height columns over a narrow alphabet, held as the elements a commitment takes.
237///
238/// # Overview
239///
240/// Each column packs on its own, so a column boundary lands on an element boundary.
241///
242/// ```text
243///     elements   [ column 0 ][ column 1 ] ... [ column k-1 ]
244///     column     2^arity elements, each holding d / a cells
245/// ```
246///
247/// That is what a stacked commitment expects of a source table.
248/// Nothing moves between the two.
249#[derive(Clone, Debug, PartialEq, Eq)]
250pub struct PackedStack<A, EF> {
251    /// Every column's elements, lowest column first.
252    elements: Vec<EF>,
253    /// Elements one column occupies.
254    column_len: usize,
255    /// Marker for the alphabet the cells came from.
256    _alphabet: PhantomData<A>,
257}
258
259impl<A, EF> PackedStack<A, EF>
260where
261    A: Coordinates,
262    EF: Coordinates + TowerLevel,
263{
264    /// Cells one wide element absorbs.
265    pub const CELLS_PER_ELEMENT: usize = EF::COORDINATES / A::COORDINATES;
266
267    /// Pack equal-height columns, lowest column first.
268    ///
269    /// # Errors
270    ///
271    /// - The alphabet is wider than the level packing it, so no cell fits.
272    /// - No column was supplied, so there is nothing to commit.
273    /// - The columns have different heights, so the layout has no common arity.
274    /// - A column's cells do not fill a whole number of elements.
275    /// - A packed column is no power-of-two length, so it covers no hypercube.
276    pub fn from_columns(columns: &[&[A]]) -> Result<Self, PackError> {
277        if A::COORDINATES > EF::COORDINATES {
278            return Err(PackError::CellWiderThanElement {
279                cell: A::COORDINATES,
280                element: EF::COORDINATES,
281            });
282        }
283        let Some((&first, rest)) = columns.split_first() else {
284            return Err(PackError::NoColumns);
285        };
286        if let Some(other) = rest.iter().find(|column| column.len() != first.len()) {
287            return Err(PackError::RaggedColumns {
288                expected: first.len(),
289                actual: other.len(),
290            });
291        }
292        if !first.len().is_multiple_of(Self::CELLS_PER_ELEMENT) {
293            return Err(PackError::PartialElement {
294                cells: first.len(),
295                per_element: Self::CELLS_PER_ELEMENT,
296            });
297        }
298
299        let column_len = first.len() / Self::CELLS_PER_ELEMENT;
300        if column_len == 0 || !column_len.is_power_of_two() {
301            return Err(PackError::NotAHypercube {
302                elements: column_len,
303            });
304        }
305
306        // One copy per column, so the whole stack is one sweep over the source.
307        //
308        // A lone column's packing is already the whole stack, so it is kept as it is.
309        let elements = if let [column] = columns {
310            pack::<A, EF>(column)
311        } else {
312            let mut elements = Vec::with_capacity(column_len * columns.len());
313            for column in columns {
314                elements.extend(pack::<A, EF>(column));
315            }
316            elements
317        };
318
319        Ok(Self {
320            elements,
321            column_len,
322            _alphabet: PhantomData,
323        })
324    }
325
326    /// Every column's elements, lowest column first.
327    #[must_use]
328    pub fn elements(&self) -> &[EF] {
329        &self.elements
330    }
331
332    /// Source columns the stack holds.
333    #[must_use]
334    pub const fn num_columns(&self) -> usize {
335        self.elements.len() / self.column_len
336    }
337
338    /// Variables one packed column's multilinear has.
339    #[must_use]
340    pub const fn column_num_variables(&self) -> usize {
341        log2_strict_usize(self.column_len)
342    }
343
344    /// Cells one source column held.
345    #[must_use]
346    pub const fn column_num_cells(&self) -> usize {
347        self.column_len * Self::CELLS_PER_ELEMENT
348    }
349
350    /// One packed column's elements, panicking on a column the stack does not hold.
351    #[must_use]
352    pub fn column(&self, index: usize) -> &[EF] {
353        assert!(index < self.num_columns(), "column index out of range");
354        &self.elements[index * self.column_len..][..self.column_len]
355    }
356
357    /// Which element of the stack, and which cell inside it, one source cell landed in.
358    ///
359    /// This is the view an opening reads, since a claim lands on the committed polynomial.
360    ///
361    /// # Panics
362    ///
363    /// Panics if the column or the cell is not one the stack holds.
364    #[must_use]
365    pub fn locate(&self, column: usize, cell: usize) -> (usize, usize) {
366        assert!(column < self.num_columns(), "column index out of range");
367        assert!(cell < self.column_num_cells(), "cell index out of range");
368        let element = column * self.column_len + cell / Self::CELLS_PER_ELEMENT;
369        (element, cell % Self::CELLS_PER_ELEMENT)
370    }
371
372    /// One source cell, read back out of the element holding it.
373    #[must_use]
374    pub fn cell(&self, column: usize, cell: usize) -> A {
375        let (element, inside) = self.locate(column, cell);
376        unpack::<A, EF>(&self.elements[element..=element])[inside]
377    }
378
379    /// One source column, read back in full.
380    #[must_use]
381    pub fn unpack_column(&self, index: usize) -> Vec<A> {
382        unpack::<A, EF>(self.column(index))
383    }
384
385    /// The stack as the multilinear a single-column commitment holds.
386    pub fn into_poly(self) -> Poly<EF> {
387        Poly::new(self.elements)
388    }
389
390    /// The stack as one source table of a commitment's stacked layout.
391    ///
392    /// Row `i` of the table is packed column `i`, the orientation that layout reads.
393    pub fn into_table(self) -> Table<EF> {
394        Table::new(RowMajorMatrix::new(self.elements, self.column_len))
395    }
396}
397
398impl<U, EF> PackedStack<PackedGf2<U>, EF>
399where
400    U: Underlier,
401    EF: Coordinates + TowerLevel,
402{
403    /// Pack a bit-sliced block whose lanes are columns rather than rows.
404    ///
405    /// ```text
406    ///     in     row r    ->  lanes are columns 0 .. d
407    ///     out    column c ->  lanes are the rows of one block
408    /// ```
409    ///
410    /// One square bit transpose per block of `d` rows, so whole words move at a time.
411    ///
412    /// # Arguments
413    ///
414    /// - `rows`: one block per row, in row order, covering whole blocks.
415    /// - `num_columns`: leading lanes to keep, the rest being unused width.
416    ///
417    /// # Errors
418    ///
419    /// - The rows are no whole number of square blocks, or ask for more than a block's lanes.
420    /// - The transposed columns do not pack, for any of the reasons columns can fail to.
421    pub fn from_bit_rows(rows: &[PackedGf2<U>], num_columns: usize) -> Result<Self, PackError> {
422        let width = PackedGf2::<U>::WIDTH;
423        if !rows.len().is_multiple_of(width) {
424            return Err(PackError::PartialBitBlock {
425                rows: rows.len(),
426                width,
427            });
428        }
429        if num_columns > width {
430            return Err(PackError::TooManyColumns {
431                requested: num_columns,
432                width,
433            });
434        }
435
436        // Column `c` takes lane `c` of every transposed block, so it grows one block at a time.
437        let blocks = rows.len() / width;
438        let mut columns = alloc::vec![Vec::with_capacity(blocks); num_columns];
439        let mut block = alloc::vec![PackedGf2::<U>::from(Gf2::ZERO); width];
440
441        for source in rows.chunks_exact(width) {
442            // The transpose works in place, so the block is copied before it runs.
443            block.copy_from_slice(source);
444            PackedGf2::transpose(&mut block);
445
446            for (column, &transposed) in columns.iter_mut().zip(&block) {
447                column.push(transposed);
448            }
449        }
450
451        let views: Vec<&[PackedGf2<U>]> = columns.iter().map(alloc::vec::Vec::as_slice).collect();
452        Self::from_columns(&views)
453    }
454}
455
456/// Why a run of columns could not be packed.
457#[derive(Clone, Copy, Debug, Error, PartialEq, Eq)]
458#[non_exhaustive]
459pub enum PackError {
460    /// The alphabet is wider than the level packing it, so no cell fits.
461    #[error("a {cell}-coordinate cell does not fit a {element}-coordinate element")]
462    CellWiderThanElement {
463        /// Coordinates one cell holds.
464        cell: usize,
465        /// Coordinates one element holds.
466        element: usize,
467    },
468
469    /// No column was supplied, so there is nothing to commit.
470    #[error("a packed stack needs at least one column")]
471    NoColumns,
472
473    /// The columns have different heights, so the layout has no common arity.
474    #[error("a column holds {actual} cells, expected {expected}")]
475    RaggedColumns {
476        /// Cells the first column holds.
477        expected: usize,
478        /// Cells the offending column holds.
479        actual: usize,
480    },
481
482    /// A column's cells do not fill a whole number of elements.
483    #[error("{cells} cells do not fill whole elements of {per_element} cells each")]
484    PartialElement {
485        /// Cells one column holds.
486        cells: usize,
487        /// Cells one element absorbs.
488        per_element: usize,
489    },
490
491    /// A packed column is no power-of-two length, so it covers no hypercube.
492    #[error("a packed column holds {elements} elements, which is no hypercube")]
493    NotAHypercube {
494        /// Elements one packed column holds.
495        elements: usize,
496    },
497
498    /// The row count is no whole number of square blocks.
499    #[error("{rows} bit-sliced rows are no whole number of {width}-row blocks")]
500    PartialBitBlock {
501        /// Rows supplied.
502        rows: usize,
503        /// Rows one square block holds.
504        width: usize,
505    },
506
507    /// More columns are asked for than a block has lanes.
508    #[error("{requested} columns exceed the {width} lanes a block holds")]
509    TooManyColumns {
510        /// Columns requested.
511        requested: usize,
512        /// Lanes one block holds.
513        width: usize,
514    },
515}
516
517#[cfg(test)]
518mod tests {
519    use p3_binary_field::{PackedGf2x8, PackedGf2x64, PackedGf2x128};
520    use p3_field::{BasedVectorSpace, PrimeCharacteristicRing};
521    use proptest::prelude::*;
522    use rand::rngs::SmallRng;
523    use rand::{RngExt, SeedableRng};
524
525    use super::*;
526
527    type EF = BinaryField128;
528
529    /// Coordinate `j` of a run of cells, read one bit at a time.
530    fn coordinate<A: Coordinates>(cells: &[A], index: usize) -> bool {
531        let bytes = coordinate_bytes(cells);
532        (bytes[index / 8] >> (index % 8)) & 1 == 1
533    }
534
535    #[test]
536    fn packing_preserves_every_coordinate_in_order() {
537        //     cells      32 words  *  32 = 1024 coordinates
538        //     elements    8 wide    * 128 = 1024 coordinates
539        let mut rng = SmallRng::seed_from_u64(0xC0DE);
540        let cells: Vec<BinaryField32> = (0..32).map(|_| rng.random()).collect();
541        let packed = pack::<BinaryField32, EF>(&cells);
542
543        assert_eq!(packed.len(), 8);
544        for index in 0..32 * 32 {
545            assert_eq!(
546                coordinate(&packed, index),
547                coordinate(&cells, index),
548                "coordinate {index}"
549            );
550        }
551    }
552
553    #[test]
554    fn the_reinterpretation_agrees_with_the_field_basis_accessor() {
555        // Invariant: the copy this module performs is the tower basis composition.
556        //
557        //     packed   one memcpy over the whole run
558        //     basis    one element at a time, through the field's own accessor
559        //
560        // The accessor composes arithmetically, so agreement is evidence rather than a tautology.
561        let mut rng = SmallRng::seed_from_u64(0xBA515);
562
563        let words: Vec<BinaryField32> = (0..64).map(|_| rng.random()).collect();
564        let by_basis: Vec<EF> = words
565            .as_chunks::<4>()
566            .0
567            .iter()
568            .map(|chunk| EF::from_basis_coefficients_fn(|index| chunk[index]))
569            .collect();
570        assert_eq!(pack::<BinaryField32, EF>(&words), by_basis);
571
572        let halves: Vec<BinaryField64> = (0..64).map(|_| rng.random()).collect();
573        let by_basis: Vec<EF> = halves
574            .as_chunks::<2>()
575            .0
576            .iter()
577            .map(|chunk| EF::from_basis_coefficients_fn(|index| chunk[index]))
578            .collect();
579        assert_eq!(pack::<BinaryField64, EF>(&halves), by_basis);
580    }
581
582    #[test]
583    fn a_round_trip_through_the_packing_is_the_identity() {
584        // Both directions are the same copy, so neither may lose or reorder a coordinate.
585        let mut rng = SmallRng::seed_from_u64(0xB1A5);
586
587        let words: Vec<BinaryField64> = (0..64).map(|_| rng.random()).collect();
588        assert_eq!(unpack::<BinaryField64, EF>(&pack::<_, EF>(&words)), words);
589
590        let bytes: Vec<BinaryField8> = (0..256).map(|_| rng.random()).collect();
591        assert_eq!(unpack::<BinaryField8, EF>(&pack::<_, EF>(&bytes)), bytes);
592
593        let blocks: Vec<PackedGf2x64> = (0..16)
594            .map(|_| PackedGf2x64::new(rng.random::<u64>()))
595            .collect();
596        assert_eq!(unpack::<PackedGf2x64, EF>(&pack::<_, EF>(&blocks)), blocks);
597    }
598
599    #[test]
600    fn a_packed_bit_block_carries_the_lanes_it_was_given() {
601        // Invariant: lane `l` of block `b` is coordinate `64b + l` of the packed run.
602        //
603        // This is what lets a bit-sliced column commit without a per-bit pass.
604        let mut rng = SmallRng::seed_from_u64(0xB175);
605        let blocks: Vec<PackedGf2x64> = (0..8)
606            .map(|_| PackedGf2x64::new(rng.random::<u64>()))
607            .collect();
608        let packed = pack::<PackedGf2x64, EF>(&blocks);
609
610        assert_eq!(packed.len(), 4);
611        for (b, block) in blocks.iter().enumerate() {
612            for lane in 0..PackedGf2x64::WIDTH {
613                let set = block.get(lane) == Gf2::ONE;
614                assert_eq!(coordinate(&packed, 64 * b + lane), set, "b={b} lane={lane}");
615            }
616        }
617    }
618
619    #[test]
620    fn the_stack_lays_columns_out_back_to_back() {
621        //     column   8 words at 4 per element, so arity 1
622        //     stack    three columns, 6 elements, column 0 first
623        let mut rng = SmallRng::seed_from_u64(0x57AC);
624        let columns: Vec<Vec<BinaryField32>> = (0..3)
625            .map(|_| (0..8).map(|_| rng.random()).collect())
626            .collect();
627        let views: Vec<&[BinaryField32]> = columns.iter().map(Vec::as_slice).collect();
628
629        let stack = PackedStack::<BinaryField32, EF>::from_columns(&views).unwrap();
630
631        assert_eq!(stack.num_columns(), 3);
632        assert_eq!(stack.column_num_variables(), 1);
633        assert_eq!(stack.column_num_cells(), 8);
634        assert_eq!(stack.elements().len(), 6);
635
636        // Each column reads back what went in, cell by cell and in bulk.
637        for (index, column) in columns.iter().enumerate() {
638            assert_eq!(&stack.unpack_column(index), column);
639            for (cell, &value) in column.iter().enumerate() {
640                assert_eq!(stack.cell(index, cell), value, "column={index} cell={cell}");
641            }
642        }
643
644        // The located element is the one holding the cell, at the offset reported.
645        let (element, inside) = stack.locate(2, 5);
646        assert_eq!(element, 2 * 2 + 1);
647        assert_eq!(inside, 1);
648    }
649
650    #[test]
651    fn the_stacked_table_keeps_one_row_per_packed_column() {
652        // The stacked layout reads a source table's rows as its polynomials.
653        let mut rng = SmallRng::seed_from_u64(0x7AB1);
654        let columns: Vec<Vec<BinaryField64>> = (0..2)
655            .map(|_| (0..8).map(|_| rng.random()).collect())
656            .collect();
657        let views: Vec<&[BinaryField64]> = columns.iter().map(Vec::as_slice).collect();
658        let stack = PackedStack::<BinaryField64, EF>::from_columns(&views).unwrap();
659
660        let table = stack.clone().into_table();
661        assert_eq!(table.num_polys(), 2);
662        assert_eq!(table.num_variables(), 2);
663        for index in 0..2 {
664            assert_eq!(table.poly(index).as_slice(), stack.column(index));
665        }
666    }
667
668    /// Exchange rows and columns of a bit-sliced block one lane at a time.
669    fn transpose_reference<U: Underlier>(
670        rows: &[PackedGf2<U>],
671        num_columns: usize,
672    ) -> Vec<Vec<PackedGf2<U>>> {
673        let width = PackedGf2::<U>::WIDTH;
674        (0..num_columns)
675            .map(|column| {
676                rows.chunks_exact(width)
677                    .map(|block| PackedGf2::<U>::from_fn(|lane| block[lane].get(column)))
678                    .collect()
679            })
680            .collect()
681    }
682
683    #[test]
684    fn a_row_major_bit_block_packs_as_its_own_transpose() {
685        // Invariant: column `c` of the stack is lane `c` of every source row.
686        //
687        //     rows      128 blocks in,  lane = column
688        //     columns    40 runs out,   lane = row within a block
689        //
690        // The reference reads one bit per source lane, sharing nothing with the transpose.
691        let mut rng = SmallRng::seed_from_u64(0x7A05);
692        let rows: Vec<PackedGf2x64> = (0..128)
693            .map(|_| PackedGf2x64::new(rng.random::<u64>()))
694            .collect();
695
696        let stack = PackedStack::<PackedGf2x64, EF>::from_bit_rows(&rows, 40).unwrap();
697        let expected = transpose_reference(&rows, 40);
698
699        assert_eq!(stack.num_columns(), 40);
700        for (column, blocks) in expected.iter().enumerate() {
701            assert_eq!(&stack.unpack_column(column), blocks, "column={column}");
702        }
703    }
704
705    #[test]
706    fn a_row_major_block_is_refused_unless_it_is_square_and_wide_enough() {
707        // A partial block has no transpose, and a column past the lanes has no source.
708        let rows = alloc::vec![PackedGf2x8::from(Gf2::ZERO); 12];
709        assert_eq!(
710            PackedStack::<PackedGf2x8, EF>::from_bit_rows(&rows, 8).unwrap_err(),
711            PackError::PartialBitBlock { rows: 12, width: 8 }
712        );
713
714        let rows = alloc::vec![PackedGf2x8::from(Gf2::ZERO); 128];
715        assert_eq!(
716            PackedStack::<PackedGf2x8, EF>::from_bit_rows(&rows, 9).unwrap_err(),
717            PackError::TooManyColumns {
718                requested: 9,
719                width: 8,
720            }
721        );
722    }
723
724    #[test]
725    fn a_stack_that_describes_no_commitment_is_refused() {
726        // No column at all.
727        assert_eq!(
728            PackedStack::<BinaryField32, EF>::from_columns(&[]).unwrap_err(),
729            PackError::NoColumns
730        );
731
732        // Columns of different heights have no common arity.
733        let long = alloc::vec![BinaryField32::ZERO; 8];
734        let short = alloc::vec![BinaryField32::ZERO; 4];
735        assert_eq!(
736            PackedStack::<BinaryField32, EF>::from_columns(&[&long, &short]).unwrap_err(),
737            PackError::RaggedColumns {
738                expected: 8,
739                actual: 4,
740            }
741        );
742
743        // Six words fill one element of four and leave two over.
744        let ragged = alloc::vec![BinaryField32::ZERO; 6];
745        assert_eq!(
746            PackedStack::<BinaryField32, EF>::from_columns(&[&ragged]).unwrap_err(),
747            PackError::PartialElement {
748                cells: 6,
749                per_element: 4,
750            }
751        );
752
753        // Twelve words fill three elements, which is no hypercube.
754        let odd = alloc::vec![BinaryField32::ZERO; 12];
755        assert_eq!(
756            PackedStack::<BinaryField32, EF>::from_columns(&[&odd]).unwrap_err(),
757            PackError::NotAHypercube { elements: 3 }
758        );
759
760        // A cell wider than the element packing it fits nowhere.
761        let wide = alloc::vec![PackedGf2x128::from(Gf2::ZERO); 4];
762        assert_eq!(
763            PackedStack::<PackedGf2x128, BinaryField64>::from_columns(&[&wide]).unwrap_err(),
764            PackError::CellWiderThanElement {
765                cell: 128,
766                element: 64,
767            }
768        );
769    }
770
771    proptest! {
772        /// Every alphabet this module packs, over every shape a column can take.
773        ///
774        /// Sixteen cells is the smallest run filling one element of the widest level.
775        #[test]
776        fn packing_is_a_bijection_on_coordinates(
777            log_cells in 4usize..=9,
778            seed: u64,
779        ) {
780            let mut rng = SmallRng::seed_from_u64(seed);
781            let cells = 1usize << log_cells;
782
783            // A one-bit alphabet, arriving bit-sliced.
784            let bits: Vec<PackedGf2x8> = (0..cells)
785                .map(|_| PackedGf2x8::new(rng.random::<u8>()))
786                .collect();
787            prop_assert_eq!(unpack::<PackedGf2x8, EF>(&pack::<_, EF>(&bits)), bits);
788
789            // A byte alphabet, where sixteen cells fill one element.
790            let bytes: Vec<BinaryField8> = (0..cells).map(|_| rng.random()).collect();
791            prop_assert_eq!(unpack::<BinaryField8, EF>(&pack::<_, EF>(&bytes)), bytes);
792
793            // A word alphabet, where four cells fill one element.
794            let words: Vec<BinaryField32> = (0..cells).map(|_| rng.random()).collect();
795            prop_assert_eq!(&unpack::<BinaryField32, EF>(&pack::<_, EF>(&words)), &words);
796
797            // The coordinate strings agree position for position, on the widest of the three.
798            let packed = pack::<BinaryField32, EF>(&words);
799            for index in 0..cells * 32 {
800                prop_assert_eq!(coordinate(&packed, index), coordinate(&words, index));
801            }
802        }
803    }
804}