Skip to content

Commit

Permalink
SystemParamBuilder - Enable type inference of closure parameter when …
Browse files Browse the repository at this point in the history
…building dynamic systems (bevyengine#14820)

# Objective

When building a system from `SystemParamBuilder`s and defining the
system as a closure, the compiler should be able to infer the parameter
types from the builder types.

## Solution

Create methods for each arity that take an argument that implements both
`SystemParamFunction` as well as `FnMut(SystemParamItem<P>,...)`. The
explicit `FnMut` constraint will allow the compiler to infer the
necessary higher-ranked lifetimes along with the parameter types.

I wanted to show that this was possible, but I can't tell whether it's
worth the complexity. It requires a separate method for each arity,
which pollutes the docs a bit:
![SystemState build_system
docs](https://github.com/user-attachments/assets/5069b749-7ec7-47e3-a5e4-1a4c78129f78)

## Example

```rust
let system = (LocalBuilder(0u64), ParamBuilder::local::<u64>())
    .build_state(&mut world)
    .build_system(|a, b| *a + *b + 1);
```
  • Loading branch information
chescock authored Aug 28, 2024
1 parent 8895113 commit 419359b
Show file tree
Hide file tree
Showing 2 changed files with 64 additions and 1 deletion.
15 changes: 15 additions & 0 deletions crates/bevy_ecs/src/system/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,21 @@ mod tests {
assert_eq!(result, 3);
}

#[test]
fn multi_param_builder_inference() {
let mut world = World::new();

world.spawn(A);
world.spawn_empty();

let system = (LocalBuilder(0u64), ParamBuilder::local::<u64>())
.build_state(&mut world)
.build_system(|a, b| *a + *b + 1);

let result = world.run_system_once(system);
assert_eq!(result, 1);
}

#[test]
fn param_set_builder() {
let mut world = World::new();
Expand Down
50 changes: 49 additions & 1 deletion crates/bevy_ecs/src/system/function_system.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,52 @@ pub struct SystemState<Param: SystemParam + 'static> {
archetype_generation: ArchetypeGeneration,
}

// Allow closure arguments to be inferred.
// For a closure to be used as a `SystemParamFunction`, it needs to be generic in any `'w` or `'s` lifetimes.
// Rust will only infer a closure to be generic over lifetimes if it's passed to a function with a Fn constraint.
// So, generate a function for each arity with an explicit `FnMut` constraint to enable higher-order lifetimes,
// along with a regular `SystemParamFunction` constraint to allow the system to be built.
macro_rules! impl_build_system {
($($param: ident),*) => {
impl<$($param: SystemParam),*> SystemState<($($param,)*)> {
/// 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.
pub fn build_system<
Out: 'static,
Marker,
F: FnMut($(SystemParamItem<$param>),*) -> Out
+ SystemParamFunction<Marker, Param = ($($param,)*), In = (), Out = Out>
>
(
self,
func: F,
) -> FunctionSystem<Marker, F>
{
self.build_any_system(func)
}

/// 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.
pub fn build_system_with_input<
Input,
Out: 'static,
Marker,
F: FnMut(In<Input>, $(SystemParamItem<$param>),*) -> Out
+ SystemParamFunction<Marker, Param = ($($param,)*), In = Input, Out = Out>,
>(
self,
func: F,
) -> FunctionSystem<Marker, F> {
self.build_any_system(func)
}
}
}
}

all_tuples!(impl_build_system, 0, 16, P);

impl<Param: SystemParam> SystemState<Param> {
/// Creates a new [`SystemState`] with default state.
///
Expand Down Expand Up @@ -242,7 +288,9 @@ impl<Param: SystemParam> SystemState<Param> {
}

/// Create a [`FunctionSystem`] from a [`SystemState`].
pub fn build_system<Marker, F: SystemParamFunction<Marker, Param = Param>>(
/// 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<Marker, F: SystemParamFunction<Marker, Param = Param>>(
self,
func: F,
) -> FunctionSystem<Marker, F> {
Expand Down

0 comments on commit 419359b

Please sign in to comment.