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, ValueVecLayout};
8use crate::{error::ConstraintSystemError, 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 follows the layout defined by
77/// [`ValueVecLayout`] and serves as the primary data structure for both constraint evaluation and
78/// polynomial commitment.
79///
80/// Between these sections, there may be padding regions to satisfy alignment requirements.
81///
82/// The words live in a buffer that starts on a 16-byte boundary.
83/// That keeps the frequent bulk copies of the vector on the aligned SIMD `memcpy` path.
84#[derive(Clone, Debug)]
85pub struct ValueVec {
86	/// Section offsets and counts that partition the words below.
87	layout: ValueVecLayout,
88	/// The committed words followed by the scratch tail, 16-byte aligned.
89	data: AlignedWords,
90}
91
92impl ValueVec {
93	/// Creates a new value vector with the given layout.
94	///
95	/// The values are filled with zeros.
96	pub fn new(layout: ValueVecLayout) -> ValueVec {
97		let size = layout.combined_len() + layout.n_scratch;
98		ValueVec {
99			layout,
100			data: AlignedWords::zeroed(size),
101		}
102	}
103
104	/// Creates a new value vector with the given layout and data.
105	///
106	/// Each segment is checked to have exactly the length the layout prescribes.
107	pub fn new_from_data(
108		layout: ValueVecLayout,
109		public: Vec<Word>,
110		private: Vec<Word>,
111	) -> Result<ValueVec, ConstraintSystemError> {
112		if public.len() != layout.n_public_words() {
113			return Err(ConstraintSystemError::ValueVecLenMismatch {
114				expected: layout.n_public_words(),
115				actual: public.len(),
116			});
117		}
118		if private.len() != layout.n_hidden_words {
119			return Err(ConstraintSystemError::ValueVecLenMismatch {
120				expected: layout.n_hidden_words,
121				actual: private.len(),
122			});
123		}
124
125		// Full buffer = public words + committed words + scratch tail.
126		let full_len = layout.combined_len() + layout.n_scratch;
127		// Fresh 16-byte-aligned buffer; the scratch tail past the committed words stays zeroed.
128		let mut data = AlignedWords::zeroed(full_len);
129		// Public words occupy the front, committed words follow.
130		data[..public.len()].copy_from_slice(&public);
131		data[public.len()..public.len() + private.len()].copy_from_slice(&private);
132
133		Ok(ValueVec { layout, data })
134	}
135
136	/// The total size of the public and committed portions of the vector (excluding scratch).
137	pub const fn size(&self) -> usize {
138		self.layout.combined_len()
139	}
140
141	/// Returns the public portion of the values vector.
142	pub fn public(&self) -> &[Word] {
143		&self.data[..self.layout.offset_witness]
144	}
145
146	/// Return all non-public values (witness + internal) without scratch space.
147	pub fn non_public(&self) -> &[Word] {
148		&self.data[self.layout.offset_witness..self.layout.combined_len()]
149	}
150
151	/// Returns the witness portion of the values vector.
152	pub fn witness(&self) -> &[Word] {
153		let start = self.layout.offset_witness;
154		let end = start + self.layout.n_witness;
155		&self.data[start..end]
156	}
157
158	/// Returns the combined values vector.
159	pub fn combined_witness(&self) -> &[Word] {
160		&self.data[..self.layout.combined_len()]
161	}
162
163	/// Evaluates an operand against this witness.
164	///
165	/// An operand is the XOR of its shifted-value terms.
166	/// An empty operand evaluates to the zero word, the XOR identity.
167	#[inline]
168	pub fn eval_operand(&self, operand: &[ShiftedValueIndex]) -> Word {
169		// Fold each shifted term into the running XOR, starting from the identity.
170		operand
171			.iter()
172			.fold(Word::ZERO, |acc, term| acc ^ term.eval(self))
173	}
174}
175
176impl Index<ValueIndex> for ValueVec {
177	type Output = Word;
178
179	fn index(&self, index: ValueIndex) -> &Self::Output {
180		&self.data[index.0 as usize]
181	}
182}
183
184impl IndexMut<ValueIndex> for ValueVec {
185	fn index_mut(&mut self, index: ValueIndex) -> &mut Self::Output {
186		&mut self.data[index.0 as usize]
187	}
188}
189
190#[cfg(test)]
191mod tests {
192	use proptest::{collection, prelude::any, prop_assert_eq, proptest};
193
194	use super::*;
195
196	#[test]
197	fn split_values_vec_and_combine() {
198		let values = ValueVec::new(ValueVecLayout {
199			n_const: 2,
200			n_inout: 2,
201			n_witness: 2,
202			n_internal: 2,
203			offset_inout: 2,
204			offset_witness: 4,
205			n_hidden_words: 4,
206			n_scratch: 0,
207		});
208
209		let public = values.public();
210		let non_public = values.non_public();
211		let combined =
212			ValueVec::new_from_data(values.layout.clone(), public.to_vec(), non_public.to_vec())
213				.unwrap();
214		assert_eq!(combined.combined_witness(), values.combined_witness());
215	}
216
217	// The property that makes the optimization work: the first word sits on a 16-byte boundary.
218	fn assert_16_byte_aligned(words: &[Word]) {
219		assert_eq!(words.as_ptr() as usize % 16, 0);
220	}
221
222	#[test]
223	fn zeroed_is_aligned_zero_filled_and_correct_length() {
224		// Cases:
225		//   0      -> empty buffer, no blocks
226		//   1, 3   -> odd, so the last block's second word is padding
227		//   2, 16  -> even, every block fully used
228		//   17     -> odd and spans many blocks
229		for len in [0, 1, 2, 3, 16, 17] {
230			let words = AlignedWords::zeroed(len);
231			// The view reports the requested word count, not the rounded-up block capacity.
232			assert_eq!(words.len(), len);
233			// Alignment must hold for every length, including the empty buffer.
234			assert_16_byte_aligned(&words);
235			// A freshly allocated buffer is entirely zero.
236			assert!(words.iter().all(|&w| w == Word::ZERO));
237		}
238	}
239
240	#[test]
241	fn deref_mut_writes_are_visible_through_deref() {
242		// Length 5 is odd, so the last block holds one valid word and one padding word.
243		let mut words = AlignedWords::zeroed(5);
244		// Write 1..=5 through the mutable view; this must not touch the padding word.
245		for (i, w) in words.iter_mut().enumerate() {
246			*w = Word::from_u64(i as u64 + 1);
247		}
248		// The shared view reads back exactly the five words written.
249		assert_eq!(
250			&*words,
251			&[
252				Word::from_u64(1),
253				Word::from_u64(2),
254				Word::from_u64(3),
255				Word::from_u64(4),
256				Word::from_u64(5),
257			]
258		);
259	}
260
261	proptest! {
262		#[test]
263		fn value_vec_preserves_words_and_alignment(
264			public in collection::vec(any::<u64>(), 4..32usize),
265			n_witness in 0..32usize,
266			n_scratch in 0..16usize,
267		) {
268			// Public words come straight from the strategy; private words use a recognizable pattern.
269			let public: Vec<Word> = public.into_iter().map(Word).collect();
270			let private: Vec<Word> = (0..n_witness).map(|i| Word::from_u64(0xdead_0000 + i as u64)).collect();
271
272			// The public section is padded to a power of two.
273			// The witness section follows it, then the scratch tail.
274			//
275			//     [0, offset_witness)                            -> public  (power of two)
276			//     [offset_witness, offset_witness + n_hidden_words) -> witness
277			//     then the scratch tail
278			let offset_witness = public.len().next_power_of_two();
279			let n_hidden_words = private.len();
280			let layout = ValueVecLayout {
281				n_const: 0,
282				n_inout: public.len(),
283				n_witness: private.len(),
284				n_internal: 0,
285				offset_inout: 0,
286				offset_witness,
287				n_hidden_words,
288				n_scratch,
289			};
290
291			// The public input must fill its whole power-of-two section, so zero-pad it.
292			let mut public_padded = public;
293			public_padded.resize(offset_witness, Word::ZERO);
294
295			let layout_combined_len = layout.combined_len();
296			let vv = ValueVec::new_from_data(layout, public_padded.clone(), private.clone()).unwrap();
297
298			// Alignment survives construction for any word count.
299			assert_16_byte_aligned(vv.combined_witness());
300			// Both sections read back byte-for-byte what went in.
301			prop_assert_eq!(vv.public(), &public_padded[..]);
302			prop_assert_eq!(vv.witness(), &private[..]);
303
304			// The scratch tail past the committed words is zeroed and addressable.
305			let combined_len = layout_combined_len;
306			for i in combined_len..combined_len + n_scratch {
307				prop_assert_eq!(vv[ValueIndex(i as u32)], Word::ZERO);
308			}
309		}
310	}
311}