Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(<condition>)]` 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.
Expand Down
95 changes: 95 additions & 0 deletions macros/src/arguments.rs
Original file line number Diff line number Diff line change
@@ -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<Self> {
let content;
let name = input.parse::<Ident>()?;
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<TokenStream2>,
pub sync_cfg: Option<TokenStream2>,
pub async_cfg: Option<TokenStream2>,
}

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<Self> {
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)
}
}
184 changes: 126 additions & 58 deletions macros/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
#![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::{
parse::{Parse, ParseStream, Result},
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<TokenStream>,
args: &AsyncGenericAttributeArgs,
) -> TokenStream2 {
let item = &mut input.0;

Expand All @@ -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(<params>)`
///
/// 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(<condition>)`
///
/// 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(<condition>)`
///
/// 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<TokenStream> = 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);
Expand Down
4 changes: 4 additions & 0 deletions tests/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,15 @@ 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");
t.compile_fail("src/tests/fail/no-impl.rs");
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");
}
Loading