Skip to main content

binius_core/constraint_system/
value_table.rs

1// Copyright 2026 The Binius Developers
2
3use std::ops::Deref;
4
5use super::{ValueIndex, ValueSegment, ValueVec, ValueVecLayout, WordSource};
6use crate::word::Word;
7
8/// The witness for a batch of `2^k` independent instances of one circuit, in wire-major order.
9///
10/// Each wire's values across all instances are grouped together, one wire per row (wire-major):
11///
12/// ```text
13///                 instance 0   instance 1   ...   instance K-1
14///   wire 0       [   w        |   w        | ... |   w        ]   <- one row
15///   wire 1       [   w        |   w        | ... |   w        ]
16///        ...
17/// ```
18///
19/// Each row is one wire and each column is one instance.
20/// So a batched interpreter can advance every instance of a wire in a single pass.
21///
22/// This is the batched counterpart of [`ValueVec`]: one instance read back out with
23/// [`Self::instance_value_vec`] is bit-for-bit the value vector populating that instance on its own
24/// would produce.
25///
26/// This table is specialized for the M4 accelerator setting, where the value vector splits with
27/// the inout values on the hidden side ([`InoutSegment::Hidden`](super::InoutSegment::Hidden)):
28///
29/// 1. **Inout wires are committed.** Every instance chooses its own inout words, so they are not
30///    one set of shared values a verifier can evaluate. They are committed with the private values
31///    instead, at the front of the hidden segment.
32/// 2. **Constants are not stored.** The constants live once on the constraint system as a
33///    `Vec<Word>`, not replicated per instance. Only the hidden segment — the inout, witness and
34///    internal words — is stored here. Reading an instance back takes the constants as an argument.
35///
36/// So the stored data holds exactly the hidden segment of every instance: `n_hidden_words` rows by
37/// `2^log_instances` columns.
38///
39/// The committed inout words are not tied to any value the verifier knows, so the statement a
40/// proof over this table makes is that *some* inout values complete every instance.
41///
42/// Populating one is the circuit frontend's business, since it takes a circuit to evaluate — see
43/// `Circuit::populate_batch`.
44///
45/// `Data` is the buffer backing the words. It defaults to [`Vec<Word>`], but the prover populates
46/// a table straight into a buffer drawn from its pool, so any `Deref<Target = [Word]>` will do.
47#[derive(Clone, Debug)]
48pub struct ValueTable<Data = Vec<Word>> {
49	/// The per-instance value layout, shared by every instance in the batch.
50	layout: ValueVecLayout,
51	/// The base-2 logarithm of the instance count.
52	log_instances: usize,
53	/// The number of hidden-word rows the proving protocol commits per instance.
54	///
55	/// This is the inout values followed by the private ones, which is
56	/// [`ConstraintSystem::n_hidden_words`](super::ConstraintSystem::n_hidden_words) under
57	/// [`InoutSegment::Hidden`](super::InoutSegment::Hidden).
58	n_hidden_words: usize,
59	/// The hidden words of every wire, in wire-major order.
60	///
61	/// Row `r` (for `r` in `0..n_hidden_words`) holds the `2^log_instances` values of hidden wire
62	/// `r` — inout value `r`, then private value `r - n_inout` — one per instance. The rows are
63	/// laid out contiguously, so the length is `n_hidden_words << log_instances`.
64	data: Data,
65}
66
67impl<Data: Deref<Target = [Word]>> ValueTable<Data> {
68	/// Builds a table from the hidden words of every instance, in wire-major order.
69	///
70	/// `data` is what [`Self::as_words`] returns: the rows of the hidden segment laid out
71	/// contiguously, each holding one wire's value in every instance. The row count follows from
72	/// the layout, so it is not passed separately.
73	///
74	/// This is the seam the frontend populates through; nothing in this crate can evaluate a
75	/// circuit to produce the words.
76	///
77	/// # Panics
78	///
79	/// Panics if `data` is not `n_hidden_words << log_instances` words long.
80	pub fn from_hidden_words(layout: ValueVecLayout, log_instances: usize, data: Data) -> Self {
81		let n_hidden_words = layout.combined_len() - layout.offset_inout();
82		assert_eq!(
83			data.len(),
84			n_hidden_words << log_instances,
85			"the data must hold one row per hidden word, one column per instance"
86		);
87		Self {
88			layout,
89			log_instances,
90			n_hidden_words,
91			data,
92		}
93	}
94
95	/// The base-2 logarithm of the number of instances.
96	pub const fn log_instances(&self) -> usize {
97		self.log_instances
98	}
99
100	/// The number of instances in the batch.
101	pub const fn n_instances(&self) -> usize {
102		1usize << self.log_instances
103	}
104
105	/// The per-instance value layout shared by every instance.
106	pub const fn layout(&self) -> &ValueVecLayout {
107		&self.layout
108	}
109
110	/// The number of hidden-word rows per instance, including the protocol's zero padding.
111	pub const fn n_hidden_words(&self) -> usize {
112		self.n_hidden_words
113	}
114
115	/// The whole batch as one flat, wire-major word buffer.
116	///
117	/// Row `r` (hidden wire `r`) occupies `data[r << log_instances .. (r + 1) << log_instances]`,
118	/// holding that wire's value in every instance.
119	pub fn as_words(&self) -> &[Word] {
120		&self.data
121	}
122
123	/// Reconstructs one instance as a standalone single-instance value vector.
124	///
125	/// Because the constants are not stored in the table, the caller supplies them (they live on
126	/// the constraint system). The result is bit-for-bit what populating this instance on its own
127	/// would produce, so it can be fed directly to single-instance constraint checking.
128	///
129	/// # Panics
130	///
131	/// Panics if the index is not below the instance count, or if `constants` does not match the
132	/// layout's constant count.
133	pub fn instance_value_vec(&self, instance: usize, constants: &[Word]) -> ValueVec {
134		assert!(instance < self.n_instances(), "instance index out of range");
135		assert_eq!(
136			constants.len(),
137			self.layout.n_const,
138			"constants length must match the layout's constant count"
139		);
140
141		// The constants are the whole public segment; the table stores everything after them.
142		let mut values = ValueVec::new(&self.layout);
143		for (i, &constant) in constants.iter().enumerate() {
144			values[ValueIndex::constant(i as u32)] = constant;
145		}
146
147		// Gather this instance's column of hidden values across every row, which the value vector
148		// holds from the first inout word onwards.
149		for row in 0..self.n_hidden_words {
150			*values.word_mut((self.layout.offset_inout() + row) as u32) =
151				self.data[(row << self.log_instances) + instance];
152		}
153
154		values
155	}
156
157	/// Returns a [`WordSource`] view of one instance's row.
158	///
159	/// Reads only the words an operand names, each straight off its strided row.
160	/// [`Self::instance_value_vec`] gathers the whole hidden segment instead.
161	///
162	/// # Panics
163	///
164	/// Panics under the same conditions as [`Self::instance_value_vec`].
165	pub fn instance_words<'a>(
166		&'a self,
167		instance: usize,
168		constants: &'a [Word],
169	) -> TableInstance<'a, Data> {
170		assert!(instance < self.n_instances(), "instance index out of range");
171		assert_eq!(
172			constants.len(),
173			self.layout.n_const,
174			"constants length must match the layout's constant count"
175		);
176		TableInstance {
177			table: self,
178			instance,
179			constants,
180		}
181	}
182}
183
184/// A [`WordSource`] view of one instance of a [`ValueTable`], as returned by
185/// [`ValueTable::instance_words`].
186#[derive(Clone, Copy)]
187pub struct TableInstance<'a, Data> {
188	table: &'a ValueTable<Data>,
189	instance: usize,
190	constants: &'a [Word],
191}
192
193impl<Data: Deref<Target = [Word]>> WordSource for TableInstance<'_, Data> {
194	#[inline]
195	fn word(&self, index: ValueIndex) -> Word {
196		// Constants live outside the table; the caller supplies them.
197		if index.segment() == ValueSegment::Constant {
198			return self.constants[index.index() as usize];
199		}
200
201		// Every other segment is a hidden row, the same one `instance_value_vec` gathers.
202		let row = self.table.layout.word_offset(index) - self.table.layout.offset_inout();
203		self.table.data[(row << self.table.log_instances) + self.instance]
204	}
205}
206
207#[cfg(test)]
208mod tests {
209	use proptest::{collection, prelude::any, prop_assert_eq, proptest};
210
211	use super::*;
212
213	/// A layout of one constant, two inout values and one private value.
214	fn layout() -> ValueVecLayout {
215		ValueVecLayout {
216			n_const: 1,
217			n_inout: 2,
218			n_witness: 1,
219			n_internal: 0,
220			n_scratch: 0,
221		}
222	}
223
224	/// A two-instance table whose hidden word `r` holds `0x10 * r + instance`.
225	fn table() -> ValueTable {
226		let n_hidden_words = layout().combined_len() - layout().offset_inout();
227		let data = (0..n_hidden_words)
228			.flat_map(|row| {
229				(0..2).map(move |instance| Word::from_u64(0x10 * row as u64 + instance))
230			})
231			.collect();
232		ValueTable::from_hidden_words(layout(), 1, data)
233	}
234
235	#[test]
236	fn the_row_count_follows_from_the_layout() {
237		let table = table();
238		assert_eq!(table.log_instances(), 1);
239		assert_eq!(table.n_instances(), 2);
240		// Three hidden words: two inout values and one private one.
241		assert_eq!(table.n_hidden_words(), 3);
242		assert_eq!(table.as_words().len(), 6);
243	}
244
245	#[test]
246	#[should_panic(expected = "one row per hidden word")]
247	fn a_buffer_of_the_wrong_length_is_rejected() {
248		ValueTable::from_hidden_words(layout(), 1, vec![Word::ZERO; 5]);
249	}
250
251	// An instance is the column at its index, behind the constants the table does not store.
252	#[test]
253	fn an_instance_reads_back_as_its_own_column() {
254		let table = table();
255		let constants = [Word::from_u64(0xc0)];
256
257		for instance in 0..2 {
258			let values = table.instance_value_vec(instance, &constants);
259			assert_eq!(values[ValueIndex::constant(0)], constants[0]);
260			assert_eq!(values[ValueIndex::inout(0)], Word::from_u64(instance as u64));
261			assert_eq!(values[ValueIndex::inout(1)], Word::from_u64(0x10 + instance as u64));
262			assert_eq!(values[ValueIndex::private(0)], Word::from_u64(0x20 + instance as u64));
263		}
264	}
265
266	#[test]
267	#[should_panic(expected = "instance index out of range")]
268	fn reading_past_the_last_instance_panics() {
269		table().instance_value_vec(2, &[Word::from_u64(0xc0)]);
270	}
271
272	#[test]
273	#[should_panic(expected = "constants length must match")]
274	fn the_wrong_number_of_constants_is_rejected() {
275		table().instance_value_vec(0, &[]);
276	}
277
278	#[test]
279	fn instance_words_reads_back_as_its_own_column() {
280		let table = table();
281		let constants = [Word::from_u64(0xc0)];
282
283		for instance in 0..2 {
284			let words = table.instance_words(instance, &constants);
285			assert_eq!(words.word(ValueIndex::constant(0)), constants[0]);
286			assert_eq!(words.word(ValueIndex::inout(0)), Word::from_u64(instance as u64));
287			assert_eq!(words.word(ValueIndex::inout(1)), Word::from_u64(0x10 + instance as u64));
288			assert_eq!(words.word(ValueIndex::private(0)), Word::from_u64(0x20 + instance as u64));
289		}
290	}
291
292	#[test]
293	#[should_panic(expected = "instance index out of range")]
294	fn instance_words_past_the_last_instance_panics() {
295		table().instance_words(2, &[Word::from_u64(0xc0)]);
296	}
297
298	#[test]
299	#[should_panic(expected = "constants length must match")]
300	fn instance_words_rejects_the_wrong_number_of_constants() {
301		table().instance_words(0, &[]);
302	}
303
304	proptest! {
305		// Pins the table-row reads `TableInstance` does against the reference: reconstructing the
306		// instance as a whole `ValueVec` and indexing into that instead.
307		#[test]
308		fn instance_words_matches_instance_value_vec(
309			data in collection::vec(any::<u64>(), 6..=6),
310			constant in any::<u64>(),
311		) {
312			let data: Vec<Word> = data.into_iter().map(Word::from_u64).collect();
313			let table = ValueTable::from_hidden_words(layout(), 1, data);
314			let constants = [Word::from_u64(constant)];
315			let indices = [
316				ValueIndex::constant(0),
317				ValueIndex::inout(0),
318				ValueIndex::inout(1),
319				ValueIndex::private(0),
320			];
321
322			for instance in 0..table.n_instances() {
323				let reference = table.instance_value_vec(instance, &constants);
324				let words = table.instance_words(instance, &constants);
325				for &index in &indices {
326					prop_assert_eq!(words.word(index), reference[index]);
327				}
328			}
329		}
330	}
331}