Skip to content
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

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions crates/bevy_ecs/macros/src/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@ pub fn derive_component(input: TokenStream) -> TokenStream {
components,
storages,
required_components,
inheritance_depth + 1
inheritance_depth + 1,
recursion_check_stack
);
});
match &require.func {
Expand All @@ -98,7 +99,8 @@ pub fn derive_component(input: TokenStream) -> TokenStream {
storages,
required_components,
|| { let x: #ident = #func().into(); x },
inheritance_depth
inheritance_depth,
recursion_check_stack
);
});
}
Expand All @@ -108,7 +110,8 @@ pub fn derive_component(input: TokenStream) -> TokenStream {
storages,
required_components,
|| { let x: #ident = (#func)().into(); x },
inheritance_depth
inheritance_depth,
recursion_check_stack
);
});
}
Expand All @@ -118,7 +121,8 @@ pub fn derive_component(input: TokenStream) -> TokenStream {
storages,
required_components,
<#ident as Default>::default,
inheritance_depth
inheritance_depth,
recursion_check_stack
);
});
}
Expand All @@ -145,9 +149,14 @@ pub fn derive_component(input: TokenStream) -> TokenStream {
storages: &mut #bevy_ecs_path::storage::Storages,
required_components: &mut #bevy_ecs_path::component::RequiredComponents,
inheritance_depth: u16,
recursion_check_stack: &mut Vec<#bevy_ecs_path::component::ComponentId>
) {
#bevy_ecs_path::component::enforce_no_required_components_recursion(components, recursion_check_stack);
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 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 empty check 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 to enforce_no_required_components_recursion so that it doesn't need to do split_last().

Copy link
Contributor Author

@SpecificProtagonist SpecificProtagonist Dec 7, 2024

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.

let self_id = components.register_component::<Self>(storages);
recursion_check_stack.push(self_id);
#(#register_required)*
#(#register_recursive_requires)*
recursion_check_stack.pop();
}

#[allow(unused_variables)]
Expand Down
1 change: 1 addition & 0 deletions crates/bevy_ecs/src/bundle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ unsafe impl<C: Component> Bundle for C {
storages,
required_components,
0,
&mut Vec::new(),
);
}

Expand Down
68 changes: 62 additions & 6 deletions crates/bevy_ecs/src/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ use core::{
marker::PhantomData,
mem::needs_drop,
};
use disqualified::ShortName;
use thiserror::Error;

pub use bevy_ecs_macros::require;
Expand Down Expand Up @@ -404,6 +405,7 @@ pub trait Component: Send + Sync + 'static {
_storages: &mut Storages,
_required_components: &mut RequiredComponents,
_inheritance_depth: u16,
_recursion_check_stack: &mut Vec<ComponentId>,
) {
}

Expand Down Expand Up @@ -1075,7 +1077,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,
Expand All @@ -1089,13 +1100,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;
Expand Down Expand Up @@ -1358,6 +1376,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`],
Expand All @@ -1371,9 +1392,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 {
Expand Down Expand Up @@ -2044,6 +2066,40 @@ impl RequiredComponents {
}
}

// 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()
.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()
}
);
}
}
}

/// Component [clone handler function](ComponentCloneFn) implemented using the [`Clone`] trait.
/// Can be [set](ComponentCloneHandlers::set_component_handler) as clone handler for the specific component it is implemented for.
/// It will panic if set as handler for any other component.
Expand Down
22 changes: 20 additions & 2 deletions crates/bevy_ecs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2061,7 +2061,7 @@ mod tests {
}

#[test]
fn remove_component_and_his_runtime_required_components() {
fn remove_component_and_its_runtime_required_components() {
#[derive(Component)]
struct X;

Expand Down Expand Up @@ -2106,7 +2106,7 @@ mod tests {
}

#[test]
fn remove_component_and_his_required_components() {
fn remove_component_and_its_required_components() {
#[derive(Component)]
#[require(Y)]
struct X;
Expand Down Expand Up @@ -2557,6 +2557,24 @@ mod tests {
assert_eq!(to_vec(required_z), vec![(b, 0), (c, 1)]);
}

#[test]
#[should_panic = "Recursive required components detected: A → B → C → B\nhelp: If this is intentional, consider merging the components."]
fn required_components_recursion_errors() {
#[derive(Component, Default)]
#[require(B)]
struct A;

#[derive(Component, Default)]
#[require(C)]
struct B;

#[derive(Component, Default)]
#[require(B)]
struct C;

World::new().register_component::<A>();
}

// These structs are primarily compilation tests to test the derive macros. Because they are
// never constructed, we have to manually silence the `dead_code` lint.
#[allow(dead_code)]
Expand Down
Loading