Skip to content

Conversation

@DanikVitek
Copy link
Collaborator

@DanikVitek DanikVitek commented Jan 18, 2025

Also:

  • Improve error messages
  • Extendable type-safe rewrite of the macro expansion

Closes #16.

@DanikVitek DanikVitek changed the title WIP: Implementing support for generic params; Extendable type-safe rewrite of the macro expansion. Implement support for generic params; Extendable type-safe rewrite of the macro expansion. Jan 20, 2025
@DanikVitek DanikVitek marked this pull request as ready for review January 20, 2025 13:03
@DanikVitek
Copy link
Collaborator Author

Hello, @scouten. Ready for review

@DanikVitek DanikVitek changed the title Implement support for generic params; Extendable type-safe rewrite of the macro expansion. Implement support for generic params; Improve error messages. Extendable type-safe rewrite of the macro expansion. Jan 20, 2025
@DanikVitek
Copy link
Collaborator Author

DanikVitek commented Jan 20, 2025

Should be marked as "Closes #16"

Needs to be new major, because async_signature now requires to explicitly specify or not specify the return type
@DanikVitek
Copy link
Collaborator Author

I need to add support for #[cfg_attr(..., async_generic)], so don not merge just yet

@DanikVitek
Copy link
Collaborator Author

Or rather, attributes on async_signature

@DanikVitek
Copy link
Collaborator Author

Battle-testing the implementation on borsh-rs

@scouten
Copy link
Owner

scouten commented Jan 21, 2025

@DanikVitek this looks very interesting. I'll want to make sure the CI runs pass before merging and also for you to add the #[cfg(...)] upgrades that you are planning.

Let me know how I can help.

Copy link
Owner

@scouten scouten left a comment

Choose a reason for hiding this comment

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

Great and welcome proposal! I look forward to the final draft as discussed in my initial comments.

…ersions, storing only respective kind of functions, and llow to specify custom trait bounds on async version
@DanikVitek DanikVitek requested a review from scouten January 21, 2025 23:35
@DanikVitek
Copy link
Collaborator Author

I might also add the sync_signature meta tag for functions to allow annotating sync functions with attributes, unique to them. For example, doc comments should be somewhat different for sync and async functions

@DanikVitek
Copy link
Collaborator Author

@DanikVitek this looks very interesting. I'll want to make sure the CI runs pass before merging and also for you to add the #[cfg(...)] upgrades that you are planning.

Let me know how I can help.

I need to add new features behind feature flags?

@scouten-adobe
Copy link

Now that I think about it, what are the reasons anybody would want to change the return type of their async function relative to the sync function? The only real reason I can come up with is if they're using a custom Future implementation. But in that case, why would they use this crate, as the custom future requires to move the implementation elsewhere, or surround the whole function body with an async move {...} block?

We haven't encountered that situation yet, but I wouldn't completely rule it out. Possible the return type could be something that implements a different type in sync vs async worlds. We do have that situation routinely for function parameters.

Copy link

@scouten-adobe scouten-adobe left a comment

Choose a reason for hiding this comment

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

This is a really impressive change and looks very well thought out.

This takes me beyond my knowledge level for Rust's proc-macros. If we merge this, would you be willing to join me as a co-maintainer of this crate?

async-std = { version = "1.0", features = ["attributes"] }
trybuild = { version = "1.0", features = ["diff"] }
async-trait = "0.1"
trybuild = { version = "<=1.0.90", features = ["diff"] }

Choose a reason for hiding this comment

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

What was the issue with newer versions of trybuild?

Copy link
Collaborator Author

@DanikVitek DanikVitek Jan 31, 2025

Choose a reason for hiding this comment

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

it required a higher MSRV. I wanted to lower the MSRV to 1.67

@DanikVitek
Copy link
Collaborator Author

This is a really impressive change and looks very well thought out.

This takes me beyond my knowledge level for Rust's proc-macros. If we merge this, would you be willing to join me as a co-maintainer of this crate?

Sure, I'd be happy to

@scouten scouten marked this pull request as ready for review February 6, 2025 18:36
@scouten
Copy link
Owner

scouten commented Feb 6, 2025

@DanikVitek I've sent you an invite to collaborator status. Looks like there are a few Clippy and rustfmt details to sort out before we merge this, but once the tests run, feel free to proceed.

@dj8yfo
Copy link

dj8yfo commented Feb 24, 2025

i'll post 2 suggestions which appear reasonable to me, though they might not be reasonable at all and totally fine to be ignored:

  1. using the following instead of impl_fut (cargo fmt appears not to react to the newline and not to attempt to fold everything into 1 line)
    #[async_generic(
        async_signature = 
            fn <'a, W: AsyncWrite>(&'a self, writer: &'a mut W) -> impl Future<Output = Result<()>> + Send + 'a
    )]
    ...
    #[async_generic(
        async_signature =
            async fn <'a, W: AsyncWrite>(&'a self, writer: &'a mut W) -> Result<()>
    )]
  1. using something more wild for if _sync { ... } else { ... } expressions, like:
@if_sync_async_expr [
    _sync { 
        /// ....
    } _async {
        /// ...
    } 
]

