Skip to main content

cranpose_macros/
lib.rs

1use proc_macro::TokenStream;
2use proc_macro_crate::{FoundCrate, crate_name};
3use proc_macro2::{Span, TokenStream as TokenStream2};
4use quote::quote;
5use syn::{FnArg, Ident, ItemFn, Pat, PatType, ReturnType, Type, parse_macro_input};
6
7mod branch_groups;
8
9fn is_fn_like_type(ty: &Type) -> bool {
10    match ty {
11        Type::ImplTrait(impl_trait) => impl_trait.bounds.iter().any(|bound| {
12            if let syn::TypeParamBound::Trait(trait_bound) = bound {
13                let path = &trait_bound.path;
14                if let Some(segment) = path.segments.last() {
15                    let ident_str = segment.ident.to_string();
16                    return ident_str == "FnMut" || ident_str == "Fn" || ident_str == "FnOnce";
17                }
18            }
19            false
20        }),
21        Type::Path(type_path) => {
22            if let Some(segment) = type_path.path.segments.last()
23                && segment.ident == "Box"
24                && let syn::PathArguments::AngleBracketed(args) = &segment.arguments
25                && let Some(syn::GenericArgument::Type(Type::TraitObject(trait_obj))) =
26                    args.args.first()
27            {
28                return trait_obj.bounds.iter().any(|bound| {
29                    if let syn::TypeParamBound::Trait(trait_bound) = bound {
30                        let path = &trait_bound.path;
31                        if let Some(segment) = path.segments.last() {
32                            let ident_str = segment.ident.to_string();
33                            return ident_str == "FnMut"
34                                || ident_str == "Fn"
35                                || ident_str == "FnOnce";
36                        }
37                    }
38                    false
39                });
40            }
41            false
42        }
43        Type::FnPtr(_) => true,
44        _ => false,
45    }
46}
47
48fn is_generic_fn_like(ty: &Type, generics: &syn::Generics) -> bool {
49    let type_ident = match ty {
50        Type::Path(type_path) if type_path.path.segments.len() == 1 => {
51            &type_path.path.segments[0].ident
52        }
53        _ => return false,
54    };
55
56    for param in &generics.params {
57        if let syn::GenericParam::Type(type_param) = param
58            && type_param.ident == *type_ident
59        {
60            for bound in &type_param.bounds {
61                if let syn::TypeParamBound::Trait(trait_bound) = bound
62                    && let Some(segment) = trait_bound.path.segments.last()
63                {
64                    let ident_str = segment.ident.to_string();
65                    if ident_str == "FnMut" || ident_str == "Fn" || ident_str == "FnOnce" {
66                        return true;
67                    }
68                }
69            }
70        }
71    }
72
73    if let Some(where_clause) = &generics.where_clause {
74        for predicate in &where_clause.predicates {
75            if let syn::WherePredicate::Type(pred) = predicate
76                && let Type::Path(bounded_type) = &pred.bounded_ty
77                && bounded_type.path.segments.len() == 1
78                && bounded_type.path.segments[0].ident == *type_ident
79            {
80                for bound in &pred.bounds {
81                    if let syn::TypeParamBound::Trait(trait_bound) = bound
82                        && let Some(segment) = trait_bound.path.segments.last()
83                    {
84                        let ident_str = segment.ident.to_string();
85                        if ident_str == "FnMut" || ident_str == "Fn" || ident_str == "FnOnce" {
86                            return true;
87                        }
88                    }
89                }
90            }
91        }
92    }
93
94    false
95}
96
97fn is_fn_param(ty: &Type, generics: &syn::Generics) -> bool {
98    is_fn_like_type(ty) || is_generic_fn_like(ty, generics)
99}
100
101fn is_zero_arg_fn_impl_trait(ty: &Type) -> bool {
102    if let Type::ImplTrait(impl_trait) = ty {
103        impl_trait.bounds.iter().any(|bound| {
104            if let syn::TypeParamBound::Trait(trait_bound) = bound
105                && let Some(segment) = trait_bound.path.segments.last()
106            {
107                let ident_str = segment.ident.to_string();
108                if (ident_str == "Fn" || ident_str == "FnMut")
109                    && let syn::PathArguments::Parenthesized(args) = &segment.arguments
110                {
111                    return args.inputs.is_empty();
112                }
113            }
114            false
115        })
116    } else {
117        false
118    }
119}
120
121fn type_bare_generic_ident(ty: &Type) -> Option<&Ident> {
122    match ty {
123        Type::Path(type_path)
124            if type_path.qself.is_none()
125                && type_path.path.segments.len() == 1
126                && type_path.path.segments[0].arguments.is_none() =>
127        {
128            Some(&type_path.path.segments[0].ident)
129        }
130        _ => None,
131    }
132}
133
134fn stream_mentions_ident(tokens: &TokenStream2, name: &str) -> bool {
135    tokens.clone().into_iter().any(|tt| match tt {
136        proc_macro2::TokenTree::Ident(ident) => ident == name,
137        proc_macro2::TokenTree::Group(group) => stream_mentions_ident(&group.stream(), name),
138        _ => false,
139    })
140}
141
142fn filter_generics(
143    generics: &syn::Generics,
144    strip: &std::collections::HashSet<String>,
145) -> syn::Generics {
146    let mut filtered = generics.clone();
147    filtered.params = filtered
148        .params
149        .into_iter()
150        .filter(|param| match param {
151            syn::GenericParam::Type(type_param) => !strip.contains(&type_param.ident.to_string()),
152            _ => true,
153        })
154        .collect();
155    if let Some(where_clause) = &mut filtered.where_clause {
156        where_clause.predicates = where_clause
157            .predicates
158            .clone()
159            .into_iter()
160            .filter(|predicate| {
161                if let syn::WherePredicate::Type(pred) = predicate
162                    && let Some(ident) = type_bare_generic_ident(&pred.bounded_ty)
163                {
164                    return !strip.contains(&ident.to_string());
165                }
166                true
167            })
168            .collect();
169        if where_clause.predicates.is_empty() {
170            filtered.where_clause = None;
171        }
172    }
173    filtered
174}
175
176fn is_node_id_return(ty: &Type) -> bool {
177    matches!(
178        ty,
179        Type::Path(type_path)
180            if type_path
181                .path
182                .segments
183                .last()
184                .is_some_and(|segment| segment.ident == "NodeId")
185    )
186}
187
188fn core_crate_path() -> TokenStream2 {
189    let crate_name = crate_name("cranpose")
190        .ok()
191        .or_else(|| crate_name("cranpose-core").ok());
192
193    match crate_name {
194        Some(FoundCrate::Itself) => quote!(crate),
195        Some(FoundCrate::Name(name)) => {
196            let ident = Ident::new(&name, Span::call_site());
197            quote!(#ident)
198        }
199        None => quote!(cranpose_core),
200    }
201}
202
203fn definition_key_stmt(core_path: &TokenStream2, caller_key_ident: &Ident) -> TokenStream2 {
204    quote! {
205        let #caller_key_ident = #core_path::composable_identity_key({
206            struct __CranposeDefinitionMarker;
207            static __CRANPOSE_DEFINITION_KEY: ::std::sync::OnceLock<#core_path::Key> =
208                ::std::sync::OnceLock::new();
209            #core_path::cached_composable_definition_key(
210                &__CRANPOSE_DEFINITION_KEY,
211                file!(),
212                line!(),
213                column!(),
214                ::std::any::TypeId::of::<__CranposeDefinitionMarker>(),
215            )
216        });
217    }
218}
219
220/// Turns a function into a composable: its body runs inside a group keyed by
221/// the call site, and recomposition skips it while its arguments are
222/// unchanged. `#[composable(no_skip)]` always re-runs the body.
223///
224/// Composables are named in CamelCase, as in Jetpack Compose, so the
225/// generated function carries `#[allow(non_snake_case)]` and callers need no
226/// lint allowance of their own.
227#[proc_macro_attribute]
228pub fn composable(attr: TokenStream, item: TokenStream) -> TokenStream {
229    let attr_tokens = TokenStream2::from(attr);
230    let mut enable_skip = true;
231    let core_path = core_crate_path();
232    if !attr_tokens.is_empty() {
233        match syn::parse2::<Ident>(attr_tokens) {
234            Ok(ident) if ident == "no_skip" => enable_skip = false,
235            Ok(other) => {
236                return syn::Error::new_spanned(other, "unsupported composable attribute")
237                    .to_compile_error()
238                    .into();
239            }
240            Err(err) => {
241                return err.to_compile_error().into();
242            }
243        }
244    }
245
246    let mut func = parse_macro_input!(item as ItemFn);
247
248    struct ParamInfo {
249        ident: Ident,
250        pat: Box<Pat>,
251        ty: Type,
252        pat_is_mut: bool,
253        is_impl_trait: bool,
254    }
255
256    let mut param_info: Vec<ParamInfo> = Vec::new();
257
258    for (index, arg) in func.sig.inputs.iter_mut().enumerate() {
259        if let FnArg::Typed(PatType { pat, ty, .. }) = arg {
260            if let Some(reserved) = find_reserved_pattern_ident(pat) {
261                let name = reserved.to_string();
262                return syn::Error::new(
263                    reserved.span(),
264                    format!("`{name}` is reserved by #[composable]"),
265                )
266                .to_compile_error()
267                .into();
268            }
269            let pat_is_mut = matches!(
270                pat.as_ref(),
271                Pat::Ident(pat_ident) if pat_ident.mutability.is_some()
272            );
273            let is_impl_trait = matches!(**ty, Type::ImplTrait(_));
274
275            if is_impl_trait {
276                let original_pat: Box<Pat> = pat.clone();
277                if let Pat::Ident(pat_ident) = &**pat {
278                    param_info.push(ParamInfo {
279                        ident: pat_ident.ident.clone(),
280                        pat: original_pat,
281                        ty: ty.as_ref().clone(),
282                        pat_is_mut,
283                        is_impl_trait: true,
284                    });
285                } else {
286                    param_info.push(ParamInfo {
287                        ident: Ident::new(&format!("__arg{index}"), Span::mixed_site()),
288                        pat: original_pat,
289                        ty: ty.as_ref().clone(),
290                        pat_is_mut,
291                        is_impl_trait: true,
292                    });
293                }
294            } else {
295                let ident = Ident::new(&format!("__arg{index}"), Span::mixed_site());
296                let original_pat: Box<Pat> = pat.clone();
297                **pat = syn::parse_quote! { #ident };
298                param_info.push(ParamInfo {
299                    ident,
300                    pat: original_pat,
301                    ty: ty.as_ref().clone(),
302                    pat_is_mut,
303                    is_impl_trait: false,
304                });
305            }
306        }
307    }
308
309    branch_groups::inject_branch_groups(&core_path, &mut func.block);
310    let has_rust_abi = match &func.sig.abi {
311        None => true,
312        Some(abi) => abi.name.as_ref().is_some_and(|name| name.value() == "Rust"),
313    };
314    if has_rust_abi {
315        func.attrs.push(syn::parse_quote!(#[track_caller]));
316    }
317    func.attrs.push(syn::parse_quote!(#[allow(non_snake_case)]));
318
319    let scope_label_ident = func.sig.ident.clone();
320    let original_block = func.block.clone();
321    let composer_ident = Ident::new("__composer", Span::mixed_site());
322    let outer_composer_ident = Ident::new("__outer_composer", Span::mixed_site());
323    let caller_key_ident = Ident::new("__cranpose_caller_key", Span::mixed_site());
324    let current_scope_ident = Ident::new("__current_scope", Span::mixed_site());
325    let result_slot_index_ident = Ident::new("__result_slot_index", Span::mixed_site());
326    let has_previous_ident = Ident::new("__has_previous", Span::mixed_site());
327    let result_ident = Ident::new("__result", Span::mixed_site());
328    let value_ident = Ident::new("__value", Span::mixed_site());
329    let key_expr = quote! { #caller_key_ident };
330    let caller_key_stmt = definition_key_stmt(&core_path, &caller_key_ident);
331
332    let rebinds_for_no_skip: Vec<_> = param_info
333        .iter()
334        .map(|info| {
335            let ident = &info.ident;
336            let pat = &info.pat;
337            quote! { let #pat = #ident; }
338        })
339        .collect();
340
341    let return_ty: syn::Type = match &func.sig.output {
342        ReturnType::Default => syn::parse_quote! { () },
343        ReturnType::Type(_, ty) => ty.as_ref().clone(),
344    };
345    let returns_unit = match &func.sig.output {
346        ReturnType::Default => true,
347        ReturnType::Type(_, ty) => {
348            matches!(ty.as_ref(), Type::Tuple(tuple) if tuple.elems.is_empty())
349        }
350    };
351    let invalidate_return_consumer = if returns_unit || is_node_id_return(&return_ty) {
352        quote! {}
353    } else {
354        quote! { #composer_ident.__invalidate_return_consumer_scope(); }
355    };
356    let _helper_ident = Ident::new(
357        &format!("__cranpose_impl_{}", func.sig.ident),
358        Span::mixed_site(),
359    );
360    let generics = func.sig.generics.clone();
361    let (_impl_generics, _ty_generics, _where_clause) = generics.split_for_impl();
362
363    let _helper_inputs: Vec<TokenStream2> = param_info
364        .iter()
365        .map(|info| {
366            let ident = &info.ident;
367            let ty = &info.ty;
368            quote! { #ident: #ty }
369        })
370        .collect();
371
372    let has_unhandled_impl_trait = param_info
373        .iter()
374        .any(|info| info.is_impl_trait && !is_zero_arg_fn_impl_trait(&info.ty));
375
376    if enable_skip && !has_unhandled_impl_trait {
377        let helper_ident = Ident::new(
378            &format!("__cranpose_impl_{}", func.sig.ident),
379            Span::mixed_site(),
380        );
381        let generics = func.sig.generics.clone();
382
383        let param_erased: Vec<bool> = param_info
384            .iter()
385            .map(|info| {
386                (info.is_impl_trait && is_zero_arg_fn_impl_trait(&info.ty))
387                    || (!info.is_impl_trait
388                        && type_bare_generic_ident(&info.ty).is_some()
389                        && is_generic_fn_like(&info.ty, &generics))
390            })
391            .collect();
392
393        let mut strippable: std::collections::HashSet<String> = param_info
394            .iter()
395            .zip(&param_erased)
396            .filter(|(info, erased)| **erased && !info.is_impl_trait)
397            .filter_map(|(info, _)| type_bare_generic_ident(&info.ty))
398            .map(Ident::to_string)
399            .collect();
400        loop {
401            use quote::ToTokens;
402            let mut used_elsewhere: Vec<TokenStream2> = Vec::new();
403            for (info, erased) in param_info.iter().zip(&param_erased) {
404                if !*erased {
405                    used_elsewhere.push(info.ty.to_token_stream());
406                }
407            }
408            used_elsewhere.push(return_ty.to_token_stream());
409            for param in &generics.params {
410                match param {
411                    syn::GenericParam::Type(type_param) => {
412                        if !strippable.contains(&type_param.ident.to_string()) {
413                            used_elsewhere.push(type_param.bounds.to_token_stream());
414                            if let Some((_, default)) = &type_param.default {
415                                used_elsewhere.push(default.to_token_stream());
416                            }
417                        }
418                    }
419                    syn::GenericParam::Const(const_param) => {
420                        used_elsewhere.push(const_param.ty.to_token_stream());
421                    }
422                    syn::GenericParam::Lifetime(_) => {}
423                }
424            }
425            if let Some(where_clause) = &generics.where_clause {
426                for predicate in &where_clause.predicates {
427                    if let syn::WherePredicate::Type(pred) = predicate
428                        && let Some(ident) = type_bare_generic_ident(&pred.bounded_ty)
429                        && strippable.contains(&ident.to_string())
430                    {
431                        continue;
432                    }
433                    used_elsewhere.push(predicate.to_token_stream());
434                }
435            }
436            let before = strippable.len();
437            strippable.retain(|name| {
438                !used_elsewhere
439                    .iter()
440                    .any(|tokens| stream_mentions_ident(tokens, name))
441            });
442            if strippable.len() == before {
443                break;
444            }
445        }
446
447        let helper_generics = filter_generics(&generics, &strippable);
448        let (impl_generics, ty_generics, where_clause) = helper_generics.split_for_impl();
449        let ty_generics_turbofish = ty_generics.as_turbofish();
450
451        let helper_inputs: Vec<TokenStream2> = param_info
452            .iter()
453            .zip(&param_erased)
454            .filter_map(|(info, erased)| {
455                if info.is_impl_trait && !is_zero_arg_fn_impl_trait(&info.ty) {
456                    None
457                } else if *erased {
458                    let ident = &info.ident;
459                    Some(quote! { #ident: ::std::boxed::Box<dyn ::core::ops::FnMut() + 'static> })
460                } else {
461                    let ident = &info.ident;
462                    let ty = &info.ty;
463                    Some(quote! { #ident: #ty })
464                }
465            })
466            .collect();
467
468        let param_state_slots: Vec<Ident> = (0..param_info.len())
469            .map(|index| Ident::new(&format!("__param_state_slot{index}"), Span::mixed_site()))
470            .collect();
471
472        let param_is_callback: Vec<bool> = param_info
473            .iter()
474            .map(|info| {
475                (info.is_impl_trait && is_zero_arg_fn_impl_trait(&info.ty))
476                    || (!info.is_impl_trait && is_fn_param(&info.ty, &generics))
477            })
478            .collect();
479
480        let param_setup: Vec<TokenStream2> = param_info
481            .iter()
482            .zip(param_state_slots.iter())
483            .zip(&param_erased)
484            .zip(&param_is_callback)
485            .map(|(((info, slot_ident), erased), is_callback)| {
486                if *is_callback {
487                    let ident = &info.ident;
488                    let update = if *erased {
489                        quote! { holder.update_boxed(#ident); }
490                    } else {
491                        quote! { holder.update(#ident); }
492                    };
493                    quote! {
494                        let #slot_ident = #composer_ident
495                            .__use_param_slot(|| #core_path::CallbackHolder::new());
496                        #composer_ident.with_slot_value::<#core_path::CallbackHolder, _>(
497                            #slot_ident,
498                            |holder| {
499                                #update
500                            },
501                        );
502                        __changed = true;
503                    }
504                } else if info.is_impl_trait {
505                    quote! { __changed = true; }
506                } else {
507                    let ident = &info.ident;
508                    let ty = &info.ty;
509                    quote! {
510                        let #slot_ident = #composer_ident
511                            .__use_param_slot(|| #core_path::ParamState::<#ty>::default());
512                        if #composer_ident.with_slot_value_mut::<#core_path::ParamState<#ty>, _>(
513                            #slot_ident,
514                            |state| state.update(&#ident),
515                        )
516                        {
517                            __changed = true;
518                        }
519                    }
520                }
521            })
522            .collect();
523
524        let param_setup_recompose: Vec<TokenStream2> = param_info
525            .iter()
526            .zip(param_state_slots.iter())
527            .zip(&param_is_callback)
528            .map(|((info, slot_ident), is_callback)| {
529                if *is_callback {
530                    quote! {
531                        let #slot_ident = #composer_ident
532                            .__use_param_slot(|| #core_path::CallbackHolder::new());
533                    }
534                } else if info.is_impl_trait {
535                    quote! {}
536                } else {
537                    let ty = &info.ty;
538                    quote! {
539                        let #slot_ident = #composer_ident
540                            .__use_param_slot(|| #core_path::ParamState::<#ty>::default());
541                    }
542                }
543            })
544            .collect();
545
546        let callback_rebinds: Vec<TokenStream2> = param_info
547            .iter()
548            .zip(param_state_slots.iter())
549            .zip(&param_is_callback)
550            .filter(|(_, is_callback)| **is_callback)
551            .map(|((info, slot_ident), _)| {
552                let pat = &info.pat;
553                let can_add_mut = matches!(pat.as_ref(), Pat::Ident(_));
554                let binding = if can_add_mut && !info.pat_is_mut {
555                    quote! { mut #pat }
556                } else {
557                    quote! { #pat }
558                };
559                quote! {
560                    #[allow(unused_mut)]
561                    let #binding = #composer_ident
562                        .with_slot_value::<#core_path::CallbackHolder, _>(
563                            #slot_ident,
564                            |holder| holder.clone_rc(),
565                        );
566                }
567            })
568            .collect();
569
570        let body_inputs: Vec<TokenStream2> = param_info
571            .iter()
572            .zip(param_state_slots.iter())
573            .zip(&param_is_callback)
574            .map(|((info, slot_ident), is_callback)| {
575                if *is_callback {
576                    quote! {
577                        #slot_ident: #core_path::ValueSlotHandle<'_, #core_path::CallbackHolder>
578                    }
579                } else {
580                    let pat = &info.pat;
581                    let ty = &info.ty;
582                    quote! { #pat: #ty }
583                }
584            })
585            .collect();
586
587        let body_args: Vec<TokenStream2> = param_info
588            .iter()
589            .zip(param_state_slots.iter())
590            .zip(&param_is_callback)
591            .map(|((info, slot_ident), is_callback)| {
592                if *is_callback {
593                    quote! { #slot_ident }
594                } else {
595                    let ident = &info.ident;
596                    quote! { #ident }
597                }
598            })
599            .collect();
600
601        let reads_for_recompose: Vec<TokenStream2> = param_info
602            .iter()
603            .zip(param_state_slots.iter())
604            .zip(&param_is_callback)
605            .filter(|(_, is_callback)| !**is_callback)
606            .map(|((info, slot_ident), _)| {
607                let ident = &info.ident;
608                let ty = &info.ty;
609                quote! {
610                    let #ident = #composer_ident
611                        .with_slot_value::<#core_path::ParamState<#ty>, _>(
612                            #slot_ident,
613                            |state| {
614                                state
615                                    .value()
616                                    .expect("composable parameter missing for recomposition")
617                            },
618                        );
619                }
620            })
621            .collect();
622
623        let body_ident = Ident::new(
624            &format!("__cranpose_body_{}", func.sig.ident),
625            Span::mixed_site(),
626        );
627        let body_call = quote! {
628            #body_ident #ty_generics_turbofish (#composer_ident #(, #body_args)*)
629        };
630
631        let recompose_fn_ident = Ident::new(
632            &format!("__cranpose_recompose_{}", func.sig.ident),
633            Span::mixed_site(),
634        );
635
636        let recompose_setter = quote! {
637            {
638                #composer_ident.set_recompose_callback(move |
639                    #composer_ident: &#core_path::Composer|
640                {
641                    let _ = #recompose_fn_ident #ty_generics_turbofish (
642                        #composer_ident
643                    );
644                });
645            }
646        };
647
648        let helper_body = if returns_unit {
649            quote! {
650                #core_path::debug_label_current_scope(stringify!(#scope_label_ident));
651                let #current_scope_ident = #composer_ident
652                    .current_recompose_scope()
653                    .expect("missing recompose scope");
654                let mut __changed = #current_scope_ident.should_recompose();
655                #(#param_setup)*
656                #recompose_setter
657                if !__changed && #current_scope_ident.has_composed_once() {
658                    #composer_ident.skip_current_group();
659                    return;
660                }
661                #body_call
662            }
663        } else {
664            quote! {
665                #core_path::debug_label_current_scope(stringify!(#scope_label_ident));
666                let #current_scope_ident = #composer_ident
667                    .current_recompose_scope()
668                    .expect("missing recompose scope");
669                let mut __changed = #current_scope_ident.should_recompose();
670                #(#param_setup)*
671                #recompose_setter
672                let #result_slot_index_ident = #composer_ident
673                    .__use_return_slot(|| #core_path::ReturnSlot::<#return_ty>::default());
674                let #has_previous_ident = #composer_ident
675                    .with_slot_value::<#core_path::ReturnSlot<#return_ty>, _>(
676                        #result_slot_index_ident,
677                        |slot| slot.get().is_some(),
678                    );
679                if !__changed && #has_previous_ident {
680                    #composer_ident.skip_current_group();
681                    let #result_ident = #composer_ident
682                        .with_slot_value::<#core_path::ReturnSlot<#return_ty>, _>(
683                            #result_slot_index_ident,
684                            |slot| {
685                                slot.get()
686                                    .expect("composable return value missing during skip")
687                            },
688                        );
689                    return #result_ident;
690                }
691                let #value_ident: #return_ty = #body_call;
692                #composer_ident.with_slot_value_mut::<#core_path::ReturnSlot<#return_ty>, _>(
693                    #result_slot_index_ident,
694                    |slot| {
695                        slot.store(#value_ident.clone());
696                    },
697                );
698                #value_ident
699            }
700        };
701
702        let recompose_fn_body = if returns_unit {
703            quote! {
704                #(#param_setup_recompose)*
705                #(#reads_for_recompose)*
706                #body_call;
707                #recompose_setter
708            }
709        } else {
710            quote! {
711                #(#param_setup_recompose)*
712                let #result_slot_index_ident = #composer_ident
713                    .__use_return_slot(|| #core_path::ReturnSlot::<#return_ty>::default());
714                #(#reads_for_recompose)*
715                let #value_ident: #return_ty = #body_call;
716                #composer_ident.with_slot_value_mut::<#core_path::ReturnSlot<#return_ty>, _>(
717                    #result_slot_index_ident,
718                    |slot| {
719                        slot.store(#value_ident.clone());
720                    },
721                );
722                #recompose_setter
723                #invalidate_return_consumer
724                #value_ident
725            }
726        };
727
728        let recompose_fn = quote! {
729            #[allow(non_snake_case)]
730            fn #recompose_fn_ident #impl_generics (
731                #composer_ident: &#core_path::Composer
732            ) -> #return_ty #where_clause {
733                #recompose_fn_body
734            }
735        };
736
737        let helper_fn = quote! {
738            #[allow(non_snake_case, clippy::too_many_arguments)]
739            fn #helper_ident #impl_generics (
740                #composer_ident: &#core_path::Composer
741                #(, #helper_inputs)*
742            ) -> #return_ty #where_clause {
743                #helper_body
744            }
745        };
746
747        let body_fn = quote! {
748            #[allow(non_snake_case, clippy::too_many_arguments)]
749            fn #body_ident #impl_generics (
750                #composer_ident: &#core_path::Composer
751                #(, #body_inputs)*
752            ) -> #return_ty #where_clause {
753                #(#callback_rebinds)*
754                #original_block
755            }
756        };
757
758        let wrapper_args: Vec<TokenStream2> = param_info
759            .iter()
760            .zip(&param_erased)
761            .filter_map(|(info, erased)| {
762                if info.is_impl_trait && !is_zero_arg_fn_impl_trait(&info.ty) {
763                    None
764                } else if *erased {
765                    let ident = &info.ident;
766                    Some(quote! { ::std::boxed::Box::new(#ident) })
767                } else {
768                    let ident = &info.ident;
769                    Some(quote! { #ident })
770                }
771            })
772            .collect();
773
774        let wrapped = quote!({
775            #caller_key_stmt
776            #core_path::with_current_composer(|#composer_ident: &#core_path::Composer| {
777                #composer_ident.with_group(#key_expr, |#composer_ident: &#core_path::Composer| {
778                    #helper_ident(#composer_ident #(, #wrapper_args)*)
779                })
780            })
781        });
782        *func.block = syn::parse2(wrapped).expect("failed to build block");
783        TokenStream::from(quote! {
784            #body_fn
785            #recompose_fn
786            #helper_fn
787            #func
788        })
789    } else {
790        let wrapped = quote!({
791            #caller_key_stmt
792            #core_path::with_current_composer(|#outer_composer_ident: &#core_path::Composer| {
793                #outer_composer_ident.with_group(#key_expr, |#composer_ident: &#core_path::Composer| {
794                    #core_path::debug_label_current_scope(stringify!(#scope_label_ident));
795                    #(#rebinds_for_no_skip)*
796                    #original_block
797                })
798            })
799        });
800        *func.block = syn::parse2(wrapped).expect("failed to build block");
801        TokenStream::from(quote! { #func })
802    }
803}
804
805fn find_reserved_pattern_ident(pat: &Pat) -> Option<&Ident> {
806    use syn::visit::Visit;
807
808    struct Scan<'ast> {
809        found: Option<&'ast Ident>,
810    }
811    impl<'ast> syn::visit::Visit<'ast> for Scan<'ast> {
812        fn visit_pat_ident(&mut self, node: &'ast syn::PatIdent) {
813            if self.found.is_none() {
814                let name = node.ident.to_string();
815                if name == "__composer" || name.starts_with("__cranpose") {
816                    self.found = Some(&node.ident);
817                }
818            }
819            syn::visit::visit_pat_ident(self, node);
820        }
821    }
822    let mut scan = Scan { found: None };
823    scan.visit_pat(pat);
824    scan.found
825}
826
827#[cfg(test)]
828#[path = "tests/macros_tests.rs"]
829mod tests;