1#![deny(missing_docs)]
2#![doc = include_str!("../README.md")]
3
4use proc_macro::TokenStream;
5use proc_macro_crate::{FoundCrate, crate_name};
6use proc_macro2::{Span, TokenStream as TokenStream2};
7use quote::quote;
8use syn::{FnArg, Ident, ItemFn, Pat, PatType, ReturnType, Type, parse_macro_input};
9
10mod branch_groups;
11#[cfg(feature = "localization")]
12mod localization;
13mod preview;
14
15#[cfg(feature = "localization")]
19#[proc_macro]
20pub fn tr(input: TokenStream) -> TokenStream {
21 localization::translate(input.into())
22 .unwrap_or_else(|error| error.to_compile_error())
23 .into()
24}
25
26#[cfg(feature = "localization")]
29#[proc_macro]
30pub fn translations(input: TokenStream) -> TokenStream {
31 localization::catalogs(input.into())
32 .unwrap_or_else(|error| error.to_compile_error())
33 .into()
34}
35
36#[cfg(feature = "localization")]
39#[proc_macro]
40pub fn message(input: TokenStream) -> TokenStream {
41 localization::message(input.into())
42 .unwrap_or_else(|error| error.to_compile_error())
43 .into()
44}
45
46#[cfg(feature = "localization")]
49#[proc_macro]
50pub fn translation_messages(input: TokenStream) -> TokenStream {
51 localization::bindings(input.into())
52 .unwrap_or_else(|error| error.to_compile_error())
53 .into()
54}
55#[cfg(test)]
56#[path = "tests/preview_tests.rs"]
57mod preview_tests;
58
59#[proc_macro_attribute]
64pub fn preview(args: TokenStream, item: TokenStream) -> TokenStream {
65 let function = parse_macro_input!(item as ItemFn);
66 preview::expand(args.into(), function)
67 .unwrap_or_else(|error| error.to_compile_error())
68 .into()
69}
70
71fn is_fn_like_type(ty: &Type) -> bool {
72 match ty {
73 Type::ImplTrait(impl_trait) => impl_trait.bounds.iter().any(|bound| {
74 if let syn::TypeParamBound::Trait(trait_bound) = bound {
75 let path = &trait_bound.path;
76 if let Some(segment) = path.segments.last() {
77 let ident_str = segment.ident.to_string();
78 return ident_str == "FnMut" || ident_str == "Fn" || ident_str == "FnOnce";
79 }
80 }
81 false
82 }),
83 Type::Path(type_path) => {
84 if let Some(segment) = type_path.path.segments.last()
85 && segment.ident == "Box"
86 && let syn::PathArguments::AngleBracketed(args) = &segment.arguments
87 && let Some(syn::GenericArgument::Type(Type::TraitObject(trait_obj))) =
88 args.args.first()
89 {
90 return trait_obj.bounds.iter().any(|bound| {
91 if let syn::TypeParamBound::Trait(trait_bound) = bound {
92 let path = &trait_bound.path;
93 if let Some(segment) = path.segments.last() {
94 let ident_str = segment.ident.to_string();
95 return ident_str == "FnMut"
96 || ident_str == "Fn"
97 || ident_str == "FnOnce";
98 }
99 }
100 false
101 });
102 }
103 false
104 }
105 Type::FnPtr(_) => true,
106 _ => false,
107 }
108}
109
110fn is_generic_fn_like(ty: &Type, generics: &syn::Generics) -> bool {
111 let type_ident = match ty {
112 Type::Path(type_path) if type_path.path.segments.len() == 1 => {
113 &type_path.path.segments[0].ident
114 }
115 _ => return false,
116 };
117
118 for param in &generics.params {
119 if let syn::GenericParam::Type(type_param) = param
120 && type_param.ident == *type_ident
121 {
122 for bound in &type_param.bounds {
123 if let syn::TypeParamBound::Trait(trait_bound) = bound
124 && let Some(segment) = trait_bound.path.segments.last()
125 {
126 let ident_str = segment.ident.to_string();
127 if ident_str == "FnMut" || ident_str == "Fn" || ident_str == "FnOnce" {
128 return true;
129 }
130 }
131 }
132 }
133 }
134
135 if let Some(where_clause) = &generics.where_clause {
136 for predicate in &where_clause.predicates {
137 if let syn::WherePredicate::Type(pred) = predicate
138 && let Type::Path(bounded_type) = &pred.bounded_ty
139 && bounded_type.path.segments.len() == 1
140 && bounded_type.path.segments[0].ident == *type_ident
141 {
142 for bound in &pred.bounds {
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 == "FnMut" || ident_str == "Fn" || ident_str == "FnOnce" {
148 return true;
149 }
150 }
151 }
152 }
153 }
154 }
155
156 false
157}
158
159struct PackedParams<'a> {
163 params: Vec<(&'a Ident, &'a Type)>,
164 returns: Option<&'a Type>,
167}
168
169impl PackedParams<'_> {
170 fn slot() -> Ident {
171 Ident::new("__params_slot", Span::mixed_site())
172 }
173
174 fn holds_return(&self) -> bool {
176 self.returns.is_some() && !self.params.is_empty()
177 }
178
179 fn state_type(&self, core_path: &TokenStream2) -> TokenStream2 {
180 let types = self.params.iter().map(|(_, ty)| ty);
181 match self.returns {
182 Some(returns) => quote! { #core_path::ParamReturnState<(#(#types,)*), #returns> },
183 None => quote! { #core_path::ParamState<(#(#types,)*)> },
184 }
185 }
186
187 fn slot_stmt(&self, core_path: &TokenStream2, composer: &Ident) -> TokenStream2 {
188 let state = self.state_type(core_path);
189 let slot = Self::slot();
190 let key = slot_key(core_path);
191 quote! {
192 let #slot = #composer.__use_param_slot(#key, || <#state>::default());
193 }
194 }
195
196 fn with_setup(
201 &self,
202 core_path: &TokenStream2,
203 composer: &Ident,
204 (previous, skippable): (&Ident, bool),
205 mut setup: Vec<TokenStream2>,
206 ) -> Vec<TokenStream2> {
207 if self.params.is_empty() {
208 return setup;
209 }
210 let state = self.state_type(core_path);
211 let slot = Self::slot();
212 let key = slot_key(core_path);
213 let param_state = Ident::new("__param_state", Span::mixed_site());
214 let stored = Ident::new("__stored", Span::mixed_site());
215 let idents = self.params.iter().map(|(ident, _)| ident);
216 let refreshes =
217 self.params.iter().enumerate().map(|(index, (ident, ty))| {
218 param_field_refresh(core_path, &stored, ident, ty, index)
219 });
220 let update = quote! {
221 #param_state.update_fields(
222 || (#(::core::clone::Clone::clone(&#idents),)*),
223 |#stored| false #(| #refreshes)*,
224 )
225 };
226 let statement = if !self.holds_return() {
227 quote! {
228 let (_, #slot) = #composer.__update_param_slot(
229 #key,
230 || <#state>::default(),
231 |#param_state: &mut #state| #update,
232 );
233 if #slot {
234 __changed = true;
235 }
236 }
237 } else if skippable {
238 quote! {
239 let (#slot, #previous) = #composer.__update_param_slot(
240 #key,
241 || <#state>::default(),
242 |#param_state: &mut #state| {
243 if #update {
244 __changed = true;
245 }
246 (!__changed).then(|| #param_state.returned()).flatten()
247 },
248 );
249 }
250 } else {
251 quote! {
252 let (#slot, ()) = #composer.__update_param_slot(
253 #key,
254 || <#state>::default(),
255 |#param_state: &mut #state| {
256 if #update {
257 __changed = true;
258 }
259 },
260 );
261 }
262 };
263 setup.insert(0, statement);
264 setup
265 }
266
267 fn store_return(
270 &self,
271 core_path: &TokenStream2,
272 composer: &Ident,
273 value: &Ident,
274 ) -> TokenStream2 {
275 let state = self.state_type(core_path);
276 let slot = Self::slot();
277 quote! {
278 #composer.with_slot_value_mut::<#state, _>(#slot, |state| {
279 state.store_return(::core::clone::Clone::clone(&#value));
280 });
281 }
282 }
283
284 fn with_slot(
286 &self,
287 core_path: &TokenStream2,
288 composer: &Ident,
289 mut setup: Vec<TokenStream2>,
290 ) -> Vec<TokenStream2> {
291 if !self.params.is_empty() {
292 setup.insert(0, self.slot_stmt(core_path, composer));
293 }
294 setup
295 }
296
297 fn reads(&self, core_path: &TokenStream2, composer: &Ident) -> Vec<TokenStream2> {
300 if self.params.is_empty() {
301 return Vec::new();
302 }
303 let state = self.state_type(core_path);
304 let slot = Self::slot();
305 let param_state = Ident::new("__param_state", Span::mixed_site());
306 let idents = self.params.iter().map(|(ident, _)| ident);
307 vec![quote! {
308 let (#(#idents,)*) = #composer.with_slot_value::<#state, _>(#slot, |#param_state| {
309 #param_state
310 .value()
311 .expect("composable parameter missing for recomposition")
312 });
313 }]
314 }
315}
316
317struct ParamInfo {
319 ident: Ident,
320 pat: Box<Pat>,
321 ty: Type,
322 pat_is_mut: bool,
323 is_impl_trait: bool,
324}
325
326fn every_param_compared(param_info: &[ParamInfo], param_is_callback: &[bool]) -> bool {
329 param_info
330 .iter()
331 .zip(param_is_callback)
332 .all(|(info, is_callback)| !*is_callback && !info.is_impl_trait)
333}
334
335struct ReturningCall<'a> {
339 core_path: &'a TokenStream2,
340 composer: &'a Ident,
341 group: &'a Ident,
342 current_scope: &'a Ident,
343 scope_label: &'a Ident,
344 return_ty: &'a Type,
345 slot_key: &'a TokenStream2,
346 packed: &'a PackedParams<'a>,
347 skippable: bool,
348 body_call: &'a TokenStream2,
349}
350
351impl ReturningCall<'_> {
352 fn previous() -> Ident {
354 Ident::new("__previous", Span::mixed_site())
355 }
356
357 fn value() -> Ident {
358 Ident::new("__value", Span::mixed_site())
359 }
360
361 fn return_slot() -> Ident {
362 Ident::new("__result_slot_index", Span::mixed_site())
363 }
364
365 fn slot_type(&self) -> TokenStream2 {
366 let (core_path, return_ty) = (self.core_path, self.return_ty);
367 quote! { #core_path::ReturnSlot<#return_ty> }
368 }
369
370 fn skip(&self) -> Option<TokenStream2> {
372 let (group, previous) = (self.group, Self::previous());
373 let result = Ident::new("__result", Span::mixed_site());
374 (self.skippable || !self.packed.holds_return()).then(|| {
375 quote! {
376 if let ::core::option::Option::Some(#result) = #previous {
377 #group.skip();
378 return #result;
379 }
380 }
381 })
382 }
383
384 fn store(&self) -> TokenStream2 {
386 let (composer, value) = (self.composer, Self::value());
387 if self.packed.holds_return() {
388 return self.packed.store_return(self.core_path, composer, &value);
389 }
390 let (slot, slot_type) = (Self::return_slot(), self.slot_type());
391 quote! {
392 #composer.with_slot_value_mut::<#slot_type, _>(#slot, |slot| {
393 slot.store(::core::clone::Clone::clone(&#value));
394 });
395 }
396 }
397
398 fn helper_body(
399 &self,
400 param_setup: &[TokenStream2],
401 recompose_setter: &TokenStream2,
402 ) -> TokenStream2 {
403 let (core_path, composer, scope_label) = (self.core_path, self.composer, self.scope_label);
404 let (current_scope, return_ty, body_call) =
405 (self.current_scope, self.return_ty, self.body_call);
406 let (previous, value, slot) = (Self::previous(), Self::value(), Self::return_slot());
407 let lookup = (!self.packed.holds_return()).then(|| {
408 let (slot_key, slot_type) = (self.slot_key, self.slot_type());
409 quote! {
410 let (#slot, #previous) = #composer.__update_return_slot(
411 #slot_key,
412 || <#slot_type>::default(),
413 |slot: &mut #slot_type| (!__changed).then(|| slot.get()).flatten(),
414 );
415 }
416 });
417 let (skip, store) = (self.skip(), self.store());
418 quote! {
419 #core_path::debug_label_current_scope(stringify!(#scope_label));
420 let mut __changed = #current_scope.should_recompose();
421 #(#param_setup)*
422 #recompose_setter
423 #lookup
424 #skip
425 let #value: #return_ty = #body_call;
426 #store
427 #value
428 }
429 }
430
431 fn recompose_body(
432 &self,
433 param_setup: &[TokenStream2],
434 reads: &[TokenStream2],
435 invalidate_return_consumer: &TokenStream2,
436 ) -> TokenStream2 {
437 let (composer, return_ty, body_call) = (self.composer, self.return_ty, self.body_call);
438 let lookup = (!self.packed.holds_return()).then(|| {
439 let (slot, slot_type) = (Self::return_slot(), self.slot_type());
440 quote! {
441 let #slot = #composer.__use_return_slot(|| <#slot_type>::default());
442 }
443 });
444 let (value, store) = (Self::value(), self.store());
445 quote! {
446 #(#param_setup)*
447 #lookup
448 #(#reads)*
449 let #value: #return_ty = #body_call;
450 #store
451 #invalidate_return_consumer
452 #value
453 }
454 }
455}
456
457fn param_field_refresh(
462 core_path: &TokenStream2,
463 stored: &Ident,
464 ident: &Ident,
465 ty: &Type,
466 index: usize,
467) -> TokenStream2 {
468 let index = syn::Index::from(index);
469 if is_shared_pointer(ty) {
470 quote! { #core_path::refresh_shared_param(&mut #stored.#index, &#ident) }
471 } else {
472 quote! { #core_path::refresh_param(&mut #stored.#index, &#ident) }
473 }
474}
475
476fn is_shared_pointer(ty: &Type) -> bool {
478 match ty {
479 Type::Path(path) if path.qself.is_none() => path
480 .path
481 .segments
482 .last()
483 .is_some_and(|segment| segment.ident == "Rc" || segment.ident == "Arc"),
484 _ => false,
485 }
486}
487
488fn is_fn_param(ty: &Type, generics: &syn::Generics) -> bool {
489 is_fn_like_type(ty) || is_generic_fn_like(ty, generics)
490}
491
492fn is_zero_arg_fn_impl_trait(ty: &Type) -> bool {
493 if let Type::ImplTrait(impl_trait) = ty {
494 impl_trait.bounds.iter().any(|bound| {
495 if let syn::TypeParamBound::Trait(trait_bound) = bound
496 && let Some(segment) = trait_bound.path.segments.last()
497 {
498 let ident_str = segment.ident.to_string();
499 if (ident_str == "Fn" || ident_str == "FnMut")
500 && let syn::PathArguments::Parenthesized(args) = &segment.arguments
501 {
502 let returns_unit = match &args.output {
503 ReturnType::Default => true,
504 ReturnType::Type(_, ty) => {
505 matches!(&**ty, Type::Tuple(tuple) if tuple.elems.is_empty())
506 }
507 };
508 return args.inputs.is_empty() && returns_unit;
509 }
510 }
511 false
512 })
513 } else {
514 false
515 }
516}
517
518fn type_bare_generic_ident(ty: &Type) -> Option<&Ident> {
519 match ty {
520 Type::Path(type_path)
521 if type_path.qself.is_none()
522 && type_path.path.segments.len() == 1
523 && type_path.path.segments[0].arguments.is_none() =>
524 {
525 Some(&type_path.path.segments[0].ident)
526 }
527 _ => None,
528 }
529}
530
531fn stream_mentions_ident(tokens: &TokenStream2, name: &str) -> bool {
532 tokens.clone().into_iter().any(|tt| match tt {
533 proc_macro2::TokenTree::Ident(ident) => ident == name,
534 proc_macro2::TokenTree::Group(group) => stream_mentions_ident(&group.stream(), name),
535 _ => false,
536 })
537}
538
539fn filter_generics(
540 generics: &syn::Generics,
541 strip: &std::collections::HashSet<String>,
542) -> syn::Generics {
543 let mut filtered = generics.clone();
544 filtered.params = filtered
545 .params
546 .into_iter()
547 .filter(|param| match param {
548 syn::GenericParam::Type(type_param) => !strip.contains(&type_param.ident.to_string()),
549 _ => true,
550 })
551 .collect();
552 if let Some(where_clause) = &mut filtered.where_clause {
553 where_clause.predicates = where_clause
554 .predicates
555 .clone()
556 .into_iter()
557 .filter(|predicate| {
558 if let syn::WherePredicate::Type(pred) = predicate
559 && let Some(ident) = type_bare_generic_ident(&pred.bounded_ty)
560 {
561 return !strip.contains(&ident.to_string());
562 }
563 true
564 })
565 .collect();
566 if where_clause.predicates.is_empty() {
567 filtered.where_clause = None;
568 }
569 }
570 filtered
571}
572
573fn is_node_id_return(ty: &Type) -> bool {
574 matches!(
575 ty,
576 Type::Path(type_path)
577 if type_path
578 .path
579 .segments
580 .last()
581 .is_some_and(|segment| segment.ident == "NodeId")
582 )
583}
584
585fn core_crate_path() -> TokenStream2 {
586 let crate_name = crate_name("cranpose")
587 .ok()
588 .or_else(|| crate_name("cranpose-core").ok());
589
590 match crate_name {
591 Some(FoundCrate::Itself) => quote!(crate),
592 Some(FoundCrate::Name(name)) => {
593 let ident = Ident::new(&name, Span::call_site());
594 quote!(#ident)
595 }
596 None => quote!(cranpose_core),
597 }
598}
599
600fn slot_key(core_path: &TokenStream2) -> TokenStream2 {
604 if cfg!(feature = "hot-reload") {
605 return quote! { #core_path::caller_location_key() };
606 }
607 quote! {{
608 const __CRANPOSE_SLOT_KEY: #core_path::Key =
609 #core_path::const_location_key(file!(), line!(), column!());
610 #core_path::noted_location_key(__CRANPOSE_SLOT_KEY, file!(), line!(), column!())
611 }}
612}
613
614fn definition_key_stmt(
615 core_path: &TokenStream2,
616 caller_key_ident: &Ident,
617 name: &Ident,
618) -> TokenStream2 {
619 if cfg!(feature = "hot-reload") {
620 let name = name.to_string();
623 return quote! {
624 let #caller_key_ident = #core_path::composable_identity_key({
625 const __CRANPOSE_DEFINITION_KEY: #core_path::Key =
626 #core_path::hot_definition_key(file!(), module_path!(), #name);
627 __CRANPOSE_DEFINITION_KEY
628 });
629 };
630 }
631 quote! {
632 let #caller_key_ident = #core_path::composable_identity_key({
633 struct __CranposeDefinitionMarker;
634 static __CRANPOSE_DEFINITION_KEY: ::std::sync::OnceLock<#core_path::Key> =
635 ::std::sync::OnceLock::new();
636 #core_path::cached_composable_definition_key(
637 &__CRANPOSE_DEFINITION_KEY,
638 file!(),
639 line!(),
640 column!(),
641 ::std::any::TypeId::of::<__CranposeDefinitionMarker>(),
642 )
643 });
644 }
645}
646
647#[proc_macro_attribute]
655pub fn composable(attr: TokenStream, item: TokenStream) -> TokenStream {
656 let attr_tokens = TokenStream2::from(attr);
657 let mut enable_skip = true;
658 let core_path = core_crate_path();
659 if !attr_tokens.is_empty() {
660 match syn::parse2::<Ident>(attr_tokens) {
661 Ok(ident) if ident == "no_skip" => enable_skip = false,
662 Ok(other) => {
663 return syn::Error::new_spanned(other, "unsupported composable attribute")
664 .to_compile_error()
665 .into();
666 }
667 Err(err) => {
668 return err.to_compile_error().into();
669 }
670 }
671 }
672
673 let mut func = parse_macro_input!(item as ItemFn);
674
675 let mut param_info: Vec<ParamInfo> = Vec::new();
676
677 for (index, arg) in func.sig.inputs.iter_mut().enumerate() {
678 if let FnArg::Typed(PatType { pat, ty, .. }) = arg {
679 if let Some(reserved) = find_reserved_pattern_ident(pat) {
680 let name = reserved.to_string();
681 return syn::Error::new(
682 reserved.span(),
683 format!("`{name}` is reserved by #[composable]"),
684 )
685 .to_compile_error()
686 .into();
687 }
688 let pat_is_mut = matches!(
689 pat.as_ref(),
690 Pat::Ident(pat_ident) if pat_ident.mutability.is_some()
691 );
692 let is_impl_trait = matches!(**ty, Type::ImplTrait(_));
693
694 if is_impl_trait {
695 let original_pat: Box<Pat> = pat.clone();
696 if let Pat::Ident(pat_ident) = &**pat {
697 param_info.push(ParamInfo {
698 ident: pat_ident.ident.clone(),
699 pat: original_pat,
700 ty: ty.as_ref().clone(),
701 pat_is_mut,
702 is_impl_trait: true,
703 });
704 } else {
705 param_info.push(ParamInfo {
706 ident: Ident::new(&format!("__arg{index}"), Span::mixed_site()),
707 pat: original_pat,
708 ty: ty.as_ref().clone(),
709 pat_is_mut,
710 is_impl_trait: true,
711 });
712 }
713 } else {
714 let ident = Ident::new(&format!("__arg{index}"), Span::mixed_site());
715 let original_pat: Box<Pat> = pat.clone();
716 **pat = syn::parse_quote! { #ident };
717 param_info.push(ParamInfo {
718 ident,
719 pat: original_pat,
720 ty: ty.as_ref().clone(),
721 pat_is_mut,
722 is_impl_trait: false,
723 });
724 }
725 }
726 }
727
728 inject_groups(&core_path, &mut func);
729 let has_rust_abi = match &func.sig.abi {
730 None => true,
731 Some(abi) => abi.name.as_ref().is_some_and(|name| name.value() == "Rust"),
732 };
733 if has_rust_abi {
734 func.attrs.push(syn::parse_quote!(#[track_caller]));
735 }
736 func.attrs.push(syn::parse_quote!(#[allow(non_snake_case)]));
737
738 let scope_label_ident = func.sig.ident.clone();
739 let original_block = func.block.clone();
740 let body_end = func.block.brace_token.span.close();
741 let composer_ident = Ident::new("__composer", Span::mixed_site());
742 let slot_key = slot_key(&core_path);
743 let outer_composer_ident = Ident::new("__outer_composer", Span::mixed_site());
744 let caller_key_ident = Ident::new("__cranpose_caller_key", Span::mixed_site());
745 let current_scope_ident = Ident::new("__current_scope", Span::mixed_site());
746 let result_ident = Ident::new("__result", Span::mixed_site());
747 let group_ident = Ident::new("__cranpose_group", Span::mixed_site());
748 let key_expr = quote! { #caller_key_ident };
749 let caller_key_stmt = definition_key_stmt(&core_path, &caller_key_ident, &scope_label_ident);
750
751 let rebinds_for_no_skip: Vec<_> = param_info
752 .iter()
753 .map(|info| {
754 let ident = &info.ident;
755 let pat = &info.pat;
756 quote! { let #pat = #ident; }
757 })
758 .collect();
759
760 let return_ty: syn::Type = match &func.sig.output {
761 ReturnType::Default => syn::parse_quote! { () },
762 ReturnType::Type(_, ty) => ty.as_ref().clone(),
763 };
764 let returns_unit = match &func.sig.output {
765 ReturnType::Default => true,
766 ReturnType::Type(_, ty) => {
767 matches!(ty.as_ref(), Type::Tuple(tuple) if tuple.elems.is_empty())
768 }
769 };
770 let invalidate_return_consumer = if returns_unit || is_node_id_return(&return_ty) {
771 quote! {}
772 } else {
773 quote! { #composer_ident.__invalidate_return_consumer_scope(); }
774 };
775 let _helper_ident = Ident::new(
776 &format!("__cranpose_impl_{}", func.sig.ident),
777 Span::mixed_site(),
778 );
779 let generics = func.sig.generics.clone();
780 let (_impl_generics, _ty_generics, _where_clause) = generics.split_for_impl();
781
782 let _helper_inputs: Vec<TokenStream2> = param_info
783 .iter()
784 .map(|info| {
785 let ident = &info.ident;
786 let ty = &info.ty;
787 quote! { #ident: #ty }
788 })
789 .collect();
790
791 let has_unhandled_impl_trait = param_info
792 .iter()
793 .any(|info| info.is_impl_trait && !is_zero_arg_fn_impl_trait(&info.ty));
794
795 if enable_skip && !has_unhandled_impl_trait {
796 let helper_ident = Ident::new(
797 &format!("__cranpose_impl_{}", func.sig.ident),
798 Span::mixed_site(),
799 );
800 let generics = func.sig.generics.clone();
801
802 let param_erased: Vec<bool> = param_info
803 .iter()
804 .map(|info| {
805 (info.is_impl_trait && is_zero_arg_fn_impl_trait(&info.ty))
806 || (!info.is_impl_trait
807 && type_bare_generic_ident(&info.ty).is_some()
808 && is_generic_fn_like(&info.ty, &generics))
809 })
810 .collect();
811
812 let mut strippable: std::collections::HashSet<String> = param_info
813 .iter()
814 .zip(¶m_erased)
815 .filter(|(info, erased)| **erased && !info.is_impl_trait)
816 .filter_map(|(info, _)| type_bare_generic_ident(&info.ty))
817 .map(Ident::to_string)
818 .collect();
819 loop {
820 use quote::ToTokens;
821 let mut used_elsewhere: Vec<TokenStream2> = Vec::new();
822 for (info, erased) in param_info.iter().zip(¶m_erased) {
823 if !*erased {
824 used_elsewhere.push(info.ty.to_token_stream());
825 }
826 }
827 used_elsewhere.push(return_ty.to_token_stream());
828 for param in &generics.params {
829 match param {
830 syn::GenericParam::Type(type_param) => {
831 if !strippable.contains(&type_param.ident.to_string()) {
832 used_elsewhere.push(type_param.bounds.to_token_stream());
833 if let Some((_, default)) = &type_param.default {
834 used_elsewhere.push(default.to_token_stream());
835 }
836 }
837 }
838 syn::GenericParam::Const(const_param) => {
839 used_elsewhere.push(const_param.ty.to_token_stream());
840 }
841 syn::GenericParam::Lifetime(_) => {}
842 }
843 }
844 if let Some(where_clause) = &generics.where_clause {
845 for predicate in &where_clause.predicates {
846 if let syn::WherePredicate::Type(pred) = predicate
847 && let Some(ident) = type_bare_generic_ident(&pred.bounded_ty)
848 && strippable.contains(&ident.to_string())
849 {
850 continue;
851 }
852 used_elsewhere.push(predicate.to_token_stream());
853 }
854 }
855 let before = strippable.len();
856 strippable.retain(|name| {
857 !used_elsewhere
858 .iter()
859 .any(|tokens| stream_mentions_ident(tokens, name))
860 });
861 if strippable.len() == before {
862 break;
863 }
864 }
865
866 let helper_generics = filter_generics(&generics, &strippable);
867 let (impl_generics, ty_generics, where_clause) = helper_generics.split_for_impl();
868 let ty_generics_turbofish = ty_generics.as_turbofish();
869
870 let helper_inputs: Vec<TokenStream2> = param_info
871 .iter()
872 .zip(¶m_erased)
873 .filter_map(|(info, erased)| {
874 if info.is_impl_trait && !is_zero_arg_fn_impl_trait(&info.ty) {
875 None
876 } else if *erased {
877 let ident = &info.ident;
878 Some(quote! { #ident: ::std::boxed::Box<dyn ::core::ops::FnMut() + 'static> })
879 } else {
880 let ident = &info.ident;
881 let ty = &info.ty;
882 Some(quote! { #ident: #ty })
883 }
884 })
885 .collect();
886
887 let param_state_slots: Vec<Ident> = (0..param_info.len())
888 .map(|index| Ident::new(&format!("__param_state_slot{index}"), Span::mixed_site()))
889 .collect();
890
891 let param_is_callback: Vec<bool> = param_info
892 .iter()
893 .map(|info| {
894 (info.is_impl_trait && is_zero_arg_fn_impl_trait(&info.ty))
895 || (!info.is_impl_trait && is_fn_param(&info.ty, &generics))
896 })
897 .collect();
898
899 let param_setup: Vec<TokenStream2> = param_info
900 .iter()
901 .zip(param_state_slots.iter())
902 .zip(¶m_erased)
903 .zip(¶m_is_callback)
904 .map(|(((info, slot_ident), erased), is_callback)| {
905 if *is_callback {
906 let ident = &info.ident;
907 let update = if *erased {
908 quote! { holder.update_boxed(#ident); }
909 } else {
910 quote! { holder.update(#ident); }
911 };
912 quote! {
913 let (#slot_ident, ()) = #composer_ident.__update_param_slot(
914 #slot_key,
915 || #core_path::CallbackHolder::new(),
916 |holder: &mut #core_path::CallbackHolder| {
917 #update
918 },
919 );
920 __changed = true;
921 }
922 } else if info.is_impl_trait {
923 quote! { __changed = true; }
924 } else {
925 quote! {}
926 }
927 })
928 .collect();
929
930 let param_setup_recompose: Vec<TokenStream2> = param_state_slots
931 .iter()
932 .zip(¶m_is_callback)
933 .filter(|(_, is_callback)| **is_callback)
934 .map(|(slot_ident, _)| {
935 quote! {
936 let #slot_ident = #composer_ident
937 .__use_param_slot(#slot_key, || #core_path::CallbackHolder::new());
938 }
939 })
940 .collect();
941
942 let callback_rebinds: Vec<TokenStream2> = param_info
943 .iter()
944 .zip(param_state_slots.iter())
945 .zip(¶m_is_callback)
946 .filter(|(_, is_callback)| **is_callback)
947 .map(|((info, slot_ident), _)| {
948 let pat = &info.pat;
949 let can_add_mut = matches!(pat.as_ref(), Pat::Ident(_));
950 let binding = if can_add_mut && !info.pat_is_mut {
951 quote! { mut #pat }
952 } else {
953 quote! { #pat }
954 };
955 quote! {
956 #[allow(unused_mut)]
957 let #binding = #composer_ident
958 .with_slot_value::<#core_path::CallbackHolder, _>(
959 #slot_ident,
960 |holder| holder.clone_rc(),
961 );
962 }
963 })
964 .collect();
965
966 let body_inputs: Vec<TokenStream2> = param_info
967 .iter()
968 .zip(param_state_slots.iter())
969 .zip(¶m_is_callback)
970 .map(|((info, slot_ident), is_callback)| {
971 if *is_callback {
972 quote! {
973 #slot_ident: #core_path::ValueSlotHandle<'_, #core_path::CallbackHolder>
974 }
975 } else {
976 let pat = &info.pat;
977 let ty = &info.ty;
978 quote! { #pat: #ty }
979 }
980 })
981 .collect();
982
983 let body_args: Vec<TokenStream2> = param_info
984 .iter()
985 .zip(param_state_slots.iter())
986 .zip(¶m_is_callback)
987 .map(|((info, slot_ident), is_callback)| {
988 if *is_callback {
989 quote! { #slot_ident }
990 } else {
991 let ident = &info.ident;
992 quote! { #ident }
993 }
994 })
995 .collect();
996
997 let packed = PackedParams {
998 params: param_info
999 .iter()
1000 .zip(¶m_is_callback)
1001 .filter(|(info, is_callback)| !**is_callback && !info.is_impl_trait)
1002 .map(|(info, _)| (&info.ident, &info.ty))
1003 .collect(),
1004 returns: (!returns_unit).then_some(&return_ty),
1005 };
1006 let skippable = every_param_compared(¶m_info, ¶m_is_callback);
1007 let param_setup = packed.with_setup(
1008 &core_path,
1009 &composer_ident,
1010 (&ReturningCall::previous(), skippable),
1011 param_setup,
1012 );
1013 let param_setup_recompose =
1014 packed.with_slot(&core_path, &composer_ident, param_setup_recompose);
1015 let reads_for_recompose = packed.reads(&core_path, &composer_ident);
1016
1017 let body_ident = Ident::new(
1018 &format!("__cranpose_body_{}", func.sig.ident),
1019 Span::mixed_site(),
1020 );
1021 let body_call = quote! {
1022 #body_ident #ty_generics_turbofish (#composer_ident #(, #body_args)*)
1023 };
1024
1025 let recompose_fn_ident = Ident::new(
1026 &format!("__cranpose_recompose_{}", func.sig.ident),
1027 Span::mixed_site(),
1028 );
1029
1030 let recompose_setter = quote! {
1031 {
1032 #composer_ident.set_recompose_callback(move |
1033 #composer_ident: &#core_path::Composer|
1034 {
1035 let _ = #recompose_fn_ident #ty_generics_turbofish (
1036 #composer_ident
1037 );
1038 });
1039 }
1040 };
1041
1042 let returning = ReturningCall {
1043 core_path: &core_path,
1044 composer: &composer_ident,
1045 group: &group_ident,
1046 current_scope: ¤t_scope_ident,
1047 scope_label: &scope_label_ident,
1048 return_ty: &return_ty,
1049 slot_key: &slot_key,
1050 packed: &packed,
1051 skippable,
1052 body_call: &body_call,
1053 };
1054 let helper_body = if returns_unit {
1055 quote! {
1056 #core_path::debug_label_current_scope(stringify!(#scope_label_ident));
1057 let mut __changed = #current_scope_ident.should_recompose();
1058 #(#param_setup)*
1059 #recompose_setter
1060 if !__changed && #current_scope_ident.has_composed_once() {
1061 #group_ident.skip();
1062 return;
1063 }
1064 #body_call
1065 }
1066 } else {
1067 returning.helper_body(¶m_setup, &recompose_setter)
1068 };
1069
1070 let recompose_fn_body = if returns_unit {
1071 quote! {
1072 #(#param_setup_recompose)*
1073 #(#reads_for_recompose)*
1074 #body_call;
1075 }
1076 } else {
1077 returning.recompose_body(
1078 ¶m_setup_recompose,
1079 &reads_for_recompose,
1080 &invalidate_return_consumer,
1081 )
1082 };
1083
1084 let slot_origin = hot_slot_origin(&core_path, &func.sig.ident, body_end);
1085 let recompose_fn = quote! {
1086 #[allow(non_snake_case)]
1087 fn #recompose_fn_ident #impl_generics (
1088 #composer_ident: &#core_path::Composer
1089 ) -> #return_ty #where_clause {
1090 #slot_origin
1091 #recompose_fn_body
1092 }
1093 };
1094
1095 let helper_fn = quote! {
1096 #[allow(non_snake_case, clippy::too_many_arguments)]
1097 fn #helper_ident #impl_generics (
1098 #composer_ident: &#core_path::Composer,
1099 #group_ident: &#core_path::ComposableGroup<'_>
1100 #(, #helper_inputs)*
1101 ) -> #return_ty #where_clause {
1102 #slot_origin
1103 let #current_scope_ident = #group_ident.scope();
1104 #helper_body
1105 }
1106 };
1107
1108 let body_fn = quote! {
1109 #[allow(non_snake_case, clippy::too_many_arguments)]
1110 fn #body_ident #impl_generics (
1111 #composer_ident: &#core_path::Composer
1112 #(, #body_inputs)*
1113 ) -> #return_ty #where_clause {
1114 #(#callback_rebinds)*
1115 let _cranpose_source_scope = #core_path::__source_scope(
1116 stringify!(#scope_label_ident), file!(), line!(), env!("CARGO_MANIFEST_DIR"),
1117 );
1118 #original_block
1119 }
1120 };
1121
1122 let wrapper_args: Vec<TokenStream2> = param_info
1123 .iter()
1124 .zip(¶m_erased)
1125 .filter_map(|(info, erased)| {
1126 if info.is_impl_trait && !is_zero_arg_fn_impl_trait(&info.ty) {
1127 None
1128 } else if *erased {
1129 let ident = &info.ident;
1130 Some(quote! { ::std::boxed::Box::new(#ident) })
1131 } else {
1132 let ident = &info.ident;
1133 Some(quote! { #ident })
1134 }
1135 })
1136 .collect();
1137
1138 let wrapped = quote!({
1139 #caller_key_stmt
1140 let #composer_ident = #core_path::__current_composer();
1141 let #group_ident = #composer_ident.__open_composable_group(#key_expr);
1142 let #result_ident = #helper_ident(
1143 &#composer_ident,
1144 &#group_ident
1145 #(, #wrapper_args)*
1146 );
1147 #group_ident.close();
1148 #result_ident
1149 });
1150 *func.block = syn::parse2(wrapped).expect("failed to build block");
1151 TokenStream::from(quote! {
1152 #body_fn
1153 #recompose_fn
1154 #helper_fn
1155 #func
1156 })
1157 } else {
1158 let wrapped = quote!({
1159 #caller_key_stmt
1160 #core_path::with_current_composer(|#outer_composer_ident: &#core_path::Composer| {
1161 #outer_composer_ident.with_group(#key_expr, |#composer_ident: &#core_path::Composer| {
1162 #core_path::debug_label_current_scope(stringify!(#scope_label_ident));
1163 #(#rebinds_for_no_skip)*
1164 let _cranpose_source_scope = #core_path::__source_scope(
1165 stringify!(#scope_label_ident), file!(), line!(), env!("CARGO_MANIFEST_DIR"),
1166 );
1167 #original_block
1168 })
1169 })
1170 });
1171 *func.block = syn::parse2(wrapped).expect("failed to build block");
1172 TokenStream::from(quote! { #func })
1173 }
1174}
1175
1176fn inject_groups(core_path: &TokenStream2, func: &mut ItemFn) {
1179 let hot_reload = cfg!(feature = "hot-reload");
1180 branch_groups::inject_branch_groups_with(
1181 core_path,
1182 &mut func.block,
1183 &func.sig.ident,
1184 hot_reload,
1185 );
1186 if hot_reload {
1187 let end = func.block.brace_token.span.close();
1188 let origin = branch_groups::hot_origin_stmt(core_path, &func.sig.ident, end);
1189 func.block
1190 .stmts
1191 .insert(0, syn::parse2(origin).expect("hot origin statement"));
1192 }
1193}
1194
1195fn hot_slot_origin(core_path: &TokenStream2, name: &Ident, end: Span) -> TokenStream2 {
1199 if cfg!(feature = "hot-reload") {
1200 branch_groups::hot_origin_stmt(core_path, name, end)
1201 } else {
1202 TokenStream2::new()
1203 }
1204}
1205
1206fn find_reserved_pattern_ident(pat: &Pat) -> Option<&Ident> {
1207 use syn::visit::Visit;
1208
1209 struct Scan<'ast> {
1210 found: Option<&'ast Ident>,
1211 }
1212 impl<'ast> syn::visit::Visit<'ast> for Scan<'ast> {
1213 fn visit_pat_ident(&mut self, node: &'ast syn::PatIdent) {
1214 if self.found.is_none() {
1215 let name = node.ident.to_string();
1216 if name == "__composer" || name.starts_with("__cranpose") {
1217 self.found = Some(&node.ident);
1218 }
1219 }
1220 syn::visit::visit_pat_ident(self, node);
1221 }
1222 }
1223 let mut scan = Scan { found: None };
1224 scan.visit_pat(pat);
1225 scan.found
1226}
1227
1228#[cfg(test)]
1229#[path = "tests/macros_tests.rs"]
1230mod tests;