From 1f583f625a7d541dc7d47a8136b31d29a5ed5441 Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Mon, 20 Jul 2026 00:59:17 +0800 Subject: chore(macros): reorganize module hierarchy in mingling_macros Restructure flat module layout into logical directories (attr/, derive/, func/, systems/, extensions/, utils.rs). Tighten internal function visibility to pub(crate). All public API signatures remain unchanged. --- mingling_macros/src/func/dispatcher.rs | 266 ++++++++++++++ mingling_macros/src/func/entry.rs | 70 ++++ mingling_macros/src/func/gen_program.rs | 602 ++++++++++++++++++++++++++++++++ mingling_macros/src/func/group.rs | 147 ++++++++ mingling_macros/src/func/node.rs | 59 ++++ mingling_macros/src/func/pack.rs | 149 ++++++++ mingling_macros/src/func/pack_err.rs | 209 +++++++++++ mingling_macros/src/func/suggest.rs | 93 +++++ 8 files changed, 1595 insertions(+) create mode 100644 mingling_macros/src/func/dispatcher.rs create mode 100644 mingling_macros/src/func/entry.rs create mode 100644 mingling_macros/src/func/gen_program.rs create mode 100644 mingling_macros/src/func/group.rs create mode 100644 mingling_macros/src/func/node.rs create mode 100644 mingling_macros/src/func/pack.rs create mode 100644 mingling_macros/src/func/pack_err.rs create mode 100644 mingling_macros/src/func/suggest.rs (limited to 'mingling_macros/src/func') diff --git a/mingling_macros/src/func/dispatcher.rs b/mingling_macros/src/func/dispatcher.rs new file mode 100644 index 0000000..808df75 --- /dev/null +++ b/mingling_macros/src/func/dispatcher.rs @@ -0,0 +1,266 @@ +#[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; + +enum DispatcherChainInput { + Default { + cmd_attrs: Vec, + entry_attrs: Vec, + command_name: syn::LitStr, + command_struct: Ident, + pack: Ident, + }, + #[cfg(feature = "extra_macros")] + Auto { + cmd_attrs: Vec, + command_name: syn::LitStr, + }, +} + +impl Parse for DispatcherChainInput { + fn parse(input: ParseStream) -> SynResult { + // Collect outer attributes for the CMD struct + let cmd_attrs = input.call(Attribute::parse_outer)?; + + if input.peek(syn::LitStr) { + // Parse the command name string first + let command_name: LitStr = input.parse()?; + + // Check if this is the abbreviated form: just "command_name" without ", CMD => Entry" + if input.is_empty() { + #[cfg(feature = "extra_macros")] + { + return Ok(DispatcherChainInput::Auto { + cmd_attrs, + command_name, + }); + } + #[cfg(not(feature = "extra_macros"))] + { + return Err(syn::Error::new( + command_name.span(), + "expected `, CommandStruct => EntryStruct` after command name", + )); + } + } + + // Default format: "command_name", CommandStruct => ChainStruct + input.parse::()?; + let command_struct = input.parse()?; + input.parse::]>()?; + let entry_attrs = input.call(Attribute::parse_outer)?; + let pack = input.parse()?; + + Ok(DispatcherChainInput::Default { + cmd_attrs, + entry_attrs, + command_name, + command_struct, + pack, + }) + } else { + Err(input.lookahead1().error()) + } + } +} + +// NOTICE: The token stream generation patterns in `dispatcher_chain` and `dispatcher_render` +// are nearly identical and could benefit from refactoring into common helper functions. + +#[allow(clippy::too_many_lines)] +pub(crate) fn dispatcher(input: TokenStream) -> TokenStream { + // Parse the input + let dispatcher_input = syn::parse_macro_input!(input as DispatcherChainInput); + + #[cfg(not(feature = "extra_macros"))] + let (command_name, command_struct, pack, cmd_attrs, entry_attrs) = match dispatcher_input { + DispatcherChainInput::Default { + cmd_attrs, + entry_attrs, + command_name, + command_struct, + pack, + } => (command_name, command_struct, pack, cmd_attrs, entry_attrs), + }; + + #[cfg(feature = "extra_macros")] + let (command_name, command_struct, pack, cmd_attrs, entry_attrs) = match dispatcher_input { + DispatcherChainInput::Default { + cmd_attrs, + entry_attrs, + command_name, + command_struct, + pack, + } => (command_name, command_struct, pack, cmd_attrs, entry_attrs), + DispatcherChainInput::Auto { + cmd_attrs, + command_name, + } => { + let command_name_str = command_name.value(); + let pascal = dotted_to_pascal_case(&command_name_str); + let command_struct = Ident::new(&format!("CMD{pascal}"), command_name.span()); + let pack = Ident::new(&format!("Entry{pascal}"), command_name.span()); + (command_name, command_struct, pack, cmd_attrs, Vec::new()) + } + }; + + let command_name_str = command_name.value(); + + let comp_entry = get_comp_entry(&pack); + + let dispatch_tree_entry = get_dispatch_tree_entry(&command_name_str, &command_struct, &pack); + + let program_type = crate::default_program_path(); + + let expanded = quote! { + #(#cmd_attrs)* + #[derive(Debug, Default)] + pub struct #command_struct; + + ::mingling::macros::pack!(#(#entry_attrs)* #pack = Vec); + + #comp_entry + #dispatch_tree_entry + + impl ::mingling::Dispatcher<#program_type> for #command_struct { + fn node(&self) -> ::mingling::Node { + ::mingling::macros::node!(#command_name_str) + } + fn begin(&self, args: Vec) -> ::mingling::ChainProcess<#program_type> { + use ::mingling::Grouped; + #pack::new(args).to_chain() + } + fn clone_dispatcher(&self) -> Box> { + Box::new(#command_struct) + } + } + }; + + expanded.into() +} + +#[cfg(feature = "comp")] +fn get_comp_entry(entry_name: &Ident) -> TokenStream2 { + let comp_entry = quote! { + impl ::mingling::CompletionEntry for #entry_name { + fn get_input(self) -> Vec { + self.inner.clone() + } + } + }; + comp_entry +} + +#[cfg(not(feature = "comp"))] +fn get_comp_entry(_entry_name: &Ident) -> TokenStream2 { + quote! {} +} + +#[cfg(feature = "dispatch_tree")] +fn get_dispatch_tree_entry( + command_name_str: &str, + command_struct: &Ident, + entry_name: &Ident, +) -> TokenStream2 { + let node_name_lit = syn::LitStr::new(command_name_str, proc_macro2::Span::call_site()); + quote! { + ::mingling::macros::register_dispatcher!(#node_name_lit, #command_struct, #entry_name); + } +} + +#[cfg(not(feature = "dispatch_tree"))] +fn get_dispatch_tree_entry( + _command_name_str: &str, + _command_struct: &Ident, + _entry_name: &Ident, +) -> TokenStream2 { + 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 { + let node_name = input.parse()?; + input.parse::()?; + let dispatcher_type = input.parse()?; + input.parse::()?; + 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 { + s.split('.') + .map(|segment| { + let mut chars = segment.chars(); + match chars.next() { + None => String::new(), + Some(c) => c.to_uppercase().to_string() + chars.as_str(), + } + }) + .collect() +} diff --git a/mingling_macros/src/func/entry.rs b/mingling_macros/src/func/entry.rs new file mode 100644 index 0000000..35209e5 --- /dev/null +++ b/mingling_macros/src/func/entry.rs @@ -0,0 +1,70 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::{Ident, LitStr, parse_macro_input}; + +enum EntryInput { + Typed { ident: Ident, strings: Vec }, + Untyped { strings: Vec }, +} + +impl syn::parse::Parse for EntryInput { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + // entry!(EntryType, ["a", "b", "c"]) + // entry!["a", "b", "c"] — comes in as just ["a", "b", "c"] + if input.peek(Ident) && input.peek2(syn::Token![,]) { + // entry!(EntryType, ["a", "b", "c"]) + let ident: Ident = input.parse()?; + let _comma: syn::Token![,] = input.parse()?; + let content; + syn::bracketed!(content in input); + let strings = parse_strings(&content)?; + Ok(EntryInput::Typed { ident, strings }) + } else { + // entry!["a", "b", "c"] — bare bracket content + let strings = parse_strings(input)?; + Ok(EntryInput::Untyped { strings }) + } + } +} + +fn parse_strings(input: &syn::parse::ParseBuffer) -> syn::Result> { + let mut strings = Vec::new(); + while !input.is_empty() { + let s: LitStr = input.parse()?; + strings.push(s.value()); + if input.peek(syn::Token![,]) { + let _comma: syn::Token![,] = input.parse()?; + } + } + Ok(strings) +} + +pub(crate) fn entry(input: TokenStream) -> TokenStream { + let parsed = parse_macro_input!(input as EntryInput); + + let strings = match &parsed { + EntryInput::Typed { strings, .. } | EntryInput::Untyped { strings } => strings, + }; + let string_exprs = strings + .iter() + .map(|s| { + let lit = syn::LitStr::new(s, proc_macro2::Span::call_site()); + quote! { #lit.to_string() } + }) + .collect::>(); + + let expanded = match parsed { + EntryInput::Typed { ident, .. } => { + quote! { + #ident::new(vec![#(#string_exprs),*]) + } + } + EntryInput::Untyped { .. } => { + quote! { + vec![#(#string_exprs),*].into() + } + } + }; + + expanded.into() +} diff --git a/mingling_macros/src/func/gen_program.rs b/mingling_macros/src/func/gen_program.rs new file mode 100644 index 0000000..7128a9a --- /dev/null +++ b/mingling_macros/src/func/gen_program.rs @@ -0,0 +1,602 @@ +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 empty map when pathf feature is not enabled. +fn load_pathf_map() -> std::collections::HashMap { + if !cfg!(feature = "pathf") { + return std::collections::HashMap::new(); + } + let out_dir = std::env::var("OUT_DIR").ok(); + let crate_name = std::env::var("CARGO_PKG_NAME").ok(); + match (out_dir, crate_name) { + (Some(dir), Some(name)) => { + let path = std::path::Path::new(&dir).join(&name).join("type_using.rs"); + match std::fs::read_to_string(&path) { + Ok(content) => content + .lines() + .filter_map(|line| { + let line = line.trim(); + if let Some(rest) = line.strip_prefix("use ") { + let path = rest.strip_suffix(';').unwrap_or(rest); + if let Some((_mod, type_name)) = path.rsplit_once("::") { + return Some((type_name.to_string(), path.to_string())); + } + } + None + }) + .collect(), + Err(_) => std::collections::HashMap::new(), + } + } + _ => std::collections::HashMap::new(), + } +} + +/// Resolves a type name to its full path token stream using the pathf mapping. +pub(crate) fn resolve_type( + name: &str, + map: &std::collections::HashMap, +) -> proc_macro2::TokenStream { + if let Some(full_path) = map.get(name) { + syn::parse_str::(full_path).unwrap_or_else(|_| { + let ident = proc_macro2::Ident::new(name, proc_macro2::Span::call_site()); + quote! { #ident } + }) + } else { + let ident = proc_macro2::Ident::new(name, proc_macro2::Span::call_site()); + quote! { #ident } + } +} + +pub(crate) fn gen_program_impl(_input: TokenStream) -> TokenStream { + #[cfg(feature = "comp")] + let comp_gen = quote! { + ::mingling::macros::program_comp_gen!(); + }; + + #[cfg(not(feature = "comp"))] + let comp_gen = quote! {}; + + TokenStream::from(quote! { + /// Alias for the current program type `crate::ThisProgram` + pub type Next = ::mingling::ChainProcess; + + impl ::mingling::Routable for ::mingling::ChainProcess + { + fn to_chain(self) -> ::mingling::ChainProcess { + match self { + ::mingling::ChainProcess::Ok((any, _)) => { + ::mingling::ChainProcess::Ok((any, mingling::NextProcess::Chain)) + } + other => other, + } + } + + fn to_render(self) -> ::mingling::ChainProcess { + match self { + ::mingling::ChainProcess::Ok((any, _)) => { + ::mingling::ChainProcess::Ok((any, mingling::NextProcess::Renderer)) + } + other => other, + } + } + } + + #comp_gen + ::mingling::macros::program_fallback_gen!(); + ::mingling::macros::program_final_gen!(); + }) +} + +#[cfg(feature = "comp")] +pub(crate) fn program_comp_gen_impl(_input: TokenStream) -> TokenStream { + #[cfg(feature = "async")] + let fn_exec_comp = quote! { + #[doc(hidden)] + #[::mingling::macros::chain] + pub async fn __exec_completion(prev: CompletionContext) -> Next { + use ::mingling::Grouped; + + let read_ctx = ::mingling::ShellContext::try_from(prev.inner); + match read_ctx { + Ok(ctx) => { + let suggest = ::mingling::CompletionHelper::exec_completion::(&ctx); + crate::CompletionSuggest::new((ctx, suggest)).to_render() + } + Err(_) => std::process::exit(1), + } + } + }; + + #[cfg(not(feature = "async"))] + let fn_exec_comp = quote! { + #[doc(hidden)] + #[::mingling::macros::chain] + pub fn __exec_completion(prev: CompletionContext) -> Next { + use ::mingling::Grouped; + + let read_ctx = ::mingling::ShellContext::try_from(prev.inner); + match read_ctx { + Ok(ctx) => { + let suggest = ::mingling::CompletionHelper::exec_completion::(&ctx); + crate::CompletionSuggest::new((ctx, suggest)).to_render() + } + Err(_) => std::process::exit(1), + } + } + }; + + #[cfg(feature = "dispatch_tree")] + let internal_dispatcher_comp = quote! { + use __internal_completion_mod::__internal_dispatcher_comp; + }; + + #[cfg(not(feature = "dispatch_tree"))] + let internal_dispatcher_comp = quote! {}; + + let comp_dispatcher = quote! { + #[doc(hidden)] + mod __internal_completion_mod { + use ::mingling::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::(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); + #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 = packed_types + .iter() + .map(|s| syn::parse_str::(s).unwrap()) + .collect(); + + let renderer_tokens: Vec = renderers + .iter() + .map(|s| syn::parse_str::(s).unwrap()) + .collect(); + + let chain_tokens: Vec = chains + .iter() + .map(|s| syn::parse_str::(s).unwrap()) + .collect(); + + let renderer_exist_tokens: Vec = renderer_exist + .iter() + .map(|s| syn::parse_str::(s).unwrap()) + .collect(); + + let chain_exist_tokens: Vec = chain_exist + .iter() + .map(|s| syn::parse_str::(s).unwrap()) + .collect(); + + let pathf_map: std::collections::HashMap = if cfg!(feature = "pathf") { + load_pathf_map() + } else { + std::collections::HashMap::new() + }; + + let pathf_uses: Vec = 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 = structural_renderers + .iter() + .map(|s| syn::parse_str::(s).unwrap()) + .collect(); + + #[cfg(feature = "structural_renderer")] + let structural_render = quote! { + fn structural_render( + any: ::mingling::AnyOutput, + 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 = 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 = completions + .iter() + .map(|s| syn::parse_str::(s).unwrap()) + .collect(); + + #[cfg(feature = "comp")] + let comp = quote! { + fn do_comp(any: &::mingling::AnyOutput, 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) -> ::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) -> ::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>::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>::proc(value) + } + } + }) + .collect(); + + let do_chain_fn = if chain_tokens.is_empty() { + quote! { + fn do_chain(_any: ::mingling::AnyOutput) -> ::mingling::ChainProcess { + ::core::panic!("No chain found for type id") + } + } + } else if ASYNC_ENABLED { + quote! { + fn do_chain( + any: ::mingling::AnyOutput, + ) -> ::std::pin::Pin<::std::boxed::Box> + ::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, + ) -> ::mingling::ChainProcess { + match any.member_id { + #(#chain_arms_sync)* + _ => ::core::panic!("No chain found for type id: {:?}", any.type_id), + } + } + } + }; + + let help_tokens: Vec = get_global_set(&HELP_REQUESTS) + .lock() + .unwrap() + .clone() + .iter() + .map(|s| syn::parse_str::(s).unwrap()) + .collect(); + + let num_variants = packed_types.len(); + let repr_type = if u8::try_from(num_variants).is_ok() { + quote! { u8 } + } else if u16::try_from(num_variants).is_ok() { + quote! { u16 } + } else if u32::try_from(num_variants).is_ok() { + quote! { u32 } + } else { + quote! { u128 } + }; + + let expanded = quote! { + #[derive(Debug, PartialEq, Eq, Clone)] + #[repr(#repr_type)] + #[allow(nonstandard_style)] + pub enum #name { + #(#packed_types),* + } + + impl ::std::fmt::Display for #name { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + match self { + #(#name::#packed_types => write!(f, stringify!(#packed_types)),)* + } + } + } + + impl ::mingling::ProgramCollect for #name { + type Enum = #name; + type ErrorDispatcherNotFound = ErrorDispatcherNotFound; + type ErrorRendererNotFound = ErrorRendererNotFound; + type ResultEmpty = ResultEmpty; + fn build_renderer_not_found(member_id: Self::Enum) -> ::mingling::AnyOutput { + ::mingling::AnyOutput::new(ErrorRendererNotFound::new(member_id.to_string())) + } + fn build_dispatcher_not_found(args: Vec) -> ::mingling::AnyOutput { + ::mingling::AnyOutput::new(ErrorDispatcherNotFound::new(args)) + } + fn build_empty_result() -> ::mingling::AnyOutput { + ::mingling::AnyOutput::new(ResultEmpty) + } + #render_fn + #do_chain_fn + fn render_help(any: ::mingling::AnyOutput) -> ::mingling::RenderResult { + #[allow(unused_imports)] + #(#pathf_uses)* + match any.member_id { + #(#help_tokens)* + _ => ::mingling::RenderResult::default(), + } + } + fn has_renderer(any: &::mingling::AnyOutput) -> bool { + match any.member_id { + #(#renderer_exist_tokens)* + _ => false + } + } + fn has_chain(any: &::mingling::AnyOutput) -> 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.rs b/mingling_macros/src/func/group.rs new file mode 100644 index 0000000..b865913 --- /dev/null +++ b/mingling_macros/src/func/group.rs @@ -0,0 +1,147 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::parse::{Parse, ParseStream}; +use syn::{Ident, Result as SynResult, TypePath}; + +/// Input for the `group!` macro +/// +/// # Syntax +/// +/// ```rust,ignore +/// /// Only a type path — uses default `crate::ThisProgram` as program +/// group!(std::io::Error); +/// group!(ParseIntError); +/// +/// /// With an alias — creates a `pub type Alias = Path;` and uses `Alias` as variant name +/// group!(IoError = std::io::Error); +/// ``` +enum GroupInput { + /// `group!(TypePath)` — variant name is the last path segment + Plain(TypePath), + + /// `group!(Alias = TypePath)` — variant name is `Alias`, also generates `pub type Alias = TypePath;` + Aliased { alias: Ident, type_path: TypePath }, +} + +impl Parse for GroupInput { + fn parse(input: ParseStream) -> SynResult { + // Peek ahead: if the second token is `=`, parse as aliased form + let fork = input.fork(); + let _first: Ident = fork.parse()?; + if fork.peek(syn::Token![=]) { + // Consume the ident and `=` from the real input + let alias: Ident = input.parse()?; + let _eq: syn::Token![=] = input.parse()?; + let type_path: TypePath = input.parse()?; + Ok(GroupInput::Aliased { alias, type_path }) + } else { + let type_path: TypePath = input.parse()?; + Ok(GroupInput::Plain(type_path)) + } + } +} + +/// Convert a type path into a valid module name segment +/// +/// e.g. `std::io::Error` -> `internal_group_std_io_error` +fn module_name_from_type(type_path: &TypePath) -> Ident { + let segments: Vec = type_path + .path + .segments + .iter() + .map(|seg| seg.ident.to_string().to_lowercase()) + .collect(); + Ident::new( + &format!("internal_group_{}", segments.join("_")), + proc_macro2::Span::call_site(), + ) +} + +/// Get the last segment name of a type path (the simple type name) +/// +/// e.g. `std::io::Error` -> `Error` +fn type_simple_name(type_path: &TypePath) -> Ident { + type_path + .path + .segments + .last() + .expect("TypePath must have at least one segment") + .ident + .clone() +} + +/// Generate the `use` token for the type path inside the generated module. +/// +/// - Multi-segment path (e.g. `std::num::ParseIntError`): `use std::num::ParseIntError;` +/// - Single-segment path (e.g. `ParseIntError`): `use super::ParseIntError;` +fn gen_type_use(type_path: &TypePath) -> proc_macro2::TokenStream { + if type_path.path.segments.len() > 1 { + // Full path: use it directly + quote! { + #[allow(unused_imports)] + use #type_path; + } + } else { + // Single ident: import from parent scope + let ident = type_simple_name(type_path); + quote! { + #[allow(unused_imports)] + use super::#ident; + } + } +} + +pub(crate) fn group_macro(input: TokenStream) -> TokenStream { + let input = syn::parse_macro_input!(input as GroupInput); + + let is_aliased = matches!(input, GroupInput::Aliased { .. }); + + let (type_path, type_name, alias_stmt) = match input { + GroupInput::Plain(type_path) => { + let type_name = type_simple_name(&type_path); + (type_path, type_name, quote! {}) + } + GroupInput::Aliased { alias, type_path } => { + let type_name = alias.clone(); + let alias_stmt = quote! { + pub type #alias = #type_path; + }; + (type_path, type_name, alias_stmt) + } + }; + + let program_path = crate::default_program_path(); + + // Create a unique module name from the type path (use alias name for aliased form) + let module_name = module_name_from_type(&type_path); + // Generate the appropriate `use` statement for the type + let type_use = gen_type_use(&type_path); + + // For aliased form, also import the alias from parent scope + let alias_use = if is_aliased { + quote! { use super::#type_name; } + } else { + quote! {} + }; + + // Generate the module with the Grouped implementation + let expanded = quote! { + #alias_stmt + #[allow(non_camel_case_types)] + mod #module_name { + use #program_path as __MinglingProgram; + #type_use + #alias_use + + impl ::mingling::Grouped<__MinglingProgram> for #type_name { + fn member_id() -> __MinglingProgram { + __MinglingProgram::#type_name + } + } + + ::mingling::macros::register_type!(#type_name); + } + }; + + expanded.into() +} diff --git a/mingling_macros/src/func/node.rs b/mingling_macros/src/func/node.rs new file mode 100644 index 0000000..1b944a1 --- /dev/null +++ b/mingling_macros/src/func/node.rs @@ -0,0 +1,59 @@ +use just_fmt::kebab_case; +use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; +use quote::quote; +use syn::parse::{Parse, ParseStream}; +use syn::{LitStr, Result as SynResult}; + +/// Parses a string literal input for the node macro +struct NodeInput { + path: LitStr, +} + +impl Parse for NodeInput { + fn parse(input: ParseStream) -> SynResult { + Ok(NodeInput { + path: input.parse()?, + }) + } +} + +pub(crate) fn node(input: TokenStream) -> TokenStream { + // Parse the input as a string literal + let input_parsed = syn::parse_macro_input!(input as NodeInput); + let path_str = input_parsed.path.value(); + + // If the input string is empty, return an empty Node + if path_str.is_empty() { + return quote! { + mingling::Node::default() + } + .into(); + } + + // Split the path by dots + let parts: Vec = path_str + .split('.') + .map(|s| { + if s.starts_with('_') { + s.to_string() + } else { + kebab_case!(s).to_string() + } + }) + .collect(); + + // Build the expression starting from Node::default() + let mut expr: TokenStream2 = quote! { + mingling::Node::default() + }; + + // Add .join() calls for each part of the path + for part in parts { + expr = quote! { + #expr.join(#part) + }; + } + + expr.into() +} diff --git a/mingling_macros/src/func/pack.rs b/mingling_macros/src/func/pack.rs new file mode 100644 index 0000000..a1a7e6b --- /dev/null +++ b/mingling_macros/src/func/pack.rs @@ -0,0 +1,149 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::parse::{Parse, ParseStream}; +use syn::{Attribute, Ident, Result as SynResult, Token, Type}; + +struct PackInput { + attrs: Vec, + type_name: Ident, + inner_type: Type, +} + +impl Parse for PackInput { + fn parse(input: ParseStream) -> SynResult { + let attrs = input.call(Attribute::parse_outer)?; + let type_name: Ident = input.parse()?; + input.parse::()?; + let inner_type: Type = input.parse()?; + + Ok(PackInput { + attrs, + type_name, + inner_type, + }) + } +} + +#[allow(clippy::too_many_lines)] +pub(crate) fn pack(input: TokenStream) -> TokenStream { + let pack_input = syn::parse_macro_input!(input as PackInput); + + let group_name = crate::default_program_path(); + let type_name = pack_input.type_name; + let inner_type = pack_input.inner_type; + let attrs = pack_input.attrs; + + // Generate the struct definition + // Note: No longer derives Serialize under structural_renderer. + // Use pack_structual! for structured output support. + let struct_def = quote! { + #(#attrs)* + pub struct #type_name { + pub(crate) inner: #inner_type, + } + }; + + // Generate the new() method + let new_impl = quote! { + impl #type_name { + /// Creates a new instance of the wrapper type + pub fn new(inner: #inner_type) -> Self { + Self { inner } + } + } + }; + + // Generate From and Into implementations + 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 + } + } + }; + + // Generate AsRef and AsMut implementations + 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 + } + } + }; + + // Generate Deref and DerefMut implementations + 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 + } + } + }; + + // Check if the inner type implements Default by generating conditional code + 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); + }; + + let expanded = quote! { + #struct_def + + #new_impl + #from_into_impl + #as_ref_impl + #deref_impl + #default_impl + #register_impl + + impl Into> for #type_name { + fn into(self) -> mingling::AnyOutput<#group_name> { + mingling::AnyOutput::new(self) + } + } + + impl Into> for #type_name { + fn into(self) -> mingling::ChainProcess<#group_name> { + mingling::AnyOutput::new(self).route_chain() + } + } + + impl ::mingling::Grouped<#group_name> for #type_name { + fn member_id() -> #group_name { + #group_name::#type_name + } + } + }; + + expanded.into() +} diff --git a/mingling_macros/src/func/pack_err.rs b/mingling_macros/src/func/pack_err.rs new file mode 100644 index 0000000..36e550a --- /dev/null +++ b/mingling_macros/src/func/pack_err.rs @@ -0,0 +1,209 @@ +use just_fmt::snake_case; +use proc_macro::TokenStream; +use quote::quote; +use syn::{Ident, Token, Type, parse_macro_input}; + +enum PackErrInput { + /// pack_err!(ErrorNotFound) + Simple { type_name: Ident }, + /// pack_err!(ErrorNotDir = PathBuf) + Typed { + type_name: Ident, + inner_type: Box, + }, +} + +impl syn::parse::Parse for PackErrInput { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let type_name: Ident = input.parse()?; + + if input.peek(Token![=]) { + input.parse::()?; + let inner_type: Type = input.parse()?; + Ok(PackErrInput::Typed { + type_name, + inner_type: Box::new(inner_type), + }) + } else { + Ok(PackErrInput::Simple { type_name }) + } + } +} + +#[allow(clippy::too_many_lines)] +pub(crate) fn pack_err(input: TokenStream) -> TokenStream { + let parsed = parse_macro_input!(input as PackErrInput); + + match parsed { + PackErrInput::Simple { type_name } => { + let name_str = type_name.to_string(); + let snake_name = snake_case!(&name_str); + + // Note: No longer derives Serialize under structural_renderer. + // Use pack_err_structural for structured output support. + let derive = quote! { + #[derive(::mingling::Grouped)] + }; + + let expanded = quote! { + #derive + 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); + }; + + expanded.into() + } + PackErrInput::Typed { + type_name, + inner_type, + } => { + let name_str = type_name.to_string(); + let snake_name = snake_case!(&name_str); + + // Note: No longer derives Serialize under structural_renderer. + // Use pack_err_structural for structured output support. + let derive = quote! { + #[derive(::mingling::Grouped)] + }; + + let expanded = quote! { + #derive + 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); + }; + + expanded.into() + } + } +} + +/// `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 for #type_name {} + impl ::mingling::__private::StructuralData 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/suggest.rs b/mingling_macros/src/func/suggest.rs new file mode 100644 index 0000000..0f2026f --- /dev/null +++ b/mingling_macros/src/func/suggest.rs @@ -0,0 +1,93 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::parse::{Parse, ParseStream}; +use syn::punctuated::Punctuated; +use syn::{Expr, LitStr, Token, parse_macro_input}; + +struct SuggestInput { + items: Punctuated, +} + +enum SuggestItem { + WithDesc(Box<(LitStr, Expr)>), // "-i" = "Insert something" + Simple(LitStr), // "-I" +} + +impl Parse for SuggestInput { + fn parse(input: ParseStream) -> syn::Result { + let items = Punctuated::parse_terminated(input)?; + Ok(SuggestInput { items }) + } +} + +impl Parse for SuggestItem { + fn parse(input: ParseStream) -> syn::Result { + let key: LitStr = input.parse()?; + + if input.peek(Token![:]) { + let _colon: Token![:] = input.parse()?; + let value: Expr = input.parse()?; + Ok(SuggestItem::WithDesc(Box::new((key, value)))) + } else { + Ok(SuggestItem::Simple(key)) + } + } +} + +#[cfg(feature = "comp")] +pub(crate) fn suggest(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as SuggestInput); + + let mut items = Vec::new(); + + for item in input.items { + match item { + SuggestItem::WithDesc(boxed) => { + let (key, value) = *boxed; + items.push(quote! { + ::mingling::SuggestItem::new_with_desc(#key.to_string(), #value.to_string()) + }); + } + SuggestItem::Simple(key) => { + items.push(quote! { + ::mingling::SuggestItem::new(#key.to_string()) + }); + } + } + } + + let expanded = if items.is_empty() { + quote! { + ::mingling::Suggest::new() + } + } else { + quote! {{ + let mut suggest = ::mingling::Suggest::new(); + #(suggest.insert(#items);)* + suggest + }} + }; + + 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() +} -- cgit