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::BareFn(_) => 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
203#[proc_macro_attribute]
204pub fn composable(attr: TokenStream, item: TokenStream) -> TokenStream {
205    let attr_tokens = TokenStream2::from(attr);
206    let mut enable_skip = true;
207    let core_path = core_crate_path();
208    if !attr_tokens.is_empty() {
209        match syn::parse2::<Ident>(attr_tokens) {
210            Ok(ident) if ident == "no_skip" => enable_skip = false,
211            Ok(other) => {
212                return syn::Error::new_spanned(other, "unsupported composable attribute")
213                    .to_compile_error()
214                    .into();
215            }
216            Err(err) => {
217                return err.to_compile_error().into();
218            }
219        }
220    }
221
222    let mut func = parse_macro_input!(item as ItemFn);
223
224    struct ParamInfo {
225        ident: Ident,
226        pat: Box<Pat>,
227        ty: Type,
228        pat_is_mut: bool,
229        is_impl_trait: bool,
230    }
231
232    let mut param_info: Vec<ParamInfo> = Vec::new();
233
234    for (index, arg) in func.sig.inputs.iter_mut().enumerate() {
235        if let FnArg::Typed(PatType { pat, ty, .. }) = arg {
236            if let Some(reserved) = find_reserved_pattern_ident(pat) {
237                let name = reserved.to_string();
238                return syn::Error::new(
239                    reserved.span(),
240                    format!("`{name}` is reserved by #[composable]"),
241                )
242                .to_compile_error()
243                .into();
244            }
245            let pat_is_mut = matches!(
246                pat.as_ref(),
247                Pat::Ident(pat_ident) if pat_ident.mutability.is_some()
248            );
249            let is_impl_trait = matches!(**ty, Type::ImplTrait(_));
250
251            if is_impl_trait {
252                let original_pat: Box<Pat> = pat.clone();
253                if let Pat::Ident(pat_ident) = &**pat {
254                    param_info.push(ParamInfo {
255                        ident: pat_ident.ident.clone(),
256                        pat: original_pat,
257                        ty: ty.as_ref().clone(),
258                        pat_is_mut,
259                        is_impl_trait: true,
260                    });
261                } else {
262                    param_info.push(ParamInfo {
263                        ident: Ident::new(&format!("__arg{}", index), Span::mixed_site()),
264                        pat: original_pat,
265                        ty: ty.as_ref().clone(),
266                        pat_is_mut,
267                        is_impl_trait: true,
268                    });
269                }
270            } else {
271                let ident = Ident::new(&format!("__arg{}", index), Span::mixed_site());
272                let original_pat: Box<Pat> = pat.clone();
273                **pat = syn::parse_quote! { #ident };
274                param_info.push(ParamInfo {
275                    ident,
276                    pat: original_pat,
277                    ty: ty.as_ref().clone(),
278                    pat_is_mut,
279                    is_impl_trait: false,
280                });
281            }
282        }
283    }
284
285    branch_groups::inject_branch_groups(&core_path, &mut func.block);
286    let has_rust_abi = match &func.sig.abi {
287        None => true,
288        Some(abi) => abi.name.as_ref().is_some_and(|name| name.value() == "Rust"),
289    };
290    if has_rust_abi {
291        func.attrs.push(syn::parse_quote!(#[track_caller]));
292    }
293
294    let scope_label_ident = func.sig.ident.clone();
295    let original_block = func.block.clone();
296    let helper_block = original_block.clone();
297    let recompose_block = original_block.clone();
298    let composer_ident = Ident::new("__composer", Span::mixed_site());
299    let outer_composer_ident = Ident::new("__outer_composer", Span::mixed_site());
300    let caller_key_ident = Ident::new("__cranpose_caller_key", Span::mixed_site());
301    let current_scope_ident = Ident::new("__current_scope", Span::mixed_site());
302    let result_slot_index_ident = Ident::new("__result_slot_index", Span::mixed_site());
303    let has_previous_ident = Ident::new("__has_previous", Span::mixed_site());
304    let result_ident = Ident::new("__result", Span::mixed_site());
305    let value_ident = Ident::new("__value", Span::mixed_site());
306    let key_expr = quote! { #caller_key_ident };
307    let caller_key_stmt = quote! {
308        let #caller_key_ident = #core_path::composable_identity_key({
309            struct __CranposeDefinitionMarker;
310            static __CRANPOSE_DEFINITION_KEY: ::std::sync::OnceLock<#core_path::Key> =
311                ::std::sync::OnceLock::new();
312            *__CRANPOSE_DEFINITION_KEY.get_or_init(|| {
313                #core_path::composable_definition_key(
314                    file!(),
315                    line!(),
316                    column!(),
317                    ::std::any::TypeId::of::<__CranposeDefinitionMarker>(),
318                )
319            })
320        });
321    };
322
323    let rebinds_for_no_skip: Vec<_> = param_info
324        .iter()
325        .map(|info| {
326            let ident = &info.ident;
327            let pat = &info.pat;
328            quote! { let #pat = #ident; }
329        })
330        .collect();
331
332    let return_ty: syn::Type = match &func.sig.output {
333        ReturnType::Default => syn::parse_quote! { () },
334        ReturnType::Type(_, ty) => ty.as_ref().clone(),
335    };
336    let returns_unit = match &func.sig.output {
337        ReturnType::Default => true,
338        ReturnType::Type(_, ty) => {
339            matches!(ty.as_ref(), Type::Tuple(tuple) if tuple.elems.is_empty())
340        }
341    };
342    let invalidate_return_consumer = if returns_unit || is_node_id_return(&return_ty) {
343        quote! {}
344    } else {
345        quote! { #composer_ident.__invalidate_return_consumer_scope(); }
346    };
347    let _helper_ident = Ident::new(
348        &format!("__cranpose_impl_{}", func.sig.ident),
349        Span::mixed_site(),
350    );
351    let generics = func.sig.generics.clone();
352    let (_impl_generics, _ty_generics, _where_clause) = generics.split_for_impl();
353
354    let _helper_inputs: Vec<TokenStream2> = param_info
355        .iter()
356        .map(|info| {
357            let ident = &info.ident;
358            let ty = &info.ty;
359            quote! { #ident: #ty }
360        })
361        .collect();
362
363    let has_unhandled_impl_trait = param_info
364        .iter()
365        .any(|info| info.is_impl_trait && !is_zero_arg_fn_impl_trait(&info.ty));
366
367    if enable_skip && !has_unhandled_impl_trait {
368        let helper_ident = Ident::new(
369            &format!("__cranpose_impl_{}", func.sig.ident),
370            Span::mixed_site(),
371        );
372        let generics = func.sig.generics.clone();
373
374        let param_erased: Vec<bool> = param_info
375            .iter()
376            .map(|info| {
377                (info.is_impl_trait && is_zero_arg_fn_impl_trait(&info.ty))
378                    || (!info.is_impl_trait
379                        && type_bare_generic_ident(&info.ty).is_some()
380                        && is_generic_fn_like(&info.ty, &generics))
381            })
382            .collect();
383
384        let mut strippable: std::collections::HashSet<String> = param_info
385            .iter()
386            .zip(&param_erased)
387            .filter(|(info, erased)| **erased && !info.is_impl_trait)
388            .filter_map(|(info, _)| type_bare_generic_ident(&info.ty))
389            .map(Ident::to_string)
390            .collect();
391        loop {
392            use quote::ToTokens;
393            let mut used_elsewhere: Vec<TokenStream2> = Vec::new();
394            for (info, erased) in param_info.iter().zip(&param_erased) {
395                if !*erased {
396                    used_elsewhere.push(info.ty.to_token_stream());
397                }
398            }
399            used_elsewhere.push(return_ty.to_token_stream());
400            for param in &generics.params {
401                match param {
402                    syn::GenericParam::Type(type_param) => {
403                        if !strippable.contains(&type_param.ident.to_string()) {
404                            used_elsewhere.push(type_param.bounds.to_token_stream());
405                            if let Some(default) = &type_param.default {
406                                used_elsewhere.push(default.to_token_stream());
407                            }
408                        }
409                    }
410                    syn::GenericParam::Const(const_param) => {
411                        used_elsewhere.push(const_param.ty.to_token_stream());
412                    }
413                    syn::GenericParam::Lifetime(_) => {}
414                }
415            }
416            if let Some(where_clause) = &generics.where_clause {
417                for predicate in &where_clause.predicates {
418                    if let syn::WherePredicate::Type(pred) = predicate
419                        && let Some(ident) = type_bare_generic_ident(&pred.bounded_ty)
420                        && strippable.contains(&ident.to_string())
421                    {
422                        continue;
423                    }
424                    used_elsewhere.push(predicate.to_token_stream());
425                }
426            }
427            let before = strippable.len();
428            strippable.retain(|name| {
429                !used_elsewhere
430                    .iter()
431                    .any(|tokens| stream_mentions_ident(tokens, name))
432            });
433            if strippable.len() == before {
434                break;
435            }
436        }
437
438        let helper_generics = filter_generics(&generics, &strippable);
439        let (impl_generics, ty_generics, where_clause) = helper_generics.split_for_impl();
440        let ty_generics_turbofish = ty_generics.as_turbofish();
441
442        let helper_inputs: Vec<TokenStream2> = param_info
443            .iter()
444            .zip(&param_erased)
445            .filter_map(|(info, erased)| {
446                if info.is_impl_trait && !is_zero_arg_fn_impl_trait(&info.ty) {
447                    None
448                } else if *erased {
449                    let ident = &info.ident;
450                    Some(quote! { #ident: ::std::boxed::Box<dyn ::core::ops::FnMut() + 'static> })
451                } else {
452                    let ident = &info.ident;
453                    let ty = &info.ty;
454                    Some(quote! { #ident: #ty })
455                }
456            })
457            .collect();
458
459        let param_state_slots: Vec<Ident> = (0..param_info.len())
460            .map(|index| Ident::new(&format!("__param_state_slot{}", index), Span::mixed_site()))
461            .collect();
462
463        let param_setup: Vec<TokenStream2> = param_info
464            .iter()
465            .zip(param_state_slots.iter())
466            .zip(&param_erased)
467            .map(|((info, slot_ident), erased)| {
468                if (info.is_impl_trait && is_zero_arg_fn_impl_trait(&info.ty))
469                    || (!info.is_impl_trait && is_fn_param(&info.ty, &generics))
470                {
471                    let ident = &info.ident;
472                    let update = if *erased {
473                        quote! { holder.update_boxed(#ident); }
474                    } else {
475                        quote! { holder.update(#ident); }
476                    };
477                    quote! {
478                        let #slot_ident = #composer_ident
479                            .__use_param_slot(|| #core_path::CallbackHolder::new());
480                        #composer_ident.with_slot_value::<#core_path::CallbackHolder, _>(
481                            #slot_ident,
482                            |holder| {
483                                #update
484                            },
485                        );
486                        __changed = true;
487                    }
488                } else if info.is_impl_trait {
489                    quote! { __changed = true; }
490                } else {
491                    let ident = &info.ident;
492                    let ty = &info.ty;
493                    quote! {
494                        let #slot_ident = #composer_ident
495                            .__use_param_slot(|| #core_path::ParamState::<#ty>::default());
496                        if #composer_ident.with_slot_value_mut::<#core_path::ParamState<#ty>, _>(
497                            #slot_ident,
498                            |state| state.update(&#ident),
499                        )
500                        {
501                            __changed = true;
502                        }
503                    }
504                }
505            })
506            .collect();
507
508        let param_setup_recompose: Vec<TokenStream2> = param_info
509            .iter()
510            .zip(param_state_slots.iter())
511            .map(|(info, slot_ident)| {
512                if (info.is_impl_trait && is_zero_arg_fn_impl_trait(&info.ty))
513                    || (!info.is_impl_trait && is_fn_param(&info.ty, &generics))
514                {
515                    quote! {
516                        let #slot_ident = #composer_ident
517                            .__use_param_slot(|| #core_path::CallbackHolder::new());
518                    }
519                } else if info.is_impl_trait {
520                    quote! {}
521                } else {
522                    let ty = &info.ty;
523                    quote! {
524                        let #slot_ident = #composer_ident
525                            .__use_param_slot(|| #core_path::ParamState::<#ty>::default());
526                    }
527                }
528            })
529            .collect();
530
531        let rebinds: Vec<TokenStream2> = param_info
532            .iter()
533            .zip(param_state_slots.iter())
534            .map(|(info, slot_ident)| {
535                if (info.is_impl_trait && is_zero_arg_fn_impl_trait(&info.ty))
536                    || (!info.is_impl_trait && is_fn_param(&info.ty, &generics))
537                {
538                    let pat = &info.pat;
539                    let can_add_mut = matches!(pat.as_ref(), Pat::Ident(_));
540                    if can_add_mut && !info.pat_is_mut {
541                        quote! {
542                            #[allow(unused_mut)]
543                            let mut #pat = #composer_ident
544                                .with_slot_value::<#core_path::CallbackHolder, _>(
545                                    #slot_ident,
546                                    |holder| holder.clone_rc(),
547                                );
548                        }
549                    } else {
550                        quote! {
551                            #[allow(unused_mut)]
552                            let #pat = #composer_ident
553                                .with_slot_value::<#core_path::CallbackHolder, _>(
554                                    #slot_ident,
555                                    |holder| holder.clone_rc(),
556                                );
557                        }
558                    }
559                } else if info.is_impl_trait {
560                    quote! {}
561                } else {
562                    let pat = &info.pat;
563                    let ident = &info.ident;
564                    quote! {
565                        let #pat = #ident;
566                    }
567                }
568            })
569            .collect();
570
571        let rebinds_for_recompose: Vec<TokenStream2> = param_info
572            .iter()
573            .zip(param_state_slots.iter())
574            .map(|(info, slot_ident)| {
575                if (info.is_impl_trait && is_zero_arg_fn_impl_trait(&info.ty))
576                    || (!info.is_impl_trait && is_fn_param(&info.ty, &generics))
577                {
578                    let pat = &info.pat;
579                    let can_add_mut = matches!(pat.as_ref(), Pat::Ident(_));
580                    if can_add_mut && !info.pat_is_mut {
581                        quote! {
582                            #[allow(unused_mut)]
583                            let mut #pat = #composer_ident
584                                .with_slot_value::<#core_path::CallbackHolder, _>(
585                                    #slot_ident,
586                                    |holder| holder.clone_rc(),
587                                );
588                        }
589                    } else {
590                        quote! {
591                            #[allow(unused_mut)]
592                            let #pat = #composer_ident
593                                .with_slot_value::<#core_path::CallbackHolder, _>(
594                                    #slot_ident,
595                                    |holder| holder.clone_rc(),
596                                );
597                        }
598                    }
599                } else if info.is_impl_trait {
600                    quote! {}
601                } else {
602                    let pat = &info.pat;
603                    let ty = &info.ty;
604                    quote! {
605                        let #pat = #composer_ident
606                            .with_slot_value::<#core_path::ParamState<#ty>, _>(
607                                #slot_ident,
608                                |state| {
609                                    state
610                                        .value()
611                                        .expect("composable parameter missing for recomposition")
612                                },
613                            );
614                    }
615                }
616            })
617            .collect();
618
619        let recompose_fn_ident = Ident::new(
620            &format!("__cranpose_recompose_{}", func.sig.ident),
621            Span::mixed_site(),
622        );
623
624        let recompose_setter = quote! {
625            {
626                #composer_ident.set_recompose_callback(move |
627                    #composer_ident: &#core_path::Composer|
628                {
629                    let _ = #recompose_fn_ident #ty_generics_turbofish (
630                        #composer_ident
631                    );
632                });
633            }
634        };
635
636        let helper_body = if returns_unit {
637            quote! {
638                #core_path::debug_label_current_scope(stringify!(#scope_label_ident));
639                let #current_scope_ident = #composer_ident
640                    .current_recompose_scope()
641                    .expect("missing recompose scope");
642                let mut __changed = #current_scope_ident.should_recompose();
643                #(#param_setup)*
644                #recompose_setter
645                if !__changed && #current_scope_ident.has_composed_once() {
646                    #composer_ident.skip_current_group();
647                    return;
648                }
649                #(#rebinds)*
650                #helper_block
651            }
652        } else {
653            quote! {
654                #core_path::debug_label_current_scope(stringify!(#scope_label_ident));
655                let #current_scope_ident = #composer_ident
656                    .current_recompose_scope()
657                    .expect("missing recompose scope");
658                let mut __changed = #current_scope_ident.should_recompose();
659                #(#param_setup)*
660                #recompose_setter
661                let #result_slot_index_ident = #composer_ident
662                    .__use_return_slot(|| #core_path::ReturnSlot::<#return_ty>::default());
663                let #has_previous_ident = #composer_ident
664                    .with_slot_value::<#core_path::ReturnSlot<#return_ty>, _>(
665                        #result_slot_index_ident,
666                        |slot| slot.get().is_some(),
667                    );
668                if !__changed && #has_previous_ident {
669                    #composer_ident.skip_current_group();
670                    let #result_ident = #composer_ident
671                        .with_slot_value::<#core_path::ReturnSlot<#return_ty>, _>(
672                            #result_slot_index_ident,
673                            |slot| {
674                                slot.get()
675                                    .expect("composable return value missing during skip")
676                            },
677                        );
678                    return #result_ident;
679                }
680                let #value_ident: #return_ty = {
681                    #(#rebinds)*
682                    #helper_block
683                };
684                #composer_ident.with_slot_value_mut::<#core_path::ReturnSlot<#return_ty>, _>(
685                    #result_slot_index_ident,
686                    |slot| {
687                        slot.store(#value_ident.clone());
688                    },
689                );
690                #value_ident
691            }
692        };
693
694        let recompose_fn_body = if returns_unit {
695            quote! {
696                #(#param_setup_recompose)*
697                #(#rebinds_for_recompose)*
698                #recompose_block
699                #recompose_setter
700            }
701        } else {
702            quote! {
703                #(#param_setup_recompose)*
704                let #result_slot_index_ident = #composer_ident
705                    .__use_return_slot(|| #core_path::ReturnSlot::<#return_ty>::default());
706                #(#rebinds_for_recompose)*
707                let #value_ident: #return_ty = {
708                    #recompose_block
709                };
710                #composer_ident.with_slot_value_mut::<#core_path::ReturnSlot<#return_ty>, _>(
711                    #result_slot_index_ident,
712                    |slot| {
713                        slot.store(#value_ident.clone());
714                    },
715                );
716                #recompose_setter
717                #invalidate_return_consumer
718                #value_ident
719            }
720        };
721
722        let recompose_fn = quote! {
723            #[allow(non_snake_case)]
724            fn #recompose_fn_ident #impl_generics (
725                #composer_ident: &#core_path::Composer
726            ) -> #return_ty #where_clause {
727                #recompose_fn_body
728            }
729        };
730
731        let helper_fn = quote! {
732            #[allow(non_snake_case, clippy::too_many_arguments)]
733            fn #helper_ident #impl_generics (
734                #composer_ident: &#core_path::Composer
735                #(, #helper_inputs)*
736            ) -> #return_ty #where_clause {
737                #helper_body
738            }
739        };
740
741        let wrapper_args: Vec<TokenStream2> = param_info
742            .iter()
743            .zip(&param_erased)
744            .filter_map(|(info, erased)| {
745                if info.is_impl_trait && !is_zero_arg_fn_impl_trait(&info.ty) {
746                    None
747                } else if *erased {
748                    let ident = &info.ident;
749                    Some(quote! { ::std::boxed::Box::new(#ident) })
750                } else {
751                    let ident = &info.ident;
752                    Some(quote! { #ident })
753                }
754            })
755            .collect();
756
757        let wrapped = quote!({
758            #caller_key_stmt
759            #core_path::with_current_composer(|#composer_ident: &#core_path::Composer| {
760                #composer_ident.with_group(#key_expr, |#composer_ident: &#core_path::Composer| {
761                    #helper_ident(#composer_ident #(, #wrapper_args)*)
762                })
763            })
764        });
765        *func.block = syn::parse2(wrapped).expect("failed to build block");
766        TokenStream::from(quote! {
767            #recompose_fn
768            #helper_fn
769            #func
770        })
771    } else {
772        let wrapped = quote!({
773            #caller_key_stmt
774            #core_path::with_current_composer(|#outer_composer_ident: &#core_path::Composer| {
775                #outer_composer_ident.with_group(#key_expr, |#composer_ident: &#core_path::Composer| {
776                    #core_path::debug_label_current_scope(stringify!(#scope_label_ident));
777                    #(#rebinds_for_no_skip)*
778                    #original_block
779                })
780            })
781        });
782        *func.block = syn::parse2(wrapped).expect("failed to build block");
783        TokenStream::from(quote! { #func })
784    }
785}
786
787fn find_reserved_pattern_ident(pat: &Pat) -> Option<&Ident> {
788    use syn::visit::Visit;
789
790    struct Scan<'ast> {
791        found: Option<&'ast Ident>,
792    }
793    impl<'ast> syn::visit::Visit<'ast> for Scan<'ast> {
794        fn visit_pat_ident(&mut self, node: &'ast syn::PatIdent) {
795            if self.found.is_none() {
796                let name = node.ident.to_string();
797                if name == "__composer" || name.starts_with("__cranpose") {
798                    self.found = Some(&node.ident);
799                }
800            }
801            syn::visit::visit_pat_ident(self, node);
802        }
803    }
804    let mut scan = Scan { found: None };
805    scan.visit_pat(pat);
806    scan.found
807}