Skip to content

Commit

Permalink
feat: std::hint::black_box function. (#6529)
Browse files Browse the repository at this point in the history
Co-authored-by: Tom French <[email protected]>
  • Loading branch information
aakoshh and TomAFrench authored Dec 9, 2024
1 parent b3c04f0 commit 237e8fa
Show file tree
Hide file tree
Showing 15 changed files with 323 additions and 47 deletions.
10 changes: 10 additions & 0 deletions compiler/noirc_evaluator/src/acir/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ use crate::brillig::{
Brillig,
};
use crate::errors::{InternalError, InternalWarning, RuntimeError, SsaReport};
use crate::ssa::ir::instruction::Hint;
use crate::ssa::{
function_builder::data_bus::DataBus,
ir::{
Expand Down Expand Up @@ -2131,6 +2132,15 @@ impl<'a> Context<'a> {
result_ids: &[ValueId],
) -> Result<Vec<AcirValue>, RuntimeError> {
match intrinsic {
Intrinsic::Hint(Hint::BlackBox) => {
// Identity function; at the ACIR level this is a no-op, it only affects the SSA.
assert_eq!(
arguments.len(),
result_ids.len(),
"ICE: BlackBox input and output lengths should match."
);
Ok(arguments.iter().map(|v| self.convert_value(*v, dfg)).collect())
}
Intrinsic::BlackBox(black_box) => {
// Slices are represented as a tuple of (length, slice contents).
// We must check the inputs to determine if there are slices
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use crate::brillig::brillig_ir::{
BrilligBinaryOp, BrilligContext, ReservedRegisters, BRILLIG_MEMORY_ADDRESSING_BIT_SIZE,
};
use crate::ssa::ir::dfg::CallStack;
use crate::ssa::ir::instruction::ConstrainError;
use crate::ssa::ir::instruction::{ConstrainError, Hint};
use crate::ssa::ir::{
basic_block::BasicBlockId,
dfg::DataFlowGraph,
Expand Down Expand Up @@ -552,6 +552,10 @@ impl<'block> BrilligBlock<'block> {
false,
);
}
Intrinsic::Hint(Hint::BlackBox) => {
let result_ids = dfg.instruction_results(instruction_id);
self.convert_ssa_identity_call(arguments, dfg, result_ids);
}
Intrinsic::BlackBox(bb_func) => {
// Slices are represented as a tuple of (length, slice contents).
// We must check the inputs to determine if there are slices
Expand Down Expand Up @@ -874,6 +878,30 @@ impl<'block> BrilligBlock<'block> {
self.brillig_context.codegen_call(func_id, &argument_variables, &return_variables);
}

/// Copy the input arguments to the results.
fn convert_ssa_identity_call(
&mut self,
arguments: &[ValueId],
dfg: &DataFlowGraph,
result_ids: &[ValueId],
) {
let argument_variables =
vecmap(arguments, |argument_id| self.convert_ssa_value(*argument_id, dfg));

let return_variables = vecmap(result_ids, |result_id| {
self.variables.define_variable(
self.function_context,
self.brillig_context,
*result_id,
dfg,
)
});

for (src, dst) in argument_variables.into_iter().zip(return_variables) {
self.brillig_context.mov_instruction(dst.extract_register(), src.extract_register());
}
}

fn validate_array_index(
&mut self,
array_variable: BrilligVariable,
Expand Down
91 changes: 49 additions & 42 deletions compiler/noirc_evaluator/src/ssa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,53 +94,15 @@ pub(crate) fn optimize_into_acir(
) -> Result<ArtifactsAndWarnings, RuntimeError> {
let ssa_gen_span = span!(Level::TRACE, "ssa_generation");
let ssa_gen_span_guard = ssa_gen_span.enter();

let mut ssa = SsaBuilder::new(
let builder = SsaBuilder::new(
program,
options.ssa_logging.clone(),
options.force_brillig_output,
options.print_codegen_timings,
&options.emit_ssa,
)?
.run_pass(Ssa::defunctionalize, "Defunctionalization")
.run_pass(Ssa::remove_paired_rc, "Removing Paired rc_inc & rc_decs")
.run_pass(Ssa::separate_runtime, "Runtime Separation")
.run_pass(Ssa::resolve_is_unconstrained, "Resolving IsUnconstrained")
.run_pass(|ssa| ssa.inline_functions(options.inliner_aggressiveness), "Inlining (1st)")
// Run mem2reg with the CFG separated into blocks
.run_pass(Ssa::mem2reg, "Mem2Reg (1st)")
.run_pass(Ssa::simplify_cfg, "Simplifying (1st)")
.run_pass(Ssa::as_slice_optimization, "`as_slice` optimization")
.try_run_pass(
Ssa::evaluate_static_assert_and_assert_constant,
"`static_assert` and `assert_constant`",
)?
.run_pass(Ssa::loop_invariant_code_motion, "Loop Invariant Code Motion")
.try_run_pass(
|ssa| ssa.unroll_loops_iteratively(options.max_bytecode_increase_percent),
"Unrolling",
)?
.run_pass(Ssa::simplify_cfg, "Simplifying (2nd)")
.run_pass(Ssa::flatten_cfg, "Flattening")
.run_pass(Ssa::remove_bit_shifts, "After Removing Bit Shifts")
// Run mem2reg once more with the flattened CFG to catch any remaining loads/stores
.run_pass(Ssa::mem2reg, "Mem2Reg (2nd)")
// Run the inlining pass again to handle functions with `InlineType::NoPredicates`.
// Before flattening is run, we treat functions marked with the `InlineType::NoPredicates` as an entry point.
// This pass must come immediately following `mem2reg` as the succeeding passes
// may create an SSA which inlining fails to handle.
.run_pass(
|ssa| ssa.inline_functions_with_no_predicates(options.inliner_aggressiveness),
"Inlining (2nd)",
)
.run_pass(Ssa::remove_if_else, "Remove IfElse")
.run_pass(Ssa::fold_constants, "Constant Folding")
.run_pass(Ssa::remove_enable_side_effects, "EnableSideEffectsIf removal")
.run_pass(Ssa::fold_constants_using_constraints, "Constraint Folding")
.run_pass(Ssa::dead_instruction_elimination, "Dead Instruction Elimination (1st)")
.run_pass(Ssa::simplify_cfg, "Simplifying:")
.run_pass(Ssa::array_set_optimization, "Array Set Optimizations")
.finish();
)?;

let mut ssa = optimize_all(builder, options)?;

let ssa_level_warnings = if options.skip_underconstrained_check {
vec![]
Expand Down Expand Up @@ -173,9 +135,54 @@ pub(crate) fn optimize_into_acir(
let artifacts = time("SSA to ACIR", options.print_codegen_timings, || {
ssa.into_acir(&brillig, options.expression_width)
})?;

Ok(ArtifactsAndWarnings(artifacts, ssa_level_warnings))
}

/// Run all SSA passes.
fn optimize_all(builder: SsaBuilder, options: &SsaEvaluatorOptions) -> Result<Ssa, RuntimeError> {
Ok(builder
.run_pass(Ssa::defunctionalize, "Defunctionalization")
.run_pass(Ssa::remove_paired_rc, "Removing Paired rc_inc & rc_decs")
.run_pass(Ssa::separate_runtime, "Runtime Separation")
.run_pass(Ssa::resolve_is_unconstrained, "Resolving IsUnconstrained")
.run_pass(|ssa| ssa.inline_functions(options.inliner_aggressiveness), "Inlining (1st)")
// Run mem2reg with the CFG separated into blocks
.run_pass(Ssa::mem2reg, "Mem2Reg (1st)")
.run_pass(Ssa::simplify_cfg, "Simplifying (1st)")
.run_pass(Ssa::as_slice_optimization, "`as_slice` optimization")
.try_run_pass(
Ssa::evaluate_static_assert_and_assert_constant,
"`static_assert` and `assert_constant`",
)?
.run_pass(Ssa::loop_invariant_code_motion, "Loop Invariant Code Motion")
.try_run_pass(
|ssa| ssa.unroll_loops_iteratively(options.max_bytecode_increase_percent),
"Unrolling",
)?
.run_pass(Ssa::simplify_cfg, "Simplifying (2nd)")
.run_pass(Ssa::flatten_cfg, "Flattening")
.run_pass(Ssa::remove_bit_shifts, "After Removing Bit Shifts")
// Run mem2reg once more with the flattened CFG to catch any remaining loads/stores
.run_pass(Ssa::mem2reg, "Mem2Reg (2nd)")
// Run the inlining pass again to handle functions with `InlineType::NoPredicates`.
// Before flattening is run, we treat functions marked with the `InlineType::NoPredicates` as an entry point.
// This pass must come immediately following `mem2reg` as the succeeding passes
// may create an SSA which inlining fails to handle.
.run_pass(
|ssa| ssa.inline_functions_with_no_predicates(options.inliner_aggressiveness),
"Inlining (2nd)",
)
.run_pass(Ssa::remove_if_else, "Remove IfElse")
.run_pass(Ssa::fold_constants, "Constant Folding")
.run_pass(Ssa::remove_enable_side_effects, "EnableSideEffectsIf removal")
.run_pass(Ssa::fold_constants_using_constraints, "Constraint Folding")
.run_pass(Ssa::dead_instruction_elimination, "Dead Instruction Elimination (1st)")
.run_pass(Ssa::simplify_cfg, "Simplifying:")
.run_pass(Ssa::array_set_optimization, "Array Set Optimizations")
.finish())
}

// Helper to time SSA passes
fn time<T>(name: &str, print_timings: bool, f: impl FnOnce() -> T) -> T {
let start_time = chrono::Utc::now().time();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::errors::{InternalBug, SsaReport};
use crate::ssa::ir::basic_block::BasicBlockId;
use crate::ssa::ir::function::RuntimeType;
use crate::ssa::ir::function::{Function, FunctionId};
use crate::ssa::ir::instruction::{Instruction, InstructionId, Intrinsic};
use crate::ssa::ir::instruction::{Hint, Instruction, InstructionId, Intrinsic};
use crate::ssa::ir::value::{Value, ValueId};
use crate::ssa::ssa_gen::Ssa;
use im::HashMap;
Expand Down Expand Up @@ -209,6 +209,7 @@ impl Context {
| Intrinsic::AsField
| Intrinsic::AsSlice
| Intrinsic::BlackBox(..)
| Intrinsic::Hint(Hint::BlackBox)
| Intrinsic::DerivePedersenGenerators
| Intrinsic::FromField
| Intrinsic::SliceInsert
Expand Down
16 changes: 16 additions & 0 deletions compiler/noirc_evaluator/src/ssa/ir/instruction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ pub(crate) enum Intrinsic {
ToBits(Endian),
ToRadix(Endian),
BlackBox(BlackBoxFunc),
Hint(Hint),
FromField,
AsField,
AsWitness,
Expand Down Expand Up @@ -95,6 +96,7 @@ impl std::fmt::Display for Intrinsic {
Intrinsic::ToRadix(Endian::Big) => write!(f, "to_be_radix"),
Intrinsic::ToRadix(Endian::Little) => write!(f, "to_le_radix"),
Intrinsic::BlackBox(function) => write!(f, "{function}"),
Intrinsic::Hint(Hint::BlackBox) => write!(f, "black_box"),
Intrinsic::FromField => write!(f, "from_field"),
Intrinsic::AsField => write!(f, "as_field"),
Intrinsic::AsWitness => write!(f, "as_witness"),
Expand Down Expand Up @@ -144,6 +146,9 @@ impl Intrinsic {
| Intrinsic::DerivePedersenGenerators
| Intrinsic::FieldLessThan => false,

// Treat the black_box hint as-if it could potentially have side effects.
Intrinsic::Hint(Hint::BlackBox) => true,

// Some black box functions have side-effects
Intrinsic::BlackBox(func) => matches!(
func,
Expand Down Expand Up @@ -214,6 +219,7 @@ impl Intrinsic {
"is_unconstrained" => Some(Intrinsic::IsUnconstrained),
"derive_pedersen_generators" => Some(Intrinsic::DerivePedersenGenerators),
"field_less_than" => Some(Intrinsic::FieldLessThan),
"black_box" => Some(Intrinsic::Hint(Hint::BlackBox)),
"array_refcount" => Some(Intrinsic::ArrayRefCount),
"slice_refcount" => Some(Intrinsic::SliceRefCount),

Expand All @@ -229,6 +235,16 @@ pub(crate) enum Endian {
Little,
}

/// Compiler hints.
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) enum Hint {
/// Hint to the compiler to treat the call as having potential side effects,
/// so that the value passed to it can survive SSA passes without being
/// simplified out completely. This facilitates testing and reproducing
/// runtime behavior with constants.
BlackBox,
}

#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
/// Instructions are used to perform tasks.
/// The instructions that the IR is able to specify are listed below.
Expand Down
3 changes: 2 additions & 1 deletion compiler/noirc_evaluator/src/ssa/ir/instruction/call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ use crate::ssa::{
opt::flatten_cfg::value_merger::ValueMerger,
};

use super::{Binary, BinaryOp, Endian, Instruction, SimplifyResult};
use super::{Binary, BinaryOp, Endian, Hint, Instruction, SimplifyResult};

mod blackbox;

Expand Down Expand Up @@ -326,6 +326,7 @@ pub(super) fn simplify_call(
SimplifyResult::None
}
}
Intrinsic::Hint(Hint::BlackBox) => SimplifyResult::None,
Intrinsic::BlackBox(bb_func) => {
simplify_black_box_func(bb_func, arguments, dfg, block, call_stack)
}
Expand Down
104 changes: 104 additions & 0 deletions compiler/noirc_evaluator/src/ssa/opt/hint.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
#[cfg(test)]
mod tests {
use acvm::acir::circuit::ExpressionWidth;

use crate::{
errors::RuntimeError,
ssa::{
opt::assert_normalized_ssa_equals, optimize_all, Ssa, SsaBuilder, SsaEvaluatorOptions,
SsaLogging,
},
};

fn run_all_passes(ssa: Ssa) -> Result<Ssa, RuntimeError> {
let options = &SsaEvaluatorOptions {
ssa_logging: SsaLogging::None,
enable_brillig_logging: false,
force_brillig_output: false,
print_codegen_timings: false,
expression_width: ExpressionWidth::default(),
emit_ssa: None,
skip_underconstrained_check: true,
inliner_aggressiveness: 0,
max_bytecode_increase_percent: None,
};

let builder = SsaBuilder {
ssa,
ssa_logging: options.ssa_logging.clone(),
print_codegen_timings: false,
};

optimize_all(builder, options)
}

/// Test that the `std::hint::black_box` function prevents some of the optimizations.
#[test]
fn test_black_box_hint() {
// fn main(sum: u32) {
// // This version simplifies into a single `constraint 50 == sum`
// assert_eq(loop(5, 10), sum);
// // This should preserve additions because `k` is opaque, as if it came from an input.
// assert_eq(loop(5, std::hint::black_box(10)), sum);
// }
// fn loop(n: u32, k: u32) -> u32 {
// let mut sum = 0;
// for _ in 0..n {
// sum = sum + k;
// }
// sum
// }

// Initial SSA:
let src = "
acir(inline) fn main f0 {
b0(v0: u32):
v4 = call f1(u32 5, u32 10) -> u32
v5 = eq v4, v0
constrain v4 == v0
v7 = call black_box(u32 10) -> u32
v9 = call f1(u32 5, v7) -> u32
v10 = eq v9, v0
constrain v9 == v0
return
}
acir(inline) fn loop f1 {
b0(v0: u32, v1: u32):
v3 = allocate -> &mut u32
store u32 0 at v3
jmp b1(u32 0)
b1(v2: u32):
v5 = lt v2, v0
jmpif v5 then: b3, else: b2
b3():
v7 = load v3 -> u32
v8 = add v7, v1
store v8 at v3
v10 = add v2, u32 1
jmp b1(v10)
b2():
v6 = load v3 -> u32
return v6
}
";

// After Array Set Optimizations:
let expected = "
acir(inline) fn main f0 {
b0(v0: u32):
constrain u32 50 == v0
v4 = call black_box(u32 10) -> u32
v5 = add v4, v4
v6 = add v5, v4
v7 = add v6, v4
v8 = add v7, v4
constrain v8 == u32 50
return
}
";

let ssa = Ssa::from_str(src).unwrap();
let ssa = run_all_passes(ssa).unwrap();
assert_normalized_ssa_equals(ssa, expected);
}
}
1 change: 1 addition & 0 deletions compiler/noirc_evaluator/src/ssa/opt/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ mod constant_folding;
mod defunctionalize;
mod die;
pub(crate) mod flatten_cfg;
mod hint;
mod inlining;
mod loop_invariant;
mod mem2reg;
Expand Down
Loading

0 comments on commit 237e8fa

Please sign in to comment.