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

Merged
merged 25 commits into from
Dec 9, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
cb717f4
Add black_box hint
aakoshh Nov 15, 2024
b97f4d6
Test that using the black_box in SSA prevented turning the loop into …
aakoshh Nov 15, 2024
2fcc268
Fix nargo package name
aakoshh Nov 15, 2024
444940a
Try to copy arg to return
aakoshh Nov 15, 2024
bf23803
Fix MOV arg order
aakoshh Nov 15, 2024
9c2eacc
Merge remote-tracking branch 'origin/master' into 6471-hint-blackbox
aakoshh Nov 15, 2024
0d69364
Add array based tests. Allow multiple return values
aakoshh Nov 15, 2024
9eb9c32
Add some tests with references
aakoshh Nov 15, 2024
558e0ed
Merge branch 'master' into 6471-hint-blackbox
aakoshh Nov 18, 2024
6b1af59
Merge branch 'master' into 6471-hint-blackbox
aakoshh Nov 18, 2024
bf223f1
Merge remote-tracking branch 'origin/master' into 6471-hint-blackbox
aakoshh Nov 18, 2024
31f3db7
Merge branch '6471-hint-blackbox' of github.com:noir-lang/noir into 6…
aakoshh Nov 18, 2024
cc3a40d
Merge remote-tracking branch 'origin/master' into 6471-hint-blackbox
aakoshh Nov 22, 2024
039bc5a
Merge remote-tracking branch 'origin/master' into 6471-hint-blackbox
aakoshh Nov 25, 2024
c28187b
Merge branch 'master' into 6471-hint-blackbox
aakoshh Nov 26, 2024
af6e2a6
Merge branch 'master' into 6471-hint-blackbox
aakoshh Nov 28, 2024
9eae4bd
Merge remote-tracking branch 'origin/master' into 6471-hint-blackbox
aakoshh Dec 2, 2024
292d08d
Merge remote-tracking branch 'origin/master' into 6471-hint-blackbox
aakoshh Dec 3, 2024
934d4b9
Merge remote-tracking branch 'origin/master' into 6471-hint-blackbox
aakoshh Dec 5, 2024
3d02519
Update test with ssa_logging
aakoshh Dec 5, 2024
ce2b4d1
Merge branch 'master' into 6471-hint-blackbox
aakoshh Dec 6, 2024
c848c93
Merge branch 'master' into 6471-hint-blackbox
aakoshh Dec 9, 2024
ee6dc06
Merge branch 'master' into 6471-hint-blackbox
aakoshh Dec 9, 2024
7def9d6
Merge branch 'master' into 6471-hint-blackbox
aakoshh Dec 9, 2024
5513a0c
Merge branch 'master' into 6471-hint-blackbox
TomAFrench Dec 9, 2024
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