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