Skip to content

Impl system condition combinators for Result-ful run conditions #19580

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 17 commits into from

Conversation

gwafotapa
Copy link
Contributor

@gwafotapa gwafotapa commented Jun 11, 2025

Objective

This is a follow-up of #19553.
I missed something. Right now the combinators (and, or, ...) only work with conditions returning bool. The same goes for condition_changed, condition_changed_to and not. The objective is to fix that to allow conditions returning Result<(), BevyError> and Result<bool, BevyError> to use these methods as well.

Fixes #19527.

Solution

It's essentially just about calling into_condition_system() in the methods. I just have a problem for not because it's implemented differently than the others. Currently not can accept any system whose output implement Not. That's a problem for systems returning Result<(), BevyError> or Result<bool, BevyError>. I see mostly two options:

  1. Leave things as they are and the new conditions cannot be used with not.
  2. Limit not to accept only run conditions. This way not could accept the new run conditions but not any system whose output implements Not.

I would say the second solution has more upsides than downsides because

  • Use cases of not with Not feels more niche than use cases of not with the new conditions
  • It is more coherent (being able to use the new run conditions with run_if but not not is confusing)
  • To invert a system whose output implements Not you can still do system.map(|x| !x)

but I may be missing something. What do you think ?

Testing

I also added a simple test for all the missing methods.

@alice-i-cecile alice-i-cecile changed the title Impl system condition Impl system condition combinators for Result-ful run conditions Jun 11, 2025
@alice-i-cecile alice-i-cecile added A-ECS Entities, components, systems, and events C-Usability A targeted quality-of-life change that makes Bevy easier to use S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Jun 11, 2025
@alice-i-cecile
Copy link
Member

@urben1680 want to review this one too?

@urben1680
Copy link
Contributor

I think into_condition_system is the right approach, it aligns into how bevy turns fallible and infallible systems into the same output (Result<(), BevyError>) via the IntoScheduleConfigs impl.

Regarding the Not case I also favor the second solution. It should be consistent.

When this leaves Draft I do a proper review. 👍

@gwafotapa gwafotapa marked this pull request as ready for review June 12, 2025 14:33
@gwafotapa
Copy link
Contributor Author

@urben1680 PR is ready for review.

Copy link
Contributor

@urben1680 urben1680 left a comment

Choose a reason for hiding this comment

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

I would only like to see a change about the not return type becoming anonymous, the other things do not bother me enough. 😄

