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

Compile Time Query checking and System Parameters #17837

Open
wants to merge 22 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 14 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
1 change: 1 addition & 0 deletions crates/bevy_core_pipeline/src/oit/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ impl Default for OrderIndependentTransparencySettings {
// we can hook on_add to issue a warning in case `layer_count` is seemingly too high.
impl Component for OrderIndependentTransparencySettings {
const STORAGE_TYPE: StorageType = StorageType::SparseSet;
const UNSTABLE_TYPE_ID: u128 = 28539417283523;
type Mutability = Mutable;

fn on_add() -> Option<ComponentHook> {
Expand Down
8 changes: 7 additions & 1 deletion crates/bevy_ecs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ categories = ["game-engines", "data-structures"]
rust-version = "1.83.0"

[features]
default = ["std", "bevy_reflect", "async_executor"]
default = ["std", "bevy_reflect", "async_executor", "diagnostic_component_names"]

# Functionality

Expand Down Expand Up @@ -53,6 +53,11 @@ bevy_debug_stepping = []
## This will often provide more detailed error messages.
track_location = []

## Provides name information in Component traits for use in System conflict diagnostics
diagnostic_component_names = [
"bevy_ecs_macros/diagnostic_component_names"
]

# Executor Backend

## Uses `async-executor` as a task execution backend.
Expand Down Expand Up @@ -138,6 +143,7 @@ variadics_please = { version = "1.1", default-features = false }
tracing = { version = "0.1", default-features = false, optional = true }
log = { version = "0.4", default-features = false }
bumpalo = "3"
const_panic = { version = "0.2.12" , features = ["rust_latest_stable"] }

[dev-dependencies]
rand = "0.8"
Expand Down
4 changes: 4 additions & 0 deletions crates/bevy_ecs/macros/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,16 @@ license = "MIT OR Apache-2.0"
[lib]
proc-macro = true

[features]
diagnostic_component_names = []

[dependencies]
bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.16.0-dev" }

syn = { version = "2.0", features = ["full"] }
quote = "1.0"
proc-macro2 = "1.0"
rand = "0.9.0"
[lints]
workspace = true

Expand Down
114 changes: 82 additions & 32 deletions crates/bevy_ecs/macros/src/component.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use proc_macro::{TokenStream, TokenTree};
use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::{format_ident, quote, ToTokens};
use rand::Rng;
use std::collections::HashSet;
use syn::{
parenthesized,
Expand Down Expand Up @@ -214,45 +215,94 @@ pub fn derive_component(input: TokenStream) -> TokenStream {
(&&&#bevy_ecs_path::component::DefaultCloneBehaviorSpecialization::<Self>::default()).default_clone_behavior()
)
};
let mut rng = rand::rng();
let unstable_type_id: u128 = rng.random();
#[cfg(feature = "diagnostic_component_names")]
{
let struct_name_2 = struct_name.to_string();
// This puts `register_required` before `register_recursive_requires` to ensure that the constructors of _all_ top
// level components are initialized first, giving them precedence over recursively defined constructors for the same component type
TokenStream::from(quote! {
impl #impl_generics #bevy_ecs_path::component::Component for #struct_name #type_generics #where_clause {
const STORAGE_TYPE: #bevy_ecs_path::component::StorageType = #storage;
const UNSTABLE_TYPE_ID: u128 = #unstable_type_id;
const STRUCT_NAME: Option<&'static str> = Some(#struct_name_2);
type Mutability = #mutable_type;
fn register_required_components(
requiree: #bevy_ecs_path::component::ComponentId,
components: &mut #bevy_ecs_path::component::Components,
required_components: &mut #bevy_ecs_path::component::RequiredComponents,
inheritance_depth: u16,
recursion_check_stack: &mut #bevy_ecs_path::__macro_exports::Vec<#bevy_ecs_path::component::ComponentId>
) {
#bevy_ecs_path::component::enforce_no_required_components_recursion(components, recursion_check_stack);
let self_id = components.register_component::<Self>();
recursion_check_stack.push(self_id);
#(#register_required)*
#(#register_recursive_requires)*
recursion_check_stack.pop();
}

// This puts `register_required` before `register_recursive_requires` to ensure that the constructors of _all_ top
// level components are initialized first, giving them precedence over recursively defined constructors for the same component type
TokenStream::from(quote! {
impl #impl_generics #bevy_ecs_path::component::Component for #struct_name #type_generics #where_clause {
const STORAGE_TYPE: #bevy_ecs_path::component::StorageType = #storage;
type Mutability = #mutable_type;
fn register_required_components(
requiree: #bevy_ecs_path::component::ComponentId,
components: &mut #bevy_ecs_path::component::Components,
required_components: &mut #bevy_ecs_path::component::RequiredComponents,
inheritance_depth: u16,
recursion_check_stack: &mut #bevy_ecs_path::__macro_exports::Vec<#bevy_ecs_path::component::ComponentId>
) {
#bevy_ecs_path::component::enforce_no_required_components_recursion(components, recursion_check_stack);
let self_id = components.register_component::<Self>();
recursion_check_stack.push(self_id);
#(#register_required)*
#(#register_recursive_requires)*
recursion_check_stack.pop();
}
#on_add
#on_insert
#on_replace
#on_remove
#on_despawn

#on_add
#on_insert
#on_replace
#on_remove
#on_despawn
fn clone_behavior() -> #bevy_ecs_path::component::ComponentCloneBehavior {
#clone_behavior
}

fn clone_behavior() -> #bevy_ecs_path::component::ComponentCloneBehavior {
#clone_behavior
#visit_entities
}

#visit_entities
}
#relationship

#relationship
#relationship_target
})
}
#[cfg(not(feature = "diagnostic_component_names"))]
{
// This puts `register_required` before `register_recursive_requires` to ensure that the constructors of _all_ top
// level components are initialized first, giving them precedence over recursively defined constructors for the same component type
TokenStream::from(quote! {
impl #impl_generics #bevy_ecs_path::component::Component for #struct_name #type_generics #where_clause {
const STORAGE_TYPE: #bevy_ecs_path::component::StorageType = #storage;
const UNSTABLE_TYPE_ID: u128 = #unstable_type_id;
type Mutability = #mutable_type;
fn register_required_components(
requiree: #bevy_ecs_path::component::ComponentId,
components: &mut #bevy_ecs_path::component::Components,
required_components: &mut #bevy_ecs_path::component::RequiredComponents,
inheritance_depth: u16,
recursion_check_stack: &mut #bevy_ecs_path::__macro_exports::Vec<#bevy_ecs_path::component::ComponentId>
) {
#bevy_ecs_path::component::enforce_no_required_components_recursion(components, recursion_check_stack);
let self_id = components.register_component::<Self>();
recursion_check_stack.push(self_id);
#(#register_required)*
#(#register_recursive_requires)*
recursion_check_stack.pop();
}

#relationship_target
})
#on_add
#on_insert
#on_replace
#on_remove
#on_despawn

fn clone_behavior() -> #bevy_ecs_path::component::ComponentCloneBehavior {
#clone_behavior
}

#visit_entities
}

#relationship

#relationship_target
})
}
}

