Skip to main content

binius_core/constraint_system/
value_vec.rs

1// Copyright 2025 Irreducible Inc.
2use std::ops::{Deref, DerefMut, Index, IndexMut};
3
4use bytemuck::{Pod, Zeroable};
5
6use super::{ShiftedValueIndex, ValueIndex, ValueVecLayout};
7use crate::{error::ConstraintSystemError, word::Word};
8
9/// A 16-byte-aligned pair of words, the storage block of the aligned word buffer.
10///
11/// - A word is 8 bytes, so a plain vector of words only lands on a 16-byte boundary half the time.
12/// - Two words inside a 16-byte-aligned block force every allocation onto that boundary.
13#[derive(Clone, Copy, Debug)]
14#[repr(C, align(16))]
15struct WordPair([Word; 2]);
16
17// SAFETY: the two impls below assert that any bit pattern is a valid `WordPair`.
18// - Each word is plain-old-data, so every field is plain-old-data.
19// - Two 8-byte words exactly fill the 16-byte size, leaving no padding bytes.
20unsafe impl Zeroable for WordPair {}
21unsafe impl Pod for WordPair {}
22
23/// A heap-allocated buffer of words whose first element is 16-byte aligned.
24///
25/// The value vector is copied in bulk on the prover's hot path:
26/// - cloned wholesale,
27/// - packed into field elements,
28/// - sliced out into owned vectors.
29///
30/// A 16-byte-aligned start keeps each of those copies on the aligned SIMD `memcpy` path.
31/// An 8-byte-aligned plain vector would instead pay a misalignment prologue half the time.
32///
33/// Storage groups two words per block, so capacity rounds up to an even count.
34/// The valid word count is tracked separately from the block count.
35/// An odd count leaves the last block's second word as zeroed, unused padding.
36#[derive(Clone, Debug)]
37struct AlignedWords {
38	/// Backing store of 16-byte-aligned blocks, one block per two words.
39	blocks: Vec<WordPair>,
40	/// Number of valid words, at most twice the block count.
41	len: usize,
42}
43
44impl AlignedWords {
45	/// Allocates a 16-byte-aligned buffer of `len` zeroed words.
46	fn zeroed(len: usize) -> Self {
47		Self {
48			// Round up to whole blocks; the macro zero-fills, so every word starts at zero.
49			blocks: vec![WordPair([Word::ZERO; 2]); len.div_ceil(2)],
50			len,
51		}
52	}
53}
54
55impl Deref for AlignedWords {
56	type Target = [Word];
57
58	fn deref(&self) -> &[Word] {
59		// Reinterpret the aligned blocks as twice as many words.
60		// Slice to the valid count, dropping the padding word of an odd buffer.
61		&bytemuck::cast_slice(&self.blocks)[..self.len]
62	}
63}
64
65impl DerefMut for AlignedWords {
66	fn deref_mut(&mut self) -> &mut [Word] {
67		// Same reinterpretation as the shared view, but handing out mutable words.
68		&mut bytemuck::cast_slice_mut(&mut self.blocks)[..self.len]
69	}
70}
71
72/// The vector of values used in constraint evaluation and proof generation.
73///
74/// `ValueVec` is the concrete instantiation of values that satisfy (or should satisfy) a
75/// [`ConstraintSystem`](super::ConstraintSystem). It follows the layout defined by
76/// [`ValueVecLayout`] and serves as the primary data structure for both constraint evaluation and
77/// polynomial commitment.
78///
79/// Between these sections, there may be padding regions to satisfy alignment requirements.
80///
81/// The words live in a buffer that starts on a 16-byte boundary.
82/// That keeps the frequent bulk copies of the vector on the aligned SIMD `memcpy` path.
83#[derive(Clone, Debug)]
84pub struct ValueVec {
85	/// Section offsets and counts that partition the words below.
86	layout: ValueVecLayout,
87	/// The committed words followed by the scratch tail, 16-byte aligned.
88	data: AlignedWords,
89}
90
91impl ValueVec {
92	/// Creates a new value vector with the given layout.
93	///
94	/// The values are filled with zeros.
95	pub fn new(layout: ValueVecLayout) -> ValueVec {
96		let size = layout.committed_total_len + layout.n_scratch;
97		ValueVec {
98			layout,
99			data: AlignedWords::zeroed(size),
100		}
101	}
102
103	/// Creates a new value vector with the given layout and data.
104	///
105	/// The data is checked to have the correct length.
106	pub fn new_from_data(
107		layout: ValueVecLayout,
108		public: Vec<Word>,
109		private: Vec<Word>,
110	) -> Result<ValueVec, ConstraintSystemError> {
111		let committed_len = public.len() + private.len();
112		if committed_len != layout.committed_total_len {
113			return Err(ConstraintSystemError::ValueVecLenMismatch {
114				expected: layout.committed_total_len,
115				actual: committed_len,
116			});
117		}
118
119		// Full buffer = committed words + scratch tail.
120		let full_len = layout.committed_total_len + layout.n_scratch;
121		// Fresh 16-byte-aligned buffer; the scratch tail past the committed words stays zeroed.
122		let mut data = AlignedWords::zeroed(full_len);
123		// Public words occupy the front of the committed region.
124		data[..public.len()].copy_from_slice(&public);
125		// Private words follow, filling the rest of the committed region.
126		data[public.len()..committed_len].copy_from_slice(&private);
127
128		Ok(ValueVec { layout, data })
129	}
130
131	/// The total size of the committed portion of the vector (excluding scratch).
132	pub const fn size(&self) -> usize {
133		self.layout.committed_total_len
134	}
135
136	/// Returns the value stored at the given index.
137	///
138	/// Panics if the index is out of bounds. Will happily return a value from the padding section.
139	pub fn get(&self, index: usize) -> Word {
140		self.data[index]
141	}
142
143	/// Sets the value at the given index.
144	///
145	/// Panics if the index is out of bounds. Will gladly assign a value to the padding section.
146	pub fn set(&mut self, index: usize, value: Word) {
147		self.data[index] = value;
148	}
149
150	/// Returns the public portion of the values vector.
151	pub fn public(&self) -> &[Word] {
152		&self.data[..self.layout.offset_witness]
153	}
154
155	/// Return all non-public values (witness + internal) without scratch space.
156	pub fn non_public(&self) -> &[Word] {
157		&self.data[self.layout.offset_witness..self.layout.committed_total_len]
158	}
159
160	/// Returns the witness portion of the values vector.
161	pub fn witness(&self) -> &[Word] {
162		let start = self.layout.offset_witness;
163		let end = start + self.layout.n_witness;
164		&self.data[start..end]
165	}
166
167	/// Returns the combined values vector.
168	pub fn combined_witness(&self) -> &[Word] {
169		let start = 0;
170		let end = self.layout.committed_total_len;
171		&self.data[start..end]
172	}
173
174	/// Evaluates an operand against this witness.
175	///
176	/// An operand is the XOR of its shifted-value terms.
177	/// An empty operand evaluates to the zero word, the XOR identity.
178	#[inline]
179	pub fn eval_operand(&self, operand: &[ShiftedValueIndex]) -> Word {
180		// Fold each shifted term into the running XOR, starting from the identity.
181		operand
182			.iter()
183			.fold(Word::ZERO, |acc, term| acc ^ term.eval(self))
184	}
185}
186
187impl Index<ValueIndex> for ValueVec {
188	type Output = Word;
189
190	fn index(&self, index: ValueIndex) -> &Self::Output {
191		&self.data[index.0 as usize]
192	}
193}
194
195impl IndexMut<ValueIndex> for ValueVec {
196	fn index_mut(&mut self, index: ValueIndex) -> &mut Self::Output {
197		&mut self.data[index.0 as usize]
198	}
199}
200
201#[cfg(test)]
202mod tests {
203	use proptest::{collection, prelude::any, prop_assert_eq, proptest};
204
205	use super::*;
206
207	#[test]
208	fn split_values_vec_and_combine() {
209		let values = ValueVec::new(ValueVecLayout {
210			n_const: 2,
211			n_inout: 2,
212			n_witness: 2,
213			n_internal: 2,
214			offset_inout: 2,
215			offset_witness: 4,
216			committed_total_len: 8,
217			n_scratch: 0,
218		});
219
220		let public = values.public();
221		let non_public = values.non_public();
222		let combined =
223			ValueVec::new_from_data(values.layout.clone(), public.to_vec(), non_public.to_vec())
224				.unwrap();
225		assert_eq!(combined.combined_witness(), values.combined_witness());
226	}
227
228	// The property that makes the optimization work: the first word sits on a 16-byte boundary.
229	fn assert_16_byte_aligned(words: &[Word]) {
230		assert_eq!(words.as_ptr() as usize % 16, 0);
231	}
232
233	#[test]
234	fn zeroed_is_aligned_zero_filled_and_correct_length() {
235		// Cases:
236		//   0      -> empty buffer, no blocks
237		//   1, 3   -> odd, so the last block's second word is padding
238		//   2, 16  -> even, every block fully used
239		//   17     -> odd and spans many blocks
240		for len in [0, 1, 2, 3, 16, 17] {
241			let words = AlignedWords::zeroed(len);
242			// The view reports the requested word count, not the rounded-up block capacity.
243			assert_eq!(words.len(), len);
244			// Alignment must hold for every length, including the empty buffer.
245			assert_16_byte_aligned(&words);
246			// A freshly allocated buffer is entirely zero.
247			assert!(words.iter().all(|&w| w == Word::ZERO));
248		}
249	}
250
251	#[test]
252	fn deref_mut_writes_are_visible_through_deref() {
253		// Length 5 is odd, so the last block holds one valid word and one padding word.
254		let mut words = AlignedWords::zeroed(5);
255		// Write 1..=5 through the mutable view; this must not touch the padding word.
256		for (i, w) in words.iter_mut().enumerate() {
257			*w = Word::from_u64(i as u64 + 1);
258		}
259		// The shared view reads back exactly the five words written.
260		assert_eq!(
261			&*words,
262			&[
263				Word::from_u64(1),
264				Word::from_u64(2),
265				Word::from_u64(3),
266				Word::from_u64(4),
267				Word::from_u64(5),
268			]
269		);
270	}
271
272	proptest! {
273		#[test]
274		fn value_vec_preserves_words_and_alignment(
275			public in collection::vec(any::<u64>(), 4..32usize),
276			n_witness in 0..32usize,
277			n_scratch in 0..16usize,
278		) {
279			// Public words come straight from the strategy; private words use a recognizable pattern.
280			let public: Vec<Word> = public.into_iter().map(Word).collect();
281			let private: Vec<Word> = (0..n_witness).map(|i| Word::from_u64(0xdead_0000 + i as u64)).collect();
282
283			// The public section is padded to a power of two.
284			// The witness section follows it, then the scratch tail.
285			//
286			//     [0, offset_witness)                    -> public  (power of two)
287			//     [offset_witness, committed_total_len)  -> witness
288			//     [committed_total_len, +n_scratch)      -> scratch
289			let offset_witness = public.len().next_power_of_two();
290			let committed_total_len = offset_witness + private.len();
291			let layout = ValueVecLayout {
292				n_const: 0,
293				n_inout: public.len(),
294				n_witness: private.len(),
295				n_internal: 0,
296				offset_inout: 0,
297				offset_witness,
298				committed_total_len,
299				n_scratch,
300			};
301
302			// The public input must fill its whole power-of-two section, so zero-pad it.
303			let mut public_padded = public;
304			public_padded.resize(offset_witness, Word::ZERO);
305
306			let vv = ValueVec::new_from_data(layout, public_padded.clone(), private.clone()).unwrap();
307
308			// Alignment survives construction for any word count.
309			assert_16_byte_aligned(vv.combined_witness());
310			// Both sections read back byte-for-byte what went in.
311			prop_assert_eq!(vv.public(), &public_padded[..]);
312			prop_assert_eq!(vv.witness(), &private[..]);
313
314			// The scratch tail past the committed words is zeroed and addressable.
315			for i in committed_total_len..committed_total_len + n_scratch {
316				prop_assert_eq!(vv.get(i), Word::ZERO);
317			}
318		}
319	}
320}