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#[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
113fn 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
124fn 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#[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(¶m_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(¶m_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(¶m_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(¶m_erased)
529 .zip(¶m_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(¶m_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(¶m_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(¶m_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(¶m_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(¶m_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(¶m_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;