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