Skip to content

Commit

Permalink
fix: refactor ibc-derive to handle context with generic types and p…
Browse files Browse the repository at this point in the history
…rojects dependent on `ibc-core` (#1037)

* fix: refactor ibc-derive to support broader cases

* fix: cargo doc

* imp: define get_impl_quote

* imp: move each set of derives to the related meta-crate

* imp: refactor ClientState drive to work with generics with trait-bounds

* fix: remove redundant derive feature

* docs: improve derive docstrings

* imp: define SupportedCrate enum
  • Loading branch information
Farhad-Shabani authored Jan 12, 2024
1 parent 10a5188 commit fcc8c57
Show file tree
Hide file tree
Showing 18 changed files with 404 additions and 156 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- [ibc-derive] Refactor `ClientState` macro derivation to handle contexts with
generic types. ([\#910](https://github.com/cosmos/ibc-rs/issues/910))
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- [ibc-derive] Adapt macro derivations to integrate with projects dependent on
`ibc-core` ([\#999](https://github.com/cosmos/ibc-rs/issues/999)).
6 changes: 2 additions & 4 deletions docs/architecture/adr-007-light-client-contexts.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,10 +193,8 @@ enum AnyConsensusState {
}

#[derive(ClientState)]
#[generics(
ClientValidationContext = MyClientValidationContext,
ClientExecutionContext = MyClientExecutionContext
)]
#[validation(MyClientValidationContext)]
#[execution(MyClientExecutionContext)]
enum AnyClientState {
Tendermint(TmClientState),
Near(NearClientState),
Expand Down
1 change: 1 addition & 0 deletions ibc-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ ibc-core-commitment-types = { workspace = true }
ibc-core-host = { workspace = true }
ibc-core-router = { workspace = true }
ibc-core-handler = { workspace = true }
ibc-derive = { workspace = true }
ibc-primitives = { workspace = true }

[features]
Expand Down
1 change: 0 additions & 1 deletion ibc-core/ics02-client/context/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ ibc-core-client-types = { workspace = true }
ibc-core-commitment-types = { workspace = true }
ibc-core-host-types = { workspace = true }
ibc-core-handler-types = { workspace = true }
ibc-derive = { workspace = true }
ibc-primitives = { workspace = true }

# cosmos dependencies
Expand Down
11 changes: 0 additions & 11 deletions ibc-core/ics02-client/context/src/client_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,17 +181,6 @@ where
) -> Result<Height, ClientError>;
}

/// Derive macro that implements [`ClientState`] for enums containing variants
/// that implement [`ClientState`].
///
/// The macro expects the attribute `#[generics(ClientValidationContext = <...>,
/// ClientExecutionContext = <...>)]` which specifies [`ClientState`]'s generic
/// arguments to be defined.
///
/// The macro does not support generic types for `ClientValidationContext` and
/// `ClientExecutionContext` (e.g. `MyType<T>` would not be supported).
pub use ibc_derive::ClientState;

use crate::context::{ClientExecutionContext, ClientValidationContext};

/// Primary client trait. Defines all the methods that clients must implement.
Expand Down
3 changes: 0 additions & 3 deletions ibc-core/ics02-client/context/src/consensus_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@
use core::marker::{Send, Sync};

use ibc_core_commitment_types::commitment::CommitmentRoot;
/// Derive macro that implements [`ConsensusState`] for enums containing
/// variants that implement [`ConsensusState`]
pub use ibc_derive::ConsensusState;
use ibc_primitives::prelude::*;
use ibc_primitives::Timestamp;

Expand Down
24 changes: 24 additions & 0 deletions ibc-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,27 @@ pub mod router {
#[doc(inline)]
pub use ibc_core_router::*;
}

/// Re-exports convenient derive macros from `ibc-derive` crate.
pub mod derive {
/// To specify the generic arguments for `ClientState`, use the following
/// attributes:
///
/// - `#[validation(<YourClientValidationContext>)]`
/// - `#[execution(<YourClientExecutionContext>)]`
///
/// The argument to the `validation` or `execution` attributes may contain
/// lifetimes or generic types and even that types might be bounded by
/// traits. For instance:
///
/// - `#[validation(Context<S>)]`
/// - `#[validation(Context<'a, S>)]`
/// - `#[validation(Context<'a, S: Clone>)]`
pub use ibc_derive::IbcCoreClientState as ClientState;
/// A derive macro for implementing the
/// [`ConsensusState`](crate::client::context::consensus_state::ConsensusState) trait for
/// enums. Enums with variants that also implement the
/// [`ConsensusState`](crate::client::context::consensus_state::ConsensusState) trait can
/// leverage this macro for automatic implementation.
pub use ibc_derive::IbcCoreConsensusState as ConsensusState;
}
1 change: 0 additions & 1 deletion ibc-derive/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,3 @@ proc-macro = true
syn = "2"
proc-macro2 = "1"
quote = "1"
darling = "0.20"
190 changes: 143 additions & 47 deletions ibc-derive/src/client_state.rs
Original file line number Diff line number Diff line change
@@ -1,29 +1,155 @@
mod traits;

use darling::FromDeriveInput;
use proc_macro2::TokenStream;
use quote::quote;
use syn::DeriveInput;
use quote::{quote, ToTokens};
use syn::{DeriveInput, Error, GenericArgument, Ident, WherePredicate};
use traits::client_state_common::impl_ClientStateCommon;
use traits::client_state_execution::impl_ClientStateExecution;
use traits::client_state_validation::impl_ClientStateValidation;

#[derive(FromDeriveInput)]
#[darling(attributes(generics))]
use crate::utils::Imports;

const MISSING_ATTR: &str = "must be annotated with #[validation(<your ClientValidationContext>) and #[execution(<your ClientExecutionContext>)]";
const MISSING_VALIDATION_ATTR: &str = "missing #[validation(<your ClientValidationContext>)]";
const MISSING_EXECUTION_ATTR: &str = "missing #[execution(<your ClientExecutionContext>)]";
const INVALID_ATTR: &str = "invalid attribute annotation";
const INVALID_ARGS: &str = "invalid context argument";

#[derive(Clone)]
pub(crate) struct ClientCtx {
ident: Ident,
generics: Vec<GenericArgument>,
predicates: Vec<WherePredicate>,
}

impl ClientCtx {
fn new(ident: Ident, generics: Vec<GenericArgument>, predicates: Vec<WherePredicate>) -> Self {
ClientCtx {
ident,
generics,
predicates,
}
}

/// Returns the `impl` quote block for the given context type, used for
/// implementing ClientValidation/ExecutionContext on the given enum.
fn impl_ts(&self) -> TokenStream {
let gens = self.generics.clone();

quote! { impl<#(#gens),*> }
}

/// Returns the `where` clause quote block for the given context type, used
/// for implementing ClientValidation/ExecutionContext on the given enum.
fn where_clause_ts(&self) -> TokenStream {
let predicates = self.predicates.clone();

quote! { where #(#predicates),* }
}
}

impl ToTokens for ClientCtx {
fn to_tokens(&self, tokens: &mut TokenStream) {
let ident = &self.ident;

let generics = self.generics.iter().map(|g| g.to_token_stream());

tokens.extend(quote! { #ident<#(#generics),*> });
}
}

pub(crate) struct Opts {
#[darling(rename = "ClientValidationContext")]
client_validation_context: syn::ExprPath,
#[darling(rename = "ClientExecutionContext")]
client_execution_context: syn::ExprPath,
client_validation_context: ClientCtx,
client_execution_context: ClientCtx,
}

impl Opts {
/// Returns the `Opts` struct from the given `DeriveInput` AST.
fn from_derive_input(ast: &DeriveInput) -> Result<Self, Error> {
let mut client_validation_context = None;
let mut client_execution_context = None;

if ast.attrs.is_empty() {
return Err(Error::new_spanned(ast, MISSING_ATTR));
}

for attr in ast.attrs.iter() {
if let syn::Meta::List(ref meta_list) = attr.meta {
let path: syn::Path = syn::parse2(meta_list.tokens.clone())?;

let path_segment = match path.segments.last() {
Some(segment) => segment.clone(),
None => return Err(Error::new_spanned(&meta_list.path, INVALID_ARGS)),
};

let meta_ident = match meta_list.path.require_ident() {
Ok(ident) => ident.to_string(),
Err(e) => return Err(Error::new_spanned(attr, e)),
};

let (gens, ps) = split_for_impl(path_segment.arguments)?;

let ctx = ClientCtx::new(path_segment.ident.clone(), gens, ps);

match meta_ident.as_str() {
"validation" => client_validation_context = Some(ctx),
"execution" => client_execution_context = Some(ctx),
_ => return Err(Error::new_spanned(&meta_list.path, INVALID_ATTR)),
};
}
}

let client_validation_context = client_validation_context
.ok_or_else(|| Error::new_spanned(ast, MISSING_VALIDATION_ATTR))?;
let client_execution_context = client_execution_context
.ok_or_else(|| Error::new_spanned(ast, MISSING_EXECUTION_ATTR))?;

Ok(Opts {
client_validation_context,
client_execution_context,
})
}
}

fn split_for_impl(
args: syn::PathArguments,
) -> Result<(Vec<GenericArgument>, Vec<WherePredicate>), Error> {
let mut generics = vec![];
let mut predicates = vec![];

if let syn::PathArguments::AngleBracketed(ref gen) = args {
for arg in gen.args.clone() {
match arg.clone() {
syn::GenericArgument::Type(_) => {
generics.push(arg);
}
syn::GenericArgument::Lifetime(_) => {
generics.push(arg);
}
syn::GenericArgument::Constraint(c) => {
let ident = c.ident.into_token_stream();

let gen = syn::parse2(ident.into_token_stream())?;

generics.push(gen);

let gen_type_param: syn::TypeParam =
syn::parse2(arg.clone().into_token_stream())?;

predicates.push(syn::parse2(gen_type_param.into_token_stream())?);
}
_ => return Err(Error::new_spanned(arg, INVALID_ARGS)),
};
}
}

Ok((generics, predicates))
}

pub fn client_state_derive_impl(ast: DeriveInput) -> TokenStream {
pub fn client_state_derive_impl(ast: DeriveInput, imports: &Imports) -> TokenStream {
let opts = match Opts::from_derive_input(&ast) {
Ok(opts) => opts,
Err(e) => panic!(
"{} must be annotated with #[generics(ClientValidationContext = <your ClientValidationContext>, ClientExecutionContext: <your ClientExecutionContext>)]: {e}",
ast.ident
),
Err(e) => panic!("{e}"),
};

let enum_name = &ast.ident;
Expand All @@ -32,45 +158,15 @@ pub fn client_state_derive_impl(ast: DeriveInput) -> TokenStream {
_ => panic!("ClientState only supports enums"),
};

let ClientStateCommon_impl_block = impl_ClientStateCommon(enum_name, enum_variants);
let ClientStateCommon_impl_block = impl_ClientStateCommon(enum_name, enum_variants, imports);
let ClientStateValidation_impl_block =
impl_ClientStateValidation(enum_name, enum_variants, &opts);
impl_ClientStateValidation(enum_name, enum_variants, &opts, imports);
let ClientStateExecution_impl_block =
impl_ClientStateExecution(enum_name, enum_variants, &opts);

let maybe_extern_crate_stmt = if is_mock(&ast) {
// Note: we must add this statement when in "mock mode"
// (i.e. in ibc-rs itself) because we don't have `ibc` as a dependency,
// so we need to define the `ibc` symbol to mean "the `self` crate".
quote! {extern crate self as ibc;}
} else {
quote! {}
};
impl_ClientStateExecution(enum_name, enum_variants, &opts, imports);

quote! {
#maybe_extern_crate_stmt

#ClientStateCommon_impl_block
#ClientStateValidation_impl_block
#ClientStateExecution_impl_block
}
}

/// We are in "mock mode" (i.e. within ibc-rs crate itself) if the user added
/// a #[mock] attribute
fn is_mock(ast: &DeriveInput) -> bool {
for attr in &ast.attrs {
let path = match attr.meta {
syn::Meta::Path(ref path) => path,
_ => continue,
};

for path_segment in path.segments.iter() {
if path_segment.ident == "mock" {
return true;
}
}
}

false
}
29 changes: 19 additions & 10 deletions ibc-derive/src/client_state/traits/client_state_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,54 +9,62 @@ use crate::utils::{get_enum_variant_type_path, Imports};
pub(crate) fn impl_ClientStateCommon(
client_state_enum_name: &Ident,
enum_variants: &Punctuated<Variant, Comma>,
imports: &Imports,
) -> TokenStream {
let verify_consensus_state_impl = delegate_call_in_match(
client_state_enum_name,
enum_variants.iter(),
quote! { verify_consensus_state(cs, consensus_state) },
imports,
);
let client_type_impl = delegate_call_in_match(
client_state_enum_name,
enum_variants.iter(),
quote! {client_type(cs)},
imports,
);
let latest_height_impl = delegate_call_in_match(
client_state_enum_name,
enum_variants.iter(),
quote! {latest_height(cs)},
imports,
);
let validate_proof_height_impl = delegate_call_in_match(
client_state_enum_name,
enum_variants.iter(),
quote! {validate_proof_height(cs, proof_height)},
imports,
);
let verify_upgrade_client_impl = delegate_call_in_match(
client_state_enum_name,
enum_variants.iter(),
quote! {verify_upgrade_client(cs, upgraded_client_state, upgraded_consensus_state, proof_upgrade_client, proof_upgrade_consensus_state, root)},
imports,
);
let verify_membership_impl = delegate_call_in_match(
client_state_enum_name,
enum_variants.iter(),
quote! {verify_membership(cs, prefix, proof, root, path, value)},
imports,
);
let verify_non_membership_impl = delegate_call_in_match(
client_state_enum_name,
enum_variants.iter(),
quote! {verify_non_membership(cs, prefix, proof, root, path)},
imports,
);

let HostClientState = client_state_enum_name;

let Any = Imports::Any();
let CommitmentRoot = Imports::CommitmentRoot();
let CommitmentPrefix = Imports::CommitmentPrefix();
let CommitmentProofBytes = Imports::CommitmentProofBytes();
let ClientStateCommon = Imports::ClientStateCommon();
let ClientType = Imports::ClientType();
let ClientError = Imports::ClientError();
let Height = Imports::Height();
let Path = Imports::Path();
let Any = imports.any();
let CommitmentRoot = imports.commitment_root();
let CommitmentPrefix = imports.commitment_prefix();
let CommitmentProofBytes = imports.commitment_proof_bytes();
let ClientStateCommon = imports.client_state_common();
let ClientType = imports.client_type();
let ClientError = imports.client_error();
let Height = imports.height();
let Path = imports.path();

quote! {
impl #ClientStateCommon for #HostClientState {
Expand Down Expand Up @@ -156,8 +164,9 @@ fn delegate_call_in_match(
enum_name: &Ident,
enum_variants: Iter<'_, Variant>,
fn_call: TokenStream,
imports: &Imports,
) -> Vec<TokenStream> {
let ClientStateCommon = Imports::ClientStateCommon();
let ClientStateCommon = imports.client_state_common();

enum_variants
.map(|variant| {
Expand Down
Loading

0 comments on commit fcc8c57

Please sign in to comment.