diff --git a/compiler/noirc_evaluator/src/ssa/ir/dfg/simplify.rs b/compiler/noirc_evaluator/src/ssa/ir/dfg/simplify.rs index 30f17d053a4..99045a19eda 100644 --- a/compiler/noirc_evaluator/src/ssa/ir/dfg/simplify.rs +++ b/compiler/noirc_evaluator/src/ssa/ir/dfg/simplify.rs @@ -2,7 +2,7 @@ use crate::ssa::ir::{ basic_block::BasicBlockId, dfg::simplify::value_merger::ValueMerger, instruction::{ - Binary, BinaryOp, ConstrainError, Instruction, + Binary, BinaryOp, ConstrainError, Instruction, Intrinsic, binary::{truncate, truncate_field}, }, types::{NumericType, Type}, @@ -111,20 +111,31 @@ pub(crate) fn simplify( } Instruction::ConstrainNotEqual(..) => None, Instruction::ArrayGet { array, index } => { - if let Some(index) = dfg.get_numeric_constant(*index) { - return try_optimize_array_get_from_previous_set(dfg, *array, index); + let array = *array; + let index = *index; + let constant_index = dfg.get_numeric_constant(index); + + // If it's an `array_get` on a value that results from a call to `slice_insert` with a + // constant index we can do `array_get` on the original slice or use the inserted value + let result = try_optimize_array_get_on_slice_insert(dfg, array, index, constant_index); + if !matches!(result, None) { + return result; } - let array_or_slice_type = dfg.type_of_value(*array); + if let Some(constant_index) = constant_index { + return try_optimize_array_get_from_previous_set(dfg, array, index, constant_index); + } + + // If the array is of length 1 then we know the only value which can be potentially read out of it. + // We can then simply assert that the index is equal to zero and return the array's contained value. + let array_or_slice_type = dfg.type_of_value(array); if matches!(array_or_slice_type, Type::Array(_, 1)) && array_or_slice_type.element_size() == 1 { - // If the array is of length 1 then we know the only value which can be potentially read out of it. - // We can then simply assert that the index is equal to zero and return the array's contained value. - optimize_length_one_array_read(dfg, block, call_stack, *array, *index) - } else { - None + return optimize_length_one_array_read(dfg, block, call_stack, array, index); } + + None } Instruction::ArraySet { array: array_id, index: index_id, value, .. } => { let array = dfg.get_array_constant(*array_id); @@ -356,7 +367,7 @@ fn optimize_length_one_array_read( ); dfg.insert_instruction_and_results(index_constraint, block, None, call_stack); - let result = try_optimize_array_get_from_previous_set(dfg, array, FieldElement::zero()); + let result = try_optimize_array_get_from_previous_set(dfg, array, index, FieldElement::zero()); if let SimplifyResult::None = result { SimplifyResult::SimplifiedToInstruction(Instruction::ArrayGet { array, index: zero }) } else { @@ -379,6 +390,7 @@ fn optimize_length_one_array_read( /// - Otherwise, we check the array value of the array set. /// - If the array value is constant, we use that array. /// - If the array value is from a previous array-set, we recur. +/// - If the array value is a parameter, we can directly get from that parameter /// /// That is, we have multiple `array_set` instructions setting various constant indexes /// of the same array, returning a modified version. We want to go backwards until we @@ -386,6 +398,7 @@ fn optimize_length_one_array_read( fn try_optimize_array_get_from_previous_set( dfg: &DataFlowGraph, mut array_id: ValueId, + index: ValueId, target_index: FieldElement, ) -> SimplifyResult { let mut elements = None; @@ -413,6 +426,16 @@ fn try_optimize_array_get_from_previous_set( _ => return SimplifyResult::None, } } else { + // If we reach a parameter, directly get from that parameter, but only if the index is a constant + if dfg.get_numeric_constant(index).is_some() { + if let Value::Param { .. } = dfg[array_id] { + return SimplifyResult::SimplifiedToInstruction(Instruction::ArrayGet { + array: array_id, + index, + }); + } + } + return SimplifyResult::None; } } @@ -426,6 +449,76 @@ fn try_optimize_array_get_from_previous_set( SimplifyResult::None } +fn try_optimize_array_get_on_slice_insert( + dfg: &mut DataFlowGraph, + array: ValueId, + index: ValueId, + constant_index: Option, +) -> SimplifyResult { + let Some(constant_index) = constant_index else { + return SimplifyResult::None; + }; + + let Some(slice_insert) = dfg.get_intrinsic(Intrinsic::SliceInsert) else { + return SimplifyResult::None; + }; + + let Value::Instruction { instruction, .. } = dfg[array] else { + return SimplifyResult::None; + }; + + let Instruction::Call { func, arguments } = &dfg[instruction] else { + return SimplifyResult::None; + }; + + if func != slice_insert { + return SimplifyResult::None; + } + + // slice_insert(length, slice, index, values...) + + // For simplicity, we only optimize when only a single value is being inserted + if arguments.len() != 4 { + return SimplifyResult::None; + } + + let mut slice = arguments[1]; + let insert_index = arguments[2]; + + let Some(insert_index_constant) = dfg.get_numeric_constant(insert_index) else { + return SimplifyResult::None; + }; + + // If the slice came from an `as_slice` call, we can get from the original array + if let Some(as_slice) = dfg.get_intrinsic(Intrinsic::AsSlice) { + if let Value::Instruction { instruction, .. } = dfg[slice] { + if let Instruction::Call { func, arguments } = &dfg[instruction] { + if func == as_slice { + slice = arguments[0]; + } + } + } + } + + match constant_index.cmp(&insert_index_constant) { + std::cmp::Ordering::Equal => { + // If we get from the index the value was inserted in, we can simplify it to that value + SimplifyResult::SimplifiedTo(arguments[3]) + } + std::cmp::Ordering::Less => { + // If the index is before the slice insert index, we can directly read from the original slice + SimplifyResult::SimplifiedToInstruction(Instruction::ArrayGet { array: slice, index }) + } + std::cmp::Ordering::Greater => { + // If the index is after the slice insert index, the index is one less in the original slice + // as in the new slice all values after the insert index have been shifted forward + let index = + dfg.make_constant(constant_index - FieldElement::one(), NumericType::length_type()); + SimplifyResult::SimplifiedToInstruction(Instruction::ArrayGet { array: slice, index }) + } + } +} + /// If we have an array set whose value is from an array get on the same array at the same index, /// we can simplify that array set to the array we were looking to perform an array set upon. /// @@ -707,4 +800,82 @@ mod tests { assert_normalized_ssa_equals(ssa, src); } + + #[test] + fn simplifies_array_get_on_slice_insert() { + let src = " + acir(inline) predicate_pure fn main f0 { + b0(v0: [Field; 3]): + v2, v3 = call as_slice(v0) -> (u32, [Field]) + v8, v9 = call slice_insert(u32 3, v3, u32 1, Field 10) -> (u32, [Field]) + v10 = array_get v9, index u32 0 -> Field + v11 = array_get v9, index u32 1 -> Field + v12 = array_get v9, index u32 2 -> Field + v13 = array_get v9, index u32 3 -> Field + return v10, v11, v12, v13 + } + "; + let ssa = Ssa::from_str_simplifying(src).unwrap(); + + // We can see that the array_gets now read from v0 instead of v9, + // and for the index 1 we use the inserted value. + assert_ssa_snapshot!(ssa, @r" + acir(inline) predicate_pure fn main f0 { + b0(v0: [Field; 3]): + v2, v3 = call as_slice(v0) -> (u32, [Field]) + v8, v9 = call slice_insert(u32 3, v3, u32 1, Field 10) -> (u32, [Field]) + v11 = array_get v0, index u32 0 -> Field + v12 = array_get v0, index u32 1 -> Field + v14 = array_get v0, index u32 2 -> Field + return v11, Field 10, v12, v14 + } + "); + } + + #[test] + fn simplifies_array_get_from_previous_array_set_with_make_array() { + let src = " + acir(inline) predicate_pure fn main f0 { + b0(): + v0 = make_array [Field 2, Field 3] : [Field; 2] + v1 = array_set mut v0, index u32 0, value Field 4 + v2 = array_get v1, index u32 0 -> Field + v3 = array_get v1, index u32 1 -> Field + return v2, v3 + } + "; + let ssa = Ssa::from_str_simplifying(src).unwrap(); + + assert_ssa_snapshot!(ssa, @r" + acir(inline) predicate_pure fn main f0 { + b0(): + v2 = make_array [Field 2, Field 3] : [Field; 2] + v4 = make_array [Field 4, Field 3] : [Field; 2] + return Field 4, Field 3 + } + "); + } + + #[test] + fn simplifies_array_get_from_previous_array_set_with_array_param() { + let src = " + acir(inline) predicate_pure fn main f0 { + b0(v0: [Field; 2]): + v1 = array_set mut v0, index u32 0, value Field 4 + v2 = array_get v1, index u32 0 -> Field + v3 = array_get v1, index u32 1 -> Field + return v2, v3 + } + "; + let ssa = Ssa::from_str_simplifying(src).unwrap(); + + assert_ssa_snapshot!(ssa, @r" + acir(inline) predicate_pure fn main f0 { + b0(v0: [Field; 2]): + v3 = array_set mut v0, index u32 0, value Field 4 + v5 = array_get v0, index u32 1 -> Field + return Field 4, v5 + } + "); + } } diff --git a/compiler/noirc_evaluator/src/ssa/mod.rs b/compiler/noirc_evaluator/src/ssa/mod.rs index 85be9067262..a31a6365e88 100644 --- a/compiler/noirc_evaluator/src/ssa/mod.rs +++ b/compiler/noirc_evaluator/src/ssa/mod.rs @@ -163,8 +163,11 @@ pub fn primary_passes(options: &SsaEvaluatorOptions) -> Vec> { "Dead Instruction Elimination", ), SsaPass::new(Ssa::simplify_cfg, "Simplifying"), - SsaPass::new(Ssa::as_slice_optimization, "`as_slice` optimization") - .and_then(Ssa::remove_unreachable_functions), + SsaPass::new( + Ssa::slice_instrinsics_length_optimization, + "slice intrinsics length optimization", + ) + .and_then(Ssa::remove_unreachable_functions), SsaPass::new_try( Ssa::evaluate_static_assert_and_assert_constant, "`static_assert` and `assert_constant`", @@ -229,6 +232,12 @@ pub fn primary_passes(options: &SsaEvaluatorOptions) -> Vec> { // We cannot run mem2reg after DIE, because it removes Store instructions. // We have to run it before, to give it a chance to turn Store+Load into known values. SsaPass::new(Ssa::mem2reg, "Mem2Reg"), + // After several Mem2Reg passes, some slice intrinsics will be able to be further optimized + SsaPass::new( + Ssa::slice_instrinsics_length_optimization, + "slice intrinsics length optimization", + ) + .and_then(Ssa::remove_unreachable_functions), SsaPass::new(Ssa::dead_instruction_elimination, "Dead Instruction Elimination"), SsaPass::new(Ssa::brillig_entry_point_analysis, "Brillig Entry Point Analysis") // Remove any potentially unnecessary duplication from the Brillig entry point analysis. diff --git a/compiler/noirc_evaluator/src/ssa/opt/as_slice_length.rs b/compiler/noirc_evaluator/src/ssa/opt/as_slice_length.rs deleted file mode 100644 index da20e9a3333..00000000000 --- a/compiler/noirc_evaluator/src/ssa/opt/as_slice_length.rs +++ /dev/null @@ -1,112 +0,0 @@ -use crate::ssa::{ - ir::{ - function::Function, - instruction::{Instruction, Intrinsic}, - types::{NumericType, Type}, - }, - ssa_gen::Ssa, -}; - -impl Ssa { - /// A simple SSA pass to find any calls to `Intrinsic::AsSlice` and replacing any references to the length of the - /// resulting slice with the length of the array from which it was generated. - /// - /// This allows the length of a slice generated from an array to be used in locations where a constant value is - /// necessary when the value of the array is unknown. - /// - /// Note that this pass must be placed before loop unrolling to be useful. - #[expect(clippy::wrong_self_convention)] - #[tracing::instrument(level = "trace", skip(self))] - pub(crate) fn as_slice_optimization(mut self) -> Self { - for func in self.functions.values_mut() { - func.as_slice_optimization(); - } - self - } -} - -impl Function { - pub(crate) fn as_slice_optimization(&mut self) { - // If `as_slice` isn't called in this function there's nothing to do - let Some(as_slice) = self.dfg.get_intrinsic(Intrinsic::AsSlice).copied() else { - return; - }; - - self.simple_optimization(|context| { - let instruction_id = context.instruction_id; - let instruction = context.instruction(); - - let (target_func, arguments) = match &instruction { - Instruction::Call { func, arguments } => (func, arguments), - _ => return, - }; - - if *target_func != as_slice { - return; - } - - let first_argument = - arguments.first().expect("AsSlice should always have one argument"); - let array_typ = context.dfg.type_of_value(*first_argument); - let Type::Array(_, length) = array_typ else { - unreachable!("AsSlice called with non-array {}", array_typ); - }; - - let [original_slice_length, _] = context.dfg.instruction_result(instruction_id); - let known_length = context.dfg.make_constant(length.into(), NumericType::length_type()); - context.replace_value(original_slice_length, known_length); - }); - } -} - -#[cfg(test)] -mod test { - use crate::assert_ssa_snapshot; - - use super::Ssa; - - #[test] - fn as_slice_length_optimization() { - // In this code we expect `return v2` to be replaced with `return u32 3` because - // that's the length of the v0 array. - let src = " - acir(inline) fn main f0 { - b0(v0: [Field; 3]): - v2, v3 = call as_slice(v0) -> (u32, [Field]) - return v2 - } - "; - let ssa = Ssa::from_str(src).unwrap(); - - let ssa = ssa.as_slice_optimization(); - assert_ssa_snapshot!(ssa, @r" - acir(inline) fn main f0 { - b0(v0: [Field; 3]): - v2, v3 = call as_slice(v0) -> (u32, [Field]) - return u32 3 - } - "); - } - - #[test] - fn as_slice_length_multiple_different_arrays() { - let src = " - acir(inline) fn main f0 { - b0(v0: [Field; 3], v1: [Field; 5]): - v3, v4 = call as_slice(v0) -> (u32, [Field]) - v5, v6 = call as_slice(v1) -> (u32, [Field]) - return v3, v5 - } - "; - let ssa = Ssa::from_str(src).unwrap(); - let ssa = ssa.as_slice_optimization(); - assert_ssa_snapshot!(ssa, @r" - acir(inline) fn main f0 { - b0(v0: [Field; 3], v1: [Field; 5]): - v3, v4 = call as_slice(v0) -> (u32, [Field]) - v5, v6 = call as_slice(v1) -> (u32, [Field]) - return u32 3, u32 5 - } - "); - } -} diff --git a/compiler/noirc_evaluator/src/ssa/opt/constant_folding/mod.rs b/compiler/noirc_evaluator/src/ssa/opt/constant_folding/mod.rs index 5f7f34ea182..aa333c3871b 100644 --- a/compiler/noirc_evaluator/src/ssa/opt/constant_folding/mod.rs +++ b/compiler/noirc_evaluator/src/ssa/opt/constant_folding/mod.rs @@ -928,29 +928,28 @@ mod test { let instructions = main.dfg[main.entry_block()].instructions(); assert_eq!(instructions.len(), 15); + let ssa = ssa.fold_constants_using_constraints(MIN_ITER); + // The `array_get` instruction after `enable_side_effects v1` is deduplicated // with the one under `enable_side_effects v0` because it doesn't require a predicate, // but the `array_set` is not, because it does require a predicate, and the subsequent // `array_get` uses a different input, so it's not a duplicate of anything. - let expected = " - acir(inline) fn main f0 { - b0(v0: u1, v1: u1, v2: [Field; 2]): - enable_side_effects v0 - v4 = array_get v2, index u32 0 -> u32 - v7 = array_set v2, index u32 1, value u32 2 - v8 = array_get v7, index u32 0 -> u32 - constrain v4 == v8 - enable_side_effects v1 - v9 = array_set v2, index u32 1, value u32 2 - v10 = array_get v9, index u32 0 -> u32 - constrain v4 == v10 - enable_side_effects v0 - return - } - "; - - let ssa = ssa.fold_constants_using_constraints(MIN_ITER); - assert_normalized_ssa_equals(ssa, expected); + assert_ssa_snapshot!(ssa, @r" + acir(inline) fn main f0 { + b0(v0: u1, v1: u1, v2: [Field; 2]): + enable_side_effects v0 + v4 = array_get v2, index u32 0 -> u32 + v7 = array_set v2, index u32 1, value u32 2 + v8 = array_get v2, index u32 0 -> u32 + constrain v4 == v8 + enable_side_effects v1 + v9 = array_set v2, index u32 1, value u32 2 + v10 = array_get v2, index u32 0 -> u32 + constrain v4 == v10 + enable_side_effects v0 + return + } + "); } #[test] diff --git a/compiler/noirc_evaluator/src/ssa/opt/mod.rs b/compiler/noirc_evaluator/src/ssa/opt/mod.rs index b8ad1905447..baf6c9a375e 100644 --- a/compiler/noirc_evaluator/src/ssa/opt/mod.rs +++ b/compiler/noirc_evaluator/src/ssa/opt/mod.rs @@ -5,7 +5,6 @@ //! Generally, these passes are also expected to minimize the final amount of instructions. mod array_set; -mod as_slice_length; mod basic_conditional; mod brillig_array_get_and_set; pub(crate) mod brillig_entry_points; @@ -37,6 +36,7 @@ mod remove_unreachable_instructions; mod remove_unused_instructions; mod simple_optimization; mod simplify_cfg; +mod slice_intrinsics_length; mod unrolling; pub use constant_folding::DEFAULT_MAX_ITER as CONSTANT_FOLDING_MAX_ITER; diff --git a/compiler/noirc_evaluator/src/ssa/opt/preprocess_fns.rs b/compiler/noirc_evaluator/src/ssa/opt/preprocess_fns.rs index b22928706b1..f0186fc2ebc 100644 --- a/compiler/noirc_evaluator/src/ssa/opt/preprocess_fns.rs +++ b/compiler/noirc_evaluator/src/ssa/opt/preprocess_fns.rs @@ -51,7 +51,7 @@ impl Ssa { // Start with an inline pass. let mut function = function.inlined(&self, &should_inline_call)?; // Help unrolling determine bounds. - function.as_slice_optimization(); + function.slice_intrinsics_length_optimization(); // Prepare for unrolling function.loop_invariant_code_motion(); // We might not be able to unroll all loops without fully inlining them, so ignore errors. 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 c2474f27bcb..2bfab7d7d71 100644 --- a/compiler/noirc_evaluator/src/ssa/opt/remove_if_else.rs +++ b/compiler/noirc_evaluator/src/ssa/opt/remove_if_else.rs @@ -551,7 +551,7 @@ mod tests { v10 = unchecked_mul v8, u32 2 v11 = unchecked_mul v9, v7 v12 = unchecked_add v10, v11 - v14 = array_get v5, index u32 1 -> u32 + v14 = array_get v1, index u32 1 -> u32 v15 = array_get v1, index u32 1 -> u32 v16 = cast v0 as u32 v17 = cast v6 as u32 @@ -757,7 +757,7 @@ mod tests { let mut ssa = Ssa::from_str(src).unwrap(); ssa = ssa.remove_if_else().unwrap(); - // Here v14 is the result of the merge (keep `[v13]`) + // Here v13 is the result of the merge (keep `[v2]`) assert_ssa_snapshot!(ssa, @r" acir(inline) impure fn main f0 { b0(v0: u1, v1: Field, v2: Field): @@ -769,11 +769,10 @@ mod tests { v9, v10 = call slice_insert(v6, v3, u32 0, v2) -> (u32, [Field]) v11 = not v0 v12 = cast v0 as u32 - v13 = array_get v10, index u32 0 -> Field - v14 = make_array [v13] : [Field] + v13 = make_array [v2] : [Field] enable_side_effects u1 1 - v17 = add v12, u32 1 - v18 = make_array [v2, v13] : [Field] + v16 = add v12, u32 1 + v17 = make_array [v2, v2] : [Field] constrain v2 == Field 1 return } diff --git a/compiler/noirc_evaluator/src/ssa/opt/slice_intrinsics_length.rs b/compiler/noirc_evaluator/src/ssa/opt/slice_intrinsics_length.rs new file mode 100644 index 00000000000..f16a4899f3b --- /dev/null +++ b/compiler/noirc_evaluator/src/ssa/opt/slice_intrinsics_length.rs @@ -0,0 +1,326 @@ +use acvm::{AcirField, FieldElement}; + +use crate::ssa::{ + ir::{ + function::Function, + instruction::{Instruction, Intrinsic}, + types::{NumericType, Type}, + }, + ssa_gen::Ssa, +}; + +impl Ssa { + /// A simple SSA pass to find any calls to slice intrinsics and replacing any references to the length of the + /// resulting slice with the length of the array from which it was generated, or with a relative + /// length based on the input length. + /// + /// This allows the length of a slice generated from an array to be used in locations where a constant value is + /// necessary when the value of the array is unknown. + /// + /// Note that this pass must be placed before loop unrolling to be useful. + #[tracing::instrument(level = "trace", skip(self))] + pub(crate) fn slice_instrinsics_length_optimization(mut self) -> Self { + for func in self.functions.values_mut() { + func.slice_intrinsics_length_optimization(); + } + self + } +} + +impl Function { + pub(crate) fn slice_intrinsics_length_optimization(&mut self) { + let as_slice = self.dfg.get_intrinsic(Intrinsic::AsSlice).copied(); + let slice_insert = self.dfg.get_intrinsic(Intrinsic::SliceInsert).copied(); + let slice_remove = self.dfg.get_intrinsic(Intrinsic::SliceRemove).copied(); + let slice_push_back = self.dfg.get_intrinsic(Intrinsic::SlicePushBack).copied(); + let slice_push_front = self.dfg.get_intrinsic(Intrinsic::SlicePushFront).copied(); + let slice_pop_back = self.dfg.get_intrinsic(Intrinsic::SlicePopBack).copied(); + let slice_pop_front = self.dfg.get_intrinsic(Intrinsic::SlicePopFront).copied(); + + let ops = [ + as_slice, + slice_insert, + slice_remove, + slice_push_back, + slice_push_front, + slice_pop_back, + slice_pop_front, + ]; + if ops.iter().all(Option::is_none) { + // No slice intrinsics used in this function + return; + } + + self.simple_optimization(|context| { + let instruction_id = context.instruction_id; + let instruction = context.instruction(); + + let (target_func, arguments) = match &instruction { + Instruction::Call { func, arguments } => (func, arguments), + _ => return, + }; + + let replacement = if as_slice.is_some_and(|op| target_func == &op) { + // For `as_slice(array)` we can replace the resulting length with the length of the array + let first_argument = + arguments.first().expect("AsSlice should always have one argument"); + let array_typ = context.dfg.type_of_value(*first_argument); + let Type::Array(_, length) = array_typ else { + unreachable!("AsSlice called with non-array {}", array_typ); + }; + + let original_slice_length = context.dfg.instruction_results(instruction_id)[0]; + Some((original_slice_length, length.into())) + } else if slice_insert.is_some_and(|op| target_func == &op) + || slice_push_front.is_some_and(|op| target_func == &op) + || slice_push_back.is_some_and(|op| target_func == &op) + { + if let Some(length) = context.dfg.get_numeric_constant(arguments[0]) { + // For `slice_insert(length, ...)` we can replace the resulting length with length + 1. + // Same goes for `slice_push_front` and `slice_push_back`. + let length = length + FieldElement::one(); + let new_slice_length = context.dfg.instruction_results(instruction_id)[0]; + Some((new_slice_length, length)) + } else { + None + } + } else if slice_remove.is_some_and(|op| target_func == &op) + || slice_pop_back.is_some_and(|op| target_func == &op) + { + if let Some(length) = context.dfg.get_numeric_constant(arguments[0]) { + if !length.is_zero() { + // For `slice_remove(length, ...)` we can replace the resulting length with length - 1. + // Same goes for `slice_pop_back`. + let length = length - FieldElement::one(); + let new_slice_length = context.dfg.instruction_results(instruction_id)[0]; + Some((new_slice_length, length)) + } else { + None + } + } else { + None + } + } else if slice_pop_front.is_some_and(|op| target_func == &op) { + if let Some(length) = context.dfg.get_numeric_constant(arguments[0]) { + if !length.is_zero() { + // For `slice_pop_front(length, ...)` we can replace the resulting length with length - 1. + let length = length - FieldElement::one(); + // Note that `(popped_element, new_slice)` is returned so the new length is + // the before last result. + let results = context.dfg.instruction_results(instruction_id); + let new_slice_length = results[results.len() - 2]; + Some((new_slice_length, length)) + } else { + None + } + } else { + None + } + } else { + None + }; + + if let Some((value_to_replace, replacement)) = replacement { + let known_length = + context.dfg.make_constant(replacement, NumericType::length_type()); + context.replace_value(value_to_replace, known_length); + } + }); + } +} + +#[cfg(test)] +mod test { + use crate::assert_ssa_snapshot; + + use super::Ssa; + + #[test] + fn as_slice_length_optimization() { + // In this code we expect `return v2` to be replaced with `return u32 3` because + // that's the length of the v0 array. + let src = " + acir(inline) fn main f0 { + b0(v0: [Field; 3]): + v2, v3 = call as_slice(v0) -> (u32, [Field]) + return v2 + } + "; + let ssa = Ssa::from_str(src).unwrap(); + + let ssa = ssa.slice_instrinsics_length_optimization(); + assert_ssa_snapshot!(ssa, @r" + acir(inline) fn main f0 { + b0(v0: [Field; 3]): + v2, v3 = call as_slice(v0) -> (u32, [Field]) + return u32 3 + } + "); + } + + #[test] + fn as_slice_length_multiple_different_arrays() { + let src = " + acir(inline) fn main f0 { + b0(v0: [Field; 3], v1: [Field; 5]): + v3, v4 = call as_slice(v0) -> (u32, [Field]) + v5, v6 = call as_slice(v1) -> (u32, [Field]) + return v3, v5 + } + "; + let ssa = Ssa::from_str(src).unwrap(); + let ssa = ssa.slice_instrinsics_length_optimization(); + assert_ssa_snapshot!(ssa, @r" + acir(inline) fn main f0 { + b0(v0: [Field; 3], v1: [Field; 5]): + v3, v4 = call as_slice(v0) -> (u32, [Field]) + v5, v6 = call as_slice(v1) -> (u32, [Field]) + return u32 3, u32 5 + } + "); + } + + #[test] + fn slice_insert_optimization() { + let src = " + acir(inline) fn main f0 { + b0(): + v0 = make_array [Field 2, Field 3] : [Field] + v1, v2 = call slice_insert(u32 2, v0, u32 1, Field 4) -> (u32, [Field]) + return v1 + } + "; + let ssa = Ssa::from_str(src).unwrap(); + + // Here `v1` was replaced with 3 because we know the new length is 2 + 1 + let ssa = ssa.slice_instrinsics_length_optimization(); + assert_ssa_snapshot!(ssa, @r" + acir(inline) fn main f0 { + b0(): + v2 = make_array [Field 2, Field 3] : [Field] + v7, v8 = call slice_insert(u32 2, v2, u32 1, Field 4) -> (u32, [Field]) + return u32 3 + } + "); + } + + #[test] + fn slice_remove_optimization() { + let src = " + acir(inline) fn main f0 { + b0(): + v0 = make_array [Field 2, Field 3] : [Field] + v1, v2, v3 = call slice_remove(u32 2, v0, u32 1) -> (u32, [Field], Field) + return v1 + } + "; + let ssa = Ssa::from_str(src).unwrap(); + + // Here `v1` was replaced with 1 because we know the new length is 2 - 1 + let ssa = ssa.slice_instrinsics_length_optimization(); + assert_ssa_snapshot!(ssa, @r" + acir(inline) fn main f0 { + b0(): + v2 = make_array [Field 2, Field 3] : [Field] + v6, v7, v8 = call slice_remove(u32 2, v2, u32 1) -> (u32, [Field], Field) + return u32 1 + } + "); + } + + #[test] + fn slice_push_front_optimization() { + let src = " + acir(inline) fn main f0 { + b0(): + v0 = make_array [Field 2, Field 3] : [Field] + v1, v2 = call slice_push_front(u32 2, v0, Field 4) -> (u32, [Field]) + return v1 + } + "; + let ssa = Ssa::from_str(src).unwrap(); + + // Here `v1` was replaced with 1 because we know the new length is 2 + 1 + let ssa = ssa.slice_instrinsics_length_optimization(); + assert_ssa_snapshot!(ssa, @r" + acir(inline) fn main f0 { + b0(): + v2 = make_array [Field 2, Field 3] : [Field] + v6, v7 = call slice_push_front(u32 2, v2, Field 4) -> (u32, [Field]) + return u32 3 + } + "); + } + + #[test] + fn slice_push_back_optimization() { + let src = " + acir(inline) fn main f0 { + b0(): + v0 = make_array [Field 2, Field 3] : [Field] + v1, v2 = call slice_push_back(u32 2, v0, Field 4) -> (u32, [Field]) + return v1 + } + "; + let ssa = Ssa::from_str(src).unwrap(); + + // Here `v1` was replaced with 1 because we know the new length is 2 + 1 + let ssa = ssa.slice_instrinsics_length_optimization(); + assert_ssa_snapshot!(ssa, @r" + acir(inline) fn main f0 { + b0(): + v2 = make_array [Field 2, Field 3] : [Field] + v6, v7 = call slice_push_back(u32 2, v2, Field 4) -> (u32, [Field]) + return u32 3 + } + "); + } + + #[test] + fn slice_pop_back_optimization() { + let src = " + acir(inline) fn main f0 { + b0(): + v0 = make_array [Field 2, Field 3] : [Field] + v1, v2, v3 = call slice_pop_back(u32 2, v0) -> (u32, [Field], Field) + return v1 + } + "; + let ssa = Ssa::from_str(src).unwrap(); + + // Here `v1` was replaced with 1 because we know the new length is 2 - 1 + let ssa = ssa.slice_instrinsics_length_optimization(); + assert_ssa_snapshot!(ssa, @r" + acir(inline) fn main f0 { + b0(): + v2 = make_array [Field 2, Field 3] : [Field] + v5, v6, v7 = call slice_pop_back(u32 2, v2) -> (u32, [Field], Field) + return u32 1 + } + "); + } + + #[test] + fn slice_pop_front_optimization() { + let src = " + acir(inline) fn main f0 { + b0(): + v0 = make_array [Field 2, Field 3] : [Field] + v1, v2, v3 = call slice_pop_front(u32 2, v0) -> (Field, u32, [Field]) + return v2 + } + "; + let ssa = Ssa::from_str(src).unwrap(); + + // Here `v2` was replaced with 1 because we know the new length is 2 - 1 + let ssa = ssa.slice_instrinsics_length_optimization(); + assert_ssa_snapshot!(ssa, @r" + acir(inline) fn main f0 { + b0(): + v2 = make_array [Field 2, Field 3] : [Field] + v5, v6, v7 = call slice_pop_front(u32 2, v2) -> (Field, u32, [Field]) + return u32 1 + } + "); + } +}