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