Skip to content

Commit

Permalink
Use better errors in wasmlanche-macros
Browse files Browse the repository at this point in the history
  • Loading branch information
richardpringle committed Mar 6, 2024
1 parent f1c1871 commit a0f65c1
Showing 1 changed file with 87 additions and 25 deletions.
112 changes: 87 additions & 25 deletions x/programs/rust/sdk_macros/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
extern crate proc_macro;

use core::panic;

use proc_macro::TokenStream;
use proc_macro2::Span;
use quote::quote;
use syn::{
parse_macro_input, Fields, FnArg, Ident, ItemEnum, ItemFn, Pat, PatType, Type, Visibility,
parse_macro_input, spanned::Spanned, Fields, FnArg, Ident, ItemEnum, ItemFn, Pat, PatType,
Type, Visibility,
};

fn convert_param(param_name: &Ident) -> proc_macro2::TokenStream {
Expand All @@ -15,6 +14,8 @@ fn convert_param(param_name: &Ident) -> proc_macro2::TokenStream {
}
}

const PROGRAM_TYPE_NAME: &str = "Program";

/// An attribute procedural macro that makes a function visible to the VM host.
/// It does so by wrapping the `item` tokenstream in a new function that can be called by the host.
/// The wrapper function will have the same name as the original function, but with "_guest" appended to it.
Expand All @@ -24,42 +25,104 @@ fn convert_param(param_name: &Ident) -> proc_macro2::TokenStream {
pub fn public(_: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as ItemFn);

if !matches!(input.vis, Visibility::Public(_)) {
let name = &input.sig.ident;
panic!("Function `{name}` must be pub to be marked with the `#[public]` attribute.");
}
let err = if !matches!(input.vis, Visibility::Public(_)) {
// let name = &input.sig.ident;
// panic!("Function `{name}` must be pub to be marked with the `#[public]` attribute.");

let err = syn::Error::new(
input.sig.span(),
"Functions with the `#[public]` attribute must have `pub` visibility.",
);

Some(err)
} else {
None
};

let name = &input.sig.ident;
let input_args = &input.sig.inputs;
let new_name = Ident::new(&format!("{}_guest", name), name.span()); // Create a new name for the generated function(name that will be called by the host)
let empty_param = Ident::new("ctx", Span::call_site()); // Create an empty parameter for the generated function
let full_params = input_args.iter().enumerate().map(|(index, fn_arg)| {
// A typed argument is a parameter. An untyped (receiver) argument is a `self` parameter.
if let FnArg::Typed(PatType { pat, ty, .. }) = fn_arg {
// ensure first parameter type is Program
if index == 0 && !is_program(ty) {
panic!("First parameter must be Program.");

let err = match (err, input_args.first()) {
(err, Some(FnArg::Typed(PatType { ty, .. }))) if is_program(ty) => err,
(Some(mut err), Some(arg)) => {
err.combine(syn::Error::new(
arg.span(),
"The first paramter of a function with the `#[public]` attribute must be of type `Program`"
));

Some(err)
}

(err, _) => {
let other = syn::Error::new(
input.sig.paren_token.span.join(),
"Functions with the `#[public]` attribute must have at least one parameter and the first parameter must be of type `Program`",
);

if let Some(mut err) = err {
err.combine(other);
Some(err)
} else {
Some(other)
}
}
};

let full_params = input_args.iter().enumerate().map(|(i, fn_arg)| {
if let FnArg::Typed(PatType { pat, ty: _, .. }) = fn_arg {
if let Pat::Ident(ref pat_ident) = **pat {
return (&pat_ident.ident, quote! { i64 });
return Ok(&pat_ident.ident);
}
// add unused variable
if let Pat::Wild(_) = **pat {
if is_program(ty) {
return (&empty_param, quote! { i64 });
} else {
panic!("Unused variables only supported for Program.")
}

if i == 0 && matches!(**pat, Pat::Wild(_)) {
return Ok(&empty_param);
}

// if let Pat::Wild(_) = **pat {
// return if is_program(ty) {
// Ok(&empty_param)
// } else {
// Err(syn::Error::new(
// pat.span(),
// "Unused variables only supported for Program.",
// ))
// };
// }
}
panic!("Unsupported function parameter format.");

Err(syn::Error::new(
fn_arg.span(),
"Unsupported function parameter format.",
))
});

let (param_names, param_types): (Vec<_>, Vec<_>) = full_params.unzip();
let result = err.map(Err).unwrap_or(Ok(vec![]));

let param_names = full_params.fold(result, |result, param| match (result, param) {
(Err(mut errors), Err(e)) => {
errors.combine(e);
Err(errors)
}
(Err(errors), Ok(_)) | (Ok(_), Err(errors)) => Err(errors),
(Ok(mut names), Ok(name)) => {
names.push(name);
Ok(names)
}
});

let param_names = match param_names {
Ok(param_names) => param_names,
Err(errors) => return errors.to_compile_error().into(),
};

let converted_params = param_names
.iter()
.map(|param_name| convert_param(param_name));

let param_types = std::iter::repeat(quote! { i64 }).take(param_names.len());

// Extract the original function's return type. This must be a WASM supported type.
let return_type = &input.sig.output;
let output = quote! {
Expand Down Expand Up @@ -152,8 +215,7 @@ fn generate_to_vec(
fn is_program(type_path: &std::boxed::Box<Type>) -> bool {
if let Type::Path(ref type_path) = **type_path {
let ident = &type_path.path.segments[0].ident;
let ident_str = ident.to_string();
ident_str == "Program"
ident == PROGRAM_TYPE_NAME
} else {
false
}
Expand Down

0 comments on commit a0f65c1

Please sign in to comment.