Skip to content

Commit

Permalink
Chain Extension: Evaluation of method return type at compile time (#1569
Browse files Browse the repository at this point in the history
)

* remove returns_result attr

* generic return type based on const eval

* working concept of const eval of result type

* cargo fmt

* code cleanup and docs

* fix typos

* uncomment other UI tests

* clarify comment and fix typos

* provide correct args to generics in doctest

* Add entry to CHANGELOG

* Fix punctuation

Co-authored-by: Michael Mueller <[email protected]>
  • Loading branch information
German and cmichi committed Jan 24, 2023
1 parent 8321e6f commit 178a52f
Show file tree
Hide file tree
Showing 10 changed files with 186 additions and 244 deletions.
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

0 comments on commit 178a52f

Please sign in to comment.