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

TritonVM / triton-vm / 10975676698

21 Sep 2024 09:09PM UTC coverage: 98.398% (-0.06%) from 98.462%
10975676698

Pull #325

github

web-flow
Merge 66b8225a3 into 7d612aa34
Pull Request #325: Thv/degree lowering hardening

371 of 385 new or added lines in 3 files covered. (96.36%)

11 existing lines in 4 files now uncovered.

24018 of 24409 relevant lines covered (98.4%)

6235954.49 hits per line

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

97.32
/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,348,039,297✔
86
            BinOp::Mul => lhs * rhs,
1,787,816,862✔
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)]
99,755✔
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,267✔
157
        matches!(self, Self::Main(_))
40,267✔
158
    }
40,267✔
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,362✔
171
        Self::Main(index)
6,362✔
172
    }
6,362✔
173

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

178
    fn evaluate(
81,606,360✔
179
        &self,
81,606,360✔
180
        main_table: ArrayView2<BFieldElement>,
81,606,360✔
181
        aux_table: ArrayView2<XFieldElement>,
81,606,360✔
182
    ) -> XFieldElement {
81,606,360✔
183
        match self {
81,606,360✔
184
            Self::Main(i) => main_table[[0, *i]].lift(),
81,571,159✔
185
            Self::Aux(i) => aux_table[[0, *i]],
35,201✔
186
        }
187
    }
81,606,360✔
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)]
306,680✔
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 {
227
    fn is_main_table_column(&self) -> bool {
8,988✔
228
        matches!(self, Self::CurrentMain(_) | Self::NextMain(_))
8,988✔
229
    }
8,988✔
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

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

252
    fn evaluate(
1,666,580,434✔
253
        &self,
1,666,580,434✔
254
        main_table: ArrayView2<BFieldElement>,
1,666,580,434✔
255
        aux_table: ArrayView2<XFieldElement>,
1,666,580,434✔
256
    ) -> XFieldElement {
1,666,580,434✔
257
        match self {
1,666,580,434✔
258
            Self::CurrentMain(i) => main_table[[0, *i]].lift(),
1,259,334,417✔
259
            Self::CurrentAux(i) => aux_table[[0, *i]],
37,615,783✔
260
            Self::NextMain(i) => main_table[[1, *i]].lift(),
331,907,326✔
261
            Self::NextAux(i) => aux_table[[1, *i]],
37,722,908✔
262
        }
263
    }
1,666,580,434✔
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,912,504✔
299
        match self {
1,486,912,504✔
300
            Self::BConst(bfe) => {
102,129,402✔
301
                "bfe".hash(state);
102,129,402✔
302
                bfe.hash(state);
102,129,402✔
303
            }
102,129,402✔
304
            Self::XConst(xfe) => {
2,561✔
305
                "xfe".hash(state);
2,561✔
306
                xfe.hash(state);
2,561✔
307
            }
2,561✔
308
            Self::Input(index) => {
401,364,297✔
309
                "input".hash(state);
401,364,297✔
310
                index.hash(state);
401,364,297✔
311
            }
401,364,297✔
312
            Self::Challenge(table_challenge_id) => {
241,070,988✔
313
                "challenge".hash(state);
241,070,988✔
314
                table_challenge_id.hash(state);
241,070,988✔
315
            }
241,070,988✔
316
            Self::BinOp(binop, lhs, rhs) => {
742,345,256✔
317
                "binop".hash(state);
742,345,256✔
318
                binop.hash(state);
742,345,256✔
319
                lhs.borrow().hash(state);
742,345,256✔
320
                rhs.borrow().hash(state);
742,345,256✔
321
            }
742,345,256✔
322
        }
323
    }
1,486,912,504✔
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,389,228✔
330
            (Self::XConst(x), Self::XConst(x_o)) => x == x_o,
163,013✔
331
            (Self::Input(i), Self::Input(i_o)) => i == i_o,
95,568,268✔
332
            (Self::Challenge(c), Self::Challenge(c_o)) => c == c_o,
66,241,810✔
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,912,504✔
343
        self.expression.hash(state)
1,486,912,504✔
344
    }
1,486,912,504✔
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,049,788✔
392
        Self {
8,049,788✔
393
            id,
8,049,788✔
394
            ref_count: 0,
8,049,788✔
395
            expression,
8,049,788✔
396
        }
8,049,788✔
397
    }
8,049,788✔
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,117,625✔
493
        match self.expression {
1,175,117,625✔
494
            CircuitExpression::BConst(bfe) => bfe.is_zero(),
88,667,969✔
495
            CircuitExpression::XConst(xfe) => xfe.is_zero(),
41,618✔
496
            _ => false,
1,086,408,038✔
497
        }
498
    }
1,175,117,625✔
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,721,348✔
503
        match self.expression {
3,721,348✔
504
            CircuitExpression::BConst(bfe) => bfe.is_one(),
880,730✔
505
            CircuitExpression::XConst(xfe) => xfe.is_one(),
20,582✔
506
            _ => false,
2,820,036✔
507
        }
508
    }
3,721,348✔
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,372✔
523
        match &self.expression {
34,372✔
524
            CircuitExpression::BConst(_) => true,
4,356✔
525
            CircuitExpression::XConst(_) => false,
245✔
526
            CircuitExpression::Input(indicator) => indicator.is_main_table_column(),
17,147✔
527
            CircuitExpression::Challenge(_) => false,
496✔
528
            CircuitExpression::BinOp(_, lhs, rhs) => {
12,128✔
529
                lhs.borrow().evaluates_to_base_element() && rhs.borrow().evaluates_to_base_element()
12,128✔
530
            }
531
        }
532
    }
34,372✔
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(),
995,266,048✔
542
            CircuitExpression::XConst(xfe) => *xfe,
21,093✔
543
            CircuitExpression::Input(input) => input.evaluate(main_table, aux_table),
1,748,186,794✔
544
            CircuitExpression::Challenge(challenge_id) => challenges[*challenge_id],
416,539,042✔
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,230,526✔
603
    binop: BinOp,
3,230,526✔
604
    lhs: ConstraintCircuitMonad<II>,
3,230,526✔
605
    rhs: ConstraintCircuitMonad<II>,
3,230,526✔
606
) -> ConstraintCircuitMonad<II> {
3,230,526✔
607
    assert!(lhs.builder.is_same_as(&rhs.builder));
3,230,526✔
608

609
    match (binop, &lhs, &rhs) {
3,230,526✔
610
        (BinOp::Add, _, zero) if zero.circuit.borrow().is_zero() => return lhs,
1,322,789✔
611
        (BinOp::Add, zero, _) if zero.circuit.borrow().is_zero() => return rhs,
1,173,505✔
612
        (BinOp::Mul, _, one) if one.circuit.borrow().is_one() => return lhs,
1,907,737✔
613
        (BinOp::Mul, one, _) if one.circuit.borrow().is_one() => return rhs,
1,813,611✔
614
        (BinOp::Mul, _, zero) if zero.circuit.borrow().is_zero() => return rhs,
1,784,819✔
615
        (BinOp::Mul, zero, _) if zero.circuit.borrow().is_zero() => return lhs,
1,644,795✔
616
        _ => (),
2,777,391✔
617
    };
2,777,391✔
618

2,777,391✔
619
    match (
2,777,391✔
620
        &lhs.circuit.borrow().expression,
2,777,391✔
621
        &rhs.circuit.borrow().expression,
2,777,391✔
622
    ) {
623
        (&CircuitExpression::BConst(l), &CircuitExpression::BConst(r)) => {
103,182✔
624
            return lhs.builder.b_constant(binop.operation(l, r))
103,182✔
625
        }
626
        (&CircuitExpression::BConst(l), &CircuitExpression::XConst(r)) => {
3,515✔
627
            return lhs.builder.x_constant(binop.operation(l, r))
3,515✔
628
        }
629
        (&CircuitExpression::XConst(l), &CircuitExpression::BConst(r)) => {
1,773✔
630
            return lhs.builder.x_constant(binop.operation(l, r))
1,773✔
631
        }
632
        (&CircuitExpression::XConst(l), &CircuitExpression::XConst(r)) => {
2,189✔
633
            return lhs.builder.x_constant(binop.operation(l, r))
2,189✔
634
        }
635
        _ => (),
2,666,732✔
636
    };
2,666,732✔
637

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

2,665,253✔
645
    let new_node = binop_new_node(binop, &lhs, &rhs);
2,665,253✔
646
    if let Some(node) = all_nodes.values().find(|&n| n == &new_node) {
2,147,483,647✔
647
        return node.to_owned();
1,429,550✔
648
    }
1,235,703✔
649

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

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

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

672
    fn add(self, rhs: Self) -> Self::Output {
782,930✔
673
        binop(BinOp::Add, self, rhs)
782,930✔
674
    }
782,930✔
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,859✔
681
        binop(BinOp::Add, self, -rhs)
539,859✔
682
    }
