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

Allow deriving protocols with Any #587

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion crates/rune-macros/src/any.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,9 @@ impl Derive {
let generics = &self.input.generics;
let mut installers = Vec::new();

let Ok(()) = expand_install_with(&cx, &self.input, &tokens, &attr, generics, &mut installers) else {
let Ok(()) =
expand_install_with(&cx, &self.input, &tokens, &attr, generics, &mut installers)
else {
return Err(cx.errors.into_inner());
};

Expand Down Expand Up @@ -152,6 +154,13 @@ pub(crate) fn expand_install_with(
}
}

installers.extend(attr.protocols.iter().map(|protocol| protocol.expand()));
installers.extend(attr.functions.iter().map(|function| {
quote_spanned! {function.span()=>
module.function_meta(#function)?;
}
}));

if let Some(install_with) = &attr.install_with {
installers.push(quote_spanned! { input.span() =>
#install_with(module)?;
Expand Down
198 changes: 143 additions & 55 deletions crates/rune-macros/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ use std::cell::RefCell;
use crate::internals::*;
use proc_macro2::Span;
use proc_macro2::TokenStream;
use quote::quote_spanned;
use quote::{quote, ToTokens};
use quote::{quote, quote_spanned, ToTokens};
use syn::parse::Parse;
use syn::parse::ParseStream;
use syn::punctuated::Punctuated;
use syn::spanned::Spanned as _;
Expand Down Expand Up @@ -74,6 +74,10 @@ pub(crate) struct TypeAttr {
pub(crate) item: Option<syn::Path>,
/// `#[rune(constructor)]`.
pub(crate) constructor: bool,
/// Protocols to "derive"
pub(crate) protocols: Vec<TypeProtocol>,
/// Assosiated functions
pub(crate) functions: Vec<syn::Path>,
/// Parsed documentation.
pub(crate) docs: Vec<syn::Expr>,
}
Expand Down Expand Up @@ -113,6 +117,64 @@ pub(crate) struct FieldProtocol {
custom: Option<syn::Path>,
}

pub(crate) struct TypeProtocol {
protocol: syn::Ident,
handler: Option<syn::Expr>,
}

impl TypeProtocol {
pub fn expand(&self) -> TokenStream {
if let Some(handler) = &self.handler {
let protocol = &self.protocol;
return quote_spanned! {protocol.span()=>
module.associated_function(rune::runtime::Protocol::#protocol, #handler)?;
};
}
match self.protocol.to_string().as_str() {
"ADD" => quote_spanned! {self.protocol.span()=>
ModProg marked this conversation as resolved.
Show resolved Hide resolved
module.associated_function(rune::runtime::Protocol::ADD, |this: Self, other: Self| this + other)?;
},
"STRING_DISPLAY" => quote_spanned! {self.protocol.span()=>
module.associated_function(rune::runtime::Protocol::STRING_DISPLAY, |this: &Self, buf: &mut ::std::string::String| {
use ::core::fmt::Write as _;
::core::write!(buf, "{this}")
})?;
},
"STRING_DEBUG" => quote_spanned! {self.protocol.span()=>
module.associated_function(rune::runtime::Protocol::STRING_DEBUG, |this: &Self, buf: &mut ::std::string::String| {
use ::core::fmt::Write as _;
::core::write!(buf, "{this:?}")
})?;
},
_ => unreachable!("`parse()` ensures only supported protocols"),
}
}
}

impl Parse for TypeProtocol {
fn parse(input: ParseStream) -> syn::Result<Self> {
let it = Self {
protocol: input.parse()?,
handler: if input.parse::<Token![=]>().is_ok() {
Some(input.parse()?)
} else {
None
},
};

if it.handler.is_some()
|| ["ADD", "STRING_DISPLAY", "STRING_DEBUG"].contains(&it.protocol.to_string().as_str())
{
Ok(it)
} else {
Err(syn::Error::new_spanned(
&it.protocol,
format!("Rune protocol `{}` cannot be derived", it.protocol),
))
}
}
}

#[derive(Default)]
pub(crate) struct Context {
pub(crate) errors: RefCell<Vec<syn::Error>>,
Expand Down Expand Up @@ -397,7 +459,7 @@ impl Context {
let mut error = false;
let mut attr = TypeAttr::default();

for a in input {
'attrs: for a in input {
if a.path().is_ident("doc") {
if let syn::Meta::NameValue(meta) = &a.meta {
attr.docs.push(meta.value.clone());
Expand All @@ -406,60 +468,86 @@ impl Context {
continue;
}

if a.path() == RUNE {
let result = a.parse_nested_meta(|meta| {
if meta.path == PARSE {
// Parse `#[rune(parse = "..")]`
meta.input.parse::<Token![=]>()?;
let s: syn::LitStr = meta.input.parse()?;

match s.value().as_str() {
"meta_only" => {
attr.parse = ParseKind::MetaOnly;
}
other => {
return Err(syn::Error::new(
meta.input.span(),
format!(
"Unsupported `#[rune(parse = ..)]` argument `{}`",
other
),
));
}
};
} else if meta.path == ITEM {
// Parse `#[rune(item = "..")]`
meta.input.parse::<Token![=]>()?;
attr.item = Some(meta.input.parse()?);
} else if meta.path == NAME {
// Parse `#[rune(name = "..")]`
meta.input.parse::<Token![=]>()?;
attr.name = Some(meta.input.parse()?);
} else if meta.path == MODULE {
// Parse `#[rune(module = <path>)]`
meta.input.parse::<Token![=]>()?;
attr.module = Some(parse_path_compat(meta.input)?);
} else if meta.path == INSTALL_WITH {
// Parse `#[rune(install_with = <path>)]`
meta.input.parse::<Token![=]>()?;
attr.install_with = Some(parse_path_compat(meta.input)?);
} else if meta.path == CONSTRUCTOR {
attr.constructor = true;
} else {
return Err(syn::Error::new_spanned(
&meta.path,
"Unsupported type attribute",
));
}
let err = 'error: {
if a.path() == RUNE {
let result = a.parse_nested_meta(|meta| {
if meta.path == PARSE {
// Parse `#[rune(parse = "..")]`
meta.input.parse::<Token![=]>()?;
let s: syn::LitStr = meta.input.parse()?;

match s.value().as_str() {
"meta_only" => {
attr.parse = ParseKind::MetaOnly;
}
other => {
return Err(syn::Error::new(
meta.input.span(),
format!(
"Unsupported `#[rune(parse = ..)]` argument `{}`",
other
),
));
}
};
} else if meta.path == ITEM {
// Parse `#[rune(item = "..")]`
meta.input.parse::<Token![=]>()?;
attr.item = Some(meta.input.parse()?);
} else if meta.path == NAME {
// Parse `#[rune(name = "..")]`
meta.input.parse::<Token![=]>()?;
attr.name = Some(meta.input.parse()?);
} else if meta.path == MODULE {
// Parse `#[rune(module = <path>)]`
meta.input.parse::<Token![=]>()?;
attr.module = Some(parse_path_compat(meta.input)?);
} else if meta.path == INSTALL_WITH {
// Parse `#[rune(install_with = <path>)]`
meta.input.parse::<Token![=]>()?;
attr.install_with = Some(parse_path_compat(meta.input)?);
} else if meta.path == CONSTRUCTOR {
attr.constructor = true;
} else {
return Err(syn::Error::new_spanned(
&meta.path,
"Unsupported type attribute",
));
}

Ok(())
});
Ok(())
});

if let Err(e) = result {
error = true;
self.error(e);
};
}
if let Err(e) = result {
break 'error e;
};
}

if a.path() == RUNE_DERIVE {
attr.protocols.extend(
match a.parse_args_with(Punctuated::<_, Token![,]>::parse_terminated) {
Ok(it) => it,
Err(err) => {
break 'error err;
}
},
);
}

if a.path() == RUNE_FUNCTIONS {
attr.functions.extend(
match a.parse_args_with(Punctuated::<_, Token![,]>::parse_terminated) {
Ok(it) => it,
Err(err) => {
break 'error err;
}
},
);
}
continue 'attrs;
};
error = true;
self.error(err);
}

if error {
Expand Down
2 changes: 2 additions & 0 deletions crates/rune-macros/src/internals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ use std::fmt;
pub struct Symbol(&'static str);

pub const RUNE: Symbol = Symbol("rune");
pub const RUNE_DERIVE: Symbol = Symbol("rune_derive");
pub const RUNE_FUNCTIONS: Symbol = Symbol("rune_functions");
pub const ID: Symbol = Symbol("id");
pub const SKIP: Symbol = Symbol("skip");
pub const ITER: Symbol = Symbol("iter");
Expand Down
2 changes: 1 addition & 1 deletion crates/rune-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ pub fn to_value(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
.into()
}

#[proc_macro_derive(Any, attributes(rune))]
#[proc_macro_derive(Any, attributes(rune, rune_derive, rune_functions))]
pub fn any(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let derive = syn::parse_macro_input!(input as any::Derive);
derive.expand().unwrap_or_else(to_compile_errors).into()
Expand Down
45 changes: 45 additions & 0 deletions crates/rune/src/any.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,51 @@ use crate::hash::Hash;
/// Ok(module)
/// }
/// ```
///
/// ## `#[rune_derive(PROTOCOL, PROTOCOL = handler, ...)]` attribute
///
/// Can directly implement supported protocols via the rust Trait, i.e.,
/// `#[rune_derive(STRING_DEBUG)]` requires the type to implement
/// [`Debug`](core::fmt::Debug) and adds a handler for
/// [`Protocol::STRING_DEBUG`](crate::runtime::Protocol::STRING_DEBUG).
///
/// For unsupported protocols, or to deviate from the trait implementation,
/// a custom handler can be specified, this can either be a closure or a path
/// to a function.
///
/// ```
/// use rune::Any;
///
/// #[derive(Any, Debug)]
/// #[rune_derive(STRING_DEBUG)]
/// #[rune_derive(INDEX_GET = |it: Self, i: usize| it.0[i])]
/// struct Struct(Vec<usize>);
/// ```
///
/// ## `#[rune_functions(some_function, ...)]` attribute
///
/// Allows specifying functions that will be registered together with the type,
/// these need to be annotated with [`#[rune::function]`](crate::function).
///
/// ```
/// use rune::Any;
///
/// #[derive(Any)]
/// #[rune_functions(Self::first, second)]
/// struct Struct(bool, usize);
///
/// impl Struct {
/// #[rune::function]
/// fn first(self) -> bool {
/// self.0
/// }
/// }
///
/// #[rune::function]
/// fn second(it: Struct) -> usize {
/// it.1
/// }
/// ```
pub use rune_macros::Any;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@udoprog The docs for the Any macro are actually not shown. Not sure if that is due to being reexported again at the crate root.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAICT the only way around that, is moving this reexport in the crate root.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Welp, time to move!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So what would be your preference? Add the documentation in rune-macros or in rune's lib.rs?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move the re-export and document it. Otherwise you have a circular dev-dependency for any doc tests that uses rune which rust-analyzer does not like. And all the documentation in one place.


/// A trait which can be stored inside of an [AnyObj](crate::runtime::AnyObj).
Expand Down
1 change: 1 addition & 0 deletions crates/rune/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,7 @@ mod quote;
mod range;
mod reference_error;
mod result;
mod rune_derive;
mod stmt_reordering;
mod tuple;
mod type_name_native;
Expand Down
Loading