Skip to content

Commit 6e959db

Browse files
MrGVSVnicopap
andauthored
bevy_reflect: Type parameter bounds (#9046)
# Objective Fixes #8965. #### Background For convenience and to ensure everything is setup properly, we automatically add certain bounds to the derived types. The current implementation does this by taking the types from all active fields and adding them to the where-clause of the generated impls. I believe this method was chosen because it won't add bounds to types that are otherwise ignored. ```rust #[derive(Reflect)] struct Foo<T, U: SomeTrait, V> { t: T, u: U::Assoc, #[reflect(ignore)] v: [V; 2] } // Generates something like: impl<T, U: SomeTrait, V> for Foo<T, U, V> where // Active: T: Reflect, U::Assoc: Reflect, // Ignored: [V; 2]: Send + Sync + Any { // ... } ``` The self-referential type fails because it ends up using _itself_ as a type bound due to being one of its own active fields. ```rust #[derive(Reflect)] struct Foo { foo: Vec<Foo> } // Foo where Vec<Foo>: Reflect -> Vec<T> where T: Reflect -> Foo where Vec<Foo>: Reflect -> ... ``` ## Solution We can't simply parse all field types for the name of our type. That would be both complex and prone to errors and false-positives. And even if it wasn't, what would we replace the bound with? Instead, I opted to go for a solution that only adds the bounds to what really needs it: the type parameters. While the bounds on concrete types make errors a bit cleaner, they aren't strictly necessary. This means we can change our generated where-clause to only add bounds to generic type parameters. Doing this, though, returns us back to the problem of over-bounding parameters that don't need to be bounded. To solve this, I added a new container attribute (based on [this](dtolnay/syn#422 (comment)) comment and @nicopap's [comment](#9046 (comment))) that allows us to pass in a custom where clause to modify what bounds are added to these type parameters. This allows us to do stuff like: ```rust trait Trait { type Assoc; } // We don't need `T` to be reflectable since we only care about `T::Assoc`. #[derive(Reflect)] #[reflect(where T::Assoc: FromReflect)] struct Foo<T: Trait>(T::Assoc); #[derive(TypePath)] struct Bar; impl Trait for Bar { type Assoc = usize; } #[derive(Reflect)] struct Baz { a: Foo<Bar>, } ``` > **Note** > I also [tried](dc139ea) allowing `#[reflect(ignore)]` to be used on the type parameters themselves, but that proved problematic since the derive macro does not consume the attribute. This is why I went with the container attribute approach. ### Alternatives One alternative could possibly be to just not add reflection bounds automatically (i.e. only add required bounds like `Send`, `Sync`, `Any`, and `TypePath`). The downside here is we add more friction to using reflection, which already comes with its own set of considerations. This is a potentially viable option, but we really need to consider whether or not the ergonomics hit is worth it. If we did decide to go the more manual route, we should at least consider something like #5772 to make it easier for users to add the right bounds (although, this could still become tricky with `FromReflect` also being automatically derived). ### Open Questions 1. Should we go with this approach or the manual alternative? 2. ~~Should we add a `skip_params` attribute to avoid the `T: 'static` trick?~~ ~~Decided to go with `custom_where()` as it's the simplest~~ Scratch that, went with a normal where clause 3. ~~`custom_where` bikeshedding?~~ No longer needed since we are using a normal where clause ### TODO - [x] Add compile-fail tests --- ## Changelog - Fixed issue preventing recursive types from deriving `Reflect` - Changed how where-clause bounds are generated by the `Reflect` derive macro - They are now only applied to the type parameters, not to all active fields - Added `#[reflect(where T: Trait, U::Assoc: Trait, ...)]` container attribute ## Migration Guide When deriving `Reflect`, generic type params that do not need the automatic reflection bounds (such as `Reflect`) applied to them will need to opt-out using a custom where clause like: `#[reflect(where T: Trait, U::Assoc: Trait, ...)]`. The attribute can define custom bounds only used by the reflection impls. To simply opt-out all the type params, we can pass in an empty where clause: `#[reflect(where)]`. ```rust // BEFORE: #[derive(Reflect)] struct Foo<T>(#[reflect(ignore)] T); // AFTER: #[derive(Reflect)] #[reflect(where)] struct Foo<T>(#[reflect(ignore)] T); ``` --------- Co-authored-by: Nicola Papale <[email protected]>
1 parent 397d111 commit 6e959db

File tree

17 files changed

+447
-285
lines changed

17 files changed

+447
-285
lines changed

crates/bevy_asset/src/handle.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ impl std::fmt::Debug for StrongHandle {
122122
/// [`Handle::Strong`] also provides access to useful [`Asset`] metadata, such as the [`AssetPath`] (if it exists).
123123
#[derive(Component, Reflect)]
124124
#[reflect(Component)]
125+
#[reflect(where A: Asset)]
125126
pub enum Handle<A: Asset> {
126127
/// A "strong" reference to a live (or loading) [`Asset`]. If a [`Handle`] is [`Handle::Strong`], the [`Asset`] will be kept
127128
/// alive until the [`Handle`] is dropped. Strong handles also provide access to additional asset metadata.

crates/bevy_asset/src/id.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ use thiserror::Error;
1717
///
1818
/// For an "untyped" / "generic-less" id, see [`UntypedAssetId`].
1919
#[derive(Reflect)]
20+
#[reflect(where A: Asset)]
2021
pub enum AssetId<A: Asset> {
2122
/// A small / efficient runtime identifier that can be used to efficiently look up an asset stored in [`Assets`]. This is
2223
/// the "default" identifier used for assets. The alternative(s) (ex: [`AssetId::Uuid`]) will only be used if assets are

crates/bevy_reflect/bevy_reflect_derive/src/container_attributes.rs

Lines changed: 70 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@
77
88
use crate::utility;
99
use bevy_macro_utils::fq_std::{FQAny, FQOption};
10-
use proc_macro2::{Ident, Span};
10+
use proc_macro2::{Ident, Span, TokenTree};
1111
use quote::quote_spanned;
1212
use syn::parse::{Parse, ParseStream};
1313
use syn::punctuated::Punctuated;
1414
use syn::spanned::Spanned;
1515
use syn::token::Comma;
16-
use syn::{Expr, LitBool, Meta, Path};
16+
use syn::{Expr, LitBool, Meta, MetaList, Path, WhereClause};
1717

1818
// The "special" trait idents that are used internally for reflection.
1919
// Received via attributes like `#[reflect(PartialEq, Hash, ...)]`
@@ -211,24 +211,40 @@ pub(crate) struct ReflectTraits {
211211
partial_eq: TraitImpl,
212212
from_reflect_attrs: FromReflectAttrs,
213213
type_path_attrs: TypePathAttrs,
214+
custom_where: Option<WhereClause>,
214215
idents: Vec<Ident>,
215216
}
216217

217218
impl ReflectTraits {
218-
pub fn from_metas(
219+
pub fn from_meta_list(
220+
meta: &MetaList,
221+
is_from_reflect_derive: bool,
222+
) -> Result<Self, syn::Error> {
223+
match meta.tokens.clone().into_iter().next() {
224+
// Handles `#[reflect(where T: Trait, U::Assoc: Trait)]`
225+
Some(TokenTree::Ident(ident)) if ident == "where" => Ok(Self {
226+
custom_where: Some(meta.parse_args::<WhereClause>()?),
227+
..Self::default()
228+
}),
229+
_ => Self::from_metas(
230+
meta.parse_args_with(Punctuated::<Meta, Comma>::parse_terminated)?,
231+
is_from_reflect_derive,
232+
),
233+
}
234+
}
235+
236+
fn from_metas(
219237
metas: Punctuated<Meta, Comma>,
220238
is_from_reflect_derive: bool,
221239
) -> Result<Self, syn::Error> {
222240
let mut traits = ReflectTraits::default();
223241
for meta in &metas {
224242
match meta {
225-
// Handles `#[reflect( Hash, Default, ... )]`
243+
// Handles `#[reflect( Debug, PartialEq, Hash, SomeTrait )]`
226244
Meta::Path(path) => {
227-
// Get the first ident in the path (hopefully the path only contains one and not `std::hash::Hash`)
228-
let Some(segment) = path.segments.iter().next() else {
245+
let Some(ident) = path.get_ident() else {
229246
continue;
230247
};
231-
let ident = &segment.ident;
232248
let ident_name = ident.to_string();
233249

234250
// Track the span where the trait is implemented for future errors
@@ -255,38 +271,38 @@ impl ReflectTraits {
255271
}
256272
}
257273
}
274+
// Handles `#[reflect( Debug(custom_debug_fn) )]`
275+
Meta::List(list) if list.path.is_ident(DEBUG_ATTR) => {
276+
let ident = list.path.get_ident().unwrap();
277+
list.parse_nested_meta(|meta| {
278+
let trait_func_ident = TraitImpl::Custom(meta.path, ident.span());
279+
traits.debug.merge(trait_func_ident)
280+
})?;
281+
}
282+
// Handles `#[reflect( PartialEq(custom_partial_eq_fn) )]`
283+
Meta::List(list) if list.path.is_ident(PARTIAL_EQ_ATTR) => {
284+
let ident = list.path.get_ident().unwrap();
285+
list.parse_nested_meta(|meta| {
286+
let trait_func_ident = TraitImpl::Custom(meta.path, ident.span());
287+
traits.partial_eq.merge(trait_func_ident)
288+
})?;
289+
}
258290
// Handles `#[reflect( Hash(custom_hash_fn) )]`
259-
Meta::List(list) => {
260-
// Get the first ident in the path (hopefully the path only contains one and not `std::hash::Hash`)
261-
let Some(segment) = list.path.segments.iter().next() else {
262-
continue;
263-
};
264-
265-
let ident = segment.ident.to_string();
266-
267-
// Track the span where the trait is implemented for future errors
268-
let span = ident.span();
269-
291+
Meta::List(list) if list.path.is_ident(HASH_ATTR) => {
292+
let ident = list.path.get_ident().unwrap();
270293
list.parse_nested_meta(|meta| {
271-
// This should be the path of the custom function
272-
let trait_func_ident = TraitImpl::Custom(meta.path, span);
273-
match ident.as_str() {
274-
DEBUG_ATTR => {
275-
traits.debug.merge(trait_func_ident)?;
276-
}
277-
PARTIAL_EQ_ATTR => {
278-
traits.partial_eq.merge(trait_func_ident)?;
279-
}
280-
HASH_ATTR => {
281-
traits.hash.merge(trait_func_ident)?;
282-
}
283-
_ => {
284-
return Err(syn::Error::new(span, "Can only use custom functions for special traits (i.e. `Hash`, `PartialEq`, `Debug`)"));
285-
}
286-
}
287-
Ok(())
294+
let trait_func_ident = TraitImpl::Custom(meta.path, ident.span());
295+
traits.hash.merge(trait_func_ident)
288296
})?;
289297
}
298+
Meta::List(list) => {
299+
return Err(syn::Error::new_spanned(
300+
list,
301+
format!(
302+
"expected one of [{DEBUG_ATTR:?}, {PARTIAL_EQ_ATTR:?}, {HASH_ATTR:?}]"
303+
),
304+
));
305+
}
290306
Meta::NameValue(pair) => {
291307
if pair.path.is_ident(FROM_REFLECT_ATTR) {
292308
traits.from_reflect_attrs.auto_derive =
@@ -402,6 +418,10 @@ impl ReflectTraits {
402418
}
403419
}
404420

421+
pub fn custom_where(&self) -> Option<&WhereClause> {
422+
self.custom_where.as_ref()
423+
}
424+
405425
/// Merges the trait implementations of this [`ReflectTraits`] with another one.
406426
///
407427
/// An error is returned if the two [`ReflectTraits`] have conflicting implementations.
@@ -411,11 +431,26 @@ impl ReflectTraits {
411431
self.partial_eq.merge(other.partial_eq)?;
412432
self.from_reflect_attrs.merge(other.from_reflect_attrs)?;
413433
self.type_path_attrs.merge(other.type_path_attrs)?;
434+
435+
self.merge_custom_where(other.custom_where);
436+
414437
for ident in other.idents {
415438
add_unique_ident(&mut self.idents, ident)?;
416439
}
417440
Ok(())
418441
}
442+
443+
fn merge_custom_where(&mut self, other: Option<WhereClause>) {
444+
match (&mut self.custom_where, other) {
445+
(Some(this), Some(other)) => {
446+
this.predicates.extend(other.predicates);
447+
}
448+
(None, Some(other)) => {
449+
self.custom_where = Some(other);
450+
}
451+
_ => {}
452+
}
453+
}
419454
}
420455

421456
impl Parse for ReflectTraits {

crates/bevy_reflect/bevy_reflect_derive/src/derive_data.rs

Lines changed: 7 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -167,10 +167,8 @@ impl<'a> ReflectDerive<'a> {
167167
}
168168

169169
reflect_mode = Some(ReflectMode::Normal);
170-
let new_traits = ReflectTraits::from_metas(
171-
meta_list.parse_args_with(Punctuated::<Meta, Comma>::parse_terminated)?,
172-
is_from_reflect_derive,
173-
)?;
170+
let new_traits =
171+
ReflectTraits::from_meta_list(meta_list, is_from_reflect_derive)?;
174172
traits.merge(new_traits)?;
175173
}
176174
Meta::List(meta_list) if meta_list.path.is_ident(REFLECT_VALUE_ATTRIBUTE_NAME) => {
@@ -182,10 +180,8 @@ impl<'a> ReflectDerive<'a> {
182180
}
183181

184182
reflect_mode = Some(ReflectMode::Value);
185-
let new_traits = ReflectTraits::from_metas(
186-
meta_list.parse_args_with(Punctuated::<Meta, Comma>::parse_terminated)?,
187-
is_from_reflect_derive,
188-
)?;
183+
let new_traits =
184+
ReflectTraits::from_meta_list(meta_list, is_from_reflect_derive)?;
189185
traits.merge(new_traits)?;
190186
}
191187
Meta::Path(path) if path.is_ident(REFLECT_VALUE_ATTRIBUTE_NAME) => {
@@ -484,7 +480,7 @@ impl<'a> ReflectStruct<'a> {
484480
}
485481

486482
pub fn where_clause_options(&self) -> WhereClauseOptions {
487-
WhereClauseOptions::new(self.meta(), self.active_fields(), self.ignored_fields())
483+
WhereClauseOptions::new(self.meta())
488484
}
489485
}
490486

@@ -507,22 +503,8 @@ impl<'a> ReflectEnum<'a> {
507503
&self.variants
508504
}
509505

510-
/// Get an iterator of fields which are exposed to the reflection API
511-
pub fn active_fields(&self) -> impl Iterator<Item = &StructField<'a>> {
512-
self.variants()
513-
.iter()
514-
.flat_map(|variant| variant.active_fields())
515-
}
516-
517-
/// Get an iterator of fields which are ignored by the reflection API
518-
pub fn ignored_fields(&self) -> impl Iterator<Item = &StructField<'a>> {
519-
self.variants()
520-
.iter()
521-
.flat_map(|variant| variant.ignored_fields())
522-
}
523-
524506
pub fn where_clause_options(&self) -> WhereClauseOptions {
525-
WhereClauseOptions::new(self.meta(), self.active_fields(), self.ignored_fields())
507+
WhereClauseOptions::new(self.meta())
526508
}
527509
}
528510

@@ -668,6 +650,7 @@ impl<'a> ReflectTypePath<'a> {
668650
where_clause: None,
669651
params: Punctuated::new(),
670652
};
653+
671654
match self {
672655
Self::Internal { generics, .. } | Self::External { generics, .. } => generics,
673656
_ => EMPTY_GENERICS,

crates/bevy_reflect/bevy_reflect_derive/src/from_reflect.rs

Lines changed: 6 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use crate::container_attributes::REFLECT_DEFAULT;
22
use crate::derive_data::ReflectEnum;
33
use crate::enum_utility::{get_variant_constructors, EnumVariantConstructors};
44
use crate::field_attributes::DefaultBehavior;
5-
use crate::utility::{extend_where_clause, ident_or_index, WhereClauseOptions};
5+
use crate::utility::{ident_or_index, WhereClauseOptions};
66
use crate::{ReflectMeta, ReflectStruct};
77
use bevy_macro_utils::fq_std::{FQAny, FQClone, FQDefault, FQOption};
88
use proc_macro2::Span;
@@ -24,7 +24,7 @@ pub(crate) fn impl_value(meta: &ReflectMeta) -> proc_macro2::TokenStream {
2424
let bevy_reflect_path = meta.bevy_reflect_path();
2525
let (impl_generics, ty_generics, where_clause) = type_path.generics().split_for_impl();
2626
let where_from_reflect_clause =
27-
extend_where_clause(where_clause, &WhereClauseOptions::new_value(meta));
27+
WhereClauseOptions::new_type_path(meta).extend_where_clause(where_clause);
2828
quote! {
2929
impl #impl_generics #bevy_reflect_path::FromReflect for #type_path #ty_generics #where_from_reflect_clause {
3030
fn from_reflect(reflect: &dyn #bevy_reflect_path::Reflect) -> #FQOption<Self> {
@@ -50,22 +50,8 @@ pub(crate) fn impl_enum(reflect_enum: &ReflectEnum) -> proc_macro2::TokenStream
5050
let (impl_generics, ty_generics, where_clause) = enum_path.generics().split_for_impl();
5151

5252
// Add FromReflect bound for each active field
53-
let where_from_reflect_clause = extend_where_clause(
54-
where_clause,
55-
&WhereClauseOptions::new_with_bounds(
56-
reflect_enum.meta(),
57-
reflect_enum.active_fields(),
58-
reflect_enum.ignored_fields(),
59-
|field| match &field.attrs.default {
60-
DefaultBehavior::Default => Some(quote!(#FQDefault)),
61-
_ => None,
62-
},
63-
|field| match &field.attrs.default {
64-
DefaultBehavior::Func(_) => None,
65-
_ => Some(quote!(#FQDefault)),
66-
},
67-
),
68-
);
53+
let where_from_reflect_clause =
54+
WhereClauseOptions::new(reflect_enum.meta()).extend_where_clause(where_clause);
6955

7056
quote! {
7157
impl #impl_generics #bevy_reflect_path::FromReflect for #enum_path #ty_generics #where_from_reflect_clause {
@@ -144,28 +130,8 @@ fn impl_struct_internal(
144130
.split_for_impl();
145131

146132
// Add FromReflect bound for each active field
147-
let where_from_reflect_clause = extend_where_clause(
148-
where_clause,
149-
&WhereClauseOptions::new_with_bounds(
150-
reflect_struct.meta(),
151-
reflect_struct.active_fields(),
152-
reflect_struct.ignored_fields(),
153-
|field| match &field.attrs.default {
154-
DefaultBehavior::Default => Some(quote!(#FQDefault)),
155-
_ => None,
156-
},
157-
|field| {
158-
if is_defaultable {
159-
None
160-
} else {
161-
match &field.attrs.default {
162-
DefaultBehavior::Func(_) => None,
163-
_ => Some(quote!(#FQDefault)),
164-
}
165-
}
166-
},
167-
),
168-
);
133+
let where_from_reflect_clause =
134+
WhereClauseOptions::new(reflect_struct.meta()).extend_where_clause(where_clause);
169135

170136
quote! {
171137
impl #impl_generics #bevy_reflect_path::FromReflect for #struct_path #ty_generics #where_from_reflect_clause {

crates/bevy_reflect/bevy_reflect_derive/src/impls/enums.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
use crate::derive_data::{EnumVariant, EnumVariantFields, ReflectEnum, StructField};
22
use crate::enum_utility::{get_variant_constructors, EnumVariantConstructors};
33
use crate::impls::{impl_type_path, impl_typed};
4-
use crate::utility::extend_where_clause;
54
use bevy_macro_utils::fq_std::{FQAny, FQBox, FQOption, FQResult};
65
use proc_macro2::{Ident, Span};
76
use quote::quote;
@@ -92,7 +91,7 @@ pub(crate) fn impl_enum(reflect_enum: &ReflectEnum) -> proc_macro2::TokenStream
9291
let (impl_generics, ty_generics, where_clause) =
9392
reflect_enum.meta().type_path().generics().split_for_impl();
9493

95-
let where_reflect_clause = extend_where_clause(where_clause, &where_clause_options);
94+
let where_reflect_clause = where_clause_options.extend_where_clause(where_clause);
9695

9796
quote! {
9897
#get_type_registration_impl

crates/bevy_reflect/bevy_reflect_derive/src/impls/structs.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use crate::impls::{impl_type_path, impl_typed};
2-
use crate::utility::{extend_where_clause, ident_or_index};
2+
use crate::utility::ident_or_index;
33
use crate::ReflectStruct;
44
use bevy_macro_utils::fq_std::{FQAny, FQBox, FQDefault, FQOption, FQResult};
55
use quote::{quote, ToTokens};
@@ -99,7 +99,7 @@ pub(crate) fn impl_struct(reflect_struct: &ReflectStruct) -> proc_macro2::TokenS
9999
.generics()
100100
.split_for_impl();
101101

102-
let where_reflect_clause = extend_where_clause(where_clause, &where_clause_options);
102+
let where_reflect_clause = where_clause_options.extend_where_clause(where_clause);
103103

104104
quote! {
105105
#get_type_registration_impl

crates/bevy_reflect/bevy_reflect_derive/src/impls/tuple_structs.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
use crate::impls::{impl_type_path, impl_typed};
2-
use crate::utility::extend_where_clause;
32
use crate::ReflectStruct;
43
use bevy_macro_utils::fq_std::{FQAny, FQBox, FQDefault, FQOption, FQResult};
54
use quote::{quote, ToTokens};
@@ -90,7 +89,7 @@ pub(crate) fn impl_tuple_struct(reflect_struct: &ReflectStruct) -> proc_macro2::
9089
.generics()
9190
.split_for_impl();
9291

93-
let where_reflect_clause = extend_where_clause(where_clause, &where_clause_options);
92+
let where_reflect_clause = where_clause_options.extend_where_clause(where_clause);
9493

9594
quote! {
9695
#get_type_registration_impl

0 commit comments

Comments
 (0)