539,859✔
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,367,878✔
689
        binop(BinOp::Mul, self, rhs)
1,367,878✔
690
    }
1,367,878✔
691
}
692

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

696
    fn neg(self) -> Self::Output {
539,859✔
697
        binop(BinOp::Mul, self.builder.minus_one(), self)
539,859✔
698
    }
539,859✔
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
type EvolvingMainConstraintsNumber = usize;
711
type EvolvingAuxConstraintsNumber = usize;
712

713
impl<II: InputIndicator> ConstraintCircuitMonad<II> {
714
    /// Unwrap a ConstraintCircuitMonad to reveal its inner ConstraintCircuit
715
    pub fn consume(&self) -> ConstraintCircuit<II> {
166,498✔
716
        self.circuit.borrow().to_owned()
166,498✔
717
    }
166,498✔
718

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

753
        let mut main_constraints = vec![];
160✔
754
        let mut aux_constraints = vec![];
160✔
755

160✔
756
        if multicircuit.is_empty() {
160✔
757
            return (main_constraints, aux_constraints);
28✔
758
        }
132✔
759

760
        while Self::multicircuit_degree(multicircuit) > target_degree {
1,520✔
761
            let chosen_node_id = Self::pick_node_to_substitute(multicircuit, target_degree);
1,388✔
762

1,388✔
763
            let new_constraint = Self::apply_substitution(
1,388✔
764
                multicircuit,
1,388✔
765
                info,
1,388✔
766
                chosen_node_id,
1,388✔
767
                main_constraints.len(),
1,388✔
768
                aux_constraints.len(),
1,388✔
769
            );
1,388✔
770

1,388✔
771
            if new_constraint.circuit.borrow().evaluates_to_base_element() {
1,388✔
772
                main_constraints.push(new_constraint)
1,207✔
773
            } else {
774
                aux_constraints.push(new_constraint)
181✔
775
            }
776
        }
777

778
        (main_constraints, aux_constraints)
132✔
779
    }
160✔
780

781
    /// Apply a substitution:
782
    ///  - create a new variable to replaces the chosen node;
783
    ///  - make all nodes that point to the chosen node point to the new
784
    ///    variable instead;
785
    ///  - return the new constraint that makes it sound: new variable minus
786
    ///    chosen node's expression.
787
    fn apply_substitution(
2,388✔
788
        multicircuit: &mut [Self],
2,388✔
789
        info: DegreeLoweringInfo,
2,388✔
790
        chosen_node_id: usize,
2,388✔
791
        new_main_constraints_count: EvolvingMainConstraintsNumber,
2,388✔
792
        new_aux_constraints_count: EvolvingAuxConstraintsNumber,
2,388✔
793
    ) -> ConstraintCircuitMonad<II> {
2,388✔
794
        let builder = multicircuit[0].builder.clone();
2,388✔
795

2,388✔
796
        // Create a new variable.
2,388✔
797
        let chosen_node = builder.all_nodes.borrow()[&chosen_node_id].clone();
2,388✔
798
        let chosen_node_is_main_col = chosen_node.circuit.borrow().evaluates_to_base_element();
2,388✔
799
        let new_input_indicator = if chosen_node_is_main_col {
2,388✔
800
            let new_main_col_idx = info.num_main_cols + new_main_constraints_count;
1,282✔
801
            II::main_table_input(new_main_col_idx)
1,282✔
802
        } else {
803
            let new_aux_col_idx = info.num_aux_cols + new_aux_constraints_count;
1,106✔
804
            II::aux_table_input(new_aux_col_idx)
1,106✔
805
        };
806
        let new_variable = builder.input(new_input_indicator);
2,388✔
807

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

811
        // Treat roots of the multicircuit explicitly.
812
        for circuit in multicircuit.iter_mut() {
133,650✔
813
            if circuit.circuit.borrow().id == chosen_node_id {
133,650✔
814
                circuit.circuit = new_variable.circuit.clone();
54✔
815
            }
133,596✔
816
        }
817

818
        // return substitution equation
819
        new_variable - chosen_node
2,388✔
820
    }
2,388✔
821

822
    /// Heuristically pick a node from the given multicircuit that is to be substituted with a new
823
    /// variable. The ID of the chosen node is returned.
824
    fn pick_node_to_substitute(
1,409✔
825
        multicircuit: &[ConstraintCircuitMonad<II>],
1,409✔
826
        target_degree: isize,
1,409✔
827
    ) -> usize {
1,409✔
828
        assert!(!multicircuit.is_empty());
1,409✔
829
        let multicircuit = multicircuit
1,409✔
830
            .iter()
1,409✔
831
            .map(|c| c.clone().consume())
123,692✔
832
            .collect_vec();
1,409✔
833

1,409✔
834
        // Computing all node degree is slow; this cache de-duplicates work.
1,409✔
835
        let node_degrees = Self::all_nodes_in_multicircuit(&multicircuit)
1,409✔
836
            .into_iter()
1,409✔
837
            .map(|node| (node.id, node.degree()))
35,915,396✔
838
            .collect::<HashMap<_, _>>();
1,409✔
839

1,409✔
840
        // Only nodes with degree > target_degree need changing.
1,409✔
841
        let high_degree_nodes = Self::all_nodes_in_multicircuit(&multicircuit)
1,409✔
842
            .into_iter()
1,409✔
843
            .filter(|node| node_degrees[&node.id] > target_degree)
35,915,396✔
844
            .unique()
1,409✔
845
            .collect_vec();
1,409✔
846

1,409✔
847
        // Collect all candidates for substitution, i.e., descendents of high_degree_nodes
1,409✔
848
        // with degree <= target_degree.
1,409✔
849
        // Substituting a node of degree 1 is both pointless and can lead to infinite iteration.
1,409✔
850
        let low_degree_nodes = Self::all_nodes_in_multicircuit(&high_degree_nodes)
1,409✔
851
            .into_iter()
1,409✔
852
            .filter(|node| 1 < node_degrees[&node.id] && node_degrees[&node.id] <= target_degree)
394,792,928✔
853
            .map(|node| node.id)
37,937,892✔
854
            .collect_vec();
1,409✔
855

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

859
        // Of the remaining nodes, keep the ones occurring the most often.
860
        let mut nodes_and_occurrences = HashMap::new();
1,409✔
861
        for node in low_degree_nodes {
37,939,301✔
862
            *nodes_and_occurrences.entry(node).or_insert(0) += 1;
37,937,892✔
863
        }
37,937,892✔
864
        let max_occurrences = nodes_and_occurrences.iter().map(|(_, &c)| c).max().unwrap();
1,424,536✔
865
        nodes_and_occurrences.retain(|_, &mut count| count == max_occurrences);
1,424,536✔
866
        let mut candidate_node_ids = nodes_and_occurrences.keys().copied().collect_vec();
1,409✔
867

1,409✔
868
        // If there are still multiple nodes, pick the one with the highest degree.
1,409✔
869
        let max_degree = candidate_node_ids
1,409✔
870
            .iter()
1,409✔
871
            .map(|node_id| node_degrees[node_id])
18,739✔
872
            .max()
1,409✔
873
            .unwrap();
1,409✔
874
        candidate_node_ids.retain(|node_id| node_degrees[node_id] == max_degree);
18,739✔
875

1,409✔
876
        candidate_node_ids.sort_unstable();
1,409✔
877

1,409✔
878
        // If there are still multiple nodes, pick any one – but deterministically so.
1,409✔
879
        candidate_node_ids.into_iter().min().unwrap()
1,409✔
880
    }
1,409✔
881

882
    /// Returns all nodes used in the multicircuit.
883
    /// This is distinct from `ConstraintCircuitBuilder::all_nodes` because it
884
    /// 1. only considers nodes used in the given multicircuit, not all nodes in the builder,
885
    /// 2. returns the nodes as [`ConstraintCircuit`]s, not as [`ConstraintCircuitMonad`]s, and
886
    /// 3. keeps duplicates, allowing to count how often a node occurs.
887
    pub fn all_nodes_in_multicircuit(
4,228✔
888
        multicircuit: &[ConstraintCircuit<II>],
4,228✔
889
    ) -> Vec<ConstraintCircuit<II>> {
4,228✔
890
        multicircuit
4,228✔
891
            .iter()
4,228✔
892
            .flat_map(Self::all_nodes_in_circuit)
4,228✔
893
            .collect()
4,228✔
894
    }
