-
Notifications
You must be signed in to change notification settings - Fork 200
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Add intrinsic to get if running inside an unconstrained context (…
…#5098) # Description ## Problem\* Currently, when creating a regular (not marked with unconstrained) function, it can end up running in both a constrained and unconstrained context. ## Summary\* This intrinsic is resolved at compile time a provides a way for developers to dispatch to different implementations in library code for constrained and unconstrained context. This intrinsic has been used in stdlib's field comparison code as demonstration. ## Additional Context It's very typical for the constrained version of a calculation to take a hint of a computed value and verify it. However, in the unconstrained version of that calculation we just need to compute the value, there is no point in verifying what we just did. This intrinsic provides a tool for devs to optimize a function for unconstrained removing these verifications. ## Documentation\* Check one: - [ ] No documentation needed. - [x] Documentation included in this PR. - [ ] **[For Experimental Features]** Documentation to be submitted in a separate PR. # PR Checklist\* - [x] I have tested the changes locally. - [x] I have formatted the changes with [Prettier](https://prettier.io/) and/or `cargo fmt` on default settings. --------- Co-authored-by: Tom French <15848336+TomAFrench@users.noreply.github.com>
- Loading branch information
1 parent
86fd0ac
commit 281ebf2
Showing
15 changed files
with
263 additions
and
76 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
56 changes: 56 additions & 0 deletions
56
compiler/noirc_evaluator/src/ssa/opt/resolve_is_unconstrained.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
use crate::ssa::{ | ||
ir::{ | ||
function::{Function, RuntimeType}, | ||
instruction::{Instruction, Intrinsic}, | ||
types::Type, | ||
value::Value, | ||
}, | ||
ssa_gen::Ssa, | ||
}; | ||
use acvm::FieldElement; | ||
use fxhash::FxHashSet as HashSet; | ||
|
||
impl Ssa { | ||
/// An SSA pass to find any calls to `Intrinsic::IsUnconstrained` and replacing any uses of the result of the intrinsic | ||
/// with the resolved boolean value. | ||
/// Note that this pass must run after the pass that does runtime separation, since in SSA generation an ACIR function can end up targeting brillig. | ||
#[tracing::instrument(level = "trace", skip(self))] | ||
pub(crate) fn resolve_is_unconstrained(mut self) -> Self { | ||
for func in self.functions.values_mut() { | ||
replace_is_unconstrained_result(func); | ||
} | ||
self | ||
} | ||
} | ||
|
||
fn replace_is_unconstrained_result(func: &mut Function) { | ||
let mut is_unconstrained_calls = HashSet::default(); | ||
// Collect all calls to is_unconstrained | ||
for block_id in func.reachable_blocks() { | ||
for &instruction_id in func.dfg[block_id].instructions() { | ||
let target_func = match &func.dfg[instruction_id] { | ||
Instruction::Call { func, .. } => *func, | ||
_ => continue, | ||
}; | ||
|
||
if let Value::Intrinsic(Intrinsic::IsUnconstrained) = &func.dfg[target_func] { | ||
is_unconstrained_calls.insert(instruction_id); | ||
} | ||
} | ||
} | ||
|
||
for instruction_id in is_unconstrained_calls { | ||
let call_returns = func.dfg.instruction_results(instruction_id); | ||
let original_return_id = call_returns[0]; | ||
|
||
// We replace the result with a fresh id. This will be unused, so the DIE pass will remove the leftover intrinsic call. | ||
func.dfg.replace_result(instruction_id, original_return_id); | ||
|
||
let is_within_unconstrained = func.dfg.make_constant( | ||
FieldElement::from(matches!(func.runtime(), RuntimeType::Brillig)), | ||
Type::bool(), | ||
); | ||
// Replace all uses of the original return value with the constant | ||
func.dfg.set_value_from_id(original_return_id, is_within_unconstrained); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
--- | ||
title: Is Unconstrained Function | ||
description: | ||
The is_unconstrained function returns wether the context at that point of the program is unconstrained or not. | ||
keywords: | ||
[ | ||
unconstrained | ||
] | ||
--- | ||
|
||
It's very common for functions in circuits to take unconstrained hints of an expensive computation and then verify it. This is done by running the hint in an unconstrained context and then verifying the result in a constrained context. | ||
|
||
When a function is marked as unconstrained, any subsequent functions that it calls will also be run in an unconstrained context. However, if we are implementing a library function, other users might call it within an unconstrained context or a constrained one. Generally, in an unconstrained context we prefer just computing the result instead of taking a hint of it and verifying it, since that'd mean doing the same computation twice: | ||
|
||
```rust | ||
|
||
fn my_expensive_computation(){ | ||
... | ||
} | ||
|
||
unconstrained fn my_expensive_computation_hint(){ | ||
my_expensive_computation() | ||
} | ||
|
||
pub fn external_interface(){ | ||
my_expensive_computation_hint(); | ||
// verify my_expensive_computation: If external_interface is called from unconstrained, this is redundant | ||
... | ||
} | ||
|
||
``` | ||
|
||
In order to improve the performance in an unconstrained context you can use the function at `std::runtime::is_unconstrained() -> bool`: | ||
|
||
|
||
```rust | ||
use dep::std::runtime::is_unconstrained; | ||
|
||
fn my_expensive_computation(){ | ||
... | ||
} | ||
|
||
unconstrained fn my_expensive_computation_hint(){ | ||
my_expensive_computation() | ||
} | ||
|
||
pub fn external_interface(){ | ||
if is_unconstrained() { | ||
my_expensive_computation(); | ||
} else { | ||
my_expensive_computation_hint(); | ||
// verify my_expensive_computation | ||
... | ||
} | ||
} | ||
|
||
``` | ||
|
||
The is_unconstrained result is resolved at compile time, so in unconstrained contexts the compiler removes the else branch, and in constrained contexts the compiler removes the if branch, reducing the amount of compute necessary to run external_interface. |
Oops, something went wrong.