Skip to content

Commit

Permalink
runtime-sdk: Implement automatic MigrationHandler derivation
Browse files Browse the repository at this point in the history
  • Loading branch information
kostko committed Sep 12, 2023
1 parent 09e6162 commit be2d299
Show file tree
Hide file tree
Showing 18 changed files with 562 additions and 633 deletions.
24 changes: 17 additions & 7 deletions runtime-sdk-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,22 +45,21 @@ pub fn error_derive(input: TokenStream) -> TokenStream {

/// Derives traits from a non-trait `impl` block (rather than from a `struct`).
///
/// Only the `MethodHandler` trait is supported. In other words, given an
/// `impl MyModule` block, the macro derives `impl MethodHandler for MyModule`.
/// See also the `#[handler]` attribute.
/// Only the `Module` trait is supported. In other words, given an `impl MyModule` block, the macro
/// derives implementations needed for implementing a module.
/// See also the `#[handler]` and `#[migration]` attributes.
#[proc_macro_attribute]
pub fn sdk_derive(args: TokenStream, input: TokenStream) -> TokenStream {
let input = syn::parse_macro_input!(input as syn::ItemImpl);
if args.to_string() == "MethodHandler" {
// TODO: Change to Module once we have more handlers.
if args.to_string() == "Module" {
module_derive::derive_module(input).into()
} else {
emit_compile_error("#[sdk_derive] only supports #[sdk_derive(MethodHandler)]");
emit_compile_error("#[sdk_derive] only supports #[sdk_derive(Module)]");
}
}

/// A helper attribute for `#[sdk_derive(...)]`. It doesn't do anyting on its own;
/// it only mark functions that represent a paratime method handler.
/// it only marks functions that represent a paratime method handler.
/// The permitted forms are:
/// - `#[handler(call = "my_module.MyCall")]`: Marks a function that handles
/// the "my_module.MyCall" call and can be passed to
Expand Down Expand Up @@ -92,6 +91,17 @@ pub fn handler(_args: TokenStream, input: TokenStream) -> TokenStream {
input
}

/// A helper attribute for `#[sdk_derive(...)]`. It doesn't do anything on its own;
/// it only marks functions that represent a module state migration.
///
/// The permitted forms are:
/// - `#[migration(init)]`: Marks the initial (genesis) migration.
/// - `#[migration(from = 1)]`: Marks a migration from version 1.
#[proc_macro_attribute]
pub fn migration(_args: TokenStream, input: TokenStream) -> TokenStream {
input
}

/// Constructs an `oasis_sdk::core::common::version::Version` from the Cargo.toml version.
#[proc_macro]
pub fn version_from_cargo(_input: TokenStream) -> TokenStream {
Expand Down
2 changes: 2 additions & 0 deletions runtime-sdk-macros/src/module_derive/method_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@ impl super::Deriver for DeriveMethodHandler {
};

quote! {
#[automatically_derived]
impl #generics sdk::module::MethodHandler for #ty {
#prefetch_impl
#dispatch_call_impl
Expand All @@ -286,6 +287,7 @@ impl super::Deriver for DeriveMethodHandler {
#allowed_interactive_calls_impl
}

#[automatically_derived]
impl #generics #ty {
#query_parameters_impl

Expand Down
167 changes: 167 additions & 0 deletions runtime-sdk-macros/src/module_derive/migration_handler.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
use std::collections::HashSet;

use proc_macro2::TokenStream;
use quote::quote;

use crate::emit_compile_error;

/// Deriver for the `MigrationHandler` trait.
pub struct DeriveMigrationHandler {
/// Item defining the `MigrationHandler::Genesis` associated type.
genesis_ty: Option<syn::ImplItem>,
/// Migration functions.
migrate_fns: Vec<MigrateFn>,
}

struct MigrateFn {
item: syn::ImplItem,
ident: syn::Ident,
from_version: u32,
}

impl DeriveMigrationHandler {
pub fn new() -> Box<Self> {
Box::new(Self {
genesis_ty: None,
migrate_fns: vec![],
})
}
}

impl super::Deriver for DeriveMigrationHandler {
fn preprocess(&mut self, item: syn::ImplItem) -> Option<syn::ImplItem> {
match item {
// We are looking for a `type Genesis = ...;` item.
syn::ImplItem::Type(ref ty) if &ty.ident.to_string() == "Genesis" => {
self.genesis_ty = Some(item);

None // Take the item.
}
syn::ImplItem::Fn(ref f) => {
// Check whether a `migration` attribute is set for the method.
if let Some(attrs) = parse_attrs(&f.attrs) {
self.migrate_fns.push(MigrateFn {
ident: f.sig.ident.clone(),
from_version: attrs.from_version,
item,
});

None
} else {
Some(item) // Return the item.
}
}
_ => Some(item), // Return the item.
}
}

fn derive(&mut self, generics: &syn::Generics, ty: &Box<syn::Type>) -> TokenStream {
let genesis_ty = if let Some(genesis_ty) = &self.genesis_ty {
genesis_ty
} else {
return quote! {};
};

let mut seen_versions = HashSet::new();
let (migrate_fns, mut migrate_arms): (Vec<_>, Vec<_>) = self.migrate_fns.iter().map(|f| {
let MigrateFn { item, ident, from_version } = f;
if seen_versions.contains(from_version) {
emit_compile_error(format!(
"Duplicate migration for version: {from_version}"
));
}
seen_versions.insert(from_version);

(
item,
if from_version == &0 {
// Version zero is special as initializing from genesis always gets us latest.
quote! { if version == #from_version { Self::#ident(genesis); version = Self::VERSION; } }
} else {
// For other versions, each migration brings us from V to V+1.
quote! { if version == #from_version { Self::#ident(); version += 1; } }
}
)
}).unzip();

// Ensure there is a genesis migration, at least an empty one that bumps the version.
if !seen_versions.contains(&0) {
migrate_arms.push(quote! {
if version == 0u32 { version = Self::VERSION; }
});
}

quote! {
#[automatically_derived]
impl #generics sdk::module::MigrationHandler for #ty {
#genesis_ty

fn init_or_migrate<C: Context>(
_ctx: &mut C,
meta: &mut sdk::modules::core::types::Metadata,
genesis: Self::Genesis,
) -> bool {
let mut version = meta.versions.get(Self::NAME).copied().unwrap_or_default();
if version == Self::VERSION {
return false; // Already the latest version.
}

#(#migrate_arms)*

if version != Self::VERSION {
panic!("no migration for module state from version {version} to {}", Self::VERSION)
}

// Update version information.
meta.versions.insert(Self::NAME.to_owned(), Self::VERSION);
return true;
}
}

#[automatically_derived]
impl #generics #ty {
#(#migrate_fns)*
}
}
}
}

#[derive(Debug, Clone, PartialEq)]
struct MigrationHandlerAttr {
/// Version that this handler handles. Zero indicates genesis.
from_version: u32,
}
impl syn::parse::Parse for MigrationHandlerAttr {
fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result<Self> {
let kind: syn::Ident = input.parse()?;
let from_version = match kind.to_string().as_str() {
"init" => 0,
"from" => {
let _: syn::token::Eq = input.parse()?;
let version: syn::LitInt = input.parse()?;

version.base10_parse()?
}
_ => return Err(syn::Error::new(kind.span(), "invalid migration kind")),
};

if !input.is_empty() {
return Err(syn::Error::new(input.span(), "unexpected extra tokens"));
}
Ok(Self { from_version })
}
}

fn parse_attrs(attrs: &[syn::Attribute]) -> Option<MigrationHandlerAttr> {
let migration_meta = attrs
.iter()
.find(|attr| attr.path().is_ident("migration"))?;
migration_meta
.parse_args()
.map_err(|err| {
emit_compile_error(format!(
"Unsupported format of #[migration(...)] attribute: {err}"
))
})
.ok()
}
16 changes: 15 additions & 1 deletion runtime-sdk-macros/src/module_derive/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
mod method_handler;
mod migration_handler;

use proc_macro2::TokenStream;
use quote::quote;
Expand All @@ -15,7 +16,10 @@ pub fn derive_module(impl_block: syn::ItemImpl) -> TokenStream {
let mut base_impls: Vec<TokenStream> = Vec::new();
let mut derivations: Vec<TokenStream> = Vec::new();

let mut derivers: Vec<Box<dyn Deriver>> = vec![method_handler::DeriveMethodHandler::new()];
let mut derivers: Vec<Box<dyn Deriver>> = vec![
migration_handler::DeriveMigrationHandler::new(),
method_handler::DeriveMethodHandler::new(),
];

// Iterate through all impl items, collecting them and then deriving everything.
'items: for item in impl_block.items {
Expand Down Expand Up @@ -43,6 +47,7 @@ pub fn derive_module(impl_block: syn::ItemImpl) -> TokenStream {
None
} else {
Some(quote! {
#[automatically_derived]
impl #module_generics #module_ty {
#(#base_impls)*
}
Expand Down Expand Up @@ -109,6 +114,7 @@ mod tests {
syn::parse_quote!(
const _: () = {
#uses
#[automatically_derived]
impl<C: Cfg> sdk::module::MethodHandler for MyModule<C> {
fn dispatch_query<C: Context>(
ctx: &mut C,
Expand All @@ -124,11 +130,13 @@ mod tests {
}
}
}
#[automatically_derived]
impl<C: Cfg> MyModule<C> {
fn query_parameters<C: Context>(_ctx: &mut C, _args: ()) -> Result<<Self as module::Module>::Parameters, <Self as module::Module>::Error> {
Ok(Self::params())
}
}
#[automatically_derived]
impl<C: Cfg> MyModule<C> {
fn unannotated_fn_should_be_passed_thru(foo: Bar) -> Baz {}
}
Expand Down Expand Up @@ -157,6 +165,7 @@ mod tests {
syn::parse_quote!(
const _: () = {
#uses
#[automatically_derived]
impl<C: Cfg> sdk::module::MethodHandler for MyModule<C> {
fn prefetch(
prefixes: &mut BTreeSet<Prefix>,
Expand Down Expand Up @@ -214,6 +223,7 @@ mod tests {
]
}
}
#[automatically_derived]
impl<C: Cfg> MyModule<C> {
fn query_parameters<C: Context>(_ctx: &mut C, _args: ()) -> Result<<Self as module::Module>::Parameters, <Self as module::Module>::Error> {
Ok(Self::params())
Expand Down Expand Up @@ -250,6 +260,7 @@ mod tests {
syn::parse_quote!(
const _: () = {
#uses
#[automatically_derived]
impl<C: Cfg> sdk::module::MethodHandler for MyModule<C> {
fn dispatch_query<C: Context>(
ctx: &mut C,
Expand Down Expand Up @@ -290,6 +301,7 @@ mod tests {
["module.ConfidentialQuery"].contains(&method)
}
}
#[automatically_derived]
impl<C: Cfg> MyModule<C> {
fn query_parameters<C: Context>(
_ctx: &mut C,
Expand Down Expand Up @@ -326,6 +338,7 @@ mod tests {
syn::parse_quote!(
const _: () = {
#uses
#[automatically_derived]
impl<C: Cfg> sdk::module::MethodHandler for MyModule<C> {
fn dispatch_query<C: Context>(
ctx: &mut C,
Expand All @@ -348,6 +361,7 @@ mod tests {
}]
}
}
#[automatically_derived]
impl<C: Cfg> MyModule<C> {
fn query_parameters<C: Context>(_ctx: &mut C, _args: ()) -> Result<<Self as module::Module>::Parameters, <Self as module::Module>::Error> {
Ok(Self::params())
Expand Down
13 changes: 5 additions & 8 deletions runtime-sdk/modules/contracts/src/abi/oasis/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -318,15 +318,12 @@ fn run_contract_with_defaults(
let mut ctx = mock.create_ctx_for_runtime::<mock::EmptyRuntime>(context::Mode::ExecuteTx, true);
let params = Parameters::default();

core::Module::<CoreConfig>::init(
&mut ctx,
core::Genesis {
parameters: core::Parameters {
max_batch_gas: gas_limit,
..Default::default()
},
core::Module::<CoreConfig>::init(core::Genesis {
parameters: core::Parameters {
max_batch_gas: gas_limit,
..Default::default()
},
);
});

let mut tx = mock::transaction();
tx.auth_info.fee.gas = gas_limit;
Expand Down
Loading

0 comments on commit be2d299

Please sign in to comment.