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