aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2026-06-27 17:46:29 +0800
committer魏曹先生 <1992414357@qq.com>2026-06-27 17:46:29 +0800
commit4f7bd4f6fa5d27cfe703c54aa029a321f40d19fb (patch)
tree97c31ddbb905d8f0e70418b0970ee6420aeac611
parent6cadc8d39adebbc263e8576c389b5ef491f10ace (diff)
feat(macros): add async mutable resource injection for `#[chain]`
Support `&mut T` resource parameters in async chain functions by using an extract-store pattern that avoids holding mutable borrows across await points. Remove the previous compile-time rejection of this combination.
-rw-r--r--CHANGELOG.md16
-rw-r--r--examples/example-async-support/src/main.rs3
-rw-r--r--mingling/src/example_docs.rs3
-rw-r--r--mingling_core/src/asset/global_resource.rs38
-rw-r--r--mingling_macros/src/chain.rs29
-rw-r--r--mingling_macros/src/lib.rs18
-rw-r--r--mingling_macros/src/res_injection.rs73
7 files changed, 149 insertions, 31 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 60b8f2c..7cc4e6f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -243,6 +243,22 @@ fn comp_my_entry(ctx: &ShellContext, res: &mut ResA) -> Suggest {
For mutable resources (`&mut T`), both macros use `Program::modify_res` (with constraint `Return: Default`) instead of `#[chain]`'s dedicated `__modify_res_and_return_route` (with constraint `Return: Into<ChainProcess>`), because the return types of help/completion are `()` and `Suggest` respectively.
+12. **\[macros\]** Added async mutable resource injection support for `#[chain]`. Previously, async chain functions could only use `&T` (immutable) resource injection; `&mut T` was rejected with a compile-time error. Now, async chain functions support `&mut T` resource injection by using an extract‑store pattern: each mutable resource is cloned out of the global store before the body executes (via `__extract_res_mut`), bound as `&mut` within a scoped block, and written back after the block completes (via `__store_res`). This avoids holding a mutable borrow across `.await` points while still providing a natural `&mut T` syntax.
+
+```rust
+use mingling::macros::{chain, pack, gen_program};
+
+pack!(MyOutput = String);
+
+#[chain]
+async fn greet(prev: HelloEntry, ec: &mut ResExitCode) -> Next {
+ let name = prev.first().cloned().unwrap_or_else(|| "World".to_string());
+ ec.exit_code = 42;
+ some_async_fn(&name).await;
+ MyOutput::new(name)
+}
+```
+
#### **BREAKING CHANGES** (API CHANGES):
---
diff --git a/examples/example-async-support/src/main.rs b/examples/example-async-support/src/main.rs
index 920b3dd..847bfea 100644
--- a/examples/example-async-support/src/main.rs
+++ b/examples/example-async-support/src/main.rs
@@ -9,8 +9,7 @@
//!
//! However, you will lose some capabilities:
//!
-//! 1. `&mut` resource injection is not available in async chain functions
-//! 2. The program will not be able to use panic unwind functionality
+//! 1. The program will not be able to use panic unwind functionality
//!
//! Run:
//! ```bash
diff --git a/mingling/src/example_docs.rs b/mingling/src/example_docs.rs
index c492a11..5208077 100644
--- a/mingling/src/example_docs.rs
+++ b/mingling/src/example_docs.rs
@@ -128,8 +128,7 @@ pub mod example_argument_parse {}
///
/// However, you will lose some capabilities:
///
-/// 1. `&mut` resource injection is not available in async chain functions
-/// 2. The program will not be able to use panic unwind functionality
+/// 1. The program will not be able to use panic unwind functionality
///
/// Run:
/// ```bash
diff --git a/mingling_core/src/asset/global_resource.rs b/mingling_core/src/asset/global_resource.rs
index 83a779d..651655a 100644
--- a/mingling_core/src/asset/global_resource.rs
+++ b/mingling_core/src/asset/global_resource.rs
@@ -74,6 +74,44 @@ where
}
}
+ /// Internal syntax for the `&mut MyResource` syntax of async #[chain], do not use directly.
+ ///
+ /// Extracts a mutable resource from the global store (clone-out), returning an
+ /// owned value. The caller must call [`__store_res`] to write back modifications.
+ #[doc(hidden)]
+ #[must_use]
+ pub fn __extract_res_mut<Res: 'static + Default + ResourceMarker + Send + Sync>(
+ &self,
+ ) -> Res {
+ let Ok(mut guard) = self.resources.lock() else {
+ return Res::res_default();
+ };
+ if let Some(arc_res) = guard
+ .get_mut(&TypeId::of::<Res>())
+ .and_then(|a| a.downcast_mut::<Arc<Res>>())
+ {
+ match Arc::try_unwrap(std::mem::take(arc_res)) {
+ Ok(val) => val,
+ Err(arc) => (*arc).res_clone(),
+ }
+ } else {
+ Res::res_default()
+ }
+ }
+
+ /// Internal syntax for the `&mut MyResource` syntax of async #[chain], do not use directly.
+ ///
+ /// Stores a modified resource value back into the global store.
+ #[doc(hidden)]
+ pub fn __store_res<Res: 'static + Send + Sync + ResourceMarker>(
+ &self,
+ val: Res,
+ ) {
+ if let Ok(mut guard) = self.resources.lock() {
+ guard.insert(TypeId::of::<Res>(), Box::new(Arc::new(val)));
+ }
+ }
+
/// Get an resources by type, returning `Res` if present
#[must_use]
pub fn res<Res: 'static + Send + Sync>(&self) -> Option<GlobalResource<Res>> {
diff --git a/mingling_macros/src/chain.rs b/mingling_macros/src/chain.rs
index 6191d39..5f72422 100644
--- a/mingling_macros/src/chain.rs
+++ b/mingling_macros/src/chain.rs
@@ -2,7 +2,7 @@
use crate::res_injection::{
ResourceInjection, extract_args_info, generate_immut_resource_bindings,
- wrap_body_with_mut_resources,
+ wrap_body_with_mut_resources, wrap_body_with_mut_resources_async,
};
use proc_macro::TokenStream;
use quote::{ToTokens, quote};
@@ -93,7 +93,11 @@ fn generate_proc_fn(
fn_body_stmts
};
- let wrapped_body = wrap_body_with_mut_resources(body_stmts, &mut_resources, program_type);
+ let wrapped_body = if is_async_fn && !mut_resources.is_empty() {
+ wrap_body_with_mut_resources_async(body_stmts, &mut_resources, program_type)
+ } else {
+ wrap_body_with_mut_resources(body_stmts, &mut_resources, program_type)
+ };
// When the function returns `()`, wrap the result with ResultEmpty
let call_or_wrapped = if is_unit_return {
@@ -252,19 +256,6 @@ fn reject_async(sig: &Signature) -> Result<(), proc_macro2::TokenStream> {
Ok(())
}
-/// Ensures no `&mut` resource injection is used in async functions.
-#[cfg(feature = "async")]
-fn reject_mut_in_async(resources: &[ResourceInjection]) -> Result<(), proc_macro2::TokenStream> {
- if let Some(mut_res) = resources.iter().find(|r| r.is_mut) {
- return Err(syn::Error::new(
- mut_res.var_name.span(),
- "Cannot use `&mut` resource injection in async chain function.",
- )
- .to_compile_error());
- }
- Ok(())
-}
-
pub fn chain_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
// Reject non-empty attribute arguments; #[chain] must be bare
if !attr.is_empty() {
@@ -304,14 +295,6 @@ pub fn chain_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
Err(e) => return e.to_compile_error().into(),
};
- // Reject `&mut` in async chains
- #[cfg(feature = "async")]
- if is_async_fn {
- if let Err(err) = reject_mut_in_async(&resources) {
- return err.into();
- }
- }
-
// Prepare building blocks
let sig = &input_fn.sig;
let inputs = &sig.inputs;
diff --git a/mingling_macros/src/lib.rs b/mingling_macros/src/lib.rs
index 21273ba..1b0cbb5 100644
--- a/mingling_macros/src/lib.rs
+++ b/mingling_macros/src/lib.rs
@@ -858,8 +858,6 @@ pub fn r_println(input: TokenStream) -> TokenStream {
/// - The first parameter (previous type) must be taken **by move**, not by reference.
/// - Resource injection parameters **must** be references (`&T` or `&mut T`),
/// owned values are not allowed.
-/// - When the `async` feature is enabled, `&mut T` cannot be used in async
-/// chain functions (only `&T` is supported for async).
///
/// # Sync Example
///
@@ -924,6 +922,22 @@ pub fn r_println(input: TokenStream) -> TokenStream {
/// }
/// ```
///
+/// # Async Example with Mutable Resource Injection
+///
+/// ```rust,ignore
+/// use mingling::macros::{chain, pack, gen_program};
+///
+/// pack!(MyOutput = String);
+///
+/// #[chain]
+/// async fn greet(prev: HelloEntry, ec: &mut ResExitCode) -> Next {
+/// let name = prev.first().cloned().unwrap_or_else(|| "World".to_string());
+/// ec.exit_code = 42;
+/// some_async_fn(&name).await;
+/// MyOutput::new(name)
+/// }
+/// ```
+///
/// # Requirements
///
/// - The function must have at least **one** parameter (the previous type in the chain).
diff --git a/mingling_macros/src/res_injection.rs b/mingling_macros/src/res_injection.rs
index 4f0be88..f2952cc 100644
--- a/mingling_macros/src/res_injection.rs
+++ b/mingling_macros/src/res_injection.rs
@@ -158,9 +158,19 @@ pub(crate) fn generate_immut_resource_bindings<'a>(
.collect()
}
+/// Generates a unique binding name for a mutable resource variable.
+fn mut_res_binding_name(var_name: &Ident) -> Ident {
+ syn::Ident::new(
+ &format!("__{}_binding", var_name),
+ var_name.span(),
+ )
+}
+
/// Wraps the function body in nested `__modify_res_and_return_route` closures for
-/// each mutable resource parameter. The innermost closure gets the original body,
-/// and each mutable parameter wraps outward from last to first.
+/// each mutable resource parameter (sync version).
+///
+/// The innermost closure gets the original body, and each mutable parameter wraps
+/// outward from last to first.
pub(crate) fn wrap_body_with_mut_resources(
fn_body_stmts: &[syn::Stmt],
mut_resources: &[&ResourceInjection],
@@ -182,3 +192,62 @@ pub(crate) fn wrap_body_with_mut_resources(
wrapped
}
+
+/// Generates code for mutable resource injection in async `#[chain]` functions.
+///
+/// Instead of wrapping in a closure (which causes lifetime issues with async),
+/// this generates a three-part pattern:
+///
+/// 1. **Extract** each mutable resource from the global store into an owned local.
+/// 2. **Body block** — the user's body with `&mut` references scoped to a block.
+/// 3. **Store back** each modified resource after the body block completes.
+///
+/// This avoids holding a `&mut` reference across `.await` points — the borrow
+/// ends when the body block closes.
+pub(crate) fn wrap_body_with_mut_resources_async(
+ fn_body_stmts: &[syn::Stmt],
+ mut_resources: &[&ResourceInjection],
+ program_type: &proc_macro2::TokenStream,
+) -> proc_macro2::TokenStream {
+ // 1. Generate extract statements and body-block `let` bindings
+ let mut extract_stmts = Vec::new();
+ let mut body_lets = Vec::new();
+
+ for res in mut_resources {
+ let var_name = &res.var_name;
+ let inner_type = &res.inner_type;
+ let binding_name = mut_res_binding_name(var_name);
+
+ extract_stmts.push(quote! {
+ let mut #binding_name = ::mingling::this::<#program_type>()
+ .__extract_res_mut::<#inner_type>();
+ });
+
+ body_lets.push(quote! {
+ let #var_name: &mut #inner_type = &mut #binding_name;
+ });
+ }
+
+ // 2. Store-back statements (reverse order is fine, resources are independent)
+ let mut store_stmts = Vec::new();
+ for res in mut_resources {
+ let var_name = &res.var_name;
+ let inner_type = &res.inner_type;
+ let binding_name = mut_res_binding_name(var_name);
+
+ store_stmts.push(quote! {
+ ::mingling::this::<#program_type>()
+ .__store_res::<#inner_type>(#binding_name);
+ });
+ }
+
+ quote! {
+ #(#extract_stmts)*
+ let __chain_result = {
+ #(#body_lets)*
+ #(#fn_body_stmts)*
+ };
+ #(#store_stmts)*
+ __chain_result
+ }
+}