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}