• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

TritonVM / triton-vm / 11719998250

07 Nov 2024 07:34AM UTC coverage: 98.317% (-0.01%) from 98.329%
11719998250

push

github

jan-ferdinand
feat!: Introduce assertion error

The instructions `assert` and `assert_vector` now accept some additional
context to simplify debugging Triton assembly programs. For now, only
an error ID is supported. For example, the error returned when running
the program

```tasm
push 2
assert error_id 42
halt
```

will now contain a field with the supplied error ID, in this case, 42.

BREAKING CHANGE: The type `InstructionError` is no longer `Copy`.

416 of 430 new or added lines in 6 files covered. (96.74%)

102 existing lines in 9 files now uncovered.

24713 of 25136 relevant lines covered (98.32%)

5979183.83 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.95
/triton-constraint-circuit/src/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
use std::cell::RefCell;
12
use std::cmp;
13
use std::collections::HashMap;
14
use std::fmt::Debug;
15
use std::fmt::Display;
16
use std::fmt::Formatter;
17
use std::fmt::Result as FmtResult;
18
use std::hash::Hash;
19
use std::hash::Hasher;
20
use std::iter::Sum;
21
use std::ops::Add;
22
use std::ops::AddAssign;
23
use std::ops::Mul;
24
use std::ops::Neg;
25
use std::ops::Sub;
26
use std::rc::Rc;
27

28
use arbitrary::Arbitrary;
29
use arbitrary::Unstructured;
30
use itertools::Itertools;
31
use ndarray::ArrayView2;
32
use num_traits::One;
33
use num_traits::Zero;
34
use quote::quote;
35
use quote::ToTokens;
36
use twenty_first::prelude::*;
37

38
mod private {
39
    // A public but un-nameable type for sealing traits.
40
    pub trait Seal {}
41
}
42

43
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
44
pub struct DegreeLoweringInfo {
45
    /// The degree after degree lowering. Must be greater than 1.
46
    pub target_degree: isize,
47

48
    /// The total number of main columns _before_ degree lowering has happened.
49
    pub num_main_cols: usize,
50

51
    /// The total number of auxiliary columns _before_ degree lowering has happened.
52
    pub num_aux_cols: usize,
53
}
54

55
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
56
pub enum BinOp {
57
    Add,
58
    Mul,
59
}
60

61
impl Display for BinOp {
62
    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
45,862✔
63
        match self {
45,862✔
64
            BinOp::Add => write!(f, "+"),
20,472✔
65
            BinOp::Mul => write!(f, "*"),
25,390✔
66
        }
67
    }
45,862✔
68
}
69

70
impl ToTokens for BinOp {
71
    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
1,310✔
72
        match self {
1,310✔
73
            BinOp::Add => tokens.extend(quote!(+)),
524✔
74
            BinOp::Mul => tokens.extend(quote!(*)),
786✔
75
        }
76
    }
1,310✔
77
}
78

79
impl BinOp {
80
    pub fn operation<L, R, O>(&self, lhs: L, rhs: R) -> O
2,147,483,647✔
81
    where
2,147,483,647✔
82
        L: Add<R, Output = O> + Mul<R, Output = O>,
2,147,483,647✔
83
    {
2,147,483,647✔
84
        match self {
2,147,483,647✔
85
            BinOp::Add => lhs + rhs,
1,238,564,973✔
86
            BinOp::Mul => lhs * rhs,
1,642,631,964✔
87
        }
88
    }
2,147,483,647✔
89
}
90

91
/// Describes the position of a variable in a constraint polynomial in the row layout applicable
92
/// for a certain kind of constraint polynomial.
93
///
94
/// The position of variable in a constraint polynomial is, in principle, a `usize`. However,
95
/// depending on the type of the constraint polynomial, this index may be an index into a single
96
/// row (for initial, consistency and terminal constraints), or a pair of adjacent rows (for
97
/// transition constraints). Additionally, the index may refer to a column in the main table, or
98
/// a column in the auxiliary table. This trait abstracts over these possibilities, and provides
99
/// a uniform interface for accessing the index.
100
///
101
/// Having `Copy + Hash + Eq` helps to put `InputIndicator`s into containers.
102
///
103
/// This is a _sealed_ trait. It is not intended (or possible) to implement this trait outside the
104
/// crate defining it.
105
pub trait InputIndicator: Debug + Display + Copy + Hash + Eq + ToTokens + private::Seal {
106
    /// `true` iff `self` refers to a column in the main table.
107
    fn is_main_table_column(&self) -> bool;
108

109
    /// `true` iff `self` refers to the current row.
110
    fn is_current_row(&self) -> bool;
111

112
    /// The index of the indicated (main or auxiliary) column.
113
    fn column(&self) -> usize;
114

115
    fn main_table_input(index: usize) -> Self;
116
    fn aux_table_input(index: usize) -> Self;
117

118
    fn evaluate(
119
        &self,
120
        main_table: ArrayView2<BFieldElement>,
121
        aux_table: ArrayView2<XFieldElement>,
122
    ) -> XFieldElement;
123
}
124

125
/// The position of a variable in a constraint polynomial that operates on a single row of the
126
/// execution trace.
127
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Arbitrary)]
106,915✔
128
pub enum SingleRowIndicator {
129
    Main(usize),
130
    Aux(usize),
131
}
132

133
impl private::Seal for SingleRowIndicator {}
134

135
impl Display for SingleRowIndicator {
136
    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
1,762✔
137
        let input_indicator: String = match self {
1,762✔
138
            Self::Main(i) => format!("main_row[{i}]"),
1,642✔
139
            Self::Aux(i) => format!("aux_row[{i}]"),
120✔
140
        };
141

142
        write!(f, "{input_indicator}")
1,762✔
143
    }
1,762✔
144
}
145

