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