Skip to content

Add opt-in generic traits for generated SoA types #73

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

Merged
merged 37 commits into from
Mar 19, 2025

Conversation

mobiusklein
Copy link
Contributor

This adds SoASlice, SoASliceMut, and SoAVec traits and adds a new attribute #[generate_traits] to automatically implement them on the generated types.

Because I saw a specific Rust version pinned in the test suite that predated the stabilization of GATs, I had to feature-gate the traits on both the internal and public-facing crates. Unfortunately, the mixed root + internal crates workspace setup didn't seem to support traits on the crate at the repository root, so I had to re-structure repository to a uniform workspace. The traits still exist when the generic_traits feature is disabled, but they have no methods or associated types so the source code is still Rust 1.63 compatible.

Hopefully, that means there are no breaking changes unless the feature is enabled.

I couldn't implement a clean "root trait" that tied all of the different types and interfaces together. I could not find a way to do so while not generating a type system query overflow or cycle or cease to function as lifetime dependencies tangled. I'm still not satisfied with the API, but I've convinced myself that as the language stands today I can't make it any better.

@mobiusklein
Copy link
Contributor Author

Added an additional optional type-level attribute macro soa_crate(::path::to::soa_derive) for locating the soa_derive crate if it is not imported from ::soa_derive directly. This is so that it can be vendor'd or aliased.

@Luthaf
Copy link
Member

Luthaf commented Jan 27, 2025

Added an additional optional type-level attribute macro soa_crate(::path::to::soa_derive) for locating the soa_derive crate if it is not imported from ::soa_derive directly. This is so that it can be vendor'd or aliased.

This is unrelated to the generic trait, right? Could you split it into a separate PR?

@Luthaf
Copy link
Member

Luthaf commented Jan 27, 2025

Regarding the main feature: it is quite a lot of new code, so I'll need to find the time to review it properly!

@Luthaf
Copy link
Member

Luthaf commented Jan 27, 2025

From a very quick look, could you explain why we need a generic_traits feature? Could this not be always enabled?

Also, please do not reorganize the code while contributing a feature. You moved the soa_derive crate around, but it is not needed. Can you move it back to the root of the repository?

@mobiusklein
Copy link
Contributor Author

Added an additional optional type-level attribute macro soa_crate(::path::to::soa_derive) for locating the soa_derive crate if it is not imported from ::soa_derive directly. This is so that it can be vendor'd or aliased.

This is unrelated to the generic trait, right? Could you split it into a separate PR?

Technically no, it just required making changes to all generated code, including the newly added trait code. If you still want it split into another PR, I can revert the changes on this branch and then re-add them afterwards in a separate PR if this one is accepted.

From a very quick look, could you explain why we need a generic_traits feature? Could this not be always enabled?

I feature-gated this behavior because it requires GATs and you had explicitly called out a Rust version without GAT support in the CI configuration. If you want to enable the behavior all the time, while retaining the added generate_traits attribute to actually opt-in to trait impls, I'll need to remove the pinned 1.63 Rust version in CI.

Also, please do not reorganize the code while contributing a feature. You moved the soa_derive crate around, but it is not needed. Can you move it back to the root of the repository?

I wasn't able to get Cargo to recognize a feature on soa_derive itself when the Cargo.toml that configured the workspace was also the one for soa_derive itself. If you want me to remove the feature gate, I can also restore the original project layout.

@mobiusklein
Copy link
Contributor Author

Just so I know whether to get started, do you agree I should remove the pre-GAT version of Rust from CI and restore the project layout?

Thank you

@Luthaf
Copy link
Member

Luthaf commented Jan 29, 2025

It looks like we only need to update the MSRV to 1.65 to get GAT, right? If so, I'm happy to update the MSRV here, remove the feature gate & code re-organization!

EDIT: this would mean updating the pinned rustc in CI to 1.65. If 1.65 has other issues/missing parts, we could also update further, but I'm trying to make sure soa_derive is not forcing downstream projects to upgrade their rustc version.

@Luthaf
Copy link
Member

Luthaf commented Jan 29, 2025

Technically no, it just required making changes to all generated code, including the newly added trait code. If you still want it split into another PR, I can revert the changes on this branch and then re-add them afterwards in a separate PR if this one is accepted.

Yes please!

If you want to enable the behavior all the time, while retaining the added generate_traits attribute to actually opt-in to trait impls

I guess this is because generate_traits will increase compile time a lot? Here as well I would prefer if the trait implementation was generated in all cases, unless the slowdown is very high.

@mobiusklein
Copy link
Contributor Author

I have reverted changes to the project layout, made trait implementation automatic, and removed the crate name option

@mobiusklein
Copy link
Contributor Author

It appears the minimum Rust version required to run the test suite is 1.78. The code itself might compile on an earlier Rust version though using it in a generic context would likely be constrained.

@mobiusklein
Copy link
Contributor Author

mobiusklein commented Feb 5, 2025

@Luthaf just to make sure this is clear, the current state of the PR is ready for review, or at least to have the MSRV change requirement rejected so I can revert all the changes and restore the feature gate or attempt some other means of achieving the same idea without forcing the project layout to change.

@Luthaf
Copy link
Member

Luthaf commented Feb 12, 2025

