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

Chain Extension: Evaluation of method return type at compile time #1569

Merged
merged 13 commits into from
Jan 24, 2023
15 changes: 9 additions & 6 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Add E2E tests for `Mapping` functions - [#1492](https://github.com/paritytech/ink/pull/1492)
- Make CallBuilder and CreateBuilder error handling optional - [#1602](https://github.com/paritytech/ink/pull/1602)
- Rename `CallBuilder::fire()` method to `invoke()` - [#1604](https://github.com/paritytech/ink/pull/1604)
- Chain Extension: Evaluation of method return type at compile time - [#1569](https://github.com/paritytech/ink/pull/1569).

### Breaking Changes
With this release there are two breaking changes related to the `CallBuilder` and
`CreateBuilder`.
With this release there are three breaking changes related to the `CallBuilder`
`CreateBuilder` and the Chain Extension API.

1. The `invoke()` methods now unwrap the `Result` from `pallet-contracts` under the hood
([#1602](https://github.com/paritytech/ink/pull/1602))
([#1602](https://github.com/paritytech/ink/pull/1602)).
If you wish to handle the error use the new `try_` variants of those methods instead.
1. The `CallBuilder::fire()` method has been renamed to `invoke()`
([#1604](https://github.com/paritytech/ink/pull/1604))

For (1), if you which to handle the the error use the new `try_` variants of those
methods instead.
1. The `returns_result` flag has been removed from the `#[ink(extension = …)]` attribute
([#1569](https://github.com/paritytech/ink/pull/1569)).
We now infer this information at compile time. If `handle_status` is set to `true`,
the return type will still be wrapped into `Result` as before.

## Version 4.0.0-beta

Expand Down
157 changes: 91 additions & 66 deletions crates/env/src/chain_extension.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,30 +58,29 @@ pub trait FromStatusCode: Sized {
/// All tuple types that may act as input parameters for the chain extension method are valid.
/// Examples include `()`, `i32`, `(u8, [u8; 5], i32)`, etc.
/// - `O` represents the return (or output) type of the chain extension method.
/// Only `Result<T, E>` or `NoResult<O>` generic types are allowed for `O`.
/// The `Result<T, E>` type says that the chain extension method returns a `Result` type
/// whereas the `NoResult<O>` type says that the chain extension method returns a non-`Result` value
/// of type `O`.
/// - `ErrorCode` represents how the chain extension method handles the chain extension's error code.
/// Only `HandleErrorCode<E>` and `IgnoreErrorCode` types are allowed that each say to either properly
/// handle or ignore the chain extension's error code respectively.
/// - `const IS_RESULT: bool` indicates if the `O` (output type) is of `Result<T, E>` type.
///
/// The type states for type parameter `O` and `ErrorCode` represent 4 different states:
///
/// 1. The chain extension method makes use of the chain extension's error code: `HandleErrorCode(E)`
/// - **A:** The chain extension method returns a `Result<T, E>` type.
/// - **B:** The chain extension method returns a type `T` that is not a `Result` type: `NoResult<T>`
/// - **A:** The chain extension method returns a `Result<T, E>` type, i.e. `IS_RESULT` is set to `true`.
/// - **B:** The chain extension method returns a type `O` that is not a `Result` type.
/// The return type is still wrapped into `Result<O, E>`
/// 2. The chain extension ignores the chain extension's error code: `IgnoreErrorCode`
/// - **A:** The chain extension method returns a `Result<T, E>` type.
/// - **B:** The chain extension method returns a type `T` that is not a `Result` type: `NoResult<T>`
/// - **A:** The chain extension method returns a `Result<T, E>` type, i.e. `IS_RESULT` is set to `true`.
/// - **B:** The chain extension method returns a type `O` that is not a `Result` type.
/// The method just returns `O`.
#[derive(Debug)]
pub struct ChainExtensionMethod<I, O, ErrorCode> {
pub struct ChainExtensionMethod<I, O, ErrorCode, const IS_RESULT: bool> {
func_id: u32,
#[allow(clippy::type_complexity)]
state: PhantomData<fn() -> (I, O, ErrorCode)>,
}

impl ChainExtensionMethod<(), (), ()> {
impl ChainExtensionMethod<(), (), (), false> {
/// Creates a new chain extension method instance.
#[inline]
pub fn build(func_id: u32) -> Self {
Expand All @@ -92,7 +91,9 @@ impl ChainExtensionMethod<(), (), ()> {
}
}

impl<O, ErrorCode> ChainExtensionMethod<(), O, ErrorCode> {
impl<O, ErrorCode, const IS_RESULT: bool>
ChainExtensionMethod<(), O, ErrorCode, IS_RESULT>
{
/// Sets the input types of the chain extension method call to `I`.
///
/// # Note
Expand All @@ -101,7 +102,7 @@ impl<O, ErrorCode> ChainExtensionMethod<(), O, ErrorCode> {
/// All tuple types that may act as input parameters for the chain extension method are valid.
/// Examples include `()`, `i32`, `(u8, [u8; 5], i32)`, etc.
#[inline]
pub fn input<I>(self) -> ChainExtensionMethod<I, O, ErrorCode>
pub fn input<I>(self) -> ChainExtensionMethod<I, O, ErrorCode, IS_RESULT>
where
I: scale::Encode,
{
Expand All @@ -112,34 +113,20 @@ impl<O, ErrorCode> ChainExtensionMethod<(), O, ErrorCode> {
}
}

impl<I, ErrorCode> ChainExtensionMethod<I, (), ErrorCode> {
/// Sets the output type of the chain extension method call to `Result<T, E>`.
impl<I, ErrorCode> ChainExtensionMethod<I, (), ErrorCode, false> {
/// Sets the output type, `O`, of the chain extension method call.
///
/// # Note
///
/// This indicates that the chain extension method return value might represent a failure.
#[inline]
pub fn output_result<T, E>(self) -> ChainExtensionMethod<I, Result<T, E>, ErrorCode>
where
Result<T, E>: scale::Decode,
E: From<scale::Error>,
{
ChainExtensionMethod {
func_id: self.func_id,
state: Default::default(),
}
}

/// Sets the output type of the chain extension method call to `O`.
/// If `const IS_RESULT: bool` is set to `true`,
/// `O` is treated as `Result<T, E>`
///
/// # Note
///
/// The set returned type `O` must not be of type `Result<T, E>`.
/// When using the `#[ink::chain_extension]` procedural macro to define
/// this chain extension method the above constraint is enforced at
/// compile time.
/// If `O` is incorrectly indicated as `Return<T, E>`,
/// the type will not satisfy trait bounds later in method builder pipeline.
#[inline]
pub fn output<O>(self) -> ChainExtensionMethod<I, state::NoResult<O>, ErrorCode>
pub fn output<O, const IS_RESULT: bool>(
self,
) -> ChainExtensionMethod<I, O, ErrorCode, IS_RESULT>
where
O: scale::Decode,
{
Expand All @@ -150,7 +137,7 @@ impl<I, ErrorCode> ChainExtensionMethod<I, (), ErrorCode> {
}
}

impl<I, O> ChainExtensionMethod<I, O, ()> {
impl<I, O, const IS_RESULT: bool> ChainExtensionMethod<I, O, (), IS_RESULT> {
/// Makes the chain extension method call assume that the returned status code is always success.
///
/// # Note
Expand All @@ -161,7 +148,9 @@ impl<I, O> ChainExtensionMethod<I, O, ()> {
///
/// The output of the chain extension method call is always decoded and returned in this case.
#[inline]
pub fn ignore_error_code(self) -> ChainExtensionMethod<I, O, state::IgnoreErrorCode> {
pub fn ignore_error_code(
self,
) -> ChainExtensionMethod<I, O, state::IgnoreErrorCode, IS_RESULT> {
ChainExtensionMethod {
func_id: self.func_id,
state: Default::default(),
Expand All @@ -177,7 +166,7 @@ impl<I, O> ChainExtensionMethod<I, O, ()> {
#[inline]
pub fn handle_error_code<ErrorCode>(
self,
) -> ChainExtensionMethod<I, O, state::HandleErrorCode<ErrorCode>>
) -> ChainExtensionMethod<I, O, state::HandleErrorCode<ErrorCode>, IS_RESULT>
where
ErrorCode: FromStatusCode,
{
Expand All @@ -201,22 +190,14 @@ pub mod state {
pub struct HandleErrorCode<T> {
error_code: PhantomData<fn() -> T>,
}

/// Type state meaning that the chain extension method deliberately does not return a `Result` type.
///
/// Additionally this is enforced by the `#[ink::chain_extension]` procedural macro when used.
#[derive(Debug)]
pub struct NoResult<T> {
no_result: PhantomData<fn() -> T>,
}
}

impl<I, T, E, ErrorCode>
ChainExtensionMethod<I, Result<T, E>, state::HandleErrorCode<ErrorCode>>
impl<I, O, ErrorCode> ChainExtensionMethod<I, O, state::HandleErrorCode<ErrorCode>, true>
where
O: IsResultType,
I: scale::Encode,
T: scale::Decode,
E: scale::Decode + From<ErrorCode> + From<scale::Error>,
<O as IsResultType>::Ok: scale::Decode,
<O as IsResultType>::Err: scale::Decode + From<ErrorCode> + From<scale::Error>,
ErrorCode: FromStatusCode,
{
/// Calls the chain extension method for case 1.A described [here].
Expand All @@ -235,6 +216,7 @@ where
///
/// Declares a chain extension method with the unique ID of 5 that requires a `bool` and an `i32`
/// as input parameters and returns a `Result<i32, MyError>` upon completion.
/// Note how we set const constant argument to `true` to indicate that return type is `Result<T, E>`.
/// It will handle the shared error code from the chain extension.
/// The call is finally invoked with arguments `true` and `42` for the `bool` and `i32` input
/// parameter respectively.
Expand All @@ -245,7 +227,7 @@ where
/// # use ink_env::chain_extension::{ChainExtensionMethod, FromStatusCode};
/// let result = ChainExtensionMethod::build(5)
/// .input::<(bool, i32)>()
/// .output_result::<i32, MyError>()
/// .output::<Result<i32, MyError>, true>()
/// .handle_error_code::<MyErrorCode>()
/// .call(&(true, 42));
/// # #[derive(scale::Encode, scale::Decode)]
Expand All @@ -262,9 +244,19 @@ where
/// # }
/// ```
#[inline]
pub fn call(self, input: &I) -> Result<T, E> {
pub fn call(
self,
input: &I,
) -> Result<<O as IsResultType>::Ok, <O as IsResultType>::Err> {
<EnvInstance as OnInstance>::on_instance(|instance| {
EnvBackend::call_chain_extension::<I, T, E, ErrorCode, _, _>(
EnvBackend::call_chain_extension::<
I,
<O as IsResultType>::Ok,
<O as IsResultType>::Err,
ErrorCode,
_,
_,
>(
instance,
self.func_id,
input,
Expand All @@ -275,11 +267,12 @@ where
}
}

impl<I, T, E> ChainExtensionMethod<I, Result<T, E>, state::IgnoreErrorCode>
impl<I, O> ChainExtensionMethod<I, O, state::IgnoreErrorCode, true>
where
O: IsResultType,
I: scale::Encode,
T: scale::Decode,
E: scale::Decode + From<scale::Error>,
<O as IsResultType>::Ok: scale::Decode,
<O as IsResultType>::Err: scale::Decode + From<scale::Error>,
{
/// Calls the chain extension method for case 2.A described [here].
///
Expand All @@ -296,6 +289,7 @@ where
///
/// Declares a chain extension method with the unique ID of 5 that requires a `bool` and an `i32`
/// as input parameters and returns a `Result<i32, MyError>` upon completion.
/// Note how we set const constant argument to `true` to indicate that return type is `Result<T, E>`.
/// It will ignore the shared error code from the chain extension and assumes that the call succeeds.
/// The call is finally invoked with arguments `true` and `42` for the `bool` and `i32` input
/// parameter respectively.
Expand All @@ -306,7 +300,7 @@ where
/// # use ink_env::chain_extension::{ChainExtensionMethod};
/// let result = ChainExtensionMethod::build(5)
/// .input::<(bool, i32)>()
/// .output_result::<i32, MyError>()
/// .output::<Result<i32, MyError>, true>()
/// .ignore_error_code()
/// .call(&(true, 42));
/// # #[derive(scale::Encode, scale::Decode)]
Expand All @@ -316,9 +310,19 @@ where
/// # }
/// ```
#[inline]
pub fn call(self, input: &I) -> Result<T, E> {
pub fn call(
self,
input: &I,
) -> Result<<O as IsResultType>::Ok, <O as IsResultType>::Err> {
<EnvInstance as OnInstance>::on_instance(|instance| {
EnvBackend::call_chain_extension::<I, T, E, E, _, _>(
EnvBackend::call_chain_extension::<
I,
<O as IsResultType>::Ok,
<O as IsResultType>::Err,
<O as IsResultType>::Err,
_,
_,
>(
instance,
self.func_id,
input,
Expand All @@ -329,8 +333,7 @@ where
}
}

impl<I, O, ErrorCode>
ChainExtensionMethod<I, state::NoResult<O>, state::HandleErrorCode<ErrorCode>>
impl<I, O, ErrorCode> ChainExtensionMethod<I, O, state::HandleErrorCode<ErrorCode>, false>
where
I: scale::Encode,
O: scale::Decode,
Expand All @@ -353,7 +356,10 @@ where
/// # Example
///
/// Declares a chain extension method with the unique ID of 5 that requires a `bool` and an `i32`
/// as input parameters and returns a `Result<i32, MyErrorCode>` upon completion.
/// as input parameters and returns a `Result<i32, MyErrorCode>` upon completion,
/// because `handle_status` flag is set.
/// We still need to indicate that the original type is not `Result<T, E>`, so
/// `const IS_RESULT` is set `false`.
/// It will handle the shared error code from the chain extension.
/// The call is finally invoked with arguments `true` and `42` for the `bool` and `i32` input
/// parameter respectively.
Expand All @@ -364,7 +370,7 @@ where
/// # use ink_env::chain_extension::{ChainExtensionMethod, FromStatusCode};
/// let result = ChainExtensionMethod::build(5)
/// .input::<(bool, i32)>()
/// .output::<i32>()
/// .output::<i32, false>()
/// .handle_error_code::<MyErrorCode>()
/// .call(&(true, 42));
/// # pub struct MyErrorCode {}
Expand All @@ -390,7 +396,7 @@ where
}
}

impl<I, O> ChainExtensionMethod<I, state::NoResult<O>, state::IgnoreErrorCode>
impl<I, O> ChainExtensionMethod<I, O, state::IgnoreErrorCode, false>
where
I: scale::Encode,
O: scale::Decode,
Expand All @@ -406,7 +412,7 @@ where
/// # Example
///
/// Declares a chain extension method with the unique ID of 5 that requires a `bool` and an `i32`
/// as input parameters and returns a `Result<i32, MyErrorCode>` upon completion.
/// as input parameters and returns a `i32` upon completion. Hence, `const IS_RESULT` is set `false`.
/// It will ignore the shared error code from the chain extension and assumes that the call succeeds.
/// The call is finally invoked with arguments `true` and `42` for the `bool` and `i32` input
/// parameter respectively.
Expand All @@ -417,7 +423,7 @@ where
/// # use ink_env::chain_extension::ChainExtensionMethod;
/// let result = ChainExtensionMethod::build(5)
/// .input::<(bool, i32)>()
/// .output::<i32>()
/// .output::<i32, false>()
/// .ignore_error_code()
/// .call(&(true, 42));
/// ```
Expand All @@ -438,3 +444,22 @@ where
})
}
}

/// Extract `Ok` and `Err` variants from `Result` type.
pub trait IsResultType: private::IsResultTypeSealed {
/// The `T` type of the `Result<T, E>`.
type Ok;
/// The `E` type of the `Result<T, E>`.
type Err;
}

impl<T, E> private::IsResultTypeSealed for Result<T, E> {}
impl<T, E> IsResultType for Result<T, E> {
type Ok = T;
type Err = E;
}

mod private {
/// Seals the `IsResultType` trait so that it cannot be implemented outside this module.
pub trait IsResultTypeSealed {}
}
Loading