From 4ac7d7dc9e6abec2f3f84dd5baf8b642727f19c3 Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Sat, 25 Apr 2026 23:41:36 +0800 Subject: Add help system with `#[help]` macro and `HelpRequest` trait --- mingling_macros/src/help.rs | 202 ++++++++++++++++++++++++++++++++++++++++++++ mingling_macros/src/lib.rs | 27 ++++++ 2 files changed, 229 insertions(+) create mode 100644 mingling_macros/src/help.rs (limited to 'mingling_macros/src') diff --git a/mingling_macros/src/help.rs b/mingling_macros/src/help.rs new file mode 100644 index 0000000..c0d8b99 --- /dev/null +++ b/mingling_macros/src/help.rs @@ -0,0 +1,202 @@ +//! Help Attribute Macro +//! +//! This module provides the `#[help]` attribute macro for automatically +//! generating structs that implement the `HelpRequest` trait from functions. +//! +//! # Syntax +//! +//! ```rust,ignore +//! #[help] +//! fn help_my_entry(prev: MyEntry) { +//! // use r_println! here +//! r_println!("Help: ..."); +//! } +//! ``` +//! +//! This expands to: +//! - A struct `HelpMyEntry` implementing `HelpRequest` with `Entry = MyEntry` +//! - The original function with injected `RenderResult` dummy context + +use proc_macro::TokenStream; +use quote::{ToTokens, quote}; +use syn::spanned::Spanned; +use syn::{ + FnArg, Ident, ItemFn, Pat, PatType, ReturnType, Signature, Type, TypePath, parse_macro_input, +}; + +/// Extracts the previous type and parameter name from function arguments +fn extract_previous_info(sig: &Signature) -> syn::Result<(Pat, TypePath)> { + // The function should have exactly one parameter + if sig.inputs.len() != 1 { + return Err(syn::Error::new( + sig.inputs.span(), + "Help function must have exactly one parameter (the entry type)", + )); + } + + // First and only parameter is the entry type + 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 + match &**ty { + Type::Path(type_path) => Ok((param_pat, type_path.clone())), + _ => Err(syn::Error::new( + ty.span(), + "Parameter type must be a type path", + )), + } + } + FnArg::Receiver(_) => Err(syn::Error::new( + arg.span(), + "Help function cannot have self parameter", + )), + } +} + +/// Validates the return type is () or empty +fn validate_return_type(sig: &Signature) -> syn::Result<()> { + match &sig.output { + ReturnType::Type(_, ty) => match &**ty { + Type::Tuple(tuple) if tuple.elems.is_empty() => Ok(()), + _ => Err(syn::Error::new( + ty.span(), + "Help function must return () or have no return type", + )), + }, + ReturnType::Default => Ok(()), + } +} + +pub 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 and parameter name from function arguments + let (prev_param, entry_type) = match extract_previous_info(&input_fn.sig) { + Ok(info) => info, + Err(e) => return e.to_compile_error().into(), + }; + + // Validate return type + if let Err(e) = validate_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 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; + + // 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()); + + // Register the help request mapping + let help_entry = build_help_entry(&struct_name, &entry_type); + let entry_str = help_entry.to_string(); + crate::HELP_REQUESTS.lock().unwrap().insert(entry_str); + + // Generate the struct and HelpRequest implementation + let expanded = quote! { + #(#fn_attrs)* + #[doc(hidden)] + #vis struct #struct_name; + + impl ::mingling::HelpRequest for #struct_name { + type Entry = #entry_type; + + fn render_help(#prev_param: Self::Entry, r: &mut ::mingling::RenderResult) { + // Create a local wrapper function that includes r parameter + // This allows r_println! to access r + #[allow(non_snake_case)] + fn help_wrapper(#prev_param: #entry_type, r: &mut ::mingling::RenderResult) { + #fn_body + } + + // Call the wrapper function + help_wrapper(#prev_param, r); + } + } + + ::mingling::macros::register_help!(#entry_type, #struct_name); + + // Keep the original function for internal use (without r parameter) + #(#fn_attrs)* + #vis fn #fn_name(#prev_param: #entry_type) { + let mut dummy_r = ::mingling::RenderResult::default(); + let r = &mut dummy_r; + #fn_body + } + }; + + 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, r); + } + } +} + +pub 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::parse_terminated); + + // Check that we have 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::(entry_type_expr.to_token_stream()) { + Ok(ty) => ty, + Err(e) => return e.to_compile_error().into(), + }; + + let struct_name = match syn::parse2::(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(); + crate::HELP_REQUESTS.lock().unwrap().insert(entry_str); + + quote! {}.into() +} diff --git a/mingling_macros/src/lib.rs b/mingling_macros/src/lib.rs index da3bf47..dbbc333 100644 --- a/mingling_macros/src/lib.rs +++ b/mingling_macros/src/lib.rs @@ -19,6 +19,7 @@ mod dispatcher; mod dispatcher_clap; mod enum_tag; mod groupped; +mod help; mod node; mod pack; mod program_setup; @@ -44,6 +45,8 @@ pub(crate) static CHAINS_EXIST: Lazy>> = Lazy::new(|| Mutex::new(BTreeSet::new())); pub(crate) static RENDERERS_EXIST: Lazy>> = Lazy::new(|| Mutex::new(BTreeSet::new())); +pub(crate) static HELP_REQUESTS: Lazy>> = + Lazy::new(|| Mutex::new(BTreeSet::new())); #[proc_macro] pub fn node(input: TokenStream) -> TokenStream { @@ -97,6 +100,16 @@ pub fn dispatcher_clap(attr: TokenStream, item: TokenStream) -> TokenStream { dispatcher_clap::dispatcher_clap_attr(attr, item) } +#[proc_macro] +pub fn register_help(input: TokenStream) -> TokenStream { + help::register_help(input) +} + +#[proc_macro_attribute] +pub fn help(_attr: TokenStream, item: TokenStream) -> TokenStream { + help::help_attr(item) +} + #[proc_macro_derive(Groupped, attributes(group))] pub fn derive_groupped(input: TokenStream) -> TokenStream { groupped::derive_groupped(input) @@ -308,6 +321,14 @@ pub fn program_final_gen(input: TokenStream) -> TokenStream { #[cfg(not(feature = "comp"))] let comp = quote! {}; + let help_tokens: Vec = HELP_REQUESTS + .lock() + .unwrap() + .clone() + .iter() + .map(|s| syn::parse_str::(s).unwrap()) + .collect(); + let expanded = quote! { #[derive(Debug, Default, PartialEq, Eq, Clone)] #[repr(u32)] @@ -340,6 +361,12 @@ pub fn program_final_gen(input: TokenStream) -> TokenStream { ::mingling::__dispatch_program_chains!( #(#chain_tokens)* ); + fn render_help(any: ::mingling::AnyOutput, r: &mut ::mingling::RenderResult) { + match any.member_id { + #(#help_tokens)* + _ => (), + } + } fn has_renderer(any: &::mingling::AnyOutput) -> bool { match any.member_id { #(#renderer_exist_tokens)* -- cgit