-
Notifications
You must be signed in to change notification settings - Fork 200
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
Unify Constrain Errors #4239
Comments
Dynamic assert payloadsCurrent stateConsider the following code containing a "dynamic" assert payload: fn option_expect<T, Err>(opt: Option<T>, err: Err) -> T {
assert(opt.is_some(), err);
opt.unwrap_unchecked()
}
fn main(option: Option<u8>) -> pub u8 {
option_expect(option, "option is none")
} vs the static assert payload version of the code: fn main(option: Option<u8>) -> pub u8 {
assert(option.is_some(), "option is none");
option.unwrap_unchecked()
} This is the generated SSA for the static version:
And this one for the "dynamic" version:
The dynamic version has undergone some modifications before conversion to SSA. Let's explain what are those: Modifications at HIR if matches!(
assert_message_expr,
Expression { kind: ExpressionKind::Literal(Literal::Str(..)), .. }
) {
return Some(self.resolve_expression(assert_message_expr));
}
let is_in_stdlib = self.path_resolver.module_id().krate.is_stdlib();
let assert_msg_call_path = if is_in_stdlib {
ExpressionKind::Variable(Path {
segments: vec![Ident::from("internal"), Ident::from("resolve_assert_message")],
kind: PathKind::Crate,
span,
})
} else {
ExpressionKind::Variable(Path {
segments: vec![
Ident::from("std"),
Ident::from("internal"),
Ident::from("resolve_assert_message"),
],
kind: PathKind::Dep,
span,
})
};
let assert_msg_call_args = vec![assert_message_expr.clone(), condition];
let assert_msg_call_expr = Expression::call(
Expression { kind: assert_msg_call_path, span },
assert_msg_call_args,
span,
);
Some(self.resolve_expression(assert_msg_call_expr)) At the HIR level, if the assert message is not a literal string, a call to #[oracle(assert_message)]
unconstrained fn assert_message_oracle<T>(_input: T) {}
unconstrained pub fn resolve_assert_message<T>(input: T, condition: bool) {
if !condition {
assert_message_oracle(input);
}
} This explains this added function in the SSA:
Modifications at monomorphizationAfter monomorphization, the assert_message oracle call is modified with metadata about the type it's being called with, in a similar way as the print opcode. if let ast::Expression::Ident(ident) = original_func.as_ref() {
if let Definition::Oracle(name) = &ident.definition {
if name.as_str() == "print" {
// Oracle calls are required to be wrapped in an unconstrained function
// The first argument to the `print` oracle is a bool, indicating a newline to be inserted at the end of the input
// The second argument is expected to always be an ident
self.append_printable_type_info(&hir_arguments[1], &mut arguments);
} else if name.as_str() == "assert_message" {
// The first argument to the `assert_message` oracle is the expression passed as a message to an `assert` or `assert_eq` statement
self.append_printable_type_info(&hir_arguments[0], &mut arguments);
}
}
} The metadata is the serialized ABI of the type: fn append_printable_type_info_inner(typ: &Type, arguments: &mut Vec<ast::Expression>) {
// Disallow printing slices and mutable references for consistency,
// since they cannot be passed from ACIR into Brillig
if matches!(typ, HirType::MutableReference(_)) {
unreachable!("println and format strings do not support mutable references.");
}
let printable_type: PrintableType = typ.into();
let abi_as_string = serde_json::to_string(&printable_type)
.expect("ICE: expected PrintableType to serialize");
arguments.push(ast::Expression::Literal(ast::Literal::Str(abi_as_string)));
} This explains the call to v3 (assert_message) in SSA:
As can be seen here, the oracle call to assert_message includes not only the string v0 but also a literal byte array, that being the abi_as_string. Compilation and runtimeThis strategy will make it so when evaluating this instruction But how is this assert_message oracle related with the original assertion? Comparison with static assert messagesWith static assert messages, no transformations happen on the frontend. The generated SSA
gets compiled to ACIR and in compilation to ACIR, the literal string is stored in a Map<OpcodeLocation, String> inside the circuit. So when the circuit fails, the runtime just picks what String is stored there for the failing opcode. If the function is brillig, instead of being added to the Map<OpcodeLocation, String> the error string is directly returned as revert data. Challenges with current stateThis approach, altough a bit fragile (since modifications in SSA assertions need to account for dynamic assertions, or bugs like this one can happen) works fine for circuits. But it poses some challenges for public functions in aztec:
Alternative approachInstead of using an entirely new approach for dynamic assertion messages, we can follow the same approach that we have for static assertion messages.
So the example code: fn option_expect<T, Err>(opt: Option<T>, err: Err) -> T {
assert(opt.is_some(), err);
opt.unwrap_unchecked()
}
fn main(option: Option<u8>) -> pub u8 {
option_expect(option, "option is none")
} would generate the following SSA:
ACIR generation and Brillig generation need access to the PrintableType metadata that is generated from the HIR for all non-string assert payloads. It could be maintained in parallel or directly embedded in the constrain instruction in SSA, altough the latter sounds worse. This is necessary since we need to build the When an opcode fails, the ACVM immediately has enough data to:
Advantages of this approach
Updates to the proposal:No metadata in the circuit structWe can avoid storing metadata about assertion payload types in the circuit struct if we put it in the ABI. |
I think that's unnecessary now anyway now we have revert data in brillig so this is a constant between all proposals. This was just a hack in the short term. |
This PR implements this proposal: noir-lang/noir#4239 (comment) - Removes legacy dynamic assertions via calling brillig + oracle - Removes legacy oracle handling for assertions - Frontend adds the HirType in the ConstrainError instead of embedding metadata about it in the program logic. - Constrain instruction in SSA now has `Values` for payload - SSA gen generates an error selector from the payload type - ACIR gen and Brillig gen handle the payload ValueIds and error selector - ABI has now error_types for non-string errors - ACVM now resolves the payload for non-constant AssertionPayloads - Nargo decodes the error for non-string errors using the ABI, allowing use in nargo tests Things to do in a followup PR: - Add an entry point in noirc_abi_wasm that allows decoding a given `Raw` (non-string) using the ABI in the same way that nargo does.
Closing this after AztecProtocol/aztec-packages#5949, we can reopen an issue for the next iteration. |
This PR implements this proposal: noir-lang/noir#4239 (comment) - Removes legacy dynamic assertions via calling brillig + oracle - Removes legacy oracle handling for assertions - Frontend adds the HirType in the ConstrainError instead of embedding metadata about it in the program logic. - Constrain instruction in SSA now has `Values` for payload - SSA gen generates an error selector from the payload type - ACIR gen and Brillig gen handle the payload ValueIds and error selector - ABI has now error_types for non-string errors - ACVM now resolves the payload for non-constant AssertionPayloads - Nargo decodes the error for non-string errors using the ABI, allowing use in nargo tests Things to do in a followup PR: - Add an entry point in noirc_abi_wasm that allows decoding a given `Raw` (non-string) using the ABI in the same way that nargo does.
Problem
This TODO was added in #4101. This is due to having two different strategies for resolving constrain instruction error messages which can be seen in this enum (https://github.com/noir-lang/noir/blob/afcb385daa572f990178eda51faf10dc20acd2d0/compiler/noirc_evaluator/src/ssa/ir/instruction.rs#L5730). We have two different strategies as sometimes when doing SSA codegen we want to include a constrain with a message, but we can't easily codegen a call to
resolve_assert_message
in the same way we do for user provided assertion messages. Thus, we have thisassert_messages
map for resolving messages specified by the compiler and separate calls toresolve_assert_message
for handling assert messages specified by the user.Happy Case
We should unify the strategy for specifying errors and error types. Ideally we would remove the
assert_messages
map from theCircuit
type. We most likely could instead store errors and their respective types on the ABI rather than on the circuit. Upon circuit failure we can then use the ABI for fetching the appropriate error.Some example pseudocode written by @TomAFrench from when we were originally planning #4101.
Unifying these constrain errors would essentially require full structured errors as part of the ABI. We could then have followup work to enable custom errors inside Noir similar to https://soliditylang.org/blog/2021/04/21/custom-errors/.
Alternatives Considered
No response
Additional Context
No response
Would you like to submit a PR for this Issue?
No
Support Needs
No response
The text was updated successfully, but these errors were encountered: