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