Improved Require Syntax (#18555)

# Objective

Requires are currently more verbose than they need to be. People would
like to define inline component values. Additionally, the current
`#[require(Foo(custom_constructor))]` and `#[require(Foo(|| Foo(10))]`
syntax doesn't really make sense within the context of the Rust type
system. #18309 was an attempt to improve ergonomics for some cases, but
it came at the cost of even more weirdness / unintuitive behavior. Our
approach as a whole needs a rethink.

## Solution

Rework the `#[require()]` syntax to make more sense. This is a breaking
change, but I think it will make the system easier to learn, while also
improving ergonomics substantially:

```rust
#[derive(Component)]
#[require(
    A, // this will use A::default()
    B(1), // inline tuple-struct value
    C { value: 1 }, // inline named-struct value
    D::Variant, // inline enum variant
    E::SOME_CONST, // inline associated const
    F::new(1), // inline constructor
    G = returns_g(), // an expression that returns G
    H = SomethingElse::new(), // expression returns SomethingElse, where SomethingElse: Into<H> 
)]
struct Foo;
```

## Migration Guide

Custom-constructor requires should use the new expression-style syntax:

```rust
// before
#[derive(Component)]
#[require(A(returns_a))]
struct Foo;

// after
#[derive(Component)]
#[require(A = returns_a())]
struct Foo;
```

Inline-closure-constructor requires should use the inline value syntax
where possible:

```rust
// before
#[derive(Component)]
#[require(A(|| A(10))]
struct Foo;

// after
#[derive(Component)]
#[require(A(10)]
struct Foo;
```

In cases where that is not possible, use the expression-style syntax:

```rust
// before
#[derive(Component)]
#[require(A(|| A(10))]
struct Foo;

// after
#[derive(Component)]
#[require(A = A(10)]
struct Foo;
```

---------

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
Co-authored-by: François Mockers <mockersf@gmail.com>
This commit is contained in:
Carter Anderson 2025-03-26 10:48:27 -07:00 committed by GitHub
parent 10f1fbf589
commit 538afe2330
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 137 additions and 81 deletions

View File

@ -18,9 +18,9 @@ use bevy_transform::prelude::{GlobalTransform, Transform};
#[require( #[require(
Camera, Camera,
DebandDither, DebandDither,
CameraRenderGraph(|| CameraRenderGraph::new(Core2d)), CameraRenderGraph::new(Core2d),
Projection(|| Projection::Orthographic(OrthographicProjection::default_2d())), Projection::Orthographic(OrthographicProjection::default_2d()),
Frustum(|| OrthographicProjection::default_2d().compute_frustum(&GlobalTransform::from(Transform::default()))), Frustum = OrthographicProjection::default_2d().compute_frustum(&GlobalTransform::from(Transform::default())),
Tonemapping(|| Tonemapping::None), Tonemapping::None,
)] )]
pub struct Camera2d; pub struct Camera2d;

View File

@ -21,8 +21,8 @@ use serde::{Deserialize, Serialize};
#[reflect(Component, Default, Clone)] #[reflect(Component, Default, Clone)]
#[require( #[require(
Camera, Camera,
DebandDither(|| DebandDither::Enabled), DebandDither::Enabled,
CameraRenderGraph(|| CameraRenderGraph::new(Core3d)), CameraRenderGraph::new(Core3d),
Projection, Projection,
Tonemapping, Tonemapping,
ColorGrading, ColorGrading,

View File

