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