diff --git a/crates/noirc_evaluator/src/frontend.rs b/crates/noirc_evaluator/src/frontend.rs new file mode 100644 index 00000000000..410f9f1a9b0 --- /dev/null +++ b/crates/noirc_evaluator/src/frontend.rs @@ -0,0 +1 @@ +pub mod variable; diff --git a/crates/noirc_evaluator/src/frontend/variable.rs b/crates/noirc_evaluator/src/frontend/variable.rs new file mode 100644 index 00000000000..449581cf93c --- /dev/null +++ b/crates/noirc_evaluator/src/frontend/variable.rs @@ -0,0 +1,23 @@ +/// A variable in the SSA IR. +/// By definition, a variable can only be defined once. +/// +/// As in Cranelift, we also allow variable use before definition. +/// This will produce side-effects which will need to be handled +/// before sealing a block. +pub struct Variable(u32); + +impl From for Variable { + fn from(value: u32) -> Self { + Variable(value) + } +} +impl From for Variable { + fn from(value: u16) -> Self { + Variable(value as u32) + } +} +impl From for Variable { + fn from(value: u8) -> Self { + Variable(value as u32) + } +} diff --git a/crates/noirc_evaluator/src/lib.rs b/crates/noirc_evaluator/src/lib.rs index 722ecc12397..166c2d58239 100644 --- a/crates/noirc_evaluator/src/lib.rs +++ b/crates/noirc_evaluator/src/lib.rs @@ -6,6 +6,13 @@ mod errors; mod ssa; +// SSA code to create the SSA based IR +// for functions and execute different optimizations. +pub mod ssa_refactor; +// Frontend helper module to translate a different AST +// into the SSA IR. +pub mod frontend; + use acvm::{ acir::circuit::{opcodes::Opcode as AcirOpcode, Circuit, PublicInputs}, acir::native_types::{Expression, Witness}, diff --git a/crates/noirc_evaluator/src/ssa_refactor.rs b/crates/noirc_evaluator/src/ssa_refactor.rs new file mode 100644 index 00000000000..073b54cbf10 --- /dev/null +++ b/crates/noirc_evaluator/src/ssa_refactor.rs @@ -0,0 +1,13 @@ +//! SSA stands for Single Static Assignment +//! The IR presented in this module will already +//! be in SSA form and will be used to apply +//! conventional optimizations like Common Subexpression +//! elimination and constant folding. +//! +//! This module heavily borrows from Cranelift +#[allow(dead_code)] +mod basic_block; +#[allow(dead_code)] +mod dfg; +#[allow(dead_code)] +mod ir; diff --git a/crates/noirc_evaluator/src/ssa_refactor/basic_block.rs b/crates/noirc_evaluator/src/ssa_refactor/basic_block.rs new file mode 100644 index 00000000000..d6c2198b4a0 --- /dev/null +++ b/crates/noirc_evaluator/src/ssa_refactor/basic_block.rs @@ -0,0 +1,37 @@ +use super::ir::instruction::{Instruction, TerminatorInstruction}; + +/// A Basic block is a maximal collection of instructions +/// such that there are only jumps at the end of block +/// and one can only enter the block from the beginning. +/// +/// This means that if one instruction is executed in a basic +/// block, then all instructions are executed. ie single-entry single-exit. +#[derive(Debug, PartialEq, Eq, Hash, Clone)] +pub(crate) struct BasicBlock { + /// Arguments to the basic block. + phi_nodes: Vec, + /// Instructions in the basic block. + instructions: Vec, + + /// A basic block is considered sealed + /// if no further predecessors will be added to it. + /// Since only filled blocks can have successors, + /// predecessors are always filled. + is_sealed: bool, + + /// The terminating instruction for the basic block. + /// + /// This will be a control flow instruction. + terminator: TerminatorInstruction, +} + +#[derive(Debug, PartialEq, Eq, Hash, Clone)] +/// An identifier for a Basic Block. +pub(crate) struct BasicBlockId; + +#[derive(Debug, PartialEq, Eq, Hash, Clone)] +/// Arguments to the basic block. +/// We use the modern Crane-lift strategy +/// of representing phi nodes as basic block +/// arguments. +pub(crate) struct BlockArguments; diff --git a/crates/noirc_evaluator/src/ssa_refactor/dfg.rs b/crates/noirc_evaluator/src/ssa_refactor/dfg.rs new file mode 100644 index 00000000000..a0830b5ecc3 --- /dev/null +++ b/crates/noirc_evaluator/src/ssa_refactor/dfg.rs @@ -0,0 +1,186 @@ +use super::{ + basic_block::{BasicBlock, BasicBlockId}, + ir::{ + extfunc::{SigRef, Signature}, + instruction::{Instruction, InstructionId, Instructions}, + types::Typ, + value::{Value, ValueId}, + }, +}; +use std::collections::HashMap; + +#[derive(Debug, Default)] +/// A convenience wrapper to store `Value`s. +pub(crate) struct ValueList(Vec); + +impl ValueList { + /// Inserts an element to the back of the list and + /// returns the `position` + pub(crate) fn push(&mut self, value: ValueId) -> usize { + self.0.push(value); + self.len() - 1 + } + /// Returns the number of values in the list. + fn len(&self) -> usize { + self.0.len() + } + + /// Removes all items from the list. + fn clear(&mut self) { + self.0.clear(); + } + /// Returns the ValueId's as a slice. + pub(crate) fn as_slice(&self) -> &[ValueId] { + &self.0 + } +} +#[derive(Debug, Default)] +pub(crate) struct DataFlowGraph { + /// All of the instructions in a function + instructions: Instructions, + + /// Stores the results for a particular instruction. + /// + /// An instruction may return multiple values + /// and for this, we will also use the cranelift strategy + /// to fetch them via indices. + /// + /// Currently, we need to define them in a better way + /// Call instructions require the func signature, but + /// other instructions may need some more reading on my part + results: HashMap, + + /// Storage for all of the values defined in this + /// function. + values: HashMap, + + /// Function signatures of external methods + signatures: HashMap, + + /// All blocks in a function + blocks: HashMap, +} + +impl DataFlowGraph { + /// Creates a new `empty` basic block + pub(crate) fn new_block(&mut self) -> BasicBlockId { + todo!() + } + + /// Inserts a new instruction into the DFG. + pub(crate) fn make_instruction(&mut self, instruction_data: Instruction) -> InstructionId { + let id = self.instructions.add_instruction(instruction_data); + + // Create a new vector to store the potential results + // for the instruction. + self.results.insert(id, Default::default()); + + id + } + + /// Attaches results to the instruction. + /// + /// Returns the number of results that this instruction + /// produces. + pub(crate) fn make_instruction_results( + &mut self, + instruction_id: InstructionId, + ctrl_typevar: Typ, + ) -> usize { + // Clear all of the results instructions associated with this + // instruction. + self.results.get_mut(&instruction_id).expect("all instructions should have a `result` allocation when instruction was added to the DFG").clear(); + + // Get all of the types that this instruction produces + // and append them as results. + let typs = self.instruction_result_types(instruction_id, ctrl_typevar); + let num_typs = typs.len(); + + for typ in typs { + self.append_result(instruction_id, typ); + } + + num_typs + } + + /// Return the result types of this instruction. + /// + /// For example, an addition instruction will return + /// one type which is the type of the operands involved. + /// This is the `ctrl_typevar` in this case. + fn instruction_result_types( + &self, + instruction_id: InstructionId, + ctrl_typevar: Typ, + ) -> Vec { + // Check if it is a call instruction. If so, we don't support that yet + let ins_data = self.instructions.get_instruction(instruction_id); + match ins_data { + Instruction::Call { .. } => todo!("function calls are not supported yet"), + ins => ins.return_types(ctrl_typevar), + } + } + + /// Appends a result type to the instruction. + pub(crate) fn append_result(&mut self, instruction_id: InstructionId, typ: Typ) -> ValueId { + let next_value_id = self.next_value(); + + // Add value to the list of results for this instruction + let res_position = self.results.get_mut(&instruction_id).unwrap().push(next_value_id); + + self.make_value(Value::Instruction { + typ, + position: res_position as u16, + instruction: instruction_id, + }) + } + + /// Stores a value and returns its `ValueId` reference. + fn make_value(&mut self, data: Value) -> ValueId { + let next_value = self.next_value(); + + self.values.insert(next_value, data); + + next_value + } + + /// Returns the next `ValueId` + fn next_value(&self) -> ValueId { + ValueId(self.values.len() as u32) + } + + /// Returns the number of instructions + /// inserted into functions. + pub(crate) fn num_instructions(&self) -> usize { + self.instructions.num_instructions() + } + + /// Returns all of result values which are attached to this instruction. + pub(crate) fn instruction_results(&self, instruction_id: InstructionId) -> &[ValueId] { + self.results.get(&instruction_id).expect("expected a list of Values").as_slice() + } +} + +#[cfg(test)] +mod tests { + use super::DataFlowGraph; + use crate::ssa_refactor::ir::{ + instruction::Instruction, + types::{NumericType, Typ}, + }; + use acvm::FieldElement; + + #[test] + fn make_instruction() { + let mut dfg = DataFlowGraph::default(); + let ins = Instruction::Immediate { value: FieldElement::from(0u128) }; + let ins_id = dfg.make_instruction(ins); + + let num_results = + dfg.make_instruction_results(ins_id, Typ::Numeric(NumericType::NativeField)); + + let results = dfg.instruction_results(ins_id); + + assert_eq!(results.len(), num_results); + } +} diff --git a/crates/noirc_evaluator/src/ssa_refactor/ir.rs b/crates/noirc_evaluator/src/ssa_refactor/ir.rs new file mode 100644 index 00000000000..aa07393203d --- /dev/null +++ b/crates/noirc_evaluator/src/ssa_refactor/ir.rs @@ -0,0 +1,5 @@ +pub(crate) mod extfunc; +mod function; +pub(crate) mod instruction; +pub(crate) mod types; +pub(crate) mod value; diff --git a/crates/noirc_evaluator/src/ssa_refactor/ir/extfunc.rs b/crates/noirc_evaluator/src/ssa_refactor/ir/extfunc.rs new file mode 100644 index 00000000000..b0e573822cf --- /dev/null +++ b/crates/noirc_evaluator/src/ssa_refactor/ir/extfunc.rs @@ -0,0 +1,23 @@ +//! Like Crane-lift all functions outside of the current function is seen as +//! external. +//! To reference external functions, one uses + +use super::types::Typ; + +#[derive(Debug, Default, Clone)] +pub(crate) struct Signature { + pub(crate) params: Vec, + pub(crate) returns: Vec, +} +/// Reference to a `Signature` in a map inside of +/// a functions DFG. +#[derive(Debug, Default, Clone, Copy)] +pub(crate) struct SigRef(pub(crate) u32); + +#[test] +fn sign_smoke() { + let mut signature = Signature::default(); + + signature.params.push(Typ::Numeric(super::types::NumericType::NativeField)); + signature.returns.push(Typ::Numeric(super::types::NumericType::Unsigned { bit_size: 32 })); +} diff --git a/crates/noirc_evaluator/src/ssa_refactor/ir/function.rs b/crates/noirc_evaluator/src/ssa_refactor/ir/function.rs new file mode 100644 index 00000000000..331c0d656d6 --- /dev/null +++ b/crates/noirc_evaluator/src/ssa_refactor/ir/function.rs @@ -0,0 +1,25 @@ +use crate::ssa_refactor::basic_block::{BasicBlock, BasicBlockId}; + +use super::instruction::Instruction; + +use noirc_errors::Location; +use std::collections::HashMap; + +/// A function holds a list of instructions. +/// These instructions are further grouped into +/// Basic blocks +#[derive(Debug)] +pub(crate) struct Function { + /// Basic blocks associated to this particular function + basic_blocks: HashMap, + + /// Maps instructions to source locations + source_locations: HashMap, + + /// The first basic block in the function + entry_block: BasicBlockId, +} + +/// FunctionId is a reference for a function +#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] +pub(crate) struct FunctionId(pub(crate) u32); diff --git a/crates/noirc_evaluator/src/ssa_refactor/ir/instruction.rs b/crates/noirc_evaluator/src/ssa_refactor/ir/instruction.rs new file mode 100644 index 00000000000..04d933d8f9e --- /dev/null +++ b/crates/noirc_evaluator/src/ssa_refactor/ir/instruction.rs @@ -0,0 +1,249 @@ +use std::collections::HashMap; + +use acvm::FieldElement; + +use super::{function::FunctionId, types::Typ, value::ValueId}; +use crate::ssa_refactor::basic_block::{BasicBlockId, BlockArguments}; + +/// Map of instructions. +/// This is similar to Arena. +#[derive(Debug, Default)] +pub(crate) struct Instructions(HashMap); + +impl Instructions { + /// Adds an instruction to the map and returns a + /// reference to the instruction. + pub(crate) fn add_instruction(&mut self, ins: Instruction) -> InstructionId { + let id = InstructionId(self.0.len() as u32); + self.0.insert(id, ins); + id + } + + /// Fetch the instruction corresponding to this + /// instruction id. + /// + /// Panics if there is no such instruction, since instructions cannot be + /// deleted. + pub(crate) fn get_instruction(&self, ins_id: InstructionId) -> &Instruction { + self.0.get(&ins_id).expect("ICE: instructions cannot be deleted") + } + + /// Returns the number of instructions stored in the map. + pub(crate) fn num_instructions(&self) -> usize { + self.0.len() + } +} + +#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] +/// Reference to an instruction +pub(crate) struct InstructionId(u32); + +#[derive(Debug, PartialEq, Eq, Hash, Clone)] +/// These are similar to built-ins in other languages. +/// These can be classified under two categories: +/// - Opcodes which the IR knows the target machine has +/// special support for. (LowLevel) +/// - Opcodes which have no function definition in the +/// source code and must be processed by the IR. An example +/// of this is println. +pub(crate) struct IntrinsicOpcodes; + +#[derive(Debug, PartialEq, Eq, Hash, Clone)] +/// Instructions are used to perform tasks. +/// The instructions that the IR is able to specify are listed below. +pub(crate) enum Instruction { + // Binary Operations + Binary(Binary), + + // Unary Operations + // + /// Converts `Value` into Typ + Cast(ValueId, Typ), + + /// Computes a bit wise not + Not(ValueId), + + /// Truncates `value` to `bit_size` + Truncate { + value: ValueId, + bit_size: u32, + max_bit_size: u32, + }, + + /// Constrains a value to be equal to true + Constrain(ValueId), + + /// Performs a function call with a list of its arguments. + Call { + func: FunctionId, + arguments: Vec, + }, + /// Performs a call to an intrinsic function and stores the + /// results in `return_arguments`. + Intrinsic { + func: IntrinsicOpcodes, + arguments: Vec, + }, + + /// Loads a value from memory. + Load(ValueId), + + /// Writes a value to memory. + Store { + destination: ValueId, + value: ValueId, + }, + + /// Stores an Immediate value + Immediate { + value: FieldElement, + }, +} + +impl Instruction { + /// Returns the number of results that this instruction + /// produces. + pub(crate) fn num_fixed_results(&self) -> usize { + match self { + Instruction::Binary(_) => 1, + Instruction::Cast(_, _) => 0, + Instruction::Not(_) => 1, + Instruction::Truncate { .. } => 1, + Instruction::Constrain(_) => 0, + // This returns 0 as the result depends on the function being called + Instruction::Call { .. } => 0, + // This also returns 0, but we could get it a compile time, + // since we know the signatures for the intrinsics + Instruction::Intrinsic { .. } => 0, + Instruction::Load(_) => 1, + Instruction::Store { .. } => 0, + Instruction::Immediate { .. } => 1, + } + } + + /// Returns the number of arguments required for a call + pub(crate) fn num_fixed_arguments(&self) -> usize { + match self { + Instruction::Binary(_) => 2, + Instruction::Cast(_, _) => 1, + Instruction::Not(_) => 1, + Instruction::Truncate { .. } => 1, + Instruction::Constrain(_) => 1, + // This returns 0 as the arguments depend on the function being called + Instruction::Call { .. } => 0, + // This also returns 0, but we could get it a compile time, + // since we know the function definition for the intrinsics + Instruction::Intrinsic { .. } => 0, + Instruction::Load(_) => 1, + Instruction::Store { .. } => 2, + Instruction::Immediate { .. } => 0, + } + } + + /// Returns the types that this instruction will return. + pub(crate) fn return_types(&self, ctrl_typevar: Typ) -> Vec { + match self { + Instruction::Binary(_) => vec![ctrl_typevar], + Instruction::Cast(_, typ) => vec![*typ], + Instruction::Not(_) => vec![ctrl_typevar], + Instruction::Truncate { .. } => vec![ctrl_typevar], + Instruction::Constrain(_) => vec![], + Instruction::Call { .. } => vec![], + Instruction::Intrinsic { .. } => vec![], + Instruction::Load(_) => vec![ctrl_typevar], + Instruction::Store { .. } => vec![], + Instruction::Immediate { .. } => vec![], + } + } +} + +/// These are operations which can exit a basic block +/// ie control flow type operations +/// +/// Since our IR needs to be in SSA form, it makes sense +/// to split up instructions like this, as we are sure that these instructions +/// will not be in the list of instructions for a basic block. +#[derive(Debug, PartialEq, Eq, Hash, Clone)] +pub(crate) enum TerminatorInstruction { + /// Control flow + /// + /// Jump If + /// + /// Jumps to the specified `destination` with + /// arguments, if the condition + /// if the condition is true. + JmpIf { condition: ValueId, destination: BasicBlockId, arguments: BlockArguments }, + /// Unconditional Jump + /// + /// Jumps to specified `destination` with `arguments` + Jmp { destination: BasicBlockId, arguments: BlockArguments }, +} + +/// A binary instruction in the IR. +#[derive(Debug, PartialEq, Eq, Hash, Clone)] +pub(crate) struct Binary { + /// Left hand side of the binary operation + pub(crate) lhs: ValueId, + /// Right hand side of the binary operation + pub(crate) rhs: ValueId, + /// The binary operation to apply + pub(crate) operator: BinaryOp, +} + +/// Binary Operations allowed in the IR. +#[derive(Debug, PartialEq, Eq, Hash, Clone)] +pub(crate) enum BinaryOp { + /// Addition of two types. + /// The result will have the same type as + /// the operands. + Add, + /// Subtraction of two types. + /// The result will have the same type as + /// the operands. + Sub, + /// Multiplication of two types. + /// The result will have the same type as + /// the operands. + Mul, + /// Division of two types. + /// The result will have the same type as + /// the operands. + Div, + /// Checks whether two types are equal. + /// Returns true if the types were equal and + /// false otherwise. + Eq, + /// Checks whether two types are equal. + /// Returns true if the types were not equal and + /// false otherwise. + Ne, +} + +#[test] +fn smoke_instructions_map_duplicate() { + let ins = Instruction::Cast(ValueId(0), Typ::Unit); + let same_ins = Instruction::Cast(ValueId(0), Typ::Unit); + + let mut ins_map = Instructions::default(); + + // Document what happens when we insert the same instruction twice + let id = ins_map.add_instruction(ins); + let id_same_ins = ins_map.add_instruction(same_ins); + + // The map is quite naive and does not check if the instruction has ben inserted + // before. We simply assign a different Id. + assert_ne!(id, id_same_ins) +} + +#[test] +fn num_instructions_smoke() { + let n = 100; + + let mut ins_map = Instructions::default(); + for i in 0..n { + let ins = Instruction::Cast(ValueId(i as u32), Typ::Unit); + ins_map.add_instruction(ins); + } + + assert_eq!(n, ins_map.num_instructions()) +} diff --git a/crates/noirc_evaluator/src/ssa_refactor/ir/types.rs b/crates/noirc_evaluator/src/ssa_refactor/ir/types.rs new file mode 100644 index 00000000000..9cf75e5ae7f --- /dev/null +++ b/crates/noirc_evaluator/src/ssa_refactor/ir/types.rs @@ -0,0 +1,24 @@ +/// A numeric type in the Intermediate representation +/// Note: we class NativeField as a numeric type +/// though we also apply limitations to it, such as not +/// being able to compare two native fields, whereas this is +/// something that you can do with a signed/unsigned integer. +/// +/// Fields do not have a notion of ordering, so this distinction +/// is reasonable. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub(crate) enum NumericType { + Signed { bit_size: u32 }, + Unsigned { bit_size: u32 }, + NativeField, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +/// All types representable in the IR. +pub(crate) enum Typ { + /// Represents numeric types in the IR + /// including field elements + Numeric(NumericType), + /// Represents the absence of a Type. + Unit, +} diff --git a/crates/noirc_evaluator/src/ssa_refactor/ir/value.rs b/crates/noirc_evaluator/src/ssa_refactor/ir/value.rs new file mode 100644 index 00000000000..c245cf95f24 --- /dev/null +++ b/crates/noirc_evaluator/src/ssa_refactor/ir/value.rs @@ -0,0 +1,20 @@ +use super::{instruction::InstructionId, types::Typ}; + +#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] +/// Value is the most basic type allowed in the IR. +/// Transition Note: This is similar to `NodeId` in our previous IR. +pub(crate) struct ValueId(pub(crate) u32); + +#[derive(Debug, PartialEq, Eq, Hash, Clone)] +pub(crate) enum Value { + /// This value was created due to an instruction + /// + /// instruction -- This is the instruction which defined it + /// typ -- This is the `Type` of the instruction + /// position -- Returns the position in the results + /// vector that this `Value` is located. + /// Example, if you add two numbers together, then the resulting + /// value would have position `0`, the typ would be the type + /// of the operands, and the instruction would map to an add instruction. + Instruction { typ: Typ, position: u16, instruction: InstructionId }, +}