Skip to main content

triton_constraint_circuit/
lib.rs

1//! Constraint circuits are a way to represent constraint polynomials in a way
2//! that is amenable to optimizations. The constraint circuit is a directed
3//! acyclic graph (DAG) of [`CircuitExpression`]s, where each
4//! `CircuitExpression` is a node in the graph. The edges of the graph are
5//! labeled with [`BinOp`]s. The leafs of the graph are the inputs to the
6//! constraint polynomial, and the (multiple) roots of the graph are the outputs
7//! of all the constraint polynomials, with each root corresponding to a
8//! different constraint polynomial. Because the graph has multiple roots, it is
9//! called a “multitree.”
10
11// See the corresponding attribute in triton_vm/lib.rs
12#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
13
14use std::cell::RefCell;
15use std::cmp;
16use std::collections::HashMap;
17use std::fmt::Debug;
18use std::fmt::Display;
19use std::fmt::Formatter;
20use std::fmt::Result as FmtResult;
21use std::hash::Hash;
22use std::hash::Hasher;
23use std::iter::Sum;
24use std::ops::Add;
25use std::ops::Mul;
26use std::ops::Neg;
27use std::ops::Sub;
28use std::rc::Rc;
29
30use arbitrary::Arbitrary;
31use arbitrary::Unstructured;
32use itertools::Itertools;
33use ndarray::ArrayView2;
34use num_traits::One;
35use num_traits::Zero;
36use quote::ToTokens;
37use quote::quote;
38use twenty_first::prelude::*;
39
40/// A reference-counted [`ConstraintCircuit`].
41type RcCircuit<II> = Rc<RefCell<ConstraintCircuit<II>>>;
42
43mod private {
44    // A public but un-nameable type for sealing traits.
45    pub trait Seal {}
46}
47
48#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
49pub struct DegreeLoweringInfo {
50    /// The degree after degree lowering. Must be greater than 1.
51    pub target_degree: isize,
52
53    /// The total number of main columns _before_ degree lowering has happened.
54    pub num_main_cols: usize,
55
56    /// The total number of auxiliary columns _before_ degree lowering has
57    /// happened.
58    pub num_aux_cols: usize,
59}
60
61#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
62pub enum BinOp {
63    Add,
64    Mul,
65}
66
67impl Display for BinOp {
68    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
69        match self {
70            BinOp::Add => write!(f, "+"),
71            BinOp::Mul => write!(f, "*"),
72        }
73    }
74}
75
76impl ToTokens for BinOp {
77    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
78        match self {
79            BinOp::Add => tokens.extend(quote!(+)),
80            BinOp::Mul => tokens.extend(quote!(*)),
81        }
82    }
83}
84
85impl BinOp {
86    pub fn operation<L, R, O>(&self, lhs: L, rhs: R) -> O
87    where
88        L: Add<R, Output = O> + Mul<R, Output = O>,
89    {
90        match self {
91            BinOp::Add => lhs + rhs,
92            BinOp::Mul => lhs * rhs,
93        }
94    }
95}
96
97/// Describes the position of a variable in a constraint polynomial in the row
98/// layout applicable for a certain kind of constraint polynomial.
99///
100/// The position of variable in a constraint polynomial is, in principle, a
101/// `usize`. However, depending on the type of the constraint polynomial, this
102/// index may be an index into a single row (for initial, consistency and
103/// terminal constraints), or a pair of adjacent rows (for transition
104/// constraints). Additionally, the index may refer to a column in the main
105/// table, or a column in the auxiliary table. This trait abstracts over these
106/// possibilities, and provides a uniform interface for accessing the index.
107///
108/// Having `Copy + Hash + Eq` helps to put `InputIndicator`s into containers.
109///
110/// This is a _sealed_ trait. It is not intended (or possible) to implement this
111/// trait outside the crate defining it.
112pub trait InputIndicator: Debug + Display + Copy + Hash + Eq + ToTokens + private::Seal {
113    /// `true` iff `self` refers to a column in the main table.
114    fn is_main_table_column(&self) -> bool;
115
116    /// `true` iff `self` refers to the current row.
117    fn is_current_row(&self) -> bool;
118
119    /// The index of the indicated (main or auxiliary) column.
120    fn column(&self) -> usize;
121
122    fn main_table_input(index: usize) -> Self;
123    fn aux_table_input(index: usize) -> Self;
124
125    fn evaluate(
126        &self,
127        main_table: ArrayView2<BFieldElement>,
128        aux_table: ArrayView2<XFieldElement>,
129    ) -> XFieldElement;
130}
131
132/// The position of a variable in a constraint polynomial that operates on a
133/// single row of the execution trace.
134#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Arbitrary)]
135pub enum SingleRowIndicator {
136    Main(usize),
137    Aux(usize),
138}
139
140impl private::Seal for SingleRowIndicator {}
141
142impl Display for SingleRowIndicator {
143    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
144        let input_indicator: String = match self {
145            Self::Main(i) => format!("main_row[{i}]"),
146            Self::Aux(i) => format!("aux_row[{i}]"),
147        };
148
149        write!(f, "{input_indicator}")
150    }
151}
152
153impl ToTokens for SingleRowIndicator {
154    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
155        match self {
156            Self::Main(i) => tokens.extend(quote!(main_row[#i])),
157            Self::Aux(i) => tokens.extend(quote!(aux_row[#i])),
158        }
159    }
160}
161
162impl InputIndicator for SingleRowIndicator {
163    fn is_main_table_column(&self) -> bool {
164        matches!(self, Self::Main(_))
165    }
166
167    fn is_current_row(&self) -> bool {
168        true
169    }
170
171    fn column(&self) -> usize {
172        match self {
173            Self::Main(i) | Self::Aux(i) => *i,
174        }
175    }
176
177    fn main_table_input(index: usize) -> Self {
178        Self::Main(index)
179    }
180
181    fn aux_table_input(index: usize) -> Self {
182        Self::Aux(index)
183    }
184
185    fn evaluate(
186        &self,
187        main_table: ArrayView2<BFieldElement>,
188        aux_table: ArrayView2<XFieldElement>,
189    ) -> XFieldElement {
190        match self {
191            Self::Main(i) => main_table[[0, *i]].lift(),
192            Self::Aux(i) => aux_table[[0, *i]],
193        }
194    }
195}
196
197/// The position of a variable in a constraint polynomial that operates on two
198/// rows (current and next) of the execution trace.
199#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Arbitrary)]
200pub enum DualRowIndicator {
201    CurrentMain(usize),
202    CurrentAux(usize),
203    NextMain(usize),
204    NextAux(usize),
205}
206
207impl private::Seal for DualRowIndicator {}
208
209impl Display for DualRowIndicator {
210    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
211        let input_indicator: String = match self {
212            Self::CurrentMain(i) => format!("current_main_row[{i}]"),
213            Self::CurrentAux(i) => format!("current_aux_row[{i}]"),
214            Self::NextMain(i) => format!("next_main_row[{i}]"),
215            Self::NextAux(i) => format!("next_aux_row[{i}]"),
216        };
217
218        write!(f, "{input_indicator}")
219    }
220}
221
222impl ToTokens for DualRowIndicator {
223    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
224        match self {
225            Self::CurrentMain(i) => tokens.extend(quote!(current_main_row[#i])),
226            Self::CurrentAux(i) => tokens.extend(quote!(current_aux_row[#i])),
227            Self::NextMain(i) => tokens.extend(quote!(next_main_row[#i])),
228            Self::NextAux(i) => tokens.extend(quote!(next_aux_row[#i])),
229        }
230    }
231}
232
233impl InputIndicator for DualRowIndicator {
234    fn is_main_table_column(&self) -> bool {
235        matches!(self, Self::CurrentMain(_) | Self::NextMain(_))
236    }
237
238    fn is_current_row(&self) -> bool {
239        matches!(self, Self::CurrentMain(_) | Self::CurrentAux(_))
240    }
241
242    fn column(&self) -> usize {
243        match self {
244            Self::CurrentMain(i) | Self::NextMain(i) | Self::CurrentAux(i) | Self::NextAux(i) => *i,
245        }
246    }
247
248    fn main_table_input(index: usize) -> Self {
249        // It seems that the choice between `CurrentMain` and `NextMain` is
250        // arbitrary: any transition constraint polynomial is evaluated on both
251        // the current and the next row. Hence, both rows are in scope.
252        Self::CurrentMain(index)
253    }
254
255    fn aux_table_input(index: usize) -> Self {
256        Self::CurrentAux(index)
257    }
258
259    fn evaluate(
260        &self,
261        main_table: ArrayView2<BFieldElement>,
262        aux_table: ArrayView2<XFieldElement>,
263    ) -> XFieldElement {
264        match self {
265            Self::CurrentMain(i) => main_table[[0, *i]].lift(),
266            Self::CurrentAux(i) => aux_table[[0, *i]],
267            Self::NextMain(i) => main_table[[1, *i]].lift(),
268            Self::NextAux(i) => aux_table[[1, *i]],
269        }
270    }
271}
272
273/// A circuit expression is the recursive data structure that represents the
274/// constraint circuit. It is a directed, acyclic graph of binary operations on
275/// a) the variables corresponding to columns in the AET, b) constants, and c)
276/// challenges. It has multiple roots, making it a “multitree.” Each root
277/// corresponds to one constraint.
278///
279/// The leafs of the tree are
280/// - constants in the base field, _i.e._, [`BFieldElement`]s,
281/// - constants in the extension field, _i.e._, [`XFieldElement`]s,
282/// - input variables, _i.e._, entries from the Algebraic Execution Trace, main
283///   or aux, and
284/// - challenges, _i.e._, (pseudo-)random values sampled through the Fiat-Shamir
285///   heuristic.
286///
287/// An internal node, representing some binary operation, is either addition or
288/// multiplication. The left and right children of the node are the operands of
289/// the binary operation. The left and right children are not themselves
290/// `CircuitExpression`s, but rather [`ConstraintCircuit`]s, which is a wrapper
291/// around `CircuitExpression` that manages additional bookkeeping information.
292#[derive(Debug, Clone)]
293pub enum CircuitExpression<II: InputIndicator> {
294    BConst(BFieldElement),
295    XConst(XFieldElement),
296    Input(II),
297    Challenge(usize),
298    BinOp(BinOp, RcCircuit<II>, RcCircuit<II>),
299}
300
301impl<II: InputIndicator> Hash for CircuitExpression<II> {
302    fn hash<H: Hasher>(&self, state: &mut H) {
303        // use domain separation to prevent weird hash collisions
304        match self {
305            Self::BConst(bfe) => {
306                0_usize.hash(state);
307                bfe.hash(state);
308            }
309            Self::XConst(xfe) => {
310                1_usize.hash(state);
311                xfe.hash(state);
312            }
313            Self::Input(index) => {
314                2_usize.hash(state);
315                index.hash(state);
316            }
317            Self::Challenge(id) => {
318                3_usize.hash(state);
319                id.hash(state);
320            }
321            Self::BinOp(binop, lhs, rhs) => {
322                4_usize.hash(state);
323                binop.hash(state);
324                lhs.borrow().hash(state);
325                rhs.borrow().hash(state);
326            }
327        }
328    }
329}
330
331impl<II: InputIndicator> PartialEq for CircuitExpression<II> {
332    fn eq(&self, other: &Self) -> bool {
333        match (self, other) {
334            (Self::BConst(b), Self::BConst(b_o)) => b == b_o,
335            (Self::XConst(x), Self::XConst(x_o)) => x == x_o,
336            (Self::Input(i), Self::Input(i_o)) => i == i_o,
337            (Self::Challenge(c), Self::Challenge(c_o)) => c == c_o,
338            (Self::BinOp(op, l, r), Self::BinOp(op_o, l_o, r_o)) => {
339                op == op_o && l == l_o && r == r_o
340            }
341            _ => false,
342        }
343    }
344}
345
346impl<II: InputIndicator> Hash for ConstraintCircuit<II> {
347    fn hash<H: Hasher>(&self, state: &mut H) {
348        self.expression.hash(state)
349    }
350}
351
352impl<II: InputIndicator> Hash for ConstraintCircuitMonad<II> {
353    fn hash<H: Hasher>(&self, state: &mut H) {
354        self.circuit.borrow().hash(state)
355    }
356}
357
358/// A wrapper around a [`CircuitExpression`] that manages additional bookkeeping
359/// information, such as node id and visited counter.
360///
361/// In contrast to [`ConstraintCircuitMonad`], this struct cannot manage the
362/// state required to insert new nodes.
363#[derive(Debug, Clone)]
364pub struct ConstraintCircuit<II: InputIndicator> {
365    pub id: u64,
366    pub ref_count: usize,
367    pub expression: CircuitExpression<II>,
368}
369
370impl<II: InputIndicator> Eq for ConstraintCircuit<II> {}
371
372impl<II: InputIndicator> PartialEq for ConstraintCircuit<II> {
373    /// Calculate equality of circuits. In particular, this function does *not*
374    /// attempt to simplify or reduce neutral terms or products. So this
375    /// comparison will return false for `a == a + 0`. It will also return
376    /// false for `XFieldElement(7) == BFieldElement(7)`
377    fn eq(&self, other: &Self) -> bool {
378        self.expression == other.expression
379    }
380}
381
382impl<II: InputIndicator> Display for ConstraintCircuit<II> {
383    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
384        match &self.expression {
385            CircuitExpression::XConst(xfe) => write!(f, "{xfe}"),
386            CircuitExpression::BConst(bfe) => write!(f, "{bfe}"),
387            CircuitExpression::Input(input) => write!(f, "{input} "),
388            CircuitExpression::Challenge(self_challenge_idx) => write!(f, "{self_challenge_idx}"),
389            CircuitExpression::BinOp(operation, lhs, rhs) => {
390                write!(f, "({}) {operation} ({})", lhs.borrow(), rhs.borrow())
391            }
392        }
393    }
394}
395
396/// An in-order iterator over [`RcCircuit`]s.
397//
398// While Morris traversal could be used, the speedup gained by this relatively
399// simple implementation is deemed enough (at the time of writing).
400#[derive(Debug, Clone)]
401struct ConstraintCircuitIter<II: InputIndicator> {
402    stack: Vec<RcCircuit<II>>,
403}
404
405impl<II: InputIndicator> Iterator for ConstraintCircuitIter<II> {
406    type Item = RcCircuit<II>;
407
408    fn next(&mut self) -> Option<Self::Item> {
409        let to_yield = self.stack.pop()?;
410        if let CircuitExpression::BinOp(_, _, ref rhs) = to_yield.borrow().expression {
411            self.push_left_lineage_to_stack(Rc::clone(rhs));
412        }
413
414        Some(to_yield)
415    }
416}
417
418impl<II: InputIndicator> ConstraintCircuitIter<II> {
419    fn new(root: ConstraintCircuit<II>) -> Self {
420        let mut iterator = Self { stack: Vec::new() };
421        iterator.push_left_lineage_to_stack(Rc::new(RefCell::new(root)));
422
423        iterator
424    }
425
426    /// Push the given node, its left child, _its_ left child, and so on, to
427    /// the stack.
428    fn push_left_lineage_to_stack(&mut self, current: RcCircuit<II>) {
429        let mut current = Some(current);
430        while let Some(new_node) = current.take() {
431            if let CircuitExpression::BinOp(_, ref lhs, _) = new_node.borrow().expression {
432                current = Some(Rc::clone(lhs));
433            }
434            self.stack.push(new_node);
435        }
436    }
437}
438
439impl<II: InputIndicator> ConstraintCircuit<II> {
440    fn new(id: u64, expression: CircuitExpression<II>) -> Self {
441        Self {
442            id,
443            ref_count: 0,
444            expression,
445        }
446    }
447
448    /// Iterate all nodes in the tree in order.
449    fn into_iter(self) -> ConstraintCircuitIter<II> {
450        ConstraintCircuitIter::new(self)
451    }
452
453    /// Reset the reference counters for the entire subtree
454    fn reset_ref_count_for_tree(&mut self) {
455        self.ref_count = 0;
456
457        if let CircuitExpression::BinOp(_, lhs, rhs) = &self.expression {
458            lhs.borrow_mut().reset_ref_count_for_tree();
459            rhs.borrow_mut().reset_ref_count_for_tree();
460        }
461    }
462
463    /// Assert that all IDs in the subtree are unique.
464    ///
465    /// # Panics
466    ///
467    /// Panics if a duplicate ID is found.
468    fn assert_unique_ids_inner(&mut self, ids: &mut HashMap<u64, ConstraintCircuit<II>>) {
469        // try to detect duplicate IDs only once for this node
470        if self.ref_count == 0 {
471            let id = self.id;
472            if let Some(other) = ids.insert(id, self.clone()) {
473                panic!("Repeated ID: {id}\nSelf:\n{self}\n{self:?}\nOther:\n{other}\n{other:?}");
474            }
475        }
476
477        // recurse in either case to correctly update ref_rount
478        self.ref_count += 1;
479        if let CircuitExpression::BinOp(_, lhs, rhs) = &self.expression {
480            lhs.borrow_mut().assert_unique_ids_inner(ids);
481            rhs.borrow_mut().assert_unique_ids_inner(ids);
482        }
483    }
484
485    /// Assert that a multicircuit has unique IDs.
486    /// Also determines how often each node is referenced, updating the
487    /// respective `ref_count`s.
488    ///
489    /// # Panics
490    ///
491    /// Panics if a duplicate ID is found.
492    pub fn assert_unique_ids(constraints: &mut [ConstraintCircuit<II>]) {
493        // inner uniqueness checks relies on reference counters being 0 for
494        // unseen nodes
495        for circuit in constraints.iter_mut() {
496            circuit.reset_ref_count_for_tree();
497        }
498        let mut ids = HashMap::new();
499        for circuit in constraints.iter_mut() {
500            circuit.assert_unique_ids_inner(&mut ids);
501        }
502    }
503
504    /// Return degree of the multivariate polynomial represented by this circuit
505    pub fn degree(&self) -> isize {
506        if self.is_zero() {
507            return -1;
508        }
509
510        match &self.expression {
511            CircuitExpression::BinOp(binop, lhs, rhs) => {
512                let degree_lhs = lhs.borrow().degree();
513                let degree_rhs = rhs.borrow().degree();
514                let degree_additive = cmp::max(degree_lhs, degree_rhs);
515                let degree_multiplicative = if cmp::min(degree_lhs, degree_rhs) <= -1 {
516                    -1
517                } else {
518                    degree_lhs + degree_rhs
519                };
520                match binop {
521                    BinOp::Add => degree_additive,
522                    BinOp::Mul => degree_multiplicative,
523                }
524            }
525            CircuitExpression::Input(_) => 1,
526            CircuitExpression::BConst(_)
527            | CircuitExpression::XConst(_)
528            | CircuitExpression::Challenge(_) => 0,
529        }
530    }
531
532    /// All unique reference counters in the subtree, sorted ascendingly.
533    pub fn all_ref_counters(&self) -> Vec<usize> {
534        let mut ref_counters = Vec::new();
535        self.all_ref_counters_inner(&mut ref_counters);
536        ref_counters.sort_unstable();
537        ref_counters.dedup();
538
539        ref_counters
540    }
541
542    fn all_ref_counters_inner(&self, ref_counters: &mut Vec<usize>) {
543        ref_counters.push(self.ref_count);
544        if let CircuitExpression::BinOp(_, lhs, rhs) = &self.expression {
545            lhs.borrow().all_ref_counters_inner(ref_counters);
546            rhs.borrow().all_ref_counters_inner(ref_counters);
547        };
548    }
549
550    /// Is the node the constant 0?
551    /// Does not catch composite expressions that will always evaluate to zero,
552    /// like `0·a`.
553    pub fn is_zero(&self) -> bool {
554        match self.expression {
555            CircuitExpression::BConst(bfe) => bfe.is_zero(),
556            CircuitExpression::XConst(xfe) => xfe.is_zero(),
557            _ => false,
558        }
559    }
560
561    /// Is the node the constant 1?
562    /// Does not catch composite expressions that will always evaluate to one,
563    /// like `1·1`.
564    pub fn is_one(&self) -> bool {
565        match self.expression {
566            CircuitExpression::BConst(bfe) => bfe.is_one(),
567            CircuitExpression::XConst(xfe) => xfe.is_one(),
568            _ => false,
569        }
570    }
571
572    pub fn is_neg_one(&self) -> bool {
573        match self.expression {
574            CircuitExpression::BConst(bfe) => (-bfe).is_one(),
575            CircuitExpression::XConst(xfe) => (-xfe).is_one(),
576            _ => false,
577        }
578    }
579
580    /// Recursively check whether this node is composed of only BFieldElements,
581    /// i.e., only uses
582    /// 1. inputs from main rows,
583    /// 2. constants from the B-field, and
584    /// 3. binary operations on BFieldElements.
585    pub fn evaluates_to_base_element(&self) -> bool {
586        match &self.expression {
587            CircuitExpression::BConst(_) => true,
588            CircuitExpression::XConst(_) => false,
589            CircuitExpression::Input(indicator) => indicator.is_main_table_column(),
590            CircuitExpression::Challenge(_) => false,
591            CircuitExpression::BinOp(_, lhs, rhs) => {
592                lhs.borrow().evaluates_to_base_element() && rhs.borrow().evaluates_to_base_element()
593            }
594        }
595    }
596
597    pub fn evaluate(
598        &self,
599        main_table: ArrayView2<BFieldElement>,
600        aux_table: ArrayView2<XFieldElement>,
601        challenges: &[XFieldElement],
602    ) -> XFieldElement {
603        match &self.expression {
604            CircuitExpression::BConst(bfe) => bfe.lift(),
605            CircuitExpression::XConst(xfe) => *xfe,
606            CircuitExpression::Input(input) => input.evaluate(main_table, aux_table),
607            CircuitExpression::Challenge(challenge_id) => challenges[*challenge_id],
608            CircuitExpression::BinOp(binop, lhs, rhs) => {
609                let lhs_value = lhs.borrow().evaluate(main_table, aux_table, challenges);
610                let rhs_value = rhs.borrow().evaluate(main_table, aux_table, challenges);
611                binop.operation(lhs_value, rhs_value)
612            }
613        }
614    }
615}
616
617/// [`ConstraintCircuit`] with extra context pertaining to the whole
618/// multicircuit.
619///
620/// This context is needed to ensure that two equal nodes (meaning: same
621/// expression) are not added to the multicircuit. It also enables a rudimentary
622/// check for node equivalence (commutation + constant folding), in which case
623/// the existing expression is used instead.
624///
625/// One can create new instances of [`ConstraintCircuitMonad`] by applying
626/// arithmetic operations to existing instances, *e.g.*, `let c = a * b;`.
627#[derive(Clone)]
628pub struct ConstraintCircuitMonad<II: InputIndicator> {
629    pub circuit: RcCircuit<II>,
630    pub builder: ConstraintCircuitBuilder<II>,
631}
632
633impl<II: InputIndicator> Debug for ConstraintCircuitMonad<II> {
634    // `all_nodes` contains itself, leading to infinite recursion during `Debug`
635    // printing. Hence, this manual implementation.
636    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
637        f.debug_struct("ConstraintCircuitMonad")
638            .field("id", &self.circuit)
639            .field("all_nodes length: ", &self.builder.all_nodes.borrow().len())
640            .field("id_counter_ref value: ", &self.builder.id_counter.borrow())
641            .finish()
642    }
643}
644
645impl<II: InputIndicator> Display for ConstraintCircuitMonad<II> {
646    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
647        write!(f, "{}", self.circuit.borrow())
648    }
649}
650
651impl<II: InputIndicator> PartialEq for ConstraintCircuitMonad<II> {
652    // Equality for the ConstraintCircuitMonad is defined by the circuit, not
653    // the other metadata (e.g. ID) that it carries around.
654    fn eq(&self, other: &Self) -> bool {
655        self.circuit == other.circuit
656    }
657}
658
659impl<II: InputIndicator> Eq for ConstraintCircuitMonad<II> {}
660
661/// Helper function for binary operations that are used to generate new parent
662/// nodes in the multitree that represents the algebraic circuit. Ensures that
663/// each newly created node has a unique ID.
664///
665/// This function does not (currently) catch expressions of the form ((x+1)+1).
666fn binop<II: InputIndicator>(
667    binop: BinOp,
668    lhs: ConstraintCircuitMonad<II>,
669    rhs: ConstraintCircuitMonad<II>,
670) -> ConstraintCircuitMonad<II> {
671    assert!(lhs.builder.is_same_as(&rhs.builder));
672
673    match (binop, &lhs, &rhs) {
674        (BinOp::Add, _, zero) if zero.circuit.borrow().is_zero() => return lhs,
675        (BinOp::Add, zero, _) if zero.circuit.borrow().is_zero() => return rhs,
676        (BinOp::Mul, _, one) if one.circuit.borrow().is_one() => return lhs,
677        (BinOp::Mul, one, _) if one.circuit.borrow().is_one() => return rhs,
678        (BinOp::Mul, _, zero) if zero.circuit.borrow().is_zero() => return rhs,
679        (BinOp::Mul, zero, _) if zero.circuit.borrow().is_zero() => return lhs,
680        _ => (),
681    };
682
683    match (
684        &lhs.circuit.borrow().expression,
685        &rhs.circuit.borrow().expression,
686    ) {
687        (&CircuitExpression::BConst(l), &CircuitExpression::BConst(r)) => {
688            return lhs.builder.b_constant(binop.operation(l, r));
689        }
690        (&CircuitExpression::BConst(l), &CircuitExpression::XConst(r)) => {
691            return lhs.builder.x_constant(binop.operation(l, r));
692        }
693        (&CircuitExpression::XConst(l), &CircuitExpression::BConst(r)) => {
694            return lhs.builder.x_constant(binop.operation(l, r));
695        }
696        (&CircuitExpression::XConst(l), &CircuitExpression::XConst(r)) => {
697            return lhs.builder.x_constant(binop.operation(l, r));
698        }
699        _ => (),
700    };
701
702    // all `BinOp`s are commutative – try both orders of the operands
703    let all_nodes = &mut lhs.builder.all_nodes.borrow_mut();
704    let new_node = binop_new_node(binop, &rhs, &lhs);
705    if let Some(node) = all_nodes.values().find(|&n| n == &new_node) {
706        return node.to_owned();
707    }
708
709    let new_node = binop_new_node(binop, &lhs, &rhs);
710    if let Some(node) = all_nodes.values().find(|&n| n == &new_node) {
711        return node.to_owned();
712    }
713
714    let new_id = new_node.circuit.borrow().id;
715    let old = all_nodes.insert(new_id, new_node.clone());
716    assert!(old.is_none(), "new node must not overwrite existing node");
717    *lhs.builder.id_counter.borrow_mut() += 1;
718
719    new_node
720}
721
722fn binop_new_node<II: InputIndicator>(
723    binop: BinOp,
724    lhs: &ConstraintCircuitMonad<II>,
725    rhs: &ConstraintCircuitMonad<II>,
726) -> ConstraintCircuitMonad<II> {
727    let id = lhs.builder.id_counter.borrow().to_owned();
728    let expression = CircuitExpression::BinOp(binop, lhs.circuit.clone(), rhs.circuit.clone());
729    let circuit = ConstraintCircuit::new(id, expression);
730
731    lhs.builder.new_monad(circuit)
732}
733
734impl<II: InputIndicator> Add for ConstraintCircuitMonad<II> {
735    type Output = ConstraintCircuitMonad<II>;
736
737    fn add(self, rhs: Self) -> Self::Output {
738        binop(BinOp::Add, self, rhs)
739    }
740}
741
742impl<II: InputIndicator> Sub for ConstraintCircuitMonad<II> {
743    type Output = ConstraintCircuitMonad<II>;
744
745    fn sub(self, rhs: Self) -> Self::Output {
746        binop(BinOp::Add, self, -rhs)
747    }
748}
749
750impl<II: InputIndicator> Mul for ConstraintCircuitMonad<II> {
751    type Output = ConstraintCircuitMonad<II>;
752
753    fn mul(self, rhs: Self) -> Self::Output {
754        binop(BinOp::Mul, self, rhs)
755    }
756}
757
758impl<II: InputIndicator> Neg for ConstraintCircuitMonad<II> {
759    type Output = ConstraintCircuitMonad<II>;
760
761    fn neg(self) -> Self::Output {
762        binop(BinOp::Mul, self.builder.minus_one(), self)
763    }
764}
765
766/// This will panic if the iterator is empty because the neutral element needs a
767/// unique ID, and we have no way of getting that here.
768impl<II: InputIndicator> Sum for ConstraintCircuitMonad<II> {
769    fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
770        iter.reduce(|accum, item| accum + item)
771            .expect("ConstraintCircuitMonad Iterator was empty")
772    }
773}
774
775struct EvolvingMainConstraintsNumber(usize);
776impl From<EvolvingMainConstraintsNumber> for usize {
777    fn from(value: EvolvingMainConstraintsNumber) -> Self {
778        value.0
779    }
780}
781
782struct EvolvingAuxConstraintsNumber(usize);
783impl From<EvolvingAuxConstraintsNumber> for usize {
784    fn from(value: EvolvingAuxConstraintsNumber) -> Self {
785        value.0
786    }
787}
788
789impl<II: InputIndicator> ConstraintCircuitMonad<II> {
790    /// Unwrap a ConstraintCircuitMonad to reveal its inner ConstraintCircuit
791    pub fn consume(&self) -> ConstraintCircuit<II> {
792        self.circuit.borrow().to_owned()
793    }
794
795    /// Lower the degree of a given multicircuit to the target degree.
796    /// This is achieved by introducing additional variables and constraints.
797    /// The appropriate substitutions are applied to the given multicircuit.
798    /// The target degree must be greater than 1.
799    ///
800    /// The new constraints are returned as two vector of
801    /// ConstraintCircuitMonads: the first corresponds to main columns and
802    /// constraints, the second to auxiliary columns and constraints. The
803    /// modifications are applied to the function argument in-place.
804    ///
805    /// Each returned constraint is guaranteed to correspond to some
806    /// `CircuitExpression::BinaryOperation(BinOp::Sub, lhs, rhs)` where
807    /// - `lhs` is the new variable, and
808    /// - `rhs` is the (sub)circuit replaced by `lhs`. These can then be used to
809    ///   construct new columns, as well as derivation rules for filling those
810    ///   new columns.
811    ///
812    /// For example, starting with the constraint set {x^4}, we insert
813    /// {y - x^2} and modify in-place (x^4) --> (y^2).
814    ///
815    /// The highest index of main and auxiliary columns used by the multicircuit
816    /// have to be provided. The uniqueness of the new columns' indices
817    /// depends on these provided values. Note that these indices are
818    /// generally not equal to the number of used columns, especially when a
819    /// tables' constraints are built using the master table's column indices.
820    pub fn lower_to_degree(
821        multicircuit: &mut [Self],
822        info: DegreeLoweringInfo,
823    ) -> (Vec<Self>, Vec<Self>) {
824        let target_degree = info.target_degree;
825        assert!(
826            target_degree > 1,
827            "Target degree must be greater than 1. Got {target_degree}."
828        );
829
830        let mut main_constraints = vec![];
831        let mut aux_constraints = vec![];
832
833        if multicircuit.is_empty() {
834            return (main_constraints, aux_constraints);
835        }
836
837        while Self::multicircuit_degree(multicircuit) > target_degree {
838            let chosen_node_id = Self::pick_node_to_substitute(multicircuit, target_degree);
839
840            let new_constraint = Self::apply_substitution(
841                multicircuit,
842                info,
843                chosen_node_id,
844                EvolvingMainConstraintsNumber(main_constraints.len()),
845                EvolvingAuxConstraintsNumber(aux_constraints.len()),
846            );
847
848            if new_constraint.circuit.borrow().evaluates_to_base_element() {
849                main_constraints.push(new_constraint)
850            } else {
851                aux_constraints.push(new_constraint)
852            }
853        }
854
855        (main_constraints, aux_constraints)
856    }
857
858    /// Apply a substitution:
859    ///  - create a new variable to replaces the chosen node;
860    ///  - make all nodes that point to the chosen node point to the new
861    ///    variable instead;
862    ///  - return the new constraint that makes it sound: new variable minus
863    ///    chosen node's expression.
864    fn apply_substitution(
865        multicircuit: &mut [Self],
866        info: DegreeLoweringInfo,
867        chosen_node_id: u64,
868        new_main_constraints_count: EvolvingMainConstraintsNumber,
869        new_aux_constraints_count: EvolvingAuxConstraintsNumber,
870    ) -> ConstraintCircuitMonad<II> {
871        let builder = multicircuit[0].builder.clone();
872
873        // Create a new variable.
874        let chosen_node = builder.all_nodes.borrow()[&chosen_node_id].clone();
875        let chosen_node_is_main_col = chosen_node.circuit.borrow().evaluates_to_base_element();
876        let new_input_indicator = if chosen_node_is_main_col {
877            let new_main_col_idx = info.num_main_cols + usize::from(new_main_constraints_count);
878            II::main_table_input(new_main_col_idx)
879        } else {
880            let new_aux_col_idx = info.num_aux_cols + usize::from(new_aux_constraints_count);
881            II::aux_table_input(new_aux_col_idx)
882        };
883        let new_variable = builder.input(new_input_indicator);
884
885        // Point all descendants of the chosen node to the new variable instead
886        builder.redirect_all_references_to_node(chosen_node_id, new_variable.clone());
887
888        // Treat roots of the multicircuit explicitly.
889        for circuit in multicircuit.iter_mut() {
890            if circuit.circuit.borrow().id == chosen_node_id {
891                circuit.circuit = new_variable.circuit.clone();
892            }
893        }
894
895        // return substitution equation
896        new_variable - chosen_node
897    }
898
899    /// Heuristically pick a node from the given multicircuit that is to be
900    /// substituted with a new variable. The ID of the chosen node is
901    /// returned.
902    fn pick_node_to_substitute(
903        multicircuit: &[ConstraintCircuitMonad<II>],
904        target_degree: isize,
905    ) -> u64 {
906        assert!(!multicircuit.is_empty());
907        let multicircuit = multicircuit
908            .iter()
909            .map(|c| c.clone().consume())
910            .collect_vec();
911
912        // Computing all node degree is slow; this cache de-duplicates work.
913        let node_degrees = Self::all_nodes_in_multicircuit(&multicircuit)
914            .map(|node| (node.borrow().id, node.borrow().degree()))
915            .collect::<HashMap<_, _>>();
916
917        // Only nodes with degree > target_degree need changing.
918        let high_degree_nodes = Self::all_nodes_in_multicircuit(&multicircuit)
919            .filter(|node| node_degrees[&node.borrow().id] > target_degree)
920            .map(|node| node.borrow().clone())
921            .unique()
922            .collect_vec();
923
924        // Collect all candidates for substitution, i.e., descendents of
925        // high_degree_nodes with degree <= target_degree. Substituting a node
926        // of degree 1 is both pointless and can lead to infinite iteration.
927        let low_degree_nodes = Self::all_nodes_in_multicircuit(&high_degree_nodes)
928            .map(|node| node.borrow().id)
929            .filter(|id| 1 < node_degrees[id] && node_degrees[id] <= target_degree)
930            .collect_vec();
931
932        // If the resulting list is empty, there is no way forward.
933        assert!(!low_degree_nodes.is_empty(), "Cannot lower degree.");
934
935        // Of the remaining nodes, keep the ones occurring the most often.
936        let mut nodes_and_occurrences = HashMap::new();
937        for node in low_degree_nodes {
938            *nodes_and_occurrences.entry(node).or_insert(0) += 1;
939        }
940        let max_occurrences = nodes_and_occurrences.iter().map(|(_, &c)| c).max().unwrap();
941        nodes_and_occurrences.retain(|_, &mut count| count == max_occurrences);
942        let mut candidate_node_ids = nodes_and_occurrences.keys().copied().collect_vec();
943
944        // If there are still multiple nodes, pick the one with the highest
945        // degree.
946        let max_degree = candidate_node_ids
947            .iter()
948            .map(|node_id| node_degrees[node_id])
949            .max()
950            .unwrap();
951        candidate_node_ids.retain(|node_id| node_degrees[node_id] == max_degree);
952
953        candidate_node_ids.sort_unstable();
954
955        // If there are still multiple nodes, pick any one – but
956        // deterministically so.
957        candidate_node_ids.into_iter().min().unwrap()
958    }
959
960    /// Returns all nodes used in the multicircuit.
961    ///
962    /// This is distinct from `ConstraintCircuitBuilder::all_nodes` because it
963    /// 1. only considers nodes used in the given multicircuit, not all nodes in
964    ///    the builder,
965    /// 2. returns the nodes as [`ConstraintCircuit`]s, not as
966    ///    [`ConstraintCircuitMonad`]s, and
967    /// 3. keeps duplicates, allowing to count how often a node occurs.
968    pub fn all_nodes_in_multicircuit(
969        multicircuit: &[ConstraintCircuit<II>],
970    ) -> impl Iterator<Item = RcCircuit<II>> + Clone {
971        multicircuit.iter().flat_map(|cc| cc.clone().into_iter())
972    }
973
974    /// Counts the number of nodes in this multicircuit. Only counts nodes that
975    /// are used; not nodes that have been forgotten.
976    pub fn num_visible_nodes(constraints: &[Self]) -> usize {
977        constraints
978            .iter()
979            .flat_map(|ccm| ccm.circuit.borrow().clone().into_iter())
980            .map(|circuit| circuit.borrow().to_owned())
981            .unique()
982            .count()
983    }
984
985    /// Returns the maximum degree of all circuits in the multicircuit.
986    pub fn multicircuit_degree(multicircuit: &[ConstraintCircuitMonad<II>]) -> isize {
987        multicircuit
988            .iter()
989            .map(|circuit| circuit.circuit.borrow().degree())
990            .max()
991            .unwrap_or(-1)
992    }
993}
994
995/// Helper struct to construct new leaf nodes (*i.e.*, input or challenge or
996/// constant) in the circuit multitree. Ensures that newly created nodes, even
997/// non-leaf nodes created through joining two other nodes using an arithmetic
998/// operation, get a unique ID.
999#[derive(Debug, Clone, Eq, PartialEq)]
1000pub struct ConstraintCircuitBuilder<II: InputIndicator> {
1001    id_counter: Rc<RefCell<u64>>,
1002    all_nodes: Rc<RefCell<HashMap<u64, ConstraintCircuitMonad<II>>>>,
1003}
1004
1005impl<II: InputIndicator> Default for ConstraintCircuitBuilder<II> {
1006    fn default() -> Self {
1007        Self::new()
1008    }
1009}
1010
1011impl<II: InputIndicator> ConstraintCircuitBuilder<II> {
1012    pub fn new() -> Self {
1013        Self {
1014            id_counter: Rc::new(RefCell::new(0)),
1015            all_nodes: Rc::new(RefCell::new(HashMap::new())),
1016        }
1017    }
1018
1019    /// Check whether two builders are the same.
1020    ///
1021    /// Notably, this is distinct from checking equality: two builders are equal
1022    /// if they are in the same state. Two builders are the same if they are
1023    /// the same instance.
1024    pub fn is_same_as(&self, other: &Self) -> bool {
1025        Rc::ptr_eq(&self.id_counter, &other.id_counter)
1026            && Rc::ptr_eq(&self.all_nodes, &other.all_nodes)
1027    }
1028
1029    fn new_monad(&self, circuit: ConstraintCircuit<II>) -> ConstraintCircuitMonad<II> {
1030        let circuit = Rc::new(RefCell::new(circuit));
1031        ConstraintCircuitMonad {
1032            circuit,
1033            builder: self.clone(),
1034        }
1035    }
1036
1037    /// The unique monad representing the constant value 0.
1038    pub fn zero(&self) -> ConstraintCircuitMonad<II> {
1039        self.b_constant(0)
1040    }
1041
1042    /// The unique monad representing the constant value 1.
1043    pub fn one(&self) -> ConstraintCircuitMonad<II> {
1044        self.b_constant(1)
1045    }
1046
1047    /// The unique monad representing the constant value -1.
1048    pub fn minus_one(&self) -> ConstraintCircuitMonad<II> {
1049        self.b_constant(-1)
1050    }
1051
1052    /// Leaf node with constant over the [base field][BFieldElement].
1053    pub fn b_constant<B>(&self, bfe: B) -> ConstraintCircuitMonad<II>
1054    where
1055        B: Into<BFieldElement>,
1056    {
1057        self.make_leaf(CircuitExpression::BConst(bfe.into()))
1058    }
1059
1060    /// Leaf node with constant over the [extension field][XFieldElement].
1061    pub fn x_constant<X>(&self, xfe: X) -> ConstraintCircuitMonad<II>
1062    where
1063        X: Into<XFieldElement>,
1064    {
1065        self.make_leaf(CircuitExpression::XConst(xfe.into()))
1066    }
1067
1068    /// Create deterministic input leaf node.
1069    pub fn input(&self, input: II) -> ConstraintCircuitMonad<II> {
1070        self.make_leaf(CircuitExpression::Input(input))
1071    }
1072
1073    /// Create challenge leaf node.
1074    pub fn challenge<C>(&self, challenge: C) -> ConstraintCircuitMonad<II>
1075    where
1076        C: Into<usize>,
1077    {
1078        self.make_leaf(CircuitExpression::Challenge(challenge.into()))
1079    }
1080
1081    fn make_leaf(&self, mut expression: CircuitExpression<II>) -> ConstraintCircuitMonad<II> {
1082        assert!(
1083            !matches!(expression, CircuitExpression::BinOp(_, _, _)),
1084            "`make_leaf` is intended for anything but `BinOp`s"
1085        );
1086
1087        // don't use X field if the B field suffices
1088        if let CircuitExpression::XConst(xfe) = expression
1089            && let Some(bfe) = xfe.unlift()
1090        {
1091            expression = CircuitExpression::BConst(bfe);
1092        }
1093
1094        let id = self.id_counter.borrow().to_owned();
1095        let circuit = ConstraintCircuit::new(id, expression);
1096        let new_node = self.new_monad(circuit);
1097
1098        if let Some(same_node) = self.all_nodes.borrow().values().find(|&n| n == &new_node) {
1099            return same_node.to_owned();
1100        }
1101
1102        let old = self.all_nodes.borrow_mut().insert(id, new_node.clone());
1103        assert!(old.is_none(), "Leaf-created node must be new… {new_node}");
1104        *self.id_counter.borrow_mut() += 1;
1105
1106        new_node
1107    }
1108
1109    /// Replace all pointers to a given node (identified by `old_id`) by one
1110    /// to the new node.
1111    ///
1112    /// A circuit's root node cannot be substituted with this method. Manual
1113    /// care must be taken to update the root node if necessary.
1114    fn redirect_all_references_to_node(&self, old_id: u64, new: ConstraintCircuitMonad<II>) {
1115        self.all_nodes.borrow_mut().remove(&old_id);
1116        for node in self.all_nodes.borrow_mut().values_mut() {
1117            let CircuitExpression::BinOp(_, ref mut left, ref mut right) =
1118                node.circuit.borrow_mut().expression
1119            else {
1120                continue;
1121            };
1122            if left.borrow().id == old_id {
1123                *left = new.circuit.clone();
1124            }
1125            if right.borrow().id == old_id {
1126                *right = new.circuit.clone();
1127            }
1128        }
1129    }
1130}
1131
1132impl<'a, II: InputIndicator + Arbitrary<'a>> Arbitrary<'a> for ConstraintCircuitMonad<II> {
1133    fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
1134        let builder = ConstraintCircuitBuilder::new();
1135        let mut random_circuit = random_circuit_leaf(&builder, u)?;
1136
1137        let num_nodes_in_circuit = u.arbitrary_len::<Self>()?;
1138        for _ in 0..num_nodes_in_circuit {
1139            let leaf = random_circuit_leaf(&builder, u)?;
1140            match u.int_in_range(0..=5)? {
1141                0 => random_circuit = random_circuit * leaf,
1142                1 => random_circuit = random_circuit + leaf,
1143                2 => random_circuit = random_circuit - leaf,
1144                3 => random_circuit = leaf * random_circuit,
1145                4 => random_circuit = leaf + random_circuit,
1146                5 => random_circuit = leaf - random_circuit,
1147                _ => unreachable!(),
1148            }
1149        }
1150
1151        Ok(random_circuit)
1152    }
1153}
1154
1155fn random_circuit_leaf<'a, II: InputIndicator + Arbitrary<'a>>(
1156    builder: &ConstraintCircuitBuilder<II>,
1157    u: &mut Unstructured<'a>,
1158) -> arbitrary::Result<ConstraintCircuitMonad<II>> {
1159    let leaf = match u.int_in_range(0..=5)? {
1160        0 => builder.input(u.arbitrary()?),
1161        1 => builder.challenge(u.arbitrary::<usize>()?),
1162        2 => builder.b_constant(u.arbitrary::<BFieldElement>()?),
1163        3 => builder.x_constant(u.arbitrary::<XFieldElement>()?),
1164        4 => builder.one(),
1165        5 => builder.zero(),
1166        _ => unreachable!(),
1167    };
1168    Ok(leaf)
1169}
1170
1171#[cfg(test)]
1172#[cfg_attr(coverage_nightly, coverage(off))]
1173mod tests {
1174    use std::collections::hash_map::DefaultHasher;
1175    use std::hash::Hasher;
1176    use std::ops::Not;
1177
1178    use itertools::Itertools;
1179    use ndarray::Array2;
1180    use ndarray::Axis;
1181    use proptest::arbitrary::Arbitrary;
1182    use proptest::collection::vec;
1183    use proptest::prelude::*;
1184    use proptest_arbitrary_adapter::arb;
1185    use test_strategy::proptest;
1186
1187    use super::*;
1188
1189    impl<II: InputIndicator> ConstraintCircuitMonad<II> {
1190        /// Check whether the given node is contained in this circuit.
1191        fn contains(&self, other: &Self) -> bool {
1192            let self_expression = &self.circuit.borrow().expression;
1193            let other_expression = &other.circuit.borrow().expression;
1194            self_expression.contains(other_expression)
1195        }
1196
1197        /// Produces an iter over all nodes in the multicircuit, if it is
1198        /// non-empty.
1199        ///
1200        /// Helper function for counting the number of nodes of a specific type.
1201        fn iter_nodes(
1202            constraints: &[Self],
1203        ) -> std::vec::IntoIter<(u64, ConstraintCircuitMonad<II>)> {
1204            let Some(first) = constraints.first() else {
1205                return vec![].into_iter();
1206            };
1207
1208            first
1209                .builder
1210                .all_nodes
1211                .borrow()
1212                .iter()
1213                .map(|(n, m)| (*n, m.clone()))
1214                .collect_vec()
1215                .into_iter()
1216        }
1217
1218        /// The total number of nodes in the multicircuit
1219        fn num_nodes(constraints: &[Self]) -> usize {
1220            Self::iter_nodes(constraints).count()
1221        }
1222
1223        /// Determine if the constraint circuit monad corresponds to a main
1224        /// table column.
1225        fn is_main_table_column(&self) -> bool {
1226            let CircuitExpression::Input(ii) = self.circuit.borrow().expression else {
1227                return false;
1228            };
1229
1230            ii.is_main_table_column()
1231        }
1232
1233        /// The number of inputs from the main table
1234        fn num_main_inputs(constraints: &[Self]) -> usize {
1235            Self::iter_nodes(constraints)
1236                .filter(|(_, cc)| cc.is_main_table_column())
1237                .filter(|(_, cc)| cc.circuit.borrow().evaluates_to_base_element())
1238                .count()
1239        }
1240
1241        /// The number of inputs from the aux table
1242        fn num_aux_inputs(constraints: &[Self]) -> usize {
1243            Self::iter_nodes(constraints)
1244                .filter(|(_, cc)| !cc.is_main_table_column())
1245                .filter(|(_, cc)| {
1246                    matches!(cc.circuit.borrow().expression, CircuitExpression::Input(_))
1247                })
1248                .count()
1249        }
1250
1251        /// The number of total (*i.e.*, main + aux) inputs
1252        fn num_inputs(constraints: &[Self]) -> usize {
1253            Self::num_main_inputs(constraints) + Self::num_aux_inputs(constraints)
1254        }
1255
1256        /// The number of challenges
1257        fn num_challenges(constraints: &[Self]) -> usize {
1258            Self::iter_nodes(constraints)
1259                .filter(|(_, cc)| {
1260                    matches!(
1261                        cc.circuit.borrow().expression,
1262                        CircuitExpression::Challenge(_)
1263                    )
1264                })
1265                .count()
1266        }
1267
1268        /// The number of `BinOp`s
1269        fn num_binops(constraints: &[Self]) -> usize {
1270            Self::iter_nodes(constraints)
1271                .filter(|(_, cc)| {
1272                    matches!(
1273                        cc.circuit.borrow().expression,
1274                        CircuitExpression::BinOp(_, _, _)
1275                    )
1276                })
1277                .count()
1278        }
1279
1280        /// The number of BFE constants
1281        fn num_bfield_constants(constraints: &[Self]) -> usize {
1282            Self::iter_nodes(constraints)
1283                .filter(|(_, cc)| {
1284                    matches!(cc.circuit.borrow().expression, CircuitExpression::BConst(_))
1285                })
1286                .count()
1287        }
1288
1289        /// The number of XFE constants
1290        fn num_xfield_constants(constraints: &[Self]) -> usize {
1291            Self::iter_nodes(constraints)
1292                .filter(|(_, cc)| {
1293                    matches!(
1294                        cc.circuit.as_ref().borrow().expression,
1295                        CircuitExpression::XConst(_)
1296                    )
1297                })
1298                .count()
1299        }
1300    }
1301
1302    impl<II: InputIndicator> CircuitExpression<II> {
1303        /// Check whether the given node is contained in this circuit.
1304        fn contains(&self, other: &Self) -> bool {
1305            if self == other {
1306                return true;
1307            }
1308            let CircuitExpression::BinOp(_, lhs, rhs) = self else {
1309                return false;
1310            };
1311
1312            lhs.borrow().expression.contains(other) || rhs.borrow().expression.contains(other)
1313        }
1314    }
1315
1316    /// The [`Hash`] trait requires:
1317    /// circuit_0 == circuit_1 => hash(circuit_0) == hash(circuit_1)
1318    ///
1319    /// By De-Morgan's law, this is equivalent to the more meaningful test:
1320    /// hash(circuit_0) != hash(circuit_1) => circuit_0 != circuit_1
1321    #[proptest]
1322    fn unequal_hash_implies_unequal_constraint_circuit_monad(
1323        #[strategy(arb())] circuit_0: ConstraintCircuitMonad<SingleRowIndicator>,
1324        #[strategy(arb())] circuit_1: ConstraintCircuitMonad<SingleRowIndicator>,
1325    ) {
1326        if hash_circuit(&circuit_0) != hash_circuit(&circuit_1) {
1327            prop_assert_ne!(circuit_0, circuit_1);
1328        }
1329    }
1330
1331    /// The hash of a node may not depend on `ref_count`, `counter`,
1332    /// `id_counter_ref`, or `all_nodes`, since `all_nodes` contains the
1333    /// digest of all nodes in the multi tree. For more details, see
1334    /// [`HashSet`].
1335    #[proptest]
1336    fn multi_circuit_hash_is_unchanged_by_meta_data(
1337        #[strategy(arb())] circuit: ConstraintCircuitMonad<DualRowIndicator>,
1338        new_ref_count: usize,
1339        new_id_counter: u64,
1340    ) {
1341        let original_digest = hash_circuit(&circuit);
1342
1343        circuit.circuit.borrow_mut().ref_count = new_ref_count;
1344        prop_assert_eq!(original_digest, hash_circuit(&circuit));
1345
1346        circuit.builder.id_counter.replace(new_id_counter);
1347        prop_assert_eq!(original_digest, hash_circuit(&circuit));
1348    }
1349
1350    fn hash_circuit<II: InputIndicator>(circuit: &ConstraintCircuitMonad<II>) -> u64 {
1351        let mut hasher = DefaultHasher::new();
1352        circuit.hash(&mut hasher);
1353        hasher.finish()
1354    }
1355
1356    #[test]
1357    fn printing_constraint_circuit_gives_expected_strings() {
1358        let builder = ConstraintCircuitBuilder::new();
1359        assert_eq!("1", builder.b_constant(1).to_string());
1360        assert_eq!(
1361            "main_row[5] ",
1362            builder.input(SingleRowIndicator::Main(5)).to_string()
1363        );
1364        assert_eq!("6", builder.challenge(6_usize).to_string());
1365
1366        let xfe_str = builder.x_constant([2, 3, 4]).to_string();
1367        assert_eq!("(4·x² + 3·x + 2)", xfe_str);
1368    }
1369
1370    #[proptest]
1371    fn constant_folding_can_deal_with_multiplication_by_one(
1372        #[strategy(arb())] c: ConstraintCircuitMonad<DualRowIndicator>,
1373    ) {
1374        let one = || c.builder.one();
1375        prop_assert_eq!(c.clone(), c.clone() * one());
1376        prop_assert_eq!(c.clone(), one() * c.clone());
1377        prop_assert_eq!(c.clone(), one() * c.clone() * one());
1378    }
1379
1380    #[proptest]
1381    fn constant_folding_can_deal_with_adding_zero(
1382        #[strategy(arb())] c: ConstraintCircuitMonad<DualRowIndicator>,
1383    ) {
1384        let zero = || c.builder.zero();
1385        prop_assert_eq!(c.clone(), c.clone() + zero());
1386        prop_assert_eq!(c.clone(), zero() + c.clone());
1387        prop_assert_eq!(c.clone(), zero() + c.clone() + zero());
1388    }
1389
1390    #[proptest]
1391    fn constant_folding_can_deal_with_subtracting_zero(
1392        #[strategy(arb())] c: ConstraintCircuitMonad<DualRowIndicator>,
1393    ) {
1394        prop_assert_eq!(c.clone(), c.clone() - c.builder.zero());
1395    }
1396
1397    #[proptest]
1398    fn constant_folding_can_deal_with_adding_effectively_zero_term(
1399        #[strategy(arb())] c: ConstraintCircuitMonad<DualRowIndicator>,
1400        modification_selectors: [bool; 4],
1401    ) {
1402        let zero = || c.builder.zero();
1403        let mut redundant_circuit = c.clone();
1404        if modification_selectors[0] {
1405            redundant_circuit = redundant_circuit + (c.clone() * zero());
1406        }
1407        if modification_selectors[1] {
1408            redundant_circuit = redundant_circuit + (zero() * c.clone());
1409        }
1410        if modification_selectors[2] {
1411            redundant_circuit = (c.clone() * zero()) + redundant_circuit;
1412        }
1413        if modification_selectors[3] {
1414            redundant_circuit = (zero() * c.clone()) + redundant_circuit;
1415        }
1416        prop_assert_eq!(c, redundant_circuit);
1417    }
1418
1419    #[proptest]
1420    fn constant_folding_does_not_replace_0_minus_circuit_with_the_circuit(
1421        #[strategy(arb())] circuit: ConstraintCircuitMonad<DualRowIndicator>,
1422    ) {
1423        if circuit.circuit.borrow().is_zero() {
1424            return Err(TestCaseError::Reject("0 - 0 actually is 0".into()));
1425        }
1426        let zero_minus_circuit = circuit.builder.zero() - circuit.clone();
1427        prop_assert_ne!(&circuit, &zero_minus_circuit);
1428    }
1429
1430    #[test]
1431    fn pointer_redirection_obliviates_a_node_in_a_circuit() {
1432        let builder = ConstraintCircuitBuilder::new();
1433        let x = |i| builder.input(SingleRowIndicator::Main(i));
1434        let constant = |c: u32| builder.b_constant(c);
1435        let challenge = |i: usize| builder.challenge(i);
1436
1437        let part = x(0) + x(1);
1438        let substitute_me = x(0) * part.clone();
1439        let root_0 = part.clone() + challenge(1) - constant(84);
1440        let root_1 = substitute_me.clone() + challenge(0) - constant(42);
1441        let root_2 = x(2) * substitute_me.clone() - challenge(1);
1442
1443        assert!(!root_0.contains(&substitute_me));
1444        assert!(root_1.contains(&substitute_me));
1445        assert!(root_2.contains(&substitute_me));
1446
1447        let new_variable = x(3);
1448        builder.redirect_all_references_to_node(
1449            substitute_me.circuit.borrow().id,
1450            new_variable.clone(),
1451        );
1452
1453        assert!(!root_0.contains(&substitute_me));
1454        assert!(!root_1.contains(&substitute_me));
1455        assert!(!root_2.contains(&substitute_me));
1456
1457        assert!(root_0.contains(&part));
1458        assert!(root_1.contains(&new_variable));
1459        assert!(root_2.contains(&new_variable));
1460    }
1461
1462    #[test]
1463    fn simple_degree_lowering() {
1464        let builder = ConstraintCircuitBuilder::new();
1465        let x = || builder.input(SingleRowIndicator::Main(0));
1466        let x_pow_3 = x() * x() * x();
1467        let x_pow_5 = x() * x() * x() * x() * x();
1468        let mut multicircuit = [x_pow_5, x_pow_3];
1469
1470        let degree_lowering_info = DegreeLoweringInfo {
1471            target_degree: 3,
1472            num_main_cols: 1,
1473            num_aux_cols: 0,
1474        };
1475        let (new_main_constraints, new_aux_constraints) =
1476            ConstraintCircuitMonad::lower_to_degree(&mut multicircuit, degree_lowering_info);
1477
1478        assert_eq!(1, new_main_constraints.len());
1479        assert!(new_aux_constraints.is_empty());
1480    }
1481
1482    #[test]
1483    fn somewhat_simple_degree_lowering() {
1484        let builder = ConstraintCircuitBuilder::new();
1485        let x = |i| builder.input(SingleRowIndicator::Main(i));
1486        let y = |i| builder.input(SingleRowIndicator::Aux(i));
1487        let b_con = |i: u64| builder.b_constant(i);
1488
1489        let constraint_0 = x(0) * x(0) * (x(1) - x(2)) - x(0) * x(2) - b_con(42);
1490        let constraint_1 = x(1) * (x(1) - b_con(5)) * x(2) * (x(2) - b_con(1));
1491        let constraint_2 = y(0)
1492            * (b_con(2) * x(0) + b_con(3) * x(1) + b_con(4) * x(2))
1493            * (b_con(4) * x(0) + b_con(8) * x(1) + b_con(16) * x(2))
1494            - y(1);
1495
1496        let mut multicircuit = [constraint_0, constraint_1, constraint_2];
1497
1498        let degree_lowering_info = DegreeLoweringInfo {
1499            target_degree: 2,
1500            num_main_cols: 3,
1501            num_aux_cols: 2,
1502        };
1503        let (new_main_constraints, new_aux_constraints) =
1504            ConstraintCircuitMonad::lower_to_degree(&mut multicircuit, degree_lowering_info);
1505
1506        assert!(new_main_constraints.len() <= 3);
1507        assert!(new_aux_constraints.len() <= 1);
1508    }
1509
1510    #[test]
1511    fn less_simple_degree_lowering() {
1512        let builder = ConstraintCircuitBuilder::new();
1513        let x = |i| builder.input(SingleRowIndicator::Main(i));
1514
1515        let constraint_0 = (x(0) * x(1) * x(2)) * (x(3) * x(4)) * x(5);
1516        let constraint_1 = (x(6) * x(7)) * (x(3) * x(4)) * x(8);
1517
1518        let mut multicircuit = [constraint_0, constraint_1];
1519
1520        let degree_lowering_info = DegreeLoweringInfo {
1521            target_degree: 3,
1522            num_main_cols: 9,
1523            num_aux_cols: 0,
1524        };
1525        let (new_main_constraints, new_aux_constraints) =
1526            ConstraintCircuitMonad::lower_to_degree(&mut multicircuit, degree_lowering_info);
1527
1528        assert!(new_main_constraints.len() <= 3);
1529        assert!(new_aux_constraints.is_empty());
1530    }
1531
1532    /// The multicircuit
1533    ///
1534    /// ```text
1535    ///      ·          ·
1536    ///     ╱ ╲        ╱ ╲
1537    ///    ·   ╲      ·   ╲
1538    ///   ╱ ╲   ╲    ╱ ╲   ╲
1539    ///  0   0   0  1   1   1
1540    /// ```
1541    fn circuit_with_multiple_options_for_degree_lowering_to_degree_2()
1542    -> [ConstraintCircuitMonad<SingleRowIndicator>; 2] {
1543        let builder = ConstraintCircuitBuilder::new();
1544        let x = |i| builder.input(SingleRowIndicator::Main(i));
1545
1546        let constraint_0 = x(0) * x(0) * x(0);
1547        let constraint_1 = x(1) * x(1) * x(1);
1548
1549        [constraint_0, constraint_1]
1550    }
1551
1552    #[test]
1553    fn pick_node_to_substitute_is_deterministic() {
1554        let multicircuit = circuit_with_multiple_options_for_degree_lowering_to_degree_2();
1555        let first_node_id = ConstraintCircuitMonad::pick_node_to_substitute(&multicircuit, 2);
1556
1557        for _ in 0..20 {
1558            let node_id_again = ConstraintCircuitMonad::pick_node_to_substitute(&multicircuit, 2);
1559            assert_eq!(first_node_id, node_id_again);
1560        }
1561    }
1562
1563    #[test]
1564    fn degree_lowering_specific_simple_circuit_is_deterministic() {
1565        let degree_lowering_info = DegreeLoweringInfo {
1566            target_degree: 2,
1567            num_main_cols: 2,
1568            num_aux_cols: 0,
1569        };
1570
1571        let mut original_multicircuit =
1572            circuit_with_multiple_options_for_degree_lowering_to_degree_2();
1573        let (new_main_constraints, _) = ConstraintCircuitMonad::lower_to_degree(
1574            &mut original_multicircuit,
1575            degree_lowering_info,
1576        );
1577
1578        for _ in 0..20 {
1579            let mut new_multicircuit =
1580                circuit_with_multiple_options_for_degree_lowering_to_degree_2();
1581            let (new_main_constraints_again, _) = ConstraintCircuitMonad::lower_to_degree(
1582                &mut new_multicircuit,
1583                degree_lowering_info,
1584            );
1585            assert_eq!(new_main_constraints, new_main_constraints_again);
1586            assert_eq!(original_multicircuit, new_multicircuit);
1587        }
1588    }
1589
1590    #[test]
1591    fn all_nodes_in_multicircuit_are_identified_correctly() {
1592        let builder = ConstraintCircuitBuilder::new();
1593
1594        let x = |i| builder.input(SingleRowIndicator::Main(i));
1595        let b_con = |i: u64| builder.b_constant(i);
1596
1597        let sub_tree_0 = x(0) * x(1) * (x(2) - b_con(1)) * x(3) * x(4);
1598        let sub_tree_1 = x(0) * x(1) * (x(2) - b_con(1)) * x(3) * x(5);
1599        let sub_tree_2 = x(10) * x(10) * x(2) * x(13);
1600        let sub_tree_3 = x(10) * x(10) * x(2) * x(14);
1601
1602        let circuit_0 = sub_tree_0.clone() + sub_tree_1.clone();
1603        let circuit_1 = sub_tree_2.clone() + sub_tree_3.clone();
1604        let circuit_2 = sub_tree_0 + sub_tree_2;
1605        let circuit_3 = sub_tree_1 + sub_tree_3;
1606
1607        let multicircuit = [circuit_0, circuit_1, circuit_2, circuit_3].map(|c| c.consume());
1608
1609        let all_nodes = ConstraintCircuitMonad::all_nodes_in_multicircuit(&multicircuit);
1610        let count_node = |node| all_nodes.clone().filter(|n| n.borrow().eq(&node)).count();
1611
1612        let x0 = x(0).consume();
1613        assert_eq!(4, count_node(x0));
1614
1615        let x2 = x(2).consume();
1616        assert_eq!(8, count_node(x2));
1617
1618        let x10 = x(10).consume();
1619        assert_eq!(8, count_node(x10));
1620
1621        let x4 = x(4).consume();
1622        assert_eq!(2, count_node(x4));
1623
1624        let x6 = x(6).consume();
1625        assert_eq!(0, count_node(x6));
1626
1627        let x0_x1 = (x(0) * x(1)).consume();
1628        assert_eq!(4, count_node(x0_x1));
1629
1630        let tree = (x(0) * x(1) * (x(2) - b_con(1))).consume();
1631        assert_eq!(4, count_node(tree));
1632
1633        let max_occurrences = all_nodes
1634            .clone()
1635            .map(|node| all_nodes.clone().filter(|n| n == &node).count())
1636            .max()
1637            .unwrap();
1638        assert_eq!(8, max_occurrences);
1639
1640        let most_frequent_nodes = all_nodes
1641            .clone()
1642            .filter(|node| all_nodes.clone().filter(|n| n == node).count() == max_occurrences)
1643            .map(|node| node.borrow().clone())
1644            .unique()
1645            .collect_vec();
1646        assert_eq!(2, most_frequent_nodes.len());
1647        assert!(most_frequent_nodes.contains(&x(2).consume()));
1648        assert!(most_frequent_nodes.contains(&x(10).consume()));
1649    }
1650
1651    #[test]
1652    fn visible_nodes_are_counted_correctly() {
1653        let builder = ConstraintCircuitBuilder::new();
1654
1655        let x = |i| builder.input(SingleRowIndicator::Main(i));
1656        let b_con = |i: u64| builder.b_constant(i);
1657
1658        // 1 node because of constant reduction
1659        let tree_of_constants = b_con(42) * b_con(7) + b_con(13) - b_con(12);
1660
1661        // 8 nodes (4 leafs, 4 bin-ops)
1662        //       ── · ─
1663        //      ╱      ╲
1664        //   ─ + ─ ·    +
1665        //  ╱     ╱ ╲  ╱ ╲
1666        // x0    -1  x1  x2
1667        let circuit_1 = (x(0) - x(1)) * (x(1) + x(2));
1668
1669        // creates new nodes, but ignored in the multicircuit below
1670        let _circuit_2 = x(1) * x(3) + (x(4) - x(5)) * x(6);
1671
1672        // 1 new node
1673        //           ·
1674        //          ╱ ╲
1675        // (x1 + x2)   consts
1676        let circuit_3 = (x(2) + x(1)) * tree_of_constants.clone();
1677
1678        let multicircuit = [tree_of_constants, circuit_1, circuit_3];
1679        let num_visible = ConstraintCircuitMonad::num_visible_nodes(&multicircuit);
1680        assert_eq!(10, num_visible);
1681    }
1682
1683    #[derive(Debug, Copy, Clone, Eq, PartialEq, test_strategy::Arbitrary)]
1684    enum CircuitOperationChoice {
1685        Add(usize, usize),
1686        Mul(usize, usize),
1687    }
1688
1689    #[derive(Debug, Copy, Clone, Eq, PartialEq, test_strategy::Arbitrary)]
1690    enum CircuitInputType {
1691        Main,
1692        Aux,
1693    }
1694
1695    #[derive(Debug, Copy, Clone, Eq, PartialEq, test_strategy::Arbitrary)]
1696    enum CircuitConstantType {
1697        Base(#[strategy(arb())] BFieldElement),
1698        Extension(#[strategy(arb())] XFieldElement),
1699    }
1700
1701    fn arbitrary_circuit_monad<II: InputIndicator>(
1702        num_inputs: usize,
1703        num_challenges: usize,
1704        num_constants: usize,
1705        num_operations: usize,
1706        num_outputs: usize,
1707    ) -> BoxedStrategy<Vec<ConstraintCircuitMonad<II>>> {
1708        (
1709            vec(CircuitInputType::arbitrary(), num_inputs),
1710            vec(CircuitConstantType::arbitrary(), num_constants),
1711            vec(CircuitOperationChoice::arbitrary(), num_operations),
1712            vec(arb::<usize>(), num_outputs),
1713        )
1714            .prop_map(move |(inputs, constants, operations, outputs)| {
1715                let builder = ConstraintCircuitBuilder::<II>::new();
1716
1717                assert_eq!(0, *builder.id_counter.borrow());
1718                assert!(
1719                    builder.all_nodes.borrow().is_empty(),
1720                    "fresh hashmap should be empty"
1721                );
1722
1723                let mut num_main_inputs = 0;
1724                let mut num_aux_inputs = 0;
1725                let mut all_nodes = vec![];
1726                let mut output_nodes = vec![];
1727
1728                for input in inputs {
1729                    match input {
1730                        CircuitInputType::Main => {
1731                            let node = builder.input(II::main_table_input(num_main_inputs));
1732                            num_main_inputs += 1;
1733                            all_nodes.push(node);
1734                        }
1735                        CircuitInputType::Aux => {
1736                            let node = builder.input(II::aux_table_input(num_aux_inputs));
1737                            num_aux_inputs += 1;
1738                            all_nodes.push(node);
1739                        }
1740                    }
1741                }
1742
1743                for i in 0..num_challenges {
1744                    let node = builder.challenge(i);
1745                    all_nodes.push(node);
1746                }
1747
1748                for constant in constants {
1749                    let node = match constant {
1750                        CircuitConstantType::Base(bfe) => builder.b_constant(bfe),
1751                        CircuitConstantType::Extension(xfe) => builder.x_constant(xfe),
1752                    };
1753                    all_nodes.push(node);
1754                }
1755
1756                if all_nodes.is_empty() {
1757                    return vec![];
1758                }
1759
1760                for operation in operations {
1761                    let (lhs, rhs) = match operation {
1762                        CircuitOperationChoice::Add(lhs, rhs) => (lhs, rhs),
1763                        CircuitOperationChoice::Mul(lhs, rhs) => (lhs, rhs),
1764                    };
1765
1766                    let lhs_index = lhs % all_nodes.len();
1767                    let rhs_index = rhs % all_nodes.len();
1768
1769                    let lhs_node = all_nodes[lhs_index].clone();
1770                    let rhs_node = all_nodes[rhs_index].clone();
1771
1772                    let node = match operation {
1773                        CircuitOperationChoice::Add(_, _) => lhs_node + rhs_node,
1774                        CircuitOperationChoice::Mul(_, _) => lhs_node * rhs_node,
1775                    };
1776                    all_nodes.push(node);
1777                }
1778
1779                for output in outputs {
1780                    let index = output % all_nodes.len();
1781                    output_nodes.push(all_nodes[index].clone());
1782                }
1783
1784                output_nodes
1785            })
1786            .boxed()
1787    }
1788
1789    #[proptest]
1790    fn node_type_counts_add_up(
1791        #[strategy(arbitrary_circuit_monad(10, 10, 10, 60, 10))] multicircuit_monad: Vec<
1792            ConstraintCircuitMonad<SingleRowIndicator>,
1793        >,
1794    ) {
1795        prop_assert_eq!(
1796            ConstraintCircuitMonad::num_nodes(&multicircuit_monad),
1797            ConstraintCircuitMonad::num_main_inputs(&multicircuit_monad)
1798                + ConstraintCircuitMonad::num_aux_inputs(&multicircuit_monad)
1799                + ConstraintCircuitMonad::num_challenges(&multicircuit_monad)
1800                + ConstraintCircuitMonad::num_bfield_constants(&multicircuit_monad)
1801                + ConstraintCircuitMonad::num_xfield_constants(&multicircuit_monad)
1802                + ConstraintCircuitMonad::num_binops(&multicircuit_monad)
1803        );
1804
1805        prop_assert_eq!(10, ConstraintCircuitMonad::num_inputs(&multicircuit_monad));
1806    }
1807
1808    /// Test the completeness and soundness of the `apply_substitution`
1809    /// function, which substitutes a single node.
1810    ///
1811    /// In this context, completeness means: "given a satisfying assignment to
1812    /// the circuit before degree lowering, one can derive a satisfying
1813    /// assignment to the circuit after degree lowering." Soundness means
1814    /// the converse.
1815    ///
1816    /// We test these features using random input vectors. Naturally, the output
1817    /// is not the zero vector (with high probability) and so the given input is
1818    /// *not* a satisfying assignment (with high probability). However, the
1819    /// circuit can be extended by way of thought experiment into one that
1820    /// subtracts a fixed constant from the original output. For the right
1821    /// choice of subtrahend, the random input now *is* a satisfying
1822    /// assignment to the circuit.
1823    ///
1824    /// Specifically, let `input` denote the original (before degree lowering)
1825    /// input, and `C` the circuit. Then `input` is a satisfying input for
1826    /// the new circuit `C'(X) = C(X) - C(input)`
1827    ///
1828    /// After applying a substitution to obtain circuit `C || k` from `C`, where
1829    /// `k = Z - some_expr(X)` and `Z` is the introduced variable, the length
1830    /// of the input and output increases by 1. Moreover, if `input` is a
1831    /// satisfying input to `C'` then `input || some_expr(input)` is* a
1832    /// satisfying input to `C' || k`.
1833    ///
1834    /// (*: If the transformation is complete.)
1835    ///
1836    /// To establish the converse, we want to start from a satisfying input to
1837    /// `C" || k` and reduce it to a satisfying input to `C"`. The requirement,
1838    /// implied by "satisfying input", that `k(X || Z) == 0` implies `Z ==
1839    /// some_expr(X)`. Therefore, in order to sample a random satisfying
1840    /// input to `C" || k`, it suffices to select `input` at random, define
1841    /// `C"(X) = C(X) - C(input)`, and evaluate `some_expr(input)`. Then
1842    /// `input || some_expr(input)` is a random satisfying input to `C" ||
1843    /// k`. It follows** that `input` is a satisfying input to `C"`.
1844    ///
1845    /// (**: If the transformation is sound.)
1846    ///
1847    /// This description makes use of the following commutative diagram.
1848    ///
1849    /// ```text
1850    ///          C ───── degree-lowering ────> C || k
1851    ///          │                               │
1852    /// subtract │                      subtract │
1853    ///    fixed │                         fixed │
1854    ///   output │                        output │
1855    ///          │                               │
1856    ///          v                               v
1857    ///          C* ─── degree-lowering ────> C* || k
1858    /// ```
1859    ///
1860    /// The point of this elaboration is that in this particular case, testing
1861    /// completeness and soundness require the same code path. If `input`
1862    /// and `input || some_expr(input)` work for circuits before and after
1863    /// degree lowering, this fact establishes both completeness and
1864    /// soundness simultaneously.
1865    //
1866    // Shrinking on this test is disabled because we noticed some weird ass
1867    // behavior. In short, shrinking does not play ball with the arbitrary
1868    // circuit generator; it seems to make the generated circuits *more*
1869    // complex, not less so.
1870    #[proptest(cases = 1000, max_shrink_iters = 0)]
1871    fn node_substitution_is_complete_and_sound(
1872        #[strategy(arbitrary_circuit_monad(10, 10, 10, 160, 10))] mut multicircuit_monad: Vec<
1873            ConstraintCircuitMonad<SingleRowIndicator>,
1874        >,
1875        #[strategy(vec(arb(), ConstraintCircuitMonad::num_main_inputs(&#multicircuit_monad)))]
1876        #[filter(!#main_input.is_empty())]
1877        main_input: Vec<BFieldElement>,
1878        #[strategy(vec(arb(), ConstraintCircuitMonad::num_aux_inputs(&#multicircuit_monad)))]
1879        #[filter(!#aux_input.is_empty())]
1880        aux_input: Vec<XFieldElement>,
1881        #[strategy(vec(arb(), ConstraintCircuitMonad::num_challenges(&#multicircuit_monad)))]
1882        challenges: Vec<XFieldElement>,
1883        #[strategy(arb())] substitution_node_index: usize,
1884    ) {
1885        let mut main_input = Array2::from_shape_vec((1, main_input.len()), main_input).unwrap();
1886        let mut aux_input = Array2::from_shape_vec((1, aux_input.len()), aux_input).unwrap();
1887
1888        let output_before_lowering = multicircuit_monad
1889            .iter()
1890            .map(|m| m.circuit.borrow())
1891            .map(|c| c.evaluate(main_input.view(), aux_input.view(), &challenges))
1892            .collect_vec();
1893
1894        // apply one step of degree-lowering
1895        let num_nodes = ConstraintCircuitMonad::num_nodes(&multicircuit_monad);
1896        let &substitution_node_id = multicircuit_monad[0]
1897            .builder
1898            .all_nodes
1899            .borrow()
1900            .iter()
1901            .cycle()
1902            .skip(substitution_node_index % num_nodes)
1903            .take(num_nodes)
1904            .find_map(|(id, monad)| monad.circuit.borrow().is_zero().not().then_some(id))
1905            .expect("no suitable nodes to substitute");
1906
1907        let degree_lowering_info = DegreeLoweringInfo {
1908            target_degree: 2,
1909            num_main_cols: main_input.len(),
1910            num_aux_cols: aux_input.len(),
1911        };
1912        let substitution_constraint = ConstraintCircuitMonad::apply_substitution(
1913            &mut multicircuit_monad,
1914            degree_lowering_info,
1915            substitution_node_id,
1916            EvolvingMainConstraintsNumber(0),
1917            EvolvingAuxConstraintsNumber(0),
1918        );
1919
1920        // extract substituted constraint
1921        let CircuitExpression::BinOp(BinOp::Add, variable, neg_expression) =
1922            &substitution_constraint.circuit.borrow().expression
1923        else {
1924            panic!();
1925        };
1926        let extra_input =
1927            match &neg_expression.borrow().expression {
1928                CircuitExpression::BinOp(BinOp::Mul, _neg_one, circuit) => circuit
1929                    .borrow()
1930                    .evaluate(main_input.view(), aux_input.view(), &challenges),
1931                CircuitExpression::BConst(c) => -c.lift(),
1932                CircuitExpression::XConst(c) => -*c,
1933                _ => panic!(),
1934            };
1935        if variable.borrow().evaluates_to_base_element() {
1936            let extra_input = extra_input.unlift().unwrap();
1937            let extra_input = Array2::from_shape_vec([1, 1], vec![extra_input]).unwrap();
1938            main_input.append(Axis(1), extra_input.view()).unwrap();
1939        } else {
1940            let extra_input = Array2::from_shape_vec([1, 1], vec![extra_input]).unwrap();
1941            aux_input.append(Axis(1), extra_input.view()).unwrap();
1942        }
1943
1944        // evaluate again
1945        let output_after_lowering = multicircuit_monad
1946            .iter()
1947            .map(|m| m.circuit.borrow())
1948            .map(|c| c.evaluate(main_input.view(), aux_input.view(), &challenges))
1949            .collect_vec();
1950        prop_assert_eq!(output_before_lowering, output_after_lowering);
1951
1952        let evaluated_constraint = substitution_constraint.circuit.borrow().evaluate(
1953            main_input.view(),
1954            aux_input.view(),
1955            &challenges,
1956        );
1957        prop_assert!(evaluated_constraint.is_zero());
1958    }
1959}