From c45bc476344093367f64b039f94d9d6d163392e2 Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Mon, 17 Nov 2025 21:44:00 -0800 Subject: [PATCH 1/7] add FromInput trait --- crates/bevy_ecs/src/schedule/set.rs | 11 +- crates/bevy_ecs/src/system/function_system.rs | 113 ++++++++++-------- crates/bevy_ecs/src/system/input.rs | 24 ++++ 3 files changed, 93 insertions(+), 55 deletions(-) diff --git a/crates/bevy_ecs/src/schedule/set.rs b/crates/bevy_ecs/src/schedule/set.rs index 7c2d718ed6be8..53c6e1565600e 100644 --- a/crates/bevy_ecs/src/schedule/set.rs +++ b/crates/bevy_ecs/src/schedule/set.rs @@ -14,8 +14,8 @@ use crate::{ define_label, intern::Interned, system::{ - ExclusiveFunctionSystem, ExclusiveSystemParamFunction, FunctionSystem, IntoResult, - IsExclusiveFunctionSystem, IsFunctionSystem, SystemParamFunction, + ExclusiveFunctionSystem, ExclusiveSystemParamFunction, FromInput, FunctionSystem, + IntoResult, IsExclusiveFunctionSystem, IsFunctionSystem, SystemParamFunction, }, }; @@ -291,14 +291,13 @@ impl IntoSystemSet<()> for S { impl IntoSystemSet<(IsFunctionSystem, Marker)> for F where Marker: 'static, - F::Out: IntoResult<()>, - F: SystemParamFunction, + F: SystemParamFunction, Out: IntoResult<()>>, { - type Set = SystemTypeSet>; + type Set = SystemTypeSet>; #[inline] fn into_system_set(self) -> Self::Set { - SystemTypeSet::>::new() + SystemTypeSet::>::new() } } diff --git a/crates/bevy_ecs/src/system/function_system.rs b/crates/bevy_ecs/src/system/function_system.rs index 5f6653be39834..8ca5cea704d92 100644 --- a/crates/bevy_ecs/src/system/function_system.rs +++ b/crates/bevy_ecs/src/system/function_system.rs @@ -6,8 +6,8 @@ use crate::{ query::FilteredAccessSet, schedule::{InternedSystemSet, SystemSet}, system::{ - check_system_change_tick, ReadOnlySystemParam, System, SystemIn, SystemInput, SystemParam, - SystemParamItem, + check_system_change_tick, FromInput, ReadOnlySystemParam, System, SystemIn, SystemInput, + SystemParam, SystemParamItem, }, world::{unsafe_world_cell::UnsafeWorldCell, DeferredWorld, World, WorldId}, }; @@ -234,15 +234,15 @@ macro_rules! impl_build_system { /// You can use [`SystemState::build_system_with_input()`] if you have input, or [`SystemState::build_any_system()`] if you don't need type inference. pub fn build_system< InnerOut: IntoResult, - Out: 'static, + Out, Marker, F: FnMut($(SystemParamItem<$param>),*) -> InnerOut - + SystemParamFunction + + SystemParamFunction > ( self, func: F, - ) -> FunctionSystem + ) -> FunctionSystem { self.build_any_system(func) } @@ -250,17 +250,20 @@ macro_rules! impl_build_system { /// Create a [`FunctionSystem`] from a [`SystemState`]. /// This method signature allows type inference of closure parameters for a system with input. /// You can use [`SystemState::build_system()`] if you have no input, or [`SystemState::build_any_system()`] if you don't need type inference. + #[inline] pub fn build_system_with_input< - Input: SystemInput, + InnerIn: SystemInput + FromInput, + In: SystemInput, InnerOut: IntoResult, - Out: 'static, + Out, Marker, - F: FnMut(Input, $(SystemParamItem<$param>),*) -> InnerOut - + SystemParamFunction, - >( + F: FnMut(InnerIn, $(SystemParamItem<$param>),*) -> InnerOut + + SystemParamFunction + > + ( self, func: F, - ) -> FunctionSystem { + ) -> FunctionSystem { self.build_any_system(func) } } @@ -311,22 +314,20 @@ impl SystemState { /// Create a [`FunctionSystem`] from a [`SystemState`]. /// This method signature allows any system function, but the compiler will not perform type inference on closure parameters. /// You can use [`SystemState::build_system()`] or [`SystemState::build_system_with_input()`] to get type inference on parameters. - pub fn build_any_system(self, func: F) -> FunctionSystem + #[inline] + pub fn build_any_system(self, func: F) -> FunctionSystem where - F: SystemParamFunction>, + In: SystemInput, + F: SystemParamFunction, Out: IntoResult, Param = Param>, { - FunctionSystem { + FunctionSystem::new( func, - #[cfg(feature = "hotpatching")] - current_ptr: subsecond::HotFn::current(>::run) - .ptr_address(), - state: Some(FunctionSystemState { + self.meta, + Some(FunctionSystemState { param: self.param_state, world_id: self.world_id, }), - system_meta: self.meta, - marker: PhantomData, - } + ) } /// Gets the metadata for this instance. @@ -475,7 +476,7 @@ impl FromWorld for SystemState { /// /// The [`Clone`] implementation for [`FunctionSystem`] returns a new instance which /// is NOT initialized. The cloned system must also be `.initialized` before it can be run. -pub struct FunctionSystem +pub struct FunctionSystem where F: SystemParamFunction, { @@ -484,8 +485,8 @@ where current_ptr: subsecond::HotFnPtr, state: Option>, system_meta: SystemMeta, - // NOTE: PhantomData T> gives this safe Send/Sync impls - marker: PhantomData (Marker, Out)>, + // NOTE: PhantomData T> gives this safe Send/Sync impls + marker: PhantomData (Marker, Out)>, } /// The state of a [`FunctionSystem`], which must be initialized with @@ -500,10 +501,23 @@ struct FunctionSystemState { world_id: WorldId, } -impl FunctionSystem +impl FunctionSystem where F: SystemParamFunction, { + #[inline] + fn new(func: F, system_meta: SystemMeta, state: Option>) -> Self { + Self { + func, + #[cfg(feature = "hotpatching")] + current_ptr: subsecond::HotFn::current(>::run) + .ptr_address(), + state, + system_meta, + marker: PhantomData, + } + } + /// Return this system with a new name. /// /// Useful to give closure systems more readable and unique names for debugging and tracing. @@ -514,7 +528,7 @@ where } // De-initializes the cloned system. -impl Clone for FunctionSystem +impl Clone for FunctionSystem where F: SystemParamFunction + Clone, { @@ -535,23 +549,16 @@ where #[doc(hidden)] pub struct IsFunctionSystem; -impl IntoSystem for F +impl IntoSystem for F where - Out: 'static, Marker: 'static, - F: SystemParamFunction>, + In: SystemInput + 'static, + Out: 'static, + F: SystemParamFunction, Out: IntoResult>, { - type System = FunctionSystem; + type System = FunctionSystem; fn into_system(func: Self) -> Self::System { - FunctionSystem { - func, - #[cfg(feature = "hotpatching")] - current_ptr: subsecond::HotFn::current(>::run) - .ptr_address(), - state: None, - system_meta: SystemMeta::new::(), - marker: PhantomData, - } + FunctionSystem::new(func, SystemMeta::new::(), None) } } @@ -596,7 +603,7 @@ impl IntoResult for Never { } } -impl FunctionSystem +impl FunctionSystem where F: SystemParamFunction, { @@ -607,13 +614,14 @@ where "System's state was not found. Did you forget to initialize this system before running it?"; } -impl System for FunctionSystem +impl System for FunctionSystem where Marker: 'static, + In: SystemInput + 'static, Out: 'static, - F: SystemParamFunction>, + F: SystemParamFunction, Out: IntoResult>, { - type In = F::In; + type In = In; type Out = Out; #[inline] @@ -637,6 +645,8 @@ where let change_tick = world.increment_change_tick(); + let input = F::In::from_inner(input); + let state = self.state.as_mut().expect(Self::ERROR_UNINITIALIZED); assert_eq!(state.world_id, world.id(), "Encountered a mismatched World. A System cannot be used with Worlds other than the one it was initialized with."); // SAFETY: @@ -748,12 +758,17 @@ where } /// SAFETY: `F`'s param is [`ReadOnlySystemParam`], so this system will only read from the world. -unsafe impl ReadOnlySystem for FunctionSystem +unsafe impl ReadOnlySystem for FunctionSystem where Marker: 'static, + In: SystemInput + 'static, Out: 'static, - F: SystemParamFunction>, - F::Param: ReadOnlySystemParam, + F: SystemParamFunction< + Marker, + In: FromInput, + Out: IntoResult, + Param: ReadOnlySystemParam, + >, { } @@ -935,7 +950,7 @@ mod tests { use core::any::TypeId; - let system = IntoSystem::into_system(function); + let system = IntoSystem::::into_system(function); assert_eq!( system.type_id(), @@ -951,13 +966,13 @@ mod tests { assert_ne!( system.type_id(), - IntoSystem::into_system(reference_system).type_id(), + IntoSystem::<(), (), _>::into_system(reference_system).type_id(), "Different systems should have different TypeIds" ); } fn function_system() {} - test(function_system); + test::<_, (), (), _>(function_system); } } diff --git a/crates/bevy_ecs/src/system/input.rs b/crates/bevy_ecs/src/system/input.rs index 429f4df018ed6..d9ab4a7e712e7 100644 --- a/crates/bevy_ecs/src/system/input.rs +++ b/crates/bevy_ecs/src/system/input.rs @@ -55,6 +55,30 @@ pub trait SystemInput: Sized { /// Shorthand way to get the [`System::In`] for a [`System`] as a [`SystemInput::Inner`]. pub type SystemIn<'a, S> = <::In as SystemInput>::Inner<'a>; +/// A type that may be constructed from the input of a [`System`]. +/// This is used to allow systems whose first parameter is a `StaticSystemInput` +/// to take an `In` as input, and can be implemented for user types to allow +/// similar conversions. +pub trait FromInput: SystemInput { + /// Converts the system input's inner representation into this type's + /// inner representation. + fn from_inner<'i>(inner: In::Inner<'i>) -> Self::Inner<'i>; +} + +impl FromInput for In { + #[inline] + fn from_inner<'i>(inner: In::Inner<'i>) -> Self::Inner<'i> { + inner + } +} + +impl<'a, In: SystemInput> FromInput for StaticSystemInput<'a, In> { + #[inline] + fn from_inner<'i>(inner: In::Inner<'i>) -> Self::Inner<'i> { + inner + } +} + /// A [`SystemInput`] type which denotes that a [`System`] receives /// an input value of type `T` from its caller. /// From f104b1ff1291a545fad41261958cb4c01545c07f Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Sun, 23 Nov 2025 11:19:19 -0800 Subject: [PATCH 2/7] tests --- crates/bevy_ecs/src/system/function_system.rs | 8 ++++---- crates/bevy_ecs/src/system/input.rs | 17 ++++++++++++++++- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/crates/bevy_ecs/src/system/function_system.rs b/crates/bevy_ecs/src/system/function_system.rs index 8ca5cea704d92..875d2683d896a 100644 --- a/crates/bevy_ecs/src/system/function_system.rs +++ b/crates/bevy_ecs/src/system/function_system.rs @@ -485,7 +485,7 @@ where current_ptr: subsecond::HotFnPtr, state: Option>, system_meta: SystemMeta, - // NOTE: PhantomData T> gives this safe Send/Sync impls + // NOTE: PhantomData T> gives this safe Send/Sync impls marker: PhantomData (Marker, Out)>, } @@ -950,7 +950,7 @@ mod tests { use core::any::TypeId; - let system = IntoSystem::::into_system(function); + let system = IntoSystem::into_system(function); assert_eq!( system.type_id(), @@ -966,13 +966,13 @@ mod tests { assert_ne!( system.type_id(), - IntoSystem::<(), (), _>::into_system(reference_system).type_id(), + IntoSystem::into_system(reference_system).type_id(), "Different systems should have different TypeIds" ); } fn function_system() {} - test::<_, (), (), _>(function_system); + test(function_system); } } diff --git a/crates/bevy_ecs/src/system/input.rs b/crates/bevy_ecs/src/system/input.rs index d9ab4a7e712e7..b9143f2079bea 100644 --- a/crates/bevy_ecs/src/system/input.rs +++ b/crates/bevy_ecs/src/system/input.rs @@ -318,7 +318,7 @@ all_tuples!( #[cfg(test)] mod tests { use crate::{ - system::{In, InMut, InRef, IntoSystem, System}, + system::{assert_is_system, In, InMut, InRef, IntoSystem, StaticSystemInput, System}, world::World, }; @@ -351,4 +351,19 @@ mod tests { by_mut.run((&mut a, b), &mut world).unwrap(); assert_eq!(a, 36); } + + #[test] + fn compatible_input() { + fn takes_usize(In(a): In) -> usize { + a + } + + fn takes_static_usize(StaticSystemInput(In(b)): StaticSystemInput>) -> usize { + b + } + + assert_is_system::, usize, _>(takes_usize); + // test if StaticSystemInput is compatible with its inner type + assert_is_system::, usize, _>(takes_static_usize); + } } From 5b40252379e4088a65ae9f8c4d9dcf3ffc8c519a Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Sat, 22 Nov 2025 11:55:58 -0800 Subject: [PATCH 3/7] docs and migration guide --- .../function_system_generics.md | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 release-content/migration-guides/function_system_generics.md diff --git a/release-content/migration-guides/function_system_generics.md b/release-content/migration-guides/function_system_generics.md new file mode 100644 index 0000000000000..d9ade91543274 --- /dev/null +++ b/release-content/migration-guides/function_system_generics.md @@ -0,0 +1,21 @@ +--- +title: "FunctionSystem Generics" +authors: ["@ecoskey"] +pull_requests: [todo!()] +--- + +`FunctionSystem` now has a new generic parameter: `In`. + +Old: `FunctionSystem` +New: `FunctionSystem` + +Additionally, there's an extra bound on the `System` and `IntoSystem` impls +related to `FunctionSystem`: + +`::In: FromInput` + +This enabled systems to take as input any *compatible* type, in addition to the +exact one specified by the system function. This shouldn't impact users at all +since it only adds functionality, but users writing heavily generic code may +want to add a similar bound. See `function_system.rs` to see how it works in +practice. From e606143aac5e22c31a6da3f02500e4e1381886a9 Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Sun, 23 Nov 2025 12:01:15 -0800 Subject: [PATCH 4/7] fix pr number --- release-content/migration-guides/function_system_generics.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release-content/migration-guides/function_system_generics.md b/release-content/migration-guides/function_system_generics.md index d9ade91543274..a302dd72cf329 100644 --- a/release-content/migration-guides/function_system_generics.md +++ b/release-content/migration-guides/function_system_generics.md @@ -1,7 +1,7 @@ --- title: "FunctionSystem Generics" authors: ["@ecoskey"] -pull_requests: [todo!()] +pull_requests: [21917] --- `FunctionSystem` now has a new generic parameter: `In`. From 826d1c0f6fa92d01b25c7355211b9b307a076060 Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Sun, 23 Nov 2025 12:15:51 -0800 Subject: [PATCH 5/7] fix tests --- crates/bevy_ecs/src/system/input.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ecs/src/system/input.rs b/crates/bevy_ecs/src/system/input.rs index b9143f2079bea..c37a08cd65be1 100644 --- a/crates/bevy_ecs/src/system/input.rs +++ b/crates/bevy_ecs/src/system/input.rs @@ -358,7 +358,7 @@ mod tests { a } - fn takes_static_usize(StaticSystemInput(In(b)): StaticSystemInput>) -> usize { + fn takes_static_usize(StaticSystemInput(b): StaticSystemInput>) -> usize { b } From dfb39d0b0d12c9f17ad8c08ca2350d8076b337ff Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Thu, 27 Nov 2025 21:28:49 -0800 Subject: [PATCH 6/7] fix tests --- crates/bevy_ecs/src/system/function_system.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/bevy_ecs/src/system/function_system.rs b/crates/bevy_ecs/src/system/function_system.rs index 875d2683d896a..fda251373c355 100644 --- a/crates/bevy_ecs/src/system/function_system.rs +++ b/crates/bevy_ecs/src/system/function_system.rs @@ -814,8 +814,10 @@ where /// let mut world = World::default(); /// world.insert_resource(Message("42".to_string())); /// -/// // pipe the `parse_message_system`'s output into the `filter_system`s input -/// let mut piped_system = IntoSystem::into_system(pipe(parse_message, filter)); +/// // pipe the `parse_message_system`'s output into the `filter_system`s input. +/// // Type annotations should only needed when using `StaticSystemInput` as input +/// // AND the input type isn't constrained by nearby code. +/// let mut piped_system = IntoSystem::<(), Option, _>::into_system(pipe(parse_message, filter)); /// piped_system.initialize(&mut world); /// assert_eq!(piped_system.run((), &mut world).unwrap(), Some(42)); /// } From 2cb66e75df1dcac757971fe9e27705599300006b Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Thu, 27 Nov 2025 22:06:30 -0800 Subject: [PATCH 7/7] what's better than perfect? STANDARDIZED!!! --- crates/bevy_ecs/src/system/function_system.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/bevy_ecs/src/system/function_system.rs b/crates/bevy_ecs/src/system/function_system.rs index fda251373c355..2543051924b6d 100644 --- a/crates/bevy_ecs/src/system/function_system.rs +++ b/crates/bevy_ecs/src/system/function_system.rs @@ -232,6 +232,7 @@ macro_rules! impl_build_system { /// Create a [`FunctionSystem`] from a [`SystemState`]. /// This method signature allows type inference of closure parameters for a system with no input. /// You can use [`SystemState::build_system_with_input()`] if you have input, or [`SystemState::build_any_system()`] if you don't need type inference. + #[inline] pub fn build_system< InnerOut: IntoResult, Out,