-
-
Notifications
You must be signed in to change notification settings - Fork 4.1k
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
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.
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.
My preference was to leave the behavior completely unchanged in this PR. That said, I prefer this behavior and won't block on it.
crates/bevy_ecs/src/system/system.rs
Outdated
#[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.
# Conflicts: # crates/bevy_ecs/src/observer/runner.rs
# Conflicts: # crates/bevy_ecs/src/observer/runner.rs # crates/bevy_ecs/src/schedule/condition.rs # crates/bevy_ecs/src/schedule/executor/multi_threaded.rs # crates/bevy_ecs/src/system/observer_system.rs # crates/bevy_ecs/src/system/schedule_system.rs # crates/bevy_ecs/src/system/system.rs
# Conflicts: # crates/bevy_ecs/src/observer/runner.rs # crates/bevy_ecs/src/schedule/executor/mod.rs # crates/bevy_ecs/src/schedule/executor/simple.rs # crates/bevy_ecs/src/schedule/executor/single_threaded.rs # crates/bevy_ecs/src/system/exclusive_function_system.rs # crates/bevy_ecs/src/system/function_system.rs # crates/bevy_ecs/src/system/observer_system.rs # crates/bevy_ecs/src/system/schedule_system.rs # crates/bevy_ecs/src/system/system.rs # crates/bevy_ecs/src/system/system_param.rs # crates/bevy_ecs/src/system/system_registry.rs
This uses less code, and is more compatible with the hotpatching implementation.
# Conflicts: # crates/bevy_ecs/src/schedule/executor/mod.rs # crates/bevy_ecs/src/schedule/executor/multi_threaded.rs # crates/bevy_ecs/src/schedule/executor/simple.rs # crates/bevy_ecs/src/schedule/executor/single_threaded.rs # crates/bevy_ecs/src/system/exclusive_function_system.rs # crates/bevy_ecs/src/system/function_system.rs # crates/bevy_ecs/src/system/observer_system.rs # crates/bevy_ecs/src/system/schedule_system.rs # crates/bevy_ecs/src/system/system.rs
…BevyError> and Result<(), BevyError> (bevyengine#19553)" This reverts commit 0e805eb.
This reverts commit d1c6fbe.
# Conflicts: # crates/bevy_ecs/src/observer/runner.rs # crates/bevy_ecs/src/system/observer_system.rs # crates/bevy_ecs/src/system/system.rs # crates/bevy_ecs/src/system/system_registry.rs
Okay, I got this working again! My original approach conflicted with the changes for hotpatching, but I managed to make a simpler approach work that removes the conflict. I did run into a few conflicts with other recent PRs that were making fallible systems work in more places. I mostly reverted them in the hope that this PR supersedes them, but tried to take any unit tests. #19678 made one-shot systems accept any output type. When combined with this PR, passing a #19553 made conditions accept One of the goals of this PR is to treat returned errors through the same path as parameter validation errors, so treating them differently in conditions would be inconsistent. In particular, pipe and combinator systems return validation errors from the second system as errors, so But based on the comments in #19403, I expect that change to be controversial. |
} | ||
} | ||
} | ||
if let Err(RunSystemError::Failed(err)) = (*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.
Idly interested in benchmarks of this. Removing the branch here should be net positive.
pub enum RunSystemError { | ||
/// System could not be run due to parameters that failed validation. | ||
/// This should not be considered an error if [`field@SystemParamValidationError::skipped`] is `true`. | ||
#[error("System {system} did not run due to failed parameter validation: {err}")] | ||
InvalidParams { | ||
/// The identifier of the system that was run. | ||
system: DebugName, | ||
/// The returned parameter validation error. | ||
err: SystemParamValidationError, | ||
}, | ||
/// This is not considered an error. | ||
Skipped(SystemParamValidationError), | ||
/// System returned an error or failed required parameter validation. | ||
Failed(BevyError), | ||
} |
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.
Ooh I prefer this framing. There's been some ambiguity with what returning Result::Err
from a system really means. Is it an error, or is it just part of normal control flow. This tells us: The system failed to complete, which is explicitly an error.
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 like this framing too.
} | ||
} | ||
|
||
/// Running system failed. | ||
#[derive(Error, Debug)] | ||
#[derive(Debug)] | ||
pub enum RunSystemError { |
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.
Bikeshed: Might we want to call this RunSystemResult
or something? It has Error
in the name and then the next line says "this is not considered an error".
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.
It has
Error
in the name and then the next line says "this is not considered an error".
Hah! Fair :).
Might we want to call this
RunSystemResult
or something?
I would like that in isolation, but Result<Out, RunSystemResult>
seems odd.
I'm happy to change it if there is a strong alternative, but I might be too stuck thinking that the thing that goes in a Result::Err
is an "error" to come up with a better name myself.
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.
Excellent migration guide.
# Conflicts: # crates/bevy_utils/src/debug_info.rs
# Conflicts: # crates/bevy_ecs/src/schedule/executor/mod.rs
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 prefer the framing, it plugs multiple consistency holes in our API design, it's noticeably simpler and the migration guide is excellent. Very nice work; merging.
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
, 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 and one-shot systems.
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.