1use acir::{
62 AcirField,
63 circuit::{
64 Circuit, Opcode,
65 brillig::BrilligFunctionId,
66 opcodes::{BlackBoxFuncCall, BlockId, FunctionInput, MemOp},
67 },
68 native_types::Witness,
69};
70use std::collections::{BTreeMap, BTreeSet, HashMap};
71
72struct RangeInfo {
74 switch_points: BTreeSet<usize>,
77 num_bits: u32,
79 is_implied: bool,
82}
83
84pub(crate) struct RangeOptimizer<'a, F: AcirField> {
85 infos: BTreeMap<Witness, RangeInfo>,
87 brillig_side_effects: &'a BTreeMap<BrilligFunctionId, bool>,
89 circuit: Circuit<F>,
90}
91
92impl<'a, F: AcirField> RangeOptimizer<'a, F> {
93 pub(crate) fn new(
96 circuit: Circuit<F>,
97 brillig_side_effects: &'a BTreeMap<BrilligFunctionId, bool>,
98 ) -> Self {
99 let infos = Self::collect_ranges(&circuit);
100 Self { circuit, infos, brillig_side_effects }
101 }
102
103 fn collect_ranges(circuit: &Circuit<F>) -> BTreeMap<Witness, RangeInfo> {
105 let mut infos: BTreeMap<Witness, RangeInfo> = BTreeMap::new();
106 let mut memory_block_lengths_bit_size: HashMap<BlockId, u32> = HashMap::new();
107
108 let update_witness_entry = |infos: &mut BTreeMap<Witness, RangeInfo>,
109 witness: Witness,
110 num_bits: u32,
111 is_implied: bool,
112 idx: usize| {
113 infos
114 .entry(witness)
115 .and_modify(|info| {
116 if num_bits < info.num_bits
117 || num_bits == info.num_bits && is_implied && !info.is_implied
118 {
119 info.switch_points.insert(idx);
120 info.num_bits = num_bits;
121 info.is_implied = is_implied;
122 }
123 })
124 .or_insert_with(|| RangeInfo {
125 num_bits,
126 is_implied,
127 switch_points: BTreeSet::from_iter(std::iter::once(idx)),
128 });
129 };
130
131 for (idx, opcode) in circuit.opcodes.iter().enumerate() {
132 match opcode {
133 Opcode::AssertZero(expr) => {
134 if expr.is_degree_one_univariate() {
137 let (k, witness) = expr.linear_combinations[0];
138 let constant = expr.q_c;
139 assert!(
140 k != F::zero(),
141 "collect_ranges: attempting to divide -constant by F::zero()"
142 );
143 let witness_value = -constant / k;
144
145 let num_bits =
146 if witness_value.is_zero() { 0 } else { witness_value.num_bits() };
147 update_witness_entry(&mut infos, witness, num_bits, true, idx);
148 }
149 }
150
151 Opcode::BlackBoxFuncCall(BlackBoxFuncCall::RANGE {
152 input: FunctionInput::Witness(witness),
153 num_bits,
154 }) => {
155 update_witness_entry(&mut infos, *witness, *num_bits, false, idx);
156 }
157
158 Opcode::MemoryInit { block_id, init, .. } => {
159 memory_block_lengths_bit_size
160 .insert(*block_id, memory_block_implied_max_bits(init));
161 }
162
163 Opcode::MemoryOp { block_id, op: MemOp { index, .. }, .. } => {
164 if let Some(witness) = index.to_witness() {
165 let num_bits = *memory_block_lengths_bit_size
166 .get(block_id)
167 .expect("memory must be initialized before any reads/writes");
168 update_witness_entry(&mut infos, witness, num_bits, true, idx);
169 }
170 }
171
172 Opcode::BlackBoxFuncCall(BlackBoxFuncCall::AND { lhs, rhs, num_bits, output })
174 | Opcode::BlackBoxFuncCall(BlackBoxFuncCall::XOR { lhs, rhs, num_bits, output }) => {
175 if let FunctionInput::Witness(witness) = lhs {
176 update_witness_entry(&mut infos, *witness, *num_bits, true, idx);
177 }
178 if let FunctionInput::Witness(witness) = rhs {
179 update_witness_entry(&mut infos, *witness, *num_bits, true, idx);
180 }
181 update_witness_entry(&mut infos, *output, *num_bits, true, idx);
182 }
183
184 Opcode::BlackBoxFuncCall(BlackBoxFuncCall::MultiScalarMul {
185 scalars,
186 predicate,
187 ..
188 }) => {
189 if predicate == &FunctionInput::Constant(F::one()) {
193 let mut scalar_iters = scalars.iter();
194 let mut lo = scalar_iters.next();
195 while lo.is_some() {
196 let lo_input = lo.unwrap();
197 let hi_input =
198 scalar_iters.next().expect("Missing scalar hi value for MSM");
199
200 if let FunctionInput::Witness(lo_witness) = lo_input {
201 update_witness_entry(&mut infos, *lo_witness, 128, true, idx);
202 }
203 if let FunctionInput::Witness(hi_witness) = hi_input {
204 update_witness_entry(&mut infos, *hi_witness, 126, true, idx);
205 }
206 lo = scalar_iters.next();
207 }
208 }
209 }
210
211 _ => {}
212 }
213 }
214 infos
215 }
216
217 pub(crate) fn replace_redundant_ranges(
228 self,
229 order_list: Vec<usize>,
230 ) -> (Circuit<F>, Vec<usize>) {
231 let mut new_order_list = Vec::with_capacity(order_list.len());
232 let mut optimized_opcodes = Vec::with_capacity(self.circuit.opcodes.len());
233 let mut next_side_effect = self.circuit.opcodes.len();
235 for (idx, opcode) in self.circuit.opcodes.into_iter().enumerate().rev() {
237 let Some(witness) = (match opcode {
238 Opcode::BlackBoxFuncCall(BlackBoxFuncCall::RANGE {
239 input: FunctionInput::Witness(witness),
240 ..
241 }) => Some(witness),
242 Opcode::BrilligCall { id, .. } => {
243 if self.brillig_side_effects.get(&id).copied().unwrap_or(true) {
245 next_side_effect = idx;
246 }
247 None
248 }
249 _ => None,
250 }) else {
251 optimized_opcodes.push(opcode.clone());
253 new_order_list.push(order_list[idx]);
254 continue;
255 };
256
257 let info = self.infos.get(&witness).expect("Could not find witness. This should never be the case if `collect_ranges` is called");
258
259 if !info.switch_points.contains(&idx) {
261 continue;
262 }
263
264 let has_stricter_before_next_side_effect = info
266 .switch_points
267 .iter()
268 .any(|switch_idx| *switch_idx > idx && *switch_idx < next_side_effect);
269
270 if has_stricter_before_next_side_effect {
272 continue;
273 }
274
275 new_order_list.push(order_list[idx]);
276 optimized_opcodes.push(opcode.clone());
277 }
278
279 optimized_opcodes.reverse();
281 new_order_list.reverse();
282
283 (Circuit { opcodes: optimized_opcodes, ..self.circuit }, new_order_list)
284 }
285}
286
287fn memory_block_implied_max_bits(init: &[Witness]) -> u32 {
289 let array_len = init.len() as u32;
290 let max_index = array_len.saturating_sub(1);
291 32 - max_index.leading_zeros()
292}
293
294#[cfg(test)]
295mod tests {
296 use std::collections::BTreeMap;
297
298 use crate::{
299 FieldElement, assert_circuit_snapshot,
300 compiler::{
301 CircuitSimulator,
302 optimizers::{
303 Opcode,
304 redundant_range::{RangeOptimizer, memory_block_implied_max_bits},
305 },
306 },
307 };
308 use acir::{
309 AcirField,
310 circuit::{Circuit, brillig::BrilligFunctionId},
311 native_types::{Expression, Witness},
312 };
313
314 #[test]
315 fn correctly_calculates_memory_block_implied_max_bits() {
316 assert_eq!(memory_block_implied_max_bits(&[]), 0);
317 assert_eq!(memory_block_implied_max_bits(&[Witness(0); 1]), 0);
318 assert_eq!(memory_block_implied_max_bits(&[Witness(0); 2]), 1);
319 assert_eq!(memory_block_implied_max_bits(&[Witness(0); 3]), 2);
320 assert_eq!(memory_block_implied_max_bits(&[Witness(0); 4]), 2);
321 assert_eq!(memory_block_implied_max_bits(&[Witness(0); 8]), 3);
322 assert_eq!(memory_block_implied_max_bits(&[Witness(0); u8::MAX as usize]), 8);
323 assert_eq!(memory_block_implied_max_bits(&[Witness(0); u16::MAX as usize]), 16);
324 }
325
326 #[test]
327 fn retain_lowest_range_size() {
328 let src = "
330 private parameters: [w1]
331 public parameters: []
332 return values: []
333 BLACKBOX::RANGE input: w1, bits: 32
334 BLACKBOX::RANGE input: w1, bits: 16
335 ";
336 let circuit = Circuit::from_str(src).unwrap();
337 assert!(CircuitSimulator::check_circuit(&circuit).is_none());
338
339 let acir_opcode_positions = circuit.opcodes.iter().enumerate().map(|(i, _)| i).collect();
340 let brillig_side_effects = BTreeMap::new();
341 let optimizer = RangeOptimizer::new(circuit, &brillig_side_effects);
342
343 let info = optimizer
344 .infos
345 .get(&Witness(1))
346 .expect("Witness(1) was inserted, but it is missing from the map");
347 assert_eq!(
348 info.num_bits, 16,
349 "expected a range size of 16 since that was the lowest bit size provided"
350 );
351
352 let (optimized_circuit, _) = optimizer.replace_redundant_ranges(acir_opcode_positions);
353 assert!(CircuitSimulator::check_circuit(&optimized_circuit).is_none());
354 assert_circuit_snapshot!(optimized_circuit, @r"
355 private parameters: [w1]
356 public parameters: []
357 return values: []
358 BLACKBOX::RANGE input: w1, bits: 16
359 ");
360 }
361
362 #[test]
363 fn remove_duplicates() {
364 let src = "
366 private parameters: [w1, w2]
367 public parameters: []
368 return values: []
369 BLACKBOX::RANGE input: w1, bits: 16
370 BLACKBOX::RANGE input: w1, bits: 16
371 BLACKBOX::RANGE input: w2, bits: 23
372 BLACKBOX::RANGE input: w2, bits: 23
373 ";
374 let circuit = Circuit::from_str(src).unwrap();
375 assert!(CircuitSimulator::check_circuit(&circuit).is_none());
376
377 let acir_opcode_positions = circuit.opcodes.iter().enumerate().map(|(i, _)| i).collect();
378 let brillig_side_effects = BTreeMap::new();
379 let optimizer = RangeOptimizer::new(circuit, &brillig_side_effects);
380 let (optimized_circuit, _) = optimizer.replace_redundant_ranges(acir_opcode_positions);
381 assert!(CircuitSimulator::check_circuit(&optimized_circuit).is_none());
382 assert_circuit_snapshot!(optimized_circuit, @r"
383 private parameters: [w1, w2]
384 public parameters: []
385 return values: []
386 BLACKBOX::RANGE input: w1, bits: 16
387 BLACKBOX::RANGE input: w2, bits: 23
388 ");
389 }
390
391 #[test]
392 fn non_range_opcodes() {
393 let src = "
396 private parameters: [w1]
397 public parameters: []
398 return values: []
399 BLACKBOX::RANGE input: w1, bits: 16
400 BLACKBOX::RANGE input: w1, bits: 16
401 ASSERT 0 = 0
402 ASSERT 0 = 0
403 ASSERT 0 = 0
404 ASSERT 0 = 0
405 ";
406 let circuit = Circuit::from_str(src).unwrap();
407 assert!(CircuitSimulator::check_circuit(&circuit).is_none());
408
409 let acir_opcode_positions = circuit.opcodes.iter().enumerate().map(|(i, _)| i).collect();
410 let brillig_side_effects = BTreeMap::new();
411 let optimizer = RangeOptimizer::new(circuit, &brillig_side_effects);
412 let (optimized_circuit, _) = optimizer.replace_redundant_ranges(acir_opcode_positions);
413 assert!(CircuitSimulator::check_circuit(&optimized_circuit).is_none());
414 assert_circuit_snapshot!(optimized_circuit, @r"
415 private parameters: [w1]
416 public parameters: []
417 return values: []
418 BLACKBOX::RANGE input: w1, bits: 16
419 ASSERT 0 = 0
420 ASSERT 0 = 0
421 ASSERT 0 = 0
422 ASSERT 0 = 0
423 ");
424 }
425
426 #[test]
427 fn constant_implied_ranges() {
428 let src = "
432 private parameters: [w1]
433 public parameters: []
434 return values: []
435 BLACKBOX::RANGE input: w1, bits: 16
436 ASSERT w1 = 0
437 ";
438 let circuit = Circuit::from_str(src).unwrap();
439 assert!(CircuitSimulator::check_circuit(&circuit).is_none());
440
441 let acir_opcode_positions = circuit.opcodes.iter().enumerate().map(|(i, _)| i).collect();
442 let brillig_side_effects = BTreeMap::new();
443 let optimizer = RangeOptimizer::new(circuit, &brillig_side_effects);
444 let (optimized_circuit, _) = optimizer.replace_redundant_ranges(acir_opcode_positions);
445 assert!(CircuitSimulator::check_circuit(&optimized_circuit).is_none());
446 assert_circuit_snapshot!(optimized_circuit, @r"
447 private parameters: [w1]
448 public parameters: []
449 return values: []
450 ASSERT w1 = 0
451 ");
452 }
453
454 #[test]
455 fn large_constant_implied_ranges() {
456 let src = "
460 private parameters: [w1]
461 public parameters: []
462 return values: []
463 BLACKBOX::RANGE input: w1, bits: 8
464 ASSERT w1 = 256
465 ";
466 let circuit = Circuit::from_str(src).unwrap();
467 assert!(CircuitSimulator::check_circuit(&circuit).is_none());
468
469 let acir_opcode_positions = circuit.opcodes.iter().enumerate().map(|(i, _)| i).collect();
470 let brillig_side_effects = BTreeMap::new();
471 let optimizer = RangeOptimizer::new(circuit, &brillig_side_effects);
472 let (optimized_circuit, _) = optimizer.replace_redundant_ranges(acir_opcode_positions);
473 assert!(CircuitSimulator::check_circuit(&optimized_circuit).is_none());
474 assert_circuit_snapshot!(optimized_circuit, @r"
475 private parameters: [w1]
476 public parameters: []
477 return values: []
478 BLACKBOX::RANGE input: w1, bits: 8
479 ASSERT w1 = 256
480 ");
481 }
482
483 #[test]
484 fn logic_opcode() {
485 let src = "
487 private parameters: [w0, w1]
488 public parameters: []
489 return values: [w2]
490 BLACKBOX::RANGE input: w0, bits: 8
491 BLACKBOX::RANGE input: w1, bits: 8
492 BLACKBOX::XOR lhs: w0, rhs: w1, output: w2, bits: 8
493 ";
494 let circuit = Circuit::from_str(src).unwrap();
495 assert!(CircuitSimulator::check_circuit(&circuit).is_none());
496
497 let acir_opcode_positions = circuit.opcodes.iter().enumerate().map(|(i, _)| i).collect();
498 let brillig_side_effects = BTreeMap::new();
499 let optimizer = RangeOptimizer::new(circuit, &brillig_side_effects);
500 let (optimized_circuit, _) = optimizer.replace_redundant_ranges(acir_opcode_positions);
501 assert!(CircuitSimulator::check_circuit(&optimized_circuit).is_none());
502 assert_circuit_snapshot!(optimized_circuit, @r"
503 private parameters: [w0, w1]
504 public parameters: []
505 return values: [w2]
506 BLACKBOX::XOR lhs: w0, rhs: w1, output: w2, bits: 8
507 ");
508 }
509
510 #[test]
511 fn potential_side_effects() {
512 let src = "
514 private parameters: [w1, w2]
515 public parameters: []
516 return values: []
517 BLACKBOX::RANGE input: w1, bits: 32
518
519 // Call brillig with w2
520 BRILLIG CALL func: 0, predicate: 1, inputs: [w2], outputs: []
521 BLACKBOX::RANGE input: w1, bits: 16
522
523 // Another call
524 BRILLIG CALL func: 0, predicate: 1, inputs: [w2], outputs: []
525
526 // One more constraint, but this is redundant.
527 BLACKBOX::RANGE input: w1, bits: 64
528
529 // assert w1 == 0
530 ASSERT w1 = 0
531 ";
532 let circuit = Circuit::from_str(src).unwrap();
533 assert!(CircuitSimulator::check_circuit(&circuit).is_none());
534
535 let acir_opcode_positions: Vec<usize> =
536 circuit.opcodes.iter().enumerate().map(|(i, _)| i).collect();
537
538 let brillig_side_effects = BTreeMap::from_iter(vec![(BrilligFunctionId(0), true)]);
540
541 let optimizer = RangeOptimizer::new(circuit, &brillig_side_effects);
542 let (optimized_circuit, _) =
543 optimizer.replace_redundant_ranges(acir_opcode_positions.clone());
544 assert!(CircuitSimulator::check_circuit(&optimized_circuit).is_none());
545
546 assert_circuit_snapshot!(optimized_circuit, @r"
548 private parameters: [w1, w2]
549 public parameters: []
550 return values: []
551 BLACKBOX::RANGE input: w1, bits: 32
552 BRILLIG CALL func: 0, predicate: 1, inputs: [w2], outputs: []
553 BLACKBOX::RANGE input: w1, bits: 16
554 BRILLIG CALL func: 0, predicate: 1, inputs: [w2], outputs: []
555 ASSERT w1 = 0
556 ");
557
558 let optimizer = RangeOptimizer::new(optimized_circuit.clone(), &brillig_side_effects);
560 let (double_optimized_circuit, _) =
561 optimizer.replace_redundant_ranges(acir_opcode_positions);
562 assert_eq!(optimized_circuit.to_string(), double_optimized_circuit.to_string());
563 }
564
565 #[test]
566 fn array_implied_ranges() {
567 let src = "
570 private parameters: [w0, w1]
571 public parameters: []
572 return values: []
573 BLACKBOX::RANGE input: w1, bits: 16
574 INIT b0 = [w0, w0, w0, w0, w0, w0, w0, w0]
575 READ w2 = b0[w1]
576 ";
577 let circuit = Circuit::from_str(src).unwrap();
578 assert!(CircuitSimulator::check_circuit(&circuit).is_none());
579
580 let acir_opcode_positions = circuit.opcodes.iter().enumerate().map(|(i, _)| i).collect();
581 let brillig_side_effects = BTreeMap::new();
582 let optimizer = RangeOptimizer::new(circuit, &brillig_side_effects);
583 let (optimized_circuit, _) = optimizer.replace_redundant_ranges(acir_opcode_positions);
584 assert!(CircuitSimulator::check_circuit(&optimized_circuit).is_none());
585 assert_circuit_snapshot!(optimized_circuit, @r"
586 private parameters: [w0, w1]
587 public parameters: []
588 return values: []
589 INIT b0 = [w0, w0, w0, w0, w0, w0, w0, w0]
590 READ w2 = b0[w1]
591 ");
592 }
593
594 #[test]
595 fn large_array_implied_ranges() {
596 let src = "
599 private parameters: [w0, w1]
600 public parameters: []
601 return values: []
602 BLACKBOX::RANGE input: w1, bits: 2
603 INIT b0 = [w0, w0, w0, w0, w0, w0, w0, w0]
604 READ w2 = b0[w1]
605 ";
606 let circuit = Circuit::from_str(src).unwrap();
607 assert!(CircuitSimulator::check_circuit(&circuit).is_none());
608
609 let acir_opcode_positions = circuit.opcodes.iter().enumerate().map(|(i, _)| i).collect();
610 let brillig_side_effects = BTreeMap::new();
611 let optimizer = RangeOptimizer::new(circuit, &brillig_side_effects);
612 let (optimized_circuit, _) = optimizer.replace_redundant_ranges(acir_opcode_positions);
613 assert!(CircuitSimulator::check_circuit(&optimized_circuit).is_none());
614 assert_circuit_snapshot!(optimized_circuit, @r"
615 private parameters: [w0, w1]
616 public parameters: []
617 return values: []
618 BLACKBOX::RANGE input: w1, bits: 2
619 INIT b0 = [w0, w0, w0, w0, w0, w0, w0, w0]
620 READ w2 = b0[w1]
621 ");
622 }
623
624 #[test]
625 #[should_panic(expected = "collect_ranges: attempting to divide -constant by F::zero()")]
626 fn collect_ranges_zero_linear_combination_panics() {
627 let src = "
628 private parameters: [w1]
629 public parameters: []
630 return values: []
631 ";
632 let mut circuit = Circuit::from_str(src).unwrap();
633 let expr = Expression {
634 mul_terms: vec![],
635 linear_combinations: vec![(FieldElement::zero(), Witness(0))],
636 q_c: FieldElement::one(),
637 };
638 let opcode = Opcode::AssertZero(expr);
639 circuit.opcodes.push(opcode);
640 RangeOptimizer::collect_ranges(&circuit);
641 }
642
643 #[test]
644 fn msm_implied_ranges() {
645 let src = "
647 private parameters: [w1, w2, w3, w4, w5, w6]
648 public parameters: []
649 return values: []
650 BLACKBOX::RANGE input: w1, bits: 128
651 BLACKBOX::RANGE input: w2, bits: 128
652 BLACKBOX::MULTI_SCALAR_MUL points: [w3, w4, 1], scalars: [w1, w2], predicate: 1, outputs: [w5, w6, w7]
653 ";
654 let circuit = Circuit::from_str(src).unwrap();
655 assert!(CircuitSimulator::check_circuit(&circuit).is_none());
656
657 let acir_opcode_positions = circuit.opcodes.iter().enumerate().map(|(i, _)| i).collect();
658 let brillig_side_effects = BTreeMap::new();
659 let optimizer = RangeOptimizer::new(circuit, &brillig_side_effects);
660
661 let lo_info = optimizer.infos.get(&Witness(1)).expect("w1 should have range info");
663 assert_eq!(lo_info.num_bits, 128, "lo scalar should be constrained to 128 bits");
664 assert!(lo_info.is_implied, "lo scalar constraint should be marked as implied");
665
666 let hi_info = optimizer.infos.get(&Witness(2)).expect("w2 should have range info");
667 assert_eq!(hi_info.num_bits, 126, "hi scalar should be constrained to 126 bits");
668 assert!(hi_info.is_implied, "hi scalar constraint should be marked as implied");
669
670 let (optimized_circuit, _) = optimizer.replace_redundant_ranges(acir_opcode_positions);
671 assert!(CircuitSimulator::check_circuit(&optimized_circuit).is_none());
672
673 assert_circuit_snapshot!(optimized_circuit, @r"
675 private parameters: [w1, w2, w3, w4, w5, w6]
676 public parameters: []
677 return values: []
678 BLACKBOX::MULTI_SCALAR_MUL points: [w3, w4, 1], scalars: [w1, w2], predicate: 1, outputs: [w5, w6, w7]
679 ");
680 }
681
682 #[test]
683 fn msm_stricter_explicit_range_retained() {
684 let src = "
686 private parameters: [w1, w2, w3, w4, w5, w6]
687 public parameters: []
688 return values: []
689 BLACKBOX::RANGE input: w1, bits: 64
690 BLACKBOX::RANGE input: w2, bits: 64
691 BLACKBOX::MULTI_SCALAR_MUL points: [w3, w4, 1], scalars: [w1, w2], predicate: 1, outputs: [w5, w6, w7]
692 ";
693 let circuit = Circuit::from_str(src).unwrap();
694 assert!(CircuitSimulator::check_circuit(&circuit).is_none());
695
696 let acir_opcode_positions = circuit.opcodes.iter().enumerate().map(|(i, _)| i).collect();
697 let brillig_side_effects = BTreeMap::new();
698 let optimizer = RangeOptimizer::new(circuit, &brillig_side_effects);
699
700 let lo_info = optimizer.infos.get(&Witness(1)).expect("w1 should have range info");
702 assert_eq!(lo_info.num_bits, 64, "explicit 64-bit range should be the strictest");
703
704 let hi_info = optimizer.infos.get(&Witness(2)).expect("w2 should have range info");
705 assert_eq!(hi_info.num_bits, 64, "explicit 64-bit range should be the strictest");
706
707 let (optimized_circuit, _) = optimizer.replace_redundant_ranges(acir_opcode_positions);
708 assert!(CircuitSimulator::check_circuit(&optimized_circuit).is_none());
709
710 assert_circuit_snapshot!(optimized_circuit, @r"
712 private parameters: [w1, w2, w3, w4, w5, w6]
713 public parameters: []
714 return values: []
715 BLACKBOX::RANGE input: w1, bits: 64
716 BLACKBOX::RANGE input: w2, bits: 64
717 BLACKBOX::MULTI_SCALAR_MUL points: [w3, w4, 1], scalars: [w1, w2], predicate: 1, outputs: [w5, w6, w7]
718 ");
719 }
720}