aboutsummaryrefslogtreecommitdiff
path: root/mingling_macros/src/systems
diff options
context:
space:
mode:
Diffstat (limited to 'mingling_macros/src/systems')
-rw-r--r--mingling_macros/src/systems/dispatch_tree_gen.rs181
-rw-r--r--mingling_macros/src/systems/res_injection.rs261
-rw-r--r--mingling_macros/src/systems/structural_data.rs336
3 files changed, 778 insertions, 0 deletions
diff --git a/mingling_macros/src/systems/dispatch_tree_gen.rs b/mingling_macros/src/systems/dispatch_tree_gen.rs
new file mode 100644
index 0000000..7383421
--- /dev/null
+++ b/mingling_macros/src/systems/dispatch_tree_gen.rs
@@ -0,0 +1,181 @@
+use std::collections::{BTreeMap, HashMap};
+
+use just_fmt::snake_case;
+use proc_macro2::TokenStream;
+use quote::quote;
+
+use crate::func::gen_program::resolve_type;
+
+/// Generate the `get_nodes()` function body for a ProgramCollect impl.
+/// If `pathf_map` is non-empty, resolves internal dispatcher statics using full paths.
+pub(crate) fn gen_get_nodes(
+ entries: &[(String, String, String)],
+ pathf_map: &HashMap<String, String>,
+) -> TokenStream {
+ let mut node_entries = Vec::new();
+
+ for (node_name, _disp_type, _entry_name) in entries {
+ let static_name_str = format!("__internal_dispatcher_{}", snake_case!(node_name));
+ let resolved = resolve_type(&static_name_str, pathf_map);
+ let node_display_name = node_name.replace('.', " ");
+ let node_display_lit = syn::LitStr::new(&node_display_name, proc_macro2::Span::call_site());
+
+ node_entries.push(quote! {
+ (#node_display_lit.to_string(), & #resolved)
+ });
+ }
+
+ quote! {
+ fn get_nodes() -> Vec<(String, &'static (dyn ::mingling::Dispatcher<Self::Enum> + Send + Sync))> {
+ vec![
+ #(#node_entries),*
+ ]
+ }
+ }
+}
+
+/// Generate the `dispatch_args_trie()` function body for a ProgramCollect impl.
+///
+/// Builds a hardcoded match tree: at each depth, group nodes by character.
+/// Single-node groups use `starts_with`; multi-node groups recurse with `nth()` match.
+///
+/// If `pathf_map` is non-empty, resolves dispatcher types using full paths.
+pub(crate) fn gen_dispatch_args_trie(
+ entries: &[(String, String, String)],
+ pathf_map: &HashMap<String, String>,
+) -> TokenStream {
+ // Prepare (display_name, disp_type) pairs.
+ // display_name = node_name.replace('.', " ")
+ let nodes: Vec<(String, String)> = entries
+ .iter()
+ .map(|(name, disp, _)| (name.replace('.', " "), disp.clone()))
+ .collect();
+
+ let dispatch_body = build_dispatch_body(&nodes, 0, pathf_map);
+
+ quote! {
+ fn dispatch_args_trie(
+ raw: &[String],
+ ) -> Result<::mingling::AnyOutput<Self::Enum>, ::mingling::error::ProgramInternalExecuteError>
+ {
+ let raw_string = format!("{} ", raw.join(" "));
+ let raw_str = raw_string.as_str();
+ let mut raw_chars = raw_str.chars();
+ #dispatch_body
+ }
+ }
+}
+
+/// Recursively build the trie match body.
+///
+/// `nodes`: slice of (display_name, disp_type) for commands that share the same prefix so far.
+/// `depth`: The character index currently being matched.
+/// `pathf_map`: optional mapping from type name to full path for resolving dispatchers.
+fn build_dispatch_body(
+ nodes: &[(String, String)],
+ depth: usize,
+ pathf_map: &HashMap<String, String>,
+) -> TokenStream {
+ if nodes.is_empty() {
+ return quote! {
+ return Ok(Self::build_dispatcher_not_found(raw.to_vec()));
+ };
+ }
+
+ // Group by character at `depth`
+ let mut groups: BTreeMap<char, Vec<(String, String)>> = BTreeMap::new();
+ let mut exact_nodes: Vec<(String, String)> = Vec::new();
+
+ for (name, disp_type) in nodes {
+ if let Some(ch) = name.chars().nth(depth) {
+ groups
+ .entry(ch)
+ .or_default()
+ .push((name.clone(), disp_type.clone()));
+ } else {
+ exact_nodes.push((name.clone(), disp_type.clone()));
+ }
+ }
+
+ // Build a dispatch arm for a single node via `starts_with`
+ let make_starts_with_arm = |name: &str, disp_type: &str| -> TokenStream {
+ let name_space = format!("{} ", name);
+ let name_lit = syn::LitStr::new(&name_space, proc_macro2::Span::call_site());
+ let disp_resolved = resolve_type(disp_type, pathf_map);
+ let prefix_word_count = name.split_whitespace().count();
+ quote! {
+ if raw_str.starts_with(#name_lit) {
+ let prefix_len = #prefix_word_count;
+ let trimmed_args: Vec<String> = raw.iter().skip(prefix_len).cloned().collect();
+ let __cp = <#disp_resolved as ::mingling::Dispatcher<Self::Enum>>::begin(
+ &#disp_resolved::default(),
+ trimmed_args,
+ );
+ return match __cp {
+ ::mingling::ChainProcess::Ok(any_output) => Ok(any_output.0),
+ ::mingling::ChainProcess::Err(chain_process_error) => {
+ Err(chain_process_error.into())
+ }
+ };
+ }
+ }
+ };
+
+ let mut arms = Vec::new();
+
+ for (&ch, sub_nodes) in &groups {
+ let ch_char = ch;
+
+ if sub_nodes.len() == 1 {
+ let (name, disp_type) = &sub_nodes[0];
+ let arm = make_starts_with_arm(name, disp_type);
+ arms.push(quote! {
+ Some(#ch_char) => {
+ #arm
+ return Ok(Self::build_dispatcher_not_found(raw.to_vec()));
+ }
+ });
+ } else {
+ let sub_body = build_dispatch_body(sub_nodes, depth + 1, pathf_map);
+ arms.push(quote! {
+ Some(#ch_char) => {
+ #sub_body
+ }
+ });
+ }
+ }
+
+ let exact_checks: Vec<TokenStream> = exact_nodes
+ .iter()
+ .map(|(name, disp_type)| make_starts_with_arm(name, disp_type))
+ .collect();
+
+ if !exact_checks.is_empty() && !groups.is_empty() {
+ let match_body = quote! {
+ match raw_chars.nth(0) {
+ #(#arms)*
+ _ => return Ok(Self::build_dispatcher_not_found(raw.to_vec())),
+ }
+ };
+ quote! {
+ #(#exact_checks)*
+ #match_body
+ }
+ } else if !exact_checks.is_empty() {
+ quote! {
+ #(#exact_checks)*
+ return Ok(Self::build_dispatcher_not_found(raw.to_vec()));
+ }
+ } else if arms.is_empty() {
+ quote! {
+ return Ok(Self::build_dispatcher_not_found(raw.to_vec()));
+ }
+ } else {
+ quote! {
+ match raw_chars.nth(0) {
+ #(#arms)*
+ _ => return Ok(Self::build_dispatcher_not_found(raw.to_vec())),
+ }
+ }
+ }
+}
diff --git a/mingling_macros/src/systems/res_injection.rs b/mingling_macros/src/systems/res_injection.rs
new file mode 100644
index 0000000..606b9a6
--- /dev/null
+++ b/mingling_macros/src/systems/res_injection.rs
@@ -0,0 +1,261 @@
+use quote::quote;
+use syn::spanned::Spanned;
+use syn::{FnArg, Ident, Pat, PatType, Signature, Type, TypePath};
+
+/// Extracted information about a resource injection parameter
+pub(crate) struct ResourceInjection {
+ pub(crate) var_name: Ident,
+ pub(crate) full_type: Type,
+ pub(crate) inner_type: TypePath,
+ pub(crate) is_ref: bool,
+ pub(crate) is_mut: bool,
+}
+
+/// Extracts the previous type and parameter name from function arguments,
+/// and collects resource injection parameters from the 2nd argument onward.
+#[allow(clippy::too_many_lines)]
+pub(crate) fn extract_args_info(
+ sig: &Signature,
+) -> syn::Result<(Pat, TypePath, Vec<ResourceInjection>)> {
+ if sig.inputs.is_empty() {
+ return Err(syn::Error::new(
+ sig.span(),
+ "Function must have at least one parameter",
+ ));
+ }
+
+ // First parameter: required, the previous type (must be owned, not a reference)
+ let first_arg = &sig.inputs[0];
+ let (prev_param, previous_type) = match first_arg {
+ FnArg::Typed(PatType { pat, ty, .. }) => {
+ let param_pat = (**pat).clone();
+ match &**ty {
+ Type::Path(type_path) => (param_pat, type_path.clone()),
+ Type::Reference(_) => {
+ return Err(syn::Error::new(
+ ty.span(),
+ "The first parameter (previous type) must be taken by move, \
+ not by reference. \
+ Use `prev: SomeEntry` instead of `prev: &SomeEntry`.",
+ ));
+ }
+ _ => {
+ return Err(syn::Error::new(
+ ty.span(),
+ "First parameter type must be a type path",
+ ));
+ }
+ }
+ }
+ FnArg::Receiver(_) => {
+ return Err(syn::Error::new(
+ first_arg.span(),
+ "Function cannot have self parameter",
+ ));
+ }
+ };
+
+ // 2nd to Nth parameters: optional, for resource injection
+ let mut resources = Vec::new();
+ for arg in sig.inputs.iter().skip(1) {
+ match arg {
+ FnArg::Typed(PatType { pat, ty, .. }) => {
+ // Extract the variable name – must be a simple identifier
+ let var_name = match &**pat {
+ Pat::Ident(pat_ident) => pat_ident.ident.clone(),
+ _ => {
+ return Err(syn::Error::new(
+ pat.span(),
+ "Resource injection parameter must be a simple identifier (e.g., `age: &Age`)",
+ ));
+ }
+ };
+
+ let full_type = *(*ty).clone();
+
+ // Try to extract inner type for reference patterns like `&Age` -> `Age`
+ // and `&mut Age` -> `Age`
+ let (inner_type, is_ref, is_mut) = match &full_type {
+ Type::Reference(ref_type) => match &*ref_type.elem {
+ Type::Path(type_path) => {
+ let is_mut = ref_type.mutability.is_some();
+ (type_path.clone(), true, is_mut)
+ }
+ _ => {
+ return Err(syn::Error::new(
+ ty.span(),
+ "Reference resource type must be a type path (e.g., `age: &Age`)",
+ ));
+ }
+ },
+ Type::Path(_) => {
+ return Err(syn::Error::new(
+ ty.span(),
+ "Resource injection parameter must be a reference (`&T` or `&mut T`), \
+ not an owned value. Use `age: &Age` instead of `age: Age`.",
+ ));
+ }
+ _ => {
+ return Err(syn::Error::new(
+ ty.span(),
+ "Resource injection type must be a type path or reference to one \
+ (e.g., `age: Age` or `age: &Age`)",
+ ));
+ }
+ };
+
+ resources.push(ResourceInjection {
+ var_name,
+ full_type,
+ inner_type,
+ is_ref,
+ is_mut,
+ });
+ }
+ FnArg::Receiver(_) => {
+ return Err(syn::Error::new(
+ arg.span(),
+ "Resource injection parameter cannot be self",
+ ));
+ }
+ }
+ }
+
+ Ok((prev_param, previous_type, resources))
+}
+
+/// Generates `let` binding statements for immutable resource injection parameters.
+///
+/// Each immutable reference parameter gets a `_binding` variable that holds the
+/// `res_or_default` result, then a shadowing `let` that borrows from it via `.as_ref()`.
+pub(crate) fn generate_immut_resource_bindings<'a>(
+ resources: impl Iterator<Item = &'a ResourceInjection>,
+ program_type: &proc_macro2::TokenStream,
+) -> Vec<proc_macro2::TokenStream> {
+ resources
+ .filter(|r| !r.is_mut)
+ .map(|res| {
+ let var_binding_name = syn::Ident::new(
+ &format!("__{}_binding", &res.var_name.to_string()),
+ res.var_name.span(),
+ );
+ let var_name = &res.var_name;
+ let full_type = &res.full_type;
+ let inner_type = &res.inner_type;
+ if res.is_ref {
+ quote! {
+ let #var_binding_name = ::mingling::this::<#program_type>()
+ .res_or_default::<#inner_type>();
+ let #var_name: #full_type = #var_binding_name.as_ref();
+ }
+ } else {
+ quote! {
+ let #var_name: #full_type = ::mingling::this::<#program_type>()
+ .res_or_default::<#full_type>();
+ }
+ }
+ })
+ .collect()
+}
+
+/// Generates a unique binding name for a mutable resource variable.
+fn mut_res_binding_name(var_name: &Ident) -> Ident {
+ syn::Ident::new(&format!("__{}_binding", var_name), var_name.span())
+}
+
+/// Wraps the function body in mutable resource closures (sync version).
+///
+/// For unit return types: uses `modify_res` (closure returns `()`).
+/// For non-unit return types: uses `__modify_res_and_return_route` (closure returns `ChainProcess<C>`).
+///
+/// The innermost closure gets the original body, and each mutable parameter wraps
+/// outward from last to first.
+pub(crate) fn wrap_body_with_mut_resources(
+ fn_body_stmts: &[syn::Stmt],
+ mut_resources: &[&ResourceInjection],
+ program_type: &proc_macro2::TokenStream,
+ is_unit_return: bool,
+) -> proc_macro2::TokenStream {
+ let mut wrapped = quote! {
+ #(#fn_body_stmts)*
+ };
+
+ for res in mut_resources {
+ let var_name = &res.var_name;
+ let inner_type = &res.inner_type;
+ if is_unit_return {
+ wrapped = quote! {
+ ::mingling::this::<#program_type>().modify_res(|#var_name: &mut #inner_type| {
+ #wrapped
+ })
+ };
+ } else {
+ wrapped = quote! {
+ ::mingling::this::<#program_type>().__modify_res_and_return_route(|#var_name: &mut #inner_type| {
+ #wrapped
+ })
+ };
+ }
+ }
+
+ wrapped
+}
+
+/// Generates code for mutable resource injection in async `#[chain]` functions.
+///
+/// Instead of wrapping in a closure (which causes lifetime issues with async),
+/// this generates a three-part pattern:
+///
+/// 1. **Extract** each mutable resource from the global store into an owned local.
+/// 2. **Body block** — the user's body with `&mut` references scoped to a block.
+/// 3. **Store back** each modified resource after the body block completes.
+///
+/// This avoids holding a `&mut` reference across `.await` points — the borrow
+/// ends when the body block closes.
+pub(crate) fn wrap_body_with_mut_resources_async(
+ fn_body_stmts: &[syn::Stmt],
+ mut_resources: &[&ResourceInjection],
+ program_type: &proc_macro2::TokenStream,
+) -> proc_macro2::TokenStream {
+ // 1. Generate extract statements and body-block `let` bindings
+ let mut extract_stmts = Vec::new();
+ let mut body_lets = Vec::new();
+
+ for res in mut_resources {
+ let var_name = &res.var_name;
+ let inner_type = &res.inner_type;
+ let binding_name = mut_res_binding_name(var_name);
+
+ extract_stmts.push(quote! {
+ let mut #binding_name = ::mingling::this::<#program_type>()
+ .__extract_res_mut::<#inner_type>();
+ });
+
+ body_lets.push(quote! {
+ let #var_name: &mut #inner_type = &mut #binding_name;
+ });
+ }
+
+ // 2. Store-back statements (reverse order is fine, resources are independent)
+ let mut store_stmts = Vec::new();
+ for res in mut_resources {
+ let var_name = &res.var_name;
+ let inner_type = &res.inner_type;
+ let binding_name = mut_res_binding_name(var_name);
+
+ store_stmts.push(quote! {
+ ::mingling::this::<#program_type>()
+ .__store_res::<#inner_type>(#binding_name);
+ });
+ }
+
+ quote! {
+ #(#extract_stmts)*
+ let __chain_result = {
+ #(#body_lets)*
+ #(#fn_body_stmts)*
+ };
+ #(#store_stmts)*
+ __chain_result
+ }
+}
diff --git a/mingling_macros/src/systems/structural_data.rs b/mingling_macros/src/systems/structural_data.rs
new file mode 100644
index 0000000..0a783ec
--- /dev/null
+++ b/mingling_macros/src/systems/structural_data.rs
@@ -0,0 +1,336 @@
+#![allow(dead_code)]
+
+use proc_macro::TokenStream;
+use quote::quote;
+use syn::{DeriveInput, Ident, TypePath, parse_macro_input};
+
+use crate::get_global_set;
+
+/// Derive macro for `StructuralData`.
+///
+/// This marks a type as eligible for structured output (JSON / YAML / TOML / RON).
+/// The type must also implement `serde::Serialize` — the generated `impl StructuralData`
+/// will fail to compile if `Serialize` is not in scope or implemented.
+///
+/// Also registers the type name in the global `STRUCTURED_TYPES` registry so that
+/// the `structural_render` match arm is generated by `gen_program!()`.
+pub(crate) fn derive_structural_data(input: TokenStream) -> TokenStream {
+ let input = parse_macro_input!(input as DeriveInput);
+ let type_name = input.ident;
+
+ // Register in STRUCTURED_TYPES
+ let type_name_str = type_name.to_string();
+ get_global_set(&crate::STRUCTURED_TYPES)
+ .lock()
+ .unwrap()
+ .insert(type_name_str);
+
+ // Generate BOTH the sealed impl AND the StructuralData impl.
+ // Users cannot implement StructuralDataSealed manually (it's #[doc(hidden)]),
+ // so the only way to get StructuralData is through this derive macro.
+ let expanded = quote! {
+ impl ::mingling::__private::StructuralDataSealed<crate::ThisProgram> for #type_name {}
+ impl ::mingling::__private::StructuralData<crate::ThisProgram> for #type_name {}
+ };
+
+ expanded.into()
+}
+
+/// `pack_structural!` — like `pack!` but also marks the type as supporting
+/// structured output via `StructuralData`.
+///
+/// # Syntax
+///
+/// ```rust,ignore
+/// pack_structural!(Info = (String, i32));
+/// ```
+///
+/// This is equivalent to:
+/// ```rust,ignore
+/// pack!(Info = (String, i32));
+/// impl ::mingling::StructuralData for Info {}
+/// ```
+pub(crate) fn pack_structural(input: TokenStream) -> TokenStream {
+ // Parse same input format as `pack!`
+ let input_parsed = syn::parse_macro_input!(input as PackStructuralInput);
+ let type_name = input_parsed.type_name;
+ let inner_type = input_parsed.inner_type;
+ let attrs = input_parsed.attrs;
+ let program_path = crate::default_program_path();
+
+ // Register in STRUCTURED_TYPES
+ let type_name_str = type_name.to_string();
+ get_global_set(&crate::STRUCTURED_TYPES)
+ .lock()
+ .unwrap()
+ .insert(type_name_str);
+
+ // Struct definition (with Serialize derive, same as pack! under structural_renderer)
+ #[cfg(not(feature = "structural_renderer"))]
+ let struct_def = quote! {
+ #(#attrs)*
+ pub struct #type_name {
+ pub inner: #inner_type,
+ }
+ };
+
+ #[cfg(feature = "structural_renderer")]
+ let struct_def = quote! {
+ #(#attrs)*
+ #[derive(serde::Serialize)]
+ pub struct #type_name {
+ pub inner: #inner_type,
+ }
+ };
+
+ // Helper impls (same as pack!)
+ let new_impl = quote! {
+ impl #type_name {
+ pub fn new(inner: #inner_type) -> Self {
+ Self { inner }
+ }
+ }
+ };
+
+ let from_into_impl = quote! {
+ impl From<#inner_type> for #type_name {
+ fn from(inner: #inner_type) -> Self {
+ Self::new(inner)
+ }
+ }
+ impl From<#type_name> for #inner_type {
+ fn from(wrapper: #type_name) -> #inner_type {
+ wrapper.inner
+ }
+ }
+ };
+
+ let as_ref_impl = quote! {
+ impl ::std::convert::AsRef<#inner_type> for #type_name {
+ fn as_ref(&self) -> &#inner_type {
+ &self.inner
+ }
+ }
+ impl ::std::convert::AsMut<#inner_type> for #type_name {
+ fn as_mut(&mut self) -> &mut #inner_type {
+ &mut self.inner
+ }
+ }
+ };
+
+ let deref_impl = quote! {
+ impl ::std::ops::Deref for #type_name {
+ type Target = #inner_type;
+ fn deref(&self) -> &Self::Target {
+ &self.inner
+ }
+ }
+ impl ::std::ops::DerefMut for #type_name {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.inner
+ }
+ }
+ };
+
+ let default_impl = quote! {
+ impl ::std::default::Default for #type_name
+ where
+ #inner_type: ::std::default::Default,
+ {
+ fn default() -> Self {
+ Self::new(::std::default::Default::default())
+ }
+ }
+ };
+
+ let register_impl = quote! {
+ ::mingling::macros::register_type!(#type_name);
+ };
+
+ // StructuralData impl + sealed + registration
+ let structural_impl = quote! {
+ impl ::mingling::__private::StructuralDataSealed<crate::ThisProgram> for #type_name {}
+ impl ::mingling::__private::StructuralData<crate::ThisProgram> for #type_name {}
+ };
+
+ let expanded = quote! {
+ #struct_def
+
+ #new_impl
+ #from_into_impl
+ #as_ref_impl
+ #deref_impl
+ #default_impl
+ #register_impl
+ #structural_impl
+
+ impl Into<::mingling::AnyOutput<#program_path>> for #type_name {
+ fn into(self) -> ::mingling::AnyOutput<#program_path> {
+ ::mingling::AnyOutput::new(self)
+ }
+ }
+
+ impl Into<::mingling::ChainProcess<#program_path>> for #type_name {
+ fn into(self) -> ::mingling::ChainProcess<#program_path> {
+ ::mingling::AnyOutput::new(self).route_chain()
+ }
+ }
+
+ impl ::mingling::Grouped<#program_path> for #type_name {
+ fn member_id() -> #program_path {
+ #program_path::#type_name
+ }
+ }
+ };
+
+ expanded.into()
+}
+
+/// Input for `pack_structural!` — same format as `pack!`.
+struct PackStructuralInput {
+ attrs: Vec<syn::Attribute>,
+ type_name: Ident,
+ inner_type: syn::Type,
+}
+
+impl syn::parse::Parse for PackStructuralInput {
+ fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
+ let attrs = input.call(syn::Attribute::parse_outer)?;
+ let type_name: Ident = input.parse()?;
+ input.parse::<syn::Token![=]>()?;
+ let inner_type: syn::Type = input.parse()?;
+ Ok(PackStructuralInput {
+ attrs,
+ type_name,
+ inner_type,
+ })
+ }
+}
+
+/// `group_structural!` — like `group!` but also marks the type as supporting
+/// structured output via `StructuralData`.
+///
+/// # Syntax
+///
+/// ```rust,ignore
+/// group_structural!(Info = (String, i32));
+/// ```
+///
+/// This is equivalent to:
+/// ```rust,ignore
+/// group!(Info = (String, i32));
+/// impl ::mingling::StructuralData for Info {}
+/// ```
+pub(crate) fn group_structural(input: TokenStream) -> TokenStream {
+ // Parse the same input as group!
+ let input_parsed = syn::parse_macro_input!(input as GroupStructuralInput);
+
+ let is_aliased = matches!(&input_parsed, GroupStructuralInput::Aliased { .. });
+
+ let (type_path, type_name, alias_stmt) = match &input_parsed {
+ GroupStructuralInput::Plain(type_path) => {
+ let name = type_path
+ .path
+ .segments
+ .last()
+ .expect("TypePath must have at least one segment")
+ .ident
+ .clone();
+ (type_path.clone(), name, quote! {})
+ }
+ GroupStructuralInput::Aliased { alias, type_path } => {
+ let alias_stmt = quote! {
+ pub(crate) type #alias = #type_path;
+ };
+ (type_path.clone(), alias.clone(), alias_stmt)
+ }
+ };
+
+ let type_name_str = type_name.to_string();
+
+ // Register in STRUCTURED_TYPES
+ get_global_set(&crate::STRUCTURED_TYPES)
+ .lock()
+ .unwrap()
+ .insert(type_name_str);
+
+ let program_path = crate::default_program_path();
+
+ // Generate unique module name
+ let segments: Vec<String> = type_path
+ .path
+ .segments
+ .iter()
+ .map(|seg| seg.ident.to_string().to_lowercase())
+ .collect();
+ let module_name = Ident::new(
+ &format!("internal_group_{}", segments.join("_")),
+ proc_macro2::Span::call_site(),
+ );
+
+ // Generate the appropriate `use` statement for the original type
+ // (consistent with gen_type_use in group_impl.rs)
+ let type_use = if type_path.path.segments.len() > 1 {
+ quote! { #[allow(unused_imports)] use #type_path; }
+ } else {
+ let ident = type_path
+ .path
+ .segments
+ .last()
+ .expect("TypePath must have at least one segment")
+ .ident
+ .clone();
+ quote! { #[allow(unused_imports)] use super::#ident; }
+ };
+
+ let alias_use = if is_aliased {
+ quote! { use super::#type_name; }
+ } else {
+ quote! {}
+ };
+
+ let expanded = quote! {
+ #alias_stmt
+ #[allow(non_camel_case_types)]
+ mod #module_name {
+ use #program_path as __MinglingProgram;
+ #type_use
+ #alias_use
+
+ 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))
+ }
+ }
+}