diff options
Diffstat (limited to 'mingling_macros/src')
| -rw-r--r-- | mingling_macros/src/attr/chain.rs | 95 | ||||
| -rw-r--r-- | mingling_macros/src/attr/completion.rs | 39 | ||||
| -rw-r--r-- | mingling_macros/src/attr/dispatcher_clap.rs | 10 | ||||
| -rw-r--r-- | mingling_macros/src/attr/help.rs | 41 | ||||
| -rw-r--r-- | mingling_macros/src/attr/renderer.rs | 47 | ||||
| -rw-r--r-- | mingling_macros/src/derive/grouped.rs | 12 | ||||
| -rw-r--r-- | mingling_macros/src/extensions.rs | 7 | ||||
| -rw-r--r-- | mingling_macros/src/extensions/buffer.rs | 95 | ||||
| -rw-r--r-- | mingling_macros/src/extensions/renderify.rs | 48 | ||||
| -rw-r--r-- | mingling_macros/src/extensions/routeify.rs | 9 | ||||
| -rw-r--r-- | mingling_macros/src/func.rs | 1 | ||||
| -rw-r--r-- | mingling_macros/src/func/dispatcher.rs | 2 | ||||
| -rw-r--r-- | mingling_macros/src/func/gen_program.rs | 112 | ||||
| -rw-r--r-- | mingling_macros/src/func/group.rs | 6 | ||||
| -rw-r--r-- | mingling_macros/src/func/pack.rs | 6 | ||||
| -rw-r--r-- | mingling_macros/src/func/pack_err.rs | 4 | ||||
| -rw-r--r-- | mingling_macros/src/func/r_print.rs | 127 | ||||
| -rw-r--r-- | mingling_macros/src/lib.rs | 398 | ||||
| -rw-r--r-- | mingling_macros/src/systems/structural_data.rs | 24 |
19 files changed, 945 insertions, 138 deletions
diff --git a/mingling_macros/src/attr/chain.rs b/mingling_macros/src/attr/chain.rs index 120e65d..dc28a39 100644 --- a/mingling_macros/src/attr/chain.rs +++ b/mingling_macros/src/attr/chain.rs @@ -34,27 +34,72 @@ fn validate_return_type(sig: &Signature) -> Result<(), proc_macro2::TokenStream> /// Builds the `proc` function implementation inside the generated `Chain` impl. /// -/// The user's function body is inlined directly, and its result is converted -/// via `.into()` to `ChainProcess<ProgramType>`. -#[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_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(); + // 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 { + 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. + // For non-unit returns, wrap in `to_chain()` so mutable-resource closures + // return `ChainProcess<C>` (as required by `__modify_res_and_return_route`). + let fn_call_expr: syn::Expr = if is_unit_return { + syn::parse_quote! { #fn_call } + } else { + syn::parse_quote! { ::mingling::Routable::<#program_type>::to_chain(#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(fn_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(fn_body_stmts, &mut_resources, program_type, is_unit_return) + wrap_body_with_mut_resources( + &[fn_call_stmt], + &mut_resources, + program_type, + is_unit_return, + ) }; let proc_body = if is_unit_return { @@ -62,13 +107,13 @@ fn generate_proc_fn( quote! { #(#immut_resource_stmts)* #wrapped_body; - <crate::ResultEmpty as ::mingling::Grouped::<crate::ThisProgram>> + <crate::ResultEmpty as ::mingling::Routable::<crate::ThisProgram>> ::to_chain(crate::ResultEmpty) } } else { quote! { #wrapped_body; - <crate::ResultEmpty as ::mingling::Grouped::<crate::ThisProgram>> + <crate::ResultEmpty as ::mingling::Routable::<crate::ThisProgram>> ::to_chain(crate::ResultEmpty) } }; @@ -82,23 +127,16 @@ fn generate_proc_fn( } else { quote! { #wrapped_body } }; - // Convert the body result to `ChainProcess` using the user-declared - // return type as the source type for a fully-qualified `Into` call. - // This works for both: - // - `-> Next` / `-> ChainProcess`: identity `From<T> for T` - // - `-> PackType`: `Into<ChainProcess>` from pack!/derive quote! { let __chain_result = { #body }; - <#origin_return_type as ::std::convert::Into< - ::mingling::ChainProcess<#program_type> - >>::into(__chain_result) + ::mingling::Routable::<#program_type>::to_chain(__chain_result) } }; #[cfg(feature = "async")] { quote! { - async fn proc(#prev_param: #previous_type) -> ::mingling::ChainProcess<#program_type> { + async fn proc(#fixed_prev: #previous_type) -> ::mingling::ChainProcess<#program_type> { #proc_body } } @@ -107,7 +145,7 @@ fn generate_proc_fn( #[cfg(not(feature = "async"))] { quote! { - fn proc(#prev_param: #previous_type) -> ::mingling::ChainProcess<#program_type> { + fn proc(#fixed_prev: #previous_type) -> ::mingling::ChainProcess<#program_type> { #proc_body } } @@ -192,13 +230,12 @@ pub(crate) 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 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; @@ -215,33 +252,23 @@ pub(crate) fn chain_attr(attr: TokenStream, item: TokenStream) -> TokenStream { // Always use the default crate-defined program path let program_type = crate::default_program_path(); - // 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_body.stmts, #[cfg(feature = "async")] is_async_fn, #[cfg(not(feature = "async"))] false, is_unit_return, - &origin_return_type, ); - // Preserve the original function untouched, with dead_code allowed - // since it may only be called through the Chain trait dispatch. + // Preserve the original function untouched // Note: do NOT add `#vis` here — `input_fn` (ItemFn) already contains its own visibility. let original_fn = quote! { - #[allow(dead_code)] #(#fn_attrs)* #input_fn }; diff --git a/mingling_macros/src/attr/completion.rs b/mingling_macros/src/attr/completion.rs index e917d7d..3ced091 100644 --- a/mingling_macros/src/attr/completion.rs +++ b/mingling_macros/src/attr/completion.rs @@ -47,11 +47,8 @@ pub(crate) fn completion_attr(attr: TokenStream, item: TokenStream) -> TokenStre // 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(crate) fn completion_attr(attr: TokenStream, item: TokenStream) -> TokenStre .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(crate) fn completion_attr(attr: TokenStream, item: TokenStream) -> TokenStre // 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(crate) fn completion_attr(attr: TokenStream, item: TokenStream) -> TokenStre // 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(crate) fn completion_attr(attr: TokenStream, item: TokenStream) -> TokenStre 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(crate) fn completion_attr(attr: TokenStream, item: TokenStream) -> TokenStre 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/attr/dispatcher_clap.rs b/mingling_macros/src/attr/dispatcher_clap.rs index 6083a52..40f7d47 100644 --- a/mingling_macros/src/attr/dispatcher_clap.rs +++ b/mingling_macros/src/attr/dispatcher_clap.rs @@ -108,23 +108,23 @@ pub(crate) fn dispatcher_clap_attr(attr: TokenStream, item: TokenStream) -> Toke 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) } }; diff --git a/mingling_macros/src/attr/help.rs b/mingling_macros/src/attr/help.rs index 6defae4..aa7bc88 100644 --- a/mingling_macros/src/attr/help.rs +++ b/mingling_macros/src/attr/help.rs @@ -1,7 +1,7 @@ use proc_macro::TokenStream; use quote::{ToTokens, quote}; use syn::spanned::Spanned; -use syn::{Ident, ItemFn, ReturnType, Signature, 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}; @@ -25,8 +25,8 @@ pub(crate) 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(), }; @@ -66,13 +66,31 @@ pub(crate) 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 fn_call = if has_resources { + quote! { #fn_name(#fixed_prev, #(#resource_args),*) } + } else { + quote! { #fn_name(#fixed_prev) } + }; - let wrapped_body = if mut_resources.is_empty() { - quote! { #(#fn_body_stmts)* } + // 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; @@ -88,10 +106,10 @@ pub(crate) 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 @@ -128,7 +146,7 @@ pub(crate) 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 { + fn render_help(#fixed_prev: Self::Entry) -> ::mingling::RenderResult { let __help_result = { #help_render_body }; ::std::convert::Into::into(__help_result) } @@ -137,7 +155,6 @@ pub(crate) fn help_attr(item: TokenStream) -> TokenStream { ::mingling::macros::register_help!(#entry_type, #struct_name); // Keep the original function unchanged - #[allow(dead_code)] #(#fn_attrs)* #vis fn #fn_name(#original_inputs) -> #original_return_type { #(#fn_body_stmts)* diff --git a/mingling_macros/src/attr/renderer.rs b/mingling_macros/src/attr/renderer.rs index c7cbb0b..828dc00 100644 --- a/mingling_macros/src/attr/renderer.rs +++ b/mingling_macros/src/attr/renderer.rs @@ -1,7 +1,7 @@ use proc_macro::TokenStream; use quote::{ToTokens, quote}; use syn::spanned::Spanned; -use syn::{ItemFn, ReturnType, Signature, 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}; @@ -31,8 +31,8 @@ pub(crate) 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(), }; @@ -69,31 +69,53 @@ pub(crate) 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. @@ -112,14 +134,13 @@ pub(crate) 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 { + fn render(#fixed_prev: Self::Previous) -> ::mingling::RenderResult { let __renderer_result = { #render_fn_body }; ::std::convert::Into::into(__renderer_result) } } // Keep the original function unchanged - #[allow(dead_code)] #(#fn_attrs)* #vis fn #fn_name(#original_inputs) -> #original_return_type { #(#fn_body_stmts)* diff --git a/mingling_macros/src/derive/grouped.rs b/mingling_macros/src/derive/grouped.rs index 307aab6..a00eea1 100644 --- a/mingling_macros/src/derive/grouped.rs +++ b/mingling_macros/src/derive/grouped.rs @@ -16,7 +16,11 @@ pub(crate) fn derive_grouped(input: TokenStream) -> TokenStream { let expanded = quote! { ::mingling::macros::register_type!(#struct_name); - impl ::mingling::Grouped<#group_ident> for #struct_name { + /// SAFETY: This is an internal implementation of the `Grouped` derive macro, + /// guaranteeing that the enum value registered by the `register_type!` macro + /// is exactly the same as the actual return value, + /// which can be confirmed via the `Ident` in the `quote!` block. + unsafe impl ::mingling::Grouped<#group_ident> for #struct_name { fn member_id() -> #group_ident { #group_ident::#struct_name } @@ -46,7 +50,11 @@ pub fn derive_grouped_serialize(input: TokenStream) -> TokenStream { ::mingling::macros::register_type!(#struct_name); - impl ::mingling::Grouped<#group_ident> for #struct_name { + /// SAFETY: This is an internal implementation of the `Grouped` derive macro, + /// guaranteeing that the enum value registered by the `register_type!` macro + /// is exactly the same as the actual return value, + /// which can be confirmed via the `Ident` in the `quote!` block. + unsafe 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 index aff20e2..c2fdb30 100644 --- a/mingling_macros/src/extensions.rs +++ b/mingling_macros/src/extensions.rs @@ -13,6 +13,13 @@ use syn::{Ident, Token}; #[cfg(feature = "extra_macros")] pub(crate) mod routeify; +/// Extension: `#[renderify]` — transforms `expr?` into `render_route!(expr)`. +#[cfg(feature = "extra_macros")] +pub(crate) mod renderify; + +/// 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>, 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/renderify.rs b/mingling_macros/src/extensions/renderify.rs new file mode 100644 index 0000000..9be8acc --- /dev/null +++ b/mingling_macros/src/extensions/renderify.rs @@ -0,0 +1,48 @@ +//! The `#[renderify]` extension — transforms `expr?` into `render_route!(expr)`. +//! +//! Designed as an extension for the Mingling attribute macro system, intended +//! to be used with `#[renderer(renderify)]`, `#[help(renderify)]`, +//! or standalone as `#[renderify]`. +//! +//! # How it works +//! +//! The macro parses the function AST and replaces every `Expr::Try` node with an +//! equivalent `render_route!(expr)` invocation, which routes errors to the +//! rendering pipeline via `crate::ThisProgram::render(AnyOutput::new(e))`. + +use proc_macro::TokenStream; +use quote::ToTokens; +use syn::spanned::Spanned; +use syn::visit_mut::VisitMut; +use syn::{Expr, ItemFn, parse_macro_input}; + +struct RenderifyTransform; + +impl VisitMut for RenderifyTransform { + 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 `render_route` ident to the `?` token's span, + // so that rust-analyzer resolves the `?` position to the `render_route!` macro + // instead of the standard Try trait, showing the render_route macro's docs on hover. + let q_span = try_expr.question_token.span(); + let route_ident = proc_macro2::Ident::new("render_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 renderify_impl(_attr: TokenStream, item: TokenStream) -> TokenStream { + let mut input_fn = parse_macro_input!(item as ItemFn); + RenderifyTransform.visit_item_fn_mut(&mut input_fn); + input_fn.to_token_stream().into() +} diff --git a/mingling_macros/src/extensions/routeify.rs b/mingling_macros/src/extensions/routeify.rs index f011fb9..7cded54 100644 --- a/mingling_macros/src/extensions/routeify.rs +++ b/mingling_macros/src/extensions/routeify.rs @@ -10,6 +10,7 @@ use proc_macro::TokenStream; use quote::ToTokens; +use syn::spanned::Spanned; use syn::visit_mut::VisitMut; use syn::{Expr, ItemFn, parse_macro_input}; @@ -23,8 +24,14 @@ impl VisitMut for RouteifyTransform { 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!(#inner_tokens) + ::mingling::macros::#route_ident!(#inner_tokens) }) { *expr = macro_expr; } diff --git a/mingling_macros/src/func.rs b/mingling_macros/src/func.rs index 720b20a..33bb094 100644 --- a/mingling_macros/src/func.rs +++ b/mingling_macros/src/func.rs @@ -8,5 +8,6 @@ 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/func/dispatcher.rs b/mingling_macros/src/func/dispatcher.rs index 808df75..a61dd26 100644 --- a/mingling_macros/src/func/dispatcher.rs +++ b/mingling_macros/src/func/dispatcher.rs @@ -135,7 +135,7 @@ pub(crate) fn dispatcher(input: TokenStream) -> TokenStream { } fn begin(&self, args: Vec<String>) -> ::mingling::ChainProcess<#program_type> { use ::mingling::Grouped; - #pack::new(args).to_chain() + ::mingling::Routable::to_chain(#pack::new(args)) } fn clone_dispatcher(&self) -> Box<dyn ::mingling::Dispatcher<#program_type>> { Box::new(#command_struct) diff --git a/mingling_macros/src/func/gen_program.rs b/mingling_macros/src/func/gen_program.rs index 7128a9a..db82504 100644 --- a/mingling_macros/src/func/gen_program.rs +++ b/mingling_macros/src/func/gen_program.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use proc_macro::TokenStream; use quote::quote; use syn::parse_macro_input; @@ -38,35 +40,33 @@ fn parse_entry_pair(entry: &proc_macro2::TokenStream) -> (proc_macro2::Ident, pr } /// 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> { +/// 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 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(), + 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. @@ -138,7 +138,7 @@ pub(crate) fn program_comp_gen_impl(_input: TokenStream) -> TokenStream { match read_ctx { Ok(ctx) => { let suggest = ::mingling::CompletionHelper::exec_completion::<crate::ThisProgram>(&ctx); - crate::CompletionSuggest::new((ctx, suggest)).to_render() + ::mingling::Routable::<crate::ThisProgram>::to_render(crate::CompletionSuggest::new((ctx, suggest))) } Err(_) => std::process::exit(1), } @@ -156,7 +156,7 @@ pub(crate) fn program_comp_gen_impl(_input: TokenStream) -> TokenStream { match read_ctx { Ok(ctx) => { let suggest = ::mingling::CompletionHelper::exec_completion::<crate::ThisProgram>(&ctx); - crate::CompletionSuggest::new((ctx, suggest)).to_render() + ::mingling::Routable::<crate::ThisProgram>::to_render(crate::CompletionSuggest::new((ctx, suggest))) } Err(_) => std::process::exit(1), } @@ -289,10 +289,34 @@ pub(crate) fn program_final_gen_impl(_input: TokenStream) -> TokenStream { .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() + let pathf_map: Option<HashMap<String, String>> = load_pathf_map(); + + #[cfg(feature = "pathf")] + 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 { - std::collections::HashMap::new() + quote! {} + }; + + #[cfg(not(feature = "pathf"))] + let pathf_hint: proc_macro2::TokenStream = 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") { @@ -318,7 +342,7 @@ pub(crate) fn program_final_gen_impl(_input: TokenStream) -> TokenStream { ) -> Result<::mingling::RenderResult, ::mingling::error::StructuralRendererSerializeError> { #[allow(unused_imports)] #(#pathf_uses)* - match any.member_id { + match any.member_id() { #(#structural_renderer_tokens)* _ => { // Non-structural types: render ResultEmpty (which implements @@ -384,7 +408,7 @@ pub(crate) fn program_final_gen_impl(_input: TokenStream) -> TokenStream { fn do_comp(any: &::mingling::AnyOutput<Self::Enum>, ctx: &::mingling::ShellContext) -> ::mingling::Suggest { #[allow(unused_imports)] #(#pathf_uses)* - match any.member_id { + match any.member_id() { #(#completion_tokens)* _ => ::mingling::Suggest::FileCompletion, } @@ -418,7 +442,7 @@ pub(crate) fn program_final_gen_impl(_input: TokenStream) -> TokenStream { }).collect(); quote! { fn render(any: ::mingling::AnyOutput<Self::Enum>) -> ::mingling::RenderResult { - match any.member_id { + match any.member_id() { #(#render_arms)* _ => ::mingling::RenderResult::default(), } @@ -470,9 +494,9 @@ pub(crate) fn program_final_gen_impl(_input: TokenStream) -> TokenStream { 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 { + match any.member_id() { #(#chain_arms_async)* - _ => ::core::panic!("No chain found for type id: {:?}", any.type_id), + _ => ::core::panic!("No chain found for type id: {:?}", any.type_id()), } } } @@ -481,9 +505,9 @@ pub(crate) fn program_final_gen_impl(_input: TokenStream) -> TokenStream { fn do_chain( any: ::mingling::AnyOutput<Self::Enum>, ) -> ::mingling::ChainProcess<Self::Enum> { - match any.member_id { + match any.member_id() { #(#chain_arms_sync)* - _ => ::core::panic!("No chain found for type id: {:?}", any.type_id), + _ => ::core::panic!("No chain found for type id: {:?}", any.type_id()), } } } @@ -509,7 +533,9 @@ pub(crate) fn program_final_gen_impl(_input: TokenStream) -> TokenStream { }; let expanded = quote! { - #[derive(Debug, PartialEq, Eq, Clone)] + #pathf_hint + + #[derive(Debug, PartialEq, Eq, Clone, Copy)] #[repr(#repr_type)] #[allow(nonstandard_style)] pub enum #name { @@ -543,19 +569,19 @@ pub(crate) fn program_final_gen_impl(_input: TokenStream) -> TokenStream { fn render_help(any: ::mingling::AnyOutput<Self::Enum>) -> ::mingling::RenderResult { #[allow(unused_imports)] #(#pathf_uses)* - match any.member_id { + match any.member_id() { #(#help_tokens)* _ => ::mingling::RenderResult::default(), } } fn has_renderer(any: &::mingling::AnyOutput<Self::Enum>) -> bool { - match any.member_id { + match any.member_id() { #(#renderer_exist_tokens)* _ => false } } fn has_chain(any: &::mingling::AnyOutput<Self::Enum>) -> bool { - match any.member_id { + match any.member_id() { #(#chain_exist_tokens)* _ => false } diff --git a/mingling_macros/src/func/group.rs b/mingling_macros/src/func/group.rs index b865913..edb1fe1 100644 --- a/mingling_macros/src/func/group.rs +++ b/mingling_macros/src/func/group.rs @@ -133,7 +133,11 @@ pub(crate) fn group_macro(input: TokenStream) -> TokenStream { #type_use #alias_use - impl ::mingling::Grouped<__MinglingProgram> for #type_name { + /// SAFETY: This is an internal implementation of the `group!` macro, + /// guaranteeing that the enum value registered by the `register_type!` macro + /// is exactly the same as the actual return value, + /// which can be confirmed via the `Ident` in the `quote!` block. + unsafe impl ::mingling::Grouped<__MinglingProgram> for #type_name { fn member_id() -> __MinglingProgram { __MinglingProgram::#type_name } diff --git a/mingling_macros/src/func/pack.rs b/mingling_macros/src/func/pack.rs index a1a7e6b..7206b8e 100644 --- a/mingling_macros/src/func/pack.rs +++ b/mingling_macros/src/func/pack.rs @@ -138,7 +138,11 @@ pub(crate) fn pack(input: TokenStream) -> TokenStream { } } - impl ::mingling::Grouped<#group_name> for #type_name { + /// SAFETY: This is an internal implementation of the `pack!` macro, + /// guaranteeing that the enum value registered by the `register_type!` macro + /// is exactly the same as the actual return value, + /// which can be confirmed via the `Ident` in the `quote!` block. + unsafe impl ::mingling::Grouped<#group_name> for #type_name { fn member_id() -> #group_name { #group_name::#type_name } diff --git a/mingling_macros/src/func/pack_err.rs b/mingling_macros/src/func/pack_err.rs index 36e550a..2a318bc 100644 --- a/mingling_macros/src/func/pack_err.rs +++ b/mingling_macros/src/func/pack_err.rs @@ -139,8 +139,8 @@ pub(crate) fn pack_err_structural(input: TokenStream) -> TokenStream { .insert(type_name_str); let structural_data = quote! { - impl ::mingling::__private::StructuralDataSealed for #type_name {} - impl ::mingling::__private::StructuralData for #type_name {} + impl ::mingling::__private::StructuralDataSealed<crate::ThisProgram> for #type_name {} + impl ::mingling::__private::StructuralData<crate::ThisProgram> for #type_name {} }; // Generate the struct + impls (same as pack_err! but with Serialize derive + sealed) diff --git a/mingling_macros/src/func/r_print.rs b/mingling_macros/src/func/r_print.rs new file mode 100644 index 0000000..86ea52f --- /dev/null +++ b/mingling_macros/src/func/r_print.rs @@ -0,0 +1,127 @@ +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") +} + +pub(crate) fn r_eprintln(input: TokenStream) -> TokenStream { + expand_print(input, "eprintln") +} + +pub(crate) fn r_eprint(input: TokenStream) -> TokenStream { + expand_print(input, "eprint") +} + +/// Parsed input for `r_append!`. +/// +/// Two forms: +/// - `(dst, src)` — explicit buffer and source +/// - `(src)` — implicit `__render_result_buffer` +struct AppendInput { + dst: Option<Ident>, + src: proc_macro2::TokenStream, +} + +impl Parse for AppendInput { + fn parse(input: ParseStream) -> syn::Result<Self> { + if input.peek(Ident) && input.peek2(Token![,]) { + let dst: Ident = input.parse()?; + let _comma: Token![,] = input.parse()?; + let src: TokenStream2 = input.parse()?; + Ok(AppendInput { + dst: Some(dst), + src, + }) + } else { + let src: TokenStream2 = input.parse()?; + Ok(AppendInput { dst: None, src }) + } + } +} + +/// `r_append!` macro: appends the contents of another `RenderResult` to this one. +/// +/// Two forms: +/// - `r_append!(dst, src)` — appends `src` into `dst` +/// - `r_append!(src)` — appends `src` into `__render_result_buffer` +pub(crate) fn r_append(input: TokenStream) -> TokenStream { + let parsed: AppendInput = match syn::parse(input) { + Ok(p) => p, + Err(e) => return e.to_compile_error().into(), + }; + + let dst_ident = parsed.dst.clone(); + let src_tokens = parsed.src; + + let expanded = match dst_ident { + Some(dst) => { + quote! { + #dst.append_other(#src_tokens); + } + } + None => { + quote! { + __render_result_buffer.append_other(#src_tokens); + } + } + }; + + expanded.into() +} diff --git a/mingling_macros/src/lib.rs b/mingling_macros/src/lib.rs index a85648f..ca3261e 100644 --- a/mingling_macros/src/lib.rs +++ b/mingling_macros/src/lib.rs @@ -141,6 +141,8 @@ //! } //! ``` +#![deny(missing_docs)] + #[cfg(feature = "extra_macros")] use quote::quote; @@ -270,6 +272,36 @@ fn entry_has_variant(entry: &str, variant_name: &str) -> bool { entry.contains(&format!(":: {variant_name} =>")) } +/// 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 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 +/// // Simple form — creates a wrapper named after the type's last segment: +/// group!(ParseIntError); +/// +/// // Aliased form — creates a wrapper with a custom name: +/// group!(ErrorIo = std::io::Error); +/// ``` +/// +/// # Example +/// +/// 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 +/// ``` +/// +/// # Requirements +/// +/// - 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 { @@ -511,6 +543,28 @@ pub fn pack_err_structural(input: TokenStream) -> TokenStream { /// 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 /// /// ```rust,ignore @@ -536,6 +590,56 @@ pub fn route(input: TokenStream) -> TokenStream { TokenStream::from(expanded) } +/// Routes errors to the rendering pipeline instead of the chain pipeline. +/// +/// This macro is similar to [`route!`] but instead of routing errors through +/// `Routable::to_chain()` (which returns `ChainProcess`), it routes them +/// directly to the renderer via `crate::ThisProgram::render(AnyOutput::new(e))` +/// (which returns `RenderResult`). +/// +/// This is useful in `#[renderer]` and `#[help]` functions where the return +/// type is `RenderResult` rather than `ChainProcess`. +/// +/// # Syntax +/// +/// ```rust,ignore +/// render_route!(expr) +/// ``` +/// +/// Where `expr` is an expression of type `Result<T, E>`. +/// +/// # Interaction with `#[routeify]` +/// +/// When `#[routeify]` is used on a `#[renderer]` or `#[help]` function (e.g. +/// `#[renderer(routeify)]` or `#[help(routeify)]`), every `expr?` is automatically +/// replaced with `render_route!(expr)` instead of `route!(expr)`. +/// +/// # Example +/// +/// ```rust,ignore +/// use mingling::macros::{renderer, render_route}; +/// use std::io::Write; +/// +/// #[renderer] +/// fn render_something(prev: SomeType) -> RenderResult { +/// let data = render_route!(fetch_data().map_err(|e| ErrorEntry::new(e.to_string())))?; +/// // ... render data +/// Ok(RenderResult::new()) +/// } +/// ``` +#[cfg(feature = "extra_macros")] +#[proc_macro] +pub fn render_route(input: TokenStream) -> TokenStream { + let expr = parse_macro_input!(input as syn::Expr); + let expanded = quote! { + match #expr { + Ok(r) => r, + Err(e) => return <crate::ThisProgram as ::mingling::ProgramCollect>::render(::mingling::AnyOutput::new(e)), + } + }; + TokenStream::from(expanded) +} + /// Creates an empty result value wrapped in `ChainProcess` for early return /// from a chain function. /// @@ -588,7 +692,7 @@ pub fn route(input: TokenStream) -> TokenStream { #[proc_macro] pub fn empty_result(_input: TokenStream) -> TokenStream { let expanded = quote! { - <crate::ResultEmpty as ::mingling::Grouped::<crate::ThisProgram>>::to_chain(crate::ResultEmpty) + <crate::ResultEmpty as ::mingling::Routable::<crate::ThisProgram>>::to_chain(crate::ResultEmpty) }; TokenStream::from(expanded) } @@ -1239,6 +1343,25 @@ pub fn help(attr: TokenStream, item: TokenStream) -> TokenStream { help::help_attr(item) } +/// Marker attribute for the Mingling lint system. +/// +/// The content of this attribute is ignored by rustc and reserved for +/// the mlint tool to interpret. All it does is pass the item through +/// unchanged. +/// +/// # Examples +/// +/// ```rust,ignore +/// #[mlint(allow(MLINT_SOME_LINT))] +/// #[mlint(warn(MLINT_SOME_LINT))] +/// #[mlint(deny(MLINT_SOME_LINT))] +/// fn some_item() {} +/// ``` +#[proc_macro_attribute] +pub fn mlint(_attr: TokenStream, item: TokenStream) -> TokenStream { + item +} + /// Extension attribute macro that transforms `expr?` into `route!(expr)`. /// /// Designed for use with `#[chain(routeify, ...)]` to enable concise error @@ -1260,6 +1383,226 @@ pub fn routeify(attr: TokenStream, item: TokenStream) -> TokenStream { extensions::routeify::routeify_impl(attr, item) } +/// Extension attribute macro that transforms `expr?` into `render_route!(expr)`. +/// +/// Designed for use with `#[renderer(renderify, ...)]` or `#[help(renderify, ...)]` +/// to enable concise error routing in renderer and help functions using the `?` +/// operator syntax. +/// +/// Unlike `#[routeify]` which routes errors to the chain pipeline via `route!`, +/// `#[renderify]` routes errors to the rendering pipeline via `render_route!`, +/// which matches the `RenderResult` return type of renderer and help functions. +/// +/// # Example +/// +/// ```rust,ignore +/// #[renderer(renderify)] +/// fn render_greeting(prev: Greeting) -> RenderResult { +/// let data = load_data()?; // expands to render_route!(load_data()) +/// r_println!("{data}"); +/// Ok(RenderResult::new()) +/// } +/// ``` +#[cfg(feature = "extra_macros")] +#[proc_macro_attribute] +pub fn renderify(attr: TokenStream, item: TokenStream) -> TokenStream { + extensions::renderify::renderify_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); +/// } +/// ``` +/// +/// 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) +} + +/// Prints text to a `RenderResult` buffer (standard error style), with a trailing newline. +/// +/// This macro works identically to `r_println!` but conceptually targets +/// "error output" — it writes into a `RenderResult` buffer with a trailing newline. +/// +/// # Implicit buffer (inside `#[buffer]` functions) +/// +/// ```rust,ignore +/// use mingling::macros::{buffer, r_eprintln}; +/// +/// #[buffer] +/// fn render() { +/// r_eprintln!("Error: {}", err_msg); +/// } +/// ``` +/// +/// # Explicit buffer +/// +/// Pass a `RenderResult` variable as the first argument: +/// +/// ```rust,ignore +/// use mingling::macros::r_eprintln; +/// use mingling::RenderResult; +/// +/// let mut r = RenderResult::new(); +/// r_eprintln!(r, "error: {}", 42); +/// assert_eq!(&*r, "error: 42\n"); +/// ``` +#[proc_macro] +pub fn r_eprintln(input: TokenStream) -> TokenStream { + func::r_print::r_eprintln(input) +} + +/// Prints text to a `RenderResult` buffer (standard error style), without a trailing newline. +/// +/// This macro works identically to `r_print!` but conceptually targets +/// "error output" — it writes into a `RenderResult` buffer without a trailing newline. +/// +/// # Implicit buffer (inside `#[buffer]` functions) +/// +/// ```rust,ignore +/// use mingling::macros::{buffer, r_eprint}; +/// +/// #[buffer] +/// fn render() { +/// r_eprint!("Error: "); +/// r_eprintln!("something went wrong"); +/// } +/// ``` +/// +/// # Explicit buffer +/// +/// ```rust,ignore +/// use mingling::macros::r_eprint; +/// use mingling::RenderResult; +/// +/// let mut r = RenderResult::new(); +/// r_eprint!(r, "error: "); +/// r_eprintln!(r, "42"); +/// assert_eq!(&*r, "error: 42\n"); +/// ``` +#[proc_macro] +pub fn r_eprint(input: TokenStream) -> TokenStream { + func::r_print::r_eprint(input) +} + +/// Appends the contents of one `RenderResult` to another. +/// +/// # Implicit buffer (inside `#[buffer]` functions) +/// +/// ```rust,ignore +/// use mingling::macros::{buffer, r_append}; +/// +/// #[buffer] +/// fn render() { +/// let other = make_other_result(); +/// r_append!(other); +/// } +/// ``` +/// +/// # Explicit buffer +/// +/// ```rust,ignore +/// use mingling::macros::r_append; +/// use mingling::RenderResult; +/// +/// let mut dst = RenderResult::new(); +/// let src = RenderResult::from("hello"); +/// r_append!(dst, src); +/// assert!(!dst.is_empty()); +/// ``` +#[proc_macro] +pub fn r_append(input: TokenStream) -> TokenStream { + func::r_print::r_append(input) +} + /// Derive macro for automatically implementing the `Grouped` trait on a struct. /// /// The `#[derive(Grouped)]` macro: @@ -1450,6 +1793,28 @@ pub fn gen_program(input: TokenStream) -> TokenStream { func::gen_program::gen_program_impl(input) } +/// Internal macro used by `gen_program!` to generate the completion infrastructure for +/// shell completion support. +/// +/// **This macro is only available with the `comp` feature.** +/// +/// 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")] #[proc_macro] pub fn program_comp_gen(input: TokenStream) -> TokenStream { @@ -1481,16 +1846,47 @@ pub fn register_type(input: TokenStream) -> TokenStream { func::gen_program::register_type_impl(input) } +/// Registers a chain mapping function into the global chain registry. +/// +/// 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`. +/// +/// 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. +/// +/// # Panics +/// +/// Panics if the global `CHAINS` mutex is poisoned. #[proc_macro] pub fn register_chain(input: TokenStream) -> TokenStream { func::gen_program::register_chain_impl(input) } +/// Registers a renderer mapping function into the global renderer registry. +/// +/// 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`. +/// +/// The entry string format is a match arm: the type variant maps to a call +/// of the registered renderer function that produces a `RenderResult`. +/// +/// # Panics +/// +/// Panics if the global `RENDERERS` mutex is poisoned. #[proc_macro] pub fn register_renderer(input: TokenStream) -> TokenStream { func::gen_program::register_renderer_impl(input) } +/// 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. #[proc_macro] pub fn program_fallback_gen(input: TokenStream) -> TokenStream { func::gen_program::program_fallback_gen_impl(input) diff --git a/mingling_macros/src/systems/structural_data.rs b/mingling_macros/src/systems/structural_data.rs index 74bcf09..46d7cf8 100644 --- a/mingling_macros/src/systems/structural_data.rs +++ b/mingling_macros/src/systems/structural_data.rs @@ -29,8 +29,8 @@ pub(crate) fn derive_structural_data(input: TokenStream) -> TokenStream { // Users cannot implement StructuralDataSealed manually (it's #[doc(hidden)]), // so the only way to get StructuralData is through this derive macro. let expanded = quote! { - impl ::mingling::__private::StructuralDataSealed for #type_name {} - impl ::mingling::__private::StructuralData for #type_name {} + impl ::mingling::__private::StructuralDataSealed<crate::ThisProgram> for #type_name {} + impl ::mingling::__private::StructuralData<crate::ThisProgram> for #type_name {} }; expanded.into() @@ -149,8 +149,8 @@ pub(crate) fn pack_structural(input: TokenStream) -> TokenStream { // StructuralData impl + sealed + registration let structural_impl = quote! { - impl ::mingling::__private::StructuralDataSealed for #type_name {} - impl ::mingling::__private::StructuralData for #type_name {} + impl ::mingling::__private::StructuralDataSealed<crate::ThisProgram> for #type_name {} + impl ::mingling::__private::StructuralData<crate::ThisProgram> for #type_name {} }; let expanded = quote! { @@ -176,7 +176,11 @@ pub(crate) fn pack_structural(input: TokenStream) -> TokenStream { } } - impl ::mingling::Grouped<#program_path> for #type_name { + /// SAFETY: This is an internal implementation of the `pack_structural!` macro, + /// guaranteeing that the enum value registered by the `register_type!` macro + /// is exactly the same as the actual return value, + /// which can be confirmed via the `Ident` in the `quote!` block. + unsafe impl ::mingling::Grouped<#program_path> for #type_name { fn member_id() -> #program_path { #program_path::#type_name } @@ -297,14 +301,18 @@ pub(crate) fn group_structural(input: TokenStream) -> TokenStream { #type_use #alias_use - impl ::mingling::Grouped<__MinglingProgram> for #type_name { + /// SAFETY: This is an internal implementation of the `pack_structural!` macro, + /// guaranteeing that the enum value registered by the `register_type!` macro + /// is exactly the same as the actual return value, + /// which can be confirmed via the `Ident` in the `quote!` block. + unsafe impl ::mingling::Grouped<__MinglingProgram> for #type_name { fn member_id() -> __MinglingProgram { __MinglingProgram::#type_name } } - impl ::mingling::__private::StructuralDataSealed for #type_name {} - impl ::mingling::__private::StructuralData for #type_name {} + impl ::mingling::__private::StructuralDataSealed<crate::ThisProgram> for #type_name {} + impl ::mingling::__private::StructuralData<crate::ThisProgram> for #type_name {} ::mingling::macros::register_type!(#type_name); } |
