use alloc::vec::Vec;
use p3_field::{Field, PackedValue};
use p3_matrix::Matrix;
use p3_matrix::dense::RowMajorMatrix;
use p3_maybe_rayon::prelude::*;
use p3_multilinear_util::point::Point;
use p3_multilinear_util::poly::{Poly, PolyView};
use p3_util::reverse_bits_len;
use rand::Rng;
use rand::distr::{Distribution, StandardUniform};
use thiserror::Error;
use tracing::info_span;
use crate::layout::plan::{LayoutShape, plan_layout, plan_stacked_layout};
use crate::table::TableShape;
const STACK_COPY_CHUNK: usize = 1 << 16;
#[derive(Debug, Clone, Copy)]
pub struct Selector {
num_variables: usize,
index: usize,
}
impl Selector {
pub const fn new(num_variables: usize, index: usize) -> Self {
assert!(index < (1 << num_variables));
Self {
num_variables,
index,
}
}
#[inline(always)]
pub fn point<F: Field>(&self) -> Point<F> {
Point::hypercube(self.index, self.num_variables)
}
#[inline(always)]
pub const fn num_variables(&self) -> usize {
self.num_variables
}
#[inline(always)]
pub const fn index(&self) -> usize {
self.index
}
#[inline(always)]
pub const fn reverse(&mut self) {
self.index = reverse_bits_len(self.index, self.num_variables);
}
#[inline(always)]
pub fn lift_prefix<Ext: Field>(&self, other: &Point<Ext>) -> Point<Ext> {
let mut out: Point<Ext> = self.point();
out.extend(other);
out
}
#[inline(always)]
pub fn lift_suffix<Ext: Field>(&self, other: &Point<Ext>) -> Point<Ext> {
let mut out = other.clone();
out.extend(&self.point());
out
}
}
#[derive(Debug, Clone)]
enum TableStorage<F: Field> {
Dense(RowMajorMatrix<F>),
Boolean {
words: RowMajorMatrix<u64>,
num_variables: usize,
},
}
#[derive(Clone, Copy)]
pub enum ColumnView<'a, F: Field> {
Dense(&'a [F]),
Boolean {
words: &'a RowMajorMatrix<u64>,
column: usize,
len: usize,
},
}
impl<F: Field> core::fmt::Debug for ColumnView<'_, F> {
fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
formatter
.debug_struct("ColumnView")
.field("len", &self.len())
.finish()
}
}
pub struct ColumnValues<'a, F: Field> {
view: ColumnView<'a, F>,
range: core::ops::Range<usize>,
}
impl<F: Field> Iterator for ColumnValues<'_, F> {
type Item = F;
fn next(&mut self) -> Option<Self::Item> {
self.range.next().map(|row| self.view.value(row))
}
fn size_hint(&self) -> (usize, Option<usize>) {
let len = self.range.len();
(len, Some(len))
}
}
impl<F: Field> DoubleEndedIterator for ColumnValues<'_, F> {
fn next_back(&mut self) -> Option<Self::Item> {
self.range.next_back().map(|row| self.view.value(row))
}
}
impl<F: Field> ExactSizeIterator for ColumnValues<'_, F> {}
impl<'a, F: Field> ColumnView<'a, F> {
#[inline]
pub const fn len(self) -> usize {
match self {
Self::Dense(values) => values.len(),
Self::Boolean { len, .. } => len,
}
}
#[inline]
pub const fn is_empty(self) -> bool {
self.len() == 0
}
#[inline]
pub fn value(self, row: usize) -> F {
assert!(row < self.len(), "column row out of bounds");
match self {
Self::Dense(values) => values[row],
Self::Boolean { words, column, .. } => F::from_bool(
((words.values[(row / 64) * words.width + column] >> (row % 64)) & 1) != 0,
),
}
}
#[inline]
pub const fn values(self) -> ColumnValues<'a, F> {
ColumnValues {
view: self,
range: 0..self.len(),
}
}
#[inline]
pub const fn as_dense(self) -> Option<&'a [F]> {
match self {
Self::Dense(values) => Some(values),
Self::Boolean { .. } => None,
}
}
#[inline]
pub fn packed_at(self, row: usize) -> F::Packing {
F::Packing::from_fn(|lane| self.value(row + lane))
}
#[inline]
pub fn boolean_word(self, word: usize) -> Option<u64> {
match self {
Self::Boolean { words, column, .. } => words
.values
.get(word.checked_mul(words.width)? + column)
.copied(),
Self::Dense(_) => None,
}
}
pub fn copy_into(self, destination: &mut [F]) {
assert_eq!(
destination.len(),
self.len(),
"column destination has wrong length"
);
if let Some(values) = self.as_dense() {
destination.copy_from_slice(values);
} else {
for (destination, value) in destination.iter_mut().zip(self.values()) {
*destination = value;
}
}
}
}
#[derive(Debug, Clone)]
pub struct Table<F: Field> {
storage: TableStorage<F>,
}
impl<F: Field> Table<F> {
pub fn new(columns: RowMajorMatrix<F>) -> Self {
assert!(
columns.width.is_power_of_two(),
"table row width must be a power of two"
);
assert!(columns.height() > 0, "table must have at least one column");
Self {
storage: TableStorage::Dense(columns),
}
}
pub fn from_packed_bits(words: RowMajorMatrix<u64>, num_variables: usize) -> Self {
assert!(
num_variables < usize::BITS as usize,
"packed table height overflows usize"
);
let height = 1usize
.checked_shl(num_variables as u32)
.expect("packed table height overflows usize");
assert!(
words.width > 0,
"packed table must have at least one column"
);
let blocks = height
.checked_add(63)
.expect("packed table block count overflows usize")
/ 64;
let expected = words
.width
.checked_mul(blocks)
.expect("packed table word count overflows usize");
assert_eq!(
words.values.len(),
expected,
"packed table must contain exactly width * ceil(height / 64) words"
);
if !height.is_multiple_of(64) {
let used = height % 64;
let high_bits = !((1u64 << used) - 1);
let last = &words.values[(blocks - 1) * words.width..blocks * words.width];
assert!(
last.iter().all(|word| word & high_bits == 0),
"packed table padding bits must be zero"
);
}
Self {
storage: TableStorage::Boolean {
words,
num_variables,
},
}
}
pub fn zero(num_polys: usize, num_variables: usize) -> Self {
Self::new(RowMajorMatrix::new(
F::zero_vec(num_polys * (1 << num_variables)),
1 << num_variables,
))
}
pub fn rand<R: Rng>(rng: &mut R, num_polys: usize, num_variables: usize) -> Self
where
StandardUniform: Distribution<F>,
{
Self::new(RowMajorMatrix::rand(rng, num_polys, 1 << num_variables))
}
pub fn iter_polys(&self) -> impl DoubleEndedIterator<Item = &[F]> {
self.dense_matrix().row_slices()
}
pub fn par_iter_polys(&self) -> impl IndexedParallelIterator<Item = &[F]> {
self.dense_matrix().par_row_slices()
}
pub fn poly(&self, id: usize) -> PolyView<'_, F> {
let dense = self.dense_matrix();
let start = id * dense.width;
PolyView::new(&dense.values[start..start + dense.width])
}
pub fn column(&self, id: usize) -> ColumnView<'_, F> {
assert!(id < self.num_polys(), "table column out of bounds");
match &self.storage {
TableStorage::Dense(columns) => {
let start = id * columns.width;
ColumnView::Dense(&columns.values[start..start + columns.width])
}
TableStorage::Boolean {
words,
num_variables,
} => ColumnView::Boolean {
words,
column: id,
len: 1usize << num_variables,
},
}
}
pub fn columns(&self) -> impl DoubleEndedIterator<Item = ColumnView<'_, F>> {
(0..self.num_polys()).map(|column| self.column(column))
}
pub fn par_columns(&self) -> impl IndexedParallelIterator<Item = ColumnView<'_, F>> {
(0..self.num_polys())
.into_par_iter()
.map(|column| self.column(column))
}
pub const fn packed_bits(&self) -> Option<&RowMajorMatrix<u64>> {
match &self.storage {
TableStorage::Dense(_) => None,
TableStorage::Boolean { words, .. } => Some(words),
}
}
pub fn into_dense(self) -> Self {
match self.storage {
TableStorage::Dense(_) => self,
TableStorage::Boolean {
words,
num_variables,
} => {
let height = 1usize << num_variables;
let width = words.width;
let mut values = F::zero_vec(width * height);
for column in 0..width {
for row in 0..height {
values[column * height + row] = F::from_bool(
((words.values[(row / 64) * width + column] >> (row % 64)) & 1) != 0,
);
}
}
Self::new(RowMajorMatrix::new(values, height))
}
}
}
fn dense_matrix(&self) -> &RowMajorMatrix<F> {
match &self.storage {
TableStorage::Dense(columns) => columns,
TableStorage::Boolean { .. } => {
panic!("dense table access is unavailable for packed Boolean storage")
}
}
}
pub fn num_polys(&self) -> usize {
match &self.storage {
TableStorage::Dense(columns) => columns.height(),
TableStorage::Boolean { words, .. } => words.width,
}
}
pub const fn num_variables(&self) -> usize {
match &self.storage {
TableStorage::Dense(columns) => columns.width.ilog2() as usize,
TableStorage::Boolean { num_variables, .. } => *num_variables,
}
}
pub fn shape(&self) -> TableShape {
TableShape::new(self.num_variables(), self.num_polys())
}
fn pad_zeros(&mut self, num_variables: usize) {
let current_num_variables = self.num_variables();
if current_num_variables < num_variables {
match &mut self.storage {
TableStorage::Dense(columns) => columns
.widen_right((1 << num_variables) - (1 << current_num_variables), F::ZERO),
TableStorage::Boolean { .. } => {
panic!("packed tables must be converted to dense before padding")
}
}
}
}
}
pub trait SuffixTableSource<F: Field>: Sync {
fn shape(&self) -> TableShape;
fn fill(&self, columns: &mut [ColumnOut<'_, F>]);
}
impl<F: Field> SuffixTableSource<F> for Table<F> {
fn shape(&self) -> TableShape {
self.shape()
}
fn fill(&self, columns: &mut [ColumnOut<'_, F>]) {
columns
.par_iter_mut()
.zip(self.par_columns())
.for_each(|(out, source)| out.copy_from_view(source));
}
}
pub struct ColumnOut<'a, F> {
values: &'a mut [F],
written: bool,
}
impl<'a, F: Field> ColumnOut<'a, F> {
pub const fn len(&self) -> usize {
self.values.len()
}
pub const fn is_empty(&self) -> bool {
self.values.is_empty()
}
pub fn write_with(&mut self, writer: impl FnOnce(&mut [F])) {
writer(self.values);
self.written = true;
}
pub fn copy_from_slice(&mut self, source: &[F])
where
F: Copy,
{
self.write_with(|destination| destination.copy_from_slice(source));
}
pub fn copy_from_view(&mut self, source: ColumnView<'_, F>) {
self.write_with(|destination| source.copy_into(destination));
}
}
#[derive(Debug, Clone)]
pub struct SuffixLayoutPlan {
source_shapes: Vec<TableShape>,
committed_shapes: Vec<TableShape>,
placements: Vec<TablePlacement>,
num_variables: usize,
folding: usize,
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
#[non_exhaustive]
pub enum SuffixLayoutPlanError {
#[error("a suffix-layout witness requires at least one table")]
NoTables,
#[error("suffix-layout preprocessing depth {folding} overflows usize")]
FoldingOverflow {
folding: usize,
},
#[error("suffix-layout table cells overflow usize")]
CellCountOverflow,
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
#[non_exhaustive]
pub enum SuffixFillError {
#[error("suffix-layout fill received {actual} tables, expected {expected}")]
TableCount {
expected: usize,
actual: usize,
},
#[error("suffix-layout table {table} shape does not match its plan")]
ShapeMismatch {
table: usize,
expected: TableShape,
actual: TableShape,
},
#[error("suffix-layout table {table} did not write column {column}")]
UnwrittenColumn {
table: usize,
column: usize,
},
}
#[derive(Debug, Clone)]
pub struct FilledSuffixWitness<'a, F: Field> {
plan: &'a SuffixLayoutPlan,
poly: Poly<F>,
}
impl SuffixLayoutPlan {
pub fn new(
source_shapes: Vec<TableShape>,
folding: usize,
) -> Result<Self, SuffixLayoutPlanError> {
if source_shapes.is_empty() {
return Err(SuffixLayoutPlanError::NoTables);
}
if folding >= usize::BITS as usize {
return Err(SuffixLayoutPlanError::FoldingOverflow { folding });
}
let committed_shapes = source_shapes
.iter()
.map(|shape| TableShape::new(shape.num_variables().max(folding), shape.width()))
.collect::<Vec<_>>();
let total_cells = committed_shapes.iter().try_fold(0usize, |total, shape| {
let rows = 1usize
.checked_shl(shape.num_variables() as u32)
.ok_or(SuffixLayoutPlanError::CellCountOverflow)?;
let cells = rows
.checked_mul(shape.width())
.ok_or(SuffixLayoutPlanError::CellCountOverflow)?;
total
.checked_add(cells)
.ok_or(SuffixLayoutPlanError::CellCountOverflow)
})?;
total_cells
.checked_next_power_of_two()
.ok_or(SuffixLayoutPlanError::CellCountOverflow)?;
let (num_variables, placements) = plan_stacked_layout(&committed_shapes);
Ok(Self {
source_shapes,
committed_shapes,
placements,
num_variables,
folding,
})
}
pub fn source_shapes(&self) -> &[TableShape] {
&self.source_shapes
}
pub fn committed_shapes(&self) -> &[TableShape] {
&self.committed_shapes
}
pub fn placements(&self) -> &[TablePlacement] {
&self.placements
}
pub const fn num_variables(&self) -> usize {
self.num_variables
}
pub fn fill<'a, F: Field>(
&'a self,
sources: &[&dyn SuffixTableSource<F>],
) -> Result<FilledSuffixWitness<'a, F>, SuffixFillError> {
if sources.len() != self.source_shapes.len() {
return Err(SuffixFillError::TableCount {
expected: self.source_shapes.len(),
actual: sources.len(),
});
}
for (table, (source, expected)) in sources.iter().zip(&self.source_shapes).enumerate() {
let actual = source.shape();
if actual != *expected {
return Err(SuffixFillError::ShapeMismatch {
table,
expected: *expected,
actual,
});
}
}
let mut poly = Poly::<F>::zero(self.num_variables);
let slots = column_slots_for_shapes(
&self.placements,
&self.committed_shapes,
0,
poly.as_mut_slice(),
);
let mut ordered = slots;
ordered.sort_unstable_by_key(|&(_, table, column)| (table, column));
let mut outputs = ordered
.into_iter()
.map(|(slot, table, _)| {
let logical_len = 1usize << self.source_shapes[table].num_variables();
let (logical, _) = slot.split_at_mut(logical_len);
ColumnOut {
values: logical,
written: false,
}
})
.collect::<Vec<_>>();
let mut rest = outputs.as_mut_slice();
let mut table_outputs = Vec::with_capacity(self.source_shapes.len());
for shape in &self.source_shapes {
let (columns, tail) = rest.split_at_mut(shape.width());
table_outputs.push(columns);
rest = tail;
}
debug_assert!(rest.is_empty());
table_outputs
.into_par_iter()
.zip(sources.par_iter())
.for_each(|(columns, source)| source.fill(columns));
let mut first_column = 0;
for (table, shape) in self.source_shapes.iter().enumerate() {
let columns = &outputs[first_column..first_column + shape.width()];
if let Some(column) = columns.iter().position(|column| !column.written) {
return Err(SuffixFillError::UnwrittenColumn { table, column });
}
first_column += shape.width();
}
Ok(FilledSuffixWitness { plan: self, poly })
}
}
impl<F: Field> FilledSuffixWitness<'_, F> {
pub fn table_shapes(&self) -> &[TableShape] {
self.plan.committed_shapes()
}
pub fn placements(&self) -> &[TablePlacement] {
self.plan.placements()
}
pub fn num_variables(&self) -> usize {
self.poly.num_variables()
}
pub const fn poly(&self) -> &Poly<F> {
&self.poly
}
pub fn into_poly(self) -> Poly<F> {
self.poly
}
pub fn into_witness(self) -> Witness<F> {
let mut values_by_table = self
.plan
.committed_shapes
.iter()
.map(|shape| Vec::with_capacity((1usize << shape.num_variables()) * shape.width()))
.collect::<Vec<_>>();
for placement in &self.plan.placements {
let shape = self.plan.committed_shapes[placement.idx()];
let rows = 1usize << shape.num_variables();
for selector in placement.selectors() {
let offset = selector.index() << shape.num_variables();
values_by_table[placement.idx()]
.extend_from_slice(&self.poly.as_slice()[offset..offset + rows]);
}
}
let tables = values_by_table
.into_iter()
.zip(&self.plan.committed_shapes)
.map(|(values, shape)| {
Table::new(RowMajorMatrix::new(values, 1usize << shape.num_variables()))
})
.collect();
Witness {
tables,
placements: self.plan.placements.clone(),
num_variables: self.plan.num_variables,
folding: self.plan.folding,
poly: None,
}
}
}
fn column_slots_for_shapes<'a, T>(
placements: &[TablePlacement],
shapes: &[TableShape],
num_folded: usize,
out: &'a mut [T],
) -> Vec<(&'a mut [T], usize, usize)> {
let mut ranges = placements
.iter()
.flat_map(|placement| {
let num_variables_table = shapes[placement.idx()].num_variables();
assert!(num_folded <= num_variables_table);
let log_len = num_variables_table - num_folded;
placement
.selectors()
.iter()
.enumerate()
.map(move |(column, selector)| {
(
selector.index() << log_len,
1 << log_len,
placement.idx(),
column,
)
})
})
.collect::<Vec<_>>();
ranges.sort_unstable_by_key(|&(offset, ..)| offset);
let mut slots = Vec::with_capacity(ranges.len());
let mut rest = out;
let mut consumed = 0;
for (offset, len, table, column) in ranges {
let (_, tail) = core::mem::take(&mut rest).split_at_mut(offset - consumed);
let (slot, tail) = tail.split_at_mut(len);
slots.push((slot, table, column));
rest = tail;
consumed = offset + len;
}
slots
}
pub(crate) fn column_slots<'a, T, F: Field>(
placements: &[TablePlacement],
tables: &[Table<F>],
num_folded: usize,
out: &'a mut [T],
) -> Vec<(&'a mut [T], usize, usize)> {
let shapes = tables.iter().map(Table::shape).collect::<Vec<_>>();
column_slots_for_shapes(placements, &shapes, num_folded, out)
}
#[derive(Debug, Clone)]
pub struct TablePlacement {
pub(super) idx: usize,
pub(super) selectors: Vec<Selector>,
}
impl TablePlacement {
pub const fn new(idx: usize, selectors: Vec<Selector>) -> Self {
Self { idx, selectors }
}
pub fn reverse_selectors(&mut self) {
self.selectors.iter_mut().for_each(Selector::reverse);
}
pub const fn num_polys(&self) -> usize {
self.selectors.len()
}
pub const fn idx(&self) -> usize {
self.idx
}
pub fn selectors(&self) -> &[Selector] {
&self.selectors
}
}
#[derive(Debug, Clone)]
pub struct Witness<F: Field> {
pub(super) tables: Vec<Table<F>>,
pub(super) placements: Vec<TablePlacement>,
pub(super) num_variables: usize,
pub(super) folding: usize,
pub(super) poly: Option<Poly<F>>,
}
impl<F: Field> Witness<F> {
#[tracing::instrument(skip_all)]
pub fn new(mut tables: Vec<Table<F>>, folding: usize) -> Self {
assert!(
!tables.is_empty(),
"Witness requires at least one source table"
);
tables = tables.into_iter().map(Table::into_dense).collect();
tables.iter_mut().for_each(|table| table.pad_zeros(folding));
let shapes: Vec<LayoutShape> = tables
.iter()
.map(|t| LayoutShape {
arity: t.num_variables(),
width: t.num_polys(),
})
.collect();
let (num_variables, placements) = plan_layout(&shapes);
Self {
tables,
placements,
num_variables,
folding,
poly: None,
}
}
#[tracing::instrument(skip_all)]
pub fn new_interleaved(mut tables: Vec<Table<F>>, folding: usize) -> Self {
assert!(
!tables.is_empty(),
"Witness requires at least one source table"
);
tables = tables.into_iter().map(Table::into_dense).collect();
tables.iter_mut().for_each(|table| table.pad_zeros(folding));
let shapes: Vec<LayoutShape> = tables
.iter()
.map(|t| LayoutShape {
arity: t.num_variables(),
width: t.num_polys(),
})
.collect();
let (num_variables, mut placements) = plan_layout(&shapes);
placements
.iter_mut()
.for_each(TablePlacement::reverse_selectors);
let mut stacked = Poly::<F>::zero(num_variables);
for placement in &placements {
let table = &tables[placement.idx()];
for (poly_idx, selector) in placement.selectors().iter().enumerate() {
let poly = table.poly(poly_idx);
for (local_idx, &value) in poly.as_slice().iter().enumerate() {
let dst = (local_idx << selector.num_variables) | selector.index;
stacked.as_mut_slice()[dst] = value;
}
}
}
Self {
tables,
placements,
num_variables,
folding,
poly: Some(stacked),
}
}
pub(super) fn write_stacked_slots(&self, out: &mut [F]) {
assert_eq!(
out.len(),
1 << self.num_variables,
"stacked destination must cover the whole hypercube"
);
debug_assert!(
out.iter().all(|value| *value == F::ZERO),
"cells outside every slot are left untouched, so the destination must arrive zeroed"
);
info_span!("stack").in_scope(|| {
column_slots(&self.placements, &self.tables, 0, out)
.into_par_iter()
.for_each(|(slot, table_idx, poly_idx)| {
slot.par_chunks_mut(STACK_COPY_CHUNK)
.zip(
self.tables[table_idx]
.poly(poly_idx)
.as_slice()
.par_chunks(STACK_COPY_CHUNK),
)
.for_each(|(slot, column)| slot.copy_from_slice(column));
});
});
}
pub(super) const fn retained_poly(&self) -> &Poly<F> {
self.poly
.as_ref()
.expect("this layout retains its stacked polynomial")
}
pub fn stacked_poly(&self) -> Poly<F> {
self.poly.clone().unwrap_or_else(|| {
let mut stacked = Poly::<F>::zero(self.num_variables);
self.write_stacked_slots(stacked.as_mut_slice());
stacked
})
}
pub const fn num_variables(&self) -> usize {
self.num_variables
}
pub fn table_shapes(&self) -> Vec<TableShape> {
self.tables.iter().map(Table::shape).collect()
}
pub(super) fn into_parts(self) -> WitnessParts<F> {
WitnessParts {
tables: self.tables,
placements: self.placements,
num_variables: self.num_variables,
folding: self.folding,
poly: self.poly,
}
}
}
#[derive(Debug, Clone)]
pub(super) struct WitnessParts<F: Field> {
pub(super) tables: Vec<Table<F>>,
pub(super) placements: Vec<TablePlacement>,
pub(super) num_variables: usize,
pub(super) folding: usize,
pub(super) poly: Option<Poly<F>>,
}
#[cfg(test)]
mod tests {
use alloc::vec;
use core::sync::atomic::{AtomicUsize, Ordering};
use p3_baby_bear::BabyBear;
use p3_field::PrimeCharacteristicRing;
use p3_field::extension::BinomialExtensionField;
use p3_util::log2_ceil_usize;
use proptest::prelude::*;
use rand::SeedableRng;
use rand::rngs::SmallRng;
use super::*;
use crate::layout::{Layout, PrefixProver, SuffixProver};
type F = BabyBear;
type EF = BinomialExtensionField<F, 4>;
struct ChunkedSource {
shape: TableShape,
columns: Vec<Vec<Vec<F>>>,
}
impl SuffixTableSource<F> for ChunkedSource {
fn shape(&self) -> TableShape {
self.shape
}
fn fill(&self, columns: &mut [ColumnOut<'_, F>]) {
for (out, chunks) in columns.iter_mut().zip(&self.columns) {
out.write_with(|destination| {
let mut offset = 0;
for chunk in chunks {
let end = offset + chunk.len();
destination[offset..end].copy_from_slice(chunk);
offset = end;
}
assert_eq!(offset, destination.len());
});
}
}
}
struct BitPackedSource {
shape: TableShape,
columns: Vec<Vec<u64>>,
}
impl SuffixTableSource<F> for BitPackedSource {
fn shape(&self) -> TableShape {
self.shape
}
fn fill(&self, columns: &mut [ColumnOut<'_, F>]) {
for (out, words) in columns.iter_mut().zip(&self.columns) {
out.write_with(|destination| {
for (row, value) in destination.iter_mut().enumerate() {
let bit = (words[row / 64] >> (row % 64)) & 1;
*value = F::from_u64(bit);
}
});
}
}
}
struct ShortSource {
shape: TableShape,
observed_len: AtomicUsize,
}
impl SuffixTableSource<F> for ShortSource {
fn shape(&self) -> TableShape {
self.shape
}
fn fill(&self, columns: &mut [ColumnOut<'_, F>]) {
self.observed_len.store(columns[0].len(), Ordering::Relaxed);
columns[0].write_with(|destination| destination.fill(F::ONE));
}
}
struct IncompleteSource {
shape: TableShape,
}
impl SuffixTableSource<F> for IncompleteSource {
fn shape(&self) -> TableShape {
self.shape
}
fn fill(&self, columns: &mut [ColumnOut<'_, F>]) {
columns[0].write_with(|destination| destination.fill(F::ONE));
}
}
fn placement_addresses(placements: &[TablePlacement]) -> Vec<(usize, Vec<(usize, usize)>)> {
placements
.iter()
.map(|placement| {
(
placement.idx(),
placement
.selectors()
.iter()
.map(|selector| (selector.num_variables(), selector.index()))
.collect(),
)
})
.collect()
}
fn assert_direct_fill_matches_dense(tables: &[Table<F>], folding: usize) {
let legacy = Witness::new(tables.to_vec(), folding);
let plan = SuffixLayoutPlan::new(tables.iter().map(Table::shape).collect(), folding)
.expect("the fixture dimensions fit a suffix layout");
let sources = tables
.iter()
.map(|table| table as &dyn SuffixTableSource<F>)
.collect::<Vec<_>>();
let direct = plan.fill(&sources).expect("every dense column is written");
assert_eq!(direct.poly().as_slice(), legacy.stacked_poly().as_slice());
assert_eq!(direct.table_shapes(), legacy.table_shapes());
assert_eq!(
placement_addresses(direct.placements()),
placement_addresses(&legacy.placements)
);
let compatible = direct.into_witness();
assert_eq!(
compatible.stacked_poly().as_slice(),
legacy.stacked_poly().as_slice()
);
assert_eq!(compatible.table_shapes(), legacy.table_shapes());
assert_eq!(compatible.tables.len(), legacy.tables.len());
for (compatible_table, legacy_table) in compatible.tables.iter().zip(&legacy.tables) {
for (compatible_poly, legacy_poly) in
compatible_table.iter_polys().zip(legacy_table.iter_polys())
{
assert_eq!(compatible_poly, legacy_poly);
}
}
}
#[test]
fn selector_new_stores_num_variables_and_index() {
let sel = Selector::new(3, 5);
assert_eq!(sel.num_variables(), 3);
assert_eq!(sel.index(), 5);
}
#[test]
#[should_panic]
fn selector_new_panics_on_index_out_of_range() {
let _ = Selector::new(2, 4);
}
#[test]
fn selector_point_returns_boolean_vector_of_right_length() {
let sel = Selector::new(3, 5);
let point: Point<F> = sel.point();
assert_eq!(point.num_variables(), 3);
for &bit in point.as_slice() {
assert!(bit == F::ZERO || bit == F::ONE);
}
}
#[test]
fn selector_lift_prefixes_selector_bits_onto_other() {
let sel = Selector::new(3, 5);
let other: Point<EF> = Point::new(vec![EF::from_u64(7), EF::from_u64(11)]);
let lifted = sel.lift_prefix(&other);
assert_eq!(lifted.num_variables(), 5);
for i in 0..2 {
assert_eq!(lifted.as_slice()[3 + i], other.as_slice()[i]);
}
}
#[test]
fn selector_lift_suffix_appends_selector_bits_after_other() {
let sel = Selector::new(3, 5);
let other: Point<EF> = Point::new(vec![EF::from_u64(7), EF::from_u64(11)]);
let lifted = sel.lift_suffix(&other);
assert_eq!(lifted.num_variables(), 5);
assert_eq!(lifted.as_slice()[0], other.as_slice()[0]);
assert_eq!(lifted.as_slice()[1], other.as_slice()[1]);
let selector_bits: Point<EF> = sel.point();
assert_eq!(lifted.as_slice()[2], selector_bits.as_slice()[0]);
assert_eq!(lifted.as_slice()[3], selector_bits.as_slice()[1]);
assert_eq!(lifted.as_slice()[4], selector_bits.as_slice()[2]);
}
#[test]
fn selector_reverse_swaps_msb_and_lsb_within_width() {
let mut sel = Selector::new(4, 0b0010);
sel.reverse();
assert_eq!(sel.num_variables(), 4);
assert_eq!(sel.index(), 0b0100);
}
#[test]
fn selector_reverse_is_idempotent_under_double_application() {
let mut sel = Selector::new(5, 13);
let original_index = sel.index();
sel.reverse();
sel.reverse();
assert_eq!(sel.num_variables(), 5);
assert_eq!(sel.index(), original_index);
}
#[test]
#[should_panic(expected = "power of two")]
fn table_new_panics_on_non_power_of_two_width() {
let _ = Table::new(RowMajorMatrix::new(F::zero_vec(3), 3));
}
#[test]
#[should_panic(expected = "at least one column")]
fn table_new_panics_on_zero_columns() {
let _ = Table::new(RowMajorMatrix::new(Vec::<F>::new(), 2));
}
#[test]
fn table_accessors_report_shape() {
let table = Table::<F>::zero(2, 3);
assert_eq!(table.num_polys(), 2);
assert_eq!(table.num_variables(), 3);
assert_eq!(table.poly(0).num_variables(), 3);
}
#[test]
fn packed_table_column_views_preserve_row_major_bit_order() {
let words = RowMajorMatrix::new(vec![0b1010_0101u64, 0b1100_0011u64], 2);
let table = Table::<F>::from_packed_bits(words, 3);
assert_eq!(table.num_polys(), 2);
assert_eq!(table.num_variables(), 3);
assert!(table.packed_bits().is_some());
assert_eq!(table.column(0).len(), 8);
assert_eq!(table.column(1).len(), 8);
assert_eq!(table.column(0).boolean_word(0), Some(0b1010_0101));
assert_eq!(table.column(1).boolean_word(0), Some(0b1100_0011));
assert_eq!(
table.column(0).values().collect::<Vec<_>>(),
(0..8)
.map(|row| F::from_bool(((0b1010_0101u64 >> row) & 1) != 0))
.collect::<Vec<_>>()
);
assert_eq!(
table
.columns()
.map(|column| column.values().collect::<Vec<_>>())
.collect::<Vec<_>>(),
vec![
table.column(0).values().collect::<Vec<_>>(),
table.column(1).values().collect::<Vec<_>>(),
]
);
}
#[test]
fn packed_table_explicit_dense_conversion_matches_boolean_columns() {
let table = Table::<F>::from_packed_bits(
RowMajorMatrix::new(vec![0b0000_1101u64, 0b0000_1011u64], 2),
3,
);
let dense = table.clone().into_dense();
assert!(dense.packed_bits().is_none());
for column in 0..2 {
assert_eq!(
dense.column(column).values().collect::<Vec<_>>(),
table.column(column).values().collect::<Vec<_>>()
);
}
}
#[test]
fn packed_table_geometry_and_suffix_fill_cover_word_boundaries() {
for num_variables in [0, 1, 5, 6, 7] {
let height = 1usize << num_variables;
let width = 3;
let words = (0..height.div_ceil(64))
.flat_map(|block| {
(0..width).map(move |column| {
(0..64).fold(0u64, |word, lane| {
let row = block * 64 + lane;
if row < height && (row + column * 3) % 5 < 2 {
word | (1u64 << lane)
} else {
word
}
})
})
})
.collect::<Vec<_>>();
let packed =
Table::<F>::from_packed_bits(RowMajorMatrix::new(words, width), num_variables);
for column in 0..width {
for row in 0..height {
let expected = if (row + column * 3) % 5 < 2 {
F::ONE
} else {
F::ZERO
};
assert_eq!(packed.column(column).value(row), expected);
}
}
let dense = packed.clone().into_dense();
let plan = SuffixLayoutPlan::new(vec![packed.shape()], 0).unwrap();
let direct = plan.fill(&[&packed]).unwrap();
let legacy = Witness::new(vec![dense], 0);
assert_eq!(direct.poly().as_slice(), legacy.stacked_poly().as_slice());
assert_eq!(direct.table_shapes(), legacy.table_shapes());
}
}
#[test]
#[should_panic(expected = "padding bits")]
fn packed_table_rejects_nonzero_padding_bits() {
let _ = Table::<F>::from_packed_bits(RowMajorMatrix::new(vec![0b1000_0001u64], 1), 2);
}
#[test]
#[should_panic(expected = "exactly")]
fn packed_table_rejects_wrong_physical_matrix_size() {
let _ = Table::<F>::from_packed_bits(RowMajorMatrix::new(vec![0u64], 1), 7);
}
#[test]
#[should_panic(expected = "at least one column")]
fn packed_table_rejects_zero_columns() {
let _ = Table::<F>::from_packed_bits(RowMajorMatrix::new(Vec::new(), 0), 0);
}
#[test]
#[should_panic(expected = "height overflows")]
fn packed_table_rejects_height_overflow() {
let _ =
Table::<F>::from_packed_bits(RowMajorMatrix::new(vec![0u64], 1), usize::BITS as usize);
}
#[test]
fn table_placement_accessors_return_stored_values() {
let selectors = vec![Selector::new(2, 0), Selector::new(2, 1)];
let placement = TablePlacement::new(7, selectors);
assert_eq!(placement.idx(), 7);
assert_eq!(placement.num_polys(), 2);
assert_eq!(placement.selectors().len(), 2);
}
#[test]
fn table_placement_reverse_selectors_flips_each_in_place() {
let selectors = vec![
Selector::new(3, 0b001),
Selector::new(3, 0b010),
Selector::new(3, 0b110),
];
let mut placement = TablePlacement::new(4, selectors);
placement.reverse_selectors();
assert_eq!(placement.idx(), 4);
for selector in placement.selectors() {
assert_eq!(selector.num_variables(), 3);
}
let indices: Vec<usize> = placement.selectors().iter().map(Selector::index).collect();
assert_eq!(indices, vec![0b100, 0b010, 0b011]);
}
#[test]
fn table_shape_reports_arity_and_width() {
let table = Table::<F>::zero(3, 4);
let shape = table.shape();
assert_eq!(shape.num_variables(), 4);
assert_eq!(shape.width(), 3);
}
fn fixture_witness() -> Witness<F> {
let mut rng = SmallRng::seed_from_u64(1);
let t0 = Table::rand(&mut rng, 2, 3);
let t1 = Table::rand(&mut rng, 2, 4);
Witness::new(vec![t0, t1], 1)
}
fn fixture_interleaved_witness() -> Witness<F> {
let mut rng = SmallRng::seed_from_u64(1);
let t0 = Table::rand(&mut rng, 2, 3);
let t1 = Table::rand(&mut rng, 2, 4);
Witness::new_interleaved(vec![t0, t1], 1)
}
#[test]
fn witness_new_places_largest_table_first() {
let w = fixture_witness();
assert_eq!(w.placements[0].idx(), 1);
assert_eq!(w.placements[1].idx(), 0);
}
#[test]
fn witness_num_variables_is_stacked_arity_rounded_up() {
let w = fixture_witness();
assert_eq!(w.num_variables(), 6);
}
#[test]
fn witness_stacks_column_evals_into_slots() {
let w = fixture_witness();
let stacked = w.stacked_poly();
for placement in &w.placements {
let table = &w.tables[placement.idx()];
for (poly_idx, selector) in placement.selectors().iter().enumerate() {
let col = table.poly(poly_idx);
let dst = selector.index() << col.num_variables();
let slot = &stacked.as_slice()[dst..dst + col.num_evals()];
assert_eq!(slot, col.as_slice());
}
}
}
#[test]
#[cfg(debug_assertions)]
#[should_panic = "the destination must arrive zeroed"]
fn stacking_rejects_a_destination_that_is_not_zeroed() {
let w = fixture_witness();
let mut out = vec![F::ONE; 1 << w.num_variables()];
w.write_stacked_slots(&mut out);
}
#[test]
fn witness_new_interleaves_by_suffix_selectors() {
let a0 = F::from_u64(10);
let a1 = F::from_u64(11);
let a2 = F::from_u64(12);
let a3 = F::from_u64(13);
let b0 = F::from_u64(20);
let b1 = F::from_u64(21);
let table_a = Table::new(RowMajorMatrix::new(vec![a0, a1, a2, a3], 4));
let table_b = Table::new(RowMajorMatrix::new(vec![b0, b1], 2));
let witness = Witness::new_interleaved(vec![table_a, table_b], 0);
assert_eq!(witness.num_variables(), 3);
assert_eq!(
witness.stacked_poly().as_slice(),
&[a0, b0, a1, F::ZERO, a2, b1, a3, F::ZERO],
);
}
#[test]
fn witness_new_pads_tables_below_folding() {
let a0 = F::from_u64(10);
let a1 = F::from_u64(11);
let table = Table::new(RowMajorMatrix::new(vec![a0, a1], 2));
let witness = Witness::new(vec![table], 3);
assert_eq!(witness.tables[0].num_variables(), 3);
assert_eq!(witness.num_variables(), 3);
assert_eq!(
witness.tables[0].poly(0).as_slice(),
&[a0, a1, F::ZERO, F::ZERO, F::ZERO, F::ZERO, F::ZERO, F::ZERO],
);
assert_eq!(
witness.stacked_poly().as_slice(),
&[a0, a1, F::ZERO, F::ZERO, F::ZERO, F::ZERO, F::ZERO, F::ZERO],
);
}
#[test]
fn witness_table_shapes_returns_shapes_in_source_order() {
let w = fixture_witness();
let shapes = w.table_shapes();
assert_eq!(shapes, vec![TableShape::new(3, 2), TableShape::new(4, 2)],);
}
#[test]
fn witness_stacked_poly_returns_the_stacked_polynomial() {
let w = fixture_witness();
let stacked = w.stacked_poly();
assert_eq!(stacked.num_variables(), w.num_variables());
assert_eq!(stacked.as_slice().len(), 1 << w.num_variables());
assert!(stacked.as_slice()[48..].iter().all(|&v| v == F::ZERO));
}
#[test]
fn witness_into_parts_preserves_every_field() {
let w = fixture_witness();
let expected_num_variables = w.num_variables();
let expected_folding = w.folding;
let expected_table_shapes = w.table_shapes();
let expected_placement_idx: Vec<usize> =
w.placements.iter().map(TablePlacement::idx).collect();
let expected_poly = w.poly.clone();
let parts = w.into_parts();
assert_eq!(parts.num_variables, expected_num_variables);
assert_eq!(parts.folding, expected_folding);
let actual_table_shapes: Vec<TableShape> = parts.tables.iter().map(Table::shape).collect();
assert_eq!(actual_table_shapes, expected_table_shapes);
let actual_placement_idx: Vec<usize> =
parts.placements.iter().map(TablePlacement::idx).collect();
assert_eq!(actual_placement_idx, expected_placement_idx);
assert_eq!(
parts.poly.map(|poly| poly.as_slice().to_vec()),
expected_poly.map(|poly| poly.as_slice().to_vec())
);
}
#[test]
fn prefix_prover_from_witness_carries_stacked_state() {
let w = fixture_interleaved_witness();
let stacked_copy = w.stacked_poly();
let num_variables = w.num_variables();
let prover = PrefixProver::<F, EF>::from_witness(w);
assert_eq!(prover.num_variables(), num_variables);
assert_eq!(prover.poly.as_slice(), stacked_copy.as_slice());
}
#[test]
#[should_panic = "this layout retains its stacked polynomial"]
fn prefix_prover_refuses_a_contiguously_stacked_witness() {
let _ = PrefixProver::<F, EF>::from_witness(fixture_witness());
}
#[test]
fn suffix_prover_from_witness_carries_stacked_state() {
let w = fixture_witness();
let num_variables = w.num_variables();
let table_shapes = w.table_shapes();
let prover = SuffixProver::<F, EF>::from_witness(w);
assert_eq!(prover.num_variables(), num_variables);
let prover_shapes: Vec<TableShape> =
prover.claims.tables.iter().map(Table::shape).collect();
assert_eq!(prover_shapes, table_shapes);
}
#[test]
fn direct_dense_fill_matches_the_existing_suffix_witness() {
let mut rng = SmallRng::seed_from_u64(0xD3E5E);
let tables = vec![Table::rand(&mut rng, 3, 8), Table::rand(&mut rng, 2, 8)];
assert_direct_fill_matches_dense(&tables, 0);
}
#[test]
fn direct_jagged_fill_matches_the_existing_suffix_witness() {
let mut rng = SmallRng::seed_from_u64(0x1A663D);
let tables = vec![
Table::rand(&mut rng, 3, 4),
Table::rand(&mut rng, 1, 9),
Table::rand(&mut rng, 2, 6),
];
assert_direct_fill_matches_dense(&tables, 0);
}
#[test]
fn fragmented_columns_fill_the_same_final_stack_as_dense_columns() {
let first = (0..32).map(F::from_usize).collect::<Vec<_>>();
let second = (100..132).map(F::from_usize).collect::<Vec<_>>();
let dense = Table::new(RowMajorMatrix::new(
first.iter().chain(&second).copied().collect(),
32,
));
let source = ChunkedSource {
shape: TableShape::new(5, 2),
columns: vec![
vec![
first[..3].to_vec(),
first[3..19].to_vec(),
first[19..].to_vec(),
],
vec![second[..17].to_vec(), second[17..].to_vec()],
],
};
let plan = SuffixLayoutPlan::new(vec![source.shape], 0).unwrap();
let direct = plan.fill(&[&source]).unwrap();
let legacy = Witness::new(vec![dense], 0);
assert_eq!(direct.poly().as_slice(), legacy.stacked_poly().as_slice());
}
#[test]
fn packed_bits_decode_directly_into_the_dense_reference_layout() {
let words = vec![
vec![0x0123_4567_89AB_CDEF, 0xF0F0_0F0F_AAAA_5555],
vec![0xDEAD_BEEF_CAFE_BABE, 0x8000_0000_0000_0001],
];
let scalar = words
.iter()
.flat_map(|column| {
(0..128).map(move |row| F::from_u64((column[row / 64] >> (row % 64)) & 1))
})
.collect::<Vec<_>>();
let dense = Table::new(RowMajorMatrix::new(scalar, 128));
let source = BitPackedSource {
shape: TableShape::new(7, 2),
columns: words,
};
let plan = SuffixLayoutPlan::new(vec![source.shape], 0).unwrap();
let direct = plan.fill(&[&source]).unwrap();
let legacy = Witness::new(vec![dense], 0);
assert_eq!(direct.poly().as_slice(), legacy.stacked_poly().as_slice());
}
#[test]
fn heterogeneous_sources_share_one_checked_fill() {
let dense = Table::new(RowMajorMatrix::new((0..4).map(F::from_usize).collect(), 2));
let chunked_values = (20..52).map(F::from_usize).collect::<Vec<_>>();
let chunked = ChunkedSource {
shape: TableShape::new(5, 1),
columns: vec![vec![
chunked_values[..7].to_vec(),
chunked_values[7..].to_vec(),
]],
};
let packed_words = vec![vec![0xA5], vec![0x3C], vec![0x81]];
let packed = BitPackedSource {
shape: TableShape::new(3, 3),
columns: packed_words.clone(),
};
let chunked_table = Table::new(RowMajorMatrix::new(chunked_values, 32));
let packed_table = Table::new(RowMajorMatrix::new(
packed_words
.iter()
.flat_map(|words| {
(0..8).map(|row| F::from_u64((words[row / 64] >> (row % 64)) & 1))
})
.collect(),
8,
));
let tables = vec![dense.clone(), chunked_table, packed_table];
let plan = SuffixLayoutPlan::new(tables.iter().map(Table::shape).collect(), 3).unwrap();
let direct = plan.fill(&[&dense, &chunked, &packed]).unwrap();
let legacy = Witness::new(tables, 3);
assert_eq!(direct.poly().as_slice(), legacy.stacked_poly().as_slice());
let compatible = direct.into_witness();
assert_eq!(
compatible.stacked_poly().as_slice(),
legacy.stacked_poly().as_slice()
);
assert_eq!(compatible.table_shapes(), legacy.table_shapes());
}
#[test]
fn preprocessing_padding_is_zero_and_hidden_from_the_producer() {
let source = ShortSource {
shape: TableShape::new(2, 1),
observed_len: AtomicUsize::new(0),
};
let plan = SuffixLayoutPlan::new(vec![source.shape], 4).unwrap();
let direct = plan.fill(&[&source]).unwrap();
assert_eq!(source.observed_len.load(Ordering::Relaxed), 4);
assert_eq!(direct.table_shapes(), &[TableShape::new(4, 1)]);
assert_eq!(&direct.poly().as_slice()[..4], &[F::ONE; 4]);
assert!(
direct.poly().as_slice()[4..]
.iter()
.all(|&value| value == F::ZERO)
);
}
#[test]
fn an_unwritten_column_is_rejected_after_safe_zero_initialization() {
let source = IncompleteSource {
shape: TableShape::new(3, 2),
};
let plan = SuffixLayoutPlan::new(vec![source.shape], 0).unwrap();
let error = plan
.fill(&[&source])
.expect_err("the second column has no completed write");
assert_eq!(
error,
SuffixFillError::UnwrittenColumn {
table: 0,
column: 1,
}
);
}
#[test]
fn source_shape_drift_is_rejected_before_filling() {
let source = IncompleteSource {
shape: TableShape::new(3, 2),
};
let plan = SuffixLayoutPlan::new(vec![TableShape::new(3, 1)], 0).unwrap();
let error = plan
.fill(&[&source])
.expect_err("the source width differs from the planned width");
assert_eq!(
error,
SuffixFillError::ShapeMismatch {
table: 0,
expected: TableShape::new(3, 1),
actual: source.shape,
}
);
}
#[test]
fn an_empty_plan_is_rejected() {
let error = SuffixLayoutPlan::new(Vec::new(), 0)
.expect_err("an empty table batch has no committed statement");
assert_eq!(error, SuffixLayoutPlanError::NoTables);
}
#[test]
fn a_rounded_stack_larger_than_the_address_space_is_rejected() {
let shape = TableShape::new(usize::BITS as usize - 1, 2);
let error = SuffixLayoutPlan::new(vec![shape], 0)
.expect_err("the two column cubes overflow the address space");
assert_eq!(error, SuffixLayoutPlanError::CellCountOverflow);
}
#[test]
fn a_source_count_mismatch_is_rejected_before_allocation() {
let plan = SuffixLayoutPlan::new(vec![TableShape::new(3, 1)], 0).unwrap();
let error = plan
.fill::<F>(&[])
.expect_err("the planned table has no source");
assert_eq!(
error,
SuffixFillError::TableCount {
expected: 1,
actual: 0,
}
);
}
fn arb_table_shapes() -> impl Strategy<Value = Vec<(usize, usize)>> {
prop::collection::vec((2usize..=5, 1usize..=3), 1..=3)
}
proptest! {
#![proptest_config(ProptestConfig { cases: 32, ..ProptestConfig::default() })]
#[test]
fn witness_stacks_columns_and_zeros_the_tail(shapes in arb_table_shapes()) {
let mut rng = SmallRng::seed_from_u64(123);
let tables: Vec<Table<F>> = shapes
.iter()
.map(|&(arity, width)| {
Table::rand(&mut rng, width, arity)
})
.collect();
let total_used: usize = tables
.iter()
.map(|t| (1 << t.num_variables()) * t.num_polys())
.sum();
let witness = Witness::new(tables, 1);
assert_eq!(witness.num_variables(), log2_ceil_usize(total_used));
let stacked = witness.stacked_poly();
let mut used = 0usize;
for placement in &witness.placements {
let table = &witness.tables[placement.idx()];
for (poly_idx, selector) in placement.selectors().iter().enumerate() {
let col = table.poly(poly_idx);
let dst = selector.index() << col.num_variables();
let slot = &stacked.as_slice()[dst..dst + col.num_evals()];
assert_eq!(slot, col.as_slice());
used += col.num_evals();
}
}
assert_eq!(used, total_used);
let stacked_len = 1usize << witness.num_variables();
for &v in &stacked.as_slice()[used..stacked_len] {
assert_eq!(v, F::ZERO);
}
}
#[test]
fn direct_fill_matches_dense_with_normalization(
shapes in arb_table_shapes(),
folding in 0usize..=6,
) {
let mut rng = SmallRng::seed_from_u64(0xD1EC7);
let tables = shapes
.iter()
.map(|&(arity, width)| Table::rand(&mut rng, width, arity))
.collect::<Vec<_>>();
assert_direct_fill_matches_dense(&tables, folding);
}
}
}