146
impl ToTokens for SingleRowIndicator {
147
    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
68✔
148
        match self {
68✔
149
            Self::Main(i) => tokens.extend(quote!(main_row[#i])),
67✔
150
            Self::Aux(i) => tokens.extend(quote!(aux_row[#i])),
1✔
151
        }
152
    }
68✔
153
}
154

155
impl InputIndicator for SingleRowIndicator {
156
    fn is_main_table_column(&self) -> bool {
40,215✔
157
        matches!(self, Self::Main(_))
40,215✔
158
    }
40,215✔
159

160
    fn is_current_row(&self) -> bool {
8✔
161
        true
8✔
162
    }
8✔
163

164
    fn column(&self) -> usize {
136✔
165
        match self {
136✔
166
            Self::Main(i) | Self::Aux(i) => *i,
136✔
167
        }
136✔
168
    }
136✔
169

170
    fn main_table_input(index: usize) -> Self {
6,364✔
171
        Self::Main(index)
6,364✔
172
    }
6,364✔
173

174
    fn aux_table_input(index: usize) -> Self {
7,346✔
175
        Self::Aux(index)
7,346✔
176
    }
7,346✔
177

178
    fn evaluate(
74,989,139✔
179
        &self,
74,989,139✔
180
        main_table: ArrayView2<BFieldElement>,
74,989,139✔
181
        aux_table: ArrayView2<XFieldElement>,
74,989,139✔
182
    ) -> XFieldElement {
74,989,139✔
183
        match self {
74,989,139✔
184
            Self::Main(i) => main_table[[0, *i]].lift(),
74,954,565✔
185
            Self::Aux(i) => aux_table[[0, *i]],
34,574✔
186
        }
187
    }
74,989,139✔
188
}
189

190
/// The position of a variable in a constraint polynomial that operates on two rows (current and
191
/// next) of the execution trace.
192
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Arbitrary)]
299,556✔
193
pub enum DualRowIndicator {
194
    CurrentMain(usize),
195
    CurrentAux(usize),
196
    NextMain(usize),
197
    NextAux(usize),
198
}
199

200
impl private::Seal for DualRowIndicator {}
201

202
impl Display for DualRowIndicator {
203
    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
24,338✔
204
        let input_indicator: String = match self {
24,338✔
205
            Self::CurrentMain(i) => format!("current_main_row[{i}]"),
17,045✔
206
            Self::CurrentAux(i) => format!("current_aux_row[{i}]"),
794✔
207
            Self::NextMain(i) => format!("next_main_row[{i}]"),
5,813✔
208
            Self::NextAux(i) => format!("next_aux_row[{i}]"),
686✔
209
        };
210

211
        write!(f, "{input_indicator}")
24,338✔
212
    }
24,338✔
213
}
214

215
impl ToTokens for DualRowIndicator {
216
    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
860✔
217
        match self {
860✔
218
            Self::CurrentMain(i) => tokens.extend(quote!(current_main_row[#i])),
623✔
219
            Self::CurrentAux(i) => tokens.extend(quote!(current_aux_row[#i])),
59✔
220
            Self::NextMain(i) => tokens.extend(quote!(next_main_row[#i])),
157✔
221
            Self::NextAux(i) => tokens.extend(quote!(next_aux_row[#i])),
21✔
222
        }
223
    }
860✔
224
}
225

226
impl InputIndicator for DualRowIndicator {
UNCOV
227
    fn is_main_table_column(&self) -> bool {
×
UNCOV
228
        matches!(self, Self::CurrentMain(_) | Self::NextMain(_))
×
UNCOV
229
    }
×
230

231
    fn is_current_row(&self) -> bool {
×
232
        matches!(self, Self::CurrentMain(_) | Self::CurrentAux(_))
×
233
    }
×
234

235
    fn column(&self) -> usize {
1,328✔
236
        match self {
1,328✔
237
            Self::CurrentMain(i) | Self::NextMain(i) | Self::CurrentAux(i) | Self::NextAux(i) => *i,
1,328✔
238
        }
1,328✔
239
    }
1,328✔
240

241
    fn main_table_input(index: usize) -> Self {
1,068✔
242
        // It seems that the choice between `CurrentMain` and `NextMain` is arbitrary:
1,068✔
243
        // any transition constraint polynomial is evaluated on both the current and the next row.
1,068✔
244
        // Hence, both rows are in scope.
1,068✔
245
        Self::CurrentMain(index)
1,068✔
246
    }
1,068✔
247

248
    fn aux_table_input(index: usize) -> Self {
×
249
        Self::CurrentAux(index)
×
250
    }
×
251

252
    fn evaluate(
1,531,224,522✔
253
        &self,
1,531,224,522✔
254
        main_table: ArrayView2<BFieldElement>,
1,531,224,522✔
255
        aux_table: ArrayView2<XFieldElement>,
1,531,224,522✔
256
    ) -> XFieldElement {
1,531,224,522✔
257
        match self {
1,531,224,522✔
258
            Self::CurrentMain(i) => main_table[[0, *i]].lift(),
1,157,053,705✔
259
            Self::CurrentAux(i) => aux_table[[0, *i]],
34,560,679✔
260
            Self::NextMain(i) => main_table[[1, *i]].lift(),
304,951,038✔
261
            Self::NextAux(i) => aux_table[[1, *i]],
34,659,100✔
262
        }
263
    }
1,531,224,522✔
264
}
265

266
/// A circuit expression is the recursive data structure that represents the constraint
267
/// circuit. It is a directed, acyclic graph of binary operations on a) the variables
268
/// corresponding to columns in the AET, b) constants, and c) challenges. It has
269
/// multiple roots, making it a “multitree.” Each root corresponds to one constraint.
270
///
271
/// The leafs of the tree are
272
/// - constants in the base field, _i.e._, [`BFieldElement`]s,
273
/// - constants in the extension field, _i.e._, [`XFieldElement`]s,
274
/// - input variables, _i.e._, entries from the Algebraic Execution Trace, main
275
///   or aux, and
276
/// - challenges, _i.e._, (pseudo-)random values sampled through the Fiat-Shamir
277
///   heuristic.
278
///
279
/// An internal node, representing some binary operation, is either addition or
280
/// multiplication. The left and right children of the node are the operands of
281
/// the binary operation. The left and right children are not themselves `CircuitExpression`s,
282
/// but rather [`ConstraintCircuit`]s, which is a wrapper around `CircuitExpression`
283
/// that manages additional bookkeeping information.
284
#[derive(Debug, Clone)]
285
pub enum CircuitExpression<II: InputIndicator> {
286
    BConst(BFieldElement),
287
    XConst(XFieldElement),
288
    Input(II),
289
    Challenge(usize),
290
    BinOp(
291
        BinOp,
292
        Rc<RefCell<ConstraintCircuit<II>>>,
293
        Rc<RefCell<ConstraintCircuit<II>>>,
294
    ),
295
}
296

297
impl<II: InputIndicator> Hash for CircuitExpression<II> {
298
    fn hash<H: Hasher>(&self, state: &mut H) {
1,486,919,178✔
299
        match self {
1,486,919,178✔
300
            Self::BConst(bfe) => {
102,129,373✔
301
                "bfe".hash(state);
102,129,373✔
302
                bfe.hash(state);
102,129,373✔
303
            }
102,129,373✔
304
            Self::XConst(xfe) => {
2,692✔
305
                "xfe".hash(state);
2,692✔
306
                xfe.hash(state);
2,692✔
307
            }
2,692✔
308
            Self::Input(index) => {
401,367,533✔
309
                "input".hash(state);
401,367,533✔
310
                index.hash(state);
401,367,533✔
311
            }
401,367,533✔
312
            Self::Challenge(table_challenge_id) => {
241,070,987✔
313
                "challenge".hash(state);
241,070,987✔
314
                table_challenge_id.hash(state);
241,070,987✔
315
            }
241,070,987✔
316
            Self::BinOp(binop, lhs, rhs) => {
742,348,593✔
317
                "binop".hash(state);
742,348,593✔
318
                binop.hash(state);
742,348,593✔
319
                lhs.borrow().hash(state);
742,348,593✔
320
                rhs.borrow().hash(state);
742,348,593✔
321
            }
742,348,593✔
322
        }
323
    }
1,486,919,178✔
324
}
325

326
impl<II: InputIndicator> PartialEq for CircuitExpression<II> {
327
    fn eq(&self, other: &Self) -> bool {
2,147,483,647✔
328
        match (self, other) {
2,147,483,647✔
329
            (Self::BConst(b), Self::BConst(b_o)) => b == b_o,
28,540,333✔
330
            (Self::XConst(x), Self::XConst(x_o)) => x == x_o,
158,056✔
331
            (Self::Input(i), Self::Input(i_o)) => i == i_o,
95,523,479✔
332
            (Self::Challenge(c), Self::Challenge(c_o)) => c == c_o,
66,362,046✔
333
            (Self::BinOp(op, l, r), Self::BinOp(op_o, l_o, r_o)) => {
2,147,483,647✔
334
                op == op_o && l == l_o && r == r_o
2,147,483,647✔
335
            }
336
            _ => false,
2,147,483,647✔
337
        }
338
    }
2,147,483,647✔
339
}
340

341
impl<II: InputIndicator> Hash for ConstraintCircuit<II> {
342
    fn hash<H: Hasher>(&self, state: &mut H) {
1,486,919,178✔
343
        self.expression.hash(state)
1,486,919,178✔
344
    }
1,486,919,178✔
345
}
346

347
impl<II: InputIndicator> Hash for ConstraintCircuitMonad<II> {
348
    fn hash<H: Hasher>(&self, state: &mut H) {
1,280✔
349
        self.circuit.borrow().hash(state)
1,280✔
350
    }
1,280✔
351
}
352

353
/// A wrapper around a [`CircuitExpression`] that manages additional bookkeeping
354
/// information, such as node id and visited counter.
355
///
356
/// In contrast to [`ConstraintCircuitMonad`], this struct cannot manage the state
357
/// required to insert new nodes.
358
#[derive(Debug, Clone)]
359
pub struct ConstraintCircuit<II: InputIndicator> {
360
    pub id: usize,
361
    pub ref_count: usize,
362
    pub expression: CircuitExpression<II>,
363
}
364

365
impl<II: InputIndicator> Eq for ConstraintCircuit<II> {}
366

367
impl<II: InputIndicator> PartialEq for ConstraintCircuit<II> {
368
    /// Calculate equality of circuits. In particular, this function does *not* attempt to
369
    /// simplify or reduce neutral terms or products. So this comparison will return false for
370
    /// `a == a + 0`. It will also return false for `XFieldElement(7) == BFieldElement(7)`
371
    fn eq(&self, other: &Self) -> bool {
2,147,483,647✔
372
        self.expression == other.expression
2,147,483,647✔
373
    }
2,147,483,647✔
374
}
375

376
impl<II: InputIndicator> Display for ConstraintCircuit<II> {
377
    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
92,670✔
378
        match &self.expression {
92,670✔
379
            CircuitExpression::XConst(xfe) => write!(f, "{xfe}"),
1✔
380
            CircuitExpression::BConst(bfe) => write!(f, "{bfe}"),
13,305✔
381
            CircuitExpression::Input(input) => write!(f, "{input} "),
26,100✔
382
            CircuitExpression::Challenge(self_challenge_idx) => write!(f, "{self_challenge_idx}"),
7,402✔
383
            CircuitExpression::BinOp(operation, lhs, rhs) => {
45,862✔
384
                write!(f, "({}) {operation} ({})", lhs.borrow(), rhs.borrow())
45,862✔
385
            }
386
        }
387
    }
92,670✔
388
}
389

390
impl<II: InputIndicator> ConstraintCircuit<II> {
391
    fn new(id: usize, expression: CircuitExpression<II>) -> Self {
8,028,483✔
392
        Self {
8,028,483✔
393
            id,
8,028,483✔
394
            ref_count: 0,
8,028,483✔
395
            expression,
8,028,483✔
396
        }
8,028,483✔
397
    }
8,028,483✔
398

399
    /// Reset the reference counters for the entire subtree
400
    fn reset_ref_count_for_tree(&mut self) {
59,657✔
401
        self.ref_count = 0;
59,657✔
402

403
        if let CircuitExpression::BinOp(_, lhs, rhs) = &self.expression {
59,657✔
404
            lhs.borrow_mut().reset_ref_count_for_tree();
29,670✔
405
            rhs.borrow_mut().reset_ref_count_for_tree();
29,670✔
406
        }
29,987✔
407
    }
59,657✔
408

409
    /// Assert that all IDs in the subtree are unique.
410
    ///
411
    /// # Panics
412
    ///
413
    /// Panics if a duplicate ID is found.
414
    fn assert_unique_ids_inner(&mut self, ids: &mut HashMap<usize, ConstraintCircuit<II>>) {
14,431✔
415
        self.ref_count += 1;
14,431✔
416

14,431✔
417
        // Try to detect duplicate IDs only once for this node.
14,431✔
418
        if self.ref_count > 1 {
14,431✔
419
            return;
6,345✔
420
        }
8,086✔
421

8,086✔
422
        let self_id = self.id;
8,086✔
423
        if let Some(other) = ids.insert(self_id, self.clone()) {
8,086✔
424
            panic!("Repeated ID: {self_id}\nSelf:\n{self}\n{self:?}\nOther:\n{other}\n{other:?}");
×
425
        }
8,086✔
426

427
        if let CircuitExpression::BinOp(_, lhs, rhs) = &self.expression {
8,086✔
428
            lhs.borrow_mut().assert_unique_ids_inner(ids);
7,057✔
429
            rhs.borrow_mut().assert_unique_ids_inner(ids);
7,057✔
430
        }
7,057✔
431
    }
14,431✔
432

433
    /// Assert that a multicircuit has unique IDs.
434
    /// Also determines how often each node is referenced, updating the respective `ref_count`s.
435
    ///
436
    /// # Panics
437
    ///
438
    /// Panics if a duplicate ID is found.
439
    pub fn assert_unique_ids(constraints: &mut [ConstraintCircuit<II>]) {
48✔
440
        // inner uniqueness checks relies on reference counters being 0 for unseen nodes
441
        for circuit in constraints.iter_mut() {
317✔
442
            circuit.reset_ref_count_for_tree();
317✔
443
        }
317✔
444
        let mut ids: HashMap<usize, ConstraintCircuit<II>> = HashMap::new();
48✔
445
        for circuit in constraints.iter_mut() {
317✔
446
            circuit.assert_unique_ids_inner(&mut ids);
317✔
447
        }
317✔
448
    }
48✔
449

450
    /// Return degree of the multivariate polynomial represented by this circuit
451
    pub fn degree(&self) -> isize {
1,169,190,419✔
452
        if self.is_zero() {
1,169,190,419✔
453
            return -1;
×
454
        }
1,169,190,419✔
455

1,169,190,419✔
456
        match &self.expression {
1,169,190,419✔
457
            CircuitExpression::BinOp(binop, lhs, rhs) => {
566,573,140✔
458
                let degree_lhs = lhs.borrow().degree();
566,573,140✔
459
                let degree_rhs = rhs.borrow().degree();
566,573,140✔
460
                let degree_additive = cmp::max(degree_lhs, degree_rhs);
566,573,140✔
461
                let degree_multiplicative = if cmp::min(degree_lhs, degree_rhs) <= -1 {
566,573,140✔
462
                    -1
×
463
                } else {
464
                    degree_lhs + degree_rhs
566,573,140✔
465
                };
466
                match binop {
566,573,140✔
467
                    BinOp::Add => degree_additive,
259,784,321✔
468
                    BinOp::Mul => degree_multiplicative,
306,788,819✔
469
                }
470
            }
471
            CircuitExpression::Input(_) => 1,
330,780,638✔
472
            CircuitExpression::BConst(_)
473
            | CircuitExpression::XConst(_)
474
            | CircuitExpression::Challenge(_) => 0,
271,836,641✔
475
        }
476
    }
1,169,190,419✔
477

478
    /// All unique reference counters in the subtree, sorted.
479
    pub fn all_ref_counters(&self) -> Vec<usize> {
27✔
480
        let mut ref_counters = vec![self.ref_count];
27✔
481
        if let CircuitExpression::BinOp(_, lhs, rhs) = &self.expression {
27✔
482
            ref_counters.extend(lhs.borrow().all_ref_counters());
12✔
483
            ref_counters.extend(rhs.borrow().all_ref_counters());
12✔
484
        };
15✔
485
        ref_counters.sort_unstable();
27✔
486
        ref_counters.dedup();
27✔
487
        ref_counters
27✔
488
    }
27✔
489

490
    /// Is the node the constant 0?
491
    /// Does not catch composite expressions that will always evaluate to zero, like `0·a`.
492
    pub fn is_zero(&self) -> bool {
1,175,092,824✔
493
        match self.expression {
1,175,092,824✔
494
            CircuitExpression::BConst(bfe) => bfe.is_zero(),
88,665,310✔
495
            CircuitExpression::XConst(xfe) => xfe.is_zero(),
40,867✔
496
            _ => false,
1,086,386,647✔
497
        }
498
    }
1,175,092,824✔
499

500
    /// Is the node the constant 1?
501
    /// Does not catch composite expressions that will always evaluate to one, like `1·1`.
502
    pub fn is_one(&self) -> bool {
3,708,294✔
503
        match self.expression {
3,708,294✔
504
            CircuitExpression::BConst(bfe) => bfe.is_one(),
878,610✔
505
            CircuitExpression::XConst(xfe) => xfe.is_one(),
20,307✔
506
            _ => false,
2,809,377✔
507
        }
508
    }
3,708,294✔
509

510
    pub fn is_neg_one(&self) -> bool {
269✔
511
        match self.expression {
269✔
512
            CircuitExpression::BConst(bfe) => (-bfe).is_one(),
269✔
513
            CircuitExpression::XConst(xfe) => (-xfe).is_one(),
×
514
            _ => false,
×
515
        }
516
    }
269✔
517

518
    /// Recursively check whether this node is composed of only BFieldElements, i.e., only uses
519
    /// 1. inputs from main rows,
520
    /// 2. constants from the B-field, and
521
    /// 3. binary operations on BFieldElements.
522
    pub fn evaluates_to_base_element(&self) -> bool {
34,359✔
523
        match &self.expression {
34,359✔
524
            CircuitExpression::BConst(_) => true,
4,348✔
525
            CircuitExpression::XConst(_) => false,
237✔
526
            CircuitExpression::Input(indicator) => indicator.is_main_table_column(),
17,095✔
527
            CircuitExpression::Challenge(_) => false,
482✔
528
            CircuitExpression::BinOp(_, lhs, rhs) => {
12,197✔
529
                lhs.borrow().evaluates_to_base_element() && rhs.borrow().evaluates_to_base_element()
12,197✔
530
            }
531
        }
532
    }
34,359✔
533

534
    pub fn evaluate(
2,147,483,647✔
535
        &self,
2,147,483,647✔
536
        main_table: ArrayView2<BFieldElement>,
2,147,483,647✔
537
        aux_table: ArrayView2<XFieldElement>,
2,147,483,647✔
538
        challenges: &[XFieldElement],
2,147,483,647✔
539
    ) -> XFieldElement {
2,147,483,647✔
540
        match &self.expression {
2,147,483,647✔
541
            CircuitExpression::BConst(bfe) => bfe.lift(),
914,438,987✔
542
            CircuitExpression::XConst(xfe) => *xfe,
20,861✔
543
            CircuitExpression::Input(input) => input.evaluate(main_table, aux_table),
1,606,213,661✔
544
            CircuitExpression::Challenge(challenge_id) => challenges[*challenge_id],
382,713,133✔
545
            CircuitExpression::BinOp(binop, lhs, rhs) => {
2,147,483,647✔
546
                let lhs_value = lhs.borrow().evaluate(main_table, aux_table, challenges);
2,147,483,647✔
547
                let rhs_value = rhs.borrow().evaluate(main_table, aux_table, challenges);
2,147,483,647✔
548
                binop.operation(lhs_value, rhs_value)
2,147,483,647✔
549
            }
550
        }
551
    }
2,147,483,647✔
552
}
553

554
/// [`ConstraintCircuit`] with extra context pertaining to the whole multicircuit.
555
///
556
/// This context is needed to ensure that two equal nodes (meaning: same expression)
557
/// are not added to the multicircuit. It also enables a rudimentary check for node
558
/// equivalence (commutation + constant folding), in which case the existing expression
559
/// is used instead.
560
///
561
/// One can create new instances of [`ConstraintCircuitMonad`] by applying arithmetic
562
/// operations to existing instances, *e.g.*, `let c = a * b;`.
563
#[derive(Clone)]
564
pub struct ConstraintCircuitMonad<II: InputIndicator> {
565
    pub circuit: Rc<RefCell<ConstraintCircuit<II>>>,
566
    pub builder: ConstraintCircuitBuilder<II>,
567
}
568

569
impl<II: InputIndicator> Debug for ConstraintCircuitMonad<II> {
570
    // `all_nodes` contains itself, leading to infinite recursion during `Debug` printing.
571
    // Hence, this manual implementation.
572
    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
×
573
        f.debug_struct("ConstraintCircuitMonad")
×
574
            .field("id", &self.circuit)
×
575
            .field("all_nodes length: ", &self.builder.all_nodes.borrow().len())
×
576
            .field("id_counter_ref value: ", &self.builder.id_counter.borrow())
×
577
            .finish()
×
578
    }
×
579
}
580

581
impl<II: InputIndicator> Display for ConstraintCircuitMonad<II> {
582
    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
946✔
583
        write!(f, "{}", self.circuit.borrow())
946✔
584
    }
946✔
585
}
586

587
impl<II: InputIndicator> PartialEq for ConstraintCircuitMonad<II> {
588
    // Equality for the ConstraintCircuitMonad is defined by the circuit, not the
589
    // other metadata (e.g. ID) that it carries around.
590
    fn eq(&self, other: &Self) -> bool {
2,147,483,647✔
591
        self.circuit == other.circuit
2,147,483,647✔
592
    }
2,147,483,647✔
593
}
594

595
impl<II: InputIndicator> Eq for ConstraintCircuitMonad<II> {}
596

597
/// Helper function for binary operations that are used to generate new parent
598
/// nodes in the multitree that represents the algebraic circuit. Ensures that
599
/// each newly created node has a unique ID.
600
///
601
/// This function does not (currently) catch expressions of the form ((x+1)+1).
602
fn binop<II: InputIndicator>(
3,218,050✔
603
    binop: BinOp,
3,218,050✔
604
    lhs: ConstraintCircuitMonad<II>,
3,218,050✔
605
    rhs: ConstraintCircuitMonad<II>,
3,218,050✔
606
) -> ConstraintCircuitMonad<II> {
3,218,050✔
607
    assert!(lhs.builder.is_same_as(&rhs.builder));
3,218,050✔
608

609
    match (binop, &lhs, &rhs) {
3,218,050✔
610
        (BinOp::Add, _, zero) if zero.circuit.borrow().is_zero() => return lhs,
1,316,828✔
611
        (BinOp::Add, zero, _) if zero.circuit.borrow().is_zero() => return rhs,
1,167,528✔
612
        (BinOp::Mul, _, one) if one.circuit.borrow().is_one() => return lhs,
1,901,222✔
613
        (BinOp::Mul, one, _) if one.circuit.borrow().is_one() => return rhs,
1,807,072✔
614
        (BinOp::Mul, _, zero) if zero.circuit.borrow().is_zero() => return rhs,
1,778,282✔
615
        (BinOp::Mul, zero, _) if zero.circuit.borrow().is_zero() => return lhs,
1,638,490✔
616
        _ => (),
2,766,673✔
617
    };
2,766,673✔
618

2,766,673✔
619
    match (
2,766,673✔
620
        &lhs.circuit.borrow().expression,
2,766,673✔
621
        &rhs.circuit.borrow().expression,
2,766,673✔
622
    ) {
623
        (&CircuitExpression::BConst(l), &CircuitExpression::BConst(r)) => {
103,262✔
624
            return lhs.builder.b_constant(binop.operation(l, r))
103,262✔
625
        }
626
        (&CircuitExpression::BConst(l), &CircuitExpression::XConst(r)) => {
3,514✔
627
            return lhs.builder.x_constant(binop.operation(l, r))
3,514✔
628
        }
629
        (&CircuitExpression::XConst(l), &CircuitExpression::BConst(r)) => {
1,726✔
630
            return lhs.builder.x_constant(binop.operation(l, r))
1,726✔
631
        }
632
        (&CircuitExpression::XConst(l), &CircuitExpression::XConst(r)) => {
2,159✔
633
            return lhs.builder.x_constant(binop.operation(l, r))
2,159✔
634
        }
635
        _ => (),
2,656,012✔
636
    };
2,656,012✔
637

2,656,012✔
638
    // all `BinOp`s are commutative – try both orders of the operands
2,656,012✔
639
    let all_nodes = &mut lhs.builder.all_nodes.borrow_mut();
2,656,012✔
640
    let new_node = binop_new_node(binop, &rhs, &lhs);
2,656,012✔
641
    if let Some(node) = all_nodes.values().find(|&n| n == &new_node) {
2,147,483,647✔
642
        return node.to_owned();
1,436✔
643
    }
2,654,576✔
644

2,654,576✔
645
    let new_node = binop_new_node(binop, &lhs, &rhs);
2,654,576✔
646
    if let Some(node) = all_nodes.values().find(|&n| n == &new_node) {
2,147,483,647✔
647
        return node.to_owned();
1,429,540✔
648
    }
1,225,036✔
649

1,225,036✔
650
    let new_id = new_node.circuit.borrow().id;
1,225,036✔
651
    let maybe_existing_node = all_nodes.insert(new_id, new_node.clone());
1,225,036✔
652
    let new_node_is_new = maybe_existing_node.is_none();
1,225,036✔
653
    assert!(new_node_is_new, "new node must not overwrite existing node");
1,225,036✔
654
    lhs.builder.id_counter.borrow_mut().add_assign(1);
1,225,036✔
655
    new_node
1,225,036✔
656
}
3,218,050✔
657

658
fn binop_new_node<II: InputIndicator>(
5,310,588✔
659
    binop: BinOp,
5,310,588✔
660
    lhs: &ConstraintCircuitMonad<II>,
5,310,588✔
661
    rhs: &ConstraintCircuitMonad<II>,
5,310,588✔
662
) -> ConstraintCircuitMonad<II> {
5,310,588✔
663
    let id = lhs.builder.id_counter.borrow().to_owned();
5,310,588✔
664
    let expression = CircuitExpression::BinOp(binop, lhs.circuit.clone(), rhs.circuit.clone());
5,310,588✔
665
    let circuit = ConstraintCircuit::new(id, expression);
5,310,588✔
666
    lhs.builder.new_monad(circuit)
5,310,588✔
667
}
5,310,588✔
668

669
impl<II: InputIndicator> Add for ConstraintCircuitMonad<II> {
670
    type Output = ConstraintCircuitMonad<II>;
671

672
    fn add(self, rhs: Self) -> Self::Output {
777,013✔
673
        binop(BinOp::Add, self, rhs)
777,013✔
674
    }
777,013✔
675
}
676

677
impl<II: InputIndicator> Sub for ConstraintCircuitMonad<II> {
678
    type Output = ConstraintCircuitMonad<II>;
679

680
    fn sub(self, rhs: Self) -> Self::Output {
539,815✔
681
        binop(BinOp::Add, self, -rhs)
539,815✔
682
    }
539,815✔
683
}
684

685
impl<II: InputIndicator> Mul for ConstraintCircuitMonad<II> {
686
    type Output = ConstraintCircuitMonad<II>;
687

688
    fn mul(self, rhs: Self) -> Self::Output {
1,361,407✔
689
        binop(BinOp::Mul, self, rhs)
1,361,407✔
690
    }
1,361,407✔
691
}
692

693
impl<II: InputIndicator> Neg for ConstraintCircuitMonad<II> {
694
    type Output = ConstraintCircuitMonad<II>;
695

696
    fn neg(self) -> Self::Output {
539,815✔
697
        binop(BinOp::Mul, self.builder.minus_one(), self)
539,815✔
698
    }
539,815✔
699
}
700

701
/// This will panic if the iterator is empty because the neutral element needs a unique ID, and
702
/// we have no way of getting that here.
703
impl<II: InputIndicator> Sum for ConstraintCircuitMonad<II> {
704
    fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
27,063✔
705
        iter.reduce(|accum, item| accum + item)
470,459✔
706
            .expect("ConstraintCircuitMonad Iterator was empty")
27,063✔
707
    }
27,063✔
708
}
709

710
struct EvolvingMainConstraintsNumber(usize);
711
impl From<EvolvingMainConstraintsNumber> for usize {
712
    fn from(value: EvolvingMainConstraintsNumber) -> Self {
114✔
713
        value.0
114✔
714
    }
114✔
715
}
716

717
struct EvolvingAuxConstraintsNumber(usize);
718
impl From<EvolvingAuxConstraintsNumber> for usize {
719
    fn from(value: EvolvingAuxConstraintsNumber) -> Self {
936✔
720
        value.0
936✔
721
    }
936✔
722
}
723

724
impl<II: InputIndicator> ConstraintCircuitMonad<II> {
725
    /// Unwrap a ConstraintCircuitMonad to reveal its inner ConstraintCircuit
726
    pub fn consume(&self) -> ConstraintCircuit<II> {
166,484✔
727
        self.circuit.borrow().to_owned()
166,484✔
728
    }
166,484✔
729

730
    /// Lower the degree of a given multicircuit to the target degree.
731
    /// This is achieved by introducing additional variables and constraints.
732
    /// The appropriate substitutions are applied to the given multicircuit.
733
    /// The target degree must be greater than 1.
734
    ///
735
    /// The new constraints are returned as two vector of ConstraintCircuitMonads:
736
    /// the first corresponds to main columns and constraints,
737
    /// the second to auxiliary columns and constraints. The modifications are
738
    /// applied to the function argument in-place.
739
    ///
740
    /// Each returned constraint is guaranteed to correspond to some
741
    /// `CircuitExpression::BinaryOperation(BinOp::Sub, lhs, rhs)` where
742
    /// - `lhs` is the new variable, and
743
    /// - `rhs` is the (sub)circuit replaced by `lhs`.
744
    ///   These can then be used to construct new columns,
745
    ///   as well as derivation rules for filling those new columns.
746
    ///
747
    /// For example, starting with the constraint set {x^4}, we insert
748
    /// {y - x^2} and modify in-place (x^4) --> (y^2).
749
    ///
750
    /// The highest index of main and auxiliary columns used by the multicircuit have to be
751
    /// provided. The uniqueness of the new columns' indices depends on these provided values.
752
    /// Note that these indices are generally not equal to the number of used columns, especially
753
    /// when a tables' constraints are built using the master table's column indices.
754
    pub fn lower_to_degree(
160✔
755
        multicircuit: &mut [Self],
160✔
756
        info: DegreeLoweringInfo,
160✔
757
    ) -> (Vec<Self>, Vec<Self>) {
160✔
758
        let target_degree = info.target_degree;
160✔
759
        assert!(
160✔
760
            target_degree > 1,
160✔
761
            "Target degree must be greater than 1. Got {target_degree}."
×
762
        );
763

764
        let mut main_constraints = vec![];
160✔
765
        let mut aux_constraints = vec![];
160✔
766

160✔
767
        if multicircuit.is_empty() {
160✔
768
            return (main_constraints, aux_constraints);
28✔
769
        }
132✔
770

771
        while Self::multicircuit_degree(multicircuit) > target_degree {
1,520✔
772
            let chosen_node_id = Self::pick_node_to_substitute(multicircuit, target_degree);
1,388✔
773

1,388✔
774
            let new_constraint = Self::apply_substitution(
1,388✔
775
                multicircuit,
1,388✔
776
                info,
1,388✔
777
                chosen_node_id,
1,388✔
778
                EvolvingMainConstraintsNumber(main_constraints.len()),
1,388✔
779
                EvolvingAuxConstraintsNumber(aux_constraints.len()),
1,388✔
780
            );
1,388✔
781

1,388✔
782
            if new_constraint.circuit.borrow().evaluates_to_base_element() {
1,388✔
783
                main_constraints.push(new_constraint)
1,207✔
784
            } else {
785
                aux_constraints.push(new_constraint)
181✔
786
            }
787
        }
788

789
        (main_constraints, aux_constraints)
132✔
790
    }
160✔
791

792
    /// Apply a substitution:
793
    ///  - create a new variable to replaces the chosen node;
794
    ///  - make all nodes that point to the chosen node point to the new
795
    ///    variable instead;
796
    ///  - return the new constraint that makes it sound: new variable minus
797
    ///    chosen node's expression.
798
    fn apply_substitution(
2,388✔
799
        multicircuit: &mut [Self],
2,388✔
800
        info: DegreeLoweringInfo,
2,388✔
801
        chosen_node_id: usize,
2,388✔
802
        new_main_constraints_count: EvolvingMainConstraintsNumber,
2,388✔
803
        new_aux_constraints_count: EvolvingAuxConstraintsNumber,
2,388✔
804
    ) -> ConstraintCircuitMonad<II> {
2,388✔
805
        let builder = multicircuit[0].builder.clone();
2,388✔
806

2,388✔
807
        // Create a new variable.
2,388✔
808
        let chosen_node = builder.all_nodes.borrow()[&chosen_node_id].clone();
2,388✔
809
        let chosen_node_is_main_col = chosen_node.circuit.borrow().evaluates_to_base_element();
2,388✔
810
        let new_input_indicator = if chosen_node_is_main_col {
2,388✔
811
            let new_main_col_idx = info.num_main_cols + usize::from(new_main_constraints_count);
1,272✔
812
            II::main_table_input(new_main_col_idx)
1,272✔
813
        } else {
814
            let new_aux_col_idx = info.num_aux_cols + usize::from(new_aux_constraints_count);
1,116✔
815
            II::aux_table_input(new_aux_col_idx)
1,116✔
816
        };
817
        let new_variable = builder.input(new_input_indicator);
2,388✔
818

2,388✔
819
        // Make all descendants of the chosen node point to the new variable instead
2,388✔
820
        builder.redirect_all_references_to_node(chosen_node_id, new_variable.clone());
2,388✔
821

822
        // Treat roots of the multicircuit explicitly.
823
        for circuit in multicircuit.iter_mut() {
133,650✔
824
            if circuit.circuit.borrow().id == chosen_node_id {
133,650✔
825
                circuit.circuit = new_variable.circuit.clone();
59✔
826
            }
133,591✔
827
        }
828

829
        // return substitution equation
830
        new_variable - chosen_node
2,388✔
831
    }
2,388✔
832

833
    /// Heuristically pick a node from the given multicircuit that is to be substituted with a new
834
    /// variable. The ID of the chosen node is returned.
835
    fn pick_node_to_substitute(
1,409✔
836
        multicircuit: &[ConstraintCircuitMonad<II>],
1,409✔
837
        target_degree: isize,
1,409✔
838
    ) -> usize {
1,409✔
839
        assert!(!multicircuit.is_empty());
1,409✔
840
        let multicircuit = multicircuit
1,409✔
841
            .iter()
1,409✔
842
            .map(|c| c.clone().consume())
123,692✔
843
            .collect_vec();
1,409✔
844

1,409✔
845
        // Computing all node degree is slow; this cache de-duplicates work.
1,409✔
846
        let node_degrees = Self::all_nodes_in_multicircuit(&multicircuit)
1,409✔
847
            .into_iter()
1,409✔
848
            .map(|node| (node.id, node.degree()))
35,915,396✔
849
            .collect::<HashMap<_, _>>();
1,409✔
850

1,409✔
851
        // Only nodes with degree > target_degree need changing.
1,409✔
852
        let high_degree_nodes = Self::all_nodes_in_multicircuit(&multicircuit)
1,409✔
853
            .into_iter()
1,409✔
854
            .filter(|node| node_degrees[&node.id] > target_degree)
35,915,396✔
855
            .unique()
1,409✔
856
            .collect_vec();
1,409✔
857

1,409✔
858
        // Collect all candidates for substitution, i.e., descendents of high_degree_nodes
1,409✔
859
        // with degree <= target_degree.
1,409✔
860
        // Substituting a node of degree 1 is both pointless and can lead to infinite iteration.
1,409✔
861
        let low_degree_nodes = Self::all_nodes_in_multicircuit(&high_degree_nodes)
1,409✔
862
            .into_iter()
1,409✔
863
            .filter(|node| 1 < node_degrees[&node.id] && node_degrees[&node.id] <= target_degree)
394,792,928✔
864
            .map(|node| node.id)
37,937,892✔
865
            .collect_vec();
1,409✔
866

1,409✔
867
        // If the resulting list is empty, there is no way forward. Stop – panic time!
1,409✔
868
        assert!(!low_degree_nodes.is_empty(), "Cannot lower degree.");
1,409✔
869

870
        // Of the remaining nodes, keep the ones occurring the most often.
871
        let mut nodes_and_occurrences = HashMap::new();
1,409✔
872
        for node in low_degree_nodes {
37,939,301✔
873
            *nodes_and_occurrences.entry(node).or_insert(0) += 1;
37,937,892✔
874
        }
37,937,892✔
875
        let max_occurrences = nodes_and_occurrences.iter().map(|(_, &c)| c).max().unwrap();
1,424,536✔
876
        nodes_and_occurrences.retain(|_, &mut count| count == max_occurrences);
1,424,536✔
877
        let mut candidate_node_ids = nodes_and_occurrences.keys().copied().collect_vec();
1,409✔
878

1,409✔
879
        // If there are still multiple nodes, pick the one with the highest degree.
1,409✔
880
        let max_degree = candidate_node_ids
1,409✔
881
            .iter()
1,409✔
882
            .map(|node_id| node_degrees[node_id])
18,739✔
883
            .max()
1,409✔
884
            .unwrap();
1,409✔
885
        candidate_node_ids.retain(|node_id| node_degrees[node_id] == max_degree);
18,739✔
886

1,409✔
887
        candidate_node_ids.sort_unstable();
1,409✔
888

1,409✔
889
        // If there are still multiple nodes, pick any one – but deterministically so.
1,409✔
890
        candidate_node_ids.into_iter().min().unwrap()
1,409✔
891
    }
1,409✔
892

893
    /// Returns all nodes used in the multicircuit.
894
    /// This is distinct from `ConstraintCircuitBuilder::all_nodes` because it
895
    /// 1. only considers nodes used in the given multicircuit, not all nodes in the builder,
896
    /// 2. returns the nodes as [`ConstraintCircuit`]s, not as [`ConstraintCircuitMonad`]s, and
897
    /// 3. keeps duplicates, allowing to count how often a node occurs.
898
    pub fn all_nodes_in_multicircuit(
4,228✔
899
        multicircuit: &[ConstraintCircuit<II>],
4,228✔
900
    ) -> Vec<ConstraintCircuit<II>> {
4,228✔
901
        multicircuit
4,228✔
902
            .iter()
4,228✔
903
            .flat_map(Self::all_nodes_in_circuit)
4,228✔
904
            .collect()
4,228✔
905
    }
4,228✔
906

907
    /// Internal helper function to recursively find all nodes in a circuit.
908
    fn all_nodes_in_circuit(circuit: &ConstraintCircuit<II>) -> Vec<ConstraintCircuit<II>> {
466,751,572✔
909
        let mut all_nodes = vec![];
466,751,572✔
910
        if let CircuitExpression::BinOp(_, lhs, rhs) = circuit.expression.clone() {
466,751,572✔
911
            let lhs_nodes = Self::all_nodes_in_circuit(&lhs.borrow());
232,882,941✔
912
            let rhs_nodes = Self::all_nodes_in_circuit(&rhs.borrow());
232,882,941✔
913
            all_nodes.extend(lhs_nodes);
232,882,941✔
914
            all_nodes.extend(rhs_nodes);
232,882,941✔
915
        };
233,868,631✔
916
        all_nodes.push(circuit.to_owned());
466,751,572✔
917
        all_nodes
466,751,572✔
918
    }
466,751,572✔
919

920
    /// Counts the number of nodes in this multicircuit. Only counts nodes that
921
    /// are used; not nodes that have been forgotten.
922
    pub fn num_visible_nodes(constraints: &[Self]) -> usize {
12✔
923
        constraints
12✔
924
            .iter()
12✔
925
            .flat_map(|ccm| Self::all_nodes_in_circuit(&ccm.circuit.borrow()))
1,384✔
926
            .unique()
12✔
927
            .count()
12✔
928
    }
12✔
929

930
    /// Returns the maximum degree of all circuits in the multicircuit.
931
    pub fn multicircuit_degree(multicircuit: &[ConstraintCircuitMonad<II>]) -> isize {
1,748✔
932
        multicircuit
1,748✔
933
            .iter()
1,748✔
934
            .map(|circuit| circuit.circuit.borrow().degree())
127,633✔
935
            .max()
1,748✔
936
            .unwrap_or(-1)
1,748✔
937
    }
1,748✔
938
}
939

940
/// Helper struct to construct new leaf nodes (*i.e.*, input or challenge or constant)
941
/// in the circuit multitree. Ensures that newly created nodes, even non-leaf nodes
942
/// created through joining two other nodes using an arithmetic operation, get a
943
/// unique ID.
944
#[derive(Debug, Clone, Eq, PartialEq)]
945
pub struct ConstraintCircuitBuilder<II: InputIndicator> {
946
    id_counter: Rc<RefCell<usize>>,
947
    all_nodes: Rc<RefCell<HashMap<usize, ConstraintCircuitMonad<II>>>>,
948
}
949

950
impl<II: InputIndicator> Default for ConstraintCircuitBuilder<II> {
951
    fn default() -> Self {
×
952
        Self::new()
×
953
    }
×
954
}
955

956
impl<II: InputIndicator> ConstraintCircuitBuilder<II> {
957
    pub fn new() -> Self {
8,656✔
958
        Self {
8,656✔
959
            id_counter: Rc::new(RefCell::new(0)),
8,656✔
960
            all_nodes: Rc::new(RefCell::new(HashMap::new())),
8,656✔
961
        }
8,656✔
962
    }
8,656✔
963

964
    /// Check whether two builders are the same.
965
    ///
966
    /// Notably, this is distinct from checking equality: two builders are equal if they are in the
967
    /// same state. Two builders are the same if they are the same instance.
968
    pub fn is_same_as(&self, other: &Self) -> bool {
3,218,050✔
969
        Rc::ptr_eq(&self.id_counter, &other.id_counter)
3,218,050✔
970
            && Rc::ptr_eq(&self.all_nodes, &other.all_nodes)
3,218,050✔
971
    }
3,218,050✔
972

973
    fn new_monad(&self, circuit: ConstraintCircuit<II>) -> ConstraintCircuitMonad<II> {
8,028,483✔
974
        let circuit = Rc::new(RefCell::new(circuit));
8,028,483✔
975
        ConstraintCircuitMonad {
8,028,483✔
976
            circuit,
8,028,483✔
977
            builder: self.clone(),
8,028,483✔
978
        }
8,028,483✔
979
    }
8,028,483✔
980

981
    /// The unique monad representing the constant value 0.
982
    pub fn zero(&self) -> ConstraintCircuitMonad<II> {
9,814✔
983
        self.b_constant(0)
9,814✔
984
    }
9,814✔
985

986
    /// The unique monad representing the constant value 1.
987
    pub fn one(&self) -> ConstraintCircuitMonad<II> {
8,976✔
988
        self.b_constant(1)
8,976✔
989
    }
8,976✔
990

991
    /// The unique monad representing the constant value -1.
992
    pub fn minus_one(&self) -> ConstraintCircuitMonad<II> {
539,815✔
993
        self.b_constant(-1)
539,815✔
994
    }
539,815✔
995

996
    /// Leaf node with constant over the [base field][BFieldElement].
997
    pub fn b_constant<B>(&self, bfe: B) -> ConstraintCircuitMonad<II>
1,121,121✔
998
    where
1,121,121✔
999
        B: Into<BFieldElement>,
1,121,121✔
1000
    {
1,121,121✔
1001
        self.make_leaf(CircuitExpression::BConst(bfe.into()))
1,121,121✔
1002
    }
1,121,121✔
1003

1004
    /// Leaf node with constant over the [extension field][XFieldElement].
1005
    pub fn x_constant<X>(&self, xfe: X) -> ConstraintCircuitMonad<II>
24,628✔
1006
    where
24,628✔
1007
        X: Into<XFieldElement>,
24,628✔
1008
    {
24,628✔
1009
        self.make_leaf(CircuitExpression::XConst(xfe.into()))
24,628✔
1010
    }
24,628✔
1011

1012
    /// Create deterministic input leaf node.
1013
    pub fn input(&self, input: II) -> ConstraintCircuitMonad<II> {
1,101,765✔
1014
        self.make_leaf(CircuitExpression::Input(input))
1,101,765✔
1015
    }
1,101,765✔
1016

1017
    /// Create challenge leaf node.
1018
    pub fn challenge<C>(&self, challenge: C) -> ConstraintCircuitMonad<II>
470,381✔
1019
    where
470,381✔
1020
        C: Into<usize>,
470,381✔
1021
    {
470,381✔
1022
        self.make_leaf(CircuitExpression::Challenge(challenge.into()))
470,381✔
1023
    }
470,381✔
1024

1025
    fn make_leaf(&self, mut expression: CircuitExpression<II>) -> ConstraintCircuitMonad<II> {
2,717,895✔
1026
        assert!(
2,717,895✔
1027
            !matches!(expression, CircuitExpression::BinOp(_, _, _)),
2,717,895✔
1028
            "`make_leaf` is intended for anything but `BinOp`s"
×
1029
        );
1030

1031
        // Don't generate an X field leaf if it can be expressed as a B field leaf
1032
        if let CircuitExpression::XConst(xfe) = expression {
2,717,895✔
1033
            if let Some(bfe) = xfe.unlift() {
24,628✔
1034
                expression = CircuitExpression::BConst(bfe);
3,218✔
1035
            }
21,410✔
1036
        }
2,693,267✔
1037

1038
        let id = self.id_counter.borrow().to_owned();
2,717,895✔
1039
        let circuit = ConstraintCircuit::new(id, expression);
2,717,895✔
1040
        let new_node = self.new_monad(circuit);
2,717,895✔
1041

1042
        if let Some(same_node) = self.all_nodes.borrow().values().find(|&n| n == &new_node) {
2,065,477,010✔
1043
            return same_node.to_owned();
2,478,027✔
1044
        }
239,868✔
1045

239,868✔
1046
        let maybe_previous_node = self.all_nodes.borrow_mut().insert(id, new_node.clone());
239,868✔
1047
        let new_node_is_new = maybe_previous_node.is_none();
239,868✔
1048
        assert!(new_node_is_new, "Leaf-created node must be new… {new_node}");
239,868✔
1049
        self.id_counter.borrow_mut().add_assign(1);
239,868✔
1050
        new_node
239,868✔
1051
    }
2,717,895✔
1052

1053
    /// Replace all pointers to a given node (identified by `old_id`) by one
1054
    /// to the new node.
1055
    ///
1056
    /// A circuit's root node cannot be substituted with this method. Manual care
1057
    /// must be taken to update the root node if necessary.
1058
    fn redirect_all_references_to_node(&self, old_id: usize, new: ConstraintCircuitMonad<II>) {
2,389✔
1059
        self.all_nodes.borrow_mut().remove(&old_id);
2,389✔
1060
        for node in self.all_nodes.borrow_mut().values_mut() {
7,152,141✔
1061
            let node_expression = &mut node.circuit.borrow_mut().expression;
7,152,141✔
1062
            let CircuitExpression::BinOp(_, ref mut node_lhs, ref mut node_rhs) = node_expression
7,152,141✔
1063
            else {
1064
                continue;
533,777✔
1065
            };
1066

1067
            if node_lhs.borrow().id == old_id {
6,618,364✔
1068
                *node_lhs = new.circuit.clone();
8,400✔
1069
            }
6,609,964✔
1070
            if node_rhs.borrow().id == old_id {
6,618,364✔
1071
                *node_rhs = new.circuit.clone();
2,068✔
1072
            }
6,616,296✔
1073
        }
1074
    }
2,389✔
1075
}
1076

1077
impl<'a, II: InputIndicator + Arbitrary<'a>> Arbitrary<'a> for ConstraintCircuitMonad<II> {
1078
    fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
2,069✔
1079
        let builder = ConstraintCircuitBuilder::new();
2,069✔
1080
        let mut random_circuit = random_circuit_leaf(&builder, u)?;
2,069✔
1081

1082
        let num_nodes_in_circuit = u.arbitrary_len::<Self>()?;
2,069✔
1083
        for _ in 0..num_nodes_in_circuit {
2,069✔
1084
            let leaf = random_circuit_leaf(&builder, u)?;
244,628✔
1085
            match u.int_in_range(0..=5)? {
244,628✔
1086
                0 => random_circuit = random_circuit * leaf,
208,068✔
1087
                1 => random_circuit = random_circuit + leaf,
7,586✔
1088
                2 => random_circuit = random_circuit - leaf,
7,390✔
1089
                3 => random_circuit = leaf * random_circuit,
7,239✔
1090
                4 => random_circuit = leaf + random_circuit,
7,215✔
1091
                5 => random_circuit = leaf - random_circuit,
7,130✔
1092
                _ => unreachable!(),
×
1093
            }
1094
        }
1095

1096
        Ok(random_circuit)
2,069✔
1097
    }
2,069✔
1098
}
1099

1100
fn random_circuit_leaf<'a, II: InputIndicator + Arbitrary<'a>>(
246,697✔
1101
    builder: &ConstraintCircuitBuilder<II>,
246,697✔
1102
    u: &mut Unstructured<'a>,
246,697✔
1103
) -> arbitrary::Result<ConstraintCircuitMonad<II>> {
246,697✔
1104
    let leaf = match u.int_in_range(0..=5)? {
246,697✔
1105
        0 => builder.input(u.arbitrary()?),
207,158✔
1106
        1 => builder.challenge(u.arbitrary::<usize>()?),
7,984✔
1107
        2 => builder.b_constant(u.arbitrary::<BFieldElement>()?),
7,897✔
1108
        3 => builder.x_constant(u.arbitrary::<XFieldElement>()?),
7,935✔
1109
        4 => builder.one(),
7,952✔
1110
        5 => builder.zero(),
7,771✔
1111
        _ => unreachable!(),
×
1112
    };
1113
    Ok(leaf)
246,697✔
1114
}
246,697✔
1115

1116
#[cfg(test)]
1117
mod tests {
1118
    use std::collections::hash_map::DefaultHasher;
1119
    use std::hash::Hasher;
1120
    use std::ops::Not;
1121

1122
    use itertools::Itertools;
1123
    use ndarray::Array2;
1124
    use ndarray::Axis;
1125
    use proptest::arbitrary::Arbitrary;
1126
    use proptest::collection::vec;
1127
    use proptest::prelude::*;
1128
    use proptest_arbitrary_interop::arb;
1129
    use rand::random;
1130
    use test_strategy::proptest;
1131

1132
    use super::*;
1133

1134
    impl<II: InputIndicator> ConstraintCircuitMonad<II> {
1135
        /// Traverse the circuit and find all nodes that are equivalent. Note that
1136
        /// two nodes are equivalent if they compute the same value on all identical
1137
        /// inputs. Equivalence is different from identity, which is when two nodes
1138
        /// connect the same set of neighbors in the same way. (There may be two
1139
        /// different ways to compute the same result; they are equivalent but
1140
        /// unequal.)
1141
        ///
1142
        /// This function returns a list of lists of equivalent nodes such that
1143
        /// every inner list can be reduced to a single node without changing the
1144
        /// circuit's function.
1145
        ///
1146
        /// Equivalent nodes are detected probabilistically using the multivariate
1147
        /// Schwartz-Zippel lemma. The false positive probability is zero (we can be
1148
        /// certain that equivalent nodes will be found). The false negative
1149
        /// probability is bounded by max_degree / (2^64 - 2^32 + 1)^3.
1150
        pub fn find_equivalent_nodes(&self) -> Vec<Vec<Rc<RefCell<ConstraintCircuit<II>>>>> {
2✔
1151
            let mut id_to_eval = HashMap::new();
2✔
1152
            let mut eval_to_ids = HashMap::new();
2✔
1153
            let mut id_to_node = HashMap::new();
2✔
1154
            Self::probe_random(
2✔
1155
                &self.circuit,
2✔
1156
                &mut id_to_eval,
2✔
1157
                &mut eval_to_ids,
2✔
1158
                &mut id_to_node,
2✔
1159
                random(),
2✔
1160
            );
2✔
1161

2✔
1162
            eval_to_ids
2✔
1163
                .values()
2✔
1164
                .filter(|ids| ids.len() >= 2)
36✔
1165
                .map(|ids| ids.iter().map(|i| id_to_node[i].clone()).collect_vec())
2✔
1166
                .collect_vec()
2✔
1167
        }
2✔
1168

1169
        /// Populate the dictionaries such that they associate with every node in
1170
        /// the circuit its evaluation in a random point. The inputs are assigned
1171
        /// random values. Equivalent nodes are detected based on evaluating to the
1172
        /// same value using the Schwartz-Zippel lemma.
1173
        fn probe_random(
52✔
1174
            circuit: &Rc<RefCell<ConstraintCircuit<II>>>,
52✔
1175
            id_to_eval: &mut HashMap<usize, XFieldElement>,
52✔
1176
            eval_to_ids: &mut HashMap<XFieldElement, Vec<usize>>,
52✔
1177
            id_to_node: &mut HashMap<usize, Rc<RefCell<ConstraintCircuit<II>>>>,
52✔
1178
            master_seed: XFieldElement,
52✔
1179
        ) -> XFieldElement {
52✔
1180
            const DOMAIN_SEPARATOR_CURR_ROW: BFieldElement = BFieldElement::new(0);
1181
            const DOMAIN_SEPARATOR_NEXT_ROW: BFieldElement = BFieldElement::new(1);
1182
            const DOMAIN_SEPARATOR_CHALLENGE: BFieldElement = BFieldElement::new(2);
1183

1184
            let circuit_id = circuit.borrow().id;
52✔
1185
            if let Some(&xfe) = id_to_eval.get(&circuit_id) {
52✔
1186
                return xfe;
15✔
1187
            }
37✔
1188

1189
            let evaluation = match &circuit.borrow().expression {
37✔
1190
                CircuitExpression::BConst(bfe) => bfe.lift(),
2✔
1191
                CircuitExpression::XConst(xfe) => *xfe,
×
1192
                CircuitExpression::Input(input) => {
8✔
1193
                    let [s0, s1, s2] = master_seed.coefficients;
8✔
1194
                    let dom_sep = if input.is_current_row() {
8✔
1195
                        DOMAIN_SEPARATOR_CURR_ROW
8✔
1196
                    } else {
1197
                        DOMAIN_SEPARATOR_NEXT_ROW
×
1198
                    };
1199
                    let i = bfe!(u64::try_from(input.column()).unwrap());
8✔
1200
                    let Digest([d0, d1, d2, _, _]) = Tip5::hash_varlen(&[s0, s1, s2, dom_sep, i]);
8✔
1201
                    xfe!([d0, d1, d2])
8✔
1202
                }
1203
                CircuitExpression::Challenge(challenge) => {
2✔
1204
                    let [s0, s1, s2] = master_seed.coefficients;
2✔
1205
                    let dom_sep = DOMAIN_SEPARATOR_CHALLENGE;
2✔
1206
                    let ch = bfe!(u64::try_from(*challenge).unwrap());
2✔
1207
                    let Digest([d0, d1, d2, _, _]) = Tip5::hash_varlen(&[s0, s1, s2, dom_sep, ch]);
2✔
1208
                    xfe!([d0, d1, d2])
2✔
1209
                }
1210
                CircuitExpression::BinOp(bin_op, lhs, rhs) => {
25✔
1211
                    let l =
25✔
1212
                        Self::probe_random(lhs, id_to_eval, eval_to_ids, id_to_node, master_seed);
25✔
1213
                    let r =
25✔
1214
                        Self::probe_random(rhs, id_to_eval, eval_to_ids, id_to_node, master_seed);
25✔
1215
                    bin_op.operation(l, r)
25✔
1216
                }
1217
            };
1218

1219
            id_to_eval.insert(circuit_id, evaluation);
37✔
1220
            eval_to_ids.entry(evaluation).or_default().push(circuit_id);
37✔
1221
            id_to_node.insert(circuit_id, circuit.clone());
37✔
1222

37✔
1223
            evaluation
37✔
1224
        }
52✔
1225

1226
        /// Check whether the given node is contained in this circuit.
1227
        fn contains(&self, other: &Self) -> bool {
9✔
1228
            let self_expression = &self.circuit.borrow().expression;
9✔
1229
            let other_expression = &other.circuit.borrow().expression;
9✔
1230
            self_expression.contains(other_expression)
9✔
1231
        }
9✔
1232

1233
        /// Produces an iter over all nodes in the multicircuit, if it is non-empty.
1234
        ///
1235
        /// Helper function for counting the number of nodes of a specific type.
1236
        fn iter_nodes(
6,307✔
1237
            constraints: &[Self],
6,307✔
1238
        ) -> std::vec::IntoIter<(usize, ConstraintCircuitMonad<II>)> {
6,307✔
1239
            let Some(first) = constraints.first() else {
6,307✔
1240
                return vec![].into_iter();
×
1241
            };
1242

1243
            first
6,307✔
1244
                .builder
6,307✔
1245
                .all_nodes
6,307✔
1246
                .borrow()
6,307✔
1247
                .iter()
6,307✔
1248
                .map(|(n, m)| (*n, m.clone()))
962,989✔
1249
                .collect_vec()
6,307✔
1250
                .into_iter()
6,307✔
1251
        }
6,307✔
1252

1253
        /// The total number of nodes in the multicircuit
1254
        fn num_nodes(constraints: &[Self]) -> usize {
1,256✔
1255
            Self::iter_nodes(constraints).count()
1,256✔
1256
        }
1,256✔
1257

1258
        /// Determine if the constraint circuit monad corresponds to a main table
1259
        /// column.
1260
        fn is_main_table_column(&self) -> bool {
470,126✔
1261
            if let CircuitExpression::Input(ii) = self.circuit.borrow().expression {
470,126✔
1262
                ii.is_main_table_column()
30,260✔
1263
            } else {
1264
                false
439,866✔
1265
            }
1266
        }
470,126✔
1267

1268
        /// The number of inputs from the main table
1269
        fn num_main_inputs(constraints: &[Self]) -> usize {
1,513✔
1270
            Self::iter_nodes(constraints)
1,513✔
1271
                .filter(|(_, cc)| cc.is_main_table_column())
235,063✔
1272
                .filter(|(_, cc)| cc.circuit.borrow().evaluates_to_base_element())
7,407✔
1273
                .count()
1,513✔
1274
        }
1,513✔
1275

1276
        /// The number of inputs from the aux table
1277
        fn num_aux_inputs(constraints: &[Self]) -> usize {
1,513✔
1278
            Self::iter_nodes(constraints)
1,513✔
1279
                .filter(|(_, cc)| !cc.is_main_table_column())
235,063✔
1280
                .filter(|(_, cc)| {
227,656✔
1281
                    matches!(cc.circuit.borrow().expression, CircuitExpression::Input(_))
227,656✔
1282
                })
227,656✔
1283
                .count()
1,513✔
1284
        }
1,513✔
1285

1286
        /// The number of total (*i.e.*, main + aux) inputs
1287
        fn num_inputs(constraints: &[Self]) -> usize {
256✔
1288
            Self::num_main_inputs(constraints) + Self::num_aux_inputs(constraints)
256✔
1289
        }
256✔
1290

1291
        /// The number of challenges
1292
        fn num_challenges(constraints: &[Self]) -> usize {
1,257✔
1293
            Self::iter_nodes(constraints)
1,257✔
1294
                .filter(|(_, cc)| {
212,137✔
1295
                    matches!(
199,567✔
1296
                        cc.circuit.borrow().expression,
212,137✔
1297
                        CircuitExpression::Challenge(_)
1298
                    )
1299
                })
212,137✔
1300
                .count()
1,257✔
1301
        }
1,257✔
1302

1303
        /// The number of `BinOp`s
1304
        fn num_binops(constraints: &[Self]) -> usize {
256✔
1305
            Self::iter_nodes(constraints)
256✔
1306
                .filter(|(_, cc)| {
22,926✔
1307
                    matches!(
8,427✔
1308
                        cc.circuit.borrow().expression,
22,926✔
1309
                        CircuitExpression::BinOp(_, _, _)
1310
                    )
1311
                })
22,926✔
1312
                .count()
256✔
1313
        }
256✔
1314

1315
        /// The number of BFE constants
1316
        fn num_bfield_constants(constraints: &[Self]) -> usize {
256✔
1317
            Self::iter_nodes(constraints)
256✔
1318
                .filter(|(_, cc)| {
22,926✔
1319
                    matches!(cc.circuit.borrow().expression, CircuitExpression::BConst(_))
22,926✔
1320
                })
22,926✔
1321
                .count()
256✔
1322
        }
256✔
1323

1324
        /// The number of XFE constants
1325
        fn num_xfield_constants(constraints: &[Self]) -> usize {
256✔
1326
            Self::iter_nodes(constraints)
256✔
1327
                .filter(|(_, cc)| {
22,926✔
1328
                    matches!(
21,107✔
1329
                        cc.circuit.as_ref().borrow().expression,
22,926✔
1330
                        CircuitExpression::XConst(_)
1331
                    )
1332
                })
22,926✔
1333
                .count()
256✔
1334
        }
256✔
1335
    }
1336

1337
    impl<II: InputIndicator> CircuitExpression<II> {
1338
        /// Check whether the given node is contained in this circuit.
1339
        fn contains(&self, other: &Self) -> bool {
43✔
1340
            if self == other {
43✔
1341
                return true;
5✔
1342
            }
38✔
1343
            let CircuitExpression::BinOp(_, lhs, rhs) = self else {
38✔
1344
                return false;
17✔
1345
            };
1346

1347
            lhs.borrow().expression.contains(other) || rhs.borrow().expression.contains(other)
21✔
1348
        }
43✔
1349
    }
1350

1351
    /// The [`Hash`] trait requires:
1352
    /// circuit_0 == circuit_1 => hash(circuit_0) == hash(circuit_1)
1353
    ///
1354
    /// By De-Morgan's law, this is equivalent to the more meaningful test:
1355
    /// hash(circuit_0) != hash(circuit_1) => circuit_0 != circuit_1
1356
    #[proptest]
512✔
1357
    fn unequal_hash_implies_unequal_constraint_circuit_monad(
1358
        #[strategy(arb())] circuit_0: ConstraintCircuitMonad<SingleRowIndicator>,
1✔
1359
        #[strategy(arb())] circuit_1: ConstraintCircuitMonad<SingleRowIndicator>,
1✔
1360
    ) {
1361
        if hash_circuit(&circuit_0) != hash_circuit(&circuit_1) {
1362
            prop_assert_ne!(circuit_0, circuit_1);
1363
        }
1364
    }
1365

1366
    /// The hash of a node may not depend on `ref_count`, `counter`, `id_counter_ref`, or
1367
    /// `all_nodes`, since `all_nodes` contains the digest of all nodes in the multi tree.
1368
    /// For more details, see [`HashSet`].
1369
    #[proptest]
512✔
1370
    fn multi_circuit_hash_is_unchanged_by_meta_data(
1371
        #[strategy(arb())] circuit: ConstraintCircuitMonad<DualRowIndicator>,
1✔
1372
        new_ref_count: usize,
1373
        new_id_counter: usize,
1374
    ) {
1375
        let original_digest = hash_circuit(&circuit);
1376

1377
        circuit.circuit.borrow_mut().ref_count = new_ref_count;
1378
        prop_assert_eq!(original_digest, hash_circuit(&circuit));
1379

1380
        circuit.builder.id_counter.replace(new_id_counter);
1381
        prop_assert_eq!(original_digest, hash_circuit(&circuit));
1382
    }
1383

1384
    fn hash_circuit<II: InputIndicator>(circuit: &ConstraintCircuitMonad<II>) -> u64 {
1,280✔
1385
        let mut hasher = DefaultHasher::new();
1,280✔
1386
        circuit.hash(&mut hasher);
1,280✔
1387
        hasher.finish()
1,280✔
1388
    }
1,280✔
1389

1390
    #[test]
1391
    fn printing_constraint_circuit_gives_expected_strings() {
1✔
1392
        let builder = ConstraintCircuitBuilder::new();
1✔
1393
        assert_eq!("1", builder.b_constant(1).to_string());
1✔
1394
        assert_eq!(
1✔
1395
            "main_row[5] ",
1✔
1396
            builder.input(SingleRowIndicator::Main(5)).to_string()
1✔
1397
        );
1✔
1398
        assert_eq!("6", builder.challenge(6_usize).to_string());
1✔
1399

1400
        let xfe_str = builder.x_constant([2, 3, 4]).to_string();
1✔
1401
        assert_eq!("(4·x² + 3·x + 2)", xfe_str);
1✔
1402
    }
1✔
1403

1404
    #[proptest]
256✔
1405
    fn constant_folding_can_deal_with_multiplication_by_one(
1406
        #[strategy(arb())] c: ConstraintCircuitMonad<DualRowIndicator>,
1✔
1407
    ) {
1408
        let one = || c.builder.one();
1,024✔
1409
        prop_assert_eq!(c.clone(), c.clone() * one());
1410
        prop_assert_eq!(c.clone(), one() * c.clone());
1411
        prop_assert_eq!(c.clone(), one() * c.clone() * one());
1412
    }
1413

1414
    #[proptest]
256✔
1415
    fn constant_folding_can_deal_with_adding_zero(
1416
        #[strategy(arb())] c: ConstraintCircuitMonad<DualRowIndicator>,
1✔
1417
    ) {
1418
        let zero = || c.builder.zero();
1,024✔
1419
        prop_assert_eq!(c.clone(), c.clone() + zero());
1420
        prop_assert_eq!(c.clone(), zero() + c.clone());
1421
        prop_assert_eq!(c.clone(), zero() + c.clone() + zero());
1422
    }
1423

1424
    #[proptest]
256✔
1425
    fn constant_folding_can_deal_with_subtracting_zero(
1426
        #[strategy(arb())] c: ConstraintCircuitMonad<DualRowIndicator>,
1✔
1427
    ) {
1428
        prop_assert_eq!(c.clone(), c.clone() - c.builder.zero());
1429
    }
1430

1431
    #[proptest]
512✔
1432
    fn constant_folding_can_deal_with_adding_effectively_zero_term(
1433
        #[strategy(arb())] c: ConstraintCircuitMonad<DualRowIndicator>,
1✔
1434
        modification_selectors: [bool; 4],
1435
    ) {
1436
        let zero = || c.builder.zero();
507✔
1437
        let mut redundant_circuit = c.clone();
1438
        if modification_selectors[0] {
1439
            redundant_circuit = redundant_circuit + (c.clone() * zero());
1440
        }
1441
        if modification_selectors[1] {
1442
            redundant_circuit = redundant_circuit + (zero() * c.clone());
1443
        }
1444
        if modification_selectors[2] {
1445
            redundant_circuit = (c.clone() * zero()) + redundant_circuit;
1446
        }
1447
        if modification_selectors[3] {
1448
            redundant_circuit = (zero() * c.clone()) + redundant_circuit;
1449
        }
1450
        prop_assert_eq!(c, redundant_circuit);
1451
    }
1452

1453
    #[proptest]
277✔
1454
    fn constant_folding_does_not_replace_0_minus_circuit_with_the_circuit(
1455
        #[strategy(arb())] circuit: ConstraintCircuitMonad<DualRowIndicator>,
1✔
1456
    ) {
1457
        if circuit.circuit.borrow().is_zero() {
1458
            return Err(TestCaseError::Reject("0 - 0 actually is 0".into()));
1459
        }
1460
        let zero_minus_circuit = circuit.builder.zero() - circuit.clone();
1461
        prop_assert_ne!(&circuit, &zero_minus_circuit);
1462
    }
1463

1464
    #[test]
1465
    fn pointer_redirection_obliviates_a_node_in_a_circuit() {
1✔
1466
        let builder = ConstraintCircuitBuilder::new();
1✔
1467
        let x = |i| builder.input(SingleRowIndicator::Main(i));
5✔
1468
        let constant = |c: u32| builder.b_constant(c);
2✔
1469
        let challenge = |i: usize| builder.challenge(i);
3✔
1470

1471
        let part = x(0) + x(1);
1✔
1472
        let substitute_me = x(0) * part.clone();
1✔
1473
        let root_0 = part.clone() + challenge(1) - constant(84);
1✔
1474
        let root_1 = substitute_me.clone() + challenge(0) - constant(42);
1✔
1475
        let root_2 = x(2) * substitute_me.clone() - challenge(1);
1✔
1476

1✔
1477
        assert!(!root_0.contains(&substitute_me));
1✔
1478
        assert!(root_1.contains(&substitute_me));
1✔
1479
        assert!(root_2.contains(&substitute_me));
1✔
1480

1481
        let new_variable = x(3);
1✔
1482
        builder.redirect_all_references_to_node(
1✔
1483
            substitute_me.circuit.borrow().id,
1✔
1484
            new_variable.clone(),
1✔
1485
        );
1✔
1486

1✔
1487
        assert!(!root_0.contains(&substitute_me));
1✔
1488
        assert!(!root_1.contains(&substitute_me));
1✔
1489
        assert!(!root_2.contains(&substitute_me));
1✔
1490

1491
        assert!(root_0.contains(&part));
1✔
1492
        assert!(root_1.contains(&new_variable));
1✔
1493
        assert!(root_2.contains(&new_variable));
1✔
1494
    }
1✔
1495

1496
    #[test]
1497
    fn simple_degree_lowering() {
1✔
1498
        let builder = ConstraintCircuitBuilder::new();
1✔
1499
        let x = || builder.input(SingleRowIndicator::Main(0));
8✔
1500
        let x_pow_3 = x() * x() * x();
1✔
1501
        let x_pow_5 = x() * x() * x() * x() * x();
1✔
1502
        let mut multicircuit = [x_pow_5, x_pow_3];
1✔
1503

1✔
1504
        let degree_lowering_info = DegreeLoweringInfo {
1✔
1505
            target_degree: 3,
1✔
1506
            num_main_cols: 1,
1✔
1507
            num_aux_cols: 0,
1✔
1508
        };
1✔
1509
        let (new_main_constraints, new_aux_constraints) =
1✔
1510
            ConstraintCircuitMonad::lower_to_degree(&mut multicircuit, degree_lowering_info);
1✔
1511

1✔
1512
        assert_eq!(1, new_main_constraints.len());
1✔
1513
        assert!(new_aux_constraints.is_empty());
1✔
1514
    }
1✔
1515

1516
    #[test]
1517
    fn somewhat_simple_degree_lowering() {
1✔
1518
        let builder = ConstraintCircuitBuilder::new();
1✔
1519
        let x = |i| builder.input(SingleRowIndicator::Main(i));
16✔
1520
        let y = |i| builder.input(SingleRowIndicator::Aux(i));
2✔
1521
        let b_con = |i: u64| builder.b_constant(i);
9✔
1522

1523
        let constraint_0 = x(0) * x(0) * (x(1) - x(2)) - x(0) * x(2) - b_con(42);
1✔
1524
        let constraint_1 = x(1) * (x(1) - b_con(5)) * x(2) * (x(2) - b_con(1));
1✔
1525
        let constraint_2 = y(0)
1✔
1526
            * (b_con(2) * x(0) + b_con(3) * x(1) + b_con(4) * x(2))
1✔
1527
            * (b_con(4) * x(0) + b_con(8) * x(1) + b_con(16) * x(2))
1✔
1528
            - y(1);
1✔
1529

1✔
1530
        let mut multicircuit = [constraint_0, constraint_1, constraint_2];
1✔
1531

1✔
1532
        let degree_lowering_info = DegreeLoweringInfo {
1✔
1533
            target_degree: 2,
1✔
1534
            num_main_cols: 3,
1✔
1535
            num_aux_cols: 2,
1✔
1536
        };
1✔
1537
        let (new_main_constraints, new_aux_constraints) =
1✔
1538
            ConstraintCircuitMonad::lower_to_degree(&mut multicircuit, degree_lowering_info);
1✔
1539

1✔
1540
        assert!(new_main_constraints.len() <= 3);
1✔
1541
        assert!(new_aux_constraints.len() <= 1);
1✔
1542
    }
1✔
1543

1544
    #[test]
1545
    fn less_simple_degree_lowering() {
1✔
1546
        let builder = ConstraintCircuitBuilder::new();
1✔
1547
        let x = |i| builder.input(SingleRowIndicator::Main(i));
11✔
1548

1549
        let constraint_0 = (x(0) * x(1) * x(2)) * (x(3) * x(4)) * x(5);
1✔
1550
        let constraint_1 = (x(6) * x(7)) * (x(3) * x(4)) * x(8);
1✔
1551

1✔
1552
        let mut multicircuit = [constraint_0, constraint_1];
1✔
1553

1✔
1554
        let degree_lowering_info = DegreeLoweringInfo {
1✔
1555
            target_degree: 3,
1✔
1556
            num_main_cols: 9,
1✔
1557
            num_aux_cols: 0,
1✔
1558
        };
1✔
1559
        let (new_main_constraints, new_aux_constraints) =
1✔
1560
            ConstraintCircuitMonad::lower_to_degree(&mut multicircuit, degree_lowering_info);
1✔
1561

1✔
1562
        assert!(new_main_constraints.len() <= 3);
1✔
1563
        assert!(new_aux_constraints.is_empty());
1✔
1564
    }
1✔
1565

1566
    fn circuit_with_multiple_options_for_degree_lowering_to_degree_2(
22✔
1567
    ) -> [ConstraintCircuitMonad<SingleRowIndicator>; 2] {
22✔
1568
        let builder = ConstraintCircuitBuilder::new();
22✔
1569
        let x = |i| builder.input(SingleRowIndicator::Main(i));
132✔
1570

1571
        let constraint_0 = x(0) * x(0) * x(0);
22✔
1572
        let constraint_1 = x(1) * x(1) * x(1);
22✔
1573

22✔
1574
        [constraint_0, constraint_1]
22✔
1575
    }
22✔
1576

1577
    #[test]
1578
    fn pick_node_to_substitute_is_deterministic() {
1✔
1579
        let multicircuit = circuit_with_multiple_options_for_degree_lowering_to_degree_2();
1✔
1580
        let first_node_id = ConstraintCircuitMonad::pick_node_to_substitute(&multicircuit, 2);
1✔
1581

1582
        for _ in 0..20 {
21✔
1583
            let node_id_again = ConstraintCircuitMonad::pick_node_to_substitute(&multicircuit, 2);
20✔
1584
            assert_eq!(first_node_id, node_id_again);
20✔
1585
        }
1586
    }
1✔
1587

1588
    #[test]
1589
    fn degree_lowering_specific_simple_circuit_is_deterministic() {
1✔
1590
        let degree_lowering_info = DegreeLoweringInfo {
1✔
1591
            target_degree: 2,
1✔
1592
            num_main_cols: 2,
1✔
1593
            num_aux_cols: 0,
1✔
1594
        };
1✔
1595

1✔
1596
        let mut original_multicircuit =
1✔
1597
            circuit_with_multiple_options_for_degree_lowering_to_degree_2();
1✔
1598
        let (new_main_constraints, _) = ConstraintCircuitMonad::lower_to_degree(
1✔
1599
            &mut original_multicircuit,
1✔
1600
            degree_lowering_info,
1✔
1601
        );
1✔
1602

1603
        for _ in 0..20 {
21✔
1604
            let mut new_multicircuit =
20✔
1605
                circuit_with_multiple_options_for_degree_lowering_to_degree_2();
20✔
1606
            let (new_main_constraints_again, _) = ConstraintCircuitMonad::lower_to_degree(
20✔
1607
                &mut new_multicircuit,
20✔
1608
                degree_lowering_info,
20✔
1609
            );
20✔
1610
            assert_eq!(new_main_constraints, new_main_constraints_again);
20✔
1611
            assert_eq!(original_multicircuit, new_multicircuit);
20✔
1612
        }
1613
    }
1✔
1614

1615
    #[test]
1616
    fn all_nodes_in_multicircuit_are_identified_correctly() {
1✔
1617
        let builder = ConstraintCircuitBuilder::new();
1✔
1618

1✔
1619
        let x = |i| builder.input(SingleRowIndicator::Main(i));
30✔
1620
        let b_con = |i: u64| builder.b_constant(i);
3✔
1621

1622
        let sub_tree_0 = x(0) * x(1) * (x(2) - b_con(1)) * x(3) * x(4);
1✔
1623
        let sub_tree_1 = x(0) * x(1) * (x(2) - b_con(1)) * x(3) * x(5);
1✔
1624
        let sub_tree_2 = x(10) * x(10) * x(2) * x(13);
1✔
1625
        let sub_tree_3 = x(10) * x(10) * x(2) * x(14);
1✔
1626

1✔
1627
        let circuit_0 = sub_tree_0.clone() + sub_tree_1.clone();
1✔
1628
        let circuit_1 = sub_tree_2.clone() + sub_tree_3.clone();
1✔
1629
        let circuit_2 = sub_tree_0 + sub_tree_2;
1✔
1630
        let circuit_3 = sub_tree_1 + sub_tree_3;
1✔
1631

1✔
1632
        let multicircuit = [circuit_0, circuit_1, circuit_2, circuit_3].map(|c| c.consume());
4✔
1633

1✔
1634
        let all_nodes = ConstraintCircuitMonad::all_nodes_in_multicircuit(&multicircuit);
1✔
1635
        let count_node = |node| all_nodes.iter().filter(|&n| n == &node).count();
532✔
1636

1637
        let x0 = x(0).consume();
1✔
1638
        assert_eq!(4, count_node(x0));
1✔
1639

1640
        let x2 = x(2).consume();
1✔
1641
        assert_eq!(8, count_node(x2));
1✔
1642

1643
        let x10 = x(10).consume();
1✔
1644
        assert_eq!(8, count_node(x10));
1✔
1645

1646
        let x4 = x(4).consume();
1✔
1647
        assert_eq!(2, count_node(x4));
1✔
1648

1649
        let x6 = x(6).consume();
1✔
1650
        assert_eq!(0, count_node(x6));
1✔
1651

1652
        let x0_x1 = (x(0) * x(1)).consume();
1✔
1653
        assert_eq!(4, count_node(x0_x1));
1✔
1654

1655
        let tree = (x(0) * x(1) * (x(2) - b_con(1))).consume();
1✔
1656
        assert_eq!(4, count_node(tree));
1✔
1657

1658
        let max_occurences = all_nodes
1✔
1659
            .iter()
1✔
1660
            .map(|node| all_nodes.iter().filter(|&n| n == node).count())
5,776✔
1661
            .max()
1✔
1662
            .unwrap();
1✔
1663
        assert_eq!(8, max_occurences);
1✔
1664

1665
        let most_frequent_nodes = all_nodes
1✔
1666
            .iter()
1✔
1667
            .filter(|&node| all_nodes.iter().filter(|&n| n == node).count() == max_occurences)
5,776✔
1668
            .unique()
1✔
1669
            .collect_vec();
1✔
1670
        assert_eq!(2, most_frequent_nodes.len());
1✔
1671
        assert!(most_frequent_nodes.contains(&&x(2).consume()));
1✔
1672
        assert!(most_frequent_nodes.contains(&&x(10).consume()));
1✔
1673
    }
1✔
1674

1675
    #[test]
1676
    fn equivalent_nodes_are_detected_when_present() {
1✔
1677
        let builder = ConstraintCircuitBuilder::new();
1✔
1678

1✔
1679
        let x = |i| builder.input(SingleRowIndicator::Main(i));
12✔
1680
        let ch = |i: usize| builder.challenge(i);
2✔
1681

1682
        let u0 = x(0) + x(1);
1✔
1683
        let u1 = x(2) + x(3);
1✔
1684
        let v = u0 * u1;
1✔
1685

1✔
1686
        let z0 = x(0) * x(2);
1✔
1687
        let z2 = x(1) * x(3);
1✔
1688

1✔
1689
        let z1 = x(1) * x(2) + x(0) * x(3);
1✔
1690
        let w = v - z0 - z2;
1✔
1691
        assert!(w.find_equivalent_nodes().is_empty());
1✔
1692

1693
        let o = ch(0) * z1 - ch(1) * w;
1✔
1694
        assert!(!o.find_equivalent_nodes().is_empty());
1✔
1695
    }
1✔
1696

1697
    #[derive(Debug, Copy, Clone, Eq, PartialEq, test_strategy::Arbitrary)]
351,040✔
1698
    enum CircuitOperationChoice {
1699
        Add(usize, usize),
1700
        Mul(usize, usize),
1701
    }
1702

1703
    #[derive(Debug, Copy, Clone, Eq, PartialEq, test_strategy::Arbitrary)]
12,570✔
1704
    enum CircuitInputType {
1705
        Main,
1706
        Aux,
1707
    }
1708

1709
    #[derive(Debug, Copy, Clone, Eq, PartialEq, test_strategy::Arbitrary)]
12,570✔
1710
    enum CircuitConstantType {
1711
        Base(#[strategy(arb())] BFieldElement),
2✔
1712
        Extension(#[strategy(arb())] XFieldElement),
2✔
1713
    }
1714

1715
    fn arbitrary_circuit_monad<II: InputIndicator>(
2✔
1716
        num_inputs: usize,
2✔
1717
        num_challenges: usize,
2✔
1718
        num_constants: usize,
2✔
1719
        num_operations: usize,
2✔
1720
        num_outputs: usize,
2✔
1721
    ) -> BoxedStrategy<Vec<ConstraintCircuitMonad<II>>> {
2✔
1722
        (
2✔
1723
            vec(CircuitInputType::arbitrary(), num_inputs),
2✔
1724
            vec(CircuitConstantType::arbitrary(), num_constants),
2✔
1725
            vec(CircuitOperationChoice::arbitrary(), num_operations),
2✔
1726
            vec(arb::<usize>(), num_outputs),
2✔
1727
        )
2✔
1728
            .prop_map(move |(inputs, constants, operations, outputs)| {
1,257✔
1729
                let builder = ConstraintCircuitBuilder::<II>::new();
1,257✔
1730

1,257✔
1731
                assert_eq!(0, *builder.id_counter.borrow());
1,257✔
1732
                assert!(
1,257✔
1733
                    builder.all_nodes.borrow().is_empty(),
1,257✔
1734
                    "fresh hashmap should be empty"
×
1735
                );
1736

1737
                let mut num_main_inputs = 0;
1,257✔
1738
                let mut num_aux_inputs = 0;
1,257✔
1739
                let mut all_nodes = vec![];
1,257✔
1740
                let mut output_nodes = vec![];
1,257✔
1741

1742
                for input in inputs {
13,827✔
1743
                    match input {
12,570✔
1744
                        CircuitInputType::Main => {
6,160✔
1745
                            let node = builder.input(II::main_table_input(num_main_inputs));
6,160✔
1746
                            num_main_inputs += 1;
6,160✔
1747
                            all_nodes.push(node);
6,160✔
1748
                        }
6,160✔
1749
                        CircuitInputType::Aux => {
6,410✔
1750
                            let node = builder.input(II::aux_table_input(num_aux_inputs));
6,410✔
1751
                            num_aux_inputs += 1;
6,410✔
1752
                            all_nodes.push(node);
6,410✔
1753
                        }
6,410✔
1754
                    }
1755
                }
1756

1757
                for i in 0..num_challenges {
12,570✔
1758
                    let node = builder.challenge(i);
12,570✔
1759
                    all_nodes.push(node);
12,570✔
1760
                }
12,570✔
1761

1762
                for constant in constants {
13,827✔
1763
                    let node = match constant {
12,570✔
1764
                        CircuitConstantType::Base(bfe) => builder.b_constant(bfe),
6,280✔
1765
                        CircuitConstantType::Extension(xfe) => builder.x_constant(xfe),
6,290✔
1766
                    };
1767
                    all_nodes.push(node);
12,570✔
1768
                }
1769

1770
                if all_nodes.is_empty() {
1,257✔
1771
                    return vec![];
×
1772
                }
1,257✔
1773

1774
                for operation in operations {
176,777✔
1775
                    let (lhs, rhs) = match operation {
175,520✔
1776
                        CircuitOperationChoice::Add(lhs, rhs) => (lhs, rhs),
87,457✔
1777
                        CircuitOperationChoice::Mul(lhs, rhs) => (lhs, rhs),
88,063✔
1778
                    };
1779

1780
                    let lhs_index = lhs % all_nodes.len();
175,520✔
1781
                    let rhs_index = rhs % all_nodes.len();
175,520✔
1782

175,520✔
1783
                    let lhs_node = all_nodes[lhs_index].clone();
175,520✔
1784
                    let rhs_node = all_nodes[rhs_index].clone();
175,520✔
1785

1786
                    let node = match operation {
175,520✔
1787
                        CircuitOperationChoice::Add(_, _) => lhs_node + rhs_node,
87,457✔
1788
                        CircuitOperationChoice::Mul(_, _) => lhs_node * rhs_node,
88,063✔
1789
                    };
1790
                    all_nodes.push(node);
175,520✔
1791
                }
1792

1793
                for output in outputs {
13,827✔
1794
                    let index = output % all_nodes.len();
12,570✔
1795
                    output_nodes.push(all_nodes[index].clone());
12,570✔
1796
                }
12,570✔
1797

1798
                output_nodes
1,257✔
1799
            })
1,257✔
1800
            .boxed()
2✔
1801
    }
2✔
1802

1803
    #[proptest]
256✔
1804
    fn node_type_counts_add_up(
1805
        #[strategy(arbitrary_circuit_monad(10, 10, 10, 60, 10))] multicircuit_monad: Vec<
1✔
1806
            ConstraintCircuitMonad<SingleRowIndicator>,
1807
        >,
1808
    ) {
1809
        prop_assert_eq!(
1810
            ConstraintCircuitMonad::num_nodes(&multicircuit_monad),
1811
            ConstraintCircuitMonad::num_main_inputs(&multicircuit_monad)
1812
                + ConstraintCircuitMonad::num_aux_inputs(&multicircuit_monad)
1813
                + ConstraintCircuitMonad::num_challenges(&multicircuit_monad)
1814
                + ConstraintCircuitMonad::num_bfield_constants(&multicircuit_monad)
1815
                + ConstraintCircuitMonad::num_xfield_constants(&multicircuit_monad)
1816
                + ConstraintCircuitMonad::num_binops(&multicircuit_monad)
1817
        );
1818

1819
        prop_assert_eq!(10, ConstraintCircuitMonad::num_inputs(&multicircuit_monad));
1820
    }
1821

1822
    /// Test the completeness and soundness of the `apply_substitution` function,
1823
    /// which substitutes a single node.
1824
    ///
1825
    /// In this context, completeness means: "given a satisfying assignment to the
1826
    /// circuit before degree lowering, one can derive a satisfying assignment to
1827
    /// the circuit after degree lowering." Soundness means the converse.
1828
    ///
1829
    /// We test these features using random input vectors. Naturally, the output
1830
    /// is not the zero vector (with high probability) and so the given input is
1831
    /// *not* a satisfying assignment (with high probability). However, the
1832
    /// circuit can be extended by way of thought experiment into one that subtracts
1833
    /// a fixed constant from the original output. For the right choice of subtrahend,
1834
    /// the random input now *is* a satisfying assignment to the circuit.
1835
    ///
1836
    /// Specifically, let `input` denote the original (before degree lowering) input,
1837
    /// and `C` the circuit. Then `input` is a satisfying input for the new circuit
1838
    /// `C'(X) = C(X) - C(input)`
1839
    ///
1840
    /// After applying a substitution to obtain circuit `C || k` from `C`, where
1841
    /// `k = Z - some_expr(X)` and `Z` is the introduced variable, the length
1842
    /// of the input and output increases by 1. Moreover, if `input` is a satisfying
1843
    /// input to `C'` then `input || some_expr(input)` is* a satisfying input to
1844
    /// `C' || k`.
1845
    ///
1846
    /// (*: If the transformation is complete.)
1847
    ///
1848
    /// To establish the converse, we want to start from a satisfying input to
1849
    /// `C" || k` and reduce it to a satisfying input to `C"`. The requirement,
1850
    /// implied by "satisfying input", that `k(X || Z) == 0` implies `Z == some_expr(X)`.
1851
    /// Therefore, in order to sample a random satisfying input to `C" || k`, it
1852
    /// suffices to select `input` at random, define `C"(X) = C(X) - C(input)`,
1853
    /// and evaluate `some_expr(input)`. Then `input || some_expr(input)` is a
1854
    /// random satisfying input to `C" || k`. It follows** that `input` is a
1855
    /// satisfying input to `C"`.
1856
    ///
1857
    /// (**: If the transformation is sound.)
1858
    ///
1859
    /// This description makes use of the following commutative diagram.
1860
    ///
1861
    /// ```text
1862
    ///          C ───── degree-lowering ────> C || k
1863
    ///          │                               │
1864
    /// subtract │                      subtract │
1865
    ///    fixed │                         fixed │
1866
    ///   output │                        output │
1867
    ///          │                               │
1868
    ///          v                               v
1869
    ///          C* ─── degree-lowering ────> C* || k
1870
    /// ```
1871
    ///
1872
    /// The point of this elaboration is that in this particular case, testing completeness
1873
    /// and soundness require the same code path. If `input` and `input || some_expr(input)`
1874
    /// work for circuits before and after degree lowering, this fact establishes
1875
    /// both completeness and soundness simultaneously.
1876
    //
1877
    // Shrinking on this test is disabled because we noticed some weird ass behavior.
1878
    // In short, shrinking does not play ball with the arbitrary circuit generator;
1879
    // it seems to make the generated circuits *more* complex, not less so.
1880
    #[proptest(cases = 1000, max_shrink_iters = 0)]
3,001✔
1881
    fn node_substitution_is_complete_and_sound(
1882
        #[strategy(arbitrary_circuit_monad(10, 10, 10, 160, 10))] mut multicircuit_monad: Vec<
1✔
1883
            ConstraintCircuitMonad<SingleRowIndicator>,
1884
        >,
1885
        #[strategy(vec(arb(), ConstraintCircuitMonad::num_main_inputs(&#multicircuit_monad)))]
1886
        #[filter(!#main_input.is_empty())]
1,001✔
1887
        main_input: Vec<BFieldElement>,
1,001✔
1888
        #[strategy(vec(arb(), ConstraintCircuitMonad::num_aux_inputs(&#multicircuit_monad)))]
1889
        #[filter(!#aux_input.is_empty())]
1,001✔
1890
        aux_input: Vec<XFieldElement>,
1,001✔
1891
        #[strategy(vec(arb(), ConstraintCircuitMonad::num_challenges(&#multicircuit_monad)))]
1892
        challenges: Vec<XFieldElement>,
1,001✔
1893
        #[strategy(arb())] substitution_node_index: usize,
1✔
1894
    ) {
1895
        let mut main_input = Array2::from_shape_vec((1, main_input.len()), main_input).unwrap();
1896
        let mut aux_input = Array2::from_shape_vec((1, aux_input.len()), aux_input).unwrap();
1897

1898
        let output_before_lowering = multicircuit_monad
1899
            .iter()
1900
            .map(|m| m.circuit.borrow())
10,000✔
1901
            .map(|c| c.evaluate(main_input.view(), aux_input.view(), &challenges))
10,000✔
1902
            .collect_vec();
1903

1904
        // apply one step of degree-lowering
1905
        let num_nodes = ConstraintCircuitMonad::num_nodes(&multicircuit_monad);
1906
        let &substitution_node_id = multicircuit_monad[0]
1907
            .builder
1908
            .all_nodes
1909
            .borrow()
1910
            .iter()
1911
            .cycle()
1912
            .skip(substitution_node_index % num_nodes)
1913
            .take(num_nodes)
1914
            .find_map(|(id, monad)| monad.circuit.borrow().is_zero().not().then_some(id))
1,000✔
1915
            .expect("no suitable nodes to substitute");
1916

1917
        let degree_lowering_info = DegreeLoweringInfo {
1918
            target_degree: 2,
1919
            num_main_cols: main_input.len(),
1920
            num_aux_cols: aux_input.len(),
1921
        };
1922
        let substitution_constraint = ConstraintCircuitMonad::apply_substitution(
1923
            &mut multicircuit_monad,
1924
            degree_lowering_info,
1925
            substitution_node_id,
1926
            EvolvingMainConstraintsNumber(0),
1927
            EvolvingAuxConstraintsNumber(0),
1928
        );
1929

1930
        // extract substituted constraint
1931
        let CircuitExpression::BinOp(BinOp::Add, variable, neg_expression) =
1932
            &substitution_constraint.circuit.borrow().expression
1933
        else {
1934
            panic!();
1935
        };
1936
        let extra_input =
1937
            match &neg_expression.borrow().expression {
1938
                CircuitExpression::BinOp(BinOp::Mul, _neg_one, circuit) => circuit
1939
                    .borrow()
1940
                    .evaluate(main_input.view(), aux_input.view(), &challenges),
1941
                CircuitExpression::BConst(c) => -c.lift(),
1942
                CircuitExpression::XConst(c) => -*c,
1943
                _ => panic!(),
1944
            };
1945
        if variable.borrow().evaluates_to_base_element() {
1946
            let extra_input = extra_input.unlift().unwrap();
1947
            let extra_input = Array2::from_shape_vec([1, 1], vec![extra_input]).unwrap();
1948
            main_input.append(Axis(1), extra_input.view()).unwrap();
1949
        } else {
1950
            let extra_input = Array2::from_shape_vec([1, 1], vec![extra_input]).unwrap();
1951
            aux_input.append(Axis(1), extra_input.view()).unwrap();
1952
        }
1953

1954
        // evaluate again
1955
        let output_after_lowering = multicircuit_monad
1956
            .iter()
1957
            .map(|m| m.circuit.borrow())
10,000✔
1958
            .map(|c| c.evaluate(main_input.view(), aux_input.view(), &challenges))
10,000✔
1959
            .collect_vec();
1960

1961
        prop_assert_eq!(output_before_lowering, output_after_lowering);
1962

1963
        prop_assert!(substitution_constraint
1964
            .circuit
1965
            .borrow()
1966
            .evaluate(main_input.view(), aux_input.view(), &challenges)
1967
            .is_zero());
1968
    }
1969
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc