acir/native_types/expression/
mod.rs

1use crate::{circuit::PublicInputs, native_types::Witness};
2use acir_field::AcirField;
3use serde::{Deserialize, Serialize};
4use std::cmp::Ordering;
5mod operators;
6
7/// An expression representing a quadratic polynomial.
8///
9/// This struct is primarily used to express arithmetic relations between variables.
10/// It includes multiplication terms, linear combinations, and a constant term.
11///
12/// # Addition polynomial
13/// - Unlike standard plonk constraints with fixed wire assignments (wL, wR, wO),
14///   we allow arbitrary fan-in and fan-out. This means we need a more flexible representation
15///   and we need more than wL, wR, and wO.
16/// - When looking at the quotient polynomial for the assert-zero opcode in standard plonk,
17///   you can interpret the structure in two ways:
18///   1. Fan-in 2 and fan-out 1
19///   2. Fan-in 1 and fan-out 2
20///
21/// # Multiplication polynomial
22/// - If we were allow the degree of the quotient polynomial to be arbitrary, then we will need a vector of wire values.
23#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Hash)]
24#[cfg_attr(feature = "arb", derive(proptest_derive::Arbitrary))]
25pub struct Expression<F> {
26    /// Collection of multiplication terms.
27    ///
28    /// To avoid having to create intermediate variables pre-optimization
29    /// We collect all of the multiplication terms in the assert-zero opcode
30    /// A multiplication term is of the form q_M * wL * wR
31    /// Hence this vector represents the following sum: q_M1 * wL1 * wR1 + q_M2 * wL2 * wR2 + .. +
32    pub mul_terms: Vec<(F, Witness, Witness)>,
33
34    /// Collection of linear terms in the expression.
35    ///
36    /// Each term follows the form: `q_L * w`, where `q_L` is a coefficient
37    /// and `w` is a witness.
38    pub linear_combinations: Vec<(F, Witness)>,
39    /// A constant term in the expression
40    // TODO: rename q_c to `constant` moreover q_X is not clear to those who
41    // TODO: are not familiar with PLONK
42    pub q_c: F,
43}
44
45impl<F: AcirField> Default for Expression<F> {
46    fn default() -> Self {
47        Expression { mul_terms: Vec::new(), linear_combinations: Vec::new(), q_c: F::zero() }
48    }
49}
50
51impl<F: AcirField> std::fmt::Display for Expression<F> {
52    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
53        display_expression(self, false, None, f)
54    }
55}
56
57impl<F> Expression<F> {
58    /// Returns the number of multiplication terms
59    pub fn num_mul_terms(&self) -> usize {
60        self.mul_terms.len()
61    }
62
63    /// Adds a new linear term to the `Expression`.
64    pub fn push_addition_term(&mut self, coefficient: F, variable: Witness) {
65        self.linear_combinations.push((coefficient, variable));
66    }
67
68    /// Adds a new quadratic term to the `Expression`.
69    pub fn push_multiplication_term(&mut self, coefficient: F, lhs: Witness, rhs: Witness) {
70        self.mul_terms.push((coefficient, lhs, rhs));
71    }
72
73    /// Returns `true` if the expression represents a constant polynomial.
74    ///
75    /// Examples:
76    /// -  f(x,y) = x + y would return false
77    /// -  f(x,y) = xy would return false, the degree here is 2
78    /// -  f(x,y) = 5 would return true, the degree is 0
79    pub fn is_const(&self) -> bool {
80        self.mul_terms.is_empty() && self.linear_combinations.is_empty()
81    }
82
83    /// Returns a `FieldElement` if the expression represents a constant polynomial.
84    /// Otherwise returns `None`.
85    ///
86    /// Examples:
87    /// - f(x,y) = x would return `None`
88    /// - f(x,y) = x + 6 would return `None`
89    /// - f(x,y) = 2*y + 6 would return `None`
90    /// - f(x,y) = x + y would return `None`
91    /// - f(x,y) = 5 would return `FieldElement(5)`
92    pub fn to_const(&self) -> Option<&F> {
93        self.is_const().then_some(&self.q_c)
94    }
95
96    /// Returns `true` if highest degree term in the expression is one or less.
97    ///
98    /// - `mul_term` in an expression contains degree-2 terms
99    /// - `linear_combinations` contains degree-1 terms
100    ///
101    /// Hence, it is sufficient to check that there are no `mul_terms`
102    ///
103    /// Examples:
104    /// -  f(x,y) = x + y would return true
105    /// -  f(x,y) = xy would return false, the degree here is 2
106    /// -  f(x,y) = 0 would return true, the degree is 0
107    pub fn is_linear(&self) -> bool {
108        self.mul_terms.is_empty()
109    }
110
111    /// Returns `true` if the expression can be seen as a degree-1 univariate polynomial
112    ///
113    /// - `mul_terms` in an expression can be univariate, however unless the coefficient
114    ///   is zero, it is always degree-2.
115    /// - `linear_combinations` contains the sum of degree-1 terms, these terms do not
116    ///   need to contain the same variable and so it can be multivariate. However, we
117    ///   have thus far only checked if `linear_combinations` contains one term, so this
118    ///   method will return false, if the `Expression` has not been simplified.
119    ///
120    /// Hence, we check in the simplest case if an expression is a degree-1 univariate,
121    /// by checking if it contains no `mul_terms` and it contains one `linear_combination` term.
122    ///
123    /// Examples:
124    /// - f(x,y) = x would return true
125    /// - f(x,y) = x + 6 would return true
126    /// - f(x,y) = 2*y + 6 would return true
127    /// - f(x,y) = x + y would return false
128    /// - f(x,y) = x + x should return true, but we return false *** (we do not simplify)
129    /// - f(x,y) = 5 would return false
130    pub fn is_degree_one_univariate(&self) -> bool {
131        self.is_linear() && self.linear_combinations.len() == 1
132    }
133
134    /// Sorts opcode in a deterministic order
135    /// XXX: We can probably make this more efficient by sorting on each phase. We only care if it is deterministic
136    pub fn sort(&mut self) {
137        self.mul_terms.sort_by(|a, b| a.1.cmp(&b.1).then(a.2.cmp(&b.2)));
138        self.linear_combinations.sort_by(|a, b| a.1.cmp(&b.1));
139    }
140
141    #[cfg(test)]
142    pub(crate) fn is_sorted(&self) -> bool {
143        self.mul_terms.iter().is_sorted_by(|a, b| a.1.cmp(&b.1).then(a.2.cmp(&b.2)).is_le())
144            && self.linear_combinations.iter().is_sorted_by(|a, b| a.1.cmp(&b.1).is_le())
145    }
146}
147
148impl<F: AcirField> Expression<F> {
149    pub fn from_field(q_c: F) -> Self {
150        Self { q_c, ..Default::default() }
151    }
152
153    pub fn zero() -> Self {
154        Self::default()
155    }
156
157    pub fn is_zero(&self) -> bool {
158        *self == Self::zero()
159    }
160
161    pub fn one() -> Self {
162        Self::from_field(F::one())
163    }
164
165    pub fn is_one(&self) -> bool {
166        *self == Self::one()
167    }
168
169    /// Returns a `Witness` if the `Expression` can be represented as a degree-1
170    /// univariate polynomial. Otherwise returns `None`.
171    ///
172    /// Note that `Witness` is only capable of expressing polynomials of the form
173    /// f(x) = x and not polynomials of the form f(x) = mx+c , so this method has
174    /// extra checks to ensure that m=1 and c=0
175    pub fn to_witness(&self) -> Option<Witness> {
176        if self.is_degree_one_univariate() {
177            // If we get here, we know that our expression is of the form `f(x) = mx+c`
178            // We want to now restrict ourselves to expressions of the form f(x) = x
179            // ie where the constant term is 0 and the coefficient in front of the variable is
180            // one.
181            let (coefficient, variable) = self.linear_combinations[0];
182            let constant = self.q_c;
183
184            if coefficient.is_one() && constant.is_zero() {
185                return Some(variable);
186            }
187        }
188        None
189    }
190
191    /// Returns `self + k*b`
192    pub fn add_mul(&self, k: F, b: &Self) -> Self {
193        if k.is_zero() {
194            return self.clone();
195        } else if self.is_const() {
196            let kb = b * k;
197            return kb + self.q_c;
198        } else if b.is_const() {
199            return self.clone() + (k * b.q_c);
200        }
201
202        let mut mul_terms: Vec<(F, Witness, Witness)> =
203            Vec::with_capacity(self.mul_terms.len() + b.mul_terms.len());
204        let mut linear_combinations: Vec<(F, Witness)> =
205            Vec::with_capacity(self.linear_combinations.len() + b.linear_combinations.len());
206        let q_c = self.q_c + k * b.q_c;
207
208        //linear combinations
209        let mut i1 = 0; //a
210        let mut i2 = 0; //b
211        while i1 < self.linear_combinations.len() && i2 < b.linear_combinations.len() {
212            let (a_c, a_w) = self.linear_combinations[i1];
213            let (b_c, b_w) = b.linear_combinations[i2];
214
215            let (coeff, witness) = match a_w.cmp(&b_w) {
216                Ordering::Greater => {
217                    i2 += 1;
218                    (k * b_c, b_w)
219                }
220                Ordering::Less => {
221                    i1 += 1;
222                    (a_c, a_w)
223                }
224                Ordering::Equal => {
225                    // Here we're taking both witnesses as the witness indices are equal.
226                    // We then advance both `i1` and `i2`.
227                    i1 += 1;
228                    i2 += 1;
229                    (a_c + k * b_c, a_w)
230                }
231            };
232
233            if !coeff.is_zero() {
234                linear_combinations.push((coeff, witness));
235            }
236        }
237
238        // Finally process all the remaining terms which we didn't handle in the above loop.
239        while i1 < self.linear_combinations.len() {
240            linear_combinations.push(self.linear_combinations[i1]);
241            i1 += 1;
242        }
243        while i2 < b.linear_combinations.len() {
244            let (b_c, b_w) = b.linear_combinations[i2];
245            let coeff = b_c * k;
246            if !coeff.is_zero() {
247                linear_combinations.push((coeff, b_w));
248            }
249            i2 += 1;
250        }
251
252        //mul terms
253
254        i1 = 0; //a
255        i2 = 0; //b
256        while i1 < self.mul_terms.len() && i2 < b.mul_terms.len() {
257            let (a_c, a_wl, a_wr) = self.mul_terms[i1];
258            let (b_c, b_wl, b_wr) = b.mul_terms[i2];
259
260            let (coeff, wl, wr) = match (a_wl, a_wr).cmp(&(b_wl, b_wr)) {
261                Ordering::Greater => {
262                    i2 += 1;
263                    (k * b_c, b_wl, b_wr)
264                }
265                Ordering::Less => {
266                    i1 += 1;
267                    (a_c, a_wl, a_wr)
268                }
269                Ordering::Equal => {
270                    // Here we're taking both terms as the witness indices are equal.
271                    // We then advance both `i1` and `i2`.
272                    i2 += 1;
273                    i1 += 1;
274                    (a_c + k * b_c, a_wl, a_wr)
275                }
276            };
277
278            if !coeff.is_zero() {
279                mul_terms.push((coeff, wl, wr));
280            }
281        }
282
283        // Finally process all the remaining terms which we didn't handle in the above loop.
284        while i1 < self.mul_terms.len() {
285            mul_terms.push(self.mul_terms[i1]);
286            i1 += 1;
287        }
288        while i2 < b.mul_terms.len() {
289            let (b_c, b_wl, b_wr) = b.mul_terms[i2];
290            let coeff = b_c * k;
291            if coeff != F::zero() {
292                mul_terms.push((coeff, b_wl, b_wr));
293            }
294            i2 += 1;
295        }
296
297        Expression { mul_terms, linear_combinations, q_c }
298    }
299
300    /// Determine the width of this expression.
301    /// The width meaning the number of unique witnesses needed for this expression.
302    pub fn width(&self) -> usize {
303        let mut width = 0;
304
305        for mul_term in &self.mul_terms {
306            // The coefficient should be non-zero, as this method is ran after the compiler removes all zero coefficient terms
307            assert_ne!(mul_term.0, F::zero());
308
309            let mut found_x = false;
310            let mut found_y = false;
311
312            for term in &self.linear_combinations {
313                let witness = &term.1;
314                let x = &mul_term.1;
315                let y = &mul_term.2;
316                if witness == x {
317                    found_x = true;
318                }
319                if witness == y {
320                    found_y = true;
321                }
322                if found_x & found_y {
323                    break;
324                }
325            }
326
327            // If the multiplication is a squaring then we must assign the two witnesses to separate wires and so we
328            // can never get a zero contribution to the width.
329            let multiplication_is_squaring = mul_term.1 == mul_term.2;
330
331            let mul_term_width_contribution = if !multiplication_is_squaring && (found_x & found_y)
332            {
333                // Both witnesses involved in the multiplication exist elsewhere in the expression.
334                // They both do not contribute to the width of the expression as this would be double-counting
335                // due to their appearance in the linear terms.
336                0
337            } else if found_x || found_y {
338                // One of the witnesses involved in the multiplication exists elsewhere in the expression.
339                // The multiplication then only contributes 1 new witness to the width.
340                1
341            } else {
342                // Worst case scenario, the multiplication is using completely unique witnesses so has a contribution of 2.
343                2
344            };
345
346            width += mul_term_width_contribution;
347        }
348
349        width += self.linear_combinations.len();
350
351        width
352    }
353}
354
355impl<F: AcirField> From<F> for Expression<F> {
356    fn from(constant: F) -> Self {
357        Expression { q_c: constant, linear_combinations: Vec::new(), mul_terms: Vec::new() }
358    }
359}
360
361impl<F: AcirField> From<Witness> for Expression<F> {
362    /// Creates an Expression from a Witness.
363    ///
364    /// This is infallible since an `Expression` is
365    /// a multi-variate polynomial and a `Witness`
366    /// can be seen as a univariate polynomial
367    fn from(wit: Witness) -> Self {
368        Expression {
369            q_c: F::zero(),
370            linear_combinations: vec![(F::one(), wit)],
371            mul_terms: Vec::new(),
372        }
373    }
374}
375
376/// Displays an expression as a quadratic polynomial.
377/// If `as_equal_to_zero` is true, the expression is displayed as equaling zero,
378/// where it's tried to shown as a polynomial equal to the largest witness, if possible.
379/// If the optional `return_values` is provided, the expression is displayed preferring to show
380/// `ASSERT return_value = ...` when possible.
381pub(crate) fn display_expression<F: AcirField>(
382    expr: &Expression<F>,
383    as_equal_to_zero: bool,
384    return_values: Option<&PublicInputs>,
385    f: &mut std::fmt::Formatter<'_>,
386) -> std::fmt::Result {
387    // This is set to an index if we show this expression "as a witness assignment", meaning
388    // that the linear combination at this index must not be printed again.
389    let mut assignment_witness: Option<usize> = None;
390
391    // If true, negate all coefficients when printing.
392    // This is set to true if we show this expression "as a witness assignment", and the witness
393    // had a coefficient of 1 and we "moved" everything to the right of the equal sign.
394    let mut negate_coefficients = false;
395
396    // Find a linear combination with a coefficient of 1 or -1 and, if there are many,
397    // keep the one with the largest witness.
398    let linear_witness_one = if as_equal_to_zero {
399        // Prefer equating to a return value if possible
400        let linear_witness_one = return_values.and_then(|return_values| {
401            expr.linear_combinations.iter().enumerate().find(|(_, (coefficient, witness))| {
402                (coefficient.is_one() || (-*coefficient).is_one())
403                    && return_values.0.contains(witness)
404            })
405        });
406        linear_witness_one.or_else(|| {
407            // Otherwise just pick the largest witness
408            expr.linear_combinations
409                .iter()
410                .enumerate()
411                .filter(|(_, (coefficient, _))| coefficient.is_one() || (-*coefficient).is_one())
412                .max_by_key(|(_, (_, witness))| witness)
413        })
414    } else {
415        None
416    };
417
418    // If we find one, show the expression as equaling this witness to everything else
419    // (this is likely to happen as in ACIR gen we tend to equate a witness to previous expressions)
420    if let Some((index, (coefficient, witness))) = linear_witness_one {
421        assignment_witness = Some(index);
422        negate_coefficients = coefficient.is_one();
423        write!(f, "{witness} = ")?;
424    } else if as_equal_to_zero {
425        write!(f, "0 = ")?;
426    }
427
428    let mut printed_term = false;
429
430    for (coefficient, witness1, witness2) in &expr.mul_terms {
431        let witnesses = [*witness1, *witness2];
432        display_term(*coefficient, witnesses, printed_term, negate_coefficients, f)?;
433        printed_term = true;
434    }
435
436    for (index, (coefficient, witness)) in expr.linear_combinations.iter().enumerate() {
437        if assignment_witness
438            .is_some_and(|show_as_assignment_index| show_as_assignment_index == index)
439        {
440            // We already printed this term as part of the assignment
441            continue;
442        }
443
444        let witnesses = [*witness];
445        display_term(*coefficient, witnesses, printed_term, negate_coefficients, f)?;
446        printed_term = true;
447    }
448
449    if expr.q_c.is_zero() {
450        if !printed_term {
451            write!(f, "0")?;
452        }
453    } else {
454        let coefficient = expr.q_c;
455        let coefficient = if negate_coefficients { -coefficient } else { coefficient };
456        let coefficient_as_string = coefficient.to_string();
457        let coefficient_is_negative = coefficient_as_string.starts_with('-');
458
459        if printed_term {
460            if coefficient_is_negative {
461                write!(f, " - ")?;
462            } else {
463                write!(f, " + ")?;
464            }
465        }
466
467        let coefficient =
468            if printed_term && coefficient_is_negative { -coefficient } else { coefficient };
469        write!(f, "{coefficient}")?;
470    }
471
472    Ok(())
473}
474
475fn display_term<F: AcirField, const N: usize>(
476    coefficient: F,
477    witnesses: [Witness; N],
478    printed_term: bool,
479    negate_coefficients: bool,
480    f: &mut std::fmt::Formatter<'_>,
481) -> std::fmt::Result {
482    let coefficient = if negate_coefficients { -coefficient } else { coefficient };
483    let coefficient_as_string = coefficient.to_string();
484    let coefficient_is_negative = coefficient_as_string.starts_with('-');
485
486    if printed_term {
487        if coefficient_is_negative {
488            write!(f, " - ")?;
489        } else {
490            write!(f, " + ")?;
491        }
492    }
493
494    let coefficient =
495        if printed_term && coefficient_is_negative { -coefficient } else { coefficient };
496
497    if coefficient.is_one() {
498        // Don't print the coefficient
499    } else if (-coefficient).is_one() {
500        write!(f, "-")?;
501    } else {
502        write!(f, "{coefficient}*")?;
503    }
504
505    for (index, witness) in witnesses.iter().enumerate() {
506        if index != 0 {
507            write!(f, "*")?;
508        }
509        write!(f, "{witness}")?;
510    }
511
512    Ok(())
513}
514
515#[cfg(test)]
516mod tests {
517    use super::*;
518    use acir_field::FieldElement;
519
520    #[test]
521    fn add_mul_smoke_test() {
522        let a = Expression::from_str("2*w1*w2").unwrap();
523
524        let k = FieldElement::from(10u128);
525        let b = Expression::from_str("3*w0*w2 + 3*w1*w2 + 4*w4*w5 + 4*w4 + 1").unwrap();
526
527        let result = a.add_mul(k, &b);
528        assert_eq!(result.to_string(), "30*w0*w2 + 32*w1*w2 + 40*w4*w5 + 40*w4 + 10");
529    }
530
531    #[test]
532    fn add_mul_with_zero_coefficient() {
533        // When k=0, should return a clone of self
534        let a = Expression::from_str("2*w1*w2 + 3*w1 + 5").unwrap();
535        let b = Expression::from_str("4*w2*w3 + 6*w2 + 7").unwrap();
536        let k = FieldElement::zero();
537
538        let result = a.add_mul(k, &b);
539        assert_eq!(result, a);
540    }
541
542    #[test]
543    fn add_mul_when_self_is_const() {
544        // When self is a constant, should return k*b + constant
545        let a = Expression::from_field(FieldElement::from(5u128));
546        let b = Expression::from_str("2*w1*w2 + 3*w1 + 4").unwrap();
547        let k = FieldElement::from(2u128);
548
549        let result = a.add_mul(k, &b);
550        assert_eq!(result.to_string(), "4*w1*w2 + 6*w1 + 13");
551    }
552
553    #[test]
554    fn add_mul_when_b_is_const() {
555        // When b is a constant, should return self + k*constant
556        let a = Expression::from_str("2*w1*w2 + 3*w1 + 4").unwrap();
557        let b = Expression::from_field(FieldElement::from(5u128));
558        let k = FieldElement::from(3u128);
559
560        let result = a.add_mul(k, &b);
561        assert_eq!(result.to_string(), "2*w1*w2 + 3*w1 + 19");
562    }
563
564    #[test]
565    fn add_mul_merges_linear_terms() {
566        // Test that linear terms with same witness are merged correctly
567        let a = Expression::from_str("5*w1 + 3*w2").unwrap();
568        let b = Expression::from_str("2*w1 + 4*w3").unwrap();
569        let k = FieldElement::from(2u128);
570
571        let result = a.add_mul(k, &b);
572        // 5*w1 + 3*w2 + 2*(2*w1 + 4*w3) = 5*w1 + 3*w2 + 4*w1 + 8*w3 = 9*w1 + 3*w2 + 8*w3
573        assert_eq!(result.to_string(), "9*w1 + 3*w2 + 8*w3");
574    }
575
576    #[test]
577    fn add_mul_merges_mul_terms() {
578        // Test that multiplication terms with same witness pair are merged correctly
579        let a = Expression::from_str("5*w1*w2 + 3*w3*w4").unwrap();
580        let b = Expression::from_str("2*w1*w2 + 4*w5*w6").unwrap();
581        let k = FieldElement::from(3u128);
582
583        let result = a.add_mul(k, &b);
584        // 5*w1*w2 + 3*w3*w4 + 3*(2*w1*w2 + 4*w5*w6) = 5*w1*w2 + 3*w3*w4 + 6*w1*w2 + 12*w5*w6
585        // = 11*w1*w2 + 3*w3*w4 + 12*w5*w6
586        assert_eq!(result.to_string(), "11*w1*w2 + 3*w3*w4 + 12*w5*w6");
587    }
588
589    #[test]
590    fn add_mul_cancels_terms_to_zero() {
591        // Test that terms that cancel out are removed
592        let a = Expression::from_str("6*w1 + 3*w1*w2").unwrap();
593        let b = Expression::from_str("3*w1 + w1*w2").unwrap();
594        let k = FieldElement::from(-2i128);
595
596        let result = a.add_mul(k, &b);
597        // 6*w1 + 3*w1*w2 + (-2)*(3*w1 + w1*w2) = 6*w1 + 3*w1*w2 - 6*w1 - 2*w1*w2
598        // = w1*w2
599        assert_eq!(result.to_string(), "w1*w2");
600    }
601
602    #[test]
603    fn add_mul_maintains_sorted_order() {
604        // Test that the result maintains sorted order for deterministic output
605        let a = Expression::from_str("w5 + w1*w3").unwrap();
606        let b = Expression::from_str("w2 + w0*w1").unwrap();
607        let k = FieldElement::one();
608
609        let result = a.add_mul(k, &b);
610        // Result should have terms in sorted order
611        assert!(result.is_sorted());
612        assert_eq!(result.to_string(), "w0*w1 + w1*w3 + w2 + w5");
613    }
614
615    #[test]
616    fn add_mul_with_constant_terms() {
617        // Test handling of constant terms
618        let a = Expression::from_str("2*w1 + 10").unwrap();
619        let b = Expression::from_str("3*w2 + 5").unwrap();
620        let k = FieldElement::from(4u128);
621
622        let result = a.add_mul(k, &b);
623        // 2*w1 + 10 + 4*(3*w2 + 5) = 2*w1 + 10 + 12*w2 + 20 = 2*w1 + 12*w2 + 30
624        assert_eq!(result.to_string(), "2*w1 + 12*w2 + 30");
625    }
626
627    #[test]
628    fn add_mul_complex_expression() {
629        // Test a complex expression with all types of terms
630        let a = Expression::from_str("2*w1*w2 + 3*w3*w4 + 5*w1 + 7*w3 + 11").unwrap();
631        let b = Expression::from_str("w1*w2 + 4*w5*w6 + 2*w1 + 6*w5 + 13").unwrap();
632        let k = FieldElement::from(2u128);
633
634        let result = a.add_mul(k, &b);
635        // 2*w1*w2 + 3*w3*w4 + 5*w1 + 7*w3 + 11 + 2*(w1*w2 + 4*w5*w6 + 2*w1 + 6*w5 + 13)
636        // = 2*w1*w2 + 3*w3*w4 + 5*w1 + 7*w3 + 11 + 2*w1*w2 + 8*w5*w6 + 4*w1 + 12*w5 + 26
637        // = 4*w1*w2 + 3*w3*w4 + 8*w5*w6 + 9*w1 + 7*w3 + 12*w5 + 37
638        assert_eq!(result.to_string(), "4*w1*w2 + 3*w3*w4 + 8*w5*w6 + 9*w1 + 7*w3 + 12*w5 + 37");
639    }
640
641    #[test]
642    fn display_zero() {
643        let zero = Expression::<FieldElement>::default();
644        assert_eq!(zero.to_string(), "0");
645    }
646}