bevy/crates/bevy_reflect/compile_fail/Cargo.toml

23 lines
509 B
TOML
Raw Normal View History

[package]
Move compile fail tests (#13196) # Objective - Follow-up of #13184 :) - We use `ui_test` to test compiler errors for our custom macros. - There are four crates related to compile fail tests - `bevy_ecs_compile_fail_tests`, `bevy_macros_compile_fail_tests`, and `bevy_reflect_compile_fail_tests`, which actually test the macros. - [`bevy_compile_test_utils`](https://github.com/bevyengine/bevy/tree/64c1c65783938facc59d9b36cbaa6deba435d84e/crates/bevy_compile_test_utils), which provides helpers and common patterns for these tests. - All of these crates reside within the `crates` directory. - This can be confusing, especially for newcomers. All of the other folders in `crates` are actual published libraries, except for these 4. ## Solution - Move all compile fail tests to a `compile_fail` folder under their corresponding crate. - E.g. `crates/bevy_ecs_compile_fail_tests` would be moved to `crates/bevy_ecs/compile_fail`. - Move `bevy_compile_test_utils` to `tools/compile_fail_utils`. There are a few benefits to this approach: 1. An internal testing detail is less intrusive (and confusing) for those who just want to browse the public Bevy interface. 2. Follows a pre-existing approach of organizing related crates inside a larger crate's folder. - See `bevy_gizmos/macros` for an example. 4. Makes consistent the terms `compile_test`, `compile_fail`, and `compile_fail_test` in code. It's all just `compile_fail` now, because we are specifically testing the error messages on compiler failures. - To be clear it can still be referred to by these terms in comments and speech, just the names of the crates and the CI command are now consistent. ## Testing Run the compile fail CI command: ```shell cargo run -p ci -- compile-fail ``` If it still passes, then my refactor was successful.
2024-05-03 13:35:21 +00:00
name = "bevy_reflect_compile_fail"
edition = "2021"
description = "Compile fail tests for Bevy Engine's reflection system"
homepage = "https://bevyengine.org"
repository = "https://github.com/bevyengine/bevy"
license = "MIT OR Apache-2.0"
publish = false
[dependencies]
bevy_reflect = { path = "../", features = ["functions"] }
[dev-dependencies]
Move compile fail tests (#13196) # Objective - Follow-up of #13184 :) - We use `ui_test` to test compiler errors for our custom macros. - There are four crates related to compile fail tests - `bevy_ecs_compile_fail_tests`, `bevy_macros_compile_fail_tests`, and `bevy_reflect_compile_fail_tests`, which actually test the macros. - [`bevy_compile_test_utils`](https://github.com/bevyengine/bevy/tree/64c1c65783938facc59d9b36cbaa6deba435d84e/crates/bevy_compile_test_utils), which provides helpers and common patterns for these tests. - All of these crates reside within the `crates` directory. - This can be confusing, especially for newcomers. All of the other folders in `crates` are actual published libraries, except for these 4. ## Solution - Move all compile fail tests to a `compile_fail` folder under their corresponding crate. - E.g. `crates/bevy_ecs_compile_fail_tests` would be moved to `crates/bevy_ecs/compile_fail`. - Move `bevy_compile_test_utils` to `tools/compile_fail_utils`. There are a few benefits to this approach: 1. An internal testing detail is less intrusive (and confusing) for those who just want to browse the public Bevy interface. 2. Follows a pre-existing approach of organizing related crates inside a larger crate's folder. - See `bevy_gizmos/macros` for an example. 4. Makes consistent the terms `compile_test`, `compile_fail`, and `compile_fail_test` in code. It's all just `compile_fail` now, because we are specifically testing the error messages on compiler failures. - To be clear it can still be referred to by these terms in comments and speech, just the names of the crates and the CI command are now consistent. ## Testing Run the compile fail CI command: ```shell cargo run -p ci -- compile-fail ``` If it still passes, then my refactor was successful.
2024-05-03 13:35:21 +00:00
compile_fail_utils = { path = "../../../tools/compile_fail_utils" }
[[test]]
name = "derive"
harness = false
bevy_reflect: Function reflection (#13152) # Objective We're able to reflect types sooooooo... why not functions? The goal of this PR is to make functions callable within a dynamic context, where type information is not readily available at compile time. For example, if we have a function: ```rust fn add(left: i32, right: i32) -> i32 { left + right } ``` And two `Reflect` values we've already validated are `i32` types: ```rust let left: Box<dyn Reflect> = Box::new(2_i32); let right: Box<dyn Reflect> = Box::new(2_i32); ``` We should be able to call `add` with these values: ```rust // ????? let result: Box<dyn Reflect> = add.call_dynamic(left, right); ``` And ideally this wouldn't just work for functions, but methods and closures too! Right now, users have two options: 1. Manually parse the reflected data and call the function themselves 2. Rely on registered type data to handle the conversions for them For a small function like `add`, this isn't too bad. But what about for more complex functions? What about for many functions? At worst, this process is error-prone. At best, it's simply tedious. And this is assuming we know the function at compile time. What if we want to accept a function dynamically and call it with our own arguments? It would be much nicer if `bevy_reflect` could alleviate some of the problems here. ## Solution Added function reflection! This adds a `DynamicFunction` type to wrap a function dynamically. This can be called with an `ArgList`, which is a dynamic list of `Reflect`-containing `Arg` arguments. It returns a `FunctionResult` which indicates whether or not the function call succeeded, returning a `Reflect`-containing `Return` type if it did succeed. Many functions can be converted into this `DynamicFunction` type thanks to the `IntoFunction` trait. Taking our previous `add` example, this might look something like (explicit types added for readability): ```rust fn add(left: i32, right: i32) -> i32 { left + right } let mut function: DynamicFunction = add.into_function(); let args: ArgList = ArgList::new().push_owned(2_i32).push_owned(2_i32); let result: Return = function.call(args).unwrap(); let value: Box<dyn Reflect> = result.unwrap_owned(); assert_eq!(value.take::<i32>().unwrap(), 4); ``` And it also works on closures: ```rust let add = |left: i32, right: i32| left + right; let mut function: DynamicFunction = add.into_function(); let args: ArgList = ArgList::new().push_owned(2_i32).push_owned(2_i32); let result: Return = function.call(args).unwrap(); let value: Box<dyn Reflect> = result.unwrap_owned(); assert_eq!(value.take::<i32>().unwrap(), 4); ``` As well as methods: ```rust #[derive(Reflect)] struct Foo(i32); impl Foo { fn add(&mut self, value: i32) { self.0 += value; } } let mut foo = Foo(2); let mut function: DynamicFunction = Foo::add.into_function(); let args: ArgList = ArgList::new().push_mut(&mut foo).push_owned(2_i32); function.call(args).unwrap(); assert_eq!(foo.0, 4); ``` ### Limitations While this does cover many functions, it is far from a perfect system and has quite a few limitations. Here are a few of the limitations when using `IntoFunction`: 1. The lifetime of the return value is only tied to the lifetime of the first argument (useful for methods). This means you can't have a function like `(a: i32, b: &i32) -> &i32` without creating the `DynamicFunction` manually. 2. Only 15 arguments are currently supported. If the first argument is a (mutable) reference, this number increases to 16. 3. Manual implementations of `Reflect` will need to implement the new `FromArg`, `GetOwnership`, and `IntoReturn` traits in order to be used as arguments/return types. And some limitations of `DynamicFunction` itself: 1. All arguments share the same lifetime, or rather, they will shrink to the shortest lifetime. 2. Closures that capture their environment may need to have their `DynamicFunction` dropped before accessing those variables again (there is a `DynamicFunction::call_once` to make this a bit easier) 3. All arguments and return types must implement `Reflect`. While not a big surprise coming from `bevy_reflect`, this implementation could actually still work by swapping `Reflect` out with `Any`. Of course, that makes working with the arguments and return values a bit harder. 4. Generic functions are not supported (unless they have been manually monomorphized) And general, reflection gotchas: 1. `&str` does not implement `Reflect`. Rather, `&'static str` implements `Reflect` (the same is true for `&Path` and similar types). This means that `&'static str` is considered an "owned" value for the sake of generating arguments. Additionally, arguments and return types containing `&str` will assume it's `&'static str`, which is almost never the desired behavior. In these cases, the only solution (I believe) is to use `&String` instead. ### Followup Work This PR is the first of two PRs I intend to work on. The second PR will aim to integrate this new function reflection system into the existing reflection traits and `TypeInfo`. The goal would be to register and call a reflected type's methods dynamically. I chose not to do that in this PR since the diff is already quite large. I also want the discussion for both PRs to be focused on their own implementation. Another followup I'd like to do is investigate allowing common container types as a return type, such as `Option<&[mut] T>` and `Result<&[mut] T, E>`. This would allow even more functions to opt into this system. I chose to not include it in this one, though, for the same reasoning as previously mentioned. ### Alternatives One alternative I had considered was adding a macro to convert any function into a reflection-based counterpart. The idea would be that a struct that wraps the function would be created and users could specify which arguments and return values should be `Reflect`. It could then be called via a new `Function` trait. I think that could still work, but it will be a fair bit more involved, requiring some slightly more complex parsing. And it of course is a bit more work for the user, since they need to create the type via macro invocation. It also makes registering these functions onto a type a bit more complicated (depending on how it's implemented). For now, I think this is a fairly simple, yet powerful solution that provides the least amount of friction for users. --- ## Showcase Bevy now adds support for storing and calling functions dynamically using reflection! ```rust // 1. Take a standard Rust function fn add(left: i32, right: i32) -> i32 { left + right } // 2. Convert it into a type-erased `DynamicFunction` using the `IntoFunction` trait let mut function: DynamicFunction = add.into_function(); // 3. Define your arguments from reflected values let args: ArgList = ArgList::new().push_owned(2_i32).push_owned(2_i32); // 4. Call the function with your arguments let result: Return = function.call(args).unwrap(); // 5. Extract the return value let value: Box<dyn Reflect> = result.unwrap_owned(); assert_eq!(value.take::<i32>().unwrap(), 4); ``` ## Changelog #### TL;DR - Added support for function reflection - Added a new `Function Reflection` example: https://github.com/bevyengine/bevy/blob/ba727898f2adff817838fc4cdb49871bbce37356/examples/reflection/function_reflection.rs#L1-L157 #### Details Added the following items: - `ArgError` enum - `ArgId` enum - `ArgInfo` struct - `ArgList` struct - `Arg` enum - `DynamicFunction` struct - `FromArg` trait (derived with `derive(Reflect)`) - `FunctionError` enum - `FunctionInfo` struct - `FunctionResult` alias - `GetOwnership` trait (derived with `derive(Reflect)`) - `IntoFunction` trait (with blanket implementation) - `IntoReturn` trait (derived with `derive(Reflect)`) - `Ownership` enum - `ReturnInfo` struct - `Return` enum --------- Co-authored-by: Periwink <charlesbour@gmail.com>
2024-07-01 13:49:08 +00:00
[[test]]
name = "func"
harness = false