@ -3,13 +3,13 @@ use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::{format_ident, quote, ToTokens}; use quote::{format_ident, quote, ToTokens};
use std::collections::HashSet; use std::collections::HashSet;
use syn::{ use syn::{
parenthesized, braced, parenthesized,
parse::Parse, parse::Parse,
parse_macro_input, parse_quote, parse_macro_input, parse_quote,
punctuated::Punctuated, punctuated::Punctuated,
spanned::Spanned, spanned::Spanned,
token::{Comma, Paren}, token::{Brace, Comma, Paren},
Data, DataEnum, DataStruct, DeriveInput, Expr, ExprCall, ExprClosure, ExprPath, Field, Fields, Data, DataEnum, DataStruct, DeriveInput, Expr, ExprCall, ExprPath, Field, FieldValue, Fields,
Ident, LitStr, Member, Path, Result, Token, Type, Visibility, Ident, LitStr, Member, Path, Result, Token, Type, Visibility,
}; };
@ -207,17 +207,7 @@ pub fn derive_component(input: TokenStream) -> TokenStream {
); );
}); });
match &require.func { match &require.func {
Some(RequireFunc::Path(func)) => { Some(func) => {
register_required.push(quote! {
components.register_required_components_manual::<Self, #ident>(
required_components,
|| { let x: #ident = #func().into(); x },
inheritance_depth,
recursion_check_stack
);
});
}
Some(RequireFunc::Closure(func)) => {
register_required.push(quote! { register_required.push(quote! {
components.register_required_components_manual::<Self, #ident>( components.register_required_components_manual::<Self, #ident>(
required_components, required_components,
@ -478,12 +468,7 @@ enum StorageTy {
struct Require { struct Require {
path: Path, path: Path,
func: Option<RequireFunc>, func: Option<TokenStream2>,
}
enum RequireFunc {
Path(Path),
Closure(ExprClosure),
} }
struct Relationship { struct Relationship {
@ -580,25 +565,71 @@ fn parse_component_attr(ast: &DeriveInput) -> Result<Attrs> {
impl Parse for Require { impl Parse for Require {
fn parse(input: syn::parse::ParseStream) -> Result<Self> { fn parse(input: syn::parse::ParseStream) -> Result<Self> {
let path = input.parse::<Path>()?; let mut path = input.parse::<Path>()?;
let func = if input.peek(Paren) { let mut last_segment_is_lower = false;
let mut is_constructor_call = false;
// Use the case of the type name to check if it's an enum
// This doesn't match everything that can be an enum according to the rust spec
// but it matches what clippy is OK with
let is_enum = {
let mut first_chars = path
.segments
.iter()
.rev()
.filter_map(|s| s.ident.to_string().chars().next());
if let Some(last) = first_chars.next() {
if last.is_uppercase() {
if let Some(last) = first_chars.next() {
last.is_uppercase()
} else {
false
}
} else {
last_segment_is_lower = true;
false
}
} else {
false
}
};
let func = if input.peek(Token![=]) {
// If there is an '=', then this is a "function style" require
let _t: syn::Token![=] = input.parse()?;
let expr: Expr = input.parse()?;
let tokens: TokenStream = quote::quote! (|| #expr).into();
Some(TokenStream2::from(tokens))
} else if input.peek(Brace) {
// This is a "value style" named-struct-like require
let content;
braced!(content in input);
let fields = Punctuated::<FieldValue, Token![,]>::parse_terminated(&content)?;
let tokens: TokenStream = quote::quote! (|| #path { #fields }).into();
Some(TokenStream2::from(tokens))
} else if input.peek(Paren) {
// This is a "value style" tuple-struct-like require
let content; let content;
parenthesized!(content in input); parenthesized!(content in input);
if let Ok(func) = content.parse::<ExprClosure>() { is_constructor_call = last_segment_is_lower;
Some(RequireFunc::Closure(func)) let fields = Punctuated::<Expr, Token![,]>::parse_terminated(&content)?;
} else { let tokens: TokenStream = quote::quote! (|| #path (#fields)).into();
let func = content.parse::<Path>()?; Some(TokenStream2::from(tokens))
Some(RequireFunc::Path(func)) } else if is_enum {
} // if this is an enum, then it is an inline enum component declaration
} else if input.peek(Token![=]) { let tokens: TokenStream = quote::quote! (|| #path).into();
let _t: syn::Token![=] = input.parse()?; Some(TokenStream2::from(tokens))
let label: Ident = input.parse()?;
let tokens: TokenStream = quote::quote! (|| #path::#label).into();
let func = syn::parse(tokens).unwrap();
Some(RequireFunc::Closure(func))
} else { } else {
// if this isn't any of the above, then it is a component ident, which will use Default
None None
}; };
if is_enum || is_constructor_call {
let path_len = path.segments.len();
path = Path {
leading_colon: path.leading_colon,
segments: Punctuated::from_iter(path.segments.into_iter().take(path_len - 1)),
};
}
Ok(Require { path, func }) Ok(Require { path, func })
} }
} }

View File

@ -160,16 +160,69 @@ use thiserror::Error;
/// assert_eq!(&C(0), world.entity(id).get::<C>().unwrap()); /// assert_eq!(&C(0), world.entity(id).get::<C>().unwrap());
/// ``` /// ```
/// ///
/// You can also define a custom constructor function or closure: /// You can define inline component values that take the following forms:
/// ```
/// # use bevy_ecs::prelude::*;
/// #[derive(Component)]
/// #[require(
/// B(1), // tuple structs
/// C { value: 1 }, // named-field structs
/// D::One, // enum variants
/// E::ONE, // associated consts
/// F::new(1) // constructors
/// )]
/// struct A;
///
/// #[derive(Component, PartialEq, Eq, Debug)]
/// struct B(u8);
///
/// #[derive(Component, PartialEq, Eq, Debug)]
/// struct C {
/// value: u8
/// }
///
/// #[derive(Component, PartialEq, Eq, Debug)]
/// enum D {
/// Zero,
/// One,
/// }
///
/// #[derive(Component, PartialEq, Eq, Debug)]
/// struct E(u8);
///
/// impl E {
/// pub const ONE: Self = Self(1);
/// }
///
/// #[derive(Component, PartialEq, Eq, Debug)]
/// struct F(u8);
///
/// impl F {
/// fn new(value: u8) -> Self {
/// Self(value)
/// }
/// }
///
/// # let mut world = World::default();
/// let id = world.spawn(A).id();
/// assert_eq!(&B(1), world.entity(id).get::<B>().unwrap());
/// assert_eq!(&C { value: 1 }, world.entity(id).get::<C>().unwrap());
/// assert_eq!(&D::One, world.entity(id).get::<D>().unwrap());
/// assert_eq!(&E(1), world.entity(id).get::<E>().unwrap());
/// assert_eq!(&F(1), world.entity(id).get::<F>().unwrap());
/// ````
///
///
/// You can also define arbitrary expressions by using `=`
/// ///
/// ``` /// ```
/// # use bevy_ecs::prelude::*; /// # use bevy_ecs::prelude::*;
/// #[derive(Component)] /// #[derive(Component)]
/// #[require(C(init_c))] /// #[require(C = init_c())]
/// struct A; /// struct A;
/// ///
/// #[derive(Component, PartialEq, Eq, Debug)] /// #[derive(Component, PartialEq, Eq, Debug)]
/// #[require(C(|| C(20)))] /// #[require(C = C(20))]
/// struct B; /// struct B;
/// ///
/// #[derive(Component, PartialEq, Eq, Debug)] /// #[derive(Component, PartialEq, Eq, Debug)]
@ -189,34 +242,6 @@ use thiserror::Error;
/// assert_eq!(&C(20), world.entity(id).get::<C>().unwrap()); /// assert_eq!(&C(20), world.entity(id).get::<C>().unwrap());
/// ``` /// ```
/// ///
/// For convenience sake, you can abbreviate enum labels or constant values, with the type inferred to match that of the component you are requiring:
///
/// ```
/// # use bevy_ecs::prelude::*;
/// #[derive(Component)]
/// #[require(B = One, C = ONE)]
/// struct A;
///
/// #[derive(Component, PartialEq, Eq, Debug)]
/// enum B {
/// Zero,
/// One,
/// Two
/// }
///
/// #[derive(Component, PartialEq, Eq, Debug)]
/// struct C(u8);
///
/// impl C {
/// pub const ONE: Self = Self(1);
/// }
///
/// # let mut world = World::default();
/// let id = world.spawn(A).id();
/// assert_eq!(&B::One, world.entity(id).get::<B>().unwrap());
/// assert_eq!(&C(1), world.entity(id).get::<C>().unwrap());
/// ````
///
/// Required components are _recursive_. This means, if a Required Component has required components, /// Required components are _recursive_. This means, if a Required Component has required components,
/// those components will _also_ be inserted if they are missing: /// those components will _also_ be inserted if they are missing:
/// ///
@ -252,13 +277,13 @@ use thiserror::Error;
/// struct X(usize); /// struct X(usize);
/// ///
/// #[derive(Component, Default)] /// #[derive(Component, Default)]
/// #[require(X(|| X(1)))] /// #[require(X(1))]
/// struct Y; /// struct Y;
/// ///
/// #[derive(Component)] /// #[derive(Component)]
/// #[require( /// #[require(
/// Y, /// Y,
/// X(|| X(2)), /// X(2),
/// )] /// )]
/// struct Z; /// struct Z;
/// ///

View File

@ -1229,7 +1229,7 @@ mod tests {
struct A; struct A;
#[derive(Component, Clone, PartialEq, Debug, Default)] #[derive(Component, Clone, PartialEq, Debug, Default)]
#[require(C(|| C(5)))] #[require(C(5))]
struct B; struct B;
#[derive(Component, Clone, PartialEq, Debug)] #[derive(Component, Clone, PartialEq, Debug)]
@ -1257,7 +1257,7 @@ mod tests {
struct A; struct A;
#[derive(Component, Clone, PartialEq, Debug, Default)] #[derive(Component, Clone, PartialEq, Debug, Default)]
#[require(C(|| C(5)))] #[require(C(5))]
struct B; struct B;
#[derive(Component, Clone, PartialEq, Debug)] #[derive(Component, Clone, PartialEq, Debug)]

View File

@ -1926,7 +1926,7 @@ mod tests {
struct X; struct X;
#[derive(Component)] #[derive(Component)]
#[require(Z(new_z))] #[require(Z = new_z())]
struct Y { struct Y {
value: String, value: String,
} }
@ -2651,7 +2651,7 @@ mod tests {
struct MyRequired(bool); struct MyRequired(bool);
#[derive(Component, Default)] #[derive(Component, Default)]
#[require(MyRequired(|| MyRequired(false)))] #[require(MyRequired(false))]
struct MiddleMan; struct MiddleMan;
#[derive(Component, Default)] #[derive(Component, Default)]
@ -2659,7 +2659,7 @@ mod tests {
struct ConflictingRequire; struct ConflictingRequire;
#[derive(Component, Default)] #[derive(Component, Default)]
#[require(MyRequired(|| MyRequired(true)))] #[require(MyRequired(true))]
struct MyComponent; struct MyComponent;
let mut world = World::new(); let mut world = World::new();

View File

@ -6157,7 +6157,7 @@ mod tests {
struct A; struct A;
#[derive(Component, Clone, PartialEq, Debug, Default)] #[derive(Component, Clone, PartialEq, Debug, Default)]
#[require(C(|| C(3)))] #[require(C(3))]
struct B; struct B;
#[derive(Component, Clone, PartialEq, Debug, Default)] #[derive(Component, Clone, PartialEq, Debug, Default)]

View File

@ -67,7 +67,7 @@ impl Plugin for ForwardDecalPlugin {
/// * Looking at forward decals at a steep angle can cause distortion. This can be mitigated by padding your decal's /// * Looking at forward decals at a steep angle can cause distortion. This can be mitigated by padding your decal's
/// texture with extra transparent pixels on the edges. /// texture with extra transparent pixels on the edges.
#[derive(Component, Reflect)] #[derive(Component, Reflect)]
#[require(Mesh3d(|| Mesh3d(FORWARD_DECAL_MESH_HANDLE)))] #[require(Mesh3d(FORWARD_DECAL_MESH_HANDLE))]
pub struct ForwardDecal; pub struct ForwardDecal;
/// Type alias for an extended material with a [`ForwardDecalMaterialExt`] extension. /// Type alias for an extended material with a [`ForwardDecalMaterialExt`] extension.

View File

@ -5,5 +5,5 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect};
/// Marker struct for buttons /// Marker struct for buttons
#[derive(Component, Debug, Default, Clone, Copy, PartialEq, Eq, Reflect)] #[derive(Component, Debug, Default, Clone, Copy, PartialEq, Eq, Reflect)]
#[reflect(Component, Default, Debug, PartialEq, Clone)] #[reflect(Component, Default, Debug, PartialEq, Clone)]
#[require(Node, FocusPolicy(|| FocusPolicy::Block), Interaction)] #[require(Node, FocusPolicy::Block, Interaction)]
pub struct Button; pub struct Button;