4,228✔
895

896
    /// Internal helper function to recursively find all nodes in a circuit.
897
    fn all_nodes_in_circuit(circuit: &ConstraintCircuit<II>) -> Vec<ConstraintCircuit<II>> {
466,751,572✔
898
        let mut all_nodes = vec![];
466,751,572✔
899
        if let CircuitExpression::BinOp(_, lhs, rhs) = circuit.expression.clone() {
466,751,572✔
900
            let lhs_nodes = Self::all_nodes_in_circuit(&lhs.borrow());
232,882,941✔
901
            let rhs_nodes = Self::all_nodes_in_circuit(&rhs.borrow());
232,882,941✔
902
            all_nodes.extend(lhs_nodes);
232,882,941✔
903
            all_nodes.extend(rhs_nodes);
232,882,941✔
904
        };
233,868,631✔
905
        all_nodes.push(circuit.to_owned());
466,751,572✔
906
        all_nodes
466,751,572✔
907
    }
466,751,572✔
908

909
    /// Counts the number of nodes in this multicircuit. Only counts nodes that
910
    /// are used; not nodes that have been forgotten.
911
    pub fn num_visible_nodes(constraints: &[Self]) -> usize {
12✔
912
        constraints
12✔
913
            .iter()
12✔
914
            .flat_map(|ccm| Self::all_nodes_in_circuit(&ccm.circuit.borrow()))
1,384✔
915
            .unique()
12✔
916
            .count()
12✔
917
    }
12✔
918

919
    /// Returns the maximum degree of all circuits in the multicircuit.
920
    pub fn multicircuit_degree(multicircuit: &[ConstraintCircuitMonad<II>]) -> isize {
1,748✔
921
        multicircuit
1,748✔
922
            .iter()
1,748✔
923
            .map(|circuit| circuit.circuit.borrow().degree())
127,633✔
924
            .max()
1,748✔
925
            .unwrap_or(-1)
1,748✔
926
    }
1,748✔
927
}
928

929
/// Helper struct to construct new leaf nodes (*i.e.*, input or challenge or constant)
930
/// in the circuit multitree. Ensures that newly created nodes, even non-leaf nodes
931
/// created through joining two other nodes using an arithmetic operation, get a
932
/// unique ID.
933
#[derive(Debug, Clone, Eq, PartialEq)]
934
pub struct ConstraintCircuitBuilder<II: InputIndicator> {
935
    id_counter: Rc<RefCell<usize>>,
936
    all_nodes: Rc<RefCell<HashMap<usize, ConstraintCircuitMonad<II>>>>,
937
}
938

939
impl<II: InputIndicator> Default for ConstraintCircuitBuilder<II> {
940
    fn default() -> Self {
×
941
        Self::new()
×
942
    }
×
943
}
944

945
impl<II: InputIndicator> ConstraintCircuitBuilder<II> {
946
    pub fn new() -> Self {
8,697✔
947
        Self {
8,697✔
948
            id_counter: Rc::new(RefCell::new(0)),
8,697✔
949
            all_nodes: Rc::new(RefCell::new(HashMap::new())),
8,697✔
950
        }
8,697✔
951
    }
8,697✔
952

953
    /// Check whether two builders are the same.
954
    ///
955
    /// Notably, this is distinct from checking equality: two builders are equal if they are in the
956
    /// same state. Two builders are the same if they are the same instance.
957
    pub fn is_same_as(&self, other: &Self) -> bool {
3,230,526✔
958
        Rc::ptr_eq(&self.id_counter, &other.id_counter)
3,230,526✔
959
            && Rc::ptr_eq(&self.all_nodes, &other.all_nodes)
3,230,526✔
960
    }
3,230,526✔
961

962
    fn new_monad(&self, circuit: ConstraintCircuit<II>) -> ConstraintCircuitMonad<II> {
8,049,788✔
963
        let circuit = Rc::new(RefCell::new(circuit));
8,049,788✔
964
        ConstraintCircuitMonad {
8,049,788✔
965
            circuit,
8,049,788✔
966
            builder: self.clone(),
8,049,788✔
967
        }
8,049,788✔
968
    }
8,049,788✔
969

970
    /// The unique monad representing the constant value 0.
971
    pub fn zero(&self) -> ConstraintCircuitMonad<II> {
9,847✔
972
        self.b_constant(0)
9,847✔
973
    }
9,847✔
974

975
    /// The unique monad representing the constant value 1.
976
    pub fn one(&self) -> ConstraintCircuitMonad<II> {
8,969✔
977
        self.b_constant(1)
8,969✔
978
    }
8,969✔
979

980
    /// The unique monad representing the constant value -1.
981
    pub fn minus_one(&self) -> ConstraintCircuitMonad<II> {
539,859✔
982
        self.b_constant(-1)
539,859✔
983
    }
539,859✔
984

985
    /// Leaf node with constant over the [base field][BFieldElement].
986
    pub fn b_constant<B>(&self, bfe: B) -> ConstraintCircuitMonad<II>
1,121,049✔
987
    where
1,121,049✔
988
        B: Into<BFieldElement>,
1,121,049✔
989
    {
1,121,049✔
990
        self.make_leaf(CircuitExpression::BConst(bfe.into()))
1,121,049✔
991
    }
1,121,049✔
992

993
    /// Leaf node with constant over the [extension field][XFieldElement].
994
    pub fn x_constant<X>(&self, xfe: X) -> ConstraintCircuitMonad<II>
24,762✔
995
    where
24,762✔
996
        X: Into<XFieldElement>,
24,762✔
997
    {
24,762✔
998
        self.make_leaf(CircuitExpression::XConst(xfe.into()))
24,762✔
999
    }
24,762✔
1000

1001
    /// Create deterministic input leaf node.
1002
    pub fn input(&self, input: II) -> ConstraintCircuitMonad<II> {
1,101,842✔
1003
        self.make_leaf(CircuitExpression::Input(input))
1,101,842✔
1004
    }
1,101,842✔
1005

1006
    /// Create challenge leaf node.
1007
    pub fn challenge<C>(&self, challenge: C) -> ConstraintCircuitMonad<II>
470,150✔
1008
    where
470,150✔
1009
        C: Into<usize>,
470,150✔
1010
    {
470,150✔
1011
        self.make_leaf(CircuitExpression::Challenge(challenge.into()))
470,150✔
1012
    }
470,150✔
1013

1014
    fn make_leaf(&self, mut expression: CircuitExpression<II>) -> ConstraintCircuitMonad<II> {
2,717,803✔
1015
        assert!(
2,717,803✔
1016
            !matches!(expression, CircuitExpression::BinOp(_, _, _)),
2,717,803✔
NEW
1017
            "`make_leaf` is intended for anything but `BinOp`s"
×
1018
        );
1019

1020
        // Don't generate an X field leaf if it can be expressed as a B field leaf
1021
        if let CircuitExpression::XConst(xfe) = expression {
2,717,803✔
1022
            if let Some(bfe) = xfe.unlift() {
24,762✔
1023
                expression = CircuitExpression::BConst(bfe);
3,256✔
1024
            }
21,506✔
1025
        }
2,693,041✔
1026

1027
        let id = self.id_counter.borrow().to_owned();
2,717,803✔
1028
        let circuit = ConstraintCircuit::new(id, expression);
2,717,803✔
1029
        let new_node = self.new_monad(circuit);
2,717,803✔
1030

1031
        if let Some(same_node) = self.all_nodes.borrow().values().find(|&n| n == &new_node) {
2,028,621,808✔
1032
            return same_node.to_owned();
2,478,056✔
1033
        }
239,747✔
1034

239,747✔
1035
        let maybe_previous_node = self.all_nodes.borrow_mut().insert(id, new_node.clone());
239,747✔
1036
        let new_node_is_new = maybe_previous_node.is_none();
239,747✔
1037
        assert!(new_node_is_new, "Leaf-created node must be new… {new_node}");
239,747✔
1038
        self.id_counter.borrow_mut().add_assign(1);
239,747✔
1039
        new_node
239,747✔
1040
    }
2,717,803✔
1041

1042
    /// Replace all pointers to a given node (identified by `old_id`) by one
1043
    /// to the new node.
1044
    ///
1045
    /// A circuit's root node cannot be substituted with this method. Manual care
1046
    /// must be taken to update the root node if necessary.
1047
    fn redirect_all_references_to_node(&self, old_id: usize, new: ConstraintCircuitMonad<II>) {
2,389✔
1048
        self.all_nodes.borrow_mut().remove(&old_id);
2,389✔
1049
        for node in self.all_nodes.borrow_mut().values_mut() {
7,162,122✔
1050
            let node_expression = &mut node.circuit.borrow_mut().expression;
7,162,122✔
1051
            let CircuitExpression::BinOp(_, ref mut node_lhs, ref mut node_rhs) = node_expression
7,162,122✔
1052
            else {
1053
                continue;
533,712✔
1054
            };
1055

1056
            if node_lhs.borrow().id == old_id {
6,628,410✔
1057
                *node_lhs = new.circuit.clone();
8,428✔
1058
            }
6,619,982✔
1059
            if node_rhs.borrow().id == old_id {
6,628,410✔
1060
                *node_rhs = new.circuit.clone();
2,167✔
1061
            }
6,626,243✔
1062
        }
1063
    }
2,389✔
1064
}
1065

1066
impl<'a, II: InputIndicator + Arbitrary<'a>> Arbitrary<'a> for ConstraintCircuitMonad<II> {
1067
    fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
2,090✔
1068
        let builder = ConstraintCircuitBuilder::new();
2,090✔
1069
        let mut random_circuit = random_circuit_leaf(&builder, u)?;
2,090✔
1070

1071
        let num_nodes_in_circuit = u.arbitrary_len::<Self>()?;
2,090✔
1072
        for _ in 0..num_nodes_in_circuit {
2,090✔
1073
            let leaf = random_circuit_leaf(&builder, u)?;
244,426✔
1074
            match u.int_in_range(0..=5)? {
244,426✔
1075
                0 => random_circuit = random_circuit * leaf,
208,167✔
1076
                1 => random_circuit = random_circuit + leaf,
7,177✔
1077
                2 => random_circuit = random_circuit - leaf,
7,364✔
1078
                3 => random_circuit = leaf * random_circuit,
7,339✔
1079
                4 => random_circuit = leaf + random_circuit,
7,203✔
1080
                5 => random_circuit = leaf - random_circuit,
7,176✔
1081
                _ => unreachable!(),
×
1082
            }
1083
        }
1084

1085
        Ok(random_circuit)
2,090✔
1086
    }
2,090✔
1087
}
1088

1089
fn random_circuit_leaf<'a, II: InputIndicator + Arbitrary<'a>>(
246,516✔
1090
    builder: &ConstraintCircuitBuilder<II>,
246,516✔
1091
    u: &mut Unstructured<'a>,
246,516✔
1092
) -> arbitrary::Result<ConstraintCircuitMonad<II>> {
246,516✔
1093
    let leaf = match u.int_in_range(0..=5)? {
246,516✔
1094
        0 => builder.input(u.arbitrary()?),
207,207✔
1095
        1 => builder.challenge(u.arbitrary::<usize>()?),
7,753✔
1096
        2 => builder.b_constant(u.arbitrary::<BFieldElement>()?),
7,789✔
1097
        3 => builder.x_constant(u.arbitrary::<XFieldElement>()?),
8,017✔
1098
        4 => builder.one(),
7,945✔
1099
        5 => builder.zero(),
7,805✔
1100
        _ => unreachable!(),
×
1101
    };
1102
    Ok(leaf)
246,516✔
1103
}
246,516✔
1104

1105
#[cfg(test)]
1106
mod tests {
1107
    use std::collections::hash_map::DefaultHasher;
1108
    use std::hash::Hasher;
1109

1110
    use itertools::Itertools;
1111
    use ndarray::Array2;
1112
    use ndarray::Axis;
1113
    use proptest::arbitrary::Arbitrary;
1114
    use proptest::collection::vec;
1115
    use proptest::prelude::*;
1116
    use proptest_arbitrary_interop::arb;
1117
    use rand::random;
1118
    use test_strategy::proptest;
1119

1120
    use super::*;
1121

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

2✔
1150
            eval_to_ids
2✔
1151
                .values()
2✔
1152
                .filter(|ids| ids.len() >= 2)
36✔
1153
                .map(|ids| ids.iter().map(|i| id_to_node[i].clone()).collect_vec())
2✔
1154
                .collect_vec()
2✔
1155
        }
2✔
1156

1157
        /// Populate the dictionaries such that they associate with every node in
1158
        /// the circuit its evaluation in a random point. The inputs are assigned
1159
        /// random values. Equivalent nodes are detected based on evaluating to the
1160
        /// same value using the Schwartz-Zippel lemma.
1161
        fn probe_random(
52✔
1162
            circuit: &Rc<RefCell<ConstraintCircuit<II>>>,
52✔
1163
            id_to_eval: &mut HashMap<usize, XFieldElement>,
52✔
1164
            eval_to_ids: &mut HashMap<XFieldElement, Vec<usize>>,
52✔
1165
            id_to_node: &mut HashMap<usize, Rc<RefCell<ConstraintCircuit<II>>>>,
52✔
1166
            master_seed: XFieldElement,
52✔
1167
        ) -> XFieldElement {
52✔
1168
            const DOMAIN_SEPARATOR_CURR_ROW: BFieldElement = BFieldElement::new(0);
1169
            const DOMAIN_SEPARATOR_NEXT_ROW: BFieldElement = BFieldElement::new(1);
1170
            const DOMAIN_SEPARATOR_CHALLENGE: BFieldElement = BFieldElement::new(2);
1171

1172
            let circuit_id = circuit.borrow().id;
52✔
1173
            if let Some(&xfe) = id_to_eval.get(&circuit_id) {
52✔
1174
                return xfe;
15✔
1175
            }
37✔
1176

1177
            let evaluation = match &circuit.borrow().expression {
37✔
1178
                CircuitExpression::BConst(bfe) => bfe.lift(),
2✔
1179
                CircuitExpression::XConst(xfe) => *xfe,
×
1180
                CircuitExpression::Input(input) => {
8✔
1181
                    let [s0, s1, s2] = master_seed.coefficients;
8✔
1182
                    let dom_sep = if input.is_current_row() {
8✔
1183
                        DOMAIN_SEPARATOR_CURR_ROW
8✔
1184
                    } else {
1185
                        DOMAIN_SEPARATOR_NEXT_ROW
×
1186
                    };
1187
                    let i = bfe!(u64::try_from(input.column()).unwrap());
8✔
1188
                    let Digest([d0, d1, d2, _, _]) = Tip5::hash_varlen(&[s0, s1, s2, dom_sep, i]);
8✔
1189
                    xfe!([d0, d1, d2])
8✔
1190
                }
1191
                CircuitExpression::Challenge(challenge) => {
2✔
1192
                    let [s0, s1, s2] = master_seed.coefficients;
2✔
1193
                    let dom_sep = DOMAIN_SEPARATOR_CHALLENGE;
2✔
1194
                    let ch = bfe!(u64::try_from(*challenge).unwrap());
2✔
1195
                    let Digest([d0, d1, d2, _, _]) = Tip5::hash_varlen(&[s0, s1, s2, dom_sep, ch]);
2✔
1196
                    xfe!([d0, d1, d2])
2✔
1197
                }
1198
                CircuitExpression::BinOp(bin_op, lhs, rhs) => {
25✔
1199
                    let l =
25✔
1200
                        Self::probe_random(lhs, id_to_eval, eval_to_ids, id_to_node, master_seed);
25✔
1201
                    let r =
25✔
1202
                        Self::probe_random(rhs, id_to_eval, eval_to_ids, id_to_node, master_seed);
25✔
1203
                    bin_op.operation(l, r)
25✔
1204
                }
1205
            };
1206

1207
            id_to_eval.insert(circuit_id, evaluation);
37✔
1208
            eval_to_ids.entry(evaluation).or_default().push(circuit_id);
37✔
1209
            id_to_node.insert(circuit_id, circuit.clone());
37✔
1210

37✔
1211
            evaluation
37✔
1212
        }
52✔
1213

1214
        /// Check whether the given node is contained in this circuit.
1215
        fn contains(&self, other: &Self) -> bool {
9✔
1216
            let self_expression = &self.circuit.borrow().expression;
9✔
1217
            let other_expression = &other.circuit.borrow().expression;
9✔
1218
            self_expression.contains(other_expression)
9✔
1219
        }
9✔
1220

1221
        /// Counts the number of inputs from the main table
1222
        fn num_nodes(constraints: &[Self]) -> usize {
256✔
1223
            constraints
256✔
1224
                .first()
256✔
1225
                .unwrap()
256✔
1226
                .builder
256✔
1227
                .all_nodes
256✔
1228
                .borrow()
256✔
1229
                .len()
256✔
1230
        }
256✔
1231

1232
        /// Counts the number of inputs from the main table
1233
        fn num_main_inputs(constraints: &[Self]) -> usize {
1,513✔
1234
            constraints
1,513✔
1235
                .first()
1,513✔
1236
                .unwrap()
1,513✔
1237
                .builder
1,513✔
1238
                .all_nodes
1,513✔
1239
                .borrow()
1,513✔
1240
                .iter()
1,513✔
1241
                .filter(|(_, cc)| {
250,101✔
1242
                    if let CircuitExpression::Input(ii) = cc.circuit.as_ref().borrow().expression {
250,101✔
1243
                        ii.is_main_table_column()
15,130✔
1244
                    } else {
1245
                        false
234,971✔
1246
                    }
1247
                })
250,101✔
1248
                .filter(|(_, cc)| cc.circuit.borrow().evaluates_to_base_element())
7,489✔
1249
                .count()
1,513✔
1250
        }
1,513✔
1251

1252
        /// Counts the number of inputs from the aux table
1253
        fn num_aux_inputs(constraints: &[Self]) -> usize {
1,513✔
1254
            constraints
1,513✔
1255
                .first()
1,513✔
1256
                .unwrap()
1,513✔
1257
                .builder
1,513✔
1258
                .all_nodes
1,513✔
1259
                .borrow()
1,513✔
1260
                .iter()
1,513✔
1261
                .filter(|(_, cc)| {
250,101✔
1262
                    if let CircuitExpression::Input(ii) = cc.circuit.as_ref().borrow().expression {
250,101✔
1263
                        !ii.is_main_table_column()
15,130✔
1264
                    } else {
1265
                        false
234,971✔
1266
                    }
1267
                })
250,101✔
1268
                .count()
1,513✔
1269
        }
1,513✔
1270

1271
        /// Counts the number of total (*i.e.*, main + aux) inputs
1272
        fn num_inputs(constraints: &[Self]) -> usize {
256✔
1273
            Self::num_main_inputs(constraints) + Self::num_aux_inputs(constraints)
256✔
1274
        }
256✔
1275

1276
        /// Counts the number of challenges
1277
        fn num_challenges(constraints: &[Self]) -> usize {
1,257✔
1278
            constraints
1,257✔
1279
                .first()
1,257✔
1280
                .unwrap()
1,257✔
1281
                .builder
1,257✔
1282
                .all_nodes
1,257✔
1283
                .borrow()
1,257✔
1284
                .iter()
1,257✔
1285
                .filter(|(_, cc)| {
224,651✔
1286
                    matches!(
212,081✔
1287
                        cc.circuit.as_ref().borrow().expression,
224,651✔
1288
                        CircuitExpression::Challenge(_)
1289
                    )
1290
                })
224,651✔
1291
                .count()
1,257✔
1292
        }
1,257✔
1293

1294
        /// Counts the number of `BinOp`s
1295
        fn num_binops(constraints: &[Self]) -> usize {
256✔
1296
            constraints
256✔
1297
                .first()
256✔
1298
                .unwrap()
256✔
1299
                .builder
256✔
1300
                .all_nodes
256✔
1301
                .borrow()
256✔
1302
                .iter()
256✔
1303
                .filter(|(_, cc)| {
25,450✔
1304
                    matches!(
8,477✔
1305
                        cc.circuit.as_ref().borrow().expression,
25,450✔
1306
                        CircuitExpression::BinOp(_, _, _)
1307
                    )
1308
                })
25,450✔
1309
                .count()
256✔
1310
        }
256✔
1311

1312
        /// Counts the number of BFE constants
1313
        fn num_bfield_constants(constraints: &[Self]) -> usize {
256✔
1314
            constraints
256✔
1315
                .first()
256✔
1316
                .unwrap()
256✔
1317
                .builder
256✔
1318
                .all_nodes
256✔
1319
                .borrow()
256✔
1320
                .iter()
256✔
1321
                .filter(|(_, cc)| {
25,450✔
1322
                    matches!(
24,018✔
1323
                        cc.circuit.as_ref().borrow().expression,
25,450✔
1324
                        CircuitExpression::BConst(_)
1325
                    )
1326
                })
25,450✔
1327
                .count()
256✔
1328
        }
256✔
1329

1330
        /// Counts the number of XFE constants
1331
        fn num_xfield_constants(constraints: &[Self]) -> usize {
256✔
1332
            constraints
256✔
1333
                .first()
256✔
1334
                .unwrap()
256✔
1335
                .builder
256✔
1336
                .all_nodes
256✔
1337
                .borrow()
256✔
1338
                .iter()
256✔
1339
                .filter(|(_, cc)| {
25,450✔
1340
                    matches!(
23,525✔
1341
                        cc.circuit.as_ref().borrow().expression,
25,450✔
1342
                        CircuitExpression::XConst(_)
1343
                    )
1344
                })
25,450✔
1345
                .count()
256✔
1346
        }
256✔
1347
    }
1348

1349
    impl<II: InputIndicator> CircuitExpression<II> {
1350
        /// Check whether the given node is contained in this circuit.
1351
        fn contains(&self, other: &Self) -> bool {
43✔
1352
            if self == other {
43✔
1353
                return true;
5✔
1354
            }
38✔
1355
            let CircuitExpression::BinOp(_, lhs, rhs) = self else {
38✔
1356
                return false;
17✔
1357
            };
1358

1359
            lhs.borrow().expression.contains(other) || rhs.borrow().expression.contains(other)
21✔
1360
        }
43✔
1361
    }
1362

1363
    /// The [`Hash`] trait requires:
1364
    /// circuit_0 == circuit_1 => hash(circuit_0) == hash(circuit_1)
1365
    ///
1366
    /// By De-Morgan's law, this is equivalent to the more meaningful test:
1367
    /// hash(circuit_0) != hash(circuit_1) => circuit_0 != circuit_1
1368
    #[proptest]
512✔
1369
    fn unequal_hash_implies_unequal_constraint_circuit_monad(
1370
        #[strategy(arb())] circuit_0: ConstraintCircuitMonad<SingleRowIndicator>,
1✔
1371
        #[strategy(arb())] circuit_1: ConstraintCircuitMonad<SingleRowIndicator>,
1✔
1372
    ) {
1373
        if hash_circuit(&circuit_0) != hash_circuit(&circuit_1) {
1374
            prop_assert_ne!(circuit_0, circuit_1);
1375
        }
1376
    }
1377

1378
    /// The hash of a node may not depend on `ref_count`, `counter`, `id_counter_ref`, or
1379
    /// `all_nodes`, since `all_nodes` contains the digest of all nodes in the multi tree.
1380
    /// For more details, see [`HashSet`].
1381
    #[proptest]
512✔
1382
    fn multi_circuit_hash_is_unchanged_by_meta_data(
1383
        #[strategy(arb())] circuit: ConstraintCircuitMonad<DualRowIndicator>,
1✔
1384
        new_ref_count: usize,
1385
        new_id_counter: usize,
1386
    ) {
1387
        let original_digest = hash_circuit(&circuit);
1388

1389
        circuit.circuit.borrow_mut().ref_count = new_ref_count;
1390
        prop_assert_eq!(original_digest, hash_circuit(&circuit));
1391

1392
        circuit.builder.id_counter.replace(new_id_counter);
1393
        prop_assert_eq!(original_digest, hash_circuit(&circuit));
1394
    }
1395

1396
    fn hash_circuit<II: InputIndicator>(circuit: &ConstraintCircuitMonad<II>) -> u64 {
1,280✔
1397
        let mut hasher = DefaultHasher::new();
1,280✔
1398
        circuit.hash(&mut hasher);
1,280✔
1399
        hasher.finish()
1,280✔
1400
    }
1,280✔
1401

1402
    #[test]
1403
    fn printing_constraint_circuit_gives_expected_strings() {
1✔
1404
        let builder = ConstraintCircuitBuilder::new();
1✔
1405
        assert_eq!("1", builder.b_constant(1).to_string());
1✔
1406
        assert_eq!(
1✔
1407
            "main_row[5] ",
1✔
1408
            builder.input(SingleRowIndicator::Main(5)).to_string()
1✔
1409
        );
1✔
1410
        assert_eq!("6", builder.challenge(6_usize).to_string());
1✔
1411

1412
        let xfe_str = builder.x_constant([2, 3, 4]).to_string();
1✔
1413
        assert_eq!("(4·x² + 3·x + 2)", xfe_str);
1✔
1414
    }
1✔
1415

1416
    #[proptest]
256✔
1417
    fn constant_folding_can_deal_with_multiplication_by_one(
1418
        #[strategy(arb())] c: ConstraintCircuitMonad<DualRowIndicator>,
1✔
1419
    ) {
1420
        let one = || c.builder.one();
1,024✔
1421
        prop_assert_eq!(c.clone(), c.clone() * one());
1422
        prop_assert_eq!(c.clone(), one() * c.clone());
1423
        prop_assert_eq!(c.clone(), one() * c.clone() * one());
1424
    }
