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

Support for generics #223

Closed
7 tasks done
jawoznia opened this issue Sep 18, 2023 · 2 comments
Closed
7 tasks done

Support for generics #223

jawoznia opened this issue Sep 18, 2023 · 2 comments
Assignees

Comments

@jawoznia
Copy link
Collaborator

jawoznia commented Sep 18, 2023

With the support for the CustomQuery and CustomMsg we can define chain specific Contracts and Interfaces.
We should also add support for generics to allow chain generic Contracts and Interfaces to work with custom queries and messages.

Expand current generic support for interface macro

Interface macro should parse bounds defined on trait and generate messages with them. We should keep current CheckGenerics functionality to filter generics used for execute and query messages for API clarity.
Also generics are currently not used in case of features like Querier and should be propagated everywhere needed.

#[interface]
pub trait MyInterface<ExecParam, QueryParam>
where
    ExecParam: CustomMsg,
    QueryParam: CustomMsg,
{
    type Error: From<StdError>;

    #[msg(exec)]
    fn exec(&self, ctx: ExecCtx, param: ExecParam) -> StdResult<Response>;

    #[msg(query)]
    fn query(&self, ctx: QueryCtx, param: QueryParam) -> StdResult<String>;
}

Generic return type

It might be that in current shape cosmwasm_schema::QueryRespones returns attribute does not support generic return type. It has to be checked and if needed implemented in cosmwasm-schema.
Otherwise it should be an easy task. Parsing return type in CheckGenerics should do the trick.

Because generic param in message structure will be used only in #[returns] attribute it will fail to compile with generic unused error. Most likely we will have to generate phantom variant for that, possibly with std::convert::Infallible.

pub enum ExecMsg<ExecParam> {
    _Phantom(std::marker::PhantomData<ExecParam>),
    Exec { },
}

Unfortunately this approach will pollute schema.

"execute": {
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "ExecuteMsg",
  "oneOf": [
    {
      "type": "object",
      "required": [
        "__phantom"
      ],
      "properties": {
        "__phantom": {
          "type": "object",
          "required": [
            "param"
          ],
          "properties": {
            "param": {
              "type": "string"
            }
          },
          "additionalProperties": false
        }
      },
      "additionalProperties": false
    },
    {
      "type": "object",
      "required": [
        "exec"
      ],
      "properties": {
        "exec": {
          "type": "object",
          "additionalProperties": false
        }
      },
      "additionalProperties": false
    }
  ]
},

Create trait to track message types of interface

While implementing Interface on Contract there is a need to distinguish exec and query generics. It would mean that our API would look like this:

#[contract]
#[messages(my::module: exec(Msg: CustomMsg + DeserializedOwned), query(Param: CustomMsg)]
impl MyContract {}

It would be an inconvinience for the user to track which generic params are used with which message. Especially once new messages would be introduced to the Interface there would be a need to reevaluate which generic params should be added to which messages.

To avoid this issue we will introduce sylvia::Interface trait which will hold interface message types.

pub trait Interface {
    type Exec;
    type Query;
}

interface macro will then generate struct generic over params used by the user and implement this Interface trait on it.
It should look something like this:

pub struct Interface<ExecParam, QueryParam> {
    _phantom: std::marker::PhantomData<(ExecParam, QueryParam)>,
}

impl<ExecParam, QueryParam> crate::sylvia::Interface for Interface<ExecParam, QueryParam> {
    type Exec = ExecMsg<ExecParam>;
    type Query = QueryMsg<QueryParam>;
}

pub trait Interface<ExecParam, QueryParam> {
    type ExecMsg<ExecParam>;
    type QueryMsg<QueryParam>;
}

Implement generic interface on contract

Now that we have support for generic interface and also Interface helper type generated we can allow user to implement it on the Contract. Thanks to Interface helper type we don't have to use the current implementation and we will just require user to provide all generic types in messages attribute.

#[contract]
#[messages(iface<String, u32> as MyInterface)]
impl MyContract { }

In case of implementation Interface on Contract we should be able to avoid adding generic types in messages attribute as we will be able to extract them from impl block structure.

#[contract]
#[messages(iface as MyInterface)]
impl MyInterface<String, u32> for MyContract {}

