diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index adc3c2c..c688ceb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,6 +20,8 @@ jobs: steps: - uses: actions/checkout@v3 - uses: dtolnay/rust-toolchain@stable + with: + components: rust-src # required for consistent error messages - run: cargo install cargo-expand - run: cargo test --verbose diff --git a/README.md b/README.md index 59cf868..c75f679 100644 --- a/README.md +++ b/README.md @@ -52,25 +52,35 @@ async fn fib(n : u32) -> u32 { } ``` -## ?Send Option +## ?Send option -The returned future has a `Send` bound to make sure it can be sent between threads. +The returned `Future` has a `Send` bound to make sure it can be sent between threads. If this is undesirable you can mark that the bound should be left out like so: ```rust #[async_recursion(?Send)] -async fn example() { +async fn returned_future_is_not_send() { // ... } ``` -In detail: +## Sync option + +The returned `Future` doesn't have a `Sync` bound as it is usually not required. +You can include a `Sync` bound as follows: + +```rust +#[async_recursion(Sync)] +async fn returned_future_is_sync() { + // ... +} +``` -- `#[async_recursion]` modifies your function to return a [`BoxFuture`], and -- `#[async_recursion(?Send)]` modifies your function to return a [`LocalBoxFuture`]. +In detail: -[`BoxFuture`]: https://docs.rs/futures/0.3.19/futures/future/type.BoxFuture.html -[`LocalBoxFuture`]: https://docs.rs/futures/0.3.19/futures/future/type.LocalBoxFuture.html +- `#[async_recursion]` modifies your function to return a boxed `Future` with a `Send` bound. +- `#[async_recursion(?Send)]` modifies your function to return a boxed `Future` _without_ a `Send` bound. +- `#[async_recursion(Sync)]` modifies your function to return a boxed `Future` with a `Send` and `Sync` bound. ### License diff --git a/src/expand.rs b/src/expand.rs index bedc281..6a00e75 100644 --- a/src/expand.rs +++ b/src/expand.rs @@ -173,6 +173,12 @@ fn transform_sig(sig: &mut Signature, args: &RecursionArgs) { quote!() }; + let sync_bound: TokenStream = if args.sync_bound { + quote!(+ ::core::marker::Sync) + } else { + quote!() + }; + let where_clause = sig .generics .where_clause @@ -196,6 +202,6 @@ fn transform_sig(sig: &mut Signature, args: &RecursionArgs) { // Modify the return type sig.output = parse_quote! { -> ::core::pin::Pin #box_lifetime #send_bound >> + dyn ::core::future::Future #box_lifetime #send_bound #sync_bound>> }; } diff --git a/src/lib.rs b/src/lib.rs index 38d701b..fbddaa4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -52,27 +52,40 @@ //! } //! ``` //! -//! ## ?Send Option +//! ## ?Send option //! -//! The returned future has a [`Send`] bound to make sure it can be sent between threads. +//! The returned [`Future`] has a [`Send`] bound to make sure it can be sent between threads. //! If this is undesirable you can mark that the bound should be left out like so: //! //! ```rust //! # use async_recursion::async_recursion; //! //! #[async_recursion(?Send)] -//! async fn example() { +//! async fn returned_future_is_not_send() { +//! // ... +//! } +//! ``` +//! +//! ## Sync option +//! +//! The returned [`Future`] doesn't have a [`Sync`] bound as it is usually not required. +//! You can include a [`Sync`] bound as follows: +//! +//! ```rust +//! # use async_recursion::async_recursion; +//! +//! #[async_recursion(Sync)] +//! async fn returned_future_is_send_and_sync() { //! // ... //! } //! ``` //! //! In detail: //! -//! - `#[async_recursion]` modifies your function to return a [`BoxFuture`], and -//! - `#[async_recursion(?Send)]` modifies your function to return a [`LocalBoxFuture`]. //! -//! [`BoxFuture`]: https://docs.rs/futures/0.3.19/futures/future/type.BoxFuture.html -//! [`LocalBoxFuture`]: https://docs.rs/futures/0.3.19/futures/future/type.LocalBoxFuture.html +//! - `#[async_recursion]` modifies your function to return a boxed [`Future`] with a [`Send`] bound. +//! - `#[async_recursion(?Send)]` modifies your function to return a boxed [`Future`] _without_ a [`Send`] bound. +//! - `#[async_recursion(Sync)]` modifies your function to return a boxed [`Future`] with [`Send`] and [`Sync`] bounds. //! //! ### License //! diff --git a/src/parse.rs b/src/parse.rs index 728dcbf..6e4e06a 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -22,30 +22,74 @@ impl Parse for AsyncItem { pub struct RecursionArgs { pub send_bound: bool, -} - -impl Default for RecursionArgs { - fn default() -> Self { - RecursionArgs { send_bound: true } - } + pub sync_bound: bool, } /// Custom keywords for parser mod kw { syn::custom_keyword!(Send); + syn::custom_keyword!(Sync); } -impl Parse for RecursionArgs { +#[derive(Debug, PartialEq, Eq)] +enum Arg { + NotSend, + Sync, +} + +impl std::fmt::Display for Arg { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::NotSend => write!(f, "?Send"), + Self::Sync => write!(f, "Sync"), + } + } +} + +impl Parse for Arg { fn parse(input: ParseStream) -> Result { - // Check for the `?Send` option if input.peek(Token![?]) { input.parse::()?; input.parse::()?; - Ok(Self { send_bound: false }) - } else if !input.is_empty() { - Err(input.error("expected `?Send` or empty")) + Ok(Arg::NotSend) } else { - Ok(Self::default()) + input.parse::()?; + Ok(Arg::Sync) } } } + +impl Parse for RecursionArgs { + fn parse(input: ParseStream) -> Result { + let mut send_bound: bool = true; + let mut sync_bound: bool = false; + + let args_parsed: Vec = + syn::punctuated::Punctuated::::parse_terminated(input) + .map_err(|e| input.error(format!("failed to parse macro arguments: {e}")))? + .into_iter() + .collect(); + + // Avoid sloppy input + if args_parsed.len() > 2 { + return Err(Error::new(Span::call_site(), "received too many arguments")); + } else if args_parsed.len() == 2 && args_parsed[0] == args_parsed[1] { + return Err(Error::new( + Span::call_site(), + format!("received duplicate argument: `{}`", args_parsed[0]), + )); + } + + for arg in args_parsed { + match arg { + Arg::NotSend => send_bound = false, + Arg::Sync => sync_bound = true, + } + } + + Ok(Self { + send_bound, + sync_bound, + }) + } +} diff --git a/tests/args_sync.rs b/tests/args_sync.rs new file mode 100644 index 0000000..5557829 --- /dev/null +++ b/tests/args_sync.rs @@ -0,0 +1,11 @@ +use async_recursion::async_recursion; + +#[async_recursion(Sync)] +async fn send_and_sync() {} + +fn assert_is_send_and_sync(_: impl Send + Sync) {} + +#[test] +fn test_sync_argument() { + assert_is_send_and_sync(send_and_sync()); +} diff --git a/tests/expand/args_not_send.expanded.rs b/tests/expand/args_not_send.expanded.rs new file mode 100644 index 0000000..a2c5952 --- /dev/null +++ b/tests/expand/args_not_send.expanded.rs @@ -0,0 +1,5 @@ +use async_recursion::async_recursion; +#[must_use] +fn no_send_bound() -> ::core::pin::Pin>> { + Box::pin(async move {}) +} diff --git a/tests/expand/args_not_send.rs b/tests/expand/args_not_send.rs new file mode 100644 index 0000000..f90931d --- /dev/null +++ b/tests/expand/args_not_send.rs @@ -0,0 +1,4 @@ +use async_recursion::async_recursion; + +#[async_recursion(?Send)] +async fn no_send_bound() {} \ No newline at end of file diff --git a/tests/expand/args_punctuated.expanded.rs b/tests/expand/args_punctuated.expanded.rs new file mode 100644 index 0000000..ab78804 --- /dev/null +++ b/tests/expand/args_punctuated.expanded.rs @@ -0,0 +1,25 @@ +use async_recursion::async_recursion; +#[must_use] +fn not_send_sync_1() -> ::core::pin::Pin< + Box + ::core::marker::Sync>, +> { + Box::pin(async move {}) +} +#[must_use] +fn not_send_sync_2() -> ::core::pin::Pin< + Box + ::core::marker::Sync>, +> { + Box::pin(async move {}) +} +#[must_use] +fn sync_not_send_1() -> ::core::pin::Pin< + Box + ::core::marker::Sync>, +> { + Box::pin(async move {}) +} +#[must_use] +fn sync_not_send_2() -> ::core::pin::Pin< + Box + ::core::marker::Sync>, +> { + Box::pin(async move {}) +} diff --git a/tests/expand/args_punctuated.rs b/tests/expand/args_punctuated.rs new file mode 100644 index 0000000..72700ec --- /dev/null +++ b/tests/expand/args_punctuated.rs @@ -0,0 +1,13 @@ +use async_recursion::async_recursion; + +#[async_recursion(?Send, Sync)] +async fn not_send_sync_1() {} + +#[async_recursion(?Send,Sync)] +async fn not_send_sync_2() {} + +#[async_recursion(Sync, ?Send)] +async fn sync_not_send_1() {} + +#[async_recursion(Sync,?Send)] +async fn sync_not_send_2() {} \ No newline at end of file diff --git a/tests/expand/args_sync.expanded.rs b/tests/expand/args_sync.expanded.rs new file mode 100644 index 0000000..f8efb49 --- /dev/null +++ b/tests/expand/args_sync.expanded.rs @@ -0,0 +1,11 @@ +use async_recursion::async_recursion; +#[must_use] +fn sync() -> ::core::pin::Pin< + Box< + dyn ::core::future::Future< + Output = (), + > + ::core::marker::Send + ::core::marker::Sync, + >, +> { + Box::pin(async move {}) +} diff --git a/tests/expand/args_sync.rs b/tests/expand/args_sync.rs new file mode 100644 index 0000000..79fae10 --- /dev/null +++ b/tests/expand/args_sync.rs @@ -0,0 +1,4 @@ +use async_recursion::async_recursion; + +#[async_recursion(Sync)] +async fn sync() {} \ No newline at end of file diff --git a/tests/expand/lifetimes_explicit_async_recursion_bound.expanded.rs b/tests/expand/lifetimes_explicit_async_recursion_bound.expanded.rs new file mode 100644 index 0000000..269ef06 --- /dev/null +++ b/tests/expand/lifetimes_explicit_async_recursion_bound.expanded.rs @@ -0,0 +1,19 @@ +use async_recursion::async_recursion; +#[must_use] +fn explicit_async_recursion_bound<'life0, 'life1, 'async_recursion>( + t: &'life0 T, + p: &'life1 [String], + prefix: Option<&'async_recursion [u8]>, + layer: Option<&'async_recursion [u8]>, +) -> ::core::pin::Pin< + Box< + dyn ::core::future::Future + 'async_recursion + ::core::marker::Send, + >, +> +where + 'life0: 'async_recursion, + 'life1: 'async_recursion, + 'async_recursion: 'async_recursion, +{ + Box::pin(async move {}) +} diff --git a/tests/expand/lifetimes_explicit_async_recursion_bound.rs b/tests/expand/lifetimes_explicit_async_recursion_bound.rs new file mode 100644 index 0000000..63d1c8e --- /dev/null +++ b/tests/expand/lifetimes_explicit_async_recursion_bound.rs @@ -0,0 +1,12 @@ +// Test that an explicit `async_recursion bound is left alone. +// This is a workaround many +use async_recursion::async_recursion; + + +#[async_recursion] +async fn explicit_async_recursion_bound( + t: &T, + p: &[String], + prefix: Option<&'async_recursion [u8]>, + layer: Option<&'async_recursion [u8]>, +) {} \ No newline at end of file diff --git a/tests/lifetimes.rs b/tests/lifetimes.rs index 11e8bec..d62cacd 100644 --- a/tests/lifetimes.rs +++ b/tests/lifetimes.rs @@ -41,6 +41,9 @@ async fn count_down(foo: Option<&str>) -> i32 { 0 } +#[async_recursion] +async fn explicit_async_recursion_bound(_: Option<&'async_recursion String>) {} + #[test] fn lifetime_expansion_works() { block_on(async move { @@ -73,5 +76,6 @@ fn lifetime_expansion_works() { assert_eq!(contains_value_2(&12, &node).await, false); count_down(None).await; + explicit_async_recursion_bound(None).await; }); } diff --git a/tests/ui/arg_not_sync.rs b/tests/ui/arg_not_sync.rs new file mode 100644 index 0000000..5e56200 --- /dev/null +++ b/tests/ui/arg_not_sync.rs @@ -0,0 +1,15 @@ +use async_recursion::async_recursion; + +fn assert_is_sync(_: impl Sync) {} + + +#[async_recursion] +async fn send_not_sync() {} + +#[async_recursion(?Send)] +async fn not_send_not_sync() {} + +fn main() { + assert_is_sync(send_not_sync()); + assert_is_sync(not_send_not_sync()); +} \ No newline at end of file diff --git a/tests/ui/arg_not_sync.stderr b/tests/ui/arg_not_sync.stderr new file mode 100644 index 0000000..b0f9991 --- /dev/null +++ b/tests/ui/arg_not_sync.stderr @@ -0,0 +1,51 @@ +error[E0277]: `dyn Future + Send` cannot be shared between threads safely + --> tests/ui/arg_not_sync.rs:13:20 + | +13 | assert_is_sync(send_not_sync()); + | -------------- ^^^^^^^^^^^^^^^ `dyn Future + Send` cannot be shared between threads safely + | | + | required by a bound introduced by this call + | + = help: the trait `Sync` is not implemented for `dyn Future + Send` + = note: required for `Unique + Send>` to implement `Sync` +note: required because it appears within the type `Box + Send>` + --> $RUST/alloc/src/boxed.rs + | + | pub struct Box< + | ^^^ +note: required because it appears within the type `Pin + Send>>` + --> $RUST/core/src/pin.rs + | + | pub struct Pin

