aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/func.rs27
-rw-r--r--src/invoke.rs13
-rw-r--r--src/lib.rs61
-rw-r--r--src/select.rs303
4 files changed, 366 insertions, 38 deletions
diff --git a/src/func.rs b/src/func.rs
index 33deb1d..2407f3e 100644
--- a/src/func.rs
+++ b/src/func.rs
@@ -4,32 +4,7 @@ use syn::{ItemFn, parse_macro_input};
use crate::args::FuncArgs;
-/// Attribute macro that generates both a sync and an async version of a function,
-/// gated by a Cargo feature flag.
-///
-/// # Usage
-///
-/// ```ignore
-/// /// Doc comments are preserved
-/// #[might_be_async::func]
-/// pub fn my_function<T: Clone>(arg: T) -> ReturnType
-/// where T: Debug
-/// {
-/// // body — written as a regular (non-async) function
-/// }
-/// ```
-///
-/// Expands to:
-/// - `#[cfg(not(feature = "async"))] fn my_function(...)` — sync version
-/// - `#[cfg(feature = "async")] async fn my_function(...)` — async version
-///
-/// An explicit feature name can be provided:
-///
-/// ```ignore
-/// #[might_be_async::func("tokio_rt")]
-/// pub fn my_function() { ... }
-/// ```
-pub fn func(attr: TokenStream, item: TokenStream) -> TokenStream {
+pub(crate) fn func(attr: TokenStream, item: TokenStream) -> TokenStream {
let args = parse_macro_input!(attr as FuncArgs);
let feature_name = &args.feature_name;
diff --git a/src/invoke.rs b/src/invoke.rs
index 6e79163..4e4a324 100644
--- a/src/invoke.rs
+++ b/src/invoke.rs
@@ -4,18 +4,7 @@ use syn::parse_macro_input;
use crate::args::InvokeInput;
-/// Wraps a call expression, adding `.await` when the async feature is enabled.
-///
-/// # Usage
-///
-/// ```ignore
-/// // Default feature name ("async"):
-/// let result = might_be_async::invoke!(some_async_fn(args));
-///
-/// // Explicit feature name:
-/// let result = might_be_async::invoke!("tokio_rt" => some_async_fn(args));
-/// ```
-pub fn invoke(input: TokenStream) -> TokenStream {
+pub(crate) fn invoke(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as InvokeInput);
let expanded = match input {
diff --git a/src/lib.rs b/src/lib.rs
index 02871bb..7d82100 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -3,13 +3,74 @@ use proc_macro::TokenStream;
mod args;
pub(crate) mod func;
pub(crate) mod invoke;
+pub(crate) mod select;
+/// Attribute macro that generates both a sync and an async version of a function,
+/// gated by a Cargo feature flag.
+///
+/// # Usage
+///
+/// ```ignore
+/// /// Doc comments are preserved
+/// #[might_be_async::func]
+/// pub fn my_function<T: Clone>(arg: T) -> ReturnType
+/// where T: Debug
+/// {
+/// // body — written as a regular (non-async) function
+/// }
+/// ```
+///
+/// Expands to:
+/// - `#[cfg(not(feature = "async"))] fn my_function(...)` — sync version
+/// - `#[cfg(feature = "async")] async fn my_function(...)` — async version
+///
+/// An explicit feature name can be provided:
+///
+/// ```ignore
+/// #[might_be_async::func("tokio_rt")]
+/// pub fn my_function() { ... }
+/// ```
#[proc_macro_attribute]
pub fn func(attr: TokenStream, item: TokenStream) -> TokenStream {
func::func(attr, item)
}
+/// Wraps a call expression, adding `.await` when the async feature is enabled.
+///
+/// # Usage
+///
+/// ```ignore
+/// // Default feature name ("async"):
+/// let result = might_be_async::invoke!(some_async_fn(args));
+///
+/// // Explicit feature name:
+/// let result = might_be_async::invoke!("tokio_rt" => some_async_fn(args));
+/// ```
#[proc_macro]
pub fn invoke(input: TokenStream) -> TokenStream {
invoke::invoke(input)
}
+
+/// Select between sync and async expressions based on a Cargo feature flag.
+///
+/// # Usage
+///
+/// ## Explicit mode (with feature names)
+///
+/// ```ignore
+/// select!["async" => expr_async().await, "sync" => expr_sync()];
+/// select!["async" => { expr_async().await }, "sync" => { expr_sync() }];
+/// select!["async" => expr_async().await, ! => expr_sync()];
+/// select![! => expr_async().await, "sync" => expr_sync()];
+/// ```
+///
+/// ## Implicit mode (auto-detect `.await`)
+///
+/// ```ignore
+/// select![expr_async().await, expr_sync()];
+/// select![{ expr_async().await }, { expr_sync() }];
+/// ```
+#[proc_macro]
+pub fn select(input: TokenStream) -> TokenStream {
+ select::select(input)
+}
diff --git a/src/select.rs b/src/select.rs
new file mode 100644
index 0000000..e309600
--- /dev/null
+++ b/src/select.rs
@@ -0,0 +1,303 @@
+use proc_macro::TokenStream;
+use proc_macro2::{Spacing, TokenStream as TokenStream2, TokenTree};
+use quote::{ToTokens, quote};
+use syn::parse::{Parse, ParseStream};
+use syn::punctuated::Punctuated;
+use syn::{Expr, LitStr, Token, parse_macro_input};
+
+// ─── SelectArm ──────────────────────────────────────────────────────────────────────────
+
+/// A single arm inside `select!`.
+pub enum SelectArm {
+ /// "feat_name" => expr
+ Explicit { feat: LitStr, body: Expr },
+ /// ! => expr
+ Not { body: Expr },
+ /// expr (no feature name — auto-detect by .await)
+ Implicit { body: Expr },
+}
+
+impl Parse for SelectArm {
+ fn parse(input: ParseStream) -> syn::Result<Self> {
+ if input.peek(LitStr) {
+ let feat: LitStr = input.parse()?;
+ input.parse::<Token![=>]>()?;
+ let body: Expr = input.parse()?;
+ Ok(SelectArm::Explicit { feat, body })
+ } else if input.peek(Token![!]) {
+ let _not: Token![!] = input.parse()?;
+ input.parse::<Token![=>]>()?;
+ let body: Expr = input.parse()?;
+ Ok(SelectArm::Not { body })
+ } else {
+ let body: Expr = input.parse()?;
+ Ok(SelectArm::Implicit { body })
+ }
+ }
+}
+
+impl ToTokens for SelectArm {
+ fn to_tokens(&self, tokens: &mut TokenStream2) {
+ match self {
+ SelectArm::Explicit { feat, body } => {
+ feat.to_tokens(tokens);
+ Token![=>].to_tokens(tokens);
+ body.to_tokens(tokens);
+ }
+ SelectArm::Not { body } => {
+ Token![!].to_tokens(tokens);
+ Token![=>].to_tokens(tokens);
+ body.to_tokens(tokens);
+ }
+ SelectArm::Implicit { body } => {
+ body.to_tokens(tokens);
+ }
+ }
+ }
+}
+
+// ─── SelectInput ────────────────────────────────────────────────────────────────────────
+
+struct SelectInput {
+ arms: Vec<SelectArm>,
+}
+
+impl Parse for SelectInput {
+ fn parse(input: ParseStream) -> syn::Result<Self> {
+ let punctuated: Punctuated<SelectArm, Token![,]> = Punctuated::parse_terminated(input)?;
+
+ if punctuated.len() != 2 {
+ return Err(syn::Error::new(
+ input.span(),
+ "select! requires exactly 2 arms",
+ ));
+ }
+
+ let arms: Vec<SelectArm> = punctuated.into_iter().collect();
+ Ok(SelectInput { arms })
+ }
+}
+
+// ─── Select macro ───────────────────────────────────────────────────────────────────────
+
+pub(crate) fn select(input: TokenStream) -> TokenStream {
+ let input = parse_macro_input!(input as SelectInput);
+ let expanded = input.expand();
+ TokenStream::from(expanded)
+}
+
+impl SelectInput {
+ fn expand(&self) -> TokenStream2 {
+ let arm0 = &self.arms[0];
+ let arm1 = &self.arms[1];
+
+ match (arm0, arm1) {
+ // Both explicit with feature names
+ (
+ SelectArm::Explicit { feat: f0, body: b0 },
+ SelectArm::Explicit { feat: f1, body: b1 },
+ ) => {
+ let f0_str = f0.value();
+ let f1_str = f1.value();
+
+ if has_not_prefix(&f0_str) {
+ let inner = &f0_str[1..];
+ quote! {{
+ #[cfg(not(feature = #inner))]
+ { #b0 }
+ #[cfg(feature = #inner)]
+ { #b1 }
+ }}
+ } else if has_not_prefix(&f1_str) {
+ let inner = &f1_str[1..];
+ quote! {{
+ #[cfg(feature = #f0)]
+ { #b0 }
+ #[cfg(not(feature = #f0))]
+ { #b1 }
+ }}
+ } else {
+ quote! {{
+ #[cfg(feature = #f0)]
+ { #b0 }
+ #[cfg(feature = #f1)]
+ { #b1 }
+ }}
+ }
+ }
+
+ // Explicit + Not
+ (SelectArm::Explicit { feat, body }, SelectArm::Not { body: not_body }) => {
+ let feat_str = feat.value();
+ if has_not_prefix(&feat_str) {
+ let inner = &feat_str[1..];
+ quote! {{
+ #[cfg(not(feature = #inner))]
+ { #body }
+ #[cfg(feature = #inner)]
+ { #not_body }
+ }}
+ } else {
+ quote! {{
+ #[cfg(feature = #feat_str)]
+ { #body }
+ #[cfg(not(feature = #feat_str))]
+ { #not_body }
+ }}
+ }
+ }
+ (SelectArm::Not { body: not_body }, SelectArm::Explicit { feat, body }) => {
+ let feat_str = feat.value();
+ if has_not_prefix(&feat_str) {
+ let inner = &feat_str[1..];
+ quote! {{
+ #[cfg(feature = #inner)]
+ { #body }
+ #[cfg(not(feature = #inner))]
+ { #not_body }
+ }}
+ } else {
+ quote! {{
+ #[cfg(not(feature = #feat_str))]
+ { #not_body }
+ #[cfg(feature = #feat_str)]
+ { #body }
+ }}
+ }
+ }
+
+ // Explicit + Implicit
+ (SelectArm::Explicit { feat, body }, SelectArm::Implicit { body: imp_body }) => {
+ let feat_str = feat.value();
+ if has_not_prefix(&feat_str) {
+ let inner = &feat_str[1..];
+ quote! {{
+ #[cfg(not(feature = #inner))]
+ { #body }
+ #[cfg(feature = #inner)]
+ { #imp_body }
+ }}
+ } else {
+ quote! {{
+ #[cfg(feature = #feat_str)]
+ { #body }
+ #[cfg(not(feature = #feat_str))]
+ { #imp_body }
+ }}
+ }
+ }
+ (SelectArm::Implicit { body: imp_body }, SelectArm::Explicit { feat, body }) => {
+ let feat_str = feat.value();
+ if has_not_prefix(&feat_str) {
+ let inner = &feat_str[1..];
+ quote! {{
+ #[cfg(feature = #inner)]
+ { #body }
+ #[cfg(not(feature = #inner))]
+ { #imp_body }
+ }}
+ } else {
+ quote! {{
+ #[cfg(not(feature = #feat_str))]
+ { #imp_body }
+ #[cfg(feature = #feat_str)]
+ { #body }
+ }}
+ }
+ }
+
+ // Both implicit — auto-detect .await
+ (SelectArm::Implicit { body: b0 }, SelectArm::Implicit { body: b1 }) => {
+ let b0_has_await = token_stream_has_await(&b0.to_token_stream());
+ let b1_has_await = token_stream_has_await(&b1.to_token_stream());
+
+ match (b0_has_await, b1_has_await) {
+ (true, false) => {
+ quote! {{
+ #[cfg(feature = "async")]
+ { #b0 }
+ #[cfg(not(feature = "async"))]
+ { #b1 }
+ }}
+ }
+ (false, true) => {
+ quote! {{
+ #[cfg(feature = "async")]
+ { #b1 }
+ #[cfg(not(feature = "async"))]
+ { #b0 }
+ }}
+ }
+ (true, true) => {
+ // Both have .await — use second as sync (strip .await)
+ let b1_stripped = strip_await_from_tokens(&b1.to_token_stream());
+ quote! {{
+ #[cfg(feature = "async")]
+ { #b0 }
+ #[cfg(not(feature = "async"))]
+ { #b1_stripped }
+ }}
+ }
+ (false, false) => {
+ // Neither has .await — use second as async (add .await)
+ quote! {{
+ #[cfg(feature = "async")]
+ { #b0 .await }
+ #[cfg(not(feature = "async"))]
+ { #b1 }
+ }}
+ }
+ }
+ }
+
+ // Two Not arms — doesn't make sense, fallback
+ (SelectArm::Not { body: b0 }, SelectArm::Not { body: b1 }) => {
+ quote! {{
+ #[cfg(feature = "async")]
+ { #b0 }
+ #[cfg(not(feature = "async"))]
+ { #b1 }
+ }}
+ }
+ }
+ }
+}
+
+// ─── Helpers ─────────────────────────────────────────────────────────────────────────────
+
+fn has_not_prefix(s: &str) -> bool {
+ s.starts_with('!')
+}
+
+fn token_stream_has_await(ts: &TokenStream2) -> bool {
+ let mut tokens = ts.clone().into_iter();
+ while let Some(token) = tokens.next() {
+ if let TokenTree::Punct(p) = &token {
+ if p.as_char() == '.' && p.spacing() == Spacing::Alone {
+ if let Some(TokenTree::Ident(ident)) = tokens.next() {
+ if ident == "await" {
+ return true;
+ }
+ }
+ }
+ }
+ }
+ false
+}
+
+fn strip_await_from_tokens(ts: &TokenStream2) -> TokenStream2 {
+ let tokens: Vec<_> = ts.clone().into_iter().collect();
+ let len = tokens.len();
+ if len >= 2 {
+ if let TokenTree::Punct(p) = &tokens[len - 2] {
+ if p.as_char() == '.' {
+ if let TokenTree::Ident(ident) = &tokens[len - 1] {
+ if ident == "await" {
+ return tokens[..len - 2].iter().cloned().collect();
+ }
+ }
+ }
+ }
+ }
+ ts.clone()
+}