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