Skip to content

Commit

Permalink
bevy_reflect: Generic parameter info (bevyengine#15475)
Browse files Browse the repository at this point in the history
# 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);
```
  • Loading branch information
MrGVSV authored and robtfm committed Oct 4, 2024
1 parent e0d7c4d commit 535805a
Show file tree
Hide file tree
Showing 17 changed files with 564 additions and 52 deletions.
27 changes: 13 additions & 14 deletions crates/bevy_reflect/derive/src/derive_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use crate::{
use quote::{quote, ToTokens};
use syn::token::Comma;

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

#[cfg_attr(
not(feature = "documentation"),
expect(
unused_mut,
reason = "Needs to be mutable if `documentation` feature is enabled.",
)
)]
let mut info = quote! {
#bevy_reflect_path::#info_struct::new::<Self>(&[
#(#field_infos),*
])
.with_custom_attributes(#custom_attributes)
};

if let Some(generics) = generate_generics(self.meta()) {
info.extend(quote! {
.with_generics(#generics)
});
}

#[cfg(feature = "documentation")]
{
let docs = self.meta().doc();
Expand Down Expand Up @@ -730,20 +730,19 @@ impl<'a> ReflectEnum<'a> {
.custom_attributes()
.to_tokens(bevy_reflect_path);

#[cfg_attr(
not(feature = "documentation"),
expect(
unused_mut,
reason = "Needs to be mutable if `documentation` feature is enabled.",
)
)]
let mut info = quote! {
#bevy_reflect_path::EnumInfo::new::<Self>(&[
#(#variants),*
])
.with_custom_attributes(#custom_attributes)
};

if let Some(generics) = generate_generics(self.meta()) {
info.extend(quote! {
.with_generics(#generics)
});
}

#[cfg(feature = "documentation")]
{
let docs = self.meta().doc();
Expand Down
72 changes: 72 additions & 0 deletions crates/bevy_reflect/derive/src/generics.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
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>(
::std::borrow::Cow::Borrowed(#name),
)
#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>(
::std::borrow::Cow::Borrowed(#name),
)
#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 ])))
}
1 change: 1 addition & 0 deletions crates/bevy_reflect/derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ mod documentation;
mod enum_utility;
mod field_attributes;
mod from_reflect;
mod generics;
mod ident;
mod impls;
mod meta;
Expand Down
9 changes: 7 additions & 2 deletions crates/bevy_reflect/src/array.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use crate::generics::impl_generic_info_methods;
use crate::{
self as bevy_reflect, type_info::impl_type_methods, utility::reflect_hasher, ApplyError,
MaybeTyped, PartialReflect, Reflect, ReflectKind, ReflectMut, ReflectOwned, ReflectRef, Type,
TypeInfo, TypePath,
Generics, MaybeTyped, PartialReflect, Reflect, ReflectKind, ReflectMut, ReflectOwned,
ReflectRef, Type, TypeInfo, TypePath,
};
use bevy_reflect_derive::impl_type_path;
use core::{
Expand Down Expand Up @@ -79,6 +80,7 @@ pub trait Array: PartialReflect {
#[derive(Clone, Debug)]
pub struct ArrayInfo {
ty: Type,
generics: Generics,
item_info: fn() -> Option<&'static TypeInfo>,
item_ty: Type,
capacity: usize,
Expand All @@ -97,6 +99,7 @@ impl ArrayInfo {
) -> Self {
Self {
ty: Type::of::<TArray>(),
generics: Generics::new(),
item_info: TItem::maybe_type_info,
item_ty: Type::of::<TItem>(),
capacity,
Expand Down Expand Up @@ -138,6 +141,8 @@ impl ArrayInfo {
pub fn docs(&self) -> Option<&'static str> {
self.docs
}

impl_generic_info_methods!(generics);
}

/// A fixed-size list of reflected values.
Expand Down
7 changes: 6 additions & 1 deletion crates/bevy_reflect/src/enums/enum_trait.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use crate::generics::impl_generic_info_methods;
use crate::{
attributes::{impl_custom_attribute_methods, CustomAttributes},
type_info::impl_type_methods,
DynamicEnum, PartialReflect, Type, TypePath, VariantInfo, VariantType,
DynamicEnum, Generics, PartialReflect, Type, TypePath, VariantInfo, VariantType,
};
use alloc::sync::Arc;
use bevy_utils::HashMap;
Expand Down Expand Up @@ -138,6 +139,7 @@ pub trait Enum: PartialReflect {
#[derive(Clone, Debug)]
pub struct EnumInfo {
ty: Type,
generics: Generics,
variants: Box<[VariantInfo]>,
variant_names: Box<[&'static str]>,
variant_indices: HashMap<&'static str, usize>,
Expand All @@ -163,6 +165,7 @@ impl EnumInfo {

Self {
ty: Type::of::<TEnum>(),
generics: Generics::new(),
variants: variants.to_vec().into_boxed_slice(),
variant_names,
variant_indices,
Expand Down Expand Up @@ -239,6 +242,8 @@ impl EnumInfo {
}

impl_custom_attribute_methods!(self.custom_attributes, "enum");

impl_generic_info_methods!(generics);
}

/// An iterator over the fields in the current enum variant.
Expand Down
Loading

0 comments on commit 535805a

Please sign in to comment.