acir/native_types/expression/
mod.rs

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