1425

1426
    #[proptest]
256✔
1427
    fn constant_folding_can_deal_with_adding_zero(
1428
        #[strategy(arb())] c: ConstraintCircuitMonad<DualRowIndicator>,
1✔
1429
    ) {
1430
        let zero = || c.builder.zero();
1,024✔
1431
        prop_assert_eq!(c.clone(), c.clone() + zero());
1432
        prop_assert_eq!(c.clone(), zero() + c.clone());
1433
        prop_assert_eq!(c.clone(), zero() + c.clone() + zero());
1434
    }
1435

1436
    #[proptest]
256✔
1437
    fn constant_folding_can_deal_with_subtracting_zero(
1438
        #[strategy(arb())] c: ConstraintCircuitMonad<DualRowIndicator>,
1✔
1439
    ) {
1440
        prop_assert_eq!(c.clone(), c.clone() - c.builder.zero());
1441
    }
1442

1443
    #[proptest]
512✔
1444
    fn constant_folding_can_deal_with_adding_effectively_zero_term(
1445
        #[strategy(arb())] c: ConstraintCircuitMonad<DualRowIndicator>,
1✔
1446
        modification_selectors: [bool; 4],
1447
    ) {
1448
        let zero = || c.builder.zero();
506✔
1449
        let mut redundant_circuit = c.clone();
1450
        if modification_selectors[0] {
1451
            redundant_circuit = redundant_circuit + (c.clone() * zero());
1452
        }
1453
        if modification_selectors[1] {
1454
            redundant_circuit = redundant_circuit + (zero() * c.clone());
1455
        }
1456
        if modification_selectors[2] {
1457
            redundant_circuit = (c.clone() * zero()) + redundant_circuit;
1458
        }
1459
        if modification_selectors[3] {
1460
            redundant_circuit = (zero() * c.clone()) + redundant_circuit;
1461
        }
1462
        prop_assert_eq!(c, redundant_circuit);
1463
    }
1464

1465
    #[proptest]
298✔
1466
    fn constant_folding_does_not_replace_0_minus_circuit_with_the_circuit(
1467
        #[strategy(arb())] circuit: ConstraintCircuitMonad<DualRowIndicator>,
1✔
1468
    ) {
1469
        if circuit.circuit.borrow().is_zero() {
1470
            return Err(TestCaseError::Reject("0 - 0 actually is 0".into()));
1471
        }
1472
        let zero_minus_circuit = circuit.builder.zero() - circuit.clone();
1473
        prop_assert_ne!(&circuit, &zero_minus_circuit);
1474
    }
1475

1476
    #[test]
1477
    fn pointer_redirection_obliviates_a_node_in_a_circuit() {
1✔
1478
        let builder = ConstraintCircuitBuilder::new();
1✔
1479
        let x = |i| builder.input(SingleRowIndicator::Main(i));
5✔
1480
        let constant = |c: u32| builder.b_constant(c);
2✔
1481
        let challenge = |i: usize| builder.challenge(i);
3✔
1482

1483
        let part = x(0) + x(1);
1✔
1484
        let substitute_me = x(0) * part.clone();
1✔
1485
        let root_0 = part.clone() + challenge(1) - constant(84);
1✔
1486
        let root_1 = substitute_me.clone() + challenge(0) - constant(42);
1✔
1487
        let root_2 = x(2) * substitute_me.clone() - challenge(1);
1✔
1488

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

1493
        let new_variable = x(3);
1✔
1494
        builder.redirect_all_references_to_node(
1✔
1495
            substitute_me.circuit.borrow().id,
1✔
1496
            new_variable.clone(),
1✔
1497
        );
1✔
1498

1✔
1499
        assert!(!root_0.contains(&substitute_me));
1✔
1500
        assert!(!root_1.contains(&substitute_me));
1✔
1501
        assert!(!root_2.contains(&substitute_me));
1✔
1502

1503
        assert!(root_0.contains(&part));
1✔
1504
        assert!(root_1.contains(&new_variable));
1✔
1505
        assert!(root_2.contains(&new_variable));
1✔
1506
    }
1✔
1507

1508
    #[test]
1509
    fn simple_degree_lowering() {
1✔
1510
        let builder = ConstraintCircuitBuilder::new();
1✔
1511
        let x = || builder.input(SingleRowIndicator::Main(0));
8✔
1512
        let x_pow_3 = x() * x() * x();
1✔
1513
        let x_pow_5 = x() * x() * x() * x() * x();
1✔
1514
        let mut multicircuit = [x_pow_5, x_pow_3];
1✔
1515

1✔
1516
        let degree_lowering_info = DegreeLoweringInfo {
1✔
1517
            target_degree: 3,
1✔
1518
            num_main_cols: 1,
1✔
1519
            num_aux_cols: 0,
1✔
1520
        };
1✔
1521
        let (new_main_constraints, new_aux_constraints) =
1✔
1522
            ConstraintCircuitMonad::lower_to_degree(&mut multicircuit, degree_lowering_info);
1✔
1523

1✔
1524
        assert_eq!(1, new_main_constraints.len());
1✔
1525
        assert!(new_aux_constraints.is_empty());
1✔
1526
    }
1✔
1527

1528
    #[test]
1529
    fn somewhat_simple_degree_lowering() {
1✔
1530
        let builder = ConstraintCircuitBuilder::new();
1✔
1531
        let x = |i| builder.input(SingleRowIndicator::Main(i));
16✔
1532
        let y = |i| builder.input(SingleRowIndicator::Aux(i));
2✔
1533
        let b_con = |i: u64| builder.b_constant(i);
9✔
1534

1535
        let constraint_0 = x(0) * x(0) * (x(1) - x(2)) - x(0) * x(2) - b_con(42);
1✔
1536
        let constraint_1 = x(1) * (x(1) - b_con(5)) * x(2) * (x(2) - b_con(1));
1✔
1537
        let constraint_2 = y(0)
1✔
1538
            * (b_con(2) * x(0) + b_con(3) * x(1) + b_con(4) * x(2))
1✔
1539
            * (b_con(4) * x(0) + b_con(8) * x(1) + b_con(16) * x(2))
1✔
1540
            - y(1);
1✔
1541

1✔
1542
        let mut multicircuit = [constraint_0, constraint_1, constraint_2];
1✔
1543

1✔
1544
        let degree_lowering_info = DegreeLoweringInfo {
1✔
1545
            target_degree: 2,
1✔
1546
            num_main_cols: 3,
1✔
1547
            num_aux_cols: 2,
1✔
1548
        };
1✔
1549
        let (new_main_constraints, new_aux_constraints) =
1✔
1550
            ConstraintCircuitMonad::lower_to_degree(&mut multicircuit, degree_lowering_info);
1✔
1551

1✔
1552
        assert!(new_main_constraints.len() <= 3);
1✔
1553
        assert!(new_aux_constraints.len() <= 1);
1✔
1554
    }
1✔
1555

1556
    #[test]
1557
    fn less_simple_degree_lowering() {
1✔
1558
        let builder = ConstraintCircuitBuilder::new();
1✔
1559
        let x = |i| builder.input(SingleRowIndicator::Main(i));
11✔
1560

1561
        let constraint_0 = (x(0) * x(1) * x(2)) * (x(3) * x(4)) * x(5);
1✔
1562
        let constraint_1 = (x(6) * x(7)) * (x(3) * x(4)) * x(8);
1✔
1563

1✔
1564
        let mut multicircuit = [constraint_0, constraint_1];
1✔
1565

1✔
1566
        let degree_lowering_info = DegreeLoweringInfo {
1✔
1567
            target_degree: 3,
1✔
1568
            num_main_cols: 9,
1✔
1569
            num_aux_cols: 0,
1✔
1570
        };
1✔
1571
        let (new_main_constraints, new_aux_constraints) =
1✔
1572
            ConstraintCircuitMonad::lower_to_degree(&mut multicircuit, degree_lowering_info);
1✔
1573

1✔
1574
        assert!(new_main_constraints.len() <= 3);
1✔
1575
        assert!(new_aux_constraints.is_empty());
1✔
1576
    }
1✔
1577

