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