Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: std::hint::black_box function. #6529

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 6 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
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 @@ -406,6 +406,10 @@ impl<'block> BrilligBlock<'block> {
let result_ids = dfg.instruction_results(instruction_id);
self.convert_ssa_function_call(*func_id, arguments, dfg, result_ids);
}
Value::Intrinsic(Intrinsic::Hint(Hint::BlackBox)) => {
let result_ids = dfg.instruction_results(instruction_id);
self.convert_ssa_identity_call(arguments, dfg, result_ids);
}
Value::Intrinsic(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 @@ -800,6 +804,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
86 changes: 48 additions & 38 deletions compiler/noirc_evaluator/src/ssa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,49 +86,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.enable_ssa_logging,
options.force_brillig_output,
options.print_codegen_timings,
&options.emit_ssa,
)?
.run_pass(Ssa::defunctionalize, "After Defunctionalization:")
.run_pass(Ssa::remove_paired_rc, "After Removing Paired rc_inc & rc_decs:")
.run_pass(Ssa::separate_runtime, "After Runtime Separation:")
.run_pass(Ssa::resolve_is_unconstrained, "After Resolving IsUnconstrained:")
.run_pass(|ssa| ssa.inline_functions(options.inliner_aggressiveness), "After Inlining (1st):")
// Run mem2reg with the CFG separated into blocks
.run_pass(Ssa::mem2reg, "After Mem2Reg (1st):")
.run_pass(Ssa::simplify_cfg, "After Simplifying (1st):")
.run_pass(Ssa::as_slice_optimization, "After `as_slice` optimization")
.try_run_pass(
Ssa::evaluate_static_assert_and_assert_constant,
"After `static_assert` and `assert_constant`:",
)?
.try_run_pass(Ssa::unroll_loops_iteratively, "After Unrolling:")?
.run_pass(Ssa::simplify_cfg, "After Simplifying (2nd):")
.run_pass(Ssa::flatten_cfg, "After 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, "After 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),
"After Inlining (2nd):",
)
.run_pass(Ssa::remove_if_else, "After Remove IfElse:")
.run_pass(Ssa::fold_constants, "After Constant Folding:")
.run_pass(Ssa::remove_enable_side_effects, "After EnableSideEffectsIf removal:")
.run_pass(Ssa::fold_constants_using_constraints, "After Constraint Folding:")
.run_pass(Ssa::dead_instruction_elimination, "After Dead Instruction Elimination:")
.run_pass(Ssa::simplify_cfg, "After Simplifying:")
.run_pass(Ssa::array_set_optimization, "After Array Set Optimizations:")
.finish();
)?;

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

let ssa_level_warnings = if options.skip_underconstrained_check {
vec![]
Expand All @@ -147,9 +113,53 @@ 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, "After Defunctionalization:")
.run_pass(Ssa::remove_paired_rc, "After Removing Paired rc_inc & rc_decs:")
.run_pass(Ssa::separate_runtime, "After Runtime Separation:")
.run_pass(Ssa::resolve_is_unconstrained, "After Resolving IsUnconstrained:")
.run_pass(
|ssa| ssa.inline_functions(options.inliner_aggressiveness),
"After Inlining (1st):",
)
// Run mem2reg with the CFG separated into blocks
.run_pass(Ssa::mem2reg, "After Mem2Reg (1st):")
.run_pass(Ssa::simplify_cfg, "After Simplifying (1st):")
.run_pass(Ssa::as_slice_optimization, "After `as_slice` optimization")
.try_run_pass(
Ssa::evaluate_static_assert_and_assert_constant,
"After `static_assert` and `assert_constant`:",
)?
.try_run_pass(Ssa::unroll_loops_iteratively, "After Unrolling:")?
.run_pass(Ssa::simplify_cfg, "After Simplifying (2nd):")
.run_pass(Ssa::flatten_cfg, "After 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, "After 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),
"After Inlining (2nd):",
)
.run_pass(Ssa::remove_if_else, "After Remove IfElse:")
.run_pass(Ssa::fold_constants, "After Constant Folding:")
.run_pass(Ssa::remove_enable_side_effects, "After EnableSideEffectsIf removal:")
.run_pass(Ssa::fold_constants_using_constraints, "After Constraint Folding:")
.run_pass(Ssa::dead_instruction_elimination, "After Dead Instruction Elimination:")
.run_pass(Ssa::simplify_cfg, "After Simplifying (3rd):")
.run_pass(Ssa::array_set_optimization, "After 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
7 changes: 6 additions & 1 deletion compiler/noirc_evaluator/src/ssa/acir_gen/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use self::acir_ir::generated_acir::BrilligStdlibFunc;
use super::function_builder::data_bus::DataBus;
use super::ir::dfg::CallStack;
use super::ir::function::FunctionId;
use super::ir::instruction::{ConstrainError, ErrorType};
use super::ir::instruction::{ConstrainError, ErrorType, Hint};
use super::ir::printer::try_to_extract_string_from_error_payload;
use super::{
ir::{
Expand Down Expand Up @@ -2170,6 +2170,11 @@ 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(), 1, "ICE: BlackBox hint must have a single argument.");
Ok(vec![self.convert_value(arguments[0], dfg)])
}
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 @@ -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 @@ -207,6 +207,7 @@ impl Context {
| Intrinsic::AsField
| Intrinsic::AsSlice
| Intrinsic::BlackBox(..)
| Intrinsic::Hint(Hint::BlackBox)
| Intrinsic::DerivePedersenGenerators
| Intrinsic::FromField
| Intrinsic::SlicePushBack
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 @@ -68,6 +68,7 @@ pub(crate) enum Intrinsic {
ToBits(Endian),
ToRadix(Endian),
BlackBox(BlackBoxFunc),
Hint(Hint),
FromField,
AsField,
AsWitness,
Expand Down Expand Up @@ -97,6 +98,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 @@ -137,6 +139,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 @@ -174,6 +179,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)),

other => BlackBoxFunc::lookup(other).map(Intrinsic::BlackBox),
}
Expand All @@ -187,6 +193,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 @@ -317,6 +317,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),
Intrinsic::AsField => {
let instruction = Instruction::Cast(
Expand Down
96 changes: 96 additions & 0 deletions compiler/noirc_evaluator/src/ssa/opt/hint.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
#[cfg(test)]
mod tests {
use acvm::acir::circuit::ExpressionWidth;

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

fn run_all_passes(ssa: Ssa) -> Result<Ssa, RuntimeError> {
let builder = SsaBuilder { ssa, print_ssa_passes: false, print_codegen_timings: false };
let options = &SsaEvaluatorOptions {
enable_ssa_logging: false,
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,
};
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 mem2reg;
mod normalize_value_ids;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use crate::ssa::{
basic_block::BasicBlockId,
dfg::DataFlowGraph,
function::{Function, RuntimeType},
instruction::{BinaryOp, Instruction, Intrinsic},
instruction::{BinaryOp, Hint, Instruction, Intrinsic},
types::Type,
value::Value,
},
Expand Down Expand Up @@ -173,6 +173,7 @@ impl Context {
| Intrinsic::ToBits(_)
| Intrinsic::ToRadix(_)
| Intrinsic::BlackBox(_)
| Intrinsic::Hint(Hint::BlackBox)
| Intrinsic::FromField
| Intrinsic::AsField
| Intrinsic::AsSlice
Expand Down
Loading
Loading