-
-
Notifications
You must be signed in to change notification settings - Fork 3.9k
Have System::run_unsafe
return Result
.
#19145
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
base: main
Are you sure you want to change the base?
Conversation
Move safety comments outside of their unsafe blocks.
@@ -443,7 +443,7 @@ impl<'w, 's> Commands<'w, 's> { | |||
/// // Return from the system successfully. | |||
/// Ok(()) | |||
/// } | |||
/// # bevy_ecs::system::assert_is_system(example_system); | |||
/// # bevy_ecs::system::assert_is_system::<(), (), _>(example_system); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is one of the type inference failures. example_system
returns Result
, so it may be a fallible ()
system or an infallible Result
system.
@@ -59,7 +59,7 @@ pub type BoxedCondition<In = ()> = Box<dyn ReadOnlySystem<In = In, Out = bool>>; | |||
/// ``` | |||
/// # use bevy_ecs::prelude::*; | |||
/// fn identity() -> impl Condition<(), In<bool>> { | |||
/// IntoSystem::into_system(|In(x)| x) | |||
/// IntoSystem::into_system(|In(x): In<bool>| x) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is another type inference failure. It has something to do with the fact that the output type is tied to the input type, as changing it to return true
instead of x
works, but I don't entirely understand why the compiler can't figure out In<bool>
from the return type of identity
.
/// run_system: impl FnOnce(SystemIn<'_, S>) -> Result<S::Out, RunSystemError>, | ||
/// ) -> Result<Self::Out, RunSystemError> { | ||
/// let result = run_system(input)?; | ||
/// Ok(!result) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Writing this in one expression as Ok(!(run_system(input)?))
fails, and I don't entirely understand why.
error[E0308]: `?` operator has incompatible types
--> crates\bevy_ecs\src\system\adapter_system.rs:39:14
|
28 | Ok(!(run_system(input)?))
| ^^^^^^^^^^^^^^^^^^ expected `std::ops::Not::Output`, found `bevy_ecs::system::System::Out`
|
= note: `?` operator cannot convert from `<S as bevy_ecs::system::System>::Out` to `<<S as bevy_ecs::system::System>::Out as Not>::Output`
= note: expected associated type `<<S as bevy_ecs::system::System>::Out as Not>::Output`
found associated type `<S as bevy_ecs::system::System>::Out`
= note: an associated type was expected, but a different one was found
Why would it expect Not::Output
as the input to !
???
a: impl FnOnce(SystemIn<'_, A>) -> Result<A::Out, RunSystemError>, | ||
b: impl FnOnce(SystemIn<'_, A>) -> Result<B::Out, RunSystemError>, | ||
) -> Result<Self::Out, RunSystemError> { | ||
Ok(a(input)? || b(input)?) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wrote it this way for consistency, but the short-circuiting behavior of a.or(b)
is a little odd. We normally treat failures in run conditions as false
-like, but or
short circuits on true
and failure, and only calls the second condition on Ok(false)
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was told this short-circuiting was preferable in review to avoid breaking existing code and reduce pointless failures.
I'm not sure I agree still, but it's best to split that change out.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was told this short-circuiting was preferable in review to avoid breaking existing code and reduce pointless failures.
I'm not sure I agree still, but it's best to split that change out.
Sorry, I don't quite follow. What would you like me to split out?
The core change here is making System::run
return a Result
, so we have to do something to handle the case where fallible systems are used with an or
combinator. Adding ?
s and wrapping it in Ok
seemed like the simplest change, and was consistent with the other combinators, so that's what I did. But it's also kind of weird, so I wanted to highlight it.
I think the only behavior change to existing code is that a failing Single
or Populated
in the second system will now return Err
and be treated as false
, while before this it would skip validation entirely and then panic.
#[error("System {system} did not run due to failed parameter validation: {err}")] | ||
InvalidParams { | ||
/// The identifier of the system that was run. | ||
system: Cow<'static, str>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This used to include the system name for context, but I removed it so that I could implement From
and use ?
, since the system name isn't available in From
. The caller will know which system they were running, so they can recover the context if necessary.
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 run conditions.
Always validate parameters when calling the safe
run_without_applying_deferred
,run
, andrun_readonly
methods on aSystem
.Solution
Have
System::run_unsafe
return aResult
.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 chaininga.pipe(b).pipe(c)
becomes difficult.c
would need to accept theResult
froma.pipe(b)
, which means it would likely need to returnResult
itself, givingResult<Result<Out>>
!Instead, we make all systems return a
Result
! We move the handling of fallible systems fromIntoScheduleConfigs
andIntoObserverSystem
toSystemParamFunction
andExclusiveSystemParamFunction
, 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.
Now that the safe
run_without_applying_deferred
,run
, andrun_readonly
methods return aResult
, 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 returningT
or an infallible system returningResult<T>
(and this is important to continue supportingpipe
-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()
(orbool
for run conditions). And it will work fine whenpipe
ing to a system with a typed input parameter.I used a dedicated
RunSystemError
for the error type instead of plainBevyError
so that skipping a system does not box an error or capture a backtrace.