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(
249 core_path: &TokenStream2,
250 caller_key_ident: &Ident,
251 name: &Ident,
252) -> TokenStream2 {
253 if cfg!(feature = "hot-reload") {
254 let name = name.to_string();
257 return quote! {
258 let #caller_key_ident = #core_path::composable_identity_key({
259 const __CRANPOSE_DEFINITION_KEY: #core_path::Key =
260 #core_path::hot_definition_key(file!(), module_path!(), #name);
261 __CRANPOSE_DEFINITION_KEY
262 });
263 };
264 }
265 quote! {
266 let #caller_key_ident = #core_path::composable_identity_key({
267 struct __CranposeDefinitionMarker;
268 static __CRANPOSE_DEFINITION_KEY: ::std::sync::OnceLock<#core_path::Key> =
269 ::std::sync::OnceLock::new();
270 #core_path::cached_composable_definition_key(
271 &__CRANPOSE_DEFINITION_KEY,
272 file!(),
273 line!(),
274 column!(),
275 ::std::any::TypeId::of::<__CranposeDefinitionMarker>(),
276 )
277 });
278 }
279}
280
281#[proc_macro_attribute]
289pub fn composable(attr: TokenStream, item: TokenStream) -> TokenStream {
290 let attr_tokens = TokenStream2::from(attr);
291 let mut enable_skip = true;
292 let core_path = core_crate_path();
293 if !attr_tokens.is_empty() {
294 match syn::parse2::<Ident>(attr_tokens) {
295 Ok(ident) if ident == "no_skip" => enable_skip = false,
296 Ok(other) => {
297 return syn::Error::new_spanned(other, "unsupported composable attribute")
298 .to_compile_error()
299 .into();
300 }
301 Err(err) => {
302 return err.to_compile_error().into();
303 }
304 }
305 }
306
307 let mut func = parse_macro_input!(item as ItemFn);
308
309 struct ParamInfo {
310 ident: Ident,
311 pat: Box<Pat>,
312 ty: Type,
313 pat_is_mut: bool,
314 is_impl_trait: bool,
315 }
316
317 let mut param_info: Vec<ParamInfo> = Vec::new();
318
319 for (index, arg) in func.sig.inputs.iter_mut().enumerate() {
320 if let FnArg::Typed(PatType { pat, ty, .. }) = arg {
321 if let Some(reserved) = find_reserved_pattern_ident(pat) {
322 let name = reserved.to_string();
323 return syn::Error::new(
324 reserved.span(),
325 format!("`{name}` is reserved by #[composable]"),
326 )
327 .to_compile_error()
328 .into();
329 }
330 let pat_is_mut = matches!(
331 pat.as_ref(),
332 Pat::Ident(pat_ident) if pat_ident.mutability.is_some()
333 );
334 let is_impl_trait = matches!(**ty, Type::ImplTrait(_));
335
336 if is_impl_trait {
337 let original_pat: Box<Pat> = pat.clone();
338 if let Pat::Ident(pat_ident) = &**pat {
339 param_info.push(ParamInfo {
340 ident: pat_ident.ident.clone(),
341 pat: original_pat,
342 ty: ty.as_ref().clone(),
343 pat_is_mut,
344 is_impl_trait: true,
345 });
346 } else {
347 param_info.push(ParamInfo {
348 ident: Ident::new(&format!("__arg{index}"), Span::mixed_site()),
349 pat: original_pat,
350 ty: ty.as_ref().clone(),
351 pat_is_mut,
352 is_impl_trait: true,
353 });
354 }
355 } else {
356 let ident = Ident::new(&format!("__arg{index}"), Span::mixed_site());
357 let original_pat: Box<Pat> = pat.clone();
358 **pat = syn::parse_quote! { #ident };
359 param_info.push(ParamInfo {
360 ident,
361 pat: original_pat,
362 ty: ty.as_ref().clone(),
363 pat_is_mut,
364 is_impl_trait: false,
365 });
366 }
367 }
368 }
369
370 inject_groups(&core_path, &mut func);
371 let has_rust_abi = match &func.sig.abi {
372 None => true,
373 Some(abi) => abi.name.as_ref().is_some_and(|name| name.value() == "Rust"),
374 };
375 if has_rust_abi {
376 func.attrs.push(syn::parse_quote!(#[track_caller]));
377 }
378 func.attrs.push(syn::parse_quote!(#[allow(non_snake_case)]));
379
380 let scope_label_ident = func.sig.ident.clone();
381 let original_block = func.block.clone();
382 let body_end = func.block.brace_token.span.close();
383 let composer_ident = Ident::new("__composer", Span::mixed_site());
384 let outer_composer_ident = Ident::new("__outer_composer", Span::mixed_site());
385 let caller_key_ident = Ident::new("__cranpose_caller_key", Span::mixed_site());
386 let current_scope_ident = Ident::new("__current_scope", Span::mixed_site());
387 let result_slot_index_ident = Ident::new("__result_slot_index", Span::mixed_site());
388 let has_previous_ident = Ident::new("__has_previous", Span::mixed_site());
389 let result_ident = Ident::new("__result", Span::mixed_site());
390 let value_ident = Ident::new("__value", Span::mixed_site());
391 let key_expr = quote! { #caller_key_ident };
392 let caller_key_stmt = definition_key_stmt(&core_path, &caller_key_ident, &scope_label_ident);
393
394 let rebinds_for_no_skip: Vec<_> = param_info
395 .iter()
396 .map(|info| {
397 let ident = &info.ident;
398 let pat = &info.pat;
399 quote! { let #pat = #ident; }
400 })
401 .collect();
402
403 let return_ty: syn::Type = match &func.sig.output {
404 ReturnType::Default => syn::parse_quote! { () },
405 ReturnType::Type(_, ty) => ty.as_ref().clone(),
406 };
407 let returns_unit = match &func.sig.output {
408 ReturnType::Default => true,
409 ReturnType::Type(_, ty) => {
410 matches!(ty.as_ref(), Type::Tuple(tuple) if tuple.elems.is_empty())
411 }
412 };
413 let invalidate_return_consumer = if returns_unit || is_node_id_return(&return_ty) {
414 quote! {}
415 } else {
416 quote! { #composer_ident.__invalidate_return_consumer_scope(); }
417 };
418 let _helper_ident = Ident::new(
419 &format!("__cranpose_impl_{}", func.sig.ident),
420 Span::mixed_site(),
421 );
422 let generics = func.sig.generics.clone();
423 let (_impl_generics, _ty_generics, _where_clause) = generics.split_for_impl();
424
425 let _helper_inputs: Vec<TokenStream2> = param_info
426 .iter()
427 .map(|info| {
428 let ident = &info.ident;
429 let ty = &info.ty;
430 quote! { #ident: #ty }
431 })
432 .collect();
433
434 let has_unhandled_impl_trait = param_info
435 .iter()
436 .any(|info| info.is_impl_trait && !is_zero_arg_fn_impl_trait(&info.ty));
437
438 if enable_skip && !has_unhandled_impl_trait {
439 let helper_ident = Ident::new(
440 &format!("__cranpose_impl_{}", func.sig.ident),
441 Span::mixed_site(),
442 );
443 let generics = func.sig.generics.clone();
444
445 let param_erased: Vec<bool> = param_info
446 .iter()
447 .map(|info| {
448 (info.is_impl_trait && is_zero_arg_fn_impl_trait(&info.ty))
449 || (!info.is_impl_trait
450 && type_bare_generic_ident(&info.ty).is_some()
451 && is_generic_fn_like(&info.ty, &generics))
452 })
453 .collect();
454
455 let mut strippable: std::collections::HashSet<String> = param_info
456 .iter()
457 .zip(¶m_erased)
458 .filter(|(info, erased)| **erased && !info.is_impl_trait)
459 .filter_map(|(info, _)| type_bare_generic_ident(&info.ty))
460 .map(Ident::to_string)
461 .collect();
462 loop {
463 use quote::ToTokens;
464 let mut used_elsewhere: Vec<TokenStream2> = Vec::new();
465 for (info, erased) in param_info.iter().zip(¶m_erased) {
466 if !*erased {
467 used_elsewhere.push(info.ty.to_token_stream());
468 }
469 }
470 used_elsewhere.push(return_ty.to_token_stream());
471 for param in &generics.params {
472 match param {
473 syn::GenericParam::Type(type_param) => {
474 if !strippable.contains(&type_param.ident.to_string()) {
475 used_elsewhere.push(type_param.bounds.to_token_stream());
476 if let Some((_, default)) = &type_param.default {
477 used_elsewhere.push(default.to_token_stream());
478 }
479 }
480 }
481 syn::GenericParam::Const(const_param) => {
482 used_elsewhere.push(const_param.ty.to_token_stream());
483 }
484 syn::GenericParam::Lifetime(_) => {}
485 }
486 }
487 if let Some(where_clause) = &generics.where_clause {
488 for predicate in &where_clause.predicates {
489 if let syn::WherePredicate::Type(pred) = predicate
490 && let Some(ident) = type_bare_generic_ident(&pred.bounded_ty)
491 && strippable.contains(&ident.to_string())
492 {
493 continue;
494 }
495 used_elsewhere.push(predicate.to_token_stream());
496 }
497 }
498 let before = strippable.len();
499 strippable.retain(|name| {
500 !used_elsewhere
501 .iter()
502 .any(|tokens| stream_mentions_ident(tokens, name))
503 });
504 if strippable.len() == before {
505 break;
506 }
507 }
508
509 let helper_generics = filter_generics(&generics, &strippable);
510 let (impl_generics, ty_generics, where_clause) = helper_generics.split_for_impl();
511 let ty_generics_turbofish = ty_generics.as_turbofish();
512
513 let helper_inputs: Vec<TokenStream2> = param_info
514 .iter()
515 .zip(¶m_erased)
516 .filter_map(|(info, erased)| {
517 if info.is_impl_trait && !is_zero_arg_fn_impl_trait(&info.ty) {
518 None
519 } else if *erased {
520 let ident = &info.ident;
521 Some(quote! { #ident: ::std::boxed::Box<dyn ::core::ops::FnMut() + 'static> })
522 } else {
523 let ident = &info.ident;
524 let ty = &info.ty;
525 Some(quote! { #ident: #ty })
526 }
527 })
528 .collect();
529
530 let param_state_slots: Vec<Ident> = (0..param_info.len())
531 .map(|index| Ident::new(&format!("__param_state_slot{index}"), Span::mixed_site()))
532 .collect();
533
534 let param_is_callback: Vec<bool> = param_info
535 .iter()
536 .map(|info| {
537 (info.is_impl_trait && is_zero_arg_fn_impl_trait(&info.ty))
538 || (!info.is_impl_trait && is_fn_param(&info.ty, &generics))
539 })
540 .collect();
541
542 let param_setup: Vec<TokenStream2> = param_info
543 .iter()
544 .zip(param_state_slots.iter())
545 .zip(¶m_erased)
546 .zip(¶m_is_callback)
547 .map(|(((info, slot_ident), erased), is_callback)| {
548 if *is_callback {
549 let ident = &info.ident;
550 let update = if *erased {
551 quote! { holder.update_boxed(#ident); }
552 } else {
553 quote! { holder.update(#ident); }
554 };
555 quote! {
556 let #slot_ident = #composer_ident
557 .__use_param_slot(|| #core_path::CallbackHolder::new());
558 #composer_ident.with_slot_value::<#core_path::CallbackHolder, _>(
559 #slot_ident,
560 |holder| {
561 #update
562 },
563 );
564 __changed = true;
565 }
566 } else if info.is_impl_trait {
567 quote! { __changed = true; }
568 } else {
569 let ident = &info.ident;
570 let ty = &info.ty;
571 let update = param_state_update(ident, ty);
572 quote! {
573 let #slot_ident = #composer_ident
574 .__use_param_slot(|| #core_path::ParamState::<#ty>::default());
575 if #composer_ident.with_slot_value_mut::<#core_path::ParamState<#ty>, _>(
576 #slot_ident,
577 |state| #update,
578 )
579 {
580 __changed = true;
581 }
582 }
583 }
584 })
585 .collect();
586
587 let param_setup_recompose: 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! {
594 let #slot_ident = #composer_ident
595 .__use_param_slot(|| #core_path::CallbackHolder::new());
596 }
597 } else if info.is_impl_trait {
598 quote! {}
599 } else {
600 let ty = &info.ty;
601 quote! {
602 let #slot_ident = #composer_ident
603 .__use_param_slot(|| #core_path::ParamState::<#ty>::default());
604 }
605 }
606 })
607 .collect();
608
609 let callback_rebinds: Vec<TokenStream2> = param_info
610 .iter()
611 .zip(param_state_slots.iter())
612 .zip(¶m_is_callback)
613 .filter(|(_, is_callback)| **is_callback)
614 .map(|((info, slot_ident), _)| {
615 let pat = &info.pat;
616 let can_add_mut = matches!(pat.as_ref(), Pat::Ident(_));
617 let binding = if can_add_mut && !info.pat_is_mut {
618 quote! { mut #pat }
619 } else {
620 quote! { #pat }
621 };
622 quote! {
623 #[allow(unused_mut)]
624 let #binding = #composer_ident
625 .with_slot_value::<#core_path::CallbackHolder, _>(
626 #slot_ident,
627 |holder| holder.clone_rc(),
628 );
629 }
630 })
631 .collect();
632
633 let body_inputs: 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! {
640 #slot_ident: #core_path::ValueSlotHandle<'_, #core_path::CallbackHolder>
641 }
642 } else {
643 let pat = &info.pat;
644 let ty = &info.ty;
645 quote! { #pat: #ty }
646 }
647 })
648 .collect();
649
650 let body_args: Vec<TokenStream2> = param_info
651 .iter()
652 .zip(param_state_slots.iter())
653 .zip(¶m_is_callback)
654 .map(|((info, slot_ident), is_callback)| {
655 if *is_callback {
656 quote! { #slot_ident }
657 } else {
658 let ident = &info.ident;
659 quote! { #ident }
660 }
661 })
662 .collect();
663
664 let reads_for_recompose: Vec<TokenStream2> = param_info
665 .iter()
666 .zip(param_state_slots.iter())
667 .zip(¶m_is_callback)
668 .filter(|(_, is_callback)| !**is_callback)
669 .map(|((info, slot_ident), _)| {
670 let ident = &info.ident;
671 let ty = &info.ty;
672 quote! {
673 let #ident = #composer_ident
674 .with_slot_value::<#core_path::ParamState<#ty>, _>(
675 #slot_ident,
676 |state| {
677 state
678 .value()
679 .expect("composable parameter missing for recomposition")
680 },
681 );
682 }
683 })
684 .collect();
685
686 let body_ident = Ident::new(
687 &format!("__cranpose_body_{}", func.sig.ident),
688 Span::mixed_site(),
689 );
690 let body_call = quote! {
691 #body_ident #ty_generics_turbofish (#composer_ident #(, #body_args)*)
692 };
693
694 let recompose_fn_ident = Ident::new(
695 &format!("__cranpose_recompose_{}", func.sig.ident),
696 Span::mixed_site(),
697 );
698
699 let recompose_setter = quote! {
700 {
701 #composer_ident.set_recompose_callback(move |
702 #composer_ident: &#core_path::Composer|
703 {
704 let _ = #recompose_fn_ident #ty_generics_turbofish (
705 #composer_ident
706 );
707 });
708 }
709 };
710
711 let helper_body = if returns_unit {
712 quote! {
713 #core_path::debug_label_current_scope(stringify!(#scope_label_ident));
714 let #current_scope_ident = #composer_ident
715 .current_recompose_scope()
716 .expect("missing recompose scope");
717 let mut __changed = #current_scope_ident.should_recompose();
718 #(#param_setup)*
719 #recompose_setter
720 if !__changed && #current_scope_ident.has_composed_once() {
721 #composer_ident.skip_current_group();
722 return;
723 }
724 #body_call
725 }
726 } else {
727 quote! {
728 #core_path::debug_label_current_scope(stringify!(#scope_label_ident));
729 let #current_scope_ident = #composer_ident
730 .current_recompose_scope()
731 .expect("missing recompose scope");
732 let mut __changed = #current_scope_ident.should_recompose();
733 #(#param_setup)*
734 #recompose_setter
735 let #result_slot_index_ident = #composer_ident
736 .__use_return_slot(|| #core_path::ReturnSlot::<#return_ty>::default());
737 let #has_previous_ident = #composer_ident
738 .with_slot_value::<#core_path::ReturnSlot<#return_ty>, _>(
739 #result_slot_index_ident,
740 |slot| slot.get().is_some(),
741 );
742 if !__changed && #has_previous_ident {
743 #composer_ident.skip_current_group();
744 let #result_ident = #composer_ident
745 .with_slot_value::<#core_path::ReturnSlot<#return_ty>, _>(
746 #result_slot_index_ident,
747 |slot| {
748 slot.get()
749 .expect("composable return value missing during skip")
750 },
751 );
752 return #result_ident;
753 }
754 let #value_ident: #return_ty = #body_call;
755 #composer_ident.with_slot_value_mut::<#core_path::ReturnSlot<#return_ty>, _>(
756 #result_slot_index_ident,
757 |slot| {
758 slot.store(#value_ident.clone());
759 },
760 );
761 #value_ident
762 }
763 };
764
765 let recompose_fn_body = if returns_unit {
766 quote! {
767 #(#param_setup_recompose)*
768 #(#reads_for_recompose)*
769 #body_call;
770 #recompose_setter
771 }
772 } else {
773 quote! {
774 #(#param_setup_recompose)*
775 let #result_slot_index_ident = #composer_ident
776 .__use_return_slot(|| #core_path::ReturnSlot::<#return_ty>::default());
777 #(#reads_for_recompose)*
778 let #value_ident: #return_ty = #body_call;
779 #composer_ident.with_slot_value_mut::<#core_path::ReturnSlot<#return_ty>, _>(
780 #result_slot_index_ident,
781 |slot| {
782 slot.store(#value_ident.clone());
783 },
784 );
785 #recompose_setter
786 #invalidate_return_consumer
787 #value_ident
788 }
789 };
790
791 let slot_origin = hot_slot_origin(&core_path, &func.sig.ident, body_end);
792 let recompose_fn = quote! {
793 #[allow(non_snake_case)]
794 fn #recompose_fn_ident #impl_generics (
795 #composer_ident: &#core_path::Composer
796 ) -> #return_ty #where_clause {
797 #slot_origin
798 #recompose_fn_body
799 }
800 };
801
802 let helper_fn = quote! {
803 #[allow(non_snake_case, clippy::too_many_arguments)]
804 fn #helper_ident #impl_generics (
805 #composer_ident: &#core_path::Composer
806 #(, #helper_inputs)*
807 ) -> #return_ty #where_clause {
808 #slot_origin
809 #helper_body
810 }
811 };
812
813 let body_fn = quote! {
814 #[allow(non_snake_case, clippy::too_many_arguments)]
815 fn #body_ident #impl_generics (
816 #composer_ident: &#core_path::Composer
817 #(, #body_inputs)*
818 ) -> #return_ty #where_clause {
819 #(#callback_rebinds)*
820 let _cranpose_source_scope = #core_path::__source_scope(
821 stringify!(#scope_label_ident), file!(), line!(), env!("CARGO_MANIFEST_DIR"),
822 );
823 #original_block
824 }
825 };
826
827 let wrapper_args: Vec<TokenStream2> = param_info
828 .iter()
829 .zip(¶m_erased)
830 .filter_map(|(info, erased)| {
831 if info.is_impl_trait && !is_zero_arg_fn_impl_trait(&info.ty) {
832 None
833 } else if *erased {
834 let ident = &info.ident;
835 Some(quote! { ::std::boxed::Box::new(#ident) })
836 } else {
837 let ident = &info.ident;
838 Some(quote! { #ident })
839 }
840 })
841 .collect();
842
843 let wrapped = quote!({
844 #caller_key_stmt
845 #core_path::with_current_composer(|#composer_ident: &#core_path::Composer| {
846 #composer_ident.with_group(#key_expr, |#composer_ident: &#core_path::Composer| {
847 #helper_ident(#composer_ident #(, #wrapper_args)*)
848 })
849 })
850 });
851 *func.block = syn::parse2(wrapped).expect("failed to build block");
852 TokenStream::from(quote! {
853 #body_fn
854 #recompose_fn
855 #helper_fn
856 #func
857 })
858 } else {
859 let wrapped = quote!({
860 #caller_key_stmt
861 #core_path::with_current_composer(|#outer_composer_ident: &#core_path::Composer| {
862 #outer_composer_ident.with_group(#key_expr, |#composer_ident: &#core_path::Composer| {
863 #core_path::debug_label_current_scope(stringify!(#scope_label_ident));
864 #(#rebinds_for_no_skip)*
865 let _cranpose_source_scope = #core_path::__source_scope(
866 stringify!(#scope_label_ident), file!(), line!(), env!("CARGO_MANIFEST_DIR"),
867 );
868 #original_block
869 })
870 })
871 });
872 *func.block = syn::parse2(wrapped).expect("failed to build block");
873 TokenStream::from(quote! { #func })
874 }
875}
876
877fn inject_groups(core_path: &TokenStream2, func: &mut ItemFn) {
880 let hot_reload = cfg!(feature = "hot-reload");
881 branch_groups::inject_branch_groups_with(
882 core_path,
883 &mut func.block,
884 &func.sig.ident,
885 hot_reload,
886 );
887 if hot_reload {
888 let end = func.block.brace_token.span.close();
889 let origin = branch_groups::hot_origin_stmt(core_path, &func.sig.ident, end);
890 func.block
891 .stmts
892 .insert(0, syn::parse2(origin).expect("hot origin statement"));
893 }
894}
895
896fn hot_slot_origin(core_path: &TokenStream2, name: &Ident, end: Span) -> TokenStream2 {
900 if cfg!(feature = "hot-reload") {
901 branch_groups::hot_origin_stmt(core_path, name, end)
902 } else {
903 TokenStream2::new()
904 }
905}
906
907fn find_reserved_pattern_ident(pat: &Pat) -> Option<&Ident> {
908 use syn::visit::Visit;
909
910 struct Scan<'ast> {
911 found: Option<&'ast Ident>,
912 }
913 impl<'ast> syn::visit::Visit<'ast> for Scan<'ast> {
914 fn visit_pat_ident(&mut self, node: &'ast syn::PatIdent) {
915 if self.found.is_none() {
916 let name = node.ident.to_string();
917 if name == "__composer" || name.starts_with("__cranpose") {
918 self.found = Some(&node.ident);
919 }
920 }
921 syn::visit::visit_pat_ident(self, node);
922 }
923 }
924 let mut scan = Scan { found: None };
925 scan.visit_pat(pat);
926 scan.found
927}
928
929#[cfg(test)]
930#[path = "tests/macros_tests.rs"]
931mod tests;