use proc_macro::TokenStream; use quote::{ToTokens, quote}; use syn::spanned::Spanned; use syn::{ItemFn, ReturnType, Signature, Type, TypePath, parse_macro_input}; use crate::get_global_set; use crate::res_injection::{extract_args_info, generate_immut_resource_bindings}; /// Extracts and returns the return type from the function signature (or None for `()` / no return type). fn extract_return_type(sig: &Signature) -> syn::Result> { match &sig.output { ReturnType::Type(_, ty) => { match &**ty { // `()` means no custom return type Type::Tuple(tuple) if tuple.elems.is_empty() => Ok(None), // Any other return type is allowed custom_ty => Ok(Some((*custom_ty).clone())), } } ReturnType::Default => Ok(None), } } pub fn renderer_attr(attr: TokenStream, item: TokenStream) -> TokenStream { // Parse attribute arguments for program path (e.g. #[renderer(my_crate::Program)]) let (program_path, _use_crate_prefix) = parse_renderer_attr_args(attr); 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, parameter name, and resource injection params let (prev_param, previous_type, resources) = match extract_args_info(&input_fn.sig) { Ok(info) => info, Err(e) => return e.to_compile_error().into(), }; // Check that the previous type is a single-segment type (no `::`) if let Some(err_tokens) = crate::check_single_segment_type(&previous_type, "#[renderer]") { return err_tokens.into(); } // Validate return type – now returns Some(type) if custom type, None if () let return_type = match extract_return_type(&input_fn.sig) { Ok(rt) => rt, Err(e) => return e.to_compile_error().into(), }; // Get function body statements let fn_body_stmts: Vec = 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(); // Determine public return type and the expression to return dummy_r let (public_return_type, result_return) = match &return_type { // User specified a custom return type (e.g. -> String) Some(custom_ty) => { let ret_ty = quote! { #custom_ty }; let expr = quote! { dummy_r.into() }; (ret_ty, expr) } // Return type is () — no custom return type specified None => { let ret_ty = quote! { () }; let expr = quote! { if !dummy_r.is_empty() { ::std::println!("{}", &*dummy_r); } }; (ret_ty, expr) } }; let inner_body_with_resources = if has_mut_resources { let mut wrapped = quote! { #(#fn_body_stmts)* }; 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 { quote! { #(#fn_body_stmts)* } }; // Build the Renderer::render body with resource injection // Renderer::render returns (), output goes through __renderer_inner_result parameter. // Resources are injected from the program context here. let render_fn_body = if has_resources { quote! { #(#immut_resource_stmts)* #inner_body_with_resources } } else { quote! { #inner_body_with_resources } }; // Build the original function body // The original function preserves the user's signature and return type. // Resource parameters are passed directly by the caller, NOT injected from context. let original_fn_body = { quote! { let mut dummy_r = ::mingling::RenderResult::default(); { let __renderer_inner_result = &mut dummy_r; #(#fn_body_stmts)* } #result_return } }; // Keep the original function signature unchanged (same params as user wrote) let original_inputs = input_fn.sig.inputs.clone(); 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(#prev_param: Self::Previous, __renderer_inner_result: &mut ::mingling::RenderResult) { #render_fn_body } } // Keep the original function for internal use (without r parameter) #(#fn_attrs)* #vis fn #fn_name(#original_inputs) -> #public_return_type { #original_fn_body } }; expanded.into() } fn parse_renderer_attr_args(attr: TokenStream) -> (proc_macro2::TokenStream, bool) { if attr.is_empty() { (crate::default_program_path(), true) } else { let path: syn::Path = syn::parse(attr).expect("Expected a path argument for #[renderer(path)]"); (quote! { #path }, false) } } /// Builds the renderer entry for the global renderers list pub fn build_renderer_entry( struct_name: &syn::Ident, previous_type: &TypePath, ) -> proc_macro2::TokenStream { quote! { #struct_name => #previous_type, } } /// Builds the renderer existence check entry pub fn build_renderer_exist_entry(previous_type: &TypePath) -> proc_macro2::TokenStream { quote! { Self::#previous_type => true, } } /// Builds the general renderer entry #[cfg(feature = "general_renderer")] pub fn build_general_renderer_entry(previous_type: &TypePath) -> proc_macro2::TokenStream { quote! { Self::#previous_type => { // 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::GeneralRenderer::render(&raw, setting, &mut __renderer_inner_result)?; Ok(__renderer_inner_result) } } } pub 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::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::(previous_type_expr.to_token_stream()) { Ok(ty) => ty, Err(e) => return e.to_compile_error().into(), }; let struct_name = match syn::parse2::(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 = "general_renderer")] let general_renderer_entry = build_general_renderer_entry(&previous_type); let mut renderers = get_global_set(&crate::RENDERERS).lock().unwrap(); let mut renderer_exist = get_global_set(&crate::RENDERERS_EXIST).lock().unwrap(); #[cfg(feature = "general_renderer")] let mut general_renderers = get_global_set(&crate::GENERAL_RENDERERS).lock().unwrap(); let renderer_entry_str = renderer_entry.to_string(); let renderer_exist_entry_str = renderer_exist_entry.to_string(); #[cfg(feature = "general_renderer")] let general_renderer_entry_str = general_renderer_entry.to_string(); renderers.insert(renderer_entry_str); renderer_exist.insert(renderer_exist_entry_str); #[cfg(feature = "general_renderer")] general_renderers.insert(general_renderer_entry_str); quote! {}.into() }