aboutsummaryrefslogtreecommitdiff
path: root/mingling_macros/src/func
diff options
context:
space:
mode:
Diffstat (limited to 'mingling_macros/src/func')
-rw-r--r--mingling_macros/src/func/dispatcher.rs186
-rw-r--r--mingling_macros/src/func/empty_result.rs11
-rw-r--r--mingling_macros/src/func/entry.rs70
-rw-r--r--mingling_macros/src/func/gen_program.rs114
-rw-r--r--mingling_macros/src/func/group.rs151
-rw-r--r--mingling_macros/src/func/group_structural.rs124
-rw-r--r--mingling_macros/src/func/node.rs59
-rw-r--r--mingling_macros/src/func/pack.rs153
-rw-r--r--mingling_macros/src/func/pack_err.rs107
-rw-r--r--mingling_macros/src/func/pack_err_structural.rs119
-rw-r--r--mingling_macros/src/func/pack_structural.rs168
-rw-r--r--mingling_macros/src/func/program_comp_gen.rs80
-rw-r--r--mingling_macros/src/func/program_fallback_gen.rs23
-rw-r--r--mingling_macros/src/func/program_final_gen.rs373
-rw-r--r--mingling_macros/src/func/r_append.rs29
-rw-r--r--mingling_macros/src/func/r_eprint.rs7
-rw-r--r--mingling_macros/src/func/r_eprintln.rs7
-rw-r--r--mingling_macros/src/func/r_print.rs85
-rw-r--r--mingling_macros/src/func/r_println.rs7
-rw-r--r--mingling_macros/src/func/register_chain.rs7
-rw-r--r--mingling_macros/src/func/register_dispatcher.rs75
-rw-r--r--mingling_macros/src/func/register_help.rs71
-rw-r--r--mingling_macros/src/func/register_renderer.rs7
-rw-r--r--mingling_macros/src/func/register_type.rs17
-rw-r--r--mingling_macros/src/func/render_route.rs15
-rw-r--r--mingling_macros/src/func/route.rs16
-rw-r--r--mingling_macros/src/func/suggest.rs72
-rw-r--r--mingling_macros/src/func/suggest_enum.rs24
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()
+}