diff --git a/crates/noirc_evaluator/src/brillig/artifact.rs b/crates/noirc_evaluator/src/brillig/artifact.rs deleted file mode 100644 index 56eaa653460..00000000000 --- a/crates/noirc_evaluator/src/brillig/artifact.rs +++ /dev/null @@ -1,123 +0,0 @@ -use crate::ssa_refactor::ir::basic_block::BasicBlockId; -use acvm::acir::brillig_vm::Opcode as BrilligOpcode; -use std::collections::HashMap; - -/// Pointer to a unresolved Jump instruction in -/// the bytecode. -pub(crate) type JumpLabel = usize; - -/// Pointer to a position in the bytecode where a -/// particular basic block starts. -pub(crate) type BlockLabel = usize; - -#[derive(Default, Debug, Clone)] -/// Artifacts resulting from the compilation of a function into brillig byte code. -/// Currently it is just the brillig bytecode of the function. -pub(crate) struct BrilligArtifact { - pub(crate) byte_code: Vec, - /// The set of jumps that need to have their locations - /// resolved. - unresolved_jumps: Vec<(JumpLabel, UnresolvedJumpLocation)>, - /// A map of the basic blocks to their positions - /// in the bytecode. - blocks: HashMap, -} - -/// When constructing the bytecode, there may be instructions -/// which require one to jump to a specific `Block` -/// or a position relative to the current instruction. -/// -/// The position of a `Block` cannot always be known -/// at this point in time, so Jumps are unresolved -/// until all blocks have been processed in the `Block` -/// variant of this enum. -/// -/// Sometimes the relative position of an Jump -/// may be known, from the Jump label, but since -/// the absolute position of a Jump label is not known until -/// after we have linked the bytecode to other functions. -/// We add relative jumps into the `Relative` variant of this enum. -#[derive(Debug, Clone, Copy)] -pub(crate) enum UnresolvedJumpLocation { - Block(BasicBlockId), - Relative(i32), -} - -impl BrilligArtifact { - /// Link some compiled brillig bytecode with its referenced artifacts. - pub(crate) fn link(&mut self, obj: &BrilligArtifact) -> Vec { - self.link_with(obj); - self.resolve_jumps(); - self.byte_code.clone() - } - - /// Link with a brillig artifact - fn link_with(&mut self, obj: &BrilligArtifact) { - let offset = self.code_len(); - for (jump_label, block_id) in &obj.unresolved_jumps { - self.unresolved_jumps.push((jump_label + offset, *block_id)); - } - - for (block_id, block_label) in &obj.blocks { - self.blocks.insert(*block_id, block_label + offset); - } - - self.byte_code.extend_from_slice(&obj.byte_code); - } - - /// Adds a unresolved jump to be fixed at the end of bytecode processing. - pub(crate) fn add_unresolved_jump(&mut self, destination: UnresolvedJumpLocation) { - self.unresolved_jumps.push((self.code_len(), destination)); - } - - /// Adds a label in the bytecode to specify where this block's - /// opcodes will start. - pub(crate) fn add_block_label(&mut self, block: BasicBlockId) { - self.blocks.insert(block, self.code_len()); - } - - /// Number of the opcodes currently in the bytecode - pub(crate) fn code_len(&self) -> usize { - self.byte_code.len() - } - - /// Resolves all of the unresolved jumps in the program. - /// - /// Note: This should only be called once all blocks are processed. - fn resolve_jumps(&mut self) { - for (jump_label, unresolved_location) in &self.unresolved_jumps { - let jump_instruction = self.byte_code[*jump_label].clone(); - - let actual_block_location = match unresolved_location { - UnresolvedJumpLocation::Block(b) => self.blocks[b], - UnresolvedJumpLocation::Relative(location) => { - (location + *jump_label as i32) as usize - } - }; - - match jump_instruction { - BrilligOpcode::Jump { location } => { - assert_eq!(location, 0, "location is not zero, which means that the jump label does not need resolving"); - - self.byte_code[*jump_label] = - BrilligOpcode::Jump { location: actual_block_location }; - } - BrilligOpcode::JumpIfNot { condition, location } => { - assert_eq!(location, 0, "location is not zero, which means that the jump label does not need resolving"); - - self.byte_code[*jump_label] = - BrilligOpcode::JumpIfNot { condition, location: actual_block_location }; - } - BrilligOpcode::JumpIf { condition, location } => { - assert_eq!(location, 0, "location is not zero, which means that the jump label does not need resolving"); - - self.byte_code[*jump_label] = - BrilligOpcode::JumpIf { condition, location: actual_block_location }; - } - _ => unreachable!( - "all jump labels should point to a jump instruction in the bytecode" - ), - } - } - } -} diff --git a/crates/noirc_evaluator/src/brillig/binary.rs b/crates/noirc_evaluator/src/brillig/binary.rs deleted file mode 100644 index 56f75b70f11..00000000000 --- a/crates/noirc_evaluator/src/brillig/binary.rs +++ /dev/null @@ -1,106 +0,0 @@ -use crate::ssa_refactor::ir::{ - instruction::BinaryOp, - types::{NumericType, Type}, -}; -use acvm::acir::brillig_vm::{BinaryFieldOp, BinaryIntOp}; - -/// Type to encapsulate the binary operation types in Brillig -pub(crate) enum BrilligBinaryOp { - Field { op: BinaryFieldOp }, - Integer { op: BinaryIntOp, bit_size: u32 }, -} - -impl BrilligBinaryOp { - /// Convert an SSA binary operation into: - /// - Brillig Binary Integer Op, if it is a integer type - /// - Brillig Binary Field Op, if it is a field type - pub(crate) fn convert_ssa_binary_op_to_brillig_binary_op( - ssa_op: BinaryOp, - typ: Type, - ) -> BrilligBinaryOp { - // First get the bit size and whether its a signed integer, if it is a numeric type - // if it is not,then we return None, indicating that - // it is a Field. - let bit_size_signedness = match typ { - Type::Numeric(numeric_type) => match numeric_type { - NumericType::Signed { bit_size } => Some((bit_size, true)), - NumericType::Unsigned { bit_size } => Some((bit_size, false)), - NumericType::NativeField => None, - }, - _ => unreachable!("only numeric types are allowed in binary operations. References are handled separately"), - }; - - fn binary_op_to_field_op(op: BinaryOp) -> BinaryFieldOp { - match op { - BinaryOp::Add => BinaryFieldOp::Add, - BinaryOp::Sub => BinaryFieldOp::Sub, - BinaryOp::Mul => BinaryFieldOp::Mul, - BinaryOp::Div => BinaryFieldOp::Div, - BinaryOp::Eq => BinaryFieldOp::Equals, - _ => unreachable!( - "Field type cannot be used with {op}. This should have been caught by the frontend" - ), - } - } - fn binary_op_to_int_op(op: BinaryOp, is_signed: bool) -> BinaryIntOp { - match op { - BinaryOp::Add => BinaryIntOp::Add, - BinaryOp::Sub => BinaryIntOp::Sub, - BinaryOp::Mul => BinaryIntOp::Mul, - BinaryOp::Div => { - if is_signed { - BinaryIntOp::SignedDiv - } else { - BinaryIntOp::UnsignedDiv - } - } - BinaryOp::Mod => unreachable!("Modulo operations are handled separately"), - BinaryOp::Eq => BinaryIntOp::Equals, - BinaryOp::Lt => BinaryIntOp::LessThan, - BinaryOp::And => BinaryIntOp::And, - BinaryOp::Or => BinaryIntOp::Or, - BinaryOp::Xor => BinaryIntOp::Xor, - BinaryOp::Shl => BinaryIntOp::Shl, - BinaryOp::Shr => BinaryIntOp::Shr, - } - } - // If bit size is available then it is a binary integer operation - match bit_size_signedness { - Some((bit_size, is_signed)) => { - let binary_int_op = binary_op_to_int_op(ssa_op, is_signed); - BrilligBinaryOp::Integer { op: binary_int_op, bit_size } - } - None => { - let binary_field_op = binary_op_to_field_op(ssa_op); - BrilligBinaryOp::Field { op: binary_field_op } - } - } - } -} - -/// Returns the type of the operation considering the types of the operands -/// TODO: SSA issues binary operations between fields and integers. -/// This probably should be explicitely casted in SSA to avoid having to coerce at this level. -pub(crate) fn type_of_binary_operation(lhs_type: Type, rhs_type: Type) -> Type { - match (lhs_type, rhs_type) { - // If either side is a Field constant then, we coerce into the type - // of the other operand - (Type::Numeric(NumericType::NativeField), typ) - | (typ, Type::Numeric(NumericType::NativeField)) => typ, - // If both sides are numeric type, then we expect their types to be - // the same. - (Type::Numeric(lhs_type), Type::Numeric(rhs_type)) => { - assert_eq!( - lhs_type, rhs_type, - "lhs and rhs types in a binary operation are always the same" - ); - Type::Numeric(lhs_type) - } - (lhs_type, rhs_type) => { - unreachable!( - "ICE: Binary operation between types {:?} and {:?} is not allowed", - lhs_type, rhs_type - ) - } - } -} diff --git a/crates/noirc_evaluator/src/brillig/brillig_gen.rs b/crates/noirc_evaluator/src/brillig/brillig_gen.rs index f98890862cf..a1f48606abe 100644 --- a/crates/noirc_evaluator/src/brillig/brillig_gen.rs +++ b/crates/noirc_evaluator/src/brillig/brillig_gen.rs @@ -1,8 +1,4 @@ -use super::{ - artifact::{BrilligArtifact, UnresolvedJumpLocation}, - binary::{type_of_binary_operation, BrilligBinaryOp}, - memory::BrilligMemory, -}; +use super::brillig_ir::{artifact::BrilligArtifact, BrilligBinaryOp, BrilligContext}; use crate::ssa_refactor::ir::{ basic_block::{BasicBlock, BasicBlockId}, dfg::DataFlowGraph, @@ -12,34 +8,20 @@ use crate::ssa_refactor::ir::{ types::{NumericType, Type}, value::{Value, ValueId}, }; -use acvm::{ - acir::brillig_vm::{ - BinaryIntOp, Opcode as BrilligOpcode, RegisterIndex, RegisterValueOrArray, - Value as BrilligValue, - }, - FieldElement, -}; +use acvm::acir::brillig_vm::{BinaryFieldOp, BinaryIntOp, RegisterIndex}; use iter_extended::vecmap; use std::collections::HashMap; #[derive(Default)] /// Generate the compilation artifacts for compiling a function into brillig bytecode. pub(crate) struct BrilligGen { - obj: BrilligArtifact, - /// A usize indicating the latest un-used register. - latest_register: usize, + /// Context for creating brillig opcodes + context: BrilligContext, /// Map from SSA values to Register Indices. ssa_value_to_register: HashMap, - /// Tracks memory allocations - memory: BrilligMemory, } impl BrilligGen { - /// Adds a brillig instruction to the brillig byte code - fn push_code(&mut self, code: BrilligOpcode) { - self.obj.byte_code.push(code); - } - /// Gets a `RegisterIndex` for a `ValueId`, if one already exists /// or creates a new `RegisterIndex` using the latest available /// free register. @@ -48,7 +30,7 @@ impl BrilligGen { return *register_index; } - let register = self.create_register(); + let register = self.context.create_register(); // Cache the `ValueId` so that if we call it again, it will // return the register that has just been created. @@ -61,76 +43,58 @@ impl BrilligGen { register } - /// Creates a new register. - fn create_register(&mut self) -> RegisterIndex { - let register = RegisterIndex::from(self.latest_register); - self.latest_register += 1; - register - } - /// Converts an SSA Basic block into a sequence of Brillig opcodes fn convert_block(&mut self, block_id: BasicBlockId, dfg: &DataFlowGraph) { - self.obj.add_block_label(block_id); + // Add a label for this block + self.context.add_label_to_next_opcode(block_id); + + // Convert the block parameters let block = &dfg[block_id]; self.convert_block_params(block, dfg); + // Convert all of the instructions int the block for instruction_id in block.instructions() { self.convert_ssa_instruction(*instruction_id, dfg); } - // Jump to the next block - let jump = block.terminator().expect("block is expected to be constructed"); - match jump { + // Process the block's terminator instruction + let terminator_instruction = + block.terminator().expect("block is expected to be constructed"); + self.convert_ssa_terminator(terminator_instruction, dfg); + } + + /// Converts an SSA terminator instruction into the necessary opcodes. + /// + /// TODO: document why the TerminatorInstruction::Return includes a stop instruction + /// TODO along with the `Self::compile` + fn convert_ssa_terminator( + &mut self, + terminator_instruction: &TerminatorInstruction, + dfg: &DataFlowGraph, + ) { + match terminator_instruction { TerminatorInstruction::JmpIf { condition, then_destination, else_destination } => { let condition = self.convert_ssa_value(*condition, dfg); - self.jump_if(condition, *then_destination); - self.jump(*else_destination); + self.context.jump_if_instruction(condition, then_destination); + self.context.jump_instruction(else_destination); } TerminatorInstruction::Jmp { destination, arguments } => { let target = &dfg[*destination]; for (src, dest) in arguments.iter().zip(target.parameters()) { let destination = self.convert_ssa_value(*dest, dfg); let source = self.convert_ssa_value(*src, dfg); - self.push_code(BrilligOpcode::Mov { destination, source }); + self.context.mov_instruction(destination, source); } - self.jump(*destination); + self.context.jump_instruction(destination); } TerminatorInstruction::Return { return_values } => { - self.convert_ssa_return(return_values, dfg); - } - } - } - - /// Adds a unresolved `Jump` instruction to the bytecode. - fn jump(&mut self, target: BasicBlockId) { - self.obj.add_unresolved_jump(UnresolvedJumpLocation::Block(target)); - self.push_code(BrilligOpcode::Jump { location: 0 }); - } - - /// Adds a unresolved `JumpIf` instruction to the bytecode. - fn jump_if(&mut self, condition: RegisterIndex, target: BasicBlockId) { - self.obj.add_unresolved_jump(UnresolvedJumpLocation::Block(target)); - self.push_code(BrilligOpcode::JumpIf { condition, location: 0 }); - } - - /// Converts the SSA return instruction into the necessary BRillig return - /// opcode. - /// - /// For Brillig, the return is implicit; The caller will take `N` values from - /// the Register starting at register index 0. `N` indicates the number of - /// return values expected. - fn convert_ssa_return(&mut self, return_values: &[ValueId], dfg: &DataFlowGraph) { - for (destination_index, value_id) in return_values.iter().enumerate() { - let return_register = self.convert_ssa_value(*value_id, dfg); - if destination_index > self.latest_register { - self.latest_register = destination_index; + let return_registers: Vec<_> = return_values + .iter() + .map(|value_id| self.convert_ssa_value(*value_id, dfg)) + .collect(); + self.context.return_instruction(&return_registers); } - self.push_code(BrilligOpcode::Mov { - destination: destination_index.into(), - source: return_register, - }); } - self.push_code(BrilligOpcode::Stop); } /// Converts SSA Block parameters into Brillig Registers. @@ -164,55 +128,35 @@ impl BrilligGen { } Instruction::Constrain(value) => { let condition = self.convert_ssa_value(*value, dfg); - // jump to the relative location after the trap - self.obj.add_unresolved_jump(UnresolvedJumpLocation::Relative(2)); - self.push_code(BrilligOpcode::JumpIf { condition, location: 0 }); - self.push_code(BrilligOpcode::Trap); + self.context.constrain_instruction(condition); } Instruction::Allocate => { let pointer_register = self.get_or_create_register(dfg.instruction_results(instruction_id)[0]); - self.allocate_array(pointer_register, 1); + self.context.allocate_array(pointer_register, 1); } Instruction::Store { address, value } => { let address_register = self.convert_ssa_value(*address, dfg); let value_register = self.convert_ssa_value(*value, dfg); - self.push_code(BrilligOpcode::Store { - destination_pointer: address_register, - source: value_register, - }); + self.context.store_instruction(address_register, value_register); } Instruction::Load { address } => { let target_register = self.get_or_create_register(dfg.instruction_results(instruction_id)[0]); let address_register = self.convert_ssa_value(*address, dfg); - self.push_code(BrilligOpcode::Load { - destination: target_register, - source_pointer: address_register, - }); + self.context.load_instruction(target_register, address_register); } Instruction::Not(value) => { - let result_ids = dfg.instruction_results(instruction_id); - let result_register = self.get_or_create_register(result_ids[0]); - assert_eq!( dfg.type_of_value(*value), Type::bool(), "not operator can only be applied to boolean values" ); - - let one = self.make_constant(FieldElement::one()); let condition = self.convert_ssa_value(*value, dfg); + let result_ids = dfg.instruction_results(instruction_id); + let result_register = self.get_or_create_register(result_ids[0]); - // Compile !x as (1 - x) - let opcode = BrilligOpcode::BinaryIntOp { - destination: result_register, - op: BinaryIntOp::Sub, - bit_size: 1, - lhs: one, - rhs: condition, - }; - self.push_code(opcode); + self.context.not_instruction(condition, result_register); } Instruction::ForeignCall { func, arguments } => { let result_ids = dfg.instruction_results(instruction_id); @@ -222,44 +166,22 @@ impl BrilligGen { let output_registers = vecmap(result_ids, |value_id| self.convert_ssa_value(*value_id, dfg)); - let opcode = BrilligOpcode::ForeignCall { - function: func.to_owned(), - destination: RegisterValueOrArray::RegisterIndex(output_registers[0]), - input: RegisterValueOrArray::RegisterIndex(input_registers[0]), - }; - self.push_code(opcode); + self.context.foreign_call_instruction( + func.to_owned(), + &input_registers, + &output_registers, + ); } Instruction::Truncate { value, .. } => { - // Effectively a no-op because brillig already has implicit truncation on integer - // operations. We need only copy the value to it's destination. let result_ids = dfg.instruction_results(instruction_id); let destination = self.get_or_create_register(result_ids[0]); let source = self.convert_ssa_value(*value, dfg); - self.push_code(BrilligOpcode::Mov { destination, source }); + self.context.truncate_instruction(destination, source); } _ => todo!("ICE: Instruction not supported {instruction:?}"), }; } - fn allocate_array(&mut self, pointer_register: RegisterIndex, size: u32) { - let array_pointer = self.memory.allocate(size as usize); - self.push_code(BrilligOpcode::Const { - destination: pointer_register, - value: BrilligValue::from(array_pointer), - }); - } - - /// Returns a register which holds the value of a constant - fn make_constant(&mut self, constant: FieldElement) -> RegisterIndex { - let register = self.create_register(); - - let const_opcode = - BrilligOpcode::Const { destination: register, value: BrilligValue::from(constant) }; - self.push_code(const_opcode); - - register - } - /// Converts the Binary instruction into a sequence of Brillig opcodes. fn convert_ssa_binary( &mut self, @@ -273,95 +195,10 @@ impl BrilligGen { let left = self.convert_ssa_value(binary.lhs, dfg); let right = self.convert_ssa_value(binary.rhs, dfg); - // Process modulo operator separately as there is no - // Brillig modulo operator and the result is multiple - // brillig opcodes. - if let BinaryOp::Mod = binary.operator { - match binary_type { - Type::Numeric(NumericType::Unsigned { bit_size }) => { - self.convert_integer_mod(result_register, left, right, bit_size, false); - return; - } - Type::Numeric(NumericType::Signed { bit_size }) => { - self.convert_integer_mod(result_register, left, right, bit_size, true); - return; - } - _ => unimplemented!("ICE: Modulo operation not supported for type {binary_type:?}"), - } - } - - let brillig_binary_op = BrilligBinaryOp::convert_ssa_binary_op_to_brillig_binary_op( - binary.operator, - binary_type, - ); - match brillig_binary_op { - BrilligBinaryOp::Field { op } => { - let opcode = BrilligOpcode::BinaryFieldOp { - op, - destination: result_register, - lhs: left, - rhs: right, - }; - self.push_code(opcode); - } - BrilligBinaryOp::Integer { op, bit_size } => { - let opcode = BrilligOpcode::BinaryIntOp { - op, - destination: result_register, - bit_size, - lhs: left, - rhs: right, - }; - self.push_code(opcode); - } - } - } + let brillig_binary_op = + convert_ssa_binary_op_to_brillig_binary_op(binary.operator, binary_type); - /// Computes left % right by emitting the necessary Brillig opcodes. - /// - /// This is done by using the following formula: - /// - /// a % b = a - (b * (a / b)) - fn convert_integer_mod( - &mut self, - result_register: RegisterIndex, - left: RegisterIndex, - right: RegisterIndex, - bit_size: u32, - signed: bool, - ) { - let scratch_register_i = self.create_register(); - let scratch_register_j = self.create_register(); - - // i = left / right - self.push_code(BrilligOpcode::BinaryIntOp { - op: match signed { - true => BinaryIntOp::SignedDiv, - false => BinaryIntOp::UnsignedDiv, - }, - destination: scratch_register_i, - bit_size, - lhs: left, - rhs: right, - }); - - // j = i * right - self.push_code(BrilligOpcode::BinaryIntOp { - op: BinaryIntOp::Mul, - destination: scratch_register_j, - bit_size, - lhs: scratch_register_i, - rhs: right, - }); - - // result_register = left - j - self.push_code(BrilligOpcode::BinaryIntOp { - op: BinaryIntOp::Sub, - destination: result_register, - bit_size, - lhs: left, - rhs: scratch_register_j, - }); + self.context.binary_instruction(left, right, result_register, brillig_binary_op); } /// Converts an SSA `ValueId` into a `RegisterIndex`. @@ -376,10 +213,8 @@ impl BrilligGen { } Value::NumericConstant { constant, .. } => { let register_index = self.get_or_create_register(value_id); - self.push_code(BrilligOpcode::Const { - destination: register_index, - value: BrilligValue::from(*constant), - }); + + self.context.const_instruction(register_index, (*constant).into()); register_index } _ => { @@ -396,8 +231,8 @@ impl BrilligGen { brillig.convert_ssa_function(func); - brillig.push_code(BrilligOpcode::Stop); - brillig.obj + + brillig.context.artifact() } /// Converting an SSA function into Brillig bytecode. @@ -415,3 +250,98 @@ impl BrilligGen { } } } + +/// Returns the type of the operation considering the types of the operands +/// TODO: SSA issues binary operations between fields and integers. +/// This probably should be explicitly casted in SSA to avoid having to coerce at this level. +pub(crate) fn type_of_binary_operation(lhs_type: Type, rhs_type: Type) -> Type { + match (lhs_type, rhs_type) { + // If either side is a Field constant then, we coerce into the type + // of the other operand + (Type::Numeric(NumericType::NativeField), typ) + | (typ, Type::Numeric(NumericType::NativeField)) => typ, + // If both sides are numeric type, then we expect their types to be + // the same. + (Type::Numeric(lhs_type), Type::Numeric(rhs_type)) => { + assert_eq!( + lhs_type, rhs_type, + "lhs and rhs types in a binary operation are always the same" + ); + Type::Numeric(lhs_type) + } + (lhs_type, rhs_type) => { + unreachable!( + "ICE: Binary operation between types {:?} and {:?} is not allowed", + lhs_type, rhs_type + ) + } + } +} + +/// Convert an SSA binary operation into: +/// - Brillig Binary Integer Op, if it is a integer type +/// - Brillig Binary Field Op, if it is a field type +pub(crate) fn convert_ssa_binary_op_to_brillig_binary_op( + ssa_op: BinaryOp, + typ: Type, +) -> BrilligBinaryOp { + // First get the bit size and whether its a signed integer, if it is a numeric type + // if it is not,then we return None, indicating that + // it is a Field. + let bit_size_signedness = match typ { + Type::Numeric(numeric_type) => match numeric_type { + NumericType::Signed { bit_size } => Some((bit_size, true)), + NumericType::Unsigned { bit_size } => Some((bit_size, false)), + NumericType::NativeField => None, + }, + _ => unreachable!("only numeric types are allowed in binary operations. References are handled separately"), + }; + + fn binary_op_to_field_op(op: BinaryOp) -> BrilligBinaryOp { + let operation = match op { + BinaryOp::Add => BinaryFieldOp::Add, + BinaryOp::Sub => BinaryFieldOp::Sub, + BinaryOp::Mul => BinaryFieldOp::Mul, + BinaryOp::Div => BinaryFieldOp::Div, + BinaryOp::Eq => BinaryFieldOp::Equals, + _ => unreachable!( + "Field type cannot be used with {op}. This should have been caught by the frontend" + ), + }; + + BrilligBinaryOp::Field { op: operation } + } + + fn binary_op_to_int_op(op: BinaryOp, bit_size: u32, is_signed: bool) -> BrilligBinaryOp { + let operation = match op { + BinaryOp::Add => BinaryIntOp::Add, + BinaryOp::Sub => BinaryIntOp::Sub, + BinaryOp::Mul => BinaryIntOp::Mul, + BinaryOp::Div => { + if is_signed { + BinaryIntOp::SignedDiv + } else { + BinaryIntOp::UnsignedDiv + } + } + BinaryOp::Mod => { + return BrilligBinaryOp::Modulo { is_signed_integer: is_signed, bit_size } + } + BinaryOp::Eq => BinaryIntOp::Equals, + BinaryOp::Lt => BinaryIntOp::LessThan, + BinaryOp::And => BinaryIntOp::And, + BinaryOp::Or => BinaryIntOp::Or, + BinaryOp::Xor => BinaryIntOp::Xor, + BinaryOp::Shl => BinaryIntOp::Shl, + BinaryOp::Shr => BinaryIntOp::Shr, + }; + + BrilligBinaryOp::Integer { op: operation, bit_size } + } + + // If bit size is available then it is a binary integer operation + match bit_size_signedness { + Some((bit_size, is_signed)) => binary_op_to_int_op(ssa_op, bit_size, is_signed), + None => binary_op_to_field_op(ssa_op), + } +} diff --git a/crates/noirc_evaluator/src/brillig/brillig_ir.rs b/crates/noirc_evaluator/src/brillig/brillig_ir.rs new file mode 100644 index 00000000000..9e9c0eb9a76 --- /dev/null +++ b/crates/noirc_evaluator/src/brillig/brillig_ir.rs @@ -0,0 +1,313 @@ +//! This module is an abstraction layer over `Brillig` +//! To allow for separation of concerns, it knows nothing +//! about SSA types, and can therefore be tested independently. +//! `brillig_gen` is therefore the module which combines both +//! ssa types and types in this module. +//! A similar paradigm can be seen with the `acir_ir` module. +pub(crate) mod artifact; +pub(crate) mod memory; + +use self::{ + artifact::{BrilligArtifact, UnresolvedJumpLocation}, + memory::BrilligMemory, +}; +use acvm::{ + acir::brillig_vm::{ + BinaryFieldOp, BinaryIntOp, Opcode as BrilligOpcode, RegisterIndex, RegisterValueOrArray, + Value, + }, + FieldElement, +}; + +/// Brillig context object that is used while constructing the +/// Brillig bytecode. +#[derive(Default)] +pub(crate) struct BrilligContext { + obj: BrilligArtifact, + /// A usize indicating the latest un-used register. + latest_register: usize, + /// Tracks memory allocations + memory: BrilligMemory, +} + +impl BrilligContext { + /// Adds a brillig instruction to the brillig byte code + pub(crate) fn push_opcode(&mut self, opcode: BrilligOpcode) { + self.obj.byte_code.push(opcode); + } + + /// Returns the artifact + pub(crate) fn artifact(self) -> BrilligArtifact { + self.obj + } + + /// Allocates an array of size `size` and stores the pointer to the array + /// in `pointer_register` + pub(crate) fn allocate_array(&mut self, pointer_register: RegisterIndex, size: u32) { + let array_pointer = self.memory.allocate(size as usize); + self.push_opcode(BrilligOpcode::Const { + destination: pointer_register, + value: Value::from(array_pointer), + }); + } + + /// Adds a label to the next opcode + pub(crate) fn add_label_to_next_opcode(&mut self, label: T) { + self.obj.add_label_at_position(label.to_string(), self.obj.index_of_next_opcode()); + } + + /// Adds a unresolved `Jump` instruction to the bytecode. + pub(crate) fn jump_instruction(&mut self, target_label: T) { + self.add_unresolved_jump( + BrilligOpcode::Jump { location: 0 }, + UnresolvedJumpLocation::Label(target_label.to_string()), + ); + } + + /// Adds a unresolved `JumpIf` instruction to the bytecode. + pub(crate) fn jump_if_instruction( + &mut self, + condition: RegisterIndex, + target_label: T, + ) { + self.add_unresolved_jump( + BrilligOpcode::JumpIf { condition, location: 0 }, + UnresolvedJumpLocation::Label(target_label.to_string()), + ); + } + + /// Adds a unresolved `Jump` instruction to the bytecode. + fn add_unresolved_jump( + &mut self, + jmp_instruction: BrilligOpcode, + destination: UnresolvedJumpLocation, + ) { + self.obj.add_unresolved_jump(jmp_instruction, destination); + } + + /// Creates a new register. + pub(crate) fn create_register(&mut self) -> RegisterIndex { + let register = RegisterIndex::from(self.latest_register); + self.latest_register += 1; + register + } +} + +impl BrilligContext { + /// Emits brillig bytecode to jump to a trap condition if `condition` + /// is false. + pub(crate) fn constrain_instruction(&mut self, condition: RegisterIndex) { + // Jump to the relative location after the trap + self.add_unresolved_jump( + BrilligOpcode::JumpIf { condition, location: 0 }, + UnresolvedJumpLocation::Relative(2), + ); + self.push_opcode(BrilligOpcode::Trap); + } + + /// Processes a return instruction. + /// + /// For Brillig, the return is implicit, since there is no explicit return instruction. + /// The caller will take `N` values from the Register starting at register index 0. + /// `N` indicates the number of return values expected. + /// + /// Brillig does not have an explicit return instruction, so this + /// method will move all register values to the first `N` values in + /// the VM. + pub(crate) fn return_instruction(&mut self, return_registers: &[RegisterIndex]) { + for (destination_index, return_register) in return_registers.iter().enumerate() { + // If the destination register index is more than the latest register, + // we update the latest register to be the destination register because the + // brillig vm will expand the number of registers internally, when it encounters + // a register that has not been initialized. + if destination_index > self.latest_register { + self.latest_register = destination_index; + } + self.mov_instruction(destination_index.into(), *return_register); + } + self.stop_instruction(); + } + + /// Emits a `mov` instruction. + /// + /// Copies the value at `source` into `destination` + pub(crate) fn mov_instruction(&mut self, destination: RegisterIndex, source: RegisterIndex) { + self.push_opcode(BrilligOpcode::Mov { destination, source }); + } + + /// Processes a binary instruction according `operation`. + /// + /// This method will compute lhs rhs + /// and store the result in the `result` register. + pub(crate) fn binary_instruction( + &mut self, + lhs: RegisterIndex, + rhs: RegisterIndex, + result: RegisterIndex, + operation: BrilligBinaryOp, + ) { + match operation { + BrilligBinaryOp::Field { op } => { + let opcode = BrilligOpcode::BinaryFieldOp { op, destination: result, lhs, rhs }; + self.push_opcode(opcode); + } + BrilligBinaryOp::Integer { op, bit_size } => { + let opcode = + BrilligOpcode::BinaryIntOp { op, destination: result, bit_size, lhs, rhs }; + self.push_opcode(opcode); + } + BrilligBinaryOp::Modulo { is_signed_integer, bit_size } => { + self.modulo_instruction(result, lhs, rhs, bit_size, is_signed_integer); + } + } + } + + /// Stores the value of `constant` in the `result` register + pub(crate) fn const_instruction(&mut self, result: RegisterIndex, constant: Value) { + self.push_opcode(BrilligOpcode::Const { destination: result, value: constant }); + } + + /// Processes a not instruction. + /// + /// Not is computed using a subtraction operation as there is no native not instruction + /// in Brillig. + pub(crate) fn not_instruction(&mut self, condition: RegisterIndex, result: RegisterIndex) { + let one = self.make_constant(Value::from(FieldElement::one())); + + // Compile !x as (1 - x) + let opcode = BrilligOpcode::BinaryIntOp { + destination: result, + op: BinaryIntOp::Sub, + bit_size: 1, + lhs: one, + rhs: condition, + }; + self.push_opcode(opcode); + } + + /// Processes a foreign call instruction. + /// + /// Note: the function being called is external and will + /// not be linked during brillig generation. + pub(crate) fn foreign_call_instruction( + &mut self, + func_name: String, + inputs: &[RegisterIndex], + outputs: &[RegisterIndex], + ) { + let opcode = BrilligOpcode::ForeignCall { + function: func_name, + destination: RegisterValueOrArray::RegisterIndex(outputs[0]), + input: RegisterValueOrArray::RegisterIndex(inputs[0]), + }; + self.push_opcode(opcode); + } + + /// Emits a load instruction + pub(crate) fn load_instruction( + &mut self, + destination: RegisterIndex, + source_pointer: RegisterIndex, + ) { + self.push_opcode(BrilligOpcode::Load { destination, source_pointer }); + } + + /// Emits a store instruction + pub(crate) fn store_instruction( + &mut self, + destination_pointer: RegisterIndex, + source: RegisterIndex, + ) { + self.push_opcode(BrilligOpcode::Store { destination_pointer, source }); + } + + /// Emits a truncate instruction. + /// + /// Note: Truncation is used as an optimization in the SSA IR + /// for the ACIR generation pass; ACIR gen does not overflow + /// on every integer operation since it would be in-efficient. + /// Instead truncation instructions are emitted as to when a + /// truncation should be done. + /// For Brillig, all integer operations will overflow as its cheap. + pub(crate) fn truncate_instruction( + &mut self, + destination_of_truncated_value: RegisterIndex, + value_to_truncate: RegisterIndex, + ) { + // Effectively a no-op because brillig already has implicit truncation on integer + // operations. We need only copy the value to it's destination. + self.mov_instruction(destination_of_truncated_value, value_to_truncate); + } + + /// Emits a stop instruction + pub(crate) fn stop_instruction(&mut self) { + self.push_opcode(BrilligOpcode::Stop); + } + + /// Returns a register which holds the value of a constant + pub(crate) fn make_constant(&mut self, constant: Value) -> RegisterIndex { + let register = self.create_register(); + self.const_instruction(register, constant); + register + } + + /// Computes left % right by emitting the necessary Brillig opcodes. + /// + /// This is done by using the following formula: + /// + /// a % b = a - (b * (a / b)) + /// + /// Brillig does not have an explicit modulo operation, + /// so we must emit multiple opcodes and process it differently + /// to other binary instructions. + pub(crate) fn modulo_instruction( + &mut self, + result_register: RegisterIndex, + left: RegisterIndex, + right: RegisterIndex, + bit_size: u32, + signed: bool, + ) { + let scratch_register_i = self.create_register(); + let scratch_register_j = self.create_register(); + + // i = left / right + self.push_opcode(BrilligOpcode::BinaryIntOp { + op: match signed { + true => BinaryIntOp::SignedDiv, + false => BinaryIntOp::UnsignedDiv, + }, + destination: scratch_register_i, + bit_size, + lhs: left, + rhs: right, + }); + + // j = i * right + self.push_opcode(BrilligOpcode::BinaryIntOp { + op: BinaryIntOp::Mul, + destination: scratch_register_j, + bit_size, + lhs: scratch_register_i, + rhs: right, + }); + + // result_register = left - j + self.push_opcode(BrilligOpcode::BinaryIntOp { + op: BinaryIntOp::Sub, + destination: result_register, + bit_size, + lhs: left, + rhs: scratch_register_j, + }); + } +} + +/// Type to encapsulate the binary operation types in Brillig +pub(crate) enum BrilligBinaryOp { + Field { op: BinaryFieldOp }, + Integer { op: BinaryIntOp, bit_size: u32 }, + // Modulo operation requires more than one opcode + // Brillig. + Modulo { is_signed_integer: bool, bit_size: u32 }, +} diff --git a/crates/noirc_evaluator/src/brillig/brillig_ir/artifact.rs b/crates/noirc_evaluator/src/brillig/brillig_ir/artifact.rs new file mode 100644 index 00000000000..8d0020ea00f --- /dev/null +++ b/crates/noirc_evaluator/src/brillig/brillig_ir/artifact.rs @@ -0,0 +1,162 @@ +use acvm::acir::brillig_vm::Opcode as BrilligOpcode; +use std::collections::HashMap; + +#[derive(Default, Debug, Clone)] +/// Artifacts resulting from the compilation of a function into brillig byte code. +/// Currently it is just the brillig bytecode of the function. +pub(crate) struct BrilligArtifact { + pub(crate) byte_code: Vec, + /// The set of jumps that need to have their locations + /// resolved. + unresolved_jumps: Vec<(JumpInstructionPosition, UnresolvedJumpLocation)>, + /// A map of labels to their position in byte code. + labels: HashMap, +} + +/// A pointer to a location in the opcode. +pub(crate) type OpcodeLocation = usize; +/// An identifier for a location in the code. +/// +/// It is assumed that an entity will keep a map +/// of labels to Opcode locations. +pub(crate) type Label = String; +/// Pointer to a unresolved Jump instruction in +/// the bytecode. +pub(crate) type JumpInstructionPosition = OpcodeLocation; + +/// When constructing the bytecode, there may be instructions +/// which require one to jump to a specific region of code (function) +/// or a position relative to the current instruction. +/// +/// The position of a function cannot always be known +/// at this point in time, so Jumps are unresolved +/// until all functions/all of the bytecode has been processed. +/// `Label` is used as the jump location and once all of the bytecode +/// has been processed, the jumps are resolved using a map from Labels +/// to their position in the bytecode. +/// +/// Sometimes the jump destination may be relative to the jump instruction. +/// Since the absolute position in the bytecode cannot be known until +/// all internal and external functions have been linked, jumps of this +/// nature cannot be fully resolved while building the bytecode either. +/// We add relative jumps into the `Relative` variant of this enum. +#[derive(Debug, Clone)] +pub(crate) enum UnresolvedJumpLocation { + Label(String), + Relative(i32), +} + +impl BrilligArtifact { + /// Link two Brillig artifacts together and resolve all unresolved jump instructions. + pub(crate) fn link(&mut self, obj: &BrilligArtifact) -> Vec { + self.append_artifact(obj); + self.resolve_jumps(); + self.byte_code.clone() + } + + /// Link with an external brillig artifact. + /// + /// This method will offset the positions in the Brillig artifact to + /// account for the fact that it is being appended to the end of this + /// Brillig artifact (self). + fn append_artifact(&mut self, obj: &BrilligArtifact) { + let offset = self.index_of_next_opcode(); + for (jump_label, jump_location) in &obj.unresolved_jumps { + self.unresolved_jumps.push((jump_label + offset, jump_location.clone())); + } + + for (label_id, position_in_bytecode) in &obj.labels { + self.labels.insert(label_id.clone(), position_in_bytecode + offset); + } + + self.byte_code.extend_from_slice(&obj.byte_code); + } + + /// Adds a brillig instruction to the brillig byte code + pub(crate) fn push_opcode(&mut self, opcode: BrilligOpcode) { + self.byte_code.push(opcode); + } + + /// Adds a unresolved jump to be fixed at the end of bytecode processing. + pub(crate) fn add_unresolved_jump( + &mut self, + jmp_instruction: BrilligOpcode, + destination: UnresolvedJumpLocation, + ) { + assert!( + Self::is_jmp_instruction(&jmp_instruction), + "expected a jump instruction, but found {jmp_instruction:?}" + ); + + self.unresolved_jumps.push((self.index_of_next_opcode(), destination)); + self.push_opcode(jmp_instruction); + } + + /// Returns true if the opcode is a jump instruction + fn is_jmp_instruction(instruction: &BrilligOpcode) -> bool { + matches!( + instruction, + BrilligOpcode::JumpIfNot { .. } + | BrilligOpcode::JumpIf { .. } + | BrilligOpcode::Jump { .. } + ) + } + + /// Adds a label in the bytecode to specify where this block's + /// opcodes will start. + pub(crate) fn add_label_at_position(&mut self, label: String, position: OpcodeLocation) { + let old_value = self.labels.insert(label.clone(), position); + assert!( + old_value.is_none(), + "overwriting label {label}. old_value = {old_value:?}, new_value = {position}" + ); + } + + /// Returns the index of the next opcode. + /// + /// This is useful for labelling regions of code + /// before you have generated the opcodes for the region. + pub(crate) fn index_of_next_opcode(&self) -> OpcodeLocation { + self.byte_code.len() + } + + /// Resolves all of the unresolved jumps in the program. + /// + /// Note: This should only be called once all blocks are processed and + /// linkage with other bytecode has happened. + fn resolve_jumps(&mut self) { + for (location_of_jump, unresolved_location) in &self.unresolved_jumps { + let resolved_location = match unresolved_location { + UnresolvedJumpLocation::Label(label) => self.labels[label], + UnresolvedJumpLocation::Relative(offset) => { + (offset + *location_of_jump as i32) as usize + } + }; + + let jump_instruction = self.byte_code[*location_of_jump].clone(); + match jump_instruction { + BrilligOpcode::Jump { location } => { + assert_eq!(location, 0, "location is not zero, which means that the jump label does not need resolving"); + + self.byte_code[*location_of_jump] = + BrilligOpcode::Jump { location: resolved_location }; + } + BrilligOpcode::JumpIfNot { condition, location } => { + assert_eq!(location, 0, "location is not zero, which means that the jump label does not need resolving"); + + self.byte_code[*location_of_jump] = + BrilligOpcode::JumpIfNot { condition, location: resolved_location }; + } + BrilligOpcode::JumpIf { condition, location } => { + assert_eq!(location, 0, "location is not zero, which means that the jump label does not need resolving"); + + self.byte_code[*location_of_jump] = + BrilligOpcode::JumpIf { condition, location: resolved_location }; + } + _ => unreachable!( + "all jump labels should point to a jump instruction in the bytecode" + ), + } + } + } +} diff --git a/crates/noirc_evaluator/src/brillig/memory.rs b/crates/noirc_evaluator/src/brillig/brillig_ir/memory.rs similarity index 100% rename from crates/noirc_evaluator/src/brillig/memory.rs rename to crates/noirc_evaluator/src/brillig/brillig_ir/memory.rs diff --git a/crates/noirc_evaluator/src/brillig/mod.rs b/crates/noirc_evaluator/src/brillig/mod.rs index 5c28619194f..4e8ea271f92 100644 --- a/crates/noirc_evaluator/src/brillig/mod.rs +++ b/crates/noirc_evaluator/src/brillig/mod.rs @@ -1,16 +1,13 @@ -use std::collections::HashMap; - -use self::{artifact::BrilligArtifact, brillig_gen::BrilligGen}; - -pub(crate) mod artifact; -pub(crate) mod binary; pub(crate) mod brillig_gen; -pub(crate) mod memory; +pub(crate) mod brillig_ir; +use self::{brillig_gen::BrilligGen, brillig_ir::artifact::BrilligArtifact}; use crate::ssa_refactor::{ ir::function::{Function, FunctionId, RuntimeType}, ssa_gen::Ssa, }; +use std::collections::HashMap; + /// Context structure for the brillig pass. /// It stores brillig-related data required for brillig generation. #[derive(Default)] diff --git a/crates/noirc_evaluator/src/ssa_refactor/acir_gen/mod.rs b/crates/noirc_evaluator/src/ssa_refactor/acir_gen/mod.rs index 4abc44c0e45..39aabda4bb1 100644 --- a/crates/noirc_evaluator/src/ssa_refactor/acir_gen/mod.rs +++ b/crates/noirc_evaluator/src/ssa_refactor/acir_gen/mod.rs @@ -2,6 +2,8 @@ use std::collections::HashMap; +use crate::brillig::{Brillig, brillig_ir::artifact::BrilligArtifact}; + use self::acir_ir::{ acir_variable::{AcirContext, AcirType, AcirVar}, errors::AcirGenError, @@ -19,7 +21,6 @@ use super::{ }, ssa_gen::Ssa, }; -use crate::brillig::{artifact::BrilligArtifact, Brillig}; use acvm::FieldElement; use iter_extended::vecmap;