1use 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
28const DEFAULT_PARALLEL_TILE_SIZE: usize = 256;
36
37pub 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 #[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 pub fn inout(&self) -> &[Wire] {
95 &self.inout
96 }
97
98 pub const fn scratch_peak_live(&self) -> usize {
104 self.scratch_peak_live
105 }
106
107 #[inline(always)]
109 pub fn witness_index(&self, wire: Wire) -> ValueIndex {
110 self.wire_mapping[wire]
111 }
112
113 #[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 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 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 pub fn populate_wire_witness(&self, w: &mut WitnessFiller<'_>) -> Result<(), PopulateError> {
173 for (index, constant) in self.constraint_system.constants.iter().enumerate() {
175 w.value_vec[ValueIndex::constant(index as u32)] = *constant;
176 }
177
178 self.eval_form
181 .evaluate(&mut w.value_vec, Some(&self.path_spec_tree))?;
182
183 Ok(())
184 }
185
186 pub fn populate_wire_witness_batched(
205 &self,
206 values: &mut StridedArray2DViewMut<'_, Word>,
207 ) -> Result<(), BatchPopulateError> {
208 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 self.eval_form
219 .evaluate_batched(values, Some(&self.path_spec_tree))
220 }
221
222 pub const fn constraint_system(&self) -> &ConstraintSystem {
224 &self.constraint_system
225 }
226
227 pub const fn value_vec_layout(&self) -> &ValueVecLayout {
229 &self.value_vec_layout
230 }
231
232 pub const fn n_gates(&self) -> usize {
237 self.gate_records.len()
238 }
239
240 pub const fn n_eval_insn(&self) -> usize {
242 self.eval_form.n_eval_insn()
243 }
244
245 pub fn simple_json_dump(&self) -> String {
247 dump_composition(&self.path_spec_tree, &self.gate_records)
248 }
249
250 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 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 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 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 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 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 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 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 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 for instance in 0..n_instances {
429 let mut filler = BatchWitnessFiller::new(self, &mut values, instance);
430 fill(instance, &mut filler);
431 }
432
433 self.populate_wire_witness_batched(&mut values)?;
435 }
436
437 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 const MIX_K: u64 = 0x0123_4567_89ab_cdef;
462
463 struct MixCircuit {
467 circuit: Circuit,
468 a: Wire,
469 b: Wire,
470 }
471
472 impl MixCircuit {
473 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 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 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 let reconstructed = table.instance_value_vec(0, constants);
572 assert_eq!(reconstructed.combined_witness(), reference.combined_witness());
573 }
574
575 proptest! {
576 #[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 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 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 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 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 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 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 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 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 #[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 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 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 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 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}