Skip to content

Commit 397f20e

Browse files
authored
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); ```
1 parent 8bcda3d commit 397f20e

File tree

17 files changed

+564
-52
lines changed

17 files changed

+564
-52
lines changed

crates/bevy_reflect/derive/src/derive_data.rs

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use crate::{
1515
use quote::{quote, ToTokens};
1616
use syn::token::Comma;
1717

18+
use crate::generics::generate_generics;
1819
use syn::{
1920
parse_str, punctuated::Punctuated, spanned::Spanned, Data, DeriveInput, Field, Fields,
2021
GenericParam, Generics, Ident, LitStr, Meta, Path, PathSegment, Type, TypeParam, Variant,
@@ -627,20 +628,19 @@ impl<'a> ReflectStruct<'a> {
627628
.custom_attributes()
628629
.to_tokens(bevy_reflect_path);
629630

630-
#[cfg_attr(
631-
not(feature = "documentation"),
632-
expect(
633-
unused_mut,
634-
reason = "Needs to be mutable if `documentation` feature is enabled.",
635-
)
636-
)]
637631
let mut info = quote! {
638632
#bevy_reflect_path::#info_struct::new::<Self>(&[
639633
#(#field_infos),*
640634
])
641635
.with_custom_attributes(#custom_attributes)
642636
};
643637

638+
if let Some(generics) = generate_generics(self.meta()) {
639+
info.extend(quote! {
640+
.with_generics(#generics)
641+
});
642+
}
643+
644644
#[cfg(feature = "documentation")]
645645
{
646646
let docs = self.meta().doc();
@@ -730,20 +730,19 @@ impl<'a> ReflectEnum<'a> {
730730
.custom_attributes()
731731
.to_tokens(bevy_reflect_path);
732732

733-
#[cfg_attr(
734-
not(feature = "documentation"),
735-
expect(
736-
unused_mut,
737-
reason = "Needs to be mutable if `documentation` feature is enabled.",
738-
)
739-
)]
740733
let mut info = quote! {
741734
#bevy_reflect_path::EnumInfo::new::<Self>(&[
742735
#(#variants),*
743736
])
744737
.with_custom_attributes(#custom_attributes)
745738
};
746739

740+
if let Some(generics) = generate_generics(self.meta()) {
741+
info.extend(quote! {
742+
.with_generics(#generics)
743+
});
744+
}
745+
747746
#[cfg(feature = "documentation")]
748747
{
749748
let docs = self.meta().doc();
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
use crate::derive_data::ReflectMeta;
2+
use proc_macro2::TokenStream;
3+
use quote::quote;
4+
use syn::punctuated::Punctuated;
5+
use syn::{GenericParam, Token};
6+
7+
/// Creates a `TokenStream` for generating an expression that creates a `Generics` instance.
8+
///
9+
/// Returns `None` if `Generics` cannot or should not be generated.
10+
pub(crate) fn generate_generics(meta: &ReflectMeta) -> Option<TokenStream> {
11+
if !meta.attrs().type_path_attrs().should_auto_derive() {
12+
// Cannot verify that all generic parameters implement `TypePath`
13+
return None;
14+
}
15+
16+
let bevy_reflect_path = meta.bevy_reflect_path();
17+
18+
let generics = meta
19+
.type_path()
20+
.generics()
21+
.params
22+
.iter()
23+
.filter_map(|param| match param {
24+
GenericParam::Type(ty_param) => {
25+
let ident = &ty_param.ident;
26+
let name = ident.to_string();
27+
let with_default = ty_param
28+
.default
29+
.as_ref()
30+
.map(|default_ty| quote!(.with_default::<#default_ty>()));
31+
32+
Some(quote! {
33+
#bevy_reflect_path::GenericInfo::Type(
34+
#bevy_reflect_path::TypeParamInfo::new::<#ident>(
35+
::std::borrow::Cow::Borrowed(#name),
36+
)
37+
#with_default
38+
)
39+
})
40+
}
41+
GenericParam::Const(const_param) => {
42+
let ty = &const_param.ty;
43+
let name = const_param.ident.to_string();
44+
let with_default = const_param.default.as_ref().map(|default| {
45+
// We add the `as #ty` to ensure that the correct type is inferred.
46+
quote!(.with_default(#default as #ty))
47+
});
48+
49+
Some(quote! {
50+
#[allow(
51+
clippy::unnecessary_cast,
52+
reason = "reflection requires an explicit type hint for const generics"
53+
)]
54+
#bevy_reflect_path::GenericInfo::Const(
55+
#bevy_reflect_path::ConstParamInfo::new::<#ty>(
56+
::std::borrow::Cow::Borrowed(#name),
57+
)
58+
#with_default
59+
)
60+
})
61+
}
62+
GenericParam::Lifetime(_) => None,
63+
})
64+
.collect::<Punctuated<_, Token![,]>>();
65+
66+
if generics.is_empty() {
67+
// No generics to generate
68+
return None;
69+
}
70+
71+
Some(quote!(#bevy_reflect_path::Generics::from_iter([ #generics ])))
72+
}

crates/bevy_reflect/derive/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ mod documentation;
2525
mod enum_utility;
2626
mod field_attributes;
2727
mod from_reflect;
28+
mod generics;
2829
mod ident;
2930
mod impls;
3031
mod meta;

crates/bevy_reflect/src/array.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
use crate::generics::impl_generic_info_methods;
12
use crate::{
23
self as bevy_reflect, type_info::impl_type_methods, utility::reflect_hasher, ApplyError,
3-
MaybeTyped, PartialReflect, Reflect, ReflectKind, ReflectMut, ReflectOwned, ReflectRef, Type,
4-
TypeInfo, TypePath,
4+
Generics, MaybeTyped, PartialReflect, Reflect, ReflectKind, ReflectMut, ReflectOwned,
5+
ReflectRef, Type, TypeInfo, TypePath,
56
};
67
use bevy_reflect_derive::impl_type_path;
78
use core::{
@@ -79,6 +80,7 @@ pub trait Array: PartialReflect {
7980
#[derive(Clone, Debug)]
8081
pub struct ArrayInfo {
8182
ty: Type,
83+
generics: Generics,
8284
item_info: fn() -> Option<&'static TypeInfo>,
8385
item_ty: Type,
8486
capacity: usize,
@@ -97,6 +99,7 @@ impl ArrayInfo {
9799
) -> Self {
98100
Self {
99101
ty: Type::of::<TArray>(),
102+
generics: Generics::new(),
100103
item_info: TItem::maybe_type_info,
101104
item_ty: Type::of::<TItem>(),
102105
capacity,
@@ -138,6 +141,8 @@ impl ArrayInfo {
138141
pub fn docs(&self) -> Option<&'static str> {
139142
self.docs
140143
}
144+
145+
impl_generic_info_methods!(generics);
141146
}
142147

143148
/// A fixed-size list of reflected values.

crates/bevy_reflect/src/enums/enum_trait.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
use crate::generics::impl_generic_info_methods;
12
use crate::{
23
attributes::{impl_custom_attribute_methods, CustomAttributes},
34
type_info::impl_type_methods,
4-
DynamicEnum, PartialReflect, Type, TypePath, VariantInfo, VariantType,
5+
DynamicEnum, Generics, PartialReflect, Type, TypePath, VariantInfo, VariantType,
56
};
67
use alloc::sync::Arc;
78
use bevy_utils::HashMap;
@@ -138,6 +139,7 @@ pub trait Enum: PartialReflect {
138139
#[derive(Clone, Debug)]
139140
pub struct EnumInfo {
140141
ty: Type,
142+
generics: Generics,
141143
variants: Box<[VariantInfo]>,
142144
variant_names: Box<[&'static str]>,
143145
variant_indices: HashMap<&'static str, usize>,
@@ -163,6 +165,7 @@ impl EnumInfo {
163165

164166
Self {
165167
ty: Type::of::<TEnum>(),
168+
generics: Generics::new(),
166169
variants: variants.to_vec().into_boxed_slice(),
167170
variant_names,
168171
variant_indices,
@@ -239,6 +242,8 @@ impl EnumInfo {
239242
}
240243

241244
impl_custom_attribute_methods!(self.custom_attributes, "enum");
245+
246+
impl_generic_info_methods!(generics);
242247
}
243248

244249
/// An iterator over the fields in the current enum variant.

0 commit comments

Comments
 (0)