acvm/pwg/
arithmetic.rs

1use acir::{
2    AcirField,
3    native_types::{Expression, Witness, WitnessMap},
4};
5
6use super::{ErrorLocation, OpcodeNotSolvable, OpcodeResolutionError, insert_value};
7
8/// An Expression solver will take a Circuit's assert-zero opcodes with witness assignments
9/// and create the other witness variables
10pub(crate) struct ExpressionSolver;
11
12#[allow(clippy::enum_variant_names)]
13pub(super) enum OpcodeStatus<F> {
14    OpcodeSatisfied(F),
15    OpcodeSolvable(F, (F, Witness)),
16    OpcodeUnsolvable,
17}
18
19pub(crate) enum MulTerm<F> {
20    OneUnknown(F, Witness), // (qM * known_witness, unknown_witness)
21    TooManyUnknowns,
22    Solved(F),
23}
24
25impl ExpressionSolver {
26    /// Derives the rest of the witness based on known witness values in a single pass.
27    ///
28    /// For expressions with 0 or 1 multiplication terms (the common case), this avoids
29    /// allocating an intermediate `Expression` and eliminates redundant witness map lookups.
30    /// Falls back to the general evaluate-based approach for 2+ mul terms or when
31    /// linear term combining is needed.
32    pub(crate) fn solve<F: AcirField>(
33        initial_witness: &mut WitnessMap<F>,
34        opcode: &Expression<F>,
35    ) -> Result<(), OpcodeResolutionError<F>> {
36        // Evaluate the multiplication term contribution.
37        // Most expressions have 0 mul terms; at most 1 is solvable without combining.
38        let (mul_constant, mut unknown) = match opcode.mul_terms.len() {
39            0 => (F::zero(), None),
40            1 => {
41                match Self::solve_mul_term_helper(&opcode.mul_terms[0], initial_witness) {
42                    MulTerm::Solved(val) => (val, None),
43                    MulTerm::OneUnknown(coeff, witness) => {
44                        let unknown = if coeff.is_zero() { None } else { Some((coeff, witness)) };
45                        (F::zero(), unknown)
46                    }
47                    MulTerm::TooManyUnknowns => {
48                        let (c, _, _) = opcode.mul_terms[0];
49                        if c.is_zero() {
50                            // Zero-coefficient mul term contributes nothing.
51                            (F::zero(), None)
52                        } else {
53                            // Both witnesses unknown — always unsolvable for a single mul term.
54                            return Err(OpcodeResolutionError::OpcodeNotSolvable(
55                                OpcodeNotSolvable::ExpressionHasTooManyUnknowns(opcode.clone()),
56                            ));
57                        }
58                    }
59                }
60            }
61            // 2+ mul terms may cancel via combining; use the general solver.
62            _ => return Self::solve_via_evaluate(initial_witness, opcode),
63        };
64
65        // Single pass over all linear terms (original + extra from partially-evaluated mul).
66        let mut sum = opcode.q_c + mul_constant;
67
68        for &(coeff, witness) in &opcode.linear_combinations {
69            if let Some(value) = initial_witness.get(&witness) {
70                sum += coeff * *value;
71            } else if !coeff.is_zero() {
72                if unknown.is_some() {
73                    // Multiple unknowns — need to try combining duplicate witnesses.
74                    return Self::solve_via_evaluate(initial_witness, opcode);
75                }
76                unknown = Some((coeff, witness));
77            }
78        }
79
80        if let Some((coeff, witness)) = unknown {
81            Self::solve_single_unknown(sum, coeff, witness, initial_witness)
82        } else {
83            Self::verify_satisfied(sum)
84        }
85    }
86
87    /// Verify that the fully-evaluated expression equals zero.
88    fn verify_satisfied<F: AcirField>(sum: F) -> Result<(), OpcodeResolutionError<F>> {
89        if sum.is_zero() {
90            Ok(())
91        } else {
92            Err(OpcodeResolutionError::UnsatisfiedConstrain {
93                opcode_location: ErrorLocation::Unresolved,
94                payload: None,
95            })
96        }
97    }
98
99    /// Solve `sum + coeff * witness = 0` for the witness.
100    fn solve_single_unknown<F: AcirField>(
101        sum: F,
102        coeff: F,
103        witness: Witness,
104        initial_witness: &mut WitnessMap<F>,
105    ) -> Result<(), OpcodeResolutionError<F>> {
106        if coeff.is_zero() {
107            Self::verify_satisfied(sum)
108        } else {
109            let assignment = -quick_invert(sum, coeff);
110            insert_value(&witness, assignment, initial_witness)
111        }
112    }
113
114    /// General solver that allocates an intermediate evaluated `Expression`.
115    /// Used as a fallback when the single-pass approach cannot handle the expression
116    /// (2+ mul terms, or linear terms that need combining).
117    fn solve_via_evaluate<F: AcirField>(
118        initial_witness: &mut WitnessMap<F>,
119        opcode: &Expression<F>,
120    ) -> Result<(), OpcodeResolutionError<F>> {
121        let opcode = &ExpressionSolver::evaluate(opcode, initial_witness);
122
123        // Evaluate multiplication terms
124        let mul_result = ExpressionSolver::mul_term_status(&opcode.mul_terms);
125
126        // If we can't solve the multiplication terms, try again by combining multiplication terms
127        // with the same witnesses to see if they all cancel out.
128        let mul_result = if mul_result.is_err() {
129            let mul_terms = ExpressionSolver::combine_mul_terms(&opcode.mul_terms);
130            ExpressionSolver::mul_term_status(&mul_terms)
131        } else {
132            mul_result
133        };
134
135        let mul_result = mul_result.map_err(|_| {
136            OpcodeResolutionError::OpcodeNotSolvable(
137                OpcodeNotSolvable::ExpressionHasTooManyUnknowns(opcode.clone()),
138            )
139        })?;
140
141        // Evaluate the fan-in terms
142        let opcode_status = ExpressionSolver::fan_in_status(&opcode.linear_combinations);
143
144        // If we can solve the multiplication terms but not the linear terms,
145        // try again by combining linear terms with the same witness.
146        let opcode_status = if matches!(
147            (&mul_result, &opcode_status),
148            (MulTerm::Solved(..), OpcodeStatus::OpcodeUnsolvable)
149        ) {
150            let linear_combinations =
151                ExpressionSolver::combine_linear_terms(&opcode.linear_combinations);
152            ExpressionSolver::fan_in_status(&linear_combinations)
153        } else {
154            opcode_status
155        };
156
157        match (mul_result, opcode_status) {
158            // Mul terms solved, one unknown in linear terms.
159            (
160                MulTerm::Solved(total_prod),
161                OpcodeStatus::OpcodeSolvable(partial_sum, (coeff, witness)),
162            ) => Self::solve_single_unknown(
163                total_prod + partial_sum + opcode.q_c,
164                coeff,
165                witness,
166                initial_witness,
167            ),
168            // Everything solved — just verify the constraint holds.
169            (MulTerm::Solved(a), OpcodeStatus::OpcodeSatisfied(b)) => {
170                Self::verify_satisfied(a + b + opcode.q_c)
171            }
172            // One unknown in the mul term, linear terms fully solved.
173            (MulTerm::OneUnknown(coeff, witness), OpcodeStatus::OpcodeSatisfied(sum)) => {
174                Self::solve_single_unknown(sum + opcode.q_c, coeff, witness, initial_witness)
175            }
176            // One unknown appears in both mul and linear terms for the same witness.
177            // Combine coefficients: solve (q + b) * w = -(a + q_c)
178            (MulTerm::OneUnknown(q, w1), OpcodeStatus::OpcodeSolvable(a, (b, w2))) => {
179                if w1 == w2 {
180                    Self::solve_single_unknown(a + opcode.q_c, q + b, w1, initial_witness)
181                } else {
182                    // TODO(https://github.com/noir-lang/noir/issues/10191): can we be more specific with this error?
183                    Err(OpcodeResolutionError::OpcodeNotSolvable(
184                        OpcodeNotSolvable::ExpressionHasTooManyUnknowns(opcode.clone()),
185                    ))
186                }
187            }
188            (MulTerm::TooManyUnknowns, _) | (_, OpcodeStatus::OpcodeUnsolvable) => {
189                Err(OpcodeResolutionError::OpcodeNotSolvable(
190                    OpcodeNotSolvable::ExpressionHasTooManyUnknowns(opcode.clone()),
191                ))
192            }
193        }
194    }
195
196    /// Indicates the 'solved' status of the mul term, after partial evaluation.
197    fn mul_term_status<F: AcirField>(
198        mul_terms: &[(F, Witness, Witness)],
199    ) -> Result<MulTerm<F>, OpcodeStatus<F>> {
200        match mul_terms.len() {
201            0 => Ok(MulTerm::Solved(F::zero())),
202            1 => Ok(MulTerm::TooManyUnknowns),
203            _ => Err(OpcodeStatus::OpcodeUnsolvable),
204        }
205    }
206
207    /// Try to solve a multiplication term of the form q*a*b, where
208    /// q is a constant and a,b are witnesses
209    /// If both a and b have known values (in the provided map), it returns the value q*a*b
210    /// If only one of a or b has a known value, it returns the linear term c*w where c is a constant and w is the unknown witness
211    /// If both a and b are unknown, it returns `MulTerm::TooManyUnknowns`
212    fn solve_mul_term_helper<F: AcirField>(
213        term: &(F, Witness, Witness),
214        witness_assignments: &WitnessMap<F>,
215    ) -> MulTerm<F> {
216        let (q_m, w_l, w_r) = term;
217        // Check if these values are in the witness assignments
218        let w_l_value = witness_assignments.get(w_l);
219        let w_r_value = witness_assignments.get(w_r);
220
221        match (w_l_value, w_r_value) {
222            (None, None) => MulTerm::TooManyUnknowns,
223            (Some(w_l), Some(w_r)) => MulTerm::Solved(*q_m * *w_l * *w_r),
224            (None, Some(w_r)) => MulTerm::OneUnknown(*q_m * *w_r, *w_l),
225            (Some(w_l), None) => MulTerm::OneUnknown(*q_m * *w_l, *w_r),
226        }
227    }
228
229    /// Reduce a linear term to its value if the witness assignment is known
230    /// If the witness value is not known in the provided map, it returns None.
231    fn solve_fan_in_term_helper<F: AcirField>(
232        term: &(F, Witness),
233        witness_assignments: &WitnessMap<F>,
234    ) -> Option<F> {
235        let (q_l, w_l) = term;
236        // Check if we have w_l
237        let w_l_value = witness_assignments.get(w_l);
238        w_l_value.map(|a| *q_l * *a)
239    }
240
241    /// Indicate the 'solved' status of the linear terms after partial evaluation.
242    pub(super) fn fan_in_status<F: AcirField>(
243        linear_combinations: &[(F, Witness)],
244    ) -> OpcodeStatus<F> {
245        match linear_combinations.len() {
246            0 => OpcodeStatus::OpcodeSatisfied(F::zero()),
247            1 => OpcodeStatus::OpcodeSolvable(F::zero(), linear_combinations[0]),
248            _ => OpcodeStatus::OpcodeUnsolvable,
249        }
250    }
251
252    // Partially evaluate the opcode using the known witnesses
253    // For instance if values of witness 'a' and 'b' are known, then
254    // the multiplication 'a*b' is removed and their multiplied values are added to the constant term
255    // If only witness 'a' is known, then the multiplication 'a*b' is replaced by the linear term '(value of b)*a'
256    // etc ...
257    // If all values are known, the partial evaluation gives a constant expression
258    // If no value is known, the partial evaluation returns the original expression
259    pub(crate) fn evaluate<F: AcirField>(
260        expr: &Expression<F>,
261        initial_witness: &WitnessMap<F>,
262    ) -> Expression<F> {
263        let mut result = Expression::default();
264        for &(c, w1, w2) in &expr.mul_terms {
265            let mul_result = ExpressionSolver::solve_mul_term_helper(&(c, w1, w2), initial_witness);
266            match mul_result {
267                MulTerm::OneUnknown(v, w) => {
268                    if !v.is_zero() {
269                        result.linear_combinations.push((v, w));
270                    }
271                }
272                MulTerm::TooManyUnknowns => {
273                    if !c.is_zero() {
274                        result.mul_terms.push((c, w1, w2));
275                    }
276                }
277                MulTerm::Solved(f) => result.q_c += f,
278            }
279        }
280        for &(c, w) in &expr.linear_combinations {
281            if let Some(f) = ExpressionSolver::solve_fan_in_term_helper(&(c, w), initial_witness) {
282                result.q_c += f;
283            } else if !c.is_zero() {
284                result.linear_combinations.push((c, w));
285            }
286        }
287        result.q_c += expr.q_c;
288        result
289    }
290
291    /// Combines linear terms with the same witness by summing their coefficients.
292    /// For example `w1 + 2*w1` becomes `3*w1`.
293    pub(crate) fn combine_linear_terms<F: AcirField>(
294        linear_combinations: &[(F, Witness)],
295    ) -> Vec<(F, Witness)> {
296        let mut combined_linear_combinations = std::collections::HashMap::new();
297
298        for (c, w) in linear_combinations {
299            let existing_c = combined_linear_combinations.entry(*w).or_insert(F::zero());
300            *existing_c += *c;
301        }
302
303        combined_linear_combinations
304            .into_iter()
305            .filter_map(
306                |(witness, coeff)| {
307                    if !coeff.is_zero() { Some((coeff, witness)) } else { None }
308                },
309            )
310            .collect()
311    }
312
313    /// Combines multiplication terms with the same witnesses by summing their coefficients.
314    /// For example `w1*w2 + 2*w2*w1` becomes `3*w1*w2`. If a coefficient ends up being zero,
315    /// the term is removed.
316    pub(crate) fn combine_mul_terms<F: AcirField>(
317        mul_terms: &[(F, Witness, Witness)],
318    ) -> Vec<(F, Witness, Witness)> {
319        // This is similar to GeneralOptimizer::simplify_mul_terms but it's duplicated because
320        // we don't have access to the acvm crate here.
321        let mut hash_map = std::collections::HashMap::new();
322
323        // Canonicalize the ordering of the multiplication, lets just order by variable name
324        for (scale, w_l, w_r) in mul_terms.iter().copied() {
325            let mut pair = [w_l, w_r];
326            pair.sort();
327
328            *hash_map.entry((pair[0], pair[1])).or_insert_with(F::zero) += scale;
329        }
330
331        hash_map
332            .into_iter()
333            .filter(|(_, scale)| !scale.is_zero())
334            .map(|((w_l, w_r), scale)| (scale, w_l, w_r))
335            .collect()
336    }
337}
338
339/// A wrapper around field division which skips the inversion if the denominator
340/// is ±1.
341///
342/// Field inversion is the most significant cost of solving [`Opcode::AssertZero`][acir::circuit::opcodes::Opcode::AssertZero]
343/// opcodes, which we can avoid when the denominator is ±1.
344fn quick_invert<F: AcirField>(numerator: F, denominator: F) -> F {
345    if denominator == F::one() {
346        numerator
347    } else if denominator == -F::one() {
348        -numerator
349    } else {
350        assert!(
351            denominator != F::zero(),
352            "quick_invert: attempting to divide numerator by F::zero()"
353        );
354        numerator / denominator
355    }
356}
357
358#[cfg(test)]
359mod tests {
360    use super::*;
361    use acir::FieldElement;
362
363    #[test]
364    /// Sanity check for the special cases of [`quick_invert`]
365    fn quick_invert_matches_slow_invert() {
366        let numerator = FieldElement::from_be_bytes_reduce("hello_world".as_bytes());
367        assert_eq!(quick_invert(numerator, FieldElement::one()), numerator / FieldElement::one());
368        assert_eq!(quick_invert(numerator, -FieldElement::one()), numerator / -FieldElement::one());
369    }
370
371    #[test]
372    #[should_panic(expected = "quick_invert: attempting to divide numerator by F::zero()")]
373    fn quick_invert_zero_denominator() {
374        quick_invert(FieldElement::one(), FieldElement::zero());
375    }
376
377    #[test]
378    fn solves_simple_assignment() {
379        let a = Witness(0);
380
381        // a - 1 == 0;
382        let opcode_a = Expression::from_str(&format!("{a} - 1")).unwrap();
383
384        let mut values = WitnessMap::new();
385        assert_eq!(ExpressionSolver::solve(&mut values, &opcode_a), Ok(()));
386
387        assert_eq!(values.get(&a).unwrap(), &FieldElement::from(1_i128));
388    }
389
390    #[test]
391    fn solves_unknown_in_mul_term() {
392        let a = Witness(0);
393        let b = Witness(1);
394        let c = Witness(2);
395        let d = Witness(3);
396
397        // a * b - b - c - d == 0;
398        let opcode_a = Expression::from_str(&format!("{a}*{b} - {b} - {c} - {d}")).unwrap();
399
400        let mut values = WitnessMap::new();
401        values.insert(b, FieldElement::from(2_i128));
402        values.insert(c, FieldElement::from(1_i128));
403        values.insert(d, FieldElement::from(1_i128));
404
405        assert_eq!(ExpressionSolver::solve(&mut values, &opcode_a), Ok(()));
406
407        assert_eq!(values.get(&a).unwrap(), &FieldElement::from(2_i128));
408    }
409
410    #[test]
411    fn solves_unknown_in_linear_term() {
412        let a = Witness(0);
413        let b = Witness(1);
414        let c = Witness(2);
415        let d = Witness(3);
416
417        // a = b + c + d;
418        let opcode_a = Expression::from_str(&format!("{a} - {b} - {c} - {d}")).unwrap();
419
420        let e = Witness(4);
421        let opcode_b = Expression::from_str(&format!("{e} - {a} - {b}")).unwrap();
422
423        let mut values = WitnessMap::new();
424        values.insert(b, FieldElement::from(2_i128));
425        values.insert(c, FieldElement::from(1_i128));
426        values.insert(d, FieldElement::from(1_i128));
427
428        assert_eq!(ExpressionSolver::solve(&mut values, &opcode_a), Ok(()));
429        assert_eq!(ExpressionSolver::solve(&mut values, &opcode_b), Ok(()));
430
431        assert_eq!(values.get(&a).unwrap(), &FieldElement::from(4_i128));
432    }
433
434    #[test]
435    fn solves_by_combining_linear_terms_after_they_have_been_multiplied_by_known_witnesses() {
436        let expr = Expression::from_str("w1 + w1*w0 - 4").unwrap();
437        let mut values = WitnessMap::new();
438        values.insert(Witness(0), FieldElement::from(1_i128));
439
440        let res = ExpressionSolver::solve(&mut values, &expr);
441        assert!(res.is_ok());
442
443        assert_eq!(values.get(&Witness(1)).unwrap(), &FieldElement::from(2_i128));
444    }
445
446    #[test]
447    fn solves_by_combining_mul_terms() {
448        let expr = Expression::from_str("w1*w2 - w2*w1 + w3 - 2").unwrap();
449        let mut values = WitnessMap::new();
450
451        let res = ExpressionSolver::solve(&mut values, &expr);
452        assert!(res.is_ok());
453
454        assert_eq!(values.get(&Witness(3)).unwrap(), &FieldElement::from(2_i128));
455    }
456}