{ + | ^^^ +note: required by a bound in `assert_is_sync` + --> tests/ui/arg_not_sync.rs:3:27 + | +3 | fn assert_is_sync(_: impl Sync) {} + | ^^^^ required by this bound in `assert_is_sync` + +error[E0277]: `dyn Future` cannot be shared between threads safely + --> tests/ui/arg_not_sync.rs:14:20 + | +14 | assert_is_sync(not_send_not_sync()); + | -------------- ^^^^^^^^^^^^^^^^^^^ `dyn Future` cannot be shared between threads safely + | | + | required by a bound introduced by this call + | + = help: the trait `Sync` is not implemented for `dyn Future` + = note: required for `Unique>` to implement `Sync` +note: required because it appears within the type `Box>` + --> $RUST/alloc/src/boxed.rs + | + | pub struct Box< + | ^^^ +note: required because it appears within the type `Pin>>` + --> $RUST/core/src/pin.rs + | + | pub struct Pin

{ + | ^^^ +note: required by a bound in `assert_is_sync` + --> tests/ui/arg_not_sync.rs:3:27 + | +3 | fn assert_is_sync(_: impl Sync) {} + | ^^^^ required by this bound in `assert_is_sync` diff --git a/tests/ui/args_invalid.rs b/tests/ui/args_invalid.rs new file mode 100644 index 0000000..cda8381 --- /dev/null +++ b/tests/ui/args_invalid.rs @@ -0,0 +1,14 @@ +use async_recursion::async_recursion; + +#[async_recursion(?Sync)] +async fn not_sync() {} + +#[async_recursion(Sync Sync)] +async fn not_punctuated() {} + +#[async_recursion(Sync?Send)] +async fn what_even_is_this() {} + + + +fn main() {} \ No newline at end of file diff --git a/tests/ui/args_invalid.stderr b/tests/ui/args_invalid.stderr new file mode 100644 index 0000000..b532a51 --- /dev/null +++ b/tests/ui/args_invalid.stderr @@ -0,0 +1,17 @@ +error: failed to parse macro arguments: expected `Send` + --> tests/ui/args_invalid.rs:3:20 + | +3 | #[async_recursion(?Sync)] + | ^^^^ + +error: failed to parse macro arguments: expected `,` + --> tests/ui/args_invalid.rs:6:24 + | +6 | #[async_recursion(Sync Sync)] + | ^^^^ + +error: failed to parse macro arguments: expected `,` + --> tests/ui/args_invalid.rs:9:23 + | +9 | #[async_recursion(Sync?Send)] + | ^ diff --git a/tests/ui/args_repeated.rs b/tests/ui/args_repeated.rs new file mode 100644 index 0000000..8d980c3 --- /dev/null +++ b/tests/ui/args_repeated.rs @@ -0,0 +1,12 @@ +use async_recursion::async_recursion; + +#[async_recursion(?Send, ?Send)] +async fn repeated_args_1() {} + +#[async_recursion(?Send, Sync, ?Send)] +async fn repeated_args_2() {} + +#[async_recursion(Sync, ?Send, Sync, ?Send)] +async fn repeated_args_3() {} + +fn main() {} \ No newline at end of file diff --git a/tests/ui/args_repeated.stderr b/tests/ui/args_repeated.stderr new file mode 100644 index 0000000..b14edfb --- /dev/null +++ b/tests/ui/args_repeated.stderr @@ -0,0 +1,23 @@ +error: received duplicate argument: `?Send` + --> tests/ui/args_repeated.rs:3:1 + | +3 | #[async_recursion(?Send, ?Send)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in the attribute macro `async_recursion` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: received too many arguments + --> tests/ui/args_repeated.rs:6:1 + | +6 | #[async_recursion(?Send, Sync, ?Send)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in the attribute macro `async_recursion` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: received too many arguments + --> tests/ui/args_repeated.rs:9:1 + | +9 | #[async_recursion(Sync, ?Send, Sync, ?Send)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in the attribute macro `async_recursion` (in Nightly builds, run with -Z macro-backtrace for more info)