diff --git a/crates/noirc_evaluator/src/ssa_refactor.rs b/crates/noirc_evaluator/src/ssa_refactor.rs index 83f76c85ec1..34061227336 100644 --- a/crates/noirc_evaluator/src/ssa_refactor.rs +++ b/crates/noirc_evaluator/src/ssa_refactor.rs @@ -17,6 +17,7 @@ use self::acir_gen::Acir; mod acir_gen; mod ir; +mod opt; mod ssa_builder; pub mod ssa_gen; @@ -24,7 +25,7 @@ pub mod ssa_gen; /// form and performing optimizations there. When finished, /// convert the final SSA into ACIR and return it. pub fn optimize_into_acir(program: Program) -> Acir { - ssa_gen::generate_ssa(program).into_acir() + ssa_gen::generate_ssa(program).inline_functions().into_acir() } /// Compiles the Program into ACIR and applies optimizations to the arithmetic gates /// This is analogous to `ssa:create_circuit` and this method is called when one wants diff --git a/crates/noirc_evaluator/src/ssa_refactor/ir/basic_block.rs b/crates/noirc_evaluator/src/ssa_refactor/ir/basic_block.rs index e8b09f518d8..30526bc296e 100644 --- a/crates/noirc_evaluator/src/ssa_refactor/ir/basic_block.rs +++ b/crates/noirc_evaluator/src/ssa_refactor/ir/basic_block.rs @@ -29,10 +29,10 @@ pub(crate) struct BasicBlock { pub(crate) type BasicBlockId = Id; impl BasicBlock { - /// Create a new BasicBlock with the given parameters. + /// Create a new BasicBlock with the given instructions. /// Parameters can also be added later via BasicBlock::add_parameter - pub(crate) fn new(parameters: Vec) -> Self { - Self { parameters, instructions: Vec::new(), terminator: None } + pub(crate) fn new(instructions: Vec) -> Self { + Self { parameters: Vec::new(), instructions, terminator: None } } /// Returns the parameters of this block @@ -57,6 +57,11 @@ impl BasicBlock { &self.instructions } + /// Retrieve a mutable reference to all instructions in this block. + pub(crate) fn instructions_mut(&mut self) -> &mut Vec { + &mut self.instructions + } + /// Sets the terminator instruction of this block. /// /// A properly-constructed block will always terminate with a TerminatorInstruction - @@ -91,8 +96,10 @@ impl BasicBlock { /// Removes the given instruction from this block if present or panics otherwise. pub(crate) fn remove_instruction(&mut self, instruction: InstructionId) { + // Iterate in reverse here as an optimization since remove_instruction is most + // often called to remove instructions at the end of a block. let index = - self.instructions.iter().position(|id| *id == instruction).unwrap_or_else(|| { + self.instructions.iter().rev().position(|id| *id == instruction).unwrap_or_else(|| { panic!("remove_instruction: No such instruction {instruction:?} in block") }); self.instructions.remove(index); diff --git a/crates/noirc_evaluator/src/ssa_refactor/ir/dfg.rs b/crates/noirc_evaluator/src/ssa_refactor/ir/dfg.rs index 67569c6a4c2..3ab345f06b9 100644 --- a/crates/noirc_evaluator/src/ssa_refactor/ir/dfg.rs +++ b/crates/noirc_evaluator/src/ssa_refactor/ir/dfg.rs @@ -13,7 +13,6 @@ use super::{ }; use acvm::FieldElement; -use iter_extended::vecmap; /// The DataFlowGraph contains most of the actual data in a function including /// its blocks, instructions, and values. This struct is largely responsible for @@ -69,22 +68,6 @@ impl DataFlowGraph { self.blocks.insert(BasicBlock::new(Vec::new())) } - /// Creates a new basic block with the given parameters. - /// After being created, the block is unreachable in the current function - /// until another block is made to jump to it. - pub(crate) fn make_block_with_parameters( - &mut self, - parameter_types: impl Iterator, - ) -> BasicBlockId { - self.blocks.insert_with_id(|entry_block| { - let parameters = vecmap(parameter_types.enumerate(), |(position, typ)| { - self.values.insert(Value::Param { block: entry_block, position, typ }) - }); - - BasicBlock::new(parameters) - }) - } - /// Get an iterator over references to each basic block within the dfg, paired with the basic /// block's id. /// @@ -95,6 +78,7 @@ impl DataFlowGraph { self.blocks.iter() } + /// Returns the parameters of the given block pub(crate) fn block_parameters(&self, block: BasicBlockId) -> &[ValueId] { self.blocks[block].parameters() } diff --git a/crates/noirc_evaluator/src/ssa_refactor/ir/dom.rs b/crates/noirc_evaluator/src/ssa_refactor/ir/dom.rs index 9a0916f62c8..dba656838b8 100644 --- a/crates/noirc_evaluator/src/ssa_refactor/ir/dom.rs +++ b/crates/noirc_evaluator/src/ssa_refactor/ir/dom.rs @@ -279,7 +279,7 @@ mod tests { builder.terminate_with_return(vec![]); let ssa = builder.finish(); - let func = ssa.functions.first().unwrap(); + let func = ssa.main(); let block0_id = func.entry_block(); let dt = DominatorTree::with_function(func); @@ -383,7 +383,7 @@ mod tests { builder.terminate_with_jmp(block1_id, vec![]); let ssa = builder.finish(); - let func = ssa.functions.first().unwrap(); + let func = ssa.main(); let block0_id = func.entry_block(); let dt = DominatorTree::with_function(func); diff --git a/crates/noirc_evaluator/src/ssa_refactor/ir/function.rs b/crates/noirc_evaluator/src/ssa_refactor/ir/function.rs index 8d90a139118..f37448462b7 100644 --- a/crates/noirc_evaluator/src/ssa_refactor/ir/function.rs +++ b/crates/noirc_evaluator/src/ssa_refactor/ir/function.rs @@ -2,6 +2,7 @@ use super::basic_block::BasicBlockId; use super::dfg::DataFlowGraph; use super::map::Id; use super::types::Type; +use super::value::ValueId; /// A function holds a list of instructions. /// These instructions are further grouped into Basic blocks @@ -10,7 +11,7 @@ use super::types::Type; /// To reference external functions its FunctionId can be used but this /// cannot be checked for correctness until inlining is performed. #[derive(Debug)] -pub struct Function { +pub(crate) struct Function { /// The first basic block in the function entry_block: BasicBlockId, @@ -54,6 +55,12 @@ impl Function { pub(crate) fn entry_block(&self) -> BasicBlockId { self.entry_block } + + /// Returns the parameters of this function. + /// The parameters will always match that of this function's entry block. + pub(crate) fn parameters(&self) -> &[ValueId] { + self.dfg.block_parameters(self.entry_block) + } } /// FunctionId is a reference for a function diff --git a/crates/noirc_evaluator/src/ssa_refactor/ir/instruction.rs b/crates/noirc_evaluator/src/ssa_refactor/ir/instruction.rs index 66f8b1e3b17..812d12b23a3 100644 --- a/crates/noirc_evaluator/src/ssa_refactor/ir/instruction.rs +++ b/crates/noirc_evaluator/src/ssa_refactor/ir/instruction.rs @@ -1,4 +1,5 @@ use acvm::acir::BlackBoxFunc; +use iter_extended::vecmap; use super::{basic_block::BasicBlockId, map::Id, types::Type, value::ValueId}; @@ -114,6 +115,42 @@ impl Instruction { Instruction::Load { .. } | Instruction::Call { .. } => InstructionResultType::Unknown, } } + + /// True if this instruction requires specifying the control type variables when + /// inserting this instruction into a DataFlowGraph. + pub(crate) fn requires_ctrl_typevars(&self) -> bool { + matches!(self.result_type(), InstructionResultType::Unknown) + } + + /// Maps each ValueId inside this instruction to a new ValueId, returning the new instruction. + /// Note that the returned instruction is fresh and will not have an assigned InstructionId + /// until it is manually inserted in a DataFlowGraph later. + pub(crate) fn map_values(&self, mut f: impl FnMut(ValueId) -> ValueId) -> Instruction { + match self { + Instruction::Binary(binary) => Instruction::Binary(Binary { + lhs: f(binary.lhs), + rhs: f(binary.rhs), + operator: binary.operator, + }), + Instruction::Cast(value, typ) => Instruction::Cast(f(*value), *typ), + Instruction::Not(value) => Instruction::Not(f(*value)), + Instruction::Truncate { value, bit_size, max_bit_size } => Instruction::Truncate { + value: f(*value), + bit_size: *bit_size, + max_bit_size: *max_bit_size, + }, + Instruction::Constrain(value) => Instruction::Constrain(f(*value)), + Instruction::Call { func, arguments } => Instruction::Call { + func: f(*func), + arguments: vecmap(arguments.iter().copied(), f), + }, + Instruction::Allocate { size } => Instruction::Allocate { size: *size }, + Instruction::Load { address } => Instruction::Load { address: f(*address) }, + Instruction::Store { address, value } => { + Instruction::Store { address: f(*address), value: f(*value) } + } + } + } } /// The possible return values for Instruction::return_types @@ -191,7 +228,7 @@ impl Binary { /// All binary operators are also only for numeric types. To implement /// e.g. equality for a compound type like a struct, one must add a /// separate Eq operation for each field and combine them later with And. -#[derive(Debug, PartialEq, Eq, Hash, Clone)] +#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)] pub(crate) enum BinaryOp { /// Addition of lhs + rhs. Add, diff --git a/crates/noirc_evaluator/src/ssa_refactor/ir/map.rs b/crates/noirc_evaluator/src/ssa_refactor/ir/map.rs index 14ea521359d..43baf4430c7 100644 --- a/crates/noirc_evaluator/src/ssa_refactor/ir/map.rs +++ b/crates/noirc_evaluator/src/ssa_refactor/ir/map.rs @@ -45,6 +45,18 @@ impl std::hash::Hash for Id { } } +impl PartialOrd for Id { + fn partial_cmp(&self, other: &Self) -> Option { + self.index.partial_cmp(&other.index) + } +} + +impl Ord for Id { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.index.cmp(&other.index) + } +} + impl Eq for Id {} impl PartialEq for Id { @@ -272,6 +284,12 @@ pub(crate) struct AtomicCounter { } impl AtomicCounter { + /// Create a new counter starting after the given Id. + /// Use AtomicCounter::default() to start at zero. + pub(crate) fn starting_after(id: Id) -> Self { + Self { next: AtomicUsize::new(id.index + 1), _marker: Default::default() } + } + /// Return the next fresh id pub(crate) fn next(&self) -> Id { Id::new(self.next.fetch_add(1, Ordering::Relaxed)) diff --git a/crates/noirc_evaluator/src/ssa_refactor/opt/inlining.rs b/crates/noirc_evaluator/src/ssa_refactor/opt/inlining.rs new file mode 100644 index 00000000000..6e7c9848748 --- /dev/null +++ b/crates/noirc_evaluator/src/ssa_refactor/opt/inlining.rs @@ -0,0 +1,420 @@ +//! This module defines the function inlining pass for the SSA IR. +//! The purpose of this pass is to inline the instructions of each function call +//! within the function caller. If all function calls are known, there will only +//! be a single function remaining when the pass finishes. +use std::collections::{HashMap, HashSet}; + +use iter_extended::vecmap; + +use crate::ssa_refactor::{ + ir::{ + basic_block::BasicBlockId, + function::{Function, FunctionId}, + instruction::{Instruction, InstructionId, TerminatorInstruction}, + value::{Value, ValueId}, + }, + ssa_builder::FunctionBuilder, + ssa_gen::Ssa, +}; + +/// An arbitrary limit to the maximum number of recursive call +/// frames at any point in time. +const RECURSION_LIMIT: u32 = 1000; + +impl Ssa { + /// Inline all functions within the IR. + /// + /// In the case of recursive functions, this will attempt + /// to recursively inline until the RECURSION_LIMIT is reached. + /// + /// Functions are recursively inlined into main until either we finish + /// inlining all functions or we encounter a function whose function id is not known. + /// When the later happens, the call instruction is kept in addition to the function + /// it refers to. The function it refers to is kept unmodified without any inlining + /// changes. This is because if the function's id later becomes known by a later + /// pass, we would need to re-run all of inlining anyway to inline it, so we might + /// as well save the work for later instead of performing it twice. + pub(crate) fn inline_functions(self) -> Ssa { + InlineContext::new(&self).inline_all(self) + } +} + +/// The context for the function inlining pass. +/// +/// This works using an internal FunctionBuilder to build a new main function from scratch. +/// Doing it this way properly handles importing instructions between functions and lets us +/// reuse the existing API at the cost of essentially cloning each of main's instructions. +struct InlineContext { + recursion_level: u32, + builder: FunctionBuilder, + + /// True if we failed to inline at least one call. If this is still false when finishing + /// inlining we can remove all other functions from the resulting Ssa struct and keep only + /// the function that was inlined into. + failed_to_inline_a_call: bool, +} + +/// The per-function inlining context contains information that is only valid for one function. +/// For example, each function has its own DataFlowGraph, and thus each function needs a translation +/// layer to translate between BlockId to BlockId for the current function and the function to +/// inline into. The same goes for ValueIds, InstructionIds, and for storing other data like +/// parameter to argument mappings. +struct PerFunctionContext<'function> { + /// The source function is the function we're currently inlining into the function being built. + source_function: &'function Function, + + /// The shared inlining context for all functions. This notably contains the FunctionBuilder used + /// to build the function we're inlining into. + context: &'function mut InlineContext, + + /// Maps ValueIds in the function being inlined to the new ValueIds to use in the function + /// being inlined into. This mapping also contains the mapping from parameter values to + /// argument values. + values: HashMap, + + /// Maps BasicBlockIds in the function being inlined to the new BasicBlockIds to use in the + /// function being inlined into. + blocks: HashMap, + + /// Maps InstructionIds from the function being inlined to the function being inlined into. + instructions: HashMap, + + /// The TerminatorInstruction::Return in the source_function will be mapped to a jmp to + /// this block in the destination function instead. + return_destination: BasicBlockId, + + /// True if we're currently working on the main function. + inlining_main: bool, +} + +impl InlineContext { + /// Create a new context object for the function inlining pass. + /// This starts off with an empty mapping of instructions for main's parameters. + /// The function being inlined into will always be the main function, although it is + /// actually a copy that is created in case the original main is still needed from a function + /// that could not be inlined calling it. + fn new(ssa: &Ssa) -> InlineContext { + let main_name = ssa.main().name().to_owned(); + let builder = FunctionBuilder::new(main_name, ssa.next_id.next()); + Self { builder, recursion_level: 0, failed_to_inline_a_call: false } + } + + /// Start inlining the main function and all functions reachable from it. + fn inline_all(mut self, ssa: Ssa) -> Ssa { + let main = ssa.main(); + let mut context = PerFunctionContext::new(&mut self, main); + context.inlining_main = true; + + // The main block is already inserted so we have to add it to context.blocks and add + // its parameters here. Failing to do so would cause context.translate_block() to add + // a fresh block for the entry block rather than use the existing one. + let entry_block = context.context.builder.current_function.entry_block(); + let original_parameters = context.source_function.parameters(); + + for parameter in original_parameters { + let typ = context.source_function.dfg.type_of_value(*parameter); + let new_parameter = context.context.builder.add_block_parameter(entry_block, typ); + context.values.insert(*parameter, new_parameter); + } + + context.blocks.insert(context.source_function.entry_block(), entry_block); + context.inline_blocks(&ssa); + self.finish(ssa) + } + + /// Inlines a function into the current function and returns the translated return values + /// of the inlined function. + fn inline_function(&mut self, ssa: &Ssa, id: FunctionId, arguments: &[ValueId]) -> &[ValueId] { + self.recursion_level += 1; + + if self.recursion_level > RECURSION_LIMIT { + panic!( + "Attempted to recur more than {RECURSION_LIMIT} times during function inlining." + ); + } + + let source_function = &ssa.functions[&id]; + let mut context = PerFunctionContext::new(self, source_function); + + let parameters = source_function.parameters(); + assert_eq!(parameters.len(), arguments.len()); + context.values = parameters.iter().copied().zip(arguments.iter().copied()).collect(); + + let current_block = context.context.builder.current_block(); + context.blocks.insert(source_function.entry_block(), current_block); + + context.inline_blocks(ssa); + let return_destination = context.return_destination; + self.builder.block_parameters(return_destination) + } + + /// Finish inlining and return the new Ssa struct with the inlined version of main. + /// If any functions failed to inline, they are not removed from the final Ssa struct. + fn finish(self, mut ssa: Ssa) -> Ssa { + let mut new_ssa = self.builder.finish(); + assert_eq!(new_ssa.functions.len(), 1); + + // If we failed to inline any call, any function may still be reachable so we + // don't remove any from the final program. We could be more precise here and + // do a reachability analysis but it should be fine to keep the extra functions + // around longer if they are not called. + if self.failed_to_inline_a_call { + let new_main = new_ssa.functions.pop_first().unwrap().1; + ssa.main_id = new_main.id(); + ssa.functions.insert(new_main.id(), new_main); + ssa + } else { + new_ssa + } + } +} + +impl<'function> PerFunctionContext<'function> { + /// Create a new PerFunctionContext from the source function. + /// The value and block mappings for this context are initially empty except + /// for containing the mapping between parameters in the source_function and + /// the arguments of the destination function. + fn new(context: &'function mut InlineContext, source_function: &'function Function) -> Self { + // Create the block to return to but don't insert its parameters until we + // have the types of the actual return values later. + Self { + return_destination: context.builder.insert_block(), + context, + source_function, + blocks: HashMap::new(), + instructions: HashMap::new(), + values: HashMap::new(), + inlining_main: false, + } + } + + /// Translates a ValueId from the function being inlined to a ValueId of the function + /// being inlined into. Note that this expects value ids for all Value::Instruction and + /// Value::Param values are already handled as a result of previous inlining of instructions + /// and blocks respectively. If these assertions trigger it means a value is being used before + /// the instruction or block that defines the value is inserted. + fn translate_value(&mut self, id: ValueId) -> ValueId { + if let Some(value) = self.values.get(&id) { + return *value; + } + + let new_value = match &self.source_function.dfg[id] { + value @ Value::Instruction { .. } => { + unreachable!("All Value::Instructions should already be known during inlining after creating the original inlined instruction. Unknown value {id} = {value:?}") + } + value @ Value::Param { .. } => { + unreachable!("All Value::Params should already be known from previous calls to translate_block. Unknown value {id} = {value:?}") + } + Value::NumericConstant { constant, typ } => { + let value = self.source_function.dfg[*constant].value(); + self.context.builder.numeric_constant(value, *typ) + } + Value::Function(function) => self.context.builder.import_function(*function), + Value::Intrinsic(intrinsic) => self.context.builder.import_intrinsic_id(*intrinsic), + }; + + self.values.insert(id, new_value); + new_value + } + + /// Translate a block id from the source function to one of the target function. + /// + /// If the block isn't already known, this will insert a new block into the target function + /// with the same parameter types as the source block. + fn translate_block( + &mut self, + source_block: BasicBlockId, + block_queue: &mut Vec, + ) -> BasicBlockId { + if let Some(block) = self.blocks.get(&source_block) { + return *block; + } + + // The block is not yet inlined, queue it + block_queue.push(source_block); + + // The block is not already present in the function being inlined into so we must create it. + // The block's instructions are not copied over as they will be copied later in inlining. + let new_block = self.context.builder.insert_block(); + let original_parameters = self.source_function.dfg.block_parameters(source_block); + + for parameter in original_parameters { + let typ = self.source_function.dfg.type_of_value(*parameter); + let new_parameter = self.context.builder.add_block_parameter(new_block, typ); + self.values.insert(*parameter, new_parameter); + } + + self.blocks.insert(source_block, new_block); + new_block + } + + /// Try to retrieve the function referred to by the given Id. + /// Expects that the given ValueId belongs to the source_function. + /// + /// Returns None if the id is not known to refer to a function. + fn get_function(&mut self, mut id: ValueId) -> Option { + id = self.translate_value(id); + match self.context.builder[id] { + Value::Function(id) => Some(id), + Value::Intrinsic(_) => None, + _ => { + self.context.failed_to_inline_a_call = true; + None + } + } + } + + /// Inline all reachable blocks within the source_function into the destination function. + fn inline_blocks(&mut self, ssa: &Ssa) { + let mut seen_blocks = HashSet::new(); + let mut block_queue = vec![self.source_function.entry_block()]; + + while let Some(source_block_id) = block_queue.pop() { + let translated_block_id = self.translate_block(source_block_id, &mut block_queue); + self.context.builder.switch_to_block(translated_block_id); + + seen_blocks.insert(source_block_id); + self.inline_block(ssa, source_block_id); + self.handle_terminator_instruction(source_block_id, &mut block_queue); + } + + self.context.builder.switch_to_block(self.return_destination); + } + + /// Inline each instruction in the given block into the function being inlined into. + /// This may recurse if it finds another function to inline if a call instruction is within this block. + fn inline_block(&mut self, ssa: &Ssa, block_id: BasicBlockId) { + let block = &self.source_function.dfg[block_id]; + for id in block.instructions() { + match &self.source_function.dfg[*id] { + Instruction::Call { func, arguments } => match self.get_function(*func) { + Some(function) => self.inline_function(ssa, *id, function, arguments), + None => self.push_instruction(*id), + }, + _ => self.push_instruction(*id), + } + } + } + + /// Inline a function call and remember the inlined return values in the values map + fn inline_function( + &mut self, + ssa: &Ssa, + call_id: InstructionId, + function: FunctionId, + arguments: &[ValueId], + ) { + let old_results = self.source_function.dfg.instruction_results(call_id); + let new_results = self.context.inline_function(ssa, function, arguments); + Self::insert_new_instruction_results(&mut self.values, old_results, new_results); + } + + /// Push the given instruction from the source_function into the current block of the + /// function being inlined into. + fn push_instruction(&mut self, id: InstructionId) { + let instruction = self.source_function.dfg[id].map_values(|id| self.translate_value(id)); + let results = self.source_function.dfg.instruction_results(id); + + let ctrl_typevars = instruction + .requires_ctrl_typevars() + .then(|| vecmap(results, |result| self.source_function.dfg.type_of_value(*result))); + + let new_results = self.context.builder.insert_instruction(instruction, ctrl_typevars); + Self::insert_new_instruction_results(&mut self.values, results, new_results); + } + + /// Modify the values HashMap to remember the mapping between an instruction result's previous + /// ValueId (from the source_function) and its new ValueId in the destination function. + fn insert_new_instruction_results( + values: &mut HashMap, + old_results: &[ValueId], + new_results: &[ValueId], + ) { + assert_eq!(old_results.len(), new_results.len()); + for (old_result, new_result) in old_results.iter().zip(new_results) { + values.insert(*old_result, *new_result); + } + } + + /// Handle the given terminator instruction from the given source function block. + /// This will push any new blocks to the destination function as needed, add them + /// to the block queue, and set the terminator instruction for the current block. + fn handle_terminator_instruction( + &mut self, + block_id: BasicBlockId, + block_queue: &mut Vec, + ) { + match self.source_function.dfg[block_id].terminator() { + Some(TerminatorInstruction::Jmp { destination, arguments }) => { + let destination = self.translate_block(*destination, block_queue); + let arguments = vecmap(arguments, |arg| self.translate_value(*arg)); + self.context.builder.terminate_with_jmp(destination, arguments); + } + Some(TerminatorInstruction::JmpIf { + condition, + then_destination, + else_destination, + }) => { + let condition = self.translate_value(*condition); + let then_block = self.translate_block(*then_destination, block_queue); + let else_block = self.translate_block(*else_destination, block_queue); + self.context.builder.terminate_with_jmpif(condition, then_block, else_block); + } + Some(TerminatorInstruction::Return { return_values }) => { + let return_values = vecmap(return_values, |value| self.translate_value(*value)); + + if self.inlining_main { + self.context.builder.terminate_with_return(return_values); + } else { + for value in &return_values { + // Add the block parameters for the return block here since we don't do + // it when inserting the block in PerFunctionContext::new + let typ = self.context.builder.current_function.dfg.type_of_value(*value); + self.context.builder.add_block_parameter(self.return_destination, typ); + } + self.context.builder.terminate_with_jmp(self.return_destination, return_values); + } + } + None => unreachable!("Block has no terminator instruction"), + } + } +} + +#[cfg(test)] +mod test { + use crate::ssa_refactor::{ + ir::{map::Id, types::Type}, + ssa_builder::FunctionBuilder, + }; + + #[test] + fn basic_inlining() { + // fn foo { + // b0(): + // v0 = call bar() + // return v0 + // } + // fn bar { + // b0(): + // return 72 + // } + let foo_id = Id::test_new(0); + let mut builder = FunctionBuilder::new("foo".into(), foo_id); + + let bar_id = Id::test_new(1); + let bar = builder.import_function(bar_id); + let results = builder.insert_call(bar, Vec::new(), vec![Type::field()]).to_vec(); + builder.terminate_with_return(results); + + builder.new_function("bar".into(), bar_id); + let expected_return = 72u128; + let seventy_two = builder.field_constant(expected_return); + builder.terminate_with_return(vec![seventy_two]); + + let ssa = builder.finish(); + assert_eq!(ssa.functions.len(), 2); + + let inlined = ssa.inline_functions(); + assert_eq!(inlined.functions.len(), 1); + } +} diff --git a/crates/noirc_evaluator/src/ssa_refactor/opt/mod.rs b/crates/noirc_evaluator/src/ssa_refactor/opt/mod.rs new file mode 100644 index 00000000000..46ca7d443bc --- /dev/null +++ b/crates/noirc_evaluator/src/ssa_refactor/opt/mod.rs @@ -0,0 +1,6 @@ +//! This folder contains each optimization pass for the SSA IR. +//! +//! Each pass is generally expected to mutate the SSA IR into a gradually +//! simpler form until the IR only has a single function remaining with 1 block within it. +//! Generally, these passes are also expected to minimize the final amount of instructions. +mod inlining; diff --git a/crates/noirc_evaluator/src/ssa_refactor/ssa_builder/mod.rs b/crates/noirc_evaluator/src/ssa_refactor/ssa_builder/mod.rs index aa67cbed583..f621503e59a 100644 --- a/crates/noirc_evaluator/src/ssa_refactor/ssa_builder/mod.rs +++ b/crates/noirc_evaluator/src/ssa_refactor/ssa_builder/mod.rs @@ -9,7 +9,10 @@ use crate::ssa_refactor::ir::{ }; use super::{ - ir::instruction::{InstructionId, Intrinsic}, + ir::{ + basic_block::BasicBlock, + instruction::{InstructionId, Intrinsic}, + }, ssa_gen::Ssa, }; @@ -95,8 +98,13 @@ impl FunctionBuilder { self.current_function.dfg.add_block_parameter(block, typ) } + /// Returns the parameters of the given block in the current function. + pub(crate) fn block_parameters(&self, block: BasicBlockId) -> &[ValueId] { + self.current_function.dfg.block_parameters(block) + } + /// Inserts a new instruction at the end of the current block and returns its results - fn insert_instruction( + pub(crate) fn insert_instruction( &mut self, instruction: Instruction, ctrl_typevars: Option>, @@ -113,6 +121,11 @@ impl FunctionBuilder { self.current_block = block; } + /// Returns the block currently being inserted into + pub(crate) fn current_block(&mut self) -> BasicBlockId { + self.current_block + } + /// Insert an allocate instruction at the end of the current block, allocating the /// given amount of field elements. Returns the result of the allocate instruction, /// which is always a Reference to the allocated data. @@ -228,8 +241,12 @@ impl FunctionBuilder { /// Retrieve a value reference to the given intrinsic operation. /// Returns None if there is no intrinsic matching the given name. pub(crate) fn import_intrinsic(&mut self, name: &str) -> Option { - Intrinsic::lookup(name) - .map(|intrinsic| self.current_function.dfg.import_intrinsic(intrinsic)) + Intrinsic::lookup(name).map(|intrinsic| self.import_intrinsic_id(intrinsic)) + } + + /// Retrieve a value reference to the given intrinsic operation. + pub(crate) fn import_intrinsic_id(&mut self, intrinsic: Intrinsic) -> ValueId { + self.current_function.dfg.import_intrinsic(intrinsic) } /// Removes the given instruction from the current block or panics otherwise. @@ -253,3 +270,11 @@ impl std::ops::Index for FunctionBuilder { &self.current_function.dfg[id] } } + +impl std::ops::Index for FunctionBuilder { + type Output = BasicBlock; + + fn index(&self, id: BasicBlockId) -> &Self::Output { + &self.current_function.dfg[id] + } +} diff --git a/crates/noirc_evaluator/src/ssa_refactor/ssa_gen/mod.rs b/crates/noirc_evaluator/src/ssa_refactor/ssa_gen/mod.rs index d6c5731e147..93a53ec70ec 100644 --- a/crates/noirc_evaluator/src/ssa_refactor/ssa_gen/mod.rs +++ b/crates/noirc_evaluator/src/ssa_refactor/ssa_gen/mod.rs @@ -2,7 +2,7 @@ mod context; mod program; mod value; -pub use program::Ssa; +pub(crate) use program::Ssa; use context::SharedContext; use iter_extended::vecmap; @@ -19,7 +19,7 @@ use super::ir::{instruction::BinaryOp, types::Type, value::ValueId}; /// Generates SSA for the given monomorphized program. /// /// This function will generate the SSA but does not perform any optimizations on it. -pub fn generate_ssa(program: Program) -> Ssa { +pub(crate) fn generate_ssa(program: Program) -> Ssa { let context = SharedContext::new(program); let main_id = Program::main_id(); diff --git a/crates/noirc_evaluator/src/ssa_refactor/ssa_gen/program.rs b/crates/noirc_evaluator/src/ssa_refactor/ssa_gen/program.rs index de4f01fc613..7f4b9a8dd25 100644 --- a/crates/noirc_evaluator/src/ssa_refactor/ssa_gen/program.rs +++ b/crates/noirc_evaluator/src/ssa_refactor/ssa_gen/program.rs @@ -1,22 +1,43 @@ -use std::fmt::Display; +use std::{collections::BTreeMap, fmt::Display}; -use crate::ssa_refactor::ir::function::Function; +use iter_extended::btree_map; + +use crate::ssa_refactor::ir::{ + function::{Function, FunctionId}, + map::AtomicCounter, +}; /// Contains the entire SSA representation of the program. -pub struct Ssa { - pub functions: Vec, +pub(crate) struct Ssa { + pub(crate) functions: BTreeMap, + pub(crate) main_id: FunctionId, + pub(crate) next_id: AtomicCounter, } impl Ssa { - /// Create a new Ssa object from the given SSA functions - pub fn new(functions: Vec) -> Self { - Self { functions } + /// Create a new Ssa object from the given SSA functions. + /// The first function in this vector is expected to be the main function. + pub(crate) fn new(functions: Vec) -> Self { + let main_id = functions.first().expect("Expected at least 1 SSA function").id(); + let mut max_id = main_id; + + let functions = btree_map(functions, |f| { + max_id = std::cmp::max(max_id, f.id()); + (f.id(), f) + }); + + Self { functions, main_id, next_id: AtomicCounter::starting_after(max_id) } + } + + /// Returns the entry-point function of the program + pub(crate) fn main(&self) -> &Function { + &self.functions[&self.main_id] } } impl Display for Ssa { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - for function in &self.functions { + for function in self.functions.values() { writeln!(f, "{function}")?; } Ok(())