diff options
Diffstat (limited to 'mingling_macros/src')
33 files changed, 1380 insertions, 1276 deletions
diff --git a/mingling_macros/src/attr.rs b/mingling_macros/src/attr.rs index e4cd826..ed7f58b 100644 --- a/mingling_macros/src/attr.rs +++ b/mingling_macros/src/attr.rs @@ -4,6 +4,7 @@ pub(crate) mod completion; #[cfg(feature = "clap")] pub(crate) mod dispatcher_clap; pub(crate) mod help; +pub(crate) mod mlint; #[cfg(feature = "extra_macros")] pub(crate) mod program_setup; pub(crate) mod renderer; diff --git a/mingling_macros/src/attr/help.rs b/mingling_macros/src/attr/help.rs index aa7bc88..b4ad989 100644 --- a/mingling_macros/src/attr/help.rs +++ b/mingling_macros/src/attr/help.rs @@ -1,5 +1,5 @@ use proc_macro::TokenStream; -use quote::{ToTokens, quote}; +use quote::quote; use syn::spanned::Spanned; use syn::{Ident, ItemFn, Pat, ReturnType, Signature, TypePath, parse_macro_input}; @@ -154,7 +154,7 @@ pub(crate) fn help_attr(item: TokenStream) -> TokenStream { ::mingling::macros::register_help!(#entry_type, #struct_name); - // Keep the original function unchanged + // Keep the original function unchanged #(#fn_attrs)* #vis fn #fn_name(#original_inputs) -> #original_return_type { #(#fn_body_stmts)* @@ -176,56 +176,3 @@ fn build_help_entry(struct_name: &Ident, entry_type: &TypePath) -> proc_macro2:: } } } - -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/mlint.rs b/mingling_macros/src/attr/mlint.rs new file mode 100644 index 0000000..176ef0a --- /dev/null +++ b/mingling_macros/src/attr/mlint.rs @@ -0,0 +1,7 @@ +use proc_macro::TokenStream; + +/// Marker attribute for the Mingling lint system. +/// All it does is pass the item through unchanged. +pub(crate) fn mlint(_attr: TokenStream, item: TokenStream) -> TokenStream { + item +} diff --git a/mingling_macros/src/derive.rs b/mingling_macros/src/derive.rs index ffa405b..666a36e 100644 --- a/mingling_macros/src/derive.rs +++ b/mingling_macros/src/derive.rs @@ -1,2 +1,4 @@ pub(crate) mod enum_tag; pub(crate) mod grouped; +#[cfg(feature = "structural_renderer")] +pub(crate) mod structural_data; diff --git a/mingling_macros/src/derive/structural_data.rs b/mingling_macros/src/derive/structural_data.rs new file mode 100644 index 0000000..acb2f28 --- /dev/null +++ b/mingling_macros/src/derive/structural_data.rs @@ -0,0 +1,26 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::{DeriveInput, parse_macro_input}; + +use crate::get_global_set; + +/// Derive macro for `StructuralData`. +pub(crate) fn derive_structural_data(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let type_name = input.ident; + + // Register in STRUCTURED_TYPES + let type_name_str = type_name.to_string(); + get_global_set(&crate::STRUCTURED_TYPES) + .lock() + .unwrap() + .insert(type_name_str); + + // Generate BOTH the sealed impl AND the StructuralData impl. + let expanded = quote! { + impl ::mingling::__private::StructuralDataSealed<crate::ThisProgram> for #type_name {} + impl ::mingling::__private::StructuralData<crate::ThisProgram> for #type_name {} + }; + + expanded.into() +} diff --git a/mingling_macros/src/func.rs b/mingling_macros/src/func.rs index 33bb094..d836b38 100644 --- a/mingling_macros/src/func.rs +++ b/mingling_macros/src/func.rs @@ -1,13 +1,40 @@ pub(crate) mod dispatcher; #[cfg(feature = "extra_macros")] +pub(crate) mod empty_result; +#[cfg(feature = "extra_macros")] pub(crate) mod entry; pub(crate) mod gen_program; #[cfg(feature = "extra_macros")] pub(crate) mod group; +#[cfg(all(feature = "structural_renderer", feature = "extra_macros"))] +pub(crate) mod group_structural; pub(crate) mod node; pub(crate) mod pack; #[cfg(feature = "extra_macros")] pub(crate) mod pack_err; +#[cfg(all(feature = "structural_renderer", feature = "extra_macros"))] +pub(crate) mod pack_err_structural; +#[cfg(feature = "structural_renderer")] +pub(crate) mod pack_structural; +#[cfg(feature = "comp")] +pub(crate) mod program_comp_gen; +pub(crate) mod program_fallback_gen; +pub(crate) mod program_final_gen; +pub(crate) mod r_append; +pub(crate) mod r_eprint; +pub(crate) mod r_eprintln; pub(crate) mod r_print; +pub(crate) mod r_println; +pub(crate) mod register_chain; +pub(crate) mod register_dispatcher; +pub(crate) mod register_help; +pub(crate) mod register_renderer; +pub(crate) mod register_type; +#[cfg(feature = "extra_macros")] +pub(crate) mod render_route; +#[cfg(feature = "extra_macros")] +pub(crate) mod route; #[cfg(feature = "comp")] pub(crate) mod suggest; +#[cfg(feature = "comp")] +pub(crate) mod suggest_enum; diff --git a/mingling_macros/src/func/dispatcher.rs b/mingling_macros/src/func/dispatcher.rs index a61dd26..4e7c42f 100644 --- a/mingling_macros/src/func/dispatcher.rs +++ b/mingling_macros/src/func/dispatcher.rs @@ -1,13 +1,8 @@ -#[cfg(feature = "dispatch_tree")] -use just_fmt::snake_case; use proc_macro::TokenStream; use proc_macro2::TokenStream as TokenStream2; use quote::quote; use syn::parse::{Parse, ParseStream}; -use syn::{Attribute, Ident, LitStr, Result as SynResult, Token}; - -#[cfg(feature = "dispatch_tree")] -use crate::COMPILE_TIME_DISPATCHERS; +use syn::{Attribute, Ident, LitStr, Token}; enum DispatcherChainInput { Default { @@ -25,7 +20,7 @@ enum DispatcherChainInput { } impl Parse for DispatcherChainInput { - fn parse(input: ParseStream) -> SynResult<Self> { + fn parse(input: ParseStream) -> syn::Result<Self> { // Collect outer attributes for the CMD struct let cmd_attrs = input.call(Attribute::parse_outer)?; @@ -184,76 +179,13 @@ fn get_dispatch_tree_entry( quote! {} } -#[cfg(feature = "dispatch_tree")] -/// Input format: ("node.name", DispatcherType, EntryName) -struct RegisterDispatcherInput { - node_name: syn::LitStr, - dispatcher_type: Ident, - entry_name: Ident, -} - -#[cfg(feature = "dispatch_tree")] -impl Parse for RegisterDispatcherInput { - fn parse(input: ParseStream) -> SynResult<Self> { - let node_name = input.parse()?; - input.parse::<Token![,]>()?; - let dispatcher_type = input.parse()?; - input.parse::<Token![,]>()?; - let entry_name = input.parse()?; - Ok(RegisterDispatcherInput { - node_name, - dispatcher_type, - entry_name, - }) - } -} - -#[cfg(feature = "dispatch_tree")] -pub(crate) fn register_dispatcher(input: TokenStream) -> TokenStream { - let RegisterDispatcherInput { - node_name, - dispatcher_type, - entry_name, - } = syn::parse_macro_input!(input as RegisterDispatcherInput); - - let node_name_str = node_name.value(); - let static_name = format!( - "__internal_dispatcher_{}", - snake_case!(node_name_str.clone()) - ); - let static_ident = Ident::new(&static_name, proc_macro2::Span::call_site()); - - // Register node info in the global collection at compile time - // Format: "node.name:DispatcherType:EntryName" - crate::get_global_set(&COMPILE_TIME_DISPATCHERS) - .lock() - .unwrap() - .insert(format!( - "{}:{}:{}", - node_name_str, dispatcher_type, entry_name - )); - - let expanded = quote! { - #[doc(hidden)] - #[allow(nonstandard_style)] - pub static #static_ident: #dispatcher_type = #dispatcher_type; - }; - - expanded.into() -} - -#[cfg(not(feature = "dispatch_tree"))] -pub(crate) fn register_dispatcher(_input: TokenStream) -> TokenStream { - quote! {}.into() -} - /// Converts a dotted command name (e.g. "remote.add") to `PascalCase` (e.g. "`RemoteAdd`"). /// /// Each segment is split by `.`, the first character of each segment is uppercased, /// and the segments are joined. This is used by the abbreviated `dispatcher!` syntax /// (when `Command => Entry` is omitted) to auto-derive struct names. #[cfg(feature = "extra_macros")] -fn dotted_to_pascal_case(s: &str) -> String { +pub(crate) fn dotted_to_pascal_case(s: &str) -> String { s.split('.') .map(|segment| { let mut chars = segment.chars(); diff --git a/mingling_macros/src/func/empty_result.rs b/mingling_macros/src/func/empty_result.rs new file mode 100644 index 0000000..9b5c78c --- /dev/null +++ b/mingling_macros/src/func/empty_result.rs @@ -0,0 +1,11 @@ +use proc_macro::TokenStream; +use quote::quote; + +/// Creates an empty result value wrapped in `ChainProcess` for early return +/// from a chain function. +pub(crate) fn empty_result(_input: TokenStream) -> TokenStream { + let expanded = quote! { + <crate::ResultEmpty as ::mingling::Routable::<crate::ThisProgram>>::to_chain(crate::ResultEmpty) + }; + TokenStream::from(expanded) +} diff --git a/mingling_macros/src/func/gen_program.rs b/mingling_macros/src/func/gen_program.rs index db82504..c5358bc 100644 --- a/mingling_macros/src/func/gen_program.rs +++ b/mingling_macros/src/func/gen_program.rs @@ -1,90 +1,11 @@ -use std::collections::HashMap; - use proc_macro::TokenStream; use quote::quote; -use syn::parse_macro_input; - -use crate::CHAINS; -use crate::CHAINS_EXIST; -#[cfg(feature = "dispatch_tree")] -use crate::COMPILE_TIME_DISPATCHERS; -#[cfg(feature = "comp")] -use crate::COMPLETIONS; -use crate::HELP_REQUESTS; -use crate::PACKED_TYPES; -use crate::RENDERERS; -use crate::RENDERERS_EXIST; -#[cfg(feature = "structural_renderer")] -use crate::STRUCTURAL_RENDERERS; -use crate::attr::{chain, renderer}; -use crate::get_global_set; -#[cfg(feature = "dispatch_tree")] -use crate::systems::dispatch_tree_gen; - -#[cfg(feature = "async")] -const ASYNC_ENABLED: bool = true; -#[cfg(not(feature = "async"))] -const ASYNC_ENABLED: bool = false; - -/// Parses an entry of the format `StructName => EnumVariant,` into a pair of idents. -fn parse_entry_pair(entry: &proc_macro2::TokenStream) -> (proc_macro2::Ident, proc_macro2::Ident) { - let s = entry.to_string(); - let arrow_idx = s - .find("=>") - .unwrap_or_else(|| panic!("Entry missing '=>': {s}")); - let struct_str = s[..arrow_idx].trim(); - let variant_str = s[arrow_idx + 2..].trim().trim_end_matches(',').trim(); - let struct_ident = proc_macro2::Ident::new(struct_str, proc_macro2::Span::call_site()); - let variant_ident = proc_macro2::Ident::new(variant_str, proc_macro2::Span::call_site()); - (struct_ident, variant_ident) -} - -/// Loads the pathf type mapping from `$OUT_DIR/{crate}/type_using.rs`. -/// Always compiled; returns None when pathf feature is not enabled, -/// or when the file does not exist / fails to load. -fn load_pathf_map() -> Option<std::collections::HashMap<String, String>> { - if !cfg!(feature = "pathf") { - return None; - } - let out_dir = std::env::var("OUT_DIR").ok()?; - let crate_name = std::env::var("CARGO_PKG_NAME").ok()?; - let path = std::path::Path::new(&out_dir) - .join(&crate_name) - .join("type_using.rs"); - let content = std::fs::read_to_string(&path).ok()?; - Some( - content - .lines() - .filter_map(|line| { - let line = line.trim(); - if let Some(rest) = line.strip_prefix("use ") { - let path = rest.strip_suffix(';').unwrap_or(rest); - if let Some((_mod, type_name)) = path.rsplit_once("::") { - return Some((type_name.to_string(), path.to_string())); - } - } - None - }) - .collect(), - ) -} - -/// Resolves a type name to its full path token stream using the pathf mapping. -pub(crate) fn resolve_type( - name: &str, - map: &std::collections::HashMap<String, String>, -) -> proc_macro2::TokenStream { - if let Some(full_path) = map.get(name) { - syn::parse_str::<proc_macro2::TokenStream>(full_path).unwrap_or_else(|_| { - let ident = proc_macro2::Ident::new(name, proc_macro2::Span::call_site()); - quote! { #ident } - }) - } else { - let ident = proc_macro2::Ident::new(name, proc_macro2::Span::call_site()); - quote! { #ident } - } -} +/// Entry point for `gen_program!()`. +/// +/// Generates the `Next` type alias, `Routable` impl for `ChainProcess`, +/// and delegates to `program_comp_gen!()`, `program_fallback_gen!()`, +/// and `program_final_gen!()`. pub(crate) fn gen_program_impl(_input: TokenStream) -> TokenStream { #[cfg(feature = "comp")] let comp_gen = quote! { @@ -124,505 +45,3 @@ pub(crate) fn gen_program_impl(_input: TokenStream) -> TokenStream { ::mingling::macros::program_final_gen!(); }) } - -#[cfg(feature = "comp")] -pub(crate) fn program_comp_gen_impl(_input: TokenStream) -> TokenStream { - #[cfg(feature = "async")] - let fn_exec_comp = quote! { - #[doc(hidden)] - #[::mingling::macros::chain] - pub async fn __exec_completion(prev: CompletionContext) -> Next { - use ::mingling::Grouped; - - let read_ctx = ::mingling::ShellContext::try_from(prev.inner); - match read_ctx { - Ok(ctx) => { - let suggest = ::mingling::CompletionHelper::exec_completion::<crate::ThisProgram>(&ctx); - ::mingling::Routable::<crate::ThisProgram>::to_render(crate::CompletionSuggest::new((ctx, suggest))) - } - Err(_) => std::process::exit(1), - } - } - }; - - #[cfg(not(feature = "async"))] - let fn_exec_comp = quote! { - #[doc(hidden)] - #[::mingling::macros::chain] - pub fn __exec_completion(prev: CompletionContext) -> Next { - use ::mingling::Grouped; - - let read_ctx = ::mingling::ShellContext::try_from(prev.inner); - match read_ctx { - Ok(ctx) => { - let suggest = ::mingling::CompletionHelper::exec_completion::<crate::ThisProgram>(&ctx); - ::mingling::Routable::<crate::ThisProgram>::to_render(crate::CompletionSuggest::new((ctx, suggest))) - } - Err(_) => std::process::exit(1), - } - } - }; - - #[cfg(feature = "dispatch_tree")] - let internal_dispatcher_comp = quote! { - use __internal_completion_mod::__internal_dispatcher_comp; - }; - - #[cfg(not(feature = "dispatch_tree"))] - let internal_dispatcher_comp = quote! {}; - - let comp_dispatcher = quote! { - #[doc(hidden)] - mod __internal_completion_mod { - use ::mingling::Grouped; - ::mingling::macros::dispatcher!("__comp", CMDCompletion => CompletionContext); - ::mingling::macros::pack!( - CompletionSuggest = (::mingling::ShellContext, ::mingling::Suggest) - ); - } - #internal_dispatcher_comp - use __internal_completion_mod::CompletionContext; - use __internal_completion_mod::CompletionSuggest; - pub use __internal_completion_mod::CMDCompletion; - - #fn_exec_comp - - ::mingling::macros::register_type!(CompletionContext); - - #[allow(unused)] - #[doc(hidden)] - #[::mingling::macros::renderer] - pub fn __render_completion(prev: CompletionSuggest) -> ::mingling::RenderResult { - let result = ::mingling::RenderResult::default(); - let (ctx, suggest) = prev.inner; - ::mingling::CompletionHelper::render_suggest::<crate::ThisProgram>(ctx, suggest); - result - } - }; - - TokenStream::from(comp_dispatcher) -} - -pub(crate) fn register_type_impl(input: TokenStream) -> TokenStream { - let type_ident = parse_macro_input!(input as syn::Ident); - let entry_str = type_ident.to_string(); - - get_global_set(&PACKED_TYPES) - .lock() - .unwrap() - .insert(entry_str); - - TokenStream::new() -} - -pub(crate) fn register_chain_impl(input: TokenStream) -> TokenStream { - chain::register_chain(input) -} - -pub(crate) fn register_renderer_impl(input: TokenStream) -> TokenStream { - renderer::register_renderer(input) -} - -pub(crate) fn program_fallback_gen_impl(_input: TokenStream) -> TokenStream { - #[cfg(feature = "structural_renderer")] - let pack_empty = quote! { - #[derive(::serde::Serialize, ::mingling::StructuralData, ::mingling::Grouped, Default)] - pub struct ResultEmpty; - }; - - #[cfg(not(feature = "structural_renderer"))] - let pack_empty = quote! { - #[derive(::mingling::Grouped, Default)] - pub struct ResultEmpty; - }; - - let expanded = quote! { - ::mingling::macros::pack!(ErrorRendererNotFound = String); - ::mingling::macros::pack!(ErrorDispatcherNotFound = Vec<String>); - #pack_empty - }; - TokenStream::from(expanded) -} - -#[allow(clippy::too_many_lines)] -pub(crate) fn program_final_gen_impl(_input: TokenStream) -> TokenStream { - let name = syn::Ident::new("ThisProgram", proc_macro2::Span::call_site()); - - let packed_types = get_global_set(&PACKED_TYPES).lock().unwrap().clone(); - - let renderers = get_global_set(&RENDERERS).lock().unwrap().clone(); - let chains = get_global_set(&CHAINS).lock().unwrap().clone(); - let renderer_exist = get_global_set(&RENDERERS_EXIST).lock().unwrap().clone(); - let chain_exist = get_global_set(&CHAINS_EXIST).lock().unwrap().clone(); - - #[cfg(feature = "structural_renderer")] - let structural_renderers = get_global_set(&STRUCTURAL_RENDERERS) - .lock() - .unwrap() - .clone(); - - #[cfg(feature = "comp")] - let completions = get_global_set(&COMPLETIONS).lock().unwrap().clone(); - - let packed_types: Vec<proc_macro2::TokenStream> = packed_types - .iter() - .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap()) - .collect(); - - let renderer_tokens: Vec<proc_macro2::TokenStream> = renderers - .iter() - .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap()) - .collect(); - - let chain_tokens: Vec<proc_macro2::TokenStream> = chains - .iter() - .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap()) - .collect(); - - let renderer_exist_tokens: Vec<proc_macro2::TokenStream> = renderer_exist - .iter() - .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap()) - .collect(); - - let chain_exist_tokens: Vec<proc_macro2::TokenStream> = chain_exist - .iter() - .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap()) - .collect(); - - let pathf_map: Option<HashMap<String, String>> = load_pathf_map(); - - #[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 { - 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") { - pathf_map - .values() - .map(|path| format!("use {};", path).parse().unwrap_or_default()) - .collect() - } else { - Vec::new() - }; - - #[cfg(feature = "structural_renderer")] - let structural_renderer_tokens: Vec<proc_macro2::TokenStream> = structural_renderers - .iter() - .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap()) - .collect(); - - #[cfg(feature = "structural_renderer")] - let structural_render = quote! { - fn structural_render( - any: ::mingling::AnyOutput<Self::Enum>, - setting: &::mingling::StructuralRendererSetting, - ) -> Result<::mingling::RenderResult, ::mingling::error::StructuralRendererSerializeError> { - #[allow(unused_imports)] - #(#pathf_uses)* - match any.member_id() { - #(#structural_renderer_tokens)* - _ => { - // Non-structural types: render ResultEmpty (which implements - // StructuralData + Serialize) instead of producing nothing. - let mut r = ::mingling::RenderResult::default(); - ::mingling::StructuralRenderer::render(&ResultEmpty, setting, &mut r)?; - Ok(r) - } - } - } - }; - - #[cfg(not(feature = "structural_renderer"))] - let structural_render = quote! {}; - - #[cfg(feature = "dispatch_tree")] - let compile_time_dispatchers: Vec<String> = get_global_set(&COMPILE_TIME_DISPATCHERS) - .lock() - .unwrap() - .clone() - .iter() - .cloned() - .collect(); - - #[cfg(feature = "dispatch_tree")] - let dispatch_tree_nodes = { - let entries: Vec<(String, String, String)> = compile_time_dispatchers - .iter() - .filter_map(|entry| { - let parts: Vec<&str> = entry.split(':').collect(); - if parts.len() == 3 { - Some(( - parts[0].to_string(), - parts[1].to_string(), - parts[2].to_string(), - )) - } else { - None - } - }) - .collect(); - - let get_nodes_fn = dispatch_tree_gen::gen_get_nodes(&entries, &pathf_map); - let dispatch_trie_fn = dispatch_tree_gen::gen_dispatch_args_trie(&entries, &pathf_map); - - quote! { - #get_nodes_fn - #dispatch_trie_fn - } - }; - - #[cfg(not(feature = "dispatch_tree"))] - let dispatch_tree_nodes = quote! {}; - - #[cfg(feature = "comp")] - let completion_tokens: Vec<proc_macro2::TokenStream> = completions - .iter() - .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap()) - .collect(); - - #[cfg(feature = "comp")] - let comp = quote! { - fn do_comp(any: &::mingling::AnyOutput<Self::Enum>, ctx: &::mingling::ShellContext) -> ::mingling::Suggest { - #[allow(unused_imports)] - #(#pathf_uses)* - match any.member_id() { - #(#completion_tokens)* - _ => ::mingling::Suggest::FileCompletion, - } - } - }; - - #[cfg(not(feature = "comp"))] - let comp = quote! {}; - - // Build render function arms from stored entries - let render_fn = - if renderer_tokens.is_empty() { - quote! { - fn render(_any: ::mingling::AnyOutput<Self::Enum>) -> ::mingling::RenderResult { - ::mingling::RenderResult::default() - } - } - } else { - let render_arms: Vec<_> = renderer_tokens.iter().map(|entry| { - let (struct_ident, variant_ident) = parse_entry_pair(entry); - let downcast_ty = resolve_type(&variant_ident.to_string(), &pathf_map); - let resolved_struct = resolve_type(&struct_ident.to_string(), &pathf_map); - quote! { - Self::#variant_ident => { - // SAFETY: The `type_id` check ensures that `any` contains a value of type `#variant_ident`, - // so downcasting to `#variant_ident` is safe. - let value = unsafe { any.downcast::<#downcast_ty>().unwrap_unchecked() }; - <#resolved_struct as ::mingling::Renderer>::render(value) - } - } - }).collect(); - quote! { - fn render(any: ::mingling::AnyOutput<Self::Enum>) -> ::mingling::RenderResult { - match any.member_id() { - #(#render_arms)* - _ => ::mingling::RenderResult::default(), - } - } - } - }; - - // Build do_chain function (async and sync versions) - let chain_arms_async: Vec<_> = chain_tokens.iter().map(|entry| { - let (struct_ident, variant_ident) = parse_entry_pair(entry); - let downcast_ty = resolve_type(&variant_ident.to_string(), &pathf_map); - let resolved_struct = resolve_type(&struct_ident.to_string(), &pathf_map); - quote! { - Self::#variant_ident => { - // SAFETY: The `type_id` check ensures that `any` contains a value of type `#variant_ident`, - // so downcasting to `#variant_ident` is safe. - let value = unsafe { any.downcast::<#downcast_ty>().unwrap_unchecked() }; - let fut = async { <#resolved_struct as ::mingling::Chain<Self::Enum>>::proc(value).await }; - ::std::boxed::Box::pin(fut) - } - } - }).collect(); - - let chain_arms_sync: Vec<_> = chain_tokens - .iter() - .map(|entry| { - let (struct_ident, variant_ident) = parse_entry_pair(entry); - let downcast_ty = resolve_type(&variant_ident.to_string(), &pathf_map); - let resolved_struct = resolve_type(&struct_ident.to_string(), &pathf_map); - quote! { - Self::#variant_ident => { - // SAFETY: The `type_id` check ensures that `any` contains a value of type `#variant_ident`, - // so downcasting to `#variant_ident` is safe. - let value = unsafe { any.downcast::<#downcast_ty>().unwrap_unchecked() }; - <#resolved_struct as ::mingling::Chain<Self::Enum>>::proc(value) - } - } - }) - .collect(); - - let do_chain_fn = if chain_tokens.is_empty() { - quote! { - fn do_chain(_any: ::mingling::AnyOutput<Self::Enum>) -> ::mingling::ChainProcess<Self::Enum> { - ::core::panic!("No chain found for type id") - } - } - } else if ASYNC_ENABLED { - quote! { - fn do_chain( - any: ::mingling::AnyOutput<Self::Enum>, - ) -> ::std::pin::Pin<::std::boxed::Box<dyn ::std::future::Future<Output = ::mingling::ChainProcess<Self::Enum>> + ::std::marker::Send>> { - match any.member_id() { - #(#chain_arms_async)* - _ => ::core::panic!("No chain found for type id: {:?}", any.type_id()), - } - } - } - } else { - quote! { - fn do_chain( - any: ::mingling::AnyOutput<Self::Enum>, - ) -> ::mingling::ChainProcess<Self::Enum> { - match any.member_id() { - #(#chain_arms_sync)* - _ => ::core::panic!("No chain found for type id: {:?}", any.type_id()), - } - } - } - }; - - let help_tokens: Vec<proc_macro2::TokenStream> = get_global_set(&HELP_REQUESTS) - .lock() - .unwrap() - .clone() - .iter() - .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap()) - .collect(); - - let num_variants = packed_types.len(); - let repr_type = if u8::try_from(num_variants).is_ok() { - quote! { u8 } - } else if u16::try_from(num_variants).is_ok() { - quote! { u16 } - } else if u32::try_from(num_variants).is_ok() { - quote! { u32 } - } else { - quote! { u128 } - }; - - let expanded = quote! { - #pathf_hint - - #[derive(Debug, PartialEq, Eq, Clone, Copy)] - #[repr(#repr_type)] - #[allow(nonstandard_style)] - pub enum #name { - #(#packed_types),* - } - - impl ::std::fmt::Display for #name { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - match self { - #(#name::#packed_types => write!(f, stringify!(#packed_types)),)* - } - } - } - - impl ::mingling::ProgramCollect for #name { - type Enum = #name; - type ErrorDispatcherNotFound = ErrorDispatcherNotFound; - type ErrorRendererNotFound = ErrorRendererNotFound; - type ResultEmpty = ResultEmpty; - fn build_renderer_not_found(member_id: Self::Enum) -> ::mingling::AnyOutput<Self::Enum> { - ::mingling::AnyOutput::new(ErrorRendererNotFound::new(member_id.to_string())) - } - fn build_dispatcher_not_found(args: Vec<String>) -> ::mingling::AnyOutput<Self::Enum> { - ::mingling::AnyOutput::new(ErrorDispatcherNotFound::new(args)) - } - fn build_empty_result() -> ::mingling::AnyOutput<Self::Enum> { - ::mingling::AnyOutput::new(ResultEmpty) - } - #render_fn - #do_chain_fn - fn render_help(any: ::mingling::AnyOutput<Self::Enum>) -> ::mingling::RenderResult { - #[allow(unused_imports)] - #(#pathf_uses)* - match any.member_id() { - #(#help_tokens)* - _ => ::mingling::RenderResult::default(), - } - } - fn has_renderer(any: &::mingling::AnyOutput<Self::Enum>) -> bool { - match any.member_id() { - #(#renderer_exist_tokens)* - _ => false - } - } - fn has_chain(any: &::mingling::AnyOutput<Self::Enum>) -> bool { - match any.member_id() { - #(#chain_exist_tokens)* - _ => false - } - } - #dispatch_tree_nodes - #structural_render - #comp - } - - impl #name { - /// Creates a new `Program<#name>` instance with default configuration. - pub fn new() -> ::mingling::Program<#name> { - ::mingling::Program::new() - } - - /// Returns a static reference to the global `Program<#name>` singleton. - pub fn this() -> &'static ::mingling::Program<#name> { - &::mingling::this::<#name>() - } - } - }; - - // Clear all global registries to prevent stale state in Rust Analyzer - get_global_set(&PACKED_TYPES).lock().unwrap().clear(); - get_global_set(&CHAINS).lock().unwrap().clear(); - get_global_set(&CHAINS_EXIST).lock().unwrap().clear(); - get_global_set(&RENDERERS).lock().unwrap().clear(); - get_global_set(&RENDERERS_EXIST).lock().unwrap().clear(); - get_global_set(&HELP_REQUESTS).lock().unwrap().clear(); - #[cfg(feature = "comp")] - get_global_set(&COMPLETIONS).lock().unwrap().clear(); - #[cfg(feature = "dispatch_tree")] - get_global_set(&COMPILE_TIME_DISPATCHERS) - .lock() - .unwrap() - .clear(); - #[cfg(feature = "structural_renderer")] - get_global_set(&STRUCTURAL_RENDERERS) - .lock() - .unwrap() - .clear(); - - TokenStream::from(expanded) -} diff --git a/mingling_macros/src/func/group_structural.rs b/mingling_macros/src/func/group_structural.rs new file mode 100644 index 0000000..2bd2f83 --- /dev/null +++ b/mingling_macros/src/func/group_structural.rs @@ -0,0 +1,124 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::{Ident, TypePath}; + +use crate::get_global_set; + +/// `group_structural!` — like `group!` but also marks the type as supporting +/// structured output via `StructuralData`. +pub(crate) fn group_structural(input: TokenStream) -> TokenStream { + // Parse the same input as group! + let input_parsed = syn::parse_macro_input!(input as GroupStructuralInput); + + let is_aliased = matches!(&input_parsed, GroupStructuralInput::Aliased { .. }); + + let (type_path, type_name, alias_stmt) = match &input_parsed { + GroupStructuralInput::Plain(type_path) => { + let name = type_path + .path + .segments + .last() + .expect("TypePath must have at least one segment") + .ident + .clone(); + (type_path.clone(), name, quote! {}) + } + GroupStructuralInput::Aliased { alias, type_path } => { + let alias_stmt = quote! { + pub(crate) type #alias = #type_path; + }; + (type_path.clone(), alias.clone(), alias_stmt) + } + }; + + let type_name_str = type_name.to_string(); + + // Register in STRUCTURED_TYPES + get_global_set(&crate::STRUCTURED_TYPES) + .lock() + .unwrap() + .insert(type_name_str); + + let program_path = crate::default_program_path(); + + // Generate unique module name + let segments: Vec<String> = type_path + .path + .segments + .iter() + .map(|seg| seg.ident.to_string().to_lowercase()) + .collect(); + let module_name = Ident::new( + &format!("internal_group_{}", segments.join("_")), + proc_macro2::Span::call_site(), + ); + + // Generate the appropriate `use` statement for the original type + let type_use = if type_path.path.segments.len() > 1 { + quote! { #[allow(unused_imports)] use #type_path; } + } else { + let ident = type_path + .path + .segments + .last() + .expect("TypePath must have at least one segment") + .ident + .clone(); + quote! { #[allow(unused_imports)] use super::#ident; } + }; + + let alias_use = if is_aliased { + quote! { use super::#type_name; } + } else { + quote! {} + }; + + let expanded = quote! { + #alias_stmt + #[allow(non_camel_case_types)] + mod #module_name { + use #program_path as __MinglingProgram; + #type_use + #alias_use + + /// 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<crate::ThisProgram> for #type_name {} + impl ::mingling::__private::StructuralData<crate::ThisProgram> for #type_name {} + + ::mingling::macros::register_type!(#type_name); + } + }; + + expanded.into() +} + +/// Input for `group_structural!` — same format as `group!`. +enum GroupStructuralInput { + Plain(TypePath), + Aliased { alias: Ident, type_path: TypePath }, +} + +impl syn::parse::Parse for GroupStructuralInput { + fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> { + let fork = input.fork(); + let _first: Ident = fork.parse()?; + if fork.peek(syn::Token![=]) { + let alias: Ident = input.parse()?; + let _eq: syn::Token![=] = input.parse()?; + let type_path: TypePath = input.parse()?; + Ok(GroupStructuralInput::Aliased { alias, type_path }) + } else { + let type_path: TypePath = input.parse()?; + Ok(GroupStructuralInput::Plain(type_path)) + } + } +} diff --git a/mingling_macros/src/func/pack_err.rs b/mingling_macros/src/func/pack_err.rs index 2a318bc..8b224b0 100644 --- a/mingling_macros/src/func/pack_err.rs +++ b/mingling_macros/src/func/pack_err.rs @@ -105,105 +105,3 @@ pub(crate) fn pack_err(input: TokenStream) -> TokenStream { } } } - -/// `pack_err_structural!` — like `pack_err!` but also marks the type as -/// supporting structured output via `StructuralData`. -/// -/// # Syntax -/// -/// ```rust,ignore -/// pack_err_structural!(ErrorNotFound); -/// pack_err_structural!(ErrorNotDir = PathBuf); -/// ``` -/// -/// This is equivalent to: -/// ```rust,ignore -/// pack_err!(ErrorNotFound); -/// impl ::mingling::__private::StructuralDataSealed for ErrorNotFound {} -/// impl ::mingling::__private::StructuralData for ErrorNotFound {} -/// ``` -#[cfg(feature = "structural_renderer")] -pub(crate) fn pack_err_structural(input: TokenStream) -> TokenStream { - let parsed = parse_macro_input!(input as PackErrInput); - - let type_name = match &parsed { - PackErrInput::Simple { type_name } => type_name.clone(), - PackErrInput::Typed { type_name, .. } => type_name.clone(), - }; - - // Register in STRUCTURED_TYPES - let type_name_str = type_name.to_string(); - crate::get_global_set(&crate::STRUCTURED_TYPES) - .lock() - .unwrap() - .insert(type_name_str); - - let structural_data = quote! { - 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) - match parsed { - PackErrInput::Simple { type_name } => { - let name_str = type_name.to_string(); - let snake_name = snake_case!(&name_str); - - let expanded = quote! { - #[derive(::mingling::Grouped, ::serde::Serialize)] - pub struct #type_name { - /// The snake_case name of this error, automatically set at compile time. - pub name: String, - } - - impl ::std::default::Default for #type_name { - fn default() -> Self { - Self { - name: #snake_name.into(), - } - } - } - - ::mingling::macros::register_type!(#type_name); - - #structural_data - }; - - expanded.into() - } - PackErrInput::Typed { - type_name, - inner_type, - } => { - let name_str = type_name.to_string(); - let snake_name = snake_case!(&name_str); - - let expanded = quote! { - #[derive(::mingling::Grouped, ::serde::Serialize)] - pub struct #type_name { - /// The snake_case name of this error, automatically set at compile time. - pub name: String, - /// Additional context info for this error. - pub info: #inner_type, - } - - impl #type_name { - /// Creates a new error with the given info. - /// The `name` field is automatically set to the snake_case of the struct name. - pub fn new(info: #inner_type) -> Self { - Self { - name: #snake_name.into(), - info, - } - } - } - - ::mingling::macros::register_type!(#type_name); - - #structural_data - }; - - expanded.into() - } - } -} diff --git a/mingling_macros/src/func/pack_err_structural.rs b/mingling_macros/src/func/pack_err_structural.rs new file mode 100644 index 0000000..7d3a6f8 --- /dev/null +++ b/mingling_macros/src/func/pack_err_structural.rs @@ -0,0 +1,119 @@ +use just_fmt::snake_case; +use proc_macro::TokenStream; +use quote::quote; +use syn::{Ident, Token, Type, parse_macro_input}; + +/// `pack_err_structural!` — like `pack_err!` but also marks the type as +/// supporting structured output via `StructuralData`. +pub(crate) fn pack_err_structural(input: TokenStream) -> TokenStream { + let parsed = parse_macro_input!(input as PackErrInput); + + let type_name = match &parsed { + PackErrInput::Simple { type_name } => type_name.clone(), + PackErrInput::Typed { type_name, .. } => type_name.clone(), + }; + + // Register in STRUCTURED_TYPES + let type_name_str = type_name.to_string(); + crate::get_global_set(&crate::STRUCTURED_TYPES) + .lock() + .unwrap() + .insert(type_name_str); + + let structural_data = quote! { + 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) + match parsed { + PackErrInput::Simple { type_name } => { + let name_str = type_name.to_string(); + let snake_name = snake_case!(&name_str); + + let expanded = quote! { + #[derive(::mingling::Grouped, ::serde::Serialize)] + pub struct #type_name { + /// The snake_case name of this error, automatically set at compile time. + pub name: String, + } + + impl ::std::default::Default for #type_name { + fn default() -> Self { + Self { + name: #snake_name.into(), + } + } + } + + ::mingling::macros::register_type!(#type_name); + + #structural_data + }; + + expanded.into() + } + PackErrInput::Typed { + type_name, + inner_type, + } => { + let name_str = type_name.to_string(); + let snake_name = snake_case!(&name_str); + + let expanded = quote! { + #[derive(::mingling::Grouped, ::serde::Serialize)] + pub struct #type_name { + /// The snake_case name of this error, automatically set at compile time. + pub name: String, + /// Additional context info for this error. + pub info: #inner_type, + } + + impl #type_name { + /// Creates a new error with the given info. + /// The `name` field is automatically set to the snake_case of the struct name. + pub fn new(info: #inner_type) -> Self { + Self { + name: #snake_name.into(), + info, + } + } + } + + ::mingling::macros::register_type!(#type_name); + + #structural_data + }; + + expanded.into() + } + } +} + +// Re-use pack_err's input parser +enum PackErrInput { + Simple { + type_name: Ident, + }, + Typed { + type_name: Ident, + inner_type: Box<Type>, + }, +} + +impl syn::parse::Parse for PackErrInput { + fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> { + let type_name: Ident = input.parse()?; + + if input.peek(Token![=]) { + input.parse::<Token![=]>()?; + let inner_type: Type = input.parse()?; + Ok(PackErrInput::Typed { + type_name, + inner_type: Box::new(inner_type), + }) + } else { + Ok(PackErrInput::Simple { type_name }) + } + } +} diff --git a/mingling_macros/src/func/pack_structural.rs b/mingling_macros/src/func/pack_structural.rs new file mode 100644 index 0000000..9399959 --- /dev/null +++ b/mingling_macros/src/func/pack_structural.rs @@ -0,0 +1,168 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::Ident; + +use crate::get_global_set; + +/// `pack_structural!` — like `pack!` but also marks the type as supporting +/// structured output via `StructuralData`. +pub(crate) fn pack_structural(input: TokenStream) -> TokenStream { + // Parse same input format as `pack!` + let input_parsed = syn::parse_macro_input!(input as PackStructuralInput); + let type_name = input_parsed.type_name; + let inner_type = input_parsed.inner_type; + let attrs = input_parsed.attrs; + let program_path = crate::default_program_path(); + + // Register in STRUCTURED_TYPES + let type_name_str = type_name.to_string(); + get_global_set(&crate::STRUCTURED_TYPES) + .lock() + .unwrap() + .insert(type_name_str); + + // Struct definition (with Serialize derive, same as pack! under structural_renderer) + #[cfg(not(feature = "structural_renderer"))] + let struct_def = quote! { + #(#attrs)* + pub struct #type_name { + pub inner: #inner_type, + } + }; + + #[cfg(feature = "structural_renderer")] + let struct_def = quote! { + #(#attrs)* + #[derive(serde::Serialize)] + pub struct #type_name { + pub inner: #inner_type, + } + }; + + // Helper impls (same as pack!) + let new_impl = quote! { + impl #type_name { + pub fn new(inner: #inner_type) -> Self { + Self { inner } + } + } + }; + + let from_into_impl = quote! { + impl From<#inner_type> for #type_name { + fn from(inner: #inner_type) -> Self { + Self::new(inner) + } + } + impl From<#type_name> for #inner_type { + fn from(wrapper: #type_name) -> #inner_type { + wrapper.inner + } + } + }; + + let as_ref_impl = quote! { + impl ::std::convert::AsRef<#inner_type> for #type_name { + fn as_ref(&self) -> &#inner_type { + &self.inner + } + } + impl ::std::convert::AsMut<#inner_type> for #type_name { + fn as_mut(&mut self) -> &mut #inner_type { + &mut self.inner + } + } + }; + + let deref_impl = quote! { + impl ::std::ops::Deref for #type_name { + type Target = #inner_type; + fn deref(&self) -> &Self::Target { + &self.inner + } + } + impl ::std::ops::DerefMut for #type_name { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } + } + }; + + let default_impl = quote! { + impl ::std::default::Default for #type_name + where + #inner_type: ::std::default::Default, + { + fn default() -> Self { + Self::new(::std::default::Default::default()) + } + } + }; + + let register_impl = quote! { + ::mingling::macros::register_type!(#type_name); + }; + + // StructuralData impl + sealed + registration + let structural_impl = quote! { + impl ::mingling::__private::StructuralDataSealed<crate::ThisProgram> for #type_name {} + impl ::mingling::__private::StructuralData<crate::ThisProgram> for #type_name {} + }; + + let expanded = quote! { + #struct_def + + #new_impl + #from_into_impl + #as_ref_impl + #deref_impl + #default_impl + #register_impl + #structural_impl + + impl Into<::mingling::AnyOutput<#program_path>> for #type_name { + fn into(self) -> ::mingling::AnyOutput<#program_path> { + ::mingling::AnyOutput::new(self) + } + } + + impl Into<::mingling::ChainProcess<#program_path>> for #type_name { + fn into(self) -> ::mingling::ChainProcess<#program_path> { + ::mingling::AnyOutput::new(self).route_chain() + } + } + + /// 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 + } + } + }; + + expanded.into() +} + +/// Input for `pack_structural!` — same format as `pack!`. +struct PackStructuralInput { + attrs: Vec<syn::Attribute>, + type_name: Ident, + inner_type: syn::Type, +} + +impl syn::parse::Parse for PackStructuralInput { + fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> { + let attrs = input.call(syn::Attribute::parse_outer)?; + let type_name: Ident = input.parse()?; + input.parse::<syn::Token![=]>()?; + let inner_type: syn::Type = input.parse()?; + Ok(PackStructuralInput { + attrs, + type_name, + inner_type, + }) + } +} diff --git a/mingling_macros/src/func/program_comp_gen.rs b/mingling_macros/src/func/program_comp_gen.rs new file mode 100644 index 0000000..b6b2546 --- /dev/null +++ b/mingling_macros/src/func/program_comp_gen.rs @@ -0,0 +1,80 @@ +use proc_macro::TokenStream; +use quote::quote; + +#[cfg(feature = "comp")] +pub(crate) fn program_comp_gen_impl(_input: TokenStream) -> TokenStream { + #[cfg(feature = "async")] + let fn_exec_comp = quote! { + #[doc(hidden)] + #[::mingling::macros::chain] + pub async fn __exec_completion(prev: CompletionContext) -> Next { + use ::mingling::Grouped; + + let read_ctx = ::mingling::ShellContext::try_from(prev.inner); + match read_ctx { + Ok(ctx) => { + let suggest = ::mingling::CompletionHelper::exec_completion::<crate::ThisProgram>(&ctx); + ::mingling::Routable::<crate::ThisProgram>::to_render(crate::CompletionSuggest::new((ctx, suggest))) + } + Err(_) => std::process::exit(1), + } + } + }; + + #[cfg(not(feature = "async"))] + let fn_exec_comp = quote! { + #[doc(hidden)] + #[::mingling::macros::chain] + pub fn __exec_completion(prev: CompletionContext) -> Next { + use ::mingling::Grouped; + + let read_ctx = ::mingling::ShellContext::try_from(prev.inner); + match read_ctx { + Ok(ctx) => { + let suggest = ::mingling::CompletionHelper::exec_completion::<crate::ThisProgram>(&ctx); + ::mingling::Routable::<crate::ThisProgram>::to_render(crate::CompletionSuggest::new((ctx, suggest))) + } + Err(_) => std::process::exit(1), + } + } + }; + + #[cfg(feature = "dispatch_tree")] + let internal_dispatcher_comp = quote! { + use __internal_completion_mod::__internal_dispatcher_comp; + }; + + #[cfg(not(feature = "dispatch_tree"))] + let internal_dispatcher_comp = quote! {}; + + let comp_dispatcher = quote! { + #[doc(hidden)] + mod __internal_completion_mod { + use ::mingling::Grouped; + ::mingling::macros::dispatcher!("__comp", CMDCompletion => CompletionContext); + ::mingling::macros::pack!( + CompletionSuggest = (::mingling::ShellContext, ::mingling::Suggest) + ); + } + #internal_dispatcher_comp + use __internal_completion_mod::CompletionContext; + use __internal_completion_mod::CompletionSuggest; + pub use __internal_completion_mod::CMDCompletion; + + #fn_exec_comp + + ::mingling::macros::register_type!(CompletionContext); + + #[allow(unused)] + #[doc(hidden)] + #[::mingling::macros::renderer] + pub fn __render_completion(prev: CompletionSuggest) -> ::mingling::RenderResult { + let result = ::mingling::RenderResult::default(); + let (ctx, suggest) = prev.inner; + ::mingling::CompletionHelper::render_suggest::<crate::ThisProgram>(ctx, suggest); + result + } + }; + + TokenStream::from(comp_dispatcher) +} diff --git a/mingling_macros/src/func/program_fallback_gen.rs b/mingling_macros/src/func/program_fallback_gen.rs new file mode 100644 index 0000000..3d095e5 --- /dev/null +++ b/mingling_macros/src/func/program_fallback_gen.rs @@ -0,0 +1,23 @@ +use proc_macro::TokenStream; +use quote::quote; + +pub(crate) fn program_fallback_gen_impl(_input: TokenStream) -> TokenStream { + #[cfg(feature = "structural_renderer")] + let pack_empty = quote! { + #[derive(::serde::Serialize, ::mingling::StructuralData, ::mingling::Grouped, Default)] + pub struct ResultEmpty; + }; + + #[cfg(not(feature = "structural_renderer"))] + let pack_empty = quote! { + #[derive(::mingling::Grouped, Default)] + pub struct ResultEmpty; + }; + + let expanded = quote! { + ::mingling::macros::pack!(ErrorRendererNotFound = String); + ::mingling::macros::pack!(ErrorDispatcherNotFound = Vec<String>); + #pack_empty + }; + TokenStream::from(expanded) +} diff --git a/mingling_macros/src/func/program_final_gen.rs b/mingling_macros/src/func/program_final_gen.rs new file mode 100644 index 0000000..d3af3b3 --- /dev/null +++ b/mingling_macros/src/func/program_final_gen.rs @@ -0,0 +1,465 @@ +use std::collections::HashMap; + +use proc_macro::TokenStream; +use quote::quote; + +use crate::CHAINS; +use crate::CHAINS_EXIST; +#[cfg(feature = "dispatch_tree")] +use crate::COMPILE_TIME_DISPATCHERS; +#[cfg(feature = "comp")] +use crate::COMPLETIONS; +use crate::HELP_REQUESTS; +use crate::PACKED_TYPES; +use crate::RENDERERS; +use crate::RENDERERS_EXIST; +#[cfg(feature = "structural_renderer")] +use crate::STRUCTURAL_RENDERERS; +use crate::get_global_set; +#[cfg(feature = "dispatch_tree")] +use crate::systems::dispatch_tree_gen; + +#[cfg(feature = "async")] +const ASYNC_ENABLED: bool = true; +#[cfg(not(feature = "async"))] +const ASYNC_ENABLED: bool = false; + +/// Parses an entry of the format `StructName => EnumVariant,` into a pair of idents. +fn parse_entry_pair(entry: &proc_macro2::TokenStream) -> (proc_macro2::Ident, proc_macro2::Ident) { + let s = entry.to_string(); + let arrow_idx = s + .find("=>") + .unwrap_or_else(|| panic!("Entry missing '=>': {s}")); + let struct_str = s[..arrow_idx].trim(); + let variant_str = s[arrow_idx + 2..].trim().trim_end_matches(',').trim(); + let struct_ident = proc_macro2::Ident::new(struct_str, proc_macro2::Span::call_site()); + let variant_ident = proc_macro2::Ident::new(variant_str, proc_macro2::Span::call_site()); + (struct_ident, variant_ident) +} + +/// Loads the pathf type mapping from `$OUT_DIR/{crate}/type_using.rs`. +fn load_pathf_map() -> Option<std::collections::HashMap<String, String>> { + if !cfg!(feature = "pathf") { + return None; + } + let out_dir = std::env::var("OUT_DIR").ok()?; + let crate_name = std::env::var("CARGO_PKG_NAME").ok()?; + let path = std::path::Path::new(&out_dir) + .join(&crate_name) + .join("type_using.rs"); + let content = std::fs::read_to_string(&path).ok()?; + Some( + content + .lines() + .filter_map(|line| { + let line = line.trim(); + if let Some(rest) = line.strip_prefix("use ") { + let path = rest.strip_suffix(';').unwrap_or(rest); + if let Some((_mod, type_name)) = path.rsplit_once("::") { + return Some((type_name.to_string(), path.to_string())); + } + } + None + }) + .collect(), + ) +} + +/// Resolves a type name to its full path token stream using the pathf mapping. +pub(crate) fn resolve_type( + name: &str, + map: &std::collections::HashMap<String, String>, +) -> proc_macro2::TokenStream { + if let Some(full_path) = map.get(name) { + syn::parse_str::<proc_macro2::TokenStream>(full_path).unwrap_or_else(|_| { + let ident = proc_macro2::Ident::new(name, proc_macro2::Span::call_site()); + quote! { #ident } + }) + } else { + let ident = proc_macro2::Ident::new(name, proc_macro2::Span::call_site()); + quote! { #ident } + } +} + +#[allow(clippy::too_many_lines)] +pub(crate) fn program_final_gen_impl(_input: TokenStream) -> TokenStream { + let name = syn::Ident::new("ThisProgram", proc_macro2::Span::call_site()); + + let packed_types = get_global_set(&PACKED_TYPES).lock().unwrap().clone(); + + let renderers = get_global_set(&RENDERERS).lock().unwrap().clone(); + let chains = get_global_set(&CHAINS).lock().unwrap().clone(); + let renderer_exist = get_global_set(&RENDERERS_EXIST).lock().unwrap().clone(); + let chain_exist = get_global_set(&CHAINS_EXIST).lock().unwrap().clone(); + + #[cfg(feature = "structural_renderer")] + let structural_renderers = get_global_set(&STRUCTURAL_RENDERERS) + .lock() + .unwrap() + .clone(); + + #[cfg(feature = "comp")] + let completions = get_global_set(&COMPLETIONS).lock().unwrap().clone(); + + let packed_types: Vec<proc_macro2::TokenStream> = packed_types + .iter() + .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap()) + .collect(); + + let renderer_tokens: Vec<proc_macro2::TokenStream> = renderers + .iter() + .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap()) + .collect(); + + let chain_tokens: Vec<proc_macro2::TokenStream> = chains + .iter() + .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap()) + .collect(); + + let renderer_exist_tokens: Vec<proc_macro2::TokenStream> = renderer_exist + .iter() + .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap()) + .collect(); + + let chain_exist_tokens: Vec<proc_macro2::TokenStream> = chain_exist + .iter() + .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap()) + .collect(); + + let pathf_map: Option<HashMap<String, String>> = load_pathf_map(); + + #[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 { + 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") { + pathf_map + .values() + .map(|path| format!("use {};", path).parse().unwrap_or_default()) + .collect() + } else { + Vec::new() + }; + + #[cfg(feature = "structural_renderer")] + let structural_renderer_tokens: Vec<proc_macro2::TokenStream> = structural_renderers + .iter() + .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap()) + .collect(); + + #[cfg(feature = "structural_renderer")] + let structural_render = quote! { + fn structural_render( + any: ::mingling::AnyOutput<Self::Enum>, + setting: &::mingling::StructuralRendererSetting, + ) -> Result<::mingling::RenderResult, ::mingling::error::StructuralRendererSerializeError> { + #[allow(unused_imports)] + #(#pathf_uses)* + match any.member_id() { + #(#structural_renderer_tokens)* + _ => { + // Non-structural types: render ResultEmpty (which implements + // StructuralData + Serialize) instead of producing nothing. + let mut r = ::mingling::RenderResult::default(); + ::mingling::StructuralRenderer::render(&ResultEmpty, setting, &mut r)?; + Ok(r) + } + } + } + }; + + #[cfg(not(feature = "structural_renderer"))] + let structural_render = quote! {}; + + #[cfg(feature = "dispatch_tree")] + let compile_time_dispatchers: Vec<String> = get_global_set(&COMPILE_TIME_DISPATCHERS) + .lock() + .unwrap() + .clone() + .iter() + .cloned() + .collect(); + + #[cfg(feature = "dispatch_tree")] + let dispatch_tree_nodes = { + let entries: Vec<(String, String, String)> = compile_time_dispatchers + .iter() + .filter_map(|entry| { + let parts: Vec<&str> = entry.split(':').collect(); + if parts.len() == 3 { + Some(( + parts[0].to_string(), + parts[1].to_string(), + parts[2].to_string(), + )) + } else { + None + } + }) + .collect(); + + let get_nodes_fn = dispatch_tree_gen::gen_get_nodes(&entries, &pathf_map); + let dispatch_trie_fn = dispatch_tree_gen::gen_dispatch_args_trie(&entries, &pathf_map); + + quote! { + #get_nodes_fn + #dispatch_trie_fn + } + }; + + #[cfg(not(feature = "dispatch_tree"))] + let dispatch_tree_nodes = quote! {}; + + #[cfg(feature = "comp")] + let completion_tokens: Vec<proc_macro2::TokenStream> = completions + .iter() + .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap()) + .collect(); + + #[cfg(feature = "comp")] + let comp = quote! { + fn do_comp(any: &::mingling::AnyOutput<Self::Enum>, ctx: &::mingling::ShellContext) -> ::mingling::Suggest { + #[allow(unused_imports)] + #(#pathf_uses)* + match any.member_id() { + #(#completion_tokens)* + _ => ::mingling::Suggest::FileCompletion, + } + } + }; + + #[cfg(not(feature = "comp"))] + let comp = quote! {}; + + // Build render function arms from stored entries + let render_fn = + if renderer_tokens.is_empty() { + quote! { + fn render(_any: ::mingling::AnyOutput<Self::Enum>) -> ::mingling::RenderResult { + ::mingling::RenderResult::default() + } + } + } else { + let render_arms: Vec<_> = renderer_tokens.iter().map(|entry| { + let (struct_ident, variant_ident) = parse_entry_pair(entry); + let downcast_ty = resolve_type(&variant_ident.to_string(), &pathf_map); + let resolved_struct = resolve_type(&struct_ident.to_string(), &pathf_map); + quote! { + Self::#variant_ident => { + // SAFETY: The `type_id` check ensures that `any` contains a value of type `#variant_ident`, + // so downcasting to `#variant_ident` is safe. + let value = unsafe { any.downcast::<#downcast_ty>().unwrap_unchecked() }; + <#resolved_struct as ::mingling::Renderer>::render(value) + } + } + }).collect(); + quote! { + fn render(any: ::mingling::AnyOutput<Self::Enum>) -> ::mingling::RenderResult { + match any.member_id() { + #(#render_arms)* + _ => ::mingling::RenderResult::default(), + } + } + } + }; + + // Build do_chain function (async and sync versions) + let chain_arms_async: Vec<_> = chain_tokens.iter().map(|entry| { + let (struct_ident, variant_ident) = parse_entry_pair(entry); + let downcast_ty = resolve_type(&variant_ident.to_string(), &pathf_map); + let resolved_struct = resolve_type(&struct_ident.to_string(), &pathf_map); + quote! { + Self::#variant_ident => { + // SAFETY: The `type_id` check ensures that `any` contains a value of type `#variant_ident`, + // so downcasting to `#variant_ident` is safe. + let value = unsafe { any.downcast::<#downcast_ty>().unwrap_unchecked() }; + let fut = async { <#resolved_struct as ::mingling::Chain<Self::Enum>>::proc(value).await }; + ::std::boxed::Box::pin(fut) + } + } + }).collect(); + + let chain_arms_sync: Vec<_> = chain_tokens + .iter() + .map(|entry| { + let (struct_ident, variant_ident) = parse_entry_pair(entry); + let downcast_ty = resolve_type(&variant_ident.to_string(), &pathf_map); + let resolved_struct = resolve_type(&struct_ident.to_string(), &pathf_map); + quote! { + Self::#variant_ident => { + // SAFETY: The `type_id` check ensures that `any` contains a value of type `#variant_ident`, + // so downcasting to `#variant_ident` is safe. + let value = unsafe { any.downcast::<#downcast_ty>().unwrap_unchecked() }; + <#resolved_struct as ::mingling::Chain<Self::Enum>>::proc(value) + } + } + }) + .collect(); + + let do_chain_fn = if chain_tokens.is_empty() { + quote! { + fn do_chain(_any: ::mingling::AnyOutput<Self::Enum>) -> ::mingling::ChainProcess<Self::Enum> { + ::core::panic!("No chain found for type id") + } + } + } else if ASYNC_ENABLED { + quote! { + fn do_chain( + any: ::mingling::AnyOutput<Self::Enum>, + ) -> ::std::pin::Pin<::std::boxed::Box<dyn ::std::future::Future<Output = ::mingling::ChainProcess<Self::Enum>> + ::std::marker::Send>> { + match any.member_id() { + #(#chain_arms_async)* + _ => ::core::panic!("No chain found for type id: {:?}", any.type_id()), + } + } + } + } else { + quote! { + fn do_chain( + any: ::mingling::AnyOutput<Self::Enum>, + ) -> ::mingling::ChainProcess<Self::Enum> { + match any.member_id() { + #(#chain_arms_sync)* + _ => ::core::panic!("No chain found for type id: {:?}", any.type_id()), + } + } + } + }; + + let help_tokens: Vec<proc_macro2::TokenStream> = get_global_set(&HELP_REQUESTS) + .lock() + .unwrap() + .clone() + .iter() + .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap()) + .collect(); + + let num_variants = packed_types.len(); + let repr_type = if u8::try_from(num_variants).is_ok() { + quote! { u8 } + } else if u16::try_from(num_variants).is_ok() { + quote! { u16 } + } else if u32::try_from(num_variants).is_ok() { + quote! { u32 } + } else { + quote! { u128 } + }; + + let expanded = quote! { + #pathf_hint + + #[derive(Debug, PartialEq, Eq, Clone, Copy)] + #[repr(#repr_type)] + #[allow(nonstandard_style)] + pub enum #name { + #(#packed_types),* + } + + impl ::std::fmt::Display for #name { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + match self { + #(#name::#packed_types => write!(f, stringify!(#packed_types)),)* + } + } + } + + impl ::mingling::ProgramCollect for #name { + type Enum = #name; + type ErrorDispatcherNotFound = ErrorDispatcherNotFound; + type ErrorRendererNotFound = ErrorRendererNotFound; + type ResultEmpty = ResultEmpty; + fn build_renderer_not_found(member_id: Self::Enum) -> ::mingling::AnyOutput<Self::Enum> { + ::mingling::AnyOutput::new(ErrorRendererNotFound::new(member_id.to_string())) + } + fn build_dispatcher_not_found(args: Vec<String>) -> ::mingling::AnyOutput<Self::Enum> { + ::mingling::AnyOutput::new(ErrorDispatcherNotFound::new(args)) + } + fn build_empty_result() -> ::mingling::AnyOutput<Self::Enum> { + ::mingling::AnyOutput::new(ResultEmpty) + } + #render_fn + #do_chain_fn + fn render_help(any: ::mingling::AnyOutput<Self::Enum>) -> ::mingling::RenderResult { + #[allow(unused_imports)] + #(#pathf_uses)* + match any.member_id() { + #(#help_tokens)* + _ => ::mingling::RenderResult::default(), + } + } + fn has_renderer(any: &::mingling::AnyOutput<Self::Enum>) -> bool { + match any.member_id() { + #(#renderer_exist_tokens)* + _ => false + } + } + fn has_chain(any: &::mingling::AnyOutput<Self::Enum>) -> bool { + match any.member_id() { + #(#chain_exist_tokens)* + _ => false + } + } + #dispatch_tree_nodes + #structural_render + #comp + } + + impl #name { + /// Creates a new `Program<#name>` instance with default configuration. + pub fn new() -> ::mingling::Program<#name> { + ::mingling::Program::new() + } + + /// Returns a static reference to the global `Program<#name>` singleton. + pub fn this() -> &'static ::mingling::Program<#name> { + &::mingling::this::<#name>() + } + } + }; + + // Clear all global registries to prevent stale state in Rust Analyzer + get_global_set(&PACKED_TYPES).lock().unwrap().clear(); + get_global_set(&CHAINS).lock().unwrap().clear(); + get_global_set(&CHAINS_EXIST).lock().unwrap().clear(); + get_global_set(&RENDERERS).lock().unwrap().clear(); + get_global_set(&RENDERERS_EXIST).lock().unwrap().clear(); + get_global_set(&HELP_REQUESTS).lock().unwrap().clear(); + #[cfg(feature = "comp")] + get_global_set(&COMPLETIONS).lock().unwrap().clear(); + #[cfg(feature = "dispatch_tree")] + get_global_set(&COMPILE_TIME_DISPATCHERS) + .lock() + .unwrap() + .clear(); + #[cfg(feature = "structural_renderer")] + get_global_set(&STRUCTURAL_RENDERERS) + .lock() + .unwrap() + .clear(); + + TokenStream::from(expanded) +} diff --git a/mingling_macros/src/func/r_append.rs b/mingling_macros/src/func/r_append.rs new file mode 100644 index 0000000..247da27 --- /dev/null +++ b/mingling_macros/src/func/r_append.rs @@ -0,0 +1,29 @@ +use proc_macro::TokenStream; +use quote::quote; + +use crate::func::r_print::AppendInput; + +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/func/r_eprint.rs b/mingling_macros/src/func/r_eprint.rs new file mode 100644 index 0000000..97023b0 --- /dev/null +++ b/mingling_macros/src/func/r_eprint.rs @@ -0,0 +1,7 @@ +use proc_macro::TokenStream; + +use crate::func::r_print::expand_print; + +pub(crate) fn r_eprint(input: TokenStream) -> TokenStream { + expand_print(input, "eprint") +} diff --git a/mingling_macros/src/func/r_eprintln.rs b/mingling_macros/src/func/r_eprintln.rs new file mode 100644 index 0000000..6bb86b8 --- /dev/null +++ b/mingling_macros/src/func/r_eprintln.rs @@ -0,0 +1,7 @@ +use proc_macro::TokenStream; + +use crate::func::r_print::expand_print; + +pub(crate) fn r_eprintln(input: TokenStream) -> TokenStream { + expand_print(input, "eprintln") +} diff --git a/mingling_macros/src/func/r_print.rs b/mingling_macros/src/func/r_print.rs index 86ea52f..20f15b8 100644 --- a/mingling_macros/src/func/r_print.rs +++ b/mingling_macros/src/func/r_print.rs @@ -9,7 +9,7 @@ use syn::{Ident, Token}; /// Two forms: /// - `(ident, format_args...)` — explicit buffer /// - `(format_args...)` — implicit `__render_result_buffer` -enum PrintInput { +pub(crate) enum PrintInput { Explicit { dst: Ident, args: TokenStream2 }, Implicit { args: TokenStream2 }, } @@ -29,7 +29,7 @@ impl Parse for PrintInput { } } -fn expand_print(input: TokenStream, method: &str) -> TokenStream { +pub(crate) 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(), @@ -53,30 +53,18 @@ fn expand_print(input: TokenStream, method: &str) -> TokenStream { 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, +pub(crate) struct AppendInput { + pub(crate) dst: Option<Ident>, + pub(crate) src: proc_macro2::TokenStream, } impl Parse for AppendInput { @@ -95,33 +83,3 @@ impl Parse for AppendInput { } } } - -/// `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/func/r_println.rs b/mingling_macros/src/func/r_println.rs new file mode 100644 index 0000000..2ba915a --- /dev/null +++ b/mingling_macros/src/func/r_println.rs @@ -0,0 +1,7 @@ +use proc_macro::TokenStream; + +use crate::func::r_print::expand_print; + +pub(crate) fn r_println(input: TokenStream) -> TokenStream { + expand_print(input, "println") +} diff --git a/mingling_macros/src/func/register_chain.rs b/mingling_macros/src/func/register_chain.rs new file mode 100644 index 0000000..4cd139b --- /dev/null +++ b/mingling_macros/src/func/register_chain.rs @@ -0,0 +1,7 @@ +use proc_macro::TokenStream; + +use crate::attr::chain; + +pub(crate) fn register_chain_impl(input: TokenStream) -> TokenStream { + chain::register_chain(input) +} diff --git a/mingling_macros/src/func/register_dispatcher.rs b/mingling_macros/src/func/register_dispatcher.rs new file mode 100644 index 0000000..0b3fbdf --- /dev/null +++ b/mingling_macros/src/func/register_dispatcher.rs @@ -0,0 +1,75 @@ +#[cfg(feature = "dispatch_tree")] +use just_fmt::snake_case; +use proc_macro::TokenStream; +use quote::quote; +#[cfg(feature = "dispatch_tree")] +use syn::parse::{Parse, ParseStream}; +#[cfg(feature = "dispatch_tree")] +use syn::{Ident, LitStr, Result as SynResult, Token}; + +#[cfg(feature = "dispatch_tree")] +use crate::COMPILE_TIME_DISPATCHERS; +#[cfg(feature = "dispatch_tree")] +use crate::get_global_set; + +#[cfg(feature = "dispatch_tree")] +struct RegisterDispatcherInput { + node_name: LitStr, + dispatcher_type: Ident, + entry_name: Ident, +} + +#[cfg(feature = "dispatch_tree")] +impl Parse for RegisterDispatcherInput { + fn parse(input: ParseStream) -> SynResult<Self> { + let node_name: LitStr = input.parse()?; + input.parse::<Token![,]>()?; + let dispatcher_type: Ident = input.parse()?; + input.parse::<Token![,]>()?; + let entry_name: Ident = input.parse()?; + Ok(RegisterDispatcherInput { + node_name, + dispatcher_type, + entry_name, + }) + } +} + +#[cfg(feature = "dispatch_tree")] +pub(crate) fn register_dispatcher(input: TokenStream) -> TokenStream { + let RegisterDispatcherInput { + node_name, + dispatcher_type, + entry_name, + } = syn::parse_macro_input!(input as RegisterDispatcherInput); + + let node_name_str = node_name.value(); + let static_name = format!( + "__internal_dispatcher_{}", + snake_case!(node_name_str.clone()) + ); + let static_ident = Ident::new(&static_name, proc_macro2::Span::call_site()); + + // Register node info in the global collection at compile time + // Format: "node.name:DispatcherType:EntryName" + get_global_set(&COMPILE_TIME_DISPATCHERS) + .lock() + .unwrap() + .insert(format!( + "{}:{}:{}", + node_name_str, dispatcher_type, entry_name + )); + + let expanded = quote! { + #[doc(hidden)] + #[allow(nonstandard_style)] + pub static #static_ident: #dispatcher_type = #dispatcher_type; + }; + + expanded.into() +} + +#[cfg(not(feature = "dispatch_tree"))] +pub(crate) fn register_dispatcher(_input: TokenStream) -> TokenStream { + quote! {}.into() +} diff --git a/mingling_macros/src/func/register_help.rs b/mingling_macros/src/func/register_help.rs new file mode 100644 index 0000000..e715244 --- /dev/null +++ b/mingling_macros/src/func/register_help.rs @@ -0,0 +1,71 @@ +use proc_macro::TokenStream; +use quote::ToTokens; +use syn::TypePath; +use syn::spanned::Spanned; + +use crate::get_global_set; + +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::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::quote! {}.into() +} + +fn build_help_entry(struct_name: &syn::Ident, entry_type: &TypePath) -> proc_macro2::TokenStream { + let enum_variant = entry_type.path.segments.last().unwrap().ident.clone(); + quote::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) + } + } +} diff --git a/mingling_macros/src/func/register_renderer.rs b/mingling_macros/src/func/register_renderer.rs new file mode 100644 index 0000000..c5324d9 --- /dev/null +++ b/mingling_macros/src/func/register_renderer.rs @@ -0,0 +1,7 @@ +use proc_macro::TokenStream; + +use crate::attr::renderer; + +pub(crate) fn register_renderer_impl(input: TokenStream) -> TokenStream { + renderer::register_renderer(input) +} diff --git a/mingling_macros/src/func/register_type.rs b/mingling_macros/src/func/register_type.rs new file mode 100644 index 0000000..593a2ec --- /dev/null +++ b/mingling_macros/src/func/register_type.rs @@ -0,0 +1,17 @@ +use proc_macro::TokenStream; +use syn::parse_macro_input; + +use crate::PACKED_TYPES; +use crate::get_global_set; + +pub(crate) fn register_type_impl(input: TokenStream) -> TokenStream { + let type_ident = parse_macro_input!(input as syn::Ident); + let entry_str = type_ident.to_string(); + + get_global_set(&PACKED_TYPES) + .lock() + .unwrap() + .insert(entry_str); + + TokenStream::new() +} diff --git a/mingling_macros/src/func/render_route.rs b/mingling_macros/src/func/render_route.rs new file mode 100644 index 0000000..8f390e6 --- /dev/null +++ b/mingling_macros/src/func/render_route.rs @@ -0,0 +1,15 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::parse_macro_input; + +/// Routes errors to the rendering pipeline instead of the chain pipeline. +pub(crate) 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) +} diff --git a/mingling_macros/src/func/route.rs b/mingling_macros/src/func/route.rs new file mode 100644 index 0000000..b338fb0 --- /dev/null +++ b/mingling_macros/src/func/route.rs @@ -0,0 +1,16 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::parse_macro_input; + +/// Routes execution depending on a condition — early-returns the error from a `Result`, +/// converting the `Ok` branch to the next chain process value. +pub(crate) fn route(input: TokenStream) -> TokenStream { + let expr = parse_macro_input!(input as syn::Expr); + let expanded = quote! { + match #expr { + Ok(r) => r, + Err(e) => return ::mingling::Routable::to_chain(e), + } + }; + TokenStream::from(expanded) +} diff --git a/mingling_macros/src/func/suggest.rs b/mingling_macros/src/func/suggest.rs index 0f2026f..85f1afe 100644 --- a/mingling_macros/src/func/suggest.rs +++ b/mingling_macros/src/func/suggest.rs @@ -70,24 +70,3 @@ pub(crate) fn suggest(input: TokenStream) -> TokenStream { expanded.into() } - -pub(crate) fn suggest_enum(input: TokenStream) -> TokenStream { - let enum_type = parse_macro_input!(input as syn::Type); - - let expanded = quote! {{ - let mut enum_suggest = ::mingling::Suggest::new(); - for (name, desc) in <#enum_type>::enums() { - if desc.is_empty() { - enum_suggest.insert(::mingling::SuggestItem::new(name.to_string())); - } else { - enum_suggest.insert(::mingling::SuggestItem::new_with_desc( - name.to_string(), - desc.to_string(), - )); - } - } - enum_suggest - }}; - - expanded.into() -} diff --git a/mingling_macros/src/func/suggest_enum.rs b/mingling_macros/src/func/suggest_enum.rs new file mode 100644 index 0000000..c8e9db0 --- /dev/null +++ b/mingling_macros/src/func/suggest_enum.rs @@ -0,0 +1,24 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::parse_macro_input; + +pub(crate) fn suggest_enum(input: TokenStream) -> TokenStream { + let enum_type = parse_macro_input!(input as syn::Type); + + let expanded = quote! {{ + let mut enum_suggest = ::mingling::Suggest::new(); + for (name, desc) in <#enum_type>::enums() { + if desc.is_empty() { + enum_suggest.insert(::mingling::SuggestItem::new(name.to_string())); + } else { + enum_suggest.insert(::mingling::SuggestItem::new_with_desc( + name.to_string(), + desc.to_string(), + )); + } + } + enum_suggest + }}; + + expanded.into() +} diff --git a/mingling_macros/src/lib.rs b/mingling_macros/src/lib.rs index ca3261e..27563a4 100644 --- a/mingling_macros/src/lib.rs +++ b/mingling_macros/src/lib.rs @@ -143,12 +143,6 @@ #![deny(missing_docs)] -#[cfg(feature = "extra_macros")] -use quote::quote; - -#[cfg(feature = "extra_macros")] -use syn::parse_macro_input; - use proc_macro::TokenStream; use std::collections::BTreeSet; use std::sync::Mutex; @@ -183,9 +177,6 @@ use func::pack_err; use func::suggest; use func::{dispatcher, node, pack}; use systems::res_injection; -#[cfg(feature = "structural_renderer")] -pub(crate) use systems::structural_data; - pub(crate) fn default_program_path() -> proc_macro2::TokenStream { quote::quote! { crate::ThisProgram } } @@ -322,7 +313,7 @@ pub fn group(input: TokenStream) -> TokenStream { #[cfg(all(feature = "structural_renderer", feature = "extra_macros"))] #[proc_macro] pub fn group_structural(input: TokenStream) -> TokenStream { - structural_data::group_structural(input) + func::group_structural::group_structural(input) } /// Creates a `Node` from a dot-separated path string. @@ -435,7 +426,7 @@ pub fn pack(input: TokenStream) -> TokenStream { #[cfg(feature = "structural_renderer")] #[proc_macro] pub fn pack_structural(input: TokenStream) -> TokenStream { - structural_data::pack_structural(input) + func::pack_structural::pack_structural(input) } /// Creates an error struct with a `name: String` field and optional `info: Type` field. @@ -520,7 +511,7 @@ pub fn pack_err(input: TokenStream) -> TokenStream { #[cfg(all(feature = "structural_renderer", feature = "extra_macros"))] #[proc_macro] pub fn pack_err_structural(input: TokenStream) -> TokenStream { - pack_err::pack_err_structural(input) + func::pack_err_structural::pack_err_structural(input) } /// Early-returns the error from a `Result`, converting the `Ok` branch to the @@ -580,14 +571,7 @@ pub fn pack_err_structural(input: TokenStream) -> TokenStream { #[cfg(feature = "extra_macros")] #[proc_macro] pub fn route(input: TokenStream) -> TokenStream { - let expr = parse_macro_input!(input as syn::Expr); - let expanded = quote! { - match #expr { - Ok(r) => r, - Err(e) => return ::mingling::Routable::to_chain(e), - } - }; - TokenStream::from(expanded) + func::route::route(input) } /// Routes errors to the rendering pipeline instead of the chain pipeline. @@ -630,14 +614,7 @@ pub fn route(input: TokenStream) -> TokenStream { #[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) + func::render_route::render_route(input) } /// Creates an empty result value wrapped in `ChainProcess` for early return @@ -690,11 +667,8 @@ pub fn render_route(input: TokenStream) -> TokenStream { /// [`ChainProcess`]: https://docs.rs/mingling/latest/mingling/enum.ChainProcess.html #[cfg(feature = "extra_macros")] #[proc_macro] -pub fn empty_result(_input: TokenStream) -> TokenStream { - let expanded = quote! { - <crate::ResultEmpty as ::mingling::Routable::<crate::ThisProgram>>::to_chain(crate::ResultEmpty) - }; - TokenStream::from(expanded) +pub fn empty_result(input: TokenStream) -> TokenStream { + func::empty_result::empty_result(input) } /// Creates a `Dispatcher` implementation for a subcommand. @@ -1233,7 +1207,7 @@ pub fn entry(input: TokenStream) -> TokenStream { /// enum variant for `EntryType` to the help rendering logic in `HelpStruct`. #[proc_macro] pub fn register_help(input: TokenStream) -> TokenStream { - help::register_help(input) + func::register_help::register_help(input) } /// Registers a dispatcher at compile time for the `dispatch_tree` feature. @@ -1262,7 +1236,7 @@ pub fn register_help(input: TokenStream) -> TokenStream { /// - `dispatch_tree_gen` module — The trie generation logic. #[proc_macro] pub fn register_dispatcher(input: TokenStream) -> TokenStream { - dispatcher::register_dispatcher(input) + func::register_dispatcher::register_dispatcher(input) } /// Declares a help rendering function for an entry type. @@ -1358,8 +1332,8 @@ pub fn help(attr: TokenStream, item: TokenStream) -> TokenStream { /// fn some_item() {} /// ``` #[proc_macro_attribute] -pub fn mlint(_attr: TokenStream, item: TokenStream) -> TokenStream { - item +pub fn mlint(attr: TokenStream, item: TokenStream) -> TokenStream { + attr::mlint::mlint(attr, item) } /// Extension attribute macro that transforms `expr?` into `route!(expr)`. @@ -1475,7 +1449,7 @@ pub fn buffer(attr: TokenStream, item: TokenStream) -> TokenStream { /// ``` #[proc_macro] pub fn r_println(input: TokenStream) -> TokenStream { - func::r_print::r_println(input) + func::r_println::r_println(input) } /// Prints text to a `RenderResult` buffer, without a trailing newline. @@ -1537,7 +1511,7 @@ pub fn r_print(input: TokenStream) -> TokenStream { /// ``` #[proc_macro] pub fn r_eprintln(input: TokenStream) -> TokenStream { - func::r_print::r_eprintln(input) + func::r_eprintln::r_eprintln(input) } /// Prints text to a `RenderResult` buffer (standard error style), without a trailing newline. @@ -1570,7 +1544,7 @@ pub fn r_eprintln(input: TokenStream) -> TokenStream { /// ``` #[proc_macro] pub fn r_eprint(input: TokenStream) -> TokenStream { - func::r_print::r_eprint(input) + func::r_eprint::r_eprint(input) } /// Appends the contents of one `RenderResult` to another. @@ -1600,7 +1574,7 @@ pub fn r_eprint(input: TokenStream) -> TokenStream { /// ``` #[proc_macro] pub fn r_append(input: TokenStream) -> TokenStream { - func::r_print::r_append(input) + func::r_append::r_append(input) } /// Derive macro for automatically implementing the `Grouped` trait on a struct. @@ -1701,7 +1675,7 @@ pub fn derive_enum_tag(input: TokenStream) -> TokenStream { #[cfg(feature = "structural_renderer")] #[proc_macro_derive(StructuralData)] pub fn derive_structural_data(input: TokenStream) -> TokenStream { - structural_data::derive_structural_data(input) + derive::structural_data::derive_structural_data(input) } /// Derive macro for implementing both `Grouped` and `serde::Serialize` on a struct. @@ -1818,7 +1792,7 @@ pub fn gen_program(input: TokenStream) -> TokenStream { #[cfg(feature = "comp")] #[proc_macro] pub fn program_comp_gen(input: TokenStream) -> TokenStream { - func::gen_program::program_comp_gen_impl(input) + func::program_comp_gen::program_comp_gen_impl(input) } /// Registers a type into the global packed types registry for inclusion in @@ -1843,7 +1817,7 @@ pub fn program_comp_gen(input: TokenStream) -> TokenStream { /// Panics if the global `PACKED_TYPES` mutex is poisoned. #[proc_macro] pub fn register_type(input: TokenStream) -> TokenStream { - func::gen_program::register_type_impl(input) + func::register_type::register_type_impl(input) } /// Registers a chain mapping function into the global chain registry. @@ -1861,7 +1835,7 @@ pub fn register_type(input: TokenStream) -> TokenStream { /// Panics if the global `CHAINS` mutex is poisoned. #[proc_macro] pub fn register_chain(input: TokenStream) -> TokenStream { - func::gen_program::register_chain_impl(input) + func::register_chain::register_chain_impl(input) } /// Registers a renderer mapping function into the global renderer registry. @@ -1879,7 +1853,7 @@ pub fn register_chain(input: TokenStream) -> TokenStream { /// Panics if the global `RENDERERS` mutex is poisoned. #[proc_macro] pub fn register_renderer(input: TokenStream) -> TokenStream { - func::gen_program::register_renderer_impl(input) + func::register_renderer::register_renderer_impl(input) } /// Internal macro used by `gen_program!` to generate the fallback types for @@ -1889,7 +1863,7 @@ pub fn register_renderer(input: TokenStream) -> TokenStream { /// be called directly by user code. #[proc_macro] pub fn program_fallback_gen(input: TokenStream) -> TokenStream { - func::gen_program::program_fallback_gen_impl(input) + func::program_fallback_gen::program_fallback_gen_impl(input) } /// Internal macro used by `gen_program!` to generate the final program enum @@ -1943,7 +1917,7 @@ pub fn program_fallback_gen(input: TokenStream) -> TokenStream { /// ``` #[proc_macro] pub fn program_final_gen(input: TokenStream) -> TokenStream { - func::gen_program::program_final_gen_impl(input) + func::program_final_gen::program_final_gen_impl(input) } /// Builds a `Suggest` instance with inline suggestion items. @@ -2064,5 +2038,5 @@ pub fn suggest(input: TokenStream) -> TokenStream { #[cfg(feature = "comp")] #[proc_macro] pub fn suggest_enum(input: TokenStream) -> TokenStream { - suggest::suggest_enum(input) + func::suggest_enum::suggest_enum(input) } diff --git a/mingling_macros/src/systems/dispatch_tree_gen.rs b/mingling_macros/src/systems/dispatch_tree_gen.rs index 7383421..9eeb637 100644 --- a/mingling_macros/src/systems/dispatch_tree_gen.rs +++ b/mingling_macros/src/systems/dispatch_tree_gen.rs @@ -4,7 +4,7 @@ use just_fmt::snake_case; use proc_macro2::TokenStream; use quote::quote; -use crate::func::gen_program::resolve_type; +use crate::func::program_final_gen::resolve_type; /// Generate the `get_nodes()` function body for a ProgramCollect impl. /// If `pathf_map` is non-empty, resolves internal dispatcher statics using full paths. diff --git a/mingling_macros/src/systems/structural_data.rs b/mingling_macros/src/systems/structural_data.rs index 46d7cf8..512d318 100644 --- a/mingling_macros/src/systems/structural_data.rs +++ b/mingling_macros/src/systems/structural_data.rs @@ -1,344 +1,6 @@ -#![allow(dead_code)] - -use proc_macro::TokenStream; -use quote::quote; -use syn::{DeriveInput, Ident, TypePath, parse_macro_input}; - -use crate::get_global_set; - -/// Derive macro for `StructuralData`. -/// -/// This marks a type as eligible for structured output (JSON / YAML / TOML / RON). -/// The type must also implement `serde::Serialize` — the generated `impl StructuralData` -/// will fail to compile if `Serialize` is not in scope or implemented. -/// -/// Also registers the type name in the global `STRUCTURED_TYPES` registry so that -/// the `structural_render` match arm is generated by `gen_program!()`. -pub(crate) fn derive_structural_data(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as DeriveInput); - let type_name = input.ident; - - // Register in STRUCTURED_TYPES - let type_name_str = type_name.to_string(); - get_global_set(&crate::STRUCTURED_TYPES) - .lock() - .unwrap() - .insert(type_name_str); - - // Generate BOTH the sealed impl AND the StructuralData impl. - // 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<crate::ThisProgram> for #type_name {} - impl ::mingling::__private::StructuralData<crate::ThisProgram> for #type_name {} - }; - - expanded.into() -} - -/// `pack_structural!` — like `pack!` but also marks the type as supporting -/// structured output via `StructuralData`. -/// -/// # Syntax -/// -/// ```rust,ignore -/// pack_structural!(Info = (String, i32)); -/// ``` -/// -/// This is equivalent to: -/// ```rust,ignore -/// pack!(Info = (String, i32)); -/// impl ::mingling::StructuralData for Info {} -/// ``` -pub(crate) fn pack_structural(input: TokenStream) -> TokenStream { - // Parse same input format as `pack!` - let input_parsed = syn::parse_macro_input!(input as PackStructuralInput); - let type_name = input_parsed.type_name; - let inner_type = input_parsed.inner_type; - let attrs = input_parsed.attrs; - let program_path = crate::default_program_path(); - - // Register in STRUCTURED_TYPES - let type_name_str = type_name.to_string(); - get_global_set(&crate::STRUCTURED_TYPES) - .lock() - .unwrap() - .insert(type_name_str); - - // Struct definition (with Serialize derive, same as pack! under structural_renderer) - #[cfg(not(feature = "structural_renderer"))] - let struct_def = quote! { - #(#attrs)* - pub struct #type_name { - pub inner: #inner_type, - } - }; - - #[cfg(feature = "structural_renderer")] - let struct_def = quote! { - #(#attrs)* - #[derive(serde::Serialize)] - pub struct #type_name { - pub inner: #inner_type, - } - }; - - // Helper impls (same as pack!) - let new_impl = quote! { - impl #type_name { - pub fn new(inner: #inner_type) -> Self { - Self { inner } - } - } - }; - - let from_into_impl = quote! { - impl From<#inner_type> for #type_name { - fn from(inner: #inner_type) -> Self { - Self::new(inner) - } - } - impl From<#type_name> for #inner_type { - fn from(wrapper: #type_name) -> #inner_type { - wrapper.inner - } - } - }; - - let as_ref_impl = quote! { - impl ::std::convert::AsRef<#inner_type> for #type_name { - fn as_ref(&self) -> &#inner_type { - &self.inner - } - } - impl ::std::convert::AsMut<#inner_type> for #type_name { - fn as_mut(&mut self) -> &mut #inner_type { - &mut self.inner - } - } - }; - - let deref_impl = quote! { - impl ::std::ops::Deref for #type_name { - type Target = #inner_type; - fn deref(&self) -> &Self::Target { - &self.inner - } - } - impl ::std::ops::DerefMut for #type_name { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.inner - } - } - }; - - let default_impl = quote! { - impl ::std::default::Default for #type_name - where - #inner_type: ::std::default::Default, - { - fn default() -> Self { - Self::new(::std::default::Default::default()) - } - } - }; - - let register_impl = quote! { - ::mingling::macros::register_type!(#type_name); - }; - - // StructuralData impl + sealed + registration - let structural_impl = quote! { - impl ::mingling::__private::StructuralDataSealed<crate::ThisProgram> for #type_name {} - impl ::mingling::__private::StructuralData<crate::ThisProgram> for #type_name {} - }; - - let expanded = quote! { - #struct_def - - #new_impl - #from_into_impl - #as_ref_impl - #deref_impl - #default_impl - #register_impl - #structural_impl - - impl Into<::mingling::AnyOutput<#program_path>> for #type_name { - fn into(self) -> ::mingling::AnyOutput<#program_path> { - ::mingling::AnyOutput::new(self) - } - } - - impl Into<::mingling::ChainProcess<#program_path>> for #type_name { - fn into(self) -> ::mingling::ChainProcess<#program_path> { - ::mingling::AnyOutput::new(self).route_chain() - } - } - - /// 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 - } - } - }; - - expanded.into() -} - -/// Input for `pack_structural!` — same format as `pack!`. -struct PackStructuralInput { - attrs: Vec<syn::Attribute>, - type_name: Ident, - inner_type: syn::Type, -} - -impl syn::parse::Parse for PackStructuralInput { - fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> { - let attrs = input.call(syn::Attribute::parse_outer)?; - let type_name: Ident = input.parse()?; - input.parse::<syn::Token![=]>()?; - let inner_type: syn::Type = input.parse()?; - Ok(PackStructuralInput { - attrs, - type_name, - inner_type, - }) - } -} - -/// `group_structural!` — like `group!` but also marks the type as supporting -/// structured output via `StructuralData`. -/// -/// # Syntax -/// -/// ```rust,ignore -/// group_structural!(Info = (String, i32)); -/// ``` -/// -/// This is equivalent to: -/// ```rust,ignore -/// group!(Info = (String, i32)); -/// impl ::mingling::StructuralData for Info {} -/// ``` -pub(crate) fn group_structural(input: TokenStream) -> TokenStream { - // Parse the same input as group! - let input_parsed = syn::parse_macro_input!(input as GroupStructuralInput); - - let is_aliased = matches!(&input_parsed, GroupStructuralInput::Aliased { .. }); - - let (type_path, type_name, alias_stmt) = match &input_parsed { - GroupStructuralInput::Plain(type_path) => { - let name = type_path - .path - .segments - .last() - .expect("TypePath must have at least one segment") - .ident - .clone(); - (type_path.clone(), name, quote! {}) - } - GroupStructuralInput::Aliased { alias, type_path } => { - let alias_stmt = quote! { - pub(crate) type #alias = #type_path; - }; - (type_path.clone(), alias.clone(), alias_stmt) - } - }; - - let type_name_str = type_name.to_string(); - - // Register in STRUCTURED_TYPES - get_global_set(&crate::STRUCTURED_TYPES) - .lock() - .unwrap() - .insert(type_name_str); - - let program_path = crate::default_program_path(); - - // Generate unique module name - let segments: Vec<String> = type_path - .path - .segments - .iter() - .map(|seg| seg.ident.to_string().to_lowercase()) - .collect(); - let module_name = Ident::new( - &format!("internal_group_{}", segments.join("_")), - proc_macro2::Span::call_site(), - ); - - // Generate the appropriate `use` statement for the original type - // (consistent with gen_type_use in group_impl.rs) - let type_use = if type_path.path.segments.len() > 1 { - quote! { #[allow(unused_imports)] use #type_path; } - } else { - let ident = type_path - .path - .segments - .last() - .expect("TypePath must have at least one segment") - .ident - .clone(); - quote! { #[allow(unused_imports)] use super::#ident; } - }; - - let alias_use = if is_aliased { - quote! { use super::#type_name; } - } else { - quote! {} - }; - - let expanded = quote! { - #alias_stmt - #[allow(non_camel_case_types)] - mod #module_name { - use #program_path as __MinglingProgram; - #type_use - #alias_use - - /// 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<crate::ThisProgram> for #type_name {} - impl ::mingling::__private::StructuralData<crate::ThisProgram> for #type_name {} - - ::mingling::macros::register_type!(#type_name); - } - }; - - expanded.into() -} - -/// Input for `group_structural!` — same format as `group!`. -enum GroupStructuralInput { - Plain(TypePath), - Aliased { alias: Ident, type_path: TypePath }, -} - -impl syn::parse::Parse for GroupStructuralInput { - fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> { - let fork = input.fork(); - let _first: Ident = fork.parse()?; - if fork.peek(syn::Token![=]) { - let alias: Ident = input.parse()?; - let _eq: syn::Token![=] = input.parse()?; - let type_path: TypePath = input.parse()?; - Ok(GroupStructuralInput::Aliased { alias, type_path }) - } else { - let type_path: TypePath = input.parse()?; - Ok(GroupStructuralInput::Plain(type_path)) - } - } -} +//! Legacy structural data module. +//! +//! Functions have been moved to: +//! - `derive::structural_data` — `derive_structural_data` +//! - `func::pack_structural` — `pack_structural` +//! - `func::group_structural` — `group_structural` |