1578
    fn circuit_with_multiple_options_for_degree_lowering_to_degree_2(
22✔
1579
    ) -> [ConstraintCircuitMonad<SingleRowIndicator>; 2] {
22✔
1580
        let builder = ConstraintCircuitBuilder::new();
22✔
1581
        let x = |i| builder.input(SingleRowIndicator::Main(i));
132✔
1582

1583
        let constraint_0 = x(0) * x(0) * x(0);
22✔
1584
        let constraint_1 = x(1) * x(1) * x(1);
22✔
1585

22✔
1586
        [constraint_0, constraint_1]
22✔
1587
    }
22✔
1588

1589
    #[test]
1590
    fn pick_node_to_substitute_is_deterministic() {
1✔
1591
        let multicircuit = circuit_with_multiple_options_for_degree_lowering_to_degree_2();
1✔
1592
        let first_node_id = ConstraintCircuitMonad::pick_node_to_substitute(&multicircuit, 2);
1✔
1593

1594
        for _ in 0..20 {
21✔
1595
            let node_id_again = ConstraintCircuitMonad::pick_node_to_substitute(&multicircuit, 2);
20✔
1596
            assert_eq!(first_node_id, node_id_again);
20✔
1597
        }
1598
    }
1✔
1599

1600
    #[test]
1601
    fn degree_lowering_specific_simple_circuit_is_deterministic() {
1✔
1602
        let degree_lowering_info = DegreeLoweringInfo {
1✔
1603
            target_degree: 2,
1✔
1604
            num_main_cols: 2,
1✔
1605
            num_aux_cols: 0,
1✔
1606
        };
1✔
1607

1✔
1608
        let mut original_multicircuit =
1✔
1609
            circuit_with_multiple_options_for_degree_lowering_to_degree_2();
1✔
1610
        let (new_main_constraints, _) = ConstraintCircuitMonad::lower_to_degree(
1✔
1611
            &mut original_multicircuit,
1✔
1612
            degree_lowering_info,
1✔
1613
        );
1✔
1614

1615
        for _ in 0..20 {
21✔
1616
            let mut new_multicircuit =
20✔
1617
                circuit_with_multiple_options_for_degree_lowering_to_degree_2();
20✔
1618
            let (new_main_constraints_again, _) = ConstraintCircuitMonad::lower_to_degree(
20✔
1619
                &mut new_multicircuit,
20✔
1620
                degree_lowering_info,
20✔
1621
            );
20✔
1622
            assert_eq!(new_main_constraints, new_main_constraints_again);
20✔
1623
            assert_eq!(original_multicircuit, new_multicircuit);
20✔
1624
        }
1625
    }
1✔
1626

1627
    #[test]
1628
    fn all_nodes_in_multicircuit_are_identified_correctly() {
1✔
1629
        let builder = ConstraintCircuitBuilder::new();
1✔
1630

1✔
1631
        let x = |i| builder.input(SingleRowIndicator::Main(i));
30✔
1632
        let b_con = |i: u64| builder.b_constant(i);
3✔
1633

1634
        let sub_tree_0 = x(0) * x(1) * (x(2) - b_con(1)) * x(3) * x(4);
1✔
1635
        let sub_tree_1 = x(0) * x(1) * (x(2) - b_con(1)) * x(3) * x(5);
1✔
1636
        let sub_tree_2 = x(10) * x(10) * x(2) * x(13);
1✔
1637
        let sub_tree_3 = x(10) * x(10) * x(2) * x(14);
1✔
1638

1✔
1639
        let circuit_0 = sub_tree_0.clone() + sub_tree_1.clone();
1✔
1640
        let circuit_1 = sub_tree_2.clone() + sub_tree_3.clone();
1✔
1641
        let circuit_2 = sub_tree_0 + sub_tree_2;
1✔
1642
        let circuit_3 = sub_tree_1 + sub_tree_3;
1✔
1643

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

1✔
1646
        let all_nodes = ConstraintCircuitMonad::all_nodes_in_multicircuit(&multicircuit);
1✔
1647
        let count_node = |node| all_nodes.iter().filter(|&n| n == &node).count();
532✔
1648

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

1652
        let x2 = x(2).consume();
1✔
1653
        assert_eq!(8, count_node(x2));
1✔
1654

1655
        let x10 = x(10).consume();
1✔
1656
        assert_eq!(8, count_node(x10));
1✔
1657

1658
        let x4 = x(4).consume();
1✔
1659
        assert_eq!(2, count_node(x4));
1✔
1660

1661
        let x6 = x(6).consume();
1✔
1662
        assert_eq!(0, count_node(x6));
1✔
1663

1664
        let x0_x1 = (x(0) * x(1)).consume();
1✔
1665
        assert_eq!(4, count_node(x0_x1));
1✔
1666

1667
        let tree = (x(0) * x(1) * (x(2) - b_con(1))).consume();
1✔
1668
        assert_eq!(4, count_node(tree));
1✔
1669

1670
        let max_occurences = all_nodes
1✔
1671
            .iter()
1✔
1672
            .map(|node| all_nodes.iter().filter(|&n| n == node).count())
5,776✔
1673
            .max()
1✔
1674
            .unwrap();
1✔
1675
        assert_eq!(8, max_occurences);
1✔
1676

1677
        let most_frequent_nodes = all_nodes
1✔
1678
            .iter()
1✔
1679
            .filter(|&node| all_nodes.iter().filter(|&n| n == node).count() == max_occurences)
5,776✔
1680
            .unique()
1✔
1681
            .collect_vec();
1✔
1682
        assert_eq!(2, most_frequent_nodes.len());
1✔
1683
        assert!(most_frequent_nodes.contains(&&x(2).consume()));
1✔
1684
        assert!(most_frequent_nodes.contains(&&x(10).consume()));
1✔
1685
    }
1✔
1686

1687
    #[test]
1688
    fn equivalent_nodes_are_detected_when_present() {
1✔
1689
        let builder = ConstraintCircuitBuilder::new();
1✔
1690

1✔
1691
        let x = |i| builder.input(SingleRowIndicator::Main(i));
12✔
1692
        let ch = |i: usize| builder.challenge(i);
2✔
1693

1694
        let u0 = x(0) + x(1);
1✔
1695
        let u1 = x(2) + x(3);
1✔
1696
        let v = u0 * u1;
1✔
1697

1✔
1698
        let z0 = x(0) * x(2);
1✔
1699
        let z2 = x(1) * x(3);
1✔
1700

1✔
1701
        let z1 = x(1) * x(2) + x(0) * x(3);
1✔
1702
        let w = v - z0 - z2;
1✔
1703
        assert!(w.find_equivalent_nodes().is_empty());
1✔
1704

1705
        let o = ch(0) * z1 - ch(1) * w;
1✔
1706
        assert!(!o.find_equivalent_nodes().is_empty());
1✔
1707
    }
1✔
1708

1709
    #[derive(Debug, Copy, Clone, Eq, PartialEq, test_strategy::Arbitrary)]
376,180✔
1710
    enum CircuitOperationChoice {
1711
        Add(usize, usize),
1712
        Mul(usize, usize),
1713
    }
1714

1715
    #[derive(Debug, Copy, Clone, Eq, PartialEq, test_strategy::Arbitrary)]
12,570✔
1716
    enum CircuitInputType {
1717
        Main,
1718
        Aux,
1719
    }
1720

1721
    #[derive(Debug, Copy, Clone, Eq, PartialEq, test_strategy::Arbitrary)]
