diff --git a/crates/bevy_reflect/src/lib.rs b/crates/bevy_reflect/src/lib.rs index 99a0a1f7b5..ebdf950c22 100644 --- a/crates/bevy_reflect/src/lib.rs +++ b/crates/bevy_reflect/src/lib.rs @@ -615,6 +615,7 @@ mod impls { pub mod attributes; mod enums; mod generics; +pub mod macros; pub mod serde; pub mod std_traits; #[cfg(feature = "debug_stack")] diff --git a/crates/bevy_reflect/src/macros/match_type.rs b/crates/bevy_reflect/src/macros/match_type.rs new file mode 100644 index 0000000000..5ccc85807a --- /dev/null +++ b/crates/bevy_reflect/src/macros/match_type.rs @@ -0,0 +1,559 @@ +/// A helper macro for downcasting a [`PartialReflect`] value to a concrete type. +/// +/// # Syntax +/// +/// The syntax of the macro closely resembles a standard `match`, but with some subtle differences. +/// +/// ```text +/// select_type! { , } +/// +/// := IDENT // the variable you’re matching +/// +/// := +/// ( , )* [ , ] // zero or more arms with optional trailing comma +/// +/// := +/// [ @ ] [ where ] => +/// +/// := +/// [ @ ] // *optional* binding for the downcasted value +/// // type pattern to match +/// [ `[` `]` ] // *optional* type array +/// [ where ] // *optional* guard +/// => // expression or block to run on match +/// +/// := IDENT | _ +/// +/// := IDENT +/// +/// := IDENT | _ // rename or ignore the downcast value +/// +/// := // determines the downcast method +/// | TYPE // -> try_take::() +/// | & TYPE // -> try_downcast_ref::() +/// | &mut TYPE // -> try_downcast_mut::() +/// | _ // -> catch‑all (no downcast) +/// +/// := a boolean expression that acts as a guard for the arm +/// := expression or block that runs if the downcast succeeds for the given type +/// ``` +/// +/// The `` must be a type that implements [`PartialReflect`]. +/// Owned values should be passed as a `Box`. +/// +/// Types are matched in the order they are defined. +/// Any `_` cases must be the last case in the list. +/// +/// If a custom binding is not provided, +/// the downcasted value will be bound to the same identifier as the input, thus shadowing it. +/// You can use `_` to ignore the downcasted value if you don’t need it, +/// which may be helpful to silence any "unused variable" lints. +/// +/// The `where` clause is optional and can be used to add an extra boolean guard. +/// If the guard evaluates to `true`, the expression will be executed if the type matches. +/// Otherwise, matching will continue to the next arm even if the type matches. +/// +/// If a `` is defined, this will be bound to a slice of [`Type`]s +/// that were checked up to and including the current arm. +/// This can be useful for debugging or logging purposes. +/// Note that this list may contain duplicates if the same type is checked in multiple arms, +/// such as when using `where` guards. +/// +/// # Examples +/// +/// ``` +/// # use bevy_reflect::macros::select_type; +/// # use bevy_reflect::PartialReflect; +/// # +/// fn stringify(mut value: Box) -> String { +/// select_type! { value, +/// // Downcast to an owned type +/// f32 => format!("{:.1}", value), +/// // Downcast to a mutable reference +/// &mut i32 => { +/// *value *= 2; +/// value.to_string() +/// }, +/// // Define custom bindings +/// chars @ &Vec => chars.iter().collect(), +/// // Define conditional guards +/// &String where value == "ping" => "pong".to_owned(), +/// &String => value.clone(), +/// // Fallback case with an optional type array +/// _ [types] => { +/// println!("Couldn't match any types: {:?}", types); +/// "".to_string() +/// }, +/// } +/// } +/// +/// assert_eq!(stringify(Box::new(123.0_f32)), "123.0"); +/// assert_eq!(stringify(Box::new(123_i32)), "246"); +/// assert_eq!(stringify(Box::new(vec!['h', 'e', 'l', 'l', 'o'])), "hello"); +/// assert_eq!(stringify(Box::new("ping".to_string())), "pong"); +/// assert_eq!(stringify(Box::new("hello".to_string())), "hello"); +/// assert_eq!(stringify(Box::new(true)), ""); +/// ``` +/// +/// [`PartialReflect`]: crate::PartialReflect +/// [`Type`]: crate::Type +#[macro_export] +macro_rules! select_type { + + // === Entry Point === // + + {$input:ident} => {{}}; + {$input:ident, $($tt:tt)*} => {{ + // We use an import over fully-qualified syntax so users don't have to + // cast to `dyn PartialReflect` or dereference manually + use $crate::PartialReflect; + + select_type!(@arm[[], $input] $($tt)*) + }}; + + // === Arm Parsing === // + // These rules take the following input (in `[]`): + // 1. The input identifier + // 2. An optional binding identifier (or `_`) + // + // Additionally, most cases are comprised of both a terminal and non-terminal rule. + + // --- Empty Case --- // + {@arm [[$($tys:ty),*], $input:ident $(as $binding:tt)?]} => {{}}; + + // --- Custom Bindings --- // + {@arm [[$($tys:ty),*], $input:ident $(as $binding:tt)?] $new_binding:tt @ $($tt:tt)+} => { + select_type!(@arm [[$($tys),*], $input as $new_binding] $($tt)+) + }; + + // --- Fallback Case --- // + {@arm [[$($tys:ty),*], $input:ident $(as $binding:tt)?] _ $([$types:ident])? $(where $condition:expr)? => $action:expr, $($tt:tt)+} => { + select_type!(@else [[$($tys),*], $input $(as $binding)?, [$($types)?], [$($condition)?], $action] $($tt)+) + }; + {@arm [[$($tys:ty),*], $input:ident $(as $binding:tt)?] _ $([$types:ident])? $(where $condition:expr)? => $action:expr $(,)?} => { + select_type!(@else [[$($tys),*], $input $(as $binding)?, [$($types)?], [$($condition)?], $action]) + }; + + // --- Mutable Downcast Case --- // + {@arm [[$($tys:ty),*], $input:ident $(as $binding:tt)?] &mut $ty:ty $([$types:ident])? $(where $condition:expr)? => $action:expr, $($tt:tt)+} => { + select_type!(@if [[$($tys,)* &mut $ty], $input $(as $binding)?, mut, $ty, [$($types)?], [$($condition)?], $action] $($tt)+) + }; + {@arm [[$($tys:ty),*], $input:ident $(as $binding:tt)?] &mut $ty:ty $([$types:ident])? $(where $condition:expr)? => $action:expr $(,)?} => { + select_type!(@if [[$($tys,)* &mut $ty], $input $(as $binding)?, mut, $ty, [$($types)?], [$($condition)?], $action]) + }; + + // --- Immutable Downcast Case --- // + {@arm [[$($tys:ty),*], $input:ident $(as $binding:tt)?] & $ty:ty $([$types:ident])? $(where $condition:expr)? => $action:expr, $($tt:tt)+} => { + select_type!(@if [[$($tys,)* &$ty], $input $(as $binding)?, ref, $ty, [$($types)?], [$($condition)?], $action] $($tt)+) + }; + {@arm [[$($tys:ty),*], $input:ident $(as $binding:tt)?] & $ty:ty $([$types:ident])? $(where $condition:expr)? => $action:expr $(,)?} => { + select_type!(@if [[$($tys,)* &$ty], $input $(as $binding)?, ref, $ty, [$($types)?], [$($condition)?], $action]) + }; + + // --- Owned Downcast Case --- // + {@arm [[$($tys:ty),*], $input:ident $(as $binding:tt)?] $ty:ty $([$types:ident])? $(where $condition:expr)? => $action:expr, $($tt:tt)+} => { + select_type!(@if [[$($tys,)* $ty], $input $(as $binding)?, box, $ty, [$($types)?], [$($condition)?], $action] $($tt)+) + }; + {@arm [[$($tys:ty),*], $input:ident $(as $binding:tt)?] $ty:ty $([$types:ident])? $(where $condition:expr)? => $action:expr $(,)?} => { + select_type!(@if [[$($tys,)* $ty], $input $(as $binding)?, box, $ty, [$($types)?], [$($condition)?], $action]) + }; + + // === Type Matching === // + // These rules take the following input (in `[]`): + // 1. The input identifier + // 2. An optional binding identifier (or `_`) + // 3. The kind of downcast (e.g., `mut`, `ref`, or `box`) + // 4. The type to downcast to + // 5. An optional condition (wrapped in `[]` for disambiguation) + // 6. The action to take if the downcast succeeds + + // This rule handles the owned downcast case + {@if [[$($tys:ty),*], $input:ident $(as $binding:tt)?, box, $ty:ty, [$($types:ident)?], [$($condition:expr)?], $action:expr] $($rest:tt)*} => { + #[allow(unused_parens, reason = "may be used for disambiguation")] + match select_type!(@downcast box, $ty, $input) { + Ok(select_type!(@bind [mut] $input $(as $binding)?)) $(if $condition)? => { + select_type!(@collect [$($tys),*] $(as $types)?); + $action + }, + $input => { + // We have to rebind `$value` here so that we can unconditionally ignore it + // due to the fact that `unused_variables` seems to be the only lint that + // is visible outside the macro when used within other crates. + #[allow( + unused_variables, + reason = "unfortunately this variable cannot receive a custom binding to let it be ignored otherwise" + )] + let mut $input = match $input { + Ok($input) => $crate::__macro_exports::alloc_utils::Box::new($input) as $crate::__macro_exports::alloc_utils::Box, + Err($input) => $input + }; + + select_type!(@arm [[$($tys),*], $input] $($rest)*) + } + } + }; + // This rule handles the mutable and immutable downcast cases + {@if [[$($tys:ty),*], $input:ident $(as $binding:tt)?, $kind:tt, $ty:ty, [$($types:ident)?], [$($condition:expr)?], $action:expr] $($rest:tt)*} => { + #[allow(unused_parens, reason = "may be used for disambiguation")] + match select_type!(@downcast $kind, $ty, $input) { + Some(select_type!(@bind [] $input $(as $binding)?)) $(if $condition)? => { + select_type!(@collect [$($tys),*] $(as $types)?); + $action + }, + _ => { + select_type!(@arm [[$($tys),*], $input] $($rest)*) + } + } + }; + + // This rule handles the fallback case where a condition has been provided + {@else [[$($tys:ty),*], $input:ident $(as $binding:tt)?, [$($types:ident)?], [$condition:expr], $action:expr] $($rest:tt)*} => {{ + select_type!(@collect [$($tys),*] $(as $types)?); + let select_type!(@bind [mut] _ $(as $binding)?) = $input; + + if $condition { + $action + } else { + select_type!(@arm [[$($tys),*], $input] $($rest)*) + } + }}; + // This rule handles the fallback case where no condition has been provided + {@else [[$($tys:ty),*], $input:ident $(as $binding:tt)?, [$($types:ident)?], [], $action:expr] $($rest:tt)*} => {{ + select_type!(@collect [$($tys),*] $(as $types)?); + let select_type!(@bind [mut] _ $(as $binding)?) = $input; + + $action + }}; + + // === Helpers === // + + // --- Downcasting --- // + // Helpers for downcasting `$input` to `$ty` + // based on the given keyword (`mut`, `ref`, or `box`). + + {@downcast mut, $ty:ty, $input:ident} => { + $input.as_partial_reflect_mut().try_downcast_mut::<$ty>() + }; + {@downcast ref, $ty:ty, $input:ident} => { + $input.as_partial_reflect().try_downcast_ref::<$ty>() + }; + {@downcast box, $ty:ty, $input:ident} => { + // We eagerly box here so that we can support non-boxed values. + $crate::__macro_exports::alloc_utils::Box::new($input).into_partial_reflect().try_take::<$ty>() + }; + + // --- Binding --- // + // Helpers for creating a binding for the downcasted value. + // This ensures that we only add `mut` when necessary, + // and that `_` is handled appropriately. + + {@bind [$($mut:tt)?] _} => { + _ + }; + {@bind [$($mut:tt)?] $input:ident} => { + $($mut)? $input + }; + {@bind [$($mut:tt)?] $input:tt as _} => { + _ + }; + {@bind [$($mut:tt)?] $input:tt as $binding:ident} => { + $($mut)? $binding + }; + + // --- Collect Types --- // + // Helpers for collecting the types into an array of `Type`. + + {@collect [$($ty:ty),*]} => {}; + {@collect [$($tys:ty),*] as $types:ident} => { + let $types: &[$crate::Type] = &[$($crate::Type::of::<$tys>(),)*]; + }; +} + +pub use select_type; + +#[cfg(test)] +mod tests { + #![allow( + clippy::allow_attributes, + unused_imports, + unused_parens, + unused_mut, + reason = "the warnings generated by these macros should only be visible to `bevy_reflect`" + )] + + use super::*; + use crate::{PartialReflect, Type}; + use alloc::boxed::Box; + use alloc::string::{String, ToString}; + use alloc::vec::Vec; + use alloc::{format, vec}; + use core::fmt::Debug; + use core::ops::MulAssign; + + #[test] + fn should_allow_empty() { + fn empty(_value: Box) { + let _: () = select_type! {_value}; + let _: () = select_type! {_value,}; + } + + empty(Box::new(42)); + } + + #[test] + fn should_downcast_ref() { + fn to_string(value: &dyn PartialReflect) -> String { + select_type! {value, + &String => value.clone(), + &i32 => value.to_string(), + &f32 => format!("{:.2}", value), + _ => "unknown".to_string() + } + } + + assert_eq!(to_string(&String::from("hello")), "hello"); + assert_eq!(to_string(&42_i32), "42"); + assert_eq!(to_string(&1.2345_f32), "1.23"); + assert_eq!(to_string(&true), "unknown"); + } + + #[test] + fn should_downcast_mut() { + fn push_value(container: &mut dyn PartialReflect, value: i32) -> bool { + select_type! {container, + &mut Vec => container.push(value), + &mut Vec => container.push(value as u32), + _ => return false + } + + true + } + + let mut list: Vec = vec![1, 2]; + assert!(push_value(&mut list, 3)); + assert_eq!(list, vec![1, 2, 3]); + + let mut list: Vec = vec![1, 2]; + assert!(push_value(&mut list, 3)); + assert_eq!(list, vec![1, 2, 3]); + + let mut list: Vec = vec![String::from("hello")]; + assert!(!push_value(&mut list, 3)); + } + + #[test] + fn should_downcast_owned() { + fn into_string(value: Box) -> Option { + select_type! {value, + String => Some(value), + i32 => Some(value.to_string()), + _ => None + } + } + + let value = Box::new("hello".to_string()); + let result = into_string(value); + assert_eq!(result, Some("hello".to_string())); + + let value = Box::new(42); + let result = into_string(value); + assert_eq!(result, Some("42".to_string())); + + let value = Box::new(true); + let result = into_string(value); + assert_eq!(result, None); + } + + #[test] + fn should_retrieve_owned() { + let original_value = String::from("hello"); + let cloned_value = original_value.clone(); + + let value = select_type! {cloned_value, + _ @ Option => panic!("unexpected type"), + _ => cloned_value + }; + + assert_eq!(value.try_take::().unwrap(), *original_value); + } + + #[test] + fn should_allow_mixed_borrows() { + fn process(value: Box) { + select_type! {value, + Option => { + let value = value.unwrap(); + assert_eq!(value, 1.0); + return; + }, + &Option => { + let value = value.as_ref().unwrap(); + assert_eq!(*value, 42); + return; + }, + &mut Option => { + let value = value.as_mut().unwrap(); + value.push_str(" world"); + assert_eq!(*value, "hello world"); + return; + }, + } + + panic!("test should not reach here"); + } + + process(Box::new(Some(String::from("hello")))); + process(Box::new(Some(42_i32))); + process(Box::new(Some(1.0_f32))); + } + + #[test] + fn should_allow_custom_bindings() { + fn process(mut value: Box) { + select_type! {value, + foo @ &mut i32 => { + *foo *= 2; + assert_eq!(*foo, 246); + return; + }, + bar @ &u32 => { + assert_eq!(*bar, 42); + return; + }, + baz @ bool => { + assert!(baz); + return; + }, + } + + panic!("test should not reach here"); + } + + process(Box::new(123_i32)); + process(Box::new(42_u32)); + process(Box::new(true)); + } + + #[test] + fn should_handle_slice_types() { + let _value = "hello world"; + + select_type! {_value, + (&str) => {}, + _ => panic!("unexpected type"), + } + } + + #[test] + fn should_capture_types() { + fn test(mut value: Box) { + select_type! {value, + _ @ &mut u8 [types] => { + assert_eq!(types.len(), 1); + assert_eq!(types[0], Type::of::<&mut u8>()); + }, + _ @ &u16 [types] => { + assert_eq!(types.len(), 2); + assert_eq!(types[0], Type::of::<&mut u8>()); + assert_eq!(types[1], Type::of::<&u16>()); + }, + u32 [types] where value > 10 => { + assert_eq!(types.len(), 3); + assert_eq!(types[0], Type::of::<&mut u8>()); + assert_eq!(types[1], Type::of::<&u16>()); + assert_eq!(types[2], Type::of::()); + }, + _ @ u32 [types] => { + assert_eq!(types.len(), 4); + assert_eq!(types[0], Type::of::<&mut u8>()); + assert_eq!(types[1], Type::of::<&u16>()); + assert_eq!(types[2], Type::of::()); + assert_eq!(types[3], Type::of::()); + }, + _ [types] => { + assert_eq!(types.len(), 4); + assert_eq!(types[0], Type::of::<&mut u8>()); + assert_eq!(types[1], Type::of::<&u16>()); + assert_eq!(types[2], Type::of::()); + assert_eq!(types[3], Type::of::()); + }, + } + } + + test(Box::new(0_u8)); + test(Box::new(0_u16)); + test(Box::new(123_u32)); + test(Box::new(0_u32)); + test(Box::new(0_u64)); + } + + #[test] + fn should_downcast_from_generic() { + fn immutable(value: &T) { + select_type! {value, + &i32 => { + assert_eq!(*value, 1); + }, + _ => panic!("unexpected type"), + } + } + + fn mutable(value: &mut T) { + select_type! {value, + &mut i32 => { + *value = 2; + }, + _ => panic!("unexpected type"), + } + } + + fn owned(value: T) { + select_type! {value, + i32 => { + assert_eq!(value, 2); + }, + _ => panic!("unexpected type"), + } + } + + let mut value = 1_i32; + immutable(&value); + mutable(&mut value); + owned(value); + } + + #[test] + fn should_downcast_to_generic() { + fn immutable>(value: &T) { + select_type! {value, + &U => { + assert_eq!(*value, 1); + }, + _ => panic!("unexpected type"), + } + } + + fn mutable>(value: &mut T) { + select_type! {value, + &mut U => { + *value *= 2; + }, + _ => panic!("unexpected type"), + } + } + + fn owned>(value: T) { + select_type! {value, + U => { + assert_eq!(value, 2); + }, + _ => panic!("unexpected type"), + } + } + + let mut value = 1_i32; + immutable::<_, i32>(&value); + mutable::<_, i32>(&mut value); + owned::<_, i32>(value); + } +} diff --git a/crates/bevy_reflect/src/macros/mod.rs b/crates/bevy_reflect/src/macros/mod.rs new file mode 100644 index 0000000000..c693276277 --- /dev/null +++ b/crates/bevy_reflect/src/macros/mod.rs @@ -0,0 +1,3 @@ +pub use match_type::*; + +mod match_type;