Skip to content

Conversation

@ecoskey
Copy link
Contributor

@ecoskey ecoskey commented Nov 12, 2025

Objective

NOTE: blocked on #21917 and #21923

resolves #16680

Add a new system param for running systems inside other systems. Also, I've included some macros for nice syntax on top.

I'm pretty proud of how nice I was able to make this, but there's still a bit of work to do, especially around generic code :)

Testing

  • Ran examples
  • Tried to migrate existing pipe and map impls, but failed. This PR is pretty robust for most use cases but isn't fully ready for generic code yet. In particular, it's difficult to make sure the input types match (often have to wrap with StaticSystemInput) and ReadOnlySystem bound is inferred correctly when using for run conditions. These only really matter for combinators like pipe and map though, since otherwise they're run exactly the same.

Showcase

Click to view showcase
fn count_a(a: Query<&A>) -> u32 {
    a.count()
}

fn count_b(b: Query<&B>) -> u32 {
    b.count()
}

let get_sum = (
    ParamBuilder::system(count_a),
    ParamBuilder::system(count_b)
)
.build_system(
    |mut run_a: SystemRunner<(), u32>, mut run_b: SystemRunner<(), u32>| -> Result<u32, RunSystemError> {
        let a = run_a.run()?;
        let b = run_b.run()?;
        Ok(a + b)
    }
);

let get_sum = compose! {
    || -> Result<u32, RunSystemError> {
        let a = run!(count_a)?;
        let b = run!(count_b)?;
        Ok(a + b)
    }
}

@ecoskey ecoskey force-pushed the feature/system_runner branch from bbc70c7 to e0c905c Compare November 12, 2025 01:23
@ecoskey ecoskey added C-Feature A new feature, making something new possible A-ECS Entities, components, systems, and events D-Complex Quite challenging from either a design or technical perspective. Ask for help! M-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide M-Release-Note Work that should be called out in the blog due to impact labels Nov 12, 2025
@ecoskey ecoskey requested a review from chescock November 12, 2025 01:28
Copy link
Contributor

@chescock chescock left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is really cool!

I think this will close #16680, so you might want to link that in the PR description.

I left a lot of comments, but they're mostly style nitpicks and brainstorming for future possibilities. The only thing that I think really needs attention is the map_err(RunSystemError::Skipped) part, since that will silently ignore missing resources. And the CI failure :).

Uninitialized {
builder: Builder,
func: Func,
meta: SystemMeta,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this here only to support with_name? (It's also used in get_last_run/set_last_run, but I think those are only supposed to be valid on initialized systems.) It might be better to just store a DebugName. If the builder is large, this could wind up being the largest variant and taking up space even after the system is built.

/// ParamBuilder::system(count_b)
/// )
/// .build_state(&mut world)
/// .build_system(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The other CI failure is because it can't tell whether the return type is u32 or Result<u32>. You can supply it like

Suggested change
/// .build_system(
/// .build_system::<_, u32, _, _>(

But it might look nicer to run the system and annotate the result, like

/// let result: usize = world.run_system_once(get_sum).unwrap();

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how does add_systems infer which to use?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how does add_systems infer which to use?

It just always requires Out = () for systems and Out = bool for conditions.

Copy link
Contributor Author

@ecoskey ecoskey Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yeah true. And the other combinators can set it explicitly, sounds good.

/// ```ignore
/// let system_a = |world: &mut World| { 10 };
/// let system_b = |a: In<u32>, world: &mut World| { println!("{}", *a + 12) };
/// compose! {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to accept a function definition instead of or in addition to a closure? That would make it easier for these systems to have names. Something like

compose! {
    fn system_name()  -> Result<(), RunSystemError> {
        run!(system_a)
    }
}

For that matter, would it make sense to use an attribute macro, like

#[compose]
fn system_name() -> Result<(), RunSystemError> {
    run!(system_a)
}

?

... Oh, maybe not, because what you'd really want that to expand to is const system_name: impl System = const { ... };, but there's no way to write the type for the const.

@chescock
Copy link
Contributor

  • Tried to migrate existing pipe and map impls, but failed. This PR is pretty robust for most use cases but isn't fully ready for generic code yet. In particular, it's difficult to make sure the input types match (often have to wrap with StaticSystemInput) and ReadOnlySystem bound is inferred correctly when using for run conditions. These only really matter for combinators like pipe and map though, since otherwise they're run exactly the same.

I think I got pipe and and mostly working on this branch! I had to add lots of 'static annotations, but I think those are mostly harmless.

As you mentioned, the big problem is that the resulting systems take StaticSystemInput<In> instead of In, which means they can't be used in the schedule because that expects In = () and not In = StaticSystemInput<()>. Maybe we could create a wrapper type that converts a System<In = StaticSystemInput<In>> to a System<In = In>?

pub trait IntoSystem<In: SystemInput, Out, Marker>: Sized {
// ...
    fn pipe2<B, BIn, BOut, MarkerB>(
        self,
        system: B,
    ) -> impl System<In = StaticSystemInput<'static, In>, Out = BOut>
    where
        Out: 'static,
        B: IntoSystem<BIn, BOut, MarkerB> + 'static,
        for<'a> BIn: SystemInput<Inner<'a> = Out> + 'static,
        In: 'static,
        BOut: 'static,
        Marker: 'static,
        MarkerB: 'static,
        Self: 'static,
    {
        compose_with!(
            |StaticSystemInput(input): StaticSystemInput<In>| -> Result<BOut, RunSystemError> {
                let value = run!(self, input)?;
                run!(system, value)
            }
        )
    }
pub trait SystemCondition<Marker, In: SystemInput = ()>:
// ...
    fn and2<M: 'static, C: SystemCondition<M, In> + 'static>(
        self,
        and: C,
    ) -> impl ReadOnlySystem<In = StaticSystemInput<'static, In>, Out = bool>
    where
        for<'a> In: SystemInput<Inner<'a>: Copy> + 'static,
        Self: 'static,
        Marker: 'static,
    {
        compose_with!(
            |StaticSystemInput(input): StaticSystemInput<In>| -> Result<bool, RunSystemError> {
                Ok(run!(self, input)? && run!(and, input)?)
            }
        )
    }

@ecoskey
Copy link
Contributor Author

ecoskey commented Nov 12, 2025

Thanks for the input! I'll spin up a few PRs for BuilderSystem and RemapInputSystem (or whatever we call it) and start cleaning things up tonight or tomorrow

Also I realized SystemInput::unwrap is probably unnecessary since you can just destructure StaticSystemInput :P

@ecoskey ecoskey force-pushed the feature/system_runner branch from cf12de9 to 6443d66 Compare November 23, 2025 21:08
@ecoskey ecoskey added the S-Blocked This cannot move forward until something else changes label Nov 24, 2025
@ecoskey ecoskey force-pushed the feature/system_runner branch from 6443d66 to c8cfcc1 Compare November 27, 2025 05:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-ECS Entities, components, systems, and events C-Feature A new feature, making something new possible D-Complex Quite challenging from either a design or technical perspective. Ask for help! M-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide M-Release-Note Work that should be called out in the blog due to impact S-Blocked This cannot move forward until something else changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SystemRunner param - run systems inside other systems

2 participants