12,570✔
1722
    enum CircuitConstantType {
1723
        Base(#[strategy(arb())] BFieldElement),
2✔
1724
        Extension(#[strategy(arb())] XFieldElement),
2✔
1725
    }
1726

1727
    fn arbitrary_circuit_monad<II: InputIndicator>(
2✔
1728
        num_inputs: usize,
2✔
1729
        num_challenges: usize,
2✔
1730
        num_constants: usize,
2✔
1731
        num_nodes: usize,
2✔
1732
        num_outputs: usize,
2✔
1733
    ) -> BoxedStrategy<Vec<ConstraintCircuitMonad<II>>> {
2✔
1734
        (
2✔
1735
            vec(CircuitInputType::arbitrary(), num_inputs),
2✔
1736
            vec(CircuitConstantType::arbitrary(), num_constants),
2✔
1737
            vec(
2✔
1738
                CircuitOperationChoice::arbitrary(),
2✔
1739
                num_nodes - num_inputs - num_constants - num_outputs,
2✔
1740
            ),
2✔
1741
            vec(arb::<usize>(), num_outputs),
2✔
1742
        )
2✔
1743
            .prop_map(move |(inputs, constants, operations, outputs)| {
1,257✔
1744
                let builder = ConstraintCircuitBuilder::<II>::new();
1,257✔
1745

1,257✔
1746
                assert_eq!(0, *builder.id_counter.as_ref().borrow());
1,257✔
1747
                assert!(
1,257✔
1748
                    builder.all_nodes.borrow().is_empty(),
1,257✔
NEW
1749
                    "fresh hashmap should be empty"
×
1750
                );
1751

1752
                let mut num_main_inputs = 0;
1,257✔
1753
                let mut num_aux_inputs = 0;
1,257✔
1754
                let mut all_nodes = vec![];
1,257✔
1755
                let mut output_nodes = vec![];
1,257✔
1756

1757
                for input in inputs {
13,827✔
1758
                    match input {
12,570✔
1759
                        CircuitInputType::Main => {
6,238✔
1760
                            let node = builder.input(II::main_table_input(num_main_inputs));
6,238✔
1761
                            num_main_inputs += 1;
6,238✔
1762
                            all_nodes.push(node);
6,238✔
1763
                        }
6,238✔
1764
                        CircuitInputType::Aux => {
6,332✔
1765
                            let node = builder.input(II::aux_table_input(num_aux_inputs));
6,332✔
1766
                            num_aux_inputs += 1;
6,332✔
1767
                            all_nodes.push(node);
6,332✔
1768
                        }
6,332✔
1769
                    }
1770
                }
1771

1772
                for i in 0..num_challenges {
12,570✔
1773
                    let node = builder.challenge(i);
12,570✔
1774
                    all_nodes.push(node);
12,570✔
1775
                }
12,570✔
1776

1777
                for constant in constants {
13,827✔
1778
                    let node = match constant {
12,570✔
1779
                        CircuitConstantType::Base(bfe) => builder.b_constant(bfe),
6,306✔
1780
                        CircuitConstantType::Extension(xfe) => builder.x_constant(xfe),
6,264✔
1781
                    };
1782
                    all_nodes.push(node);
12,570✔
1783
                }
1784

1785
                if all_nodes.is_empty() {
1,257✔
NEW
1786
                    return vec![];
×
1787
                }
1,257✔
1788

1789
                for operation in operations {
189,347✔
1790
                    let (lhs, rhs) = match operation {
188,090✔
1791
                        CircuitOperationChoice::Add(lhs, rhs) => (lhs, rhs),
93,784✔
1792
                        CircuitOperationChoice::Mul(lhs, rhs) => (lhs, rhs),
94,306✔
1793
                    };
1794

1795
                    let lhs_index = lhs % all_nodes.len();
188,090✔
1796
                    let rhs_index = rhs % all_nodes.len();
188,090✔
1797

188,090✔
1798
                    let lhs_node = all_nodes[lhs_index].clone();
188,090✔
1799
                    let rhs_node = all_nodes[rhs_index].clone();
188,090✔
1800

1801
                    let node = match operation {
188,090✔
1802
                        CircuitOperationChoice::Add(_, _) => lhs_node + rhs_node,
93,784✔
1803
                        CircuitOperationChoice::Mul(_, _) => lhs_node * rhs_node,
94,306✔
1804
                    };
1805
                    all_nodes.push(node);
188,090✔
1806
                }
1807

1808
                for output in outputs {
13,827✔
1809
                    let index = output % all_nodes.len();
12,570✔
1810
                    output_nodes.push(all_nodes[index].clone());
12,570✔
1811
                }
12,570✔
1812

1813
                output_nodes
1,257✔
1814
            })
1,257✔
1815
            .boxed()
2✔
1816
    }
2✔
1817

1818
    #[proptest]
256✔
1819
    fn node_type_counts_add_up(
1820
        #[strategy(arbitrary_circuit_monad(10, 10, 10, 100, 10))] multicircuit_monad: Vec<
1✔
1821
            ConstraintCircuitMonad<SingleRowIndicator>,
1822
        >,
1823
    ) {
1824
        prop_assert_eq!(
1825
            ConstraintCircuitMonad::num_nodes(&multicircuit_monad),
1826
            ConstraintCircuitMonad::num_main_inputs(&multicircuit_monad)
1827
                + ConstraintCircuitMonad::num_aux_inputs(&multicircuit_monad)
1828
                + ConstraintCircuitMonad::num_challenges(&multicircuit_monad)
1829
                + ConstraintCircuitMonad::num_bfield_constants(&multicircuit_monad)
1830
                + ConstraintCircuitMonad::num_xfield_constants(&multicircuit_monad)
1831
                + ConstraintCircuitMonad::num_binops(&multicircuit_monad)
1832
        );
1833

1834
        prop_assert_eq!(10, ConstraintCircuitMonad::num_inputs(&multicircuit_monad));
1835
    }
1836

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

1913
        let output_before_lowering = multicircuit_monad
1914
            .iter()
1915
            .map(|constraint| {
10,000✔
1916
                constraint.circuit.borrow().evaluate(
10,000✔
1917
                    main_input.view(),
10,000✔
1918
                    aux_input.view(),
10,000✔
1919
                    &challenges,
10,000✔
1920
                )
10,000✔
1921
            })
10,000✔
1922
            .collect_vec();
1923

1924
        // apply one step of degree-lowering
1925
        let num_nodes = multicircuit_monad[0].builder.all_nodes.borrow().len();
1926
        let (mut substitution_node_id, mut substituted) = multicircuit_monad[0]
1927
            .builder
1928
            .all_nodes
1929
            .borrow()
1930
            .iter()
1931
            .nth(substitution_node_index % num_nodes)
1932
            .map(|(i, n)| (*i, n.clone()))
1,000✔
1933
            .unwrap();
1934
        let mut j = 0;
1935
        while substituted.circuit.borrow().is_zero() && j < num_nodes {
1936
            (substitution_node_id, substituted) = multicircuit_monad[0]
1937
                .builder
1938
                .all_nodes
1939
                .borrow()
1940
                .iter()
1941
                .nth((substitution_node_index + j) % num_nodes)
NEW
1942
                .map(|(i, n)| (*i, n.clone()))
×
1943
                .unwrap();
1944
            j += 1;
1945
        }
1946
        prop_assert_ne!(j, num_nodes, "no suitable nodes to substitute");
1947

1948
        let degree_lowering_info = DegreeLoweringInfo {
1949
            target_degree: 2,
1950
            num_main_cols: main_input.len(),
1951
            num_aux_cols: aux_input.len(),
1952
        };
1953
        let substitution_constraint = ConstraintCircuitMonad::apply_substitution(
1954
            &mut multicircuit_monad,
1955
            degree_lowering_info,
1956
            substitution_node_id,
1957
            0,
1958
            0,
1959
        );
1960

1961
        // extract substituted constraint
1962
        let CircuitExpression::BinOp(BinOp::Add, variable, neg_expression) =
1963
            &substitution_constraint.circuit.as_ref().borrow().expression
1964
        else {
1965
            unreachable!();
1966
        };
1967
        let extra_input =
1968
            match &neg_expression.as_ref().borrow().expression {
1969
                CircuitExpression::BinOp(BinOp::Mul, _neg_one, circuit) => circuit
1970
                    .borrow()
1971
                    .evaluate(main_input.view(), aux_input.view(), &challenges),
1972
                CircuitExpression::BConst(c) => -c.lift(),
1973
                CircuitExpression::XConst(c) => -*c,
1974
                _ => {
1975
                    unreachable!()
1976
                }
1977
            };
1978
        if variable.borrow().evaluates_to_base_element() {
1979
            main_input
1980
                .append(
1981
                    Axis(1),
1982
                    Array2::from_shape_vec([1, 1], vec![extra_input.coefficients[0]])
1983
                        .unwrap()
1984
                        .view(),
1985
                )
1986
                .unwrap();
1987
        } else {
1988
            aux_input
1989
                .append(
1990
                    Axis(1),
1991
                    Array2::from_shape_vec([1, 1], vec![extra_input])
1992
                        .unwrap()
1993
                        .view(),
1994
                )
1995
                .unwrap();
1996
        }
1997

1998
        // evaluate again
1999
        let output_after_lowering = multicircuit_monad
2000
            .iter()
2001
            .map(|constraint| {
10,000✔
2002
                constraint.circuit.borrow().evaluate(
10,000✔
2003
                    main_input.view(),
10,000✔
2004
                    aux_input.view(),
10,000✔
2005
                    &challenges,
10,000✔
2006
                )
10,000✔
2007
            })
10,000✔
2008
            .collect_vec();
2009

2010
        // assert same value in original constraints
2011
        prop_assert_eq!(output_before_lowering, output_after_lowering);
2012

2013
        // assert zero in substitution constraint
2014
        prop_assert!(substitution_constraint
2015
            .circuit
2016
            .borrow()
2017
            .evaluate(main_input.view(), aux_input.view(), &challenges)
2018
            .is_zero());
2019
    }
2020
}
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