Hey @mobiusklein! Sorry for the delayed answer, both personal and work life have been taking a lot of my time recently. I'll likely have to continue dealing with it for a couple more weeks, so this might take some time, sorry in advance.

Just to be clear, 1.78 is only required for the tests of the new traits, right? I'd like to keep the code in a state that can compile on recent-ish ubuntu, which is currently using 1.75. So for now bumping CI to 1.78 would be fine for me, and I can add another CI job that checks with an older version that the code still compiles, excluding the tests for the new traits.

@mobiusklein
Copy link
Contributor Author

Thank you for the warning, not a problem. I am currently using this for prototyping, but if it advances, I may need to release a fork since I don't think vendoring works with proc-macros. This was a great way to learn about procedural macros and if I am not careful I'll rewrite more of my traits this way.

Yes, that's correct. I had also looked into how hard it would be to introduce conditional compilation for a specific Rust version, but unfortunately that requires adding a build script and/or extra dependencies. At one point the GATs were actually causing a compiler crash on Rust 1.80 or 1.82, but I don't think that version of the code is being used here.

Copy link
Member

@Luthaf Luthaf left a comment

Choose a reason for hiding this comment

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

This looks good overall! There are a couple of places where I did not understand the reason behind the code, and what looks like a couple of leftover from development.

@@ -124,7 +125,7 @@ pub fn derive(input: &Input) -> TokenStream {
/// Get an iterator over the
#[doc = #ref_doc_url]
/// in this vector
pub fn iter(&self) -> #iter_name {
pub fn iter(&self) -> <#name as ::soa_derive::SoAIter>::Iter {
Copy link
Member

Choose a reason for hiding this comment

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

I don't understand why this is required here?

For readbility, I find referring to the type directly better, instead of going through associated types of a trait

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 was likely tying the type system in balloon animals at one point and that made the system happy, and after reverting that particular design, it remained happy with those aliases so they remained. I'll restored the generated names

Copy link
Member

Choose a reason for hiding this comment

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

I think this still needs updating

src/lib.rs Outdated
//! # mod cheese {
//! # use soa_derive::{StructOfArray, prelude::*};
//! #[derive(StructOfArray)]
//! #[generate_traits]
Copy link
Member

Choose a reason for hiding this comment

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

not sure if this is up to date, but what would be the cost of always generating the trait implementations?

And if the cost is too high to always enable them, then this should be scoped with something like #[soa_derive(generate_traits)]

src/lib.rs Outdated
Comment on lines 788 to 790
pub mod prelude {
pub use super::{SoAVec, SoAIter, SoASlice, SoASliceMut, SoAPointers, StructOfArray, ToSoAVec, IntoSoAIter, SoAAppendVec};
}
Copy link
Member

Choose a reason for hiding this comment

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

This is everything defined by this crate, isn't it? I don't think it's worth having a separate prelude in this case, the same can be achieved with use soa_derive::* instead of use soa_derive::prelude::*

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That'd also pull the soa_zip macro and its helper, soa_zip_impl, along with SoAIndex, which isn't necessary to use the generic traits or the types themselves.

The prelude also means that if in the future you were to add more things to the crate root, they would only be pulled by a wildcard import if they were also added to the prelude. This is probably overly defensive future-proofing though.

If you still think this is acceptable, I'm fine with removing the prelude module.

mobiusklein and others added 3 commits February 25, 2025 18:31
Co-authored-by: Guillaume Fraux <[email protected]>
@mobiusklein
Copy link
Contributor Author

There were a lot of things left over from development, thank you for noting that. I had tried several different overlapping designs to try to find a way out of the corners I was painting myself into with the type system. At one point it was even crashing rustc 1.82.

I'll go through the documentation again now that the implementation is more-or-less acceptable. Thank you.

@mobiusklein
Copy link
Contributor Author

Can you please re-iterate your ruling on the prelude module? If you still want it removed, I'll remove it and then do a final pass over the documentation.

@Luthaf
Copy link
Member

Luthaf commented Mar 16, 2025

Let's remove the prelude for now, we can always add it back down the line if it is needed, but for now I don't think it is worth doing.

Thanks for your patience with this! I'll try to do a final review tomorrow, but I think the code is in a great shape overall!

Copy link
Member

@Luthaf Luthaf left a comment

Choose a reason for hiding this comment

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

Thanks a lot for this work! I'll try to make the code work on CI with an earlier rustc, and then make a new release with this

Comment on lines +145 to +156
impl<'a> From<#ref_name<'a>> for #name where #( for<'b> #fields_types: Clone, )* {
fn from(value: #ref_name<'a>) -> #name {
value.to_owned()
}
}

impl<'a> From<&'a #ref_name<'a>> for #name where #( for<'b> #fields_types: Clone, )* {
fn from(value: &'a #ref_name<'a>) -> #name {
value.to_owned()
}
}

Copy link
Member

Choose a reason for hiding this comment

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

ok, this makes sense. Thanks for the detailed explanation!

@Luthaf Luthaf merged commit 6a66d43 into lumol-org:master Mar 19, 2025
5 checks passed
@Luthaf
Copy link
Member

Luthaf commented Mar 19, 2025

Now published as v0.14.0 on crates.io!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants