Skip to content

Commit c329d0d

Browse files
committed
Add Sync support
1 parent f01f486 commit c329d0d

21 files changed

+343
-28
lines changed

.github/workflows/ci.yml

+2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ jobs:
2020
steps:
2121
- uses: actions/checkout@v3
2222
- uses: dtolnay/rust-toolchain@stable
23+
with:
24+
components: rust-src # required for consistent error messages
2325
- run: cargo install cargo-expand
2426
- run: cargo test --verbose
2527

README.md

+18-8
Original file line numberDiff line numberDiff line change
@@ -52,25 +52,35 @@ async fn fib(n : u32) -> u32 {
5252
}
5353
```
5454

55-
## ?Send Option
55+
## ?Send option
5656

57-
The returned future has a `Send` bound to make sure it can be sent between threads.
57+
The returned `Future` has a `Send` bound to make sure it can be sent between threads.
5858
If this is undesirable you can mark that the bound should be left out like so:
5959

6060
```rust
6161
#[async_recursion(?Send)]
62-
async fn example() {
62+
async fn returned_future_is_not_send() {
6363
// ...
6464
}
6565
```
6666

67-
In detail:
67+
## Sync option
68+
69+
The returned `Future` doesn't have a `Sync` bound as it is usually not required.
70+
You can include a `Sync` bound as follows:
71+
72+
```rust
73+
#[async_recursion(Sync)]
74+
async fn returned_future_is_sync() {
75+
// ...
76+
}
77+
```
6878

69-
- `#[async_recursion]` modifies your function to return a [`BoxFuture`], and
70-
- `#[async_recursion(?Send)]` modifies your function to return a [`LocalBoxFuture`].
79+
In detail:
7180

72-
[`BoxFuture`]: https://docs.rs/futures/0.3.19/futures/future/type.BoxFuture.html
73-
[`LocalBoxFuture`]: https://docs.rs/futures/0.3.19/futures/future/type.LocalBoxFuture.html
81+
- `#[async_recursion]` modifies your function to return a boxed `Future` with a `Send` bound.
82+
- `#[async_recursion(?Send)]` modifies your function to return a boxed `Future` _without_ a `Send` bound.
83+
- `#[async_recursion(Sync)]` modifies your function to return a boxed `Future` with a `Send` and `Sync` bound.
7484

7585
### License
7686

src/expand.rs

+7-1
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,12 @@ fn transform_sig(sig: &mut Signature, args: &RecursionArgs) {
173173
quote!()
174174
};
175175

176+
let sync_bound: TokenStream = if args.sync_bound {
177+
quote!(+ ::core::marker::Sync)
178+
} else {
179+
quote!()
180+
};
181+
176182
let where_clause = sig
177183
.generics
178184
.where_clause
@@ -196,6 +202,6 @@ fn transform_sig(sig: &mut Signature, args: &RecursionArgs) {
196202
// Modify the return type
197203
sig.output = parse_quote! {
198204
-> ::core::pin::Pin<Box<
199-
dyn ::core::future::Future<Output = #ret> #box_lifetime #send_bound >>
205+
dyn ::core::future::Future<Output = #ret> #box_lifetime #send_bound #sync_bound>>
200206
};
201207
}

src/lib.rs

+20-7
Original file line numberDiff line numberDiff line change
@@ -52,27 +52,40 @@
5252
//! }
5353
//! ```
5454
//!
55-
//! ## ?Send Option
55+
//! ## ?Send option
5656
//!
57-
//! The returned future has a [`Send`] bound to make sure it can be sent between threads.
57+
//! The returned [`Future`] has a [`Send`] bound to make sure it can be sent between threads.
5858
//! If this is undesirable you can mark that the bound should be left out like so:
5959
//!
6060
//! ```rust
6161
//! # use async_recursion::async_recursion;
6262
//!
6363
//! #[async_recursion(?Send)]
64-
//! async fn example() {
64+
//! async fn returned_future_is_not_send() {
65+
//! // ...
66+
//! }
67+
//! ```
68+
//!
69+
//! ## Sync option
70+
//!
71+
//! The returned [`Future`] doesn't have a [`Sync`] bound as it is usually not required.
72+
//! You can include a [`Sync`] bound as follows:
73+
//!
74+
//! ```rust
75+
//! # use async_recursion::async_recursion;
76+
//!
77+
//! #[async_recursion(Sync)]
78+
//! async fn returned_future_is_send_and_sync() {
6579
//! // ...
6680
//! }
6781
//! ```
6882
//!
6983
//! In detail:
7084
//!
71-
//! - `#[async_recursion]` modifies your function to return a [`BoxFuture`], and
72-
//! - `#[async_recursion(?Send)]` modifies your function to return a [`LocalBoxFuture`].
7385
//!
74-
//! [`BoxFuture`]: https://docs.rs/futures/0.3.19/futures/future/type.BoxFuture.html
75-
//! [`LocalBoxFuture`]: https://docs.rs/futures/0.3.19/futures/future/type.LocalBoxFuture.html
86+
//! - `#[async_recursion]` modifies your function to return a boxed [`Future`] with a [`Send`] bound.
87+
//! - `#[async_recursion(?Send)]` modifies your function to return a boxed [`Future`] _without_ a [`Send`] bound.
88+
//! - `#[async_recursion(Sync)]` modifies your function to return a boxed [`Future`] with [`Send`] and [`Sync`] bounds.
7689
//!
7790
//! ### License
7891
//!

src/parse.rs

+56-12
Original file line numberDiff line numberDiff line change
@@ -22,30 +22,74 @@ impl Parse for AsyncItem {
2222

2323
pub struct RecursionArgs {
2424
pub send_bound: bool,
25-
}
26-
27-
impl Default for RecursionArgs {
28-
fn default() -> Self {
29-
RecursionArgs { send_bound: true }
30-
}
25+
pub sync_bound: bool,
3126
}
3227

3328
/// Custom keywords for parser
3429
mod kw {
3530
syn::custom_keyword!(Send);
31+
syn::custom_keyword!(Sync);
3632
}
3733

38-
impl Parse for RecursionArgs {
34+
#[derive(Debug, PartialEq, Eq)]
35+
enum Arg {
36+
NotSend,
37+
Sync,
38+
}
39+
40+
impl std::fmt::Display for Arg {
41+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42+
match self {
43+
Self::NotSend => write!(f, "?Send"),
44+
Self::Sync => write!(f, "Sync"),
45+
}
46+
}
47+
}
48+
49+
impl Parse for Arg {
3950
fn parse(input: ParseStream) -> Result<Self> {
40-
// Check for the `?Send` option
4151
if input.peek(Token![?]) {
4252
input.parse::<Question>()?;
4353
input.parse::<kw::Send>()?;
44-
Ok(Self { send_bound: false })
45-
} else if !input.is_empty() {
46-
Err(input.error("expected `?Send` or empty"))
54+
Ok(Arg::NotSend)
4755
} else {
48-
Ok(Self::default())
56+
input.parse::<kw::Sync>()?;
57+
Ok(Arg::Sync)
4958
}
5059
}
5160
}
61+
62+
impl Parse for RecursionArgs {
63+
fn parse(input: ParseStream) -> Result<Self> {
64+
let mut send_bound: bool = true;
65+
let mut sync_bound: bool = false;
66+
67+
let args_parsed: Vec<Arg> =
68+
syn::punctuated::Punctuated::<Arg, syn::Token![,]>::parse_terminated(input)
69+
.map_err(|e| input.error(format!("failed to parse macro arguments: {e}")))?
70+
.into_iter()
71+
.collect();
72+
73+
// Avoid sloppy input
74+
if args_parsed.len() > 2 {
75+
return Err(Error::new(Span::call_site(), "received too many arguments"));
76+
} else if args_parsed.len() == 2 && args_parsed[0] == args_parsed[1] {
77+
return Err(Error::new(
78+
Span::call_site(),
79+
format!("received duplicate argument: `{}`", args_parsed[0]),
80+
));
81+
}
82+
83+
for arg in args_parsed {
84+
match arg {
85+
Arg::NotSend => send_bound = false,
86+
Arg::Sync => sync_bound = true,
87+
}
88+
}
89+
90+
Ok(Self {
91+
send_bound,
92+
sync_bound,
93+
})
94+
}
95+
}

tests/args_sync.rs

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
use async_recursion::async_recursion;
2+
3+
#[async_recursion(Sync)]
4+
async fn send_and_sync() {}
5+
6+
fn assert_is_send_and_sync(_: impl Send + Sync) {}
7+
8+
#[test]
9+
fn test_sync_argument() {
10+
assert_is_send_and_sync(send_and_sync());
11+
}
+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
use async_recursion::async_recursion;
2+
#[must_use]
3+
fn no_send_bound() -> ::core::pin::Pin<Box<dyn ::core::future::Future<Output = ()>>> {
4+
Box::pin(async move {})
5+
}

tests/expand/args_not_send.rs

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
use async_recursion::async_recursion;
2+
3+
#[async_recursion(?Send)]
4+
async fn no_send_bound() {}
+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
use async_recursion::async_recursion;
2+
#[must_use]
3+
fn not_send_sync_1() -> ::core::pin::Pin<
4+
Box<dyn ::core::future::Future<Output = ()> + ::core::marker::Sync>,
5+
> {
6+
Box::pin(async move {})
7+
}
8+
#[must_use]
9+
fn not_send_sync_2() -> ::core::pin::Pin<
10+
Box<dyn ::core::future::Future<Output = ()> + ::core::marker::Sync>,
11+
> {
12+
Box::pin(async move {})
13+
}
14+
#[must_use]
15+
fn sync_not_send_1() -> ::core::pin::Pin<
16+
Box<dyn ::core::future::Future<Output = ()> + ::core::marker::Sync>,
17+
> {
18+
Box::pin(async move {})
19+
}
20+
#[must_use]
21+
fn sync_not_send_2() -> ::core::pin::Pin<
22+
Box<dyn ::core::future::Future<Output = ()> + ::core::marker::Sync>,
23+
> {
24+
Box::pin(async move {})
25+
}

tests/expand/args_punctuated.rs

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
use async_recursion::async_recursion;
2+
3+
#[async_recursion(?Send, Sync)]
4+
async fn not_send_sync_1() {}
5+
6+
#[async_recursion(?Send,Sync)]
7+
async fn not_send_sync_2() {}
8+
9+
#[async_recursion(Sync, ?Send)]
10+
async fn sync_not_send_1() {}
11+
12+
#[async_recursion(Sync,?Send)]
13+
async fn sync_not_send_2() {}

tests/expand/args_sync.expanded.rs

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
use async_recursion::async_recursion;
2+
#[must_use]
3+
fn sync() -> ::core::pin::Pin<
4+
Box<
5+
dyn ::core::future::Future<
6+
Output = (),
7+
> + ::core::marker::Send + ::core::marker::Sync,
8+
>,
9+
> {
10+
Box::pin(async move {})
11+
}

tests/expand/args_sync.rs

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
use async_recursion::async_recursion;
2+
3+
#[async_recursion(Sync)]
4+
async fn sync() {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
use async_recursion::async_recursion;
2+
#[must_use]
3+
fn explicit_async_recursion_bound<'life0, 'life1, 'async_recursion>(
4+
t: &'life0 T,
5+
p: &'life1 [String],
6+
prefix: Option<&'async_recursion [u8]>,
7+
layer: Option<&'async_recursion [u8]>,
8+
) -> ::core::pin::Pin<
9+
Box<
10+
dyn ::core::future::Future<Output = ()> + 'async_recursion + ::core::marker::Send,
11+
>,
12+
>
13+
where
14+
'life0: 'async_recursion,
15+
'life1: 'async_recursion,
16+
'async_recursion: 'async_recursion,
17+
{
18+
Box::pin(async move {})
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Test that an explicit `async_recursion bound is left alone.
2+
// This is a workaround many
3+
use async_recursion::async_recursion;
4+
5+
6+
#[async_recursion]
7+
async fn explicit_async_recursion_bound(
8+
t: &T,
9+
p: &[String],
10+
prefix: Option<&'async_recursion [u8]>,
11+
layer: Option<&'async_recursion [u8]>,
12+
) {}

tests/lifetimes.rs

+4
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ async fn count_down(foo: Option<&str>) -> i32 {
4141
0
4242
}
4343

44+
#[async_recursion]
45+
async fn explicit_async_recursion_bound(_: Option<&'async_recursion String>) {}
46+
4447
#[test]
4548
fn lifetime_expansion_works() {
4649
block_on(async move {
@@ -73,5 +76,6 @@ fn lifetime_expansion_works() {
7376
assert_eq!(contains_value_2(&12, &node).await, false);
7477

7578
count_down(None).await;
79+
explicit_async_recursion_bound(None).await;
7680
});
7781
}

tests/ui/arg_not_sync.rs

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
use async_recursion::async_recursion;
2+
3+
fn assert_is_sync(_: impl Sync) {}
4+
5+
6+
#[async_recursion]
7+
async fn send_not_sync() {}
8+
9+
#[async_recursion(?Send)]
10+
async fn not_send_not_sync() {}
11+
12+
fn main() {
13+
assert_is_sync(send_not_sync());
14+
assert_is_sync(not_send_not_sync());
15+
}

0 commit comments

Comments
 (0)