diff --git a/compiler/noirc_evaluator/src/ssa/opt/array_set.rs b/compiler/noirc_evaluator/src/ssa/opt/array_set.rs index 6d48b8c0d67..267a2c105f2 100644 --- a/compiler/noirc_evaluator/src/ssa/opt/array_set.rs +++ b/compiler/noirc_evaluator/src/ssa/opt/array_set.rs @@ -2,6 +2,7 @@ use crate::ssa::{ ir::{ basic_block::BasicBlockId, dfg::DataFlowGraph, + function::Function, instruction::{Instruction, InstructionId, TerminatorInstruction}, types::Type::{Array, Slice}, value::ValueId, @@ -17,32 +18,38 @@ impl Ssa { #[tracing::instrument(level = "trace", skip(self))] pub(crate) fn array_set_optimization(mut self) -> Self { for func in self.functions.values_mut() { - let reachable_blocks = func.reachable_blocks(); - - if !func.runtime().is_entry_point() { - assert_eq!(reachable_blocks.len(), 1, "Expected there to be 1 block remaining in Acir function for array_set optimization"); - } - let mut array_to_last_use = HashMap::default(); - let mut instructions_to_update = HashSet::default(); - let mut arrays_from_load = HashSet::default(); - - for block in reachable_blocks.iter() { - analyze_last_uses( - &func.dfg, - *block, - &mut array_to_last_use, - &mut instructions_to_update, - &mut arrays_from_load, - ); - } - for block in reachable_blocks { - make_mutable(&mut func.dfg, block, &instructions_to_update); - } + func.array_set_optimization(); } self } } +impl Function { + pub(crate) fn array_set_optimization(&mut self) { + let reachable_blocks = self.reachable_blocks(); + + if !self.runtime().is_entry_point() { + assert_eq!(reachable_blocks.len(), 1, "Expected there to be 1 block remaining in Acir function for array_set optimization"); + } + let mut array_to_last_use = HashMap::default(); + let mut instructions_to_update = HashSet::default(); + let mut arrays_from_load = HashSet::default(); + + for block in reachable_blocks.iter() { + analyze_last_uses( + &self.dfg, + *block, + &mut array_to_last_use, + &mut instructions_to_update, + &mut arrays_from_load, + ); + } + for block in reachable_blocks { + make_mutable(&mut self.dfg, block, &instructions_to_update); + } + } +} + /// Builds the set of ArraySet instructions that can be made mutable /// because their input value is unused elsewhere afterward. fn analyze_last_uses( diff --git a/compiler/noirc_evaluator/src/ssa/opt/as_slice_length.rs b/compiler/noirc_evaluator/src/ssa/opt/as_slice_length.rs index 69eab1da0ed..59917e8589b 100644 --- a/compiler/noirc_evaluator/src/ssa/opt/as_slice_length.rs +++ b/compiler/noirc_evaluator/src/ssa/opt/as_slice_length.rs @@ -20,13 +20,19 @@ impl Ssa { #[tracing::instrument(level = "trace", skip(self))] pub(crate) fn as_slice_optimization(mut self) -> Self { for func in self.functions.values_mut() { - let known_slice_lengths = known_slice_lengths(func); - replace_known_slice_lengths(func, known_slice_lengths); + func.as_slice_optimization(); } self } } +impl Function { + pub(crate) fn as_slice_optimization(&mut self) { + let known_slice_lengths = known_slice_lengths(self); + replace_known_slice_lengths(self, known_slice_lengths); + } +} + fn known_slice_lengths(func: &Function) -> HashMap { let mut known_slice_lengths = HashMap::default(); for block_id in func.reachable_blocks() { diff --git a/compiler/noirc_evaluator/src/ssa/opt/assert_constant.rs b/compiler/noirc_evaluator/src/ssa/opt/assert_constant.rs index ae0681a55ff..348c78683a0 100644 --- a/compiler/noirc_evaluator/src/ssa/opt/assert_constant.rs +++ b/compiler/noirc_evaluator/src/ssa/opt/assert_constant.rs @@ -26,22 +26,31 @@ impl Ssa { mut self, ) -> Result { for function in self.functions.values_mut() { - for block in function.reachable_blocks() { - // Unfortunately we can't just use instructions.retain(...) here since - // check_instruction can also return an error - let instructions = function.dfg[block].take_instructions(); - let mut filtered_instructions = Vec::with_capacity(instructions.len()); + function.evaluate_static_assert_and_assert_constant()?; + } + Ok(self) + } +} - for instruction in instructions { - if check_instruction(function, instruction)? { - filtered_instructions.push(instruction); - } - } +impl Function { + pub(crate) fn evaluate_static_assert_and_assert_constant( + &mut self, + ) -> Result<(), RuntimeError> { + for block in self.reachable_blocks() { + // Unfortunately we can't just use instructions.retain(...) here since + // check_instruction can also return an error + let instructions = self.dfg[block].take_instructions(); + let mut filtered_instructions = Vec::with_capacity(instructions.len()); - *function.dfg[block].instructions_mut() = filtered_instructions; + for instruction in instructions { + if check_instruction(self, instruction)? { + filtered_instructions.push(instruction); + } } + + *self.dfg[block].instructions_mut() = filtered_instructions; } - Ok(self) + Ok(()) } } diff --git a/compiler/noirc_evaluator/src/ssa/opt/constant_folding.rs b/compiler/noirc_evaluator/src/ssa/opt/constant_folding.rs index ff9a63c8d79..ea422fdff09 100644 --- a/compiler/noirc_evaluator/src/ssa/opt/constant_folding.rs +++ b/compiler/noirc_evaluator/src/ssa/opt/constant_folding.rs @@ -44,7 +44,7 @@ impl Ssa { #[tracing::instrument(level = "trace", skip(self))] pub(crate) fn fold_constants(mut self) -> Ssa { for function in self.functions.values_mut() { - constant_fold(function, false); + function.constant_fold(false); } self } @@ -57,25 +57,27 @@ impl Ssa { #[tracing::instrument(level = "trace", skip(self))] pub(crate) fn fold_constants_using_constraints(mut self) -> Ssa { for function in self.functions.values_mut() { - constant_fold(function, true); + function.constant_fold(true); } self } } -/// The structure of this pass is simple: -/// Go through each block and re-insert all instructions. -fn constant_fold(function: &mut Function, use_constraint_info: bool) { - let mut context = Context { use_constraint_info, ..Default::default() }; - context.block_queue.push(function.entry_block()); +impl Function { + /// The structure of this pass is simple: + /// Go through each block and re-insert all instructions. + pub(crate) fn constant_fold(&mut self, use_constraint_info: bool) { + let mut context = Context { use_constraint_info, ..Default::default() }; + context.block_queue.push(self.entry_block()); - while let Some(block) = context.block_queue.pop() { - if context.visited_blocks.contains(&block) { - continue; - } + while let Some(block) = context.block_queue.pop() { + if context.visited_blocks.contains(&block) { + continue; + } - context.visited_blocks.insert(block); - context.fold_constants_in_block(function, block); + context.visited_blocks.insert(block); + context.fold_constants_in_block(self, block); + } } } diff --git a/compiler/noirc_evaluator/src/ssa/opt/die.rs b/compiler/noirc_evaluator/src/ssa/opt/die.rs index 54af2e9ad5d..beca7c41e5c 100644 --- a/compiler/noirc_evaluator/src/ssa/opt/die.rs +++ b/compiler/noirc_evaluator/src/ssa/opt/die.rs @@ -25,44 +25,46 @@ impl Ssa { #[tracing::instrument(level = "trace", skip(self))] pub(crate) fn dead_instruction_elimination(mut self) -> Ssa { for function in self.functions.values_mut() { - dead_instruction_elimination(function, true); + function.dead_instruction_elimination(true); } self } } -/// Removes any unused instructions in the reachable blocks of the given function. -/// -/// The blocks of the function are iterated in post order, such that any blocks containing -/// instructions that reference results from an instruction in another block are evaluated first. -/// If we did not iterate blocks in this order we could not safely say whether or not the results -/// of its instructions are needed elsewhere. -fn dead_instruction_elimination(function: &mut Function, insert_out_of_bounds_checks: bool) { - let mut context = Context::default(); - for call_data in &function.dfg.data_bus.call_data { - context.mark_used_instruction_results(&function.dfg, call_data.array_id); - } +impl Function { + /// Removes any unused instructions in the reachable blocks of the given function. + /// + /// The blocks of the function are iterated in post order, such that any blocks containing + /// instructions that reference results from an instruction in another block are evaluated first. + /// If we did not iterate blocks in this order we could not safely say whether or not the results + /// of its instructions are needed elsewhere. + pub(crate) fn dead_instruction_elimination(&mut self, insert_out_of_bounds_checks: bool) { + let mut context = Context::default(); + for call_data in &self.dfg.data_bus.call_data { + context.mark_used_instruction_results(&self.dfg, call_data.array_id); + } - let mut inserted_out_of_bounds_checks = false; + let mut inserted_out_of_bounds_checks = false; - let blocks = PostOrder::with_function(function); - for block in blocks.as_slice() { - inserted_out_of_bounds_checks |= context.remove_unused_instructions_in_block( - function, - *block, - insert_out_of_bounds_checks, - ); - } + let blocks = PostOrder::with_function(self); + for block in blocks.as_slice() { + inserted_out_of_bounds_checks |= context.remove_unused_instructions_in_block( + self, + *block, + insert_out_of_bounds_checks, + ); + } - // If we inserted out of bounds check, let's run the pass again with those new - // instructions (we don't want to remove those checks, or instructions that are - // dependencies of those checks) - if inserted_out_of_bounds_checks { - dead_instruction_elimination(function, false); - return; - } + // If we inserted out of bounds check, let's run the pass again with those new + // instructions (we don't want to remove those checks, or instructions that are + // dependencies of those checks) + if inserted_out_of_bounds_checks { + self.dead_instruction_elimination(false); + return; + } - context.remove_rc_instructions(&mut function.dfg); + context.remove_rc_instructions(&mut self.dfg); + } } /// Per function context for tracking unused values and which instructions to remove. diff --git a/compiler/noirc_evaluator/src/ssa/opt/mem2reg.rs b/compiler/noirc_evaluator/src/ssa/opt/mem2reg.rs index 68c04e3b4b4..165e0e36b71 100644 --- a/compiler/noirc_evaluator/src/ssa/opt/mem2reg.rs +++ b/compiler/noirc_evaluator/src/ssa/opt/mem2reg.rs @@ -123,16 +123,22 @@ impl Ssa { #[tracing::instrument(level = "trace", skip(self))] pub(crate) fn mem2reg(mut self) -> Ssa { for function in self.functions.values_mut() { - let mut context = PerFunctionContext::new(function); - context.mem2reg(); - context.remove_instructions(); - context.update_data_bus(); + function.mem2reg(); } self } } +impl Function { + pub(crate) fn mem2reg(&mut self) { + let mut context = PerFunctionContext::new(self); + context.mem2reg(); + context.remove_instructions(); + context.update_data_bus(); + } +} + struct PerFunctionContext<'f> { cfg: ControlFlowGraph, post_order: PostOrder, diff --git a/compiler/noirc_evaluator/src/ssa/opt/rc.rs b/compiler/noirc_evaluator/src/ssa/opt/rc.rs index 06025fd9e8b..c879f6c8fff 100644 --- a/compiler/noirc_evaluator/src/ssa/opt/rc.rs +++ b/compiler/noirc_evaluator/src/ssa/opt/rc.rs @@ -22,7 +22,7 @@ impl Ssa { #[tracing::instrument(level = "trace", skip(self))] pub(crate) fn remove_paired_rc(mut self) -> Ssa { for function in self.functions.values_mut() { - remove_paired_rc(function); + function.remove_paired_rc(); } self } @@ -44,26 +44,28 @@ pub(crate) struct RcInstruction { pub(crate) possibly_mutated: bool, } -/// This function is very simplistic for now. It takes advantage of the fact that dec_rc -/// instructions are currently issued only at the end of a function for parameters and will -/// only check the first and last block for inc & dec rc instructions to be removed. The rest -/// of the function is still checked for array_set instructions. -/// -/// This restriction lets this function largely ignore merging intermediate results from other -/// blocks and handling loops. -fn remove_paired_rc(function: &mut Function) { - // `dec_rc` is only issued for parameters currently so we can speed things - // up a bit by skipping any functions without them. - if !contains_array_parameter(function) { - return; - } +impl Function { + /// This function is very simplistic for now. It takes advantage of the fact that dec_rc + /// instructions are currently issued only at the end of a function for parameters and will + /// only check the first and last block for inc & dec rc instructions to be removed. The rest + /// of the function is still checked for array_set instructions. + /// + /// This restriction lets this function largely ignore merging intermediate results from other + /// blocks and handling loops. + pub(crate) fn remove_paired_rc(&mut self) { + // `dec_rc` is only issued for parameters currently so we can speed things + // up a bit by skipping any functions without them. + if !contains_array_parameter(self) { + return; + } - let mut context = Context::default(); + let mut context = Context::default(); - context.find_rcs_in_entry_block(function); - context.scan_for_array_sets(function); - let to_remove = context.find_rcs_to_remove(function); - remove_instructions(to_remove, function); + context.find_rcs_in_entry_block(self); + context.scan_for_array_sets(self); + let to_remove = context.find_rcs_to_remove(self); + remove_instructions(to_remove, self); + } } fn contains_array_parameter(function: &mut Function) -> bool { diff --git a/compiler/noirc_evaluator/src/ssa/opt/remove_bit_shifts.rs b/compiler/noirc_evaluator/src/ssa/opt/remove_bit_shifts.rs index 984c0de04ca..4b2d753f072 100644 --- a/compiler/noirc_evaluator/src/ssa/opt/remove_bit_shifts.rs +++ b/compiler/noirc_evaluator/src/ssa/opt/remove_bit_shifts.rs @@ -21,24 +21,30 @@ impl Ssa { #[tracing::instrument(level = "trace", skip(self))] pub(crate) fn remove_bit_shifts(mut self) -> Ssa { for function in self.functions.values_mut() { - remove_bit_shifts(function); + function.remove_bit_shifts(); } self } } -/// The structure of this pass is simple: -/// Go through each block and re-insert all instructions. -fn remove_bit_shifts(function: &mut Function) { - if let RuntimeType::Brillig = function.runtime() { - return; - } +impl Function { + /// The structure of this pass is simple: + /// Go through each block and re-insert all instructions. + pub(crate) fn remove_bit_shifts(&mut self) { + if let RuntimeType::Brillig = self.runtime() { + return; + } - let block = function.entry_block(); - let mut context = - Context { function, new_instructions: Vec::new(), block, call_stack: CallStack::default() }; + let block = self.entry_block(); + let mut context = Context { + function: self, + new_instructions: Vec::new(), + block, + call_stack: CallStack::default(), + }; - context.remove_bit_shifts(); + context.remove_bit_shifts(); + } } struct Context<'f> { diff --git a/compiler/noirc_evaluator/src/ssa/opt/remove_enable_side_effects.rs b/compiler/noirc_evaluator/src/ssa/opt/remove_enable_side_effects.rs index a56786b2603..c611ab682bf 100644 --- a/compiler/noirc_evaluator/src/ssa/opt/remove_enable_side_effects.rs +++ b/compiler/noirc_evaluator/src/ssa/opt/remove_enable_side_effects.rs @@ -29,23 +29,25 @@ impl Ssa { #[tracing::instrument(level = "trace", skip(self))] pub(crate) fn remove_enable_side_effects(mut self) -> Ssa { for function in self.functions.values_mut() { - remove_enable_side_effects(function); + function.remove_enable_side_effects(); } self } } -fn remove_enable_side_effects(function: &mut Function) { - let mut context = Context::default(); - context.block_queue.push(function.entry_block()); +impl Function { + pub(crate) fn remove_enable_side_effects(&mut self) { + let mut context = Context::default(); + context.block_queue.push(self.entry_block()); - while let Some(block) = context.block_queue.pop() { - if context.visited_blocks.contains(&block) { - continue; - } + while let Some(block) = context.block_queue.pop() { + if context.visited_blocks.contains(&block) { + continue; + } - context.visited_blocks.insert(block); - context.remove_enable_side_effects_in_block(function, block); + context.visited_blocks.insert(block); + context.remove_enable_side_effects_in_block(self, block); + } } } diff --git a/compiler/noirc_evaluator/src/ssa/opt/remove_if_else.rs b/compiler/noirc_evaluator/src/ssa/opt/remove_if_else.rs index 8d6225afabf..9f01800bca6 100644 --- a/compiler/noirc_evaluator/src/ssa/opt/remove_if_else.rs +++ b/compiler/noirc_evaluator/src/ssa/opt/remove_if_else.rs @@ -28,17 +28,23 @@ impl Ssa { #[tracing::instrument(level = "trace", skip(self))] pub(crate) fn remove_if_else(mut self) -> Ssa { for function in self.functions.values_mut() { - // This should match the check in flatten_cfg - if let crate::ssa::ir::function::RuntimeType::Brillig = function.runtime() { - continue; - } - - Context::default().remove_if_else(function); + function.remove_if_else(); } self } } +impl Function { + pub(crate) fn remove_if_else(&mut self) { + // This should match the check in flatten_cfg + if let crate::ssa::ir::function::RuntimeType::Brillig = self.runtime() { + // skip + } else { + Context::default().remove_if_else(self); + } + } +} + #[derive(Default)] struct Context { slice_sizes: HashMap, diff --git a/compiler/noirc_evaluator/src/ssa/opt/resolve_is_unconstrained.rs b/compiler/noirc_evaluator/src/ssa/opt/resolve_is_unconstrained.rs index 2c9e33ae528..1768cbddec3 100644 --- a/compiler/noirc_evaluator/src/ssa/opt/resolve_is_unconstrained.rs +++ b/compiler/noirc_evaluator/src/ssa/opt/resolve_is_unconstrained.rs @@ -17,40 +17,42 @@ impl Ssa { #[tracing::instrument(level = "trace", skip(self))] pub(crate) fn resolve_is_unconstrained(mut self) -> Self { for func in self.functions.values_mut() { - replace_is_unconstrained_result(func); + func.replace_is_unconstrained_result(); } self } } -fn replace_is_unconstrained_result(func: &mut Function) { - let mut is_unconstrained_calls = HashSet::default(); - // Collect all calls to is_unconstrained - for block_id in func.reachable_blocks() { - for &instruction_id in func.dfg[block_id].instructions() { - let target_func = match &func.dfg[instruction_id] { - Instruction::Call { func, .. } => *func, - _ => continue, - }; +impl Function { + pub(crate) fn replace_is_unconstrained_result(&mut self) { + let mut is_unconstrained_calls = HashSet::default(); + // Collect all calls to is_unconstrained + for block_id in self.reachable_blocks() { + for &instruction_id in self.dfg[block_id].instructions() { + let target_func = match &self.dfg[instruction_id] { + Instruction::Call { func, .. } => *func, + _ => continue, + }; - if let Value::Intrinsic(Intrinsic::IsUnconstrained) = &func.dfg[target_func] { - is_unconstrained_calls.insert(instruction_id); + if let Value::Intrinsic(Intrinsic::IsUnconstrained) = &self.dfg[target_func] { + is_unconstrained_calls.insert(instruction_id); + } } } - } - for instruction_id in is_unconstrained_calls { - let call_returns = func.dfg.instruction_results(instruction_id); - let original_return_id = call_returns[0]; + for instruction_id in is_unconstrained_calls { + let call_returns = self.dfg.instruction_results(instruction_id); + let original_return_id = call_returns[0]; - // We replace the result with a fresh id. This will be unused, so the DIE pass will remove the leftover intrinsic call. - func.dfg.replace_result(instruction_id, original_return_id); + // We replace the result with a fresh id. This will be unused, so the DIE pass will remove the leftover intrinsic call. + self.dfg.replace_result(instruction_id, original_return_id); - let is_within_unconstrained = func.dfg.make_constant( - FieldElement::from(matches!(func.runtime(), RuntimeType::Brillig)), - Type::bool(), - ); - // Replace all uses of the original return value with the constant - func.dfg.set_value_from_id(original_return_id, is_within_unconstrained); + let is_within_unconstrained = self.dfg.make_constant( + FieldElement::from(matches!(self.runtime(), RuntimeType::Brillig)), + Type::bool(), + ); + // Replace all uses of the original return value with the constant + self.dfg.set_value_from_id(original_return_id, is_within_unconstrained); + } } } diff --git a/compiler/noirc_evaluator/src/ssa/opt/simplify_cfg.rs b/compiler/noirc_evaluator/src/ssa/opt/simplify_cfg.rs index 6887873dbc3..b77bc8c72f3 100644 --- a/compiler/noirc_evaluator/src/ssa/opt/simplify_cfg.rs +++ b/compiler/noirc_evaluator/src/ssa/opt/simplify_cfg.rs @@ -36,48 +36,51 @@ impl Ssa { #[tracing::instrument(level = "trace", skip(self))] pub(crate) fn simplify_cfg(mut self) -> Self { for function in self.functions.values_mut() { - simplify_function(function); + function.simplify_function(); } self } } -/// Simplify a function's cfg by going through each block to check for any simple blocks that can -/// be inlined into their predecessor. -fn simplify_function(function: &mut Function) { - let mut cfg = ControlFlowGraph::with_function(function); - let mut stack = vec![function.entry_block()]; - let mut visited = HashSet::new(); - - while let Some(block) = stack.pop() { - if visited.insert(block) { - stack.extend(function.dfg[block].successors().filter(|block| !visited.contains(block))); - } +impl Function { + /// Simplify a function's cfg by going through each block to check for any simple blocks that can + /// be inlined into their predecessor. + pub(crate) fn simplify_function(&mut self) { + let mut cfg = ControlFlowGraph::with_function(self); + let mut stack = vec![self.entry_block()]; + let mut visited = HashSet::new(); + + while let Some(block) = stack.pop() { + if visited.insert(block) { + stack.extend(self.dfg[block].successors().filter(|block| !visited.contains(block))); + } - // This call is before try_inline_into_predecessor so that if it succeeds in changing a - // jmpif into a jmp, the block may then be inlined entirely into its predecessor in try_inline_into_predecessor. - check_for_constant_jmpif(function, block, &mut cfg); + // This call is before try_inline_into_predecessor so that if it succeeds in changing a + // jmpif into a jmp, the block may then be inlined entirely into its predecessor in try_inline_into_predecessor. + check_for_constant_jmpif(self, block, &mut cfg); - let mut predecessors = cfg.predecessors(block); + let mut predecessors = cfg.predecessors(block); - if predecessors.len() == 1 { - let predecessor = predecessors.next().expect("Already checked length of predecessors"); - drop(predecessors); + if predecessors.len() == 1 { + let predecessor = + predecessors.next().expect("Already checked length of predecessors"); + drop(predecessors); - // If the block has only 1 predecessor, we can safely remove its block parameters - remove_block_parameters(function, block, predecessor); + // If the block has only 1 predecessor, we can safely remove its block parameters + remove_block_parameters(self, block, predecessor); - // Note: this function relies on `remove_block_parameters` being called first. - // Otherwise the inlined block will refer to parameters that no longer exist. - // - // If successful, `block` will be empty and unreachable after this call, so any - // optimizations performed after this point on the same block should check if - // the inlining here was successful before continuing. - try_inline_into_predecessor(function, &mut cfg, block, predecessor); - } else { - drop(predecessors); + // Note: this function relies on `remove_block_parameters` being called first. + // Otherwise the inlined block will refer to parameters that no longer exist. + // + // If successful, `block` will be empty and unreachable after this call, so any + // optimizations performed after this point on the same block should check if + // the inlining here was successful before continuing. + try_inline_into_predecessor(self, &mut cfg, block, predecessor); + } else { + drop(predecessors); - check_for_double_jmp(function, block, &mut cfg); + check_for_double_jmp(self, block, &mut cfg); + } } } } diff --git a/compiler/noirc_evaluator/src/ssa/opt/unrolling.rs b/compiler/noirc_evaluator/src/ssa/opt/unrolling.rs index f3e11e04e3a..d6ed11ddf0e 100644 --- a/compiler/noirc_evaluator/src/ssa/opt/unrolling.rs +++ b/compiler/noirc_evaluator/src/ssa/opt/unrolling.rs @@ -74,16 +74,49 @@ impl Ssa { pub(crate) fn try_to_unroll_loops(mut self) -> (Ssa, Vec) { let mut errors = vec![]; for function in self.functions.values_mut() { - // Loop unrolling in brillig can lead to a code explosion currently. This can - // also be true for ACIR, but we have no alternative to unrolling in ACIR. - // Brillig also generally prefers smaller code rather than faster code. - if function.runtime() == RuntimeType::Brillig { - continue; + function.try_to_unroll_loops(&mut errors); + } + (self, errors) + } +} + +impl Function { + // TODO(https://github.com/noir-lang/noir/issues/6192): are both this and + // TODO: Ssa::unroll_loops_iteratively needed? Likely able to be combined + pub(crate) fn unroll_loops_iteratively(&mut self) -> Result<(), RuntimeError> { + // Try to unroll loops first: + let mut unroll_errors = vec![]; + self.try_to_unroll_loops(&mut unroll_errors); + + // Keep unrolling until no more errors are found + while !unroll_errors.is_empty() { + let prev_unroll_err_count = unroll_errors.len(); + + // Simplify the SSA before retrying + + // Do a mem2reg after the last unroll to aid simplify_cfg + self.mem2reg(); + self.simplify_function(); + // Do another mem2reg after simplify_cfg to aid the next unroll + self.mem2reg(); + + // Unroll again + self.try_to_unroll_loops(&mut unroll_errors); + // If we didn't manage to unroll any more loops, exit + if unroll_errors.len() >= prev_unroll_err_count { + return Err(unroll_errors.swap_remove(0)); } + } + Ok(()) + } - errors.extend(find_all_loops(function).unroll_each_loop(function)); + pub(crate) fn try_to_unroll_loops(&mut self, errors: &mut Vec) { + // Loop unrolling in brillig can lead to a code explosion currently. This can + // also be true for ACIR, but we have no alternative to unrolling in ACIR. + // Brillig also generally prefers smaller code rather than faster code. + if self.runtime() != RuntimeType::Brillig { + errors.extend(find_all_loops(self).unroll_each_loop(self)); } - (self, errors) } }