From a0931d5208d529a8e72dca8fb6847daec3289269 Mon Sep 17 00:00:00 2001 From: Maxim Vezenov Date: Wed, 27 Nov 2024 19:23:53 +0000 Subject: [PATCH 1/8] accurate hoisting of array get when using induction variable --- .../src/ssa/opt/loop_invariant.rs | 163 +++++++++++++++++- .../noirc_evaluator/src/ssa/opt/unrolling.rs | 10 +- .../loop_invariant_regression/src/main.nr | 19 +- 3 files changed, 180 insertions(+), 12 deletions(-) diff --git a/compiler/noirc_evaluator/src/ssa/opt/loop_invariant.rs b/compiler/noirc_evaluator/src/ssa/opt/loop_invariant.rs index 14233ca73e5..350f2395d7c 100644 --- a/compiler/noirc_evaluator/src/ssa/opt/loop_invariant.rs +++ b/compiler/noirc_evaluator/src/ssa/opt/loop_invariant.rs @@ -7,14 +7,19 @@ //! - Already marked as loop invariants //! //! We also check that we are not hoisting instructions with side effects. -use fxhash::FxHashSet as HashSet; +use acvm::{acir::AcirField, FieldElement}; +use fxhash::{FxHashMap as HashMap, FxHashSet as HashSet}; use crate::ssa::{ ir::{ basic_block::BasicBlockId, + // cfg::ControlFlowGraph, + // dom::DominatorTree, function::{Function, RuntimeType}, function_inserter::FunctionInserter, - instruction::InstructionId, + instruction::{Instruction, InstructionId}, + // post_order::PostOrder, + types::Type, value::ValueId, }, Ssa, @@ -45,25 +50,40 @@ impl Function { } impl Loops { - fn hoist_loop_invariants(self, function: &mut Function) { + fn hoist_loop_invariants(mut self, function: &mut Function) { + // let cfg = ControlFlowGraph::with_function(function); + // let post_order = PostOrder::with_function(function); + // let mut dom_tree = DominatorTree::with_cfg_and_post_order(&cfg, &post_order); + let mut context = LoopInvariantContext::new(function); - for loop_ in self.yet_to_unroll.iter() { + while let Some(loop_) = self.yet_to_unroll.pop() { let Ok(pre_header) = loop_.get_pre_header(context.inserter.function, &self.cfg) else { // If the loop does not have a preheader we skip hoisting loop invariants for this loop continue; }; - context.hoist_loop_invariants(loop_, pre_header); + + context.hoist_loop_invariants(&loop_, pre_header); + + context.defined_in_loop.clear(); } context.map_dependent_instructions(); } } +impl Loop { + fn get_induction_variable_value(&self, function: &Function) -> ValueId { + function.dfg.block_parameters(self.header)[0] + } +} + struct LoopInvariantContext<'f> { inserter: FunctionInserter<'f>, defined_in_loop: HashSet, loop_invariants: HashSet, + // Maps induction variable -> fixed upper loop bound + outer_induction_variables: HashMap, } impl<'f> LoopInvariantContext<'f> { @@ -72,6 +92,7 @@ impl<'f> LoopInvariantContext<'f> { inserter: FunctionInserter::new(function), defined_in_loop: HashSet::default(), loop_invariants: HashSet::default(), + outer_induction_variables: HashMap::default(), } } @@ -91,10 +112,20 @@ impl<'f> LoopInvariantContext<'f> { self.update_values_defined_in_loop_and_invariants(instruction_id, hoist_invariant); } } + + let upper_bound = loop_.get_const_upper_bound(self.inserter.function); + if let Some(upper_bound) = upper_bound { + let induction_variable = loop_.get_induction_variable_value(self.inserter.function); + let induction_variable = self.inserter.resolve(induction_variable); + self.outer_induction_variables.insert(induction_variable, upper_bound); + } } /// Gather the variables declared within the loop fn set_values_defined_in_loop(&mut self, loop_: &Loop) { + // Check whether the param is an induction variable and the block for which it is a param + // Later when we are checking whether a value is a loop invariant we special case + // this parameter for block in loop_.blocks.iter() { let params = self.inserter.function.dfg.block_parameters(*block); self.defined_in_loop.extend(params); @@ -143,7 +174,23 @@ impl<'f> LoopInvariantContext<'f> { is_loop_invariant &= !self.defined_in_loop.contains(&value) || self.loop_invariants.contains(&value); }); - is_loop_invariant && instruction.can_be_deduplicated(&self.inserter.function.dfg, false) + let can_be_deduplicated = + instruction.can_be_deduplicated(&self.inserter.function.dfg, false); + + let can_be_deduplicated = match &instruction { + Instruction::ArrayGet { array, index } => { + let array_typ = self.inserter.function.dfg.type_of_value(*array); + let upper_bound = self.outer_induction_variables.get(index); + if let (Type::Array(_, len), Some(upper_bound)) = (array_typ, upper_bound) { + upper_bound.to_u128() as usize <= len + } else { + can_be_deduplicated + } + } + _ => can_be_deduplicated, + }; + + is_loop_invariant && can_be_deduplicated } fn map_dependent_instructions(&mut self) { @@ -375,4 +422,108 @@ mod test { // The code should be unchanged assert_normalized_ssa_equals(ssa, src); } + + #[test] + fn hoist_array_gets_using_induction_variable_with_const_bound() { + // SSA for the following program: + // + // fn triple_loop(x: u32) { + // let arr = [2; 5]; + // for i in 0..4 { + // for j in 0..4 { + // for _ in 0..4 { + // assert_eq(arr[i], x); + // assert_eq(arr[j], x); + // } + // } + // } + // } + // + // `arr[i]` and `arr[j]` are safe to hoist as we know the maximum possible index + // to be used for both array accesses. + // We want to make sure `arr[i]` is hoisted to the outermost loop body and that + // `arr[j]` is hoisted to the second outermost loop body. + let src = " + brillig(inline) fn main f0 { + b0(v0: u32, v1: u32): + v6 = make_array [u32 2, u32 2, u32 2, u32 2, u32 2] : [u32; 5] + inc_rc v6 + jmp b1(u32 0) + b1(v2: u32): + v9 = lt v2, u32 4 + jmpif v9 then: b3, else: b2 + b3(): + jmp b4(u32 0) + b4(v3: u32): + v10 = lt v3, u32 4 + jmpif v10 then: b6, else: b5 + b6(): + jmp b7(u32 0) + b7(v4: u32): + v13 = lt v4, u32 4 + jmpif v13 then: b9, else: b8 + b9(): + v15 = array_get v6, index v2 -> u32 + v16 = eq v15, v0 + constrain v15 == v0 + v17 = array_get v6, index v3 -> u32 + v18 = eq v17, v0 + constrain v17 == v0 + v19 = add v4, u32 1 + jmp b7(v19) + b8(): + v14 = add v3, u32 1 + jmp b4(v14) + b5(): + v12 = add v2, u32 1 + jmp b1(v12) + b2(): + return + } + "; + + let ssa = Ssa::from_str(src).unwrap(); + + let expected = " + brillig(inline) fn main f0 { + b0(v0: u32, v1: u32): + v6 = make_array [u32 2, u32 2, u32 2, u32 2, u32 2] : [u32; 5] + inc_rc v6 + jmp b1(u32 0) + b1(v2: u32): + v9 = lt v2, u32 4 + jmpif v9 then: b3, else: b2 + b3(): + v10 = array_get v6, index v2 -> u32 + v11 = eq v10, v0 + jmp b4(u32 0) + b4(v3: u32): + v12 = lt v3, u32 4 + jmpif v12 then: b6, else: b5 + b6(): + v15 = array_get v6, index v3 -> u32 + v16 = eq v15, v0 + jmp b7(u32 0) + b7(v4: u32): + v17 = lt v4, u32 4 + jmpif v17 then: b9, else: b8 + b9(): + constrain v10 == v0 + constrain v15 == v0 + v19 = add v4, u32 1 + jmp b7(v19) + b8(): + v18 = add v3, u32 1 + jmp b4(v18) + b5(): + v14 = add v2, u32 1 + jmp b1(v14) + b2(): + return + } + "; + + let ssa = ssa.loop_invariant_code_motion(); + assert_normalized_ssa_equals(ssa, expected); + } } diff --git a/compiler/noirc_evaluator/src/ssa/opt/unrolling.rs b/compiler/noirc_evaluator/src/ssa/opt/unrolling.rs index 777c16dacd1..fcfa7abfe8f 100644 --- a/compiler/noirc_evaluator/src/ssa/opt/unrolling.rs +++ b/compiler/noirc_evaluator/src/ssa/opt/unrolling.rs @@ -82,10 +82,11 @@ impl Function { } } +#[derive(Clone, Debug)] pub(super) struct Loop { /// The header block of a loop is the block which dominates all the /// other blocks in the loop. - header: BasicBlockId, + pub(super) header: BasicBlockId, /// The start of the back_edge n -> d is the block n at the end of /// the loop that jumps back to the header block d which restarts the loop. @@ -269,7 +270,7 @@ impl Loop { /// v5 = lt v1, u32 4 // Upper bound /// jmpif v5 then: b3, else: b2 /// ``` - fn get_const_upper_bound(&self, function: &Function) -> Option { + pub(super) fn get_const_upper_bound(&self, function: &Function) -> Option { let block = &function.dfg[self.header]; let instructions = block.instructions(); assert_eq!( @@ -715,7 +716,10 @@ impl BoilerplateStats { /// ... /// ``` /// We're looking for the terminating jump of the `main` predecessor of `loop_entry`. -fn get_induction_variable(function: &Function, block: BasicBlockId) -> Result { +pub(super) fn get_induction_variable( + function: &Function, + block: BasicBlockId, +) -> Result { match function.dfg[block].terminator() { Some(TerminatorInstruction::Jmp { arguments, call_stack: location, .. }) => { // This assumption will no longer be valid if e.g. mutable variables are represented as diff --git a/test_programs/execution_success/loop_invariant_regression/src/main.nr b/test_programs/execution_success/loop_invariant_regression/src/main.nr index 25f6e92f868..1f2a0e08187 100644 --- a/test_programs/execution_success/loop_invariant_regression/src/main.nr +++ b/test_programs/execution_success/loop_invariant_regression/src/main.nr @@ -2,12 +2,25 @@ // to be hoisted to the loop's pre-header block. fn main(x: u32, y: u32) { loop(4, x, y); + array_read_loop(4, x); } fn loop(upper_bound: u32, x: u32, y: u32) { for _ in 0..upper_bound { - let mut z = x * y; - z = z * x; - assert_eq(z, 12); + for _ in 0..upper_bound { + assert_eq(x, y); + } } } + +fn array_read_loop(upper_bound: u32, x: u32) { + let arr = [2; 5]; + for i in 0..upper_bound { + for j in 0..upper_bound { + for _ in 0..upper_bound { + assert_eq(arr[i], x); + assert_eq(arr[j], x); + } + } + } +} \ No newline at end of file From f59617ed72d548b6c6c3ec8d7e980da734592908 Mon Sep 17 00:00:00 2001 From: Maxim Vezenov Date: Wed, 27 Nov 2024 19:24:54 +0000 Subject: [PATCH 2/8] cleanup --- compiler/noirc_evaluator/src/ssa/opt/loop_invariant.rs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/compiler/noirc_evaluator/src/ssa/opt/loop_invariant.rs b/compiler/noirc_evaluator/src/ssa/opt/loop_invariant.rs index 350f2395d7c..0eba8e1a7a2 100644 --- a/compiler/noirc_evaluator/src/ssa/opt/loop_invariant.rs +++ b/compiler/noirc_evaluator/src/ssa/opt/loop_invariant.rs @@ -13,12 +13,9 @@ use fxhash::{FxHashMap as HashMap, FxHashSet as HashSet}; use crate::ssa::{ ir::{ basic_block::BasicBlockId, - // cfg::ControlFlowGraph, - // dom::DominatorTree, function::{Function, RuntimeType}, function_inserter::FunctionInserter, instruction::{Instruction, InstructionId}, - // post_order::PostOrder, types::Type, value::ValueId, }, @@ -51,10 +48,6 @@ impl Function { impl Loops { fn hoist_loop_invariants(mut self, function: &mut Function) { - // let cfg = ControlFlowGraph::with_function(function); - // let post_order = PostOrder::with_function(function); - // let mut dom_tree = DominatorTree::with_cfg_and_post_order(&cfg, &post_order); - let mut context = LoopInvariantContext::new(function); while let Some(loop_) = self.yet_to_unroll.pop() { From 511c1f09d36d355c9b639d8853bb832144a8aa57 Mon Sep 17 00:00:00 2001 From: Maxim Vezenov Date: Wed, 27 Nov 2024 20:29:35 +0000 Subject: [PATCH 3/8] revert failing change in loop_invariant_regression' --- .../loop_invariant_regression/src/main.nr | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test_programs/execution_success/loop_invariant_regression/src/main.nr b/test_programs/execution_success/loop_invariant_regression/src/main.nr index 1f2a0e08187..c28ce063116 100644 --- a/test_programs/execution_success/loop_invariant_regression/src/main.nr +++ b/test_programs/execution_success/loop_invariant_regression/src/main.nr @@ -7,9 +7,9 @@ fn main(x: u32, y: u32) { fn loop(upper_bound: u32, x: u32, y: u32) { for _ in 0..upper_bound { - for _ in 0..upper_bound { - assert_eq(x, y); - } + let mut z = x * y; + z = z * x; + assert_eq(z, 12); } } @@ -23,4 +23,4 @@ fn array_read_loop(upper_bound: u32, x: u32) { } } } -} \ No newline at end of file +} From 840fe981ca259ed339f685807385509dd366d634 Mon Sep 17 00:00:00 2001 From: Maxim Vezenov Date: Wed, 27 Nov 2024 20:30:38 +0000 Subject: [PATCH 4/8] cleanup --- compiler/noirc_evaluator/src/ssa/opt/unrolling.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/noirc_evaluator/src/ssa/opt/unrolling.rs b/compiler/noirc_evaluator/src/ssa/opt/unrolling.rs index fcfa7abfe8f..1e04282c019 100644 --- a/compiler/noirc_evaluator/src/ssa/opt/unrolling.rs +++ b/compiler/noirc_evaluator/src/ssa/opt/unrolling.rs @@ -716,7 +716,7 @@ impl BoilerplateStats { /// ... /// ``` /// We're looking for the terminating jump of the `main` predecessor of `loop_entry`. -pub(super) fn get_induction_variable( +fn get_induction_variable( function: &Function, block: BasicBlockId, ) -> Result { From edc265dcb81163103116e42425e5a2d245440181 Mon Sep 17 00:00:00 2001 From: Maxim Vezenov Date: Wed, 27 Nov 2024 20:41:56 +0000 Subject: [PATCH 5/8] comments and fmt --- .../src/ssa/opt/loop_invariant.rs | 21 +++++++++++++++++++ .../noirc_evaluator/src/ssa/opt/unrolling.rs | 5 +---- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/compiler/noirc_evaluator/src/ssa/opt/loop_invariant.rs b/compiler/noirc_evaluator/src/ssa/opt/loop_invariant.rs index 0eba8e1a7a2..65826ccf40e 100644 --- a/compiler/noirc_evaluator/src/ssa/opt/loop_invariant.rs +++ b/compiler/noirc_evaluator/src/ssa/opt/loop_invariant.rs @@ -50,6 +50,9 @@ impl Loops { fn hoist_loop_invariants(mut self, function: &mut Function) { let mut context = LoopInvariantContext::new(function); + // The loops should be sorted by the number of blocks. + // We want to access outer nested loops first, which we do by popping + // from the top of the list. while let Some(loop_) = self.yet_to_unroll.pop() { let Ok(pre_header) = loop_.get_pre_header(context.inserter.function, &self.cfg) else { // If the loop does not have a preheader we skip hoisting loop invariants for this loop @@ -66,6 +69,20 @@ impl Loops { } impl Loop { + /// Find the value that controls whether to perform a loop iteration. + /// This is going to be the block parameter of the loop header. + /// + /// Consider the following example of a `for i in 0..4` loop: + /// ```text + /// brillig(inline) fn main f0 { + /// b0(v0: u32): + /// ... + /// jmp b1(u32 0) + /// b1(v1: u32): // Loop header + /// v5 = lt v1, u32 4 // Upper bound + /// jmpif v5 then: b3, else: b2 + /// ``` + /// In the example above, `v1` is the induction variable fn get_induction_variable_value(&self, function: &Function) -> ValueId { function.dfg.block_parameters(self.header)[0] } @@ -106,6 +123,9 @@ impl<'f> LoopInvariantContext<'f> { } } + // Keep track of a loop induction variable and respective upper bound. + // This will be used by later loops to determine whether they have operations + // reliant upon the maximum induction variable. let upper_bound = loop_.get_const_upper_bound(self.inserter.function); if let Some(upper_bound) = upper_bound { let induction_variable = loop_.get_induction_variable_value(self.inserter.function); @@ -167,6 +187,7 @@ impl<'f> LoopInvariantContext<'f> { is_loop_invariant &= !self.defined_in_loop.contains(&value) || self.loop_invariants.contains(&value); }); + let can_be_deduplicated = instruction.can_be_deduplicated(&self.inserter.function.dfg, false); diff --git a/compiler/noirc_evaluator/src/ssa/opt/unrolling.rs b/compiler/noirc_evaluator/src/ssa/opt/unrolling.rs index 1e04282c019..efd3bad2781 100644 --- a/compiler/noirc_evaluator/src/ssa/opt/unrolling.rs +++ b/compiler/noirc_evaluator/src/ssa/opt/unrolling.rs @@ -716,10 +716,7 @@ impl BoilerplateStats { /// ... /// ``` /// We're looking for the terminating jump of the `main` predecessor of `loop_entry`. -fn get_induction_variable( - function: &Function, - block: BasicBlockId, -) -> Result { +fn get_induction_variable(function: &Function, block: BasicBlockId) -> Result { match function.dfg[block].terminator() { Some(TerminatorInstruction::Jmp { arguments, call_stack: location, .. }) => { // This assumption will no longer be valid if e.g. mutable variables are represented as From 5aa31819b1a3745a34f8c79a43edcccce7b95e11 Mon Sep 17 00:00:00 2001 From: Maxim Vezenov Date: Wed, 27 Nov 2024 20:53:51 +0000 Subject: [PATCH 6/8] additional comments and cleanup' --- .../src/ssa/opt/loop_invariant.rs | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/compiler/noirc_evaluator/src/ssa/opt/loop_invariant.rs b/compiler/noirc_evaluator/src/ssa/opt/loop_invariant.rs index 65826ccf40e..2e1b2c748c1 100644 --- a/compiler/noirc_evaluator/src/ssa/opt/loop_invariant.rs +++ b/compiler/noirc_evaluator/src/ssa/opt/loop_invariant.rs @@ -188,23 +188,34 @@ impl<'f> LoopInvariantContext<'f> { !self.defined_in_loop.contains(&value) || self.loop_invariants.contains(&value); }); - let can_be_deduplicated = - instruction.can_be_deduplicated(&self.inserter.function.dfg, false); + let can_be_deduplicated = instruction + .can_be_deduplicated(&self.inserter.function.dfg, false) + || self.can_be_deduplicated_from_upper_bound(&instruction); - let can_be_deduplicated = match &instruction { + is_loop_invariant && can_be_deduplicated + } + + /// Certain instructions can take advantage of that our induction variable has a fixed maximum. + /// + /// For example, an array access can usually only be safely deduplicated when we have a constant + /// index that is below the length of the array. + /// Checking an array get where the index is the loop's induction variable on its own + /// would determine that the instruction is not safe for hoisting. + /// However, if we know that the induction variable's upper bound will always be in bounds of the array + /// we can safely hoist the array access. + fn can_be_deduplicated_from_upper_bound(&self, instruction: &Instruction) -> bool { + match instruction { Instruction::ArrayGet { array, index } => { let array_typ = self.inserter.function.dfg.type_of_value(*array); let upper_bound = self.outer_induction_variables.get(index); if let (Type::Array(_, len), Some(upper_bound)) = (array_typ, upper_bound) { upper_bound.to_u128() as usize <= len } else { - can_be_deduplicated + false } } - _ => can_be_deduplicated, - }; - - is_loop_invariant && can_be_deduplicated + _ => false, + } } fn map_dependent_instructions(&mut self) { From 66419d1ecb3e4d98b9026be4c78582c9715f5c36 Mon Sep 17 00:00:00 2001 From: Maxim Vezenov Date: Wed, 27 Nov 2024 20:56:03 +0000 Subject: [PATCH 7/8] one more cleanup --- compiler/noirc_evaluator/src/ssa/opt/unrolling.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/compiler/noirc_evaluator/src/ssa/opt/unrolling.rs b/compiler/noirc_evaluator/src/ssa/opt/unrolling.rs index efd3bad2781..67cb81fed8c 100644 --- a/compiler/noirc_evaluator/src/ssa/opt/unrolling.rs +++ b/compiler/noirc_evaluator/src/ssa/opt/unrolling.rs @@ -82,7 +82,6 @@ impl Function { } } -#[derive(Clone, Debug)] pub(super) struct Loop { /// The header block of a loop is the block which dominates all the /// other blocks in the loop. From 6091273d837061ee2ec0f04ad81e72eb460fa803 Mon Sep 17 00:00:00 2001 From: Maxim Vezenov Date: Mon, 2 Dec 2024 16:32:08 +0000 Subject: [PATCH 8/8] pr comments --- .../src/ssa/opt/loop_invariant.rs | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/compiler/noirc_evaluator/src/ssa/opt/loop_invariant.rs b/compiler/noirc_evaluator/src/ssa/opt/loop_invariant.rs index 2e1b2c748c1..f1dd511402e 100644 --- a/compiler/noirc_evaluator/src/ssa/opt/loop_invariant.rs +++ b/compiler/noirc_evaluator/src/ssa/opt/loop_invariant.rs @@ -60,8 +60,6 @@ impl Loops { }; context.hoist_loop_invariants(&loop_, pre_header); - - context.defined_in_loop.clear(); } context.map_dependent_instructions(); @@ -83,7 +81,7 @@ impl Loop { /// jmpif v5 then: b3, else: b2 /// ``` /// In the example above, `v1` is the induction variable - fn get_induction_variable_value(&self, function: &Function) -> ValueId { + fn get_induction_variable(&self, function: &Function) -> ValueId { function.dfg.block_parameters(self.header)[0] } } @@ -119,7 +117,7 @@ impl<'f> LoopInvariantContext<'f> { self.inserter.push_instruction(instruction_id, *block); } - self.update_values_defined_in_loop_and_invariants(instruction_id, hoist_invariant); + self.extend_values_defined_in_loop_and_invariants(instruction_id, hoist_invariant); } } @@ -128,7 +126,7 @@ impl<'f> LoopInvariantContext<'f> { // reliant upon the maximum induction variable. let upper_bound = loop_.get_const_upper_bound(self.inserter.function); if let Some(upper_bound) = upper_bound { - let induction_variable = loop_.get_induction_variable_value(self.inserter.function); + let induction_variable = loop_.get_induction_variable(self.inserter.function); let induction_variable = self.inserter.resolve(induction_variable); self.outer_induction_variables.insert(induction_variable, upper_bound); } @@ -136,9 +134,12 @@ impl<'f> LoopInvariantContext<'f> { /// Gather the variables declared within the loop fn set_values_defined_in_loop(&mut self, loop_: &Loop) { - // Check whether the param is an induction variable and the block for which it is a param - // Later when we are checking whether a value is a loop invariant we special case - // this parameter + // Clear any values that may be defined in previous loops, as the context is per function. + self.defined_in_loop.clear(); + // These are safe to keep per function, but we want to be clear that these values + // are used per loop. + self.loop_invariants.clear(); + for block in loop_.blocks.iter() { let params = self.inserter.function.dfg.block_parameters(*block); self.defined_in_loop.extend(params); @@ -151,7 +152,7 @@ impl<'f> LoopInvariantContext<'f> { /// Update any values defined in the loop and loop invariants after a /// analyzing and re-inserting a loop's instruction. - fn update_values_defined_in_loop_and_invariants( + fn extend_values_defined_in_loop_and_invariants( &mut self, instruction_id: InstructionId, hoist_invariant: bool, @@ -218,6 +219,14 @@ impl<'f> LoopInvariantContext<'f> { } } + /// Loop invariant hoisting only operates over loop instructions. + /// The `FunctionInserter` is used for mapping old values to new values after + /// re-inserting loop invariant instructions. + /// However, there may be instructions which are not within loops that are + /// still reliant upon the instruction results altered during the pass. + /// This method re-inserts all instructions so that all instructions have + /// correct new value IDs based upon the `FunctionInserter` internal map. + /// Leaving out this mapping could lead to instructions with values that do not exist. fn map_dependent_instructions(&mut self) { let blocks = self.inserter.function.reachable_blocks(); for block in blocks {