Implement multiple generic interfaces on contract

User might want to implement multiple Interfaces on the Contract. In such case generic types will also be provided via messages attribute.

#[contract]
#[messages(iface<String, u32> as MyInterface)]
#[messages(another<MyMsg> as Another)]
impl MyContract { }

Behavior should not change here and forwarding generic attributes from messages should work the same as in case of a single generic interface.
The only issue might appear in ContractExecMsg and ContractQueryMsg generation. Here proper test has to show that generics are properly concatenated

Support generics on Contracts

User might want to define the Contract to store some generic data. We should support this case.
We should use CheckGenerics functionality here to block generics from being forwarded to generated messages and use these generics only when reffering to Contract in generated code.

pub struct MyContract<UserData> {
    pub user_data: UserData,
}

#[contract]
impl<UserData> MyContract<UserData> { }

User might also want to generate generic contract messages.

pub struct MyContract<ExecParam> where ExecParam: CustomMsg {
    pub _phantom: PhantomData<ExecParam>,
}

#[contract]
impl<ExecParam> MyContract<ExecParam> where ExecParam: CustomMsg {
    #[msg(query)]
    fn query(&self, ctx: QueryCtx) -> Result<> {}
    #[msg(exec)]
    fn execute(&self, ctx: ExecCtx, param: ExecParam) -> Result<> {}
}

// Generated messages
pub enum QueryMsg {
    Query {},
}

pub enum ExecMsg<ExecParam> {
    Execute {param: ExecParam},
}

impl<ExecParam> ExecMsg<ExecParam> where ExecParam: CustomMsg {}

Implement non-generic Interface on generic Contract

Below example should not require any additional work. ContractQueryMsg<T> should be able to "dispatch message further" to the Interface.

pub struct MyContract<T> {}

#[contract]
#[messages(iface as MyInterface)]
impl<T> MyContract<T> { 
    #[msg(query)]
    fn query(&self, ctx: QueryCtx, param: T) -> Result<> {}
}

#[contract]
#[messages(iface as MyInterface)]
impl<T> MyInterface for MyContract<T> {
    #[msg(query)]
    fn ifce_query(&self, ctx: QueryCtx) -> Result<> {}
}

Entry points

entry_points cannot have generic types so we have to generate them with solid types.
It's required only by entry_points so we can it as param to this macro. This types refer to generics defined on Contract not Interface.

#[entry_points<String, u32>]
#[contract]
impl<Msg1, Msg2> MyContract<Msg1, Msg2> {
}

Basing on that macro should generate something like this:

pub fn execute(
    deps: sylvia::cw_std::DepsMut<sylvia::cw_std::Empty>,
    env: sylvia::cw_std::Env,
    info: sylvia::cw_std::MessageInfo,
    msg: ContractExecMsg,
) -> Result<sylvia::cw_std::Response<sylvia::cw_std::Empty>, sylvia::cw_std::StdError> {
    msg.dispatch(&Contract::<String, u32>::new(), (deps, env, info))
        .map_err(Into::into)
}

Update docs

Describe usage of generics in sylvia-book and README.

Tasks split

@jawoznia jawoznia self-assigned this Sep 18, 2023
@hashedone
Copy link
Collaborator

Could you add example usages of the new messages form?

In general, please provide examples of how you expect API to look like when:

  • The contract type is generic. Note that you need to provide generic types to be used for the contract in entry points - there is a question if you provide them as part of #[contract] or #[entry_points]
  • The interface is generic but is implemented on non-generic contract
  • The interface is generic and is implemented on a generic contract

Also, examples of using generics as the custom msgs/queries should be here. For now, the #[messages(my::module: exec(Msg: msg + bonds), query(Param: query + bonds)] looks very suspicious to me - anything that is generic should look like generics, what is type bound should most likely look like type bounds. However, I cannot be sure unless I see the proposed usage.

Before starting the implementation, we should have confirmed the API we want to achieve.

@hashedone
Copy link
Collaborator

One comment about entry points - they cannot be generic at the end of the day. There has to be concrete monomoprhisation to be called in the entry points. I would see #[entry_points<Generic, Arguments>] form for generic contracts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants