Skip to main content

binius_frontend/artifact/
circuit.rs

1// Copyright 2025 Irreducible Inc.
2// Copyright 2026 The Binius Developers
3
4//! The artifact a build hands back: a constraint system plus what it takes to fill a witness.
5
6use binius_compute::{Allocator, BufferData, VecLike};
7use binius_core::{
8	ValueTable,
9	constraint_system::{ConstraintSystem, ValueIndex, ValueSegment, ValueVec, ValueVecLayout},
10	word::Word,
11};
12use binius_utils::{rayon::prelude::*, strided_array::StridedArray2DViewMut};
13use cranelift_entity::SecondaryMap;
14
15use crate::{
16	artifact::{
17		dump::dump_composition,
18		witness::{BatchWitnessFiller, PopulateError, WitnessFiller},
19	},
20	eval_form::{BatchPopulateError, EvalForm},
21	ir::{
22		GateBody, Wire,
23		path::{PathSpec, PathSpecTree},
24	},
25	pass::BuiltGates,
26};
27
28/// Default instance count of one parallel witness-generation tile.
29///
30/// A tile is one thread's fully contiguous working buffer for a small group of instances.
31/// Measured runtime on a hash-permutation fixture was flat for tile sizes 64 through 1024.
32/// 256 sits in the middle of that flat range.
33/// Large enough to amortize the per-tile setup cost.
34/// Small enough that a tile's buffer is a fraction of a typical L2 cache.
35const DEFAULT_PARALLEL_TILE_SIZE: usize = 256;
36
37/// An artifact that represents a built circuit.
38///
39/// The difference from [`ConstraintSystem`] is that a circuit retains enough information to
40/// perform circuit evaluation to generate internal witness values.
41pub struct Circuit {
42	path_spec_tree: PathSpecTree,
43	gate_records: Vec<(PathSpec, GateBody)>,
44	constraint_system: ConstraintSystem,
45	value_vec_layout: ValueVecLayout,
46	wire_mapping: SecondaryMap<Wire, ValueIndex>,
47	inout: Vec<Wire>,
48	eval_form: EvalForm,
49	scratch_peak_live: usize,
50	scratch_pooled: bool,
51}
52
53impl Circuit {
54	/// Creates a new circuit with the given shared data and wire mapping. Only used during building
55	/// by the circuit builder.
56	#[allow(clippy::too_many_arguments)]
57	pub(crate) fn new(
58		built_gates: BuiltGates,
59		constraint_system: ConstraintSystem,
60		value_vec_layout: ValueVecLayout,
61		wire_mapping: SecondaryMap<Wire, ValueIndex>,
62		inout: Vec<Wire>,
63		eval_form: EvalForm,
64		scratch_peak_live: usize,
65		scratch_pooled: bool,
66	) -> Self {
67		let BuiltGates {
68			path_spec_tree,
69			gate_records,
70		} = built_gates;
71		Self {
72			path_spec_tree,
73			gate_records,
74			constraint_system,
75			value_vec_layout,
76			wire_mapping,
77			inout,
78			eval_form,
79			scratch_peak_live,
80			scratch_pooled,
81		}
82	}
83
84	/// Returns the wires forming the circuit's public interface, in inout segment order.
85	///
86	/// Inout value `i` of the value vector is the word wire `inout()[i]` holds, so this is the
87	/// reverse of [`Self::witness_index`] over that segment. A caller assembling the positional
88	/// public-input vector a verifier takes reads it straight off this slice.
89	///
90	/// The segment is ordered by wire creation, so a wire promoted with
91	/// [`CircuitBuilder::mark_inout`](crate::CircuitBuilder::mark_inout) follows the declared inout
92	/// wires only if its gate created it later; among promotions the order is the one their gates
93	/// ran in, not the order they were promoted in.
94	pub fn inout(&self) -> &[Wire] {
95		&self.inout
96	}
97
98	/// Returns the smallest scratch segment this circuit could run with.
99	///
100	/// This is the largest number of uncommitted temporaries alive at the same time.
101	/// It is what the segment shrinks to once slots are shared.
102	/// It is reported whether or not sharing is on, so the unused headroom stays visible.
103	pub const fn scratch_peak_live(&self) -> usize {
104		self.scratch_peak_live
105	}
106
107	/// For the given wire, returns its index in the witness vector.
108	#[inline(always)]
109	pub fn witness_index(&self, wire: Wire) -> ValueIndex {
110		self.wire_mapping[wire]
111	}
112
113	/// For the given wire, returns the row it occupies in a transposed value array.
114	///
115	/// This is the wire's flat position in the value vector, counting the scratch tail, which is
116	/// how [`Self::populate_wire_witness_batched`] numbers the rows it fills.
117	#[inline(always)]
118	pub fn witness_row(&self, wire: Wire) -> usize {
119		self.value_vec_layout.word_offset(self.witness_index(wire))
120	}
121
122	/// Panics if the wire's storage is a scratch slot shared with another value.
123	///
124	/// Scratch pooling reclaims a slot once its current value's last read has run.
125	/// A shared slot then holds whatever value most recently claimed it, not this wire's.
126	/// So this rejects the read outright rather than returning that wrong value.
127	pub(crate) fn assert_not_pooled(&self, wire: Wire, index: ValueIndex) {
128		assert!(
129			!self.scratch_pooled || index.segment() != ValueSegment::Scratch,
130			"wire {wire:?} cannot be read back through a witness filler: its storage is a scratch \
131			 slot shared with another value under scratch pooling, and the slot may already hold a \
132			 different value by the time the circuit has finished evaluating. Disable scratch \
133			 pooling for this build (set `Options::enable_scratch_pooling` to `false`), or make \
134			 this value committed instead of scratch, e.g. by marking it inout or referencing it \
135			 from a constraint."
136		);
137	}
138
139	/// Creates a new witness filler for this circuit.
140	pub fn new_witness_filler(&self) -> WitnessFiller<'_> {
141		WitnessFiller {
142			circuit: self,
143			value_vec: ValueVec::new(&self.value_vec_layout),
144		}
145	}
146
147	/// Populates non-input values (wires) in the witness.
148	///
149	/// Specifically, this will evaluate the circuit gate-by-gate and save the results in the
150	/// witness vector.
151	///
152	/// This function expects that the input wires are already filled. The input wires are
153	///
154	/// - [`CircuitBuilder::add_inout`],
155	/// - [`CircuitBuilder::add_witness`] that were not created by the gates,
156	///
157	/// The wires created by [`CircuitBuilder::add_constant`] (and its convenience methods)
158	/// are automatically populated by this function as well. So is a wire promoted with
159	/// [`CircuitBuilder::mark_inout`]: it is public but gate-derived, so the caller leaves it
160	/// unset and reads the computed value back afterwards.
161	///
162	/// # Errors
163	///
164	/// Returns [`PopulateError`] when any assertion fails.
165	/// Each failure names the circuit path the assertion was declared under.
166	/// Evaluation runs to completion first, so every violation is reported at once.
167	///
168	/// [`CircuitBuilder::add_constant`]: crate::CircuitBuilder::add_constant
169	/// [`CircuitBuilder::add_inout`]: crate::CircuitBuilder::add_inout
170	/// [`CircuitBuilder::add_witness`]: crate::CircuitBuilder::add_witness
171	/// [`CircuitBuilder::mark_inout`]: crate::CircuitBuilder::mark_inout
172	pub fn populate_wire_witness(&self, w: &mut WitnessFiller<'_>) -> Result<(), PopulateError> {
173		// Fill the constant part from the witness.
174		for (index, constant) in self.constraint_system.constants.iter().enumerate() {
175			w.value_vec[ValueIndex::constant(index as u32)] = *constant;
176		}
177
178		// Execute the evaluation form - it modifies the ValueVec in place
179		// Pass the PathSpecTree for assertion error symbolication
180		self.eval_form
181			.evaluate(&mut w.value_vec, Some(&self.path_spec_tree))?;
182
183		Ok(())
184	}
185
186	/// Populates non-input values for a batch of instances at once.
187	///
188	/// This is the structure-of-arrays counterpart to [`Self::populate_wire_witness`]. `values` is
189	/// the transposed value array: rows are value-vector indices (in the same order a single
190	/// instance's [`ValueVec`] uses) and columns are instances. Its height must be the full
191	/// value-vector length (including scratch) and its width is the instance count.
192	///
193	/// The caller must fill each instance's input rows first — the witness wires and any declared
194	/// inout wires, but not a wire promoted with [`CircuitBuilder::mark_inout`], which its gate
195	/// derives. This function fills the constant rows (broadcasting each constant across every
196	/// instance) and then evaluates the circuit gate-by-gate for all instances.
197	///
198	/// # Errors
199	///
200	/// If any instance is not satisfiable, returns an error naming the lowest-indexed failing
201	/// instance and its assertion failures.
202	///
203	/// [`CircuitBuilder::mark_inout`]: crate::CircuitBuilder::mark_inout
204	pub fn populate_wire_witness_batched(
205		&self,
206		values: &mut StridedArray2DViewMut<'_, Word>,
207	) -> Result<(), BatchPopulateError> {
208		// Broadcast each constant into its row across every instance. The constants are the same
209		// for all instances, so this fills the constant rows uniformly.
210		let n_instances = values.width();
211		for (index, &constant) in self.constraint_system.constants.iter().enumerate() {
212			for instance in 0..n_instances {
213				values[(index, instance)] = constant;
214			}
215		}
216
217		// Evaluate the bytecode across all instances, symbolicating assertion failures.
218		self.eval_form
219			.evaluate_batched(values, Some(&self.path_spec_tree))
220	}
221
222	/// Returns the constraint system for this circuit.
223	pub const fn constraint_system(&self) -> &ConstraintSystem {
224		&self.constraint_system
225	}
226
227	/// Returns the layout of the value vector this circuit fills.
228	pub const fn value_vec_layout(&self) -> &ValueVecLayout {
229		&self.value_vec_layout
230	}
231
232	/// Returns the number of gates in this circuit.
233	///
234	/// Depending on what type of gates this circuit uses, the number of constraints might be
235	/// significantly larger.
236	pub const fn n_gates(&self) -> usize {
237		self.gate_records.len()
238	}
239
240	/// Returns the number of evaluation instructions in this circuit.
241	pub const fn n_eval_insn(&self) -> usize {
242		self.eval_form.n_eval_insn()
243	}
244
245	/// Returns a string with a JSON dump that is useful to profile the circuit.
246	pub fn simple_json_dump(&self) -> String {
247		dump_composition(&self.path_spec_tree, &self.gate_records)
248	}
249
250	/// Builds the batch witness in wire-major order, populating all `2^log_instances` instances.
251	///
252	/// The instances are independent. For each, `fill` sets the input wires; the batched
253	/// interpreter then derives every remaining wire, filling all instances of one wire at a time.
254	///
255	/// # Arguments
256	///
257	/// - `alloc`: backs the returned table's words and the transient buffer built to fill it.
258	/// - `log_instances`: base-2 logarithm of the instance count.
259	/// - `fill`: sets the input wires of instance `i`, for `i` in `0..2^log_instances`. It must
260	///   assign every witness input and every inout wire on each call.
261	///
262	/// # Errors
263	///
264	/// Returns an error naming the lowest-indexed instance whose inputs do not satisfy the circuit.
265	pub fn populate_batch<A, F>(
266		&self,
267		alloc: &A,
268		log_instances: usize,
269		fill: F,
270	) -> Result<ValueTable<A::Vec<Word>>, BatchPopulateError>
271	where
272		A: Allocator,
273		F: Fn(usize, &mut BatchWitnessFiller<'_, '_>),
274	{
275		self.populate_batch_with(alloc, log_instances, fill)
276	}
277
278	/// Builds the batch witness in parallel, one contiguous tile of instances per thread.
279	///
280	/// - A column stripe of the shared value array is not contiguous.
281	/// - A thread reading it touches one short run per wire row.
282	/// - Successive rows of the same stripe sit one instance count apart in memory.
283	/// - A tile avoids this: each thread gets its own small, fully contiguous buffer.
284	/// - A thread fills, evaluates, then gathers its own tile into the batch's real layout.
285	///
286	/// # Errors
287	///
288	/// Returns an error naming a failing instance whose inputs do not satisfy the circuit.
289	/// The reported instance is not guaranteed to be the lowest failing one across all tiles.
290	pub fn populate_batch_parallel<A, F>(
291		&self,
292		alloc: &A,
293		log_instances: usize,
294		fill: F,
295	) -> Result<ValueTable<A::Vec<Word>>, BatchPopulateError>
296	where
297		A: Allocator,
298		F: Fn(usize, &mut BatchWitnessFiller<'_, '_>) + Sync,
299	{
300		self.populate_batch_parallel_with_stripe_width(
301			alloc,
302			log_instances,
303			DEFAULT_PARALLEL_TILE_SIZE,
304			fill,
305		)
306	}
307
308	/// Builds the batch witness in parallel using a caller-provided tile size.
309	///
310	/// Exposed for benchmarking tile sizes.
311	/// Production callers should use [`Self::populate_batch_parallel`].
312	///
313	/// # Errors
314	///
315	/// Returns an error naming a failing instance whose inputs do not satisfy the circuit.
316	/// The reported instance is not guaranteed to be the lowest failing one across all tiles.
317	///
318	/// # Panics
319	///
320	/// Panics if `stripe_width == 0`.
321	pub fn populate_batch_parallel_with_stripe_width<A, F>(
322		&self,
323		alloc: &A,
324		log_instances: usize,
325		stripe_width: usize,
326		fill: F,
327	) -> Result<ValueTable<A::Vec<Word>>, BatchPopulateError>
328	where
329		A: Allocator,
330		F: Fn(usize, &mut BatchWitnessFiller<'_, '_>) + Sync,
331	{
332		assert!(stripe_width > 0, "stripe width must be positive");
333
334		let layout = self.value_vec_layout().clone();
335		let n_instances = 1usize << log_instances;
336		let full_len = layout.combined_len() + layout.n_scratch;
337		let offset_inout = layout.offset_inout();
338		let n_hidden_words = layout.combined_len() - offset_inout;
339		let tile_size = stripe_width;
340
341		// The destination buffer: row per hidden word, column per instance.
342		// Each tile below gathers into one contiguous stripe of its columns.
343		let data_len = n_hidden_words << log_instances;
344		let mut data = alloc.alloc::<Word>(data_len);
345		data.resize(data_len, Word::ZERO);
346		{
347			let dest =
348				StridedArray2DViewMut::without_stride(&mut data, n_hidden_words, n_instances)
349					.expect("n_hidden_words * n_instances == data.len() by construction");
350
351			(0..n_instances)
352				.into_par_iter()
353				.step_by(tile_size)
354				.zip(dest.into_par_strides(tile_size))
355				.map(|(tile_start, mut dest_stripe)| -> Result<(), BatchPopulateError> {
356					let tile_width = dest_stripe.width();
357
358					// One thread's fully contiguous working buffer for this tile's instances.
359					// Drawn from the same allocator as the destination.
360					// So a caller pooling buffers across proofs recycles every tile too.
361					let tile_len = full_len * tile_width;
362					let mut tile_buf = alloc.alloc::<Word>(tile_len);
363					tile_buf.resize(tile_len, Word::ZERO);
364					let mut tile_view =
365						StridedArray2DViewMut::without_stride(&mut tile_buf, full_len, tile_width)
366							.expect("full_len * tile_width == tile_buf.len() by construction");
367
368					// The caller assigns each instance's input wires into its column of the tile.
369					for local in 0..tile_width {
370						let mut filler = BatchWitnessFiller::new(self, &mut tile_view, local);
371						fill(tile_start + local, &mut filler);
372					}
373
374					// Broadcast the constants, then evaluate every wire the tile still needs.
375					// The tile is small enough to stay resident in cache for the whole pass.
376					// A failure here names a tile-local instance.
377					// Shift it back to the batch-global instance index the caller originally saw.
378					self.populate_wire_witness_batched(&mut tile_view)
379						.map_err(|err| BatchPopulateError {
380							instance: err.instance + tile_start,
381							source: err.source,
382						})?;
383
384					// Gather the tile's hidden rows into their real position in the batch.
385					for row in 0..n_hidden_words {
386						for local in 0..tile_width {
387							dest_stripe[(row, local)] = tile_view[(row + offset_inout, local)];
388						}
389					}
390
391					Ok(())
392				})
393				.collect::<Result<Vec<()>, _>>()?;
394		}
395
396		Ok(ValueTable::from_hidden_words(layout, log_instances, data))
397	}
398
399	fn populate_batch_with<A, F>(
400		&self,
401		alloc: &A,
402		log_instances: usize,
403		fill: F,
404	) -> Result<ValueTable<A::Vec<Word>>, BatchPopulateError>
405	where
406		A: Allocator,
407		F: Fn(usize, &mut BatchWitnessFiller<'_, '_>),
408	{
409		let layout = self.value_vec_layout().clone();
410		let n_instances = 1usize << log_instances;
411
412		// The transient working buffer spans the full value vector — constants, inputs, internal
413		// values, and scratch — for every instance, in wire-major order. It is zeroed rather than
414		// merely reserved: a recycled block starts out holding the last batch's words, and the
415		// words no gate and no filler writes — an unassigned witness wire, say — are committed
416		// alongside the rest.
417		let full_len = layout.combined_len() + layout.n_scratch;
418		let working_len = full_len << log_instances;
419		let mut working = alloc.alloc::<Word>(working_len);
420		working.resize(working_len, Word::ZERO);
421
422		{
423			let mut values =
424				StridedArray2DViewMut::without_stride(&mut working, full_len, n_instances)
425					.expect("full_len * n_instances == working.len() by construction");
426
427			// The caller assigns each instance's witness input wires into that instance's column.
428			for instance in 0..n_instances {
429				let mut filler = BatchWitnessFiller::new(self, &mut values, instance);
430				fill(instance, &mut filler);
431			}
432
433			// Broadcast the constants and evaluate every instance's remaining wires.
434			self.populate_wire_witness_batched(&mut values)?;
435		}
436
437		// Keep the hidden segment: inout rows, then private rows.
438		// Shift it down to the front in place, then truncate the rest away.
439		// The buffer becomes the returned data, so no second allocation is ever live.
440		let start = layout.offset_inout() << log_instances;
441		let end = layout.combined_len() << log_instances;
442		working.copy_within(start..end, 0);
443		working.truncate(end - start);
444
445		Ok(ValueTable::from_hidden_words(layout, log_instances, working))
446	}
447}
448
449#[cfg(test)]
450mod tests {
451	use std::ops::IndexMut;
452
453	use binius_compute::GlobalAllocator;
454	use binius_core::{ValueVec, constraint_system::InoutSegment};
455	use proptest::prelude::*;
456
457	use super::*;
458	use crate::{AssertionFailure, CircuitBuilder};
459
460	/// The constant the mix circuit XORs its first input against.
461	const MIX_K: u64 = 0x0123_4567_89ab_cdef;
462
463	// A circuit deriving four words from two public inputs and a constant, each promoted to a
464	// public output. The promotions are what keep the derivations alive under dead-code
465	// elimination.
466	struct MixCircuit {
467		circuit: Circuit,
468		a: Wire,
469		b: Wire,
470	}
471
472	impl MixCircuit {
473		// Assigns one instance's inputs; the circuit derives its public outputs.
474		fn fill<F: IndexMut<Wire, Output = Word>>(&self, filler: &mut F, a: u64, b: u64) {
475			filler[self.a] = Word(a);
476			filler[self.b] = Word(b);
477		}
478	}
479
480	fn mix_circuit() -> MixCircuit {
481		let builder = CircuitBuilder::new();
482		let a = builder.add_inout();
483		let b = builder.add_inout();
484		let k = builder.add_constant_64(MIX_K);
485
486		let and = builder.band(a, b);
487		let xor = builder.bxor(a, k);
488		let (sum, _cout) = builder.iadd(a, b);
489		let rot = builder.rotr(b, 7);
490		let or = builder.bor(and, rot);
491
492		for wire in [and, xor, sum, or] {
493			builder.mark_inout(wire);
494		}
495
496		MixCircuit {
497			circuit: builder.build(),
498			a,
499			b,
500		}
501	}
502
503	// Populate one instance on its own through the ordinary single-instance flow.
504	fn reference_value_vec(c: &MixCircuit, a: u64, b: u64) -> ValueVec {
505		let mut filler = c.circuit.new_witness_filler();
506		c.fill(&mut filler, a, b);
507		c.circuit.populate_wire_witness(&mut filler).unwrap();
508		filler.into_value_vec()
509	}
510
511	#[test]
512	fn shape_matches_layout() {
513		let c = mix_circuit();
514		let log_instances = 3;
515		let table = c
516			.circuit
517			.populate_batch(&GlobalAllocator, log_instances, |i, w| {
518				c.fill(w, i as u64, i as u64 + 1);
519			})
520			.unwrap();
521
522		let layout = c.circuit.value_vec_layout();
523		assert_eq!(table.log_instances(), log_instances);
524		assert_eq!(table.n_instances(), 8);
525		let n_hidden_words = c
526			.circuit
527			.constraint_system()
528			.n_hidden_words(InoutSegment::Hidden);
529		assert_eq!(table.n_hidden_words(), n_hidden_words);
530		assert_eq!(table.as_words().len(), n_hidden_words * 8);
531		// The committed rows are the inout values the layout stores, then the private ones.
532		assert_eq!(n_hidden_words, layout.n_inout + layout.n_private());
533	}
534
535	#[test]
536	fn every_instance_satisfies_the_constraint_system() {
537		let c = mix_circuit();
538		let constants = &c.circuit.constraint_system().constants;
539
540		let table = c
541			.circuit
542			.populate_batch(&GlobalAllocator, 2, |i, w| {
543				c.fill(w, i as u64 * 0x9e37_79b9, i as u64 ^ 0xdead);
544			})
545			.unwrap();
546
547		for i in 0..table.n_instances() {
548			let vv = table.instance_value_vec(i, constants);
549			c.circuit
550				.constraint_system()
551				.verify(&vv)
552				.unwrap_or_else(|e| panic!("instance {i} failed verification: {e}"));
553		}
554	}
555
556	#[test]
557	fn single_instance_batch_matches_reference() {
558		let c = mix_circuit();
559		let constants = &c.circuit.constraint_system().constants;
560
561		let table = c
562			.circuit
563			.populate_batch(&GlobalAllocator, 0, |_, w| {
564				c.fill(w, 0xABCD, 0x0F0F);
565			})
566			.unwrap();
567
568		assert_eq!(table.n_instances(), 1);
569		let reference = reference_value_vec(&c, 0xABCD, 0x0F0F);
570		// The reconstructed instance equals the reference's committed witness, word for word.
571		let reconstructed = table.instance_value_vec(0, constants);
572		assert_eq!(reconstructed.combined_witness(), reference.combined_witness());
573	}
574
575	proptest! {
576		// Invariant: every batch instance equals the single-instance witness for the same inputs.
577		#[test]
578		fn batch_instances_match_single_instance_reference(
579			inputs in prop::collection::vec((any::<u64>(), any::<u64>()), 4),
580		) {
581			let c = mix_circuit();
582			let constants = c.circuit.constraint_system().constants.clone();
583
584			let table = c.circuit.populate_batch(&GlobalAllocator, 2, |i, w| {
585				let (a, b) = inputs[i];
586				c.fill(w, a, b);
587			})
588			.unwrap();
589
590			for (i, &(a, b)) in inputs.iter().enumerate() {
591				let reference = reference_value_vec(&c, a, b);
592				let reconstructed = table.instance_value_vec(i, &constants);
593				prop_assert_eq!(reconstructed.combined_witness(), reference.combined_witness());
594			}
595		}
596	}
597
598	#[test]
599	fn parallel_population_matches_serial_for_varied_stripe_widths() {
600		let c = mix_circuit();
601		// 1024 instances: four times the default tile size.
602		// Even the default tile size must split the batch into more than one tile.
603		let log_instances = 10;
604		let fill = |i: usize, w: &mut BatchWitnessFiller<'_, '_>| {
605			c.fill(
606				w,
607				(i as u64).wrapping_mul(0x9e37_79b9),
608				(i as u64).rotate_left(17) ^ 0xdead_beef,
609			);
610		};
611
612		let serial = c
613			.circuit
614			.populate_batch(&GlobalAllocator, log_instances, fill)
615			.unwrap();
616
617		let default_parallel = c
618			.circuit
619			.populate_batch_parallel(&GlobalAllocator, log_instances, fill)
620			.unwrap();
621		assert_eq!(default_parallel.as_words(), serial.as_words());
622
623		// Widths span from a handful of instances to more than the whole batch.
624		// The small widths produce many tiles; the two largest each produce a single tile.
625		for stripe_width in [1, 2, 3, 8, 64, 256, 1024, 4096] {
626			let parallel = c
627				.circuit
628				.populate_batch_parallel_with_stripe_width(
629					&GlobalAllocator,
630					log_instances,
631					stripe_width,
632					fill,
633				)
634				.unwrap();
635
636			assert_eq!(
637				parallel.as_words(),
638				serial.as_words(),
639				"stripe width {stripe_width} changed the populated table"
640			);
641		}
642	}
643
644	#[test]
645	fn unsatisfiable_instance_reports_its_index() {
646		// A circuit that asserts a == b; instances where they differ fail.
647		let builder = CircuitBuilder::new();
648		let a = builder.add_inout();
649		let b = builder.add_inout();
650		builder.assert_eq("a_eq_b", a, b);
651		let circuit = builder.build();
652
653		// Instance 2 violates a == b; the others satisfy it.
654		let result = circuit.populate_batch(&GlobalAllocator, 2, |i, w| {
655			w[a] = Word(i as u64);
656			w[b] = Word(if i == 2 { 99 } else { i as u64 });
657		});
658
659		let err = result.expect_err("instance 2 violates a == b");
660		assert_eq!(err.instance, 2);
661		assert_eq!(err.source.total, 1);
662		assert_eq!(
663			err.source.failures,
664			vec![AssertionFailure {
665				path: ".a_eq_b".to_string(),
666				detail: "Word(0x0000000000000002) != Word(0x0000000000000063)".to_string(),
667			}]
668		);
669	}
670
671	#[test]
672	fn parallel_unsatisfiable_instance_reports_global_index_across_stripes() {
673		// A circuit that asserts a == b; instances where they differ fail.
674		let builder = CircuitBuilder::new();
675		let a = builder.add_inout();
676		let b = builder.add_inout();
677		builder.assert_eq("a_eq_b", a, b);
678		let circuit = builder.build();
679
680		// Instance 5 is in the third two-column stripe. Reporting a local stripe index would
681		// incorrectly return 1 instead of the global instance index 5.
682		let result =
683			circuit.populate_batch_parallel_with_stripe_width(&GlobalAllocator, 3, 2, |i, w| {
684				w[a] = Word(i as u64);
685				w[b] = Word(if i == 5 { 99 } else { i as u64 });
686			});
687
688		let err = result.expect_err("instance 5 violates a == b");
689		assert_eq!(err.instance, 5);
690		assert_eq!(err.source.total, 1);
691		assert_eq!(
692			err.source.failures,
693			vec![AssertionFailure {
694				path: ".a_eq_b".to_string(),
695				detail: "Word(0x0000000000000005) != Word(0x0000000000000063)".to_string(),
696			}]
697		);
698	}
699
700	#[test]
701	fn parallel_failure_diagnostics_report_global_instance_across_stripes() {
702		// A circuit that asserts a == b; instances where they differ fail.
703		let builder = CircuitBuilder::new();
704		let a = builder.add_inout();
705		let b = builder.add_inout();
706		builder.assert_eq("a_eq_b", a, b);
707		let circuit = builder.build();
708
709		// Instances 5 and 7 fail in different two-column stripes. The parallel path may report
710		// either stripe depending on scheduling, but it must report a global instance index and
711		// diagnostics for that instance rather than aggregating unrelated stripes.
712		let fill = |i: usize, w: &mut BatchWitnessFiller<'_, '_>| {
713			w[a] = Word(i as u64);
714			w[b] = Word(if i == 5 || i == 7 { 99 } else { i as u64 });
715		};
716		let parallel = circuit
717			.populate_batch_parallel_with_stripe_width(&GlobalAllocator, 3, 2, fill)
718			.expect_err("instances fail");
719
720		assert!(parallel.instance == 5 || parallel.instance == 7);
721		assert_eq!(parallel.source.total, 1);
722		assert_eq!(
723			parallel.source.failures,
724			vec![AssertionFailure {
725				path: ".a_eq_b".to_string(),
726				detail: format!("Word(0x{0:016x}) != Word(0x0000000000000063)", parallel.instance),
727			}]
728		);
729	}
730
731	// Inout wires are committed, so they lead the stored rows: row `i` is inout value `i`, and the
732	// private values follow. Each instance carries its own inout words.
733	#[test]
734	fn inout_wires_lead_the_committed_rows() {
735		let builder = CircuitBuilder::new();
736		let a = builder.add_inout();
737		let b = builder.add_inout();
738		// A private wire, so the table carries private rows behind the inout ones.
739		let w = builder.add_witness();
740		let and = builder.band(a, b);
741		let mixed = builder.bxor(and, w);
742		builder.mark_inout(and);
743		builder.mark_inout(mixed);
744		let circuit = builder.build();
745
746		let log_instances = 2;
747		let table = circuit
748			.populate_batch(&GlobalAllocator, log_instances, |i, f| {
749				f[a] = Word(i as u64);
750				f[b] = Word(i as u64 + 0x100);
751				f[w] = Word(i as u64 ^ 0xbeef);
752			})
753			.unwrap();
754
755		// The inout values lead the private ones, all of them committed.
756		let layout = circuit.value_vec_layout();
757		assert_eq!(layout.n_inout, 4);
758		assert!(layout.n_private() > 0, "the fixture must carry private rows too");
759		assert_eq!(table.n_hidden_words(), layout.n_inout + layout.n_private());
760
761		// The inout rows hold what the filler assigned, one column per instance.
762		let inout_row =
763			|row: usize| &table.as_words()[row << log_instances..(row + 1) << log_instances];
764		assert_eq!(inout_row(0), [Word(0), Word(1), Word(2), Word(3)]);
765		assert_eq!(inout_row(1), [Word(0x100), Word(0x101), Word(0x102), Word(0x103)]);
766
767		// And each instance reconstructs to a witness the constraint system accepts, inout
768		// values included.
769		let constants = &circuit.constraint_system().constants;
770		for i in 0..table.n_instances() {
771			let vv = table.instance_value_vec(i, constants);
772			assert_eq!(vv[circuit.witness_index(a)], Word(i as u64));
773			assert_eq!(vv[circuit.witness_index(b)], Word(i as u64 + 0x100));
774			circuit
775				.constraint_system()
776				.verify(&vv)
777				.unwrap_or_else(|e| panic!("instance {i} failed verification: {e}"));
778		}
779	}
780}