Comment on lines 1077 to 1085
pub fn not<Marker, In, Out, C>(
condition: C,
) -> AdapterSystem<fn(bool) -> bool, C::ConditionSystem>
where
TOut: core::ops::Not,
T: IntoSystem<(), TOut, Marker>,
In: SystemInput,
C: SystemCondition<Marker, In, Out>,
{
let condition = IntoSystem::into_system(condition);
let name = format!("!{}", condition.name());
NotSystem::new(super::NotMarker, condition, name.into())
let f: fn(bool) -> bool = |x| !x;
IntoSystem::into_system(condition.into_condition_system().map(f))
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we can return an impl SystemCondition<(), In> here so you don't need to turn that mapping function into a function pointer which requires slower dynamic dispatch.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Returning impl SystemCondition<(), In> caused the compiler to complain about a missing Clone bound. I finally went another way. Using the approach in NotSystem of defining a helper type, I removed the use of a function pointer. I also reworked on the implementations of SystemCondition to remove function pointers there as well.

Comment on lines -1161 to -1164
/// Invokes [`Not`] with the output of another system.
///
/// See [`common_conditions::not`] for examples.
pub type NotSystem<S> = AdapterSystem<NotMarker, S>;
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder why this explicit type was needed and why it needed to be public when all other methods returned impl ....

I think there are two alternatives here:

  1. Write a migration guide that this type alias is gone.
  2. Stick to the explicit type returned by not and change this to this: (maybe with a #[deprecated] as it is unused in the engine now)
type NotSystem<T, In = (), Out = bool> = AdapterSystem<fn(bool) -> bool, <T as SystemCondition<(), In, Out>>::ConditionSystem>;

I strongly prefer 1, also because I think 2 does not technically resemble the original NotSystem alias depending on how exotic the system output were with that Not impl. I am not even sure 2. works.

But I am fine with not do anything here as I assume nobody used this alias.

Copy link
Contributor Author

@gwafotapa gwafotapa Jun 14, 2025

Choose a reason for hiding this comment

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

I don't know why this was needed either. I wrote the migration guide. I also removed a line in the documentation example of trait Adapt.

@@ -1214,7 +1232,7 @@ where
fn combine(
input: <Self::In as SystemInput>::Inner<'_>,
a: impl FnOnce(SystemIn<'_, A>) -> A::Out,
b: impl FnOnce(SystemIn<'_, A>) -> B::Out,
Copy link
Contributor

Choose a reason for hiding this comment

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

Huh, was this ever working? If not, was this ever used?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As far as I can tell, this was only ever used in conjunction with run_if which only accepts a system with In = (). So this never came into play as this is always called with In = () for both systems A and B. But this might change in the future so we may as well correct it now.

Comment on lines +1484 to +1485
counter *= 3 * 7 * 13 * 17 * 23 * 29;
assert_eq!(world.resource::<Counter>().0, counter);
Copy link
Contributor

Choose a reason for hiding this comment

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

These tests are ridiculous because who reads them must first understand that these primes can only multiply to the correct value if the expected conditions ran. But all previous tests here look like that so I guess it is fine. I just wanted to say it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Noted. I thought the intent was clear and it is concise this way.

@urben1680
Copy link
Contributor

This PR seems to fix #19527

@gwafotapa
Copy link
Contributor Author

gwafotapa commented Jun 14, 2025

This PR seems to fix #19527

Indeed it does. Assuming @lotus128 was using it with a system returning a bool, not whose output required Not.

@@ -16,7 +16,6 @@ use crate::{
/// use bevy_ecs::system::{Adapt, AdapterSystem};
///
/// // A system adapter that inverts the result of a system.
/// // NOTE: Instead of manually implementing this, you can just use `bevy_ecs::schedule::common_conditions::not`.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

not() cannot accept any system whose output is Not anymore so this is not a manual implementation of it anymore.

/// Used with [`AdapterSystem`] to inverse the `bool` returned by the system.
#[doc(hidden)]
#[derive(Clone, Copy)]
pub struct InverseBoolAdapter;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This used to be NotMarker. It's now outputting a bool instead of a Not. Thought the name bad.

@alice-i-cecile alice-i-cecile added this to the 0.17 milestone Jun 14, 2025
@chescock
Copy link
Contributor

Note that #19145 would address the same issue with an incompatible approach, by making all systems return Result.

github-merge-queue bot pushed a commit that referenced this pull request Jul 3, 2025
# Objective

Allow combinator and pipe systems to delay validation of the second
system, while still allowing the second system to be skipped.

Fixes #18796

Allow fallible systems to be used as one-shot systems, reporting errors
to the error handler when used through commands.

Fixes #19722

Allow fallible systems to be used as run conditions, including when used
with combinators. Alternative to #19580.

Always validate parameters when calling the safe
`run_without_applying_deferred`, `run`, and `run_readonly` methods on a
`System`.

## Solution

Have `System::run_unsafe` return a `Result`.  

We want pipe systems to run the first system before validating the
second, since the first system may affect whether the second system has
valid parameters. But if the second system skips then we have no output
value to return! So, pipe systems must return a `Result` that indicates
whether the second system ran.

But if we just make pipe systems have `Out = Result<B::Out>`, then
chaining `a.pipe(b).pipe(c)` becomes difficult. `c` would need to accept
the `Result` from `a.pipe(b)`, which means it would likely need to
return `Result` itself, giving `Result<Result<Out>>`!

Instead, we make *all* systems return a `Result`! We move the handling
of fallible systems from `IntoScheduleConfigs` and `IntoObserverSystem`
to `SystemParamFunction` and `ExclusiveSystemParamFunction`, so that an
infallible system can be wrapped before being passed to a combinator.

As a side effect, this enables fallible systems to be used as run
conditions and one-shot systems.

Now that the safe `run_without_applying_deferred`, `run`, and
`run_readonly` methods return a `Result`, we can have them perform
parameter validation themselves instead of requiring each caller to
remember to call them. `run_unsafe` will continue to not validate
parameters, since it is used in the multi-threaded executor when we want
to validate and run in separate tasks.

Note that this makes type inference a little more brittle. A function
that returns `Result<T>` can be considered either a fallible system
returning `T` or an infallible system returning `Result<T>` (and this is
important to continue supporting `pipe`-based error handling)! So there
are some cases where the output type of a system can no longer be
inferred. It will work fine when directly adding to a schedule, since
then the output type is fixed to `()` (or `bool` for run conditions).
And it will work fine when `pipe`ing to a system with a typed input
parameter.

I used a dedicated `RunSystemError` for the error type instead of plain
`BevyError` so that skipping a system does not box an error or capture a
backtrace.
@alice-i-cecile
Copy link
Member

@gwafotapa, #19145 has been merged. Is this still needed? I'm removing it from the 0.17 milestone, as this does not appear to be critical and may be obsolete.

@alice-i-cecile alice-i-cecile removed this from the 0.17 milestone Jul 29, 2025
@alice-i-cecile alice-i-cecile added S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged and removed S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Jul 29, 2025
@gwafotapa
Copy link
Contributor Author

Is this still needed?

@alice-i-cecile Not anymore.

@gwafotapa gwafotapa closed this Jul 29, 2025
@gwafotapa gwafotapa deleted the impl_system_condition branch July 30, 2025 07:17
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-Usability A targeted quality-of-life change that makes Bevy easier to use S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged
Projects
None yet
Development

Successfully merging this pull request may close these issues.

bevy::ecs::schedule::common_conditions::not should support SystemInput
4 participants