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/arguments.rs b/macros/src/arguments.rs new file mode 100644 index 0000000..3de71c6 --- /dev/null +++ b/macros/src/arguments.rs @@ -0,0 +1,95 @@ +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: TokenStream2, +} + +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, + }) + } +} + +#[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 TokenStream2> { + 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..6d328c4 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,14 +9,16 @@ 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( input: &mut Item, is_async: bool, - alt_sig: Option, + args: &AsyncGenericAttributeArgs, ) -> TokenStream2 { let item = &mut input.0; @@ -25,76 +27,142 @@ 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) } +/// 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 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 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, async_signature); + let async_tokens = convert_sync_async(&mut item, true, &args); let mut tokens = sync_tokens; tokens.extend(async_tokens); 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/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) + | ^^^^ 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)); +}