aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2026-07-23 08:08:33 +0800
committer魏曹先生 <1992414357@qq.com>2026-07-23 14:01:45 +0800
commit68a652ed2f51d366bb8033497e6dfe545895410e (patch)
treed463833846248410733e196a0939c59cd67ca8a2 /src
feat: scaffold crate structure and implement core macros
Add the project skeleton, LICENSE files, README, Makefile, doc examples, and the initial implementation of `#[func]`, `invoke!`, and `select!` procedural macros.
Diffstat (limited to 'src')
-rw-r--r--src/config.rs38
-rw-r--r--src/func.rs81
-rw-r--r--src/invoke.rs89
-rw-r--r--src/lib.rs105
-rw-r--r--src/select.rs311
5 files changed, 624 insertions, 0 deletions
diff --git a/src/config.rs b/src/config.rs
new file mode 100644
index 0000000..06aa088
--- /dev/null
+++ b/src/config.rs
@@ -0,0 +1,38 @@
+use std::path::Path;
+use std::sync::OnceLock;
+
+/// Returns the default feature name configured in the consuming crate's
+/// `Cargo.toml` under `[package.metadata.might_be_async.default_feature_name]`.
+///
+/// If the metadata key is absent or unreadable, falls back to `"async"`.
+pub(crate) fn default_feature_name() -> &'static str {
+ static DEFAULT: OnceLock<String> = OnceLock::new();
+ DEFAULT.get_or_init(read_default_feature_name)
+}
+
+fn read_default_feature_name() -> String {
+ let manifest_dir = match std::env::var("CARGO_MANIFEST_DIR") {
+ Ok(dir) => dir,
+ Err(_) => return "async".to_string(),
+ };
+
+ let cargo_toml_path = Path::new(&manifest_dir).join("Cargo.toml");
+ let content = match std::fs::read_to_string(cargo_toml_path) {
+ Ok(c) => c,
+ Err(_) => return "async".to_string(),
+ };
+
+ let value: toml::Value = match content.parse() {
+ Ok(v) => v,
+ Err(_) => return "async".to_string(),
+ };
+
+ value
+ .get("package")
+ .and_then(|p| p.get("metadata"))
+ .and_then(|m| m.get("might_be_async"))
+ .and_then(|a| a.get("default_feature_name"))
+ .and_then(|v| v.as_str())
+ .unwrap_or("async")
+ .to_string()
+}
diff --git a/src/func.rs b/src/func.rs
new file mode 100644
index 0000000..5557992
--- /dev/null
+++ b/src/func.rs
@@ -0,0 +1,81 @@
+use crate::SynResult;
+use crate::config::default_feature_name;
+use proc_macro::TokenStream;
+use quote::quote;
+use syn::{
+ ItemFn, LitStr,
+ parse::{Parse, ParseStream},
+ parse_macro_input,
+};
+
+#[doc = include_str!("../doc/args/func.md")]
+pub struct FuncArgs {
+ pub feature_name: String,
+}
+
+impl Default for FuncArgs {
+ fn default() -> Self {
+ FuncArgs {
+ feature_name: default_feature_name().to_string(),
+ }
+ }
+}
+
+impl Parse for FuncArgs {
+ fn parse(input: ParseStream) -> SynResult<Self> {
+ if input.is_empty() {
+ return Ok(FuncArgs::default());
+ }
+
+ // Parse: "feature_name"
+ let feat_lit: LitStr = input.parse()?;
+ let feature_name = feat_lit.value();
+
+ Ok(FuncArgs { feature_name })
+ }
+}
+
+pub(crate) fn func(attr: TokenStream, item: TokenStream) -> TokenStream {
+ let args = parse_macro_input!(attr as FuncArgs);
+ let feature_name = &args.feature_name;
+
+ let input_fn = parse_macro_input!(item as ItemFn);
+ let attrs = &input_fn.attrs;
+ let vis = &input_fn.vis;
+ let sig = &input_fn.sig;
+ let block = &input_fn.block;
+
+ let expanded = quote! {
+ #(#attrs)*
+ #[cfg(not(feature = #feature_name))]
+ #vis #sig #block
+
+ #(#attrs)*
+ #[cfg(feature = #feature_name)]
+ #vis async #sig #block
+ };
+
+ TokenStream::from(expanded)
+}
+
+/// Test module for `FuncArgs` parsing.
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn empty_input_defaults_to_async() {
+ // Input: empty → defaults to "foo_async"
+ let input = proc_macro2::TokenStream::new();
+ let args: FuncArgs = syn::parse2(input).unwrap();
+ assert_eq!(args.feature_name, "foo_async");
+ }
+
+ #[test]
+ fn custom_feature_name_parsed() {
+ // Input: "custom_name"
+ let input: proc_macro2::TokenStream = "\"custom_name\"".parse().unwrap();
+ let args: FuncArgs = syn::parse2(input).unwrap();
+ assert_eq!(args.feature_name, "custom_name");
+ }
+}
diff --git a/src/invoke.rs b/src/invoke.rs
new file mode 100644
index 0000000..f7da20a
--- /dev/null
+++ b/src/invoke.rs
@@ -0,0 +1,89 @@
+use crate::SynResult;
+use crate::TokenStream2;
+use crate::config::default_feature_name;
+use proc_macro::TokenStream;
+use quote::quote;
+use syn::{
+ LitStr, Token,
+ parse::{Parse, ParseStream},
+ parse_macro_input,
+};
+
+#[doc = include_str!("../doc/args/invoke.md")]
+pub enum InvokeArgs {
+ /// invoke!(expr) — feature name defaults to "async"
+ Default(TokenStream2),
+
+ /// invoke!("feat" => expr) — explicit feature name
+ Explicit(LitStr, TokenStream2),
+}
+
+impl Parse for InvokeArgs {
+ fn parse(input: ParseStream) -> SynResult<Self> {
+ if input.peek(LitStr) {
+ let feat: LitStr = input.parse()?;
+ input.parse::<Token![=>]>()?;
+ let expr: TokenStream2 = input.parse()?;
+ Ok(InvokeArgs::Explicit(feat, expr))
+ } else {
+ let expr: TokenStream2 = input.parse()?;
+ Ok(InvokeArgs::Default(expr))
+ }
+ }
+}
+
+pub(crate) fn invoke(input: TokenStream) -> TokenStream {
+ let input = parse_macro_input!(input as InvokeArgs);
+
+ let expanded = match input {
+ InvokeArgs::Default(expr) => {
+ let feat = default_feature_name();
+ quote! {{
+ #[cfg(feature = #feat)]
+ { #expr.await }
+ #[cfg(not(feature = #feat))]
+ { #expr }
+ }}
+ }
+ InvokeArgs::Explicit(feat, expr) => {
+ let feat_name = &feat;
+ quote! {{
+ #[cfg(feature = #feat_name)]
+ { #expr.await }
+ #[cfg(not(feature = #feat_name))]
+ { #expr }
+ }}
+ }
+ };
+
+ TokenStream::from(expanded)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn default_variant() {
+ // Input: an expression without feature name → Default variant
+ let input: proc_macro2::TokenStream = "compute(5)".parse().unwrap();
+ let args: InvokeArgs = syn::parse2(input).unwrap();
+ match args {
+ InvokeArgs::Default(_) => {} // expected
+ _ => panic!("expected Default variant"),
+ }
+ }
+
+ #[test]
+ fn explicit_variant() {
+ // Input: "my_ft" => expr → Explicit variant with feature "my_ft"
+ let input: proc_macro2::TokenStream = "\"my_ft\" => compute(5)".parse().unwrap();
+ let args: InvokeArgs = syn::parse2(input).unwrap();
+ match args {
+ InvokeArgs::Explicit(feat, _) => {
+ assert_eq!(feat.value(), "my_ft");
+ }
+ _ => panic!("expected Explicit variant"),
+ }
+ }
+}
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..d6a3c37
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,105 @@
+#![doc = include_str!("../doc/lib.md")]
+#![deny(missing_docs)]
+
+use proc_macro::TokenStream;
+
+pub(crate) mod config;
+pub(crate) mod func;
+pub(crate) mod invoke;
+pub(crate) mod select;
+
+pub(crate) use proc_macro2::TokenStream as TokenStream2;
+pub(crate) use syn::Result as SynResult;
+
+#[doc = include_str!("../doc/func.md")]
+///
+/// # How to use?
+///
+/// ```
+/// # use might_be_async::*;
+#[doc = include_str!("../doc/usage/func.rs")]
+/// ```
+///
+/// # Expanded
+///
+/// The above code will be expanded into the following:
+///
+/// Sync:
+///
+/// ```
+/// # use might_be_async::*;
+#[doc = include_str!("../doc/usage/func_expand.rs")]
+/// ```
+///
+/// Async:
+///
+/// ```
+/// # use might_be_async::*;
+#[doc = include_str!("../doc/usage/func_async_expand.rs")]
+/// ```
+#[proc_macro_attribute]
+pub fn func(attr: TokenStream, item: TokenStream) -> TokenStream {
+ func::func(attr, item)
+}
+
+#[doc = include_str!("../doc/invoke.md")]
+///
+/// # How to use?
+///
+/// ```
+/// # use might_be_async::*;
+#[doc = include_str!("../doc/usage/invoke.rs")]
+/// ```
+///
+/// # Expanded
+///
+/// The above code will be expanded into the following:
+///
+/// Sync:
+///
+/// ```
+/// # use might_be_async::*;
+#[doc = include_str!("../doc/usage/invoke_expand.rs")]
+/// ```
+///
+/// Async:
+///
+/// ```
+/// # use might_be_async::*;
+#[doc = include_str!("../doc/usage/invoke_async_expand.rs")]
+/// ```
+#[proc_macro]
+pub fn invoke(input: TokenStream) -> TokenStream {
+ invoke::invoke(input)
+}
+
+#[doc = include_str!("../doc/select.md")]
+///
+/// # How to use?
+///
+/// ```
+/// # use might_be_async::*;
+#[doc = include_str!("../doc/usage/select.rs")]
+/// ```
+///
+/// # Expanded
+///
+/// The above code will be expanded into the following:
+///
+/// Sync:
+///
+/// ```
+/// # use might_be_async::*;
+#[doc = include_str!("../doc/usage/select_expand.rs")]
+/// ```
+///
+/// Async:
+///
+/// ```
+/// # use might_be_async::*;
+#[doc = include_str!("../doc/usage/select_async_expand.rs")]
+/// ```
+#[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..1a2f8b3
--- /dev/null
+++ b/src/select.rs
@@ -0,0 +1,311 @@
+use crate::SynResult;
+use crate::TokenStream2;
+use crate::config::default_feature_name;
+use proc_macro::TokenStream;
+use proc_macro2::{Spacing, TokenTree};
+use quote::{ToTokens, quote};
+use syn::parse::{Parse, ParseStream};
+use syn::{Expr, LitStr, Token, parse_macro_input};
+
+#[doc = include_str!("../doc/args/select_arm.md")]
+pub enum SelectArmArgs {
+ /// "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 SelectArmArgs {
+ fn parse(input: ParseStream) -> SynResult<Self> {
+ parse_one_arm(input)
+ }
+}
+
+struct SelectInput {
+ arm0: SelectArmArgs,
+ arm1: SelectArmArgs,
+}
+
+impl Parse for SelectInput {
+ fn parse(input: ParseStream) -> SynResult<Self> {
+ let arm0 = parse_one_arm(input)?;
+ input.parse::<Token![else]>()?;
+ let arm1 = parse_one_arm(input)?;
+ Ok(SelectInput { arm0, arm1 })
+ }
+}
+
+/// Parse one arm: either `"feat" => { expr }`, `! => { expr }`, or `{ expr }`.
+pub fn parse_one_arm(input: ParseStream) -> SynResult<SelectArmArgs> {
+ // Parse an explicit feature arm: "feat_name" => { expr }
+ if input.peek(LitStr) {
+ let feat: LitStr = input.parse()?;
+ input.parse::<Token![=>]>()?;
+ let body: Expr = input.parse()?;
+ Ok(SelectArmArgs::Explicit { feat, body })
+ }
+ // Parse a negation arm: ! => { expr }
+ else if input.peek(Token![!]) {
+ input.parse::<Token![!]>()?;
+ input.parse::<Token![=>]>()?;
+ let body: Expr = input.parse()?;
+ Ok(SelectArmArgs::Not { body })
+ }
+ // Parse an implicit arm: { expr } (no feature name — will auto-detect by .await)
+ else {
+ // Expect a block expression { ... }
+ let body: Expr = input.parse()?;
+ Ok(SelectArmArgs::Implicit { body })
+ }
+}
+
+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.arm0;
+ let arm1 = &self.arm1;
+
+ match (arm0, arm1) {
+ // Both explicit — use cfg!() since no .await to worry about
+ (
+ SelectArmArgs::Explicit { feat: f0, body: b0 },
+ SelectArmArgs::Explicit { feat: f1, body: b1 },
+ ) => {
+ let f0_str = f0.value();
+ let f1_str = f1.value();
+
+ if has_not_prefix(&f0_str) && has_not_prefix(&f1_str) {
+ cfg_block(&quote! { #b0 }, &quote! { #b1 })
+ } else if has_not_prefix(&f0_str) {
+ let inner = &f0_str[1..];
+ cfg_block_with_feat(inner, &quote! { #b1 }, &quote! { #b0 })
+ } else if has_not_prefix(&f1_str) {
+ cfg_block_with_feat(&f0_str, &quote! { #b0 }, &quote! { #b1 })
+ } else {
+ both_explicit_block(&f0_str, &quote! { #b0 }, &f1_str, &quote! { #b1 })
+ }
+ }
+
+ // Explicit + Not — cfg!() safe (no .await)
+ (SelectArmArgs::Explicit { feat, body }, SelectArmArgs::Not { body: not_body }) => {
+ let feat_str = feat.value();
+ if has_not_prefix(&feat_str) {
+ let inner = &feat_str[1..];
+ cfg_block_with_feat(inner, &quote! { #not_body }, &quote! { #body })
+ } else {
+ cfg_block_with_feat(&feat_str, &quote! { #body }, &quote! { #not_body })
+ }
+ }
+
+ // Not + Explicit — cfg!() safe (no .await)
+ (SelectArmArgs::Not { body: not_body }, SelectArmArgs::Explicit { feat, body }) => {
+ let feat_str = feat.value();
+ if has_not_prefix(&feat_str) {
+ let inner = &feat_str[1..];
+ cfg_block_with_feat(inner, &quote! { #body }, &quote! { #not_body })
+ } else {
+ cfg_block_with_feat(&feat_str, &quote! { #body }, &quote! { #not_body })
+ }
+ }
+
+ // Explicit + Implicit — cfg!() safe (arms have no .await from this context)
+ (
+ SelectArmArgs::Explicit { feat, body },
+ SelectArmArgs::Implicit { body: imp_body },
+ ) => {
+ let feat_str = feat.value();
+ if has_not_prefix(&feat_str) {
+ let inner = &feat_str[1..];
+ cfg_block_with_feat(inner, &quote! { #imp_body }, &quote! { #body })
+ } else {
+ cfg_block_with_feat(&feat_str, &quote! { #body }, &quote! { #imp_body })
+ }
+ }
+
+ // Implicit + Explicit — cfg!() safe (arms have no .await from this context)
+ (
+ SelectArmArgs::Implicit { body: imp_body },
+ SelectArmArgs::Explicit { feat, body },
+ ) => {
+ let feat_str = feat.value();
+ if has_not_prefix(&feat_str) {
+ let inner = &feat_str[1..];
+ cfg_block_with_feat(inner, &quote! { #body }, &quote! { #imp_body })
+ } else {
+ cfg_block_with_feat(&feat_str, &quote! { #body }, &quote! { #imp_body })
+ }
+ }
+
+ // Both implicit — use #[cfg] blocks to handle .await correctly
+ (SelectArmArgs::Implicit { body: b0 }, SelectArmArgs::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) => cfg_block(&quote! { #b0 }, &quote! { #b1 }),
+ (false, true) => cfg_block(&quote! { #b1 }, &quote! { #b0 }),
+ (true, true) => {
+ let b1_stripped = strip_await_from_tokens(&b1.to_token_stream());
+ cfg_block(&quote! { #b0 }, &quote! { #b1_stripped })
+ }
+ (false, false) => cfg_block(&quote! { #b0 }, &quote! { #b1 }),
+ }
+ }
+
+ // Not + Implicit
+ (SelectArmArgs::Not { body: not_body }, SelectArmArgs::Implicit { body: imp_body }) => {
+ cfg_block(&quote! { #not_body }, &quote! { #imp_body })
+ }
+
+ // Implicit + Not — use #[cfg] blocks to handle .await correctly
+ (SelectArmArgs::Implicit { body: imp_body }, SelectArmArgs::Not { body: not_body }) => {
+ cfg_block(&quote! { #not_body }, &quote! { #imp_body })
+ }
+
+ // Two Not
+ (SelectArmArgs::Not { body: b0 }, SelectArmArgs::Not { body: b1 }) => {
+ cfg_block(&quote! { #b0 }, &quote! { #b1 })
+ }
+ }
+ }
+}
+
+/// Generate a block that uses the default feature name.
+///
+/// This function creates a `#[cfg]` block that conditionally compiles one of two branches
+/// based on whether the default feature (as returned by [`default_feature_name()`]) is enabled.
+fn cfg_block(async_branch: &TokenStream2, sync_branch: &TokenStream2) -> TokenStream2 {
+ let feat = default_feature_name();
+ cfg_block_with_feat(feat, async_branch, sync_branch)
+}
+
+/// Generate a block using a specific feature name.
+fn cfg_block_with_feat(
+ feat: &str,
+ async_branch: &TokenStream2,
+ sync_branch: &TokenStream2,
+) -> TokenStream2 {
+ quote! {{
+ #[cfg(feature = #feat)]
+ { #async_branch }
+ #[cfg(not(feature = #feat))]
+ { #sync_branch }
+ }}
+}
+
+/// Generate a block where each arm is gated by its own feature.
+fn both_explicit_block(
+ feat0: &str,
+ branch0: &TokenStream2,
+ feat1: &str,
+ branch1: &TokenStream2,
+) -> TokenStream2 {
+ quote! {{
+ #[cfg(feature = #feat0)]
+ { #branch0 }
+ #[cfg(feature = #feat1)]
+ { #branch1 }
+ }}
+}
+
+/// Checks if the given string has the '!' (not) prefix.
+/// This is used to denote negated feature names in select! arms.
+fn has_not_prefix(s: &str) -> bool {
+ s.starts_with('!')
+}
+
+/// Checks if the given token stream contains a `.await` expression.
+///
+/// This function traverses the token stream looking for the pattern `. await`,
+/// which indicates an `.await` call in Rust syntax. It is used to determine
+/// whether an implicit select arm contains async code, which influences how
+/// the generated code handles the `cfg` blocks.
+///
+/// Returns `true` if `.await` is found, `false` otherwise.
+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
+ && p.as_char() == '.'
+ && p.spacing() == Spacing::Alone
+ && let Some(TokenTree::Ident(ident)) = tokens.next()
+ && ident == "await"
+ {
+ return true;
+ }
+ }
+ false
+}
+
+/// Strips a trailing `.await` from a token stream.
+fn strip_await_from_tokens(ts: &TokenStream2) -> TokenStream2 {
+ let tokens: Vec<_> = ts.clone().into_iter().collect();
+ let len = tokens.len();
+ if len >= 2
+ && let TokenTree::Punct(p) = &tokens[len - 2]
+ && p.as_char() == '.'
+ && let TokenTree::Ident(ident) = &tokens[len - 1]
+ && ident == "await"
+ {
+ return tokens[..len - 2].iter().cloned().collect();
+ }
+ ts.clone()
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::select::SelectArmArgs;
+ use quote::ToTokens;
+
+ #[test]
+ fn test_explicit_arm() {
+ let input: proc_macro2::TokenStream = "\"async\" => { 100 }".parse().unwrap();
+ let arm: SelectArmArgs = syn::parse2(input).unwrap();
+ match &arm {
+ SelectArmArgs::Explicit { feat, body } => {
+ assert_eq!(feat.value(), "async");
+ let s = body.to_token_stream().to_string();
+ assert!(s.contains("100"), "body should contain 100, got: {s}");
+ }
+ _ => panic!("expected Explicit variant"),
+ }
+ }
+
+ #[test]
+ fn test_not_arm() {
+ let input: proc_macro2::TokenStream = "! => { 200 }".parse().unwrap();
+ let arm: SelectArmArgs = syn::parse2(input).unwrap();
+ match &arm {
+ SelectArmArgs::Not { body } => {
+ let s = body.to_token_stream().to_string();
+ assert!(s.contains("200"), "body should contain 200, got: {s}");
+ }
+ _ => panic!("expected Not variant"),
+ }
+ }
+
+ #[test]
+ fn test_implicit_arm() {
+ let input: proc_macro2::TokenStream = "{ 1 + 2 }".parse().unwrap();
+ let arm: SelectArmArgs = syn::parse2(input).unwrap();
+ match &arm {
+ SelectArmArgs::Implicit { body } => {
+ let s = body.to_token_stream().to_string();
+ assert!(
+ s.contains("1 + 2") || s.contains("1+2"),
+ "body should contain 1 + 2, got: {s}"
+ );
+ }
+ _ => panic!("expected Implicit variant"),
+ }
+ }
+}