Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions compiler/noirc_evaluator/src/brillig/brillig_gen.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
//! The code generation logic for converting [crate::ssa] objects into their respective [Brillig] artifacts.
pub(crate) mod brillig_black_box;
pub(crate) mod brillig_block;
pub(crate) mod brillig_block_variables;
Expand All @@ -20,6 +21,24 @@ use super::{
};
use crate::{errors::InternalError, ssa::ir::function::Function};

/// Generates a complete Brillig entry point artifact for a given SSA-level [Function], linking all dependencies.
///
/// This function is responsible for generating a final Brillig artifact corresponding to a compiled SSA [Function].
/// It sets up the entry point context, registers input/output parameters, and recursively resolves and links
/// all transitive Brillig function dependencies.
///
/// # Parameters
/// - func: The SSA [Function] to compile as the entry point.
/// - arguments: Brillig-compatible [BrilligParameter] inputs to the function
/// - brillig: The [context structure][Brillig] of all known Brillig artifacts for dependency resolution.
/// - options: Brillig compilation options (e.g., debug trace settings).
///
/// # Returns
/// - Ok([GeneratedBrillig]): Fully linked artifact for the entry point that can be executed as a Brillig program.
/// - Err([InternalError]): If linking fails to find a dependency
///
/// # Panics
/// - If the global memory size for the function has not been precomputed.
pub(crate) fn gen_brillig_for(
func: &Function,
arguments: Vec<BrilligParameter>,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
//! Codegen for native (black box) function calls.
use acvm::{
AcirField,
acir::{
Expand Down Expand Up @@ -424,6 +425,8 @@ pub(crate) fn convert_black_box_call<F: AcirField + DebugToString, Registers: Re
}
}

/// Converts a Brillig array or vector into a heap-allocated [HeapVector]
/// suitable for use as an input to a Brillig [BlackBoxOp].
fn convert_array_or_vector<F: AcirField + DebugToString, Registers: RegisterAllocator>(
brillig_context: &mut BrilligContext<F, Registers>,
array_or_vector: BrilligVariable,
Expand Down
131 changes: 121 additions & 10 deletions compiler/noirc_evaluator/src/brillig/brillig_gen/brillig_block.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
//! Module containing Brillig-gen logic specific to an SSA function's basic blocks.
use crate::brillig::brillig_ir::artifact::Label;
use crate::brillig::brillig_ir::brillig_variable::{
BrilligArray, BrilligVariable, BrilligVector, SingleAddrVariable, type_to_heap_value_type,
Expand Down Expand Up @@ -33,8 +34,9 @@ use super::brillig_fn::FunctionContext;
use super::brillig_globals::HoistedConstantsToBrilligGlobals;
use super::constant_allocation::InstructionLocation;

/// Generate the compilation artifacts for compiling a function into brillig bytecode.
/// Context structure for compiling a [function block][crate::ssa::ir::basic_block::BasicBlock] into Brillig bytecode.
pub(crate) struct BrilligBlock<'block, Registers: RegisterAllocator> {
/// Per-function context shared across all of a function's blocks
pub(crate) function_context: &'block mut FunctionContext,
/// The basic block that is being converted
pub(crate) block_id: BasicBlockId,
Expand All @@ -45,14 +47,22 @@ pub(crate) struct BrilligBlock<'block, Registers: RegisterAllocator> {
/// For each instruction, the set of values that are not used anymore after it.
pub(crate) last_uses: HashMap<InstructionId, HashSet<ValueId>>,

/// Mapping of SSA [ValueId]s to their already instantiated values in the Brillig IR.
pub(crate) globals: &'block HashMap<ValueId, BrilligVariable>,
/// Pre-instantiated constants values shared across functions which have hoisted to the global memory space.
pub(crate) hoisted_global_constants: &'block HoistedConstantsToBrilligGlobals,

/// Status variable for whether we are generating Brillig bytecode for a function or globals.
/// This is primarily used for gating local variable specific logic.
/// For example, liveness analysis for globals is unnecessary (and adds complexity),
/// and instead globals live throughout the entirety of the program.
pub(crate) building_globals: bool,
}

impl<'block, Registers: RegisterAllocator> BrilligBlock<'block, Registers> {
/// Converts an SSA Basic block into a sequence of Brillig opcodes
///
/// This method contains the necessary initial variable and register setup for compiling
/// an SSA block by accessing the pre-computed liveness context.
pub(crate) fn compile(
function_context: &'block mut FunctionContext,
brillig_context: &'block mut BrilligContext<FieldElement, Registers>,
Expand Down Expand Up @@ -101,6 +111,25 @@ impl<'block, Registers: RegisterAllocator> BrilligBlock<'block, Registers> {
brillig_block.convert_block(dfg, call_stacks);
}

/// Converts SSA globals into Brillig global values.
///
/// Global values can be:
/// - Numeric constants
/// - Instructions that compute global values
/// - Pre-hoisted constants (shared across functions and stored in global memory)
///
/// This method expects SSA globals to already be converted to a [DataFlowGraph]
/// as to share codegen logic with standard SSA function blocks.
///
/// This method also emits any necessary debugging initialization logic (e.g., allocating a counter used
/// to track array copies).
///
/// # Returns
/// A map of hoisted (constant, type) pairs to their allocated Brillig variables,
/// which are used to resolve references to these constants throughout Brillig lowering.
///
/// # Panics
/// - Globals graph contains values other than a [constant][Value::NumericConstant] or [instruction][Value::Instruction]
pub(crate) fn compile_globals(
&mut self,
globals: &DataFlowGraph,
Expand Down Expand Up @@ -150,6 +179,10 @@ impl<'block, Registers: RegisterAllocator> BrilligBlock<'block, Registers> {
new_hoisted_constants
}

/// Internal method for [BrilligBlock::compile] that actually kicks off the Brillig compilation process
///
/// At this point any Brillig context, should be contained in [BrilligBlock] and this function should
/// only need to accept external SSA and debugging structures.
fn convert_block(&mut self, dfg: &DataFlowGraph, call_stacks: &mut CallStackHelper) {
// Add a label for this block
let block_label = self.create_block_label_for_current_function(self.block_id);
Expand Down Expand Up @@ -197,9 +230,6 @@ impl<'block, Registers: RegisterAllocator> BrilligBlock<'block, Registers> {
}

/// 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,
Expand Down Expand Up @@ -1053,6 +1083,11 @@ impl<'block, Registers: RegisterAllocator> BrilligBlock<'block, Registers> {
self.brillig_context.set_call_stack(CallStackId::root());
}

/// Debug utility method to determine whether an array's reference count (RC) is zero.
/// If RC's have drifted down to zero it means the RC increment/decrement instructions
/// have been written incorrectly.
///
/// Should only be called if [BrilligContext::enable_debug_assertions] returns true.
fn assert_rc_neq_zero(&mut self, rc_register: MemoryAddress) {
let zero = SingleAddrVariable::new(self.brillig_context.allocate_register(), 32);

Expand All @@ -1072,6 +1107,7 @@ impl<'block, Registers: RegisterAllocator> BrilligBlock<'block, Registers> {
self.brillig_context.deallocate_single_addr(condition);
}

/// Internal method to codegen an [Instruction::Call] to a [Value::Function]
fn convert_ssa_function_call(
&mut self,
func_id: FunctionId,
Expand Down Expand Up @@ -1116,6 +1152,10 @@ impl<'block, Registers: RegisterAllocator> BrilligBlock<'block, Registers> {
}
}

/// Load from an array variable at a specific index into a specified destination
///
/// # Panics
/// - The array variable is not a [BrilligVariable::BrilligArray] or [BrilligVariable::BrilligVector] when `has_offset` is false
fn convert_ssa_array_get(
&mut self,
array_variable: BrilligVariable,
Expand All @@ -1140,11 +1180,13 @@ impl<'block, Registers: RegisterAllocator> BrilligBlock<'block, Registers> {
}
}

/// Array set operation in SSA returns a new array or slice that is a copy of the parameter array or slice
/// With a specific value changed.
/// Array set operation in SSA returns a new array or vector that is a copy of the parameter array or vector
/// with a specific value changed.
///
/// Returns `source_size_as_register`, which is expected to be deallocated with:
/// `self.brillig_context.deallocate_register(source_size_as_register)`
/// Whether an actual copy other the array occurs or we write into the same source array is determined by the
/// [call into the array copy procedure][BrilligContext::call_array_copy_procedure].
/// If the reference count of an array pointer is one, we write directly to the array.
/// Look at the [procedure compilation][crate::brillig::brillig_ir::procedures::compile_procedure] for the exact procedure's codegen.
fn convert_ssa_array_set(
&mut self,
source_variable: BrilligVariable,
Expand Down Expand Up @@ -1704,6 +1746,8 @@ impl<'block, Registers: RegisterAllocator> BrilligBlock<'block, Registers> {
self.brillig_context.deallocate_single_addr(left_is_negative);
}

/// Overflow checks for the following unsigned binary operations
/// - Checked Add/Sub/Mul
#[allow(clippy::too_many_arguments)]
fn add_overflow_check(
&mut self,
Expand Down Expand Up @@ -1790,13 +1834,23 @@ impl<'block, Registers: RegisterAllocator> BrilligBlock<'block, Registers> {
}
}

/// Accepts a list of constant values to be initialized
///
/// This method does no checks as to whether the supplied constants are actually constants.
/// It is expected that this method is called before converting an SSA instruction to Brillig
/// and the constants to be initialized have been precomputed and stored in [FunctionContext::constant_allocation].
fn initialize_constants(&mut self, constants: &[ValueId], dfg: &DataFlowGraph) {
for &constant_id in constants {
self.convert_ssa_value(constant_id, dfg);
}
}

/// Converts an SSA `ValueId` into a `RegisterOrMemory`. Initializes if necessary.
/// Converts an SSA [ValueId] into a [BrilligVariable]. Initializes if necessary.
///
/// This method also first checks whether the SSA value is a hoisted global constant.
/// If it has already been initialized in the global space, we return the already existing variable.
/// If an SSA value is a [Value::Global], we check whether the value exists in the [BrilligBlock::globals] map,
/// otherwise the method panics.
pub(crate) fn convert_ssa_value(
&mut self,
value_id: ValueId,
Expand Down Expand Up @@ -1869,6 +1923,24 @@ impl<'block, Registers: RegisterAllocator> BrilligBlock<'block, Registers> {
}
}

/// Initializes a constant array in Brillig memory.
///
/// This method is responsible for writing a constant array's contents into memory, starting
/// from the given `pointer`. It chooses between compile-time or runtime initialization
/// depending on the data pattern and size.
///
/// If the array is large (`>10` items), its elements are all numeric, and all items are identical,
/// a **runtime loop** is generated to perform the initialization more efficiently.
///
/// Otherwise, the method falls back to a straightforward **compile-time** initialization, where
/// each array element is emitted explicitly.
///
/// This optimization helps reduce Brillig bytecode size and runtime cost when initializing large,
/// uniform arrays.
///
/// # Example
/// For an array like [5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5], a runtime loop will be used
/// For an array like [1, 2, 3, 4], each element will be set explicitly
fn initialize_constant_array(
&mut self,
data: &im::Vector<ValueId>,
Expand Down Expand Up @@ -1909,6 +1981,14 @@ impl<'block, Registers: RegisterAllocator> BrilligBlock<'block, Registers> {
}
}

/// Codegens Brillig instructions to initialize a large, constant array using a runtime loop.
///
/// This method assumes the array consists of identical items repeated multiple times.
/// It generates a Brillig loop that writes the repeated item into memory efficiently,
/// reducing bytecode size and instruction count compared to unrolling each element.
///
/// For complex types (e.g., tuples), multiple memory writes happen per loop iteration.
/// For primitive type (e.g., u32, Field), a single memory write happens per loop iteration.
fn initialize_constant_array_runtime(
&mut self,
item_types: Arc<Vec<Type>>,
Expand Down Expand Up @@ -1991,6 +2071,14 @@ impl<'block, Registers: RegisterAllocator> BrilligBlock<'block, Registers> {
self.brillig_context.deallocate_single_addr(end_pointer_variable);
}

/// Codegens Brillig instructions to initialize a constant array at compile time.
///
/// This method generates one `store` instruction per array element, writing each
/// value from the SSA into consecutive memory addresses starting at `pointer`.
///
/// Unlike [initialize_constant_array_runtime][Self::initialize_constant_array_runtime], this
/// does not use loops and emits one instruction per write, which can increase bytecode size
/// but provides fine-grained control.
fn initialize_constant_array_comptime(
&mut self,
data: &im::Vector<crate::ssa::ir::map::Id<Value>>,
Expand Down Expand Up @@ -2032,6 +2120,20 @@ impl<'block, Registers: RegisterAllocator> BrilligBlock<'block, Registers> {
variable.extract_single_addr()
}

/// Allocates a variable to hold the result of an external function call (e.g., foreign or black box).
/// For more information about foreign function calls in Brillig take a look at the [foreign call opcode][acvm::acir::brillig::Opcode::ForeignCall].
///
/// This is typically used during Brillig codegen for calls to [Value::ForeignFunction], where
/// external host functions return values back into the program.
///
/// Numeric types and fixed-sized array results are directly allocated.
/// As vector's are determined at runtime they are allocated differently.
/// - Allocates memory for a [BrilligVector], which holds a pointer and dynamic size.
/// - Initializes the pointer using the free memory pointer.
/// - The actual size will be updated after the foreign function call returns.
///
/// # Returns
/// A [BrilligVariable] representing the allocated memory structure to store the foreign call's result.
fn allocate_external_call_result(
&mut self,
result: ValueId,
Expand Down Expand Up @@ -2080,6 +2182,12 @@ impl<'block, Registers: RegisterAllocator> BrilligBlock<'block, Registers> {
}
}

/// Recursively allocates memory for a nested array returned from a foreign function call.
///
/// # Panics
/// - If the provided `typ` is not an array.
/// - If any slice types are encountered within the nested structure, since slices
/// require runtime size information and cannot be allocated statically here.
fn allocate_foreign_call_result_array(&mut self, typ: &Type, array: BrilligArray) {
let Type::Array(types, size) = typ else {
unreachable!("ICE: allocate_foreign_call_array() expects an array, got {typ:?}")
Expand Down Expand Up @@ -2156,6 +2264,9 @@ impl<'block, Registers: RegisterAllocator> BrilligBlock<'block, Registers> {
}
}

/// If the supplied value is a numeric constant check whether it is exists within
/// the precomputed [hoisted globals map][Self::hoisted_global_constants].
/// If the variable exists as a hoisted global return that value, otherwise return `None`.
fn get_hoisted_global(
&self,
dfg: &DataFlowGraph,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
//! This module handles allocation, tracking, and lifetime management of variables
//! within a Brillig compiled SSA basic block.
//!
//! [BlockVariables] maintains a set of SSA [ValueId]s that are live and available
//! during the compilation of a single SSA block into Brillig instructions. It cooperates
//! with the [FunctionContext] to manage the mapping from SSA values to [BrilligVariable]s
//! and with the [BrilligContext] for allocating registers.
//!
//! Variables are:
//! - Allocated when first defined in a block (if not already global or hoisted to the global space).
//! - Cached for reuse to avoid redundant register allocation.
//! - Deallocated explicitly when no longer needed (as determined by SSA liveness).
use acvm::FieldElement;
use fxhash::FxHashSet as HashSet;

Expand All @@ -19,6 +31,15 @@ use crate::{

use super::brillig_fn::FunctionContext;

/// Tracks SSA variables that are live and usable during Brillig compilation of a block.
///
/// This structure is meant to be instantiated per SSA basic block and initialized using the
/// the set of live variables that must be available at the block's entry.
///
/// It implements:
/// - A set of active [ValueId]s that are allocated and usable.
/// - The interface to define new variables as needed for instructions within the block.
/// - Utilities to remove, check, and retrieve variables during Brillig codegen.
#[derive(Debug, Default)]
pub(crate) struct BlockVariables {
available_variables: HashSet<ValueId>,
Expand Down
15 changes: 14 additions & 1 deletion compiler/noirc_evaluator/src/brillig/brillig_gen/brillig_fn.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
//! Module containing Brillig-gen logic specific to SSA [Function]'s.
use iter_extended::vecmap;

use crate::{
Expand All @@ -17,10 +18,15 @@ use fxhash::FxHashMap as HashMap;

use super::{constant_allocation::ConstantAllocation, variable_liveness::VariableLiveness};

/// Information required to compile an SSA [Function] into Brillig bytecode.
///
/// This structure is instantiated once per function and used throughout basic block code generation.
/// It can also represent a non-function context (e.g., global instantiation) to reuse block codegen logic
/// by leaving its `function_id` field unset.
#[derive(Default)]
pub(crate) struct FunctionContext {
/// A `FunctionContext` is necessary for using a Brillig block's code gen, but sometimes
/// such as with globals, we are not within a function and do not have a function id.
/// such as with globals, we are not within a function and do not have a [FunctionId].
function_id: Option<FunctionId>,
/// Map from SSA values its allocation. Since values can be only defined once in SSA form, we insert them here on when we allocate them at their definition.
pub(crate) ssa_value_allocations: HashMap<ValueId, BrilligVariable>,
Expand Down Expand Up @@ -60,6 +66,13 @@ impl FunctionContext {
self.function_id.expect("ICE: function_id should already be set")
}

/// Converts an SSA [Type] into a corresponding [BrilligParameter].
///
/// This conversion defines the calling convention for Brillig functions,
/// ensuring that SSA values are correctly mapped to memory layouts understood by the VM.
///
/// # Panics
/// Panics if called with a slice type, as a slice's memory layout cannot be inferred without runtime data.
pub(crate) fn ssa_type_to_parameter(typ: &Type) -> BrilligParameter {
match typ {
Type::Numeric(_) | Type::Reference(_) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
//! Codegen for converting SSA globals to Brillig bytecode.
use std::collections::{BTreeMap, BTreeSet};

use acvm::FieldElement;
Expand Down Expand Up @@ -46,7 +47,7 @@ pub(crate) struct BrilligGlobals {

/// Mapping of SSA value ids to their Brillig allocations
pub(crate) type SsaToBrilligGlobals = HashMap<ValueId, BrilligVariable>;

/// Mapping of constant values shared across functions hoisted to the global memory space
pub(crate) type HoistedConstantsToBrilligGlobals =
HashMap<(FieldElement, NumericType), BrilligVariable>;
/// Mapping of a constant value and the number of functions in which it occurs
Expand Down
Loading
Loading