Skip to content
55 changes: 54 additions & 1 deletion crates/bevy_reflect/derive/src/container_attributes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ mod kw {
syn::custom_keyword!(type_path);
syn::custom_keyword!(Debug);
syn::custom_keyword!(PartialEq);
syn::custom_keyword!(PartialOrd);
syn::custom_keyword!(Hash);
syn::custom_keyword!(Clone);
syn::custom_keyword!(no_field_bounds);
Expand All @@ -33,6 +34,7 @@ mod kw {
// Received via attributes like `#[reflect(PartialEq, Hash, ...)]`
const DEBUG_ATTR: &str = "Debug";
const PARTIAL_EQ_ATTR: &str = "PartialEq";
const PARTIAL_ORD_ATTR: &str = "PartialOrd";
const HASH_ATTR: &str = "Hash";

// The traits listed below are not considered "special" (i.e. they use the `ReflectMyTrait` syntax)
Expand Down Expand Up @@ -180,6 +182,7 @@ pub(crate) struct ContainerAttributes {
clone: TraitImpl,
debug: TraitImpl,
hash: TraitImpl,
partial_ord: TraitImpl,
partial_eq: TraitImpl,
from_reflect_attrs: FromReflectAttrs,
type_path_attrs: TypePathAttrs,
Expand Down Expand Up @@ -248,6 +251,8 @@ impl ContainerAttributes {
self.parse_debug(input)
} else if lookahead.peek(kw::Hash) {
self.parse_hash(input)
} else if lookahead.peek(kw::PartialOrd) {
self.parse_partial_ord(input)
} else if lookahead.peek(kw::PartialEq) {
self.parse_partial_eq(input)
} else if lookahead.peek(Ident::peek_any) {
Expand All @@ -266,7 +271,7 @@ impl ContainerAttributes {

if input.peek(token::Paren) {
return Err(syn::Error::new(ident.span(), format!(
"only [{DEBUG_ATTR:?}, {PARTIAL_EQ_ATTR:?}, {HASH_ATTR:?}] may specify custom functions",
"only [{DEBUG_ATTR:?}, {PARTIAL_EQ_ATTR:?}, {PARTIAL_ORD_ATTR:?}, {HASH_ATTR:?}] may specify custom functions",
)));
}

Expand Down Expand Up @@ -342,6 +347,27 @@ impl ContainerAttributes {
Ok(())
}

/// Parse special `PartialOrd` registration.
///
/// Examples:
/// - `#[reflect(PartialOrd)]`
/// - `#[reflect(PartialOrd(custom_partial_cmp_fn))]`
fn parse_partial_ord(&mut self, input: ParseStream) -> syn::Result<()> {
let ident = input.parse::<kw::PartialOrd>()?;

if input.peek(token::Paren) {
let content;
parenthesized!(content in input);
let path = content.parse::<Path>()?;
self.partial_ord
.merge(TraitImpl::Custom(path, ident.span))?;
} else {
self.partial_ord = TraitImpl::Implemented(ident.span);
}

Ok(())
}

/// Parse special `Hash` registration.
///
/// Examples:
Expand Down Expand Up @@ -546,6 +572,33 @@ impl ContainerAttributes {
}
}

/// Returns the implementation of `PartialReflect::reflect_partial_cmp` as a `TokenStream`.
///
/// If `PartialOrd` was not registered, returns `None`.
pub fn get_partial_ord_impl(
&self,
bevy_reflect_path: &Path,
) -> Option<proc_macro2::TokenStream> {
match &self.partial_ord {
&TraitImpl::Implemented(span) => Some(quote_spanned! {span=>
fn reflect_partial_cmp(&self, value: &dyn #bevy_reflect_path::PartialReflect) -> #FQOption<::core::cmp::Ordering> {
let value = <dyn #bevy_reflect_path::PartialReflect>::try_downcast_ref::<Self>(value);
if let #FQOption::Some(value) = value {
::core::cmp::PartialOrd::partial_cmp(self, value)
} else {
#FQOption::None
}
}
}),
&TraitImpl::Custom(ref impl_fn, span) => Some(quote_spanned! {span=>
fn reflect_partial_cmp(&self, value: &dyn #bevy_reflect_path::PartialReflect) -> #FQOption<::core::cmp::Ordering> {
#impl_fn(self, value)
}
}),
TraitImpl::NotImplemented => None,
}
}

/// Returns the implementation of `PartialReflect::debug` as a `TokenStream`.
///
/// If `Debug` was not registered, returns `None`.
Expand Down
16 changes: 16 additions & 0 deletions crates/bevy_reflect/derive/src/impls/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ pub fn common_partial_reflect_methods(
meta: &ReflectMeta,
default_partial_eq_delegate: impl FnOnce() -> Option<proc_macro2::TokenStream>,
default_hash_delegate: impl FnOnce() -> Option<proc_macro2::TokenStream>,
default_partial_ord_delegate: impl FnOnce() -> Option<proc_macro2::TokenStream>,
) -> proc_macro2::TokenStream {
let bevy_reflect_path = meta.bevy_reflect_path();

Expand All @@ -100,6 +101,19 @@ pub fn common_partial_reflect_methods(
}
})
});
let partial_ord_fn = meta
.attrs()
.get_partial_ord_impl(bevy_reflect_path)
.or_else(move || {
let default_delegate = default_partial_ord_delegate();
default_delegate.map(|func| {
quote! {
fn reflect_partial_cmp(&self, value: &dyn #bevy_reflect_path::PartialReflect) -> #FQOption<::core::cmp::Ordering> {
(#func)(self, value)
}
}
})
});
let hash_fn = meta
.attrs()
.get_hash_impl(bevy_reflect_path)
Expand Down Expand Up @@ -151,6 +165,8 @@ pub fn common_partial_reflect_methods(

#partial_eq_fn

#partial_ord_fn

#debug_fn
}
}
Expand Down
1 change: 1 addition & 0 deletions crates/bevy_reflect/derive/src/impls/enums.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ pub(crate) fn impl_enum(reflect_enum: &ReflectEnum) -> proc_macro2::TokenStream
reflect_enum.meta(),
|| Some(quote!(#bevy_reflect_path::enum_partial_eq)),
|| Some(quote!(#bevy_reflect_path::enum_hash)),
|| Some(quote!(#bevy_reflect_path::enum_partial_cmp)),
);
let clone_fn = reflect_enum.get_clone_impl();

Expand Down
2 changes: 1 addition & 1 deletion crates/bevy_reflect/derive/src/impls/opaque.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ pub(crate) fn impl_opaque(meta: &ReflectMeta) -> proc_macro2::TokenStream {

let type_path_impl = impl_type_path(meta);
let full_reflect_impl = impl_full_reflect(&where_clause_options);
let common_methods = common_partial_reflect_methods(meta, || None, || None);
let common_methods = common_partial_reflect_methods(meta, || None, || None, || None);
let clone_fn = meta.attrs().get_clone_impl(bevy_reflect_path);

let apply_impl = if let Some(remote_ty) = meta.remote_ty() {
Expand Down
1 change: 1 addition & 0 deletions crates/bevy_reflect/derive/src/impls/structs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ pub(crate) fn impl_struct(reflect_struct: &ReflectStruct) -> proc_macro2::TokenS
reflect_struct.meta(),
|| Some(quote!(#bevy_reflect_path::struct_partial_eq)),
|| None,
|| Some(quote!(#bevy_reflect_path::struct_partial_cmp)),
);
let clone_fn = reflect_struct.get_clone_impl();

Expand Down
1 change: 1 addition & 0 deletions crates/bevy_reflect/derive/src/impls/tuple_structs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ pub(crate) fn impl_tuple_struct(reflect_struct: &ReflectStruct) -> proc_macro2::
reflect_struct.meta(),
|| Some(quote!(#bevy_reflect_path::tuple_struct_partial_eq)),
|| None,
|| Some(quote!(#bevy_reflect_path::tuple_struct_partial_cmp)),
);
let clone_fn = reflect_struct.get_clone_impl();

Expand Down
5 changes: 5 additions & 0 deletions crates/bevy_reflect/derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,11 @@ fn match_reflect_impls(ast: DeriveInput, source: ReflectImplSource) -> TokenStre
/// A custom implementation may be provided using `#[reflect(PartialEq(my_partial_eq_func))]` where
/// `my_partial_eq_func` is the path to a function matching the signature:
/// `(&Self, value: &dyn #bevy_reflect_path::Reflect) -> bool`.
/// * `#[reflect(PartialOrd)]` will force the implementation of `PartialReflect::reflect_partial_cmp`
/// to rely on the type's [`PartialOrd`] implementation.
/// A custom implementation may be provided using `#[reflect(PartialOrd(my_partial_cmp_fn))]` where
/// `my_partial_cmp_fn` is the path to a function matching the signature:
/// `(&Self, value: &dyn #bevy_reflect_path::PartialReflect) -> Option<::core::cmp::Ordering>`.
/// * `#[reflect(Hash)]` will force the implementation of `Reflect::reflect_hash` to rely on
/// the type's [`Hash`] implementation.
/// A custom implementation may be provided using `#[reflect(Hash(my_hash_func))]` where
Expand Down
31 changes: 31 additions & 0 deletions crates/bevy_reflect/src/array.rs
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,10 @@ impl PartialReflect for DynamicArray {
array_partial_eq(self, value)
}

fn reflect_partial_cmp(&self, value: &dyn PartialReflect) -> Option<::core::cmp::Ordering> {
array_partial_cmp(self, value)
}

fn debug(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
write!(f, "DynamicArray(")?;
array_debug(self, f)?;
Expand Down Expand Up @@ -463,6 +467,33 @@ pub fn array_partial_eq<A: Array + ?Sized>(
Some(true)
}

/// Lexicographically compares two [arrays](Array) and returns their ordering.
///
/// Returns [`None`] if the comparison couldn't be performed (e.g., kinds mismatch
/// or an element comparison returns `None`).
#[inline]
pub fn array_partial_cmp<A: Array + ?Sized>(
array: &A,
reflect: &dyn PartialReflect,
) -> Option<::core::cmp::Ordering> {
let ReflectRef::Array(reflect_array) = reflect.reflect_ref() else {
return None;
};

let min_len = core::cmp::min(array.len(), reflect_array.len());

for (a, b) in array.iter().zip(reflect_array.iter()).take(min_len) {
match a.reflect_partial_cmp(b) {
None => return None,
Some(core::cmp::Ordering::Equal) => continue,
Some(ord) => return Some(ord),
}
}

// If all compared elements were equal, order by length
Some(array.len().cmp(&reflect_array.len()))
}

/// The default debug formatter for [`Array`] types.
///
/// # Example
Expand Down
11 changes: 8 additions & 3 deletions crates/bevy_reflect/src/enums/dynamic_enum.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use bevy_reflect_derive::impl_type_path;

use crate::{
enum_debug, enum_hash, enum_partial_eq, ApplyError, DynamicStruct, DynamicTuple, Enum,
PartialReflect, Reflect, ReflectKind, ReflectMut, ReflectOwned, ReflectRef, Struct, Tuple,
TypeInfo, VariantFieldIter, VariantType,
enum_debug, enum_hash, enum_partial_cmp, enum_partial_eq, ApplyError, DynamicStruct,
DynamicTuple, Enum, PartialReflect, Reflect, ReflectKind, ReflectMut, ReflectOwned, ReflectRef,
Struct, Tuple, TypeInfo, VariantFieldIter, VariantType,
};

use alloc::{boxed::Box, string::String};
Expand Down Expand Up @@ -396,6 +396,11 @@ impl PartialReflect for DynamicEnum {
enum_partial_eq(self, value)
}

#[inline]
fn reflect_partial_cmp(&self, value: &dyn PartialReflect) -> Option<::core::cmp::Ordering> {
enum_partial_cmp(self, value)
}

#[inline]
fn debug(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
write!(f, "DynamicEnum(")?;
Expand Down
67 changes: 67 additions & 0 deletions crates/bevy_reflect/src/enums/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ pub fn enum_partial_eq<TEnum: Enum + ?Sized>(a: &TEnum, b: &dyn PartialReflect)

match a.variant_type() {
VariantType::Struct => {
if a.field_len() != b.field_len() {
return Some(false);
}
// Same struct fields?
for field in a.iter_fields() {
let field_name = field.name().unwrap();
Expand All @@ -60,6 +63,9 @@ pub fn enum_partial_eq<TEnum: Enum + ?Sized>(a: &TEnum, b: &dyn PartialReflect)
Some(true)
}
VariantType::Tuple => {
if a.field_len() != b.field_len() {
return Some(false);
}
// Same tuple fields?
for (i, field) in a.iter_fields().enumerate() {
if let Some(field_value) = b.field_at(i) {
Expand All @@ -78,6 +84,67 @@ pub fn enum_partial_eq<TEnum: Enum + ?Sized>(a: &TEnum, b: &dyn PartialReflect)
}
}

/// Compares two [`Enum`] values (by variant) and returns their ordering.
///
/// Returns [`None`] if the comparison couldn't be performed (e.g., kinds mismatch
/// or an element comparison returns `None`).
///
/// The ordering is same with `derive` macro. First order by variant index, then by fields.
#[inline]
pub fn enum_partial_cmp<TEnum: Enum + ?Sized>(
a: &TEnum,
b: &dyn PartialReflect,
) -> Option<::core::cmp::Ordering> {
// Both enums?
let ReflectRef::Enum(b) = b.reflect_ref() else {
return None;
};

// Same variant name?
if a.variant_name() != b.variant_name() {
// Different variant names, determining ordering by variant index
return Some(a.variant_index().cmp(&b.variant_index()));
}

// Same variant type?
if !a.is_variant(b.variant_type()) {
return None;
}

match a.variant_type() {
VariantType::Struct => {
if a.field_len() != b.field_len() {
return None;
}
crate::struct_trait::partial_cmp_by_field_names(
a.field_len(),
|i| a.name_at(i),
|i| a.field_at(i),
|i| b.name_at(i),
|i| b.field_at(i),
|name| b.field(name),
)
}
VariantType::Tuple => {
if a.field_len() != b.field_len() {
return None;
}
for (i, field) in a.iter_fields().enumerate() {
if let Some(field_value) = b.field_at(i) {
match field.value().reflect_partial_cmp(field_value) {
None => return None,
Some(core::cmp::Ordering::Equal) => continue,
Some(ord) => return Some(ord),
}
}
return None;
}
Some(core::cmp::Ordering::Equal)
}
_ => Some(core::cmp::Ordering::Equal),
}
}

/// The default debug formatter for [`Enum`] types.
///
/// # Example
Expand Down
23 changes: 23 additions & 0 deletions crates/bevy_reflect/src/enums/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -689,5 +689,28 @@ mod tests {
!a.reflect_partial_eq(b).unwrap_or_default(),
"expected TestEnum::C{{value: 123}} != TestEnum::C2{{value: 1.23}}"
);

#[derive(Reflect)]
enum TestEnum2 {
A,
A1,
B(usize, usize),
C { value: i32, value2: f32 },
}
let a: &dyn PartialReflect = &TestEnum::C { value: 123 };
let a2: &dyn PartialReflect = &TestEnum2::C {
value: 123,
value2: 1.23,
};
assert!(
!a.reflect_partial_eq(a2).unwrap_or_default(),
"expected TestEnum::C{{value: 123}} != TestEnum2::C{{value: 123, value2: 1.23}}"
);
let b: &dyn PartialReflect = &TestEnum::B(123);
let b2 = &TestEnum2::B(123, 321);
assert!(
!b.reflect_partial_eq(b2).unwrap_or_default(),
"expected TestEnum::C{{value: 123}} != TestEnum2::B(123, 321)"
);
}
}
12 changes: 12 additions & 0 deletions crates/bevy_reflect/src/impls/alloc/borrow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,14 @@ impl PartialReflect for Cow<'static, str> {
}
}

fn reflect_partial_cmp(&self, value: &dyn PartialReflect) -> Option<core::cmp::Ordering> {
if let Some(value) = value.try_downcast_ref::<Self>() {
Some(PartialOrd::partial_cmp(self, value)?)
} else {
None
}
}

fn debug(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Debug::fmt(self, f)
}
Expand Down Expand Up @@ -252,6 +260,10 @@ impl<T: FromReflect + MaybeTyped + Clone + TypePath + GetTypeRegistration> Parti
crate::list_partial_eq(self, value)
}

fn reflect_partial_cmp(&self, value: &dyn PartialReflect) -> Option<::core::cmp::Ordering> {
crate::list_partial_cmp(self, value)
}

fn apply(&mut self, value: &dyn PartialReflect) {
crate::list_apply(self, value);
}
Expand Down
Loading