diff options
Diffstat (limited to 'mingling_macros/src/systems')
| -rw-r--r-- | mingling_macros/src/systems/dispatch_tree_gen.rs | 181 | ||||
| -rw-r--r-- | mingling_macros/src/systems/res_injection.rs | 261 | ||||
| -rw-r--r-- | mingling_macros/src/systems/structural_data.rs | 336 |
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)) + } + } +} |
