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		super::shift::eval_operand(self, operand)
213	}
214}
215
216impl Index<ValueIndex> for ValueVec {
217	type Output = Word;
218
219	fn index(&self, index: ValueIndex) -> &Self::Output {
220		&self.data[self.word_offset(index)]
221	}
222}
223
224impl IndexMut<ValueIndex> for ValueVec {
225	fn index_mut(&mut self, index: ValueIndex) -> &mut Self::Output {
226		let offset = self.word_offset(index);
227		&mut self.data[offset]
228	}
229}
230
231/// A source of words addressable by [`ValueIndex`].
232///
233/// [`ValueVec`] reads its own buffer.
234/// A [`ValueTable`](super::ValueTable) row reads a strided column instead.
235pub trait WordSource {
236	/// Returns the word at the given index.
237	fn word(&self, index: ValueIndex) -> Word;
238}
239
240impl WordSource for ValueVec {
241	#[inline]
242	fn word(&self, index: ValueIndex) -> Word {
243		self[index]
244	}
245}
246
247#[cfg(test)]
248mod tests {
249	use proptest::{collection, prelude::any, prop_assert_eq, proptest};
250
251	use super::*;
252
253	#[test]
254	fn split_values_vec_and_combine() {
255		let layout = ValueVecLayout {
256			n_const: 2,
257			n_inout: 2,
258			n_witness: 2,
259			n_internal: 2,
260			n_scratch: 0,
261		};
262		let values = ValueVec::new(&layout);
263
264		let public = values.public();
265		let non_public = values.non_public();
266		let combined = ValueVec::new_from_data(layout.n_const, public, non_public);
267		assert_eq!(combined.combined_witness(), values.combined_witness());
268	}
269
270	// The property that makes the optimization work: the first word sits on a 16-byte boundary.
271	fn assert_16_byte_aligned(words: &[Word]) {
272		assert_eq!(words.as_ptr() as usize % 16, 0);
273	}
274
275	#[test]
276	fn zeroed_is_aligned_zero_filled_and_correct_length() {
277		// Cases:
278		//   0      -> empty buffer, no blocks
279		//   1, 3   -> odd, so the last block's second word is padding
280		//   2, 16  -> even, every block fully used
281		//   17     -> odd and spans many blocks
282		for len in [0, 1, 2, 3, 16, 17] {
283			let words = AlignedWords::zeroed(len);
284			// The view reports the requested word count, not the rounded-up block capacity.
285			assert_eq!(words.len(), len);
286			// Alignment must hold for every length, including the empty buffer.
287			assert_16_byte_aligned(&words);
288			// A freshly allocated buffer is entirely zero.
289			assert!(words.iter().all(|&w| w == Word::ZERO));
290		}
291	}
292
293	#[test]
294	fn deref_mut_writes_are_visible_through_deref() {
295		// Length 5 is odd, so the last block holds one valid word and one padding word.
296		let mut words = AlignedWords::zeroed(5);
297		// Write 1..=5 through the mutable view; this must not touch the padding word.
298		for (i, w) in words.iter_mut().enumerate() {
299			*w = Word::from_u64(i as u64 + 1);
300		}
301		// The shared view reads back exactly the five words written.
302		assert_eq!(
303			&*words,
304			&[
305				Word::from_u64(1),
306				Word::from_u64(2),
307				Word::from_u64(3),
308				Word::from_u64(4),
309				Word::from_u64(5),
310			]
311		);
312	}
313
314	proptest! {
315		#[test]
316		fn value_vec_preserves_words_and_alignment(
317			public in collection::vec(any::<u64>(), 4..32usize),
318			n_witness in 0..32usize,
319			n_scratch in 0..16usize,
320		) {
321			// Public words come straight from the strategy; private words use a recognizable pattern.
322			let public: Vec<Word> = public.into_iter().map(Word).collect();
323			let private: Vec<Word> = (0..n_witness).map(|i| Word::from_u64(0xdead_0000 + i as u64)).collect();
324
325			// The sections sit back to back, then the scratch tail:
326			//
327			//     [0, public.len())                            -> public
328			//     [public.len(), public.len() + private.len()) -> private
329			//     then the scratch tail
330			let layout = ValueVecLayout {
331				n_const: 0,
332				n_inout: public.len(),
333				n_witness: private.len(),
334				n_internal: 0,
335				n_scratch,
336			};
337
338			// A vector built from the layout carries the scratch tail; one built from its
339			// segments holds only those words.
340			let zeroed = ValueVec::new(&layout);
341			let vv = ValueVec::new_from_data(layout.n_const, &public, &private);
342
343			// Alignment survives construction for any word count.
344			assert_16_byte_aligned(vv.combined_witness());
345			// Both sections read back byte-for-byte what went in.
346			prop_assert_eq!(vv.public(), &public[..]);
347			prop_assert_eq!(vv.non_public(), &private[..]);
348
349			// The scratch tail past the committed words is zeroed and addressable.
350			for slot in 0..n_scratch {
351				prop_assert_eq!(zeroed[ValueIndex::scratch(slot as u32)], Word::ZERO);
352			}
353		}
354	}
355}