diff options
Diffstat (limited to 'mingling_macros/src/func')
28 files changed, 2177 insertions, 0 deletions
diff --git a/mingling_macros/src/func/dispatcher.rs b/mingling_macros/src/func/dispatcher.rs new file mode 100644 index 0000000..ef02618 --- /dev/null +++ b/mingling_macros/src/func/dispatcher.rs @@ -0,0 +1,186 @@ +use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; +use quote::quote; +use syn::parse::{Parse, ParseStream}; +use syn::{Attribute, Ident, LitStr, Token}; + +enum DispatcherChainInput { + Default { + cmd_attrs: Vec<Attribute>, + entry_attrs: Vec<Attribute>, + command_name: syn::LitStr, + command_struct: Ident, + pack: Ident, + }, + #[cfg(feature = "extra_macros")] + Auto { + cmd_attrs: Vec<Attribute>, + command_name: syn::LitStr, + }, +} + +impl Parse for DispatcherChainInput { + fn parse(input: ParseStream) -> syn::Result<Self> { + // 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::<Token![,]>()?; + let command_struct = input.parse()?; + input.parse::<Token![=>]>()?; + 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 = just_fmt::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<String>); + + impl From<#pack> for crate::Entry { + fn from(value: #pack) -> Self { + crate::Entry::new(value.inner) + } + } + + #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<String>) -> ::mingling::ChainProcess<#program_type> { + use ::mingling::Grouped; + ::mingling::Routable::to_chain(#pack::new(args)) + } + fn clone_dispatcher(&self) -> Box<dyn ::mingling::Dispatcher<#program_type>> { + 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<String> { + 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! {} +} 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/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<String> }, + Untyped { strings: Vec<String> }, +} + +impl syn::parse::Parse for EntryInput { + fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> { + // 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<Vec<String>> { + 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::<Vec<_>>(); + + 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..d50041e --- /dev/null +++ b/mingling_macros/src/func/gen_program.rs @@ -0,0 +1,114 @@ +use proc_macro::TokenStream; +use quote::quote; + +/// 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! { + ::mingling::macros::program_comp_gen!(); + }; + + #[cfg(not(feature = "comp"))] + let comp_gen = quote! {}; + + // When pathf is enabled, load the type_using.rs generated by the build script + // and emit its use statements so types from submodules are in scope. + #[cfg(feature = "pathf")] + let pathf_uses: Vec<proc_macro2::TokenStream> = { + let uses = load_pathf_uses(); + if uses.is_empty() { + // The file might not exist yet — emit a clear hint + let hint: proc_macro2::TokenStream = syn::parse_quote! { + compile_error!( + "pathf: `{}` not found or empty.\n\ + Make sure `build.rs` calls `mingling::build::analyze_and_build_type_mapping().unwrap();`\n\ + with features [\"build\", \"pathf\"] enabled." + ); + }; + vec![hint] + } else { + uses + } + }; + #[cfg(not(feature = "pathf"))] + let pathf_uses: Vec<proc_macro2::TokenStream> = Vec::new(); + + #[cfg(feature = "pathf")] + let super_use = quote! {}; + + #[cfg(not(feature = "pathf"))] + let super_use = quote! { + use super::*; + }; + + TokenStream::from(quote! { + pub use __this_program_impl::*; + + #[doc(hidden)] + pub mod __this_program_impl { + #super_use + #(#pathf_uses)* + + /// Alias for the current program type `ThisProgram` + pub type Next = ::mingling::ChainProcess<ThisProgram>; + + ::mingling::macros::pack!(Entry = Vec<String>); + + impl ::mingling::Routable<ThisProgram> for ::mingling::ChainProcess<ThisProgram> + { + fn to_chain(self) -> ::mingling::ChainProcess<ThisProgram> { + match self { + ::mingling::ChainProcess::Ok((any, _)) => { + ::mingling::ChainProcess::Ok((any, mingling::NextProcess::Chain)) + } + other => other, + } + } + + fn to_render(self) -> ::mingling::ChainProcess<ThisProgram> { + 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!(); + } + }) +} + +/// Loads `type_using.rs` generated by the pathf build script and returns each +/// `use ...;` line as a token stream, ready to be emitted in the generated output. +#[cfg(feature = "pathf")] +fn load_pathf_uses() -> Vec<proc_macro2::TokenStream> { + let out_dir = match std::env::var("OUT_DIR") { + Ok(d) => d, + Err(_) => return Vec::new(), + }; + let crate_name = match std::env::var("CARGO_PKG_NAME") { + Ok(n) => n, + Err(_) => return Vec::new(), + }; + let path = std::path::Path::new(&out_dir) + .join(&crate_name) + .join("type_using.rs"); + let content = match std::fs::read_to_string(&path) { + Ok(c) => c, + Err(_) => return Vec::new(), + }; + content + .lines() + .map(|line| line.trim().to_string()) + .filter(|line| !line.is_empty()) + .filter_map(|line| line.parse::<proc_macro2::TokenStream>().ok()) + .collect() +} diff --git a/mingling_macros/src/func/group.rs b/mingling_macros/src/func/group.rs new file mode 100644 index 0000000..edb1fe1 --- /dev/null +++ b/mingling_macros/src/func/group.rs @@ -0,0 +1,151 @@ +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<Self> { + // 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<String> = 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 + + /// SAFETY: This is an internal implementation of the `group!` macro, + /// guaranteeing that the enum value registered by the `register_type!` macro + /// is exactly the same as the actual return value, + /// which can be confirmed via the `Ident` in the `quote!` block. + unsafe impl ::mingling::Grouped<__MinglingProgram> for #type_name { + fn member_id() -> __MinglingProgram { + __MinglingProgram::#type_name + } + } + + ::mingling::macros::register_type!(#type_name); + } + }; + + expanded.into() +} 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/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<Self> { + 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<String> = 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..7206b8e --- /dev/null +++ b/mingling_macros/src/func/pack.rs @@ -0,0 +1,153 @@ +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<Attribute>, + type_name: Ident, + inner_type: Type, +} + +impl Parse for PackInput { + fn parse(input: ParseStream) -> SynResult<Self> { + let attrs = input.call(Attribute::parse_outer)?; + let type_name: Ident = input.parse()?; + input.parse::<Token![=]>()?; + 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<mingling::AnyOutput<#group_name>> for #type_name { + fn into(self) -> mingling::AnyOutput<#group_name> { + mingling::AnyOutput::new(self) + } + } + + impl Into<mingling::ChainProcess<#group_name>> for #type_name { + fn into(self) -> mingling::ChainProcess<#group_name> { + mingling::AnyOutput::new(self).route_chain() + } + } + + /// SAFETY: This is an internal implementation of the `pack!` macro, + /// guaranteeing that the enum value registered by the `register_type!` macro + /// is exactly the same as the actual return value, + /// which can be confirmed via the `Ident` in the `quote!` block. + unsafe impl ::mingling::Grouped<#group_name> for #type_name { + fn member_id() -> #group_name { + #group_name::#type_name + } + } + }; + + 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..8b224b0 --- /dev/null +++ b/mingling_macros/src/func/pack_err.rs @@ -0,0 +1,107 @@ +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<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 }) + } + } +} + +#[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() + } + } +} 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..2fbb0e0 --- /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(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(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..0eed1db --- /dev/null +++ b/mingling_macros/src/func/program_final_gen.rs @@ -0,0 +1,373 @@ +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) +} + +/// Helper: convert a string ident into a token stream for the generated code. +/// Types are expected to be in scope (e.g. via pathf glob re-exports), so bare +/// idents suffice. +fn ident_tokens(name: &str) -> proc_macro2::TokenStream { + 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(); + + #[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> { + match any.member_id() { + #(#structural_renderer_tokens)* + _ => { + 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); + let dispatch_trie_fn = dispatch_tree_gen::gen_dispatch_args_trie(&entries); + + 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 { + 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 = ident_tokens(&variant_ident.to_string()); + let resolved_struct = ident_tokens(&struct_ident.to_string()); + quote! { + Self::#variant_ident => { + 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 = ident_tokens(&variant_ident.to_string()); + let resolved_struct = ident_tokens(&struct_ident.to_string()); + quote! { + Self::#variant_ident => { + 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 = ident_tokens(&variant_ident.to_string()); + let resolved_struct = ident_tokens(&struct_ident.to_string()); + quote! { + Self::#variant_ident => { + 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! { + #[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 { + 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 new file mode 100644 index 0000000..20f15b8 --- /dev/null +++ b/mingling_macros/src/func/r_print.rs @@ -0,0 +1,85 @@ +use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; +use quote::quote; +use syn::parse::{Parse, ParseStream}; +use syn::{Ident, Token}; + +/// Parsed input for `r_println!` and `r_print!`. +/// +/// Two forms: +/// - `(ident, format_args...)` — explicit buffer +/// - `(format_args...)` — implicit `__render_result_buffer` +pub(crate) enum PrintInput { + Explicit { dst: Ident, args: TokenStream2 }, + Implicit { args: TokenStream2 }, +} + +impl Parse for PrintInput { + fn parse(input: ParseStream) -> syn::Result<Self> { + // Peek: if the next token is an ident followed by a comma, it's the explicit form + if input.peek(Ident) && input.peek2(Token![,]) { + let dst: Ident = input.parse()?; + let _comma: Token![,] = input.parse()?; + let args: TokenStream2 = input.parse()?; + Ok(PrintInput::Explicit { dst, args }) + } else { + let args: TokenStream2 = input.parse()?; + Ok(PrintInput::Implicit { args }) + } + } +} + +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(), + }; + + let method_ident = Ident::new(method, proc_macro2::Span::call_site()); + + let expanded = match parsed { + PrintInput::Explicit { dst, args } => { + quote! { + #dst.#method_ident(format!(#args)) + } + } + PrintInput::Implicit { args } => { + quote! { + __render_result_buffer.#method_ident(format!(#args)) + } + } + }; + + expanded.into() +} + +pub(crate) fn r_print(input: TokenStream) -> TokenStream { + expand_print(input, "print") +} + +/// Parsed input for `r_append!`. +/// +/// Two forms: +/// - `(dst, src)` — explicit buffer and source +/// - `(src)` — implicit `__render_result_buffer` +pub(crate) struct AppendInput { + pub(crate) dst: Option<Ident>, + pub(crate) src: proc_macro2::TokenStream, +} + +impl Parse for AppendInput { + fn parse(input: ParseStream) -> syn::Result<Self> { + if input.peek(Ident) && input.peek2(Token![,]) { + let dst: Ident = input.parse()?; + let _comma: Token![,] = input.parse()?; + let src: TokenStream2 = input.parse()?; + Ok(AppendInput { + dst: Some(dst), + src, + }) + } else { + let src: TokenStream2 = input.parse()?; + Ok(AppendInput { dst: None, src }) + } + } +} 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 new file mode 100644 index 0000000..85f1afe --- /dev/null +++ b/mingling_macros/src/func/suggest.rs @@ -0,0 +1,72 @@ +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<SuggestItem, Token![,]>, +} + +enum SuggestItem { + WithDesc(Box<(LitStr, Expr)>), // "-i" = "Insert something" + Simple(LitStr), // "-I" +} + +impl Parse for SuggestInput { + fn parse(input: ParseStream) -> syn::Result<Self> { + let items = Punctuated::parse_terminated(input)?; + Ok(SuggestInput { items }) + } +} + +impl Parse for SuggestItem { + fn parse(input: ParseStream) -> syn::Result<Self> { + 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() +} 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() +} |
