diff options
Diffstat (limited to 'mingling_macros/src/attr/renderer.rs')
| -rw-r--r-- | mingling_macros/src/attr/renderer.rs | 274 |
1 files changed, 274 insertions, 0 deletions
diff --git a/mingling_macros/src/attr/renderer.rs b/mingling_macros/src/attr/renderer.rs new file mode 100644 index 0000000..828dc00 --- /dev/null +++ b/mingling_macros/src/attr/renderer.rs @@ -0,0 +1,274 @@ +use proc_macro::TokenStream; +use quote::{ToTokens, quote}; +use syn::spanned::Spanned; +use syn::{ItemFn, Pat, ReturnType, Signature, TypePath, parse_macro_input}; + +use crate::get_global_set; +use crate::res_injection::{extract_args_info, generate_immut_resource_bindings}; + +/// Extracts the user's return type, returning `None` for no return type. +fn extract_user_return_type(sig: &Signature) -> Option<proc_macro2::TokenStream> { + match &sig.output { + ReturnType::Type(_, ty) => Some(quote! { #ty }), + ReturnType::Default => None, + } +} + +#[allow(clippy::too_many_lines)] +pub(crate) fn renderer_attr(attr: TokenStream, item: TokenStream) -> TokenStream { + // #[renderer] takes no arguments; always use the default program path + let _ = attr; + let program_path = crate::default_program_path(); + let program_type = &program_path; + + // Parse the function item + let input_fn = parse_macro_input!(item as ItemFn); + + // Validate the function is not async + if input_fn.sig.asyncness.is_some() { + return syn::Error::new(input_fn.sig.span(), "Renderer function cannot be async") + .to_compile_error() + .into(); + } + + // Extract the previous type and resource injection params + let (_, previous_type, resources) = match extract_args_info(&input_fn.sig) { + Ok(info) => info, + Err(e) => return e.to_compile_error().into(), + }; + + // Determine the user's return type and whether it needs to be converted to RenderResult + let user_return_type = extract_user_return_type(&input_fn.sig); + + // Get function body statements + let fn_body_stmts: Vec<syn::Stmt> = input_fn.block.stmts.clone(); + + // Get function attributes (excluding the renderer attribute) + let mut fn_attrs = input_fn.attrs.clone(); + + // Remove any #[renderer(...)] attributes to avoid infinite recursion + fn_attrs.retain(|attr| !attr.path().is_ident("renderer")); + + // Get function visibility + let vis = &input_fn.vis; + + // Get function name + let fn_name = &input_fn.sig.ident; + + // Generate struct name from function name using pascal_case + let internal_name = format!( + "__internal_renderer_{}", + just_fmt::snake_case!(fn_name.to_string()) + ); + let struct_name = syn::Ident::new(&internal_name, fn_name.span()); + + let has_resources = !resources.is_empty(); + let has_mut_resources = resources.iter().any(|r| r.is_mut); + + // Generate resource bindings for immutable resources + let immut_resource_stmts = generate_immut_resource_bindings(resources.iter(), program_type); + let mut_resources: Vec<_> = resources.iter().filter(|r| r.is_mut).collect(); + + // Build the call to the original function with resource arguments injected + let resource_args: Vec<_> = resources + .iter() + .map(|res| { + let var_name = &res.var_name; + quote! { #var_name } + }) + .collect(); + + // Use a fixed parameter name `prev` for the trait method, regardless of + // the user's original parameter name (which may be `_` and cannot be + // referenced in expression position). + let fixed_prev: Pat = syn::parse_quote!(prev); + + let fn_call = if has_resources { + quote! { #fn_name(#fixed_prev, #(#resource_args),*) } + } else { + quote! { #fn_name(#fixed_prev) } + }; + + // Wrap the function call with modify_res for mutable resources + let inner_call = if has_mut_resources { + let mut wrapped = fn_call; + for res in mut_resources.iter().rev() { + let var_name = &res.var_name; + let inner_type = &res.inner_type; + wrapped = quote! { + ::mingling::this::<#program_type>() + .modify_res(|#var_name: &mut #inner_type| { + #wrapped + }) + }; + } + wrapped + } else { + fn_call + }; + + // Build the Renderer::render body with resource injection. + // The trait method injects resources and calls the original function. + let render_fn_body = if has_resources { + quote! { + #(#immut_resource_stmts)* + #inner_call + } + } else { + quote! { #inner_call } + }; + + // The original function preserves the user's exact signature and body. + // Resource parameters are passed directly by the caller, NOT injected from context. + let original_inputs = input_fn.sig.inputs.clone(); + let original_return_type = user_return_type.clone().unwrap_or(quote! { () }); + + let expanded = quote! { + #(#fn_attrs)* + #[doc(hidden)] + #[allow(non_camel_case_types)] + #vis struct #struct_name; + + ::mingling::macros::register_renderer!(#previous_type, #struct_name); + + impl ::mingling::Renderer for #struct_name { + type Previous = #previous_type; + + fn render(#fixed_prev: Self::Previous) -> ::mingling::RenderResult { + let __renderer_result = { #render_fn_body }; + ::std::convert::Into::into(__renderer_result) + } + } + + // Keep the original function unchanged + #(#fn_attrs)* + #vis fn #fn_name(#original_inputs) -> #original_return_type { + #(#fn_body_stmts)* + } + }; + + expanded.into() +} + +/// Builds the renderer entry for the global renderers list +pub(crate) fn build_renderer_entry( + struct_name: &syn::Ident, + previous_type: &TypePath, +) -> proc_macro2::TokenStream { + let enum_variant = &previous_type.path.segments.last().unwrap().ident; + quote! { + #struct_name => #enum_variant, + } +} + +/// Builds the renderer existence check entry +pub(crate) fn build_renderer_exist_entry(previous_type: &TypePath) -> proc_macro2::TokenStream { + let enum_variant = &previous_type.path.segments.last().unwrap().ident; + quote! { + Self::#enum_variant => true, + } +} + +/// Builds the structural renderer entry +#[cfg(feature = "structural_renderer")] +pub(crate) fn build_structural_renderer_entry( + previous_type: &TypePath, +) -> proc_macro2::TokenStream { + let enum_variant = &previous_type.path.segments.last().unwrap().ident; + quote! { + Self::#enum_variant => { + // SAFETY: Only types that match will enter this branch for forced conversion, + // and `AnyOutput::new` ensures the type implements serde::Serialize + let raw = unsafe { any.restore::<#previous_type>().unwrap_unchecked() }; + let mut __renderer_inner_result = ::mingling::RenderResult::default(); + ::mingling::StructuralRenderer::render(&raw, setting, &mut __renderer_inner_result)?; + Ok(__renderer_inner_result) + } + } +} + +pub(crate) fn register_renderer(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 that there are exactly two elements + if input_parsed.len() != 2 { + return syn::Error::new( + input_parsed.span(), + "Expected exactly two comma-separated arguments: `PreviousType, StructName`", + ) + .to_compile_error() + .into(); + } + + // Extract the two elements + let previous_type_expr = &input_parsed[0]; + let struct_name_expr = &input_parsed[1]; + + // Convert expressions to TypePath and Ident + let previous_type = match syn::parse2::<TypePath>(previous_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 renderer in the global list + let renderer_entry = build_renderer_entry(&struct_name, &previous_type); + let renderer_exist_entry = build_renderer_exist_entry(&previous_type); + #[cfg(feature = "structural_renderer")] + let structural_renderer_entry = build_structural_renderer_entry(&previous_type); + + let renderer_entry_str = renderer_entry.to_string(); + let renderer_exist_entry_str = renderer_exist_entry.to_string(); + + #[cfg(feature = "structural_renderer")] + let structural_renderer_entry_str = structural_renderer_entry.to_string(); + + // Check for duplicate variant before acquiring other locks + let variant_name = previous_type + .path + .segments + .last() + .unwrap() + .ident + .to_string(); + { + let renderers = get_global_set(&crate::RENDERERS).lock().unwrap(); + if let Err(err) = crate::check_duplicate_variant( + &renderers, + &renderer_entry_str, + &variant_name, + "renderer", + previous_type.span(), + ) { + return err.into(); + } + } // renderers lock released here + + let mut renderers = get_global_set(&crate::RENDERERS).lock().unwrap(); + let mut renderer_exist = get_global_set(&crate::RENDERERS_EXIST).lock().unwrap(); + + #[cfg(feature = "structural_renderer")] + let mut structural_renderers = get_global_set(&crate::STRUCTURAL_RENDERERS).lock().unwrap(); + + renderers.insert(renderer_entry_str); + renderer_exist.insert(renderer_exist_entry_str); + + // Only register structural renderer if the type is in STRUCTURED_TYPES + #[cfg(feature = "structural_renderer")] + { + let is_structured = get_global_set(&crate::STRUCTURED_TYPES) + .lock() + .unwrap() + .contains(&variant_name); + if is_structured { + structural_renderers.insert(structural_renderer_entry_str); + } + } + + quote! {}.into() +} |
