From 6a84bb68cecc751b742560a0603f61c18ba1da4b Mon Sep 17 00:00:00 2001 From: sirasistant Date: Thu, 23 May 2024 15:32:16 +0000 Subject: [PATCH 1/4] feat: Improve variable liveness by considering block params --- .../src/brillig/brillig_gen.rs | 2 +- .../src/brillig/brillig_gen/brillig_block.rs | 35 ++++---- .../brillig_gen/brillig_block_variables.rs | 48 +++-------- .../src/brillig/brillig_gen/brillig_fn.rs | 31 ++----- .../brillig/brillig_gen/brillig_slice_ops.rs | 4 +- .../brillig/brillig_gen/variable_liveness.rs | 81 +++++++++++++------ 6 files changed, 92 insertions(+), 109 deletions(-) diff --git a/compiler/noirc_evaluator/src/brillig/brillig_gen.rs b/compiler/noirc_evaluator/src/brillig/brillig_gen.rs index 22814a22889..5576d673f1d 100644 --- a/compiler/noirc_evaluator/src/brillig/brillig_gen.rs +++ b/compiler/noirc_evaluator/src/brillig/brillig_gen.rs @@ -14,7 +14,7 @@ use crate::ssa::ir::function::Function; pub(crate) fn convert_ssa_function(func: &Function, enable_debug_trace: bool) -> BrilligArtifact { let mut brillig_context = BrilligContext::new(enable_debug_trace); - let mut function_context = FunctionContext::new(func, &mut brillig_context); + let mut function_context = FunctionContext::new(func); brillig_context.enter_context(FunctionContext::function_id_to_function_label(func.id())); diff --git a/compiler/noirc_evaluator/src/brillig/brillig_gen/brillig_block.rs b/compiler/noirc_evaluator/src/brillig/brillig_gen/brillig_block.rs index 6a4f9f5cc0e..0dbab80dace 100644 --- a/compiler/noirc_evaluator/src/brillig/brillig_gen/brillig_block.rs +++ b/compiler/noirc_evaluator/src/brillig/brillig_gen/brillig_block.rs @@ -7,7 +7,7 @@ use crate::brillig::brillig_ir::{ use crate::ssa::ir::dfg::CallStack; use crate::ssa::ir::instruction::ConstrainError; use crate::ssa::ir::{ - basic_block::{BasicBlock, BasicBlockId}, + basic_block::BasicBlockId, dfg::DataFlowGraph, function::FunctionId, instruction::{ @@ -49,8 +49,7 @@ impl<'block> BrilligBlock<'block> { dfg: &DataFlowGraph, ) { let live_in = function_context.liveness.get_live_in(&block_id); - let variables = - BlockVariables::new(live_in.clone(), function_context.all_block_parameters()); + let variables = BlockVariables::new(live_in.clone()); brillig_context.set_allocated_registers( variables @@ -72,9 +71,9 @@ impl<'block> BrilligBlock<'block> { let block_label = self.create_block_label_for_current_function(self.block_id); self.brillig_context.enter_context(block_label); - // Convert the block parameters + self.convert_block_params(dfg); + let block = &dfg[self.block_id]; - self.convert_block_params(block, dfg); // Convert all of the instructions into the block for instruction_id in block.instructions() { @@ -134,12 +133,8 @@ impl<'block> BrilligBlock<'block> { let target_block = &dfg[*destination_block]; for (src, dest) in arguments.iter().zip(target_block.parameters()) { // Destinations are block parameters so they should have been allocated previously. - let destination = self.variables.get_block_param( - self.function_context, - *destination_block, - *dest, - dfg, - ); + let destination = + self.variables.get_allocation(self.function_context, *dest, dfg); let source = self.convert_ssa_value(*src, dfg); self.pass_variable(source, destination); } @@ -206,10 +201,14 @@ impl<'block> BrilligBlock<'block> { } } - /// Converts SSA Block parameters into Brillig Registers. - fn convert_block_params(&mut self, block: &BasicBlock, dfg: &DataFlowGraph) { - for param_id in block.parameters() { - let value = &dfg[*param_id]; + /// Allocates the block parameters that the given block is defining + fn convert_block_params(&mut self, dfg: &DataFlowGraph) { + // We don't allocate the block parameters here, we allocate the parameters the block is defining. + // Since predecessors to a block have to know where the parameters of the block are allocated to pass data to it, + // the block parameters need to be defined/allocated before the given block. Variable liveness provides when the block parameters are defined. + // For the entry block, the defined block params will be the params of the function + any extra params of blocks it's the immediate dominator of. + for param_id in self.function_context.liveness.defined_block_params(&self.block_id) { + let value = &dfg[param_id]; let param_type = match value { Value::Param { typ, .. } => typ, _ => unreachable!("ICE: Only Param type values should appear in block parameters"), @@ -220,10 +219,10 @@ impl<'block> BrilligBlock<'block> { // Be a valid pointer to the array. // For slices, two registers are passed, the pointer to the data and a register holding the size of the slice. Type::Numeric(_) | Type::Array(..) | Type::Slice(..) | Type::Reference(_) => { - self.variables.get_block_param( + self.variables.define_variable( self.function_context, - self.block_id, - *param_id, + self.brillig_context, + param_id, dfg, ); } diff --git a/compiler/noirc_evaluator/src/brillig/brillig_gen/brillig_block_variables.rs b/compiler/noirc_evaluator/src/brillig/brillig_gen/brillig_block_variables.rs index 892e82d771a..fb9a8577d94 100644 --- a/compiler/noirc_evaluator/src/brillig/brillig_gen/brillig_block_variables.rs +++ b/compiler/noirc_evaluator/src/brillig/brillig_gen/brillig_block_variables.rs @@ -9,7 +9,6 @@ use crate::{ BrilligContext, }, ssa::ir::{ - basic_block::BasicBlockId, dfg::DataFlowGraph, types::{CompositeType, Type}, value::ValueId, @@ -21,18 +20,13 @@ use super::brillig_fn::FunctionContext; #[derive(Debug, Default)] pub(crate) struct BlockVariables { available_variables: HashSet, - block_parameters: HashSet, available_constants: HashMap, } impl BlockVariables { /// Creates a BlockVariables instance. It uses the variables that are live in to the block and the global available variables (block parameters) - pub(crate) fn new(live_in: HashSet, all_block_parameters: HashSet) -> Self { - BlockVariables { - available_variables: live_in.into_iter().chain(all_block_parameters.clone()).collect(), - block_parameters: all_block_parameters, - ..Default::default() - } + pub(crate) fn new(live_in: HashSet) -> Self { + BlockVariables { available_variables: live_in, ..Default::default() } } /// Returns all non-constant variables that have not been removed at this point. @@ -92,16 +86,13 @@ impl BlockVariables { brillig_context: &mut BrilligContext, ) { assert!(self.available_variables.remove(value_id), "ICE: Variable is not available"); - // Block parameters should not be deallocated - if !self.block_parameters.contains(value_id) { - let variable = function_context - .ssa_value_allocations - .get(value_id) - .expect("ICE: Variable allocation not found"); - variable.extract_registers().iter().for_each(|register| { - brillig_context.deallocate_register(*register); - }); - } + let variable = function_context + .ssa_value_allocations + .get(value_id) + .expect("ICE: Variable allocation not found"); + variable.extract_registers().iter().for_each(|register| { + brillig_context.deallocate_register(*register); + }); } /// For a given SSA value id, return the corresponding cached allocation. @@ -155,27 +146,6 @@ impl BlockVariables { pub(crate) fn dump_constants(&mut self) { self.available_constants.clear(); } - - /// For a given block parameter, return the allocation that was done globally to the function. - pub(crate) fn get_block_param( - &mut self, - function_context: &FunctionContext, - block_id: BasicBlockId, - value_id: ValueId, - dfg: &DataFlowGraph, - ) -> BrilligVariable { - let value_id = dfg.resolve(value_id); - assert!( - function_context - .block_parameters - .get(&block_id) - .expect("Block not found") - .contains(&value_id), - "Value is not a block parameter" - ); - - *function_context.ssa_value_allocations.get(&value_id).expect("Block param not found") - } } /// Computes the length of an array. This will match with the indexes that SSA will issue diff --git a/compiler/noirc_evaluator/src/brillig/brillig_gen/brillig_fn.rs b/compiler/noirc_evaluator/src/brillig/brillig_gen/brillig_fn.rs index 617e400b92f..000d1230ece 100644 --- a/compiler/noirc_evaluator/src/brillig/brillig_gen/brillig_fn.rs +++ b/compiler/noirc_evaluator/src/brillig/brillig_gen/brillig_fn.rs @@ -4,7 +4,6 @@ use crate::{ brillig::brillig_ir::{ artifact::{BrilligParameter, Label}, brillig_variable::{get_bit_size_from_ssa_type, BrilligVariable}, - BrilligContext, }, ssa::ir::{ basic_block::BasicBlockId, @@ -14,16 +13,14 @@ use crate::{ value::ValueId, }, }; -use fxhash::{FxHashMap as HashMap, FxHashSet as HashSet}; +use fxhash::FxHashMap as HashMap; -use super::{brillig_block_variables::allocate_value, variable_liveness::VariableLiveness}; +use super::variable_liveness::VariableLiveness; pub(crate) struct FunctionContext { pub(crate) function_id: FunctionId, /// Map from SSA values its allocation. Since values can be only defined once in SSA form, we insert them here on when we allocate them at their definition. pub(crate) ssa_value_allocations: HashMap, - /// Block parameters are pre allocated at the function level. - pub(crate) block_parameters: HashMap>, /// The block ids of the function in reverse post order. pub(crate) blocks: Vec, /// Liveness information for each variable in the function. @@ -31,40 +28,22 @@ pub(crate) struct FunctionContext { } impl FunctionContext { - /// Creates a new function context. It will allocate parameters for all blocks and compute the liveness of every variable. - pub(crate) fn new(function: &Function, brillig_context: &mut BrilligContext) -> Self { + /// Creates a new function context. It will compute the liveness of every variable. + pub(crate) fn new(function: &Function) -> Self { let id = function.id(); let mut reverse_post_order = Vec::new(); reverse_post_order.extend_from_slice(PostOrder::with_function(function).as_slice()); reverse_post_order.reverse(); - let mut block_parameters = HashMap::default(); - let mut ssa_variable_to_register_or_memory = HashMap::default(); - - for &block_id in &reverse_post_order { - let block = &function.dfg[block_id]; - let parameters = block.parameters().to_vec(); - parameters.iter().for_each(|&value_id| { - let variable = allocate_value(value_id, brillig_context, &function.dfg); - ssa_variable_to_register_or_memory.insert(value_id, variable); - }); - block_parameters.insert(block_id, parameters); - } - Self { function_id: id, - ssa_value_allocations: ssa_variable_to_register_or_memory, - block_parameters, + ssa_value_allocations: HashMap::default(), blocks: reverse_post_order, liveness: VariableLiveness::from_function(function), } } - pub(crate) fn all_block_parameters(&self) -> HashSet { - self.block_parameters.values().flat_map(|parameters| parameters.iter()).cloned().collect() - } - /// Creates a function label from a given SSA function id. pub(crate) fn function_id_to_function_label(function_id: FunctionId) -> Label { function_id.to_string() diff --git a/compiler/noirc_evaluator/src/brillig/brillig_gen/brillig_slice_ops.rs b/compiler/noirc_evaluator/src/brillig/brillig_gen/brillig_slice_ops.rs index 3e1515b1eed..491086e8c0f 100644 --- a/compiler/noirc_evaluator/src/brillig/brillig_gen/brillig_slice_ops.rs +++ b/compiler/noirc_evaluator/src/brillig/brillig_gen/brillig_slice_ops.rs @@ -377,9 +377,9 @@ mod tests { builder.set_runtime(RuntimeType::Brillig); let ssa = builder.finish(); - let mut brillig_context = create_context(); + let brillig_context = create_context(); - let function_context = FunctionContext::new(ssa.main(), &mut brillig_context); + let function_context = FunctionContext::new(ssa.main()); (ssa, function_context, brillig_context) } diff --git a/compiler/noirc_evaluator/src/brillig/brillig_gen/variable_liveness.rs b/compiler/noirc_evaluator/src/brillig/brillig_gen/variable_liveness.rs index c9f1cd1e247..24dcf8999f6 100644 --- a/compiler/noirc_evaluator/src/brillig/brillig_gen/variable_liveness.rs +++ b/compiler/noirc_evaluator/src/brillig/brillig_gen/variable_liveness.rs @@ -104,23 +104,6 @@ fn variables_used_in_block(block: &BasicBlock, dfg: &DataFlowGraph) -> Vec; -fn compute_defined_variables(block: &BasicBlock, dfg: &DataFlowGraph) -> Variables { - let mut defined_vars = HashSet::default(); - - for parameter in block.parameters() { - defined_vars.insert(dfg.resolve(*parameter)); - } - - for instruction_id in block.instructions() { - let result_values = dfg.instruction_results(*instruction_id); - for result_value in result_values { - defined_vars.insert(dfg.resolve(*result_value)); - } - } - - defined_vars -} - fn compute_used_before_def( block: &BasicBlock, dfg: &DataFlowGraph, @@ -142,6 +125,8 @@ pub(crate) struct VariableLiveness { live_in: HashMap, /// The variables that stop being alive after each specific instruction last_uses: HashMap, + /// The list of block params the given block is defining. The order matters for the entry block, so it's a vec. + param_definitions: HashMap>, } impl VariableLiveness { @@ -150,8 +135,15 @@ impl VariableLiveness { let cfg = ControlFlowGraph::with_function(func); let post_order = PostOrder::with_function(func); - let mut instance = - Self { cfg, post_order, live_in: HashMap::default(), last_uses: HashMap::default() }; + let mut instance = Self { + cfg, + post_order, + live_in: HashMap::default(), + last_uses: HashMap::default(), + param_definitions: HashMap::default(), + }; + + instance.compute_block_param_definitions(func); instance.compute_live_in_of_blocks(func); @@ -179,6 +171,30 @@ impl VariableLiveness { self.last_uses.get(block_id).expect("Last uses should have been calculated") } + /// Retrieves the list of block params the given block is defining. + /// Block params are defined before the block that owns them (since they are used by the predecessor blocks). They must be defined in the immediate dominator. + /// This is the last point where the block param can be allocated without it being allocated in different places in different branches. + pub(crate) fn defined_block_params(&self, block_id: &BasicBlockId) -> Vec { + self.param_definitions.get(block_id).cloned().unwrap_or_default() + } + + fn compute_block_param_definitions(&mut self, func: &Function) { + let tree = DominatorTree::with_cfg_and_post_order(&self.cfg, &self.post_order); + + // Going in reverse post order to process the entry block first + let mut reverse_post_order = Vec::new(); + reverse_post_order.extend_from_slice(self.post_order.as_slice()); + reverse_post_order.reverse(); + for block in reverse_post_order { + let params = func.dfg[block].parameters(); + // If it has no dominator, it's the entry block + let dominator_block = tree.immediate_dominator(block).unwrap_or(func.entry_block()); + let definitions_for_the_dominator = + self.param_definitions.entry(dominator_block).or_default(); + definitions_for_the_dominator.extend(params.iter()); + } + } + fn compute_live_in_of_blocks(&mut self, func: &Function) { let back_edges = find_back_edges(func, &self.cfg, &self.post_order); @@ -197,9 +213,10 @@ impl VariableLiveness { block_id: BasicBlockId, back_edges: &HashSet, ) { - let block = &func.dfg[block_id]; + let defined = self.compute_defined_variables(block_id, &func.dfg); + + let block: &BasicBlock = &func.dfg[block_id]; - let defined = compute_defined_variables(block, &func.dfg); let used_before_def = compute_used_before_def(block, &func.dfg, &defined); let mut live_out = HashSet::default(); @@ -222,6 +239,24 @@ impl VariableLiveness { self.live_in.insert(block_id, used_before_def.union(&passthrough_vars).cloned().collect()); } + fn compute_defined_variables(&self, block_id: BasicBlockId, dfg: &DataFlowGraph) -> Variables { + let block: &BasicBlock = &dfg[block_id]; + let mut defined_vars = HashSet::default(); + + for parameter in self.defined_block_params(&block_id) { + defined_vars.insert(dfg.resolve(parameter)); + } + + for instruction_id in block.instructions() { + let result_values = dfg.instruction_results(*instruction_id); + for result_value in result_values { + defined_vars.insert(dfg.resolve(*result_value)); + } + } + + defined_vars + } + fn update_live_ins_within_loop(&mut self, back_edge: BackEdge) { let header_live_ins = self .live_in @@ -513,12 +548,12 @@ mod test { let liveness = VariableLiveness::from_function(func); assert!(liveness.get_live_in(&func.entry_block()).is_empty()); - assert_eq!(liveness.get_live_in(&b1), &FxHashSet::from_iter([v0, v1, v3].into_iter())); + assert_eq!(liveness.get_live_in(&b1), &FxHashSet::from_iter([v0, v1, v3, v4].into_iter())); assert_eq!(liveness.get_live_in(&b3), &FxHashSet::from_iter([v3].into_iter())); assert_eq!(liveness.get_live_in(&b2), &FxHashSet::from_iter([v0, v1, v3, v4].into_iter())); assert_eq!( liveness.get_live_in(&b4), - &FxHashSet::from_iter([v0, v1, v3, v4, v6].into_iter()) + &FxHashSet::from_iter([v0, v1, v3, v4, v6, v7].into_iter()) ); assert_eq!(liveness.get_live_in(&b6), &FxHashSet::from_iter([v0, v1, v3, v4].into_iter())); assert_eq!( From bbd2ace674fe2ebf1b973d0b3c476c07e499fd81 Mon Sep 17 00:00:00 2001 From: sirasistant Date: Thu, 23 May 2024 16:21:42 +0000 Subject: [PATCH 2/4] fix: unused block params --- .../brillig/brillig_gen/variable_liveness.rs | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/compiler/noirc_evaluator/src/brillig/brillig_gen/variable_liveness.rs b/compiler/noirc_evaluator/src/brillig/brillig_gen/variable_liveness.rs index 24dcf8999f6..f1c1d7294e4 100644 --- a/compiler/noirc_evaluator/src/brillig/brillig_gen/variable_liveness.rs +++ b/compiler/noirc_evaluator/src/brillig/brillig_gen/variable_liveness.rs @@ -93,6 +93,9 @@ fn variables_used_in_block(block: &BasicBlock, dfg: &DataFlowGraph) -> Vec Date: Fri, 24 May 2024 08:11:50 +0000 Subject: [PATCH 3/4] improve test --- .../brillig/brillig_gen/variable_liveness.rs | 49 ++++++++++++++----- 1 file changed, 36 insertions(+), 13 deletions(-) diff --git a/compiler/noirc_evaluator/src/brillig/brillig_gen/variable_liveness.rs b/compiler/noirc_evaluator/src/brillig/brillig_gen/variable_liveness.rs index f1c1d7294e4..52eded81919 100644 --- a/compiler/noirc_evaluator/src/brillig/brillig_gen/variable_liveness.rs +++ b/compiler/noirc_evaluator/src/brillig/brillig_gen/variable_liveness.rs @@ -580,33 +580,56 @@ mod test { } #[test] - fn unused_block_param() { - // brillig fn main f1 { - // b0(): - // jmp b1(Field 0) - // b1(v1: Field): - // return - // } + fn block_params() { + // brillig fn main f0 { + // b0(v0: u1): + // jmpif v0 then: b1, else: b2 + // b1(): + // jmp b3(Field 27, Field 29) + // b3(v1: Field, v2: Field): + // return v1 + // b2(): + // jmp b3(Field 28, Field 40) + // } let main_id = Id::test_new(1); let mut builder = FunctionBuilder::new("main".into(), main_id); builder.set_runtime(RuntimeType::Brillig); + let v0 = builder.add_parameter(Type::bool()); + let b1 = builder.insert_block(); + let b2 = builder.insert_block(); + let b3 = builder.insert_block(); - let v0 = builder.numeric_constant(0u128, Type::field()); - builder.terminate_with_jmp(b1, vec![v0]); + builder.terminate_with_jmpif(v0, b1, b2); builder.switch_to_block(b1); + let twenty_seven = builder.field_constant(27_u128); + let twenty_nine = builder.field_constant(29_u128); + builder.terminate_with_jmp(b3, vec![twenty_seven, twenty_nine]); + + builder.switch_to_block(b3); + let v1 = builder.add_block_parameter(b3, Type::field()); + let v2 = builder.add_block_parameter(b3, Type::field()); + builder.terminate_with_return(vec![v1]); - let v1 = builder.add_block_parameter(b1, Type::field()); - builder.terminate_with_return(vec![]); + builder.switch_to_block(b2); + let twenty_eight = builder.field_constant(28_u128); + let forty = builder.field_constant(40_u128); + builder.terminate_with_jmp(b3, vec![twenty_eight, forty]); let ssa = builder.finish(); let func = ssa.main(); let liveness = VariableLiveness::from_function(func); - assert_eq!(liveness.defined_block_params(&func.entry_block()), vec![v1]); - assert_eq!(liveness.get_live_in(&b1), &FxHashSet::from_iter([v1].into_iter())); + // Entry point defines its own params and also b3's params. + assert_eq!(liveness.defined_block_params(&func.entry_block()), vec![v0, v1, v2]); + assert_eq!(liveness.defined_block_params(&b1), vec![]); + assert_eq!(liveness.defined_block_params(&b2), vec![]); + assert_eq!(liveness.defined_block_params(&b3), vec![]); + + assert_eq!(liveness.get_live_in(&b1), &FxHashSet::from_iter([v1, v2].into_iter())); + assert_eq!(liveness.get_live_in(&b2), &FxHashSet::from_iter([v1, v2].into_iter())); } } From 8d8c540f66e92e50aa51ce6c891492373c666ce7 Mon Sep 17 00:00:00 2001 From: sirasistant Date: Fri, 24 May 2024 15:11:36 +0000 Subject: [PATCH 4/4] test: added e2e test trying to overflow the stack via too many block params --- .../Nargo.toml | 6 ++ .../Prover.toml | 1 + .../src/main.nr | 91 +++++++++++++++++++ 3 files changed, 98 insertions(+) create mode 100644 test_programs/execution_success/brillig_block_parameter_liveness/Nargo.toml create mode 100644 test_programs/execution_success/brillig_block_parameter_liveness/Prover.toml create mode 100644 test_programs/execution_success/brillig_block_parameter_liveness/src/main.nr diff --git a/test_programs/execution_success/brillig_block_parameter_liveness/Nargo.toml b/test_programs/execution_success/brillig_block_parameter_liveness/Nargo.toml new file mode 100644 index 00000000000..e094862a1dc --- /dev/null +++ b/test_programs/execution_success/brillig_block_parameter_liveness/Nargo.toml @@ -0,0 +1,6 @@ +[package] +name = "brillig_block_parameter_liveness" +type = "bin" +authors = [""] + +[dependencies] diff --git a/test_programs/execution_success/brillig_block_parameter_liveness/Prover.toml b/test_programs/execution_success/brillig_block_parameter_liveness/Prover.toml new file mode 100644 index 00000000000..6b7fd9c6ab6 --- /dev/null +++ b/test_programs/execution_success/brillig_block_parameter_liveness/Prover.toml @@ -0,0 +1 @@ +conditions = ["1", "0", "1", "0", "1"] diff --git a/test_programs/execution_success/brillig_block_parameter_liveness/src/main.nr b/test_programs/execution_success/brillig_block_parameter_liveness/src/main.nr new file mode 100644 index 00000000000..142290ecbe2 --- /dev/null +++ b/test_programs/execution_success/brillig_block_parameter_liveness/src/main.nr @@ -0,0 +1,91 @@ +// Tests that we run liveness in block parameters by trying to create too many block parameters to fit in the stack + +// Uses up 10 stack items +struct Inner { + a: u64, + b: u64, + c: u64, + d: u64, + e: u64, + f: u64, + g: u64, + h: u64, + i: u64, + j: u64, +} + +// Uses up 50 stack items +struct Middle { + inner_a: Inner, + inner_b: Inner, + inner_c: Inner, + inner_d: Inner, + inner_e: Inner, +} + +// Uses up 500 stack items +struct Outer { + middle_a: Middle, + middle_b: Middle, + middle_c: Middle, + middle_d: Middle, + middle_e: Middle, + middle_f: Middle, + middle_g: Middle, + middle_h: Middle, +} + +// If we don't take into account block parameter liveness, this function will need 5*500=2500 stack items +unconstrained fn main(conditions: [bool; 5]) -> pub Outer { + let out0 = if conditions[0] { + let mut outer: Outer = dep::std::unsafe::zeroed(); + outer.middle_a.inner_a.a = 1; + outer + } else { + let mut outer: Outer= dep::std::unsafe::zeroed(); + outer.middle_f.inner_c.d = 2; + outer + }; + + let out1 = if conditions[1] { + let mut new_outer = out0; + new_outer.middle_a.inner_a.b = 3; + new_outer + } else { + let mut new_outer = out0; + new_outer.middle_f.inner_c.c = 4; + new_outer + }; + + let out2 = if conditions[2] { + let mut new_outer = out1; + new_outer.middle_a.inner_a.c = 5; + new_outer + } else { + let mut new_outer = out1; + new_outer.middle_f.inner_c.b = 6; + new_outer + }; + + let out3 = if conditions[3] { + let mut new_outer = out2; + new_outer.middle_a.inner_a.d = 7; + new_outer + } else { + let mut new_outer = out2; + new_outer.middle_f.inner_c.a = 8; + new_outer + }; + + let out4 = if conditions[4] { + let mut new_outer = out3; + new_outer.middle_a.inner_a.f = 9; + new_outer + } else { + let mut new_outer = out3; + new_outer.middle_f.inner_c.f = 10; + new_outer + }; + + out4 +}