use alloc::vec::Vec;
use core::ops::Deref;
use p3_field::Field;
use p3_maybe_rayon::prelude::*;
use super::error::JaggedIngestError;
use super::layout::JaggedLayout;
const WORD_BITS: usize = 64;
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum ConversionPass {
Shared,
Copied,
Gathered,
Widened,
}
#[derive(Clone, Copy, Debug)]
pub enum ColumnSource<'a, F> {
Dense(&'a [F]),
Interleaved {
cells: &'a [F],
first: usize,
stride: usize,
height: usize,
},
Bits {
words: &'a [u64],
height: usize,
},
Chunked(&'a [Self]),
}
impl<F: Field> ColumnSource<'_, F> {
#[must_use]
pub fn height(&self) -> usize {
match self {
Self::Dense(cells) => cells.len(),
Self::Interleaved { height, .. } | Self::Bits { height, .. } => *height,
Self::Chunked(parts) => parts.iter().map(Self::height).sum(),
}
}
#[must_use]
pub fn heights(columns: &[Self]) -> Vec<usize> {
columns.iter().map(Self::height).collect()
}
#[must_use]
pub fn pass(&self) -> ConversionPass {
match self {
Self::Dense(_) => ConversionPass::Copied,
Self::Interleaved { .. } => ConversionPass::Gathered,
Self::Bits { .. } => ConversionPass::Widened,
Self::Chunked(parts) => parts
.iter()
.map(Self::pass)
.max()
.unwrap_or(ConversionPass::Copied),
}
}
fn validate(&self, column: usize) -> Result<(), JaggedIngestError> {
match self {
Self::Dense(_) => Ok(()),
Self::Interleaved {
cells,
first,
stride,
height,
} => {
if *height == 0 {
return Ok(());
}
if *stride == 0 {
return Err(JaggedIngestError::ZeroStride { column });
}
let last = first
.checked_add((height - 1).saturating_mul(*stride))
.ok_or(JaggedIngestError::StrideOutOfRange {
column,
required: usize::MAX,
available: cells.len(),
})?;
if last >= cells.len() {
return Err(JaggedIngestError::StrideOutOfRange {
column,
required: last + 1,
available: cells.len(),
});
}
Ok(())
}
Self::Bits { words, height } => {
let required = height.div_ceil(WORD_BITS);
if words.len() < required {
return Err(JaggedIngestError::PackedWordsTooShort {
column,
required,
available: words.len(),
});
}
Ok(())
}
Self::Chunked(parts) => parts.iter().try_for_each(|part| part.validate(column)),
}
}
fn write_into(&self, out: &mut [F]) {
match self {
Self::Dense(cells) => out.copy_from_slice(cells),
Self::Interleaved {
cells,
first,
stride,
..
} => {
for (row, slot) in out.iter_mut().enumerate() {
*slot = cells[first + row * stride];
}
}
Self::Bits { words, .. } => {
for (row, slot) in out.iter_mut().enumerate() {
let bit = (words[row / WORD_BITS] >> (row % WORD_BITS)) & 1;
*slot = F::from_bool(bit == 1);
}
}
Self::Chunked(parts) => {
let mut rest = out;
for part in *parts {
let (head, tail) = rest.split_at_mut(part.height());
part.write_into(head);
rest = tail;
}
}
}
}
}
#[derive(Clone, Copy, Debug)]
pub enum TraceSource<'a, F> {
Committed(&'a [F]),
Columns(&'a [ColumnSource<'a, F>]),
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct IngestReport {
shared: usize,
copied: usize,
gathered: usize,
widened: usize,
envelope: usize,
}
impl IngestReport {
const fn charge(&mut self, pass: ConversionPass, cells: usize) {
let counter = match pass {
ConversionPass::Shared => &mut self.shared,
ConversionPass::Copied => &mut self.copied,
ConversionPass::Gathered => &mut self.gathered,
ConversionPass::Widened => &mut self.widened,
};
*counter += cells;
}
#[must_use]
pub const fn charged(&self, pass: ConversionPass) -> usize {
match pass {
ConversionPass::Shared => self.shared,
ConversionPass::Copied => self.copied,
ConversionPass::Gathered => self.gathered,
ConversionPass::Widened => self.widened,
}
}
#[must_use]
pub const fn live(&self) -> usize {
self.shared + self.copied + self.gathered + self.widened
}
#[must_use]
pub const fn converted(&self) -> usize {
self.copied + self.gathered + self.widened
}
#[must_use]
pub const fn envelope(&self) -> usize {
self.envelope
}
#[must_use]
pub const fn reads_no_cell(&self) -> bool {
self.converted() == 0 && self.envelope == 0
}
}
#[derive(Clone, Debug)]
pub enum JaggedWitness<'a, F> {
Shared(&'a [F]),
Assembled(Vec<F>),
}
impl<'a, F: Field> JaggedWitness<'a, F> {
pub fn read(
layout: &JaggedLayout,
source: TraceSource<'a, F>,
) -> Result<(Self, IngestReport), JaggedIngestError> {
let capacity = layout.dense_capacity();
let mut report = IngestReport::default();
let columns = match source {
TraceSource::Committed(cells) => {
if cells.len() < capacity {
return Err(JaggedIngestError::CommittedVectorTooShort {
required: capacity,
available: cells.len(),
});
}
report.charge(ConversionPass::Shared, layout.area());
return Ok((Self::Shared(&cells[..capacity]), report));
}
TraceSource::Columns(columns) => columns,
};
if columns.len() != layout.num_columns() {
return Err(JaggedIngestError::ColumnCountMismatch {
expected: layout.num_columns(),
actual: columns.len(),
});
}
for (index, column) in columns.iter().enumerate() {
let declared = layout.column_height(index);
let actual = column.height();
if actual != declared {
return Err(JaggedIngestError::ColumnHeightMismatch {
column: index,
expected: declared,
actual,
});
}
column.validate(index)?;
report.charge(column.pass(), actual);
}
report.envelope = capacity - layout.area();
let mut witness = F::zero_vec(capacity);
let mut slots = Vec::with_capacity(columns.len());
let mut rest = &mut witness[..layout.area()];
for column in columns {
let (head, tail) = rest.split_at_mut(column.height());
slots.push(head);
rest = tail;
}
slots
.into_par_iter()
.zip(columns.par_iter())
.for_each(|(slot, column)| column.write_into(slot));
Ok((Self::Assembled(witness), report))
}
}
impl<F> Deref for JaggedWitness<'_, F> {
type Target = [F];
fn deref(&self) -> &[F] {
match self {
Self::Shared(cells) => cells,
Self::Assembled(cells) => cells,
}
}
}
#[cfg(test)]
mod tests {
use alloc::vec;
use p3_field::PrimeCharacteristicRing;
use proptest::prelude::*;
use super::*;
use crate::tests::F;
fn expected_cells(source: &ColumnSource<'_, F>) -> Vec<F> {
match source {
ColumnSource::Dense(cells) => cells.to_vec(),
ColumnSource::Interleaved {
cells,
first,
stride,
height,
} => (0..*height)
.map(|row| cells[first + row * stride])
.collect(),
ColumnSource::Bits { words, height } => (0..*height)
.map(|row| {
let word = words[row / 64];
F::from_bool(word & (1 << (row % 64)) != 0)
})
.collect(),
ColumnSource::Chunked(parts) => parts.iter().flat_map(expected_cells).collect(),
}
}
fn cells(values: &[u64]) -> Vec<F> {
values.iter().map(|&value| F::from_u64(value)).collect()
}
#[test]
fn a_producer_that_already_owns_the_committed_vector_pays_nothing() {
let layout = JaggedLayout::new(3, &[3, 0, 5, 1]).unwrap();
let committed = cells(&(1..=16).collect::<Vec<_>>());
let (witness, report) =
JaggedWitness::read(&layout, TraceSource::Committed(&committed)).unwrap();
assert!(matches!(witness, JaggedWitness::Shared(_)));
assert_eq!(&*witness, &committed[..]);
assert!(report.reads_no_cell());
assert_eq!(report.live(), 9);
assert_eq!(report.charged(ConversionPass::Shared), 9);
assert_eq!(report.converted(), 0);
assert_eq!(report.envelope(), 0);
let long = cells(&(1..=32).collect::<Vec<_>>());
let (witness, _) = JaggedWitness::read(&layout, TraceSource::Committed(&long)).unwrap();
assert_eq!(witness.len(), 16);
}
#[test]
fn every_arrival_shape_lands_on_the_same_committed_vector() {
let layout = JaggedLayout::new(3, &[3, 0, 2, 4]).unwrap();
let expected = cells(&[1, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0]);
let first = cells(&[1, 0, 1]);
let third = cells(&[1, 1]);
let fourth = cells(&[0, 1, 1, 0]);
let dense = [
ColumnSource::Dense(&first),
ColumnSource::Dense(&[]),
ColumnSource::Dense(&third),
ColumnSource::Dense(&fourth),
];
let (witness, report) = JaggedWitness::read(&layout, TraceSource::Columns(&dense)).unwrap();
assert_eq!(&*witness, &expected[..]);
assert_eq!(report.charged(ConversionPass::Copied), 9);
assert_eq!(report.envelope(), 7);
assert!(!report.reads_no_cell());
let block = cells(&[1, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0]);
let interleaved = [0usize, 1, 2, 3].map(|column| ColumnSource::Interleaved {
cells: &block,
first: column,
stride: 4,
height: layout.column_height(column),
});
let (witness, report) =
JaggedWitness::read(&layout, TraceSource::Columns(&interleaved)).unwrap();
assert_eq!(&*witness, &expected[..]);
assert_eq!(report.charged(ConversionPass::Gathered), 9);
let words = [0b101u64, 0, 0b11, 0b0110];
let packed: [ColumnSource<'_, F>; 4] = [0usize, 1, 2, 3].map(|column| ColumnSource::Bits {
words: &words[column..=column],
height: layout.column_height(column),
});
let (witness, report) =
JaggedWitness::read(&layout, TraceSource::Columns(&packed)).unwrap();
assert_eq!(&*witness, &expected[..]);
assert_eq!(report.charged(ConversionPass::Widened), 9);
let head = cells(&[1, 0]);
let tail = cells(&[1]);
let parts = [ColumnSource::Dense(&head), ColumnSource::Dense(&tail)];
let chunked = [
ColumnSource::Chunked(&parts),
ColumnSource::Dense(&[]),
ColumnSource::Dense(&third),
ColumnSource::Dense(&fourth),
];
let (witness, _) = JaggedWitness::read(&layout, TraceSource::Columns(&chunked)).unwrap();
assert_eq!(&*witness, &expected[..]);
}
#[test]
fn a_packed_column_widens_its_bits_least_significant_first() {
let layout = JaggedLayout::new(4, &[9, 0]).unwrap();
let words = [0b1_1000_1101u64];
let sources: [ColumnSource<'_, F>; 2] = [
ColumnSource::Bits {
words: &words,
height: 9,
},
ColumnSource::Dense(&[]),
];
let (witness, report) =
JaggedWitness::read(&layout, TraceSource::Columns(&sources)).unwrap();
assert_eq!(
witness[..9],
cells(&[1, 0, 1, 1, 0, 0, 0, 1, 1])[..],
"bits read from the least significant end of the word"
);
assert_eq!(report.charged(ConversionPass::Widened), 9);
assert_eq!(report.envelope(), 7);
}
#[test]
fn the_declared_shape_names_the_pass_a_producer_pays_for() {
let bits = [0u64];
let dense = cells(&[1, 2]);
let parts = [
ColumnSource::Dense(&dense),
ColumnSource::Bits {
words: &bits,
height: 1,
},
];
assert_eq!(ColumnSource::Dense(&dense).pass(), ConversionPass::Copied);
assert_eq!(
ColumnSource::<F>::Bits {
words: &bits,
height: 1
}
.pass(),
ConversionPass::Widened
);
assert_eq!(
ColumnSource::Chunked(&parts).pass(),
ConversionPass::Widened
);
assert_eq!(ColumnSource::Chunked(&parts).height(), 3);
}
#[test]
fn a_trace_the_geometry_cannot_read_is_refused() {
let layout = JaggedLayout::new(3, &[3, 0, 2, 4]).unwrap();
let three = cells(&[1, 2, 3]);
let short = [ColumnSource::Dense(&three)];
assert_eq!(
JaggedWitness::read(&layout, TraceSource::Columns(&short)).err(),
Some(JaggedIngestError::ColumnCountMismatch {
expected: 4,
actual: 1
})
);
let wrong = [
ColumnSource::Dense(&three),
ColumnSource::Dense(&[]),
ColumnSource::Dense(&three),
ColumnSource::Dense(&three),
];
assert_eq!(
JaggedWitness::read(&layout, TraceSource::Columns(&wrong)).err(),
Some(JaggedIngestError::ColumnHeightMismatch {
column: 2,
expected: 2,
actual: 3
})
);
let block = cells(&[1, 2, 3, 4, 5, 6]);
let over = [
ColumnSource::Dense(&three),
ColumnSource::Dense(&[]),
ColumnSource::Dense(&cells(&[1, 2])),
ColumnSource::Interleaved {
cells: &block,
first: 1,
stride: 2,
height: 4,
},
];
assert_eq!(
JaggedWitness::read(&layout, TraceSource::Columns(&over)).err(),
Some(JaggedIngestError::StrideOutOfRange {
column: 3,
required: 8,
available: 6
})
);
let stalled = [
ColumnSource::Dense(&three),
ColumnSource::Dense(&[]),
ColumnSource::Dense(&cells(&[1, 2])),
ColumnSource::Interleaved {
cells: &block,
first: 0,
stride: 0,
height: 4,
},
];
assert_eq!(
JaggedWitness::read(&layout, TraceSource::Columns(&stalled)).err(),
Some(JaggedIngestError::ZeroStride { column: 3 })
);
let words = [0u64];
let packed = [
ColumnSource::Dense(&three),
ColumnSource::Dense(&[]),
ColumnSource::Bits {
words: &[],
height: 2,
},
ColumnSource::Bits {
words: &words,
height: 4,
},
];
assert_eq!(
JaggedWitness::read(&layout, TraceSource::Columns(&packed)).err(),
Some(JaggedIngestError::PackedWordsTooShort {
column: 2,
required: 1,
available: 0
})
);
let committed = cells(&[1, 2, 3]);
assert_eq!(
JaggedWitness::read(&layout, TraceSource::Committed(&committed)).err(),
Some(JaggedIngestError::CommittedVectorTooShort {
required: 16,
available: 3
})
);
}
#[test]
fn a_geometry_reads_its_heights_off_the_sources() {
let first = cells(&[1, 2, 3]);
let words = [0u64];
let sources = [
ColumnSource::Dense(&first),
ColumnSource::Bits {
words: &words,
height: 5,
},
];
assert_eq!(ColumnSource::heights(&sources), vec![3, 5]);
let layout = JaggedLayout::new(3, &ColumnSource::heights(&sources)).unwrap();
assert_eq!(layout.column_height(0), 3);
assert_eq!(layout.column_height(1), 5);
assert_eq!(layout.area(), 8);
}
proptest! {
#[test]
fn the_live_prefix_is_the_concatenation_of_the_columns(
heights in prop::collection::vec(0usize..=9, 4),
shapes in prop::collection::vec(0usize..3, 4),
values in prop::collection::vec(any::<u64>(), 64),
) {
let layout = JaggedLayout::new(4, &heights).unwrap();
let block = cells(&values);
let words = values;
let sources = heights
.iter()
.enumerate()
.map(|(column, &height)| match shapes[column] {
0 => ColumnSource::Dense(&block[..height]),
1 => ColumnSource::Interleaved {
cells: &block,
first: column,
stride: 4,
height,
},
_ => ColumnSource::Bits {
words: &words,
height,
},
})
.collect::<Vec<_>>();
let (witness, report) = JaggedWitness::read(&layout, TraceSource::Columns(&sources)).unwrap();
let expected = sources.iter().flat_map(expected_cells).collect::<Vec<_>>();
prop_assert_eq!(witness.len(), layout.dense_capacity());
prop_assert_eq!(&witness[..layout.area()], &expected[..]);
prop_assert!(witness[layout.area()..].iter().all(|cell| *cell == F::ZERO));
prop_assert_eq!(report.live(), layout.area());
prop_assert_eq!(report.converted(), layout.area());
prop_assert_eq!(report.envelope(), layout.dense_capacity() - layout.area());
}
}
}