acvm/compiler/optimizers/common_subexpression/
merge_expressions.rs1use std::collections::{BTreeSet, HashMap};
2
3use acir::{
4 AcirField,
5 circuit::{
6 Circuit, Opcode,
7 brillig::{BrilligInputs, BrilligOutputs},
8 opcodes::BlockId,
9 },
10 native_types::{Expression, Witness},
11};
12use rustc_hash::FxHashMap;
13
14use crate::compiler::{CircuitSimulator, optimizers::GeneralOptimizer};
15
16pub(crate) struct MergeExpressionsOptimizer<F: AcirField> {
17 resolved_blocks: HashMap<BlockId, BTreeSet<Witness>>,
18 modified_gates: HashMap<usize, Opcode<F>>,
19 deleted_gates: BTreeSet<usize>,
20}
21
22impl<F: AcirField> MergeExpressionsOptimizer<F> {
23 pub(crate) fn new() -> Self {
24 MergeExpressionsOptimizer {
25 resolved_blocks: HashMap::new(),
26 modified_gates: HashMap::new(),
27 deleted_gates: BTreeSet::new(),
28 }
29 }
30
31 #[tracing::instrument(level = "trace", name = "merge_expressions", skip_all)]
59 pub(crate) fn eliminate_intermediate_variable(
60 &mut self,
61 circuit: &Circuit<F>,
62 acir_opcode_positions: Vec<usize>,
63 ) -> (Vec<Opcode<F>>, Vec<usize>) {
64 self.modified_gates.clear();
66 self.deleted_gates.clear();
67 self.resolved_blocks.clear();
68
69 let circuit_io: BTreeSet<Witness> =
71 circuit.circuit_arguments().union(&circuit.public_inputs().0).copied().collect();
72
73 let mut used_witnesses: FxHashMap<Witness, BTreeSet<usize>> = FxHashMap::default();
74 for (i, opcode) in circuit.opcodes.iter().enumerate() {
75 let witnesses = self.witness_inputs(opcode);
76 if let Opcode::MemoryInit { block_id, .. } = opcode {
77 self.resolved_blocks.insert(*block_id, witnesses.clone());
78 }
79 for w in witnesses {
80 if !circuit_io.contains(&w) {
82 used_witnesses.entry(w).or_default().insert(i);
83 }
84 }
85 }
86
87 for (op1, opcode) in circuit.opcodes.iter().enumerate() {
89 if !matches!(opcode, Opcode::AssertZero(_)) {
90 continue;
91 }
92 let input_witnesses = match self.get_opcode(op1, circuit) {
94 Some(opcode) => self.witness_inputs(opcode),
95 None => continue,
96 };
97 for w in input_witnesses {
98 let Some(gates_using_w) = used_witnesses.get(&w) else {
99 continue;
100 };
101 if gates_using_w.len() == 2 {
103 let first = *gates_using_w.first().expect("gates_using_w.len == 2");
104 let second = *gates_using_w.last().expect("gates_using_w.len == 2");
105 let op2 = if second == op1 {
106 first
107 } else {
108 assert!(op1 == first);
110 second
111 };
112
113 if op1 != op2 {
116 let (source, target) = if op1 < op2 { (op1, op2) } else { (op2, op1) };
117
118 let merge = match (
122 self.get_opcode(target, circuit),
123 self.get_opcode(source, circuit),
124 ) {
125 (
126 Some(Opcode::AssertZero(expr_use)),
127 Some(Opcode::AssertZero(expr_define)),
128 ) => Self::merge_expression(expr_use, expr_define, w).map(|expr| {
129 let witnesses: Vec<Witness> =
130 CircuitSimulator::expr_witness(expr_use)
131 .chain(CircuitSimulator::expr_witness(expr_define))
132 .collect();
133 (expr, witnesses)
134 }),
135 _ => None,
136 };
137
138 if let Some((expr, witnesses)) = merge {
139 self.modified_gates.insert(target, Opcode::AssertZero(expr));
140 self.deleted_gates.insert(source);
141 for w2 in witnesses {
143 if !circuit_io.contains(&w2) {
144 used_witnesses.entry(w2).and_modify(|v| {
145 v.insert(target);
146 v.remove(&source);
147 });
148 }
149 }
150 break;
153 }
154 }
155 }
156 }
157 }
158
159 let mut new_circuit = Vec::new();
161 let mut new_acir_opcode_positions = Vec::new();
162
163 for (i, opcode_position) in acir_opcode_positions.iter().enumerate() {
164 if let Some(opcode) = self.get_opcode(i, circuit) {
165 new_circuit.push(opcode.clone());
166 new_acir_opcode_positions.push(*opcode_position);
167 }
168 }
169 (new_circuit, new_acir_opcode_positions)
170 }
171
172 fn for_each_brillig_input_witness(&self, input: &BrilligInputs<F>, mut f: impl FnMut(Witness)) {
173 match input {
174 BrilligInputs::Single(expr) => {
175 for witness in CircuitSimulator::expr_witness(expr) {
176 f(witness);
177 }
178 }
179 BrilligInputs::Array(exprs) => {
180 for expr in exprs {
181 for witness in CircuitSimulator::expr_witness(expr) {
182 f(witness);
183 }
184 }
185 }
186 BrilligInputs::MemoryArray(block_id) => {
187 for witness in self.resolved_blocks.get(block_id).expect("Unknown block id") {
188 f(*witness);
189 }
190 }
191 }
192 }
193
194 fn for_each_brillig_output_witness(&self, output: &BrilligOutputs, mut f: impl FnMut(Witness)) {
195 match output {
196 BrilligOutputs::Simple(witness) => f(*witness),
197 BrilligOutputs::Array(witnesses) => {
198 for witness in witnesses {
199 f(*witness);
200 }
201 }
202 }
203 }
204
205 fn witness_inputs(&self, opcode: &Opcode<F>) -> BTreeSet<Witness> {
207 match opcode {
208 Opcode::AssertZero(expr) => CircuitSimulator::expr_witness(expr).collect(),
209 Opcode::BlackBoxFuncCall(bb_func) => {
210 let mut witnesses = bb_func.get_input_witnesses();
211 witnesses.extend(bb_func.get_outputs_vec());
212 if let Some(w) = bb_func.get_predicate() {
213 witnesses.insert(w);
214 }
215 witnesses
216 }
217 Opcode::MemoryOp { block_id: _, op } => [op.index, op.value].into_iter().collect(),
218
219 Opcode::MemoryInit { block_id: _, init, block_type: _ } => {
220 init.iter().copied().collect()
221 }
222 Opcode::BrilligCall { inputs, outputs, predicate, .. } => {
223 let mut witnesses = BTreeSet::new();
224 for i in inputs {
225 self.for_each_brillig_input_witness(i, |witness| {
226 witnesses.insert(witness);
227 });
228 }
229 witnesses.extend(CircuitSimulator::expr_witness(predicate));
230 for i in outputs {
231 self.for_each_brillig_output_witness(i, |witness| {
232 witnesses.insert(witness);
233 });
234 }
235 witnesses
236 }
237 Opcode::Call { id: _, inputs, outputs, predicate } => {
238 let mut witnesses: BTreeSet<Witness> = inputs.iter().copied().collect();
239 witnesses.extend(outputs);
240 witnesses.extend(CircuitSimulator::expr_witness(predicate));
241 witnesses
242 }
243 }
244 }
245
246 fn merge_expression(
249 target: &Expression<F>,
250 expr: &Expression<F>,
251 witness: Witness,
252 ) -> Option<Expression<F>> {
253 for m in &target.mul_terms {
255 if m.1 == witness || m.2 == witness {
256 return None;
257 }
258 }
259 for m in &expr.mul_terms {
260 if m.1 == witness || m.2 == witness {
261 return None;
262 }
263 }
264
265 for k in &target.linear_combinations {
266 if k.1 == witness {
267 for i in &expr.linear_combinations {
268 if i.1 == witness {
269 assert!(
270 i.0 != F::zero(),
271 "merge_expression: attempting to divide k.0 by F::zero"
272 );
273 let expr = target.add_mul(-(k.0 / i.0), expr);
274 let expr = GeneralOptimizer::optimize(expr);
275 return Some(expr);
276 }
277 }
278 }
279 }
280 None
281 }
282
283 fn get_opcode<'a>(&'a self, index: usize, circuit: &'a Circuit<F>) -> Option<&'a Opcode<F>> {
288 if self.deleted_gates.contains(&index) {
289 return None;
290 }
291 self.modified_gates.get(&index).or_else(|| circuit.opcodes.get(index))
292 }
293}
294
295#[cfg(test)]
296mod tests {
297 use crate::{
298 assert_circuit_snapshot,
299 compiler::{
300 CircuitSimulator,
301 optimizers::common_subexpression::merge_expressions::MergeExpressionsOptimizer,
302 },
303 };
304 use acir::{
305 AcirField, FieldElement,
306 circuit::Circuit,
307 native_types::{Expression, Witness},
308 };
309
310 fn merge_expressions(circuit: Circuit<FieldElement>) -> Circuit<FieldElement> {
311 assert!(CircuitSimulator::check_circuit(&circuit).is_none());
312 let mut merge_optimizer = MergeExpressionsOptimizer::new();
313 let acir_opcode_positions = vec![0; 20];
314 let (opcodes, _) =
315 merge_optimizer.eliminate_intermediate_variable(&circuit, acir_opcode_positions);
316 let mut optimized_circuit = circuit;
317 optimized_circuit.opcodes = opcodes;
318
319 assert!(CircuitSimulator::check_circuit(&optimized_circuit).is_none());
321 optimized_circuit
322 }
323
324 #[test]
325 fn merges_expressions() {
326 let src = "
327 private parameters: [w0]
328 public parameters: []
329 return values: [w2]
330 ASSERT 2*w1 = w0 + 5
331 ASSERT w2 = 4*w1 + 4
332 ";
333 let circuit = Circuit::from_str(src).unwrap();
334 let optimized_circuit = merge_expressions(circuit);
335 assert_circuit_snapshot!(optimized_circuit, @r"
336 private parameters: [w0]
337 public parameters: []
338 return values: [w2]
339 ASSERT w2 = 2*w0 + 14
340 ");
341 }
342
343 #[test]
344 fn does_not_eliminate_witnesses_returned_from_brillig() {
345 let src = "
346 private parameters: [w0]
347 public parameters: []
348 return values: []
349 BRILLIG CALL func: 0, predicate: 1, inputs: [], outputs: [w1]
350 ASSERT 2*w0 + 3*w1 + w2 + 1 = 0
351 ASSERT 2*w0 + 2*w1 + w5 + 1 = 0
352 ";
353 let circuit = Circuit::from_str(src).unwrap();
354 let optimized_circuit = merge_expressions(circuit.clone());
355 assert_eq!(circuit, optimized_circuit);
356 }
357
358 #[test]
359 fn does_not_eliminate_witnesses_returned_from_circuit() {
360 let src = "
361 private parameters: [w0]
362 public parameters: []
363 return values: [w1, w2]
364 ASSERT -w0*w0 + w1 = 0
365 ASSERT -w1 + w2 = 0
366 ";
367 let circuit = Circuit::from_str(src).unwrap();
368 let optimized_circuit = merge_expressions(circuit.clone());
369 assert_eq!(circuit, optimized_circuit);
370 }
371
372 #[test]
373 fn does_not_attempt_to_merge_into_previous_opcodes() {
374 let src = "
375 private parameters: [w0, w1]
376 public parameters: []
377 return values: []
378 ASSERT w0*w0 - w4 = 0
379 ASSERT w0*w1 + w5 = 0
380 ASSERT -w2 + w4 + w5 = 0
381 ASSERT w2 - w3 + w4 + w5 = 0
382 BLACKBOX::RANGE input: w3, bits: 32
383 ";
384 let circuit = Circuit::from_str(src).unwrap();
385
386 let optimized_circuit = merge_expressions(circuit);
387 assert_circuit_snapshot!(optimized_circuit, @r"
388 private parameters: [w0, w1]
389 public parameters: []
390 return values: []
391 ASSERT w5 = -w0*w1
392 ASSERT w3 = 2*w0*w0 + 2*w5
393 BLACKBOX::RANGE input: w3, bits: 32
394 ");
395 }
396
397 #[test]
398 fn takes_blackbox_opcode_outputs_into_account() {
399 let src = "
404 private parameters: [w0, w1]
405 public parameters: []
406 return values: [w2]
407 BRILLIG CALL func: 0, predicate: 1, inputs: [], outputs: [w3]
408 BLACKBOX::AND lhs: w0, rhs: w1, output: w4, bits: 8
409 ASSERT w3 - w4 = 0
410 ASSERT -w2 + w4 = 0
411 ";
412 let circuit = Circuit::from_str(src).unwrap();
413 let optimized_circuit = merge_expressions(circuit.clone());
414 assert_eq!(circuit, optimized_circuit);
415 }
416
417 #[test]
418 #[should_panic(expected = "merge_expression: attempting to divide k.0 by F::zero")]
419 fn merge_expression_on_zero_linear_combination_panics() {
420 let opcode_a = Expression {
421 mul_terms: vec![],
422 linear_combinations: vec![(FieldElement::one(), Witness(0))],
423 q_c: FieldElement::zero(),
424 };
425 let opcode_b = Expression {
426 mul_terms: vec![],
427 linear_combinations: vec![(FieldElement::zero(), Witness(0))],
428 q_c: FieldElement::zero(),
429 };
430 assert_eq!(
431 MergeExpressionsOptimizer::merge_expression(&opcode_a, &opcode_b, Witness(0),),
432 Some(opcode_a)
433 );
434 }
435
436 #[test]
437 fn does_not_eliminate_witnesses_used_in_brillig_call_predicates() {
438 let src = "
439 private parameters: [w2]
440 public parameters: [w0, w1]
441 return values: [w3]
442 BLACKBOX::RANGE input: w0, bits: 1
443 BLACKBOX::RANGE input: w1, bits: 1
444 BLACKBOX::RANGE input: w2, bits: 1
445 ASSERT w4 = w0*w1
446 ASSERT w5 = -w2 + 1
447 BRILLIG CALL func: 0, predicate: w4*w5, inputs: [w2], outputs: [w6]
448 ASSERT w3 = -w5 + 1
449 ";
450 let circuit = Circuit::from_str(src).unwrap();
451 let optimized_circuit = merge_expressions(circuit.clone());
452 assert_eq!(circuit, optimized_circuit);
453 }
454}