diff options
Diffstat (limited to 'mingling_macros/src/attr')
| -rw-r--r-- | mingling_macros/src/attr/chain.rs | 347 | ||||
| -rw-r--r-- | mingling_macros/src/attr/completion.rs | 239 | ||||
| -rw-r--r-- | mingling_macros/src/attr/dispatcher_clap.rs | 241 | ||||
| -rw-r--r-- | mingling_macros/src/attr/help.rs | 214 | ||||
| -rw-r--r-- | mingling_macros/src/attr/program_setup.rs | 116 | ||||
| -rw-r--r-- | mingling_macros/src/attr/renderer.rs | 253 |
6 files changed, 1410 insertions, 0 deletions
diff --git a/mingling_macros/src/attr/chain.rs b/mingling_macros/src/attr/chain.rs new file mode 100644 index 0000000..120e65d --- /dev/null +++ b/mingling_macros/src/attr/chain.rs @@ -0,0 +1,347 @@ +#![allow(clippy::too_many_arguments)] + +use crate::res_injection::{ + ResourceInjection, extract_args_info, generate_immut_resource_bindings, + wrap_body_with_mut_resources, wrap_body_with_mut_resources_async, +}; +use proc_macro::TokenStream; +use quote::{ToTokens, quote}; +use syn::spanned::Spanned; +use syn::{Ident, ItemFn, Pat, ReturnType, Signature, Type, TypePath, parse_macro_input}; + +/// Checks whether the return type is `()` +fn is_unit_return_type(sig: &Signature) -> bool { + match &sig.output { + ReturnType::Type(_, ty) => match &**ty { + Type::Tuple(tuple) => tuple.elems.is_empty(), + _ => false, + }, + ReturnType::Default => true, + } +} + +/// 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> { + // `()` or omitted is always valid + if is_unit_return_type(sig) { + return Ok(()); + } + + Ok(()) +} + +/// 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)] +fn generate_proc_fn( + 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(); + + let wrapped_body = if is_async_fn && !mut_resources.is_empty() { + wrap_body_with_mut_resources_async(fn_body_stmts, &mut_resources, program_type) + } else { + wrap_body_with_mut_resources(fn_body_stmts, &mut_resources, program_type, is_unit_return) + }; + + let proc_body = if is_unit_return { + let body_with_ending = if has_resources { + quote! { + #(#immut_resource_stmts)* + #wrapped_body; + <crate::ResultEmpty as ::mingling::Grouped::<crate::ThisProgram>> + ::to_chain(crate::ResultEmpty) + } + } else { + quote! { + #wrapped_body; + <crate::ResultEmpty as ::mingling::Grouped::<crate::ThisProgram>> + ::to_chain(crate::ResultEmpty) + } + }; + quote! { #body_with_ending } + } else { + let body = if has_resources { + quote! { + #(#immut_resource_stmts)* + #wrapped_body + } + } 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) + } + }; + + #[cfg(feature = "async")] + { + quote! { + async fn proc(#prev_param: #previous_type) -> ::mingling::ChainProcess<#program_type> { + #proc_body + } + } + } + + #[cfg(not(feature = "async"))] + { + quote! { + fn proc(#prev_param: #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 preserved original function. +fn generate_struct_and_impl( + fn_attrs: &[syn::Attribute], + vis: &syn::Visibility, + struct_name: &Ident, + previous_type: &TypePath, + previous_type_str: &proc_macro2::TokenStream, + program_type: &proc_macro2::TokenStream, + proc_fn: &proc_macro2::TokenStream, + original_fn: &proc_macro2::TokenStream, +) -> proc_macro2::TokenStream { + quote! { + #(#fn_attrs)* + #[doc(hidden)] + #[allow(non_camel_case_types)] + #vis struct #struct_name; + + ::mingling::macros::register_chain!(#previous_type_str, #struct_name); + + impl ::mingling::Chain<#program_type> for #struct_name { + type Previous = #previous_type; + + #proc_fn + } + + // Keep the original function unchanged + #original_fn + } +} + +/// Ensures the function is not async when the `async` feature is disabled. +#[cfg(not(feature = "async"))] +fn reject_async(sig: &Signature) -> Result<(), proc_macro2::TokenStream> { + if sig.asyncness.is_some() { + return Err(syn::Error::new( + sig.span(), + "Chain function cannot be async when async feature is disabled", + ) + .to_compile_error()); + } + Ok(()) +} + +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( + attr.into_iter().next().unwrap().span().into(), + "#[chain] does not accept arguments", + ) + .to_compile_error() + .into(); + } + + // Parse the function item + let input_fn = parse_macro_input!(item as ItemFn); + + // Handle async feature gate + #[cfg(feature = "async")] + let is_async_fn = input_fn.sig.asyncness.is_some(); + + #[cfg(not(feature = "async"))] + { + if let Err(err) = reject_async(&input_fn.sig) { + return err.into(); + } + } + + // Check if return type is unit + let is_unit_return = is_unit_return_type(&input_fn.sig); + + // Validate return type + if let Err(err) = validate_return_type(&input_fn.sig) { + return err.into(); + } + + // Extract the previous type, parameter name, and resource injection params + let (prev_param, 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; + let fn_name = &input_fn.sig.ident; + let has_resources = !resources.is_empty(); + + // Generate struct name + let internal_name = format!( + "__internal_chain_{}", + just_fmt::snake_case!(fn_name.to_string()) + ); + let struct_name = Ident::new(&internal_name, fn_name.span()); + + // 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( + 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. + // 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 + }; + + // Assemble the final output + let previous_type_str = quote! { #previous_type }; + let expanded = generate_struct_and_impl( + &fn_attrs, + vis, + &struct_name, + &previous_type, + &previous_type_str, + &program_type, + &proc_fn, + &original_fn, + ); + + expanded.into() +} + +/// Builds a match arm for chain mapping +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, + } +} + +/// Builds a match arm for chain existence check +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(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); + + // Check that there are exactly two elements + if input_parsed.len() != 2 { + return syn::Error::new( + input_parsed.span(), + "Expected exactly two comma-separated arguments: `PreviousType, StructName`", + ) + .to_compile_error() + .into(); + } + + // Extract the two elements + let previous_type_expr = &input_parsed[0]; + let struct_name_expr = &input_parsed[1]; + + // Convert expressions to TypePath and Ident + let previous_type = match syn::parse2::<TypePath>(previous_type_expr.to_token_stream()) { + Ok(ty) => ty, + Err(e) => return e.to_compile_error().into(), + }; + + let struct_name = match syn::parse2::<syn::Ident>(struct_name_expr.to_token_stream()) { + Ok(ident) => ident, + Err(e) => return e.to_compile_error().into(), + }; + + // Record the chain mapping: previous_type => struct_name + let chain_entry = build_chain_arm(&struct_name, &previous_type); + + // Record the chain existence check + let chain_exist_entry = build_chain_exist_arm(&previous_type); + + let mut chains = crate::get_global_set(&crate::CHAINS).lock().unwrap(); + let mut chain_exist = crate::get_global_set(&crate::CHAINS_EXIST).lock().unwrap(); + + let chain_entry_str = chain_entry.to_string(); + let chain_exist_entry_str = chain_exist_entry.to_string(); + + // Check for duplicate variant before inserting + let variant_name = previous_type + .path + .segments + .last() + .unwrap() + .ident + .to_string(); + if let Err(err) = crate::check_duplicate_variant( + &chains, + &chain_entry_str, + &variant_name, + "chain", + previous_type.span(), + ) { + return err.into(); + } + + chains.insert(chain_entry_str); + chain_exist.insert(chain_exist_entry_str); + + quote! {}.into() +} diff --git a/mingling_macros/src/attr/completion.rs b/mingling_macros/src/attr/completion.rs new file mode 100644 index 0000000..e917d7d --- /dev/null +++ b/mingling_macros/src/attr/completion.rs @@ -0,0 +1,239 @@ +use crate::res_injection::{ResourceInjection, generate_immut_resource_bindings}; +use proc_macro::TokenStream; +use quote::quote; +use syn::spanned::Spanned; +use syn::{FnArg, Ident, ItemFn, Pat, PatType, Type, TypePath, parse_macro_input}; + +#[cfg(feature = "comp")] +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() { + return syn::Error::new( + proc_macro2::Span::call_site(), + "completion attribute requires a previous type argument, e.g. #[completion(HelloEntry)]", + ) + .to_compile_error() + .into(); + } else { + parse_macro_input!(attr as TypePath) + }; + let previous_type_ident = &previous_type_path.path.segments.last().unwrap().ident; + + // 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(), "Completion function cannot be async") + .to_compile_error() + .into(); + } + + // Get the function signature parts + let sig = &input_fn.sig; + let inputs = &sig.inputs; + let output = &sig.output; + + // Must have at least one parameter ctx + if inputs.is_empty() { + return syn::Error::new( + inputs.span(), + "Completion function must have at least one parameter: `ctx: &ShellContext`", + ) + .to_compile_error() + .into(); + } + + // 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()) + } + FnArg::Receiver(_) => { + return syn::Error::new( + first_arg.span(), + "Completion function cannot have self parameter", + ) + .to_compile_error() + .into(); + } + }; + + // Extract resources from params 2 through N, skipping ctx + let resources = match extract_resources_from_args(sig, 1) { + Ok(r) => r, + Err(e) => return e.to_compile_error().into(), + }; + + // 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(); + fn_attrs.retain(|attr| !attr.path().is_ident("completion")); + + // Get function visibility + let vis = &input_fn.vis; + + // Get function name + let fn_name = &sig.ident; + + // Generate internal name from function name using snake_case + let internal_name = format!( + "__internal_completion_{}", + just_fmt::snake_case!(fn_name.to_string()) + ); + let struct_name = Ident::new(&internal_name, fn_name.span()); + + let program_type = crate::default_program_path(); + let has_resources = !resources.is_empty(); + let mut_resources: Vec<_> = resources.iter().filter(|r| r.is_mut).collect(); + + // 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)* } + } else { + let mut wrapped = quote! { #(#fn_body_stmts)* }; + 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| { + #wrapped + }) + }; + } + wrapped + }; + + let comp_body = if has_resources { + quote! { + #(#immut_resource_stmts)* + #wrapped_body + } + } else { + quote! { #(#fn_body_stmts)* } + }; + + // Generate the struct and implementation + // The `comp` trait method only takes `ctx` as the first parameter; resources are injected internally + + let expanded = quote! { + #(#fn_attrs)* + #[doc(hidden)] + #[allow(non_camel_case_types)] + #vis struct #struct_name; + + impl ::mingling::Completion for #struct_name { + type Previous = #previous_type_path; + + fn comp(#ctx_pat: &::mingling::ShellContext) #output { + #comp_body + } + } + + // Keep the original function for internal use + #(#fn_attrs)* + #vis fn #fn_name(#inputs) #output { + #fn_body + } + }; + + let completion_entry = quote! { + Self::#previous_type_ident => <#struct_name as ::mingling::Completion>::comp(ctx), + }; + + let mut completions = get_global_set(&crate::COMPLETIONS).lock().unwrap(); + let completion_str = completion_entry.to_string(); + + // Check for duplicate variant before inserting + let variant_name = previous_type_ident.to_string(); + if let Err(err) = crate::check_duplicate_variant( + &completions, + &completion_str, + &variant_name, + "completion", + previous_type_path.span(), + ) { + return err.into(); + } + + completions.insert(completion_str); + + expanded.into() +} + +/// Extract resource injection parameters from function arguments (skipping the first N params). +fn extract_resources_from_args( + sig: &syn::Signature, + skip: usize, +) -> syn::Result<Vec<ResourceInjection>> { + let mut resources = Vec::new(); + for arg in sig.inputs.iter().skip(skip) { + match arg { + FnArg::Typed(PatType { pat, ty, .. }) => { + let var_name = match &**pat { + Pat::Ident(pat_ident) => pat_ident.ident.clone(), + _ => { + return Err(syn::Error::new( + pat.span(), + "Resource injection parameter must be a simple identifier", + )); + } + }; + + let full_type = *(*ty).clone(); + + let (inner_type, is_ref, is_mut) = match &full_type { + Type::Reference(ref_type) => match &*ref_type.elem { + Type::Path(type_path) => { + let is_mut = ref_type.mutability.is_some(); + (type_path.clone(), true, is_mut) + } + _ => { + return Err(syn::Error::new( + ty.span(), + "Reference resource type must be a type path", + )); + } + }, + Type::Path(_) => { + return Err(syn::Error::new( + ty.span(), + "Resource injection parameter must be a reference (`&T` or `&mut T`)", + )); + } + _ => { + return Err(syn::Error::new( + ty.span(), + "Resource injection type must be a type path or reference", + )); + } + }; + + resources.push(ResourceInjection { + var_name, + full_type, + inner_type, + is_ref, + is_mut, + }); + } + FnArg::Receiver(_) => { + return Err(syn::Error::new( + arg.span(), + "Resource injection parameter cannot be self", + )); + } + } + } + Ok(resources) +} diff --git a/mingling_macros/src/attr/dispatcher_clap.rs b/mingling_macros/src/attr/dispatcher_clap.rs new file mode 100644 index 0000000..6083a52 --- /dev/null +++ b/mingling_macros/src/attr/dispatcher_clap.rs @@ -0,0 +1,241 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::{ + Ident, ItemStruct, LitBool, LitStr, Token, + parse::{Parse, ParseStream}, + parse_macro_input, +}; + +/// Parsed key-value options after the first positional arguments +struct ClapOptions { + /// `error = ErrorStruct` + error_struct: Option<Ident>, + /// `help = true` (bool only) + help_enabled: bool, +} + +impl Parse for ClapOptions { + fn parse(input: ParseStream) -> syn::Result<Self> { + let mut error_struct = None; + let mut help_enabled = false; + + while !input.is_empty() { + // Parse leading comma + input.parse::<Token![,]>()?; + + // Allow trailing comma + if input.is_empty() { + break; + } + + let key: Ident = input.parse()?; + input.parse::<Token![=]>()?; + + if key == "error" { + let value: Ident = input.parse()?; + if error_struct.is_some() { + return Err(syn::Error::new(key.span(), "duplicate `error` key")); + } + error_struct = Some(value); + } else if key == "help" { + let value: LitBool = input.parse()?; + if value.value() == false { + // help = false is allowed but does nothing + help_enabled = false; + } else { + help_enabled = true; + } + } else { + return Err(syn::Error::new( + key.span(), + "unknown key, expected `error` or `help`", + )); + } + } + + Ok(ClapOptions { + error_struct, + help_enabled, + }) + } +} + +/// Input for the dispatcher_clap attribute +struct DispatcherClapInput { + /// `("cmd", Disp, ...)` + command_name: LitStr, + dispatcher_struct: Ident, + options: ClapOptions, +} + +impl Parse for DispatcherClapInput { + fn parse(input: ParseStream) -> syn::Result<Self> { + // Format: "cmd", Disp, ... + let command_name: LitStr = input.parse()?; + input.parse::<Token![,]>()?; + let dispatcher_struct: Ident = input.parse()?; + + let options = if input.is_empty() { + ClapOptions { + error_struct: None, + help_enabled: false, + } + } else { + input.parse::<ClapOptions>()? + }; + + Ok(DispatcherClapInput { + command_name, + dispatcher_struct, + options, + }) + } +} + +#[cfg(feature = "clap")] +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; + + let program_path = crate::default_program_path(); + + let command_name_str = attr_input.command_name.value(); + let dispatcher_struct = &attr_input.dispatcher_struct; + let options = &attr_input.options; + + // Generate the `begin` method body + 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(); + } + match <#struct_name as ::clap::Parser>::try_parse_from(clap_args) { + Ok(parsed) => parsed.to_chain(), + Err(e) => { + return #error_struct::new(format!("{}", e.render().ansi())).to_render() + }, + } + } + } else { + quote! { + if ::mingling::this::<#program_path>().user_context.help { + return #struct_name::default().to_chain(); + } + let parsed = <#struct_name as ::clap::Parser>::try_parse_from(clap_args) + .unwrap_or_else(|e| e.exit()); + parsed.to_chain() + } + }; + + // Generate the error pack type + let error_pack = options.error_struct.as_ref().map(|error_struct| { + quote! { + ::mingling::macros::pack!(#error_struct = String); + } + }); + + // Generate the #[help] block if help = true + let help_gen = if options.help_enabled { + let dispatcher_name_str = dispatcher_struct.to_string(); + let help_fn_name_str = format!("__{}_help", just_fmt::snake_case!(&dispatcher_name_str)); + let help_fn_name = Ident::new(&help_fn_name_str, proc_macro2::Span::call_site()); + + Some(quote! { + #[allow(non_snake_case)] + #[::mingling::macros::help] + pub(crate) fn #help_fn_name(_prev: #struct_name) -> ::mingling::RenderResult { + use std::io::Write; + use clap::ColorChoice; + + let this = ::mingling::this::<#program_path>(); + match this.stdout_setting.clap_help_print_behaviour { + ::mingling::ClapHelpPrintBehaviour::WriteToRenderResult => { + let mut cmd = <#struct_name as ::clap::CommandFactory>::command() + .color(ColorChoice::Always); + let styled = cmd.render_help(); + let mut result = ::mingling::RenderResult::new(); + let _ = write!(result, "{}", styled.ansi()); + result + } + ::mingling::ClapHelpPrintBehaviour::PrintDirectly => { + let mut command = <#struct_name as ::clap::CommandFactory>::command(); + command.print_help().unwrap(); + ::mingling::RenderResult::new() + } + } + } + }) + } else { + None + }; + + let dispatch_tree_entry = + get_dispatch_tree_entry(&command_name_str, dispatcher_struct, &struct_name); + + let expanded = quote! { + // Keep the original struct definition + #input_struct + + // Generate the error wrapper type via pack! + #error_pack + + // Generate the help block if enabled + #help_gen + + // Dispatch tree registration (if feature enabled) + #dispatch_tree_entry + + // Generate the dispatcher struct + #[doc(hidden)] + #[derive(Default)] + pub(crate) struct #dispatcher_struct; + + impl ::mingling::Dispatcher<#program_path> for #dispatcher_struct { + fn node(&self) -> ::mingling::Node { + ::mingling::macros::node!(#command_name_str) + } + + fn begin( + &self, + args: Vec<String>, + ) -> ::mingling::ChainProcess<#program_path> { + // Prepend a dummy program name for clap's parse_from + let clap_args = std::iter::once(String::new()) + .chain(args) + .collect::<Vec<_>>(); + + #begin_body + } + + fn clone_dispatcher( + &self, + ) -> Box<dyn ::mingling::Dispatcher<#program_path>> { + Box::new(#dispatcher_struct) + } + } + }; + + expanded.into() +} + +#[cfg(feature = "dispatch_tree")] +fn get_dispatch_tree_entry( + command_name_str: &str, + dispatcher_struct: &Ident, + entry_name: &Ident, +) -> proc_macro2::TokenStream { + let node_name_lit = syn::LitStr::new(command_name_str, proc_macro2::Span::call_site()); + quote! { + ::mingling::macros::register_dispatcher!(#node_name_lit, #dispatcher_struct, #entry_name); + } +} + +#[cfg(not(feature = "dispatch_tree"))] +fn get_dispatch_tree_entry( + _command_name_str: &str, + _dispatcher_struct: &Ident, + _entry_name: &Ident, +) -> proc_macro2::TokenStream { + quote! {} +} diff --git a/mingling_macros/src/attr/help.rs b/mingling_macros/src/attr/help.rs new file mode 100644 index 0000000..6defae4 --- /dev/null +++ b/mingling_macros/src/attr/help.rs @@ -0,0 +1,214 @@ +use proc_macro::TokenStream; +use quote::{ToTokens, quote}; +use syn::spanned::Spanned; +use syn::{Ident, ItemFn, ReturnType, Signature, TypePath, parse_macro_input}; + +use crate::get_global_set; +use crate::res_injection::{extract_args_info, generate_immut_resource_bindings}; + +/// 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) => Some(quote! { #ty }), + ReturnType::Default => None, + } +} + +pub(crate) fn help_attr(item: TokenStream) -> TokenStream { + // 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(), "Help function cannot be async") + .to_compile_error() + .into(); + } + + // Extract the entry type, parameter name, and resource injection params + let (prev_param, entry_type, resources) = match extract_args_info(&input_fn.sig) { + Ok(info) => info, + Err(e) => 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; + let fn_body_stmts = &fn_body.stmts; + + // Get function attributes excluding the help attribute + let mut fn_attrs = input_fn.attrs.clone(); + fn_attrs.retain(|attr| !attr.path().is_ident("help")); + + // Get function visibility + let vis = &input_fn.vis; + + // Get function name + 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!( + "__internal_help_{}", + just_fmt::snake_case!(fn_name.to_string()) + ); + let struct_name = Ident::new(&internal_name, fn_name.span()); + + let program_type = crate::default_program_path(); + let has_resources = !resources.is_empty(); + let mut_resources: Vec<_> = resources.iter().filter(|r| r.is_mut).collect(); + + // 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 + + let wrapped_body = if mut_resources.is_empty() { + quote! { #(#fn_body_stmts)* } + } else { + let mut wrapped = quote! { #(#fn_body_stmts)* }; + 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| { + #wrapped + }) + }; + } + wrapped + }; + + let help_render_body = if has_resources { + quote! { + #(#immut_resource_stmts)* + #wrapped_body + } + } else { + quote! { #(#fn_body_stmts)* } + }; + + // Register the help request mapping + let help_entry = build_help_entry(&struct_name, &entry_type); + let entry_str = help_entry.to_string(); + + // Check for duplicate variant before inserting + let variant_name = entry_type.path.segments.last().unwrap().ident.to_string(); + { + let helps = get_global_set(&crate::HELP_REQUESTS).lock().unwrap(); + if let Err(err) = crate::check_duplicate_variant( + &helps, + &entry_str, + &variant_name, + "help", + entry_type.span(), + ) { + return err.into(); + } + } + + get_global_set(&crate::HELP_REQUESTS) + .lock() + .unwrap() + .insert(entry_str); + + // Generate the struct and HelpRequest implementation + let expanded = quote! { + #(#fn_attrs)* + #[doc(hidden)] + #[allow(non_camel_case_types)] + #vis struct #struct_name; + + impl ::mingling::HelpRequest for #struct_name { + type Entry = #entry_type; + + fn render_help(#prev_param: 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 + #[allow(dead_code)] + #(#fn_attrs)* + #vis fn #fn_name(#original_inputs) -> #original_return_type { + #(#fn_body_stmts)* + } + }; + + expanded.into() +} + +/// Builds a help request entry for the global help requests list +fn build_help_entry(struct_name: &Ident, entry_type: &TypePath) -> proc_macro2::TokenStream { + let enum_variant = &entry_type.path.segments.last().unwrap().ident; + quote! { + Self::#enum_variant => { + // SAFETY: The member_id check ensures that `any` contains a value of type `#entry_type`, + // so downcasting to `#entry_type` is safe. + let value = unsafe { any.downcast::<#entry_type>().unwrap_unchecked() }; + <#struct_name as ::mingling::HelpRequest>::render_help(value) + } + } +} + +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); + + // Check if there are exactly two elements + if input_parsed.len() != 2 { + return syn::Error::new( + input_parsed.span(), + "Expected exactly two comma-separated arguments: `EntryType, StructName`", + ) + .to_compile_error() + .into(); + } + + // Extract the two elements + let entry_type_expr = &input_parsed[0]; + let struct_name_expr = &input_parsed[1]; + + // Convert expressions to TypePath and Ident + let entry_type = match syn::parse2::<TypePath>(entry_type_expr.to_token_stream()) { + Ok(ty) => ty, + Err(e) => return e.to_compile_error().into(), + }; + + let struct_name = match syn::parse2::<syn::Ident>(struct_name_expr.to_token_stream()) { + Ok(ident) => ident, + Err(e) => return e.to_compile_error().into(), + }; + + // Register the help request mapping + let help_entry = build_help_entry(&struct_name, &entry_type); + let entry_str = help_entry.to_string(); + + // Check if entry was already pre-inserted by `#[help]` attribute + let mut helps = get_global_set(&crate::HELP_REQUESTS).lock().unwrap(); + if helps.contains(&entry_str) { + // Already registered by `#[help]`, no duplicate check needed + return quote! {}.into(); + } + + // Check for duplicate variant (different struct, same type) + let variant_name = entry_type.path.segments.last().unwrap().ident.to_string(); + if let Err(err) = + crate::check_duplicate_variant(&helps, &entry_str, &variant_name, "help", entry_type.span()) + { + return err.into(); + } + + helps.insert(entry_str); + + quote! {}.into() +} diff --git a/mingling_macros/src/attr/program_setup.rs b/mingling_macros/src/attr/program_setup.rs new file mode 100644 index 0000000..dee5a1c --- /dev/null +++ b/mingling_macros/src/attr/program_setup.rs @@ -0,0 +1,116 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::spanned::Spanned; +use syn::{FnArg, Ident, ItemFn, Pat, PatType, ReturnType, Signature, Type, parse_macro_input}; + +/// Extracts the program parameter from function arguments +fn extract_program_param(sig: &Signature) -> syn::Result<(Pat, Type)> { + // The function should have exactly one parameter + if sig.inputs.len() != 1 { + return Err(syn::Error::new( + sig.inputs.span(), + "Setup function must have exactly one parameter", + )); + } + + let arg = &sig.inputs[0]; + match arg { + FnArg::Typed(PatType { pat, ty, .. }) => { + // Extract the pattern (parameter name) + let param_pat = (**pat).clone(); + // Extract the type as-is + let param_type = (**ty).clone(); + Ok((param_pat, param_type)) + } + FnArg::Receiver(_) => Err(syn::Error::new( + arg.span(), + "Setup function cannot have self parameter", + )), + } +} + +/// Extracts and validates the return type +fn extract_return_type(sig: &Signature) -> syn::Result<()> { + // Setup functions should return () or have no return type + match &sig.output { + ReturnType::Type(_, ty) => { + // Check if it's () + match &**ty { + Type::Tuple(tuple) if tuple.elems.is_empty() => Ok(()), + _ => Err(syn::Error::new( + ty.span(), + "Setup function must return () or have no return type", + )), + } + } + ReturnType::Default => Ok(()), + } +} + +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(); + + // 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(), "Setup function cannot be async") + .to_compile_error() + .into(); + } + + // Extract the program parameter + let (program_param, program_type) = match extract_program_param(&input_fn.sig) { + Ok(info) => info, + Err(e) => return e.to_compile_error().into(), + }; + + // Validate return type + if let Err(e) = extract_return_type(&input_fn.sig) { + return e.to_compile_error().into(); + } + + // Get the function body + let fn_body = &input_fn.block; + + // Get function attributes (excluding the setup attribute) + let mut fn_attrs = input_fn.attrs.clone(); + + // Remove any #[program_setup(...)] attributes to avoid infinite recursion + fn_attrs.retain(|attr| !attr.path().is_ident("setup")); + + // Get function visibility + let vis = &input_fn.vis; + + // Get function name + let fn_name = &input_fn.sig.ident; + + // Generate struct name from function name using pascal_case + let pascal_case_name = just_fmt::pascal_case!(fn_name.to_string()); + let struct_name = Ident::new(&pascal_case_name, fn_name.span()); + + // Generate the struct and implementation + let expanded = quote! { + #(#fn_attrs)* + #[doc(hidden)] + #vis struct #struct_name; + + impl ::mingling::setup::ProgramSetup<#program_path> for #struct_name { + fn setup(self, program: &mut ::mingling::Program<#program_path>) { + // Call the original function with the actual Program type + #fn_name(program); + } + } + + // Keep the original function for internal use + #(#fn_attrs)* + #vis fn #fn_name(#program_param: #program_type) { + #fn_body + } + }; + + expanded.into() +} diff --git a/mingling_macros/src/attr/renderer.rs b/mingling_macros/src/attr/renderer.rs new file mode 100644 index 0000000..c7cbb0b --- /dev/null +++ b/mingling_macros/src/attr/renderer.rs @@ -0,0 +1,253 @@ +use proc_macro::TokenStream; +use quote::{ToTokens, quote}; +use syn::spanned::Spanned; +use syn::{ItemFn, ReturnType, Signature, TypePath, parse_macro_input}; + +use crate::get_global_set; +use crate::res_injection::{extract_args_info, generate_immut_resource_bindings}; + +/// 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) => Some(quote! { #ty }), + ReturnType::Default => None, + } +} + +#[allow(clippy::too_many_lines)] +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(); + let program_type = &program_path; + + // 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(), "Renderer function cannot be async") + .to_compile_error() + .into(); + } + + // Extract the previous type, parameter name, and resource injection params + let (prev_param, previous_type, resources) = match extract_args_info(&input_fn.sig) { + Ok(info) => info, + Err(e) => 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(); + + // Get function attributes (excluding the renderer attribute) + let mut fn_attrs = input_fn.attrs.clone(); + + // Remove any #[renderer(...)] attributes to avoid infinite recursion + fn_attrs.retain(|attr| !attr.path().is_ident("renderer")); + + // Get function visibility + let vis = &input_fn.vis; + + // Get function name + let fn_name = &input_fn.sig.ident; + + // Generate struct name from function name using pascal_case + let internal_name = format!( + "__internal_renderer_{}", + just_fmt::snake_case!(fn_name.to_string()) + ); + let struct_name = syn::Ident::new(&internal_name, fn_name.span()); + + let has_resources = !resources.is_empty(); + let has_mut_resources = resources.iter().any(|r| r.is_mut); + + // Generate resource bindings for immutable resources + 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)* }; + 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| { + #wrapped + }) + }; + } + wrapped + } else { + quote! { #(#fn_body_stmts)* } + }; + + // Build the Renderer::render body with resource injection + // The user's body now directly creates and returns a RenderResult. + let render_fn_body = if has_resources { + quote! { + #(#immut_resource_stmts)* + #inner_body_with_resources + } + } else { + quote! { #inner_body_with_resources } + }; + + // 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 = user_return_type.clone().unwrap_or(quote! { () }); + + let expanded = quote! { + #(#fn_attrs)* + #[doc(hidden)] + #[allow(non_camel_case_types)] + #vis struct #struct_name; + + ::mingling::macros::register_renderer!(#previous_type, #struct_name); + + impl ::mingling::Renderer for #struct_name { + type Previous = #previous_type; + + fn render(#prev_param: 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)* + } + }; + + expanded.into() +} + +/// Builds the renderer entry for the global renderers list +pub(crate) fn build_renderer_entry( + struct_name: &syn::Ident, + previous_type: &TypePath, +) -> proc_macro2::TokenStream { + let enum_variant = &previous_type.path.segments.last().unwrap().ident; + quote! { + #struct_name => #enum_variant, + } +} + +/// Builds the renderer existence check entry +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, + } +} + +/// Builds the structural renderer entry +#[cfg(feature = "structural_renderer")] +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 => { + // SAFETY: Only types that match will enter this branch for forced conversion, + // and `AnyOutput::new` ensures the type implements serde::Serialize + let raw = unsafe { any.restore::<#previous_type>().unwrap_unchecked() }; + let mut __renderer_inner_result = ::mingling::RenderResult::default(); + ::mingling::StructuralRenderer::render(&raw, setting, &mut __renderer_inner_result)?; + Ok(__renderer_inner_result) + } + } +} + +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); + + // Check that there are exactly two elements + if input_parsed.len() != 2 { + return syn::Error::new( + input_parsed.span(), + "Expected exactly two comma-separated arguments: `PreviousType, StructName`", + ) + .to_compile_error() + .into(); + } + + // Extract the two elements + let previous_type_expr = &input_parsed[0]; + let struct_name_expr = &input_parsed[1]; + + // Convert expressions to TypePath and Ident + let previous_type = match syn::parse2::<TypePath>(previous_type_expr.to_token_stream()) { + Ok(ty) => ty, + Err(e) => return e.to_compile_error().into(), + }; + + let struct_name = match syn::parse2::<syn::Ident>(struct_name_expr.to_token_stream()) { + Ok(ident) => ident, + Err(e) => return e.to_compile_error().into(), + }; + + // Register the renderer in the global list + let renderer_entry = build_renderer_entry(&struct_name, &previous_type); + let renderer_exist_entry = build_renderer_exist_entry(&previous_type); + #[cfg(feature = "structural_renderer")] + let structural_renderer_entry = build_structural_renderer_entry(&previous_type); + + let renderer_entry_str = renderer_entry.to_string(); + let renderer_exist_entry_str = renderer_exist_entry.to_string(); + + #[cfg(feature = "structural_renderer")] + let structural_renderer_entry_str = structural_renderer_entry.to_string(); + + // Check for duplicate variant before acquiring other locks + let variant_name = previous_type + .path + .segments + .last() + .unwrap() + .ident + .to_string(); + { + let renderers = get_global_set(&crate::RENDERERS).lock().unwrap(); + if let Err(err) = crate::check_duplicate_variant( + &renderers, + &renderer_entry_str, + &variant_name, + "renderer", + previous_type.span(), + ) { + return err.into(); + } + } // renderers lock released here + + let mut renderers = get_global_set(&crate::RENDERERS).lock().unwrap(); + let mut renderer_exist = get_global_set(&crate::RENDERERS_EXIST).lock().unwrap(); + + #[cfg(feature = "structural_renderer")] + let mut structural_renderers = get_global_set(&crate::STRUCTURAL_RENDERERS).lock().unwrap(); + + renderers.insert(renderer_entry_str); + renderer_exist.insert(renderer_exist_entry_str); + + // Only register structural renderer if the type is in STRUCTURED_TYPES + #[cfg(feature = "structural_renderer")] + { + let is_structured = get_global_set(&crate::STRUCTURED_TYPES) + .lock() + .unwrap() + .contains(&variant_name); + if is_structured { + structural_renderers.insert(structural_renderer_entry_str); + } + } + + quote! {}.into() +} |
