Skip to content

Conversation

MrGVSV
Copy link
Member

@MrGVSV MrGVSV commented Sep 27, 2024

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).

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, users 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!

#[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:

#[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);

@MrGVSV MrGVSV added C-Feature A new feature, making something new possible A-Reflection Runtime information about types D-Modest A "normal" level of difficulty; suitable for simple features or challenging fixes S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Sep 27, 2024
Copy link
Contributor

@aecsocket aecsocket left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, just some questions

Copy link
Contributor

@pablo-lua pablo-lua left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice API for knowing Generics of a type, LGTM

@pablo-lua pablo-lua added S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it and removed S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Sep 28, 2024
@alice-i-cecile alice-i-cecile added the M-Needs-Release-Note Work that should be called out in the blog due to impact label Sep 30, 2024
@alice-i-cecile alice-i-cecile added this pull request to the merge queue Sep 30, 2024
Merged via the queue into bevyengine:main with commit 397f20e Sep 30, 2024
28 checks passed
robtfm pushed a commit to robtfm/bevy that referenced this pull request Oct 4, 2024
# 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);
```
@alice-i-cecile
Copy link
Member

Thank you to everyone involved with the authoring or reviewing of this PR! This work is relatively important and needs release notes! Head over to bevyengine/bevy-website#1705 if you'd like to help out.

@MrGVSV MrGVSV deleted the mrgvsv/reflect/generic-type-info branch October 24, 2024 04:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-Reflection Runtime information about types C-Feature A new feature, making something new possible D-Modest A "normal" level of difficulty; suitable for simple features or challenging fixes M-Needs-Release-Note Work that should be called out in the blog due to impact S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it
Projects
Status: In Progress
Development

Successfully merging this pull request may close these issues.

4 participants