use pil2_std_lib::Std;
use proofman_fields::PrimeField64;
use rayon::prelude::*;
use std::sync::Arc;
const Q_HSC_LEN: usize = 1 << 22;
const CHUNK_LEN: usize = 1 << 16;
const CARRY_LEN: usize = 1 << 23;
const LT_LEN: usize = (1 << 18) + (1 << 18) + (1 << 17);
const CARRY_BIAS: i64 = (1 << 22) - 1;
const Q_HSC_BASE: usize = 0;
const CHUNK_BASE: usize = Q_HSC_BASE + Q_HSC_LEN;
const CARRY_BASE: usize = CHUNK_BASE + CHUNK_LEN;
const LT_BASE: usize = CARRY_BASE + CARRY_LEN;
const CACHE_LEN: usize = LT_BASE + LT_LEN;
pub const CACHE_BYTES: usize = CACHE_LEN * std::mem::size_of::<u32>();
pub struct MultiplicityCache {
counts: Vec<u32>,
}
impl Default for MultiplicityCache {
fn default() -> Self {
Self::new()
}
}
impl MultiplicityCache {
pub fn new() -> Self {
Self { counts: vec![0u32; CACHE_LEN] }
}
#[inline(always)]
pub fn q_hsc(&mut self, value: i64) -> i64 {
debug_assert!(
(0..Q_HSC_LEN as i64).contains(&value),
"q_hsc value {value} outside [0, 2^22)"
);
self.counts[Q_HSC_BASE + value as usize] += 1;
value
}
#[inline(always)]
pub fn chunk(&mut self, value: i64) -> i64 {
debug_assert!(
(0..CHUNK_LEN as i64).contains(&value),
"chunk value {value} outside [0, 2^16)"
);
self.counts[CHUNK_BASE + value as usize] += 1;
value
}
#[inline(always)]
pub fn carry(&mut self, value: i64) -> i64 {
debug_assert!(
(-CARRY_BIAS..=CARRY_BIAS + 1).contains(&value),
"carry value {value} outside [-(2^22-1), 2^22]"
);
self.counts[CARRY_BASE + (value + CARRY_BIAS) as usize] += 1;
value
}
#[inline(always)]
pub fn lt_row(&mut self, row: usize) {
debug_assert!(row < LT_LEN, "lt row {row} outside the table's {LT_LEN} rows");
self.counts[LT_BASE + row] += 1;
}
#[inline(always)]
pub fn q_column(&mut self, value: i64, last_clock: bool) -> i64 {
if last_clock {
self.q_hsc(value)
} else {
self.chunk(value)
}
}
pub fn add(&mut self, other: &MultiplicityCache) {
self.counts
.par_iter_mut()
.zip(other.counts.par_iter())
.for_each(|(dst, src)| *dst = dst.saturating_add(*src));
}
pub fn flush<F: PrimeField64>(
&self,
std: &Arc<Std<F>>,
q_hsc_range_id: usize,
chunk_range_id: usize,
carry_range_id: usize,
lt_table_id: usize,
) {
std.range_check_ranged(q_hsc_range_id, None, &self.counts[Q_HSC_BASE..CHUNK_BASE]);
std.range_check_ranged(chunk_range_id, None, &self.counts[CHUNK_BASE..CARRY_BASE]);
std.range_check_ranged(carry_range_id, None, &self.counts[CARRY_BASE..LT_BASE]);
std.inc_virtual_rows_ranged(lt_table_id, None, &self.counts[LT_BASE..]);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_layout_matches_the_registered_ranges() {
assert_eq!(Q_HSC_LEN, (1 << 22), "q_hsc is [0, 2^22 - 1]");
assert_eq!(CHUNK_LEN, 0xFFFF + 1, "chunk is [0, 0xFFFF]");
assert_eq!(CARRY_LEN, ((1 << 22) + CARRY_BIAS + 1) as usize, "carry is [-(2^22-1), 2^22]");
assert_eq!(
LT_LEN,
(1 << 18) + (1 << 18) + (1 << 17),
"ARITH_EQ_LT_TABLE_SIZE in arith_eq_lt_table.pil"
);
assert_eq!(LT_LEN, 0xA0000, "and the bound calculate_table_row enforces");
assert_eq!(CACHE_BYTES, 53_215_232, "50.75 MiB per batch: 48.25 of ranges + 2.5 of LT");
}
#[test]
fn lt_rows_count_into_their_own_region() {
let mut cache = MultiplicityCache::new();
cache.lt_row(0);
cache.lt_row(LT_LEN - 1);
assert_eq!(cache.counts[LT_BASE], 1, "row 0 is the region's first bucket");
assert_eq!(cache.counts[CACHE_LEN - 1], 1, "the last row is the last bucket");
assert_eq!(cache.counts[LT_BASE - 1], 0, "the carry range above it is untouched");
assert_eq!(cache.counts.iter().filter(|&&c| c != 0).count(), 2);
}
#[test]
fn each_range_counts_into_its_own_region() {
let mut cache = MultiplicityCache::new();
cache.q_hsc(0);
cache.q_hsc(Q_HSC_LEN as i64 - 1);
cache.chunk(0);
cache.chunk(0xFFFF);
cache.carry(-CARRY_BIAS);
cache.carry(0);
cache.carry(CARRY_BIAS + 1);
assert_eq!(cache.counts[Q_HSC_BASE], 1);
assert_eq!(cache.counts[CHUNK_BASE - 1], 1);
assert_eq!(cache.counts[CHUNK_BASE], 1);
assert_eq!(cache.counts[CARRY_BASE - 1], 1);
assert_eq!(cache.counts[CARRY_BASE], 1, "the lowest carry is the region's first bucket");
assert_eq!(cache.counts[CARRY_BASE + CARRY_BIAS as usize], 1, "carry 0 sits at the bias");
assert_eq!(cache.counts[LT_BASE - 1], 1, "the highest carry closes the carry region");
assert_eq!(cache.counts.iter().filter(|&&c| c != 0).count(), 7, "no bucket counted twice");
}
#[test]
fn q_column_picks_the_range_by_clock() {
let mut cache = MultiplicityCache::new();
cache.q_column(5, true);
cache.q_column(5, false);
assert_eq!(cache.counts[Q_HSC_BASE + 5], 1, "last clock counts into q_hsc");
assert_eq!(cache.counts[CHUNK_BASE + 5], 1, "the others count into chunk");
}
#[test]
fn add_sums_the_buckets() {
let mut a = MultiplicityCache::new();
let mut b = MultiplicityCache::new();
a.chunk(7);
a.chunk(7);
b.chunk(7);
b.carry(-1);
a.add(&b);
assert_eq!(a.counts[CHUNK_BASE + 7], 3);
assert_eq!(a.counts[CARRY_BASE + (CARRY_BIAS - 1) as usize], 1);
}
#[test]
fn add_saturates_instead_of_wrapping() {
let mut a = MultiplicityCache::new();
let mut b = MultiplicityCache::new();
a.counts[CHUNK_BASE] = u32::MAX - 1;
b.counts[CHUNK_BASE] = 5;
a.add(&b);
assert_eq!(a.counts[CHUNK_BASE], u32::MAX);
}
}