Skip to main content

binius_core/constraint_system/
value_vec.rs

1// Copyright 2025 Irreducible Inc.
2// Copyright 2026 The Binius Developers
3use std::ops::{Deref, DerefMut, Index, IndexMut};
4
5use bytemuck::{Pod, Zeroable};
6
7use super::{ShiftedValueIndex, ValueIndex, ValueSegment, ValueVecLayout};
8use crate::word::Word;
9
10/// A 16-byte-aligned pair of words, the storage block of the aligned word buffer.
11///
12/// - A word is 8 bytes, so a plain vector of words only lands on a 16-byte boundary half the time.
13/// - Two words inside a 16-byte-aligned block force every allocation onto that boundary.
14#[derive(Clone, Copy, Debug)]
15#[repr(C, align(16))]
16struct WordPair([Word; 2]);
17
18// SAFETY: the two impls below assert that any bit pattern is a valid `WordPair`.
19// - Each word is plain-old-data, so every field is plain-old-data.
20// - Two 8-byte words exactly fill the 16-byte size, leaving no padding bytes.
21unsafe impl Zeroable for WordPair {}
22unsafe impl Pod for WordPair {}
23
24/// A heap-allocated buffer of words whose first element is 16-byte aligned.
25///
26/// The value vector is copied in bulk on the prover's hot path:
27/// - cloned wholesale,
28/// - packed into field elements,
29/// - sliced out into owned vectors.
30///
31/// A 16-byte-aligned start keeps each of those copies on the aligned SIMD `memcpy` path.
32/// An 8-byte-aligned plain vector would instead pay a misalignment prologue half the time.
33///
34/// Storage groups two words per block, so capacity rounds up to an even count.
35/// The valid word count is tracked separately from the block count.
36/// An odd count leaves the last block's second word as zeroed, unused padding.
37#[derive(Clone, Debug)]
38struct AlignedWords {
39	/// Backing store of 16-byte-aligned blocks, one block per two words.
40	blocks: Vec<WordPair>,
41	/// Number of valid words, at most twice the block count.
42	len: usize,
43}
44
45impl AlignedWords {
46	/// Allocates a 16-byte-aligned buffer of `len` zeroed words.
47	fn zeroed(len: usize) -> Self {
48		Self {
49			// Round up to whole blocks; the macro zero-fills, so every word starts at zero.
50			blocks: vec![WordPair([Word::ZERO; 2]); len.div_ceil(2)],
51			len,
52		}
53	}
54}
55
56impl Deref for AlignedWords {
57	type Target = [Word];
58
59	fn deref(&self) -> &[Word] {
60		// Reinterpret the aligned blocks as twice as many words.
61		// Slice to the valid count, dropping the padding word of an odd buffer.
62		&bytemuck::cast_slice(&self.blocks)[..self.len]
63	}
64}
65
66impl DerefMut for AlignedWords {
67	fn deref_mut(&mut self) -> &mut [Word] {
68		// Same reinterpretation as the shared view, but handing out mutable words.
69		&mut bytemuck::cast_slice_mut(&mut self.blocks)[..self.len]
70	}
71}
72
73/// The vector of values used in constraint evaluation and proof generation.
74///
75/// `ValueVec` is the concrete instantiation of values that satisfy (or should satisfy) a
76/// [`ConstraintSystem`](super::ConstraintSystem). It is the primary data structure for both
77/// constraint evaluation and polynomial commitment.
78///
79/// The words are the public values followed by the private ones, stored back to back with no
80/// padding. A vector built with [`Self::new`] carries the circuit's scratch tail past them; one
81/// built with [`Self::new_from_data`] ends with the private values.
82///
83/// The words live in a buffer that starts on a 16-byte boundary.
84/// That keeps the frequent bulk copies of the vector on the aligned SIMD `memcpy` path.
85///
86/// # Addressing
87///
88/// A [`ValueIndex`] names a word by its segment and its position within that segment, and the
89/// vector stores the two counts that place each segment in the buffer.
90///
91/// These are *storage* positions, and they are deliberately not the positions the proving
92/// protocol reads a word at: the protocol pads the public segment to a power of two and the
93/// hidden segment to at least that width, so it addresses the same word further along. Its
94/// addresses come from
95/// [`ConstraintSystem::word_offset`](super::ConstraintSystem::word_offset) instead. Nothing needs
96/// both, because the prover pads each segment as it packs it into field elements.
97#[derive(Clone, Debug)]
98pub struct ValueVec {
99	/// The number of constants, which is where the inout values start.
100	n_const: usize,
101	/// The number of public values, which is where the private ones start.
102	n_public_values: usize,
103	/// The number of private values.
104	n_private: usize,
105	/// The values followed by the scratch tail, 16-byte aligned.
106	data: AlignedWords,
107}
108
109impl ValueVec {
110	/// Creates a zero-filled value vector holding the sections of the given circuit layout,
111	/// including its scratch tail.
112	pub fn new(layout: &ValueVecLayout) -> ValueVec {
113		ValueVec {
114			n_const: layout.n_const,
115			n_public_values: layout.offset_witness(),
116			n_private: layout.n_private(),
117			data: AlignedWords::zeroed(layout.combined_len() + layout.n_scratch),
118		}
119	}
120
121	/// Creates a value vector from the words of its public and private values.
122	///
123	/// The vector has no scratch tail; scratch words only exist while a circuit is evaluated.
124	///
125	/// `n_const` splits the public words into the constants and the inout values, which is what
126	/// resolves an [`InOut`](super::ValueSegment::InOut) index. Rebuilding a vector from
127	/// serialized segments therefore needs the system describing them, so prefer
128	/// [`ConstraintSystem::value_vec_from_data`](super::ConstraintSystem::value_vec_from_data),
129	/// which passes it for you.
130	pub fn new_from_data(n_const: usize, public: &[Word], private: &[Word]) -> ValueVec {
131		// Fresh 16-byte-aligned buffer holding the public words followed by the private ones.
132		let mut data = AlignedWords::zeroed(public.len() + private.len());
133		data[..public.len()].copy_from_slice(public);
134		data[public.len()..].copy_from_slice(private);
135
136		ValueVec {
137			n_const,
138			n_public_values: public.len(),
139			n_private: private.len(),
140			data,
141		}
142	}
143
144	/// Returns one word by its flat position, counting the scratch tail.
145	///
146	/// This is the view for the few readers that address whole segments rather than named values:
147	/// the evaluation form, whose bytecode holds one register per position, and the batch witness,
148	/// which copies a segment across including its padding. Everything else names words by
149	/// [`ValueIndex`], which cannot reach a padding word.
150	#[inline]
151	pub fn word(&self, offset: u32) -> Word {
152		self.data[offset as usize]
153	}
154
155	/// Returns a mutable reference to one word by its flat position, counting the scratch tail.
156	///
157	/// This is the mutable counterpart of [`Self::word`], which documents when to reach for it.
158	#[inline]
159	pub fn word_mut(&mut self, offset: u32) -> &mut Word {
160		&mut self.data[offset as usize]
161	}
162
163	/// The flat position of the word a [`ValueIndex`] names.
164	///
165	/// A vector built by [`Self::new_from_data`] has no scratch tail, so a scratch index lands
166	/// past the last word and panics rather than reading a committed one.
167	#[inline]
168	const fn word_offset(&self, index: ValueIndex) -> usize {
169		let segment_start = match index.segment() {
170			ValueSegment::Constant => 0,
171			ValueSegment::InOut => self.n_const,
172			ValueSegment::Private => self.n_public_values,
173			ValueSegment::Scratch => self.size(),
174		};
175		segment_start + index.index() as usize
176	}
177
178	/// The number of values the vector holds, excluding scratch.
179	pub const fn size(&self) -> usize {
180		self.n_public_values + self.n_private
181	}
182
183	/// Returns the public values: the constants followed by the inout values.
184	///
185	/// These are the words as the circuit declares them, unpadded. The prover pads them up to the
186	/// public segment width as it packs them.
187	pub fn public(&self) -> &[Word] {
188		&self.data[..self.n_public_values]
189	}
190
191	/// Returns the inout values: the public values past the constants.
192	pub fn inout(&self) -> &[Word] {
193		&self.data[self.n_const..self.n_public_values]
194	}
195
196	/// Returns the private values, unpadded and without scratch space.
197	pub fn non_public(&self) -> &[Word] {
198		&self.data[self.n_public_values..self.size()]
199	}
200
201	/// Returns the combined values vector.
202	pub fn combined_witness(&self) -> &[Word] {
203		&self.data[..self.size()]
204	}
205
206	/// Evaluates an operand against this witness.
207	///
208	/// An operand is the XOR of its shifted-value terms.
209	/// An empty operand evaluates to the zero word, the XOR identity.
210	#[inline]
211	pub fn eval_operand(&self, operand: &[ShiftedValueIndex]) -> Word {
212		// Fold each shifted term into the running XOR, starting from the identity.
213		operand
214			.iter()
215			.fold(Word::ZERO, |acc, term| acc ^ term.eval(self))
216	}
217}
218
219impl Index<ValueIndex> for ValueVec {
220	type Output = Word;
221
222	fn index(&self, index: ValueIndex) -> &Self::Output {
223		&self.data[self.word_offset(index)]
224	}
225}
226
227impl IndexMut<ValueIndex> for ValueVec {
228	fn index_mut(&mut self, index: ValueIndex) -> &mut Self::Output {
229		let offset = self.word_offset(index);
230		&mut self.data[offset]
231	}
232}
233
234#[cfg(test)]
235mod tests {
236	use proptest::{collection, prelude::any, prop_assert_eq, proptest};
237
238	use super::*;
239
240	#[test]
241	fn split_values_vec_and_combine() {
242		let layout = ValueVecLayout {
243			n_const: 2,
244			n_inout: 2,
245			n_witness: 2,
246			n_internal: 2,
247			n_scratch: 0,
248		};
249		let values = ValueVec::new(&layout);
250
251		let public = values.public();
252		let non_public = values.non_public();
253		let combined = ValueVec::new_from_data(layout.n_const, public, non_public);
254		assert_eq!(combined.combined_witness(), values.combined_witness());
255	}
256
257	// The property that makes the optimization work: the first word sits on a 16-byte boundary.
258	fn assert_16_byte_aligned(words: &[Word]) {
259		assert_eq!(words.as_ptr() as usize % 16, 0);
260	}
261
262	#[test]
263	fn zeroed_is_aligned_zero_filled_and_correct_length() {
264		// Cases:
265		//   0      -> empty buffer, no blocks
266		//   1, 3   -> odd, so the last block's second word is padding
267		//   2, 16  -> even, every block fully used
268		//   17     -> odd and spans many blocks
269		for len in [0, 1, 2, 3, 16, 17] {
270			let words = AlignedWords::zeroed(len);
271			// The view reports the requested word count, not the rounded-up block capacity.
272			assert_eq!(words.len(), len);
273			// Alignment must hold for every length, including the empty buffer.
274			assert_16_byte_aligned(&words);
275			// A freshly allocated buffer is entirely zero.
276			assert!(words.iter().all(|&w| w == Word::ZERO));
277		}
278	}
279
280	#[test]
281	fn deref_mut_writes_are_visible_through_deref() {
282		// Length 5 is odd, so the last block holds one valid word and one padding word.
283		let mut words = AlignedWords::zeroed(5);
284		// Write 1..=5 through the mutable view; this must not touch the padding word.
285		for (i, w) in words.iter_mut().enumerate() {
286			*w = Word::from_u64(i as u64 + 1);
287		}
288		// The shared view reads back exactly the five words written.
289		assert_eq!(
290			&*words,
291			&[
292				Word::from_u64(1),
293				Word::from_u64(2),
294				Word::from_u64(3),
295				Word::from_u64(4),
296				Word::from_u64(5),
297			]
298		);
299	}
300
301	proptest! {
302		#[test]
303		fn value_vec_preserves_words_and_alignment(
304			public in collection::vec(any::<u64>(), 4..32usize),
305			n_witness in 0..32usize,
306			n_scratch in 0..16usize,
307		) {
308			// Public words come straight from the strategy; private words use a recognizable pattern.
309			let public: Vec<Word> = public.into_iter().map(Word).collect();
310			let private: Vec<Word> = (0..n_witness).map(|i| Word::from_u64(0xdead_0000 + i as u64)).collect();
311
312			// The sections sit back to back, then the scratch tail:
313			//
314			//     [0, public.len())                            -> public
315			//     [public.len(), public.len() + private.len()) -> private
316			//     then the scratch tail
317			let layout = ValueVecLayout {
318				n_const: 0,
319				n_inout: public.len(),
320				n_witness: private.len(),
321				n_internal: 0,
322				n_scratch,
323			};
324
325			// A vector built from the layout carries the scratch tail; one built from its
326			// segments holds only those words.
327			let zeroed = ValueVec::new(&layout);
328			let vv = ValueVec::new_from_data(layout.n_const, &public, &private);
329
330			// Alignment survives construction for any word count.
331			assert_16_byte_aligned(vv.combined_witness());
332			// Both sections read back byte-for-byte what went in.
333			prop_assert_eq!(vv.public(), &public[..]);
334			prop_assert_eq!(vv.non_public(), &private[..]);
335
336			// The scratch tail past the committed words is zeroed and addressable.
337			for slot in 0..n_scratch {
338				prop_assert_eq!(zeroed[ValueIndex::scratch(slot as u32)], Word::ZERO);
339			}
340		}
341	}
342}