-
-
Notifications
You must be signed in to change notification settings - Fork 3.6k
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
Descriptive error message for circular required components recursion #16648
Changes from 4 commits
0b35eb7
f8edcca
e800e41
bc40d61
3e88789
29bf588
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -227,6 +227,7 @@ unsafe impl<C: Component> Bundle for C { | |
storages, | ||
required_components, | ||
0, | ||
&mut Vec::new(), | ||
); | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -28,6 +28,7 @@ use core::{ | |
mem::needs_drop, | ||
}; | ||
use derive_more::derive::{Display, Error}; | ||
use disqualified::ShortName; | ||
|
||
pub use bevy_ecs_macros::require; | ||
|
||
|
@@ -390,6 +391,7 @@ pub trait Component: Send + Sync + 'static { | |
_storages: &mut Storages, | ||
_required_components: &mut RequiredComponents, | ||
_inheritance_depth: u16, | ||
_recursion_check_stack: &mut Vec<ComponentId>, | ||
) { | ||
} | ||
|
||
|
@@ -401,6 +403,40 @@ pub trait Component: Send + Sync + 'static { | |
} | ||
} | ||
|
||
// NOTE: This should maybe be private, but it is currently public so that `bevy_ecs_macros` can use it. | ||
// This exists as a standalone function instead of being inlined into the component derive macro so as | ||
// to reduce the amount of generated code. | ||
#[doc(hidden)] | ||
pub fn enforce_no_required_components_recursion( | ||
components: &Components, | ||
recursion_check_stack: &[ComponentId], | ||
) { | ||
if let Some((&requiree, check)) = recursion_check_stack.split_last() { | ||
if let Some(direct_recursion) = check | ||
.iter() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Quadratic algorithms always make me nervous. Would it make sense to pass a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think we'll see deep enough chains of required components where the lookup in the vec has a measurable perf impact. The depth will be low, and comparisons with a list of integers that laid out successively in memory is really fast so the constant factor is very small. For a depth of 50 components depending on another in a chain – much deeper than what we should see – doing all of these checks for all of the components took a combined total of 200µs on my laptop. Still, while I don't think it's necessary, adding a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, I think you're right that the chains will be small, so an extra check wouldn't be worth the complexity. |
||
.position(|&id| id == requiree) | ||
.map(|index| index == check.len() - 1) | ||
{ | ||
panic!( | ||
"Recursive required components detected: {}\nhelp: {}", | ||
recursion_check_stack | ||
.iter() | ||
.map(|id| format!("{}", ShortName(components.get_name(*id).unwrap()))) | ||
.collect::<Vec<_>>() | ||
.join(" → "), | ||
if direct_recursion { | ||
format!( | ||
"Remove require({})", | ||
ShortName(components.get_name(requiree).unwrap()) | ||
) | ||
} else { | ||
"If this is intentional, consider merging the components.".into() | ||
} | ||
); | ||
} | ||
} | ||
} | ||
|
||
/// The storage used for a specific component type. | ||
/// | ||
/// # Examples | ||
|
@@ -987,7 +1023,16 @@ impl Components { | |
/// * [`Components::register_component_with_descriptor()`] | ||
#[inline] | ||
pub fn register_component<T: Component>(&mut self, storages: &mut Storages) -> ComponentId { | ||
let mut registered = false; | ||
self.register_component_internal::<T>(storages, &mut Vec::new()) | ||
} | ||
|
||
#[inline] | ||
fn register_component_internal<T: Component>( | ||
&mut self, | ||
storages: &mut Storages, | ||
recursion_check_stack: &mut Vec<ComponentId>, | ||
) -> ComponentId { | ||
let mut is_new_registration = false; | ||
let id = { | ||
let Components { | ||
indices, | ||
|
@@ -1001,13 +1046,20 @@ impl Components { | |
storages, | ||
ComponentDescriptor::new::<T>(), | ||
); | ||
registered = true; | ||
is_new_registration = true; | ||
id | ||
}) | ||
}; | ||
if registered { | ||
if is_new_registration { | ||
let mut required_components = RequiredComponents::default(); | ||
T::register_required_components(id, self, storages, &mut required_components, 0); | ||
T::register_required_components( | ||
id, | ||
self, | ||
storages, | ||
&mut required_components, | ||
0, | ||
recursion_check_stack, | ||
); | ||
let info = &mut self.components[id.index()]; | ||
T::register_component_hooks(&mut info.hooks); | ||
info.required_components = required_components; | ||
|
@@ -1270,6 +1322,9 @@ impl Components { | |
/// A direct requirement has a depth of `0`, and each level of inheritance increases the depth by `1`. | ||
/// Lower depths are more specific requirements, and can override existing less specific registrations. | ||
/// | ||
/// The `recursion_check_stack` allows checking whether this component tried to register itself as its | ||
/// own (indirect) required component. | ||
/// | ||
/// This method does *not* register any components as required by components that require `T`. | ||
/// | ||
/// Only use this method if you know what you are doing. In most cases, you should instead use [`World::register_required_components`], | ||
|
@@ -1283,9 +1338,10 @@ impl Components { | |
required_components: &mut RequiredComponents, | ||
constructor: fn() -> R, | ||
inheritance_depth: u16, | ||
recursion_check_stack: &mut Vec<ComponentId>, | ||
) { | ||
let requiree = self.register_component::<T>(storages); | ||
let required = self.register_component::<R>(storages); | ||
let requiree = self.register_component_internal::<T>(storages, recursion_check_stack); | ||
let required = self.register_component_internal::<R>(storages, recursion_check_stack); | ||
|
||
// SAFETY: We just created the components. | ||
unsafe { | ||
|
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 think doing this check before the
push
means you have one more level of recursion than is necessary.Like, if you register a component that requires itself, then the first time this is called the stack will be empty, and the second time it will have one element that it binds to
requiree
but an emptycheck
list. So it will pass on the second call, even though it had enough information to detect the error!The simplest thing to do might be to pass
self_id
toenforce_no_required_components_recursion
so that it doesn't need to dosplit_last()
.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.
Performance wise the extra recursion doesn't matter, as it's only in the panic case (unless I've misunderstood something).
Wouldn't make the code simpler either, as I would then have to append the
self_id
if the check fails so the full cycle is printed.