fn visit_entities(data: &Data, bevy_ecs_path: &Path, is_relationship: bool) -> TokenStream2 {
Expand Down
12 changes: 12 additions & 0 deletions crates/bevy_ecs/src/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,18 @@ pub trait Component: Send + Sync + 'static {
/// A constant indicating the storage type used for this component.
const STORAGE_TYPE: StorageType;

/// A randomly generated u128 that is consistent per component type, used to uniquely identify
/// components in const contexts.
/// This enables compile-time validation of component access patterns
/// for both inter-query and intra-query correctness.
const UNSTABLE_TYPE_ID: u128;

/// The name of the component's struct, if available.
/// This is used to provide more helpful error messages when component access conflicts
/// are detected at compile-time. Will be None if the component isn't derived automatically
/// or if the name isn't available.
#[cfg(feature = "diagnostic_component_names")]
const STRUCT_NAME: Option<&'static str> = None;
/// A marker type to assist Bevy with determining if this component is
/// mutable, or immutable. Mutable components will have [`Component<Mutability = Mutable>`],
/// while immutable components will instead have [`Component<Mutability = Immutable>`].
Expand Down
1 change: 1 addition & 0 deletions crates/bevy_ecs/src/observer/entity_observer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ pub struct ObservedBy(pub(crate) Vec<Entity>);

impl Component for ObservedBy {
const STORAGE_TYPE: StorageType = StorageType::SparseSet;
const UNSTABLE_TYPE_ID: u128 = 1;
type Mutability = Mutable;

fn on_remove() -> Option<ComponentHook> {
Expand Down
2 changes: 2 additions & 0 deletions crates/bevy_ecs/src/observer/runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ impl ObserverState {

impl Component for ObserverState {
const STORAGE_TYPE: StorageType = StorageType::SparseSet;
const UNSTABLE_TYPE_ID: u128 = 2;
type Mutability = Mutable;

fn on_add() -> Option<ComponentHook> {
Expand Down Expand Up @@ -335,6 +336,7 @@ impl Observer {

impl Component for Observer {
const STORAGE_TYPE: StorageType = StorageType::SparseSet;
const UNSTABLE_TYPE_ID: u128 = 3;
type Mutability = Mutable;
fn on_add() -> Option<ComponentHook> {
Some(|world, context| {
Expand Down
70 changes: 67 additions & 3 deletions crates/bevy_ecs/src/query/fetch.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
use crate::system::const_param_checking::{
AccessTreeContainer, ComponentAccess, ComponentAccessTree,
};
use crate::{
archetype::{Archetype, Archetypes},
bundle::Bundle,
Expand Down Expand Up @@ -284,6 +287,19 @@
/// and is visible to the end user when calling e.g. `Query<Self>::get`.
type Item<'a>;

/// A compile-time representation of how this query accesses components.
/// Used to validate query compatibility and detect conflicting access patterns
/// during const evaluation.
/// By default it implements an Ignore, and can be overrided by implementations such as

Check warning on line 293 in crates/bevy_ecs/src/query/fetch.rs

View workflow job for this annotation

GitHub Actions / typos

"overrided" should be "overridden" or "overrode".
/// impl<T: Component> or impl<T0: QueryData, T1: QueryData>
/// There we recursively build up this component access tree, checking for intra-query
/// errors along the way.
const COMPONENT_ACCESS_TREE_QUERY_DATA: ComponentAccessTree = ComponentAccessTree {
this: ComponentAccess::Ignore,
left: None,
right: None,
};

/// This function manually implements subtyping for the query items.
fn shrink<'wlong: 'wshort, 'wshort>(item: Self::Item<'wlong>) -> Self::Item<'wshort>;

Expand Down Expand Up @@ -1203,10 +1219,15 @@
}

/// SAFETY: `Self` is the same as `Self::ReadOnly`
unsafe impl<T: Component> QueryData for &T {
unsafe impl<T: Component> QueryData for &T
where
for<'a> &'a T: AccessTreeContainer,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do these bounds need to be higher-ranked, and not just Self: AccessTreeContainer? It looks like they are only used for Self::COMPONENT_ACCESS_TREE.

For that matter, do you need AccessTreeContainer at all? Given that UNSTABLE_TYPE_ID and STRUCT_NAME are already on Component, it looks like all it does is share a bit of code between &T and Ref<T> and between &mut T and Mut<T>. It might be simpler to remove the trait and do

const COMPONENT_ACCESS_TREE_QUERY_DATA: ComponentAccessTree = <&T as QueryData>::COMPONENT_ACCESS_TREE;

to share the code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this works? We have an implementation ON &mut T where T is component because we wanna be able to tell if a component access is a ref or a mut.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doing where Self: AccessTreeContainer seems to compile when I try it.

And copy-pasting the definition from impl<T: Component> AccessTreeContainer for &T to unsafe impl<T: Component> QueryData for &T works other than privacy, which should let you get rid of AccessTreeContainer completely. If you want to leave them private then I expect you could make a const fn new, although I didn't try that.

... And I don't see a definition of COMPONENT_ACCESS_TREE at all in QueryData for Ref or Mut? Well, if I add

    const COMPONENT_ACCESS_TREE_QUERY_DATA: ConstTree<ComponentAccess> =
        <&T as QueryData>::COMPONENT_ACCESS_TREE_QUERY_DATA;

to unsafe impl<'__w, T: Component> QueryData for Ref<'__w, T> { then it seems to work, so you could still share the definitions between &T and Ref<T>.

{
type ReadOnly = Self;
type Item<'w> = &'w T;

const COMPONENT_ACCESS_TREE_QUERY_DATA: ComponentAccessTree = Self::COMPONENT_ACCESS_TREE;

fn shrink<'wlong: 'wshort, 'wshort>(item: &'wlong T) -> &'wshort T {
item
}
Expand Down Expand Up @@ -1568,10 +1589,15 @@
}

/// SAFETY: access of `&T` is a subset of `&mut T`
unsafe impl<'__w, T: Component<Mutability = Mutable>> QueryData for &'__w mut T {
unsafe impl<'__w, T: Component<Mutability = Mutable>> QueryData for &'__w mut T
where
for<'a> &'a mut T: AccessTreeContainer,
{
type ReadOnly = &'__w T;
type Item<'w> = Mut<'w, T>;

const COMPONENT_ACCESS_TREE_QUERY_DATA: ComponentAccessTree = Self::COMPONENT_ACCESS_TREE;

fn shrink<'wlong: 'wshort, 'wshort>(item: Mut<'wlong, T>) -> Mut<'wshort, T> {
item
}
Expand Down Expand Up @@ -1710,7 +1736,10 @@
}

// SAFETY: access of `Ref<T>` is a subset of `Mut<T>`
unsafe impl<'__w, T: Component<Mutability = Mutable>> QueryData for Mut<'__w, T> {
unsafe impl<'__w, T: Component<Mutability = Mutable>> QueryData for Mut<'__w, T>
where
for<'a> &'a mut T: AccessTreeContainer,
{
type ReadOnly = Ref<'__w, T>;
type Item<'w> = Mut<'w, T>;

Expand Down Expand Up @@ -1841,6 +1870,9 @@
type ReadOnly = Option<T::ReadOnly>;
type Item<'w> = Option<T::Item<'w>>;

const COMPONENT_ACCESS_TREE_QUERY_DATA: ComponentAccessTree =
T::COMPONENT_ACCESS_TREE_QUERY_DATA;

fn shrink<'wlong: 'wshort, 'wshort>(item: Self::Item<'wlong>) -> Self::Item<'wshort> {
item.map(T::shrink)
}
Expand Down Expand Up @@ -2052,6 +2084,8 @@
type ReadOnly = ($($name::ReadOnly,)*);
type Item<'w> = ($($name::Item<'w>,)*);

const COMPONENT_ACCESS_TREE_QUERY_DATA: ComponentAccessTree = impl_tuple_query_data!(@tree $($name),*);

fn shrink<'wlong: 'wshort, 'wshort>(item: Self::Item<'wlong>) -> Self::Item<'wshort> {
let ($($name,)*) = item;
($(
Expand All @@ -2076,6 +2110,36 @@
unsafe impl<$($name: ReadOnlyQueryData),*> ReadOnlyQueryData for ($($name,)*) {}

};

// Handle empty case
(@tree) => {
ComponentAccessTree {
this: ComponentAccess::Ignore,
left: None,
right: None,
}
};
// Handle single item case
(@tree $t0:ident) => {
$t0::COMPONENT_ACCESS_TREE_QUERY_DATA
};
// Handle two item case
(@tree $t0:ident, $t1:ident) => {
ComponentAccessTree::combine(
&$t0::COMPONENT_ACCESS_TREE_QUERY_DATA,
&$t1::COMPONENT_ACCESS_TREE_QUERY_DATA,
)
};
// Handle three or more items case
(@tree $t0:ident, $t1:ident, $($rest:ident),+) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you need separate cases for two and three items? It seems like you could make the two-item case take (@tree $t0:ident, $($rest:ident),+) and do the induction there.

Alternately, I think you can do this without recursion. It might be simpler as something like

const COMPONENT_ACCESS_TREE_QUERY_DATA: ComponentAccessTree = const {
    let tree = ComponentAccessTree { this: ComponentAccess::Ignore, left: None, right: None };
    $(
        let tree = ComponentAccessTree::combine(tree, $name);
    )*
    tree
};

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

 let tree = &ConstTreeInner::<ComponentAccess>::combine(tree, $name::COMPONENT_ACCESS_TREE_QUERY_DATA);
     |                                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
     |                                   |
     |                                   creates a temporary value which is freed while still in use
     |                                   argument requires that borrow lasts for `'static`

Sadly it seems like this doesn't work. But if you could elaborate on how to do the two item case instead I would like to switch to that!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I see. I was expecting it to do static promotion... but that's only possible if the inner values are themselves const, and tree isn't const. And if you try to make a bunch of nested const TREE = then it complains that they "can't use generic parameters from outer item". Yuck.


But if you could elaborate on how to do the two item case instead I would like to switch to that!

The simplest change is

    (@tree) => {
        &ConstTreeInner::Empty
    };
    (@tree $t0:ident) => {
        $t0::COMPONENT_ACCESS_TREE_QUERY_DATA
    };
    (@tree $t0:ident, $($rest:ident),+) => {
        &ConstTreeInner::<ComponentAccess>::combine(
            $t0::COMPONENT_ACCESS_TREE_QUERY_DATA,
                impl_tuple_query_data!(@tree $($rest),+)
        )
    };

But you could go one further with

    // Base case
    (@tree) => {
        &ConstTreeInner::Empty
    };
    // Inductive case
    (@tree $t0:ident, $($rest:ident),*) => {
        &ConstTreeInner::<ComponentAccess>::combine(
            $t0::COMPONENT_ACCESS_TREE_QUERY_DATA,
            impl_tuple_query_data!(@tree $($rest),*)
        )
    };

for the cost of an extra combine(Empty).

... and for that matter, since we're already generating that same code for smaller tuples, you could use their trees rather than doing a second recursion:

    // Base case
    (@tree) => {
        &ConstTreeInner::Empty
    };
    // Inductive case
    (@tree $t0:ident, $($rest:ident),*) => {
        &ConstTreeInner::<ComponentAccess>::combine(
            $t0::COMPONENT_ACCESS_TREE_QUERY_DATA,
            <($($rest,)*)>::COMPONENT_ACCESS_TREE_QUERY_DATA
        )
    };

That should generate a little less code.

ComponentAccessTree::combine(
&$t0::COMPONENT_ACCESS_TREE_QUERY_DATA,
&ComponentAccessTree::combine(
&$t1::COMPONENT_ACCESS_TREE_QUERY_DATA,
&impl_tuple_query_data!(@tree $($rest),+)
)
)
};
}

macro_rules! impl_anytuple_fetch {
Expand Down
Loading
Loading