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