acvm/compiler/optimizers/common_subexpression/
mod.rs

1//! The `CommonSubexpressionOptimizer`: a meta-transformer which factors common subexpressions out
2//! of a [`Circuit`] into intermediate witnesses, reducing the total number of opcodes.
3//!
4//! The name comes from its net effect. The sub-passes below collectively detect subexpressions that
5//! the circuit computes more than once and bind each of them to a single intermediate witness, so
6//! the value is constrained once and then reused. [`csat::CSatTransformer`] is the pass that *forms*
7//! the candidate subexpressions (by slicing wide expressions into backend-width-sized chunks, caching
8//! identical chunks so the same subexpression maps to the same witness), and
9//! [`merge_expressions::MergeExpressionsOptimizer`] is the pass that *discards* the candidates which
10//! turned out not to be common (those used in only two opcodes are merged back). What survives are the
11//! genuinely shared subexpressions.
12//!
13//! ## On opcode "width"
14//!
15//! Several passes here are parameterized by a `width`. This is **not** a hard upper bound on the size
16//! of an emitted opcode: ACIR places no limit on how many terms an `AssertZero` opcode may contain,
17//! and proving backends are expected to handle linear combinations of unbounded width. The `width` is
18//! a heuristic target that controls the granularity at which the [`csat::CSatTransformer`] slices
19//! expressions, which in turn determines which subexpressions become reuse candidates. Because it is
20//! only a target, an emitted opcode may legitimately exceed it — see [`csat::CSatTransformer`] for the
21//! cases where this happens (e.g. an opcode whose sole unknown sits inside a multiplication term, which
22//! cannot be sliced without making the circuit unsolvable). Solvability, checked by
23//! [`crate::compiler::CircuitSimulator`], is the property these passes actually preserve; width is not.
24//!
25//! ## Sub-passes
26//!
27//! ### CSAT: slices `AssertZero` opcodes towards the backend's preferred width.
28//!
29//! For instance, with a width of 4, the `AssertZero` opcode `x1 + x2 + x3 + x4 + x5 - y = 0` is sliced using
30//! 2 intermediate variables (z1, z2):
31//! ```text
32//! x1 + x2 + x3 = z1
33//! x4 + x5 = z2
34//! z1 + z2 - y = 0
35//! ```
36//! If x1,..x5 are inputs to the program, they are tagged as 'solvable', and would be used to compute the value of y.
37//! If we generated the intermediate variable `x4 + x5 - y = z3` instead, we would get an unsolvable circuit because
38//! that `AssertZero` opcode has two unknown values: y and z3.
39//! So the CSAT transformation keeps track of which witnesses would be solved for each opcode in order to only generate
40//! solvable intermediate variables. Identical slices are cached, so a subexpression appearing in several opcodes is
41//! assigned a single shared intermediate witness — this is where the common subexpressions are formed.
42//!
43//! ### Eliminate intermediate variables
44//!
45//! The 'eliminate intermediate variables' pass will remove any intermediate variables (for instance created by the previous transformation)
46//! that are used in exactly two `AssertZero` opcodes.
47//! This results in arithmetic opcodes having linear combinations of potentially large width.
48//! For instance if the intermediate variable is z1 and is only used in y:
49//! ```text
50//! z1 = x1 + x2 + x3
51//! y = z1 + x4
52//! ```
53//! We remove it, undoing the work done during the CSAT transformation: `y = x1 + x2 + x3 + x4`.
54//!
55//! We do this because the backend is expected to handle linear combinations of 'unbounded width' in a more efficient way
56//! than the 'CSAT transformation'.
57//! However, it is worthwhile to keep an intermediate variable if it is used in more than two opcodes: that is precisely a
58//! common subexpression, and materializing it once is cheaper than recomputing it in each opcode.
59//!
60//! ### `redundant_range`
61//!
62//! The 'range optimization' pass, from the optimizers module, will remove any redundant range opcodes.
63use std::collections::BTreeMap;
64
65use acir::{
66    AcirField,
67    circuit::{
68        Circuit, Opcode,
69        brillig::{BrilligFunctionId, BrilligInputs, BrilligOutputs},
70        opcodes::{BlackBoxFuncCall, FunctionInput},
71    },
72    native_types::{Expression, Witness},
73};
74use indexmap::IndexMap;
75
76mod csat;
77mod merge_expressions;
78
79use csat::CSatTransformer;
80use merge_expressions::MergeExpressionsOptimizer;
81
82use tracing::info;
83
84use super::RangeOptimizer;
85
86/// We use multiple passes to stabilize the output in many cases
87const DEFAULT_MAX_TRANSFORMER_PASSES: usize = 3;
88const DEFAULT_EXPRESSION_WIDTH: usize = 4;
89
90/// Applies backend specific optimizations to a [`Circuit`].
91///
92/// Accepts an injected `acir_opcode_positions` to allow transformations to be applied directly after optimizations.
93///
94/// Does multiple passes until the output stabilizes.
95///
96/// Pre-Conditions:
97/// - General Optimizer must run before this pass,
98///   when `max_transformer_passes_or_default.unwrap_or(DEFAULT_MAX_TRANSFORMER_PASSES)` is greater than 0
99#[tracing::instrument(level = "trace", name = "transform_acir", skip(acir, acir_opcode_positions))]
100pub(super) fn transform_internal<F: AcirField>(
101    mut acir: Circuit<F>,
102    mut acir_opcode_positions: Vec<usize>,
103    brillig_side_effects: &BTreeMap<BrilligFunctionId, bool>,
104    max_transformer_passes_or_default: Option<usize>,
105) -> (Circuit<F>, Vec<usize>, bool) {
106    if acir.opcodes.len() == 1 && matches!(acir.opcodes[0], Opcode::BrilligCall { .. }) {
107        info!("Program is fully unconstrained, skipping transformation pass");
108        return (acir, acir_opcode_positions, true);
109    }
110
111    // Allow multiple passes until we have stable output.
112    // We use opcode count rather than a structural hash as the convergence signal,
113    // because intermediate variables are created in step 1. to slice expressions towards the width
114    // and then sometimes are removed in step 2. to benefit from the 'big-add' gates of the proving system.
115    // Opcode count tracks the metric we actually care about.
116    let mut prev_opcode_count = acir.opcodes.len();
117
118    let mut opcode_count_stabilized = false;
119
120    let max_transformer_passes =
121        max_transformer_passes_or_default.unwrap_or(DEFAULT_MAX_TRANSFORMER_PASSES);
122
123    // For most test programs it would be enough to loop here, but some of them
124    // don't stabilize unless we also repeat the backend agnostic optimizations.
125    for _ in 0..max_transformer_passes {
126        info!("Number of opcodes {}", acir.opcodes.len());
127        let (new_acir, new_acir_opcode_positions) =
128            transform_internal_once(acir, acir_opcode_positions, brillig_side_effects);
129
130        acir = new_acir;
131        acir_opcode_positions = new_acir_opcode_positions;
132
133        let new_opcode_count = acir.opcodes.len();
134
135        if new_opcode_count == prev_opcode_count {
136            opcode_count_stabilized = true;
137            break;
138        }
139        prev_opcode_count = new_opcode_count;
140    }
141
142    (acir, acir_opcode_positions, opcode_count_stabilized)
143}
144
145/// Accepts an injected `acir_opcode_positions` to allow transformations to be applied directly after optimizations.
146///
147/// It first performs the 'CSAT transformation' in one pass, slicing wide expressions into intermediate variables.
148/// Then it performs `eliminate_intermediate_variable()` which (re-)combines intermediate variables used only twice.
149/// It concludes with a round of `replace_redundant_ranges()` which removes range checks made redundant by the previous pass.
150///
151/// Pre-Conditions:
152/// - General Optimizer must run before this pass
153#[tracing::instrument(
154    level = "trace",
155    name = "transform_acir_once",
156    skip(acir, acir_opcode_positions)
157)]
158fn transform_internal_once<F: AcirField>(
159    mut acir: Circuit<F>,
160    acir_opcode_positions: Vec<usize>,
161    brillig_side_effects: &BTreeMap<BrilligFunctionId, bool>,
162) -> (Circuit<F>, Vec<usize>) {
163    // 1. CSAT transformation
164    // Process each opcode in the circuit by marking the solvable witnesses and slicing the AssertZero opcodes
165    // towards the backend's preferred width by creating intermediate variables.
166    // Knowing if a witness is solvable avoids creating un-solvable intermediate variables.
167    let csat_span = tracing::trace_span!("csat_transformer").entered();
168    let mut transformer = CSatTransformer::new(DEFAULT_EXPRESSION_WIDTH);
169    for value in acir.circuit_arguments() {
170        transformer.mark_solvable(value);
171    }
172
173    let mut new_acir_opcode_positions: Vec<usize> = Vec::with_capacity(acir_opcode_positions.len());
174    // Optimize the assert-zero gates by slicing them towards the backend's preferred width and
175    // creating intermediate variables when necessary
176    let mut transformed_opcodes = Vec::new();
177
178    let mut next_witness_index = max_witness(&acir).witness_index() + 1;
179    // maps a normalized expression to the intermediate variable which represents the expression, along with its 'norm'
180    // the 'norm' is simply the value of the first non-zero coefficient in the expression, taken from the linear terms, or quadratic terms if there is none.
181    let mut intermediate_variables: IndexMap<Expression<F>, (F, Witness)> = IndexMap::new();
182    for (index, opcode) in acir.opcodes.into_iter().enumerate() {
183        match opcode {
184            Opcode::AssertZero(arith_expr) => {
185                let len = intermediate_variables.len();
186
187                let arith_expr = transformer.transform(
188                    arith_expr,
189                    &mut intermediate_variables,
190                    &mut next_witness_index,
191                );
192
193                let mut new_opcodes = Vec::new();
194                for (g, (norm, w)) in intermediate_variables.iter().skip(len) {
195                    // de-normalize
196                    let mut intermediate_opcode = g * *norm;
197                    // constrain the intermediate opcode to the intermediate variable
198                    intermediate_opcode.linear_combinations.push((-F::one(), *w));
199                    intermediate_opcode.sort();
200                    new_opcodes.push(intermediate_opcode);
201                }
202                new_opcodes.push(arith_expr);
203                for opcode in new_opcodes {
204                    new_acir_opcode_positions.push(acir_opcode_positions[index]);
205                    transformed_opcodes.push(Opcode::AssertZero(opcode));
206                }
207            }
208            Opcode::BlackBoxFuncCall(ref func) => {
209                for witness in func.get_outputs_vec() {
210                    transformer.mark_solvable(witness);
211                }
212
213                new_acir_opcode_positions.push(acir_opcode_positions[index]);
214                transformed_opcodes.push(opcode);
215            }
216            Opcode::MemoryInit { .. } => {
217                // `MemoryInit` does not write values to the `WitnessMap`
218                new_acir_opcode_positions.push(acir_opcode_positions[index]);
219                transformed_opcodes.push(opcode);
220            }
221            Opcode::MemoryOp { ref op, .. } => {
222                transformer.mark_solvable(op.value);
223                new_acir_opcode_positions.push(acir_opcode_positions[index]);
224                transformed_opcodes.push(opcode);
225            }
226            Opcode::BrilligCall { ref outputs, .. } => {
227                for output in outputs {
228                    match output {
229                        BrilligOutputs::Simple(witness) => transformer.mark_solvable(*witness),
230                        BrilligOutputs::Array(witnesses) => {
231                            for witness in witnesses {
232                                transformer.mark_solvable(*witness);
233                            }
234                        }
235                    }
236                }
237
238                new_acir_opcode_positions.push(acir_opcode_positions[index]);
239                transformed_opcodes.push(opcode);
240            }
241            Opcode::Call { ref outputs, .. } => {
242                for witness in outputs {
243                    transformer.mark_solvable(*witness);
244                }
245
246                // `Call` does not write values to the `WitnessMap`
247                // A separate ACIR function should have its own respective `WitnessMap`
248                new_acir_opcode_positions.push(acir_opcode_positions[index]);
249                transformed_opcodes.push(opcode);
250            }
251        }
252    }
253
254    acir = Circuit {
255        opcodes: transformed_opcodes,
256        // The transformer does not add new public inputs
257        ..acir
258    };
259    drop(csat_span);
260
261    // 2. Eliminate intermediate variables, when they are used in exactly two arithmetic opcodes.
262    let mut merge_optimizer = MergeExpressionsOptimizer::new();
263
264    let (opcodes, new_acir_opcode_positions) =
265        merge_optimizer.eliminate_intermediate_variable(&acir, new_acir_opcode_positions);
266
267    acir = Circuit {
268        opcodes,
269        // The optimizer does not add new public inputs
270        ..acir
271    };
272
273    // 3. Remove redundant range constraints.
274    // The `MergeOptimizer` can merge two witnesses which have range opcodes applied to them
275    // so we run the `RangeOptimizer` afterwards to clear these up.
276    let range_optimizer = RangeOptimizer::new(acir, brillig_side_effects);
277    let (acir, new_acir_opcode_positions) =
278        range_optimizer.replace_redundant_ranges(new_acir_opcode_positions);
279
280    (acir, new_acir_opcode_positions)
281}
282
283/// Find the witness with the highest ID in the circuit.
284fn max_witness<F: AcirField>(circuit: &Circuit<F>) -> Witness {
285    let mut witnesses = WitnessFolder::new(Witness::default(), |state, witness| {
286        *state = witness.max(*state);
287    });
288    witnesses.fold_circuit(circuit);
289    witnesses.into_state()
290}
291
292/// Fold all witnesses in a circuit.
293struct WitnessFolder<S, A> {
294    state: S,
295    accumulate: A,
296}
297
298impl<S, A> WitnessFolder<S, A>
299where
300    A: Fn(&mut S, Witness),
301{
302    /// Create the folder with some initial state and an accumulator function.
303    fn new(init: S, accumulate: A) -> Self {
304        Self { state: init, accumulate }
305    }
306
307    /// Take the accumulated state.
308    fn into_state(self) -> S {
309        self.state
310    }
311
312    /// Add all witnesses from the circuit.
313    fn fold_circuit<F: AcirField>(&mut self, circuit: &Circuit<F>) {
314        self.fold_many(circuit.private_parameters.iter());
315        self.fold_many(circuit.public_parameters.0.iter());
316        self.fold_many(circuit.return_values.0.iter());
317        for opcode in &circuit.opcodes {
318            self.fold_opcode(opcode);
319        }
320    }
321
322    /// Fold a witness into the state.
323    fn fold(&mut self, witness: Witness) {
324        (self.accumulate)(&mut self.state, witness);
325    }
326
327    /// Fold many witnesses into the state.
328    fn fold_many<'w, I: Iterator<Item = &'w Witness>>(&mut self, witnesses: I) {
329        for witness in witnesses {
330            self.fold(*witness);
331        }
332    }
333
334    /// Add witnesses from the opcode.
335    fn fold_opcode<F: AcirField>(&mut self, opcode: &Opcode<F>) {
336        match opcode {
337            Opcode::AssertZero(expr) => {
338                self.fold_expr(expr);
339            }
340            Opcode::BlackBoxFuncCall(call) => self.fold_blackbox(call),
341            Opcode::MemoryOp { block_id: _, op } => {
342                self.fold(op.index);
343                self.fold(op.value);
344            }
345            Opcode::MemoryInit { block_id: _, init, block_type: _ } => {
346                for witness in init {
347                    self.fold(*witness);
348                }
349            }
350            // We keep the display for a BrilligCall and circuit Call separate as they
351            // are distinct in their functionality and we should maintain this separation for debugging.
352            Opcode::BrilligCall { id: _, inputs, outputs, predicate } => {
353                self.fold_expr(predicate);
354                self.fold_brillig_inputs(inputs);
355                self.fold_brillig_outputs(outputs);
356            }
357            Opcode::Call { id: _, inputs, outputs, predicate } => {
358                self.fold_expr(predicate);
359                self.fold_many(inputs.iter());
360                self.fold_many(outputs.iter());
361            }
362        }
363    }
364
365    fn fold_expr<F: AcirField>(&mut self, expr: &Expression<F>) {
366        for i in &expr.mul_terms {
367            self.fold(i.1);
368            self.fold(i.2);
369        }
370        for i in &expr.linear_combinations {
371            self.fold(i.1);
372        }
373    }
374
375    fn fold_brillig_inputs<F: AcirField>(&mut self, inputs: &[BrilligInputs<F>]) {
376        for input in inputs {
377            match input {
378                BrilligInputs::Single(expr) => {
379                    self.fold_expr(expr);
380                }
381                BrilligInputs::Array(exprs) => {
382                    for expr in exprs {
383                        self.fold_expr(expr);
384                    }
385                }
386                BrilligInputs::MemoryArray(_) => {}
387            }
388        }
389    }
390
391    fn fold_brillig_outputs(&mut self, outputs: &[BrilligOutputs]) {
392        for output in outputs {
393            match output {
394                BrilligOutputs::Simple(witness) => {
395                    self.fold(*witness);
396                }
397                BrilligOutputs::Array(witnesses) => self.fold_many(witnesses.iter()),
398            }
399        }
400    }
401
402    fn fold_blackbox<F: AcirField>(&mut self, call: &BlackBoxFuncCall<F>) {
403        match call {
404            BlackBoxFuncCall::AES128Encrypt { inputs, iv, key, outputs } => {
405                self.fold_inputs(inputs.as_slice());
406                self.fold_inputs(iv.as_slice());
407                self.fold_inputs(key.as_slice());
408                self.fold_many(outputs.iter());
409            }
410            BlackBoxFuncCall::AND { lhs, rhs, output, .. } => {
411                self.fold_input(lhs);
412                self.fold_input(rhs);
413                self.fold(*output);
414            }
415            BlackBoxFuncCall::XOR { lhs, rhs, output, .. } => {
416                self.fold_input(lhs);
417                self.fold_input(rhs);
418                self.fold(*output);
419            }
420            BlackBoxFuncCall::RANGE { input, .. } => {
421                self.fold_input(input);
422            }
423            BlackBoxFuncCall::Blake2s { inputs, outputs } => {
424                self.fold_inputs(inputs.as_slice());
425                self.fold_many(outputs.iter());
426            }
427            BlackBoxFuncCall::Blake3 { inputs, outputs } => {
428                self.fold_inputs(inputs.as_slice());
429                self.fold_many(outputs.iter());
430            }
431            BlackBoxFuncCall::EcdsaSecp256k1 {
432                public_key_x,
433                public_key_y,
434                signature,
435                hashed_message,
436                output,
437                predicate,
438            } => {
439                self.fold_inputs(public_key_x.as_slice());
440                self.fold_inputs(public_key_y.as_slice());
441                self.fold_inputs(signature.as_slice());
442                self.fold_inputs(hashed_message.as_slice());
443                self.fold(*output);
444                self.fold_input(predicate);
445            }
446            BlackBoxFuncCall::EcdsaSecp256r1 {
447                public_key_x,
448                public_key_y,
449                signature,
450                hashed_message,
451                output,
452                predicate,
453            } => {
454                self.fold_inputs(public_key_x.as_slice());
455                self.fold_inputs(public_key_y.as_slice());
456                self.fold_inputs(signature.as_slice());
457                self.fold_inputs(hashed_message.as_slice());
458                self.fold(*output);
459                self.fold_input(predicate);
460            }
461            BlackBoxFuncCall::MultiScalarMul { points, scalars, predicate, outputs } => {
462                self.fold_inputs(points.as_slice());
463                self.fold_inputs(scalars.as_slice());
464                self.fold_input(predicate);
465                let (x, y) = outputs;
466                self.fold(*x);
467                self.fold(*y);
468            }
469            BlackBoxFuncCall::EmbeddedCurveAdd { input1, input2, predicate, outputs } => {
470                self.fold_inputs(input1.as_slice());
471                self.fold_inputs(input2.as_slice());
472                self.fold_input(predicate);
473                let (x, y) = outputs;
474                self.fold(*x);
475                self.fold(*y);
476            }
477            BlackBoxFuncCall::Keccakf1600 { inputs, outputs } => {
478                self.fold_inputs(inputs.as_slice());
479                self.fold_many(outputs.iter());
480            }
481            BlackBoxFuncCall::RecursiveAggregation {
482                verification_key,
483                proof,
484                public_inputs,
485                key_hash,
486                proof_type: _,
487                predicate,
488            } => {
489                self.fold_inputs(verification_key.as_slice());
490                self.fold_inputs(proof.as_slice());
491                self.fold_inputs(public_inputs.as_slice());
492                self.fold_input(key_hash);
493                self.fold_input(predicate);
494            }
495            BlackBoxFuncCall::Poseidon2Permutation { inputs, outputs } => {
496                self.fold_inputs(inputs.as_slice());
497                self.fold_many(outputs.iter());
498            }
499            BlackBoxFuncCall::Sha256Compression { inputs, hash_values, outputs } => {
500                self.fold_inputs(inputs.as_slice());
501                self.fold_inputs(hash_values.as_slice());
502                self.fold_many(outputs.iter());
503            }
504        }
505    }
506
507    fn fold_inputs<F: AcirField>(&mut self, inputs: &[FunctionInput<F>]) {
508        for input in inputs {
509            self.fold_input(input);
510        }
511    }
512
513    fn fold_input<F: AcirField>(&mut self, input: &FunctionInput<F>) {
514        if let FunctionInput::Witness(witness) = input {
515            self.fold(*witness);
516        }
517    }
518}
519
520#[cfg(test)]
521mod tests {
522    use super::transform_internal;
523    use crate::compiler::CircuitSimulator;
524    use acir::FieldElement;
525    use acir::circuit::{Circuit, Opcode, brillig::BrilligFunctionId};
526    use std::collections::BTreeMap;
527
528    #[test]
529    fn assert_zero_solving_for_a_multiplication_unknown_is_kept_intact() {
530        // An AssertZero whose only unknown (`w1`, the return value) sits inside a multiplication term,
531        // alongside `width` solvable linear terms: `w0*w1 = w2 + w3 + w4 + w5`.
532        //
533        // The multiplication term cannot be hoisted into an intermediate variable (the intermediate
534        // would be unsolvable, as it depends on the unknown `w1`), and the opcode as a whole is what
535        // solves `w1`. So the transformer must emit it unchanged, as a single opcode whose `width()`
536        // exceeds the `width` of 4. ACIR places no upper bound on opcode width, so this is valid; the
537        // property that matters is that the circuit stays solvable.
538        let src = r#"private parameters: [w0, w2, w3, w4, w5]
539        public parameters: []
540        return values: [w1]
541        ASSERT w0*w1 = w2 + w3 + w4 + w5
542        "#;
543        let acir = Circuit::<FieldElement>::from_str(src).unwrap();
544        assert!(CircuitSimulator::check_circuit(&acir).is_none());
545
546        let acir_opcode_positions = (0..acir.opcodes.len()).collect();
547        let (transformed, _, _) =
548            transform_internal(acir, acir_opcode_positions, &BTreeMap::new(), None);
549
550        // The opcode is emitted as-is: a single AssertZero with its multiplication term retained.
551        assert_eq!(transformed.opcodes.len(), 1);
552        let Opcode::AssertZero(expr) = &transformed.opcodes[0] else {
553            panic!("expected a single AssertZero opcode");
554        };
555        assert_eq!(expr.mul_terms.len(), 1, "the multiplication term must be preserved");
556        assert!(expr.width() > 4, "the opcode is wider than the target width, as expected");
557
558        // The transformed circuit remains solvable, which is the contract the transformer upholds.
559        assert!(CircuitSimulator::check_circuit(&transformed).is_none());
560    }
561
562    #[test]
563    fn test_max_transformer_passes() {
564        let formatted_acir = r#"private parameters: [w0]
565        public parameters: []
566        return values: [w1, w2, w3, w4, w5, w6, w7, w8, w9, w10, w11, w12, w13, w14, w15, w16, w17, w18, w19, w20, w21, w22, w23, w24, w25, w26, w27, w28, w29, w30, w31]
567        BRILLIG CALL func: 0, predicate: 1, inputs: [w0, 31, 256], outputs: [w32, w33, w34, w35, w36, w37, w38, w39, w40, w41, w42, w43, w44, w45, w46, w47, w48, w49, w50, w51, w52, w53, w54, w55, w56, w57, w58, w59, w60, w61, w62]
568        BLACKBOX::RANGE input: w35, bits: 8
569        BLACKBOX::RANGE input: w36, bits: 8
570        BLACKBOX::RANGE input: w37, bits: 8
571        BLACKBOX::RANGE input: w38, bits: 8
572        BLACKBOX::RANGE input: w39, bits: 8
573        BLACKBOX::RANGE input: w40, bits: 8
574        BLACKBOX::RANGE input: w41, bits: 8
575        BLACKBOX::RANGE input: w42, bits: 8
576        BLACKBOX::RANGE input: w43, bits: 8
577        BLACKBOX::RANGE input: w44, bits: 8
578        BLACKBOX::RANGE input: w45, bits: 8
579        BLACKBOX::RANGE input: w46, bits: 8
580        BLACKBOX::RANGE input: w47, bits: 8
581        BLACKBOX::RANGE input: w48, bits: 8
582        BLACKBOX::RANGE input: w49, bits: 8
583        BLACKBOX::RANGE input: w50, bits: 8
584        BLACKBOX::RANGE input: w51, bits: 8
585        BLACKBOX::RANGE input: w52, bits: 8
586        BLACKBOX::RANGE input: w53, bits: 8
587        BLACKBOX::RANGE input: w54, bits: 8
588        BLACKBOX::RANGE input: w55, bits: 8
589        BLACKBOX::RANGE input: w56, bits: 8
590        BLACKBOX::RANGE input: w57, bits: 8
591        BLACKBOX::RANGE input: w58, bits: 8
592        BLACKBOX::RANGE input: w59, bits: 8
593        BLACKBOX::RANGE input: w60, bits: 8
594        BLACKBOX::RANGE input: w61, bits: 8
595        BLACKBOX::RANGE input: w62, bits: 8
596        ASSERT w32 = w0 - 256*w33 - 65536*w34 - 16777216*w35 - 4294967296*w36 - 1099511627776*w37 - 281474976710656*w38 - 72057594037927936*w39 - 18446744073709551616*w40 - 4722366482869645213696*w41 - 1208925819614629174706176*w42 - 309485009821345068724781056*w43 - 79228162514264337593543950336*w44 - 20282409603651670423947251286016*w45 - 5192296858534827628530496329220096*w46 - 1329227995784915872903807060280344576*w47 - 340282366920938463463374607431768211456*w48 - 87112285931760246646623899502532662132736*w49 - 22300745198530623141535718272648361505980416*w50 - 5708990770823839524233143877797980545530986496*w51 - 1461501637330902918203684832716283019655932542976*w52 - 374144419156711147060143317175368453031918731001856*w53 - 95780971304118053647396689196894323976171195136475136*w54 - 24519928653854221733733552434404946937899825954937634816*w55 - 6277101735386680763835789423207666416102355444464034512896*w56 - 1606938044258990275541962092341162602522202993782792835301376*w57 - 411376139330301510538742295639337626245683966408394965837152256*w58 - 105312291668557186697918027683670432318895095400549111254310977536*w59 - 26959946667150639794667015087019630673637144422540572481103610249216*w60 - 6901746346790563787434755862277025452451108972170386555162524223799296*w61 - 1766847064778384329583297500742918515827483896875618958121606201292619776*w62
597        ASSERT w32 = 60
598        ASSERT w33 = 33
599        ASSERT w34 = 31
600        ASSERT w0 = 16777216*w35 + 4294967296*w36 + 1099511627776*w37 + 281474976710656*w38 + 72057594037927936*w39 + 18446744073709551616*w40 + 4722366482869645213696*w41 + 1208925819614629174706176*w42 + 309485009821345068724781056*w43 + 79228162514264337593543950336*w44 + 20282409603651670423947251286016*w45 + 5192296858534827628530496329220096*w46 + 1329227995784915872903807060280344576*w47 + 340282366920938463463374607431768211456*w48 + 87112285931760246646623899502532662132736*w49 + 22300745198530623141535718272648361505980416*w50 + 5708990770823839524233143877797980545530986496*w51 + 1461501637330902918203684832716283019655932542976*w52 + 374144419156711147060143317175368453031918731001856*w53 + 95780971304118053647396689196894323976171195136475136*w54 + 24519928653854221733733552434404946937899825954937634816*w55 + 6277101735386680763835789423207666416102355444464034512896*w56 + 1606938044258990275541962092341162602522202993782792835301376*w57 + 411376139330301510538742295639337626245683966408394965837152256*w58 + 105312291668557186697918027683670432318895095400549111254310977536*w59 + 26959946667150639794667015087019630673637144422540572481103610249216*w60 + 6901746346790563787434755862277025452451108972170386555162524223799296*w61 + 1766847064778384329583297500742918515827483896875618958121606201292619776*w62 + 2040124
601        ASSERT w62 = w1
602        ASSERT w61 = w2
603        ASSERT w60 = w3
604        ASSERT w59 = w4
605        ASSERT w58 = w5
606        ASSERT w57 = w6
607        ASSERT w56 = w7
608        ASSERT w55 = w8
609        ASSERT w54 = w9
610        ASSERT w53 = w10
611        ASSERT w52 = w11
612        ASSERT w51 = w12
613        ASSERT w50 = w13
614        ASSERT w49 = w14
615        ASSERT w48 = w15
616        ASSERT w47 = w16
617        ASSERT w46 = w17
618        ASSERT w45 = w18
619        ASSERT w44 = w19
620        ASSERT w43 = w20
621        ASSERT w42 = w21
622        ASSERT w41 = w22
623        ASSERT w40 = w23
624        ASSERT w39 = w24
625        ASSERT w38 = w25
626        ASSERT w37 = w26
627        ASSERT w36 = w27
628        ASSERT w35 = w28
629        ASSERT w29 = 31
630        ASSERT w30 = 33
631        ASSERT w31 = 60
632        "#;
633
634        let acir = Circuit::from_str(formatted_acir).unwrap();
635        assert!(CircuitSimulator::check_circuit(&acir).is_none());
636
637        let acir_opcode_positions = vec![
638            0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
639            24, 25, 26, 27, 28, 29, 29, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43,
640            44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64,
641        ];
642        let mut brillig_side_effects = BTreeMap::new();
643        brillig_side_effects.insert(BrilligFunctionId::new(0), false);
644
645        let (_, _, opcode_count_stabilized) =
646            transform_internal(acir, acir_opcode_positions, &brillig_side_effects, None);
647        assert!(!opcode_count_stabilized);
648    }
649}