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

Refine derive macro. #819

Merged
merged 1 commit into from
Jan 25, 2024
Merged
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
137 changes: 100 additions & 37 deletions codegen/src/custom_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,38 +6,100 @@ pub fn derive_custom_type_impl(input: DeriveInput) -> TokenStream {
let name = input.ident;

let accessors = match input.data {
syn::Data::Struct(ref data) => match data.fields {
syn::Fields::Named(ref fields) => {
let iter = fields.named.iter().map(|field| {
let mut get_fn = None;
let mut set_fn = None;
let mut readonly = false;
for attr in field.attrs.iter() {
if attr.path().is_ident("get") {
get_fn = Some(
attr.parse_args()
.unwrap_or_else(syn::Error::into_compile_error),
);
} else if attr.path().is_ident("set") {
set_fn = Some(
attr.parse_args()
.unwrap_or_else(syn::Error::into_compile_error),
);
} else if attr.path().is_ident("readonly") {
readonly = true;
syn::Data::Struct(syn::DataStruct {
fields: syn::Fields::Named(ref fields),
..
}) => {
let iter = fields.named.iter().map(|field| {
let mut name = None;
let mut get_fn = None;
let mut set_fn = None;
let mut readonly = false;
let mut skip = false;

for attr in field.attrs.iter() {
if attr.path().is_ident("rhai_custom_type_skip") {
if get_fn.is_some() || set_fn.is_some() || name.is_some() {
return syn::Error::new(
Span::call_site(),
"cannot use 'rhai_custom_type_skip' with other attributes",
)
.into_compile_error();
}

skip = true;
continue;
}

generate_accessor_fns(&field.ident.as_ref().unwrap(), get_fn, set_fn, readonly)
});
quote! {#(#iter)*}
}
syn::Fields::Unnamed(_) => {
syn::Error::new(Span::call_site(), "tuple structs are not yet implemented")
.into_compile_error()
}
syn::Fields::Unit => quote! {},
},
if skip {
return syn::Error::new(
Span::call_site(),
"cannot use 'rhai_custom_type_skip' with other attributes",
)
.into_compile_error();
}

if attr.path().is_ident("rhai_custom_type_name") {
name = Some(
attr.parse_args()
.unwrap_or_else(syn::Error::into_compile_error),
);
} else if attr.path().is_ident("rhai_custom_type_get") {
get_fn = Some(
attr.parse_args()
.unwrap_or_else(syn::Error::into_compile_error),
);
} else if attr.path().is_ident("rhai_custom_type_set") {
if readonly {
return syn::Error::new(
Span::call_site(),
"cannot use 'rhai_custom_type_set' with 'rhai_custom_type_readonly'",
)
.into_compile_error();
}
set_fn = Some(
attr.parse_args()
.unwrap_or_else(syn::Error::into_compile_error),
);
} else if attr.path().is_ident("rhai_custom_type_readonly") {
if set_fn.is_some() {
return syn::Error::new(
Span::call_site(),
"cannot use 'rhai_custom_type_readonly' with 'rhai_custom_type_set'",
)
.into_compile_error();
}
readonly = true;
}
}

if !skip {
generate_accessor_fns(
&field.ident.as_ref().unwrap(),
name,
get_fn,
set_fn,
readonly,
)
} else {
quote! {}
}
});

quote! {#(#iter)*}
}

syn::Data::Struct(syn::DataStruct {
fields: syn::Fields::Unnamed(_),
..
}) => syn::Error::new(Span::call_site(), "tuple structs are not yet implemented")
.into_compile_error(),

syn::Data::Struct(syn::DataStruct {
fields: syn::Fields::Unit,
..
}) => quote! {},

syn::Data::Enum(_) => {
syn::Error::new(Span::call_site(), "enums are not yet implemented").into_compile_error()
}
Expand All @@ -47,8 +109,8 @@ pub fn derive_custom_type_impl(input: DeriveInput) -> TokenStream {
};

quote! {
impl ::rhai::CustomType for #name {
fn build(mut builder: ::rhai::TypeBuilder<Self>) {
impl CustomType for #name {
fn build(mut builder: TypeBuilder<Self>) {
#accessors;
}
}
Expand All @@ -57,6 +119,7 @@ pub fn derive_custom_type_impl(input: DeriveInput) -> TokenStream {

fn generate_accessor_fns(
field: &Ident,
name: Option<TokenStream>,
get: Option<TokenStream>,
set: Option<TokenStream>,
readonly: bool,
Expand All @@ -69,17 +132,17 @@ fn generate_accessor_fns(
.map(|func| quote! {#func})
.unwrap_or_else(|| quote! {|obj: &mut Self, val| obj.#field = val});

let name = name
.map(|field| quote! { #field })
.unwrap_or_else(|| quote! { stringify!(#field) });

if readonly {
quote! {
builder.with_get("#field", #get);
builder.with_get(#name, #get);
}
} else {
quote! {
builder.with_get_set(
"#field",
#get,
#set,
);
builder.with_get_set(#name, #get, #set);
}
}
}
11 changes: 10 additions & 1 deletion codegen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -410,7 +410,16 @@ pub fn set_exported_global_fn(args: proc_macro::TokenStream) -> proc_macro::Toke
}
}

#[proc_macro_derive(CustomType, attributes(get, set, readonly))]
#[proc_macro_derive(
CustomType,
attributes(
rhai_custom_type_name,
rhai_custom_type_get,
rhai_custom_type_set,
rhai_custom_type_readonly,
rhai_custom_type_skip
)
)]
pub fn derive_custom_type(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let expanded = custom_type::derive_custom_type_impl(input);
Expand Down
22 changes: 16 additions & 6 deletions codegen/tests/test_derive.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
use rhai_codegen::CustomType;
use rhai::{CustomType, TypeBuilder, INT};

// Sanity check to make sure everything compiles
#[derive(Clone, CustomType)]
pub struct Foo {
#[get(get_bar)]
bar: i32,
#[readonly]
#[rhai_custom_type_skip]
_dummy: INT,
#[rhai_custom_type_get(get_bar)]
bar: INT,
#[rhai_custom_type_name("boo")]
#[rhai_custom_type_readonly]
baz: String,
qux: Vec<i32>,
#[rhai_custom_type_set(Self::set_qux)]
qux: Vec<INT>,
}

fn get_bar(_this: &mut Foo) -> i32 {
impl Foo {
pub fn set_qux(&mut self, value: Vec<INT>) {
self.qux = value;
}
}

fn get_bar(_this: &mut Foo) -> INT {
42
}
70 changes: 68 additions & 2 deletions tests/build_type.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
#![cfg(not(feature = "no_object"))]
use rhai::{CustomType, Engine, EvalAltResult, Position, TypeBuilder, INT};
use rhai::{CustomType, Engine, EvalAltResult, Position, Scope, TypeBuilder, INT};

#[test]
fn build_type() {
fn test_build_type() {
#[derive(Debug, Clone, PartialEq, Eq)]
struct Vec3 {
x: INT,
Expand Down Expand Up @@ -32,7 +32,7 @@
fn set_z(&mut self, z: INT) {
self.z = z
}
fn get_component(&mut self, idx: INT) -> Result<INT, Box<EvalAltResult>> {

Check warning on line 35 in tests/build_type.rs

View workflow job for this annotation

GitHub Actions / Build (ubuntu-latest, --features testing-environ,no_index,serde,metadata,internals,debugging, sta...

method `get_component` is never used
match idx {
0 => Ok(self.x),
1 => Ok(self.y),
Expand Down Expand Up @@ -156,3 +156,69 @@
6,
);
}

#[test]
fn test_build_type_macro() {
#[derive(Debug, Clone, Eq, PartialEq)] // <- necessary for any custom type
#[derive(CustomType)] // <- auto-implement 'CustomType'
struct Foo {
#[rhai_custom_type_skip]
dummy: i64, // <- skip this field
#[rhai_custom_type_readonly] // <- only auto-implement getters
bar: i64,
#[rhai_custom_type_name("emphasize")]
baz: bool, // <- auto-implement getter/setter for 'baz'
#[rhai_custom_type_set(Self::set_hello)] // <- call custom setter for 'hello'
hello: String, // <- auto-implement getter for 'hello'
}

impl Foo {
pub fn set_hello(&mut self, value: String) {
self.hello = if self.baz {
let mut s = self.hello.clone();
s.push_str(&value);
for _ in 0..self.bar {
s.push('!');
}
s
} else {
value
};
}
}

let mut engine = Engine::new();
engine.build_type::<Foo>();

let mut scope = Scope::new();
scope.push(
"foo",
Foo {
dummy: 0,
bar: 5,
baz: false,
hello: "hey".to_string(),
},
);

assert_eq!(
engine
.eval_with_scope::<Foo>(
&mut scope,
r#"
foo.hello = "this should not be seen";
foo.hello = "world!";
foo.emphasize = true;
foo.hello = "yo";
foo
"#
)
.unwrap(),
Foo {
dummy: 0,
bar: 5,
baz: true,
hello: "world!yo!!!!!".into()
}
);
}
Loading