bevy_reflect: Generic parameter info (#15475)
# Objective
Currently, reflecting a generic type provides no information about the
generic parameters. This means that you can't get access to the type of
`T` in `Foo<T>` without creating custom type data (we do this for
[`ReflectHandle`](https://docs.rs/bevy/0.14.2/bevy/asset/struct.ReflectHandle.html#method.asset_type_id)).
## Solution
This PR makes it so that generic type parameters and generic const
parameters are tracked in a `Generics` struct stored on the `TypeInfo`
for a type.
For example, `struct Foo<T, const N: usize>` will store `T` and `N` as a
`TypeParamInfo` and `ConstParamInfo`, respectively.
The stored information includes:
- The name of the generic parameter (i.e. `T`, `N`, etc.)
- The type of the generic parameter (remember that we're dealing with
monomorphized types, so this will actually be a concrete type)
- The default type/value, if any (e.g. `f32` in `T = f32` or `10` in
`const N: usize = 10`)
### Caveats
The only requirement for this to work is that the user does not opt-out
of the automatic `TypePath` derive with `#[reflect(type_path = false)]`.
Doing so prevents the macro code from 100% knowing that the generic type
implements `TypePath`. This in turn means the generated `Typed` impl
can't add generics to the type.
There are two solutions for this—both of which I think we should explore
in a future PR:
1. We could just not use `TypePath`. This would mean that we can't store
the `Type` of the generic, but we can at least store the `TypeId`.
2. We could provide a way to opt out of the automatic `Typed` derive
with a `#[reflect(typed = false)]` attribute. This would allow users to
manually implement `Typed` to add whatever generic information they need
(e.g. skipping a parameter that can't implement `TypePath` while the
rest can).
I originally thought about making `Generics` an enum with `Generic`,
`NonGeneric`, and `Unavailable` variants to signify whether there are
generics, no generics, or generics that cannot be added due to opting
out of `TypePath`. I ultimately decided against this as I think it adds
a bit too much complexity for such an uncommon problem.
Additionally, user's don't necessarily _have_ to know the generics of a
type, so just skipping them should generally be fine for now.
## Testing
You can test locally by running:
```
cargo test --package bevy_reflect
```
---
## Showcase
You can now access generic parameters via `TypeInfo`!
```rust
#[derive(Reflect)]
struct MyStruct<T, const N: usize>([T; N]);
let generics = MyStruct::<f32, 10>::type_info().generics();
// Get by index:
let t = generics.get(0).unwrap();
assert_eq!(t.name(), "T");
assert!(t.ty().is::<f32>());
assert!(!t.is_const());
// Or by name:
let n = generics.get_named("N").unwrap();
assert_eq!(n.name(), "N");
assert!(n.ty().is::<usize>());
assert!(n.is_const());
```
You can even access parameter defaults:
```rust
#[derive(Reflect)]
struct MyStruct<T = String, const N: usize = 10>([T; N]);
let generics = MyStruct::<f32, 5>::type_info().generics();
let GenericInfo::Type(info) = generics.get_named("T").unwrap() else {
panic!("expected a type parameter");
};
let default = info.default().unwrap();
assert!(default.is::<String>());
let GenericInfo::Const(info) = generics.get_named("N").unwrap() else {
panic!("expected a const parameter");
};
let default = info.default().unwrap();
assert_eq!(default.downcast_ref::<usize>().unwrap(), &10);
```
2024-09-30 17:58:37 +00:00
|
|
|
use crate::derive_data::ReflectMeta;
|
|
|
|
use proc_macro2::TokenStream;
|
|
|
|
use quote::quote;
|
|
|
|
use syn::punctuated::Punctuated;
|
|
|
|
use syn::{GenericParam, Token};
|
|
|
|
|
|
|
|
/// Creates a `TokenStream` for generating an expression that creates a `Generics` instance.
|
|
|
|
///
|
|
|
|
/// Returns `None` if `Generics` cannot or should not be generated.
|
|
|
|
pub(crate) fn generate_generics(meta: &ReflectMeta) -> Option<TokenStream> {
|
|
|
|
if !meta.attrs().type_path_attrs().should_auto_derive() {
|
|
|
|
// Cannot verify that all generic parameters implement `TypePath`
|
|
|
|
return None;
|
|
|
|
}
|
|
|
|
|
|
|
|
let bevy_reflect_path = meta.bevy_reflect_path();
|
|
|
|
|
|
|
|
let generics = meta
|
|
|
|
.type_path()
|
|
|
|
.generics()
|
|
|
|
.params
|
|
|
|
.iter()
|
|
|
|
.filter_map(|param| match param {
|
|
|
|
GenericParam::Type(ty_param) => {
|
|
|
|
let ident = &ty_param.ident;
|
|
|
|
let name = ident.to_string();
|
|
|
|
let with_default = ty_param
|
|
|
|
.default
|
|
|
|
.as_ref()
|
|
|
|
.map(|default_ty| quote!(.with_default::<#default_ty>()));
|
|
|
|
|
|
|
|
Some(quote! {
|
|
|
|
#bevy_reflect_path::GenericInfo::Type(
|
|
|
|
#bevy_reflect_path::TypeParamInfo::new::<#ident>(
|
2024-12-05 21:15:21 +00:00
|
|
|
#bevy_reflect_path::__macro_exports::alloc_utils::Cow::Borrowed(#name),
|
bevy_reflect: Generic parameter info (#15475)
# Objective
Currently, reflecting a generic type provides no information about the
generic parameters. This means that you can't get access to the type of
`T` in `Foo<T>` without creating custom type data (we do this for
[`ReflectHandle`](https://docs.rs/bevy/0.14.2/bevy/asset/struct.ReflectHandle.html#method.asset_type_id)).
## Solution
This PR makes it so that generic type parameters and generic const
parameters are tracked in a `Generics` struct stored on the `TypeInfo`
for a type.
For example, `struct Foo<T, const N: usize>` will store `T` and `N` as a
`TypeParamInfo` and `ConstParamInfo`, respectively.
The stored information includes:
- The name of the generic parameter (i.e. `T`, `N`, etc.)
- The type of the generic parameter (remember that we're dealing with
monomorphized types, so this will actually be a concrete type)
- The default type/value, if any (e.g. `f32` in `T = f32` or `10` in
`const N: usize = 10`)
### Caveats
The only requirement for this to work is that the user does not opt-out
of the automatic `TypePath` derive with `#[reflect(type_path = false)]`.
Doing so prevents the macro code from 100% knowing that the generic type
implements `TypePath`. This in turn means the generated `Typed` impl
can't add generics to the type.
There are two solutions for this—both of which I think we should explore
in a future PR:
1. We could just not use `TypePath`. This would mean that we can't store
the `Type` of the generic, but we can at least store the `TypeId`.
2. We could provide a way to opt out of the automatic `Typed` derive
with a `#[reflect(typed = false)]` attribute. This would allow users to
manually implement `Typed` to add whatever generic information they need
(e.g. skipping a parameter that can't implement `TypePath` while the
rest can).
I originally thought about making `Generics` an enum with `Generic`,
`NonGeneric`, and `Unavailable` variants to signify whether there are
generics, no generics, or generics that cannot be added due to opting
out of `TypePath`. I ultimately decided against this as I think it adds
a bit too much complexity for such an uncommon problem.
Additionally, user's don't necessarily _have_ to know the generics of a
type, so just skipping them should generally be fine for now.
## Testing
You can test locally by running:
```
cargo test --package bevy_reflect
```
---
## Showcase
You can now access generic parameters via `TypeInfo`!
```rust
#[derive(Reflect)]
struct MyStruct<T, const N: usize>([T; N]);
let generics = MyStruct::<f32, 10>::type_info().generics();
// Get by index:
let t = generics.get(0).unwrap();
assert_eq!(t.name(), "T");
assert!(t.ty().is::<f32>());
assert!(!t.is_const());
// Or by name:
let n = generics.get_named("N").unwrap();
assert_eq!(n.name(), "N");
assert!(n.ty().is::<usize>());
assert!(n.is_const());
```
You can even access parameter defaults:
```rust
#[derive(Reflect)]
struct MyStruct<T = String, const N: usize = 10>([T; N]);
let generics = MyStruct::<f32, 5>::type_info().generics();
let GenericInfo::Type(info) = generics.get_named("T").unwrap() else {
panic!("expected a type parameter");
};
let default = info.default().unwrap();
assert!(default.is::<String>());
let GenericInfo::Const(info) = generics.get_named("N").unwrap() else {
panic!("expected a const parameter");
};
let default = info.default().unwrap();
assert_eq!(default.downcast_ref::<usize>().unwrap(), &10);
```
2024-09-30 17:58:37 +00:00
|
|
|
)
|
|
|
|
#with_default
|
|
|
|
)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
GenericParam::Const(const_param) => {
|
|
|
|
let ty = &const_param.ty;
|
|
|
|
let name = const_param.ident.to_string();
|
|
|
|
let with_default = const_param.default.as_ref().map(|default| {
|
|
|
|
// We add the `as #ty` to ensure that the correct type is inferred.
|
|
|
|
quote!(.with_default(#default as #ty))
|
|
|
|
});
|
|
|
|
|
|
|
|
Some(quote! {
|
|
|
|
#[allow(
|
|
|
|
clippy::unnecessary_cast,
|
|
|
|
reason = "reflection requires an explicit type hint for const generics"
|
|
|
|
)]
|
|
|
|
#bevy_reflect_path::GenericInfo::Const(
|
|
|
|
#bevy_reflect_path::ConstParamInfo::new::<#ty>(
|
2024-12-05 21:15:21 +00:00
|
|
|
#bevy_reflect_path::__macro_exports::alloc_utils::Cow::Borrowed(#name),
|
bevy_reflect: Generic parameter info (#15475)
# Objective
Currently, reflecting a generic type provides no information about the
generic parameters. This means that you can't get access to the type of
`T` in `Foo<T>` without creating custom type data (we do this for
[`ReflectHandle`](https://docs.rs/bevy/0.14.2/bevy/asset/struct.ReflectHandle.html#method.asset_type_id)).
## Solution
This PR makes it so that generic type parameters and generic const
parameters are tracked in a `Generics` struct stored on the `TypeInfo`
for a type.
For example, `struct Foo<T, const N: usize>` will store `T` and `N` as a
`TypeParamInfo` and `ConstParamInfo`, respectively.
The stored information includes:
- The name of the generic parameter (i.e. `T`, `N`, etc.)
- The type of the generic parameter (remember that we're dealing with
monomorphized types, so this will actually be a concrete type)
- The default type/value, if any (e.g. `f32` in `T = f32` or `10` in
`const N: usize = 10`)
### Caveats
The only requirement for this to work is that the user does not opt-out
of the automatic `TypePath` derive with `#[reflect(type_path = false)]`.
Doing so prevents the macro code from 100% knowing that the generic type
implements `TypePath`. This in turn means the generated `Typed` impl
can't add generics to the type.
There are two solutions for this—both of which I think we should explore
in a future PR:
1. We could just not use `TypePath`. This would mean that we can't store
the `Type` of the generic, but we can at least store the `TypeId`.
2. We could provide a way to opt out of the automatic `Typed` derive
with a `#[reflect(typed = false)]` attribute. This would allow users to
manually implement `Typed` to add whatever generic information they need
(e.g. skipping a parameter that can't implement `TypePath` while the
rest can).
I originally thought about making `Generics` an enum with `Generic`,
`NonGeneric`, and `Unavailable` variants to signify whether there are
generics, no generics, or generics that cannot be added due to opting
out of `TypePath`. I ultimately decided against this as I think it adds
a bit too much complexity for such an uncommon problem.
Additionally, user's don't necessarily _have_ to know the generics of a
type, so just skipping them should generally be fine for now.
## Testing
You can test locally by running:
```
cargo test --package bevy_reflect
```
---
## Showcase
You can now access generic parameters via `TypeInfo`!
```rust
#[derive(Reflect)]
struct MyStruct<T, const N: usize>([T; N]);
let generics = MyStruct::<f32, 10>::type_info().generics();
// Get by index:
let t = generics.get(0).unwrap();
assert_eq!(t.name(), "T");
assert!(t.ty().is::<f32>());
assert!(!t.is_const());
// Or by name:
let n = generics.get_named("N").unwrap();
assert_eq!(n.name(), "N");
assert!(n.ty().is::<usize>());
assert!(n.is_const());
```
You can even access parameter defaults:
```rust
#[derive(Reflect)]
struct MyStruct<T = String, const N: usize = 10>([T; N]);
let generics = MyStruct::<f32, 5>::type_info().generics();
let GenericInfo::Type(info) = generics.get_named("T").unwrap() else {
panic!("expected a type parameter");
};
let default = info.default().unwrap();
assert!(default.is::<String>());
let GenericInfo::Const(info) = generics.get_named("N").unwrap() else {
panic!("expected a const parameter");
};
let default = info.default().unwrap();
assert_eq!(default.downcast_ref::<usize>().unwrap(), &10);
```
2024-09-30 17:58:37 +00:00
|
|
|
)
|
|
|
|
#with_default
|
|
|
|
)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
GenericParam::Lifetime(_) => None,
|
|
|
|
})
|
|
|
|
.collect::<Punctuated<_, Token![,]>>();
|
|
|
|
|
|
|
|
if generics.is_empty() {
|
|
|
|
// No generics to generate
|
|
|
|
return None;
|
|
|
|
}
|
|
|
|
|
|
|
|
Some(quote!(#bevy_reflect_path::Generics::from_iter([ #generics ])))
|
|
|
|
}
|