diff --git a/compiler/noirc_evaluator/src/ssa.rs b/compiler/noirc_evaluator/src/ssa.rs index 0c4e42f09ef..33cfb918ece 100644 --- a/compiler/noirc_evaluator/src/ssa.rs +++ b/compiler/noirc_evaluator/src/ssa.rs @@ -33,6 +33,7 @@ use noirc_frontend::{ hir_def::{function::FunctionSignature, types::Type as HirType}, monomorphization::ast::Program, }; +use opt::unrolling::UnrollMode; use tracing::{span, Level}; use self::{ @@ -86,48 +87,14 @@ pub(crate) fn optimize_into_acir( let ssa_gen_span = span!(Level::TRACE, "ssa_generation"); let ssa_gen_span_guard = ssa_gen_span.enter(); - let mut ssa = SsaBuilder::new( + let builder = SsaBuilder::new( program, options.enable_ssa_logging, options.force_brillig_output, options.print_codegen_timings, &options.emit_ssa, - )? - .run_pass(Ssa::defunctionalize, "After Defunctionalization:") - .run_pass(Ssa::remove_paired_rc, "After Removing Paired rc_inc & rc_decs:") - .run_pass(Ssa::separate_runtime, "After Runtime Separation:") - .run_pass(Ssa::resolve_is_unconstrained, "After Resolving IsUnconstrained:") - .run_pass(|ssa| ssa.inline_functions(options.inliner_aggressiveness), "After Inlining:") - // Run mem2reg with the CFG separated into blocks - .run_pass(Ssa::mem2reg, "After Mem2Reg (1st):") - .run_pass(Ssa::simplify_cfg, "After Simplifying (1st):") - .run_pass(Ssa::as_slice_optimization, "After `as_slice` optimization") - .try_run_pass( - Ssa::evaluate_static_assert_and_assert_constant, - "After `static_assert` and `assert_constant`:", - )? - .try_run_pass(Ssa::unroll_loops_iteratively, "After Unrolling:")? - .run_pass(Ssa::simplify_cfg, "After Simplifying (2nd):") - .run_pass(Ssa::flatten_cfg, "After Flattening:") - .run_pass(Ssa::remove_bit_shifts, "After Removing Bit Shifts:") - // Run mem2reg once more with the flattened CFG to catch any remaining loads/stores - .run_pass(Ssa::mem2reg, "After Mem2Reg (2nd):") - // Run the inlining pass again to handle functions with `InlineType::NoPredicates`. - // Before flattening is run, we treat functions marked with the `InlineType::NoPredicates` as an entry point. - // This pass must come immediately following `mem2reg` as the succeeding passes - // may create an SSA which inlining fails to handle. - .run_pass( - |ssa| ssa.inline_functions_with_no_predicates(options.inliner_aggressiveness), - "After Inlining:", - ) - .run_pass(Ssa::remove_if_else, "After Remove IfElse:") - .run_pass(Ssa::fold_constants, "After Constant Folding:") - .run_pass(Ssa::remove_enable_side_effects, "After EnableSideEffectsIf removal:") - .run_pass(Ssa::fold_constants_using_constraints, "After Constraint Folding:") - .run_pass(Ssa::dead_instruction_elimination, "After Dead Instruction Elimination:") - .run_pass(Ssa::simplify_cfg, "After Simplifying:") - .run_pass(Ssa::array_set_optimization, "After Array Set Optimizations:") - .finish(); + )?; + let mut ssa = optimize_ssa(builder, options.inliner_aggressiveness)?; let ssa_level_warnings = if options.skip_underconstrained_check { vec![] @@ -149,6 +116,71 @@ pub(crate) fn optimize_into_acir( Ok(ArtifactsAndWarnings(artifacts, ssa_level_warnings)) } +fn optimize_ssa(builder: SsaBuilder, inliner_aggressiveness: i64) -> Result { + let builder = builder + .run_pass(Ssa::defunctionalize, "After Defunctionalization:") + .run_pass(Ssa::remove_paired_rc, "After Removing Paired rc_inc & rc_decs:") + .run_pass(Ssa::separate_runtime, "After Runtime Separation:") + .run_pass(Ssa::resolve_is_unconstrained, "After Resolving IsUnconstrained:") + .run_pass(|ssa| ssa.inline_functions(inliner_aggressiveness), "After Inlining:") + .run_pass( + |ssa| ssa.inline_const_brillig_calls(inliner_aggressiveness), + "After Inlining Const Brillig Calls:", + ); + let ssa = optimize_ssa_after_inline_const_brillig_calls( + builder, + inliner_aggressiveness, + true, // inline functions with no predicates + UnrollMode::Acir, + )?; + Ok(ssa) +} + +fn optimize_ssa_after_inline_const_brillig_calls( + builder: SsaBuilder, + inliner_aggressiveness: i64, + inline_functions_with_no_predicates: bool, + unroll_mode: UnrollMode, +) -> Result { + let builder = builder + // Run mem2reg with the CFG separated into blocks + .run_pass(Ssa::mem2reg, "After Mem2Reg (1st):") + .run_pass(Ssa::simplify_cfg, "After Simplifying (1st):") + .run_pass(Ssa::as_slice_optimization, "After `as_slice` optimization") + .try_run_pass( + Ssa::evaluate_static_assert_and_assert_constant, + "After `static_assert` and `assert_constant`:", + )? + .try_run_pass(|ssa| Ssa::unroll_loops_iteratively(ssa, unroll_mode), "After Unrolling:")? + .run_pass(Ssa::simplify_cfg, "After Simplifying (2nd):") + .run_pass(Ssa::flatten_cfg, "After Flattening:") + .run_pass(Ssa::remove_bit_shifts, "After Removing Bit Shifts:") + // Run mem2reg once more with the flattened CFG to catch any remaining loads/stores + .run_pass(Ssa::mem2reg, "After Mem2Reg (2nd):"); + let builder = if inline_functions_with_no_predicates { + // Run the inlining pass again to handle functions with `InlineType::NoPredicates`. + // Before flattening is run, we treat functions marked with the `InlineType::NoPredicates` as an entry point. + // This pass must come immediately following `mem2reg` as the succeeding passes + // may create an SSA which inlining fails to handle. + builder.run_pass( + |ssa| ssa.inline_functions_with_no_predicates(inliner_aggressiveness), + "After Inlining:", + ) + } else { + builder + }; + let ssa = builder + .run_pass(Ssa::remove_if_else, "After Remove IfElse:") + .run_pass(Ssa::fold_constants, "After Constant Folding:") + .run_pass(Ssa::remove_enable_side_effects, "After EnableSideEffectsIf removal:") + .run_pass(Ssa::fold_constants_using_constraints, "After Constraint Folding:") + .run_pass(Ssa::dead_instruction_elimination, "After Dead Instruction Elimination:") + .run_pass(Ssa::simplify_cfg, "After Simplifying:") + .run_pass(Ssa::array_set_optimization, "After Array Set Optimizations:") + .finish(); + Ok(ssa) +} + // Helper to time SSA passes fn time(name: &str, print_timings: bool, f: impl FnOnce() -> T) -> T { let start_time = chrono::Utc::now().time(); @@ -428,11 +460,10 @@ impl SsaBuilder { } /// The same as `run_pass` but for passes that may fail - fn try_run_pass( - mut self, - pass: fn(Ssa) -> Result, - msg: &str, - ) -> Result { + fn try_run_pass(mut self, pass: F, msg: &str) -> Result + where + F: FnOnce(Ssa) -> Result, + { self.ssa = time(msg, self.print_codegen_timings, || pass(self.ssa))?; Ok(self.print(msg)) } diff --git a/compiler/noirc_evaluator/src/ssa/opt/flatten_cfg.rs b/compiler/noirc_evaluator/src/ssa/opt/flatten_cfg.rs index 984f639df00..bc7b8f2d88a 100644 --- a/compiler/noirc_evaluator/src/ssa/opt/flatten_cfg.rs +++ b/compiler/noirc_evaluator/src/ssa/opt/flatten_cfg.rs @@ -250,7 +250,10 @@ struct ConditionalContext { call_stack: CallStack, } -fn flatten_function_cfg(function: &mut Function, no_predicates: &HashMap) { +pub(crate) fn flatten_function_cfg( + function: &mut Function, + no_predicates: &HashMap, +) { // This pass may run forever on a brillig function. // Analyze will check if the predecessors have been processed and push the block to the back of // the queue. This loops forever if there are still any loops present in the program. diff --git a/compiler/noirc_evaluator/src/ssa/opt/inline_const_brillig_calls.rs b/compiler/noirc_evaluator/src/ssa/opt/inline_const_brillig_calls.rs new file mode 100644 index 00000000000..3b28c78e7fe --- /dev/null +++ b/compiler/noirc_evaluator/src/ssa/opt/inline_const_brillig_calls.rs @@ -0,0 +1,241 @@ +//! This pass tries to inline calls to brillig functions that have all constant arguments. +use std::collections::{BTreeMap, HashSet}; + +use acvm::acir::circuit::ErrorSelector; +use noirc_frontend::{monomorphization::ast::InlineType, Type}; + +use crate::{ + errors::RuntimeError, + ssa::{ + ir::{ + function::{Function, FunctionId, RuntimeType}, + instruction::{Instruction, InstructionId, TerminatorInstruction}, + value::{Value, ValueId}, + }, + optimize_ssa_after_inline_const_brillig_calls, Ssa, SsaBuilder, UnrollMode, + }, +}; + +impl Ssa { + #[tracing::instrument(level = "trace", skip(self))] + pub(crate) fn inline_const_brillig_calls(mut self, inliner_aggressiveness: i64) -> Self { + let error_selector_to_type = &self.error_selector_to_type; + + // Collect all brillig functions so that later we can find them when processing a call instruction + let mut brillig_functions = BTreeMap::::new(); + for (func_id, func) in &self.functions { + if let RuntimeType::Brillig(..) = func.runtime() { + let cloned_function = Function::clone_with_id(*func_id, func); + brillig_functions.insert(*func_id, cloned_function); + }; + } + + // Keep track of which brillig functions we couldn't completely inline: we'll remove the ones we could. + let mut brillig_functions_we_could_not_inline = HashSet::new(); + + for func in self.functions.values_mut() { + func.inline_const_brillig_calls( + &brillig_functions, + &mut brillig_functions_we_could_not_inline, + inliner_aggressiveness, + error_selector_to_type, + ); + } + + // Remove the brillig functions that are no longer called + for func_id in brillig_functions.keys() { + // We never want to remove the main function (it could be `unconstrained` or it + // could have been turned into brillig if `--force-brillig` was given) + if self.main_id == *func_id { + continue; + } + + if brillig_functions_we_could_not_inline.contains(func_id) { + continue; + } + + // We also don't want to remove entry points + if self.entry_point_to_generated_index.contains_key(func_id) { + continue; + } + + self.functions.remove(func_id); + } + + self + } +} + +/// Result of trying to optimize an instruction (any instruction) in this pass. +enum OptimizeResult { + /// Nothing was done because the instruction wasn't a call to a brillig function, + /// or some arguments to it were not constants. + NotABrilligCall, + /// The instruction was a call to a brillig function, but we couldn't optimize it. + CannotOptimize(FunctionId), + /// The instruction was a call to a brillig function and we were able to optimize it, + /// returning the optimized function and the constant values it returned. + Optimized(Function, Vec), +} + +impl Function { + pub(crate) fn inline_const_brillig_calls( + &mut self, + brillig_functions: &BTreeMap, + brillig_functions_we_could_not_inline: &mut HashSet, + inliner_aggressiveness: i64, + error_selector_to_type: &BTreeMap, + ) { + for block_id in self.reachable_blocks() { + for instruction_id in self.dfg[block_id].take_instructions() { + let optimize_result = self.optimize_const_brillig_call( + instruction_id, + brillig_functions, + brillig_functions_we_could_not_inline, + inliner_aggressiveness, + error_selector_to_type, + ); + match optimize_result { + OptimizeResult::NotABrilligCall => { + self.dfg[block_id].instructions_mut().push(instruction_id); + } + OptimizeResult::CannotOptimize(func_id) => { + self.dfg[block_id].instructions_mut().push(instruction_id); + brillig_functions_we_could_not_inline.insert(func_id); + } + OptimizeResult::Optimized(function, return_values) => { + // Replace the instruction results with the constant values we got + let current_results = self.dfg.instruction_results(instruction_id).to_vec(); + assert_eq!(return_values.len(), current_results.len()); + + for (current_result_id, return_value_id) in + current_results.iter().zip(return_values) + { + let new_return_value_id = + function.copy_constant_to_function(return_value_id, self); + self.dfg.set_value_from_id(*current_result_id, new_return_value_id); + } + } + } + } + } + } + + /// Tries to optimize an instruction if it's a call that points to a brillig function, + /// and all its arguments are constant. + fn optimize_const_brillig_call( + &mut self, + instruction_id: InstructionId, + brillig_functions: &BTreeMap, + brillig_functions_we_could_not_inline: &mut HashSet, + inliner_aggressiveness: i64, + error_selector_to_type: &BTreeMap, + ) -> OptimizeResult { + let instruction = &self.dfg[instruction_id]; + let Instruction::Call { func: func_id, arguments } = instruction else { + return OptimizeResult::NotABrilligCall; + }; + + let func_value = &self.dfg[*func_id]; + let Value::Function(func_id) = func_value else { + return OptimizeResult::NotABrilligCall; + }; + let func_id = *func_id; + + let Some(function) = brillig_functions.get(&func_id) else { + return OptimizeResult::NotABrilligCall; + }; + + if !arguments.iter().all(|argument| self.dfg.is_constant(*argument)) { + return OptimizeResult::CannotOptimize(func_id); + } + + // The function we have is already a copy of the original function, but we need to clone + // it again because there might be multiple calls to the same brillig function. + let mut function = Function::clone_with_id(func_id, function); + + // Find the entry block and remove its parameters + let entry_block_id = function.entry_block(); + let entry_block = &mut function.dfg[entry_block_id]; + let entry_block_parameters = entry_block.take_parameters(); + + assert_eq!(arguments.len(), entry_block_parameters.len()); + + // Replace the ValueId of parameters with the ValueId of arguments + for (parameter_id, argument_id) in entry_block_parameters.iter().zip(arguments) { + // Lookup the argument in the current function and insert it in the function copy + let new_argument_id = self.copy_constant_to_function(*argument_id, &mut function); + function.dfg.set_value_from_id(*parameter_id, new_argument_id); + } + + // Try to fully optimize the function. If we can't, we can't inline it's constant value. + let Ok(mut function) = optimize(function, inliner_aggressiveness, error_selector_to_type) + else { + return OptimizeResult::CannotOptimize(func_id); + }; + + let entry_block = &mut function.dfg[entry_block_id]; + + // If the entry block has instructions, we can't inline it (we need a terminator) + if !entry_block.instructions().is_empty() { + brillig_functions_we_could_not_inline.insert(func_id); + return OptimizeResult::CannotOptimize(func_id); + } + + let terminator = entry_block.take_terminator(); + let TerminatorInstruction::Return { return_values, call_stack: _ } = terminator else { + return OptimizeResult::CannotOptimize(func_id); + }; + + // Sanity check: make sure all returned values are constant + if !return_values.iter().all(|value_id| function.dfg.is_constant(*value_id)) { + return OptimizeResult::CannotOptimize(func_id); + } + + OptimizeResult::Optimized(function, return_values) + } + + /// Copies a constant from this function to another one. + /// Though it might seem we can just take a value out of `self` and call `make_value` on `function`, + /// if the constant is an array the values will still keep pointing to `self`. So, this function + /// recursively copies the array values too. + fn copy_constant_to_function(&self, constant_id: ValueId, function: &mut Function) -> ValueId { + if let Some((constant, typ)) = self.dfg.get_numeric_constant_with_type(constant_id) { + function.dfg.make_constant(constant, typ) + } else if let Some((constants, typ)) = self.dfg.get_array_constant(constant_id) { + let new_constants = constants + .iter() + .map(|constant_id| self.copy_constant_to_function(*constant_id, function)) + .collect(); + function.dfg.make_array(new_constants, typ) + } else { + unreachable!("A constant should be either a numeric constant or an array constant") + } + } +} + +/// Optimizes a function by running the same passes as `optimize_into_acir` +/// after the `inline_const_brillig_calls` pass. +/// The function is changed to be an ACIR function so the function can potentially +/// be optimized into a single return terminator. +fn optimize( + mut function: Function, + inliner_aggressiveness: i64, + error_selector_to_type: &BTreeMap, +) -> Result { + function.set_runtime(RuntimeType::Acir(InlineType::InlineAlways)); + + let ssa = Ssa::new(vec![function], error_selector_to_type.clone()); + let builder = SsaBuilder { ssa, print_ssa_passes: false, print_codegen_timings: false }; + let mut ssa = optimize_ssa_after_inline_const_brillig_calls( + builder, + inliner_aggressiveness, + // Don't inline functions with no predicates. + // The reason for this is that in this specific context the `Ssa` object only holds + // a single function. For inlining to work we need to know all other functions that + // exist (so we can inline them). Here we choose to skip this optimization for simplicity reasons. + false, + UnrollMode::Brillig, + )?; + Ok(ssa.functions.pop_first().unwrap().1) +} diff --git a/compiler/noirc_evaluator/src/ssa/opt/mod.rs b/compiler/noirc_evaluator/src/ssa/opt/mod.rs index f3dbd58fa69..9cd71e08973 100644 --- a/compiler/noirc_evaluator/src/ssa/opt/mod.rs +++ b/compiler/noirc_evaluator/src/ssa/opt/mod.rs @@ -10,6 +10,7 @@ mod constant_folding; mod defunctionalize; mod die; pub(crate) mod flatten_cfg; +mod inline_const_brillig_calls; mod inlining; mod mem2reg; mod normalize_value_ids; @@ -20,4 +21,4 @@ mod remove_if_else; mod resolve_is_unconstrained; mod runtime_separation; mod simplify_cfg; -mod unrolling; +pub(crate) mod unrolling; diff --git a/compiler/noirc_evaluator/src/ssa/opt/unrolling.rs b/compiler/noirc_evaluator/src/ssa/opt/unrolling.rs index 661109c1786..55681618853 100644 --- a/compiler/noirc_evaluator/src/ssa/opt/unrolling.rs +++ b/compiler/noirc_evaluator/src/ssa/opt/unrolling.rs @@ -37,13 +37,29 @@ use crate::{ }; use fxhash::FxHashMap as HashMap; +/// In this mode we are unrolling. +#[derive(Debug, Clone, Copy)] +pub(crate) enum UnrollMode { + /// This is the normal unroll mode, where we are unrolling a brillig function. + Acir, + /// There's one optimization, `inline_const_brillig_calls`, where we try to optimize + /// brillig functions with all constant arguments. For that we turn the brillig function + /// into an acir one and try to optimize it. This brillig function could then have + /// `break` statements that can't be possible in acir, so in this mode we don't panic + /// if we end up in unexpected situations that might have been produced by `break`. + Brillig, +} + impl Ssa { /// Loop unrolling can return errors, since ACIR functions need to be fully unrolled. /// This meta-pass will keep trying to unroll loops and simplifying the SSA until no more errors are found. - pub(crate) fn unroll_loops_iteratively(mut ssa: Ssa) -> Result { + pub(crate) fn unroll_loops_iteratively( + mut ssa: Ssa, + mode: UnrollMode, + ) -> Result { // Try to unroll loops first: let mut unroll_errors; - (ssa, unroll_errors) = ssa.try_to_unroll_loops(); + (ssa, unroll_errors) = ssa.try_to_unroll_loops(mode); // Keep unrolling until no more errors are found while !unroll_errors.is_empty() { @@ -58,7 +74,7 @@ impl Ssa { ssa = ssa.mem2reg(); // Unroll again - (ssa, unroll_errors) = ssa.try_to_unroll_loops(); + (ssa, unroll_errors) = ssa.try_to_unroll_loops(mode); // 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)); @@ -71,22 +87,22 @@ impl Ssa { /// If any loop cannot be unrolled, it is left as-is or in a partially unrolled state. /// Returns the ssa along with all unrolling errors encountered #[tracing::instrument(level = "trace", skip(self))] - pub(crate) fn try_to_unroll_loops(mut self) -> (Ssa, Vec) { + pub(crate) fn try_to_unroll_loops(mut self, mode: UnrollMode) -> (Ssa, Vec) { let mut errors = vec![]; for function in self.functions.values_mut() { - function.try_to_unroll_loops(&mut errors); + function.try_to_unroll_loops(mode, &mut errors); } (self, errors) } } impl Function { - pub(crate) fn try_to_unroll_loops(&mut self, errors: &mut Vec) { + pub(crate) fn try_to_unroll_loops(&mut self, mode: UnrollMode, 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 !matches!(self.runtime(), RuntimeType::Brillig(_)) { - errors.extend(find_all_loops(self).unroll_each_loop(self)); + errors.extend(find_all_loops(self).unroll_each_loop(self, mode)); } } } @@ -151,7 +167,7 @@ fn find_all_loops(function: &Function) -> Loops { impl Loops { /// Unroll all loops within a given function. /// Any loops which fail to be unrolled (due to using non-constant indices) will be unmodified. - fn unroll_each_loop(mut self, function: &mut Function) -> Vec { + fn unroll_each_loop(mut self, function: &mut Function, mode: UnrollMode) -> Vec { let mut unroll_errors = vec![]; while let Some(next_loop) = self.yet_to_unroll.pop() { // If we've previously modified a block in this loop we need to refresh the context. @@ -161,13 +177,13 @@ impl Loops { new_context.failed_to_unroll = self.failed_to_unroll; return unroll_errors .into_iter() - .chain(new_context.unroll_each_loop(function)) + .chain(new_context.unroll_each_loop(function, mode)) .collect(); } // Don't try to unroll the loop again if it is known to fail if !self.failed_to_unroll.contains(&next_loop.header) { - match unroll_loop(function, &self.cfg, &next_loop) { + match unroll_loop(function, &self.cfg, &next_loop, mode) { Ok(_) => self.modified_blocks.extend(next_loop.blocks), Err(call_stack) => { self.failed_to_unroll.insert(next_loop.header); @@ -217,12 +233,13 @@ fn unroll_loop( function: &mut Function, cfg: &ControlFlowGraph, loop_: &Loop, + mode: UnrollMode, ) -> Result<(), CallStack> { - let mut unroll_into = get_pre_header(cfg, loop_); + let mut unroll_into = get_pre_header(cfg, loop_, mode)?; let mut jump_value = get_induction_variable(function, unroll_into)?; while let Some(context) = unroll_loop_header(function, loop_, unroll_into, jump_value)? { - let (last_block, last_value) = context.unroll_loop_iteration(); + let (last_block, last_value) = context.unroll_loop_iteration(mode)?; unroll_into = last_block; jump_value = last_value; } @@ -233,14 +250,29 @@ fn unroll_loop( /// The loop pre-header is the block that comes before the loop begins. Generally a header block /// is expected to have 2 predecessors: the pre-header and the final block of the loop which jumps /// back to the beginning. -fn get_pre_header(cfg: &ControlFlowGraph, loop_: &Loop) -> BasicBlockId { +fn get_pre_header( + cfg: &ControlFlowGraph, + loop_: &Loop, + mode: UnrollMode, +) -> Result { let mut pre_header = cfg .predecessors(loop_.header) .filter(|predecessor| *predecessor != loop_.back_edge_start) .collect::>(); - assert_eq!(pre_header.len(), 1); - pre_header.remove(0) + match mode { + UnrollMode::Acir => { + assert_eq!(pre_header.len(), 1); + Ok(pre_header.remove(0)) + } + UnrollMode::Brillig => { + if pre_header.len() == 1 { + Ok(pre_header.remove(0)) + } else { + Err(CallStack::new()) + } + } + } } /// Return the induction value of the current iteration of the loop, from the given block's jmp arguments. @@ -364,7 +396,10 @@ impl<'f> LoopIteration<'f> { /// It is expected the terminator instructions are set up to branch into an empty block /// for further unrolling. When the loop is finished this will need to be mutated to /// jump to the end of the loop instead. - fn unroll_loop_iteration(mut self) -> (BasicBlockId, ValueId) { + fn unroll_loop_iteration( + mut self, + mode: UnrollMode, + ) -> Result<(BasicBlockId, ValueId), CallStack> { let mut next_blocks = self.unroll_loop_block(); while let Some(block) = next_blocks.pop() { @@ -377,8 +412,12 @@ impl<'f> LoopIteration<'f> { } } - self.induction_value - .expect("Expected to find the induction variable by end of loop iteration") + match mode { + UnrollMode::Acir => Ok(self + .induction_value + .expect("Expected to find the induction variable by end of loop iteration")), + UnrollMode::Brillig => self.induction_value.ok_or_else(CallStack::new), + } } /// Unroll a single block in the current iteration of the loop @@ -510,6 +549,7 @@ mod tests { use crate::ssa::{ function_builder::FunctionBuilder, ir::{instruction::BinaryOp, map::Id, types::Type}, + opt::unrolling::UnrollMode, }; #[test] @@ -630,7 +670,7 @@ mod tests { // } // The final block count is not 1 because unrolling creates some unnecessary jmps. // If a simplify cfg pass is ran afterward, the expected block count will be 1. - let (ssa, errors) = ssa.try_to_unroll_loops(); + let (ssa, errors) = ssa.try_to_unroll_loops(UnrollMode::Acir); assert_eq!(errors.len(), 0, "All loops should be unrolled"); assert_eq!(ssa.main().reachable_blocks().len(), 5); } @@ -680,7 +720,7 @@ mod tests { assert_eq!(ssa.main().reachable_blocks().len(), 4); // Expected that we failed to unroll the loop - let (_, errors) = ssa.try_to_unroll_loops(); + let (_, errors) = ssa.try_to_unroll_loops(UnrollMode::Acir); assert_eq!(errors.len(), 1, "Expected to fail to unroll loop"); } } diff --git a/test_programs/execution_success/unconstrained_loop_with_break/Nargo.toml b/test_programs/execution_success/unconstrained_loop_with_break/Nargo.toml new file mode 100644 index 00000000000..f789a4b7d5f --- /dev/null +++ b/test_programs/execution_success/unconstrained_loop_with_break/Nargo.toml @@ -0,0 +1,7 @@ +[package] +name = "unconstrained_loop_with_break" +version = "0.1.0" +type = "bin" +authors = [""] + +[dependencies] diff --git a/test_programs/execution_success/unconstrained_loop_with_break/src/main.nr b/test_programs/execution_success/unconstrained_loop_with_break/src/main.nr new file mode 100644 index 00000000000..ae28eb23c58 --- /dev/null +++ b/test_programs/execution_success/unconstrained_loop_with_break/src/main.nr @@ -0,0 +1,11 @@ +fn main() { + unsafe { foo() }; +} + +unconstrained fn foo() { + for i in 0..1 { + if i == 0 { + break; + } + } +}