Add PhantomData<T> support for the GodotConvert derive macro#1619
Add PhantomData<T> support for the GodotConvert derive macro#1619ProphetOSpam wants to merge 6 commits into
Conversation
|
Also, right now this only supports PhantomData with structs; enums will still error in the same way. I think there probably should be support for enums as well; What are your thoughts? |
There was a problem hiding this comment.
Thanks for your contribution! 🙂
Would be interesting to hear about your use case -- where did you need this? Possibly an example could also be briefly mentioned in docs (no need for code).
I think there probably should be support for enums as well; What are your thoughts?
Let's not add features unless we need them. This PR already adds some complexity for a very niche feature, which also needs to be maintained -- hence my question about use case above 😉
Additionally, as a consequence of the changes, you can now have generics on non-PhantomData fields as well, like:
I don't see something that speaks against it, does the generated code do what's expected? Maybe a short itest would be nice.
Your implementation hardcodes PhantomData as an identifier. Could you elaborate this design choice? AFAIR the original idea was to allow any ZST members, but the hardcoding has multiple problems:
- It doesn't allow
MyGhostData<T>(PhantomData<T>). - It breaks if
PhantomDatais renamed on import or type-aliased (edge case). - It breaks if another type is named
PhantomData(very edge case).
Not huge deals, but those are inherent limitations of proc-macros operating on the syntax level.
| // being used. These error messages are a bit more clear with our intentions, | ||
| // though. | ||
|
|
||
| if let Some(generic_params) = &enum_.generic_params { |
There was a problem hiding this comment.
Shorter, no empty line:
| if let Some(generic_params) = &enum_.generic_params { | |
| // We only have C-style enums, so Rust would already complain that generics are unused. | |
| // This provides clearer error messages though. |
| } | ||
|
|
||
| let data = ConvertType::parse_declaration(item)?; | ||
| let data = ConvertType::parse_declaration(item.clone())?; |
There was a problem hiding this comment.
Is it possible to avoid this clone by changing the parameter to take a shared-ref? Might propagate to other code that accesses it.
Or would it require an internal clone()? In that case there's no point in refactoring.
| ty_name: name, | ||
| where_clause: where_clause.clone(), | ||
| generic_params: generic_params.clone(), | ||
| convert_type: data, |
There was a problem hiding this comment.
Also here, if item doesn't need to be cloned, it could just be destructured, and we wouldn't need to needlessly copy around these parts.
| /// | ||
| /// If `None`, then this represents a tuple-struct with one field. | ||
| pub name: Option<Ident>, | ||
| pub name: FieldType, |
There was a problem hiding this comment.
"name" vs. "field type". One of the two is badly named.
Also, unrelated to your changes -- I noticed that we call the type NewtypeStruct, which is a bit confusing given that newtypes typically have shape Struct(InnerType). Could you add an extra /// line for the struct to explain that it allows multiple named or tuple fields, as long as all but one are ZSTs?
| fn phantom_predicate(field: &TupleField) -> bool { | ||
| // Some types we don't care about are not paths, like references | ||
| if let Some(path) = field.ty.as_path() { | ||
| // This unwrap only fails if the field had no type specified, which isn't valid code anyways. | ||
| return path.segments.last().unwrap().ident | ||
| == Ident::new("PhantomData", Span::mixed_site()); | ||
| } | ||
| false | ||
| } |
There was a problem hiding this comment.
phantom_predicate is rather unclear. Why not simply is_phantom_data?
Also, can you use this maybe?
gdext/godot-macros/src/util/mod.rs
Lines 247 to 253 in 1a9e41f
And maybe more generally, checking on the syntax level against types is brittle, and we can't easily cover edge cases. Do we have to do it?
| fn phantom_predicate(field: &NamedField) -> bool { | ||
| // Some types we don't care about are not paths, like references | ||
| if let Some(path) = field.ty.as_path() { | ||
| // This unwrap only fails if the field had no type specified, which isn't valid code anyways. | ||
| return path.segments.last().unwrap().ident | ||
| == Ident::new("PhantomData", Span::mixed_site()); | ||
| } | ||
| false | ||
| } | ||
|
|
||
| let mut non_phantom_fields = fields | ||
| .fields | ||
| .items() | ||
| .filter(|field| !phantom_predicate(field)); | ||
|
|
||
| let maybe_field = non_phantom_fields.next(); | ||
|
|
||
| let total_count = if maybe_field.is_none() { | ||
| 0 | ||
| } else { | ||
| non_phantom_fields.count() + 1 | ||
| }; | ||
|
|
||
| if total_count != 1 { | ||
| return bail!( | ||
| &fields.fields, | ||
| "GodotConvert expects a struct with a single field, not {} fields", | ||
| fields.fields.len() | ||
| "GodotConvert expects a struct with a single non-PhantomData field, not {} fields", | ||
| total_count | ||
| ); | ||
| } |
| // If we're in here we have at least 1 bound, and rust doesn't error if the | ||
| // bound is already 'static, i.e. `T: 'static + 'static` works. It's a | ||
| // little hacky, but there's not really a reason to inspect all of the | ||
| // bound's tokens if we really don't care what it is. | ||
| bound | ||
| .tokens | ||
| .append(&mut quote! {+ 'static}.into_iter().collect()); |
There was a problem hiding this comment.
You explain the hack but not why it's necessary.
Also nitpick, please use available 120-145 chars per line.
|
API docs are being generated and will be shortly available at: https://godot-rust.github.io/docs/gdext/pr-1619 |
In my project I have an This: pub struct Id<T>(u32, Phantom<T>);
#[derive(GodotConvert)]
pub struct GId(u32);
impl<T> From<Id<T>> for GId {
fn from(value: Id<T>) -> GId {
Self(value.0)
}
}
impl<T> From<GId> for Id<T> {
fn from(value: GId) -> Id<T> {
Self(value.0, PhantomData)
}
}becomes this: #[derive(GodotConvert)]
pub struct Id<T>(u32, Phantom<T>);Additionally, I use pub struct CardDef {
...
}
pub struct Card {
pub def_id: Id<CardDef>,
...
}These cards can be created in rust or through godot as well (I make these GodotClasses), and it would have to be a lot more boilerplate in order to create a #[derive(GodotClass)]
#[class(no_init)]
pub struct GCard {
#[var]
pub def_id: GId,
...
}
// More `From` impls
...Something I just realized is that in order to do what I was saying above there needs to be PhantomData handling for I'll add a summary of the above as an example for the relevant documentation.
Oki doki, sounds good. I don't think there's an alternative to the above solution (if there is, I'd be happy to know!).
I believe it does. If you look here, there's an example with
You are very correct. It should not be checking for the name |
|
Actually, we need to know what field is the target of the newtype for creating the struct in like: type Ghost<T> = PhantomData<T>;
struct MyStruct<T, U>(
PhantomData<T>,
#[target]
GString,
Ghost<T>,
// The following field would error because its not ZST
bool
); |
|
I'm going to rewrite it to include const assertions instead of looking at PhantomData directly, so I left the comments pertaining to that unaddressed, as they'll be irrelevant by then. |
|
Actually, instead of a like: type Ghost<T> = PhantomData<T>;
struct MyStruct<T, U>(
#[ignore]
PhantomData<T>,
GString,
#[ignore]
Ghost<T>,
) |
|
Also, I won't be able to work much on this for the next 2 weeks, but I will see it through |
Technically it's still backwards-compatible, because we didn't support more than 1 field so far. But it's usually nicer when adding a field, to also add the attribute along with it, rather than needing to change a different field. I would recommend Another thought I had whether we should scope this to |
Fix for #1563
You can now do stuff like:
Additionally, as a consequence of the changes, you can now have generics on non-PhantomData fields as well, like:
I'm not sure this is intended behavior, however. I can't see a reason why not, but I'd like clarification. If it isn't I'll go add a check to make sure you can't use generics on non-PhantomData fields.