From 9661b55faa200323176228e2e30a9c718ea205db Mon Sep 17 00:00:00 2001 From: Peter Goodspeed-Niklaus Date: Thu, 10 Apr 2025 14:38:25 +0200 Subject: [PATCH 1/4] feat: rewrite argument parsing and add new legal arg names Rewriting argument parsing allows us to succinctly parse three different arguments with the same overall shape. We'll need that capability shortly. --- macros/src/arguments.rs | 96 +++++++++++++++++++++++ macros/src/lib.rs | 36 ++------- tests/src/tests/fail/no-macro-args.stderr | 8 +- 3 files changed, 104 insertions(+), 36 deletions(-) create mode 100644 macros/src/arguments.rs diff --git a/macros/src/arguments.rs b/macros/src/arguments.rs new file mode 100644 index 0000000..c41d8ab --- /dev/null +++ b/macros/src/arguments.rs @@ -0,0 +1,96 @@ +use proc_macro::TokenStream; +use proc_macro2::{Ident, TokenStream as TokenStream2}; +use syn::{ + parenthesized, + parse::{Parse, ParseStream, Result}, + token, Error, Token, +}; + +const ASYNC_SIG: &str = "async_signature"; +const SYNC_CFG: &str = "sync_cfg"; +const ASYNC_CFG: &str = "async_cfg"; +const VALID_ARGUMENTS: &[&str] = &[ASYNC_SIG, SYNC_CFG, ASYNC_CFG]; + +/// matches `ident(...)` +struct NamedParenGroup { + name: Ident, + _paren: token::Paren, + contents: TokenStream, +} + +impl Parse for NamedParenGroup { + fn parse(input: ParseStream) -> Result { + let content; + let name = input.parse::()?; + if !VALID_ARGUMENTS.contains(&name.to_string().as_str()) { + return Err(Error::new( + name.span(), + format!( + "invalid argument for async_generic. valid arguments: {}", + VALID_ARGUMENTS.join(", ") + ), + )); + } + let _paren = parenthesized!(content in input); + // For whatever reason, there isn't a convenient shorthand to consume the whole + // rest of a `ParseStream` and store it as a `TokenStream`, so we have to do it manually: + let mut contents = TokenStream2::default(); + content.step(|cursor| { + let mut rest = *cursor; + while let Some((tt, next)) = rest.token_tree() { + contents.extend(std::iter::once(tt)); + rest = next; + } + Ok(((), rest)) + })?; + Ok(Self { + name, + _paren, + contents: contents.into(), + }) + } +} + +#[derive(Default)] +pub struct AsyncGenericAttributeArgs { + pub async_signature: Option, + pub sync_cfg: Option, + pub async_cfg: Option, +} + +impl AsyncGenericAttributeArgs { + /// Associate the argument with one of the legal attributes by name + fn recognize(&mut self, ident: Ident) -> Result<&mut TokenStream> { + let field = match ident.to_string().as_str() { + ASYNC_SIG => &mut self.async_signature, + SYNC_CFG => &mut self.sync_cfg, + ASYNC_CFG => &mut self.async_cfg, + _ => { + return Err(syn::Error::new( + ident.span(), + "unrecognized async_generic argument", + )) + } + }; + if field.is_some() { + return Err(syn::Error::new( + ident.span(), + "duplicate async_generic argument", + )); + } + Ok(field.insert(Default::default())) + } +} + +impl Parse for AsyncGenericAttributeArgs { + fn parse(input: ParseStream) -> Result { + let mut args = Self::default(); + + for named_group in input.parse_terminated(NamedParenGroup::parse, Token![,])? { + let field = args.recognize(named_group.name)?; + *field = named_group.contents; + } + + Ok(args) + } +} diff --git a/macros/src/lib.rs b/macros/src/lib.rs index 305f1fd..2235acd 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -1,7 +1,7 @@ #![deny(warnings)] #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg, doc_cfg_hide))] -use proc_macro::{TokenStream, TokenTree}; +use proc_macro::TokenStream; use proc_macro2::{Ident, Span, TokenStream as TokenStream2, TokenTree as TokenTree2}; use quote::quote; use syn::{ @@ -9,8 +9,10 @@ use syn::{ parse_macro_input, Attribute, Error, ItemFn, Token, }; +use crate::arguments::AsyncGenericAttributeArgs; use crate::desugar_if_async::DesugarIfAsync; +mod arguments; mod desugar_if_async; fn convert_sync_async( @@ -59,42 +61,14 @@ fn convert_sync_async( #[proc_macro_attribute] pub fn async_generic(args: TokenStream, input: TokenStream) -> TokenStream { - let mut async_signature: Option = None; - - if !args.to_string().is_empty() { - let mut atokens = args.into_iter(); - loop { - if let Some(TokenTree::Ident(i)) = atokens.next() { - if i.to_string() != *"async_signature" { - break; - } - } else { - break; - } - - if let Some(TokenTree::Group(g)) = atokens.next() { - if atokens.next().is_none() && g.delimiter() == proc_macro::Delimiter::Parenthesis { - async_signature = Some(g.stream()); - } - } - } - - if async_signature.is_none() { - return syn::Error::new( - Span::call_site(), - "async_generic can only take a async_signature argument", - ) - .to_compile_error() - .into(); - } - }; + let args = parse_macro_input!(args as AsyncGenericAttributeArgs); let input_clone = input.clone(); let mut item = parse_macro_input!(input_clone as Item); let sync_tokens = convert_sync_async(&mut item, false, None); let mut item = parse_macro_input!(input as Item); - let async_tokens = convert_sync_async(&mut item, true, async_signature); + let async_tokens = convert_sync_async(&mut item, true, args.async_signature); let mut tokens = sync_tokens; tokens.extend(async_tokens); diff --git a/tests/src/tests/fail/no-macro-args.stderr b/tests/src/tests/fail/no-macro-args.stderr index 529c3e5..dbfd637 100644 --- a/tests/src/tests/fail/no-macro-args.stderr +++ b/tests/src/tests/fail/no-macro-args.stderr @@ -1,7 +1,5 @@ -error: async_generic can only take a async_signature argument - --> src/tests/fail/no-macro-args.rs:3:1 +error: invalid argument for async_generic. valid arguments: async_signature, sync_cfg, async_cfg + --> src/tests/fail/no-macro-args.rs:3:17 | 3 | #[async_generic(Send)] - | ^^^^^^^^^^^^^^^^^^^^^^ - | - = note: this error originates in the attribute macro `async_generic` (in Nightly builds, run with -Z macro-backtrace for more info) + | ^^^^ From 34dd273f924c1f0a403c935712bc018f1cd2330a Mon Sep 17 00:00:00 2001 From: Peter Goodspeed-Niklaus Date: Thu, 10 Apr 2025 15:56:06 +0200 Subject: [PATCH 2/4] feat: implement `sync_cfg` and `async_cfg` When the `#[async_generic]` macro has a `sync_cfg(...)` argument, the parameters to that argument are passed into a `#[cfg(...)]` block for the sync version of the function. When the `#[async_generic]` macro has an `async_cfg(...)` argument, the parameters to that argument are passed into a `#[cfg(...)]` block for the async version of that function. Together, this gives users complete control over which versions of the functions are compiled, when. --- macros/src/arguments.rs | 13 ++++----- macros/src/lib.rs | 64 +++++++++++++++++++++++------------------ 2 files changed, 42 insertions(+), 35 deletions(-) diff --git a/macros/src/arguments.rs b/macros/src/arguments.rs index c41d8ab..3de71c6 100644 --- a/macros/src/arguments.rs +++ b/macros/src/arguments.rs @@ -1,4 +1,3 @@ -use proc_macro::TokenStream; use proc_macro2::{Ident, TokenStream as TokenStream2}; use syn::{ parenthesized, @@ -15,7 +14,7 @@ const VALID_ARGUMENTS: &[&str] = &[ASYNC_SIG, SYNC_CFG, ASYNC_CFG]; struct NamedParenGroup { name: Ident, _paren: token::Paren, - contents: TokenStream, + contents: TokenStream2, } impl Parse for NamedParenGroup { @@ -46,21 +45,21 @@ impl Parse for NamedParenGroup { Ok(Self { name, _paren, - contents: contents.into(), + contents, }) } } #[derive(Default)] pub struct AsyncGenericAttributeArgs { - pub async_signature: Option, - pub sync_cfg: Option, - pub async_cfg: Option, + pub async_signature: Option, + pub sync_cfg: Option, + pub async_cfg: Option, } impl AsyncGenericAttributeArgs { /// Associate the argument with one of the legal attributes by name - fn recognize(&mut self, ident: Ident) -> Result<&mut TokenStream> { + fn recognize(&mut self, ident: Ident) -> Result<&mut TokenStream2> { let field = match ident.to_string().as_str() { ASYNC_SIG => &mut self.async_signature, SYNC_CFG => &mut self.sync_cfg, diff --git a/macros/src/lib.rs b/macros/src/lib.rs index 2235acd..af368e3 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -18,7 +18,7 @@ mod desugar_if_async; fn convert_sync_async( input: &mut Item, is_async: bool, - alt_sig: Option, + args: &AsyncGenericAttributeArgs, ) -> TokenStream2 { let item = &mut input.0; @@ -27,34 +27,42 @@ fn convert_sync_async( item.sig.ident = Ident::new(&format!("{}_async", item.sig.ident), Span::call_site()); } - let tokens = quote!(#item); - - let tokens = if let Some(alt_sig) = alt_sig { - let mut found_fn = false; - let mut found_args = false; + let cfg_attr = match (is_async, args.sync_cfg.as_ref(), args.async_cfg.as_ref()) { + (false, Some(sync_cfg), _) => quote! { #[cfg(#sync_cfg)] }, + (true, _, Some(async_cfg)) => quote! { #[cfg(#async_cfg)] }, + _ => Default::default(), + }; + let mut tokens = quote!(#cfg_attr #item); - let old_tokens = tokens.into_iter().map(|token| match &token { - TokenTree2::Ident(i) => { - found_fn = found_fn || &i.to_string() == "fn"; - token - } - TokenTree2::Group(g) => { - if found_fn && !found_args && g.delimiter() == proc_macro2::Delimiter::Parenthesis { - found_args = true; - return TokenTree2::Group(proc_macro2::Group::new( - proc_macro2::Delimiter::Parenthesis, - alt_sig.clone().into(), - )); + if is_async { + if let Some(alt_sig) = args.async_signature.as_ref() { + let mut found_fn = false; + let mut found_args = false; + + let old_tokens = tokens.into_iter().map(|token| match &token { + TokenTree2::Ident(i) => { + found_fn = found_fn || &i.to_string() == "fn"; + token } - token - } - _ => token, - }); + TokenTree2::Group(g) => { + if found_fn + && !found_args + && g.delimiter() == proc_macro2::Delimiter::Parenthesis + { + found_args = true; + return TokenTree2::Group(proc_macro2::Group::new( + proc_macro2::Delimiter::Parenthesis, + alt_sig.to_owned().into(), + )); + } + token + } + _ => token, + }); - TokenStream2::from_iter(old_tokens) - } else { - tokens - }; + tokens = TokenStream2::from_iter(old_tokens); + } + } DesugarIfAsync { is_async }.desugar_if_async(tokens) } @@ -65,10 +73,10 @@ pub fn async_generic(args: TokenStream, input: TokenStream) -> TokenStream { let input_clone = input.clone(); let mut item = parse_macro_input!(input_clone as Item); - let sync_tokens = convert_sync_async(&mut item, false, None); + let sync_tokens = convert_sync_async(&mut item, false, &args); let mut item = parse_macro_input!(input as Item); - let async_tokens = convert_sync_async(&mut item, true, args.async_signature); + let async_tokens = convert_sync_async(&mut item, true, &args); let mut tokens = sync_tokens; tokens.extend(async_tokens); From 032ee4c38804c499ad3aa95a9c91d5f4f81dbfe2 Mon Sep 17 00:00:00 2001 From: Peter Goodspeed-Niklaus Date: Thu, 10 Apr 2025 15:59:14 +0200 Subject: [PATCH 3/4] test: add tests demonstrating correctness of `sync_cfg`, `async_cfg` One test each (the passing tests) demonstrate that items which should be retained are in fact retained. One test each (the failing tests) demonstrate that items which should be omitted are in fact omitted. --- tests/src/lib.rs | 4 +++ tests/src/tests/fail/async-only.rs | 35 ++++++++++++++++++++++++++ tests/src/tests/fail/async-only.stderr | 11 ++++++++ tests/src/tests/fail/sync-only.rs | 35 ++++++++++++++++++++++++++ tests/src/tests/fail/sync-only.stderr | 5 ++++ tests/src/tests/pass/async-only.rs | 25 ++++++++++++++++++ tests/src/tests/pass/sync-only.rs | 25 ++++++++++++++++++ 7 files changed, 140 insertions(+) create mode 100644 tests/src/tests/fail/async-only.rs create mode 100644 tests/src/tests/fail/async-only.stderr create mode 100644 tests/src/tests/fail/sync-only.rs create mode 100644 tests/src/tests/fail/sync-only.stderr create mode 100644 tests/src/tests/pass/async-only.rs create mode 100644 tests/src/tests/pass/sync-only.rs diff --git a/tests/src/lib.rs b/tests/src/lib.rs index bd0c596..85e3d7f 100644 --- a/tests/src/lib.rs +++ b/tests/src/lib.rs @@ -5,6 +5,8 @@ fn tests() { t.pass("src/tests/pass/generic-fn.rs"); t.pass("src/tests/pass/generic-fn-with-visibility.rs"); t.pass("src/tests/pass/struct-method-generic.rs"); + t.pass("src/tests/pass/async-only.rs"); + t.pass("src/tests/pass/sync-only.rs"); t.compile_fail("src/tests/fail/misuse-of-underscore-async.rs"); t.compile_fail("src/tests/fail/no-async-fn.rs"); @@ -12,4 +14,6 @@ fn tests() { t.compile_fail("src/tests/fail/no-macro-args.rs"); t.compile_fail("src/tests/fail/no-struct.rs"); t.compile_fail("src/tests/fail/no-trait.rs"); + t.compile_fail("src/tests/fail/sync-only.rs"); + t.compile_fail("src/tests/fail/async-only.rs"); } diff --git a/tests/src/tests/fail/async-only.rs b/tests/src/tests/fail/async-only.rs new file mode 100644 index 0000000..ba9a749 --- /dev/null +++ b/tests/src/tests/fail/async-only.rs @@ -0,0 +1,35 @@ +use async_generic::async_generic; + +#[async_generic(sync_cfg(target_family = "wasm"), async_signature(thing: &AsyncThing))] +fn do_stuff(thing: &SyncThing) -> String { + if _async { + thing.do_stuff().await + } else { + thing.do_stuff() + } +} + +struct SyncThing {} + +impl SyncThing { + fn do_stuff(&self) -> String { + "sync".to_owned() + } +} + +struct AsyncThing {} + +impl AsyncThing { + async fn do_stuff(&self) -> String { + "async".to_owned() + } +} + +#[async_std::main] +async fn main() { + let st = SyncThing {}; + let at = AsyncThing {}; + + println!("sync => {}", do_stuff(&st)); + println!("async => {}", do_stuff_async(&at).await); +} diff --git a/tests/src/tests/fail/async-only.stderr b/tests/src/tests/fail/async-only.stderr new file mode 100644 index 0000000..8c040fb --- /dev/null +++ b/tests/src/tests/fail/async-only.stderr @@ -0,0 +1,11 @@ +error[E0425]: cannot find function `do_stuff` in this scope + --> src/tests/fail/async-only.rs:33:28 + | +33 | println!("sync => {}", do_stuff(&st)); + | ^^^^^^^^ not found in this scope + | +help: use the `.` operator to call the method `do_stuff` on `&SyncThing` + | +33 - println!("sync => {}", do_stuff(&st)); +33 + println!("sync => {}", (&st).do_stuff()); + | diff --git a/tests/src/tests/fail/sync-only.rs b/tests/src/tests/fail/sync-only.rs new file mode 100644 index 0000000..8d05554 --- /dev/null +++ b/tests/src/tests/fail/sync-only.rs @@ -0,0 +1,35 @@ +use async_generic::async_generic; + +#[async_generic(async_cfg(target_family = "wasm"))] +fn do_stuff(thing: &SyncThing) -> String { + if _async { + thing.do_stuff().await + } else { + thing.do_stuff() + } +} + +struct SyncThing {} + +impl SyncThing { + fn do_stuff(&self) -> String { + "sync".to_owned() + } +} + +struct AsyncThing {} + +impl AsyncThing { + async fn do_stuff(&self) -> String { + "async".to_owned() + } +} + +#[async_std::main] +async fn main() { + let st = SyncThing {}; + let at = AsyncThing {}; + + println!("sync => {}", do_stuff(&st)); + println!("async => {}", do_stuff_async(&at).await); +} diff --git a/tests/src/tests/fail/sync-only.stderr b/tests/src/tests/fail/sync-only.stderr new file mode 100644 index 0000000..1cff1e5 --- /dev/null +++ b/tests/src/tests/fail/sync-only.stderr @@ -0,0 +1,5 @@ +error[E0425]: cannot find function `do_stuff_async` in this scope + --> src/tests/fail/sync-only.rs:34:29 + | +34 | println!("async => {}", do_stuff_async(&at).await); + | ^^^^^^^^^^^^^^ not found in this scope diff --git a/tests/src/tests/pass/async-only.rs b/tests/src/tests/pass/async-only.rs new file mode 100644 index 0000000..4ecdab1 --- /dev/null +++ b/tests/src/tests/pass/async-only.rs @@ -0,0 +1,25 @@ +use async_generic::async_generic; + +#[async_generic(sync_cfg(target_family = "wasm"))] +fn do_stuff(thing: &AsyncThing) -> String { + if _async { + thing.do_stuff().await + } else { + thing.do_stuff() + } +} + +struct AsyncThing {} + +impl AsyncThing { + async fn do_stuff(&self) -> String { + "async".to_owned() + } +} + +#[async_std::main] +async fn main() { + let at = AsyncThing {}; + + println!("async => {}", do_stuff_async(&at).await); +} diff --git a/tests/src/tests/pass/sync-only.rs b/tests/src/tests/pass/sync-only.rs new file mode 100644 index 0000000..874856e --- /dev/null +++ b/tests/src/tests/pass/sync-only.rs @@ -0,0 +1,25 @@ +use async_generic::async_generic; + +#[async_generic(async_cfg(target_family = "wasm"))] +fn do_stuff(thing: &SyncThing) -> String { + if _async { + thing.do_stuff().await + } else { + thing.do_stuff() + } +} + +struct SyncThing {} + +impl SyncThing { + fn do_stuff(&self) -> String { + "async".to_owned() + } +} + +#[async_std::main] +async fn main() { + let st = SyncThing {}; + + println!("async => {}", do_stuff(&st)); +} From 7914c663b8d71772a5c046b355fd04a8294d9b9b Mon Sep 17 00:00:00 2001 From: Peter Goodspeed-Niklaus Date: Thu, 10 Apr 2025 16:46:51 +0200 Subject: [PATCH 4/4] docs: add documentation for the optional attribute arguments --- README.md | 30 ++++++++++++++++- macros/src/lib.rs | 86 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6fe67b6..e02cf88 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,6 @@ The `async_generic` crate introduces a single proc macro also named `async_gener The macro outputs _two_ versions of the function, one synchronous and one that's async. The functions are identical to each other, except as follows: * When writing the async flavor of the function, the macro inserts the `async` modifier for you and renames the function (to avoid a name collision) by adding an `_async` suffix to the existing function name. -* The attribute macro _may_ contain an `async_signature` argument. If that exists, the async function's argument parameters are replaced. (See example below.) * You can write `if _sync` or `if _async` blocks inside this block. The contents of these blocks will only appear in the corresponding sync or async flavors of the functions. You _may_ specify an `else` clause, which will only appear in the opposite flavor of the function. You may not combine `_sync` or `_async` with any other expression. (These aren't _really_ variables in the function scope, and they will cause "undefined identifier" errors if you try that.) A simple example: @@ -50,6 +49,10 @@ async fn main() { } ``` +### Signature Modification + +The attribute macro may contain an `async_signature` argument. If that exists, the async function's argument parameters are replaced. + An example with different function arguments in the sync and async flavors: ```rust @@ -91,6 +94,31 @@ async fn main() { } ``` + +### Conditional compilation + +The attribute macro may contain a `sync_cfg` or `async_cfg` argument. If either of these are set, the relevant expansion function is annotated with a `#[cfg()]` attribute. For example, + +```rust +#[async_generic(sync_cfg(feature = "sync"), async_cfg(feature = "async"))] +fn do_stuff(thing: &Thing) -> String { + todo!() +} +``` + +This will be transformed into conditionally compiled functions as follows: + +```rust +#[cfg(feature = "sync")] +fn do_stuff(thing: &Thing) -> String { + todo!() +} +#[cfg(feature = "async")] +async fn do_stuff_async(thing: &Thing) -> String { + todo!() +} +``` + ## Why not use `maybe-async`? This crate is loosely derived from the excellent work of the [`maybe-async`](https://crates.io/crates/maybe-async) crate, but is intended to solve a subtly different problem. diff --git a/macros/src/lib.rs b/macros/src/lib.rs index af368e3..6d328c4 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -67,6 +67,92 @@ fn convert_sync_async( DesugarIfAsync { is_async }.desugar_if_async(tokens) } +/// Produce both a sync and an async version of this function. +/// +/// The async version of this function has `_async` appended to its name. +/// +/// ## Arguments +/// +/// This macro optionally accepts certain arguments, as follows. +/// +/// ### `async_signature()` +/// +/// Calling this function with an `async_signature` argument replaces its parameter list with the specified parameters. +/// +/// For example: +/// +/// ```rust +/// # use async_generic::async_generic; +/// # struct SyncThing; +/// # struct AsyncThing; +/// #[async_generic(async_signature(thing: &AsyncThing))] +/// fn do_stuff(thing: &SyncThing) -> String { +/// todo!() +/// } +/// ``` +/// +/// Expands to these functions: +/// +/// ```rust +/// # struct SyncThing; +/// # struct AsyncThing; +/// fn do_stuff(thing: &SyncThing) -> String { +/// todo!() +/// } +/// async fn do_stuff_async(thing: &AsyncThing) -> String { +/// todo!() +/// } +/// ``` +/// +/// ### `sync_cfg()` +/// +/// Calling this function with a `sync_cfg` argument adds a conditional compilation marker to the sync version of the emitted function. +/// +/// For example: +/// +/// ```rust,ignore +/// #[async_generic(sync_cfg(any(test, feature = "sync")))] +/// fn do_stuff(thing: &Thing) { +/// todo!() +/// } +/// ``` +/// +/// Expands to these functions +/// +/// ```rust,ignore +/// #[cfg(any(test, feature = "sync"))] +/// fn do_stuff(thing: &Thing) -> String { +/// todo!() +/// } +/// async fn do_stuff_async(thing: &Thing) -> String { +/// todo!() +/// } +/// ``` +/// +/// ### `async_cfg()` +/// +/// Calling this function with an `async_cfg` argument adds a conditional compilation marker to the asycn version of the emitted function. +/// +/// For examples: +/// +/// ```rust,ignore +/// #[async_generic(async_cfg(feature = "async"))] +/// fn do_stuff(thing: &Thing) { +/// todo!() +/// } +/// ``` +/// +/// Expands to these functions +/// +/// ```rust,ignore +/// fn do_stuff(thing: &Thing) -> String { +/// todo!() +/// } +/// #[cfg(any(test, feature = "async"))] +/// async fn do_stuff_async(thing: &Thing) -> String { +/// todo!() +/// } +/// ``` #[proc_macro_attribute] pub fn async_generic(args: TokenStream, input: TokenStream) -> TokenStream { let args = parse_macro_input!(args as AsyncGenericAttributeArgs);