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}