or smth else of that kind, so that it's clearly visible that it's special proc-macro magic and not regular rust syntax.
This way imo it would be more justified to use this arbitrarily as long as it can be parsed, due to the barrier with regular syntax having been established.

@DanikVitek
Copy link
Collaborator Author

DanikVitek commented Feb 26, 2025

  1. using the following instead of impl_fut (cargo fmt appears not to react to the newline and not to attempt to fold everything into 1 line)
    #[async_generic(
        async_signature = 
            fn <'a, W: AsyncWrite>(&'a self, writer: &'a mut W) -> impl Future<Output = Result<()>> + Send + 'a
    )]
    ...
    #[async_generic(
        async_signature =
            async fn <'a, W: AsyncWrite>(&'a self, writer: &'a mut W) -> Result<()>
    )]

@dj8yfo @scouten regarding this, I've came up with 5 "modes" alongside the impl_fut one: async_fn (default), impl_fut, pin_box_fut, ready, pin_box_ready. These keywords indicate to the macro, which tupe of function body to generate for the async function:

  • async_fn — simply inline the body;
  • impl_fut — wrap the result into async move {} if there's at least one .await call;
  • pin_box_fut — same as impl_fut, but additionally wrapped into Box::pin() call;
  • ready — wrap the function body into the core::future::ready() call. If the function body is synchronous, this is better than async {};
  • pin_box_ready — same as ready, but additionally wrapped into Box::pin() call.

I'm not sure how else to better indicate this niche behavior, but open for suggestions

@dj8yfo
Copy link

dj8yfo commented Feb 27, 2025

@DanikVitek well, then maybe just split it into
async_signature = fn ... , async_mode = impl_fut,
where async_mode should only be used if it's smth exotic and not the most frequent default. This way it'll also be clear that these 5 options are part of async_mode toggle documentation.
It's really quite hard to read when both are piled up in a single attribute async_signature[impl_fut]<'boom_some_lifetime...,

@dj8yfo
Copy link

dj8yfo commented Feb 27, 2025

Also _signature implies it includes method name too, maybe the name could be included too to explicitly show it's the same in the case of traits. In some other cases it may theoretically differ

@DanikVitek
Copy link
Collaborator Author

DanikVitek commented Feb 27, 2025

  1. using the following instead of impl_fut (cargo fmt appears not to react to the newline and not to attempt to fold everything into 1 line)
    #[async_generic(
        async_signature = 
            fn <'a, W: AsyncWrite>(&'a self, writer: &'a mut W) -> impl Future<Output = Result<()>> + Send + 'a
    )]
    ...
    #[async_generic(
        async_signature =
            async fn <'a, W: AsyncWrite>(&'a self, writer: &'a mut W) -> Result<()>
    )]

@dj8yfo Also, the next question is how to assign attributes on the signatures? Like this:

#[async_generic(
    async_signature =
        /// docs
        #[cfg(feature = "async")]
        async fn <'a, W: AsyncWrite>(&'a self, writer: &'a mut W) -> Result<()>
)]

or the same way it was before:

#[async_generic(
    /// docs
    #[cfg(feature = "async")]
    async_signature =
        async fn <'a, W: AsyncWrite>(&'a self, writer: &'a mut W) -> Result<()>
)]

?
If the first variant is used, it becomes much more verbose to add attributes to an async function with the same parameters as the sync function

@DanikVitek
Copy link
Collaborator Author

And the function signature is tied to its mode, so async fn cannot be used with anything other than async_fn mode and vice versa

@dj8yfo
Copy link

dj8yfo commented Feb 28, 2025

And the function signature is tied to its mode, so async fn cannot be used with anything other than async_fn mode and vice versa

this can be enforced by a diagnostic, and the user may experiment with wrong combinations and learn about it from diagnostic

@dj8yfo
Copy link

dj8yfo commented Feb 28, 2025

or the same way it was before:

#[async_generic(
    /// docs
    #[cfg(feature = "async")]
    async_signature =
        async fn <'a, W: AsyncWrite>(&'a self, writer: &'a mut W) -> Result<()>
)]

?

this looks better, but maybe you could consider splitting this into async_variant_attributes, smth like:

#[async_generic(
     async_variant_attributes = (
         #[doc = "....."]
         #[cfg(feature = "async")]
    ), 
    async_signature =
        async fn <'a, W: AsyncWrite>(&'a self, writer: &'a mut W) -> Result<()>
)]

This way it could be also mentioned on the async_variant_attributes toggle doc that it replaces all attributes on the method/function of the sync variant, which is similar to current behaviour, i believe.

@DanikVitek DanikVitek marked this pull request as draft March 13, 2025 18:20
@scouten
Copy link
Owner

scouten commented Nov 1, 2025

My sincere apologies for letting this linger for a long time. I have some split loyalties here – I'd like to take this change, but I also realize that I need to keep this simple enough that I can rely on and maintain this crate for use by my team (see https://crates.io/c2pa).

For that reason, I've decided not to merge this PR. I'd like to invite you to take the work done in this PR into a new more advanced generic crate. Please use any existing code from async_generic with my blessing.

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.

Generic dependent on sync/async

4 participants