Skip to content

Commit 9254297

Browse files
alice-i-cecilecart
andauthored
Use never_say_never hack to work around Rust 2024 regression for fn traits (#18804)
# Objective After #17967, closures which always panic no longer satisfy various Bevy traits. Principally, this affects observers, systems and commands. While this may seem pointless (systems which always panic are kind of useless), it is distinctly annoying when using the `todo!` macro, or when writing tests that should panic. Fixes #18778. ## Solution - Add failing tests to demonstrate the problem - Add the trick from [`never_say_never`](https://docs.rs/never-say-never/latest/never_say_never/) to name the `!` type on stable Rust - Write looots of docs explaining what the heck is going on and why we've done this terrible thing ## To do Unfortunately I couldn't figure out how to avoid conflicting impls, and I am out of time for today, the week and uh the week after that. Vacation! If you feel like finishing this for me, please submit PRs to my branch and I can review and press the button for it while I'm off. Unless you're Cart, in which case you have write permissions to my branch! - [ ] fix for commands - [ ] fix for systems - [ ] fix for observers - [ ] revert bevyengine/bevy-website#2092 ## Testing I've added a compile test for these failure cases and a few adjacent non-failing cases (with explicit return types). --------- Co-authored-by: Carter Anderson <[email protected]>
1 parent d7ec6a9 commit 9254297

File tree

7 files changed

+145
-12
lines changed

7 files changed

+145
-12
lines changed

crates/bevy_ecs/src/error/command_handling.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use core::{any::type_name, fmt};
22

33
use crate::{
44
entity::Entity,
5+
never::Never,
56
system::{entity_command::EntityCommandError, Command, EntityCommand},
67
world::{error::EntityMutableFetchError, World},
78
};
@@ -42,6 +43,17 @@ where
4243
}
4344
}
4445

46+
impl<C> HandleError<Never> for C
47+
where
48+
C: Command<Never>,
49+
{
50+
fn handle_error_with(self, _error_handler: fn(BevyError, ErrorContext)) -> impl Command {
51+
move |world: &mut World| {
52+
self.apply(world);
53+
}
54+
}
55+
}
56+
4557
impl<C> HandleError for C
4658
where
4759
C: Command,

crates/bevy_ecs/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ pub mod identifier;
4343
pub mod intern;
4444
pub mod label;
4545
pub mod name;
46+
pub mod never;
4647
pub mod observer;
4748
pub mod query;
4849
#[cfg(feature = "bevy_reflect")]

crates/bevy_ecs/src/never.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
//! A workaround for the `!` type in stable Rust.
2+
//!
3+
//! This approach is taken from the [`never_say_never`] crate,
4+
//! reimplemented here to avoid adding a new dependency.
5+
//!
6+
//! This module exists due to a change in [never type fallback inference] in the Rust 2024 edition.
7+
//! This caused failures in `bevy_ecs`'s traits which are implemented for functions
8+
//! (like [`System`](crate::system::System)) when working with panicking closures.
9+
//!
10+
//! Note that using this hack is not recommended in general;
11+
//! by doing so you are knowingly opting out of rustc's stability guarantees.
12+
//! Code that compiles due to this hack may break in future versions of Rust.
13+
//!
14+
//! Please read [issue #18778](https://github.com/bevyengine/bevy/issues/18778) for an explanation of why
15+
//! Bevy has chosen to use this workaround.
16+
//!
17+
//! [`never_say_never`]: https://crates.io/crates/never_say_never
18+
//! [never type fallback inference]: https://doc.rust-lang.org/edition-guide/rust-2024/never-type-fallback.html
19+
20+
mod fn_ret {
21+
/// A helper trait for naming the ! type.
22+
#[doc(hidden)]
23+
pub trait FnRet {
24+
/// The return type of the function.
25+
type Output;
26+
}
27+
28+
/// This blanket implementation allows us to name the never type,
29+
/// by using the associated type of this trait for `fn() -> !`.
30+
impl<R> FnRet for fn() -> R {
31+
type Output = R;
32+
}
33+
}
34+
35+
/// A hacky type alias for the `!` (never) type.
36+
///
37+
/// This knowingly opts out of rustc's stability guarantees.
38+
/// Read the module documentation carefully before using this!
39+
pub type Never = <fn() -> ! as fn_ret::FnRet>::Output;

crates/bevy_ecs/src/schedule/config.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use variadics_please::all_tuples;
33

