aboutsummaryrefslogtreecommitdiff
path: root/mingling_macros
diff options
context:
space:
mode:
Diffstat (limited to 'mingling_macros')
-rw-r--r--mingling_macros/src/attr.rs9
-rw-r--r--mingling_macros/src/attr/chain.rs (renamed from mingling_macros/src/chain.rs)270
-rw-r--r--mingling_macros/src/attr/completion.rs (renamed from mingling_macros/src/completion.rs)41
-rw-r--r--mingling_macros/src/attr/dispatcher_clap.rs (renamed from mingling_macros/src/dispatcher_clap.rs)16
-rw-r--r--mingling_macros/src/attr/help.rs (renamed from mingling_macros/src/help.rs)86
-rw-r--r--mingling_macros/src/attr/program_setup.rs (renamed from mingling_macros/src/program_setup.rs)2
-rw-r--r--mingling_macros/src/attr/renderer.rs (renamed from mingling_macros/src/renderer.rs)102
-rw-r--r--mingling_macros/src/derive.rs2
-rw-r--r--mingling_macros/src/derive/enum_tag.rs (renamed from mingling_macros/src/enum_tag.rs)2
-rw-r--r--mingling_macros/src/derive/grouped.rs (renamed from mingling_macros/src/groupped.rs)12
-rw-r--r--mingling_macros/src/extensions.rs119
-rw-r--r--mingling_macros/src/extensions/buffer.rs95
-rw-r--r--mingling_macros/src/extensions/routeify.rs46
-rw-r--r--mingling_macros/src/func.rs13
-rw-r--r--mingling_macros/src/func/dispatcher.rs (renamed from mingling_macros/src/dispatcher.rs)10
-rw-r--r--mingling_macros/src/func/entry.rs (renamed from mingling_macros/src/entry.rs)2
-rw-r--r--mingling_macros/src/func/gen_program.rs624
-rw-r--r--mingling_macros/src/func/group.rs (renamed from mingling_macros/src/group_impl.rs)6
-rw-r--r--mingling_macros/src/func/node.rs (renamed from mingling_macros/src/node.rs)2
-rw-r--r--mingling_macros/src/func/pack.rs (renamed from mingling_macros/src/pack.rs)4
-rw-r--r--mingling_macros/src/func/pack_err.rs (renamed from mingling_macros/src/pack_err.rs)12
-rw-r--r--mingling_macros/src/func/r_print.rs62
-rw-r--r--mingling_macros/src/func/suggest.rs (renamed from mingling_macros/src/suggest.rs)4
-rw-r--r--mingling_macros/src/lib.rs990
-rw-r--r--mingling_macros/src/systems.rs5
-rw-r--r--mingling_macros/src/systems/dispatch_tree_gen.rs (renamed from mingling_macros/src/dispatch_tree_gen.rs)6
-rw-r--r--mingling_macros/src/systems/res_injection.rs (renamed from mingling_macros/src/res_injection.rs)25
-rw-r--r--mingling_macros/src/systems/structural_data.rs (renamed from mingling_macros/src/structural_data.rs)4
-rw-r--r--mingling_macros/src/utils.rs1
29 files changed, 1553 insertions, 1019 deletions
diff --git a/mingling_macros/src/attr.rs b/mingling_macros/src/attr.rs
new file mode 100644
index 0000000..e4cd826
--- /dev/null
+++ b/mingling_macros/src/attr.rs
@@ -0,0 +1,9 @@
+pub(crate) mod chain;
+#[cfg(feature = "comp")]
+pub(crate) mod completion;
+#[cfg(feature = "clap")]
+pub(crate) mod dispatcher_clap;
+pub(crate) mod help;
+#[cfg(feature = "extra_macros")]
+pub(crate) mod program_setup;
+pub(crate) mod renderer;
diff --git a/mingling_macros/src/chain.rs b/mingling_macros/src/attr/chain.rs
index 5f72422..dbd87dd 100644
--- a/mingling_macros/src/chain.rs
+++ b/mingling_macros/src/attr/chain.rs
@@ -9,8 +9,7 @@ use quote::{ToTokens, quote};
use syn::spanned::Spanned;
use syn::{Ident, ItemFn, Pat, ReturnType, Signature, Type, TypePath, parse_macro_input};
-/// Validates that the return type of the function is `Next`.
-/// Checks whether the return type is `()` (unit).
+/// Checks whether the return type is `()`
fn is_unit_return_type(sig: &Signature) -> bool {
match &sig.output {
ReturnType::Type(_, ty) => match &**ty {
@@ -21,124 +20,121 @@ fn is_unit_return_type(sig: &Signature) -> bool {
}
}
+/// Validates that the return type is acceptable.
+/// Accepts `()`, `Next`, `ChainProcess<...>`, or any type that can
+/// be converted to `ChainProcess` via `.into()` (i.e. any pack type).
fn validate_return_type(sig: &Signature) -> Result<(), proc_macro2::TokenStream> {
- // If return type is `()`, it's valid (no Next required)
+ // `()` or omitted is always valid
if is_unit_return_type(sig) {
return Ok(());
}
- match &sig.output {
- ReturnType::Type(_, ty) => match &**ty {
- Type::Path(type_path) => {
- let last_segment = type_path.path.segments.last().unwrap();
- if last_segment.ident != "Next" {
- return Err(syn::Error::new(
- ty.span(),
- "Chain function must return `Next` or `()`",
- )
- .to_compile_error());
- }
- }
- _ => {
- return Err(syn::Error::new(
- ty.span(),
- "Chain function must return `Next` or `()`",
- )
- .to_compile_error());
- }
- },
- ReturnType::Default => {
- return Err(syn::Error::new(
- sig.span(),
- "Chain function must specify a return type (must be `Next` or `()`)",
- )
- .to_compile_error());
- }
- }
Ok(())
}
-/// Builds the `proc` function implementation that serves as the actual chain
-/// entry point inside the generated `Chain` impl.
+/// Builds the `proc` function implementation inside the generated `Chain` impl.
///
-/// * Without resources: delegates directly to the original function.
-/// * With resources: inlines the body and prepends resource bindings.
-#[allow(unused_variables)]
+/// Instead of inlining the user's body, the trait method calls the original
+/// function by name, with resources injected from the application context.
fn generate_proc_fn(
+ fn_name: &Ident,
has_resources: bool,
resources: &[ResourceInjection],
program_type: &proc_macro2::TokenStream,
previous_type: &TypePath,
- prev_param: &Pat,
- fn_name: &Ident,
- fn_body_stmts: &[syn::Stmt],
is_async_fn: bool,
is_unit_return: bool,
+ origin_return_type: &proc_macro2::TokenStream,
) -> proc_macro2::TokenStream {
let immut_resource_stmts = generate_immut_resource_bindings(resources.iter(), program_type);
let mut_resources: Vec<_> = resources.iter().filter(|r| r.is_mut).collect();
- let body_stmts: &[syn::Stmt] = if is_unit_return && has_resources {
- let mut stmts = fn_body_stmts.to_vec();
- stmts.push(syn::Stmt::Expr(
- syn::parse_quote! {
- <crate::ResultEmpty as ::mingling::Groupped::<crate::ThisProgram>>
- ::to_chain(crate::ResultEmpty)
- },
- None,
- ));
- // Box::leak to get a &'static [syn::Stmt]
- Box::leak(Box::new(stmts))
+ // Use a fixed parameter name `prev` for the trait method, regardless of
+ // the user's original parameter name (which may be `_` and cannot be
+ // referenced in expression position).
+ let fixed_prev: Pat = syn::parse_quote!(prev);
+
+ // Build the call to the original function with resource arguments injected.
+ // The variable names come from the resource injection bindings
+ // (immut bindings are `let #name = …`, mut closures receive `|#name: &mut T|`),
+ // which match the original function's parameter names.
+ let resource_args: Vec<_> = resources
+ .iter()
+ .map(|res| {
+ let var_name = &res.var_name;
+ quote! { #var_name }
+ })
+ .collect();
+
+ let fn_call = if has_resources {
+ let call = quote! { #fn_name(#fixed_prev, #(#resource_args),*) };
+ if is_async_fn {
+ quote! { #call.await }
+ } else {
+ call
+ }
} else {
- fn_body_stmts
+ let call = quote! { #fn_name(#fixed_prev) };
+ if is_async_fn {
+ quote! { #call.await }
+ } else {
+ call
+ }
};
+ // Convert the function call to a syn::Stmt so existing wrapping functions can use it
+ let fn_call_expr: syn::Expr = syn::parse_quote! { #fn_call };
+ let fn_call_stmt = syn::Stmt::Expr(fn_call_expr, None);
+
let wrapped_body = if is_async_fn && !mut_resources.is_empty() {
- wrap_body_with_mut_resources_async(body_stmts, &mut_resources, program_type)
+ wrap_body_with_mut_resources_async(&[fn_call_stmt], &mut_resources, program_type)
} else {
- wrap_body_with_mut_resources(body_stmts, &mut_resources, program_type)
+ wrap_body_with_mut_resources(
+ &[fn_call_stmt],
+ &mut_resources,
+ program_type,
+ is_unit_return,
+ )
};
- // When the function returns `()`, wrap the result with ResultEmpty
- let call_or_wrapped = if is_unit_return {
- if has_resources {
+ let proc_body = if is_unit_return {
+ let body_with_ending = if has_resources {
quote! {
#(#immut_resource_stmts)*
- #wrapped_body
+ #wrapped_body;
+ <crate::ResultEmpty as ::mingling::Routable::<crate::ThisProgram>>
+ ::to_chain(crate::ResultEmpty)
}
} else {
- let call = if is_async_fn {
- quote! { #fn_name(#prev_param).await; }
- } else {
- quote! { #fn_name(#prev_param); }
- };
quote! {
- #call
- <crate::ResultEmpty as ::mingling::Groupped::<crate::ThisProgram>>
+ #wrapped_body;
+ <crate::ResultEmpty as ::mingling::Routable::<crate::ThisProgram>>
::to_chain(crate::ResultEmpty)
}
- }
- } else if has_resources {
- quote! {
- #(#immut_resource_stmts)*
- #wrapped_body
- }
+ };
+ quote! { #body_with_ending }
} else {
- let call = if is_async_fn {
- quote! { #fn_name(#prev_param).await.into() }
+ let body = if has_resources {
+ quote! {
+ #(#immut_resource_stmts)*
+ #wrapped_body
+ }
} else {
- quote! { #fn_name(#prev_param).into() }
+ quote! { #wrapped_body }
};
quote! {
- #call
+ let __chain_result = { #body };
+ <#origin_return_type as ::std::convert::Into<
+ ::mingling::ChainProcess<#program_type>
+ >>::into(__chain_result)
}
};
#[cfg(feature = "async")]
{
quote! {
- async fn proc(#prev_param: #previous_type) -> ::mingling::ChainProcess<#program_type> {
- #call_or_wrapped
+ async fn proc(#fixed_prev: #previous_type) -> ::mingling::ChainProcess<#program_type> {
+ #proc_body
}
}
}
@@ -146,74 +142,15 @@ fn generate_proc_fn(
#[cfg(not(feature = "async"))]
{
quote! {
- fn proc(#prev_param: #previous_type) -> ::mingling::ChainProcess<#program_type> {
- #call_or_wrapped
- }
- }
- }
-}
-
-/// Generates the original function signature (kept for backwards compatibility /
-/// internal use), with its return type changed to `impl Into<ChainProcess<..>>`.
-#[allow(unused_variables)]
-fn generate_original_fn(
- fn_attrs: &[syn::Attribute],
- vis: &syn::Visibility,
- fn_name: &Ident,
- inputs: &syn::punctuated::Punctuated<syn::FnArg, syn::Token![,]>,
- fn_body: &syn::Block,
- is_async_fn: bool,
- program_type: &proc_macro2::TokenStream,
- is_unit_return: bool,
-) -> proc_macro2::TokenStream {
- // Both unit and Next return types need to produce `impl Into<ChainProcess<ProgramType>>`
- let return_type = quote! { impl Into<::mingling::ChainProcess<#program_type>> };
-
- let body = if is_unit_return {
- quote! {
- {
- #fn_body
- <crate::ResultEmpty as ::mingling::Groupped::<crate::ThisProgram>>::to_chain(crate::ResultEmpty)
- }
- }
- } else {
- quote! {
- {
- let _: crate::Next;
- let _: Next;
- #fn_body
- }
- }
- };
-
- #[cfg(feature = "async")]
- {
- let async_kw = if is_async_fn {
- quote! { async }
- } else {
- quote! {}
- };
- quote! {
- #(#fn_attrs)*
- #vis #async_kw fn #fn_name(#inputs) -> #return_type {
- #body
- }
- }
- }
-
- #[cfg(not(feature = "async"))]
- {
- quote! {
- #(#fn_attrs)*
- #vis fn #fn_name(#inputs) -> #return_type {
- #body
+ fn proc(#fixed_prev: #previous_type) -> ::mingling::ChainProcess<#program_type> {
+ #proc_body
}
}
}
}
/// Assembles the final expanded output: hidden struct, `register_chain!` invocation,
-/// `Chain` impl with the `proc` method, and the original function.
+/// `Chain` impl with the `proc` method, and the preserved original function.
fn generate_struct_and_impl(
fn_attrs: &[syn::Attribute],
vis: &syn::Visibility,
@@ -222,7 +159,7 @@ fn generate_struct_and_impl(
previous_type_str: &proc_macro2::TokenStream,
program_type: &proc_macro2::TokenStream,
proc_fn: &proc_macro2::TokenStream,
- origin_proc_fn: &proc_macro2::TokenStream,
+ original_fn: &proc_macro2::TokenStream,
) -> proc_macro2::TokenStream {
quote! {
#(#fn_attrs)*
@@ -238,8 +175,8 @@ fn generate_struct_and_impl(
#proc_fn
}
- // Keep the original function for internal use
- #origin_proc_fn
+ // Keep the original function unchanged
+ #original_fn
}
}
@@ -256,7 +193,7 @@ fn reject_async(sig: &Signature) -> Result<(), proc_macro2::TokenStream> {
Ok(())
}
-pub fn chain_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
+pub(crate) fn chain_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
// Reject non-empty attribute arguments; #[chain] must be bare
if !attr.is_empty() {
return syn::Error::new(
@@ -290,15 +227,12 @@ pub fn chain_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
}
// Extract the previous type, parameter name, and resource injection params
- let (prev_param, previous_type, resources) = match extract_args_info(&input_fn.sig) {
+ let (_, previous_type, resources) = match extract_args_info(&input_fn.sig) {
Ok(info) => info,
Err(e) => return e.to_compile_error().into(),
};
// Prepare building blocks
- let sig = &input_fn.sig;
- let inputs = &sig.inputs;
- let fn_body = &input_fn.block;
let mut fn_attrs = input_fn.attrs.clone();
fn_attrs.retain(|attr| !attr.path().is_ident("chain"));
let vis = &input_fn.vis;
@@ -315,36 +249,33 @@ pub fn chain_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
// Always use the default crate-defined program path
let program_type = crate::default_program_path();
- // Generate the `proc` function
+ // Extract the user's return type for the explicit Into turbofish
+ let origin_return_type = match &input_fn.sig.output {
+ ReturnType::Type(_, ty) => quote! { #ty },
+ ReturnType::Default => quote! { () },
+ };
+
+ // Generate the `proc` function for the Chain impl
let proc_fn = generate_proc_fn(
+ fn_name,
has_resources,
&resources,
&program_type,
&previous_type,
- &prev_param,
- fn_name,
- &fn_body.stmts,
#[cfg(feature = "async")]
is_async_fn,
#[cfg(not(feature = "async"))]
false,
is_unit_return,
+ &origin_return_type,
);
- // Generate the original function
- let origin_proc_fn = generate_original_fn(
- &fn_attrs,
- vis,
- fn_name,
- inputs,
- fn_body,
- #[cfg(feature = "async")]
- is_async_fn,
- #[cfg(not(feature = "async"))]
- false,
- &program_type,
- is_unit_return,
- );
+ // Preserve the original function untouched
+ // Note: do NOT add `#vis` here — `input_fn` (ItemFn) already contains its own visibility.
+ let original_fn = quote! {
+ #(#fn_attrs)*
+ #input_fn
+ };
// Assemble the final output
let previous_type_str = quote! { #previous_type };
@@ -356,14 +287,17 @@ pub fn chain_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
&previous_type_str,
&program_type,
&proc_fn,
- &origin_proc_fn,
+ &original_fn,
);
expanded.into()
}
/// Builds a match arm for chain mapping
-pub fn build_chain_arm(struct_name: &Ident, previous_type: &TypePath) -> proc_macro2::TokenStream {
+pub(crate) fn build_chain_arm(
+ struct_name: &Ident,
+ previous_type: &TypePath,
+) -> proc_macro2::TokenStream {
let enum_variant = &previous_type.path.segments.last().unwrap().ident;
quote! {
#struct_name => #enum_variant,
@@ -371,14 +305,14 @@ pub fn build_chain_arm(struct_name: &Ident, previous_type: &TypePath) -> proc_ma
}
/// Builds a match arm for chain existence check
-pub fn build_chain_exist_arm(previous_type: &TypePath) -> proc_macro2::TokenStream {
+pub(crate) fn build_chain_exist_arm(previous_type: &TypePath) -> proc_macro2::TokenStream {
let enum_variant = &previous_type.path.segments.last().unwrap().ident;
quote! {
Self::#enum_variant => true,
}
}
-pub fn register_chain(input: TokenStream) -> TokenStream {
+pub(crate) fn register_chain(input: TokenStream) -> TokenStream {
// Parse the input as a comma-separated list of arguments
let input_parsed = syn::parse_macro_input!(input with syn::punctuated::Punctuated<syn::Expr, syn::Token![,]>::parse_terminated);
diff --git a/mingling_macros/src/completion.rs b/mingling_macros/src/attr/completion.rs
index ae01462..3ced091 100644
--- a/mingling_macros/src/completion.rs
+++ b/mingling_macros/src/attr/completion.rs
@@ -5,7 +5,7 @@ use syn::spanned::Spanned;
use syn::{FnArg, Ident, ItemFn, Pat, PatType, Type, TypePath, parse_macro_input};
#[cfg(feature = "comp")]
-pub fn completion_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
+pub(crate) fn completion_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
// Parse the attribute arguments such as HelloEntry or crate::EntryFine from #[completion(crate::EntryFine)]
use crate::get_global_set;
let previous_type_path: TypePath = if attr.is_empty() {
@@ -47,11 +47,8 @@ pub fn completion_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
// Extract the first param pattern and type for the ctx parameter
let first_arg = &inputs[0];
- let (ctx_pat, _ctx_type) = match first_arg {
- FnArg::Typed(PatType { pat, ty, .. }) => {
- let param_pat = (**pat).clone();
- (param_pat, (**ty).clone())
- }
+ let _ctx_type = match first_arg {
+ FnArg::Typed(PatType { ty, .. }) => (**ty).clone(),
FnArg::Receiver(_) => {
return syn::Error::new(
first_arg.span(),
@@ -61,6 +58,7 @@ pub fn completion_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
.into();
}
};
+ let fixed_ctx: Pat = syn::parse_quote!(ctx);
// Extract resources from params 2 through N, skipping ctx
let resources = match extract_resources_from_args(sig, 1) {
@@ -70,7 +68,6 @@ pub fn completion_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
// Get the function body
let fn_body = &input_fn.block;
- let fn_body_stmts = &fn_body.stmts;
// Get function attributes excluding the completion attribute
let mut fn_attrs = input_fn.attrs.clone();
@@ -96,12 +93,26 @@ pub fn completion_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
// Generate immutable resource bindings
let immut_resource_stmts = generate_immut_resource_bindings(resources.iter(), &program_type);
- // Build the comp method body with resource injection
- // Use modify_res for mutable resources same pattern as renderer.rs
- let wrapped_body = if mut_resources.is_empty() {
- quote! { #(#fn_body_stmts)* }
+ // Build the call to the original function with resource arguments injected
+ let resource_args: Vec<_> = resources
+ .iter()
+ .map(|res| {
+ let var_name = &res.var_name;
+ quote! { #var_name }
+ })
+ .collect();
+
+ let fn_call = if has_resources {
+ quote! { #fn_name(#fixed_ctx, #(#resource_args),*) }
+ } else {
+ quote! { #fn_name(#fixed_ctx) }
+ };
+
+ // Wrap the function call with modify_res for mutable resources
+ let inner_call = if mut_resources.is_empty() {
+ fn_call
} else {
- let mut wrapped = quote! { #(#fn_body_stmts)* };
+ let mut wrapped = fn_call;
for res in mut_resources.iter().rev() {
let var_name = &res.var_name;
let inner_type = &res.inner_type;
@@ -117,10 +128,10 @@ pub fn completion_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
let comp_body = if has_resources {
quote! {
#(#immut_resource_stmts)*
- #wrapped_body
+ #inner_call
}
} else {
- quote! { #(#fn_body_stmts)* }
+ quote! { #inner_call }
};
// Generate the struct and implementation
@@ -135,7 +146,7 @@ pub fn completion_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
impl ::mingling::Completion for #struct_name {
type Previous = #previous_type_path;
- fn comp(#ctx_pat: &::mingling::ShellContext) #output {
+ fn comp(#fixed_ctx: &::mingling::ShellContext) #output {
#comp_body
}
}
diff --git a/mingling_macros/src/dispatcher_clap.rs b/mingling_macros/src/attr/dispatcher_clap.rs
index 0945e31..40f7d47 100644
--- a/mingling_macros/src/dispatcher_clap.rs
+++ b/mingling_macros/src/attr/dispatcher_clap.rs
@@ -93,7 +93,7 @@ impl Parse for DispatcherClapInput {
}
#[cfg(feature = "clap")]
-pub fn dispatcher_clap_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
+pub(crate) fn dispatcher_clap_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
let attr_input = parse_macro_input!(attr as DispatcherClapInput);
let input_struct = parse_macro_input!(item as ItemStruct);
let struct_name = &input_struct.ident;
@@ -108,23 +108,23 @@ pub fn dispatcher_clap_attr(attr: TokenStream, item: TokenStream) -> TokenStream
let begin_body = if let Some(ref error_struct) = options.error_struct {
quote! {
if ::mingling::this::<#program_path>().user_context.help {
- return #struct_name::default().to_chain();
+ return ::mingling::Routable::<#program_path>::to_chain(#struct_name::default());
}
match <#struct_name as ::clap::Parser>::try_parse_from(clap_args) {
- Ok(parsed) => parsed.to_chain(),
+ Ok(parsed) => ::mingling::Routable::<#program_path>::to_chain(parsed),
Err(e) => {
- return #error_struct::new(format!("{}", e.render().ansi())).to_render()
+ return ::mingling::Routable::<#program_path>::to_render(#error_struct::new(format!("{}", e.render().ansi())))
},
}
}
} else {
quote! {
if ::mingling::this::<#program_path>().user_context.help {
- return #struct_name::default().to_chain();
+ return ::mingling::Routable::<#program_path>::to_chain(#struct_name::default());
}
let parsed = <#struct_name as ::clap::Parser>::try_parse_from(clap_args)
.unwrap_or_else(|e| e.exit());
- parsed.to_chain()
+ ::mingling::Routable::<#program_path>::to_chain(parsed)
}
};
@@ -144,7 +144,7 @@ pub fn dispatcher_clap_attr(attr: TokenStream, item: TokenStream) -> TokenStream
Some(quote! {
#[allow(non_snake_case)]
#[::mingling::macros::help]
- pub fn #help_fn_name(_prev: #struct_name) -> ::mingling::RenderResult {
+ pub(crate) fn #help_fn_name(_prev: #struct_name) -> ::mingling::RenderResult {
use std::io::Write;
use clap::ColorChoice;
@@ -189,7 +189,7 @@ pub fn dispatcher_clap_attr(attr: TokenStream, item: TokenStream) -> TokenStream
// Generate the dispatcher struct
#[doc(hidden)]
#[derive(Default)]
- pub struct #dispatcher_struct;
+ pub(crate) struct #dispatcher_struct;
impl ::mingling::Dispatcher<#program_path> for #dispatcher_struct {
fn node(&self) -> ::mingling::Node {
diff --git a/mingling_macros/src/help.rs b/mingling_macros/src/attr/help.rs
index 1903d07..aa7bc88 100644
--- a/mingling_macros/src/help.rs
+++ b/mingling_macros/src/attr/help.rs
@@ -1,38 +1,20 @@
use proc_macro::TokenStream;
use quote::{ToTokens, quote};
use syn::spanned::Spanned;
-use syn::{Ident, ItemFn, ReturnType, Type, TypePath, parse_macro_input};
+use syn::{Ident, ItemFn, Pat, ReturnType, Signature, TypePath, parse_macro_input};
use crate::get_global_set;
use crate::res_injection::{extract_args_info, generate_immut_resource_bindings};
-/// Validates that the function returns `::mingling::RenderResult`.
-fn validate_render_result_return(sig: &syn::Signature) -> syn::Result<()> {
+/// Extracts the user's return type, returning `None` for no return type.
+fn extract_user_return_type(sig: &Signature) -> Option<proc_macro2::TokenStream> {
match &sig.output {
- ReturnType::Type(_, ty) => match &**ty {
- Type::Path(type_path) => {
- let last_seg = type_path.path.segments.last().map(|s| s.ident.to_string());
- match last_seg.as_deref() {
- Some("RenderResult") => Ok(()),
- _ => Err(syn::Error::new(
- ty.span(),
- "Help function must return `RenderResult`",
- )),
- }
- }
- _ => Err(syn::Error::new(
- ty.span(),
- "Help function must return `RenderResult`",
- )),
- },
- ReturnType::Default => Err(syn::Error::new(
- sig.span(),
- "Help function must have a return type `-> RenderResult`",
- )),
+ ReturnType::Type(_, ty) => Some(quote! { #ty }),
+ ReturnType::Default => None,
}
}
-pub fn help_attr(item: TokenStream) -> TokenStream {
+pub(crate) fn help_attr(item: TokenStream) -> TokenStream {
// Parse the function item
let input_fn = parse_macro_input!(item as ItemFn);
@@ -43,16 +25,14 @@ pub fn help_attr(item: TokenStream) -> TokenStream {
.into();
}
- // Extract the entry type, parameter name, and resource injection params
- let (prev_param, entry_type, resources) = match extract_args_info(&input_fn.sig) {
+ // Extract the entry type and resource injection params
+ let (_, entry_type, resources) = match extract_args_info(&input_fn.sig) {
Ok(info) => info,
Err(e) => return e.to_compile_error().into(),
};
- // Validate return type is RenderResult
- if let Err(e) = validate_render_result_return(&input_fn.sig) {
- return e.to_compile_error().into();
- }
+ // Determine the user's return type for preserving the original function
+ let user_return_type = extract_user_return_type(&input_fn.sig);
// Get the function body
let fn_body = &input_fn.block;
@@ -69,8 +49,8 @@ pub fn help_attr(item: TokenStream) -> TokenStream {
let fn_name = &input_fn.sig.ident;
// Get original inputs to keep the original function
-
let original_inputs = input_fn.sig.inputs.clone();
+ let original_return_type = user_return_type.clone().unwrap_or(quote! { () });
// Generate internal name using snake_case
let internal_name = format!(
@@ -86,13 +66,31 @@ pub fn help_attr(item: TokenStream) -> TokenStream {
// Generate immutable resource bindings
let immut_resource_stmts = generate_immut_resource_bindings(resources.iter(), &program_type);
- // Build the render_help body with resource injection
- // Use modify_res for mutable resources same pattern as renderer.rs
+ // Build the call to the original function with resource arguments injected
+ let resource_args: Vec<_> = resources
+ .iter()
+ .map(|res| {
+ let var_name = &res.var_name;
+ quote! { #var_name }
+ })
+ .collect();
+
+ // Use a fixed parameter name `prev` for the trait method, regardless of
+ // the user's original parameter name (which may be `_` and cannot be
+ // referenced in expression position).
+ let fixed_prev: Pat = syn::parse_quote!(prev);
- let wrapped_body = if mut_resources.is_empty() {
- quote! { #(#fn_body_stmts)* }
+ let fn_call = if has_resources {
+ quote! { #fn_name(#fixed_prev, #(#resource_args),*) }
} else {
- let mut wrapped = quote! { #(#fn_body_stmts)* };
+ quote! { #fn_name(#fixed_prev) }
+ };
+
+ // Wrap the function call with modify_res for mutable resources
+ let inner_call = if mut_resources.is_empty() {
+ fn_call
+ } else {
+ let mut wrapped = fn_call;
for res in mut_resources.iter().rev() {
let var_name = &res.var_name;
let inner_type = &res.inner_type;
@@ -108,10 +106,10 @@ pub fn help_attr(item: TokenStream) -> TokenStream {
let help_render_body = if has_resources {
quote! {
#(#immut_resource_stmts)*
- #wrapped_body
+ #inner_call
}
} else {
- quote! { #(#fn_body_stmts)* }
+ quote! { #inner_call }
};
// Register the help request mapping
@@ -148,18 +146,18 @@ pub fn help_attr(item: TokenStream) -> TokenStream {
impl ::mingling::HelpRequest for #struct_name {
type Entry = #entry_type;
- fn render_help(#prev_param: Self::Entry) -> ::mingling::RenderResult {
- #help_render_body
+ fn render_help(#fixed_prev: Self::Entry) -> ::mingling::RenderResult {
+ let __help_result = { #help_render_body };
+ ::std::convert::Into::into(__help_result)
}
}
::mingling::macros::register_help!(#entry_type, #struct_name);
// Keep the original function unchanged
-
#(#fn_attrs)*
- #vis fn #fn_name(#original_inputs) -> ::mingling::RenderResult {
- #fn_body
+ #vis fn #fn_name(#original_inputs) -> #original_return_type {
+ #(#fn_body_stmts)*
}
};
@@ -179,7 +177,7 @@ fn build_help_entry(struct_name: &Ident, entry_type: &TypePath) -> proc_macro2::
}
}
-pub fn register_help(input: TokenStream) -> TokenStream {
+pub(crate) fn register_help(input: TokenStream) -> TokenStream {
// Parse the input as a comma-separated list of arguments
let input_parsed = syn::parse_macro_input!(input with syn::punctuated::Punctuated<syn::Expr, syn::Token![,]>::parse_terminated);
diff --git a/mingling_macros/src/program_setup.rs b/mingling_macros/src/attr/program_setup.rs
index 7fd9d16..dee5a1c 100644
--- a/mingling_macros/src/program_setup.rs
+++ b/mingling_macros/src/attr/program_setup.rs
@@ -47,7 +47,7 @@ fn extract_return_type(sig: &Signature) -> syn::Result<()> {
}
}
-pub fn setup_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
+pub(crate) fn setup_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
// #[program_setup] takes no arguments; always use the default program path
let _ = attr;
let program_path = crate::default_program_path();
diff --git a/mingling_macros/src/renderer.rs b/mingling_macros/src/attr/renderer.rs
index f47511c..828dc00 100644
--- a/mingling_macros/src/renderer.rs
+++ b/mingling_macros/src/attr/renderer.rs
@@ -1,43 +1,21 @@
use proc_macro::TokenStream;
use quote::{ToTokens, quote};
use syn::spanned::Spanned;
-use syn::{ItemFn, ReturnType, Signature, Type, TypePath, parse_macro_input};
+use syn::{ItemFn, Pat, ReturnType, Signature, TypePath, parse_macro_input};
use crate::get_global_set;
use crate::res_injection::{extract_args_info, generate_immut_resource_bindings};
-/// Validates that the function returns `::mingling::RenderResult`.
-fn validate_render_result_return(sig: &Signature) -> syn::Result<()> {
+/// Extracts the user's return type, returning `None` for no return type.
+fn extract_user_return_type(sig: &Signature) -> Option<proc_macro2::TokenStream> {
match &sig.output {
- ReturnType::Type(_, ty) => {
- // Check if the return type is RenderResult
- match &**ty {
- Type::Path(type_path) => {
- let segments = &type_path.path.segments;
- let last_seg = segments.last().map(|s| s.ident.to_string());
- match last_seg.as_deref() {
- Some("RenderResult") => Ok(()),
- _ => Err(syn::Error::new(
- ty.span(),
- "Renderer function must return `RenderResult`",
- )),
- }
- }
- _ => Err(syn::Error::new(
- ty.span(),
- "Renderer function must return `RenderResult`",
- )),
- }
- }
- ReturnType::Default => Err(syn::Error::new(
- sig.span(),
- "Renderer function must have a return type `-> RenderResult`",
- )),
+ ReturnType::Type(_, ty) => Some(quote! { #ty }),
+ ReturnType::Default => None,
}
}
#[allow(clippy::too_many_lines)]
-pub fn renderer_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
+pub(crate) fn renderer_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
// #[renderer] takes no arguments; always use the default program path
let _ = attr;
let program_path = crate::default_program_path();
@@ -53,16 +31,14 @@ pub fn renderer_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
.into();
}
- // Extract the previous type, parameter name, and resource injection params
- let (prev_param, previous_type, resources) = match extract_args_info(&input_fn.sig) {
+ // Extract the previous type and resource injection params
+ let (_, previous_type, resources) = match extract_args_info(&input_fn.sig) {
Ok(info) => info,
Err(e) => return e.to_compile_error().into(),
};
- // Validate that the function returns RenderResult
- if let Err(e) = validate_render_result_return(&input_fn.sig) {
- return e.to_compile_error().into();
- }
+ // Determine the user's return type and whether it needs to be converted to RenderResult
+ let user_return_type = extract_user_return_type(&input_fn.sig);
// Get function body statements
let fn_body_stmts: Vec<syn::Stmt> = input_fn.block.stmts.clone();
@@ -93,40 +69,59 @@ pub fn renderer_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
let immut_resource_stmts = generate_immut_resource_bindings(resources.iter(), program_type);
let mut_resources: Vec<_> = resources.iter().filter(|r| r.is_mut).collect();
- let inner_body_with_resources = if has_mut_resources {
- let mut wrapped = quote! { #(#fn_body_stmts)* };
+ // Build the call to the original function with resource arguments injected
+ let resource_args: Vec<_> = resources
+ .iter()
+ .map(|res| {
+ let var_name = &res.var_name;
+ quote! { #var_name }
+ })
+ .collect();
+
+ // Use a fixed parameter name `prev` for the trait method, regardless of
+ // the user's original parameter name (which may be `_` and cannot be
+ // referenced in expression position).
+ let fixed_prev: Pat = syn::parse_quote!(prev);
+
+ let fn_call = if has_resources {
+ quote! { #fn_name(#fixed_prev, #(#resource_args),*) }
+ } else {
+ quote! { #fn_name(#fixed_prev) }
+ };
+
+ // Wrap the function call with modify_res for mutable resources
+ let inner_call = if has_mut_resources {
+ let mut wrapped = fn_call;
for res in mut_resources.iter().rev() {
let var_name = &res.var_name;
let inner_type = &res.inner_type;
wrapped = quote! {
- ::mingling::this::<#program_type>().modify_res(|#var_name: &mut #inner_type| {
+ ::mingling::this::<#program_type>()
+ .modify_res(|#var_name: &mut #inner_type| {
#wrapped
})
};
}
wrapped
} else {
- quote! { #(#fn_body_stmts)* }
+ fn_call
};
- // Build the Renderer::render body with resource injection
- // The user's body now directly creates and returns a RenderResult.
+ // Build the Renderer::render body with resource injection.
+ // The trait method injects resources and calls the original function.
let render_fn_body = if has_resources {
quote! {
#(#immut_resource_stmts)*
- #inner_body_with_resources
+ #inner_call
}
} else {
- quote! { #inner_body_with_resources }
+ quote! { #inner_call }
};
// The original function preserves the user's exact signature and body.
// Resource parameters are passed directly by the caller, NOT injected from context.
let original_inputs = input_fn.sig.inputs.clone();
- let original_return_type = match &input_fn.sig.output {
- ReturnType::Type(_, ty) => quote! { #ty },
- ReturnType::Default => unreachable!("Already validated that return type is RenderResult"),
- };
+ let original_return_type = user_return_type.clone().unwrap_or(quote! { () });
let expanded = quote! {
#(#fn_attrs)*
@@ -139,8 +134,9 @@ pub fn renderer_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
impl ::mingling::Renderer for #struct_name {
type Previous = #previous_type;
- fn render(#prev_param: Self::Previous) -> ::mingling::RenderResult {
- #render_fn_body
+ fn render(#fixed_prev: Self::Previous) -> ::mingling::RenderResult {
+ let __renderer_result = { #render_fn_body };
+ ::std::convert::Into::into(__renderer_result)
}
}
@@ -155,7 +151,7 @@ pub fn renderer_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
}
/// Builds the renderer entry for the global renderers list
-pub fn build_renderer_entry(
+pub(crate) fn build_renderer_entry(
struct_name: &syn::Ident,
previous_type: &TypePath,
) -> proc_macro2::TokenStream {
@@ -166,7 +162,7 @@ pub fn build_renderer_entry(
}
/// Builds the renderer existence check entry
-pub fn build_renderer_exist_entry(previous_type: &TypePath) -> proc_macro2::TokenStream {
+pub(crate) fn build_renderer_exist_entry(previous_type: &TypePath) -> proc_macro2::TokenStream {
let enum_variant = &previous_type.path.segments.last().unwrap().ident;
quote! {
Self::#enum_variant => true,
@@ -175,7 +171,9 @@ pub fn build_renderer_exist_entry(previous_type: &TypePath) -> proc_macro2::Toke
/// Builds the structural renderer entry
#[cfg(feature = "structural_renderer")]
-pub fn build_structural_renderer_entry(previous_type: &TypePath) -> proc_macro2::TokenStream {
+pub(crate) fn build_structural_renderer_entry(
+ previous_type: &TypePath,
+) -> proc_macro2::TokenStream {
let enum_variant = &previous_type.path.segments.last().unwrap().ident;
quote! {
Self::#enum_variant => {
@@ -189,7 +187,7 @@ pub fn build_structural_renderer_entry(previous_type: &TypePath) -> proc_macro2:
}
}
-pub fn register_renderer(input: TokenStream) -> TokenStream {
+pub(crate) fn register_renderer(input: TokenStream) -> TokenStream {
// Parse the input as a comma-separated list of arguments
let input_parsed = syn::parse_macro_input!(input with syn::punctuated::Punctuated<syn::Expr, syn::Token![,]>::parse_terminated);
diff --git a/mingling_macros/src/derive.rs b/mingling_macros/src/derive.rs
new file mode 100644
index 0000000..ffa405b
--- /dev/null
+++ b/mingling_macros/src/derive.rs
@@ -0,0 +1,2 @@
+pub(crate) mod enum_tag;
+pub(crate) mod grouped;
diff --git a/mingling_macros/src/enum_tag.rs b/mingling_macros/src/derive/enum_tag.rs
index 6277b69..a7f71f0 100644
--- a/mingling_macros/src/enum_tag.rs
+++ b/mingling_macros/src/derive/enum_tag.rs
@@ -4,7 +4,7 @@ use syn::{
Attribute, Data, DeriveInput, Error, Fields, Ident, LitStr, Result, Variant, parse_macro_input,
};
-pub fn derive_enum_tag(input: TokenStream) -> TokenStream {
+pub(crate) fn derive_enum_tag(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
match derive_enum_tag_impl(input) {
diff --git a/mingling_macros/src/groupped.rs b/mingling_macros/src/derive/grouped.rs
index 8aee003..307aab6 100644
--- a/mingling_macros/src/groupped.rs
+++ b/mingling_macros/src/derive/grouped.rs
@@ -2,7 +2,7 @@ use proc_macro::TokenStream;
use quote::quote;
use syn::{DeriveInput, Ident, parse_macro_input};
-pub fn derive_groupped(input: TokenStream) -> TokenStream {
+pub(crate) fn derive_grouped(input: TokenStream) -> TokenStream {
// Parse the input struct/enum
let input = parse_macro_input!(input as DeriveInput);
let struct_name = input.ident;
@@ -12,11 +12,11 @@ pub fn derive_groupped(input: TokenStream) -> TokenStream {
let any_output_convert_impls =
proc_macro2::TokenStream::from(build_any_output_convert_impls(&struct_name, &group_ident));
- // Generate the Groupped trait implementation
+ // Generate the Grouped trait implementation
let expanded = quote! {
::mingling::macros::register_type!(#struct_name);
- impl ::mingling::Groupped<#group_ident> for #struct_name {
+ impl ::mingling::Grouped<#group_ident> for #struct_name {
fn member_id() -> #group_ident {
#group_ident::#struct_name
}
@@ -29,7 +29,7 @@ pub fn derive_groupped(input: TokenStream) -> TokenStream {
}
#[cfg(feature = "structural_renderer")]
-pub fn derive_groupped_serialize(input: TokenStream) -> TokenStream {
+pub fn derive_grouped_serialize(input: TokenStream) -> TokenStream {
// Parse the input struct/enum
let input_parsed = parse_macro_input!(input as DeriveInput);
let struct_name = input_parsed.ident.clone();
@@ -39,14 +39,14 @@ pub fn derive_groupped_serialize(input: TokenStream) -> TokenStream {
let any_output_convert_impls =
proc_macro2::TokenStream::from(build_any_output_convert_impls(&struct_name, &group_ident));
- // Generate both Serialize and Groupped implementations
+ // Generate both Serialize and Grouped implementations
let expanded = quote! {
#[derive(serde::Serialize)]
#input_parsed
::mingling::macros::register_type!(#struct_name);
- impl ::mingling::Groupped<#group_ident> for #struct_name {
+ impl ::mingling::Grouped<#group_ident> for #struct_name {
fn member_id() -> #group_ident {
#group_ident::#struct_name
}
diff --git a/mingling_macros/src/extensions.rs b/mingling_macros/src/extensions.rs
new file mode 100644
index 0000000..022761b
--- /dev/null
+++ b/mingling_macros/src/extensions.rs
@@ -0,0 +1,119 @@
+//! Extension point mechanism for Mingling attribute macros.
+//!
+//! This module provides a way for attribute macros like `#[chain]`, `#[renderer]`,
+//! `#[help]`, and `#[completion]` to accept extension identifiers that are
+//! applied as outer attributes before the bare macro.
+
+use proc_macro::TokenStream;
+use quote::quote;
+use syn::parse::{Parse, ParseStream};
+use syn::{Ident, Token};
+
+/// Extension: `#[routeify]` — transforms `expr?` into `route!(expr)`.
+#[cfg(feature = "extra_macros")]
+pub(crate) mod routeify;
+
+/// Extension: `#[buffer]` — wraps a unit-returning function to return `RenderResult`.
+pub(crate) mod buffer;
+
+/// Parsed extensions from an attribute macro like `#[chain(routeify, other_ext)]`.
+pub(crate) struct Extensions {
+ pub(crate) exts: Vec<Ident>,
+}
+
+impl Parse for Extensions {
+ fn parse(input: ParseStream) -> syn::Result<Self> {
+ let mut exts = Vec::new();
+ while !input.is_empty() {
+ let ident: Ident = input.parse()?;
+ exts.push(ident);
+ if input.peek(Token![,]) {
+ let _ = input.parse::<Token![,]>();
+ }
+ }
+ Ok(Extensions { exts })
+ }
+}
+
+/// Parsed extensions for `#[completion(EntryType, routeify, ...)]`.
+#[cfg(feature = "comp")]
+pub(crate) struct CompletionExt {
+ pub(crate) entry_type: proc_macro2::TokenStream,
+ pub(crate) exts: Vec<Ident>,
+}
+
+#[cfg(feature = "comp")]
+impl Parse for CompletionExt {
+ fn parse(input: ParseStream) -> syn::Result<Self> {
+ let entry_type: proc_macro2::TokenStream = input.parse()?;
+ let mut exts = Vec::new();
+ while !input.is_empty() {
+ let _ = input.parse::<Token![,]>();
+ if input.is_empty() {
+ break;
+ }
+ let ident: Ident = input.parse()?;
+ exts.push(ident);
+ }
+ Ok(CompletionExt { entry_type, exts })
+ }
+}
+
+/// Generates a re-dispatch token stream for attribute macros that take **no fixed arguments**
+/// (chain, renderer, help).
+pub(crate) fn try_redispatch_simple(
+ attr: TokenStream,
+ item: &TokenStream,
+ bare_attr_name: &str,
+) -> Option<TokenStream> {
+ if attr.is_empty() {
+ return None;
+ }
+
+ let exts: Extensions = syn::parse(attr).ok()?;
+ if exts.exts.is_empty() {
+ return None;
+ }
+
+ let bare = Ident::new(bare_attr_name, proc_macro2::Span::call_site());
+ let exts = &exts.exts;
+ let item = proc_macro2::TokenStream::from(item.clone());
+
+ Some(
+ quote! {
+ #(#[#exts])*
+ #[#bare]
+ #item
+ }
+ .into(),
+ )
+}
+
+/// Generates a re-dispatch token stream for `#[completion(EntryType, ...)]`.
+#[cfg(feature = "comp")]
+pub(crate) fn try_redispatch_completion(
+ attr: TokenStream,
+ item: &TokenStream,
+) -> Option<TokenStream> {
+ if attr.is_empty() {
+ return None;
+ }
+
+ let parsed: CompletionExt = syn::parse(attr).ok()?;
+ if parsed.exts.is_empty() {
+ return None;
+ }
+
+ let entry_type = &parsed.entry_type;
+ let exts = &parsed.exts;
+ let item = proc_macro2::TokenStream::from(item.clone());
+
+ Some(
+ quote! {
+ #(#[#exts])*
+ #[::mingling::macros::completion(#entry_type)]
+ #item
+ }
+ .into(),
+ )
+}
diff --git a/mingling_macros/src/extensions/buffer.rs b/mingling_macros/src/extensions/buffer.rs
new file mode 100644
index 0000000..27eb612
--- /dev/null
+++ b/mingling_macros/src/extensions/buffer.rs
@@ -0,0 +1,95 @@
+use proc_macro::TokenStream;
+use quote::quote;
+use syn::spanned::Spanned;
+use syn::{ItemFn, ReturnType, Type, parse_macro_input};
+
+/// Checks whether the return type is unit `()`.
+fn is_unit_return_type(sig: &syn::Signature) -> bool {
+ match &sig.output {
+ ReturnType::Type(_, ty) => match &**ty {
+ Type::Tuple(tuple) => tuple.elems.is_empty(),
+ _ => false,
+ },
+ ReturnType::Default => true,
+ }
+}
+
+/// The `#[buffer]` attribute macro.
+///
+/// Wraps a unit-returning function to produce a `::mingling::RenderResult` by
+/// injecting a local `__render_result_buffer` variable. Inside the function
+/// body, the `r_print!` / `r_println!` macros can write into the buffer.
+///
+/// # Example
+///
+/// ```rust,ignore
+/// use mingling::macros::{buffer, r_println};
+///
+/// #[buffer]
+/// fn render_greeting(prev: Greeting) {
+/// r_println!("Hello, {}!", *prev);
+/// }
+/// ```
+///
+/// Expands to:
+///
+/// ```rust,ignore
+/// fn render_greeting(prev: Greeting) -> mingling::RenderResult {
+/// let mut __render_result_buffer = mingling::RenderResult::new();
+/// {
+/// r_println!("Hello, {}!", *prev);
+/// }
+/// __render_result_buffer
+/// }
+/// ```
+pub(crate) fn buffer_impl(attr: TokenStream, item: TokenStream) -> TokenStream {
+ // Reject non-empty attribute arguments; #[buffer] must be bare
+ if !attr.is_empty() {
+ return syn::Error::new(
+ attr.into_iter().next().unwrap().span().into(),
+ "#[buffer] does not accept arguments",
+ )
+ .to_compile_error()
+ .into();
+ }
+
+ // Parse the function item
+ let input_fn = parse_macro_input!(item as ItemFn);
+
+ // Validate the function is not async
+ if input_fn.sig.asyncness.is_some() {
+ return syn::Error::new(input_fn.sig.span(), "Buffer function cannot be async")
+ .to_compile_error()
+ .into();
+ }
+
+ // Validate return type is unit
+ if !is_unit_return_type(&input_fn.sig) {
+ return syn::Error::new(
+ input_fn.sig.span(),
+ "#[buffer] function must not have a return value (must return `()`)",
+ )
+ .to_compile_error()
+ .into();
+ }
+
+ // Get function attributes (excluding the buffer attribute)
+ let mut fn_attrs = input_fn.attrs.clone();
+ fn_attrs.retain(|attr| !attr.path().is_ident("buffer"));
+
+ let vis = &input_fn.vis;
+ let fn_name = &input_fn.sig.ident;
+ let inputs = &input_fn.sig.inputs;
+ let fn_body = &input_fn.block;
+
+ let expanded = quote! {
+ #(#fn_attrs)*
+ #vis fn #fn_name(#inputs) -> ::mingling::RenderResult {
+ let mut __render_result_buffer = ::mingling::RenderResult::new();
+ #fn_body
+ __render_result_buffer
+ }
+ };
+
+ expanded.into()
+}
diff --git a/mingling_macros/src/extensions/routeify.rs b/mingling_macros/src/extensions/routeify.rs
new file mode 100644
index 0000000..7cded54
--- /dev/null
+++ b/mingling_macros/src/extensions/routeify.rs
@@ -0,0 +1,46 @@
+//! The `#[routeify]` extension — transforms `expr?` into `route!(expr)`.
+//!
+//! Designed as an extension for the Mingling attribute macro system, intended
+//! to be used with `#[chain(routeify)]` or standalone as `#[routeify]`.
+//!
+//! # How it works
+//!
+//! The macro parses the function AST and replaces every `Expr::Try` node with an
+//! equivalent `route!(expr)` invocation.
+
+use proc_macro::TokenStream;
+use quote::ToTokens;
+use syn::spanned::Spanned;
+use syn::visit_mut::VisitMut;
+use syn::{Expr, ItemFn, parse_macro_input};
+
+struct RouteifyTransform;
+
+impl VisitMut for RouteifyTransform {
+ fn visit_expr_mut(&mut self, expr: &mut Expr) {
+ syn::visit_mut::visit_expr_mut(self, expr);
+
+ if let Expr::Try(try_expr) = expr {
+ let inner = &*try_expr.expr;
+ let inner_tokens = inner.to_token_stream();
+
+ // Set the span of the generated `route` ident to the `?` token's span,
+ // so that rust-analyzer resolves the `?` position to the `route!` macro
+ // instead of the standard Try trait, showing the route macro's docs on hover.
+ let q_span = try_expr.question_token.span();
+ let route_ident = proc_macro2::Ident::new("route", q_span);
+
+ if let Ok(macro_expr) = syn::parse2::<Expr>(quote::quote! {
+ ::mingling::macros::#route_ident!(#inner_tokens)
+ }) {
+ *expr = macro_expr;
+ }
+ }
+ }
+}
+
+pub(crate) fn routeify_impl(_attr: TokenStream, item: TokenStream) -> TokenStream {
+ let mut input_fn = parse_macro_input!(item as ItemFn);
+ RouteifyTransform.visit_item_fn_mut(&mut input_fn);
+ input_fn.to_token_stream().into()
+}
diff --git a/mingling_macros/src/func.rs b/mingling_macros/src/func.rs
new file mode 100644
index 0000000..33bb094
--- /dev/null
+++ b/mingling_macros/src/func.rs
@@ -0,0 +1,13 @@
+pub(crate) mod dispatcher;
+#[cfg(feature = "extra_macros")]
+pub(crate) mod entry;
+pub(crate) mod gen_program;
+#[cfg(feature = "extra_macros")]
+pub(crate) mod group;
+pub(crate) mod node;
+pub(crate) mod pack;
+#[cfg(feature = "extra_macros")]
+pub(crate) mod pack_err;
+pub(crate) mod r_print;
+#[cfg(feature = "comp")]
+pub(crate) mod suggest;
diff --git a/mingling_macros/src/dispatcher.rs b/mingling_macros/src/func/dispatcher.rs
index 2a7c850..a61dd26 100644
--- a/mingling_macros/src/dispatcher.rs
+++ b/mingling_macros/src/func/dispatcher.rs
@@ -75,7 +75,7 @@ impl Parse for DispatcherChainInput {
// are nearly identical and could benefit from refactoring into common helper functions.
#[allow(clippy::too_many_lines)]
-pub fn dispatcher(input: TokenStream) -> TokenStream {
+pub(crate) fn dispatcher(input: TokenStream) -> TokenStream {
// Parse the input
let dispatcher_input = syn::parse_macro_input!(input as DispatcherChainInput);
@@ -134,8 +134,8 @@ pub fn dispatcher(input: TokenStream) -> TokenStream {
::mingling::macros::node!(#command_name_str)
}
fn begin(&self, args: Vec<String>) -> ::mingling::ChainProcess<#program_type> {
- use ::mingling::Groupped;
- #pack::new(args).to_chain()
+ use ::mingling::Grouped;
+ ::mingling::Routable::to_chain(#pack::new(args))
}
fn clone_dispatcher(&self) -> Box<dyn ::mingling::Dispatcher<#program_type>> {
Box::new(#command_struct)
@@ -209,7 +209,7 @@ impl Parse for RegisterDispatcherInput {
}
#[cfg(feature = "dispatch_tree")]
-pub fn register_dispatcher(input: TokenStream) -> TokenStream {
+pub(crate) fn register_dispatcher(input: TokenStream) -> TokenStream {
let RegisterDispatcherInput {
node_name,
dispatcher_type,
@@ -243,7 +243,7 @@ pub fn register_dispatcher(input: TokenStream) -> TokenStream {
}
#[cfg(not(feature = "dispatch_tree"))]
-pub fn register_dispatcher(_input: TokenStream) -> TokenStream {
+pub(crate) fn register_dispatcher(_input: TokenStream) -> TokenStream {
quote! {}.into()
}
diff --git a/mingling_macros/src/entry.rs b/mingling_macros/src/func/entry.rs
index 2ac5d6b..35209e5 100644
--- a/mingling_macros/src/entry.rs
+++ b/mingling_macros/src/func/entry.rs
@@ -39,7 +39,7 @@ fn parse_strings(input: &syn::parse::ParseBuffer) -> syn::Result<Vec<String>> {
Ok(strings)
}
-pub fn entry(input: TokenStream) -> TokenStream {
+pub(crate) fn entry(input: TokenStream) -> TokenStream {
let parsed = parse_macro_input!(input as EntryInput);
let strings = match &parsed {
diff --git a/mingling_macros/src/func/gen_program.rs b/mingling_macros/src/func/gen_program.rs
new file mode 100644
index 0000000..f12cbd7
--- /dev/null
+++ b/mingling_macros/src/func/gen_program.rs
@@ -0,0 +1,624 @@
+use std::collections::HashMap;
+
+use proc_macro::TokenStream;
+use quote::quote;
+use syn::parse_macro_input;
+
+use crate::CHAINS;
+use crate::CHAINS_EXIST;
+#[cfg(feature = "dispatch_tree")]
+use crate::COMPILE_TIME_DISPATCHERS;
+#[cfg(feature = "comp")]
+use crate::COMPLETIONS;
+use crate::HELP_REQUESTS;
+use crate::PACKED_TYPES;
+use crate::RENDERERS;
+use crate::RENDERERS_EXIST;
+#[cfg(feature = "structural_renderer")]
+use crate::STRUCTURAL_RENDERERS;
+use crate::attr::{chain, renderer};
+use crate::get_global_set;
+#[cfg(feature = "dispatch_tree")]
+use crate::systems::dispatch_tree_gen;
+
+#[cfg(feature = "async")]
+const ASYNC_ENABLED: bool = true;
+#[cfg(not(feature = "async"))]
+const ASYNC_ENABLED: bool = false;
+
+/// Parses an entry of the format `StructName => EnumVariant,` into a pair of idents.
+fn parse_entry_pair(entry: &proc_macro2::TokenStream) -> (proc_macro2::Ident, proc_macro2::Ident) {
+ let s = entry.to_string();
+ let arrow_idx = s
+ .find("=>")
+ .unwrap_or_else(|| panic!("Entry missing '=>': {s}"));
+ let struct_str = s[..arrow_idx].trim();
+ let variant_str = s[arrow_idx + 2..].trim().trim_end_matches(',').trim();
+ let struct_ident = proc_macro2::Ident::new(struct_str, proc_macro2::Span::call_site());
+ let variant_ident = proc_macro2::Ident::new(variant_str, proc_macro2::Span::call_site());
+ (struct_ident, variant_ident)
+}
+
+/// Loads the pathf type mapping from `$OUT_DIR/{crate}/type_using.rs`.
+/// Always compiled; returns None when pathf feature is not enabled,
+/// or when the file does not exist / fails to load.
+fn load_pathf_map() -> Option<std::collections::HashMap<String, String>> {
+ if !cfg!(feature = "pathf") {
+ return None;
+ }
+ let out_dir = std::env::var("OUT_DIR").ok()?;
+ let crate_name = std::env::var("CARGO_PKG_NAME").ok()?;
+ let path = std::path::Path::new(&out_dir)
+ .join(&crate_name)
+ .join("type_using.rs");
+ let content = std::fs::read_to_string(&path).ok()?;
+ Some(
+ content
+ .lines()
+ .filter_map(|line| {
+ let line = line.trim();
+ if let Some(rest) = line.strip_prefix("use ") {
+ let path = rest.strip_suffix(';').unwrap_or(rest);
+ if let Some((_mod, type_name)) = path.rsplit_once("::") {
+ return Some((type_name.to_string(), path.to_string()));
+ }
+ }
+ None
+ })
+ .collect(),
+ )
+}
+
+/// Resolves a type name to its full path token stream using the pathf mapping.
+pub(crate) fn resolve_type(
+ name: &str,
+ map: &std::collections::HashMap<String, String>,
+) -> proc_macro2::TokenStream {
+ if let Some(full_path) = map.get(name) {
+ syn::parse_str::<proc_macro2::TokenStream>(full_path).unwrap_or_else(|_| {
+ let ident = proc_macro2::Ident::new(name, proc_macro2::Span::call_site());
+ quote! { #ident }
+ })
+ } else {
+ let ident = proc_macro2::Ident::new(name, proc_macro2::Span::call_site());
+ quote! { #ident }
+ }
+}
+
+pub(crate) fn gen_program_impl(_input: TokenStream) -> TokenStream {
+ #[cfg(feature = "comp")]
+ let comp_gen = quote! {
+ ::mingling::macros::program_comp_gen!();
+ };
+
+ #[cfg(not(feature = "comp"))]
+ let comp_gen = quote! {};
+
+ TokenStream::from(quote! {
+ /// Alias for the current program type `crate::ThisProgram`
+ pub type Next = ::mingling::ChainProcess<crate::ThisProgram>;
+
+ impl ::mingling::Routable<crate::ThisProgram> for ::mingling::ChainProcess<crate::ThisProgram>
+ {
+ fn to_chain(self) -> ::mingling::ChainProcess<crate::ThisProgram> {
+ match self {
+ ::mingling::ChainProcess::Ok((any, _)) => {
+ ::mingling::ChainProcess::Ok((any, mingling::NextProcess::Chain))
+ }
+ other => other,
+ }
+ }
+
+ fn to_render(self) -> ::mingling::ChainProcess<crate::ThisProgram> {
+ match self {
+ ::mingling::ChainProcess::Ok((any, _)) => {
+ ::mingling::ChainProcess::Ok((any, mingling::NextProcess::Renderer))
+ }
+ other => other,
+ }
+ }
+ }
+
+ #comp_gen
+ ::mingling::macros::program_fallback_gen!();
+ ::mingling::macros::program_final_gen!();
+ })
+}
+
+#[cfg(feature = "comp")]
+pub(crate) fn program_comp_gen_impl(_input: TokenStream) -> TokenStream {
+ #[cfg(feature = "async")]
+ let fn_exec_comp = quote! {
+ #[doc(hidden)]
+ #[::mingling::macros::chain]
+ pub async fn __exec_completion(prev: CompletionContext) -> Next {
+ use ::mingling::Grouped;
+
+ let read_ctx = ::mingling::ShellContext::try_from(prev.inner);
+ match read_ctx {
+ Ok(ctx) => {
+ let suggest = ::mingling::CompletionHelper::exec_completion::<crate::ThisProgram>(&ctx);
+ ::mingling::Routable::<crate::ThisProgram>::to_render(crate::CompletionSuggest::new((ctx, suggest)))
+ }
+ Err(_) => std::process::exit(1),
+ }
+ }
+ };
+
+ #[cfg(not(feature = "async"))]
+ let fn_exec_comp = quote! {
+ #[doc(hidden)]
+ #[::mingling::macros::chain]
+ pub fn __exec_completion(prev: CompletionContext) -> Next {
+ use ::mingling::Grouped;
+
+ let read_ctx = ::mingling::ShellContext::try_from(prev.inner);
+ match read_ctx {
+ Ok(ctx) => {
+ let suggest = ::mingling::CompletionHelper::exec_completion::<crate::ThisProgram>(&ctx);
+ ::mingling::Routable::<crate::ThisProgram>::to_render(crate::CompletionSuggest::new((ctx, suggest)))
+ }
+ Err(_) => std::process::exit(1),
+ }
+ }
+ };
+
+ #[cfg(feature = "dispatch_tree")]
+ let internal_dispatcher_comp = quote! {
+ use __internal_completion_mod::__internal_dispatcher_comp;
+ };
+
+ #[cfg(not(feature = "dispatch_tree"))]
+ let internal_dispatcher_comp = quote! {};
+
+ let comp_dispatcher = quote! {
+ #[doc(hidden)]
+ mod __internal_completion_mod {
+ use ::mingling::Grouped;
+ ::mingling::macros::dispatcher!("__comp", CMDCompletion => CompletionContext);
+ ::mingling::macros::pack!(
+ CompletionSuggest = (::mingling::ShellContext, ::mingling::Suggest)
+ );
+ }
+ #internal_dispatcher_comp
+ use __internal_completion_mod::CompletionContext;
+ use __internal_completion_mod::CompletionSuggest;
+ pub use __internal_completion_mod::CMDCompletion;
+
+ #fn_exec_comp
+
+ ::mingling::macros::register_type!(CompletionContext);
+
+ #[allow(unused)]
+ #[doc(hidden)]
+ #[::mingling::macros::renderer]
+ pub fn __render_completion(prev: CompletionSuggest) -> ::mingling::RenderResult {
+ let result = ::mingling::RenderResult::default();
+ let (ctx, suggest) = prev.inner;
+ ::mingling::CompletionHelper::render_suggest::<crate::ThisProgram>(ctx, suggest);
+ result
+ }
+ };
+
+ TokenStream::from(comp_dispatcher)
+}
+
+pub(crate) fn register_type_impl(input: TokenStream) -> TokenStream {
+ let type_ident = parse_macro_input!(input as syn::Ident);
+ let entry_str = type_ident.to_string();
+
+ get_global_set(&PACKED_TYPES)
+ .lock()
+ .unwrap()
+ .insert(entry_str);
+
+ TokenStream::new()
+}
+
+pub(crate) fn register_chain_impl(input: TokenStream) -> TokenStream {
+ chain::register_chain(input)
+}
+
+pub(crate) fn register_renderer_impl(input: TokenStream) -> TokenStream {
+ renderer::register_renderer(input)
+}
+
+pub(crate) fn program_fallback_gen_impl(_input: TokenStream) -> TokenStream {
+ #[cfg(feature = "structural_renderer")]
+ let pack_empty = quote! {
+ #[derive(::serde::Serialize, ::mingling::StructuralData, ::mingling::Grouped, Default)]
+ pub struct ResultEmpty;
+ };
+
+ #[cfg(not(feature = "structural_renderer"))]
+ let pack_empty = quote! {
+ #[derive(::mingling::Grouped, Default)]
+ pub struct ResultEmpty;
+ };
+
+ let expanded = quote! {
+ ::mingling::macros::pack!(ErrorRendererNotFound = String);
+ ::mingling::macros::pack!(ErrorDispatcherNotFound = Vec<String>);
+ #pack_empty
+ };
+ TokenStream::from(expanded)
+}
+
+#[allow(clippy::too_many_lines)]
+pub(crate) fn program_final_gen_impl(_input: TokenStream) -> TokenStream {
+ let name = syn::Ident::new("ThisProgram", proc_macro2::Span::call_site());
+
+ let packed_types = get_global_set(&PACKED_TYPES).lock().unwrap().clone();
+
+ let renderers = get_global_set(&RENDERERS).lock().unwrap().clone();
+ let chains = get_global_set(&CHAINS).lock().unwrap().clone();
+ let renderer_exist = get_global_set(&RENDERERS_EXIST).lock().unwrap().clone();
+ let chain_exist = get_global_set(&CHAINS_EXIST).lock().unwrap().clone();
+
+ #[cfg(feature = "structural_renderer")]
+ let structural_renderers = get_global_set(&STRUCTURAL_RENDERERS)
+ .lock()
+ .unwrap()
+ .clone();
+
+ #[cfg(feature = "comp")]
+ let completions = get_global_set(&COMPLETIONS).lock().unwrap().clone();
+
+ let packed_types: Vec<proc_macro2::TokenStream> = packed_types
+ .iter()
+ .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap())
+ .collect();
+
+ let renderer_tokens: Vec<proc_macro2::TokenStream> = renderers
+ .iter()
+ .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap())
+ .collect();
+
+ let chain_tokens: Vec<proc_macro2::TokenStream> = chains
+ .iter()
+ .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap())
+ .collect();
+
+ let renderer_exist_tokens: Vec<proc_macro2::TokenStream> = renderer_exist
+ .iter()
+ .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap())
+ .collect();
+
+ let chain_exist_tokens: Vec<proc_macro2::TokenStream> = chain_exist
+ .iter()
+ .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap())
+ .collect();
+
+ let pathf_map: Option<HashMap<String, String>> = load_pathf_map();
+
+ let pathf_hint: proc_macro2::TokenStream = if pathf_map.is_none() {
+ quote! {
+ compile_error!(
+"Cannot load type mapping computed by `pathf`.
+If not yet configured, execute `mingling::build::analyze_and_build_type_mapping()` in your `build.rs`.
+
+fn main() {
+ // Require features: [\"builds\", \"pathf\"]
+ mingling::build::analyze_and_build_type_mapping().unwrap();
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\\__ Write to `build.rs`
+
+}
+ ");
+ }
+ } else {
+ quote! {}
+ };
+
+ let pathf_map: HashMap<String, String> = if cfg!(feature = "pathf") {
+ pathf_map.unwrap_or_default()
+ } else {
+ HashMap::new()
+ };
+
+ let pathf_uses: Vec<proc_macro2::TokenStream> = if cfg!(feature = "pathf") {
+ pathf_map
+ .values()
+ .map(|path| format!("use {};", path).parse().unwrap_or_default())
+ .collect()
+ } else {
+ Vec::new()
+ };
+
+ #[cfg(feature = "structural_renderer")]
+ let structural_renderer_tokens: Vec<proc_macro2::TokenStream> = structural_renderers
+ .iter()
+ .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap())
+ .collect();
+
+ #[cfg(feature = "structural_renderer")]
+ let structural_render = quote! {
+ fn structural_render(
+ any: ::mingling::AnyOutput<Self::Enum>,
+ setting: &::mingling::StructuralRendererSetting,
+ ) -> Result<::mingling::RenderResult, ::mingling::error::StructuralRendererSerializeError> {
+ #[allow(unused_imports)]
+ #(#pathf_uses)*
+ match any.member_id {
+ #(#structural_renderer_tokens)*
+ _ => {
+ // Non-structural types: render ResultEmpty (which implements
+ // StructuralData + Serialize) instead of producing nothing.
+ let mut r = ::mingling::RenderResult::default();
+ ::mingling::StructuralRenderer::render(&ResultEmpty, setting, &mut r)?;
+ Ok(r)
+ }
+ }
+ }
+ };
+
+ #[cfg(not(feature = "structural_renderer"))]
+ let structural_render = quote! {};
+
+ #[cfg(feature = "dispatch_tree")]
+ let compile_time_dispatchers: Vec<String> = get_global_set(&COMPILE_TIME_DISPATCHERS)
+ .lock()
+ .unwrap()
+ .clone()
+ .iter()
+ .cloned()
+ .collect();
+
+ #[cfg(feature = "dispatch_tree")]
+ let dispatch_tree_nodes = {
+ let entries: Vec<(String, String, String)> = compile_time_dispatchers
+ .iter()
+ .filter_map(|entry| {
+ let parts: Vec<&str> = entry.split(':').collect();
+ if parts.len() == 3 {
+ Some((
+ parts[0].to_string(),
+ parts[1].to_string(),
+ parts[2].to_string(),
+ ))
+ } else {
+ None
+ }
+ })
+ .collect();
+
+ let get_nodes_fn = dispatch_tree_gen::gen_get_nodes(&entries, &pathf_map);
+ let dispatch_trie_fn = dispatch_tree_gen::gen_dispatch_args_trie(&entries, &pathf_map);
+
+ quote! {
+ #get_nodes_fn
+ #dispatch_trie_fn
+ }
+ };
+
+ #[cfg(not(feature = "dispatch_tree"))]
+ let dispatch_tree_nodes = quote! {};
+
+ #[cfg(feature = "comp")]
+ let completion_tokens: Vec<proc_macro2::TokenStream> = completions
+ .iter()
+ .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap())
+ .collect();
+
+ #[cfg(feature = "comp")]
+ let comp = quote! {
+ fn do_comp(any: &::mingling::AnyOutput<Self::Enum>, ctx: &::mingling::ShellContext) -> ::mingling::Suggest {
+ #[allow(unused_imports)]
+ #(#pathf_uses)*
+ match any.member_id {
+ #(#completion_tokens)*
+ _ => ::mingling::Suggest::FileCompletion,
+ }
+ }
+ };
+
+ #[cfg(not(feature = "comp"))]
+ let comp = quote! {};
+
+ // Build render function arms from stored entries
+ let render_fn =
+ if renderer_tokens.is_empty() {
+ quote! {
+ fn render(_any: ::mingling::AnyOutput<Self::Enum>) -> ::mingling::RenderResult {
+ ::mingling::RenderResult::default()
+ }
+ }
+ } else {
+ let render_arms: Vec<_> = renderer_tokens.iter().map(|entry| {
+ let (struct_ident, variant_ident) = parse_entry_pair(entry);
+ let downcast_ty = resolve_type(&variant_ident.to_string(), &pathf_map);
+ let resolved_struct = resolve_type(&struct_ident.to_string(), &pathf_map);
+ quote! {
+ Self::#variant_ident => {
+ // SAFETY: The `type_id` check ensures that `any` contains a value of type `#variant_ident`,
+ // so downcasting to `#variant_ident` is safe.
+ let value = unsafe { any.downcast::<#downcast_ty>().unwrap_unchecked() };
+ <#resolved_struct as ::mingling::Renderer>::render(value)
+ }
+ }
+ }).collect();
+ quote! {
+ fn render(any: ::mingling::AnyOutput<Self::Enum>) -> ::mingling::RenderResult {
+ match any.member_id {
+ #(#render_arms)*
+ _ => ::mingling::RenderResult::default(),
+ }
+ }
+ }
+ };
+
+ // Build do_chain function (async and sync versions)
+ let chain_arms_async: Vec<_> = chain_tokens.iter().map(|entry| {
+ let (struct_ident, variant_ident) = parse_entry_pair(entry);
+ let downcast_ty = resolve_type(&variant_ident.to_string(), &pathf_map);
+ let resolved_struct = resolve_type(&struct_ident.to_string(), &pathf_map);
+ quote! {
+ Self::#variant_ident => {
+ // SAFETY: The `type_id` check ensures that `any` contains a value of type `#variant_ident`,
+ // so downcasting to `#variant_ident` is safe.
+ let value = unsafe { any.downcast::<#downcast_ty>().unwrap_unchecked() };
+ let fut = async { <#resolved_struct as ::mingling::Chain<Self::Enum>>::proc(value).await };
+ ::std::boxed::Box::pin(fut)
+ }
+ }
+ }).collect();
+
+ let chain_arms_sync: Vec<_> = chain_tokens
+ .iter()
+ .map(|entry| {
+ let (struct_ident, variant_ident) = parse_entry_pair(entry);
+ let downcast_ty = resolve_type(&variant_ident.to_string(), &pathf_map);
+ let resolved_struct = resolve_type(&struct_ident.to_string(), &pathf_map);
+ quote! {
+ Self::#variant_ident => {
+ // SAFETY: The `type_id` check ensures that `any` contains a value of type `#variant_ident`,
+ // so downcasting to `#variant_ident` is safe.
+ let value = unsafe { any.downcast::<#downcast_ty>().unwrap_unchecked() };
+ <#resolved_struct as ::mingling::Chain<Self::Enum>>::proc(value)
+ }
+ }
+ })
+ .collect();
+
+ let do_chain_fn = if chain_tokens.is_empty() {
+ quote! {
+ fn do_chain(_any: ::mingling::AnyOutput<Self::Enum>) -> ::mingling::ChainProcess<Self::Enum> {
+ ::core::panic!("No chain found for type id")
+ }
+ }
+ } else if ASYNC_ENABLED {
+ quote! {
+ fn do_chain(
+ any: ::mingling::AnyOutput<Self::Enum>,
+ ) -> ::std::pin::Pin<::std::boxed::Box<dyn ::std::future::Future<Output = ::mingling::ChainProcess<Self::Enum>> + ::std::marker::Send>> {
+ match any.member_id {
+ #(#chain_arms_async)*
+ _ => ::core::panic!("No chain found for type id: {:?}", any.type_id),
+ }
+ }
+ }
+ } else {
+ quote! {
+ fn do_chain(
+ any: ::mingling::AnyOutput<Self::Enum>,
+ ) -> ::mingling::ChainProcess<Self::Enum> {
+ match any.member_id {
+ #(#chain_arms_sync)*
+ _ => ::core::panic!("No chain found for type id: {:?}", any.type_id),
+ }
+ }
+ }
+ };
+
+ let help_tokens: Vec<proc_macro2::TokenStream> = get_global_set(&HELP_REQUESTS)
+ .lock()
+ .unwrap()
+ .clone()
+ .iter()
+ .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap())
+ .collect();
+
+ let num_variants = packed_types.len();
+ let repr_type = if u8::try_from(num_variants).is_ok() {
+ quote! { u8 }
+ } else if u16::try_from(num_variants).is_ok() {
+ quote! { u16 }
+ } else if u32::try_from(num_variants).is_ok() {
+ quote! { u32 }
+ } else {
+ quote! { u128 }
+ };
+
+ let expanded = quote! {
+ #pathf_hint
+
+ #[derive(Debug, PartialEq, Eq, Clone)]
+ #[repr(#repr_type)]
+ #[allow(nonstandard_style)]
+ pub enum #name {
+ #(#packed_types),*
+ }
+
+ impl ::std::fmt::Display for #name {
+ fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
+ match self {
+ #(#name::#packed_types => write!(f, stringify!(#packed_types)),)*
+ }
+ }
+ }
+
+ impl ::mingling::ProgramCollect for #name {
+ type Enum = #name;
+ type ErrorDispatcherNotFound = ErrorDispatcherNotFound;
+ type ErrorRendererNotFound = ErrorRendererNotFound;
+ type ResultEmpty = ResultEmpty;
+ fn build_renderer_not_found(member_id: Self::Enum) -> ::mingling::AnyOutput<Self::Enum> {
+ ::mingling::AnyOutput::new(ErrorRendererNotFound::new(member_id.to_string()))
+ }
+ fn build_dispatcher_not_found(args: Vec<String>) -> ::mingling::AnyOutput<Self::Enum> {
+ ::mingling::AnyOutput::new(ErrorDispatcherNotFound::new(args))
+ }
+ fn build_empty_result() -> ::mingling::AnyOutput<Self::Enum> {
+ ::mingling::AnyOutput::new(ResultEmpty)
+ }
+ #render_fn
+ #do_chain_fn
+ fn render_help(any: ::mingling::AnyOutput<Self::Enum>) -> ::mingling::RenderResult {
+ #[allow(unused_imports)]
+ #(#pathf_uses)*
+ match any.member_id {
+ #(#help_tokens)*
+ _ => ::mingling::RenderResult::default(),
+ }
+ }
+ fn has_renderer(any: &::mingling::AnyOutput<Self::Enum>) -> bool {
+ match any.member_id {
+ #(#renderer_exist_tokens)*
+ _ => false
+ }
+ }
+ fn has_chain(any: &::mingling::AnyOutput<Self::Enum>) -> bool {
+ match any.member_id {
+ #(#chain_exist_tokens)*
+ _ => false
+ }
+ }
+ #dispatch_tree_nodes
+ #structural_render
+ #comp
+ }
+
+ impl #name {
+ /// Creates a new `Program<#name>` instance with default configuration.
+ pub fn new() -> ::mingling::Program<#name> {
+ ::mingling::Program::new()
+ }
+
+ /// Returns a static reference to the global `Program<#name>` singleton.
+ pub fn this() -> &'static ::mingling::Program<#name> {
+ &::mingling::this::<#name>()
+ }
+ }
+ };
+
+ // Clear all global registries to prevent stale state in Rust Analyzer
+ get_global_set(&PACKED_TYPES).lock().unwrap().clear();
+ get_global_set(&CHAINS).lock().unwrap().clear();
+ get_global_set(&CHAINS_EXIST).lock().unwrap().clear();
+ get_global_set(&RENDERERS).lock().unwrap().clear();
+ get_global_set(&RENDERERS_EXIST).lock().unwrap().clear();
+ get_global_set(&HELP_REQUESTS).lock().unwrap().clear();
+ #[cfg(feature = "comp")]
+ get_global_set(&COMPLETIONS).lock().unwrap().clear();
+ #[cfg(feature = "dispatch_tree")]
+ get_global_set(&COMPILE_TIME_DISPATCHERS)
+ .lock()
+ .unwrap()
+ .clear();
+ #[cfg(feature = "structural_renderer")]
+ get_global_set(&STRUCTURAL_RENDERERS)
+ .lock()
+ .unwrap()
+ .clear();
+
+ TokenStream::from(expanded)
+}
diff --git a/mingling_macros/src/group_impl.rs b/mingling_macros/src/func/group.rs
index 59da9dd..b865913 100644
--- a/mingling_macros/src/group_impl.rs
+++ b/mingling_macros/src/func/group.rs
@@ -91,7 +91,7 @@ fn gen_type_use(type_path: &TypePath) -> proc_macro2::TokenStream {
}
}
-pub fn group_macro(input: TokenStream) -> TokenStream {
+pub(crate) fn group_macro(input: TokenStream) -> TokenStream {
let input = syn::parse_macro_input!(input as GroupInput);
let is_aliased = matches!(input, GroupInput::Aliased { .. });
@@ -124,7 +124,7 @@ pub fn group_macro(input: TokenStream) -> TokenStream {
quote! {}
};
- // Generate the module with the Groupped implementation
+ // Generate the module with the Grouped implementation
let expanded = quote! {
#alias_stmt
#[allow(non_camel_case_types)]
@@ -133,7 +133,7 @@ pub fn group_macro(input: TokenStream) -> TokenStream {
#type_use
#alias_use
- impl ::mingling::Groupped<__MinglingProgram> for #type_name {
+ impl ::mingling::Grouped<__MinglingProgram> for #type_name {
fn member_id() -> __MinglingProgram {
__MinglingProgram::#type_name
}
diff --git a/mingling_macros/src/node.rs b/mingling_macros/src/func/node.rs
index b3a61c6..1b944a1 100644
--- a/mingling_macros/src/node.rs
+++ b/mingling_macros/src/func/node.rs
@@ -18,7 +18,7 @@ impl Parse for NodeInput {
}
}
-pub fn node(input: TokenStream) -> TokenStream {
+pub(crate) fn node(input: TokenStream) -> TokenStream {
// Parse the input as a string literal
let input_parsed = syn::parse_macro_input!(input as NodeInput);
let path_str = input_parsed.path.value();
diff --git a/mingling_macros/src/pack.rs b/mingling_macros/src/func/pack.rs
index 7f05232..a1a7e6b 100644
--- a/mingling_macros/src/pack.rs
+++ b/mingling_macros/src/func/pack.rs
@@ -25,7 +25,7 @@ impl Parse for PackInput {
}
#[allow(clippy::too_many_lines)]
-pub fn pack(input: TokenStream) -> TokenStream {
+pub(crate) fn pack(input: TokenStream) -> TokenStream {
let pack_input = syn::parse_macro_input!(input as PackInput);
let group_name = crate::default_program_path();
@@ -138,7 +138,7 @@ pub fn pack(input: TokenStream) -> TokenStream {
}
}
- impl ::mingling::Groupped<#group_name> for #type_name {
+ impl ::mingling::Grouped<#group_name> for #type_name {
fn member_id() -> #group_name {
#group_name::#type_name
}
diff --git a/mingling_macros/src/pack_err.rs b/mingling_macros/src/func/pack_err.rs
index ba7cf17..36e550a 100644
--- a/mingling_macros/src/pack_err.rs
+++ b/mingling_macros/src/func/pack_err.rs
@@ -31,7 +31,7 @@ impl syn::parse::Parse for PackErrInput {
}
#[allow(clippy::too_many_lines)]
-pub fn pack_err(input: TokenStream) -> TokenStream {
+pub(crate) fn pack_err(input: TokenStream) -> TokenStream {
let parsed = parse_macro_input!(input as PackErrInput);
match parsed {
@@ -42,7 +42,7 @@ pub fn pack_err(input: TokenStream) -> TokenStream {
// Note: No longer derives Serialize under structural_renderer.
// Use pack_err_structural for structured output support.
let derive = quote! {
- #[derive(::mingling::Groupped)]
+ #[derive(::mingling::Grouped)]
};
let expanded = quote! {
@@ -75,7 +75,7 @@ pub fn pack_err(input: TokenStream) -> TokenStream {
// Note: No longer derives Serialize under structural_renderer.
// Use pack_err_structural for structured output support.
let derive = quote! {
- #[derive(::mingling::Groupped)]
+ #[derive(::mingling::Grouped)]
};
let expanded = quote! {
@@ -123,7 +123,7 @@ pub fn pack_err(input: TokenStream) -> TokenStream {
/// impl ::mingling::__private::StructuralData for ErrorNotFound {}
/// ```
#[cfg(feature = "structural_renderer")]
-pub fn pack_err_structural(input: TokenStream) -> TokenStream {
+pub(crate) fn pack_err_structural(input: TokenStream) -> TokenStream {
let parsed = parse_macro_input!(input as PackErrInput);
let type_name = match &parsed {
@@ -150,7 +150,7 @@ pub fn pack_err_structural(input: TokenStream) -> TokenStream {
let snake_name = snake_case!(&name_str);
let expanded = quote! {
- #[derive(::mingling::Groupped, ::serde::Serialize)]
+ #[derive(::mingling::Grouped, ::serde::Serialize)]
pub struct #type_name {
/// The snake_case name of this error, automatically set at compile time.
pub name: String,
@@ -179,7 +179,7 @@ pub fn pack_err_structural(input: TokenStream) -> TokenStream {
let snake_name = snake_case!(&name_str);
let expanded = quote! {
- #[derive(::mingling::Groupped, ::serde::Serialize)]
+ #[derive(::mingling::Grouped, ::serde::Serialize)]
pub struct #type_name {
/// The snake_case name of this error, automatically set at compile time.
pub name: String,
diff --git a/mingling_macros/src/func/r_print.rs b/mingling_macros/src/func/r_print.rs
new file mode 100644
index 0000000..e81b544
--- /dev/null
+++ b/mingling_macros/src/func/r_print.rs
@@ -0,0 +1,62 @@
+use proc_macro::TokenStream;
+use proc_macro2::TokenStream as TokenStream2;
+use quote::quote;
+use syn::parse::{Parse, ParseStream};
+use syn::{Ident, Token};
+
+/// Parsed input for `r_println!` and `r_print!`.
+///
+/// Two forms:
+/// - `(ident, format_args...)` — explicit buffer
+/// - `(format_args...)` — implicit `__render_result_buffer`
+enum PrintInput {
+ Explicit { dst: Ident, args: TokenStream2 },
+ Implicit { args: TokenStream2 },
+}
+
+impl Parse for PrintInput {
+ fn parse(input: ParseStream) -> syn::Result<Self> {
+ // Peek: if the next token is an ident followed by a comma, it's the explicit form
+ if input.peek(Ident) && input.peek2(Token![,]) {
+ let dst: Ident = input.parse()?;
+ let _comma: Token![,] = input.parse()?;
+ let args: TokenStream2 = input.parse()?;
+ Ok(PrintInput::Explicit { dst, args })
+ } else {
+ let args: TokenStream2 = input.parse()?;
+ Ok(PrintInput::Implicit { args })
+ }
+ }
+}
+
+fn expand_print(input: TokenStream, method: &str) -> TokenStream {
+ let parsed: PrintInput = match syn::parse(input) {
+ Ok(p) => p,
+ Err(e) => return e.to_compile_error().into(),
+ };
+
+ let method_ident = Ident::new(method, proc_macro2::Span::call_site());
+
+ let expanded = match parsed {
+ PrintInput::Explicit { dst, args } => {
+ quote! {
+ #dst.#method_ident(format!(#args))
+ }
+ }
+ PrintInput::Implicit { args } => {
+ quote! {
+ __render_result_buffer.#method_ident(format!(#args))
+ }
+ }
+ };
+
+ expanded.into()
+}
+
+pub(crate) fn r_println(input: TokenStream) -> TokenStream {
+ expand_print(input, "println")
+}
+
+pub(crate) fn r_print(input: TokenStream) -> TokenStream {
+ expand_print(input, "print")
+}
diff --git a/mingling_macros/src/suggest.rs b/mingling_macros/src/func/suggest.rs
index d3ab446..0f2026f 100644
--- a/mingling_macros/src/suggest.rs
+++ b/mingling_macros/src/func/suggest.rs
@@ -35,7 +35,7 @@ impl Parse for SuggestItem {
}
#[cfg(feature = "comp")]
-pub fn suggest(input: TokenStream) -> TokenStream {
+pub(crate) fn suggest(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as SuggestInput);
let mut items = Vec::new();
@@ -71,7 +71,7 @@ pub fn suggest(input: TokenStream) -> TokenStream {
expanded.into()
}
-pub fn suggest_enum(input: TokenStream) -> TokenStream {
+pub(crate) fn suggest_enum(input: TokenStream) -> TokenStream {
let enum_type = parse_macro_input!(input as syn::Type);
let expanded = quote! {{
diff --git a/mingling_macros/src/lib.rs b/mingling_macros/src/lib.rs
index d0f603a..56ed999 100644
--- a/mingling_macros/src/lib.rs
+++ b/mingling_macros/src/lib.rs
@@ -13,7 +13,7 @@
//! ┌──────────────────────────────────────────────────────────────────┐
//! │ Phase 1: Declaration │
//! │ │
-//! │ dispatcher! pack! node! #[derive(Groupped)] │
+//! │ dispatcher! pack! node! #[derive(Grouped)] │
//! │ │ │ │ │ │
//! │ V V V V │
//! │ Declares Wraps a Builds Makes a type │
@@ -55,7 +55,7 @@
//! | `pack_err!` | Creates an error struct with automatic `name` field |
//! | `pack_err_structural!` | Like `pack_err!` but also derives `StructuralData` for structured output |
//! | `entry!` | Creates a packed entry from string literals |
-//! | [`#[derive(Groupped)]`](derive@Groupped) | Makes a type recognizable by the framework's type registry |
+//! | [`#[derive(Grouped)]`](derive@Grouped) | Makes a type recognizable by the framework's type registry |
//! | `#[derive(StructuralData)]` | Marks a type as eligible for structured output (JSON/YAML/etc.) |
//! | [`#[derive(EnumTag)]`](derive@EnumTag) | Adds enum variant metadata (name, description) |
//!
@@ -141,40 +141,50 @@
//! }
//! ```
-use proc_macro::TokenStream;
+#![deny(missing_docs)]
+
+#[cfg(feature = "extra_macros")]
use quote::quote;
+
+#[cfg(feature = "extra_macros")]
+use syn::parse_macro_input;
+
+use proc_macro::TokenStream;
use std::collections::BTreeSet;
use std::sync::Mutex;
use std::sync::OnceLock;
-use syn::parse_macro_input;
-mod chain;
+mod attr;
+mod derive;
+mod func;
+mod systems;
+
+mod extensions;
+mod utils;
+
+// Bring all sub-modules into scope at the old paths so that existing
+// references (e.g. `chain::chain_attr`, `renderer::renderer_attr`)
+// continue to work without any `use`-path changes.
#[cfg(feature = "comp")]
-mod completion;
-#[cfg(feature = "dispatch_tree")]
-mod dispatch_tree_gen;
-mod dispatcher;
+use attr::completion;
#[cfg(feature = "clap")]
-mod dispatcher_clap;
+use attr::dispatcher_clap;
#[cfg(feature = "extra_macros")]
-mod entry;
-mod enum_tag;
+use attr::program_setup;
+use attr::{chain, help, renderer};
+use derive::{enum_tag, grouped};
#[cfg(feature = "extra_macros")]
-mod group_impl;
-mod groupped;
-mod help;
-mod node;
-mod pack;
+use func::entry;
#[cfg(feature = "extra_macros")]
-mod pack_err;
+pub(crate) use func::group as group_impl;
#[cfg(feature = "extra_macros")]
-mod program_setup;
-mod renderer;
-mod res_injection;
-#[cfg(feature = "structural_renderer")]
-mod structural_data;
+use func::pack_err;
#[cfg(feature = "comp")]
-mod suggest;
+use func::suggest;
+use func::{dispatcher, node, pack};
+use systems::res_injection;
+#[cfg(feature = "structural_renderer")]
+pub(crate) use systems::structural_data;
pub(crate) fn default_program_path() -> proc_macro2::TokenStream {
quote::quote! { crate::ThisProgram }
@@ -262,44 +272,36 @@ fn entry_has_variant(entry: &str, variant_name: &str) -> bool {
entry.contains(&format!(":: {variant_name} =>"))
}
-/// Registers an outside-type as a member of a program group without modifying its definition.
+/// Registers an outside type (from `std` or other crates) as a type recognizable
+/// by the Mingling framework, without modifying the original type definition.
///
-/// This macro allows you to use outside-types from external crates (like `std::io::Error`)
-/// within the Mingling framework by generating a `Groupped` implementation and registering
-/// the type's simple name as an enum variant.
+/// This macro generates a newtype wrapper around the given type that implements
+/// `Grouped`, `Into<AnyOutput>`, `Into<ChainProcess>`, and the `Routable` trait,
+/// making the outside type usable in `#[chain]` and `#[renderer]` functions.
///
/// # Syntax
///
/// ```rust,ignore
-/// group!(std::io::Error);
+/// // Simple form — creates a wrapper named after the type's last segment:
/// group!(ParseIntError);
-/// ```
///
-/// The type is registered under the default program (`crate::ThisProgram`).
-///
-/// # How it works
-///
-/// The macro generates a module containing:
-/// - A `use` import for the program path and the outside-type
-/// - An `impl Groupped<Program>` for the outside-type
-/// - A `register_type!` call with the type's simple name
-///
-/// The type's simple name (e.g. `Error`) is used as the enum variant in the generated
-/// program enum, just like `#[derive(Groupped)]` or `pack!`.
+/// // Aliased form — creates a wrapper with a custom name:
+/// group!(ErrorIo = std::io::Error);
+/// ```
///
/// # Example
///
-/// ```rust,ignore
-/// use mingling::macros::group;
-///
-/// // Register std::io::Error as a group member
-/// group!(std::io::Error);
+/// See the full example in the crate documentation or run:
+/// ```bash
+/// cargo run --example example-outside-type -- parse 42
+/// cargo run --example example-outside-type -- parse hello
+/// cargo run --example example-outside-type -- error
/// ```
///
-/// After expansion, the type can be used in chains and renderers like any
-/// `#[derive(Groupped)]` type.
+/// # Requirements
///
-/// This macro is only available with the `extra_macros` feature.
+/// - The type must be accessible at the call site (imported or fully qualified).
+/// - The alias name (if provided) must not conflict with existing types.
#[cfg(feature = "extra_macros")]
#[proc_macro]
pub fn group(input: TokenStream) -> TokenStream {
@@ -402,7 +404,7 @@ pub fn node(input: TokenStream) -> TokenStream {
/// - `AsRef<String>`, `AsMut<String>`
/// - `Default` if `String: Default`
/// - `Into<AnyOutput<ThisProgram>>`, `Into<ChainProcess<ThisProgram>>`
-/// - Implements `Groupped<ThisProgram>` with `member_id()` returning the enum variant
+/// - Implements `Grouped<ThisProgram>` with `member_id()` returning the enum variant
///
/// The struct is also registered via `register_type!` so that `gen_program!`
/// can include it in the program enum.
@@ -438,7 +440,7 @@ pub fn pack_structural(input: TokenStream) -> TokenStream {
/// Creates an error struct with a `name: String` field and optional `info: Type` field.
///
-/// This macro provides a concise way to define error types that implement `Groupped`
+/// This macro provides a concise way to define error types that implement `Grouped`
/// and are registered for inclusion in the program enum.
///
/// The `name` field is automatically set to the snake_case version of the struct name
@@ -461,7 +463,7 @@ pub fn pack_structural(input: TokenStream) -> TokenStream {
/// For `pack_err!(ErrorNotFound)`:
///
/// ```rust,ignore
-/// #[derive(::mingling::Groupped)]
+/// #[derive(::mingling::Grouped)]
/// pub struct ErrorNotFound {
/// name: String,
/// }
@@ -478,7 +480,7 @@ pub fn pack_structural(input: TokenStream) -> TokenStream {
/// For `pack_err!(ErrorNotDir = PathBuf)`:
///
/// ```rust,ignore
-/// #[derive(::mingling::Groupped)]
+/// #[derive(::mingling::Grouped)]
/// pub struct ErrorNotDir {
/// name: String,
/// info: PathBuf,
@@ -521,20 +523,47 @@ pub fn pack_err_structural(input: TokenStream) -> TokenStream {
pack_err::pack_err_structural(input)
}
-/// Early-returns an error from a `Result`, converting the `Ok` branch to a
-/// `ChainProcess`.
+/// Early-returns the error from a `Result`, converting the `Ok` branch to the
+/// next chain process value.
///
/// This macro is equivalent to:
/// ```rust,ignore
/// match expr {
/// Ok(r) => r,
-/// Err(e) => return ::mingling::Groupped::to_chain(e),
+/// Err(e) => return ::mingling::Routable::to_chain(e),
/// }
/// ```
///
-/// It is useful inside chain functions where you have a `Result<SomeType, SomeType>`
-/// where both types implement `Groupped` and want to propagate the error case
-/// as an early return via `Groupped::to_chain()`.
+/// It is useful inside chain functions where you have a `Result<SuccessType, ErrorType>`
+/// where both types implement `Routable` and you want to propagate the error case
+/// as an early return via `Routable::to_chain()`.
+///
+/// The key difference from a simple `?` operator is that `route!` converts **both**
+/// the success and error types into the chain process — the `Ok` value is unwrapped
+/// directly, while the `Err` value is converted via `Routable::to_chain()` and
+/// returned early.
+///
+/// ## Interaction with `#[routeify]`
+///
+/// The [`#[routeify]`](attr.routeify.html) attribute macro automatically replaces
+/// every `expr?` inside a function with `route!(expr)`. This means you can use the
+/// familiar `?` syntax in chain functions instead of writing `route!(...)`
+/// explicitly:
+///
+/// ```rust,ignore
+/// use mingling::macros::chain;
+///
+/// #[chain(routeify)]
+/// fn process(prev: SomeEntry) -> Next {
+/// // `?` here expands to `route!(...)` → this macro → the match block
+/// let value = some_fallible_call()?;
+/// value.to_chain()
+/// }
+/// ```
+///
+/// Because `#[routeify]` maps the span of `?` to this macro, hovering over `?` in
+/// a `#[routeify]` function will display this documentation — explaining what
+/// the `?` actually expands to.
///
/// # Example
///
@@ -542,7 +571,7 @@ pub fn pack_err_structural(input: TokenStream) -> TokenStream {
/// use mingling::macros::{chain, route};
///
/// #[chain]
-/// fn process(prev: SomeEntry) -> ChainProcess<ThisProgram> {
+/// fn process(prev: SomeEntry) -> Next {
/// let value = route!(current_dir().map_err(|e| ErrorEntry::new(e.to_string_lossy().to_string())));
/// // value is the PathBuf from current_dir()
/// value.to_chain()
@@ -555,7 +584,7 @@ pub fn route(input: TokenStream) -> TokenStream {
let expanded = quote! {
match #expr {
Ok(r) => r,
- Err(e) => return ::mingling::Groupped::to_chain(e),
+ Err(e) => return ::mingling::Routable::to_chain(e),
}
};
TokenStream::from(expanded)
@@ -613,7 +642,7 @@ pub fn route(input: TokenStream) -> TokenStream {
#[proc_macro]
pub fn empty_result(_input: TokenStream) -> TokenStream {
let expanded = quote! {
- <crate::ResultEmpty as ::mingling::Groupped::<crate::ThisProgram>>::to_chain(crate::ResultEmpty)
+ <crate::ResultEmpty as ::mingling::Routable::<crate::ThisProgram>>::to_chain(crate::ResultEmpty)
};
TokenStream::from(expanded)
}
@@ -867,10 +896,16 @@ pub fn dispatcher(input: TokenStream) -> TokenStream {
///
/// - The function must have at least **one** parameter (the previous type in the chain).
/// - The first parameter must be taken **by move**.
-/// - The function must return `Next` (the type alias generated by `gen_program!`, which equals `ChainProcess<ProgramName>`).
+/// - The function may return `Next`, `ChainProcess<ProgramName>`, `()`, or omit the return type.
+/// - The original function signature is preserved unchanged.
/// - With the `async` feature, async functions are supported; without it, async functions are rejected.
#[proc_macro_attribute]
pub fn chain(attr: TokenStream, item: TokenStream) -> TokenStream {
+ // Extension point: if attr contains extension identifiers like `routeify`,
+ // re-dispatch as #[ext1] #[ext2] #[chain] fn ...
+ if let Some(redispatch) = extensions::try_redispatch_simple(attr.clone(), &item, "chain") {
+ return redispatch;
+ }
chain::chain_attr(attr, item)
}
@@ -938,6 +973,9 @@ pub fn chain(attr: TokenStream, item: TokenStream) -> TokenStream {
/// ```
#[proc_macro_attribute]
pub fn renderer(attr: TokenStream, item: TokenStream) -> TokenStream {
+ if let Some(redispatch) = extensions::try_redispatch_simple(attr.clone(), &item, "renderer") {
+ return redispatch;
+ }
renderer::renderer_attr(attr, item)
}
@@ -992,6 +1030,9 @@ pub fn renderer(attr: TokenStream, item: TokenStream) -> TokenStream {
#[cfg(feature = "comp")]
#[proc_macro_attribute]
pub fn completion(attr: TokenStream, item: TokenStream) -> TokenStream {
+ if let Some(redispatch) = extensions::try_redispatch_completion(attr.clone(), &item) {
+ return redispatch;
+ }
completion::completion_attr(attr, item)
}
@@ -1234,7 +1275,7 @@ pub fn register_dispatcher(input: TokenStream) -> TokenStream {
///
/// - The function must have exactly one parameter (the entry type to provide help for).
/// - The parameter type must be a single-segment type path (e.g., `MyEntry`, not `other::MyEntry`).
-/// - The function must return `RenderResult`.
+/// - The function may return `RenderResult`, `()`, or any type that implements `Into<RenderResult>`.
/// - The function cannot be async.
///
/// # See also
@@ -1245,14 +1286,136 @@ pub fn register_dispatcher(input: TokenStream) -> TokenStream {
///
/// [`BasicProgramSetup`]: https://docs.rs/mingling/latest/mingling/setup/struct.BasicProgramSetup.html
#[proc_macro_attribute]
-pub fn help(_attr: TokenStream, item: TokenStream) -> TokenStream {
+pub fn help(attr: TokenStream, item: TokenStream) -> TokenStream {
+ if let Some(redispatch) = extensions::try_redispatch_simple(attr.clone(), &item, "help") {
+ return redispatch;
+ }
help::help_attr(item)
}
-/// Derive macro for automatically implementing the `Groupped` trait on a struct.
+/// Extension attribute macro that transforms `expr?` into `route!(expr)`.
+///
+/// Designed for use with `#[chain(routeify, ...)]` to enable concise error
+/// routing in chain functions using the `?` operator syntax.
+///
+/// # Example
+///
+/// ```rust,ignore
+/// #[chain(routeify)]
+/// fn handle_calc(args: EntryCalculate) -> Next {
+/// let a = args.pick(&arg![f32]).to_result()?;
+/// let op = args.pick(&arg![Operator]).to_result()?;
+/// StateCalculate { number_a: a, operator: op, ... }.to_chain()
+/// }
+/// ```
+#[cfg(feature = "extra_macros")]
+#[proc_macro_attribute]
+pub fn routeify(attr: TokenStream, item: TokenStream) -> TokenStream {
+ extensions::routeify::routeify_impl(attr, item)
+}
+
+/// Wraps a unit-returning function to produce a `RenderResult`.
+///
+/// The `#[buffer]` attribute macro injects a local `__render_result_buffer`
+/// variable of type `::mingling::RenderResult` and changes the function's
+/// return type to `::mingling::RenderResult`. Inside the body, use the
+/// `r_print!` and `r_println!` macros to write into the buffer.
+///
+/// # Example
+///
+/// ```rust,ignore
+/// use mingling::macros::{buffer, r_println};
+///
+/// #[buffer]
+/// fn render_my_type(prev: MyType) {
+/// r_println!("Value: {:?}", *prev);
+/// }
+/// ```
///
-/// The `#[derive(Groupped)]` macro:
-/// 1. Implements `Groupped<crate::ThisProgram>`.
+/// This expands to:
+///
+/// ```rust,ignore
+/// fn render_my_type(prev: MyType) -> mingling::RenderResult {
+/// let mut __render_result_buffer = mingling::RenderResult::new();
+/// {
+/// r_println!("Value: {:?}", *prev);
+/// }
+/// __render_result_buffer
+/// }
+/// ```
+///
+/// # Requirements
+///
+/// - The function must return `()` (unit).
+/// - The function cannot be async.
+#[proc_macro_attribute]
+pub fn buffer(attr: TokenStream, item: TokenStream) -> TokenStream {
+ extensions::buffer::buffer_impl(attr, item)
+}
+
+/// Prints text to a `RenderResult` buffer, with a trailing newline.
+///
+/// # Implicit buffer (inside `#[buffer]` functions)
+///
+/// ```rust,ignore
+/// use mingling::macros::{buffer, r_println};
+///
+/// #[buffer]
+/// fn render() {
+/// r_println!("Hello, {}!", name);
+/// }
+/// ```
+///
+/// # Explicit buffer
+///
+/// Pass a `RenderResult` variable as the first argument:
+///
+/// ```rust,ignore
+/// use mingling::macros::r_println;
+/// use mingling::RenderResult;
+///
+/// let mut r = RenderResult::new();
+/// r_println!(r, "value: {}", 42);
+/// assert_eq!(&*r, "value: 42\n");
+/// ```
+#[proc_macro]
+pub fn r_println(input: TokenStream) -> TokenStream {
+ func::r_print::r_println(input)
+}
+
+/// Prints text to a `RenderResult` buffer, without a trailing newline.
+///
+/// # Implicit buffer (inside `#[buffer]` functions)
+///
+/// ```rust,ignore
+/// use mingling::macros::{buffer, r_print};
+///
+/// #[buffer]
+/// fn render() {
+/// r_print!("Hello, ");
+/// r_println!("world!");
+/// }
+/// ```
+///
+/// # Explicit buffer
+///
+/// ```rust,ignore
+/// use mingling::macros::r_print;
+/// use mingling::RenderResult;
+///
+/// let mut r = RenderResult::new();
+/// r_print!(r, "value: {}", 42);
+/// assert_eq!(&*r, "value: 42");
+/// ```
+#[proc_macro]
+pub fn r_print(input: TokenStream) -> TokenStream {
+ func::r_print::r_print(input)
+}
+
+/// Derive macro for automatically implementing the `Grouped` trait on a struct.
+///
+/// The `#[derive(Grouped)]` macro:
+/// 1. Implements `Grouped<crate::ThisProgram>`.
/// 2. Registers the type via `register_type!` so it's included in the program enum.
/// 3. Generates `Into<AnyOutput<Group>>` and `Into<ChainProcess<Group>>` conversions.
/// 4. Adds `to_chain()` and `to_render()` methods to the struct.
@@ -1260,7 +1423,7 @@ pub fn help(_attr: TokenStream, item: TokenStream) -> TokenStream {
/// # Syntax
///
/// ```rust,ignore
-/// #[derive(Groupped)]
+/// #[derive(Grouped)]
/// struct MyStruct {
/// // ...
/// }
@@ -1269,9 +1432,9 @@ pub fn help(_attr: TokenStream, item: TokenStream) -> TokenStream {
/// # Example
///
/// ```rust,ignore
-/// use mingling::macros::Groupped;
+/// use mingling::macros::Grouped;
///
-/// #[derive(Groupped)]
+/// #[derive(Grouped)]
/// struct Greeting {
/// name: String,
/// }
@@ -1279,9 +1442,9 @@ pub fn help(_attr: TokenStream, item: TokenStream) -> TokenStream {
///
/// This is equivalent to using `pack!` but works with custom structs that
/// have named fields. For simple wrappers, prefer `pack!`.
-#[proc_macro_derive(Groupped, attributes(group))]
-pub fn derive_groupped(input: TokenStream) -> TokenStream {
- groupped::derive_groupped(input)
+#[proc_macro_derive(Grouped, attributes(group))]
+pub fn derive_grouped(input: TokenStream) -> TokenStream {
+ grouped::derive_grouped(input)
}
/// Derive macro for automatically implementing the `EnumTag` trait on an enum
@@ -1350,18 +1513,18 @@ pub fn derive_structural_data(input: TokenStream) -> TokenStream {
structural_data::derive_structural_data(input)
}
-/// Derive macro for implementing both `Groupped` and `serde::Serialize` on a struct.
+/// Derive macro for implementing both `Grouped` and `serde::Serialize` on a struct.
///
/// **This macro is only available with the `structural_renderer` feature.**
///
-/// This is identical to `#[derive(Groupped)]` but also adds `#[derive(serde::Serialize)]`
+/// This is identical to `#[derive(Grouped)]` but also adds `#[derive(serde::Serialize)]`
/// to the struct, which is required for the structural renderer to serialize output
/// to formats like JSON, YAML, TOML, or RON.
///
/// # Syntax
///
/// ```rust,ignore
-/// #[derive(GrouppedSerialize)]
+/// #[derive(GroupedSerialize)]
/// struct Info {
/// name: String,
/// age: i32,
@@ -1371,19 +1534,19 @@ pub fn derive_structural_data(input: TokenStream) -> TokenStream {
/// # Example
///
/// ```rust,ignore
-/// use mingling::GrouppedSerialize;
+/// use mingling::GroupedSerialize;
/// use serde::Serialize;
///
-/// #[derive(GrouppedSerialize)]
+/// #[derive(GroupedSerialize)]
/// struct Info {
/// name: String,
/// age: i32,
/// }
/// ```
#[cfg(feature = "structural_renderer")]
-#[proc_macro_derive(GrouppedSerialize, attributes(group))]
-pub fn derive_groupped_serialize(input: TokenStream) -> TokenStream {
- groupped::derive_groupped_serialize(input)
+#[proc_macro_derive(GroupedSerialize, attributes(group))]
+pub fn derive_grouped_serialize(input: TokenStream) -> TokenStream {
+ grouped::derive_grouped_serialize(input)
}
/// Generates the program enum and all collected types, chains, and renderers.
@@ -1435,119 +1598,42 @@ pub fn derive_groupped_serialize(input: TokenStream) -> TokenStream {
/// gen_program!();
/// ```
#[proc_macro]
-pub fn gen_program(_input: TokenStream) -> TokenStream {
- #[cfg(feature = "comp")]
- let comp_gen = quote! {
- ::mingling::macros::program_comp_gen!();
- };
-
- #[cfg(not(feature = "comp"))]
- let comp_gen = quote! {};
-
- TokenStream::from(quote! {
- pub type Next = ::mingling::ChainProcess<crate::ThisProgram>;
-
- #comp_gen
- ::mingling::macros::program_fallback_gen!();
- ::mingling::macros::program_final_gen!();
- })
+pub fn gen_program(input: TokenStream) -> TokenStream {
+ func::gen_program::gen_program_impl(input)
}
-/// Internal macro used by `gen_program!` to generate completion infrastructure.
+/// Internal macro used by `gen_program!` to generate the completion infrastructure for
+/// shell completion support.
///
/// **This macro is only available with the `comp` feature.**
///
-/// This is an internal macro and should not be called directly by user code.
-/// It generates a completion dispatcher, the `CompletionContext` type, and
-/// the execution/render logic for shell completion.
-///
-/// The generated module `__completion_gen` contains:
-/// - A `__comp` dispatcher that routes completion requests
-/// - A `__exec_completion` chain that processes `CompletionContext` into `CompletionSuggest`
-/// - A `__render_completion` renderer that outputs completion suggestions
-#[proc_macro]
+/// The `program_comp_gen!` macro generates:
+/// 1. A hidden `__internal_completion_mod` module containing:
+/// - A `CompletionContext` packed type (wrapping `ShellContext`), dispatched via `"__comp"`.
+/// - A `CompletionSuggest` packed type (wrapping `(ShellContext, Suggest)`).
+/// - An internal dispatcher (`CMDCompletion`) for the `"__comp"` command path.
+/// 2. An internal chain function `__exec_completion` that:
+/// - Reads a `ShellContext` from the packed `CompletionContext`.
+/// - Calls `CompletionHelper::exec_completion::<ThisProgram>(&ctx)` to generate suggestions.
+/// - Routes the result to the completion renderer via `CompletionSuggest`.
+/// 3. An internal renderer `__render_completion` that renders the suggestions via
+/// `CompletionHelper::render_suggest`.
+///
+/// When the `dispatch_tree` feature is enabled, it also imports the internal dispatcher
+/// from the generated module into the parent scope for trie-based dispatch.
+///
+/// This macro is called automatically by `gen_program!` and should not be called
+/// directly by user code.
#[cfg(feature = "comp")]
-pub fn program_comp_gen(_input: TokenStream) -> TokenStream {
- #[cfg(feature = "async")]
- let fn_exec_comp = quote! {
- #[doc(hidden)]
- #[::mingling::macros::chain]
- pub async fn __exec_completion(prev: CompletionContext) -> Next {
- use ::mingling::Groupped;
-
- let read_ctx = ::mingling::ShellContext::try_from(prev.inner);
- match read_ctx {
- Ok(ctx) => {
- let suggest = ::mingling::CompletionHelper::exec_completion::<crate::ThisProgram>(&ctx);
- crate::CompletionSuggest::new((ctx, suggest)).to_render()
- }
- Err(_) => std::process::exit(1),
- }
- }
- };
-
- #[cfg(not(feature = "async"))]
- let fn_exec_comp = quote! {
- #[doc(hidden)]
- #[::mingling::macros::chain]
- pub fn __exec_completion(prev: CompletionContext) -> Next {
- use ::mingling::Groupped;
-
- let read_ctx = ::mingling::ShellContext::try_from(prev.inner);
- match read_ctx {
- Ok(ctx) => {
- let suggest = ::mingling::CompletionHelper::exec_completion::<crate::ThisProgram>(&ctx);
- crate::CompletionSuggest::new((ctx, suggest)).to_render()
- }
- Err(_) => std::process::exit(1),
- }
- }
- };
-
- #[cfg(feature = "dispatch_tree")]
- let internal_dispatcher_comp = quote! {
- use __internal_completion_mod::__internal_dispatcher_comp;
- };
-
- #[cfg(not(feature = "dispatch_tree"))]
- let internal_dispatcher_comp = quote! {};
-
- let comp_dispatcher = quote! {
- #[doc(hidden)]
- mod __internal_completion_mod {
- use ::mingling::Groupped;
- ::mingling::macros::dispatcher!("__comp", CMDCompletion => CompletionContext);
- ::mingling::macros::pack!(
- CompletionSuggest = (::mingling::ShellContext, ::mingling::Suggest)
- );
- }
- #internal_dispatcher_comp
- use __internal_completion_mod::CompletionContext;
- use __internal_completion_mod::CompletionSuggest;
- pub use __internal_completion_mod::CMDCompletion;
-
- #fn_exec_comp
-
- ::mingling::macros::register_type!(CompletionContext);
-
- #[allow(unused)]
- #[doc(hidden)]
- #[::mingling::macros::renderer]
- pub fn __render_completion(prev: CompletionSuggest) -> ::mingling::RenderResult {
- let result = ::mingling::RenderResult::default();
- let (ctx, suggest) = prev.inner;
- ::mingling::CompletionHelper::render_suggest::<crate::ThisProgram>(ctx, suggest);
- result
- }
- };
-
- TokenStream::from(comp_dispatcher)
+#[proc_macro]
+pub fn program_comp_gen(input: TokenStream) -> TokenStream {
+ func::gen_program::program_comp_gen_impl(input)
}
/// Registers a type into the global packed types registry for inclusion in
/// the program enum generated by `gen_program!`.
///
-/// This macro is called internally by `pack!` and `#[derive(Groupped)]`(`macro.derive_groupped.html`)
+/// This macro is called internally by `pack!` and `#[derive(Grouped)]`(`macro.derive_grouped.html`)
/// and is generally not needed in user code. However, it can be used for manual
/// registration if you are implementing custom type registration outside of
/// the standard macros.
@@ -1566,115 +1652,60 @@ pub fn program_comp_gen(_input: TokenStream) -> TokenStream {
/// Panics if the global `PACKED_TYPES` mutex is poisoned.
#[proc_macro]
pub fn register_type(input: TokenStream) -> TokenStream {
- let type_ident = parse_macro_input!(input as syn::Ident);
- let entry_str = type_ident.to_string();
-
- get_global_set(&PACKED_TYPES)
- .lock()
- .unwrap()
- .insert(entry_str);
-
- TokenStream::new()
+ func::gen_program::register_type_impl(input)
}
-/// Registers a chain mapping from a previous type to a chain struct.
+/// Registers a chain mapping function into the global chain registry.
///
-/// This macro is called internally by `#[chain]`(macro.chain.html) and is
-/// generally not needed in user code. It inserts entries into the global
-/// `CHAINS` and `CHAINS_EXIST` registries.
+/// This macro is called internally by `#[chain]` and is generally not needed
+/// in user code. Each call stores a string entry containing the source-to-target
+/// type mapping, which is later consumed by `gen_program!` to generate the
+/// `has_chain` and `do_chain` dispatch logic in `ProgramCollect`.
///
-/// # Syntax
+/// The entry string format is a match arm: the source variant maps to a call
+/// that converts the value into a `ChainProcess` via the destination type.
///
-/// ```rust,ignore
-/// register_chain!(PreviousType, ChainStruct);
-/// ```
+/// # Panics
///
-/// The `PreviousType` is the input type of the chain step, and `ChainStruct`
-/// is the generated struct that implements the `Chain` trait.
+/// Panics if the global `CHAINS` mutex is poisoned.
#[proc_macro]
pub fn register_chain(input: TokenStream) -> TokenStream {
- chain::register_chain(input)
+ func::gen_program::register_chain_impl(input)
}
-/// Registers a renderer mapping from a type to a renderer struct.
+/// Registers a renderer mapping function into the global renderer registry.
///
-/// This macro is called internally by `#[renderer]`(macro.renderer.html) and is
-/// generally not needed in user code. It inserts entries into the global
-/// `RENDERERS`, `RENDERERS_EXIST` and (with `structural_renderer` feature)
-/// `STRUCTURAL_RENDERERS` registries.
+/// This macro is called internally by `#[renderer]` and is generally not
+/// needed in user code. Each call stores a string entry containing the
+/// type-to-render mapping, which is later consumed by `gen_program!` to
+/// generate the `has_renderer` and `render` dispatch logic in `ProgramCollect`.
///
-/// # Syntax
+/// The entry string format is a match arm: the type variant maps to a call
+/// of the registered renderer function that produces a `RenderResult`.
///
-/// ```rust,ignore
-/// register_renderer!(PreviousType, RendererStruct);
-/// ```
+/// # Panics
///
-/// The `PreviousType` is the input type of the renderer, and `RendererStruct`
-/// is the generated struct that implements the `Renderer` trait.
+/// Panics if the global `RENDERERS` mutex is poisoned.
#[proc_macro]
pub fn register_renderer(input: TokenStream) -> TokenStream {
- renderer::register_renderer(input)
+ func::gen_program::register_renderer_impl(input)
}
-/// Internal macro used by `gen_program!` to generate fallback types.
-///
-/// This macro generates the fallback wrapper types that are essential
-/// for error handling in the Mingling pipeline:
-///
-/// - **`ErrorRendererNotFound`** — Wraps a `String` (the name of the missing renderer).
-/// Used when no matching renderer is found for a given output type.
-/// - **`ErrorDispatcherNotFound`** — Wraps `Vec<String>` (the unrecognized command args).
-/// Used when no matching dispatcher is found for user input.
-/// - **`ResultEmpty`** — Wraps `()` (the unit type).
-/// Used when the chain returns an empty result.
-///
-/// Users can (and should) write `#[renderer]` functions for these types
-/// to provide meaningful error messages.
+/// Internal macro used by `gen_program!` to generate the fallback types for
+/// error cases when no dispatcher or renderer is found.
///
/// This macro is called automatically by `gen_program!` and should not
/// be called directly by user code.
-///
-/// # Syntax
-///
-/// ```rust,ignore
-/// // Called internally by gen_program!:
-/// program_fallback_gen!();
-/// ```
-///
-/// # Generated code equivalent
-///
-/// ```rust,ignore
-/// pack!(ErrorRendererNotFound = String);
-/// pack!(ErrorDispatcherNotFound = Vec<String>);
-/// pack!(ResultEmpty = ());
-/// ```
#[proc_macro]
-pub fn program_fallback_gen(_input: TokenStream) -> TokenStream {
- #[cfg(feature = "structural_renderer")]
- let pack_empty = quote! {
- #[derive(::serde::Serialize, ::mingling::StructuralData, ::mingling::Groupped, Default)]
- pub struct ResultEmpty;
- };
-
- #[cfg(not(feature = "structural_renderer"))]
- let pack_empty = quote! {
- #[derive(::mingling::Groupped, Default)]
- pub struct ResultEmpty;
- };
-
- let expanded = quote! {
- ::mingling::macros::pack!(ErrorRendererNotFound = String);
- ::mingling::macros::pack!(ErrorDispatcherNotFound = Vec<String>);
- #pack_empty
- };
- TokenStream::from(expanded)
+pub fn program_fallback_gen(input: TokenStream) -> TokenStream {
+ func::gen_program::program_fallback_gen_impl(input)
}
/// Internal macro used by `gen_program!` to generate the final program enum
/// and its `ProgramCollect` implementation.
///
/// This is the core code generation macro that:
-/// 1. Collects all registered types (from `pack!`, `#[derive(Groupped)]`, etc.) and
+/// 1. Collects all registered types (from `pack!`, `#[derive(Grouped)]`, etc.) and
/// creates an enum with each type as a variant.
/// 2. Generates the `Display` implementation for the enum.
/// 3. Generates the `ProgramCollect` implementation that dispatches to all
@@ -1719,434 +1750,9 @@ pub fn program_fallback_gen(_input: TokenStream) -> TokenStream {
/// pub fn new() -> Program<MyProgram> { Program::new() }
/// }
/// ```
-///
-/// # Panics
-///
-// Feature detection: baked into the proc-macro binary at compile time
-#[cfg(feature = "async")]
-const ASYNC_ENABLED: bool = true;
-#[cfg(not(feature = "async"))]
-const ASYNC_ENABLED: bool = false;
-
-/// Parses an entry of the format `StructName => EnumVariant,` into a pair of idents.
-fn parse_entry_pair(entry: &proc_macro2::TokenStream) -> (proc_macro2::Ident, proc_macro2::Ident) {
- let s = entry.to_string();
- let arrow_idx = s
- .find("=>")
- .unwrap_or_else(|| panic!("Entry missing '=>': {s}"));
- let struct_str = s[..arrow_idx].trim();
- let variant_str = s[arrow_idx + 2..].trim().trim_end_matches(',').trim();
- let struct_ident = proc_macro2::Ident::new(struct_str, proc_macro2::Span::call_site());
- let variant_ident = proc_macro2::Ident::new(variant_str, proc_macro2::Span::call_site());
- (struct_ident, variant_ident)
-}
-
-/// Loads the pathf type mapping from `$OUT_DIR/{crate}/type_using.rs`.
-/// Always compiled; returns empty map when pathf feature is not enabled.
-fn load_pathf_map() -> std::collections::HashMap<String, String> {
- if !cfg!(feature = "pathf") {
- return std::collections::HashMap::new();
- }
- let out_dir = std::env::var("OUT_DIR").ok();
- let crate_name = std::env::var("CARGO_PKG_NAME").ok();
- match (out_dir, crate_name) {
- (Some(dir), Some(name)) => {
- let path = std::path::Path::new(&dir).join(&name).join("type_using.rs");
- match std::fs::read_to_string(&path) {
- Ok(content) => content
- .lines()
- .filter_map(|line| {
- let line = line.trim();
- if let Some(rest) = line.strip_prefix("use ") {
- let path = rest.strip_suffix(';').unwrap_or(rest);
- if let Some((_mod, type_name)) = path.rsplit_once("::") {
- return Some((type_name.to_string(), path.to_string()));
- }
- }
- None
- })
- .collect(),
- Err(_) => std::collections::HashMap::new(),
- }
- }
- _ => std::collections::HashMap::new(),
- }
-}
-
-/// Resolves a type name to its full path token stream using the pathf mapping.
-pub(crate) fn resolve_type(
- name: &str,
- map: &std::collections::HashMap<String, String>,
-) -> proc_macro2::TokenStream {
- if let Some(full_path) = map.get(name) {
- syn::parse_str::<proc_macro2::TokenStream>(full_path).unwrap_or_else(|_| {
- let ident = proc_macro2::Ident::new(name, proc_macro2::Span::call_site());
- quote! { #ident }
- })
- } else {
- let ident = proc_macro2::Ident::new(name, proc_macro2::Span::call_site());
- quote! { #ident }
- }
-}
-
-/// Panics if any of the global registries (`PACKED_TYPES`, `RENDERERS`, `CHAINS`, etc.)
-/// are poisoned.
#[proc_macro]
-#[allow(clippy::too_many_lines)]
-pub fn program_final_gen(_input: TokenStream) -> TokenStream {
- let name = syn::Ident::new("ThisProgram", proc_macro2::Span::call_site());
-
- let packed_types = get_global_set(&PACKED_TYPES).lock().unwrap().clone();
-
- let renderers = get_global_set(&RENDERERS).lock().unwrap().clone();
- let chains = get_global_set(&CHAINS).lock().unwrap().clone();
- let renderer_exist = get_global_set(&RENDERERS_EXIST).lock().unwrap().clone();
- let chain_exist = get_global_set(&CHAINS_EXIST).lock().unwrap().clone();
-
- #[cfg(feature = "structural_renderer")]
- let structural_renderers = get_global_set(&STRUCTURAL_RENDERERS)
- .lock()
- .unwrap()
- .clone();
-
- #[cfg(feature = "comp")]
- let completions = get_global_set(&COMPLETIONS).lock().unwrap().clone();
-
- let packed_types: Vec<proc_macro2::TokenStream> = packed_types
- .iter()
- .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap())
- .collect();
-
- let renderer_tokens: Vec<proc_macro2::TokenStream> = renderers
- .iter()
- .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap())
- .collect();
-
- let chain_tokens: Vec<proc_macro2::TokenStream> = chains
- .iter()
- .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap())
- .collect();
-
- let renderer_exist_tokens: Vec<proc_macro2::TokenStream> = renderer_exist
- .iter()
- .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap())
- .collect();
-
- let chain_exist_tokens: Vec<proc_macro2::TokenStream> = chain_exist
- .iter()
- .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap())
- .collect();
-
- let pathf_map: std::collections::HashMap<String, String> = if cfg!(feature = "pathf") {
- load_pathf_map()
- } else {
- std::collections::HashMap::new()
- };
-
- let pathf_uses: Vec<proc_macro2::TokenStream> = if cfg!(feature = "pathf") {
- pathf_map
- .values()
- .map(|path| format!("use {};", path).parse().unwrap_or_default())
- .collect()
- } else {
- Vec::new()
- };
-
- #[cfg(feature = "structural_renderer")]
- let structural_renderer_tokens: Vec<proc_macro2::TokenStream> = structural_renderers
- .iter()
- .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap())
- .collect();
-
- #[cfg(feature = "structural_renderer")]
- let structural_render = quote! {
- fn structural_render(
- any: ::mingling::AnyOutput<Self::Enum>,
- setting: &::mingling::StructuralRendererSetting,
- ) -> Result<::mingling::RenderResult, ::mingling::error::StructuralRendererSerializeError> {
- #[allow(unused_imports)]
- #(#pathf_uses)*
- match any.member_id {
- #(#structural_renderer_tokens)*
- _ => {
- // Non-structural types: render ResultEmpty (which implements
- // StructuralData + Serialize) instead of producing nothing.
- let mut r = ::mingling::RenderResult::default();
- ::mingling::StructuralRenderer::render(&ResultEmpty, setting, &mut r)?;
- Ok(r)
- }
- }
- }
- };
-
- #[cfg(not(feature = "structural_renderer"))]
- let structural_render = quote! {};
-
- #[cfg(feature = "dispatch_tree")]
- let compile_time_dispatchers: Vec<String> = get_global_set(&COMPILE_TIME_DISPATCHERS)
- .lock()
- .unwrap()
- .clone()
- .iter()
- .cloned()
- .collect();
-
- #[cfg(feature = "dispatch_tree")]
- let dispatch_tree_nodes = {
- let entries: Vec<(String, String, String)> = compile_time_dispatchers
- .iter()
- .filter_map(|entry| {
- let parts: Vec<&str> = entry.split(':').collect();
- if parts.len() == 3 {
- Some((
- parts[0].to_string(),
- parts[1].to_string(),
- parts[2].to_string(),
- ))
- } else {
- None
- }
- })
- .collect();
-
- let get_nodes_fn = dispatch_tree_gen::gen_get_nodes(&entries, &pathf_map);
- let dispatch_trie_fn = dispatch_tree_gen::gen_dispatch_args_trie(&entries, &pathf_map);
-
- quote! {
- #get_nodes_fn
- #dispatch_trie_fn
- }
- };
-
- #[cfg(not(feature = "dispatch_tree"))]
- let dispatch_tree_nodes = quote! {};
-
- #[cfg(feature = "comp")]
- let completion_tokens: Vec<proc_macro2::TokenStream> = completions
- .iter()
- .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap())
- .collect();
-
- #[cfg(feature = "comp")]
- let comp = quote! {
- fn do_comp(any: &::mingling::AnyOutput<Self::Enum>, ctx: &::mingling::ShellContext) -> ::mingling::Suggest {
- #[allow(unused_imports)]
- #(#pathf_uses)*
- match any.member_id {
- #(#completion_tokens)*
- _ => ::mingling::Suggest::FileCompletion,
- }
- }
- };
-
- #[cfg(not(feature = "comp"))]
- let comp = quote! {};
-
- // Build render function arms from stored entries
- let render_fn =
- if renderer_tokens.is_empty() {
- quote! {
- fn render(_any: ::mingling::AnyOutput<Self::Enum>) -> ::mingling::RenderResult {
- ::mingling::RenderResult::default()
- }
- }
- } else {
- let render_arms: Vec<_> = renderer_tokens.iter().map(|entry| {
- let (struct_ident, variant_ident) = parse_entry_pair(entry);
- let downcast_ty = resolve_type(&variant_ident.to_string(), &pathf_map);
- let resolved_struct = resolve_type(&struct_ident.to_string(), &pathf_map);
- quote! {
- Self::#variant_ident => {
- // SAFETY: The `type_id` check ensures that `any` contains a value of type `#variant_ident`,
- // so downcasting to `#variant_ident` is safe.
- let value = unsafe { any.downcast::<#downcast_ty>().unwrap_unchecked() };
- <#resolved_struct as ::mingling::Renderer>::render(value)
- }
- }
- }).collect();
- quote! {
- fn render(any: ::mingling::AnyOutput<Self::Enum>) -> ::mingling::RenderResult {
- match any.member_id {
- #(#render_arms)*
- _ => ::mingling::RenderResult::default(),
- }
- }
- }
- };
-
- // Build do_chain function (async and sync versions)
- let chain_arms_async: Vec<_> = chain_tokens.iter().map(|entry| {
- let (struct_ident, variant_ident) = parse_entry_pair(entry);
- let downcast_ty = resolve_type(&variant_ident.to_string(), &pathf_map);
- let resolved_struct = resolve_type(&struct_ident.to_string(), &pathf_map);
- quote! {
- Self::#variant_ident => {
- // SAFETY: The `type_id` check ensures that `any` contains a value of type `#variant_ident`,
- // so downcasting to `#variant_ident` is safe.
- let value = unsafe { any.downcast::<#downcast_ty>().unwrap_unchecked() };
- let fut = async { <#resolved_struct as ::mingling::Chain<Self::Enum>>::proc(value).await };
- ::std::boxed::Box::pin(fut)
- }
- }
- }).collect();
-
- let chain_arms_sync: Vec<_> = chain_tokens
- .iter()
- .map(|entry| {
- let (struct_ident, variant_ident) = parse_entry_pair(entry);
- let downcast_ty = resolve_type(&variant_ident.to_string(), &pathf_map);
- let resolved_struct = resolve_type(&struct_ident.to_string(), &pathf_map);
- quote! {
- Self::#variant_ident => {
- // SAFETY: The `type_id` check ensures that `any` contains a value of type `#variant_ident`,
- // so downcasting to `#variant_ident` is safe.
- let value = unsafe { any.downcast::<#downcast_ty>().unwrap_unchecked() };
- <#resolved_struct as ::mingling::Chain<Self::Enum>>::proc(value)
- }
- }
- })
- .collect();
-
- let do_chain_fn = if chain_tokens.is_empty() {
- quote! {
- fn do_chain(_any: ::mingling::AnyOutput<Self::Enum>) -> ::mingling::ChainProcess<Self::Enum> {
- ::core::panic!("No chain found for type id")
- }
- }
- } else if ASYNC_ENABLED {
- quote! {
- fn do_chain(
- any: ::mingling::AnyOutput<Self::Enum>,
- ) -> ::std::pin::Pin<::std::boxed::Box<dyn ::std::future::Future<Output = ::mingling::ChainProcess<Self::Enum>> + ::std::marker::Send>> {
- match any.member_id {
- #(#chain_arms_async)*
- _ => ::core::panic!("No chain found for type id: {:?}", any.type_id),
- }
- }
- }
- } else {
- quote! {
- fn do_chain(
- any: ::mingling::AnyOutput<Self::Enum>,
- ) -> ::mingling::ChainProcess<Self::Enum> {
- match any.member_id {
- #(#chain_arms_sync)*
- _ => ::core::panic!("No chain found for type id: {:?}", any.type_id),
- }
- }
- }
- };
-
- let help_tokens: Vec<proc_macro2::TokenStream> = get_global_set(&HELP_REQUESTS)
- .lock()
- .unwrap()
- .clone()
- .iter()
- .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap())
- .collect();
-
- let num_variants = packed_types.len();
- let repr_type = if u8::try_from(num_variants).is_ok() {
- quote! { u8 }
- } else if u16::try_from(num_variants).is_ok() {
- quote! { u16 }
- } else if u32::try_from(num_variants).is_ok() {
- quote! { u32 }
- } else {
- quote! { u128 }
- };
-
- let expanded = quote! {
- #[derive(Debug, PartialEq, Eq, Clone)]
- #[repr(#repr_type)]
- #[allow(nonstandard_style)]
- pub enum #name {
- #(#packed_types),*
- }
-
- impl ::std::fmt::Display for #name {
- fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
- match self {
- #(#name::#packed_types => write!(f, stringify!(#packed_types)),)*
- }
- }
- }
-
- impl ::mingling::ProgramCollect for #name {
- type Enum = #name;
- type ErrorDispatcherNotFound = ErrorDispatcherNotFound;
- type ErrorRendererNotFound = ErrorRendererNotFound;
- type ResultEmpty = ResultEmpty;
- fn build_renderer_not_found(member_id: Self::Enum) -> ::mingling::AnyOutput<Self::Enum> {
- ::mingling::AnyOutput::new(ErrorRendererNotFound::new(member_id.to_string()))
- }
- fn build_dispatcher_not_found(args: Vec<String>) -> ::mingling::AnyOutput<Self::Enum> {
- ::mingling::AnyOutput::new(ErrorDispatcherNotFound::new(args))
- }
- fn build_empty_result() -> ::mingling::AnyOutput<Self::Enum> {
- ::mingling::AnyOutput::new(ResultEmpty)
- }
- #render_fn
- #do_chain_fn
- fn render_help(any: ::mingling::AnyOutput<Self::Enum>) -> ::mingling::RenderResult {
- #[allow(unused_imports)]
- #(#pathf_uses)*
- match any.member_id {
- #(#help_tokens)*
- _ => ::mingling::RenderResult::default(),
- }
- }
- fn has_renderer(any: &::mingling::AnyOutput<Self::Enum>) -> bool {
- match any.member_id {
- #(#renderer_exist_tokens)*
- _ => false
- }
- }
- fn has_chain(any: &::mingling::AnyOutput<Self::Enum>) -> bool {
- match any.member_id {
- #(#chain_exist_tokens)*
- _ => false
- }
- }
- #dispatch_tree_nodes
- #structural_render
- #comp
- }
-
- impl #name {
- /// Creates a new `Program<#name>` instance with default configuration.
- pub fn new() -> ::mingling::Program<#name> {
- ::mingling::Program::new()
- }
-
- /// Returns a static reference to the global `Program<#name>` singleton.
- pub fn this() -> &'static ::mingling::Program<#name> {
- &::mingling::this::<#name>()
- }
- }
- };
-
- // Clear all global registries to prevent stale state in Rust Analyzer
- get_global_set(&PACKED_TYPES).lock().unwrap().clear();
- get_global_set(&CHAINS).lock().unwrap().clear();
- get_global_set(&CHAINS_EXIST).lock().unwrap().clear();
- get_global_set(&RENDERERS).lock().unwrap().clear();
- get_global_set(&RENDERERS_EXIST).lock().unwrap().clear();
- get_global_set(&HELP_REQUESTS).lock().unwrap().clear();
- #[cfg(feature = "comp")]
- get_global_set(&COMPLETIONS).lock().unwrap().clear();
- #[cfg(feature = "dispatch_tree")]
- get_global_set(&COMPILE_TIME_DISPATCHERS)
- .lock()
- .unwrap()
- .clear();
- #[cfg(feature = "structural_renderer")]
- get_global_set(&STRUCTURAL_RENDERERS)
- .lock()
- .unwrap()
- .clear();
-
- TokenStream::from(expanded)
+pub fn program_final_gen(input: TokenStream) -> TokenStream {
+ func::gen_program::program_final_gen_impl(input)
}
/// Builds a `Suggest` instance with inline suggestion items.
diff --git a/mingling_macros/src/systems.rs b/mingling_macros/src/systems.rs
new file mode 100644
index 0000000..53e58c5
--- /dev/null
+++ b/mingling_macros/src/systems.rs
@@ -0,0 +1,5 @@
+#[cfg(feature = "dispatch_tree")]
+pub(crate) mod dispatch_tree_gen;
+pub(crate) mod res_injection;
+#[cfg(feature = "structural_renderer")]
+pub(crate) mod structural_data;
diff --git a/mingling_macros/src/dispatch_tree_gen.rs b/mingling_macros/src/systems/dispatch_tree_gen.rs
index b66e2f4..7383421 100644
--- a/mingling_macros/src/dispatch_tree_gen.rs
+++ b/mingling_macros/src/systems/dispatch_tree_gen.rs
@@ -4,11 +4,11 @@ use just_fmt::snake_case;
use proc_macro2::TokenStream;
use quote::quote;
-use crate::resolve_type;
+use crate::func::gen_program::resolve_type;
/// Generate the `get_nodes()` function body for a ProgramCollect impl.
/// If `pathf_map` is non-empty, resolves internal dispatcher statics using full paths.
-pub fn gen_get_nodes(
+pub(crate) fn gen_get_nodes(
entries: &[(String, String, String)],
pathf_map: &HashMap<String, String>,
) -> TokenStream {
@@ -40,7 +40,7 @@ pub fn gen_get_nodes(
/// Single-node groups use `starts_with`; multi-node groups recurse with `nth()` match.
///
/// If `pathf_map` is non-empty, resolves dispatcher types using full paths.
-pub fn gen_dispatch_args_trie(
+pub(crate) fn gen_dispatch_args_trie(
entries: &[(String, String, String)],
pathf_map: &HashMap<String, String>,
) -> TokenStream {
diff --git a/mingling_macros/src/res_injection.rs b/mingling_macros/src/systems/res_injection.rs
index 09da889..606b9a6 100644
--- a/mingling_macros/src/res_injection.rs
+++ b/mingling_macros/src/systems/res_injection.rs
@@ -163,8 +163,10 @@ 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 (sync version).
+/// Wraps the function body in mutable resource closures (sync version).
+///
+/// For unit return types: uses `modify_res` (closure returns `()`).
+/// For non-unit return types: uses `__modify_res_and_return_route` (closure returns `ChainProcess<C>`).
///
/// The innermost closure gets the original body, and each mutable parameter wraps
/// outward from last to first.
@@ -172,6 +174,7 @@ pub(crate) fn wrap_body_with_mut_resources(
fn_body_stmts: &[syn::Stmt],
mut_resources: &[&ResourceInjection],
program_type: &proc_macro2::TokenStream,
+ is_unit_return: bool,
) -> proc_macro2::TokenStream {
let mut wrapped = quote! {
#(#fn_body_stmts)*
@@ -180,11 +183,19 @@ pub(crate) fn wrap_body_with_mut_resources(
for res in mut_resources {
let var_name = &res.var_name;
let inner_type = &res.inner_type;
- wrapped = quote! {
- ::mingling::this::<#program_type>().__modify_res_and_return_route(|#var_name: &mut #inner_type| {
- #wrapped
- }).into()
- };
+ if is_unit_return {
+ wrapped = quote! {
+ ::mingling::this::<#program_type>().modify_res(|#var_name: &mut #inner_type| {
+ #wrapped
+ })
+ };
+ } else {
+ wrapped = quote! {
+ ::mingling::this::<#program_type>().__modify_res_and_return_route(|#var_name: &mut #inner_type| {
+ #wrapped
+ })
+ };
+ }
}
wrapped
diff --git a/mingling_macros/src/structural_data.rs b/mingling_macros/src/systems/structural_data.rs
index 5350d7e..74bcf09 100644
--- a/mingling_macros/src/structural_data.rs
+++ b/mingling_macros/src/systems/structural_data.rs
@@ -176,7 +176,7 @@ pub(crate) fn pack_structural(input: TokenStream) -> TokenStream {
}
}
- impl ::mingling::Groupped<#program_path> for #type_name {
+ impl ::mingling::Grouped<#program_path> for #type_name {
fn member_id() -> #program_path {
#program_path::#type_name
}
@@ -297,7 +297,7 @@ pub(crate) fn group_structural(input: TokenStream) -> TokenStream {
#type_use
#alias_use
- impl ::mingling::Groupped<__MinglingProgram> for #type_name {
+ impl ::mingling::Grouped<__MinglingProgram> for #type_name {
fn member_id() -> __MinglingProgram {
__MinglingProgram::#type_name
}
diff --git a/mingling_macros/src/utils.rs b/mingling_macros/src/utils.rs
new file mode 100644
index 0000000..49d0e7a
--- /dev/null
+++ b/mingling_macros/src/utils.rs
@@ -0,0 +1 @@
+// Shared utilities for the macro crate.