44
use crate::{
55
error::Result,
6+
never::Never,
67
schedule::{
78
auto_insert_apply_deferred::IgnoreDeferred,
89
condition::{BoxedCondition, Condition},
@@ -570,6 +571,16 @@ where
570571
}
571572
}
572573

574+
impl<F, Marker> IntoScheduleConfigs<ScheduleSystem, (Never, Marker)> for F
575+
where
576+
F: IntoSystem<(), Never, Marker>,
577+
{
578+
fn into_configs(self) -> ScheduleConfigs<ScheduleSystem> {
579+
let wrapper = InfallibleSystemWrapper::new(IntoSystem::into_system(self));
580+
ScheduleConfigs::ScheduleConfig(ScheduleSystem::into_config(Box::new(wrapper)))
581+
}
582+
}
583+
573584
/// Marker component to allow for conflicting implementations of [`IntoScheduleConfigs`]
574585
#[doc(hidden)]
575586
pub struct Fallible;

crates/bevy_ecs/src/system/mod.rs

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,8 @@ mod tests {
337337
component::{Component, Components},
338338
entity::{Entities, Entity},
339339
error::Result,
340-
prelude::{AnyOf, EntityRef},
340+
name::Name,
341+
prelude::{AnyOf, EntityRef, Trigger},
341342
query::{Added, Changed, Or, With, Without},
342343
removal_detection::RemovedComponents,
343344
resource::Resource,
@@ -349,7 +350,7 @@ mod tests {
349350
Commands, In, IntoSystem, Local, NonSend, NonSendMut, ParamSet, Query, Res, ResMut,
350351
Single, StaticSystemParam, System, SystemState,
351352
},
352-
world::{DeferredWorld, EntityMut, FromWorld, World},
353+
world::{DeferredWorld, EntityMut, FromWorld, OnAdd, World},
353354
};
354355

355356
use super::ScheduleSystem;
@@ -1823,4 +1824,59 @@ mod tests {
18231824
let mut world = World::new();
18241825
run_system(&mut world, sys);
18251826
}
1827+
1828+
// Regression test for
1829+
// https://github.com/bevyengine/bevy/issues/18778
1830+
//
1831+
// Dear rustc team, please reach out if you encounter this
1832+
// in a crater run and we can work something out!
1833+
//
1834+
// These todo! macro calls should never be removed;
1835+
// they're intended to demonstrate real-world usage
1836+
// in a way that's clearer than simply calling `panic!`
1837+
//
1838+
// Because type inference behaves differently for functions and closures,
1839+
// we need to test both, in addition to explicitly annotating the return type
1840+
// to ensure that there are no upstream regressions there.
1841+
#[test]
1842+
fn nondiverging_never_trait_impls() {
1843+
// This test is a compilation test:
1844+
// no meaningful logic is ever actually evaluated.
1845+
// It is simply intended to check that the correct traits are implemented
1846+
// when todo! or similar nondiverging panics are used.
1847+
let mut world = World::new();
1848+
let mut schedule = Schedule::default();
1849+
1850+
fn sys(_query: Query<&Name>) {
1851+
todo!()
1852+
}
1853+
1854+
schedule.add_systems(sys);
1855+
schedule.add_systems(|_query: Query<&Name>| {});
1856+
schedule.add_systems(|_query: Query<&Name>| todo!());
1857+
#[expect(clippy::unused_unit, reason = "this forces the () return type")]
1858+
schedule.add_systems(|_query: Query<&Name>| -> () { todo!() });
1859+
1860+
fn obs(_trigger: Trigger<OnAdd, Name>) {
1861+
todo!()
1862+
}
1863+
1864+
world.add_observer(obs);
1865+
world.add_observer(|_trigger: Trigger<OnAdd, Name>| {});
1866+
world.add_observer(|_trigger: Trigger<OnAdd, Name>| todo!());
1867+
#[expect(clippy::unused_unit, reason = "this forces the () return type")]
1868+
world.add_observer(|_trigger: Trigger<OnAdd, Name>| -> () { todo!() });
1869+
1870+
fn my_command(_world: &mut World) {
1871+
todo!()
1872+
}
1873+
1874+
world.commands().queue(my_command);
1875+
world.commands().queue(|_world: &mut World| {});
1876+
world.commands().queue(|_world: &mut World| todo!());
1877+
#[expect(clippy::unused_unit, reason = "this forces the () return type")]
1878+
world
1879+
.commands()
1880+
.queue(|_world: &mut World| -> () { todo!() });
1881+
}
18261882
}

crates/bevy_ecs/src/system/observer_system.rs

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use crate::{
55
archetype::ArchetypeComponentId,
66
component::{ComponentId, Tick},
77
error::Result,
8+
never::Never,
89
prelude::{Bundle, Trigger},
910
query::Access,
1011
schedule::{Fallible, Infallible},
@@ -45,7 +46,7 @@ pub trait IntoObserverSystem<E: 'static, B: Bundle, M, Out = Result>: Send + 'st
4546
fn into_system(this: Self) -> Self::System;
4647
}
4748

48-
impl<E, B, M, Out, S> IntoObserverSystem<E, B, (Fallible, M), Out> for S
49+
impl<E, B, M, S, Out> IntoObserverSystem<E, B, (Fallible, M), Out> for S
4950
where
5051
S: IntoSystem<Trigger<'static, E, B>, Out, M> + Send + 'static,
5152
S::System: ObserverSystem<E, B, Out>,
@@ -66,20 +67,32 @@ where
6667
E: Send + Sync + 'static,
6768
B: Bundle,
6869
{
69-
type System = InfallibleObserverWrapper<E, B, S::System>;
70+
type System = InfallibleObserverWrapper<E, B, S::System, ()>;
71+
72+
fn into_system(this: Self) -> Self::System {
73+
InfallibleObserverWrapper::new(IntoSystem::into_system(this))
74+
}
75+
}
76+
impl<E, B, M, S> IntoObserverSystem<E, B, (Never, M), Result> for S
77+
where
78+
S: IntoSystem<Trigger<'static, E, B>, Never, M> + Send + 'static,
79+
E: Send + Sync + 'static,
80+
B: Bundle,
81+
{
82+
type System = InfallibleObserverWrapper<E, B, S::System, Never>;
7083

7184
fn into_system(this: Self) -> Self::System {
7285
InfallibleObserverWrapper::new(IntoSystem::into_system(this))
7386
}
7487
}
7588

7689
/// A wrapper that converts an observer system that returns `()` into one that returns `Ok(())`.
77-
pub struct InfallibleObserverWrapper<E, B, S> {
90+
pub struct InfallibleObserverWrapper<E, B, S, Out> {
7891
observer: S,
79-
_marker: PhantomData<(E, B)>,
92+
_marker: PhantomData<(E, B, Out)>,
8093
}
8194

82-
impl<E, B, S> InfallibleObserverWrapper<E, B, S> {
95+
impl<E, B, S, Out> InfallibleObserverWrapper<E, B, S, Out> {
8396
/// Create a new `InfallibleObserverWrapper`.
8497
pub fn new(observer: S) -> Self {
8598
Self {
@@ -89,11 +102,12 @@ impl<E, B, S> InfallibleObserverWrapper<E, B, S> {
89102
}
90103
}
91104

92-
impl<E, B, S> System for InfallibleObserverWrapper<E, B, S>
105+
impl<E, B, S, Out> System for InfallibleObserverWrapper<E, B, S, Out>
93106
where
94-
S: ObserverSystem<E, B, ()>,
107+
S: ObserverSystem<E, B, Out>,
95108
E: Send + Sync + 'static,
96109
B: Bundle,
110+
Out: Send + Sync + 'static,
97111
{
98112
type In = Trigger<'static, E, B>;
99113
type Out = Result;

crates/bevy_ecs/src/system/schedule_system.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,16 @@ use crate::{
1212
use super::{IntoSystem, SystemParamValidationError};
1313

1414
/// A wrapper system to change a system that returns `()` to return `Ok(())` to make it into a [`ScheduleSystem`]
15-
pub struct InfallibleSystemWrapper<S: System<In = (), Out = ()>>(S);
15+
pub struct InfallibleSystemWrapper<S: System<In = ()>>(S);
1616

17-
impl<S: System<In = (), Out = ()>> InfallibleSystemWrapper<S> {
17+
impl<S: System<In = ()>> InfallibleSystemWrapper<S> {
1818
/// Create a new `OkWrapperSystem`
1919
pub fn new(system: S) -> Self {
2020
Self(IntoSystem::into_system(system))
2121
}
2222
}
2323

24-
impl<S: System<In = (), Out = ()>> System for InfallibleSystemWrapper<S> {
24+
impl<S: System<In = ()>> System for InfallibleSystemWrapper<S> {
2525
type In = ();
2626
type Out = Result;
2727

0 commit comments

Comments
 (0)